Files
group_fqcd_jr/docs/44-演示流程.md
T
lzf_0626 7208713c17 feat(portal): 接入历史净值,产品详情页恢复真实走势图
`fin_nav_history` 一直是**空表**,所以产品详情页画不出走势图 —— 此前那条曲线是
前端 `mock-data.js` 里编的 12 个点位,接入公开产品接口时把它去掉了
(走势图最容易被当成真实业绩),页面改为显示"尚未接入"。本次补上完整链路。

## 1. 取数(app/infrastructure/fund_market_adapter.py)

新增 `fetch_nav_history()`:东方财富历史净值接口(`api.fund.eastmoney.com/f10/lsjz`)
分页取序列。

- **为什么不复用 `fetch_kline`**:它走 `push2his.eastmoney.com`,
  该域名在本环境实测连接被拒(`RemoteProtocolError`);
- **为什么不复用 `hq.get_southern_fund_nav_history`**:那个函数校验**南方基金白名单**,
  而 `fin_product` 里有非南方基金的产品(510300 是华泰柏瑞的),调它会直接 `ValueError`。

⚠️ 实现里踩到一个坑:接口**会忽略请求中的 `pageSize`**(实测固定每次返回 20 条)。
最初按硬编码的 30 判断"是否最后一页",于是 `len(items) < 30` 永远成立、只取到第一页 ——
走势图看上去"有数据",其实只有最近 20 天,而且毫无报错。
改为按首屏**实际条数** + `TotalCount` 推算页数后,同样区间取到 124 条。

## 2. 落库(tools/sync_nav_history.py,新增)

写入 `fin_nav_history`,按 `(product_id, nav_date)` 幂等 upsert。
实测:20 只产品 / **2513 行** / 2026-03-17 ~ 09-13;重跑**新写入 0 行**。

## 3. 接口(P002)

`GET /api/v1/products/{product_code}/nav-history`,编号 **P002**,已登记 `docs/05` §19。
鉴权口径与 P001 相同(要求有效令牌、不校验权限码,访客令牌可用);`days` 有界 1–365。

**表为空时返回 `count=0` 与空数组,而不是报错** —— 调用方据此显示"尚未接入",
**不得回退到编造曲线**。产品不存在或未上市 → `404`(否则前端分不清"没有数据"
和"没有这只产品")。

## 4. 前端

详情页按序列画 SVG 折线,期数标题改为动态("近 N 个交易日")。
表为空时仍显示"尚未接入"占位,并补上此前缺失的 `.detail-chart__empty` 样式。
`product-detail.js` / `.css` / `index.html` 的缓存版本参数一并 bump 到 `-7`。

## 5. 演示数据

`tools/seed_demo_data.py` 增加第 5 步「历史净值」(现 **11 步**),
否则换台机器演示时走势图又会是空的。

验证:P002 实测 515450 / 510300 各 120 个净值点;ruff 通过;mypy 251 文件 0 错;
unit+contract 1391 passed;integration 108 passed;e2e 冒烟 40/40。
2026-09-13 23:38:13 +08:00

14 KiB
Raw Blame History

演示流程(照着走)

读者:负责演示的人。本文按"操作 → 看到什么 → 这体现什么"三段式写, 可以直接照着念。 主线时长:约 8 分钟;含讲解约 15 分钟。 全部内容均已实测(2026-09-13);标 ⚠️ 的地方是容易翻车的点。


0. 演示前准备

0.1 准备演示数据(首次、或换了机器才需要)

python tools/seed_demo_data.py

10 步:账号权限 → 演示口令 → 客户账户 → 场内行情 → 风控预警样本 → 投顾数据 → 三类发布配置 → 知识库素材。脚本会逐步打印完成/失败。

⚠️ 第 2 步不是幂等的:重跑等于重设密码(bcrypt 每次加盐不同)。

0.2 一条必须知道的硬规则:行情有效期只有 15 分钟

下单要求行情快照落在 15 分钟内(app/service/trade_service.py 的 MAX_QUOTE_AGE), 系统没有任何自动刷新机制 —— 超时后所有委托直接返回 503 行情已过期。

