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

49 KiB
Raw Permalink Blame History

工序执行情况表重做 - 完整实施方案

文档版本: 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 关联关系

-- 销售订单 → 工单通过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 表新增字段:

current_process_name        -- 当前执行工序(避免子查询)
current_process_sort        -- 当前工序序号(用于进度条)
total_process_count         -- 总工序数避免COUNT
completed_process_count     -- 已完成工序数(统计用)
overall_completion_rate     -- 整体完成率避免AVG
last_report_time           -- 最后报工时间(排序用)

pro_workorder_entry 表新增字段:

reported_quantity          -- 已报工数量避免SUM
completion_rate           -- 完成率(避免除法计算)
last_report_time         -- 最后报工时间(排序用)

2.3 索引设计

-- 工单主表索引
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对象

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对象

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;
}
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;
}
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接口

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 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接口

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实现

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

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

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. 工序进度计算

    // 收集所有工单的工序,按工序名称分组
    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. 进度条状态

    • 绿色 successavgCompletionRate >= 100
    • 🔵 蓝色 process0 < avgCompletionRate < 100
    • 灰色 waitavgCompletionRate == 0
  4. 样式设计

    .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

<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 或对应的路由文件

{
  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

-- 在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步启动后端

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步启动前端

cd mes-ui
npm install
npm run dev

第4步访问页面

http://localhost:80
登录后访问:生产管理 → 工序执行情况表

9.4 验证清单 ✓

功能验证

  • 工单列表正常显示
  • 进度条正确显示当前工序和完成率
  • 点击展开/收起按钮正常
  • 工序列表懒加载成功
  • 工序状态(未开始/进行中/已完成)正确
  • 点击报工单按钮弹窗正常
  • 报工单列表显示正确
  • 未报工工序显示"未完成"状态
  • 搜索条件(产品名称、批次号、状态)有效
  • 分页功能正常

性能验证

  • 工单列表查询 < 100ms
  • 工序列表查询 < 50ms
  • 报工单查询 < 50ms
  • 10万级数据不卡顿

数据验证

-- 验证工单统计字段是否正确
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: 检查触发器是否创建成功:

SHOW TRIGGERS LIKE 'pro_report';

9.6 上线检查清单 🚀

部署前

  • 备份数据库
  • 在测试环境验证所有功能
  • 压力测试通过
  • 代码已提交到Git

部署时

  • 停止应用服务
  • 执行SQL脚本
  • 部署后端代码
  • 部署前端代码
  • 清除缓存

部署后

  • 验证接口正常
  • 验证页面访问正常
  • 检查日志无异常
  • 性能监控正常

项目状态: 🔄 需求已更新为销售订单版,代码待重新生成


十、完整实现代码(销售订单版)

10.1 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

查询订单的工单列表(含工序)

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

查询工序列表

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

文档结束