"""人工验证用:按「金币/现金/邀请」排列组合 + 活跃/新用户对照,造一批账号。 用法(仓库根目录,venv 解释器): .venv/Scripts/python.exe scripts/seed_inactivity_cases.py # 造号(会先清掉上次 vcase*) .venv/Scripts/python.exe scripts/seed_inactivity_cases.py --clean # 只清理,不造 配合默认配置 INACTIVITY_RESET_DAYS=15 / INACTIVITY_WARN_DAYS_BEFORE=7,2 验证。 造完把 worker 打开(见 README/对话里的 .env),启动服务即会在 RUN_HOUR 后跑一轮。 ⚠️ worker 清零针对**库里所有**符合条件的用户,不止 vcase*——dev 库里若有其它"老且有余额、 无近期活跃事件"的用户,也会被一起清。要干净验证建议用一个空/副本 dev 库。 """ from __future__ import annotations import os import sys from datetime import UTC, datetime, timedelta sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from sqlalchemy import delete, select # noqa: E402 from app.db.session import SessionLocal # noqa: E402 from app.models.analytics_event import AnalyticsEvent # noqa: E402 from app.models.inactivity import ( # noqa: E402 InactivityNotificationLog, InactivityResetLog, ) from app.models.user import User # noqa: E402 from app.models.wallet import ( # noqa: E402 CashTransaction, CoinAccount, CoinTransaction, InviteCashTransaction, ) from app.repositories import wallet as wallet_repo # noqa: E402 MARK = "vcase" # username 前缀,用于清理 # label, 创建于N天前, coin, cash, invite, 近期事件(N天前)or None, 预期 CASES = [ ("1 三桶全有", 30, 100, 200, 300, None, "清 coin+cash;invite=300 保留;审计1行+2流水"), ("2 金币+现金", 30, 100, 200, 0, None, "清 coin+cash;审计1行+2流水"), ("3 金币+邀请", 30, 100, 0, 300, None, "清 coin;invite=300 保留;审计1行+1流水"), ("4 现金+邀请", 30, 0, 200, 300, None, "清 cash;invite=300 保留;审计1行+1流水"), ("5 只有金币", 30, 100, 0, 0, None, "清 coin;审计1行+1流水"), ("6 只有现金", 30, 0, 200, 0, None, "清 cash;审计1行+1流水"), ("7 只有邀请(红线)", 30, 0, 0, 300, None, "不选中/不清/无审计/无流水;invite=300 原封"), ("8 预警窗(10天)", 10, 50, 60, 70, None, "不清;发 T-7 预警;notification_log 1行;余额不动"), ("9 活跃兜底", 30, 100, 200, 300, 1, "昨日 home_view→last_active 近→不清不警"), ("10 新用户(3天)", 3, 100, 200, 0, None, "created_at 近→不清不警"), ] def _mark_uids(db) -> list[int]: return list(db.execute(select(User.id).where(User.username.like(f"{MARK}%"))).scalars()) def clean(db) -> int: uids = _mark_uids(db) if uids: for model in ( InactivityResetLog, InactivityNotificationLog, CoinTransaction, CashTransaction, InviteCashTransaction, AnalyticsEvent, CoinAccount, ): db.execute(delete(model).where(model.user_id.in_(uids))) db.execute(delete(User).where(User.id.in_(uids))) db.commit() return len(uids) def seed(db) -> None: now = datetime.now(UTC) print(f"{'#':>3} {'uid':>5} {'案例':<16} {'coin/cash/invite':<18} {'创建':<7} 预期") for i, (label, days_ago, coin, cash, invite, ev_days, expected) in enumerate(CASES, 1): u = User( phone=f"seed_tmp_{i}", username=f"{MARK}{i}", status="active", created_at=now - timedelta(days=days_ago), last_login_at=now, # 登录很新——但登录不算活跃,清零该发生照发生 ) db.add(u) db.flush() # 拿自增 id u.phone = f"1{u.id:010d}" # 用全局唯一 id 拼 "100…" 段手机号,dev 库里绝不撞 acc = wallet_repo.get_or_create_account(db, u.id, commit=False) acc.coin_balance, acc.cash_balance_cents, acc.invite_cash_balance_cents = coin, cash, invite acc.total_coin_earned = coin if ev_days is not None: db.add(AnalyticsEvent( event="home_view", device_id=MARK, user_id=u.id, client_ts=0, created_at=now - timedelta(days=ev_days), )) db.flush() print(f"{i:>3} {u.id:>5} {label:<16} {f'{coin}/{cash}/{invite}':<18} {f'{days_ago}天前':<7} {expected}") db.commit() def check(db) -> None: """worker 跑完后:打印每个 vcase 账号的当前三桶余额 + 是否有审计/预警行。""" rows = db.execute( select(User.id, User.username).where(User.username.like(f"{MARK}%")).order_by(User.id) ).all() if not rows: print("没有 vcase* 账号(先跑一次不带参数造号)") return print(f"{'uid':>5} {'账号':<8} {'coin/cash/invite(现在)':<24} {'审计':<5} 预警") for uid, uname in rows: acc = db.get(CoinAccount, uid) bal = f"{acc.coin_balance}/{acc.cash_balance_cents}/{acc.invite_cash_balance_cents}" if acc else "—" has_reset = db.execute( select(InactivityResetLog.id).where(InactivityResetLog.user_id == uid).limit(1) ).first() stages = db.execute( select(InactivityNotificationLog.stage).where(InactivityNotificationLog.user_id == uid) ).scalars().all() warn = ",".join(f"T-{s}" for s in stages) if stages else "—" print(f"{uid:>5} {uname:<8} {bal:<24} {'有' if has_reset else '—':<5} {warn}") def main() -> None: db = SessionLocal() try: if "--check" in sys.argv: check(db) return removed = clean(db) if removed: print(f"已清理上次 {removed} 个 {MARK}* 账号") if "--clean" in sys.argv: return seed(db) print("\n造号完成。打开 worker(INACTIVITY_RESET_ENABLED=true, RUN_HOUR=17)后启动服务," "≥17:00 首个 tick 即跑一轮。验完 `--clean` 清理。") finally: db.close() if __name__ == "__main__": main()