merge: 集成投顾组 3 个提交(解除与「投顾模块清除」的冲突)+ 客服 Agent 重构收口
## 为什么要合并
远端 `origin/qyqy_develop` 领先 3 个提交(`5607751` / `2fe7d0c` / `74b7d00`:投顾需求与架构文档、
客户主动申报投顾方案 + 受理自动出草稿、方案交付落点与推荐依据 LLM 增强),而本地 `5d0becb`
按 `D4.4` / `D4.5`(CS-PURGE-2026-012/013)把投顾模块整体清除了。**两个目标不可兼得**:
远端新代码反向 import 已被清除的模块(`app.model.investment_goal`、
`app.service.product_recommendation_service`、`app.service.advisor_rollout_service`),
强行推进只会让两边都跑不起来。
**裁定:投顾组的新功能 > 本地的投顾清除。** 依据是 `D4.4` §0-②③ 自己写下的风险
——按名字清投顾会同时拆掉产品数据底座与 MVP 硬阻断,并失去"改 6 个底座文件时的对照组"。
本次合并因此**恢复投顾模块**;就代码面而言,`D4.4` / `D4.5` 的清除结果被本次合并取代
(留痕见 `开发文档\D1.6` §4.37)。
## 冲突怎么解的(12 处)
- **8 处 modify/delete 取远端**:`recommendations.py` / `product_recommendation_service.py` /
`employee-advisor/dashboard/{actions-module.js,dashboard.css,dashboard.js,index.html}` /
`tools/{check_portal_modules.py,grant_advisor_role.py}` —— 即"我删、远端改",保留投顾文件。
- **3 处内容冲突取远端**:`app/main.py`(投顾 import 与 `include_router`)、
`common/api-client.js`(投顾端点表)、`tests/unit/api/test_portal_frontend.py`(4 条投顾前端契约)。
- **1 处取远端 + 保留我方**:`app/main.py` 解除冲突的同时,保留本轮的
`/customer-service-test` 挂载移除(该联调页与用例已随重构作废)。
## 因"取消清除"而必须回滚的语义改动(否则恢复出来的投顾代码跑不动)
- `app/service/agent/bootstrap.py`:恢复 `AdvisorAgent` 与 5 个投顾工具注册
(`query_investment_goal` / `analyze_portfolio` / `generate_asset_allocation` /
`recommend_products` / `compare_products`);客服 Agent 注释按本轮口径保留。
- `app/core/config.py`:恢复 `advisor_rollout_enabled` / `advisor_rollout_customer_ids`。
- `app/static/portal/common/layout/app-shell.js`:恢复投顾工作台导航与 `advisor` 角色名。
- `tools/seed_test_rbac.py`:恢复"admin 取全量元组"的授权模型(保留远端新增的
9070-9074 权限码与客户侧 9071/9072 绑定)。
- `tools/portal_api_check.py`:恢复投顾实测用例(AD003/AD005/AD011/A047 与 `advisor_t` 登录),
并**新增判定**:被渲染的集合为空(0 条)时判 `SKIP` 而不是 `FAIL`
—— "没有行"与"字段没带"是两回事,混报会把排查方向带偏。
- `app/static/portal/common/api-client.js`:以远端为基准,重新叠加本轮的
**访客令牌 `Authorization` 优先**修复(浮窗访客身份稳定性)。
## 数据库夹具同步(代码恢复 ⇒ 夹具也要恢复)
- `tools/grant_advisor_role.py`:新建 `advisor` 角色并授权(实测 34 项权限)。
- `tools/create_test_user.py --id 9020 --username advisor_t --role advisor`:重建演示账号。
## 集成期发现并修掉的过期断言
- `tests/unit/test_advisor_migration_contract.py`:alembic 末端钉死值仍是
`20260914_baseline_auto_increment`,而远端新增了 `20260916_advisor_service_request`
⇒ 这条断言**在远端分支上本身就是红的**。本次把它更新到新末端并补了注释。
## 验证(本机实测)
| 门禁 | 结果 |
|---|---|
| `pytest -q`(全量,含集成) | **1909 passed / 3 skipped / 0 failed** |
| `ruff check app tools tests` | 20(远端分支 22,本地仅客服线基线 19) |
| `mypy app` | 2(= 既有基线) |
| `tools/portal_api_check.py` | 40 项:通过 35 / 失败 0 / 跳过 5 |
| `tools/e2e_smoke_test.py --read-only` | 31/31 |
| `_eval_harness/http_probe.py` | 11/11 succeeded |
| `_consistency.py` | GATE PASS |
| `_fe_boundary_http.py`(前端入参边界真机) | 全部符合预期 |
## 未做(如实登记)
- **投顾演示数据未灌**:`AD011` / `A047` 需要 `advisor_product_suitability_reference`
这类带 `source_url` + `document_sha256` 的证据行,而披露文件不在仓库里;
`tools/seed_advisor_demo.py` 明确"不编证据"(fail closed),故这两条按空集 SKIP。
- **客服线文档目录仍未入库**:`客服agent/`、`开发文档/`(权威副本在本机)与
`_chunks_report.txt`(本地构建产物)依旧排除在提交之外。
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
"""客户申报投顾方案 + 投顾受理审批 的 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
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Analysis-only dynamic asset allocation endpoint."""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.api.dependencies.auth import build_request_context
|
||||
from app.api.dependencies.rate_limit import enforce_rate_limit
|
||||
from app.api.schemas.asset_allocation import AssetAllocationQuery
|
||||
from app.core.contracts import RequestContext
|
||||
from app.service.advisor_rollout_service import enforce_advisor_rollout
|
||||
from app.service.asset_allocation_service import AssetAllocationService
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/v1/advisor", tags=["advisor-asset-allocation"],
|
||||
dependencies=[Depends(enforce_rate_limit), Depends(enforce_advisor_rollout)],
|
||||
)
|
||||
|
||||
|
||||
@router.post("/asset-allocation")
|
||||
async def generate_asset_allocation(
|
||||
payload: AssetAllocationQuery,
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
return await AssetAllocationService(enforce_profile_governance=True).generate_for_agent(
|
||||
payload, context
|
||||
)
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Investment-goal collection and goal-book review endpoints."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, Header, status
|
||||
|
||||
from app.api.dependencies.auth import build_request_context
|
||||
from app.api.dependencies.rate_limit import enforce_rate_limit
|
||||
from app.api.schemas.investment_goals import (
|
||||
InvestmentGoalBookPublish,
|
||||
InvestmentGoalBookReview,
|
||||
InvestmentGoalConfirmation,
|
||||
InvestmentGoalCreate,
|
||||
)
|
||||
from app.core.contracts import RequestContext
|
||||
from app.service.advisor_rollout_service import enforce_advisor_rollout
|
||||
from app.service.investment_goal_service import InvestmentGoalService
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/v1/advisor", tags=["advisor-investment-goals"],
|
||||
dependencies=[Depends(enforce_rate_limit), Depends(enforce_advisor_rollout)],
|
||||
)
|
||||
|
||||
|
||||
@router.post("/investment-goals", status_code=status.HTTP_201_CREATED)
|
||||
async def create_investment_goal(
|
||||
payload: InvestmentGoalCreate,
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
key: str | None = Header(default=None, alias="Idempotency-Key"),
|
||||
) -> dict[str, object]:
|
||||
return await InvestmentGoalService().create(payload, context, key)
|
||||
|
||||
|
||||
@router.get("/investment-goals/current")
|
||||
async def current_own_investment_goal(
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
return await InvestmentGoalService().current(int(context.user_id), context)
|
||||
|
||||
|
||||
@router.get("/customers/{customer_id}/investment-goals/current")
|
||||
async def current_customer_investment_goal(
|
||||
customer_id: int,
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
return await InvestmentGoalService().current(customer_id, context)
|
||||
|
||||
|
||||
@router.post("/investment-goals/{goal_no}/confirmations")
|
||||
async def confirm_investment_goal(
|
||||
goal_no: str,
|
||||
payload: InvestmentGoalConfirmation,
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
key: str | None = Header(default=None, alias="Idempotency-Key"),
|
||||
) -> dict[str, object]:
|
||||
return await InvestmentGoalService().confirm(goal_no, context, key)
|
||||
|
||||
|
||||
@router.get("/investment-goals/{goal_no}/goal-book")
|
||||
async def investment_goal_book(
|
||||
goal_no: str,
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
) -> dict[str, Any]:
|
||||
return await InvestmentGoalService().goal_book(goal_no, context)
|
||||
|
||||
|
||||
@router.post("/investment-goals/{goal_no}/goal-book/reviews")
|
||||
async def review_investment_goal_book(
|
||||
goal_no: str,
|
||||
payload: InvestmentGoalBookReview,
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
key: str | None = Header(default=None, alias="Idempotency-Key"),
|
||||
) -> dict[str, object]:
|
||||
return await InvestmentGoalService().review_book(goal_no, payload, context, key)
|
||||
|
||||
|
||||
@router.post("/investment-goals/{goal_no}/goal-book/publications")
|
||||
async def publish_investment_goal_book(
|
||||
goal_no: str,
|
||||
payload: InvestmentGoalBookPublish,
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
key: str | None = Header(default=None, alias="Idempotency-Key"),
|
||||
) -> dict[str, object]:
|
||||
return await InvestmentGoalService().publish_book(goal_no, payload, context, key)
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Read-only portfolio-analysis endpoint."""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.api.dependencies.auth import build_request_context
|
||||
from app.api.dependencies.rate_limit import enforce_rate_limit
|
||||
from app.api.schemas.portfolio_analysis import PortfolioAnalysisQuery
|
||||
from app.core.contracts import RequestContext
|
||||
from app.service.advisor_rollout_service import enforce_advisor_rollout
|
||||
from app.service.portfolio_analysis_service import PortfolioAnalysisService
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/v1/advisor", tags=["advisor-portfolio-analysis"],
|
||||
dependencies=[Depends(enforce_rate_limit), Depends(enforce_advisor_rollout)],
|
||||
)
|
||||
|
||||
|
||||
@router.post("/portfolio-analysis")
|
||||
async def analyze_portfolio(
|
||||
payload: PortfolioAnalysisQuery,
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
return await PortfolioAnalysisService(enforce_profile_governance=True).analyze_for_agent(
|
||||
payload, context
|
||||
)
|
||||
@@ -0,0 +1,174 @@
|
||||
"""Recommendation generation and reviewed publication endpoints."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
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.product_recommendation_contracts import ProductRecommendationQuery
|
||||
from app.service.advisor_rollout_service import enforce_advisor_rollout
|
||||
from app.service.product_recommendation_service import ProductRecommendationService
|
||||
|
||||
advisor_router = APIRouter(
|
||||
prefix="/api/v1/advisor",
|
||||
tags=["advisor-recommendations"],
|
||||
dependencies=[Depends(enforce_rate_limit), Depends(enforce_advisor_rollout)],
|
||||
)
|
||||
admin_router = APIRouter(
|
||||
prefix="/api/v1/admin",
|
||||
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")
|
||||
async def generate_recommendation(
|
||||
payload: ProductRecommendationQuery,
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
key: str | None = Header(default=None, alias="Idempotency-Key"),
|
||||
) -> dict[str, object]:
|
||||
return await ProductRecommendationService(enforce_profile_governance=True).generate(
|
||||
payload, context, key
|
||||
)
|
||||
|
||||
|
||||
@advisor_router.get("/recommendations/published")
|
||||
async def published_recommendations(
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
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/...`
|
||||
# 下、且服务层还有 `admin=True` 角色闸门 —— 于是投顾生成完草案后**无法自行推进**,
|
||||
# 草案永远停在 `pending_review`,必须切到管理员账号才能审。业务要求投顾能审自己的方案。
|
||||
#
|
||||
# 与 admin 路由的关系:两者调用**同一个服务方法**,管理面复核队列
|
||||
# (`GET /api/v1/admin/advisor/pending-contents`)仍保持 admin 专属、未放宽。
|
||||
@advisor_router.post("/recommendations/{content_id}/reviews")
|
||||
async def advisor_review_recommendation(
|
||||
payload: dict[str, Any],
|
||||
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]:
|
||||
decision = payload.get("decision")
|
||||
if decision not in {"approved", "rejected"}:
|
||||
from app.core.errors import ValidationAgentError
|
||||
|
||||
raise ValidationAgentError("decision 必须为 approved 或 rejected")
|
||||
comment = payload.get("comment", "")
|
||||
if not isinstance(comment, str):
|
||||
raise ValueError("comment must be a string")
|
||||
return await ProductRecommendationService().review(content_id, decision, comment, context, key)
|
||||
|
||||
|
||||
@advisor_router.post("/recommendations/{content_id}/publications")
|
||||
async def advisor_publish_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]:
|
||||
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)],
|
||||
)
|
||||
async def pending_advisor_contents(
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
"""待审核的投顾内容(推荐方案 + 投资方案书)。编号 `A047`。
|
||||
|
||||
补这个入口的原因:审核/发布端点都要求先拿到 `content_id`,而此前**没有**任何
|
||||
端点能列出待审内容,管理员拿不到 id ⇒ 审核链路不可达。返回体里的
|
||||
`content_type` 用于前端区分两类内容。
|
||||
"""
|
||||
return await ProductRecommendationService().pending_reviews(context)
|
||||
|
||||
|
||||
@admin_router.post(
|
||||
"/advisor/recommendations/{content_id}/reviews",
|
||||
dependencies=[Depends(enforce_advisor_rollout)],
|
||||
)
|
||||
async def review_recommendation(
|
||||
payload: dict[str, Any],
|
||||
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]:
|
||||
decision = payload.get("decision")
|
||||
if decision not in {"approved", "rejected"}:
|
||||
from app.core.errors import ValidationAgentError
|
||||
|
||||
raise ValidationAgentError("decision 必须为 approved 或 rejected")
|
||||
comment = payload.get("comment", "")
|
||||
if not isinstance(comment, str):
|
||||
raise ValueError("comment must be a string")
|
||||
return await ProductRecommendationService().review(content_id, decision, comment, context, key)
|
||||
|
||||
|
||||
@admin_router.post(
|
||||
"/advisor/recommendations/{content_id}/publications",
|
||||
dependencies=[Depends(enforce_advisor_rollout)],
|
||||
)
|
||||
async def publish_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]:
|
||||
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)
|
||||
@@ -0,0 +1,3 @@
|
||||
from app.core.advisor_allocation_contracts import AssetAllocationQuery
|
||||
|
||||
__all__ = ["AssetAllocationQuery"]
|
||||
@@ -0,0 +1,15 @@
|
||||
"""HTTP DTOs for investment-goal collection and goal-book review."""
|
||||
|
||||
from app.core.investment_goal_contracts import (
|
||||
InvestmentGoalBookPublish,
|
||||
InvestmentGoalBookReview,
|
||||
InvestmentGoalConfirmation,
|
||||
InvestmentGoalCreate,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"InvestmentGoalBookPublish",
|
||||
"InvestmentGoalBookReview",
|
||||
"InvestmentGoalConfirmation",
|
||||
"InvestmentGoalCreate",
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
from app.core.portfolio_analysis_contracts import PortfolioAnalysisQuery
|
||||
|
||||
__all__ = ["PortfolioAnalysisQuery"]
|
||||
Reference in New Issue
Block a user