|
|
@@ -0,0 +1,166 @@
|
|
|
+# AI 数据清洗改造计划(修订版)
|
|
|
+
|
|
|
+已确认口径:**必填列任何数据错误(空值/类型不合法/函数执行异常/超长溢出)→ 整行拒绝**;**无模板自建表照常入库,但单独统计,不计入模板匹配成功**;**每一行为什么没进去必须可追溯**。
|
|
|
+
|
|
|
+## 一、评审结论(摘要)
|
|
|
+
|
|
|
+**匹配率**:表头归一化删除括号语义(`金额(元)`/`金额(万元)`、`账号(本方)`/`账号(对方)` 同指纹);打分集合匹配不看列序、无同义词、短模板难破 0.90;AI 模板复用不校验 md5/列序、不跑 verify;`header_md5` 全局唯一但物理表案件级;同批次同表头撞唯一键;列数不一致直接 drop 共享表;模型自评 0.6 可把 0 分候选升 STRONG;CSV/加密/多块/合并单元格探测缺口大。
|
|
|
+
|
|
|
+**入库率**:出口 B 无日期/时间转换(出口 A 有 `FunDate/FunTime/FunDateTime`);`DATE` 收到 datetime 串、`TIME` 被猜成 `DATE_TIME`,DuckDB cast 失败整批 FAIL;金额不支持负数/括号/币种;`NUMBER→INTEGER` 溢出;一个坏值让整批 INSERT 失败;required 来自模型、必填失败整行丢且不计数;异常路径不清理;统计把未清洗算成功。
|
|
|
+
|
|
|
+**类型匹配率**:类型只按英文字段名关键词猜;`ColStat.shape` 只进提示词没进建表;出口 B 忽略 `table_field` 类型;`directionConf/fieldLen` 丢失;`FunMultiplier` 整数 NPE、小数只乘整数部分。
|
|
|
+
|
|
|
+## 二、Commit 1(Phase A):类型化、行级诊断与入库止血
|
|
|
+
|
|
|
+### A0. 行级失败诊断与成功/失败统计(本次新增重点)
|
|
|
+
|
|
|
+**A0-1 行清洗结果模型**
|
|
|
+- 新增 `ColumnError`:`colIndex / columnName / reason / valuePreview / blocking`。
|
|
|
+- reason 枚举:`REQUIRED_EMPTY`、`REQUIRED_TYPE_INVALID`、`REQUIRED_FUNC_ERROR`、`REQUIRED_LENGTH_OVERFLOW`、`OPTIONAL_TYPE_INVALID`、`OPTIONAL_FUNC_ERROR`、`OPTIONAL_LENGTH_OVERFLOW`。
|
|
|
+- `AiRecordCleaner` 新增 `cleanRowDetailed(...)`:**遍历所有列、收集全部错误**,不再遇到第一个必填错误就返回;任一 `blocking=true` → 整行拒绝;只有非必填错误 → 单元格置 null、行照常入库。
|
|
|
+- 函数链异常不再被 `FunProcess.onException` 静默吞掉:在 AI 侧捕获 `FunException`,记录 `FUNC_ERROR`;必填列 → 行拒绝,非必填 → 列置空。
|
|
|
+- 必填规则:空值、日期/金额/数字/身份证/电话/卡号解析失败、函数异常、长度/精度溢出,全部整行拒绝(已确认)。
|
|
|
+
|
|
|
+**A0-2 失败行落库(可逐行追溯)**
|
|
|
+- 在案件 DuckDB 新增 `ai_clean_reject`(懒建表,`CREATE TABLE IF NOT EXISTS`):
|
|
|
+ ```
|
|
|
+ id, job_id, case_id, file_id, sheet_id, block_no, row_no,
|
|
|
+ blocking_col_index, blocking_col_name, reason,
|
|
|
+ errors_json, -- 该行所有列错误(列名/原因/脱敏值预览)
|
|
|
+ create_time
|
|
|
+ ```
|
|
|
+ 索引 `(job_id, row_no)`。
|
|
|
+- 每个被拒绝的行写一条记录;`blocking_col_index/name` 是第一个阻塞列,`errors_json` 保留该行全部列错误,能回答“因为哪一列没进去”。
|
|
|
+- 非必填错误(行已入库但单元格为空)默认只做逐列聚合计数 + 样例,不逐行落库;配置 `zsjz.ai-clean.reject-log-optional=true` 可逐行记录。
|
|
|
+- 批量写入(攒批 flush),避免逐行 SQL;任务失败时按 `ai_job_id` 清理半截记录;删除文件/撤销任务时同步清理。
|
|
|
+- 脱敏:身份证/手机号/卡号默认掩码(保留首尾),配置 `zsjz.ai-clean.mask-sensitive=false` 可关闭;值预览长度可配。
|
|
|
+
|
|
|
+**A0-3 日志**
|
|
|
+- 每行拒绝的权威记录在 `ai_clean_reject`;应用日志只打前 N 条(默认 20)避免刷爆:
|
|
|
+ `WARN AI reject job=... file=... sheet=... row=42 col=3(金额) reason=REQUIRED_TYPE_INVALID value=1.2万`。
|
|
|
+- 每个 sheet 结束打一条汇总:`source / loaded / rejected / warned / topReasons / topColumns`。
|
|
|
+- 配置 `zsjz.ai-clean.reject-log-sample`(默认 20)、`reject-log-all`(默认 false,调试用)。
|
|
|
+- 写入异常(BatchWriter/连接):记录 `jobId`、当前 `rowNo`、异常信息;该行记 `WRITER_ERROR`,剩余行计 `row_unprocessed`。
|
|
|
+
|
|
|
+**A0-4 成功/失败条数口径**
|
|
|
+- `ai_clean_job` 新增:`row_total`(表头后非空源行)、`row_loaded`、`row_rejected`,新增 `row_warned`、`row_unprocessed`。
|
|
|
+- 成功任务:`row_loaded + row_rejected = row_total`;`row_warned` 是已入库但含非必填错误的行数(不重复计入失败)。
|
|
|
+- 失败任务:半截新行按 job 清理,`row_loaded=0`(最终表没有新增行);`row_rejected=已处理且被拒的行`;`row_unprocessed=row_total-已处理行`。
|
|
|
+- `job.detail.load` 写 JSON:`reasonCounts`、`columnRejectCounts`、`optionalErrorCounts`、`topExamples`(脱敏)。
|
|
|
+- `ai_clean_batch` 新增:`loaded_rows`、`rejected_rows`、`self_built_count`;`SmartCleanStatusVO` 下发。
|
|
|
+- 终态/`getFiles`:SUCCESS 用 `rowTotal/rowLoaded/rowRejected`;NEED_REVIEW/SKIP/FAIL 不再用 `file_info.dataNum` 计成功,成功数记 0;自建表单独展示。
|
|
|
+- SSE 终态:`completed = 既有清洗成功行 + AI rowLoaded`;`failed = 既有失败行 + AI rowRejected`;未清洗单独计数,不混入成功。
|
|
|
+
|
|
|
+**A0-5 查询接口与前端**
|
|
|
+- 新增 `GET /aiClean/job/rejections?jobId=&page=&size=&reason=`,分页返回 `row_no / 列名 / 原因 / 脱敏值 / errors_json`。
|
|
|
+- `jobs` 接口返回 `rowTotal/rowLoaded/rowRejected/rowWarned` 与 `detail.load` 汇总。
|
|
|
+- 前端:AI 表结果页/导入文件管理增加「失败明细」抽屉或表格(复用现有表格组件),显示成功/失败/警告条数与逐行原因;先做只读查看。
|
|
|
+
|
|
|
+### A1. 统一类型转换器
|
|
|
+新增 `AiValueCoercer`,`AiRecordCleaner` 调用,输出 Java 类型:
|
|
|
+- DATE → `LocalDate`、TIME → `LocalTime`、DATE_TIME → `LocalDateTime`;支持 ISO、`yyyy/M/d`、`yyyy.MM.dd`、`yyyyMMdd`、中文日期、带时间/ISO、纯时间;DATE 取日期部分、DATE_TIME 纯日期补 00:00、TIME 从 datetime 取时间;严格解析;文本 Excel 序列号仅在 DATE/DATE_TIME 且 1..2958465 兜底;10/13 位时间戳仅在表头/字段名含“时间戳/timestamp”且范围合理时解析。
|
|
|
+- MONEY → `BigDecimal`:全角转半角;去 `¥¥$€£元`/空格;支持正负号、括号负数;千分位校验后去除;支持科学计数法;百分号默认拒绝;`setScale(2, HALF_UP)`;整数位 >16 按坏值处理。
|
|
|
+- NUMBER → `BigDecimal`:千分位/科学计数法;按 `DECIMAL(38,6)` 校验精度。
|
|
|
+- PHONE/ACCT_CARD_NO/ID_CARD_NUMBER:电话支持 `+86`/空格/横线;卡号拒绝科学计数法、校验 13–19 位(可选 Luhn);身份证支持 15/18 位并校验出生日期/校验位;失败返回 null 而非空串。
|
|
|
+- `fieldType` 统一 trim+uppercase;类型化列使用保留 `+/-/.` 的清洗规则,TEXT 保持既有行为。
|
|
|
+- 初始化 `GlobalCache.FUN_REGULAR_MAP` 五个具名正则(`EXTRACT_NAMED` 目前空实现);未知 key fail-open 并记日志。
|
|
|
+
|
|
|
+### A2. 类型来源与 ColStat
|
|
|
+- 优先级:出口 A `table_field` → AI 模板 `ai_clean_field` → `ColStat.shape` → 表头关键词 → TEXT。
|
|
|
+- `AiRulePlan.forAi` 接收 `List<ColStat>` 与候选模板:绑定列用模板类型/required/fieldLen/directionConf;未绑定列用形状;自建表全部用形状+关键词。
|
|
|
+- `forStoredTemplate` 填 `directionConf`;`forExisting` 补 `zf/directionConf`;新增 `forExistingColumns` 供 verify。
|
|
|
+- `typeOf` 改词边界/中文关键词,补 TIME/NUMBER,修 `card_no/id_card/cert_no/accountNo`,避免 `updateUser/candidate/totalTime` 误判。
|
|
|
+- 修 `ColStatBuilder.shape`:日期时间/纯时间、负/括号/币种金额、13–19 位卡号不判 NUMBER、带前缀手机号;日期规则用 `find`/锚定正则。
|
|
|
+- 自建表按形状建 DATE/TIME/TIMESTAMP/DECIMAL 列,不再全 TEXT。
|
|
|
+
|
|
|
+### A3. DDL 与数值范围
|
|
|
+- `DuckTypeMapper`:MONEY → `DECIMAL(18,2)`(保持兼容,入库校验 16 位整数);NUMBER → `DECIMAL(38,6)`;显式列出 PHONE/ACCT/ID/DEBIT/CALL → TEXT;AI 表不使用 `VARCHAR(n)`,避免超长整批失败。
|
|
|
+- `FunMultiplier` 用 `BigDecimal.multiply` 重写;`SCALE` 整数/小数一致。
|
|
|
+- 入库前校验 DECIMAL 精度/长度,超范围按坏值处理,不让 DuckDB 抛到批处理层。
|
|
|
+
|
|
|
+### A4. 校验与置信度
|
|
|
+- `verify` 使用与 load 相同的执行计划(出口 A `forExistingColumns`、复用 `forStoredTemplate`、新建 `forAi`);抽 `AiRulePlan.resolve(...)` 一处生成。
|
|
|
+- `requiredFillRate` 改为逐行 required 非空率;`llmScore` 传 `draft.confidence`。
|
|
|
+- 任何 required 列被抽样拒绝(错误率 >5%)→ 直接 NeedReview,不静默删列。
|
|
|
+- 复用 AI 模板也跑一次抽样 verify(raw 表 300 行,无模型调用);精确 md5 直通只跳过模型,不跳过类型/必填检查。
|
|
|
+
|
|
|
+### A5. 失败隔离与清理
|
|
|
+- 新表:异常/零行/注册失败 → drop 新表,不登记。
|
|
|
+- 复用表:先写新 job 的行,成功后删除同 `file_id+sheet_id` 且 `ai_job_id != 当前` 的旧行;失败时 `deleteByJob` 清理半截行,旧行保留。
|
|
|
+- 复用表不存在 → 按存档字段在当前案件 `createAiTable`;失败则 FAIL,不跨案件写。
|
|
|
+- 拒绝记录按 job 清理;删除文件/撤销任务同步清理 `ai_clean_reject`。
|
|
|
+
|
|
|
+### A6. 数据库变更
|
|
|
+- `ai_clean_job`:`row_rejected`、`row_warned`、`row_unprocessed`;`ai_clean_batch`:`loaded_rows`、`rejected_rows`、`self_built_count`。
|
|
|
+- 案件 DuckDB:懒建 `ai_clean_reject`。
|
|
|
+- 更新 `sql/ai_clean.sql` + 幂等迁移脚本/启动迁移(`ALTER TABLE ... ADD COLUMN IF NOT EXISTS`)。
|
|
|
+
|
|
|
+### A7. 测试
|
|
|
+- 格式矩阵:日期/时间/金额/数字/电话/身份证/卡号;必填失败整行拒绝、非必填置空计数。
|
|
|
+- 行级诊断:10 行样本中 3 行必填坏值 + 1 行非必填坏值 → `row_total=10, row_loaded=7, row_rejected=3, row_warned=1`;`ai_clean_reject` 3 条,`row_no/列名/reason/errors_json` 正确;API 分页可查。
|
|
|
+- 一行多列错误 → 记录全部列错误,`blocking` 列取第一个。
|
|
|
+- 函数异常(SCALE/EXTRACT)必填 → 行拒绝并记 `REQUIRED_FUNC_ERROR`。
|
|
|
+- 真 DuckDB:DATE/TIME/TIMESTAMP/DECIMAL 混合格式全量入库;坏值不导致整批失败。
|
|
|
+- 复用失败保留旧行;新表失败清理;写入异常统计 `row_unprocessed`。
|
|
|
+- 统计:SUCCESS/NEED_REVIEW/SKIP/FAIL/自建表的 batch、SSE、`getFiles` 数字一致。
|
|
|
+- 脱敏:身份证/手机号/卡号在日志和接口中掩码。
|
|
|
+
|
|
|
+## 三、Commit 2(Phase B):模板匹配提升
|
|
|
+
|
|
|
+### B1. 表头规范化与指纹
|
|
|
+- 括号内容保留为语义 token(`金额(元)`→`金额元`、`账号(本方)`→`账号本方`);只去装饰性标点/空白。
|
|
|
+- 指纹只取非空表头并编码位置(`0=姓名|2=金额`),尾部空列/padding 不影响;`headers` 改 JSON 存储(legacy 逗号 fallback)。
|
|
|
+
|
|
|
+### B2. 复用安全与案件隔离
|
|
|
+- AI 模板复用必须 `headerMd5` 完全相等且列序一致;否则按表头名重映射或重调模型,绝不直接套旧 colIndex。
|
|
|
+- 复用也跑 verify;复用前确认当前案件物理表存在,缺失则按存档字段建表。
|
|
|
+- 唯一键改 `(case_id_src, header_md5)`;同批次同表头按 `(case, md5)` 复用;`register` 冲突后复用/重激活 OFFLINE。
|
|
|
+- 列数不一致不再 `dropTable`:`ALTER TABLE ADD COLUMN` 或新模板;只删当前 file/sheet 的行。
|
|
|
+
|
|
|
+### B3. 打分与出口闸门
|
|
|
+- 增加字段名匹配、required 覆盖、`ColStat.shape` 类型兼容、表头顺序;修正 Jaccard 去重。
|
|
|
+- 同义词小词典(户名/账户名称、发生额/交易金额、交易日期/日期、账号/卡号、电话/手机号等)。
|
|
|
+- 阈值自适应:短模板 ≤3 列要求全中或 ≥0.9;普通模板 `STRONG≈0.85 + required 全覆盖 + shape 不矛盾`;`WEAK≈0.55` 接入。
|
|
|
+- 出口 A 必须确定性分数达标 + required 覆盖;模型自评只做辅助,0 分候选不能靠 0.6 自评升 A。
|
|
|
+
|
|
|
+### B4. 提示词与绑定校验
|
|
|
+- 候选 brief 增加 score/来源/category/表名/required/类型/字段中文名;AI 模板展示 `cN + 中文名 + 类型`。
|
|
|
+- 输出改稳定 `fieldId` + `fileColIndex`(明确 0 基);`filterBinds` 做 trim/大小写/下划线归一,接受中文/英文别名,检测 1-based 偏移、重复源列/目标字段。
|
|
|
+- 无效绑定回灌模型 repair 一次;仍失败 → NeedReview;第二次 `askLlm` 单候选重编号 #1。
|
|
|
+- 模型 `headerRow` 校验后使用;headWindow 改为围绕 primaryBlock 表头行。
|
|
|
+
|
|
|
+### B5. 向量召回与候选池
|
|
|
+- `mergeRecall` 真正补候选:召回命中不在池内的 ONLINE 模板加载字段后追加;去重;header top N + vector top M 配额。
|
|
|
+- 嵌入/查询文本同构;移除随机 `ai_clean_{id}`、通用 sheet 名、纯 cN;加入分类/字段中文名/类型。
|
|
|
+- 候选池一次 join + 缓存;跳过无字段模板;按表头/字段集合去重;过滤 `main_id` 不在 `StrategyType` 的既有模板。
|
|
|
+
|
|
|
+### B6. 探测与结构
|
|
|
+- CSV 走 `AiRowReader`/raw 表流式前 200 行探测,不再用 POI;CSV 不再 Skip。
|
|
|
+- 加密文件把 `FileInfo.password` 传入 `SheetProbe`/`AiRowReader`。
|
|
|
+- 多块:按 `BlockHint` 每块建 job(`block_no` 已有唯一键),每块独立 headers/colStats/md5;入库只读 `firstDataRow..lastDataRow`。
|
|
|
+- 合并单元格:先尝试表头压平(横向填充、纵向 `/` 合并),成功则不转人工;数据区合并不再一刀切;无法压平才 NeedReview;`BlockHint.mergedRegions` 真正装配。
|
|
|
+- 表头定位加入列形状/文本密度/关键词/下一行数据打分;结构门移到模型调用之前。
|
|
|
+
|
|
|
+### B7. 指标与闭环
|
|
|
+- 批次增加漏斗:headerCache/scoreHit/llmHit/selfBuilt/needReview/failed;`matched_by` 区分 HEADER_CACHE/SCORE/LLM/SELF_HEADER。
|
|
|
+- 后续:人工修正 API、DRAFT→ONLINE 晋升、负样本/golden set。
|
|
|
+
|
|
|
+### B8. 测试
|
|
|
+- 归一化/指纹:括号限定词、单位、尾部空列、列序、逗号列名。
|
|
|
+- 匹配:同义词、短模板、列序变化、低分候选不得出口 A、向量补候选、配额。
|
|
|
+- 复用:精确 md5、列序变化重映射、表不存在建表、同批次同 md5、跨案件隔离。
|
|
|
+- 探测:CSV、加密、多块、合并表头、20 行以后表头。
|
|
|
+- 端到端:多块/合并/CSV 实际入库;匹配漏斗计数;无错列写入。
|
|
|
+
|
|
|
+## 四、验收标准
|
|
|
+- **Commit 1**:上述行级诊断/计数测试全绿;真 DuckDB 上 DATE/TIME/TIMESTAMP/DECIMAL 不再整批失败;`ai_clean_reject` 能查到每一行被拒的列与原因;`row_total=row_loaded+row_rejected`(成功任务);未清洗 sheet 不再计成功。
|
|
|
+- **Commit 2**:golden set(同表头/同义词/列序/多列/少列/括号单位)档位符合预期;列序变化不写错列;同批次同 md5 全部入库;跨案件互不污染;CSV/多块/合并表头可入库。
|
|
|
+- **前后对比**:用 `smart-clean-test-cases.md` + 三个真实样本跑同一批,记录匹配漏斗、成功/失败行数、逐列错误率;以“自建表不参与匹配率、未清洗不计成功”后的真实基线为准。
|
|
|
+
|
|
|
+## 五、风险与顺序
|
|
|
+- 拒绝记录表会随数据量增长:按 job/file 清理 + 可配置保留上限;应用日志只采样。
|
|
|
+- 脱敏默认开启,避免身份证/手机号/卡号进日志。
|
|
|
+- 必填严格化会增加失败行,但每一条都能在 `ai_clean_reject` 中看到原因。
|
|
|
+- 唯一键变更需要迁移/回滚脚本;先建新索引、回填 `case_id_src`、再删旧索引。
|
|
|
+- 多块/合并表头会增加 job 与模型调用;加每 sheet 最大块数、每批 token 预算护栏。
|
|
|
+- 先提交 Commit 1 并跑完测试,再开始 Commit 2。
|