Procházet zdrojové kódy

积分订单聚水同步,取消订单同步

yjwang před 10 hodinami
rodič
revize
ca13076a7c

+ 26 - 0
fs-admin/src/main/java/com/fs/his/controller/FsIntegralAfterSalesController.java

@@ -155,6 +155,32 @@ public class FsIntegralAfterSalesController extends BaseController
         return toAjax(fsIntegralAfterSalesService.updateFsIntegralAfterSales(fsIntegralAfterSales));
     }
 
+    /**
+     * 同步聚水潭取消订单/上传售后单
+     * <p>独立的聚水潭取消接口,与审核流程解耦,由后台手动触发。</p>
+     * <p>复用普通订单的聚水潭HTTP层接口(cancel/aftersaleUpload),不修改普通订单代码:
+     * <ul>
+     *   <li>未发货:调用聚水潭取消订单接口</li>
+     *   <li>已发货:上传售后单到聚水潭</li>
+     * </ul>
+     * 聚水潭订单ID来源于积分订单的 extendOrderId(推送聚水潭时回写)。</p>
+     */
+    @PreAuthorize("@ss.hasPermi('his:integralAfterSales:audit')")
+    @Log(title = "积分售后-同步聚水潭取消", businessType = BusinessType.UPDATE)
+    @PutMapping("/cancelJst")
+    public AjaxResult cancelJst(@RequestBody FsIntegralAfterSales fsIntegralAfterSales)
+    {
+        if (fsIntegralAfterSales.getId() == null) {
+            return AjaxResult.error("售后单ID不能为空");
+        }
+        R result = fsIntegralAfterSalesService.cancelJstOrder(fsIntegralAfterSales.getId());
+        // R 类无 FAIL 常量,约定:成功 code=200,失败 code=500
+        if (result != null && Integer.valueOf(500).equals(result.get("code"))) {
+            return AjaxResult.error(String.valueOf(result.get("msg")));
+        }
+        return AjaxResult.success(result != null ? result.get("msg") : "聚水潭同步成功");
+    }
+
     /**
      * 撤销售后单(salesStatus: 0 → 1)
      * <p>仅正常状态(salesStatus=0)且未进入退款流程(status < 4)的售后单可撤销。</p>

+ 4 - 0
fs-service/src/main/java/com/fs/his/domain/FsIntegralGoods.java

@@ -70,6 +70,10 @@ public class FsIntegralGoods extends BaseEntity
     @Excel(name = "产品编码")
     private String barCode;
 
+    /** 是否药品 0否 1是 */
+    @Excel(name = "是否药品", dictType = "sys_yes_no")
+    private Integer isDrug;
+
     /**
      * 非数据库字段 仅用于订单信息存储购买数量
      */

+ 3 - 0
fs-service/src/main/java/com/fs/his/domain/FsIntegralOrder.java

@@ -124,6 +124,9 @@ public class FsIntegralOrder extends BaseEntity
     @Excel(name = "销售公司ID")
     private Long companyId;
 
+    /** ERP外部订单号(聚水潭返回的oId,用于幂等判断,防止重复推送) */
+    private String extendOrderId;
+
     /** 是否可申请售后 0不可 1可(非表字段,由 Controller 按订单状态+售后时效计算填充) */
     @TableField(exist = false)
     private Integer isAfterSales;

+ 17 - 0
fs-service/src/main/java/com/fs/his/service/IFsIntegralAfterSalesService.java

@@ -129,4 +129,21 @@ public interface IFsIntegralAfterSalesService
      * @return 操作结果
      */
     R revoke(Long userId, Long salesId);
+
+    /**
+     * 同步聚水潭取消订单/上传售后单(积分订单版)
+     * <p>与普通订单 {@link com.fs.hisStore.service.impl.FsStoreAfterSalesScrmServiceImpl#audit1} 中的聚水潭逻辑对齐,
+     * 但完全独立实现,不复用 {@link com.fs.erp.service.IErpOrderService#refundUpdateScrm},
+     * 因为该方法内部硬编码反查 fs_store_order_scrm 普通订单表,无法处理积分订单。</p>
+     * <p>直接复用聚水潭HTTP层接口:
+     * <ul>
+     *   <li>未发货:{@link com.fs.erp.http.JstErpHttpService#cancel} 调用聚水潭取消订单接口</li>
+     *   <li>已发货:{@link com.fs.erp.http.JstErpHttpService#aftersaleUpload} 上传售后单到聚水潭</li>
+     * </ul>
+     * 聚水潭订单ID来源于积分订单的 extendOrderId(推送聚水潭时回写)。</p>
+     *
+     * @param afterSalesId 积分售后单主键
+     * @return 操作结果,成功返回 R.ok,失败返回 R.error(异常不阻断审核主流程)
+     */
+    R cancelJstOrder(Long afterSalesId);
 }

+ 13 - 0
fs-service/src/main/java/com/fs/his/service/IFsIntegralOrderService.java

