Browse Source

[华信科] 龙旗对接 库存、订单

zxl 2 months ago
parent
commit
6b1c02f7ab

+ 1 - 1
pom.xml

@@ -224,7 +224,7 @@
                                                 <arg>-Xlint:unchecked</arg>
                                                 <arg>-Xlint:deprecation</arg>
                                                 <arg>-bootclasspath</arg>
-                                                <arg>${env.JAVA_HOME}/jre/lib/rt.jar</arg>
+                                                <arg>${env.JAVA_HOME}/jre/lib/rt.jar;${env.JAVA_HOME}/jre/lib/jce.jar</arg>
                                                 <arg>-extdirs</arg>
                                                 <!-- windows下用分号隔开 -->
                                                 <arg>${env.JAVA_HOME}/jre/lib:${project.basedir}/lib</arg>

+ 1 - 1
src/main/java/com/uas/eis/core/WebAppConfig.java

@@ -27,7 +27,7 @@ public class WebAppConfig extends WebMvcConfigurationSupport{
 	@Override
 	public void addInterceptors(InterceptorRegistry registry){
 		registry.addInterceptor(apiSignLoginInterceptor()).addPathPatterns("/api/**","/mes/**")
-				.excludePathPatterns("/login", "/erp/**");
+				.excludePathPatterns("/login", "/erp/**", "/api/longcheer/**");
 		registry.addInterceptor(new DataSourceInterceptor()).addPathPatterns("/*/**");
 	}
 	

+ 15 - 0
src/main/java/com/uas/eis/longcheer/LongcheerAutoConfiguration.java

@@ -0,0 +1,15 @@
+package com.uas.eis.longcheer;
+
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 龙旗接口对接模块自动装配
+ *
+ * @author ZXL
+ * @date 2026-06-26
+ */
+@Configuration
+@ComponentScan(basePackages = "com.uas.eis.longcheer")
+public class LongcheerAutoConfiguration {
+}

+ 158 - 0
src/main/java/com/uas/eis/longcheer/config/LongcheerConfig.java

@@ -0,0 +1,158 @@
+package com.uas.eis.longcheer.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 龙旗接口对接配置属性
+ *
+ * @author ZXL
+ * @date 2026-06-26
+ */
+@Component
+@ConfigurationProperties(prefix = "longcheer")
+public class LongcheerConfig {
+
+    /** 库存同步接口配置 */
+    private Inventory inventory = new Inventory();
+
+    /** 采购订单推送接口配置 */
+    private Po po = new Po();
+
+    /** 请求超时时间(毫秒),默认 30000 */
+    private int timeout = 30000;
+
+    /** 失败重试次数,默认 3 */
+    private int retryTimes = 3;
+
+    /** 重试间隔(毫秒),默认 30000 */
+    private int retryInterval = 30000;
+
+    /** ELS 账号 -> 租户标识 的映射关系,用于龙旗不传 master 参数时自动定位租户 */
+    private java.util.Map<String, String> accountMapping = new java.util.HashMap<String, String>();
+
+    /** 租户标识 -> 供应商 ERP 编码 的映射关系,用于库存推送接口动态填 supplierErpCode */
+    private java.util.Map<String, String> supplierMapping = new java.util.HashMap<String, String>();
+
+    public static class Inventory {
+        /** 应用密钥 Key */
+        private String appKey;
+        /** 应用密钥 Secret */
+        private String appSecret;
+        /** 接口地址 */
+        private String url;
+
+        public String getAppKey() {
+            return appKey;
+        }
+
+        public void setAppKey(String appKey) {
+            this.appKey = appKey;
+        }
+
+        public String getAppSecret() {
+            return appSecret;
+        }
+
+        public void setAppSecret(String appSecret) {
+            this.appSecret = appSecret;
+        }
+
+        public String getUrl() {
+            return url;
+        }
+
+        public void setUrl(String url) {
+            this.url = url;
+        }
+    }
+
+    public static class Po {
+        /** 应用密钥 Key */
+        private String appKey;
+        /** 应用密钥 Secret */
+        private String appSecret;
+        /** 接口地址 */
+        private String url;
+
+        public String getAppKey() {
+            return appKey;
+        }
+
+        public void setAppKey(String appKey) {
+            this.appKey = appKey;
+        }
+
+        public String getAppSecret() {
+            return appSecret;
+        }
+
+        public void setAppSecret(String appSecret) {
+            this.appSecret = appSecret;
+        }
+
+        public String getUrl() {
+            return url;
+        }
+
+        public void setUrl(String url) {
+            this.url = url;
+        }
+    }
+
+    public Inventory getInventory() {
+        return inventory;
+    }
+
+    public void setInventory(Inventory inventory) {
+        this.inventory = inventory;
+    }
+
+    public Po getPo() {
+        return po;
+    }
+
+    public void setPo(Po po) {
+        this.po = po;
+    }
+
+    public int getTimeout() {
+        return timeout;
+    }
+
+    public void setTimeout(int timeout) {
+        this.timeout = timeout;
+    }
+
+    public int getRetryTimes() {
+        return retryTimes;
+    }
+
+    public void setRetryTimes(int retryTimes) {
+        this.retryTimes = retryTimes;
+    }
+
+    public int getRetryInterval() {
+        return retryInterval;
+    }
+
+    public void setRetryInterval(int retryInterval) {
+        this.retryInterval = retryInterval;
+    }
+
+    public java.util.Map<String, String> getAccountMapping() {
+        return accountMapping;
+    }
+
+    public void setAccountMapping(java.util.Map<String, String> accountMapping) {
+        this.accountMapping = accountMapping;
+    }
+
+    public java.util.Map<String, String> getSupplierMapping() {
+        return supplierMapping;
+    }
+
+    public void setSupplierMapping(java.util.Map<String, String> supplierMapping) {
+        this.supplierMapping = supplierMapping;
+    }
+}

+ 98 - 0
src/main/java/com/uas/eis/longcheer/controller/LongcheerController.java

@@ -0,0 +1,98 @@
+package com.uas.eis.longcheer.controller;
+
+import com.uas.eis.longcheer.config.LongcheerConfig;
+import com.uas.eis.longcheer.dto.LongcheerResponse;
+import com.uas.eis.longcheer.entity.PoPushRequest;
+import com.uas.eis.longcheer.service.LongcheerService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+
+/**
+ * 龙旗接口对接控制器
+ *
+ * @author ZXL
+ * @date 2026-06-26
+ */
+@RestController
+@RequestMapping("/api/longcheer")
+public class LongcheerController {
+
+    private static final Logger logger = LoggerFactory.getLogger(LongcheerController.class);
+
+    @Resource
+    private LongcheerService longcheerService;
+
+    @Resource
+    private LongcheerConfig longcheerConfig;
+
+    /**
+     * 接口01:供应商库存同步接口
+     * 供应商 → 龙旗(本系统为推送方)
+     *
+     * @param master 账套标识
+     * @param id     库存记录ID
+     * @return 统一响应
+     */
+    @PostMapping("/inventory/push")
+    public LongcheerResponse pushInventory(@RequestBody String master, Integer id) {
+
+        try {
+            LongcheerResponse response = longcheerService.pushInventory(master, id);
+            logger.info("[龙旗接口]库存同步响应,code={},message={}", response.getCode(), response.getMessage());
+            return response;
+        } catch (Exception e) {
+            logger.error("[龙旗接口]库存同步异常", e);
+            return new LongcheerResponse(500, "库存同步失败:" + e.getMessage(), false);
+        }
+    }
+
+    /**
+     * 接口02:供应商采购订单同步接口
+     * 龙旗 → 供应商(本系统为接收方)
+     * 龙旗通过 x-app-key / x-app-secret 鉴权后推送采购订单数据到本接口
+     *
+     * @param appKey  请求头 x-app-key
+     * @param appSecret 请求头 x-app-secret
+     * @param request 龙旗推送的采购订单数据
+     * @return 统一响应
+     */
+    @PostMapping("/po/push")
+    public LongcheerResponse receivePo(
+            @RequestHeader(value = "x-app-key", required = false) String appKey,
+            @RequestHeader(value = "x-app-secret", required = false) String appSecret,
+            @RequestBody PoPushRequest request) {
+
+        // 鉴权验证:校验 x-app-key 和 x-app-secret
+        String configAppKey = longcheerConfig.getPo().getAppKey();
+        String configAppSecret = longcheerConfig.getPo().getAppSecret();
+        if (isBlank(appKey) || isBlank(appSecret)
+                || !appKey.equals(configAppKey) || !appSecret.equals(configAppSecret)) {
+            logger.warn("[龙旗接口]采购订单接收鉴权失败,appKey={}", appKey);
+            return new LongcheerResponse(401, "鉴权失败:x-app-key或x-app-secret不正确", false);
+        }
+
+        try {
+            LongcheerResponse response = longcheerService.receivePo(request);
+            logger.info("[龙旗接口]采购订单接收响应,erpOrderNumber={},code={},message={}",
+                    request == null ? null : request.getErpOrderNumber(), response.getCode(), response.getMessage());
+            return response;
+        } catch (Exception e) {
+            logger.error("[龙旗接口]采购订单接收异常,erpOrderNumber={}",
+                    request == null ? null : request.getErpOrderNumber(), e);
+            return new LongcheerResponse(500, "采购订单接收失败:" + e.getMessage(), false);
+        }
+    }
+
+    /**
+     * 判断字符串是否为空
+     *
+     * @param str 字符串
+     * @return true 表示为空
+     */
+    private boolean isBlank(String str) {
+        return str == null || str.trim().isEmpty();
+    }
+}

+ 77 - 0
src/main/java/com/uas/eis/longcheer/dto/LongcheerResponse.java

@@ -0,0 +1,77 @@
+package com.uas.eis.longcheer.dto;
+
+import java.io.Serializable;
+
+/**
+ * 龙旗接口统一响应
+ *
+ * @author ZXL
+ * @date 2026-06-26
+ */
+public class LongcheerResponse implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 状态码,200 表示成功 */
+    private Integer code;
+
+    /** 响应消息 */
+    private String message;
+
+    /** 是否成功 */
+    private Boolean success;
+
+    /** 时间戳(毫秒) */
+    private Long timestamp;
+
+    public LongcheerResponse() {
+    }
+
+    public LongcheerResponse(Integer code, String message, Boolean success) {
+        this.code = code;
+        this.message = message;
+        this.success = success;
+        this.timestamp = System.currentTimeMillis();
+    }
+
+    /**
+     * 判断是否成功
+     *
+     * @return true 表示成功
+     */
+    public boolean isOk() {
+        return Boolean.TRUE.equals(success) && Integer.valueOf(200).equals(code);
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public void setCode(Integer code) {
+        this.code = code;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+
+    public Boolean getSuccess() {
+        return success;
+    }
+
+    public void setSuccess(Boolean success) {
+        this.success = success;
+    }
+
+    public Long getTimestamp() {
+        return timestamp;
+    }
+
+    public void setTimestamp(Long timestamp) {
+        this.timestamp = timestamp;
+    }
+}

+ 56 - 0
src/main/java/com/uas/eis/longcheer/entity/InventoryDetail.java

@@ -0,0 +1,56 @@
+package com.uas.eis.longcheer.entity;
+
+import java.io.Serializable;
+
+/**
+ * 库存明细
+ *
+ * @author ZXL
+ * @date 2026-06-26
+ */
+public class InventoryDetail implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 龙旗物料编码 */
+    private String longCheerMaterialCode;
+
+    /** 原厂未结 PO 数 */
+    private String unClosedPo;
+
+    /** 供应商库存数量 */
+    private String supplierInventory;
+
+    public InventoryDetail() {
+    }
+
+    public InventoryDetail(String longCheerMaterialCode, String unClosedPo, String supplierInventory) {
+        this.longCheerMaterialCode = longCheerMaterialCode;
+        this.unClosedPo = unClosedPo;
+        this.supplierInventory = supplierInventory;
+    }
+
+    public String getLongCheerMaterialCode() {
+        return longCheerMaterialCode;
+    }
+
+    public void setLongCheerMaterialCode(String longCheerMaterialCode) {
+        this.longCheerMaterialCode = longCheerMaterialCode;
+    }
+
+    public String getUnClosedPo() {
+        return unClosedPo;
+    }
+
+    public void setUnClosedPo(String unClosedPo) {
+        this.unClosedPo = unClosedPo;
+    }
+
+    public String getSupplierInventory() {
+        return supplierInventory;
+    }
+
+    public void setSupplierInventory(String supplierInventory) {
+        this.supplierInventory = supplierInventory;
+    }
+}

+ 52 - 0
src/main/java/com/uas/eis/longcheer/entity/InventoryPushRequest.java

@@ -0,0 +1,52 @@
+package com.uas.eis.longcheer.entity;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 供应商库存同步接口入参
+ * 接口地址:/els/supplierApi/invokeSrm/inventoryReceive
+ *
+ * @author ZXL
+ * @date 2026-06-26
+ */
+public class InventoryPushRequest implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** ELS 账号(龙旗分配) */
+    private String elsCode;
+
+    /** 供应商 ERP 编码 */
+    private String supplierErpCode;
+
+    /** 库存明细列表 */
+    private List<InventoryDetail> inventoryDetails;
+
+    public InventoryPushRequest() {
+    }
+
+    public String getElsCode() {
+        return elsCode;
+    }
+
+    public void setElsCode(String elsCode) {
+        this.elsCode = elsCode;
+    }
+
+    public String getSupplierErpCode() {
+        return supplierErpCode;
+    }
+
+    public void setSupplierErpCode(String supplierErpCode) {
+        this.supplierErpCode = supplierErpCode;
+    }
+
+    public List<InventoryDetail> getInventoryDetails() {
+        return inventoryDetails;
+    }
+
+    public void setInventoryDetails(List<InventoryDetail> inventoryDetails) {
+        this.inventoryDetails = inventoryDetails;
+    }
+}

