自动化立体仓库 - WMS系统
zwl
1 天以前 9145f8a44c6ae733019e43c775cc30243032e502
src/main/java/com/zy/asrs/service/impl/OpenServiceImpl.java
@@ -51,6 +51,8 @@
    private static final Map<Integer, BigDecimal> INBOUND_WEIGHT_FACTOR_BY_SOURCE_STA;
    private static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
    private static final int DEFAULT_OUT_ORDER_BATCH_PRIORITY = 100;
    private static final int DEFAULT_OUT_ORDER_BATCH_PRIORITY_THRESHOLD = 1;
    // ERP 出库口大于该阈值时,/outOrder 只落出库订单,由定时器后续生成任务。
    private static final int PENDING_OUT_ORDER_STATION_THRESHOLD = 600;
    // 延迟出库订单使用独立单据类型,便于和人工/页面创建的出库单区分来源。
@@ -465,7 +467,7 @@
        }
        if (Objects.equals(param.getExecute(), 2)) {
            // execute=2 先关闭订单开关,阻止定时器继续为未生成任务的明细建 WrkMast。
            // 已经生成的任务按状态分流:11 尚未下发,走本地取消;12/13 已下发或执行中,需要通知 WCS 暂停。
            // 已经生成的任务按状态分流:11 尚未下发,走本地取消;12/13 已下发或执行中,需要通知 WCS 取消。
            Map<String, Object> result = new HashMap<>();
            result.put("orderNo", param.getOrderId());
            result.put("execute", param.getExecute());
@@ -487,6 +489,7 @@
                return R.ok("出库订单已中止").add(result);
            }
            List<HashMap<String,Object>> taskList = new ArrayList<>();
            List<WrkMast> wcsCancelTasks = new ArrayList<>();
            int cancelledLocalTaskCount = 0;
            for (WrkMast wrkMast : activeTasks) {
                HashMap<String,Object> hashMap = new HashMap<>();
@@ -498,14 +501,23 @@
                    continue;
                }
                taskList.add(hashMap);
                wcsCancelTasks.add(wrkMast);
            }
            int cancelledWcsTaskCount = 0;
            if (!taskList.isEmpty()) {
                // wrk_sts=12/13 等已进入 WCS 侧的任务必须校验 WCS 返回,失败则事务回滚,避免本地误报已暂停。
                // wrk_sts=12/13 等已进入 WCS 侧的任务必须校验 WCS 返回,失败则事务回滚,避免本地误报已取消。
                R wcsR = wcsApiService.pauseOutTasks(taskList);
                requireWcsPauseOk(wcsR);
                // WCS 已确认取消后,本地也要取消任务并回滚订单明细 work_qty。
                // 回滚逻辑在 WorkService.cancelWrkMast 中统一处理,确保 WCS 回调 task_cancel 走同一套口径。
                for (WrkMast wrkMast : wcsCancelTasks) {
                    workService.cancelWrkMast(wrkMast.getWrkNo()+"", 9955L);
                    cancelledWcsTaskCount++;
                }
            }
            result.put("cancelledLocalTaskCount", cancelledLocalTaskCount);
            result.put("pausedWcsTaskCount", taskList.size());
            result.put("cancelledWcsTaskCount", cancelledWcsTaskCount);
            return R.ok("出库订单已中止").add(result);
        }
        throw new CoolException("reason仅支持1或2");
@@ -1615,7 +1627,7 @@
        wrkMast.setIoTime(now);
        wrkMast.setWrkSts(11L); // 工作状态:11.生成出库ID
        wrkMast.setIoType(ioType); // 入出库状态
        wrkMast.setIoPri(13D); // 优先级:13
        wrkMast.setIoPri(Double.valueOf(i)); // 优先级
        wrkMast.setCrnNo(locMast.getCrnNo());
        wrkMast.setSourceStaNo(staDesc.getCrnStn()); // 源站
        wrkMast.setStaNo(staDesc.getStnNo()); // 目标站
