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

49 KiB
Raw Permalink Blame History

工序执行情况表 - 一键完成性能优化方案

📊 当前调用接口分析

一、前端打开对话框阶段

  1. 查询工序路线列表

    • 接口:GET /production/route/list
    • 估计耗时:100-300ms
    • 说明:查询所有可用的工序路线
  2. 查询工序路线详情(循环调用)

    • 接口:GET /production/route/{id}
    • 调用次数:N次每个路线1次
    • 估计耗时:100-200ms × N次 = 300-600ms假设3个路线
    • 说明:为了显示每个路线的工序数量

二、选择工序路线后阶段(handleRouteChange

  1. 查询工序路线详情

    • 接口:GET /production/route/{id}
    • 估计耗时:100-200ms
    • 说明:获取选中路线的工序列表
  2. 批量查询工位信息

    • 接口:GET /masterdata/station/list?processIds=1,2,3,4,5
    • 估计耗时:200-500ms
    • 说明:查询所有工序对应的工位和车间信息

三、提交执行阶段

  1. 执行一键完成
    • 接口: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. 方案1批量插入 - 必须实施
  2. 方案2前端缓存 - 推荐实施
  3. 方案3批量接口 - 可选实施
  4. 方案4数据库索引 - 可选实施
  5. 方案5异步生成 - 暂不推荐

🎯 推荐实施步骤

第一阶段(立即实施):

  1. 实施方案1批量插入工单和报工单
    • 预期提速50-70%
    • 开发时间1-2小时

第二阶段(短期实施):

  1. 实施方案2前端缓存路线详情

    • 预期提速10-20%
    • 开发时间30分钟
  2. 实施方案3后端批量工位查询接口

    • 预期提速5-10%
    • 开发时间1小时

第三阶段(长期优化):

  1. 实施方案4数据库索引优化

    • 预期提速5-10%
    • 执行时间10分钟
  2. 评估方案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 行):

@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.javaautoCompleteSaleOrder 方法
  • 不修改文件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);

// ✅ 第三步收集插入后的IDsaveBatch会自动回填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);

🛡️ 安全性检查清单

确保不影响其他功能:

  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秒

📝 代码实施模板

@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% 的时间节省! 🚀


实施完成标准

  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 属性

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%    │
│                                     │
│    正在生成报工单...                 │
│                                     │
└─────────────────────────────────────┘

进度阶段:

  1. 0-20%:正在准备数据...
  2. 20-40%:正在生成工单...
  3. 40-70%:正在生成报工单...
  4. 70-90%:正在更新订单状态...
  5. 90-95%:即将完成...
  6. 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

  1. 在 data 中添加 progressDialog 对象
  2. 在 template 中添加进度对话框

第二步:修改执行方法

  1. 替换 executeAutoComplete 方法为优化版本
  2. 根据需要选择"基础版"或"优化版"

第三步:测试验证

  1. 测试进度条是否正常显示
  2. 测试进度消息是否正确切换
  3. 测试完成后是否正确关闭
  4. 测试失败时的显示效果

🎨 样式优化(可选)

自定义进度条颜色

<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小时自定义样式和动画效果

推荐先实施基础版,效果好的话再考虑优化!


🎯 最终推荐方案

选择方案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 行):

@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. 定时完成调用了一键完成服务
  2. 优化一键完成后,定时完成会自动受益
  3. ⚠️ 但定时完成是串行处理多个订单,还有优化空间

🎯 优化方案

方案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. 方案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:优化一键完成(批量插入)
    • 这是最重要的优化
    • 定时完成自动受益
    • 完全不影响其他功能

观察效果后决定:

  1. 方案3:添加进度日志

    • 如果定时任务经常执行
    • 建议添加日志便于监控
  2. 方案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. 定时完成基于一键完成实现
  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传递给 generateWorkOrdersbatchGenerateReports 方法,避免重复查询

修改文件

  • 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方法不影响其他功能
  • 单个订单的一键完成和批量订单的定时完成都受益
  • 保持原有事务逻辑和数据一致性