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;
/**
* 袋码批量生成执行器(同步与异步共用)
*
* 与旧实现的关键差异:
*
* - 编码批量向 Redis 申请,避免每个袋码两次网络往返;
* - 袋序号一次性分配,分段只做偏移计算;
* - 每段独立事务落库,并在段间回写进度、检查取消与超时。
*
*/
@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("生成任务已取消");
}
}
}