Files
group_fqcd_jr/app/static/portal
lzf_0626 e31420df29 feat(portal): 补齐四个前端缺口——客户详情、知识库管理、配置项与路由编辑
先审计了全部 **141 个后端端点**:前端注册 68 个,注册的**全部有效**(没有一个打不通)。
本提交补的是其中真正影响可用性的四类。

## 1. 客户端:委托详情(T004)与成交详情(T008)

两个接口后端一直存在,但订单页 / 成交明细页**只接了列表(T003 / T007)** ——
客户点不进任何一条记录,看不到成交价、费用构成、确认时间与行情来源。

改为**行内展开**(点「详情」在原行下方展开,再点收起),不跳页:

- 复用新加的公共样式 `.list-detail`(`common/customer-list.css`),两页共用而不各写一份
- 详情取不到时**不整页报错**:列表本身是好的,只恢复按钮并记一次错误

## 2. 管理员:知识库管理(K002 / K003 / K004)

客服的**全部回答都来自已入库的知识**,而此前**没有任何页面能管理知识库** ——
只能靠命令行脚本 `tools/seed_knowledge_demo.py` 灌数据,管理员既看不到也改不了。

新增「知识库」标签页:文档列表 + 上传(.txt/.md/.docx)+ 失效。两处要点:

- **K003 的成功体是裸的** `{items, count}`、**没有 `data` 信封** ——
  `request()` 仍会去取 `payload.data`(那是 undefined),所以列表要两面都兜,
  否则永远显示"知识库为空"、而库里其实有数据;
- 上传是 **JSON + base64**,不是 multipart(一期契约如此,见 `knowledge_management.py`)。

## 3. 管理员:配置项与模型路由(A001 / A008–A010 / A018–A020)

此前只能对**已存在**的版本走"校验→审核→激活",**既不能新建版本、也不能往里加配置项**
—— 新建的版本永远是空的、校验必然失败;模型路由规则同样既看不到也改不了。

- 新增「新建配置版本」表单(版本号 / 标题 / 变更说明)
- 每个版本加「内容」按钮(**与状态无关**:草稿阶段就要能加,否则版本永远空)→
  展开该版本的**配置项**与**模型路由规则**,两者都支持新增与编辑
- 配置项的「值」按 JSON 输入并在前端校验:与其让后端 422,不如就地拦住并说清哪里不对
- `fallbacks` 暂不在界面编辑(提交空数组),需要时用接口补

这些端点**都已在 `docs/05` §19 有编号**,直接复用,无需新增编号。

## 4. 两个"死端点"查证后**保留**

初查发现 `RK013`(风控日报非流式,已被 RK014 流式取代)与 `ADVISOR_GOAL`
(投顾自己的目标;投顾是员工、没有目标 → 永远 404)注册了却无人调用,一度删除。
但 `tests/unit/api/test_portal_frontend.py` 立刻失败 —— 它把"页面会用到的端点"
固定成一张清单,**注册与调用是两件事**。已恢复注册,并就地注明它们当前无人调用、
但受契约保护。

顺带发现:**`RK013`–`RK015`(风控日报)也不在 §19**,与投顾 AD 段原先的情况相同,
属文档缺口(未在本提交内补)。

## 辅助改动

`apiClient` 增加 `del()` 与 `put()`:真正发出的方法一直由端点表里的 `method` 决定,
所以 `post('K004')` 也能发出 DELETE —— 但读代码的人会以为发的是 POST。
现在意图与行为一致。

验证:unit+contract **1397 passed**(含前端契约 37);integration **110 passed**;
ruff 通过;mypy 251 文件 0 错;e2e 冒烟 **40/40**;相关页面与静态资源全部 200。
2026-09-14 00:51:13 +08:00
..

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/dashboard/ 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 与过期判断迟早不一致。 公开产品数据不得与登录后的真实账户数据混用。

三点口径(改前端前先读):

  1. change_pct 可能是 null(行情只同步过一个交易日时算不出涨跌)。 调用方必须显示"暂无",不得当成 0 —— formatPercent 收到 null 会渲染成 +0.00%, 那等于告诉客户"今天平盘"。
  2. 历史净值曲线没有数据源:fin_nav_history 目前 0 行,详情页因此不画走势图, 并显式说明"尚未接入"。此前那条曲线是 mock 里 12 个编造点位 —— 走势图最容易被当成真数据。
  3. 产品级披露在 common/product-notes.js(如 510300 的"同指数参考产品,非本公司发行")。 fin_product 没有这个字段,所以它留在前端;新增需要披露的产品时改那一份。 凡渲染公开产品的页面都要挂 data-source-notice 说明数据来源。

客户页面均由 common/auth.js 执行入口守卫,接口路径只在 common/api-client.js 的端点表登记。