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) => ` - - - - - + box.innerHTML = `
预警号客户规则等级状态操作
${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)}
` + + 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 ` + + + + + `).join('') + + + + `; + }).join('') + '
预警号客户等级规则状态操作
${no}${esc(a.customer_name || a.customer_no || a.customer_id || '')}${esc(a.risk_level ?? a.level ?? '')}${esc(rules)}${esc(a.status ?? '')} - - - -
'; } @@ -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 ? `` + items.map((m) => ` @@ -926,6 +937,7 @@ function renderAdmin(box) {

审计流水

谁做了什么、有没有被拒,都记在这里(GET /api/v1/admin/audit-records)。

+ 1-100
加载中…
@@ -1003,7 +1015,11 @@ async function userRoles() { } async function loadAudit() { - const limit = $('audit-limit').value.trim() || '20'; + // 该接口 limit 的取值范围是 1-100;钳一下,免得填大值得到 422。 + let limit = parseInt($('audit-limit').value.trim() || '20', 10); + if (!Number.isFinite(limit)) limit = 20; + limit = Math.min(100, Math.max(1, limit)); + $('audit-limit').value = limit; const r = await GET('/api/v1/admin/audit-records', { limit }); const items = r.body?.data?.items || r.body?.data || []; const box = $('audit');
邮件 ID主题状态操作
${esc(m.mail_id ?? m.id)}