Просмотр исходного кода

Merge remote-tracking branch 'origin/ScrmStores' into ScrmStores

yjwang 5 дней назад
Родитель
Сommit
957d721eb9
27 измененных файлов с 1335 добавлено и 50 удалено
  1. 0 3
      fs-admin/src/main/java/com/fs/FSApplication.java
  2. 51 0
      fs-admin/src/main/java/com/fs/his/controller/FsIntegralGoodsController.java
  3. 162 0
      fs-admin/src/main/java/com/fs/hisStore/task/IntegralStoreTask.java
  4. 16 0
      fs-service/src/main/java/com/fs/his/domain/FsIntegralGoods.java
  5. 62 0
      fs-service/src/main/java/com/fs/his/domain/FsIntegralOrderItem.java
  6. 20 2
      fs-service/src/main/java/com/fs/his/mapper/FsIntegralGoodsMapper.java
  7. 49 0
      fs-service/src/main/java/com/fs/his/mapper/FsIntegralOrderItemMapper.java
  8. 21 3
      fs-service/src/main/java/com/fs/his/mapper/FsIntegralOrderMapper.java
  9. 2 0
      fs-service/src/main/java/com/fs/his/mapper/FsStorePaymentMapper.java
  10. 12 0
      fs-service/src/main/java/com/fs/his/service/IFsIntegralGoodsService.java
  11. 48 0
      fs-service/src/main/java/com/fs/his/service/IFsIntegralOrderItemService.java
  12. 20 0
      fs-service/src/main/java/com/fs/his/service/IFsIntegralOrderService.java
  13. 10 1
      fs-service/src/main/java/com/fs/his/service/impl/FsIntegralAfterSalesServiceImpl.java
  14. 83 0
      fs-service/src/main/java/com/fs/his/service/impl/FsIntegralCartServiceImpl.java
  15. 37 0
      fs-service/src/main/java/com/fs/his/service/impl/FsIntegralGoodsServiceImpl.java
  16. 72 0
      fs-service/src/main/java/com/fs/his/service/impl/FsIntegralOrderItemServiceImpl.java
  17. 523 35
      fs-service/src/main/java/com/fs/his/service/impl/FsIntegralOrderServiceImpl.java
  18. 6 0
      fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java
  19. 5 0
      fs-service/src/main/java/com/fs/his/vo/FsIntegralAfterSalesVO.java
  20. 9 0
      fs-service/src/main/java/com/fs/his/vo/FsIntegralGoodsListUVO.java
  21. 16 0
      fs-service/src/main/java/com/fs/his/vo/FsIntegralGoodsListVO.java
  22. 5 0
      fs-service/src/main/java/com/fs/his/vo/FsIntegralOrderPVO.java
  23. 25 1
      fs-service/src/main/resources/mapper/his/FsIntegralGoodsMapper.xml
  24. 62 0
      fs-service/src/main/resources/mapper/his/FsIntegralOrderItemMapper.xml
  25. 11 1
      fs-user-app/src/main/java/com/fs/app/controller/IntegralController.java
  26. 2 2
      fs-user-app/src/main/java/com/fs/app/controller/UserController.java
  27. 6 2
      fs-user-app/src/main/java/com/fs/app/param/FsUserEditParam.java

+ 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;

+ 51 - 0
fs-admin/src/main/java/com/fs/his/controller/FsIntegralGoodsController.java

@@ -20,6 +20,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -134,6 +135,56 @@ public class FsIntegralGoodsController extends BaseController
         return toAjax(fsIntegralGoodsService.deleteFsIntegralGoodsByGoodsIds(goodsIds));
     }
 
+    /**
+     * 批量修改有效期
+     * <p>支持长期有效和非长期有效两种模式,非长期有效需指定开始/结束时间。</p>
+     */
+    @PreAuthorize("@ss.hasPermi('his:integralGoods:edit')")
+    @Log(title = "积分商品", businessType = BusinessType.UPDATE)
+    @PutMapping("/batchUpdateValidPeriod")
+    public AjaxResult batchUpdateValidPeriod(@RequestBody Map<String, Object> params)
+    {
+        Object goodsIdsObj = params.get("goodsIds");
+        if (goodsIdsObj == null) {
+            return AjaxResult.error("请选择需要修改的商品");
+        }
+        List<Integer> idList = (List<Integer>) goodsIdsObj;
+        if (idList.isEmpty()) {
+            return AjaxResult.error("请选择需要修改的商品");
+        }
+        Long[] goodsIds = idList.stream().map(Long::valueOf).toArray(Long[]::new);
+
+        Integer isLongTerm = params.get("isLongTerm") == null ? null : Integer.valueOf(params.get("isLongTerm").toString());
+        Date startTime = parseDate(params.get("validStartTime"));
+        Date endTime = parseDate(params.get("validEndTime"));
+
+        redisCacheUtil.delRedisKey("getIntegralGoodsList");
+        redisCacheUtil.delRedisKey("getIntegralGoodsById");
+        return toAjax(fsIntegralGoodsService.batchUpdateValidPeriod(goodsIds, isLongTerm, startTime, endTime));
+    }
+
+    /**
+     * 解析前端传入的时间参数(支持时间戳和 yyyy-MM-dd HH:mm:ss 格式字符串)
+     */
+    private Date parseDate(Object value) {
+        if (value == null) {
+            return null;
+        }
+        String str = value.toString();
+        if (str.isEmpty()) {
+            return null;
+        }
+        // 纯数字按时间戳处理
+        if (str.matches("\\d+")) {
+            return new Date(Long.parseLong(str));
+        }
+        try {
+            return new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(str);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
     @GetMapping("/getChooseIntegralGoodsList")
     public R getChooseIntegralGoodsList(@RequestParam(required = false) String goodsName,
                                         @RequestParam(required = false) Integer goodsType,

+ 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());
+        }
+    }
+}

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

