云
2026-10-25 daf9f8dbb7ba36eb283221d2c6022039aefaf97d
feat(aftersales): 售后退货单同步MES批次信息并优化工单处理流程

- 实现售后退货单批准后同步MES解析的批次信息回写到售后明细
- 新增MesWmReturnSalesLineRespDTO用于跨模块查询退货单行批次信息
- 优化售后工单创建逻辑,填处理人时自动设置为处理中状态
- 增加对已指派工单修改时的状态转换处理
- 添加handlerUserId字段到工单保存请求对象
- 修复考勤记录中员工关联验证错误提示
- 优化考勤状态查询接口返回未关联员工时的友好提示
- 修复YEAR_MONTH字段映射MySQL保留字问题
- 增加薪酬结构删除前引用检查防止误删
- 强化转正申请中申请人和员工关联验证
- 优化条码服务启用状态检查避免停用条码被使用
- 完善销售出库和退货批次自动生成功能
- 添加HRM菜单组件路径修复SQL脚本
- 添加条码配置初始化SQL脚本
已添加3个文件
已修改20个文件
395 ■■■■■ 文件已修改
.gitignore 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/hrm_menu_component_fix_20261024.sql 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/xgdl_barcode_config_seed_20261025.sql 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/returnobj/AfterSaleReturnServiceImpl.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceRecordController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmEmployeeSalarySaveReqVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryStructureSaveReqVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmAttendanceSummaryDO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/HrmEmployeeSalaryMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmRegularizeApplicationServiceImpl.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmEmployeeSalaryServiceImpl.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryStructureServiceImpl.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/MesWmReturnSalesApi.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/dto/MesWmReturnSalesLineRespDTO.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/MesWmReturnSalesApiImpl.java 7 ●●●●● 补丁 | 查看 | 原始文档 | 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/wm/barcode/MesWmBarcodeServiceImpl.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesLineServiceImpl.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnsales/MesWmReturnSalesLineServiceImpl.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore
@@ -32,7 +32,9 @@
!sql/mysql/xgdl_aftersales_return_link_repair_20261024.sql
!sql/mysql/xgdl_aftersales_refund_confirm_20261024.sql
!sql/mysql/hrm_attendance_location_20261024.sql
!sql/mysql/hrm_menu_component_fix_20261024.sql
!sql/mysql/xgdl_enablement_menu_fix_20261024.sql
!sql/mysql/xgdl_barcode_config_seed_20261025.sql
*.md
!docs/xgdl-business-flow-alignment-20261024.md
*.xlsx
sql/mysql/hrm_menu_component_fix_20261024.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
-- ä¿®æ­£ HRM èœå•组件路径。仅处理这三个已确认的菜单,不改动其他租户定制菜单。
UPDATE system_menu
SET component = 'hrm/regularize/index'
WHERE id = 6007 AND path = 'regularize'
  AND (component IS NULL OR component <> 'hrm/regularize/index');
UPDATE system_menu
SET component = 'hrm/salary/structure/index'
WHERE id = 1065710 AND path = 'salary-structure'
  AND (component IS NULL OR component <> 'hrm/salary/structure/index');
UPDATE system_menu
SET component = 'hrm/salary/employee-salary/index'
WHERE id = 1065720 AND path = 'employee-salary'
  AND (component IS NULL OR component <> 'hrm/salary/employee-salary/index');
sql/mysql/xgdl_barcode_config_seed_20261025.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
-- æ–°é«˜ç”µåŠ›æ¡ç é…ç½®ï¼šä»…è¡¥å½“å‰å­—å…¸ä¸­å­˜åœ¨ã€ä¸”å°šæœªé…ç½®çš„ä¸šåŠ¡ç±»åž‹ã€‚
-- æ¨¡æ¿åªä½¿ç”¨æ¡ç ç”Ÿæˆå™¨æ”¯æŒçš„ {BUSINESSCODE} å ä½ç¬¦ï¼›ç»Ÿä¸€é‡‡ç”¨äºŒç»´ç ã€‚
-- é‡å¤æ‰§è¡Œä¸ä¼šè¦†ç›–已有有效配置,也不会恢复逻辑删除的旧配置。
INSERT INTO mes_wm_barcode_config
    (format, biz_type, content_format, content_example, auto_generate_flag,
     status, remark, creator, updater, deleted)
SELECT 1, seed.biz_type,
       CONCAT(seed.prefix, '-{BUSINESSCODE}'),
       CONCAT(seed.prefix, '-DEMO001'),
       b'1', 0, '新高电力业务类型默认条码配置', '1', '1', b'0'
