自动化立体仓库 - WMS系统
zwl
8 小时以前 87e99cdcfa55ee1a3b19a19506b05e4f554a416e
src/main/java/com/zy/asrs/controller/OpenController.java
@@ -524,35 +524,79 @@
    }
    /**
     * 7.11 出库通知单(传递有序无序规则)
     * 7.11 出库通知单(传递有序无序规则)。
     *
     * ERP 出库通知统一先落出库订单,不再由接口线程直接创建 WrkMast/WrkDetl。
     *
     * 关键规则:
     * 1. orderId 作为出库订单号落到 man_order_pakout.order_no。
     * 2. stationId > 600 时必须传 entryWmsCode,后续按 entryWmsCode 作为进仓编号分批生成任务。
     * 3. stationId <= 600 时允许 entryWmsCode 为空,后续生成任务时使用 orderId 作为批次键。
     * 4. 建单阶段只校验库存和满库位,不锁库位、不改 loc_sts,真正占库仍由定时生成任务时处理。
     *
     * 为什么这里不再直接调用 outOrderBatch:
     * - 直接建 WrkMast 会马上把库位从 F 改为 R,ERP 重发、人工中止、WCS 取消后的补偿都很难统一。
     * - 先落订单明细后,未生成任务的明细可以删除重下发;已生成任务的明细由取消逻辑回滚 work_qty。
     * - 低站点和高站点走同一条订单链路后,pakoutOrderPause/execute 才能同时控制“未生成”和“已生成”的任务。
     */
    @PostMapping("/outOrder")
    public synchronized R outOrder(@RequestBody ArrayList<OutTaskParam> params, HttpServletRequest request) {
        int count = params.size();
        if (Cools.isEmpty(params)) {
            return R.error("请求参数不能为空");
        }
        log.info("[outOrder] cache: {}", JSON.toJSONString(params));
        request.setAttribute("cache", params);
        Map<String, List<OutTaskParam>> linesByBatchSeq = new LinkedHashMap<>();
        // 统一做有序/无序校验的分组。
        //
        // 注意:这里的分组 key 不是 ERP 原始 batchSeq,而是“未来生成 WrkMast.batchSeq 的值”。
        // 这样可以在接口入口就校验最终任务批次里的 seq 是否连续,避免订单已经落库后定时器才失败。
        //
        // 批次键规则:
        // - 高站点按 orderId + entryWmsCode;
        // - 低站点按 orderId + orderId。
        Map<String, List<OutTaskParam>> linesByValidateKey = new LinkedHashMap<>();
        for (OutTaskParam outTaskParam : params) {
            if (Cools.isEmpty(outTaskParam) || Cools.isEmpty(outTaskParam.getOrderId())) {
                return R.error("出库单号不能为空");
            }
            if (Cools.isEmpty(outTaskParam.getBatchSeq())) {
                // batchSeq 仍保存 ERP 字段;未传时用 orderId 补齐,便于明细追溯。
                // 低站点真正生成任务时不使用该字段作为批次键,而是固定使用 orderId。
                outTaskParam.setBatchSeq(outTaskParam.getOrderId());
            }
            if (Cools.isEmpty(outTaskParam.getStationId())) {
                return R.error("托盘「" + outTaskParam.getPalletId() + "」出库口编码不能为空");
            }
            linesByBatchSeq.computeIfAbsent(outTaskParam.getBatchSeq(), k -> new ArrayList<>()).add(outTaskParam);
            Integer stationId;
            try {
                stationId = Integer.valueOf(outTaskParam.getStationId());
            } catch (NumberFormatException ex) {
                return R.error("托盘「" + outTaskParam.getPalletId() + "」出库口编码格式错误:" + outTaskParam.getStationId());
            }
            if (stationId > 600) {
                // 高站点任务必须带进仓编号;这是定时生成任务时的批次边界,
                // 也是 OutboundBatchSeqReleaseGuard 判断能否放行下一进仓编号的依据。
                // 低站点不做该限制,低站点没有进仓编号时会统一使用 orderId 作为批次键。
                if (Cools.isEmpty(outTaskParam.getEntryWmsCode())) {
                    return R.error("托盘「" + outTaskParam.getPalletId() + "」进仓编号不能为空");
                }
            }
            linesByValidateKey.computeIfAbsent(buildOutOrderValidateKey(outTaskParam), k -> new ArrayList<>()).add(outTaskParam);
        }
        for (Map.Entry<String, List<OutTaskParam>> entry : linesByBatchSeq.entrySet()) {
        // 仍保留原接口的有序/无序规则:
        // - seq=0 表示无序,同批次所有明细都必须为 0;
        // - seq>0 表示有序,同批次必须从 1 开始连续;
        // - seq=0 和 seq>0 不能混用。
        //
        // 所有明细虽然暂不生成任务,也要在建单前保证同一任务批次内的明细顺序合法,
        // 否则后续定时生成任务时才失败会更难追溯到 ERP 原始请求。
        for (Map.Entry<String, List<OutTaskParam>> entry : linesByValidateKey.entrySet()) {
            List<OutTaskParam> lines = entry.getValue();
            OutTaskParam head = lines.get(0);
            String oid = head.getOrderId();
            String batchSeq = head.getBatchSeq();
            String batchSeq = resolveOutOrderTaskBatchKey(head);
            boolean hasZero = false;
            boolean hasPositive = false;
            List<Integer> orderedSeqs = new ArrayList<>(lines.size());
@@ -583,8 +627,9 @@
            }
        }
        // 重新按校验分组顺序展开,保证重复托盘、库存、库位校验与上面的批次校验看到同一批数据。
        List<OutTaskParam> groupedParams = new ArrayList<>(params.size());
        for (List<OutTaskParam> lines : linesByBatchSeq.values()) {
        for (List<OutTaskParam> lines : linesByValidateKey.values()) {
            groupedParams.addAll(lines);
        }
@@ -616,6 +661,11 @@
//            }
//        }
        // 订单化后仍必须确认托盘有库存且当前库位为满库位。
        //
        // 这里只做准入校验,不在 controller 层提前锁库位:
        // - 如果这里提前改 loc_sts,会造成“订单未执行但库存已被占用”;
        // - 真正锁库位必须和 WrkMast/WrkDetl 创建在同一个事务里完成,所以仍由 outOrderBatch 处理。
        List<OutTaskParam> missingStock = Lists.newArrayList();
        List<OutTaskParam> missingLoc = Lists.newArrayList();
        for (OutTaskParam outTaskParam : groupedParams) {
@@ -645,8 +695,11 @@
            }
            return R.error("没有找到托盘码对应库位:" + String.join(",", badPalletIds)).add(missingLoc);
        }
        return openService.outOrderBatch(linesByBatchSeq,count);
        // ERP /outOrder 默认创建可执行订单:
        // - status=1:定时器允许扫描并生成任务;
        // - 生成任务后服务层会立即把 wrk_sts=11 的任务置为 pdcType=Y;
        // - 因此 ERP 侧仍保持“通知后自动出库”的体验,只是任务生成从接口线程转移到了订单调度链路。
        return openService.outOrderCreatePakoutOrder(groupedParams, true);
    }
    /**
@@ -694,8 +747,50 @@
        return openService.pakoutOrderPause(param);
    }
    private String buildOutOrderBatchKey(OutTaskParam param) {
        return param.getOrderId() + "#" + param.getBatchSeq();
    /**
     * 公开执行接口。
     *
     * IoT/MQTT 等非 ERP 入口会先创建 status=0 的出库订单,只有调用该接口后才允许生成任务和下发。
     * 该接口只支持 execute=1;中止仍走 /order/pakout/pause/default/v1 的 execute=2。
     */
    @PostMapping("/order/pakout/execute/default/v1")
    public synchronized R pakoutOrderExecute(@RequestBody OpenOrderPakoutExecuteParam param, HttpServletRequest request) {
        if (request != null) {
            log.info("[pakoutOrderExecute] cache: {}", param == null ? "null" : JSON.toJSONString(param));
            request.setAttribute("cache", param);
        }
        if (Cools.isEmpty(param) || Cools.isEmpty(param.getOrderId())) {
            return R.error("orderNo is empty");
        }
        return openService.pakoutOrderExecute(param);
    }
    private String buildOutOrderValidateKey(OutTaskParam param) {
        return param.getOrderId() + "#" + resolveOutOrderTaskBatchKey(param);
    }
    /**
     * 计算 /outOrder 入口的最终任务批次键。
     *
     * 这个值必须和 OpenServiceImpl.resolvePakoutTaskBatchSeq 保持一致:
     * - 高站点:entryWmsCode;
     * - 低站点:orderId。
     *
     * 入口校验和后续生成任务使用同一口径,才能保证 seq 校验、批次守卫和 WCS 下发批次一致。
     */
    private String resolveOutOrderTaskBatchKey(OutTaskParam param) {
        if (param != null && isPendingOutOrderStation(param.getStationId()) && !Cools.isEmpty(param.getEntryWmsCode())) {
            return param.getEntryWmsCode();
        }
        return param == null ? null : param.getOrderId();
    }
    private boolean isPendingOutOrderStation(String stationId) {
        try {
            return Integer.valueOf(stationId) > 600;
        } catch (Exception ignored) {
            return false;
        }
    }
    /**
@@ -736,8 +831,8 @@
            SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd");
            Calendar calendar = Calendar.getInstance();
            calendar.add(Calendar.DATE, -12);
            for(int i=0;i<12;i++) {
            calendar.add(Calendar.DATE, -7);
            for(int i=0;i<7;i++) {
                boolean flag = true;
                calendar.add(Calendar.DATE, 1);
                String str = sf.format(calendar.getTime());
@@ -790,8 +885,8 @@
            ArrayList<Number> data4 = new ArrayList<Number>();
            SimpleDateFormat sfCube = new SimpleDateFormat("yyyy-MM-dd");
            Calendar calendarCube = Calendar.getInstance();
            calendarCube.add(Calendar.DATE, -12);
            for (int i = 0; i < 12; i++) {
            calendarCube.add(Calendar.DATE, -7);
            for (int i = 0; i < 7; i++) {
                calendarCube.add(Calendar.DATE, 1);
                String str = sfCube.format(calendarCube.getTime());
                WorkCubeTotalAxis cubeAxis = cubeMap.get(str);
@@ -986,6 +1081,7 @@
        Integer sum = 0;
        if (ioType != null && ioType < 100) {
            supp = String.valueOf(resolveInboundSupp(wrkMast));
            map.put("supp", supp);
        }else {
            String[] split = wrkDetls.get(0).getSupp().split("/");
            if (split != null && split.length > 0) {
@@ -993,10 +1089,10 @@
            }else {
                sum = Integer.valueOf(wrkDetls.get(0).getSupp());
            }
            List<WrkMast> userNo = wrkMastService.selectList(new EntityWrapper<WrkMast>().eq("user_no", wrkMast.getUserNo()));
            suppCount = sum - userNo.size()+1;
            List<WrkMast> userNo = wrkMastService.selectList(new EntityWrapper<WrkMast>().eq("user_no", wrkMast.getUserNo()).in("wrk_sts",11,12,21,22,25));
            suppCount = sum - userNo.size();
            map.put("supp", suppCount + "/" + sum);
        }
        map.put("supp", suppCount + "/" + sum);
        //耗时
        Long costTime = resolveCostTime(wrkMast);