From 552055ee2dbe8233bbb2dba19870ef69b1e2a428 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=8D=BF=E4=BA=91=E7=A7=8B=E6=9C=88?= <15273589815@163.com>
Date: Sat, 12 Sep 2026 16:03:22 +0800
Subject: [PATCH] =?UTF-8?q?fix(portal):=20=E4=BF=AE=E9=A3=8E=E6=8E=A7?=
=?UTF-8?q?=E9=A2=84=E8=AD=A6=E5=88=97=E8=A1=A8=20422=EF=BC=88limit=20?=
=?UTF-8?q?=E4=B8=8A=E9=99=90=E6=98=AF=205=EF=BC=8C=E6=94=B9=E4=B8=BA?=
=?UTF-8?q?=E4=B8=8D=E4=BC=A0=EF=BC=89=EF=BC=9B=E8=BF=90=E8=90=A5=E9=82=AE?=
=?UTF-8?q?=E4=BB=B6=E6=94=B9=E7=94=A8=20page/page=5Fsize=EF=BC=9B?=
=?UTF-8?q?=E5=AE=A1=E8=AE=A1=20limit=20=E9=92=B3=E5=88=B6=E5=88=B0=201-10?=
=?UTF-8?q?0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- risk/alerts 传 limit=20 会 422 query.limit<=5,导致整张表格渲染不出来、
行内确认/升级/解决按钮随之消失 —— 这正是验收清单 3-3/3-6~3-9 看不到功能的原因
- 实测列表返回 2 条预警;处置接口有状态前置条件(409 = 当前状态不允许)
- 清单 3-3/3-6 更新为实测结果并写明 limit 上限与状态机约束
---
docs/40-前端验收清单.md | 12 ++++++----
tools/portal.py | 50 +++++++++++++++++++++++++++--------------
2 files changed, 41 insertions(+), 21 deletions(-)
diff --git a/docs/40-前端验收清单.md b/docs/40-前端验收清单.md
index 3b05289..9bf8c75 100644
--- a/docs/40-前端验收清单.md
+++ b/docs/40-前端验收清单.md
@@ -76,16 +76,20 @@ D:\conda\envs\jr_py313\python.exe tools\portal.py --base-url http://127.0.0.1:80
|---|---|---|
| 3-1 | 进入即自动加载"总览" | 数字卡片;数据范围 `all`(能看全部客户的预警) ✅实测 |
| 3-2 | 点「刷新总览」 | 同上,HTTP 200 ✅实测 |
-| 3-3 | 点「拉取预警列表」→ 看表格 | 预警号 / 客户 / 规则 / 等级 / 状态 / 操作;空库时提示"可先点触发一次扫描" ✅实测 |
+| 3-3 | 进入即自动加载"预警列表" | **表格出现**:预警号 / 客户 / 等级 / 规则 / 状态 / 操作。本机实测 2 条:`ALDEMO0002`(高,RW-015/RW-003)、`ALDEMO0001`(中,RW-007/RW-002/RW-012),均"待处理" ✅实测
⚠️ **门户刻意不传 `limit`**:该接口 `limit` 上限是 **5**,传 20 会得到 `422 query.limit: Input should be less than or equal to 5`,**整张表格渲染不出来**(行内按钮也随之消失) |
| 3-4 | 点「触发一次扫描」 | **HTTP 200,`body.code=0`**(这条以前会因缺幂等头报 422,已修) ✅实测 |
| 3-5 | 点「生成日报」 | HTTP 200,返回日报内容 ⚠️按契约 |
-| 3-6 | 对某条预警点「确认」 | 二次确认后 HTTP 200,状态变为已确认;**审计里会留痕** ⚠️按契约(写操作) |
-| 3-7 | 点「升级」/「解决」 | 同上,各自状态流转 ⚠️按契约 |
-| 3-8 | 点某个默认不存在的预警号 | 404,提示资源不存在 —— 属正常 fail closed ⚠️按契约 |
+| 3-6 | 对某条预警点「确认」 | 二次确认后返回业务结果。✅实测:`POST .../acknowledgements` → **409「只有待处理的预警才能确认解决」** —— 该动作**有状态前置条件**,不是任意状态都能点,属正常业务约束(页面会原样显示原因) |
+| 3-7 | 点「升级」/「解决」 | 同上,各自有状态约束;状态不符时返回 409 ⚠️按契约 |
+| 3-8 | 点一个**不存在**的预警号 | 404 资源不存在 —— 正常 fail closed ⚠️按契约 |
| 3-9 | 用**客户**账号访问风控接口 | **403 缺少操作权限**(`risk_t` 能看,`cust_t` 不能) ✅实测(客户访问 `/admin/roles` 为 403) |
> ⚠️ **注意**:`risk_t` 有 `audit:read`,所以它访问 `/api/v1/admin/roles` 是 **200 而不是 403** ——
> 这是种子设计如此,不是越权漏洞。
+>
+> ⚠️ 行内三条按钮对应 `acknowledgements` / `escalations` / `resolutions` 三个端点,都是**写操作**:
+> 会在库里留数据与审计,且**受状态机约束**。列表拿不到数据时这三个按钮不会出现 ——
+> 先确认 3-3 是否正常。
---
diff --git a/tools/portal.py b/tools/portal.py
index 3006b54..0d0e689 100644
--- a/tools/portal.py
+++ b/tools/portal.py
@@ -744,25 +744,34 @@ async function loadRisk() {
}
async function loadAlerts() {
- const r = await GET('/api/v1/risk/alerts', { limit: 20 });
- const items = r.body?.data?.items || r.body?.data || [];
+ // 刻意**不传 limit**:该接口的 limit 上限是 5(传 20 会 422
+ // `query.limit: Input should be less than or equal to 5`),
+ // 不传则用平台默认值,最稳。
+ const r = await GET('/api/v1/risk/alerts');
+ const items = Array.isArray(r.body?.data) ? r.body.data
+ : (r.body?.data?.items || []);
const box = $('alerts');
if (!Array.isArray(items) || !items.length) {
- box.innerHTML = `
没有预警数据(HTTP ${r.status})。可先点「触发一次扫描」。
`;
+ box.innerHTML = `没有预警数据(HTTP ${r.status}${r.body?.code ? ' code=' + r.body.code : ''})。可先点「触发一次扫描」。
+ ${esc(pretty(r.body))}`;
return;
}
- box.innerHTML = `| 预警号 | 客户 | 规则 | 等级 | 状态 | 操作 |
`
- + items.map((a) => `
- ${esc(a.alert_no ?? a.alert_id)} |
- ${esc(a.customer_id)} |
- ${esc(a.rule_code ?? a.rule ?? '')} |
- ${esc(a.level ?? a.severity ?? '')} |
- ${esc(a.status)} |
+ box.innerHTML = `| 预警号 | 客户 | 等级 | 规则 | 状态 | 操作 |
`
+ + items.map((a) => {
+ const no = esc(a.alert_no ?? a.alert_id ?? '');
+ const rules = Array.isArray(a.rule_codes) ? a.rule_codes.join(', ') : (a.rule_code ?? '');
+ return `
+ ${no} |
+ ${esc(a.customer_name || a.customer_no || a.customer_id || '')} |
+ ${esc(a.risk_level ?? a.level ?? '')} |
+ ${esc(rules)} |
+ ${esc(a.status ?? '')} |
-
-
-
- |
`).join('')
+
+
+
+ `;
+ }).join('')
+ '
';
}
@@ -845,8 +854,10 @@ async function loadMailbox() {
}
async function loadMails() {
- const r = await GET('/api/v1/offsite-fund/mails', { limit: 20 });
- const items = r.body?.data?.items || r.body?.data || [];
+ // 该接口的分页参数是 `page` / `page_size`(不是 limit)。
+ const r = await GET('/api/v1/offsite-fund/mails', { page: 1, page_size: 20 });
+ const raw = r.body?.data;
+ const items = Array.isArray(raw) ? raw : (raw?.items || []);
$('mails').innerHTML = Array.isArray(items) && items.length
? `