## 现象(你的截图)
任务 `PM-20260914-0007` 的合规检查结果为:
```
业绩排名来源不满足要求
补充三年期以上公开评价数据来源 · block
```
输入快照里的实证:
```json
ranking = {"enabled": true, "ranking_text": null, "public_source": null,
"institution_name": null, "evaluation_period_years": null}
```
## 根因:勾选框有了,填来源的地方没有
`promotion_compliance.py:82-91` 的规则是:
```python
if ranking.get("enabled"):
years = _years(ranking.get("evaluation_period_years"))
if years < 3 or not ranking.get("institution_name") or not ranking.get("public_source"):
→ block "performance.ranking_source_invalid"
```
而前端**只有**一个复选框 `performance_info.ranking.enabled`:
- 表单里**没有** `institution_name` / `public_source` / `evaluation_period_years` /
`ranking_text` 四个输入(实测枚举全部 `data-promo-field` 只有那一个 ranking 字段);
- `inputsPayload()` 也只提交 `ranking: { enabled }`。
⇒ 运营一旦勾选「展示排名」,来源四项永远是 null,规则必然阻断,
而界面上**无处可填** —— 要么取消勾选,要么永远生成不了。这是**死锁**,不是数据问题。
## 修法(前后端契约字段本就有,只补界面与提交)
1. `promotion/index.html`:在展示选项上方新增一组输入(`data-ranking-source`):
评价期间(年,需 ≥3)/ 评价机构 / 公开来源 / 排名文本;
2. `promotion/promotion.js`:`inputsPayload()` 的 `ranking` 补上这四个字段
(契约 `RankingInfo` 本就定义了 `institution_name` / `evaluation_period_years`
/ `ranking_text` / `public_source`,`evaluation_period_years` 是字符串,如 "3年")。
## 验证
- 用**真实任务 0007 的输入**跑 `PromotionComplianceChecker.check_inputs()`:
- 现状:3 条阻断(`history_short` + **`ranking_source_invalid`** + `data_attachment_missing`);
- 把来源填全后:**`ranking_source_invalid` 消失**(其余两条由"未上传业绩文件"引起,上传即解);
- 反例:评价期间改成 2 年 → 仍拦;3 年但缺公开来源 → 仍拦(规则没有被放松);
- 从源文件抽出 `inputsPayload()` 用桩真实调用:四个来源字段都进 payload;未勾选时 `enabled=false`;
- `index.html` 标签净增 +1 `<div>` / +1 `</div>`(结构平衡);`node --check` 通过;
- `pytest tests/unit tests/contract` → 1458 passed, 2 skipped, 0 failed。
## 附带说明
你看到的是"2 条"是因为**点了两次生成**:每次生成都会把该次的 findings 落库,
页面"读取合规结果"会把历史记录一并列出(不是一次调用重复产出)。
Portal 路由与角色映射
正式门户使用原生 JavaScript、原生 CSS 与同源 fetch,静态资源统一从 /static/portal/ 加载。
| 路由 | 角色 | 数据源 |
|---|---|---|
/portal/guest/home/ |
访客 / 全部 | P001(推荐位取前 3 只) |
/portal/guest/products/ |
访客 / 全部 | P001 |
/portal/guest/product-detail/?code= |
访客 / 全部 | P001(不含历史净值曲线) |
/portal/customer/login/ |
未登录客户 | A034 |
/portal/customer/dashboard/ |
customer / admin | T001、T002 |
/portal/customer/holdings/ |
customer / admin | T006 |
/portal/customer/profit-loss/ |
customer / admin | T001 |
/portal/customer/orders/ |
customer / admin | T003 |
/portal/customer/transactions/ |
customer / admin | T007 |
/portal/customer/cash-ledger/ |
customer / admin | T009 |
/portal/customer/risk-questionnaire/ |
customer | ONB001、ONB002 |
/portal/employee-console/login/ |
未登录员工 / 管理员 | A034 |
/portal/employee-console/workspace/ |
admin / super_admin | A002-A006、A012、A033、A035-A040、客服转人工管理接口 |
/portal/employee-risk/dashboard/ |
risk_operator / admin / super_admin | /api/v1/risk/**、R001-R003 |
/portal/employee-advisor/dashboard/ |
advisor / admin / super_admin | /api/v1/advisor/recommendations/published(本人 + 名下归属客户的已发布交付物) |
运营账号默认进入 /portal/employee-operations/offsite/ |
operator / admin / super_admin | /api/v1/offsite-fund/mails、/api/v1/offsite-fund/mailbox-status |
投顾页的数据口径:接口按「本人 +
sys_customer_assignment里名下归属客户」过滤,且同时覆盖investment_goal_book(方案书,发布后review_status='published')与advisor_recommendation_plan(推荐方案,审核后review_status='approved')两类内容。 方案书的审核与发布都要求管理员(investment-goal:review/publish都带admin=True), 投顾自己发不出来 —— 这是有意设计的复核环节,不是缺陷。
客服浮窗由公开首页、基金产品页和客户工作台统一挂载。访客使用 /api/v1/visitor-tokens
获取短期令牌(角色 visitor,权限只有 agent:run + knowledge:query),因此必须走
query_knowledge 这个工具名;登录客户走 search_knowledge。两者都要出现在发布配置的
agent_tools/customer_service:<intent> 白名单里,缺哪一条,对应人群就一问即失败。
公开产品数据来自 GET /api/v1/products(编号 P001,见 docs/05 §19),
产品与净值取自 fin_product(status='上市')、行情取自 fin_market_price 的最新一行。
访客页面通过 common/visitor-token.js 取短期访客令牌后调用;该文件是访客令牌的唯一实现
(客服浮窗也用它),不要在页面里另写一份,否则存储 key 与过期判断迟早不一致。
公开产品数据不得与登录后的真实账户数据混用。
三点口径(改前端前先读):
change_pct可能是null(行情只同步过一个交易日时算不出涨跌)。 调用方必须显示"暂无",不得当成0——formatPercent收到null会渲染成+0.00%, 那等于告诉客户"今天平盘"。- 历史净值曲线没有数据源:
fin_nav_history目前 0 行,详情页因此不画走势图, 并显式说明"尚未接入"。此前那条曲线是 mock 里 12 个编造点位 —— 走势图最容易被当成真数据。 - 产品级披露在
common/product-notes.js(如 510300 的"同指数参考产品,非本公司发行")。fin_product没有这个字段,所以它留在前端;新增需要披露的产品时改那一份。 凡渲染公开产品的页面都要挂data-source-notice说明数据来源。
客户页面均由 common/auth.js 执行入口守卫,接口路径只在 common/api-client.js 的端点表登记。