Files
MES/yawei-mes/.tasks/2025-11-05_工序执行情况表重做(优化一键完成按钮).md

1744 lines
49 KiB
Markdown
Raw Permalink Normal View History

2026-04-02 10:38:23 +08:00
# 工序执行情况表 - 一键完成性能优化方案
## 📊 当前调用接口分析
### 一、前端打开对话框阶段
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方法不影响其他功能
- ✅ 单个订单的一键完成和批量订单的定时完成都受益
- ✅ 保持原有事务逻辑和数据一致性