自动化立体仓库 - WMS系统
zwl
17 小时以前 ff66ddf96807fac02e01c7d2ecdfd1ba808af9c5
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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
package com.zy.asrs.task;
 
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.core.common.Cools;
import com.core.common.R;
import com.zy.api.controller.params.WorkTaskParams;
import com.zy.api.service.WcsApiService;
import com.zy.asrs.entity.WrkMast;
import com.zy.asrs.service.WrkMastService;
import com.zy.asrs.task.core.ReturnT;
import com.zy.asrs.task.handler.WorkMastHandler;
import com.zy.asrs.task.support.OutboundBatchSeqReleaseGuard;
import com.zy.asrs.task.support.WorkPublishLockKeys;
import com.zy.asrs.utils.Utils;
import com.zy.common.utils.RedisUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
 
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Date;
import java.util.Map;
import java.util.List;
import java.util.Objects;
 
/**
 * Created by vincent on 2020/7/7
 */
@Component
public class WorkMastScheduler {
 
    private static final Logger log = LoggerFactory.getLogger(WorkMastScheduler.class);
    private static final int MAX_PUBLISH_TASKS_ONCE = 20;
    private static final long OUTBOUND_USER_NO_LOCK_SECONDS = 60L;
 
    @Autowired
    private WcsApiService wcsApiService;
    @Autowired
    private WrkMastService wrkMastService;
    @Autowired
    private WorkMastHandler workMastHandler;
    @Autowired
    private OutboundBatchSeqReleaseGuard outboundBatchSeqReleaseGuard;
    @Autowired
    private RedisUtil redisUtil;
 
    @Scheduled(cron = "0/3 * * * * ? ")
    private void execute(){
        List<WrkMast> wrkMasts = wrkMastService.selectToBeCompleteData();
        if (wrkMasts.isEmpty()) {
            return;
        }
        for (WrkMast wrkMast : wrkMasts) {
            ReturnT<String> returnT = workMastHandler.start(wrkMast);
            if (!returnT.isSuccess()) {
                wrkMast.setUpdMk("X");
                wrkMast.setErrorMemo(returnT.getMsg());
                wrkMast.setErrorTime(new Date());
                if (!wrkMastService.updateById(wrkMast)) {
                    log.error("工作档[workNo={}]标记待处理失败", wrkMast.getWrkNo());
                }
            }
        }
    }
 
    /**
     * 任务自动下发。
     * <p>
     * 调度器只负责从工作档中挑出“当前允许下发”的任务,并将其转换成 WCS 接口需要的报文结构;
     * 出库任务按 userNo -> batchSeq 分层汇总后串行下发,确保同一 userNo 下前一个 batchSeq 完成后再发下一个。
     * <p>
     * 当前批量下发的归并维度是:
     * 1. WCS接口路径(入库/出库/移库不能混发);
     * 2. 出库任务按 work_mast.user_no -> work_mast.batch_seq 分层汇总,并按 batchSeq 自然升序下发。
     *
     * @author Ryan
     * @date 2026/1/10 14:42
     */
    @Scheduled(cron = "0/10 * * * * ? ")
    private synchronized void autoPubTasks() {
        // 仅处理待下发/已生成下发号的工作档。
        List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<WrkMast>().in("wrk_sts", Arrays.asList(1L, 11L))
                .orderBy("user_no", true)
                .orderBy("batch_seq", true)
                .orderBy("plt_type", true));
        if (wrkMasts.isEmpty()) {
            return;
        }
 
