docs/44 演示流程按 2026-09-14 实测同步:今日盈亏可讲、投顾自助审核、管理员八页签、权限数与已知空数据

This commit is contained in:
2026-09-15 00:14:20 +08:00
parent 776b504ee6
commit 42a330c4c9
+122 -59
View File
@@ -3,7 +3,16 @@
> **读者**:负责演示的人。本文按"操作 → 看到什么 → 这体现什么"三段式写,
> 可以直接照着念。
> **主线时长**:约 8 分钟;含讲解约 15 分钟。
> **全部内容均已实测**(2026-09-13);标 ⚠️ 的地方是容易翻车的点。
> **全部内容均已实测**;标 ⚠️ 的地方是容易翻车的点。
>
> **2026-09-14 复核**:本文档已按当天实测数据更新四处(「今日盈亏」已实现、投顾推荐方案
> 已开放自助审核发布、管理员工作台为八个标签页、角色权限数与已知空数据)。数字类结论都标了
> 实测日期——**换环境或换库后要重查**。
>
> **配套文档**(想看"为什么"而不是"怎么演"就看这两份):
> - `docs/演示用/全功能流程-大白话版.md` —— 从登入讲到六个门户的全部流程,不用术语;
> - `docs/演示用/记忆架构与Worker工作流程-大白话版.md` —— 记忆链路与后台 Worker 的详细工作流程;
> - `docs/演示用/今日盈亏实现说明-2026-09-14.md` —— 「今日盈亏」的口径、数据坑与复算命令。
---
@@ -34,7 +43,7 @@ python tools/seed_demo_data.py
python tools/sync_market_prices.py
```
它按产品 upsert(幂等),几秒内把 20 只产品行情全部刷新,15 分钟窗口重新计时。
它按产品 upsert(幂等),几秒内把**全部产品(当前库里 26 只)**行情刷新,15 分钟窗口重新计时。
> 不想让启动脚本联网刷行情:`start.ps1 -SkipPriceSync`
@@ -72,9 +81,14 @@ powershell -ExecutionPolicy Bypass -File start.ps1 -Port 8100 # 换端口(80
| 窗口 | 作用 | 少了它会怎样 |
|---|---|---|
| API | 所有接口与页面 | 什么都没有 |
| **Worker** | Agent 对话、知识向量同步、记忆抽取、风控扫描 | 客服对话一直"超时";新知识不进 Milvus 且**无任何报错** |
| **Worker** | Agent 真正执行、领域事件投递、知识向量同步、记忆抽取与画像投影、会话片段聚合、**场外收件** | 客服对话一直"超时";新知识不进 Milvus(**无任何报错**);记忆不更新;场外收不到邮件 |
> ⚠️ 两个窗口都别关。启动脚本自己的窗口跑完就可以关(服务在独立窗口里)。
>
> 注:**风控定时扫描不在这个 Worker 里** —— 它是独立进程
> (`python -m app.worker.risk_scan_scheduler`)且默认关闭;演示时用的是风控工作台上的
> **「扫描」按钮**(同步执行,最长 60 秒)。
> 详细分工见 `docs/演示用/记忆架构与Worker工作流程-大白话版.md`。
### 0.4 开场前自检(务必做)
@@ -85,7 +99,8 @@ python tools/e2e_smoke_test.py --read-only # 业务链路:登录→下单
python tools/portal_api_check.py # 接口契约:按前端的方式核对每个端点的状态码与字段
```
看最后一行:**40/40** 与 **41 项全通过**才开始演示。有 FAIL 就按 §4 排查。
看最后一行:**40/40** 与 **41 项全通过**才开始演示(用例数会随开发增减,
判断健康的准则是 **0 FAIL**,不是绝对条数)。有 FAIL 就按 §4 排查。
> 两者的分工:`e2e_smoke_test.py` 回答"**这条业务走得通吗**",
> `portal_api_check.py` 回答"**接口返回的东西前端能不能正确显示**"。
@@ -107,7 +122,12 @@ python tools/portal_api_check.py # 接口契约:按前端的方
|---|---|
| **打开** | <http://127.0.0.1:8000/portal/guest/products/> |
| **操作** | 点右下角客服浮窗 → 输入「场内基金的管理费率是多少」→ 发送 |
| **预期** | **约 3–4 秒**返回:本平台 20 只场内基金的管理费率 0.15%~1.20%/年,并列出 ETF 与 LOF 的差异 |
| **预期** | **约 3–4 秒**返回:本平台场内基金的管理费率区间(0.15%~1.20%/年),并列出 ETF 与 LOF 的差异 |
> ⚠️ 知识库文案是**人工审核入库时的快照**(当前 active 知识 24 份、元数据 199 行),
> 里面写的产品数量**可能与产品列表的当前数量不一致**。
> 被问到时如实说:"这是知识库里的内容,知识是人工入库的,产品列表取的是实时数据。"
> 不要现场改知识库文案来对齐数字。
**这体现什么**:客服不是"模型随便聊",而是**检索已发布知识后作答**,答案可追溯;
延迟稳定在 3–4 秒(一次意图分类 + 一次向量检索)。
@@ -137,18 +157,25 @@ python tools/portal_api_check.py # 接口契约:按前端的方
**这体现什么**:持仓按**真实行情**计价(不是写死的假数);页面底部有数据来源说明。
点左侧「我的持仓」「资金流水」「成交明细」各看一眼即可。
点顶部导航「我的持仓」「资金流水」「成交明细」「盈亏分析」各看一眼即可。
> ⚠️ **刻意不要指着"今日盈亏"讲** —— 该字段目前是**占位 0**(硬编码,见 §3 同名问答与 SRS Q22)。
> 讲「持有盈亏」「总市值」「可用资金」这三项,它们都是真实计算的。
> 被追问时按 §3 的话术回答,不要现编。
> ✅ **「今日盈亏」现在可以直接讲**(2026-09-14 起是真实计算,不再是占位 0)。
> 实测口径与数字(当天数据):`cust_t` 看板 **今日盈亏 −132.70(−0.2528%)**;
> 逐只:`510300` −94.50、`510500` −27.30、`159948` −8.80、`511810` −1.80、
> `515450` +2.00、`588890` −2.30、`160128` 0.00。
>
> 被追问口径时一句话答:**"今天收盘值多少钱 − 昨天收盘值多少钱 − 今天买入花的钱 + 今天卖出收回的钱"
> (不含手续费);基准优先用行情(和同页的"最新价/市值"同源,客户能自己核对),
> 行情只有一天的产品回退用净值。** 语义上是"**最近一个交易日**"而不是盘中实时。
> 想现场证明它没算错:`python tools/check_today_profit_loss.py`(纯 SQL 独立复算并逐只比对)。
> 详见 `docs/演示用/今日盈亏实现说明-2026-09-14.md`。
### 场景 3 · 客户下单(1.5 min)⭐ 重点
| | |
|---|---|
| **打开** | 客户页 → 交易记录 → 或直接用接口 |
| **说明** | 前端下单表单待完善,本场用接口演示更直观(见下) |
| **打开** | 客户页 → 资产总览 → 「去下单」(**页面本身就有下单弹窗**),或直接用下边的接口 |
| **说明** | 弹窗里填基金代码 / 方向 / 数量即可;接口方式更直观,二者等价 |
```powershell
# 演示用:买入 100 份沪深300ETF
@@ -160,15 +187,19 @@ Invoke-RestMethod -Uri http://127.0.0.1:8000/api/v1/users/me/orders -Method Post
-Body $body -ContentType application/json | ConvertTo-Json -Depth 4
```
**预期**:`status=已成交`、`executed_price=4.579`(**真实行情价**)、
成交金额 457.90、手续费 0.05、实际扣款 457.95(`net_amount`)。
**预期**:`status=已成交`;`executed_price` = **当时的最新行情价**(演示前先刷一次行情,
按页面/返回里的真实数字念,**别背固定价**);成交金额 = 100 × 该价格;
再扣一笔手续费(按费率规则),`net_amount` 就是实际扣款。
**这体现什么**:
- **成交价来自外部行情**,不是模拟随机数;
- 下单前会做**适当性校验**(风险等级匹配),不匹配直接拒绝;
- 成交后持仓、资金流水、成交明细**三处同时落账**(可当场刷新场景 2 的页面看变化)。
- 成交后委托、成交、资金流水、持仓**四处同时落账**(可当场刷新场景 2 的页面看变化)。
> ⚠️ 如果返回 `503 FUND_QUOTE_UNAVAILABLE` → 跑 §0.2 的行情同步。
>
> ⚠️ 幂等键只防"同一次请求被重发",**在页面上连点两下仍会下两笔**(前端每次提交生成新键,
> 属已知前端缺口)。演示时点一次就好。
### 场景 4 · 客服对话与转人工(1 min)
@@ -185,18 +216,25 @@ Invoke-RestMethod -Uri http://127.0.0.1:8000/api/v1/users/me/orders -Method Post
| | |
|---|---|
| **打开** | <http://127.0.0.1:8000/portal/employee-console/login/> → `risk_t` / `666666` |
| **预期** | 概览:未闭环预警数、高风险数、待处理、已超时 |
| **预期** | 概览:未闭环预警数、高风险数、待处理、已超时(**2026-09-14 实测:未闭环 7 条 / 预警总数 32 条**) |
> 队列空或想现场造一条:页面上的**「扫描」按钮**会同步跑一次风控扫描(最长 60 秒)。
> 也可以跑 `python tools/e2e_smoke_test.py` —— 它会自动造一条待处理预警并跑完整闭环。
**操作顺序(这是核心闭环)**:
1. 点预警队列里的某条 → 打开详情;
2. 详情里有**八类证据**入口:客户、产品、交易、资金流水、持仓、登录记录、预警、通知;
3. 点「确认接收」→ 状态变为已确认;
4. 点「进入调查」→ 状态变为调查中;
5. 点「完成结案」→ 状态变为**已结案**,并且**按风险等级扣减客户行为分**。
3. 点「确认接收」→ **处置状态不变**,只把"确认状态"置为已确认(记录谁、何时看到了这条预警);
4. 点「进入调查」→ 处置状态:待处理 → 调查中;
5. 点「完成结案」→ 状态变为**已结案**(只有"调查中"的才能结案),并且**按风险等级扣客户行为分**:
低 3 / 中 5 / 高 20(行为分 0–20 封顶)。
> 另有两个动作可以按需插入:**关闭误报**(→ 已排除,必须填理由)与**升级处理**
> (只打"已升级"标记,**不改处置状态**)。
**这体现什么**:预警处置是**有状态机、有留痕、有连带影响**的 ——
不是改个字段。每一步都记审计(谁、何时、做了什么)。
不是改个字段。每一步都记审计(谁、何时、做了什么),结案还会影响客户行为分。
> 若队列里没有「待处理」的预警(都已被处置过),跑
> `python tools/e2e_smoke_test.py` —— 它会自动造一条待处理预警并跑完整闭环。
@@ -215,24 +253,36 @@ Invoke-RestMethod -Uri http://127.0.0.1:8000/api/v1/users/me/orders -Method Post
| | |
|---|---|
| **操作** | 风控工作台 → 日报 → 生成 |
| **预期** | 文本**流式**逐段出现(SSE),完成后可填多个邮箱发送 |
| **预期** | 文本**流式**逐段出现(SSE),一共 9 段;完成后可填多个邮箱发送 |
**这体现什么**:长任务走 SSE 流式,而不是让页面转圈等 30 秒。
> 结尾会标来源:模型可用时是"大模型生成",不可用时**自动降级为规则模板并如实标注**——
> 降级不是报错,被问到就这么讲。
> ⚠️ **发送邮件默认是"演练"**:本机 SMTP 开关默认关着、dry-run 默认开着,所以流程会走完、
> 状态会返回,但**不会真的投递**。要真发得先配 SMTP。
### 场景 7 · 投顾工作台(1 min)
| | |
|---|---|
| **打开** | <http://127.0.0.1:8000/portal/employee-advisor/dashboard/> |
| **账号** | `advisor_t` / `abc12345` |
| **预期** | 一张「投资目标方案书 · 客户 9001」卡片,含发布时间与方案内容 |
| **预期** | 概览四张卡 + 客户区(**页面内置 4 位演示客户**:张总/李阿姨/王工/陈医生,对应真实客户号 9101–9104)+ 7 个动作按钮 + 「已发布交付物」 |
**这体现什么**:投顾看到的是**自己服务的客户**的已发布交付物。
数据范围按 `sys_customer_assignment` 归属关系判定(不是按 `data_scope` ——
那会把 all 级权限放大成"看全部客户")。
**这体现什么**:投顾看到的是**自己服务的客户**的交付物。
数据范围按 `sys_customer_assignment` 归属关系判定(不是按 `data_scope` —— 那会把 all 级权限放大成"看全部客户"),
并且有**灰度开关**:没开放的账号直接 403。
> 方案书的**审核与发布都要求管理员**(`admin=True`),投顾自己发不出来。
> 这是有意的复核环节,不是缺陷。
选一个客户点「生成推荐方案」,结果区会给出方案与**六步流水线**进度。
若候选池为空,页面会**把原因写清楚**(证据缺失/流动性不够/适当性不匹配/没有客户目标)+ 计数 + 两条出路。
> **审核与发布的口径(2026-09-14 更新,别再照旧话术讲)**:
> - **产品推荐方案**:**投顾可以自助审核 / 驳回 / 发布给客户**(本次已开放),管理员复核队列同样能做;
> - **投资目标方案书**:投顾做到"录入目标 → 确认目标 → 查看方案书"为止,**审核与发布仍归管理员**。
>
> 没有后端也能演:网址加 `?demo=1` 进**离线演示模式**(本地引擎、无令牌、页面明确标注
> "演示模式 · 本地引擎"、方案标"规则模拟,不构成真实推荐")。**用它时务必主动说明这是演示模式。**
### 场景 8 · 管理员治理(1.5 min)
@@ -240,21 +290,27 @@ Invoke-RestMethod -Uri http://127.0.0.1:8000/api/v1/users/me/orders -Method Post
|---|---|
| **打开** | <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) |
| 角色与权限 | 五个角色的权限数(**2026-09-14 实测**):admin **62** / advisor **31** / customer **26** / risk_operator **10** / operator **7**;还能按用户 ID 查"服务端实际解析出来的角色与数据范围" |
| 配置与模型 | 工具白名单、提示词**走发布状态机**(草稿 → 待审核 → 已审核 → 生效,可回滚;被取代的版本落 superseded),不是改配置文件;模型路由规则也在这里 |
| 审计记录 | 每一次权限判定、工具调用都有记录;没有"看敏感详情"的权限时,详情由服务端**脱敏** |
| 转人工工单 | 场景 4 建的工单在这里(**当前库里 40 条**);**不返回客户标识与原始对话**(二次脱敏) |
| 画像候选 | 客服从对话里提炼的画像候选,批准后才进正式记忆 |
| 投顾复核 | 待审的投顾交付物(推荐方案 + 投资方案书)→ 审核通过 / 驳回 / 发布给客户 |
| 知识库 | 上传文档(faq / product / policy)→ 自动切分入库并投向量同步;**客服就是靠它作答**(当前 active 知识 24 份、知识元数据 199 行) |
| 画像漂移复核 | 客户重新测评导致画像变化时人工确认是否采纳 |
**这体现什么**:**权限变更必须过审核流程并留痕** —— 这是金融场景的硬要求,
所以平台刻意**不提供**"直接改权限"的接口。
> ⚠️ 三个"前端未接、后端已有"的小缺口,被问到照实说:配置发布页没有"驳回/回滚"按钮、
> 模型端点页是只读的(当前 3 个端点)、审计页没有筛选框(只看最近 20 条)。
---
## 2. 备选场景(时间充足时)
@@ -262,9 +318,11 @@ Invoke-RestMethod -Uri http://127.0.0.1:8000/api/v1/users/me/orders -Method Post
| 场景 | 怎么演 | 体现什么 |
|---|---|---|
| 配置发布四态 | 管理员工作台建一个草稿版本 → 提交校验 → 审核 → 激活 | 改配置是走流程的,可回滚 |
| 五角色权限对比 | 用五个账号分别登录,看入口守卫各跳哪里 | 越权在**前端就被挡**,服务端还有一层 |
| 五角色权限对比 | 用五个账号分别登录,看入口守卫各跳哪里 | 前端只是体验层,真正的拦截在服务端权限码与数据范围 |
| 知识入库 | `python tools/seed_knowledge_demo.py` 后复测场景 1 | 知识是**可运营**的,不是写死的 |
| 平台体检 | `python tools/e2e_smoke_test.py` | 40 项全绿,交付质量可自证 |
| 记忆链路 | 客户连问几轮 → 看 `memory_unit` 里新增长期记忆 → Worker 投影到 Neo4j/Milvus | "记住客户"是可查的,不是模型自己说记住了(见 `docs/演示用/记忆架构与Worker工作流程-大白话版.md`) |
| 后台排障 | 停掉 Worker → 客服变"繁忙" → 查 `agent_run.status='queued'` → 起回 Worker 自动恢复 | 异步三段式的可观测性 |
---
@@ -290,27 +348,25 @@ A:三层 —— 前端按权限隐藏入口;服务端每次请求**重新解
**Q:为什么不让管理员直接改权限?**
A:金融场景要求权限变更留痕可追溯,所以统一走配置发布流程(草稿→校验→审核→激活)。
**Q:客户看板上的"今日盈亏"为什么是 0?**
A:**这项本期是占位值,不是真实算出来的**,不回避这一点。
代码里 `today_profit_loss` / `today_profit_loss_ratio` 目前是**硬编码 `ZERO`**
(`app/service/trade_service.py` 的持仓列表 L678、账户看板 L748–749);
而它旁边的**"持有盈亏"是真算的**(`market_value − cost_amount`,L660 / L726)。
所以「持有盈亏 / 总市值 / 可用资金」可以照着讲,**"今日盈亏"不要当成真实数字念**。
**Q:客户看板上的"今日盈亏"是怎么算的?**
A:**2026-09-14 起是真实计算的**(此前是占位 0,已修)。一句话口径:
是否本期实现**已列为待业务确认项**(`docs/软件需求文档-2026-09-14.md` **Q22**),
并且已经实测过两条口径的可行性:
```
今日盈亏 = 今日市值 − 昨日持仓市值 − 今日买入金额 + 今日卖出金额 (不含交易费用)
昨日持仓数量 = 今日持仓数量 − 今日买入份额 + 今日卖出份额 (由当日成交反推)
```
| 口径 | 数据现状 | 结论 |
|---|---|---|
| 用行情算(今收 − 昨收) | `fin_market_price` **只在同步时才写行**,实测每个产品**只有 2 行** | ❌ 取不到连续"昨收" |
| 用净值算(今净值 − 上一交易日净值) | `fin_nav_history` 每个产品 **120~160 行连续交易日净值** | ✅ 建议按这个口径做 |
基准**优先用行情**(最近两个交易日收盘价,与同页"最新价/市值"同源、客户能自己核对),
行情只有一天的产品(演示数据里 `15911` / `159991-159995`)**回退用净值**。
语义是「**最近一个交易日**相对前一交易日」的盈亏,**不是盘中实时** —— 这句一定要讲,
否则会被当成 bug。
> ⚠️ 同时要讲清语义:净值是**日终**数据,所以它其实是"**最近一个交易日**的盈亏",
> **不是盘中实时** —— 这一点业务确认时要一并定下来。
>
> 💡 时间紧、不想被追问的话:**场景 2 讲解时避开"今日盈亏"这一栏**,
> 只讲持有盈亏、总市值、可用资金。万一被直接问到,就按上面这段答,
> **不要含糊过去也不要现编** —— 它属于"已知且已登记的范围边界",不是缺陷。
**现场可自证**:`python tools/check_today_profit_loss.py`(纯 SQL 独立复算,与接口逐只比对,
不一致会以退出码 1 结束)。实测:`cust_t` = −132.70(−0.2528%)。
细节见 `docs/演示用/今日盈亏实现说明-2026-09-14.md`。
> 💡 当天买卖会让"今日盈亏"包含当日已实现部分(卖出份额仍算"昨收→卖出价",
> 当日买入的份额只算"买入价→今收")。这是刻意的,不是算错。
---
@@ -323,6 +379,8 @@ A:**这项本期是占位值,不是真实算出来的**,不回避这一点
| **所有页面一起变慢/卡住**(不是某一个请求超时,而是客服、风控、看板**同时**迟钝) | 检索层用的是**同步** Milvus 客户端,单次 `search` 卡住会**阻塞整个 API 进程**(连 Worker 心跳一起停)—— 已知项,见 `docs/演示用/代码库全面审查报告-2026-09-14.md` **P1-2** | 先在 Docker Desktop 确认 Milvus 在跑;**不用改代码也不用重启** —— Milvus 恢复后自动正常。这条是「演示版本不修、但要能一眼认出」的典型:平时完全无感,只有 Milvus 抖动时才现形 |
| 改了页面看不到效果 | 浏览器缓存 | `Ctrl+F5` 硬刷新 |
| 知识库问答答不上 | 向量没同步 | 确认 Worker 在跑,然后跑 `tools/seed_knowledge_demo.py` |
| 场外发送通知"成功"但对方没收到 | **SMTP 默认是演练模式**(开关关、dry-run 开) | 这是刻意配置:要在页面上演示"发送"就走完了;要真发得先配 SMTP(见 §5 已知空数据) |
| 风控扫描报"正在扫描中/忙" | 手工扫描与定时扫描**共用同一把数据库咨询锁**,互斥 | 等这一轮结束再点(定时扫描默认是关闭的,一般遇不到) |
| 平台起不来 | MySQL 没起 | 看 `start.ps1` 的依赖检查输出 |
| 风控队列没有"待处理"预警 | 演示样本都被处置过了 | `python tools/e2e_smoke_test.py`(自动造一条) |
@@ -363,15 +421,20 @@ python tools/e2e_smoke_test.py
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 # 全量体检(6 条线 40 项)
python tools/e2e_smoke_test.py --read-only # 只读体检,不动数据
python tools/check_today_profit_loss.py # 「今日盈亏」纯 SQL 复算并与接口逐只比对
python tools/portal_api_check.py # 接口契约体检(按前端的方式调,41 项)
```
**已知的空数据**(演示时别点进去尴尬)
**已知的空数据 / 会"看着像坏了"的地方**(演示时别点进去尴尬;**2026-09-14 实测**)
| 位置 | 现状 |
|---|---|
| 运营工作台 | 场外邮件 0 条、邮箱未初始化(需要真实邮件源) |
| 管理员 · 画像候选 | 0 条(需要客户对话触发画像抽取) |
| 位置 | 现状 | 原因 |
|---|---|---|
| 运营 · 场外邮件 | **当前 4 封**(不是 0 了),但**不会有新的** | 收件依赖真实可用的邮箱配置与常驻 Worker;发件还依赖 SMTP |
| 运营 · 通知发送 | 只"演练",不真发 | SMTP 开关默认关闭 + dry-run 默认开启 |
| 管理员 · 画像候选 | 0 条 | 需要客户对话触发候选抽取,且要管理员批准才进正式记忆 |
| 投顾 · 客户列表 | 页面内置 4 位演示客户 | 前端暂无"查我的客户"接口,真实范围在服务端按归属判定 |
| 风控 · 未闭环预警 | **7 条**(总数 32) | 想造新的就点页面上的「扫描」 |
**端口**:API `8000` / MySQL `3306` / Redis `6379` / Milvus `19530`
**端口**:API `8000` / MySQL `3306` / Redis `6379` / Milvus `19530` / Neo4j `7687`