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

代码优化提交、积分相关

yjwang 1 день назад
Родитель
Сommit
1f7c6c68f5
69 измененных файлов с 2104 добавлено и 785 удалено
  1. 15 0
      .trae/specs/optimize-points-calculation/checklist.md
  2. 44 0
      .trae/specs/optimize-points-calculation/spec.md
  3. 23 0
      .trae/specs/optimize-points-calculation/tasks.md
  4. 12 8
      fs-admin/src/main/java/com/fs/his/controller/FsExpressCommonController.java
  5. 68 19
      fs-admin/src/main/java/com/fs/his/controller/FsUserIntegralLogsController.java
  6. 0 101
      fs-admin/src/main/java/com/fs/hisStore/TestYb.java
  7. 1 0
      fs-common/pom.xml
  8. 68 0
      fs-common/src/main/java/com/fs/common/sharding/ShardingUtil.java
  9. 49 0
      fs-company/src/main/java/com/fs/company/controller/store/FsUserIntegralLogsController.java
  10. 1 3
      fs-company/src/main/java/com/fs/framework/config/DataSourceConfig.java
  11. 1 1
      fs-framework/src/main/java/com/fs/framework/config/DataSourceConfig.java
  12. 3 3
      fs-redis/src/main/java/com/fs/framework/config/DataSourceConfig.java
  13. 3 1
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseOrderServiceImpl.java
  14. 3 2
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseServiceImpl.java
  15. 3 1
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  16. 8 2
      fs-service/src/main/java/com/fs/his/config/PointsGrantRuleConfig.java
  17. 3 0
      fs-service/src/main/java/com/fs/his/enums/FsUserIntegralLogTypeEnum.java
  18. 96 53
      fs-service/src/main/java/com/fs/his/mapper/FsUserIntegralLogsMapper.java
  19. 51 0
      fs-service/src/main/java/com/fs/his/service/IFsUserIntegralLogsService.java
  20. 5 3
      fs-service/src/main/java/com/fs/his/service/impl/FsIntegralOrderServiceImpl.java
  21. 237 159
      fs-service/src/main/java/com/fs/his/service/impl/FsUserIntegralLogsServiceImpl.java
  22. 2 1
      fs-service/src/main/java/com/fs/his/service/impl/FsUserNewTaskServiceImpl.java
  23. 9 5
      fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java
  24. 2 1
      fs-service/src/main/java/com/fs/his/service/impl/FsUserSignServiceImpl.java
  25. 3 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreOrderScrm.java
  26. 2 0
      fs-service/src/main/java/com/fs/hisStore/dto/FsStoreOrderComputeDTO.java
  27. 7 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductScrmMapper.java
  28. 10 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStorePromotionActivityMapper.java
  29. 3 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStorePromotionUsageMapper.java
  30. 41 29
      fs-service/src/main/java/com/fs/hisStore/mapper/FsUserIntegralLogsScrmMapper.java
  31. 4 1
      fs-service/src/main/java/com/fs/hisStore/mapper/FsUserScrmMapper.java
  32. 6 0
      fs-service/src/main/java/com/fs/hisStore/param/FsStoreOrderComputedParam.java
  33. 9 0
      fs-service/src/main/java/com/fs/hisStore/param/FsStoreOrderCreateParam.java
  34. 13 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreOrderScrmService.java
  35. 10 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsUserIntegralLogsScrmService.java
  36. 3 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsUserScrmService.java
  37. 2 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsIntegralOrderScrmServiceImpl.java
  38. 25 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreAfterSalesScrmServiceImpl.java
  39. 520 75
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  40. 1 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java
  41. 9 4
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStorePromotionComputeServiceImpl.java
  42. 10 4
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStorePromotionServiceImpl.java
  43. 99 13
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsUserIntegralLogsScrmServiceImpl.java
  44. 12 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsUserScrmServiceImpl.java
  45. 2 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsUserSignScrmServiceImpl.java
  46. 7 1
      fs-service/src/main/java/com/fs/hisStore/support/FsStorePromotionTierCalculator.java
  47. 2 5
      fs-service/src/main/java/com/fs/pay/pay/util/PayUtil.java
  48. 3 3
      fs-service/src/main/java/com/fs/pay/service/impl/PayScrmServiceImpl.java
  49. 1 0
      fs-service/src/main/java/com/fs/system/vo/DictVO.java
  50. 2 5
      fs-service/src/main/java/com/fs/ybPay/util/PayUtil.java
  51. 114 45
      fs-service/src/main/resources/mapper/his/FsUserIntegralLogsMapper.xml
  52. 5 1
      fs-service/src/main/resources/mapper/hisStore/FsStoreOrderScrmMapper.xml
  53. 101 0
      fs-service/src/main/resources/mapper/hisStore/FsStoreProductScrmMapper.xml
  54. 17 0
      fs-service/src/main/resources/mapper/hisStore/FsStorePromotionActivityMapper.xml
  55. 12 1
      fs-service/src/main/resources/mapper/hisStore/FsStorePromotionUsageMapper.xml
  56. 48 46
      fs-service/src/main/resources/mapper/hisStore/FsUserIntegralLogsScrmMapper.xml
  57. 3 2
      fs-store/src/main/java/com/fs/framework/config/DruidConfig.java
  58. 13 1
      fs-store/src/main/java/com/fs/hisStore/controller/store/FsStoreAfterSalesScrmController.java
  59. 49 0
      fs-store/src/main/java/com/fs/store/controller/store/FsUserIntegralLogsController.java
  60. 1 1
      fs-user-app/src/main/java/com/fs/app/controller/UserAddressController.java
  61. 32 22
      fs-user-app/src/main/java/com/fs/app/controller/YbPayController.java
  62. 3 2
      fs-user-app/src/main/java/com/fs/app/controller/store/CourseScrmController.java
  63. 10 52
      fs-user-app/src/main/java/com/fs/app/controller/store/HuifuPayScrmController.java
  64. 3 29
      fs-user-app/src/main/java/com/fs/app/controller/store/IntegralScrmController.java
  65. 115 46
      fs-user-app/src/main/java/com/fs/app/controller/store/PayScrmController.java
  66. 48 6
      fs-user-app/src/main/java/com/fs/app/controller/store/StoreOrderScrmController.java
  67. 7 3
      fs-user-app/src/main/java/com/fs/app/controller/store/UserSignScrmController.java
  68. 9 17
      fs-user-app/src/main/java/com/fs/app/redis/OrderExpireHandler.java
  69. 1 3
      fs-user-app/src/main/java/com/fs/framework/config/DataSourceConfig.java

+ 15 - 0
.trae/specs/optimize-points-calculation/checklist.md

@@ -0,0 +1,15 @@
+# Checklist
+
+- [x] 积分发放使用 `RoundingMode.HALF_UP`(四舍五入),不再使用 `ROUND_CEILING`
+- [x] `FsStoreOrderComputedParam.preAllocatedIntegral` 类型为 `Integer`
+- [x] `computedOrders` 中 `preAllocatedIntegrals` 数组类型为 `Integer[]`
+- [x] `totalIntegral` 使用 `long` 类型,`user.getIntegral()` 使用 `longValue()` 取值
+- [x] `totalDeduction` 使用 `BigDecimal.divide` 计算,保留 2 位小数
+- [x] 子订单积分分配使用 `BigDecimal` 算术,不出现 `double` 中间运算
+- [x] cap 超限后 `allocatedDeductionSum` 和 `allocatedIntegralSum` 使用 cap 后的值累加
+- [x] 最后一个店铺积分 = `totalIntegral - allocatedIntegralSum`(减法兜底)
+- [x] 最后一个店铺抵扣金额 = `totalDeduction - allocatedDeductionSum`(减法兜底)
+- [x] 所有积分值均为整数,不出现小数点
+- [x] `computedOrderMultiStore` 中 `preAllocatedIntegral` 的 Integer → double 转换正确
+- [x] 旧的单店铺积分逻辑(`computedOrder` / `store.integral`)不受影响
+- [x] 退款积分逻辑(`refundIntegral`)不受影响

+ 44 - 0
.trae/specs/optimize-points-calculation/spec.md

@@ -0,0 +1,44 @@
+# 积分计算优化 Spec
+
+## Why
+积分相关代码存在多个精度和计算正确性问题:积分发放使用 `ROUND_CEILING`(进位取整)而非用户要求的四舍五入;多店铺积分分摊使用 `double` 算术存在浮点精度风险;`preAllocatedIntegral` 使用 `Double` 类型不符合积分必须为整数的约束;多店铺分摊中 cap 修正后未回写累计值导致最后一个店铺减法兜底结果错误。
+
+## What Changes
+- 积分发放取整方式从 `ROUND_CEILING` 改为 `ROUND_HALF_UP`(四舍五入)
+- `FsStoreOrderComputedParam.preAllocatedIntegral` 类型从 `Double` 改为 `Integer`
+- `computedOrders` 中积分分摊计算全部改用 `BigDecimal` 算术,消除 `double` 精度风险
+- 修复多店铺分摊中 `subDeduction` 超限 cap 后未更新 `allocatedDeductionSum` / `allocatedIntegralSum` 的 bug
+- `user.getIntegral().intValue()` 改为 `longValue()`,`totalIntegral` 改为 `long` 类型,避免大值截断
+- `totalDeduction` 计算改用 `BigDecimal.divide` 确保精度
+
+## Impact
+- Affected code: `FsUserIntegralLogsServiceImpl.java`、`FsStoreOrderScrmServiceImpl.java`、`FsStoreOrderComputedParam.java`
+- 不影响旧的单店铺积分逻辑(`computedOrder` / `store.integral` 配置)
+- 不影响退款积分逻辑(`refundIntegral`)
+- `preAllocatedIntegral` 类型变更需同步修改 `computedOrders` 和 `computedOrderMultiStore` 中的使用
+
+## MODIFIED Requirements
+
+### Requirement: 积分发放取整
+积分发放时,`amount × rate` 的结果 SHALL 使用 `RoundingMode.HALF_UP`(四舍五入)取整,不得使用 `ROUND_CEILING`(进位取整)。
+
+#### Scenario: 正常发放
+- **WHEN** 消费金额为 10.5 元,发放比例为 0.1(1元=0.1积分)
+- **THEN** 计算结果 1.05 → 四舍五入为 1 积分(非进位取整的 2)
+
+### Requirement: 多店铺积分分摊计算精度
+多店铺积分分摊 SHALL 全程使用 `BigDecimal` 算术,禁止使用 `double` 中间运算。积分值 SHALL 始终为整数,不得出现小数。
+
+#### Scenario: 比例分摊
+- **WHEN** 合计 100 元,子订单 A=95 元、B=5 元,总积分 100
+- **THEN** A 分摊 95 积分,B 分摊 5 积分(最后一个用总计减去已分配)
+
+#### Scenario: 超限 cap 后累计值修正
+- **WHEN** 子订单分摊的抵扣金额超过该子订单应付金额时
+- **THEN** cap 后的值 SHALL 回写到 `allocatedDeductionSum` 和 `allocatedIntegralSum`,确保最后一个店铺的减法兜底结果正确
+
+### Requirement: preAllocatedIntegral 类型
+`FsStoreOrderComputedParam.preAllocatedIntegral` SHALL 为 `Integer` 类型,不得使用 `Double`。
+
+### Requirement: 用户积分取值精度
+读取用户积分余额时 SHALL 使用 `longValue()`,不得使用 `intValue()` 以防大值截断。

+ 23 - 0
.trae/specs/optimize-points-calculation/tasks.md

@@ -0,0 +1,23 @@
+# Tasks
+
+- [x] Task 1: 积分发放取整方式从 ROUND_CEILING 改为 ROUND_HALF_UP
+  - [x] SubTask 1.1: 修改 `FsUserIntegralLogsServiceImpl.java` grantPoints 方法中 `setScale(0, BigDecimal.ROUND_CEILING)` → `setScale(0, RoundingMode.HALF_UP)`
+
+- [x] Task 2: preAllocatedIntegral 类型从 Double 改为 Integer
+  - [x] SubTask 2.1: 修改 `FsStoreOrderComputedParam.java` 中 `preAllocatedIntegral` 字段类型 `Double` → `Integer`
+  - [x] SubTask 2.2: 修改 `FsStoreOrderScrmServiceImpl.java` computedOrders 中 `Double[] preAllocatedIntegrals` → `Integer[]`,及赋值处去掉 `(double)` 强转
+  - [x] SubTask 2.3: 修改 `FsStoreOrderScrmServiceImpl.java` computedOrderMultiStore 中 `usedIntegral = param.getPreAllocatedIntegral()` 赋值处,确保 Integer → double 的正确转换
+
+- [x] Task 3: 多店铺积分分摊计算改用 BigDecimal 算术
+  - [x] SubTask 3.1: 将 `totalIntegral` 从 `int` 改为 `long`,`user.getIntegral().intValue()` 改为 `longValue()`
+  - [x] SubTask 3.2: `totalDeduction` 计算改用 `BigDecimal.divide(BigDecimal.valueOf(payRate), 2, RoundingMode.HALF_UP)` 替代 `NumberUtil.div`
+  - [x] SubTask 3.3: 子订单积分分配 `Math.round(totalIntegral * ratio.doubleValue())` 改用 `BigDecimal` 计算:`BigDecimal.valueOf(totalIntegral).multiply(ratio).setScale(0, RoundingMode.HALF_UP).intValue()`
+  - [x] SubTask 3.4: cap 超限时积分重算 `Math.round(subDeduction.doubleValue() * payRate)` 改用 `BigDecimal` 计算:`subDeduction.multiply(BigDecimal.valueOf(payRate)).setScale(0, RoundingMode.HALF_UP).intValue()`
+
+- [x] Task 4: 修复多店铺分摊 cap 后未更新累计值的 bug
+  - [x] SubTask 4.1: 将 `allocatedDeductionSum` 和 `allocatedIntegralSum` 的累加操作移到 cap 判断之后,使用 cap 后的值进行累加
+
+# Task Dependencies
+- Task 2 和 Task 3 可并行
+- Task 4 依赖 Task 3(在 BigDecimal 改造基础上修复)
+- Task 1 无依赖

+ 12 - 8
fs-admin/src/main/java/com/fs/his/controller/FsExpressCommonController.java

@@ -4,6 +4,7 @@ import cn.hutool.core.date.DateTime;
 import com.alibaba.fastjson.JSON;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.R;
+import com.fs.common.sharding.ShardingUtil;
 import com.fs.his.domain.FsUser;
 import com.fs.his.domain.FsUserIntegralLogs;
 import com.fs.his.domain.Tlintegral;
@@ -69,9 +70,10 @@ public class FsExpressCommonController extends BaseController {
 				logs.setUserId(userMap.getUserId());
 				logs.setBalance(userMap.getIntegral());
 				logs.setLogType(8);
-				logs.setCreateTime(new Date());
-				integralLogsMapper.insertFsUserIntegralLogs(logs);
-			}else {
+			logs.setCreateTime(new Date());
+			int shard = ShardingUtil.shardOf(userMap.getUserId());
+			integralLogsMapper.insertFsUserIntegralLogs(logs, shard);
+		}else {
 				FsUser userMap=new FsUser();
 				userMap.setUserId(user.getUserId());
 				Long integral = user.getIntegral();
@@ -83,11 +85,12 @@ public class FsExpressCommonController extends BaseController {
 				logs.setUserId(userMap.getUserId());
 				logs.setBalance(userMap.getIntegral());
 				logs.setLogType(8);
-				logs.setCreateTime(new Date());
-				integralLogsMapper.insertFsUserIntegralLogs(logs);
-			}
+			logs.setCreateTime(new Date());
+			int shard = ShardingUtil.shardOf(userMap.getUserId());
+			integralLogsMapper.insertFsUserIntegralLogs(logs, shard);
+		}
 
-		} else{
+	} else{
 			FsUser newUser=new FsUser();
 			newUser.setPhone(tl.getPhoneNum());
 			newUser.setNickName("微信用户"+tl.getPhoneNum().substring(tl.getPhoneNum().length()-4));
@@ -102,7 +105,8 @@ public class FsExpressCommonController extends BaseController {
 			logs.setBalance(newUser.getIntegral());
 			logs.setLogType(8);
 			logs.setCreateTime(new Date());
-			integralLogsMapper.insertFsUserIntegralLogs(logs);
+			int shard = ShardingUtil.shardOf(newUser.getUserId());
+			integralLogsMapper.insertFsUserIntegralLogs(logs, shard);
 		}
 		return R.ok();
 	}

+ 68 - 19
fs-admin/src/main/java/com/fs/his/controller/FsUserIntegralLogsController.java

@@ -3,13 +3,14 @@ package com.fs.his.controller;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.PageDomain;
 import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.core.page.TableSupport;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.his.domain.FsUserIntegralLogs;
 import com.fs.his.param.FsUserIntegralLogsParam;
 import com.fs.his.service.IFsUserIntegralLogsService;
