1
14 小时以前 34503a2e9a29418346a4ac8f84170ab8f4321d6e
lsh#行号适配
9个文件已添加
5个文件已修改
983 ■■■■■ 已修改文件
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/WmsErpController.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/phyz/ERPController.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/app/OpenApiOrder.java 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/app/OpenApiOrderItem.java 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/app/OpenApiOrderItemMap.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/app/OpenApiOrderReportEvent.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/mapper/OpenApiOrderItemMapMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/mapper/OpenApiOrderItemMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/mapper/OpenApiOrderMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/mapper/OpenApiOrderReportEventMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/phyz/ErpReportService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/phyz/impl/ErpReportServiceImpl.java 534 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/resources/sql/open_api_order_new_tables.sql 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/WarehouseParams.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/WmsErpController.java
@@ -97,9 +97,6 @@
    @ApiOperation("订单信息上报")
    @PostMapping("/report/order")
    public CommonResponse reportOrders(@RequestBody ReportParams params) {
        if (true){
            return CommonResponse.ok();
        }
        if (Objects.isNull(params)) {
            throw new CoolException("参数不能为空!!");
        }
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/phyz/ERPController.java
@@ -41,7 +41,6 @@
    @Autowired(required = false)
    private WmsServerFeignClient wmsServerFeignClient;
    @ApiOperation("仓库信息同步")
    @PostMapping("/wareHouse/sync")
    public CommonResponse syncWareHouse(@RequestBody Object objParams) {
@@ -158,6 +157,44 @@
        return CommonResponse.ok(errorMsg.toString());
    }
    @ApiOperation("入/出库任务通知单(新链路)")
    @PostMapping("/order/add/new")
    public CommonResponse addOrderNew(@RequestBody Object objParams) {
        if (Objects.isNull(objParams)) {
            throw new CoolException("参数不能为空!!");
        }
        if (SIMULATED_DATA_ENABLE.equals("1")) {
            return CommonResponse.ok();
        }
        JSONArray params = paramsFormat(objParams);
        List<Order> orderList = JSON.parseArray(params.toJSONString(), Order.class);
        StringBuffer msg = new StringBuffer();
        boolean allSuccess = true;
        for (Order order : orderList) {
            String result = erpReportService.addOrderToServerNew(order);
            if ("200".equals(result)) {
                msg.append(order.getOrderNo()).append("下发成功;");
            } else {
                allSuccess = false;
                msg.append(order.getOrderNo()).append("下发失败,原因:").append(result).append(";");
            }
        }
        return allSuccess ? CommonResponse.ok(msg.toString()) : CommonResponse.error(msg.toString());
    }
    @ApiOperation("订单信息上报(新链路)")
    @PostMapping("/report/order/new")
    public CommonResponse reportOrderNew(@RequestBody Object objParams) {
        if (Objects.isNull(objParams)) {
            throw new CoolException("参数不能为空!!");
        }
        if (SIMULATED_DATA_ENABLE.equals("1")) {
            return CommonResponse.ok();
        }
        return erpReportService.reportOrderNew(objParams);
    }
    @ApiOperation("入/出库任务通知单取消")
    @PostMapping("/order/cancel")
    public CommonResponse cancelOrder(@RequestBody Object objParams) {
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/app/OpenApiOrder.java
New file
@@ -0,0 +1,113 @@
package com.vincent.rsf.openApi.entity.app;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
@Data
@Accessors(chain = true)
@TableName("open_api_order")
public class OpenApiOrder implements Serializable {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    private String code;
    @TableField("po_code")
    private String poCode;
    @TableField("po_id")
    private Long poId;
    private String type;
    @TableField("wk_type")
    private String wkType;
    private BigDecimal anfme;
    private BigDecimal qty;
    @TableField("work_qty")
    private BigDecimal workQty;
    @TableField("exce_status")
    private Integer exceStatus;
    private Integer status;
    @TableField("business_time")
    private Date businessTime;
    @TableField("order_internal_code")
    private String orderInternalCode;
    @TableField("station_id")
    private String stationId;
    @TableField("stock_direct")
    private String stockDirect;
    @TableField("customer_id")
    private String customerId;
    @TableField("customer_name")
    private String customerName;
    @TableField("supplier_id")
    private String supplierId;
    @TableField("supplier_name")
    private String supplierName;
    @TableField("stock_org_id")
    private String stockOrgId;
    @TableField("stock_org_name")
    private String stockOrgName;
    @TableField("purchase_org_id")
    private String purchaseOrgId;
    @TableField("purchase_org_name")
    private String purchaseOrgName;
    @TableField("purchase_user_id")
    private String purchaseUserId;
    @TableField("purchase_user_name")
    private String purchaseUserName;
    @TableField("prd_org_id")
    private String prdOrgId;
    @TableField("prd_org_name")
    private String prdOrgName;
    @TableField("sale_org_id")
    private String saleOrgId;
    @TableField("sale_org_name")
    private String saleOrgName;
    @TableField("sale_user_id")
    private String saleUserId;
    @TableField("sale_user_name")
    private String saleUserName;
    @TableField("create_time")
    private Date createTime;
    @TableField("update_time")
    private Date updateTime;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/app/OpenApiOrderItem.java
New file
@@ -0,0 +1,111 @@
package com.vincent.rsf.openApi.entity.app;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
@Data
@Accessors(chain = true)
@TableName("open_api_order_item")
public class OpenApiOrderItem implements Serializable {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @TableField("order_id")
    private Long orderId;
    @TableField("order_code")
    private String orderCode;
    @TableField("plat_item_id")
    private String platItemId;
    @TableField("plat_order_code")
    private String platOrderCode;
    @TableField("plat_work_code")
    private String platWorkCode;
    @TableField("matnr_code")
    private String matnrCode;
    private String maktx;
    private String model;
    private String spec;
    private BigDecimal anfme;
    @TableField("stock_unit")
    private String stockUnit;
    @TableField("work_qty")
    private BigDecimal workQty;
    private BigDecimal qty;
    private String batch;
    private String memo;
    @TableField("target_warehouse_id")
    private String targetWarehouseId;
    @TableField("source_warehouse_id")
    private String sourceWarehouseId;
    @TableField("owner_id")
    private String ownerId;
    @TableField("base_unit")
    private String baseUnit;
    @TableField("use_org_id")
    private String useOrgId;
    @TableField("use_org_name")
    private String useOrgName;
    @TableField("erp_cls_id")
    private String erpClsId;
    @TableField("price_unit_id")
    private String priceUnitId;
    @TableField("in_stock_type")
    private String inStockType;
    @TableField("owner_type_id")
    private String ownerTypeId;
    @TableField("owner_name")
    private String ownerName;
    @TableField("keeper_type_id")
    private String keeperTypeId;
    @TableField("keeper_id")
    private String keeperId;
    @TableField("keeper_name")
    private String keeperName;
    private Integer status;
    private Integer deleted;
    @TableField("create_time")
    private Date createTime;
    @TableField("update_time")
    private Date updateTime;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/app/OpenApiOrderItemMap.java
New file
@@ -0,0 +1,61 @@
package com.vincent.rsf.openApi.entity.app;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
@Data
@Accessors(chain = true)
@TableName("open_api_order_item_map")
public class OpenApiOrderItemMap implements Serializable {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @TableField("order_id")
    private Long orderId;
    @TableField("order_code")
    private String orderCode;
    @TableField("source_item_id")
    private Long sourceItemId;
    @TableField("source_line_id")
    private String sourceLineId;
    @TableField("merge_matnr_code")
    private String mergeMatnrCode;
    @TableField("merge_batch")
    private String mergeBatch;
    @TableField("seq_no")
    private Integer seqNo;
    @TableField("source_qty")
    private BigDecimal sourceQty;
    @TableField("allocated_qty")
    private BigDecimal allocatedQty;
    @TableField("reported_qty")
    private BigDecimal reportedQty;
    private Integer status;
    private Integer deleted;
    @TableField("create_time")
    private Date createTime;
    @TableField("update_time")
    private Date updateTime;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/app/OpenApiOrderReportEvent.java
New file
@@ -0,0 +1,42 @@
package com.vincent.rsf.openApi.entity.app;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
@Data
@Accessors(chain = true)
@TableName("open_api_order_report_event")
public class OpenApiOrderReportEvent implements Serializable {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @TableField("event_id")
    private String eventId;
    @TableField("task_no")
    private String taskNo;
    @TableField("report_no")
    private String reportNo;
    @TableField("order_code")
    private String orderCode;
    private String payload;
    private Integer status;
    @TableField("create_time")
    private Date createTime;
    @TableField("update_time")
    private Date updateTime;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/mapper/OpenApiOrderItemMapMapper.java
New file
@@ -0,0 +1,9 @@
package com.vincent.rsf.openApi.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vincent.rsf.openApi.entity.app.OpenApiOrderItemMap;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OpenApiOrderItemMapMapper extends BaseMapper<OpenApiOrderItemMap> {
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/mapper/OpenApiOrderItemMapper.java
New file
@@ -0,0 +1,9 @@
package com.vincent.rsf.openApi.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vincent.rsf.openApi.entity.app.OpenApiOrderItem;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OpenApiOrderItemMapper extends BaseMapper<OpenApiOrderItem> {
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/mapper/OpenApiOrderMapper.java
New file
@@ -0,0 +1,9 @@
package com.vincent.rsf.openApi.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vincent.rsf.openApi.entity.app.OpenApiOrder;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OpenApiOrderMapper extends BaseMapper<OpenApiOrder> {
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/mapper/OpenApiOrderReportEventMapper.java
New file
@@ -0,0 +1,9 @@
package com.vincent.rsf.openApi.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vincent.rsf.openApi.entity.app.OpenApiOrderReportEvent;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OpenApiOrderReportEventMapper extends BaseMapper<OpenApiOrderReportEvent> {
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/phyz/ErpReportService.java
@@ -15,8 +15,11 @@
    String syncCustomer(List<Customer> customerList);
    String syncSupplier(List<Supplier> supplierList);
    String addOrderToServer(Order order);
    String addOrderToServerNew(Order order);
    JSONObject loginBySign() throws UnsupportedEncodingException, NoSuchAlgorithmException;
    CommonResponse reportInOrOutBound(Object params);
    CommonResponse reportOrderNew(Object params);
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/phyz/impl/ErpReportServiceImpl.java
@@ -3,15 +3,27 @@
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.app.OpenApiOrderReportEvent;
import com.vincent.rsf.openApi.entity.constant.WmsConstant;
import com.vincent.rsf.openApi.entity.dto.CommonResponse;
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.mapper.OpenApiOrderReportEventMapper;
import com.vincent.rsf.openApi.service.WmsErpService;
import com.vincent.rsf.openApi.service.phyz.ErpReportService;
import com.vincent.rsf.openApi.utils.ParamsMapUtils;
@@ -42,9 +54,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
@@ -62,6 +77,16 @@
    private WmsErpService wmsErpService;
    @Resource
    private RestTemplate restTemplate;
    @Resource
    private OpenApiOrderMapper openApiOrderMapper;
    @Resource
    private OpenApiOrderItemMapper openApiOrderItemMapper;
    @Resource
    private OpenApiOrderItemMapMapper openApiOrderItemMapMapper;
    @Resource
    private OpenApiOrderReportEventMapper openApiOrderReportEventMapper;
    private static final BigDecimal ZERO = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
    @PostConstruct
@@ -245,8 +270,9 @@
            }
            Map<String, Object> map = new HashMap<>();
            map.put("name", warehouse.getWareHouseName());
            map.put("code", warehouse.getUseOrgId());
            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);
@@ -388,6 +414,512 @@
//        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 eventId = pickString(root, "eventId", "EventId");
        String taskNo = pickString(root, "taskNo", "TaskNo");
        String reportNo = pickString(root, "reportNo", "ReportNo");
        if (StringUtils.isBlank(eventId) && StringUtils.isBlank(taskNo) && StringUtils.isBlank(reportNo)) {
            throw new CoolException("幂等键不能为空(eventId/taskNo/reportNo)");
        }
        if (existsReportEvent(eventId, taskNo, reportNo)) {
            return CommonResponse.ok("重复回传已忽略");
        }
        OpenApiOrderReportEvent reportEvent = saveReportEvent(eventId, taskNo, reportNo, pickString(root, "orderNo", "WMSNO"), root.toJSONString());
        JSONArray dataArray = pickArray(root, "Data", "data");
        if (Objects.isNull(dataArray) || dataArray.isEmpty()) {
            return CommonResponse.error("回传明细为空");
        }
        int allocateCount = 0;
        int erpReportCount = 0;
        List<String> errors = new ArrayList<>();
        for (int i = 0; i < dataArray.size(); i++) {
            JSONObject row = dataArray.getJSONObject(i);
            String orderNo = pickString(row, "WMSNO", "wmsNo", "orderNo", "OrderNo");
            String matnrCode = pickString(row, "ItemCode", "itemCode", "matNr", "MatNr");
            String batch = pickString(row, "Batch", "batch", "GoodsNO", "goodsNo");
            BigDecimal doneQty = pickDecimal(row, "qty", "doneQty", "InQty", "OutQty");
            if (StringUtils.isBlank(orderNo) || StringUtils.isBlank(matnrCode) || doneQty.compareTo(ZERO) <= 0) {
                errors.add("第" + (i + 1) + "行缺少必要字段(WMSNO/ItemCode/数量)");
                continue;
            }
            doneQty = doneQty.setScale(2, RoundingMode.HALF_UP);
            try {
                int singleAllocated = allocateMergedDoneQty(orderNo, matnrCode, batch, doneQty);
                allocateCount += singleAllocated;
                erpReportCount += reportReadyLinesToErp(orderNo);
                refreshOrderFinishStatus(orderNo);
            } catch (Exception e) {
                log.error("处理回传失败,orderNo={}, matnr={}, batch={}", orderNo, matnrCode, batch, e);
                errors.add("第" + (i + 1) + "行处理失败:" + e.getMessage());
            }
        }
        if (!errors.isEmpty()) {
            reportEvent.setStatus(2);
            openApiOrderReportEventMapper.updateById(reportEvent);
            return CommonResponse.error("处理完成,但存在异常:" + String.join(" | ", errors));
        }
        reportEvent.setStatus(1);
        openApiOrderReportEventMapper.updateById(reportEvent);
        Map<String, Object> result = new HashMap<>();
        result.put("allocatedCount", allocateCount);
        result.put("erpReportCount", erpReportCount);
        return CommonResponse.ok(result);
    }
    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)
                .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 boolean existsReportEvent(String eventId, String taskNo, String reportNo) {
        return openApiOrderReportEventMapper.selectCount(new LambdaQueryWrapper<OpenApiOrderReportEvent>()
                .eq(OpenApiOrderReportEvent::getEventId, defaultString(eventId))
                .eq(OpenApiOrderReportEvent::getTaskNo, defaultString(taskNo))
                .eq(OpenApiOrderReportEvent::getReportNo, defaultString(reportNo))
                .eq(OpenApiOrderReportEvent::getStatus, 1)) > 0;
    }
    private OpenApiOrderReportEvent saveReportEvent(String eventId, String taskNo, String reportNo, String orderCode, String payload) {
        OpenApiOrderReportEvent event = new OpenApiOrderReportEvent()
                .setEventId(defaultString(eventId))
                .setTaskNo(defaultString(taskNo))
                .setReportNo(defaultString(reportNo))
                .setOrderCode(orderCode)
                .setPayload(payload)
                .setStatus(0);
        openApiOrderReportEventMapper.insert(event);
        return event;
    }
    private int allocateMergedDoneQty(String orderNo, String matnrCode, String batch, BigDecimal mergedDoneQty) {
        OpenApiOrder order = openApiOrderMapper.selectOne(new LambdaQueryWrapper<OpenApiOrder>()
                .eq(OpenApiOrder::getCode, orderNo)
                .last("limit 1"));
        if (Objects.isNull(order)) {
            throw new CoolException("未找到订单:" + orderNo);
        }
        LambdaQueryWrapper<OpenApiOrderItemMap> 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);
        }
        List<OpenApiOrderItemMap> 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));
            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));
        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 = openApiOrderItemMapper.selectById(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();
rsf-open-api/src/main/resources/sql/open_api_order_new_tables.sql
New file
@@ -0,0 +1,38 @@
-- /erp/order/add/new 与 /erp/report/order/new 最小表结构
-- 1) 合并映射表:记录“合并维度 -> 原始行”分配关系
CREATE TABLE IF NOT EXISTS `open_api_order_item_map` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `order_id` bigint NOT NULL COMMENT 'open_api_order.id',
  `order_code` varchar(45) NOT NULL COMMENT '单号',
  `source_item_id` bigint NOT NULL COMMENT 'open_api_order_item.id',
  `source_line_id` varchar(45) DEFAULT NULL COMMENT '原始行号',
  `merge_matnr_code` varchar(45) NOT NULL COMMENT '合并维度-物料编码',
  `merge_batch` varchar(110) DEFAULT '' COMMENT '合并维度-批次',
  `seq_no` int NOT NULL DEFAULT 1 COMMENT '反向分配顺序',
  `source_qty` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '原始行应完成数量',
  `allocated_qty` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '已分配完成量',
  `reported_qty` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '已回传ERP数量',
  `status` int NOT NULL DEFAULT 1 COMMENT '状态{1:正常,0:冻结}',
  `deleted` int NOT NULL DEFAULT 0 COMMENT '是否删除{1:是,0:否}',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_order_merge` (`order_id`, `merge_matnr_code`, `merge_batch`),
  KEY `idx_source_item` (`source_item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT='open-api 订单行映射表';
-- 2) 回传事件幂等表:eventId/taskNo/reportNo
CREATE TABLE IF NOT EXISTS `open_api_order_report_event` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `event_id` varchar(64) NOT NULL DEFAULT '' COMMENT '事件ID',
  `task_no` varchar(64) NOT NULL DEFAULT '' COMMENT '任务号',
  `report_no` varchar(64) NOT NULL DEFAULT '' COMMENT '回传号',
  `order_code` varchar(45) DEFAULT NULL COMMENT '单号',
  `payload` text COMMENT '原始报文',
  `status` int NOT NULL DEFAULT 0 COMMENT '处理状态{0:处理中,1:成功,2:失败}',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_event_task_report` (`event_id`, `task_no`, `report_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT='open-api 回传事件幂等表';
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/WarehouseParams.java
@@ -18,6 +18,9 @@
    @ApiModelProperty("工厂")
    private String factory;
    @ApiModelProperty("工厂ID")
    private String factoryId;
    @ApiModelProperty("编码")
    private String code;