云
2026-10-26 0857cf1830039d60ce73005f386f284cf575e511
feat(mes): 新增ERP销售订单生产状态枚举及MES工艺路线优化

- 新增ErpSaleOrderProductionStatusEnum枚举,支持已取消状态
- 重构工艺路线工序产出配置,支持按产品归属配置产出物料
- 实现工艺路线工序产出归属验证和过滤逻辑
- 优化生产任务产出物料解析逻辑,支持按产品过滤产出
- 新增按工作站统计待报工任务数功能
- 修复物料名称唯一性校验逻辑,支持MDM主数据场景
- 优化工艺路线工序控制器,支持产出归属配置和默认仓库填充
- 新增PRO_ROUTE_LAST_OUTPUT_ATTRIBUTION_REQUIRED错误码定义
- 优化销售订单重新生成生产计划后状态重置逻辑
- 移除MesMdItemMapper中废弃的selectByName方法
已添加4个文件
已修改26个文件
816 ■■■■■ 文件已修改
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderDO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpSaleOrderProductionStatusEnum.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/listener/ProductionFinishedListener.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteProcessController.java 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessOutputVO.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessRespVO.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessSaveReqVO.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/MesProTaskController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/MesProWorkOrderProcessController.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/process/MesProWorkOrderProcessRespVO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/process/MesProWorkOrderProcessSaveReqVO.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProcessOutputDO.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/workorder/MesProWorkOrderProcessDO.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/md/item/MesMdItemMapper.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/route/MesProRouteProcessOutputMapper.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/task/MesProTaskMapper.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/workorder/MesProWorkOrderProcessMapper.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemServiceImpl.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProcessService.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProcessServiceImpl.java 190 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteServiceImpl.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskServiceImpl.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderProcessService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderProcessServiceImpl.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceServiceImpl.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderDO.java
@@ -127,7 +127,9 @@
    // ========== ç”Ÿäº§å…³è” ==========
    /**
     * ç”Ÿäº§çŠ¶æ€ï¼š0-未完成,1-已完成
     * ç”Ÿäº§çŠ¶æ€ï¼š0-未完成,1-已完成,2-已取消
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.erp.enums.ErpSaleOrderProductionStatusEnum}
     */
    private Integer productionStatus;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpSaleOrderProductionStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.erp.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
 * ERP é”€å”®è®¢å•生产状态枚举
 *
 * @author è¶…级管理员
 */
@RequiredArgsConstructor
@Getter
public enum ErpSaleOrderProductionStatusEnum {
    NOT_FINISHED(0, "未完成"),
    FINISHED(1, "已完成"),
    CANCELED(2, "已取消"); // ç”Ÿäº§è®¡åˆ’被取消
    private final Integer status;
    private final String name;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java
@@ -23,6 +23,7 @@
import cn.iocoder.yudao.module.erp.dal.mysql.sale.ErpSaleOrderMapper;
import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
import cn.iocoder.yudao.module.erp.enums.ErpSaleOrderProductionStatusEnum;
import cn.iocoder.yudao.module.erp.api.sale.enums.ErpSaleOrderOutStatusEnum;
import cn.iocoder.yudao.module.erp.service.finance.ErpAccountService;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
@@ -35,6 +36,7 @@
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -248,6 +250,13 @@
                    .setMpsId(mpsId));
            createdCount++;
        }
        // é‡æ–°ç”Ÿæˆç”Ÿäº§è®¡åˆ’后,订单生产状态回到未完成(覆盖此前的“已取消”)
        if (createdCount > 0) {
            saleOrderMapper.update(null, new LambdaUpdateWrapper<ErpSaleOrderDO>()
                    .eq(ErpSaleOrderDO::getId, orderId)
                    .set(ErpSaleOrderDO::getProductionStatus, ErpSaleOrderProductionStatusEnum.NOT_FINISHED.getStatus())
                    .set(ErpSaleOrderDO::getProductionFinishTime, null));
        }
        log.info("[createMpsForSaleOrder] é”€å”®è®¢å•({}) å®¡æ ¸å®Œæˆï¼Œåˆ›å»º MPS {} æ¡ï¼Œå¤ç”¨ MPS {} æ¡",
                orderId, createdCount, reusedCount);
    }
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/listener/ProductionFinishedListener.java
@@ -2,6 +2,7 @@
import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderDO;
import cn.iocoder.yudao.module.erp.dal.mysql.sale.ErpSaleOrderMapper;
import cn.iocoder.yudao.module.erp.enums.ErpSaleOrderProductionStatusEnum;
import cn.iocoder.yudao.module.mes.api.event.ProductionFinishedEvent;
import cn.iocoder.yudao.module.mes.api.event.ProductionCanceledEvent;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
@@ -46,9 +47,9 @@
        // ä½¿ç”¨æ˜¾å¼ update wrapper æ‰èƒ½æŠŠå·²å­˜åœ¨çš„完成时间真正更新为 NULL
        saleOrderMapper.update(null, new LambdaUpdateWrapper<ErpSaleOrderDO>()
                .eq(ErpSaleOrderDO::getId, event.getSaleOrderId())
                .set(ErpSaleOrderDO::getProductionStatus, 0)
                .set(ErpSaleOrderDO::getProductionStatus, ErpSaleOrderProductionStatusEnum.CANCELED.getStatus())
                .set(ErpSaleOrderDO::getProductionFinishTime, null));
        log.info("[onProductionCanceled] é”€å”®è®¢å• {} ç”Ÿäº§çŠ¶æ€å·²æ¢å¤ä¸ºæœªå®Œæˆ", event.getSaleOrderId());
        log.info("[onProductionCanceled] é”€å”®è®¢å• {} ç”Ÿäº§çŠ¶æ€å·²æ›´æ–°ä¸ºå·²å–æ¶ˆ", event.getSaleOrderId());
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteProcessController.java
@@ -6,15 +6,25 @@
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.process.MesProProcessBomItemVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.process.MesProRouteProcessOutputVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.process.MesProRouteProcessRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.process.MesProRouteProcessSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessOutputDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductBomDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -43,6 +53,14 @@
    private MesProProcessService processService;
    @Resource
    private MesMdItemService mdItemService;
    @Resource
    private MesProRouteProductService routeProductService;
    @Resource
    private MesWmWarehouseService warehouseService;
    @Resource
    private MesWmWarehouseLocationService warehouseLocationService;
    @Resource
    private MesWmWarehouseAreaService warehouseAreaService;
    @PostMapping("/create")
    @Operation(summary = "创建工艺路线工序")
