feat(投顾): 方案交付落点、可视化图表与推荐依据的大模型增强
交付落点
- 新增 GET /api/v1/users/me/advisor-contents(客户读**自己**已发布方案):
「发送给客户」原先只改数据状态、客户端没有任何页面或接口能读到它
- 客户端新增「我的投顾方案」页与导航入口
可视化(投顾结果区与客户页**共用** common/advisor-plan-view.js,避免两处漂移)
- 净值折线图(带坐标轴与网格)、组合业绩等权合成曲线(含区间收益与最大回撤)、
资产配置环形图与图例、组合构成条
- 修 num(null)=0 的假 0:Number(null)/Number('') 会得 0,导致「没数据」被渲染成 0.00%;
现一律显示「--」。同理管理费/起投未维护时按没数据处理,不显示 0
- 涨跌口径为「涨红跌绿」(A 股习惯),由 CSS 变量 --plan-up / --plan-down 集中定义
推荐依据接入大模型(可选,失败即回退)
- 新增 AdvisorReasonService:**只改文案,不参与选品**(候选池与排序在它之前已固定)
- 输入只允许是已算出的真实参数(风险等级、排序得分、区间收益、最大回撤、期限与流动性)
- 命中收益承诺词(保本/保证收益/稳赚/无风险…)整条丢弃并回退规则文案
- 未启用 / 缺密钥 / 超时 / 解析失败一律回退,推荐主流程不因模型不可用而失败
- 前端标注来源(AI 生成 / 规则生成)
数据与权限
- 客户角色补齐:绑 customer 角色、补建缺失的账户与交易段权限码(9060-9065)
- 净值全量同步(20 只产品),行情同步脚本按 --codes 分块(全量一次会被超时终止)
测试
- 新增 tests/unit/service/test_advisor_reason_service.py(10 项,专测三条合规边界)
- 前端模块自检纳入 service-request-module;补「两处共用同一渲染」回归测试
This commit is contained in:
@@ -21,6 +21,15 @@ admin_router = APIRouter(
|
||||
tags=["platform-admin"],
|
||||
dependencies=[Depends(enforce_rate_limit)],
|
||||
)
|
||||
#: 客户自助路由:客户看**自己**已发布的投顾交付物。
|
||||
#: 前缀挂在 `/api/v1/users/me` 下,与 §T 段(交易)同一约定。
|
||||
#: **故意不挂 `enforce_advisor_rollout`** —— 那是投顾业务的灰度闸门(按
|
||||
#: `sys_customer_assignment` 归属命中白名单),客户看自己的交付物不该被它拦下。
|
||||
client_router = APIRouter(
|
||||
prefix="/api/v1/users/me",
|
||||
tags=["advisor-deliveries"],
|
||||
dependencies=[Depends(enforce_rate_limit)],
|
||||
)
|
||||
|
||||
|
||||
@advisor_router.post("/recommendations")
|
||||
@@ -41,6 +50,19 @@ async def published_recommendations(
|
||||
return await ProductRecommendationService().published(context)
|
||||
|
||||
|
||||
@advisor_router.get("/recommendations/history")
|
||||
async def recommendation_history(
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
"""历史方案留档:本人 + 名下客户的**全部状态**方案与方案书。
|
||||
|
||||
与 `/recommendations/published` 分开的原因:published 只给"已发布、对客户可见"的内容,
|
||||
投顾刚生成、还在待审的草案不在其中。历史记录面板要的是"以前生成过什么",
|
||||
所以这里返回全部状态,按生成时间倒序。
|
||||
"""
|
||||
return await ProductRecommendationService().history(context)
|
||||
|
||||
|
||||
# ---- 投顾自助审核/发布(2026-09-14 新增)------------------------------------
|
||||
#
|
||||
# 为什么要有这两个**投顾侧**路由:审核/发布原先只在 `/api/v1/admin/advisor/...`
|
||||
@@ -76,6 +98,20 @@ async def advisor_publish_recommendation(
|
||||
return await ProductRecommendationService().publish(content_id, context, key)
|
||||
|
||||
|
||||
@advisor_router.delete("/recommendations/{content_id}")
|
||||
async def advisor_delete_recommendation(
|
||||
content_id: int = Path(gt=0),
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
key: str | None = Header(default=None, alias="Idempotency-Key"),
|
||||
) -> dict[str, object]:
|
||||
"""删除推荐方案(历史记录里的「删除」按钮)。
|
||||
|
||||
仅推荐方案可删:投资方案书被 `advisor_investment_goal.goal_book_content_id`
|
||||
(`NO ACTION` 外键、`NOT NULL`)引用,硬删会撞外键 —— 方案书走自己的生命周期。
|
||||
"""
|
||||
return await ProductRecommendationService().delete(content_id, context, key)
|
||||
|
||||
|
||||
@admin_router.get(
|
||||
"/advisor/pending-contents",
|
||||
dependencies=[Depends(enforce_advisor_rollout)],
|
||||
@@ -123,3 +159,16 @@ async def publish_recommendation(
|
||||
key: str | None = Header(default=None, alias="Idempotency-Key"),
|
||||
) -> dict[str, object]:
|
||||
return await ProductRecommendationService().publish(content_id, context, key)
|
||||
|
||||
|
||||
# ---- 客户侧:接收投顾交付物 ------------------------------------------------
|
||||
#
|
||||
# 「发送给客户」此前**没有落点** —— 投顾发布后只是把 `published_at` 置上,
|
||||
# 客户端门户没有任何页面/接口能读到它(客户"可见"只体现在数据口径上)。
|
||||
# 这条路由补上落点:客户登录后看**自己**已发布的推荐方案与方案书。
|
||||
@client_router.get("/advisor-contents")
|
||||
async def my_advisor_contents(
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
"""我的投顾方案:本人已审核发布的投顾交付物(按发布时间倒序,最多 50 条)。"""
|
||||
return await ProductRecommendationService().my_published(context)
|
||||
|
||||
Reference in New Issue
Block a user