云
2026-10-25 d54fbc9d4f8981fa0f7f613b33a28a8cd4f764ea
feat(system): 优化用户搜索功能并完善ERP和MES模块验证逻辑

- 在AdminUserMapper中添加昵称搜索支持
- 为ERP采购请求添加定金金额必填验证和默认值处理
- 强化销售订单状态更新权限控制,特定状态需超级管理员权限
- 在MES工艺路线中添加仓库配置验证和产出产品校验
- 优化MES设备物料创建逻辑,使用独立的分类编码查询
- 添加MES排班计划编码自动生成功能
- 为MES质检单据添加检测结果必填验证
- 在HRM考勤规则中调整最小打卡半径限制并添加表单验证
- 扩展MDM物料分类API以支持编码查询功能
已添加1个文件
已修改33个文件
393 ■■■■■ 文件已修改
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestSaveReqVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/listener/ProductionFinishedListener.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleSaveReqVO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRuleServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/category/MdmItemCategoryApi.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/category/MdmItemCategoryApiImpl.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/category/MdmItemCategoryService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/category/MdmItemCategoryServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemServiceImpl.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/event/ProductionCanceledEvent.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/cal/plan/vo/MesCalPlanSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteProductController.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/product/MesProRouteProductRespVO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/product/MesProRouteProductSaveReqVO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/vo/MesQcIpqcSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/iqc/vo/MesQcIqcSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/rqc/vo/MesQcRqcSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProductDO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | 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/md/autocode/MesMdAutoCodeRuleCodeEnum.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/plan/MesCalPlanServiceImpl.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/machinery/MesDvMachineryServiceImpl.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProcessServiceImpl.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProductServiceImpl.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteServiceImpl.java 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceServiceImpl.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productreceipt/MesWmProductReceiptServiceImpl.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserPageReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/user/AdminUserMapper.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestSaveReqVO.java
@@ -31,7 +31,8 @@
    @Schema(description = "优惠率,百分比", example = "10")
    private BigDecimal discountPercent;
    @Schema(description = "定金金额", example = "1000.00")
    @Schema(description = "定金金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000.00")
    @NotNull(message = "定金金额不能为空")
    private BigDecimal depositPrice;
    @Schema(description = "备注", example = "备注")
@@ -90,4 +91,4 @@
    }
}
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java
@@ -97,7 +97,8 @@
    @PutMapping("/update-status")
    @Operation(summary = "更新销售订单的状态")
    @PreAuthorize("@ss.hasPermission('erp:sale-order:update-status')")
    @PreAuthorize("@ss.hasPermission('erp:sale-order:update-status') "
            + "&& (#status != 10 || @ss.hasRole('super_admin'))")
    public CommonResult<Boolean> updateSaleOrderStatus(@RequestParam("id") Long id,
                                                      @RequestParam("status") Integer status) {
        saleOrderService.updateSaleOrderStatus(id, status);
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java
@@ -131,6 +131,10 @@
                .setStatus(ErpPurchaseRequestStatusEnum.DRAFT.getStatus())
                .setRequestUserId(userId)
                .setRequestDeptId(deptId));
        // å…¼å®¹æ—§å®¢æˆ·ç«¯æœªä¼ å®šé‡‘的情况,数据库与业务层统一按 0 å¤„理
        if (purchaseRequest.getDepositPrice() == null) {
            purchaseRequest.setDepositPrice(BigDecimal.ZERO);
        }
        calculateTotalPrice(purchaseRequest, requestItems);
        purchaseRequestMapper.insert(purchaseRequest);
@@ -159,6 +163,9 @@
        // 4. æ›´æ–°ç”³è¯·å•
        ErpPurchaseRequestDO updateObj = BeanUtils.toBean(updateReqVO, ErpPurchaseRequestDO.class);
        if (updateObj.getDepositPrice() == null) {
            updateObj.setDepositPrice(BigDecimal.ZERO);
        }
        calculateTotalPrice(updateObj, requestItems);
        purchaseRequestMapper.updateById(updateObj);