-import com.fs.his.vo.FsTestReportListVO;
 import com.fs.his.vo.FsUserIntegralLogsListVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -31,38 +32,31 @@ public class FsUserIntegralLogsController extends BaseController
     private IFsUserIntegralLogsService fsUserIntegralLogsService;
 
     /**
-     * 查询积分记录列表
+     * 查询积分记录列表(总后台跨全部分表查询)
+     * <p>fs_user_integral_logs 已按 user_id % 10 分表,此接口通过显式 UNION ALL 所有分表
+     * 实现全量数据查询,走 MASTER 数据源,不依赖 ShardingSphere 路由。</p>
      */
     @PreAuthorize("@ss.hasPermi('his:userIntegralLogs:list')")
     @GetMapping("/list")
     public TableDataInfo list(FsUserIntegralLogsParam fsUserIntegralLogs)
     {
-        startPage();
-        List<FsUserIntegralLogsListVO> list = fsUserIntegralLogsService.selectFsUserIntegralLogsListVO(fsUserIntegralLogs);
-        for (FsUserIntegralLogsListVO vo : list) {
-            if (vo.getPhone()!=null&&vo.getPhone()!=""){
-                vo.setPhone(vo.getPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
-            }
-
-        }
-        return getDataTable(list);
+        PageDomain pageDomain = TableSupport.buildPageRequest();
+        TableDataInfo table = fsUserIntegralLogsService.selectListAllShards(
+                fsUserIntegralLogs, pageDomain.getPageNum(), pageDomain.getPageSize());
+        maskPhone(table);
+        return table;
     }
 
     /**
-     * 导出积分记录列表
+     * 导出积分记录列表(总后台跨全部分表查询,不分页)
      */
     @PreAuthorize("@ss.hasPermi('his:userIntegralLogs:export')")
     @Log(title = "积分记录", businessType = BusinessType.EXPORT)
     @GetMapping("/export")
     public AjaxResult export(FsUserIntegralLogsParam fsUserIntegralLogs)
     {
-        List<FsUserIntegralLogsListVO> list = fsUserIntegralLogsService.selectFsUserIntegralLogsListVO(fsUserIntegralLogs);
-        for (FsUserIntegralLogsListVO vo : list) {
-            if (vo.getPhone()!=null&&vo.getPhone()!=""){
-                vo.setPhone(vo.getPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
-            }
-
-        }
+        List<FsUserIntegralLogsListVO> list = fsUserIntegralLogsService.selectListAllShardsAll(fsUserIntegralLogs);
+        maskPhone(list);
         ExcelUtil<FsUserIntegralLogsListVO> util = new ExcelUtil<FsUserIntegralLogsListVO>(FsUserIntegralLogsListVO.class);
         return util.exportExcel(list, "积分记录数据");
     }
@@ -77,6 +71,18 @@ public class FsUserIntegralLogsController extends BaseController
         return AjaxResult.success(fsUserIntegralLogsService.selectFsUserIntegralLogsById(id));
     }
 
+    /**
+     * 按订单 ID 查询积分明细列表
+     * 用于订单详情页展示该订单相关的全部积分变动(发放、抵扣、退回等)
+     */
+    @PreAuthorize("@ss.hasPermi('his:userIntegralLogs:query')")
+    @GetMapping("/getByOrderId/{orderId}")
+    public AjaxResult getByOrderId(@PathVariable("orderId") Long orderId)
+    {
+        List<FsUserIntegralLogs> list = fsUserIntegralLogsService.selectByBusinessId(orderId.toString());
+        return AjaxResult.success(list);
+    }
+
     /**
      * 新增积分记录
      */
@@ -109,4 +115,47 @@ public class FsUserIntegralLogsController extends BaseController
     {
         return toAjax(fsUserIntegralLogsService.deleteFsUserIntegralLogsByIds(ids));
     }
+
+    /**
+     * 手机号脱敏(中间四位用 * 替换)
+     *
+     * @param list 积分明细列表
+     */
+    @SuppressWarnings("unchecked")
+    private void maskPhone(List<?> list)
+    {
+        if (list == null || list.isEmpty())
+        {
+            return;
+        }
+        for (Object item : list)
+        {
+            if (item instanceof FsUserIntegralLogsListVO)
+            {
+                maskPhoneSingle((FsUserIntegralLogsListVO) item);
+            }
+        }
+    }
+
+    /**
+     * 对 TableDataInfo 中的 rows 进行手机号脱敏
+     *
+     * @param table 分页结果
+     */
+    @SuppressWarnings("unchecked")
+    private void maskPhone(TableDataInfo table)
+    {
+        maskPhone((List<?>) table.getRows());
+    }
+
+    /**
+     * 单条记录手机号脱敏
+     */
+    private void maskPhoneSingle(FsUserIntegralLogsListVO vo)
+    {
+        if (vo.getPhone() != null && !vo.getPhone().isEmpty())
+        {
+            vo.setPhone(vo.getPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+        }
+    }
 }

+ 0 - 101
fs-admin/src/main/java/com/fs/hisStore/TestYb.java

@@ -1,101 +0,0 @@
-package com.fs.hisStore;
-
-import com.fs.ybPay.aggpay.AggpayClient;
-import com.fs.ybPay.aggpay.AggpayClientBuilder;
-import com.fs.ybPay.aggpay.request.PrePayRequest;
-import com.fs.ybPay.aggpay.response.PrePayResponse;
-import com.yeepay.yop.sdk.exception.YopClientException;
-import com.yeepay.yop.sdk.service.common.YopClient;
-import com.yeepay.yop.sdk.service.common.YopClientBuilder;
-import org.joda.time.DateTime;
-
-import java.math.BigDecimal;
-
-/**
- * 易宝支付测试类
- * 
- * 测试前请确保:
- * 1. 已在 resources/config/yop_sdk_config_default.json 配置正确的:
- *    - app_key: 商户编号
- *    - isv_private_key: 商户私钥
- *    - yop_public_key: 易宝公钥(已配置)
- * 2. 已添加 BouncyCastle 依赖(pom.xml)
- */
-public class TestYb {
-    static YopClient yopClient = YopClientBuilder.builder().build();
-    private static final AggpayClient api = AggpayClientBuilder.builder().build();
-    public static void main(String[] args) {
-        System.out.println("========== 易宝支付SDK测试 ==========");
-        
-        // 测试1: 基础SDK调用测试
-//        testBasicSdk();
-        
-         //测试2: 微信小程序支付下单测试(需要配置正确参数后取消注释)
-         testWxMiniPay();
-        
-        // 测试3: 订单查询测试
-        // testOrderQuery();
-        
-        // 测试4: 退款测试
-        // testRefund();
-    }
-
-
-    /**
-     * 测试微信小程序支付下单
-     */
-    private static void testWxMiniPay() {
-        PrePayRequest request = new PrePayRequest();
-        request.setParentMerchantNo("10092979478");
-        request.setMerchantNo("10092979478");
-        request.setOrderId("ORDER202401010515217305372872");
-        request.setOrderAmount(new BigDecimal("0"));
-        request.setExpiredTime(new DateTime());
-        request.setNotifyUrl("https://notify.merchant.com/xxx");
-        request.setRedirectUrl("https://notify.merchant.com/xxx");
-        request.setMemo("memo_example");
-        request.setGoodsName("旺仔牛奶");
-        request.setFundProcessType(PrePayRequest.FundProcessTypeEnum.fromValue("DELAY_SETTLE"));
-        request.setPayWay("MINI_PROGRAM");
-        request.setChannel(PrePayRequest.ChannelEnum.fromValue("WECHAT"));
-        request.setScene("OFFLINE");
-        request.setPromotionType("WECHAT_B2B");
-        request.setAppId("wx012574bf7bc1836d");
-        request.setUserId("2088176118911271 、olkcn7obuausx40U8TjVj-5QEdT7");
-        request.setUserIp("127.12.1.60");
-        request.setChannelSpecifiedInfo("{\"hbFqNum\":\"3\",\"hbFqSellerPercent\":\"0\",\"sysServiceProviderId\":\"\"}");
-        request.setChannelPromotionInfo("channelPromotionInfo_example");
-        request.setIdentityInfo("{\"identityVerifyType\":\"Y\",\"payerIdType\":\"IDENTITY_CARD\",\"payerNumber\":\"234512198006252456\",\"payerName\":\"名字\"}");
-        request.setLimitCredit("N");
-        request.setUniqueOrderNo("1012202101070000001989946571");
-        request.setToken("83BCDF29CFACB4411533080B67864EF8C907CCDC5E10A707C285FEA10CDB8221");
-        request.setCsUrl("csUrl_example");
-        request.setAccountLinkInfo("{accountProvider\":\"BOL\",\"token\":\"xxx\"}");
-        request.setYpPromotionInfo("自定义支付立减:[{\"amount\":\"0.01\",\"type\":\"CUSTOM_REDUCTION\"}],自定义补贴商户[{\"type\":\"CUSTOM_ALLOWANCE\"}]");
-        request.setBankCode("BOC");
-        request.setBusinessInfo("businessInfo_example");
-        request.setUserAuthCode("userAuthCode_example");
-        request.setChannelActivityInfo("{\"food_order_type\":\"QR_FOOD_ORDER\"}");
-        request.setTerminalId("terminalId_example");
-        request.setTerminalSceneInfo("{\"storeId\":\"门店id\",\"storeName\":\"门店名称\",\"operatorId\":\"商户操作员编号\",\"alipayStoreId\":\"支付宝的店铺编号\",\"areaCode\":\"门店行政区划码\",\"address\":\"门店详细地址\"}");
-        request.setYpAccountBookNo("ypAccountBookNo_example");
-        request.setTerminalInfo("{\"shopName\":\"网点名称\",\"shopCustomerNumber\":\"网点编号\"}");
-        request.setProductInfo("[{\"id\":\"random_reduction_pro\"}]");
-        request.setDivideDetail("[{\"amount\":\"金额\",\"ledgerNo\":\"分账商编\",\"divideDetailDesc\":\"分账说明\"}]");
-        request.setDivideNotifyUrl("divideNotifyUrl_example");
-        request.setFeeSubsidyInfo("[{\"subsidyMerchantNo\":\"10080009498\",\"subsidyAccountType\":\"FEE_ACCOUNT\",\"subsidyType\":\"ABSOLUTE\",\"subsidyProportion\":\"\",\"subsidyCalculateType\":\"SINGLE_PERCENT\",\"subsidyPercentFee\":\"0.6\",\"subsidyFixedFee\":\"\",\"subsidySingleMaxFee\":\"\"}]");
-        request.setAgreementId("agreementId_example");
-        request.setCreditOrderId("creditOrderId_example");
-        request.setPayMedium("payMedium_example");
-        request.setReceiverAccountType("receiverAccountType_example");
-        request.setJdbtPaymentInfo("{\"installmentNum\":\"3\",\"lockPlan\":\"3\",\"riskInfo\":{\"tradeScene\":\"01\",\"ifPickup\":\"01\",\"orderEid\":\"device2736253\",\"orderAccount\":\"user236781532\",\"acctRegTime\":\"2023-01-0112:00:00\",\"goodsId\":\"goods001\",\"type\":\"GT01\",\"price\":\"1000\",\"num\":\"2\",\"payCodeId\":\"paycode28367835\",\"name\":\"张三\",\"mobile\":\"13800138000\",\"province\":\"北京市\",\"city\":\"北京市\",\"county\":\"朝阳区\",\"address\":\"北京市朝阳区某某街道7号\"}}");
-        request.setAppUpIdentifier("appUpIdentifier_example");
-        request.setPreConsumeNotifyUrl("http://xxx.xxx.com/xxx");
-        try {
-            PrePayResponse response = api.prePay(request);
-            System.out.println("请求状态: " + response);
-        } catch (YopClientException e) {
-            System.out.println(e.getMessage());
-        }
-    }
-}

+ 1 - 0
fs-common/pom.xml

@@ -132,6 +132,7 @@
             <artifactId>mybatis-plus-boot-starter</artifactId>
             <version>3.1.0</version>
         </dependency>
+
         <!-- https://mvnrepository.com/artifact/org.springframework/spring-expression -->
         <dependency>
             <groupId>org.springframework</groupId>

+ 68 - 0
fs-common/src/main/java/com/fs/common/sharding/ShardingUtil.java

@@ -0,0 +1,68 @@
+package com.fs.common.sharding;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * fs_user_integral_logs 分表工具类
+ *
+ * <p>分表规则:user_id % {@link #SHARD_COUNT},物理表 fs_user_integral_logs_0 ~ fs_user_integral_logs_9</p>
+ *
+ * <p>使用场景:</p>
+ * <ul>
+ *   <li>INSERT/SELECT/UPDATE/DELETE 带 userId 时,调用 {@link #shardOf(Long)} 获取单表后缀</li>
+ *   <li>总后台等需要查全部数据时,调用 {@link #allShards()} 获取所有分表后缀</li>
+ * </ul>
+ *
+ * @author fs
+ */
+public final class ShardingUtil
+{
+    /** 分表总数 */
+    public static final int SHARD_COUNT = 10;
+
+    private ShardingUtil() {}
+
+    /**
+     * 根据 userId 计算分表后缀
+     *
+     * @param userId 用户ID(分片键)
+     * @return 分表后缀(0 ~ 9)
+     */
+    public static int shardOf(Long userId)
+    {
+        if (userId == null)
+        {
+            throw new IllegalArgumentException("userId 不能为空,无法计算分表");
+        }
+        return (int) (userId % SHARD_COUNT);
+    }
+
+    /**
+     * 获取所有分表后缀列表(0 ~ 9)
+     *
+     * @return 不可变分表后缀列表
+     */
+    public static List<Integer> allShards()
+    {
+        List<Integer> shards = new ArrayList<>(SHARD_COUNT);
+        for (int i = 0; i < SHARD_COUNT; i++)
+        {
+            shards.add(i);
+        }
+        return shards;
+    }
+
+    /**
+     * 根据 userId 计算分表后缀,返回单元素列表
+     * <p>便于与 {@link #allShards()} 统一传参给 UNION ALL 查询</p>
+     *
+     * @param userId 用户ID
+     * @return 单元素分表后缀列表
+     */
+    public static List<Integer> shardListOf(Long userId)
+    {
+        return Collections.singletonList(shardOf(userId));
+    }
+}

+ 49 - 0
fs-company/src/main/java/com/fs/company/controller/store/FsUserIntegralLogsController.java

@@ -0,0 +1,49 @@
+package com.fs.company.controller.store;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.his.domain.FsUserIntegralLogs;
+import com.fs.his.service.IFsUserIntegralLogsService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 销售端积分明细Controller
+ *
+ * <p>提供按订单 ID 查询积分明细的接口,供销售端订单详情页和售后详情页展示积分变动记录。
+ * 复用 fs-service 的 {@link IFsUserIntegralLogsService#selectByBusinessId(String)},
+ * 内部会广播到 fs_user_integral_logs 全部分表(_0~_9)合并结果。</p>
+ *
+ * @author fs
+ */
+@RestController
+@RequestMapping("/store/userIntegralLogs")
+public class FsUserIntegralLogsController extends BaseController
+{
+    @Autowired
+    private IFsUserIntegralLogsService fsUserIntegralLogsService;
+
+    /**
+     * 按订单 ID 查询积分明细列表
+     * <p>用于订单详情页/售后详情页展示该订单相关的全部积分变动(发放、抵扣、退回等)。
+     * 由于不含 user_id 分片键,内部会广播到全部分表合并结果,适用于低频详情查询。</p>
+     *
+     * @param orderId 订单 ID
+     * @return 积分明细列表,按 id 升序
+     */
+    @GetMapping("/getByOrderId/{orderId}")
+    public AjaxResult getByOrderId(@PathVariable("orderId") Long orderId)
+    {
+        if (orderId == null || orderId <= 0)
+        {
+            return AjaxResult.error("订单ID不能为空");
+        }
+        List<FsUserIntegralLogs> list = fsUserIntegralLogsService.selectByBusinessId(orderId.toString());
+        return AjaxResult.success(list);
+    }
+}

+ 1 - 3
fs-company/src/main/java/com/fs/framework/config/DataSourceConfig.java

@@ -34,13 +34,11 @@ public class DataSourceConfig {
         return new DruidDataSource();
     }
 
-
-
     @Bean
     @Primary
     public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("sopDataSource") DataSource sopDataSource) {
         Map<Object, Object> targetDataSources = new HashMap<>();
-        targetDataSources.put(DataSourceType.MASTER, masterDataSource);
+        targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
         targetDataSources.put(DataSourceType.SOP.name(), sopDataSource);
         return new DynamicDataSource(masterDataSource, targetDataSources);
     }

+ 1 - 1
fs-framework/src/main/java/com/fs/framework/config/DataSourceConfig.java

@@ -55,7 +55,7 @@ public class DataSourceConfig {
     @Primary
     public DynamicDataSource dataSource(@Qualifier("clickhouseDataSource") DataSource clickhouseDataSource, @Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("sopDataSource") DataSource sopDataSource, @Qualifier("masterSlaveDataSource") DataSource masterSlaveDataSource, @Qualifier("sopSlaveDataSource") DataSource sopSlaveDataSource) {
         Map<Object, Object> targetDataSources = new HashMap<>();
-        targetDataSources.put(DataSourceType.MASTER, masterDataSource);
+        targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
         targetDataSources.put(DataSourceType.SOP.name(), sopDataSource);
         targetDataSources.put(DataSourceType.CLICKHOUSE.name(), clickhouseDataSource);
         targetDataSources.put(DataSourceType.SLAVE.name(), masterSlaveDataSource);

+ 3 - 3
fs-redis/src/main/java/com/fs/framework/config/DataSourceConfig.java

@@ -35,12 +35,12 @@ public class DataSourceConfig {
         return new DruidDataSource();
     }
 
-
-
     @Bean
     @Primary
-    public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("sopDataSource") DataSource sopDataSource) {
+    public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
+                                        @Qualifier("sopDataSource") DataSource sopDataSource) {
         Map<Object, Object> targetDataSources = new HashMap<>();
+        targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
         targetDataSources.put(DataSourceType.SOP.name(), sopDataSource);
         return new DynamicDataSource(masterDataSource, targetDataSources);
     }

+ 3 - 1
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseOrderServiceImpl.java

@@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.fs.common.core.domain.R;
 import com.fs.common.exception.CustomException;
+import com.fs.common.sharding.ShardingUtil;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.StringUtils;
@@ -344,7 +345,8 @@ public class FsUserCourseOrderServiceImpl implements IFsUserCourseOrderService
                 logs.setLogType(9);
                 logs.setBusinessId(order.getOrderId().toString());
                 logs.setCreateTime(new Date());
-                fsUserIntegralLogsMapper.insertFsUserIntegralLogs(logs);
+                int shard = ShardingUtil.shardOf(order.getUserId());
+                fsUserIntegralLogsMapper.insertFsUserIntegralLogs(logs, shard);
                 return R.ok("兑换成功").put("order",order);
             } else {
                 return R.error("订单创建失败");

+ 3 - 2
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseServiceImpl.java

@@ -15,6 +15,7 @@ import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.sharding.ShardingUtil;
 import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.StringUtils;
@@ -378,7 +379,7 @@ public class FsUserCourseServiceImpl implements IFsUserCourseService
         }
         String json =configService.selectConfigByKey("his.integral");
         IntegralConfig config= JSONUtil.toBean(json,IntegralConfig.class);
-        Integer todayTotalIntegral= fsUserIntegralLogsMapper.selectTodayTotalIntegral(config.getIntegralTypeByOneDay(),param.getUserId());
+        Integer todayTotalIntegral= fsUserIntegralLogsMapper.selectTodayTotalIntegral(config.getIntegralTypeByOneDay(),param.getUserId(), ShardingUtil.shardOf(param.getUserId()));
         if (todayTotalIntegral==null){
             todayTotalIntegral=0;
         }
@@ -387,7 +388,7 @@ public class FsUserCourseServiceImpl implements IFsUserCourseService
         }
         int rate = (int)((double)param.getDuration() / video.getDuration() * 100);
         if (rate>=90){
-            FsUserIntegralLogs integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralByCourseVideo(param.getVideoId(),param.getUserId());
+            FsUserIntegralLogs integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralByCourseVideo(param.getVideoId(),param.getUserId(), ShardingUtil.shardOf(param.getUserId()));
             if (integralLogs!=null){
                 return R.error("该用户已获取此小节积分");
             }

+ 3 - 1
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -15,6 +15,7 @@ import com.fs.common.core.domain.entity.SysDictData;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.enums.BizResponseEnum;
 import com.fs.common.exception.CustomException;
+import com.fs.common.sharding.ShardingUtil;
 import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.StringUtils;
@@ -2062,7 +2063,8 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
         integralLogs.setLogType(17);
         integralLogs.setBusinessId(StringUtils.isNotEmpty(log.getLogId().toString()) ? log.getLogId().toString() : null);
         integralLogs.setCreateTime(new Date());
-        fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLogs);
+        int shard = ShardingUtil.shardOf(user.getUserId());
+        fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLogs, shard);
 
         //更新看课记录的奖励类型
         log.setRewardType(config.getRewardType());

+ 8 - 2
fs-service/src/main/java/com/fs/his/config/PointsGrantRuleConfig.java

@@ -37,12 +37,18 @@ public class PointsGrantRuleConfig implements Serializable {
     /** 用户每月积分获取上限(0=不限制) */
     private Integer monthlyLimit;
 
-    /** 订单最高抵扣积分(0=不限制):用户每笔订单最多可用积分抵扣的数量 */
-    private Integer maxDeduct;
+    /** 订单最高抵扣比例(0=不限制,50=最多抵扣订单金额的50%):防0元购买 */
+    private Integer maxDeductPercent;
 
     /** 满X元可抵扣(0=不限制):订单金额达到此阈值才可使用积分抵扣 */
     private BigDecimal minDeductAmount;
 
+    /** 最低起用积分(0=不限制):用户可用积分不足此值时不可抵扣 */
+    private Integer minUseIntegral;
+
+    /** 积分抵扣功能开关:0=关闭 1=开启 */
+    private Integer deductEnabled;
+
     /** 是否启用:0=禁用 1=启用 */
     private Integer enabled;
 }

+ 3 - 0
fs-service/src/main/java/com/fs/his/enums/FsUserIntegralLogTypeEnum.java

@@ -32,6 +32,9 @@ public enum FsUserIntegralLogTypeEnum {
     TYPE_22(22,"首次完成积分商城下单"),
     TYPE_23(23,"管理员添加"),
     TYPE_24(24, "付费课程订阅"),
+    TYPE_25(25, "下单使用积分抵扣"),
+    TYPE_26(26, "取消订单退回积分"),
+    TYPE_27(27, "退款订单退回积分"),
     ;
 
 

+ 96 - 53
fs-service/src/main/java/com/fs/his/mapper/FsUserIntegralLogsMapper.java

@@ -16,6 +16,9 @@ import java.util.List;
 /**
  * 积分记录Mapper接口
  *
+ * <p>手动分表:fs_user_integral_logs 按 user_id % 10 分为 _0 ~ _9 共 10 张物理表。
+ * 所有方法通过 {@code shard} 参数指定物理表后缀,由 Service 层通过 {@link com.fs.common.sharding.ShardingUtil} 计算。</p>
+ *
  * @author fs
  * @date 2023-11-02
  */
@@ -24,52 +27,59 @@ public interface FsUserIntegralLogsMapper
     /**
      * 查询积分记录
      *
-     * @param id 积分记录主键
+     * @param id   积分记录主键
+     * @param shard 分表后缀(0~9)
      * @return 积分记录
      */
-    public FsUserIntegralLogs selectFsUserIntegralLogsById(Long id);
+    FsUserIntegralLogs selectFsUserIntegralLogsById(@Param("id") Long id, @Param("shard") int shard);
 
     /**
      * 查询积分记录列表
      *
-     * @param fsUserIntegralLogs 积分记录
+     * @param log   查询条件
+     * @param shard 分表后缀(0~9)
      * @return 积分记录集合
      */
-    public List<FsUserIntegralLogs> selectFsUserIntegralLogsList(FsUserIntegralLogs fsUserIntegralLogs);
+    List<FsUserIntegralLogs> selectFsUserIntegralLogsList(@Param("log") FsUserIntegralLogs log, @Param("shard") int shard);
 
     /**
      * 新增积分记录
      *
-     * @param fsUserIntegralLogs 积分记录
+     * @param log   积分记录
+     * @param shard 分表后缀(0~9)
      * @return 结果
      */
-    public int insertFsUserIntegralLogs(FsUserIntegralLogs fsUserIntegralLogs);
+    int insertFsUserIntegralLogs(@Param("log") FsUserIntegralLogs log, @Param("shard") int shard);
 
     /**
      * 修改积分记录
      *
-     * @param fsUserIntegralLogs 积分记录
+     * @param log   积分记录
+     * @param shard 分表后缀(0~9)
      * @return 结果
      */
-    public int updateFsUserIntegralLogs(FsUserIntegralLogs fsUserIntegralLogs);
+    int updateFsUserIntegralLogs(@Param("log") FsUserIntegralLogs log, @Param("shard") int shard);
 
     /**
      * 删除积分记录
      *
-     * @param id 积分记录主键
+     * @param id    积分记录主键
+     * @param shard 分表后缀(0~9)
      * @return 结果
      */
-    public int deleteFsUserIntegralLogsById(Long id);
+    int deleteFsUserIntegralLogsById(@Param("id") Long id, @Param("shard") int shard);
 
     /**
      * 批量删除积分记录
      *
-     * @param ids 需要删除的数据主键集合
+     * @param ids   需要删除的数据主键集合
+     * @param shard 分表后缀(0~9)
      * @return 结果
      */
-    public int deleteFsUserIntegralLogsByIds(Long[] ids);
+    int deleteFsUserIntegralLogsByIds(@Param("ids") Long[] ids, @Param("shard") int shard);
+
     @Select({"<script> " +
-            "select l.*,u.nick_name,u.phone from fs_user_integral_logs l LEFT JOIN fs_user u ON u.user_id=l.user_id "+
+            "select l.*,u.nick_name,u.phone from fs_user_integral_logs_${shard} l LEFT JOIN fs_user u ON u.user_id=l.user_id "+
             " <where>  \n" +
             "            <if test=\"userId != null \"> and l.user_id = #{userId}</if>\n" +
             "            <if test=\"logType != null  and logType != ''\"> and log_type = #{logType}</if>\n" +
@@ -80,9 +90,10 @@ public interface FsUserIntegralLogsMapper
             "            <if test=\"eTime != null \">  and DATE(l.create_time) &lt;= DATE(#{eTime})</if>\n" +
             "        </where> order by id desc"+
             "</script>"})
-    List<FsUserIntegralLogsListVO> selectFsUserIntegralLogsListVO(FsUserIntegralLogsParam fsUserIntegralLogs);
+    List<FsUserIntegralLogsListVO> selectFsUserIntegralLogsListVO(@Param("fsUserIntegralLogs") FsUserIntegralLogsParam fsUserIntegralLogs, @Param("shard") int shard);
+
     @Select({"<script> " +
-            "select l.*  from fs_user_integral_logs l  " +
+            "select l.*  from fs_user_integral_logs_${shard} l  " +
             "where 1=1  " +
             "<if test = 'maps.userId != null       '> " +
             "and l.user_id = #{maps.userId}  " +
@@ -95,70 +106,102 @@ public interface FsUserIntegralLogsMapper
             "</if>" +
             " order by l.id desc "+
             "</script>"})
-    List<FsUserIntegralLogsListUVO> selectFsUserIntegralLogsListUVO(@Param("maps")FsUserIntegralLogsListUParam param);
-    @Select("select user_id  from fs_user_integral_logs WHERE DATE_FORMAT(create_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m') and log_type=2 and `status`=0  and business_type=2 GROUP BY user_id HAVING count(user_id) > 5;")
-    List<Long> subFsUserIntegralLogsByOrder5();
+    List<FsUserIntegralLogsListUVO> selectFsUserIntegralLogsListUVO(@Param("maps") FsUserIntegralLogsListUParam param, @Param("shard") int shard);
 
-    @Select("select * from fs_user_integral_logs where user_id = #{id} AND `status`=0 and DATE_FORMAT(create_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m') and ((business_type=2 and log_type=2) or log_type=6)")
-    List<FsUserIntegralLogs> selectFsUserIntegralByStatus0(Long id);
+    @Select("select user_id from fs_user_integral_logs_${shard} WHERE DATE_FORMAT(create_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m') and log_type=2 and `status`=0 and business_type=2 GROUP BY user_id HAVING count(user_id) > 5")
+    List<Long> subFsUserIntegralLogsByOrder5(@Param("shard") int shard);
 
-    @Select("select * from fs_user_integral_logs where log_type=10 and user_id = #{userId} and business_id = #{videoId} limit 1")
-    FsUserIntegralLogs selectFsUserIntegralByCourseVideo(@Param("videoId") Long videoId,@Param("userId")Long userId);
+    @Select("select * from fs_user_integral_logs_${shard} where user_id = #{id} AND `status`=0 and DATE_FORMAT(create_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m') and ((business_type=2 and log_type=2) or log_type=6)")
+    List<FsUserIntegralLogs> selectFsUserIntegralByStatus0(@Param("id") Long id, @Param("shard") int shard);
 
-    @Select("select * from fs_user_integral_logs where log_type=11 and user_id = #{userId} limit 1")
-    FsUserIntegralLogs selectFsUserIntegralLogsAddPatient(@Param("userId")Long userId);
+    @Select("select * from fs_user_integral_logs_${shard} where log_type=10 and user_id = #{userId} and business_id = #{videoId} limit 1")
+    FsUserIntegralLogs selectFsUserIntegralByCourseVideo(@Param("videoId") Long videoId, @Param("userId") Long userId, @Param("shard") int shard);
 
+    @Select("select * from fs_user_integral_logs_${shard} where log_type=11 and user_id = #{userId} limit 1")
+    FsUserIntegralLogs selectFsUserIntegralLogsAddPatient(@Param("userId") Long userId, @Param("shard") int shard);
 
-    @Select("select * from fs_user_integral_logs where log_type=12 and user_id = #{userId} limit 1")
-    FsUserIntegralLogs selectFsUserIntegralLogsAddUserAddress(@Param("userId")Long userId);
+    @Select("select * from fs_user_integral_logs_${shard} where log_type=12 and user_id = #{userId} limit 1")
+    FsUserIntegralLogs selectFsUserIntegralLogsAddUserAddress(@Param("userId") Long userId, @Param("shard") int shard);
 
     @Select({"<script> " +
-            "SELECT SUM(integral) AS todayTotalIntegral FROM fs_user_integral_logs " +
+            "SELECT SUM(integral) AS todayTotalIntegral FROM fs_user_integral_logs_${shard} " +
             "WHERE user_id = #{userId} AND log_type IN " +
             "<foreach item='type' collection='types' open='(' separator=',' close=')'> " +
             "            #{type} " +
             "</foreach>"+
             " AND DATE(create_time) = CURDATE()"+
             "</script>"})
-    Integer selectTodayTotalIntegral(@Param("types") Integer[] integralTypeByOneDay, @Param("userId") Long userId);
+    Integer selectTodayTotalIntegral(@Param("types") Integer[] integralTypeByOneDay, @Param("userId") Long userId, @Param("shard") int shard);
 
-    @Select("select count(1) from fs_user_integral_logs where log_type = 14 and user_id =#{userId} AND DATE(create_time) = CURDATE()")
-    Integer selectTodayVideoIntegralCount(@Param("userId") Long userId);
+    @Select("select count(1) from fs_user_integral_logs_${shard} where log_type = 14 and user_id =#{userId} AND DATE(create_time) = CURDATE()")
+    Integer selectTodayVideoIntegralCount(@Param("userId") Long userId, @Param("shard") int shard);
 
-    @Select("SELECT user_id, COUNT(*) AS num \n" +
-            "FROM fs_user_integral_logs\n" +
-            "WHERE log_type = 12\n" +
-            "GROUP BY user_id\n" )
-    List<SubIntegralVO> selectSubIntegralByLogType12();
+    @Select("SELECT user_id, COUNT(*) AS num FROM fs_user_integral_logs_${shard} WHERE log_type = 12 GROUP BY user_id")
+    List<SubIntegralVO> selectSubIntegralByLogType12(@Param("shard") int shard);
 
-    @Delete("delete from fs_user_integral_logs where log_type = #{logType} and user_id = #{userId}")
-    void deleteFsUserIntegralLogsByType(@Param("logType") int i,@Param("userId") Long userId);
+    @Delete("delete from fs_user_integral_logs_${shard} where log_type = #{logType} and user_id = #{userId}")
+    void deleteFsUserIntegralLogsByType(@Param("logType") int logType, @Param("userId") Long userId, @Param("shard") int shard);
 
+    @Select("select count(1) from fs_user_integral_logs_${shard} where log_type = 16 and user_id =#{userId} and business_id = #{videoId} ")
+    Long selectH5VideoIntegralCount(@Param("userId") Long userId, @Param("videoId") Long videoId, @Param("shard") int shard);
 
-    @Select("select count(1) from fs_user_integral_logs where log_type = 16 and user_id =#{userId} and business_id = #{videoId} ")
-    Long selectH5VideoIntegralCount(@Param("userId") Long userId,@Param("videoId") Long videoId);
-
-    List<FsUserIntegralLogs> selectFsUserIntegralLogsByUserIdAndLogType(@Param("userId") Long userId, @Param("logType") Integer logType, @Param("date") LocalDate date);
+    List<FsUserIntegralLogs> selectFsUserIntegralLogsByUserIdAndLogType(@Param("userId") Long userId, @Param("logType") Integer logType, @Param("date") LocalDate date, @Param("shard") int shard);
 
     /**
      * 查询用户当月指定类型的积分发放总额(仅统计正向积分,按月汇总)
-     *
-     * @param userId  用户ID
-     * @param logType 积分类型
-     * @return 当月累计积分总额,无记录返回0
      */
-    @Select("SELECT IFNULL(SUM(integral), 0) FROM fs_user_integral_logs WHERE user_id = #{userId} AND log_type = #{logType} AND integral > 0 AND DATE_FORMAT(create_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')")
-    Long selectMonthlyTotalIntegral(@Param("userId") Long userId, @Param("logType") Integer logType);
+    @Select("SELECT IFNULL(SUM(integral), 0) FROM fs_user_integral_logs_${shard} WHERE user_id = #{userId} AND log_type = #{logType} AND integral > 0 AND DATE_FORMAT(create_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')")
+    Long selectMonthlyTotalIntegral(@Param("userId") Long userId, @Param("logType") Integer logType, @Param("shard") int shard);
 
     /**
      * 根据用户ID、订单号、积分类型查询原始积分发放记录
      * 用于退款扣回场景:通过原订单号找到发放记录,按比例扣除
+     */
+    @Select("SELECT * FROM fs_user_integral_logs_${shard} WHERE user_id = #{userId} AND business_id = #{businessId} AND log_type = #{logType} ORDER BY id DESC LIMIT 1")
+    FsUserIntegralLogs selectByUserIdAndBusinessId(@Param("userId") Long userId, @Param("businessId") String businessId, @Param("logType") Integer logType, @Param("shard") int shard);
+
+    /**
+     * 按订单号查询该订单的所有积分明细记录(不限定 logType)
+     * <p>不含 user_id 条件,需在 Service 层广播到全部分表合并结果。</p>
+     */
+    @Select("SELECT * FROM fs_user_integral_logs_${shard} WHERE business_id = #{businessId} ORDER BY id ASC")
+    List<FsUserIntegralLogs> selectByBusinessId(@Param("businessId") String businessId, @Param("shard") int shard);
+
+    /**
+     * 按用户ID + 订单号查询该订单的所有积分明细记录(不限定 logType)
      *
-     * @param userId     用户ID
-     * @param businessId 原订单号
-     * @param logType    积分类型(2=消费获得积分)
-     * @return 原始积分流水记录
+     * @param userId     用户ID(分片键)
+     * @param businessId 订单号
+     * @param shard      分表后缀(0~9)
+     * @return 积分明细列表,按 id 升序
+     */
+    @Select("SELECT * FROM fs_user_integral_logs_${shard} WHERE user_id = #{userId} AND business_id = #{businessId} ORDER BY id ASC")
+    List<FsUserIntegralLogs> selectByUserIdAndBusinessIdAll(@Param("userId") Long userId, @Param("businessId") String businessId, @Param("shard") int shard);
+
+    /**
+     * 总后台跨分表分页查询积分明细列表
+     * <p>显式 UNION ALL 命中的物理分表,外层关联 fs_user。</p>
+     *
+     * @param shards   命中的分表后缀列表
+     * @param param    查询条件
+     * @param offset   LIMIT 偏移量
+     * @param pageSize 每页条数
+     * @return 积分明细列表
+     */
+    List<FsUserIntegralLogsListVO> selectListAllShards(@Param("shards") List<Integer> shards,
+                                                       @Param("param") FsUserIntegralLogsParam param,
+                                                       @Param("offset") int offset,
+                                                       @Param("pageSize") int pageSize);
+
+    /**
+     * 总后台跨分表查询总数
+     */
+    long selectCountAllShards(@Param("shards") List<Integer> shards,
+                              @Param("param") FsUserIntegralLogsParam param);
+
+    /**
+     * 总后台跨分表全量查询(用于导出,不分页)
      */
-    @Select("SELECT * FROM fs_user_integral_logs WHERE user_id = #{userId} AND business_id = #{businessId} AND log_type = #{logType} ORDER BY id DESC LIMIT 1")
-    FsUserIntegralLogs selectByUserIdAndBusinessId(@Param("userId") Long userId, @Param("businessId") String businessId, @Param("logType") Integer logType);
+    List<FsUserIntegralLogsListVO> selectListAllShardsAll(@Param("shards") List<Integer> shards,
+                                                          @Param("param") FsUserIntegralLogsParam param);
 }

+ 51 - 0
fs-service/src/main/java/com/fs/his/service/IFsUserIntegralLogsService.java

@@ -1,6 +1,7 @@
 package com.fs.his.service;
 
 import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
 import com.fs.his.domain.FsUserIntegralLogs;
 import com.fs.his.param.FsUserAddIntegralParam;
 import com.fs.his.param.FsUserAddIntegralTemplateParam;
@@ -106,4 +107,54 @@ public interface IFsUserIntegralLogsService
      * @return R.ok("发放成功") 或 R.error("失败原因")
      */
     R grantPoints(FsUserGrantPointsParam param);
+
+    /**
+     * 按订单号查询该订单的所有积分明细记录(不限定 logType)
+     * 用于订单详情页展示该订单相关的全部积分变动(发放、抵扣、退回等)
+     * <p>注意:此方法不含 user_id,ShardingSphere 会广播到全部分表,仅适用于低频管理端查询。</p>
+     *
+     * @param businessId 订单 ID(字符串形式)
+     * @return 积分明细列表,按 id 升序
+     */
+    List<FsUserIntegralLogs> selectByBusinessId(String businessId);
+
+    /**
+     * 按用户ID + 订单号查询该订单的所有积分明细记录(不限定 logType)
+     * <p>包含 user_id 分片键,ShardingSphere 精确路由到对应物理表,适用于高频退款等业务场景。</p>
+     *
+     * @param userId     用户ID(分片键)
+     * @param businessId 订单 ID(字符串形式)
+     * @return 积分明细列表,按 id 升序
+     */
+    List<FsUserIntegralLogs> selectByUserIdAndBusinessIdAll(Long userId, String businessId);
+
+    /**
+     * 插入积分日志(独立事务,供外部 @Transactional 方法跨数据源调用)
+     * <p>内部使用 REQUIRES_NEW + @DataSource(SHARDING),确保积分日志写入分表。</p>
+     *
+     * @param logs 积分日志记录
+     * @return 影响行数
+     */
+    int insertLog(FsUserIntegralLogs logs);
+
+    /**
+     * 总后台分页查询积分明细(跨全部分表)
+     * <p>显式 UNION ALL 物理分表,走 MASTER 数据源,不依赖 ShardingSphere 路由,
+     * 避免 PageHelper 与 ShardingSphere 兼容性问题。</p>
+     * <p>智能分片路由:查询条件含 userId 时精确路由到单表(userId%10),否则查全部 10 张表。</p>
+     *
+     * @param param    查询条件
+     * @param pageNum  当前页码(从请求参数获取)
+     * @param pageSize 每页条数(从请求参数获取)
+     * @return 分页结果
+     */
+    TableDataInfo selectListAllShards(FsUserIntegralLogsParam param, Integer pageNum, Integer pageSize);
+
+    /**
+     * 总后台全量查询积分明细(跨全部分表,用于导出)
+     *
+     * @param param 查询条件
+     * @return 积分明细列表
+     */
+    List<FsUserIntegralLogsListVO> selectListAllShardsAll(FsUserIntegralLogsParam param);
 }

+ 5 - 3
fs-service/src/main/java/com/fs/his/service/impl/FsIntegralOrderServiceImpl.java

@@ -15,6 +15,7 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.exception.CustomException;
 import com.fs.common.exception.ServiceException;
+import com.fs.common.sharding.ShardingUtil;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
@@ -309,7 +310,8 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
                 logs.setLogType(5);
                 logs.setBusinessId(order.getOrderId().toString());
                 logs.setCreateTime(new Date());
-                fsUserIntegralLogsMapper.insertFsUserIntegralLogs(logs);
+                int shard = ShardingUtil.shardOf(order.getUserId());
+                fsUserIntegralLogsMapper.insertFsUserIntegralLogs(logs, shard);
             }
 
             // 积分支付
@@ -498,7 +500,7 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
         logs.setLogType(5);
         logs.setBusinessId(order.getOrderId().toString());
         logs.setCreateTime(new Date());
-        fsUserIntegralLogsMapper.insertFsUserIntegralLogs(logs);
+        userIntegralLogsService.insertLog(logs);
 
         return R.ok();
     }
@@ -647,7 +649,7 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
             fsUserIntegralLogs.setIntegral(Long.parseLong(fsIntegralOrder.getIntegral()));
             fsUserIntegralLogs.setBusinessType(2);
             fsUserIntegralLogs.setStatus(0);
-            i = fsUserIntegralLogsMapper.insertFsUserIntegralLogs(fsUserIntegralLogs);
+            i = userIntegralLogsService.insertLog(fsUserIntegralLogs);
         }
         return i;
     }

+ 237 - 159
fs-service/src/main/java/com/fs/his/service/impl/FsUserIntegralLogsServiceImpl.java

@@ -2,6 +2,9 @@ package com.fs.his.service.impl;
 
 import cn.hutool.json.JSONUtil;
 import com.fs.common.core.domain.R;
+import com.fs.common.constant.HttpStatus;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.sharding.ShardingUtil;
 import com.fs.common.utils.DateUtils;
 import com.fs.his.config.IntegralConfig;
 import com.fs.his.config.PointsGrantRuleConfig;
@@ -29,16 +32,21 @@ import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.time.LocalDate;
 import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
 
 /**
  * 积分记录Service业务层处理
  *
+ * <p>手动分表:fs_user_integral_logs 按 user_id % 10 路由到 _0~_9 物理表。
+ * Service 层通过 {@link ShardingUtil#shardOf(Long)} 计算分表后缀,传给 Mapper。</p>
+ *
  * @author fs
  * @date 2023-11-02
  */