FROM (
    SELECT 102 AS biz_type, 'WH' AS prefix UNION ALL
    SELECT 103, 'LOC' UNION ALL
    SELECT 104, 'AREA' UNION ALL
    SELECT 105, 'PKG' UNION ALL
    SELECT 106, 'STOCK' UNION ALL
    SELECT 107, 'BATCH' UNION ALL
    SELECT 109, 'SN' UNION ALL
    SELECT 300, 'CARD' UNION ALL
    SELECT 301, 'WO' UNION ALL
    SELECT 302, 'TRANS' UNION ALL
    SELECT 400, 'EQUIP' UNION ALL
    SELECT 500, 'TOOL' UNION ALL
    SELECT 600, 'ITEM' UNION ALL
    SELECT 601, 'VENDOR' UNION ALL
    SELECT 602, 'WS' UNION ALL
    SELECT 603, 'WORKSHOP' UNION ALL
    SELECT 604, 'USER' UNION ALL
    SELECT 605, 'CLIENT'
) seed
WHERE EXISTS (
    SELECT 1 FROM system_dict_data dict
    WHERE dict.dict_type = 'mes_wm_barcode_biz_type'
      AND CAST(dict.value AS UNSIGNED) = seed.biz_type
      AND dict.status = 0 AND dict.deleted = b'0'
)
AND NOT EXISTS (
    SELECT 1 FROM mes_wm_barcode_config config
    WHERE config.biz_type = seed.biz_type AND config.deleted = b'0'
);
-- ä¸‹åˆ—旧模板使用未实现的占位符,会被渲染为空串,导致后续条码内容重复。
-- ä»…对完全匹配的错误模板修正,保留用户自行维护的其它模板。
UPDATE mes_wm_barcode_config
SET content_format = 'WH-{BUSINESSCODE}', content_example = 'WH-WH001'
WHERE biz_type = 102 AND content_format = 'SN-{serialNo}' AND deleted = b'0';
UPDATE mes_wm_barcode_config
SET content_format = 'BATCH-{BUSINESSCODE}'
WHERE biz_type = 107 AND content_format = 'BATCH-{batchCode}' AND deleted = b'0';
UPDATE mes_wm_barcode_config
SET content_format = 'SN-{BUSINESSCODE}'
WHERE biz_type = 109 AND content_format = 'SN-{serialNo}' AND deleted = b'0';
UPDATE mes_wm_barcode_config
SET content_format = 'ITEM-{BUSINESSCODE}'
WHERE biz_type = 600 AND content_format = 'ITEM-{code}' AND deleted = b'0';
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketSaveReqVO.java
@@ -52,6 +52,9 @@
    @Schema(description = "严重程度: 0-轻微 1-一般 2-严重 3-致命", example = "1")
    private Integer severityLevel;
    @Schema(description = "处理人用户编号", example = "1")
    private Long handlerUserId;
    @Schema(description = "备注", example = "客户要求尽快处理")
    private String remark;
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/returnobj/AfterSaleReturnServiceImpl.java
@@ -22,6 +22,7 @@
import cn.iocoder.yudao.module.aftersales.service.permission.bo.AfterSalePermissionCreateReqBO;
import cn.iocoder.yudao.module.mes.api.returnsales.MesWmReturnSalesApi;
import cn.iocoder.yudao.module.mes.api.returnsales.dto.MesWmReturnSalesCreateReqDTO;
import cn.iocoder.yudao.module.mes.api.returnsales.dto.MesWmReturnSalesLineRespDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -215,7 +216,10 @@
        }).toList());
        Long mesReturnId = mesWmReturnSalesApi.createReturnSales(mesReqDTO);
        // 4. æ›´æ–°é€€è´§ç”³è¯·çŠ¶æ€
        // 4. å›žå†™ MES è§£æžåŽçš„æ‰¹æ¬¡ï¼ˆåŽŸå•æœªè®°å½•æ‰¹æ¬¡çš„å•†å“ç”± MES è‡ªåŠ¨ç”Ÿæˆé€€è´§æ‰¹æ¬¡ï¼‰
        syncReturnItemBatch(id, mesReturnId);
        // 5. æ›´æ–°é€€è´§ç”³è¯·çŠ¶æ€
        returnMapper.updateById(new AfterSaleReturnDO().setId(id)
                .setStatus(AfterSaleReturnStatusEnum.APPROVED.getStatus())
                .setMesReturnId(mesReturnId)
@@ -223,6 +227,30 @@
                .setAuditTime(LocalDateTime.now()));
    }
    /**
     * å°† MES é€€è´§å•行上的批次回写到售后明细,便于页面展示与追溯
     */
    private void syncReturnItemBatch(Long returnId, Long mesReturnId) {
        List<MesWmReturnSalesLineRespDTO> mesLines = mesWmReturnSalesApi.getReturnSalesLineList(mesReturnId);
        if (CollUtil.isEmpty(mesLines)) {
            return;
        }
        Map<Long, MesWmReturnSalesLineRespDTO> lineMap = mesLines.stream()
                .filter(line -> line.getBatchId() != null)
                .collect(Collectors.toMap(MesWmReturnSalesLineRespDTO::getItemId, line -> line, (a, b) -> a));
        if (lineMap.isEmpty()) {
            return;
        }
        for (AfterSaleReturnItemDO item : returnItemMapper.selectListByReturnId(returnId)) {
            MesWmReturnSalesLineRespDTO line = lineMap.get(item.getItemId());
            if (line == null || item.getBatchId() != null) {
                continue;
            }
            returnItemMapper.updateById(new AfterSaleReturnItemDO().setId(item.getId())
                    .setBatchId(line.getBatchId()).setBatchCode(line.getBatchCode()));
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void cancelReturn(Long id) {
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/ticket/AfterSaleTicketServiceImpl.java
@@ -65,10 +65,12 @@
            throw exception(TICKET_NO_EXISTS);
        }
        // 2. æ’入工单
        // 2. æ’入工单(填了处理人即视为已指派,直接进入处理中)
        AfterSaleTicketDO ticket = BeanUtils.toBean(createReqVO, AfterSaleTicketDO.class)
                .setNo(no)
                .setStatus(AfterSaleTicketStatusEnum.DRAFT.getStatus())
                .setStatus(createReqVO.getHandlerUserId() != null
                        ? AfterSaleTicketStatusEnum.PROCESSING.getStatus()
                        : AfterSaleTicketStatusEnum.DRAFT.getStatus())
                .setOwnerUserId(userId);
        ticketMapper.insert(ticket);
@@ -103,8 +105,12 @@
        AfterSaleTicketDO ticket = validateTicketExists(updateReqVO.getId());
        validateTicketStatus(ticket, AfterSaleTicketStatusEnum.DRAFT);
        // 2. æ›´æ–°å·¥å•
        // 2. æ›´æ–°å·¥å•(草稿单填了处理人即视为指派,进入处理中)
        AfterSaleTicketDO updateObj = BeanUtils.toBean(updateReqVO, AfterSaleTicketDO.class);
        if (AfterSaleTicketStatusEnum.DRAFT.getStatus().equals(ticket.getStatus())
                && updateReqVO.getHandlerUserId() != null) {
            updateObj.setStatus(AfterSaleTicketStatusEnum.PROCESSING.getStatus());
        }
        ticketMapper.updateById(updateObj);
        // 3. æ›´æ–°æ˜Žç»†è¡Œï¼ˆå…ˆåˆ åŽå¢žï¼‰
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java
@@ -101,6 +101,7 @@
    ErrorCode ATTENDANCE_LOCATION_ACCURACY_INVALID = new ErrorCode(1_030_004_053, "定位精度须大于 0 ä¸”不超过 100 ç±³ï¼Œè¯·åˆ°å¼€é˜”位置重试");
    ErrorCode ATTENDANCE_LOCATION_SYSTEM_INVALID = new ErrorCode(1_030_004_054, "打卡坐标系必须为 WGS84");
    ErrorCode ATTENDANCE_LOCATION_OUT_OF_RANGE = new ErrorCode(1_030_004_055, "当前位置距“{}”{}米,超过允许范围{}ç±³");
    ErrorCode ATTENDANCE_EMPLOYEE_LINK_REQUIRED = new ErrorCode(1_030_004_056, "当前账号未关联员工档案,请联系管理员在员工管理中完成关联");
    // ========== ç»©æ•ˆè€ƒæ ¸ 1-030-008-000 ==========
    ErrorCode PERFORMANCE_APPRAISE_NOT_EXISTS = new ErrorCode(1_030_008_000, "绩效考核方案不存在");
@@ -154,6 +155,7 @@
    ErrorCode LEAVE_TYPE_CONFIG_EXISTS = new ErrorCode(1_030_005_032, "已存在请假类型扣款配置【{}】,且未开启覆盖!");
    ErrorCode TAX_RATE_CONFIG_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_030_005_033, "导入个税税率配置数据不能为空");
    ErrorCode TAX_RATE_CONFIG_EXISTS = new ErrorCode(1_030_005_034, "已存在个税税率配置【{}】,且未开启覆盖!");
    ErrorCode SALARY_STRUCTURE_IN_USE = new ErrorCode(1_030_005_035, "该薪酬结构已有员工薪酬档案引用,不能删除");
    // ========== è½¬æ­£ç”³è¯· 1-030-006-000 ==========
    ErrorCode REGULARIZE_APPLICATION_NOT_EXISTS = new ErrorCode(1_030_006_000, "转正申请不存在");
@@ -164,6 +166,8 @@
    ErrorCode REGULARIZE_APPLICATION_EXISTS = new ErrorCode(1_030_006_005, "该员工已存在转正申请,请勿重复提交");
    ErrorCode REGULARIZE_APPLICATION_AUDIT_FAIL_NOT_PROCESS = new ErrorCode(1_030_006_006, "只有审批中状态的转正申请才能审核");
    ErrorCode REGULARIZE_APPLICATION_AUDIT_REJECT_REASON_REQUIRED = new ErrorCode(1_030_006_007, "审核不通过时必须填写不通过原因");
    ErrorCode REGULARIZE_APPLICANT_NOT_EXISTS = new ErrorCode(1_030_006_008, "转正申请人不存在,请选择有效的系统用户");
    ErrorCode REGULARIZE_EMPLOYEE_LINK_REQUIRED = new ErrorCode(1_030_006_009, "申请人未关联员工档案,请先在员工管理中完成关联");
    // ========== åŸ¹è®­ç®¡ç† 1-030-009-000 ==========
    ErrorCode TRAINING_NOT_EXISTS = new ErrorCode(1_030_009_000, "培训活动不存在");
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceRecordController.java
@@ -45,7 +45,7 @@
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.EMPLOYEE_NOT_EXISTS;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.ATTENDANCE_EMPLOYEE_LINK_REQUIRED;
@Tag(name = "管理后台 - HRM è€ƒå‹¤è®°å½•")
@RestController
@@ -71,8 +71,7 @@
        Long loginUserId = getLoginUserId();
        HrmEmployeeDO employee = employeeService.getEmployeeByUserId(loginUserId);
        if (employee == null) {
            throw exception(EMPLOYEE_NOT_EXISTS,
                    "当前登录账号未关联员工档案,无法打卡。请联系管理员在【员工管理】中将该登录账号绑定到员工。");
            throw exception(ATTENDANCE_EMPLOYEE_LINK_REQUIRED);
        }
        HrmAttendanceClockResult result = attendanceRecordService.clockIn(employee.getId(), LocalDateTime.now(), reqVO, legacyLocation);
        return success(buildClockResultVO(result, employee));
@@ -87,8 +86,7 @@
        Long loginUserId = getLoginUserId();
        HrmEmployeeDO employee = employeeService.getEmployeeByUserId(loginUserId);
        if (employee == null) {
            throw exception(EMPLOYEE_NOT_EXISTS,
                    "当前登录账号未关联员工档案,无法打卡。请联系管理员在【员工管理】中将该登录账号绑定到员工。");
            throw exception(ATTENDANCE_EMPLOYEE_LINK_REQUIRED);
        }
        HrmAttendanceClockResult result = attendanceRecordService.clockOut(employee.getId(), LocalDateTime.now(), reqVO, legacyLocation);
        return success(buildClockResultVO(result, employee));
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmEmployeeSalarySaveReqVO.java
@@ -2,6 +2,7 @@
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.DecimalMin;
import lombok.Data;
import java.math.BigDecimal;
@@ -19,9 +20,12 @@
    private Long userId;
    @Schema(description = "薪酬结构ID", example = "1")
    @NotNull(message = "薪酬结构不能为空")
    private Long salaryStructureId;
    @Schema(description = "基本工资", example = "8000.00")
    @NotNull(message = "基本工资不能为空")
    @DecimalMin(value = "0", message = "基本工资不能为负数")
    private BigDecimal baseSalary;
    @Schema(description = "绩效工资", example = "2000.00")
@@ -47,4 +51,4 @@
    @Schema(description = "备注", example = "备注信息")
    private String remark;
}
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryStructureSaveReqVO.java
@@ -3,6 +3,7 @@
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.DecimalMin;
import lombok.Data;
import java.math.BigDecimal;
@@ -19,6 +20,8 @@
    private String name;
    @Schema(description = "基本工资", example = "8000.00")
    @NotNull(message = "基本工资不能为空")
    @DecimalMin(value = "0", message = "基本工资不能为负数")
    private BigDecimal baseSalary;
    @Schema(description = "绩效工资占比(%)", example = "20")
