已确认口径:必填列任何数据错误(空值/类型不合法/函数执行异常/超长溢出)→ 整行拒绝;无模板自建表照常入库,但单独统计,不计入模板匹配成功;每一行为什么没进去必须可追溯。
匹配率:表头归一化删除括号语义(金额(元)/金额(万元)、账号(本方)/账号(对方) 同指纹);打分集合匹配不看列序、无同义词、短模板难破 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、小数只乘整数部分。
A0-1 行清洗结果模型
ColumnError:colIndex / columnName / reason / valuePreview / blocking。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万。source / loaded / rejected / warned / topReasons / topColumns。zsjz.ai-clean.reject-log-sample(默认 20)、reject-log-all(默认 false,调试用)。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 是已入库但含非必填错误的行数(不重复计入失败)。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;自建表单独展示。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 汇总。新增 AiValueCoercer,AiRecordCleaner 调用,输出 Java 类型:
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”且范围合理时解析。BigDecimal:全角转半角;去 ¥¥$€£元/空格;支持正负号、括号负数;千分位校验后去除;支持科学计数法;百分号默认拒绝;setScale(2, HALF_UP);整数位 >16 按坏值处理。BigDecimal:千分位/科学计数法;按 DECIMAL(38,6) 校验精度。+86/空格/横线;卡号拒绝科学计数法、校验 13–19 位(可选 Luhn);身份证支持 15/18 位并校验出生日期/校验位;失败返回 null 而非空串。fieldType 统一 trim+uppercase;类型化列使用保留 +/-/. 的清洗规则,TEXT 保持既有行为。GlobalCache.FUN_REGULAR_MAP 五个具名正则(EXTRACT_NAMED 目前空实现);未知 key fail-open 并记日志。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/锚定正则。DuckTypeMapper:MONEY → DECIMAL(18,2)(保持兼容,入库校验 16 位整数);NUMBER → DECIMAL(38,6);显式列出 PHONE/ACCT/ID/DEBIT/CALL → TEXT;AI 表不使用 VARCHAR(n),避免超长整批失败。FunMultiplier 用 BigDecimal.multiply 重写;SCALE 整数/小数一致。verify 使用与 load 相同的执行计划(出口 A forExistingColumns、复用 forStoredTemplate、新建 forAi);抽 AiRulePlan.resolve(...) 一处生成。requiredFillRate 改为逐行 required 非空率;llmScore 传 draft.confidence。file_id+sheet_id 且 ai_job_id != 当前 的旧行;失败时 deleteByJob 清理半截行,旧行保留。createAiTable;失败则 FAIL,不跨案件写。ai_clean_reject。ai_clean_job:row_rejected、row_warned、row_unprocessed;ai_clean_batch:loaded_rows、rejected_rows、self_built_count。ai_clean_reject。sql/ai_clean.sql + 幂等迁移脚本/启动迁移(ALTER TABLE ... ADD COLUMN IF NOT EXISTS)。row_total=10, row_loaded=7, row_rejected=3, row_warned=1;ai_clean_reject 3 条,row_no/列名/reason/errors_json 正确;API 分页可查。blocking 列取第一个。REQUIRED_FUNC_ERROR。row_unprocessed。getFiles 数字一致。金额(元)→金额元、账号(本方)→账号本方);只去装饰性标点/空白。0=姓名|2=金额),尾部空列/padding 不影响;headers 改 JSON 存储(legacy 逗号 fallback)。headerMd5 完全相等且列序一致;否则按表头名重映射或重调模型,绝不直接套旧 colIndex。(case_id_src, header_md5);同批次同表头按 (case, md5) 复用;register 冲突后复用/重激活 OFFLINE。dropTable:ALTER TABLE ADD COLUMN 或新模板;只删当前 file/sheet 的行。ColStat.shape 类型兼容、表头顺序;修正 Jaccard 去重。STRONG≈0.85 + required 全覆盖 + shape 不矛盾;WEAK≈0.55 接入。cN + 中文名 + 类型。fieldId + fileColIndex(明确 0 基);filterBinds 做 trim/大小写/下划线归一,接受中文/英文别名,检测 1-based 偏移、重复源列/目标字段。askLlm 单候选重编号 #1。headerRow 校验后使用;headWindow 改为围绕 primaryBlock 表头行。mergeRecall 真正补候选:召回命中不在池内的 ONLINE 模板加载字段后追加;去重;header top N + vector top M 配额。ai_clean_{id}、通用 sheet 名、纯 cN;加入分类/字段中文名/类型。main_id 不在 StrategyType 的既有模板。AiRowReader/raw 表流式前 200 行探测,不再用 POI;CSV 不再 Skip。FileInfo.password 传入 SheetProbe/AiRowReader。BlockHint 每块建 job(block_no 已有唯一键),每块独立 headers/colStats/md5;入库只读 firstDataRow..lastDataRow。/ 合并),成功则不转人工;数据区合并不再一刀切;无法压平才 NeedReview;BlockHint.mergedRegions 真正装配。matched_by 区分 HEADER_CACHE/SCORE/LLM/SELF_HEADER。ai_clean_reject 能查到每一行被拒的列与原因;row_total=row_loaded+row_rejected(成功任务);未清洗 sheet 不再计成功。../../docs/design/smart-clean-test-cases.md + 三个真实样本跑同一批,记录匹配漏斗、成功/失败行数、逐列错误率;以“自建表不参与匹配率、未清洗不计成功”后的真实基线为准。ai_clean_reject 中看到原因。case_id_src、再删旧索引。