自动化立体仓库 - WMS系统
cl
2 天以前 1a0bdd8df58435ec37e9d8345e67cd092902b5e4
序号控制
12个文件已修改
590 ■■■■ 已修改文件
src/main/java/com/zy/api/controller/WcsApiController.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/api/service/WcsApiService.java 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/api/service/impl/WcsApiServiceImpl.java 411 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/OpenController.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/mapper/WrkMastLogMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/mapper/WrkMastMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/WrkMastService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/OpenServiceImpl.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/WrkMastServiceImpl.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/task/WcsDeviceStatusScheduler.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/task/WorkMastScheduler.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/integration/iot/client/impl/PahoIotMqttClient.java 48 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/api/controller/WcsApiController.java
@@ -15,6 +15,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Objects;
@@ -48,10 +49,11 @@
//    @ManagerAuth
    @ApiOperation("设备执行状态回写-wcs任务完成回写")
    @PostMapping("/openapi/report")
    public R receviceTaskFromWcs(@RequestBody ReceviceTaskParams params) {
    public R receviceTaskFromWcs(@RequestBody ReceviceTaskParams params, HttpServletRequest request) {
        if (Objects.isNull(params)) {
            return R.error("参数不能为空!!");
        }
        request.setAttribute("cache", params);
        return wcsApiService.receviceTaskFromWcs(params);
    }
src/main/java/com/zy/api/service/WcsApiService.java
@@ -44,7 +44,14 @@
     * @date 2026/3/21 11:30
     * @return com.core.common.R
     */
    R syncDeviceStatusFromWcs();
    default R syncDeviceStatusFromWcs() {
        return syncDeviceStatusFromWcs(true);
    }
    /**
     * @param logOnFailure false 时不输出 ERROR 级失败日志(供定时任务降噪)
     */
    R syncDeviceStatusFromWcs(boolean logOnFailure);
    /**
     * batch pause out tasks
src/main/java/com/zy/api/service/impl/WcsApiServiceImpl.java
@@ -1,6 +1,7 @@
package com.zy.api.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.core.common.Cools;
@@ -38,10 +39,18 @@
    private static final String YES = "Y";
    private static final String NO = "N";
    /** 同一 WCS 路径、同一单号下一组下发的任务条数上限 */
    private static final int WCS_PUB_BATCH_SIZE = 20;
    /** 三方接口统计:本系统调用 WCS 的 namespace 约定 */
    private static final String NS_WMS_TO_WCS = "本系统请求WCS";
    @Autowired
    private LocMastService locMastService;
    @Autowired
    private WrkMastService wrkMastService;
    @Autowired
    private WrkMastLogService wrkMastLogService;
    @Autowired
    private WorkService workService;
    @Autowired
@@ -69,6 +78,8 @@
    private String stopOutTask;
    @Value("${wcs.address.getDeviceStatus:/openapi/getDeviceStatus}")
    private String getDeviceStatus;
    @Value("${wcs.address.queryTask:/openapi/queryTask}")
    private String queryTaskPath;
    @Value("${wcs.status-sync.method:GET}")
    private String deviceStatusMethod;
    @Autowired
@@ -77,6 +88,8 @@
    private BasDevpService basDevpService;
    @Autowired
    private BasCrnpService basCrnpService;
    @Autowired
    private ApiLogService apiLogService;
    /**
@@ -98,8 +111,11 @@
            return R.error(validateMsg);
        }
        String url = resolveTaskPath(params);
        String response;
        String requestJson = JSON.toJSONString(params);
        String response = null;
        R r = R.ok();
        Throwable wcsThrown = null;
        boolean wcsBizOk = false;
        try {
            log.info("下发搬运任务给wcs="+JSON.toJSONString(params));
            response = new HttpHandler.Builder()
@@ -107,12 +123,13 @@
                    .setPath(url)
                    .setHttps(wcs_address != null && wcs_address.startsWith("https://"))
                    .setTimeout(10, TimeUnit.SECONDS)
                    .setJson(JSON.toJSONString(params))
                    .setJson(requestJson)
                    .build()
                    .doPost();
            JSONObject jsonObject = JSON.parseObject(response);
            log.info("下发任务给wcs的返回值="+response);
            Integer code = jsonObject.getInteger("code");
            wcsBizOk = code != null && code == 200;
            if (code==200) {
                updateWrkMastAfterPublish(wrkMast);
@@ -121,7 +138,10 @@
                r =R.error();
            }
        } catch (IOException e) {
            wcsThrown = e;
            throw new RuntimeException(e);
        } finally {
            logWcsToApiLog(url, requestJson, response, wcsThrown, wcsBizOk);
        }
        return r;
    }
@@ -132,9 +152,8 @@
            return R.error("任务不能为空!!");
        }
        // 先一次性把本批任务对应的工作档捞出来,避免循环内重复查库。
        Map<String, WrkMast> wrkMastMap = getWrkMastMap(paramsList);
        Map<String, List<WorkTaskParams>> groupedTasks = new LinkedHashMap<>();
        List<WorkTaskParams> accepted = new ArrayList<>();
        List<String> skipMsgs = new ArrayList<>();
        for (WorkTaskParams params : paramsList) {
@@ -148,58 +167,64 @@
                skipMsgs.add(buildTaskMsg(params, validateMsg));
                continue;
            }
            // 分组主键 = 接口路径 + userNo。
            // 这样既能保证入库/出库/移库不会混发,也能保证相同 userNo 的任务会打包到同一次 WCS 请求中。
            String groupKey = buildBatchGroupKey(params, wrkMast);
            groupedTasks.computeIfAbsent(groupKey, key -> new ArrayList<>()).add(params);
            accepted.add(params);
        }
        if (groupedTasks.isEmpty()) {
        if (accepted.isEmpty()) {
            return R.error(skipMsgs.isEmpty() ? "无可下发任务" : skipMsgs.get(0)).add(skipMsgs);
        }
        accepted = filterOutboundByContiguousPlt(accepted, wrkMastMap, skipMsgs);
        if (accepted.isEmpty()) {
            return R.error(skipMsgs.isEmpty() ? "无可下发任务" : skipMsgs.get(0)).add(skipMsgs);
        }
        accepted.sort(pubWcsSortComparator(wrkMastMap));
        List<List<WorkTaskParams>> chunks = buildPubChunks(accepted, wrkMastMap);
        int successCount = 0;
        List<String> failMsgs = new ArrayList<>();
        for (List<WorkTaskParams> group : groupedTasks.values()) {
            if (group == null || group.isEmpty()) {
        List<WorkTaskParams> lastSentChunk = null;
        String skipGroupKey = null;
        for (List<WorkTaskParams> chunk : chunks) {
            if (chunk == null || chunk.isEmpty()) {
                continue;
            }
            WorkTaskParams head = chunk.get(0);
            WrkMast headMast = wrkMastMap.get(head.getTaskNo());
            String key = buildBatchGroupKey(head, headMast);
            if (skipGroupKey != null && skipGroupKey.equals(key)) {
                continue;
            }
            // 同一组内的任务类型一致,因此取第一条即可确定本组应该调用哪个 WCS 接口。
            String path = resolveTaskPath(group.get(0));
            Map<String, Object> payload = new HashMap<>();
            // WCS 批量下发报文统一使用 {"tasks":[...]} 结构。
            payload.put("taskList", buildTaskPayloads(group));
            String response = null;
            try {
                log.info("批量下发搬运任务给wcs={}", JSON.toJSONString(payload));
                response = new HttpHandler.Builder()
                        .setUri(wcs_address)
                        .setPath(path)
//                        .setHttps(wcs_address != null && wcs_address.startsWith("https://"))
                        .setTimeout(10, TimeUnit.SECONDS)
                        .setJson(JSON.toJSONString(payload))
                        .build()
                        .doPost();
                JSONObject jsonObject = JSON.parseObject(response == null ? "{}" : response);
                log.info("批量下发任务给wcs的返回值={}", response);
                Integer code = jsonObject.getInteger("code");
                if (code != null && code == 200) {
                    successCount += group.size();
                    // 只有整组下发成功,才回写本地工作档状态,避免 WMS/WCS 状态分叉。
                    for (WorkTaskParams params : group) {
                        updateWrkMastAfterPublish(wrkMastMap.get(params.getTaskNo()));
                    }
                } else {
                    String msg = jsonObject.getString("msg");
                    failMsgs.add("path=" + path + ", msg=" + (Cools.isEmpty(msg) ? "WCS下发任务失败" : msg));
                    log.error("批量下发任务给wcs失败, path:{}, request:{}, response:{}", path, JSON.toJSONString(payload), response);
                }
            } catch (IOException e) {
                failMsgs.add("path=" + path + ", msg=" + e.getMessage());
                log.error("批量下发任务给wcs异常, path:{}, request:{}, response:{}", path, JSON.toJSONString(payload), response, e);
            if (!outboundChunkPredecessorPltReady(chunk, wrkMastMap)) {
                skipGroupKey = key;
                continue;
            }
            if (lastSentChunk != null) {
                WorkTaskParams lastHead = lastSentChunk.get(0);
                String lastKey = buildBatchGroupKey(lastHead, wrkMastMap.get(lastHead.getTaskNo()));
                if (lastKey.equals(key)) {
                    if (!sameOrderNextChunkAllowed(lastSentChunk)) {
                        skipGroupKey = key;
                        continue;
                    }
                }
                if (!sleepOneMinuteBeforeNextChunk()) {
                    break;
                }
            }
            int ok = postWcsBatchChunk(chunk, wrkMastMap, failMsgs);
            if (ok <= 0) {
                skipGroupKey = key;
                continue;
            }
            successCount += ok;
            lastSentChunk = chunk;
        }
        Map<String, Object> result = new HashMap<>();
@@ -218,6 +243,268 @@
            return R.error(msg).add(result);
        }
        return R.ok(failMsgs.isEmpty() && skipMsgs.isEmpty() ? "操作成功" : "部分任务下发成功").add(result);
    }
    private int postWcsBatchChunk(List<WorkTaskParams> chunk, Map<String, WrkMast> wrkMastMap, List<String> failMsgs) {
        if (chunk == null || chunk.isEmpty()) {
            return 0;
        }
        String path = resolveTaskPath(chunk.get(0));
        Map<String, Object> payload = new HashMap<>();
        payload.put("taskList", buildTaskPayloads(chunk));
        String requestJson = JSON.toJSONString(payload);
        String response = null;
        Throwable wcsThrown = null;
        boolean wcsBizOk = false;
        try {
            log.info("批量下发搬运任务给wcs={}", requestJson);
            response = new HttpHandler.Builder()
                    .setUri(wcs_address)
                    .setPath(path)
                    .setTimeout(60, TimeUnit.SECONDS)
                    .setJson(requestJson)
                    .build()
                    .doPost();
            JSONObject jsonObject = JSON.parseObject(response == null ? "{}" : response);
            log.info("批量下发任务给wcs的返回值={}", response);
            Integer code = jsonObject.getInteger("code");
            wcsBizOk = code != null && code == 200;
            if (wcsBizOk) {
                for (WorkTaskParams params : chunk) {
                    updateWrkMastAfterPublish(wrkMastMap.get(params.getTaskNo()));
                }
                return chunk.size();
            }
            String msg = jsonObject.getString("msg");
            failMsgs.add("path=" + path + ", msg=" + (Cools.isEmpty(msg) ? "WCS下发任务失败" : msg));
            log.error("批量下发任务给wcs失败, path:{}, request:{}, response:{}", path, requestJson, response);
        } catch (IOException e) {
            wcsThrown = e;
            failMsgs.add("path=" + path + ", msg=" + e.getMessage());
            log.error("批量下发任务给wcs异常, path:{}, request:{}, response:{}", path, requestJson, response, e);
        } finally {
            logWcsToApiLog(path, requestJson, response, wcsThrown, wcsBizOk);
        }
        return 0;
    }
    /**
     * 出库:仅当单号、序号均有效时做跳号校验;单号空或序号无效仍下发。入库/移库不处理。
     */
    private List<WorkTaskParams> filterOutboundByContiguousPlt(List<WorkTaskParams> accepted, Map<String, WrkMast> wrkMastMap, List<String> skipMsgs) {
        Map<String, Integer> reachCache = new HashMap<>();
        List<WorkTaskParams> kept = new ArrayList<>();
        for (WorkTaskParams p : accepted) {
            if (!"out".equalsIgnoreCase(p.getType())) {
                kept.add(p);
                continue;
            }
            WrkMast w = wrkMastMap.get(p.getTaskNo());
            String userNo = sortUserNoForPub(p, w);
            Integer plt = sortPltForPub(p, w);
            if (Cools.isEmpty(userNo) || plt == null || plt <= 0) {
                kept.add(p);
                continue;
            }
            int maxReach = reachCache.computeIfAbsent(userNo, wrkMastService::outboundSeqMaxContiguousPlt);
            if (plt > maxReach) {
                skipMsgs.add(buildTaskMsg(p, "出库序号跳号,跳过"));
                continue;
            }
            kept.add(p);
        }
        return kept;
    }
    private List<List<WorkTaskParams>> buildPubChunks(List<WorkTaskParams> accepted, Map<String, WrkMast> wrkMastMap) {
        List<List<WorkTaskParams>> chunks = new ArrayList<>();
        int index = 0;
        while (index < accepted.size()) {
            WorkTaskParams head = accepted.get(index);
            WrkMast headMast = wrkMastMap.get(head.getTaskNo());
            String headGroupKey = buildBatchGroupKey(head, headMast);
            List<WorkTaskParams> chunk = new ArrayList<>();
            while (index < accepted.size() && chunk.size() < WCS_PUB_BATCH_SIZE) {
                WorkTaskParams cur = accepted.get(index);
                WrkMast curMast = wrkMastMap.get(cur.getTaskNo());
                if (!headGroupKey.equals(buildBatchGroupKey(cur, curMast))) {
                    break;
                }
                chunk.add(cur);
                index++;
            }
            chunks.add(chunk);
        }
        return chunks;
    }
    private boolean sleepOneMinuteBeforeNextChunk() {
        try {
            TimeUnit.MINUTES.sleep(1);
            return true;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.warn("批量下发WCS组间等待被中断", e);
            return false;
        }
    }
    /**
     * 同单下一组:优先 WCS queryTask;失败或无数据则主表已非 11 或已进历史表。
     */
    private boolean sameOrderNextChunkAllowed(List<WorkTaskParams> lastSentChunk) {
        if (lastSentChunk == null || lastSentChunk.isEmpty()) {
            return false;
        }
        if (!Boolean.parseBoolean(String.valueOf(switchValue))) {
            return true;
        }
        WorkTaskParams last = lastSentChunk.get(lastSentChunk.size() - 1);
        if (last != null && !Cools.isEmpty(last.getTaskNo()) && wcsQueryTaskShowsTask(last.getTaskNo())) {
            return true;
        }
        if (last != null && !Cools.isEmpty(last.getTaskNo())) {
            log.info("WCS queryTask 无数据或失败,回退 WMS 主表/历史校验, taskNo={}", last.getTaskNo());
        }
        return previousChunkTasksReleasedInWms(lastSentChunk);
    }
    private boolean wcsQueryTaskShowsTask(String taskNo) {
        Map<String, Object> body = new HashMap<>();
        body.put("taskNo", taskNo);
        try {
            String response = new HttpHandler.Builder()
                    .setUri(wcs_address)
                    .setPath(queryTaskPath)
                    .setHttps(wcs_address != null && wcs_address.startsWith("https://"))
                    .setTimeout(60, TimeUnit.SECONDS)
                    .setJson(JSON.toJSONString(body))
                    .build()
                    .doPost();
            JSONObject jo = JSON.parseObject(response == null ? "{}" : response);
            Integer code = jo.getInteger("code");
            return code != null && code == 200 && queryTaskDataNonEmpty(jo.get("data"));
        } catch (IOException e) {
            log.warn("WCS queryTask 异常, taskNo={}", taskNo, e);
            return false;
        }
    }
    /**
     * 上一组每条:主表无则看历史表;主表有则 wrk_sts 不能仍为 11。
     */
    private boolean previousChunkTasksReleasedInWms(List<WorkTaskParams> chunk) {
        for (WorkTaskParams p : chunk) {
            if (p == null || Cools.isEmpty(p.getTaskNo())) {
                return false;
            }
            Integer wrkNo = Integer.valueOf(p.getTaskNo());
            WrkMast m = wrkMastService.selectById(wrkNo);
            if (m == null) {
                int logCnt = wrkMastLogService.selectCount(new EntityWrapper<WrkMastLog>().eq("wrk_no", wrkNo));
                if (logCnt <= 0) {
                    return false;
                }
            } else if (m.getWrkSts() != null && Objects.equals(m.getWrkSts(), 11L)) {
                return false;
            }
        }
        return true;
    }
    /**
     * 出库每组下发前:本组有有效最小序号且&gt;1 时,只校验「最小序号-1」一档;序号全无则跳过本条件。
     */
    private boolean outboundChunkPredecessorPltReady(List<WorkTaskParams> chunk, Map<String, WrkMast> wrkMastMap) {
        if (chunk == null || chunk.isEmpty()) {
            return true;
        }
        WorkTaskParams head = chunk.get(0);
        if (!"out".equalsIgnoreCase(head.getType())) {
            return true;
        }
        WrkMast headMast = wrkMastMap.get(head.getTaskNo());
        String userNo = sortUserNoForPub(head, headMast);
        if (Cools.isEmpty(userNo)) {
            return true;
        }
        int minPlt = Integer.MAX_VALUE;
        for (WorkTaskParams p : chunk) {
            if (!"out".equalsIgnoreCase(p.getType())) {
                continue;
            }
            Integer plt = sortPltForPub(p, wrkMastMap.get(p.getTaskNo()));
            if (plt != null && plt > 0 && plt < minPlt) {
                minPlt = plt;
            }
        }
        if (minPlt == Integer.MAX_VALUE || minPlt <= 1) {
            return true;
        }
        return outboundPltSlotReleasedInWms(userNo, minPlt - 1);
    }
    private boolean outboundPltSlotReleasedInWms(String userNo, int pltType) {
        List<WrkMast> rows = wrkMastService.selectList(new EntityWrapper<WrkMast>()
                .eq("user_no", userNo)
                .eq("io_type", 101)
                .eq("plt_type", pltType));
        if (rows != null && !rows.isEmpty()) {
            for (WrkMast m : rows) {
                if (m != null && m.getWrkSts() != null && Objects.equals(m.getWrkSts(), 11L)) {
                    return false;
                }
            }
            return true;
        }
        int logCnt = wrkMastLogService.selectCount(new EntityWrapper<WrkMastLog>()
                .eq("user_no", userNo)
                .eq("io_type", 101)
                .eq("plt_type", pltType));
        return logCnt > 0;
    }
    private static boolean queryTaskDataNonEmpty(Object data) {
        if (data == null) {
            return false;
        }
        if (data instanceof JSONArray) {
            return !((JSONArray) data).isEmpty();
        }
        if (data instanceof Collection) {
            return !((Collection<?>) data).isEmpty();
        }
        if (data instanceof String) {
            String s = (String) data;
            if (Cools.isEmpty(s)) {
                return false;
            }
            JSONArray arr = JSON.parseArray(s);
            return arr != null && !arr.isEmpty();
        }
        return true;
    }
    private Comparator<WorkTaskParams> pubWcsSortComparator(Map<String, WrkMast> wrkMastMap) {
        return Comparator
                .comparing((WorkTaskParams p) -> Optional.ofNullable(p.getType()).orElse(""), String.CASE_INSENSITIVE_ORDER)
                .thenComparing(p -> sortUserNoForPub(p, wrkMastMap.get(p.getTaskNo())), Comparator.nullsLast(String::compareTo))
                .thenComparing(p -> sortPltForPub(p, wrkMastMap.get(p.getTaskNo())), Comparator.nullsLast(Integer::compareTo));
    }
    private static String sortUserNoForPub(WorkTaskParams p, WrkMast wrkMast) {
        String userNo = wrkMast == null ? null : wrkMast.getUserNo();
        if (Cools.isEmpty(userNo)) {
            userNo = p.getBatch();
        }
        return Cools.isEmpty(userNo) ? null : userNo;
    }
    private static Integer sortPltForPub(WorkTaskParams p, WrkMast wrkMast) {
        if (wrkMast != null && wrkMast.getPltType() != null) {
            return wrkMast.getPltType();
        }
        return p.getBatchSeq();
    }
    /**
@@ -284,7 +571,7 @@
    }
    @Override
    public R syncDeviceStatusFromWcs() {
    public R syncDeviceStatusFromWcs(boolean logOnFailure) {
        if (!Boolean.parseBoolean(String.valueOf(switchValue))) {
            return R.ok("WCS开关关闭");
        }
@@ -311,7 +598,11 @@
            log.info("同步WCS设备状态成功, stationCount={}, crnCount={}", stationCount, crnCount);
            return R.ok("同步成功").add(result);
        } catch (Exception e) {
            log.error("同步WCS设备状态异常, response={}", response, e);
            if (logOnFailure) {
                log.error("同步WCS设备状态异常, response={}", response, e);
            } else {
                log.debug("同步WCS设备状态异常, response={}", response, e);
            }
            return R.error("同步WCS设备状态失败: " + e.getMessage());
        }
    }
@@ -504,30 +795,48 @@
        }
        HashMap<String,Object> map = new  HashMap<>();
        map.put("taskList", params);
        String response;
        String requestJson = JSON.toJSONString(map);
        String response = null;
        Throwable wcsThrown = null;
        boolean wcsBizOk = false;
        try {
            log.info("调用WCS取消出库任务, request={}", JSON.toJSONString(params));
            log.info("调用WCS取消出库任务, request={}", requestJson);
            response = new HttpHandler.Builder()
                    .setUri(wcs_address)
                    .setPath(stopOutTask)
//                    .setHttps(wcs_address != null && wcs_address.startsWith("https://"))
                    .setTimeout(10, TimeUnit.SECONDS)
                    .setJson(JSON.toJSONString(map))
                    .setJson(requestJson)
                    .build()
                    .doPost();
            JSONObject jsonObject = JSON.parseObject(response == null ? "{}" : response);
            log.info("WCS取消出库任务返回, response={}", response);
            Integer code = jsonObject.getInteger("code");
            if (code == null || !Objects.equals(code, 200)) {
            wcsBizOk = code != null && Objects.equals(code, 200);
            if (!wcsBizOk) {
                String msg = jsonObject.getString("msg");
                throw new CoolException(Cools.isEmpty(msg) ? "WCS取消出库任务失败" : msg);
            }
            return R.ok(Cools.isEmpty(jsonObject.getString("msg")) ? "操作成功" : jsonObject.getString("msg"));
        } catch (IOException e) {
            wcsThrown = e;
            throw new CoolException("调用WCS取消出库任务失败: " + e.getMessage());
        } finally {
            logWcsToApiLog(stopOutTask, requestJson, response, wcsThrown, wcsBizOk);
        }
    }
    private void logWcsToApiLog(String path, String requestJson, String response, Throwable thrown, boolean wcsBizOk) {
        String fullUrl = (wcs_address == null ? "" : wcs_address) + (path == null ? "" : path);
        boolean success = thrown == null && wcsBizOk;
        String resp = response == null ? "" : response;
        if (thrown != null && Cools.isEmpty(resp)) {
            resp = thrown.getMessage() == null ? "" : thrown.getMessage();
        }
        apiLogService.save(NS_WMS_TO_WCS, fullUrl, "-", "-",
                requestJson == null ? "" : requestJson, resp, success);
    }
    private String requestDeviceStatusFromWcs() throws IOException {
        HttpHandler.Builder builder = new HttpHandler.Builder()
                .setUri(wcs_address)
src/main/java/com/zy/asrs/controller/OpenController.java
@@ -57,6 +57,8 @@
    private MatService matService;
    @Autowired
    private LocMastService locMastService;
    @Autowired
    private ReportQueryMapper reportQueryMapper;
//    @PostMapping("/order/matSync/default/v1")
////    @AppAuth(memo = "商品信息同步接口")
@@ -486,6 +488,38 @@
            }
            orderIds.add(outTaskParam.getOrderId());
        }
        Map<String, List<OutTaskParam>> linesByOrder = new LinkedHashMap<>();
        for (OutTaskParam outTaskParam : params) {
            linesByOrder.computeIfAbsent(outTaskParam.getOrderId(), k -> new ArrayList<>()).add(outTaskParam);
        }
        for (Map.Entry<String, List<OutTaskParam>> entry : linesByOrder.entrySet()) {
            String oid = entry.getKey();
            List<OutTaskParam> lines = entry.getValue();
            List<Integer> seqs = new ArrayList<>(lines.size());
            for (OutTaskParam line : lines) {
                if (line.getSeq() == null) {
                    return R.error("出库单「" + oid + "」序号不能为空");
                }
                seqs.add(line.getSeq());
            }
            Collections.sort(seqs);
            for (int i = 0; i < seqs.size(); i++) {
                if (!String.valueOf(seqs.get(i)).equals(String.valueOf(i + 1))) {
                    return R.error("出库单「" + oid + "」序号不连续");
                }
            }
        }
        Set<String> seenPallet = new LinkedHashSet<>();
        for (OutTaskParam outTaskParam : params) {
            String pid = outTaskParam.getPalletId();
            String palletKey = pid == null ? "" : pid;
            if (!seenPallet.add(palletKey)) {
                return R.error("托盘号重复:" + (Cools.isEmpty(pid) ? "(空)" : pid));
            }
        }
//        if (!orderIds.isEmpty()) {
//            Set<String> existedOrderIds = new LinkedHashSet<>();
//            List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<WrkMast>().in("user_no", orderIds));
@@ -518,9 +552,9 @@
        }
        for (OutTaskParam outTaskParam : validOutOrders) {
            R r = openService.outOrder(outTaskParam,validOutOrders.size());
            if (!r.get("code").equals(200)){
                return r;
            LocMast locMast = locMastService.selectOne(new EntityWrapper<LocMast>().eq("loc_sts", "F").eq("barcode", outTaskParam.getPalletId()));
            if (locMast == null) {
                throw new CoolException("没有找到托盘码=" + outTaskParam.getPalletId() + "对应的库位");
            }
        }
src/main/java/com/zy/asrs/mapper/WrkMastLogMapper.java
@@ -6,6 +6,7 @@
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
import java.util.List;
@@ -19,6 +20,12 @@
    int save(Integer workNo);
    /**
     * 出库 101 下该单号已出现的 plt_type(历史表)
     */
    @Select("SELECT plt_type FROM asr_wrk_mast_log WHERE io_type = 101 AND plt_type IS NOT NULL AND plt_type > 0 AND user_no = #{userNo}")
    List<Integer> listOutboundPltTypesByUserNo(@Param("userNo") String userNo);
    /**
     * 查询库存移动流水记录
     */
    List<InventoryFlowDto> inventoryFlowList(@Param("curr") Integer curr,@Param("limit") Integer limit, @Param("param") Map<String, Object> param);
src/main/java/com/zy/asrs/mapper/WrkMastMapper.java
@@ -27,4 +27,10 @@
            , @Param("standby1")String standby1, @Param("standby2")String standby2, @Param("standby3")String standby3
            , @Param("boxType1")String boxType1, @Param("boxType2")String boxType2, @Param("boxType3")String boxType3, @Param("crnNo") Integer crnNo);
    /**
     * 出库 101 下该单号已出现的 plt_type(主表)
     */
    @Select("SELECT plt_type FROM asr_wrk_mast WHERE io_type = 101 AND plt_type IS NOT NULL AND plt_type > 0 AND user_no = #{userNo}")
    List<Integer> listOutboundPltTypesByUserNo(@Param("userNo") String userNo);
}
src/main/java/com/zy/asrs/service/WrkMastService.java
@@ -29,4 +29,9 @@
    WrkMast selectWrkMast(Integer workNo,String barcode);
    /**
     * 出库(io_type=101)同一 user_no:主表与历史表并集下,自 1 起连续存在的最大 plt_type。
     */
    int outboundSeqMaxContiguousPlt(String userNo);
}
src/main/java/com/zy/asrs/service/impl/OpenServiceImpl.java
@@ -33,7 +33,6 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.rmi.CORBA.Util;
import java.util.*;
import java.util.stream.Collectors;
@@ -428,6 +427,10 @@
            for (WrkMast wrkMast : activeTasks) {
                HashMap<String,Object> hashMap = new HashMap<>();
                hashMap.put("taskNo", wrkMast.getWrkNo());
                if (!Cools.isEmpty(wrkMast) && wrkMast.getWrkSts() ==11L) {
                    workService.cancelWrkMast(wrkMast.getWrkNo()+"", 9955L);
                    continue;
                }
                taskList.add(hashMap);
            }
            wcsApiService.pauseOutTasks(taskList);
