Parcourir la source

积分订单加入商品明细、积分订单对接聚水微信发货

yjwang il y a 1 jour
Parent
commit
6109f346a6

+ 0 - 3
fs-admin/src/main/java/com/fs/FSApplication.java

@@ -1,11 +1,8 @@
 package com.fs;
 
-import com.qiniu.common.Zone;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
-import org.springframework.core.io.Resource;
-import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
 import org.springframework.scheduling.annotation.EnableAsync;
 import org.springframework.scheduling.annotation.EnableScheduling;
 import org.springframework.transaction.annotation.Transactional;

+ 162 - 0
fs-admin/src/main/java/com/fs/hisStore/task/IntegralStoreTask.java

@@ -0,0 +1,162 @@
+package com.fs.hisStore.task;
+
+import com.fs.erp.constant.ErpQueryOrderStatusEnum;
+import com.fs.erp.dto.OrderQueryRequestDTO;
+import com.fs.erp.dto.OrderQueryResponseDTO;
+import com.fs.erp.http.JstErpHttpService;
+import com.fs.his.config.FsSysConfig;
+import com.fs.his.domain.FsIntegralOrder;
+import com.fs.his.mapper.FsIntegralOrderMapper;
+import com.fs.his.service.IFsIntegralOrderService;
+import com.fs.his.utils.ConfigUtil;
+import com.fs.common.utils.StringUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 积分商城定时任务
+ * <p>与 {@link MallStoreTask} 普通商城定时任务隔离,独立处理积分订单的聚水潭发货同步。</p>
+ * <p>Quartz调用目标:integralStoreTask.deliveryOp()</p>
+ *
+ * @author fs
+ */
+@Slf4j
+@Component("integralStoreTask")
+public class IntegralStoreTask {
+
+    @Autowired
+    private FsIntegralOrderMapper fsIntegralOrderMapper;
+
+    @Autowired
+    private IFsIntegralOrderService integralOrderService;
+
+    @Autowired
+    private JstErpHttpService jstErpHttpService;
+
+    @Autowired
+    private ConfigUtil configUtil;
+
+    /**
+     * 聚水潭同步发货(每5分钟执行一次)
+     * <p>流程:
+     * <ol>
+     *   <li>查询待同步订单(status=1待发货、extend_order_id非空、delivery_sn为空)</li>
+     *   <li>按extend_order_id批量查询聚水潭订单状态(20个一组)</li>
+     *   <li>已发货(Sent)的订单回写快递单号、快递公司、发货时间,状态→2待收货</li>
+     *   <li>订阅物流回调 + 微信小程序发货同步</li>
+     *   <li>批量同步溯源码和批次号</li>
+     * </ol>
+     */
+    public void deliveryOp() {
+        // 1. 校验ERP配置(必须是聚水潭)
+        FsSysConfig erpConfig = configUtil.getSysConfig();
+        if (erpConfig.getErpOpen() == null || erpConfig.getErpOpen() != 1
+                || erpConfig.getErpType() == null || erpConfig.getErpType() != 5) {
+            return;
+        }
+
+        // 2. 查询待同步发货的积分订单
+        List<FsIntegralOrder> list = fsIntegralOrderMapper.selectUpdateExpress();
+        if (list == null || list.isEmpty()) {
+            return;
+        }
+
+        // 3. 收集extendOrderId,建立oId→订单映射
+        List<String> oIdStrList = new ArrayList<>();
+        Map<String, FsIntegralOrder> orderMap = new HashMap<>();
+        for (FsIntegralOrder order : list) {
+            String extendOrderId = order.getExtendOrderId();
+            if (StringUtils.isNotEmpty(extendOrderId)) {
+                try {
+                    Long.valueOf(extendOrderId);
+                    oIdStrList.add(extendOrderId);
+                    orderMap.put(extendOrderId, order);
+                } catch (NumberFormatException e) {
+                    log.warn("积分订单extendOrderId无法转为Long,跳过 orderId={}, extendOrderId={}",
+                            order.getOrderId(), extendOrderId);
+                }
+            }
+        }
+        if (oIdStrList.isEmpty()) {
+            return;
+        }
+
+        // 4. 分批查询聚水潭订单(oIds最多20条)
+        int batchSize = 20;
+        List<FsIntegralOrder> deliveredOrders = new ArrayList<>();
+
+        for (int i = 0; i < oIdStrList.size(); i += batchSize) {
+            List<String> batchStr = oIdStrList.subList(i, Math.min(i + batchSize, oIdStrList.size()));
+            List<Long> batchOIds = new ArrayList<>();
+            for (String s : batchStr) {
+                batchOIds.add(Long.valueOf(s));
+            }
+
+            OrderQueryRequestDTO requestDTO = new OrderQueryRequestDTO();
+            requestDTO.setOIds(batchOIds);
+
+            OrderQueryResponseDTO response;
+            try {
+                response = jstErpHttpService.query(requestDTO);
+            } catch (Exception e) {
+                log.error("查询聚水潭订单失败 batchOIds={}", batchOIds, e);
+                continue;
+            }
+
+            if (response == null || response.getOrders() == null || response.getOrders().isEmpty()) {
+                continue;
+            }
+
+            // 5. 遍历聚水潭订单,处理已发货的
+            for (OrderQueryResponseDTO.Order jstOrder : response.getOrders()) {
+                if (jstOrder.getOId() == null) {
+                    continue;
+                }
+                String oIdStr = String.valueOf(jstOrder.getOId());
+                FsIntegralOrder matchedOrder = orderMap.get(oIdStr);
+                if (matchedOrder == null) {
+                    continue;
+                }
+
+                // 仅处理已发货(Sent)状态
+                if (!ErpQueryOrderStatusEnum.SENT.getCode().equals(jstOrder.getStatus())) {
+                    continue;
+                }
+
+                // 聚水潭物流信息:lId=快递单号, lcId=物流公司编码, logisticsCompany=物流公司名称
+                String trackingNo = jstOrder.getLId();
+                String expressCode = jstOrder.getLcId();
+                String expressName = jstOrder.getLogisticsCompany();
+
+                if (StringUtils.isEmpty(trackingNo)) {
+                    log.info("聚水潭订单已发货但无快递单号,跳过 oId={}, orderCode={}", oIdStr, matchedOrder.getOrderCode());
+                    continue;
+                }
+
+                integralOrderService.deliveryOrderBySync(matchedOrder, trackingNo, expressCode, expressName);
+                deliveredOrders.add(matchedOrder);
+            }
+
+            // 批次间停顿200ms,避免触发聚水潭频率限制
+            if (i + batchSize < oIdStrList.size()) {
+                try {
+                    Thread.sleep(200);
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                }
+            }
+        }
+
+        // 6. 批量同步溯源码和批次号
+        if (!deliveredOrders.isEmpty()) {
+            integralOrderService.syncSkuSnFromJstBatch(deliveredOrders);
+            log.info("积分订单聚水潭同步发货完成,本次同步{}笔订单", deliveredOrders.size());
+        }
+    }
+}

