49 KiB
工序执行情况表 - 一键完成性能优化方案
📊 当前调用接口分析
一、前端打开对话框阶段
-
查询工序路线列表
- 接口:
GET /production/route/list - 估计耗时:100-300ms
- 说明:查询所有可用的工序路线
- 接口:
-
查询工序路线详情(循环调用)
- 接口:
GET /production/route/{id} - 调用次数:N次(每个路线1次)
- 估计耗时:100-200ms × N次 = 300-600ms(假设3个路线)
- 说明:为了显示每个路线的工序数量
- 接口:
二、选择工序路线后阶段(handleRouteChange)
-
查询工序路线详情
- 接口:
GET /production/route/{id} - 估计耗时:100-200ms
- 说明:获取选中路线的工序列表
- 接口:
-
批量查询工位信息
- 接口:
GET /masterdata/station/list?processIds=1,2,3,4,5 - 估计耗时:200-500ms
- 说明:查询所有工序对应的工位和车间信息
- 接口:
三、提交执行阶段
- 执行一键完成
- 接口:
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:工单和报工单逐个插入
现状:
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:多次查询物料、路线等数据
现状:
// 每次调用都要查询
Material material = materialMapper.selectById(materialId);
Route route = routeMapper.selectRouteById(routeId);
SalOrderEntry entry = salOrderEntryMapper.selectById(entryId);
性能影响:
- 重复查询相同数据
- 每次查询 50-100ms
问题3:工序车间查询逐个执行
现状:
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%
实现方式:
// 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%
实现方式:
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%
新增接口:
/**
* 批量查询工位(按工序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%
需要创建的索引:
-- 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:异步生成(可选方案)⭐
预期提速:用户体验提升
实现方式:
@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(批量插入) - 必须实施 ⭐⭐⭐⭐⭐
- 方案2(前端缓存) - 推荐实施 ⭐⭐⭐
- 方案3(批量接口) - 可选实施 ⭐⭐
- 方案4(数据库索引) - 可选实施 ⭐⭐
- 方案5(异步生成) - 暂不推荐 ⭐
🎯 推荐实施步骤
第一阶段(立即实施):
- ✅ 实施方案1:批量插入工单和报工单
- 预期提速:50-70%
- 开发时间:1-2小时
第二阶段(短期实施):
-
✅ 实施方案2:前端缓存路线详情
- 预期提速:10-20%
- 开发时间:30分钟
-
✅ 实施方案3:后端批量工位查询接口
- 预期提速:5-10%
- 开发时间:1小时
第三阶段(长期优化):
-
⏰ 实施方案4:数据库索引优化
- 预期提速:5-10%
- 执行时间:10分钟
-
⏰ 评估方案5:是否需要异步生成
- 取决于用户反馈
💡 其他建议
-
监控性能瓶颈
- 在关键方法中添加耗时日志
- 记录每个步骤的执行时间
- 便于后续精准优化
-
压力测试
- 测试并发多个订单时的性能
- 测试大量工序(10+)的情况
- 确保优化后不影响稳定性
-
用户提示优化
- 显示进度百分比
- 显示当前执行的步骤
- 提供取消操作选项
📝 备注
- 本方案基于当前代码分析得出
- 实际耗时可能因服务器性能、数据量等因素有所差异
- 建议先实施方案1,观察效果后再决定是否实施其他方案
方案一:批量插入工单和报工单 - 详细实施方案
⚠️ 核心原则:只修改一键完成功能,不影响其他功能
🎯 实施目标
- ✅ 将工单和报工单从逐个插入改为批量插入
- ✅ 提速 50-70%(10-20秒 → 2-5秒)
- ❌ 绝对不能影响其他地方使用工单和报工单的功能
📋 当前代码分析
当前位置:
yjh-mes/src/main/java/cn/sourceplan/production/service/impl/AutoCompleteServiceImpl.java
当前实现(第 152-607 行):
@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:
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
import java.util.ArrayList;
检查 WorkOrderService 和 ReportService 是否继承 IService:
// 如果继承了 IService,就可以使用 saveBatch 方法
public interface IWorkOrderService extends IService<WorkOrder> {
// ...
}
public interface IReportService extends IService<Report> {
// ...
}
步骤2:修改工单批量插入逻辑
原代码(需要找到类似的循环):
// 大约在第 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());
}
优化后代码(批量插入):
// ✅ 第一步:收集所有工单到列表
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);
// ✅ 第三步:收集插入后的ID(saveBatch会自动回填ID)
for (WorkOrder workOrder : workOrderList) {
workOrderIds.add(workOrder.getId());
}
步骤3:修改报工单批量插入逻辑
原代码(需要找到类似的循环):
// 大约在第 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); // ❌ 逐个插入
}
优化后代码(批量插入):
// ✅ 第一步:收集所有报工单到列表
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);
🛡️ 安全性检查清单
✅ 确保不影响其他功能:
-
只修改 autoCompleteSaleOrder 方法内部的代码
- ❌ 不修改 WorkOrderService 的其他方法
- ❌ 不修改 ReportService 的其他方法
- ❌ 不修改 Mapper 的 insert 方法
-
保持方法签名不变
- ✅ 方法名:
autoCompleteSaleOrder - ✅ 参数:
AutoCompleteDTO dto - ✅ 返回值:
AjaxResult - ✅ 事务注解:
@Transactional
- ✅ 方法名:
-
保持数据完整性
- ✅ 工单和报工单的数量不变
- ✅ 工单和报工单的关联关系不变
- ✅ 字段值设置逻辑不变
- ✅ 事务回滚机制不变
-
保持业务逻辑不变
- ✅ 工单编号生成逻辑不变
- ✅ 时间计算逻辑不变
- ✅ 状态更新逻辑不变
- ✅ 返回结果格式不变
🔍 完整的修改前后对比
修改前的流程:
开始
↓
循环处理工序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秒)
📝 代码实施模板
@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. 保持字段设置逻辑完全一致
// ❌ 错误:修改了字段设置逻辑
workOrder.setStatus("A"); // 原来是 "0"
// ✅ 正确:完全复制原有的字段设置代码
workOrder.setStatus("0"); // 保持不变
2. 保持工单和报工单的对应关系
// ✅ 确保工序1的工单对应工序1的报工单
for (int i = 0; i < processConfigs.size(); i++) {
Report report = new Report();
report.setWorkorderId(workOrderIds.get(i)); // 使用相同索引
// ...
}
3. 保持事务完整性
// ✅ 事务注解保持不变
@Transactional(rollbackFor = Exception.class, timeout = 600)
// ✅ 如果批量插入失败,整个事务会回滚
// ✅ 不会出现只插入了部分数据的情况
4. 添加性能监控日志
// ✅ 添加耗时日志,便于验证优化效果
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% 的时间节省! 🚀
✅ 实施完成标准
- ✅ 代码编译通过,没有语法错误
- ✅ 一键完成功能测试通过
- ✅ 控制台日志显示批量插入耗时显著减少
- ✅ 其他功能回归测试通过
- ✅ 没有新增的异常或错误日志
🎯 下一步行动
- 确认当前 WorkOrderService 和 ReportService 是否继承了 IService
- 如果没有,先让它们继承 IService(这不会影响现有功能)
- 按照上述模板修改 autoCompleteSaleOrder 方法
- 编译并测试
- 观察性能提升效果
需要我开始实施吗?
前端进度显示优化方案
📊 目标
在一键完成执行过程中,显示实时进度百分比,提升用户体验。
🎯 实现方案(推荐:方案2 - 模拟进度)
方案1:真实进度(需要后端配合)⭐⭐⭐⭐⭐
优点: 显示真实进度,准确可靠
缺点: 需要后端支持(WebSocket/SSE/轮询)
开发时间: 4-6小时
实现步骤:
- 后端改为异步执行
- 后端通过 WebSocket 推送进度
- 前端监听进度更新
- 显示进度条和百分比
方案2:模拟进度(推荐)⭐⭐⭐⭐
优点: 无需后端修改,实施简单
缺点: 进度是估算的,不是真实进度
开发时间: 30分钟
实现方式: 根据预估的总时间(优化后约 3-5秒),显示一个平滑增长的进度条。
🚀 方案2实施代码(推荐)
修改位置:
mes-ui/src/views/mes/statement/saleOrderExecution/index.vue
1. 添加 data 属性
data() {
return {
// ... 现有属性 ...
// 进度条相关
progressDialog: {
visible: false,
percentage: 0,
status: 'success',
message: '正在准备...'
}
}
}
2. 添加进度对话框模板
<!-- 在 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 方法
/** 执行一键完成 */
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% │
│ │
│ 正在生成报工单... │
│ │
└─────────────────────────────────────┘
进度阶段:
- 0-20%:正在准备数据...
- 20-40%:正在生成工单...
- 40-70%:正在生成报工单...
- 70-90%:正在更新订单状态...
- 90-95%:即将完成...
- 100%:完成!
🎯 优化版本(更精确的进度显示)
如果想要更精确的进度显示,可以根据工序数量动态计算:
/** 执行一键完成(优化版) */
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
- 在 data 中添加
progressDialog对象 - 在 template 中添加进度对话框
第二步:修改执行方法
- 替换
executeAutoComplete方法为优化版本 - 根据需要选择"基础版"或"优化版"
第三步:测试验证
- 测试进度条是否正常显示
- 测试进度消息是否正确切换
- 测试完成后是否正确关闭
- 测试失败时的显示效果
🎨 样式优化(可选)
自定义进度条颜色
<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>
添加动画效果
<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小时):自定义样式和动画效果 ⭐⭐⭐
推荐先实施基础版,效果好的话再考虑优化!
🎯 最终推荐方案
⭐ 选择方案A:85%上限的模拟进度条
选择理由:
1. 最佳用户体验 🌟
- ✅ 进度条始终流畅移动,不会"卡死"
- ✅ 85%上限留足缓冲,不会给用户"快完成了却还要等很久"的焦虑感
- ✅ 即使后端超时,用户也能看到明确的进度状态
2. 技术实现简单 🛠️
- ✅ 纯前端实现,无需修改后端
- ✅ 代码量少,维护成本低
- ✅ 实施时间短(30分钟内)
3. 风险最低 🛡️
- ✅ 不影响现有功能
- ✅ 不涉及后端逻辑修改
- ✅ 即使前端出错,也不会影响实际业务
4. 符合实际场景 📊
- ✅ 后端实际执行时间波动大(5秒-60秒)
- ✅ "模拟进度"比"假精确进度"更诚实
- ✅ 用户更关心"有进度反馈"而非"精确到百分之几"
对比其他方案:
| 方案 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|
| 方案A:85%上限 | 🟢 流畅 🟢 不卡死 🟢 体验好 | 🟡 不够精确 | ⭐⭐⭐⭐⭐ |
| 方案B:95%上限 | 🟢 看起来更快 | 🔴 容易"卡死" | ⭐⭐⭐ |
| 方案C:精确进度 | 🟢 精确 | 🔴 需要后端改造 | ⭐⭐⭐⭐ |
| 方案D:WebSocket | 🟢 实时 | 🔴 技术复杂度高 | ⭐⭐ |
实施计划:
- ✅ 先实施方案A(30分钟)
- 📊 观察用户反馈
- 🔄 如果需要更精确,再考虑方案C(需要1-2天)
预期效果:
- 🎯 用户能看到实时进度反馈
- ⏱️ 85%前进度流畅增长(约45秒)
- ⏳ 85%-100%等待实际完成(5-15秒)
- ✅ 完成后立即显示100%成功状态
定时完成功能优化方案
🔍 定时完成与一键完成的关系分析
代码位置:
yjh-mes/src/main/java/cn/sourceplan/production/service/impl/TimedCompleteServiceImpl.java
当前实现(第 118-178 行):
@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行
AjaxResult result = autoCompleteService.autoCompleteSaleOrder(autoCompleteDTO);
结论:
- ✅ 定时完成调用了一键完成服务
- ✅ 优化一键完成后,定时完成会自动受益
- ⚠️ 但定时完成是串行处理多个订单,还有优化空间
🎯 优化方案
方案1:优化一键完成(已覆盖)⭐⭐⭐⭐⭐
说明: 这是主要优化,定时完成会自动受益
优化前:
- 一键完成处理1个订单:10-20秒
- 定时完成处理N个订单:10-20秒 × N
优化后(批量插入):
- 一键完成处理1个订单:2-5秒
- 定时完成处理N个订单:2-5秒 × N
效果:
- ✅ 单订单速度提升 50-70%
- ✅ 无需修改定时完成代码
- ✅ 完全不影响其他功能
方案2:改进事务粒度(可选)⭐⭐⭐
说明: 当前定时完成使用一个大事务,可能导致锁等待
当前问题:
@Transactional(rollbackFor = Exception.class) // ❌ 整个批量执行在一个事务中
public AjaxResult batchExecute(BatchExecuteDTO dto) {
for (OverdueOrderVO order : orders) {
autoCompleteService.autoCompleteSaleOrder(autoCompleteDTO);
}
}
问题分析:
- 处理10个订单可能需要 20-50秒
- 整个过程在一个事务中,锁持有时间长
- 如果第9个订单失败,前8个也会回滚
优化方案:
// ❌ 移除方法级别的 @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:添加进度推送(可选)⭐⭐
说明: 定时任务可能处理多个订单,添加进度反馈
实现方式:
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(优化一键完成)
- ✅ 完全不修改 TimedCompleteServiceImpl
- ✅ 只优化 AutoCompleteServiceImpl
- ✅ 定时完成自动受益
-
方案2(改进事务粒度)
- ⚠️ 需要移除 batchExecute 的 @Transactional
- ✅ 每个订单的处理仍在事务中(由 autoCompleteSaleOrder 保证)
- ⚠️ 需要测试日志记录是否正常
-
方案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:优化一键完成(批量插入)
- 这是最重要的优化
- 定时完成自动受益
- 完全不影响其他功能
观察效果后决定:
-
⏰ 方案3:添加进度日志
- 如果定时任务经常执行
- 建议添加日志便于监控
-
⏰ 方案2:事务粒度优化
- 如果发现事务锁等待问题
- 再考虑实施此方案
📝 实施代码示例
方案3:添加进度日志(完整代码)
/**
* 批量执行自动完成
*/
@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(批量插入优化)
- 第二步:观察效果,测试验证
- 第三步:根据需要实施方案3(进度日志)
预期效果:定时完成处理10个订单从2.5分钟降到0.5分钟! 🚀
✅ 实施记录
方案1:批量查询优化(已完成)
实施时间:2025-11-03
优化内容:
- ✅ 批量查询工序路线:在
autoCompleteSaleOrder方法开始时,一次性查询所有工序的持续时间,构建processDurationMap - ✅ 批量查询车间信息:一次性查询所有需要的车间,构建
workshopMap - ✅ 批量查询工位信息:一次性查询所有需要的工位,构建
stationMap - ✅ 批量查询设备信息:一次性查询所有需要的设备,构建
equipmentMap - ✅ 传递缓存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方法,不影响其他功能
- ✅ 单个订单的一键完成和批量订单的定时完成都受益
- ✅ 保持原有事务逻辑和数据一致性