Files
shaguabijia-app-server/app/repositories/signin.py
T
2026-07-22 10:53:21 +08:00

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