自动化立体仓库 - WMS系统
zwl
8 天以前 3971255dadf007563d83d887e8c2ef465242fc87
双伸出库改成订单出库
3个文件已修改
244 ■■■■ 已修改文件
src/main/java/com/zy/api/service/impl/WcsApiServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/OpenServiceImpl.java 179 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/WorkServiceImpl.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/api/service/impl/WcsApiServiceImpl.java
@@ -687,6 +687,11 @@
        }
        WrkMast mast = wrkMastService.selectOne(new EntityWrapper<WrkMast>().eq("wrk_no", params.getSuperTaskNo()));
        if (Objects.isNull(mast)) {
            // pakoutOrderPause 中止时,WMS 在 WCS 确认取消后会立即本地取消并归档任务。
            // 如果 WCS 后续又补发 task_cancel 回调,此时当前工作档已不存在,按幂等成功处理。
            if ("task".equalsIgnoreCase(params.getNotifyType()) && "task_cancel".equalsIgnoreCase(params.getMsgType())) {
                return R.ok();
            }
            throw new CoolException("任务档不存在!!");
        }
src/main/java/com/zy/asrs/service/impl/OpenServiceImpl.java
@@ -465,7 +465,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 +487,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 +499,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");
@@ -1737,38 +1747,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 +1779,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 +1940,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 +2069,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 + "正在出库,无法修改单据");
        }
    }
src/main/java/com/zy/asrs/service/impl/WorkServiceImpl.java
@@ -76,6 +76,8 @@
    @Autowired
    private OrderDetlService orderDetlService;
    @Autowired
    private OrderDetlPakoutService orderDetlPakoutService;
    @Autowired
    private ConfigService configService;
    @Autowired
    private WcsController wcsController;
@@ -1534,6 +1536,7 @@
                    throw new CoolException("保存数据失败");
                }
            }
            rollbackPakoutOrderWorkQty(wrkMast, wrkDetls, userId, now);
            for (WrkDetl wrkDetl : wrkDetls) {
//                if (!Cools.isEmpty(wrkDetl.getOrderNo())) {
////                    if (!orderDetlService.decrease(wrkDetl.getOrderNo(), wrkDetl.getMatnr(), wrkDetl.getBatch(), wrkDetl.getAnfme())) {
@@ -1621,6 +1624,63 @@
        }
    }
    /**
     * 出库任务取消时回滚出库订单明细的任务生成数量。
     *
     * 延迟出库订单中:
     * - work_qty 表示已经生成任务的数量;
     * - qty 表示已经完成出库的数量。
     *
     * WCS 取消或本地取消任务后,任务会被移入历史档并删除当前工作档。
     * 如果不回滚 work_qty,后续同一订单再次下发时系统会误认为这些明细仍已生成任务。
     */
    private void rollbackPakoutOrderWorkQty(WrkMast wrkMast, List<WrkDetl> wrkDetls, Long userId, Date now) {
        if (wrkMast == null || Cools.isEmpty(wrkDetls)) {
            return;
        }
        for (WrkDetl wrkDetl : wrkDetls) {
            if (wrkDetl == null || Cools.isEmpty(wrkDetl.getOrderNo())) {
                continue;
            }
            OrderDetlPakout orderDetl = findPakoutOrderDetlForRollback(wrkMast, wrkDetl);
            if (orderDetl == null) {
                continue;
            }
            double oldWorkQty = safeDouble(orderDetl.getWorkQty());
            double rollbackQty = safeDouble(wrkDetl.getAnfme());
            double completedQty = safeDouble(orderDetl.getQty());
            double newWorkQty = Math.max(completedQty, oldWorkQty - rollbackQty);
            orderDetl.setWorkQty(newWorkQty);
            orderDetl.setUpdateBy(userId);
            orderDetl.setUpdateTime(now);
            if (!orderDetlPakoutService.updateById(orderDetl)) {
                throw new CoolException("出库任务取消回滚订单明细作业数量失败:" + wrkDetl.getOrderNo());
            }
        }
    }
    private OrderDetlPakout findPakoutOrderDetlForRollback(WrkMast wrkMast, WrkDetl wrkDetl) {
        String palletId = null;
        if (wrkDetl != null && !Cools.isEmpty(wrkDetl.getZpallet())) {
            palletId = wrkDetl.getZpallet();
        } else if (wrkMast != null) {
            palletId = wrkMast.getBarcode();
        }
        if (!Cools.isEmpty(palletId)) {
            OrderDetlPakout orderDetl = orderDetlPakoutService.selectItemByOrderNoAndPallet(wrkDetl.getOrderNo(), palletId);
            if (orderDetl != null) {
                return orderDetl;
            }
        }
        return orderDetlPakoutService.selectItem(wrkDetl.getOrderNo(), wrkDetl.getMatnr(), wrkDetl.getBatch(), wrkDetl.getBrand(),
                wrkDetl.getStandby1(), wrkDetl.getStandby2(), wrkDetl.getStandby3(),
                wrkDetl.getBoxType1(), wrkDetl.getBoxType2(), wrkDetl.getBoxType3());
    }
    private double safeDouble(Double value) {
        return value == null ? 0.0D : value;
    }
    @Override
    @Transactional
    public void pickWrkMast(String workNo, Long userId) {