refactor(cleaner): remove Markdown report generator

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-04-13 12:06:19 +08:00
parent 998edb4b84
commit 7dfa88c2a3
3 changed files with 855 additions and 378 deletions

View File

@@ -0,0 +1,235 @@
# 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()
→ ② 插入 CleanerExecutionStatus=pending
→ ③ 插入 CleanerOrderHistory所有订单Status=pending
→ ④ 执行清理CleanerApplicationService.runCleaner
→ ⑤ 更新 CleanerExecutionStatus=success/failed/partial/crashed
→ ⑥ 更新 CleanerOrderHistory每个订单的结果
→ ⑦ 插入 CleanerMaterialDetail每个物料的操作明细
→ ⑧ 如果 crashed → 外层重试
→ 插入新的 CleanerExecutionAttemptNumber=2, Status=pending
→ 插入新的 CleanerOrderHistoryAttemptNumber=2, Status=pending
→ 重新执行
→ 更新执行表和订单表状态
→ 插入物料明细
```
- 步骤 ②③:在 `cleaner-handler.ts` 中,执行前写入,记录操作人、全局配置、待处理订单
- 步骤 ⑤⑥⑦:在 `CleanerApplicationService` 中,执行完成后回调 DAO 写入结果
- 步骤 ⑧:外层重试时,三张表都新增 AttemptNumber=2 的记录,首次尝试的数据完整保留
## 变更清单
### 新增文件
1. **`src/main/services/database/cleaner-operation-history-dao.ts`**
- `CleanerOperationHistoryDAO`
- 执行表操作insertExecution、updateExecutionStatus
- 订单表操作insertOrderRecords、updateOrderStatus
- 物料表操作insertMaterialDetails
- 查询操作getBatches、getBatchDetails含订单+物料、deleteBatch
- 参考 `ExtractorOperationHistoryDAO` 的模式,表名使用 `ERPAuto.CleanerExecution``ERPAuto.CleanerOrderHistory``ERPAuto.CleanerMaterialDetail`
2. **`src/main/types/cleaner-history.types.ts`**
- `CleanerExecutionRecord``CleanerOrderRecord``CleanerMaterialRecord`
- `CleanerBatchStats``InsertCleanerExecutionInput``InsertOrderInput``InsertMaterialDetailInput`
3. **`src/renderer/src/components/CleanerOperationHistoryModal.tsx`**
- 操作历史弹窗,复用 ExtractorOperationHistoryModal 的 UI 模式
- 批次列表(按 BatchId 聚合,显示操作时间、用户、状态、成功/失败数,区分多次尝试)
- 展开明细(订单列表,每订单的删除/跳过/失败数)
- 物料级详情(第二层展开,显示每个物料的操作结果)
- 管理员可按用户筛选、可删除批次
### 修改文件
4. **`src/main/ipc/cleaner-handler.ts`**
- `CLEANER_RUN` handler 中:执行前插入 execution + order 的 pending 记录,执行后更新结果
- 新增 IPC handlers`CLEANER_HISTORY_BATCHES``CLEANER_HISTORY_DETAILS``CLEANER_HISTORY_DELETE`
5. **`src/main/services/cleaner/cleaner-application-service.ts`**
- `runCleaner` 接收 `batchId` 参数
- 移除 `generateExecutionId()` 函数
- 移除 `generateAndUploadReport()` 方法
- 移除 `executionId` 相关逻辑
- 外层重试时,通过 DAO 写入 AttemptNumber=2 的执行记录和订单记录,不覆盖首次尝试
- 执行完成后回调 DAO 写入订单结果和物料明细
6. **`src/main/ipc/index.ts`**
- 注册新的 cleaner history IPC handlers
7. **`src/preload/api/cleaner.ts`**
- 新增 IPC 调用方法getBatches、getBatchDetails、deleteBatch
8. **`src/preload/index.d.ts`**
- `CleanerAPI` 接口新增 getBatches、getBatchDetails、deleteBatch 类型声明
9. **`src/renderer/src/pages/CleanerPage.tsx`**
- 新增"操作历史"按钮
- 引入 CleanerOperationHistoryModal
### 删除文件
10. **`src/main/services/report/cleaner-report-generator.ts`**
- 整个文件删除,报告生成逻辑不再需要
### 可选清理
11. **`src/renderer/src/components/ReportViewerDialog.tsx`**
- 基于 RustFS 文件的报告查看器Cleaner 不再使用
- 如果 Extractor 不共用此组件,可删除
12. **`src/renderer/src/components/ReportAnalysisDialog.tsx`**
- 基于报告文件的分析Cleaner 不再使用
- 后续可基于数据库重新实现统计分析
## 移除的概念
| 概念 | 原因 |
|------|------|
| ExecutionIdCLN-时间戳-随机) | 为文件名设计,数据库用 UUID |
| generateExecutionId() | 随 ExecutionId 一起移除 |
| CleanerReportGenerator | Markdown 报告生成器,被数据库替代 |
| generateAndUploadReport() | RustFS 上传链路,被数据库写入替代 |
| 报告文件名去重 | 数据库 UUID 天然唯一 |
| 重试覆盖旧报告 | 数据库保留所有尝试记录 |
## 不涉及的部分
- Extractor 的持久化逻辑不变
- 数据库 schema 迁移(需 DBA 创建表,应用层只做 CRUD
- 后续智能跳过功能(基于已有 success 记录跳过已成功的订单)
- 内层重试逻辑(订单级/物料级)不变

View File

@@ -0,0 +1,620 @@
# Cleaner 数据库持久化实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 将 Cleaner 的执行记录从 Markdown 文件持久化迁移到数据库(三张表:执行级、订单级、物料级),并在前端新增操作历史弹窗。
**Architecture:** 新建 `CleanerOperationHistoryDAO` 操作三张表(`ERPAuto.CleanerExecution``ERPAuto.CleanerOrderHistory``ERPAuto.CleanerMaterialDetail`),通过新增 IPC handlers 暴露给前端。执行前写入 pending 记录,执行后更新结果和物料明细。外层重试时新增 AttemptNumber=2 的记录,不覆盖首次尝试。移除 Markdown 报告生成和 RustFS 上传链路。
**Tech Stack:** TypeScript, Electron IPC, SQL (MySQL/SQL Server/PostgreSQL via existing DAO+dialect pattern), React
---
## Task 1: 新增类型定义
**Files:**
- Create: `src/main/types/cleaner-history.types.ts`
**Step 1: 创建类型文件**
```typescript
// src/main/types/cleaner-history.types.ts
/**
* Cleaner 操作历史类型定义
*/
/** 执行级记录 */
export interface CleanerExecutionRecord {
id?: number
batchId: string
attemptNumber: number
userId: number
username: string
operationTime: Date
endTime: Date | null
status: string
isDryRun: boolean
totalOrders: number
ordersProcessed: number
totalMaterialsDeleted: number
totalMaterialsSkipped: number
totalMaterialsFailed: number
totalUncertainDeletions: number
errorMessage: string | null
appVersion: string | null
}
/** 订单级记录 */
export interface CleanerOrderRecord {
id?: number
batchId: string
attemptNumber: number
orderNumber: string
status: string
materialsDeleted: number
materialsSkipped: number
materialsFailed: number
uncertainDeletions: number
retryCount: number
retrySuccess: boolean
errorMessage: string | null
}
/** 物料级记录 */
export interface CleanerMaterialRecord {
id?: number
batchId: string
attemptNumber: number
orderNumber: string
materialCode: string
materialName: string
rowNumber: number
result: string
reason: string | null
attemptCount: number
finalErrorCategory: string | null
}
/** 批次统计(前端列表展示用) */
export interface CleanerBatchStats {
batchId: string
userId: number
username: string
operationTime: string
/** 最终一次尝试的状态 */
status: string
totalAttempts: number
totalOrders: number
ordersProcessed: number
totalMaterialsDeleted: number
totalMaterialsFailed: number
successCount: number
failedCount: number
isDryRun: boolean
}
/** 插入执行记录的输入 */
export interface InsertCleanerExecutionInput {
batchId: string
attemptNumber: number
userId: number
username: string
isDryRun: boolean
totalOrders: number
appVersion: string
}
/** 插入订单记录的输入 */
export interface InsertOrderInput {
orderNumber: string
}
/** 插入物料明细的输入 */
export interface InsertMaterialDetailInput {
orderNumber: string
materialCode: string
materialName: string
rowNumber: number
result: string
reason: string | null
attemptCount: number
finalErrorCategory: string | null
}
/** 查询批次的选项 */
export interface GetCleanerBatchesOptions {
limit?: number
offset?: number
usernames?: string[]
}
```
**Step 2: 验证类型检查通过**
Run: `npm run typecheck`
Expected: PASS新文件不影响现有代码
**Step 3: Commit**
```
feat(cleaner): add type definitions for cleaner operation history
```
---
## Task 2: 新增 DAO 层
**Files:**
- Create: `src/main/services/database/cleaner-operation-history-dao.ts`
**Step 1: 创建 DAO 文件**
参考 `extractor-operation-history-dao.ts` 的模式(`create()` 获取数据库连接、`createDialect()` 处理 SQL 方言、`trackDuration()` 记录耗时)。表名使用 `ERPAuto` schema。
关键方法:
```typescript
export class CleanerOperationHistoryDAO {
private dbService: IDatabaseService | null = null
private dialect: SqlDialect | null = null
// ===== 执行表 =====
private getExecutionTableName(): string {
return this.getDialect().quoteTableName('ERPAuto', 'CleanerExecution')
}
async insertExecution(input: InsertCleanerExecutionInput): Promise<boolean>
async updateExecutionStatus(batchId: string, attemptNumber: number, status: string, ordersProcessed: number, materialsDeleted: number, materialsSkipped: number, materialsFailed: number, uncertainDeletions: number, endTime: Date, errorMessage?: string): Promise<boolean>
// ===== 订单表 =====
private getOrderTableName(): string {
return this.getDialect().quoteTableName('ERPAuto', 'CleanerOrderHistory')
}
async insertOrderRecords(batchId: string, attemptNumber: number, orders: InsertOrderInput[]): Promise<boolean>
async updateOrderStatus(batchId: string, attemptNumber: number, orderNumber: string, status: string, materialsDeleted: number, materialsSkipped: number, materialsFailed: number, uncertainDeletions: number, retryCount: number, retrySuccess: boolean, errorMessage?: string): Promise<boolean>
// ===== 物料表 =====
private getMaterialTableName(): string {
return this.getDialect().quoteTableName('ERPAuto', 'CleanerMaterialDetail')
}
async insertMaterialDetails(batchId: string, attemptNumber: number, details: InsertMaterialDetailInput[]): Promise<boolean>
// ===== 查询 =====
async getBatches(userId?: number, options?: GetCleanerBatchesOptions): Promise<CleanerBatchStats[]>
async getBatchDetails(batchId: string): Promise<{ executions: CleanerExecutionRecord[]; orders: CleanerOrderRecord[] }>
async getMaterialDetails(batchId: string, attemptNumber: number, orderNumber: string): Promise<CleanerMaterialRecord[]>
// ===== 删除 =====
async deleteBatch(batchId: string, requestingUserId: number, isAdmin: boolean): Promise<{ success: boolean; error?: string }>
// ===== 列询执行级记录 =====
async getMaterialDetails(batchId: string, attemptNumber: number, orderNumber: string): Promise<CleanerMaterialRecord[]>
// ===== 删除 =====
async deleteBatch(batchId: string, requestingUserId: number, isAdmin: boolean): Promise<{ success: boolean; error?: string }>
async disconnect(): Promise<void>
}
```
`getBatches` 查询逻辑:
- `GROUP BY BatchId`,取 `MAX(AttemptNumber)` 对应的执行记录状态作为最终状态
- 汇总订单级的 success/failed 计数
- 支持 userId 过滤(普通用户)和 usernames 过滤(管理员)
- 支持分页
`getBatchDetails` 查询逻辑:
- 返回某 BatchId 下所有 execution 记录 + order 记录
- 前端用 attemptNumber 区分不同尝试
每个 INSERT/UPDATE 使用 `trackDuration()` 包裹error handling 与 Extractor DAO 一致。
**Step 2: 验证类型检查通过**
Run: `npm run typecheck`
Expected: PASS
**Step 3: Commit**
```
feat(cleaner): add CleanerOperationHistoryDAO for three-table persistence
```
---
## Task 3: 新增 IPC channels
**Files:**
- Modify: `src/shared/ipc-channels.ts`
**Step 1: 添加 cleaner history channels**
在现有的 `CLEANER_PROGRESS` 之后添加:
```typescript
// Cleaner history
CLEANER_HISTORY_GET_BATCHES: 'cleanerHistory:getBatches',
CLEANER_HISTORY_GET_BATCH_DETAILS: 'cleanerHistory:getBatchDetails',
CLEANER_HISTORY_GET_MATERIAL_DETAILS: 'cleanerHistory:getMaterialDetails',
CLEANER_HISTORY_DELETE_BATCH: 'cleanerHistory:deleteBatch',
```
**Step 2: Commit**
```
feat(cleaner): add IPC channels for cleaner operation history
```
---
## Task 4: 新增 IPC handler
**Files:**
- Create: `src/main/ipc/cleaner-history-handler.ts`
- Modify: `src/main/ipc/index.ts` — 注册新 handler
**Step 1: 创建 cleaner-history-handler.ts**
参考 `operation-history-handler.ts` 的模式。四个 handler
- `CLEANER_HISTORY_GET_BATCHES`获取批次列表Admin 看全部User 看自己的
- `CLEANER_HISTORY_GET_BATCH_DETAILS`:获取某个批次的执行记录和订单记录
- `CLEANER_HISTORY_GET_MATERIAL_DETAILS`:获取某个订单的物料明细
- `CLEANER_HISTORY_DELETE_BATCH`:删除批次,权限校验与 Extractor 一致
```typescript
export function registerCleanerHistoryHandlers(): void {
const dao = new CleanerOperationHistoryDAO()
ipcMain.handle(
IPC_CHANNELS.CLEANER_HISTORY_GET_BATCHES,
async (event, options?: GetCleanerBatchesOptions): Promise<IpcResult<CleanerBatchStats[]>> => {
return withErrorHandling(async () => {
const currentUser = SessionManager.getInstance().getUserInfo()
if (!currentUser) throw new Error('用户未登录')
const userId = currentUser.userType === 'Admin' ? undefined : currentUser.id
return dao.getBatches(userId, options)
}, 'cleanerHistory:getBatches')
}
)
ipcMain.handle(
IPC_CHANNELS.CLEANER_HISTORY_GET_BATCH_DETAILS,
async (event, batchId: string): Promise<IpcResult<{ executions: CleanerExecutionRecord[]; orders: CleanerOrderRecord[] }>> => {
// ... 与 operation-history-handler 的 getBatchDetails 模式一致
}
)
ipcMain.handle(
IPC_CHANNELS.CLEANER_HISTORY_GET_MATERIAL_DETAILS,
async (event, batchId: string, attemptNumber: number, orderNumber: string): Promise<IpcResult<CleanerMaterialRecord[]>> => {
// ...
}
)
ipcMain.handle(
IPC_CHANNELS.CLEANER_HISTORY_DELETE_BATCH,
async (event, batchId: string): Promise<IpcResult<{ deleted: boolean }>> => {
// ... 权限校验后删除三张表的记录
}
)
}
```
**Step 2: 在 index.ts 中注册**
`registerIpcHandlers()` 中添加 `registerCleanerHistoryHandlers()` 调用,并在顶部添加 import。
**Step 3: 验证类型检查通过**
Run: `npm run typecheck`
Expected: PASS
**Step 4: Commit**
```
feat(cleaner): add IPC handlers for cleaner operation history
```
---
## Task 5: 新增 Preload API
**Files:**
- Modify: `src/preload/api/cleaner.ts` — 新增 history 方法
- Modify: `src/preload/index.d.ts` — 新增类型声明
**Step 1: 在 cleaner.ts 中新增 history 方法**
```typescript
import type {
CleanerBatchStats,
CleanerExecutionRecord,
CleanerOrderRecord,
CleanerMaterialRecord,
GetCleanerBatchesOptions
} from '../../main/types/cleaner-history.types'
// 在 cleanerApi 对象中追加:
getHistoryBatches: (options?: GetCleanerBatchesOptions): Promise<IpcResult<CleanerBatchStats[]>> =>
invokeIpc(IPC_CHANNELS.CLEANER_HISTORY_GET_BATCHES, options),
getHistoryBatchDetails: (batchId: string): Promise<IpcResult<{
executions: CleanerExecutionRecord[]
orders: CleanerOrderRecord[]
}>> =>
invokeIpc(IPC_CHANNELS.CLEANER_HISTORY_GET_BATCH_DETAILS, batchId),
getHistoryMaterialDetails: (batchId: string, attemptNumber: number, orderNumber: string): Promise<IpcResult<CleanerMaterialRecord[]>> =>
invokeIpc(IPC_CHANNELS.CLEANER_HISTORY_GET_MATERIAL_DETAILS, batchId, attemptNumber, orderNumber),
deleteHistoryBatch: (batchId: string): Promise<IpcResult<{ deleted: boolean }>> =>
invokeIpc(IPC_CHANNELS.CLEANER_HISTORY_DELETE_BATCH, batchId),
```
**Step 2: 在 index.d.ts 中更新 CleanerAPI 接口**
`CleanerAPI` 接口中添加对应的类型声明,与实际 API 对齐。
**Step 3: 验证类型检查通过**
Run: `npm run typecheck`
Expected: PASS
**Step 4: Commit**
```
feat(cleaner): add preload API for cleaner operation history
```
---
## Task 6: 改造 CleanerApplicationService — 写入数据库记录
**Files:**
- Modify: `src/main/services/cleaner/cleaner-application-service.ts`
这是核心变更。`runCleaner` 方法需要:
**Step 1: 修改 runCleaner 签名,接收 batchId 和 DAO**
```typescript
async runCleaner(
eventSender: WebContents,
input: CleanerInput,
batchId: string,
historyDao: CleanerOperationHistoryDAO
): Promise<CleanerResult>
```
**Step 2: 移除报告相关代码**
- 删除 `import { app } from 'electron'`(仅用于 `app.getVersion()`
- 删除 `generateExecutionId()` 函数
- 删除 `generateAndUploadReport()` 方法
- 删除所有 `executionId` 相关变量和日志
**Step 3: 插入 pending 订单记录**
在登录成功后、执行清理前,调用 `historyDao.insertOrderRecords(batchId, 1, orders)` 写入 pending 状态的订单记录。
**Step 4: 执行后更新订单记录和写入物料明细**
清理完成后遍历 `result.details``OrderCleanDetail[]`),对每个订单:
- 调用 `historyDao.updateOrderStatus(...)` 更新订单结果
- 调用 `historyDao.insertMaterialDetails(...)` 写入物料明细skipped + failed 材料全部写入)
**Step 5: 更新执行记录状态**
调用 `historyDao.updateExecutionStatus(batchId, 1, ...)` 更新为最终状态。
**Step 6: 外层重试改造**
`result.crashed` 时:
1. 调用 `historyDao.updateExecutionStatus(batchId, 1, 'crashed', ...)` 标记首次尝试为 crashed
2. 调用 `historyDao.insertExecution({ batchId, attemptNumber: 2, ... })` 创建第二次尝试
3. 调用 `historyDao.insertOrderRecords(batchId, 2, orders)` 写入第二次尝试的 pending 订单
4. 重新登录并执行
5. 执行后更新 AttemptNumber=2 的订单和物料记录
**Step 7: 验证类型检查通过**
Run: `npm run typecheck`
Expected: PASS
**Step 8: Commit**
```
refactor(cleaner): replace report generation with database persistence
```
---
## Task 7: 改造 cleaner-handler.ts — 执行前后写入
**Files:**
- Modify: `src/main/ipc/cleaner-handler.ts`
**Step 1: 修改 CLEANER_RUN handler**
在调用 `cleanerService.runCleaner()` 之前:
1. 获取当前用户信息
2. `batchId = randomUUID()`
3. 创建 `CleanerOperationHistoryDAO` 实例
4. 调用 `dao.insertExecution({ batchId, attemptNumber: 1, userId, username, isDryRun, totalOrders, appVersion })`
`batchId``dao` 传入 `runCleaner()`
执行完成后(无论成功失败),更新执行记录的最终状态。
**Step 2: 移除 app.getVersion() 调用**
`appVersion` 改为在 handler 层获取(因为 handler 已有 electron 访问权限),传给 DAO。
**Step 3: 验证类型检查通过**
Run: `npm run typecheck`
Expected: PASS
**Step 4: Commit**
```
refactor(cleaner): write execution records to database in IPC handler
```
---
## Task 8: 删除 Markdown 报告生成器
**Files:**
- Delete: `src/main/services/report/cleaner-report-generator.ts`
**Step 1: 删除文件**
删除 `cleaner-report-generator.ts`
**Step 2: 检查是否有其他文件引用它**
搜索 `cleaner-report-generator``CleanerReportGenerator`,如有引用则一并移除(主要是 `cleaner-application-service.ts` 中已删除的 import
**Step 3: 验证编译通过**
Run: `npm run typecheck`
Expected: PASS
**Step 4: Commit**
```
refactor(cleaner): remove Markdown report generator
```
---
## Task 9: 前端 — 新增操作历史弹窗
**Files:**
- Create: `src/renderer/src/components/CleanerOperationHistoryModal.tsx`
- Modify: `src/renderer/src/pages/CleanerPage.tsx`
**Step 1: 创建 CleanerOperationHistoryModal**
参考 `ExtractorOperationHistoryModal.tsx` 的 UI 模式和代码结构。关键差异:
- 数据源使用 `window.electron.cleaner.getHistoryBatches()` 等新 API
- 批次列表增加"尝试次数"列和"模拟运行"标识
- 展开明细时顶部显示执行级信息尝试次数、crashed 状态等)
- 订单表格增加 deleted/skipped/failed/uncertain 列
- 订单行可再次展开查看物料明细(调用 `getHistoryMaterialDetails`
- 管理员按用户筛选、删除功能与 Extractor 一致
**Step 2: 在 CleanerPage 中添加"操作历史"按钮和弹窗**
-`CleanerToolbar` 中添加"操作历史"按钮(或直接在 CleanerPage 添加)
- 引入 `CleanerOperationHistoryModal` 组件
- 传入 `user``isOpen/onClose` 控制
**Step 3: 验证类型检查通过**
Run: `npm run typecheck`
Expected: PASS
**Step 4: Commit**
```
feat(cleaner): add operation history modal with database-backed records
```
---
## Task 10: 更新 renderer 类型定义
**Files:**
- Modify: `src/renderer/src/hooks/cleaner/types.ts`
**Step 1: 添加 history 相关类型**
在 types.ts 中添加前端需要的类型(或直接从 `cleaner-history.types.ts` import根据项目的前端类型引用模式决定
**Step 2: 验证类型检查通过**
Run: `npm run typecheck`
Expected: PASS
**Step 3: Commit**
```
feat(cleaner): add renderer types for cleaner operation history
```
---
## Task 11: 清理旧代码
**Files:**
- Modify: `src/renderer/src/hooks/cleaner/types.ts` — 移除 `CleanerReportData.crashed`(如果不再需要)
- 检查 `ReportViewerDialog.tsx``ReportAnalysisDialog.tsx` 是否仍被 Cleaner 使用
**Step 1: 清理 renderer 中不再需要的类型**
- `CleanerReportData` 中如果 `crashed` 字段已无用,移除
- 确认 `CleanerPhase``'retry'` 值是否仍需要(前端进度通知仍在使用,保留)
**Step 2: 评估 ReportViewerDialog 和 ReportAnalysisDialog**
这两个组件目前用于查看 Markdown 报告文件。如果 Cleaner 不再使用它们:
- 在 CleanerPage 中移除相关按钮和引用
- 不删除组件本身Extractor 可能仍在使用,后续统一清理)
**Step 3: 验证编译和类型检查通过**
Run: `npm run typecheck && npm run lint`
Expected: PASS
**Step 4: Commit**
```
chore(cleaner): clean up legacy report-related code
```
---
## Task 12: 集成测试
**Step 1: 运行完整类型检查**
Run: `npm run typecheck`
Expected: PASS
**Step 2: 运行 lint**
Run: `npm run lint`
Expected: PASS
**Step 3: 运行单元测试**
Run: `npm run test`
Expected: PASS
**Step 4: 手动验证**
1. 启动 `npm run dev`
2. 在 Cleaner 页面执行一次清理(模拟运行)
3. 检查数据库三张表是否正确写入
4. 点击"操作历史"按钮,验证批次列表和详情展示
5. 模拟崩溃场景(如果可以),验证外层重试写入 AttemptNumber=2 的记录
6. 用管理员账号验证用户筛选和删除功能
---
## 执行顺序
```
Task 1 (types) → Task 2 (DAO) → Task 3 (IPC channels) → Task 4 (IPC handler)
→ Task 5 (preload) → Task 6 (CleanerApplicationService) → Task 7 (cleaner-handler)
→ Task 8 (删除报告生成器) → Task 10 (renderer types) → Task 9 (前端弹窗)
→ Task 11 (清理) → Task 12 (集成测试)
```
Task 9 和 Task 10 可以并行。Task 8 必须在 Task 6、7 之后。