@@ -165,4 +165,17 @@ public interface IFsIntegralOrderService
      * 积分订单支付金额计算
      */
     R computeOrder(FsIntegralOrderComputeParam param);
+
+    /**
+     * 推送积分订单到聚水潭ERP
+     * <p>
+     * 与普通订单({@link com.fs.hisStore.service.IFsStoreOrderScrmService#createOmsOrder})隔离,
+     * 独立处理积分订单的ERP推送,避免因表结构差异(无独立item表、地址格式不同、payType语义不同)导致逻辑混乱。
+     * <p>
+     * 触发时机:积分订单支付回调({@link #payConfirmByYop})成功后调用
+     *
+     * @param orderId 积分订单主键
+     * @return 推送结果,成功返回R.ok,失败返回R.error
+     */
+    R createOmsOrder(Long orderId);
 }

+ 215 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsIntegralAfterSalesServiceImpl.java

@@ -1,16 +1,26 @@
 package com.fs.his.service.impl;
 
+import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
 import com.fs.common.core.domain.R;
 import com.fs.common.exception.CustomException;
 import com.fs.common.utils.StringUtils;
+import com.fs.erp.constant.AfterSalesOrderStatusEnum;
+import com.fs.erp.dto.OrderCancelRequestDTO;
+import com.fs.erp.dto.OrderQueryRequestDTO;
+import com.fs.erp.dto.OrderQueryResponseDTO;
+import com.fs.erp.dto.RefundItemDTO;
+import com.fs.erp.http.JstErpHttpService;
+import com.fs.his.config.FsSysConfig;
 import com.fs.his.domain.FsIntegralAfterSales;
+import com.fs.his.domain.FsIntegralGoods;
 import com.fs.his.domain.FsIntegralOrder;
 import com.fs.his.mapper.FsIntegralAfterSalesMapper;
 import com.fs.his.mapper.FsIntegralOrderMapper;
 import com.fs.his.param.FsIntegralAfterSalesApplyParam;
 import com.fs.his.param.FsIntegralAfterSalesQueryParam;
 import com.fs.his.service.IFsIntegralAfterSalesService;
+import com.fs.his.utils.ConfigUtil;
 import com.fs.his.vo.FsIntegralAfterSalesVO;
 import com.fs.hisStore.config.StoreConfig;
 import com.fs.system.service.ISysConfigService;
@@ -21,7 +31,9 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.math.BigDecimal;
+import java.util.ArrayList;
 import java.util.Calendar;
+import java.util.Collections;
 import java.util.Date;
 import java.util.GregorianCalendar;
 import java.util.List;
@@ -46,6 +58,18 @@ public class FsIntegralAfterSalesServiceImpl implements IFsIntegralAfterSalesSer
     @Autowired
     private ISysConfigService configService;
 