演示时踩过这个坑:明明上午刷过行情,下午演示第一单就 503。

怎么办:start.ps1 已经会替你刷新一次(见 0.3),所以正常不用管。 万一演示中途下单报 503 —— 在新窗口跑一次就行,不用重启任何服务(已实测):

python tools/sync_market_prices.py

它按产品 upsert(幂等),几秒内把 20 只产品行情全部刷新,15 分钟窗口重新计时。

不想让启动脚本联网刷行情:start.ps1 -SkipPriceSync

0.3 启动平台

powershell -ExecutionPolicy Bypass -File start.ps1

它按顺序做四件事:找解释器 → 检查 MySQL/Redis/Milvus → 刷新行情 → 起两个窗口,然后打印访问入口与账号。 它会开两个窗口:

窗口 作用 少了它会怎样
API 所有接口与页面 什么都没有
Worker Agent 对话、知识向量同步、记忆抽取、风控扫描 客服对话一直"超时";新知识不进 Milvus 且无任何报错

0.4 开场前自检(务必做)

python tools/e2e_smoke_test.py --read-only

看最后一行:40/40 通过才开始演示。有 FAIL 就按 §4 排查。

0.5 打开第一个页面

http://127.0.0.1:8000/portal/


1. 主线场景

场景 1 · 访客咨询与知识边界(1.5 min)

打开 http://127.0.0.1:8000/portal/guest/products/
操作 点右下角客服浮窗 → 输入「场内基金的管理费率是多少」→ 发送
预期 约 3–4 秒返回:本平台 20 只场内基金的管理费率 0.15%~1.20%/年,并列出 ETF 与 LOF 的差异

这体现什么:客服不是"模型随便聊",而是检索已发布知识后作答,答案可追溯; 延迟稳定在 3–4 秒(一次意图分类 + 一次向量检索)。

接着再问一句(体现安全边界):「我的账户里有多少钱?」 → 预期:回复"该服务需要登录后才能查询您的个人信息",不编造账户数字。

产品页的数据来自真实接口(GET /api/v1/products):产品与净值取自 fin_product, 行情由 tools/sync_market_prices.py 从行情源同步,页面底部有来源声明。

⚠️ 两个可能被问到、但其实不是故障的地方:

  • 产品列表的「最近涨跌」列当前显示 "暂无" —— 当日涨跌要两个交易日的收盘价才算得出, 而行情库目前只有一个交易日。跑过第二次行情同步后自然会显示。我们不编这个数。
  • 产品详情的净值走势图现在是真实数据:fin_nav_history 由 tools/sync_nav_history.py(演示数据第 5 步)从东财净值接口同步,近 120 个交易日。 若图上显示"尚未接入",说明第 5 步没跑 —— 补跑即可,不用重启服务。

场景 2 · 客户资产(1 min)

打开 http://127.0.0.1:8000/portal/customer/login/
账号 cust_t / 123456
预期 自动进资产总览:总资产约 10 万、可用资金、持仓市值(7 只持仓)

这体现什么:持仓按真实行情计价(不是写死的假数);页面底部有数据来源说明。

点左侧「我的持仓」「资金流水」「成交明细」各看一眼即可。

场景 3 · 客户下单(1.5 min)⭐ 重点

打开 客户页 → 交易记录 → 或直接用接口
说明 前端下单表单待完善,本场用接口演示更直观(见下)
# 演示用:买入 100 份沪深300ETF
$body = '{"product_code":"510300","order_side":"buy","quantity":100}'
$r = Invoke-RestMethod -Uri http://127.0.0.1:8000/api/v1/auth/tokens -Method Post `
     -Body '{"username":"cust_t","password":"123456"}' -ContentType application/json
