Skip to content

Commit

Permalink
奖池管理界面“立即停止”功能 (#838)
Browse files Browse the repository at this point in the history
* Immediate stop action for pool admin.

Sets the end time to now and invokes run_lottery on all lottery pools that's selected.

* Add select_for_update lock in run_lottery

Try to avoid race conditions.

* Check pool end time >= now in terminate_pool
  • Loading branch information
Deophius authored Aug 31, 2024
1 parent 23235f7 commit df3027e
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 96 deletions.
190 changes: 95 additions & 95 deletions app/YQPoint_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,7 @@ def buy_random_pool(user: User, pool_id: str) -> Tuple[MESSAGECONTEXT, int, int]
return wrong(str(e)), -1, 2


@transaction.atomic
def run_lottery(pool_id: int):
"""
抽奖;更新PoolRecord表和PoolItem表;给所有参与者发送通知
Expand All @@ -564,105 +565,104 @@ def run_lottery(pool_id: int):
:type pool_id: int
"""
# 部分参考了course_utils.py的draw_lots函数
pool = Pool.objects.get(id=pool_id, type=Pool.Type.LOTTERY)
pool = Pool.objects.select_for_update().get(id=pool_id, type=Pool.Type.LOTTERY)
assert not PoolRecord.objects.filter( # 此时pool关联的所有records都应该是LOTTERING
pool=pool).exclude(status=PoolRecord.Status.LOTTERING).exists()
with transaction.atomic():
related_records = PoolRecord.objects.filter(
pool=pool, status=PoolRecord.Status.LOTTERING)
records_num = related_records.count()
if records_num == 0:
return

# 抽奖
record_ids_and_participant_ids = list(
related_records.values("id", "user__id"))
items = pool.items.all()
user2prize_names = {d["user__id"]: []
for d in record_ids_and_participant_ids} # 便于发通知
winner_record_id2item_id = {} # poolrecord.id: poolitem.id,便于更新poolrecord
loser_record_ids = [] # poolrecord.id,便于更新poolrecord
num_all_items = 0 # 该奖池中奖品总数
for item in items:
num_all_items += item.origin_num - item.consumed_num
if num_all_items >= records_num: # 抽奖记录数少于或等于奖品数,人人有奖,给每个记录分配一个随机奖品
shuffled_items = select_random_prize(
items, records_num) # 随机选出待发放的奖品
for i in range(records_num): # 遍历所有记录,每个记录都有奖品
user2prize_names[record_ids_and_participant_ids[i]["user__id"]].append(
items.get(id=shuffled_items[i]).prize.name
)
winner_record_id2item_id[record_ids_and_participant_ids[i]
["id"]] = shuffled_items[i]
else: # 抽奖记录数多于奖品数,给每个奖品分配一个中奖者
for item in items: # 遍历所有奖品,每个奖品都会送给一个记录
for i in range(item.origin_num - item.consumed_num):
winner_record_index = random.randint(
0, len(record_ids_and_participant_ids) - 1)
user2prize_names[record_ids_and_participant_ids[winner_record_index]["user__id"]].append(
item.prize.name)
winner_record_id2item_id[record_ids_and_participant_ids[winner_record_index]["id"]] = item.id
# 因为记录多,奖品少,这里肯定不会pop成空列表
record_ids_and_participant_ids.pop(winner_record_index)
# pop剩下的就是没中奖的那些记录
loser_record_ids = [d["id"]
for d in record_ids_and_participant_ids]

# 更新数据库
for winner_record_id, poolitem_id in winner_record_id2item_id.items():
record = PoolRecord.objects.select_for_update().get(id=winner_record_id)
item = PoolItem.objects.select_for_update().get(id=poolitem_id)
record.status = PoolRecord.Status.UN_REDEEM
record.prize = item.prize
record.time = datetime.now()
item.consumed_num += 1
record.save()
item.save()
for loser_record_id in loser_record_ids:
record = PoolRecord.objects.select_for_update().get(id=loser_record_id)
record.status = PoolRecord.Status.NOT_LUCKY
record.time = datetime.now()
record.save()

# 给中奖的同学发送通知
sender = Organization.objects.get(
oname=CONFIG.yqpoint.org_name).get_user()
for user_id in user2prize_names.keys():
receiver = User.objects.get(id=user_id)
typename = Notification.Type.NEEDREAD
title = Notification.Title.LOTTERY_INFORM
content = f"恭喜您在奖池【{pool.title}】中抽中奖品"
for prize_name in user2prize_names[user_id]:
content += f"【{prize_name}】" # 可能出现重复,即一种奖品中了好几次,不过感觉问题也不太大
notification_create(
receiver=receiver,
sender=sender,
typename=typename,
title=title,
content=content,
# URL=f'', # TODO: 我的奖品页面?
to_wechat=dict(app=WechatApp.TO_PARTICIPANT,
level=WechatMessageLevel.IMPORTANT),
related_records = PoolRecord.objects.filter(
pool=pool, status=PoolRecord.Status.LOTTERING)
records_num = related_records.count()
if records_num == 0:
return

# 抽奖
record_ids_and_participant_ids = list(
related_records.values("id", "user__id"))
items = pool.items.all()
user2prize_names = {d["user__id"]: []
for d in record_ids_and_participant_ids} # 便于发通知
winner_record_id2item_id = {} # poolrecord.id: poolitem.id,便于更新poolrecord
loser_record_ids = [] # poolrecord.id,便于更新poolrecord
num_all_items = 0 # 该奖池中奖品总数
for item in items:
num_all_items += item.origin_num - item.consumed_num
if num_all_items >= records_num: # 抽奖记录数少于或等于奖品数,人人有奖,给每个记录分配一个随机奖品
shuffled_items = select_random_prize(
items, records_num) # 随机选出待发放的奖品
for i in range(records_num): # 遍历所有记录,每个记录都有奖品
user2prize_names[record_ids_and_participant_ids[i]["user__id"]].append(
items.get(id=shuffled_items[i]).prize.name
)
winner_record_id2item_id[record_ids_and_participant_ids[i]
["id"]] = shuffled_items[i]
else: # 抽奖记录数多于奖品数,给每个奖品分配一个中奖者
for item in items: # 遍历所有奖品,每个奖品都会送给一个记录
for i in range(item.origin_num - item.consumed_num):
winner_record_index = random.randint(
0, len(record_ids_and_participant_ids) - 1)
user2prize_names[record_ids_and_participant_ids[winner_record_index]["user__id"]].append(
item.prize.name)
winner_record_id2item_id[record_ids_and_participant_ids[winner_record_index]["id"]] = item.id
# 因为记录多,奖品少,这里肯定不会pop成空列表
record_ids_and_participant_ids.pop(winner_record_index)
# pop剩下的就是没中奖的那些记录
loser_record_ids = [d["id"]
for d in record_ids_and_participant_ids]

# 更新数据库
for winner_record_id, poolitem_id in winner_record_id2item_id.items():
record = PoolRecord.objects.select_for_update().get(id=winner_record_id)
item = PoolItem.objects.select_for_update().get(id=poolitem_id)
record.status = PoolRecord.Status.UN_REDEEM
record.prize = item.prize
record.time = datetime.now()
item.consumed_num += 1
record.save()
item.save()
for loser_record_id in loser_record_ids:
record = PoolRecord.objects.select_for_update().get(id=loser_record_id)
record.status = PoolRecord.Status.NOT_LUCKY
record.time = datetime.now()
record.save()

# 给中奖的同学发送通知
sender = Organization.objects.get(
oname=CONFIG.yqpoint.org_name).get_user()
for user_id in user2prize_names.keys():
receiver = User.objects.get(id=user_id)
typename = Notification.Type.NEEDREAD
title = Notification.Title.LOTTERY_INFORM
content = f"恭喜您在奖池【{pool.title}】中抽中奖品"
for prize_name in user2prize_names[user_id]:
content += f"【{prize_name}】" # 可能出现重复,即一种奖品中了好几次,不过感觉问题也不太大
notification_create(
receiver=receiver,
sender=sender,
typename=typename,
title=title,
content=content,
# URL=f'', # TODO: 我的奖品页面?
to_wechat=dict(app=WechatApp.TO_PARTICIPANT,
level=WechatMessageLevel.IMPORTANT),
)

# 给没中奖的同学发送通知
receivers = PoolRecord.objects.filter(
id__in=loser_record_ids,
).values_list("user", flat=True)
receivers = User.objects.filter(id__in=receivers)
content = f"很抱歉通知您,您在奖池【{pool.title}】中没有中奖"

if len(receivers) > 0:
bulk_notification_create(
receivers=receivers,
sender=sender,
typename=typename,
title=title,
content=content,
# URL=f'', # TODO: 我的奖品页面?
to_wechat=dict(app=WechatApp.TO_PARTICIPANT,
level=WechatMessageLevel.IMPORTANT),
)
# 给没中奖的同学发送通知
receivers = PoolRecord.objects.filter(
id__in=loser_record_ids,
).values_list("user", flat=True)
receivers = User.objects.filter(id__in=receivers)
content = f"很抱歉通知您,您在奖池【{pool.title}】中没有中奖"

if len(receivers) > 0:
bulk_notification_create(
receivers=receivers,
sender=sender,
typename=typename,
title=title,
content=content,
# URL=f'', # TODO: 我的奖品页面?
to_wechat=dict(app=WechatApp.TO_PARTICIPANT,
level=WechatMessageLevel.IMPORTANT),
)


def get_income_expenditure(
Expand Down
13 changes: 12 additions & 1 deletion app/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from utils.admin_utils import *
from app.models import *
from scheduler.cancel import remove_job

from app.YQPoint_utils import run_lottery

# 通用内联模型
@readonly_inline
Expand Down Expand Up @@ -808,6 +808,17 @@ class PrizeAdmin(admin.ModelAdmin):
@admin.register(Pool)
class PoolAdmin(admin.ModelAdmin):
inlines = [PoolItemInline]
actions = []

@as_action('立即结束', actions, 'change', update = True)
def terminate_pool(self, request, queryset: QuerySet['Pool']):
if queryset.filter(end__isnull = False, end__lt = datetime.now()).exists():
raise ValueError('请不要在已结束的奖池上调用!')
queryset.update(end = datetime.now())
# Immediately get the results of the lottery pools
lottery_pool_ids = list(queryset.filter(type = Pool.Type.LOTTERY).values_list('id', flat = True))
for pool_id in lottery_pool_ids:
run_lottery(pool_id)


@admin.register(PoolRecord)
Expand Down

0 comments on commit df3027e

Please sign in to comment.