客户在自己主页提交申报 → 投顾工作台受理 → 自动跑既有推荐逻辑生成一份待审核草稿 → 投顾再走既有的「审核通过 → 发送给客户」。补上原先「客户只能被动等方案」的缺口。 后端 - 新增表 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),已进种子与授权工具 前端 - 投顾工作台新增「客户申报」面板(受理 / 驳回,驳回理由客户可见) - 客户页新增申报表单(金额/期限/风险偏好/备注)与「我的申报」列表 客户自助路由刻意不挂投顾灰度闸门:那是投顾业务的灰度,客户提交自己的申请不该被它拦下。
86 lines
3.6 KiB
Python
86 lines
3.6 KiB
Python
"""客户申报投顾方案 + 投顾受理审批 的 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
|
||
)
|