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