@@ -67,133 +75,147 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
     private static final String INTEGRAL_MONTHLY_KEY = "integral:grant:monthly:%d:%s";
 
     /**
-     * 查询积分记录
-     *
-     * @param id 积分记录主键
-     * @return 积分记录
+     * 查询积分记录(按 id 广播全部分表,低频管理端查询)
      */
     @Override
     public FsUserIntegralLogs selectFsUserIntegralLogsById(Long id)
     {
-        return fsUserIntegralLogsMapper.selectFsUserIntegralLogsById(id);
+        for (int shard : ShardingUtil.allShards())
+        {
+            FsUserIntegralLogs result = fsUserIntegralLogsMapper.selectFsUserIntegralLogsById(id, shard);
+            if (result != null)
+            {
+                return result;
+            }
+        }
+        return null;
     }
 
     /**
-     * 查询积分记录列表
-     *
-     * @param fsUserIntegralLogs 积分记录
-     * @return 积分记录
+     * 查询积分记录列表(按 userId 精确路由到单表)
      */
     @Override
     public List<FsUserIntegralLogs> selectFsUserIntegralLogsList(FsUserIntegralLogs fsUserIntegralLogs)
     {
-        return fsUserIntegralLogsMapper.selectFsUserIntegralLogsList(fsUserIntegralLogs);
+        int shard = ShardingUtil.shardOf(fsUserIntegralLogs.getUserId());
+        return fsUserIntegralLogsMapper.selectFsUserIntegralLogsList(fsUserIntegralLogs, shard);
     }
 
     /**
-     * 新增积分记录
-     *
-     * @param fsUserIntegralLogs 积分记录
-     * @return 结果
+     * 新增积分记录(按 userId 精确路由到单表)
      */
     @Override
     public int insertFsUserIntegralLogs(FsUserIntegralLogs fsUserIntegralLogs)
     {
         fsUserIntegralLogs.setCreateTime(DateUtils.getNowDate());
-        return fsUserIntegralLogsMapper.insertFsUserIntegralLogs(fsUserIntegralLogs);
+        int shard = ShardingUtil.shardOf(fsUserIntegralLogs.getUserId());
+        return fsUserIntegralLogsMapper.insertFsUserIntegralLogs(fsUserIntegralLogs, shard);
     }
 
     /**
-     * 修改积分记录
-     *
-     * @param fsUserIntegralLogs 积分记录
-     * @return 结果
+     * 修改积分记录(按 userId 精确路由到单表)
      */
     @Override
     public int updateFsUserIntegralLogs(FsUserIntegralLogs fsUserIntegralLogs)
     {
-        return fsUserIntegralLogsMapper.updateFsUserIntegralLogs(fsUserIntegralLogs);
+        int shard = ShardingUtil.shardOf(fsUserIntegralLogs.getUserId());
+        return fsUserIntegralLogsMapper.updateFsUserIntegralLogs(fsUserIntegralLogs, shard);
     }
 
     /**
-     * 批量删除积分记录
-     *
-     * @param ids 需要删除的积分记录主键
-     * @return 结果
+     * 批量删除积分记录(广播全部分表)
      */
     @Override
     public int deleteFsUserIntegralLogsByIds(Long[] ids)
     {
-        return fsUserIntegralLogsMapper.deleteFsUserIntegralLogsByIds(ids);
+        int count = 0;
+        for (int shard : ShardingUtil.allShards())
+        {
+            count += fsUserIntegralLogsMapper.deleteFsUserIntegralLogsByIds(ids, shard);
+        }
+        return count;
     }
 
     /**
-     * 删除积分记录信息
-     *
-     * @param id 积分记录主键
-     * @return 结果
+     * 删除积分记录信息(广播全部分表)
      */
     @Override
     public int deleteFsUserIntegralLogsById(Long id)
     {
-        return fsUserIntegralLogsMapper.deleteFsUserIntegralLogsById(id);
+        for (int shard : ShardingUtil.allShards())
+        {
+            int count = fsUserIntegralLogsMapper.deleteFsUserIntegralLogsById(id, shard);
+            if (count > 0)
+            {
+                return count;
+            }
+        }
+        return 0;
     }
 
     @Override
     public List<FsUserIntegralLogsListVO> selectFsUserIntegralLogsListVO(FsUserIntegralLogsParam fsUserIntegralLogs) {
-        return fsUserIntegralLogsMapper.selectFsUserIntegralLogsListVO(fsUserIntegralLogs);
+        int shard = ShardingUtil.shardOf(fsUserIntegralLogs.getUserId());
+        return fsUserIntegralLogsMapper.selectFsUserIntegralLogsListVO(fsUserIntegralLogs, shard);
     }
 
     @Override
     public List<FsUserIntegralLogsListUVO> selectFsUserIntegralLogsListUVO(FsUserIntegralLogsListUParam param) {
-        return fsUserIntegralLogsMapper.selectFsUserIntegralLogsListUVO(param);
+        int shard = ShardingUtil.shardOf(param.getUserId());
+        return fsUserIntegralLogsMapper.selectFsUserIntegralLogsListUVO(param, shard);
     }
 
     @Override
     public void subFsUserIntegralLogsByOrder5() {
-        List<Long> longs = fsUserIntegralLogsMapper.subFsUserIntegralLogsByOrder5();
-        for (Long id : longs) {
-
-            FsUser fsUser = fsUserMapper.selectFsUserByUserId(id);
-            if(fsUser!=null&&fsUser.getIntegral()!=0){
-                List<FsUserIntegralLogs> log =  fsUserIntegralLogsMapper.selectFsUserIntegralByStatus0(id);
-                for (FsUserIntegralLogs fsUserIntegralLogs : log) {
-                    Long integral = fsUser.getIntegral();
-                    Long integralLog = fsUserIntegralLogs.getIntegral();
-                    Long balance = integral - integralLog;
-                    if (balance<0){
-                        balance=0L;
+        // 广播全部分表查询符合条件的 userId
+        for (int shard : ShardingUtil.allShards())
+        {
+            List<Long> longs = fsUserIntegralLogsMapper.subFsUserIntegralLogsByOrder5(shard);
+            for (Long id : longs) {
+
+                FsUser fsUser = fsUserMapper.selectFsUserByUserId(id);
+                if(fsUser!=null&&fsUser.getIntegral()!=0){
+                    int userShard = ShardingUtil.shardOf(id);
+                    List<FsUserIntegralLogs> log =  fsUserIntegralLogsMapper.selectFsUserIntegralByStatus0(id, userShard);
+                    for (FsUserIntegralLogs fsUserIntegralLogs : log) {
+                        Long integral = fsUser.getIntegral();
+                        Long integralLog = fsUserIntegralLogs.getIntegral();
+                        Long balance = integral - integralLog;
+                        if (balance<0){
+                            balance=0L;
+                        }
+                        fsUser.setIntegral(balance);
+                        fsUserMapper.updateFsUser(fsUser);
+                        fsUserIntegralLogs.setStatus(1);
+                        fsUserIntegralLogsMapper.updateFsUserIntegralLogs(fsUserIntegralLogs, userShard);
+                        FsUserIntegralLogs logs = new FsUserIntegralLogs();
+                        logs.setIntegral(0-integralLog);
+                        logs.setUserId(id);
+                        logs.setBalance(balance);
+                        logs.setLogType(7);
+                        logs.setStatus(1);
+                        logs.setCreateTime(new Date());
+                        fsUserIntegralLogsMapper.insertFsUserIntegralLogs(logs, userShard);
                     }
-                    fsUser.setIntegral(balance);
-                    fsUserMapper.updateFsUser(fsUser);
-                    fsUserIntegralLogs.setStatus(1);
-                    fsUserIntegralLogsMapper.updateFsUserIntegralLogs(fsUserIntegralLogs);
-                    FsUserIntegralLogs logs = new FsUserIntegralLogs();
-                    logs.setIntegral(0-integralLog);
-                    logs.setUserId(id);
-                    logs.setBalance(balance);
-                    logs.setLogType(7);
-                    logs.setStatus(1);
-                    logs.setCreateTime(new Date());
-                    fsUserIntegralLogsMapper.insertFsUserIntegralLogs(logs);
-                }
 
 
-            }
+                }
 
+            }
         }
     }
 
     @Override
     @Transactional
     public void addUserAddressIntegral(Long userId, Long addressId) {
+        int shard = ShardingUtil.shardOf(userId);
         FsUser user = fsUserMapper.selectFsUserIsNew(userId);
         if (user!=null){
-            FsUserIntegralLogs integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsAddUserAddress(userId);
+            FsUserIntegralLogs integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsAddUserAddress(userId, shard);
             if (integralLogs==null){
                 FsUser userMap=new FsUser();
                 userMap.setUserId(user.getUserId());
-//                userMap.setIntegral(user.getIntegral()+1000L);
+                userMap.setIntegral(user.getIntegral()+1000L);
                 fsUserMapper.updateFsUser(userMap);
                 integralLogs = new FsUserIntegralLogs();
                 integralLogs.setIntegral(1000L);
@@ -202,7 +224,7 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
                 integralLogs.setLogType(12);
                 integralLogs.setBusinessId(addressId.toString());
                 integralLogs.setCreateTime(new Date());
-                fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLogs);
+                fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLogs, shard);
             }
         }
     }
@@ -216,7 +238,8 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
             IntegralConfig config= JSONUtil.toBean(json,IntegralConfig.class);
             System.out.println("Types: " + Arrays.toString(config.getIntegralTypeByOneDay()));
             System.out.println("User ID: " + param.getUserId());
-            Integer todayTotalIntegral= fsUserIntegralLogsMapper.selectTodayTotalIntegral(config.getIntegralTypeByOneDay(),param.getUserId());
+            int shard = ShardingUtil.shardOf(param.getUserId());
+            Integer todayTotalIntegral= fsUserIntegralLogsMapper.selectTodayTotalIntegral(config.getIntegralTypeByOneDay(),param.getUserId(), shard);
             if (todayTotalIntegral==null){
                 todayTotalIntegral=0;
             }
@@ -248,11 +271,12 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
             Integer[] integralTypeByOneDay = config.getIntegralTypeByOneDay();
             System.out.println("Types: " + Arrays.toString(integralTypeByOneDay));
             System.out.println("User ID: " + param.getUserId());
+            int shard = ShardingUtil.shardOf(param.getUserId());
 
             //判断是否在限额类型内
             if (integralTypeByOneDay != null) {
                 if (Arrays.asList(integralTypeByOneDay).contains(param.getLogType())){
-                    Integer todayTotalIntegral= fsUserIntegralLogsMapper.selectTodayTotalIntegral(integralTypeByOneDay,param.getUserId());
+                    Integer todayTotalIntegral= fsUserIntegralLogsMapper.selectTodayTotalIntegral(integralTypeByOneDay,param.getUserId(), shard);
                     if (todayTotalIntegral==null){
                         todayTotalIntegral=0;
                     }
@@ -261,58 +285,24 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
                     }
                 }
             }
-//            if (Arrays.asList(integralTypeByOneDay).contains(param.getLogType())){
-//                Integer todayTotalIntegral= fsUserIntegralLogsMapper.selectTodayTotalIntegral(integralTypeByOneDay,param.getUserId());
-//                if (todayTotalIntegral==null){
-//                    todayTotalIntegral=0;
-//                }
-//                if (todayTotalIntegral>=config.getIntegralByOneDay()){
-//                    return R.error("当天积分已达限额");
-//                }
-//            }
             Integer integralNum = null;
             FsUserIntegralLogTypeEnum logsType = null;
             List<FsUserIntegralLogs> integralLogs;
             switch (param.getLogType()){
-//                case 1: //签到
-//                    integralNum = config.getIntegralInvite();
-//                    logsType = FsUserIntegralLogTypeEnum.TYPE_1;
-//                    break;
                 case 2: //消费获得积分
                     integralNum = param.getPoints();
                     logsType = FsUserIntegralLogTypeEnum.TYPE_2;
                     break;
                 case 3: //分享获得积分
-                    integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(param.getUserId(),param.getLogType(), LocalDate.now());
+                    integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(param.getUserId(),param.getLogType(), LocalDate.now(), shard);
                     if (integralLogs.size() >= 5){
                         return R.error("今日看课积分已达上限");
                     }
                     integralNum = config.getIntegralShare();
                     logsType = FsUserIntegralLogTypeEnum.TYPE_3;
                     break;
-//                case 4: //退款扣除积分
-//                    integralNum = config.getIntegralInvite();
-//                    logsType = FsUserIntegralLogTypeEnum.TYPE_4;
-//                    break;
-//                case 5: // 购买积分商品
-//                    integralNum = config.getIntegralInvite();
-//                    logsType = FsUserIntegralLogTypeEnum.TYPE_5;
-//                    break;
-//                case 6: // 平台取消售后
-//                    integralNum = config.getIntegralInvite();
-//                    logsType = FsUserIntegralLogTypeEnum.TYPE_6;
-//                    break;
-//                case 7: //积分过期
-//                    logsType = FsUserIntegralLogTypeEnum.TYPE_7;
-//                    break;
-//                case 8: //螳螂同步积分
-//                    logsType = FsUserIntegralLogTypeEnum.TYPE_8;
-//                    break;
-//                case 9: //购买课程小节扣除
-//                    logsType = FsUserIntegralLogTypeEnum.TYPE_9;
-//                    break;
                 case 10: //观看课程获取积分
-                    integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(param.getUserId(),param.getLogType(), LocalDate.now());
+                    integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(param.getUserId(),param.getLogType(), LocalDate.now(), shard);
                     if (integralLogs.size() >= 3){
                         return R.error("今日看课积分已达上限");
                     }
@@ -320,7 +310,7 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
                     logsType = FsUserIntegralLogTypeEnum.TYPE_10;
                     break;
                 case 11: //新用户完善就诊人获得积分
-                    integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(param.getUserId(),param.getLogType(), null);
+                    integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(param.getUserId(),param.getLogType(), null, shard);
                     if (!integralLogs.isEmpty()){
                         return R.error("已领取过该积分");
                     }
@@ -328,7 +318,7 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
                     logsType = FsUserIntegralLogTypeEnum.TYPE_11;
                     break;
                 case 12: //新用户填写收货地址获取积分
-                    integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(param.getUserId(),param.getLogType(), null);
+                    integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(param.getUserId(),param.getLogType(), null, shard);
                     if (!integralLogs.isEmpty()){
                         return R.error("已领取过该积分");
                     }
@@ -341,21 +331,12 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
                     break;
                 case 14: //浏览视频获得积分
                     return addVideoIntegral(config.getIntegralFirstVideo(),config.getIntegralFinishVideo(),user.getUserId(),user.getIntegral());
-//                case 15: //首次下单疗法获得积分
-//                    logsType = FsUserIntegralLogTypeEnum.TYPE_15;
-//                    break;
-//                case 16: //点播看课获得积分
-//                    logsType = FsUserIntegralLogTypeEnum.TYPE_16;
-//                    break;
-//                case 17: //点播答题获得积分
-//                    logsType = FsUserIntegralLogTypeEnum.TYPE_17;
-//                    break;
                 case 18: //邀请新用户获取积分
                     integralNum = config.getIntegralInvite();
                     logsType = FsUserIntegralLogTypeEnum.TYPE_18;
                     break;
                 case 19: //填写邀请码获取积分
-                    integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(param.getUserId(),param.getLogType(), null);
+                    integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(param.getUserId(),param.getLogType(), null, shard);
                     if (!integralLogs.isEmpty()){
                         return R.error("已领取过该积分");
                     }
@@ -363,7 +344,7 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
                     logsType = FsUserIntegralLogTypeEnum.TYPE_19;
                     break;
                 case 20: //完成手机号绑定注册
-                    integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(param.getUserId(),param.getLogType(), null);
+                    integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(param.getUserId(),param.getLogType(), null, shard);
                     if (!integralLogs.isEmpty()){
                         return R.error("已领取过该积分");
                     }
@@ -371,7 +352,7 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
                     logsType = FsUserIntegralLogTypeEnum.TYPE_20;
                     break;
                 case 21: //完成专家咨询
-                    integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(param.getUserId(),param.getLogType(), null);
+                    integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(param.getUserId(),param.getLogType(), null, shard);
                     if (!integralLogs.isEmpty()){
                         return R.error("已领取过该积分");
                     }
@@ -379,7 +360,7 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
                     logsType = FsUserIntegralLogTypeEnum.TYPE_21;
                     break;
                 case 22: //完成首次积分商城下单
-                    integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(param.getUserId(),param.getLogType(), null);
+                    integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(param.getUserId(),param.getLogType(), null, shard);
                     if (!integralLogs.isEmpty()){
                         return R.error("已领取过该积分");
                     }
@@ -415,6 +396,7 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
      * @return
      */
     public R updateAddIntegral(Integer integralNum, Long userId, Long userIntegral, FsUserIntegralLogTypeEnum logsType, String businessId, String remark) {
+        int shard = ShardingUtil.shardOf(userId);
         FsUser userMap=new FsUser();
         userMap.setUserId(userId);
         userMap.setIntegral(userIntegral+integralNum);
@@ -427,14 +409,15 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
         integralLogs.setCreateTime(new Date());
         integralLogs.setBusinessId(businessId);
         integralLogs.setRemark(remark);
-        if (fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLogs)>0){
+        if (fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLogs, shard)>0){
             return R.ok(logsType.getDesc()+":"+integralNum+"积分");
         }
         return R.error("获取积分失败");
     }
 
     private R addVideoIntegral(Integer integralFirstVideo,Integer integralFinishVideo,Long userId,Long userIntegral) {
-        Integer videoIntegralCount = fsUserIntegralLogsMapper.selectTodayVideoIntegralCount(userId);
+        int shard = ShardingUtil.shardOf(userId);
+        Integer videoIntegralCount = fsUserIntegralLogsMapper.selectTodayVideoIntegralCount(userId, shard);
         Integer addIntegral = integralFirstVideo-videoIntegralCount;
         if (addIntegral<=integralFinishVideo){
             addIntegral=integralFinishVideo;
@@ -449,13 +432,14 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
         integralLogs.setBalance(userMap.getIntegral());
         integralLogs.setLogType(14);
         integralLogs.setCreateTime(new Date());
-        if (fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLogs)>0){
+        if (fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLogs, shard)>0){
             return R.ok("观看视频获得"+addIntegral+"积分");
         }
         return R.error("获取积分失败");
     }
 
     public R addProductIntegral(Integer integralProduct,Long userId,Long userIntegral) {
+        int shard = ShardingUtil.shardOf(userId);
         FsUser userMap=new FsUser();
         userMap.setUserId(userId);
         userMap.setIntegral(userIntegral+integralProduct);
@@ -466,7 +450,7 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
         integralLogs.setBalance(userMap.getIntegral());
         integralLogs.setLogType(13);
         integralLogs.setCreateTime(new Date());
-        if (fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLogs)>0){
+        if (fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLogs, shard)>0){
             return R.ok("浏览商品获得"+integralProduct+"积分");
         }
         return R.error("获取积分失败");
@@ -474,22 +458,26 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
 
     @Override
     public R subIntegral() {
-        List<SubIntegralVO> list  = fsUserIntegralLogsMapper.selectSubIntegralByLogType12();
-        for (SubIntegralVO vo : list){
-            FsUser user = fsUserMapper.selectFsUserByUserId(vo.getUserId());
-            if (user!=null){
-                FsUser userMap = new FsUser();
-                Long subIntegral = 1000 * vo.getNum();
-                Long integral = user.getIntegral()-subIntegral;
-                if (integral<0){
-                    userMap.setIntegral(0L);
-                }else {
-                    userMap.setIntegral(integral);
+        // 广播全部分表查询
+        for (int shard : ShardingUtil.allShards())
+        {
+            List<SubIntegralVO> list  = fsUserIntegralLogsMapper.selectSubIntegralByLogType12(shard);
+            for (SubIntegralVO vo : list){
+                FsUser user = fsUserMapper.selectFsUserByUserId(vo.getUserId());
+                if (user!=null){
+                    FsUser userMap = new FsUser();
+                    Long subIntegral = 1000 * vo.getNum();
+                    Long integral = user.getIntegral()-subIntegral;
+                    if (integral<0){
+                        userMap.setIntegral(0L);
+                    }else {
+                        userMap.setIntegral(integral);
+                    }
+                    userMap.setUserId(user.getUserId());
+                    fsUserMapper.updateFsUser(userMap);
                 }
-                userMap.setUserId(user.getUserId());
-                fsUserMapper.updateFsUser(userMap);
-            }
 
+            }
         }
         return R.ok();
     }
@@ -505,25 +493,7 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
         int isNewUser = 1;
         int isFinishConsultation = 0;
         int isFinishFirstOrderPoint = 0;
-//        long expireDays = 0;
         Date createTime = null;
-//        FsUser fsUser = fsUserMapper.selectFsUserIsNew(userId);
-//        if (fsUser!=null){
-//            isNewUser = 1;
-
-//            Date createTime = fsUser.getCreateTime(); // 获取用户创建时间
-//            LocalDateTime createDateTime = LocalDateTime.ofInstant(createTime.toInstant(), ZoneId.systemDefault()); // 将 Date 转换为 LocalDateTime
-//            LocalDateTime now = LocalDateTime.now(); // 获取当前时间
-//            long daysElapsed = ChronoUnit.DAYS.between(createDateTime, now); // 计算已经过去的天数
-//            long remainingDays = Math.max(0, 7 - daysElapsed); // 计算剩余天数,确保不为负数
-//
-//            // 输出剩余天数
-//            System.out.println("剩余新手期天数:" + remainingDays);
-//
-//            expireDays = remainingDays;
-
-
-//        }
         FsUserNewTask newTask = fsUserNewTaskMapper.selectFsUserNewTaskByUserId(userId);
         if (newTask!=null){
             taskOne = newTask.getTaskOne();
@@ -531,23 +501,24 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
             taskThree = newTask.getTaskThree();
             createTime = newTask.getCreateTime();
         }
-        FsUserIntegralLogs logs1 = fsUserIntegralLogsMapper.selectFsUserIntegralLogsAddPatient(userId);
+        int shard = ShardingUtil.shardOf(userId);
+        FsUserIntegralLogs logs1 = fsUserIntegralLogsMapper.selectFsUserIntegralLogsAddPatient(userId, shard);
         if (logs1!=null){
             taskFour = 1;
         }
-        FsUserIntegralLogs logs2 = fsUserIntegralLogsMapper.selectFsUserIntegralLogsAddUserAddress(userId);
+        FsUserIntegralLogs logs2 = fsUserIntegralLogsMapper.selectFsUserIntegralLogsAddUserAddress(userId, shard);
         if (logs2!=null){
             taskFive = 1;
         }
 
         // 完成专家咨询
-        List<FsUserIntegralLogs> integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(userId, FsUserIntegralLogTypeEnum.TYPE_21.getValue(), null);
+        List<FsUserIntegralLogs> integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(userId, FsUserIntegralLogTypeEnum.TYPE_21.getValue(), null, shard);
         if (!integralLogs.isEmpty()) {
             isFinishConsultation = 1;
         }
 
         // 完成首次积分商城下单
-        integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(userId, FsUserIntegralLogTypeEnum.TYPE_22.getValue(), null);
+        integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(userId, FsUserIntegralLogTypeEnum.TYPE_22.getValue(), null, shard);
         if (!integralLogs.isEmpty()) {
             isFinishFirstOrderPoint = 1;
         }
@@ -618,7 +589,7 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
                 // 计算发放积分:amount × rate,进位取整
                 integralCount = param.getAmount()
                         .multiply(rule.getRate() != null ? rule.getRate() : BigDecimal.ZERO)
-                        .setScale(0, BigDecimal.ROUND_CEILING)
+                        .setScale(0, RoundingMode.HALF_UP)
                         .longValue();
                 if (integralCount <= 0) {
                     log.info("计算积分 <= 0,跳过发放: userId={}, amount={}, rate={}", userId, param.getAmount(), rule.getRate());
@@ -634,8 +605,9 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
                 if (param.getOriginalOrderNo() == null || param.getOriginalOrderNo().isEmpty()) {
                     return R.error("退款扣回需提供原订单号(originalOrderNo)");
                 }
+                int refundShard = ShardingUtil.shardOf(userId);
                 FsUserIntegralLogs originalLog = fsUserIntegralLogsMapper.selectByUserIdAndBusinessId(
-                        userId, param.getOriginalOrderNo(), 2);
+                        userId, param.getOriginalOrderNo(), 2, refundShard);
                 if (originalLog == null || originalLog.getIntegral() == null || originalLog.getIntegral() <= 0) {
                     log.info("未找到原消费积分记录,无需扣除: userId={}, originalOrderNo={}", userId, param.getOriginalOrderNo());
                     return R.ok("未找到该订单的积分发放记录");
@@ -688,6 +660,7 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
             fsUserMapper.updateFsUser(updateUser);
 
             // 写入积分流水
+            int grantShard = ShardingUtil.shardOf(userId);
             FsUserIntegralLogs logRecord = new FsUserIntegralLogs();
             logRecord.setUserId(userId);
             logRecord.setLogType(logType);
@@ -696,7 +669,7 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
             logRecord.setBusinessId(param.getBusinessId());
             logRecord.setRemark(param.getRemark());
             logRecord.setCreateTime(new Date());
-            fsUserIntegralLogsMapper.insertFsUserIntegralLogs(logRecord);
+            fsUserIntegralLogsMapper.insertFsUserIntegralLogs(logRecord, grantShard);
 
             // 发放成功后无需累加 Redis 计数器(购买积分不设上限)
 
@@ -793,13 +766,14 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
             return Long.parseLong(value);
         }
         // Redis 无缓存 → DB 兜底
+        int shard = ShardingUtil.shardOf(userId);
         Long dbTotal;
         if (isDaily) {
             Integer result = fsUserIntegralLogsMapper.selectTodayTotalIntegral(
-                    new Integer[]{logType}, userId);
+                    new Integer[]{logType}, userId, shard);
             dbTotal = result != null ? result.longValue() : 0L;
         } else {
-            dbTotal = fsUserIntegralLogsMapper.selectMonthlyTotalIntegral(userId, logType);
+            dbTotal = fsUserIntegralLogsMapper.selectMonthlyTotalIntegral(userId, logType, shard);
         }
         // 回写 Redis(根据是日 or 月设置过期时间)
         stringRedisTemplate.opsForValue().set(key, String.valueOf(dbTotal),
@@ -822,4 +796,108 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
             log.warn("Redis计数器累加失败(不影响主流程): userId={}, logType={}", userId, logType, e);
         }
     }
+
+    /**
+     * 按订单号查询该订单的所有积分明细记录(不限定 logType)
+     * <p>不含 userId,需广播全部分表合并结果(低频管理端查询)</p>
+     */
+    @Override
+    public List<FsUserIntegralLogs> selectByBusinessId(String businessId) {
+        List<FsUserIntegralLogs> result = new ArrayList<>();
+        for (int shard : ShardingUtil.allShards())
+        {
+            result.addAll(fsUserIntegralLogsMapper.selectByBusinessId(businessId, shard));
+        }
+        return result;
+    }
+
+    /**
+     * 按用户ID + 订单号查询该订单的所有积分明细记录(不限定 logType)
+     * 包含 user_id 分片键,精确路由到对应物理表
+     */
+    @Override
+    public List<FsUserIntegralLogs> selectByUserIdAndBusinessIdAll(Long userId, String businessId) {
+        int shard = ShardingUtil.shardOf(userId);
+        return fsUserIntegralLogsMapper.selectByUserIdAndBusinessIdAll(userId, businessId, shard);
+    }
+
+    /**
+     * 插入积分日志(独立事务,供外部 @Transactional 方法调用)
+     * <p>使用 REQUIRES_NEW 传播级别,确保在调用方事务中也能正确写入分表。</p>
+     *
+     * @param logs 积分日志记录(需包含 userId 用于计算分表)
+     * @return 影响行数
+     */
+    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
+    @Override
+    public int insertLog(FsUserIntegralLogs logs) {
+        int shard = ShardingUtil.shardOf(logs.getUserId());
+        return fsUserIntegralLogsMapper.insertFsUserIntegralLogs(logs, shard);
+    }
+
+    /**
+     * 总后台分页查询积分明细(跨全部分表)
+     *
+     * <p>显式 UNION ALL 物理表名,智能分片路由:查询条件含 userId 时精确路由到单表,
+     * 无 userId 时查全部 10 张分表。手动分页,确保分页结果准确。</p>
+     *
+     * @param param    查询条件
+     * @param pageNum  当前页码
+     * @param pageSize 每页条数
+     * @return 分页结果
+     */
+    @Override
+    public TableDataInfo selectListAllShards(FsUserIntegralLogsParam param, Integer pageNum, Integer pageSize)
+    {
+        // 分页参数兜底
+        if (pageNum == null || pageNum < 1)
+        {
+            pageNum = 1;
+        }
+        if (pageSize == null || pageSize < 1)
+        {
+            pageSize = 10;
+        }
+        int offset = (pageNum - 1) * pageSize;
+
+        // 智能分片路由:有 userId 精确路由到单表,无 userId 查全部分表
+        List<Integer> shards = param.getUserId() != null
+                ? ShardingUtil.shardListOf(param.getUserId())
+                : ShardingUtil.allShards();
+
+        // 查询总数
+        long total = fsUserIntegralLogsMapper.selectCountAllShards(shards, param);
+
+        TableDataInfo table = new TableDataInfo();
+        table.setCode(HttpStatus.SUCCESS);
+        table.setMsg("查询成功");
+        table.setTotal(total);
+
+        // 总数为 0 时直接返回空列表,避免无意义查询
+        if (total == 0)
+        {
+            table.setRows(new ArrayList<>());
+            return table;
+        }
+
+        // 查询分页数据
+        List<FsUserIntegralLogsListVO> list = fsUserIntegralLogsMapper.selectListAllShards(shards, param, offset, pageSize);
+        table.setRows(list);
+        return table;
+    }
+
+    /**
+     * 总后台全量查询积分明细(跨全部分表,用于导出)
+     *
+     * @param param 查询条件
+     * @return 积分明细列表
+     */
+    @Override
+    public List<FsUserIntegralLogsListVO> selectListAllShardsAll(FsUserIntegralLogsParam param)
+    {
+        List<Integer> shards = param.getUserId() != null
+                ? ShardingUtil.shardListOf(param.getUserId())
+                : ShardingUtil.allShards();
+        return fsUserIntegralLogsMapper.selectListAllShardsAll(shards, param);
+    }
 }

+ 2 - 1
fs-service/src/main/java/com/fs/his/service/impl/FsUserNewTaskServiceImpl.java

@@ -5,6 +5,7 @@ import java.util.Date;
 import java.util.List;
 
 import cn.hutool.json.JSONUtil;
+import com.fs.common.sharding.ShardingUtil;
 import com.fs.common.core.domain.R;
 import com.fs.common.utils.DateUtils;
 import com.fs.his.config.CouponConfig;
@@ -213,7 +214,7 @@ public class FsUserNewTaskServiceImpl implements IFsUserNewTaskService
                 integralLogs.setLogType(15);
                 integralLogs.setBusinessId(orderId.toString());
                 integralLogs.setCreateTime(new Date());
-                fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLogs);
+                fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLogs, ShardingUtil.shardOf(userId));
             }
         }
     }

+ 9 - 5
fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java

@@ -18,6 +18,7 @@ import com.alibaba.fastjson.JSON;
 import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.fs.common.sharding.ShardingUtil;
 import com.fs.common.constant.HttpStatus;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.ResponseResult;
@@ -399,7 +400,8 @@ public class FsUserServiceImpl implements IFsUserService {
         if (fsUser != null) {
             String json = configService.selectConfigByKey("his.integral");
             IntegralConfig config = JSONUtil.toBean(json, IntegralConfig.class);
-            Integer todayTotalIntegral = integralLogsMapper.selectTodayTotalIntegral(config.getIntegralTypeByOneDay(), userId);
+            int shard = ShardingUtil.shardOf(userId);
+            Integer todayTotalIntegral = integralLogsMapper.selectTodayTotalIntegral(config.getIntegralTypeByOneDay(), userId, shard);
             if (todayTotalIntegral == null) {
                 todayTotalIntegral = 0;
             }
@@ -421,7 +423,7 @@ public class FsUserServiceImpl implements IFsUserService {
             logs.setLogType(6);
             logs.setBusinessId(orderId.toString());
             logs.setCreateTime(new Date());
-            integralLogsMapper.insertFsUserIntegralLogs(logs);
+            integralLogsMapper.insertFsUserIntegralLogs(logs, shard);
 
         }
     }
@@ -440,7 +442,7 @@ public class FsUserServiceImpl implements IFsUserService {
             logs.setBalance(0L);
             logs.setLogType(7);
             logs.setCreateTime(new Date());
-            integralLogsMapper.insertFsUserIntegralLogs(logs);
+            integralLogsMapper.insertFsUserIntegralLogs(logs, ShardingUtil.shardOf(userId));
         }
 
     }
