自动化立体仓库 - WMS系统
zwl
8 小时以前 87e99cdcfa55ee1a3b19a19506b05e4f554a416e
src/main/java/com/zy/asrs/controller/OpenController.java
@@ -37,6 +37,7 @@
    private static final boolean auth = true;
    private static final String  sign_arm_order = "|s|LABEL_";
    private static final String  sign_arm_sku = "|sku|LABEL_";
    private static final long MILLIS_PER_MINUTE = 60L * 1000L;
    public static final ArrayList<String> APP_KEY_LIST = new ArrayList<String>() {{
        add("ea1f0459efc02a79f046f982767939ae");
    }};
@@ -56,6 +57,8 @@
    @Autowired
    private MatService matService;
    @Autowired
    private LocMastService locMastService;
    @Autowired
    private ReportQueryMapper reportQueryMapper;
//    @PostMapping("/order/matSync/default/v1")
@@ -123,6 +126,23 @@
        return R.ok().add(openService.pakinOrderComplete(param));
    }
    /**
     * 托盘入库历史记录重报ERP
     */
    @PostMapping("/order/pakin/erp/report/v1")
//    @AppAuth(memo = "入库历史重报ERP")
    public synchronized R reportPakinHistoryToErp(@RequestBody(required = false) List<String> barcodes) {
//        auth(appkey, barcodes, request);
        if (Cools.isEmpty(barcodes)) {
            return R.parse(BaseRes.PARAM);
        }
        try {
            return openService.reportPakinHistoryToErp(barcodes);
        } catch (Exception e) {
            return R.error(e.getMessage());
        }
    }
