7bf04f2655
## 修改内容 - 新增短信、一键登录、比价三类风控事件与规则聚合 - 新增管理员忽略、封禁、解封、重置报警和阈值配置接口 - 风险列表返回当前有效限制的 restriction_id,供后台已封禁视图解除 - 在短信/一键登录、比价、任务领奖、提现链路接入风险记录与限制 - 新增通用行为流水、风险事件、主体限制模型及 Alembic 迁移 - 新增本地演示数据脚本与风控测试 ## 验证 - ruff check:通过 - Alembic 全新 SQLite upgrade head / downgrade -1:通过 - 风控测试:通过,覆盖封禁列表 restriction_id 与解除链路 - 全量测试:532 passed,8 failed;其中 7 项在干净 origin/main 独立复现,另 1 项独立复跑通过,未发现本分支新增回归 --------- Co-authored-by: unknown <798648091@qq.com> Reviewed-on: #174 Co-authored-by: linkeyu <linkeyu@wonderable.ai> Co-committed-by: linkeyu <linkeyu@wonderable.ai>
275 lines
9.4 KiB
Python
275 lines
9.4 KiB
Python
"""为本地“风控监控”页面灌入一组可重复的完整演示数据。
|
|
|
|
仅允许在 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()
|