@@ -40,4 +43,4 @@
    @Schema(description = "备注", example = "备注信息")
    private String remark;
}
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmAttendanceSummaryDO.java
@@ -2,6 +2,7 @@
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
@@ -43,7 +44,10 @@
    /**
     * å¹´æœˆï¼Œæ ¼å¼ï¼šyyyy-MM
     *
     * YEAR_MONTH æ˜¯ MySQL ä¿ç•™å­—,必须加反引号
     */
    @TableField("`year_month`")
    private String yearMonth;
    /**
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/HrmEmployeeSalaryMapper.java
@@ -18,6 +18,11 @@
@Mapper
public interface HrmEmployeeSalaryMapper extends BaseMapperX<HrmEmployeeSalaryDO> {
    default long selectCountByStructureId(Long structureId) {
        return selectCount(new LambdaQueryWrapperX<HrmEmployeeSalaryDO>()
                .eq(HrmEmployeeSalaryDO::getSalaryStructureId, structureId));
    }
    default HrmEmployeeSalaryDO selectByUserIdAndEffectiveDate(Long userId, LocalDate effectiveDate) {
        return selectOne(new LambdaQueryWrapperX<HrmEmployeeSalaryDO>()
                .eq(HrmEmployeeSalaryDO::getUserId, userId)
@@ -46,4 +51,4 @@
                .orderByDesc(HrmEmployeeSalaryDO::getId));
    }
}
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmRegularizeApplicationServiceImpl.java
@@ -98,17 +98,13 @@
        if (userId == null) {
            userId = getCurrentUserId();
        }
        AdminUserRespDTO user = validateApplicant(userId);
        HrmRegularizeApplicationDO existing = regularizeApplicationMapper.selectByUserIdAndStatusNot(userId,
                HrmAuditStatusEnum.CANCEL.getStatus());
        if (existing != null) {
            throw exception(REGULARIZE_APPLICATION_EXISTS);
        }
        String no = generateUniqueNo();
        AdminUserRespDTO user = adminUserApi.getUser(userId);
        if (user == null) {
            userId = getCurrentUserId();
            user = adminUserApi.getUser(userId);
        }
        HrmRegularizeApplicationDO application = BeanUtils.toBean(createReqVO, HrmRegularizeApplicationDO.class);
        application.setNo(no);
        application.setUserId(userId);
@@ -121,6 +117,17 @@
    private Long getCurrentUserId() {
        return SecurityFrameworkUtils.getLoginUserId();
    }
    private AdminUserRespDTO validateApplicant(Long userId) {
        AdminUserRespDTO user = userId == null ? null : adminUserApi.getUser(userId);
        if (user == null) {
            throw exception(REGULARIZE_APPLICANT_NOT_EXISTS);
        }
        if (employeeMapper.selectByUserId(userId) == null) {
            throw exception(REGULARIZE_EMPLOYEE_LINK_REQUIRED);
        }
        return user;
    }
    private String generateUniqueNo() {
@@ -137,6 +144,9 @@
        if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())
                && !Objects.equals(application.getStatus(), HrmAuditStatusEnum.REJECT.getStatus())) {
            throw exception(REGULARIZE_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
        }
        if (updateReqVO.getUserId() != null) {
            validateApplicant(updateReqVO.getUserId());
        }
        HrmRegularizeApplicationDO updateObj = BeanUtils.toBean(updateReqVO, HrmRegularizeApplicationDO.class);
        regularizeApplicationMapper.updateById(updateObj);
@@ -248,6 +258,7 @@
                && !Objects.equals(application.getStatus(), HrmAuditStatusEnum.REJECT.getStatus())) {
            throw exception(REGULARIZE_APPLICATION_SUBMIT_FAIL_NOT_EDITABLE);
        }
        validateApplicant(application.getUserId());
        // 2. æ ¡éªŒå®¡æ‰¹é…ç½®å·²å¯ç”¨ä¸”配置了有效审批人(未配置时抛出带明确提示的业务异常)
        approvalConfigApi.validateApprovalEnabledAndGetApprovers(REGULARIZE_APPROVE_BIZ_TYPE);
@@ -310,8 +321,7 @@
    private void writeBackEmployee(HrmRegularizeApplicationDO application) {
        HrmEmployeeDO employee = employeeMapper.selectByUserId(application.getUserId());
        if (employee == null) {
            log.warn("[writeBackEmployee] è½¬æ­£ç”³è¯·({}) ç”¨æˆ·({}) æœªæ‰¾åˆ°å‘˜å·¥æ¡£æ¡ˆï¼Œè·³è¿‡å›žå†™", application.getId(), application.getUserId());
            return;
            throw exception(REGULARIZE_EMPLOYEE_LINK_REQUIRED);
        }
        LocalDate regularDate = application.getExpectedDate() != null ? application.getExpectedDate() : LocalDate.now();
        employeeMapper.updateById(HrmEmployeeDO.builder()
@@ -361,4 +371,4 @@
        return user == null ? null : user.getNickname();
    }
}
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java
@@ -83,14 +83,21 @@
    @Override
    public HrmAttendanceMyStatusRespVO getMyStatus(Long loginUserId, LocalDate date) {
        LocalDate queryDate = date != null ? date : LocalDate.now();
        HrmAttendanceMyStatusRespVO respVO = new HrmAttendanceMyStatusRespVO();
        respVO.setUserId(loginUserId);
        respVO.setDate(queryDate);
        respVO.setServerTime(LocalDateTime.now());
        // 1. ç™»å½•用户 -> å‘˜å·¥æ¡£æ¡ˆ
        HrmEmployeeDO employee = employeeService.getEmployeeByUserId(loginUserId);
        if (employee == null) {
            throw exception(EMPLOYEE_NOT_EXISTS,
                    "当前登录账号未关联员工档案,无法打卡。请联系管理员在【员工管理】中将该登录账号绑定到员工。");
            HrmAttendanceMyStatusRespVO.Clockable clockable = new HrmAttendanceMyStatusRespVO.Clockable();
            clockable.setClockIn(false);
            clockable.setClockOut(false);
            clockable.setNextAction("none");
            clockable.setMessage("当前账号未关联员工档案,请联系管理员在员工管理中完成关联");
            respVO.setClockable(clockable);
            return respVO;
        }
        HrmAttendanceMyStatusRespVO respVO = new HrmAttendanceMyStatusRespVO();
        respVO.setUserId(loginUserId);
        respVO.setEmployeeId(employee.getId());
        respVO.setEmployeeName(employee.getName());
        respVO.setDeptId(employee.getDeptId());
@@ -100,8 +107,6 @@
                respVO.setDeptName(dept.getName());
            }
        }
        respVO.setDate(queryDate);
        respVO.setServerTime(LocalDateTime.now());
        // 2. æ˜¯å¦å·¥ä½œæ—¥ + ä¼‘息原因
        boolean workDay = attendanceHolidayService.isWorkDay(queryDate);
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmEmployeeSalaryServiceImpl.java
@@ -4,6 +4,8 @@
import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.HrmEmployeeSalaryPageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmEmployeeSalaryDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmEmployeeSalaryMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSalaryStructureMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@@ -12,6 +14,9 @@
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.EMPLOYEE_SALARY_NOT_EXISTS;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.EMPLOYEE_SALARY_EXISTS;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.HRM_EMPLOYEE_NOT_EXISTS;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.SALARY_STRUCTURE_NOT_EXISTS;
/**
 * å‘˜å·¥è–ªé…¬æ¡£æ¡ˆ Service å®žçŽ°ç±»
@@ -24,9 +29,15 @@
    @Resource
    private HrmEmployeeSalaryMapper employeeSalaryMapper;
    @Resource
    private HrmSalaryStructureMapper structureMapper;
    @Resource
    private HrmEmployeeMapper employeeMapper;
    @Override
    public Long createEmployeeSalary(HrmEmployeeSalaryDO salary) {
        validateReferences(salary);
        validateUniqueDate(salary);
        employeeSalaryMapper.insert(salary);
        return salary.getId();
    }
@@ -34,6 +45,8 @@
    @Override
    public void updateEmployeeSalary(HrmEmployeeSalaryDO salary) {
        validateEmployeeSalaryExists(salary.getId());
        validateReferences(salary);
        validateUniqueDate(salary);
        employeeSalaryMapper.updateById(salary);
    }
@@ -69,4 +82,22 @@
        }
    }
}
    private void validateReferences(HrmEmployeeSalaryDO salary) {
        if (salary.getUserId() == null || employeeMapper.selectById(salary.getUserId()) == null) {
            throw exception(HRM_EMPLOYEE_NOT_EXISTS);
        }
        if (salary.getSalaryStructureId() == null
                || structureMapper.selectById(salary.getSalaryStructureId()) == null) {
            throw exception(SALARY_STRUCTURE_NOT_EXISTS);
        }
    }
    private void validateUniqueDate(HrmEmployeeSalaryDO salary) {
        HrmEmployeeSalaryDO existing = employeeSalaryMapper.selectByUserIdAndEffectiveDate(
                salary.getUserId(), salary.getEffectiveDate());
        if (existing != null && !existing.getId().equals(salary.getId())) {
            throw exception(EMPLOYEE_SALARY_EXISTS);
        }
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryStructureServiceImpl.java
@@ -4,6 +4,7 @@
import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.HrmSalaryStructurePageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryStructureDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSalaryStructureMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmEmployeeSalaryMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@@ -12,6 +13,7 @@
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.SALARY_STRUCTURE_NOT_EXISTS;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.SALARY_STRUCTURE_IN_USE;
/**
 * è–ªé…¬ç»“æž„ Service å®žçŽ°ç±»
@@ -24,6 +26,8 @@
    @Resource
    private HrmSalaryStructureMapper structureMapper;
    @Resource
    private HrmEmployeeSalaryMapper employeeSalaryMapper;
    @Override
    public Long createStructure(HrmSalaryStructureDO structure) {
@@ -40,6 +44,9 @@
    @Override
    public void deleteStructure(Long id) {
        validateStructureExists(id);
        if (employeeSalaryMapper.selectCountByStructureId(id) > 0) {
            throw exception(SALARY_STRUCTURE_IN_USE);
        }
        structureMapper.deleteById(id);
    }
@@ -64,4 +71,4 @@
        }
    }
}
}
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/MesWmReturnSalesApi.java
@@ -1,6 +1,9 @@
package cn.iocoder.yudao.module.mes.api.returnsales;
import cn.iocoder.yudao.module.mes.api.returnsales.dto.MesWmReturnSalesCreateReqDTO;
import cn.iocoder.yudao.module.mes.api.returnsales.dto.MesWmReturnSalesLineRespDTO;
import java.util.List;
/**
 * MES é”€å”®é€€è´§å• API æŽ¥å£
@@ -20,4 +23,12 @@
    /** å–消尚未完成入库的销售退货单。 */
    void cancelReturnSales(Long id);
    /**
     * æŸ¥è¯¢é”€å”®é€€è´§å•行(用于获取 MES è§£æžåŽå›žå¡«çš„æ‰¹æ¬¡ï¼‰
     *
     * @param returnSalesId é”€å”®é€€è´§å•编号
     * @return é€€è´§å•行列表
     */
    List<MesWmReturnSalesLineRespDTO> getReturnSalesLineList(Long returnSalesId);
}
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/dto/MesWmReturnSalesLineRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.mes.api.returnsales.dto;
import lombok.Data;
/**
 * MES é”€å”®é€€è´§å•行 Response DTO(跨模块查询批次等字段)
 *
 * @author è¶…级管理员
 */