@@ -605,4 +612,4 @@
                .setItems(items);
    }
}
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/listener/ProductionFinishedListener.java
@@ -3,6 +3,8 @@
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.mes.api.event.ProductionFinishedEvent;
import cn.iocoder.yudao.module.mes.api.event.ProductionCanceledEvent;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
@@ -37,4 +39,16 @@
        log.info("[onProductionFinished] é”€å”®è®¢å• {} ç”Ÿäº§çŠ¶æ€å·²æ›´æ–°ä¸ºå®Œæˆ", event.getSaleOrderId());
    }
}
    @EventListener
    @Async
    public void onProductionCanceled(ProductionCanceledEvent event) {
        log.info("[onProductionCanceled] æ”¶åˆ°ç”Ÿäº§å–消事件,销售订单ID: {}", event.getSaleOrderId());
        // ä½¿ç”¨æ˜¾å¼ update wrapper æ‰èƒ½æŠŠå·²å­˜åœ¨çš„完成时间真正更新为 NULL
        saleOrderMapper.update(null, new LambdaUpdateWrapper<ErpSaleOrderDO>()
                .eq(ErpSaleOrderDO::getId, event.getSaleOrderId())
                .set(ErpSaleOrderDO::getProductionStatus, 0)
                .set(ErpSaleOrderDO::getProductionFinishTime, null));
        log.info("[onProductionCanceled] é”€å”®è®¢å• {} ç”Ÿäº§çŠ¶æ€å·²æ¢å¤ä¸ºæœªå®Œæˆ", event.getSaleOrderId());
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleSaveReqVO.java
@@ -5,6 +5,7 @@
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.AssertTrue;
import java.math.BigDecimal;
import java.time.LocalTime;
@@ -54,6 +55,17 @@
    @Schema(description = "坐标系,仅支持 WGS84")
    private String coordinateSystem;
    @AssertTrue(message = "启用定位打卡时,地点名称、经纬度和10~5000米打卡半径必须完整配置")
    public boolean isLocationConfigValid() {
        if (!Boolean.TRUE.equals(locationRequired)) {
            return true;
        }
        return locationName != null && !locationName.isBlank()
                && longitude != null && latitude != null && radiusMeters != null
                && radiusMeters >= 10 && radiusMeters <= 5000
                && "WGS84".equalsIgnoreCase(coordinateSystem);
    }
    @Schema(description = "状态:0-启用,1-禁用", example = "0")
    private Integer status;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java
@@ -447,7 +447,7 @@
        boolean required = Boolean.TRUE.equals(rule.getLocationRequired());
        if (required && (StrUtil.isBlank(rule.getLocationName()) || rule.getLongitude() == null
                || rule.getLatitude() == null || rule.getRadiusMeters() == null
                || rule.getRadiusMeters() < 50 || rule.getRadiusMeters() > 5000
                || rule.getRadiusMeters() < 10 || rule.getRadiusMeters() > 5000
                || !"WGS84".equalsIgnoreCase(rule.getCoordinateSystem()))) {
            throw exception(ATTENDANCE_LOCATION_RULE_INVALID);
        }
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRuleServiceImpl.java
@@ -134,7 +134,7 @@
                || reqVO.getLatitude().compareTo(BigDecimal.valueOf(90)) > 0)) {
            throw exception(ATTENDANCE_LOCATION_COORDINATE_INVALID);
        }
        if (reqVO.getRadiusMeters() < 50 || reqVO.getRadiusMeters() > 5000) {
        if (reqVO.getRadiusMeters() < 10 || reqVO.getRadiusMeters() > 5000) {
            throw exception(ATTENDANCE_LOCATION_RULE_INVALID);
        }
        if (Boolean.TRUE.equals(reqVO.getLocationRequired())
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/category/MdmItemCategoryApi.java
@@ -24,6 +24,14 @@
    CommonResult<MdmItemCategoryRespDTO> getItemCategory(Long id);
    /**
     * æ ¹æ®ç¼–码获取物料分类
     *
     * @param code åˆ†ç±»ç¼–码
     * @return ç‰©æ–™åˆ†ç±»
     */
    CommonResult<MdmItemCategoryRespDTO> getItemCategoryByCode(String code);
    /**
     * èŽ·å–ç‰©æ–™åˆ†ç±»åˆ—è¡¨
     *
     * @param ids åˆ†ç±»ç¼–号列表
@@ -47,4 +55,4 @@
     */
    CommonResult<MdmItemCategoryRespDTO> getItemCategoryByItemOrProduct(String itemOrProduct);
}
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/category/MdmItemCategoryApiImpl.java
@@ -35,6 +35,12 @@
    }
    @Override
    public CommonResult<MdmItemCategoryRespDTO> getItemCategoryByCode(String code) {
        MdmItemCategoryDO itemCategory = itemCategoryService.getItemCategoryByCode(code);
        return success(BeanUtils.toBean(itemCategory, MdmItemCategoryRespDTO.class));
    }
    @Override
    public CommonResult<List<MdmItemCategoryRespDTO>> getItemCategoryList(Collection<Long> ids) {
        List<MdmItemCategoryDO> list = itemCategoryService.getItemCategoryList(ids);
        return success(BeanUtils.toBean(list, MdmItemCategoryRespDTO.class));
@@ -52,4 +58,4 @@
        return success(BeanUtils.toBean(itemCategory, MdmItemCategoryRespDTO.class));
    }
}
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/category/MdmItemCategoryService.java
@@ -58,6 +58,14 @@
    MdmItemCategoryDO getItemCategory(Long id);
    /**
     * æ ¹æ®ç¼–码获得物料分类
     *
     * @param code åˆ†ç±»ç¼–码
     * @return ç‰©æ–™åˆ†ç±»
     */
    MdmItemCategoryDO getItemCategoryByCode(String code);
    /**
     * æ ¡éªŒç‰©æ–™åˆ†ç±»å­˜åœ¨
     *
     * @param id ç¼–号
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/category/MdmItemCategoryServiceImpl.java
@@ -115,6 +115,11 @@
    }
    @Override
    public MdmItemCategoryDO getItemCategoryByCode(String code) {
        return itemCategoryMapper.selectByCode(code);
    }
    @Override
    public PageResult<MdmItemCategoryDO> getItemCategoryPage(MdmItemCategoryPageReqVO pageReqVO) {
        return itemCategoryMapper.selectPage(pageReqVO);
    }
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemServiceImpl.java
@@ -5,10 +5,8 @@
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemImportExcelVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemImportReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemImportRespVO;
@@ -28,12 +26,9 @@
import cn.iocoder.yudao.module.mdm.service.category.MdmItemCategoryService;
import cn.iocoder.yudao.module.mdm.service.unit.MdmUnitMeasureService;
import cn.iocoder.yudao.module.mes.api.item.MesMdItemApi;
import cn.iocoder.yudao.module.mes.api.item.MesMdItemTypeApi;
import cn.iocoder.yudao.module.mes.api.item.dto.MesMdItemSyncReqDTO;
import cn.iocoder.yudao.module.mes.api.item.dto.MesMdItemTypeRespDTO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -78,10 +73,6 @@
    private MesMdItemApi mesMdItemApi;
    @Resource
    private MdmItemBatchConfigMapper itemBatchConfigMapper;
    @Resource
    private MesMdItemTypeApi mesMdItemTypeApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -152,8 +143,11 @@
        // è§£æžåˆ†ç±» id -> code(使用 MDM æœ¬åœ°åˆ†ç±»æœåŠ¡ï¼‰
        String itemTypeCode = null;
        if (reqVO.getCategoryId() != null) {
            CommonResult<MesMdItemTypeRespDTO> itemType = mesMdItemTypeApi.getItemType(reqVO.getCategoryId());
            itemTypeCode = itemType.getData().getCode();
            MdmItemCategoryDO category = itemCategoryService.getItemCategory(reqVO.getCategoryId());
            if (category == null) {
                throw exception(MDM_ITEM_CATEGORY_NOT_EXISTS);
            }
            itemTypeCode = category.getCode();
        }
        // ç»„装同步 DTO
        MesMdItemSyncReqDTO syncReqDTO = new MesMdItemSyncReqDTO();
@@ -545,4 +539,4 @@
        }
    }
}
}
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/event/ProductionCanceledEvent.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.mes.api.event;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
 * é”€å”®è®¢å•关联的生产计划取消事件。
 */
