From 2c5fe9cdef8aa32010b40f708ebabcb72c064ad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=BF=E4=BA=91=E7=A7=8B=E6=9C=88?= <15273589815@163.com> Date: Mon, 14 Sep 2026 20:50:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=8A=E9=A3=8E=E6=8E=A7=E6=89=AB=E6=8F=8F?= =?UTF-8?q?=E6=BC=94=E7=A4=BA=E6=95=B0=E6=8D=AE=E6=8E=A5=E8=BF=9B=E4=B8=80?= =?UTF-8?q?=E9=94=AE=E5=87=86=E5=A4=87=EF=BC=88=E7=AC=AC=2012=20=E6=AD=A5?= =?UTF-8?q?=EF=BC=89=EF=BC=8C=E5=B9=B6=E8=AE=A9=E7=BC=96=E6=8E=92=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=BB=99=E6=AD=A5=E9=AA=A4=E4=BC=A0=E5=8F=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 改动(`tools/seed_demo_data.py`) 1. **`STEPS` 增加第四个字段「额外命令行参数」**。这不是可有可无的装饰: `seed_risk_demo_data.py` **不带参数时只打印计划、不连数据库**(默认是干跑模式), 不给它传 `--apply` 的话,接进编排只会打印一句"确认无误后使用 --apply 正式写入" 然后退出码 0 —— **看起来像成功,实际什么都没准备**。这类"假成功"最难查, 所以参数必须能传得进去。 2. **新增第 12 步「风控扫描演示数据」**,带 `--apply --dry-run-scan`:先写入上游 业务数据,再跑一次规则干跑并把结果回滚。净效果是「写入上游数据 + 顺带验证 5 个场景都真的命中预期规则」。 3. `run_step` / `--list` / 主循环相应支持第四项;执行时把参数**打印出来**, 这样手跑时一眼能看出这一步到底会不会落库。 ## 为什么放第 12 步,而不是插在「风控预警样本」旁边 避免**改动后续步骤的编号**。文档与肌肉记忆里已经有 `--only 4,10`、`--from 4` 这类用法,插队会让它们全部错位。而且它本身是独立号段(客户 12001-12005、 产品 159991-159995),放最后不影响任何既有步骤。 ## 第 6 步与第 12 步不是重复(docstring 已写明) - 第 6 步 `seed_risk_alert_demo_data.py`:**直接摆好**三条现成预警, 保证风控页面一打开就有东西可看; - 第 12 步 `seed_risk_demo_data.py`:**只造上游**,刻意不写 `fin_risk_alert`, 让风控扫描器按规则生成 —— 演示覆盖的是**规则 → 证据 → 通知**完整链路, 而不是"预先摆好的假预警"。 ## 实测 - `--list` -> **12 步**,第 12 步末尾显示 `[--apply --dry-run-scan]` - `--only 12 --dry-run` -> 只打印、不执行(并且打印出了参数) - `--only 12` -> **真跑通**,退出码 0: [12/12] 风控扫描演示数据 用时 3.8s 退出码 0 [命中] 客户 12001:RW-003 (高) [命中] 客户 12002:RW-007 (高) [命中] 客户 12003:RW-007 (中) [命中] 客户 12004:RW-012 (高) [命中] 客户 12005:RW-015、RW-018 (低) - `ruff check tools/seed_demo_data.py` -> All checks passed --- tools/seed_demo_data.py | 85 +++++++++++++++++++++++++++++++---------- 1 file changed, 64 insertions(+), 21 deletions(-) diff --git a/tools/seed_demo_data.py b/tools/seed_demo_data.py index 5862a51..e9dd5e3 100644 --- a/tools/seed_demo_data.py +++ b/tools/seed_demo_data.py @@ -19,20 +19,35 @@ | 7 | 投顾演示数据 | 投顾工作台要有方案可看 | | 8-10 | 各类发布配置 | Agent 工具白名单与提示词,缺了客服/风控会"失败关闭" | | 11 | 知识库素材 | 客服答得出问题的前提 | +| 12 | 风控扫描演示数据 | 造**上游**业务数据,让扫描器自己生成预警(客户 12001-12005) | -## ⚠️ 两个必须知道的点 +⚠️ **第 6 步与第 12 步是两套不同的风控演示数据,不是重复**: +- 第 6 步 `seed_risk_alert_demo_data.py` **直接摆好**三条不同状态的预警,保证风控页面 + 一打开就有东西可看; +- 第 12 步 `seed_risk_demo_data.py` **只造上游**(客户/产品/账户/持仓/交易/流水/登录/工单), + **刻意不写 `fin_risk_alert`**,让风控扫描器按规则生成 —— 演示时覆盖的是 + **规则 → 证据 → 通知**完整链路,而不是"预先摆好的假预警"。 + +**第 12 步需要带参数跑**(它默认是只打印计划、不连库的干跑模式),所以它额外传 +`--apply --dry-run-scan`:写入上游数据,然后跑一次规则干跑**并把结果回滚**, +顺带验证 5 个场景都真的命中预期规则。 + +## ⚠️ 三个必须知道的点 1. **最后一步与行情都需要 Agent Worker 才会真正生效**:知识入库只写 MySQL + 投 outbox 事件, 向量由 Worker 消费事件后写 Milvus;没有 Worker 时现象是"客服照旧答不上",且**没有报错**。 所以跑完本脚本**必须**再起 Worker(`start.ps1` 会一起起)。 2. **第 2 步不是幂等的**:`set_user_password.py` 重跑等于**重设密码**(bcrypt 每次加盐不同)。 这是有意的(改密就该覆盖),但要知道它不是"已存在就跳过"。 +3. **第 12 步不依赖 Worker**:它调的是 `RiskRuleEngine.refresh_alerts()`(进程内), + 不是通过事件队列;干跑部分结束会 `rollback()`,不会留下正式预警。 ## 用法 python tools/seed_demo_data.py # 全跑 python tools/seed_demo_data.py --from 4 # 从第 4 步开始(重跑行情等) python tools/seed_demo_data.py --only 4,10 # 只跑指定步骤 + python tools/seed_demo_data.py --only 12 # 只准备风控扫描演示数据 python tools/seed_demo_data.py --list # 只列步骤 """ @@ -49,28 +64,51 @@ PROJECT_ROOT = Path(__file__).resolve().parents[1] if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(errors="replace") # type: ignore[union-attr] -#: (标题, 脚本相对路径, 备注) -STEPS: tuple[tuple[str, str, str], ...] = ( - ("账号与权限", "tools/seed_test_rbac.py", "五个演示角色 + 权限号段 9001-9046"), - ("演示口令", "tools/set_user_password.py", "⚠️ 非幂等:重跑等于重设密码"), - ("客户账户与持仓", "tools/seed_sim_account_demo.py", "客户 9001 开 10 万虚拟资金 + 持仓"), - ("场内行情", "tools/sync_market_prices.py", "⚠️ 下单硬前置;行情会过期,演示前必跑"), - ("历史净值", "tools/sync_nav_history.py", "产品详情页走势图的数据源(fin_nav_history)"), - ("风控预警样本", "tools/seed_risk_alert_demo_data.py", "三条不同状态的演示预警"), - ("投顾演示数据", "tools/seed_advisor_demo.py", "投顾工作台要展示的方案与归属"), - ("风控 Agent 白名单", "tools/publish_risk_agent_config.py", "缺了风控助手工具会失败关闭"), - ("客服配置白名单", "tools/publish_customer_service_config.py", "缺了客服工具会失败关闭"), - ("客服闲聊提示词", "tools/publish_chitchat_prompt.py", "话术走发布配置,不硬编码"), - ("知识库素材", "tools/seed_knowledge_demo.py", "⚠️ 需要 Worker 才会写进 Milvus"), +#: (标题, 脚本相对路径, 备注, 额外命令行参数) +#: +#: 第四个字段是为「默认行为不是写入」的脚本准备的:`seed_risk_demo_data.py` 不带参数时 +#: 只打印计划、**不连数据库**,必须显式传 `--apply` 才会真正写入。不给它传参的话, +#: 加进本脚本只会打印一段"确认无误后使用 --apply 正式写入"然后什么也不做 —— +#: 看起来像成功,实际什么都没准备,这类"假成功"最难查。 +STEPS: tuple[tuple[str, str, str, tuple[str, ...]], ...] = ( + ("账号与权限", "tools/seed_test_rbac.py", "演示角色 + 权限号段", ()), + ("演示口令", "tools/set_user_password.py", "⚠️ 非幂等:重跑等于重设密码", ()), + ("客户账户与持仓", "tools/seed_sim_account_demo.py", "客户 9001 开 10 万虚拟资金 + 持仓", ()), + ("场内行情", "tools/sync_market_prices.py", "⚠️ 下单硬前置;行情会过期,演示前必跑", ()), + ("历史净值", "tools/sync_nav_history.py", "产品详情页走势图的数据源(fin_nav_history)", ()), + ("风控预警样本", "tools/seed_risk_alert_demo_data.py", "三条不同状态的演示预警", ()), + ("投顾演示数据", "tools/seed_advisor_demo.py", "投顾工作台要展示的方案与归属", ()), + ("风控 Agent 白名单", "tools/publish_risk_agent_config.py", "缺了风控助手工具会失败关闭", ()), + ("客服配置白名单", "tools/publish_customer_service_config.py", "缺了客服工具会失败关闭", ()), + ("客服闲聊提示词", "tools/publish_chitchat_prompt.py", "话术走发布配置,不硬编码", ()), + ("知识库素材", "tools/seed_knowledge_demo.py", "⚠️ 需要 Worker 才会写进 Milvus", ()), + ( + "风控扫描演示数据", + "tools/seed_risk_demo_data.py", + "上游业务数据(客户 12001-12005),让扫描器自己生成预警", + # 不带参数时它只打印计划、不连库;--dry-run-scan 跑完规则干跑会回滚, + # 所以净效果是「写入上游数据 + 顺带验证 5 个场景都命中预期规则」。 + ("--apply", "--dry-run-scan"), + ), ) -def run_step(index: int, title: str, script: str, note: str, *, dry_run: bool) -> bool: +def run_step( + index: int, + title: str, + script: str, + note: str, + extra_args: tuple[str, ...], + *, + dry_run: bool, +) -> bool: path = PROJECT_ROOT / script print() print("=" * 88) print(f"[{index}/{len(STEPS)}] {title} —— {note}") - print(f" {script}") + # 把额外参数一并打出来:有些脚本不带参数是"只打印计划不写库", + # 手跑时能一眼看出这一步到底会不会落库。 + print(" " + " ".join([script, *extra_args])) print("=" * 88) if not path.exists(): print(f"[跳过] 脚本不存在:{path}") @@ -81,7 +119,11 @@ def run_step(index: int, title: str, script: str, note: str, *, dry_run: bool) - started = time.perf_counter() # 刻意**不捕获输出**(继承 stdio):既不吞掉子脚本的报错, # 也避免在受限环境里因 piped stdio 失败。 - result = subprocess.run([sys.executable, str(path)], check=False, cwd=str(PROJECT_ROOT)) + result = subprocess.run( + [sys.executable, str(path), *extra_args], + check=False, + cwd=str(PROJECT_ROOT), + ) elapsed = time.perf_counter() - started ok = result.returncode == 0 print(f"[{'完成' if ok else '失败'}] {title} 用时 {elapsed:.1f}s 退出码 {result.returncode}") @@ -98,8 +140,9 @@ def main() -> int: if args.list: print(f"共 {len(STEPS)} 步:") - for index, (title, script, note) in enumerate(STEPS, 1): - print(f" {index:>2}. {title:<20} {script:<48} {note}") + for index, (title, script, note, extra) in enumerate(STEPS, 1): + suffix = f" [{' '.join(extra)}]" if extra else "" + print(f" {index:>2}. {title:<20} {script:<48} {note}{suffix}") return 0 only = {int(part) for part in args.only.split(",") if part.strip()} if args.only else None @@ -114,8 +157,8 @@ def main() -> int: print(f"准备演示数据:{len(selected)} 步" + ("(dry-run)" if args.dry_run else "")) failed: list[str] = [] - for index, title, script, note in selected: - if not run_step(index, title, script, note, dry_run=args.dry_run): + for index, title, script, note, extra in selected: + if not run_step(index, title, script, note, extra, dry_run=args.dry_run): failed.append(title) print()