# AI 数据清洗 E2E 验收(2026-10-22) ## 1. 目标 验证本轮改造在真实案件、真实数据源上的表现: - 行级失败可追溯:每一行为什么没进去、因为哪一列、原值。 - 必填列坏值整行拒绝;非必填坏值置空、行照常入库。 - 成功 / 失败 / 警告 / 未处理条数口径正确。 - 日期、时间、金额、数字、电话、身份证、卡号的类型化与入库。 - CSV 不再被探测阶段直接 Skip;模板复用不会因列序变化写错列。 - AI 动态表在结果页/数据页可查,类型正确。 ## 2. 环境 | 项 | 值 | |---|---| | 后端 | `ai-server`,`http://127.0.0.1:8980/js/a` | | 前端 | `http://localhost:3100` | | 案件 | 新建 **AI清洗验收-20261022**(案件 id 10) | | 登录 | 开发环境默认 `admin` | | 迁移 | 已执行 `sql/ai_clean_row_stats.sql`(否则 `ai_clean_batch.loaded_rows` 等列不存在,智能清洗 500) | > **自动化环境限制**:ZCode 内置浏览器不支持系统文件选择框。样本通过页面同源的 > `POST /dm/preFileUpload`(携带页面登录 token)注入;随后 **智能清洗、导入文件列表、 > 数据页、失败明细弹窗全部在界面里操作/核对**。在普通浏览器里可直接用「选择文件」。 ## 3. 测试样本 样本在 [fixtures](fixtures),可用 [generate_fixtures.py](generate_fixtures.py) 重新生成。 | 文件 | 内容 | 目的 | |---|---|---| | `01_self_built_types.xlsx` | 标题行 + 空行 + 9 列表头 + 7 行数据(含各类脏值、空行隔断) | 自建表类型推断、日期/金额/数字/证件转换、row_no 缺口 | | `02_reordered_headers.xlsx` | 与 01 同表头但倒序 | 模板复用必须同序(单测覆盖,UI 未跑) | | `03_messy_title_blank.xlsx` | 单格标题 + 空行 + 表头 + 4 行数据(其中一行坏金额/日期) | 表头定位、标题/空行不阻塞、坏值置空 | | `04_csv_types.csv` | 与 01 同数据,UTF-8 CSV | CSV 探测/入库;可选列脏值只置空 | | `05_required_bad.xlsx` | 同 9 列表头,20 行;第 1 行身份证 `12345` 非法 | 必填列坏值整行拒绝 + 失败明细 | | `06_csv_clean.csv` | 同 9 列表头,7 行全合法 | CSV 正常入库 | | `07_required_bad_id18.xlsx` | 同 9 列表头,20 行;第 1 行身份证 `110105194912310021`(校验位错) | 必填列整行拒绝 + 失败明细完整展示原值(不脱敏) | 01 的 9 列与脏值覆盖: - 姓名:`张三/李四/...` - 身份证号:18 位合法、15 位老证、坏校验位、`12345` - 手机号:`+86 139-0000-1111`、`123` - 银行卡号:`6222 0202 ...`、科学计数法 `6.22202E+15` - 交易日期:ISO、`2026/1/6`、`2026年1月7日`、`2026-02-30`、Excel 序列号 `45000`、紧凑 `20260108` - 交易时间:datetime、ISO、纯时间 `10:30`、`25:00`、epoch `1704067200` - 金额:`1,234.50`、`-100.00`、`(200.50)`、`约100元`、`¥2,300.5`、`100元` - 数量:整数、`1.5`、`abc` ## 4. 用例与结果 ### TC-01 自建表类型推断与脏值置空(01) - 操作:上传 `01` → 智能清洗(无候选 → 出口 B 自建表)。 - 预期:7 行入库;无效的非必填值置空;日期/金额/数量落真实类型。 - 实测:`loadedRows=7, rejectedRows=0, warned=2, selfBuiltCount=1`;AI 表「经营者信息」7 行。 - 类型实测(`ai_clean_field`): - c1 身份证号 `ID_CARD_NUMBER`、c2 手机号 `PHONE`、c3 银行卡号 `ACCT_CARD_NO`(物理 TEXT) - c4 交易日期 `DATE`、c5 交易时间 `DATE_TIME`(TIMESTAMP) - c6 金额 `MONEY`(DECIMAL(18,2))、c7 数量 `NUMBER`(DECIMAL(38,6))、c8 备注 TEXT - 数据实测: - 王五:身份证/手机/卡/纯时间 → NULL;日期 `2026-01-07`;金额 `(200.50)` → `-200.50`;数量 `1.5`。 - 赵六:坏日期/时间/金额/数量 → NULL;手机 `13800002222`、卡号正常入库。 - 钱七:`45000` → `2023-03-15`;`1704067200` → `2024-01-01 08:00:00`;`¥2,300.5` → `2,300.50`。 - 孙八:`20260108` → `2026-01-08`;`100元` → `100.00`。 - 周九:空行之后仍入库,`row_no` 保留原文件物理行号。 - 截图:[ai-table-40-rows.png](screenshots/ai-table-40-rows.png)(含 01 的 7 行) ### TC-02 必填列坏值整行拒绝 + 失败明细(05) - 前置:把 01 生成的 AI 模板字段 `c1`(身份证号)临时改为 `required=1`。 - 操作:上传 `05`(20 行,第 1 行身份证 `12345`)→ 智能清洗(指纹命中同一 AI 模板,出口 B 复用)。 - 预期:19 行入库、1 行拒绝;失败明细能看到行号/列/原因/原值。 - 实测:`loadedRows=19, rejectedRows=1, unprocessedRows=0`;job `rowTotal=20, rowLoaded=19, rowRejected=1`。 - 失败明细接口: ```json {"row_no":2,"blocking_col_index":1,"blocking_col_name":"c1", "blocking_col_cn":"身份证号", "reason":"REQUIRED_TYPE_INVALID", "errors_json":"[{\"colIndex\":1,\"column\":\"c1\",\"columnCn\":\"身份证号\",\"reason\":\"REQUIRED_TYPE_INVALID\",\"value\":\"12345\",\"blocking\":true}]"} ``` - 失败日志:`AI 清洗拒绝: ... rowNo=2, col=c1(身份证号), reason=REQUIRED_TYPE_INVALID, value=12345` - 界面:导入文件列表 → 展开文件 → 工作表行「导入失败 1」→ 弹窗显示: - 文件 / 工作表、**成功 19 / 失败 1** - 行号 2、阻塞列 `c1(身份证号)`、原因「必填列类型不合法」、原值预览 `12345` - 截图:[ai-reject-modal.png](screenshots/ai-reject-modal.png) - 后置:`required` 已重置为 0,避免影响后续样本。 ### TC-03 CSV 脏数据:CSV 不再 Skip,可选脏值只置空(04) - 第一次跑:CSV 探测成功、指纹命中 AI 模板(`headerScore=1.0`),但抽样校验因身份证列 2/7=28.6% 非法(非必填)判 **待人工**。 - **本次发现并修复**:非必填列错误不应计入抽样硬门,否则和「非必填置空、行照常入库」冲突。 修复后重跑:`loadedRows=7, rejectedRows=0, warned=2`,复用同一 AI 模板。 - 结论:CSV 探测/入库链路正常;可选脏值进入 `rowWarned`/`detail.load.optional`,不阻塞行。 ### TC-04 干净 CSV 正常入库(06) - 操作:上传 `06` → 智能清洗(指纹命中 AI 模板)。 - 实测:`loadedRows=7, rejectedRows=0, selfBuiltCount=0`。 - 结论:CSV 不再被 `SheetProbe` 用 POI 拒之门外。 ### TC-05 标题行 + 空行 + 表头定位(03) - 操作:上传 `03` → 智能清洗(无候选 → 自建表「人员交易」)。 - 实测:`loadedRows=4, rejectedRows=0`;坏金额/日期两列置空,行保留。 - 数据页:AI 表「人员交易」4 行。 - 结论:标题行(1 格)、空行、行号缺口均未阻塞;表头定位正确。 ### TC-06 统计口径与界面列表 最终导入文件列表([import-list-5-files.png](screenshots/import-list-5-files.png)): | 文件 | 导入成功 | 导入失败 | |---|---:|---:| | 06_csv_clean.csv | 7 | 0 | | 03_messy_title_blank.xlsx | 4 | 0 | | 04_csv_types.csv | 7 | 0 | | 05_required_bad.xlsx | 19 | 1 | | 01_self_built_types.xlsx | 7 | 0 | 数据页([ai-table-40-rows.png](screenshots/ai-table-40-rows.png)): - 「经营者信息」40 行 = 01 的 7 + 05 的 19 + 06 的 7 + 04 的 7。 - 「人员交易」4 行。 - 日期/时间/金额/数量在界面上按类型展示,脏值显示为空而不是空白字符串。 ### TC-07 单测覆盖 `mvn -pl ai-server -Dtest='com.zsjz.ai.module.aiclean.**.*Test,com.zsjz.ai.module.dm.**.*Test' test`: **193 个用例通过**(5 个 live 跳过)。覆盖: - 行级失败原因、必填整行拒绝、非必填置空计数 - `AiValueCoercer` 日期/时间/金额/数字/电话/身份证/卡号格式矩阵 - `HeaderScorer` 同义词/列序/单位/形状/必填覆盖与出口 A 闸门 - `sameOrderedHeaders` 复用安全 - 真 DuckDB 的类型化入库、reject 表分页/清理、先写后删 ### TC-08 必填身份证整行拒绝 + 原值不脱敏(07) - 前置:把 AI 模板字段 `c1`(身份证号)临时改为 `required=1`。 - 操作:上传 `07`(20 行,第 1 行身份证为校验位错误的完整 18 位 `110105194912310021`)→ 智能清洗。 - 预期:19 行入库、1 行拒绝;失败明细完整显示 18 位原值(不脱敏、只做超长截断)。 - 实测:`loadedRows=19, rejectedRows=1`;文件列表「导入成功 19 / 导入失败 1」。 - 失败明细弹窗: - 行号 `2` - 阻塞列 `c1(身份证号)`(有中文名时括号展示) - 原因「必填列类型不合法」 - 原值预览 `110105194912310021`(完整 18 位,无掩码) - 截图:[ai-reject-modal-id18.png](screenshots/ai-reject-modal-id18.png) - 后置:`required` 已重置为 0。 - 任务日志:进度页内只保留内存日志,离开页面即清空;导入文件页不再提供「任务日志」入口, 旧版本遗留的 `localStorage.case_clean_task_log` 会在进入进度页时清除。 ## 5. 本次发现并修复的问题 1. **非必填脏值被当成硬错误**:`AiRuleVerifier` 用 `cleanRow` 的全部错误计算列错误率, 可选身份证列 28.6% 非法导致整表转人工。修复:`cleanRow` 只回填阻塞错误, `cleanRowDetailed` 仍保留全部错误供 warned/明细统计(提交 `c4b167b`)。 2. **单 sheet 文件的失败入口**:文件行原来没有 `aiJobId/cleanMode`,点「导入失败」只能走旧 `clean_error_log`。修复:单 sheet 文件把 sheet 的 AI 任务信息带到文件行(提交 `c4b167b`)。 3. **迁移缺失**:`ai_clean_batch.loaded_rows` 等新列未建时智能清洗 500;必须执行 `sql/ai_clean_row_stats.sql`。 ## 6. 未覆盖 / 已知限制 - **LLM 匹配/打分/向量**:本次 UI 验收为了速度临时关闭了向量召回和本地 LLM (本地模型单次调用可能数分钟,DashScope 嵌入额度已耗尽)。匹配逻辑由 `HeaderScorerTest` / `AiTemplateMatcherTest` 覆盖;恢复 LLM 后建议再跑一次真实模型样本。 - **`02_reordered_headers.xlsx`**:需要 LLM 重新绑定,本次 UI 未跑;`sameOrderedHeaders` 单测已钉住「列序变化不复用旧 colIndex」。 - **多块 sheet / 合并表头**:本轮未实现,仍只处理 primaryBlock;`02` 之外的乱序/合并样本 待 Phase B 后续。 - **跨案件同表头**:`header_md5` 全局唯一键尚未改案件作用域,本轮未做双案件并发验收。 - 界面「导入失败」在**文件行**(多 sheet 文件)仍走旧日志;单 sheet 已走 AI 明细。 ## 7. 复现步骤 1. 执行 `sql/ai_clean_row_stats.sql`。 2. 启动后端 `mvn -pl ai-server spring-boot:run`;启动前端 `pnpm dev`。 3. 新建并打开案件,上传 `fixtures/` 下样本(普通浏览器可直接选择文件)。 4. 点「清洗数据」→「智能清洗」,或调用 `POST /js/a/aiClean/smartClean`。 5. 打开「案件数据 → 导入文件」,核对成功/失败数;展开文件点「导入失败」看明细。 6. 打开「案件数据 → 导入数据」,核对 AI 动态表行数、列名与日期/金额/数字展示。 7. 必填拒绝用例:先让 `01` 生成 AI 模板,再执行 ```sql update ai_clean_field set required=1 where ai_template_id = :tpl and column_name = 'c1'; ``` 上传 `05_required_bad.xlsx` 并智能清洗;验收后把 `required` 重置为 0。