@Data
@AllArgsConstructor
public class ProductionCanceledEvent {
    private Long saleOrderId;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/cal/plan/vo/MesCalPlanSaveReqVO.java
@@ -14,8 +14,7 @@
    @Schema(description = "计划编号", example = "1024")
    private Long id;
    @Schema(description = "计划编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "PLAN001")
    @NotEmpty(message = "计划编码不能为空")
    @Schema(description = "计划编码(新增时为空则由系统自动生成)", example = "PLAN001")
    private String code;
    @Schema(description = "计划名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024年排班计划")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteProductController.java
@@ -10,9 +10,15 @@
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.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.md.unitmeasure.MesMdUnitMeasureService;
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;
@@ -42,6 +48,12 @@
    private MesMdItemService itemService;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    @Resource
    private MesWmWarehouseService warehouseService;
    @Resource
    private MesWmWarehouseLocationService warehouseLocationService;
    @Resource
    private MesWmWarehouseAreaService warehouseAreaService;
    @PostMapping("/create")
    @Operation(summary = "创建工艺路线产品")
@@ -96,6 +108,12 @@
                convertSet(list, MesProRouteProductDO::getItemId));
        Map<Long, MesMdUnitMeasureDO> unitMap = unitMeasureService.getUnitMeasureMap(
                convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
        Map<Long, MesWmWarehouseDO> warehouseMap = warehouseService.getWarehouseMap(
                convertSet(list, MesProRouteProductDO::getWarehouseId));
        Map<Long, MesWmWarehouseLocationDO> locationMap = warehouseLocationService.getWarehouseLocationMap(
                convertSet(list, MesProRouteProductDO::getLocationId));
        Map<Long, MesWmWarehouseAreaDO> areaMap = warehouseAreaService.getWarehouseAreaMap(
                convertSet(list, MesProRouteProductDO::getAreaId));
        // 2. æ‹¼æŽ¥ VO
        return BeanUtils.toBean(list, MesProRouteProductRespVO.class, vo -> {
            MapUtils.findAndThen(itemMap, vo.getItemId(), item -> {
@@ -104,6 +122,9 @@
                MapUtils.findAndThen(unitMap, item.getUnitMeasureId(),
                        unit -> vo.setUnitName(unit.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()));
        });
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/product/MesProRouteProductRespVO.java
@@ -19,6 +19,24 @@
    @Schema(description = "产品物料编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long itemId;
    @Schema(description = "报工产出仓库编号")
    private Long warehouseId;
    @Schema(description = "报工产出仓库名称")
    private String warehouseName;
    @Schema(description = "报工产出库区编号")
    private Long locationId;
    @Schema(description = "报工产出库区名称")
    private String locationName;
    @Schema(description = "报工产出库位编号")
    private Long areaId;
    @Schema(description = "报工产出库位名称")
    private String areaName;
    @Schema(description = "产品物料编码")
    private String itemCode;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/product/MesProRouteProductSaveReqVO.java
@@ -21,6 +21,18 @@
    @NotNull(message = "产品物料编号不能为空")
    private Long itemId;
    @Schema(description = "报工产出仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "报工产出仓库不能为空")
    private Long warehouseId;
    @Schema(description = "报工产出库区编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "报工产出库区不能为空")
    private Long locationId;
    @Schema(description = "报工产出库位编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "报工产出库位不能为空")
    private Long areaId;
    @Schema(description = "生产数量", example = "100")
    private Integer quantity;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/vo/MesQcIpqcSaveReqVO.java
@@ -97,7 +97,8 @@
    // ========== æ£€éªŒ ==========
    @Schema(description = "检测结果", example = "1")
    @Schema(description = "检测结果", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "检测结果不能为空")
    private Integer checkResult;
    @Schema(description = "检测日期")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/iqc/vo/MesQcIqcSaveReqVO.java
@@ -78,7 +78,8 @@
    @NotNull(message = "检测日期不能为空")
    private LocalDateTime inspectDate;
    @Schema(description = "检测结果", example = "1")
    @Schema(description = "检测结果", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "检测结果不能为空")
    private Integer checkResult;
    @Schema(description = "来料日期", requiredMode = Schema.RequiredMode.REQUIRED)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcSaveReqVO.java
@@ -74,7 +74,8 @@
    // ========== æ£€éªŒ ==========
    @Schema(description = "检测结果", example = "1")
    @Schema(description = "检测结果", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "检测结果不能为空")
    private Integer checkResult;
    @Schema(description = "出货日期", requiredMode = Schema.RequiredMode.REQUIRED)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/rqc/vo/MesQcRqcSaveReqVO.java
@@ -75,7 +75,8 @@
    // ========== æ£€éªŒ ==========
    @Schema(description = "检测结果", example = "1")
    @Schema(description = "检测结果", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "检测结果不能为空")
    private Integer checkResult;
    @Schema(description = "检测日期", requiredMode = Schema.RequiredMode.REQUIRED)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProductDO.java
@@ -40,6 +40,12 @@
     * å…³è” {@link cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO#getId()}
     */
    private Long itemId;
    /** æŠ¥å·¥äº§å‡ºä»“库 */
    private Long warehouseId;
    /** æŠ¥å·¥äº§å‡ºåº“区 */
    private Long locationId;
    /** æŠ¥å·¥äº§å‡ºåº“位 */
    private Long areaId;
    /**
     * ç”Ÿäº§æ•°é‡
     */
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -289,6 +289,11 @@
    ErrorCode PRO_ROUTE_PRODUCT_NOT_EXISTS = new ErrorCode(1_040_501_200, "工艺路线产品不存在");
    ErrorCode PRO_ROUTE_PRODUCT_ITEM_DUPLICATE = new ErrorCode(1_040_501_201, "此产品已配置了工艺路线");
    ErrorCode PRO_ROUTE_PRODUCT_ITEM_TYPE_INVALID = new ErrorCode(1_040_501_202, "工艺路线只能关联产品类型物料");
    ErrorCode PRO_ROUTE_PRODUCT_WAREHOUSE_REQUIRED = new ErrorCode(1_040_501_203, "产品【{}】未配置报工入库仓库、库区和库位");
    ErrorCode PRO_ROUTE_PROCESS_OUTPUT_REQUIRED = new ErrorCode(1_040_501_204, "工序【{}】未配置产出产品");
    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, "末道工序【{}】只能产出成品分类产品,且必须包含全部绑定成品");
    // ========== 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/enums/md/autocode/MesMdAutoCodeRuleCodeEnum.java
@@ -28,6 +28,7 @@
    WM_PACKAGE_CODE("WM_PACKAGE_CODE", "装箱单编码"),
    WM_BATCH_CODE("WM_BATCH_CODE", "批次编码"),
    PRO_MPS_CODE("PRO_MPS_CODE", "生产计划编码"),
    CAL_PLAN_CODE("CAL_PLAN_CODE", "排班计划编码"),
    PRO_MAINTENANCE_PLAN_CODE("PRO_MAINTENANCE_PLAN_CODE", "电力作业计划编码"),
    PRO_TASK_CODE("PRO_TASK_CODE", "生产任务编码"),
    QC_IQC_CODE("QC_IQC_CODE", "来料检验单编码"),
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/plan/MesCalPlanServiceImpl.java
@@ -13,7 +13,9 @@
import cn.iocoder.yudao.module.mes.dal.mysql.cal.plan.MesCalPlanMapper;
import cn.iocoder.yudao.module.mes.enums.cal.MesCalPlanStatusEnum;
import cn.iocoder.yudao.module.mes.enums.cal.MesCalShiftTypeEnum;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
import cn.iocoder.yudao.module.mes.service.cal.team.MesCalTeamShiftService;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@@ -51,10 +53,17 @@
    @Resource
    @Lazy
    private MesCalTeamShiftService teamShiftService;
    @Resource
    private MesMdAutoCodeRecordService autoCodeRecordService;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createPlan(MesCalPlanSaveReqVO createReqVO) {
        // å‰ç«¯é€šå¸¸ä¼šé¢„生成编码;接口侧仍提供兜底,避免移动端、导入外的调用方漏传后流程中断。
        if (StrUtil.isBlank(createReqVO.getCode())) {
            createReqVO.setCode(autoCodeRecordService.generateAutoCode(
                    MesMdAutoCodeRuleCodeEnum.CAL_PLAN_CODE.getCode()));
        }
        // 1. æ ¡éªŒç¼–码唯一
        validatePlanCodeUnique(null, createReqVO.getCode());
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/machinery/MesDvMachineryServiceImpl.java
@@ -8,6 +8,8 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemCreateReqDTO;
import cn.iocoder.yudao.module.mdm.api.category.MdmItemCategoryApi;
import cn.iocoder.yudao.module.mdm.api.category.dto.MdmItemCategoryRespDTO;
import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi;
import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
import cn.iocoder.yudao.module.mdm.enums.MdmItemTypeEnum;
@@ -104,6 +106,8 @@
    // ========== è®¾å¤‡ç‰©æ–™è”动依赖 ==========
    @Resource
    private MdmItemApi mdmItemApi;
    @Resource
    private MdmItemCategoryApi mdmItemCategoryApi;
    @Resource
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @Resource
@@ -382,9 +386,15 @@
     * @return MDM ç‰©æ–™ç¼–号(= MES ç‰©æ–™ç¼–号)
     */
    private Long createEquipmentMaterial(String code, String name, String specification, String remark) {
        // 1. è§£æžâ€œè®¾å¤‡â€ç±»åž‹ï¼ˆMES ä¾§ï¼‰ä¸Žâ€œå°â€å•位(MDM ä¾§ + MES ä¾§ï¼‰ï¼Œç¼ºå¤±åˆ™æç¤ºä¸»æ•°æ®æœªåˆå§‹åŒ–
        // 1. åˆ†åˆ«æŒ‰ç¼–码解析 MDM åˆ†ç±»ã€MES ç±»åž‹å’Œä¸¤ä¾§å•位,禁止依赖两套表主键恰好一致。
        MesMdItemTypeDO itemType = mesMdItemTypeService.getItemTypeByCode(EQUIPMENT_ITEM_TYPE_CODE);
        if (itemType == null) {
            throw exception(DV_MACHINERY_EQUIP_MASTER_MISSING);
        }
        CommonResult<MdmItemCategoryRespDTO> categoryResult =
                mdmItemCategoryApi.getItemCategoryByCode(EQUIPMENT_ITEM_TYPE_CODE);
        MdmItemCategoryRespDTO mdmCategory = categoryResult.isSuccess() ? categoryResult.getData() : null;
        if (mdmCategory == null) {
            throw exception(DV_MACHINERY_EQUIP_MASTER_MISSING);
        }
        if (mesMdUnitMeasureService.getUnitMeasureByCode(EQUIPMENT_UNIT_CODE) == null) {
@@ -396,13 +406,13 @@
            throw exception(DV_MACHINERY_EQUIP_MASTER_MISSING);
        }
        // 2. åˆ›å»º MDM ç‰©æ–™ï¼›åˆ†ç±»ä½¿ç”¨â€œè®¾å¤‡â€åˆ†ç±»ï¼ˆå…¶ id ä¸Ž MES ç±»åž‹ id å¯¹é½ï¼Œç”±åˆå§‹åŒ–脚本保证)
        // 2. åˆ›å»º MDM ç‰©æ–™ï¼›åˆ†ç±»å– MDM è‡ªèº«ä¸»é”®ï¼Œå…è®¸ä¸åŒçŽ¯å¢ƒä¸­ä¸¤å¥—åˆ†ç±»è¡¨çš„ä¸»é”®ä¸ä¸€è‡´ã€‚
        MdmItemCreateReqDTO createReqDTO = new MdmItemCreateReqDTO();
        createReqDTO.setCode(code);
        createReqDTO.setName(name);
        createReqDTO.setSpecification(specification);
        createReqDTO.setRemark(remark);
        createReqDTO.setCategoryId(itemType.getId());
        createReqDTO.setCategoryId(mdmCategory.getId());
        createReqDTO.setUnitMeasureId(mdmUnit.getId());
        createReqDTO.setItemType(MdmItemTypeEnum.EQUIPMENT.getType());
        createReqDTO.setStatus(0);
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java
@@ -10,6 +10,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsImportRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsSaveReqVO;
import cn.iocoder.yudao.module.mes.api.event.ProductionCanceledEvent;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.MesProWorkOrderSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.mps.MesProMpsDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.mps.MesProMpsMergeItemDO;
@@ -42,6 +43,7 @@
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
@@ -95,6 +97,8 @@
    private MesProRouteProcessService routeProcessService;
    @Resource
    private MesProWorkOrderProcessService workOrderProcessService;
    @Resource
    private ApplicationEventPublisher eventPublisher;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -308,6 +312,10 @@
        mpsMapper.updateById(new MesProMpsDO()
                .setId(id)
                .setStatus(MesProMpsStatusEnum.CANCELED.getStatus()));
        // æ¥æºäºŽé”€å”®è®¢å•时,同步恢复销售订单生产状态,允许重新创建生产计划
        if (mps.getSaleOrderId() != null) {
            eventPublisher.publishEvent(new ProductionCanceledEvent(mps.getSaleOrderId()));
        }
    }
    @Override
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProcessServiceImpl.java
@@ -92,12 +92,11 @@
        // 2. æ›´æ–°
        MesProRouteProcessDO updateObj = BeanUtils.toBean(updateReqVO, MesProRouteProcessDO.class);
        if (CollUtil.isNotEmpty(updateReqVO.getOutputItemIds())) {
            updateObj.setOutputItemId(
        // ç©ºæ•°ç»„也要显式写入空串,否则 MyBatis-Plus é»˜è®¤å¿½ç•¥ null,用户清空产出品后旧配置仍会残留。
        updateObj.setOutputItemId(CollUtil.isEmpty(updateReqVO.getOutputItemIds()) ? "" :
                updateReqVO.getOutputItemIds().stream()
                    .map(String::valueOf)
                    .collect(Collectors.joining(",")));
        }
                        .map(String::valueOf)
                        .collect(Collectors.joining(",")));
        routeProcessMapper.updateById(updateObj);
        // 3. é‡å»ºå·¥åºé“¾
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProductServiceImpl.java
@@ -10,6 +10,7 @@
import cn.iocoder.yudao.module.mes.enums.md.MesMdItemTypeEnum;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemTypeService;
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;
@@ -43,6 +44,8 @@
    private MesMdItemService itemService;
    @Resource
    private MesMdItemTypeService itemTypeService;
    @Resource
    private MesWmWarehouseAreaService warehouseAreaService;
    @Override
    public Long createRouteProduct(MesProRouteProductSaveReqVO createReqVO) {
@@ -52,6 +55,8 @@
        validateProductItem(createReqVO.getItemId());
        // 1.2 æ ¡éªŒäº§å“å”¯ä¸€æ€§ï¼ˆä¸€ä¸ªäº§å“åªèƒ½å…³è”一条工艺路线)
        validateItemUnique(null, createReqVO.getItemId());
        warehouseAreaService.validateWarehouseAreaExists(createReqVO.getWarehouseId(),
                createReqVO.getLocationId(), createReqVO.getAreaId());
        // 2. æ’å…¥
        MesProRouteProductDO routeProduct = BeanUtils.toBean(createReqVO, MesProRouteProductDO.class);
@@ -69,6 +74,8 @@
        validateProductItem(updateReqVO.getItemId());
        // 1.3 æ ¡éªŒäº§å“å”¯ä¸€æ€§
        validateItemUnique(updateReqVO.getId(), updateReqVO.getItemId());
        warehouseAreaService.validateWarehouseAreaExists(updateReqVO.getWarehouseId(),
                updateReqVO.getLocationId(), updateReqVO.getAreaId());
        // 2. æ›´æ–°
        MesProRouteProductDO updateObj = BeanUtils.toBean(updateReqVO, MesProRouteProductDO.class);
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteServiceImpl.java
@@ -16,8 +16,11 @@
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;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemTypeDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.route.MesProRouteMapper;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemTypeService;
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;
@@ -29,6 +32,10 @@
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
import java.util.Comparator;
import java.util.Arrays;
import java.util.stream.Collectors;
import java.util.concurrent.atomic.AtomicInteger;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -58,6 +65,10 @@
    @Resource
    @Lazy
    private MesMdItemService itemService;
    @Resource
    private MesMdItemTypeService itemTypeService;
    @Resource
    private MesWmWarehouseAreaService warehouseAreaService;
    @Override
    public Long createRoute(MesProRouteSaveReqVO createReqVO) {
@@ -147,13 +158,75 @@
        // 3. æ‰€æœ‰äº§å“å¿…须配置了 BOM æ¶ˆè€—
        List<MesProRouteProductDO> productList = routeProductService.getRouteProductListByRouteId(routeId);
        for (MesProRouteProductDO product : productList) {
            MesMdItemDO item = itemService.validateItemExists(product.getItemId());
            if (product.getWarehouseId() == null || product.getLocationId() == null || product.getAreaId() == null) {
                throw exception(PRO_ROUTE_PRODUCT_WAREHOUSE_REQUIRED, item.getName());
            }
            warehouseAreaService.validateWarehouseAreaExists(product.getWarehouseId(),
                    product.getLocationId(), product.getAreaId());
            List<MesProRouteProductBomDO> bomList = routeProductBomService
                    .getRouteProductBomList(routeId, null, product.getItemId());
            if (CollUtil.isEmpty(bomList)) {
                MesMdItemDO item = itemService.validateItemExists(product.getItemId());
                throw exception(PRO_ROUTE_ENABLE_PRODUCT_NO_BOM, item.getName());
            }
        }
        validateProcessOutputs(processList, productList);
    }
    private void validateProcessOutputs(List<MesProRouteProcessDO> processList,
                                        List<MesProRouteProductDO> productList) {
        if (CollUtil.isEmpty(productList)) {
            throw exception(PRO_ROUTE_LAST_OUTPUT_INVALID, processList.get(processList.size() - 1).getProcessId());
        }
        Set<Long> boundItemIds = productList.stream().map(MesProRouteProductDO::getItemId).collect(Collectors.toSet());
        Set<Long> finishedItemIds = productList.stream()
                .map(MesProRouteProductDO::getItemId)
                .filter(itemId -> isItemUnderCategory(itemId, "FG"))
                .collect(Collectors.toSet());
        MesProRouteProcessDO lastProcess = processList.stream()
                .max(Comparator.comparing(MesProRouteProcessDO::getSort)).orElseThrow();
        for (MesProRouteProcessDO process : processList) {
            Set<Long> outputIds = parseOutputItemIds(process.getOutputItemId());
            if (CollUtil.isEmpty(outputIds)) {
                throw exception(PRO_ROUTE_PROCESS_OUTPUT_REQUIRED, process.getProcessId());
            }
            for (Long outputId : outputIds) {
                if (!boundItemIds.contains(outputId)) {
                    MesMdItemDO item = itemService.validateItemExists(outputId);
                    throw exception(PRO_ROUTE_PROCESS_OUTPUT_NOT_BOUND, item.getName());
                }
            }
            if (process.getId().equals(lastProcess.getId())) {
                if (finishedItemIds.isEmpty() || !outputIds.equals(finishedItemIds)) {
                    throw exception(PRO_ROUTE_LAST_OUTPUT_INVALID, process.getProcessId());
                }
            } else if (outputIds.stream().anyMatch(itemId -> !isItemUnderCategory(itemId, "SF"))) {
                throw exception(PRO_ROUTE_MIDDLE_OUTPUT_INVALID, process.getProcessId());
            }
        }
    }
    private Set<Long> parseOutputItemIds(String value) {
        if (StrUtil.isBlank(value)) {
            return Collections.emptySet();
        }
        return Arrays.stream(value.split(",")).map(String::trim).filter(StrUtil::isNotBlank)
                .map(Long::valueOf).collect(Collectors.toSet());
    }
    private boolean isItemUnderCategory(Long itemId, String rootCode) {
        MesMdItemDO item = itemService.validateItemExists(itemId);
        MesMdItemTypeDO type = itemTypeService.getItemType(item.getItemTypeId());
        while (type != null) {
            if (rootCode.equals(type.getCode())) {
                return true;
            }
            if (type.getParentId() == null || MesMdItemTypeDO.PARENT_ID_ROOT.equals(type.getParentId())) {
                return false;
            }
            type = itemTypeService.getItemType(type.getParentId());
        }
        return false;
    }
    @Override
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceServiceImpl.java
@@ -9,11 +9,13 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
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.pro.route.MesProRouteProductDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDetailDO;
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.wm.productproduce.MesWmProductProduceMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackMapper;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmProductProduceStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
@@ -23,6 +25,7 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderProcessService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService;
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
import cn.iocoder.yudao.module.mes.service.wm.productreceipt.MesWmProductReceiptService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
@@ -52,6 +55,8 @@
    @Resource
    private MesWmProductProduceMapper productProduceMapper;
    @Resource
    private MesProFeedbackMapper feedbackMapper;
    @Resource
    private MesWmProductProduceLineService productProduceLineService;
@@ -61,6 +66,8 @@
    private MesProWorkOrderService workOrderService;
    @Resource
    private MesProWorkOrderProcessService workOrderProcessService;
    @Resource
    private MesProRouteProductService routeProductService;
    @Resource
    private MesWmProductReceiptService productReceiptService;
    @Resource
@@ -86,7 +93,7 @@
        }
        // 2. åˆ›å»ºåº“存事务
        createTransactionList(id, lines);
        createTransactionList(produce, lines);
        // 3. æ›´æ–°å…¥åº“单状态
        productProduceMapper.updateById(new MesWmProductProduceDO()
@@ -100,7 +107,11 @@
        }
    }
    private void createTransactionList(Long produceId, List<MesWmProductProduceLineDO> lines) {
    private void createTransactionList(MesWmProductProduceDO produce, List<MesWmProductProduceLineDO> lines) {
        MesProFeedbackDO feedback = produce.getFeedbackId() != null
                ? feedbackMapper.selectById(produce.getFeedbackId()) : null;
        String bizCode = feedback != null && cn.hutool.core.util.StrUtil.isNotBlank(feedback.getCode())
                ? feedback.getCode() : "PRODUCE-" + produce.getId();
        for (MesWmProductProduceLineDO line : lines) {
            // 1. æ ¡éªŒæ¯è¡Œæ˜Žç»†æ•°é‡ä¹‹å’Œç­‰äºŽè¡Œæ•°é‡
            List<MesWmProductProduceDetailDO> details = productProduceDetailService.getProductProduceDetailListByLineId(
@@ -117,8 +128,8 @@
                        .setQuantity(detail.getQuantity())
                        .setBatchId(detail.getBatchId()).setBatchCode(detail.getBatchCode())
                        .setWarehouseId(detail.getWarehouseId()).setLocationId(detail.getLocationId()).setAreaId(detail.getAreaId())
                        .setBizType(MesBizTypeConstants.WM_PRODUCT_PRODUCE).setBizId(produceId)
                        .setBizCode("").setBizLineId(line.getId()));
                        .setBizType(MesBizTypeConstants.WM_PRODUCT_PRODUCE).setBizId(produce.getId())
                        .setBizCode(bizCode).setBizLineId(line.getId()));
            }
        }
    }
@@ -127,6 +138,11 @@
    @Transactional(rollbackFor = Exception.class)
    public MesWmProductProduceDO generateProductProduce(MesProFeedbackDO feedback, boolean checkFlag,
                                                         MesProFeedbackApproveReqVO approveReqVO) {
        // ä¸€ä¸ªæŠ¥å·¥åªèƒ½ç”Ÿæˆä¸€å¼ ç”Ÿäº§äº§å‡ºå•;审批接口重试时直接返回既有单据
        MesWmProductProduceDO existingProduce = productProduceMapper.selectByFeedbackId(feedback.getId());
        if (existingProduce != null) {
            return existingProduce;
        }
        // 0. æŸ¥è¯¢å…³è”的工单(用于获取 clientId ç­‰ä¿¡æ¯ï¼‰
        MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(feedback.getWorkOrderId());
@@ -166,10 +182,8 @@
        Long batchId = batch != null ? batch.getId() : null;
        String batchCode = batch != null ? batch.getCode() : null;
        // 1.5 èŽ·å–è™šæ‹Ÿçº¿è¾¹åº“
        MesWmWarehouseDO virtualWarehouse = warehouseService.getWarehouseByCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE);
        MesWmWarehouseLocationDO virtualLocation = locationService.getWarehouseLocationByCode(MesWmWarehouseLocationDO.WIP_VIRTUAL_LOCATION);
        MesWmWarehouseAreaDO virtualArea = areaService.getWarehouseAreaByCode(MesWmWarehouseAreaDO.WIP_VIRTUAL_AREA);
        // 1.5 ä¼˜å…ˆä½¿ç”¨å·¥è‰ºè·¯çº¿äº§å“ç»‘定的报工仓库;历史数据未配置时回退虚拟线边库。
        WarehouseTarget outputTarget = resolveOutputTarget(feedback.getItemId());
        // 3. æ ¹æ®æ˜¯å¦éœ€è¦æ£€éªŒåˆ†æ”¯å¤„理
        if (checkFlag) {
@@ -189,7 +203,7 @@
                productProduceLineService.createProductProduceLine(unqualifiedLine);
                MesWmProductProduceDetailDO unqualifiedDetail = buildProduceDetail(produce, feedback, batchId, batchCode,
                        unqualifiedLine.getId(), unqualifiedQty,
                        virtualWarehouse.getId(), virtualLocation.getId(), virtualArea.getId());
                        outputTarget.warehouseId(), outputTarget.locationId(), outputTarget.areaId());
                productProduceDetailService.createProductProduceDetail(unqualifiedDetail);
            }
            // 3.2.2 åˆæ ¼å“è¡Œ + æ˜Žç»†
@@ -199,7 +213,7 @@
                productProduceLineService.createProductProduceLine(qualifiedLine);
                MesWmProductProduceDetailDO qualifiedDetail = buildProduceDetail(produce, feedback, batchId, batchCode,
                        qualifiedLine.getId(), qualifiedQty,
                        virtualWarehouse.getId(), virtualLocation.getId(), virtualArea.getId());
                        outputTarget.warehouseId(), outputTarget.locationId(), outputTarget.areaId());
                productProduceDetailService.createProductProduceDetail(qualifiedDetail);
            }
        }
@@ -259,13 +273,9 @@
        if (produce == null) {
            throw exception(WM_PRODUCT_PRODUCE_NOT_EXISTS);
        }
        // 1.2 èŽ·å–è™šæ‹Ÿçº¿è¾¹åº“
        MesWmWarehouseDO virtualWarehouse = warehouseService.getWarehouseByCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE);
        MesWmWarehouseLocationDO virtualLocation = locationService.getWarehouseLocationByCode(MesWmWarehouseLocationDO.WIP_VIRTUAL_LOCATION);
        MesWmWarehouseAreaDO virtualArea = areaService.getWarehouseAreaByCode(MesWmWarehouseAreaDO.WIP_VIRTUAL_AREA);
        // 2. é€šè¿‡ sourceLineId ç›´æŽ¥å®šä½å¾…检验行
        MesWmProductProduceLineDO pendingLine = productProduceLineService.validateProductProduceLineExists(sourceLineId);
        WarehouseTarget outputTarget = resolveOutputTarget(pendingLine.getItemId());
        // 3A. æƒ…况一:存在不合格品数量,需要拆分行
        if (unqualifiedQty != null && unqualifiedQty.compareTo(BigDecimal.ZERO) > 0) {
@@ -282,7 +292,7 @@
                    .produceId(produce.getId()).lineId(unqualifiedLine.getId())
                    .itemId(pendingLine.getItemId()).quantity(unqualifiedQty)
                    .batchId(pendingLine.getBatchId()).batchCode(pendingLine.getBatchCode())
                    .warehouseId(virtualWarehouse.getId()).locationId(virtualLocation.getId()).areaId(virtualArea.getId())
                    .warehouseId(outputTarget.warehouseId()).locationId(outputTarget.locationId()).areaId(outputTarget.areaId())
                    .build());
            // 3A.2 åˆæ ¼å“ï¼šå¤ç”¨åŽŸå¾…æ£€è¡Œï¼Œæ›´æ–°æ•°é‡å’ŒçŠ¶æ€ + æ˜Žç»†
            pendingLine.setQuantity(ObjectUtil.defaultIfNull(qualifiedQty, BigDecimal.ZERO));
@@ -293,7 +303,7 @@
                        .produceId(produce.getId()).lineId(pendingLine.getId())
                        .itemId(pendingLine.getItemId()).quantity(qualifiedQty)
                        .batchId(pendingLine.getBatchId()).batchCode(pendingLine.getBatchCode())
                        .warehouseId(virtualWarehouse.getId()).locationId(virtualLocation.getId()).areaId(virtualArea.getId())
                        .warehouseId(outputTarget.warehouseId()).locationId(outputTarget.locationId()).areaId(outputTarget.areaId())
                        .build());
            }
        } else {
@@ -304,7 +314,7 @@
                    .produceId(produce.getId()).lineId(pendingLine.getId())
                    .itemId(pendingLine.getItemId()).quantity(pendingLine.getQuantity())
                    .batchId(pendingLine.getBatchId()).batchCode(pendingLine.getBatchCode())
                    .warehouseId(virtualWarehouse.getId()).locationId(virtualLocation.getId()).areaId(virtualArea.getId())
                    .warehouseId(outputTarget.warehouseId()).locationId(outputTarget.locationId()).areaId(outputTarget.areaId())
                    .build());
        }