@@ -1626,9 +1638,9 @@
        wrkMast.setExitMk("N"); // 退出
        wrkMast.setEmptyMk("N"); // 空板
        wrkMast.setLinkMis("N");
        wrkMast.setPdcType("N");
        wrkMast.setContainerNo(param.getContainerNo());
        wrkMast.setTeu(teu);
        wrkMast.setPdcType(locMast.getCrnNo()>=19?"Y":"N");//自动任务下发标记
        wrkMast.setPlateNo(param.getPlateNo());
        wrkMast.setTrainNo(param.getTrainNo());
        wrkMast.setFreqType(param.getFreqType());
@@ -1700,23 +1712,39 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public R outOrderBatch(Map<String, List<OutTaskParam>> linesByBatchSeq,int count) {
        int i = 0;
        for (Map.Entry<String, List<OutTaskParam>> entry : linesByBatchSeq.entrySet()) {
            int i = DEFAULT_OUT_ORDER_BATCH_PRIORITY;
            int j = 0;
            int priorityThreshold = getOutOrderBatchPriorityThreshold();
            for (OutTaskParam outTaskParam : entry.getValue()) {
                if(outTaskParam.getSeq()!=0){
                    i= outTaskParam.getSeq();
                }else{
                    i++;
                }
                int teu = Cools.isEmpty(outTaskParam.getTeu())?0:outTaskParam.getTeu();
                R r = outOrder(outTaskParam, count, teu ,i);
                if (!Objects.equals(r.get("code"), 200)) {
                    throw new CoolException("出库建单失败");
                }
                j++;
                if (j >= priorityThreshold) {
                    i--;
                    j = 0;
                }
            }
        }
        return R.ok();
    }
    private int getOutOrderBatchPriorityThreshold() {
        Parameter parameter = Parameter.get();
        if (parameter == null || Cools.isEmpty(parameter.getOutOrderBatchPriorityThreshold())) {
            return DEFAULT_OUT_ORDER_BATCH_PRIORITY_THRESHOLD;
        }
        try {
            int threshold = Integer.parseInt(parameter.getOutOrderBatchPriorityThreshold().trim());
            return threshold <= 0 ? DEFAULT_OUT_ORDER_BATCH_PRIORITY_THRESHOLD : threshold;
        } catch (NumberFormatException ignored) {
            return DEFAULT_OUT_ORDER_BATCH_PRIORITY_THRESHOLD;
        }
    }
    @Override
