package cn.iocoder.yudao.module.mes.service.pro.bagcode; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO; import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeGenerateTaskMapper; import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper; import cn.iocoder.yudao.module.mes.dal.redis.MesCodeRedisDAO; import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeGenerateBizTypeEnum; import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeStatusEnum; import cn.iocoder.yudao.module.mes.enums.pro.MesProPalletStatusEnum; import cn.iocoder.yudao.module.mes.framework.bagcode.MesProBagCodeGenerateProperties; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; /** * 袋码批量生成执行器(同步与异步共用) *

* 与旧实现的关键差异: *

*/ @Slf4j @Component public class MesProBagCodeBatchGenerator { @Resource private MesProBagCodeMapper bagCodeMapper; @Resource private MesProBagCodeGenerateTaskMapper taskMapper; @Resource private MesCodeRedisDAO codeRedisDao; @Resource private MesProBagCodeChunkWriter chunkWriter; @Resource private MesProBagCodeGenerateProperties properties; /** * 执行生成 * * @param spec 生成参数 * @param taskId 任务编号;为 null 表示同步生成,不回写进度 * @param cancelFlag 取消标记 * @return 生成结果(袋码数 + 托盘编号) */ public GenerateResult generate(MesProBagCodeGenerateSpec spec, Long taskId, AtomicBoolean cancelFlag) { assertNotCancelled(cancelFlag); long deadline = System.currentTimeMillis() + Duration.ofMinutes(Math.max(1, properties.getMaxRuntimeMinutes())).toMillis(); // 1. 全局袋序号一次性分配:后续分段只做偏移,避免每段都查库内最大值 int startNo = codeRedisDao.allocateBagNoStart(spec.totalCount(), bagCodeMapper.selectMaxBagNo()); // 2. 托盘(仅数据包模式且按 N 袋换算时生成) PalletRefs palletRefs = createPallets(spec, cancelFlag, deadline); // 3. 袋码分段落库 int chunkSize = Math.max(1, properties.getChunkSize()); int finished = 0; while (finished < spec.totalCount()) { assertNotCancelled(cancelFlag); assertNotTimeout(deadline); int size = Math.min(chunkSize, spec.totalCount() - finished); // 每个袋码需要追溯码 + 业务码 List codes = codeRedisDao.generateBatch(size * 2); List chunk = new ArrayList<>(size); for (int i = 0; i < size; i++) { int index = finished + i; PalletRef pallet = palletRefs.resolve(spec, index); chunk.add(new MesProBagCodeDO() .setUuid(spec.uuid()) .setCode(codes.get(i * 2)) .setBusinessCode(codes.get(i * 2 + 1)) .setBagNo(startNo + index) .setItemId(spec.itemId()) .setItemCode(spec.itemCode()) .setItemName(spec.itemName()) .setPalletId(pallet == null ? null : pallet.id()) .setPalletCode(pallet == null ? null : pallet.tempCode()) .setStatus(MesProBagCodeStatusEnum.GENERATED.getStatus()) .setRemark(spec.remark())); } chunkWriter.insertBagCodes(chunk); finished += size; if (taskId != null) { taskMapper.updateProgress(taskId, finished); } } return new GenerateResult(finished, palletRefs.ids()); } /** * 按需创建托盘:数据包模式下袋码按「每托 N 袋」均分绑定 */ private PalletRefs createPallets(MesProBagCodeGenerateSpec spec, AtomicBoolean cancelFlag, long deadline) { if (spec.bizType() != MesProBagCodeGenerateBizTypeEnum.PACKAGE || spec.palletCount() <= 0) { return PalletRefs.EMPTY; } int chunkSize = Math.max(1, properties.getChunkSize()); List ids = new ArrayList<>(spec.palletCount()); List tempCodes = new ArrayList<>(spec.palletCount()); int created = 0; while (created < spec.palletCount()) { assertNotCancelled(cancelFlag); assertNotTimeout(deadline); int size = Math.min(chunkSize, spec.palletCount() - created); // 每个托盘需要临时码 + 业务码 List codes = codeRedisDao.generateBatch(size * 2); List pallets = new ArrayList<>(size); for (int i = 0; i < size; i++) { pallets.add(new MesProPalletDO() .setTempCode(codes.get(i * 2)) .setBusinessCode(codes.get(i * 2 + 1)) .setBagCount(spec.perPallet()) .setAutoGenerated(true) .setItemId(spec.itemId()) .setGenerateUuid(spec.uuid()) .setStatus(MesProPalletStatusEnum.IN_USE.getStatus())); } chunkWriter.insertPallets(pallets); pallets.forEach(pallet -> { ids.add(pallet.getId()); tempCodes.add(pallet.getTempCode()); }); created += size; } return new PalletRefs(ids, tempCodes); } private void assertNotCancelled(AtomicBoolean cancelFlag) { if (cancelFlag != null && cancelFlag.get()) { throw new CancelledException(); } } private void assertNotTimeout(long deadline) { if (System.currentTimeMillis() > deadline) { throw new IllegalStateException("生成任务超过最长运行时间(" + properties.getMaxRuntimeMinutes() + " 分钟)"); } } /** * 托盘引用:只保留袋码需要回写的两个字段,避免在内存中堆积整表 DO */ private record PalletRefs(List ids, List tempCodes) { private static final PalletRefs EMPTY = new PalletRefs(List.of(), List.of()); private PalletRef resolve(MesProBagCodeGenerateSpec spec, int bagIndex) { if (ids.isEmpty() || spec.perPallet() <= 0) { return null; } int palletIndex = bagIndex / spec.perPallet(); return new PalletRef(ids.get(palletIndex), tempCodes.get(palletIndex)); } } private record PalletRef(Long id, String tempCode) { } /** * 生成结果:同步路径需要托盘编号用于响应,异步路径只关心完成数量 */ public record GenerateResult(int finishedCount, List palletIds) { } /** * 取消信号:生成线程检测到取消时抛出,由调用方统一清理半成品 */ public static class CancelledException extends RuntimeException { public CancelledException() { super("生成任务已取消"); } } }