+ 67 - 0
src/main/java/com/uas/eis/longcheer/entity/OrderStatusEnum.java

@@ -0,0 +1,67 @@
+package com.uas.eis.longcheer.entity;
+
+import java.io.Serializable;
+
+/**
+ * 订单状态枚举
+ *
+ * @author ZXL
+ * @date 2026-06-26
+ */
+public enum OrderStatusEnum implements Serializable {
+
+    /** 新建 */
+    NEW("0", "新建"),
+
+    /** 待供应商确认 */
+    WAIT_SUPPLIER_CONFIRM("1", "待供应商确认"),
+
+    /** 待采购确认 */
+    WAIT_PURCHASE_CONFIRM("2", "待采购确认"),
+
+    /** 供应商已拒绝 */
+    SUPPLIER_REJECTED("3", "供应商已拒绝"),
+
+    /** 已确认 */
+    CONFIRMED("5", "已确认"),
+
+    /** 部分确认 */
+    PARTIAL_CONFIRMED("6", "部分确认"),
+
+    /** 变更中 */
+    CHANGING("7", "变更中"),
+
+    /** 已作废 */
+    CANCELLED("10", "已作废");
+
+    private final String code;
+    private final String desc;
+
+    OrderStatusEnum(String code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getDesc() {
+        return desc;
+    }
+
+    /**
+     * 根据 code 获取枚举
+     *
+     * @param code 枚举 code
+     * @return 枚举值,未找到返回 null
+     */
+    public static OrderStatusEnum getByCode(String code) {
+        for (OrderStatusEnum item : values()) {
+            if (item.code.equals(code)) {
+                return item;
+            }
+        }
+        return null;
+    }
+}

+ 70 - 0
src/main/java/com/uas/eis/longcheer/entity/OrderTypeEnum.java

@@ -0,0 +1,70 @@
+package com.uas.eis.longcheer.entity;
+
+import java.io.Serializable;
+
+/**
+ * 订单类型枚举
+ *
+ * @author ZXL
+ * @date 2026-06-26
+ */
+public enum OrderTypeEnum implements Serializable {
+
+    /** 震坤行 */
+    ZKDD("zkdd", "震坤行"),
+
+    /** 模具 */
+    MJDD("mjdd", "模具"),
+
+    /** HMI-AVAP */
+    HMIDD("hmidd", "HMI-AVAP"),
+
+    /** 智能柜 */
+    ZNGDD("zngdd", "智能柜"),
+
+    /** 标准 */
+    BZDD("bzdd", "标准"),
+
+    /** 寄售A */
+    JSDDA("jsdda", "寄售"),
+
+    /** 寄售B */
+    JSDDB("jsddb", "结算"),
+
+    /** 赠品 */
+    ZPDDD("zpdd", "赠品"),
+
+    /** 费用 */
+    FYDD("fydd", "费用");
+
+    private final String code;
+    private final String desc;
+
+    OrderTypeEnum(String code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getDesc() {
+        return desc;
+    }
+
+    /**
+     * 根据 code 获取枚举
+     *
+     * @param code 枚举 code
+     * @return 枚举值,未找到返回 null
+     */
+    public static OrderTypeEnum getByCode(String code) {
+        for (OrderTypeEnum item : values()) {
+            if (item.code.equals(code)) {
+                return item;
+            }
+        }
+        return null;
+    }
+}

+ 238 - 0
src/main/java/com/uas/eis/longcheer/entity/PoLine.java

@@ -0,0 +1,238 @@
+package com.uas.eis.longcheer.entity;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 采购订单行明细
+ *
+ * @author ZXL
+ * @date 2026-06-26
+ */
+public class PoLine implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** ERP 行号 */
+    private String erpOrderLineNo;
+
+    /** 库存组织代码 */
+    private String factory;
+
+    /** 库存组织名称 */
+    private String factoryName;
+
+    /** 物料编码 */
+    private String materialNumber;
+
+    /** 物料规格 */
+    private String materialSpec;
+
+    /** 物料描述 */
+    private String materialDesc;
+
+    /** 生产厂家 */
+    private String manufacturer;
+
+    /** 要求交期 YYYY-MM-DD */
+    private String requireDate;
+
+    /** 数量(6位小数) */
+    private BigDecimal quantity;
+
+    /** 采购单位 */
+    private String purchaseUnit;
+
+    /** 未税单价(8位小数) */
+    private BigDecimal netPrice;
+
+    /** 未税金额(2位小数) */
+    private BigDecimal netAmount;
+
+    /** 采购类型(枚举 code) */
+    private String purchaseType;
+
+    /** 回复交期 YYYY-MM-DD */
+    private String replyDate;
+
+    /** 收货地点 */
+    private String placeReceipt;
+
+    /** 交货地址 */
+    private String deliveryAddress;
+
+    /** 关闭标识(0/1) */
+    private Integer isClose;
+
+    /** 项目号 */
+    private String lineProjectNumber;
+
+    /** 收货联系人 */
+    private String receiveContact;
+
+    /** 收货联系电话 */
+    private String receivePhone;
+
+    public PoLine() {
+    }
+
+    public String getErpOrderLineNo() {
+        return erpOrderLineNo;
+    }
+
+    public void setErpOrderLineNo(String erpOrderLineNo) {
+        this.erpOrderLineNo = erpOrderLineNo;
+    }
+
+    public String getFactory() {
+        return factory;
+    }
+
+    public void setFactory(String factory) {
+        this.factory = factory;
+    }
+
+    public String getFactoryName() {
+        return factoryName;
+    }
+
+    public void setFactoryName(String factoryName) {
+        this.factoryName = factoryName;
+    }
+
+    public String getMaterialNumber() {
+        return materialNumber;
+    }
+
+    public void setMaterialNumber(String materialNumber) {
+        this.materialNumber = materialNumber;
+    }
+
+    public String getMaterialSpec() {
+        return materialSpec;
+    }
+
+    public void setMaterialSpec(String materialSpec) {
+        this.materialSpec = materialSpec;
+    }
+
+    public String getMaterialDesc() {
+        return materialDesc;
+    }
+
+    public void setMaterialDesc(String materialDesc) {
+        this.materialDesc = materialDesc;
+    }
+
+    public String getManufacturer() {
+        return manufacturer;
+    }
+
+    public void setManufacturer(String manufacturer) {
+        this.manufacturer = manufacturer;
+    }
+
+    public String getRequireDate() {
+        return requireDate;
+    }
+
+    public void setRequireDate(String requireDate) {
+        this.requireDate = requireDate;
+    }
+
+    public BigDecimal getQuantity() {
+        return quantity;
+    }
+
+    public void setQuantity(BigDecimal quantity) {
+        this.quantity = quantity;
+    }
+
+    public String getPurchaseUnit() {
+        return purchaseUnit;
+    }
+
+    public void setPurchaseUnit(String purchaseUnit) {
+        this.purchaseUnit = purchaseUnit;
+    }
+
+    public BigDecimal getNetPrice() {
+        return netPrice;
+    }
+
+    public void setNetPrice(BigDecimal netPrice) {
+        this.netPrice = netPrice;
+    }
+
+    public BigDecimal getNetAmount() {
+        return netAmount;
+    }
+
+    public void setNetAmount(BigDecimal netAmount) {
+        this.netAmount = netAmount;
+    }
+
+    public String getPurchaseType() {
+        return purchaseType;
+    }
+
+    public void setPurchaseType(String purchaseType) {
+        this.purchaseType = purchaseType;
+    }
+
+    public String getReplyDate() {
+        return replyDate;
+    }
+
+    public void setReplyDate(String replyDate) {
+        this.replyDate = replyDate;
+    }
+
+    public String getPlaceReceipt() {
+        return placeReceipt;
+    }
+
+    public void setPlaceReceipt(String placeReceipt) {
+        this.placeReceipt = placeReceipt;
+    }
+
+    public String getDeliveryAddress() {
+        return deliveryAddress;
+    }
+
+    public void setDeliveryAddress(String deliveryAddress) {
+        this.deliveryAddress = deliveryAddress;
+    }
+
+    public Integer getIsClose() {
+        return isClose;
+    }
+
+    public void setIsClose(Integer isClose) {
+        this.isClose = isClose;
+    }
+
+    public String getLineProjectNumber() {
+        return lineProjectNumber;
+    }
+
+    public void setLineProjectNumber(String lineProjectNumber) {
+        this.lineProjectNumber = lineProjectNumber;
+    }
+
+    public String getReceiveContact() {
+        return receiveContact;
+    }
+
+    public void setReceiveContact(String receiveContact) {
+        this.receiveContact = receiveContact;
+    }
+
+    public String getReceivePhone() {
+        return receivePhone;
+    }
+
+    public void setReceivePhone(String receivePhone) {
+        this.receivePhone = receivePhone;
+    }
+}

+ 240 - 0
src/main/java/com/uas/eis/longcheer/entity/PoPushRequest.java

@@ -0,0 +1,240 @@
+package com.uas.eis.longcheer.entity;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 供应商采购订单同步接口入参
+ * 接口地址:/api/v1/po/push
+ *
+ * @author ZXL
+ * @date 2026-06-26
+ */
+public class PoPushRequest implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** ERP 订单号(唯一) */
+    private String erpOrderNumber;
+
+    /** ELS 账号 */
+    private String elsAccount;
+
+    /** 供应商编号 */
+    private String supplierCode;
+
+    /** 供应商名称 */
+    private String supplierName;
+
+    /** 业务实体 */
+    private String companyCode;
+
+    /** 订单类型(枚举 code) */
+    private String orderType;
+
+    /** 订单状态(枚举 code) */
+    private String orderStatus;
+
+    /** 是否赠品(0/1) */
+    private Integer ifGiveaway;
+
+    /** 供应商地点 */
+    private String supplierLocation;
+
+    /** 币别(如 CNY) */
+    private String currency;
+
+    /** 客户 */
+    private String customer;
+
+    /** 项目 */
+    private String headerProjectName;
+
+    /** 背靠背 SO */
+    private String backSo;
+
+    /** GST NO */
+    private String gtsNo;
+
+    /** 印度交货地址 */
+    private String gtsDeliveryAddress;
+
+    /** 需方备注 */
+    private String purchaseRemark;
+
+    /** 采购员(格式:000101_张三) */
+    private String purchasePrincipal;
+
+    /** 订单日期 YYYY-MM-DD */
+    private String orderDate;
+
+    /** 订单明细列表 */
+    private List<PoLine> poLines;
+
+    /** 内部使用:URL 上的 master 参数(Controller 注入,Service 用它来做租户定位) */
+    private transient String master;
+
+    public PoPushRequest() {
+    }
+
+    public String getMaster() {
+        return master;
+    }
+
+    public void setMaster(String master) {
+        this.master = master;
+    }
+
+    public String getErpOrderNumber() {
+        return erpOrderNumber;
+    }
+
+    public void setErpOrderNumber(String erpOrderNumber) {
+        this.erpOrderNumber = erpOrderNumber;
+    }
+
+    public String getElsAccount() {
+        return elsAccount;
+    }
+
+    public void setElsAccount(String elsAccount) {
+        this.elsAccount = elsAccount;
+    }
+
+    public String getSupplierCode() {
+        return supplierCode;
+    }
+
+    public void setSupplierCode(String supplierCode) {
+        this.supplierCode = supplierCode;
+    }
+
+    public String getSupplierName() {
+        return supplierName;
+    }
+
+    public void setSupplierName(String supplierName) {
+        this.supplierName = supplierName;
+    }
+
+    public String getCompanyCode() {
+        return companyCode;
+    }
+
+    public void setCompanyCode(String companyCode) {
+        this.companyCode = companyCode;
+    }
+
+    public String getOrderType() {
+        return orderType;
+    }
+
+    public void setOrderType(String orderType) {
+        this.orderType = orderType;
+    }
+
+    public String getOrderStatus() {
+        return orderStatus;
+    }
+
+    public void setOrderStatus(String orderStatus) {
+        this.orderStatus = orderStatus;
+    }
+
+    public Integer getIfGiveaway() {
+        return ifGiveaway;
+    }
+
+    public void setIfGiveaway(Integer ifGiveaway) {
+        this.ifGiveaway = ifGiveaway;
+    }
+
+    public String getSupplierLocation() {
+        return supplierLocation;
+    }
+
+    public void setSupplierLocation(String supplierLocation) {
+        this.supplierLocation = supplierLocation;
+    }
+
+    public String getCurrency() {
+        return currency;
+    }
+
+    public void setCurrency(String currency) {
+        this.currency = currency;
+    }
+
+    public String getCustomer() {
+        return customer;
+    }
+
+    public void setCustomer(String customer) {
+        this.customer = customer;
+    }
+
+    public String getHeaderProjectName() {
+        return headerProjectName;
+    }
+
+    public void setHeaderProjectName(String headerProjectName) {
+        this.headerProjectName = headerProjectName;
+    }
+
+    public String getBackSo() {
+        return backSo;
+    }
+
+    public void setBackSo(String backSo) {
+        this.backSo = backSo;
+    }
+
+    public String getGtsNo() {
+        return gtsNo;
+    }
+
+    public void setGtsNo(String gtsNo) {
+        this.gtsNo = gtsNo;
+    }
+
+    public String getGtsDeliveryAddress() {
+        return gtsDeliveryAddress;
+    }
+
+    public void setGtsDeliveryAddress(String gtsDeliveryAddress) {
+        this.gtsDeliveryAddress = gtsDeliveryAddress;
+    }
+
+    public String getPurchaseRemark() {
+        return purchaseRemark;
+    }
+
+    public void setPurchaseRemark(String purchaseRemark) {
+        this.purchaseRemark = purchaseRemark;
+    }
+
+    public String getPurchasePrincipal() {
+        return purchasePrincipal;
+    }
+
+    public void setPurchasePrincipal(String purchasePrincipal) {
+        this.purchasePrincipal = purchasePrincipal;
+    }
+
+    public String getOrderDate() {
+        return orderDate;
+    }
+
+    public void setOrderDate(String orderDate) {
+        this.orderDate = orderDate;
+    }
+
+    public List<PoLine> getPoLines() {
+        return poLines;
+    }
+
+    public void setPoLines(List<PoLine> poLines) {
+        this.poLines = poLines;
+    }
+}

