新增《全平台功能流程 · 大白话版》:从登入讲到六门户全部流程

- 读者是第一次接触平台的人:全文不用"链路/编排/收敛"这类词,讲"点一下之后发生了什么"
- 结构:三个角色(浏览器/API/Worker)→ 访客 → 登入与角色落地 → 客户 8 页 → 风控 →
  投顾 → 运营三条线 → 管理员 8 个页签 → 底座 9 件事 → 一条主线时间线 → 常见疑问 → 速查
- 每节都标了文件/接口依据,并写清"已知空数据"与"已知小偏差"(通知类型下拉未接线、
  NL2SQL 错误信息列恒为 --、推广页无 form 导致 required 不生效、场外默认 dry-run 等)
- 顺带纠正三处易误传的说法:幂等键只防"同一次请求重发"(连点仍会下两笔)、
  客户侧前端权限门是装饰性的(后端 403 才是拦截)、FM-03 熔断目前只在前端拦
- 风控演示数据那 6 行 RISKDEMO 场外申购/赎回:代码注释与今日盈亏说明改口径为
  "风控异常交易演示的触发材料,刻意保留",不再当脏数据;今日盈亏按 transaction_type 语义排除它们
This commit is contained in:
2026-09-15 00:10:23 +08:00
parent 9dd2802d67
commit 776b504ee6
3 changed files with 723 additions and 10 deletions
+6 -5
View File
@@ -274,11 +274,12 @@ class TradeService:
) -> _TodayTradeFlow:
"""基准日当天该客户在该产品上的成交汇总(用于反推昨日持仓数量)。
**只认本平台的场内成交**(`transaction_type` ∈ {买入, 卖出})。演示库里
`fin_transaction` 混进了风控演示用的场外申购/赎回(`RISKDEMO-*`,`transaction_type`
是「申购」「赎回」,且不改持仓表):把它们算成当日买卖会让「昨日持仓数量」凭空多出
几十万份、今日盈亏变成几万元(实测 12001 从 −21.22 变成 −1612.72)。
`AGENTS.md` 规则 8 也要求场外流程不写场内表 —— 这里按类型甄别,不依赖编号前缀。
**只认本平台的场内成交**(`transaction_type` ∈ {买入, 卖出})。演示库里 6 行
`RISKDEMO-*` 记录(`transaction_type` 是「申购」「赎回」)是**风控「异常交易」演示的
触发材料,刻意保留**,不是真实成交:它们不改持仓表,份额也与持仓对不上
(客户 12001 实际持有 10000 份,那条"赎回"写的是 750000 份)。
把它们当当日买卖会把「昨日持仓数量」算成 76 万份、今日盈亏从 −21.22 变成 −1612.72。
这里按 `transaction_type` **语义**甄别,不依赖编号前缀 —— 换一批演示数据也不用改代码。
"""
rows = (
(
@@ -52,14 +52,17 @@
会拿到**同一天的两行**,把"昨收"算成"今收"(盈亏恒等 0)。
→ 实现里取 8 行**按日期去重**后再取两日(`_latest_two_trading_days` / `_latest_two_nav_days`)。
### 3.2 演示库里混进了场外申购/赎回
### 3.2 演示库里混进了场外申购/赎回(**刻意保留的演示材料**)
`fin_transaction` 里有 6 行 `transaction_no` 形如 `RISKDEMO-*`、`transaction_type` 是
「申购」「赎回」的记录(风控演示用,**不改持仓表**,违反 `AGENTS.md` 规则 8 但已在库里)。
它们 `order_side` 是 `buy`/`sell`,若按 `order_side` 汇总,客户 12001 的"昨日持仓数量"
会被抬到 **760000 份**,今日盈亏从 `-21.22` 变成 **`-1612.72`**(多出两个数量级)。
「申购」「赎回」的记录。**它们是风控「异常交易」演示的触发材料,是有意保留的**,
不是脏数据、也不是漏删:风控专员那边的预警队列要靠它们才能造出"大额赎回""频繁申购"这类场景。
它们**不代表真实场内成交**(不改持仓表:客户 12001 实际持有 10000 份,那条"赎回"写的是 750000 份)。
问题在于:若按 `order_side` 汇总当日成交,客户 12001 的"昨日持仓数量"会被抬到 **760000 份**,
今日盈亏从 `-21.22` 变成 **`-1612.72`**(多出两个数量级)。
→ 实现里**只认 `transaction_type` ∈ {买入, 卖出}**(本平台场内下单的唯一写入值),
按**语义**甄别而不是按编号前缀。
按**语义**甄别而不是按编号前缀 —— 这样演示材料可以继续保留,换一批演示数据也不用改代码。
### 3.3 货币 ETF 的净值与价格不同量纲
@@ -0,0 +1,709 @@
# 全平台功能流程 · 大白话版(2026-09-14)
> **读者**:第一次接触这个平台的人(演示听众 / 接手的人 / 业务方)。
> **写法**:不用"链路""编排""收敛""双写"这类词,讲的是"**点一下之后到底发生了什么**"。
> 每一句说法都能在代码或数据库里查到,关键处标了文件路径,方便你反查。
> **配套**:想照着演一遍看 `docs/44-演示流程.md`;只想看「今日盈亏」看 `今日盈亏实现说明-2026-09-14.md`。
---
## 0. 三十秒版:这平台到底是什么
1. 它是一个**基金业务的"通用 Agent 平台"**:把"会话、登录鉴权、记忆、模型调用、工具调用、
合规校验、审计留痕、配置发布"这些**每个业务都要用**的东西做成底座,
上面挂着 **7 个业务 Agent** 和**一条不做 AI 的模拟交易服务**。
2. **不做真实交易**:场内基金是**模拟盘**(钱是假钱),但**行情是真行情**(取自公开数据源)。
场外基金那条线只做"运营流程"(收邮件、认单据、发通知),同样不下真实单。
3. 对外一共**六个门**(门户),每个角色一个:
| 门户 | 地址 | 谁能进 | 一句话作用 |
|---|---|---|---|
| 访客 | `/portal/guest/home/` | 任何人(不用登录) | 看产品、问客服 |
| 客户 | `/portal/customer/login/` | 客户 | 看资产、下单、看流水、做风险测评 |
| 风控 | `/portal/employee-risk/dashboard/` | 风控专员 | 处置预警、出日报 |
| 投顾 | `/portal/employee-advisor/dashboard/` | 投顾 | 给自己服务的客户出方案 |
| 运营 | `/portal/employee-operations/offsite/` | 运营 | 场外收件、推广材料、问数(NL2SQL) |
| 管理员 | `/portal/employee-console/workspace/` | 管理员 | 权限、配置发布、审计、工单 |
> 员工侧**只有一个登录页**:`/portal/employee-console/login/`。
> 登录后系统按你的角色把你送到上面四个工作台里的某一个(见 §3)。
> 客户侧有**自己的**登录页 `/portal/customer/login/`,员工账号在客户登录页会被拒绝,反之亦然。
---
## 1. 跑起来一共三个"人":浏览器、API、Worker
看懂这三者的分工,后面所有的"为什么没反应"都能自己判断。
| 角色 | 是什么 | 少了它会怎样 |
|---|---|---|
| **浏览器** | 六个门户的页面(原生 JS,没有前端框架)。它只做两件事:调接口、把结果画出来 | 没有界面(但接口还能用 curl 调) |
| **API** | `python -m uvicorn app.main:app --port 8000`。所有接口 + 所有页面都从它出 | 什么都没有 |
| **Worker** | `python -m app.worker`。**后台慢慢干的活**都归它 | 客服对话永远"繁忙/超时";新知识搜不到;记忆不更新;**而且它不跑通常一声不响** |
**Worker 具体干哪些活**(都在 `app/worker/runtime.py` 的 `run_once()` 和
`app/worker/__main__.py` 里,一轮一轮轮询):
1. **Agent 运行**:把"排队中"的 Agent 请求领出来真正执行(**这一条最关键**,见下);
2. 领域事件投递(`domain_event_outbox`):事件先落库、再由它一条条消费,失败就重试、重试到头进死信;
3. 知识向量同步:把知识库内容写进 Milvus(不写进去,客服就检索不到);
4. 记忆抽取与画像重建:从对话里提炼长期记忆 → 重建画像 → 投到 Neo4j(关系)与 Milvus(向量);
5. 会话片段聚合(把若干轮对话合成一个"事件片段",供长期记忆用);
6. 清理派生数据:记忆失效/销户时删掉图库与向量库里的副本,删不掉就如实记"跳过",**不假装删了**;
7. **场外收件**:`OffsiteMailWorker` 在**同一个进程、同一个入口**里跑(邮箱收信、认单据)。
> ⚠️ **"客服一直转圈/超时" 十有八九是 Worker 没在跑**。
> 因为 Agent 请求是**三段式**:API 只负责受理并返回 `202`("收到了")→ 请求进队列
> (`agent_run.status='queued'`)→ **Worker 领单才真正执行**,结果再落库。
> 没有 Worker,队列里那行就一直停着,而页面只会显示"客服繁忙/响应超时"——
> 看起来像慢,其实是**没人处理**。一句话排查:查 `agent_run` 最新那行是不是 `queued`。
**数据库和中间件**(都在本机):
| 组件 | 端口 | 干什么 |
|---|---|---|
| MySQL | 3306 | **唯一权威数据源**,90 张表 |
| Redis | 6379 | 配置缓存、客服会话短期上下文 |
| Milvus | 19530 | 向量检索(知识库片段、长期记忆向量) |
| Neo4j | 7687 | 客户画像的**关系**(标签、产品、多跳),是派生视图,**一律以 MySQL 为准** |
**一键启动**:双击桌面的 `启动金融Agent平台.bat`(或 `powershell -File start.ps1`)——
它按顺序做六件事:找解释器 → 检查 MySQL/Redis/Milvus → 刷行情 → 起 **API 与 Worker 两个窗口**
→ 等 API 真正应答 → 自动打开浏览器。**重复双击是安全的**,不会起第二份。
⚠️ 那个 bat 必须同时满足"GBK 编码 + CRLF 换行 + 无 BOM",所以**别手写**,
改完 `start.ps1` 后跑 `python tools/make_launcher_bat.py` 重新生成。
---
## 2. 第一眼:访客门户(不用登录就能看)
打开 <http://127.0.0.1:8000/portal/>,它会跳到 `/portal/guest/home/`。三个页面:
| 页面 | 看到什么 | 数据从哪来 |
|---|---|---|
| 首页 | 平台的宣传页 + 推荐位(前 3 只基金) | 推荐位来自 `GET /api/v1/products`,其余静态 |
| 产品列表 | 场内基金清单、最新价、"最近涨跌";还能切"基金排行"视图 | `GET /api/v1/products`(真实接口,不是写死的假数据) |
| 产品详情 | 产品要素 + **净值走势图** | 净值来自 `fin_nav_history`(当前库里 26 只产品、3253 个净值日) |
**"最近涨跌"有个刻意的设计**:这个数**只显示行情源直接给的涨跌幅**。
源没给、或这只产品走的是净值降级路径时,页面显示"**暂无**"而**不是 0** ——
因为 0 会被读成"今天平盘",那是编数据。
**右下角客服浮窗**(本平台最值得看的 AI 能力,因为它"会拒答"):
1. 你发一句话 → 平台先判断**意图**(是问费率?问规则?还是问账户?);
2. 命知识类意图 → 去**已发布的知识**里做向量检索;
3. 有把握才答:命中分 ≥ 0.75 直接答;0.55~0.75 之间还要求"领先第二名 ≥ 0.07";否则不答;
4. 答的时候**带引用来源**,你可以点开看出自哪份材料;
5. 答不了 → 引导你**转人工**(点"转人工客服"才建工单);
问"我的账户里有多少钱"这类**要登录才能查**的问题 → 它会明确说"需要登录后查询",**不编数字**。
**它是"异步 + 轮询",不是"一问一答的同步调用"**:前端把问题交上去,接口立刻返回 `202`(收到了),
然后每 0.5 秒问一次"好了吗",最多问 40 次(约 20 秒)。所以**Worker 没在跑时**,
你看到的就是那句"客服繁忙,暂时没能给出答复"——不是模型慢,是没人处理。
(平台也提供了更省事的 SSE 推送端点,但浮窗目前用的是轮询。)
> 访客没有账号也能用,是因为平台给了它一张**范围极小的临时通行证**
> (`POST /api/v1/visitor-tokens`,只带 `knowledge:query` 等最小权限)。
> 客户/员工登录后走的是另一套令牌,权限大得多。
>
> 两个"知道就好"的前端细节:产品详情页如果 `?code=` 写错了,页面**不会报错**,
> 而是静默展示第一只基金;「资金流水」页因为导航配置里 `active` 是空串,
> **顶部导航不会高亮**它(功能正常,只是不好看)。
---
## 3. 登录:我该去哪个门、进去之后能看到什么
### 3.1 一次登录到底做了什么
1. 你在登录页填用户名 + 密码,前端调 `POST /api/v1/auth/tokens`(端点代号 **A034**);
2. 接口返回你的**角色列表**(roles);
3. 前端做一次**门禁检查**:客户登录页只接受 `customer`;员工登录页只接受
`risk_operator / operator / advisor / admin / super_admin`。角色的门走错了会被拦下,
并明确提示"该账号不是客户账号,请使用后台登录入口"(`app/static/portal/common/login-controller.js`);
4. 登录成功 → 令牌写进 **cookie(`auth_token`)+ sessionStorage 缓存**,
跨标签页以 cookie 为准;
5. 按角色**送到不同的工作台**(下表);客户如果**还没做风险测评**,会被强制先跳到问卷页
(`GET /api/v1/onboarding/risk-questionnaire` 返回 `required=true` 时);
6. 令牌失效/换账号 → 自动跳回登录页并带 `?reason=session-expired` 之类的提示。
### 3.2 角色 → 落地页(`app/static/portal/common/auth.js` 的 `staffHomeForRoles`)
| 角色 | 登录后落到 |
|---|---|
| `customer`(客户) | `/portal/customer/dashboard/`(未测评则先到 `/portal/customer/risk-questionnaire/`) |
| `risk_operator`(风控专员) | `/portal/employee-risk/dashboard/` |
| `advisor`(投顾) | `/portal/employee-advisor/dashboard/` |
| `operator`(运营) | `/portal/employee-operations/offsite/` |
| `admin` / `super_admin`(管理员) | `/portal/employee-console/workspace/` |
### 3.3 为什么"令牌里不带权限"
**令牌里只有"你是谁",没有"你能干什么"。** 每次请求到达服务端时,
平台**重新查一遍**你的角色、权限码和**数据范围**。这么做换来两件事:
- 管理员改了权限,**下一次请求立刻生效**,不用等你重新登录;
- 令牌被别人拿到,也不能靠它越权——服务端照样按当前权限判。
### 3.4 越权有三道防线,但**只有后两道是真的在拦**
| 防线 | 在哪 | 实际强度 |
|---|---|---|
| 第一道:前端隐藏/禁用入口 | 各门户页面(员工工作台会调 `A038` 拿服务端角色与权限后再刷按钮) | **体验层,不是安全层**。而且客户门户这一道目前是**装饰性的**:登录响应里不含权限清单,前端对客户角色直接按"全量权限"渲染,所以 `data-permission-required` 恒为"已授权"。**别把它当防线。** |
| 第二道:权限码 | 服务端每个接口都要一个权限码,比如下单要 `trade:order:create` | **真拦**。没有就 403(`AGENT_PERMISSION_DENIED` 一类) |
| 第三道:数据范围 | `data_scope` + **客户归属关系**(`sys_customer_assignment`) | **真拦**。有"看客户"的权限,也只能看**分配给自己**的客户 |
> 换句话说:**能不能点,看前端;能不能成,看后端。** 判断"某人到底能不能做某件事",
> 一律以服务端为准;前端只是提前把不该看到的藏起来。
>
> 第三道是最容易被忽略、也最关键的一道:**员工号和客户号是同号段的**
> (演示数据里客户 9001-9020、员工 9002/9020 并存)。
> 如果只按"号"去查,风控/投顾就会读到**陌生客户**的长期记忆,而且不会报错。
> 所以平台把"能读哪些客户"**收敛到一个文件**(`app/core/memory_scope.py`):
> 客户只读自己;员工只读分配给自己的客户,且单次最多 10 个;归属没维护就**失败关闭**。
---
## 4. 客户门户:八个页面,一页一句
| 页面 | 地址 | 干什么 | 主要接口 |
|---|---|---|---|
| 登录 | `/portal/customer/login/` | 登录 | A034 |
| **资产总览** | `/portal/customer/dashboard/` | 总资产、持仓市值、累计盈亏、**今日盈亏** | T001 |
| **我的持仓** | `/portal/customer/holdings/` | 每只基金的份额、成本、最新价、市值、持有盈亏、今日盈亏 | T006 |
| 委托 | `/portal/customer/orders/` | 我下过的委托及状态(**页面上没有撤单按钮**:市价单下单即成交,能撤的极少) | T003 / T004(撤单接口 T005 已就绪但页面未接) |
| 成交 | `/portal/customer/transactions/` | 已经成交的每一笔(价格、数量、费用) | T007 / T008 |
| 资金流水 | `/portal/customer/cash-ledger/` | 每一笔钱的进出与余额变化 | T009 |
| 盈亏分析 | `/portal/customer/profit-loss/` | 累计盈亏 / 今日盈亏 + 按持仓拆分 | T001(复用看板数据) |
| **风险测评** | `/portal/customer/risk-questionnaire/` | 填问卷,决定你的风险承受等级(C1–C5) | ONB001 / ONB002 |
### 4.1 下单一笔,中间发生了六件事(这是全平台最该讲清楚的一条)
1. **你点了买入** → 前端调 `POST /api/v1/users/me/orders`(**T002**),
并且必须带一个**幂等键**(`Idempotency-Key`,16–128 个可打印 ASCII 字符)。
它的作用是:**同一次请求被重发(超时重试、网络抖动)时只成交一笔**。
⚠️ 注意边界:**它防的是"同一次请求的重发",不防"你又点了一次"**——
前端目前每提交一次就生成一个新键,所以手动连点两下**确实会产生两笔委托**。
要防这个得让前端为"一次下单意图"复用同一个键,属于**已知的前端缺口**。
2. **服务端先看行情**:取这只基金的最新行情快照,要求它在 **15 分钟内**。
超过 15 分钟直接拒绝(`503 行情已过期`)——**这是演示最容易翻的一环**,
因为系统**没有任何自动刷新**,补刷命令是 `python tools/sync_market_prices.py`(立即生效,不用重启)。
3. **适当性校验**:你的风险等级(C1–C5)和这只基金的风险等级(R1–R5)必须匹配,
不匹配直接拒单。这就是"**风险测评不是形式**"的落地处。
4. **算钱**:按 `fin_fee_rule` 里优先级最高且生效的费率规则算手续费,
成交时把费率**固化**进这笔成交记录(以后改费率不影响历史)。
5. **落账(同一个数据库事务里同时写四处)**:委托、成交、资金流水、持仓。
任何一处失败,整笔回滚。所以这三页(资产/持仓/流水)看到的数字**永远对得上**。
6. **成交价来自外部行情**,不是随机数;订单状态直接是"已成交"——
因为首版是**市价委托立即全额成交**,没有撮合排队。
> 顺带解释一个常被当 bug 的现象:**撤单几乎总是失败**。
> 因为市价单下单即成交,只有停在"待风控"的委托才能撤,所以页面点撤单大概率提示"该委托已成交"。
### 4.2 「今日盈亏」是怎么算的(2026-09-14 起是真实数据)
一句话:**今天收盘时值多少钱 − 昨天收盘时值多少钱 − 今天买入花掉的钱 + 今天卖出收回的钱**
(不含手续费,和旁边的"持有盈亏"同口径)。
两个值得在演示时说清的细节:
- 当天买的份额**只算"买入价 → 今收"**,不享受昨天到今天那段涨幅(昨天它还不是你的);
- 行情只有一天的产品会**回退用净值**算,所以不会出现"某只永远是 0"。
完整口径、三个数据坑、逐只复算命令见 `今日盈亏实现说明-2026-09-14.md`。
---
## 5. 风控专员工作台(预警的"从哪来 → 怎么处置 → 怎么闭环")
**进门**:从员工登录页登录后自动落到 `/portal/employee-risk/dashboard/`;
页面加载时先调 `A038` 把**服务端真正解析出来的**角色、权限、数据范围拿回来,
再据此决定哪些按钮可点(这就是 §3.4 说的"第一道只是体验层")。
### 5.1 上半屏:概览四张卡
| 卡片 | 含义 |
|---|---|
| 未闭环预警 | 还没走完处置流程的预警总数 |
| 高风险 | 其中等级为"高风险"的数量 |
| 待处理 | 状态还是"待处理"的数量 |
| 已超时 | 超过处置时限还没动的(**大于 0 会标红**) |
### 5.2 预警是从哪来的
两条路:**页面上点"手动扫描"**(`RK006`,同步执行、最长 60 秒),
或**定时调度**(`app/worker/risk_scan_scheduler.py`,是个独立的可选进程)。
扫描会按规则(`RW-xxx`)核对交易、资金、持仓、登录等数据,命中就落一条预警。
> **演示数据里那些"异常交易"记录是刻意的**:客户 12001–12005 名下有 6 行
> `RISKDEMO-*` 记录(大额申购/赎回),就是**为了造出"大额赎回""频繁申购"这类预警场景**
> 才写进去的**演示触发材料,有意保留**,不是脏数据、也不是漏删。
> 它们**不代表真实场内成交**(不改持仓表,份额也和持仓对不上)。
> 副作用只有一个:算「今日盈亏」时必须**只认本平台的场内成交**(买入/卖出),
> 否则这些虚拟份额会把"昨日持仓"算到几十万份 —— 详见 `今日盈亏实现说明-2026-09-14.md`。
### 5.3 预警队列与筛选
游标分页(不是页码),**每页固定 10 条**。筛选项:关键词、客户编号、产品代码、
风险等级(高/中/低)、规则编号(必须形如 `RW-001`)。
后端还支持"产品名 + 时间范围"两个筛选,**页面暂未提供输入框**(属可用性缺口,不是故障)。
### 5.4 预警详情:为什么说它"有据可查"
一条预警点开,能看到四层东西:
1. **基本信息**:预警编号、风险等级、客户、产品、处置状态、确认状态、命中规则、是否已升级;
2. **证据摘要**:触发这条预警的那几条关键事实;
3. **七个关联业务区段**:把这个客户在这条线索上的交易、资金、持仓等上下文摆在一起;
4. **证据快照 / 证据归档**:当时的字段原样留存,事后可复查。
另外"证据查询"标签页提供 **8 类只读实体**的直查:客户、产品、交易、资金流水、
持仓、登录记录、预警、通知。**全部是只读的**,改不了任何数据。
### 5.5 处置是"五步状态机",不是改个字段
| 动作 | 效果 |
|---|---|
| **确认接收** | 只记录"谁在什么时候看到了",**状态不变** |
| **进入调查** | 状态:待处理 → 调查中 |
| **关闭误报** | 状态 → 已排除,**必须填理由** |
| **完成结案** | 只有"调查中"的才能结案;**必须填结论**,并且**按风险等级扣客户行为分:低 3 / 中 5 / 高 20**(行为分 0–20 封顶) |
| **升级处理** | 只打"已升级"标记,**不改状态**,可以和上面几步并行 |
每一步都写审计(谁、何时、做了什么)。所以"这条预警谁处理的、依据是什么"事后能查清。
### 5.6 日报与助手
- **日报**:点生成后**逐段出现**(SSE 推流),一共 9 段(当日数量、等级分布、重点事件、
未闭环、误报情况、类型分布、处置结果、规则效果、优化建议)。
末尾会标来源:模型可用时是"大模型生成",不可用时**自动降级为规则模板**并如实标注——
**降级不是报错**。生成完可以填多个邮箱发送(逗号/分号/空格分隔)。
- **风控助手**:预设问法 6 个(通用的 3 个 + 绑定当前预警的 3 个)。
它只会调三个**只读**工具:`get_risk_overview`、`search_risk_alerts`、`get_alert_evidence`,
输出的是"研判与话术草案",**并明确声明不构成任何处置结论,处置一律由人工做**。
- **通知记录**:可以看到每条通知的渠道、发送状态、失败原因。顶部铃铛只统计"站内提醒"。
⚠️ 邮件通道依赖 SMTP 配置,授权码失效时会显示"发送失败"(详见 §12)。
---
## 6. 投顾工作台(给自己服务的客户出方案)
**进门**:员工登录页登录 → 落到 `/portal/employee-advisor/dashboard/`。
投顾能操作的客户范围**由服务端按归属关系限定**(`sys_customer_assignment`:本人 + 分配给我的客户),
拿不到全量客户——这是数据范围,不是页面缺陷。(页面上那份"我的客户"名单**目前是内置的演示数据**,
真正的范围以服务端为准,见 §6.1。)
### 6.1 页面上有什么
| 区域 | 作用 |
|---|---|
| 概览四张卡 | 已发布交付物数量、名下客户数、风评熔断数、会话来源 |
| 客户区 | 客户列表 + 参数 + 7 个动作按钮 + 一句自然语言输入框。⚠️ **这份客户名单目前是页面内置的演示数据**(张总 9101 / 李阿姨 9102 / 王工 9103 / 陈医生 9104);**真正的客户范围在服务端**按归属关系(`sys_customer_assignment`)判定,并且有一个**灰度开关**:没开放的账号直接 403「当前投顾功能尚未对该账号开放」 |
| 操作区 | 7 个动作:**组合分析 / 资产配置 / 生成推荐方案 / 录入客户目标 / 目标确认与方案书 / 画像评分 / 调仓建议** |
| 结果区 | 动作结果 + **推荐流水线六步**进度(① 鉴权 ② 画像 ③ 筛选 ④ 方案 ⑤ 合规 ⑥ 审核留痕) |
| 已发布交付物 | 已发布给客户的方案书/推荐方案(只列"我自己 + 归属我的客户"的,且已发布、倒序 20 条) |
| 助手对话框 | 用自然语言下指令。⚠️ 它走的是**前端正则意图路由**(识别"组合分析/配置/推荐"等词后复用同一套按钮动作),**刻意没有走 Agent 异步链路**——因为异步 run 需要常驻 Worker 与发布配置,投顾工作台要保证离线也能演示 |
### 6.2 三种动作,真实的只有三种(这点要讲清楚)
| 动作 | 走哪里 |
|---|---|
| 组合分析、资产配置、生成推荐方案 | **走真实后端**(`/api/v1/advisor/portfolio-analysis`、`/asset-allocation`、`/recommendations`),并且会带上所选客户的 `customer_id` |
| 画像评分、调仓建议 | 页面**本地演示引擎**算(后端暂无对应接口),数据取自页面内置的演示产品/客户表 |
| 录入客户目标、目标确认与方案书 | 走真实后端(投资目标与方案书接口) |
> 这是**刻意标注**的边界:页面内置了一张演示产品与客户表,只服务"离线演示"和那两个后端没有的动作。
> 演示时如果被问"这个评分是怎么来的",照实说"这一步是前端演示引擎,后端暂未提供该接口"即可。
### 6.3 生成推荐方案:为什么可能"一个产品都没有"
这是最容易被误判成故障的一步。系统不是"随机挑几只",而是**逐道闸门筛**,全过了才进候选:
1. **可交易**:产品状态是"上市"、在沪深交易所、当前在开放窗口内;
2. **适当性证据**:该产品在治理表里有 `verified`(已核验)的适当性记录,机构、来源链接、文档摘要都要齐;
3. **合同证据**:同样要有已核验的合同快照;
4. **不在治理待办里**(有未决事项的产品先排除);
5. **规模**(如果设了门槛);
6. **流动性**:日均成交额要达到门槛(日频 1000 万、7 日内 100 万、30 日内 10 万);
7. **硬性适当性过滤**:按客户风险等级(C1–C5)挡掉不匹配的产品;
8. 剩下的按规则**排序**取前列。
所以"候选池为空"时,页面会**把原因讲清楚**(证据缺失/流动性不够/适当性不匹配/没有客户目标),
并给出两条路(补证据材料,或走管理员审核的既有交付物)——**不是一句"没有通过校验的产品"**。
实测:客户 9101(C4)出的方案里有 3 只 R4 产品,9102(C1)只有 1 只 R1。
### 6.4 审核与发布:哪一步投顾自己能做
| 交付物 | 投顾能做什么 | 谁来做审核/发布 |
|---|---|---|
| **产品推荐方案** | 生成、**自助审核 / 驳回 / 发布给客户** | 投顾自己(2026-09-14 起开放),管理员复核队列同样能做 |
| **投资目标方案书** | 录入目标 → 确认目标 → 查看方案书 | **仍归管理员**(接口上是 `admin=True` 的复核环节,故意的) |
方案书与推荐方案都有状态:目标 `待确认 → 已确认`;方案书 `待审核 → 审核通过 → 已发布`(或已退回)。
### 6.5 合规熔断(FM-03)
客户的风险测评**超过 12 个月**即视为失效:页面会**冻结购买类动作**
(生成推荐方案、资产配置、画像评分、调仓建议),只留赎回方向,并明确显示熔断原因。
⚠️ 这个拦截目前发生在**前端闸门**——后端只判断"测评有没有有效期字段",
不比较是否已过期。所以**不要绕过页面直接调接口来"演示"这件事**,那里拦不住。
### 6.6 一个专门的"离线演示模式":网址后面加 `?demo=1`
投顾工作台有一条**唯一能绕过登录守卫**的路径:`/portal/employee-advisor/dashboard/?demo=1`。
进去之后:
- 不持有令牌(所有真实接口请求都不发),页面顶部明确标注「**演示模式 · 本地引擎**」;
- 数据范围文案改成"本地演示引擎",指标卡的数据源也标成"本地引擎";
- 生成的方案带「**规则模拟,不构成真实推荐**」字样;
- 没有退出按钮(本来就是离线)。
**它是给"没有后端 / 没有 Worker 也要讲一遍投顾流程"准备的**,演示时务必**主动说明这是演示模式**,
别让人以为真实链路就是这么跑的。
---
## 7. 运营门户(三条线:场外收件 / 推广材料 / 问数)
**进门**:员工登录页登录后落到 `/portal/employee-operations/offsite/`。
顶部导航四个入口:概览、场外基金、推广材料、NL2SQL。
### 7.1 线一 · 场外基金受理:把"收到的邮件"变成"核对过的单据 + 发出的通知"
这是一条**确定性流程**(不靠模型自由发挥),完整走一遍是这样:
| 步 | 操作 | 背后发生了什么 |
|---|---|---|
| 1 | 看收件状态 | ⚠️ **页面上没有"上传邮件/投递"入口**:邮件是 Worker 通过 IMAP 从**真实邮箱**收进来的(**Worker 不在跑 = 永远不收信**),而且**发件人必须在白名单里**,否则回 403「发件人不在场外业务白名单」。状态显示"运行中/未启用";游标卡住时可以点"恢复收件游标"(⚠️ 它只把状态从"阻塞"重置为"空闲",**不会跳过那封失败的邮件**) |
| 2 | 打开一封邮件 | 左侧列表 → 右边显示正文、**附件原件**(可预览/下载)、**OCR 识别字段**、关联单据 |
| 3 | **人工修正 OCR 字段并保存** | 识别不可能 100% 准,所以运营可以逐字段改。**保存后系统会自动重新核对一遍**(触发 NL2SQL → 拉回字段 → 重判规则),不用再手动点一次 |
| 4 | (必要时)识别重试 | 附件识别失败可以提交重试 |
| 5 | 看单据要素 | 申请日期、申购金额 / 赎回份额、机构、基金代码 |
| 6 | 看**规则结果**表 | 每条规则一行:规则名 / 结果(满足·不满足·无法判断)/ 单据值 / 规则判断。可点"重新核对并判定规则"(这一步**只重跑计算与判定**,不会重新识别、也不会重新生成自然语言) |
| 7 | **人工下结论** | "确认正常" 或 "确认异常" —— 这是**人的决策**,系统只给依据。⚠️ 识别或核对没走完就点确认,会回 422「识别或数据核对尚未完成,不能进行业务确认」 |
| 8 | 创建并发送通知 | 按结论选通知类型(邮件回执 / 正常回执 / 异常回执 / 风险通知 / 清算通知),**正文可编辑**,然后发送 |
| 9 | 收尾 | 结清统计可重算(只统计"已确认正常且已成功回执"的单据);也可以彻底删除一封邮件(附件、OCR、规则、通知一并删除,**删除审计保留**) |
三个"知道就好、别被问倒"的点:
- 后端对**通知类型与确认结论的一致性有硬校验**:风险通知 / 异常回执只允许"已确认异常",
清算通知 / 正常回执只允许"已确认正常",邮件回执必须先完成人工确认——**对不上直接 422**。
- ⚠️ **发送通知默认是"演练"(dry-run)**:本机配置里 SMTP 发送是关着的、dry-run 是开着的,
所以点"发送通知"会返回发送结果与状态,但**不会真的把邮件发出去**。要真发得先配 SMTP。
- ⚠️「收件状态」显示的是**配置**,不是 Worker 心跳:Worker 假死时它**仍然显示"运行中"**。
判断 Worker 死活要看进程,别只看这一格。
> ⚠️ 这一步是演示最容易尴尬的地方:**邮件列表可能是 0 条**,因为收件依赖真实可用的邮箱配置;
> 发通知还额外依赖 SMTP 授权码。没有真实邮件源时,这页就是空的——**是"已知空数据",不是坏了**。
### 7.2 线二 · 推广材料:把产品事实编排成"可审核、可交付"的材料
| 步 | 操作 | 要点 |
|---|---|---|
| 1 | 创建任务 | 任务编号、产品名称、产品代码、材料标题、视觉风格(均衡配置 / 稳健专业 / 成长研究)。**进页面不会发任何请求**,任务号由后端按 `PM-日期-序号` 生成 |
| 2 | 填结构化资料 | 产品信息、管理人信息、团队与策略、费用/业绩/风险四组。⚠️ 页面上带 `*` 的必填**其实不会触发浏览器校验**(页面没有 `<form>`),靠后端兜底 |
| 3 | 上传附件 | 基金经理照片(jpg/png/webp)+ **业绩数据文件**(csv/xlsx/xlsm)。上传时**解析就是上传的一部分**,没有单独的"解析"按钮 |
| 4 | 生成材料 | 产出 PPTX、宣传长图、PDF 和若干图表。这一步是**同步**的(前端单独给了 120 秒超时),**不依赖 Worker**;只有背景图会调外部图像服务,不可用时自动降级为程序化背景 |
| 5 | 看合规检查 | 每条命中显示规则名 + 建议,分"阻断"与"提示"两类 |
| 6 | 审核 | 审核通过 / 要求修改 / 审核驳回,都要写意见 |
| 7 | 交付 | 审核通过后填投顾编号(逗号分隔)。⚠️ 首版交付只写**内部记录**,还没接外部投顾端 |
**合规检查是真会拦人的**(以下都是"阻断"级,不是提示):
- 勾了"展示排名"就必须同时给出**评价期间 ≥3 年 + 评价机构 + 公开来源**;
- 勾了"展示业绩"就必须已上传业绩数据文件,且历史月份 > 6;
- 出现收益 / 回撤 / 波动率 / 夏普比率里任意一项,**三项风险指标必须齐**;
- 七项费用必须填全;
- 材料里不许出现"保本、稳赚、无风险、保证收益、抢购"这类词;
- 必须有法定风险声明;不许把"从业年限"和"投资管理经验"混为一谈。
两个"为什么这么设计"的点,被问到很值得讲:
- **业绩数字不是手填的**:数据截止日期、历史月份、产品收益、最大回撤这几个字段是**只读**的,
由后端从你上传的**业绩数据文件**里算出来;波动率与**夏普比率**才需要手工填。
换句话说,**编不出来**——要么有数据文件,要么填不上。
- **"排名"必须给出来源**:要单独填"评价期间(年,需 ≥3)/ 评价机构 / 公开来源 / 排名文本"四项。
因为排名属于**公开可核查表述**,没有来源就不让写进去(这是合规要求,不是表单啰嗦)。
- 上传的幂等键用"**文件字节数 + 修改时间**"生成,**不用文件名**:
中文文件名会让浏览器在构造请求头时直接抛错,表现为"网络连接失败"、而服务端一条记录都没有(已修)。
### 7.3 线三 · NL2SQL:用中文问数,但**只能读、只能查白名单表**
页面顶部自己写着三条边界:**只读查询 / 白名单表 / 权限审计**。
它有两个模式:
| 模式 | 怎么用 |
|---|---|
| **场外单据核对** | 输入单据 `task_id` → 点"生成自然语言"(生成后可**人工修改**)→ 勾选"我已人工确认原始文件内容" → "执行只读核对" → 返回每张表核对规则的状态、返回行数、错误信息。这一段是**同步**的,不排队 |
| **通用自然语言查询** | 直接输入业务问题(如"查询某基金最近一个交易日的净值")→ 提交后**页面每 1.2 秒轮询一次运行状态**(依赖常驻 Worker,否则会一直停在"排队中")→ 展示 **AI 生成的 SQL** + 最终查询结果(没有图表) |
**关键点**:SQL 由**后端的 Agent 链路**生成,并且只能碰**发布配置里白名单允许的表**;
前端不拼 SQL,也不能绕过白名单。查询、权限、审计与最终状态都在后端。
(操作界面里连"生成 SQL"按钮都没有——这是刻意的。)
后端会拦下这些写法,报错文案就写在那里,可以直接念:
`仅支持 SELECT 查询`、`检测到非只读数据库操作`、`禁止使用 SELECT *`、
`SQL 使用的数据表与查询计划不一致`、`查询字段不在白名单内`、
`查询涉及的表缺少合法关联路径`、`查询参数过多`。
单次查询结果最多 50 行;表最多跨 3 个业务域。
> 演示时如果问"它会不会乱查":讲这条链路是"**自然语言 → 意图识别 → 白名单工具 → 生成 SQL → 只读执行**",
> 越界的表在工具层就被挡掉。这里也踩过一个真实缺陷:**只识别 6 位数字代码**,
> 而演示产品里有 5 位代码(如 `15911`),结果"查的是别的产品的数据、状态还显示成功"——
> 已修正为 5–6 位并加了单位校验。
---
## 8. 管理员工作台(平台的"总控台")
**进门**:员工登录页登录 → `staffHomeForRoles()` 判为管理员 → 落到 `/portal/employee-console/workspace/`。
**上半屏四张卡**:有效角色数、生效配置版本数、可用模型端点、待人工复核(工单 + 画像候选 + 画像漂移之和)。
下面是**八个标签页**,逐个一句话说清"它管什么、能写还是只读":
| 标签页 | 能做什么 | 读写 |
|---|---|---|
| **角色与权限** | 看角色清单(角色名/代码/权限数/用户数/状态);输入一个用户 ID,看**服务端实际解析出来的**角色、权限、数据范围 | 只读 |
| **配置与模型** | 平台里**唯一能改配置**的地方:新建发布版本 → 校验 → 审核 → 激活;管平台配置项(命名空间:工具白名单 `agent_tools`、记忆 `memory`、关系 `relationship`、运行时 `runtime`、行情 `fund_market`)与模型路由规则 | 读写 |
| **审计记录** | 最近 20 条审计(动作、主体、入口、时间);没有"看敏感详情"的权限时,详情由服务端**脱敏** | 只读 |
| **转人工工单** | 看客服转人工的工单与**二次脱敏**的会话摘要 | 只读 |
| **画像候选** | 客户对话里提炼出的画像候选,批准后才进正式记忆(批准/驳回都要填意见) | 读写 |
| **投顾复核** | 待审的投顾交付物(推荐方案 + 投资方案书)→ 审核通过 / 驳回 / 发布给客户 | 读写 |
| **知识库** | 上传文档(faq / product / policy)→ 自动切分入库并投向量同步;**客服就是靠它作答**;也能让某份知识失效 | 读写 |
| **画像漂移复核** | 客户重新测评导致画像变化时,人工确认是否采纳 | 读写 |
**配置发布的状态机**(`config_release` 表里能直接看到这些取值):
```
草稿 draft → 待审核 pending_review → 已审核 approved → 生效 active
↘ 驳回 rejected
生效版本被新版本取代 → superseded(当前库里绝大多数历史版本都是这个状态)
```
**为什么管理员也不能"直接改权限"**:平台的立场是**权限与提示词的变更必须留痕、可复核、可回滚**,
所以后端根本没提供"直接改"的接口,只能走上面这条状态机。这不是没做,是刻意不做。
> **已知的三个小缺口**(后端有接口、页面没接):页面没有"驳回 / 回滚"按钮(审核只有"通过")、
> 模型端点页是只读的(包含激活入口)、审计页没有筛选框(只能看最近 20 条)。
> 演示被问到时按"前端未接、后端已有"回答即可。
---
## 9. 底座里那几件"看不见、但决定一切"的事
这九件事是**跨所有业务**的,任何一个业务页面背后都会用到其中几件。
### 9.1 鉴权与数据范围
令牌只回答"你是谁";**权限与数据范围每次请求重新算**。
数据范围只有三个有效取值:`self`(只看自己)、`own_customers`(只看分配给我的客户)、`all`(全部);
写成别的值会被**静默丢弃**(等于没给范围)。员工能看到哪些客户由 `sys_customer_assignment` 决定。
### 9.2 幂等:为什么"重发"不会重复扣钱
所有**会产生数据**的操作(下单、建会话、转人工、配置发布、场外确认……)都要求带 `Idempotency-Key`,
服务端把"键 + 结果"记在 `api_request_receipt` 里。同一个键再来一次,**直接把上次的结果还给你**,
不会重复执行。键的规则是 **16–128 个可打印 ASCII 字符**——
这条规则是真踩过坑:曾经前端把**中文文件名**当成键,浏览器在发请求前就抛 `UnicodeEncodeError`,
页面只显示"网络连接失败",服务端**一条记录都没有**,非常难查。
⚠️ **它的边界要说清**:幂等键保护的是"**同一次请求的重发**",不是"**同一个操作意图**"。
前端目前每次提交都用新键,所以手动连点两下会真的下两笔单。
要彻底防重,需要前端把"一次下单意图"的键**稳定下来**(把键与表单实例绑定),这是已知缺口。
### 9.3 三个 Agent、七个业务 Agent
- 所有业务 Agent 都必须继承公共的 `BaseAgent`,并且只能由 `AgentFactory` 创建 ——
为的是**不许绕过**鉴权、记忆、模型路由、工具、合规、审计这条公共流程。
(规矩写在 `AGENTS.md`,有测试守着。)
- 目前 7 个:客服、风控、投顾、场外基金、推广材料、平台探针、基金查询示例。
- **工具能不能用,是两个集合的交集**:代码里注册的 ∩ 当前**生效配置**里发布的。
缺发布配置 → **失败关闭**(宁可不能用,也不放开)。这就是为什么"代码明明写了这个工具,
调用却报没有权限"——去管理员工作台看当前生效的发布版本里有没有它。
### 9.4 记忆:值在 MySQL、关系在 Neo4j、向量在 Milvus
一条"记住客户说过什么"的完整过程:
1. 对话发生 → 若干轮之后 Worker 把这几轮**聚合成一个片段**;
2. 从片段里**抽取长期记忆**(只抽明确的业务事实;客服和访客**不写**长期记忆);
3. 记忆入库(MySQL,**唯一权威**)→ 触发**画像重建** → 生成画像快照;
4. 画像与记忆再**投影**出去:关系进 Neo4j、向量进 Milvus(集合 `user_long_term_memory_v1`);
5. 下次这个客户再来,就能"召回"到相关记忆,注入给模型。
> 为什么投影要绕一层"事件":抽取发生时那条记忆还在**没提交的事务**里,
> 另开一个连接去重建画像根本看不到它。走事件就一定是"提交之后"才消费,数据一定可见。
**谁能读到谁的记忆**只有一个判定口径(`app/core/memory_scope.py`):
客户只读自己;员工只读**分配给自己的**客户,单次上限 10 个;归属没维护 → 失败关闭。
不这么收敛的后果是**越权陷阱**:员工号和客户号同号段,按号查会读到陌生客户的记忆且不报错。
### 9.5 模型路由
所有模型调用都从统一的网关走,按"任务类型"选端点(意图分类 / 向量化 / 生成各不相同),
端点在数据库里配置、可切换。密钥放在 `.env`(**不进仓库**)。
演示环境当前用 `deepseek-flash` 做意图与生成、`qwen` 做向量化。
### 9.6 配置发布:为什么管理员也"不能直接改权限"
平台**故意不提供**"直接改权限/直接改提示词"的接口,所有变更必须走状态机:
```
草稿(draft) → 待审核(pending_review) → 生效(active) → 被新版本取代(superseded)
```
一次发布是**整版本替换**:生效版本里包含哪些工具白名单、提示词、模型路由规则,一目了然、可回滚。
受管的内容只有三张表:平台配置项、提示词版本、模型路由规则。
生效后平台会发一个事件去**清缓存**,让新配置立刻对所有请求生效。
### 9.7 事件与 Outbox:先落库,再干活
"要慢慢做的事"(投递记忆、同步向量、重建画像、发通知)都不在请求里硬等,
而是**先往事件表写一行**,再由 Worker 一条条消费:失败了记重试次数、退避重试,
重试到头进"死信",**永远不丢、可查、可重放**。
> 一个真实踩过的坑:事件里表示"投到哪个存储"的字段**必须是小写英文**
> (`milvus` / `neo4j`)。文档里曾写成大写 `MILVUS`,消费端按小写分派,
> 结果**事件谁都领不到、永久滞留、且不报错**。
### 9.8 审计:每一次判断都留痕
权限判定、工具调用、预警处置、投影清理、转人工……都往 `interaction_audit` 写一条:
谁、什么身份、对哪个客户、在哪个门户、做了什么、结果如何。
管理面的工单接口**不返回客户标识与原始对话**(脱敏),这是刻意的。
### 9.9 降级:宁可说"没做成",也不假装成功
Milvus/Neo4j 没配或连不上时,平台的取向是**显式降级 + 留痕**:
事件保持"待处理"、审计里写"跳过 + 原因",**绝不写一条"已同步"**。
所以看到"待处理堆积"先怀疑依赖没起来,而不是怀疑业务代码。
---
## 10. 把上面全部串起来:一条主线的时间线(从登录到一笔成交)
| # | 你做的事 | 平台内部发生什么 | 落在哪 |
|---|---|---|---|
| 1 | 打开 `http://127.0.0.1:8000/portal/` | 307 跳到访客首页 | — |
| 2 | 打开客户登录页,填 `cust_t / 123456` | 前端调 `A034` 换令牌;核对"这是不是客户账号";写 cookie | `sys_user` + 令牌 |
| 3 | 自动跳资产总览 | 前端带令牌调 `T001`;服务端**重新解析**身份/权限/数据范围 → 查账户、持仓、最新行情、昨日基准 → 算市值、持有盈亏、**今日盈亏** | 读 `fin_sim_account` / `fin_holding` / `fin_market_price` / `fin_nav_history` / `fin_transaction` |
| 4 | 切到"我的持仓" | 调 `T006`,同一套计算(保证两页口径完全一致) | 同上 |
| 5 | 点买入 100 份 510300 | 前端生成幂等键 → 调 `T002` | — |
| 6 | — | 服务端:查幂等表(重复键直接返回上次结果)→ 校验行情是否在 15 分钟内 → 适当性校验 → 按费率规则算钱 → **一个事务里写委托、成交、资金流水、持仓** | `api_request_receipt` / `fin_sim_order` / `fin_transaction` / `fin_cash_ledger` / `fin_holding` |
| 7 | 刷新页面 | 持仓、成交、流水三页**同时**出现这笔;今日盈亏把"今天买的这部分"按成交价计入 | 同上 |
| 8 | 顺手在浮窗问客服"场内基金管理费率是多少" | 前端建会话 `C001` → 发问 `R001`,**立即返回 202**(受理)→ 事件落库 → **Worker 领单**执行 → 检索已发布知识 → 有把握才答、附引用 → 结果落库 → 前端取回 | `conversation*` / `agent_run` / `domain_event_outbox` / `interaction_audit` |
| 9 | 对话结束、过一会儿再问同样的问题 | Worker 聚合会话片段 → 抽取长期记忆 → 重建画像 → 投影到 Neo4j(关系)与 Milvus(向量)→ 下次召回用得到 | `memory_unit` / `profile_snapshots` / `memory_sync_outbox` |
| 10 | (若触发风控)风控专员在队列里处置 | 扫描 → 生成预警 → 通知;确认/调查/结案每一步写审计,结案按等级扣客户行为分 | `fin_risk_alert` / `fin_risk_notification` / `interaction_audit` |
---
## 11. 常见疑问(大白话问答)
**Q:下单报"行情已过期"(503)怎么办?**
A:行情只认 15 分钟内的快照,且**不会自动刷新**。跑一次
`python tools/sync_market_prices.py` 即可,**立即生效、不用重启任何服务**。
**Q:客服一直"繁忙/响应超时"?**
A:先看 **Worker 在不在跑**(`python -m app.worker`)。Agent 请求是"受理 → Worker 执行 → 落结果"
三段式,没有 Worker,队列里那行会一直停在 `queued`,页面只会显示超时。
**Q:整个平台突然都很卡(客服、看板、风控同时迟钝)?**
A:多半是 **Milvus 抖动**:检索层用的是同步客户端,一次卡住会阻塞整个 API 进程。
确认 Docker Desktop / Milvus 在跑即可,**恢复后自动正常**,不用改代码也不用重启。
**Q:为什么"今日盈亏"显示的是最近一个交易日的数?**
A:因为它用的行情/净值都是**日终数据**,语义就是"最近一个交易日相对前一交易日的盈亏",
不是盘中实时。当天数据还没同步时,它会等于"昨日"。
**Q:为什么投顾看不到某个客户?**
A:投顾只能看**分配给自己**的客户(`sys_customer_assignment`)。没分配就看不到,这是数据范围,不是故障。
**Q:为什么运营那边的邮件是 0 条?**
A:场外收件需要**真实可用的邮箱**(收件依赖邮箱配置;发件还依赖 SMTP 授权码)。
没有真实邮箱源时,这一页就是空的——这是"已知空数据",不是坏了。
**Q:为什么管理员不能直接改权限?**
A:金融场景要求权限变更**留痕、可追溯、可回滚**,所以统一走"草稿→审核→生效"的发布流程。
**Q:为什么有些问题客服答不上来?**
A:知识是**人工审核后入库**的。检索不到、或把握不够(分数不够高/候选并列)时就转人工——
**宁可转人工也不硬答**,这是刻意的取舍。
**Q:为什么撤单总是失败?**
A:首版是市价委托**立即全额成交**,只有停在"待风控"的委托能撤,所以基本都会提示"该委托已成交"。
**Q:点了"发送通知",为什么对方没收到邮件?**
A:本机 SMTP 默认是**演练模式**(发送开关关着、dry-run 开着),所以流程会走完、状态会显示,
但**不会真的发出去**。要真发得先配好 SMTP。另外发件还依赖邮箱的 SMTP 授权码,授权码失效会显示"发送失败"。
**Q:场外的"收件状态"显示运行中,为什么就是收不到邮件?**
A:那一格反映的是**配置**,不是 Worker 心跳——Worker 卡死时它仍然显示"运行中"。
另外**发件人必须在白名单里**,不在白名单的邮件会被拒(403)。
**Q:为什么有的接口报错却还是 HTTP 200?**
A:场外这条线的业务错误用的是 `HTTP 200 + {code, message}` 的信封(前端把 `code ≥ 400` 当错误处理),
参数格式错误才会是真正的 HTTP 422。看错误要看 `code` 与 `message`,别只看状态码。
---
## 12. 速查
**六个门户**
```
访客 http://127.0.0.1:8000/portal/guest/home/
客户 http://127.0.0.1:8000/portal/customer/login/
风控 http://127.0.0.1:8000/portal/employee-console/login/ → 自动进风控工作台
投顾 http://127.0.0.1:8000/portal/employee-console/login/ → 自动进投顾工作台
运营 http://127.0.0.1:8000/portal/employee-console/login/ → 自动进运营门户
管理员 http://127.0.0.1:8000/portal/employee-console/login/ → 自动进管理员工作台
```
**演示账号**
| 角色 | 用户名 | 密码 |
|---|---|---|
| 客户 | `cust_t` | `123456` |
| 风控专员 | `risk_t` | `666666` |
| 管理员 | `admin_t` | `88888888` |
| 投顾 | `advisor_t` | `abc12345` |
| 运营 | `offsite_t` | `offsite123` |
| 风控演示客户 | `12001`–`12005` | `risk12345` |
| 投顾演示客户 | `advc_zhang` / `advc_li` / `advc_chen` | (见投顾线脚本) |
**常用命令**
```powershell
powershell -ExecutionPolicy Bypass -File start.ps1 # 起 API + Worker(自动刷行情、开浏览器)
python -m app.worker # 只起 Worker
python tools/seed_demo_data.py # 准备演示数据(10 步,首次/换机器)
python tools/sync_market_prices.py # 刷行情(下单报 503 时补跑)
python tools/check_today_profit_loss.py # 复算并核对「今日盈亏」
python tools/e2e_smoke_test.py --read-only # 业务链路体检(不动数据)
python tools/portal_api_check.py # 接口契约体检(按前端的方式调)
```
**端口**:API `8000` / MySQL `3306` / Redis `6379` / Milvus `19530` / Neo4j `7687`
**已知空数据(演示时别点进去尴尬)**
| 位置 | 现状 | 原因 |
|---|---|---|
| 运营 · 场外邮件 | 0 条 | 需要真实可用的收件邮箱 |
| 运营 · 通知发送 | 只"演练",不真发 | SMTP 开关默认关闭 + dry-run 默认开启 |
| 管理员 · 画像候选 | 0 条 | 需要客户对话触发画像抽取 |
| 投顾 · 客户列表 | 页面内置 4 位演示客户 | 前端暂无"查我的客户"接口,真实范围在服务端按归属判定 |
**已知小偏差**(不影响主流程;被问到就照实说,别硬解释)
| 现象 | 实情 |
|---|---|
| 场外通知类型的下拉"切了没用" | 那个下拉没接事件处理,**页面实际只能创建"正常回执/异常回执"**;风险通知、清算通知、邮件回执在这一页点不出来(接口是有的) |
| NL2SQL 场外核对结果里"错误信息"列永远是 `--` | 后端返回的记录里没这个字段,前端取了个不存在的键 |
| 场外部分写操作偶尔报"请求超时" | 这些接口用的是默认 8 秒超时,而同步核对与统计重算本身可能跑到 10 秒以上(接口能跑完,只是前端先放弃了) |
| 识别重试"提交了"但状态没变 | 后端失败时也返回 `code:0` + "已保留异常状态",前端一律提示"已提交识别重试" |
| 推广材料页的必填校验不弹提示 | 页面没有 `<form>`,`required` 不生效,必填靠后端返回错误 |
| 投顾"已发布交付物"空态写"需管理员审核" | 文案没跟进"投顾可自助审核发布"这次改动 |
| 管理员配置发布没有"驳回/回滚"按钮、模型端点页是只读的、审计页没有筛选框 | 后端接口都有,前端未接 |
| 客户「资金流水」页顶部导航不高亮 | 导航配置里该页的 `active` 是空串 |