docs: 新增投顾 Agent 需求文档与功能架构文档

依据仓库既有设计与已落地代码反推整理,内容与当前实现一致(非前瞻设计)。

- docs/46-投顾Agent需求文档.md
  · 背景与三个真实断点(交付无落点 / 文案不可读 / 客户无入口)
  · 目标 G1-G5 与 4 条非目标(不真实下单、不预测收益、不自动投资、不做场外)
  · 角色场景 SC-01~SC-05、两条主链路流程图
  · 功能需求 FR-01~FR-21(含口径、优先级、验收标准)
  · 业务规则:候选池硬约束 fail-closed、5 条合规红线、状态机、FM-03 熔断
  · 非功能需求、异常处理表、权限矩阵、数据需求、验收清单、11 项待确认问题

- docs/47-投顾Agent功能架构文档.md
  · 五层架构总览、后端模块划分与 3 条设计约束、数据模型与关系
  · 接口清单(投顾 8 / 客户 3 / 依赖能力 3 组)
  · 三张时序图(生成推荐、审核发布、申报受理)
  · 外部依赖降级矩阵、前端模块结构与 2 条硬约定、权限灰度、配置清单、已知约束
This commit is contained in:
Windows
2026-09-16 18:17:45 +08:00
parent 8643ad1efc
commit 560775156c
2 changed files with 598 additions and 0 deletions
+314
View File
@@ -0,0 +1,314 @@
# 投顾 Agent 需求文档(PRD)
> 文档版本:v1.0(2026-09-16)
> 文档状态:**已实现反推稿** —— 本章所记需求均已在本仓库落地并本地验证,非前瞻设计
> 适用范围:投顾 Agent(投顾工作台 + 客户端「我的投顾方案」+ 客户主动申报)
> 依据材料(均为仓库内既有文档与代码,非本文件臆造):
> · 设计:`docs/01`(通用 Agent 平台)、`docs/03`(端到端流程)、`docs/05`(接口文档)、
> `docs/22`(四大 Agent 拆解)、`docs/30`(投顾迁移 TODO)、`docs/31`(投顾灰度与回滚)、
> `docs/43`(场内产品手册)、`docs/44`(演示流程)
> · 代码:`app/service/product_recommendation_service.py`、`advisor_service_request_service.py`、
> `advisor_reason_service.py`、`app/api/controllers/recommendations.py`、`advisor_service_requests.py`、
> `app/static/portal/employee-advisor/dashboard/*`、`app/static/portal/customer/advisor-plans/*`
---
## 1. 背景与目标
### 1.1 问题陈述
投顾业务的真实痛点在于「**选品可解释 + 交付可追溯**」:
- 投顾给客户荐品,必须能说清**为什么是这几只**(适当性匹配、期限/流动性匹配、证据可核查);
- 方案从生成到到达客户手里,必须**留痕**、且**未经审核不得对客户可见**;
- 客户侧长期是单向接收,缺少「主动提出需求」的入口。
此前系统里推荐能力存在,但存在三处断点:
| 断点 | 表现 |
|---|---|
| 交付无落点 | 投顾「发送给客户」只改数据状态,客户端没有任何页面/接口能读到已发布方案 |
| 文案不可读 | 每只产品的「推荐依据」是同一句套话,客户看不出差异 |
| 客户无入口 | 客户只能被动等方案,无法主动申报 |
### 1.2 目标(G)
| 编号 | 目标 | 衡量方式 |
|---|---|---|
| G1 | 推荐**可解释**:每只入选产品都能给出基于真实参数的依据 | 方案里每只产品都有独立依据,且含风险等级/匹配关系 |
| G2 | 交付**可追溯**:审核与发布分离,未发布对客户不可见 | 状态机 + `published_at` 双条件;客户接口只返回已发布 |
| G3 | 呈现**可感知**:客户能看到走势图、指标与组合构成 | 客户端可展示净值折线图、环形配置图、区间收益与最大回撤 |
| G4 | 客户**能发起**:客户可主动申报,投顾后台受理后出方案 | 申报→受理→生成草稿→审核→发送 全链路打通 |
| G5 | 合规**不越线**:不得承诺收益,证据缺失即整只排除 | 硬约束 fail-closed + 文案合规守卫 |
### 1.3 非目标(本期明确不做)
| 非目标 | 说明 |
|---|---|
| 真实下单 | 推荐方案**不产生交易指令**,客户仍需在交易页自行下单 |
| 收益预测 | 不预测未来收益,只呈现历史区间收益与最大回撤 |
| 自动投资 | 无自动调仓、无定投、无代客操作 |
| 场外基金 | 投顾范围限定场内基金,场外走独立模块(见 `docs/22`) |
---
## 2. 角色与场景
### 2.1 角色定义
| 角色 | 角色码 | 入口 | 核心诉求 |
|---|---|---|---|
| 投顾 | `advisor` | `/portal/employee-advisor/dashboard/` | 代客出方案、审核发布、处理客户申报 |
| 客户 | `customer` | `/portal/customer/advisor-plans/` | 接收方案、主动申报、查看申报进度 |
| 管理员 | `admin` / `super_admin` | 管理面 | 审核、发布、故障处置(不受灰度限制) |
### 2.2 核心场景
| 编号 | 场景 | 触发方 |
|---|---|---|
| SC-01 | 投顾选择名下客户 → 生成推荐方案 → 审核通过 → 发送给客户 | 投顾 |
| SC-02 | 投顾查看历史方案记录(含草稿)→ 抽查内容详情 | 投顾 |
| SC-03 | 客户登录 → 在「我的投顾方案」查看已收到的方案 | 客户 |
| SC-04 | 客户提交投顾方案申报 → 投顾受理 → 自动出草稿 | 客户→投顾 |
| SC-05 | 投顾受理后审核、发布,客户看到方案变为「已发送方案」 | 投顾→客户 |
---
## 3. 业务流程
### 3.1 主链路 A:投顾主动出方案
```mermaid
flowchart LR
A[投顾选择客户] --> B[合规熔断校验 FM-03]
B -- 测评失效 --> X[熔断拦截 转人工]
B -- 通过 --> C[生成推荐方案]
C --> D{候选池硬约束}
D -- 某产品缺证据 --> E[整只排除 fail-closed]
D -- 通过 --> F[排序取 TopN]
E --> F
F --> G[LLM 生成推荐依据]
G --> H[落库 pending_review]
H --> I[投顾审核通过]
I --> J[发布给客户]
J --> K[客户可见]
```
### 3.2 主链路 B:客户主动申报
```mermaid
flowchart LR
A[客户填写申报单] --> B[服务端校验风险测评有效性]
B -- 未测评/已过期 --> X[拒绝 并提示原因]
B -- 通过 --> C[落库 pending]
C --> D[投顾工作台出现待办]
D --> E{投顾受理 or 驳回}
E -- 驳回 --> F[填理由 客户可见]
E -- 受理 --> G[自动跑推荐逻辑 出草稿]
G --> H[投顾审核 + 发送]
H --> I[申报单变 已发送方案]
```
---
## 4. 功能需求
优先级:**P0** 本期必须 / **P1** 重要 / **P2** 增强。
### 4.1 客户与画像
| 编号 | 功能 | 口径 | 优先级 | 验收标准 |
|---|---|---|---|---|
| FR-01 | 投顾查看名下客户 | 范围 = 本人 + `sys_customer_assignment` 归属客户 | P0 | 非归属客户不可见,越权返回 403 |
| FR-02 | 风险测评(客户自助) | `ONB001` 取题 / `ONB002` 提交;结果落服务端权威画像 | P0 | 测评结果由服务端权威返回,前端不得自行计算风险等级 |
| FR-03 | 熔断前置校验 | 测评超过 12 个月或缺失 → FM-03 拦截 | P0 | 前端展示熔断横幅,接口返回 `blocked_by_fuse` |
### 4.2 组合诊断与配置
| 编号 | 功能 | 口径 | 优先级 | 验收标准 |
|---|---|---|---|---|
| FR-04 | 组合分析 | 集中度、行业穿透与流动性 | P1 | 返回可展示的诊断结论 |
| FR-05 | 资产配置 | 按风评与目标给出配比与偏离 | P1 | 目标缺失时返回 `investment_goal_required` |
| FR-06 | 画像评分 | 本地演示引擎计算(后端无对应接口,明确标注"规则模拟") | P2 | 结果区必须带演示标识 |
| FR-07 | 调仓建议 | 同上,本地引擎 | P2 | 结果区必须带演示标识 |
> FR-06/FR-07 由本地演示引擎计算,前端会明确标注;**不得**被误认为真实后端结论。
### 4.3 推荐方案生成(核心)
| 编号 | 功能 | 口径 | 优先级 | 验收标准 |
|---|---|---|---|---|
| FR-08 | 生成推荐方案 | `POST /api/v1/advisor/recommendations`,需 16-128 位 ASCII 幂等键 | P0 | 同键重试返回同一方案,不重复建单 |
| FR-09 | 候选池硬约束 | 产品须同时具备:场内可交易 + 销售机构**已验证**适当性证据 + 基金合同快照;缺任一即整只排除 | P0 | 排除原因可查(前端「排除原因」提示词) |
| FR-10 | 排序与入选 | 按适当性等级、目标期限、目标流动性筛选后综合打分取 TopN(默认 3) | P0 | 返回 `selection_summary`:候选/入选/排除数 |
| FR-11 | 推荐依据生成 | 规则文案兜底;配置大模型时改写为面向客户的说明 | P0 | 文案必须含真实参数;命中收益承诺词即丢弃并回退 |
| FR-12 | 审核 | `reviews` 接口,decision ∈ {approved, rejected} | P0 | 未审核不得发布 |
| FR-13 | 发布 | `publications` 接口,置 `review_status` 与 `published_at` | P0 | 发布后客户可见;重复发布幂等 |
### 4.4 历史记录与交付
| 编号 | 功能 | 口径 | 优先级 | 验收标准 |
|---|---|---|---|---|
| FR-14 | 历史方案记录 | 本人生成的全部状态方案 | P0 | 状态标签与后端状态一致,不使用枚举兜底文案 |
| FR-15 | 客户接收方案 | `GET /api/v1/users/me/advisor-contents`:只看**本人**、已审核且 `published_at` 非空 | P0 | 未发布的方案对客户不可见 |
| FR-16 | 方案可视化 | 净值折线图(带坐标轴/网格)、组合业绩(等权合成 + 最大回撤)、资产配置环形图 | P1 | 数据缺失显示「暂无数据」,不显示 0 |
### 4.5 客户主动申报
| 编号 | 功能 | 口径 | 优先级 | 验收标准 |
|---|---|---|---|---|
| FR-17 | 客户提交申报 | 金额(万元)/ 投资期限 / 风险偏好 / 备注 | P0 | 服务端校验取值与风评有效性 |
| FR-18 | 我的申报 | 只看自己的申报单与状态 | P0 | 状态含 待受理/已受理/已发送/已驳回 |
| FR-19 | 投顾申报队列 | 本人 + 归属客户的申报,待受理排前 | P0 | 非归属客户的申报不可处理 |
| FR-20 | 受理并生成方案 | 受理即调用现有推荐逻辑自动出**待审核草稿** | P0 | 生成失败给出人话原因,并保持待受理 |
| FR-21 | 驳回 | 需填理由(可选),客户可见 | P1 | 驳回后申报单不可再被受理 |
---
## 5. 业务规则
### 5.1 候选池硬约束(fail-closed)
产品必须同时满足三项,缺一即排除:
1. **场内可交易**;
2. **适当性证据**:销售机构的**已验证**适当性证据,带可核查来源链接与 `document_sha256`;
3. **合同快照**:基金合同快照,同样带来源与摘要。
按目标继续过滤:
| 维度 | 规则 |
|---|---|
| 风险等级 | 客户风评等级对应的禁投风险级直接排除 |
| 投资期限 | 短于目标期限下限的产品排除 |
| 流动性 | 不符合目标流动性要求(如 30 日内可用)的产品排除 |
### 5.2 合规红线(必须遵守,代码强制)
| # | 红线 |
|---|---|
| 1 | 不得出现收益承诺与绝对化表述(保本/保证收益/稳赚/无风险/收益承诺…) |
| 2 | 未审核发布的方案对客户不可见 |
| 3 | 推荐不产生交易指令,仅作分析参考 |
| 4 | 客户不得自行修改风险测评结果(服务端权威) |
| 5 | 全流程留痕(审核、发布、受理、驳回均记录操作人与时间) |
### 5.3 状态机
**推荐方案**
```mermaid
stateDiagram-v2
[*] --> pending_review: 生成方案
pending_review --> approved: 审核通过
pending_review --> rejected: 驳回
approved --> 已发布: 发送给客户
rejected --> [*]
已发布 --> [*]
```
> 「已发布」不是独立状态字段,而是 `review_status ∈ {approved, published}` **且** `published_at` 非空 —— 避免两处状态各写各的而对不上。
**客户申报单**
```mermaid
stateDiagram-v2
[*] --> pending: 客户提交
pending --> accepted: 投顾受理(自动生成方案草稿)
pending --> rejected: 投顾驳回
accepted --> 已发送: 关联方案发布后推导
```
> `已发送(delivered)` 同样**不落库**,由关联方案的 `published_at` 推导。
### 5.4 熔断 FM-03
| 条件 | 结果 |
|---|---|
| 无风险测评 | 拦截,提示先完成测评 |
| 测评超过 12 个月 | 拦截(服务端判 `valid_until`,缺失时按 12 个月兜底) |
| 熔断生效时 | 客户仅可办理赎回,流程转人工 |
---
## 6. 非功能需求
| 类别 | 要求 |
|---|---|
| 幂等 | 写接口必须带 16-128 位 ASCII `Idempotency-Key`;同键重试返回首次结果 |
| 降级 | 图谱(Neo4j)、大模型、行情任一不可用,业务**继续可用**并明确标注降级 |
| 留痕 | 审核/发布/受理/驳回写入审计;演示口径留存 ≥ 20 年 |
| 限流 | 按「用户 + 方法 + 路由」计数,超限 429;Redis 不可用时降级放行 |
| 灰度 | `ADVISOR_ROLLOUT_ENABLED` 开启后仅白名单客户与管理员可访问投顾业务 |
| 前端一致性 | 同一页面内同一模块的 `?v=` 版本号必须一致(否则双实例) |
---
## 7. 异常处理
| 场景 | 返回 | 前端呈现 | 可恢复动作 |
|---|---|---|---|
| 客户无风险测评 | `profile_required` | 结果区提示 | 先做风险测评 |
| 无已确认投资目标 | `investment_goal_required` | 结果区提示 | 先录入并确认目标 |
| 推荐数量越界 | 422 `RECOMMENDATION_PARAM_INVALID` | 结果区提示 | 改为 1-3 |
| 权限不足 | 403 | 「当前账户暂不可访问 / 权限不足」+ toast | 换有权限的账号 |
| 未开户(资金类) | 404 | 「客户未开户」 | 先建模拟账户 |
| 行情/净值缺失 | 503 / 空数据 | 「暂无净值数据」 | 跑 `tools/sync_nav_history.py` |
| 大模型不可用 | 无异常 | 依据回退为规则文案 | 关闭开关或换 key |
| 熔断 | `blocked_by_fuse` | 熔断横幅 | 转人工 |
---
## 8. 权限矩阵
| 能力 | 客户 | 投顾 | 管理员 |
|---|---|---|---|
| 提交/查看**自己的**申报 | ✅ `advisor-request:write:self` / `read:self` | — | — |
| 查看**自己的**已发布方案 | ✅ `product-recommendation:read:self` | — | — |
| 生成/查看/删除推荐方案 | — | ✅ | ✅ |
| 审核 / 发布方案 | — | ✅(自助) | ✅ |
| 查看客户申报队列 | — | ✅ `advisor-request:read` | ✅ |
| 受理 / 驳回申报 | — | ✅ `advisor-request:review` | ✅ |
| 灰度限制 | — | 受白名单限制 | 不受限 |
> 权限码定义源:`tools/seed_test_rbac.py`;投顾角色的授权工具:`tools/grant_advisor_role.py`。
---
## 9. 数据需求
| 表 | 用途 | 关键字段 |
|---|---|---|
| `client_facing_content` | 方案与方案书 | `content_type` / `customer_id` / `review_status` / `published_at` / `draft_content` |
| `advisor_service_request` | 客户申报单 | `request_no`(唯一) / `customer_id` / `amount_wan` / `horizon` / `risk_preference` / `status` / `result_content_id` |
| `sys_customer_assignment` | 投顾-客户归属 | 决定投顾可见范围 |
| `fin_product` / `fin_market_price` / `fin_nav_history` | 产品、行情、净值 | 图表与指标的数据源 |
| `api_request_receipt` | 幂等 | `user_id` / `scope_hash` / `idempotency_key` / `request_hash` |
| `interaction_audit` | 审计留痕 | 操作人、动作、明细 |
---
## 10. 验收清单
| # | 验收项 | 方法 |
|---|---|---|
| 1 | 投顾生成方案 → 结果区出现卡片与图表 | 工作台执行「生成推荐方案」 |
| 2 | 审核通过后发布 → 客户可见 | 客户页出现该方案 |
| 3 | 未发布方案对客户不可见 | 客户页不出现草稿 |
| 4 | 同幂等键重试不重复建单 | 连续两次提交同键 |
| 5 | 客户申报 → 投顾队列出现 → 受理后自动出草稿 | 两端对照 |
| 6 | 文案命中收益承诺词被丢弃 | 单测 `test_advisor_reason_service.py` |
| 7 | 大模型不可用时回退规则文案 | 关闭开关后重生成 |
| 8 | 越权客户不可见/不可处理 | 换非归属客户验证 403 |
---
## 11. 待确认问题
| # | 问题 | 影响 |
|---|---|---|
| Q1 | 适当性证据与合同快照的**真实数据来源**何时接入?当前候选为 0(证据缺失 fail-closed) | 阻塞:决定演示用真实方案还是本地引擎 |
| Q2 | 推荐依据是否允许出现历史业绩之外的表述(如基金经理风格)? | 影响文案提示词 |
| Q3 | 客户申报是否需要额度/频率限制(防刷)? | 影响 FR-17 |
| Q4 | 申报单与投资目标的关系:受理后是否自动创建投资目标? | 影响 FR-20 之后流程 |
| Q5 | 灰度白名单的最终放量范围? | 影响验收范围 |
+284
View File
@@ -0,0 +1,284 @@
# 投顾 Agent 功能架构文档
> 文档版本:v1.0(2026-09-16)
> 适用范围:投顾 Agent 的后端服务、前端模块、数据模型、接口与降级策略
> 配套文档:`docs/46-投顾Agent需求文档.md`(需求与验收)
> 说明:本文描述的是**仓库当前已落地的实现**,不是前瞻设计
---
## 1. 架构总览
```
┌─────────────────────────────────────────────────────────────────┐
│ 客户端门户 /portal/customer/ │
│ advisor-plans(我的投顾方案 / 主动申报 / 申报进度) │
│ dashboard · holdings · orders · cash-ledger · risk-questionnaire│
└─────────────────────────────────────────────────────────────────┘
│ HTTPS + Bearer JWT
┌─────────────────────────────────────────────────────────────────┐
│ 投顾工作台 /portal/employee-advisor/dashboard/ │
│ dashboard.js(壳/引导/指标) │
│ ├ actions-module 七项操作 + 熔断闸门 + 结果渲染 │
│ ├ assistant-module 自然语言意图路由 │
│ ├ history-module 历史方案记录(只读 + 详情弹窗) │
│ ├ service-request-module 客户申报队列(受理/驳回) │
│ ├ published-module 已发布交付物 │
│ ├ customer-module 客户选择 │
│ └ advisor-engine / advisor-config 本地演示引擎与常量 │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────┐
│ FastAPI 入口层 app/api/controllers/ │
│ recommendations.py 推荐(生成/历史/已发布/审核/发布/删除)│
│ advisor_service_requests.py 客户申报 + 投顾受理 │
│ public_platform.py 公开产品与净值(P001/P002) │
│ onboarding.py / trading.py 风险测评 / 场内模拟交易 │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────┐
│ 服务层 app/service/ │
│ ProductRecommendationService 选品 + 排序 + 审核发布(核心) │
│ AdvisorServiceRequestService 客户申报与受理 │
│ AdvisorReasonService 推荐依据的大模型增强(可选) │
│ SuitabilityService 服务端权威风险画像 │
│ InvestmentGoalService 投资目标 │
│ MarketPriceSyncService 行情同步 │
│ AuthorizationService / IdentityService 权限与身份 │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────┐
│ 存储与依赖 │
│ MySQL(业务主库) · Redis(限流/缓存) · Milvus(知识向量) │
│ Neo4j(图谱,可降级) · 外部行情/净值源 · DeepSeek(可选) │
└─────────────────────────────────────────────────────────────────┘
```
---
## 2. 后端模块划分
| 层 | 文件 | 职责 |
|---|---|---|
| 控制器 | `app/api/controllers/recommendations.py` | 三个 router:`advisor`(投顾)、`admin`(平台管理员)、`client`(客户自助) |
| 控制器 | `app/api/controllers/advisor_service_requests.py` | 客户申报(`/users/me`)+ 投顾受理(`/advisor`) |
| 服务 | `product_recommendation_service.py` | 硬约束过滤、排序、审核、发布、历史;**唯一选品入口** |
| 服务 | `advisor_service_request_service.py` | 申报创建/查询、队列、受理(联动生成草稿)、驳回 |
| 服务 | `advisor_reason_service.py` | 推荐依据的大模型增强(合规守卫 + 失败回退) |
| 仓储 | `advisor_product_repository.py` | 权威候选产品(场内可交易 + 适当性证据 + 合同快照) |
| 模型 | `advisor_service_request.py` | 申报单 ORM 模型与状态常量 |
| 模型 | `investment_goal.py` | `ClientFacingContent`(方案/方案书) |
**设计约束(踩过坑换来的)**
- 客户自助路由**不挂投顾灰度闸门**(`enforce_advisor_rollout`)—— 那是投顾业务灰度,客户查看自己的方案不该被它拦。
- 归属范围**只有一处实现**:`ProductRecommendationService._visible_customer_ids`(本人 + `sys_customer_assignment`),历史、已发布、申报队列全部复用它。
- 写操作统一走 `ApiTransactionService.execute`(16-128 位 ASCII 幂等键)。
---
## 3. 数据模型
| 表 | 说明 | 关键字段 |
|---|---|---|
| `client_facing_content` | 方案与方案书 | `content_type`(`advisor_recommendation_plan` / `investment_goal_book`)、`customer_id`、`review_status`、`published_at`、`draft_content` |
| `advisor_service_request` | 客户申报单 | `request_no`(唯一)、`customer_id`、`amount_wan`、`horizon`、`risk_preference`、`note`、`status`、`handled_by/at`、`advisor_note`、`result_content_id` |
| `sys_customer_assignment` | 投顾-客户归属 | 决定可见范围 |
| `fin_product` / `fin_market_price` / `fin_nav_history` | 产品 / 行情 / 历史净值 | 图表与指标数据源 |
| `api_request_receipt` | 幂等 | `user_id` + `scope_hash` + `idempotency_key` + `request_hash` |
| `interaction_audit` | 审计留痕 | 操作人、动作、明细、时间 |
**关系要点**
```
advisor_service_request.result_content_id ──▶ client_facing_content.id
client_facing_content.customer_id ──▶ sys_user.id(客户)
sys_customer_assignment ──▶ 投顾可见客户集合
```
> 「已发布 / 已发送」**不落库**:由 `published_at` 是否为空推导,避免两处状态不一致。
---
## 4. 接口清单
### 4.1 投顾侧(`/api/v1/advisor`,挂限流 + 灰度)
| 方法 | 路径 | 权限 | 端点 ID | 幂等 |
|---|---|---|---|---|
| POST | `/advisor/recommendations` | `product-recommendation:generate:customer` | `ADVISOR_RECOMMEND` | ✅ |
| GET | `/advisor/recommendations/history` | `product-recommendation:history` | `ADVISOR_HISTORY` | — |
| GET | `/advisor/recommendations/published` | `product-recommendation:read:self` | `ADVISOR_PUBLISHED` | — |
| DELETE | `/advisor/recommendations/{contentId}` | `product-recommendation:delete` | `ADVISOR_DELETE_RECOMMENDATION` | ✅ |
| POST | `/advisor/recommendations/{contentId}/reviews` | `product-recommendation:review` | — | ✅ |
| POST | `/advisor/recommendations/{contentId}/publications` | `product-recommendation:publish` | — | ✅ |
| GET | `/advisor/service-requests` | `advisor-request:read` | `ADVISOR_SERVICE_REQUESTS` | — |
| POST | `/advisor/service-requests/{requestNo}/reviews` | `advisor-request:review` | `ADVISOR_SERVICE_REQUEST_REVIEW` | ✅ |
### 4.2 客户侧(`/api/v1/users/me`,只挂限流)
| 方法 | 路径 | 权限 | 端点 ID |
|---|---|---|---|
| GET | `/users/me/advisor-contents` | `product-recommendation:read:self` | `MY_ADVISOR_CONTENTS` |
| GET | `/users/me/advisor-requests` | `advisor-request:read:self` | `MY_ADVISOR_REQUESTS` |
| POST | `/users/me/advisor-requests` | `advisor-request:write:self` | `ADVISOR_REQUEST_CREATE` |
### 4.3 依赖的既有能力
| 端点 | 用途 |
|---|---|
| `ONB001` / `ONB002` | 风险测评取题与提交 |
| `T001`–`T009` | 账户看板、委托、持仓、成交、资金明细 |
| `P001` / `P002` | 产品列表(含最新行情)与历史净值序列(图表数据源) |
---
## 5. 关键时序
### 5.1 生成推荐方案
```mermaid
sequenceDiagram
participant A as 投顾前端
participant C as recommendations.py
participant S as ProductRecommendationService
participant R as AdvisorProductRepository
participant G as 图谱 Neo4j
participant L as DeepSeek 可选
participant DB as MySQL
A->>C: POST /advisor/recommendations (Idempotency-Key)
C->>S: generate(payload, context, key)
S->>S: 权限 + 熔断 + 目标校验
S->>R: 权威候选产品
R-->>S: 候选(含硬约束证据)
S->>G: 组合行业上下文
G-->>S: 正常 / 降级 degraded
S->>S: 过滤 + 排序取 TopN
S->>L: 真实参数 → 推荐依据
L-->>S: 文案 / 失败(回退规则文案)
S->>DB: 落库 pending_review
S-->>A: 返回结果(信封,含 content_id)
```
### 5.2 审核与发布
```mermaid
sequenceDiagram
participant A as 投顾/管理员
participant C as recommendations.py
participant S as ProductRecommendationService
participant DB as MySQL
A->>C: POST .../reviews {decision: approved}
C->>S: review(content_id, decision, context, key)
S->>DB: review_status = approved(幂等)
A->>C: POST .../publications
C->>S: publish(content_id, context, key)
S->>DB: review_status + published_at
S-->>A: 发布成功
Note over DB: 此后客户接口才能读到该方案
```
### 5.3 客户申报受理
```mermaid
sequenceDiagram
participant U as 客户
participant C1 as advisor_service_requests.py
participant S as AdvisorServiceRequestService
participant P as ProductRecommendationService
participant A as 投顾
participant DB as MySQL
U->>C1: POST /users/me/advisor-requests
S->>S: 校验风评有效性(FM-03)
S->>DB: 落库 pending
A->>C1: GET /advisor/service-requests
A->>C1: POST .../reviews {decision: accepted}
S->>P: generate(customer_id) 自动生成草稿
P-->>S: content_id
S->>DB: status=accepted + result_content_id
S-->>A: 返回草稿编号(待审核)
```
---
## 6. 外部依赖与降级矩阵
| 依赖 | 用途 | 不可用时 | 表现 |
|---|---|---|---|
| MySQL | 主存储 | 不可用即故障 | 无降级 |
| Redis | 限流 | 自动降级为放行 | 不限流 |
| Neo4j | 组合行业图谱 | 降级 | 方案标记 `graph_status=degraded`,业务继续 |
| 行情源(腾讯) | 最新价 / 涨跌幅 | 无行情 | 相关指标显示「暂无行情」 |
| 净值源(东财) | 历史净值 | 无数据 | 折线图显示「暂无净值数据」 |
| DeepSeek | 推荐依据改写 | 回退 | 使用规则文案,`reason_source=rule` |
> 行情与净值**没有周期性刷新**:演示前需跑 `tools/sync_market_prices.py` 与 `tools/sync_nav_history.py`(后者全量一次会被超时终止,建议按 `--codes` 分块)。
---
## 7. 前端模块结构
**投顾工作台**(`employee-advisor/dashboard/`)
| 模块 | 职责 |
|---|---|
| `dashboard.js` | 壳、引导、指标卡、在线探测与占位 |
| `actions-module.js` | 七项操作的表单/请求/熔断/结果渲染 |
| `assistant-module.js` | 自然语言意图路由(复用 `actions` 的 resolve) |
| `history-module.js` | 历史方案记录(只读 + 详情弹窗) |
| `service-request-module.js` | 客户申报队列(受理/驳回) |
| `published-module.js` | 已发布交付物 |
| `customer-module.js` | 客户选择 |
| `advisor-engine.js` / `advisor-config.js` | 本地演示引擎与常量 |
**客户端**(`customer/advisor-plans/` + `common/`)
| 文件 | 职责 |
|---|---|
| `advisor-plans.js` | 已收到方案列表、申报表单、我的申报 |
| `common/advisor-plan-view.js` | **投顾与客户共用**的方案可视化渲染(折线图/环形图/指标) |
| `common/advisor-plan.css` | 上述组件的样式(含涨跌色变量) |
**两条硬约定**
1. 同一页面内,同一模块的 `?v=` 版本号必须一致 —— 不同版本会被浏览器当成两个模块,导致双实例。
2. 共享模块**不 import `api-client.js`**,apiClient 由调用方传入(依赖注入),否则两端版本不同会多出实例。
---
## 8. 权限与灰度
| 项 | 说明 |
|---|---|
| 权限码定义源 | `tools/seed_test_rbac.py`(投顾 9071-9074 为客户申报相关) |
| 投顾授权工具 | `tools/grant_advisor_role.py`(权限码必须与种子一致,由 `check_rbac_seed_consistency.py` 校验) |
| 客户授权 | 由种子 `CUSTOMER_PERMISSIONS` 决定;既有环境需按同名补建并授权 |
| 灰度 | `ADVISOR_ROLLOUT_ENABLED` + `ADVISOR_ROLLOUT_CUSTOMER_IDS`;管理员不受限 |
| 幂等 | 所有写接口要求 16-128 位 ASCII `Idempotency-Key` |
---
## 9. 配置清单
| 配置 | 说明 |
|---|---|
| `ADVISOR_ROLLOUT_ENABLED` / `ADVISOR_ROLLOUT_CUSTOMER_IDS` | 投顾灰度 |
| `ADVISOR_REASON_LLM_ENABLED` | 推荐依据是否启用大模型 |
| `ADVISOR_REASON_LLM_BASE_URL` / `_MODEL` / `_TIMEOUT_SECONDS` | 模型地址、模型名、超时 |
| `DEEPSEEK_API_KEY` | 模型密钥(`.env` 已被 `.gitignore` 忽略,不得入库) |
| `NEO4J_URI` / `NEO4J_PASSWORD` | 图谱,留空即降级 |
---
## 10. 已知约束与演进方向
| 约束 | 说明 | 演进建议 |
|---|---|---|
| 候选池可能为空 | 本环境适当性证据与合同快照暂无数据,fail-closed 导致候选为 0 | 接入治理侧证据导入(`tools/import_product_governance_reference.py`) |
| 本地演示引擎 | 画像评分/调仓建议为前端规则模拟 | 后端补接口后切换数据源 |
| 行情/净值无自动刷新 | 依赖手工同步脚本 | 增加定时同步或接入实时源 |
| 涨跌色口径 | 新图表为**涨红跌绿**;产品列表/收益明细为绿涨红跌 | 统一口径后改 `advisor-plan.css` 两个变量即可 |
| alembic 版本指针 | 本地库指针与实际结构对不上,新表依赖手工应用 DDL | 修复版本指针后回归 `alembic upgrade head` |