@@ -1,6 +1,8 @@
 package com.fs.his.domain;
 
 import java.math.BigDecimal;
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
 import lombok.Data;
@@ -74,6 +76,20 @@ public class FsIntegralGoods extends BaseEntity
     @Excel(name = "是否药品", dictType = "sys_yes_no")
     private Integer isDrug;
 
+    /** 是否长期有效 0是 1否(默认0长期有效) */
+    @Excel(name = "是否长期有效")
+    private Integer isLongTerm;
+
+    /** 有效期开始时间(is_long_term=1时生效) */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "有效期开始时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date validStartTime;
+
+    /** 有效期结束时间(is_long_term=1时生效) */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "有效期结束时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date validEndTime;
+
     /**
      * 非数据库字段 仅用于订单信息存储购买数量
      */

+ 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;
+}

+ 20 - 2
fs-service/src/main/java/com/fs/his/mapper/FsIntegralGoodsMapper.java

@@ -11,6 +11,7 @@ import org.apache.ibatis.annotations.Select;
 import org.apache.ibatis.annotations.Update;
 
 import java.util.Collection;
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
 
@@ -70,7 +71,7 @@ public interface FsIntegralGoodsMapper
      */
     public int deleteFsIntegralGoodsByGoodsIds(Long[] goodsIds);
 
-    @Select({"<script> select goods_id,bar_code, img_url, images, goods_name, ot_price, goods_type, status, integral, cash, sort, stock, descs, create_time from fs_integral_goods" +
+    @Select({"<script> select goods_id,bar_code, img_url, images, goods_name, ot_price, goods_type, status, integral, cash, sort, stock, limit_num, is_long_term, valid_start_time, valid_end_time, descs, create_time from fs_integral_goods" +
             "<where>  \n" +
             "            <if test=\"goodsName != null  and goodsName != ''\"> and goods_name like concat('%', #{goodsName}, '%')</if>\n" +
             "            <if test=\"goodsType != null \"> and goods_type = #{goodsType}</if>\n" +
@@ -80,8 +81,11 @@ public interface FsIntegralGoodsMapper
             "</script>"})
     List<FsIntegralGoodsListVO> selectFsIntegralGoodsListVO(FsIntegralGoods fsIntegralGoods);
     @Select({"<script> " +
-            "select g.goods_id,g.img_url,g.images,g.goods_name,g.ot_price,g.goods_type,g.integral,g.cash,g.sort  from fs_integral_goods g  " +
+            "select g.goods_id,g.img_url,g.images,g.goods_name,g.ot_price,g.goods_type,g.integral,g.cash,g.sort," +
+            "g.is_long_term,g.valid_start_time,g.valid_end_time  from fs_integral_goods g  " +
             "where g.status=1  " +
+            "and (g.is_long_term = 0 " +
+            "     or (g.is_long_term = 1 and g.valid_start_time &lt;= NOW() and g.valid_end_time &gt;= NOW())) " +
             "<if test = 'maps.goodsType != null and maps.goodsType != 0     '> " +
             "and g.goods_type = #{maps.goodsType}  " +
             "</if>" +
@@ -111,4 +115,18 @@ public interface FsIntegralGoodsMapper
      * 根据id集合查询积分商品列表
      */
     List<FsGoodsVO> getFsGoodsVOListByIds(@Param("goodsIds") List<Long> goodsIds);
+
+    /**
+     * 批量修改有效期
+     *
+     * @param goodsIds   商品ID集合
+     * @param isLongTerm 是否长期有效 0是 1否
+     * @param startTime  活动开始时间(isLongTerm=1时生效,为空则置null)
+     * @param endTime    活动结束时间(isLongTerm=1时生效,为空则置null)
+     * @return 影响行数
+     */
+    int batchUpdateValidPeriod(@Param("goodsIds") Long[] goodsIds,
+                               @Param("isLongTerm") Integer isLongTerm,
+                               @Param("startTime") Date startTime,
+                               @Param("endTime") Date endTime);
 }

+ 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);
+}

+ 21 - 3
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>
@@ -160,7 +169,15 @@ public interface FsIntegralOrderMapper
 
     /**
      * 统计用户已购买指定积分商品的有效订单数(限购校验用)
-     * <p>排除无效订单:5已取消、-1退款中、-2已退款、-3已取消;通过 JSON 函数精确匹配 item_json 中的 goodsId。</p>
+     * <p>仅排除以下两种终态无效订单:5已取消、-2已退款;
+     * 其余状态(含 -1退款中、-3已取消、1待发货、2待收货、3已完成、4待支付)均计入限购数量,
+     * 避免退款流程进行中或异常取消时被用户绕过限购。</p>
+     * <p>通过 JSON 函数精确匹配 item_json 中的 goodsId,兼容两种格式:
+     * <ul>
+     *   <li>数组格式(新):[{"goodsId":123,...}],使用 '$[*].goodsId' 路径</li>
+     *   <li>单对象格式(旧):{"goodsId":123,...},使用 '$.goodsId' 路径</li>
+     * </ul>
+     * 任一匹配即计入有效订单数。</p>
      *
      * @param userId  用户ID
      * @param goodsId 积分商品ID
@@ -168,7 +185,8 @@ public interface FsIntegralOrderMapper
      */
     @Select("SELECT COUNT(1) FROM fs_integral_order " +
             "WHERE user_id = #{userId} " +
-            "AND status NOT IN (5, -1, -2, -3) " +
-            "AND JSON_CONTAINS(JSON_EXTRACT(item_json, '$[*].goodsId'), CAST(#{goodsId} AS JSON))")
+            "AND status NOT IN (5, -2) " +
+            "AND (JSON_CONTAINS(JSON_EXTRACT(item_json, '$[*].goodsId'), CAST(#{goodsId} AS JSON)) " +
+            "     OR JSON_EXTRACT(item_json, '$.goodsId') = #{goodsId})")
     int selectUserPurchasedCount(@Param("userId") Long userId, @Param("goodsId") Long goodsId);
 }