@Data
public class MesWmReturnSalesLineRespDTO {
    /**
     * è¡Œç¼–号
     */
    private Long id;
    /**
     * ç‰©æ–™ç¼–号
     */
    private Long itemId;
    /**
     * æ‰¹æ¬¡ç¼–号
     */
    private Long batchId;
    /**
     * æ‰¹æ¬¡å·
     */
    private String batchCode;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/MesWmReturnSalesApiImpl.java
@@ -2,6 +2,7 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.api.returnsales.dto.MesWmReturnSalesCreateReqDTO;
import cn.iocoder.yudao.module.mes.api.returnsales.dto.MesWmReturnSalesLineRespDTO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.returnsales.vo.MesWmReturnSalesSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.returnsales.vo.line.MesWmReturnSalesLineSaveReqVO;
import cn.iocoder.yudao.module.mes.service.wm.returnsales.MesWmReturnSalesService;
@@ -49,4 +50,10 @@
        returnSalesService.cancelReturnSales(id);
    }
    @Override
    public List<MesWmReturnSalesLineRespDTO> getReturnSalesLineList(Long returnSalesId) {
        return BeanUtils.toBean(returnSalesLineService.getReturnSalesLineListByReturnId(returnSalesId),
                MesWmReturnSalesLineRespDTO.class);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -825,6 +825,7 @@
    ErrorCode BARCODE_CONFIG_NOT_EXISTS = new ErrorCode(1_040_731_005, "条码配置不存在");
    ErrorCode BARCODE_FORMAT_INVALID = new ErrorCode(1_040_731_006, "条码格式不支持渲染");
    ErrorCode BARCODE_CONTENT_EMPTY = new ErrorCode(1_040_731_007, "条码内容不能为空");
    ErrorCode WM_BARCODE_DISABLED = new ErrorCode(1_040_731_008, "条码已停用,无法继续扫码使用");
    // ========== MES ä»“库管理-SN ç ï¼ˆ1-040-732-000) ==========
    ErrorCode WM_SN_NOT_EXISTS = new ErrorCode(1_040_732_000, "SN ç ä¸å­˜åœ¨");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/barcode/MesWmBarcodeServiceImpl.java
@@ -70,7 +70,8 @@
        // 3. ä¿å­˜æ¡ç è®°å½•
        MesWmBarcodeDO barcode = BeanUtils.toBean(createReqVO, MesWmBarcodeDO.class).setContent(content)
                .setConfigId(config.getId()).setFormat(config.getFormat());
                .setConfigId(config.getId()).setFormat(config.getFormat())
                .setStatus(ObjUtil.defaultIfNull(createReqVO.getStatus(), CommonStatusEnum.ENABLE.getStatus()));
        barcodeMapper.insert(barcode);
        return barcode.getId();
    }
