10 KiB
Cleaner 数据库持久化设计
背景
Cleaner 当前使用 Markdown 文件做执行记录持久化,通过 RustFS 上传存储。存在以下问题:
- 报告是非结构化文本,无法程序化查询和统计
- 历史记录无法按用户、时间、状态筛选
- 重试时依赖文件名去重,覆盖了首次执行的崩溃信息
- 前端需要通过 RustFS 下载报告再解析展示,链路长且脆弱
Extractor 已有成熟的数据库持久化模式(ExtractorOperationHistory 表 + DAO + 前端弹窗),Cleaner 应复用相同模式。
设计决策
| 决策项 | 选择 | 理由 |
|---|---|---|
| 表结构 | 独立建表,不与 Extractor 共用 | Cleaner 数据结构差异大(双层、物料级详情),独立更清晰 |
| 记录粒度 | 执行 + 订单 + 物料三层 | 执行表存全局信息,订单表存订单汇总,物料表存操作明细 |
| 批次标识 | BatchId(UUID),与 Extractor 一致 |
标准、简洁,不需要嵌入时间戳 |
| 重试记录 | 不覆盖,每次尝试独立写入,用 AttemptNumber 区分 |
保留完整审计链,为后续智能跳过提供数据基础 |
| 报告文件 | 移除 Markdown 报告和 RustFS 上传 | 数据库完全替代,报告相关代码(CleanerReportGenerator、generateAndUploadReport)删除 |
| 前端历史 | 独立 CleanerOperationHistoryModal,复用 Extractor 的 UI 模式 | 放在 CleanerPage 上,与 Extractor 的"操作历史"按钮对齐 |
数据库表结构
所有表的 schema 为 ERPAuto。
1. CleanerExecution(执行级)
全限定名:ERPAuto.CleanerExecution
一次清理操作(含重试)的全局信息。每次尝试一行记录。
| 列名 | 类型 | 说明 |
|---|---|---|
| ID | INT IDENTITY | 自增主键 |
| BatchId | UNIQUEIDENTIFIER | 批次 ID,一次清理操作(含重试)共享 |
| AttemptNumber | INT | 第几次尝试(1=首次,2=外层重试) |
| UserId | INT | 操作用户 ID |
| Username | NVARCHAR(255) | 操作用户名 |
| OperationTime | DATETIME | 操作时间 |
| EndTime | DATETIME | 结束时间 |
| Status | NVARCHAR(50) | pending / success / failed / partial / crashed |
| IsDryRun | BIT | 是否模拟运行 |
| TotalOrders | INT | 订单总数 |
| OrdersProcessed | INT | 已处理订单数 |
| TotalMaterialsDeleted | INT | 总删除物料数 |
| TotalMaterialsSkipped | INT | 总跳过物料数 |
| TotalMaterialsFailed | INT | 总失败物料数 |
| TotalUncertainDeletions | INT | 总不确定删除数 |
| ErrorMessage | NVARCHAR(MAX) | 全局错误信息(如外层崩溃原因) |
| AppVersion | NVARCHAR(20) | 应用版本号 |
2. CleanerOrderHistory(订单级)
全限定名:ERPAuto.CleanerOrderHistory
每个订单在每次尝试中的执行结果。每个订单每次尝试一行记录。
| 列名 | 类型 | 说明 |
|---|---|---|
| ID | INT IDENTITY | 自增主键 |
| BatchId | UNIQUEIDENTIFIER | 关联执行表 BatchId |
| AttemptNumber | INT | 关联执行表 AttemptNumber |
| OrderNumber | NVARCHAR(255) | 订单号 |
| Status | NVARCHAR(50) | pending / success / failed |
| MaterialsDeleted | INT | 删除物料数 |
| MaterialsSkipped | INT | 跳过物料数 |
| MaterialsFailed | INT | 删除失败物料数 |
| UncertainDeletions | INT | 不确定删除数 |
| RetryCount | INT | 内层重试次数 |
| RetrySuccess | BIT | 内层重试是否成功 |
| ErrorMessage | NVARCHAR(MAX) | 错误信息 |
关联方式:BatchId + AttemptNumber 关联执行表。
3. CleanerMaterialDetail(物料级)
全限定名:ERPAuto.CleanerMaterialDetail
每个物料在每次尝试中的操作明细。
| 列名 | 类型 | 说明 |
|---|---|---|
| ID | INT IDENTITY | 自增主键 |
| BatchId | UNIQUEIDENTIFIER | 关联执行表 BatchId |
| AttemptNumber | INT | 关联执行表 AttemptNumber |
| OrderNumber | NVARCHAR(255) | 所属订单号 |
| MaterialCode | NVARCHAR(255) | 物料代码 |
| MaterialName | NVARCHAR(255) | 物料名称 |
| RowNumber | INT | 行号 |
| Result | NVARCHAR(50) | deleted / skipped / failed / uncertain |
| Reason | NVARCHAR(MAX) | 跳过/失败原因 |
| AttemptCount | INT | 删除尝试次数 |
| FinalErrorCategory | NVARCHAR(50) | 最终错误分类 |
关联方式:BatchId + AttemptNumber + OrderNumber 关联订单表。
数据示例
首次执行到第 80 个订单时崩溃,外层重试成功完成全部 211 个订单:
CleanerExecution
BatchId=uuid-1, Attempt=1, Status=crashed, TotalOrders=211, Processed=80, ...
BatchId=uuid-1, Attempt=2, Status=success, TotalOrders=211, Processed=211, ...
CleanerOrderHistory(Attempt=1 中部分记录)
BatchId=uuid-1, Attempt=1, Order=SC001, Status=success, Deleted=5, Skipped=1
BatchId=uuid-1, Attempt=1, Order=SC080, Status=crashed, Error=查询超时
CleanerOrderHistory(Attempt=2 中部分记录)
BatchId=uuid-1, Attempt=2, Order=SC001, Status=success, Deleted=5, Skipped=1
BatchId=uuid-1, Attempt=2, Order=SC080, Status=success, Deleted=3, Skipped=0
BatchId=uuid-1, Attempt=2, Order=SC211, Status=success, Deleted=2, Skipped=0
CleanerMaterialDetail(SC080 在 Attempt=2 中的物料)
BatchId=uuid-1, Attempt=2, Order=SC080, Material=MAT-001, Result=deleted
BatchId=uuid-1, Attempt=2, Order=SC080, Material=MAT-002, Result=skipped, Reason=不可删除
写入时机
用户点击"执行清理"
→ IPC: cleaner:run
→ cleaner-handler.ts
→ ① BatchId = randomUUID()
→ ② 插入 CleanerExecution(Status=pending)
→ ③ 插入 CleanerOrderHistory(所有订单,Status=pending)
→ ④ 执行清理(CleanerApplicationService.runCleaner)
→ ⑤ 更新 CleanerExecution(Status=success/failed/partial/crashed)
→ ⑥ 更新 CleanerOrderHistory(每个订单的结果)
→ ⑦ 插入 CleanerMaterialDetail(每个物料的操作明细)
→ ⑧ 如果 crashed → 外层重试
→ 插入新的 CleanerExecution(AttemptNumber=2, Status=pending)
→ 插入新的 CleanerOrderHistory(AttemptNumber=2, Status=pending)
→ 重新执行
→ 更新执行表和订单表状态
→ 插入物料明细
- 步骤 ②③:在
cleaner-handler.ts中,执行前写入,记录操作人、全局配置、待处理订单 - 步骤 ⑤⑥⑦:在
CleanerApplicationService中,执行完成后回调 DAO 写入结果 - 步骤 ⑧:外层重试时,三张表都新增 AttemptNumber=2 的记录,首次尝试的数据完整保留
变更清单
新增文件
-
src/main/services/database/cleaner-operation-history-dao.tsCleanerOperationHistoryDAO类- 执行表操作:insertExecution、updateExecutionStatus
- 订单表操作:insertOrderRecords、updateOrderStatus
- 物料表操作:insertMaterialDetails
- 查询操作:getBatches、getBatchDetails(含订单+物料)、deleteBatch
- 参考
ExtractorOperationHistoryDAO的模式,表名使用ERPAuto.CleanerExecution、ERPAuto.CleanerOrderHistory、ERPAuto.CleanerMaterialDetail
-
src/main/types/cleaner-history.types.tsCleanerExecutionRecord、CleanerOrderRecord、CleanerMaterialRecordCleanerBatchStats、InsertCleanerExecutionInput、InsertOrderInput、InsertMaterialDetailInput
-
src/renderer/src/components/CleanerOperationHistoryModal.tsx- 操作历史弹窗,复用 ExtractorOperationHistoryModal 的 UI 模式
- 批次列表(按 BatchId 聚合,显示操作时间、用户、状态、成功/失败数,区分多次尝试)
- 展开明细(订单列表,每订单的删除/跳过/失败数)
- 物料级详情(第二层展开,显示每个物料的操作结果)
- 管理员可按用户筛选、可删除批次
修改文件
-
src/main/ipc/cleaner-handler.tsCLEANER_RUNhandler 中:执行前插入 execution + order 的 pending 记录,执行后更新结果- 新增 IPC handlers:
CLEANER_HISTORY_BATCHES、CLEANER_HISTORY_DETAILS、CLEANER_HISTORY_DELETE
-
src/main/services/cleaner/cleaner-application-service.tsrunCleaner接收batchId参数- 移除
generateExecutionId()函数 - 移除
generateAndUploadReport()方法 - 移除
executionId相关逻辑 - 外层重试时,通过 DAO 写入 AttemptNumber=2 的执行记录和订单记录,不覆盖首次尝试
- 执行完成后回调 DAO 写入订单结果和物料明细
-
src/main/ipc/index.ts- 注册新的 cleaner history IPC handlers
-
src/preload/api/cleaner.ts- 新增 IPC 调用方法:getBatches、getBatchDetails、deleteBatch
-
src/preload/index.d.tsCleanerAPI接口新增 getBatches、getBatchDetails、deleteBatch 类型声明
-
src/renderer/src/pages/CleanerPage.tsx- 新增"操作历史"按钮
- 引入 CleanerOperationHistoryModal
删除文件
src/main/services/report/cleaner-report-generator.ts- 整个文件删除,报告生成逻辑不再需要
可选清理
-
src/renderer/src/components/ReportViewerDialog.tsx- 基于 RustFS 文件的报告查看器,Cleaner 不再使用
- 如果 Extractor 不共用此组件,可删除
-
src/renderer/src/components/ReportAnalysisDialog.tsx- 基于报告文件的分析,Cleaner 不再使用
- 后续可基于数据库重新实现统计分析
移除的概念
| 概念 | 原因 |
|---|---|
| ExecutionId(CLN-时间戳-随机) | 为文件名设计,数据库用 UUID |
| generateExecutionId() | 随 ExecutionId 一起移除 |
| CleanerReportGenerator | Markdown 报告生成器,被数据库替代 |
| generateAndUploadReport() | RustFS 上传链路,被数据库写入替代 |
| 报告文件名去重 | 数据库 UUID 天然唯一 |
| 重试覆盖旧报告 | 数据库保留所有尝试记录 |
不涉及的部分
- Extractor 的持久化逻辑不变
- 数据库 schema 迁移(需 DBA 创建表,应用层只做 CRUD)
- 后续智能跳过功能(基于已有 success 记录跳过已成功的订单)
- 内层重试逻辑(订单级/物料级)不变