Files
shaguabijia-app-server/docs/database/inactivity_notification_log.md
T
guke a86688ccfb feat(welfare): 15 天不活跃清零(金币+折算现金,邀请金不清)+ 预警 + admin 活跃口径统一 (#143)
背景 / 目标
连续 15 天(北京自然日)没有「首页可见 / 发起比价 / 发起领券」行为的用户判定为流失,每日自动清零其金币 + 折算现金;清零前按可配置节奏预警;全程留审计。纯登录不算活跃;邀请奖励金物理隔离、不清(产品红线)。

改了什么
活跃口径共享模块 app/repositories/activity.py:worker(清零/预警)与 admin(最近活跃)单一真源,防漂移。
清零业务逻辑 inactivity.py:选取 / 逐用户清零(行锁 + 幂等)/ 预警分档 + streak 去重 / run_once 组合,预警故障逐用户隔离、绝不阻塞清零。
每日 worker inactivity_reset_worker.py(仿 daily_exchange:文件锁 + 北京日守卫 + RUN_HOUR 门槛 + 总闸)+ main.py lifespan 接线。
可插拔通知器 notifier.py(v1 LogNotifier 日志占位,预留 JPush/短信)。
配置 INACTIVITY_*(阈值 / 预警档 / 执行点 / 通道 / 开关)。
admin 最近活跃口径改用共享模块(移除 last_login_at、纳入 show/home、以 created_at 为基线)。
审计:inactivity_reset_log + inactivity_notification_log 两表 + 钱包流水双写(biz_type=inactivity_reset,ref_id 交叉)。
文档:设计 spec / 实现 plan / docs/database/ 两表字典。
关键产品决策
邀请金不清:只清金币 + 折算现金,invite_cash_balance_cents 原封(仅快照入审计)。
活跃口径 = max(created_at, 首页可见, 比价, 领券),不含 last_login_at;首页可见 = event=show + page=home。
时间边界:北京自然日 0 点对齐(见 activity.reset_cutoff)。
总闸默认关,灰度验证后再开。
数据库变更
新表:inactivity_reset_log、inactivity_notification_log。
analytics_event 新增复合覆盖索引 ix_analytics_event_active (event, page, user_id, created_at)(活跃口径聚合热点)。
修复了 base 上的迁移多头(135e79414fd0 与 phone_rebind_log 同从 comparison_llm_cost 分叉)→ 加空 merge 修订,alembic upgrade head 恢复单头正常。
⚠️ 上线注意(合并后 / 开总闸前)
INACTIVITY_RESET_ENABLED 默认 false;开启前提 = show/home 埋点全量铺满——否则"只登录不操作"且注册满 15 天的老用户会落到 created_at 基线被误清。
前端依赖:Android 端需在首页可见上报 event=show + page=home(携带登录后的 user_id)。
admin「最近活跃」口径变化(去登录、纳入 home_view、created_at 基线):属预期变化,需产品/运营知会;与 DAU(_period_active_user_ids,仍含登录)是两套指标。
清零对 C 端「金币流水」可见(biz_type=inactivity_reset,备注「15天不活跃清零」)。
灰度:先只看预警/清零名单对不对,再开总闸。

---------

Co-authored-by: guke <guke@autohome.com.cn>
Reviewed-on: #143
2026-07-18 19:11:45 +08:00

3.1 KiB

inactivity_notification_log — 不活跃清零前预警记录

模型 app/models/inactivity.py · 仓库 app/repositories/inactivity.py · 通知器 app/integrations/notifier.py · ← 索引 · 总览

清零前按可配置节奏(INACTIVITY_WARN_DAYS_BEFORE,默认清零前 7 天、2 天各一次)向用户预警"账户里的 xx 金币和 xx 现金将被清零"。每发一次预警写一行,记推送时的余额快照 + 提前天数档 + 通道 + 状态。兼作两用:预警去重依据(同 streak 内 stage==k 且 created_at > last_active 即已推过、不重推)与占位 outbox(v1 通道=log,只打日志不真推;后续接 JPush/短信同层扩展)。append-only,不更新。预警只涉及会被清的金币 + 折算现金;邀请奖励金不清、不预警(invite_cash_balance_cents 仅作账户状态快照)。

用在哪 / 增删改查

  • C(插入):inactivity.run_warn_once 命中预警档、且本 streak 未推过时,调 notifier.warn 后写一行(status = 通知器返回,占位实现为 placeholder)。
  • U / D:无(append-only)。
  • R:预警去重查询(user_id + stage + created_at > last_active);未来接真实推送时作待推送 outbox。

字段

类型 约束 / 默认 说明(取值 / join)
id Integer PK, autoincrement 主键
user_id Integer NOT NULL, index 预警对象;只索引不设外键(同 analytics_event)
stage Integer NOT NULL 提前天数档(如 7 / 2,即清零前第几天推)
inactive_days Integer NOT NULL 推送时的不活跃天数(北京自然日)
coin_balance Integer NOT NULL 推送时金币余额快照(将被清)
cash_balance_cents Integer NOT NULL 推送时折算现金余额快照(分,将被清)
invite_cash_balance_cents Integer NOT NULL 推送时邀请奖励金余额快照(分,不清、不在预警额度内)
channel String(16) NOT NULL 通道:log(占位) / jpush / sms
status String(16) NOT NULL 状态:placeholder(占位未真推) / sent / failed
created_at DateTime(tz) server_default now(), index 推送时刻;去重比 created_at > last_active(用户回归后 last_active 前移 → 旧行自然失效、开启新 streak)

关系 / Join Key

  • user_iduser.id(无外键直连,靠 user_id 关联)。
  • inactivity_reset_log 无直接外键;同一 streak 内先有若干预警行,到期后有一行清零。

索引与约束

  • PK id;ix_inactivity_notification_log_user_idix_inactivity_notification_log_created_at

注意

  • 预警去重按 streak:判据是 created_at > last_active;用户一有活跃(home_view/比价/领券),last_active 前移,旧预警行"失效",回归后可重新进入预警。
  • 占位实现:v1 LogNotifierlogger.warning("[inactivity-warn] ...")、返回 placeholder,不真推(参照心跳告警"本期先不接推送"先例)。
  • 漏跑补发:worker 漏跑数天后某用户可能同时满足多档,只补发最紧急的未推档(最小提前天数),避免刷屏。