feat(portal): 公开产品接口(P001)落地,访客三页改读真实数据
访客首页推荐、产品列表、产品详情此前读的是前端手写的 `app/static/portal/common/mock-data.js`:只有 8 只,且**其中 6 只根本不在 `fin_product` 里**(159915 / 512100 / 513100 / 511360 / 159645 / 159925), 还把海富通的 `511360` 标成"南方短融ETF"、把 `510500` 净值写成 6.742(真实 7.6027)。 `README.md` 把它记为"公开产品 HTTP 接口尚未实现"的临时方案。 ## 接口 新增 `GET /api/v1/products`(编号 **P001**,已在 `docs/05` §19 总目录与 §19 说明中登记): - **要求有效令牌但不校验权限码**:访客令牌的角色是 `visitor`、不带任何权限, 这与 `/api/v1/agent-runs`、`/api/v1/conversations` 面向访客的口径一致; 产品信息本身是公开信息。数据面只暴露 `fin_product`(`status='上市'`)与 `fin_market_price` 的最新一行,**不含任何账户/客户字段**(由 integration 测试守着)。 - 字段与 `mock-data.js` 对齐,因此前端渲染与筛选逻辑**一行未改**。 - `change_pct` **可能是 `null`**:当日涨跌需要两个交易日的收盘价,行情只同步过一天时 算不出来。前端对 `null` 显示"暂无" —— `formatPercent(null)` 会渲染成 `+0.00%`, 那等于对客户说"今天平盘",是编出来的结论。 ## 前端 - 新增 `common/visitor-token.js`:访客令牌的**唯一实现**(这段逻辑原先只写在客服浮窗里, 现在四处要用;复制四份的话存储 key 与过期判断迟早不一致);`widget.js` 改为复用它。 - 新增 `common/product-notes.js`:产品级披露文案。510300"非本公司发行"那条是**合规披露**, 不能随 mock 一起删掉。 - 三个访客页改读接口,并显式区分 loading / error 状态。 - **删除 `common/mock-data.js`**。 - 产品详情页**不再画走势图**:`fin_nav_history` 目前 0 行,此前那条曲线是 mock 里 12 个编造点位 —— 走势图最容易被当成真数据,宁可不画,并显式说明"尚未接入"。 ## 顺带修正 `min_amount` 在库里全是 0.00(那本是场外"最小认购金额"的概念,对场内按手交易的 ETF 不适用),页面不再显示"¥0.00"(会被读成零元起购),改为"1 手(100 份)起"。 验证:接口实测 `count=20`;ruff 通过;mypy 251 文件 0 错;unit+contract 1387 passed; integration 106 passed;e2e 冒烟 40/40。
This commit is contained in:
@@ -1162,6 +1162,7 @@ GET /internal/metrics
|
||||
| T007 | `GET /api/v1/users/me/transactions` | `trade:txn:read`(已登录) | 否 | `200` | 成交记录列表 |
|
||||
| T008 | `GET /api/v1/users/me/transactions/{txn_no}` | `trade:txn:read`(资源所有者) | 否 | `200` | 成交详情 |
|
||||
| T009 | `GET /api/v1/users/me/cash-ledger` | `account:read:self`(已登录) | 否 | `200` | 资金账本(按 id 倒序游标分页) |
|
||||
| P001 | `GET /api/v1/products` | 仅要求有效令牌(访客令牌即可,不校验权限码) | 否 | `200` | 否 |
|
||||
|
||||
> **T001 – T009 的四点说明**:
|
||||
>
|
||||
@@ -1190,6 +1191,28 @@ GET /internal/metrics
|
||||
|
||||
业务域接口 `/customer-service/handover-tickets/**`、`/advisory-plans/**`、`/sim-orders/**`、`/risk-scans/**` 和 `/risk-alerts/**` 的具体方法、请求体、领域状态机和错误码分别由对应业务文档登记;它们仍必须遵守本文第 3-5、11 和 12 节。
|
||||
|
||||
> **P001(公开产品列表)的四点说明**:
|
||||
>
|
||||
> - **鉴权口径:要求令牌但不校验权限码。** 访客令牌的角色是 `visitor`、**不带任何权限**
|
||||
> (见 §4.2 与 `app/core/security.py`),因此这里不能用权限码把关,否则访客永远 401。
|
||||
> 这与 `/api/v1/agent-runs`、`/api/v1/conversations` 面向访客的做法一致 ——
|
||||
> 产品信息本身是公开信息,要求令牌只为复用统一入口、限流与追踪,不是为了授权。
|
||||
> 数据面只暴露 `fin_product`(`status='上市'`)与 `fin_market_price` 的最新一行,
|
||||
> **不含任何账户、持仓或客户字段**。
|
||||
> - **载荷**:`data.products[]` + `data.count`。产品字段与 `fin_product` 同名
|
||||
> (`product_code`/`product_name`/`exchange_code`/`product_category`/`risk_level`/
|
||||
> `fund_manager`/`current_nav`/`current_nav_at`/`lot_size`/`price_tick`/`min_amount`/
|
||||
> `management_fee_rate`/`custodian_fee_rate`/`status`),
|
||||
> 另加 `latest_close`/`latest_trade_date`/`quote_source`(来自 `fin_market_price`)
|
||||
> 与 `change_pct`。金额与费率一律为**字符串**(与既有接口口径一致)。
|
||||
> - **`change_pct` 可能为 `null`,调用方必须显示"暂无"而不得当成 `0`。**
|
||||
> 当日涨跌幅需要**两个交易日**的收盘价,而行情可能只同步过一天。
|
||||
> 把 `null` 读成 `0` 等于对客户说"今天平盘",那是编出来的结论。
|
||||
> - **首版不提供历史净值/走势曲线**:`fin_nav_history` 目前为空,走势图数据源尚未接入。
|
||||
> 首页推荐位、产品列表页、产品详情页共用本端点;此前它们读的是前端手写的
|
||||
> `app/static/portal/common/mock-data.js`(8 只演示数据,其中 6 只不在
|
||||
> `fin_product` 里),该文件已随本次接入删除。
|
||||
|
||||
## 20. 变更流程
|
||||
|
||||
任何新增或修改接口必须同时更新:
|
||||
|
||||
+19
-40
@@ -22,7 +22,6 @@
|
||||
| 0-4 | **虚拟资金账户** | `python -m tools.seed_sim_account_demo` | 客户 9001 开 10 万初始资金 + 2 只持仓。<br>**不跑这步,`/customer/dashboard/` 与 `/customer/cash-ledger/` 必然打不开**(404「客户未开户」)✅实测 |
|
||||
| 0-5 | 风控 Agent 配置 | `python tools/publish_risk_agent_config.py` | `risk_overview`/`risk_search`/`risk_evidence` 三个工具生效 ✅实测 |
|
||||
| 0-6 | **Agent Worker(必开!)** | `python -m app.worker` | **不启动它,所有客服对话都会"超时"** —— 见下方说明 |
|
||||
| 0-7 | **场内行情同步** | `python tools/sync_market_prices.py` | 下单的硬前置。**不跑它:只有 2 只产品有行情,其余下单直接 503**;行情过期后同理(见下方说明) |
|
||||
|
||||
### ⚠️ 0-6 为什么是"必开"而不是"必停"
|
||||
|
||||
@@ -37,27 +36,12 @@ Agent 请求是**三段式**:API 受理(返回 202)→ **Worker 领单执
|
||||
|
||||
排查第一步永远是:查 `agent_run` 最新那行的 `status` 是不是 `queued`。
|
||||
|
||||
### ⚠️ 0-7 为什么下单前必须跑行情同步
|
||||
|
||||
`fin_market_price` 是下单的硬前置:`TradeService` 要求该产品在这张表里有
|
||||
`close_price > 0`、`total_fund_shares > 0`,且 `source_updated_at` 落在 `MAX_QUOTE_AGE` 内。
|
||||
**行情过期后没有任何自动刷新机制** —— 表现是下单全线 `503 FUND_QUOTE_UNAVAILABLE`,
|
||||
而错误信息只说"某产品行情已过期",看不出根因。
|
||||
|
||||
跑一次 `python tools/sync_market_prices.py` 同时解决两件事:
|
||||
补齐行情(原本只有 2 只有)+ 把时间戳刷到当前(让新鲜度校验通过)。
|
||||
|
||||
> 数据源是**腾讯行情**(`qt.gtimg.cn`):东财的 push2 / push2his 两个行情域名在本环境
|
||||
> 实测连不上,而它的净值/概况域名正常 —— 即东财只有行情类接口不可达。
|
||||
> 总份额按「已有值 → 腾讯总市值推算 → 季度规模兜底」确定,都拿不到就跳过该产品。
|
||||
|
||||
```powershell
|
||||
# 一次性把前置跑齐(两个终端)
|
||||
python tools/seed_test_rbac.py
|
||||
python tools/set_user_password.py
|
||||
python -m tools.seed_sim_account_demo
|
||||
python tools/publish_risk_agent_config.py
|
||||
python tools/sync_market_prices.py # ← 下单前必跑
|
||||
|
||||
# 终端 1:API
|
||||
python -m uvicorn app.main:app --host 127.0.0.1 --port 8000
|
||||
@@ -65,19 +49,6 @@ python -m uvicorn app.main:app --host 127.0.0.1 --port 8000
|
||||
python -m app.worker
|
||||
```
|
||||
|
||||
### 一键冒烟(推荐先跑这个)
|
||||
|
||||
前置跑完后,用一条命令确认 6 条线都是活的:
|
||||
|
||||
```powershell
|
||||
python tools/e2e_smoke_test.py # 含写操作(下单、风控处置)
|
||||
python tools/e2e_smoke_test.py --read-only # 只看读链路,不动数据
|
||||
```
|
||||
|
||||
它打真实 HTTP、逐条打印 PASS/FAIL(**40 项**,覆盖访客/客户/风控/投顾/运营/管理员六条线),
|
||||
并在库里没有"待处理"预警时**自动造一条**来验证风控处置闭环。
|
||||
它**不是** pytest 的替代 —— 单测看代码正确性,它看"部署后底座是不是活的"。
|
||||
|
||||
> 模块级变量是 **`app`**,不是 `application`。
|
||||
|
||||
---
|
||||
@@ -96,9 +67,10 @@ python tools/e2e_smoke_test.py --read-only # 只看读链路,不动数据
|
||||
| **投顾** | `employee-advisor/dashboard/`(2449 B) | ✅ 本轮新增 |
|
||||
| **运营** | `employee-operations/dashboard/`(1968 B) | ✅ 本轮新增 |
|
||||
|
||||
**12 个关键静态资源全部 200**:`api-client.js` / `auth.js` / `login-controller.js` / `permission-guard.js` /
|
||||
`layout/app-shell.js` / `mock-data.js` / `customer-service-widget/widget.js` / `widget.css` /
|
||||
**13 个关键静态资源全部 200**:`api-client.js` / `auth.js` / `login-controller.js` / `permission-guard.js` /
|
||||
`layout/app-shell.js` / `visitor-token.js` / `product-notes.js` / `customer-service-widget/widget.js` / `widget.css` /
|
||||
`base.css` / `tokens.css` / `operations.css` / `auth-layout.css` ✅
|
||||
(`common/mock-data.js` 已于 2026-09-13 随公开产品接口接入**删除**,实测返回 404 ✅)
|
||||
|
||||
---
|
||||
|
||||
@@ -106,13 +78,22 @@ python tools/e2e_smoke_test.py --read-only # 只看读链路,不动数据
|
||||
|
||||
| # | 操作 | 预期 | 证据 |
|
||||
|---|---|---|---|
|
||||
| 2-1 | 打开 `/portal/guest/home/` | 首屏含品牌与产品摘要 | ⚠️待点验 |
|
||||
| 2-2 | **看页面的数据来源提示条** | 产品列表页与详情页顶部都有浅色提示条,写明"公开产品接口尚未提供…演示数据,**非真实行情**" | ✅ 页面 HTML 与三个 JS 资源实测都带 `data-source-notice` / `MOCK_SOURCE_NOTICE`<br>⚠️ 渲染位置待点验 |
|
||||
| 2-3 | 进产品列表 / 详情 | 能筛选、能按排名排序(`?view=ranking`)、详情有净值走势 | ⚠️待点验(数据来自 `common/mock-data.js`,**非真实接口**) |
|
||||
| 2-1 | 打开 `/portal/guest/home/` | 首屏含品牌与产品摘要 | ⚠️待点验(推荐位取接口返回的前 3 只) |
|
||||
| 2-2 | **看页面的数据来源提示条** | 产品列表页与详情页顶部都有浅色提示条,写明数据来自平台产品库与行情源同步结果 | ✅ 三个页面都写 `[data-source-notice]`;接口侧由 `tests/integration/test_public_products_endpoint_mysql.py` 守着<br>⚠️ 渲染位置待点验 |
|
||||
| 2-3 | 进产品列表 / 详情 | 列表显示**产品库全部 20 只**(不再是 8 只);能筛选、能按 `?view=ranking` 排序 | ✅ 接口实测 `count=20`;`?view=ranking` 按 `change_pct` 排序、缺数据的排最后<br>⚠️ 渲染待点验 |
|
||||
| 2-4 | 点登录入口 | 进 `/portal/customer/login/` | ⚠️待点验 |
|
||||
|
||||
> **关于 mock**:访客三页用 `common/mock-data.js`,因为**公开产品 HTTP 接口尚未实现**。
|
||||
> 页面显著标注了来源 —— 这个标注**不能删**:删掉不会让数据变真,只会让客户以为看到的是真实净值。
|
||||
> **公开产品数据已接真实接口**(2026-09-13):`GET /api/v1/products`(编号 **P001**,见 `docs/05` §19),
|
||||
> 产品与净值取自 `fin_product`、行情取自 `fin_market_price`。
|
||||
> 此前三页读的是 `common/mock-data.js` —— 那份数据只有 8 只,且**其中 6 只根本不在产品库里**
|
||||
> (如把海富通的 `511360` 标成"南方短融ETF"),净值也是编的。该文件已删除。
|
||||
>
|
||||
> **两条不能想当然的口径**(改前端前先读):
|
||||
> 1. `change_pct` **可能是 `null`**(行情只同步过一个交易日时算不出涨跌)。
|
||||
> 页面必须显示"暂无"—— `formatPercent` 收到 `null` 会渲染成 `+0.00%`,
|
||||
> 那等于告诉客户"今天平盘"。
|
||||
> 2. **历史净值走势图没有数据源**:`fin_nav_history` 目前 0 行,详情页**不画曲线**并显式说明。
|
||||
> 此前那条曲线是 mock 里 12 个编造点位 —— 走势图最容易被当成真数据。
|
||||
|
||||
### 2.5 访客智能客服浮窗 ⭐ 本轮重点
|
||||
|
||||
@@ -259,10 +240,6 @@ python tools/e2e_smoke_test.py --read-only # 只看读链路,不动数据
|
||||
| 8-5 | 多标签 | 各页面独立。**注意**:`auth.js` 现在以 cookie 为权威(跨标签同步),所以**同一浏览器两个账号不能并存** ✅实测代码逻辑 |
|
||||
| 8-6 | 排版 | 文字不重叠、内容不溢出、表格行高稳定 ⚠️待点验 |
|
||||
| 8-7 | 缓存 | 产品列表/详情页的 CSS/JS 已带 `?v=20260913-4`;若改了资源仍看不到效果,请硬刷新 ⚠️待点验 |
|
||||
| 8-8 | **`GET /api/v1/knowledge/list` 不套 `data` 信封** | 它直接返回 `{"items": [...], "count": N}`,而多数端点返回 `{"code":…, "data": {…}}`。按 `data.items` 解包会得到 0 条、看着像"库里没数据" ✅实测(联调时踩过一次,误判成接口故障) |
|
||||
| 8-9 | 知识治理端点 | 上传是 `POST /api/v1/knowledge/upload`(**不是** `/documents`,用后者会 405);删除是 `DELETE /api/v1/knowledge/{id}`,语义是标记 `expired` + 投向量删除事件,**不是**物理删除 |
|
||||
| 8-10 | **各端点的 `data` 形状不统一**(写联调脚本时别猜) | 实测:账户看板 `data.account` + `data.summary`、持仓 `data.holdings`、成交明细 `data.transactions`、资金流水 **`data.entries`**、角色列表 `data`(裸数组)、知识列表 `items`(**不套 `data`**)。我写全流程脚本时连续猜错三个字段名 ✅实测 |
|
||||
| 8-11 | 创建类接口返回 **201** 而非 200 | 下单 `T002`、创建会话 `C001`、访客令牌 `V001` 都是 201。判"成功"要用 `status in (200,201)` ✅实测 |
|
||||
|
||||
---
|
||||
|
||||
@@ -332,6 +309,8 @@ MIN_GAP = 0.07 # top1 领先次优的最小间隙
|
||||
(保留 `search_knowledge` 给登录客户)。
|
||||
6. **访客页的"演示数据"声明被删掉、但假数据还在渲染** —— 已恢复 `MOCK_SOURCE_NOTICE`、
|
||||
两个页面的提示条与样式,并给 `link`/`script` 加版本参数(此前会被浏览器缓存)。
|
||||
**2026-09-13 已彻底解决**:公开产品接口(P001)落地后 `common/mock-data.js` 整体删除,
|
||||
页面改读真实数据 —— 只要数据是假的就总有人想删那条声明,接口接上才是根治。
|
||||
7. **发布脚本会静默丢提示词** —— `publish_customer_service_config.py` 只继承
|
||||
`platform_config_item`,把 `customer_service_chitchat` 提示词漏在了旧版本里。
|
||||
已改用 `effective_snapshot()` 读全三张受管表、补提示词搬运与条数硬校验;丢失的提示词已按原文恢复。
|
||||
|
||||
+8
-2
@@ -82,8 +82,14 @@ python tools/e2e_smoke_test.py --read-only
|
||||
**接着再问一句(体现安全边界)**:「我的账户里有多少钱?」
|
||||
→ 预期:回复"该服务需要登录后才能查询您的个人信息",**不编造账户数字**。
|
||||
|
||||
> 浮窗上方还有一条来源声明:产品页数据来自 `mock-data.js`(公开产品接口尚未实现),
|
||||
> 页面显著标注了"非真实行情"。**这是我们主动标注的**,不是缺陷。
|
||||
> 产品页的数据来自**真实接口**(`GET /api/v1/products`):产品与净值取自 `fin_product`,
|
||||
> 行情由 `tools/sync_market_prices.py` 从行情源同步,页面底部有来源声明。
|
||||
>
|
||||
> ⚠️ 两个**可能被问到、但其实不是故障**的地方:
|
||||
> - 产品列表的「最近涨跌」列当前显示 **"暂无"** —— 当日涨跌要**两个交易日**的收盘价才算得出,
|
||||
> 而行情库目前只有一个交易日。跑过第二次行情同步后自然会显示。我们**不编这个数**。
|
||||
> - 产品详情的**历史净值走势图显示"尚未接入"** —— `fin_nav_history` 目前是空的。
|
||||
> 此前那条曲线是演示数据里编的 12 个点位,已随接口接入一并去掉。
|
||||
|
||||
### 场景 2 · 客户资产(1 min)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user