+    /**
+     * 聚水潭HTTP服务(复用普通订单的HTTP层接口,不依赖普通订单Service)
+     */
+    @Autowired
+    private JstErpHttpService jstErpHttpService;
+
+    /**
+     * ERP配置工具(读取 his.config 全局配置,与积分订单推送聚水潭时一致)
+     */
+    @Autowired
+    private ConfigUtil configUtil;
+
     /**
      * 查询积分订单售后单
      *
@@ -389,4 +413,195 @@ public class FsIntegralAfterSalesServiceImpl implements IFsIntegralAfterSalesSer
         }
         return afterSales;
     }
+
+    /**
+     * 同步聚水潭取消订单/上传售后单(积分订单版)
+     * <p>与普通订单 audit1 中的聚水潭逻辑对齐,但完全独立实现:
+     * <ul>
+     *   <li>未发货:复用 {@link JstErpHttpService#cancel} 调用聚水潭取消订单接口</li>
+     *   <li>已发货:复用 {@link JstErpHttpService#aftersaleUpload} 上传售后单到聚水潭</li>
+     * </ul>
+     * 聚水潭订单ID来源:积分订单的 extendOrderId(推送聚水潭时回写)。
+     * 不复用 {@link com.fs.erp.service.IErpOrderService#refundUpdateScrm},
+     * 因其内部硬编码反查 fs_store_order_scrm 普通订单表。</p>
+     */
+    @Override
+    public R cancelJstOrder(Long afterSalesId) {
+        // 1. 查询售后单
+        FsIntegralAfterSales afterSales = fsIntegralAfterSalesMapper.selectFsIntegralAfterSalesById(afterSalesId);
+        if (afterSales == null) {
+            log.warn("[积分订单聚水潭取消] 售后单不存在 afterSalesId={}", afterSalesId);
+            return R.error("售后单不存在");
+        }
+
+        // 2. 查询积分订单
+        FsIntegralOrder order = fsIntegralOrderMapper.selectFsIntegralOrderByOrderId(afterSales.getOrderId());
+        if (order == null) {
+            log.warn("[积分订单聚水潭取消] 积分订单不存在 afterSalesId={}, orderId={}", afterSalesId, afterSales.getOrderId());
+            return R.error("积分订单不存在");
+        }
+
+        // 3. 校验ERP配置(与推送时一致:erpOpen=1 且 erpType=5 聚水潭)
+        FsSysConfig erpConfig = configUtil.getSysConfig();
+        if (erpConfig == null || erpConfig.getErpOpen() == null || erpConfig.getErpOpen() != 1) {
+            log.info("[积分订单聚水潭取消] 未启用ERP,跳过 afterSalesId={}", afterSalesId);
+            return R.ok("未启用ERP,跳过同步");
+        }
+        if (erpConfig.getErpType() == null || erpConfig.getErpType() != 5) {
+            log.info("[积分订单聚水潭取消] 当前ERP类型非聚水潭,跳过 afterSalesId={}, erpType={}", afterSalesId, erpConfig.getErpType());
+            return R.ok("当前ERP类型不支持");
+        }
+
+        // 4. 校验聚水潭订单ID(推送聚水潭时回写的 extendOrderId)
+        String extendOrderId = order.getExtendOrderId();
+        if (StringUtils.isBlank(extendOrderId)) {
+            log.warn("[积分订单聚水潭取消] 聚水潭订单ID为空,订单未推送过聚水潭 afterSalesId={}, orderCode={}",
+                    afterSalesId, order.getOrderCode());
+            return R.error("订单未推送聚水潭,无法取消");
+        }
+
+        // 5. 查询聚水潭订单状态,判断已发货/未发货
+        String jstStatus = queryJstOrderStatus(order.getOrderCode(), erpConfig.getErpJstShopCode());
+        boolean alreadySent = jstStatus != null ? "Sent".equalsIgnoreCase(jstStatus)
+                : (order.getStatus() != null && order.getStatus() == 2);
+
+        // 6. 店铺编码(与推送时一致,取全局配置)
+        String shopCode = erpConfig.getErpJstShopCode();
+
+        try {
+            if (alreadySent) {
+                // 已发货:上传售后单到聚水潭(复用普通订单的 RefundOrderDTO + aftersaleUpload)
+                uploadIntegralAftersale(afterSales, order, shopCode);
+            } else {
+                // 未发货:取消聚水潭订单(复用普通订单的 OrderCancelRequestDTO + cancel)
+                cancelJstOrder(extendOrderId);
+            }
+        } catch (Exception e) {
+            log.error("[积分订单聚水潭取消] 异常 afterSalesId={}, orderCode={}, extendOrderId={}",
+                    afterSalesId, order.getOrderCode(), extendOrderId, e);
+            return R.error("聚水潭同步异常: " + e.getMessage());
+        }
+
+        return R.ok("聚水潭同步成功");
+    }
+
+    /**
+     * 查询聚水潭订单状态
+     * <p>复用普通订单的 OrderQueryRequestDTO/OrderQueryResponseDTO + jstErpHttpService.query 接口,
+     * 不依赖普通订单 Service。</p>
+     *
+     * @param orderCode 平台订单号
+     * @param shopCode  聚水潭店铺编码
+     * @return 聚水潭订单状态字符串(Sent/Delivering/WaitConfirm/Cancelled 等);查不到或异常返回 null
+     */
+    private String queryJstOrderStatus(String orderCode, String shopCode) {
+        if (StringUtils.isBlank(orderCode)) {
+            return null;
+        }
+        try {
+            OrderQueryRequestDTO requestDTO = OrderQueryRequestDTO.builder()
+                    .soIds(Collections.singletonList(orderCode))
+                    .pageIndex(1)
+                    .pageSize(1)
+                    .isGetTotal(false)
+                    .build();
+            if (StringUtils.isNotBlank(shopCode) && !"0".equals(shopCode)) {
+                try {
+                    requestDTO.setShopId(Integer.valueOf(shopCode));
+                } catch (NumberFormatException ignore) {
+                    // shop_code 非数字时忽略该参数,由聚水潭内部匹配
+                }
+            }
+            OrderQueryResponseDTO responseDTO = jstErpHttpService.query(requestDTO);
+            if (responseDTO == null || responseDTO.getOrders() == null || responseDTO.getOrders().isEmpty()) {
+                return null;
+            }
+            return responseDTO.getOrders().get(0).getStatus();
+        } catch (Exception e) {
+            log.error("[积分订单聚水潭取消] 查询聚水潭订单状态异常 orderCode={}", orderCode, e);
+            return null;
+        }
+    }
+
+    /**
+     * 取消聚水潭订单(未发货场景)
+     * <p>复用普通订单的 OrderCancelRequestDTO + jstErpHttpService.cancel 接口。</p>
+     *
+     * @param extendOrderId 聚水潭订单ID(oId)
+     */
+    private void cancelJstOrder(String extendOrderId) {
+        OrderCancelRequestDTO requestDTO = new OrderCancelRequestDTO()
+                .setOIds(Collections.singletonList(Integer.valueOf(extendOrderId)))
+                .setCancelType("用户退款")
+                .setRemark("积分订单售后取消");
+        jstErpHttpService.cancel(requestDTO);
+        log.info("[积分订单聚水潭取消] 取消订单成功 extendOrderId={}", extendOrderId);
+    }
+
+    /**
+     * 上传售后单到聚水潭(已发货场景)
+     * <p>复用普通订单的 RefundOrderDTO/RefundItemDTO + jstErpHttpService.aftersaleUpload 接口。
+     * 商品信息从积分订单的 itemJson 解析(List&lt;FsIntegralGoods&gt;),不依赖普通订单商品表。</p>
+     *
+     * @param afterSales 积分售后单
+     * @param order      积分订单
+     * @param shopCode   聚水潭店铺编码
+     */
+    private void uploadIntegralAftersale(FsIntegralAfterSales afterSales, FsIntegralOrder order, String shopCode) {
+        com.fs.ybPay.dto.RefundOrderDTO refundOrderDTO = new com.fs.ybPay.dto.RefundOrderDTO();
+
+        // 退货退款(有寄回物流单号)vs 仅退款
+        boolean isReturnRefund = StringUtils.isNotBlank(afterSales.getDeliverySn());
+        if (isReturnRefund) {
+            refundOrderDTO.setShopStatus(AfterSalesOrderStatusEnum.WAIT_SELLER_CONFIRM_GOODS.getCode());
+            refundOrderDTO.setType("普通退货");
+            refundOrderDTO.setLogisticsCompany(afterSales.getDeliveryName());
+            refundOrderDTO.setLId(afterSales.getDeliverySn());
+        } else {
+            refundOrderDTO.setShopStatus(AfterSalesOrderStatusEnum.WAIT_SELLER_AGREE.getCode());
+            refundOrderDTO.setType("仅退款");
+        }
+
+        refundOrderDTO.setQuestionType("可更新");
+        refundOrderDTO.setOuterAsId(String.valueOf(afterSales.getId()));
+        refundOrderDTO.setRemark("积分订单售后退款");
+        refundOrderDTO.setSoId(order.getOrderCode());
+        refundOrderDTO.setRefund(order.getPayMoney() != null ? order.getPayMoney() : BigDecimal.ZERO);
+        refundOrderDTO.setPayment(BigDecimal.ZERO);
+        refundOrderDTO.setTotalAmount(order.getPayMoney() != null ? order.getPayMoney() : BigDecimal.ZERO);
+
+        if (StringUtils.isNotBlank(shopCode) && !"0".equals(shopCode)) {
+            try {
+                refundOrderDTO.setShopId(Long.valueOf(shopCode));
+            } catch (NumberFormatException ignore) {
+                // shop_code 非数字时不传 shop_id,由聚水潭内部匹配
+            }
+        }
+
+        // 构造退货商品列表(从 itemJson 解析,复用普通订单的 RefundItemDTO)
+        List<RefundItemDTO> refundItems = new ArrayList<>();
+        if (StringUtils.isNotBlank(order.getItemJson())) {
+            try {
+                List<FsIntegralGoods> goodsList = JSONUtil.toList(JSONUtil.parseArray(order.getItemJson()), FsIntegralGoods.class);
+                if (goodsList != null) {
+                    for (FsIntegralGoods goods : goodsList) {
+                        RefundItemDTO ri = new RefundItemDTO();
+                        ri.setSkuId(StringUtils.isNotBlank(goods.getBarCode()) ? goods.getBarCode() : "");
+                        ri.setQty(goods.getNum() != null ? goods.getNum() : 1);
+                        ri.setAmount(goods.getCash() != null ? goods.getCash() : BigDecimal.ZERO);
+                        // 聚水潭文档:items[].type 只能是 退货/换货/其它/补发
+                        ri.setType("退货");
+                        refundItems.add(ri);
+                    }
+                }
+            } catch (Exception e) {
+                log.error("[积分订单聚水潭取消] 解析积分订单itemJson失败 orderCode={}", order.getOrderCode(), e);
+            }
+        }
+        refundOrderDTO.setItems(refundItems);
+
+        Object uploadResp = jstErpHttpService.aftersaleUpload(refundOrderDTO);
+        log.info("[积分订单聚水潭取消] 售后单上传成功 outerAsId={}, soId={}, response={}",
+                afterSales.getId(), order.getOrderCode(), JSONUtil.toJsonStr(uploadResp));
+    }
 }

