自动化立体仓库 - WMS系统
1.针对7.3接口文档,新增了几个字段需要加入到组托档中,不额外加字段
2.针对7.11接口文档,对outOrder方法进行重写,batchSeq在wrkMast表中新增一个字段放,entryWmsCode、outDoorNo这两个在wrkDetl中找两个字段存放
3.针对7.7接口文档,上报时加上orderId出库单号
4.针对7.9接口文档,wcs会先请求wms,只需要palletId托盘码,errorMsg错误信息;wms转发给加上orderId出库单号转发给ERP
5.针对7.10接口文档,ERP先按照这个文档发给wms,wms再发给wcs,发给wcs这块先不写
1个文件已删除
8个文件已添加
20个文件已修改
1335 ■■■■ 已修改文件
src/main/java/com/zy/api/service/impl/WcsApiServiceImpl.java 111 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/OpenController.java 89 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/WrkMast.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/WrkMastLog.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/param/ErpOutOrderAbnormalReportParam.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/param/ErpOutTaskLockReportParam.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/param/MesToCombParam.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/param/OutOrderAbnormalHandleParam.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/param/OutOrderAbnormalReportParam.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/param/OutTaskParam.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/mapper/WrkMastLogMapper.java 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/mapper/WrkMastMapper.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/OpenService.java 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/WrkMastService.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/OpenServiceImpl.java 242 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/WrkMastServiceImpl.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/task/WorkMastScheduler.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/task/WorkOutLockErpReportScheduler.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/task/handler/AutomaticallyIssueWCSTasksHandler.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/task/handler/NotifyLogHandler.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/task/handler/WorkOutErpReportHandler.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/task/handler/WorkOutLockErpReportHandler.java 297 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/common/web/WcsController.java 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application.yml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/WrkMastLogMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/WrkMastMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/sql/20260410_zhongyang_batch_seq.sql 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/sql/20260411_out_task_lock_status.sql 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/zy/common/service/CommonServiceRun2AllocationTest.java 329 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/api/service/impl/WcsApiServiceImpl.java
@@ -45,6 +45,10 @@
    private static final Long WCS_SYNC_USER = 9999L;
    private static final String YES = "Y";
    private static final String NO = "N";
    private static final long OUT_LOCK_REPORT_PENDING_WRK_STS = 13L;
    private static final long OUT_LOCK_REPORT_SUCCESS_WRK_STS = 21L;
    private static final long OUT_LOCK_REPORT_FAIL_WRK_STS = 22L;
    private static final String OUT_LOCK_REPORT_PENDING_FLAG = "P";
    /** 同一 WCS 路径、同一单号下一组下发的任务条数上限 */
    private static final int WCS_PUB_BATCH_SIZE = 20;
@@ -302,7 +306,7 @@
    }
    /**
     * 出库:仅当单号、序号均有效时做跳号校验;单号空或序号无效仍下发。入库/移库不处理。
     * 出库:仅当单号、批次、序号均有效时做批次内跳号校验;无效时仍下发。入库/移库不处理。
     */
    private List<WorkTaskParams> filterOutboundByContiguousPlt(List<WorkTaskParams> accepted, Map<String, WrkMast> wrkMastMap, List<String> skipMsgs) {
        Map<String, Integer> reachCache = new HashMap<>();
@@ -314,12 +318,14 @@
            }
            WrkMast w = wrkMastMap.get(p.getTaskNo());
            String userNo = sortUserNoForPub(p, w);
            String batchGroup = sortBatchGroupForPub(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);
            String cacheKey = buildOutboundBatchCacheKey(userNo, batchGroup);
            int maxReach = reachCache.computeIfAbsent(cacheKey, key -> wrkMastService.outboundSeqMaxContiguousPlt(userNo, batchGroup));
            if (plt > maxReach) {
                skipMsgs.add(buildTaskMsg(p, "出库序号跳号,跳过"));
                continue;
@@ -363,7 +369,7 @@
    }
    /**
     * 同单下一组:优先 WCS queryTask;失败或无数据则主表已非 11 或已进历史表。
     * 同单同批下一组:优先 WCS queryTask;失败或无数据则主表已非 11 或已进历史表。
     */
    private boolean sameOrderNextChunkAllowed(List<WorkTaskParams> lastSentChunk) {
        if (lastSentChunk == null || lastSentChunk.isEmpty()) {
@@ -426,7 +432,7 @@
    }
    /**
     * 出库每组下发前:本组有有效最小序号且&gt;1 时,只校验「最小序号-1」一档;序号全无则跳过本条件。
     * 出库每组下发前:本组有有效最小序号且&gt;1 时,只校验「同单同批的最小序号-1」一档;序号全无则跳过本条件。
     */
    private boolean outboundChunkPredecessorPltReady(List<WorkTaskParams> chunk, Map<String, WrkMast> wrkMastMap) {
        if (chunk == null || chunk.isEmpty()) {
@@ -438,6 +444,7 @@
        }
        WrkMast headMast = wrkMastMap.get(head.getTaskNo());
        String userNo = sortUserNoForPub(head, headMast);
        String batchGroup = sortBatchGroupForPub(head, headMast);
        if (Cools.isEmpty(userNo)) {
            return true;
        }
@@ -454,14 +461,20 @@
        if (minPlt == Integer.MAX_VALUE || minPlt <= 1) {
            return true;
        }
        return outboundPltSlotReleasedInWms(userNo, minPlt - 1);
        return outboundPltSlotReleasedInWms(userNo, batchGroup, 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));
    private boolean outboundPltSlotReleasedInWms(String userNo, String batchSeq, int pltType) {
        EntityWrapper<WrkMast> mastWrapper = new EntityWrapper<>();
        mastWrapper.eq("user_no", userNo);
        mastWrapper.eq("io_type", 101);
        mastWrapper.eq("plt_type", pltType);
        if (batchSeq == null) {
            mastWrapper.isNull("batch_seq");
        } else {
            mastWrapper.eq("batch_seq", batchSeq);
        }
        List<WrkMast> rows = wrkMastService.selectList(mastWrapper);
        if (rows != null && !rows.isEmpty()) {
            for (WrkMast m : rows) {
                if (m != null && m.getWrkSts() != null && Objects.equals(m.getWrkSts(), 11L)) {
@@ -470,10 +483,16 @@
            }
            return true;
        }
        int logCnt = wrkMastLogService.selectCount(new EntityWrapper<WrkMastLog>()
                .eq("user_no", userNo)
                .eq("io_type", 101)
                .eq("plt_type", pltType));
        EntityWrapper<WrkMastLog> logWrapper = new EntityWrapper<>();
        logWrapper.eq("user_no", userNo);
        logWrapper.eq("io_type", 101);
        logWrapper.eq("plt_type", pltType);
        if (batchSeq == null) {
            logWrapper.isNull("batch_seq");
        } else {
            logWrapper.eq("batch_seq", batchSeq);
        }
        int logCnt = wrkMastLogService.selectCount(logWrapper);
        return logCnt > 0;
    }
@@ -502,6 +521,7 @@
        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 -> sortBatchGroupForPub(p, wrkMastMap.get(p.getTaskNo())), Comparator.nullsLast(String::compareTo))
                .thenComparing(p -> sortPltForPub(p, wrkMastMap.get(p.getTaskNo())), Comparator.nullsLast(Integer::compareTo));
    }
