自动化立体仓库 - WMS系统
src/main/java/com/zy/api/service/impl/WcsApiServiceImpl.java
@@ -24,11 +24,7 @@
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@@ -79,37 +75,24 @@
     */
    @Override
    public R pubWrkToWcs(WorkTaskParams params) {
        if (Objects.isNull(params.getTaskNo())) {
            return R.error("任务号不能为空!!");
        if (params == null) {
            return R.error("参数不能为空!!");
        }
        if (Objects.isNull(params.getBarcode())) {
            return R.error("托盘码不能为空!!");
        WrkMast wrkMast = wrkMastService.selectOne(new EntityWrapper<WrkMast>().eq("wrk_no", params.getTaskNo()));
        String validateMsg = validatePubTask(params, wrkMast);
        if (!Cools.isEmpty(validateMsg)) {
            return R.error(validateMsg);
        }
        if (Objects.isNull(params.getLocNo())) {
            return R.error("目标库位不能为空!!");
        }
        String url = createInTask;
        if (!Objects.isNull(params.getType()) && params.getType().equals("out")) {
            url = getWcs_address;
        }else if (!Objects.isNull(params.getType()) && params.getType().equals("move")) {
            url = createLocMoveTask;
        }
        String url = resolveTaskPath(params);
        String response;
        R r = R.ok();
        WrkMast wrkMast = wrkMastService.selectOne(new EntityWrapper<WrkMast>().eq("wrk_no", params.getTaskNo()));
        if (!Objects.isNull(wrkMast) && "out".equalsIgnoreCase(params.getType())) {
            if ("Y".equalsIgnoreCase(wrkMast.getPauseMk())) {
                return R.error("task paused");
            }
            if (requiresOutboundErpConfirm(wrkMast) && !"Y".equalsIgnoreCase(wrkMast.getPdcType())) {
                return R.error("task not confirmed by erp");
            }
        }
        try {
            log.info("下发搬运任务给wcs="+JSON.toJSONString(params));
            response = new HttpHandler.Builder()
                    .setUri(wcs_address)
                    .setPath(url)
                    .setHttps(wcs_address != null && wcs_address.startsWith("https://"))
                    .setTimeout(10, TimeUnit.SECONDS)
                    .setJson(JSON.toJSONString(params))
                    .build()
                    .doPost();
@@ -118,21 +101,7 @@
            Integer code = jsonObject.getInteger("code");
            if (code==200) {
                if (!Objects.isNull(wrkMast)) {
                    if (wrkMast.getIoType()==1 || wrkMast.getIoType()==10) {
                        wrkMast.setWrkSts(2L);
                        wrkMast.setModiTime(new Date());
                        wrkMastService.updateById(wrkMast);
                    }else if(wrkMast.getIoType()==2){
                        wrkMast.setWrkSts(2L);
                        wrkMast.setModiTime(new Date());
                        wrkMastService.updateById(wrkMast);
                    }else if (wrkMast.getIoType()==101 || wrkMast.getIoType()==110) {
                        wrkMast.setWrkSts(12L);
                        wrkMast.setModiTime(new Date());
                        wrkMastService.updateById(wrkMast);
                    }
                }
                updateWrkMastAfterPublish(wrkMast);
                //TODO 上报是否成功
            }else {
                r =R.error();
@@ -141,6 +110,100 @@
            throw new RuntimeException(e);
        }
        return r;
    }
    @Override
    public R pubWrksToWcs(List<WorkTaskParams> paramsList) {
        if (paramsList == null || paramsList.isEmpty()) {
            return R.error("任务不能为空!!");
        }
        // 先一次性把本批任务对应的工作档捞出来,避免循环内重复查库。
        Map<String, WrkMast> wrkMastMap = getWrkMastMap(paramsList);
        Map<String, List<WorkTaskParams>> groupedTasks = new LinkedHashMap<>();
        List<String> skipMsgs = new ArrayList<>();
        for (WorkTaskParams params : paramsList) {
            if (params == null) {
                skipMsgs.add("任务不能为空!!");
                continue;
            }
            WrkMast wrkMast = wrkMastMap.get(params.getTaskNo());
            String validateMsg = validatePubTask(params, wrkMast);
            if (!Cools.isEmpty(validateMsg)) {
                skipMsgs.add(buildTaskMsg(params, validateMsg));
                continue;
            }
            // 分组主键 = 接口路径 + userNo。
            // 这样既能保证入库/出库/移库不会混发,也能保证相同 userNo 的任务会打包到同一次 WCS 请求中。
            String groupKey = buildBatchGroupKey(params, wrkMast);
            groupedTasks.computeIfAbsent(groupKey, key -> new ArrayList<>()).add(params);
        }
        if (groupedTasks.isEmpty()) {
            return R.error(skipMsgs.isEmpty() ? "无可下发任务" : skipMsgs.get(0)).add(skipMsgs);
        }
        int successCount = 0;
        List<String> failMsgs = new ArrayList<>();
        for (List<WorkTaskParams> group : groupedTasks.values()) {
            if (group == null || group.isEmpty()) {
                continue;
            }
            // 同一组内的任务类型一致,因此取第一条即可确定本组应该调用哪个 WCS 接口。
            String path = resolveTaskPath(group.get(0));
            Map<String, Object> payload = new HashMap<>();
            // WCS 批量下发报文统一使用 {"tasks":[...]} 结构。
            payload.put("tasks", buildTaskPayloads(group));
            String response = null;
            try {
                log.info("批量下发搬运任务给wcs={}", JSON.toJSONString(payload));
                response = new HttpHandler.Builder()
                        .setUri(wcs_address)
                        .setPath(path)
                        .setHttps(wcs_address != null && wcs_address.startsWith("https://"))
                        .setTimeout(10, TimeUnit.SECONDS)
                        .setJson(JSON.toJSONString(payload))
                        .build()
                        .doPost();
                JSONObject jsonObject = JSON.parseObject(response == null ? "{}" : response);
                log.info("批量下发任务给wcs的返回值={}", response);
                Integer code = jsonObject.getInteger("code");
                if (code != null && code == 200) {
                    successCount += group.size();
                    // 只有整组下发成功,才回写本地工作档状态,避免 WMS/WCS 状态分叉。
                    for (WorkTaskParams params : group) {
                        updateWrkMastAfterPublish(wrkMastMap.get(params.getTaskNo()));
                    }
                } else {
                    String msg = jsonObject.getString("msg");
                    failMsgs.add("path=" + path + ", msg=" + (Cools.isEmpty(msg) ? "WCS下发任务失败" : msg));
                    log.error("批量下发任务给wcs失败, path:{}, request:{}, response:{}", path, JSON.toJSONString(payload), response);
                }
            } catch (IOException e) {
                failMsgs.add("path=" + path + ", msg=" + e.getMessage());
                log.error("批量下发任务给wcs异常, path:{}, request:{}, response:{}", path, JSON.toJSONString(payload), response, e);
            }
        }
        Map<String, Object> result = new HashMap<>();
        result.put("successCount", successCount);
        result.put("skipCount", skipMsgs.size());
        result.put("failCount", failMsgs.size());
        if (!skipMsgs.isEmpty()) {
            result.put("skipMsgs", skipMsgs);
        }
        if (!failMsgs.isEmpty()) {
            result.put("failMsgs", failMsgs);
        }
        if (successCount == 0) {
            String msg = !failMsgs.isEmpty() ? failMsgs.get(0) : (skipMsgs.isEmpty() ? "WCS下发任务失败" : skipMsgs.get(0));
            return R.error(msg).add(result);
        }
        return R.ok(failMsgs.isEmpty() && skipMsgs.isEmpty() ? "操作成功" : "部分任务下发成功").add(result);
    }
    /**
@@ -210,9 +273,183 @@
        Integer ioType = wrkMast == null ? null : wrkMast.getIoType();
        return ioType != null && (ioType == 101 || ioType == 103 || ioType == 104 || ioType == 107 || ioType == 110);
    }
    /**
     * 校验单条任务是否满足下发前提。
     * <p>
     * 这里既校验接口必填项,也校验业务约束,例如:
     * 1. 出库任务是否被暂停;
     * 2. 需要 ERP 确认的出库任务是否已确认。
     */
    private String validatePubTask(WorkTaskParams params, WrkMast wrkMast) {
        if (params == null) {
            return "参数不能为空!!";
        }
        if (Cools.isEmpty(params.getTaskNo())) {
            return "任务号不能为空!!";
        }
        if (Cools.isEmpty(params.getBarcode())) {
            return "托盘码不能为空!!";
        }
        if (Cools.isEmpty(params.getLocNo())) {
            return "目标库位不能为空!!";
        }
        if (!Objects.isNull(wrkMast) && "out".equalsIgnoreCase(params.getType())) {
            if ("Y".equalsIgnoreCase(wrkMast.getPauseMk())) {
                return "task paused";
            }
            if (requiresOutboundErpConfirm(wrkMast) && !"Y".equalsIgnoreCase(wrkMast.getPdcType())) {
                return "task not confirmed by erp";
            }
        }
        return null;
    }
    /**
     * 按任务类型选择 WCS 接口地址。
     * in -> 入库接口
     * out -> 出库接口
     * move -> 移库接口
     */
    private String resolveTaskPath(WorkTaskParams params) {
        if (!Objects.isNull(params.getType()) && params.getType().equals("out")) {
            return getWcs_address;
        }
        if (!Objects.isNull(params.getType()) && params.getType().equals("move")) {
            return createLocMoveTask;
        }
        return createInTask;
    }
    /**
     * WCS 下发成功后推进本地工作档状态。
     * <p>
     * 这里只处理“已下发”这一层状态,不处理设备执行完成状态;
     * 设备执行完成依然以 WCS 回写为准。
     */
    private void updateWrkMastAfterPublish(WrkMast wrkMast) {
        if (Objects.isNull(wrkMast)) {
            return;
        }
        if (wrkMast.getIoType()==1 || wrkMast.getIoType()==10) {
            wrkMast.setWrkSts(2L);
            wrkMast.setModiTime(new Date());
            wrkMastService.updateById(wrkMast);
        }else if(wrkMast.getIoType()==2){
            wrkMast.setWrkSts(2L);
            wrkMast.setModiTime(new Date());
            wrkMastService.updateById(wrkMast);
        }else if (wrkMast.getIoType()==101 || wrkMast.getIoType()==110) {
            wrkMast.setWrkSts(12L);
            wrkMast.setModiTime(new Date());
            wrkMastService.updateById(wrkMast);
        }
    }
    /**
     * 把本次待下发的 taskNo 批量映射成工作档,供后续校验、按 userNo 分组、状态回写复用。
     */
    private Map<String, WrkMast> getWrkMastMap(List<WorkTaskParams> paramsList) {
        List<String> taskNos = paramsList.stream()
                .filter(Objects::nonNull)
                .map(WorkTaskParams::getTaskNo)
                .filter(taskNo -> !Cools.isEmpty(taskNo))
                .distinct()
                .collect(Collectors.toList());
        if (taskNos.isEmpty()) {
            return Collections.emptyMap();
        }
        List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<WrkMast>().in("wrk_no", taskNos));
        if (wrkMasts == null || wrkMasts.isEmpty()) {
            return Collections.emptyMap();
        }
        return wrkMasts.stream()
                .filter(Objects::nonNull)
                .collect(Collectors.toMap(mast -> String.valueOf(mast.getWrkNo()), mast -> mast, (left, right) -> left, LinkedHashMap::new));
    }
    /**
     * 构造批量下发的分组键。
     * <p>
     * 分组规则:
     * 1. 先按接口路径区分,避免不同任务类型混用同一个 WCS 接口;
     * 2. 再按 userNo 区分,确保相同 userNo 的任务一起上报。
     * <p>
     * 正常情况下 userNo 取自 work_mast.user_no;
     * 如果当前没查到工作档,则回退到请求里的 batch 字段,保证兼容已有调用。
     */
    private String buildBatchGroupKey(WorkTaskParams params, WrkMast wrkMast) {
        String path = resolveTaskPath(params);
        String userNo = wrkMast == null ? null : wrkMast.getUserNo();
        if (Cools.isEmpty(userNo)) {
            userNo = params.getBatch();
        }
        if (Cools.isEmpty(userNo)) {
            userNo = "_NO_USER_";
        }
        return path + "#" + userNo;
    }
    /**
     * 将一组业务参数转换成 WCS 批量接口的 tasks 数组。
     */
    private List<Map<String, Object>> buildTaskPayloads(List<WorkTaskParams> tasks) {
        List<Map<String, Object>> payloads = new ArrayList<>();
        for (WorkTaskParams task : tasks) {
            payloads.add(buildTaskPayload(task));
        }
        return payloads;
    }
    /**
     * 组装单条任务的 WCS 请求体。
     * 只放当前任务类型实际需要的字段;空字段不透传,避免给 WCS 造成歧义。
     */
    private Map<String, Object> buildTaskPayload(WorkTaskParams params) {
        Map<String, Object> task = new LinkedHashMap<>();
        if (!Cools.isEmpty(params.getTaskNo())) {
            task.put("taskNo", params.getTaskNo());
        }
        if (!Cools.isEmpty(params.getLocNo())) {
            task.put("locNo", params.getLocNo());
        }
        if (!Cools.isEmpty(params.getSourceLocNo())) {
            task.put("sourceLocNo", params.getSourceLocNo());
        }
        if (!Cools.isEmpty(params.getSourceStaNo())) {
            task.put("sourceStaNo", params.getSourceStaNo());
        }
        if (!Cools.isEmpty(params.getBarcode())) {
            task.put("barcode", params.getBarcode());
        }
        if (!Objects.isNull(params.getTaskPri())) {
            task.put("taskPri", params.getTaskPri());
        }
        if (!Cools.isEmpty(params.getStaNo())) {
            task.put("staNo", params.getStaNo());
        }
        if (!Cools.isEmpty(params.getBatch())) {
            task.put("batch", params.getBatch());
        }
        if (!Objects.isNull(params.getBatchSeq())) {
            task.put("batchSeq", params.getBatchSeq());
        }
        return task;
    }
    /**
     * 构造跳过/失败信息时统一带上 taskNo,便于排查具体是哪一条工作档未被下发。
     */
    private String buildTaskMsg(WorkTaskParams params, String msg) {
        if (params == null || Cools.isEmpty(params.getTaskNo())) {
            return msg;
        }
        return "taskNo=" + params.getTaskNo() + ", msg=" + msg;
    }
    @Override
    public R pauseOutTasks(StopOutTaskParams params) {
        if (params == null || params.getTasks() == null || params.getTasks().isEmpty()) {
    public R pauseOutTasks(List<HashMap<String,Object>> params) {
        if (params == null || params.size() == 0) {
            return R.ok("无任务需要取消");
        }
        if (!Boolean.parseBoolean(String.valueOf(switchValue))) {
@@ -236,18 +473,7 @@
                String msg = jsonObject.getString("msg");
                throw new CoolException(Cools.isEmpty(msg) ? "WCS取消出库任务失败" : msg);
            }
            JSONObject data = jsonObject.getJSONObject("data");
            List<String> successList = data == null || data.getJSONArray("successList") == null
                    ? Collections.emptyList()
                    : data.getJSONArray("successList").toJavaList(String.class);
            List<String> failList = data == null || data.getJSONArray("failList") == null
                    ? Collections.emptyList()
                    : data.getJSONArray("failList").toJavaList(String.class);
            R result = R.ok(Cools.isEmpty(jsonObject.getString("msg")) ? "操作成功" : jsonObject.getString("msg"));
            result.put("data", data);
            result.put("successList", successList);
            result.put("failList", failList);
            return result;
            return R.ok(Cools.isEmpty(jsonObject.getString("msg")) ? "操作成功" : jsonObject.getString("msg"));
        } catch (IOException e) {
            throw new CoolException("调用WCS取消出库任务失败: " + e.getMessage());
        }