feat(advisor): 登记 17 个投顾端点并补管理员复核入口(A047 待审队列)
## 1. docs/05 §19 补登 17 个投顾端点
这批端点此前**只存在于代码中**,§19 一条都没登记;而 §12 写的入口
`/api/v1/advisory-plans/**` 与实际路径 `/api/v1/advisor/**` 也不符(已修正)。
- **A041–A046**:管理员治理(配置回测、画像标签与漂移复核、推荐方案审核与发布)
- **AD001–AD011**:投顾自用。**新开 `AD` 号段**的理由:它与 A 段是两个不同的权限面
—— A 段是 `/api/v1/admin/**` 管理面,AD 段是 `/api/v1/advisor/**` 投顾自用;
混在一个号段里,"这条到底谁能调"就得逐条去读权限列。
- 另加 AD 段说明块:`investment-goal` 的两套权限码(`...:self` / `...:customer`)、
AD006/AD007 虽在投顾路径下却要求 `admin`、灰度开关 `enforce_advisor_rollout` 前置、
幂等范围(AD008/AD009 无幂等头)、以及 404/409 的失败口径。
§19 现为 **90 个端点 / 9 个号段**,无重复。
## 2. 管理员复核入口 + A047 待审队列
**发现一个让审核链路不可达的缺口**:`review` / `publish` 都要求调用方先拿到键
(推荐方案是 `content_id`、方案书是 `goal_no`),而此前**没有任何端点能列出待审内容**
—— 管理员拿不到键,投顾生成的东西就永远停在待审状态。
- 新增 `GET /api/v1/admin/advisor/pending-contents`(编号 **A047**):一次返回两类待审内容。
两类内容的"待审"取值不同(推荐方案 `pending_review`、方案书 `pending`),
只判其中一个会整类漏掉,所以用 `PENDING_STATES` 一并匹配。
- **为方案书一并查出 `goal_no`** —— 它的审核/发布端点(AD006/AD007)按 `goal_no` 寻址,
只给 `content_id` 的话管理员拿到列表也调不动。已由 integration 测试守住这一点。
- 管理员工作台新增「投顾复核」标签页:列出待审内容,支持审核通过 / 驳回 / 发布;
前端按 `content_type` 自动选择端点、寻址键与载荷
(方案书发布要 `{publish: true}`,推荐方案发布不读 body)。
- 发布前校验状态:未审核通过不允许发布,与 `publish_book` 的 `IllegalState` 一致。
## 3. ⚠️ 同时发现:投顾的三个分析功能对投顾本人不可用
`ProductRecommendationQuery` **没有 `customer_id`** 字段,而 `generate` 用的是
`int(context.user_id)`(`product_recommendation_service.py:69`)—— 即**把投顾自己**
当成了服务对象。投顾是员工、没有风险测评与持仓,于是实测:
POST /api/v1/advisor/recommendations → {"status": "profile_required"}
POST /api/v1/advisor/asset-allocation → {"status": "profile_required"}
**组合分析、资产配置、生成推荐草案这三个功能,投顾调用必然拿不到结果。**
这是"投顾功能很奇怪"的直接来源之一。修它要改接口契约(加 `customer_id`、
并确定"投顾能对哪些客户生成"的权限口径),属产品决策,未在本提交内改动。
验证:unit+contract **1397 passed**;新增 integration 用例 2 passed;ruff 通过;
mypy 251 文件 0 错;A047 实测管理员 200(带出方案书的 `goal_no`)、投顾 403。
This commit is contained in:
+47
-1
@@ -955,7 +955,7 @@ Outbox 消费者按 `event_id` 幂等。失败事件保留并重试,超过阈
|
||||
| 业务域 | 接口入口 | 归属文档 | Agent 边界 |
|
||||
|---|---|---|---|
|
||||
| 客服工单 | `/api/v1/customer-service/handover-tickets/**` | 客服业务文档 | 可生成摘要和转人工请求,不分配、接单、解决或关闭工单 |
|
||||
| 投顾方案 | `/api/v1/advisory-plans/**` | 投顾业务文档 | 只生成分析草案,不代替投顾审核发布 |
|
||||
| 投顾方案 | `/api/v1/advisor/**`(编号见 §19 的 `AD` 段与 `A041`–`A046`) | 本文 §19 | 只生成分析草案,不代替投顾审核发布 |
|
||||
| 场内模拟交易 | `/api/v1/sim-orders/**` | 交易业务文档 | 只读查询,不创建、确认或撤销委托 |
|
||||
| 风控扫描 | `/api/v1/risk/**` | 风控业务文档 | 可解释规则结果,不启动人工处置 |
|
||||
| 风险预警 | `/api/v1/risk/**` | 风控业务文档 | 只读分析,不确认、升级或关闭预警 |
|
||||
@@ -1108,6 +1108,24 @@ GET /internal/metrics
|
||||
| M004 | `POST /api/v1/users/me/memory-candidates/{candidate_id}/decisions` | `memory:candidate:confirm` | 必须 | `200` | 用户确认/拒绝 |
|
||||
| A039 | `GET /api/v1/admin/customer-profile-candidates` | `memory:candidate:review` | 否 | `200` | 候选审核列表 |
|
||||
| A040 | `POST /api/v1/admin/customer-profile-candidates/{candidate_id}/reviews` | `memory:candidate:review` | 必须 | `200` | 候选审核 |
|
||||
| A041 | `POST /api/v1/admin/advisor/asset-allocation-backtests` | `asset-allocation:backtest`(+`admin`) | 必须 | `201` | 配置回测 |
|
||||
| A042 | `GET /api/v1/admin/advisor/profile-tags` | `profile-governance:read`(+`admin`) | 否 | `200` | 敏感访问 |
|
||||
| A043 | `GET /api/v1/admin/advisor/profile-drift-reviews` | `profile-governance:read`(+`admin`) | 否 | `200` | 敏感访问 |
|
||||
| A044 | `POST /api/v1/admin/advisor/profile-drift-reviews/{review_id}/reviews` | `profile-governance:review`(+`admin`) | 必须 | `200` | 画像漂移复核 |
|
||||
| A045 | `POST /api/v1/admin/advisor/recommendations/{content_id}/reviews` | `product-recommendation:review`(+`admin`) | 必须 | `200` | 推荐方案审核 |
|
||||
| A046 | `POST /api/v1/admin/advisor/recommendations/{content_id}/publications` | `product-recommendation:publish`(+`admin`) | 必须 | `200` | 推荐方案发布 |
|
||||
| A047 | `GET /api/v1/admin/advisor/pending-contents` | `product-recommendation:review`(+`admin`) | 否 | `200` | 否 |
|
||||
| AD001 | `POST /api/v1/advisor/investment-goals` | `investment-goal:write:self` / `:customer` | 必须 | `201` | 投资目标创建 |
|
||||
| AD002 | `GET /api/v1/advisor/investment-goals/current` | `investment-goal:read:self` | 否 | `200` | 否 |
|
||||
| AD003 | `GET /api/v1/advisor/customers/{customer_id}/investment-goals/current` | `investment-goal:read:self` / `:customer` | 否 | `200` | 否 |
|
||||
| AD004 | `POST /api/v1/advisor/investment-goals/{goal_no}/confirmations` | `investment-goal:confirm:self` / `:customer` | 必须 | `200` | 目标确认 |
|
||||
| AD005 | `GET /api/v1/advisor/investment-goals/{goal_no}/goal-book` | `investment-goal:read:self` / `:customer` | 否 | `200` | 否 |
|
||||
| AD006 | `POST /api/v1/advisor/investment-goals/{goal_no}/goal-book/reviews` | `investment-goal:review`(+`admin`) | 必须 | `200` | 方案书审核 |
|
||||
| AD007 | `POST /api/v1/advisor/investment-goals/{goal_no}/goal-book/publications` | `investment-goal:publish`(+`admin`) | 必须 | `200` | 方案书发布 |
|
||||
| AD008 | `POST /api/v1/advisor/portfolio-analysis` | `portfolio-analysis:read:self` | 否 | `200` | 否 |
|
||||
| AD009 | `POST /api/v1/advisor/asset-allocation` | `asset-allocation:generate:self` | 否 | `200` | 否 |
|
||||
| AD010 | `POST /api/v1/advisor/recommendations` | `product-recommendation:generate:self` | 必须 | `200` | 否 |
|
||||
| AD011 | `GET /api/v1/advisor/recommendations/published` | `product-recommendation:read:self` | 否 | `200` | 否 |
|
||||
| K001 | `GET /api/v1/knowledge-references/{reference_token}` | `knowledge:reference:read` | 否 | `200` | 否 |
|
||||
| K002 | `POST /api/v1/knowledge/upload` | `knowledge:manage` | 否 | `201` | 知识文档变更 |
|
||||
| K003 | `GET /api/v1/knowledge/list` | `knowledge:manage` | 否 | `200` | 否 |
|
||||
@@ -1192,6 +1210,34 @@ GET /internal/metrics
|
||||
|
||||
业务域接口 `/customer-service/handover-tickets/**`、`/advisory-plans/**`、`/sim-orders/**`、`/risk-scans/**` 和 `/risk-alerts/**` 的具体方法、请求体、领域状态机和错误码分别由对应业务文档登记;它们仍必须遵守本文第 3-5、11 和 12 节。
|
||||
|
||||
> **AD 段(投顾自用)与 A041–A046(投顾治理)的六点说明**:
|
||||
>
|
||||
> 这批端点原先**只存在于代码中**,`§19` 一条都没登记(2026-09-13 补登)。当时 §12 写的
|
||||
> 入口是 `/api/v1/advisory-plans/**`,与实际路径 `/api/v1/advisor/**` **不符**,
|
||||
> 也已一并修正。门禁脚本 `check_docs_endpoint_ids.py` 只校验 §19 **内部**编号唯一性,
|
||||
> **查不出"代码里有端点、文档里没登记"**这类缺口 —— 新增端点时请按 §20 主动登记。
|
||||
>
|
||||
> - **为什么新开 `AD` 号段**:这批端点落在 `/api/v1/advisor/**`,与 A 段的
|
||||
> `/api/v1/admin/**` 是两个不同的权限面(A 段是管理面,AD 段是投顾自用)。
|
||||
> 混在一个号段里,"这条到底是投顾能调还是只有管理员能调"就得逐条去读权限列。
|
||||
> - **`investment-goal` 的两套权限码**:`investment-goal:<action>:self`(本人)与
|
||||
> `investment-goal:<action>:customer`(名下客户),由
|
||||
> `InvestmentGoalService._assert_customer_access` 按 `customer_id` 是否等于
|
||||
> `context.user_id` 选择。`:customer` 那一支还要求数据范围是 `all`,或者
|
||||
> `own_customers` 且该客户确实在 `sys_customer_assignment` 里 —— 否则返回
|
||||
> `404 客户不可访问`(**注意是 404 不是 403**:不向调用方泄露"该客户存在但你没权看")。
|
||||
> - **AD006 / AD007 虽在投顾路径下,却要求 `admin`**:方案书的审核与发布是**管理员的
|
||||
> 复核动作**(`review_book` / `publish_book` 都带 `admin=True`),投顾本人发不出来。
|
||||
> 这是有意的复核环节,不是遗漏。
|
||||
> - **`enforce_advisor_rollout` 是额外前置**:AD 段每一条都挂了投顾灰度开关,
|
||||
> 未放行时在鉴权之后、业务逻辑之前就被拦下。
|
||||
> - **幂等**:AD001 / AD004 / AD006 / AD007 / AD010 与 A041 / A044 / A045 / A046 接受
|
||||
> `Idempotency-Key`;**AD008 / AD009(组合分析、资产配置)没有幂等头** ——
|
||||
> 它们是纯分析入口,不落业务单据。
|
||||
> - **失败口径**:目标或方案书不存在 → `404`;状态不允许(重复确认、未审核就发布)
|
||||
> → `409`。⚠️ 409 目前一律复用 `RUN_NOT_CANCELLABLE` 这个码(见 §3.6),
|
||||
> 所以"投资目标不能确认"会报出字面像"运行不可取消"的码,属于已知的文档缺口。
|
||||
|
||||
> **P001(公开产品列表)的四点说明**:
|
||||
>
|
||||
> - **鉴权口径:要求令牌但不校验权限码。** 访客令牌的角色是 `visitor`、**不带任何权限**
|
||||
|
||||
Reference in New Issue
Block a user