        List<WorkTaskParams> paramsList = new ArrayList<>();
        Map<String, LinkedHashMap<String, List<WorkTaskParams>>> outboundTasksByUserNo = new LinkedHashMap<>();
        for (WrkMast wrkMast : wrkMasts) {
            // 出库类任务(ioType > 100)默认需要 ERP 确认;未确认的任务在这里直接跳过。
            if (wrkMast.getIoType() > 100 && !"Y".equalsIgnoreCase(wrkMast.getPdcType())) {
                continue;
            }
 
            if (isOutboundPublishTask(wrkMast)) {
                if (Cools.isEmpty(wrkMast.getBatchSeq())) {
                    log.warn("出库进仓编号(batchSeq)为空,跳过下发, wrkNo={}, userNo={}",
                            wrkMast.getWrkNo(), wrkMast.getUserNo());
                    continue;
                }
                WorkTaskParams params = buildWorkTaskParams(wrkMast);
                String userNo = normalizeGroupKey(wrkMast.getUserNo());
                String batchSeq = normalizeGroupKey(wrkMast.getBatchSeq());
                outboundTasksByUserNo
                        .computeIfAbsent(userNo, key -> new LinkedHashMap<>())
                        .computeIfAbsent(batchSeq, key -> new ArrayList<>())
                        .add(params);
            } else {
                paramsList.add(buildWorkTaskParams(wrkMast));
            }
        }
 
        if (publishTaskChunks(paramsList)) {
            return;
        }
 
        if (outboundTasksByUserNo.isEmpty()) {
            return;
        }
 
