本文引用的文件
AI 数据清洗在“智能清洗”链路中,把用户上传的文件按模板或 AI 推断规则逐行清洗:必填列出错会整行拒绝,非必填列出错只清空该单元格并继续入库。系统需要同时回答三件事:
围绕这些目标,代码定义了 AiRowCleanResult、ColumnError、AiRejectReason、AiRejectRecord 四个核心概念,并在 DuckDB 案件库中落盘失败明细,在 ai_clean_job 和 ai_clean_batch 中聚合统计。
| 字段 | 含义 | 说明 |
|---|---|---|
| values | 列名到清洗后值的映射 | 被拒绝的行也保留可用的部分值,用于排障 |
| errors | 本行所有列的错误列表 | 不只记录第一个阻塞错误 |
| rejected | 是否整行拒绝 | 存在任意 blocking 错误时为真 |
它还提供两个诊断方法:
firstBlocking():返回第一个导致整行拒绝的错误列;没有阻塞错误时为空。hasOptionalError():是否存在非阻塞错误;这类错误不会阻止入库,但会把对应单元格置空。这个模型是“单行清洗”的边界:它不关心数据库,也不直接写日志,只表达“这一行洗完后是什么状态”。
章节来源
| 字段 | 含义 | 说明 |
|---|---|---|
| colIndex | 原文件中的列序号 | 0 基 |
| columnName | 动态表物理列名 | 例如 c0、c1 |
| reason | 失败原因枚举 | 来自 AiRejectReason |
| valuePreview | 原始值预览 | 超长截断,不做脱敏 |
| blocking | 是否导致整行拒绝 | 由原因枚举决定 |
ColumnError 是比 AiRejectRecord 更细的粒度:一个拒绝行可能包含多个列错误,但只有第一个 blocking 列会被选为“拒绝原因列”。
章节来源
| 枚举值 | 含义 | 是否阻塞 | 典型场景 |
|---|---|---|---|
| REQUIRED_EMPTY | 必填列为空 | 是 | 身份证号、金额等必填项缺失 |
| REQUIRED_TYPE_INVALID | 必填列类型解析失败 | 是 | 日期、数字、身份证、电话、卡号格式不对 |
| REQUIRED_FUNC_ERROR | 必填列清洗函数执行异常 | 是 | 自定义转换函数抛错 |
| REQUIRED_LENGTH_OVERFLOW | 必填列超长或精度溢出 | 是 | 数值超出目标长度/精度 |
| OPTIONAL_TYPE_INVALID | 非必填列类型解析失败 | 否 | 可选字段格式错误,单元格置空 |
| OPTIONAL_FUNC_ERROR | 非必填列清洗函数执行异常 | 否 | 可选字段转换异常 |
| OPTIONAL_LENGTH_OVERFLOW | 非必填列超长或精度溢出 | 否 | 可选字段超出长度/精度 |
| WRITER_ERROR | 写入数据库失败 | 是 | DuckDB 批量写入异常 |
关键语义:
REQUIRED_ 开头的错误一律阻塞整行。WRITER_ERROR 也阻塞整行,因为它表示“这行没能成功落库”。label() 提供界面文案,如“必填列为空”“非必填列类型不合法(已置空)”。章节来源
| 字段 | 含义 |
|---|---|
| jobId | AI 清洗任务 id |
| caseId | 案件 id |
| fileId | 文件根 id |
| sheetId | sheet id |
| blockNo | 表块序号 |
| rowNo | 原文件物理行号 |
| blockingColIndex | 第一个阻塞列的文件列序号 |
| blockingColName | 第一个阻塞列的物理列名 |
| blockingColNameCn | 第一个阻塞列的中文表头 |
| reason | 拒绝原因枚举名称 |
| errorsJson | 该行全部列错误的 JSON 序列化 |
一条 AiRejectRecord 对应一个被拒绝的数据行。它的价值在于:既能回答“这一行为什么没进去”,也能通过 errorsJson 还原整行的列级错误,从而支持“按列聚合看哪一列错得最多”。
章节来源
AiRejectRecord.java:1-35
classDiagram
class AiRowCleanResult {
+values
+errors
+rejected
+firstBlocking()
+hasOptionalError()
}
class ColumnError {
+colIndex
+columnName
+reason
+valuePreview
+blocking
}
class AiRejectReason {
+REQUIRED_EMPTY
+REQUIRED_TYPE_INVALID
+REQUIRED_FUNC_ERROR
+REQUIRED_LENGTH_OVERFLOW
+OPTIONAL_TYPE_INVALID
+OPTIONAL_FUNC_ERROR
+OPTIONAL_LENGTH_OVERFLOW
+WRITER_ERROR
+blocking()
+label()
}
class AiRejectRecord {
+jobId
+caseId
+fileId
+sheetId
+blockNo
+rowNo
+blockingColIndex
+blockingColName
+blockingColNameCn
+reason
+errorsJson
}
AiRowCleanResult --> ColumnError : "包含"
ColumnError --> AiRejectReason : "使用"
AiRejectRecord --> AiRejectReason : "reason 引用"
图表来源
清洗过程不是简单的“成功/失败二选一”,而是三层关系:
ColumnError,包括空值、类型不合法、函数异常、超长溢出。AiRowCleanResult.rejected = true;否则行可以入库。AiRejectReason 作为 AiRejectRecord.reason。因此:
rejected = false 的行,即使它们仍有非阻塞错误。rejected = true 的行,最终会写入 ai_clean_reject。“拒绝原因分类”是用户可理解的标签,用于统计“必填列为空有多少、类型不合法有多少、写入失败有多少”。
flowchart TD
Start["读取一行原始数据"] --> CleanColumns["逐列清洗<br/>生成 ColumnError"]
CleanColumns --> HasBlocking{"是否存在阻塞错误?"}
HasBlocking -->|"是"| Reject["标记行拒绝<br/>写入 ai_clean_reject"]
HasBlocking -->|"否"| Insert["写入目标表"]
Reject --> OptionalErrors{"是否有非阻塞错误?"}
OptionalErrors -->|"有"| Warn["计入 warned 数量"]
OptionalErrors -->|"无"| End
Insert --> End["完成"]
图表来源
章节来源
清洗结果并不只保存在一张表里,而是分两层:
| 层级 | 存储位置 | 内容 | 用途 |
|---|---|---|---|
| 行级明细 | DuckDB 案件库的 ai_clean_reject |
每个被拒绝行一条记录 | 查“某行为什么没进”、按列统计 |
| 任务级统计 | PostgreSQL 元数据表的 ai_clean_job、ai_clean_batch |
总行数、成功数、拒绝数、警告数、未处理数 | 进度页、批次汇总、治理树 |
AiRecordCleaner.cleanRowDetailed 负责逐列清洗:
AiRejectCollector 负责把拒绝行转成 AiRejectRecord,并按原因、列、非必填错误分别聚合计数。它还会采样前若干条拒绝日志,避免十万行文件中大量拒绝记录刷爆应用日志。
拒绝记录采用批缓冲写入:
ai_clean_reject 表。每条记录包含 job、file、sheet、block、rowNo、首个阻塞列、原因和全部列错误 JSON。
sequenceDiagram
participant Cleaner as "AiRecordCleaner"
participant Collector as "AiRejectCollector"
participant DB as "AiDuckDb"
participant Table as "ai_clean_reject"
Cleaner->>Collector: record(rowNo, result)
alt 行被拒绝
Collector->>Collector: 更新 rejected / reasons / columns
Collector->>DB: insertRejects(records)
DB->>Table: INSERT 批量写入
else 行成功但有非阻塞错误
Collector->>Collector: 更新 warned / optionalCounts
end
图表来源
章节来源
ai_clean_job 中维护以下行级统计:
| 字段 | 含义 |
|---|---|
| row_total | 输入总行数 |
| row_loaded | 成功入库行数 |
| row_rejected | 因必填为空、类型错误、函数异常、超长或写入失败而整行拒绝的行数 |
| row_warned | 已入库但包含非必填错误的行数 |
| row_unprocessed | 失败任务中未处理到的行数 |
ai_clean_batch 中维护以下批次维度统计:
| 字段 | 含义 |
|---|---|
| loaded_rows | 出口 B 实际入库行数 |
| rejected_rows | 出口 B 逐行拒绝行数 |
| unprocessed_rows | 待人工、跳过或失败任务剩余行数 |
| self_built_count | 无模板自建表的 sheet 数 |
该脚本不重新建表,而是为已存在的 ai_clean_job 和 ai_clean_batch 增加统计列:
ai_clean_job 增加 row_rejected、row_warned、row_unprocessed。ai_clean_batch 增加 loaded_rows、rejected_rows、unprocessed_rows、self_built_count。它与 ai_clean.sql 的关系是:
ai_clean.sql:定义 AI 清洗元数据主表结构,包括模板、字段、规则、任务、批次、同义词库。ai_clean_row_stats.sql:在已有结构上扩展行级统计列,便于后续按任务/批次统计。
graph TB
Job["ai_clean_job<br/>row_total / row_loaded / row_rejected / row_warned / row_unprocessed"]
Batch["ai_clean_batch<br/>loaded_rows / rejected_rows / unprocessed_rows / self_built_count"]
Reject["ai_clean_reject<br/>拒绝行明细"]
Template["ai_clean_template"]
Field["ai_clean_field"]
Rule["ai_clean_rule"]
Template --> Job
Field --> Job
Rule --> Job
Job --> Batch
Job --> Reject
图表来源
章节来源
前端“数据清洗进度”页面在任务完成后展示:
点击“查看结果”会进入 AI 动态表的数据分页接口。后端 AiCleanService.dataPage 会:
c0..cN 查询 DuckDB;head、pages.records、pages.total 等;章节来源
“查看清洗日志”打开 CleanLogModal,基于 governApi.cleanErrorLogList 查询 CleanErrorLog 相关日志。它支持:
注意:这里主要展示的是“清洗日志”,不是 ai_clean_reject 的全部拒绝明细。真正的拒绝明细需要通过 SQL 查询 DuckDB 案件库中的 ai_clean_reject 表。
章节来源
成功数据位于 AI 生成的动态表中,表名通常以 AI_ 前缀开头,例如“AI_银行交易流水”。可通过以下方式导出:
由于后端分页接口已经封装了 AiDuckDb.selectPage 和 AiDuckDb.count,也可以通过调用后端 /govern/treeTablePage 的 AI 分流接口获取结构化数据。
拒绝数据位于 DuckDB 案件库的 ai_clean_reject 表中,关键字段包括:
job_id:关联 ai_clean_job。file_id、sheet_id、block_no、row_no:定位原始文件中的行。blocking_col_index、blocking_col_name、blocking_col_name_cn:首个阻塞列。reason:拒绝原因枚举名称。errors_json:该行全部列错误的 JSON,包含列名、原因、原值预览和是否阻塞。常用查询方向:
章节来源
AI_ 前缀。ai_clean_reject 表中,按 job/file/sheet/block/rowNo 定位。ai_clean_job 和 ai_clean_batch 中,包含状态、耗时、token 用量、统计摘要等。ai_clean_template、ai_clean_field、ai_clean_rule 中,属于长期资产。当前代码中没有统一的定时清理策略删除历史拒绝记录;拒绝记录的生命周期通常与任务生命周期一致。
撤销接口 AiCleanService.revert 会在按 job 撤销时:
ai_clean_reject 记录;这意味着:如果用户主动撤销清洗结果,成功数据和拒绝记录都会清理。
章节来源
AiRejectCollector 每 500 条拒绝记录 flush 一次,避免频繁单条插入。
AiDuckDb.BatchWriter 每 2000 行 flush 一次,采用流式写入器,避免在回调中全量 collect。这是处理大表的关键设计。
拒绝日志默认只打前若干条,而不是每行都打。可通过配置控制:
zsjz.ai-clean.reject-log-sample:抽样条数;zsjz.ai-clean.reject-log-all:是否每行拒绝都打日志。VerifyReport 的 previewRows 限制为前 5 行,detail JSON 最大长度 8000,防止 jobs 列表查询被大对象撑爆。
拒绝记录中的 valuePreview 只做超长截断,不做脱敏,长度为固定上限,避免过长值污染统计和日志。
章节来源
可能原因:
排查步骤:
ai_clean_job.status 和 exit_code。ai_clean_job.row_total 是否为 0。章节来源
可能原因:
ai_clean_reject 已被删除。排查步骤:
ai_clean_job.row_rejected。job_id 精确查询 ai_clean_reject。revert。章节来源
可能原因:
optionalCounts。blockingColNameCn 可能为空,此时显示回退到物理列名。排查建议:
blocking_col_index 和 blocking_col_name。errors_json,查看该行所有列错误。ai_clean_field.field_name_cn 还原中文列名。WRITER_ERROR,检查 errors_json 中的消息,而不是依赖列名。章节来源
可能原因:
AiRejectCollector.pending 缓冲区。排查建议:
ai_clean_job 最新状态。章节来源
可能原因:
errors_json 中包含原值预览,且未做脱敏。优化建议:
errors_json。章节来源
AI 清洗结果的设计围绕“行级可诊断、列级可统计、任务级可观测”展开:
AiRowCleanResult 表达单行清洗结果;ColumnError 表达单列错误;AiRejectReason 区分必填与非必填、类型/函数/长度/写入失败;AiRejectRecord 持久化拒绝行,支撑排障和聚合统计;ai_clean_job 和 ai_clean_batch 提供任务与批次维度的行数统计;ai_clean_row_stats.sql 负责扩展统计列,使现有表能承载新的统计需求;ai_clean_reject 表分析。在大规模数据下,系统通过批写入、流式写入、日志采样和 JSON 有界控制性能;在可维护性上,通过撤销接口、job 唯一键和统计摘要保证结果可追溯、可清理、可对比。