+ 62 - 0
fs-service/src/main/java/com/fs/his/domain/FsIntegralOrderItem.java

@@ -0,0 +1,62 @@
+package com.fs.his.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 积分订单商品明细对象 fs_integral_order_item
+ * <p>承接聚水潭出库单回写的溯源码/批次号,与主表 fs_integral_order 的 item_json 快照职责分离:
+ * item_json 用于列表/详情只读展示,明细表用于行级更新(溯源码、批次号)。</p>
+ *
+ * @author fs
+ * @date 2026-07-29
+ */
+@Data
+public class FsIntegralOrderItem extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 明细ID */
+    private Long itemId;
+
+    /** 订单ID */
+    private Long orderId;
+
+    /** 订单编号 */
+    private String orderCode;
+
+    /** 积分商品ID */
+    private Long goodsId;
+
+    /** 商品名称 */
+    private String goodsName;
+
+    /** 商品编号(聚水潭 sku_id 匹配键) */
+    private String barCode;
+
+    /** 购买数量 */
+    private Integer num;
+
+    /** 支付金额 */
+    private BigDecimal cash;
+
+    /** 是否药品 0否 1是(下单时从商品快照冗余) */
+    private Integer isDrug;
+
+    /** 溯源码(多个逗号分隔,聚水潭出库单回写) */
+    private String verifyCode;
+
+    /** 批次号(多个逗号分隔,聚水潭出库单回写) */
+    private String batchNumber;
+
+    /** 商品快照JSON(图/原价/分类等) */
+    private String jsonInfo;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+}

+ 49 - 0
fs-service/src/main/java/com/fs/his/mapper/FsIntegralOrderItemMapper.java

@@ -0,0 +1,49 @@
+package com.fs.his.mapper;
+
+import com.fs.his.domain.FsIntegralOrderItem;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 积分订单商品明细Mapper接口
+ *
+ * @author fs
+ * @date 2026-07-29
+ */
+public interface FsIntegralOrderItemMapper
+{
+    /**
+     * 批量插入积分订单商品明细
+     *
+     * @param items 明细集合
+     * @return 结果
+     */
+    public int batchInsert(List<FsIntegralOrderItem> items);
+
+    /**
+     * 按订单ID查询明细列表(用于详情)
+     *
+     * @param orderId 订单ID
+     * @return 明细集合
+     */
+    public List<FsIntegralOrderItem> selectListByOrderId(Long orderId);
+
+    /**
+     * 按订单ID集合批量查询明细(用于列表批量回填)
+     *
+     * @param orderIds 订单ID集合
+     * @return 明细集合
+     */
+    public List<FsIntegralOrderItem> selectListByOrderIds(List<Long> orderIds);
+
+    /**
+     * 聚水潭同步时更新溯源码和批次号
+     *
+     * @param itemId      明细ID
+     * @param verifyCode  溯源码
+     * @param batchNumber 批次号
+     * @return 结果
+     */
+    public int updateVerifyCodeAndBatch(@Param("itemId") Long itemId, @Param("verifyCode") String verifyCode, @Param("batchNumber") String batchNumber);
+}

+ 9 - 0
fs-service/src/main/java/com/fs/his/mapper/FsIntegralOrderMapper.java

@@ -107,6 +107,15 @@ public interface FsIntegralOrderMapper
     @Select("select fio.*,fu.nick_name,fu.phone from fs_integral_order fio left join fs_user fu on fu.user_id = fio.user_id where order_id=#{orderId} ")
     FsIntegralOrderPVO selectFsIntegralOrderPVO(Long orderId);
 
