云
21 小时以前 704265308b1227dbada5a36b2027b588c062ded2
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/batch/MesProBatchServiceImpl.java
@@ -7,7 +7,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchPreGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.pallet.vo.MesProPalletBindBatchReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchWorkerDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.line.MesProLineDO;
@@ -15,11 +15,13 @@
import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper;
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.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.pro.line.MesProLineService;
import cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletService;
import cn.iocoder.yudao.module.mes.service.trace.integrity.MesTraceIntegrityService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@@ -58,49 +60,62 @@
    private MesProPalletMapper palletMapper;
    @Resource
    private MesProPalletService palletService;
    @Resource
    private MesCodeRedisDAO codeRedisDao;
    @Resource
    private MesMdAutoCodeRecordService autoCodeRecordService;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private MesMdmItemService mdmItemService;
    @Resource
    private MesProLineService lineService;
    @Resource
    private MesTraceIntegrityService integrityService;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createBatch(MesProBatchSaveReqVO createReqVO) {
        // 1. 校验关联数据(品种、产线、生产人员)
        MesMdmItemDO item = mdmItemService.validateItemExists(createReqVO.getItemId());
        // 1. 校验关联数据(产线、生产人员)
        MesProLineDO line = lineService.validateLineExistsAndEnable(createReqVO.getLineId());
        validateRelations(createReqVO);
        createReqVO.setItemId(resolveBindingItem(null, createReqVO));
        // 2. 生成批次号(编码规则:前缀 + 日期 + 产线编码 + 品种编码 + 流水号)
        String code = generateBatchCode(line.getCode(), item.getCode());
        // 2. 生成批次号(编码规则:前缀 + 日期 + 产线编码 + 流水号)
        String code = generateBatchCode(line.getCode());
        // 3. 插入批次(产线编码/名称冗余回填)
        // 3. 插入批次(产线编码/名称冗余回填;勾选绑定托盘时批次直接进入生产中)
        boolean bindPallets = CollUtil.isNotEmpty(createReqVO.getPalletIds());
        MesProBatchDO batch = BeanUtils.toBean(createReqVO, MesProBatchDO.class)
                .setCode(code)
                .setBusinessCode(codeRedisDao.generate())
                .setLineCode(line.getCode())
                .setLineName(line.getName())
                .setBagQuantity(0)
                .setPalletQuantity(0)
                .setStatus(MesProBatchStatusEnum.DRAFT.getStatus());
                .setStatus(bindPallets ? MesProBatchStatusEnum.PRODUCING.getStatus()
                        : MesProBatchStatusEnum.DRAFT.getStatus());
        batchMapper.insert(batch);
        // 4. 插入生产人员配置
        insertBatchWorkers(batch.getId(), createReqVO.getWorkers());
        // 5. 绑定托盘(级联生成正式托盘码并录入托盘下袋码,失败整体回滚)
        bindPalletsToBatch(batch.getId(), createReqVO.getPalletIds());
        // 6. 生成批次完整性摘要
        integrityService.recordBatchIntegrity(batch.getId());
        return batch.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public List<MesProBatchDO> preGenerateBatch(MesProBatchPreGenerateReqVO reqVO) {
        // 1. 生成临时批次码(全局 8 位序号,格式 PB-00000001)
        int startSeq = batchMapper.selectMaxTempCodeSeq() + 1;
        // 1. 通过统一编码生成器分配临时码及业务码,不再解析历史临时码序号。
        List<MesProBatchDO> list = new ArrayList<>(reqVO.getCount());
        for (int i = 0; i < reqVO.getCount(); i++) {
            list.add(new MesProBatchDO()
                    .setTempCode("PB-" + StrUtil.padPre(String.valueOf(startSeq + i), 8, '0'))
                    .setTempCode(codeRedisDao.generate())
                    .setBusinessCode(codeRedisDao.generate())
                    .setBagQuantity(0)
                    .setPalletQuantity(0)
                    .setStatus(MesProBatchStatusEnum.DRAFT.getStatus())
@@ -125,53 +140,67 @@
            throw exception(PRO_BATCH_ALREADY_FILLED);
        }
        // 2. 校验关联数据并生成正式批次号
        MesMdmItemDO item = mdmItemService.validateItemExists(fillReqVO.getItemId());
        MesProLineDO line = lineService.validateLineExistsAndEnable(fillReqVO.getLineId());
        validateRelations(fillReqVO);
        String code = generateBatchCode(line.getCode(), item.getCode());
        // 3. 落库业务信息(保留临时批次码用于历史扫码追溯;产线编码/名称冗余回填)
        fillReqVO.setItemId(resolveBindingItem(batch, fillReqVO));
        String code = generateBatchCode(line.getCode());
        // 3. 落库业务信息(保留临时批次码用于历史扫码追溯;产线编码/名称冗余回填;勾选绑定托盘时批次直接进入生产中)
        boolean bindPallets = CollUtil.isNotEmpty(fillReqVO.getPalletIds());
        MesProBatchDO updateObj = BeanUtils.toBean(fillReqVO, MesProBatchDO.class)
                .setCode(code)
                .setLineCode(line.getCode())
                .setLineName(line.getName())
                .setStatus(MesProBatchStatusEnum.DRAFT.getStatus());
                .setStatus(bindPallets ? MesProBatchStatusEnum.PRODUCING.getStatus()
                        : MesProBatchStatusEnum.DRAFT.getStatus());
        batchMapper.updateById(updateObj);
        // 4. 重建生产人员配置
        batchWorkerMapper.deleteByBatchId(fillReqVO.getId());
        insertBatchWorkers(fillReqVO.getId(), fillReqVO.getWorkers());
        // 5. 绑定托盘(级联生成正式托盘码并录入托盘下袋码,失败整体回滚)
        bindPalletsToBatch(fillReqVO.getId(), fillReqVO.getPalletIds());
        // 6. 生成批次完整性摘要(补录正式信息后覆盖)
        integrityService.recordBatchIntegrity(fillReqVO.getId());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateBatch(MesProBatchSaveReqVO updateReqVO) {
        // 1. 校验存在 + 状态(草稿/生产中才允许修改)
        MesProBatchDO oldBatch = validateBatchExists(updateReqVO.getId());
        MesProBatchDO oldBatch = batchMapper.selectByIdForUpdate(updateReqVO.getId());
        if (oldBatch == null) throw exception(PRO_BATCH_NOT_EXISTS);
        validateBatchEditable(oldBatch);
        // 2. 校验关联数据(生产人员)
        validateRelations(updateReqVO);
        updateReqVO.setItemId(resolveBindingItem(oldBatch, updateReqVO));
        // 3. 未生成正式批次号或品种/产线变化时重新生成批次号(批次号包含产线编码 + 品种编码要素)
        // 3. 未生成正式批次号或产线变化时重新生成批次号(批次号包含产线编码要素)
        boolean codeEmpty = StrUtil.isEmpty(oldBatch.getCode());
        boolean itemChanged = !Objects.equals(oldBatch.getItemId(), updateReqVO.getItemId());
        boolean lineChanged = !Objects.equals(oldBatch.getLineId(), updateReqVO.getLineId());
        String code = oldBatch.getCode();
        MesProBatchDO updateObj = BeanUtils.toBean(updateReqVO, MesProBatchDO.class);
        if (codeEmpty || itemChanged || lineChanged) {
            MesMdmItemDO item = mdmItemService.validateItemExists(updateReqVO.getItemId());
        if (codeEmpty || lineChanged) {
            // 变更产线时,目标产线必须启用;未变更则仅校验存在(避免停用产线的历史批次无法编辑其他字段)
            MesProLineDO line = (lineChanged || codeEmpty)
                    ? lineService.validateLineExistsAndEnable(updateReqVO.getLineId())
                    : lineService.validateLineExists(updateReqVO.getLineId());
            code = generateBatchCode(line.getCode(), item.getCode());
            code = generateBatchCode(line.getCode());
            updateObj.setLineCode(line.getCode()).setLineName(line.getName());
        }
        // 4. 更新批次
        // 4. 更新批次(勾选绑定托盘时批次直接进入生产中;取消勾选不回退,已绑定托盘不可解绑)
        boolean bindPallets = CollUtil.isNotEmpty(updateReqVO.getPalletIds());
        updateObj.setCode(code);
        if (bindPallets) {
            updateObj.setStatus(MesProBatchStatusEnum.PRODUCING.getStatus());
        }
        batchMapper.updateById(updateObj);
        // 5. 重建生产人员配置
        batchWorkerMapper.deleteByBatchId(updateReqVO.getId());
        insertBatchWorkers(updateReqVO.getId(), updateReqVO.getWorkers());
        // 6. 绑定新增勾选的托盘(级联生成正式托盘码并录入托盘下袋码,失败整体回滚)
        bindPalletsToBatch(updateReqVO.getId(), updateReqVO.getPalletIds());
        // 7. 摘要覆盖范围含品种/产线/生产日期/计划数量,编辑后重新生成避免误报篡改
        integrityService.recordBatchIntegrity(updateReqVO.getId());
    }
    @Override
@@ -226,6 +255,33 @@
    }
    @Override
    public MesProBatchDO getBatchByScanCode(String code) {
        return batchMapper.selectByScanCode(code);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int generateBusinessCodes(java.util.List<Long> ids) {
        if (ids == null || ids.isEmpty() || ids.size() > 200 || ids.stream().anyMatch(java.util.Objects::isNull))
            throw new cn.iocoder.yudao.framework.common.exception.ServiceException(400, "请选择 1 至 200 条记录");
        java.util.List<MesProBatchDO> records = new java.util.ArrayList<>();
        for (Long id : ids.stream().distinct().sorted().toList()) {
            MesProBatchDO record = batchMapper.selectByIdForUpdate(id);
            if (record == null) throw exception(PRO_BATCH_NOT_EXISTS);
            if (MesProBatchStatusEnum.CANCELED.getStatus().equals(record.getStatus()))
                throw new cn.iocoder.yudao.framework.common.exception.ServiceException(400, "已作废记录不可补生成业务码");
            records.add(record);
        }
        int count = 0;
        for (MesProBatchDO record : records) {
            if (StrUtil.isNotBlank(record.getBusinessCode())) continue;
            batchMapper.updateById(new MesProBatchDO().setId(record.getId()).setBusinessCode(codeRedisDao.generate()));
            count++;
        }
        return count;
    }
    @Override
    public MesProBatchDO getBatchByCode(String code) {
        MesProBatchDO batch = batchMapper.selectByCode(code);
        if (batch == null) {
@@ -274,7 +330,7 @@
    /**
     * 校验关联数据(生产人员:责任人 + 生产人员配置)
     * <p>
     * 品种、产线的校验在创建/更新流程中内联执行(需取编码参与批次号生成)
     * 产线的校验在创建/更新流程中内联执行(需取编码参与批次号生成)
     */
    private void validateRelations(MesProBatchSaveReqVO reqVO) {
        Set<Long> userIds = new HashSet<>();
@@ -284,16 +340,51 @@
    }
    /**
     * 批次绑定托盘(可选):同事务内复用托盘绑定批次逻辑,
     * 生成正式托盘码并级联录入托盘下未绑批次的袋码,失败整体回滚
     */
    private Long resolveBindingItem(MesProBatchDO batch, MesProBatchSaveReqVO req) {
        Long itemId = batch == null ? null : batch.getItemId();
        if (batch != null) {
            for (Long existing : bagCodeMapper.selectDistinctItemIdsByBatchId(batch.getId()))
                itemId = cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletItemResolver.merge(itemId, existing);
            for (var pallet : palletMapper.selectListByBatchId(batch.getId()))
                itemId = cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletItemResolver.merge(itemId,
                        cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletItemResolver.resolve(pallet, bagCodeMapper.selectListByPalletId(pallet.getId())));
        }
        if (CollUtil.isNotEmpty(req.getPalletIds())) for (Long id : req.getPalletIds().stream().distinct().sorted().toList()) {
            var pallet = palletMapper.selectByIdForUpdate(id);
            if (pallet == null) throw exception(PRO_PALLET_NOT_EXISTS);
            Long candidate = cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletItemResolver.resolve(pallet, bagCodeMapper.selectListByPalletId(id));
            if (candidate == null) throw new cn.iocoder.yudao.framework.common.exception.ServiceException(400, "托盘缺少品种,请先绑定同品种袋码");
            itemId = cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletItemResolver.merge(itemId, candidate);
        }
        // 请求里的品种只能与已绑定/待绑定托盘一致,不能通过编辑改变批次品种。
        if (req.getItemId() != null && !Objects.equals(req.getItemId(), itemId))
            throw new cn.iocoder.yudao.framework.common.exception.ServiceException(400, "品种由绑定托盘带入,不可修改为其他品种");
        return itemId;
    }
    private void bindPalletsToBatch(Long batchId, List<Long> palletIds) {
        if (CollUtil.isEmpty(palletIds)) {
            return;
        }
        MesProPalletBindBatchReqVO bindReqVO = new MesProPalletBindBatchReqVO();
        bindReqVO.setBatchId(batchId);
        bindReqVO.setIds(palletIds);
        palletService.bindBatch(bindReqVO);
    }
    /**
     * 生成批次号
     * <p>
     * 编码规则:固定前缀(PC) + 日期(yyyyMMdd) + 输入字符(产线编码 + 品种编码) + 流水号(按日循环)
     * 编码规则:固定前缀(PC) + 日期(yyyyMMdd) + 输入字符(产线编码) + 流水号(按日循环)
     *
     * @param lineCode 产线编码
     * @param itemCode 品种(物料)编码
     * @return 批次号
     */
    private String generateBatchCode(String lineCode, String itemCode) {
        String inputChar = StrUtil.emptyToDefault(lineCode, "") + StrUtil.emptyToDefault(itemCode, "");
    private String generateBatchCode(String lineCode) {
        String inputChar = StrUtil.emptyToDefault(lineCode, "");
        return autoCodeRecordService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.PRO_BATCH_CODE.getCode(), inputChar);
    }