Files
MES/yawei-mes/.tasks/2025-10-30_工序执行情况表重做.md

1675 lines
49 KiB
Markdown
Raw Normal View History

2026-04-02 10:38:23 +08:00
# 工序执行情况表重做 - 完整实施方案
> **文档版本:** 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
```
---
**文档结束**