docs(welfare): 15天不活跃清零金币/现金 设计文档(spec) (#144)

修改INACTIVITY_RESET_ENABLED语义,为false时清空金币操作只记录审计日志,不执行操作。

---------

Co-authored-by: guke <guke@autohome.com.cn>
Reviewed-on: #144
This commit was merged in pull request #144.
This commit is contained in:
2026-07-19 00:14:03 +08:00
parent a86688ccfb
commit 130a7dff29
5 changed files with 119 additions and 38 deletions
+65 -3
View File
@@ -302,11 +302,35 @@ def test_run_once_warns_then_resets() -> None:
db.close()
def test_worker_disabled_returns_none(monkeypatch) -> None:
def test_worker_run_once_entry_dry_run(monkeypatch) -> None:
"""ENABLED=false(默认语义)→ worker 常驻但只记审计不清(dry_run = not ENABLED)。"""
from sqlalchemy import select
from app.core import inactivity_reset_worker as w
from app.core.config import settings
monkeypatch.setattr(settings, "INACTIVITY_RESET_ENABLED", False)
assert w.start_inactivity_reset_worker() is None
from app.models.wallet import CoinAccount
monkeypatch.setattr(settings, "INACTIVITY_RESET_ENABLED", False) # false = 只记审计
monkeypatch.setattr(settings, "INACTIVITY_RESET_DAYS", 15)
monkeypatch.setattr(settings, "INACTIVITY_WARN_DAYS_BEFORE", "")
monkeypatch.setattr(w, "_cn_today", lambda: date(2026, 2, 1))
db = SessionLocal()
try:
uid = _new_user(db, created_at=datetime(2026, 1, 1, tzinfo=timezone.utc), coin=100)
db.commit()
finally:
db.close()
w._run_once_entry()
db = SessionLocal()
try:
assert db.get(CoinAccount, uid).coin_balance == 100 # 没清
log = db.execute(select(InactivityResetLog).where(InactivityResetLog.user_id == uid)).scalar_one()
assert log.reason.endswith("dryrun") # 记了审计
finally:
db.close()
def test_worker_run_once_entry_executes(monkeypatch) -> None:
@@ -338,6 +362,44 @@ def test_worker_run_once_entry_executes(monkeypatch) -> None:
db.close()
def test_run_once_dry_run_records_audit_but_does_not_clear() -> None:
"""dry-run:只写审计(标 dryrun)、不动钱、不预警;重复跑不重复记(streak dedup)。"""
from sqlalchemy import select
from app.integrations.notifier import LogNotifier
from app.models.wallet import CoinAccount, CoinTransaction
from app.repositories import inactivity
db = SessionLocal()
try:
today = date(2026, 2, 1)
old = _new_user(db, created_at=datetime(2026, 1, 10, tzinfo=timezone.utc), coin=100, cash=200, invite=300)
warn_uid = _new_user(db, created_at=datetime(2026, 1, 22, tzinfo=timezone.utc), coin=50) # 预警窗
db.commit()
stats = inactivity.run_once(db, notifier=LogNotifier(), reset_days=15,
warn_stages=[7, 2], today=today, dry_run=True)
acc = db.get(CoinAccount, old)
assert (acc.coin_balance, acc.cash_balance_cents, acc.invite_cash_balance_cents) == (100, 200, 300) # 原封
log = db.execute(select(InactivityResetLog).where(InactivityResetLog.user_id == old)).scalar_one()
assert log.coin_balance_before == 100 and log.reason.endswith("dryrun") # 审计标 dryrun
assert db.execute(select(CoinTransaction).where(
CoinTransaction.user_id == old, CoinTransaction.biz_type == "inactivity_reset")).first() is None # 无流水
assert stats["warned"] == 0 # dry-run 不预警
assert db.execute(select(InactivityNotificationLog).where(
InactivityNotificationLog.user_id == warn_uid)).first() is None
assert stats["cleared"] == 1 # dry-run:cleared=记了几条
# 再跑一次 → 不重复记(dedup),余额仍原封
inactivity.run_once(db, notifier=LogNotifier(), reset_days=15, warn_stages=[7, 2], today=today, dry_run=True)
assert len(db.execute(select(InactivityResetLog).where(
InactivityResetLog.user_id == old)).scalars().all()) == 1
assert db.get(CoinAccount, old).coin_balance == 100
finally:
db.rollback()
db.close()
def test_admin_list_users_last_active_ignores_login() -> None:
"""admin 用户列表 last_active_at 改用共享口径:登录不算活跃(baseline=created_at)、只认活跃事件。"""
from app.admin.repositories import queries