+    /**
+     * 查询待同步聚水潭发货信息的积分订单
+     * <p>条件:status=1(待发货)、extend_order_id非空(已推送聚水潭)、delivery_sn为空(未回写快递单号)</p>
+     *
+     * @return 待同步订单集合
+     */
+    @Select("select * from fs_integral_order where status = 1 and extend_order_id is not null and extend_order_id != '' and delivery_sn is null")
+    List<FsIntegralOrder> selectUpdateExpress();
+
     /**
      * 取消订单(status → -3 已取消)
      * <p>用户主动取消未支付订单,或管理员主动取消未发货订单。</p>

+ 2 - 0
fs-service/src/main/java/com/fs/his/mapper/FsStorePaymentMapper.java

@@ -110,6 +110,8 @@ public interface FsStorePaymentMapper
     FsStorePaymentVO selectFsStorePaymentByPaymentIdVO(Long paymentId);
     @Select("SELECT * FROM fs_store_payment where business_id=#{orderId} and status=1 and business_type=2")
     List<FsStorePayment> selectFsStorePaymentByOrderId(Long orderId);
+    @Select("SELECT * FROM fs_store_payment where business_id=#{orderId} and status=1 and business_type=6")
+    List<FsStorePayment> selectFsStorePaymentByIntegralOrderId(Long orderId);
     @Select("SELECT * FROM fs_store_payment where business_id=#{orderId} and status=1 and business_type=1")
     List<FsStorePayment> selectFsStorePaymentByInquiryOrderId(Long orderId);
     @Select("SELECT * FROM fs_store_payment where business_id=#{orderId} and status=1 and business_type=3")

+ 48 - 0
fs-service/src/main/java/com/fs/his/service/IFsIntegralOrderItemService.java

@@ -0,0 +1,48 @@
+package com.fs.his.service;
+
+import com.fs.his.domain.FsIntegralOrderItem;
+
+import java.util.List;
+
+/**
+ * 积分订单商品明细Service接口
+ *
+ * @author fs
+ * @date 2026-07-29
+ */
+public interface IFsIntegralOrderItemService
+{
+    /**
+     * 批量插入积分订单商品明细
+     *
+     * @param items 明细集合
+     * @return 结果
+     */
+    int batchInsert(List<FsIntegralOrderItem> items);
+
+    /**
+     * 按订单ID查询明细列表(用于详情)
+     *
+     * @param orderId 订单ID
+     * @return 明细集合
+     */
+    List<FsIntegralOrderItem> selectListByOrderId(Long orderId);
+
+    /**
+     * 按订单ID集合批量查询明细(用于列表批量回填)
+     *
+     * @param orderIds 订单ID集合
+     * @return 明细集合
+     */
+    List<FsIntegralOrderItem> selectListByOrderIds(List<Long> orderIds);
+
+    /**
+     * 聚水潭同步时更新溯源码和批次号
+     *
+     * @param itemId      明细ID
+     * @param verifyCode  溯源码
+     * @param batchNumber 批次号
+     * @return 结果
+     */
+    int updateVerifyCodeAndBatch(Long itemId, String verifyCode, String batchNumber);
+}

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

@@ -178,4 +178,24 @@ public interface IFsIntegralOrderService
      * @return 推送结果,成功返回R.ok,失败返回R.error
      */
     R createOmsOrder(Long orderId);
+
+    /**
+     * 聚水潭同步发货(单订单回写物流信息)
+     * <p>由 IntegralStoreTask.deliveryOp 调用,将聚水潭查询到的快递信息回写到积分订单。</p>
+     * <p>流程:更新订单状态为待收货(2) → 订阅物流回调 → 微信小程序发货同步。</p>
+     *
+     * @param order        积分订单
+     * @param trackingNo   快递单号(聚水潭 lId / mail_no)
+     * @param expressCode  快递公司OMS编码(聚水潭 lcId)
+     * @param expressName  快递公司名称(聚水潭 logisticsCompany)
+     */
+    void deliveryOrderBySync(FsIntegralOrder order, String trackingNo, String expressCode, String expressName);
+
+    /**
+     * 批量同步聚水潭溯源码和批次号
+     * <p>查询聚水潭销售出库单,按 sku_id(即 barCode)匹配积分订单明细,回写 verifyCode 和 batchNumber。</p>
+     *
+     * @param orders 已发货的积分订单集合
+     */
+    void syncSkuSnFromJstBatch(List<FsIntegralOrder> orders);
 }

+ 10 - 1
fs-service/src/main/java/com/fs/his/service/impl/FsIntegralAfterSalesServiceImpl.java

@@ -16,6 +16,7 @@ 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.FsIntegralOrderItemMapper;
 import com.fs.his.mapper.FsIntegralOrderMapper;
 import com.fs.his.param.FsIntegralAfterSalesApplyParam;
 import com.fs.his.param.FsIntegralAfterSalesQueryParam;
@@ -55,6 +56,9 @@ public class FsIntegralAfterSalesServiceImpl implements IFsIntegralAfterSalesSer
     @Autowired
     private FsIntegralOrderMapper fsIntegralOrderMapper;
 
+    @Autowired
+    private FsIntegralOrderItemMapper fsIntegralOrderItemMapper;
+
     @Autowired
     private ISysConfigService configService;
 
