"""一次性任务 CRUD:列表状态 + 领奖。 领奖原子化:写 user_task(唯一约束防重复)+ 发金币,同一事务 commit。 任务列表 + 奖励来自 rewards.get_task_rewards(运营后台可改)。 """ from __future__ import annotations from dataclasses import dataclass from sqlalchemy import func, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from app.core import rewards from app.models.task import UserTask from app.models.wallet import CoinTransaction from app.repositories import wallet as crud_wallet def _notification_grant_times(db: Session, user_id: int) -> int: """打开消息提醒已领取次数 = 该 biz_type 的【正向】金币流水条数(可重复任务不写 user_task)。 限 amount>0 只数发放笔,避免日后出现该 biz_type 的回滚/冲正负向流水时把次数算大、减半档位错。 """ biz = f"task_{rewards.TASK_ENABLE_NOTIFICATION}" return db.execute( select(func.count()).select_from(CoinTransaction).where( CoinTransaction.user_id == user_id, CoinTransaction.biz_type == biz, CoinTransaction.amount > 0, ) ).scalar_one() or 0 class UnknownTaskError(Exception): """task_key 不在已知任务表里。""" class AlreadyClaimedError(Exception): """该任务已经领过奖了。""" @dataclass class TaskState: task_key: str coin: int claimed: bool def list_tasks(db: Session, user_id: int) -> list[TaskState]: """返回所有已知任务及其领取状态。 打开消息提醒是可重复任务:claimed 恒 False,coin = 下一次(减半后)的金额; 客户端按「通知权限是否开启」决定展不展示(开着就隐藏,不在这里判断)。 其余为一次性任务,user_task 唯一约束去重。 """ task_rewards = rewards.get_task_rewards(db) stmt = select(UserTask.task_key).where(UserTask.user_id == user_id) claimed_keys = set(db.execute(stmt).scalars().all()) result: list[TaskState] = [] for key, coin in task_rewards.items(): if key == rewards.TASK_ENABLE_NOTIFICATION: times = _notification_grant_times(db, user_id) exhausted = times >= rewards.notification_max_claims(coin) result.append( TaskState( task_key=key, coin=rewards.notification_reward(coin, times), claimed=exhausted, # 领满(减半到底)后置 True,客户端据此可隐藏 ) ) else: result.append(TaskState(task_key=key, coin=coin, claimed=key in claimed_keys)) return result def claim_task(db: Session, user_id: int, task_key: str) -> tuple[int, int]: """领取一次性任务奖励。返回 (发放金币, 领奖后余额)。 未知任务抛 UnknownTaskError;重复领取抛 AlreadyClaimedError。 """ task_rewards = rewards.get_task_rewards(db) if task_key not in task_rewards: raise UnknownTaskError # 打开消息提醒:可重复领取,每次减半。不写 user_task(不去重),已领次数用金币流水条数算; # ref_id 带序号(task_key:N)让流水可读、且 (user_id,biz_type,ref_id) 唯一索引能挡并发/连点 # 重复发放。减半到底(notification_max_claims)后不再可领。客户端只在通知权限关闭时展示+触发。 if task_key == rewards.TASK_ENABLE_NOTIFICATION: base = task_rewards[task_key] times = _notification_grant_times(db, user_id) if times >= rewards.notification_max_claims(base): raise AlreadyClaimedError coin = rewards.notification_reward(base, times) try: acc, _ = crud_wallet.grant_coins( db, user_id, coin, biz_type=f"task_{task_key}", ref_id=f"{task_key}:{times + 1}", ) db.commit() except IntegrityError as e: # 并发/连点:另一个请求已抢先发了同一序号(撞唯一索引)。回滚,按已领处理,不双发。 db.rollback() raise AlreadyClaimedError from e return coin, acc.coin_balance existing = db.execute( select(UserTask).where( UserTask.user_id == user_id, UserTask.task_key == task_key ) ).scalar_one_or_none() if existing is not None: raise AlreadyClaimedError coin = task_rewards[task_key] db.add( UserTask( user_id=user_id, task_key=task_key, status="completed", coin_awarded=coin, ) ) acc, _ = crud_wallet.grant_coins( db, user_id, coin, biz_type=f"task_{task_key}", ref_id=task_key ) db.commit() return coin, acc.coin_balance