b4fe20aaca
前端确定"首页可见"埋点为 event=show + page=home(组合,非单一 event 名),故活跃口径 不能只改一个常量: - activity.active_event_condition():首页可见(event=show & page=home)∪ 比价 ∪ 领券, worker 子查询(last_active_subqueries)与 admin _attach_last_active 共用(单一真源); ACTIVE_EVENTS 收窄为仅两个纯 event 名,HOME_VIEW_EVENT="show" + 新增 HOME_VIEW_PAGE="home" - 测试:新增 show/home 算活跃、show/其他页不算的用例;_add_event 支持 page;口径常量断言更新 - seed 脚本 / spec / plan 同步 home_view → show+page=home Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
140 lines
6.0 KiB
Python
140 lines
6.0 KiB
Python
"""人工验证用:按「金币/现金/邀请」排列组合 + 活跃/新用户对照,造一批账号。
|
|
|
|
用法(仓库根目录,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=show + page=home
|
|
event="show", page="home", 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()
|