1
5 天以前 db06b944e7886832d20b8e3ae62b2cb70bcba30f
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/phyz/impl/ErpReportServiceImpl.java
@@ -3,16 +3,25 @@
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.cfg.CoercionAction;
import com.fasterxml.jackson.databind.cfg.CoercionInputShape;
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.openApi.config.PlatformProperties;
import com.vincent.rsf.openApi.entity.app.OpenApiOrder;
import com.vincent.rsf.openApi.entity.app.OpenApiOrderItem;
import com.vincent.rsf.openApi.entity.app.OpenApiOrderItemMap;
import com.vincent.rsf.openApi.entity.constant.WmsConstant;
import com.vincent.rsf.openApi.entity.dto.CommonResponse;
import com.vincent.rsf.openApi.entity.phyz.ErpReportParams;
import com.vincent.rsf.openApi.entity.phyz.Order;
import com.vincent.rsf.openApi.entity.phyz.*;
import com.vincent.rsf.openApi.entity.params.ReportDataParam;
import com.vincent.rsf.openApi.entity.params.ReportParams;
import com.vincent.rsf.openApi.mapper.OpenApiOrderItemMapMapper;
import com.vincent.rsf.openApi.mapper.OpenApiOrderItemMapper;
import com.vincent.rsf.openApi.mapper.OpenApiOrderMapper;
import com.vincent.rsf.openApi.service.WmsErpService;
import com.vincent.rsf.openApi.service.phyz.ErpReportService;
import com.vincent.rsf.openApi.utils.ParamsMapUtils;
@@ -34,8 +43,8 @@
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
@@ -43,9 +52,12 @@
import java.security.cert.X509Certificate;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Service
@@ -63,6 +75,14 @@
    private WmsErpService wmsErpService;
    @Resource
    private RestTemplate restTemplate;
    @Resource
    private OpenApiOrderMapper openApiOrderMapper;
    @Resource
    private OpenApiOrderItemMapper openApiOrderItemMapper;
    @Resource
    private OpenApiOrderItemMapMapper openApiOrderItemMapMapper;
    private static final BigDecimal ZERO = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
    @PostConstruct
