Files
MES/yawei-mes/.tasks/2025-11-05_工序执行情况表重做(优化一键完成按钮).md
2026-04-02 10:39:03 +08:00

1744 lines
49 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 工序执行情况表 - 一键完成性能优化方案
## 📊 当前调用接口分析
### 一、前端打开对话框阶段
1. **查询工序路线列表**
- 接口:`GET /production/route/list`
- 估计耗时:**100-300ms**
- 说明:查询所有可用的工序路线
2. **查询工序路线详情**(循环调用)
- 接口:`GET /production/route/{id}`
- 调用次数:**N次**每个路线1次
- 估计耗时:**100-200ms × N次 = 300-600ms**假设3个路线
- 说明:为了显示每个路线的工序数量
### 二、选择工序路线后阶段(`handleRouteChange`
3. **查询工序路线详情**
- 接口:`GET /production/route/{id}`
- 估计耗时:**100-200ms**
- 说明:获取选中路线的工序列表
4. **批量查询工位信息**
- 接口:`GET /masterdata/station/list?processIds=1,2,3,4,5`
- 估计耗时:**200-500ms**
- 说明:查询所有工序对应的工位和车间信息
### 三、提交执行阶段
5. **执行一键完成**
- 接口:`POST /production/autoComplete/execute`
- 估计耗时:**10-60秒**(核心耗时)
- 说明:后端批量生成工单和报工单
**后端操作详情:**
- 查询销售订单明细:**50-100ms**
- 查询物料信息:**50-100ms**
- 查询工序路线:**50-100ms**
- **生成5个工单****1-2秒 × 5 = 5-10秒**
- **生成5个报工单****1-2秒 × 5 = 5-10秒**
- **更新销售订单状态****100-200ms**
- 数据库事务提交:**200-500ms**
**总计:约 10-20秒**
---
## ⏱️ 当前性能问题分析
### 问题1工单和报工单逐个插入
**现状:**
```java
for (ProcessConfigDTO config : processConfigs) {
WorkOrder workOrder = new WorkOrder();
// ... 设置工单数据
workOrderMapper.insert(workOrder); // 逐个插入N次数据库操作
}
for (ProcessConfigDTO config : processConfigs) {
Report report = new Report();
// ... 设置报工单数据
reportMapper.insert(report); // 逐个插入N次数据库操作
}
```
**性能影响:**
- 每次插入都要:网络往返 + SQL解析 + 索引更新 + 事务日志
- 5个工序 = **10次数据库插入** = 约 **5-10秒**
---
### 问题2多次查询物料、路线等数据
**现状:**
```java
// 每次调用都要查询
Material material = materialMapper.selectById(materialId);
Route route = routeMapper.selectRouteById(routeId);
SalOrderEntry entry = salOrderEntryMapper.selectById(entryId);
```
**性能影响:**
- 重复查询相同数据
- 每次查询 **50-100ms**
---
### 问题3工序车间查询逐个执行
**现状:**
```java
for (ProcessConfigDTO config : processConfigs) {
// 每个工序查询一次工位
QueryWrapper<Station> qw = new QueryWrapper<>();
qw.eq("status", "0");
qw.apply("FIND_IN_SET({0}, process_ids)", processId);
Station station = stationMapper.selectOne(qw);
}
```
**性能影响:**
- 5个工序 = **5次数据库查询** = 约 **1-2秒**
---
## 🚀 优化方案
### 方案1批量插入工单和报工单高优先级⭐⭐⭐⭐⭐
**预期提速50-70%**
**实现方式:**
```java
// 1. 收集所有工单
List<WorkOrder> workOrders = new ArrayList<>();
for (ProcessConfigDTO config : processConfigs) {
WorkOrder workOrder = new WorkOrder();
// ... 设置数据
workOrders.add(workOrder);
}
// 2. 批量插入(使用 MyBatis-Plus 的 saveBatch
workOrderService.saveBatch(workOrders);
// 3. 收集所有报工单
List<Report> reports = new ArrayList<>();
for (ProcessConfigDTO config : processConfigs) {
Report report = new Report();
// ... 设置数据
reports.add(report);
}
// 4. 批量插入
reportService.saveBatch(reports);
```
**优势:**
- 10次独立插入 → **2次批量插入**
- 减少网络往返和事务开销
- **预计耗时从 5-10秒 降到 1-2秒**
---
### 方案2前端缓存工序路线详情中优先级⭐⭐⭐
**预期提速10-20%**
**实现方式:**
```javascript
data() {
return {
routeDetailsCache: {} // 路线详情缓存
}
}
async loadRouteOptions() {
const response = await listRoute({ status: '0' })
this.routeOptions = response.rows || []
// 批量预加载所有路线详情并缓存
const promises = this.routeOptions.map(async (route) => {
if (!this.routeDetailsCache[route.id]) {
const detail = await getRoute(route.id)
this.routeDetailsCache[route.id] = detail.data
}
})
await Promise.all(promises)
}
async handleRouteChange(routeId) {
// 使用缓存的详情
const routeDetail = this.routeDetailsCache[routeId]
// ... 处理逻辑
}
```
**优势:**
- 避免重复查询同一个路线详情
- **预计耗时从 300-600ms 降到 50-100ms**
---
### 方案3后端增加批量接口低优先级⭐⭐
**预期提速5-10%**
**新增接口:**
```java
/**
* 批量查询工位按工序ID列表
*/
@GetMapping("/station/batchByProcessIds")
public List<Station> batchGetStationsByProcessIds(@RequestParam String processIds) {
// processIds = "1,2,3,4,5"
String[] ids = processIds.split(",");
List<Station> stations = new ArrayList<>();
for (String processId : ids) {
QueryWrapper<Station> qw = new QueryWrapper<>();
qw.eq("status", "0");
qw.apply("FIND_IN_SET({0}, process_ids)", processId);
qw.last("LIMIT 1");
Station station = stationMapper.selectOne(qw);
if (station != null) {
stations.add(station);
}
}
return stations;
}
```
**优势:**
- 5次独立查询 → **1次批量查询**
- 减少网络往返
- **预计耗时从 1-2秒 降到 200-500ms**
---
### 方案4数据库索引优化低优先级⭐⭐
**预期提速5-10%**
**需要创建的索引:**
```sql
-- 1. 工位表索引(已在之前方案中)
CREATE INDEX idx_station_process ON md_station(process_ids);
CREATE INDEX idx_station_workshop ON md_station(workshop_id);
-- 2. 工单表索引
CREATE INDEX idx_workorder_source ON pro_workorder(source_info(100));
CREATE INDEX idx_workorder_entry_workorder ON pro_workorder_entry(workorder_id);
-- 3. 报工单表索引
CREATE INDEX idx_report_workorder ON pro_report(workorder_id);
CREATE INDEX idx_report_workorder_entry ON pro_report(workorder_entry_id);
```
**优势:**
- 加速查询操作
- 特别是检查是否已生成工单的查询
---
### 方案5异步生成可选方案
**预期提速:用户体验提升**
**实现方式:**
```java
@PostMapping("/autoComplete/execute")
public AjaxResult executeAsync(@RequestBody AutoCompleteDTO dto) {
// 创建异步任务
String taskId = UUID.randomUUID().toString();
CompletableFuture.runAsync(() -> {
autoCompleteService.autoCompleteSaleOrder(dto);
});
return AjaxResult.success("任务已提交,正在后台生成...", taskId);
}
@GetMapping("/autoComplete/status/{taskId}")
public AjaxResult getTaskStatus(@PathVariable String taskId) {
// 查询任务状态
return AjaxResult.success(taskStatus);
}
```
**优势:**
- 前端立即收到响应
- 用户可以继续其他操作
- 需要额外的任务状态管理
**劣势:**
- 实现复杂度高
- 需要任务队列和状态管理
---
## 📈 优化效果预估
### 当前性能:
- **总耗时10-20秒**
- 用户体验:⭐⭐(较差)
### 优化后方案1+2+3
- **总耗时2-5秒**
- **提速60-75%**
- 用户体验:⭐⭐⭐⭐(良好)
### 各方案优先级:
1. **方案1批量插入** - 必须实施 ⭐⭐⭐⭐⭐
2. **方案2前端缓存** - 推荐实施 ⭐⭐⭐
3. **方案3批量接口** - 可选实施 ⭐⭐
4. **方案4数据库索引** - 可选实施 ⭐⭐
5. **方案5异步生成** - 暂不推荐 ⭐
---
## 🎯 推荐实施步骤
### 第一阶段(立即实施):
1. ✅ 实施方案1批量插入工单和报工单
- 预期提速50-70%
- 开发时间1-2小时
### 第二阶段(短期实施):
2. ✅ 实施方案2前端缓存路线详情
- 预期提速10-20%
- 开发时间30分钟
3. ✅ 实施方案3后端批量工位查询接口
- 预期提速5-10%
- 开发时间1小时
### 第三阶段(长期优化):
4. ⏰ 实施方案4数据库索引优化
- 预期提速5-10%
- 执行时间10分钟
5. ⏰ 评估方案5是否需要异步生成
- 取决于用户反馈
---
## 💡 其他建议
1. **监控性能瓶颈**
- 在关键方法中添加耗时日志
- 记录每个步骤的执行时间
- 便于后续精准优化
2. **压力测试**
- 测试并发多个订单时的性能
- 测试大量工序10+)的情况
- 确保优化后不影响稳定性
3. **用户提示优化**
- 显示进度百分比
- 显示当前执行的步骤
- 提供取消操作选项
---
## 📝 备注
- 本方案基于当前代码分析得出
- 实际耗时可能因服务器性能、数据量等因素有所差异
- 建议先实施方案1观察效果后再决定是否实施其他方案
---
---
# 方案一:批量插入工单和报工单 - 详细实施方案
## ⚠️ 核心原则:只修改一键完成功能,不影响其他功能
### 🎯 实施目标
- ✅ 将工单和报工单从逐个插入改为批量插入
- ✅ 提速 50-70%10-20秒 → 2-5秒
-**绝对不能影响其他地方使用工单和报工单的功能**
---
## 📋 当前代码分析
### 当前位置:
`yjh-mes/src/main/java/cn/sourceplan/production/service/impl/AutoCompleteServiceImpl.java`
### 当前实现(第 152-607 行):
```java
@Override
@Transactional(rollbackFor = Exception.class, timeout = 600)
public AjaxResult autoCompleteSaleOrder(AutoCompleteDTO dto) {
// ... 前面的逻辑 ...
// ❌ 问题代码1逐个插入工单约第 200-300 行)
for (ProcessConfigDTO config : processConfigs) {
WorkOrder workOrder = new WorkOrder();
// ... 设置工单数据 ...
workOrderMapper.insert(workOrder); // 逐个插入
workOrderIds.add(workOrder.getId());
}
// ❌ 问题代码2逐个插入报工单约第 400-500 行)
for (ProcessConfigDTO config : processConfigs) {
Report report = new Report();
// ... 设置报工单数据 ...
reportMapper.insert(report); // 逐个插入
}
// ... 后面的逻辑 ...
}
```
---
## ✅ 优化后的实现方案
### 修改范围:
- **只修改文件**`AutoCompleteServiceImpl.java``autoCompleteSaleOrder` 方法
- **不修改文件**WorkOrderService、ReportService、Mapper 等其他文件
- **不影响功能**:其他地方调用 insert 方法的地方
---
## 🔧 详细实施步骤
### 步骤1引入必要的依赖如果还没有
**检查是否已有这些 import**
```java
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
import java.util.ArrayList;
```
**检查 WorkOrderService 和 ReportService 是否继承 IService**
```java
// 如果继承了 IService就可以使用 saveBatch 方法
public interface IWorkOrderService extends IService<WorkOrder> {
// ...
}
public interface IReportService extends IService<Report> {
// ...
}
```
---
### 步骤2修改工单批量插入逻辑
**原代码(需要找到类似的循环):**
```java
// 大约在第 200-300 行之间
List<Long> workOrderIds = new ArrayList<>();
for (ProcessConfigDTO config : processConfigs) {
WorkOrder workOrder = new WorkOrder();
workOrder.setNumber(generateWorkOrderNumber());
workOrder.setMaterialId(materialId);
workOrder.setQuantity(config.getReportQuantity());
// ... 设置其他字段 ...
workOrderMapper.insert(workOrder); // ❌ 逐个插入
workOrderIds.add(workOrder.getId());
}
```
**优化后代码(批量插入):**
```java
// ✅ 第一步:收集所有工单到列表
List<WorkOrder> workOrderList = new ArrayList<>();
List<Long> workOrderIds = new ArrayList<>();
for (ProcessConfigDTO config : processConfigs) {
WorkOrder workOrder = new WorkOrder();
workOrder.setNumber(generateWorkOrderNumber());
workOrder.setMaterialId(materialId);
workOrder.setQuantity(config.getReportQuantity());
// ... 设置其他字段(保持不变)...
workOrderList.add(workOrder); // ✅ 先收集,不插入
}
// ✅ 第二步:批量插入所有工单
workOrderService.saveBatch(workOrderList);
// ✅ 第三步收集插入后的IDsaveBatch会自动回填ID
for (WorkOrder workOrder : workOrderList) {
workOrderIds.add(workOrder.getId());
}
```
---
### 步骤3修改报工单批量插入逻辑
**原代码(需要找到类似的循环):**
```java
// 大约在第 400-500 行之间
for (ProcessConfigDTO config : processConfigs) {
Report report = new Report();
report.setWorkorderId(workOrderIds.get(index));
report.setReportUserId(config.getReportUserId());
report.setReportTime(parseReportTime(config.getReportTime()));
// ... 设置其他字段 ...
reportMapper.insert(report); // ❌ 逐个插入
}
```
**优化后代码(批量插入):**
```java
// ✅ 第一步:收集所有报工单到列表
List<Report> reportList = new ArrayList<>();
for (int i = 0; i < processConfigs.size(); i++) {
ProcessConfigDTO config = processConfigs.get(i);
Report report = new Report();
report.setWorkorderId(workOrderIds.get(i)); // 使用对应的工单ID
report.setReportUserId(config.getReportUserId());
report.setReportTime(parseReportTime(config.getReportTime()));
// ... 设置其他字段(保持不变)...
reportList.add(report); // ✅ 先收集,不插入
}
// ✅ 第二步:批量插入所有报工单
reportService.saveBatch(reportList);
```
---
## 🛡️ 安全性检查清单
### ✅ 确保不影响其他功能:
1. **只修改 autoCompleteSaleOrder 方法内部的代码**
- ❌ 不修改 WorkOrderService 的其他方法
- ❌ 不修改 ReportService 的其他方法
- ❌ 不修改 Mapper 的 insert 方法
2. **保持方法签名不变**
- ✅ 方法名:`autoCompleteSaleOrder`
- ✅ 参数:`AutoCompleteDTO dto`
- ✅ 返回值:`AjaxResult`
- ✅ 事务注解:`@Transactional`
3. **保持数据完整性**
- ✅ 工单和报工单的数量不变
- ✅ 工单和报工单的关联关系不变
- ✅ 字段值设置逻辑不变
- ✅ 事务回滚机制不变
4. **保持业务逻辑不变**
- ✅ 工单编号生成逻辑不变
- ✅ 时间计算逻辑不变
- ✅ 状态更新逻辑不变
- ✅ 返回结果格式不变
---
## 🔍 完整的修改前后对比
### 修改前的流程:
```
开始
循环处理工序1
- 创建工单对象
- 设置字段
- INSERT 操作100-200ms
- 获取ID
循环处理工序2
- 创建工单对象
- 设置字段
- INSERT 操作100-200ms
- 获取ID
... 重复N次
循环处理工序1报工单
- 创建报工单对象
- 设置字段
- INSERT 操作100-200ms
循环处理工序2报工单
- 创建报工单对象
- 设置字段
- INSERT 操作100-200ms
... 重复N次
结束
总耗时:约 10-20秒5工序 × 2类型 × 200ms
```
### 修改后的流程:
```
开始
收集所有工单到列表:
- 创建工单对象1
- 创建工单对象2
- 创建工单对象3
- 创建工单对象4
- 创建工单对象5
批量INSERT工单一次操作500-1000ms
收集所有报工单到列表:
- 创建报工单对象1
- 创建报工单对象2
- 创建报工单对象3
- 创建报工单对象4
- 创建报工单对象5
批量INSERT报工单一次操作500-1000ms
结束
总耗时:约 2-5秒2次批量操作 × 1秒
```
---
## 📝 代码实施模板
```java
@Override
@Transactional(rollbackFor = Exception.class, timeout = 600)
public AjaxResult autoCompleteSaleOrder(AutoCompleteDTO dto) {
System.out.println("========== 开始一键完成 ==========");
// ... 前面的查询逻辑保持不变 ...
// ============ 优化点1批量生成工单 ============
List<WorkOrder> workOrderList = new ArrayList<>();
List<Long> workOrderIds = new ArrayList<>();
// 第一步:收集所有工单
for (ProcessConfigDTO config : dto.getProcessConfigs()) {
WorkOrder workOrder = new WorkOrder();
// ... 设置所有字段(保持原有逻辑)...
workOrderList.add(workOrder);
}
// 第二步:批量插入工单
System.out.println("批量插入 " + workOrderList.size() + " 个工单...");
long startTime = System.currentTimeMillis();
workOrderService.saveBatch(workOrderList);
System.out.println("工单批量插入完成,耗时:" + (System.currentTimeMillis() - startTime) + "ms");
// 第三步收集工单ID
for (WorkOrder workOrder : workOrderList) {
workOrderIds.add(workOrder.getId());
}
// ============ 优化点2批量生成报工单 ============
List<Report> reportList = new ArrayList<>();
// 第一步:收集所有报工单
for (int i = 0; i < dto.getProcessConfigs().size(); i++) {
ProcessConfigDTO config = dto.getProcessConfigs().get(i);
Report report = new Report();
report.setWorkorderId(workOrderIds.get(i)); // 关联工单ID
// ... 设置所有字段(保持原有逻辑)...
reportList.add(report);
}
// 第二步:批量插入报工单
System.out.println("批量插入 " + reportList.size() + " 个报工单...");
startTime = System.currentTimeMillis();
reportService.saveBatch(reportList);
System.out.println("报工单批量插入完成,耗时:" + (System.currentTimeMillis() - startTime) + "ms");
// ... 后面的更新逻辑保持不变 ...
System.out.println("========== 一键完成结束 ==========");
return AjaxResult.success("一键完成成功!生成 " + workOrderList.size() + " 个工单和 " + reportList.size() + " 个报工单");
}
```
---
## ⚠️ 重要注意事项
### 1. 保持字段设置逻辑完全一致
```java
// ❌ 错误:修改了字段设置逻辑
workOrder.setStatus("A"); // 原来是 "0"
// ✅ 正确:完全复制原有的字段设置代码
workOrder.setStatus("0"); // 保持不变
```
### 2. 保持工单和报工单的对应关系
```java
// ✅ 确保工序1的工单对应工序1的报工单
for (int i = 0; i < processConfigs.size(); i++) {
Report report = new Report();
report.setWorkorderId(workOrderIds.get(i)); // 使用相同索引
// ...
}
```
### 3. 保持事务完整性
```java
// ✅ 事务注解保持不变
@Transactional(rollbackFor = Exception.class, timeout = 600)
// ✅ 如果批量插入失败,整个事务会回滚
// ✅ 不会出现只插入了部分数据的情况
```
### 4. 添加性能监控日志
```java
// ✅ 添加耗时日志,便于验证优化效果
long startTime = System.currentTimeMillis();
workOrderService.saveBatch(workOrderList);
System.out.println("批量插入耗时:" + (System.currentTimeMillis() - startTime) + "ms");
```
---
## 🧪 测试验证清单
### 功能测试:
- [ ] 一键完成能正常生成工单
- [ ] 一键完成能正常生成报工单
- [ ] 工单编号连续且正确
- [ ] 报工单关联了正确的工单
- [ ] 销售订单状态正确更新
- [ ] 时间计算逻辑正确
### 性能测试:
- [ ] 查看控制台日志,批量插入耗时是否显著减少
- [ ] 整体一键完成耗时是否从 10-20秒 降到 2-5秒
- [ ] 数据库连接数没有异常增长
### 兼容性测试:
- [ ] 其他地方手动创建工单仍然正常
- [ ] 其他地方手动创建报工单仍然正常
- [ ] 其他业务流程不受影响
---
## 📊 预期效果
### 优化前:
```
工单1 INSERT: 200ms
工单2 INSERT: 200ms
工单3 INSERT: 200ms
工单4 INSERT: 200ms
工单5 INSERT: 200ms
报工单1 INSERT: 200ms
报工单2 INSERT: 200ms
报工单3 INSERT: 200ms
报工单4 INSERT: 200ms
报工单5 INSERT: 200ms
-------------------
总计2000ms (2秒)
```
### 优化后:
```
收集5个工单: 10ms
批量INSERT工单: 500ms
收集5个报工单: 10ms
批量INSERT报工单: 500ms
-------------------
总计1020ms (1秒)
```
### 提速比例:
**约 50% 的时间节省!** 🚀
---
## ✅ 实施完成标准
1. ✅ 代码编译通过,没有语法错误
2. ✅ 一键完成功能测试通过
3. ✅ 控制台日志显示批量插入耗时显著减少
4. ✅ 其他功能回归测试通过
5. ✅ 没有新增的异常或错误日志
---
## 🎯 下一步行动
1. 确认当前 WorkOrderService 和 ReportService 是否继承了 IService
2. 如果没有,先让它们继承 IService这不会影响现有功能
3. 按照上述模板修改 autoCompleteSaleOrder 方法
4. 编译并测试
5. 观察性能提升效果
**需要我开始实施吗?**
---
---
# 前端进度显示优化方案
## 📊 目标
在一键完成执行过程中,显示实时进度百分比,提升用户体验。
## 🎯 实现方案推荐方案2 - 模拟进度)
### 方案1真实进度需要后端配合⭐⭐⭐⭐⭐
**优点:** 显示真实进度,准确可靠
**缺点:** 需要后端支持WebSocket/SSE/轮询)
**开发时间:** 4-6小时
**实现步骤:**
1. 后端改为异步执行
2. 后端通过 WebSocket 推送进度
3. 前端监听进度更新
4. 显示进度条和百分比
---
### 方案2模拟进度推荐⭐⭐⭐⭐
**优点:** 无需后端修改,实施简单
**缺点:** 进度是估算的,不是真实进度
**开发时间:** 30分钟
**实现方式:**
根据预估的总时间(优化后约 3-5秒显示一个平滑增长的进度条。
---
## 🚀 方案2实施代码推荐
### 修改位置:
`mes-ui/src/views/mes/statement/saleOrderExecution/index.vue`
### 1. 添加 data 属性
```javascript
data() {
return {
// ... 现有属性 ...
// 进度条相关
progressDialog: {
visible: false,
percentage: 0,
status: 'success',
message: '正在准备...'
}
}
}
```
### 2. 添加进度对话框模板
```vue
<!-- template 中添加进度对话框 -->
<el-dialog
title="一键完成进度"
:visible.sync="progressDialog.visible"
width="500px"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="false"
>
<div style="padding: 20px 0;">
<el-progress
:percentage="progressDialog.percentage"
:status="progressDialog.status"
:stroke-width="20"
></el-progress>
<div style="text-align: center; margin-top: 20px; font-size: 14px; color: #606266;">
{{ progressDialog.message }}
</div>
</div>
</el-dialog>
```
### 3. 修改 executeAutoComplete 方法
```javascript
/** 执行一键完成 */
async executeAutoComplete() {
this.autoCompleteLoading = true
// 显示进度对话框
this.progressDialog.visible = true
this.progressDialog.percentage = 0
this.progressDialog.status = 'success'
this.progressDialog.message = '正在准备数据...'
// 模拟进度更新
const progressTimer = setInterval(() => {
if (this.progressDialog.percentage < 95) {
// 根据当前进度调整增长速度越接近100%越慢)
const increment = this.progressDialog.percentage < 30 ? 3 :
this.progressDialog.percentage < 60 ? 2 :
this.progressDialog.percentage < 80 ? 1 : 0.5
this.progressDialog.percentage += increment
// 根据进度显示不同的消息
if (this.progressDialog.percentage < 20) {
this.progressDialog.message = '正在准备数据...'
} else if (this.progressDialog.percentage < 40) {
this.progressDialog.message = '正在生成工单...'
} else if (this.progressDialog.percentage < 70) {
this.progressDialog.message = '正在生成报工单...'
} else if (this.progressDialog.percentage < 90) {
this.progressDialog.message = '正在更新订单状态...'
} else {
this.progressDialog.message = '即将完成...'
}
}
}, 100) // 每100ms更新一次进度
try {
const params = {
saleOrderId: this.currentOrder.id,
saleOrderEntryId: this.currentOrder.saleOrderEntryId,
routeId: this.autoCompleteForm.routeId,
processConfigs: this.autoCompleteForm.processConfigs
}
const response = await autoCompleteSaleOrder(params)
// 完成后设置进度为100%
clearInterval(progressTimer)
this.progressDialog.percentage = 100
this.progressDialog.status = 'success'
this.progressDialog.message = '完成!'
// 延迟1秒后关闭进度对话框并显示成功消息
setTimeout(() => {
this.progressDialog.visible = false
this.$message.success(response.msg || '一键完成成功!')
this.previewDialogVisible = false
this.autoCompleteDialogVisible = false
this.getList()
}, 1000)
} catch (error) {
// 出错时停止进度更新
clearInterval(progressTimer)
this.progressDialog.status = 'exception'
this.progressDialog.message = '生成失败'
// 只显示非超时错误
if (!error.message || !error.message.includes('timeout')) {
setTimeout(() => {
this.progressDialog.visible = false
this.$message.error(error.message || '一键完成失败,请重试')
}, 1500)
} else {
// 超时情况:继续显示进度,因为后端可能还在处理
this.progressDialog.status = 'warning'
this.progressDialog.message = '处理时间较长,请稍候...'
}
} finally {
this.autoCompleteLoading = false
}
},
```
---
## 🎨 效果预览
### 显示效果:
```
┌─────────────────────────────────────┐
│ 一键完成进度 │
├─────────────────────────────────────┤
│ │
│ ████████████████████░░░░░ 65% │
│ │
│ 正在生成报工单... │
│ │
└─────────────────────────────────────┘
```
### 进度阶段:
1. **0-20%**:正在准备数据...
2. **20-40%**:正在生成工单...
3. **40-70%**:正在生成报工单...
4. **70-90%**:正在更新订单状态...
5. **90-95%**:即将完成...
6. **100%**:完成!
---
## 🎯 优化版本(更精确的进度显示)
如果想要更精确的进度显示,可以根据工序数量动态计算:
```javascript
/** 执行一键完成(优化版) */
async executeAutoComplete() {
this.autoCompleteLoading = true
// 计算总步骤数
const processCount = this.autoCompleteForm.processConfigs.length
const totalSteps = processCount * 2 + 2 // 工单数 + 报工单数 + 准备 + 完成
let currentStep = 0
// 显示进度对话框
this.progressDialog.visible = true
this.progressDialog.percentage = 0
this.progressDialog.status = 'success'
this.progressDialog.message = '正在准备数据...'
// 模拟进度更新
const updateProgress = () => {
currentStep++
this.progressDialog.percentage = Math.min(95, Math.floor((currentStep / totalSteps) * 100))
// 根据进度更新消息
const progress = this.progressDialog.percentage
if (progress < 20) {
this.progressDialog.message = '正在准备数据...'
} else if (progress < 50) {
this.progressDialog.message = `正在生成工单 (${Math.floor(progress / 50 * processCount)}/${processCount})...`
} else if (progress < 90) {
this.progressDialog.message = `正在生成报工单 (${Math.floor((progress - 50) / 40 * processCount)}/${processCount})...`
} else {
this.progressDialog.message = '正在更新订单状态...'
}
}
// 预估每步耗时(优化后约 3秒 / 总步骤数)
const stepInterval = 3000 / totalSteps
const progressTimer = setInterval(updateProgress, stepInterval)
try {
const params = {
saleOrderId: this.currentOrder.id,
saleOrderEntryId: this.currentOrder.saleOrderEntryId,
routeId: this.autoCompleteForm.routeId,
processConfigs: this.autoCompleteForm.processConfigs
}
const response = await autoCompleteSaleOrder(params)
// 完成
clearInterval(progressTimer)
this.progressDialog.percentage = 100
this.progressDialog.status = 'success'
this.progressDialog.message = `完成!已生成 ${processCount} 个工单和 ${processCount} 个报工单`
setTimeout(() => {
this.progressDialog.visible = false
this.$message.success(response.msg || '一键完成成功!')
this.previewDialogVisible = false
this.autoCompleteDialogVisible = false
this.getList()
}, 1500)
} catch (error) {
clearInterval(progressTimer)
this.progressDialog.status = 'exception'
this.progressDialog.message = '生成失败'
if (!error.message || !error.message.includes('timeout')) {
setTimeout(() => {
this.progressDialog.visible = false
this.$message.error(error.message || '一键完成失败,请重试')
}, 1500)
} else {
this.progressDialog.status = 'warning'
this.progressDialog.message = '处理时间较长,后台仍在处理...'
}
} finally {
this.autoCompleteLoading = false
}
},
```
---
## 📋 实施步骤
### 第一步添加进度对话框UI
1. 在 data 中添加 `progressDialog` 对象
2. 在 template 中添加进度对话框
### 第二步:修改执行方法
1. 替换 `executeAutoComplete` 方法为优化版本
2. 根据需要选择"基础版"或"优化版"
### 第三步:测试验证
1. 测试进度条是否正常显示
2. 测试进度消息是否正确切换
3. 测试完成后是否正确关闭
4. 测试失败时的显示效果
---
## 🎨 样式优化(可选)
### 自定义进度条颜色
```vue
<el-progress
:percentage="progressDialog.percentage"
:status="progressDialog.status"
:stroke-width="20"
:color="customColors"
></el-progress>
<script>
data() {
return {
customColors: [
{color: '#f56c6c', percentage: 20},
{color: '#e6a23c', percentage: 40},
{color: '#5cb87a', percentage: 60},
{color: '#1989fa', percentage: 80},
{color: '#6f7ad3', percentage: 100}
]
}
}
</script>
```
### 添加动画效果
```vue
<div class="progress-container" v-if="progressDialog.visible">
<div class="progress-content">
<el-progress
:percentage="progressDialog.percentage"
:status="progressDialog.status"
:stroke-width="24"
:text-inside="true"
:color="customColors"
></el-progress>
<div class="progress-message">
<i class="el-icon-loading" v-if="progressDialog.status === 'success'"></i>
<i class="el-icon-warning" v-else-if="progressDialog.status === 'warning'"></i>
<i class="el-icon-circle-close" v-else-if="progressDialog.status === 'exception'"></i>
{{ progressDialog.message }}
</div>
</div>
</div>
<style scoped>
.progress-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.progress-content {
background: white;
padding: 40px;
border-radius: 8px;
min-width: 500px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.progress-message {
text-align: center;
margin-top: 20px;
font-size: 16px;
color: #606266;
}
.progress-message i {
margin-right: 8px;
font-size: 18px;
}
</style>
```
---
## 💡 用户体验建议
### 1. 避免假进度
- ❌ 不要让进度条停在 99% 很久
- ✅ 在 95% 时放慢速度,给用户心理准备
### 2. 提供有意义的消息
- ❌ "正在处理..."(太笼统)
- ✅ "正在生成第 3/5 个工单..."(具体明确)
### 3. 处理超时情况
- 如果超时,进度条改为警告状态
- 提示用户"处理时间较长,后台仍在执行..."
- 不要突然关闭或报错
### 4. 完成后的反馈
- 显示 100% 并保持 1-2 秒
- 显示成功消息
- 平滑关闭进度对话框
---
## ✅ 实施优先级
- **基础版**30分钟简单的模拟进度条 ⭐⭐⭐⭐⭐
- **优化版**1小时根据工序数量的精确进度 ⭐⭐⭐⭐
- **美化版**2小时自定义样式和动画效果 ⭐⭐⭐
**推荐先实施基础版,效果好的话再考虑优化!**
---
## 🎯 最终推荐方案
### ⭐ 选择方案A85%上限的模拟进度条
**选择理由:**
#### 1. **最佳用户体验** 🌟
- ✅ 进度条始终流畅移动,不会"卡死"
- ✅ 85%上限留足缓冲,不会给用户"快完成了却还要等很久"的焦虑感
- ✅ 即使后端超时,用户也能看到明确的进度状态
#### 2. **技术实现简单** 🛠️
- ✅ 纯前端实现,无需修改后端
- ✅ 代码量少,维护成本低
- ✅ 实施时间短30分钟内
#### 3. **风险最低** 🛡️
- ✅ 不影响现有功能
- ✅ 不涉及后端逻辑修改
- ✅ 即使前端出错,也不会影响实际业务
#### 4. **符合实际场景** 📊
- ✅ 后端实际执行时间波动大5秒-60秒
- ✅ "模拟进度"比"假精确进度"更诚实
- ✅ 用户更关心"有进度反馈"而非"精确到百分之几"
**对比其他方案:**
| 方案 | 优点 | 缺点 | 推荐度 |
|------|------|------|--------|
| **方案A85%上限** | 🟢 流畅 🟢 不卡死 🟢 体验好 | 🟡 不够精确 | ⭐⭐⭐⭐⭐ |
| 方案B95%上限 | 🟢 看起来更快 | 🔴 容易"卡死" | ⭐⭐⭐ |
| 方案C精确进度 | 🟢 精确 | 🔴 需要后端改造 | ⭐⭐⭐⭐ |
| 方案DWebSocket | 🟢 实时 | 🔴 技术复杂度高 | ⭐⭐ |
**实施计划:**
1. ✅ 先实施方案A30分钟
2. 📊 观察用户反馈
3. 🔄 如果需要更精确再考虑方案C需要1-2天
**预期效果:**
- 🎯 用户能看到实时进度反馈
- ⏱️ 85%前进度流畅增长约45秒
- ⏳ 85%-100%等待实际完成5-15秒
- ✅ 完成后立即显示100%成功状态
---
---
# 定时完成功能优化方案
## 🔍 定时完成与一键完成的关系分析
### 代码位置:
`yjh-mes/src/main/java/cn/sourceplan/production/service/impl/TimedCompleteServiceImpl.java`
### 当前实现(第 118-178 行):
```java
@Override
@Transactional(rollbackFor = Exception.class)
public AjaxResult batchExecute(BatchExecuteDTO dto) {
// 1. 查询超期订单
List<OverdueOrderVO> orders = getOverdueOrders(dto.getModuleName(), dto.getDayThreshold());
// 2. 批量执行(实际是串行循环)
int successCount = 0;
int failCount = 0;
for (OverdueOrderVO order : orders) {
try {
// ✅ 调用一键完成服务
AutoCompleteDTO autoCompleteDTO = buildAutoCompleteDTO(order);
AjaxResult result = autoCompleteService.autoCompleteSaleOrder(autoCompleteDTO);
if (result.get("code").equals(200)) {
successCount++;
} else {
failCount++;
}
} catch (Exception e) {
failCount++;
}
}
// 3. 记录执行日志
saveExecuteLog(...);
return AjaxResult.success("批量执行完成", result);
}
```
---
## 📊 关键发现
### ✅ 定时完成 **基于** 一键完成实现
**证据:** 第145行
```java
AjaxResult result = autoCompleteService.autoCompleteSaleOrder(autoCompleteDTO);
```
**结论:**
1.**定时完成调用了一键完成服务**
2.**优化一键完成后,定时完成会自动受益**
3. ⚠️ **但定时完成是串行处理多个订单,还有优化空间**
---
## 🎯 优化方案
### 方案1优化一键完成已覆盖⭐⭐⭐⭐⭐
**说明:** 这是主要优化,定时完成会自动受益
**优化前:**
- 一键完成处理1个订单10-20秒
- 定时完成处理N个订单10-20秒 × N
**优化后(批量插入):**
- 一键完成处理1个订单2-5秒
- 定时完成处理N个订单2-5秒 × N
**效果:**
- ✅ 单订单速度提升 **50-70%**
-**无需修改定时完成代码**
-**完全不影响其他功能**
---
### 方案2改进事务粒度可选⭐⭐⭐
**说明:** 当前定时完成使用一个大事务,可能导致锁等待
**当前问题:**
```java
@Transactional(rollbackFor = Exception.class) // ❌ 整个批量执行在一个事务中
public AjaxResult batchExecute(BatchExecuteDTO dto) {
for (OverdueOrderVO order : orders) {
autoCompleteService.autoCompleteSaleOrder(autoCompleteDTO);
}
}
```
**问题分析:**
- 处理10个订单可能需要 20-50秒
- 整个过程在一个事务中,锁持有时间长
- 如果第9个订单失败前8个也会回滚
**优化方案:**
```java
// ❌ 移除方法级别的 @Transactional
// @Transactional(rollbackFor = Exception.class)
public AjaxResult batchExecute(BatchExecuteDTO dto) {
List<OverdueOrderVO> orders = getOverdueOrders(...);
int successCount = 0;
int failCount = 0;
for (OverdueOrderVO order : orders) {
try {
// ✅ 每个订单独立事务(由 autoCompleteSaleOrder 的 @Transactional 控制)
AutoCompleteDTO autoCompleteDTO = buildAutoCompleteDTO(order);
AjaxResult result = autoCompleteService.autoCompleteSaleOrder(autoCompleteDTO);
if (result.get("code").equals(200)) {
successCount++;
} else {
failCount++;
}
} catch (Exception e) {
// ✅ 单个订单失败不影响其他订单
failCount++;
}
}
// ✅ 记录日志不在事务中
saveExecuteLog(...);
return AjaxResult.success(...);
}
```
**优势:**
- ✅ 单个订单失败不影响其他订单
- ✅ 减少锁持有时间
- ✅ 提高并发性能
**注意:**
- ⚠️ 这个修改需要仔细测试
- ⚠️ 确保不影响日志记录等逻辑
---
### 方案3添加进度推送可选⭐⭐
**说明:** 定时任务可能处理多个订单,添加进度反馈
**实现方式:**
```java
public AjaxResult batchExecute(BatchExecuteDTO dto) {
List<OverdueOrderVO> orders = getOverdueOrders(...);
int successCount = 0;
int failCount = 0;
int totalCount = orders.size();
for (int i = 0; i < orders.size(); i++) {
OverdueOrderVO order = orders.get(i);
try {
// 打印进度日志
System.out.println(String.format(
"定时完成进度:%d/%d (%.1f%%) - 订单:%s",
i + 1,
totalCount,
((i + 1) * 100.0 / totalCount),
order.getOrderNumber()
));
AutoCompleteDTO autoCompleteDTO = buildAutoCompleteDTO(order);
AjaxResult result = autoCompleteService.autoCompleteSaleOrder(autoCompleteDTO);
if (result.get("code").equals(200)) {
successCount++;
System.out.println("✅ 订单 " + order.getOrderNumber() + " 处理成功");
} else {
failCount++;
System.out.println("❌ 订单 " + order.getOrderNumber() + " 处理失败:" + result.get("msg"));
}
} catch (Exception e) {
failCount++;
System.out.println("❌ 订单 " + order.getOrderNumber() + " 处理异常:" + e.getMessage());
}
}
// 打印最终统计
System.out.println(String.format(
"定时完成执行完毕:总计 %d 个订单,成功 %d 个,失败 %d 个",
totalCount, successCount, failCount
));
saveExecuteLog(...);
return AjaxResult.success(...);
}
```
**优势:**
- ✅ 便于监控执行进度
- ✅ 便于排查问题
- ✅ 可以在日志中看到详细信息
---
## 🛡️ 安全性检查清单
### ✅ 确保不影响其他功能:
1. **方案1优化一键完成**
- ✅ 完全不修改 TimedCompleteServiceImpl
- ✅ 只优化 AutoCompleteServiceImpl
- ✅ 定时完成自动受益
2. **方案2改进事务粒度**
- ⚠️ 需要移除 batchExecute 的 @Transactional
- ✅ 每个订单的处理仍在事务中(由 autoCompleteSaleOrder 保证)
- ⚠️ 需要测试日志记录是否正常
3. **方案3添加进度日志**
- ✅ 只添加日志,不改变逻辑
- ✅ 完全安全
---
## 📊 性能对比
### 场景定时任务处理10个订单
#### 优化前(当前):
```
订单115秒
订单215秒
订单315秒
...
订单1015秒
-------------------
总计150秒2.5分钟)
```
#### 优化后方案1批量插入
```
订单13秒
订单23秒
订单33秒
...
订单103秒
-------------------
总计30秒0.5分钟)
```
#### 提速效果:
- **总时间150秒 → 30秒**
- **提速80%** 🚀
---
## 📋 推荐实施方案
### 第一阶段(立即实施):✅ 必须
**方案1优化一键完成批量插入**
- 修改范围:只修改 `AutoCompleteServiceImpl.java`
- 不修改定时完成代码
- 定时完成自动受益
- **预期提速80%**
### 第二阶段(可选优化):⚠️ 谨慎
**方案2改进事务粒度**
- 修改范围:只修改 `TimedCompleteServiceImpl.batchExecute` 方法
- 需要充分测试
- 进一步提升并发性能
### 第三阶段(锦上添花):✅ 推荐
**方案3添加进度日志**
- 修改范围:只在 `batchExecute` 方法中添加日志
- 完全安全
- 便于监控和排查
---
## 🎯 实施优先级总结
| 方案 | 优先级 | 修改范围 | 风险 | 效果 |
|------|--------|----------|------|------|
| **方案1批量插入** | ⭐⭐⭐⭐⭐ | AutoCompleteServiceImpl | 低 | 提速80% |
| **方案2事务优化** | ⭐⭐⭐ | TimedCompleteServiceImpl | 中 | 提高并发 |
| **方案3进度日志** | ⭐⭐⭐⭐ | TimedCompleteServiceImpl | 无 | 便于监控 |
---
## ✅ 实施建议
### 立即实施:
1.**方案1**:优化一键完成(批量插入)
- 这是最重要的优化
- 定时完成自动受益
- 完全不影响其他功能
### 观察效果后决定:
2.**方案3**:添加进度日志
- 如果定时任务经常执行
- 建议添加日志便于监控
3.**方案2**:事务粒度优化
- 如果发现事务锁等待问题
- 再考虑实施此方案
---
## 📝 实施代码示例
### 方案3添加进度日志完整代码
```java
/**
* 批量执行自动完成
*/
@Override
@Transactional(rollbackFor = Exception.class)
public AjaxResult batchExecute(BatchExecuteDTO dto) {
long startTime = System.currentTimeMillis();
System.out.println("========== 开始定时完成批量执行 ==========");
System.out.println("执行类型:" + dto.getExecuteType());
System.out.println("天数阈值:" + dto.getDayThreshold());
// 1. 查询超期订单
List<OverdueOrderVO> orders = getOverdueOrders(
dto.getModuleName(),
dto.getDayThreshold()
);
if (orders.isEmpty()) {
System.out.println("暂无需要处理的订单");
return AjaxResult.success("暂无需要处理的订单");
}
System.out.println("查询到 " + orders.size() + " 个超期订单");
// 2. 批量执行
int successCount = 0;
int failCount = 0;
List<Map<String, Object>> failedOrders = new ArrayList<>();
int totalCount = orders.size();
for (int i = 0; i < orders.size(); i++) {
OverdueOrderVO order = orders.get(i);
// 打印进度
System.out.println(String.format(
"\n[%d/%d] 正在处理订单:%s (物料:%s数量%s)",
i + 1,
totalCount,
order.getOrderNumber(),
order.getMaterialName(),
order.getQuantity()
));
try {
long orderStartTime = System.currentTimeMillis();
// 调用一键完成服务
AutoCompleteDTO autoCompleteDTO = buildAutoCompleteDTO(order);
AjaxResult result = autoCompleteService.autoCompleteSaleOrder(autoCompleteDTO);
long orderDuration = System.currentTimeMillis() - orderStartTime;
if (result.get("code").equals(200)) {
successCount++;
System.out.println(String.format(
"✅ 订单 %s 处理成功,耗时:%dms",
order.getOrderNumber(),
orderDuration
));
} else {
failCount++;
String errorMsg = (String) result.get("msg");
System.out.println(String.format(
"❌ 订单 %s 处理失败:%s",
order.getOrderNumber(),
errorMsg
));
Map<String, Object> failedOrder = new HashMap<>();
failedOrder.put("orderNumber", order.getOrderNumber());
failedOrder.put("error", errorMsg);
failedOrders.add(failedOrder);
}
} catch (Exception e) {
failCount++;
System.out.println(String.format(
"❌ 订单 %s 处理异常:%s",
order.getOrderNumber(),
e.getMessage()
));
Map<String, Object> failedOrder = new HashMap<>();
failedOrder.put("orderNumber", order.getOrderNumber());
failedOrder.put("error", e.getMessage());
failedOrders.add(failedOrder);
}
// 打印当前进度统计
System.out.println(String.format(
"进度:%.1f%% | 成功:%d | 失败:%d | 剩余:%d",
((i + 1) * 100.0 / totalCount),
successCount,
failCount,
totalCount - (i + 1)
));
}
// 3. 记录执行日志
long duration = (System.currentTimeMillis() - startTime) / 1000;
saveExecuteLog(dto, orders.size(), successCount, failCount, duration, failedOrders);
// 4. 打印最终统计
System.out.println("\n========== 定时完成批量执行完毕 ==========");
System.out.println(String.format(
"总计:%d 个订单 | 成功:%d | 失败:%d | 耗时:%d秒",
totalCount,
successCount,
failCount,
duration
));
if (!failedOrders.isEmpty()) {
System.out.println("失败订单列表:");
for (Map<String, Object> failed : failedOrders) {
System.out.println(" - " + failed.get("orderNumber") + ": " + failed.get("error"));
}
}
// 5. 返回结果
Map<String, Object> result = new HashMap<>();
result.put("totalCount", orders.size());
result.put("successCount", successCount);
result.put("failCount", failCount);
result.put("failedOrders", failedOrders);
result.put("duration", duration);
return AjaxResult.success("批量执行完成", result);
}
```
---
## 💡 总结
### 关键点:
1.**定时完成基于一键完成实现**
2.**优化一键完成,定时完成自动受益**
3.**无需修改定时完成核心逻辑**
4.**可选:添加进度日志提升可观察性**
### 实施顺序:
1. **第一步**实施方案1批量插入优化
2. **第二步**:观察效果,测试验证
3. **第三步**根据需要实施方案3进度日志
**预期效果定时完成处理10个订单从2.5分钟降到0.5分钟!** 🚀
---
## ✅ 实施记录
### 方案1批量查询优化已完成
**实施时间**2025-11-03
**优化内容**
1.**批量查询工序路线**:在 `autoCompleteSaleOrder` 方法开始时,一次性查询所有工序的持续时间,构建 `processDurationMap`
2.**批量查询车间信息**:一次性查询所有需要的车间,构建 `workshopMap`
3.**批量查询工位信息**:一次性查询所有需要的工位,构建 `stationMap`
4.**批量查询设备信息**:一次性查询所有需要的设备,构建 `equipmentMap`
5.**传递缓存Map**将这些Map传递给 `generateWorkOrders``batchGenerateReports` 方法,避免重复查询
**修改文件**
- `yjh-mes/src/main/java/cn/sourceplan/production/service/impl/AutoCompleteServiceImpl.java`
-`autoCompleteSaleOrder` 方法中添加批量查询逻辑第193-257行
- 修改 `generateWorkOrders` 方法签名接受4个Map参数第347-349行
- 修改 `batchGenerateReports` 方法签名接受2个Map参数第557-558行
- 移除 `generateWorkOrders` 内部的重复批量查询逻辑
- 修改工单和报工单生成循环中的查询逻辑从Map获取数据而不是查询数据库
**性能提升**
- **单个订单6个工序**
- 优化前约18次数据库查询6次工序路线 + 6次车间 + 6次工位
- 优化后约4次数据库查询1次批量工序路线 + 1次批量车间 + 1次批量工位 + 1次批量设备
- **减少约78%的数据库查询次数**
- **批量处理10个订单每个6个工序**
- 优化前约180次重复查询
- 优化后每个订单仅需4次批量查询
- **预期耗时从30秒降到15秒左右**
**影响范围**
- ✅ 仅修改private方法不影响其他功能
- ✅ 单个订单的一键完成和批量订单的定时完成都受益
- ✅ 保持原有事务逻辑和数据一致性