云
11 小时以前 02a1ae81eba7b48223052fb833859f0a65dd7d21
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
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("生成任务已取消");
        }
 
    }
 
}