+ 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")

+ 12 - 0
fs-service/src/main/java/com/fs/his/service/IFsIntegralGoodsService.java

@@ -7,6 +7,7 @@ import com.fs.his.vo.FsIntegralGoodsChooseVO;
 import com.fs.his.vo.FsIntegralGoodsListUVO;
 import com.fs.his.vo.FsIntegralGoodsListVO;
 
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
 
@@ -78,4 +79,15 @@ public interface IFsIntegralGoodsService
      * 获取选择积分商品列表
      */
     List<FsIntegralGoodsChooseVO> getChooseIntegralGoodsListByMap(Map<String, Object> params);
+
+    /**
+     * 批量修改有效期
+     *
+     * @param goodsIds   商品ID集合
+     * @param isLongTerm 是否长期有效 0是 1否
+     * @param startTime  活动开始时间(isLongTerm=1时生效)
+     * @param endTime    活动结束时间(isLongTerm=1时生效)
+     * @return 影响行数
+     */
+    int batchUpdateValidPeriod(Long[] goodsIds, Integer isLongTerm, Date startTime, Date endTime);
 }

+ 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;
     }
 
     /**

+ 83 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsIntegralCartServiceImpl.java

@@ -9,16 +9,19 @@ import com.fs.his.domain.FsIntegralGoods;
 import com.fs.his.domain.FsUser;
 import com.fs.his.mapper.FsIntegralCartMapper;
 import com.fs.his.mapper.FsIntegralGoodsMapper;
+import com.fs.his.mapper.FsIntegralOrderMapper;
 import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.service.IFsIntegralCartService;
 import com.fs.his.vo.FsIntegralCartVO;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
 import javax.validation.constraints.NotNull;
 import java.time.LocalDateTime;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -28,10 +31,22 @@ import java.util.Objects;
 @Service
 public class FsIntegralCartServiceImpl extends ServiceImpl<FsIntegralCartMapper, FsIntegralCart> implements IFsIntegralCartService {
 
+    /**
+     * 限购计数器 Redis Hash Key 前缀(与 FsIntegralOrderServiceImpl 保持一致)
+     */
+    private static final String REDIS_KEY_INTEGRAL_PURCHASE_COUNT = "integral:purchase:count:";
+
     @Resource
     private FsIntegralGoodsMapper goodsMapper;
     @Resource
     private FsUserMapper userMapper;