+ 254 - 1
fs-service/src/main/java/com/fs/his/service/impl/FsIntegralOrderServiceImpl.java

@@ -5,6 +5,7 @@ import cn.hutool.core.lang.TypeReference;
 import cn.hutool.core.util.IdUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
 import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -22,6 +23,10 @@ import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.mapper.CompanyUserMapper;
 import com.fs.core.utils.OrderCodeUtils;
+import com.fs.erp.constant.OrderStatusEnum;
+import com.fs.erp.dto.*;
+import com.fs.erp.http.JstErpHttpService;
+import com.fs.his.config.FsSysConfig;
 import com.fs.his.domain.*;
 import com.fs.his.dto.ComputeIntegralOrderMoneyDTO;
 import com.fs.his.enums.BusinessTypeEnum;
@@ -34,6 +39,7 @@ import com.fs.his.service.IFsIntegralOrderService;
 import com.fs.his.service.IFsStorePaymentService;
 import com.fs.his.service.IFsUserIntegralLogsService;
 import com.fs.his.service.IFsUserService;
+import com.fs.his.utils.ConfigUtil;
 import com.fs.his.utils.PhoneUtil;
 import com.fs.his.vo.FsIntegralOrderListUVO;
 import com.fs.his.vo.FsIntegralOrderListVO;