+ 70 - 0
src/main/java/com/uas/eis/longcheer/entity/PurchaseTypeEnum.java

@@ -0,0 +1,70 @@
+package com.uas.eis.longcheer.entity;
+
+import java.io.Serializable;
+
+/**
+ * 采购类型枚举
+ *
+ * @author ZXL
+ * @date 2026-06-26
+ */
+public enum PurchaseTypeEnum implements Serializable {
+
+    /** 生产料量产 */
+    PRODUCTION_MASS("1", "生产料量产"),
+
+    /** 生产料售后 */
+    PRODUCTION_AFTER_SALE("2", "生产料售后"),
+
+    /** 试产料 */
+    TRIAL("3", "试产料"),
+
+    /** 办公耗材 */
+    OFFICE_SUPPLIES("4", "办公耗材"),
+
+    /** 模具 */
+    MOULD("5", "模具"),
+
+    /** 资产 */
+    ASSET("6", "资产"),
+
+    /** 资产检修 */
+    ASSET_MAINTENANCE("7", "资产检修"),
+
+    /** 委外测试 */
+    OUTSOURCING_TEST("8", "委外测试"),
+
+    /** 服务类 */
+    SERVICE("9", "服务类");
+
+    private final String code;
+    private final String desc;
+
+    PurchaseTypeEnum(String code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getDesc() {
+        return desc;
+    }
+
+    /**
+     * 根据 code 获取枚举
+     *
+     * @param code 枚举 code
+     * @return 枚举值,未找到返回 null
+     */
+    public static PurchaseTypeEnum getByCode(String code) {
+        for (PurchaseTypeEnum item : values()) {
+            if (item.code.equals(code)) {
+                return item;
+            }
+        }
+        return null;
+    }
+}

+ 36 - 0
src/main/java/com/uas/eis/longcheer/service/LongcheerService.java

@@ -0,0 +1,36 @@
+package com.uas.eis.longcheer.service;
+
+import com.uas.eis.longcheer.dto.LongcheerResponse;
+import com.uas.eis.longcheer.entity.InventoryPushRequest;
+import com.uas.eis.longcheer.entity.PoPushRequest;
+
+/**
+ * 龙旗接口对接服务接口
+ *
+ * @author ZXL
+ * @date 2026-06-26
+ */
+public interface LongcheerService {
+
+    /**
+     * 接口01:推送供应商库存数据到龙旗
+     * 供应商 → 龙旗
+     *
+     * @param request 库存同步请求
+     * @return 统一响应
+     */
+    LongcheerResponse pushInventory(String master, Integer id);
+
+    /**
+     * 接口02:接收龙旗推送的采购订单
+     * 龙旗 → 供应商(本系统为接收方)
+     * <p>
+     * 账套定位优先级:
+     * 1) request.elsAccount 在配置 accountMapping 中的映射
+     * 2) 上述都没匹配上时返回 400 报错
+     *
+     * @param request 龙旗推送的采购订单数据
+     * @return 统一响应
+     */
+    LongcheerResponse receivePo(PoPushRequest request);
+}

+ 521 - 0
src/main/java/com/uas/eis/longcheer/serviceImpl/LongcheerServiceImpl.java

@@ -0,0 +1,521 @@
+package com.uas.eis.longcheer.serviceImpl;
+
+import com.alibaba.fastjson.JSON;
+import com.uas.eis.core.config.SpObserver;
+import com.uas.eis.dao.BaseDao;
+import com.uas.eis.dao.SqlRowList;
+import com.uas.eis.longcheer.config.LongcheerConfig;
+import com.uas.eis.longcheer.dto.LongcheerResponse;
+import com.uas.eis.longcheer.entity.InventoryDetail;
+import com.uas.eis.longcheer.entity.InventoryPushRequest;
+import com.uas.eis.longcheer.entity.PoLine;
+import com.uas.eis.longcheer.entity.PoPushRequest;
+import com.uas.eis.longcheer.service.LongcheerService;
+import com.uas.eis.longcheer.utils.LongcheerHttpClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 龙旗接口对接服务实现
+ *
+ * @author ZXL
+ * @date 2026-06-26
+ */
+@Service
+public class LongcheerServiceImpl implements LongcheerService {
+
+    private static final Logger logger = LoggerFactory.getLogger(LongcheerServiceImpl.class);
+
+    /** 采购订单头表 */
+    private static final String TABLE_PO_HEADER = "LQPURCHASEORDER";
+    /** 采购订单行表 */
+    private static final String TABLE_PO_LINE = "LQPURCHASEORDERLINE";
+    /** 头表序列名 */
+    private static final String SEQ_PO_HEADER = "LQPO_SEQ";
+    /** 行表序列名 */
+    private static final String SEQ_PO_LINE = "LQPL_SEQ";
+
+    @Resource
+    private LongcheerConfig longcheerConfig;
+    @Autowired
+    private BaseDao baseDao;
+
+    @Override
+    public LongcheerResponse pushInventory(String master, Integer id) {
+        SpObserver.putSp(master);
+        InventoryPushRequest request = new InventoryPushRequest();
+        SqlRowList rs = baseDao.queryForRowSet("select * from LQINVENTORY left join LQINVENTORYDETAIL on LQI_ID = LQID_LQIID " +
+                "where LQI_ID = ?", id);
+        List<InventoryDetail> requestdets = new ArrayList<InventoryDetail>();
+        // 根据 master 解析 elsCode 和 supplierErpCode
+        String elsCode = resolveElsCode(master);
+        String supplierErpCode = resolveSupplierErpCode(master);
+        logger.info("[龙旗接口]库存推送动态解析,master={},elsCode={},supplierErpCode={}",
+                master, elsCode, supplierErpCode);
+        request.setElsCode(elsCode);
+        request.setSupplierErpCode(supplierErpCode);
+        while (rs.next()) {
+            InventoryDetail requestdet = new InventoryDetail();
+            // 龙旗物料编码(必填)
+            requestdet.setLongCheerMaterialCode(rs.getGeneralString("lqid_lqprcode"));
+            // 原厂未结PO数
+            requestdet.setUnClosedPo(String.valueOf(rs.getInt("lqid_poqty")));
+            // 供应商库存数量
+            requestdet.setSupplierInventory(String.valueOf(rs.getInt("lqid_kcqty")));
+            requestdets.add(requestdet);
+        }
+        request.setInventoryDetails(requestdets);
+
+        // 调用龙旗接口
+        String url = longcheerConfig.getInventory().getUrl();
+        // 需要提供
+        String appKey = longcheerConfig.getInventory().getAppKey();
+        String appSecret = longcheerConfig.getInventory().getAppSecret();
+
+        String requestBody = JSON.toJSONString(request);
+        logger.info("[龙旗接口]调用库存同步接口,url={}", url);
+
+        String responseBody = LongcheerHttpClient.post(url, appKey, appSecret, requestBody,
+                longcheerConfig.getTimeout(), longcheerConfig.getRetryTimes(), longcheerConfig.getRetryInterval());
+
+        return JSON.parseObject(responseBody, LongcheerResponse.class);
+    }
+
+    /**
+     * 接口02:接收龙旗推送的采购订单数据并入库
+     * 龙旗 → 供应商(本系统为接收方)
+     * <p>
+     * 账套定位优先级:
+     * 1) request.master(URL 传入,兼容旧调用方)
+     * 2) longcheerConfig.accountMapping.get(request.elsAccount)
+     * 3) 上述都没匹配上时返回 400 报错
+     *
+     * @param request 龙旗推送的采购订单数据
+     * @return 统一响应
+     */
+    @Override
+    public LongcheerResponse receivePo(PoPushRequest request) {
+        logger.info("[龙旗接口]接收到采购订单推送,erpOrderNumber={},elsAccount={},supplierCode={}",
+                request.getErpOrderNumber(), request.getElsAccount(), request.getSupplierCode());
+
+        // 必填字段校验
+        String validateMsg = validatePoRequest(request);
+        if (validateMsg != null) {
+            return new LongcheerResponse(400, validateMsg, false);
+        }
+
+        // 账套定位:master 优先,其次按 elsAccount 在配置里查映射
+        String master = resolveMaster(request);
+        if (isBlank(master)) {
+            logger.warn("[龙旗接口]无法定位账套,erpOrderNumber={},elsAccount={}",
+                    request.getErpOrderNumber(), request.getElsAccount());
+            return new LongcheerResponse(400,
+                    "无法定位账套:URL未传master且elsAccount未配置映射关系,请联系管理员", false);
+        }
+
+        // 切换到对应账套数据源
+        SpObserver.putSp(master);
+
+        // 检查订单是否已存在(按 erpOrderNumber 去重)
+        boolean exists = baseDao.checkIf(TABLE_PO_HEADER,
+                "LQPO_ERPORDERNUMBER = '" + request.getErpOrderNumber().replace("'", "''") + "'");
+
+        if (exists) {
+            // 订单已存在,执行更新
+            int poId = updatePoHeader(request);
+            // 先删除旧明细,再重新插入
+            baseDao.deleteByCondition(TABLE_PO_LINE, "LQPL_POID = " + poId);
+            insertPoLines(poId, request.getPoLines());
+            //插入订单表
+            int said = baseDao.getSeqId("Sale_SEQ");
+            String sacode = baseDao.sGetMaxNumber("Sale",2);
+            baseDao.execute("insert into sale(sa_id,sa_code,sa_pocode,sa_date,sa_kind,sa_currency,sa_rate,sa_custcode,sa_custname,sa_apcustcode,sa_apcustname, " +
+                    "sa_paymentscode,sa_payments,sa_sellercode,sa_seller,sa_departmentcode,sa_departmentname,sa_status,sa_statuscode,sa_recorder,sa_recorddate,SA_POID_USER) " +
+                    "select "+said+",'"+sacode+"',LQPO_ERPORDERNUMBER,to_date(LQPO_ORDERDATE,'yyyy-mm-dd'),LQPO_ORDERTYPE,LQPO_CURRENCY,cm_crrate,cu_code,cu_name,cu_arcode,cu_arname, " +
+                    "cu_paymentscode,cu_payments,cu_sellercode,cu_sellername,em_departmentcode,em_depart,'在录入','ENTERING','管理员',sysdate,LQPO_ID " +
+                    "from LQPURCHASEORDER left join (select * from CUSTOMTABLE left join CUSTOMTABLEDETAIL on ct_id = cd_ctid where ct_caller='LQCustomerDZ' and ct_status = '已审核') on cd_varchar50_1 = LQPO_COMPANYCODE " +
+                    "left join Customer on cu_code = cd_varchar50_2 left join CurrencysMonth on cm_crname = LQPO_CURRENCY and cm_yearmonth = to_char(to_date(LQPO_ORDERDATE,'yyyy-mm-dd'),'yyyymm') " +
+                    "left join employee on em_code = cu_sellercode where LQPO_ID = " + poId);
+            baseDao.execute("insert into saledetail (sd_id,sd_said,sd_code,sd_detno,sd_prodcode,sd_custprodcode,sd_qty,sd_price,sd_total,sd_taxrate,sd_costprice,sd_taxtotal,sd_delivery)" +
+                    "select Saledetail_SEQ.nextval,"+said+",'"+sacode+"',LQPL_ERPORDERLINENO,cd_varchar50_1,LQPL_MATERIALNUMBER,LQPL_QUANTITY,0,0,0,LQPL_NETPRICE,LQPL_NETAMOUNT,to_date(LQPL_REPLYDATE,'yyyy-mm-dd') " +
+                    "from LQPURCHASEORDERLINE left join (select * from CUSTOMTABLE left join CUSTOMTABLEDETAIL on ct_id = cd_ctid where ct_caller='LQProductDZ' and ct_status = '已审核') on cd_varchar50_2 = LQPL_MATERIALNUMBER ");
+            baseDao.execute("update saledetail set sd_taxrate = (select cu_taxrate from sale left join Customer on sa_custcode = cu_code where sa_id = sd_said) where sd_said = " + said);
+            baseDao.execute("update saledetail set sd_price = round(sd_costprice * (1 + sd_taxrate / 100),8) where sd_said = " + said);
+            baseDao.execute("update saledetail set sd_total = round(sd_qty * sd_price,2) where sd_said = " + said);
+            logger.info("[龙旗接口]采购订单更新成功,erpOrderNumber={},poId={},明细数={}",
+                    request.getErpOrderNumber(), poId, request.getPoLines().size());
+        } else {
+            // 新订单,执行插入
+            int poId = baseDao.getSeqId(SEQ_PO_HEADER);
+            insertPoHeader(poId, request);
+            insertPoLines(poId, request.getPoLines());
+            //插入订单表
+            int said = baseDao.getSeqId("Sale_SEQ");
+            String sacode = baseDao.sGetMaxNumber("Sale",2);
+            baseDao.execute("insert into sale(sa_id,sa_code,sa_pocode,sa_date,sa_kind,sa_currency,sa_rate,sa_custcode,sa_custname,sa_apcustcode,sa_apcustname, " +
+                    "sa_paymentscode,sa_payments,sa_sellercode,sa_seller,sa_departmentcode,sa_departmentname,sa_status,sa_statuscode,sa_recorder,sa_recorddate,SA_POID_USER) " +
+                    "select "+said+",'"+sacode+"',LQPO_ERPORDERNUMBER,to_date(LQPO_ORDERDATE,'yyyy-mm-dd'),LQPO_ORDERTYPE,LQPO_CURRENCY,cm_crrate,cu_code,cu_name,cu_arcode,cu_arname, " +
+                    "cu_paymentscode,cu_payments,cu_sellercode,cu_sellername,em_departmentcode,em_depart,'在录入','ENTERING','管理员',sysdate,LQPO_ID " +
+                    "from LQPURCHASEORDER left join (select * from CUSTOMTABLE left join CUSTOMTABLEDETAIL on ct_id = cd_ctid where ct_caller='LQCustomerDZ' and ct_status = '已审核') on cd_varchar50_1 = LQPO_COMPANYCODE " +
+                    "left join Customer on cu_code = cd_varchar50_2 left join CurrencysMonth on cm_crname = LQPO_CURRENCY and cm_yearmonth = to_char(to_date(LQPO_ORDERDATE,'yyyy-mm-dd'),'yyyymm') " +
+                    "left join employee on em_code = cu_sellercode where LQPO_ID = " + poId);
+            baseDao.execute("insert into saledetail (sd_id,sd_said,sd_code,sd_detno,sd_prodcode,sd_custprodcode,sd_qty,sd_price,sd_total,sd_taxrate,sd_costprice,sd_taxtotal,sd_delivery)" +
+                    "select Saledetail_SEQ.nextval,"+said+",'"+sacode+"',LQPL_ERPORDERLINENO,cd_varchar50_1,LQPL_MATERIALNUMBER,LQPL_QUANTITY,0,0,0,LQPL_NETPRICE,LQPL_NETAMOUNT,to_date(LQPL_REPLYDATE,'yyyy-mm-dd') " +
+                    "from LQPURCHASEORDERLINE left join (select * from CUSTOMTABLE left join CUSTOMTABLEDETAIL on ct_id = cd_ctid where ct_caller='LQProductDZ' and ct_status = '已审核') on cd_varchar50_2 = LQPL_MATERIALNUMBER ");
+            baseDao.execute("update saledetail set sd_taxrate = (select cu_taxrate from sale left join Customer on sa_custcode = cu_code where sa_id = sd_said) where sd_said = " + said);
+            baseDao.execute("update saledetail set sd_price = round(sd_costprice * (1 + sd_taxrate / 100),8) where sd_said = " + said);
+            baseDao.execute("update saledetail set sd_total = round(sd_qty * sd_price,2) where sd_said = " + said);
+            logger.info("[龙旗接口]采购订单新增成功,erpOrderNumber={},poId={},明细数={}",
+                    request.getErpOrderNumber(), poId, request.getPoLines().size());
+        }
+
+        return new LongcheerResponse(200, "操作成功!", true);
+    }
+
+    /**
+     * 校验采购订单必填字段
+     *
+     * @param request 采购订单请求
+     * @return 校验失败返回错误信息,校验通过返回 null
+     */
+    private String validatePoRequest(PoPushRequest request) {
+        if (request == null) {
+            return "请求体不能为空";
+        }
+        if (isBlank(request.getErpOrderNumber())) {
+            return "ERP订单号(erpOrderNumber)不能为空";
+        }
+        if (isBlank(request.getElsAccount())) {
+            return "ELS账号(elsAccount)不能为空";
+        }
+        if (isBlank(request.getSupplierCode())) {
+            return "供应商编号(supplierCode)不能为空";
+        }
+        if (isBlank(request.getCompanyCode())) {
+            return "业务实体(companyCode)不能为空";
+        }
+        if (isBlank(request.getOrderType())) {
+            return "订单类型(orderType)不能为空";
+        }
+        if (isBlank(request.getOrderStatus())) {
+            return "订单状态(orderStatus)不能为空";
+        }
+        if (isBlank(request.getSupplierLocation())) {
+            return "供应商地点(supplierLocation)不能为空";
+        }
+        if (isBlank(request.getCurrency())) {
+            return "币别(currency)不能为空";
+        }
+        if (isBlank(request.getPurchasePrincipal())) {
+            return "采购员(purchasePrincipal)不能为空";
+        }
+        if (isBlank(request.getOrderDate())) {
+            return "订单日期(orderDate)不能为空";
+        }
+        if (request.getPoLines() == null || request.getPoLines().isEmpty()) {
+            return "订单明细(poLines)不能为空";
+        }
+        // 校验每行明细
+        for (int i = 0; i < request.getPoLines().size(); i++) {
+            PoLine line = request.getPoLines().get(i);
+            String prefix = "第" + (i + 1) + "行明细:";
+            if (isBlank(line.getErpOrderLineNo())) {
+                return prefix + "ERP行号(erpOrderLineNo)不能为空";
+            }
+            if (isBlank(line.getFactory())) {
+                return prefix + "库存组织代码(factory)不能为空";
+            }
+            if (isBlank(line.getMaterialNumber())) {
+                return prefix + "物料编码(materialNumber)不能为空";
+            }
+            if (isBlank(line.getManufacturer())) {
+                return prefix + "生产厂家(manufacturer)不能为空";
+            }
+            if (isBlank(line.getRequireDate())) {
+                return prefix + "要求交期(requireDate)不能为空";
+            }
+            if (line.getQuantity() == null) {
+                return prefix + "数量(quantity)不能为空";
+            }
+            if (isBlank(line.getPurchaseUnit())) {
+                return prefix + "采购单位(purchaseUnit)不能为空";
+            }
+            if (line.getNetPrice() == null) {
+                return prefix + "未税单价(netPrice)不能为空";
+            }
+            if (line.getNetAmount() == null) {
+                return prefix + "未税金额(netAmount)不能为空";
+            }
+            if (isBlank(line.getPurchaseType())) {
+                return prefix + "采购类型(purchaseType)不能为空";
+            }
+            if (isBlank(line.getPlaceReceipt())) {
+                return prefix + "收货地点(placeReceipt)不能为空";
+            }
+            if (isBlank(line.getDeliveryAddress())) {
+                return prefix + "交货地址(deliveryAddress)不能为空";
+            }
+            if (isBlank(line.getReceiveContact())) {
+                return prefix + "收货联系人(receiveContact)不能为空";
+            }
+            if (isBlank(line.getReceivePhone())) {
+                return prefix + "收货联系电话(receivePhone)不能为空";
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 新增采购订单头
+     *
+     * @param poId    主键ID
+     * @param request 采购订单请求
+     */
+    private void insertPoHeader(int poId, PoPushRequest request) {
+        String sql = "insert into " + TABLE_PO_HEADER + " ("
+                + "LQPO_ID, LQPO_ERPORDERNUMBER, LQPO_ELSACCOUNT, LQPO_SUPPLIERCODE, LQPO_SUPPLIERNAME, "
+                + "LQPO_COMPANYCODE, LQPO_ORDERTYPE, LQPO_ORDERSTATUS, LQPO_IFGIVEAWAY, LQPO_SUPPLIERLOCATION, "
+                + "LQPO_CURRENCY, LQPO_CUSTOMER, LQPO_HEADERPROJECTNAME, LQPO_BACKSO, LQPO_GTSNO, "
+                + "LQPO_GTSDELIVERYADDRESS, LQPO_PURCHASEREMARK, LQPO_PURCHASEPRINCIPAL, LQPO_ORDERDATE, "
+                + "LQPO_STATUS, LQPO_RECEIVETIME"
+                + ") values (?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?, sysdate)";
+
+        baseDao.execute(sql,
+                poId,
+                request.getErpOrderNumber(),
+                request.getElsAccount(),
+                request.getSupplierCode(),
+                request.getSupplierName(),
+                request.getCompanyCode(),
+                request.getOrderType(),
+                request.getOrderStatus(),
+                request.getIfGiveaway(),
+                request.getSupplierLocation(),
+                request.getCurrency(),
+                request.getCustomer(),
+                request.getHeaderProjectName(),
+                request.getBackSo(),
+                request.getGtsNo(),
+                request.getGtsDeliveryAddress(),
+                request.getPurchaseRemark(),
+                request.getPurchasePrincipal(),
+                request.getOrderDate(),
+                "待处理"
+        );
+    }
+
+    /**
+     * 更新采购订单头(订单已存在时)
+     *
+     * @param request 采购订单请求
+     * @return 订单主键ID
+     */
+    private int updatePoHeader(PoPushRequest request) {
+        // 查询已存在订单的ID
+        SqlRowList rs = baseDao.queryForRowSet(
+                "select LQPO_ID from " + TABLE_PO_HEADER
+                        + " where LQPO_ERPORDERNUMBER = ?", request.getErpOrderNumber());
+        int poId = 0;
+        if (rs.next()) {
+            poId = rs.getInt("LQPO_ID");
+        }
+
+        String sql = "update " + TABLE_PO_HEADER + " set "
+                + "LQPO_ELSACCOUNT = ?, LQPO_SUPPLIERCODE = ?, LQPO_SUPPLIERNAME = ?, "
+                + "LQPO_COMPANYCODE = ?, LQPO_ORDERTYPE = ?, LQPO_ORDERSTATUS = ?, LQPO_IFGIVEAWAY = ?, "
+                + "LQPO_SUPPLIERLOCATION = ?, LQPO_CURRENCY = ?, LQPO_CUSTOMER = ?, "
+                + "LQPO_HEADERPROJECTNAME = ?, LQPO_BACKSO = ?, LQPO_GTSNO = ?, "
+                + "LQPO_GTSDELIVERYADDRESS = ?, LQPO_PURCHASEREMARK = ?, LQPO_PURCHASEPRINCIPAL = ?, "
+                + "LQPO_ORDERDATE = ?, LQPO_STATUS = ?, LQPO_RECEIVETIME = sysdate "
+                + "where LQPO_ID = ?";
+
+        baseDao.execute(sql,
+                request.getElsAccount(),
+                request.getSupplierCode(),
+                request.getSupplierName(),
+                request.getCompanyCode(),
+                request.getOrderType(),
+                request.getOrderStatus(),
+                request.getIfGiveaway(),
+                request.getSupplierLocation(),
+                request.getCurrency(),
+                request.getCustomer(),
+                request.getHeaderProjectName(),
+                request.getBackSo(),
+                request.getGtsNo(),
+                request.getGtsDeliveryAddress(),
+                request.getPurchaseRemark(),
+                request.getPurchasePrincipal(),
+                request.getOrderDate(),
+                "待处理",
+                poId
+        );
+
+        return poId;
+    }
+
+    /**
+     * 批量插入采购订单明细
+     *
+     * @param poId  订单主键ID
+     * @param lines 明细列表
+     */
+    private void insertPoLines(int poId, List<PoLine> lines) {
+        String sql = "insert into " + TABLE_PO_LINE + " ("
+                + "LQPL_ID, LQPL_POID, LQPL_ERPORDERLINENO, LQPL_FACTORY, LQPL_FACTORYNAME, "
+                + "LQPL_MATERIALNUMBER, LQPL_MATERIALSPEC, LQPL_MATERIALDESC, LQPL_MANUFACTURER, "
+                + "LQPL_REQUIREDATE, LQPL_QUANTITY, LQPL_PURCHASEUNIT, LQPL_NETPRICE, LQPL_NETAMOUNT, "
+                + "LQPL_PURCHASETYPE, LQPL_REPLYDATE, LQPL_PLACERECEIPT, LQPL_DELIVERYADDRESS, "
+                + "LQPL_ISCLOSE, LQPL_LINEPROJECTNUMBER, LQPL_RECEIVECONTACT, LQPL_RECEIVEPHONE"
+                + ") values (?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?, ?,?,?, ?,?,?,?)";
+
+        for (PoLine line : lines) {
+            int lineId = baseDao.getSeqId(SEQ_PO_LINE);
+            /*System.out.println(showSql(sql,lineId,
+                    poId,
+                    line.getErpOrderLineNo(),
+                    line.getFactory(),
+                    line.getFactoryName(),
+                    line.getMaterialNumber(),
+                    line.getMaterialSpec(),
+                    line.getMaterialDesc(),
+                    line.getManufacturer(),
+                    line.getRequireDate(),
+                    line.getQuantity(),
+                    line.getPurchaseUnit(),
+                    line.getNetPrice(),
+                    line.getNetAmount(),
+                    line.getPurchaseType(),
+                    line.getReplyDate(),
+                    line.getPlaceReceipt(),
+                    line.getDeliveryAddress(),
+                    line.getIsClose(),
+                    line.getLineProjectNumber(),
+                    line.getReceiveContact(),
+                    line.getReceivePhone()));*/
+            baseDao.execute(sql,
+                    lineId,
+                    poId,
+                    line.getErpOrderLineNo(),
+                    line.getFactory(),
+                    line.getFactoryName(),
+                    line.getMaterialNumber(),
+                    line.getMaterialSpec(),
+                    line.getMaterialDesc(),
+                    line.getManufacturer(),
+                    line.getRequireDate(),
+                    line.getQuantity(),
+                    line.getPurchaseUnit(),
+                    line.getNetPrice(),
+                    line.getNetAmount(),
+                    line.getPurchaseType(),
+                    line.getReplyDate(),
+                    line.getPlaceReceipt(),
+                    line.getDeliveryAddress(),
+                    line.getIsClose(),
+                    line.getLineProjectNumber(),
+                    line.getReceiveContact(),
+                    line.getReceivePhone()
+            );
+        }
+    }
+
+    public static String showSql(String sql, Object... params) {
+        for (Object param : params) {
+            if (param == null) {
+                sql = sql.replaceFirst("\\?", "NULL");
+            } else if (param instanceof Number) {
+                sql = sql.replaceFirst("\\?", param.toString());
+            } else {
+                sql = sql.replaceFirst("\\?",
+                        "'" + param.toString().replace("'", "''") + "'");
+            }
+        }
+        return sql;
+    }
+
+    /**
+     * 解析账套标识
+     * 优先级:URL master > yml accountMapping[elsAccount]
+     *
+     * @param request 采购订单请求
+     * @return 账套标识,未匹配到返回 null
+     */
+    private String resolveMaster(PoPushRequest request) {
+        // 1. URL 上的 master 优先
+        if (!isBlank(request.getMaster())) {
+            return request.getMaster().trim();
+        }
+        // 2. 按 elsAccount 查配置映射
+        if (longcheerConfig != null && longcheerConfig.getAccountMapping() != null
+                && !isBlank(request.getElsAccount())) {
+            String mapped = longcheerConfig.getAccountMapping().get(request.getElsAccount().trim());
+            if (!isBlank(mapped)) {
+                logger.info("[龙旗接口]按elsAccount自动映射账套,elsAccount={},master={}",
+                        request.getElsAccount(), mapped);
+                return mapped.trim();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 根据 master 解析对应的 elsCode(ELS 账号)
+     * 通过 accountMapping 反向查找(value → key)
+     *
+     * @param master 租户标识
+     * @return elsCode,未匹配到返回空串
+     */
+    private String resolveElsCode(String master) {
+        if (isBlank(master) || longcheerConfig == null
+                || longcheerConfig.getAccountMapping() == null
+                || longcheerConfig.getAccountMapping().isEmpty()) {
+            return "";
+        }
+        String trimmed = master.trim();
+        for (java.util.Map.Entry<String, String> entry : longcheerConfig.getAccountMapping().entrySet()) {
+            if (entry.getValue() != null && trimmed.equalsIgnoreCase(entry.getValue().trim())) {
+                return entry.getKey();
+            }
+        }
+        logger.warn("[龙旗接口]未找到master对应的elsCode,master={}", master);
+        return "";
+    }
+
+    /**
+     * 根据 master 解析对应的 supplierErpCode(供应商 ERP 编码)
+     * 查 supplierMapping(key=master, value=supplierErpCode)
+     *
+     * @param master 租户标识
+     * @return supplierErpCode,未匹配到返回空串
+     */
+    private String resolveSupplierErpCode(String master) {
+        if (isBlank(master) || longcheerConfig == null
+                || longcheerConfig.getSupplierMapping() == null
+                || longcheerConfig.getSupplierMapping().isEmpty()) {
+            return "";
+        }
+        String mapped = longcheerConfig.getSupplierMapping().get(master.trim());
+        if (isBlank(mapped)) {
+            logger.warn("[龙旗接口]未找到master对应的supplierErpCode,master={}", master);
+            return "";
+        }
+        return mapped.trim();
+    }
+
+    /**
+     * 判断字符串是否为空
+     *
+     * @param str 字符串
+     * @return true 表示为空
+     */
+    private boolean isBlank(String str) {
+        return str == null || str.trim().isEmpty();
+    }
+}

+ 98 - 0
src/main/java/com/uas/eis/longcheer/utils/LongcheerHttpClient.java

@@ -0,0 +1,98 @@
+package com.uas.eis.longcheer.utils;
+
+import okhttp3.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 龙旗接口 HTTP 客户端
+ * 封装 OkHttp,自动携带鉴权 Header,支持重试
+ *
+ * @author ZXL
+ * @date 2026-06-26
+ */
+public class LongcheerHttpClient {
+
+    private static final Logger logger = LoggerFactory.getLogger(LongcheerHttpClient.class);
+
+    private static final MediaType JSON_TYPE = MediaType.parse("application/json; charset=utf-8");
+
+    private static final OkHttpClient HTTP_CLIENT = new OkHttpClient.Builder()
+            .connectTimeout(30, TimeUnit.SECONDS)
+            .readTimeout(30, TimeUnit.SECONDS)
+            .writeTimeout(30, TimeUnit.SECONDS)
+            .build();
+
+    /**
+     * 发送 POST 请求(带重试)
+     *
+     * @param url           接口地址
+     * @param appKey        应用密钥 Key
+     * @param appSecret     应用密钥 Secret
+     * @param requestBody   请求体(JSON 字符串)
+     * @param timeout       超时时间(毫秒)
+     * @param retryTimes    重试次数
+     * @param retryInterval 重试间隔(毫秒)
+     * @return 响应体字符串
+     */
+    public static String post(String url, String appKey, String appSecret,
+                             String requestBody, int timeout, int retryTimes, int retryInterval) {
+        int attempt = 0;
+        Exception lastException = null;
+
+        while (attempt <= retryTimes) {
+            try {
+                String response = doPost(url, appKey, appSecret, requestBody, timeout);
+                logger.info("[龙旗HTTP]请求成功,url={},attempt={}", url, attempt + 1);
+                return response;
+            } catch (Exception e) {
+                lastException = e;
+                attempt++;
+                if (attempt <= retryTimes) {
+                    logger.warn("[龙旗HTTP]请求失败,url={},attempt={},重试间隔={}ms,异常:{}",
+                            url, attempt, retryInterval, e.getMessage());
+                    try {
+                        Thread.sleep(retryInterval);
+                    } catch (InterruptedException ie) {
+                        Thread.currentThread().interrupt();
+                        throw new RuntimeException("重试中断", ie);
+                    }
+                }
+            }
+        }
+
+        logger.error("[龙旗HTTP]请求失败,已重试{}次,url={}", retryTimes, url, lastException);
+        throw new RuntimeException("调用龙旗接口失败:" + (lastException != null ? lastException.getMessage() : "未知错误"));
+    }
+
+    private static String doPost(String url, String appKey, String appSecret,
+                                String requestBody, int timeout) throws IOException {
+        OkHttpClient client = HTTP_CLIENT.newBuilder()
+                .connectTimeout(timeout, TimeUnit.MILLISECONDS)
+                .readTimeout(timeout, TimeUnit.MILLISECONDS)
+                .writeTimeout(timeout, TimeUnit.MILLISECONDS)
+                .build();
+
+        Request request = new Request.Builder()
+                .url(url)
+                .addHeader("x-app-key", appKey)
+                .addHeader("x-app-secret", appSecret)
+                .addHeader("Content-Type", "application/json")
+                .post(RequestBody.create(JSON_TYPE,requestBody))
+                .build();
+
+        try (Response response = client.newCall(request).execute()) {
+            if (!response.isSuccessful()) {
+                throw new IOException("HTTP 状态码:" + response.code() + ",响应:" + response.message());
+            }
+            ResponseBody body = response.body();
+            if (body == null) {
+                throw new IOException("响应体为空");
+            }
+            return body.string();
+        }
+    }
+}

+ 104 - 0
src/main/java/com/uas/eis/task/LongcheerTask.java

@@ -0,0 +1,104 @@
+package com.uas.eis.task;
+
+import com.uas.eis.core.config.SpObserver;
+import com.uas.eis.dao.BaseDao;
+import com.uas.eis.dao.SqlRowList;
+import com.uas.eis.longcheer.dto.LongcheerResponse;
+import com.uas.eis.longcheer.service.LongcheerService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙旗接口定时任务
+ *
+ * @author ZXL
+ * @date 2026-06-30
+ */
+@Slf4j
+@Component
+public class LongcheerTask {
+
+    @Autowired
+    private LongcheerService longcheerService;
+
+    @Autowired
+    private BaseDao baseDao;
+
+    /**
+     * 库存同步定时任务
+     * 每天凌晨 2:00 执行
+     * 遍历所有账套,查询待推送的库存记录,调用龙旗库存同步接口
+     */
+    //@Scheduled(cron = "0 0 2 * * ?")
+    public void pushInventoryTask() {
+        log.info("[龙旗定时任务]库存同步开始=========start=============");
+        Date startDate = new Date();
+
+        // 1. 清除数据源标识,使用默认数据源查询所有账套
+        SpObserver.clear();
+        List<Map<String, Object>> masterList = baseDao.queryForList("select MA_USER from DATACENTER_P.master WHERE MA_USER IN ('LHWX_SZ_P','LHWX_HK_P')");
+        if (masterList == null || masterList.isEmpty()) {
+            log.info("[龙旗定时任务]未查询到账套信息,任务结束");
+            return;
+        }
+
+        int successCount = 0;
+        int failCount = 0;
+
+        // 2. 遍历每个账套
+        for (Map<String, Object> masterMap : masterList) {
+            String master = String.valueOf(masterMap.get("MA_USER"));
+            log.info("[龙旗定时任务]当前处理账套:{}", master);
+
+            try {
+                // 切换到账套数据源,查询待推送的库存记录
+                SpObserver.putSp(master);
+                // TODO 取出最新审核的单据
+                SqlRowList rs = baseDao.queryForRowSet("SELECT LQI_ID FROM ( SELECT LQI_ID FROM LQINVENTORY WHERE LQI_STATUS = '已审核' ORDER BY LQI_INDATE DESC) WHERE ROWNUM = 1");
+                List<Integer> idList = new ArrayList<>();
+                while (rs.next()) {
+                    idList.add(rs.getInt("LQI_ID"));
+                }
+
+                if (idList.isEmpty()) {
+                    log.info("[龙旗定时任务]账套={} 无待推送库存记录", master);
+                    continue;
+                }
+
+                log.info("[龙旗定时任务]账套={} 待推送记录数={}", master, idList.size());
+
+                // 3. 逐条推送
+                for (Integer id : idList) {
+                    try {
+                        LongcheerResponse response = longcheerService.pushInventory(master, id);
+                        if (response != null && response.isOk()) {
+                            successCount++;
+                            log.info("[龙旗定时任务]账套={} LQI_ID={} 推送成功", master, id);
+                        } else {
+                            failCount++;
+                            String msg = response != null ? response.getMessage() : "响应为空";
+                            log.warn("[龙旗定时任务]账套={} LQI_ID={} 推送失败,message={}", master, id, msg);
+                        }
+                    } catch (Exception e) {
+                        failCount++;
+                        log.error("[龙旗定时任务]账套={} LQI_ID={} 推送异常:{}", master, id, e.getMessage(), e);
+                    }
+                }
+            } catch (Exception e) {
+                log.error("[龙旗定时任务]账套={} 处理异常:{}", master, e.getMessage(), e);
+            } finally {
+                SpObserver.clear();
+            }
+        }
+
+        long costSeconds = (System.currentTimeMillis() - startDate.getTime()) / 1000;
+        log.info("[龙旗定时任务]库存同步结束,成功={},失败={},耗时={}秒", successCount, failCount, costSeconds);
+    }
+}

+ 24 - 1
src/main/resources/application-dev.yml

@@ -2,7 +2,7 @@ spring:
     datasource:
         type: com.alibaba.druid.pool.DruidDataSource
         driverClassName: oracle.jdbc.OracleDriver
-        username: LHWX_HK_P
+        username: DATACENTER_P
         password: select!#%*(
         url: jdbc:oracle:thin:@127.0.0.1:1521:orcl
         initialSize: 1
@@ -29,4 +29,27 @@ action:
     api_action: /EIS/api,/EIS/mes,/EIS/erp
     public_actions: /EIS/logout,/EIS/hello1
 
+# 龙旗接口对接配置
+longcheer:
+    inventory:
+        app-key: 4865982d
+        app-secret: cc0486b594134a679ff8a0d94144d685
+        url: https://srm-uat.longcheer.com:8019/els/supplierApi/invokeSrm/inventoryReceive
+    po:
+        app-key: a23b0e49
+        app-secret: cce462b6026f49e8b49e66d27c76b785
+        url: https://srm-uat.longcheer.com:8019/api/v1/po/push
+    timeout: 30000
+    retry-times: 3
+    retry-interval: 30000
+    # ELS 账号 -> 账套标识 的映射(龙旗不传 master 时按 elsAccount 自动定位账套)
+    account-mapping:
+        3582452: LHWX_SZ_P
+        3582700: LHWX_HK_P
+    # 账套标识 -> 供应商 ERP 编码 的映射(库存推送接口动态填 supplierErpCode)
+    supplier-mapping:
+        LHWX_SZ_P: 103362
+        LHWX_HK_P: 101661
+
+
 

+ 24 - 2
src/main/resources/application-prod.yml

@@ -2,9 +2,9 @@ spring:
     datasource:
         type: com.alibaba.druid.pool.DruidDataSource
         driverClassName: oracle.jdbc.OracleDriver
-        username: LHWX_HK_P
+        username: DATACENTER_P
         password: select!#%*(
-        url: jdbc:oracle:thin:@127.0.0.1:1521:orcl
+        url: jdbc:oracle:thin:@113.108.118.106:1521:orcl
         initialSize: 1
         maxActive: 3
         maxWait: 30000
@@ -28,3 +28,25 @@ server:
 action:
     api_action: /EIS/api,/EIS/mes,/EIS/erp
     public_actions: /EIS/logout,/EIS/hello1
+
+# 龙旗接口对接配置
+longcheer:
+    inventory:
+        app-key: 4865982d
+        app-secret: cc0486b594134a679ff8a0d94144d685
+        url: https://srm-uat.longcheer.com:8019/els/supplierApi/invokeSrm/inventoryReceive
+    po:
+        app-key: a23b0e49
+        app-secret: cce462b6026f49e8b49e66d27c76b785
+        url: https://srm-uat.longcheer.com:8019/api/v1/po/push
+    timeout: 30000
+    retry-times: 3
+    retry-interval: 30000
+    # ELS 账号 -> 账套标识 的映射(龙旗不传 master 时按 elsAccount 自动定位账套)
+    account-mapping:
+        3582452: LHWX_SZ_P
+        3582700: LHWX_HK_P
+    # 账套标识 -> 供应商 ERP 编码 的映射(库存推送接口动态填 supplierErpCode)
+    supplier-mapping:
+        LHWX_SZ_P: 103362
+        LHWX_HK_P: 101661

+ 22 - 0
src/main/resources/application.yml

@@ -8,3 +8,25 @@ spring:
             enabled: true
     message:
         encoding: UTF-8
+
+# 龙旗接口对接配置
+longcheer:
+  inventory:
+    app-key: 4865982d
+    app-secret: cc0486b594134a679ff8a0d94144d685
+    url: https://srm-uat.longcheer.com:8019/els/supplierApi/invokeSrm/inventoryReceive
+  po:
+    app-key: a23b0e49
+    app-secret: cce462b6026f49e8b49e66d27c76b785
+    url: https://srm-uat.longcheer.com:8019/api/v1/po/push
+  timeout: 30000
+  retry-times: 3
+  retry-interval: 30000
+  # ELS 账号 -> 账套标识 的映射(龙旗不传 master 时按 elsAccount 自动定位账套)
+  account-mapping:
+    3582452: LHWX_SZ_P
+    3582700: LHWX_HK_P
+  # 账套标识 -> 供应商 ERP 编码 的映射(库存推送接口动态填 supplierErpCode)
+  supplier-mapping:
+    LHWX_SZ_P: 103362
+    LHWX_HK_P: 101661