@@ -466,11 +468,13 @@ public class FsUserServiceImpl implements IFsUserService {
     public void subUserIntegral(Long userId, Long orderId, Integer orderType) {
         FsUser fsUser = fsUserMapper.selectFsUserByUserId(userId);
         if (fsUser != null) {
+            int shard = ShardingUtil.shardOf(userId);
             FsUserIntegralLogs maps = new FsUserIntegralLogs();
+            maps.setUserId(userId);
             maps.setLogType(2);
             maps.setBusinessType(orderType);
             maps.setBusinessId(orderId.toString());
-            List<FsUserIntegralLogs> logsList = integralLogsMapper.selectFsUserIntegralLogsList(maps);
+            List<FsUserIntegralLogs> logsList = integralLogsMapper.selectFsUserIntegralLogsList(maps, shard);
             if (logsList.size() > 0) {
                 for (FsUserIntegralLogs userLogs : logsList) {
                     fsUser.setIntegral(fsUser.getIntegral() - userLogs.getIntegral());
@@ -485,7 +489,7 @@ public class FsUserServiceImpl implements IFsUserService {
                     logs.setLogType(4);
                     logs.setBusinessId(orderId.toString());
                     logs.setCreateTime(new Date());
-                    integralLogsMapper.insertFsUserIntegralLogs(logs);
+                    integralLogsMapper.insertFsUserIntegralLogs(logs, shard);
                 }
             }
 

+ 2 - 1
fs-service/src/main/java/com/fs/his/service/impl/FsUserSignServiceImpl.java

@@ -4,6 +4,7 @@ import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.util.NumberUtil;
 import cn.hutool.json.JSONArray;
 import cn.hutool.json.JSONUtil;
+import com.fs.common.sharding.ShardingUtil;
 import com.fs.common.exception.CustomException;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.StringUtils;
@@ -166,7 +167,7 @@ public class FsUserSignServiceImpl implements IFsUserSignService
         logs.setLogType(1);
         logs.setBusinessId(userSign.getId().toString());
         logs.setCreateTime(new Date());
-        integralLogsMapper.insertFsUserIntegralLogs(logs);
+        integralLogsMapper.insertFsUserIntegralLogs(logs, ShardingUtil.shardOf(logs.getUserId()));
 
         //用户积分增加
         FsUser userMap =new  FsUser();

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreOrderScrm.java

@@ -166,6 +166,9 @@ public class FsStoreOrderScrm extends BaseEntity
     /** 给用户退了多少积分 */
     private BigDecimal backIntegral;
 
+    /** 积分抵扣说明 */
+    private String integralDesc;
+
     /** 备注 */
     @Excel(name = "备注")
     private String mark;

+ 2 - 0
fs-service/src/main/java/com/fs/hisStore/dto/FsStoreOrderComputeDTO.java

@@ -32,4 +32,6 @@ public class FsStoreOrderComputeDTO implements Serializable
     /** 活动优惠金额(满减/折扣) */
     private BigDecimal promotionDiscountAmount;
 
+    /** 积分抵扣说明 */
+    private String integralDesc;
 }

+ 7 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductScrmMapper.java

@@ -62,6 +62,13 @@ public interface FsStoreProductScrmMapper
      */
     public int updateFsStoreProduct(FsStoreProductScrm fsStoreProduct);
 
+    /**
+     * 修改商品
+     *
+     * @param fsStoreProduct 商品
+     * @return 结果
+     */
+    public int updateFsStoreProductNullable(FsStoreProductScrm fsStoreProduct);
     /**
      * 删除商品
      *

+ 10 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStorePromotionActivityMapper.java

@@ -28,4 +28,14 @@ public interface FsStorePromotionActivityMapper {
                               @Param("excludeId") Long excludeId);
 
     List<FsStorePromotionActivity> selectActivePromotionsByStoreId(@Param("storeId") Long storeId);
+
+    /**
+     * 条件启用活动:仅当指定时间段内无其它启用中活动时才更新,避免并发竞态
+     *
+     * @return 受影响行数,0 表示因时间冲突未更新
+     */
+    int enableActivityIfNoConflict(@Param("id") Long id,
+                                   @Param("storeId") Long storeId,
+                                   @Param("startTime") Date startTime,
+                                   @Param("endTime") Date endTime);
 }

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStorePromotionUsageMapper.java

@@ -16,6 +16,9 @@ public interface FsStorePromotionUsageMapper {
     List<FsStorePromotionUsageCountDTO> countEffectiveByActivityIdsAndUser(@Param("activityIds") List<Long> activityIds,
                                                                           @Param("userId") Long userId);
 
+    FsStorePromotionUsage selectByOrderIdAndActivityId(@Param("orderId") Long orderId,
+                                                       @Param("activityId") Long activityId);
+
     int insertFsStorePromotionUsage(FsStorePromotionUsage usage);
 
     int updateUsageStatusByOrderId(@Param("orderId") Long orderId,

+ 41 - 29
fs-service/src/main/java/com/fs/hisStore/mapper/FsUserIntegralLogsScrmMapper.java

@@ -13,6 +13,9 @@ import java.util.List;
 /**
  * 积分记录Mapper接口
  *
+ * <p>手动分表:fs_user_integral_logs 按 user_id % 10 分为 _0 ~ _9 共 10 张物理表。
+ * 所有方法通过 {@code shard} 参数指定物理表后缀,由 Service 层通过 {@link com.fs.common.sharding.ShardingUtil} 计算。</p>
+ *
  * @author fs
  * @date 2023-11-02
  */
@@ -21,65 +24,73 @@ public interface FsUserIntegralLogsScrmMapper
     /**
      * 查询积分记录
      *
-     * @param id 积分记录主键
+     * @param id   积分记录主键
+     * @param shard 分表后缀(0~9)
      * @return 积分记录
      */
-    public FsUserIntegralLogsScrm selectFsUserIntegralLogsById(Long id);
+    FsUserIntegralLogsScrm selectFsUserIntegralLogsById(@Param("id") Long id, @Param("shard") int shard);
 
     /**
      * 查询积分记录列表
      *
-     * @param fsUserIntegralLogs 积分记录
+     * @param log   查询条件
+     * @param shard 分表后缀(0~9)
      * @return 积分记录集合
      */
-    public List<FsUserIntegralLogsScrm> selectFsUserIntegralLogsList(FsUserIntegralLogsScrm fsUserIntegralLogs);
+    List<FsUserIntegralLogsScrm> selectFsUserIntegralLogsList(@Param("log") FsUserIntegralLogsScrm log, @Param("shard") int shard);
 
     /**
      * 新增积分记录
      *
-     * @param fsUserIntegralLogs 积分记录
+     * @param log   积分记录
+     * @param shard 分表后缀(0~9)
      * @return 结果
      */
-    public int insertFsUserIntegralLogs(FsUserIntegralLogsScrm fsUserIntegralLogs);
+    int insertFsUserIntegralLogs(@Param("log") FsUserIntegralLogsScrm log, @Param("shard") int shard);
 
     /**
      * 修改积分记录
      *
-     * @param fsUserIntegralLogs 积分记录
+     * @param log   积分记录
+     * @param shard 分表后缀(0~9)
      * @return 结果
      */
-    public int updateFsUserIntegralLogs(FsUserIntegralLogsScrm fsUserIntegralLogs);
+    int updateFsUserIntegralLogs(@Param("log") FsUserIntegralLogsScrm log, @Param("shard") int shard);
 
     /**
      * 删除积分记录
      *
-     * @param id 积分记录主键
+     * @param id    积分记录主键
+     * @param shard 分表后缀(0~9)
      * @return 结果
      */
-    public int deleteFsUserIntegralLogsById(Long id);
+    int deleteFsUserIntegralLogsById(@Param("id") Long id, @Param("shard") int shard);
 
     /**
      * 批量删除积分记录
      *
-     * @param ids 需要删除的数据主键集合
+     * @param ids   需要删除的数据主键集合
+     * @param shard 分表后缀(0~9)
      * @return 结果
      */
-    public int deleteFsUserIntegralLogsByIds(Long[] ids);
+    int deleteFsUserIntegralLogsByIds(@Param("ids") Long[] ids, @Param("shard") int shard);
+
     @Select({"<script> " +
-            "select l.*,u.nickname,u.phone from fs_user_integral_logs l LEFT JOIN fs_user u ON u.user_id=l.user_id "+
+            "select l.*,u.nickname,u.phone from fs_user_integral_logs_${shard} l LEFT JOIN fs_user u ON u.user_id=l.user_id "+
             " <where>  \n" +
-            "            <if test=\"userId != null \"> and l.user_id = #{userId}</if>\n" +
-            "            <if test=\"logType != null  and logType != ''\"> and log_type = #{logType}</if>\n" +
-            "            <if test=\"phone != null \"> and u.phone = #{phone}</if>\n" +
-            "            <if test=\"businessId != null  and businessId != ''\"> and business_id = #{businessId}</if>\n" +
-            "            <if test=\"createTime != null \"> and create_time = #{createTime}</if>\n" +
-            "            <if test=\"sTime != null \">  and DATE(l.create_time) &gt;= DATE(#{sTime})</if>\n" +
-            "            <if test=\"eTime != null \">  and DATE(l.create_time) &lt;= DATE(#{eTime})</if>\n" +
+            "            <if test=\"fsUserIntegralLogs.userId != null \"> and l.user_id = #{fsUserIntegralLogs.userId}</if>\n" +
+            "            <if test=\"fsUserIntegralLogs.logType != null  and fsUserIntegralLogs.logType != ''\"> and log_type = #{fsUserIntegralLogs.logType}</if>\n" +
+            "            <if test=\"fsUserIntegralLogs.phone != null \"> and u.phone = #{fsUserIntegralLogs.phone}</if>\n" +
+            "            <if test=\"fsUserIntegralLogs.businessId != null  and fsUserIntegralLogs.businessId != ''\"> and business_id = #{fsUserIntegralLogs.businessId}</if>\n" +
+            "            <if test=\"fsUserIntegralLogs.createTime != null \"> and create_time = #{fsUserIntegralLogs.createTime}</if>\n" +
+            "            <if test=\"fsUserIntegralLogs.sTime != null \">  and DATE(l.create_time) &gt;= DATE(#{fsUserIntegralLogs.sTime})</if>\n" +
+            "            <if test=\"fsUserIntegralLogs.eTime != null \">  and DATE(l.create_time) &lt;= DATE(#{fsUserIntegralLogs.eTime})</if>\n" +
             "        </where> order by id desc"+
             "</script>"})
-    List<FsUserIntegralLogsListVO> selectFsUserIntegralLogsListVO(FsUserIntegralLogsParam fsUserIntegralLogs);
+    List<FsUserIntegralLogsListVO> selectFsUserIntegralLogsListVO(@Param("fsUserIntegralLogs") FsUserIntegralLogsParam fsUserIntegralLogs, @Param("shard") int shard);
+
     @Select({"<script> " +
-            "select l.*  from fs_user_integral_logs l  " +
+            "select l.*  from fs_user_integral_logs_${shard} l  " +
             "where 1=1  " +
             "<if test = 'maps.userId != null       '> " +
             "and l.user_id = #{maps.userId}  " +
@@ -92,13 +103,14 @@ public interface FsUserIntegralLogsScrmMapper
             "</if>" +
             " order by l.id desc "+
             "</script>"})
-    List<FsUserIntegralLogsListUVO> selectFsUserIntegralLogsListUVO(@Param("maps")FsUserIntegralLogsListUParam param);
-    @Select("select user_id  from fs_user_integral_logs WHERE DATE_FORMAT(create_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m') and log_type=2 and `status`=0  and business_type=2 GROUP BY user_id HAVING count(user_id) > 5;")
-    List<Long> subFsUserIntegralLogsByOrder5();
+    List<FsUserIntegralLogsListUVO> selectFsUserIntegralLogsListUVO(@Param("maps") FsUserIntegralLogsListUParam param, @Param("shard") int shard);
+
+    @Select("select user_id from fs_user_integral_logs_${shard} WHERE DATE_FORMAT(create_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m') and log_type=2 and `status`=0 and business_type=2 GROUP BY user_id HAVING count(user_id) > 5")
+    List<Long> subFsUserIntegralLogsByOrder5(@Param("shard") int shard);
 
-    @Select("select * from fs_user_integral_logs where user_id = #{id} AND `status`=0 and DATE_FORMAT(create_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m') and ((business_type=2 and log_type=2) or log_type=6)")
-    List<FsUserIntegralLogsScrm> selectFsUserIntegralByStatus0(Long id);
+    @Select("select * from fs_user_integral_logs_${shard} where user_id = #{id} AND `status`=0 and DATE_FORMAT(create_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m') and ((business_type=2 and log_type=2) or log_type=6)")
+    List<FsUserIntegralLogsScrm> selectFsUserIntegralByStatus0(@Param("id") Long id, @Param("shard") int shard);
 
-    @Select("select * from fs_user_integral_logs where log_type=10 and user_id = #{userId} and business_id = #{videoId} limit 1")
-    FsUserIntegralLogsScrm selectFsUserIntegralByCourseVideo(@Param("videoId") Long videoId, @Param("userId")Long userId);
+    @Select("select * from fs_user_integral_logs_${shard} where log_type=10 and user_id = #{userId} and business_id = #{videoId} limit 1")
+    FsUserIntegralLogsScrm selectFsUserIntegralByCourseVideo(@Param("videoId") Long videoId, @Param("userId") Long userId, @Param("shard") int shard);
 }

+ 4 - 1
fs-service/src/main/java/com/fs/hisStore/mapper/FsUserScrmMapper.java

@@ -116,8 +116,11 @@ public interface FsUserScrmMapper
     public int deleteFsUserByIds(Long[] userIds);
 
     @Update("update fs_user set integral=integral-#{integral}" +
-            " where user_id=#{uid}")
+            " where user_id=#{uid} and integral >= #{integral}")
     int decIntegral(@Param("uid") Long uid, @Param("integral")double integral);
+    @Update("update fs_user set integral=integral+#{integral}" +
+            " where user_id=#{uid}")
+    int incIntegral(@Param("uid") Long uid, @Param("integral")double integral);
     @Update("update fs_user set pay_count=pay_count+1" +
             " where user_id=#{userId}")
     int incPayCount(Long userId);

+ 6 - 0
fs-service/src/main/java/com/fs/hisStore/param/FsStoreOrderComputedParam.java

@@ -4,6 +4,7 @@ import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
 import java.io.Serializable;
+import java.math.BigDecimal;
 
 @Data
 public class FsStoreOrderComputedParam implements Serializable
@@ -21,4 +22,9 @@ public class FsStoreOrderComputedParam implements Serializable
 
     private String createOrderKey;
 
+    /** 多店铺场景下预分配的本单积分抵扣金额(由computedOrders按比例计算后传入) */
+    private BigDecimal preAllocatedDeduction;
+
+    /** 多店铺场景下预分配的本单使用积分数量 */
+    private Integer preAllocatedIntegral;
 }

+ 9 - 0
fs-service/src/main/java/com/fs/hisStore/param/FsStoreOrderCreateParam.java

@@ -61,4 +61,13 @@ public class FsStoreOrderCreateParam implements Serializable
 
     //组合订单唯一标识
     private String combinationOrderId;
+
+    /** 多店铺场景下预分配的本单积分抵扣金额(由 computedOrders 按店铺金额比例计算后传入,createMultiStore 内部使用) */
+    private BigDecimal preAllocatedDeduction;
+
+    /** 多店铺场景下预分配的本单使用积分数量(整数) */
+    private Integer preAllocatedIntegral;
+
+    /** 积分抵扣说明(多店铺场景由 computedOrders 生成后透传,落库到订单 integral_desc 字段) */
+    private String integralDesc;
 }

+ 13 - 0
fs-service/src/main/java/com/fs/hisStore/service/IFsStoreOrderScrmService.java

@@ -111,6 +111,19 @@ public interface IFsStoreOrderScrmService
 
     void refundOrder(Long orderId);
 
+    /**
+     * 售后退款时退回下单抵扣积分(供 FsStoreAfterSalesScrmServiceImpl 调用)
+     * 内部有 backIntegral 幂等校验,已退过则跳过
+     */
+    void refundIntegralForAfterSales(FsStoreOrderScrm order);
+
+    /**
+     * 售后退款时处理积分退还/扣回(独立事务 REQUIRES_NEW,积分异常不影响退款主事务)
+     * 场景A:退回下单抵扣积分(logType=25→26)
+     * 场景B:扣回已发放消费积分(logType=2→4,受 refundDeduct 开关控制)
+     */
+    void refundIntegralByAfterSales(FsStoreOrderScrm order);
+
     void deliveryOrder(String orderCode,String deliveryId,String deliverCode,String deliverName);
 
     /**

+ 10 - 0
fs-service/src/main/java/com/fs/hisStore/service/IFsUserIntegralLogsScrmService.java

@@ -70,4 +70,14 @@ public interface IFsUserIntegralLogsScrmService
 
     void subFsUserIntegralLogsByOrder5();
 
+    /**
+     * 插入积分日志(独立事务,供外部 @Transactional 方法调用)
+     * <p>内部使用 REQUIRES_NEW 传播级别,并通过 {@link com.fs.common.sharding.ShardingUtil} 计算分表后缀,
+     * 确保积分日志写入正确的物理分表。</p>
+     *
+     * @param logs 积分日志记录
+     * @return 影响行数
+     */
+    int insertLog(FsUserIntegralLogsScrm logs);
+
 }

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/service/IFsUserScrmService.java

@@ -100,6 +100,9 @@ public interface IFsUserScrmService
 
     int decIntegral( Long uid,  double usedIntegral);
 
+    /** 原子加回积分(取消订单/退款退回积分时使用,SQL 层 integral = integral + ?,并发安全) */
+    int incIntegral(Long uid, double integral);
+
     int incPayCount(Long userId);
 
     FsUserScrm selectFsUserByPhone(String phone);

+ 2 - 1
fs-service/src/main/java/com/fs/hisStore/service/impl/FsIntegralOrderScrmServiceImpl.java

@@ -5,6 +5,7 @@ import cn.hutool.core.util.IdUtil;
 import cn.hutool.json.JSONUtil;
 import com.fs.common.core.domain.R;
 import com.fs.common.exception.CustomException;
+import com.fs.common.sharding.ShardingUtil;
 import com.fs.common.utils.DateUtils;
 import com.fs.hisStore.domain.*;
 import com.fs.hisStore.mapper.*;
@@ -190,7 +191,7 @@ public class FsIntegralOrderScrmServiceImpl implements IFsIntegralOrderScrmServi
             logs.setLogType(4);
             logs.setBusinessId(order.getOrderId().toString());
             logs.setCreateTime(new Date());
-            fsUserIntegralLogsMapper.insertFsUserIntegralLogs(logs);
+            fsUserIntegralLogsMapper.insertFsUserIntegralLogs(logs, ShardingUtil.shardOf(logs.getUserId()));
             return R.ok("兑换成功").put("order",order);
         }
         else{

+ 25 - 1
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreAfterSalesScrmServiceImpl.java

@@ -89,6 +89,8 @@ import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.interceptor.TransactionAspectSupport;
+import org.springframework.transaction.support.TransactionSynchronizationAdapter;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
 
 import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
@@ -884,9 +886,13 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
                                 paymentService.updateFsStorePayment(paymentMap);
                             }
                             else {
+                                // 退款失败必须回滚事务,避免售后单/订单状态被错误标记为已完成
+                                TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
                                 return R.error("退款请求失败"+refundQueryResult.getErrCodeDes());
                             }
                         } catch (WxPayException e) {
+                            // 退款失败必须回滚事务,避免售后单/订单状态被错误标记为已完成
+                            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
                             return R.error("退款请求失败"+e.getErrCodeDes());
                         }
                     }else if (payment.getPayMode()!=null&&payment.getPayMode().equals("hf")){
@@ -980,6 +986,24 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
         if(order.getTuiUserId()!=null&&order.getTuiUserId()>0){
             userService.subTuiMoney(order);
         }
+
+        // 积分退还:推迟到主事务提交后执行,避免 REQUIRES_NEW 与主事务行锁死锁
+        // 原因:主事务已 update fs_store_order_scrm 持有行锁,REQUIRES_NEW 内 refundIntegral
+        //       又 update 同一行 back_integral,导致 Lock wait timeout
+        // 修复:afterCommit 回调时主事务已提交、行锁已释放,REQUIRES_NEW 可正常获取锁
+        final FsStoreOrderScrm orderForIntegral = order;
+        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
+            @Override
+            public void afterCommit() {
+                try {
+                    fsStoreOrderService.refundIntegralByAfterSales(orderForIntegral);
+                } catch (Exception e) {
+                    // 积分处理失败不影响已提交的退款事务,仅记日志,后续可人工补偿或重试
+                    logger.error("售后退款积分处理失败,orderId={},需人工核对积分", orderForIntegral.getId(), e);
+                }
+            }
+        });
+
         return R.ok();
     }
 
