chore: run pretier format across project
- Format TypeScript source files - Format documentation files - Update eslint config formatting
This commit is contained in:
@@ -13,14 +13,14 @@ Extractor 已有成熟的数据库持久化模式(`ExtractorOperationHistory`
|
||||
|
||||
## 设计决策
|
||||
|
||||
| 决策项 | 选择 | 理由 |
|
||||
|--------|------|------|
|
||||
| 表结构 | 独立建表,不与 Extractor 共用 | Cleaner 数据结构差异大(双层、物料级详情),独立更清晰 |
|
||||
| 记录粒度 | 执行 + 订单 + 物料三层 | 执行表存全局信息,订单表存订单汇总,物料表存操作明细 |
|
||||
| 批次标识 | `BatchId`(UUID),与 Extractor 一致 | 标准、简洁,不需要嵌入时间戳 |
|
||||
| 重试记录 | 不覆盖,每次尝试独立写入,用 `AttemptNumber` 区分 | 保留完整审计链,为后续智能跳过提供数据基础 |
|
||||
| 报告文件 | 移除 Markdown 报告和 RustFS 上传 | 数据库完全替代,报告相关代码(CleanerReportGenerator、generateAndUploadReport)删除 |
|
||||
| 前端历史 | 独立 CleanerOperationHistoryModal,复用 Extractor 的 UI 模式 | 放在 CleanerPage 上,与 Extractor 的"操作历史"按钮对齐 |
|
||||
| 决策项 | 选择 | 理由 |
|
||||
| -------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
|
||||
| 表结构 | 独立建表,不与 Extractor 共用 | Cleaner 数据结构差异大(双层、物料级详情),独立更清晰 |
|
||||
| 记录粒度 | 执行 + 订单 + 物料三层 | 执行表存全局信息,订单表存订单汇总,物料表存操作明细 |
|
||||
| 批次标识 | `BatchId`(UUID),与 Extractor 一致 | 标准、简洁,不需要嵌入时间戳 |
|
||||
| 重试记录 | 不覆盖,每次尝试独立写入,用 `AttemptNumber` 区分 | 保留完整审计链,为后续智能跳过提供数据基础 |
|
||||
| 报告文件 | 移除 Markdown 报告和 RustFS 上传 | 数据库完全替代,报告相关代码(CleanerReportGenerator、generateAndUploadReport)删除 |
|
||||
| 前端历史 | 独立 CleanerOperationHistoryModal,复用 Extractor 的 UI 模式 | 放在 CleanerPage 上,与 Extractor 的"操作历史"按钮对齐 |
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
@@ -32,25 +32,25 @@ Extractor 已有成熟的数据库持久化模式(`ExtractorOperationHistory`
|
||||
|
||||
一次清理操作(含重试)的全局信息。每次尝试一行记录。
|
||||
|
||||
| 列名 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| 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) | 应用版本号 |
|
||||
| 列名 | 类型 | 说明 |
|
||||
| ----------------------- | ---------------- | ---------------------------------------------- |
|
||||
| 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`(订单级)
|
||||
|
||||
@@ -58,20 +58,20 @@ Extractor 已有成熟的数据库持久化模式(`ExtractorOperationHistory`
|
||||
|
||||
每个订单在每次尝试中的执行结果。每个订单每次尝试一行记录。
|
||||
|
||||
| 列名 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| 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) | 错误信息 |
|
||||
| 列名 | 类型 | 说明 |
|
||||
| ------------------ | ---------------- | -------------------------- |
|
||||
| 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` 关联执行表。
|
||||
|
||||
@@ -81,19 +81,19 @@ Extractor 已有成熟的数据库持久化模式(`ExtractorOperationHistory`
|
||||
|
||||
每个物料在每次尝试中的操作明细。
|
||||
|
||||
| 列名 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| 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) | 最终错误分类 |
|
||||
| 列名 | 类型 | 说明 |
|
||||
| ------------------ | ---------------- | -------------------------------------- |
|
||||
| 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` 关联订单表。
|
||||
|
||||
@@ -102,18 +102,21 @@ Extractor 已有成熟的数据库持久化模式(`ExtractorOperationHistory`
|
||||
首次执行到第 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
|
||||
@@ -121,6 +124,7 @@ 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=不可删除
|
||||
@@ -218,14 +222,14 @@ BatchId=uuid-1, Attempt=2, Order=SC080, Material=MAT-002, Result=skipped, Reason
|
||||
|
||||
## 移除的概念
|
||||
|
||||
| 概念 | 原因 |
|
||||
|------|------|
|
||||
| ExecutionId(CLN-时间戳-随机) | 为文件名设计,数据库用 UUID |
|
||||
| generateExecutionId() | 随 ExecutionId 一起移除 |
|
||||
| CleanerReportGenerator | Markdown 报告生成器,被数据库替代 |
|
||||
| generateAndUploadReport() | RustFS 上传链路,被数据库写入替代 |
|
||||
| 报告文件名去重 | 数据库 UUID 天然唯一 |
|
||||
| 重试覆盖旧报告 | 数据库保留所有尝试记录 |
|
||||
| 概念 | 原因 |
|
||||
| ------------------------------ | --------------------------------- |
|
||||
| ExecutionId(CLN-时间戳-随机) | 为文件名设计,数据库用 UUID |
|
||||
| generateExecutionId() | 随 ExecutionId 一起移除 |
|
||||
| CleanerReportGenerator | Markdown 报告生成器,被数据库替代 |
|
||||
| generateAndUploadReport() | RustFS 上传链路,被数据库写入替代 |
|
||||
| 报告文件名去重 | 数据库 UUID 天然唯一 |
|
||||
| 重试覆盖旧报告 | 数据库保留所有尝试记录 |
|
||||
|
||||
## 不涉及的部分
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
## Task 1: 新增类型定义
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `src/main/types/cleaner-history.types.ts`
|
||||
|
||||
**Step 1: 创建类型文件**
|
||||
@@ -146,6 +147,7 @@ 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 文件**
|
||||
@@ -165,47 +167,101 @@ export class CleanerOperationHistoryDAO {
|
||||
}
|
||||
|
||||
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>
|
||||
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>
|
||||
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 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 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 deleteBatch(
|
||||
batchId: string,
|
||||
requestingUserId: number,
|
||||
isAdmin: boolean
|
||||
): Promise<{ success: boolean; error?: string }>
|
||||
|
||||
// ===== 列询执行级记录 =====
|
||||
async getMaterialDetails(batchId: string, attemptNumber: number, orderNumber: string): Promise<CleanerMaterialRecord[]>
|
||||
async getMaterialDetails(
|
||||
batchId: string,
|
||||
attemptNumber: number,
|
||||
orderNumber: string
|
||||
): Promise<CleanerMaterialRecord[]>
|
||||
|
||||
// ===== 删除 =====
|
||||
async deleteBatch(batchId: string, requestingUserId: number, isAdmin: boolean): Promise<{ success: boolean; error?: string }>
|
||||
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 区分不同尝试
|
||||
|
||||
@@ -227,6 +283,7 @@ feat(cleaner): add CleanerOperationHistoryDAO for three-table persistence
|
||||
## Task 3: 新增 IPC channels
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/shared/ipc-channels.ts`
|
||||
|
||||
**Step 1: 添加 cleaner history channels**
|
||||
@@ -252,6 +309,7 @@ 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
|
||||
|
||||
@@ -282,14 +340,24 @@ export function registerCleanerHistoryHandlers(): void {
|
||||
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.CLEANER_HISTORY_GET_BATCH_DETAILS,
|
||||
async (event, batchId: string): Promise<IpcResult<{ executions: CleanerExecutionRecord[]; orders: CleanerOrderRecord[] }>> => {
|
||||
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[]>> => {
|
||||
async (
|
||||
event,
|
||||
batchId: string,
|
||||
attemptNumber: number,
|
||||
orderNumber: string
|
||||
): Promise<IpcResult<CleanerMaterialRecord[]>> => {
|
||||
// ...
|
||||
}
|
||||
)
|
||||
@@ -323,6 +391,7 @@ 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` — 新增类型声明
|
||||
|
||||
@@ -374,6 +443,7 @@ feat(cleaner): add preload API for cleaner operation history
|
||||
## Task 6: 改造 CleanerApplicationService — 写入数据库记录
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/main/services/cleaner/cleaner-application-service.ts`
|
||||
|
||||
这是核心变更。`runCleaner` 方法需要:
|
||||
@@ -403,6 +473,7 @@ async runCleaner(
|
||||
**Step 4: 执行后更新订单记录和写入物料明细**
|
||||
|
||||
清理完成后遍历 `result.details`(`OrderCleanDetail[]`),对每个订单:
|
||||
|
||||
- 调用 `historyDao.updateOrderStatus(...)` 更新订单结果
|
||||
- 调用 `historyDao.insertMaterialDetails(...)` 写入物料明细(skipped + failed 材料全部写入)
|
||||
|
||||
@@ -413,6 +484,7 @@ async runCleaner(
|
||||
**Step 6: 外层重试改造**
|
||||
|
||||
当 `result.crashed` 时:
|
||||
|
||||
1. 调用 `historyDao.updateExecutionStatus(batchId, 1, 'crashed', ...)` 标记首次尝试为 crashed
|
||||
2. 调用 `historyDao.insertExecution({ batchId, attemptNumber: 2, ... })` 创建第二次尝试
|
||||
3. 调用 `historyDao.insertOrderRecords(batchId, 2, orders)` 写入第二次尝试的 pending 订单
|
||||
@@ -435,11 +507,13 @@ 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` 实例
|
||||
@@ -469,6 +543,7 @@ 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: 删除文件**
|
||||
@@ -495,6 +570,7 @@ refactor(cleaner): remove Markdown report generator
|
||||
## Task 9: 前端 — 新增操作历史弹窗
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `src/renderer/src/components/CleanerOperationHistoryModal.tsx`
|
||||
- Modify: `src/renderer/src/pages/CleanerPage.tsx`
|
||||
|
||||
@@ -531,6 +607,7 @@ 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 相关类型**
|
||||
@@ -553,6 +630,7 @@ 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 使用
|
||||
|
||||
@@ -564,6 +642,7 @@ feat(cleaner): add renderer types for cleaner operation history
|
||||
**Step 2: 评估 ReportViewerDialog 和 ReportAnalysisDialog**
|
||||
|
||||
这两个组件目前用于查看 Markdown 报告文件。如果 Cleaner 不再使用它们:
|
||||
|
||||
- 在 CleanerPage 中移除相关按钮和引用
|
||||
- 不删除组件本身(Extractor 可能仍在使用,后续统一清理)
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
|
||||
## 设计决策
|
||||
|
||||
| 决策项 | 选择 | 理由 |
|
||||
|---|---|---|
|
||||
| 重试层级 | CleanerApplicationService | 崩溃后浏览器不可用,必须重新登录 |
|
||||
| 重试范围 | 全部订单重新跑 | 简单可靠,物料删除是幂等操作 |
|
||||
| 最大重试次数 | 1 次 | 覆盖瞬态故障,不过度消耗时间 |
|
||||
| 触发条件 | result.crashed === true | 仅 outer catch 触发时才重试 |
|
||||
| 报告去重 | 执行 ID | 用户点击执行时生成,重试不变 |
|
||||
| 决策项 | 选择 | 理由 |
|
||||
| ------------ | ------------------------- | -------------------------------- |
|
||||
| 重试层级 | CleanerApplicationService | 崩溃后浏览器不可用,必须重新登录 |
|
||||
| 重试范围 | 全部订单重新跑 | 简单可靠,物料删除是幂等操作 |
|
||||
| 最大重试次数 | 1 次 | 覆盖瞬态故障,不过度消耗时间 |
|
||||
| 触发条件 | result.crashed === true | 仅 outer catch 触发时才重试 |
|
||||
| 报告去重 | 执行 ID | 用户点击执行时生成,重试不变 |
|
||||
|
||||
## 变更清单
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
```typescript
|
||||
export interface CleanerResult {
|
||||
// ... 现有字段
|
||||
crashed?: boolean // true = outer catch triggered, 流程级崩溃
|
||||
crashed?: boolean // true = outer catch triggered, 流程级崩溃
|
||||
}
|
||||
```
|
||||
|
||||
@@ -87,6 +87,7 @@ runCleaner(eventSender, input) {
|
||||
生成时机: `runCleaner()` 入口处,在 ERP 登录之前。重试时同一个 executionId 不变。
|
||||
|
||||
用途:
|
||||
|
||||
- 报告文件名: `cleaner-report-CLN-20260410112930-A7FK.md`
|
||||
- RustFS 存储路径中包含该 ID,重试时覆盖同一文件
|
||||
- 报告内容中显示该 ID
|
||||
@@ -98,13 +99,13 @@ runCleaner(eventSender, input) {
|
||||
在执行摘要表格中新增字段:
|
||||
|
||||
```markdown
|
||||
| 项目 | 值 |
|
||||
| ---------------- | --------------------------------- |
|
||||
| **执行 ID** | `CLN-20260410112930-A7FK` | ← 新增
|
||||
| **应用版本** | `1.11.1` | ← 新增
|
||||
| **执行时间** | `2026-04-10 11:29:30` |
|
||||
| **执行模式** | `正式执行` |
|
||||
| ... | ... |
|
||||
| 项目 | 值 |
|
||||
| ------------ | ------------------------- | ------ |
|
||||
| **执行 ID** | `CLN-20260410112930-A7FK` | ← 新增 |
|
||||
| **应用版本** | `1.11.1` | ← 新增 |
|
||||
| **执行时间** | `2026-04-10 11:29:30` |
|
||||
| **执行模式** | `正式执行` |
|
||||
| ... | ... |
|
||||
```
|
||||
|
||||
- **执行 ID**: 从 ReportOptions 传入
|
||||
@@ -118,8 +119,8 @@ export interface ReportOptions {
|
||||
username: string
|
||||
startTime: number
|
||||
endTime: number
|
||||
executionId: string // ← 新增
|
||||
appVersion: string // ← 新增
|
||||
executionId: string // ← 新增
|
||||
appVersion: string // ← 新增
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -484,11 +484,7 @@ export class CleanerApplicationService {
|
||||
batchId,
|
||||
attemptNumber,
|
||||
detail.orderNumber,
|
||||
detail.notFound
|
||||
? 'erp_not_found'
|
||||
: detail.errors.length > 0
|
||||
? 'failed'
|
||||
: 'success',
|
||||
detail.notFound ? 'erp_not_found' : detail.errors.length > 0 ? 'failed' : 'success',
|
||||
detail.materialsDeleted,
|
||||
detail.materialsSkipped,
|
||||
detail.materialsFailed,
|
||||
|
||||
@@ -26,8 +26,12 @@ interface RetryResult {
|
||||
}
|
||||
|
||||
interface ProgressState {
|
||||
completedOrders: number
|
||||
ordersStarted: number // 开始处理的订单数
|
||||
ordersCompleted: number // 已完成订单数
|
||||
totalOrders: number
|
||||
progressText?: string // "ordersCompleted/totalOrders (xx%)"
|
||||
lastCompletedOrder?: string // 最后完成的订单号
|
||||
lastActivityTime?: number // 最后活动时间戳(健康检查用)
|
||||
}
|
||||
|
||||
interface QueryResultRow {
|
||||
@@ -56,6 +60,48 @@ class AsyncMutex {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 并发追踪器 - 监控 worker 状态和 mutex 等待情况
|
||||
*/
|
||||
class ConcurrencyTracker {
|
||||
private activeWorkers = 0
|
||||
private waitQueue = 0
|
||||
private mutexWaitCount = 0
|
||||
|
||||
workerStarted() {
|
||||
this.activeWorkers++
|
||||
log.verbose('[CONCURRENCY] Worker started', {
|
||||
activeWorkers: this.activeWorkers,
|
||||
waitQueue: this.waitQueue,
|
||||
waitingForPopupMutex: this.mutexWaitCount > 0
|
||||
})
|
||||
}
|
||||
|
||||
workerCompleted() {
|
||||
this.activeWorkers--
|
||||
log.verbose('[CONCURRENCY] Worker completed', {
|
||||
activeWorkers: this.activeWorkers,
|
||||
queueRemaining: this.waitQueue
|
||||
})
|
||||
}
|
||||
|
||||
waitingForMutex() {
|
||||
this.mutexWaitCount++
|
||||
log.warn('[CONCURRENCY] Worker waiting for popup mutex', {
|
||||
mutexWaitCount: this.mutexWaitCount,
|
||||
activeWorkers: this.activeWorkers
|
||||
})
|
||||
}
|
||||
|
||||
acquiredMutex() {
|
||||
this.mutexWaitCount--
|
||||
log.verbose('[CONCURRENCY] Worker acquired popup mutex', {
|
||||
mutexWaitCount: this.mutexWaitCount,
|
||||
activeWorkers: this.activeWorkers
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleaner Service Options
|
||||
*/
|
||||
@@ -242,7 +288,8 @@ export class CleanerService {
|
||||
const orderBatches = createBatches(input.orderNumbers, queryBatchSize)
|
||||
const popupMutex = new AsyncMutex()
|
||||
const progressState: ProgressState = {
|
||||
completedOrders: 0,
|
||||
ordersStarted: 0,
|
||||
ordersCompleted: 0,
|
||||
totalOrders
|
||||
}
|
||||
|
||||
@@ -257,58 +304,131 @@ export class CleanerService {
|
||||
totalMaterials
|
||||
})
|
||||
|
||||
// [新增] 健康检查定时器 - 检测长时间无进展
|
||||
let lastActivityTime = Date.now()
|
||||
progressState.lastActivityTime = lastActivityTime
|
||||
const healthCheckInterval = setInterval(() => {
|
||||
const secondsSinceLastActivity = (Date.now() - lastActivityTime) / 1000
|
||||
|
||||
if (secondsSinceLastActivity > 60) {
|
||||
log.warn('[HEALTH_CHECK] 长时间无进展', {
|
||||
ordersCompleted: progressState.ordersCompleted,
|
||||
totalOrders: progressState.totalOrders,
|
||||
lastCompletedOrder: progressState.lastCompletedOrder,
|
||||
noProgressSeconds: secondsSinceLastActivity,
|
||||
suspectedStuck: secondsSinceLastActivity > 180,
|
||||
healthStatus: secondsSinceLastActivity > 180 ? 'critical' : 'warning'
|
||||
})
|
||||
}
|
||||
}, 30000) // 每 30 秒检查一次
|
||||
|
||||
// [新增] 为每个 batch 创建并发追踪器
|
||||
const tracker = new ConcurrencyTracker()
|
||||
|
||||
// Track batch processing duration with 5s slow threshold
|
||||
await trackDuration(
|
||||
async () => {
|
||||
await this.queryOrders(workFrame, batchOrders)
|
||||
await this.waitForLoading(workFrame)
|
||||
|
||||
const queriedRows = await this.collectQueryResultRows(workFrame)
|
||||
const queriedOrderNumbersInBatch = new Set(queriedRows.map((row) => row.orderNumber))
|
||||
|
||||
await runWithConcurrency(queriedRows, processConcurrency, async (row) => {
|
||||
const { rowIndex, orderNumber } = row
|
||||
const openedDetailPage = await popupMutex.runExclusive(async () => {
|
||||
return await this.openDetailPageFromRow(workFrame, popupPage!, rowIndex)
|
||||
})
|
||||
|
||||
let detail: OrderCleanDetail
|
||||
try {
|
||||
detail = await this.processDetailPage({
|
||||
detailPage: openedDetailPage,
|
||||
deleteSet,
|
||||
dryRun,
|
||||
expectedOrderNumber: orderNumber,
|
||||
progressState,
|
||||
onProgress: input.onProgress
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
detail = this.createErrorDetail(orderNumber, message)
|
||||
} finally {
|
||||
progressState.completedOrders += 1
|
||||
}
|
||||
|
||||
result.details.push(detail)
|
||||
|
||||
if (detail.errors.length > 0) {
|
||||
result.errors.push(`Order ${detail.orderNumber}: ${detail.errors.join('; ')}`)
|
||||
return
|
||||
}
|
||||
|
||||
result.ordersProcessed += 1
|
||||
result.materialsDeleted += detail.materialsDeleted
|
||||
result.materialsSkipped += detail.materialsSkipped
|
||||
result.materialsFailed += detail.materialsFailed
|
||||
result.uncertainDeletions += detail.uncertainDeletions
|
||||
// Phase 1: Query orders
|
||||
await trackDuration(async () => await this.queryOrders(workFrame, batchOrders), {
|
||||
operationName: 'query',
|
||||
message: '执行订单查询',
|
||||
slowThresholdMs: 3000,
|
||||
context: { orderCount: batchOrders.length }
|
||||
})
|
||||
|
||||
const missingOrders = getMissingOrders(batchOrders, queriedOrderNumbersInBatch)
|
||||
for (const missingOrder of missingOrders) {
|
||||
const missingMessage = '订单未出现在查询结果中'
|
||||
result.errors.push(`Order ${missingOrder}: ${missingMessage}`)
|
||||
result.details.push(this.createErrorDetail(missingOrder, missingMessage, true))
|
||||
}
|
||||
// Phase 2: Wait for loading complete
|
||||
await trackDuration(async () => await this.waitForLoading(workFrame), {
|
||||
operationName: 'wait_loading',
|
||||
message: '等待加载完成',
|
||||
slowThresholdMs: 5000
|
||||
})
|
||||
|
||||
// Phase 3: Collect query results
|
||||
const collectResult = await trackDuration(
|
||||
async () => await this.collectQueryResultRows(workFrame),
|
||||
{
|
||||
operationName: 'collect_results',
|
||||
message: '收集查询结果',
|
||||
slowThresholdMs: 2000
|
||||
}
|
||||
)
|
||||
const queriedRows = collectResult.result
|
||||
const queriedOrderNumbersInBatch = new Set(queriedRows.map((row) => row.orderNumber))
|
||||
|
||||
// Phase 4: Process all orders in batch
|
||||
await trackDuration(
|
||||
async () => {
|
||||
await runWithConcurrency(queriedRows, processConcurrency, async (row) => {
|
||||
const { rowIndex, orderNumber } = row
|
||||
|
||||
// [新增] Worker 开始追踪
|
||||
tracker.workerStarted()
|
||||
|
||||
try {
|
||||
const openedDetailPage = await popupMutex.runExclusive(async () => {
|
||||
// [新增] Mutex 等待追踪
|
||||
tracker.waitingForMutex()
|
||||
const page = await this.openDetailPageFromRow(workFrame, popupPage!, rowIndex)
|
||||
// [新增] Mutex 获取追踪
|
||||
tracker.acquiredMutex()
|
||||
return page
|
||||
})
|
||||
|
||||
let detail: OrderCleanDetail
|
||||
try {
|
||||
detail = await this.processDetailPage({
|
||||
detailPage: openedDetailPage,
|
||||
deleteSet,
|
||||
dryRun,
|
||||
expectedOrderNumber: orderNumber,
|
||||
progressState,
|
||||
onProgress: input.onProgress
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
detail = this.createErrorDetail(orderNumber, message)
|
||||
} finally {
|
||||
progressState.ordersStarted += 1
|
||||
progressState.ordersCompleted += 1
|
||||
progressState.lastCompletedOrder = orderNumber
|
||||
lastActivityTime = Date.now() // [新增] 健康检查:更新活动时间
|
||||
}
|
||||
|
||||
result.details.push(detail)
|
||||
|
||||
if (detail.errors.length > 0) {
|
||||
result.errors.push(`Order ${detail.orderNumber}: ${detail.errors.join('; ')}`)
|
||||
return
|
||||
}
|
||||
|
||||
result.ordersProcessed += 1
|
||||
result.materialsDeleted += detail.materialsDeleted
|
||||
result.materialsSkipped += detail.materialsSkipped
|
||||
result.materialsFailed += detail.materialsFailed
|
||||
result.uncertainDeletions += detail.uncertainDeletions
|
||||
} finally {
|
||||
// [新增] Worker 完成追踪
|
||||
tracker.workerCompleted()
|
||||
}
|
||||
})
|
||||
|
||||
// Handle missing orders
|
||||
const missingOrders = getMissingOrders(batchOrders, queriedOrderNumbersInBatch)
|
||||
for (const missingOrder of missingOrders) {
|
||||
const missingMessage = '订单未出现在查询结果中'
|
||||
result.errors.push(`Order ${missingOrder}: ${missingMessage}`)
|
||||
result.details.push(this.createErrorDetail(missingOrder, missingMessage, true))
|
||||
}
|
||||
},
|
||||
{
|
||||
operationName: 'process_all_orders_in_batch',
|
||||
message: `处理批次中所有${queriedRows.length}个订单`,
|
||||
slowThresholdMs: 10000
|
||||
}
|
||||
)
|
||||
|
||||
// Phase 5: Save batch results (already included in process_all_orders_in_batch)
|
||||
// No separate save step needed as results are accumulated in result object
|
||||
},
|
||||
{
|
||||
operationName: `batch-${batchIndex + 1}-${orderBatches[batchIndex].length}-orders`,
|
||||
@@ -323,6 +443,9 @@ export class CleanerService {
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// [新增] 清理健康检查定时器
|
||||
clearInterval(healthCheckInterval)
|
||||
}
|
||||
|
||||
const retryResult = await this.retryFailedOrders({
|
||||
@@ -381,7 +504,16 @@ export class CleanerService {
|
||||
dryRun,
|
||||
orderNumbers: input.orderNumbers,
|
||||
materialCodes: input.materialCodes,
|
||||
...(popupPage ? await capturePageContext(popupPage, undefined, 'cleaner.outerCatch') : {})
|
||||
...(popupPage
|
||||
? await capturePageContext(
|
||||
popupPage,
|
||||
undefined,
|
||||
'cleaner.outerCatch',
|
||||
undefined,
|
||||
undefined,
|
||||
'outer_catch'
|
||||
)
|
||||
: {})
|
||||
})
|
||||
result.errors.push(`Clean failed: ${message}`)
|
||||
result.crashed = true
|
||||
@@ -438,7 +570,14 @@ export class CleanerService {
|
||||
log.error('[导航失败] forwardFrame 为空', {
|
||||
elapsedMs: Date.now() - navStartTime,
|
||||
pageUrl: popupPage.url(),
|
||||
contextData: await capturePageContext(popupPage, undefined, 'nav.forwardFrame')
|
||||
contextData: await capturePageContext(
|
||||
popupPage,
|
||||
undefined,
|
||||
'nav.forwardFrame',
|
||||
undefined,
|
||||
undefined,
|
||||
'nav_forward_frame'
|
||||
)
|
||||
})
|
||||
throw new Error('无法访问弹出窗口的 forwardFrame')
|
||||
}
|
||||
@@ -457,7 +596,14 @@ export class CleanerService {
|
||||
log.error('[导航失败] workFrame 为空', {
|
||||
elapsedMs: Date.now() - navStartTime,
|
||||
pageUrl: popupPage.url(),
|
||||
contextData: await capturePageContext(popupPage, undefined, 'nav.workFrame')
|
||||
contextData: await capturePageContext(
|
||||
popupPage,
|
||||
undefined,
|
||||
'nav.workFrame',
|
||||
undefined,
|
||||
undefined,
|
||||
'nav_work_frame'
|
||||
)
|
||||
})
|
||||
throw new Error('无法访问内部工作框架')
|
||||
}
|
||||
@@ -680,14 +826,40 @@ export class CleanerService {
|
||||
const { detailPage, deleteSet, dryRun, progressState, expectedOrderNumber, onProgress } = params
|
||||
const processStartTime = Date.now()
|
||||
|
||||
log.info('[ORDER_START] 开始处理订单', {
|
||||
orderIndex: progressState.ordersStarted,
|
||||
orderPosition: `${progressState.ordersStarted + 1}/${progressState.totalOrders}`,
|
||||
orderNumber: expectedOrderNumber,
|
||||
phase: 'starting',
|
||||
elapsedMs: Date.now() - processStartTime
|
||||
})
|
||||
|
||||
log.info('[订单详情处理开始]', {
|
||||
expectedOrderNumber,
|
||||
dryRun,
|
||||
deleteSetSize: deleteSet.size,
|
||||
completedOrders: progressState.completedOrders,
|
||||
ordersStarted: progressState.ordersStarted,
|
||||
ordersCompleted: progressState.ordersCompleted,
|
||||
totalOrders: progressState.totalOrders
|
||||
})
|
||||
|
||||
let detailCount = 0
|
||||
const detail: OrderCleanDetail = {
|
||||
orderNumber: expectedOrderNumber || 'UNKNOWN',
|
||||
materialsDeleted: 0,
|
||||
materialsSkipped: 0,
|
||||
errors: [],
|
||||
skippedMaterials: [],
|
||||
deletedMaterials: [],
|
||||
retryCount: 0,
|
||||
retryAttempts: [],
|
||||
retriedAt: undefined,
|
||||
retrySuccess: false,
|
||||
materialsFailed: 0,
|
||||
failedMaterials: [],
|
||||
uncertainDeletions: 0
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 1: Access forward frame
|
||||
log.debug('[详情页面 Step 1] 准备访问 forwardFrame')
|
||||
@@ -699,7 +871,14 @@ export class CleanerService {
|
||||
log.error('[详情页面失败] forwardFrame 访问失败', {
|
||||
elapsedMs: Date.now() - processStartTime,
|
||||
pageUrl: detailPage.url(),
|
||||
contextData: await capturePageContext(detailPage)
|
||||
contextData: await capturePageContext(
|
||||
detailPage,
|
||||
undefined,
|
||||
'processDetail.forwardFrame',
|
||||
expectedOrderNumber,
|
||||
undefined,
|
||||
'process_detail_forward_frame'
|
||||
)
|
||||
})
|
||||
throw new Error(errorMsg)
|
||||
}
|
||||
@@ -721,7 +900,10 @@ export class CleanerService {
|
||||
contextData: await capturePageContext(
|
||||
detailPage,
|
||||
undefined,
|
||||
'processDetail.detailInnerFrame'
|
||||
'processDetail.detailInnerFrame',
|
||||
expectedOrderNumber,
|
||||
undefined,
|
||||
'process_detail_inner_frame'
|
||||
)
|
||||
})
|
||||
throw new Error(errorMsg)
|
||||
@@ -752,21 +934,8 @@ export class CleanerService {
|
||||
usedFallback: !sourceOrderNumber && !!expectedOrderNumber
|
||||
})
|
||||
|
||||
const detail: OrderCleanDetail = {
|
||||
orderNumber,
|
||||
materialsDeleted: 0,
|
||||
materialsSkipped: 0,
|
||||
errors: [],
|
||||
skippedMaterials: [],
|
||||
deletedMaterials: [],
|
||||
retryCount: 0,
|
||||
retryAttempts: [],
|
||||
retriedAt: undefined,
|
||||
retrySuccess: false,
|
||||
materialsFailed: 0,
|
||||
failedMaterials: [],
|
||||
uncertainDeletions: 0
|
||||
}
|
||||
// 更新 orderNumber 为实际提取的值或 fallback
|
||||
detail.orderNumber = orderNumber
|
||||
|
||||
// Step 5: Get material counts and status
|
||||
log.debug('[详情页面 Step 5] 读取物料数量和状态')
|
||||
@@ -788,13 +957,13 @@ export class CleanerService {
|
||||
onProgress?.(
|
||||
`开始处理订单:${orderNumber}`,
|
||||
this.calculateProgress(
|
||||
progressState.completedOrders,
|
||||
progressState.ordersStarted,
|
||||
0,
|
||||
detailCount,
|
||||
progressState.totalOrders
|
||||
),
|
||||
{
|
||||
currentOrderIndex: progressState.completedOrders + 1,
|
||||
currentOrderIndex: progressState.ordersStarted + 1,
|
||||
totalOrders: progressState.totalOrders,
|
||||
currentMaterialIndex: 0,
|
||||
totalMaterialsInOrder: detailCount,
|
||||
@@ -861,7 +1030,7 @@ export class CleanerService {
|
||||
const pendingQty = await this.getInputValue(childForm, /^累计待发数量$/)
|
||||
|
||||
const progress = this.calculateProgress(
|
||||
progressState.completedOrders,
|
||||
progressState.ordersStarted,
|
||||
materialIdx,
|
||||
detailCount,
|
||||
progressState.totalOrders
|
||||
@@ -871,7 +1040,7 @@ export class CleanerService {
|
||||
`订单 ${orderNumber} - 物料 ${materialIdx}/${detailCount}: ${materialName}`,
|
||||
progress,
|
||||
{
|
||||
currentOrderIndex: progressState.completedOrders + 1,
|
||||
currentOrderIndex: progressState.ordersStarted + 1,
|
||||
totalOrders: progressState.totalOrders,
|
||||
currentMaterialIndex: materialIdx,
|
||||
totalMaterialsInOrder: detailCount,
|
||||
@@ -1078,10 +1247,31 @@ export class CleanerService {
|
||||
orderNumber: expectedOrderNumber || 'UNKNOWN',
|
||||
error: message,
|
||||
elapsedMs: Date.now() - processStartTime,
|
||||
contextData: await capturePageContext(detailPage, undefined, 'processDetail.error')
|
||||
contextData: await capturePageContext(
|
||||
detailPage,
|
||||
undefined,
|
||||
'processDetail.error',
|
||||
expectedOrderNumber,
|
||||
undefined,
|
||||
'process_detail_error'
|
||||
)
|
||||
})
|
||||
throw error
|
||||
} finally {
|
||||
const totalOrderTime = Date.now() - processStartTime
|
||||
log.info('[ORDER_COMPLETE] 订单处理完成', {
|
||||
orderIndex: progressState.ordersStarted,
|
||||
orderPosition: `${progressState.ordersStarted + 1}/${progressState.totalOrders}`,
|
||||
ordersCompleted: progressState.ordersCompleted,
|
||||
orderNumber: expectedOrderNumber || 'UNKNOWN',
|
||||
totalMaterials: detailCount,
|
||||
deleted: detail?.materialsDeleted ?? 0,
|
||||
skipped: detail?.materialsSkipped ?? 0,
|
||||
failed: detail?.materialsFailed ?? 0,
|
||||
totalOrderTimeMs: totalOrderTime,
|
||||
isSlow: totalOrderTime > 30000
|
||||
})
|
||||
|
||||
log.debug('[详情页面清理] 准备关闭详情页面', { pageUrl: detailPage.url() })
|
||||
await detailPage.close()
|
||||
log.debug('[详情页面清理完成] 详情页已关闭')
|
||||
@@ -1115,7 +1305,11 @@ export class CleanerService {
|
||||
return /^SC\d{14}$/.test(value)
|
||||
}
|
||||
|
||||
private createErrorDetail(orderNumber: string, message: string, notFound: boolean = false): OrderCleanDetail {
|
||||
private createErrorDetail(
|
||||
orderNumber: string,
|
||||
message: string,
|
||||
notFound: boolean = false
|
||||
): OrderCleanDetail {
|
||||
return {
|
||||
orderNumber,
|
||||
materialsDeleted: 0,
|
||||
@@ -1589,7 +1783,8 @@ export class CleanerService {
|
||||
dryRun,
|
||||
expectedOrderNumber: orderNumber,
|
||||
progressState: {
|
||||
completedOrders: detailIndex,
|
||||
ordersStarted: detailIndex,
|
||||
ordersCompleted: 0,
|
||||
totalOrders: failedDetails.length
|
||||
},
|
||||
onProgress: (message, progress, extra) => {
|
||||
|
||||
@@ -82,7 +82,14 @@ export class ErpAuthService {
|
||||
|
||||
if (!contentFrame) {
|
||||
log.error('Failed to access forwardFrame content frame', {
|
||||
...(await capturePageContext(page))
|
||||
...(await capturePageContext(
|
||||
page,
|
||||
undefined,
|
||||
'auth.forwardFrame',
|
||||
undefined,
|
||||
undefined,
|
||||
'auth_forward_frame'
|
||||
))
|
||||
})
|
||||
throw new Error('Failed to access forwardFrame content frame')
|
||||
}
|
||||
@@ -96,7 +103,14 @@ export class ErpAuthService {
|
||||
} catch (e) {
|
||||
log.error('Failed to find username input', {
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
...(await capturePageContext(page, undefined, 'login.username'))
|
||||
...(await capturePageContext(
|
||||
page,
|
||||
undefined,
|
||||
'login.username',
|
||||
undefined,
|
||||
undefined,
|
||||
'login_username'
|
||||
))
|
||||
})
|
||||
throw new Error(`Failed to find username input: ${e}`)
|
||||
}
|
||||
@@ -107,7 +121,14 @@ export class ErpAuthService {
|
||||
} catch (e) {
|
||||
log.error('Failed to find password input', {
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
...(await capturePageContext(page, undefined, 'login.password'))
|
||||
...(await capturePageContext(
|
||||
page,
|
||||
undefined,
|
||||
'login.password',
|
||||
undefined,
|
||||
undefined,
|
||||
'login_password'
|
||||
))
|
||||
})
|
||||
throw new Error(`Failed to find password input: ${e}`)
|
||||
}
|
||||
@@ -118,7 +139,14 @@ export class ErpAuthService {
|
||||
} catch (e) {
|
||||
log.error('Failed to click login button', {
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
...(await capturePageContext(page, undefined, 'login.button'))
|
||||
...(await capturePageContext(
|
||||
page,
|
||||
undefined,
|
||||
'login.button',
|
||||
undefined,
|
||||
undefined,
|
||||
'login_button'
|
||||
))
|
||||
})
|
||||
throw new Error(`Failed to click login button: ${e}`)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ export interface ErpErrorContext {
|
||||
targetSelector?: string
|
||||
step?: string
|
||||
screenshotPath?: string
|
||||
orderId?: string
|
||||
materialCode?: string
|
||||
errorStage?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,11 +72,18 @@ async function captureScreenshot(page: Page, step?: string): Promise<string | un
|
||||
*
|
||||
* @param page - The Playwright page to inspect
|
||||
* @param targetSelector - Optional selector that was being targeted
|
||||
* @param step - Optional step name for context
|
||||
* @param orderId - Optional order ID for error correlation
|
||||
* @param materialCode - Optional material code for error correlation
|
||||
* @param stage - Optional stage name (defaults to 'unknown')
|
||||
*/
|
||||
export async function capturePageContext(
|
||||
page: Page,
|
||||
targetSelector?: string,
|
||||
step?: string
|
||||
step?: string,
|
||||
orderId?: string,
|
||||
materialCode?: string,
|
||||
stage: string = 'unknown'
|
||||
): Promise<ErpErrorContext> {
|
||||
const ctx: ErpErrorContext = {}
|
||||
|
||||
@@ -98,6 +108,16 @@ export async function capturePageContext(
|
||||
ctx.step = step
|
||||
}
|
||||
|
||||
if (orderId) {
|
||||
ctx.orderId = orderId
|
||||
}
|
||||
|
||||
if (materialCode) {
|
||||
ctx.materialCode = materialCode
|
||||
}
|
||||
|
||||
ctx.errorStage = stage
|
||||
|
||||
ctx.screenshotPath = await captureScreenshot(page, step)
|
||||
|
||||
return ctx
|
||||
|
||||
@@ -116,7 +116,14 @@ export class ExtractorCore {
|
||||
|
||||
if (!fFrame) {
|
||||
log.error('Failed to access popup forward frame', {
|
||||
...(await capturePageContext(popupPage, undefined, 'navigate.forwardFrame'))
|
||||
...(await capturePageContext(
|
||||
popupPage,
|
||||
undefined,
|
||||
'navigate.forwardFrame',
|
||||
undefined,
|
||||
undefined,
|
||||
'navigate_forward_frame'
|
||||
))
|
||||
})
|
||||
throw new Error('Failed to access popup forward frame')
|
||||
}
|
||||
@@ -128,7 +135,14 @@ export class ExtractorCore {
|
||||
|
||||
if (!workFrame) {
|
||||
log.error('Failed to access inner work frame', {
|
||||
...(await capturePageContext(popupPage, undefined, 'navigate.innerFrame'))
|
||||
...(await capturePageContext(
|
||||
popupPage,
|
||||
undefined,
|
||||
'navigate.innerFrame',
|
||||
undefined,
|
||||
undefined,
|
||||
'navigate_inner_frame'
|
||||
))
|
||||
})
|
||||
throw new Error('Failed to access inner work frame')
|
||||
}
|
||||
|
||||
@@ -42,9 +42,7 @@ async function runMySQLMigration(): Promise<void> {
|
||||
return
|
||||
}
|
||||
|
||||
await service.query(
|
||||
`ALTER TABLE CleanerOrderHistory ADD COLUMN ProductionId VARCHAR(50) NULL`
|
||||
)
|
||||
await service.query(`ALTER TABLE CleanerOrderHistory ADD COLUMN ProductionId VARCHAR(50) NULL`)
|
||||
console.log('Added ProductionId column to CleanerOrderHistory.')
|
||||
} finally {
|
||||
if (service.isConnected()) await service.disconnect()
|
||||
|
||||
@@ -91,7 +91,7 @@ vi.mock('../../../../src/main/services/erp/erp-auth', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('../../../../src/main/services/database/cleaner-operation-history-dao', () => ({
|
||||
CleanerOperationHistoryDAO: class {
|
||||
CleanerOperationHistoryDAO: class {
|
||||
async getBatchDetails(batchId: string) {
|
||||
return { executions: [{ attemptNumber: 1, isDryRun: false }], orders: [] }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user