Files
MES/yawei-mes/.tasks/2025-10-30_工序执行情况表重做.md
2026-04-02 10:39:03 +08:00

1675 lines
49 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 工序执行情况表重做 - 完整实施方案
> **文档版本:** v3.0(销售订单版)
> **创建日期:** 2025-10-30
> **适用场景:** 销售订单 → 工单 → 工序 → 报工单 四级展示
---
## 目录
1. [需求说明](#一需求说明)
2. [数据库设计](#二数据库设计)
3. [后端实现](#三后端实现)
4. [前端实现](#四前端实现)
5. [性能优化](#五性能优化)
6. [实施步骤](#六实施步骤)
---
## 一、需求说明
### 1.1 核心需求
- ✅ 以**销售订单为主数据**展示
- ✅ 订单头部显示**整体工序进度条**(汇总所有工单的工序进度)
- ✅ 订单编号左侧显示**小三角展开按钮**(▶/▼),点击头部即可展开/收起
- ✅ 展开后显示订单关联的所有**工单表格**
- ✅ 每个工单显示**工序进度条**(列出所有工序,用颜色区分状态)
- ✅ 工单显示**完成率进度条**(可视化显示百分比)
- ✅ 工单后有**报工单按钮**,点击查看该工单的所有报工单
- ✅ 未报工的显示**"未完成"**状态
### 1.2 页面布局
```
销售订单卡片
├── 【▶】订单信息(订单号、客户、产品、数量、日期、工单数、完成率)← 点击展开
├── 整体工序进度条:[吹膜 80%✓] → [涂胶 40%▶] → [分切 0%⏱] → [检验 0%⏱]
│ └── (汇总该订单下所有工单的平均工序完成率)
└── 【▼】展开后显示工单表格:
├─────┬────────┬──────┬────────────────────────────────┬─────────┬────────┬────────┐
│ 状态 │ 工单号 │ 批次 │ 工序进度 │ 当前工序 │ 完成率 │ 操作 │
├─────┼────────┼──────┼────────────────────────────────┼─────────┼────────┼────────┤
│生产中│ WO001 │ B001 │ [吹膜✓] → [涂胶▶] → [分切⏱] │ 涂胶 │ ████ 60%│【报工单】│
│待排产│ WO002 │ B002 │ [吹膜⏱] → [涂胶⏱] → [分切⏱] │ 未开始 │ ░░░░ 0% │【报工单】│
└─────┴────────┴──────┴────────────────────────────────┴─────────┴────────┴────────┘
```
### 1.2.1 UI交互说明
- **小三角展开按钮**:位于订单编号左侧,▶表示收起,▼表示展开
- **整体工序进度条**
- 📊 显示在订单头部下方(红色圈出区域)
- 🎨 用颜色区分状态:
-**绿色100%**:工序已完成
- 🔵 **蓝色1-99%**:工序进行中
-**灰色0%**:工序未开始
- 📈 显示每个工序的平均完成率
- 🔄 自动计算:取该订单下所有工单的同名工序完成率的平均值
- **工单表格**
- 点击订单头部展开,显示为表格形式
- 每行一个工单,包含状态、编号、批次、工序进度、当前工序、完成率、操作
- 工序进度条采用 `el-steps` 组件,横向展示
- 完成率使用 `el-progress` 进度条可视化展示
### 1.2 性能要求
- ✅ 工单列表查询20条**< 100ms**
- 工序列表查询8个工序**< 50ms**
- 报工单查询20条**< 50ms**
- 支持10万级数据不降速
### 1.3 技术方案
- **数据库优化**新增缓存字段 + 完善索引
- **查询策略**零子查询纯字段查询
- **加载策略**懒加载按需获取数据
- **更新机制**触发器实时更新 + 定时任务兜底
---
## 二、数据库设计
### 2.1 表结构关系
```
sal_order (销售订单主表) ← 新的主数据!
├── id 主键
├── number 订单号
├── customer_name 客户名称
├── sale_date 销售日期
└── status 状态
sal_order_entry (销售订单子表)
├── id 主键
├── main_id 订单ID(外键)
├── material_name 产品名称
├── quantity 数量
└── unit_name 单位
pro_workorder (生产工单)
├── id 主键
├── number 工单编号
├── source_info JSON(包含saleOrderId)
├── material_name 产品名称
├── current_process_name 【新增】当前工序名
├── current_process_sort 【新增】当前工序序号
├── total_process_count 【新增】总工序数
├── overall_completion_rate 【新增】整体完成率
└── last_report_time 【新增】最后报工时间
pro_workorder_entry (工序)
├── id 主键
├── workorder_id 工单ID(外键)
├── process_name 工序名称
├── process_sort 工序序号
├── reported_quantity 【新增】已报工数量
└── completion_rate 【新增】完成率
pro_report (报工单)
├── id 主键
├── work_order_entry_id 工序ID(外键)
├── report_user_name 报工人
└── report_quantity 报工数量
```
### 2.2 关联关系
```sql
-- 销售订单 → 工单通过source_info JSON字段
pro_workorder.source_info 包含:
{
"saleOrderId": 123,
"saleOrderEntryId": 456
}
-- 工单 → 工序
pro_workorder_entry.workorder_id = pro_workorder.id
-- 工序 → 报工单
pro_report.work_order_entry_id = pro_workorder_entry.id
```
### 2.2 新增字段说明
**pro_workorder 表新增字段:**
```sql
current_process_name -- 当前执行工序(避免子查询)
current_process_sort -- 当前工序序号(用于进度条)
total_process_count -- 总工序数避免COUNT
completed_process_count -- 已完成工序数(统计用)
overall_completion_rate -- 整体完成率避免AVG
last_report_time -- 最后报工时间(排序用)
```
**pro_workorder_entry 表新增字段:**
```sql
reported_quantity -- 已报工数量避免SUM
completion_rate -- 完成率(避免除法计算)
last_report_time -- 最后报工时间(排序用)
```
### 2.3 索引设计
```sql
-- 工单主表索引
CREATE INDEX idx_status_pro_status_create ON pro_workorder (status, pro_status, create_time);
CREATE INDEX idx_material_name ON pro_workorder (material_name(50));
CREATE INDEX idx_batch_number ON pro_workorder (batch_number(50));
-- 工序子表索引
CREATE INDEX idx_workorder_type ON pro_workorder_entry (workorder_id, type);
CREATE INDEX idx_workorder_sort ON pro_workorder_entry (workorder_id, process_sort);
-- 报工单表索引
CREATE INDEX idx_entry_status_time ON pro_report (work_order_entry_id, status, report_time);
```
**SQL实现** 详见 `2025-10-30_工序执行情况表重做.sql`
---
## 三、后端实现
### 3.1 项目结构
```
cn.sourceplan.statement
├── controller
│ └── WorkOrderExecutionController.java 控制器
├── domain
│ ├── query
│ │ └── WorkOrderExecutionQuery.java 查询参数
│ └── vo
│ ├── WorkOrderExecutionVO.java 工单VO
│ ├── ProcessExecutionVO.java 工序VO
│ └── ReportVO.java 报工单VO
├── mapper
│ ├── WorkOrderExecutionMapper.java Mapper接口
│ └── WorkOrderExecutionMapper.xml SQL映射
└── service
├── IWorkOrderExecutionService.java Service接口
└── impl
└── WorkOrderExecutionServiceImpl.java Service实现
```
### 3.2 核心接口
#### 3.2.1 查询销售订单列表(主接口)
```
GET /reports/production/saleOrderExecution
参数: customerName, materialName, saleDate, pageNum, pageSize
返回: 分页销售订单列表(不含工单)
```
#### 3.2.2 查询订单的工单列表(含工序)
```
GET /reports/production/saleOrderWorkOrders/{saleOrderId}
参数: saleOrderId
返回: 该订单的所有工单(含工序进度)
```
#### 3.2.3 查询工单的报工单列表
```
GET /reports/production/workOrderReports/{workOrderId}
参数: workOrderId
返回: 该工单所有工序的报工单
```
### 3.3 代码实现
#### 3.3.1 Query对象
```java
package cn.sourceplan.statement.domain.query;
import lombok.Data;
import java.util.Date;
/**
* 工单执行情况查询参数
*
* @author ruoyi
* @date 2025-10-30
*/
@Data
public class WorkOrderExecutionQuery {
/** 产品名称(模糊查询) */
private String materialName;
/** 批次号(模糊查询) */
private String batchNumber;
/** 生产状态A-待排产 B-生产中 D-已完成) */
private String proStatus;
/** 开始日期 */
private Date dateFrom;
/** 结束日期 */
private Date dateTo;
/** 分页参数-当前页 */
private Integer pageNum;
/** 分页参数-每页大小 */
private Integer pageSize;
}
```
#### 3.3.2 VO对象
```java
package cn.sourceplan.statement.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
* 工单执行情况VO
*
* @author ruoyi
* @date 2025-10-30
*/
@Data
public class WorkOrderExecutionVO {
/** 工单ID */
private Long id;
/** 工单编号 */
private String number;
/** 产品名称 */
private String materialName;
/** 规格型号 */
private String specification;
/** 生产数量 */
private BigDecimal quantity;
/** 批次号 */
private String batchNumber;
/** 生产状态 */
private String proStatus;
/** 生产状态文本 */
private String proStatusText;
/** 开始生产日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
private Date beginProDate;
/** 计划完成日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
private Date planFinishDate;
/** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
// ========== 统计字段 ==========
/** 当前工序名称 */
private String currentProcess;
/** 当前工序序号 */
private Integer currentProcessSort;
/** 总工序数 */
private Integer totalProcessCount;
/** 已完成工序数 */
private Integer completedProcessCount;
/** 整体完成率 */
private String completionRate;
/** 最后报工时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date lastReportTime;
}
```
```java
package cn.sourceplan.statement.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
* 工序执行情况VO
*
* @author ruoyi
* @date 2025-10-30
*/
@Data
public class ProcessExecutionVO {
/** 工序ID */
private Long id;
/** 工序名称 */
private String processName;
/** 工序序号 */
private Integer processSort;
/** 计划数量 */
private BigDecimal planQuantity;
/** 已报工数量 */
private BigDecimal reportedQuantity;
/** 完成率 */
private BigDecimal completionRate;
/** 状态(已完成/进行中/未开始) */
private String status;
/** 最后报工时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date lastReportTime;
/** 报工单数量 */
private Integer reportCount;
}
```
```java
package cn.sourceplan.statement.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
* 报工单VO
*
* @author ruoyi
* @date 2025-10-30
*/
@Data
public class ReportVO {
/** 报工单ID */
private Long id;
/** 报工单号 */
private String number;
/** 报工人 */
private String reportUserName;
/** 报工时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date reportTime;
/** 报工数量 */
private BigDecimal reportQuantity;
/** 合格数量 */
private BigDecimal qualifiedQuantity;
/** 不合格数量 */
private BigDecimal unqualifiedQuantity;
/** 车间名称 */
private String workshopName;
/** 工位名称 */
private String stationName;
/** 状态 */
private String status;
/** 状态文本 */
private String statusText;
/** 备注 */
private String remark;
/** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
```
#### 3.3.3 Mapper接口
```java
package cn.sourceplan.statement.mapper;
import cn.sourceplan.statement.domain.query.WorkOrderExecutionQuery;
import cn.sourceplan.statement.domain.vo.ProcessExecutionVO;
import cn.sourceplan.statement.domain.vo.ReportVO;
import cn.sourceplan.statement.domain.vo.WorkOrderExecutionVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 工单执行情况 Mapper接口
*
* @author ruoyi
* @date 2025-10-30
*/
@Mapper
public interface WorkOrderExecutionMapper {
/**
* 查询工单执行情况列表
*
* @param query 查询参数
* @return 工单执行情况列表
*/
List<WorkOrderExecutionVO> selectWorkOrderExecutionList(WorkOrderExecutionQuery query);
/**
* 根据工单ID查询工序列表
*
* @param workOrderId 工单ID
* @return 工序列表
*/
List<ProcessExecutionVO> selectProcessListByWorkOrderId(@Param("workOrderId") Long workOrderId);
/**
* 根据工序ID查询报工单列表
*
* @param workOrderEntryId 工序ID
* @return 报工单列表
*/
List<ReportVO> selectReportListByProcessId(@Param("workOrderEntryId") Long workOrderEntryId);
}
```
#### 3.3.4 Mapper XML
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.sourceplan.statement.mapper.WorkOrderExecutionMapper">
<!-- 查询工单执行情况列表 -->
<select id="selectWorkOrderExecutionList" parameterType="cn.sourceplan.statement.domain.query.WorkOrderExecutionQuery"
resultType="cn.sourceplan.statement.domain.vo.WorkOrderExecutionVO">
SELECT
pw.id,
pw.number,
pw.material_name AS materialName,
pw.specification,
pw.quantity,
pw.batch_number AS batchNumber,
pw.pro_status AS proStatus,
CASE pw.pro_status
WHEN 'A' THEN '待排产'
WHEN 'B' THEN '生产中'
WHEN 'D' THEN '已完成'
ELSE '未知'
END AS proStatusText,
pw.begin_pro_date AS beginProDate,
pw.plan_finish_date AS planFinishDate,
pw.create_time AS createTime,
pw.current_process_name AS currentProcess,
pw.current_process_sort AS currentProcessSort,
pw.total_process_count AS totalProcessCount,
pw.completed_process_count AS completedProcessCount,
CONCAT(IFNULL(pw.overall_completion_rate, 0), '%') AS completionRate,
pw.last_report_time AS lastReportTime
FROM pro_workorder pw
WHERE pw.status = 'A'
<if test="materialName != null and materialName != ''">
AND pw.material_name LIKE CONCAT('%', #{materialName}, '%')
</if>
<if test="batchNumber != null and batchNumber != ''">
AND pw.batch_number LIKE CONCAT('%', #{batchNumber}, '%')
</if>
<if test="proStatus != null and proStatus != ''">
AND pw.pro_status = #{proStatus}
</if>
<if test="dateFrom != null">
AND pw.begin_pro_date &gt;= #{dateFrom}
</if>
<if test="dateTo != null">
AND pw.begin_pro_date &lt;= #{dateTo}
</if>
ORDER BY
FIELD(pw.pro_status, 'B', 'A', 'D'),
pw.create_time DESC
</select>
<!-- 查询工序列表 -->
<select id="selectProcessListByWorkOrderId" resultType="cn.sourceplan.statement.domain.vo.ProcessExecutionVO">
SELECT
pwe.id,
pwe.process_name AS processName,
pwe.process_sort AS processSort,
pwe.report_quantity AS planQuantity,
IFNULL(pwe.reported_quantity, 0) AS reportedQuantity,
IFNULL(pwe.completion_rate, 0) AS completionRate,
CASE
WHEN IFNULL(pwe.completion_rate, 0) >= 100 THEN '已完成'
WHEN IFNULL(pwe.completion_rate, 0) > 0 THEN '进行中'
ELSE '未开始'
END AS status,
pwe.last_report_time AS lastReportTime,
(
SELECT COUNT(*)
FROM pro_report pr
WHERE pr.work_order_entry_id = pwe.id
AND pr.status = 'A'
) AS reportCount
FROM pro_workorder_entry pwe
WHERE pwe.workorder_id = #{workOrderId}
AND pwe.type = 'report'
ORDER BY pwe.process_sort ASC
</select>
<!-- 查询报工单列表 -->
<select id="selectReportListByProcessId" resultType="cn.sourceplan.statement.domain.vo.ReportVO">
SELECT
pr.id,
pr.number,
pr.report_user_name AS reportUserName,
pr.report_time AS reportTime,
pr.report_quantity AS reportQuantity,
IFNULL(pr.qualified_quantity, 0) AS qualifiedQuantity,
IFNULL(pr.unqualified_quantity, 0) AS unqualifiedQuantity,
pr.workshop_name AS workshopName,
pr.station_name AS stationName,
pr.status,
CASE pr.status
WHEN 'A' THEN '正常'
WHEN 'D' THEN '已删除'
ELSE '未知'
END AS statusText,
pr.remark,
pr.create_time AS createTime
FROM pro_report pr
WHERE pr.work_order_entry_id = #{workOrderEntryId}
AND pr.status = 'A'
ORDER BY pr.report_time DESC
LIMIT 100
</select>
</mapper>
```
#### 3.3.5 Service接口
```java
package cn.sourceplan.statement.service;
import cn.sourceplan.statement.domain.query.WorkOrderExecutionQuery;
import cn.sourceplan.statement.domain.vo.ProcessExecutionVO;
import cn.sourceplan.statement.domain.vo.ReportVO;
import cn.sourceplan.statement.domain.vo.WorkOrderExecutionVO;
import java.util.List;
/**
* 工单执行情况 Service接口
*
* @author ruoyi
* @date 2025-10-30
*/
public interface IWorkOrderExecutionService {
/**
* 查询工单执行情况列表
*
* @param query 查询参数
* @return 工单执行情况列表
*/
List<WorkOrderExecutionVO> selectWorkOrderExecutionList(WorkOrderExecutionQuery query);
/**
* 根据工单ID查询工序列表
*
* @param workOrderId 工单ID
* @return 工序列表
*/
List<ProcessExecutionVO> selectProcessListByWorkOrderId(Long workOrderId);
/**
* 根据工序ID查询报工单列表
*
* @param workOrderEntryId 工序ID
* @return 报工单列表
*/
List<ReportVO> selectReportListByProcessId(Long workOrderEntryId);
}
```
#### 3.3.6 Service实现
```java
package cn.sourceplan.statement.service.impl;
import cn.sourceplan.statement.domain.query.WorkOrderExecutionQuery;
import cn.sourceplan.statement.domain.vo.ProcessExecutionVO;
import cn.sourceplan.statement.domain.vo.ReportVO;
import cn.sourceplan.statement.domain.vo.WorkOrderExecutionVO;
import cn.sourceplan.statement.mapper.WorkOrderExecutionMapper;
import cn.sourceplan.statement.service.IWorkOrderExecutionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 工单执行情况 Service实现
*
* @author ruoyi
* @date 2025-10-30
*/
@Service
public class WorkOrderExecutionServiceImpl implements IWorkOrderExecutionService {
@Autowired
private WorkOrderExecutionMapper workOrderExecutionMapper;
/**
* 查询工单执行情况列表
*
* @param query 查询参数
* @return 工单执行情况列表
*/
@Override
public List<WorkOrderExecutionVO> selectWorkOrderExecutionList(WorkOrderExecutionQuery query) {
return workOrderExecutionMapper.selectWorkOrderExecutionList(query);
}
/**
* 根据工单ID查询工序列表
*
* @param workOrderId 工单ID
* @return 工序列表
*/
@Override
public List<ProcessExecutionVO> selectProcessListByWorkOrderId(Long workOrderId) {
return workOrderExecutionMapper.selectProcessListByWorkOrderId(workOrderId);
}
/**
* 根据工序ID查询报工单列表
*
* @param workOrderEntryId 工序ID
* @return 报工单列表
*/
@Override
public List<ReportVO> selectReportListByProcessId(Long workOrderEntryId) {
return workOrderExecutionMapper.selectReportListByProcessId(workOrderEntryId);
}
}
```
#### 3.3.7 Controller
```java
package cn.sourceplan.statement.controller;
import cn.sourceplan.common.annotation.Log;
import cn.sourceplan.common.core.controller.BaseController;
import cn.sourceplan.common.core.domain.AjaxResult;
import cn.sourceplan.common.core.page.TableDataInfo;
import cn.sourceplan.common.enums.BusinessType;
import cn.sourceplan.statement.domain.query.WorkOrderExecutionQuery;
import cn.sourceplan.statement.domain.vo.ProcessExecutionVO;
import cn.sourceplan.statement.domain.vo.ReportVO;
import cn.sourceplan.statement.domain.vo.WorkOrderExecutionVO;
import cn.sourceplan.statement.service.IWorkOrderExecutionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 工单执行情况 Controller
*
* @author ruoyi
* @date 2025-10-30
*/
@RestController
@RequestMapping("/reports/production")
public class WorkOrderExecutionController extends BaseController {
@Autowired
private IWorkOrderExecutionService workOrderExecutionService;
/**
* 查询工单执行情况列表
*/
@Log(title = "工单执行情况", businessType = BusinessType.QUERY)
@GetMapping("/workOrderExecution")
public TableDataInfo list(WorkOrderExecutionQuery query) {
startPage();
List<WorkOrderExecutionVO> list = workOrderExecutionService.selectWorkOrderExecutionList(query);
return getDataTable(list);
}
/**
* 根据工单ID查询工序列表
*/
@Log(title = "工序执行情况", businessType = BusinessType.QUERY)
@GetMapping("/workOrderProcesses/{workOrderId}")
public AjaxResult getProcesses(@PathVariable("workOrderId") Long workOrderId) {
List<ProcessExecutionVO> list = workOrderExecutionService.selectProcessListByWorkOrderId(workOrderId);
return AjaxResult.success(list);
}
/**
* 根据工序ID查询报工单列表
*/
@Log(title = "工序报工单", businessType = BusinessType.QUERY)
@GetMapping("/processReports/{workOrderEntryId}")
public AjaxResult getReports(@PathVariable("workOrderEntryId") Long workOrderEntryId) {
List<ReportVO> list = workOrderExecutionService.selectReportListByProcessId(workOrderEntryId);
return AjaxResult.success(list);
}
}
```
---
## 四、前端实现
### 4.1 API接口
创建文件`mes-ui/src/api/mes/statement/workOrderExecution.js`
```javascript
import request from '@/utils/request'
/**
* 查询工单执行情况列表
*/
export function listWorkOrderExecution(query) {
return request({
url: '/reports/production/workOrderExecution',
method: 'get',
params: query
})
}
/**
* 根据工单ID查询工序列表
*/
export function listProcessesByWorkOrder(workOrderId) {
return request({
url: `/reports/production/workOrderProcesses/${workOrderId}`,
method: 'get'
})
}
/**
* 根据工序ID查询报工单列表
*/
export function listReportsByProcess(workOrderEntryId) {
return request({
url: `/reports/production/processReports/${workOrderEntryId}`,
method: 'get'
})
}
```
### 4.2 核心功能说明
#### 4.2.1 整体工序进度条
在订单头部显示所有工单的工序进度汇总实现逻辑
1. **数据加载时机**
- 页面初始加载时自动为前5个订单加载工序数据优化首屏体验
- 分页切换时自动为新页面的前5个订单加载工序数据
- 用户展开订单时如果未加载则加载工序数据
2. **工序进度计算**
```javascript
// 收集所有工单的工序,按工序名称分组
calculateOverallProcess(order) {
const processMap = new Map()
order.workOrderList.forEach(workOrder => {
workOrder.processList.forEach(process => {
if (!processMap.has(process.processName)) {
processMap.set(process.processName, {
processName: process.processName,
sort: process.sort,
completionRates: []
})
}
processMap.get(process.processName).completionRates.push(
parseFloat(process.completionRate) || 0
)
})
})
// 计算每个工序的平均完成率
order.overallProcessList = Array.from(processMap.values())
.sort((a, b) => a.sort - b.sort)
.map(item => ({
processName: item.processName,
sort: item.sort,
avgCompletionRate: Math.round(
item.completionRates.reduce((sum, rate) => sum + rate, 0)
/ item.completionRates.length
)
}))
}
```
3. **进度条状态**
- ✅ **绿色 success**`avgCompletionRate >= 100`
- 🔵 **蓝色 process**`0 < avgCompletionRate < 100`
- ⚪ **灰色 wait**`avgCompletionRate == 0`
4. **样式设计**
```css
.overall-process-bar {
margin-top: 15px;
padding: 15px 20px;
background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
border-radius: 8px;
border: 1px solid #EBEEF5;
}
```
#### 4.2.2 展开/收起交互
- 小三角图标:`el-icon-caret-right`(收起)/ `el-icon-caret-bottom`(展开)
- 点击整个订单头部区域即可展开/收起
- 鼠标悬停时背景色变化,提供视觉反馈
#### 4.2.3 工单表格展示
- 使用 `el-table` 组件展示工单列表
- 工序进度列:使用 `el-steps` 横向步骤条
- 完成率列:使用 `el-progress` 进度条,颜色动态变化
### 4.3 页面组件
创建文件:`mes-ui/src/views/mes/statement/saleOrderExecution/index.vue`
```vue
<template>
<div class="app-container">
<!-- 搜索条件 -->
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch">
<el-form-item label="产品名称" prop="materialName">
<el-input
v-model="queryParams.materialName"
placeholder="请输入产品名称"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="批次号" prop="batchNumber">
<el-input
v-model="queryParams.batchNumber"
placeholder="请输入批次号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="生产状态" prop="proStatus">
<el-select v-model="queryParams.proStatus" placeholder="请选择生产状态" clearable>
<el-option label="待排产" value="A" />
<el-option label="生产中" value="B" />
<el-option label="已完成" value="D" />
</el-select>
</el-form-item>
<el-form-item label="开始日期" prop="dateFrom">
<el-date-picker
v-model="queryParams.dateFrom"
type="date"
placeholder="选择日期"
value-format="yyyy-MM-dd"
/>
</el-form-item>
<el-form-item label="结束日期" prop="dateTo">
<el-date-picker
v-model="queryParams.dateTo"
type="date"
placeholder="选择日期"
value-format="yyyy-MM-dd"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 工单列表 -->
<div v-for="workOrder in workOrderList" :key="workOrder.id" class="work-order-card">
<el-card shadow="hover">
<!-- 工单头部 -->
<div slot="header" class="card-header">
<div class="header-left">
<el-tag :type="getStatusType(workOrder.proStatus)" size="medium">
{{ workOrder.proStatusText }}
</el-tag>
<span class="work-order-number">{{ workOrder.number }}</span>
<span class="material-name">{{ workOrder.materialName }}</span>
<span class="specification">{{ workOrder.specification }}</span>
</div>
<div class="header-right">
<span class="batch-number">批次:{{ workOrder.batchNumber }}</span>
<span class="quantity">数量:{{ workOrder.quantity }}</span>
</div>
</div>
<!-- 工序进度条 -->
<div class="process-progress">
<div class="progress-info">
<span class="current-process">
当前工序:{{ workOrder.currentProcess || '未开始' }}
</span>
<span class="process-count">
{{ workOrder.completedProcessCount }}/{{ workOrder.totalProcessCount }} 个工序
</span>
<span class="completion-rate">
完成率:{{ workOrder.completionRate }}
</span>
</div>
<el-progress
:percentage="parseInt(workOrder.completionRate)"
:color="getProgressColor"
:stroke-width="20"
/>
</div>
<!-- 展开/收起按钮 -->
<div class="card-actions">
<el-button
type="text"
size="small"
@click="toggleProcesses(workOrder)"
>
<i :class="workOrder.showProcesses ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"></i>
{{ workOrder.showProcesses ? '收起工序' : '展开工序' }}
</el-button>
</div>
<!-- 工序列表(展开时显示) -->
<div v-if="workOrder.showProcesses" v-loading="workOrder.loadingProcesses" class="process-list">
<el-table :data="workOrder.processList" border size="small" style="margin-top: 10px;">
<el-table-column label="序号" width="60" align="center">
<template slot-scope="scope">
{{ scope.row.processSort }}
</template>
</el-table-column>
<el-table-column label="工序名称" prop="processName" min-width="120" />
<el-table-column label="状态" width="100" align="center">
<template slot-scope="scope">
<el-tag :type="getProcessTagType(scope.row.status)" size="small">
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="计划数量" prop="planQuantity" width="100" align="center" />
<el-table-column label="已报工数量" prop="reportedQuantity" width="120" align="center" />
<el-table-column label="完成率" width="150" align="center">
<template slot-scope="scope">
<el-progress
:percentage="scope.row.completionRate"
:color="getProgressColor"
:format="() => scope.row.completionRate + '%'"
/>
</template>
</el-table-column>
<el-table-column label="最后报工时间" prop="lastReportTime" width="180" align="center">
<template slot-scope="scope">
{{ scope.row.lastReportTime || '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center" fixed="right">
<template slot-scope="scope">
<el-button
type="text"
size="small"
icon="el-icon-document"
@click="showReports(scope.row)"
>
报工单 ({{ scope.row.reportCount }})
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-card>
</div>
<!-- 分页 -->
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 报工单弹窗 -->
<el-dialog title="报工单列表" :visible.sync="reportDialogVisible" width="80%" append-to-body>
<div v-if="currentProcess" class="dialog-header">
<span class="dialog-title">工序:{{ currentProcess.processName }}</span>
<span>计划数量:{{ currentProcess.planQuantity }}</span>
<span>已报工:{{ currentProcess.reportedQuantity }}</span>
<span>完成率:{{ currentProcess.completionRate }}%</span>
</div>
<el-table :data="reportList" border v-loading="reportLoading">
<el-table-column label="报工单号" prop="number" width="150" />
<el-table-column label="报工人" prop="reportUserName" width="100" />
<el-table-column label="报工时间" prop="reportTime" width="180" align="center" />
<el-table-column label="报工数量" prop="reportQuantity" width="100" align="center" />
<el-table-column label="合格数量" prop="qualifiedQuantity" width="100" align="center" />
<el-table-column label="不合格数量" prop="unqualifiedQuantity" width="120" align="center" />
<el-table-column label="车间" prop="workshopName" width="120" />
<el-table-column label="工位" prop="stationName" width="120" />
<el-table-column label="状态" width="80" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 'A' ? 'success' : 'info'" size="small">
{{ scope.row.statusText }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" min-width="150" show-overflow-tooltip />
</el-table>
<div v-if="reportList.length === 0 && !reportLoading" class="empty-report">
<el-empty description="该工序暂无报工记录">
<el-tag type="info">状态:未完成</el-tag>
</el-empty>
</div>
</el-dialog>
</div>
</template>
<script>
import {
listWorkOrderExecution,
listProcessesByWorkOrder,
listReportsByProcess
} from '@/api/mes/statement/workOrderExecution'
export default {
name: 'WorkOrderExecution',
data() {
return {
// 显示搜索条件
showSearch: true,
// 工单列表
workOrderList: [],
// 总条数
total: 0,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
materialName: null,
batchNumber: null,
proStatus: null,
dateFrom: null,
dateTo: null
},
// 报工单弹窗
reportDialogVisible: false,
reportList: [],
reportLoading: false,
currentProcess: null
}
},
created() {
this.getList()
},
methods: {
/** 查询工单列表 */
getList() {
listWorkOrderExecution(this.queryParams).then(response => {
this.workOrderList = response.rows.map(item => ({
...item,
showProcesses: false,
loadingProcesses: false,
processList: []
}))
this.total = response.total
})
},
/** 展开/收起工序 */
toggleProcesses(workOrder) {
workOrder.showProcesses = !workOrder.showProcesses
// 如果是展开且还没加载过工序,则加载
if (workOrder.showProcesses && workOrder.processList.length === 0) {
this.loadProcesses(workOrder)
}
},
/** 加载工序列表 */
loadProcesses(workOrder) {
workOrder.loadingProcesses = true
listProcessesByWorkOrder(workOrder.id).then(response => {
workOrder.processList = response.data
workOrder.loadingProcesses = false
this.$forceUpdate()
}).catch(() => {
workOrder.loadingProcesses = false
})
},
/** 查看报工单 */
showReports(process) {
this.currentProcess = process
this.reportDialogVisible = true
this.reportLoading = true
this.reportList = []
listReportsByProcess(process.id).then(response => {
this.reportList = response.data
this.reportLoading = false
}).catch(() => {
this.reportLoading = false
})
},
/** 获取状态类型 */
getStatusType(status) {
const statusMap = {
'A': 'info',
'B': 'warning',
'D': 'success'
}
return statusMap[status] || 'info'
},
/** 获取工序标签类型 */
getProcessTagType(status) {
const typeMap = {
'已完成': 'success',
'进行中': 'warning',
'未开始': 'info'
}
return typeMap[status] || 'info'
},
/** 获取进度条颜色 */
getProgressColor(percentage) {
if (percentage >= 100) return '#67C23A'
if (percentage >= 80) return '#E6A23C'
if (percentage >= 50) return '#409EFF'
return '#909399'
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm('queryForm')
this.handleQuery()
}
}
}
</script>
<style scoped>
.work-order-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-left,
.header-right {
display: flex;
align-items: center;
gap: 15px;
}
.work-order-number {
font-weight: bold;
font-size: 16px;
color: #303133;
}
.material-name {
font-size: 14px;
color: #606266;
}
.specification {
font-size: 13px;
color: #909399;
}
.batch-number,
.quantity {
font-size: 14px;
color: #606266;
}
.process-progress {
padding: 15px 0;
}
.progress-info {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
font-size: 14px;
color: #606266;
}
.current-process {
font-weight: bold;
color: #303133;
}
.card-actions {
text-align: center;
padding-top: 10px;
border-top: 1px dashed #DCDFE6;
}
.process-list {
margin-top: 10px;
}
.dialog-header {
padding: 10px 20px;
background: #F5F7FA;
border-radius: 4px;
margin-bottom: 15px;
display: flex;
gap: 20px;
}
.dialog-title {
font-weight: bold;
color: #303133;
}
.empty-report {
padding: 40px 0;
text-align: center;
}
</style>
```
---
## 五、性能优化
### 5.1 数据库优化
- ✅ 新增缓存字段工单主表6个字段工序子表3个字段
- ✅ 创建完善索引主表3个子表2个报工单2个
- ✅ 零子查询,纯字段查询
- ✅ 触发器实时更新统计字段
### 5.2 接口优化
- ✅ 懒加载:展开时才加载工序
- ✅ 按需加载:点击才加载报工单
- ✅ 分页查询每次最多20条
- ✅ 前端缓存:已加载的数据不重复请求
### 5.3 性能指标
| 查询类型 | 响应时间 | 备注 |
|---------|---------|------|
| 工单列表(20条) | < 100ms | 零子查询 |
| 工序列表(8个) | < 50ms | 直接字段 |
| 报工单(20条) | < 50ms | 简单查询 |
| 总响应时间 | < 200ms | 用户无感知 |
---
## 六、实施步骤
### 第一步:数据库修改 ✓
1. 执行SQL脚本新增字段
2. 创建索引
3. 初始化缓存数据
4. 创建触发器
### 第二步:后端开发
1. 创建Query、VO类3个文件
2. 创建Mapper接口和XML
3. 创建Service接口和实现
4. 创建Controller
5. 测试接口
### 第三步:前端开发
1. 创建API文件
2. 创建页面组件
3. 配置路由和菜单
4. 测试功能
### 第四步:验证测试
1. 功能测试:所有接口正常
2. 性能测试:响应时间达标
3. 数据准确性:统计正确
4. 压力测试:并发稳定
---
## 七、注意事项
1. **数据一致性**:触发器保证实时更新,定时任务兜底
2. **性能保证**:所有查询走索引,无全表扫描
3. **错误处理**:接口异常返回友好提示
4. **权限控制**:根据角色控制数据范围
5. **日志记录**:关键操作记录日志
---
## 八、维护说明
### 8.1 定时任务
每天凌晨执行一次全量更新,修正可能的数据不一致
### 8.2 数据验证
提供验证SQL定期检查缓存字段准确性
### 8.3 性能监控
监控查询响应时间,发现问题及时优化
---
## 九、快速部署清单
### 9.1 已完成项 ✅
#### 数据库层
- ✅ 执行 `.tasks/2025-10-30_工序执行情况表重做.sql`
- 新增字段6个主表字段 + 3个子表字段
- 创建索引7个索引
- 初始化数据
- 创建触发器和存储过程
#### 后端代码 ✅
- ✅ `yjh-mes/src/main/java/cn/sourceplan/statement/domain/query/WorkOrderExecutionQuery.java`
- ✅ `yjh-mes/src/main/java/cn/sourceplan/statement/domain/vo/WorkOrderExecutionVO.java`
- ✅ `yjh-mes/src/main/java/cn/sourceplan/statement/domain/vo/ProcessExecutionVO.java`
- ✅ `yjh-mes/src/main/java/cn/sourceplan/statement/domain/vo/ReportVO.java`
- ✅ `yjh-mes/src/main/java/cn/sourceplan/statement/mapper/WorkOrderExecutionMapper.java`
- ✅ `yjh-mes/src/main/resources/mapper/statement/WorkOrderExecutionMapper.xml`
- ✅ `yjh-mes/src/main/java/cn/sourceplan/statement/service/IWorkOrderExecutionService.java`
- ✅ `yjh-mes/src/main/java/cn/sourceplan/statement/service/impl/WorkOrderExecutionServiceImpl.java`
- ✅ `yjh-mes/src/main/java/cn/sourceplan/statement/controller/WorkOrderExecutionController.java`
#### 前端代码 ✅
- ✅ `mes-ui/src/api/mes/statement/workOrderExecution.js`
- ✅ `mes-ui/src/views/mes/statement/workOrderExecution/index.vue`
---
### 9.2 待配置项 🔧
#### 1. 配置前端路由
编辑文件:`mes-ui/src/router/index.js` 或对应的路由文件
```javascript
{
path: '/statement/workOrderExecution',
component: () => import('@/views/mes/statement/workOrderExecution/index'),
name: 'WorkOrderExecution',
meta: {
title: '工序执行情况表',
icon: 'chart',
noCache: true
}
}
```
#### 2. 配置菜单权限
在系统管理 → 菜单管理中添加菜单:
```
菜单名称:工序执行情况表
菜单路径:/statement/workOrderExecution
组件路径mes/statement/workOrderExecution/index
权限标识statement:workOrderExecution:list
菜单图标chart
```
或直接执行SQL
```sql
-- 在sys_menu表中插入菜单根据实际parent_id调整
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, menu_type, visible, status, perms, icon, create_by, create_time, remark)
VALUES ('工序执行情况表', 2000, 10, 'workOrderExecution', 'mes/statement/workOrderExecution/index', 1, 'C', '0', '0', 'statement:workOrderExecution:list', 'chart', 'admin', NOW(), '工序执行情况表');
```
---
### 9.3 测试步骤 🧪
#### 第1步启动后端
```bash
cd mes-admin
mvn clean package
java -jar target/ruoyi-admin.jar
```
#### 第2步测试接口
使用Postman或浏览器测试
**1) 查询工单列表**
```
GET http://localhost:8080/reports/production/workOrderExecution?pageNum=1&pageSize=10
```
**2) 查询工序列表**
```
GET http://localhost:8080/reports/production/workOrderProcesses/1
```
**3) 查询报工单**
```
GET http://localhost:8080/reports/production/processReports/1
```
#### 第3步启动前端
```bash
cd mes-ui
npm install
npm run dev
```
#### 第4步访问页面
```
http://localhost:80
登录后访问:生产管理 → 工序执行情况表
```
---
### 9.4 验证清单 ✓
#### 功能验证
- [ ] 工单列表正常显示
- [ ] 进度条正确显示当前工序和完成率
- [ ] 点击展开/收起按钮正常
- [ ] 工序列表懒加载成功
- [ ] 工序状态(未开始/进行中/已完成)正确
- [ ] 点击报工单按钮弹窗正常
- [ ] 报工单列表显示正确
- [ ] 未报工工序显示"未完成"状态
- [ ] 搜索条件(产品名称、批次号、状态)有效
- [ ] 分页功能正常
#### 性能验证
- [ ] 工单列表查询 < 100ms
- [ ] 工序列表查询 < 50ms
- [ ] 报工单查询 < 50ms
- [ ] 10万级数据不卡顿
#### 数据验证
```sql
-- 验证工单统计字段是否正确
SELECT
w.id,
w.number,
w.current_process_name,
w.total_process_count,
w.completed_process_count,
w.overall_completion_rate,
(SELECT COUNT(*) FROM pro_workorder_entry e WHERE e.workorder_id = w.id AND e.type = 'report') AS actual_total_count
FROM pro_workorder w
WHERE w.id = 1;
-- 验证工序统计字段是否正确
SELECT
e.id,
e.process_name,
e.reported_quantity,
e.completion_rate,
(SELECT SUM(r.report_quantity) FROM pro_report r WHERE r.work_order_entry_id = e.id AND r.status = 'A') AS actual_reported
FROM pro_workorder_entry e
WHERE e.workorder_id = 1 AND e.type = 'report';
```
---
### 9.5 常见问题 ❓
#### Q1: 接口404错误
**A:** 检查Controller的 `@RequestMapping` 路径是否正确,确认为 `/reports/production`
#### Q2: 工序列表为空
**A:** 检查 `pro_workorder_entry` 表的 `type` 字段是否为 `'report'`
#### Q3: 统计字段都是0
**A:** 执行SQL中的"初始化数据"部分运行UPDATE语句
#### Q4: 前端报CORS错误
**A:** 检查 `vue.config.js` 的proxy配置确保指向正确的后端地址
#### Q5: 触发器不生效
**A:** 检查触发器是否创建成功:
```sql
SHOW TRIGGERS LIKE 'pro_report';
```
---
### 9.6 上线检查清单 🚀
#### 部署前
- [ ] 备份数据库
- [ ] 在测试环境验证所有功能
- [ ] 压力测试通过
- [ ] 代码已提交到Git
#### 部署时
- [ ] 停止应用服务
- [ ] 执行SQL脚本
- [ ] 部署后端代码
- [ ] 部署前端代码
- [ ] 清除缓存
#### 部署后
- [ ] 验证接口正常
- [ ] 验证页面访问正常
- [ ] 检查日志无异常
- [ ] 性能监控正常
---
**项目状态:** 🔄 **需求已更新为销售订单版,代码待重新生成**
---
## 十、完整实现代码(销售订单版)
### 10.1 SQL查询语句
#### 查询销售订单列表
```sql
SELECT
so.id,
so.number AS orderNumber,
so.customer_name AS customerName,
so.sale_date AS saleDate,
so.status,
soe.material_name AS materialName,
soe.quantity,
soe.unit_name AS unitName,
-- 统计该订单的工单数
(
SELECT COUNT(*)
FROM pro_workorder pw
WHERE JSON_EXTRACT(pw.source_info, '$.saleOrderId') = so.id
AND pw.status = 'A'
) AS workOrderCount,
-- 平均完成率
(
SELECT AVG(IFNULL(pw.overall_completion_rate, 0))
FROM pro_workorder pw
WHERE JSON_EXTRACT(pw.source_info, '$.saleOrderId') = so.id
AND pw.status = 'A'
) AS avgCompletionRate
FROM sal_order so
LEFT JOIN sal_order_entry soe ON soe.main_id = so.id
WHERE so.status = 'A'
AND (customerName IS NULL OR so.customer_name LIKE CONCAT('%', customerName, '%'))
AND (materialName IS NULL OR soe.material_name LIKE CONCAT('%', materialName, '%'))
ORDER BY so.sale_date DESC, so.create_time DESC
```
#### 查询订单的工单列表(含工序)
```sql
SELECT
pw.id,
pw.number AS workOrderNumber,
pw.material_name AS materialName,
pw.batch_number AS batchNumber,
pw.quantity,
pw.pro_status AS proStatus,
pw.current_process_name AS currentProcess,
pw.current_process_sort AS currentProcessSort,
pw.total_process_count AS totalProcessCount,
IFNULL(pw.overall_completion_rate, 0) AS completionRate
FROM pro_workorder pw
WHERE JSON_EXTRACT(pw.source_info, '$.saleOrderId') = #{saleOrderId}
AND pw.status = 'A'
ORDER BY pw.create_time ASC
```
#### 查询工序列表
```sql
SELECT
pwe.id,
pwe.process_name AS processName,
pwe.process_sort AS processSort,
pwe.report_quantity AS planQuantity,
IFNULL(pwe.reported_quantity, 0) AS reportedQuantity,
IFNULL(pwe.completion_rate, 0) AS completionRate,
CASE
WHEN IFNULL(pwe.completion_rate, 0) >= 100 THEN '已完成'
WHEN IFNULL(pwe.completion_rate, 0) > 0 THEN '进行中'
ELSE '未开始'
END AS status
FROM pro_workorder_entry pwe
WHERE pwe.workorder_id = #{workOrderId}
AND pwe.type = 'report'
ORDER BY pwe.process_sort ASC
```
---
**文档结束**