# 工序执行情况表 - 一键完成性能优化方案 ## 📊 当前调用接口分析 ### 一、前端打开对话框阶段 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 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 workOrders = new ArrayList<>(); for (ProcessConfigDTO config : processConfigs) { WorkOrder workOrder = new WorkOrder(); // ... 设置数据 workOrders.add(workOrder); } // 2. 批量插入(使用 MyBatis-Plus 的 saveBatch) workOrderService.saveBatch(workOrders); // 3. 收集所有报工单 List 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 batchGetStationsByProcessIds(@RequestParam String processIds) { // processIds = "1,2,3,4,5" String[] ids = processIds.split(","); List stations = new ArrayList<>(); for (String processId : ids) { QueryWrapper 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 { // ... } public interface IReportService extends IService { // ... } ``` --- ### 步骤2:修改工单批量插入逻辑 **原代码(需要找到类似的循环):** ```java // 大约在第 200-300 行之间 List 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 workOrderList = new ArrayList<>(); List 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); // ✅ 第三步:收集插入后的ID(saveBatch会自动回填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 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 workOrderList = new ArrayList<>(); List 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 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
{{ progressDialog.message }}
``` ### 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 ``` ### 添加动画效果 ```vue
{{ progressDialog.message }}
``` --- ## 💡 用户体验建议 ### 1. 避免假进度 - ❌ 不要让进度条停在 99% 很久 - ✅ 在 95% 时放慢速度,给用户心理准备 ### 2. 提供有意义的消息 - ❌ "正在处理..."(太笼统) - ✅ "正在生成第 3/5 个工单..."(具体明确) ### 3. 处理超时情况 - 如果超时,进度条改为警告状态 - 提示用户"处理时间较长,后台仍在执行..." - 不要突然关闭或报错 ### 4. 完成后的反馈 - 显示 100% 并保持 1-2 秒 - 显示成功消息 - 平滑关闭进度对话框 --- ## ✅ 实施优先级 - **基础版**(30分钟):简单的模拟进度条 ⭐⭐⭐⭐⭐ - **优化版**(1小时):根据工序数量的精确进度 ⭐⭐⭐⭐ - **美化版**(2小时):自定义样式和动画效果 ⭐⭐⭐ **推荐先实施基础版,效果好的话再考虑优化!** --- ## 🎯 最终推荐方案 ### ⭐ 选择方案A:85%上限的模拟进度条 **选择理由:** #### 1. **最佳用户体验** 🌟 - ✅ 进度条始终流畅移动,不会"卡死" - ✅ 85%上限留足缓冲,不会给用户"快完成了却还要等很久"的焦虑感 - ✅ 即使后端超时,用户也能看到明确的进度状态 #### 2. **技术实现简单** 🛠️ - ✅ 纯前端实现,无需修改后端 - ✅ 代码量少,维护成本低 - ✅ 实施时间短(30分钟内) #### 3. **风险最低** 🛡️ - ✅ 不影响现有功能 - ✅ 不涉及后端逻辑修改 - ✅ 即使前端出错,也不会影响实际业务 #### 4. **符合实际场景** 📊 - ✅ 后端实际执行时间波动大(5秒-60秒) - ✅ "模拟进度"比"假精确进度"更诚实 - ✅ 用户更关心"有进度反馈"而非"精确到百分之几" **对比其他方案:** | 方案 | 优点 | 缺点 | 推荐度 | |------|------|------|--------| | **方案A:85%上限** | 🟢 流畅 🟢 不卡死 🟢 体验好 | 🟡 不够精确 | ⭐⭐⭐⭐⭐ | | 方案B:95%上限 | 🟢 看起来更快 | 🔴 容易"卡死" | ⭐⭐⭐ | | 方案C:精确进度 | 🟢 精确 | 🔴 需要后端改造 | ⭐⭐⭐⭐ | | 方案D:WebSocket | 🟢 实时 | 🔴 技术复杂度高 | ⭐⭐ | **实施计划:** 1. ✅ 先实施方案A(30分钟) 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 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 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 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个订单 #### 优化前(当前): ``` 订单1:15秒 订单2:15秒 订单3:15秒 ... 订单10:15秒 ------------------- 总计:150秒(2.5分钟) ``` #### 优化后(方案1:批量插入): ``` 订单1:3秒 订单2:3秒 订单3:3秒 ... 订单10:3秒 ------------------- 总计: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 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> 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 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 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 failed : failedOrders) { System.out.println(" - " + failed.get("orderNumber") + ": " + failed.get("error")); } } // 5. 返回结果 Map 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方法,不影响其他功能 - ✅ 单个订单的一键完成和批量订单的定时完成都受益 - ✅ 保持原有事务逻辑和数据一致性