Invoke-RestMethod -Uri http://127.0.0.1:8000/api/v1/users/me/orders -Method Post `
     -Headers @{ Authorization = "Bearer $($r.data.access_token)"; "Idempotency-Key" = [guid]::NewGuid().ToString("N") } `
     -Body $body -ContentType application/json | ConvertTo-Json -Depth 4

预期:status=已成交、executed_price=4.579(真实行情价)、 成交金额 457.90、手续费 0.05、实际扣款 457.95(net_amount)。

这体现什么:

  • 成交价来自外部行情,不是模拟随机数;
  • 下单前会做适当性校验(风险等级匹配),不匹配直接拒绝;
  • 成交后持仓、资金流水、成交明细三处同时落账(可当场刷新场景 2 的页面看变化)。

⚠️ 如果返回 503 FUND_QUOTE_UNAVAILABLE → 跑 §0.2 的行情同步。

场景 4 · 客服对话与转人工(1 min)

操作 客户页右下角浮窗 → 问「基金定投是什么」→ 点「转人工客服」
预期 先得到知识库答案;转人工返回 202,工单进入管理员队列

这体现什么:客户主动请求人工与"答不上来"是两条不同路径 —— 前者建工单, 后者按既定口径引导拨打客服热线(不建工单,符合我们"答不了就转人工、不硬答"的原则)。

场景 5 · 风控预警处置(2 min)⭐ 重点

打开 http://127.0.0.1:8000/portal/employee-console/login/ → risk_t / 666666
预期 概览:未闭环预警数、高风险数、待处理、已超时

操作顺序(这是核心闭环):

  1. 点预警队列里的某条 → 打开详情;
  2. 详情里有八类证据入口:客户、产品、交易、资金流水、持仓、登录记录、预警、通知;
  3. 点「确认接收」→ 状态变为已确认;
  4. 点「进入调查」→ 状态变为调查中;
  5. 点「完成结案」→ 状态变为已结案,并且按风险等级扣减客户行为分。

这体现什么:预警处置是有状态机、有留痕、有连带影响的 —— 不是改个字段。每一步都记审计(谁、何时、做了什么)。

若队列里没有「待处理」的预警(都已被处置过),跑 python tools/e2e_smoke_test.py —— 它会自动造一条待处理预警并跑完整闭环。

补一步「风控助手」(30 秒,很值得演):点上方「风控助手」标签 → 点预设的 「当前风险概览」按钮 → Agent 调 get_risk_overview 只读工具返回未闭环预警概览, 结尾并声明"以上仅为查询与复核线索,不构成任何已确认、误报、关闭或升级的处置结论, 最终请由风控专员人工复核并留痕"。

这体现什么:Agent 只做只读查询与研判草案,处置动作一律留给人工。 实测这条问法命中的是 risk_overview 意图、置信度 1.0000 —— 是配置好的意图分派,不是让模型自由发挥。

场景 6 · 风控日报(1 min)

操作 风控工作台 → 日报 → 生成
预期 文本流式逐段出现(SSE),完成后可填多个邮箱发送

这体现什么:长任务走 SSE 流式,而不是让页面转圈等 30 秒。

场景 7 · 投顾工作台(1 min)

打开 http://127.0.0.1:8000/portal/employee-advisor/dashboard/
账号 advisor_t / abc12345
预期 一张「投资目标方案书 · 客户 9001」卡片,含发布时间与方案内容

这体现什么:投顾看到的是自己服务的客户的已发布交付物。 数据范围按 sys_customer_assignment 归属关系判定(不是按 data_scope —— 那会把 all 级权限放大成"看全部客户")。

方案书的审核与发布都要求管理员(admin=True),投顾自己发不出来。 这是有意的复核环节,不是缺陷。

场景 8 · 管理员治理(1.5 min)

打开 http://127.0.0.1:8000/portal/employee-console/workspace/
账号 admin_t / 88888888
预期 四个指标卡 + 六个标签页

挨个点一遍,每处一句话:

标签页 讲解要点
角色权限 五个角色,权限数分别是 admin 59 / advisor 28 / customer 26 / risk_operator 10 / operator 2
配置发布 工具白名单、提示词走发布状态机(校验→审核→激活),不是改配置文件
审计 每一次权限判定、工具调用都有记录
转人工工单 场景 4 建的工单在这里;不返回客户标识与原始对话(脱敏)
模型端点 当前用 deepseek-flash(意图分类)+ qwen 向量(embedding)

这体现什么:权限变更必须过审核流程并留痕 —— 这是金融场景的硬要求, 所以平台刻意不提供"直接改权限"的接口。


2. 备选场景(时间充足时)

场景 怎么演 体现什么
配置发布四态 管理员工作台建一个草稿版本 → 提交校验 → 审核 → 激活 改配置是走流程的,可回滚
五角色权限对比 用五个账号分别登录,看入口守卫各跳哪里 越权在前端就被挡,服务端还有一层
知识入库 python tools/seed_knowledge_demo.py 后复测场景 1 知识是可运营的,不是写死的
平台体检 python tools/e2e_smoke_test.py 40 项全绿,交付质量可自证

3. 可能被问到的问题(建议话术)

Q:这是真实行情吗? A:行情取自公开数据源(腾讯行情),成交价用的就是它。但交易本身是模拟的 —— 平台是"场内基金模拟交易",不下真实单。

Q:Agent 会不会乱答? A:不会。客服只回答检索到的已发布知识,且有置信度门槛: 命中分数 ≥0.75 直接答;0.55~0.75 之间还要求领先次优 ≥0.07; 不满足就引导人工。宁可转人工也不答错 —— 这是刻意的取舍。

Q:为什么有些问题答不上来? A:知识库是人工审核入库的,目前覆盖交易规则、20 只产品、费率与常见问答。 问不到的地方会转人工,这是设计而不是故障。

Q:数据安全怎么保证? A:三层 —— 前端按权限隐藏入口;服务端每次请求重新解析权限与数据范围(令牌里不带权限); 工单等管理面接口不返回客户标识与原始对话。所有判定都有审计。

Q:为什么不让管理员直接改权限? A:金融场景要求权限变更留痕可追溯,所以统一走配置发布流程(草稿→校验→审核→激活)。


4. 出问题怎么办

现象 原因 怎么办
客户下单返回 503 行情已过期 行情快照过期(有效期仅 15 分钟) python tools/sync_market_prices.py —— 立即生效,无需重启服务
客服一直"繁忙/超时" Worker 没在跑 看 start.ps1 起的第二个窗口;或 python -m app.worker
改了页面看不到效果 浏览器缓存 Ctrl+F5 硬刷新
知识库问答答不上 向量没同步 确认 Worker 在跑,然后跑 tools/seed_knowledge_demo.py
平台起不来 MySQL 没起 看 start.ps1 的依赖检查输出
风控队列没有"待处理"预警 演示样本都被处置过了 python tools/e2e_smoke_test.py(自动造一条)

一条命令定位:

python tools/e2e_smoke_test.py

它逐条打印 6 条线 40 项的结果,FAIL 的那条就是问题所在。


5. 速查

账号

角色 用户名 密码 登录入口
客户 cust_t 123456 /portal/customer/login/
风控专员 risk_t 666666 /portal/employee-console/login/
管理员 admin_t 88888888 同上
投顾 advisor_t abc12345 同上(→ 投顾工作台)
运营 offsite_t offsite123 同上(→ 运营工作台)

常用命令

python tools/seed_demo_data.py            # 准备演示数据(10 步,首次/换机器)
python tools/sync_market_prices.py        # 刷行情(15 分钟有效期,下单 503 时补跑)
powershell -ExecutionPolicy Bypass -File start.ps1   # 起 API + Worker(自动刷行情)
python tools/e2e_smoke_test.py            # 全量体检(40 项)
python tools/e2e_smoke_test.py --read-only # 只读体检,不动数据

已知的空数据(演示时别点进去尴尬)

位置 现状
运营工作台 场外邮件 0 条、邮箱未初始化(需要真实邮件源)
管理员 · 画像候选 0 条(需要客户对话触发画像抽取)

端口:API 8000 / MySQL 3306 / Redis 6379 / Milvus 19530