@@ -1300,7 +1303,7 @@
    public R outOrder(OutTaskParam param,int count) {
        LocMast locMast = locMastService.selectOne(new EntityWrapper<LocMast>().eq("loc_sts", "F").eq("barcode", param.getPalletId()));
        if (locMast == null) {
            return R.error("没有找到托盘码=" + param.getPalletId() + "对应的库位");
            throw new CoolException("没有找到托盘码=" + param.getPalletId() + "对应的库位");
        }
        Integer ioType = 101;
        // 获取路径
src/main/java/com/zy/asrs/service/impl/WrkMastServiceImpl.java
@@ -5,16 +5,23 @@
import com.core.common.Cools;
import com.zy.asrs.entity.WrkMast;
import com.zy.asrs.entity.result.FindLocNoAttributeVo;
import com.zy.asrs.mapper.WrkMastLogMapper;
import com.zy.asrs.mapper.WrkMastMapper;
import com.zy.asrs.service.WrkMastService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Slf4j
@Service("wrkMastService")
public class WrkMastServiceImpl extends ServiceImpl<WrkMastMapper, WrkMast> implements WrkMastService {
    @Autowired
    private WrkMastLogMapper wrkMastLogMapper;
    @Override
    public int getWorkingMast(Integer devpNo) {
@@ -63,4 +70,29 @@
    public WrkMast selectWrkMast(Integer workNo, String barcode) {
        return this.baseMapper.selectWrkMast(workNo, barcode);
    }
    @Override
    public int outboundSeqMaxContiguousPlt(String userNo) {
        List<Integer> fromMast = baseMapper.listOutboundPltTypesByUserNo(userNo);
        List<Integer> fromLog = wrkMastLogMapper.listOutboundPltTypesByUserNo(userNo);
        Set<Integer> filled = new HashSet<>();
        addPositivePlt(fromMast, filled);
        addPositivePlt(fromLog, filled);
        int h = 0;
        while (filled.contains(h + 1)) {
            h++;
        }
        return h;
    }
    private static void addPositivePlt(List<Integer> list, Set<Integer> target) {
        if (list == null) {
            return;
        }
        for (Integer p : list) {
            if (p != null && p > 0) {
                target.add(p);
            }
        }
    }
}
src/main/java/com/zy/asrs/task/WcsDeviceStatusScheduler.java
@@ -20,6 +20,9 @@
    @Value("${wcs.status-sync.enabled:true}")
    private Boolean enabled;
    @Value("${wcs.status-sync.log-on-failure:true}")
    private Boolean logOnFailure;
    @Scheduled(
            initialDelayString = "${wcs.status-sync.initial-delay:10000}",
            fixedDelayString = "${wcs.status-sync.fixed-delay:5000}"
@@ -28,8 +31,8 @@
        if (!Boolean.TRUE.equals(enabled)) {
            return;
        }
        R result = wcsApiService.syncDeviceStatusFromWcs();
        if (!Objects.equals(result.get("code"), 200)) {
        R result = wcsApiService.syncDeviceStatusFromWcs(Boolean.TRUE.equals(logOnFailure));
        if (Boolean.TRUE.equals(logOnFailure) && !Objects.equals(result.get("code"), 200)) {
            log.warn("轮询同步WCS设备状态失败, result={}", result);
        }
    }
src/main/java/com/zy/asrs/task/WorkMastScheduler.java
@@ -5,7 +5,6 @@
import com.core.common.R;
import com.zy.api.controller.params.WorkTaskParams;
import com.zy.api.service.WcsApiService;
import com.zy.asrs.entity.LocMast;
import com.zy.asrs.entity.WrkMast;
import com.zy.asrs.service.WrkMastService;
import com.zy.asrs.task.core.ReturnT;
@@ -19,7 +18,6 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
@@ -70,20 +68,20 @@
     * @author Ryan
     * @date 2026/1/10 14:42
     */
    @Scheduled(cron = "0/3 * * * * ? ")
    @Scheduled(cron = "0/10 * * * * ? ")
    private void autoPubTasks() {
        // 仅处理待下发/已生成下发号的工作档。
        List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<WrkMast>().in("wrk_sts", Arrays.asList(1L, 11L)));
        List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<WrkMast>().in("wrk_sts", Arrays.asList(1L, 11L))
                .orderBy("user_no", true)
                .orderBy("plt_type", true));
        if (wrkMasts.isEmpty()) {
            return;
        }
        // 打散顺序,避免固定排序下同一批任务长期占用调度机会。
        Collections.shuffle(wrkMasts);
        List<WorkTaskParams> paramsList = new ArrayList<>();
        for (WrkMast wrkMast : wrkMasts) {
            // 出库类任务(ioType > 100)默认需要 ERP 确认;未确认的任务在这里直接跳过。
            if (wrkMast.getIoType()>100&& !wrkMast.getPdcType().equals("Y")) {
            if (wrkMast.getIoType() > 100 && !"Y".equalsIgnoreCase(wrkMast.getPdcType())) {
                continue;
            }
@@ -103,7 +101,7 @@
                        .setBatchSeq(wrkMast.getPltType())
                        .setBarcode(wrkMast.getBarcode());
            // 2: 入库。入库接口使用 sourceStaNo + 目标库位。
            }else if(wrkMast.getIoType()==2&& !Cools.isEmpty(wrkMast.getSourceStaNo())){
            } else if (wrkMast.getIoType() == 2 && !Cools.isEmpty(wrkMast.getSourceStaNo())) {
                params.setType("in")
                        .setTaskNo(wrkMast.getWrkNo()+"")
                        .setSourceStaNo(String.valueOf(wrkMast.getSourceStaNo()))
src/main/java/com/zy/integration/iot/client/impl/PahoIotMqttClient.java
@@ -6,6 +6,7 @@
import com.zy.integration.iot.util.IotMqttSslUtils;
import com.zy.iot.config.IotProperties;
import com.zy.iot.entity.IotTopicConfig;
import com.zy.iot.service.IotDbConfigService;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallbackExtended;
@@ -33,13 +34,15 @@
    @Autowired
    private IotProperties iotProperties;
    @Autowired
    private IotDbConfigService iotDbConfigService;
    @Autowired
    private IotInboundMessageHandler inboundMessageHandler;
    private volatile MqttClient mqttClient;
    @PostConstruct
    public void init() {
        if (!iotProperties.isEnabled()) {
        if (!iotDbConfigService.isMqttEnabled()) {
            return;
        }
        try {
@@ -68,8 +71,33 @@
     * 懒加载建连,首次发送或启动时自动连接并补订阅主题。
     */
    @Override
    public synchronized void reconnectFromDbConfig() {
        iotDbConfigService.refreshCache();
        try {
            if (mqttClient != null) {
                try {
                    if (mqttClient.isConnected()) {
                        mqttClient.disconnect();
                    }
                    mqttClient.close();
                } catch (Exception e) {
                    log.warn("IoT MQTT disconnect before reconnect", e);
                }
                mqttClient = null;
            }
            if (!iotDbConfigService.isMqttEnabled()) {
                log.info("IoT MQTT 已由配置关闭,未建立连接");
                return;
            }
            ensureConnected();
        } catch (Exception e) {
            log.error("IoT MQTT 重连失败", e);
        }
    }
    @Override
    public synchronized void ensureConnected() throws Exception {
        if (!iotProperties.isEnabled()) {
        if (!iotDbConfigService.isMqttEnabled()) {
            return;
        }
        if (!hasRequiredConfig()) {
@@ -84,6 +112,8 @@
        }
        mqttClient.connect(buildOptions());
        subscribeTopics();
        log.info("IoT MQTT 已连接 serverURI={} clientId={} tls={}",
                iotProperties.getServerUri(), iotProperties.getResolvedClientId(), iotProperties.isTlsEnabled());
    }
    /**
@@ -91,7 +121,7 @@
     */
    @Override
    public void publish(String topic, String payload) throws Exception {
        if (!iotProperties.isEnabled() || Cools.isEmpty(topic) || payload == null) {
        if (!iotDbConfigService.isMqttEnabled() || Cools.isEmpty(topic) || payload == null) {
            return;
        }
        ensureConnected();
@@ -101,6 +131,7 @@
        MqttMessage mqttMessage = new MqttMessage(payload.getBytes(StandardCharsets.UTF_8));
        mqttMessage.setQos(1);
        mqttMessage.setRetained(false);
        log.info("IoT MQTT 发送 topic={} payload={}", topic, payload);
        mqttClient.publish(topic, mqttMessage);
    }
@@ -135,7 +166,9 @@
            @Override
            public void messageArrived(String topic, MqttMessage message) {
                inboundMessageHandler.handleMessage(topic, new String(message.getPayload(), StandardCharsets.UTF_8));
                String body = new String(message.getPayload(), StandardCharsets.UTF_8);
                log.info("IoT MQTT 接收 topic={} payload={}", topic, body);
                inboundMessageHandler.handleMessage(topic, body);
            }
            @Override
@@ -146,7 +179,10 @@
            public void connectComplete(boolean reconnect, String serverURI) {
                try {
                    subscribeTopics();
                    log.info("iot mqtt connected, reconnect={}, serverURI={}", reconnect, serverURI);
                    if (reconnect) {
                        log.info("IoT MQTT 自动重连成功 serverURI={} clientId={}",
                                serverURI, iotProperties.getResolvedClientId());
                    }
                } catch (Exception e) {
                    log.error("subscribe iot topics failed", e);
                }
@@ -178,7 +214,7 @@
        if (mqttClient == null || !mqttClient.isConnected()) {
            return;
        }
        IotTopicConfig topics = iotProperties.getTopics();
        IotTopicConfig topics = iotDbConfigService.getEffectiveTopics();
        subscribe(topics.getEgressStow());
        subscribe(topics.getEgressPick());
        subscribe(topics.getEgressFeedback());