@@ -518,6 +538,19 @@
            return wrkMast.getPltType();
        }
        return p.getBatchSeq();
    }
    private static String sortBatchGroupForPub(WorkTaskParams p, WrkMast wrkMast) {
        if (wrkMast != null) {
            return wrkMast.getBatchSeq();
        }
        return null;
    }
    private static String buildOutboundBatchCacheKey(String userNo, String batchSeq) {
        String safeUserNo = Cools.isEmpty(userNo) ? "_NO_USER_" : userNo;
        String safeBatchSeq = Cools.isEmpty(batchSeq) ? "_NO_BATCH_" : batchSeq;
        return safeUserNo + "#" + safeBatchSeq;
    }
    /**
@@ -544,13 +577,26 @@
        }
        if (params.getNotifyType().equals("task")) {
        if (isOutboundCrnTaskRun(params)) {
            // WCS出库任务开始:堆垛机开始执行出库任务,工作状态 12 -> 13。
            if (isOutboundTask(mast) && Objects.equals(mast.getWrkSts(), 12L)) {
                mast.setWrkSts(OUT_LOCK_REPORT_PENDING_WRK_STS);
                mast.setExpTime(0D);
                mast.setLogMk(OUT_LOCK_REPORT_PENDING_FLAG);
                mast.setLogErrMemo(null);
                mast.setLogErrTime(null);
                mast.setModiTime(new Date());
                if (!wrkMastService.updateById(mast)) {
                    throw new CoolException("任务状态修改失败!!");
                }
            }
        } else if (params.getNotifyType().equals("task")) {
            //任务
            if (params.getMsgType().equals("task_complete")) {
                if (mast.getIoType() == 1 || mast.getIoType() == 2 ||mast.getIoType() == 10) {
                    mast.setWrkSts(4L);
                } else if ((mast.getIoType() == 101||mast.getIoType()==110) && mast.getWrkSts()<14) {
                } else if (isOutboundTask(mast) && canMarkOutboundTaskComplete(mast)) {
                    mast.setWrkSts(14L);
                    if(Cools.isEmpty(mast.getStaNo())){
                        mast.setOveMk("Y");
@@ -581,6 +627,25 @@
        }
        return R.ok();
    }
    private boolean isOutboundCrnTaskRun(ReceviceTaskParams params) {
        return params != null
                && "Crn".equalsIgnoreCase(params.getNotifyType())
                && "crn_out_task_run".equalsIgnoreCase(params.getMsgType());
    }
    private boolean isOutboundTask(WrkMast mast) {
        return mast != null && mast.getIoType() != null && (mast.getIoType() == 101 || mast.getIoType() == 110);
    }
    private boolean canMarkOutboundTaskComplete(WrkMast mast) {
        if (mast == null || mast.getWrkSts() == null) {
            return false;
        }
        return mast.getWrkSts() < 14
                || mast.getWrkSts().equals(OUT_LOCK_REPORT_SUCCESS_WRK_STS)
                || mast.getWrkSts().equals(OUT_LOCK_REPORT_FAIL_WRK_STS);
    }
    @Override
@@ -993,21 +1058,23 @@
     * <p>
     * 分组规则:
     * 1. 先按接口路径区分,避免不同任务类型混用同一个 WCS 接口;
     * 2. 再按 userNo 区分,确保相同 userNo 的任务一起上报。
     * 2. 再按 userNo + batchSeq 区分,确保相同订单同批次的任务一起上报。
     * <p>
     * 正常情况下 userNo 取自 work_mast.user_no;
     * 如果当前没查到工作档,则回退到请求里的 batch 字段,保证兼容已有调用。
     * batchSeq 取自 work_mast.batch_seq;如果当前没查到工作档,则只按 userNo 回退兼容已有调用。
     */
    private String buildBatchGroupKey(WorkTaskParams params, WrkMast wrkMast) {
        String path = resolveTaskPath(params);
        String userNo = wrkMast == null ? null : wrkMast.getUserNo();
        String batchGroup = wrkMast == null ? null : wrkMast.getBatchSeq();
        if (Cools.isEmpty(userNo)) {
            userNo = params.getBatch();
        }
        if (Cools.isEmpty(userNo)) {
            userNo = "_NO_USER_";
        }
        return path + "#" + userNo;
        String batchKey = Cools.isEmpty(batchGroup) ? "_NO_BATCH_" : batchGroup;
        return path + "#" + userNo + "#" + batchKey;
    }
    /**
@@ -1048,10 +1115,12 @@
        if (!Cools.isEmpty(params.getStaNo())) {
            task.put("staNo", params.getStaNo());
        }
        if (!Cools.isEmpty(params.getBatch())) {
        boolean includeOutBatch = !"out".equalsIgnoreCase(params.getType())
                || (params.getBatchSeq() != null && params.getBatchSeq() > 0);
        if (includeOutBatch && !Cools.isEmpty(params.getBatch())) {
            task.put("batch", params.getBatch());
        }
        if (!Objects.isNull(params.getBatchSeq())) {
        if (includeOutBatch && !Objects.isNull(params.getBatchSeq())) {
            task.put("batchSeq", params.getBatchSeq());
        }
        return task;
src/main/java/com/zy/asrs/controller/OpenController.java
@@ -439,8 +439,7 @@
    }
    /**
     * 组托信息下发
     * return
     * 7.3 组托信息下发
     */
    @PostMapping("/comb/auth")
    public synchronized R comb(@RequestBody ArrayList<MesToCombParam> param, HttpServletRequest request) {
@@ -487,9 +486,8 @@
    }
    /**
     * 出库通知单
     * 7.11 出库通知单(传递有序无序规则)
     */
    @PostMapping("/outOrder")
    public synchronized R outOrder(@RequestBody ArrayList<OutTaskParam> params, HttpServletRequest request) {
        if (Cools.isEmpty(params)) {
@@ -497,32 +495,51 @@
        }
        log.info("[outOrder] cache: {}", JSON.toJSONString(params));
        request.setAttribute("cache", params);
        Set<String> orderIds = new LinkedHashSet<>();
        Map<String, List<OutTaskParam>> linesByBatch = new LinkedHashMap<>();
        for (OutTaskParam outTaskParam : params) {
            if (Cools.isEmpty(outTaskParam) || Cools.isEmpty(outTaskParam.getOrderId())) {
                return R.error("出库单号不能为空");
            }
            orderIds.add(outTaskParam.getOrderId());
            if (Cools.isEmpty(outTaskParam.getBatchSeq())) {
                outTaskParam.setBatchSeq(outTaskParam.getOrderId());
            }
            if (Cools.isEmpty(outTaskParam.getStationId())) {
                return R.error("托盘「" + outTaskParam.getPalletId() + "」出库口编码不能为空");
            }
            linesByBatch.computeIfAbsent(outTaskParam.getBatchSeq(), k -> new ArrayList<>()).add(outTaskParam);
        }
        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();
        for (Map.Entry<String, List<OutTaskParam>> entry : linesByBatch.entrySet()) {
            List<OutTaskParam> lines = entry.getValue();
            List<Integer> seqs = new ArrayList<>(lines.size());
            OutTaskParam head = lines.get(0);
            String oid = head.getOrderId();
            String batchSeq = head.getBatchSeq();
            boolean hasZero = false;
            boolean hasPositive = false;
            List<Integer> orderedSeqs = new ArrayList<>(lines.size());
            for (OutTaskParam line : lines) {
                if (line.getSeq() == null) {
                    return R.error("出库单「" + oid + "」序号不能为空");
                    return R.error("出库单「" + oid + "」批次「" + batchSeq + "」序号不能为空");
                }
                seqs.add(line.getSeq());
                if (line.getSeq() < 0) {
                    return R.error("出库单「" + oid + "」批次「" + batchSeq + "」序号不能小于0");
                }
                if (line.getSeq() == 0) {
                    hasZero = true;
                } else {
                    hasPositive = true;
                    orderedSeqs.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 + "」序号不连贯");
            if (hasZero && hasPositive) {
                return R.error("出库单「" + oid + "」批次「" + batchSeq + "」序号不能混用无序和有序");
            }
            if (!hasZero) {
                Collections.sort(orderedSeqs);
                for (int i = 0; i < orderedSeqs.size(); i++) {
                    if (!Objects.equals(orderedSeqs.get(i), i + 1)) {
                        return R.error("出库单「" + oid + "」批次「" + batchSeq + "」序号不连贯");
                    }
                }
            }
        }
@@ -589,6 +606,36 @@
    }
    /**
     * 7.9 出库异常变动上报
     */
    @PostMapping("/order/pakout/abnormal/report/v1")
    public synchronized R outOrderAbnormalReport(@RequestBody OutOrderAbnormalReportParam param, HttpServletRequest request) {
        if (request != null) {
            log.info("[outOrderAbnormalReport] cache: {}", param == null ? "null" : JSON.toJSONString(param));
            request.setAttribute("cache", param);
        }
        if (Cools.isEmpty(param) || Cools.isEmpty(param.getPalletId())) {
            return R.error("palletId不能为空");
        }
        return openService.outOrderAbnormalReport(param);
    }
    /**
     * 7.10 出库异常变动处理
     */
    @PostMapping("/order/pakout/abnormal/handle/v1")
    public synchronized R outOrderAbnormalHandle(@RequestBody OutOrderAbnormalHandleParam param, HttpServletRequest request) {
        if (request != null) {
            log.info("[outOrderAbnormalHandle] cache: {}", param == null ? "null" : JSON.toJSONString(param));
            request.setAttribute("cache", param);
        }
        if (Cools.isEmpty(param) || Cools.isEmpty(param.getPalletId())) {
            return R.error("palletId不能为空");
        }
        return openService.outOrderAbnormalHandle(param);
    }
    /**
     * pause out order
     */
    @PostMapping("/order/pakout/pause/default/v1")
@@ -603,6 +650,10 @@
        return openService.pakoutOrderPause(param);
    }
    private String buildOutOrderBatchKey(OutTaskParam param) {
        return param.getOrderId() + "#" + param.getBatchSeq();
    }
    /*************************************电视机程序***********************************************/
