feat(mes): 优化生产订单与排产任务列表显示,新增启停工及校验逻辑
1. 生产订单列表展示优化 (WorkOrder)
- 新增「生产状态」列,全局注册 `ORDER_PRODUCTION_STATUS` 常量并绑定系统动态字典。
- 彻底重构「工序生产进度」单元格显示:弃用臃肿的 Steps 组件,改为极简横向布局(彩色状态圆点 + 进度百分比),解决表格行高被强行撑开及内容截断问题,支持横向滚动。
- 优化「完成进度」列宽及展示样式,将百分比数值外置同行显示,更加直观。
2. 排产任务列表改造 (Task)
- 顶部表头重构,由单一标题重构为「全部 / 未完成 / 已完成」的 Tabs 标签页切换结构。
- 重写数据过滤逻辑,剥离写死的 status 查询,改为使用 `scheduleStatus` 字段动态响应 Tabs 切换 (0: 未完成, 1: 已完成)。
3. 工单业务逻辑增强与状态流转
- 落实「工单查询逻辑设计方案」,优化底层的工单条件检索逻辑。
- 实现「添加工单启停功能」与「停工状态」,支持工单在执行过程中的暂停与恢复业务流转。
- 强化容错与防呆设计:操作「完成订单」时增加前置校验 `handleCheckFinish`,若生产状态为未完成,强制弹出二次确认 Modal,避免误操作。
# 相关讨论/参考方案
# - 添加工单启停功能
# - 停工状态功能设计方案
# - 工单查询逻辑设计方案
# - 工单完成进度显示优化
已修改18个文件
已添加9个文件
769 ■■■■■ 文件已修改
sql/mysql/task_pause_record.sql 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/MesProTaskController.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/vo/MesProTaskPauseRecordRespVO.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/vo/MesProTaskPauseReqVO.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/vo/MesProTaskRespVO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/vo/MesProTaskResumeReqVO.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/MesProWorkOrderController.java 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/MesProWorkOrderPageReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/MesProWorkOrderProcessProgressVO.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/MesProWorkOrderRespVO.java 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/task/MesProTaskPauseRecordDO.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/task/MesProTaskMapper.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/task/MesProTaskPauseRecordMapper.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/workorder/MesProWorkOrderMapper.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/workorder/MesProWorkOrderProcessMapper.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/pro/MesProTaskStatusEnum.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/pro/MesProWorkOrderProductionStatusEnum.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java 51 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskServiceImpl.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderProcessService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderProcessServiceImpl.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderServiceImpl.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/resources/mapper/pro/workorder/MesProWorkOrderMapper.xml 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-local.yaml 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/task_pause_record.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
CREATE TABLE `mes_pro_task_pause_record` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
  `task_id` bigint(20) NOT NULL COMMENT '任务编号',
  `work_order_id` bigint(20) DEFAULT NULL COMMENT '工单编号',
  `workstation_id` bigint(20) DEFAULT NULL COMMENT '工作站编号',
  `reason` varchar(255) NOT NULL COMMENT '停工原因',
  `pause_time` datetime NOT NULL COMMENT '停工时间',
  `pause_user_id` bigint(20) NOT NULL COMMENT '停工操作人ID',
  `resume_time` datetime DEFAULT NULL COMMENT '复工时间',
  `resume_user_id` bigint(20) DEFAULT NULL COMMENT '复工操作人ID',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
  `tenant_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '租户编号',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_task_id` (`task_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='生产任务停工记录表';
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/MesProTaskController.java
@@ -12,6 +12,8 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.GanttDataRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskAutoGeneratePreviewRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskAutoGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskPauseReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskResumeReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskSaveReqVO;
@@ -48,6 +50,11 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum;
import cn.iocoder.yudao.module.mes.service.pro.feedback.MesProFeedbackService;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskPauseRecordDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskPauseRecordMapper;
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskPauseRecordRespVO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import java.io.IOException;
import java.math.BigDecimal;
@@ -83,6 +90,10 @@
    private MesProRouteProcessService routeProcessService;
    @Resource
    private MesProFeedbackService feedbackService;
    @Resource
    private MesProTaskPauseRecordMapper pauseRecordMapper;
    @Resource
    private AdminUserApi adminUserApi;
    @PostMapping("/create")
    @Operation(summary = "创建生产任务")
@@ -119,6 +130,22 @@
    @PreAuthorize("@ss.hasPermission('mes:pro-task:delete')")
    public CommonResult<Boolean> deleteTask(@RequestParam("id") Long id) {
        taskService.deleteTask(id);
        return success(true);
    }
    @PutMapping("/pause")
    @Operation(summary = "生产任务停工")
    @PreAuthorize("@ss.hasPermission('mes:pro-task:update')")
    public CommonResult<Boolean> pauseTask(@Valid @RequestBody MesProTaskPauseReqVO pauseReqVO) {
        taskService.pauseTask(pauseReqVO);
        return success(true);
    }
    @PutMapping("/resume")
    @Operation(summary = "生产任务复工")
    @PreAuthorize("@ss.hasPermission('mes:pro-task:update')")
    public CommonResult<Boolean> resumeTask(@Valid @RequestBody MesProTaskResumeReqVO resumeReqVO) {
        taskService.resumeTask(resumeReqVO);
        return success(true);
    }
@@ -164,7 +191,35 @@
    @PreAuthorize("@ss.hasPermission('mes:pro-task:query')")
    public CommonResult<PageResult<MesProTaskRespVO>> getPendingFeedbackPage(@Valid MesProTaskPageReqVO pageReqVO) {
        PageResult<MesProTaskDO> pageResult = taskService.getPendingFeedbackPage(pageReqVO);
        return success(new PageResult<>(buildTaskRespVOList(pageResult.getList()), pageResult.getTotal()));
        List<MesProTaskRespVO> respVOList = buildTaskRespVOList(pageResult.getList());
        if (CollUtil.isNotEmpty(respVOList)) {
            Set<Long> taskIds = convertSet(respVOList, MesProTaskRespVO::getId);
            List<MesProTaskPauseRecordDO> records = pauseRecordMapper.selectListByTaskIds(taskIds);
            if (CollUtil.isNotEmpty(records)) {
                Set<Long> userIds = new HashSet<>();
                records.forEach(r -> {
                    if (r.getPauseUserId() != null) userIds.add(r.getPauseUserId());
                    if (r.getResumeUserId() != null) userIds.add(r.getResumeUserId());
                });
                Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
                Map<Long, List<MesProTaskPauseRecordRespVO>> pauseRecordMap = new HashMap<>();
                for (MesProTaskPauseRecordDO record : records) {
                    MesProTaskPauseRecordRespVO vo = BeanUtils.toBean(record, MesProTaskPauseRecordRespVO.class);
                    if (record.getPauseUserId() != null && userMap.containsKey(record.getPauseUserId())) {
                        vo.setPauseUserName(userMap.get(record.getPauseUserId()).getNickname());
                    }
                    if (record.getResumeUserId() != null && userMap.containsKey(record.getResumeUserId())) {
                        vo.setResumeUserName(userMap.get(record.getResumeUserId()).getNickname());
                    }
                    pauseRecordMap.computeIfAbsent(record.getTaskId(), k -> new ArrayList<>()).add(vo);
                }
                respVOList.forEach(vo -> vo.setPauseRecords(pauseRecordMap.get(vo.getId())));
            }
        }
        return success(new PageResult<>(respVOList, pageResult.getTotal()));
    }
    @GetMapping("/gantt-list")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/vo/MesProTaskPauseRecordRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES ç”Ÿäº§ä»»åŠ¡åœå·¥åŽ†å²è®°å½• Response VO")
@Data
public class MesProTaskPauseRecordRespVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long id;
    @Schema(description = "生产任务ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    private Long taskId;
    @Schema(description = "停工操作人ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long pauseUserId;
    @Schema(description = "停工操作人姓名", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
    private String pauseUserName;
    @Schema(description = "停工时间", requiredMode = Schema.RequiredMode.REQUIRED)
    private LocalDateTime pauseTime;
    @Schema(description = "停工原因", example = "设备故障")
    private String reason;
    @Schema(description = "备注", example = "紧急停工")
    private String remark;
    @Schema(description = "复工人ID", example = "2")
    private Long resumeUserId;
    @Schema(description = "复工人姓名", example = "李四")
    private String resumeUserName;
    @Schema(description = "复工时间")
    private LocalDateTime resumeTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/vo/MesProTaskPauseReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - ç”Ÿäº§ä»»åŠ¡åœå·¥ Request VO")
@Data
public class MesProTaskPauseReqVO {
    @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    @NotNull(message = "任务编号不能为空")
    private Long id;
    @Schema(description = "停工原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "设备故障")
    @NotEmpty(message = "停工原因不能为空")
    private String reason;
    @Schema(description = "备注", example = "维修中")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/vo/MesProTaskRespVO.java
@@ -5,6 +5,7 @@
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - MES ç”Ÿäº§ä»»åŠ¡ Response VO")
@Data
@@ -130,4 +131,7 @@
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    private LocalDateTime createTime;
    @Schema(description = "停工历史记录")
    private List<MesProTaskPauseRecordRespVO> pauseRecords;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/vo/MesProTaskResumeReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - ç”Ÿäº§ä»»åŠ¡å¤å·¥ Request VO")
@Data
public class MesProTaskResumeReqVO {
    @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    @NotNull(message = "任务编号不能为空")
    private Long id;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/MesProWorkOrderController.java
@@ -9,20 +9,24 @@
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.MesProWorkOrderPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.MesProWorkOrderProgressRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.MesProWorkOrderRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.MesProWorkOrderSaveReqVO;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi;
import cn.iocoder.yudao.module.srm.api.supplier.dto.SrmSupplierRespDTO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderProcessDO;
import cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderProductionStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderProcessService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi;
import cn.iocoder.yudao.module.srm.api.supplier.dto.SrmSupplierRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -34,10 +38,12 @@
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.*;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@Tag(name = "管理后台 - MES ç”Ÿäº§å·¥å•")
@@ -56,6 +62,12 @@
    private SrmSupplierApi srmSupplierApi;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    @Resource
    private MesProWorkOrderProcessService workOrderProcessService;
    @Resource
    private MesProTaskService taskService;
    @Resource
    private MesProProcessService processService;
    @PostMapping("/create")
    @Operation(summary = "创建生产工单")
@@ -98,6 +110,14 @@
    @PreAuthorize("@ss.hasPermission('mes:pro-work-order:query')")
    public CommonResult<PageResult<MesProWorkOrderRespVO>> getWorkOrderPage(@Valid MesProWorkOrderPageReqVO pageReqVO) {
        PageResult<MesProWorkOrderDO> pageResult = workOrderService.getWorkOrderPage(pageReqVO);
        return success(new PageResult<>(buildWorkOrderRespVOList(pageResult.getList()), pageResult.getTotal()));
    }
    @GetMapping("/schedule-page")
    @Operation(summary = "获得生产工单调度状态分页")
    @PreAuthorize("@ss.hasPermission('mes:pro-work-order:query')")
    public CommonResult<PageResult<MesProWorkOrderRespVO>> getWorkOrderSchedulePage(@Valid MesProWorkOrderPageReqVO pageReqVO) {
        PageResult<MesProWorkOrderDO> pageResult = workOrderService.getWorkOrderSchedulePage(pageReqVO);
        return success(new PageResult<>(buildWorkOrderRespVOList(pageResult.getList()), pageResult.getTotal()));
    }
@@ -163,6 +183,7 @@
            return Collections.emptyList();
        }
        // 1. æ‰¹é‡èŽ·å–å…³è”æ•°æ®
        Set<Long> workOrderIds = convertSet(list, MesProWorkOrderDO::getId);
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
                convertSet(list, MesProWorkOrderDO::getProductId));
        Map<Long, CrmCustomerRespDTO> customerMap = customerApi.getCustomerMap(
@@ -175,6 +196,17 @@
                convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
        Map<Long, MesProWorkOrderDO> parentMap = workOrderService.getWorkOrderMap(
                convertSet(list, MesProWorkOrderDO::getParentId));
        // èŽ·å–æ‰€æœ‰å…³è”çš„å·¥å•å·¥åºå’Œä»»åŠ¡
        List<MesProWorkOrderProcessDO> allProcesses = workOrderProcessService.getWorkOrderProcessListByWorkOrderIds(workOrderIds);
        List<MesProTaskDO> allTasks = taskService.getTaskListByWorkOrderIds(workOrderIds);
        Set<Long> processIds = convertSet(allProcesses, MesProWorkOrderProcessDO::getProcessId);
        Map<Long, MesProProcessDO> processMap = processService.getProcessMap(processIds);
        Map<Long, List<MesProWorkOrderProcessDO>> woProcessMap = convertMultiMap(allProcesses, MesProWorkOrderProcessDO::getWorkOrderId);
        Map<Long, List<MesProTaskDO>> woTaskMap = convertMultiMap(allTasks, MesProTaskDO::getWorkOrderId);
        // 2. æ‹¼æŽ¥ VO(单位名称从产品关联的 item èŽ·å¾—ï¼‰
        return BeanUtils.toBean(list, MesProWorkOrderRespVO.class, vo -> {
            MapUtils.findAndThen(itemMap, vo.getProductId(), item -> {
@@ -189,6 +221,60 @@
                    supplier -> vo.setVendorName(supplier.getName()));
            MapUtils.findAndThen(parentMap, vo.getParentId(),
                    parent -> vo.setParentCode(parent.getCode()));
            // è®¡ç®—完工进度 (quantityProduced / quantity * 100)
            BigDecimal produced = vo.getQuantityProduced() != null ? vo.getQuantityProduced() : BigDecimal.ZERO;
            if (vo.getQuantity() != null && vo.getQuantity().compareTo(BigDecimal.ZERO) > 0) {
                vo.setCompletionProgress(produced.multiply(new BigDecimal("100")).divide(vo.getQuantity(), 2, java.math.RoundingMode.HALF_UP));
            } else {
                vo.setCompletionProgress(produced.compareTo(BigDecimal.ZERO) > 0 ? new BigDecimal("100.00") : BigDecimal.ZERO);
            }
            // è®¡ç®—进度和状态
            List<MesProWorkOrderProcessDO> processes = woProcessMap.getOrDefault(vo.getId(), Collections.emptyList());
            List<MesProTaskDO> tasks = woTaskMap.getOrDefault(vo.getId(), Collections.emptyList());
            List<MesProWorkOrderProcessProgressVO> progressList = new ArrayList<>();
            boolean allCompleted = !processes.isEmpty();
            boolean anyStarted = false;
            Map<Long, List<MesProTaskDO>> processTaskMap = convertMultiMap(tasks, MesProTaskDO::getProcessId);
            for (MesProWorkOrderProcessDO process : processes) {
                List<MesProTaskDO> pTasks = processTaskMap.getOrDefault(process.getProcessId(), Collections.emptyList());
                BigDecimal totalQty = pTasks.stream().map(t -> t.getQuantity() != null ? t.getQuantity() : BigDecimal.ZERO).reduce(BigDecimal.ZERO, BigDecimal::add);
                BigDecimal totalProduced = pTasks.stream().map(t -> t.getProducedQuantity() != null ? t.getProducedQuantity() : BigDecimal.ZERO).reduce(BigDecimal.ZERO, BigDecimal::add);
                MesProWorkOrderProcessProgressVO progressVO = new MesProWorkOrderProcessProgressVO();
                MesProProcessDO processDO = processMap.get(process.getProcessId());
                progressVO.setProcessName(processDO != null ? processDO.getName() : "");
                if (totalQty.compareTo(BigDecimal.ZERO) > 0) {
                    progressVO.setProgress(totalProduced.multiply(new BigDecimal("100")).divide(totalQty, 2, java.math.RoundingMode.HALF_UP));
                } else {
                    progressVO.setProgress(totalProduced.compareTo(BigDecimal.ZERO) > 0 ? new BigDecimal("100.00") : BigDecimal.ZERO);
                }
                progressList.add(progressVO);
                if (totalProduced.compareTo(BigDecimal.ZERO) > 0) {
                    anyStarted = true;
                }
                if (totalQty.compareTo(BigDecimal.ZERO) == 0 || totalProduced.compareTo(totalQty) < 0) {
                    allCompleted = false;
                }
            }
            vo.setProcessProgressList(progressList);
            if (processes.isEmpty()) {
                vo.setProductionStatus(MesProWorkOrderProductionStatusEnum.WAITING.getStatus()); // å¾…生产
            } else if (allCompleted) {
                vo.setProductionStatus(MesProWorkOrderProductionStatusEnum.FINISHED.getStatus()); // ç”Ÿäº§å®Œæˆ
            } else if (anyStarted) {
                vo.setProductionStatus(MesProWorkOrderProductionStatusEnum.PRODUCING.getStatus()); // ç”Ÿäº§ä¸­
            } else {
                vo.setProductionStatus(MesProWorkOrderProductionStatusEnum.WAITING.getStatus()); // å¾…生产
            }
        });
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/MesProWorkOrderPageReqVO.java
@@ -42,4 +42,7 @@
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime[] requestDate;
    @Schema(description = "调度状态(空=所有,0=未完成,1=已完成)", example = "1")
    private Integer scheduleStatus;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/MesProWorkOrderProcessProgressVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - MES å·¥å•工序进度 VO")
@Data
public class MesProWorkOrderProcessProgressVO {
    @Schema(description = "工序名称", example = "打磨工序")
    private String processName;
    @Schema(description = "工序进度(%)", example = "50.00")
    private BigDecimal progress;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/MesProWorkOrderRespVO.java
@@ -61,13 +61,17 @@
    @ExcelProperty("单位")
    private String unitMeasureName;
    @Schema(description = "生产数量", example = "100.00")
    @ExcelProperty("生产数量")
    @Schema(description = "订单数量", example = "100.00")
    @ExcelProperty("订单数量")
    private BigDecimal quantity;
    @Schema(description = "已生产数量", example = "50.00")
    @ExcelProperty("已生产数量")
    private BigDecimal quantityProduced;
    @Schema(description = "完工进度", example = "50.00")
    @ExcelProperty("完工进度")
    private BigDecimal completionProgress;
    @Schema(description = "调整数量", example = "0")
    @ExcelProperty("调整数量")
@@ -134,4 +138,11 @@
    @ExcelProperty("创建时间")
    private LocalDateTime createTime;
    @Schema(description = "工序进度列表")
    private java.util.List<MesProWorkOrderProcessProgressVO> processProgressList;
    @Schema(description = "生产状态 (0: å¾…生产, 1: ç”Ÿäº§ä¸­, 2: ç”Ÿäº§å®Œæˆ)")
    @ExcelProperty("生产状态")
    private Integer productionStatus;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/task/MesProTaskPauseRecordDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,68 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.pro.task;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
/**
 * MES ç”Ÿäº§ä»»åŠ¡åœå·¥è®°å½• DO
 *
 * @author è¶…级管理员
 */
@TableName("mes_pro_task_pause_record")
@KeySequence("mes_pro_task_pause_record_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesProTaskPauseRecordDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * ä»»åŠ¡ç¼–å·
     */
    private Long taskId;
    /**
     * å·¥å•编号
     */
    private Long workOrderId;
    /**
     * å·¥ä½œç«™ç¼–号
     */
    private Long workstationId;
    /**
     * åœå·¥åŽŸå› 
     */
    private String reason;
    /**
     * åœå·¥æ—¶é—´
     */
    private LocalDateTime pauseTime;
    /**
     * åœå·¥æ“ä½œäººID
     */
    private Long pauseUserId;
    /**
     * å¤å·¥æ—¶é—´
     */
    private LocalDateTime resumeTime;
    /**
     * å¤å·¥æ“ä½œäººID
     */
    private Long resumeUserId;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/task/MesProTaskMapper.java
@@ -49,7 +49,7 @@
        MPJLambdaWrapperX<MesProTaskDO> query = new MPJLambdaWrapperX<>();
        query.selectAll(MesProTaskDO.class);
        // å¾…报工台账:进行中的任务(排除已完成/已取消),且仍有待报工量
        query.eq(MesProTaskDO::getStatus, MesProTaskStatusEnum.PREPARE.getStatus());
        query.in(MesProTaskDO::getStatus, MesProTaskStatusEnum.PREPARE.getStatus(),MesProTaskStatusEnum.PAUSED.getStatus());
        query.apply("(quantity - IFNULL(produced_quantity, 0)) > 0");
        query.likeIfPresent(MesProTaskDO::getCode, reqVO.getCode())
                .likeIfPresent(MesProTaskDO::getName, reqVO.getName())
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/task/MesProTaskPauseRecordMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.mes.dal.mysql.pro.task;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskPauseRecordDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
@Mapper
public interface MesProTaskPauseRecordMapper extends BaseMapperX<MesProTaskPauseRecordDO> {
    default MesProTaskPauseRecordDO selectLastUnresumedByTaskId(Long taskId) {
        return selectOne(new LambdaQueryWrapperX<MesProTaskPauseRecordDO>()
                .eq(MesProTaskPauseRecordDO::getTaskId, taskId)
                .isNull(MesProTaskPauseRecordDO::getResumeTime)
                .orderByDesc(MesProTaskPauseRecordDO::getId)
                .last("LIMIT 1"));
    }
    default List<MesProTaskPauseRecordDO> selectListByTaskIds(Collection<Long> taskIds) {
        return selectList(new LambdaQueryWrapperX<MesProTaskPauseRecordDO>()
                .inIfPresent(MesProTaskPauseRecordDO::getTaskId, taskIds)
                .orderByDesc(MesProTaskPauseRecordDO::getId));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/workorder/MesProWorkOrderMapper.java
@@ -58,6 +58,20 @@
        return selectPage(reqVO, wrapper);
    }
    /**
     * åˆ†é¡µæŸ¥è¯¢ï¼ˆå¸¦å®¢æˆ·æƒé™è¿‡æ»¤ä¸”支持调度状态查询)
     */
    default PageResult<MesProWorkOrderDO> selectSchedulePage(MesProWorkOrderPageReqVO pageReqVO, List<Long> permittedClientIds) {
        com.baomidou.mybatisplus.core.metadata.IPage<MesProWorkOrderDO> mpPage = cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils.buildPage(pageReqVO);
        com.baomidou.mybatisplus.core.metadata.IPage<MesProWorkOrderDO> result = selectSchedulePage(mpPage, pageReqVO, permittedClientIds);
        return new PageResult<>(result.getRecords(), result.getTotal());
    }
    com.baomidou.mybatisplus.core.metadata.IPage<MesProWorkOrderDO> selectSchedulePage(
            com.baomidou.mybatisplus.core.metadata.IPage<MesProWorkOrderDO> page,
            @org.apache.ibatis.annotations.Param("pageReqVO") MesProWorkOrderPageReqVO pageReqVO,
            @org.apache.ibatis.annotations.Param("permittedClientIds") List<Long> permittedClientIds);
    default MesProWorkOrderDO selectByCode(String code) {
        return selectOne(MesProWorkOrderDO::getCode, code);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/workorder/MesProWorkOrderProcessMapper.java
@@ -29,9 +29,22 @@
                .orderByAsc(MesProWorkOrderProcessDO::getSort));
    }
    default MesProWorkOrderProcessDO selectLastByWorkOrderId(Long workOrderId) {
        return selectOne(new LambdaQueryWrapperX<MesProWorkOrderProcessDO>()
                .eq(MesProWorkOrderProcessDO::getWorkOrderId, workOrderId)
                .orderByDesc(MesProWorkOrderProcessDO::getSort)
                .last("LIMIT 1"));
    }
    default void deleteByWorkOrderId(Long workOrderId) {
        delete(new LambdaQueryWrapperX<MesProWorkOrderProcessDO>()
                .eq(MesProWorkOrderProcessDO::getWorkOrderId, workOrderId));
    }
    default List<MesProWorkOrderProcessDO> selectListByWorkOrderIds(java.util.Collection<Long> workOrderIds) {
        return selectList(new LambdaQueryWrapperX<MesProWorkOrderProcessDO>()
                .in(MesProWorkOrderProcessDO::getWorkOrderId, workOrderIds)
                .orderByAsc(MesProWorkOrderProcessDO::getWorkOrderId, MesProWorkOrderProcessDO::getSort));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -286,7 +286,9 @@
    ErrorCode PRO_TASK_ROUTE_PROCESS_MISMATCH = new ErrorCode(1_040_503_004, "生产任务与当前工艺路线或工序不一致");
    ErrorCode PRO_TASK_ITEM_MISMATCH = new ErrorCode(1_040_503_005, "检验物料与生产任务物料不一致");
    ErrorCode PRO_TASK_HAS_FEEDBACK = new ErrorCode(1_040_503_006, "该任务下存在报工记录,无法删除(避免报工追溯断链)");
    ErrorCode PRO_WORKSTATION_PROCESS_MISMATCH = new ErrorCode(1_040_503_006, "工作站所属工序与当前工序不一致");
    ErrorCode PRO_WORKSTATION_PROCESS_MISMATCH = new ErrorCode(1_040_503_009, "工作站所属工序与当前工序不一致"); // ä¿®æ­£ç¼–号冲突
    ErrorCode PRO_TASK_CANNOT_PAUSE = new ErrorCode(1_040_503_007, "只有【生产中】或【部分完成】的任务可以停工");
    ErrorCode PRO_TASK_NOT_PAUSED = new ErrorCode(1_040_503_008, "任务不处于【已停工】状态,无法复工");
    // ========== MES ç”Ÿäº§ç®¡ç†-生产任务投料(1-040-503-100) ==========
    ErrorCode PRO_TASK_ISSUE_NOT_EXISTS = new ErrorCode(1_040_503_100, "生产任务投料记录不存在");
@@ -314,6 +316,7 @@
    ErrorCode PRO_FEEDBACK_QUALIFIED_UNQUALIFIED_REQUIRED = new ErrorCode(1_040_506_011, "请输入合格品和不良品数量,且合计须大于 0");
    ErrorCode PRO_FEEDBACK_UNCHECK_QUANTITY_EXISTS = new ErrorCode(1_040_506_012, "当前报工单未完成检验(待检数量:{}),无法执行报工");
    ErrorCode PRO_FEEDBACK_BACKFLUSH_BOM_REQUIRED = new ErrorCode(1_040_506_013, "当前工序为倒冲工序,但未配置BOM消耗,无法报工");
    ErrorCode PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED = new ErrorCode(1_040_506_014, "生产任务处于停工状态,无法报工");
    // ========== MES ç”Ÿäº§ç®¡ç†-生产流转卡(1-040-507-000) ==========
    ErrorCode PRO_CARD_NOT_EXISTS = new ErrorCode(1_040_507_000, "生产流转卡不存在");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/pro/MesProTaskStatusEnum.java
@@ -46,7 +46,11 @@
     *
     * å¯¹åº” MesProTaskService#cancelTaskByOrderId æ–¹æ³•
     */
    CANCELED(MesOrderStatusConstants.CANCELLED, "已取消");
    CANCELED(MesOrderStatusConstants.CANCELLED, "已取消"),
    /**
     * å·²åœå·¥
     */
    PAUSED(9, "已停工");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(MesProTaskStatusEnum::getStatus).toArray(Integer[]::new);
@@ -75,13 +79,13 @@
    }
    /**
     * åˆ¤æ–­æ˜¯å¦å¯ä»¥æŠ¥å·¥ï¼ˆéžç»ˆæ€ï¼‰
     * åˆ¤æ–­æ˜¯å¦å¯ä»¥æŠ¥å·¥ï¼ˆéžç»ˆæ€ä¸”非停工)
     *
     * @param status çŠ¶æ€å€¼
     * @return æ˜¯å¦å¯ä»¥æŠ¥å·¥
     */
    public static boolean canFeedback(Integer status) {
        return !isEndStatus(status);
        return !isEndStatus(status) && !ObjectUtils.equalsAny(status, PAUSED.status);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/pro/MesProWorkOrderProductionStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
package cn.iocoder.yudao.module.mes.enums.pro;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * MES ç”Ÿäº§å·¥å•生产状态枚举
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum MesProWorkOrderProductionStatusEnum implements ArrayValuable<Integer> {
    /**
     * å¾…生产
     */
    WAITING(0, "待生产"),
    /**
     * ç”Ÿäº§ä¸­
     */
    PRODUCING(1, "生产中"),
    /**
     * ç”Ÿäº§å®Œæˆ
     */
    FINISHED(2, "生产完成");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(MesProWorkOrderProductionStatusEnum::getStatus).toArray(Integer[]::new);
    /**
     * çŠ¶æ€å€¼
     */
    private final Integer status;
    /**
     * çŠ¶æ€å
     */
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
@@ -15,15 +15,18 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderBomDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackMapper;
import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProTaskStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService;
import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderBomService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderProcessService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import cn.iocoder.yudao.module.mes.service.wm.itemconsume.MesWmItemConsumeService;
import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceLineService;
@@ -70,6 +73,8 @@
    private MesProRouteProcessService routeProcessService;
    @Resource
    private MesProWorkOrderBomService workOrderBomService;
    @Resource
    private MesProWorkOrderProcessService workOrderProcessService;
    @Resource
    @Lazy // é¿å…å¾ªçŽ¯ä¾èµ–
    private MesProTaskService taskService;
@@ -215,6 +220,18 @@
                .setUncheckQuantity(BigDecimal.ZERO));
        // 6. æ ¹æ®æ˜¯å¦éœ€è¦æ£€éªŒï¼Œå†³å®šäº§å‡ºå’Œå…¥åº“方式
        // 6.2 è°ƒæ•´ åªè¦å®¡æ‰¹æˆåŠŸç›´æŽ¥æ›´æ–°
        // å…³é”®å·¥åºï¼šæ›´æ–°ä»»åŠ¡ + å·¥å•的已生产数量;非关键工序:只更新任务的已生产数量(工单只看关键工序产出)
        if (keyFlag) {
            updateTaskAndWorkOrderByFeedback(feedback);
        } else {
            taskService.updateProducedQuantity(feedback.getTaskId(),
                    feedback.getFeedbackQuantity(),
                    ObjectUtil.defaultIfNull(feedback.getQualifiedQuantity(), BigDecimal.ZERO),
                    ObjectUtil.defaultIfNull(feedback.getUnqualifiedQuantity(), BigDecimal.ZERO));
                checkAndPublishProductionFinished(feedback.getWorkOrderId());
        }
        if (checkFlag) {
            // 6.1 éœ€è¦æ£€éªŒï¼šç”Ÿæˆäº§å‡ºå•(质量状态=待检验),更新报工状态为待检验,等质检完成回调后再入库
            // å®Œæˆæ—¶å›žè°ƒè§ï¼šMesQcIpqcServiceImpl#finishIpqc â†’ splitPendingAndFinishProduce + updateProFeedbackWhenIpqcFinish
@@ -226,16 +243,7 @@
            // 6.2 æ— éœ€æ£€éªŒï¼šç”Ÿæˆäº§å‡ºå•(按合格/不合格拆行),直接完成入库
            MesWmProductProduceDO produce = productProduceService.generateProductProduce(feedback, false, reqVO);
            productProduceService.finishProductProduce(produce.getId());
            // å…³é”®å·¥åºï¼šæ›´æ–°ä»»åŠ¡ + å·¥å•的已生产数量;非关键工序:只更新任务的已生产数量(工单只看关键工序产出)
            if (keyFlag) {
                updateTaskAndWorkOrderByFeedback(feedback);
            } else {
                taskService.updateProducedQuantity(feedback.getTaskId(),
                        feedback.getFeedbackQuantity(),
                        ObjectUtil.defaultIfNull(feedback.getQualifiedQuantity(), BigDecimal.ZERO),
                        ObjectUtil.defaultIfNull(feedback.getUnqualifiedQuantity(), BigDecimal.ZERO));
                checkAndPublishProductionFinished(feedback.getWorkOrderId());
            }
        }
        return true; // å·²å®Œæˆ
@@ -248,6 +256,7 @@
     *   <li>使用报工数量(feedbackQuantity),累加任务和工单的已生产数量</li>
     *   <li>使用产出单行按 qualityStatus èšåˆçš„合格品/不合格品数量,累加任务的合格品/不合格品数量
     *       ï¼ˆä¸ç›´æŽ¥ç”¨ feedback ä¸Šçš„æ•°é‡ï¼Œç¡®ä¿è´¨æ£€å›žè°ƒåœºæ™¯ä¸‹æ•°é‡æ¥æºæ­£ç¡®ï¼‰</li>
     *      isPush æ˜¯å¦æŽ¨é€ç”Ÿäº§å®Œæˆäº‹ä»¶
     * </ul>
     *
     * @param feedback æŠ¥å·¥å•
@@ -269,15 +278,19 @@
        // 2. æ›´æ–°ä»»åŠ¡çš„å·²ç”Ÿäº§/合格/不合格数量
        taskService.updateProducedQuantity(feedback.getTaskId(),
                feedback.getFeedbackQuantity(), qualifiedQty, unqualifiedQty);
        // 3. æ›´æ–°å·¥å•的已生产数量
        // 3. æ›´æ–°è®¢å•的已生产数量
        // æ–°å¢žé€»è¾‘:只有在该笔订单最后一个工序才更新生产订单的已生产数量
        MesProWorkOrderProcessDO lastProcess = workOrderProcessService.getLastWorkOrderProcessByWorkOrderId(feedback.getWorkOrderId());
        if (lastProcess != null && ObjUtil.equal(lastProcess.getProcessId(), feedback.getProcessId())) {
        workOrderService.updateProducedQuantity(feedback.getWorkOrderId(),
                feedback.getFeedbackQuantity());
        // 4. æ›´æ–° MPS çš„已生产数量(如果是销售订单来源)
        MesProMpsRespDTO mps = mpsApi.getMpsByWorkOrderId(feedback.getWorkOrderId());
        if (mps != null) {
            mpsApi.updateMpsProducedQuantity(mps.getId(), feedback.getFeedbackQuantity());
        }
        }
        // 5. æ£€æŸ¥å¹¶å‘布生产完成事件
        checkAndPublishProductionFinished(feedback.getWorkOrderId());
@@ -342,6 +355,11 @@
        // 1. æ ¡éªŒä»»åŠ¡å­˜åœ¨ä¸”æœªç»ˆæ€
        MesProTaskDO task = taskService.validateTaskNotFinished(reqVO.getTaskId());
        // æ ¡éªŒåœå·¥çŠ¶æ€ä¸èƒ½æŠ¥å·¥
        if (!MesProTaskStatusEnum.canFeedback(task.getStatus())) {
            throw exception(PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED);
        }
        // 2. ä»Žä»»åŠ¡è‡ªåŠ¨å¡«å……å…³è”å­—æ®µï¼ˆå‰ç«¯æ— éœ€ä¼ å…¥ï¼ŒåŽç«¯ç»Ÿä¸€ä»Žä»»åŠ¡èŽ·å–ï¼‰
        reqVO.setWorkOrderId(task.getWorkOrderId());
        reqVO.setRouteId(task.getRouteId());
@@ -380,6 +398,12 @@
    public MesProFeedbackQuickCreateRespVO quickCreateFeedback(MesProFeedbackQuickCreateReqVO reqVO) {
        // 1. æ ¡éªŒä»»åŠ¡å­˜åœ¨ä¸”éžç»ˆæ€
        MesProTaskDO task = taskService.validateTaskNotFinished(reqVO.getTaskId());
        // æ ¡éªŒåœå·¥çŠ¶æ€ä¸èƒ½æŠ¥å·¥
        if (!MesProTaskStatusEnum.canFeedback(task.getStatus())) {
            throw exception(PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED);
        }
        // 2. æ ¡éªŒå·¥è‰ºè·¯çº¿å·¥åºé…ç½®
        MesProRouteProcessDO routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId(
                task.getRouteId(), task.getProcessId());
@@ -486,7 +510,8 @@
        // 4. æ›´æ–°ä»»åŠ¡/工单的已生产数量
        feedback.setQualifiedQuantity(qualifiedQty).setUnqualifiedQuantity(unqualifiedQty);
        updateTaskAndWorkOrderByFeedback(feedback);
        //5.检查并发布生产完成事件
        checkAndPublishProductionFinished(feedback.getWorkOrderId());
    }
    @Override
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskService.java
@@ -162,4 +162,18 @@
    List<Long> autoGenerateTasks(
            cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskAutoGenerateReqVO reqVO);
    /**
     * ç”Ÿäº§ä»»åŠ¡åœå·¥
     *
     * @param pauseReqVO åœå·¥è¯·æ±‚
     */
    void pauseTask(cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskPauseReqVO pauseReqVO);
    /**
     * ç”Ÿäº§ä»»åŠ¡å¤å·¥
     *
     * @param resumeReqVO å¤å·¥è¯·æ±‚
     */
    void resumeTask(cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskResumeReqVO resumeReqVO);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskServiceImpl.java
@@ -50,8 +50,16 @@
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_TASK_ALREADY_FINISHED;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_TASK_CANNOT_PAUSE;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_TASK_HAS_FEEDBACK;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_TASK_NOT_EXISTS;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_TASK_NOT_PAUSED;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskPauseRecordDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskPauseRecordMapper;
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskPauseReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskResumeReqVO;
/**
 * MES ç”Ÿäº§ä»»åŠ¡ Service å®žçŽ°ç±»
@@ -67,6 +75,9 @@
    @Resource
    private MesProFeedbackMapper feedbackMapper;
    @Resource
    private MesProTaskPauseRecordMapper taskPauseRecordMapper;
    @Resource
    @Lazy
@@ -429,4 +440,52 @@
        return taskIds;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void pauseTask(MesProTaskPauseReqVO pauseReqVO) {
        // 1. æ ¡éªŒä»»åŠ¡å­˜åœ¨
        MesProTaskDO task = validateTaskExists(pauseReqVO.getId());
        // 2. æ ¡éªŒçŠ¶æ€æ˜¯å¦å…è®¸åœå·¥ï¼ˆå¦‚å¿…é¡»æ˜¯ ç”Ÿäº§ä¸­ æˆ– éƒ¨åˆ†å®Œæˆï¼‰
        if (!ObjectUtils.equalsAny(task.getStatus(), MesProTaskStatusEnum.PREPARE.getStatus())) {
            throw exception(PRO_TASK_CANNOT_PAUSE);
        }
        // 3. ä¿®æ”¹ä»»åŠ¡çŠ¶æ€ä¸ºå·²åœå·¥
        taskMapper.updateById(new MesProTaskDO().setId(task.getId()).setStatus(MesProTaskStatusEnum.PAUSED.getStatus()));
        // 4. åˆ›å»ºåœå·¥è®°å½•
        MesProTaskPauseRecordDO pauseRecord = new MesProTaskPauseRecordDO()
                .setTaskId(task.getId())
                .setWorkOrderId(task.getWorkOrderId())
                .setWorkstationId(task.getWorkstationId())
                .setReason(pauseReqVO.getReason())
                .setRemark(pauseReqVO.getRemark())
                .setPauseTime(LocalDateTime.now())
                .setPauseUserId(SecurityFrameworkUtils.getLoginUserId());
        taskPauseRecordMapper.insert(pauseRecord);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void resumeTask(MesProTaskResumeReqVO resumeReqVO) {
        // 1. æ ¡éªŒä»»åŠ¡å­˜åœ¨
        MesProTaskDO task = validateTaskExists(resumeReqVO.getId());
        // 2. æ ¡éªŒçŠ¶æ€å¿…é¡»æ˜¯å·²åœå·¥
        if (!ObjectUtils.equalsAny(task.getStatus(), MesProTaskStatusEnum.PAUSED.getStatus())) {
            throw exception(PRO_TASK_NOT_PAUSED);
        }
        // 3. æ¢å¤ä»»åŠ¡çŠ¶æ€
        Integer resumeStatus = MesProTaskStatusEnum.PREPARE.getStatus();
        taskMapper.updateById(new MesProTaskDO().setId(task.getId()).setStatus(resumeStatus));
        // 4. æ›´æ–°æœ€æ–°çš„一条停工记录
        MesProTaskPauseRecordDO pauseRecord = taskPauseRecordMapper.selectLastUnresumedByTaskId(task.getId());
        if (pauseRecord != null) {
            pauseRecord.setResumeTime(LocalDateTime.now());
            pauseRecord.setResumeUserId(SecurityFrameworkUtils.getLoginUserId());
            taskPauseRecordMapper.updateById(pauseRecord);
        }
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderProcessService.java
@@ -62,6 +62,22 @@
    List<MesProWorkOrderProcessDO> getWorkOrderProcessListByWorkOrderId(Long workOrderId);
    /**
     * æ ¹æ®å·¥å•编号获得最后一道工序
     *
     * @param workOrderId å·¥å•编号
     * @return æœ€åŽä¸€é“工序
     */
    MesProWorkOrderProcessDO getLastWorkOrderProcessByWorkOrderId(Long workOrderId);
    /**
     * æ ¹æ®å·¥å•编号列表获得工序列表
     *
     * @param workOrderIds å·¥å•编号列表
     * @return å·¥åºåˆ—表
     */
    List<MesProWorkOrderProcessDO> getWorkOrderProcessListByWorkOrderIds(java.util.Collection<Long> workOrderIds);
    /**
     * æ ¹æ®å·¥å•编号删除工序
     *
     * @param workOrderId å·¥å•编号
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderProcessServiceImpl.java
@@ -122,6 +122,19 @@
    }
    @Override
    public MesProWorkOrderProcessDO getLastWorkOrderProcessByWorkOrderId(Long workOrderId) {
        return workOrderProcessMapper.selectLastByWorkOrderId(workOrderId);
    }
    @Override
    public List<MesProWorkOrderProcessDO> getWorkOrderProcessListByWorkOrderIds(java.util.Collection<Long> workOrderIds) {
        if (CollUtil.isEmpty(workOrderIds)) {
            return Collections.emptyList();
        }
        return workOrderProcessMapper.selectListByWorkOrderIds(workOrderIds);
    }
    @Override
    public void deleteWorkOrderProcessByWorkOrderId(Long workOrderId) {
        workOrderProcessMapper.deleteByWorkOrderId(workOrderId);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderService.java
@@ -75,12 +75,20 @@
    PageResult<MesProWorkOrderDO> getWorkOrderPage(MesProWorkOrderPageReqVO pageReqVO);
    /**
     * èŽ·å¾—ç”Ÿäº§å·¥å•åˆ†é¡µï¼ˆå¸¦è°ƒåº¦çŠ¶æ€ï¼‰
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return ç”Ÿäº§å·¥å•分页
     */
    PageResult<MesProWorkOrderDO> getWorkOrderSchedulePage(MesProWorkOrderPageReqVO pageReqVO);
    /**
     * èŽ·å¾—å·¥å•åˆ—è¡¨
     *
     * @param ids ç¼–号数组
     * @return å·¥å•列表
     */
    List<MesProWorkOrderDO> getWorkOrderList(Collection<Long> ids);
    List<MesProWorkOrderDO> getWorkOrderList(java.util.Collection<Long> ids);
    /**
     * èŽ·å¾—å·¥å• Map
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderServiceImpl.java
@@ -167,6 +167,13 @@
    }
    @Override
    public PageResult<MesProWorkOrderDO> getWorkOrderSchedulePage(MesProWorkOrderPageReqVO pageReqVO) {
        // èŽ·å–ç”¨æˆ·æœ‰æƒé™çš„å®¢æˆ· ID åˆ—表
        List<Long> permittedClientIds = customerApi.getPermittedCustomerIds();
        return workOrderMapper.selectSchedulePage(pageReqVO, permittedClientIds);
    }
    @Override
    public List<MesProWorkOrderDO> getWorkOrderList(Collection<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return Collections.emptyList();
yudao-module-mes/src/main/resources/mapper/pro/workorder/MesProWorkOrderMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,108 @@
<?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.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderMapper">
    <select id="selectSchedulePage" resultType="cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO">
        SELECT
            t.*
        FROM
            mes_pro_work_order t
        <where>
            t.deleted = 0
            <if test="pageReqVO.code != null and pageReqVO.code != ''">
                AND t.code LIKE CONCAT('%', #{pageReqVO.code}, '%')
            </if>
            <if test="pageReqVO.name != null and pageReqVO.name != ''">
                AND t.name LIKE CONCAT('%', #{pageReqVO.name}, '%')
            </if>
            <if test="pageReqVO.type != null">
                AND t.type = #{pageReqVO.type}
            </if>
            <if test="pageReqVO.orderSourceCode != null and pageReqVO.orderSourceCode != ''">
                AND t.order_source_code LIKE CONCAT('%', #{pageReqVO.orderSourceCode}, '%')
            </if>
            <if test="pageReqVO.productId != null">
                AND t.product_id = #{pageReqVO.productId}
            </if>
            <if test="pageReqVO.clientId != null">
                AND t.client_id = #{pageReqVO.clientId}
            </if>
            <if test="pageReqVO.status != null">
                AND t.status = #{pageReqVO.status}
            </if>
            <if test="pageReqVO.requestDate != null and pageReqVO.requestDate.length > 0">
                AND t.request_date >= #{pageReqVO.requestDate[0]}
                <if test="pageReqVO.requestDate.length > 1">
                    AND t.request_date &lt;= #{pageReqVO.requestDate[1]}
                </if>
            </if>
            <if test="permittedClientIds != null">
                <choose>
                    <when test="permittedClientIds.size() > 0">
                        AND (t.client_id IN
                        <foreach collection="permittedClientIds" item="id" open="(" separator="," close=")">
                            #{id}
                        </foreach>
                        OR t.client_id IS NULL)
                    </when>
                    <otherwise>
                        AND 1 = 0
                    </otherwise>
                </choose>
            </if>
            <if test="pageReqVO.scheduleStatus != null">
                <choose>
                    <!-- scheduleStatus: 1=已完成 (工单状态=4 æˆ–者 (有工序 ä¸” æ‰€æœ‰å·¥åºéƒ½æœ‰ä»»åŠ¡ ä¸” æ‰€æœ‰ä»»åŠ¡å·²å®Œæˆ)) -->
                    <when test="pageReqVO.scheduleStatus == 1">
                        AND (
                            t.status = 2
                            OR (
                                t.status != 4
                                AND EXISTS (SELECT 1 FROM mes_pro_work_order_process wp WHERE wp.work_order_id = t.id AND wp.deleted = 0)
                                AND NOT EXISTS (
                                    SELECT 1 FROM mes_pro_work_order_process wp
                                    WHERE wp.work_order_id = t.id AND wp.deleted = 0
                                    AND NOT EXISTS (
                                        SELECT 1 FROM mes_pro_task task
                                        WHERE task.work_order_id = t.id AND task.process_id = wp.process_id AND task.deleted = 0
                                    )
                                )
                                AND NOT EXISTS (
                                    SELECT 1 FROM mes_pro_task task
                                    WHERE task.work_order_id = t.id AND task.deleted = 0
                                    AND IFNULL(task.produced_quantity, 0) &lt; IFNULL(task.quantity, 0)
                                )
                            )
                        )
                    </when>
                    <!-- scheduleStatus: 0=未完成 (工单状态!=4 å¹¶ä¸” (无工序 æˆ– æœ‰æŸå·¥åºæ— ä»»åŠ¡ æˆ– å­˜åœ¨æœªå®Œæˆçš„任务)) -->
                    <when test="pageReqVO.scheduleStatus == 0">
                        AND (
                            t.status in (1)
                            AND (
                                NOT EXISTS (SELECT 1 FROM mes_pro_work_order_process wp WHERE wp.work_order_id = t.id AND wp.deleted = 0)
                                OR EXISTS (
                                    SELECT 1 FROM mes_pro_work_order_process wp
                                    WHERE wp.work_order_id = t.id AND wp.deleted = 0
                                    AND NOT EXISTS (
                                        SELECT 1 FROM mes_pro_task task
                                        WHERE task.work_order_id = t.id AND task.process_id = wp.process_id AND task.deleted = 0
                                    )
                                )
                                OR EXISTS (
                                    SELECT 1 FROM mes_pro_task task
                                    WHERE task.work_order_id = t.id AND task.deleted = 0
                                    AND IFNULL(task.produced_quantity, 0) &lt; IFNULL(task.quantity, 0)
                                )
                            )
                        )
                    </when>
                </choose>
            </if>
        </where>
        ORDER BY t.id DESC
    </select>
</mapper>
yudao-server/src/main/resources/application-local.yaml
@@ -75,7 +75,7 @@
          #          url: jdbc:postgresql://127.0.0.1:5432/postgres # OpenGauss è¿žæŽ¥çš„示例
          #          url: jdbc:postgresql://127.0.0.1:5866/ruoyi-vue-pro # ç€šé«˜ HighGo è¿žæŽ¥çš„示例
          username: root
          password: 123456
          password: root@2026
          #          username: sa # SQL Server è¿žæŽ¥çš„示例
          #          password: Yudao@2024 # SQL Server è¿žæŽ¥çš„示例
          #          username: SYSDBA # DM è¿žæŽ¥çš„示例
@@ -88,7 +88,7 @@
          lazy: true # å¼€å¯æ‡’加载,保证启动速度
          url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true
          username: root
          password: 123456
          password: root@2026
#        tdengine: # IoT æ•°æ®åº“(需要 IoT ç‰©è”网再开启噢!)
#          lazy: true # å¼€å¯æ‡’加载,保证启动速度
#          url: jdbc:TAOS-WS://127.0.0.1:6041/ruoyi_vue_pro?varcharAsString=true&enableAutoReconnect=true # å‚见:https://t.zsxq.com/qaX9c,开启 TDengine WebSocket è‡ªåŠ¨é‡è¿ž
@@ -104,7 +104,7 @@
      host: 127.0.0.1 # åœ°å€
      port: 6379 # ç«¯å£
      database: 0 # æ•°æ®åº“索引
#      password: dev # å¯†ç ï¼Œå»ºè®®ç”Ÿäº§çŽ¯å¢ƒå¼€å¯
      password: root@2026 # å¯†ç ï¼Œå»ºè®®ç”Ÿäº§çŽ¯å¢ƒå¼€å¯
--- #################### å®šæ—¶ä»»åŠ¡ç›¸å…³é…ç½® ####################