@@ -180,6 +184,7 @@ public class FsIntegralAfterSalesServiceImpl implements IFsIntegralAfterSalesSer
 
     /**
      * 查询积分订单售后单详情(含关联用户、企业、积分订单信息)
+     * <p>同时填充订单商品明细(含溯源码/批次号/是否药品),供详情页展示。</p>
      *
      * @param id 售后单主键
      * @return 售后单VO
@@ -187,7 +192,11 @@ public class FsIntegralAfterSalesServiceImpl implements IFsIntegralAfterSalesSer
     @Override
     public FsIntegralAfterSalesVO selectFsIntegralAfterSalesVOById(Long id)
     {
-        return fsIntegralAfterSalesMapper.selectFsIntegralAfterSalesVOById(id);
+        FsIntegralAfterSalesVO vo = fsIntegralAfterSalesMapper.selectFsIntegralAfterSalesVOById(id);
+        if (vo != null && vo.getOrderId() != null) {
+            vo.setItemList(fsIntegralOrderItemMapper.selectListByOrderId(vo.getOrderId()));
+        }
+        return vo;
     }
 
     /**

+ 72 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsIntegralOrderItemServiceImpl.java

@@ -0,0 +1,72 @@
+package com.fs.his.service.impl;
+
+import com.fs.his.domain.FsIntegralOrderItem;
+import com.fs.his.mapper.FsIntegralOrderItemMapper;
+import com.fs.his.service.IFsIntegralOrderItemService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 积分订单商品明细Service实现
+ *
+ * @author fs
+ * @date 2026-07-29
+ */
+@Service
+public class FsIntegralOrderItemServiceImpl implements IFsIntegralOrderItemService
+{
+    @Autowired
+    private FsIntegralOrderItemMapper fsIntegralOrderItemMapper;
+
+    /**
+     * 批量插入积分订单商品明细
+     *
+     * @param items 明细集合
+     * @return 结果
+     */
+    @Override
+    public int batchInsert(List<FsIntegralOrderItem> items)
+    {
+        return fsIntegralOrderItemMapper.batchInsert(items);
+    }
+
+    /**
+     * 按订单ID查询明细列表(用于详情)
+     *
+     * @param orderId 订单ID
+     * @return 明细集合
+     */
+    @Override
+    public List<FsIntegralOrderItem> selectListByOrderId(Long orderId)
+    {
+        return fsIntegralOrderItemMapper.selectListByOrderId(orderId);
+    }
+
+    /**
+     * 按订单ID集合批量查询明细(用于列表批量回填)
+     *
+     * @param orderIds 订单ID集合
+     * @return 明细集合
+     */
+    @Override
+    public List<FsIntegralOrderItem> selectListByOrderIds(List<Long> orderIds)
+    {
+        return fsIntegralOrderItemMapper.selectListByOrderIds(orderIds);
+    }
+
+    /**
+     * 聚水潭同步时更新溯源码和批次号
+     *
+     * @param itemId      明细ID
+     * @param verifyCode  溯源码
+     * @param batchNumber 批次号
+     * @return 结果
+     */
+    @Override
+    public int updateVerifyCodeAndBatch(Long itemId, String verifyCode, String batchNumber)
+    {
+        return fsIntegralOrderItemMapper.updateVerifyCodeAndBatch(itemId, verifyCode, batchNumber);
+    }
+}

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

@@ -1,5 +1,7 @@
 package com.fs.his.service.impl;
 
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.bean.shop.request.shipping.*;
 import cn.hutool.core.date.DateTime;
 import cn.hutool.core.lang.TypeReference;
 import cn.hutool.core.util.IdUtil;
@@ -22,7 +24,9 @@ import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.mapper.CompanyUserMapper;
+import com.fs.core.config.WxMaConfiguration;
 import com.fs.core.utils.OrderCodeUtils;
+import com.fs.erp.constant.ErpQueryOrderStatusEnum;
 import com.fs.erp.constant.OrderStatusEnum;
 import com.fs.erp.dto.*;
 import com.fs.erp.http.JstErpHttpService;
@@ -34,6 +38,7 @@ import com.fs.his.enums.FsUserIntegralLogTypeEnum;
 import com.fs.his.enums.PaymentMethodEnum;
 import com.fs.his.mapper.*;
 import com.fs.his.param.*;
+import com.fs.his.service.IFsExpressService;
 import com.fs.his.service.IFsIntegralCartService;
 import com.fs.his.service.IFsIntegralOrderService;
 import com.fs.his.service.IFsStorePaymentService;
@@ -48,6 +53,7 @@ import com.fs.his.vo.FsStoreProductDeliverExcelVO;
 import com.fs.hisStore.domain.FsShippingTemplatesFreeScrm;
 import com.fs.hisStore.domain.FsShippingTemplatesRegionScrm;
 import com.fs.hisStore.domain.FsShippingTemplatesScrm;
+import com.fs.hisStore.enums.ShipperCodeEnum;
 import com.fs.hisStore.enums.ShippingTempEnum;
 import com.fs.hisStore.mapper.FsShippingTemplatesFreeScrmMapper;
 import com.fs.hisStore.mapper.FsShippingTemplatesRegionScrmMapper;
@@ -74,6 +80,9 @@ import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.text.SimpleDateFormat;
 import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
@@ -106,6 +115,9 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
     @Autowired
     private FsIntegralOrderMapper fsIntegralOrderMapper;
 
+    @Autowired
+    private FsIntegralOrderItemMapper fsIntegralOrderItemMapper;
+
     @Autowired
     private FsUserAddressMapper fsUserAddressMapper;
 
@@ -179,6 +191,12 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
     @Autowired
     private JstErpHttpService jstErpHttpService;
 
