Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f06fd275e | ||
|
|
c86508989b | ||
|
|
838783e384 | ||
|
|
d5028bfcf4 | ||
|
|
b2b29e9754 | ||
|
|
cba3c8c4f0 | ||
|
|
343cb24234 | ||
|
|
4ce5b91340 | ||
|
|
1f033eb315 | ||
|
|
681f3ba517 |
288
docs/README.md
Normal file
288
docs/README.md
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
# ERPAuto 文档指南
|
||||||
|
|
||||||
|
本文档是 ERPAuto 项目文档的**分类指南和编写规范**,用于:
|
||||||
|
|
||||||
|
- 指导文档的分类和归档
|
||||||
|
- 规范新文档的命名和格式
|
||||||
|
- 帮助开发者快速定位应创建的文档类型
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 文档分类体系
|
||||||
|
|
||||||
|
### 一、按受众分类
|
||||||
|
|
||||||
|
| 分类 | 目录 | 受众 | 内容示例 |
|
||||||
|
| -------------- | ------------ | ---------- | ---------------------------- |
|
||||||
|
| **用户文档** | `user/` | 最终用户 | 使用指南、配置说明、迁移指南 |
|
||||||
|
| **开发者文档** | `developer/` | 开发人员 | 架构设计、开发指南、模块说明 |
|
||||||
|
| **内部文档** | `internal/` | 项目维护者 | 分析报告、优化计划、模板 |
|
||||||
|
|
||||||
|
### 二、按内容类型分类
|
||||||
|
|
||||||
|
| 分类 | 目录 | 内容特点 |
|
||||||
|
| ------------ | ----------------------------------- | -------------------------------- |
|
||||||
|
| **功能特性** | `features/` | 功能说明、业务流程、重构概览 |
|
||||||
|
| **调试指南** | `debugging/` | 调试指南、快速参考、故障排查 |
|
||||||
|
| **测试文档** | `testing/` | 测试计划、测试报告、测试基础设施 |
|
||||||
|
| **模块文档** | `cleaner/`, `browser/`, `database/` | 特定模块的详细文档 |
|
||||||
|
| **计划文档** | `plans/` | 设计方案、实施计划 |
|
||||||
|
| **发布说明** | `releases/` | 版本发布记录 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 文档命名规范
|
||||||
|
|
||||||
|
### 文件名格式
|
||||||
|
|
||||||
|
```
|
||||||
|
<主题>-<子主题>-<类型>.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**规则:**
|
||||||
|
|
||||||
|
- 使用**小写字母**和**连字符** (`-`)
|
||||||
|
- 不使用空格、下划线或大写字母
|
||||||
|
- 保持简短但有描述性
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ user-override-match-feature.md
|
||||||
|
✅ settings-partial-save.md
|
||||||
|
✅ cleaner-validation-flow.md
|
||||||
|
✅ test-improvement-plan.md
|
||||||
|
|
||||||
|
❌ UserOverrideMatchFeature.md # 驼峰命名
|
||||||
|
❌ user_override_match.md # 下划线
|
||||||
|
❌ user override match.md # 空格
|
||||||
|
```
|
||||||
|
|
||||||
|
### 类型后缀约定
|
||||||
|
|
||||||
|
| 后缀 | 用途 | 示例 |
|
||||||
|
| -------------- | ---------- | ----------------------------------------- |
|
||||||
|
| `-guide.md` | 指南类文档 | `erp-login-debug-guide.md` |
|
||||||
|
| `-quickref.md` | 快速参考 | `erp-login-debug-quickref.md` |
|
||||||
|
| `-flow.md` | 流程说明 | `settings-save-button-flow.md` |
|
||||||
|
| `-feature.md` | 功能特性 | `user-override-match-feature.md` |
|
||||||
|
| `-plan.md` | 计划方案 | `test-improvement-plan.md` |
|
||||||
|
| `-report.md` | 报告总结 | `TEST_REVIEW_REPORT.md` |
|
||||||
|
| `-template.md` | 模板文件 | `cleaner-execution-report-template.md` |
|
||||||
|
| `-overview.md` | 概览说明 | `validation-handler-refactor-overview.md` |
|
||||||
|
|
||||||
|
### Plans 路径专用命名规范
|
||||||
|
|
||||||
|
`plans/` 目录使用**日期前缀**命名法,便于按时间排序和管理:
|
||||||
|
|
||||||
|
```
|
||||||
|
<YYYY-MM-DD>-<描述>-<类型>.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**类型标识:**
|
||||||
|
|
||||||
|
| 类型后缀 | 用途 | 内容重点 |
|
||||||
|
| ------------ | -------- | -------------------------------------- |
|
||||||
|
| `-plan.md` | 实施计划 | 任务分解、时间线、资源分配、风险评估 |
|
||||||
|
| `-design.md` | 设计方案 | 技术架构、接口设计、数据模型、决策理由 |
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ 2026-04-13-cleaner-db-persistence-plan.md
|
||||||
|
✅ 2026-04-13-cleaner-db-persistence-design.md
|
||||||
|
✅ 2026-04-05-postgresql-integration-plan.md
|
||||||
|
✅ 2026-04-05-postgresql-integration-design.md
|
||||||
|
|
||||||
|
❌ cleaner-db-plan.md # 缺少日期
|
||||||
|
❌ 2026-4-13-cleaner-db-plan.md # 日期格式不正确(应为 2026-04-13)
|
||||||
|
❌ 2026-04-13-plan-cleaner-db.md # 类型应在最后
|
||||||
|
```
|
||||||
|
|
||||||
|
**相关文件对:**
|
||||||
|
同一个项目通常会有配对的计划和设计文档:
|
||||||
|
|
||||||
|
- `2026-04-13-cleaner-db-persistence-plan.md` - 实施计划
|
||||||
|
- `2026-04-13-cleaner-db-persistence-design.md` - 设计方案
|
||||||
|
|
||||||
|
使用相同的日期和描述,便于关联查找。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗂️ 分类决策流程
|
||||||
|
|
||||||
|
创建新文档时,按以下流程确定分类:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 文档的读者是谁?
|
||||||
|
├─ 最终用户 → user/
|
||||||
|
├─ 开发者 → developer/
|
||||||
|
└─ 项目维护者 → internal/ 或其他专业目录
|
||||||
|
|
||||||
|
2. 文档的内容类型是什么?
|
||||||
|
├─ 功能说明 → features/
|
||||||
|
├─ 调试帮助 → debugging/
|
||||||
|
├─ 测试相关 → testing/
|
||||||
|
├─ 模块特定 → cleaner/, browser/, database/
|
||||||
|
├─ 设计计划 → plans/
|
||||||
|
└─ 发布记录 → releases/
|
||||||
|
|
||||||
|
3. 是否需要快速参考?
|
||||||
|
└─ 是 → 使用 -quickref.md 后缀,放入 debugging/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 分类示例
|
||||||
|
|
||||||
|
| 文档主题 | 正确分类 | 理由 |
|
||||||
|
| ----------------- | ----------------------------------------------------- | ------------ |
|
||||||
|
| 如何配置 ERP 连接 | `user/config-erp-guide.md` | 用户操作指南 |
|
||||||
|
| 日志系统设计 | `developer/architecture/logging-design.md` | 架构设计 |
|
||||||
|
| 登录失败排查 | `debugging/erp-login-quickref.md` | 调试快速参考 |
|
||||||
|
| 测试覆盖率分析 | `testing/coverage-analysis-report.md` | 测试报告 |
|
||||||
|
| 物料清理模块说明 | `cleaner/module-overview.md` | 模块文档 |
|
||||||
|
| 新功能实施计划 | `plans/2026-04-14-new-feature-implementation-plan.md` | 实施计划 |
|
||||||
|
| 数据库设计文档 | `plans/2026-04-14-database-schema-design.md` | 设计方案 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 文档模板
|
||||||
|
|
||||||
|
### 指南类文档模板
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# <功能> 指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
简要说明文档目的和适用范围。
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
列出使用该功能的前提条件。
|
||||||
|
|
||||||
|
## 操作步骤
|
||||||
|
|
||||||
|
1. 步骤一
|
||||||
|
2. 步骤二
|
||||||
|
3. 步骤三
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
- Q: 问题描述
|
||||||
|
- A: 解决方案
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [相关文档 1](link)
|
||||||
|
- [相关文档 2](link)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 功能特性文档模板
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# <功能名称> 特性说明
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
为什么需要这个功能。
|
||||||
|
|
||||||
|
## 功能描述
|
||||||
|
|
||||||
|
功能的具体行为和预期结果。
|
||||||
|
|
||||||
|
## 用户流程
|
||||||
|
|
||||||
|
用户使用该功能的完整流程。
|
||||||
|
|
||||||
|
## 技术实现
|
||||||
|
|
||||||
|
关键实现细节(可选)。
|
||||||
|
|
||||||
|
## 影响范围
|
||||||
|
|
||||||
|
对其他模块的影响。
|
||||||
|
```
|
||||||
|
|
||||||
|
### 计划文档模板
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# <项目名称> 实施计划
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
项目要达成的目标。
|
||||||
|
|
||||||
|
## 范围
|
||||||
|
|
||||||
|
包含和不包含的内容。
|
||||||
|
|
||||||
|
## 任务分解
|
||||||
|
|
||||||
|
- [ ] 任务 1
|
||||||
|
- [ ] 任务 2
|
||||||
|
- [ ] 任务 3
|
||||||
|
|
||||||
|
## 时间线
|
||||||
|
|
||||||
|
预计开始和结束时间。
|
||||||
|
|
||||||
|
## 风险
|
||||||
|
|
||||||
|
可能的风险和应对措施。
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 文档维护
|
||||||
|
|
||||||
|
### 文档更新
|
||||||
|
|
||||||
|
- **功能变更时**:同步更新相关文档
|
||||||
|
- **发现错误时**:立即修正并提交
|
||||||
|
- **版本发布时**:更新 `releases/` 中的发布说明
|
||||||
|
|
||||||
|
### 文档审查
|
||||||
|
|
||||||
|
新文档创建后,应检查:
|
||||||
|
|
||||||
|
- [ ] 分类是否正确
|
||||||
|
- [ ] 命名是否符合规范
|
||||||
|
- [ ] 是否使用了模板
|
||||||
|
- [ ] 链接是否有效
|
||||||
|
- [ ] 是否添加到相关索引
|
||||||
|
|
||||||
|
### 废弃文档
|
||||||
|
|
||||||
|
过时的文档应:
|
||||||
|
|
||||||
|
1. 在文件顶部添加 `> ⚠️ 已废弃` 标记
|
||||||
|
2. 说明废弃原因和替代文档
|
||||||
|
3. 在下一个版本发布时移至 `archive/` 目录
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 根目录文档
|
||||||
|
|
||||||
|
`docs/` 根目录仅保留**跨category的项目级文档**:
|
||||||
|
|
||||||
|
| 文档 | 用途 |
|
||||||
|
| -------------------------------------- | ----------------- |
|
||||||
|
| `README.md` | 本文档 - 分类指南 |
|
||||||
|
| `build-and-release-guide.md` | 构建和发布流程 |
|
||||||
|
| `portable-auto-update-architecture.md` | 便携版更新架构 |
|
||||||
|
|
||||||
|
**原则**:如果文档不属于特定分类,且对项目整体重要,可放在根目录。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 找不到合适的分类?
|
||||||
|
|
||||||
|
如果现有分类无法容纳你的文档:
|
||||||
|
|
||||||
|
1. 检查是否可以归入 `internal/`(内部文档)
|
||||||
|
2. 考虑是否应该创建新的子目录
|
||||||
|
3. 在提交 PR 时说明分类理由
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_最后更新:2026-04-14_
|
||||||
@@ -14,7 +14,7 @@ Extractor 已有成熟的数据库持久化模式(`ExtractorOperationHistory`
|
|||||||
## 设计决策
|
## 设计决策
|
||||||
|
|
||||||
| 决策项 | 选择 | 理由 |
|
| 决策项 | 选择 | 理由 |
|
||||||
|--------|------|------|
|
| -------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
|
||||||
| 表结构 | 独立建表,不与 Extractor 共用 | Cleaner 数据结构差异大(双层、物料级详情),独立更清晰 |
|
| 表结构 | 独立建表,不与 Extractor 共用 | Cleaner 数据结构差异大(双层、物料级详情),独立更清晰 |
|
||||||
| 记录粒度 | 执行 + 订单 + 物料三层 | 执行表存全局信息,订单表存订单汇总,物料表存操作明细 |
|
| 记录粒度 | 执行 + 订单 + 物料三层 | 执行表存全局信息,订单表存订单汇总,物料表存操作明细 |
|
||||||
| 批次标识 | `BatchId`(UUID),与 Extractor 一致 | 标准、简洁,不需要嵌入时间戳 |
|
| 批次标识 | `BatchId`(UUID),与 Extractor 一致 | 标准、简洁,不需要嵌入时间戳 |
|
||||||
@@ -33,7 +33,7 @@ Extractor 已有成熟的数据库持久化模式(`ExtractorOperationHistory`
|
|||||||
一次清理操作(含重试)的全局信息。每次尝试一行记录。
|
一次清理操作(含重试)的全局信息。每次尝试一行记录。
|
||||||
|
|
||||||
| 列名 | 类型 | 说明 |
|
| 列名 | 类型 | 说明 |
|
||||||
|------|------|------|
|
| ----------------------- | ---------------- | ---------------------------------------------- |
|
||||||
| ID | INT IDENTITY | 自增主键 |
|
| ID | INT IDENTITY | 自增主键 |
|
||||||
| BatchId | UNIQUEIDENTIFIER | 批次 ID,一次清理操作(含重试)共享 |
|
| BatchId | UNIQUEIDENTIFIER | 批次 ID,一次清理操作(含重试)共享 |
|
||||||
| AttemptNumber | INT | 第几次尝试(1=首次,2=外层重试) |
|
| AttemptNumber | INT | 第几次尝试(1=首次,2=外层重试) |
|
||||||
@@ -59,7 +59,7 @@ Extractor 已有成熟的数据库持久化模式(`ExtractorOperationHistory`
|
|||||||
每个订单在每次尝试中的执行结果。每个订单每次尝试一行记录。
|
每个订单在每次尝试中的执行结果。每个订单每次尝试一行记录。
|
||||||
|
|
||||||
| 列名 | 类型 | 说明 |
|
| 列名 | 类型 | 说明 |
|
||||||
|------|------|------|
|
| ------------------ | ---------------- | -------------------------- |
|
||||||
| ID | INT IDENTITY | 自增主键 |
|
| ID | INT IDENTITY | 自增主键 |
|
||||||
| BatchId | UNIQUEIDENTIFIER | 关联执行表 BatchId |
|
| BatchId | UNIQUEIDENTIFIER | 关联执行表 BatchId |
|
||||||
| AttemptNumber | INT | 关联执行表 AttemptNumber |
|
| AttemptNumber | INT | 关联执行表 AttemptNumber |
|
||||||
@@ -82,7 +82,7 @@ Extractor 已有成熟的数据库持久化模式(`ExtractorOperationHistory`
|
|||||||
每个物料在每次尝试中的操作明细。
|
每个物料在每次尝试中的操作明细。
|
||||||
|
|
||||||
| 列名 | 类型 | 说明 |
|
| 列名 | 类型 | 说明 |
|
||||||
|------|------|------|
|
| ------------------ | ---------------- | -------------------------------------- |
|
||||||
| ID | INT IDENTITY | 自增主键 |
|
| ID | INT IDENTITY | 自增主键 |
|
||||||
| BatchId | UNIQUEIDENTIFIER | 关联执行表 BatchId |
|
| BatchId | UNIQUEIDENTIFIER | 关联执行表 BatchId |
|
||||||
| AttemptNumber | INT | 关联执行表 AttemptNumber |
|
| AttemptNumber | INT | 关联执行表 AttemptNumber |
|
||||||
@@ -102,18 +102,21 @@ Extractor 已有成熟的数据库持久化模式(`ExtractorOperationHistory`
|
|||||||
首次执行到第 80 个订单时崩溃,外层重试成功完成全部 211 个订单:
|
首次执行到第 80 个订单时崩溃,外层重试成功完成全部 211 个订单:
|
||||||
|
|
||||||
**CleanerExecution**
|
**CleanerExecution**
|
||||||
|
|
||||||
```
|
```
|
||||||
BatchId=uuid-1, Attempt=1, Status=crashed, TotalOrders=211, Processed=80, ...
|
BatchId=uuid-1, Attempt=1, Status=crashed, TotalOrders=211, Processed=80, ...
|
||||||
BatchId=uuid-1, Attempt=2, Status=success, TotalOrders=211, Processed=211, ...
|
BatchId=uuid-1, Attempt=2, Status=success, TotalOrders=211, Processed=211, ...
|
||||||
```
|
```
|
||||||
|
|
||||||
**CleanerOrderHistory**(Attempt=1 中部分记录)
|
**CleanerOrderHistory**(Attempt=1 中部分记录)
|
||||||
|
|
||||||
```
|
```
|
||||||
BatchId=uuid-1, Attempt=1, Order=SC001, Status=success, Deleted=5, Skipped=1
|
BatchId=uuid-1, Attempt=1, Order=SC001, Status=success, Deleted=5, Skipped=1
|
||||||
BatchId=uuid-1, Attempt=1, Order=SC080, Status=crashed, Error=查询超时
|
BatchId=uuid-1, Attempt=1, Order=SC080, Status=crashed, Error=查询超时
|
||||||
```
|
```
|
||||||
|
|
||||||
**CleanerOrderHistory**(Attempt=2 中部分记录)
|
**CleanerOrderHistory**(Attempt=2 中部分记录)
|
||||||
|
|
||||||
```
|
```
|
||||||
BatchId=uuid-1, Attempt=2, Order=SC001, Status=success, Deleted=5, Skipped=1
|
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=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 中的物料)
|
**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-001, Result=deleted
|
||||||
BatchId=uuid-1, Attempt=2, Order=SC080, Material=MAT-002, Result=skipped, Reason=不可删除
|
BatchId=uuid-1, Attempt=2, Order=SC080, Material=MAT-002, Result=skipped, Reason=不可删除
|
||||||
@@ -219,7 +223,7 @@ BatchId=uuid-1, Attempt=2, Order=SC080, Material=MAT-002, Result=skipped, Reason
|
|||||||
## 移除的概念
|
## 移除的概念
|
||||||
|
|
||||||
| 概念 | 原因 |
|
| 概念 | 原因 |
|
||||||
|------|------|
|
| ------------------------------ | --------------------------------- |
|
||||||
| ExecutionId(CLN-时间戳-随机) | 为文件名设计,数据库用 UUID |
|
| ExecutionId(CLN-时间戳-随机) | 为文件名设计,数据库用 UUID |
|
||||||
| generateExecutionId() | 随 ExecutionId 一起移除 |
|
| generateExecutionId() | 随 ExecutionId 一起移除 |
|
||||||
| CleanerReportGenerator | Markdown 报告生成器,被数据库替代 |
|
| CleanerReportGenerator | Markdown 报告生成器,被数据库替代 |
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
## Task 1: 新增类型定义
|
## Task 1: 新增类型定义
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
|
|
||||||
- Create: `src/main/types/cleaner-history.types.ts`
|
- Create: `src/main/types/cleaner-history.types.ts`
|
||||||
|
|
||||||
**Step 1: 创建类型文件**
|
**Step 1: 创建类型文件**
|
||||||
@@ -146,6 +147,7 @@ feat(cleaner): add type definitions for cleaner operation history
|
|||||||
## Task 2: 新增 DAO 层
|
## Task 2: 新增 DAO 层
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
|
|
||||||
- Create: `src/main/services/database/cleaner-operation-history-dao.ts`
|
- Create: `src/main/services/database/cleaner-operation-history-dao.ts`
|
||||||
|
|
||||||
**Step 1: 创建 DAO 文件**
|
**Step 1: 创建 DAO 文件**
|
||||||
@@ -165,47 +167,101 @@ export class CleanerOperationHistoryDAO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async insertExecution(input: InsertCleanerExecutionInput): Promise<boolean>
|
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 {
|
private getOrderTableName(): string {
|
||||||
return this.getDialect().quoteTableName('ERPAuto', 'CleanerOrderHistory')
|
return this.getDialect().quoteTableName('ERPAuto', 'CleanerOrderHistory')
|
||||||
}
|
}
|
||||||
|
|
||||||
async insertOrderRecords(batchId: string, attemptNumber: number, orders: InsertOrderInput[]): Promise<boolean>
|
async insertOrderRecords(
|
||||||
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>
|
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 {
|
private getMaterialTableName(): string {
|
||||||
return this.getDialect().quoteTableName('ERPAuto', 'CleanerMaterialDetail')
|
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 getBatches(
|
||||||
async getBatchDetails(batchId: string): Promise<{ executions: CleanerExecutionRecord[]; orders: CleanerOrderRecord[] }>
|
userId?: number,
|
||||||
async getMaterialDetails(batchId: string, attemptNumber: number, orderNumber: string): Promise<CleanerMaterialRecord[]>
|
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>
|
async disconnect(): Promise<void>
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`getBatches` 查询逻辑:
|
`getBatches` 查询逻辑:
|
||||||
|
|
||||||
- `GROUP BY BatchId`,取 `MAX(AttemptNumber)` 对应的执行记录状态作为最终状态
|
- `GROUP BY BatchId`,取 `MAX(AttemptNumber)` 对应的执行记录状态作为最终状态
|
||||||
- 汇总订单级的 success/failed 计数
|
- 汇总订单级的 success/failed 计数
|
||||||
- 支持 userId 过滤(普通用户)和 usernames 过滤(管理员)
|
- 支持 userId 过滤(普通用户)和 usernames 过滤(管理员)
|
||||||
- 支持分页
|
- 支持分页
|
||||||
|
|
||||||
`getBatchDetails` 查询逻辑:
|
`getBatchDetails` 查询逻辑:
|
||||||
|
|
||||||
- 返回某 BatchId 下所有 execution 记录 + order 记录
|
- 返回某 BatchId 下所有 execution 记录 + order 记录
|
||||||
- 前端用 attemptNumber 区分不同尝试
|
- 前端用 attemptNumber 区分不同尝试
|
||||||
|
|
||||||
@@ -227,6 +283,7 @@ feat(cleaner): add CleanerOperationHistoryDAO for three-table persistence
|
|||||||
## Task 3: 新增 IPC channels
|
## Task 3: 新增 IPC channels
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
|
|
||||||
- Modify: `src/shared/ipc-channels.ts`
|
- Modify: `src/shared/ipc-channels.ts`
|
||||||
|
|
||||||
**Step 1: 添加 cleaner history channels**
|
**Step 1: 添加 cleaner history channels**
|
||||||
@@ -252,6 +309,7 @@ feat(cleaner): add IPC channels for cleaner operation history
|
|||||||
## Task 4: 新增 IPC handler
|
## Task 4: 新增 IPC handler
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
|
|
||||||
- Create: `src/main/ipc/cleaner-history-handler.ts`
|
- Create: `src/main/ipc/cleaner-history-handler.ts`
|
||||||
- Modify: `src/main/ipc/index.ts` — 注册新 handler
|
- Modify: `src/main/ipc/index.ts` — 注册新 handler
|
||||||
|
|
||||||
@@ -282,14 +340,24 @@ export function registerCleanerHistoryHandlers(): void {
|
|||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
IPC_CHANNELS.CLEANER_HISTORY_GET_BATCH_DETAILS,
|
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 模式一致
|
// ... 与 operation-history-handler 的 getBatchDetails 模式一致
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
IPC_CHANNELS.CLEANER_HISTORY_GET_MATERIAL_DETAILS,
|
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
|
## Task 5: 新增 Preload API
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
|
|
||||||
- Modify: `src/preload/api/cleaner.ts` — 新增 history 方法
|
- Modify: `src/preload/api/cleaner.ts` — 新增 history 方法
|
||||||
- Modify: `src/preload/index.d.ts` — 新增类型声明
|
- Modify: `src/preload/index.d.ts` — 新增类型声明
|
||||||
|
|
||||||
@@ -374,6 +443,7 @@ feat(cleaner): add preload API for cleaner operation history
|
|||||||
## Task 6: 改造 CleanerApplicationService — 写入数据库记录
|
## Task 6: 改造 CleanerApplicationService — 写入数据库记录
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
|
|
||||||
- Modify: `src/main/services/cleaner/cleaner-application-service.ts`
|
- Modify: `src/main/services/cleaner/cleaner-application-service.ts`
|
||||||
|
|
||||||
这是核心变更。`runCleaner` 方法需要:
|
这是核心变更。`runCleaner` 方法需要:
|
||||||
@@ -403,6 +473,7 @@ async runCleaner(
|
|||||||
**Step 4: 执行后更新订单记录和写入物料明细**
|
**Step 4: 执行后更新订单记录和写入物料明细**
|
||||||
|
|
||||||
清理完成后遍历 `result.details`(`OrderCleanDetail[]`),对每个订单:
|
清理完成后遍历 `result.details`(`OrderCleanDetail[]`),对每个订单:
|
||||||
|
|
||||||
- 调用 `historyDao.updateOrderStatus(...)` 更新订单结果
|
- 调用 `historyDao.updateOrderStatus(...)` 更新订单结果
|
||||||
- 调用 `historyDao.insertMaterialDetails(...)` 写入物料明细(skipped + failed 材料全部写入)
|
- 调用 `historyDao.insertMaterialDetails(...)` 写入物料明细(skipped + failed 材料全部写入)
|
||||||
|
|
||||||
@@ -413,6 +484,7 @@ async runCleaner(
|
|||||||
**Step 6: 外层重试改造**
|
**Step 6: 外层重试改造**
|
||||||
|
|
||||||
当 `result.crashed` 时:
|
当 `result.crashed` 时:
|
||||||
|
|
||||||
1. 调用 `historyDao.updateExecutionStatus(batchId, 1, 'crashed', ...)` 标记首次尝试为 crashed
|
1. 调用 `historyDao.updateExecutionStatus(batchId, 1, 'crashed', ...)` 标记首次尝试为 crashed
|
||||||
2. 调用 `historyDao.insertExecution({ batchId, attemptNumber: 2, ... })` 创建第二次尝试
|
2. 调用 `historyDao.insertExecution({ batchId, attemptNumber: 2, ... })` 创建第二次尝试
|
||||||
3. 调用 `historyDao.insertOrderRecords(batchId, 2, orders)` 写入第二次尝试的 pending 订单
|
3. 调用 `historyDao.insertOrderRecords(batchId, 2, orders)` 写入第二次尝试的 pending 订单
|
||||||
@@ -435,11 +507,13 @@ refactor(cleaner): replace report generation with database persistence
|
|||||||
## Task 7: 改造 cleaner-handler.ts — 执行前后写入
|
## Task 7: 改造 cleaner-handler.ts — 执行前后写入
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
|
|
||||||
- Modify: `src/main/ipc/cleaner-handler.ts`
|
- Modify: `src/main/ipc/cleaner-handler.ts`
|
||||||
|
|
||||||
**Step 1: 修改 CLEANER_RUN handler**
|
**Step 1: 修改 CLEANER_RUN handler**
|
||||||
|
|
||||||
在调用 `cleanerService.runCleaner()` 之前:
|
在调用 `cleanerService.runCleaner()` 之前:
|
||||||
|
|
||||||
1. 获取当前用户信息
|
1. 获取当前用户信息
|
||||||
2. `batchId = randomUUID()`
|
2. `batchId = randomUUID()`
|
||||||
3. 创建 `CleanerOperationHistoryDAO` 实例
|
3. 创建 `CleanerOperationHistoryDAO` 实例
|
||||||
@@ -469,6 +543,7 @@ refactor(cleaner): write execution records to database in IPC handler
|
|||||||
## Task 8: 删除 Markdown 报告生成器
|
## Task 8: 删除 Markdown 报告生成器
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
|
|
||||||
- Delete: `src/main/services/report/cleaner-report-generator.ts`
|
- Delete: `src/main/services/report/cleaner-report-generator.ts`
|
||||||
|
|
||||||
**Step 1: 删除文件**
|
**Step 1: 删除文件**
|
||||||
@@ -495,6 +570,7 @@ refactor(cleaner): remove Markdown report generator
|
|||||||
## Task 9: 前端 — 新增操作历史弹窗
|
## Task 9: 前端 — 新增操作历史弹窗
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
|
|
||||||
- Create: `src/renderer/src/components/CleanerOperationHistoryModal.tsx`
|
- Create: `src/renderer/src/components/CleanerOperationHistoryModal.tsx`
|
||||||
- Modify: `src/renderer/src/pages/CleanerPage.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 类型定义
|
## Task 10: 更新 renderer 类型定义
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
|
|
||||||
- Modify: `src/renderer/src/hooks/cleaner/types.ts`
|
- Modify: `src/renderer/src/hooks/cleaner/types.ts`
|
||||||
|
|
||||||
**Step 1: 添加 history 相关类型**
|
**Step 1: 添加 history 相关类型**
|
||||||
@@ -553,6 +630,7 @@ feat(cleaner): add renderer types for cleaner operation history
|
|||||||
## Task 11: 清理旧代码
|
## Task 11: 清理旧代码
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
|
|
||||||
- Modify: `src/renderer/src/hooks/cleaner/types.ts` — 移除 `CleanerReportData.crashed`(如果不再需要)
|
- Modify: `src/renderer/src/hooks/cleaner/types.ts` — 移除 `CleanerReportData.crashed`(如果不再需要)
|
||||||
- 检查 `ReportViewerDialog.tsx`、`ReportAnalysisDialog.tsx` 是否仍被 Cleaner 使用
|
- 检查 `ReportViewerDialog.tsx`、`ReportAnalysisDialog.tsx` 是否仍被 Cleaner 使用
|
||||||
|
|
||||||
@@ -564,6 +642,7 @@ feat(cleaner): add renderer types for cleaner operation history
|
|||||||
**Step 2: 评估 ReportViewerDialog 和 ReportAnalysisDialog**
|
**Step 2: 评估 ReportViewerDialog 和 ReportAnalysisDialog**
|
||||||
|
|
||||||
这两个组件目前用于查看 Markdown 报告文件。如果 Cleaner 不再使用它们:
|
这两个组件目前用于查看 Markdown 报告文件。如果 Cleaner 不再使用它们:
|
||||||
|
|
||||||
- 在 CleanerPage 中移除相关按钮和引用
|
- 在 CleanerPage 中移除相关按钮和引用
|
||||||
- 不删除组件本身(Extractor 可能仍在使用,后续统一清理)
|
- 不删除组件本身(Extractor 可能仍在使用,后续统一清理)
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
## 设计决策
|
## 设计决策
|
||||||
|
|
||||||
| 决策项 | 选择 | 理由 |
|
| 决策项 | 选择 | 理由 |
|
||||||
|---|---|---|
|
| ------------ | ------------------------- | -------------------------------- |
|
||||||
| 重试层级 | CleanerApplicationService | 崩溃后浏览器不可用,必须重新登录 |
|
| 重试层级 | CleanerApplicationService | 崩溃后浏览器不可用,必须重新登录 |
|
||||||
| 重试范围 | 全部订单重新跑 | 简单可靠,物料删除是幂等操作 |
|
| 重试范围 | 全部订单重新跑 | 简单可靠,物料删除是幂等操作 |
|
||||||
| 最大重试次数 | 1 次 | 覆盖瞬态故障,不过度消耗时间 |
|
| 最大重试次数 | 1 次 | 覆盖瞬态故障,不过度消耗时间 |
|
||||||
@@ -87,6 +87,7 @@ runCleaner(eventSender, input) {
|
|||||||
生成时机: `runCleaner()` 入口处,在 ERP 登录之前。重试时同一个 executionId 不变。
|
生成时机: `runCleaner()` 入口处,在 ERP 登录之前。重试时同一个 executionId 不变。
|
||||||
|
|
||||||
用途:
|
用途:
|
||||||
|
|
||||||
- 报告文件名: `cleaner-report-CLN-20260410112930-A7FK.md`
|
- 报告文件名: `cleaner-report-CLN-20260410112930-A7FK.md`
|
||||||
- RustFS 存储路径中包含该 ID,重试时覆盖同一文件
|
- RustFS 存储路径中包含该 ID,重试时覆盖同一文件
|
||||||
- 报告内容中显示该 ID
|
- 报告内容中显示该 ID
|
||||||
@@ -99,9 +100,9 @@ runCleaner(eventSender, input) {
|
|||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
| 项目 | 值 |
|
| 项目 | 值 |
|
||||||
| ---------------- | --------------------------------- |
|
| ------------ | ------------------------- | ------ |
|
||||||
| **执行 ID** | `CLN-20260410112930-A7FK` | ← 新增
|
| **执行 ID** | `CLN-20260410112930-A7FK` | ← 新增 |
|
||||||
| **应用版本** | `1.11.1` | ← 新增
|
| **应用版本** | `1.11.1` | ← 新增 |
|
||||||
| **执行时间** | `2026-04-10 11:29:30` |
|
| **执行时间** | `2026-04-10 11:29:30` |
|
||||||
| **执行模式** | `正式执行` |
|
| **执行模式** | `正式执行` |
|
||||||
| ... | ... |
|
| ... | ... |
|
||||||
|
|||||||
6
docs/releases/1.12.1.md
Normal file
6
docs/releases/1.12.1.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# 1.12.1
|
||||||
|
|
||||||
|
## 界面与交互
|
||||||
|
|
||||||
|
- 操作历史面板新增序号列,订单和物料明细表均可直观查看行号。
|
||||||
|
- 物料操作结果改用图标显示(已删除 / 已跳过 / 不确定 / 失败),悬停可查看状态名称。
|
||||||
5
docs/releases/1.12.2.md
Normal file
5
docs/releases/1.12.2.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 1.12.2
|
||||||
|
|
||||||
|
## 问题修复
|
||||||
|
|
||||||
|
- 修复管理员切换用户后登出,再次选择用户无法进入应用的问题。
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "erpauto",
|
"name": "erpauto",
|
||||||
"version": "1.12.0",
|
"version": "1.12.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "erpauto",
|
"name": "erpauto",
|
||||||
"version": "1.12.0",
|
"version": "1.12.2",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.929.0",
|
"@aws-sdk/client-s3": "^3.929.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "erpauto",
|
"name": "erpauto",
|
||||||
"version": "1.12.0",
|
"version": "1.12.2",
|
||||||
"description": "An Electron application with React and TypeScript",
|
"description": "An Electron application with React and TypeScript",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "example.com",
|
"author": "example.com",
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ export class AuthApplicationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.sessionManager.logout()
|
this.sessionManager.logout()
|
||||||
|
this.silentLoginPromise = null
|
||||||
await this.updateService.setUserContext(null)
|
await this.updateService.setUserContext(null)
|
||||||
},
|
},
|
||||||
{ operation: 'logout' }
|
{ operation: 'logout' }
|
||||||
|
|||||||
@@ -484,11 +484,7 @@ export class CleanerApplicationService {
|
|||||||
batchId,
|
batchId,
|
||||||
attemptNumber,
|
attemptNumber,
|
||||||
detail.orderNumber,
|
detail.orderNumber,
|
||||||
detail.notFound
|
detail.notFound ? 'erp_not_found' : detail.errors.length > 0 ? 'failed' : 'success',
|
||||||
? 'erp_not_found'
|
|
||||||
: detail.errors.length > 0
|
|
||||||
? 'failed'
|
|
||||||
: 'success',
|
|
||||||
detail.materialsDeleted,
|
detail.materialsDeleted,
|
||||||
detail.materialsSkipped,
|
detail.materialsSkipped,
|
||||||
detail.materialsFailed,
|
detail.materialsFailed,
|
||||||
|
|||||||
@@ -26,8 +26,12 @@ interface RetryResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ProgressState {
|
interface ProgressState {
|
||||||
completedOrders: number
|
ordersStarted: number // 开始处理的订单数
|
||||||
|
ordersCompleted: number // 已完成订单数
|
||||||
totalOrders: number
|
totalOrders: number
|
||||||
|
progressText?: string // "ordersCompleted/totalOrders (xx%)"
|
||||||
|
lastCompletedOrder?: string // 最后完成的订单号
|
||||||
|
lastActivityTime?: number // 最后活动时间戳(健康检查用)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QueryResultRow {
|
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
|
* Cleaner Service Options
|
||||||
*/
|
*/
|
||||||
@@ -242,7 +288,8 @@ export class CleanerService {
|
|||||||
const orderBatches = createBatches(input.orderNumbers, queryBatchSize)
|
const orderBatches = createBatches(input.orderNumbers, queryBatchSize)
|
||||||
const popupMutex = new AsyncMutex()
|
const popupMutex = new AsyncMutex()
|
||||||
const progressState: ProgressState = {
|
const progressState: ProgressState = {
|
||||||
completedOrders: 0,
|
ordersStarted: 0,
|
||||||
|
ordersCompleted: 0,
|
||||||
totalOrders
|
totalOrders
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,19 +304,74 @@ export class CleanerService {
|
|||||||
totalMaterials
|
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
|
// Track batch processing duration with 5s slow threshold
|
||||||
await trackDuration(
|
await trackDuration(
|
||||||
async () => {
|
async () => {
|
||||||
await this.queryOrders(workFrame, batchOrders)
|
// Phase 1: Query orders
|
||||||
await this.waitForLoading(workFrame)
|
await trackDuration(async () => await this.queryOrders(workFrame, batchOrders), {
|
||||||
|
operationName: 'query',
|
||||||
|
message: '执行订单查询',
|
||||||
|
slowThresholdMs: 3000,
|
||||||
|
context: { orderCount: batchOrders.length }
|
||||||
|
})
|
||||||
|
|
||||||
const queriedRows = await this.collectQueryResultRows(workFrame)
|
// 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))
|
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) => {
|
await runWithConcurrency(queriedRows, processConcurrency, async (row) => {
|
||||||
const { rowIndex, orderNumber } = row
|
const { rowIndex, orderNumber } = row
|
||||||
|
|
||||||
|
// [新增] Worker 开始追踪
|
||||||
|
tracker.workerStarted()
|
||||||
|
|
||||||
|
try {
|
||||||
const openedDetailPage = await popupMutex.runExclusive(async () => {
|
const openedDetailPage = await popupMutex.runExclusive(async () => {
|
||||||
return await this.openDetailPageFromRow(workFrame, popupPage!, rowIndex)
|
// [新增] Mutex 等待追踪
|
||||||
|
tracker.waitingForMutex()
|
||||||
|
const page = await this.openDetailPageFromRow(workFrame, popupPage!, rowIndex)
|
||||||
|
// [新增] Mutex 获取追踪
|
||||||
|
tracker.acquiredMutex()
|
||||||
|
return page
|
||||||
})
|
})
|
||||||
|
|
||||||
let detail: OrderCleanDetail
|
let detail: OrderCleanDetail
|
||||||
@@ -286,7 +388,10 @@ export class CleanerService {
|
|||||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||||
detail = this.createErrorDetail(orderNumber, message)
|
detail = this.createErrorDetail(orderNumber, message)
|
||||||
} finally {
|
} finally {
|
||||||
progressState.completedOrders += 1
|
progressState.ordersStarted += 1
|
||||||
|
progressState.ordersCompleted += 1
|
||||||
|
progressState.lastCompletedOrder = orderNumber
|
||||||
|
lastActivityTime = Date.now() // [新增] 健康检查:更新活动时间
|
||||||
}
|
}
|
||||||
|
|
||||||
result.details.push(detail)
|
result.details.push(detail)
|
||||||
@@ -301,8 +406,13 @@ export class CleanerService {
|
|||||||
result.materialsSkipped += detail.materialsSkipped
|
result.materialsSkipped += detail.materialsSkipped
|
||||||
result.materialsFailed += detail.materialsFailed
|
result.materialsFailed += detail.materialsFailed
|
||||||
result.uncertainDeletions += detail.uncertainDeletions
|
result.uncertainDeletions += detail.uncertainDeletions
|
||||||
|
} finally {
|
||||||
|
// [新增] Worker 完成追踪
|
||||||
|
tracker.workerCompleted()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Handle missing orders
|
||||||
const missingOrders = getMissingOrders(batchOrders, queriedOrderNumbersInBatch)
|
const missingOrders = getMissingOrders(batchOrders, queriedOrderNumbersInBatch)
|
||||||
for (const missingOrder of missingOrders) {
|
for (const missingOrder of missingOrders) {
|
||||||
const missingMessage = '订单未出现在查询结果中'
|
const missingMessage = '订单未出现在查询结果中'
|
||||||
@@ -310,6 +420,16 @@ export class CleanerService {
|
|||||||
result.details.push(this.createErrorDetail(missingOrder, missingMessage, true))
|
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`,
|
operationName: `batch-${batchIndex + 1}-${orderBatches[batchIndex].length}-orders`,
|
||||||
message: `Batch ${batchIndex + 1}/${orderBatches.length}`,
|
message: `Batch ${batchIndex + 1}/${orderBatches.length}`,
|
||||||
@@ -323,6 +443,9 @@ export class CleanerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// [新增] 清理健康检查定时器
|
||||||
|
clearInterval(healthCheckInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
const retryResult = await this.retryFailedOrders({
|
const retryResult = await this.retryFailedOrders({
|
||||||
@@ -381,7 +504,16 @@ export class CleanerService {
|
|||||||
dryRun,
|
dryRun,
|
||||||
orderNumbers: input.orderNumbers,
|
orderNumbers: input.orderNumbers,
|
||||||
materialCodes: input.materialCodes,
|
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.errors.push(`Clean failed: ${message}`)
|
||||||
result.crashed = true
|
result.crashed = true
|
||||||
@@ -438,7 +570,14 @@ export class CleanerService {
|
|||||||
log.error('[导航失败] forwardFrame 为空', {
|
log.error('[导航失败] forwardFrame 为空', {
|
||||||
elapsedMs: Date.now() - navStartTime,
|
elapsedMs: Date.now() - navStartTime,
|
||||||
pageUrl: popupPage.url(),
|
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')
|
throw new Error('无法访问弹出窗口的 forwardFrame')
|
||||||
}
|
}
|
||||||
@@ -457,7 +596,14 @@ export class CleanerService {
|
|||||||
log.error('[导航失败] workFrame 为空', {
|
log.error('[导航失败] workFrame 为空', {
|
||||||
elapsedMs: Date.now() - navStartTime,
|
elapsedMs: Date.now() - navStartTime,
|
||||||
pageUrl: popupPage.url(),
|
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('无法访问内部工作框架')
|
throw new Error('无法访问内部工作框架')
|
||||||
}
|
}
|
||||||
@@ -680,14 +826,40 @@ export class CleanerService {
|
|||||||
const { detailPage, deleteSet, dryRun, progressState, expectedOrderNumber, onProgress } = params
|
const { detailPage, deleteSet, dryRun, progressState, expectedOrderNumber, onProgress } = params
|
||||||
const processStartTime = Date.now()
|
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('[订单详情处理开始]', {
|
log.info('[订单详情处理开始]', {
|
||||||
expectedOrderNumber,
|
expectedOrderNumber,
|
||||||
dryRun,
|
dryRun,
|
||||||
deleteSetSize: deleteSet.size,
|
deleteSetSize: deleteSet.size,
|
||||||
completedOrders: progressState.completedOrders,
|
ordersStarted: progressState.ordersStarted,
|
||||||
|
ordersCompleted: progressState.ordersCompleted,
|
||||||
totalOrders: progressState.totalOrders
|
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 {
|
try {
|
||||||
// Step 1: Access forward frame
|
// Step 1: Access forward frame
|
||||||
log.debug('[详情页面 Step 1] 准备访问 forwardFrame')
|
log.debug('[详情页面 Step 1] 准备访问 forwardFrame')
|
||||||
@@ -699,7 +871,14 @@ export class CleanerService {
|
|||||||
log.error('[详情页面失败] forwardFrame 访问失败', {
|
log.error('[详情页面失败] forwardFrame 访问失败', {
|
||||||
elapsedMs: Date.now() - processStartTime,
|
elapsedMs: Date.now() - processStartTime,
|
||||||
pageUrl: detailPage.url(),
|
pageUrl: detailPage.url(),
|
||||||
contextData: await capturePageContext(detailPage)
|
contextData: await capturePageContext(
|
||||||
|
detailPage,
|
||||||
|
undefined,
|
||||||
|
'processDetail.forwardFrame',
|
||||||
|
expectedOrderNumber,
|
||||||
|
undefined,
|
||||||
|
'process_detail_forward_frame'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
throw new Error(errorMsg)
|
throw new Error(errorMsg)
|
||||||
}
|
}
|
||||||
@@ -721,7 +900,10 @@ export class CleanerService {
|
|||||||
contextData: await capturePageContext(
|
contextData: await capturePageContext(
|
||||||
detailPage,
|
detailPage,
|
||||||
undefined,
|
undefined,
|
||||||
'processDetail.detailInnerFrame'
|
'processDetail.detailInnerFrame',
|
||||||
|
expectedOrderNumber,
|
||||||
|
undefined,
|
||||||
|
'process_detail_inner_frame'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
throw new Error(errorMsg)
|
throw new Error(errorMsg)
|
||||||
@@ -752,21 +934,8 @@ export class CleanerService {
|
|||||||
usedFallback: !sourceOrderNumber && !!expectedOrderNumber
|
usedFallback: !sourceOrderNumber && !!expectedOrderNumber
|
||||||
})
|
})
|
||||||
|
|
||||||
const detail: OrderCleanDetail = {
|
// 更新 orderNumber 为实际提取的值或 fallback
|
||||||
orderNumber,
|
detail.orderNumber = orderNumber
|
||||||
materialsDeleted: 0,
|
|
||||||
materialsSkipped: 0,
|
|
||||||
errors: [],
|
|
||||||
skippedMaterials: [],
|
|
||||||
deletedMaterials: [],
|
|
||||||
retryCount: 0,
|
|
||||||
retryAttempts: [],
|
|
||||||
retriedAt: undefined,
|
|
||||||
retrySuccess: false,
|
|
||||||
materialsFailed: 0,
|
|
||||||
failedMaterials: [],
|
|
||||||
uncertainDeletions: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 5: Get material counts and status
|
// Step 5: Get material counts and status
|
||||||
log.debug('[详情页面 Step 5] 读取物料数量和状态')
|
log.debug('[详情页面 Step 5] 读取物料数量和状态')
|
||||||
@@ -788,13 +957,13 @@ export class CleanerService {
|
|||||||
onProgress?.(
|
onProgress?.(
|
||||||
`开始处理订单:${orderNumber}`,
|
`开始处理订单:${orderNumber}`,
|
||||||
this.calculateProgress(
|
this.calculateProgress(
|
||||||
progressState.completedOrders,
|
progressState.ordersStarted,
|
||||||
0,
|
0,
|
||||||
detailCount,
|
detailCount,
|
||||||
progressState.totalOrders
|
progressState.totalOrders
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
currentOrderIndex: progressState.completedOrders + 1,
|
currentOrderIndex: progressState.ordersStarted + 1,
|
||||||
totalOrders: progressState.totalOrders,
|
totalOrders: progressState.totalOrders,
|
||||||
currentMaterialIndex: 0,
|
currentMaterialIndex: 0,
|
||||||
totalMaterialsInOrder: detailCount,
|
totalMaterialsInOrder: detailCount,
|
||||||
@@ -861,7 +1030,7 @@ export class CleanerService {
|
|||||||
const pendingQty = await this.getInputValue(childForm, /^累计待发数量$/)
|
const pendingQty = await this.getInputValue(childForm, /^累计待发数量$/)
|
||||||
|
|
||||||
const progress = this.calculateProgress(
|
const progress = this.calculateProgress(
|
||||||
progressState.completedOrders,
|
progressState.ordersStarted,
|
||||||
materialIdx,
|
materialIdx,
|
||||||
detailCount,
|
detailCount,
|
||||||
progressState.totalOrders
|
progressState.totalOrders
|
||||||
@@ -871,7 +1040,7 @@ export class CleanerService {
|
|||||||
`订单 ${orderNumber} - 物料 ${materialIdx}/${detailCount}: ${materialName}`,
|
`订单 ${orderNumber} - 物料 ${materialIdx}/${detailCount}: ${materialName}`,
|
||||||
progress,
|
progress,
|
||||||
{
|
{
|
||||||
currentOrderIndex: progressState.completedOrders + 1,
|
currentOrderIndex: progressState.ordersStarted + 1,
|
||||||
totalOrders: progressState.totalOrders,
|
totalOrders: progressState.totalOrders,
|
||||||
currentMaterialIndex: materialIdx,
|
currentMaterialIndex: materialIdx,
|
||||||
totalMaterialsInOrder: detailCount,
|
totalMaterialsInOrder: detailCount,
|
||||||
@@ -1078,10 +1247,31 @@ export class CleanerService {
|
|||||||
orderNumber: expectedOrderNumber || 'UNKNOWN',
|
orderNumber: expectedOrderNumber || 'UNKNOWN',
|
||||||
error: message,
|
error: message,
|
||||||
elapsedMs: Date.now() - processStartTime,
|
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
|
throw error
|
||||||
} finally {
|
} 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() })
|
log.debug('[详情页面清理] 准备关闭详情页面', { pageUrl: detailPage.url() })
|
||||||
await detailPage.close()
|
await detailPage.close()
|
||||||
log.debug('[详情页面清理完成] 详情页已关闭')
|
log.debug('[详情页面清理完成] 详情页已关闭')
|
||||||
@@ -1115,7 +1305,11 @@ export class CleanerService {
|
|||||||
return /^SC\d{14}$/.test(value)
|
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 {
|
return {
|
||||||
orderNumber,
|
orderNumber,
|
||||||
materialsDeleted: 0,
|
materialsDeleted: 0,
|
||||||
@@ -1589,7 +1783,8 @@ export class CleanerService {
|
|||||||
dryRun,
|
dryRun,
|
||||||
expectedOrderNumber: orderNumber,
|
expectedOrderNumber: orderNumber,
|
||||||
progressState: {
|
progressState: {
|
||||||
completedOrders: detailIndex,
|
ordersStarted: detailIndex,
|
||||||
|
ordersCompleted: 0,
|
||||||
totalOrders: failedDetails.length
|
totalOrders: failedDetails.length
|
||||||
},
|
},
|
||||||
onProgress: (message, progress, extra) => {
|
onProgress: (message, progress, extra) => {
|
||||||
|
|||||||
@@ -82,7 +82,14 @@ export class ErpAuthService {
|
|||||||
|
|
||||||
if (!contentFrame) {
|
if (!contentFrame) {
|
||||||
log.error('Failed to access forwardFrame content frame', {
|
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')
|
throw new Error('Failed to access forwardFrame content frame')
|
||||||
}
|
}
|
||||||
@@ -96,7 +103,14 @@ export class ErpAuthService {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error('Failed to find username input', {
|
log.error('Failed to find username input', {
|
||||||
error: e instanceof Error ? e.message : String(e),
|
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}`)
|
throw new Error(`Failed to find username input: ${e}`)
|
||||||
}
|
}
|
||||||
@@ -107,7 +121,14 @@ export class ErpAuthService {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error('Failed to find password input', {
|
log.error('Failed to find password input', {
|
||||||
error: e instanceof Error ? e.message : String(e),
|
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}`)
|
throw new Error(`Failed to find password input: ${e}`)
|
||||||
}
|
}
|
||||||
@@ -118,7 +139,14 @@ export class ErpAuthService {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error('Failed to click login button', {
|
log.error('Failed to click login button', {
|
||||||
error: e instanceof Error ? e.message : String(e),
|
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}`)
|
throw new Error(`Failed to click login button: ${e}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ export interface ErpErrorContext {
|
|||||||
targetSelector?: string
|
targetSelector?: string
|
||||||
step?: string
|
step?: string
|
||||||
screenshotPath?: 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 page - The Playwright page to inspect
|
||||||
* @param targetSelector - Optional selector that was being targeted
|
* @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(
|
export async function capturePageContext(
|
||||||
page: Page,
|
page: Page,
|
||||||
targetSelector?: string,
|
targetSelector?: string,
|
||||||
step?: string
|
step?: string,
|
||||||
|
orderId?: string,
|
||||||
|
materialCode?: string,
|
||||||
|
stage: string = 'unknown'
|
||||||
): Promise<ErpErrorContext> {
|
): Promise<ErpErrorContext> {
|
||||||
const ctx: ErpErrorContext = {}
|
const ctx: ErpErrorContext = {}
|
||||||
|
|
||||||
@@ -98,6 +108,16 @@ export async function capturePageContext(
|
|||||||
ctx.step = step
|
ctx.step = step
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (orderId) {
|
||||||
|
ctx.orderId = orderId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (materialCode) {
|
||||||
|
ctx.materialCode = materialCode
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.errorStage = stage
|
||||||
|
|
||||||
ctx.screenshotPath = await captureScreenshot(page, step)
|
ctx.screenshotPath = await captureScreenshot(page, step)
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|||||||
@@ -116,7 +116,14 @@ export class ExtractorCore {
|
|||||||
|
|
||||||
if (!fFrame) {
|
if (!fFrame) {
|
||||||
log.error('Failed to access popup forward frame', {
|
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')
|
throw new Error('Failed to access popup forward frame')
|
||||||
}
|
}
|
||||||
@@ -128,7 +135,14 @@ export class ExtractorCore {
|
|||||||
|
|
||||||
if (!workFrame) {
|
if (!workFrame) {
|
||||||
log.error('Failed to access inner work frame', {
|
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')
|
throw new Error('Failed to access inner work frame')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,9 +42,7 @@ async function runMySQLMigration(): Promise<void> {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await service.query(
|
await service.query(`ALTER TABLE CleanerOrderHistory ADD COLUMN ProductionId VARCHAR(50) NULL`)
|
||||||
`ALTER TABLE CleanerOrderHistory ADD COLUMN ProductionId VARCHAR(50) NULL`
|
|
||||||
)
|
|
||||||
console.log('Added ProductionId column to CleanerOrderHistory.')
|
console.log('Added ProductionId column to CleanerOrderHistory.')
|
||||||
} finally {
|
} finally {
|
||||||
if (service.isConnected()) await service.disconnect()
|
if (service.isConnected()) await service.disconnect()
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
XCircle,
|
XCircle,
|
||||||
|
CircleMinus,
|
||||||
|
AlertTriangle,
|
||||||
Clock,
|
Clock,
|
||||||
Copy,
|
Copy,
|
||||||
FlaskConical
|
FlaskConical
|
||||||
@@ -270,9 +272,7 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const filteredOrders =
|
const filteredOrders =
|
||||||
currentAttempt !== undefined
|
currentAttempt !== undefined ? orders.filter((o) => o.attemptNumber === currentAttempt) : orders
|
||||||
? orders.filter((o) => o.attemptNumber === currentAttempt)
|
|
||||||
: orders
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
||||||
@@ -291,9 +291,7 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
|||||||
<div className="flex-1 grid grid-cols-7 gap-3 text-sm">
|
<div className="flex-1 grid grid-cols-7 gap-3 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-gray-500 text-xs">操作时间</div>
|
<div className="text-gray-500 text-xs">操作时间</div>
|
||||||
<div className="font-medium text-gray-900">
|
<div className="font-medium text-gray-900">{formatDateTime(batch.operationTime)}</div>
|
||||||
{formatDateTime(batch.operationTime)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-gray-500 text-xs">操作用户</div>
|
<div className="text-gray-500 text-xs">操作用户</div>
|
||||||
@@ -318,9 +316,7 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-gray-500 text-xs">已删除</div>
|
<div className="text-gray-500 text-xs">已删除</div>
|
||||||
<div className="font-medium text-green-600">
|
<div className="font-medium text-green-600">{batch.totalMaterialsDeleted}</div>
|
||||||
{batch.totalMaterialsDeleted}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-gray-500 text-xs">失败</div>
|
<div className="text-gray-500 text-xs">失败</div>
|
||||||
@@ -389,24 +385,16 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
|||||||
)}
|
)}
|
||||||
<div className="flex flex-wrap gap-4 text-xs text-gray-600">
|
<div className="flex flex-wrap gap-4 text-xs text-gray-600">
|
||||||
{executions
|
{executions
|
||||||
.filter(
|
.filter((e) => currentAttempt === undefined || e.attemptNumber === currentAttempt)
|
||||||
(e) =>
|
|
||||||
currentAttempt === undefined ||
|
|
||||||
e.attemptNumber === currentAttempt
|
|
||||||
)
|
|
||||||
.map((exec) => (
|
.map((exec) => (
|
||||||
<React.Fragment key={exec.attemptNumber}>
|
<React.Fragment key={exec.attemptNumber}>
|
||||||
<span>
|
<span>耗时:{formatDuration(exec.operationTime, exec.endTime)}</span>
|
||||||
耗时:{formatDuration(exec.operationTime, exec.endTime)}
|
|
||||||
</span>
|
|
||||||
<span>
|
<span>
|
||||||
订单:{exec.ordersProcessed}/{exec.totalOrders}
|
订单:{exec.ordersProcessed}/{exec.totalOrders}
|
||||||
</span>
|
</span>
|
||||||
<span>删除:{exec.totalMaterialsDeleted}</span>
|
<span>删除:{exec.totalMaterialsDeleted}</span>
|
||||||
{exec.totalMaterialsFailed > 0 && (
|
{exec.totalMaterialsFailed > 0 && (
|
||||||
<span className="text-red-600">
|
<span className="text-red-600">失败:{exec.totalMaterialsFailed}</span>
|
||||||
失败:{exec.totalMaterialsFailed}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
{exec.totalUncertainDeletions > 0 && (
|
{exec.totalUncertainDeletions > 0 && (
|
||||||
<span className="text-amber-600">
|
<span className="text-amber-600">
|
||||||
@@ -419,9 +407,7 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
|||||||
{exec.errorMessage.length > 80 ? '...' : ''}
|
{exec.errorMessage.length > 80 ? '...' : ''}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{exec.appVersion && (
|
{exec.appVersion && <span className="text-gray-400">v{exec.appVersion}</span>}
|
||||||
<span className="text-gray-400">v{exec.appVersion}</span>
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -435,9 +421,10 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
|||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-2 text-left font-medium text-gray-600 w-8" />
|
<th className="px-4 py-2 text-left font-medium text-gray-600 w-8" />
|
||||||
<th className="px-4 py-2 text-left font-medium text-gray-600">
|
<th className="px-4 py-2 text-left font-medium text-gray-600 w-12 text-center">
|
||||||
总排号
|
序号
|
||||||
</th>
|
</th>
|
||||||
|
<th className="px-4 py-2 text-left font-medium text-gray-600">总排号</th>
|
||||||
<th className="px-4 py-2 text-left font-medium text-gray-600">
|
<th className="px-4 py-2 text-left font-medium text-gray-600">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
订单号
|
订单号
|
||||||
@@ -446,38 +433,21 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
|||||||
onClick={() => handleCopyColumn('orderNumber')}
|
onClick={() => handleCopyColumn('orderNumber')}
|
||||||
title="复制所有订单号"
|
title="复制所有订单号"
|
||||||
>
|
>
|
||||||
<Copy
|
<Copy size={14} className="text-gray-500 hover:text-gray-700" />
|
||||||
size={14}
|
|
||||||
className="text-gray-500 hover:text-gray-700"
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-2 text-left font-medium text-gray-600">
|
<th className="px-4 py-2 text-left font-medium text-gray-600">状态</th>
|
||||||
状态
|
<th className="px-4 py-2 text-left font-medium text-gray-600">重试</th>
|
||||||
</th>
|
<th className="px-4 py-2 text-left font-medium text-gray-600">已删除</th>
|
||||||
<th className="px-4 py-2 text-left font-medium text-gray-600">
|
<th className="px-4 py-2 text-left font-medium text-gray-600">已跳过</th>
|
||||||
重试
|
<th className="px-4 py-2 text-left font-medium text-gray-600">失败</th>
|
||||||
</th>
|
<th className="px-4 py-2 text-left font-medium text-gray-600">不确定</th>
|
||||||
<th className="px-4 py-2 text-left font-medium text-gray-600">
|
<th className="px-4 py-2 text-left font-medium text-gray-600">错误信息</th>
|
||||||
已删除
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-2 text-left font-medium text-gray-600">
|
|
||||||
已跳过
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-2 text-left font-medium text-gray-600">
|
|
||||||
失败
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-2 text-left font-medium text-gray-600">
|
|
||||||
不确定
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-2 text-left font-medium text-gray-600">
|
|
||||||
错误信息
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-100">
|
<tbody className="divide-y divide-gray-100">
|
||||||
{filteredOrders.map((order) => {
|
{filteredOrders.map((order, index) => {
|
||||||
const orderKey = `${currentAttempt ?? order.attemptNumber}:${order.orderNumber}`
|
const orderKey = `${currentAttempt ?? order.attemptNumber}:${order.orderNumber}`
|
||||||
const isOrderExpanded = expandedOrders.has(orderKey)
|
const isOrderExpanded = expandedOrders.has(orderKey)
|
||||||
const materials = orderMaterials.get(orderKey) || []
|
const materials = orderMaterials.get(orderKey) || []
|
||||||
@@ -501,6 +471,9 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
|||||||
<ChevronRight size={14} className="text-gray-400" />
|
<ChevronRight size={14} className="text-gray-400" />
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-4 py-2 text-gray-500 font-medium text-xs text-center">
|
||||||
|
{index + 1}
|
||||||
|
</td>
|
||||||
<td className="px-4 py-2 text-gray-900 font-mono text-xs">
|
<td className="px-4 py-2 text-gray-900 font-mono text-xs">
|
||||||
{order.productionId || '-'}
|
{order.productionId || '-'}
|
||||||
</td>
|
</td>
|
||||||
@@ -541,15 +514,9 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
|||||||
<span className="text-gray-400 text-xs">-</span>
|
<span className="text-gray-400 text-xs">-</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 text-green-600">
|
<td className="px-4 py-2 text-green-600">{order.materialsDeleted}</td>
|
||||||
{order.materialsDeleted}
|
<td className="px-4 py-2 text-gray-500">{order.materialsSkipped}</td>
|
||||||
</td>
|
<td className="px-4 py-2 text-red-600">{order.materialsFailed || '-'}</td>
|
||||||
<td className="px-4 py-2 text-gray-500">
|
|
||||||
{order.materialsSkipped}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-2 text-red-600">
|
|
||||||
{order.materialsFailed || '-'}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-2 text-amber-600">
|
<td className="px-4 py-2 text-amber-600">
|
||||||
{order.uncertainDeletions || '-'}
|
{order.uncertainDeletions || '-'}
|
||||||
</td>
|
</td>
|
||||||
@@ -561,30 +528,25 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
|||||||
{/* Material details */}
|
{/* Material details */}
|
||||||
{isOrderExpanded && (
|
{isOrderExpanded && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={10} className="bg-gray-50/50 px-8 py-3">
|
<td colSpan={11} className="bg-gray-50/50 px-8 py-3">
|
||||||
{isLoadingMaterials ? (
|
{isLoadingMaterials ? (
|
||||||
<div className="text-xs text-gray-500">
|
<div className="text-xs text-gray-500">加载物料详情...</div>
|
||||||
加载物料详情...
|
|
||||||
</div>
|
|
||||||
) : materials.length > 0 ? (
|
) : materials.length > 0 ? (
|
||||||
<table className="w-full text-xs">
|
<table className="w-full text-xs">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="text-gray-500">
|
<tr className="text-gray-500">
|
||||||
|
<th className="px-3 py-1.5 text-left font-medium w-12 text-center">
|
||||||
|
序号
|
||||||
|
</th>
|
||||||
<th className="px-3 py-1.5 text-left font-medium">
|
<th className="px-3 py-1.5 text-left font-medium">
|
||||||
物料编码
|
物料编码
|
||||||
</th>
|
</th>
|
||||||
<th className="px-3 py-1.5 text-left font-medium">
|
<th className="px-3 py-1.5 text-left font-medium">
|
||||||
物料名称
|
物料名称
|
||||||
</th>
|
</th>
|
||||||
<th className="px-3 py-1.5 text-left font-medium">
|
<th className="px-3 py-1.5 text-left font-medium">行号</th>
|
||||||
行号
|
<th className="px-3 py-1.5 text-left font-medium">结果</th>
|
||||||
</th>
|
<th className="px-3 py-1.5 text-left font-medium">原因</th>
|
||||||
<th className="px-3 py-1.5 text-left font-medium">
|
|
||||||
结果
|
|
||||||
</th>
|
|
||||||
<th className="px-3 py-1.5 text-left font-medium">
|
|
||||||
原因
|
|
||||||
</th>
|
|
||||||
<th className="px-3 py-1.5 text-left font-medium">
|
<th className="px-3 py-1.5 text-left font-medium">
|
||||||
尝试次数
|
尝试次数
|
||||||
</th>
|
</th>
|
||||||
@@ -593,6 +555,9 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
|||||||
<tbody className="divide-y divide-gray-100">
|
<tbody className="divide-y divide-gray-100">
|
||||||
{materials.map((mat, idx) => (
|
{materials.map((mat, idx) => (
|
||||||
<tr key={idx} className="hover:bg-gray-50">
|
<tr key={idx} className="hover:bg-gray-50">
|
||||||
|
<td className="px-3 py-1.5 text-gray-500 font-medium text-xs text-center">
|
||||||
|
{idx + 1}
|
||||||
|
</td>
|
||||||
<td className="px-3 py-1.5 font-mono text-gray-700">
|
<td className="px-3 py-1.5 font-mono text-gray-700">
|
||||||
{mat.materialCode}
|
{mat.materialCode}
|
||||||
</td>
|
</td>
|
||||||
@@ -603,29 +568,51 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
|||||||
{mat.rowNumber}
|
{mat.rowNumber}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-1.5">
|
<td className="px-3 py-1.5">
|
||||||
|
{mat.result === 'success' || mat.result === 'deleted' ? (
|
||||||
<span
|
<span
|
||||||
className={`px-1.5 py-0.5 rounded text-xs font-medium ${
|
className="inline-block cursor-help"
|
||||||
mat.result === 'deleted'
|
title="Deleted"
|
||||||
? 'bg-green-100 text-green-700'
|
|
||||||
: mat.result === 'skipped'
|
|
||||||
? 'bg-gray-100 text-gray-700'
|
|
||||||
: mat.result === 'failed'
|
|
||||||
? 'bg-red-100 text-red-700'
|
|
||||||
: mat.result === 'uncertain'
|
|
||||||
? 'bg-amber-100 text-amber-700'
|
|
||||||
: 'bg-gray-100 text-gray-700'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{mat.result === 'deleted'
|
<CheckCircle
|
||||||
? '已删除'
|
size={16}
|
||||||
: mat.result === 'skipped'
|
className="text-green-600 flex-shrink-0"
|
||||||
? '已跳过'
|
aria-label="Deleted"
|
||||||
: mat.result === 'failed'
|
/>
|
||||||
? '失败'
|
|
||||||
: mat.result === 'uncertain'
|
|
||||||
? '不确定'
|
|
||||||
: mat.result}
|
|
||||||
</span>
|
</span>
|
||||||
|
) : mat.result === 'skipped' ? (
|
||||||
|
<span
|
||||||
|
className="inline-block cursor-help"
|
||||||
|
title="Skipped"
|
||||||
|
>
|
||||||
|
<CircleMinus
|
||||||
|
size={16}
|
||||||
|
className="text-gray-400 flex-shrink-0"
|
||||||
|
aria-label="Skipped"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
) : mat.result === 'uncertain' ? (
|
||||||
|
<span
|
||||||
|
className="inline-block cursor-help"
|
||||||
|
title="Uncertain"
|
||||||
|
>
|
||||||
|
<AlertTriangle
|
||||||
|
size={16}
|
||||||
|
className="text-amber-600 flex-shrink-0"
|
||||||
|
aria-label="Uncertain"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
) : mat.result?.startsWith('failed') ? (
|
||||||
|
<span
|
||||||
|
className="inline-block cursor-help"
|
||||||
|
title="Failed"
|
||||||
|
>
|
||||||
|
<XCircle
|
||||||
|
size={16}
|
||||||
|
className="text-red-600 flex-shrink-0"
|
||||||
|
aria-label="Failed"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-1.5 text-gray-600 max-w-xs truncate">
|
<td className="px-3 py-1.5 text-gray-600 max-w-xs truncate">
|
||||||
{mat.reason || '-'}
|
{mat.reason || '-'}
|
||||||
@@ -644,9 +631,7 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-xs text-gray-500">
|
<div className="text-xs text-gray-500">暂无物料详情</div>
|
||||||
暂无物料详情
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -658,9 +643,7 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="px-4 py-6 text-center text-sm text-gray-500">
|
<div className="px-4 py-6 text-center text-sm text-gray-500">暂无订单记录</div>
|
||||||
暂无订单记录
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -721,8 +704,6 @@ export const CleanerOperationHistoryModal: React.FC<CleanerOperationHistoryModal
|
|||||||
}
|
}
|
||||||
}, [logger])
|
}, [logger])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
void fetchBatches()
|
void fetchBatches()
|
||||||
@@ -749,7 +730,13 @@ export const CleanerOperationHistoryModal: React.FC<CleanerOperationHistoryModal
|
|||||||
if (!isOpen) return null
|
if (!isOpen) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} title="清理操作历史" size="3xl" className="!max-w-[68rem]">
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
title="清理操作历史"
|
||||||
|
size="3xl"
|
||||||
|
className="!max-w-[68rem]"
|
||||||
|
>
|
||||||
<div className="flex flex-col h-[70vh]">
|
<div className="flex flex-col h-[70vh]">
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
<div className="flex items-start justify-between mb-4 pb-4 border-b border-gray-200">
|
<div className="flex items-start justify-between mb-4 pb-4 border-b border-gray-200">
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ vi.mock('../../../../src/main/services/erp/erp-auth', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../../../../src/main/services/database/cleaner-operation-history-dao', () => ({
|
vi.mock('../../../../src/main/services/database/cleaner-operation-history-dao', () => ({
|
||||||
CleanerOperationHistoryDAO: class {
|
CleanerOperationHistoryDAO: class {
|
||||||
async getBatchDetails(batchId: string) {
|
async getBatchDetails(batchId: string) {
|
||||||
return { executions: [{ attemptNumber: 1, isDryRun: false }], orders: [] }
|
return { executions: [{ attemptNumber: 1, isDryRun: false }], orders: [] }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user