Skip to content

Commit

Permalink
活动限定奖池 (#837)
Browse files Browse the repository at this point in the history
* Add activity foreign key in `Pool`

Make the exchange attributes optional

Commit migration file

* Checks if user participates in required activity

* Refactor out active, activity and time checks

* Set blank flags on pool item and record attributes

Removed the NULL flag because we have a non-null default value.

* Check activity status when making frontend dict

So if the user didn't take part in the required activity, the pool won't
show up in the `showPools` page.

* Fix model definitions

Use class instead of string when specifying foreign key; add None as
default for Pool.activity
  • Loading branch information
Deophius authored Aug 31, 2024
1 parent 80dd236 commit 23235f7
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 26 deletions.
81 changes: 57 additions & 24 deletions app/YQPoint_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import List, Dict, Optional, Tuple
from datetime import datetime, timedelta, date

from django.db.models import QuerySet, Q
from django.db.models import QuerySet, Q, Exists, OuterRef
from django.forms.models import model_to_dict

from generic.models import User, YQPointRecord
Expand All @@ -14,6 +14,7 @@
PoolRecord,
Notification,
Organization,
Participation,
)
from app.extern.wechat import WechatApp, WechatMessageLevel
from app.notification_utils import bulk_notification_create, notification_create
Expand Down Expand Up @@ -136,7 +137,7 @@ def add_signin_point(user: User):

def get_pools_and_items(pool_type: Pool.Type, user: User, frontend_dict: Dict[str, any]):
"""
获取某一种类的所有当前开放的pool的前端所需信息
获取某一种类的所有当前开放的pool的前端所需信息。如果用户未参加奖池关联的活动,这个奖池的信息不会被返回。
:param pool_type: pool种类
:type pool_type: Pool.Type
Expand All @@ -145,9 +146,18 @@ def get_pools_and_items(pool_type: Pool.Type, user: User, frontend_dict: Dict[st
:param frontend_dict: 前端字典
:type frontend_dict: Dict[str, any]
"""
assert hasattr(user, 'naturalperson'), "非个人用户发起了奖池兑换请求"
pools = Pool.objects.filter(
Q(type=pool_type) & Q(start__lte=datetime.now())
& (Q(end__isnull=True) | Q(end__gte=datetime.now() - timedelta(days=1))))
& (Q(end__isnull=True) | Q(end__gte=datetime.now() - timedelta(days=1)))
& (Q(activity__isnull=True) | Exists(
Participation.objects.filter(
activity = OuterRef('activity'),
person = user.naturalperson,
status = Participation.AttendStatus.ATTENDED,
)
))
)