@@ -70,9 +90,263 @@
        ERP_REPORT_URL = erpApi.getErpUrl();
    }
    @Override
    public String syncMaterial(List<Material> materialList){
        if (materialList.isEmpty()) {
            throw new CoolException("参数不能为空!!");
        }
        /**WMS基础配置链接*/
        String wmsUrl = wmsApi.getHost() + ":" + wmsApi.getPort() + WmsConstant.SYNC_MATNRS;
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-Type", "application/json");
        headers.add("api-version", "v2.0");
        // Material -> BaseMatParms 字段一一对应后再下发
        List<Map<String, Object>> baseMatParamsList = materialToBaseMatParams(materialList);
        HttpEntity httpEntity2 = new HttpEntity<>(baseMatParamsList, headers);//cs
        // sync/warehouse
        ResponseEntity<String> exchange = restTemplate.exchange(wmsUrl, HttpMethod.POST, httpEntity2, String.class);//cs
        log.info("同步物料信息返回结果: {}", exchange);
        if (Objects.isNull(exchange.getBody())) {
            throw new CoolException("查询失败!!");
        } else {
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.coercionConfigDefaults().setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsEmpty);
            try {
                CommonResponse result = objectMapper.readValue(exchange.getBody(), CommonResponse.class);
                if (result.getCode() == 200) {
//                    JSONObject object = JSONObject.parseObject(JSONObject.toJSONString(result.getData()));
                    return "200";
                } else {
                    return result.getMsg();
//                    throw new CoolException("查询失败!!");
                }
            } catch (JsonProcessingException e) {
                return e.getMessage();
//                throw new CoolException(e.getMessage());
            }
        }
    }
    /**
     * Material 字段映射为 server 端 BaseMatParms 字段
     */
    private List<Map<String, Object>> materialToBaseMatParams(List<Material> materialList) {
        List<Map<String, Object>> mappedList = new ArrayList<>();
        for (Material material : materialList) {
            if (Objects.isNull(material)) {
                continue;
            }
            Map<String, Object> map = new HashMap<>();
            map.put("maktx", material.getMakTx());
            map.put("matnr", material.getMatNr());
            map.put("groupName", material.getGroupName());
            map.put("model", material.getModel());
            map.put("weight", material.getWeight() == null ? null : material.getWeight().toString());
            map.put("color", material.getColor());
            map.put("size", material.getSize());
            map.put("spec", material.getSpec());
            map.put("describle", material.getDescribe());
            map.put("unit", material.getUnit());
            map.put("operateType", material.getOperateType());
            mappedList.add(map);
        }
        return mappedList;
    }
    public int addOrderToServer(Order order) {
    @Override
    public String syncWareHouse(List<Warehouse> warehouseList){
        if (warehouseList.isEmpty()) {
            throw new CoolException("参数不能为空!!");
        }
        /**WMS基础配置链接*/
        String wmsUrl = wmsApi.getHost() + ":" + wmsApi.getPort() + WmsConstant.SYNC_WAREHOUSE;
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-Type", "application/json");
        headers.add("api-version", "v2.0");
        // Warehouse -> WarehouseParams 字段一一对应后再下发
        List<Map<String, Object>> warehouseParamsList = warehouseToWarehouseParams(warehouseList);
        HttpEntity httpEntity2 = new HttpEntity<>(warehouseParamsList, headers);//cs
        // sync/warehouse
        ResponseEntity<String> exchange = restTemplate.exchange(wmsUrl, HttpMethod.POST, httpEntity2, String.class);//cs
        log.info("同步仓库信息返回结果: {}", exchange);
        if (Objects.isNull(exchange.getBody())) {
            throw new CoolException("查询失败!!");
        } else {
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.coercionConfigDefaults().setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsEmpty);
            try {
                CommonResponse result = objectMapper.readValue(exchange.getBody(), CommonResponse.class);
                if (result.getCode() == 200) {
//                    JSONObject object = JSONObject.parseObject(JSONObject.toJSONString(result.getData()));
                    return "200";
                } else {
                    return result.getMsg();
//                    throw new CoolException("查询失败!!");
                }
            } catch (JsonProcessingException e) {
                return e.getMessage();
//                throw new CoolException(e.getMessage());
            }
        }
    }
    @Override
    public String syncCustomer(List<Customer> customerList){
        if (customerList.isEmpty()) {
            throw new CoolException("参数不能为空!!");
        }
        /**WMS基础配置链接*/
        String wmsUrl = wmsApi.getHost() + ":" + wmsApi.getPort() + WmsConstant.SYNC_COMPANIES;
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-Type", "application/json");
        headers.add("api-version", "v2.0");
        // Customer -> CompaniesParam 字段一一对应后再下发
        List<Map<String, Object>> companiesParamsList = customerToCompaniesParams(customerList);
        HttpEntity httpEntity2 = new HttpEntity<>(companiesParamsList, headers);//cs
        ResponseEntity<String> exchange = restTemplate.exchange(wmsUrl, HttpMethod.POST, httpEntity2, String.class);//cs
        log.info("同步客户信息返回结果: {}", exchange);
        if (Objects.isNull(exchange.getBody())) {
            throw new CoolException("查询失败!!");
        } else {
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.coercionConfigDefaults().setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsEmpty);
            try {
                CommonResponse result = objectMapper.readValue(exchange.getBody(), CommonResponse.class);
                if (result.getCode() == 200) {
                    return "200";
                } else {
                    return result.getMsg();
                }
            } catch (JsonProcessingException e) {
                return e.getMessage();
            }
        }
    }
    @Override
    public String syncSupplier(List<Supplier> supplierList){
        if (supplierList.isEmpty()) {
            throw new CoolException("参数不能为空!!");
        }
        /**WMS基础配置链接*/
        String wmsUrl = wmsApi.getHost() + ":" + wmsApi.getPort() + WmsConstant.SYNC_COMPANIES;
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-Type", "application/json");
        headers.add("api-version", "v2.0");
        // Supplier -> supplierParams 字段一一对应后再下发
        List<Map<String, Object>> supplierParamsList = customerToSupplierParams(supplierList);
        HttpEntity httpEntity2 = new HttpEntity<>(supplierParamsList, headers);//cs
        ResponseEntity<String> exchange = restTemplate.exchange(wmsUrl, HttpMethod.POST, httpEntity2, String.class);//cs
        log.info("同步客户信息返回结果: {}", exchange);
        if (Objects.isNull(exchange.getBody())) {
            throw new CoolException("查询失败!!");
        } else {
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.coercionConfigDefaults().setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsEmpty);
            try {
                CommonResponse result = objectMapper.readValue(exchange.getBody(), CommonResponse.class);
                if (result.getCode() == 200) {
                    return "200";
                } else {
                    return result.getMsg();
                }
            } catch (JsonProcessingException e) {
                return e.getMessage();
            }
        }
    }
    /**
     * Warehouse 字段映射为 server 端 WarehouseParams 字段
     */
    private List<Map<String, Object>> warehouseToWarehouseParams(List<Warehouse> warehouseList) {
        List<Map<String, Object>> mappedList = new ArrayList<>();
        for (Warehouse warehouse : warehouseList) {
            if (Objects.isNull(warehouse)) {
                continue;
            }
            Map<String, Object> map = new HashMap<>();
            map.put("name", warehouse.getWareHouseName());
            map.put("code", warehouse.getWareHouseId());
            map.put("factory",  warehouse.getUseOrgName());
            map.put("factoryId",  warehouse.getUseOrgId());
            map.put("address", warehouse.getAddress());
            map.put("type", null);
            map.put("longitude", null);
            map.put("latitude", null);
            map.put("operateType", warehouse.getOperateType());
            mappedList.add(map);
        }
        return mappedList;
    }
    /**
     * Customer 字段映射为 server 端 CompaniesParam 字段
     */
    private List<Map<String, Object>> customerToCompaniesParams(List<Customer> customerList) {
        List<Map<String, Object>> mappedList = new ArrayList<>();
        int rowNum = 0;
        for (Customer customer : customerList) {
            rowNum++;
            if (Objects.isNull(customer)) {
                continue;
            }
            if (StringUtils.isBlank(customer.getCustomerId())) {
                throw new CoolException("客户同步失败:第" + rowNum + "行客户编码为空");
            }
            if (StringUtils.isBlank(customer.getCustomerName())) {
                throw new CoolException("客户同步失败:第" + rowNum + "行客户名称为空");
            }
            Map<String, Object> map = new HashMap<>();
            map.put("name", customer.getCustomerName());
            map.put("nameEn", null);
            map.put("breifCode", customer.getCustomerNickName());
            // server 端类型转换使用中文描述
            map.put("type", "客户");
            map.put("contact", customer.getContact());
            map.put("tel", customer.getTelephone());
            map.put("email", customer.getEmail());
            map.put("pcode", null);
            map.put("city", customer.getCustomerGroup());
            map.put("province", null);
            map.put("address", customer.getAddress());
            map.put("code", customer.getCustomerId());
            map.put("operateType", customer.getOperateType());
            mappedList.add(map);
        }
        return mappedList;
    }
    /**
     * Customer 字段映射为 server 端 CompaniesParam 字段
     */
    private List<Map<String, Object>> customerToSupplierParams(List<Supplier> supplierList) {
        List<Map<String, Object>> mappedList = new ArrayList<>();
        for (Supplier supplier : supplierList) {
            if (Objects.isNull(supplier)) {
                continue;
            }
            Map<String, Object> map = new HashMap<>();
            map.put("name", supplier.getSupplierName());
            map.put("nameEn", null);
            map.put("breifCode", supplier.getSupplierName());
            // server 端类型转换使用中文描述
            map.put("type", supplier.getType());
            map.put("contact", supplier.getContact());
            map.put("tel", supplier.getTelephone());
            map.put("email", supplier.getEmail());
            map.put("pcode", null);
            map.put("city", supplier.getSupplierGroup());
            map.put("province", null);
            map.put("address", supplier.getAddress());
            map.put("code", supplier.getSupplierId());
            map.put("operateType", supplier.getOperateType());
            mappedList.add(map);
        }
        return mappedList;
    }
    @Override
    public String addOrderToServer(Order order) {
        if (Objects.isNull(order.getOrderNo()) || order.getOrderNo().isEmpty()) {
            throw new CoolException("订单号不能为空!!");
        }
@@ -94,12 +368,16 @@
        JSONObject params = JSONObject.parseObject(JSON.toJSONString(order));
        JSONObject mappedData = ParamsMapUtils.apiMaps("erp", "orderId", params);
        mappedData.put("updateBy", "erp");
        mapParams = objectToMap(mappedData);
        maps.add(mapParams);
        log.info("修改订单信息及状态: {}, 请求参数: {}", wmsUrl, JSONArray.toJSONString(maps));
        HttpEntity<List<Map<String, Object>>> httpEntity = new HttpEntity<>(maps, headers);
        ArrayList<Order> orders = new ArrayList<>();
        orders.add(order);
        HttpEntity httpEntity2 = new HttpEntity<>(orders, headers);//cs
        // asnOrder/items/save
        ResponseEntity<String> exchange = restTemplate.exchange(wmsUrl, HttpMethod.POST, httpEntity, String.class);
        ResponseEntity<String> exchange = restTemplate.exchange(wmsUrl, HttpMethod.POST, httpEntity2, String.class);//cs
        log.info("订单修改返回结果: {}", exchange);
        if (Objects.isNull(exchange.getBody())) {
            throw new CoolException("查询失败!!");
@@ -110,13 +388,13 @@
                CommonResponse result = objectMapper.readValue(exchange.getBody(), CommonResponse.class);
                if (result.getCode() == 200) {
//                    JSONObject object = JSONObject.parseObject(JSONObject.toJSONString(result.getData()));
                    return 1;
                    return "200";
                } else {
                    return 0;
                    return result.getMsg();
//                    throw new CoolException("查询失败!!");
                }
            } catch (JsonProcessingException e) {
                return 0;
                return e.getMessage();
//                throw new CoolException(e.getMessage());
            }
        }