@@ -111,7 +129,35 @@
                }
            }
        }
        // å¡«å……工艺路线产品上配置的默认报工仓库(下单时作为工序默认值)
        fillDefaultOutputWarehouse(result, productId);
        return success(result);
    }
    private void fillDefaultOutputWarehouse(List<MesProRouteProcessRespVO> list, Long productId) {
        if (CollUtil.isEmpty(list)) {
            return;
        }
        MesProRouteProductDO routeProduct = routeProductService.getRouteProductByItemId(productId);
        if (routeProduct == null || routeProduct.getWarehouseId() == null) {
            return;
        }
        MesWmWarehouseDO warehouse = warehouseService.getWarehouse(routeProduct.getWarehouseId());
        MesWmWarehouseLocationDO location = routeProduct.getLocationId() != null
                ? warehouseLocationService.getWarehouseLocation(routeProduct.getLocationId()) : null;
        MesWmWarehouseAreaDO area = routeProduct.getAreaId() != null
                ? warehouseAreaService.getWarehouseArea(routeProduct.getAreaId()) : null;
        String warehouseName = warehouse != null ? warehouse.getName() : null;
        String locationName = location != null ? location.getName() : null;
        String areaName = area != null ? area.getName() : null;
        for (MesProRouteProcessRespVO vo : list) {
            vo.setWarehouseId(routeProduct.getWarehouseId());
            vo.setWarehouseName(warehouseName);
            vo.setLocationId(routeProduct.getLocationId());
            vo.setLocationName(locationName);
            vo.setAreaId(routeProduct.getAreaId());
            vo.setAreaName(areaName);
        }
    }
    @GetMapping("/list-by-route")
@@ -161,6 +207,41 @@
        }
        Map<Long, MesMdItemDO> outputItemMap = allOutputItemIds.isEmpty()
                ? Collections.emptyMap() : mdItemService.getItemMap(allOutputItemIds);
        // 2.1 äº§å‡ºå½’属配置(productId ä¸ºç©ºè¡¨ç¤ºå¤šäº§å“é€šç”¨ï¼‰
        Map<Long, List<MesProRouteProcessOutputVO>> outputsByRouteProcessId = new HashMap<>();
        Set<Long> routeProcessIds = convertSet(list, MesProRouteProcessDO::getId);
        routeProcessIds.removeAll(Collections.singleton(null));
        if (CollUtil.isNotEmpty(routeProcessIds)) {
            List<MesProRouteProcessOutputDO> outputList = routeProcessService
                    .getRouteProcessOutputListByRouteProcessIds(routeProcessIds);
            if (CollUtil.isNotEmpty(outputList)) {
                Set<Long> ownerItemIds = new HashSet<>();
                outputList.forEach(output -> {
                    if (output.getProductId() != null) {
                        ownerItemIds.add(output.getProductId());
                    }
                    if (output.getItemId() != null) {
                        ownerItemIds.add(output.getItemId());
                    }
                });
                Map<Long, MesMdItemDO> ownerItemMap = mdItemService.getItemMap(ownerItemIds);
                for (MesProRouteProcessOutputDO output : outputList) {
                    MesProRouteProcessOutputVO outputVO = new MesProRouteProcessOutputVO();
                    outputVO.setId(output.getId());
                    outputVO.setRouteProcessId(output.getRouteProcessId());
                    outputVO.setProductId(output.getProductId());
                    outputVO.setItemId(output.getItemId());
                    outputVO.setSort(output.getSort());
                    outputVO.setRemark(output.getRemark());
                    MapUtils.findAndThen(ownerItemMap, output.getItemId(),
                            item -> outputVO.setItemCode(item.getCode()).setItemName(item.getName()));
                    MapUtils.findAndThen(ownerItemMap, output.getProductId(),
                            product -> outputVO.setProductName(product.getName()));
                    outputsByRouteProcessId.computeIfAbsent(output.getRouteProcessId(),
                            key -> new ArrayList<>()).add(outputVO);
                }
            }
        }
        // 3. æ‹¼æŽ¥ VO
        List<MesProRouteProcessRespVO> result = BeanUtils.toBean(list, MesProRouteProcessRespVO.class);
        for (int i = 0; i < result.size(); i++) {
@@ -188,6 +269,7 @@
                    vo.setOutputItemName(firstItem.getName());
                }
            }
            vo.setOutputs(outputsByRouteProcessId.getOrDefault(vo.getId(), Collections.emptyList()));
        }
        return result;
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessOutputVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.process;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - MES å·¥è‰ºè·¯çº¿å·¥åºäº§å‡ºå½’属 Response VO")
@Data
public class MesProRouteProcessOutputVO {
    @Schema(description = "编号", example = "1")
    private Long id;
    @Schema(description = "工艺路线工序编号", example = "1")
    private Long routeProcessId;
    @Schema(description = "归属产品编号,为空表示多产品通用")
    private Long productId;
    @Schema(description = "归属产品名称")
    private String productName;
    @Schema(description = "产出物料编号", example = "100")
    private Long itemId;
    @Schema(description = "产出物料编码")
    private String itemCode;
    @Schema(description = "产出物料名称")
    private String itemName;
    @Schema(description = "排序", example = "1")
    private Integer sort;
    @Schema(description = "备注")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessRespVO.java
@@ -70,6 +70,27 @@
    @Schema(description = "产出产品物料名称(逗号拼接)", example = "产品A, äº§å“B")
    private String outputItemNames;
    @Schema(description = "产出物料归属配置(productId ä¸ºç©ºè¡¨ç¤ºå¤šäº§å“é€šç”¨ï¼‰")
    private List<MesProRouteProcessOutputVO> outputs;
    @Schema(description = "默认报工仓库编号(取自工艺路线产品的报工仓库配置)", example = "10")
    private Long warehouseId;
    @Schema(description = "默认报工仓库名称")
    private String warehouseName;
    @Schema(description = "默认报工库区编号", example = "110")
    private Long locationId;
    @Schema(description = "默认报工库区名称")
    private String locationName;
    @Schema(description = "默认报工库位编号", example = "210")
    private Long areaId;
    @Schema(description = "默认报工库位名称")
    private String areaName;
    @Schema(description = "备注")
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessSaveReqVO.java
@@ -49,7 +49,28 @@
    @Schema(description = "产出产品物料编号列表(多选,为空时默认用工单成品)", example = "[100, 101]")
    private List<Long> outputItemIds;
    @Schema(description = "产出物料归属配置(优先于 outputItemIds;productId ä¸ºç©ºè¡¨ç¤ºå¤šäº§å“é€šç”¨ï¼‰")
    private List<OutputItem> outputs;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "产出物料归属")
    @Data
    public static class OutputItem {
        @Schema(description = "归属产品编号,为空表示多产品通用")
        private Long productId;
        @Schema(description = "产出物料编号", requiredMode = Schema.RequiredMode.REQUIRED)
        private Long itemId;
        @Schema(description = "排序")
        private Integer sort;
        @Schema(description = "备注")
        private String remark;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/MesProTaskController.java
@@ -222,6 +222,13 @@
        return success(new PageResult<>(respVOList, pageResult.getTotal()));
    }
    @GetMapping("/pending-count-by-workstation")
    @Operation(summary = "按工作站获得待报工任务数", description = "工作台选择工作站卡片展示,口径同待报工台账")
    @PreAuthorize("@ss.hasPermission('mes:pro-task:query')")
    public CommonResult<Map<Long, Long>> getPendingFeedbackCountByWorkstation() {
        return success(taskService.getPendingFeedbackCountByWorkstation());
    }
    @GetMapping("/gantt-list")
    @Operation(summary = "获得甘特图任务列表", description = "后端组装工单=project + ä»»åŠ¡=task åˆ—表")
    @PreAuthorize("@ss.hasPermission('mes:pro-task:query')")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/MesProWorkOrderProcessController.java