+    @Resource
+    private FsIntegralOrderMapper orderMapper;
+    /**
+     * 限购计数器专用 RedisTemplate(Hash 结构,只读不写,写操作在 FsIntegralOrderServiceImpl)
+     */
+    @Resource(name = "redisTemplateForInteger")
+    private RedisTemplate<String, Integer> redisTemplateForInteger;
 
     /**
      * 添加或修改购物车
@@ -62,6 +77,29 @@ public class FsIntegralCartServiceImpl extends ServiceImpl<FsIntegralCartMapper,
             throw new CustomException("库存不足");
         }
 
+        // 有效期校验:isLongTerm=1 非长期有效时,校验当前时间是否在有效期区间内
+        if (fsIntegralGoods.getIsLongTerm() != null && fsIntegralGoods.getIsLongTerm() == 1) {
+            Date now = new Date();
+            if (fsIntegralGoods.getValidStartTime() == null || fsIntegralGoods.getValidEndTime() == null
+                    || now.before(fsIntegralGoods.getValidStartTime()) || now.after(fsIntegralGoods.getValidEndTime())) {
+                throw new CustomException("积分商品「" + fsIntegralGoods.getGoodsName() + "」已过兑换有效期限");
+            }
+        }
+
+        // 限购校验:limitNum > 0 表示开启限购,校验"已购订单数 + 购物车已有数量 + 本次加入数量"是否超出限购
+        if (fsIntegralGoods.getLimitNum() != null && fsIntegralGoods.getLimitNum() > 0) {
+            // 已下单的有效订单数(仅排除5已取消、-2已退款),读 Redis 计数器(懒加载+降级查库)
+            int purchasedCount = getPurchasedCount(userId, goodsId);
+            // 购物车中已存在的数量(同用户同商品)
+            Integer existCartNum = getCartCount(userId, goodsId);
+            int existNum = existCartNum == null ? 0 : existCartNum;
+            // isCart=0 表示累加到现有购物车,此时本次 cartNum 为新增量;isCart=1 表示覆盖现有购物车,此时 cartNum 为最终数量
+            int totalNum = isCart != null && isCart == 1 ? cartNum : existNum + cartNum;
+            if (purchasedCount + totalNum > fsIntegralGoods.getLimitNum()) {
+                throw new CustomException("超过限购数量,每人限购" + fsIntegralGoods.getLimitNum() + "件");
+            }
+        }
+
         FsIntegralCart cart = new FsIntegralCart();
         cart.setUserId(userId);
         cart.setGoodsId(goodsId);
@@ -88,6 +126,51 @@ public class FsIntegralCartServiceImpl extends ServiceImpl<FsIntegralCartMapper,
         baseMapper.delete(wrapper);
     }
 
+    /**
+     * 查询用户购物车中指定商品的已有数量(限购校验用)
+     * @param userId    用户ID
+     * @param goodsId   商品ID
+     * @return 数量(不存在返回 null)
+     */
+    private Integer getCartCount(Long userId, Long goodsId) {
+        FsIntegralCart cart = baseMapper.selectOne(Wrappers.<FsIntegralCart>lambdaQuery()
+                .eq(FsIntegralCart::getUserId, userId)
+                .eq(FsIntegralCart::getGoodsId, goodsId));
+        return cart == null ? null : cart.getCartNum();
+    }
+
+    /**
+     * 获取用户已购买指定积分商品的有效订单数(限购校验用,Redis 计数器方案)
+     * <p>读取 Redis Hash(key=integral:purchase:count:{userId}, hashKey={goodsId}),
+     * 未命中时从数据库懒加载并回填,Redis 不可用时降级查库。
+     * 与 FsIntegralOrderServiceImpl.getPurchasedCount 逻辑保持一致,仅读不写。</p>
+     *
+     * @param userId  用户ID
+     * @param goodsId 积分商品ID
+     * @return 已购买有效订单数(非负)
+     */
+    private int getPurchasedCount(Long userId, Long goodsId) {
+        if (userId == null || goodsId == null) {
+            return 0;
+        }
+        String redisKey = REDIS_KEY_INTEGRAL_PURCHASE_COUNT + userId;
+        String hashKey = goodsId.toString();
+        try {
+            Integer count = (Integer) redisTemplateForInteger.opsForHash().get(redisKey, hashKey);
+            if (count != null) {
+                return count;
+            }
+            // 懒加载:Redis 无记录时从 DB 初始化
+            int dbCount = orderMapper.selectUserPurchasedCount(userId, goodsId);
+            redisTemplateForInteger.opsForHash().put(redisKey, hashKey, dbCount);
+            return dbCount;
+        } catch (Exception e) {
+            // Redis 异常降级查库,保证限购功能可用
+            log.warn("限购计数器读取Redis失败,降级查DB userId={}, goodsId={}", userId, goodsId, e);
+            return orderMapper.selectUserPurchasedCount(userId, goodsId);
+        }
+    }
+
     /**
      * 获取购物车商品数量
      * @param params    参数

+ 37 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsIntegralGoodsServiceImpl.java

@@ -75,6 +75,10 @@ public class FsIntegralGoodsServiceImpl implements IFsIntegralGoodsService
     public int insertFsIntegralGoods(FsIntegralGoods fsIntegralGoods)
     {
         fsIntegralGoods.setCreateTime(DateUtils.getNowDate());
+        // 默认长期有效
+        if (fsIntegralGoods.getIsLongTerm() == null) {
+            fsIntegralGoods.setIsLongTerm(0);
+        }
         return fsIntegralGoodsMapper.insertFsIntegralGoods(fsIntegralGoods);
     }
 
@@ -256,4 +260,37 @@ public class FsIntegralGoodsServiceImpl implements IFsIntegralGoodsService
     public List<FsIntegralGoodsChooseVO> getChooseIntegralGoodsListByMap(Map<String, Object> params) {
         return fsIntegralGoodsMapper.getChooseIntegralGoodsListByMap(params);
     }
+
+    /**
+     * 批量修改有效期
+     * <p>长期有效时清空开始/结束时间,非长期有效时校验时间区间。</p>
+     *
+     * @param goodsIds   商品ID集合
+     * @param isLongTerm 是否长期有效 0是 1否
+     * @param startTime  活动开始时间
+     * @param endTime    活动结束时间
+     * @return 影响行数
+     */
+    @Override
+    public int batchUpdateValidPeriod(Long[] goodsIds, Integer isLongTerm, Date startTime, Date endTime) {
+        if (goodsIds == null || goodsIds.length == 0) {
+            throw new ServiceException("请选择需要修改的商品");
+        }
+        if (isLongTerm == null) {
+            throw new ServiceException("请选择是否长期有效");
+        }
+        // 非长期有效时,校验时间
+        if (isLongTerm == 1) {
+            if (startTime == null || endTime == null) {
+                throw new ServiceException("非长期有效时,开始时间和结束时间不能为空");
+            }
+            if (startTime.after(endTime)) {
+                throw new ServiceException("开始时间不能晚于结束时间");
+            }
+        }
+        // 长期有效时,开始/结束时间置空
+        Date effectiveStart = isLongTerm == 0 ? null : startTime;
+        Date effectiveEnd = isLongTerm == 0 ? null : endTime;
+        return fsIntegralGoodsMapper.batchUpdateValidPeriod(goodsIds, isLongTerm, effectiveStart, effectiveEnd);
+    }
 }

+ 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);
+    }
+}

+ 523 - 35
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,9 +80,14 @@ 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;
+import javax.annotation.Resource;
+import org.springframework.data.redis.core.RedisTemplate;
 
 /**
  * 积分商品订单Service业务层处理
@@ -89,14 +100,13 @@ import java.util.stream.Collectors;
 public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
 {
     /**
-     * 限购缓存 Redis Key 前缀
-     * <p>完整格式:integral:purchase:count:{userId}:{goodsId},缓存用户已购某商品的有效订单数</p>
+     * 限购计数器 Redis Hash Key 前缀
+     * <p>完整格式:integral:purchase:count:{userId}(Hash 结构)
+     * HashKey 为 goodsId,Value 为已购有效订单数(仅排除 status=5已取消、-2已退款)。
+     * 通过 HINCRBY 原子增减保证一致性:下单成功 +N,取消/退款 -N。</p>
      */
     private static final String REDIS_KEY_INTEGRAL_PURCHASE_COUNT = "integral:purchase:count:";
 