src/main/java/com/zy/asrs/entity/WrkMast.java
@@ -121,6 +121,10 @@
    @TableField("plt_type")
    private Integer pltType;
    @ApiModelProperty(value= "出库批次序号")
    @TableField("batch_seq")
    private String batchSeq;
    /**
     * 拣料
     */
src/main/java/com/zy/asrs/entity/WrkMastLog.java
@@ -147,6 +147,10 @@
    @TableField("plt_type")
    private Integer pltType;
    @ApiModelProperty(value= "")
    @TableField("batch_seq")
    private String batchSeq;
    /**
     * 空板
     */
src/main/java/com/zy/asrs/entity/param/ErpOutOrderAbnormalReportParam.java
New file
@@ -0,0 +1,16 @@
package com.zy.asrs.entity.param;
import lombok.Data;
@Data
public class ErpOutOrderAbnormalReportParam {
    // 托盘编码
    private String palletId;
    // 错误信息
    private String errorMsg;
    // 出库单号
    private String orderId;
}
src/main/java/com/zy/asrs/entity/param/ErpOutTaskLockReportParam.java
New file
@@ -0,0 +1,18 @@
package com.zy.asrs.entity.param;
import lombok.Data;
import java.util.Date;
@Data
public class ErpOutTaskLockReportParam {
    // 托盘编码
    private String palletId;
    // 出库单号
    private String orderId;
    //开始时间
    private String startTime;
}
src/main/java/com/zy/asrs/entity/param/MesToCombParam.java
@@ -13,6 +13,12 @@
    private Double anfme;
    //进仓编号
    private String entryWmsCode;
    //客户Id
    private String customerId;
    //客户名称
    private String customerName;
    //item编号
    private String item;
    //po单号
    private String orderId;
    //批号
