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
This commit was merged in pull request #143.
This commit is contained in:
@@ -42,6 +42,8 @@
|
||||
| `ad_ecpm_record` | 广告展示 eCPM 上报(收益对账) | `models/ad_ecpm.py` | [详情](./ad_ecpm_record.md) |
|
||||
| `ad_feed_reward_record` | 信息流/Draw 广告结算记录(10 秒一份,client_event_id 幂等;`ad_type`+`feed_scene` 分形态/场景) | `models/ad_feed_reward.py` | [详情](./ad_feed_reward_record.md) |
|
||||
| `ad_pangle_daily_revenue` | 穿山甲 GroMore 后台收益日表(定时拉取,收益报表/大盘真实收益源,#92) | `models/ad_pangle_revenue.py` | [详情](./ad_pangle_daily_revenue.md) |
|
||||
| `inactivity_reset_log` | 15 天不活跃清零审计(每次清零一行;清零前三桶余额快照+原因+不活跃天数;只清金币+现金,邀请金仅快照) | `models/inactivity.py` | [详情](./inactivity_reset_log.md) |
|
||||
| `inactivity_notification_log` | 不活跃清零前预警记录(余额快照+档位+通道+状态;streak 去重依据 + 占位 outbox) | `models/inactivity.py` | [详情](./inactivity_notification_log.md) |
|
||||
|
||||
### 比价 / 省钱
|
||||
| 表 | 用途 | 模型 | 文档 |
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# inactivity_notification_log — 不活跃清零前预警记录
|
||||
|
||||
> 模型 `app/models/inactivity.py` · 仓库 `app/repositories/inactivity.py` · 通知器 `app/integrations/notifier.py` · [← 索引](./README.md) · [总览](./OVERVIEW.md)
|
||||
|
||||
清零前按可配置节奏(`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_id` → `user.id`(无外键直连,靠 `user_id` 关联)。
|
||||
- 与 `inactivity_reset_log` 无直接外键;同一 streak 内先有若干预警行,到期后有一行清零。
|
||||
|
||||
## 索引与约束
|
||||
- PK `id`;`ix_inactivity_notification_log_user_id`、`ix_inactivity_notification_log_created_at`。
|
||||
|
||||
## 注意
|
||||
- **预警去重按 streak**:判据是 `created_at > last_active`;用户一有活跃(`home_view`/比价/领券),`last_active` 前移,旧预警行"失效",回归后可重新进入预警。
|
||||
- **占位实现**:v1 `LogNotifier` 只 `logger.warning("[inactivity-warn] ...")`、返回 `placeholder`,不真推(参照心跳告警"本期先不接推送"先例)。
|
||||
- **漏跑补发**:worker 漏跑数天后某用户可能同时满足多档,只补发**最紧急的未推档**(最小提前天数),避免刷屏。
|
||||
@@ -0,0 +1,35 @@
|
||||
# inactivity_reset_log — 15 天不活跃清零审计
|
||||
|
||||
> 模型 `app/models/inactivity.py` · 仓库 `app/repositories/inactivity.py` · worker `app/core/inactivity_reset_worker.py` · [← 索引](./README.md) · [总览](./OVERVIEW.md)
|
||||
|
||||
连续 15 天不活跃(北京自然日,活跃口径见 `app/repositories/activity.py`:`home_view` + 发起比价 + 发起领券,**不含登录**)的用户,worker 每日自动清零其**金币 + 折算现金**。每清一个用户写一行,记清零前三桶余额快照 + 原因 + 判定时的活跃时间/不活跃天数,供纠纷排查。清零同时另写 2 条钱包流水(`coin_transaction` / `cash_transaction`,`biz_type=inactivity_reset`,`ref_id=` 本表 `id`),资金流可逐笔回溯、人工恢复。**邀请奖励金(`invite_cash_balance_cents`)是产品红线、不清零**,本表 `invite_cash_balance_cents_before` 仅为清零时仍保留的邀请金快照(非被清金额)。append-only,不更新。
|
||||
|
||||
## 用在哪 / 增删改查
|
||||
- **C(插入)**:`inactivity.clear_user` 逐用户清零(独立事务、行锁)时写一行,`db.flush()` 拿 `id` 作流水 `ref_id` 交叉链接。
|
||||
- **U / D**:无(append-only 审计)。
|
||||
- **R**:纠纷排查 / 对账(与 `coin_transaction` / `cash_transaction` 的 `ref_id` 交叉核对)。
|
||||
|
||||
## 字段
|
||||
| 列 | 类型 | 约束 / 默认 | 说明(取值 / join) |
|
||||
|---|---|---|---|
|
||||
| `id` | Integer | **PK**, autoincrement | 主键;作 `ref_id` 写入两条清零流水 |
|
||||
| `user_id` | Integer | NOT NULL, index | 被清零用户;只索引不设外键(同 `analytics_event`,避免删用户级联 / 留历史) |
|
||||
| `coin_balance_before` | Integer | NOT NULL | 清零前金币余额(个数);= 对应 `coin_transaction.amount` 绝对值 |
|
||||
| `cash_balance_cents_before` | Integer | NOT NULL | 清零前折算现金余额(分);= 对应 `cash_transaction.amount_cents` 绝对值 |
|
||||
| `invite_cash_balance_cents_before` | Integer | NOT NULL | 清零时的**邀请奖励金**余额快照(分)——**不清、原封保留**,仅记录以证明"未动邀请金" |
|
||||
| `last_active_at` | DateTime(tz) | nullable | 判定时的最近活跃时刻(UTC);无任何活跃信号时兜底为 `user.created_at` |
|
||||
| `inactive_days` | Integer | NOT NULL | 判定时的不活跃天数(北京自然日) |
|
||||
| `reason` | String(32) | NOT NULL | 清零原因,如 `inactive_15d` |
|
||||
| `reset_at` | DateTime(tz) | server_default now(), index | 清零时刻 |
|
||||
|
||||
## 关系 / Join Key
|
||||
- `user_id` → `user.id`(无外键直连,靠 `user_id` 关联)。
|
||||
- `id` → `coin_transaction.ref_id` / `cash_transaction.ref_id`(`biz_type=inactivity_reset`):审计行 ↔ 资金流水交叉对账。
|
||||
|
||||
## 索引与约束
|
||||
- PK `id`;`ix_inactivity_reset_log_user_id`(按用户查)、`ix_inactivity_reset_log_reset_at`(按时间查)。
|
||||
|
||||
## 注意
|
||||
- **只清 2 桶**:金币 + 折算现金;**邀请现金不清**(两本账物理隔离,见 [`coin_account`](./coin_account.md) / `wallet.CoinAccount` 注释)。
|
||||
- **天然幂等**:清完余额=0,次日不再匹配;worker 重启 / 多次唤醒 / 补跑都不会重复清零或重复流水。
|
||||
- **总闸默认关**(`INACTIVITY_RESET_ENABLED=false`),灰度验证清零名单后再开。
|
||||
Reference in New Issue
Block a user