-    /** 限购缓存过期时间(1小时),过期后回源数据库重建 */
-    private static final Integer PURCHASE_COUNT_CACHE_TTL = 1;
-
     @Autowired
     private FsCouponMapper fsCouponMapper;
 
@@ -106,6 +116,9 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
     @Autowired
     private FsIntegralOrderMapper fsIntegralOrderMapper;
 
+    @Autowired
+    private FsIntegralOrderItemMapper fsIntegralOrderItemMapper;
+
     @Autowired
     private FsUserAddressMapper fsUserAddressMapper;
 
@@ -138,6 +151,13 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
     private IFsUserIntegralLogsService userIntegralLogsService;
     @Autowired
     private RedisCache redisCache;
+    /**
+     * 限购计数器专用 RedisTemplate(Hash 结构)
+     * <p>Key: integral:purchase:count:{userId},HashKey: {goodsId},Value: 已购有效订单数
+     * 使用 GenericToStringSerializer 序列化 Integer,支持 HINCRBY 原子增减操作</p>
+     */
+    @Resource(name = "redisTemplateForInteger")
+    private RedisTemplate<String, Integer> redisTemplateForInteger;
     @Autowired
     private IPayService ybPayService;
     /**
@@ -179,6 +199,12 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
     @Autowired
     private JstErpHttpService jstErpHttpService;
 
+    /**
+     * 快递公司服务(物流回调订阅、本地快递编码映射)
+     */
+    @Autowired
+    private IFsExpressService expressService;
+
     /**
      * 查询积分商品订单
      *
@@ -310,6 +336,14 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
         if (Objects.isNull(integralGoods)) {
             return R.error("商品不存在");
         }
+        // 有效期校验:isLongTerm=1 非长期有效时,校验当前时间是否在有效期区间内
+        if (integralGoods.getIsLongTerm() != null && integralGoods.getIsLongTerm() == 1) {
+            Date now = new Date();
+            if (integralGoods.getValidStartTime() == null || integralGoods.getValidEndTime() == null
+                    || now.before(integralGoods.getValidStartTime()) || now.after(integralGoods.getValidEndTime())) {
+                return R.error("积分商品已过兑换有效期限");
+            }
+        }
         if(integralGoods.getStock()<=0L){
             return R.error("库存不足");
         }
@@ -317,9 +351,9 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
             return R.error("积分不足");
         }
 
-        // 限购校验:limitNum > 0 表示开启限购,校验用户已购数量是否已达上限
+        // 限购校验:limitNum > 0 表示开启限购,读 Redis 计数器(懒加载+降级查库)
         if (integralGoods.getLimitNum() != null && integralGoods.getLimitNum() > 0) {
-            int purchasedCount = getUserPurchasedCount(param.getUserId(), param.getGoodsId());
+            int purchasedCount = getPurchasedCount(param.getUserId(), param.getGoodsId());
             if (purchasedCount + 1 > integralGoods.getLimitNum()) {
                 return R.error("超过限购数量,每人限购" + integralGoods.getLimitNum() + "件");
             }
@@ -399,6 +433,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 规范实现
@@ -416,8 +454,10 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
                 userIntegralLogsService.insertLog(logs);
             }
 
-            // 失效限购缓存,下次查询时从数据库重建(保证数据一致性)
-            invalidatePurchaseCountCache(order.getUserId(), goodsItem.get(0).getGoodsId());
+            // 限购计数器 +N(遍历所有商品,购物车下单时 goodsItem 可能为多个)
+            for (FsIntegralGoods g : goodsItem) {
+                incrPurchasedCount(order.getUserId(), g.getGoodsId(), g.getNum() == null ? 1 : g.getNum());
+            }
 
             // 积分支付
             if (order.getPayType() == 1) {
@@ -443,44 +483,143 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
     }
 
     /**
-     * 获取用户已购买指定积分商品的有效订单数(限购校验用)
-     * <p>缓存策略:优先查 Redis,未命中则回源数据库并回填缓存(1小时过期)。
-     * 缓存过期后会重新从数据库查询,保证用户取消订单后限购数量能及时更新。</p>
+     * 构建积分订单商品明细列表
+     * <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;
+    }
+
+    /**
+     * 获取用户已购买指定积分商品的有效订单数(限购校验用,Redis 计数器方案)
+     * <p>读取 Redis Hash(key=integral:purchase:count:{userId}, hashKey={goodsId}),
+     * 未命中时从数据库懒加载并回填,Redis 不可用时降级查库。</p>
      *
      * @param userId  用户ID
      * @param goodsId 积分商品ID
      * @return 已购买有效订单数(非负)
      */