@@ -312,4 +322,23 @@
        finishProductProduce(produce.getId());
    }
    private WarehouseTarget resolveOutputTarget(Long itemId) {
        MesProRouteProductDO routeProduct = itemId != null
                ? routeProductService.getRouteProductByItemId(itemId) : null;
        if (routeProduct != null && routeProduct.getWarehouseId() != null
                && routeProduct.getLocationId() != null && routeProduct.getAreaId() != null) {
            areaService.validateWarehouseAreaExists(routeProduct.getWarehouseId(),
                    routeProduct.getLocationId(), routeProduct.getAreaId());
            return new WarehouseTarget(routeProduct.getWarehouseId(), routeProduct.getLocationId(), routeProduct.getAreaId());
        }
        MesWmWarehouseDO warehouse = warehouseService.getWarehouseByCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE);
        MesWmWarehouseLocationDO location = locationService.getWarehouseLocationByCode(
                MesWmWarehouseLocationDO.WIP_VIRTUAL_LOCATION);
        MesWmWarehouseAreaDO area = areaService.getWarehouseAreaByCode(MesWmWarehouseAreaDO.WIP_VIRTUAL_AREA);
        return new WarehouseTarget(warehouse.getId(), location.getId(), area.getId());
    }
    private record WarehouseTarget(Long warehouseId, Long locationId, Long areaId) {
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productreceipt/MesWmProductReceiptServiceImpl.java
@@ -81,6 +81,8 @@
    private MesWmWarehouseLocationService locationService;
    @Resource
    private MesWmWarehouseAreaService areaService;
    @Resource
    private cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceDetailService productProduceDetailService;
    @Override
    public Long createProductReceipt(MesWmProductReceiptSaveReqVO createReqVO) {
@@ -143,8 +145,13 @@
            MesMdItemDO item = mdItemService.getItem(produceLine.getItemId());
            Long mdmItemId = item != null && item.getMdmItemId() != null
                    ? item.getMdmItemId() : produceLine.getItemId();
            List<cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDetailDO> details =
                    productProduceDetailService.getProductProduceDetailListByLineId(produceLine.getId());
            Long warehouseId = CollUtil.isNotEmpty(details) ? details.get(0).getWarehouseId() : virtualWarehouse.getId();
            Long locationId = CollUtil.isNotEmpty(details) ? details.get(0).getLocationId() : virtualLocation.getId();
            Long areaId = CollUtil.isNotEmpty(details) ? details.get(0).getAreaId() : virtualArea.getId();
            MesWmMaterialStockDO stock = materialStockService.getMaterialStockByCompositeKey(
                    mdmItemId, virtualWarehouse.getId(), virtualLocation.getId(), virtualArea.getId(),
                    mdmItemId, warehouseId, locationId, areaId,
                    produceLine.getBatchId(), null);
            productReceiptLineMapper.insert(MesWmProductReceiptLineDO.builder()
                    .receiptId(receipt.getId())
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserPageReqVO.java
@@ -22,6 +22,9 @@
    @Schema(description = "用户账号,模糊匹配", example = "yudao")
    private String username;
    @Schema(description = "用户昵称,模糊匹配", example = "管理员")
    private String nickname;
    @Schema(description = "手机号码,模糊匹配", example = "yudao")
    private String mobile;
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/user/AdminUserMapper.java
@@ -28,6 +28,7 @@
    default PageResult<AdminUserDO> selectPage(UserPageReqVO reqVO, Collection<Long> deptIds, Collection<Long> userIds) {
        return selectPage(reqVO, new LambdaQueryWrapperX<AdminUserDO>()
                .likeIfPresent(AdminUserDO::getUsername, reqVO.getUsername())
                .likeIfPresent(AdminUserDO::getNickname, reqVO.getNickname())
                .likeIfPresent(AdminUserDO::getMobile, reqVO.getMobile())
                .eqIfPresent(AdminUserDO::getStatus, reqVO.getStatus())
                .betweenIfPresent(AdminUserDO::getCreateTime, reqVO.getCreateTime())