8d7b91219a
## 改动
解绑微信前若有**待审核(reviewing)**提现单,首次解绑被拦下返回二次确认,用户确认后带 `force=true` 才真解绑。
- `schemas`: `UnbindWechatRequest{force}` + `UnbindWechatResultOut{needs_confirm, message}`
- `repositories`: `has_reviewing_withdraw`(只看 reviewing)
- `api`: `unbind-wechat` 接受可选 body(兼容旧无 body 客户端);`bound` 返回真实绑定态
- `docs/api`: 同步协议
## 为什么只拦 reviewing
reviewing 单解绑后审核打款会回读空 openid → 自动退回现金(钱不丢、只为让用户知情);pending 转账已在途、解绑影响不到,不拦。
配套客户端见 shaguabijia-app-android 同名分支。
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: OuYingJun1024 <1034284404@qq.com>
Reviewed-on: #46
Co-authored-by: ouzhou <ouzhou@wonderable.ai>
Co-committed-by: ouzhou <ouzhou@wonderable.ai>
129 lines
4.7 KiB
Python
129 lines
4.7 KiB
Python
"""一次性任务 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
|