-    private int getUserPurchasedCount(Long userId, Long goodsId) {
-        String redisKey = REDIS_KEY_INTEGRAL_PURCHASE_COUNT + userId + ":" + goodsId;
-        Integer count = redisCache.getCacheObject(redisKey);
-        if (count != null) {
-            return count;
-        }
-        // 回源数据库查询有效订单数(排除已取消和已删除)
-        count = fsIntegralOrderMapper.selectUserPurchasedCount(userId, goodsId);
-        if (count < 0) {
-            count = 0;
-        }
-        // 回填 Redis 缓存,1小时过期
-        redisCache.setCacheObject(redisKey, count, PURCHASE_COUNT_CACHE_TTL, TimeUnit.HOURS);
-        return count;
+    private int getPurchasedCount(Long userId, Long goodsId) {
+        if (userId == null || goodsId == null) {
+            return 0;
+        }
+        String redisKey = REDIS_KEY_INTEGRAL_PURCHASE_COUNT + userId;
+        String hashKey = goodsId.toString();
+        try {
+            Integer count = (Integer) redisTemplateForInteger.opsForHash().get(redisKey, hashKey);
+            if (count != null) {
+                return count;
+            }
+            // 懒加载:Redis 无记录时从 DB 初始化(并发下多次 put 只是覆盖为相同的 DB 值,结果一致)
+            int dbCount = fsIntegralOrderMapper.selectUserPurchasedCount(userId, goodsId);
+            redisTemplateForInteger.opsForHash().put(redisKey, hashKey, dbCount);
+            return dbCount;
+        } catch (Exception e) {
+            // Redis 异常降级查库,保证限购功能可用
+            log.warn("限购计数器读取Redis失败,降级查DB userId={}, goodsId={}", userId, goodsId, e);
+            return fsIntegralOrderMapper.selectUserPurchasedCount(userId, goodsId);
+        }
+    }
+
+    /**
+     * 增加用户已购商品计数(订单创建成功后调用)
+     * <p>通过 HINCRBY 原子增加,保证高并发下计数准确。</p>
+     *
+     * @param userId  用户ID
+     * @param goodsId 积分商品ID
+     * @param num     本次购买数量
+     */
+    private void incrPurchasedCount(Long userId, Long goodsId, int num) {
+        if (userId == null || goodsId == null || num <= 0) {
+            return;
+        }
+        try {
+            String redisKey = REDIS_KEY_INTEGRAL_PURCHASE_COUNT + userId;
+            redisTemplateForInteger.opsForHash().increment(redisKey, goodsId.toString(), num);
+        } catch (Exception e) {
+            // Redis 异常不影响主流程,下次限购校验时懒加载会从 DB 重建最新值
+            log.warn("限购计数器incr失败 userId={}, goodsId={}, num={}", userId, goodsId, num, e);
+        }
     }
 
     /**
-     * 失效用户购买指定商品的限购缓存
-     * <p>订单创建成功后调用,确保下次限购校验时从数据库重新加载最新数据。</p>
+     * 减少用户已购商品计数(取消订单/退款成功后调用)
+     * <p>通过 HINCRBY 原子减少,保证计数与订单终态一致。
+     * 仅在订单变为终态无效(5已取消、-2已退款)时调用。</p>
      *
      * @param userId  用户ID
      * @param goodsId 积分商品ID
+     * @param num     本次退还数量
      */
-    private void invalidatePurchaseCountCache(Long userId, Long goodsId) {
+    private void decrPurchasedCount(Long userId, Long goodsId, int num) {
+        if (userId == null || goodsId == null || num <= 0) {
+            return;
+        }
         try {
-            String redisKey = REDIS_KEY_INTEGRAL_PURCHASE_COUNT + userId + ":" + goodsId;
-            redisCache.deleteObject(redisKey);
+            String redisKey = REDIS_KEY_INTEGRAL_PURCHASE_COUNT + userId;
+            // 使用 increment 传负数实现递减;递减后若计数为负,说明 Redis 与 DB 不一致(如懒加载前旧数据),重置为 0
+            Long newVal = redisTemplateForInteger.opsForHash().increment(redisKey, goodsId.toString(), -num);
+            if (newVal != null && newVal < 0) {
+                redisTemplateForInteger.opsForHash().put(redisKey, goodsId.toString(), 0);
+            }
         } catch (Exception e) {
-            // 缓存失效失败不影响主流程,下次限购校验时缓存过期后会自动重建
-            log.warn("失效限购缓存失败, userId={}, goodsId={}", userId, goodsId, e);
+            // Redis 异常不影响主流程,下次限购校验时懒加载会从 DB 重建最新值
+            log.warn("限购计数器decr失败 userId={}, goodsId={}, num={}", userId, goodsId, num, e);
+        }
+    }
+
+    /**
+     * 减少订单中所有商品对应的限购计数(订单变为终态无效时调用)
+     * <p>解析 item_json 获取 goodsId 和购买数量,逐一调用 decr。
+     * 兼容 item_json 单对象(旧数据)和数组(新数据)两种格式。</p>
+     *
+     * @param order 积分订单
+     */
+    private void decrPurchasedCountByOrder(FsIntegralOrder order) {
+        if (order == null || order.getUserId() == null || StringUtils.isEmpty(order.getItemJson())) {
+            return;
+        }
+        String itemJson = order.getItemJson();
+        try {
+            if (itemJson.startsWith("[") && itemJson.endsWith("]")) {
+                List<FsIntegralGoods> goodsItem = JSONUtil.toBean(itemJson, new TypeReference<List<FsIntegralGoods>>(){}, true);
+                for (FsIntegralGoods goods : goodsItem) {
+                    if (goods != null && goods.getGoodsId() != null) {
+                        decrPurchasedCount(order.getUserId(), goods.getGoodsId(),
+                                goods.getNum() == null ? 1 : goods.getNum());
+                    }
+                }
+            } else {
+                FsIntegralGoods goods = JSONUtil.toBean(itemJson, FsIntegralGoods.class);
+                if (goods != null && goods.getGoodsId() != null) {
+                    decrPurchasedCount(order.getUserId(), goods.getGoodsId(),
+                            goods.getNum() == null ? 1 : goods.getNum());
+                }
+            }
+        } catch (Exception e) {
+            log.warn("订单限购计数decr失败, orderId={}, itemJson={}", order.getOrderId(), itemJson, e);
         }
     }
 
@@ -518,10 +657,27 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
             if (Objects.isNull(integralGoods)) {
                 throw new CustomException("商品不存在");
             }
+            // 有效期校验:非长期有效时,校验当前时间是否在有效期区间内
+            if (integralGoods.getIsLongTerm() != null && integralGoods.getIsLongTerm() == 1) {
+                Date now = new Date();
+                if (integralGoods.getValidStartTime() == null || integralGoods.getValidEndTime() == null
+                        || now.before(integralGoods.getValidStartTime()) || now.after(integralGoods.getValidEndTime())) {
+                    throw new CustomException("积分商品「" + integralGoods.getGoodsName() + "」已过兑换有效期限");
+                }
+            }
             if(integralGoods.getStock() < cart.getCartNum()){
                 throw new CustomException("库存不足");
             }
 
+            // 限购校验:limitNum > 0 表示开启限购,读 Redis 计数器(懒加载+降级查库)
+            if (integralGoods.getLimitNum() != null && integralGoods.getLimitNum() > 0) {
+                int purchasedCount = getPurchasedCount(param.getUserId(), cart.getGoodsId());
+                if (purchasedCount + cart.getCartNum() > integralGoods.getLimitNum()) {
+                    throw new CustomException("商品「" + integralGoods.getGoodsName()
+                            + "」超过限购数量,每人限购" + integralGoods.getLimitNum() + "件");
+                }
+            }
+
             // 减库存
             if (fsIntegralGoodsMapper.subStock(integralGoods.getGoodsId(), cart.getCartNum()) <= 0) {
                 throw new CustomException("库存不足");
@@ -534,7 +690,15 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
             goodsItem.add(integralGoods);
         }
 
-        return createOrder(user, address, totalIntegral, totalCash, goodsItem, null);
+        R result = createOrder(user, address, totalIntegral, totalCash, goodsItem, null);
+        // 订单创建成功后,清除购物车中已下单的商品(限购计数器 +N 已在 createOrder 内部完成)
+        Object codeObj = result == null ? null : result.get("code");
+        if (codeObj != null && Integer.valueOf(200).equals(codeObj)) {
+            // 清除购物车中已下单的商品(按购物车ID删除,避免误删同商品的其他购物车记录)
+            List<Long> cartIds = carts.stream().map(FsIntegralCart::getId).collect(Collectors.toList());
+            cartService.delCartByIds(user.getUserId(), cartIds);
+        }
+        return result;
     }
 
     @Override
@@ -607,7 +771,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;
     }
 
     /**
@@ -649,6 +818,9 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
         logs.setCreateTime(new Date());
         userIntegralLogsService.insertLog(logs);
 
+        // 订单取消后,限购计数器 -N(status=5不计入限购),保证计数与订单终态一致
+        decrPurchasedCountByOrder(order);
+
         return R.ok();
     }
 
@@ -939,6 +1111,8 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
         // 5. 无需等待回调的场景(纯积分订单无支付记录、或易宝退款立即成功),直接标记为已退款(-2)
         if (!needCallback) {
             fsIntegralOrderMapper.refundOrder(order.getOrderId());
+            // 订单已变为已退款(-2)终态,限购计数器 -N(status=-2不计入限购),保证计数与订单终态一致
+            decrPurchasedCountByOrder(order);
         }
 
         return 1;
@@ -1201,6 +1375,8 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
             int rows = fsIntegralOrderMapper.refundOrder(orderId);
             if (rows > 0) {
                 log.info("积分订单退款回调(Yop) 更新订单状态完成 orderId: {}, status: -1->-2", orderId);
+                // 订单已变为已退款(-2)终态,限购计数器 -N(status=-2不计入限购),保证计数与订单终态一致
+                decrPurchasedCountByOrder(order);
             } else {
                 log.warn("积分订单退款回调(Yop) 更新订单状态失败(可能已被其他流程处理) orderId: {}", orderId);
             }
@@ -1764,4 +1940,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;

+ 9 - 0
fs-service/src/main/java/com/fs/his/vo/FsIntegralGoodsListUVO.java

@@ -11,6 +11,7 @@ import java.util.Date;
 @Data
 public class FsIntegralGoodsListUVO implements Serializable {
 
+    private static final long serialVersionUID = 1L;
 
     /** id */
     private Long goodsId;