@@ -14,10 +14,16 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderBomDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
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.wm.warehouse.MesWmWarehouseAreaService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -47,6 +53,12 @@
    private MesProProcessService processService;
    @Resource
    private MesMdItemService itemService;
    @Resource
    private MesWmWarehouseService warehouseService;
    @Resource
    private MesWmWarehouseLocationService warehouseLocationService;
    @Resource
    private MesWmWarehouseAreaService warehouseAreaService;
    @PostMapping("/create")
    @Operation(summary = "创建工单工序")
@@ -150,6 +162,19 @@
        }
        Map<Long, MesMdItemDO> itemMap = CollUtil.isNotEmpty(allOutputItemIds)
                ? itemService.getItemMap(allOutputItemIds) : Collections.emptyMap();
        // èŽ·å–æŠ¥å·¥ä»“åº“/库区/库位
        Set<Long> warehouseIds = convertSet(list, MesProWorkOrderProcessDO::getWarehouseId);
        warehouseIds.removeAll(Collections.singleton(null));
        Set<Long> locationIds = convertSet(list, MesProWorkOrderProcessDO::getLocationId);
        locationIds.removeAll(Collections.singleton(null));
        Set<Long> areaIds = convertSet(list, MesProWorkOrderProcessDO::getAreaId);
        areaIds.removeAll(Collections.singleton(null));
        Map<Long, MesWmWarehouseDO> warehouseMap = CollUtil.isNotEmpty(warehouseIds)
                ? warehouseService.getWarehouseMap(warehouseIds) : Collections.emptyMap();
        Map<Long, MesWmWarehouseLocationDO> locationMap = CollUtil.isNotEmpty(locationIds)
                ? warehouseLocationService.getWarehouseLocationMap(locationIds) : Collections.emptyMap();
        Map<Long, MesWmWarehouseAreaDO> areaMap = CollUtil.isNotEmpty(areaIds)
                ? warehouseAreaService.getWarehouseAreaMap(areaIds) : Collections.emptyMap();
        // 2. æŸ¥è¯¢æŠ•æ–™BOM(按工序分组)
        Set<Long> workOrderIds = convertSet(list, MesProWorkOrderProcessDO::getWorkOrderId);
@@ -185,6 +210,13 @@
            MapUtils.findAndThen(processMap, vo.getNextProcessId(), process -> {
                vo.setNextProcessName(process.getName());
            });
            // æŠ¥å·¥ä»“库/库区/库位名称
            MapUtils.findAndThen(warehouseMap, vo.getWarehouseId(),
                    warehouse -> vo.setWarehouseName(warehouse.getName()));
            MapUtils.findAndThen(locationMap, vo.getLocationId(),
                    location -> vo.setLocationName(location.getName()));
            MapUtils.findAndThen(areaMap, vo.getAreaId(),
                    area -> vo.setAreaName(area.getName()));
            // äº§å‡ºç‰©æ–™ï¼ˆæ”¯æŒå¤šé€‰ï¼‰
            List<Long> ids = voOutputItemIdsMap.getOrDefault((long) i, Collections.emptyList());
            if (!ids.isEmpty()) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/process/MesProWorkOrderProcessRespVO.java
@@ -83,6 +83,24 @@
    @Schema(description = "产出产品物料名称(逗号拼接)", example = "产品A, äº§å“B")
    private String outputItemNames;
    @Schema(description = "报工仓库编号", example = "10")
    private Long warehouseId;
    @Schema(description = "报工仓库名称", example = "成品仓")
    private String warehouseName;
    @Schema(description = "报工库区编号", example = "110")
    private Long locationId;
    @Schema(description = "报工库区名称", example = "成品仓通用库区")
    private String locationName;
    @Schema(description = "报工库位编号", example = "210")
    private Long areaId;
    @Schema(description = "报工库位名称", example = "成品仓默认库位")
    private String areaName;
    @Schema(description = "备注", example = "备注")
    @ExcelProperty("备注")
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/process/MesProWorkOrderProcessSaveReqVO.java
@@ -52,6 +52,15 @@
    @Schema(description = "产出产品物料编号列表(多选,为空时默认用工单成品)", example = "[100, 101]")
    private List<Long> outputItemIds;
    @Schema(description = "报工仓库编号", example = "10")
    private Long warehouseId;
    @Schema(description = "报工库区编号", example = "110")
    private Long locationId;
    @Schema(description = "报工库位编号", example = "210")
    private Long areaId;
    @Schema(description = "备注")
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProcessOutputDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.pro.route;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
 * MES å·¥è‰ºè·¯çº¿å·¥åºäº§å‡ºå½’属 DO
 *
 * åŒä¸€é“工序的产出可以按产品区分:归属产品为空表示该路线下所有产品通用。
 *
 * @author è¶…级管理员
 */
