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;
|
|
/**
|
* 袋码批量生成执行器(同步与异步共用)
|
* <p>
|
* 与旧实现的关键差异:
|
* <ul>
|
* <li>编码批量向 Redis 申请,避免每个袋码两次网络往返;</li>
|
* <li>袋序号一次性分配,分段只做偏移计算;</li>
|
* <li>每段独立事务落库,并在段间回写进度、检查取消与超时。</li>
|
* </ul>
|
*/
|
@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<String> codes = codeRedisDao.generateBatch(size * 2);
|
List<MesProBagCodeDO> 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<Long> ids = new ArrayList<>(spec.palletCount());
|
List<String> 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<String> codes = codeRedisDao.generateBatch(size * 2);
|
List<MesProPalletDO> 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<Long> ids, List<String> 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<Long> palletIds) {
|
}
|
|
/**
|
* 取消信号:生成线程检测到取消时抛出,由调用方统一清理半成品
|
*/
|
public static class CancelledException extends RuntimeException {
|
|
public CancelledException() {
|
super("生成任务已取消");
|
}
|
|
}
|
|
}
|