@@ -66,6 +72,7 @@ import org.springframework.transaction.interceptor.TransactionAspectSupport;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
+import java.text.SimpleDateFormat;
 import java.time.LocalDateTime;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
@@ -160,6 +167,18 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
     @Autowired
     private FsIntegralAfterSalesMapper fsIntegralAfterSalesMapper;
 
+    /**
+     * ERP配置工具(读取 his.config 中的 erpOpen/erpType/erpJstShopCode 等)
+     */
+    @Autowired
+    private ConfigUtil configUtil;
+
+    /**
+     * 聚水潭HTTP服务(当前积分订单仅支持推送聚水潭,如需扩展其他ERP需重构为策略模式)
+     */
+    @Autowired
+    private JstErpHttpService jstErpHttpService;
+
     /**
      * 查询积分商品订单
      *
@@ -356,7 +375,9 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
         order.setIntegral(totalIntegral.toString());
         order.setItemJson(JSONUtil.toJsonStr(goodsItem));
         order.setUserName(address.getRealName());
-        order.setUserAddress(address.getProvince()+address.getCity()+address.getDistrict()+address.getDetail());
+        // 地址用空格分隔省市区+详细地址,与普通订单(FsStoreOrderScrm)格式保持一致,
+        // 便于推送聚水潭时通过 split(" ") 拆分独立的省市区字段
+        order.setUserAddress(address.getProvince()+" "+address.getCity()+" "+address.getDistrict()+" "+address.getDetail());
         order.setUserPhone(address.getPhone());
         order.setCreateTime(new Date());
         order.setCompanyUserId(companyUserId);
@@ -812,6 +833,19 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
         integralParam.setBusinessId(order.getOrderId().toString());
 //        userIntegralLogsService.addIntegralTemplate(integralParam);
 
+        // 8. 推送聚水潭ERP(与普通订单payConfirmMultiStore一致:推送异常用try-catch隔离,不影响支付状态)
+        try {
+            R erpResult = createOmsOrder(order.getOrderId());
+            if (erpResult != null && erpResult.containsKey("code") && Integer.valueOf(200).equals(erpResult.get("code"))) {
+                log.info("积分订单回调(Yop) 推送聚水潭成功 orderId={}", order.getOrderId());
+            } else {
+                log.warn("积分订单回调(Yop) 推送聚水潭未成功 orderId={}, msg={}", order.getOrderId(),
+                        erpResult == null ? "返回null" : erpResult.get("msg"));
+            }
+        } catch (Exception e) {
+            log.error("积分订单回调(Yop) 推送聚水潭异常 orderId={}", order.getOrderId(), e);
+        }
+
         return "success";
     }
 
@@ -1511,4 +1545,223 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
         BigDecimal continueUnits = extra.divide(continueCount, 0, RoundingMode.CEILING);
         return firstPrice.add(continueUnits.multiply(continuePrice));
     }
+
+    /**
+     * 推送积分订单到聚水潭ERP
+     * <p>
+     * 与普通订单推送隔离,独立处理积分订单的聚水潭推送逻辑:
+     * <ul>
+     *   <li>商品信息从 {@code order.itemJson} 解析(List&lt;FsIntegralGoods&gt;),不依赖 fs_store_order_item_scrm 表</li>
+     *   <li>商品单价取 {@code FsIntegralGoods.cash}(支付金额),不反查 fs_store_product_scrm 表</li>
+     *   <li>收货地址通过空格分隔拆分省市区(创建订单时已统一为空格分隔格式)</li>
+     *   <li>不走货到付款逻辑(积分订单 payType 语义与普通订单不同)</li>
+     *   <li>卖家备注追加"兑换积分"信息,便于聚水潭侧识别</li>
+     * </ul>
+     */
+    @Override
+    public R createOmsOrder(Long orderId) {
+        // 1. 查询积分订单
+        FsIntegralOrder order = fsIntegralOrderMapper.selectFsIntegralOrderByOrderId(orderId);
+        if (order == null) {
+            log.error("推送聚水潭失败:积分订单不存在 orderId={}", orderId);
+            return R.error("积分订单不存在");
+        }
+
+        // 2. 读取ERP配置,判断是否启用
+        FsSysConfig erpConfig = configUtil.getSysConfig();
+        if (erpConfig == null || erpConfig.getErpOpen() == null || erpConfig.getErpOpen() != 1) {
+            log.info("未启用ERP,跳过推送积分订单 orderId={}", orderId);
+            return R.error("未启用ERP");
+        }
+
+        // 3. 判断ERP类型(当前仅支持聚水潭,erpType=5)
+        if (erpConfig.getErpType() == null || erpConfig.getErpType() != 5) {
+            log.info("当前ERP类型非聚水潭,跳过推送积分订单 orderId={}, erpType={}", orderId, erpConfig.getErpType());
+            return R.error("当前ERP类型不支持积分订单推送");
+        }
+
+        // 4. 公司黑名单校验
+        if (erpConfig.getNoErpCompany() != null && order.getCompanyId() != null
+                && erpConfig.getNoErpCompany().contains(order.getCompanyId())) {
+            log.info("公司{}在ERP黑名单中,跳过推送积分订单 orderId={}", order.getCompanyId(), orderId);
+            return R.error("相关公司不推送ERP");
+        }
+
+        // 5. 幂等校验:已推送过(extendOrderId非空)则跳过,防止重复推送
+        if (StringUtils.isNotEmpty(order.getExtendOrderId())) {
+            log.info("积分订单已推送聚水潭,跳过 orderId={}, extendOrderId={}", orderId, order.getExtendOrderId());
+            return R.error("订单已推送ERP");
+        }
+
+        // 6. 解析商品列表(从itemJson解析List<FsIntegralGoods>)
+        List<FsIntegralGoods> goodsList;
+        try {
+            goodsList = JSONUtil.toList(JSONUtil.parseArray(order.getItemJson()), FsIntegralGoods.class);
+        } catch (Exception e) {
+            log.error("解析积分订单商品JSON失败 orderId={}", orderId, e);
+            return R.error("解析商品信息失败");
+        }
+        if (goodsList == null || goodsList.isEmpty()) {
+            log.error("积分订单商品列表为空 orderId={}", orderId);
+            return R.error("订单商品为空");
+        }
+
+        // 7. 拆分收货地址(空格分隔:省 市 区 详细地址,与普通订单格式一致)
+        String[] addrParts = order.getUserAddress() == null ? new String[0] : order.getUserAddress().split(" ");
+        String province = addrParts.length > 0 ? addrParts[0] : "";
+        String city = addrParts.length > 1 ? addrParts[1] : "";
+        String district = addrParts.length > 2 ? addrParts[2] : "";
+        // 详细地址:第4段及之后用空格拼接;若格式不标准(段数不足),兜底用原始地址
+        String detailAddress = addrParts.length > 3
+                ? String.join(" ", Arrays.copyOfRange(addrParts, 3, addrParts.length))
+                : (addrParts.length > 0 ? order.getUserAddress() : "");
+
+        // 8. 校验聚水潭店铺编码
+        String shopCode = erpConfig.getErpJstShopCode();
+        if (StringUtils.isEmpty(shopCode)) {
+            log.error("聚水潭店铺编码(erpJstShopCode)未配置,无法推送积分订单 orderId={}", orderId);
+            return R.error("聚水潭店铺编码未配置");
+        }
+
+        // 9. 构造ShopOrderDTO
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+        String payDateStr = order.getPayTime() != null ? sdf.format(java.sql.Timestamp.valueOf(order.getPayTime())) : sdf.format(new Date());
+
+        ShopOrderDTO shopOrderDTO = new ShopOrderDTO();
+        shopOrderDTO.setShopId(Long.valueOf(shopCode));
+        shopOrderDTO.setSoId(order.getOrderCode());
+        shopOrderDTO.setOrderDate(payDateStr);
+        shopOrderDTO.setShopStatus(OrderStatusEnum.WAIT_SELLER_SEND_GOODS.name());
+        shopOrderDTO.setShopBuyerId(order.getUserName());
+        shopOrderDTO.setReceiverState(province);
+        shopOrderDTO.setReceiverCity(city);
+        shopOrderDTO.setReceiverDistrict(district);
+        shopOrderDTO.setReceiverAddress(detailAddress);
+        shopOrderDTO.setReceiverName(order.getUserName());
+        shopOrderDTO.setReceiverPhone(order.getUserPhone());
+        shopOrderDTO.setPayAmount(order.getPayMoney() != null ? order.getPayMoney().doubleValue() : 0d);
+        shopOrderDTO.setFreight(0d);
+
+        // 10. 卖家备注:积分订单无公司名称概念,仅记录兑换积分信息,便于聚水潭侧识别
+        String sellerMemo = StringUtils.isNotEmpty(order.getIntegral())
+                ? "兑换积分:" + order.getIntegral()
+                : "";
+        shopOrderDTO.setRemark(sellerMemo);
+
+        // 11. 构造订单商品项列表(积分商品无规格概念,propertiesValue传空串)
+        List<OrderItemDTO> itemDTOList = new ArrayList<>();
+        for (int i = 0; i < goodsList.size(); i++) {
+            FsIntegralGoods goods = goodsList.get(i);
+            OrderItemDTO itemDTO = new OrderItemDTO();
+            String barCode = StringUtils.isNotEmpty(goods.getBarCode()) ? goods.getBarCode() : "";
+            itemDTO.setSkuId(barCode);
+            itemDTO.setShopSkuId(barCode);
+            itemDTO.setShopIId(barCode);
+            itemDTO.setName(goods.getGoodsName());
+            itemDTO.setPropertiesValue("");
+
+            BigDecimal price = goods.getCash() != null ? goods.getCash() : BigDecimal.ZERO;
+            int qty = goods.getNum() != null ? goods.getNum() : 1;
+            itemDTO.setPrice(price);
+            itemDTO.setAmount(price.multiply(BigDecimal.valueOf(qty)));
+            itemDTO.setQty(qty);
+            itemDTO.setPic(goods.getImgUrl());
+            itemDTO.setOuterOiId(String.format("%s%d", order.getOrderCode(), i + 1));
+            itemDTOList.add(itemDTO);
+        }
+        shopOrderDTO.setItems(itemDTOList);
+
+        // 12. 构造支付信息(积分订单不走货到付款逻辑)
+        PaymentDTO paymentDTO = new PaymentDTO();
+        paymentDTO.setAmount(order.getPayMoney() != null ? order.getPayMoney().doubleValue() : 0d);
+        paymentDTO.setOuterPayId(order.getOrderCode());
+        paymentDTO.setPayDate(payDateStr);
+        paymentDTO.setPayment(resolvePaymentDesc(order.getPayType()));
+        paymentDTO.setBuyerAccount(order.getUserName());
+        paymentDTO.setSellerAccount("平台销售");
+        shopOrderDTO.setPay(paymentDTO);
+
+        // 13. 推送聚水潭
+        ErpOrderResponseDTO response;
+        try {
+            response = jstErpHttpService.upload(shopOrderDTO);
+        } catch (Exception e) {
+            log.error("推送聚水潭HTTP异常 orderId={}, orderCode={}", orderId, order.getOrderCode(), e);
+            return R.error("推送聚水潭异常: " + e.getMessage());
+        }
+
+        if (response == null || CollectionUtils.isEmpty(response.getDatas())) {
+            log.error("推送聚水潭返回空数据 orderId={}, orderCode={}", orderId, order.getOrderCode());
+            return R.error("推送聚水潭返回空数据");
+        }
+
+        ErpOrderResponseDTO.OrderData orderData = response.getDatas().get(0);
+        String erpOrderId = String.valueOf(orderData.getOId());
+        log.info("推送聚水潭成功 orderId={}, orderCode={}, erpOrderId={}", orderId, order.getOrderCode(), erpOrderId);
+
+        // 14. 回写extendOrderId(用于幂等判断)
+        FsIntegralOrder updateOrder = new FsIntegralOrder();
+        updateOrder.setOrderId(orderId);
+        updateOrder.setExtendOrderId(erpOrderId);
+        fsIntegralOrderMapper.updateFsIntegralOrder(updateOrder);
+
+        // 15. 指定发货仓(仓库编码来源于积分规则页面配置的 sys.erp.returnInfo)
+        String warehouseCode = resolveIntegralWarehouseCode();
+        if (StringUtils.isNotEmpty(warehouseCode)) {
+            try {
+                OrderModifyWmsRequestDTO wmsDto = new OrderModifyWmsRequestDTO()
+                        .setOId(Long.valueOf(erpOrderId))
+                        .setWmsCoId(Long.valueOf(warehouseCode));
+                List<OrderModifyWmsRequestDTO> wmsList = Collections.singletonList(wmsDto);
+                jstErpHttpService.modifyWmsUpload(wmsList);
+                log.info("指定发货仓成功 orderId={}, erpOrderId={}, wmsCoId={}", orderId, erpOrderId, warehouseCode);
+            } catch (Exception e) {
+                log.error("指定发货仓失败 orderId={}, erpOrderId={}, wmsCoId={}", orderId, erpOrderId, warehouseCode, e);
+            }
+        }
+
+        return R.ok("推送聚水潭成功");
+    }
+
+    /**
+     * 读取积分规则页面配置的聚水潭仓库编码
+     * <p>
+     * 配置来源:sys_config 表 configKey = {@code sys.erp.returnInfo},
+     * 由后台积分规则页面(pointsRule/index.vue)维护,值为JSON:
+     * <pre>{"warehouseCode":"18849902","returnConsignee":"...","returnPhone":"...","returnAddress":"..."}</pre>
+     *
+     * @return 仓库编码;未配置或解析异常时返回 null(不指定发货仓,由聚水潭默认仓发货)
+     */
+    private String resolveIntegralWarehouseCode() {
+        try {
+            String configValue = configService.selectConfigByKey("sys.erp.returnInfo");
+            if (StringUtils.isEmpty(configValue)) {
+                return null;
+            }
+            JSONObject json = JSONUtil.parseObj(configValue);
+            String warehouseCode = json.getStr("warehouseCode");
+            if (StringUtils.isEmpty(warehouseCode)) {
+                log.warn("积分规则ERP配置(sys.erp.returnInfo)未配置warehouseCode");
+                return null;
+            }
+            return warehouseCode;
+        } catch (Exception e) {
+            log.error("读取积分规则ERP配置(sys.erp.returnInfo)异常", e);
+            return null;
+        }
+    }
+
+    /**
+     * 根据积分订单payType解析支付方式描述
+     *
+     * @param payType 支付类型 1积分 2现金 3积分+现金
+     */
+    private String resolvePaymentDesc(Integer payType) {
+        if (payType == null) return "积分兑换";
+        switch (payType) {
+            case 2: return "微信支付";
+            case 3: return "积分+微信支付";
+            default: return "积分兑换";
+        }
+    }
 }

