给 trigger_rule_codes 加 JSON 多值索引;登记 P3 处理结果(docs/25 P3 #21)
实测确认 #21 描述准确:fin_risk_alert 只有 11 个普通 BTREE 索引 + 主键 + alert_no 唯一键,规则命中的 JSON_CONTAINS 查询 EXPLAIN 为 type=ALL、possible_keys=NULL, 即全表扫。MySQL 8.0.27 支持多值索引,故新增迁移 20260911_risk_rule_index: ADD INDEX idx_fin_risk_alert_trigger_rule_codes ((CAST(`trigger_rule_codes` AS CHAR(16) ARRAY))) 迁移幂等(先查 information_schema.STATISTICS),upgrade/downgrade 往返已验证。 生效后 EXPLAIN 变为 access_type=range 且 key 命中该索引,原始证据留档在 docs/evidence/risk-index-probe.json(由 tools/probe_risk_index.py 生成,只读探查)。 只解决一半,另一半如实记为限制:若干 like(f"%{keyword}%") 全表扫无法用 B-tree 索引, 根治需全文索引 + 中文分词组件(部署依赖),本轮不做。 revision 名刻意压到 32 字符以内 —— alembic_version.version_num 是 VARCHAR(32), 超长会在写版本号时报 1406,而 DDL 是非事务的,那时索引已经建好了。 docs/25 追加"P3 处理结果"表,逐条登记 17-25 的状态:#19 是协议级重做(keyset 分页) 不单方面改,#21 部分修复,#25 前半段不成立,其余已修。
This commit is contained in:
@@ -240,6 +240,20 @@ Asia/Shanghai 展示。北京 08:00 前生成时,统计窗口是"前一日 08:
|
||||
| 24 | SSE 未校验 `Accept`(`SseNotAcceptableError`/406 已定义但未被使用) | `api/controllers/risk.py:182-204` |
|
||||
| 25 | 接口未登记 `docs/05`:§19 目录里一条风控接口都没有,而 §20 明确要求"新增接口必须同步更新 §19";且 §12 约定的前缀是 `/risk-scans/**`、`/risk-alerts/**`,实现是 `/api/v1/risk` | `docs/05-接口文档.md` §12/§19/§20 |
|
||||
|
||||
**P3 处理结果(2026-09-11)**
|
||||
|
||||
| # | 状态 | 处理 |
|
||||
|---|---|---|
|
||||
| 17 | ✅ 已修 | 列表信封改为 `{data: [...], meta: {trace_id, next_cursor, has_more}}`,对齐 §3.3;五个列表端点统一走 `_list_envelope` |
|
||||
| 18 | ✅ 已修 | 游标内嵌 SHA-256 指纹(`user_id` + `data_scope`/`customer_ids` + 查询条件;`/evidence/{source}` 的 `source` 一并绑定,否则 customers 的游标能直接翻 products)。指纹不符一律 `400 INVALID_CURSOR`。**刻意排除 `limit`**:它是分页参数不是查询条件 |
|
||||
| 19 | ⚠️ 已知限制 | offset 游标无法根治跳行/重复,要根治得改 keyset 分页(游标携带"上一页最后一条的排序键")。这会**改动分页协议本身**,且当前排序首列是 `case(alert_level=HIGH…)` 这种计算列,需要连排序一起去掉 —— 属于协议级重做,不在本轮单方面改 |
|
||||
| 20 | ✅ 已修 | 详情证据每类封顶 200 条、日报每组封顶 5000 条,用 `limit + 1` 判定截断,并在响应里暴露 `evidence_truncated` / `data_truncated`。**不静默截断**:日报计数直接来自行数,静默截断等于给出一份看起来正常、实际少统计的日报 |
|
||||
| 21 | 🟡 部分修复 | `trigger_rule_codes` 已加 JSON 多值索引(迁移 `20260911_risk_rule_index`);`EXPLAIN` 由 `type=ALL`、`possible_keys=NULL` 变为 `type=range` 并命中 `idx_fin_risk_alert_trigger_rule_codes`,实测证据留档在 `docs/evidence/risk-index-probe.json`。**`like(f"%{keyword}%")` 依然全表扫**:前后通配符在 B-tree 上无解,根治需全文索引 + 中文分词组件(部署依赖),本轮不做,如实记为限制 |
|
||||
| 22 | ✅ 已修 | 6 个写接口接入平台 `api_request_receipt` 幂等。新增 `ApiTransactionService.execute_in`:原 `execute` 自开 `SessionFactory()` 与 `session.begin()`,而 `RiskActionService._finish` 内部会 commit,套进去就是"内层提交外层事务",故改为在调用方事务内读写幂等记录 |
|
||||
| 23 | ✅ 已修 | **不把 413 降成 422**:413 是上传超限的标准语义,前端文档也已按 413 做提示映射,改为在 `docs/05` §3.5 状态码表**补登** 413,契约以"补齐"而非"改动"方式对齐 |
|
||||
| 24 | ✅ 已修 | SSE 端点补 `Accept` 协商(抽到 `app/api/dependencies/negotiation.py` 与 `/agent-runs/{run_id}/events` 共用)。顺带发现一个更隐蔽的问题:鉴权原本在 async generator 内部,403 只能在响应头发出**之后**抛出,表现为"200 + 半截流",现改为构造 `StreamingResponse` 前完成 |
|
||||
| 25 | 🟡 部分成立 | "§19 一条风控接口都没有"**不成立**:§19 末尾写明业务域接口由各自业务文档登记,15 条端点已在 `docs/风控业务演示文档/06-模块接口与字段映射.md` 逐条登记。真问题是 §12 写的 `/risk-scans/**`、`/risk-alerts/**` 与实际实现 `/api/v1/risk/**` 不符,已按实现更新 §12 并加说明 |
|
||||
|
||||
---
|
||||
|
||||
## 六、做得好、建议保持 ✅
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"mysql_version": "8.0.27",
|
||||
"column": [
|
||||
{
|
||||
"COLUMN_NAME": "trigger_rule_codes",
|
||||
"COLUMN_TYPE": "json",
|
||||
"DATA_TYPE": "json",
|
||||
"IS_NULLABLE": "NO"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"INDEX_NAME": "idx_fin_risk_alert_alert_level",
|
||||
"COLUMN_NAME": "alert_level",
|
||||
"INDEX_TYPE": "BTREE",
|
||||
"EXPRESSION": null
|
||||
},
|
||||
{
|
||||
"INDEX_NAME": "idx_fin_risk_alert_alert_type",
|
||||
"COLUMN_NAME": "alert_type",
|
||||
"INDEX_TYPE": "BTREE",
|
||||
"EXPRESSION": null
|
||||
},
|
||||
{
|
||||
"INDEX_NAME": "idx_fin_risk_alert_created_at",
|
||||
"COLUMN_NAME": "created_at",
|
||||
"INDEX_TYPE": "BTREE",
|
||||
"EXPRESSION": null
|
||||
},
|
||||
{
|
||||
"INDEX_NAME": "idx_fin_risk_alert_customer_id",
|
||||
"COLUMN_NAME": "customer_id",
|
||||
"INDEX_TYPE": "BTREE",
|
||||
"EXPRESSION": null
|
||||
},
|
||||
{
|
||||
"INDEX_NAME": "idx_fin_risk_alert_due_at",
|
||||
"COLUMN_NAME": "due_at",
|
||||
"INDEX_TYPE": "BTREE",
|
||||
"EXPRESSION": null
|
||||
},
|
||||
{
|
||||
"INDEX_NAME": "idx_fin_risk_alert_handler_id",
|
||||
"COLUMN_NAME": "handler_id",
|
||||
"INDEX_TYPE": "BTREE",
|
||||
"EXPRESSION": null
|
||||
},
|
||||
{
|
||||
"INDEX_NAME": "idx_fin_risk_alert_primary_risk_work_order_id",
|
||||
"COLUMN_NAME": "primary_risk_work_order_id",
|
||||
"INDEX_TYPE": "BTREE",
|
||||
"EXPRESSION": null
|
||||
},
|
||||
{
|
||||
"INDEX_NAME": "idx_fin_risk_alert_priority_score",
|
||||
"COLUMN_NAME": "priority_score",
|
||||
"INDEX_TYPE": "BTREE",
|
||||
"EXPRESSION": null
|
||||
},
|
||||
{
|
||||
"INDEX_NAME": "idx_fin_risk_alert_related_order_id",
|
||||
"COLUMN_NAME": "related_order_id",
|
||||
"INDEX_TYPE": "BTREE",
|
||||
"EXPRESSION": null
|
||||
},
|
||||
{
|
||||
"INDEX_NAME": "idx_fin_risk_alert_related_transaction_id",
|
||||
"COLUMN_NAME": "related_transaction_id",
|
||||
"INDEX_TYPE": "BTREE",
|
||||
"EXPRESSION": null
|
||||
},
|
||||
{
|
||||
"INDEX_NAME": "idx_fin_risk_alert_related_work_order_id",
|
||||
"COLUMN_NAME": "related_work_order_id",
|
||||
"INDEX_TYPE": "BTREE",
|
||||
"EXPRESSION": null
|
||||
},
|
||||
{
|
||||
"INDEX_NAME": "idx_fin_risk_alert_status",
|
||||
"COLUMN_NAME": "status",
|
||||
"INDEX_TYPE": "BTREE",
|
||||
"EXPRESSION": null
|
||||
},
|
||||
{
|
||||
"INDEX_NAME": "idx_fin_risk_alert_trigger_rule_codes",
|
||||
"COLUMN_NAME": null,
|
||||
"INDEX_TYPE": "BTREE",
|
||||
"EXPRESSION": "cast(`trigger_rule_codes` as char(16) array)"
|
||||
},
|
||||
{
|
||||
"INDEX_NAME": "PRIMARY",
|
||||
"COLUMN_NAME": "id",
|
||||
"INDEX_TYPE": "BTREE",
|
||||
"EXPRESSION": null
|
||||
},
|
||||
{
|
||||
"INDEX_NAME": "uk_fin_risk_alert_alert_no",
|
||||
"COLUMN_NAME": "alert_no",
|
||||
"INDEX_TYPE": "BTREE",
|
||||
"EXPRESSION": null
|
||||
}
|
||||
],
|
||||
"row_count": 3,
|
||||
"non_array_rows": 0,
|
||||
"distinct_rule_codes": [
|
||||
{
|
||||
"raw_codes": "[\"RW-007\", \"RW-002\", \"RW-012\"]",
|
||||
"rows_count": 1
|
||||
},
|
||||
{
|
||||
"raw_codes": "[\"RW-015\", \"RW-003\"]",
|
||||
"rows_count": 1
|
||||
},
|
||||
{
|
||||
"raw_codes": "[\"RW-018\"]",
|
||||
"rows_count": 1
|
||||
}
|
||||
],
|
||||
"explain_json_contains": {
|
||||
"query_block": {
|
||||
"select_id": 1,
|
||||
"cost_info": {
|
||||
"query_cost": "0.71"
|
||||
},
|
||||
"table": {
|
||||
"table_name": "fin_risk_alert",
|
||||
"access_type": "range",
|
||||
"possible_keys": [
|
||||
"idx_fin_risk_alert_trigger_rule_codes"
|
||||
],
|
||||
"key": "idx_fin_risk_alert_trigger_rule_codes",
|
||||
"used_key_parts": [
|
||||
"cast(`trigger_rule_codes` as char(16) array)"
|
||||
],
|
||||
"key_length": "67",
|
||||
"rows_examined_per_scan": 1,
|
||||
"rows_produced_per_join": 1,
|
||||
"filtered": "100.00",
|
||||
"cost_info": {
|
||||
"read_cost": "0.61",
|
||||
"eval_cost": "0.10",
|
||||
"prefix_cost": "0.71",
|
||||
"data_read_per_join": "936"
|
||||
},
|
||||
"used_columns": [
|
||||
"id",
|
||||
"trigger_rule_codes",
|
||||
"cast(`trigger_rule_codes` as char(16) array)"
|
||||
],
|
||||
"attached_condition": "json_contains(cast(`trigger_rule_codes` as char(16) array),json'[\"RW-018\"]')"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user