云
2 天以前 87733eb3623593da247c051593d64b8ff9e1d3a4
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/batch/MesProBatchServiceImpl.java
@@ -20,9 +20,7 @@
import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchWorkerMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper;
import cn.iocoder.yudao.module.mes.dal.redis.MesCodeRedisDAO;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProBatchStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.service.pro.line.MesProLineService;
import cn.iocoder.yudao.module.mes.service.pro.bagcode.MesProBagCodeService;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
@@ -35,12 +33,14 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@@ -55,6 +55,12 @@
@Validated
@Slf4j
public class MesProBatchServiceImpl implements MesProBatchService {
    /**
     * 批次号格式:4 位年份 + 3 位流水(如 2026001)
     */
    private static final Pattern BATCH_CODE_PATTERN = Pattern.compile("^[0-9]{7}$");
    private static final int BATCH_CODE_MAX_SERIAL = 999;
    @Resource
    private MesProBatchMapper batchMapper;
@@ -78,8 +84,6 @@
    private MesCodeRedisDAO codeRedisDao;
    @Resource
    private MesMdAutoCodeRecordService autoCodeRecordService;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private MesProLineService lineService;
@@ -94,8 +98,8 @@
        validateRelations(createReqVO);
        createReqVO.setItemId(resolveBindingItem(null, createReqVO));
        // 2. 生成批次号(编码规则:前缀 + 日期 + 产线编码 + 流水号)
        String code = generateBatchCode(line.getCode());
        // 2. 确定批次号(人工输入优先,未输入则按「年份+3位流水」自动生成)
        String code = resolveBatchCode(createReqVO.getCode(), null);
        // 3. 插入批次(产线编码/名称冗余回填;勾选绑定托盘时批次直接进入生产中)
        boolean hasBindings = hasBindings(createReqVO);
@@ -152,11 +156,11 @@
        if (StrUtil.isNotEmpty(batch.getCode())) {
            throw exception(PRO_BATCH_ALREADY_FILLED);
        }
        // 2. 校验关联数据并生成正式批次号
        // 2. 校验关联数据并确定正式批次号(人工输入优先,未输入则自动生成)
        MesProLineDO line = lineService.validateLineExistsAndEnable(fillReqVO.getLineId());
        validateRelations(fillReqVO);
        fillReqVO.setItemId(resolveBindingItem(batch, fillReqVO));
        String code = generateBatchCode(line.getCode());
        String code = resolveBatchCode(fillReqVO.getCode(), null);
        // 3. 落库业务信息(保留临时批次码用于历史扫码追溯;产线编码/名称冗余回填;勾选绑定托盘时批次直接进入生产中)
        boolean hasBindings = hasBindings(fillReqVO);
        MesProBatchDO updateObj = BeanUtils.toBean(fillReqVO, MesProBatchDO.class)
@@ -187,17 +191,14 @@
        validateRelations(updateReqVO);
        updateReqVO.setItemId(resolveBindingItem(oldBatch, updateReqVO));
        // 3. 未生成正式批次号或产线变化时重新生成批次号(批次号包含产线编码要素)
        // 3. 批次号仅含年份与流水号,生成后不再随产线变化;未生成正式批次号时补生成
        boolean codeEmpty = StrUtil.isEmpty(oldBatch.getCode());
        boolean lineChanged = !Objects.equals(oldBatch.getLineId(), updateReqVO.getLineId());
        String code = oldBatch.getCode();
        String code = codeEmpty ? resolveBatchCode(updateReqVO.getCode(), oldBatch.getId()) : oldBatch.getCode();
        MesProBatchDO updateObj = BeanUtils.toBean(updateReqVO, MesProBatchDO.class);
        if (codeEmpty || lineChanged) {
            // 变更产线时,目标产线必须启用;未变更则仅校验存在(避免停用产线的历史批次无法编辑其他字段)
            MesProLineDO line = (lineChanged || codeEmpty)
                    ? lineService.validateLineExistsAndEnable(updateReqVO.getLineId())
                    : lineService.validateLineExists(updateReqVO.getLineId());
            code = generateBatchCode(line.getCode());
        if (lineChanged || codeEmpty) {
            // 变更产线时,目标产线必须启用
            MesProLineDO line = lineService.validateLineExistsAndEnable(updateReqVO.getLineId());
            updateObj.setLineCode(line.getCode()).setLineName(line.getName());
        }
        // 4. 更新批次(勾选绑定托盘时批次直接进入生产中;取消勾选不回退,已绑定托盘不可解绑)
@@ -448,16 +449,50 @@
    }
    /**
     * 生成批次号
     * <p>
     * 编码规则:固定前缀(PC) + 日期(yyyyMMdd) + 输入字符(产线编码) + 流水号(按日循环)
     * 确定批次号:人工输入优先,未输入则按「当前年份 + 3 位流水」自动生成
     *
     * @param lineCode 产线编码
     * @return 批次号
     * @param requestedCode 人工输入的批次号,可为空
     * @param selfId        当前批次编号(更新时排除自身,新增传 null)
     * @return 最终批次号
     */
    private String generateBatchCode(String lineCode) {
        String inputChar = StrUtil.emptyToDefault(lineCode, "");
        return autoCodeRecordService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.PRO_BATCH_CODE.getCode(), inputChar);
    private String resolveBatchCode(String requestedCode, Long selfId) {
        if (StrUtil.isBlank(requestedCode)) {
            return generateNextBatchCode();
        }
        String code = StrUtil.trim(requestedCode);
        validateBatchCode(code, selfId);
        return code;
    }
    /**
     * 校验批次号格式(4 位年份 + 3 位流水,如 2026001)及唯一性
     */
    private void validateBatchCode(String code, Long selfId) {
        if (!BATCH_CODE_PATTERN.matcher(code).matches()) {
            throw exception(PRO_BATCH_CODE_INVALID);
        }
        int year = Integer.parseInt(code.substring(0, 4));
        int serial = Integer.parseInt(code.substring(4));
        if (year < 2000 || year > 2100 || serial < 1) {
            throw exception(PRO_BATCH_CODE_INVALID);
        }
        MesProBatchDO existing = batchMapper.selectByCodeIgnoreDeleted(code);
        if (existing != null && !Objects.equals(existing.getId(), selfId)) {
            throw exception(PRO_BATCH_CODE_DUPLICATE, code);
        }
    }
    /**
     * 生成当前年份的下一个批次号
     */
    private String generateNextBatchCode() {
        String year = String.valueOf(LocalDate.now().getYear());
        Integer maxSerial = batchMapper.selectMaxCodeSerialByYear(year);
        int serial = codeRedisDao.allocateBatchNo(year, maxSerial);
        if (serial > BATCH_CODE_MAX_SERIAL) {
            throw exception(PRO_BATCH_CODE_EXHAUSTED);
        }
        return year + String.format("%03d", serial);
    }
    /**