        for (Map.Entry<String, LinkedHashMap<String, List<WorkTaskParams>>> userEntry : outboundTasksByUserNo.entrySet()) {
            String userNo = userEntry.getKey();
            List<String> batchSeqs = new ArrayList<>(userEntry.getValue().keySet());
            batchSeqs.sort(this::compareBatchSeqNatural);
 
            for (String batchSeq : batchSeqs) {
                String blockMsg = outboundBatchSeqReleaseGuard.validateReady(userNo, batchSeq);
                if (!Cools.isEmpty(blockMsg)) {
                    log.info(blockMsg);
                    break;
                }
 
                List<WorkTaskParams> batchParams = userEntry.getValue().get(batchSeq);
                if (batchParams == null || batchParams.isEmpty()) {
                    continue;
                }
 
                if (publishOutboundTaskChunks(userNo, batchSeq, batchParams)) {
                    return;
                }
            }
        }
    }
 
    private WorkTaskParams buildWorkTaskParams(WrkMast wrkMast) {
        // WMS 库位编码转换成 WCS 可识别的库位编码。
        String wcsSourceLocNo = Cools.isEmpty(wrkMast.getSourceLocNo()) ? "" : Utils.WMSLocToWCSLoc(wrkMast.getSourceLocNo());
        String wcsLocNo = Cools.isEmpty(wrkMast.getLocNo()) ? "" : Utils.WMSLocToWCSLoc(wrkMast.getLocNo());
        WorkTaskParams params = new WorkTaskParams();
 
        // 101: 出库。有序任务才向 WCS 传 batch/batchSeq;seq=0 表示无序,不传这两个字段。
        if (wrkMast.getIoType() == 101) {
            params.setType("out")
                    .setTaskNo(wrkMast.getWrkNo() + "")
                    .setLocNo(wcsSourceLocNo)
                    .setStaNo(String.valueOf(wrkMast.getStaNo()))
                    .setTaskPri(wrkMast.getIoPri().intValue())
                    .setBarcode(wrkMast.getBarcode());
            if (wrkMast.getPltType() != null && wrkMast.getPltType() > 0) {
                params.setBatch(wrkMast.getBatchSeq())
                        .setBatchSeq(wrkMast.getPltType());
            }
        // 2: 入库。入库接口使用 sourceStaNo + 目标库位。
        } else if (wrkMast.getIoType() == 2 && !Cools.isEmpty(wrkMast.getSourceStaNo())) {
            params.setType("in")
                    .setTaskNo(wrkMast.getWrkNo() + "")
                    .setSourceStaNo(String.valueOf(wrkMast.getSourceStaNo()))
                    .setLocNo(wcsLocNo)
                    .setTaskPri(wrkMast.getIoPri().intValue())
                    .setBarcode(wrkMast.getBarcode());
        // 其余走移库接口,源库位和目标库位都需要带给 WCS。
        } else {
            params.setType("move")
                    .setTaskNo(wrkMast.getWrkNo() + "")
                    .setSourceLocNo(wcsSourceLocNo)
                    .setLocNo(wcsLocNo)
                    .setBarcode(wrkMast.getBarcode());
        }
        return params;
    }
 
    private boolean isOutboundPublishTask(WrkMast wrkMast) {
        return wrkMast != null && Objects.equals(wrkMast.getIoType(), 101);
    }
 
    private int compareBatchSeqNatural(String left, String right) {
        String safeLeft = Cools.isEmpty(left) ? "" : left;
        String safeRight = Cools.isEmpty(right) ? "" : right;
        int leftIndex = 0;
        int rightIndex = 0;
        while (leftIndex < safeLeft.length() && rightIndex < safeRight.length()) {
            char leftChar = safeLeft.charAt(leftIndex);
            char rightChar = safeRight.charAt(rightIndex);
            if (Character.isDigit(leftChar) && Character.isDigit(rightChar)) {
                int leftStart = leftIndex;
                int rightStart = rightIndex;
                while (leftIndex < safeLeft.length() && Character.isDigit(safeLeft.charAt(leftIndex))) {
                    leftIndex++;
                }
                while (rightIndex < safeRight.length() && Character.isDigit(safeRight.charAt(rightIndex))) {
                    rightIndex++;
                }
                String leftNumber = safeLeft.substring(leftStart, leftIndex);
                String rightNumber = safeRight.substring(rightStart, rightIndex);
                int compare = new BigInteger(leftNumber).compareTo(new BigInteger(rightNumber));
                if (compare != 0) {
                    return compare;
                }
                compare = Integer.compare(leftNumber.length(), rightNumber.length());
                if (compare != 0) {
                    return compare;
                }
                continue;
            }
            int compare = Character.compare(leftChar, rightChar);
            if (compare != 0) {
                return compare;
            }
            leftIndex++;
            rightIndex++;
        }
        return Integer.compare(safeLeft.length(), safeRight.length());
    }
 
    private String normalizeGroupKey(String value) {
        return Cools.isEmpty(value) ? "" : value;
    }
 
    private boolean publishTaskChunks(List<WorkTaskParams> paramsList) {
        if (paramsList == null || paramsList.isEmpty()) {
            return false;
        }
        for (int start = 0; start < paramsList.size(); start += MAX_PUBLISH_TASKS_ONCE) {
            int end = Math.min(start + MAX_PUBLISH_TASKS_ONCE, paramsList.size());
            List<WorkTaskParams> chunk = paramsList.subList(start, end);
            R r = wcsApiService.pubWrksToWcs(chunk);
            if (isWcsSuccess(r)) {
                return true;
            }
            log.warn("批量下发任务到WCS失败, start={}, size={}, result={}", start, chunk.size(), r);
        }
        return false;
    }
 
    private boolean publishOutboundTaskChunks(String userNo, String batchSeq, List<WorkTaskParams> batchParams) {
        if (batchParams == null || batchParams.isEmpty()) {
            return false;
        }
        for (int start = 0; start < batchParams.size(); start += MAX_PUBLISH_TASKS_ONCE) {
            int end = Math.min(start + MAX_PUBLISH_TASKS_ONCE, batchParams.size());
            List<WorkTaskParams> chunk = batchParams.subList(start, end);
            String lockKey = WorkPublishLockKeys.outboundUserNoLock(userNo);
            String lockValue = String.valueOf(System.currentTimeMillis());
            if (!redisUtil.setIfAbsent(lockKey, lockValue, OUTBOUND_USER_NO_LOCK_SECONDS)) {
                log.info("出库任务正在下发,跳过本轮, userNo={}, batchSeq={}, lockKey={}", userNo, batchSeq, lockKey);
                return false;
            }
            try {
                R r = wcsApiService.pubWrksToWcs(chunk);
                if (isWcsSuccess(r)) {
                    return true;
                }
                log.warn("批量下发出库任务到WCS失败, userNo={}, batchSeq={}, start={}, size={}, result={}",
                        userNo, batchSeq, start, chunk.size(), r);
            } finally {
                Object currentLockValue = redisUtil.get(lockKey);
                if (Objects.equals(currentLockValue, lockValue)) {
                    redisUtil.del(lockKey);
                }
            }
        }
        return false;
    }
 
    private boolean isWcsSuccess(R r) {
        return r != null && Objects.equals(r.get("code"), 200);
    }
 
}