+    /**
+     * 快递公司服务(物流回调订阅、本地快递编码映射)
+     */
+    @Autowired
+    private IFsExpressService expressService;
+
     /**
      * 查询积分商品订单
      *
@@ -399,6 +417,10 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
         }
 
         if(fsIntegralOrderMapper.insertFsIntegralOrder(order)>0){
+            // 批量写入订单商品明细表(与主订单同事务,承接聚水潭回写的溯源码/批次号)
+            List<FsIntegralOrderItem> orderItems = buildOrderItems(order, goodsItem);
+            fsIntegralOrderItemMapper.batchInsert(orderItems);
+
             if (order.getPayType() != 2) {
                 // 原子扣减用户积分(SQL 层 integral = integral - ? where integral >= ?,并发安全)
                 // 参考 ylrz 商城订单 FsStoreOrderScrmServiceImpl.decIntegral 规范实现
@@ -442,6 +464,36 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
         }
     }
 
+    /**
+     * 构建积分订单商品明细列表
+     * <p>从下单时的 goodsItem(List<FsIntegralGoods>)映射为明细实体,
+     * 冗余 isDrug 字段,避免后续商品被修改影响历史订单的药品属性;
+     * 商品快照 jsonInfo 存入图/原价/分类等只读展示字段。</p>
+     *
+     * @param order     积分订单(已含 orderId/orderCode)
+     * @param goodsItem 下单商品集合
+     * @return 明细实体集合
+     */
+    private List<FsIntegralOrderItem> buildOrderItems(FsIntegralOrder order, List<FsIntegralGoods> goodsItem) {
+        List<FsIntegralOrderItem> items = new ArrayList<>();
+        Date now = new Date();
+        for (FsIntegralGoods g : goodsItem) {
+            FsIntegralOrderItem item = new FsIntegralOrderItem();
+            item.setOrderId(order.getOrderId());
+            item.setOrderCode(order.getOrderCode());
+            item.setGoodsId(g.getGoodsId());
+            item.setGoodsName(g.getGoodsName());
+            item.setBarCode(g.getBarCode());
+            item.setNum(g.getNum() != null ? g.getNum() : 1);
+            item.setCash(g.getCash());
+            item.setIsDrug(g.getIsDrug() != null ? g.getIsDrug() : 0);
+            item.setJsonInfo(JSONUtil.toJsonStr(g));
+            item.setCreateTime(now);
+            items.add(item);
+        }
+        return items;
+    }
+
     /**
      * 获取用户已购买指定积分商品的有效订单数(限购校验用)
      * <p>缓存策略:优先查 Redis,未命中则回源数据库并回填缓存(1小时过期)。
@@ -607,7 +659,12 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
 
     @Override
     public FsIntegralOrderPVO selectFsIntegralOrderPVO(Long orderId) {
-        return fsIntegralOrderMapper.selectFsIntegralOrderPVO(orderId);
+        FsIntegralOrderPVO pvo = fsIntegralOrderMapper.selectFsIntegralOrderPVO(orderId);
+        if (pvo != null) {
+            // 填充订单商品明细(含溯源码/批次号/是否药品)
+            pvo.setItemList(fsIntegralOrderItemMapper.selectListByOrderId(orderId));
+        }
+        return pvo;
     }
 
     /**
@@ -1764,4 +1821,316 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
             default: return "积分兑换";
         }
     }
+
+    /**
+     * 聚水潭同步发货(单订单回写物流信息)
+     * <p>流程:更新订单状态为待收货(2) → 订阅物流回调 → 微信小程序发货同步。</p>
+     * <p>与普通订单({@link com.fs.hisStore.service.impl.FsStoreOrderScrmServiceImpl#deliveryOrder})逻辑对齐,
+     * 区别:积分订单 deliveryCode=快递公司编码、deliverySn=快递单号、无订单状态日志表、无模板消息。</p>
+     *
+     * @param order        积分订单
+     * @param trackingNo   快递单号(聚水潭 lId / mail_no)
+     * @param expressCode  快递公司OMS编码(聚水潭 lcId)
+     * @param expressName  快递公司名称(聚水潭 logisticsCompany)
+     */
+    @Override
+    public void deliveryOrderBySync(FsIntegralOrder order, String trackingNo, String expressCode, String expressName) {
+        try {
+            // 1. 通过OMS编码映射本地快递公司编码
+            String localExpressCode = expressCode;
+            String localExpressName = expressName;
+            FsExpress express = expressService.selectFsExpressByOmsCode(expressCode);
+            if (express != null) {
+                localExpressCode = express.getCode();
+                if (StringUtils.isNotEmpty(express.getName())) {
+                    localExpressName = express.getName();
+                }
+            }
+
+            // 2. 回写订单物流信息,状态 → 2 待收货
+            FsIntegralOrder updateOrder = new FsIntegralOrder();
+            updateOrder.setOrderId(order.getOrderId());
+            updateOrder.setStatus(2);
+            updateOrder.setDeliveryCode(localExpressCode);
+            updateOrder.setDeliveryName(localExpressName);
+            updateOrder.setDeliverySn(trackingNo);
+            updateOrder.setDeliveryTime(DateUtils.getNowDate());
+            fsIntegralOrderMapper.updateFsIntegralOrder(updateOrder);
+            log.info("聚水潭同步发货回写成功 orderId={}, orderCode={}, trackingNo={}, expressCode={}",
+                    order.getOrderId(), order.getOrderCode(), trackingNo, localExpressCode);
+
+            // 3. 订阅物流回调(顺丰需要校验手机号后四位)
+            String lastFourNumber = "";
+            if (ShipperCodeEnum.SF.getValue().equals(localExpressCode)) {
+                lastFourNumber = order.getUserPhone();
+                if (StringUtils.isNotEmpty(lastFourNumber) && lastFourNumber.length() == 11) {
+                    lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
+                }
+            }
+            try {
+                expressService.subscribeEspress(order.getOrderCode(), localExpressCode, trackingNo, lastFourNumber);
+            } catch (Exception e) {
+                log.warn("订阅物流回调异常 orderCode={}, trackingNo={}", order.getOrderCode(), trackingNo, e);
+            }
+
+            // 4. 微信小程序发货同步(线上发货)
+            syncWeChatShippingOnline(order, trackingNo, localExpressCode);
+        } catch (Exception e) {
+            log.error("聚水潭同步发货异常 orderId={}, orderCode={}", order.getOrderId(), order.getOrderCode(), e);
+        }
+    }
+
+    /**
+     * 同步微信发货(线上发货,logisticsType=1)
+     * <p>复用 fs_store_payment 中的 appId/openId/bankTransactionId 调用微信小程序发货信息同步接口。</p>
+     *
+     * @param order        积分订单
+     * @param trackingNo   快递单号
+     * @param expressCode  本地快递公司编码
+     */
+    private void syncWeChatShippingOnline(FsIntegralOrder order, String trackingNo, String expressCode) {
+        try {
+            List<FsStorePayment> payments = fsStorePaymentMapper.selectFsStorePaymentByIntegralOrderId(order.getOrderId());
+            FsStorePayment payment = null;
+            if (payments != null && !payments.isEmpty()) {
+                for (FsStorePayment p : payments) {
+                    if (p.getStatus() != null && p.getStatus() == 1
+                            && StringUtils.isNotEmpty(p.getBankTransactionId())
+                            && StringUtils.isNotEmpty(p.getOpenId())
+                            && StringUtils.isNotEmpty(p.getAppId())) {
+                        payment = p;
+                        break;
+                    }
+                }
+            }
+            if (payment == null) {
+                log.info("微信发货同步跳过,未找到有效支付记录 orderCode={}", order.getOrderCode());
+                return;
+            }
+
+            // 取首个商品名称作为物品描述
+            String itemDesc = "积分商品";
+            if (StringUtils.isNotEmpty(order.getItemJson())) {
+                try {
+                    cn.hutool.json.JSONArray itemArray = JSONUtil.parseArray(order.getItemJson());
+                    if (!itemArray.isEmpty()) {
+                        String goodsName = itemArray.getJSONObject(0).getStr("goodsName");
+                        if (StringUtils.isNotEmpty(goodsName)) {
+                            itemDesc = goodsName;
+                        }
+                    }
+                } catch (Exception ignore) {
+                }
+            }
+
+            String uploadTime = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"))
+                    .format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
+            WxMaService wxService = WxMaConfiguration.getMaService(payment.getAppId());
+            uploadSingleShippingInfoOnline(wxService, payment, order, trackingNo, expressCode, itemDesc, uploadTime);
+        } catch (Exception e) {
+            log.error("微信发货同步异常 orderCode={}", order.getOrderCode(), e);
+        }
+    }
+
+    /**
+     * 上传单个订单物流信息到微信(线上发货,logisticsType=1)
+     */
+    private boolean uploadSingleShippingInfoOnline(WxMaService wxService,
+                                                   FsStorePayment payment,
+                                                   FsIntegralOrder order,
+                                                   String trackingNo,
+                                                   String expressCode,
+                                                   String itemDesc,
+                                                   String uploadTime) {
+        try {
+            WxMaOrderShippingInfoUploadRequest request = new WxMaOrderShippingInfoUploadRequest();
+            OrderKeyBean orderKeyBean = new OrderKeyBean();
+            orderKeyBean.setOrderNumberType(2);
+            orderKeyBean.setTransactionId(payment.getBankTransactionId());
+            request.setOrderKey(orderKeyBean);
+            request.setDeliveryMode(1);
+            request.setLogisticsType(1);
+
+            List<ShippingListBean> shippingList = new ArrayList<>();
+            ShippingListBean shippingListBean = new ShippingListBean();
+            shippingListBean.setTrackingNo(trackingNo);
+            shippingListBean.setExpressCompany(expressCode);
+            shippingListBean.setItemDesc(StringUtils.isEmpty(itemDesc) ? "积分商品" : itemDesc);
+            ContactBean contactBean = new ContactBean();
+            contactBean.setReceiverContact(order.getUserPhone());
+            shippingListBean.setContact(contactBean);
+            shippingList.add(shippingListBean);
+            request.setShippingList(shippingList);
+            request.setUploadTime(uploadTime);
+
+            PayerBean payerBean = new PayerBean();
+            payerBean.setOpenid(payment.getOpenId());
+            request.setPayer(payerBean);
+
+            boolean success = wxService.getWxMaOrderShippingService().upload(request).getErrCode() == 0;
+            if (!success) {
+                log.warn("上传物流信息到微信返回非0 orderCode={}", order.getOrderCode());
+            }
+            return success;
+        } catch (Exception e) {
+            log.error("上传物流信息到微信失败 orderCode={}", order.getOrderCode(), e);
+            return false;
+        }
+    }
+
+    /**
+     * 批量同步聚水潭溯源码和批次号
+     * <p>查询聚水潭销售出库单,按 sku_id(即 barCode)匹配积分订单明细,回写 verifyCode 和 batchNumber。</p>
+     * <p>与普通订单({@link com.fs.hisStore.service.impl.FsStoreOrderScrmServiceImpl#syncSkuSnFromJstBatch})逻辑对齐。</p>
+     *
+     * @param orders 已发货的积分订单集合
+     */
+    @Override
+    public void syncSkuSnFromJstBatch(List<FsIntegralOrder> orders) {
+        try {
+            FsSysConfig erpConfig = configUtil.getSysConfig();
+            if (erpConfig.getErpOpen() == null || erpConfig.getErpOpen() != 1
+                    || erpConfig.getErpType() == null || erpConfig.getErpType() != 5) {
+                return;
+            }
+
+            // 过滤有效订单(必须有extendOrderId)
+            List<FsIntegralOrder> validOrders = new ArrayList<>();
+            for (FsIntegralOrder order : orders) {
+                if (StringUtils.isNotEmpty(order.getExtendOrderId())) {
+                    validOrders.add(order);
+                }
+            }
+            if (validOrders.isEmpty()) {
+                return;
+            }
+
+            // 批量查询所有订单明细
+            List<Long> orderIds = new ArrayList<>();
+            for (FsIntegralOrder order : validOrders) {
+                orderIds.add(order.getOrderId());
+            }
+            List<FsIntegralOrderItem> allItems = fsIntegralOrderItemMapper.selectListByOrderIds(orderIds);
+            Map<Long, List<FsIntegralOrderItem>> orderItemsMap = new HashMap<>();
+            if (allItems != null) {
+                for (FsIntegralOrderItem item : allItems) {
+                    orderItemsMap.computeIfAbsent(item.getOrderId(), k -> new ArrayList<>()).add(item);
+                }
+            }
+
+            // 收集所有extendOrderId,按最多50个一组分批查询销售出库单
+            List<String> allOIds = new ArrayList<>();
+            Map<String, FsIntegralOrder> extendOrderMap = new HashMap<>();
+            for (FsIntegralOrder order : validOrders) {
+                allOIds.add(order.getExtendOrderId());
+                extendOrderMap.put(order.getExtendOrderId(), order);
+            }
+
+            // 分批查询出库单(o_ids最多50条)
+            int batchSize = 50;
+            List<OutboundOrderQueryResponseDTO.OutboundOrder> allOutboundOrders = new ArrayList<>();
+            for (int i = 0; i < allOIds.size(); i += batchSize) {
+                List<String> batchOIds = allOIds.subList(i, Math.min(i + batchSize, allOIds.size()));
+
+                int pageIndex = 1;
+                while (true) {
+                    OutboundOrderQueryRequestDTO requestDTO = OutboundOrderQueryRequestDTO.builder()
+                            .oIds(batchOIds)
+                            .status("Confirmed")
+                            .pageIndex(pageIndex)
+                            .pageSize(50)
+                            .build();
+                    OutboundOrderQueryResponseDTO response = jstErpHttpService.queryOutboundOrder(requestDTO);
+                    if (response == null || response.getData() == null
+                            || response.getData().getDatas() == null || response.getData().getDatas().isEmpty()) {
+                        break;
+                    }
+                    allOutboundOrders.addAll(response.getData().getDatas());
+                    if (!Boolean.TRUE.equals(response.getData().getHasNext())) {
+                        break;
+                    }
+                    pageIndex++;
+                }
+            }
+
+            if (allOutboundOrders.isEmpty()) {
+                log.info("聚水潭销售出库批量查询无数据,积分订单数: {}", validOrders.size());
+                return;
+            }
+
+            // 按o_id分组出库单
+            Map<String, List<OutboundOrderQueryResponseDTO.OutboundOrder>> outboundByOId = new HashMap<>();
+            for (OutboundOrderQueryResponseDTO.OutboundOrder outOrder : allOutboundOrders) {
+                if (outOrder.getOId() != null) {
+                    String oIdStr = String.valueOf(outOrder.getOId());
+                    outboundByOId.computeIfAbsent(oIdStr, k -> new ArrayList<>()).add(outOrder);
+                }
+            }
+
+            // 遍历每个订单,从出库单中提取溯源码和批次号
+            for (FsIntegralOrder order : validOrders) {
+                List<OutboundOrderQueryResponseDTO.OutboundOrder> matchedOutOrders = outboundByOId.get(order.getExtendOrderId());
+                if (matchedOutOrders == null || matchedOutOrders.isEmpty()) {
+                    continue;
+                }
+
+                List<FsIntegralOrderItem> items = orderItemsMap.get(order.getOrderId());
+                if (items == null || items.isEmpty()) {
+                    continue;
+                }
+
+                // 从出库单中提取: sns[]→溯源码, batchs[]→批次号,按sku_id分组
+                Map<String, StringBuilder> skuSnMap = new HashMap<>();
+                Map<String, StringBuilder> batchMap = new HashMap<>();
+
+                for (OutboundOrderQueryResponseDTO.OutboundOrder outOrder : matchedOutOrders) {
+                    // 提取唯一码(sns)
+                    if (outOrder.getSns() != null) {
+                        for (OutboundOrderQueryResponseDTO.SnInfo snInfo : outOrder.getSns()) {
+                            String skuId = snInfo.getSkuId();
+                            if (StringUtils.isNotEmpty(skuId) && StringUtils.isNotEmpty(snInfo.getSn())) {
+                                skuSnMap.computeIfAbsent(skuId, k -> new StringBuilder());
+                                if (skuSnMap.get(skuId).length() > 0) {
+                                    skuSnMap.get(skuId).append(",");
+                                }
+                                skuSnMap.get(skuId).append(snInfo.getSn());
+                            }
+                        }
+                    }
+                    // 提取批次号(batchs)
+                    if (outOrder.getBatchs() != null) {
+                        for (OutboundOrderQueryResponseDTO.BatchInfo batchInfo : outOrder.getBatchs()) {
+                            String skuId = batchInfo.getSkuId();
+                            String batchNo = batchInfo.getBatchNo();
+                            if (StringUtils.isNotEmpty(skuId) && StringUtils.isNotEmpty(batchNo)) {
+                                batchMap.computeIfAbsent(skuId, k -> new StringBuilder());
+                                if (batchMap.get(skuId).length() > 0 && !batchMap.get(skuId).toString().contains(batchNo)) {
+                                    batchMap.get(skuId).append(",");
+                                    batchMap.get(skuId).append(batchNo);
+                                } else if (batchMap.get(skuId).length() == 0) {
+                                    batchMap.get(skuId).append(batchNo);
+                                }
+                            }
+                        }
+                    }
+                }
+
+                // 通过barCode匹配sku_id,回写溯源码和批次号
+                for (FsIntegralOrderItem item : items) {
+                    String barCode = item.getBarCode();
+                    if (StringUtils.isNotEmpty(barCode)) {
+                        String verifyCode = skuSnMap.containsKey(barCode) ? skuSnMap.get(barCode).toString() : null;
+                        String batchNumber = batchMap.containsKey(barCode) ? batchMap.get(barCode).toString() : null;
+                        if (StringUtils.isNotEmpty(verifyCode) || StringUtils.isNotEmpty(batchNumber)) {
+                            fsIntegralOrderItemMapper.updateVerifyCodeAndBatch(item.getItemId(), verifyCode, batchNumber);
+                            log.info("积分订单明细溯源码同步成功 orderCode={}, barCode={}, verifyCode={}, batchNumber={}",
+                                    order.getOrderCode(), barCode, verifyCode, batchNumber);
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("批量同步聚水潭溯源码失败(积分订单)", e);
+        }
+    }
 }

+ 6 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java

@@ -1450,6 +1450,12 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
             storePayment.setOpenId(getOpenIdForPaymentMethod(user, payOrderParam.getPaymentMethod(), payConfig));
         }
 
+        // 设置appId(优先用参数传入的,为空用支付配置的appId,用于微信发货同步)
+        String appId = StringUtils.isNotBlank(payOrderParam.getAppId()) ? payOrderParam.getAppId() : payConfig.getAppId();
+        if (StringUtils.isNotBlank(appId)) {
+            storePayment.setAppId(appId);
+        }
+
         if (fsStorePaymentMapper.insertFsStorePayment(storePayment) <= 0) {
             throw new CustomException("支付订单创建失败");
         }

+ 5 - 0
fs-service/src/main/java/com/fs/his/vo/FsIntegralAfterSalesVO.java

@@ -3,11 +3,13 @@ package com.fs.his.vo;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
 import com.fs.his.domain.FsIntegralAfterSales;
+import com.fs.his.domain.FsIntegralOrderItem;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 
 import java.math.BigDecimal;
 import java.util.Date;
+import java.util.List;
 
 /**
  * 积分订单售后单展示VO
@@ -49,6 +51,9 @@ public class FsIntegralAfterSalesVO extends FsIntegralAfterSales
     /** 商品信息JSON(商品名称、原价、数量、积分、图片等统一从此字段解析) */
     private String itemJson;
 
+    /** 订单商品明细列表(含溯源码/批次号/是否药品,详情接口填充) */
+    private List<FsIntegralOrderItem> itemList;
+
     /** 收货地址 */
     @Excel(name = "收货地址")
     private String userAddress;

+ 5 - 0
fs-service/src/main/java/com/fs/his/vo/FsIntegralOrderPVO.java

@@ -3,9 +3,11 @@ package com.fs.his.vo;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
+import com.fs.his.domain.FsIntegralOrderItem;
 import lombok.Data;
 
 import java.util.Date;
+import java.util.List;
 
 /**
  * 积分商品订单对象 fs_integral_order
@@ -45,6 +47,9 @@ public class FsIntegralOrderPVO extends BaseEntity
     @Excel(name = "商品信息")
     private String itemJson;
 
+    /** 订单商品明细列表(含溯源码/批次号/是否药品,详情接口填充) */
+    private List<FsIntegralOrderItem> itemList;
+
     /** 支付积分 */
     @Excel(name = "支付积分")
     private String integral;

+ 62 - 0
fs-service/src/main/resources/mapper/his/FsIntegralOrderItemMapper.xml

@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.his.mapper.FsIntegralOrderItemMapper">
+
+    <resultMap type="FsIntegralOrderItem" id="FsIntegralOrderItemResult">
+        <result property="itemId"        column="item_id"        />
+        <result property="orderId"       column="order_id"       />
+        <result property="orderCode"     column="order_code"     />
+        <result property="goodsId"       column="goods_id"       />
+        <result property="goodsName"     column="goods_name"     />
+        <result property="barCode"       column="bar_code"       />
+        <result property="num"           column="num"            />
+        <result property="cash"          column="cash"           />
+        <result property="isDrug"        column="is_drug"        />
+        <result property="verifyCode"    column="verify_code"    />
+        <result property="batchNumber"   column="batch_number"   />
+        <result property="jsonInfo"      column="json_info"      />
+        <result property="createTime"    column="create_time"    />
+    </resultMap>
+
+    <sql id="selectFsIntegralOrderItemVo">
+        select item_id, order_id, order_code, goods_id, goods_name, bar_code,
+               num, cash, is_drug, verify_code, batch_number, json_info, create_time
+        from fs_integral_order_item
+    </sql>
+
+    <insert id="batchInsert" useGeneratedKeys="true" keyProperty="itemId">
+        insert into fs_integral_order_item
+        (order_id, order_code, goods_id, goods_name, bar_code, num, cash, is_drug, verify_code, batch_number, json_info, create_time)
+        values
+        <foreach collection="list" item="item" separator=",">
+            (#{item.orderId}, #{item.orderCode}, #{item.goodsId}, #{item.goodsName}, #{item.barCode},
+             #{item.num}, #{item.cash}, #{item.isDrug}, #{item.verifyCode}, #{item.batchNumber},
+             #{item.jsonInfo}, #{item.createTime})
+        </foreach>
+    </insert>
+
+    <select id="selectListByOrderId" parameterType="Long" resultMap="FsIntegralOrderItemResult">
+        <include refid="selectFsIntegralOrderItemVo"/>
+        where order_id = #{orderId}
+        order by item_id asc
+    </select>
+
+    <select id="selectListByOrderIds" resultMap="FsIntegralOrderItemResult">
+        <include refid="selectFsIntegralOrderItemVo"/>
+        where order_id in
+        <foreach collection="list" item="orderId" open="(" separator="," close=")">
+            #{orderId}
+        </foreach>
+        order by order_id, item_id
+    </select>
+
+    <update id="updateVerifyCodeAndBatch">
+        update fs_integral_order_item
+        set verify_code = #{verifyCode},
+            batch_number = #{batchNumber}
+        where item_id = #{itemId}
+    </update>
+
+</mapper>