@@ -132,6 +410,650 @@
//        asnOrderService.saveOrderAndItems(params, getLoginUserId());
    }
    @Override
    public String addOrderToServerNew(Order order) {
        if (Objects.isNull(order) || StringUtils.isBlank(order.getOrderNo())) {
            throw new CoolException("订单号不能为空!!");
        }
        if (Objects.isNull(order.getOrderItems()) || order.getOrderItems().isEmpty()) {
            throw new CoolException("订单明细不能为空!!");
        }
        OpenApiOrder persistedOrder = saveOpenApiOrder(order);
        List<OpenApiOrderItem> persistedItems = saveOpenApiOrderItems(persistedOrder, order.getOrderItems());
        saveOpenApiOrderItemMappings(persistedOrder, persistedItems);
        Order mergedOrder = buildMergedOrder(order);
        String dispatchResult = addOrderToServer(mergedOrder);
        if (!"200".equals(dispatchResult)) {
            return dispatchResult;
        }
        return "200";
    }
    @Override
    public CommonResponse reportOrderNew(Object params) {
        if (Objects.isNull(params)) {
            throw new CoolException("参数不能为空!!");
        }
        JSONObject root = JSONObject.parseObject(JSON.toJSONString(params));
        String orderNo = pickString(root, "orderNo", "OrderNo", "WMSNO", "wmsno");
        if (StringUtils.isBlank(orderNo)) {
            return CommonResponse.error("orderNo不能为空");
        }
        // 解析明细:优先取 orderItems(Order格式),兼容 Data/data(ReportParams格式)
        JSONArray dataArray = pickArray(root, "orderItems", "orderItems");
        if (Objects.isNull(dataArray) || dataArray.isEmpty()) {
            dataArray = pickArray(root, "Data", "data");
        }
        if (Objects.isNull(dataArray) || dataArray.isEmpty()) {
            return CommonResponse.error("回传明细为空");
        }
        // 对回传数据做反映射(WMS字段名 → ERP字段名),兼容旧格式
        JSONObject mappedRoot = ParamsMapUtils.reverseApiMaps("erp", "orderId", root);
        JSONArray mappedDataArray = pickArray(mappedRoot, "orderItems", "orderItems");
        if (Objects.isNull(mappedDataArray) || mappedDataArray.isEmpty()) {
            mappedDataArray = pickArray(mappedRoot, "Data", "data");
        }
        if (Objects.isNull(mappedDataArray) || mappedDataArray.isEmpty()) {
            mappedDataArray = dataArray;
        }
        int allocateCount = 0;
        List<String> errors = new ArrayList<>();
        Set<String> affectedOrderNos = new HashSet<>();
        Map<String, OpenApiOrder> orderCache = new HashMap<>();
        for (int i = 0; i < mappedDataArray.size(); i++) {
            JSONObject row = mappedDataArray.getJSONObject(i);
            String matnrCode = pickString(row, "matNr", "MatNr", "ItemCode", "itemCode");
            String batch = pickString(row, "batch", "Batch", "GoodsNO", "goodsNo");
            String lineId = pickString(row, "lineId", "LineId", "sourceLineId");
            BigDecimal doneQty = pickDecimal(row, "doneQty", "anfme", "qty", "InQty", "inQty", "OutQty", "outQty", "pdqty", "PDQty");
            if (StringUtils.isBlank(matnrCode) || doneQty.compareTo(ZERO) <= 0) {
                errors.add("第" + (i + 1) + "行缺少必要字段(matNr/数量)");
                continue;
            }
            doneQty = doneQty.setScale(2, RoundingMode.HALF_UP);
            try {
                int singleAllocated = allocateMergedDoneQty(orderNo, matnrCode, batch, lineId, doneQty, orderCache);
                allocateCount += singleAllocated;
                affectedOrderNos.add(orderNo);
            } catch (Exception e) {
                log.error("处理回传失败,orderNo={}, matnr={}, batch={}, lineId={}", orderNo, matnrCode, batch, lineId, e);
                errors.add("第" + (i + 1) + "行处理失败:" + e.getMessage());
            }
        }
        // 循环结束后,每个受影响订单只刷新一次状态
        for (String no : affectedOrderNos) {
            refreshOrderFinishStatus(no);
        }
        if (!errors.isEmpty()) {
            return CommonResponse.error("处理完成,但存在异常:" + String.join(" | ", errors));
        }
        Map<String, Object> result = new HashMap<>();
        result.put("allocatedCount", allocateCount);
        return CommonResponse.ok(result);
    }
    /**
     * 查询已完成但未上报ERP的订单,组装ReportParams上报ERP
     * 由asrs-schedule定时任务触发
     */
    @Override
    public CommonResponse reportOrdersToErp() {
        List<OpenApiOrder> pendingOrders = openApiOrderMapper.selectList(new LambdaQueryWrapper<OpenApiOrder>()
                .eq(OpenApiOrder::getExceStatus, 2)
                .eq(OpenApiOrder::getNtyStatus, 0)
                .last("limit 50"));
        if (pendingOrders.isEmpty()) {
            return CommonResponse.ok("无待上报ERP的订单");
        }
        List<String> errors = new ArrayList<>();
        int successCount = 0;
        for (OpenApiOrder order : pendingOrders) {
            try {
                List<OpenApiOrderItem> items = openApiOrderItemMapper.selectList(new LambdaQueryWrapper<OpenApiOrderItem>()
                        .eq(OpenApiOrderItem::getOrderId, order.getId()));
                if (items.isEmpty()) {
                    log.warn("订单无明细,跳过上报ERP,orderNo={}", order.getCode());
                    continue;
                }
                ReportParams erpParams = buildReportParams(order, items);
                CommonResponse erpResp = wmsErpService.reportOrders(erpParams);
                if (Objects.nonNull(erpResp) && Objects.equals(erpResp.getCode(), 200)) {
                    order.setNtyStatus(1);
                    openApiOrderMapper.updateById(order);
                    successCount++;
                    log.info("上报ERP成功,orderNo={}", order.getCode());
                } else {
                    String msg = Objects.isNull(erpResp) ? "ERP响应为空" : erpResp.getMsg();
                    log.warn("上报ERP失败,orderNo={},msg={}", order.getCode(), msg);
                    errors.add(order.getCode() + ":" + msg);
                }
            } catch (Exception e) {
                log.error("上报ERP异常,orderNo={}", order.getCode(), e);
                errors.add(order.getCode() + ":" + e.getMessage());
            }
        }
        if (!errors.isEmpty()) {
            Map<String, Object> result = new HashMap<>();
            result.put("successCount", successCount);
            result.put("totalCount", pendingOrders.size());
            return CommonResponse.error("部分订单上报ERP失败:" + String.join(" | ", errors));
        }
        Map<String, Object> result = new HashMap<>();
        result.put("successCount", successCount);
        result.put("totalCount", pendingOrders.size());
        return CommonResponse.ok(result);
    }
    /**
     * 从OpenApiOrder+OpenApiOrderItem组装ReportParams
     */
    private ReportParams buildReportParams(OpenApiOrder order, List<OpenApiOrderItem> items) {
        String erpOrderType = resolveErpOrderType(order.getType(), order.getWkType());
        List<ReportDataParam> reportDataList = new ArrayList<>();
        for (OpenApiOrderItem item : items) {
            BigDecimal qty = defaultQty(item.getQty());
            if (qty.compareTo(ZERO) <= 0) {
                continue;
            }
            ReportDataParam dataParam = new ReportDataParam()
                    .setWMSNO(order.getCode())
                    .setPONO(order.getPoCode())
                    .setOrderNO(order.getCode())
                    .setGoodsNO(item.getBatch())
                    .setItemCode(item.getMatnrCode())
                    .setEditUser("schedule")
                    .setEditDate(new Date())
                    .setMemoDtl(item.getMemo());
            if ("in".equalsIgnoreCase(order.getType())) {
                dataParam.setInQty(qty.doubleValue());
            } else {
                dataParam.setOutQty(qty.doubleValue());
            }
            reportDataList.add(dataParam);
        }
        return new ReportParams()
                .setOrderType(erpOrderType)
                .setAction("Update")
                .setData(reportDataList);
    }
    /**
     * 根据type和wkType解析ERP上报的订单类型
     */
    private String resolveErpOrderType(String type, String wkType) {
        if (StringUtils.isBlank(wkType)) {
            return "in".equalsIgnoreCase(type) ? "PO_Instock" : "WO_Outstock";
        }
        switch (wkType) {
            case "PUR_ReceiveBill": return "PO_Instock";
            case "PUR_MRB": return "PR_Outstock";
            case "PRD_ReturnMtrl": return "WR_Instock";
            case "PRD_PickMtrl": return "WO_Outstock";
            case "PRD_FeedMtrl": return "WR_Instock_BL";
            case "PRD_INSTOCK":
            case "PRD_MORPT": return "WO_Outstock";
            case "STK_MISCELLANEOUS": return "In_Instock";
            case "STK_MisDelivery": return "Io_Outstock";
            default:
                return "in".equalsIgnoreCase(type) ? "PO_Instock" : "WO_Outstock";
        }
    }
    private OpenApiOrder saveOpenApiOrder(Order order) {
        OpenApiOrder existing = openApiOrderMapper.selectOne(new LambdaQueryWrapper<OpenApiOrder>()
                .eq(OpenApiOrder::getCode, order.getOrderNo())
                .last("limit 1"));
        BigDecimal totalQty = sumOrderItemQty(order.getOrderItems());
        Date businessTime = toDate(order.getBusinessTime());
        OpenApiOrder target = Objects.isNull(existing) ? new OpenApiOrder() : existing;
        target.setCode(order.getOrderNo())
                .setType(order.getType())
                .setWkType(order.getWkType())
                .setAnfme(totalQty)
                .setQty(ZERO)
                .setWorkQty(ZERO)
                .setExceStatus(0)
                .setNtyStatus(0)
                .setStatus(1)
                .setBusinessTime(businessTime)
                .setOrderInternalCode(order.getOrderInternalCode())
                .setStationId(order.getStationId())
                .setStockDirect(order.getStockDirect())
                .setCustomerId(order.getCustomerId())
                .setCustomerName(order.getCustomerName())
                .setSupplierId(order.getSupplierId())
                .setSupplierName(order.getSupplierName())
                .setStockOrgId(order.getStockOrgId())
                .setStockOrgName(order.getStockOrgName())
                .setPurchaseOrgId(order.getPurchaseOrgId())
                .setPurchaseOrgName(order.getPurchaseOrgName())
                .setPurchaseUserId(order.getPurchaseUserId())
                .setPurchaseUserName(order.getPurchaseUserName())
                .setPrdOrgId(order.getPrdOrgId())
                .setPrdOrgName(order.getPrdOrgName())
                .setSaleOrgId(order.getSaleOrgId())
                .setSaleOrgName(order.getSaleOrgName())
                .setSaleUserId(order.getSaleUserId())
                .setSaleUserName(order.getSaleUserName());
        if (Objects.isNull(existing)) {
            openApiOrderMapper.insert(target);
        } else {
            openApiOrderMapper.updateById(target);
            openApiOrderItemMapper.delete(new LambdaQueryWrapper<OpenApiOrderItem>().eq(OpenApiOrderItem::getOrderId, target.getId()));
            openApiOrderItemMapMapper.delete(new LambdaQueryWrapper<OpenApiOrderItemMap>().eq(OpenApiOrderItemMap::getOrderId, target.getId()));
        }
        return target;
    }
    private List<OpenApiOrderItem> saveOpenApiOrderItems(OpenApiOrder order, List<OrderItem> orderItems) {
        List<OpenApiOrderItem> saved = new ArrayList<>();
        for (OrderItem src : orderItems) {
            OpenApiOrderItem target = new OpenApiOrderItem()
                    .setOrderId(order.getId())
                    .setOrderCode(order.getCode())
                    .setPlatItemId(src.getLineId())
                    .setPlatWorkCode(src.getPlanNo())
                    .setMatnrCode(src.getMatNr())
                    .setMaktx(src.getMakTx())
                    .setModel(src.getModel())
                    .setSpec(src.getSpec())
                    .setAnfme(toDecimal(src.getAnfme()))
                    .setStockUnit(src.getUnit())
                    .setWorkQty(ZERO)
                    .setQty(ZERO)
                    .setBatch(src.getBatch())
                    .setMemo(src.getMemo())
                    .setTargetWarehouseId(src.getTargetWarehouseId())
                    .setSourceWarehouseId(src.getSourceWarehouseId())
                    .setOwnerId(src.getOwnerId())
                    .setBaseUnit(src.getBaseUnit())
                    .setUseOrgId(src.getUseOrgId())
                    .setUseOrgName(src.getUseOrgName())
                    .setErpClsId(src.getErpClsId())
                    .setPriceUnitId(src.getPriceUnitId())
                    .setInStockType(src.getInStockType())
                    .setOwnerTypeId(src.getOwnerTypeId())
                    .setOwnerName(src.getOwnerName())
                    .setKeeperTypeId(src.getKeeperTypeId())
                    .setKeeperId(src.getKeeperId())
                    .setKeeperName(src.getKeeperName())
                    .setStatus(1)
                    .setDeleted(0);
            openApiOrderItemMapper.insert(target);
            saved.add(target);
        }
        return saved;
    }
    private void saveOpenApiOrderItemMappings(OpenApiOrder order, List<OpenApiOrderItem> items) {
        Map<String, List<OpenApiOrderItem>> grouped = items.stream().collect(Collectors.groupingBy(i -> mergeKey(i.getMatnrCode(), i.getBatch())));
        for (List<OpenApiOrderItem> sameGroupItems : grouped.values()) {
            sameGroupItems.sort(Comparator.comparing(OpenApiOrderItem::getId));
            for (int i = 0; i < sameGroupItems.size(); i++) {
                OpenApiOrderItem item = sameGroupItems.get(i);
                OpenApiOrderItemMap map = new OpenApiOrderItemMap()
                        .setOrderId(order.getId())
                        .setOrderCode(order.getCode())
                        .setSourceItemId(item.getId())
                        .setSourceLineId(item.getPlatItemId())
                        .setMergeMatnrCode(defaultString(item.getMatnrCode()))
                        .setMergeBatch(defaultString(item.getBatch()))
                        .setSeqNo(i + 1)
                        .setSourceQty(defaultQty(item.getAnfme()))
                        .setAllocatedQty(ZERO)
                        .setReportedQty(ZERO)
                        .setStatus(1)
                        .setDeleted(0);
                openApiOrderItemMapMapper.insert(map);
            }
        }
    }
    private Order buildMergedOrder(Order source) {
        Order merged = new Order()
                .setOrderNo(source.getOrderNo())
                .setOrderInternalCode(source.getOrderInternalCode())
                .setWkType(source.getWkType())
                .setCreateTime(source.getCreateTime())
                .setBusinessTime(source.getBusinessTime())
                .setStockDirect(source.getStockDirect())
                .setStationId(source.getStationId())
                .setCustomerId(source.getCustomerId())
                .setCustomerName(source.getCustomerName())
                .setSupplierId(source.getSupplierId())
                .setSupplierName(source.getSupplierName())
                .setStockOrgId(source.getStockOrgId())
                .setStockOrgName(source.getStockOrgName())
                .setPurchaseOrgId(source.getPurchaseOrgId())
                .setPurchaseOrgName(source.getPurchaseOrgName())
                .setPurchaseUserId(source.getPurchaseUserId())
                .setPurchaseUserName(source.getPurchaseUserName())
                .setPrdOrgId(source.getPrdOrgId())
                .setPrdOrgName(source.getPrdOrgName())
                .setSaleOrgId(source.getSaleOrgId())
                .setSaleOrgName(source.getSaleOrgName())
                .setSaleUserId(source.getSaleUserId())
                .setSaleUserName(source.getSaleUserName());
        merged.setType(source.getType());
        Map<String, List<OrderItem>> grouped = source.getOrderItems().stream().collect(Collectors.groupingBy(i -> mergeKey(i.getMatNr(), i.getBatch())));
        List<OrderItem> mergedItems = new ArrayList<>();
        int idx = 1;
        for (List<OrderItem> groupItems : grouped.values()) {
            OrderItem first = groupItems.get(0);
            BigDecimal qty = groupItems.stream()
                    .map(i -> toDecimal(i.getAnfme()))
                    .reduce(ZERO, BigDecimal::add)
                    .setScale(2, RoundingMode.HALF_UP);
            OrderItem mergedItem = new OrderItem()
                    .setLineId("M" + idx++)
                    .setPlanNo(first.getPlanNo())
                    .setMatNr(first.getMatNr())
                    .setMakTx(first.getMakTx())
                    .setModel(first.getModel())
                    .setAnfme(qty.doubleValue())
                    .setBatch(first.getBatch())
                    .setUnit(first.getUnit())
                    .setBaseUnit(first.getBaseUnit())
                    .setPriceUnitId(first.getPriceUnitId())
                    .setPalletId(first.getPalletId())
                    .setTargetWarehouseId(first.getTargetWarehouseId())
                    .setSourceWarehouseId(first.getSourceWarehouseId())
                    .setInStockType(first.getInStockType())
                    .setOwnerTypeId(first.getOwnerTypeId())
                    .setOwnerId(first.getOwnerId())
                    .setOwnerName(first.getOwnerName())
                    .setKeeperTypeId(first.getKeeperTypeId())
                    .setKeeperId(first.getKeeperId())
                    .setKeeperName(first.getKeeperName())
                    .setMemo(first.getMemo())
                    .setUseOrgId(first.getUseOrgId())
                    .setUseOrgName(first.getUseOrgName())
                    .setErpClsId(first.getErpClsId());
            mergedItem.setSpec(first.getSpec());
            mergedItems.add(mergedItem);
        }
        merged.setOrderItems(mergedItems);
        return merged;
    }
    private int allocateMergedDoneQty(String orderNo, String matnrCode, String batch, String lineId, BigDecimal mergedDoneQty, Map<String, OpenApiOrder> orderCache) {
        OpenApiOrder order = orderCache.computeIfAbsent(orderNo, k ->
                openApiOrderMapper.selectOne(new LambdaQueryWrapper<OpenApiOrder>()
                        .eq(OpenApiOrder::getCode, k)
                        .last("limit 1")));
        if (Objects.isNull(order)) {
            throw new CoolException("未找到订单:" + orderNo);
        }
        LambdaQueryWrapper<OpenApiOrderItemMap> queryWrapper = new LambdaQueryWrapper<OpenApiOrderItemMap>()
                .eq(OpenApiOrderItemMap::getOrderId, order.getId())
                .orderByAsc(OpenApiOrderItemMap::getSeqNo)
                .orderByAsc(OpenApiOrderItemMap::getId);
        // 优先用lineId精确匹配原始行
        if (StringUtils.isNotBlank(lineId)) {
            queryWrapper.eq(OpenApiOrderItemMap::getSourceLineId, lineId);
        } else {
            queryWrapper.eq(OpenApiOrderItemMap::getMergeMatnrCode, defaultString(matnrCode));
            if (StringUtils.isNotBlank(batch)) {
                queryWrapper.eq(OpenApiOrderItemMap::getMergeBatch, batch);
            }
        }
        List<OpenApiOrderItemMap> mappingRows = openApiOrderItemMapMapper.selectList(queryWrapper);
        if (mappingRows.isEmpty() && StringUtils.isNotBlank(lineId)) {
            // lineId匹配不到时,回退到matNr+batch匹配
            queryWrapper = new LambdaQueryWrapper<OpenApiOrderItemMap>()
                    .eq(OpenApiOrderItemMap::getOrderId, order.getId())
                    .eq(OpenApiOrderItemMap::getMergeMatnrCode, defaultString(matnrCode))
                    .orderByAsc(OpenApiOrderItemMap::getSeqNo)
                    .orderByAsc(OpenApiOrderItemMap::getId);
            if (StringUtils.isNotBlank(batch)) {
                queryWrapper.eq(OpenApiOrderItemMap::getMergeBatch, batch);
            }
            mappingRows = openApiOrderItemMapMapper.selectList(queryWrapper);
        }
        if (mappingRows.isEmpty()) {
            throw new CoolException("未找到映射关系,订单:" + orderNo + ",物料:" + matnrCode);
        }
        BigDecimal remaining = mergedDoneQty;
        int allocateRows = 0;
        for (OpenApiOrderItemMap row : mappingRows) {
            if (remaining.compareTo(ZERO) <= 0) {
                break;
            }
            BigDecimal sourceQty = defaultQty(row.getSourceQty());
            BigDecimal allocatedQty = defaultQty(row.getAllocatedQty());
            BigDecimal canAllocate = sourceQty.subtract(allocatedQty);
            if (canAllocate.compareTo(ZERO) <= 0) {
                continue;
            }
            BigDecimal toAllocate = remaining.min(canAllocate).setScale(2, RoundingMode.HALF_UP);
            if (toAllocate.compareTo(ZERO) <= 0) {
                continue;
            }
            row.setAllocatedQty(allocatedQty.add(toAllocate).setScale(2, RoundingMode.HALF_UP));
            openApiOrderItemMapMapper.updateById(row);
            openApiOrderItemMapper.update(null, new LambdaUpdateWrapper<OpenApiOrderItem>()
                    .eq(OpenApiOrderItem::getId, row.getSourceItemId())
                    .setSql("qty = IFNULL(qty,0) + " + toAllocate.toPlainString()));
            remaining = remaining.subtract(toAllocate).setScale(2, RoundingMode.HALF_UP);
            allocateRows++;
        }
        if (remaining.compareTo(ZERO) > 0) {
            log.warn("回传数量超过未分配数量,orderNo={}, matnr={}, batch={}, overflow={}", orderNo, matnrCode, batch, remaining);
        }
        return allocateRows;
    }
    private int reportReadyLinesToErp(String orderNo) {
        OpenApiOrder order = openApiOrderMapper.selectOne(new LambdaQueryWrapper<OpenApiOrder>()
                .eq(OpenApiOrder::getCode, orderNo)
                .last("limit 1"));
        if (Objects.isNull(order)) {
            return 0;
        }
        List<OpenApiOrderItemMap> mappings = openApiOrderItemMapMapper.selectList(new LambdaQueryWrapper<OpenApiOrderItemMap>()
                .eq(OpenApiOrderItemMap::getOrderId, order.getId())
                .orderByAsc(OpenApiOrderItemMap::getSeqNo)
                .orderByAsc(OpenApiOrderItemMap::getId));
        // 批量查询所有 sourceItemId 对应的 OpenApiOrderItem,消除 N+1
        List<Long> itemIds = mappings.stream()
                .map(OpenApiOrderItemMap::getSourceItemId)
                .filter(Objects::nonNull)
                .distinct()
                .toList();
        Map<Long, OpenApiOrderItem> itemMap = new HashMap<>();
        if (!itemIds.isEmpty()) {
            List<OpenApiOrderItem> items = openApiOrderItemMapper.selectBatchIds(itemIds);
            for (OpenApiOrderItem item : items) {
                itemMap.put(item.getId(), item);
            }
        }
        int successCount = 0;
        for (OpenApiOrderItemMap mapping : mappings) {
            BigDecimal allocatedQty = defaultQty(mapping.getAllocatedQty());
            BigDecimal reportedQty = defaultQty(mapping.getReportedQty());
            BigDecimal delta = allocatedQty.subtract(reportedQty).setScale(2, RoundingMode.HALF_UP);
            if (delta.compareTo(ZERO) <= 0) {
                continue;
            }
            OpenApiOrderItem item = itemMap.get(mapping.getSourceItemId());
            if (Objects.isNull(item)) {
                continue;
            }
            ReportParams params = buildLineReportParams(order, item, delta);
            CommonResponse erpResp = wmsErpService.reportOrders(params);
            if (Objects.nonNull(erpResp) && Objects.equals(erpResp.getCode(), 200)) {
                mapping.setReportedQty(reportedQty.add(delta).setScale(2, RoundingMode.HALF_UP));
                openApiOrderItemMapMapper.updateById(mapping);
                successCount++;
            } else {
                String msg = Objects.isNull(erpResp) ? "ERP响应为空" : erpResp.getMsg();
                log.warn("ERP行上报失败,orderNo={}, itemId={}, msg={}", orderNo, item.getId(), msg);
            }
        }
        return successCount;
    }
    private ReportParams buildLineReportParams(OpenApiOrder order, OpenApiOrderItem item, BigDecimal deltaQty) {
        ReportDataParam dataParam = new ReportDataParam()
                .setWMSNO(order.getCode())
                .setPONO(order.getPoCode())
                .setOrderNO(item.getPlatWorkCode())
                .setGoodsNO(item.getBatch())
                .setItemCode(item.getMatnrCode())
                .setEditUser("open-api")
                .setEditDate(new Date())
                .setMemoDtl("lineId=" + defaultString(item.getPlatItemId()));
        if ("in".equalsIgnoreCase(order.getType())) {
            dataParam.setInQty(deltaQty.doubleValue());
        } else {
            dataParam.setOutQty(deltaQty.doubleValue());
        }
        return new ReportParams().setOrderType(order.getType()).setAction("Update").setData(Collections.singletonList(dataParam));
    }
    private void refreshOrderFinishStatus(String orderNo) {
        OpenApiOrder order = openApiOrderMapper.selectOne(new LambdaQueryWrapper<OpenApiOrder>()
                .eq(OpenApiOrder::getCode, orderNo)
                .last("limit 1"));
        if (Objects.isNull(order)) {
            return;
        }
        List<OpenApiOrderItem> items = openApiOrderItemMapper.selectList(new LambdaQueryWrapper<OpenApiOrderItem>()
                .eq(OpenApiOrderItem::getOrderId, order.getId()));
        BigDecimal totalPlan = items.stream().map(i -> defaultQty(i.getAnfme())).reduce(ZERO, BigDecimal::add);
        BigDecimal totalDone = items.stream().map(i -> defaultQty(i.getQty())).reduce(ZERO, BigDecimal::add);
        int exceStatus = 0;
        if (totalDone.compareTo(ZERO) > 0 && totalDone.compareTo(totalPlan) < 0) {
            exceStatus = 1;
        } else if (totalDone.compareTo(totalPlan) >= 0 && totalPlan.compareTo(ZERO) > 0) {
            exceStatus = 2;
        }
        order.setQty(totalDone.setScale(2, RoundingMode.HALF_UP))
                .setWorkQty(totalDone.setScale(2, RoundingMode.HALF_UP))
                .setExceStatus(exceStatus);
        openApiOrderMapper.updateById(order);
    }
    private BigDecimal sumOrderItemQty(List<OrderItem> orderItems) {
        return orderItems.stream()
                .filter(Objects::nonNull)
                .map(i -> toDecimal(i.getAnfme()))
                .reduce(ZERO, BigDecimal::add)
                .setScale(2, RoundingMode.HALF_UP);
    }
    private BigDecimal toDecimal(Double value) {
        if (Objects.isNull(value)) {
            return ZERO;
        }
        return BigDecimal.valueOf(value).setScale(2, RoundingMode.HALF_UP);
    }
    private BigDecimal defaultQty(BigDecimal value) {
        return Objects.isNull(value) ? ZERO : value.setScale(2, RoundingMode.HALF_UP);
    }
    private Date toDate(Long timestampSeconds) {
        if (Objects.isNull(timestampSeconds) || timestampSeconds <= 0L) {
            return null;
        }
        return new Date(timestampSeconds * 1000);
    }
    private String mergeKey(String matnr, String batch) {
        return defaultString(matnr) + "@@" + defaultString(batch);
    }
    private String defaultString(String value) {
        return StringUtils.trimToEmpty(value);
    }
    private String pickString(JSONObject object, String... candidates) {
        if (Objects.isNull(object) || Objects.isNull(candidates)) {
            return "";
        }
        for (String key : candidates) {
            Object value = object.get(key);
            if (Objects.nonNull(value) && StringUtils.isNotBlank(String.valueOf(value))) {
                return String.valueOf(value).trim();
            }
        }
        return "";
    }
    private JSONArray pickArray(JSONObject object, String... candidates) {
        if (Objects.isNull(object) || Objects.isNull(candidates)) {
            return new JSONArray();
        }
        for (String key : candidates) {
            Object value = object.get(key);
            if (value instanceof JSONArray) {
                return (JSONArray) value;
            }
            if (value instanceof List) {
                return JSONArray.parseArray(JSON.toJSONString(value));
            }
            if (value instanceof String && ((String) value).trim().startsWith("[")) {
                return JSONArray.parseArray((String) value);
            }
        }
        return new JSONArray();
    }
    private BigDecimal pickDecimal(JSONObject object, String... candidates) {
        if (Objects.isNull(object) || Objects.isNull(candidates)) {
            return ZERO;
        }
        for (String key : candidates) {
            Object value = object.get(key);
            if (Objects.isNull(value)) {
                continue;
            }
            try {
                return new BigDecimal(String.valueOf(value)).abs().setScale(2, RoundingMode.HALF_UP);
            } catch (Exception ignore) {
                // ignore invalid value
            }
        }
        return ZERO;
    }
    public static Map<String, Object> objectToMap(Object obj) {
        Map<String, Object> map = new HashMap<>();
        Field[] fields = obj.getClass().getDeclaredFields();