@@ -41,7 +42,15 @@ public class FsIntegralGoodsListUVO implements Serializable {
     /** 排序 */
     private Long sort;
 
+    /** 是否长期有效 0是 1否 */
+    private Integer isLongTerm;
 
+    /** 有效期开始时间(is_long_term=1时生效) */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date validStartTime;
 
+    /** 有效期结束时间(is_long_term=1时生效) */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date validEndTime;
 
 }

+ 16 - 0
fs-service/src/main/java/com/fs/his/vo/FsIntegralGoodsListVO.java

@@ -51,6 +51,22 @@ public class FsIntegralGoodsListVO {
     /** 库存 */
     @Excel(name = "库存")
     private Long stock;
+
+    /** 限购数量(0=不限购,>0表示每个用户限购件数) */
+    @Excel(name = "限购数量")
+    private Integer limitNum;
+
+    /** 是否长期有效 0是 1否 */
+    private Integer isLongTerm;
+
+    /** 有效期开始时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date validStartTime;
+
+    /** 有效期结束时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date validEndTime;
+
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private Date createTime;
 }

+ 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;

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

@@ -21,10 +21,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="createTime"    column="create_time"    />
         <result property="barCode"    column="bar_code"    />
         <result property="isDrug"    column="is_drug"    />
+        <result property="isLongTerm"    column="is_long_term"    />
+        <result property="validStartTime"    column="valid_start_time"    />
+        <result property="validEndTime"    column="valid_end_time"    />
     </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, is_drug 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, is_long_term, valid_start_time, valid_end_time from fs_integral_goods
     </sql>
 
     <select id="selectFsIntegralGoodsList" parameterType="FsIntegralGoods" resultMap="FsIntegralGoodsResult">
@@ -64,6 +67,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="createTime != null">create_time,</if>
             <if test="barCode != null">bar_code,</if>
             <if test="isDrug != null">is_drug,</if>
+            <if test="isLongTerm != null">is_long_term,</if>
+            <if test="validStartTime != null">valid_start_time,</if>
+            <if test="validEndTime != null">valid_end_time,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="goodsId != null">#{goodsId},</if>
@@ -82,6 +88,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="createTime != null">#{createTime},</if>
             <if test="barCode != null">#{barCode},</if>
             <if test="isDrug != null">#{isDrug},</if>
+            <if test="isLongTerm != null">#{isLongTerm},</if>
+            <if test="validStartTime != null">#{validStartTime},</if>
+            <if test="validEndTime != null">#{validEndTime},</if>
          </trim>
     </insert>
 
@@ -103,10 +112,25 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="createTime != null">create_time = #{createTime},</if>
             <if test="barCode != null">bar_code = #{barCode},</if>
             <if test="isDrug != null">is_drug = #{isDrug},</if>
+            <if test="isLongTerm != null">is_long_term = #{isLongTerm},</if>
+            <if test="validStartTime != null">valid_start_time = #{validStartTime},</if>
+            <if test="validEndTime != null">valid_end_time = #{validEndTime},</if>
         </trim>
         where goods_id = #{goodsId}
     </update>
 
+    <!-- 批量修改有效期 -->
+    <update id="batchUpdateValidPeriod">
+        update fs_integral_goods
+        set is_long_term = #{isLongTerm},
+            valid_start_time = <choose><when test="startTime != null">#{startTime}</when><otherwise>null</otherwise></choose>,
+            valid_end_time = <choose><when test="endTime != null">#{endTime}</when><otherwise>null</otherwise></choose>
+        where goods_id in
+        <foreach item="goodsId" collection="goodsIds" open="(" separator="," close=")">
+            #{goodsId}
+        </foreach>
+    </update>
+
     <delete id="deleteFsIntegralGoodsByGoodsId" parameterType="Long">
         delete from fs_integral_goods where goods_id = #{goodsId}
     </delete>

+ 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>

+ 11 - 1
fs-user-app/src/main/java/com/fs/app/controller/IntegralController.java

@@ -75,9 +75,19 @@ public class IntegralController extends  AppBaseController {
 
     @ApiOperation("获取积分商品详情")
     @GetMapping("/getIntegralGoodsById")
-    @Cacheable(value = "getIntegralGoodsById", key = "#goodsId")
     public R getIntegralGoodsById(@RequestParam("goodsId")Long goodsId, HttpServletRequest request){
         FsIntegralGoods goods=goodsService.selectFsIntegralGoodsByGoodsId(goodsId);
+        if (goods == null) {
+            return R.error("商品不存在");
+        }
+        // 有效期校验:isLongTerm=1 非长期有效时,校验当前时间是否在有效期区间内
+        if (goods.getIsLongTerm() != null && goods.getIsLongTerm() == 1) {
+            Date now = new Date();
+            if (goods.getValidStartTime() == null || goods.getValidEndTime() == null
+                    || now.before(goods.getValidStartTime()) || now.after(goods.getValidEndTime())) {
+                return R.error("积分商品已过兑换有效期限");
+            }
+        }
         return R.ok().put("data",goods);
     }
 

+ 2 - 2
fs-user-app/src/main/java/com/fs/app/controller/UserController.java

@@ -216,7 +216,7 @@ public class UserController extends  AppBaseController {
     public R editUser(@RequestBody @Valid FsUserEditParam param, HttpServletRequest request){
         param.setUserId(Long.parseLong(getUserId()));
         log.info("【修改用户头像昵称】:{}",param);
-        if (param.getNickname().length()>50){
+        if (param.getNickName().length()>50){
             return R.error("请授权正确的昵称!");
         }
         if (StringUtils.isEmpty(param.getAvatar())){
@@ -225,7 +225,7 @@ public class UserController extends  AppBaseController {
         FsUser user=new FsUser();
         user.setUserId(Long.parseLong(getUserId()));
         user.setAvatar(param.getAvatar());
-        user.setNickName(param.getNickname());
+        user.setNickName(param.getNickName());
         if(param.getSex() != null){
             user.setSex(param.getSex());
         }

+ 6 - 2
fs-user-app/src/main/java/com/fs/app/param/FsUserEditParam.java

@@ -13,9 +13,13 @@ import java.io.Serializable;
 @JsonIgnoreProperties(ignoreUnknown = true)
 public class FsUserEditParam implements Serializable {
 
+//    @NotNull(message = "用户昵称不能为空!")
+//    @JsonAlias({"nickname", "nickName"})
+//    private String nickname;
+
     @NotNull(message = "用户昵称不能为空!")
-    @JsonAlias({"nickname", "nickName"})
-    private String nickname;
+    @JsonAlias({"nickName", "nickname"})
+    private String nickName;
 
     //    @NotBlank(message = "用户头像不能为空!")
     private String avatar;