73970087ff
Co-authored-by: guke <guke@wonderable.ai> Co-authored-by: 左辰勇 <exinglang@gmail.com> Reviewed-on: #154 Co-authored-by: zuochenyong <zuochenyong@wonderable.ai> Co-committed-by: zuochenyong <zuochenyong@wonderable.ai>
131 lines
4.3 KiB
Python
131 lines
4.3 KiB
Python
"""签到 CRUD:状态查询 + 执行签到。
|
|
|
|
7 天循环规则(2026-06 由 14 天改 7 天一轮,周期长度统一取 SIGNIN_CYCLE_LEN):
|
|
- 昨天签过 → 今天 cycle_day = 昨天 % SIGNIN_CYCLE_LEN + 1,streak += 1(连续)
|
|
- 否则(首签 / 断签) → cycle_day = 1,streak = 1(重置)
|
|
今天的 cycle_day 决定发多少金币(档位来自 rewards.get_signin_rewards,运营后台可改)。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from datetime import timedelta
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core import rewards
|
|
from app.core.rewards import SIGNIN_CYCLE_LEN, cn_today
|
|
from app.models.signin import SigninRecord
|
|
from app.repositories import wallet as crud_wallet
|
|
|
|
|
|
class AlreadySignedError(Exception):
|
|
"""今天已经签过了。"""
|
|
|
|
|
|
@dataclass
|
|
class SigninStep:
|
|
day: int # 1..14
|
|
coin: int
|
|
status: str # claimed / today / locked
|
|
|
|
|
|
@dataclass
|
|
class SigninStatus:
|
|
today_signed: bool
|
|
consecutive_days: int # 当前已确认的连续签到天数
|
|
today_cycle_day: int # 今天落在循环的第几档(1..14)
|
|
today_coin: int # 今天这一档的金币
|
|
can_claim: bool # 现在能否签到(= not today_signed)
|
|
steps: list[SigninStep]
|
|
|
|
|
|
def _latest_record(db: Session, user_id: int) -> SigninRecord | None:
|
|
stmt = (
|
|
select(SigninRecord)
|
|
.where(SigninRecord.user_id == user_id)
|
|
.order_by(SigninRecord.signin_date.desc())
|
|
.limit(1)
|
|
)
|
|
return db.execute(stmt).scalar_one_or_none()
|
|
|
|
|
|
def _next_cycle_day(last: SigninRecord | None, today) -> int:
|
|
"""若现在签到,今天会落在第几档(1..14)。"""
|
|
if last is not None and last.signin_date == today - timedelta(days=1):
|
|
return last.cycle_day % SIGNIN_CYCLE_LEN + 1
|
|
return 1
|
|
|
|
|
|
def get_status(db: Session, user_id: int) -> SigninStatus:
|
|
today = cn_today()
|
|
rewards_list = rewards.get_signin_rewards(db) # 配置 or 默认(长度=SIGNIN_CYCLE_LEN)
|
|
last = _latest_record(db, user_id)
|
|
today_signed = last is not None and last.signin_date == today
|
|
|
|
if today_signed:
|
|
# 14→7 改制遗留:老记录 cycle_day 可能 >SIGNIN_CYCLE_LEN,归一化到 1..LEN 防 rewards_list 越界
|
|
today_cycle_day = (last.cycle_day - 1) % SIGNIN_CYCLE_LEN + 1
|
|
consecutive_days = last.streak
|
|
else:
|
|
today_cycle_day = _next_cycle_day(last, today)
|
|
# 未签时的"已连续"= 还在连续窗口内的既有 streak,否则 0
|
|
if last is not None and last.signin_date == today - timedelta(days=1):
|
|
consecutive_days = last.streak
|
|
else:
|
|
consecutive_days = 0
|
|
|
|
steps: list[SigninStep] = []
|
|
for day in range(1, SIGNIN_CYCLE_LEN + 1):
|
|
if today_signed:
|
|
status = "claimed" if day <= today_cycle_day else "locked"
|
|
else:
|
|
if day < today_cycle_day:
|
|
status = "claimed"
|
|
elif day == today_cycle_day:
|
|
status = "today"
|
|
else:
|
|
status = "locked"
|
|
steps.append(SigninStep(day=day, coin=rewards_list[day - 1], status=status))
|
|
|
|
return SigninStatus(
|
|
today_signed=today_signed,
|
|
consecutive_days=consecutive_days,
|
|
today_cycle_day=today_cycle_day,
|
|
today_coin=rewards_list[today_cycle_day - 1],
|
|
can_claim=not today_signed,
|
|
steps=steps,
|
|
)
|
|
|
|
|
|
def do_signin(db: Session, user_id: int) -> tuple[SigninRecord, int]:
|
|
"""执行签到。返回 (签到记录, 签到后金币余额)。今天已签则抛 AlreadySignedError。"""
|
|
today = cn_today()
|
|
last = _latest_record(db, user_id)
|
|
if last is not None and last.signin_date == today:
|
|
raise AlreadySignedError
|
|
|
|
if last is not None and last.signin_date == today - timedelta(days=1):
|
|
cycle_day = last.cycle_day % SIGNIN_CYCLE_LEN + 1
|
|
streak = last.streak + 1
|
|
else:
|
|
cycle_day = 1
|
|
streak = 1
|
|
|
|
coin = rewards.get_signin_rewards(db)[cycle_day - 1]
|
|
record = SigninRecord(
|
|
user_id=user_id,
|
|
signin_date=today,
|
|
cycle_day=cycle_day,
|
|
streak=streak,
|
|
coin_awarded=coin,
|
|
)
|
|
db.add(record)
|
|
acc, _ = crud_wallet.grant_coins(
|
|
db, user_id, coin, biz_type="signin", ref_id=today.isoformat(),
|
|
remark=f"每日签到 第{cycle_day}天",
|
|
)
|
|
db.commit()
|
|
db.refresh(record)
|
|
return record, acc.coin_balance
|