@@ -141,7 +142,7 @@
        if (barcode == null) {
            return;
        }
        if (ObjUtil.notEqual(barcode, id)) {
        if (ObjUtil.notEqual(barcode.getId(), id)) {
            throw exception(WM_BARCODE_ALREADY_EXISTS);
        }
    }
@@ -241,6 +242,10 @@
        // 1. ä¼˜å…ˆæŒ‰æ¡ç å†…容精确命中已有条码记录
        MesWmBarcodeDO barcode = barcodeMapper.selectByContent(content);
        if (barcode != null) {
            // åœç”¨åªå½±å“åŽç»­ä½¿ç”¨ï¼Œä¸åˆ é™¤åŽ†å²è®°å½•å’Œè¿½æº¯æ•°æ®
            if (!CommonStatusEnum.isEnable(barcode.getStatus())) {
                throw exception(WM_BARCODE_DISABLED);
            }
            Map<String, Object> result = new LinkedHashMap<>();
            result.put("matched", true);
            result.put("barcodeId", barcode.getId());
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesLineServiceImpl.java
@@ -5,8 +5,10 @@
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.line.MesWmProductSalesLinePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.line.MesWmProductSalesLineSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.MesWmProductSalesLineMapper;
@@ -167,15 +169,45 @@
    }
    /**
     * æ ¹æ® batchCode è§£æž batchId
     * è§£æžå¹¶å¡«å……批次
     *
     * <p>启用批次管理的物料出库时必须落批次;未记录批次时按物料 + å®¢æˆ· + é”€å”®è®¢å•自动生成,
     * ä¾›åŽç»­é€€è´§ç­‰ä¸šåŠ¡è¿½æº¯ã€‚
     */
    private void fillBatchId(MesWmProductSalesLineSaveReqVO reqVO) {
        if (StrUtil.isBlank(reqVO.getBatchCode())) {
    private void fillBatchId(MesWmProductSalesDO sales, MesMdmItemDO item,
                             MesWmProductSalesLineSaveReqVO reqVO) {
        if (StrUtil.isNotBlank(reqVO.getBatchCode())) {
            MesWmBatchDO batch = batchService.getBatchByCode(reqVO.getBatchCode());
            if (batch != null) {
                reqVO.setBatchId(batch.getId());
                return;
            }
            if (!Boolean.TRUE.equals(item.getIsBatchManaged())) {
                reqVO.setBatchId(null);
                return;
            }
            // æŒ‡å®šäº†æ‰¹æ¬¡å·ä½†ç³»ç»Ÿä¸­ä¸å­˜åœ¨ï¼ŒæŒ‰è¯¥æ‰¹æ¬¡å·å»ºæ¡£
            MesWmBatchDO created = batchService.getOrGenerateBatchCode(buildBatchGenerateReq(sales, reqVO));
            reqVO.setBatchId(created != null ? created.getId() : null);
            return;
        }
        if (!Boolean.TRUE.equals(item.getIsBatchManaged())) {
            reqVO.setBatchId(null);
            return;
        }
        MesWmBatchDO batch = batchService.getBatchByCode(reqVO.getBatchCode());
        reqVO.setBatchId(batch != null ? batch.getId() : null);
        // æœªè®°å½•批次,自动生成
        MesWmBatchDO batch = batchService.getOrGenerateBatchCode(buildBatchGenerateReq(sales, reqVO));
        reqVO.setBatchId(batch != null ? batch.getId() : null)
                .setBatchCode(batch != null ? batch.getCode() : null);
    }
    private MesWmBatchGenerateReqVO buildBatchGenerateReq(MesWmProductSalesDO sales,
                                                          MesWmProductSalesLineSaveReqVO reqVO) {
        return new MesWmBatchGenerateReqVO()
                .setItemId(reqVO.getItemId())
                .setBatchCode(reqVO.getBatchCode())
                .setClientId(sales.getClientId())
                .setSalesOrderCode(sales.getSalesOrderCode());
    }
    /**
@@ -185,11 +217,11 @@
        // æ ¡éªŒä¸»å•存在且为草稿状态
        MesWmProductSalesDO sales = productSalesService.validateProductSalesExistsAndDraft(reqVO.getSalesId());
        // æ ¡éªŒç‰©æ–™å­˜åœ¨
        mdmItemService.validateItemExistsAndEnable(reqVO.getItemId());
        MesMdmItemDO item = mdmItemService.validateItemExistsAndEnable(reqVO.getItemId());
        // æ ¡éªŒå…³è”发货通知单行(存在性 + å½’属 + å­—段一致性)
        validateSalesNoticeLine(sales, reqVO);
        // æ ¹æ® batchCode è§£æž batchId
        fillBatchId(reqVO);
        // è§£æžæ‰¹æ¬¡
        fillBatchId(sales, item, reqVO);
    }
    /**
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnsales/MesWmReturnSalesLineServiceImpl.java
@@ -4,10 +4,12 @@
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.returnsales.vo.line.MesWmReturnSalesLinePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.returnsales.vo.line.MesWmReturnSalesLineSaveReqVO;
import cn.iocoder.yudao.module.mes.api.event.MesReturnSalesProgressEvent;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnsales.MesWmReturnSalesLineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnsales.MesWmReturnSalesDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.returnsales.MesWmReturnSalesLineMapper;
@@ -27,6 +29,7 @@
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.MD_ITEM_BATCH_REQUIRED;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_BATCH_CLIENT_MISMATCH;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_RETURN_SALES_LINE_NOT_EXISTS;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_RETURN_SALES_RQC_SOURCE_INVALID;
@@ -87,9 +90,9 @@
     */
    private void validateLineSaveData(MesWmReturnSalesLineSaveReqVO reqVO) {
        // æ ¡éªŒé€€è´§å•存在 + è‰ç¨¿çŠ¶æ€
        returnSalesService.validateReturnSalesExistsAndPrepare(reqVO.getReturnId());
        // æ ¡éªŒç‰©æ–™å­˜åœ¨ + æ‰¹æ¬¡ç®¡ç†
        validateItemBatchManagement(reqVO.getReturnId(), reqVO.getItemId(), reqVO.getBatchId());
        MesWmReturnSalesDO returnSales = returnSalesService.validateReturnSalesExistsAndPrepare(reqVO.getReturnId());
        // æ ¡éªŒç‰©æ–™å­˜åœ¨ + è§£æžæ‰¹æ¬¡
        resolveAndValidateBatch(returnSales, reqVO);
    }
    @Override
@@ -153,24 +156,33 @@
    }
    /**
     * æ ¡éªŒç‰©æ–™æ‰¹æ¬¡ç®¡ç†
     * è§£æžå¹¶æ ¡éªŒç‰©æ–™æ‰¹æ¬¡
     *
     * @param returnId é€€è´§å•ID
     * @param itemId   ç‰©æ–™ID
     * @param batchId  æ‰¹æ¬¡ID
     * <p>退货商品在销售出库时可能未记录批次,此时按物料 + å®¢æˆ· + é”€å”®è®¢å•自动生成退货批次,
     * ä¿è¯é€€è´§å•仍可追溯到具体批次;已有批次则校验物料归属,批次未指定客户时视为通用批次。
     */
    private void validateItemBatchManagement(Long returnId, Long itemId, Long batchId) {
        MesMdmItemDO item = mdmItemService.validateItemExistsAndEnable(itemId);
        // å¦‚果物料启用了批次管理,则必须选择批次
        if (Boolean.TRUE.equals(item.getIsBatchManaged()) && batchId == null) {
    private void resolveAndValidateBatch(MesWmReturnSalesDO returnSales, MesWmReturnSalesLineSaveReqVO reqVO) {
        MesMdmItemDO item = mdmItemService.validateItemExistsAndEnable(reqVO.getItemId());
        if (reqVO.getBatchId() != null) {
            MesWmBatchDO batch = batchService.validateBatchExists(reqVO.getBatchId(), reqVO.getItemId());
            if (batch.getClientId() != null && ObjUtil.notEqual(batch.getClientId(), returnSales.getClientId())) {
                throw exception(WM_BATCH_CLIENT_MISMATCH);
            }
            reqVO.setBatchCode(batch.getCode());
            return;
        }
        if (!Boolean.TRUE.equals(item.getIsBatchManaged())) {
            return;
        }
        MesWmBatchGenerateReqVO batchReqVO = new MesWmBatchGenerateReqVO()
                .setItemId(reqVO.getItemId())
                .setClientId(returnSales.getClientId())
                .setSalesOrderCode(returnSales.getSalesOrderCode());
        MesWmBatchDO batch = batchService.getOrGenerateBatchCode(batchReqVO);
        if (batch == null) {
            throw exception(MD_ITEM_BATCH_REQUIRED);
        }
        // æ ¡éªŒæ‰¹æ¬¡å­˜åœ¨ä¸”属于当前物料和客户
        if (batchId != null) {
            // ä»Žçˆ¶å•获取客户ID,校验批次归属
            MesWmReturnSalesDO returnSales = returnSalesService.validateReturnSalesExistsAndPrepare(returnId);
            batchService.validateBatchExists(batchId, itemId, returnSales.getClientId(), null);
        }
        reqVO.setBatchId(batch.getId()).setBatchCode(batch.getCode());
    }
    @Override