pools_info = []
# 此列表中含有若干dict,每个dict对应一个待展示的pool,例如:
Expand Down Expand Up @@ -258,6 +268,41 @@ def get_pools_and_items(pool_type: Pool.Type, user: User, frontend_dict: Dict[st
frontend_dict["pools_info"] = pools_info


def check_user_pool(user: User, pool: Pool) -> None | str:
"""
检查用户 user 是否已经毕业,以及是否参加了 pool 所关联的活动;当前是否在奖池的运行时间内。
:param user: 当前用户
:type user: User
:param pool: 待购买的物品所属奖池
:type pool: Pool
:return: 如果检查通过,返回None。否则,返回一个可以作为 wrong() 函数参数的字符串。
:rtype: None | str
"""
# 检查奖池运行时间
if pool.start > datetime.now():
return '当前奖池时间未开始!'
if pool.end is not None and pool.end < datetime.now():
return '当前奖池时间已结束!'

# 检查用户是否已经毕业
if not user.active:
return '您已毕业!'

# 检查用户是否参加了相关的活动
if pool.activity is not None:
assert hasattr(user, 'naturalperson'), "非个人用户发起了奖池兑换请求"
participates = Participation.objects.filter(
activity = pool.activity,
person = user.naturalperson,
status = Participation.AttendStatus.ATTENDED,
)
if not participates.exists():
return '该奖池为"' + str(pool.activity) + '"活动限定奖池,请先参加再来购买!'

return None


def buy_exchange_item(
user: User, poolitem_id: str,
attributes: dict[str, str] = {}) -> MESSAGECONTEXT:
Expand All @@ -278,15 +323,11 @@ def buy_exchange_item(
id=poolitem_id, pool__type=Pool.Type.EXCHANGE)
except:
return wrong('奖品不存在!')
if poolitem.pool.start > datetime.now():
return wrong('兑换时间未开始!')
if poolitem.pool.end is not None and poolitem.pool.end < datetime.now():
return wrong('兑换时间已结束!')
if poolitem.origin_num - poolitem.consumed_num <= 0:
return wrong('奖品已售罄!')
# 检查用户是否已经毕业
if not user.active:
return wrong('您已毕业!')
other_errors = check_user_pool(user, poolitem.pool)
if other_errors:
return wrong(other_errors)

my_exchanged_time = PoolRecord.objects.filter(
user=user, pool=poolitem.pool, prize=poolitem.prize).count()
Expand Down Expand Up @@ -354,16 +395,12 @@ def buy_lottery_pool(user: User, pool_id: str) -> MESSAGECONTEXT:
pool = Pool.objects.get(id=pool_id, type=Pool.Type.LOTTERY)
except:
return wrong('抽奖不存在!')
if pool.start > datetime.now():
return wrong('抽奖未开始!')
if pool.end is not None and pool.end < datetime.now(): # 实际上抽奖类的奖池的end应该不可能是None
return wrong('抽奖已结束!')
my_entry_time = PoolRecord.objects.filter(pool=pool, user=user).count()
if my_entry_time >= pool.entry_time:
return wrong('您在本奖池中抽奖的次数已达上限!')
# 检查用户是否已经毕业
if not user.active:
return wrong('您已毕业!')
other_errors = check_user_pool(user, pool)
if other_errors:
return wrong(other_errors)

try:
with transaction.atomic():
Expand Down Expand Up @@ -450,20 +487,16 @@ def buy_random_pool(user: User, pool_id: str) -> Tuple[MESSAGECONTEXT, int, int]
pool = Pool.objects.get(id=pool_id, type=Pool.Type.RANDOM)
except:
return wrong('盲盒不存在!'), -1, 2
if pool.start > datetime.now():
return wrong('盲盒兑换时间未开始!'), -1, 2
if pool.end is not None and pool.end < datetime.now():
return wrong('盲盒兑换时间已结束!'), -1, 2
my_entry_time = PoolRecord.objects.filter(pool=pool, user=user).count()
if my_entry_time >= pool.entry_time:
return wrong('您兑换这款盲盒的次数已达上限!'), -1, 2
total_entry_time = PoolRecord.objects.filter(pool=pool).count()
capacity = pool.get_capacity()
if capacity <= total_entry_time:
return wrong('盲盒已售罄!'), -1, 2
# 检查用户是否已经毕业
if not user.active:
return wrong('您已毕业!')
other_errors = check_user_pool(user, pool)
if other_errors:
return wrong(other_errors), -1, 2

try:
with transaction.atomic():
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 5.0.1 on 2024-08-31 15:14

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("app", "0007_poolitem_exchange_attributes_poolrecord_attributes"),
]

operations = [
migrations.AddField(
model_name="pool",
name="activity",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="app.activity",
),
),
migrations.AlterField(
model_name="poolitem",
name="exchange_attributes",
field=models.JSONField(blank=True, default=list, verbose_name="属性"),
),
migrations.AlterField(
model_name="poolrecord",
name="attributes",
field=models.JSONField(blank=True, default=dict, verbose_name="属性"),
),
]
7 changes: 5 additions & 2 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1829,6 +1829,9 @@ class Type(models.TextChoices):
redeem_start = models.DateTimeField(
'兑奖开始时间', null=True, blank=True) # 指线下获取奖品实物
redeem_end = models.DateTimeField('兑奖结束时间', null=True, blank=True)
# 如果activity非空,只有参加了该活动的用户可以参与这个奖池的兑换。
activity = models.ForeignKey(Activity, on_delete=models.SET_NULL,
null=True, blank=True, default=None)

@invalid_for_frontend
def __str__(self):
Expand Down Expand Up @@ -1856,7 +1859,7 @@ class Meta:
# 下面三个在 pool 类型为兑换奖池时有效
exchange_limit = models.IntegerField('单人兑换上限', default=0)
exchange_price = models.IntegerField('价格', null=True, blank=True)
exchange_attributes = models.JSONField('属性', default=list)
exchange_attributes = models.JSONField('属性', default=list, blank=True)
# 下面这个在抽奖/盲盒奖池中有效
is_big_prize = models.BooleanField('是否特别奖品', default=False)
is_empty_prize = models.BooleanField('是否空盒', default=False)
Expand Down Expand Up @@ -1891,7 +1894,7 @@ class Status(models.TextChoices):
Prize, verbose_name='奖品', on_delete=models.CASCADE,
null=True, blank=True,
)
attributes = models.JSONField('属性', default=dict)
attributes = models.JSONField('属性', default=dict, blank=True)
status = models.CharField('状态', choices=Status.choices, max_length=15)
time = models.DateTimeField('记录时间', auto_now_add=True)
redeem_time = models.DateTimeField('兑奖时间', null=True, blank=True)
Expand Down

0 comments on commit 23235f7

Please sign in to comment.