+ 6 - 1
fs-service/src/main/resources/mapper/his/FsIntegralGoodsMapper.xml

@@ -20,10 +20,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="descs"    column="descs"    />
         <result property="createTime"    column="create_time"    />
         <result property="barCode"    column="bar_code"    />
+        <result property="isDrug"    column="is_drug"    />
     </resultMap>
 
     <sql id="selectFsIntegralGoodsVo">
-        select goods_id, img_url, images,bar_code, goods_name, ot_price, goods_type, status, integral, cash, sort, stock, limit_num, descs, create_time from fs_integral_goods
+        select goods_id, img_url, images, bar_code, goods_name, ot_price, goods_type, status, integral, cash, sort, stock, limit_num, descs, create_time, is_drug from fs_integral_goods
     </sql>
 
     <select id="selectFsIntegralGoodsList" parameterType="FsIntegralGoods" resultMap="FsIntegralGoodsResult">
@@ -32,6 +33,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="goodsName != null  and goodsName != ''"> and goods_name like concat('%', #{goodsName}, '%')</if>
             <if test="goodsType != null "> and goods_type = #{goodsType}</if>
             <if test="status != null "> and status = #{status}</if>
+            <if test="isDrug != null "> and is_drug = #{isDrug}</if>
             <if test="integral != null "> and integral = #{integral}</if>
             <if test="cash != null "> and cash = #{cash}</if>
             <if test="createTime != null "> and create_time = #{createTime}</if>
@@ -61,6 +63,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="descs != null">descs,</if>
             <if test="createTime != null">create_time,</if>
             <if test="barCode != null">bar_code,</if>
+            <if test="isDrug != null">is_drug,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="goodsId != null">#{goodsId},</if>
@@ -78,6 +81,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="descs != null">#{descs},</if>
             <if test="createTime != null">#{createTime},</if>
             <if test="barCode != null">#{barCode},</if>
+            <if test="isDrug != null">#{isDrug},</if>
          </trim>
     </insert>
 
@@ -98,6 +102,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="descs != null">descs = #{descs},</if>
             <if test="createTime != null">create_time = #{createTime},</if>
             <if test="barCode != null">bar_code = #{barCode},</if>
+            <if test="isDrug != null">is_drug = #{isDrug},</if>
         </trim>
         where goods_id = #{goodsId}
     </update>

+ 5 - 1
fs-service/src/main/resources/mapper/his/FsIntegralOrderMapper.xml

@@ -27,10 +27,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="createTime"    column="create_time"    />
         <result property="remark"    column="remark"    />
         <result property="barCode"    column="bar_code"    />
+        <result property="extendOrderId"    column="extend_order_id"    />
     </resultMap>
 
     <sql id="selectFsIntegralOrderVo">
-        select order_id, order_code, user_id,bar_code, user_name, user_phone, user_address, item_json, integral, back_integral, pay_money,is_pay,pay_time,pay_type, status, delivery_code, delivery_name, delivery_sn, delivery_time, finish_time, create_time,qw_user_id,company_user_id,company_id,remark from fs_integral_order
+        select order_id, order_code, user_id,bar_code, user_name, user_phone, user_address, item_json, integral, back_integral, pay_money,is_pay,pay_time,pay_type, status, delivery_code, delivery_name, delivery_sn, delivery_time, finish_time, create_time,qw_user_id,company_user_id,company_id,remark,extend_order_id from fs_integral_order
     </sql>
 
     <select id="selectFsIntegralOrderList" parameterType="FsIntegralOrder" resultMap="FsIntegralOrderResult">
@@ -84,6 +85,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="qwUserId != null">qw_user_id,</if>
             <if test="companyUserId != null">company_user_id,</if>
             <if test="companyId != null">company_id,</if>
+            <if test="extendOrderId != null">extend_order_id,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="orderId != null">#{orderId},</if>
@@ -110,6 +112,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="qwUserId != null">#{qwUserId},</if>
             <if test="companyUserId != null">#{companyUserId},</if>
             <if test="companyId != null">#{companyId},</if>
+            <if test="extendOrderId != null">#{extendOrderId},</if>
          </trim>
     </insert>
 
@@ -136,6 +139,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="createTime != null">create_time = #{createTime},</if>
             <if test="remark != null">remark = #{remark},</if>
             <if test="barCode != null">bar_code = #{barCode},</if>
+            <if test="extendOrderId != null">extend_order_id = #{extendOrderId},</if>
         </trim>
         where order_id = #{orderId}
     </update>