Files
shaguabijia-app-server/app/models/inactivity.py
T
guke 40c99afd92 feat(welfare): 完成不活跃清零-邀请金不清 + 预警编排 + worker + admin口径统一
- 清零改两桶(金币 + 折算现金);邀请奖励金物理隔离、不清(产品红线),仅快照入审计
- 预警编排 select_warn_candidates / run_warn_once / run_once(分档 + streak 去重);
  notifier.warn 去掉 invite 参数(邀请金不清、不预警)
- 预警逐用户 try/except 隔离 + run_once 保证预警故障绝不阻塞清零(接真推送前必备)
- 每日 worker inactivity_reset_worker(文件锁 + 北京日守卫 + RUN_HOUR 门槛 + 总闸默认关)
  + main.py lifespan 接线
- admin 最近活跃口径改用共享 activity(移除 last_login_at、纳入 home_view 占位)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 17:32:52 +08:00

59 lines
3.1 KiB
Python

"""15 天不活跃清零相关表。
- inactivity_reset_log:每次清零一行,记清零前三桶余额快照 + 原因 + 判定时活跃时间/不活跃天数,
供纠纷排查(需求①)。清零同时另写 2 条钱包流水(金币 + 折算现金,biz_type=inactivity_reset),
资金流可逐笔回溯。**邀请现金是产品红线、不清零**,invite_cash_balance_cents_before 仅为清零时
仍保留的邀请现金快照(便于排查、非被清金额;见 wallet.CoinAccount 注释)。
- inactivity_notification_log:每次预警一行,记推送时余额快照 + 档位 + 通道 + 状态,
兼作"预警去重"依据(created_at > last_active)与"待推送"占位 outbox(v1 通道=log)。
append-only,不更新。user_id 只索引、不设外键(同 analytics_event,避免删用户级联/历史留痕)。
"""
from __future__ import annotations
from datetime import datetime
from sqlalchemy import DateTime, Integer, String, func
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base
class InactivityResetLog(Base):
__tablename__ = "inactivity_reset_log"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(Integer, index=True, nullable=False)
coin_balance_before: Mapped[int] = mapped_column(Integer, nullable=False)
cash_balance_cents_before: Mapped[int] = mapped_column(Integer, nullable=False)
invite_cash_balance_cents_before: Mapped[int] = mapped_column(Integer, nullable=False)
last_active_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
inactive_days: Mapped[int] = mapped_column(Integer, nullable=False)
reason: Mapped[str] = mapped_column(String(32), nullable=False)
reset_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), index=True, nullable=False
)
def __repr__(self) -> str: # pragma: no cover
return f"<InactivityResetLog id={self.id} user_id={self.user_id} coin={self.coin_balance_before}>"
class InactivityNotificationLog(Base):
__tablename__ = "inactivity_notification_log"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(Integer, index=True, nullable=False)
stage: Mapped[int] = mapped_column(Integer, nullable=False) # 提前天数档(如 7 / 2)
inactive_days: Mapped[int] = mapped_column(Integer, nullable=False)
coin_balance: Mapped[int] = mapped_column(Integer, nullable=False)
cash_balance_cents: Mapped[int] = mapped_column(Integer, nullable=False)
invite_cash_balance_cents: Mapped[int] = mapped_column(Integer, nullable=False)
channel: Mapped[str] = mapped_column(String(16), nullable=False) # log / jpush / sms
status: Mapped[str] = mapped_column(String(16), nullable=False) # placeholder / sent / failed
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), index=True, nullable=False
)
def __repr__(self) -> str: # pragma: no cover
return f"<InactivityNotificationLog id={self.id} user_id={self.user_id} stage={self.stage}>"