@@ -31,6 +37,8 @@
    private String bizNo;
    //是否散货,0 非散货;1 散货;为了管控出货速率,散货可以出慢点。
    private Integer package1;
    //外库门号
    private String outDoorNo;
    //入库通知来源,ERP 默认 erp,MQTT 组托传 aws。
    private String boxType1;
}
src/main/java/com/zy/asrs/entity/param/OutOrderAbnormalHandleParam.java
New file
@@ -0,0 +1,13 @@
package com.zy.asrs.entity.param;
import lombok.Data;
@Data
public class OutOrderAbnormalHandleParam {
    // 托盘编码
    private String palletId;
    // 操作类型,0 转无序;2 调整至最后一托
    private Integer operateType;
}
src/main/java/com/zy/asrs/entity/param/OutOrderAbnormalReportParam.java
New file
@@ -0,0 +1,13 @@
package com.zy.asrs.entity.param;
import lombok.Data;
@Data
public class OutOrderAbnormalReportParam {
    // 托盘编码
    private String palletId;
    // 错误信息
    private String errorMsg;
}
src/main/java/com/zy/asrs/entity/param/OutTaskParam.java
@@ -4,6 +4,8 @@
@Data
public class OutTaskParam {
    //同订单中批次标识
    private String batchSeq;
    //出库顺序,从1开始
    private Integer seq;
    //托盘编码
@@ -18,4 +20,6 @@
    private String entryWmsCode;
    //出库单号
    private String orderId;
    //外库门号
    private String outDoorNo;
}
src/main/java/com/zy/asrs/mapper/WrkMastLogMapper.java
@@ -16,14 +16,24 @@
@Repository
public interface WrkMastLogMapper extends BaseMapper<WrkMastLog> {
    @Insert("insert into asr_wrk_mast_log select * from asr_wrk_mast where wrk_no=#{workNo}")
    @Insert("INSERT INTO asr_wrk_mast_log (" +
            "wrk_no, inv_wh, ymd, mk, whs_type, wrk_sts, io_type, crn_no, sheet_no, io_pri, wrk_date, loc_no, sta_no, source_sta_no, source_loc_no, loc_sts, " +
            "picking, link_mis, online_yn, upd_mk, exit_mk, plt_type, batch_seq, empty_mk, io_time, ctn_type, packed, ove_mk, mtn_type, user_no, crn_str_time, " +
            "crn_end_time, plc_str_time, crn_pos_time, load_time, exp_time, ref_wrkno, ref_iotime, modi_user, modi_time, appe_user, appe_time, pause_mk, " +
            "error_time, error_memo, ctn_kind, manu_type, memo_m, sc_weight, log_mk, log_err_time, log_err_memo, barcode, Pdc_type, ctn_no, full_plt, pre_have, take_none) " +
            "SELECT " +
            "wrk_no, inv_wh, ymd, mk, whs_type, wrk_sts, io_type, crn_no, sheet_no, io_pri, wrk_date, loc_no, sta_no, source_sta_no, source_loc_no, loc_sts, " +
            "picking, link_mis, online_yn, upd_mk, exit_mk, plt_type, batch_seq, empty_mk, io_time, ctn_type, packed, ove_mk, mtn_type, user_no, crn_str_time, " +
            "crn_end_time, plc_str_time, crn_pos_time, load_time, exp_time, ref_wrkno, ref_iotime, modi_user, modi_time, appe_user, appe_time, pause_mk, " +
            "error_time, error_memo, ctn_kind, manu_type, memo, sc_weight, log_mk, log_err_time, log_err_memo, barcode, Pdc_type, ctn_no, full_plt, pre_have, take_none " +
            "FROM asr_wrk_mast WHERE wrk_no=#{workNo}")
    int save(Integer workNo);
    /**
     * 出库 101 下该单号已出现的 plt_type(历史表)
     * 出库 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);
    @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} AND ((#{batchSeq} IS NULL AND batch_seq IS NULL) OR batch_seq = #{batchSeq})")
    List<Integer> listOutboundPltTypesByUserNo(@Param("userNo") String userNo, @Param("batchSeq") String batchSeq);
    /**
     * 查询库存移动流水记录
src/main/java/com/zy/asrs/mapper/WrkMastMapper.java
@@ -28,9 +28,9 @@
            , @Param("boxType1")String boxType1, @Param("boxType2")String boxType2, @Param("boxType3")String boxType3, @Param("crnNo") Integer crnNo);
    /**
     * 出库 101 下该单号已出现的 plt_type(主表)
     * 出库 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);
    @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} AND ((#{batchSeq} IS NULL AND batch_seq IS NULL) OR batch_seq = #{batchSeq})")
    List<Integer> listOutboundPltTypesByUserNo(@Param("userNo") String userNo, @Param("batchSeq") String batchSeq);
}
src/main/java/com/zy/asrs/service/OpenService.java
@@ -73,17 +73,28 @@
    R stationAll ();
    /**
     * mes发组托档给到wms
     * 7.3 组托信息下发
     */
    R mesToComb(MesToCombParam param);
    /**
     *
     * 7.11 出库通知单(传递有序无序规则)
     */
    R outOrder(OutTaskParam  param,int count);
    /**
     * 批量出库建单,同一事务:任一行失败则全部回滚。
     * 7.11 出库通知单(传递有序无序规则)批量建单,同一事务:任一行失败则全部回滚。
     */
    R outOrderBatch(List<OutTaskParam> params);
    /**
     * 7.9 出库异常变动上报
     */
    R outOrderAbnormalReport(OutOrderAbnormalReportParam param);
    /**
     * 7.10 出库异常变动处理
     */
    R outOrderAbnormalHandle(OutOrderAbnormalHandleParam param);
}
src/main/java/com/zy/asrs/service/WrkMastService.java
@@ -30,8 +30,8 @@
    WrkMast selectWrkMast(Integer workNo,String barcode);
    /**
     * 出库(io_type=101)同一 user_no:主表与历史表并集下,自 1 起连续存在的最大 plt_type。
     * 出库(io_type=101)同一 user_no、同一 batch_seq:主表与历史表并集下,自 1 起连续存在的最大 plt_type。
     */
    int outboundSeqMaxContiguousPlt(String userNo);
    int outboundSeqMaxContiguousPlt(String userNo, String batchSeq);
}
src/main/java/com/zy/asrs/service/impl/OpenServiceImpl.java
@@ -87,6 +87,10 @@
    private String mesUrl;
    @Value("${mes.stationaddress}")
    private String stationAddress;
    @Value("${erp.address.URL:}")
    private String erpUrl;
    @Value("${erp.address.OutErroraddress:}")
    private String erpOutErrorAddress;
    @Autowired
    private WaitPakinService waitPakinService;
    @Autowired
@@ -1298,6 +1302,9 @@
        return mesUrl + stationAddress;
    }
    /**
     * 7.3 组托信息下发。
     */
    @Override
    public R mesToComb(MesToCombParam param) {
        if (Cools.isEmpty(param.getPalletId())) {
@@ -1339,6 +1346,12 @@
        waitPakin.setOrigin(String.valueOf(param.getStorageArea()));//建议入库区域
        waitPakin.setManu(String.valueOf(param.getLocId()));//mes具体库位编号
        waitPakin.setThreeCode(param.getBizNo());
        // 7.3 新增字段复用现有组托档承载位,不新增列。
        waitPakin.setSuppCode(param.getCustomerId());
        waitPakin.setSupp(param.getCustomerName());
        waitPakin.setStandby3(param.getItem());
        waitPakin.setStandby1(param.getEntryWmsCode());
        waitPakin.setStandby2(param.getOutDoorNo());
        waitPakin.setBeBatch(param.getPackage1());//是否散货,0 非散货;1 散货;为了管控出货速率,散货可以出慢点。
        // ERP 入口默认打 erp,MQTT 组托会在参数里显式传 aws。
        waitPakin.setBoxType1(Cools.isEmpty(param.getBoxType1()) ? "erp" : param.getBoxType1());
@@ -1348,15 +1361,36 @@
        return R.ok().add(Cools.add("palletId", param.getPalletId()).add("orderId", param.getOrderId()));
    }
    /**
     * 7.11 出库通知单(传递有序无序规则)单条建单。
     */
    @Override
    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) {
            throw new CoolException("没有找到托盘码=" + param.getPalletId() + "对应的库位");
        }
        if (Cools.isEmpty(param.getStationId())) {
            throw new CoolException("出库口编码不能为空");
        }
        if (Cools.isEmpty(param.getBatchSeq())) {
            throw new CoolException("批次标识不能为空");
        }
        if (param.getSeq() == null || param.getSeq() < 0) {
            throw new CoolException("出库顺序不能为空且不能小于0");
        }
        Integer ioType = 101;
        // 获取路径
        StaDesc staDesc = staDescService.queryCrnStn(ioType, locMast.getCrnNo(), Integer.valueOf(param.getStationId()));
        Integer stationId;
        try {
            stationId = Integer.valueOf(param.getStationId());
        } catch (NumberFormatException ex) {
            throw new CoolException("出库口编码格式错误:" + param.getStationId());
        }
        StaDesc staDesc = staDescService.queryCrnStn(ioType, locMast.getCrnNo(), stationId);
        if (staDesc == null) {
            throw new CoolException("未找到出库口=" + param.getStationId() + "对应路径");
        }
        Date now = new Date();
        // 生成工作号
        int workNo = commonService.getWorkNo(WorkNoType.getWorkNoType(ioType));
@@ -1378,7 +1412,9 @@
        wrkMast.setEmptyMk("N"); // 空板
        wrkMast.setLinkMis("N");
        wrkMast.setPdcType("N");
        // 7.11:orderId 存 userNo,batchSeq 存批次标识,seq 存批次内顺序。
        wrkMast.setUserNo(param.getOrderId());//订单号
        wrkMast.setBatchSeq(param.getBatchSeq());//订单内批次标识
        wrkMast.setPltType(param.getSeq());//出库顺序,从1开始
        wrkMast.setTakeNone("0");  //0非自动
        wrkMast.setAppeUser(9995L); // 操作人员数据