@TableName("mes_pro_route_process_output")
@KeySequence("mes_pro_route_process_output_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesProRouteProcessOutputDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * å·¥è‰ºè·¯çº¿å·¥åºç¼–号
     *
     * å…³è” {@link MesProRouteProcessDO#getId()}
     */
    private Long routeProcessId;
    /**
     * å·¥è‰ºè·¯çº¿ç¼–号
     *
     * å…³è” {@link MesProRouteDO#getId()}
     */
    private Long routeId;
    /**
     * å·¥åºç¼–号
     *
     * å…³è” {@link MesProProcessDO#getId()}
     */
    private Long processId;
    /**
     * å½’属产品编号,为空表示多产品通用
     *
     * å…³è” {@link MesMdItemDO#getId()}
     */
    private Long productId;
    /**
     * äº§å‡ºç‰©æ–™ç¼–号
     *
     * å…³è” {@link MesMdItemDO#getId()}
     */
    private Long itemId;
    /**
     * æŽ’序
     */
    private Integer sort;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/workorder/MesProWorkOrderProcessDO.java
@@ -90,6 +90,25 @@
     */
    private String outputItemId;
    /**
     * æŠ¥å·¥ä»“库编号
     *
     * ä¸‹å•时从工艺路线产品带入,可手工调整;报工产出按此仓库入库
     * å…³è” {@link cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO#getId()}
     */
    private Long warehouseId;
    /**
     * æŠ¥å·¥åº“区编号
     *
     * å…³è” {@link cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO#getId()}
     */
    private Long locationId;
    /**
     * æŠ¥å·¥åº“位编号
     *
     * å…³è” {@link cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO#getId()}
     */
    private Long areaId;
    /**
     * å¤‡æ³¨
     */
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/md/item/MesMdItemMapper.java
@@ -41,10 +41,6 @@
        return selectOne(MesMdItemDO::getCode, code);
    }
    default MesMdItemDO selectByName(String name) {
        return selectOne(MesMdItemDO::getName, name);
    }
    default Long selectCountByItemTypeId(Long itemTypeId) {
        return selectCount(MesMdItemDO::getItemTypeId, itemTypeId);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/route/MesProRouteProcessOutputMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
package cn.iocoder.yudao.module.mes.dal.mysql.pro.route;
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.route.MesProRouteProcessOutputDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
 * MES å·¥è‰ºè·¯çº¿å·¥åºäº§å‡ºå½’属 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesProRouteProcessOutputMapper extends BaseMapperX<MesProRouteProcessOutputDO> {
    default List<MesProRouteProcessOutputDO> selectListByRouteProcessId(Long routeProcessId) {
        return selectList(new LambdaQueryWrapperX<MesProRouteProcessOutputDO>()
                .eq(MesProRouteProcessOutputDO::getRouteProcessId, routeProcessId)
                .orderByAsc(MesProRouteProcessOutputDO::getSort));
    }
    default List<MesProRouteProcessOutputDO> selectListByRouteProcessIds(Collection<Long> routeProcessIds) {
        return selectList(new LambdaQueryWrapperX<MesProRouteProcessOutputDO>()
                .in(MesProRouteProcessOutputDO::getRouteProcessId, routeProcessIds)
                .orderByAsc(MesProRouteProcessOutputDO::getSort));
    }
    default List<MesProRouteProcessOutputDO> selectListByRouteId(Long routeId) {
        return selectList(new LambdaQueryWrapperX<MesProRouteProcessOutputDO>()
                .eq(MesProRouteProcessOutputDO::getRouteId, routeId)
                .orderByAsc(MesProRouteProcessOutputDO::getSort));
    }
    default void deleteByRouteProcessId(Long routeProcessId) {
        delete(new LambdaQueryWrapperX<MesProRouteProcessOutputDO>()
                .eq(MesProRouteProcessOutputDO::getRouteProcessId, routeProcessId));
    }
    default void deleteByRouteId(Long routeId) {
        delete(new LambdaQueryWrapperX<MesProRouteProcessOutputDO>()
                .eq(MesProRouteProcessOutputDO::getRouteId, routeId));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/task/MesProTaskMapper.java
@@ -67,6 +67,22 @@
        return selectPage(reqVO, query);
    }
    /**
     * æŒ‰å·¥ä½œç«™ç»Ÿè®¡å¾…报工任务数,口径与 {@link #selectPendingFeedbackPage} ä¸€è‡´
     *
     * @return æ¯è¡ŒåŒ…含 workstation_id、pending_count
     */
    default List<Map<String, Object>> selectPendingFeedbackCountByWorkstation() {
        return selectMaps(new QueryWrapper<MesProTaskDO>()
                .select("workstation_id", "COUNT(*) AS pending_count")
                .in("status", MesProTaskStatusEnum.PREPARE.getStatus(),
                        MesProTaskStatusEnum.PRODUCING.getStatus(), MesProTaskStatusEnum.PARTIAL_FINISHED.getStatus(),
                        MesProTaskStatusEnum.PAUSED.getStatus())
                .apply("(quantity - IFNULL(produced_quantity, 0)) > 0")
                .isNotNull("workstation_id")
                .groupBy("workstation_id"));
    }
    default List<MesProTaskDO> selectListByWorkOrderId(Long workOrderId) {
        return selectList(new LambdaQueryWrapperX<MesProTaskDO>()
                .eqIfPresent(MesProTaskDO::getWorkOrderId, workOrderId)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/workorder/MesProWorkOrderProcessMapper.java
@@ -5,6 +5,7 @@
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.process.MesProWorkOrderProcessPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderProcessDO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@@ -29,6 +30,14 @@
                .orderByAsc(MesProWorkOrderProcessDO::getSort));
    }
    default MesProWorkOrderProcessDO selectByWorkOrderIdAndProcessId(Long workOrderId, Long processId) {
        return selectOne(new LambdaQueryWrapperX<MesProWorkOrderProcessDO>()
                .eq(MesProWorkOrderProcessDO::getWorkOrderId, workOrderId)
                .eq(MesProWorkOrderProcessDO::getProcessId, processId)
                .orderByAsc(MesProWorkOrderProcessDO::getSort)
                .last("LIMIT 1"));
    }
    default MesProWorkOrderProcessDO selectLastByWorkOrderId(Long workOrderId) {
        return selectOne(new LambdaQueryWrapperX<MesProWorkOrderProcessDO>()
                .eq(MesProWorkOrderProcessDO::getWorkOrderId, workOrderId)
@@ -36,6 +45,17 @@
                .last("LIMIT 1"));
    }
    /**
     * æ›´æ–°æŠ¥å·¥ä»“库:允许置空(updateById ä¼šå¿½ç•¥ null å­—段,故单独处理)
     */
    default void updateWarehouseById(Long id, Long warehouseId, Long locationId, Long areaId) {
        update(null, new LambdaUpdateWrapper<MesProWorkOrderProcessDO>()
                .set(MesProWorkOrderProcessDO::getWarehouseId, warehouseId)
                .set(MesProWorkOrderProcessDO::getLocationId, locationId)
                .set(MesProWorkOrderProcessDO::getAreaId, areaId)
                .eq(MesProWorkOrderProcessDO::getId, id));
    }
    default void deleteByWorkOrderId(Long workOrderId) {
        delete(new LambdaQueryWrapperX<MesProWorkOrderProcessDO>()
                .eq(MesProWorkOrderProcessDO::getWorkOrderId, workOrderId));
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -294,6 +294,7 @@
    ErrorCode PRO_ROUTE_PROCESS_OUTPUT_NOT_BOUND = new ErrorCode(1_040_501_205, "工序产出产品【{}】未绑定到当前工艺路线");
    ErrorCode PRO_ROUTE_MIDDLE_OUTPUT_INVALID = new ErrorCode(1_040_501_206, "中间工序【{}】只能产出半成品分类产品");
    ErrorCode PRO_ROUTE_LAST_OUTPUT_INVALID = new ErrorCode(1_040_501_207, "末道工序【{}】只能产出成品分类产品,且必须包含全部绑定成品");
    ErrorCode PRO_ROUTE_LAST_OUTPUT_ATTRIBUTION_REQUIRED = new ErrorCode(1_040_501_208, "末道工序【{}】的成品产出必须按产品归属配置,每个成品归属到自身,不能使用“通用”");
    // ========== MES ç”Ÿäº§ç®¡ç†-工艺路线产品BOM(1-040-501-300) ==========
    ErrorCode PRO_ROUTE_PRODUCT_BOM_NOT_EXISTS = new ErrorCode(1_040_501_300, "工艺路线产品 BOM ä¸å­˜åœ¨");
    ErrorCode PRO_ROUTE_PRODUCT_BOM_DUPLICATE = new ErrorCode(1_040_501_301, "当前 BOM ç‰©æ–™åœ¨æ­¤å·¥åºå·²ç»é…ç½®è¿‡");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemServiceImpl.java
@@ -135,8 +135,10 @@
    private void validateItemSaveData(MesMdItemSaveReqVO reqVO) {
        // æ ¡éªŒç‰©æ–™ç¼–码的唯一性
        validateItemCodeUnique(reqVO.getId(), reqVO.getCode());
        // æ ¡éªŒç‰©æ–™åç§°çš„唯一性
        validateItemNameUnique(reqVO.getId(), reqVO.getName());
        // æ ¡éªŒç‰©æ–™åç§°çš„唯一性:MDM ä¸»æ•°æ®ä»¥ç¼–码为唯一标识,允许同名不同规格
        if (reqVO.getMdmItemId() == null) {
            validateItemNameUnique(reqVO.getId(), reqVO.getName());
        }
        // æ ¡éªŒç‰©æ–™åˆ†ç±»å­˜åœ¨
        validateItemTypeExists(reqVO.getItemTypeId());
        // æ ¡éªŒè®¡é‡å•位存在
@@ -212,11 +214,9 @@
    }
    private void validateItemNameUnique(Long id, String name) {
        MesMdItemDO item = itemMapper.selectByName(name);
        if (item == null) {
            return;
        }
        if (ObjUtil.notEqual(item.getId(), id)) {
        // ä½¿ç”¨åˆ—表查询:存量数据可能存在同名物料,避免 selectOne ç›´æŽ¥æŠ›å¼‚常
        List<MesMdItemDO> items = itemMapper.selectList(MesMdItemDO::getName, name);
        if (items.stream().anyMatch(item -> ObjUtil.notEqual(item.getId(), id))) {
            throw exception(MD_ITEM_NAME_DUPLICATE);
        }
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
@@ -114,8 +114,9 @@
        // 1. æ ¡éªŒ
        MesProTaskDO task = validateFeedbackData(createReqVO);
        // 2. æ’入(自动填充 itemId)
        // 2. æ’入(主键由数据库自增,忽略请求中可能残留的 id;自动填充 itemId)
        MesProFeedbackDO feedback = BeanUtils.toBean(createReqVO, MesProFeedbackDO.class)
                .setId(null)
                .setStatus(MesProFeedbackStatusEnum.PREPARE.getStatus())
                .setItemId(task.getItemId());
        feedbackMapper.insert(feedback);
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java
@@ -617,8 +617,9 @@
        }
        Long routeId = routeProduct.getRouteId();
        // 2. æ ¡éªŒå·¥è‰ºè·¯çº¿æ˜¯å¦æœ‰å·¥åº
        List<MesProRouteProcessDO> routeProcesses = routeProcessService.getRouteProcessListByRouteId(routeId);
        // 2. æ ¡éªŒå·¥è‰ºè·¯çº¿æ˜¯å¦æœ‰å·¥åºï¼ˆæŒ‰äº§å“è¿‡æ»¤å…¶å®ƒäº§å“çš„产出物料)
        List<MesProRouteProcessDO> routeProcesses = routeProcessService
                .getRouteProcessListByProductId(mps.getProductId());
        if (CollUtil.isEmpty(routeProcesses)) {
            throw exception(PRO_ROUTE_ENABLE_NO_PROCESS);
        }
@@ -658,6 +659,10 @@
                        .map(Long::valueOf).collect(Collectors.toList()));
            }
            process.setColorCode(rp.getColorCode());
            // å¸¦å…¥å·¥è‰ºè·¯çº¿äº§å“é…ç½®çš„æŠ¥å·¥ä»“库
            process.setWarehouseId(routeProduct.getWarehouseId());
            process.setLocationId(routeProduct.getLocationId());
            process.setAreaId(routeProduct.getAreaId());
            // å¸¦å…¥å·¥åºæŠ•æ–™BOM
            List<MesProRouteProductBomDO> routeBomItems = bomMap != null ? bomMap.get(rp.getProcessId()) : null;
            if (CollUtil.isNotEmpty(routeBomItems)) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProcessService.java
@@ -67,6 +67,27 @@
    List<MesProRouteProcessDO> getRouteProcessListByProductId(Long productId);
    /**
     * èŽ·å¾—å·¥åºåœ¨æŒ‡å®šäº§å“ä¸‹çš„äº§å‡ºç‰©æ–™ç¼–å·
     *
     * æŒ‰äº§å‡ºå½’属过滤:归属为空(多产品通用)或归属等于指定产品的产出。
     * æ— å½’属数据时返回 {@code null},调用方需自行回退;有归属数据但该产品无产出时返回空列表。
     *
     * @param routeProcessId å·¥è‰ºè·¯çº¿å·¥åºç¼–号
     * @param productId      äº§å“ç¼–号
     * @return äº§å‡ºç‰©æ–™ç¼–号列表(按排序),无归属数据时为 null
     */
    List<Long> getOutputItemIdsByRouteProcessId(Long routeProcessId, Long productId);
    /**
     * æŒ‰å·¥è‰ºè·¯çº¿å·¥åºç¼–号批量获得产出归属配置
     *
     * @param routeProcessIds å·¥è‰ºè·¯çº¿å·¥åºç¼–号数组
     * @return äº§å‡ºå½’属列表
     */
    List<cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessOutputDO>
        getRouteProcessOutputListByRouteProcessIds(Collection<Long> routeProcessIds);
    /**
     * æŒ‰äº§å“èŽ·å¾—å·¥åºæŠ•æ–™BOM Map
     *
     * key = å·¥åºID,value = è¯¥å·¥åºä¸‹çš„æŠ•æ–™BOM明细列表
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProcessServiceImpl.java
@@ -1,12 +1,15 @@
package cn.iocoder.yudao.module.mes.service.pro.route;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.NumberUtil;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.process.MesProRouteProcessSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessOutputDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductBomDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.route.MesProRouteProcessMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.route.MesProRouteProcessOutputMapper;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
@@ -14,11 +17,16 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -35,6 +43,8 @@
    @Resource
    private MesProRouteProcessMapper routeProcessMapper;
    @Resource
    private MesProRouteProcessOutputMapper routeProcessOutputMapper;
    @Resource
    @Lazy
@@ -63,13 +73,10 @@
        // 2. æ’å…¥
        MesProRouteProcessDO routeProcess = BeanUtils.toBean(createReqVO, MesProRouteProcessDO.class);
        if (CollUtil.isNotEmpty(createReqVO.getOutputItemIds())) {
            routeProcess.setOutputItemId(
                createReqVO.getOutputItemIds().stream()
                    .map(String::valueOf)
                    .collect(Collectors.joining(",")));
        }
        routeProcess.setOutputItemId(resolveOutputItemId(createReqVO));
        routeProcessMapper.insert(routeProcess);
        // 2.1 ä¿å­˜äº§å‡ºå½’属配置
        saveRouteProcessOutputs(routeProcess, createReqVO);
        // 3. æ›´æ–°å·¥åºé“¾
        rebuildProcessChain(createReqVO.getRouteId());
@@ -93,11 +100,10 @@
        // 2. æ›´æ–°
        MesProRouteProcessDO updateObj = BeanUtils.toBean(updateReqVO, MesProRouteProcessDO.class);
        // ç©ºæ•°ç»„也要显式写入空串,否则 MyBatis-Plus é»˜è®¤å¿½ç•¥ null,用户清空产出品后旧配置仍会残留。
        updateObj.setOutputItemId(CollUtil.isEmpty(updateReqVO.getOutputItemIds()) ? "" :
                updateReqVO.getOutputItemIds().stream()
                        .map(String::valueOf)
                        .collect(Collectors.joining(",")));
        updateObj.setOutputItemId(resolveOutputItemId(updateReqVO));
        routeProcessMapper.updateById(updateObj);
        // 2.1 è¦†ç›–产出归属配置
        saveRouteProcessOutputs(updateObj, updateReqVO);
        // 3. é‡å»ºå·¥åºé“¾
        rebuildProcessChain(updateReqVO.getRouteId());
@@ -111,8 +117,9 @@
        // 1.2 å·²å¯ç”¨çš„工艺路线,不允许操作
        routeService.validateRouteNotEnable(routeProcess.getRouteId());
        // 2. åˆ é™¤
        // 2. åˆ é™¤ï¼ˆå«äº§å‡ºå½’属配置)
        routeProcessMapper.deleteById(id);
        routeProcessOutputMapper.deleteByRouteProcessId(id);
        // 3. é‡å»ºå·¥åºé“¾
        rebuildProcessChain(routeProcess.getRouteId());
@@ -211,7 +218,165 @@
            return Collections.emptyList();
        }
        // 2. è¿”回该工艺路线的工序列表
        return routeProcessMapper.selectListByRouteId(routeProduct.getRouteId());
        List<MesProRouteProcessDO> list = routeProcessMapper.selectListByRouteId(routeProduct.getRouteId());
        // 3. æŒ‰äº§å‡ºå½’属过滤:通用产出 + å½“前产品专属产出
        Map<Long, List<MesProRouteProcessOutputDO>> outputMap = routeProcessOutputMapper
                .selectListByRouteId(routeProduct.getRouteId()).stream()
                .collect(Collectors.groupingBy(MesProRouteProcessOutputDO::getRouteProcessId));
        for (MesProRouteProcessDO process : list) {
            List<MesProRouteProcessOutputDO> outputs = outputMap.get(process.getId());
            if (CollUtil.isEmpty(outputs)) {
                // å­˜é‡æ•°æ®æœªæ‹†åˆ†å½’属时,退回旧的“剔除同路线其它产品”逻辑
                filterOtherProductOutputs(Collections.singletonList(process), routeProduct);
                continue;
            }
            String filtered = outputs.stream()
                    .filter(output -> output.getProductId() == null || output.getProductId().equals(productId))
                    .map(MesProRouteProcessOutputDO::getItemId)
                    .filter(Objects::nonNull)
                    .distinct()
                    .map(String::valueOf)
                    .collect(Collectors.joining(","));
            process.setOutputItemId(filtered.isEmpty() ? null : filtered);
        }
        return list;
    }
    @Override
    public List<Long> getOutputItemIdsByRouteProcessId(Long routeProcessId, Long productId) {
        List<MesProRouteProcessOutputDO> outputs = routeProcessOutputMapper.selectListByRouteProcessId(routeProcessId);
        if (CollUtil.isEmpty(outputs)) {
            // æœªé…ç½®äº§å‡ºå½’属(存量数据)返回 null,便于调用方回退聚合产出
            return null;
        }
        return outputs.stream()
                .filter(output -> output.getProductId() == null || output.getProductId().equals(productId))
                .map(MesProRouteProcessOutputDO::getItemId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
    }
    @Override
    public List<MesProRouteProcessOutputDO> getRouteProcessOutputListByRouteProcessIds(Collection<Long> routeProcessIds) {
        if (CollUtil.isEmpty(routeProcessIds)) {
            return Collections.emptyList();
        }
        return routeProcessOutputMapper.selectListByRouteProcessIds(routeProcessIds);
    }
    /**
     * è®¡ç®—工序产出物料的聚合串(逗号分隔,去重)
     *
     * ä¼˜å…ˆä½¿ç”¨äº§å‡ºå½’属配置(outputs),未传时兼容 outputItemIds。
     */
    private String resolveOutputItemId(MesProRouteProcessSaveReqVO reqVO) {
        List<Long> itemIds;
        if (CollUtil.isNotEmpty(reqVO.getOutputs())) {
            itemIds = reqVO.getOutputs().stream()
                    .filter(Objects::nonNull)
                    .sorted(Comparator.comparing(MesProRouteProcessSaveReqVO.OutputItem::getSort,
                            Comparator.nullsLast(Comparator.naturalOrder())))
                    .map(MesProRouteProcessSaveReqVO.OutputItem::getItemId)
                    .filter(Objects::nonNull)
                    .distinct()
                    .collect(Collectors.toList());
        } else if (CollUtil.isNotEmpty(reqVO.getOutputItemIds())) {
            itemIds = reqVO.getOutputItemIds().stream()
                    .filter(Objects::nonNull)
                    .distinct()
                    .collect(Collectors.toList());
        } else {
            itemIds = Collections.emptyList();
        }
        return itemIds.stream().map(String::valueOf).collect(Collectors.joining(","));
    }
    /**
     * è¦†ç›–保存工序产出归属配置
     */
    private void saveRouteProcessOutputs(MesProRouteProcessDO routeProcess,
                                         MesProRouteProcessSaveReqVO reqVO) {
        routeProcessOutputMapper.deleteByRouteProcessId(routeProcess.getId());
        if (CollUtil.isEmpty(reqVO.getOutputs()) && CollUtil.isEmpty(reqVO.getOutputItemIds())) {
            return;
        }
        Set<Long> routeProductIds = routeProductService.getRouteProductListByRouteId(routeProcess.getRouteId())
                .stream().map(MesProRouteProductDO::getItemId).filter(Objects::nonNull).collect(Collectors.toSet());
        List<MesProRouteProcessOutputDO> outputs = new ArrayList<>();
        Set<String> keys = new HashSet<>();
        int index = 0;
        if (CollUtil.isNotEmpty(reqVO.getOutputs())) {
            for (MesProRouteProcessSaveReqVO.OutputItem output : reqVO.getOutputs()) {
                if (output == null || output.getItemId() == null) {
                    continue;
                }
                index++;
                if (!keys.add(output.getProductId() + "_" + output.getItemId())) {
                    continue;
                }
                outputs.add(buildOutput(routeProcess, output.getProductId(), output.getItemId(),
                        output.getSort() != null ? output.getSort() : index, output.getRemark()));
            }
        } else {
            // å…¼å®¹åªä¼  outputItemIds çš„调用方:产出是路线产品时归属到自身,否则视为通用
            for (Long itemId : reqVO.getOutputItemIds()) {
                if (itemId == null) {
                    continue;
                }
                index++;
                Long productId = routeProductIds.contains(itemId) ? itemId : null;
                if (!keys.add(productId + "_" + itemId)) {
                    continue;
                }
                outputs.add(buildOutput(routeProcess, productId, itemId, index, null));
            }
        }
        if (CollUtil.isNotEmpty(outputs)) {
            routeProcessOutputMapper.insertBatch(outputs);
        }
    }
    private MesProRouteProcessOutputDO buildOutput(MesProRouteProcessDO routeProcess, Long productId,
                                                   Long itemId, Integer sort, String remark) {
        return MesProRouteProcessOutputDO.builder()
                .routeProcessId(routeProcess.getId())
                .routeId(routeProcess.getRouteId())
                .processId(routeProcess.getProcessId())
                .productId(productId)
                .itemId(itemId)
                .sort(sort)
                .remark(remark)
                .build();
    }
    /**
     * è¿‡æ»¤äº§å‡ºç‰©æ–™ï¼šå‰”除同一工艺路线下其它产品的产出,只保留当前产品的产出和半成品产出
     */
    private void filterOtherProductOutputs(List<MesProRouteProcessDO> list, MesProRouteProductDO routeProduct) {
        if (CollUtil.isEmpty(list) || routeProduct.getItemId() == null) {
            return;
        }
        Set<Long> otherProductIds = routeProductService.getRouteProductListByRouteId(routeProduct.getRouteId())
                .stream()
                .map(MesProRouteProductDO::getItemId)
                .filter(itemId -> itemId != null && !itemId.equals(routeProduct.getItemId()))
                .collect(Collectors.toSet());
        if (otherProductIds.isEmpty()) {
            return;
        }
        for (MesProRouteProcessDO process : list) {
            String outputItemId = process.getOutputItemId();
            if (outputItemId == null || outputItemId.isBlank()) {
                continue;
            }
            String filtered = Arrays.stream(outputItemId.split(","))
                    .map(String::trim)
                    .filter(s -> !s.isEmpty())
                    .filter(s -> !otherProductIds.contains(NumberUtil.parseLong(s, -1L)))
                    .collect(Collectors.joining(","));
            process.setOutputItemId(filtered.isEmpty() ? null : filtered);
        }
    }
    @Override
@@ -236,6 +401,7 @@
    @Override
    public void deleteRouteProcessByRouteId(Long routeId) {
        routeProcessMapper.deleteByRouteId(routeId);
        routeProcessOutputMapper.deleteByRouteId(routeId);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteServiceImpl.java
@@ -13,6 +13,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRouteSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessOutputDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductBomDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
@@ -35,6 +36,7 @@
import java.util.Set;
import java.util.Comparator;
import java.util.Arrays;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.concurrent.atomic.AtomicInteger;
@@ -209,6 +211,20 @@
                throw exception(PRO_ROUTE_MIDDLE_OUTPUT_INVALID, process.getProcessId());
            }
        }
        // 4. æœ«é“工序成品产出必须按产品归属,避免“通用”产出串到其它产品的工单
        List<MesProRouteProcessOutputDO> lastOutputs = routeProcessService
                .getRouteProcessOutputListByRouteProcessIds(Collections.singletonList(lastProcess.getId()));
        if (CollUtil.isNotEmpty(lastOutputs)) {
            boolean attributionInvalid = lastOutputs.stream().anyMatch(output ->
                    output.getProductId() == null || !output.getProductId().equals(output.getItemId()));
            Set<Long> attributedProducts = lastOutputs.stream()
                    .map(MesProRouteProcessOutputDO::getProductId)
                    .filter(Objects::nonNull)
                    .collect(Collectors.toSet());
            if (attributionInvalid || !attributedProducts.containsAll(finishedItemIds)) {
                throw exception(PRO_ROUTE_LAST_OUTPUT_ATTRIBUTION_REQUIRED, lastProcess.getProcessId());
            }
        }
    }
    private Set<Long> parseOutputItemIds(String value) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskService.java
@@ -75,6 +75,13 @@
    PageResult<MesProTaskDO> getPendingFeedbackPage(MesProTaskPageReqVO pageReqVO);
    /**
     * æŒ‰å·¥ä½œç«™ç»Ÿè®¡å¾…报工任务数
     *
     * @return å·¥ä½œç«™ç¼–号 -> å¾…报工任务数
     */
    Map<Long, Long> getPendingFeedbackCountByWorkstation();
    /**
     * æ ¹æ®å·¥å•编号列表,批量获得生产任务列表
     *
     * @param workOrderIds å·¥å•编号列表
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskServiceImpl.java
@@ -45,7 +45,9 @@
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -127,11 +129,7 @@
        if (itemId == null) {
            MesProRouteProcessDO routeProcess = routeProcessService
                    .getRouteProcessByRouteIdAndProcessId(createReqVO.getRouteId(), createReqVO.getProcessId());
            if (routeProcess != null && routeProcess.getOutputItemId() != null) {
                itemId = Long.valueOf(routeProcess.getOutputItemId().split(",")[0].trim());
            } else {
                itemId = workOrder.getProductId();
            }
            itemId = resolveTaskItemId(routeProcess, workOrder.getProductId());
        }
        MesMdItemDO item = itemService.validateItemExists(itemId);
@@ -256,6 +254,30 @@
        return routeProcesses;
    }
    /**
     * ä»»åŠ¡äº§å‡ºç‰©æ–™ï¼šä¼˜å…ˆå–å·¥åºåœ¨å½“å‰äº§å“ä¸‹çš„äº§å‡ºï¼ˆæŒ‰å½’å±žè¿‡æ»¤ï¼‰ï¼Œæ²¡æœ‰äº§å‡ºæ—¶å–å·¥å•æˆå“
     */
    private Long resolveTaskItemId(MesProRouteProcessDO routeProcess, Long productId) {
        if (routeProcess == null) {
            return productId;
        }
        if (routeProcess.getId() != null) {
            // å·¥è‰ºè·¯çº¿æ¨¡æ¿ï¼šæŒ‰äº§å‡ºå½’属过滤出当前产品的产出
            List<Long> outputIds = routeProcessService
                    .getOutputItemIdsByRouteProcessId(routeProcess.getId(), productId);
            if (outputIds != null) {
                // æœ‰å½’属配置:该产品无产出时取工单成品;null è¡¨ç¤ºå­˜é‡æ— å½’属数据,继续走聚合兜底
                return CollUtil.isNotEmpty(outputIds) ? outputIds.get(0) : productId;
            }
        }
        // å·¥å•工序快照 / å­˜é‡æ— å½’属数据:产出已按产品过滤,取第一个
        String outputItemId = routeProcess.getOutputItemId();
        if (outputItemId != null && !outputItemId.isBlank()) {
            return Long.valueOf(outputItemId.split(",")[0].trim());
        }
        return productId;
    }
    @Override
    public void deleteTask(Long id) {
        // 1. æ ¡éªŒå­˜åœ¨ä¸”非终态
@@ -286,6 +308,20 @@
    @Override
    public PageResult<MesProTaskDO> getPendingFeedbackPage(MesProTaskPageReqVO pageReqVO) {
        return taskMapper.selectPendingFeedbackPage(pageReqVO);
    }
    @Override
    public Map<Long, Long> getPendingFeedbackCountByWorkstation() {
        List<Map<String, Object>> rows = taskMapper.selectPendingFeedbackCountByWorkstation();
        Map<Long, Long> result = new HashMap<>(rows.size());
        for (Map<String, Object> row : rows) {
            Object workstationId = row.get("workstation_id");
            Object count = row.get("pending_count");
            if (workstationId instanceof Number && count instanceof Number) {
                result.put(((Number) workstationId).longValue(), ((Number) count).longValue());
            }
        }
        return result;
    }
    @Override
@@ -443,7 +479,7 @@
        List<Long> taskIds = new java.util.ArrayList<>();
        for (MesProRouteProcessDO rp : routeProcesses) {
            cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO proc = procMap.get(rp.getProcessId());
            Long itemId = rp.getOutputItemId() != null ? Long.valueOf(rp.getOutputItemId().split(",")[0].trim()) : workOrder.getProductId();
            Long itemId = resolveTaskItemId(rp, workOrder.getProductId());
            cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO item = itemService.validateItemExists(itemId);
            MesProTaskDO task = new MesProTaskDO()
                    .setWorkOrderId(workOrder.getId())
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderProcessService.java
@@ -62,6 +62,15 @@
    List<MesProWorkOrderProcessDO> getWorkOrderProcessListByWorkOrderId(Long workOrderId);
    /**
     * æ ¹æ®å·¥å•编号 + å·¥åºç¼–号获得工单工序
     *
     * @param workOrderId å·¥å•编号
     * @param processId   å·¥åºç¼–号
     * @return å·¥å•工序
     */
    MesProWorkOrderProcessDO getWorkOrderProcessByWorkOrderIdAndProcessId(Long workOrderId, Long processId);
    /**
     * æ ¹æ®å·¥å•编号获得最后一道工序
     *
     * @param workOrderId å·¥å•编号
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderProcessServiceImpl.java
@@ -7,12 +7,15 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.process.MesProWorkOrderProcessSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.process.MesProWorkOrderProcessSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderBomDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderProcessDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderBomMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderProcessMapper;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@@ -48,6 +51,10 @@
    private MesProProcessService processService;
    @Resource
    private MesProRouteProcessService routeProcessService;
    @Resource
    private MesProRouteProductService routeProductService;
    @Resource
    private MesWmWarehouseAreaService warehouseAreaService;
    @Resource
    @Lazy
    private MesProWorkOrderBomService workOrderBomService;
@@ -89,6 +96,9 @@
                    .collect(Collectors.joining(",")));
        }
        workOrderProcessMapper.updateById(updateObj);
        // æŠ¥å·¥ä»“库允许置空,单独更新
        workOrderProcessMapper.updateWarehouseById(updateReqVO.getId(), updateReqVO.getWarehouseId(),
                updateReqVO.getLocationId(), updateReqVO.getAreaId());
        // 3. é‡å»ºå·¥åºé“¾
        rebuildProcessChain(updateReqVO.getWorkOrderId());
@@ -122,6 +132,11 @@
    }
    @Override
    public MesProWorkOrderProcessDO getWorkOrderProcessByWorkOrderIdAndProcessId(Long workOrderId, Long processId) {
        return workOrderProcessMapper.selectByWorkOrderIdAndProcessId(workOrderId, processId);
    }
    @Override
    public MesProWorkOrderProcessDO getLastWorkOrderProcessByWorkOrderId(Long workOrderId) {
        return workOrderProcessMapper.selectLastByWorkOrderId(workOrderId);
    }
@@ -146,6 +161,8 @@
        if (CollUtil.isEmpty(routeProcessList)) {
            return Collections.emptyList();
        }
        // å·¥è‰ºè·¯çº¿äº§å“ä¸Šé…ç½®çš„æŠ¥å·¥ä»“库作为工序默认值,下单时可再修改
        MesProRouteProductDO routeProduct = routeProductService.getRouteProductByItemId(productId);
        // è½¬æ¢ä¸ºå·¥å•工序 DO(不设置 workOrderId,前端保存时再设置)
        List<MesProWorkOrderProcessDO> processList = new ArrayList<>(routeProcessList.size());
        for (MesProRouteProcessDO routeProcess : routeProcessList) {
@@ -161,6 +178,9 @@
                    .checkFlag(routeProcess.getCheckFlag())
                    .backflushFlag(routeProcess.getBackflushFlag())
                    .outputItemId(routeProcess.getOutputItemId())
                    .warehouseId(routeProduct != null ? routeProduct.getWarehouseId() : null)
                    .locationId(routeProduct != null ? routeProduct.getLocationId() : null)
                    .areaId(routeProduct != null ? routeProduct.getAreaId() : null)
                    .remark(routeProcess.getRemark())
                    .build());
        }
@@ -212,6 +232,9 @@
                    updateObj.setOutputItemId(item.getOutputItemIds().stream().map(String::valueOf).collect(Collectors.joining(",")));
                }
                workOrderProcessMapper.updateById(updateObj);
                // æŠ¥å·¥ä»“库允许置空,单独更新
                workOrderProcessMapper.updateWarehouseById(item.getId(), item.getWarehouseId(),
                        item.getLocationId(), item.getAreaId());
            }
        }
@@ -277,7 +300,12 @@
                throw exception(PRO_WORK_ORDER_PROCESS_DUPLICATE);
            }
        }
        // 4. æ ¡éªŒå…³é”®å·¥åºå”¯ä¸€
        // 4. æ ¡éªŒæŠ¥å·¥ä»“库层级(仓库/库区/库位同时填写时校验归属关系)
        if (reqVO.getWarehouseId() != null && reqVO.getLocationId() != null && reqVO.getAreaId() != null) {
            warehouseAreaService.validateWarehouseAreaExists(reqVO.getWarehouseId(),
                    reqVO.getLocationId(), reqVO.getAreaId());
        }
        // 5. æ ¡éªŒå…³é”®å·¥åºå”¯ä¸€
        if (Boolean.TRUE.equals(reqVO.getKeyFlag())) {
            for (MesProWorkOrderProcessDO process : list) {
                if (reqVO.getId() != null && reqVO.getId().equals(process.getId())) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceServiceImpl.java
@@ -182,8 +182,9 @@
        Long batchId = batch != null ? batch.getId() : null;
        String batchCode = batch != null ? batch.getCode() : null;
        // 1.5 ä¼˜å…ˆä½¿ç”¨å·¥è‰ºè·¯çº¿äº§å“ç»‘定的报工仓库;历史数据未配置时回退虚拟线边库。
        WarehouseTarget outputTarget = resolveOutputTarget(feedback.getItemId());
        // 1.5 ä¼˜å…ˆä½¿ç”¨å·¥å•工序配置的报工仓库(下单时可编辑),其次工艺路线产品,最后回退虚拟线边库
        WarehouseTarget outputTarget = resolveOutputTarget(feedback.getWorkOrderId(),
                feedback.getProcessId(), feedback.getItemId());
        // 3. æ ¹æ®æ˜¯å¦éœ€è¦æ£€éªŒåˆ†æ”¯å¤„理
        if (checkFlag) {
@@ -275,7 +276,8 @@
        }
        // 2. é€šè¿‡ sourceLineId ç›´æŽ¥å®šä½å¾…检验行
        MesWmProductProduceLineDO pendingLine = productProduceLineService.validateProductProduceLineExists(sourceLineId);
        WarehouseTarget outputTarget = resolveOutputTarget(pendingLine.getItemId());
        WarehouseTarget outputTarget = resolveOutputTarget(produce.getWorkOrderId(),
                produce.getProcessId(), pendingLine.getItemId());
        // 3A. æƒ…况一:存在不合格品数量,需要拆分行
        if (unqualifiedQty != null && unqualifiedQty.compareTo(BigDecimal.ZERO) > 0) {
@@ -322,7 +324,20 @@
        finishProductProduce(produce.getId());
    }
    private WarehouseTarget resolveOutputTarget(Long itemId) {
    private WarehouseTarget resolveOutputTarget(Long workOrderId, Long processId, Long itemId) {
        // 1. å·¥å•工序配置(下单时可编辑)
        if (workOrderId != null && processId != null) {
            MesProWorkOrderProcessDO workOrderProcess = workOrderProcessService
                    .getWorkOrderProcessByWorkOrderIdAndProcessId(workOrderId, processId);
            if (workOrderProcess != null && workOrderProcess.getWarehouseId() != null
                    && workOrderProcess.getLocationId() != null && workOrderProcess.getAreaId() != null) {
                areaService.validateWarehouseAreaExists(workOrderProcess.getWarehouseId(),
                        workOrderProcess.getLocationId(), workOrderProcess.getAreaId());
                return new WarehouseTarget(workOrderProcess.getWarehouseId(),
                        workOrderProcess.getLocationId(), workOrderProcess.getAreaId());
            }
        }
        // 2. å·¥è‰ºè·¯çº¿äº§å“é…ç½®
        MesProRouteProductDO routeProduct = itemId != null
                ? routeProductService.getRouteProductByItemId(itemId) : null;
        if (routeProduct != null && routeProduct.getWarehouseId() != null