"""为本地“风控监控”页面灌入一组可重复的完整演示数据。 仅允许在 APP_ENV=dev 运行。脚本会重建 ``risk-demo-*`` 前缀的数据,并创建本地 后台账号 ``risk_demo / RiskDemo123!``,方便端到端和视觉验收。 """ from __future__ import annotations from datetime import timedelta from sqlalchemy import delete, select from app.admin.repositories import admin_user as admin_repo from app.core.config import settings from app.core.security import hash_password from app.db.session import SessionLocal from app.models.comparison import ComparisonRecord from app.models.risk import BehaviorEvent, RiskIncident, SubjectRestriction from app.repositories import risk as risk_repo from app.repositories import user as user_repo DEMO_PREFIX = "risk-demo-" ADMIN_USERNAME = "risk_demo" ADMIN_PASSWORD = "RiskDemo123!" def _reset_demo(db) -> None: demo_user_ids = list( db.scalars( select(ComparisonRecord.user_id) .where( ComparisonRecord.trace_id.like(f"{DEMO_PREFIX}%"), ComparisonRecord.user_id.is_not(None), ) .distinct() ).all() ) demo_incident_ids = list( db.scalars( select(RiskIncident.id).where( (RiskIncident.subject_id.like(f"{DEMO_PREFIX}%")) | ( (RiskIncident.rule_code == risk_repo.RULE_COMPARE_DAILY) & (RiskIncident.subject_id.in_([str(value) for value in demo_user_ids])) ) ) ).all() ) if demo_incident_ids: db.execute( delete(SubjectRestriction).where( SubjectRestriction.incident_id.in_(demo_incident_ids) ) ) db.execute( delete(SubjectRestriction).where( SubjectRestriction.subject_id.like(f"{DEMO_PREFIX}%") ) ) db.execute( delete(RiskIncident).where( (RiskIncident.subject_id.like(f"{DEMO_PREFIX}%")) | ( (RiskIncident.rule_code == risk_repo.RULE_COMPARE_DAILY) & (RiskIncident.subject_id.in_([str(value) for value in demo_user_ids])) ) ) ) db.execute( delete(BehaviorEvent).where( BehaviorEvent.subject_id.like(f"{DEMO_PREFIX}%") ) ) db.execute( delete(ComparisonRecord).where( ComparisonRecord.trace_id.like(f"{DEMO_PREFIX}%") ) ) db.commit() def _ensure_admin(db) -> None: admin = admin_repo.get_by_username(db, ADMIN_USERNAME) if admin is None: admin_repo.create_admin( db, username=ADMIN_USERNAME, password=ADMIN_PASSWORD, role="super_admin", ) return admin.password_hash = hash_password(ADMIN_PASSWORD) admin.role = "super_admin" admin.status = "active" db.commit() def _seed_sms(db, now) -> None: alerted = [ ("risk-demo-sms-oppo-a5", "OPPO A5", "13812343001", 9), ("risk-demo-sms-xiaomi14", "Xiaomi 14", "13812343002", 7), ("risk-demo-sms-iphone15", "iPhone 15", "13812343003", 5), ] events: list[BehaviorEvent] = [] for device_id, model, phone, count in alerted: for index in range(count): events.append( BehaviorEvent( event_type=risk_repo.EVENT_SMS_SEND, subject_type="device", subject_id=device_id, device_id=device_id, device_model=model, phone=phone if index == 0 else f"138{index:08d}"[-11:], client_ip="127.0.0.1", outcome="success", details={"demo": True}, occurred_at=now + timedelta(seconds=index), ) ) # 其余成功下发分散到未达阈值的设备,每台最多 4 条,确保报警设备仍严格为 3 台。 remaining = 1284 - sum(row[3] for row in alerted) for index in range(remaining): device_no = index // 4 device_id = f"risk-demo-sms-normal-{device_no:04d}" events.append( BehaviorEvent( event_type=risk_repo.EVENT_SMS_SEND, subject_type="device", subject_id=device_id, device_id=device_id, device_model="演示普通设备", phone=f"137{index % 100_000_000:08d}", client_ip="127.0.0.1", outcome="success", details={"demo": True}, occurred_at=now + timedelta(seconds=index % 1800), ) ) db.add_all(events) db.flush() for device_id, _, phone, _ in alerted: risk_repo.evaluate_behavior_rule( db, rule_code=risk_repo.RULE_SMS_HOURLY, subject_id=device_id, at=now, ) user = user_repo.upsert_user_for_login( db, phone=phone, register_channel="sms" ) db.add( BehaviorEvent( event_type=risk_repo.EVENT_SMS_LOGIN, subject_type="device", subject_id=device_id, user_id=user.id, device_id=device_id, phone=phone, outcome="success", details={"demo": True}, occurred_at=now + timedelta(minutes=2), ) ) db.commit() def _seed_oneclick(db, now) -> None: devices = [ ("risk-demo-oneclick-oppo-reno", "OPPO Reno", "13912343001", 482), ("risk-demo-oneclick-vivo-y36", "vivo Y36", "13912343002", 481), ] for device_id, model, phone, count in devices: user = user_repo.upsert_user_for_login( db, phone=phone, register_channel="jverify" ) db.add_all( [ BehaviorEvent( event_type=risk_repo.EVENT_ONECLICK_LOGIN, subject_type="device", subject_id=device_id, user_id=user.id, device_id=device_id, device_model=model, phone=phone, client_ip="127.0.0.1", outcome="failed" if index % 11 == 0 else "success", reason="运营商校验失败" if index % 11 == 0 else None, details={"demo": True}, occurred_at=now + timedelta(seconds=index * 2), ) for index in range(count) ] ) db.flush() risk_repo.evaluate_behavior_rule( db, rule_code=risk_repo.RULE_ONECLICK_DAILY, subject_id=device_id, at=now, ) db.commit() def _seed_compare(db, now) -> None: local_now = now.astimezone(risk_repo.CN_TZ).replace(tzinfo=None) accounts = [ ("13612343281", "risk-demo-compare-device-a", 1053), ("13511106208", "risk-demo-compare-device-b", 1052), ] for phone, device_id, count in accounts: user = user_repo.upsert_user_for_login( db, phone=phone, register_channel="sms" ) rows = [] for index in range(count): rows.append( ComparisonRecord( user_id=user.id, device_id=device_id, trace_id=f"{DEMO_PREFIX}compare-{user.id}-{index}", store_name=("春熙路小吃店", "科技园轻食", "万达广场烤肉")[ index % 3 ], product_names=("招牌套餐", "鸡胸沙拉", "双人烤肉")[index % 3], status="failed" if index % 17 == 0 else "success", information="演示比价记录", items=[{"name": "演示菜品", "qty": 1}], comparison_results=[ { "platform_id": "meituan", "platform_name": "美团", "price": 23.8, }, { "platform_id": "taobao", "platform_name": "淘宝", "price": 21.5, }, { "platform_id": "jd", "platform_name": "京东", "price": 22.2, }, ], skipped_dish_names=[], saved_amount_cents=230, raw_payload={"demo": True}, created_at=local_now + timedelta(seconds=index * 2), ) ) db.add_all(rows) db.commit() risk_repo.sync_compare_incident(db, user_id=user.id, at=local_now) def main() -> None: if settings.APP_ENV != "dev": raise SystemExit("拒绝执行:风控演示数据脚本仅允许 APP_ENV=dev") now = risk_repo.utcnow().replace(minute=10, second=0, microsecond=0) with SessionLocal() as db: _reset_demo(db) _ensure_admin(db) _seed_sms(db, now) _seed_oneclick(db, now) _seed_compare(db, now) print("风控监控演示数据已重建") print(f"后台账号:{ADMIN_USERNAME}") print(f"后台密码:{ADMIN_PASSWORD}") print("期望卡片:短信 3 / 1284;一键登录 2 / 963;比价 2 / 2105") if __name__ == "__main__": main()