自动化立体仓库 - WMS系统
zwl
13 小时以前 a1f7856f0f450883c7060444a4fc2b721720a051
src/main/java/com/zy/asrs/service/impl/OpenServiceImpl.java
@@ -19,6 +19,7 @@
import com.zy.asrs.utils.Utils;
import com.zy.common.constant.AgvConstant;
import com.zy.common.constant.ArmConstant;
import com.zy.common.entity.Parameter;
import com.zy.common.model.DetlDto;
import com.zy.common.model.LocDetlDto;
import com.zy.common.model.LocDto;
@@ -33,6 +34,8 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
@@ -42,6 +45,15 @@
@Slf4j
@Service
public class OpenServiceImpl implements OpenService {
    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";
    static {
        Map<Integer, BigDecimal> factorMap = new HashMap<>();
        factorMap.put(112, new BigDecimal("0.98"));
        INBOUND_WEIGHT_FACTOR_BY_SOURCE_STA = Collections.unmodifiableMap(factorMap);
    }
    @Autowired
    private OrderService orderService;
@@ -87,10 +99,24 @@
    private String mesUrl;
    @Value("${mes.stationaddress}")
    private String stationAddress;
    @Value("${erp.address.URL:}")
    private String erpUrl;
    @Value("${erp.switch.ErpReportOld}")
    private boolean erpReportOld;
    @Value("${erp.address.Inaddress:}")
    private String erpInAddress;
    @Value("${erp.address.OutErroraddress:}")
    private String erpOutErrorAddress;
    @Autowired
    private WaitPakinService waitPakinService;
    @Autowired
    private WaitPakinLogService waitPakinLogService;
    @Autowired
    private WrkMastService wrkMastService;
    @Autowired
    private WrkMastLogService wrkMastLogService;
    @Autowired
    private WrkDetlLogService wrkDetlLogService;
    @Autowired
    private WcsApiService wcsApiService;
    @Autowired
@@ -417,7 +443,7 @@
            return R.ok(confirmedCount == 0 ? "任务已确认执行" : "ERP确认执行出库成功").add(result);
        }
        if (Objects.equals(param.getExecute(), 2)) {
            // ERP请求取消任务:按 plt_type 从大到小先 WCS 再 WMS;失败或异常则停止后续,接口仍返回原成功结构。
            // ERP请求取消任务:直接收集任务号,按 taskList 格式发送给 WCS。
            Map<String, Object> result = new HashMap<>();
            result.put("orderNo", param.getOrderId());
            result.put("execute", param.getExecute());
@@ -425,26 +451,17 @@
            if (activeTasks.isEmpty()) {
                return R.ok("无有效出库任务").add(result);
            }
            List<WrkMast> sorted = new ArrayList<>(activeTasks);
            sorted.sort(Comparator.comparing(WrkMast::getPltType, Comparator.nullsLast(Comparator.reverseOrder())));
            for (WrkMast wrkMast : sorted) {
                try {
                    if (!Cools.isEmpty(wrkMast) && wrkMast.getWrkSts() == 11L) {
                        workService.cancelWrkMast(wrkMast.getWrkNo() + "", 9955L);
                    }else{
                    HashMap<String, Object> hashMap = new HashMap<>();
                    hashMap.put("taskNo", wrkMast.getWrkNo());
                    List<HashMap<String, Object>> one = new ArrayList<>();
                    one.add(hashMap);
                    R wcsR = wcsApiService.pauseOutTasks(one);
                    requireWcsPauseOk(wcsR);
                    workService.cancelWrkMast(wrkMast.getWrkNo() + "", 9955L);
                    }
                } catch (Exception e) {
                    log.warn("[pakoutOrderPause] execute=2 取消中止, orderNo={}, err={}", param.getOrderId(), e.getMessage());
                    break;
            List<HashMap<String,Object>> taskList = new ArrayList<>();
            for (WrkMast wrkMast : activeTasks) {
                HashMap<String,Object> hashMap = new HashMap<>();
                hashMap.put("taskNo", wrkMast.getWrkNo());
                if (!Cools.isEmpty(wrkMast) && wrkMast.getWrkSts() ==11L) {
                    workService.cancelWrkMast(wrkMast.getWrkNo()+"", 9955L);
                    continue;
                }
                taskList.add(hashMap);
            }
            wcsApiService.pauseOutTasks(taskList);
            return R.ok("取消任务已发送至WCS").add(result);
        }
        throw new CoolException("reason仅支持1或2");
@@ -664,6 +681,117 @@
    @Transactional
    public List<StockVo> queryStock() {
        return locDetlService.queryStockTotal();
    }
    @Override
    public R reportPakinHistoryToErp(List<String> barcodes) {
        List<String> normalizedBarcodes = normalizeBarcodes(barcodes);
        if (normalizedBarcodes.isEmpty()) {
            return R.error("托盘码集合不能为空");
        }
        if (!isErpReportEnabled()) {
            return R.error("ERP reporting is disabled");
        }
        if (Cools.isEmpty(erpInAddress)) {
            return R.error("ERP入库上报地址未配置");
        }
        Map<String, WrkMastLog> latestInboundLogMap = loadLatestInboundHistoryLogMap(normalizedBarcodes);
        Map<Integer, List<WrkDetlLog>> wrkDetlLogMap = loadWrkDetlLogMap(latestInboundLogMap.values());
        Map<String, WaitPakinLog> waitPakinLogMap = loadWaitPakinLogMap(normalizedBarcodes);
        String requestUrl = buildErpInboundRequestUrl();
        List<Map<String, Object>> rows = new ArrayList<>();
        int successCount = 0;
        int failCount = 0;
        for (String barcode : normalizedBarcodes) {
            Map<String, Object> row = new LinkedHashMap<>();
            row.put("barcode", barcode);
            WrkMastLog wrkMastLog = latestInboundLogMap.get(barcode);
            if (wrkMastLog == null) {
                row.put("success", false);
                row.put("message", "未找到最新入库历史记录");
                rows.add(row);
                failCount++;
                continue;
            }
            WaitPakinLog waitPakinLog = waitPakinLogMap.get(barcode);
            List<WrkDetlLog> wrkDetlLogs = wrkDetlLogMap.getOrDefault(wrkMastLog.getWrkNo(), Collections.emptyList());
            ErpPakinReportParam param = buildInboundErpParam(barcode, wrkMastLog, wrkDetlLogs, waitPakinLog);
            if (Cools.isEmpty(param.getPalletId())) {
                row.put("success", false);
                row.put("message", "托盘码缺少上报字段[palletId]");
                rows.add(row);
                failCount++;
                continue;
            }
            if (Cools.isEmpty(param.getLocId())) {
                row.put("success", false);
                row.put("message", "托盘码缺少上报字段[locId]");
                rows.add(row);
                failCount++;
                continue;
            }
            String request = JSON.toJSONString(param);
            String response = "";
            boolean success = false;
            String errorMsg = null;
            try {
                response = new HttpHandler.Builder()
                        .setUri(erpUrl)
                        .setPath(erpInAddress)
                        .setJson(request)
                        .build()
                        .doPost();
                success = isErpCallSuccess(response);
                if (!success) {
                    errorMsg = extractErpCallError(response);
                }
            } catch (Exception e) {
                errorMsg = e.getMessage();
            } finally {
                try {
                    apiLogService.save(
                            "Inbound ERP Report",
                            requestUrl,
                            null,
                            "127.0.0.1",
                            request,
                            response,
                            success,
                            "barcode=" + barcode + ",workNo=" + wrkMastLog.getWrkNo()
                    );
                } catch (Exception logEx) {
                    log.error("save inbound erp api log failed", logEx);
                }
            }
            row.put("workNo", wrkMastLog.getWrkNo());
            Integer sourceStaNo = resolveInboundSourceStaNo(wrkMastLog);
            row.put("sourceStaNo", sourceStaNo);
            row.put("weightFactor", resolveInboundWeightFactor(sourceStaNo));
            row.put("erpWeight", param.getWeight());
            row.put("success", success);
            row.put("message", success ? "OK" : errorMsg);
            rows.add(row);
            if (success) {
                successCount++;
            } else {
                failCount++;
            }
        }
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("total", normalizedBarcodes.size());
        result.put("successCount", successCount);
        result.put("failCount", failCount);
        result.put("rows", rows);
        return R.ok().add(result);
    }
    @Override
@@ -1307,6 +1435,9 @@
        return mesUrl + stationAddress;
    }
    /**
     * 7.3 组托信息下发。
     */
    @Override
    public R mesToComb(MesToCombParam param) {
        if (Cools.isEmpty(param.getPalletId())) {
@@ -1339,15 +1470,22 @@
        waitPakin.setIoStatus("N");     // 入出状态
        waitPakin.setAnfme(param.getAnfme());  // 数量
        waitPakin.setFreqType(param.getFreqType());
        waitPakin.setStatus("Y");    // 状态
        waitPakin.setAppeUser(9995L);
        waitPakin.setAppeTime(now);
        waitPakin.setModiUser(9995L);
        waitPakin.setModiTime(now);
        waitPakin.setOrderNo(String.valueOf(param.getOrderId()));
        waitPakin.setOrderNo(String.valueOf(param.getBizNo()));
        waitPakin.setOrigin(String.valueOf(param.getStorageArea()));//建议入库区域
        waitPakin.setManu(String.valueOf(param.getLocId()));//mes具体库位编号
        waitPakin.setThreeCode(param.getBizNo());
        // 7.3 新增字段复用现有组托档承载位,不新增列。
        waitPakin.setSuppCode(param.getCustomerId());
        waitPakin.setSupp(param.getCustomerName());
        waitPakin.setStandby3(param.getItem());
        waitPakin.setStandby1(param.getEntryWmsCode());
        waitPakin.setStandby2(param.getOutDoorNo());
        waitPakin.setBeBatch(param.getPackage1());//是否散货,0 非散货;1 散货;为了管控出货速率,散货可以出慢点。
        // ERP 入口默认打 erp,MQTT 组托会在参数里显式传 aws。
        waitPakin.setBoxType1(Cools.isEmpty(param.getBoxType1()) ? "erp" : param.getBoxType1());
@@ -1357,15 +1495,40 @@
        return R.ok().add(Cools.add("palletId", param.getPalletId()).add("orderId", param.getOrderId()));
    }
    /**
     * 7.11 出库通知单(传递有序无序规则)单条建单。
     */
    @Override
    public R outOrder(OutTaskParam param,int count) {
        return outOrder(param, count, 1);
    }
    private R outOrder(OutTaskParam param, int count, int teu) {
        LocMast locMast = locMastService.selectOne(new EntityWrapper<LocMast>().eq("loc_sts", "F").eq("barcode", param.getPalletId()));
        if (locMast == null) {
            throw new CoolException("没有找到托盘码=" + param.getPalletId() + "对应的库位");
        }
        if (Cools.isEmpty(param.getStationId())) {
            throw new CoolException("出库口编码不能为空");
        }
        if (Cools.isEmpty(param.getBatchSeq())) {
            throw new CoolException("批次标识不能为空");
        }
        if (param.getSeq() == null || param.getSeq() < 0) {
            throw new CoolException("出库顺序不能为空且不能小于0");
        }
        Integer ioType = 101;
        // 获取路径
        StaDesc staDesc = staDescService.queryCrnStn(ioType, locMast.getCrnNo(), Integer.valueOf(param.getStationId()));
        Integer stationId;
        try {
            stationId = Integer.valueOf(param.getStationId());
        } catch (NumberFormatException ex) {
            throw new CoolException("出库口编码格式错误:" + param.getStationId());
        }
        StaDesc staDesc = staDescService.queryCrnStn(ioType, locMast.getCrnNo(), stationId);
        if (staDesc == null) {
            throw new CoolException("未找到出库口=" + param.getStationId() + "对应路径");
        }
        Date now = new Date();
        // 生成工作号
        int workNo = commonService.getWorkNo(WorkNoType.getWorkNoType(ioType));
@@ -1387,7 +1550,10 @@
        wrkMast.setEmptyMk("N"); // 空板
        wrkMast.setLinkMis("N");
        wrkMast.setPdcType("N");
        wrkMast.setContainerNo(param.getContainerNo());
        // 7.11:orderId 存 userNo,batchSeq 存批次标识,seq 存批次内顺序。
        wrkMast.setUserNo(param.getOrderId());//订单号
        wrkMast.setBatchSeq(param.getBatchSeq());//订单内批次标识
        wrkMast.setPltType(param.getSeq());//出库顺序,从1开始
        wrkMast.setTakeNone("0");  //0非自动
        wrkMast.setAppeUser(9995L); // 操作人员数据
@@ -1414,6 +1580,10 @@
            wrkDetl.setAppeUser(9995L);
            wrkDetl.setModiTime(now);
            wrkDetl.setModiUser(9995L);
            wrkDetl.setTeu(teu);
            // 7.11:entryWmsCode、outDoorNo 复用明细备用字段。
            wrkDetl.setStandby1(param.getEntryWmsCode());
            wrkDetl.setStandby2(param.getOutDoorNo());
            wrkDetl.setSupp(param.getSeq()+"/"+count);
            if (!wrkDetlService.insert(wrkDetl)) {
@@ -1436,18 +1606,531 @@
        return R.ok().add(Cools.add("wrkNo", workNo).add("orderId", param.getOrderId()));
    }
    /**
     * 7.11 出库通知单(传递有序无序规则)批量建单。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public R outOrderBatch(List<OutTaskParam> params) {
        int n = params.size();
        Map<String, Integer> batchLineCounts = new HashMap<>();
        Map<String, Integer> batchTeuCounts = buildOutOrderBatchTeuCounts(params);
        for (OutTaskParam outTaskParam : params) {
            R r = outOrder(outTaskParam, n);
            batchLineCounts.merge(buildOutOrderBatchKey(outTaskParam), 1, Integer::sum);
        }
        for (OutTaskParam outTaskParam : params) {
            int count = batchLineCounts.getOrDefault(buildOutOrderBatchKey(outTaskParam), n);
            int teu = batchTeuCounts.getOrDefault(outTaskParam.getBatchSeq(), 1);
            R r = outOrder(outTaskParam, count, teu);
            if (!Objects.equals(r.get("code"), 200)) {
                throw new CoolException("出库建单失败");
            }
        }
        return R.ok();
    }
    /**
     * 7.9 出库异常变动上报。
     * WCS 只传 palletId、errorMsg,WMS 解析 orderId 后转发 ERP。
     */
    @Override
    public R outOrderAbnormalReport(OutOrderAbnormalReportParam param) {
        if (param == null || Cools.isEmpty(param.getPalletId())) {
            return R.error("palletId不能为空");
        }
        String orderId = resolveOutboundOrderId(param.getPalletId());
        if (Cools.isEmpty(orderId)) {
            return R.error("未找到托盘对应出库单号:" + param.getPalletId());
        }
        if (Cools.isEmpty(erpOutErrorAddress)) {
            return R.error("ERP出库异常上报地址未配置");
        }
        ErpOutOrderAbnormalReportParam erpParam = new ErpOutOrderAbnormalReportParam();
        erpParam.setPalletId(param.getPalletId());
        erpParam.setErrorMsg(param.getErrorMsg());
        erpParam.setOrderId(orderId);
        String requestJson = JSON.toJSONString(erpParam);
        String response = "";
        boolean pushOk = false;
        String errorMsg = null;
        String pushUrl = buildErpOutErrorRequestUrl();
        try {
            response = new HttpHandler.Builder()
                    .setUri(erpUrl)
                    .setPath(erpOutErrorAddress)
                    .setJson(requestJson)
                    .build()
                    .doPost();
            pushOk = isErpCallSuccess(response);
            if (!pushOk) {
                errorMsg = extractErpCallError(response);
            }
        } catch (Exception e) {
            errorMsg = e.getMessage();
            log.error("推ERP出库异常上报失败, palletId={}, orderId={}", param.getPalletId(), orderId, e);
        } finally {
            try {
                apiLogService.save(
                        "推ERP-出库异常上报",
                        pushUrl,
                        null,
                        "127.0.0.1",
                        requestJson,
                        response,
                        pushOk,
                        "palletId=" + param.getPalletId() + ",orderId=" + orderId
                );
            } catch (Exception logEx) {
                log.error("save out abnormal api log failed", logEx);
            }
        }
        if (!pushOk) {
            return R.error(Cools.isEmpty(errorMsg) ? "ERP出库异常上报失败" : errorMsg);
        }
        return R.ok().add(Cools.add("palletId", param.getPalletId()).add("orderId", orderId));
    }
    /**
     * 7.10 出库异常变动处理。
     * 本期只接收、校验和记录,不改本地任务,也不转发 WCS。
     */
    @Override
    public R outOrderAbnormalHandle(OutOrderAbnormalHandleParam param) {
        if (param == null || Cools.isEmpty(param.getPalletId())) {
            return R.error("palletId不能为空");
        }
        if (param.getOperateType() == null || !(Objects.equals(param.getOperateType(), 0) || Objects.equals(param.getOperateType(), 2))) {
            return R.error("operateType只允许0或2");
        }
        if (!existsOutboundPallet(param.getPalletId())) {
            return R.error("未找到托盘:" + param.getPalletId());
        }
        log.info("接收出库异常处理指令, palletId={}, operateType={}", param.getPalletId(), param.getOperateType());
        return R.ok().add(Cools.add("palletId", param.getPalletId()).add("operateType", param.getOperateType()));
    }
    private String buildOutOrderBatchKey(OutTaskParam param) {
        return param.getOrderId() + "#" + param.getBatchSeq();
    }
    private Map<String, Integer> buildOutOrderBatchTeuCounts(List<OutTaskParam> params) {
        Map<String, Set<String>> batchOrderIds = new HashMap<>();
        for (OutTaskParam param : params) {
            batchOrderIds.computeIfAbsent(param.getBatchSeq(), k -> new LinkedHashSet<>()).add(param.getOrderId());
        }
        Map<String, Integer> batchTeuCounts = new HashMap<>();
        for (Map.Entry<String, Set<String>> entry : batchOrderIds.entrySet()) {
            batchTeuCounts.put(entry.getKey(), entry.getValue().size());
        }
        return batchTeuCounts;
    }
    private String resolveOutboundOrderId(String palletId) {
        List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<WrkMast>()
                .eq("io_type", 101)
                .eq("barcode", palletId)
                .orderBy("io_time", false)
                .orderBy("wrk_no", false));
        if (!Cools.isEmpty(wrkMasts)) {
            for (WrkMast wrkMast : wrkMasts) {
                if (!Cools.isEmpty(wrkMast.getUserNo())) {
                    return wrkMast.getUserNo();
                }
            }
        }
        List<WrkDetl> wrkDetls = wrkDetlService.selectList(new EntityWrapper<WrkDetl>()
                .eq("zpallet", palletId)
                .orderBy("io_time", false)
                .orderBy("wrk_no", false));
        if (!Cools.isEmpty(wrkDetls)) {
            for (WrkDetl wrkDetl : wrkDetls) {
                if (!Cools.isEmpty(wrkDetl.getOrderNo())) {
                    return wrkDetl.getOrderNo();
                }
            }
        }
        return null;
    }
    private boolean existsOutboundPallet(String palletId) {
        if (locDetlService.selectCount(new EntityWrapper<LocDetl>().eq("zpallet", palletId)) > 0) {
            return true;
        }
        if (wrkMastService.selectCount(new EntityWrapper<WrkMast>().eq("io_type", 101).eq("barcode", palletId)) > 0) {
            return true;
        }
        return wrkDetlService.selectCount(new EntityWrapper<WrkDetl>().eq("zpallet", palletId)) > 0;
    }
    private boolean isErpCallSuccess(String response) {
        if (Cools.isEmpty(response)) {
            return false;
        }
        try {
            JSONObject jsonObject = JSON.parseObject(response);
            if (jsonObject == null) {
                return false;
            }
            Boolean success = jsonObject.getBoolean("success");
            if (success != null) {
                return success;
            }
            Integer code = jsonObject.getInteger("code");
            if (code != null) {
                return code == 200 || code == 0;
            }
            Integer status = jsonObject.getInteger("status");
            if (status != null) {
                return status == 200 || status == 0;
            }
            String result = jsonObject.getString("result");
            if (!Cools.isEmpty(result)) {
                return "success".equalsIgnoreCase(result) || "ok".equalsIgnoreCase(result) || "true".equalsIgnoreCase(result);
            }
        } catch (Exception ignore) {
            return response.toLowerCase().contains("success") && response.toLowerCase().contains("true");
        }
        return false;
    }
    private String extractErpCallError(String response) {
        if (Cools.isEmpty(response)) {
            return "empty erp response";
        }
        try {
            JSONObject jsonObject = JSON.parseObject(response);
            if (jsonObject == null) {
                return response;
            }
            String[] keys = new String[]{"msg", "message", "error", "errMsg"};
            for (String key : keys) {
                String value = jsonObject.getString(key);
                if (!Cools.isEmpty(value)) {
                    return value;
                }
            }
        } catch (Exception ignore) {
        }
        return response;
    }
    private boolean isErpReportEnabled() {
        if (!erpReportOld) {
            return false;
        }
        String erpReport = Parameter.get().getErpReport();
        return Cools.isEmpty(erpReport) || "true".equalsIgnoreCase(erpReport);
    }
    private List<String> normalizeBarcodes(List<String> barcodes) {
        if (barcodes == null || barcodes.isEmpty()) {
            return Collections.emptyList();
        }
        LinkedHashSet<String> normalized = new LinkedHashSet<>();
        for (String barcode : barcodes) {
            if (barcode == null) {
                continue;
            }
            String value = barcode.trim();
            if (!value.isEmpty()) {
                normalized.add(value);
            }
        }
        return new ArrayList<>(normalized);
    }
    private Map<String, WrkMastLog> loadLatestInboundHistoryLogMap(List<String> barcodes) {
        Map<String, WrkMastLog> latestInboundLogMap = new LinkedHashMap<>();
        if (barcodes == null || barcodes.isEmpty()) {
            return latestInboundLogMap;
        }
        List<WrkMastLog> wrkMastLogs = wrkMastLogService.selectList(new EntityWrapper<WrkMastLog>()
                .in("barcode", barcodes)
                .orderBy("barcode", true)
                .orderBy("io_time", false)
                .orderBy("wrk_no", false));
        if (Cools.isEmpty(wrkMastLogs)) {
            return latestInboundLogMap;
        }
        for (WrkMastLog wrkMastLog : wrkMastLogs) {
            if (wrkMastLog == null || Cools.isEmpty(wrkMastLog.getBarcode())) {
                continue;
            }
            if (!isInboundHistoryLog(wrkMastLog.getIoType())) {
                continue;
            }
            latestInboundLogMap.putIfAbsent(wrkMastLog.getBarcode(), wrkMastLog);
        }
        return latestInboundLogMap;
    }
    private boolean isInboundHistoryLog(Integer ioType) {
        if (ioType == null) {
            return false;
        }
        // 历史表里既有入库完成,也有库存调整;这里只取真正的入库类记录。
        return ioType < 19 || ioType == 53 || ioType == 54 || ioType == 57;
    }
    private Map<Integer, List<WrkDetlLog>> loadWrkDetlLogMap(Collection<WrkMastLog> wrkMastLogs) {
        Map<Integer, List<WrkDetlLog>> wrkDetlLogMap = new HashMap<>();
        if (wrkMastLogs == null || wrkMastLogs.isEmpty()) {
            return wrkDetlLogMap;
        }
        LinkedHashSet<Integer> wrkNos = new LinkedHashSet<>();
        for (WrkMastLog wrkMastLog : wrkMastLogs) {
            if (wrkMastLog != null && wrkMastLog.getWrkNo() != null) {
                wrkNos.add(wrkMastLog.getWrkNo());
            }
        }
        if (wrkNos.isEmpty()) {
            return wrkDetlLogMap;
        }
        List<WrkDetlLog> wrkDetlLogs = wrkDetlLogService.selectList(new EntityWrapper<WrkDetlLog>().in("wrk_no", wrkNos));
        if (Cools.isEmpty(wrkDetlLogs)) {
            return wrkDetlLogMap;
        }
        for (WrkDetlLog wrkDetlLog : wrkDetlLogs) {
            if (wrkDetlLog == null || wrkDetlLog.getWrkNo() == null) {
                continue;
            }
            wrkDetlLogMap.computeIfAbsent(wrkDetlLog.getWrkNo(), k -> new ArrayList<>()).add(wrkDetlLog);
        }
        return wrkDetlLogMap;
    }
    private Map<String, WaitPakinLog> loadWaitPakinLogMap(List<String> barcodes) {
        Map<String, WaitPakinLog> waitPakinLogMap = new LinkedHashMap<>();
        if (barcodes == null || barcodes.isEmpty()) {
            return waitPakinLogMap;
        }
        List<WaitPakinLog> waitPakinLogs = waitPakinLogService.selectList(new EntityWrapper<WaitPakinLog>()
                .in("zpallet", barcodes)
                .orderBy("zpallet", true)
                .orderBy("appe_time", false)
                .orderBy("modi_time", false));
        if (Cools.isEmpty(waitPakinLogs)) {
            return waitPakinLogMap;
        }
        for (WaitPakinLog waitPakinLog : waitPakinLogs) {
            if (waitPakinLog == null || Cools.isEmpty(waitPakinLog.getZpallet())) {
                continue;
            }
            waitPakinLogMap.putIfAbsent(waitPakinLog.getZpallet(), waitPakinLog);
        }
        return waitPakinLogMap;
    }
    private ErpPakinReportParam buildInboundErpParam(String barcode,
                                                     WrkMastLog wrkMastLog,
                                                     List<WrkDetlLog> wrkDetlLogs,
                                                     WaitPakinLog waitPakinLog) {
        ErpPakinReportParam param = new ErpPakinReportParam();
        param.setPalletId(resolvePalletId(barcode, wrkMastLog, wrkDetlLogs, waitPakinLog));
        param.setAnfme(resolveInboundAnfme(wrkDetlLogs, waitPakinLog));
        param.setLocId(transformInboundLocId(resolveInboundLocNo(wrkMastLog, waitPakinLog)));
        param.setWeight(resolveInboundWeight(wrkMastLog, waitPakinLog));
        param.setCreateTime(formatDate(resolveInboundCreateTime(wrkMastLog)));
        param.setBizNo(resolveInboundBizNo(wrkDetlLogs, waitPakinLog));
        param.setStartTime(formatDate(resolveInboundStartTime(wrkMastLog, waitPakinLog)));
        return param;
    }
    private String resolvePalletId(String barcode,
                                   WrkMastLog wrkMastLog,
                                   List<WrkDetlLog> wrkDetlLogs,
                                   WaitPakinLog waitPakinLog) {
        if (wrkMastLog != null && !Cools.isEmpty(wrkMastLog.getBarcode())) {
            return wrkMastLog.getBarcode();
        }
        if (wrkDetlLogs != null) {
            for (WrkDetlLog wrkDetlLog : wrkDetlLogs) {
                if (wrkDetlLog != null && !Cools.isEmpty(wrkDetlLog.getZpallet())) {
                    return wrkDetlLog.getZpallet();
                }
            }
        }
        if (waitPakinLog != null && !Cools.isEmpty(waitPakinLog.getZpallet())) {
            return waitPakinLog.getZpallet();
        }
        return barcode;
    }
    private Double resolveInboundAnfme(List<WrkDetlLog> wrkDetlLogs, WaitPakinLog waitPakinLog) {
        double total = 0D;
        boolean hasDetail = false;
        if (wrkDetlLogs != null) {
            for (WrkDetlLog wrkDetlLog : wrkDetlLogs) {
                if (wrkDetlLog == null || wrkDetlLog.getAnfme() == null) {
                    continue;
                }
                total += wrkDetlLog.getAnfme();
                hasDetail = true;
            }
        }
        if (hasDetail) {
            return total;
        }
        if (waitPakinLog != null && waitPakinLog.getAnfme() != null) {
            return waitPakinLog.getAnfme();
        }
        return total;
    }
    private BigDecimal resolveInboundWeight(WrkMastLog wrkMastLog, WaitPakinLog waitPakinLog) {
        BigDecimal baseWeight = BigDecimal.ZERO;
        if (wrkMastLog != null && wrkMastLog.getScWeight() != null) {
            baseWeight = BigDecimal.valueOf(wrkMastLog.getScWeight());
        } else if (waitPakinLog != null && waitPakinLog.getWeight() != null) {
            baseWeight = BigDecimal.valueOf(waitPakinLog.getWeight());
        }
        Integer sourceStaNo = resolveInboundSourceStaNo(wrkMastLog);
        return baseWeight.multiply(resolveInboundWeightFactor(sourceStaNo));
    }
    private Integer resolveInboundSourceStaNo(WrkMastLog wrkMastLog) {
        if (wrkMastLog == null) {
            return null;
        }
        if (wrkMastLog.getSourceStaNo() != null) {
            return wrkMastLog.getSourceStaNo();
        }
        return wrkMastLog.getStaNo();
    }
    private BigDecimal resolveInboundWeightFactor(Integer sourceStaNo) {
        if (sourceStaNo == null) {
            return BigDecimal.ONE;
        }
        BigDecimal factor = INBOUND_WEIGHT_FACTOR_BY_SOURCE_STA.get(sourceStaNo);
        return factor == null ? BigDecimal.ONE : factor;
    }
    private String resolveInboundLocNo(WrkMastLog wrkMastLog, WaitPakinLog waitPakinLog) {
        if (wrkMastLog != null && !Cools.isEmpty(wrkMastLog.getLocNo())) {
            return wrkMastLog.getLocNo();
        }
        if (waitPakinLog != null && !Cools.isEmpty(waitPakinLog.getLocNo())) {
            return waitPakinLog.getLocNo();
        }
        return null;
    }
    private Date resolveInboundCreateTime(WrkMastLog wrkMastLog) {
        if (wrkMastLog == null) {
            return new Date();
        }
        if (wrkMastLog.getCrnEndTime() != null) {
            return wrkMastLog.getCrnEndTime();
        }
        if (wrkMastLog.getModiTime() != null) {
            return wrkMastLog.getModiTime();
        }
        if (wrkMastLog.getIoTime() != null) {
            return wrkMastLog.getIoTime();
        }
        return new Date();
    }
    private Date resolveInboundStartTime(WrkMastLog wrkMastLog, WaitPakinLog waitPakinLog) {
        if (waitPakinLog != null && waitPakinLog.getAppeTime() != null) {
            return waitPakinLog.getAppeTime();
        }
        if (wrkMastLog == null) {
            return new Date();
        }
        if (wrkMastLog.getAppeTime() != null) {
            return wrkMastLog.getAppeTime();
        }
        if (wrkMastLog.getIoTime() != null) {
            return wrkMastLog.getIoTime();
        }
        if (wrkMastLog.getModiTime() != null) {
            return wrkMastLog.getModiTime();
        }
        return new Date();
    }
    private String resolveInboundBizNo(List<WrkDetlLog> wrkDetlLogs, WaitPakinLog waitPakinLog) {
        if (wrkDetlLogs != null) {
            for (WrkDetlLog wrkDetlLog : wrkDetlLogs) {
                if (wrkDetlLog != null && !Cools.isEmpty(wrkDetlLog.getThreeCode())) {
                    return wrkDetlLog.getThreeCode();
                }
            }
        }
        if (waitPakinLog != null && !Cools.isEmpty(waitPakinLog.getThreeCode())) {
            return waitPakinLog.getThreeCode();
        }
        return null;
    }
    private String transformInboundLocId(String locId) {
        if (Cools.isEmpty(locId)) {
            return null;
        }
        String trimmed = locId.trim();
        if (trimmed.length() < 7) {
            return trimmed;
        }
        String row = trimmed.substring(0, 2);
        String col = trimmed.substring(2, 5);
        String lev = trimmed.substring(5, 7);
        try {
            int rowNo = Integer.parseInt(row);
            if (rowNo >= 37) {
                row = "C" + row;
            } else if (rowNo >= 13) {
                row = "B" + row;
            } else {
                row = "A" + row;
            }
        } catch (Exception ignore) {
            return trimmed;
        }
        return row + "-" + col + "-" + lev;
    }
    private String formatDate(Date date) {
        if (date == null) {
            return null;
        }
        return new SimpleDateFormat(DATE_TIME_PATTERN).format(date);
    }
    private String buildErpInboundRequestUrl() {
        if (Cools.isEmpty(erpUrl)) {
            return erpInAddress;
        }
        if (erpInAddress == null) {
            return erpUrl;
        }
        if (erpUrl.endsWith("/") && erpInAddress.startsWith("/")) {
            return erpUrl + erpInAddress.substring(1);
        }
        if (!erpUrl.endsWith("/") && !erpInAddress.startsWith("/")) {
            return erpUrl + "/" + erpInAddress;
        }
        return erpUrl + erpInAddress;
    }
    private String buildErpOutErrorRequestUrl() {
        if (Cools.isEmpty(erpUrl)) {
            return erpOutErrorAddress;
        }
        if (erpOutErrorAddress == null) {
            return erpUrl;
        }
        if (erpUrl.endsWith("/") && erpOutErrorAddress.startsWith("/")) {
            return erpUrl + erpOutErrorAddress.substring(1);
        }
        if (!erpUrl.endsWith("/") && !erpOutErrorAddress.startsWith("/")) {
            return erpUrl + "/" + erpOutErrorAddress;
        }
        return erpUrl + erpOutErrorAddress;
    }
}