feat(welfare): 不活跃清零 worker 常驻 + 默认 dry-run(ENABLED=false 只记审计不清)

改 INACTIVITY_RESET_ENABLED 语义:worker 不再"关=不启动"、改为**常驻**;ENABLED 只控是否**真清**——
false(默认)= 只写审计名单(reason=inactive_Nd_dryrun)、不动钱不写流水、不预警(dry-run 灰度看名单);
true = 真清金币+现金 + 预警。

- clear_user(dry_run):写审计跳出账,按 streak 去重(reset_at>last_active)不重复记
- run_reset_once/run_once:透传 dry_run;dry-run 抑制预警(不通知一个不会发生的清零)
- worker:start 常驻(去掉 ENABLED 启动闸);_run_once_entry dry_run=not ENABLED;启动日志标 mode
- 测试:dry-run 只记审计/不清/不预警/dedup + worker ENABLED=false→dry-run;去掉过时 disabled 用例
- config/worker 注释 + spec §6/§13/误清防护 同步新语义

测试安全:lifespan 在测试里不跑(TestClient 非 with 形式),worker 常驻不影响用例。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
guke
2026-07-18 23:39:50 +08:00
parent 5fdf2b1ad3
commit 0cc2e9dec8
5 changed files with 119 additions and 38 deletions
+7 -5
View File
@@ -6,7 +6,8 @@
健壮性:
- **逐用户幂等**:清完余额=0 次日不再匹配;预警按 streak 去重。启动补跑 / 多次唤醒 / 重启都安全。
- **同机多进程互斥**:文件锁保证多 worker 只有一个实际跑。
- **开关**:settings.INACTIVITY_RESET_ENABLED=false 时不启动(默认关,灰度验证后再开)。
- **常驻 + dry-run 默认**:worker 一直跑;INACTIVITY_RESET_ENABLED=false(默认)只记审计名单、
不动钱(dry-run 灰度看名单),=true 才真清。
⚠️ 这是不可逆批量资金操作(清空金币 + 折算现金,**邀请现金不清**)。口径见
app.repositories.inactivity / app.repositories.activity。
@@ -87,6 +88,7 @@ def _run_once_entry() -> dict:
reset_days=settings.INACTIVITY_RESET_DAYS,
warn_stages=settings.inactivity_warn_stages,
today=_cn_today(),
dry_run=not settings.INACTIVITY_RESET_ENABLED, # ENABLED=false → 只记审计名单、不清
)
@@ -102,9 +104,10 @@ async def _run_loop() -> None:
async def _run_locked_loop(interval: int) -> None:
logger.info(
"inactivity reset worker started interval=%ss run_hour=%s",
"inactivity reset worker started interval=%ss run_hour=%s mode=%s",
interval,
settings.INACTIVITY_RESET_RUN_HOUR,
"clear" if settings.INACTIVITY_RESET_ENABLED else "dry-run(audit-only)",
)
# 本进程上次跑过的北京日;None=尚未跑过本进程(当天到点即补)。
last_run: date | None = None
@@ -129,9 +132,8 @@ async def _run_locked_loop(interval: int) -> None:
def start_inactivity_reset_worker() -> asyncio.Task | None:
if not settings.INACTIVITY_RESET_ENABLED:
logger.info("inactivity reset disabled (INACTIVITY_RESET_ENABLED=false)")
return None
# worker 常驻(不再有"完全关"档);INACTIVITY_RESET_ENABLED 只决定是否**真清**:
# false(默认)= 只记审计名单(dry-run,不动钱),true = 真清金币+现金。
return asyncio.create_task(_run_loop(), name="inactivity-reset")