@@ -151,6 +171,7 @@
    private void auth(String appkey, Object obj, HttpServletRequest request) {
        log.info("{}接口被访问;appkey:{};请求数据:{}", "open/sensorType/list/auth/v1", appkey, JSON.toJSONString(obj));
        log.info("[auth] cache: {}", obj == null ? "null" : JSON.toJSONString(obj));
        request.setAttribute("cache", obj);
        if (!auth) {
            return;
@@ -209,8 +230,8 @@
    @PostMapping("/arm/task/v1")
    @AppAuth(memo = "分拣线上报接收")
    public synchronized R TaskArmReport(@RequestHeader(required = false) String appkey,
                                           @RequestBody TaskArmReportParam param,
                                           HttpServletRequest request) {
                                        @RequestBody TaskArmReportParam param,
                                        HttpServletRequest request) {
        auth(appkey, param, request);
        if (Cools.isEmpty(param)) {
            return R.parse(BaseRes.PARAM);
@@ -254,8 +275,8 @@
    @PostMapping("/arm/task/cycle_result")
    @AppAuth(memo = "单码完成")
    public synchronized R TaskArmCycleResult(@RequestHeader(required = false) String appkey,
                                        @RequestBody TaskArmCycleResultParam param,
                                        HttpServletRequest request) {
                                             @RequestBody TaskArmCycleResultParam param,
                                             HttpServletRequest request) {
        auth(appkey, param, request);
        if (Cools.isEmpty(param)) {
            return R.parse(BaseRes.PARAM);
@@ -286,8 +307,8 @@
    @PostMapping("/arm/task/workspace_status")
    @AppAuth(memo = "托盘完成")
    public synchronized R TaskArmWorkspaceStatus(@RequestHeader(required = false) String appkey,
                                             @RequestBody TaskArmWorkspaceStatusParam param,
                                             HttpServletRequest request) {
                                                 @RequestBody TaskArmWorkspaceStatusParam param,
                                                 HttpServletRequest request) {
        auth(appkey, param, request);
        if (Cools.isEmpty(param)) {
            return R.parse(BaseRes.PARAM);
@@ -319,8 +340,8 @@
    @PostMapping("/arm/task/loc_status")
//    @AppAuth(memo = "托盘就绪状态查询")
    public synchronized R TaskArmLocStatus(@RequestHeader(required = false) String appkey,
                                                 @RequestBody ArmOKParam param,
                                                 HttpServletRequest request) {
                                           @RequestBody ArmOKParam param,
                                           HttpServletRequest request) {
        auth(appkey, param, request);
        if (Cools.isEmpty(param)) {
            return R.parse(BaseRes.PARAM);
@@ -346,8 +367,8 @@
    @PostMapping("/armAbnormalOperation")
//    @AppAuth(memo = "异常上报")
    public synchronized R ArmAbnormalOperation(@RequestHeader(required = false) String appkey,
                                                 @RequestBody TaskArmErrorParam param,
                                                 HttpServletRequest request) {
                                               @RequestBody TaskArmErrorParam param,
                                               HttpServletRequest request) {
        auth(appkey, param, request);
        if (Cools.isEmpty(param.getArm_no())) {
            return R.error("机械臂编号[Arm_no]不能为空");
@@ -396,8 +417,12 @@
    @PostMapping("/order/matSync/default/v2")
//    @AppAuth(memo = "商品信息同步接口")
    public synchronized R syncMatInfoV2(@RequestBody(required = false) List<MatSyncParam.MatParam> param){
    public synchronized R syncMatInfoV2(@RequestBody(required = false) List<MatSyncParam.MatParam> param,
                                        HttpServletRequest request) {
        if (request != null) {
            log.info("[syncMatInfoV2] cache: {}", param == null ? "null" : JSON.toJSONString(param));
            request.setAttribute("cache", param);
        }
        System.out.println(param);
        if (Cools.isEmpty(param)) {
            return R.parse(BaseRes.PARAM);
@@ -422,16 +447,44 @@
     * return
     */
    @PostMapping("/station/all")
    public synchronized R stationAll(){
    public synchronized R stationAll(HttpServletRequest request) {
        if (request != null) {
            String cachePayload = JSON.toJSONString(Collections.singletonMap("op", "stationAll"));
            log.info("[stationAll] cache: {}", cachePayload);
            request.setAttribute("cache", cachePayload);
        }
        return openService.stationAll();
    }
    /**
     * 组托信息下发
     * return
     * 7.3 组托信息下发
     */
    @PostMapping("/comb/auth")
    public synchronized R comb(@RequestBody ArrayList<MesToCombParam> param){
    public synchronized R comb(@RequestBody ArrayList<MesToCombParam> param, HttpServletRequest request) {
        if (request != null) {
            log.info("[comb] cache: {}", param == null ? "null" : JSON.toJSONString(param));
            request.setAttribute("cache", param);
        }
        if (Cools.isEmpty(param)) {
            return R.error("没有入库数据");
        }
        boolean boo =false;
        for (MesToCombParam mesToCombParam : param) {
            if (mesToCombParam.getOperateType()==2){
                int countLoc = locDetlService.selectCount(new EntityWrapper<LocDetl>().eq("zpallet", mesToCombParam.getPalletId()));
                int countWrk = wrkDetlService.selectCount(new EntityWrapper<WrkDetl>().eq("zpallet", mesToCombParam.getPalletId()));
                int countwait = waitPakinService.selectCount(new EntityWrapper<WaitPakin>().eq("zpallet",mesToCombParam.getPalletId()).eq("io_status", "Y"));
                if (countLoc > 0 || countWrk > 0 || countwait > 0) {
                    return R.error(mesToCombParam.getPalletId()+"-工作档/库存条码数据已存在,无法删除");
                }
                waitPakinService.delete(new EntityWrapper<WaitPakin>().eq("zpallet",mesToCombParam.getPalletId()));
                boo = true;
            }
        }
        if (boo){
            return R.ok();
        }
        List<MesToCombParam> errorComb = Lists.newArrayList();
        List<MesToCombParam> validComb = Lists.newArrayList();
        for (MesToCombParam mesToCombParam : param) {
@@ -471,21 +524,124 @@
    }
    /**
     * 出库通知单
     * 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){
    public synchronized R outOrder(@RequestBody ArrayList<OutTaskParam> params, HttpServletRequest request) {
        if (Cools.isEmpty(params)) {
            return R.error("请求参数不能为空");
        }
        Set<String> orderIds = new LinkedHashSet<>();
        log.info("[outOrder] cache: {}", JSON.toJSONString(params));
        request.setAttribute("cache", params);
        // 统一做有序/无序校验的分组。
        //
        // 注意:这里的分组 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("出库单号不能为空");
            }
            orderIds.add(outTaskParam.getOrderId());
            if (Cools.isEmpty(outTaskParam.getBatchSeq())) {
                // batchSeq 仍保存 ERP 字段;未传时用 orderId 补齐,便于明细追溯。
                // 低站点真正生成任务时不使用该字段作为批次键,而是固定使用 orderId。
                outTaskParam.setBatchSeq(outTaskParam.getOrderId());
            }
            if (Cools.isEmpty(outTaskParam.getStationId())) {
                return R.error("托盘「" + outTaskParam.getPalletId() + "」出库口编码不能为空");
            }
            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);
        }
        // 仍保留原接口的有序/无序规则:
        // - 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 = resolveOutOrderTaskBatchKey(head);
            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 + "」批次「" + batchSeq + "」序号不能为空");
                }
                if (line.getSeq() < 0) {
                    return R.error("出库单「" + oid + "」批次「" + batchSeq + "」序号不能小于0");
                }
                if (line.getSeq() == 0) {
                    hasZero = true;
                } else {
                    hasPositive = true;
                    orderedSeqs.add(line.getSeq());
                }
            }
            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 + "」序号不连贯");
                    }
                }
            }
        }
        // 重新按校验分组顺序展开,保证重复托盘、库存、库位校验与上面的批次校验看到同一批数据。
        List<OutTaskParam> groupedParams = new ArrayList<>(params.size());
        for (List<OutTaskParam> lines : linesByValidateKey.values()) {
            groupedParams.addAll(lines);
        }
        Set<String> seenPallet = new LinkedHashSet<>();
        for (OutTaskParam outTaskParam : groupedParams) {
            String pid = outTaskParam.getPalletId();
            String palletKey = pid == null ? "" : pid;
            if (!seenPallet.add(palletKey)) {
                return R.error("托盘号重复:" + (Cools.isEmpty(pid) ? "(空)" : pid));
            }
        }
//        if (!orderIds.isEmpty()) {
//            Set<String> existedOrderIds = new LinkedHashSet<>();
//            List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<WrkMast>().in("user_no", orderIds));
@@ -505,48 +661,152 @@
//            }
//        }
        List<OutTaskParam> errorOutOrders = Lists.newArrayList();
        List<OutTaskParam> validOutOrders = Lists.newArrayList();
        for (OutTaskParam outTaskParam : params) {
            // TODO:待測試,校驗庫存信息,不存在則返回
        // 订单化后仍必须确认托盘有库存且当前库位为满库位。
        //
        // 这里只做准入校验,不在 controller 层提前锁库位:
        // - 如果这里提前改 loc_sts,会造成“订单未执行但库存已被占用”;
        // - 真正锁库位必须和 WrkMast/WrkDetl 创建在同一个事务里完成,所以仍由 outOrderBatch 处理。
        List<OutTaskParam> missingStock = Lists.newArrayList();
        List<OutTaskParam> missingLoc = Lists.newArrayList();
        for (OutTaskParam outTaskParam : groupedParams) {
            int countLoc = locDetlService.selectCount(new EntityWrapper<LocDetl>().eq("zpallet", outTaskParam.getPalletId()));
            if (countLoc == 0){
                errorOutOrders.add(outTaskParam);
            if (countLoc == 0) {
                missingStock.add(outTaskParam);
                continue;
            }
            validOutOrders.add(outTaskParam);
        }
        for (OutTaskParam outTaskParam : validOutOrders) {
            R r = openService.outOrder(outTaskParam,validOutOrders.size());
            if (!r.get("code").equals(200)){
                return r;
            LocMast locMast = locMastService.selectOne(new EntityWrapper<LocMast>().eq("loc_sts", "F").eq("barcode", outTaskParam.getPalletId()));
            if (locMast == null) {
                missingLoc.add(outTaskParam);
            }
        }
        if(errorOutOrders.size() > 0) {
            return R.error("库存中不存在该托盘").add(errorOutOrders);
        if (!missingStock.isEmpty()) {
            List<String> missingPalletIds = new ArrayList<>(missingStock.size());
            for (OutTaskParam p : missingStock) {
                String pid = p.getPalletId();
                missingPalletIds.add(Cools.isEmpty(pid) ? "(空)" : pid);
            }
            return R.error("库存中不存在该托盘:" + String.join(",", missingPalletIds)).add(missingStock);
        }
        if (!missingLoc.isEmpty()) {
            List<String> badPalletIds = new ArrayList<>(missingLoc.size());
            for (OutTaskParam p : missingLoc) {
                String pid = p.getPalletId();
                badPalletIds.add(Cools.isEmpty(pid) ? "(空)" : pid);
            }
            return R.error("没有找到托盘码对应库位:" + String.join(",", badPalletIds)).add(missingLoc);
        }
        // ERP /outOrder 默认创建可执行订单:
        // - status=1:定时器允许扫描并生成任务;
        // - 生成任务后服务层会立即把 wrk_sts=11 的任务置为 pdcType=Y;
        // - 因此 ERP 侧仍保持“通知后自动出库”的体验,只是任务生成从接口线程转移到了订单调度链路。
        return openService.outOrderCreatePakoutOrder(groupedParams, true);
    }
        return R.ok();
    /**
     * 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")
    public synchronized R pakoutOrderPause(@RequestBody OpenOrderPakoutPauseParam param){
    public synchronized R pakoutOrderPause(@RequestBody OpenOrderPakoutPauseParam param, HttpServletRequest request) {
        if (request != null) {
            log.info("[pakoutOrderPause] 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.pakoutOrderPause(param);
    }
    /**
     * 公开执行接口。
     *
     * 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;
        }
    }
    /**
     * 推荐出库站点
     */
    @PostMapping("/pakoutStaNo")
    public synchronized R pakoutStaNo(@RequestBody List<String> barcodes) {
        String StaNo = "1,2,3,4,5";
        return R.ok().add(StaNo);
    }
    /*************************************电视机程序***********************************************/
    @GetMapping("/locDetl/statistics")
    public R locDetlStatistics(){
    public synchronized R locDetlStatistics(){
        HashMap<String, Object> param = new HashMap<>();
        Page<LocDetl> stockStatis = locDetlService.getStockStatis(toPage(1, 100, param, LocDetl.class));
        for (LocDetl locDetl : stockStatis.getRecords()) {
@@ -559,20 +819,20 @@
    }
    @GetMapping("/line/charts")
    public R locIoLineCharts(){
    public synchronized R locIoLineCharts(){
        Map<String,Object> map=new HashMap<String, Object>();
        List<AxisBean> list = new ArrayList<AxisBean>();
        List<WorkChartAxis> listChart = reportQueryMapper.getChartAxis();
        if(listChart!=null) {
            ArrayList<Integer> data1 = new ArrayList<Integer>();
            ArrayList<Integer> data2 = new ArrayList<Integer>();
            ArrayList<Number> data1 = new ArrayList<Number>();
            ArrayList<Number> data2 = new ArrayList<Number>();
            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());
@@ -590,17 +850,150 @@
                }
            }
            AxisBean inqty = new AxisBean();
            inqty.setName("入库数量");
            Integer[] array1 = new Integer[data1.size()];
            inqty.setName("入库托盘数");
            Number[] array1 = new Number[data1.size()];
            inqty.setData(data1.toArray(array1));
            list.add(inqty);
            AxisBean outqty = new AxisBean();
            outqty.setName("出库数量");
            Integer[] array2 = new Integer[data2.size()];
            outqty.setName("出库托盘数");
            Number[] array2 = new Number[data2.size()];
            outqty.setData(data2.toArray(array2));
            list.add(outqty);
            AxisBean  teu= new AxisBean();
            teu.setName("TEU");
            List<WorkTeuTotalAxis> workTeuTotalAxes = reportQueryMapper.getteuTotal();
            ArrayList<Number> data3 = new ArrayList<Number>();
            for (WorkTeuTotalAxis w : workTeuTotalAxes) {
                data3.add(w.getTeu_total());
            }
            Number[] array3 = new Number[data3.size()];
            teu.setData(data3.toArray(array3));
            list.add(teu);
            AxisBean cube5 = new AxisBean();
            cube5.setName("入库体积");
            List<WorkCubeTotalAxis> workCubeTotalAxes = reportQueryMapper.getInboundCubeTotal();
            Map<String, WorkCubeTotalAxis> cubeMap = new HashMap<String, WorkCubeTotalAxis>();
            if (workCubeTotalAxes != null) {
                for (WorkCubeTotalAxis w : workCubeTotalAxes) {
                    if (w.getYmd() != null) {
                        cubeMap.put(w.getYmd(), w);
                    }
                }
            }
            ArrayList<Number> data4 = new ArrayList<Number>();
            SimpleDateFormat sfCube = new SimpleDateFormat("yyyy-MM-dd");
            Calendar calendarCube = Calendar.getInstance();
            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);
                data4.add(cubeAxis == null || cubeAxis.getCube5Total() == null ? 0 : cubeAxis.getCube5Total());
            }
            Number[] array4 = new Number[data4.size()];
            cube5.setData(data4.toArray(array4));
            list.add(cube5);
        }
        map.put("rows",list);
        return R.ok(map);
    }
    /**
     * 入出库按小时折线:横轴为「当前整点起向前共 12 小时」滚动窗口,与库表 ymd(yyyy-MM-dd HH)对齐
     */
    @GetMapping("/line/charts/hourly")
    public synchronized R locIoLineChartsHourly() {
        Map<String, Object> map = new HashMap<>();
        List<AxisBean> list = new ArrayList<>();
        List<WorkChartAxis> listChart = reportQueryMapper.getChartAxisHourly();
        if (listChart == null) {
            listChart = Collections.emptyList();
        }
        ArrayList<Integer> data1 = new ArrayList<>();
        ArrayList<Integer> data2 = new ArrayList<>();
        ArrayList<Double> data3 = new ArrayList<>();
        ArrayList<Double> data4 = new ArrayList<>();
        ArrayList<Double> data5 = new ArrayList<>();
        List<String> categories = new ArrayList<>();
        final int n = 12;
        SimpleDateFormat sfKey = new SimpleDateFormat("yyyy-MM-dd HH");
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        calendar.add(Calendar.HOUR_OF_DAY, -(n - 1));
        for (int i = 0; i < n; i++) {
            String key = sfKey.format(calendar.getTime());
            categories.add(String.valueOf(calendar.get(Calendar.HOUR_OF_DAY)));
            int inV = 0;
            int outV = 0;
            double inC = 0;
            double outC = 0;
            double outD = 0;
            for (WorkChartAxis w : listChart) {
                if (w.getYmd() != null && key.equals(w.getYmd().trim())) {
                    inV = w.getInqty();
                    outV = w.getOutqty();
                    inC = w.getCubeInqty();
                    outC = w.getCubeOutqty();
                    outD = w.getOutTeu();
                    break;
                }
            }
            data1.add(inV);
            data2.add(outV);
            data3.add(inC);
            data4.add(outC);
            data5.add(outD);
            calendar.add(Calendar.HOUR_OF_DAY, 1);
        }
        AxisBean inqty = new AxisBean();
        inqty.setName("入库托盘数");
        Integer[] array1 = new Integer[data1.size()];
        inqty.setData(data1.toArray(array1));
        list.add(inqty);
        AxisBean outqty = new AxisBean();
        outqty.setName("出库托盘数");
        Integer[] array2 = new Integer[data2.size()];
        outqty.setData(data2.toArray(array2));
        list.add(outqty);
        if (data3.size() >0) {
            AxisBean cubeInqty = new AxisBean();
            cubeInqty.setName("入库体积");
            Double [] array3 = new Double[data3.size()];
            cubeInqty.setData(data3.toArray(array3));
            list.add(cubeInqty);
        }
        if (data4.size() >0) {
            AxisBean cubeOutqty = new AxisBean();
            cubeOutqty.setName("出库体积");
            Double[] array4 = new Double[data4.size()];
            cubeOutqty.setData(data4.toArray(array4));
            list.add(cubeOutqty);
        }
        if (data5.size() >0) {
            AxisBean cubeOutqty = new AxisBean();
            cubeOutqty.setName("出库TEU");
            Double[] array5 = new Double[data4.size()];
            cubeOutqty.setData(data5.toArray(array5));
            list.add(cubeOutqty);
        }
        map.put("categories", categories);
        map.put("rows", list);
        return R.ok(map);
    }
@@ -635,7 +1028,8 @@
        }
        // 总库位数
        Integer total = (int) Arith.add(0, locUseRate.getFqty(), locUseRate.getOqty(), locUseRate.getUqty(), locUseRate.getXqty());
        Integer total1 = (int) Arith.add(0, locUseRate.getFqty(), locUseRate.getOqty(), locUseRate.getUqty(), locUseRate.getXqty());
        Integer total = total1>40000?6528:total1;
        // 使用中
        Integer used = locUseRate.getFqty() + locUseRate.getUqty();
        // 库位使用率
@@ -657,7 +1051,11 @@
     * 任务查询接口
     */
    @PostMapping("/queryTask")
    public synchronized R queryTask(@RequestBody QueryTaskParam param) {
    public synchronized R queryTask(@RequestBody QueryTaskParam param, HttpServletRequest request) {
        if (request != null) {
            log.info("[queryTask] cache: {}", param == null ? "null" : JSON.toJSONString(param));
            request.setAttribute("cache", param);
        }
        if (Cools.isEmpty(param)) {
            return R.parse(BaseRes.PARAM);
        }
@@ -675,7 +1073,74 @@
        map.put("taskNo", param.getTaskNo());
        map.put("ioType", wrkMast.getIoType());
        map.put("wrkDetls", wrkDetls);
        Integer count = 0;
        String supp = "";
        //该订单累计入出库件数
        Integer ioType = wrkMast.getIoType();
        Integer suppCount = 0;
        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) {
                sum = Integer.valueOf(split[split.length - 1]);
            }else {
                sum = Integer.valueOf(wrkDetls.get(0).getSupp());
            }
            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);
        }
        //耗时
        Long costTime = resolveCostTime(wrkMast);
        map.put("costTime", String.valueOf(costTime));
        return R.ok().add(map);
    }
    Long resolveCostTime(WrkMast wrkMast) {
        return resolveCostTime(wrkMast, new Date());
    }
    Long resolveCostTime(WrkMast wrkMast, Date now) {
        if (wrkMast == null || wrkMast.getIoType() == null) {
            return 0L;
        }
        if (wrkMast.getIoType() < 100) {
            if (Cools.isEmpty(wrkMast.getTrainNo())) {
                return 0L;
            }
            return minutesBetween(wrkMastService.firstInboundCreateTimeByTrainNo(wrkMast.getTrainNo()), resolveTaskCreateTime(wrkMast));
        }
        return minutesBetween(resolveTaskCreateTime(wrkMast), now);
    }
    int resolveInboundSupp(WrkMast wrkMast) {
        if (wrkMast == null || wrkMast.getIoType() == null || wrkMast.getIoType() >= 100 || Cools.isEmpty(wrkMast.getTrainNo())) {
            return 0;
        }
        return wrkMastService.finishedInboundPalletCountByTrainNo(wrkMast.getTrainNo());
    }
    private Date resolveTaskCreateTime(WrkMast wrkMast) {
        if (wrkMast == null) {
            return null;
        }
        return wrkMast.getAppeTime() == null ? wrkMast.getIoTime() : wrkMast.getAppeTime();
    }
    private Long minutesBetween(Date startTime, Date endTime) {
        if (startTime == null || endTime == null) {
            return 0L;
        }
        long diff = endTime.getTime() - startTime.getTime();
        if (diff <= 0) {
            return 0L;
        }
        return diff / MILLIS_PER_MINUTE;
    }
}