@@ -1075,7 +1099,7 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
         //仅退款
         if(storeAfterSales.getServiceType().equals(0)){
             //仅退款未发货处理
-            if(storeAfterSales.getOrderStatus().equals(OrderInfoEnum.STATUS_1.getValue())){
+            if(storeAfterSales.getOrderStatus().equals(OrderInfoEnum.STATUS_1.getValue()) || storeAfterSales.getOrderStatus().equals(OrderInfoEnum.STATUS_3.getValue())){
                 //审核通过
                 storeAfterSales.setStatus(3);
                 fsStoreAfterSalesMapper.updateFsStoreAfterSales(storeAfterSales);

+ 520 - 75
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -27,6 +27,7 @@ import com.fs.common.event.TemplateEvent;
 import com.fs.common.event.TemplateListenEnum;
 import com.fs.common.exception.CustomException;
 import com.fs.common.exception.ServiceException;
+import com.fs.common.sharding.ShardingUtil;
 import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.IpUtil;
@@ -71,6 +72,7 @@ import com.fs.his.mapper.FsUserIntegralLogsMapper;
 import com.fs.his.domain.FsUserIntegralLogs;
 import com.fs.his.enums.FsStoreOrderLogEnum;
 import com.fs.his.enums.FsStoreOrderStatusEnum;
+import com.fs.his.enums.FsUserIntegralLogTypeEnum;
 import com.fs.his.mapper.*;
 import com.fs.his.param.FsStoreOrderSalesParam;
 import com.fs.his.service.IFsPrescribeService;
@@ -112,6 +114,7 @@ import com.fs.pay.pay.dto.RefundDTO;
 import com.fs.pay.service.IPayService;
 import com.fs.hisStore.config.StoreConfig;
 import com.fs.hisStore.config.StoreIntegralConfig;
+import com.fs.his.config.PointsGrantRuleConfig;
 import com.fs.hisStore.constants.StoreConstants;
 import com.fs.hisStore.domain.*;
 import com.fs.hisStore.enums.*;
@@ -153,6 +156,7 @@ import org.springframework.transaction.interceptor.TransactionAspectSupport;
 import javax.annotation.PostConstruct;
 import java.lang.reflect.Field;
 import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.net.URLEncoder;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
@@ -731,35 +735,67 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         BigDecimal deductionPrice = BigDecimal.ZERO; //积分抵扣金额
         double usedIntegral = 0; //使用了多少积分
         if (param.getUseIntegral() != null && param.getUseIntegral() == 1 && user.getIntegral().intValue() > 0) {
-            //如果积分总和小于用户积分 抵扣比例 计算抵扣价格
-            //默认配置
-            Double integralMax = Double.valueOf(100);
-            BigDecimal integralFull = new BigDecimal(100);
-            Double integralRatio = Double.valueOf(1);
-            String json = configService.selectConfigByKey("store.integral");
-            if (StringUtils.isEmpty(json)) {
-
-            } else {
-                StoreIntegralConfig integralConfig = JSONUtil.toBean(json, StoreIntegralConfig.class);
-                integralMax = integralConfig.getIntegralMax();
-                integralFull = integralConfig.getIntegralFull();
-                integralRatio = integralConfig.getIntegralRatio();
-
-            }
-            if (priceGroup.getTotalPrice().compareTo(integralFull) >= 0) {
-                Double userIntegral = user.getIntegral().doubleValue();
-                if (integralMax.intValue() > 0 && Double.compare(userIntegral, integralMax) >= 0) {
-                    userIntegral = integralMax;
+            // 新积分配置:points.grantRule
+            Integer payRate = 100;
+            Integer maxDeductPercent = 50;
+            BigDecimal minDeductAmount = new BigDecimal(100);
+            Integer minUseIntegral = 0;
+            Integer deductEnabled = 1;
+            String json = configService.selectConfigByKey("points.grantRule");
+            if (StringUtils.isNotEmpty(json)) {
+                try {
+                    PointsGrantRuleConfig rule = JSONUtil.toBean(json, PointsGrantRuleConfig.class);
+                    if (rule != null && rule.getEnabled() != null && rule.getEnabled() == 1) {
+                        if (rule.getPayRate() != null && rule.getPayRate() > 0) {
+                            payRate = rule.getPayRate();
+                        }
+                        if (rule.getMaxDeductPercent() != null && rule.getMaxDeductPercent() > 0) {
+                            maxDeductPercent = rule.getMaxDeductPercent();
+                        }
+                        if (rule.getMinDeductAmount() != null) {
+                            minDeductAmount = rule.getMinDeductAmount();
+                        }
+                        if (rule.getMinUseIntegral() != null && rule.getMinUseIntegral() > 0) {
+                            minUseIntegral = rule.getMinUseIntegral();
+                        }
+                        if (rule.getDeductEnabled() != null) {
+                            deductEnabled = rule.getDeductEnabled();
+                        }
+                    }
+                } catch (Exception e) {
+                    log.warn("积分抵扣规则配置解析失败,使用默认值", e);
+                }
+            }
+            // 积分抵扣功能关闭或积分不足起用门槛,跳过
+            if (deductEnabled == 1
+                    && (minUseIntegral == 0 || user.getIntegral().intValue() >= minUseIntegral)
+                    && priceGroup.getTotalPrice().compareTo(minDeductAmount) >= 0) {
+                long userIntegral = user.getIntegral().longValue();
+                deductionPrice = BigDecimal.valueOf(userIntegral)
+                        .divide(BigDecimal.valueOf(payRate), 2, RoundingMode.HALF_UP);
+                // 最高抵扣比例限制
+                if (maxDeductPercent > 0 && maxDeductPercent < 100) {
+                    BigDecimal maxDeductionAmount = priceGroup.getTotalPrice()
+                            .multiply(BigDecimal.valueOf(maxDeductPercent))
+                            .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
+                    if (deductionPrice.compareTo(maxDeductionAmount) > 0) {
+                        deductionPrice = maxDeductionAmount;
+                        userIntegral = deductionPrice
+                                .multiply(BigDecimal.valueOf(payRate))
+                                .setScale(0, RoundingMode.DOWN)
+                                .longValue();
+                    }
                 }
-                deductionPrice = BigDecimal.valueOf(NumberUtil.mul(userIntegral, integralRatio));
                 if (deductionPrice.compareTo(payPrice) < 0) {
                     payPrice = NumberUtil.sub(payPrice, deductionPrice);
                     usedIntegral = userIntegral;
                 } else {
                     deductionPrice = payPrice;
                     payPrice = BigDecimal.ZERO;
-                    usedIntegral = NumberUtil.round(NumberUtil.div(deductionPrice,
-                            BigDecimal.valueOf(integralRatio)), 2).doubleValue();
+                    usedIntegral = deductionPrice
+                            .multiply(BigDecimal.valueOf(payRate))
+                            .setScale(0, RoundingMode.DOWN)
+                            .doubleValue();
                 }
             }
         }
@@ -912,6 +948,9 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             storeOrder.setUseIntegral(BigDecimal.valueOf(dto.getUsedIntegral()));
             storeOrder.setBackIntegral(BigDecimal.ZERO);
             storeOrder.setGainIntegral(BigDecimal.ZERO);
+            if (dto.getIntegralDesc() != null) {
+                storeOrder.setIntegralDesc(dto.getIntegralDesc());
+            }
             storeOrder.setMark(param.getMark());
             //todo 获取成本价
             BigDecimal costPrice = this.getOrderSumPrice(carts, "costPrice");
@@ -926,7 +965,12 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 storeOrder.setServiceFee(config.getServiceFee());
             }
 
-            FsStorePromotionComputeResultVO promotionResult = applyPromotionFields(storeOrder, carts, userId, param.getCouponUserId(), param.getStoreId());
+            boolean skipPromotion = false;
+            if (StringUtils.isNotEmpty(param.getCreateOrderKey())) {
+                Boolean isAdjusted = redisCache.getCacheObject("createOrderIsAdjusted:" + param.getCreateOrderKey());
+                skipPromotion = (isAdjusted != null && isAdjusted);
+            }
+            FsStorePromotionComputeResultVO promotionResult = applyPromotionFields(storeOrder, carts, userId, param.getCouponUserId(), param.getStoreId(), skipPromotion);
 
             //后台制单处理
             if (param.getPayPrice() != null && param.getPayPrice().compareTo(BigDecimal.ZERO) > 0) {
@@ -1259,14 +1303,28 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
 
     /**
      * 积分抵扣
+     * 1. 原子扣减 fs_user.integral(SQL 层 integral = integral - ?,并发安全)
+     * 2. 写账单流水 fs_user_bill(保持原有行为)
+     * 3. 写积分明细流水 fs_user_integral_logs(logType=25 下单使用积分抵扣,integral 存负数表示扣减)
      */
     private void decIntegral(Long uid, double usedIntegral, double deductionPrice, String busId) {
         userService.decIntegral(uid, usedIntegral);
         FsUserScrm user = userService.selectFsUserById(uid);
-        //积分记录
+        //积分记录-账单流水
         billService.addBill(uid, BillDetailEnum.CATEGORY_2.getValue(), 0, BillDetailEnum.TYPE_1.getDesc(), usedIntegral, user.getIntegral().doubleValue(),
                 "购买商品使用" + usedIntegral + "积分抵扣" + deductionPrice + "元", busId, 0l);
-
+        //积分记录-积分明细流水(integral 存负数,balance 记录扣减后余额)
+        FsUserIntegralLogs integralLog = new FsUserIntegralLogs();
+        integralLog.setUserId(uid);
+        integralLog.setLogType(FsUserIntegralLogTypeEnum.TYPE_25.getValue());
+        integralLog.setIntegral(-Math.round(usedIntegral));
+        integralLog.setBalance(user.getIntegral() == null ? 0L : user.getIntegral().longValue());
+        integralLog.setBusinessId(busId);
+        integralLog.setBusinessType(0);
+        integralLog.setStatus(0);
+        integralLog.setCreateTime(new Date());
+        int shard = ShardingUtil.shardOf(uid);
+        fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLog, shard);
     }
 
     public void deStockIncSales(List<FsStoreCartScrmQueryVO> cartInfo) {
@@ -1285,6 +1343,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
 
     //未支付取消订单
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public void cancelOrder(Long orderId) {
         FsStoreOrderScrm order = fsStoreOrderMapper.selectFsStoreOrderById(orderId);
         if (order.getStatus() == OrderInfoEnum.STATUS_0.getValue()) {
@@ -1402,11 +1461,63 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             //退回积分
             this.refundIntegral(order);
             //退回库存
+
             this.refundStock(order);
             fsStoreOrderMapper.cancelOrder(orderId);
         }
     }
 
+    @Override
+    public void refundIntegralForAfterSales(FsStoreOrderScrm order) {
+        // 委托给已有的私有 refundIntegral 方法,内部有 backIntegral 幂等校验 + 原子加回 + 写流水
+        this.refundIntegral(order);
+    }
+
+    /**
+     * 售后退款时处理积分退还/扣回(独立事务,积分异常不影响退款主事务)
+     * <p>
+     * 通过 fsStoreOrderService 代理 Bean 调用,REQUIRES_NEW 生效。
+     * 场景A:有 logType=25(下单抵扣)且无 logType=27(未退过)→ 退回下单抵扣积分(写 TYPE_27 退款订单退回积分)
+     * 场景B:有 logType=2(消费发放)且无 logType=4(未扣回)→ 扣回已发放消费积分
+     */
+    @Override
+    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
+    public void refundIntegralByAfterSales(FsStoreOrderScrm order) {
+        if (order == null || order.getId() == null || order.getUserId() == null) {
+            return;
+        }
+        String orderId = order.getId().toString();
+        Long userId = order.getUserId();
+
+        // 查询该订单的所有积分明细记录(带 user_id 分片键,ShardingSphere 精确路由)
+        List<FsUserIntegralLogs> logs = fsUserIntegralLogsMapper.selectByUserIdAndBusinessIdAll(userId, orderId, ShardingUtil.shardOf(userId));
+        if (logs == null || logs.isEmpty()) {
+            return;
+        }
+
+        // 场景A:退回下单抵扣积分(logType=25 存在 且 logType=27 不存在)
+        boolean hasDeductRecord = logs.stream().anyMatch(l -> Integer.valueOf(25).equals(l.getLogType()));
+        boolean hasRefundRecord = logs.stream().anyMatch(l -> Integer.valueOf(27).equals(l.getLogType()));
+        if (hasDeductRecord && !hasRefundRecord) {
+            // 写 TYPE_27 退款订单退回积分(区别于 TYPE_26 取消订单退回积分)
+            this.refundIntegral(order, FsUserIntegralLogTypeEnum.TYPE_27.getValue());
+        }
+
+        // 场景B:扣回已发放消费积分(logType=2 存在 且 logType=4 不存在)
+        boolean hasGrantRecord = logs.stream().anyMatch(l -> Integer.valueOf(2).equals(l.getLogType()));
+        boolean hasRevokeRecord = logs.stream().anyMatch(l -> Integer.valueOf(4).equals(l.getLogType()));
+        if (hasGrantRecord && !hasRevokeRecord) {
+            // 受 points.grantRule.refundDeduct 开关控制,内部有余额保护扣到 0
+            FsUserGrantPointsParam grantParam = new FsUserGrantPointsParam();
+            grantParam.setUserId(userId);
+            grantParam.setLogType(4);
+            grantParam.setOriginalOrderNo(orderId);
+            grantParam.setBusinessId(orderId);
+            grantParam.setRemark("售后退款扣回消费积分");
+            fsUserIntegralLogsService.grantPoints(grantParam);
+        }
+    }
+
     @Override
     public void deliveryOrder(String orderCode, String deliveryId, String deliverCode, String deliverName) {
         FsStoreOrderScrm order = fsStoreOrderMapper.selectFsStoreOrderByOrderCode(orderCode);
@@ -2307,7 +2418,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         try {
             // 订单级幂等校验:同一订单不可重复发放积分
             FsUserIntegralLogs existingLog = fsUserIntegralLogsMapper.selectByUserIdAndBusinessId(
-                    order.getUserId(), order.getId().toString(), 2);
+                    order.getUserId(), order.getId().toString(), 2, ShardingUtil.shardOf(order.getUserId()));
             if (existingLog != null) {
                 log.info("订单已发放过积分,跳过: orderId={}, orderCode={}", order.getId(), order.getOrderCode());
                 return;
@@ -2555,6 +2666,9 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     @Override
     public R createOmsOrder(Long orderId) throws ParseException {
         FsStoreOrderScrm order = fsStoreOrderMapper.selectFsStoreOrderById(orderId);
+        // 【关键日志】查询订单状态,确认事务内可见性
+        logger.info("【createOmsOrder-查询订单】orderId={}, orderCode={}, status={}, paid={}, extendOrderId={}",
+                order.getId(), order.getOrderCode(), order.getStatus(), order.getPaid(), order.getExtendOrderId());
         FsSysConfig erpConfig = configUtil.generateStructConfigByKey(SysConfigEnum.HIS_CONFIG.getKey(), FsSysConfig.class);
         List<Long> noErpCompany = erpConfig.getNoErpCompany();
         if (noErpCompany != null && noErpCompany.contains(order.getCompanyId())) {
@@ -2583,7 +2697,17 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         redisCache.setCacheObject(DELIVERY + ":" + response.getCode(), order.getOrderCode());
         //写入外部订单号
         order.setExtendOrderId(response.getCode());
-        fsStoreOrderMapper.updateFsStoreOrder(order);
+        // 【关键修复】只更新必要字段,避免用完整 order 对象覆盖 status、paid 等字段
+        FsStoreOrderScrm updateOrder = new FsStoreOrderScrm();
+        updateOrder.setId(order.getId());
+        updateOrder.setExtendOrderId(response.getCode());
+        updateOrder.setDeliveryName(order.getDeliveryName());
+        updateOrder.setDeliverySn(order.getDeliverySn());
+        logger.info("【createOmsOrder-更新前】orderId={}, orderCode={}, status={}, paid={}, extendOrderId={}, deliveryName={}, deliverySn={}",
+                order.getId(), order.getOrderCode(), order.getStatus(), order.getPaid(), order.getExtendOrderId(), order.getDeliveryName(), order.getDeliverySn());
+        fsStoreOrderMapper.updateFsStoreOrder(updateOrder);
+        logger.info("【createOmsOrder-更新完成】orderId={}, orderCode={}, 已更新 extendOrderId={}, deliveryName={}, deliverySn={}",
+                order.getId(), order.getOrderCode(), order.getExtendOrderId(), order.getDeliveryName(), order.getDeliverySn());
 
         // 聚水潭:如果设置了仓库编码,调用指定发货仓接口
         if (erpOrderService == jSTOrderService && StringUtils.isNotBlank(erpOrder.getWarehouse_code())) {
@@ -3601,9 +3725,17 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
 
 
     /**
-     * 退回积分
+     * 退回积分(默认 logType=26 取消订单退回积分)
      */
     private void refundIntegral(FsStoreOrderScrm order) {
+        refundIntegral(order, FsUserIntegralLogTypeEnum.TYPE_26.getValue());
+    }
+
+    /**
+     * 退回积分
+     * @param logType 积分明细类型(26=取消订单退回积分,27=退款订单退回积分)
+     */
+    private void refundIntegral(FsStoreOrderScrm order, Integer logType) {
 
         if (order.getPayIntegral().compareTo(BigDecimal.ZERO) > 0) {
             order.setUseIntegral(order.getPayIntegral());
@@ -3614,16 +3746,26 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         if (order.getBackIntegral().compareTo(BigDecimal.ZERO) > 0) {
             return;
         }
+        // 原子加回积分(SQL 层 integral = integral + ?,并发安全,与下单时 decIntegral 原子扣减对称)
+        userService.incIntegral(order.getUserId(), order.getUseIntegral().doubleValue());
         FsUserScrm user = userService.selectFsUserById(order.getUserId());
-        //增加积分
-        BigDecimal newIntegral = NumberUtil.add(order.getUseIntegral(), user.getIntegral());
-        user.setIntegral(newIntegral.longValue());
-        userService.updateFsUser(user);
-        //增加流水
+        //增加流水-账单
         billService.addBill(user.getUserId(), BillDetailEnum.CATEGORY_2.getValue(), 1, BillDetailEnum.TYPE_5.getDesc(),
                 order.getUseIntegral().doubleValue(),
-                newIntegral.doubleValue(),
+                user.getIntegral().doubleValue(),
                 "购买商品失败,回退积分" + order.getUseIntegral(), order.getId().toString(), 0l);
+        //增加流水-积分明细(integral 存正数表示增加,与下单扣减 TYPE_25 对称)
+        FsUserIntegralLogs integralLog = new FsUserIntegralLogs();
+        integralLog.setUserId(order.getUserId());
+        integralLog.setLogType(logType);
+        integralLog.setIntegral(Math.round(order.getUseIntegral().doubleValue()));
+        integralLog.setBalance(user.getIntegral() == null ? 0L : user.getIntegral().longValue());
+        integralLog.setBusinessId(order.getId().toString());
+        integralLog.setBusinessType(0);
+        integralLog.setStatus(0);
+        integralLog.setCreateTime(new Date());
+        int shard = ShardingUtil.shardOf(order.getUserId());
+        fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLog, shard);
         //更新订单回退积分
         FsStoreOrderScrm storeOrder = new FsStoreOrderScrm();
         storeOrder.setBackIntegral(order.getUseIntegral());
@@ -5447,18 +5589,24 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     @Transactional
     @Synchronized
     public String payConfirmMultiStore(Integer type, Long orderId, String payCode, String tradeNo, String bankTransactionId, String bankSerialNo) {
+        logger.info("【payConfirmMultiStore-开始】type={}, payCode={}, orderId={}, tradeNo={}, bankTransactionId={}, bankSerialNo={}",
+                type, payCode, orderId, tradeNo, bankTransactionId, bankSerialNo);
         try {
             List<FsStoreOrderScrm> orders = new ArrayList<>();
             if (type.equals(1)) {
                 List<FsStorePaymentScrm> storePayments = paymentService.selectFsStorePaymentByPaymentCode(payCode);
                 if (storePayments == null || storePayments.isEmpty()) {
-                    logger.warn("payConfirmMultiStore: 未找到支付记录, payCode={}", payCode);
+                    // 【关键日志】未找到支付记录:可能支付记录被删除或 payCode 不匹配,返回空后 Controller 返回 SUCCESS,易宝不重试
+                    logger.error("【payConfirmMultiStore-未找到支付记录】payCode={} 返回空,事务回滚,Controller将返回SUCCESS(易宝不重试)", payCode);
                     TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
                     return "";
                 }
+                logger.info("【payConfirmMultiStore-支付记录数】payCode={}, paymentCount={}", payCode, storePayments.size());
 
                 // 【关键修复】第一步:先加载并校验所有订单状态,避免 payment 已更新但 order 更新失败
                 boolean allPaymentAlreadyPaid = true;
+                int orderLoadCount = 0;
+                int orderLoadFailCount = 0;
                 for (FsStorePaymentScrm storePayment : storePayments) {
                     if (storePayment == null) {
                         continue;
@@ -5466,30 +5614,54 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                     // 检查 payment 是否已处理(幂等判断)
                     if (storePayment.getStatus() != null && !storePayment.getStatus().equals(0)) {
                         // payment 已经是非待支付状态,不再更新 payment,但仍需校验订单
-                        logger.info("payConfirmMultiStore: payment已处理, paymentId={}, status={}, payCode={}",
-                                storePayment.getPaymentId(), storePayment.getStatus(), payCode);
+                        logger.info("【payConfirmMultiStore-payment已处理】paymentId={}, status={}, orderId={}, businessOrderId={}, payCode={}",
+                                storePayment.getPaymentId(), storePayment.getStatus(), storePayment.getOrderId(), storePayment.getBusinessOrderId(), payCode);
                         allPaymentAlreadyPaid = allPaymentAlreadyPaid && true;
                     } else {
                         allPaymentAlreadyPaid = false;
+                        logger.info("【payConfirmMultiStore-payment待处理】paymentId={}, status=0, orderId={}, businessOrderId={}, payCode={}",
+                                storePayment.getPaymentId(), storePayment.getOrderId(), storePayment.getBusinessOrderId(), payCode);
                     }
                     // 加载关联订单
                     if (storePayment.getBusinessOrderId() != null && storePayment.getBusinessOrderId().length() > 30) {
-                        orders.addAll(fsStoreOrderMapper.getStoreOrderByCombinationId(storePayment.getBusinessOrderId()));
+                        // 组合订单ID
+                        List<FsStoreOrderScrm> comboOrders = fsStoreOrderMapper.getStoreOrderByCombinationId(storePayment.getBusinessOrderId());
+                        if (comboOrders == null || comboOrders.isEmpty()) {
+                            // 【关键日志】组合订单查询返回空,可能导致后续 orders 为空
+                            logger.error("【payConfirmMultiStore-组合订单查询空】paymentId={}, businessOrderId={}, payCode={} getStoreOrderByCombinationId返回空",
+                                    storePayment.getPaymentId(), storePayment.getBusinessOrderId(), payCode);
+                            orderLoadFailCount++;
+                        } else {
+                            orders.addAll(comboOrders);
+                            orderLoadCount += comboOrders.size();
+                        }
                     } else {
                         FsStoreOrderScrm order = fsStoreOrderMapper.selectFsStoreOrderById(storePayment.getOrderId());
                         if (order != null) {
                             orders.add(order);
+                            orderLoadCount++;
+                        } else {
+                            // 【关键日志】订单查询返回空
+                            logger.error("【payConfirmMultiStore-订单查询空】paymentId={}, orderId={}, payCode={} selectFsStoreOrderById返回空",
+                                    storePayment.getPaymentId(), storePayment.getOrderId(), payCode);
+                            orderLoadFailCount++;
                         }
                     }
                 }
+                logger.info("【payConfirmMultiStore-订单加载完成】payCode={}, 加载成功={}, 加载失败={}, allPaymentAlreadyPaid={}",
+                        payCode, orderLoadCount, orderLoadFailCount, allPaymentAlreadyPaid);
 
                 if (orders.isEmpty()) {
-                    logger.warn("payConfirmMultiStore: 未找到关联订单, payCode={}", payCode);
+                    // 【关键日志】未找到关联订单:订单可能被删除,返回空后 Controller 返回 SUCCESS,易宝不重试
+                    logger.error("【payConfirmMultiStore-未找到关联订单】payCode={}, paymentCount={}, 返回空,事务回滚,Controller将返回SUCCESS(易宝不重试)",
+                            payCode, storePayments.size());
                     TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
                     return "";
                 }
 
                 // 【关键修复】第二步:校验订单状态,跳过已支付的订单
+                int skipCount = 0;
+                int needUpdateCount = 0;
                 for (FsStoreOrderScrm order : orders) {
                     if (order == null) {
                         continue;
@@ -5497,20 +5669,29 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                     // 如果订单已经支付完成,跳过(幂等)
                     if (order.getPaid() != null && order.getPaid().equals(OrderInfoEnum.PAY_STATUS_1.getValue())
                             && order.getStatus() != null && order.getStatus().equals(OrderInfoEnum.STATUS_1.getValue())) {
-                        logger.info("payConfirmMultiStore: 订单已支付,跳过, orderId={}, orderCode={}", order.getId(), order.getOrderCode());
+                        logger.info("【payConfirmMultiStore-订单已支付跳过】orderId={}, orderCode={}, status={}, paid={}",
+                                order.getId(), order.getOrderCode(), order.getStatus(), order.getPaid());
+                        skipCount++;
                         continue;
                     }
                     // 订单状态不是待支付且也不是已支付,说明订单状态异常
                     if (order.getStatus() != null && !order.getStatus().equals(OrderInfoEnum.STATUS_0.getValue())
                             && !order.getStatus().equals(OrderInfoEnum.STATUS_1.getValue())) {
-                        logger.error("payConfirmMultiStore: 订单状态异常, orderId={}, status={}, orderCode={}",
-                                order.getId(), order.getStatus(), order.getOrderCode());
+                        // 【关键日志】订单状态异常:可能被其他流程修改,返回空后 Controller 返回 SUCCESS,易宝不重试
+                        logger.error("【payConfirmMultiStore-订单状态异常】orderId={}, orderCode={}, 当前status={}, paid={}(期望status=0待支付或1已支付),返回空,事务回滚,Controller将返回SUCCESS(易宝不重试)",
+                                order.getId(), order.getOrderCode(), order.getStatus(), order.getPaid());
                         TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
                         return "";
                     }
+                    needUpdateCount++;
+                    logger.info("【payConfirmMultiStore-订单待更新】orderId={}, orderCode={}, 当前status={}, paid={}",
+                            order.getId(), order.getOrderCode(), order.getStatus(), order.getPaid());
                 }
+                logger.info("【payConfirmMultiStore-订单校验完成】payCode={}, 总数={}, 跳过已支付={}, 待更新={}",
+                        payCode, orders.size(), skipCount, needUpdateCount);
 
                 // 【关键修复】第三步:先更新订单状态,再更新 payment(保证订单优先落库)
+                int orderUpdatedCount = 0;
                 for (FsStoreOrderScrm order : orders) {
                     if (order == null) {
                         continue;
@@ -5525,14 +5706,19 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                     storeOrder.setPaid(OrderInfoEnum.PAY_STATUS_1.getValue());
                     storeOrder.setStatus(OrderInfoEnum.STATUS_1.getValue());
                     storeOrder.setPayTime(new Date());
-                    fsStoreOrderMapper.updateFsStoreOrder(storeOrder);
+                    int updateRows = fsStoreOrderMapper.updateFsStoreOrder(storeOrder);
+                    logger.info("【payConfirmMultiStore-订单状态更新】orderId={}, orderCode={}, 更新前status={}, paid={}, updateRows={}",
+                            order.getId(), order.getOrderCode(), order.getStatus(), order.getPaid(), updateRows);
+                    orderUpdatedCount += updateRows;
 
                     // 增加状态日志
                     orderStatusService.create(order.getId(), OrderLogEnum.PAY_ORDER_SUCCESS.getValue(),
                             OrderLogEnum.PAY_ORDER_SUCCESS.getDesc());
                 }
+                logger.info("【payConfirmMultiStore-订单更新完成】payCode={}, 实际更新行数={}", payCode, orderUpdatedCount);
 
                 // 【关键修复】第四步:更新 payment 状态(订单已落库,payment 更新即使异常也有 orders 兜底)
+                int paymentUpdatedCount = 0;
                 for (FsStorePaymentScrm storePayment : storePayments) {
                     if (storePayment == null) {
                         continue;
@@ -5543,14 +5729,20 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                         storePayment.setTradeNo(tradeNo);
                         storePayment.setBankSerialNo(bankSerialNo);
                         storePayment.setBankTransactionId(bankTransactionId);
-                        paymentService.updateFsStorePayment(storePayment);
+                        int updateRows = paymentService.updateFsStorePayment(storePayment);
+                        logger.info("【payConfirmMultiStore-payment更新】paymentId={}, orderId={}, updateRows={}",
+                                storePayment.getPaymentId(), storePayment.getOrderId(), updateRows);
+                        paymentUpdatedCount += updateRows;
                     }
                 }
+                logger.info("【payConfirmMultiStore-payment更新完成】payCode={}, 实际更新行数={}", payCode, paymentUpdatedCount);
 
             } else if (type.equals(2)) {
                 //货到付款
                 FsStoreOrderScrm order = fsStoreOrderMapper.selectFsStoreOrderById(orderId);
                 if (!order.getStatus().equals(OrderInfoEnum.STATUS_0.getValue())) {
+                    logger.error("【payConfirmMultiStore-货到付款订单状态异常】orderId={}, status={}(期望0待支付),返回空,事务回滚",
+                            orderId, order.getStatus());
                     TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
                     return "";
                 }
@@ -5565,6 +5757,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
 
                 orderStatusService.create(order.getId(), OrderLogEnum.PAY_ORDER_SUCCESS.getValue(),
                         OrderLogEnum.PAY_ORDER_SUCCESS.getDesc());
+                logger.info("【payConfirmMultiStore-货到付款处理成功】orderId={}", orderId);
             }
 
             //第五步:OMS推送、佣金等后置处理(非关键路径,异常不影响订单支付状态)
@@ -5610,11 +5803,26 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                     }
                 }
             }
+            // 【关键诊断】第五步完成后,查询订单实际状态(事务内),确认 status/paid 是否正确
+            for (FsStoreOrderScrm order : orders) {
+                if (order == null) continue;
+                FsStoreOrderScrm latestOrder = fsStoreOrderMapper.selectFsStoreOrderById(order.getId());
+                logger.info("【payConfirmMultiStore-事务内最终状态】orderId={}, orderCode={}, status={}, paid={}, extendOrderId={}, deliveryName={}, deliverySn={}",
+                        order.getId(), order.getOrderCode(),
+                        latestOrder != null ? latestOrder.getStatus() : "null",
+                        latestOrder != null ? latestOrder.getPaid() : "null",
+                        latestOrder != null ? latestOrder.getExtendOrderId() : "null",
+                        latestOrder != null ? latestOrder.getDeliveryName() : "null",
+                        latestOrder != null ? latestOrder.getDeliverySn() : "null");
+            }
         } catch (Exception e) {
-            logger.error("payConfirmMultiStore 异常, payCode={}, orderId={}", payCode, orderId, e);
+            // 【关键日志】异常场景:事务回滚,但返回空后 Controller 返回 SUCCESS,易宝不重试,订单状态可能仍为待支付
+            logger.error("【payConfirmMultiStore-异常】payCode={}, orderId={}, 返回空,事务回滚,Controller将返回SUCCESS(易宝不重试),订单状态可能仍为待支付",
+                    payCode, orderId, e);
             TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
             return "";
         }
+        logger.info("【payConfirmMultiStore-成功返回】payCode={}, orderId={}", payCode, orderId);
         return "success";
     }
 
@@ -5845,6 +6053,12 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             storeOrder.setUseIntegral(BigDecimal.valueOf(dto.getUsedIntegral()));
             storeOrder.setBackIntegral(BigDecimal.ZERO);
             storeOrder.setGainIntegral(BigDecimal.ZERO);
+            // 积分抵扣说明:多店铺场景由 Controller 预分配后透传到 param,单店铺场景从 dto 取(均可能为空)
+            if (StringUtils.isNotEmpty(param.getIntegralDesc())) {
+                storeOrder.setIntegralDesc(param.getIntegralDesc());
+            } else if (StringUtils.isNotEmpty(dto.getIntegralDesc())) {
+                storeOrder.setIntegralDesc(dto.getIntegralDesc());
+            }
             storeOrder.setMark(param.getMark());
             //todo 获取成本价
             BigDecimal costPrice = this.getOrderSumPrice(carts, "costPrice");
@@ -5853,7 +6067,12 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             storeOrder.setShippingType(1);
             storeOrder.setCreateTime(new Date());
             storeOrder.setStatus(0);
-            FsStorePromotionComputeResultVO promotionResult = applyPromotionFields(storeOrder, carts, userId, param.getCouponUserId(), param.getStoreId());
+            boolean skipPromotion = false;
+            if (StringUtils.isNotEmpty(param.getCreateOrderKey())) {
+                Boolean isAdjusted = redisCache.getCacheObject("createOrderIsAdjusted:" + param.getCreateOrderKey());
+                skipPromotion = (isAdjusted != null && isAdjusted);
+            }
+            FsStorePromotionComputeResultVO promotionResult = applyPromotionFields(storeOrder, carts, userId, param.getCouponUserId(), param.getStoreId(), skipPromotion);
             //后台制单处理
             if (param.getPayPrice() != null && param.getPayPrice().compareTo(BigDecimal.ZERO) > 0) {
                 storeOrder.setPayPrice(param.getPayPrice());
@@ -6051,7 +6270,175 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             createOrderKeys = Arrays.asList(params.getCreateOrderKey().split(","));
         }
 
+        // ========== 多店铺积分合并计算 ==========
+        // 1. 收集各子订单购物车,计算各自的 totalPrice 和 payPrice(未扣积分前)
+        int storeCount = orderKeys.size();
+        BigDecimal[] subTotalPrices = new BigDecimal[storeCount];
+        BigDecimal[] subPreDeductPayPrices = new BigDecimal[storeCount];
+        BigDecimal combinedTotal = BigDecimal.ZERO;
+
+        for (int i = 0; i < storeCount; i++) {
+            List<FsStoreCartQueryVO> carts = redisCache.getCacheObject("orderCarts:" + orderKeys.get(i));
+            if (carts == null) {
+                throw new CustomException("订单已过期", 501);
+            }
+            BigDecimal subPayPrice = getOrderSumPrice(carts, "truePrice");
+
+            String createOrderKey = (createOrderKeys != null && i < createOrderKeys.size()) ? createOrderKeys.get(i) : null;
+            if (StringUtils.isNotEmpty(createOrderKey)) {
+                Integer payType = redisCache.getCacheObject("createOrderPayType:" + createOrderKey);
+                if (payType != null && payType == 3) {
+                    subPayPrice = redisCache.getCacheObject("createOrderAmount:" + createOrderKey);
+                } else {
+                    BigDecimal money = redisCache.getCacheObject("createOrderMoney:" + createOrderKey);
+                    if (money != null) {
+                        subPayPrice = money;
+                    }
+                }
+                FsOrderMoneyDetailsVo detailsVo = redisCache.getCacheObject("createOrderMoneyDetails:" + createOrderKey);
+                if (detailsVo != null) {
+                    subPayPrice = detailsVo.getPayPrice();
+                }
+            }
+            subPreDeductPayPrices[i] = subPayPrice;
+            // 用 payPrice 作为该子订单的"实付前金额"来参与比例分摊
+            subTotalPrices[i] = subPayPrice;
+            combinedTotal = NumberUtil.add(combinedTotal, subPayPrice);
+        }
+
+        // 2. 读积分配置,计算合并抵扣
+        Integer[] preAllocatedIntegrals = new Integer[storeCount];
+        BigDecimal[] preAllocatedDeductions = new BigDecimal[storeCount];
+        String[] integralDescs = new String[storeCount];
+
+        if (params.getUseIntegral() != null && params.getUseIntegral() == 1) {
+            FsUserScrm user = userService.selectFsUserById(uid);
+            if (user.getIntegral().intValue() > 0) {
+                // 新积分配置:points.grantRule
+                Integer payRate = 100;       // X积分 = 1元,默认100
+                Integer maxDeductPercent = 50; // 最高抵扣比例,默认50%
+                BigDecimal minDeductAmount = new BigDecimal(100); // 满X元可抵扣,默认100
+                Integer minUseIntegral = 0;  // 最低起用积分,默认不限制
+                Integer deductEnabled = 1;   // 积分抵扣开关,默认开启
+                String json = configService.selectConfigByKey("points.grantRule");
+                if (StringUtils.isNotEmpty(json)) {
+                    try {
+                        PointsGrantRuleConfig rule = JSONUtil.toBean(json, PointsGrantRuleConfig.class);
+                        if (rule != null && rule.getEnabled() != null && rule.getEnabled() == 1) {
+                            if (rule.getPayRate() != null && rule.getPayRate() > 0) {
+                                payRate = rule.getPayRate();
+                            }
+                            if (rule.getMaxDeductPercent() != null && rule.getMaxDeductPercent() > 0) {
+                                maxDeductPercent = rule.getMaxDeductPercent();
+                            }
+                            if (rule.getMinDeductAmount() != null) {
+                                minDeductAmount = rule.getMinDeductAmount();
+                            }
+                            if (rule.getMinUseIntegral() != null && rule.getMinUseIntegral() > 0) {
+                                minUseIntegral = rule.getMinUseIntegral();
+                            }
+                            if (rule.getDeductEnabled() != null) {
+                                deductEnabled = rule.getDeductEnabled();
+                            }
+                        }
+                    } catch (Exception e) {
+                        log.warn("积分抵扣规则配置解析失败,使用默认值", e);
+                    }
+                }
+
+                // 积分抵扣功能关闭,跳过
+                if (deductEnabled == 0) {
+                    // do nothing, preAllocated arrays remain null/zero
+                } else if (minUseIntegral > 0 && user.getIntegral().intValue() < minUseIntegral) {
+                    // 用户积分不足最低起用门槛,跳过
+                } else if (combinedTotal.compareTo(minDeductAmount) >= 0) {
+                    // 可用积分 = 用户全部积分(maxDeduct 已废弃,改用 maxDeductPercent 按比例限制)
+                    long totalIntegral = user.getIntegral().longValue();
+                    // 总抵扣金额 = 总积分 / payRate(如 100积分/100 = 1.00元)
+                    BigDecimal totalDeduction = BigDecimal.valueOf(totalIntegral)
+                            .divide(BigDecimal.valueOf(payRate), 2, RoundingMode.HALF_UP);
+                    // 最高抵扣比例限制:抵扣金额不超过订单总额的 maxDeductPercent%
+                    if (maxDeductPercent > 0 && maxDeductPercent < 100) {
+                        BigDecimal maxDeductionAmount = combinedTotal
+                                .multiply(BigDecimal.valueOf(maxDeductPercent))
+                                .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
+                        if (totalDeduction.compareTo(maxDeductionAmount) > 0) {
+                            totalDeduction = maxDeductionAmount;
+                            // 反推实际可用积分(向下取整,保证不超额)
+                            totalIntegral = totalDeduction
+                                    .multiply(BigDecimal.valueOf(payRate))
+                                    .setScale(0, RoundingMode.DOWN)
+                                    .longValue();
+                        }
+                    }
+
+                    long allocatedIntegralSum = 0;
+                    BigDecimal allocatedDeductionSum = BigDecimal.ZERO;
+
+                    for (int i = 0; i < storeCount; i++) {
+                        if (combinedTotal.compareTo(BigDecimal.ZERO) <= 0) {
+                            preAllocatedDeductions[i] = BigDecimal.ZERO;
+                            preAllocatedIntegrals[i] = 0;
+                            integralDescs[i] = null;
+                            continue;
+                        }
+
+                        BigDecimal ratio = NumberUtil.div(subTotalPrices[i], combinedTotal, 6);
+
+                        // 积分分配:前 N-1 个四舍五入,最后一个用减法兜底确保分文不差
+                        int subIntegral;
+                        if (i == storeCount - 1) {
+                            subIntegral = (int) (totalIntegral - allocatedIntegralSum);
+                        } else {
+                            subIntegral = BigDecimal.valueOf(totalIntegral)
+                                    .multiply(ratio)
+                                    .setScale(0, RoundingMode.HALF_UP)
+                                    .intValue();
+                        }
 
+                        // 抵扣金额分配:同理,最后一个用减法兜底
+                        BigDecimal subDeduction;
+                        if (i == storeCount - 1) {
+                            subDeduction = NumberUtil.sub(totalDeduction, allocatedDeductionSum);
+                        } else {
+                            subDeduction = NumberUtil.mul(totalDeduction, ratio);
+                        }
+
+                        // 不超过该子订单应付金额
+                        if (subDeduction.compareTo(subPreDeductPayPrices[i]) > 0) {
+                            subDeduction = subPreDeductPayPrices[i];
+                            // 积分也等比例缩减
+                            subIntegral = subDeduction
+                                    .multiply(BigDecimal.valueOf(payRate))
+                                    .setScale(0, RoundingMode.HALF_UP)
+                                    .intValue();
+                        }
+
+                        // cap 之后再累加,确保最后一个店铺减法兜底正确
+                        if (i != storeCount - 1) {
+                            allocatedIntegralSum += subIntegral;
+                            allocatedDeductionSum = NumberUtil.add(allocatedDeductionSum, subDeduction);
+                        }
+
+                        preAllocatedDeductions[i] = subDeduction;
+                        preAllocatedIntegrals[i] = subIntegral;
+                        integralDescs[i] = String.format(
+                            "组合订单%d个店铺合计%.2f元,满%s元可抵扣(比例上限%d%%),共使用%d积分抵扣%.2f元,本单分摊%.2f元(%d积分)",
+                            storeCount,
+                            combinedTotal.doubleValue(),
+                            minDeductAmount.stripTrailingZeros().toPlainString(),
+                            maxDeductPercent,
+                            totalIntegral,
+                            totalDeduction.doubleValue(),
+                            subDeduction.doubleValue(),
+                            subIntegral
+                        );
+                    }
+                }
+            }
+        }
+
+        // 3. 逐子订单计算
         for (int i = 0; i < orderKeys.size(); i++) {
             FsStoreOrderComputedParam computedParam = new FsStoreOrderComputedParam();
             BeanUtils.copyProperties(params, computedParam);
@@ -6059,8 +6446,14 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             if (!createOrderKeys.isEmpty() && createOrderKeys.get(i) != null) {
                 computedParam.setCreateOrderKey(createOrderKeys.get(i));
             }
+            // 注入预分配积分抵扣
+            if (preAllocatedDeductions[i] != null && preAllocatedDeductions[i].compareTo(BigDecimal.ZERO) > 0) {
+                computedParam.setPreAllocatedDeduction(preAllocatedDeductions[i]);
+                computedParam.setPreAllocatedIntegral(preAllocatedIntegrals[i]);
+            }
 
             FsStoreOrderComputeDTO dto = this.computedOrderMultiStore(uid, computedParam);
+            dto.setIntegralDesc(integralDescs[i]);
             payPriceTotal = NumberUtil.add(payPriceTotal, dto.getPayPrice());
             totalPriceTotal = NumberUtil.add(totalPriceTotal, dto.getTotalPrice());
             usedIntegralTotal += dto.getUsedIntegral();
@@ -6373,37 +6766,82 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         payPrice = NumberUtil.add(payPrice, payPostage);
         FsUserScrm user = userService.selectFsUserById(uid);
         // 积分抵扣
-        BigDecimal deductionPrice = BigDecimal.ZERO; //积分抵扣金额
-        double usedIntegral = 0; //使用了多少积分
+        BigDecimal deductionPrice = BigDecimal.ZERO;
+        double usedIntegral = 0;
         if (param.getUseIntegral() != null && param.getUseIntegral() == 1 && user.getIntegral().intValue() > 0) {
-            //如果积分总和小于用户积分 抵扣比例 计算抵扣价格
-            //默认配置
-            Double integralMax = Double.valueOf(100);
-            BigDecimal integralFull = new BigDecimal(100);
-            Double integralRatio = Double.valueOf(1);
-            String json = configService.selectConfigByKey("store.integral");
-            if (StringUtils.isEmpty(json)) {
-
-            } else {
-                StoreIntegralConfig integralConfig = JSONUtil.toBean(json, StoreIntegralConfig.class);
-                integralMax = integralConfig.getIntegralMax();
-                integralFull = integralConfig.getIntegralFull();
-                integralRatio = integralConfig.getIntegralRatio();
-            }
-            if (priceGroup.getTotalPrice().compareTo(integralFull) >= 0) {
-                Double userIntegral = user.getIntegral().doubleValue();
-                if (integralMax.intValue() > 0 && Double.compare(userIntegral, integralMax) >= 0) {
-                    userIntegral = integralMax;
-                }
-                deductionPrice = BigDecimal.valueOf(NumberUtil.mul(userIntegral, integralRatio));
-                if (deductionPrice.compareTo(payPrice) < 0) {
-                    payPrice = NumberUtil.sub(payPrice, deductionPrice);
-                    usedIntegral = userIntegral;
-                } else {
+            // 多店铺场景:使用预分配的抵扣金额
+            if (param.getPreAllocatedDeduction() != null && param.getPreAllocatedDeduction().compareTo(BigDecimal.ZERO) > 0) {
+                deductionPrice = param.getPreAllocatedDeduction();
+                usedIntegral = param.getPreAllocatedIntegral() != null ? param.getPreAllocatedIntegral().doubleValue() : 0;
+                if (deductionPrice.compareTo(payPrice) >= 0) {
                     deductionPrice = payPrice;
                     payPrice = BigDecimal.ZERO;
-                    usedIntegral = NumberUtil.round(NumberUtil.div(deductionPrice,
-                            BigDecimal.valueOf(integralRatio)), 2).doubleValue();
+                } else {
+                    payPrice = NumberUtil.sub(payPrice, deductionPrice);
+                }
+            } else {
+                // 单店铺场景:使用新积分配置 points.grantRule
+                Integer payRate = 100;
+                Integer maxDeductPercent = 50;
+                BigDecimal minDeductAmount = new BigDecimal(100);
+                Integer minUseIntegral = 0;
+                Integer deductEnabled = 1;
+                String json = configService.selectConfigByKey("points.grantRule");
+                if (StringUtils.isNotEmpty(json)) {
+                    try {
+                        PointsGrantRuleConfig rule = JSONUtil.toBean(json, PointsGrantRuleConfig.class);
+                        if (rule != null && rule.getEnabled() != null && rule.getEnabled() == 1) {
+                            if (rule.getPayRate() != null && rule.getPayRate() > 0) {
+                                payRate = rule.getPayRate();
+                            }
+                            if (rule.getMaxDeductPercent() != null && rule.getMaxDeductPercent() > 0) {
+                                maxDeductPercent = rule.getMaxDeductPercent();
+                            }
+                            if (rule.getMinDeductAmount() != null) {
+                                minDeductAmount = rule.getMinDeductAmount();
+                            }
+                            if (rule.getMinUseIntegral() != null && rule.getMinUseIntegral() > 0) {
+                                minUseIntegral = rule.getMinUseIntegral();
+                            }
+                            if (rule.getDeductEnabled() != null) {
+                                deductEnabled = rule.getDeductEnabled();
+                            }
+                        }
+                    } catch (Exception e) {
+                        log.warn("积分抵扣规则配置解析失败,使用默认值", e);
+                    }
+                }
+                // 积分抵扣功能关闭或积分不足起用门槛,跳过
+                if (deductEnabled == 1
+                        && (minUseIntegral == 0 || user.getIntegral().intValue() >= minUseIntegral)
+                        && priceGroup.getTotalPrice().compareTo(minDeductAmount) >= 0) {
+                    long userIntegral = user.getIntegral().longValue();
+                    deductionPrice = BigDecimal.valueOf(userIntegral)
+                            .divide(BigDecimal.valueOf(payRate), 2, RoundingMode.HALF_UP);
+                    // 最高抵扣比例限制
+                    if (maxDeductPercent > 0 && maxDeductPercent < 100) {
+                        BigDecimal maxDeductionAmount = priceGroup.getTotalPrice()
+                                .multiply(BigDecimal.valueOf(maxDeductPercent))
+                                .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
+                        if (deductionPrice.compareTo(maxDeductionAmount) > 0) {
+                            deductionPrice = maxDeductionAmount;
+                            userIntegral = deductionPrice
+                                    .multiply(BigDecimal.valueOf(payRate))
+                                    .setScale(0, RoundingMode.DOWN)
+                                    .longValue();
+                        }
+                    }
+                    if (deductionPrice.compareTo(payPrice) < 0) {
+                        payPrice = NumberUtil.sub(payPrice, deductionPrice);
+                        usedIntegral = userIntegral;
+                    } else {
+                        deductionPrice = payPrice;
+                        payPrice = BigDecimal.ZERO;
+                        usedIntegral = deductionPrice
+                                .multiply(BigDecimal.valueOf(payRate))
+                                .setScale(0, RoundingMode.DOWN)
+                                .doubleValue();
+                    }
                 }
             }
         }
@@ -6445,6 +6883,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 .usedIntegral(usedIntegral)
                 .payIntegral(priceGroup.getPayIntegral())
                 .promotionDiscountAmount(promotionResult.getPromotionDiscountAmount())
+                .integralDesc(null)
                 .build();
     }
 
@@ -6501,16 +6940,21 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     /**
      * 计算并应用满减活动优惠到订单对象(不持久化),返回计算结果供后续复用
      *
+     * @param skipPromotion 制单改价场景应跳过满减计算,避免重复优惠
      * @return 活动计算结果,无活动或无优惠时返回 null
      */
     private FsStorePromotionComputeResultVO applyPromotionFields(FsStoreOrderScrm storeOrder,
                                                                   List<FsStoreCartQueryVO> carts,
                                                                   long userId,
                                                                   Long couponUserId,
-                                                                  Long storeId) {
+                                                                  Long storeId,
+                                                                  boolean skipPromotion) {
         if (storeOrder == null) {
             return null;
         }
+        if (skipPromotion) {
+            return null;
+        }
         FsStorePromotionComputeResultVO promotion = promotionComputeService.autoApplyBestPromotion(
                 userId, carts, storeId, couponUserId);
         if (promotion == null || promotion.getPromotionDiscountAmount() == null
@@ -6790,6 +7234,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public void cancelOrderReuse(FsStoreOrderScrm order) {
         if (order.getStatus() == OrderInfoEnum.STATUS_0.getValue()) {
             if (!StringUtils.isEmpty(order.getExtendOrderId())) {

+ 1 - 1
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java

@@ -599,7 +599,7 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
             if(oldFsStoreProduct.getIsAudit() != null){
                 product.setIsAudit("0");
             }
-            fsStoreProductMapper.updateFsStoreProduct(product);
+            fsStoreProductMapper.updateFsStoreProductNullable(product);
 //            else外面也会调用这个方法,为啥会在if中调用执行handleProductAttributes
 //            handleProductAttributes(param, product, storeId);
         } else{

+ 9 - 4
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStorePromotionComputeServiceImpl.java

@@ -332,10 +332,10 @@ public class FsStorePromotionComputeServiceImpl implements IFsStorePromotionComp
                                          List<FsStorePromotionTier> tiers,
                                          Long couponUserId,
                                          int usedCount) {
-        // if (activity.getLimitPerUser() != null && activity.getLimitPerUser() > 0
-        //         && usedCount >= activity.getLimitPerUser()) {
-        //     return "您已超过该活动参与次数限制";
-        // }
+        if (activity.getLimitPerUser() != null && activity.getLimitPerUser() > 0
+                && usedCount >= activity.getLimitPerUser()) {
+            return "您已超过该活动参与次数限制";
+        }
         if (couponUserId != null && activity.getIsStackable() != null && activity.getIsStackable() == 0) {
             return "该活动不可与优惠券叠加使用";
         }
@@ -520,6 +520,11 @@ public class FsStorePromotionComputeServiceImpl implements IFsStorePromotionComp
         Date now = new Date();
         usage.setUsageTime(now);
         usage.setCreateTime(now);
+        // 幂等校验:同一订单同一活动只记录一次
+        FsStorePromotionUsage existUsage = usageMapper.selectByOrderIdAndActivityId(order.getId(), promotion.getPromotionActivityId());
+        if (existUsage != null) {
+            return;
+        }
         usageMapper.insertFsStorePromotionUsage(usage);
     }
 

+ 10 - 4
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStorePromotionServiceImpl.java

@@ -36,6 +36,7 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
 
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
@@ -387,6 +388,7 @@ public class FsStorePromotionServiceImpl implements IFsStorePromotionService {
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public int enableActivity(Long id) {
         FsStorePromotionActivity activity = activityMapper.selectFsStorePromotionActivityById(id);
         if (activity == null) {
@@ -395,11 +397,13 @@ public class FsStorePromotionServiceImpl implements IFsStorePromotionService {
         if (activity.getActivityEndTime().before(new Date())) {
             throw new ServiceException("活动已过期,无法启用");
         }
-        int conflict = activityMapper.countConflictActivity(activity.getStoreId(), activity.getStartTime(), activity.getActivityEndTime(), id);
-        if (conflict > 0) {
+        // 条件更新:原子性检查时间冲突并启用,避免并发竞态
+        int affected = activityMapper.enableActivityIfNoConflict(id, activity.getStoreId(),
+                activity.getStartTime(), activity.getActivityEndTime());
+        if (affected == 0) {
             throw new ServiceException("该时间段内已存在启用的满减活动,请调整时间");
         }
-        return activityMapper.updateManualStatus(id, 1);
+        return affected;
     }
 
     @Override
@@ -580,12 +584,14 @@ public class FsStorePromotionServiceImpl implements IFsStorePromotionService {
         if (CollectionUtils.isEmpty(tiers)) {
             return;
         }
+        // 保存前按门槛金额升序排序,确保 sortOrder 与门槛金额顺序一致
+        tiers.sort(Comparator.comparing(FsStorePromotionTierDTO::getThresholdAmount));
         List<FsStorePromotionTier> tierList = new ArrayList<>();
         int sort = 1;
         for (FsStorePromotionTierDTO dto : tiers) {
             FsStorePromotionTier tier = new FsStorePromotionTier();
             tier.setActivityId(activityId);
-            tier.setSortOrder(dto.getSortOrder() != null ? dto.getSortOrder() : sort);
+            tier.setSortOrder(sort);
             tier.setThresholdAmount(dto.getThresholdAmount());
             tier.setDiscountAmount(dto.getDiscountAmount());
             tierList.add(tier);

+ 99 - 13
fs-service/src/main/java/com/fs/hisStore/service/impl/FsUserIntegralLogsScrmServiceImpl.java

@@ -1,5 +1,6 @@
 package com.fs.hisStore.service.impl;
 
+import com.fs.common.sharding.ShardingUtil;
 import com.fs.common.utils.DateUtils;
 import com.fs.hisStore.domain.FsUserScrm;
 import com.fs.hisStore.domain.FsUserIntegralLogsScrm;
@@ -12,14 +13,22 @@ import com.fs.hisStore.vo.FsUserIntegralLogsListUVO;
 import com.fs.hisStore.vo.FsUserIntegralLogsListVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.math.BigDecimal;
+import java.util.ArrayList;
 import java.util.Date;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Set;
 
 /**
  * 积分记录Service业务层处理
  *
+ * <p>手动分表:fs_user_integral_logs 按 user_id % 10 分为 _0 ~ _9 共 10 张物理表。
+ * 由 Service 层通过 {@link ShardingUtil} 计算分表后缀并传给 Mapper,不再依赖 ShardingSphere 路由。</p>
+ *
  * @author fs
  * @date 2023-11-02
  */
@@ -39,7 +48,16 @@ public class FsUserIntegralLogsScrmServiceImpl implements IFsUserIntegralLogsScr
     @Override
     public FsUserIntegralLogsScrm selectFsUserIntegralLogsById(Long id)
     {
-        return fsUserIntegralLogsMapper.selectFsUserIntegralLogsById(id);
+        // 无分片键,需扫描全部分表
+        for (Integer shard : ShardingUtil.allShards())
+        {
+            FsUserIntegralLogsScrm log = fsUserIntegralLogsMapper.selectFsUserIntegralLogsById(id, shard);
+            if (log != null)
+            {
+                return log;
+            }
+        }
+        return null;
     }
 
     /**
@@ -51,7 +69,18 @@ public class FsUserIntegralLogsScrmServiceImpl implements IFsUserIntegralLogsScr
     @Override
     public List<FsUserIntegralLogsScrm> selectFsUserIntegralLogsList(FsUserIntegralLogsScrm fsUserIntegralLogs)
     {
-        return fsUserIntegralLogsMapper.selectFsUserIntegralLogsList(fsUserIntegralLogs);
+        // 有 userId 精确路由到单表,无 userId 扫描全部分表合并
+        if (fsUserIntegralLogs.getUserId() != null)
+        {
+            int shard = ShardingUtil.shardOf(fsUserIntegralLogs.getUserId());
+            return fsUserIntegralLogsMapper.selectFsUserIntegralLogsList(fsUserIntegralLogs, shard);
+        }
+        List<FsUserIntegralLogsScrm> result = new ArrayList<>();
+        for (Integer shard : ShardingUtil.allShards())
+        {
+            result.addAll(fsUserIntegralLogsMapper.selectFsUserIntegralLogsList(fsUserIntegralLogs, shard));
+        }
+        return result;
     }
 
     /**
@@ -64,7 +93,8 @@ public class FsUserIntegralLogsScrmServiceImpl implements IFsUserIntegralLogsScr
     public int insertFsUserIntegralLogs(FsUserIntegralLogsScrm fsUserIntegralLogs)
     {
         fsUserIntegralLogs.setCreateTime(DateUtils.getNowDate());
-        return fsUserIntegralLogsMapper.insertFsUserIntegralLogs(fsUserIntegralLogs);
+        int shard = ShardingUtil.shardOf(fsUserIntegralLogs.getUserId());
+        return fsUserIntegralLogsMapper.insertFsUserIntegralLogs(fsUserIntegralLogs, shard);
     }
 
     /**
@@ -76,7 +106,8 @@ public class FsUserIntegralLogsScrmServiceImpl implements IFsUserIntegralLogsScr
     @Override
     public int updateFsUserIntegralLogs(FsUserIntegralLogsScrm fsUserIntegralLogs)
     {
-        return fsUserIntegralLogsMapper.updateFsUserIntegralLogs(fsUserIntegralLogs);
+        int shard = ShardingUtil.shardOf(fsUserIntegralLogs.getUserId());
+        return fsUserIntegralLogsMapper.updateFsUserIntegralLogs(fsUserIntegralLogs, shard);
     }
 
     /**
@@ -88,7 +119,13 @@ public class FsUserIntegralLogsScrmServiceImpl implements IFsUserIntegralLogsScr
     @Override
     public int deleteFsUserIntegralLogsByIds(Long[] ids)
     {
-        return fsUserIntegralLogsMapper.deleteFsUserIntegralLogsByIds(ids);
+        // 无分片键,需扫描全部分表
+        int total = 0;
+        for (Integer shard : ShardingUtil.allShards())
+        {
+            total += fsUserIntegralLogsMapper.deleteFsUserIntegralLogsByIds(ids, shard);
+        }
+        return total;
     }
 
     /**
@@ -100,27 +137,61 @@ public class FsUserIntegralLogsScrmServiceImpl implements IFsUserIntegralLogsScr
     @Override
     public int deleteFsUserIntegralLogsById(Long id)
     {
-        return fsUserIntegralLogsMapper.deleteFsUserIntegralLogsById(id);
+        // 无分片键,需扫描全部分表
+        int total = 0;
+        for (Integer shard : ShardingUtil.allShards())
+        {
+            total += fsUserIntegralLogsMapper.deleteFsUserIntegralLogsById(id, shard);
+        }
+        return total;
     }
 
     @Override
     public List<FsUserIntegralLogsListVO> selectFsUserIntegralLogsListVO(FsUserIntegralLogsParam fsUserIntegralLogs) {
-        return fsUserIntegralLogsMapper.selectFsUserIntegralLogsListVO(fsUserIntegralLogs);
+        // 有 userId 精确路由到单表,无 userId 扫描全部分表合并
+        if (fsUserIntegralLogs.getUserId() != null)
+        {
+            int shard = ShardingUtil.shardOf(fsUserIntegralLogs.getUserId());
+            return fsUserIntegralLogsMapper.selectFsUserIntegralLogsListVO(fsUserIntegralLogs, shard);
+        }
+        List<FsUserIntegralLogsListVO> result = new ArrayList<>();
+        for (Integer shard : ShardingUtil.allShards())
+        {
+            result.addAll(fsUserIntegralLogsMapper.selectFsUserIntegralLogsListVO(fsUserIntegralLogs, shard));
+        }
+        return result;
     }
 
     @Override
     public List<FsUserIntegralLogsListUVO> selectFsUserIntegralLogsListUVO(FsUserIntegralLogsListUParam param) {
-        return fsUserIntegralLogsMapper.selectFsUserIntegralLogsListUVO(param);
+        // 有 userId 精确路由到单表,无 userId 扫描全部分表合并
+        if (param.getUserId() != null)
+        {
+            int shard = ShardingUtil.shardOf(param.getUserId());
+            return fsUserIntegralLogsMapper.selectFsUserIntegralLogsListUVO(param, shard);
+        }
+        List<FsUserIntegralLogsListUVO> result = new ArrayList<>();
+        for (Integer shard : ShardingUtil.allShards())
+        {
+            result.addAll(fsUserIntegralLogsMapper.selectFsUserIntegralLogsListUVO(param, shard));
+        }
+        return result;
     }
 
     @Override
     public void subFsUserIntegralLogsByOrder5() {
-        List<Long> longs = fsUserIntegralLogsMapper.subFsUserIntegralLogsByOrder5();
-        for (Long id : longs) {
+        // 循环所有分表,合并 user_id 列表去重
+        Set<Long> userIds = new LinkedHashSet<>();
+        for (Integer shard : ShardingUtil.allShards())
+        {
+            userIds.addAll(fsUserIntegralLogsMapper.subFsUserIntegralLogsByOrder5(shard));
+        }
+        for (Long id : userIds) {
 
             FsUserScrm fsUser = fsUserMapper.selectFsUserByUserId(id);
             if(fsUser != null &&fsUser.getIntegral()== 0){
-                List<FsUserIntegralLogsScrm> log =  fsUserIntegralLogsMapper.selectFsUserIntegralByStatus0(id);
+                int shard = ShardingUtil.shardOf(id);
+                List<FsUserIntegralLogsScrm> log =  fsUserIntegralLogsMapper.selectFsUserIntegralByStatus0(id, shard);
                 for (FsUserIntegralLogsScrm fsUserIntegralLogs : log) {
                     Long integral = fsUser.getIntegral();
                     BigDecimal integralLog = fsUserIntegralLogs.getIntegral();
@@ -131,7 +202,7 @@ public class FsUserIntegralLogsScrmServiceImpl implements IFsUserIntegralLogsScr
                     fsUser.setIntegral(balance);
                     fsUserMapper.updateFsUser(fsUser);
                     fsUserIntegralLogs.setStatus(1);
-                    fsUserIntegralLogsMapper.updateFsUserIntegralLogs(fsUserIntegralLogs);
+                    fsUserIntegralLogsMapper.updateFsUserIntegralLogs(fsUserIntegralLogs, shard);
                     FsUserIntegralLogsScrm logs = new FsUserIntegralLogsScrm();
                     logs.setIntegral(integralLog.negate());
                     logs.setUserId(id);
@@ -139,7 +210,7 @@ public class FsUserIntegralLogsScrmServiceImpl implements IFsUserIntegralLogsScr
                     logs.setLogType(7);
                     logs.setStatus(1);
                     logs.setCreateTime(new Date());
-                    fsUserIntegralLogsMapper.insertFsUserIntegralLogs(logs);
+                    fsUserIntegralLogsMapper.insertFsUserIntegralLogs(logs, shard);
                 }
 
 
@@ -147,4 +218,19 @@ public class FsUserIntegralLogsScrmServiceImpl implements IFsUserIntegralLogsScr
 
         }
     }
+
+    /**
+     * 插入积分日志(独立事务,供外部 @Transactional 方法调用)
+     * <p>使用 REQUIRES_NEW 传播级别,确保在调用方事务中也能正确写入分表。
+     * 内部通过 {@link ShardingUtil#shardOf(Long)} 计算分表后缀并传给 Mapper。</p>
+     *
+     * @param logs 积分日志记录
+     * @return 影响行数
+     */
+    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
+    @Override
+    public int insertLog(FsUserIntegralLogsScrm logs) {
+        int shard = ShardingUtil.shardOf(logs.getUserId());
+        return fsUserIntegralLogsMapper.insertFsUserIntegralLogs(logs, shard);
+    }
 }

+ 12 - 1
fs-service/src/main/java/com/fs/hisStore/service/impl/FsUserScrmServiceImpl.java

@@ -4,6 +4,7 @@ import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.json.JSONUtil;
 import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.fs.common.constant.HttpStatus;
+import com.fs.common.exception.CustomException;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.ResponseResult;
 import com.fs.common.core.domain.entity.SysDictData;
@@ -232,7 +233,17 @@ public class FsUserScrmServiceImpl implements IFsUserScrmService
 
     @Override
     public int decIntegral(Long uid, double usedIntegral) {
-        return fsUserMapper.decIntegral(uid,usedIntegral);
+        int rows = fsUserMapper.decIntegral(uid, usedIntegral);
+        // 影响行数为 0 说明用户积分余额不足(可能预计算与实际扣减之间被其他途径消耗),扣减失败
+        if (rows == 0) {
+            throw new CustomException("用户积分余额不足,无法抵扣");
+        }
+        return rows;
+    }
+
+    @Override
+    public int incIntegral(Long uid, double integral) {
+        return fsUserMapper.incIntegral(uid, integral);
     }
 
     @Override

+ 2 - 1
fs-service/src/main/java/com/fs/hisStore/service/impl/FsUserSignScrmServiceImpl.java

@@ -9,6 +9,7 @@ import cn.hutool.core.util.NumberUtil;
 import cn.hutool.json.JSONArray;
 import cn.hutool.json.JSONUtil;
 import com.fs.common.exception.CustomException;
+import com.fs.common.sharding.ShardingUtil;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.hisStore.config.StoreSignConfig;
@@ -171,7 +172,7 @@ public class FsUserSignScrmServiceImpl implements IFsUserSignScrmService
         logs.setLogType(1);
         logs.setBusinessId(userSign.getId().toString());
         logs.setCreateTime(new Date());
-        integralLogsMapper.insertFsUserIntegralLogs(logs);
+        integralLogsMapper.insertFsUserIntegralLogs(logs, ShardingUtil.shardOf(logs.getUserId()));
         //用户积分增加
         FsUserScrm userMap =new FsUserScrm();
         userMap.setIntegral(user.getIntegral()+signNumber);

+ 7 - 1
fs-service/src/main/java/com/fs/hisStore/support/FsStorePromotionTierCalculator.java

@@ -10,9 +10,11 @@ import java.io.Serializable;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * 阶梯满减/件数折扣计算工具
@@ -96,8 +98,12 @@ public final class FsStorePromotionTierCalculator {
         if (eligibleAmount == null || tiers == null || tiers.isEmpty()) {
             return null;
         }
+        // 防御性排序:确保按门槛金额升序,避免上游未排序时取到非最高档
+        List<FsStorePromotionTier> sortedTiers = tiers.stream()
+                .sorted(Comparator.comparing(FsStorePromotionTier::getThresholdAmount))
+                .collect(Collectors.toList());
         FsStorePromotionTier matched = null;
-        for (FsStorePromotionTier tier : tiers) {
+        for (FsStorePromotionTier tier : sortedTiers) {
             if (tier.getThresholdAmount() != null
                     && eligibleAmount.compareTo(tier.getThresholdAmount()) >= 0) {
                 matched = tier;

+ 2 - 5
fs-service/src/main/java/com/fs/pay/pay/util/PayUtil.java

@@ -107,7 +107,7 @@ public class PayUtil {
 
 
 
-    public static boolean isSgin(String s){
+    public static boolean isSign(String s){
         Gson gson = new Gson();
         // 使用Type引用来获取Map类型
         Type type = new com.google.gson.reflect.TypeToken<Map<String, String>>(){}.getType();
@@ -119,9 +119,6 @@ public class PayUtil {
         }
         String sign = PayUtil.sign(map);
         String sg = map.get("sign");
-        if (sg.equals(sign)){
-            return false;
-        }
-        return true;
+        return sg.equals(sign);
     }
 }

+ 3 - 3
fs-service/src/main/java/com/fs/pay/service/impl/PayScrmServiceImpl.java

@@ -378,7 +378,7 @@ public class PayScrmServiceImpl implements IPayService {
         wxJspayDTO.setNotifyUrl(PayConfig.notifyUrl);
         Map<String, String> map = wxJspayDTO.toMap();
         String responseStr = endPayHttp(PayConfig.wxJspayUrl, map);
-        if (isSgin(responseStr)){
+        if (isSign(responseStr)){
             //请求失败101 或者验签失败
         }
         CreateWxOrderResult result = GsonFromJson(responseStr, CreateWxOrderResult.class);
@@ -392,7 +392,7 @@ public class PayScrmServiceImpl implements IPayService {
         refundDTO.setAccount(PayConfig.account);
         Map<String, String> map = refundDTO.toMap();
         String responseStr = endPayHttp(PayConfig.refundUrl, map);
-        if (isSgin(responseStr)){
+        if (isSign(responseStr)){
             //请求失败101 或者验签失败
         }
         RefundResult result = GsonFromJson(responseStr, RefundResult.class);
@@ -405,7 +405,7 @@ public class PayScrmServiceImpl implements IPayService {
         o.setAccount(PayConfig.account);
         Map<String, String> map = o.toMap();
         String responseStr = endPayHttp(PayConfig.orderQueryUrl, map);
-        if (isSgin(responseStr)){
+        if (isSign(responseStr)){
             //请求失败101 或者验签失败
         }
         OrderResult orderResponse = GsonFromJson(responseStr, OrderResult.class);

+ 1 - 0
fs-service/src/main/java/com/fs/system/vo/DictVO.java

@@ -3,6 +3,7 @@ package com.fs.system.vo;
 import java.io.Serializable;
 
 public class DictVO implements Serializable {
+    private static final long serialVersionUID = 1L;
     String dictType;
     String dictLabel;
     String dictValue;

+ 2 - 5
fs-service/src/main/java/com/fs/ybPay/util/PayUtil.java

@@ -112,7 +112,7 @@ public class PayUtil {
 
 
 
-    public static boolean isSgin(String s,String secretyKey){
+    public static boolean isSign(String s,String secretyKey){
         Gson gson = new Gson();
         // 使用Type引用来获取Map类型
         Type type = new com.google.gson.reflect.TypeToken<Map<String, String>>(){}.getType();
@@ -124,9 +124,6 @@ public class PayUtil {
         }
         String sign = PayUtil.sign(map,secretyKey);
         String sg = map.get("sign");
-        if (sg.equals(sign)){
-            return false;
-        }
-        return true;
+        return sg.equals(sign);
     }
 }

+ 114 - 45
fs-service/src/main/resources/mapper/his/FsUserIntegralLogsMapper.xml

@@ -16,30 +16,32 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="status"    column="status"    />
     </resultMap>
 
+    <!-- ==================== 手动分表:表名通过 ${shard} 动态拼接(0~9) ==================== -->
+
     <sql id="selectFsUserIntegralLogsVo">
-        select id, user_id,status, log_type,business_type, integral, balance, business_id, create_time from fs_user_integral_logs
+        select id, user_id,status, log_type,business_type, integral, balance, business_id, create_time from fs_user_integral_logs_${shard}
     </sql>
 
-    <select id="selectFsUserIntegralLogsList" parameterType="FsUserIntegralLogs" resultMap="FsUserIntegralLogsResult">
+    <select id="selectFsUserIntegralLogsList" resultMap="FsUserIntegralLogsResult">
         <include refid="selectFsUserIntegralLogsVo"/>
         <where>
-            <if test="userId != null "> and user_id = #{userId}</if>
-            <if test="logType != null  and logType != ''"> and log_type = #{logType}</if>
-            <if test="integral != null "> and integral = #{integral}</if>
-            <if test="balance != null "> and balance = #{balance}</if>
-            <if test="businessId != null  and businessId != ''"> and business_id = #{businessId}</if>
-            <if test="createTime != null "> and create_time = #{createTime}</if>
-            <if test="businessType != null "> and business_type = #{businessType}</if>
+            <if test="log.userId != null "> and user_id = #{log.userId}</if>
+            <if test="log.logType != null  and log.logType != ''"> and log_type = #{log.logType}</if>
+            <if test="log.integral != null "> and integral = #{log.integral}</if>
+            <if test="log.balance != null "> and balance = #{log.balance}</if>
+            <if test="log.businessId != null  and log.businessId != ''"> and business_id = #{log.businessId}</if>
+            <if test="log.createTime != null "> and create_time = #{log.createTime}</if>
+            <if test="log.businessType != null "> and business_type = #{log.businessType}</if>
         </where>
     </select>
 
-    <select id="selectFsUserIntegralLogsById" parameterType="Long" resultMap="FsUserIntegralLogsResult">
+    <select id="selectFsUserIntegralLogsById" resultMap="FsUserIntegralLogsResult">
         <include refid="selectFsUserIntegralLogsVo"/>
         where id = #{id}
     </select>
 
     <select id="selectFsUserIntegralLogsByUserIdAndLogType" resultType="com.fs.his.domain.FsUserIntegralLogs">
-        select * from fs_user_integral_logs where log_type=#{logType} and user_id = #{userId}
+        select * from fs_user_integral_logs_${shard} where log_type=#{logType} and user_id = #{userId}
         <if test="date != null">
             <![CDATA[
             and create_time >= #{date} and create_time < date_add(#{date}, interval 1 day)
@@ -47,53 +49,120 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </if>
     </select>
 
-    <insert id="insertFsUserIntegralLogs" parameterType="FsUserIntegralLogs" useGeneratedKeys="true" keyProperty="id">
-        insert into fs_user_integral_logs
+    <insert id="insertFsUserIntegralLogs" useGeneratedKeys="true" keyProperty="log.id">
+        insert into fs_user_integral_logs_${shard}
         <trim prefix="(" suffix=")" suffixOverrides=",">
-            <if test="userId != null">user_id,</if>
-            <if test="logType != null">log_type,</if>
-            <if test="integral != null">integral,</if>
-            <if test="balance != null">balance,</if>
-            <if test="businessId != null">business_id,</if>
-            <if test="createTime != null">create_time,</if>
-            <if test="businessType != null">business_type,</if>
-            <if test="status != null">status,</if>
+            <if test="log.userId != null">user_id,</if>
+            <if test="log.logType != null">log_type,</if>
+            <if test="log.integral != null">integral,</if>
+            <if test="log.balance != null">balance,</if>
+            <if test="log.businessId != null">business_id,</if>
+            <if test="log.createTime != null">create_time,</if>
+            <if test="log.businessType != null">business_type,</if>
+            <if test="log.status != null">status,</if>
         </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
-            <if test="userId != null">#{userId},</if>
-            <if test="logType != null">#{logType},</if>
-            <if test="integral != null">#{integral},</if>
-            <if test="balance != null">#{balance},</if>
-            <if test="businessId != null">#{businessId},</if>
-            <if test="createTime != null">#{createTime},</if>
-            <if test="businessType != null">#{businessType},</if>
-            <if test="status != null">#{status},</if>
+            <if test="log.userId != null">#{log.userId},</if>
+            <if test="log.logType != null">#{log.logType},</if>
+            <if test="log.integral != null">#{log.integral},</if>
+            <if test="log.balance != null">#{log.balance},</if>
+            <if test="log.businessId != null">#{log.businessId},</if>
+            <if test="log.createTime != null">#{log.createTime},</if>
+            <if test="log.businessType != null">#{log.businessType},</if>
+            <if test="log.status != null">#{log.status},</if>
          </trim>
     </insert>
 
-    <update id="updateFsUserIntegralLogs" parameterType="FsUserIntegralLogs">
-        update fs_user_integral_logs
+    <update id="updateFsUserIntegralLogs">
+        update fs_user_integral_logs_${shard}
         <trim prefix="SET" suffixOverrides=",">
-            <if test="userId != null">user_id = #{userId},</if>
-            <if test="logType != null">log_type = #{logType},</if>
-            <if test="integral != null">integral = #{integral},</if>
-            <if test="balance != null">balance = #{balance},</if>
-            <if test="businessId != null">business_id = #{businessId},</if>
-            <if test="createTime != null">create_time = #{createTime},</if>
-            <if test="businessType != null">business_type = #{businessType},</if>
-            <if test="status != null">status = #{status},</if>
+            <if test="log.userId != null">user_id = #{log.userId},</if>
+            <if test="log.logType != null">log_type = #{log.logType},</if>
+            <if test="log.integral != null">integral = #{log.integral},</if>
+            <if test="log.balance != null">balance = #{log.balance},</if>
+            <if test="log.businessId != null">business_id = #{log.businessId},</if>
+            <if test="log.createTime != null">create_time = #{log.createTime},</if>
+            <if test="log.businessType != null">business_type = #{log.businessType},</if>
+            <if test="log.status != null">status = #{log.status},</if>
         </trim>
-        where id = #{id}
+        where id = #{log.id}
     </update>
 
-    <delete id="deleteFsUserIntegralLogsById" parameterType="Long">
-        delete from fs_user_integral_logs where id = #{id}
+    <delete id="deleteFsUserIntegralLogsById">
+        delete from fs_user_integral_logs_${shard} where id = #{id}
     </delete>
 
-    <delete id="deleteFsUserIntegralLogsByIds" parameterType="String">
-        delete from fs_user_integral_logs where id in
-        <foreach item="id" collection="array" open="(" separator="," close=")">
+    <delete id="deleteFsUserIntegralLogsByIds">
+        delete from fs_user_integral_logs_${shard} where id in
+        <foreach item="id" collection="ids" open="(" separator="," close=")">
             #{id}
         </foreach>
     </delete>
+
+    <!-- ==================== 总后台跨分表查询(显式 UNION ALL 物理表) ==================== -->
+
+    <!-- 分表公共 WHERE 条件(积分表自身字段) -->
+    <sql id="shardWhereCondition">
+        <if test="param.userId != null"> and user_id = #{param.userId}</if>
+        <if test="param.logType != null and param.logType != ''"> and log_type = #{param.logType}</if>
+        <if test="param.businessId != null and param.businessId != ''"> and business_id = #{param.businessId}</if>
+        <if test="param.sTime != null"> and DATE(create_time) &gt;= DATE(#{param.sTime})</if>
+        <if test="param.eTime != null"> and DATE(create_time) &lt;= DATE(#{param.eTime})</if>
+    </sql>
+
+    <!-- 总后台跨分表分页查询:UNION ALL 所有命中的物理分表,外层关联 fs_user 获取用户信息 -->
+    <select id="selectListAllShards" resultType="com.fs.his.vo.FsUserIntegralLogsListVO">
+        SELECT t.id, t.user_id, t.log_type, t.integral, t.balance, t.business_id, t.create_time,
+               u.nick_name, u.phone
+        FROM (
+            <foreach collection="shards" item="shard" separator="UNION ALL">
+                SELECT id, user_id, log_type, integral, balance, business_id, create_time
+                FROM fs_user_integral_logs_${shard}
+                <where>
+                    <include refid="shardWhereCondition"/>
+                </where>
+            </foreach>
+        ) t
+        LEFT JOIN fs_user u ON u.user_id = t.user_id
+        <where>
+            <if test="param.phone != null and param.phone != ''"> and u.phone = #{param.phone}</if>
+        </where>
+        ORDER BY t.id DESC
+        LIMIT #{offset}, #{pageSize}
+    </select>
+
+    <!-- 总后台跨分表总数查询:COUNT 包裹 UNION ALL,phone 条件用子查询下推到各分表 -->
+    <select id="selectCountAllShards" resultType="long">
+        SELECT COUNT(1) FROM (
+            <foreach collection="shards" item="shard" separator="UNION ALL">
+                SELECT 1 FROM fs_user_integral_logs_${shard}
+                <where>
+                    <include refid="shardWhereCondition"/>
+                    <if test="param.phone != null and param.phone != ''">
+                        and user_id IN (SELECT user_id FROM fs_user WHERE phone = #{param.phone})
+                    </if>
+                </where>
+            </foreach>
+        ) tmp
+    </select>
+
+    <!-- 总后台跨分表全量查询(用于导出,不分页) -->
+    <select id="selectListAllShardsAll" resultType="com.fs.his.vo.FsUserIntegralLogsListVO">
+        SELECT t.id, t.user_id, t.log_type, t.integral, t.balance, t.business_id, t.create_time,
+               u.nick_name, u.phone
+        FROM (
+            <foreach collection="shards" item="shard" separator="UNION ALL">
+                SELECT id, user_id, log_type, integral, balance, business_id, create_time
+                FROM fs_user_integral_logs_${shard}
+                <where>
+                    <include refid="shardWhereCondition"/>
+                </where>
+            </foreach>
+        ) t
+        LEFT JOIN fs_user u ON u.user_id = t.user_id
+        <where>
+            <if test="param.phone != null and param.phone != ''"> and u.phone = #{param.phone}</if>
+        </where>
+        ORDER BY t.id DESC
+    </select>
 </mapper>

+ 5 - 1
fs-service/src/main/resources/mapper/hisStore/FsStoreOrderScrmMapper.xml

@@ -50,6 +50,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="useIntegral"    column="use_integral"    />
         <result property="payIntegral"    column="pay_integral"    />
         <result property="backIntegral"    column="back_integral"    />
+        <result property="integralDesc"   column="integral_desc"   />
         <result property="mark"    column="mark"    />
         <result property="isDel"    column="is_del"    />
         <result property="remark"    column="remark"    />
@@ -100,7 +101,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 
     <sql id="selectFsStoreOrderVo">
-        select id, order_code,service_fee, extend_order_id,pay_order_id,bank_order_id, user_id,order_visit, real_name, user_phone, user_address, cart_id, freight_price, total_num, total_price, total_postage, pay_price, pay_postage,pay_delivery,pay_money, deduction_price, coupon_id, coupon_price, promotion_activity_id, promotion_tier_id, promotion_discount_amount, paid, pay_time, pay_type, create_time, update_time, status, refund_status, refund_reason_wap_img, refund_reason_wap_explain, refund_reason_time, refund_reason_wap, refund_reason, refund_price, delivery_sn, delivery_name, delivery_type, delivery_id, gain_integral, use_integral, pay_integral, back_integral, mark, is_del, remark, cost, verify_code, store_id, shipping_type, is_channel, is_remind, is_sys_del,is_prescribe,prescribe_id ,company_id,company_user_id,is_package,package_json,item_json,order_type,package_id,finish_time,delivery_status,delivery_pay_status,delivery_time,delivery_pay_time,delivery_pay_money,tui_money,tui_money_status,delivery_import_time,tui_user_id,tui_user_money_status,order_create_type,store_house_code,dept_id,is_edit_money,customer_id,is_pay_remain,delivery_send_time,certificates,schedule_id,combination_order_id,batch_number,is_divide,is_settled,is_divided from fs_store_order_scrm
+        select id, order_code,service_fee, extend_order_id,pay_order_id,bank_order_id, user_id,order_visit, real_name, user_phone, user_address, cart_id, freight_price, total_num, total_price, total_postage, pay_price, pay_postage,pay_delivery,pay_money, deduction_price, coupon_id, coupon_price, promotion_activity_id, promotion_tier_id, promotion_discount_amount, paid, pay_time, pay_type, create_time, update_time, status, refund_status, refund_reason_wap_img, refund_reason_wap_explain, refund_reason_time, refund_reason_wap, refund_reason, refund_price, delivery_sn, delivery_name, delivery_type, delivery_id, gain_integral, use_integral, pay_integral, back_integral, integral_desc, mark, is_del, remark, cost, verify_code, store_id, shipping_type, is_channel, is_remind, is_sys_del,is_prescribe,prescribe_id ,company_id,company_user_id,is_package,package_json,item_json,order_type,package_id,finish_time,delivery_status,delivery_pay_status,delivery_time,delivery_pay_time,delivery_pay_money,tui_money,tui_money_status,delivery_import_time,tui_user_id,tui_user_money_status,order_create_type,store_house_code,dept_id,is_edit_money,customer_id,is_pay_remain,delivery_send_time,certificates,schedule_id,combination_order_id,batch_number,is_divide,is_settled,is_divided from fs_store_order_scrm
     </sql>
 
     <select id="selectFsStoreOrderList" parameterType="FsStoreOrderScrm" resultMap="FsStoreOrderResult">
@@ -216,6 +217,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="useIntegral != null">use_integral,</if>
             <if test="payIntegral != null">pay_integral,</if>
             <if test="backIntegral != null">back_integral,</if>
+            <if test="integralDesc != null and integralDesc != ''">integral_desc,</if>
             <if test="mark != null and mark != ''">mark,</if>
             <if test="isDel != null">is_del,</if>
             <if test="remark != null">remark,</if>
@@ -310,6 +312,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="useIntegral != null">#{useIntegral},</if>
             <if test="payIntegral != null">#{payIntegral},</if>
             <if test="backIntegral != null">#{backIntegral},</if>
+            <if test="integralDesc != null and integralDesc != ''">#{integralDesc},</if>
             <if test="mark != null and mark != ''">#{mark},</if>
             <if test="isDel != null">#{isDel},</if>
             <if test="remark != null">#{remark},</if>
@@ -404,6 +407,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="useIntegral != null">use_integral = #{useIntegral},</if>
             <if test="payIntegral != null">pay_integral = #{payIntegral},</if>
             <if test="backIntegral != null">back_integral = #{backIntegral},</if>
+            <if test="integralDesc != null and integralDesc != ''">integral_desc = #{integralDesc},</if>
             <if test="mark != null and mark != ''">mark = #{mark},</if>
             <if test="isDel != null">is_del = #{isDel},</if>
             <if test="remark != null">remark = #{remark},</if>

+ 101 - 0
fs-service/src/main/resources/mapper/hisStore/FsStoreProductScrmMapper.xml

@@ -542,6 +542,107 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </trim>
         where product_id = #{productId}
     </update>
+    <update id="updateFsStoreProductNullable" parameterType="FsStoreProductScrm">
+        update fs_store_product_scrm
+        SET
+            image = #{image},
+            video = #{video},
+            slider_image = #{sliderImage},
+            product_name = #{productName},
+            product_info = #{productInfo},
+            keyword = #{keyword},
+            bar_code = #{barCode},
+            cate_id = #{cateId},
+            price = #{price},
+            vip_price = #{vipPrice},
+            ot_price = #{otPrice},
+            agent_price = #{agentPrice},
+            postage = #{postage},
+            unit_name = #{unitName},
+            sort = #{sort},
+            sales = #{sales},
+            stock = #{stock},
+            is_show = #{isShow},
+            is_hot = #{isHot},
+            is_benefit = #{isBenefit},
+            is_best = #{isBest},
+            is_new = #{isNew},
+            description = #{description},
+            create_time = #{createTime},
+            update_time = #{updateTime},
+            is_postage = #{isPostage},
+            is_del = #{isDel},
+            give_integral = #{giveIntegral},
+            cost = #{cost},
+            is_good = #{isGood},
+            browse = #{browse},
+            code_path = #{codePath},
+            temp_id = #{tempId},
+            spec_type = #{specType},
+            is_integral = #{isIntegral},
+            integral = #{integral},
+            product_type = #{productType},
+            prescribe_code = #{prescribeCode},
+            prescribe_spec = #{prescribeSpec},
+            prescribe_factory = #{prescribeFactory},
+            prescribe_name = #{prescribeName},
+            is_display = #{isDisplay},
+            tui_cate_id = #{tuiCateId},
+            company_ids = #{companyIds},
+            is_drug = #{isDrug},
+            drug_image = #{drugImage},
+            drug_reg_cert_no = #{drugRegCertNo},
+            common_name = #{commonName},
+            dosage_form = #{dosageForm},
+            unit_price = #{unitPrice},
+            batch_number = #{batchNumber},
+            mah = #{mah},
+            mah_address = #{mahAddress},
+            manufacturer = #{manufacturer},
+            manufacturer_address = #{manufacturerAddress},
+            indications = #{indications},
+            ingredient = #{ingredient},
+            dosage = #{dosage},
+            adverse_reactions = #{adverseReactions},
+            contraindications = #{contraindications},
+            precautions = #{precautions},
+            is_audit = #{isAudit},
+            store_id = #{storeId},
+            instruction_manual = #{instructionManual},
+            review_audit = #{reviewAudit},
+            qualification_certificate = #{qualificationCertificate},
+            qualification_certificate_start = #{qualificationCertificateStart},
+            qualification_certificate_end = #{qualificationCertificateEnd},
+            business = #{business},
+            business_start = #{businessStart},
+            business_end = #{businessEnd},
+            license = #{license},
+            license_start = #{licenseStart},
+            license_end = #{licenseEnd},
+            certificate = #{certificate},
+            certificate_start = #{certificateStart},
+            certificate_end = #{certificateEnd},
+            voucher = #{voucher},
+            voucher_start = #{voucherStart},
+            voucher_end = #{voucherEnd},
+            gmp_auth = #{gmpAuth},
+            gmp_auth_start = #{gmpAuthStart},
+            gmp_auth_end = #{gmpAuthEnd},
+            business_link = #{businessLink},
+            medical_device_code = #{medicalDeviceCode},
+            is_business_permanent = #{isBusinessPermanent},
+            is_license_permanent = #{isLicensePermanent},
+            is_certificate_permanent = #{isCertificatePermanent},
+            is_gmp_auth_permanent = #{isGmpAuthPermanent},
+            medical_reg_cert_no = #{medicalRegCertNo},
+            registrant_info = #{registrantInfo},
+            prod_license_no = #{prodLicenseNo},
+            prod_tech_req_no = #{prodTechReqNo},
+            product_structure = #{productStructure},
+            storage_conditions = #{storageConditions},
+            specification = #{specification}
+        where product_id = #{productId}
+    </update>
     <update id="batchModify">
         <foreach collection="productId" separator=";" item="item">
             update fs_store_product_scrm set

+ 17 - 0
fs-service/src/main/resources/mapper/hisStore/FsStorePromotionActivityMapper.xml

@@ -162,4 +162,21 @@
           AND a.end_time &gt;= NOW()
         ORDER BY a.id DESC
     </select>
+
+    <update id="enableActivityIfNoConflict">
+        UPDATE fs_store_promotion_activity
+        SET status = 1, manual_status = 1, update_time = NOW()
+        WHERE id = #{id} AND is_del = 0
+          AND NOT EXISTS (
+            SELECT 1 FROM (
+              SELECT 1 FROM fs_store_promotion_activity
+              WHERE is_del = 0
+                AND store_id = #{storeId}
+                AND id != #{id}
+                AND manual_status = 1
+                AND start_time &lt;= #{endTime}
+                AND end_time &gt;= #{startTime}
+            ) tmp
+          )
+    </update>
 </mapper>

+ 12 - 1
fs-service/src/main/resources/mapper/hisStore/FsStorePromotionUsageMapper.xml

@@ -7,7 +7,7 @@
         FROM fs_store_promotion_usage
         WHERE activity_id = #{activityId}
           AND user_id = #{userId}
-          AND usage_status = 1
+          AND usage_status IN (0, 1)
     </select>
 
     <select id="countEffectiveByActivityIdsAndUser"
@@ -23,6 +23,17 @@
         GROUP BY activity_id
     </select>
 
+    <select id="selectByOrderIdAndActivityId"
+            resultType="com.fs.hisStore.domain.FsStorePromotionUsage">
+        SELECT id, activity_id AS activityId, user_id AS userId, order_id AS orderId,
+               order_amount AS orderAmount, discount_amount AS discountAmount, tier_id AS tierId,
+               usage_status AS usageStatus, usage_time AS usageTime, create_time AS createTime
+        FROM fs_store_promotion_usage
+        WHERE order_id = #{orderId}
+          AND activity_id = #{activityId}
+        LIMIT 1
+    </select>
+
     <insert id="insertFsStorePromotionUsage" useGeneratedKeys="true" keyProperty="id">
         INSERT INTO fs_store_promotion_usage
         (activity_id, user_id, order_id, order_amount, discount_amount, tier_id, usage_status, usage_time, create_time)

+ 48 - 46
fs-service/src/main/resources/mapper/hisStore/FsUserIntegralLogsScrmMapper.xml

@@ -16,76 +16,78 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="status"    column="status"    />
     </resultMap>
 
+    <!-- ==================== 手动分表:表名通过 ${shard} 动态拼接(0~9) ==================== -->
+
     <sql id="selectFsUserIntegralLogsVo">
-        select id, user_id,status, log_type,business_type, integral, balance, business_id, create_time from fs_user_integral_logs
+        select id, user_id,status, log_type,business_type, integral, balance, business_id, create_time from fs_user_integral_logs_${shard}
     </sql>
 
-    <select id="selectFsUserIntegralLogsList" parameterType="FsUserIntegralLogsScrm" resultMap="FsUserIntegralLogsResult">
+    <select id="selectFsUserIntegralLogsList" resultMap="FsUserIntegralLogsResult">
         <include refid="selectFsUserIntegralLogsVo"/>
         <where>
-            <if test="userId != null "> and user_id = #{userId}</if>
-            <if test="logType != null  and logType != ''"> and log_type = #{logType}</if>
-            <if test="integral != null "> and integral = #{integral}</if>
-            <if test="balance != null "> and balance = #{balance}</if>
-            <if test="businessId != null  and businessId != ''"> and business_id = #{businessId}</if>
-            <if test="createTime != null "> and create_time = #{createTime}</if>
-            <if test="businessType != null "> and business_type = #{businessType}</if>
+            <if test="log.userId != null "> and user_id = #{log.userId}</if>
+            <if test="log.logType != null  and log.logType != ''"> and log_type = #{log.logType}</if>
+            <if test="log.integral != null "> and integral = #{log.integral}</if>
+            <if test="log.balance != null "> and balance = #{log.balance}</if>
+            <if test="log.businessId != null  and log.businessId != ''"> and business_id = #{log.businessId}</if>
+            <if test="log.createTime != null "> and create_time = #{log.createTime}</if>
+            <if test="log.businessType != null "> and business_type = #{log.businessType}</if>
         </where>
     </select>
 
-    <select id="selectFsUserIntegralLogsById" parameterType="Long" resultMap="FsUserIntegralLogsResult">
+    <select id="selectFsUserIntegralLogsById" resultMap="FsUserIntegralLogsResult">
         <include refid="selectFsUserIntegralLogsVo"/>
         where id = #{id}
     </select>
 
-    <insert id="insertFsUserIntegralLogs" parameterType="FsUserIntegralLogsScrm">
-        insert into fs_user_integral_logs
+    <insert id="insertFsUserIntegralLogs" useGeneratedKeys="true" keyProperty="log.id">
+        insert into fs_user_integral_logs_${shard}
         <trim prefix="(" suffix=")" suffixOverrides=",">
-            <if test="id != null">id,</if>
-            <if test="userId != null">user_id,</if>
-            <if test="logType != null">log_type,</if>
-            <if test="integral != null">integral,</if>
-            <if test="balance != null">balance,</if>
-            <if test="businessId != null">business_id,</if>
-            <if test="createTime != null">create_time,</if>
-            <if test="businessType != null">business_type,</if>
-            <if test="status != null">status,</if>
+            <if test="log.id != null">id,</if>
+            <if test="log.userId != null">user_id,</if>
+            <if test="log.logType != null">log_type,</if>
+            <if test="log.integral != null">integral,</if>
+            <if test="log.balance != null">balance,</if>
+            <if test="log.businessId != null">business_id,</if>
+            <if test="log.createTime != null">create_time,</if>
+            <if test="log.businessType != null">business_type,</if>
+            <if test="log.status != null">status,</if>
         </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
-            <if test="id != null">#{id},</if>
-            <if test="userId != null">#{userId},</if>
-            <if test="logType != null">#{logType},</if>
-            <if test="integral != null">#{integral},</if>
-            <if test="balance != null">#{balance},</if>
-            <if test="businessId != null">#{businessId},</if>
-            <if test="createTime != null">#{createTime},</if>
-            <if test="businessType != null">#{businessType},</if>
-            <if test="status != null">#{status},</if>
+            <if test="log.id != null">#{log.id},</if>
+            <if test="log.userId != null">#{log.userId},</if>
+            <if test="log.logType != null">#{log.logType},</if>
+            <if test="log.integral != null">#{log.integral},</if>
+            <if test="log.balance != null">#{log.balance},</if>
+            <if test="log.businessId != null">#{log.businessId},</if>
+            <if test="log.createTime != null">#{log.createTime},</if>
+            <if test="log.businessType != null">#{log.businessType},</if>
+            <if test="log.status != null">#{log.status},</if>
          </trim>
     </insert>
 
-    <update id="updateFsUserIntegralLogs" parameterType="FsUserIntegralLogsScrm">
-        update fs_user_integral_logs
+    <update id="updateFsUserIntegralLogs">
+        update fs_user_integral_logs_${shard}
         <trim prefix="SET" suffixOverrides=",">
-            <if test="userId != null">user_id = #{userId},</if>
-            <if test="logType != null">log_type = #{logType},</if>
-            <if test="integral != null">integral = #{integral},</if>
-            <if test="balance != null">balance = #{balance},</if>
-            <if test="businessId != null">business_id = #{businessId},</if>
-            <if test="createTime != null">create_time = #{createTime},</if>
-            <if test="businessType != null">business_type = #{businessType},</if>
-            <if test="status != null">status = #{status},</if>
+            <if test="log.userId != null">user_id = #{log.userId},</if>
+            <if test="log.logType != null">log_type = #{log.logType},</if>
+            <if test="log.integral != null">integral = #{log.integral},</if>
+            <if test="log.balance != null">balance = #{log.balance},</if>
+            <if test="log.businessId != null">business_id = #{log.businessId},</if>
+            <if test="log.createTime != null">create_time = #{log.createTime},</if>
+            <if test="log.businessType != null">business_type = #{log.businessType},</if>
+            <if test="log.status != null">status = #{log.status},</if>
         </trim>
-        where id = #{id}
+        where id = #{log.id}
     </update>
 
-    <delete id="deleteFsUserIntegralLogsById" parameterType="Long">
-        delete from fs_user_integral_logs where id = #{id}
+    <delete id="deleteFsUserIntegralLogsById">
+        delete from fs_user_integral_logs_${shard} where id = #{id}
     </delete>
 
-    <delete id="deleteFsUserIntegralLogsByIds" parameterType="String">
-        delete from fs_user_integral_logs where id in
-        <foreach item="id" collection="array" open="(" separator="," close=")">
+    <delete id="deleteFsUserIntegralLogsByIds">
+        delete from fs_user_integral_logs_${shard} where id in
+        <foreach item="id" collection="ids" open="(" separator="," close=")">
             #{id}
         </foreach>
     </delete>

+ 3 - 2
fs-store/src/main/java/com/fs/framework/config/DruidConfig.java

@@ -4,10 +4,11 @@ import com.alibaba.druid.pool.DruidDataSource;
 import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
 import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
 import com.alibaba.druid.util.Utils;
+import com.fs.framework.config.properties.DruidProperties;
 import com.fs.framework.datasource.DynamicDataSource;
 import com.fs.common.enums.DataSourceType;
 import com.fs.common.utils.spring.SpringUtils;
-import com.fs.framework.config.properties.DruidProperties;
+import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.boot.web.servlet.FilterRegistrationBean;
@@ -48,7 +49,7 @@ public class DruidConfig
 
     @Bean(name = "dynamicDataSource")
     @Primary
-    public DynamicDataSource dataSource(DataSource masterDataSource)
+    public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource)
     {
         Map<Object, Object> targetDataSources = new HashMap<>();
         targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);

+ 13 - 1
fs-store/src/main/java/com/fs/hisStore/controller/store/FsStoreAfterSalesScrmController.java

@@ -153,12 +153,24 @@ public class FsStoreAfterSalesScrmController extends BaseController
 
     /**
      * 获取售后记录详细信息
+     * 参考总后台 FsStoreAfterSalesScrmController.getInfo:
+     * 若 afterSales.orderId 为 null,通过 orderCode 反查 FsStoreOrderScrm 获取订单 ID,
+     * 避免前端因订单 ID 丢失而无法加载积分明细
      */
 
     @GetMapping(value = "/{id}")
     public AjaxResult getInfo(@PathVariable("id") Long id)
     {
-        return AjaxResult.success(fsStoreAfterSalesService.selectFsStoreAfterSalesVOById(id));
+        FsStoreAfterSalesVO afterSales = fsStoreAfterSalesService.selectFsStoreAfterSalesVOById(id);
+        if (afterSales != null && afterSales.getOrderId() == null
+                && StrUtil.isNotBlank(afterSales.getOrderCode())) {
+            // orderId 为 null 时,参考总后台通过 orderCode 反查订单补充 orderId
+            FsStoreOrderScrm order = fsStoreOrderService.selectFsStoreOrderByOrderCode(afterSales.getOrderCode());
+            if (order != null && order.getId() != null) {
+                afterSales.setOrderId(order.getId());
+            }
+        }
+        return AjaxResult.success(afterSales);
     }
 
     /**

+ 49 - 0
fs-store/src/main/java/com/fs/store/controller/store/FsUserIntegralLogsController.java

@@ -0,0 +1,49 @@
+package com.fs.store.controller.store;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.his.domain.FsUserIntegralLogs;
+import com.fs.his.service.IFsUserIntegralLogsService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 店铺端积分明细Controller
+ *
+ * <p>提供按订单 ID 查询积分明细的接口,供店铺端订单详情页和售后详情页展示积分变动记录。
+ * 复用 fs-service 的 {@link IFsUserIntegralLogsService#selectByBusinessId(String)},
+ * 内部会广播到 fs_user_integral_logs 全部分表(_0~_9)合并结果。</p>
+ *
+ * @author fs
+ */
+@RestController
+@RequestMapping("/store/store/userIntegralLogs")
+public class FsUserIntegralLogsController extends BaseController
+{
+    @Autowired
+    private IFsUserIntegralLogsService fsUserIntegralLogsService;
+
+    /**
+     * 按订单 ID 查询积分明细列表
+     * <p>用于订单详情页/售后详情页展示该订单相关的全部积分变动(发放、抵扣、退回等)。
+     * 由于不含 user_id 分片键,内部会广播到全部分表合并结果,适用于低频详情查询。</p>
+     *
+     * @param orderId 订单 ID
+     * @return 积分明细列表,按 id 升序
+     */
+    @GetMapping("/getByOrderId/{orderId}")
+    public AjaxResult getByOrderId(@PathVariable("orderId") Long orderId)
+    {
+        if (orderId == null || orderId <= 0)
+        {
+            return AjaxResult.error("订单ID不能为空");
+        }
+        List<FsUserIntegralLogs> list = fsUserIntegralLogsService.selectByBusinessId(orderId.toString());
+        return AjaxResult.success(list);
+    }
+}

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

@@ -114,7 +114,7 @@ public class UserAddressController extends  AppBaseController {
 //        integralTemplateParam.setBusinessId(userAddress.getAddressId().toString());
 //        fsUserIntegralLogsService.addIntegralTemplate(integralTemplateParam);
 
-        fsUserIntegralLogsService.addUserAddressIntegral(Long.parseLong(getUserId()),userAddress.getAddressId());
+//        fsUserIntegralLogsService.addUserAddressIntegral(Long.parseLong(getUserId()),userAddress.getAddressId());
         return R.ok("操作成功");
     }
 

+ 32 - 22
fs-user-app/src/main/java/com/fs/app/controller/YbPayController.java

@@ -1,8 +1,6 @@
 package com.fs.app.controller;
 
 import com.fs.common.core.controller.BaseController;
-import com.fs.common.exception.CustomException;
-import com.fs.common.utils.spring.SpringUtils;
 import com.fs.course.service.IFsUserCourseOrderService;
 import com.fs.course.service.IFsUserVipOrderService;
 import com.fs.his.domain.FsPayConfig;
@@ -11,9 +9,7 @@ import com.fs.his.service.IFsIntegralOrderService;
 import com.fs.his.service.IFsPackageOrderService;
 import com.fs.his.service.IFsStoreOrderService;
 
-import com.fs.system.domain.SysConfig;
-import com.fs.system.mapper.SysConfigMapper;
-import com.fs.ybPay.domain.OrderCallback;
+import com.fs.system.service.ISysConfigService;
 import com.fs.ybPay.util.PayUtil;
 import com.google.gson.Gson;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -46,41 +42,55 @@ public class YbPayController extends BaseController
     private IFsUserVipOrderService userVipOrderService;
     @Autowired
     private IFsIntegralOrderService integralOrderService;
+    @Autowired
+    private ISysConfigService configService;
 
     @PostMapping("/payNotify")
-    public String payNotify(@RequestBody Map<String, String> params, @RequestBody OrderCallback o) {
-        System.out.println("支付回调");
+    public String payNotify(@RequestBody Map<String, String> params) {
+        logger.info("易宝支付回调:{}", params);
         //验证
-        SysConfigMapper sysConfigMapper= SpringUtils.getBean(SysConfigMapper.class);
-        SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey("his.pay");
-        FsPayConfig fsPayConfig = new Gson().fromJson(sysConfig.getConfigValue(), FsPayConfig.class);
-        String sign = PayUtil.sign(params,fsPayConfig.getYbKey());
+        String configJson = configService.selectConfigByKey("his.pay");
+        FsPayConfig fsPayConfig = new Gson().fromJson(configJson, FsPayConfig.class);
+        String sign = PayUtil.sign(params, fsPayConfig.getYbKey());
         String sg = params.get("sign");
-        if (!sg.equals(sign)){
-            throw new CustomException("验签错误");
+        if (sg == null) {
+            logger.warn("易宝支付回调sign为空");
+            return "FAIL";
+        }
+        if (!sg.equals(sign)) {
+            logger.warn("易宝支付回调验签失败");
+            return "FAIL";
         }
-        String[] orderId=o.getLowOrderId().split("-");
-        switch (orderId[0]){
+        String lowOrderId = params.get("lowOrderId");
+        String upOrderId = params.get("upOrderId");
+        String[] orderId = lowOrderId.split("-");
+        if (orderId.length < 2) {
+            logger.warn("易宝支付回调lowOrderId格式不正确:{}", lowOrderId);
+            return "FAIL";
+        }
+        switch (orderId[0]) {
             case "inquiry":
-                inquiryOrderService.payConfirm("",orderId[1],o.getUpOrderId(),"",1,null,null);
+                inquiryOrderService.payConfirm("", orderId[1], upOrderId, "", 1, null, null);
                 break;
             case "store":
-                storeOrderService.payConfirm("",orderId[1],o.getUpOrderId(),"",1,null,null);
+                storeOrderService.payConfirm("", orderId[1], upOrderId, "", 1, null, null);
                 break;
             case "package":
-                packageOrderService.payConfirm("",orderId[1],o.getUpOrderId(),"",1,null,null);
+                packageOrderService.payConfirm("", orderId[1], upOrderId, "", 1, null, null);
                 break;
             case "course":
-                courseOrderService.payConfirm("",orderId[1],o.getUpOrderId(),"",1,null,null);
+                courseOrderService.payConfirm("", orderId[1], upOrderId, "", 1, null, null);
                 break;
             case "appvip":
-                userVipOrderService.payConfirm("",orderId[1],o.getUpOrderId(),"",1,null,null);
+                userVipOrderService.payConfirm("", orderId[1], upOrderId, "", 1, null, null);
                 break;
             case "integral":
-                integralOrderService.payConfirm("",orderId[1],o.getUpOrderId(),"",1,null,null);
+                integralOrderService.payConfirm("", orderId[1], upOrderId, "", 1, null, null);
+                break;
+            default:
+                logger.warn("易宝支付回调未知的业务类型:{}", orderId[0]);
                 break;
         }
-        System.out.println(o);
         return "SUCCESS";
     }
 

+ 3 - 2
fs-user-app/src/main/java/com/fs/app/controller/store/CourseScrmController.java

@@ -31,6 +31,7 @@ import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.cache.annotation.Cacheable;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 import springfox.documentation.spring.web.readers.operation.CachingOperationNameGenerator;
 
@@ -331,7 +332,7 @@ public class CourseScrmController extends AppBaseController {
     @Login
     @ApiOperation("获取积分")
     @PostMapping("/getIntegral")
-    public R getIntegral(@RequestBody FsUserCourseGetIntegralParam param, HttpServletRequest request){
+    public R getIntegral(@Validated @RequestBody FsUserCourseGetIntegralParam param, HttpServletRequest request){
         param.setUserId(Long.parseLong(getUserId()));
         return courseService.getIntegralByCourseVideo(param);
     }
@@ -460,7 +461,7 @@ public class CourseScrmController extends AppBaseController {
     @Login
     @ApiOperation("每十分钟获取积分")
     @PostMapping("/getIntegralByH5Video")
-    public R getIntegralByH5Video(@RequestBody FsUserCourseVideoFinishUParam param)
+    public R getIntegralByH5Video(@Validated @RequestBody FsUserCourseVideoFinishUParam param)
     {
         param.setUserId(Long.parseLong(getUserId()));
         return courseVideoService.getIntegralByH5Video(param);

+ 10 - 52
fs-user-app/src/main/java/com/fs/app/controller/store/HuifuPayScrmController.java

@@ -1,71 +1,29 @@
 package com.fs.app.controller.store;
 
 import com.alibaba.fastjson.JSON;
-import com.fs.course.service.IFsCourseProductOrderService;
-import com.fs.course.service.IFsUserCourseOrderService;
-import com.fs.course.service.IFsUserVipOrderService;
-import com.fs.his.service.IFsInquiryOrderService;
-import com.fs.his.service.IFsIntegralOrderService;
-import com.fs.his.service.IFsPackageOrderService;
-import com.fs.his.service.IFsStoreOrderService;
-import com.fs.hisStore.service.IFsStoreOrderScrmService;
 import com.fs.huifuPay.domain.HuiFuOnlinePay;
 import com.fs.huifuPay.domain.HuiFuOnlineRefund;
 import com.fs.huifuPay.domain.HuiFuResult;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 @RestController
 @RequestMapping(value="/store/app/hfPay")
+@Slf4j
 public class HuifuPayScrmController {
-    @Autowired
-    private IFsInquiryOrderService inquiryOrderService;
-    @Autowired
-    private IFsStoreOrderScrmService storeOrderService;
-    @Autowired
-    private IFsPackageOrderService packageOrderService;
-    @Autowired
-    private IFsUserCourseOrderService courseOrderService;
-    @Autowired
-    private IFsUserVipOrderService vipOrderService;
-    @Autowired
-    private IFsIntegralOrderService integralOrderService;
-    @Autowired
-    private IFsCourseProductOrderService courseProductOrderService;
 
-    org.slf4j.Logger logger= LoggerFactory.getLogger(getClass());
     @RequestMapping("/payNotifyUrl")
     public String asyncMessage(@RequestParam String resp_desc,@RequestParam String resp_code,@RequestParam String sign,@RequestParam String resp_data) {
-        HuiFuResult huiFuResult = JSON.parseObject(resp_data, HuiFuResult.class);
-        logger.info("汇付支付回调:"+huiFuResult);
-        if(huiFuResult.getResp_code().equals("00000000") && huiFuResult.getNotify_type().equals("1")){
-            String[] orderId=huiFuResult.getReq_seq_id().split("-");
-//            switch (orderId[0]){
-//                case "inquiry":
-//                    inquiryOrderService.payConfirm("",orderId[1],huiFuResult.getHf_seq_id(),"",1);
-//                    break;
-//                case "store":
-//                    storeOrderService.payConfirm(1,null,orderId[1],huiFuResult.getHf_seq_id(),null,null);
-//                    break;
-//                case "package":
-//                    packageOrderService.payConfirm("",orderId[1],huiFuResult.getHf_seq_id(),"",1);
-//                    break;
-//                case "course":
-//                    courseOrderService.payConfirm("",orderId[1],huiFuResult.getHf_seq_id(),"",1);
-//                    break;
-//                case "appvip":
-//                    vipOrderService.payConfirm("",orderId[1],huiFuResult.getHf_seq_id(),"",1);
-//                    break;
-//                case "integral":
-//                    integralOrderService.payConfirm("",orderId[1],huiFuResult.getHf_seq_id(),"",1);
-//                    break;
-//                case "product":
-//                    courseProductOrderService.payConfirm("",orderId[1],huiFuResult.getHf_seq_id(),"",1);
-//                    break;
-//            }
+        try {
+            HuiFuResult huiFuResult = JSON.parseObject(resp_data, HuiFuResult.class);
+            log.info("汇付支付回调:"+huiFuResult);
+            if(huiFuResult.getResp_code().equals("00000000") && huiFuResult.getNotify_type().equals("1")){
+                String[] orderId=huiFuResult.getReq_seq_id().split("-");
+            }
+        } catch (Exception e) {
+            log.error("汇付支付回调处理异常, resp_data={}", resp_data, e);
         }
 
         return "ok";

+ 3 - 29
fs-user-app/src/main/java/com/fs/app/controller/store/IntegralScrmController.java

@@ -7,7 +7,6 @@ import com.fs.common.annotation.RepeatSubmit;
 import com.fs.common.core.domain.R;
 import com.fs.hisStore.domain.FsIntegralGoodsScrm;
 import com.fs.hisStore.domain.FsIntegralOrderScrm;
-import com.fs.hisStore.domain.FsUserScrm;
 import com.fs.hisStore.param.FsIntegralGoodsListUParam;
 import com.fs.hisStore.param.FsIntegralOrderCreateParam;
 import com.fs.hisStore.param.FsIntegralOrderListUParam;
@@ -16,7 +15,6 @@ import com.fs.hisStore.service.*;
 import com.fs.hisStore.vo.FsIntegralGoodsListUVO;
 import com.fs.hisStore.vo.FsIntegralOrderListUVO;
 import com.fs.hisStore.vo.FsUserIntegralLogsListUVO;
-import com.fs.system.service.ISysConfigService;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 import io.swagger.annotations.Api;
@@ -40,13 +38,7 @@ public class IntegralScrmController extends AppBaseController {
     private IFsUserIntegralLogsScrmService userIntegralLogsService;
     @Autowired
     private IFsIntegralOrderScrmService integralOrderService;
-    @Autowired
-    private IFsUserScrmService userService;
-    @Autowired
-    private IFsUserSignScrmService userSignService;
 
-    @Autowired
-    private ISysConfigService configService;
     @ApiOperation("获取积分商品列表")
     @GetMapping("/getIntegralGoodsList")
     public R getIntegralGoodsList(FsIntegralGoodsListUParam param)
@@ -91,6 +83,9 @@ public class IntegralScrmController extends AppBaseController {
     @GetMapping("/getIntegralOrderById")
     public R getIntegralOrderById(@RequestParam("orderId")Long orderId, HttpServletRequest request){
         FsIntegralOrderScrm order=integralOrderService.selectFsIntegralOrderByOrderId(orderId);
+        if (order == null || !order.getUserId().equals(Long.parseLong(getUserId()))) {
+            return R.error("无权访问该订单");
+        }
         return R.ok().put("data",order);
     }
 
@@ -103,25 +98,4 @@ public class IntegralScrmController extends AppBaseController {
         param.setUserId(Long.parseLong(getUserId()));
         return integralOrderService.createOrder(param);
     }
-
-    @Login
-    @ApiOperation("获取用户签到记录")
-    @GetMapping("/getUserSign")
-    public R getUserSign(HttpServletRequest request){
-        FsUserScrm user=userService.selectFsUserByUserId(Long.parseLong(getUserId()));
-        //获取签到配置
-        String json=configService.selectConfigByKey("store.sign");
-        //判断用户昨天是否签到过
-        Long signNum=userSignService.getSign(user);
-        Boolean isDaySign=userSignService.isDaySign(user);
-        return R.ok().put("isDaySign", isDaySign).put("signNum", signNum).put("integral",user.getIntegral().intValue()).put("sign", json);
-    }
-    @Login
-    @ApiOperation("签到")
-    @PostMapping("/sign")
-    public R sign(HttpServletRequest request){
-        FsUserScrm user=userService.selectFsUserByUserId(Long.parseLong(getUserId()));
-        Long integral=userSignService.sign(user);
-        return R.ok("签到获得" + integral + "积分");
-    }
 }

+ 115 - 46
fs-user-app/src/main/java/com/fs/app/controller/store/PayScrmController.java

@@ -19,10 +19,9 @@ import com.fs.ybPay.dto.YopDivideApplyResponseDTO;
 import com.fs.ybPay.service.IYopPayService;
 import com.fs.huifuPay.domain.HuiFuResult;
 import com.fs.huifuPay.sdk.opps.core.utils.HuiFuUtils;
-import com.fs.ybPay.domain.OrderCallback;
 import com.fs.ybPay.domain.OrderResult;
 import com.fs.pay.pay.dto.OrderQueryDTO;
-import com.fs.pay.pay.util.PayUtil;
+import com.fs.ybPay.util.PayUtil;
 import com.fs.pay.service.IPayService;
 import com.fs.system.service.ISysConfigService;
 import com.google.gson.Gson;
@@ -51,6 +50,8 @@ public class PayScrmController {
 
     protected final Logger logger = LoggerFactory.getLogger(BaseController.class);
 
+    private static final int PAY_SOURCE_ONLINE = 1;
+
     @Autowired
     private RedisCache redisCache;
     @Autowired
@@ -106,28 +107,48 @@ public class PayScrmController {
         logger.info("汇付支付回调:" + o);
         if (o.getResp_code().equals("00000000") && o.getNotify_type().equals("1")) {
             String[] order = o.getReq_seq_id().split("-");
-            switch (order[0]) {
-                case "store":
-                    try {
-                        HuiFuUtils.updateDivItem(order[1]);
-                    } catch (Exception e) {
-                        logger.error("-------分账明细回调错误{}", e.getMessage());
-                    }
-                    return orderService.payConfirm(1, null, order[1], o.getHf_seq_id(), o.getOut_trans_id(), o.getParty_order_id());
-                case "store_remain":
-                    try {
-                        HuiFuUtils.updateDivItem(order[1]);
-                    } catch (Exception e) {
-                        logger.error("-------分账明细回调错误{}", e.getMessage());
-                    }
-                    return orderService.payRemainConfirm(order[1], o.getHf_seq_id(), o.getOut_trans_id(), o.getParty_order_id());
-                case "payment":
-                    try {
-                        HuiFuUtils.updateDivItem(order[1]);
-                    } catch (Exception e) {
-                        logger.error("-------分账明细回调错误{}", e.getMessage());
-                    }
-                    return storePaymentService.payConfirm(order[1], o.getHf_seq_id(), o.getOut_trans_id(), o.getParty_order_id());
+            //加分布式锁,防止并发重复回调
+            RLock lock = redissonClient.getLock("pay:notify:lock:" + order[1]);
+            try {
+                boolean locked = lock.tryLock(200, 30000, TimeUnit.MILLISECONDS);
+                if (!locked) {
+                    logger.warn("汇付支付回调获取锁失败,orderNo={}", order[1]);
+                    return "ok";
+                }
+                switch (order[0]) {
+                    case "store":
+                        try {
+                            HuiFuUtils.updateDivItem(order[1]);
+                        } catch (Exception e) {
+                            logger.error("-------分账明细回调错误{}", e.getMessage());
+                        }
+                        return orderService.payConfirm(PAY_SOURCE_ONLINE, null, order[1], o.getHf_seq_id(), o.getOut_trans_id(), o.getParty_order_id());
+                    case "store_remain":
+                        try {
+                            HuiFuUtils.updateDivItem(order[1]);
+                        } catch (Exception e) {
+                            logger.error("-------分账明细回调错误{}", e.getMessage());
+                        }
+                        return orderService.payRemainConfirm(order[1], o.getHf_seq_id(), o.getOut_trans_id(), o.getParty_order_id());
+                    case "payment":
+                        try {
+                            HuiFuUtils.updateDivItem(order[1]);
+                        } catch (Exception e) {
+                            logger.error("-------分账明细回调错误{}", e.getMessage());
+                        }
+                        return storePaymentService.payConfirm(order[1], o.getHf_seq_id(), o.getOut_trans_id(), o.getParty_order_id());
+                    default:
+                        logger.warn("未知的支付业务类型 bizType: {}", order[0]);
+                        break;
+                }
+            } catch (InterruptedException e) {
+                logger.error("汇付支付回调获取锁被中断,orderNo={}", order[1], e);
+                Thread.currentThread().interrupt();
+                return "ok";
+            } finally {
+                if (lock.isHeldByCurrentThread()) {
+                    lock.unlock();
+                }
             }
 
         }
@@ -136,24 +157,57 @@ public class PayScrmController {
 
     @ApiOperation("易宝第三方支付回调")
     @PostMapping(value = "/ybPayNotify")
-    public String ybPayNotify(@RequestBody Map<String, String> params, @RequestBody OrderCallback o) throws Exception {
+    public String ybPayNotify(@RequestBody Map<String, String> params) throws Exception {
+        //从DB配置读取易宝密钥
+        String configJson = configService.selectConfigByKey("his.pay");
+        FsPayConfig fsPayConfig = new Gson().fromJson(configJson, FsPayConfig.class);
         //验证
-        String sign = PayUtil.sign(params);
+        String sign = PayUtil.sign(params, fsPayConfig.getYbKey());
         String sg = params.get("sign");
+        if (sg == null) {
+            logger.warn("易宝支付回调sign为空");
+            return "FAIL";
+        }
         if (!sg.equals(sign)) {
-            return "fail";
+            return "FAIL";
         }
+        String lowOrderId = params.get("lowOrderId");
+        String upOrderId = params.get("upOrderId");
         OrderQueryDTO orderQueryDTO = new OrderQueryDTO();
-        orderQueryDTO.setLowOrderId(o.getLowOrderId());
+        orderQueryDTO.setLowOrderId(lowOrderId);
         OrderResult orderResult = ybPayService.getOrder(orderQueryDTO);
-        String[] order = o.getLowOrderId().split("-");
-        switch (order[0]) {
-            case "store":
-                return orderService.payConfirm(1, null, order[1], o.getUpOrderId(), orderResult.getBankTrxId(), orderResult.getBankOrderId());
-            case "store_remain":
-                return orderService.payRemainConfirm(order[1], o.getUpOrderId(), orderResult.getBankTrxId(), orderResult.getBankOrderId());
-            case "payment":
-                return storePaymentService.payConfirm(order[1], o.getUpOrderId(), orderResult.getBankTrxId(), orderResult.getBankOrderId());
+        String[] order = lowOrderId.split("-");
+        if (order.length < 2) {
+            logger.warn("易宝支付回调lowOrderId格式不正确:{}", lowOrderId);
+            return "FAIL";
+        }
+        //加分布式锁,防止并发重复回调
+        RLock lock = redissonClient.getLock("pay:notify:lock:" + order[1]);
+        try {
+            boolean locked = lock.tryLock(200, 30000, TimeUnit.MILLISECONDS);
+            if (!locked) {
+                logger.warn("易宝支付回调获取锁失败,orderNo={}", order[1]);
+                return "SUCCESS";
+            }
+            switch (order[0]) {
+                case "store":
+                    return orderService.payConfirm(PAY_SOURCE_ONLINE, null, order[1], upOrderId, orderResult.getBankTrxId(), orderResult.getBankOrderId());
+                case "store_remain":
+                    return orderService.payRemainConfirm(order[1], upOrderId, orderResult.getBankTrxId(), orderResult.getBankOrderId());
+                case "payment":
+                    return storePaymentService.payConfirm(order[1], upOrderId, orderResult.getBankTrxId(), orderResult.getBankOrderId());
+                default:
+                    logger.warn("未知的支付业务类型 bizType: {}", order[0]);
+                    break;
+            }
+        } catch (InterruptedException e) {
+            logger.error("易宝支付回调获取锁被中断,orderNo={}", order[1], e);
+            Thread.currentThread().interrupt();
+            return "SUCCESS";
+        } finally {
+            if (lock.isHeldByCurrentThread()) {
+                lock.unlock();
+            }
         }
         return "SUCCESS";
     }
@@ -172,11 +226,14 @@ public class PayScrmController {
             String[] order = o.getReq_seq_id().split("-");
             switch (order[0]) {
                 case "store":
-                    return orderService.payConfirmMultiStore(1, null, order[1], o.getHf_seq_id(), o.getOut_trans_id(), o.getParty_order_id());
+                    return orderService.payConfirmMultiStore(PAY_SOURCE_ONLINE, null, order[1], o.getHf_seq_id(), o.getOut_trans_id(), o.getParty_order_id());
                 case "store_remain":
                     return orderService.payRemainConfirmMultiStore(order[1], o.getHf_seq_id(), o.getOut_trans_id(), o.getParty_order_id());
                 case "payment":
                     return storePaymentService.payConfirmMultiStore(order[1], o.getHf_seq_id(), o.getOut_trans_id(), o.getParty_order_id());
+                default:
+                    logger.warn("未知的支付业务类型 bizType: {}", order[0]);
+                    break;
             }
         }
         return "ok";
@@ -256,36 +313,48 @@ public class PayScrmController {
             try {
                 boolean locked = lock.tryLock(200, 30000, TimeUnit.MILLISECONDS);
                 if (!locked) {
-                    logger.warn("易宝支付回调获取锁失败,payCode={}", payCode);
+                    // 【关键日志】锁获取失败:并发回调或处理耗时较长,订单状态可能未更新,但返回SUCCESS会导致易宝不重试
+                    logger.error("【易宝回调-锁失败】payCode={}, bizType={}, orderId={}, uniqueOrderNo={} 锁等待200ms超时,返回SUCCESS(易宝不会重试,需关注订单状态)",
+                            payCode, bizType, orderId, uniqueOrderNo);
                     return "SUCCESS";
                 }
+                logger.info("【易宝回调-锁获取成功】payCode={}, bizType={}, orderId={}", payCode, bizType, orderId);
 
                 //根据业务前缀路由到对应的多店铺处理方法
                 switch (bizType) {
                     case "store": {
                         // 商城订单支付成功 → 多店铺确认
-                        String result = orderService.payConfirmMultiStore(1, null, payCode, uniqueOrderNo, channelTrxId, bankOrderId);
+                        long startMs = System.currentTimeMillis();
+                        String result = orderService.payConfirmMultiStore(PAY_SOURCE_ONLINE, null, payCode, uniqueOrderNo, channelTrxId, bankOrderId);
+                        long costMs = System.currentTimeMillis() - startMs;
                         if ("success".equals(result) || "SUCCESS".equals(result)) {
+                            logger.info("【易宝回调-处理成功】store, payCode={}, cost={}ms, uniqueOrderNo={}", payCode, costMs, uniqueOrderNo);
                             return "SUCCESS";
                         }
-                        // 处理失败返回FAIL,让易宝重试(注意:空字符串表示已处理过/幂等,返回SUCCESS避免重复回调
+                        // 空字符串:可能是幂等已处理,也可能是处理失败(事务已回滚
                         if (StringUtils.isBlank(result)) {
-                            logger.warn("易宝回调payConfirmMultiStore返回空(payment已处理或状态异常),payCode={}", payCode);
+                            // 【关键日志】空字符串场景需重点排查:Service 已回滚事务,但 Controller 返回 SUCCESS,易宝不会重试
+                            logger.error("【易宝回调-返回空】store, payCode={}, cost={}ms, uniqueOrderNo={} Service返回空(可能未找到支付记录/订单/状态异常/异常回滚),事务已回滚但返回SUCCESS,订单状态可能仍为待支付",
+                                    payCode, costMs, uniqueOrderNo);
                             return "SUCCESS";
                         }
-                        logger.error("易宝回调payConfirmMultiStore处理失败:result={},payCode={},返回FAIL触发重试", result, payCode);
+                        logger.error("【易宝回调-处理失败】store, payCode={}, cost={}ms, result={}, uniqueOrderNo={} 返回FAIL触发易宝重试", payCode, costMs, result, uniqueOrderNo);
                         return "FAIL";
                     }
                     case "store_remain": {
+                        long startMs = System.currentTimeMillis();
                         String result = orderService.payRemainConfirmMultiStore(payCode, uniqueOrderNo, channelTrxId, bankOrderId);
+                        long costMs = System.currentTimeMillis() - startMs;
                         if ("success".equals(result) || "SUCCESS".equals(result)) {
+                            logger.info("【易宝回调-尾款处理成功】store_remain, payCode={}, cost={}ms, uniqueOrderNo={}", payCode, costMs, uniqueOrderNo);
                             return "SUCCESS";
                         }
                         if (StringUtils.isBlank(result)) {
-                            logger.warn("易宝回调payRemainConfirmMultiStore返回空,payCode={}", payCode);
+                            logger.error("【易宝回调-尾款返回空】store_remain, payCode={}, cost={}ms, uniqueOrderNo={} Service返回空(可能未找到支付记录/订单/异常回滚),事务已回滚但返回SUCCESS",
+                                    payCode, costMs, uniqueOrderNo);
                             return "SUCCESS";
                         }
-                        logger.error("易宝回调payRemainConfirmMultiStore处理失败:result={},payCode={},返回FAIL触发重试", result, payCode);
+                        logger.error("【易宝回调-尾款处理失败】store_remain, payCode={}, cost={}ms, result={}, uniqueOrderNo={} 返回FAIL触发易宝重试", payCode, costMs, result, uniqueOrderNo);
                         return "FAIL";
                     }
 //                    case "payment": {
@@ -297,11 +366,11 @@ public class PayScrmController {
 //                        return "SUCCESS";
 //                    }
                     default:
-                        logger.warn("易宝回调未知的业务前缀:{},orderId:{}", bizType, orderId);
+                        logger.error("【易宝回调-未知业务前缀】bizType={}, orderId={}, payCode={} 无法路由处理", bizType, orderId, payCode);
                         break;
                 }
             } catch (InterruptedException e) {
-                logger.error("易宝支付回调获取锁被中断,payCode={}", payCode, e);
+                logger.error("【易宝回调-锁中断】payCode={}, orderId={} 获取锁被中断,返回SUCCESS(易宝不会重试)", payCode, orderId, e);
                 Thread.currentThread().interrupt();
                 return "SUCCESS";
             } finally {

+ 48 - 6
fs-user-app/src/main/java/com/fs/app/controller/store/StoreOrderScrmController.java

@@ -14,6 +14,7 @@ import com.fs.app.annotation.Login;
 import com.fs.app.controller.AppBaseController;
 import com.fs.app.service.StoreOrderScrmAmountService;
 import com.fs.app.pay.constant.PaymentTypeConstant;
+import com.fs.common.annotation.RepeatSubmit;
 import com.fs.common.core.domain.R;
 import com.fs.common.exception.CustomException;
 import com.fs.common.utils.*;
@@ -376,6 +377,7 @@ public class StoreOrderScrmController extends AppBaseController {
     @Login
     @ApiOperation("创建订单")
     @PostMapping("/create")
+    @RepeatSubmit
     public R create(@Validated @RequestBody FsStoreOrderCreateParam param, HttpServletRequest request){
         String userId= getUserId();
         log.info("开始创建订单,登录用户id:{}", userId);
@@ -390,9 +392,10 @@ public class StoreOrderScrmController extends AppBaseController {
     @Login
     @ApiOperation("创建订单")
     @PostMapping("/createMultiStore")
+    @RepeatSubmit
 //    public R create(@Validated @RequestBody FsStoreOrderCreateParam param, HttpServletRequest request){
     public R createMultiStore(@Validated @RequestBody FsStoreOrderCreateGroupStoreParam param, HttpServletRequest request) {
-//        return orderService.createOrder(Long.parseLong(getUserId()),param);
+        long uid = Long.parseLong(getUserId());
         List<R> res = new ArrayList<>();
         String combinationOrderId = IdUtil.randomUUID();
         List<String> orderKeys = param.getOrderKeys();
@@ -403,6 +406,12 @@ public class StoreOrderScrmController extends AppBaseController {
         } else {
             createOrderKeys = Arrays.asList(param.getCreateOrderKey().split(","));
         }
+
+        // 先做一次预计算,按店铺金额比例分配积分抵扣(最后一个店铺用减法兜底,保证积分总和与用户实扣一致)
+        FsStoreOrderComputedGroupStoreParam computedGroupParam = new FsStoreOrderComputedGroupStoreParam();
+        BeanUtils.copyProperties(param, computedGroupParam);
+        List<FsStoreOrderComputeDTO> dtos = orderService.computedOrders(uid, computedGroupParam);
+
         for (int i = 0; i < orderKeys.size(); i++) {
             FsStoreOrderCreateParam fsStoreOrderCreateParam = new FsStoreOrderCreateParam();
             BeanUtils.copyProperties(param, fsStoreOrderCreateParam);
@@ -411,7 +420,14 @@ public class StoreOrderScrmController extends AppBaseController {
             fsStoreOrderCreateParam.setOrderKey(orderKeys.get(i));
             fsStoreOrderCreateParam.setCombinationOrderId(combinationOrderId);
             fsStoreOrderCreateParam.setCreateOrderKey(createOrderKeys.get(i));
-            R order = orderService.createOrderMultiStore(Long.parseLong(getUserId()), fsStoreOrderCreateParam);
+            // 注入预分配的积分抵扣结果(按店铺金额比例分摊,最后一个店铺用减法兜底,积分为整数)
+            FsStoreOrderComputeDTO dto = (dtos != null && i < dtos.size()) ? dtos.get(i) : null;
+            if (dto != null && dto.getUsedIntegral() != null && dto.getUsedIntegral() > 0) {
+                fsStoreOrderCreateParam.setPreAllocatedDeduction(dto.getDeductionPrice());
+                fsStoreOrderCreateParam.setPreAllocatedIntegral(dto.getUsedIntegral().intValue());
+                fsStoreOrderCreateParam.setIntegralDesc(dto.getIntegralDesc());
+            }
+            R order = orderService.createOrderMultiStore(uid, fsStoreOrderCreateParam);
             res.add(order);
         }
         return R.ok().put("data",res);
@@ -503,7 +519,14 @@ public class StoreOrderScrmController extends AppBaseController {
                     order.setPayDelivery(order.getPayPrice());
                     order.setPayMoney(BigDecimal.ZERO);
                 }
-                orderService.updateFsStoreOrder(order);
+                // 【关键修复】只更新支付相关字段,避免用完整 order 对象覆盖 status、paid 等字段
+                // 完整 order 对象的 status=0/paid=0 会与支付回调并发覆盖已支付状态
+                FsStoreOrderScrm payUpdateOrder = new FsStoreOrderScrm();
+                payUpdateOrder.setId(order.getId());
+                payUpdateOrder.setPayType(order.getPayType());
+                payUpdateOrder.setPayMoney(order.getPayMoney());
+                payUpdateOrder.setPayDelivery(order.getPayDelivery());
+                orderService.updateFsStoreOrder(payUpdateOrder);
             }
             String payCode = IdUtil.getSnowflake(0, 0).nextIdStr();
 //          order.setOrderCode(orderCode);
@@ -698,7 +721,13 @@ public class StoreOrderScrmController extends AppBaseController {
                         order.setPayDelivery(order.getPayPrice());
                         order.setPayMoney(BigDecimal.ZERO);
                     }
-                    orderService.updateFsStoreOrder(order);
+                    // 【关键修复】只更新支付相关字段,避免与支付回调并发覆盖 status、paid
+                    FsStoreOrderScrm combPayUpdateOrder = new FsStoreOrderScrm();
+                    combPayUpdateOrder.setId(order.getId());
+                    combPayUpdateOrder.setPayType(order.getPayType());
+                    combPayUpdateOrder.setPayMoney(order.getPayMoney());
+                    combPayUpdateOrder.setPayDelivery(order.getPayDelivery());
+                    orderService.updateFsStoreOrder(combPayUpdateOrder);
                 }
             } else {
                 return R.error(36672,"用户OPENID不存在");
@@ -966,7 +995,13 @@ public class StoreOrderScrmController extends AppBaseController {
                     order.setPayMoney(payMoney);
                     order.setPayDelivery(order.getPayPrice().subtract(payMoney) );
                 }
-                orderService.updateFsStoreOrder(order);
+                // 【关键修复】只更新支付相关字段,避免用完整 order 对象覆盖 status、paid 等字段
+                FsStoreOrderScrm editPayUpdateOrder = new FsStoreOrderScrm();
+                editPayUpdateOrder.setId(order.getId());
+                editPayUpdateOrder.setPayType(order.getPayType());
+                editPayUpdateOrder.setPayMoney(order.getPayMoney());
+                editPayUpdateOrder.setPayDelivery(order.getPayDelivery());
+                orderService.updateFsStoreOrder(editPayUpdateOrder);
             }
             return R.ok().put("order",order);
         }
@@ -1596,7 +1631,13 @@ public class StoreOrderScrmController extends AppBaseController {
                             prescribeOrder = prescribeOrder + "," + orders.get(i).getId();
                         }
                     }
-                    orderService.updateFsStoreOrder(orders.get(i));
+                    // 【关键修复】只更新支付相关字段,避免与支付回调并发覆盖 status、paid
+                    FsStoreOrderScrm combEditPayUpdateOrder = new FsStoreOrderScrm();
+                    combEditPayUpdateOrder.setId(orders.get(i).getId());
+                    combEditPayUpdateOrder.setPayType(orders.get(i).getPayType());
+                    combEditPayUpdateOrder.setPayMoney(orders.get(i).getPayMoney());
+                    combEditPayUpdateOrder.setPayDelivery(orders.get(i).getPayDelivery());
+                    orderService.updateFsStoreOrder(combEditPayUpdateOrder);
                     ids.add(orders.get(i).getId());
                     orderCodes.add(orders.get(i).getOrderCode());
                 }
@@ -1879,3 +1920,4 @@ public class StoreOrderScrmController extends AppBaseController {
         return IpUtils.getIpAddr(request).split(",")[0];
     }
 }
+

+ 7 - 3
fs-user-app/src/main/java/com/fs/app/controller/store/UserSignScrmController.java

@@ -31,6 +31,8 @@ import java.util.List;
 @RequestMapping(value="/store/app/sign")
 public class UserSignScrmController extends AppBaseController {
 
+    private static final String CONFIG_KEY_STORE_SIGN = "store.sign";
+
     @Autowired
     private IFsUserScrmService userService;
     @Autowired
@@ -46,11 +48,13 @@ public class UserSignScrmController extends AppBaseController {
     public R getUserSign(HttpServletRequest request){
         FsUserScrm user=userService.selectFsUserById(Long.parseLong(getUserId()));
         //获取签到配置
-        String json=configService.selectConfigByKey("store.sign");
+        String json=configService.selectConfigByKey(CONFIG_KEY_STORE_SIGN);
         //判断用户昨天是否签到过
         Long signNum=signService.getSign(user);
         Boolean isDaySign=signService.isDaySign(user);
-        return R.ok().put("isDaySign", isDaySign).put("signNum", signNum).put("integral",user.getIntegral().intValue()).put("sign", json);
+        Long integralVal = user.getIntegral();
+        int integralInt = integralVal != null ? integralVal.intValue() : 0;
+        return R.ok().put("isDaySign", isDaySign).put("signNum", signNum).put("integral",integralInt).put("sign", json);
     }
 
 
@@ -68,7 +72,7 @@ public class UserSignScrmController extends AppBaseController {
     @GetMapping("/getIntegral")
     public R getIntegral(FsUserBillQueryParam param, HttpServletRequest request){
         PageHelper.startPage(param.getPage(), param.getPageSize());
-        param.setUserId(param.getUserId());
+        param.setUserId(Long.parseLong(getUserId()));
         param.setCategory(BillDetailEnum.CATEGORY_2.getValue());
         List<FsUserBillScrm> list=billService.selectFsUserBillListQuery(param);
         PageInfo<FsUserBillScrm> listPageInfo=new PageInfo<>(list);

+ 9 - 17
fs-user-app/src/main/java/com/fs/app/redis/OrderExpireHandler.java

@@ -1,42 +1,32 @@
 package com.fs.app.redis;
 
 import cn.hutool.json.JSONUtil;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
 import com.fs.hisStore.domain.FsStoreOrderScrm;
-import com.fs.hisStore.domain.FsStoreProductScrm;
-import com.fs.hisStore.mapper.FsStoreOrderItemScrmMapper;
 import com.fs.hisStore.mapper.FsStoreOrderScrmMapper;
-import com.fs.hisStore.mapper.FsStoreProductScrmMapper;
-import com.fs.hisStore.mapper.FsStoreVerifyCodeScrmMapper;
 import com.fs.hisStore.service.IFsStoreOrderScrmService;
-import com.fs.hisStore.service.IFsStoreVerifyCodeScrmService;
-import com.fs.hisStore.domain.FsStoreVerifyCodeScrm;
-import com.fs.hisStore.vo.FsStoreOrderItemVO;
 import com.fs.system.service.ISysConfigService;
 import lombok.extern.slf4j.Slf4j;
-import org.checkerframework.checker.units.qual.A;
 import org.redisson.api.RLock;
 import org.redisson.api.RedissonClient;
-import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
-import org.springframework.transaction.annotation.Transactional;
 
 import java.time.LocalDateTime;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
 
 /**
- * @description: 订单过期处理
+ * @description: 订单过期处理(仅商城环境装配,看课环境不装配)
  * @author: Guos
  * @time: 2025/12/4 上午10:23
  */
 @Slf4j
 @Component
+// 仅商城环境装配(互易享 appId),看课环境(鸿良 appId: wx98abee0aa8ccdd3c)不注册定时任务
+@ConditionalOnProperty(prefix = "wx.open", name = "appId", havingValue = "wx7796a33a71912e32")
 public class OrderExpireHandler {
 
     @Autowired
@@ -50,8 +40,6 @@ public class OrderExpireHandler {
     @Autowired
     private ISysConfigService configService;
 
-    private static final String LOCK_KEY_PREFIX = "order-expire-product-lock:";
-
     private static final int LOCK_WAIT_TIME = 3; // 锁等待时间
     private static final int LOCK_LEASE_TIME = 66; // 锁持有时间秒
 
@@ -111,7 +99,11 @@ public class OrderExpireHandler {
             }
         }
     }
-    @Transactional(rollbackFor = Exception.class)
+
+    /**
+     * 处理单个超时订单(事务由 cancelOrderReuse 方法自身的 @Transactional 保证,
+     * 此处不再重复标注——同类内部调用 @Transactional 不生效)
+     */
     void processExpiredOrder(FsStoreOrderScrm order) {
         Long orderId = order.getId();
         if (orderId == null) {

+ 1 - 3
fs-user-app/src/main/java/com/fs/framework/config/DataSourceConfig.java

@@ -34,13 +34,11 @@ public class DataSourceConfig {
         return new DruidDataSource();
     }
 
-
-
     @Bean
     @Primary
     public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("sopDataSource") DataSource sopDataSource) {
         Map<Object, Object> targetDataSources = new HashMap<>();
-        targetDataSources.put(DataSourceType.MASTER, masterDataSource);
+        targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
         targetDataSources.put(DataSourceType.SOP.name(), sopDataSource);
         return new DynamicDataSource(masterDataSource, targetDataSources);
     }