@@ -1405,6 +1441,9 @@
            wrkDetl.setAppeUser(9995L);
            wrkDetl.setModiTime(now);
            wrkDetl.setModiUser(9995L);
            // 7.11:entryWmsCode、outDoorNo 复用明细备用字段。
            wrkDetl.setStandby1(param.getEntryWmsCode());
            wrkDetl.setStandby2(param.getOutDoorNo());
            wrkDetl.setSupp(param.getSeq()+"/"+count);
            if (!wrkDetlService.insert(wrkDetl)) {
@@ -1427,18 +1466,217 @@
        return R.ok().add(Cools.add("wrkNo", workNo).add("orderId", param.getOrderId()));
    }
    /**
     * 7.11 出库通知单(传递有序无序规则)批量建单。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public R outOrderBatch(List<OutTaskParam> params) {
        int n = params.size();
        Map<String, Integer> batchLineCounts = new HashMap<>();
        for (OutTaskParam outTaskParam : params) {
            R r = outOrder(outTaskParam, n);
            batchLineCounts.merge(buildOutOrderBatchKey(outTaskParam), 1, Integer::sum);
        }
        for (OutTaskParam outTaskParam : params) {
            int count = batchLineCounts.getOrDefault(buildOutOrderBatchKey(outTaskParam), n);
            R r = outOrder(outTaskParam, count);
            if (!Objects.equals(r.get("code"), 200)) {
                throw new CoolException("出库建单失败");
            }
        }
        return R.ok();
    }
    /**
     * 7.9 出库异常变动上报。
     * WCS 只传 palletId、errorMsg,WMS 解析 orderId 后转发 ERP。
     */
    @Override
    public R outOrderAbnormalReport(OutOrderAbnormalReportParam param) {
        if (param == null || Cools.isEmpty(param.getPalletId())) {
            return R.error("palletId不能为空");
        }
        String orderId = resolveOutboundOrderId(param.getPalletId());
        if (Cools.isEmpty(orderId)) {
            return R.error("未找到托盘对应出库单号:" + param.getPalletId());
        }
        if (Cools.isEmpty(erpOutErrorAddress)) {
            return R.error("ERP出库异常上报地址未配置");
        }
        ErpOutOrderAbnormalReportParam erpParam = new ErpOutOrderAbnormalReportParam();
        erpParam.setPalletId(param.getPalletId());
        erpParam.setErrorMsg(param.getErrorMsg());
        erpParam.setOrderId(orderId);
        String requestJson = JSON.toJSONString(erpParam);
        String response = "";
        boolean pushOk = false;
        String errorMsg = null;
        String pushUrl = buildErpOutErrorRequestUrl();
        try {
            response = new HttpHandler.Builder()
                    .setUri(erpUrl)
                    .setPath(erpOutErrorAddress)
                    .setJson(requestJson)
                    .build()
                    .doPost();
            pushOk = isErpCallSuccess(response);
            if (!pushOk) {
                errorMsg = extractErpCallError(response);
            }
        } catch (Exception e) {
            errorMsg = e.getMessage();
            log.error("推ERP出库异常上报失败, palletId={}, orderId={}", param.getPalletId(), orderId, e);
        } finally {
            try {
                apiLogService.save(
                        "推ERP-出库异常上报",
                        pushUrl,
                        null,
                        "127.0.0.1",
                        requestJson,
                        response,
                        pushOk,
                        "palletId=" + param.getPalletId() + ",orderId=" + orderId
                );
            } catch (Exception logEx) {
                log.error("save out abnormal api log failed", logEx);
            }
        }
        if (!pushOk) {
            return R.error(Cools.isEmpty(errorMsg) ? "ERP出库异常上报失败" : errorMsg);
        }
        return R.ok().add(Cools.add("palletId", param.getPalletId()).add("orderId", orderId));
    }
    /**
     * 7.10 出库异常变动处理。
     * 本期只接收、校验和记录,不改本地任务,也不转发 WCS。
     */
    @Override
    public R outOrderAbnormalHandle(OutOrderAbnormalHandleParam param) {
        if (param == null || Cools.isEmpty(param.getPalletId())) {
            return R.error("palletId不能为空");
        }
        if (param.getOperateType() == null || !(Objects.equals(param.getOperateType(), 0) || Objects.equals(param.getOperateType(), 2))) {
            return R.error("operateType只允许0或2");
        }
        if (!existsOutboundPallet(param.getPalletId())) {
            return R.error("未找到托盘:" + param.getPalletId());
        }
        log.info("接收出库异常处理指令, palletId={}, operateType={}", param.getPalletId(), param.getOperateType());
        return R.ok().add(Cools.add("palletId", param.getPalletId()).add("operateType", param.getOperateType()));
    }
    private String buildOutOrderBatchKey(OutTaskParam param) {
        return param.getOrderId() + "#" + param.getBatchSeq();
    }
    private String resolveOutboundOrderId(String palletId) {
        List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<WrkMast>()
                .eq("io_type", 101)
                .eq("barcode", palletId)
                .orderBy("io_time", false)
                .orderBy("wrk_no", false));
        if (!Cools.isEmpty(wrkMasts)) {
            for (WrkMast wrkMast : wrkMasts) {
                if (!Cools.isEmpty(wrkMast.getUserNo())) {
                    return wrkMast.getUserNo();
                }
            }
        }
        List<WrkDetl> wrkDetls = wrkDetlService.selectList(new EntityWrapper<WrkDetl>()
                .eq("zpallet", palletId)
                .orderBy("io_time", false)
                .orderBy("wrk_no", false));
        if (!Cools.isEmpty(wrkDetls)) {
            for (WrkDetl wrkDetl : wrkDetls) {
                if (!Cools.isEmpty(wrkDetl.getOrderNo())) {
                    return wrkDetl.getOrderNo();
                }
            }
        }
        return null;
    }
    private boolean existsOutboundPallet(String palletId) {
        if (locDetlService.selectCount(new EntityWrapper<LocDetl>().eq("zpallet", palletId)) > 0) {
            return true;
        }
        if (wrkMastService.selectCount(new EntityWrapper<WrkMast>().eq("io_type", 101).eq("barcode", palletId)) > 0) {
            return true;
        }
        return wrkDetlService.selectCount(new EntityWrapper<WrkDetl>().eq("zpallet", palletId)) > 0;
    }
    private boolean isErpCallSuccess(String response) {
        if (Cools.isEmpty(response)) {
            return false;
        }
        try {
            JSONObject jsonObject = JSON.parseObject(response);
            if (jsonObject == null) {
                return false;
            }
            Boolean success = jsonObject.getBoolean("success");
            if (success != null) {
                return success;
            }
            Integer code = jsonObject.getInteger("code");
            if (code != null) {
                return code == 200 || code == 0;
            }
            Integer status = jsonObject.getInteger("status");
            if (status != null) {
                return status == 200 || status == 0;
            }
            String result = jsonObject.getString("result");
            if (!Cools.isEmpty(result)) {
                return "success".equalsIgnoreCase(result) || "ok".equalsIgnoreCase(result) || "true".equalsIgnoreCase(result);
            }
        } catch (Exception ignore) {
            return response.toLowerCase().contains("success") && response.toLowerCase().contains("true");
        }
        return false;
    }
    private String extractErpCallError(String response) {
        if (Cools.isEmpty(response)) {
            return "empty erp response";
        }
        try {
            JSONObject jsonObject = JSON.parseObject(response);
            if (jsonObject == null) {
                return response;
            }
            String[] keys = new String[]{"msg", "message", "error", "errMsg"};
            for (String key : keys) {
                String value = jsonObject.getString(key);
                if (!Cools.isEmpty(value)) {
                    return value;
                }
            }
        } catch (Exception ignore) {
        }
        return response;
    }
    private String buildErpOutErrorRequestUrl() {
        if (Cools.isEmpty(erpUrl)) {
            return erpOutErrorAddress;
        }
        if (erpOutErrorAddress == null) {
            return erpUrl;
        }
        if (erpUrl.endsWith("/") && erpOutErrorAddress.startsWith("/")) {
            return erpUrl + erpOutErrorAddress.substring(1);
        }
        if (!erpUrl.endsWith("/") && !erpOutErrorAddress.startsWith("/")) {
            return erpUrl + "/" + erpOutErrorAddress;
        }
        return erpUrl + erpOutErrorAddress;
    }
}
src/main/java/com/zy/asrs/service/impl/WrkMastServiceImpl.java
@@ -72,9 +72,9 @@
    }
    @Override
    public int outboundSeqMaxContiguousPlt(String userNo) {
        List<Integer> fromMast = baseMapper.listOutboundPltTypesByUserNo(userNo);
        List<Integer> fromLog = wrkMastLogMapper.listOutboundPltTypesByUserNo(userNo);
    public int outboundSeqMaxContiguousPlt(String userNo, String batchSeq) {
        List<Integer> fromMast = baseMapper.listOutboundPltTypesByUserNo(userNo, batchSeq);
        List<Integer> fromLog = wrkMastLogMapper.listOutboundPltTypesByUserNo(userNo, batchSeq);
        Set<Integer> filled = new HashSet<>();
        addPositivePlt(fromMast, filled);
        addPositivePlt(fromLog, filled);
src/main/java/com/zy/asrs/task/WorkMastScheduler.java
@@ -63,7 +63,7 @@
     * <p>
     * 当前批量下发的归并维度是:
     * 1. WCS接口路径(入库/出库/移库不能混发);
     * 2. work_mast.user_no(相同 userNo 的任务必须放到同一批次一起上报)。
     * 2. work_mast.user_no + batch_seq(相同订单同批次的任务必须放到同一批次一起上报)。
     *
     * @author Ryan
     * @date 2026/1/10 14:42
@@ -73,6 +73,7 @@
        // 仅处理待下发/已生成下发号的工作档。
        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;
@@ -90,16 +91,18 @@
            String wcsLocNo = Cools.isEmpty(wrkMast.getLocNo()) ? "" : Utils.WMSLocToWCSLoc(wrkMast.getLocNo());
            WorkTaskParams params = new WorkTaskParams();
            // 101: 出库。此处 batch 字段承载 userNo,后续 service 层会据此把相同 userNo 的任务并到一批。
            // 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())
                        .setBatch(wrkMast.getUserNo())
                        .setBatchSeq(wrkMast.getPltType())
                        .setBarcode(wrkMast.getBarcode());
                if (wrkMast.getPltType() != null && wrkMast.getPltType() > 0) {
                    params.setBatch(wrkMast.getUserNo())
                            .setBatchSeq(wrkMast.getPltType());
                }
            // 2: 入库。入库接口使用 sourceStaNo + 目标库位。
            } else if (wrkMast.getIoType() == 2 && !Cools.isEmpty(wrkMast.getSourceStaNo())) {
                params.setType("in")
src/main/java/com/zy/asrs/task/WorkOutLockErpReportScheduler.java
New file
@@ -0,0 +1,47 @@
package com.zy.asrs.task;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
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.WorkOutLockErpReportHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
@Slf4j
@Component
public class WorkOutLockErpReportScheduler {
    @Autowired
    private WrkMastService wrkMastService;
    @Autowired
    private WorkOutLockErpReportHandler workOutLockErpReportHandler;
    /**
     * 7.12 出库任务锁定。
     * WCS 上报出库任务开始后,WMS 将 wrk_sts=13 的出库任务锁定信息上报 ERP。
     */
    @Scheduled(cron = "0/10 * * * * ? ")
    private void execute() {
        List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<WrkMast>()
                .eq("wrk_sts", WorkOutLockErpReportHandler.ERP_LOCK_REPORT_PENDING_WRK_STS)
                .in("io_type", Arrays.asList(101, 110))
                .orderBy("io_time", true)
                .orderBy("wrk_no", true));
        if (wrkMasts.isEmpty()) {
            return;
        }
        for (WrkMast wrkMast : wrkMasts) {
            ReturnT<String> result = workOutLockErpReportHandler.start(wrkMast);
            if (!result.isSuccess()) {
                log.error("workNo={} outbound erp lock report failed: {}", wrkMast.getWrkNo(), result.getMsg());
            }
        }
    }
}
src/main/java/com/zy/asrs/task/handler/AutomaticallyIssueWCSTasksHandler.java
@@ -91,8 +91,11 @@
                    if (m.getIoPri() != null) {
                        t.put("taskPri", m.getIoPri().intValue());
                    }
                    t.put("batchSeq", m.getPltType());
                    if (m.getUserNo() != null) {
                    // 7.11:seq=0 表示无序,WCS 下发不传 batch 和 batchSeq。
                    if (m.getPltType() != null && m.getPltType() > 0) {
                        t.put("batchSeq", m.getPltType());
                    }
                    if (m.getPltType() != null && m.getPltType() > 0 && m.getUserNo() != null) {
                        t.put("batch", m.getUserNo());
                    }
                    tasks.add(t);
src/main/java/com/zy/asrs/task/handler/NotifyLogHandler.java
@@ -14,6 +14,16 @@
@Service
public class NotifyLogHandler extends AbstractHandler<String> {
    private static final String WAIT_PAKIN_ARCHIVE_COLUMNS =
            "zpallet, anfme, loc_no, matnr, maktx, batch, order_no, specs, model, color, brand, unit, price, sku, units, barcode, " +
            "origin, manu, manu_date, item_num, safe_qty, weight, man_length, volume, three_code, supp, supp_code, be_batch, dead_time, " +
            "dead_warn, source, inspect, danger, status, io_status, modi_time, modi_user, appe_time, appe_user, memo, standby1, standby2, " +
            "standby3, box_type1, box_type2, box_type3";
    private static final String ARCHIVE_FINISHED_WAIT_PAKIN_SQL =
            "insert into cust_wait_pakin_log (" + WAIT_PAKIN_ARCHIVE_COLUMNS + ") " +
            "select " + WAIT_PAKIN_ARCHIVE_COLUMNS + " from cust_wait_pakin where io_status = 'F'";
    @Autowired
    private JdbcTemplate jdbcTemplate;
@@ -21,7 +31,7 @@
    public ReturnT<String> start() {
        try {
            // 入库通知档转历史档
            int pakInLog = jdbcTemplate.update("insert into cust_wait_pakin_log select * from cust_wait_pakin where io_status = 'F';");
            int pakInLog = jdbcTemplate.update(ARCHIVE_FINISHED_WAIT_PAKIN_SQL);
            if (pakInLog > 0) {
                int pakInDelete = jdbcTemplate.update("delete from cust_wait_pakin where io_status = 'F';");
                if (pakInDelete <= 0) {
src/main/java/com/zy/asrs/task/handler/WorkOutErpReportHandler.java
@@ -52,6 +52,10 @@
    @Value("${erp.address.Outaddress}")
    private String erpOutAddress;
    /**
     * 7.7 出库完成更新。
     * WMS 在出库完成后向 ERP 上报 palletId、createTime、startTime、orderId。
     */
    @Transactional(rollbackFor = Exception.class)
    public ReturnT<String> start(WrkMast source) {
        WrkMast wrkMast = wrkMastService.selectById(source.getWrkNo());
@@ -69,6 +73,9 @@
        if (wrkDetls != null && wrkDetls.size() > 0) {
            param.setOrderId(wrkDetls.get(0).getOrderNo());
        }
        if (Cools.isEmpty(param.getOrderId())) {
            param.setOrderId(wrkMast.getUserNo());
        }
        String request = JSON.toJSONString(param);
        String response = "";
        boolean success = false;
src/main/java/com/zy/asrs/task/handler/WorkOutLockErpReportHandler.java
New file
@@ -0,0 +1,297 @@
package com.zy.asrs.task.handler;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.core.common.Cools;
import com.zy.asrs.entity.WrkDetl;
import com.zy.asrs.entity.WrkMast;
import com.zy.asrs.entity.param.ErpOutTaskLockReportParam;
import com.zy.asrs.service.ApiLogService;
import com.zy.asrs.service.WrkDetlService;
import com.zy.asrs.service.WrkMastService;
import com.zy.asrs.task.AbstractHandler;
import com.zy.asrs.task.core.ReturnT;
import com.zy.common.entity.Parameter;
import com.zy.common.utils.HttpHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
@Slf4j
@Service
public class WorkOutLockErpReportHandler extends AbstractHandler<String> {
    public static final long ERP_LOCK_REPORT_PENDING_WRK_STS = 13L;
    public static final long ERP_LOCK_REPORT_SUCCESS_WRK_STS = 21L;
    public static final long ERP_LOCK_REPORT_FAIL_WRK_STS = 22L;
    public static final int ERP_LOCK_REPORT_MAX_RETRY_TIMES = 3;
    public static final String ERP_LOCK_REPORT_PENDING_FLAG = "P";
    public static final String ERP_LOCK_REPORT_SUCCESS_FLAG = "Y";
    public static final String ERP_LOCK_REPORT_FAIL_FLAG = "F";
    private static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
    @Autowired
    private WrkMastService wrkMastService;
    @Autowired
    private WrkDetlService wrkDetlService;
    @Autowired
    private ApiLogService apiLogService;
    @Value("${erp.switch.ErpReportOld}")
    private boolean erpReportOld;
    @Value("${erp.address.URL:}")
    private String erpUrl;
    @Value("${erp.address.OutLockaddress:}")
    private String erpOutLockAddress;
    /**
     * 7.12 出库任务锁定。
     * WCS 出库任务开始后,WMS 向 ERP 上报 palletId、orderId;失败最多重试三次。
     */
    @Transactional(rollbackFor = Exception.class)
    public ReturnT<String> start(WrkMast source) {
        WrkMast wrkMast = wrkMastService.selectById(source.getWrkNo());
        if (wrkMast == null || !Long.valueOf(ERP_LOCK_REPORT_PENDING_WRK_STS).equals(wrkMast.getWrkSts())) {
            return SUCCESS;
        }
        if (!isErpReportEnabled()) {
            finishReport(wrkMast, false, getRetryTimes(wrkMast), "ERP reporting is disabled", false);
            return FAIL.setMsg("ERP reporting is disabled");
        }
        if (Cools.isEmpty(erpOutLockAddress)) {
            return failWithoutCallingErp(wrkMast, "ERP出库任务锁定上报地址未配置");
        }
        List<WrkDetl> wrkDetls = wrkDetlService.selectByWrkNo(wrkMast.getWrkNo());
        ErpOutTaskLockReportParam param = buildParam(wrkMast, wrkDetls);
        String validateMsg = validateParam(param);
        if (!Cools.isEmpty(validateMsg)) {
            return failWithoutCallingErp(wrkMast, validateMsg);
        }
        String request = JSON.toJSONString(param);
        String response = "";
        boolean success = false;
        String errorMsg = null;
        String requestUrl = buildRequestUrl();
        try {
            response = new HttpHandler.Builder()
                    .setUri(erpUrl)
                    .setPath(erpOutLockAddress)
                    .setJson(request)
                    .build()
                    .doPost();
            success = isErpReportSuccess(response);
            if (!success) {
                errorMsg = extractErrorMsg(response);
            }
            finishReport(wrkMast, success, getRetryTimes(wrkMast), errorMsg, true);
        } catch (Exception e) {
            errorMsg = e.getMessage();
            finishReport(wrkMast, false, getRetryTimes(wrkMast), errorMsg, true);
        } finally {
            try {
                apiLogService.save(
                        "Outbound ERP Lock Report",
                        requestUrl,
                        null,
                        "127.0.0.1",
                        request,
                        response,
                        success,
                        "workNo=" + wrkMast.getWrkNo()
                );
            } catch (Exception logEx) {
                log.error("save outbound erp lock api log failed", logEx);
            }
        }
        if (success) {
            return SUCCESS;
        }
        return FAIL.setMsg(errorMsg);
    }
    private ReturnT<String> failWithoutCallingErp(WrkMast wrkMast, String errorMsg) {
        finishReport(wrkMast, false, getRetryTimes(wrkMast), errorMsg, true);
        return FAIL.setMsg(errorMsg);
    }
    private boolean isErpReportEnabled() {
        if (!erpReportOld) {
            return false;
        }
        String erpReport = Parameter.get().getErpReport();
        return Cools.isEmpty(erpReport) || "true".equalsIgnoreCase(erpReport);
    }
    private ErpOutTaskLockReportParam buildParam(WrkMast wrkMast, List<WrkDetl> wrkDetls) {
        ErpOutTaskLockReportParam param = new ErpOutTaskLockReportParam();
        param.setPalletId(resolvePalletId(wrkMast, wrkDetls));
        param.setOrderId(resolveOrderId(wrkMast, wrkDetls));
        param.setStartTime(formatDate(new Date()));
        return param;
    }
    private String formatDate(Date date) {
        if (date == null) {
            return null;
        }
        return new SimpleDateFormat(DATE_TIME_PATTERN).format(date);
    }
    private String resolvePalletId(WrkMast wrkMast, List<WrkDetl> wrkDetls) {
        if (!Cools.isEmpty(wrkMast.getBarcode())) {
            return wrkMast.getBarcode();
        }
        if (wrkDetls == null) {
            return null;
        }
        for (WrkDetl wrkDetl : wrkDetls) {
            if (!Cools.isEmpty(wrkDetl.getZpallet())) {
                return wrkDetl.getZpallet();
            }
        }
        return null;
    }
    private String resolveOrderId(WrkMast wrkMast, List<WrkDetl> wrkDetls) {
        if (wrkDetls != null) {
            for (WrkDetl wrkDetl : wrkDetls) {
                if (!Cools.isEmpty(wrkDetl.getOrderNo())) {
                    return wrkDetl.getOrderNo();
                }
            }
        }
        return wrkMast.getUserNo();
    }
    private String validateParam(ErpOutTaskLockReportParam param) {
        if (param == null || Cools.isEmpty(param.getPalletId())) {
            return "ERP出库任务锁定上报失败:palletId为空";
        }
        if (Cools.isEmpty(param.getOrderId())) {
            return "ERP出库任务锁定上报失败:orderId为空";
        }
        return null;
    }
    private boolean isErpReportSuccess(String response) {
        if (Cools.isEmpty(response)) {
            return false;
        }
        try {
            JSONObject jsonObject = JSON.parseObject(response);
            if (jsonObject == null) {
                return false;
            }
            Boolean success = jsonObject.getBoolean("success");
            if (success != null) {
                return success;
            }
            Integer code = jsonObject.getInteger("code");
            if (code != null) {
                return code == 200 || code == 0;
            }
            Integer status = jsonObject.getInteger("status");
            if (status != null) {
                return status == 200 || status == 0;
            }
            String result = jsonObject.getString("result");
            if (!Cools.isEmpty(result)) {
                return "success".equalsIgnoreCase(result) || "ok".equalsIgnoreCase(result) || "true".equalsIgnoreCase(result);
            }
        } catch (Exception ignore) {
            return response.toLowerCase().contains("success") && response.toLowerCase().contains("true");
        }
        return false;
    }
    private String extractErrorMsg(String response) {
        if (Cools.isEmpty(response)) {
            return "empty erp response";
        }
        try {
            JSONObject jsonObject = JSON.parseObject(response);
            if (jsonObject == null) {
                return response;
            }
            String[] keys = new String[]{"msg", "message", "error", "errMsg"};
            for (String key : keys) {
                String value = jsonObject.getString(key);
                if (!Cools.isEmpty(value)) {
                    return value;
                }
            }
        } catch (Exception ignore) {
        }
        return response;
    }
    private int getRetryTimes(WrkMast wrkMast) {
        if (wrkMast.getExpTime() == null) {
            return 0;
        }
        return wrkMast.getExpTime().intValue();
    }
    private void finishReport(WrkMast wrkMast, boolean success, int currentRetryTimes, String errorMsg, boolean countCurrentAttempt) {
        int retryTimes = currentRetryTimes + (countCurrentAttempt ? 1 : 0);
        Date now = new Date();
        wrkMast.setExpTime((double) retryTimes);
        wrkMast.setModiTime(now);
        if (success) {
            wrkMast.setWrkSts(ERP_LOCK_REPORT_SUCCESS_WRK_STS);
            wrkMast.setLogMk(ERP_LOCK_REPORT_SUCCESS_FLAG);
            wrkMast.setLogErrMemo(null);
            wrkMast.setLogErrTime(null);
        } else {
            wrkMast.setLogErrMemo(truncate(errorMsg, 500));
            wrkMast.setLogErrTime(now);
            if (retryTimes >= ERP_LOCK_REPORT_MAX_RETRY_TIMES || !countCurrentAttempt) {
                wrkMast.setWrkSts(ERP_LOCK_REPORT_FAIL_WRK_STS);
                wrkMast.setLogMk(ERP_LOCK_REPORT_FAIL_FLAG);
            } else {
                wrkMast.setWrkSts(ERP_LOCK_REPORT_PENDING_WRK_STS);
                wrkMast.setLogMk(ERP_LOCK_REPORT_PENDING_FLAG);
            }
        }
        if (!wrkMastService.updateById(wrkMast)) {
            throw new IllegalStateException("update outbound erp lock report status failed, workNo=" + wrkMast.getWrkNo());
        }
    }
    private String truncate(String message, int maxLength) {
        if (message == null || message.length() <= maxLength) {
            return message;
        }
        return message.substring(0, maxLength);
    }
    private String buildRequestUrl() {
        if (Cools.isEmpty(erpUrl)) {
            return erpOutLockAddress;
        }
        if (erpOutLockAddress == null) {
            return erpUrl;
        }
        if (erpUrl.endsWith("/") && erpOutLockAddress.startsWith("/")) {
            return erpUrl + erpOutLockAddress.substring(1);
        }
        if (!erpUrl.endsWith("/") && !erpOutLockAddress.startsWith("/")) {
            return erpUrl + "/" + erpOutLockAddress;
        }
        return erpUrl + erpOutLockAddress;
    }
}
src/main/java/com/zy/common/web/WcsController.java
@@ -463,10 +463,7 @@
            // 源站点状态检测
            BasDevp sourceStaNo = basDevpService.checkSiteStatus(devpNo, true);
            // 检索库位
            List<String> matnrs = waitPakins.stream().map(WaitPakin::getMatnr).distinct().collect(Collectors.toList());
            List<String> batchs = waitPakins.stream().map(WaitPakin::getBatch).distinct().collect(Collectors.toList());
            FindLocNoAttributeVo findLocNoAttributeVo = new FindLocNoAttributeVo(matnrs.get(0), batchs.get(0));
//        FindLocNoAttributeVo findLocNoAttributeVo = new FindLocNoAttributeVo(waitPakins.get(0));
            FindLocNoAttributeVo findLocNoAttributeVo = new FindLocNoAttributeVo(waitPakins.get(0));
            // IoT 指定了目标库位时优先尝试该库位;不可用时再退回现有自动找位规则。
            StartupDto dto = buildPreferredStartupDto(devpNo, extractPreferredInboundLoc(waitPakins));
            if (dto == null) {
src/main/resources/application.yml
@@ -98,6 +98,10 @@
    Inaddress: /api/Service/InPalletCompleted
    #出库上报
    Outaddress: /api/Service/OutPalletCompleted
    #出库异常上报
    OutErroraddress: /api/Service/OutPalletAbnormal
    #出库任务锁定上报
    OutLockaddress: /api/Service/OutPalletLocked
#wcs任务下发
wcs:
src/main/resources/mapper/WrkMastLogMapper.xml
@@ -27,6 +27,7 @@
        <result column="upd_mk" property="updMk" />
        <result column="exit_mk" property="exitMk" />
        <result column="plt_type" property="pltType" />
        <result column="batch_seq" property="batchSeq" />
        <result column="empty_mk" property="emptyMk" />
        <result column="io_time" property="ioTime" />
        <result column="ctn_type" property="ctnType" />
src/main/resources/mapper/WrkMastMapper.xml
@@ -26,6 +26,7 @@
        <result column="upd_mk" property="updMk" />
        <result column="exit_mk" property="exitMk" />
        <result column="plt_type" property="pltType" />
        <result column="batch_seq" property="batchSeq" />
        <result column="empty_mk" property="emptyMk" />
        <result column="io_time" property="ioTime" />
        <result column="ctn_type" property="ctnType" />
src/main/resources/sql/20260410_zhongyang_batch_seq.sql
New file
@@ -0,0 +1,23 @@
IF COL_LENGTH('dbo.asr_wrk_mast', 'batch_seq') IS NULL
BEGIN
    ALTER TABLE [dbo].[asr_wrk_mast]
        ADD [batch_seq] NVARCHAR(100) NULL;
END
ELSE
BEGIN
    ALTER TABLE [dbo].[asr_wrk_mast]
        ALTER COLUMN [batch_seq] NVARCHAR(100) NULL;
END
GO
IF COL_LENGTH('dbo.asr_wrk_mast_log', 'batch_seq') IS NULL
BEGIN
    ALTER TABLE [dbo].[asr_wrk_mast_log]
        ADD [batch_seq] NVARCHAR(100) NULL;
END
ELSE
BEGIN
    ALTER TABLE [dbo].[asr_wrk_mast_log]
        ALTER COLUMN [batch_seq] NVARCHAR(100) NULL;
END
GO
src/main/resources/sql/20260411_out_task_lock_status.sql
New file
@@ -0,0 +1,18 @@
IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'dbo.asr_bas_wrk_status') AND name = N'wrk_sts')
BEGIN
    PRINT 'asr_bas_wrk_status not found, skip 7.12 status initialization';
END
ELSE
BEGIN
    IF NOT EXISTS (SELECT 1 FROM asr_bas_wrk_status WHERE wrk_sts = 21)
    BEGIN
        INSERT INTO asr_bas_wrk_status (wrk_sts, wrk_desc, modi_time, appe_time)
        VALUES (21, N'给ERP开始任务下发成功', GETDATE(), GETDATE());
    END
    IF NOT EXISTS (SELECT 1 FROM asr_bas_wrk_status WHERE wrk_sts = 22)
    BEGIN
        INSERT INTO asr_bas_wrk_status (wrk_sts, wrk_desc, modi_time, appe_time)
        VALUES (22, N'给ERP开始任务下发失败', GETDATE(), GETDATE());
    END
END
src/test/java/com/zy/common/service/CommonServiceRun2AllocationTest.java
File was deleted