Files
group_fqcd_jr/app/api/controllers/advisor_service_requests.py
T
Windows 2fe7d0c506 feat(投顾): 客户主动申报投顾方案 + 投顾受理自动生成方案草稿
客户在自己主页提交申报 → 投顾工作台受理 → 自动跑既有推荐逻辑生成一份待审核草稿 →
投顾再走既有的「审核通过 → 发送给客户」。补上原先「客户只能被动等方案」的缺口。

后端
- 新增表 advisor_service_request(本轮新建,带 AUTO_INCREMENT)+ 迁移
  20260916_advisor_service_request(幂等:先查表再建,兼容本库 alembic 指针滞后)
- 新增 AdvisorServiceRequestService:create / list_mine / queue / review
  · 申报前置:风险测评必须存在且未失效(FM-03,12 个月),服务端判
  · 队列复用 ProductRecommendationService._visible_customer_ids(本人 + 归属),待受理排前
  · 受理即调用推荐逻辑生成草稿并把 content_id 回填;生成前置失败给出人话原因并保持待受理
  · 「已发送客户」不落库,由关联方案 published_at 推导,避免两处状态各写各的
- 权限码 9071-9074(客户 write/read:self、投顾 read/review),已进种子与授权工具

前端
- 投顾工作台新增「客户申报」面板(受理 / 驳回,驳回理由客户可见)
- 客户页新增申报表单(金额/期限/风险偏好/备注)与「我的申报」列表

客户自助路由刻意不挂投顾灰度闸门:那是投顾业务的灰度,客户提交自己的申请不该被它拦下。
2026-09-16 18:17:46 +08:00

86 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""客户申报投顾方案 + 投顾受理审批 的 HTTP 入口(`docs/05` 未登记的 AD 段新端点)。
| 段 | 端点 | 权限码 | 摘要 |
|---|---|---|---|
| 客户 | `POST /api/v1/users/me/advisor-requests` | `advisor-request:write:self` | 提交申报 |
| 客户 | `GET /api/v1/users/me/advisor-requests` | `advisor-request:read:self` | 我的申报(含状态) |
| 投顾 | `GET /api/v1/advisor/service-requests` | `advisor-request:read` | 名下客户的申报队列 |
| 投顾 | `POST /api/v1/advisor/service-requests/{request_no}/reviews` | `advisor-request:review` | 受理 / 驳回 |
客户侧刻意**不挂投顾灰度闸门**(`enforce_advisor_rollout`)—— 那是投顾业务的灰度,
客户提交自己的服务申请不该被它拦下;投顾侧照旧挂上。
"""
from __future__ import annotations
from fastapi import APIRouter, Depends, Header, Path
from app.api.dependencies.auth import build_request_context
from app.api.dependencies.rate_limit import enforce_rate_limit
from app.core.contracts import RequestContext
from app.core.errors import ValidationAgentError
from app.service.advisor_rollout_service import enforce_advisor_rollout
from app.service.advisor_service_request_service import (
AdvisorServiceRequestCreate,
AdvisorServiceRequestService,
)
client_router = APIRouter(
prefix="/api/v1/users/me",
tags=["advisor-service-requests"],
dependencies=[Depends(enforce_rate_limit)],
)
advisor_router = APIRouter(
prefix="/api/v1/advisor",
tags=["advisor-service-requests"],
dependencies=[Depends(enforce_rate_limit), Depends(enforce_advisor_rollout)],
)
@client_router.post("/advisor-requests")
async def create_advisor_request(
payload: AdvisorServiceRequestCreate,
context: RequestContext = Depends(build_request_context), # noqa: B008
key: str | None = Header(default=None, alias="Idempotency-Key"),
) -> dict[str, object]:
"""客户主动申报投顾方案(需已完成且未过期的风险测评)。"""
return await AdvisorServiceRequestService().create(payload, context, key)
@client_router.get("/advisor-requests")
async def my_advisor_requests(
context: RequestContext = Depends(build_request_context), # noqa: B008
) -> dict[str, object]:
"""我的申报单(待受理 / 已受理 / 已发送 / 已驳回)。"""
return await AdvisorServiceRequestService().list_mine(context)
@advisor_router.get("/service-requests")
async def advisor_service_request_queue(
context: RequestContext = Depends(build_request_context), # noqa: B008
) -> dict[str, object]:
"""名下客户提交的申报队列(待受理排前面)。"""
return await AdvisorServiceRequestService().queue(context)
@advisor_router.post("/service-requests/{request_no}/reviews")
async def review_advisor_service_request(
payload: dict[str, object],
request_no: str = Path(min_length=8, max_length=32),
context: RequestContext = Depends(build_request_context), # noqa: B008
key: str | None = Header(default=None, alias="Idempotency-Key"),
) -> dict[str, object]:
"""受理(**自动生成方案草稿**)或驳回客户的申报。
受理后投顾仍需走既有的「审核通过 → 发送给客户」,方案才会到客户手里。
"""
decision = payload.get("decision")
if decision not in {"accepted", "rejected"}:
raise ValidationAgentError("decision 必须为 accepted 或 rejected")
comment = payload.get("comment", "")
if not isinstance(comment, str):
raise ValidationAgentError("comment 必须是字符串")
return await AdvisorServiceRequestService().review(
request_no, str(decision), comment, context, key
)