@@ -1737,38 +1765,21 @@
        Date now = new Date();
        int orderCount = 0;
        int detailCount = 0;
        int removedUndispatchedDetailCount = 0;
        List<String> orderNos = new ArrayList<>();
        for (Map.Entry<String, List<OutTaskParam>> entry : paramsByOrderNo.entrySet()) {
            String orderNo = entry.getKey();
            // 延迟建单允许覆盖“尚未生成任务、未进入执行”的旧订单;
            // 一旦已有活动任务或历史任务,则禁止覆盖,避免重复出库同一订单。
            // 延迟建单允许同一个 orderNo 再次下发:
            // 已下发/已完成明细保留追溯;未下发明细删除后,使用本次接口参数重新插入。
            assertPendingPakoutOrderCanReplace(orderNo);
            OrderPakout order = orderPakoutService.selectByNo(orderNo);
            if (order != null) {
                orderPakoutService.remove(order.getId());
            }
            DocType docType = docTypeService.selectOrAdd(OUT_ORDER_PENDING_DOC_TYPE, Boolean.FALSE);
            order = new OrderPakout();
            order.setUuid(String.valueOf(snowflakeIdWorker.nextId()));
            order.setOrderNo(orderNo);
            order.setOrderTime(DateUtils.convert(now));
            order.setDocType(docType.getDocId());
            // settle=1 表示待生成任务;生成过至少一个进仓编号批次后置为 2。
            // status=1 是启动开关,pakoutOrderPause(execute=2) 会置 0 阻止定时器继续生成。
            order.setSettle(1L);
            order.setStatus(1);
            order.setCreateBy(9527L);
            order.setCreateTime(now);
            order.setUpdateBy(9527L);
            order.setUpdateTime(now);
            // moveStatus 保留现有“备货/移库”语义,这里不复用它做暂停开关。
            order.setMoveStatus(0);
            // 2 表示出库方向,和 man_order_detl_pakout 明细保持一致。
            order.setPakinPakoutStatus(2);
            if (!orderPakoutService.insert(order)) {
                throw new CoolException("生成出库订单失败:" + orderNo);
            if (order == null) {
                order = createPendingPakoutOrderHeader(orderNo, now);
            } else {
                assertNoNonReplaceablePendingDetailConflict(order, entry.getValue());
                removedUndispatchedDetailCount += removeUndispatchedPendingDetails(order.getId());
                refreshPendingPakoutOrderForResubmit(order, now);
            }
            for (OutTaskParam param : entry.getValue()) {
@@ -1786,6 +1797,7 @@
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("orderCount", orderCount);
        result.put("detailCount", detailCount);
        result.put("removedUndispatchedDetailCount", removedUndispatchedDetailCount);
        result.put("orderNos", orderNos);
        return R.ok("出库订单生成成功").add(result);
    }
@@ -1946,12 +1958,126 @@
        }
    }
    private OrderPakout createPendingPakoutOrderHeader(String orderNo, Date now) {
        DocType docType = docTypeService.selectOrAdd(OUT_ORDER_PENDING_DOC_TYPE, Boolean.FALSE);
        OrderPakout order = new OrderPakout();
        order.setUuid(String.valueOf(snowflakeIdWorker.nextId()));
        order.setOrderNo(orderNo);
        order.setOrderTime(DateUtils.convert(now));
        order.setDocType(docType.getDocId());
        // settle=1 表示待生成任务;生成过至少一个进仓编号批次后置为 2。
        // status=1 是启动开关,pakoutOrderPause(execute=2) 会置 0 阻止定时器继续生成。
        order.setSettle(1L);
        order.setStatus(1);
        order.setCreateBy(9527L);
        order.setCreateTime(now);
        order.setUpdateBy(9527L);
        order.setUpdateTime(now);
        // moveStatus 保留现有“备货/移库”语义,这里不复用它做暂停开关。
        order.setMoveStatus(0);
        // 2 表示出库方向,和 man_order_detl_pakout 明细保持一致。
        order.setPakinPakoutStatus(2);
        if (!orderPakoutService.insert(order)) {
            throw new CoolException("生成出库订单失败:" + orderNo);
        }
        return order;
    }
    private void refreshPendingPakoutOrderForResubmit(OrderPakout order, Date now) {
        if (order == null) {
            return;
        }
        boolean hasDispatchedDetail = hasDispatchedPendingDetail(order.getId());
        order.setStatus(1);
        order.setSettle(hasDispatchedDetail ? 2L : 1L);
        order.setUpdateBy(9527L);
        order.setUpdateTime(now);
        order.setPakinPakoutStatus(2);
        if (!orderPakoutService.updateById(order)) {
            throw new CoolException("更新出库订单失败:" + order.getOrderNo());
        }
    }
    private boolean hasDispatchedPendingDetail(Long orderId) {
        List<OrderDetlPakout> details = orderDetlPakoutService.selectList(new EntityWrapper<OrderDetlPakout>()
                .eq("order_id", orderId)
                .eq("status", 1));
        if (Cools.isEmpty(details)) {
            return false;
        }
        for (OrderDetlPakout detail : details) {
            if (!isUndispatchedPendingDetail(detail)) {
                return true;
            }
        }
        return false;
    }
    /**
     * 删除同订单中尚未下发的明细。
     *
     * 判断口径:
     * - work_qty <= 0:尚未生成有效任务,或中止后任务已被取消并完成回滚。
     * - qty <= 0:没有业务完成量,删除后不会丢失已完成出库记录。
     */
    private int removeUndispatchedPendingDetails(Long orderId) {
        List<OrderDetlPakout> details = orderDetlPakoutService.selectList(new EntityWrapper<OrderDetlPakout>()
                .eq("order_id", orderId)
                .eq("status", 1));
        if (Cools.isEmpty(details)) {
            return 0;
        }
        int removed = 0;
        for (OrderDetlPakout detail : details) {
            if (!isUndispatchedPendingDetail(detail)) {
                continue;
            }
            if (!orderDetlPakoutService.deleteById(detail.getId())) {
                throw new CoolException("删除未下发出库订单明细失败:" + detail.getOrderNo());
            }
            removed++;
        }
        return removed;
    }
    private void assertNoNonReplaceablePendingDetailConflict(OrderPakout order, List<OutTaskParam> params) {
        if (order == null || Cools.isEmpty(params)) {
            return;
        }
        Set<String> newPalletIds = params.stream()
                .filter(Objects::nonNull)
                .map(OutTaskParam::getPalletId)
                .filter(palletId -> !Cools.isEmpty(palletId))
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (newPalletIds.isEmpty()) {
            return;
        }
        List<OrderDetlPakout> details = orderDetlPakoutService.selectList(new EntityWrapper<OrderDetlPakout>()
                .eq("order_id", order.getId())
                .eq("status", 1));
        if (Cools.isEmpty(details)) {
            return;
        }
        for (OrderDetlPakout detail : details) {
            if (detail == null || !newPalletIds.contains(detail.getPalletId()) || isUndispatchedPendingDetail(detail)) {
                continue;
            }
            throw new CoolException("托盘「" + detail.getPalletId() + "」已存在已下发或已完成出库明细,无法覆盖");
        }
    }
    private boolean isUndispatchedPendingDetail(OrderDetlPakout detail) {
        return detail != null
                && safeDouble(detail.getWorkQty()) <= 0.0D
                && safeDouble(detail.getQty()) <= 0.0D;
    }
    /**
     * 判断延迟出库订单是否还能被当前接口请求覆盖。
     *
     * 允许覆盖的边界只限于“已建订单但尚未生成任何任务”的情况。
     * 只要 WrkMast 或 WrkMastLog 已存在,就说明订单已经进入执行链路或历史链路,
     * 再覆盖会破坏任务、库存和 ERP 回传之间的可追溯关系。
     * 活动任务仍然存在时不允许覆盖,因为任务、库位、WCS 指令还在执行链路上。
     * 已经取消并归档到 WrkMastLog 的任务允许重下发;其订单明细 work_qty 已在取消时回滚,
     * 本次接口会删除未下发明细并插入新明细。
     */
    private void assertPendingPakoutOrderCanReplace(String orderNo) {
        List<WrkMast> activeTasks = findActiveOutboundTasks(orderNo);
@@ -1961,14 +2087,11 @@
        int activeWrkCount = wrkMastService.selectCount(new EntityWrapper<WrkMast>()
                .eq("io_type", 101)
                .eq("user_no", orderNo));
        int historyWrkCount = wrkMastLogService.selectCount(new EntityWrapper<WrkMastLog>()
                .eq("io_type", 101)
                .eq("user_no", orderNo));
        if (activeWrkCount > 0 || historyWrkCount > 0) {
            throw new CoolException("出库订单已存在任务档或任务历史档,无法覆盖:" + orderNo);
        if (activeWrkCount > 0) {
            throw new CoolException("出库订单已存在任务档,无法覆盖:" + orderNo);
        }
        OrderPakout order = orderPakoutService.selectByNo(orderNo);
        if (order != null && order.getSettle() != null && order.getSettle() > 1L) {
        if (order != null && order.getSettle() != null && order.getSettle() > 2L) {
            throw new CoolException(orderNo + "正在出库,无法修改单据");
        }
    }