a86688ccfb
背景 / 目标 连续 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
69 lines
3.3 KiB
Python
69 lines
3.3 KiB
Python
"""add inactivity tables
|
|
|
|
Revision ID: 135e79414fd0
|
|
Revises: comparison_llm_cost
|
|
Create Date: 2026-07-16 18:31:02.105929
|
|
|
|
"""
|
|
from typing import Sequence, Union
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
|
|
|
|
# revision identifiers, used by Alembic.
|
|
revision: str = '135e79414fd0'
|
|
down_revision: Union[str, Sequence[str], None] = 'comparison_llm_cost'
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.create_table(
|
|
"inactivity_reset_log",
|
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
|
sa.Column("user_id", sa.Integer(), nullable=False),
|
|
sa.Column("coin_balance_before", sa.Integer(), nullable=False),
|
|
sa.Column("cash_balance_cents_before", sa.Integer(), nullable=False),
|
|
sa.Column("invite_cash_balance_cents_before", sa.Integer(), nullable=False),
|
|
sa.Column("last_active_at", sa.DateTime(timezone=True), nullable=True),
|
|
sa.Column("inactive_days", sa.Integer(), nullable=False),
|
|
sa.Column("reason", sa.String(length=32), nullable=False),
|
|
sa.Column("reset_at", sa.DateTime(timezone=True),
|
|
server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
|
sa.PrimaryKeyConstraint("id"),
|
|
)
|
|
with op.batch_alter_table("inactivity_reset_log", schema=None) as batch_op:
|
|
batch_op.create_index(batch_op.f("ix_inactivity_reset_log_user_id"), ["user_id"], unique=False)
|
|
batch_op.create_index(batch_op.f("ix_inactivity_reset_log_reset_at"), ["reset_at"], unique=False)
|
|
|
|
op.create_table(
|
|
"inactivity_notification_log",
|
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
|
sa.Column("user_id", sa.Integer(), nullable=False),
|
|
sa.Column("stage", sa.Integer(), nullable=False),
|
|
sa.Column("inactive_days", sa.Integer(), nullable=False),
|
|
sa.Column("coin_balance", sa.Integer(), nullable=False),
|
|
sa.Column("cash_balance_cents", sa.Integer(), nullable=False),
|
|
sa.Column("invite_cash_balance_cents", sa.Integer(), nullable=False),
|
|
sa.Column("channel", sa.String(length=16), nullable=False),
|
|
sa.Column("status", sa.String(length=16), nullable=False),
|
|
sa.Column("created_at", sa.DateTime(timezone=True),
|
|
server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
|
sa.PrimaryKeyConstraint("id"),
|
|
)
|
|
with op.batch_alter_table("inactivity_notification_log", schema=None) as batch_op:
|
|
batch_op.create_index(batch_op.f("ix_inactivity_notification_log_user_id"), ["user_id"], unique=False)
|
|
batch_op.create_index(batch_op.f("ix_inactivity_notification_log_created_at"), ["created_at"], unique=False)
|
|
|
|
|
|
def downgrade() -> None:
|
|
with op.batch_alter_table("inactivity_notification_log", schema=None) as batch_op:
|
|
batch_op.drop_index(batch_op.f("ix_inactivity_notification_log_created_at"))
|
|
batch_op.drop_index(batch_op.f("ix_inactivity_notification_log_user_id"))
|
|
op.drop_table("inactivity_notification_log")
|
|
with op.batch_alter_table("inactivity_reset_log", schema=None) as batch_op:
|
|
batch_op.drop_index(batch_op.f("ix_inactivity_reset_log_reset_at"))
|
|
batch_op.drop_index(batch_op.f("ix_inactivity_reset_log_user_id"))
|
|
op.drop_table("inactivity_reset_log")
|