Browse Source

Merge remote-tracking branch 'origin/企微聊天' into 企微聊天

yh 4 days ago
parent
commit
473868668e
86 changed files with 5555 additions and 163 deletions
  1. 27 0
      fs-admin/src/main/java/com/fs/his/controller/AdProfitDetailController.java
  2. 99 0
      fs-admin/src/main/java/com/fs/his/controller/FsConsecutiveWithdrawRecordController.java
  3. 69 0
      fs-admin/src/main/java/com/fs/his/controller/FsIntegralRedPacketLogController.java
  4. 19 4
      fs-admin/src/main/java/com/fs/his/task/CompanyBalanceTask.java
  5. 8 8
      fs-common/src/main/java/com/fs/common/utils/luckyDraw/LotteryUtil.java
  6. 5 0
      fs-service/src/main/java/com/fs/aiChat/mapper/InterestAiChatSessionMapper.java
  7. 17 0
      fs-service/src/main/java/com/fs/aiChat/param/InterestAiMessage.java
  8. 3 0
      fs-service/src/main/java/com/fs/company/domain/CompanyRedPacketBalanceLogs.java
  9. 5 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyRedPacketBalanceLogsMapper.java
  10. 4 2
      fs-service/src/main/java/com/fs/company/service/ICompanyService.java
  11. 177 24
      fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java
  12. 2 0
      fs-service/src/main/java/com/fs/course/param/FsCourseSendRewardUParam.java
  13. 2 0
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java
  14. 1 1
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseRewardRoundServiceImpl.java
  15. 671 75
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  16. 64 0
      fs-service/src/main/java/com/fs/his/config/AppConfig.java
  17. 120 0
      fs-service/src/main/java/com/fs/his/config/AppPageConfig.java
  18. 56 0
      fs-service/src/main/java/com/fs/his/domain/FsConsecutiveWithdrawRecord.java
  19. 12 0
      fs-service/src/main/java/com/fs/his/domain/FsIntegralCart.java
  20. 11 0
      fs-service/src/main/java/com/fs/his/domain/FsIntegralOrder.java
  21. 69 0
      fs-service/src/main/java/com/fs/his/domain/FsIntegralRedPacketLog.java
  22. 50 0
      fs-service/src/main/java/com/fs/his/domain/FsUserActiveLog.java
  23. 30 0
      fs-service/src/main/java/com/fs/his/dto/ActiveConfigDTO.java
  24. 1 0
      fs-service/src/main/java/com/fs/his/enums/FsUserIntegralLogTypeEnum.java
  25. 5 0
      fs-service/src/main/java/com/fs/his/mapper/AdProfitDetailMapper.java
  26. 81 0
      fs-service/src/main/java/com/fs/his/mapper/FsConsecutiveWithdrawRecordMapper.java
  27. 6 0
      fs-service/src/main/java/com/fs/his/mapper/FsIntegralCartMapper.java
  28. 4 7
      fs-service/src/main/java/com/fs/his/mapper/FsIntegralGoodsMapper.java
  29. 103 0
      fs-service/src/main/java/com/fs/his/mapper/FsIntegralRedPacketLogMapper.java
  30. 2 0
      fs-service/src/main/java/com/fs/his/mapper/FsStoreOrderItemMapper.java
  31. 68 0
      fs-service/src/main/java/com/fs/his/mapper/FsUserActiveLogMapper.java
  32. 3 1
      fs-service/src/main/java/com/fs/his/mapper/FsUserIntegralLogsMapper.java
  33. 12 0
      fs-service/src/main/java/com/fs/his/param/AdProfitDetailStatisticsParam.java
  34. 26 0
      fs-service/src/main/java/com/fs/his/param/AddGoodsIntoCartParam.java
  35. 18 0
      fs-service/src/main/java/com/fs/his/param/CreateOrderFromCartParm.java
  36. 9 0
      fs-service/src/main/java/com/fs/his/param/FsConsecutiveWithdrawRecordParam.java
  37. 13 0
      fs-service/src/main/java/com/fs/his/param/FsIntegralRedPacketLogParam.java
  38. 19 0
      fs-service/src/main/java/com/fs/his/param/FsIntegralWithdrawalParam.java
  39. 17 0
      fs-service/src/main/java/com/fs/his/param/GetFsIntegralCartDetailsParm.java
  40. 14 0
      fs-service/src/main/java/com/fs/his/param/GetFsIntegralCartListParam.java
  41. 4 0
      fs-service/src/main/java/com/fs/his/service/IAdProfitDetailService.java
  42. 66 0
      fs-service/src/main/java/com/fs/his/service/IFsConsecutiveWithdrawRecordService.java
  43. 15 0
      fs-service/src/main/java/com/fs/his/service/IFsIntegralCartService.java
  44. 74 0
      fs-service/src/main/java/com/fs/his/service/IFsIntegralRedPacketLogService.java
  45. 1 0
      fs-service/src/main/java/com/fs/his/service/IFsStoreOrderService.java
  46. 9 0
      fs-service/src/main/java/com/fs/his/service/IFsStorePaymentService.java
  47. 70 0
      fs-service/src/main/java/com/fs/his/service/IFsUserActiveLogService.java
  48. 3 4
      fs-service/src/main/java/com/fs/his/service/IFsUserIntegralLogsService.java
  49. 3 0
      fs-service/src/main/java/com/fs/his/service/IFsUserService.java
  50. 70 0
      fs-service/src/main/java/com/fs/his/service/impl/AdProfitDetailServiceImpl.java
  51. 405 0
      fs-service/src/main/java/com/fs/his/service/impl/FsConsecutiveWithdrawRecordServiceImpl.java
  52. 224 11
      fs-service/src/main/java/com/fs/his/service/impl/FsIntegralCartServiceImpl.java
  53. 454 0
      fs-service/src/main/java/com/fs/his/service/impl/FsIntegralRedPacketLogServiceImpl.java
  54. 5 0
      fs-service/src/main/java/com/fs/his/service/impl/FsStoreOrderServiceImpl.java
  55. 332 7
      fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java
  56. 257 0
      fs-service/src/main/java/com/fs/his/service/impl/FsUserActiveLogServiceImpl.java
  57. 6 4
      fs-service/src/main/java/com/fs/his/service/impl/FsUserIntegralLogsServiceImpl.java
  58. 207 4
      fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java
  59. 14 0
      fs-service/src/main/java/com/fs/his/vo/AdProfitDetailStatisticsVo.java
  60. 12 0
      fs-service/src/main/java/com/fs/his/vo/FsConsecutiveWithdrawRecordVo.java
  61. 50 0
      fs-service/src/main/java/com/fs/his/vo/FsIntegralGoodsVo.java
  62. 13 0
      fs-service/src/main/java/com/fs/his/vo/FsIntegralRedPacketLogVo.java
  63. 43 0
      fs-service/src/main/java/com/fs/his/vo/GetCartGoodsDetailsVo.java
  64. 37 0
      fs-service/src/main/java/com/fs/his/vo/GetFsIntegralCartDetailsVo.java
  65. 63 0
      fs-service/src/main/java/com/fs/his/vo/GetFsIntegralCartListVo.java
  66. 2 1
      fs-service/src/main/java/com/fs/qw/service/impl/LuckyBagServiceImpl.java
  67. 17 2
      fs-service/src/main/resources/mapper/company/CompanyRedPacketBalanceLogsMapper.xml
  68. 118 0
      fs-service/src/main/resources/mapper/his/AdProfitDetailMapper.xml
  69. 137 0
      fs-service/src/main/resources/mapper/his/FsConsecutiveWithdrawRecordMapper.xml
  70. 31 3
      fs-service/src/main/resources/mapper/his/FsIntegralCartMapper.xml
  71. 6 0
      fs-service/src/main/resources/mapper/his/FsIntegralGoodsMapper.xml
  72. 266 0
      fs-service/src/main/resources/mapper/his/FsIntegralRedPacketLogMapper.xml
  73. 6 0
      fs-service/src/main/resources/mapper/his/FsStoreOrderItemMapper.xml
  74. 100 0
      fs-service/src/main/resources/mapper/his/FsUserActiveLogMapper.xml
  75. 74 0
      fs-service/src/main/resources/mapper/his/FsUserIntegralLogsMapper.xml
  76. 10 1
      fs-service/src/main/resources/mapper/his/FsUserMapper.xml
  77. 7 0
      fs-service/src/main/resources/mapper/his/InterestAiChatSessionMapper.xml
  78. 18 0
      fs-user-app/src/main/java/com/fs/app/controller/CommonController.java
  79. 98 0
      fs-user-app/src/main/java/com/fs/app/controller/FsIntegralCartController.java
  80. 95 0
      fs-user-app/src/main/java/com/fs/app/controller/IntegralController.java
  81. 35 2
      fs-user-app/src/main/java/com/fs/app/controller/UserController.java
  82. 5 0
      fs-user-app/src/main/java/com/fs/app/controller/WxPayController.java
  83. 35 0
      fs-user-app/src/main/java/com/fs/app/controller/aiChat/controller/AiDoctorController.java
  84. 121 0
      fs-user-app/src/main/java/com/fs/app/controller/aiChat/controller/InterestAiController.java
  85. 14 1
      fs-user-app/src/main/java/com/fs/app/controller/course/CourseFsUserController.java
  86. 1 1
      fs-user-app/src/main/java/com/fs/app/controller/course/CourseQwController.java

+ 27 - 0
fs-admin/src/main/java/com/fs/his/controller/AdProfitDetailController.java

@@ -9,7 +9,9 @@ import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.his.domain.AdProfitDetail;
 import com.fs.his.domain.AdProfitDetail;
 import com.fs.his.dto.AdProfitDetailDto;
 import com.fs.his.dto.AdProfitDetailDto;
+import com.fs.his.param.AdProfitDetailStatisticsParam;
 import com.fs.his.service.IAdProfitDetailService;
 import com.fs.his.service.IAdProfitDetailService;
+import com.fs.his.vo.AdProfitDetailStatisticsVo;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
@@ -107,4 +109,29 @@ public class AdProfitDetailController extends BaseController
     {
     {
         return adProfitDetailService.getWithFinishAndMayWithdraw();
         return adProfitDetailService.getWithFinishAndMayWithdraw();
     }
     }
+
+    /**
+     * 统计
+     */
+    @PreAuthorize("@ss.hasPermi('his:statistics:commission')")
+    @GetMapping("/statistics")
+    public R statistics(AdProfitDetailStatisticsParam param)
+    {
+        List<AdProfitDetailStatisticsVo> list = adProfitDetailService.statisticsList(param);
+        return R.ok().put("list",list);
+    }
+
+    /**
+     * 导出统计
+     */
+    @PreAuthorize("@ss.hasPermi('his:statistics:commissionExport')")
+    @GetMapping("/exportCommission")
+    @Log(title = "广告分佣统计导出", businessType = BusinessType.EXPORT)
+    public AjaxResult exportCommission(AdProfitDetailStatisticsParam param)
+    {
+        List<AdProfitDetailStatisticsVo> list = adProfitDetailService.statisticsList(param);
+
+        ExcelUtil<AdProfitDetailStatisticsVo> util = new ExcelUtil<AdProfitDetailStatisticsVo>(AdProfitDetailStatisticsVo.class);
+        return util.exportExcel(list, "广告分佣统计数据");
+    }
 }
 }

+ 99 - 0
fs-admin/src/main/java/com/fs/his/controller/FsConsecutiveWithdrawRecordController.java

@@ -0,0 +1,99 @@
+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.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.his.domain.FsConsecutiveWithdrawRecord;
+import com.fs.his.param.FsConsecutiveWithdrawRecordParam;
+import com.fs.his.service.IFsConsecutiveWithdrawRecordService;
+import com.fs.his.vo.FsConsecutiveWithdrawRecordVo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 连续提现记录Controller
+ * 
+ * @author fs
+ * @date 2026-02-04
+ */
+@RestController
+@RequestMapping("/his/consecutiveWithdrawRecord")
+public class FsConsecutiveWithdrawRecordController extends BaseController
+{
+    @Autowired
+    private IFsConsecutiveWithdrawRecordService fsConsecutiveWithdrawRecordService;
+
+    /**
+     * 查询连续提现记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('his:consecutiveWithdrawRecord:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsConsecutiveWithdrawRecordParam param)
+    {
+        startPage();
+        List<FsConsecutiveWithdrawRecordVo> list = fsConsecutiveWithdrawRecordService.selectFsConsecutiveWithdrawRecordList(param);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出连续提现记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('his:consecutiveWithdrawRecord:export')")
+    @Log(title = "连续提现记录", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsConsecutiveWithdrawRecordParam fsConsecutiveWithdrawRecord)
+    {
+        List<FsConsecutiveWithdrawRecordVo> list = fsConsecutiveWithdrawRecordService.selectFsConsecutiveWithdrawRecordList(fsConsecutiveWithdrawRecord);
+        ExcelUtil<FsConsecutiveWithdrawRecordVo> util = new ExcelUtil<FsConsecutiveWithdrawRecordVo>(FsConsecutiveWithdrawRecordVo.class);
+        return util.exportExcel(list, "连续提现记录数据");
+    }
+
+    /**
+     * 获取连续提现记录详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('his:consecutiveWithdrawRecord:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(fsConsecutiveWithdrawRecordService.selectFsConsecutiveWithdrawRecordById(id));
+    }
+
+    /**
+     * 新增连续提现记录
+     */
+    @PreAuthorize("@ss.hasPermi('his:consecutiveWithdrawRecord:add')")
+    @Log(title = "连续提现记录", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsConsecutiveWithdrawRecord fsConsecutiveWithdrawRecord)
+    {
+        return toAjax(fsConsecutiveWithdrawRecordService.insertFsConsecutiveWithdrawRecord(fsConsecutiveWithdrawRecord));
+    }
+
+    /**
+     * 修改连续提现记录
+     */
+    @PreAuthorize("@ss.hasPermi('his:consecutiveWithdrawRecord:edit')")
+    @Log(title = "连续提现记录", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsConsecutiveWithdrawRecord fsConsecutiveWithdrawRecord)
+    {
+        return toAjax(fsConsecutiveWithdrawRecordService.updateFsConsecutiveWithdrawRecord(fsConsecutiveWithdrawRecord));
+    }
+
+    /**
+     * 删除连续提现记录
+     */
+    @PreAuthorize("@ss.hasPermi('his:consecutiveWithdrawRecord:remove')")
+    @Log(title = "连续提现记录", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(fsConsecutiveWithdrawRecordService.deleteFsConsecutiveWithdrawRecordByIds(ids));
+    }
+}

+ 69 - 0
fs-admin/src/main/java/com/fs/his/controller/FsIntegralRedPacketLogController.java

@@ -0,0 +1,69 @@
+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.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.his.param.FsIntegralRedPacketLogParam;
+import com.fs.his.service.IFsIntegralRedPacketLogService;
+import com.fs.his.vo.FsIntegralRedPacketLogVo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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
+ * 
+ * @author fs
+ * @date 2026-01-22
+ */
+@RestController
+@RequestMapping("/his/integralRedPacketLog")
+public class FsIntegralRedPacketLogController extends BaseController
+{
+    @Autowired
+    private IFsIntegralRedPacketLogService fsIntegralRedPacketLogService;
+
+    /**
+     * 查询积分佣金红包记录列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(FsIntegralRedPacketLogParam fsIntegralRedPacketLog)
+    {
+        startPage();
+        List<FsIntegralRedPacketLogVo> list = fsIntegralRedPacketLogService.getList(fsIntegralRedPacketLog);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出积分佣金红包记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('his:integralRedPacketLog:export')")
+    @Log(title = "积分佣金红包记录", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsIntegralRedPacketLogParam fsIntegralRedPacketLog)
+    {
+        List<FsIntegralRedPacketLogVo> list = fsIntegralRedPacketLogService.getList(fsIntegralRedPacketLog);
+        ExcelUtil<FsIntegralRedPacketLogVo> util = new ExcelUtil<FsIntegralRedPacketLogVo>(FsIntegralRedPacketLogVo.class);
+        return util.exportExcel(list, "积分佣金红包记录数据");
+    }
+
+    /**
+     * 获取积分佣金红包记录详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('his:integralRedPacketLog:query')")
+    @GetMapping(value = "/{logId}")
+    public AjaxResult getInfo(@PathVariable("logId") Long logId)
+    {
+        return AjaxResult.success(fsIntegralRedPacketLogService.selectFsIntegralRedPacketLogByLogId(logId));
+    }
+
+
+}

+ 19 - 4
fs-admin/src/main/java/com/fs/his/task/CompanyBalanceTask.java

@@ -1,5 +1,7 @@
 package com.fs.his.task;
 package com.fs.his.task;
 
 
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
 import com.fs.company.service.ICompanyService;
 import com.fs.company.service.ICompanyService;
 import com.fs.company.vo.RedPacketMoneyVO;
 import com.fs.company.vo.RedPacketMoneyVO;
 import com.fs.course.service.BalanceRollbackErrorService;
 import com.fs.course.service.BalanceRollbackErrorService;
@@ -7,6 +9,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 import org.springframework.stereotype.Component;
 
 
+import java.util.Date;
 import java.util.List;
 import java.util.List;
 
 
 /**
 /**
@@ -62,15 +65,27 @@ public class CompanyBalanceTask {
     }
     }
 
 
     /**
     /**
-     * @Description: 红包余额回滚(回滚的是客户没领取的红包),红包记录表中,两天没领取的记录不会再发送
+     * @Description: 优化成回滚前查询记录,一笔一笔回滚
      * @Param: 每天0点执行一次
      * @Param: 每天0点执行一次
      * @Return:
      * @Return:
      * @Author xgb
      * @Author xgb
      * @Date 2025/11/7 9:48
      * @Date 2025/11/7 9:48
      */
      */
-    public void rollbackRedPacketMoney() throws Exception {
-        // 这个地方真加的是company money字段 xgb 红包余额独立后这个方法弃用
-        companyService.rollbackRedPacketMoney();
+    public void rollbackRedPacketMoney(String time) throws Exception {
+        // 默认是前两天时间
+        String createSTime;
+        String createETime;
+        if (StringUtils.isNotBlank(time)) {
+            Date date = DateUtils.parseDate(time);
+            createSTime = time+" 00:00:00";
+            createETime = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD,DateUtils.addDays(date, 1))+" 00:00:00";
+        } else {
+            createSTime = DateUtils.parseDateToStr( DateUtils.YYYY_MM_DD,DateUtils.addDays(new Date(), -2))+" 00:00:00";
+            createETime = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD,DateUtils.addDays(new Date(), -1))+" 00:00:00";
+        }
+
+        // 这个地方真加的是company money字段 xgb
+        companyService.rollbackRedPacketMoney(createSTime, createETime);
     }
     }
 
 
     /**
     /**

+ 8 - 8
fs-common/src/main/java/com/fs/common/utils/luckyDraw/LotteryUtil.java

@@ -20,16 +20,16 @@ public class LotteryUtil {
         double total = prizes.stream().mapToDouble(Prize::getProbability).sum();
         double total = prizes.stream().mapToDouble(Prize::getProbability).sum();
 
 
         // [0, total) 之间取随机数
         // [0, total) 之间取随机数
-//        double random = ThreadLocalRandom.current().nextDouble() * total;
+        double random = ThreadLocalRandom.current().nextDouble() * total;
 
 
 //        // 逐步累加找到落点
 //        // 逐步累加找到落点
-//        double sum = 0;
-//        for (Prize prize : prizes) {
-//            sum += prize.getProbability();
-//            if (random < sum) {
-//                return prize;
-//            }
-//        }
+        double sum = 0;
+        for (Prize prize : prizes) {
+            sum += prize.getProbability();
+            if (random < sum) {
+                return prize;
+            }
+        }
 
 
         // 理论上不会到这里
         // 理论上不会到这里
         return prizes.get(prizes.size() - 1);
         return prizes.get(prizes.size() - 1);

+ 5 - 0
fs-service/src/main/java/com/fs/aiChat/mapper/InterestAiChatSessionMapper.java

@@ -5,6 +5,7 @@ import com.fs.aiChat.domain.DoctorAiChatLog;
 import com.fs.aiChat.domain.InterestAiChatMsg;
 import com.fs.aiChat.domain.InterestAiChatMsg;
 import com.fs.aiChat.domain.InterestAiSession;
 import com.fs.aiChat.domain.InterestAiSession;
 import com.fs.aiChat.domain.SessionRoleInfo;
 import com.fs.aiChat.domain.SessionRoleInfo;
+import com.fs.aiChat.param.InterestAiMessage;
 import com.fs.his.domain.FsInterestAiMsg;
 import com.fs.his.domain.FsInterestAiMsg;
 import com.fs.his.domain.FsInterestAiRole;
 import com.fs.his.domain.FsInterestAiRole;
 import com.fs.his.domain.FsInterestAiSession;
 import com.fs.his.domain.FsInterestAiSession;
@@ -56,4 +57,8 @@ public interface InterestAiChatSessionMapper extends BaseMapper<DoctorAiChatLog>
 
 
 
 
     FsInterestAiRole selectFsInterestAiRoleByRoleId(Long roleId);
     FsInterestAiRole selectFsInterestAiRoleByRoleId(Long roleId);
+
+    Integer selectSessionIdByUserAndRole(@Param("userId") Long userId, @Param("roleId") Integer roleId);
+
+    Integer selectAiMsgBySessionIdAndMsg(InterestAiMessage message);
 }
 }

+ 17 - 0
fs-service/src/main/java/com/fs/aiChat/param/InterestAiMessage.java

@@ -1,7 +1,10 @@
 package com.fs.aiChat.param;
 package com.fs.aiChat.param;
 
 
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import lombok.Data;
 
 
+import java.util.Date;
+
 @Data
 @Data
 public class InterestAiMessage {
 public class InterestAiMessage {
     Long userId;
     Long userId;
@@ -11,5 +14,19 @@ public class InterestAiMessage {
     String roleName;
     String roleName;
     String message;
     String message;
     Long sessionId;
     Long sessionId;
+    @JsonFormat(
+            pattern = "yyyy/MM/dd HH:mm:ss",
+            shape = JsonFormat.Shape.STRING
+    )
+    Date startTime;
+    @JsonFormat(
+            pattern = "yyyy/MM/dd HH:mm:ss",
+            shape = JsonFormat.Shape.STRING
+    )
+    Date endTime;
+
+    String deviceId;
+    Long isRefresh;
+    String beginTime;
 //    Integer isWelcome;
 //    Integer isWelcome;
 }
 }

+ 3 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyRedPacketBalanceLogs.java

@@ -42,5 +42,8 @@ public class CompanyRedPacketBalanceLogs extends BaseEntity{
     /** 是否处理状态(0-初始化,1-已同步) */
     /** 是否处理状态(0-初始化,1-已同步) */
     private Long status;
     private Long status;
 
 
+    // 红包日志id
+    private Long redPacketId;
+
 
 
 }
 }

+ 5 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyRedPacketBalanceLogsMapper.java

@@ -4,6 +4,7 @@ import java.util.List;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.company.domain.Company;
 import com.fs.company.domain.Company;
 import com.fs.company.domain.CompanyRedPacketBalanceLogs;
 import com.fs.company.domain.CompanyRedPacketBalanceLogs;
+import org.apache.ibatis.annotations.Param;
 
 
 /**
 /**
  * 企业红包余额记录Mapper接口
  * 企业红包余额记录Mapper接口
@@ -61,4 +62,8 @@ public interface CompanyRedPacketBalanceLogsMapper extends BaseMapper<CompanyRed
     int deleteCompanyRedPacketBalanceLogsByLogsIds(Long[] logsIds);
     int deleteCompanyRedPacketBalanceLogsByLogsIds(Long[] logsIds);
 
 
     Company getCompanyRedPacketBalance(Long companyId);
     Company getCompanyRedPacketBalance(Long companyId);
+
+    List<CompanyRedPacketBalanceLogs> selectCompanyRedPacketBalanceLogsListByStatus(@Param("createSTime") String createSTime, @Param("createETime") String createETime);
+
+    void updateCompanyRedPacketBalanceLogsByRedPacketId(CompanyRedPacketBalanceLogs redLogs);
 }
 }

+ 4 - 2
fs-service/src/main/java/com/fs/company/service/ICompanyService.java

@@ -177,7 +177,7 @@ public interface ICompanyService
 
 
     void redPacketTopUpCompany(Long companyId, BigDecimal money,String type);
     void redPacketTopUpCompany(Long companyId, BigDecimal money,String type);
 
 
-    void asyncRecordBalanceLog(Long companyId, BigDecimal money,Integer logType, BigDecimal balance, String remark);
+    void asyncRecordBalanceLog(Long companyId, BigDecimal money, Integer logType, BigDecimal balance, String remark, Long logId);
 
 
     void recordRedPacketBalance();
     void recordRedPacketBalance();
 
 
@@ -187,7 +187,7 @@ public interface ICompanyService
      */
      */
     void batchUpdateCompany(List<Company> list);
     void batchUpdateCompany(List<Company> list);
 
 
-    void rollbackRedPacketMoney();
+    void rollbackRedPacketMoney(String createSTime, String createETime);
 
 
 
 
     List<CompanyVO> liveShowList(CompanyParam param);
     List<CompanyVO> liveShowList(CompanyParam param);
@@ -197,4 +197,6 @@ public interface ICompanyService
     void addCompanyTuiLiveMoney(LiveOrder order);
     void addCompanyTuiLiveMoney(LiveOrder order);
 
 
     void subLiveCompanyMoney(LiveOrder order);
     void subLiveCompanyMoney(LiveOrder order);
+
+    R checkMchTransferStatusByBatchID(String batchId, Long companyId, String appId);
 }
 }

+ 177 - 24
fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java

@@ -19,11 +19,14 @@ import com.fs.company.domain.*;
 import com.fs.company.mapper.*;
 import com.fs.company.mapper.*;
 import com.fs.company.param.CompanyLiveShowParam;
 import com.fs.company.param.CompanyLiveShowParam;
 import com.fs.company.param.CompanyParam;
 import com.fs.company.param.CompanyParam;
-import com.fs.company.service.ICompanyMiniappService;
-import com.fs.company.service.ICompanyProfitService;
-import com.fs.company.service.ICompanyRoleService;
+import com.fs.company.service.*;
 import com.fs.company.vo.*;
 import com.fs.company.vo.*;
+import com.fs.core.config.WxOpenProperties;
+import com.fs.course.config.CourseConfig;
+import com.fs.course.config.RedPacketConfig;
+import com.fs.course.domain.FsCourseRedPacketLog;
 import com.fs.course.mapper.FsCourseRedPacketLogMapper;
 import com.fs.course.mapper.FsCourseRedPacketLogMapper;
+import com.fs.his.config.AppConfig;
 import com.fs.his.config.StoreConfig;
 import com.fs.his.config.StoreConfig;
 import com.fs.his.domain.FsInquiryOrder;
 import com.fs.his.domain.FsInquiryOrder;
 import com.fs.his.domain.FsStoreOrder;
 import com.fs.his.domain.FsStoreOrder;
@@ -41,6 +44,14 @@ import com.fs.store.config.CompanyMenuConfig;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.service.ISysConfigService;
 import com.fs.system.service.ISysConfigService;
+import com.github.binarywang.wxpay.bean.transfer.QueryTransferBatchesRequest;
+import com.github.binarywang.wxpay.bean.transfer.QueryTransferBatchesResult;
+import com.github.binarywang.wxpay.bean.transfer.TransferBillsGetResult;
+import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.TransferService;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageHelper;
 import com.google.gson.Gson;
 import com.google.gson.Gson;
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.collections4.CollectionUtils;
@@ -49,10 +60,10 @@ import org.redisson.api.RLock;
 import org.redisson.api.RedissonClient;
 import org.redisson.api.RedissonClient;
 import org.slf4j.Logger;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
-import com.fs.company.service.ICompanyService;
 import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.support.TransactionTemplate;
 import org.springframework.transaction.support.TransactionTemplate;
@@ -127,6 +138,12 @@ public class CompanyServiceImpl implements ICompanyService
     @Autowired
     @Autowired
     private CompanyRedPacketBalanceLogsMapper companyRedPacketBalanceLogsMapper;
     private CompanyRedPacketBalanceLogsMapper companyRedPacketBalanceLogsMapper;
 
 
+    @Autowired
+    private ICompanyConfigService companyConfigService;
+
+    @Autowired
+    private WxOpenProperties openProperties;
+
 
 
     @Override
     @Override
     public List<CompanyVO> liveShowList(CompanyParam param) {
     public List<CompanyVO> liveShowList(CompanyParam param) {
@@ -1448,7 +1465,7 @@ public class CompanyServiceImpl implements ICompanyService
                                     // 记录余额变更日志
                                     // 记录余额变更日志
                                     String remark = "同步公司余额,差额: " + amount+"(正数为增加,负数为扣减)";
                                     String remark = "同步公司余额,差额: " + amount+"(正数为增加,负数为扣减)";
                                     // 实际不发生交易只是从缓存同步金额到数据库中 交易金额登记为0,备注清楚同步的金额
                                     // 实际不发生交易只是从缓存同步金额到数据库中 交易金额登记为0,备注清楚同步的金额
-                                    asyncRecordBalanceLog(company.getCompanyId(),new BigDecimal(0),17,redisMoney,remark);
+                                    asyncRecordBalanceLog(company.getCompanyId(),new BigDecimal(0),17,redisMoney,remark, null);
                                 }
                                 }
                             }
                             }
                             return null;
                             return null;
@@ -1459,6 +1476,13 @@ public class CompanyServiceImpl implements ICompanyService
                 }));
                 }));
     }
     }
 
 
+    /**
+     * @Description: 红包充值
+     * @Param: type 充值类型 1 充值 2 扣款
+     * @Return:
+     * @Author xgb
+     * @Date 2025/11/3 14:01
+     */
     /**
     /**
      * @Description: 红包充值
      * @Description: 红包充值
      * @Param: type 充值类型 1 充值 2 扣款
      * @Param: type 充值类型 1 充值 2 扣款
@@ -1501,7 +1525,7 @@ public class CompanyServiceImpl implements ICompanyService
                 redisCache.setCacheObject(companyMoneyKey, newMoney.toString());
                 redisCache.setCacheObject(companyMoneyKey, newMoney.toString());
 
 
                 // 异步登记余额添加日志
                 // 异步登记余额添加日志
-                asyncRecordBalanceLog(companyId,money,16,newMoney,"红包充值(负数为扣款)");
+                asyncRecordBalanceLog(companyId,money,16,newMoney,"红包充值(负数为扣款)", null);
 
 
             } else {
             } else {
                 logger.error("获取redis锁失败,异常请求参数companyId:{},money:{},type:{}",companyId,money, type);
                 logger.error("获取redis锁失败,异常请求参数companyId:{},money:{},type:{}",companyId,money, type);
@@ -1527,15 +1551,17 @@ public class CompanyServiceImpl implements ICompanyService
 
 
     /**
     /**
      * 异步登记余额添加日志  xgb
      * 异步登记余额添加日志  xgb
+     *
      * @param companyId 公司ID
      * @param companyId 公司ID
-     * @param money 变更金额
-     * @param balance 当前余额
-     * @param remark 备注信息
-     * @param logType 16-红包余额充值 15-红包余额扣除 17-同步公司余额
+     * @param money     变更金额
+     * @param logType   16-红包余额充值 15-红包余额扣除 17-同步公司余额
+     * @param balance   当前余额
+     * @param remark    备注信息
+     * @param logId
      */
      */
     @Async
     @Async
     @Override
     @Override
-    public void asyncRecordBalanceLog(Long companyId, BigDecimal money,Integer logType, BigDecimal balance, String remark) {
+    public void asyncRecordBalanceLog(Long companyId, BigDecimal money, Integer logType, BigDecimal balance, String remark, Long logId) {
         try {
         try {
             CompanyRedPacketBalanceLogs log = new CompanyRedPacketBalanceLogs();
             CompanyRedPacketBalanceLogs log = new CompanyRedPacketBalanceLogs();
             log.setCompanyId(companyId);
             log.setCompanyId(companyId);
@@ -1544,6 +1570,7 @@ public class CompanyServiceImpl implements ICompanyService
             log.setLogsType(logType); // 同步余额
             log.setLogsType(logType); // 同步余额
             log.setBalance(balance);
             log.setBalance(balance);
             log.setCreateTime(new Date());
             log.setCreateTime(new Date());
+            log.setRedPacketId(logId);
             companyRedPacketBalanceLogsMapper.insertCompanyRedPacketBalanceLogs(log);
             companyRedPacketBalanceLogsMapper.insertCompanyRedPacketBalanceLogs(log);
         } catch (Exception e) {
         } catch (Exception e) {
             logger.error("异步登记红包余额日志失败 - 公司ID: {}, 金额: {}, 余额: {}, 备注: {}",
             logger.error("异步登记红包余额日志失败 - 公司ID: {}, 金额: {}, 余额: {}, 备注: {}",
@@ -1574,7 +1601,7 @@ public class CompanyServiceImpl implements ICompanyService
                     // 实际不发生交易只是从缓存获取当天余额报错25小时 交易金额登记为0,备注清楚同步的金额
                     // 实际不发生交易只是从缓存获取当天余额报错25小时 交易金额登记为0,备注清楚同步的金额
                     String remark = "时间:" + time +",当前公司余额,金额: " + moneyStr;
                     String remark = "时间:" + time +",当前公司余额,金额: " + moneyStr;
                     BigDecimal money = new BigDecimal(moneyStr);
                     BigDecimal money = new BigDecimal(moneyStr);
-                    asyncRecordBalanceLog(company.getCompanyId(),new BigDecimal(0),18,money,remark);
+                    asyncRecordBalanceLog(company.getCompanyId(),new BigDecimal(0),18,money,remark, null);
                 }
                 }
                 return null;
                 return null;
             });
             });
@@ -1596,20 +1623,67 @@ public class CompanyServiceImpl implements ICompanyService
      * @Param:
      * @Param:
      * @Return:
      * @Return:
      * @Author xgb
      * @Author xgb
-     * @Date 2025/11/7 9:53
+     * @Date 2025/12/25 9:32
      */
      */
     @Override
     @Override
-    public void rollbackRedPacketMoney() {
-        List<RedPacketMoneyVO> redPacketMoneyVOS = fsCourseRedPacketLogMapper.selectFsCourseAddRedPacketLogByCompany();
-        for(RedPacketMoneyVO company:redPacketMoneyVOS){
-            logger.info("红包余额回滚开始:{}",company);
-        }
-        Optional.ofNullable(redPacketMoneyVOS).ifPresent(list -> list.forEach(company -> {
+    public void rollbackRedPacketMoney(String createSTime, String createETime) {
+        // 回滚前查询一下红包记录
+        List<CompanyRedPacketBalanceLogs> companyRedPacketBalanceLogsList = companyRedPacketBalanceLogsMapper.selectCompanyRedPacketBalanceLogsListByStatus(createSTime, createETime);
+
+        Optional.ofNullable(companyRedPacketBalanceLogsList).ifPresent(list -> list.forEach(company -> {
+
+            if(company.getRedPacketId()==null){// 无数据跳过
+                logger.info("红包记录未登记,流水{}",company.getLogsId());
+                return;
+            }
+
+            // 查询红包记录
+            FsCourseRedPacketLog redLogs = fsCourseRedPacketLogMapper.selectFsCourseRedPacketLogByLogId(company.getRedPacketId());
+            if(redLogs==null){
+                logger.error("未查询到红包记录,流水{}",company.getLogsId());
+                return;
+            }
 
 
             if(company.getCompanyId()==null){
             if(company.getCompanyId()==null){
-                logger.error("红包记录表中存在公司id为null的异常数据");
+                logger.error("红包记录表中存在公司id为null的异常数据,流水{}",company.getLogsId());
+                return;
+            }
+
+            if(!StringUtils.isEmpty(redLogs.getBatchId())){
+                R result=checkMchTransferStatusByBatchID(redLogs.getBatchId(),redLogs.getCompanyId(),redLogs.getAppId());
+                if("200".equals(String.valueOf(result.get("code")))){
+                    FsCourseRedPacketLog update = new FsCourseRedPacketLog();
+                    update.setUpdateTime(new Date());
+                    update.setLogId(redLogs.getLogId());
+
+                    // 更新扣减状态
+                    CompanyRedPacketBalanceLogs redBalanceLogs = new CompanyRedPacketBalanceLogs();
+                    redBalanceLogs.setRedPacketId(company.getRedPacketId());
+
+                    if("success".equals(result.get("status"))){
+                        update.setStatus(1);
+                        fsCourseRedPacketLogMapper.updateFsCourseRedPacketLog(update);
+
+                        redBalanceLogs.setStatus(1L);
+                        companyRedPacketBalanceLogsMapper.updateCompanyRedPacketBalanceLogsByRedPacketId(redBalanceLogs);
+                        return;
+                    }else if("fail".equals(result.get("status"))){// 只对失败的部分进行回滚
+                        // 更新支付状态
+//                        update.setStatus(2); // 已退回
+//                        fsCourseRedPacketLogMapper.updateFsCourseRedPacketLog(update);
+
+                        redBalanceLogs.setStatus(2L);
+                        companyRedPacketBalanceLogsMapper.updateCompanyRedPacketBalanceLogsByRedPacketId(redBalanceLogs);
+                    }
+                }else {
+                    logger.info("商户转账状态查询失败,流水{}",company.getLogsId());
+                    return;
+                }
+            }else {
+                logger.error("红包记录表中存在商户批次号为null的异常数据,流水{}",company.getLogsId());
                 return;
                 return;
             }
             }
+
             String companyMoneyKey = FsConstants.COMPANY_MONEY_KEY + company.getCompanyId();
             String companyMoneyKey = FsConstants.COMPANY_MONEY_KEY + company.getCompanyId();
             // 加锁,与看课发放红包的加锁保持一致
             // 加锁,与看课发放红包的加锁保持一致
             RLock lock = redissonClient.getLock(FsConstants.COMPANY_MONEY_LOCK + company.getCompanyId());
             RLock lock = redissonClient.getLock(FsConstants.COMPANY_MONEY_LOCK + company.getCompanyId());
@@ -1623,14 +1697,14 @@ public class CompanyServiceImpl implements ICompanyService
                     if (StringUtils.isNotEmpty(moneyStr)) {
                     if (StringUtils.isNotEmpty(moneyStr)) {
                         redisMoney = new BigDecimal(moneyStr);
                         redisMoney = new BigDecimal(moneyStr);
                     }else {
                     }else {
-                        logger.error("缓存公司id:{}的余额不存在,回滚金额{}",company.getCompanyId(),company.getMoney());
+                        logger.error("缓存公司id:{}的余额不存在,回滚金额{}",company.getCompanyId(),redLogs.getAmount());
                         return;
                         return;
                     }
                     }
-                    BigDecimal newMoney = redisMoney.add(company.getMoney());
+                    BigDecimal newMoney = redisMoney.add(redLogs.getAmount());
                     redisCache.setCacheObject(companyMoneyKey, newMoney.toString());
                     redisCache.setCacheObject(companyMoneyKey, newMoney.toString());
 
 
-                    String remark = "执行时间:"+DateUtils.getTime()+",T2天客户未领取红包退回,金额: " + company.getMoney();
-                    asyncRecordBalanceLog(company.getCompanyId(),company.getMoney(),16,newMoney,remark);
+                    String remark = "执行时间:"+DateUtils.getTime()+",T2天客户未领取红包退回,金额: " + redLogs.getAmount();
+                    asyncRecordBalanceLog(company.getCompanyId(),redLogs.getAmount(),19,newMoney,remark, redLogs.getLogId());
                 }
                 }
             } catch (Exception e) {
             } catch (Exception e) {
                 logger.error("退回的红包同步增加到缓存和数据表,参数错误,请求异常,异常信息:{}", e.getMessage(), e);
                 logger.error("退回的红包同步增加到缓存和数据表,参数错误,请求异常,异常信息:{}", e.getMessage(), e);
@@ -1646,4 +1720,83 @@ public class CompanyServiceImpl implements ICompanyService
         }));
         }));
     }
     }
 
 
+    @Override
+    public R checkMchTransferStatusByBatchID(String batchId, Long companyId, String appId) {
+        // 获取配置信息
+        CourseConfig courseConfig = JSONUtil.toBean(configService.selectConfigByKey("course.config"), CourseConfig.class);
+
+        String json;
+        RedPacketConfig config = new RedPacketConfig();
+        // 根据红包模式获取配置
+        switch (courseConfig.getRedPacketMode()){
+            case 1:
+                json = configService.selectConfigByKey("redPacket.config");
+                config = JSONUtil.toBean(json, RedPacketConfig.class);
+                break;
+            case 2:
+                json = companyConfigService.selectRedPacketConfigByKey(companyId);
+                //如果分公司配置为空就走总后台的配置
+                if (StringUtils.isEmpty(json)){
+                    json = configService.selectConfigByKey("redPacket.config");
+                }
+                config = JSONUtil.toBean(json, RedPacketConfig.class);
+                break;
+            default:
+                throw new UnsupportedOperationException("当前红包模式不支持!");
+        }
+
+        //创建微信订单
+        WxPayConfig payConfig = new WxPayConfig();
+        String appAppId = openProperties.getAppId();
+        if(StringUtils.equals(appAppId,appId)){
+            json = configService.selectConfigByKey("app.config");
+            AppConfig appRedPacketConfig = JSONUtil.toBean(json, AppConfig.class);
+            BeanUtils.copyProperties(appRedPacketConfig,payConfig);
+        }else {
+            BeanUtils.copyProperties(config,payConfig);
+        }
+
+
+        WxPayService wxPayService = new WxPayServiceImpl();
+        wxPayService.setConfig(payConfig);
+        TransferService transferService=wxPayService.getTransferService();
+
+        Map<String,Object> map = new HashMap<>();
+        map.put("status","待确认"); //
+        if (config.getIsNew() != null && config.getIsNew() == 1) {
+            try {
+                TransferBillsGetResult queryRedPacketResult = transferService.getBillsByTransferBillNo(batchId);
+                logger.info("FsCourseRedPacketLog-batchId:{},【红包处理】查询批次结果:{}",batchId,queryRedPacketResult.toString());
+                if(("SUCCESS").equals(queryRedPacketResult.getState())){
+                    map.put("status","success");
+                }else if(("FAIL").equals(queryRedPacketResult.getState())){
+                    map.put("status","fail");
+                }
+                return R.ok(map);
+            } catch (WxPayException e) {
+                logger.error(e.getMessage());
+                return R.error(e.getMessage());
+            }
+        } else {
+            QueryTransferBatchesRequest request = new QueryTransferBatchesRequest();
+            request.setBatchId(batchId);
+            request.setNeedQueryDetail(false);
+
+            try {
+                QueryTransferBatchesResult queryTransferBatchesResult = transferService.transferBatchesBatchId(request);
+                logger.info("FsCourseRedPacketLog-batchId,【红包处理】查询批次结果:{}",batchId,queryTransferBatchesResult.toString());
+                if(("FINISHED").equals(queryTransferBatchesResult.getTransferBatch().getBatchStatus())){
+                    map.put("status","success");
+                }else if(("CLOSED").equals(queryTransferBatchesResult.getTransferBatch().getBatchStatus())){
+                    map.put("status","fail");
+                }
+                return R.ok(map);
+            } catch (WxPayException e) {
+                logger.error(e.getMessage());
+                return R.error(e.getMessage());
+            }
+        }
+
+    }
+
 }
 }

+ 2 - 0
fs-service/src/main/java/com/fs/course/param/FsCourseSendRewardUParam.java

@@ -41,4 +41,6 @@ public class FsCourseSendRewardUParam implements Serializable
     private Integer isAuto;
     private Integer isAuto;
     private Integer rewardType; //奖励类型 1红包 2积分 3随机转盘 4保底转盘 5大礼品
     private Integer rewardType; //奖励类型 1红包 2积分 3随机转盘 4保底转盘 5大礼品
 
 
+    private Long watchLogId;
+
 }
 }

+ 2 - 0
fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java

@@ -265,4 +265,6 @@ public interface IFsUserCourseVideoService extends IService<FsUserCourseVideo> {
      * 领取签到大礼品奖品
      * 领取签到大礼品奖品
      */
      */
     Map<String, Object> claimSignReward(Long userId);
     Map<String, Object> claimSignReward(Long userId);
+
+    R withdrawal(FsCourseSendRewardUParam param);
 }
 }

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

@@ -256,7 +256,7 @@ public class FsCourseRewardRoundServiceImpl extends ServiceImpl<FsCourseRewardRo
         rewardRound.setActualRewards(String.valueOf(auctual));
         rewardRound.setActualRewards(String.valueOf(auctual));
         if (result.get("code").equals(200)){
         if (result.get("code").equals(200)){
             insertFsCourseRewardRound(rewardRound);
             insertFsCourseRewardRound(rewardRound);
-            return R.ok("芳华币+"+auctual);
+            return R.ok("积分币+"+auctual);
         }else {
         }else {
             return result;
             return result;
         }
         }

+ 671 - 75
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -297,6 +297,9 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     @Autowired
     @Autowired
     private FsUserSignMapper fsUserSignMapper;
     private FsUserSignMapper fsUserSignMapper;
 
 
+    @Autowired
+    private FsCourseRewardVideoRelationMapper videoRelationMapper;
+
 
 
     /**
     /**
      * 查询课堂视频
      * 查询课堂视频
@@ -1540,11 +1543,16 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
             }
             }
 
 
             log.info("奖励类型:{}", config.getRewardType());
             log.info("奖励类型:{}", config.getRewardType());
-            // 根据奖励类型发放不同奖励
             switch (config.getRewardType()) {
             switch (config.getRewardType()) {
                 // 红包奖励
                 // 红包奖励
                 case 1:
                 case 1:
-                    return sendRedPacketReward(param, user, watchLog, video, config);
+                    if (param.getSource() == 3){
+                        param.setWatchLogId(watchLog.getLogId());
+                        return withdrawal(param);
+                    } else {
+                        return sendRedPacketReward(param, user, watchLog, video, config);
+                    }
+
                 // 积分奖励
                 // 积分奖励
                 case 2:
                 case 2:
                     return sendIntegralReward(param, user, watchLog, config);
                     return sendIntegralReward(param, user, watchLog, config);
@@ -1662,7 +1670,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
             if (log.getRewardType() == 1) {
             if (log.getRewardType() == 1) {
                 FsCourseRedPacketLog fsCourseRedPacketLog = redPacketLogMapper.selectUserFsCourseRedPacketLog(param.getVideoId(), param.getUserId(), param.getPeriodId());
                 FsCourseRedPacketLog fsCourseRedPacketLog = redPacketLogMapper.selectUserFsCourseRedPacketLog(param.getVideoId(), param.getUserId(), param.getPeriodId());
                 if (fsCourseRedPacketLog != null && fsCourseRedPacketLog.getStatus() == 1) {
                 if (fsCourseRedPacketLog != null && fsCourseRedPacketLog.getStatus() == 1) {
-                    return R.error("已领取该课程奖励,不可重复领取!");
+                    return R.error("已领取该课程奖励,不可重复领取!").put("data",fsCourseRedPacketLog.getResult());
                 }
                 }
                 if (fsCourseRedPacketLog != null && fsCourseRedPacketLog.getStatus() == 0) {
                 if (fsCourseRedPacketLog != null && fsCourseRedPacketLog.getStatus() == 0) {
                     if (StringUtils.isNotEmpty(fsCourseRedPacketLog.getResult())) {
                     if (StringUtils.isNotEmpty(fsCourseRedPacketLog.getResult())) {
@@ -1686,7 +1694,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
 
 
         // 判断来源是否是app,如是app,则发放积分奖励
         // 判断来源是否是app,如是app,则发放积分奖励
         int sourceApp = 3;
         int sourceApp = 3;
-        if (sourceApp == param.getSource() && !CloudHostUtils.hasCloudHostName("中康")) {
+        if (sourceApp == param.getSource() && !CloudHostUtils.hasCloudHostName("中康","金牛明医")) {
             return sendIntegralReward(param, user, log, config);
             return sendIntegralReward(param, user, log, config);
         }
         }
 
 
@@ -1694,7 +1702,18 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         switch (config.getRewardType()) {
         switch (config.getRewardType()) {
             // 红包奖励
             // 红包奖励
             case 1:
             case 1:
-                return sendRedPacketRewardFsUser(param, user, log, video, config);
+                if (sourceApp == param.getSource()){
+                    WxSendRedPacketParam packetParam = new WxSendRedPacketParam();
+                    String openId = getOpenId(param, user);
+                    if (StringUtils.isBlank(openId)) {
+                        return R.error("请重新使用微信登录");
+                    }
+                    packetParam.setOpenId(openId);
+                    BeanUtils.copyProperties(param, packetParam);
+                    return sendAppRedPacket(packetParam, log,video, config);
+                } else {
+                    return sendRedPacketRewardFsUser(param, user, log, video, config);
+                }
             // 积分奖励
             // 积分奖励
             case 2:
             case 2:
                 return sendIntegralReward(param, user, log, config);
                 return sendIntegralReward(param, user, log, config);
@@ -1930,7 +1949,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
 
 
                 // 异步登记余额扣减日志
                 // 异步登记余额扣减日志
                 BigDecimal money=amount.multiply(BigDecimal.valueOf(-1));
                 BigDecimal money=amount.multiply(BigDecimal.valueOf(-1));
-                companyService.asyncRecordBalanceLog(param.getCompanyId(), money, 15, newMoney, "发放红包");
+                companyService.asyncRecordBalanceLog(param.getCompanyId(), money, 15, newMoney, "发放红包",redPacketLog.getLogId());
 //            redisCache.setCacheObject("h5user:redPacket:"+param.getUserId(),LocalDateTime.now().toString());
 //            redisCache.setCacheObject("h5user:redPacket:"+param.getUserId(),LocalDateTime.now().toString());
 
 
                 return sendRedPacket;
                 return sendRedPacket;
@@ -2041,11 +2060,6 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         packetParam.setOpenId(user.getMpOpenId());
         packetParam.setOpenId(user.getMpOpenId());
         // 来源是小程序切换openId
         // 来源是小程序切换openId
         if (param.getSource() == 2) {
         if (param.getSource() == 2) {
-            //处理多小程序问题
-//            Company company = companyMapper.selectCompanyById(param.getCompanyId());
-//            if (company.getCourseMiniAppId()==null){
-//                return R.error("销售公司参数错误,未绑定小程序");
-//            }
             if (user.getMpOpenId() != null && !isNewWxMerchant) {
             if (user.getMpOpenId() != null && !isNewWxMerchant) {
                 packetParam.setOpenId(user.getMpOpenId());
                 packetParam.setOpenId(user.getMpOpenId());
             } else {
             } else {
@@ -2193,7 +2207,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
 
 
                     // 异步登记余额扣减日志
                     // 异步登记余额扣减日志
                     BigDecimal money = amount.multiply(BigDecimal.valueOf(-1));
                     BigDecimal money = amount.multiply(BigDecimal.valueOf(-1));
-                    companyService.asyncRecordBalanceLog(param.getCompanyId(), money, 15, newMoney, "发放红包");
+                    companyService.asyncRecordBalanceLog(param.getCompanyId(), money, 15, newMoney, "发放红包", redPacketLog.getLogId());
 
 
                     return sendRedPacket;
                     return sendRedPacket;
 
 
@@ -2212,48 +2226,48 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                     return R.error("服务商余额不足,请联系群主服务器充值!");
                     return R.error("服务商余额不足,请联系群主服务器充值!");
                 }
                 }
 
 
-             try{
-                 // 发送红包
-                 R sendRedPacket = paymentService.sendRedPacket(packetParam);
-                 if (sendRedPacket.get("code").equals(200)) {
-                     FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();
-                     TransferBillsResult transferBillsResult;
-                     if (sendRedPacket.get("isNew").equals(1)) {
-                         transferBillsResult = (TransferBillsResult) sendRedPacket.get("data");
-                         redPacketLog.setResult(JSON.toJSONString(sendRedPacket));
-                         redPacketLog.setOutBatchNo(transferBillsResult.getOutBillNo());
-                         redPacketLog.setBatchId(transferBillsResult.getTransferBillNo());
-                     } else {
-                         redPacketLog.setOutBatchNo(sendRedPacket.get("orderCode").toString());
-                         redPacketLog.setBatchId(sendRedPacket.get("batchId").toString());
-                     }
-                     // 添加红包记录
-                     redPacketLog.setCourseId(param.getCourseId());
-                     redPacketLog.setCompanyId(param.getCompanyId());
-                     redPacketLog.setUserId(param.getUserId());
-                     redPacketLog.setVideoId(param.getVideoId());
-                     redPacketLog.setStatus(0);
-                     redPacketLog.setQwUserId(param.getQwUserId() != null ? param.getQwUserId() : null);
-                     redPacketLog.setCompanyUserId(param.getCompanyUserId());
-                     redPacketLog.setCreateTime(new Date());
-                     redPacketLog.setAmount(amount);
-                     redPacketLog.setWatchLogId(log.getLogId() != null ? log.getLogId() : null);
-                     redPacketLog.setPeriodId(param.getPeriodId());
-                     redPacketLog.setAppId(param.getAppId());
-
-                     redPacketLogMapper.insertFsCourseRedPacketLog(redPacketLog);
-
-                     // 更新观看记录的奖励类型
-                     log.setRewardType(config.getRewardType());
-                     courseWatchLogMapper.updateFsCourseWatchLog(log);
-
-                     return sendRedPacket;
-                 } else {
-                     return R.error("奖励发送失败,请联系客服");
-                 }
-             }catch (Exception e){
-                 return R.error("发放奖励失败,请联系客服");
-             }
+                try{
+                    // 发送红包
+                    R sendRedPacket = paymentService.sendRedPacket(packetParam);
+                    if (sendRedPacket.get("code").equals(200)) {
+                        FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();
+                        TransferBillsResult transferBillsResult;
+                        if (sendRedPacket.get("isNew").equals(1)) {
+                            transferBillsResult = (TransferBillsResult) sendRedPacket.get("data");
+                            redPacketLog.setResult(JSON.toJSONString(sendRedPacket));
+                            redPacketLog.setOutBatchNo(transferBillsResult.getOutBillNo());
+                            redPacketLog.setBatchId(transferBillsResult.getTransferBillNo());
+                        } else {
+                            redPacketLog.setOutBatchNo(sendRedPacket.get("orderCode").toString());
+                            redPacketLog.setBatchId(sendRedPacket.get("batchId").toString());
+                        }
+                        // 添加红包记录
+                        redPacketLog.setCourseId(param.getCourseId());
+                        redPacketLog.setCompanyId(param.getCompanyId());
+                        redPacketLog.setUserId(param.getUserId());
+                        redPacketLog.setVideoId(param.getVideoId());
+                        redPacketLog.setStatus(0);
+                        redPacketLog.setQwUserId(param.getQwUserId() != null ? param.getQwUserId() : null);
+                        redPacketLog.setCompanyUserId(param.getCompanyUserId());
+                        redPacketLog.setCreateTime(new Date());
+                        redPacketLog.setAmount(amount);
+                        redPacketLog.setWatchLogId(log.getLogId() != null ? log.getLogId() : null);
+                        redPacketLog.setPeriodId(param.getPeriodId());
+                        redPacketLog.setAppId(param.getAppId());
+
+                        redPacketLogMapper.insertFsCourseRedPacketLog(redPacketLog);
+
+                        // 更新观看记录的奖励类型
+                        log.setRewardType(config.getRewardType());
+                        courseWatchLogMapper.updateFsCourseWatchLog(log);
+
+                        return sendRedPacket;
+                    } else {
+                        return R.error("奖励发送失败,请联系客服");
+                    }
+                }catch (Exception e){
+                    return R.error(e.getMessage());
+                }
 
 
             }
             }
         } else {
         } else {
@@ -2394,31 +2408,49 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
 
 
     /**
     /**
      * 获取用户openId
      * 获取用户openId
-     *
-     * @param userId    用户ID
-     * @param companyId 公司ID
-     * @param source    来源 1公众号 2小程序
-     * @return openId
      */
      */
-    private String getOpenId(Long userId, Long companyId, Integer source) {
-        Company company = companyMapper.selectCompanyById(companyId);
-        String appId = source == 1 ? company.getCourseMaAppId() : company.getCourseMiniAppId();
+    private String getOpenId(FsCourseSendRewardUParam param, FsUser user) {
+        Integer source = param.getSource();
+        Long userId = param.getUserId();
+        switch (source) {
+            case 1:
+                Company company = companyMapper.selectCompanyById(param.getCompanyId());
+                String appId = company.getCourseMaAppId();
 
 
-        // 公司配置为空时获取默认配置
-        if (StringUtils.isBlank(appId)) {
-            String json = configService.selectConfigByKey("course.config");
-            CourseConfig config = JSON.parseObject(json, CourseConfig.class);
-            appId = source == 1 ? config.getMpAppId() : config.getMiniprogramAppid();
-        }
+                // 公司配置为空时获取默认配置
+                if (StringUtils.isBlank(appId)) {
+                    String json = configService.selectConfigByKey("course.config");
+                    CourseConfig config = JSON.parseObject(json, CourseConfig.class);
+                    appId = config.getMpAppId();
+                }
 
 
-        // 查询openId
-        Wrapper<FsUserWx> queryWrapper = Wrappers.<FsUserWx>lambdaQuery().eq(FsUserWx::getFsUserId, userId).eq(FsUserWx::getAppId, appId);
-        FsUserWx fsUserWx = fsUserWxService.getOne(queryWrapper);
-        if (Objects.isNull(fsUserWx)) {
-            throw new CustomException("获取openId失败");
-        }
+                // 查询openId
+                Wrapper<FsUserWx> queryWrapper = Wrappers.<FsUserWx>lambdaQuery().eq(FsUserWx::getFsUserId, userId).eq(FsUserWx::getAppId, appId);
+                FsUserWx fsUserWx = fsUserWxService.getOne(queryWrapper);
+                if (Objects.isNull(fsUserWx)) {
+                    throw new CustomException("获取openId失败");
+                }
 
 
-        return fsUserWx.getOpenId();
+                return fsUserWx.getOpenId();
+            case 2:
+                FsUserWx userWx = fsUserWxService.selectByAppIdAndUserId(param.getAppId(),userId,1);
+                if (Objects.nonNull(userWx) && StringUtils.isNotBlank(userWx.getOpenId())) {
+                    return userWx.getOpenId();
+                }
+
+                if (StringUtils.isNotBlank(user.getCourseMaOpenId())) {
+                    try {
+                        handleFsUserWx(user,param.getAppId());
+                    } catch (Exception e){
+                        log.error("【更新或插入用户与小程序的绑定关系失败】:{}", userId, e);
+                    }
+                    return user.getCourseMaOpenId();
+                }
+                break;
+            case 3:
+                return user.getAppOpenId();
+        }
+        return null;
     }
     }
 
 
     /**
     /**
@@ -2434,6 +2466,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         FsUser userMap = new FsUser();
         FsUser userMap = new FsUser();
         userMap.setUserId(user.getUserId());
         userMap.setUserId(user.getUserId());
         userMap.setIntegral(user.getIntegral() + config.getAnswerIntegral());
         userMap.setIntegral(user.getIntegral() + config.getAnswerIntegral());
+        userMap.setWithdrawIntegral(user.getWithdrawIntegral() + config.getAnswerIntegral());
         fsUserMapper.updateFsUser(userMap);
         fsUserMapper.updateFsUser(userMap);
 
 
         // 记录积分日志
         // 记录积分日志
@@ -4853,6 +4886,44 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         }
         }
     }
     }
 
 
+    /**
+     * 用户提现
+     * @param param
+     */
+    @Override
+    @Transactional
+    public R withdrawal(FsCourseSendRewardUParam param) {
+        Long userId = param.getUserId();
+        // 生成锁的key,基于用户ID和视频ID确保同一用户同一视频的请求被锁定
+        String lockKey = "reward_red_lock:user:" + userId;
+        RLock lock = redissonClient.getLock(lockKey);
+
+        try {
+            // 尝试获取锁,等待时间5秒,锁过期时间30秒
+            boolean isLocked = lock.tryLock(5, 300, TimeUnit.SECONDS);
+            if (!isLocked) {
+                logger.warn("获取锁失败,用户ID:{}", userId);
+                return R.error("操作频繁,请稍后再试!");
+            }
+
+            logger.info("成功获取锁,开始处理奖励发放,用户ID:{}", userId);
+            return executeWithdrawal(param);
+
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            logger.error("获取锁被中断,用户ID:{}", userId, e);
+            return R.error("系统繁忙,请重试!");
+        } finally {
+            // 释放锁
+            if (lock.isHeldByCurrentThread()) {
+                lock.unlock();
+                logger.info("释放锁成功,用户ID:{}", userId);
+            }
+        }
+
+
+    }
+
 
 
     public void uploadSingleTaskWithRetry(FsVideoResource videoResource,Integer type) {
     public void uploadSingleTaskWithRetry(FsVideoResource videoResource,Integer type) {
         int maxRetry = 3;
         int maxRetry = 3;
@@ -5037,6 +5108,39 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         return R.ok("奖励发放成功");
         return R.ok("奖励发放成功");
     }
     }
 
 
+    /**
+     * 发送优惠券
+     */
+    private R sendCouponNew(FsCourseSendRewardUParam param, String couponId, Integer num) {
+        log.debug("发送优惠券 param: {}, couponId: {}, num: {}", JSON.toJSONString(param), couponId, num);
+
+        FsCoupon coupon = fsCouponMapper.selectFsCouponByCouponId(Long.parseLong(couponId));
+        //不存在
+        if (coupon == null) {
+            return R.error("优惠券不存在");
+        }
+        //停用
+        if (coupon.getStatus()==0) {
+            return R.error("优惠券不存在");
+        }
+
+        FsUserCoupon fsUserCoupon = new FsUserCoupon();
+        fsUserCoupon.setCouponId(coupon.getCouponId());
+        fsUserCoupon.setCouponCode("C"+System.currentTimeMillis());
+        fsUserCoupon.setUserId(param.getUserId());
+        fsUserCoupon.setCreateTime(DateUtils.getNowDate());
+        if (coupon.getLimitType() == 2){
+            long limitDay = coupon.getLimitDay().longValue() * 24 * 60 * 60 * 1000;
+            long time = new Date().getTime();
+            fsUserCoupon.setLimitTime(new Date(limitDay+time));
+        }else {
+            fsUserCoupon.setLimitTime(coupon.getLimitTime());
+        }
+        fsUserCoupon.setStatus(0);
+        fsUserCouponMapper.insertFsUserCoupon(fsUserCoupon);
+        return R.ok("奖励发放成功");
+    }
+
     /**
     /**
      * 发送优惠券
      * 发送优惠券
      */
      */
@@ -5070,5 +5174,497 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         return R.ok("奖励发放成功");
         return R.ok("奖励发放成功");
     }
     }
 
 
+    private R executeWithdrawal(FsCourseSendRewardUParam param){
+        log.info("进入用户判断");
+        FsUser user = fsUserMapper.selectFsUserByUserId(param.getUserId());
+        if (user == null) {
+            return R.error("未识别到用户信息");
+        }
+        FsCourseWatchLog log = null;
+        if (param.getWatchLogId() != null){
+            //企微自动发课
+            log = courseWatchLogMapper.selectFsCourseWatchLogByLogId(param.getWatchLogId());
+        } else {
+            log = courseWatchLogMapper.getWatchCourseVideoByFsUser(param.getUserId(), param.getVideoId(), param.getCompanyUserId());
+        }
+        if (log == null) {
+            return R.error("无记录");
+        }
+
+        if (log.getLogType() != 2) {
+            return R.error("未完课");
+        }
+
+        FsCourseAnswerLogs rightLog = courseAnswerLogsMapper.selectRightLogByCourseVideo(param.getVideoId(), param.getUserId(), param.getQwUserId());
+        if (rightLog == null && !CloudHostUtils.hasCloudHostName("金牛明医")) {
+            logger.error("未答题:{}", param.getUserId());
+            return R.error("未答题");
+        }
+
+        FsCourseRedPacketLog fsCourseRedPacketLog = redPacketLogMapper.selectUserFsCourseRedPacketLog(param.getVideoId(), param.getUserId(), param.getPeriodId());
+
+        if (log.getRewardType() != null) {
+            if (log.getRewardType() == 1) {
+                if (fsCourseRedPacketLog != null && fsCourseRedPacketLog.getStatus() == 1) {
+                    return R.error("已领取该课程奖励,不可重复领取!");
+                }
+                if (fsCourseRedPacketLog != null && fsCourseRedPacketLog.getStatus() == 0) {
+                    if (StringUtils.isNotEmpty(fsCourseRedPacketLog.getResult())) {
+                        R r = JSON.parseObject(fsCourseRedPacketLog.getResult(), R.class);
+                        return r;
+                    } else {
+                        return R.error("操作频繁,请稍后再试!");
+                    }
+                }
+            } else if (log.getRewardType() == 2) {
+                return R.error("已领取该课程奖励,不可重复领取!");
+            }
+        }
+
+        // 获取视频信息
+        FsUserCourseVideo video = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(param.getVideoId());
+
+        // 获取配置信息
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+
+        // 判断来源是否是app,如是app,则发放积分奖励
+//        int sourceApp = 3;
+//        if (sourceApp == param.getSource() /*&& !CloudHostUtils.hasCloudHostName("中康")*/) {
+//            return sendIntegralReward(param, user, log, config);
+//        }
+        if (ObjectUtils.isEmpty(param.getRewardType())){
+            param.setRewardType(config.getRewardType());
+        }
+        // 根据奖励类型发放不同奖励
+        switch (param.getRewardType()) {
+            // 红包奖励
+            case 1:
+                //来源是小程序切换openId
+                WxSendRedPacketParam packetParam = new WxSendRedPacketParam();
+                String openId = getOpenId(param, user);
+                if (StringUtils.isBlank(openId)) {
+                    return R.error("请重新使用微信登录");
+                }
+                packetParam.setOpenId(openId);
+                BeanUtils.copyProperties(param, packetParam);
+
+                return sendAppRedPacket(packetParam, log,video, config);
+            // 积分奖励
+            case 2:
+                return sendIntegralReward(param, user, log, config);
+            // 转盘
+            case 3:
+                return drawTurntable(param, user, log);
+            // 保底转盘
+            case 4:
+                return drawTurntableGuarantee(param, user, log);
+            default:
+                return R.error("参数错误!");
+        }
+    }
+
+    private R sendAppRedPacket(WxSendRedPacketParam packetParam,FsCourseWatchLog log,FsUserCourseVideo video,CourseConfig config) {
+        FsUserCoursePeriodDays periodDays = new FsUserCoursePeriodDays();
+        periodDays.setVideoId(log.getVideoId());
+        periodDays.setPeriodId(log.getPeriodId());
+        //正常情况是只能查询到一条,之前可能存在重复的脏数据,暂使用查询list的方式
+        List<FsUserCoursePeriodDays> fsUserCoursePeriodDays = fsUserCoursePeriodDaysMapper.selectFsUserCoursePeriodDaysList(periodDays);
+        if (fsUserCoursePeriodDays != null && !fsUserCoursePeriodDays.isEmpty()) {
+            periodDays = fsUserCoursePeriodDays.get(0);
+        }
+        if (periodDays != null && periodDays.getLastJoinTime() != null && LocalDateTime.now().isAfter(periodDays.getLastJoinTime())) {
+            return R.error(403, "已超过领取红包时间");
+        }
+
+
+        // 确定红包金额
+        BigDecimal amount = BigDecimal.ZERO;
+        FsUserCourseVideoRedPackage redPackage = fsUserCourseVideoRedPackageMapper.selectRedPacketByCompanyId(log.getVideoId(), log.getCompanyId(), log.getPeriodId());
+
+        if (redPackage != null && redPackage.getRedPacketMoney() != null) {
+            amount = redPackage.getRedPacketMoney();
+        } else if (video != null && video.getRedPacketMoney() != null) {
+            amount = video.getRedPacketMoney();
+        }
+        packetParam.setAmount(amount);
+
+        if (amount.compareTo(BigDecimal.ZERO) > 0) {
+
+            // 打开红包扣减功能
+            if ("1".equals(config.getIsRedPackageBalanceDeduction())) {
+                // 先注释 20251024 redis 余额 充值没有考虑 其余扣减没有考虑
+                // ===================== 20251022 xgb 修改 本次修改目的为了实时扣减公司余额=====================
+                // 1 使用redis缓存加锁 预扣减余额 红包发送失败 恢复redis缓存余额,如果回滚失败登记异常记录表 定时任务重新回滚余额
+                // 2 另起定时任务 同步缓存余额到redis中
+                // 3 注意!!!!! 启动系统时查询公司账户余额(这个时候要保证余额正确)启动会自动保存到redis缓存中
+                // 注意!!!!! 打开这个开关前记得检测redis缓存余额是否正确 若不正确 修改数据库字段red_package_money,删除redis缓存,重启系统,
+
+
+                // 预设值异常对象
+
+                BalanceRollbackError balanceRollbackError = new BalanceRollbackError();
+                balanceRollbackError.setCompanyId(packetParam.getCompanyId());
+                balanceRollbackError.setUserId(log.getUserId());
+                balanceRollbackError.setLogId(log.getLogId());
+                balanceRollbackError.setVideoId(log.getVideoId());
+                balanceRollbackError.setStatus(0);
+                balanceRollbackError.setMoney(amount);
+
+                if (packetParam.getCompanyId() == null) {
+                    logger.error("发送红包参数错误,公司不能为空,异常请求参数{}", packetParam);
+                    return R.error("发送红包失败,请联系管理员");
+                }
+                String companyMoneyKey = FsConstants.COMPANY_MONEY_KEY + packetParam.getCompanyId();
+
+                // 第一次加锁:预扣减余额
+                RLock lock1 = redissonClient.getLock(FsConstants.COMPANY_MONEY_LOCK + packetParam.getCompanyId());
+                boolean lockAcquired = false;
+                BigDecimal newMoney;
+                try {
+                    if (lock1.tryLock(3, 10, TimeUnit.SECONDS)) {
+                        lockAcquired = true;
+                        BigDecimal originalMoney;
+                        // 获取当前余额
+                        String moneyStr = redisCache.getCacheObject(companyMoneyKey);
+                        if (StringUtils.isNotEmpty(moneyStr)) {
+                            originalMoney = new BigDecimal(moneyStr);
+                        } else {
+                            // 缓存没有值,重启系统恢复redis数据 保证数据正确性
+                            logger.error("发送红包获取redis余额缓存异常,异常请求参数{}", packetParam);
+                            return R.error("系统异常,请稍后重试");
+                        }
+
+                        if (originalMoney.compareTo(BigDecimal.ZERO) < 0) {
+                            logger.error("服务商余额不足,异常请求参数{}", packetParam);
+                            return R.error("服务商余额不足,请联系群主服务器充值!");
+                        }
+
+                        // 预扣减金额
+                        newMoney = originalMoney.subtract(amount);
+                        redisCache.setCacheObject(companyMoneyKey, newMoney.toString());
+                    } else {
+                        logger.error("获取redis锁失败,异常请求参数{}", packetParam);
+                        return R.error("系统繁忙,请稍后重试");
+                    }
+                } catch (Exception e) {
+                    logger.error("预扣减余额失败: 异常请求参数{},异常信息{}", packetParam, e.getMessage(), e);
+                    return R.error("系统异常,请稍后重试");
+                } finally {
+                    // 只有在成功获取锁的情况下才释放锁
+                    if (lockAcquired && lock1.isHeldByCurrentThread()) {
+                        try {
+                            lock1.unlock();
+                        } catch (IllegalMonitorStateException e) {
+                            logger.warn("尝试释放非当前线程持有的锁: companyId={}", packetParam.getCompanyId());
+                        }
+                    }
+                }
+
+
+                // 调用第三方接口(锁外操作)
+                R sendRedPacket;
+                try {
+                    sendRedPacket = paymentService.sendAppRedPacket(packetParam);
+                } catch (Exception e) {
+                    logger.error("红包发送异常: 异常请求参数{}", packetParam, e);
+                    // 异常时回滚余额
+
+                    rollbackBalance(balanceRollbackError);
+                    return R.error("奖励发送失败,请联系客服");
+                }
+
+                // 红包发送成功处理
+                if (sendRedPacket.get("code").equals(200)) {
+                    FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();
+                    TransferBillsResult transferBillsResult;
+                    if (sendRedPacket.get("isNew").equals(1)) {
+                        transferBillsResult = (TransferBillsResult) sendRedPacket.get("data");
+                        redPacketLog.setResult(JSON.toJSONString(sendRedPacket));
+                        redPacketLog.setOutBatchNo(transferBillsResult.getOutBillNo());
+                        redPacketLog.setBatchId(transferBillsResult.getTransferBillNo());
+                    } else {
+                        redPacketLog.setOutBatchNo(sendRedPacket.get("orderCode").toString());
+                        redPacketLog.setBatchId(sendRedPacket.get("batchId").toString());
+                    }
+                    // 添加红包记录
+                    redPacketLog.setCourseId(log.getCourseId());
+                    redPacketLog.setCompanyId(log.getCompanyId());
+                    redPacketLog.setUserId(log.getUserId());
+                    redPacketLog.setVideoId(log.getVideoId());
+                    redPacketLog.setStatus(0);
+                    redPacketLog.setQwUserId(log.getQwUserId() != null ? log.getQwUserId().toString() : null);
+                    redPacketLog.setCompanyUserId(log.getCompanyUserId());
+                    redPacketLog.setCreateTime(new Date());
+                    redPacketLog.setAmount(amount);
+                    redPacketLog.setWatchLogId(log.getLogId() != null ? log.getLogId() : null);
+                    redPacketLog.setPeriodId(log.getPeriodId());
+                    redPacketLog.setAppId(packetParam.getAppId());
+
+                    redPacketLogMapper.insertFsCourseRedPacketLog(redPacketLog);
+
+                    // 更新观看记录的奖励类型
+                    log.setRewardType(config.getRewardType());
+                    courseWatchLogMapper.updateFsCourseWatchLog(log);
+
+                    // 异步登记余额扣减日志
+                    BigDecimal money = amount.multiply(BigDecimal.valueOf(-1));
+                    companyService.asyncRecordBalanceLog(log.getCompanyId(), money, 15, newMoney, "发放红包", redPacketLog.getLogId());
+
+                    return sendRedPacket;
+
+
+                } else {
+                    // 发送失败,回滚余额
+                    rollbackBalance(balanceRollbackError);
+                    return R.error("奖励发送失败,请联系客服");
+                }
+
+                // ===================== 本次修改目的为了实时扣减公司余额=====================
+            } else {
+                Company company = companyMapper.selectCompanyById(log.getCompanyId());
+                BigDecimal money = company.getMoney();
+                if (money.compareTo(BigDecimal.ZERO) <= 0) {
+                    return R.error("服务商余额不足,请联系群主服务器充值!");
+                }
+
+                try{
+                    // 发送红包
+                    R sendRedPacket = paymentService.sendAppRedPacket(packetParam);
+                    if (sendRedPacket.get("code").equals(200)) {
+                        FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();
+                        TransferBillsResult transferBillsResult;
+                        if (sendRedPacket.get("isNew").equals(1)) {
+                            transferBillsResult = (TransferBillsResult) sendRedPacket.get("data");
+                            redPacketLog.setResult(JSON.toJSONString(sendRedPacket));
+                            redPacketLog.setOutBatchNo(transferBillsResult.getOutBillNo());
+                            redPacketLog.setBatchId(transferBillsResult.getTransferBillNo());
+                        } else {
+                            redPacketLog.setOutBatchNo(sendRedPacket.get("orderCode").toString());
+                            redPacketLog.setBatchId(sendRedPacket.get("batchId").toString());
+                        }
+                        // 添加红包记录
+                        redPacketLog.setCourseId(log.getCourseId());
+                        redPacketLog.setCompanyId(log.getCompanyId());
+                        redPacketLog.setUserId(log.getUserId());
+                        redPacketLog.setVideoId(log.getVideoId());
+                        redPacketLog.setStatus(0);
+                        redPacketLog.setQwUserId(log.getQwUserId() != null ? log.getQwUserId().toString() : null);
+                        redPacketLog.setCompanyUserId(log.getCompanyUserId());
+                        redPacketLog.setCreateTime(new Date());
+                        redPacketLog.setAmount(amount);
+                        redPacketLog.setWatchLogId(log.getLogId() != null ? log.getLogId() : null);
+                        redPacketLog.setPeriodId(log.getPeriodId());
+                        redPacketLog.setAppId( packetParam.getAppId());
+
+                        redPacketLogMapper.insertFsCourseRedPacketLog(redPacketLog);
+
+                        // 更新观看记录的奖励类型
+                        log.setRewardType(config.getRewardType());
+                        courseWatchLogMapper.updateFsCourseWatchLog(log);
+
+                        return sendRedPacket;
+                    } else {
+                        return R.error("奖励发送失败,请联系客服");
+                    }
+                }catch (Exception e){
+                    return R.error(e.getMessage());
+                }
+
+            }
+        } else {
+            FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();
+            // 添加红包记录
+            redPacketLog.setCourseId(log.getCourseId());
+//            redPacketLog.setOutBatchNo(sendRedPacket.get("orderCode").toString());
+            redPacketLog.setCompanyId(log.getCompanyId());
+            redPacketLog.setUserId(log.getUserId());
+            redPacketLog.setVideoId(log.getVideoId());
+            redPacketLog.setStatus(1);
+            redPacketLog.setQwUserId(log.getQwUserId() != null ? log.getQwUserId().toString() : null);
+            redPacketLog.setCompanyUserId(log.getCompanyUserId());
+            redPacketLog.setCreateTime(new Date());
+            redPacketLog.setAmount(BigDecimal.ZERO);
+            redPacketLog.setWatchLogId(log.getLogId() != null ? log.getLogId() : null);
+            redPacketLog.setPeriodId(log.getPeriodId());
+            redPacketLog.setAppId( packetParam.getAppId());
+            redPacketLogMapper.insertFsCourseRedPacketLog(redPacketLog);
+
+            // 更新观看记录的奖励类
+            log.setRewardType(config.getRewardType());
+            courseWatchLogMapper.updateFsCourseWatchLog(log);
+            return R.ok("答题成功!");
+        }
+    }
+
+    private R drawTurntable(FsCourseSendRewardUParam param, FsUser user, FsCourseWatchLog watchLog) {
+        log.debug("转盘抽奖 param: {}, user: {}, watchLog: {}",
+                JSON.toJSONString(param),JSON.toJSONString(user),JSON.toJSONString(watchLog));
+        return draw(param, user, watchLog, 4);
+    }
+
+    /**
+     * 抽奖
+     */
+    private R draw(FsCourseSendRewardUParam param, FsUser user, FsCourseWatchLog watchLog, Integer drawType) {
+        FsCourseReward rewardConfig = videoRelationMapper.getRewardByCompanyIdAndVideoIdAndType(watchLog.getCompanyId(), watchLog.getVideoId(), drawType);
+        String typeName = drawType == 4 ? "转盘" : "保底转盘";
+        if (Objects.isNull(rewardConfig)) {
+            throw new CustomException("销售公司未配置"+ typeName +",请选择其他奖励类型");
+        }
+
+        if (StringUtils.isBlank(rewardConfig.getActualRewards())) {
+            throw new CustomException(typeName + "配置错误1,请联系管理员");
+        }
+
+        // 解析JSON配置
+        JSONArray jsonArray;
+        try {
+            jsonArray = JSON.parseArray(rewardConfig.getActualRewards());
+        } catch (Exception e) {
+            log.error("解析{}配置JSON失败", typeName, e);
+            throw new CustomException(typeName + "配置错误:JSON格式不正确");
+        }
+
+        if (jsonArray == null || jsonArray.isEmpty()) {
+            throw new CustomException(typeName + "配置错误:奖品列表为空");
+        }
+
+        // 配置关系
+        FsCourseRewardVideoRelation relation = videoRelationMapper.selectByCompanyIdAndVideoIdAndRewardId(watchLog.getCompanyId(), watchLog.getVideoId(), rewardConfig.getId());
+
+        List<Prize> prizes = buildPrizesNew(jsonArray, param, rewardConfig.getId(), drawType);
+        Prize prize = LotteryUtil.draw(prizes);
+        if (Objects.isNull(prize)) {
+            throw new CustomException(typeName + "无可抽取奖励");
+        }
+
+        FsCourseRewardRound rewardRound = new FsCourseRewardRound();
+        rewardRound.setRewardId(rewardConfig.getId());
+        rewardRound.setUserId(user.getUserId());
+        rewardRound.setRewardType(rewardConfig.getRewardType());
+        rewardRound.setCompanyId(watchLog.getCompanyId());
+        rewardRound.setActualRewards(prize.getAmount());
+        rewardRound.setCreateTime(new Date());
+        rewardRound.setStatus(1L);
+        rewardRound.setWatchId(watchLog.getLogId());
+        rewardRound.setRuleId(prize.getCode());
+        rewardRound.setRewardVideoRelationId(relation.getId());
+
+        // 抽中奖励商品
+        if (prize.getType() == 5) {
+            rewardRound.setGoodsId(Long.parseLong(prize.getGoodsId()));
+        }
+
+        roundMapper.insertFsCourseRewardRound(rewardRound);
+
+        // 获取配置信息
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        FsUserCourseVideo video = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(param.getVideoId());
+        switch (prize.getType()) {
+            case 1:
+                //来源是小程序切换openId
+                WxSendRedPacketParam packetParam = new WxSendRedPacketParam();
+                String openId = getOpenId(param, user);
+                if (StringUtils.isBlank(openId)) {
+                    return R.error("请重新使用微信登录");
+                }
+                packetParam.setOpenId(openId);
+                packetParam.setAmount(new BigDecimal(prize.getAmount()));
+                packetParam.setSource(param.getSource());
+                packetParam.setAppId(param.getAppId());
+                return sendAppRedPacket(packetParam, watchLog,video, config);
+            case 2:
+                return sendIntegralReward(param, user, watchLog, config);
+            case 3:
+                return R.ok().put("data", prize.getCode());
+            case 4:
+                return sendCouponNew(param, prize.getCouponId(), Integer.parseInt(prize.getAmount())).put("data", prize.getCode());
+            case 5:
+                Map<String, Object> result = new HashMap<>();
+                result.put("roundId", rewardRound.getId());
+                result.put("goodsId", prize.getGoodsId());
+                result.put("data", prize.getCode());
+                return R.ok(result);
+            default:
+                return R.error(typeName + "配置错误4,请联系管理员");
+        }
+    }
+
+    /**
+     * 构建奖品列表
+     */
+    private List<Prize> buildPrizesNew(JSONArray jsonArray, FsCourseSendRewardUParam param, Long rewardId, Integer drawType) {
+        List<Prize> prizes = new ArrayList<>();
+
+        // 如果是保底转盘,需要查询已领取记录
+        List<FsCourseRewardRound> rounds = new ArrayList<>();
+        if (drawType == 5) { // 保底转盘
+            FsCourseRewardRound query = new FsCourseRewardRound();
+            query.setUserId(param.getUserId());
+            query.setCompanyId(param.getCompanyId());
+            query.setRewardId(rewardId);
+            rounds = roundMapper.selectFsCourseRewardRoundList(query);
+        }
+
+        Set<String> claimedCodes = rounds.stream()
+                .map(FsCourseRewardRound::getRuleId)
+                .collect(Collectors.toSet());
+
+        for (Object o : jsonArray) {
+            try {
+                JSONObject json = (JSONObject) o;
+
+                Integer type = json.getInteger("type");
+                String amount = json.getString("amount");
+                String code = json.getString("code");
+                String couponId = json.getString("couponId");
+
+                // 安全解析概率
+                double probability = parseProbability(json.getString("probability"));
+                if (probability <= 0) {
+                    log.warn("奖品概率配置错误,跳过: {}", json);
+                    continue;
+                }
+
+                // 保底转盘特殊逻辑
+                if (drawType == 5) {
+                    // 跳过已领取的
+                    if (claimedCodes.contains(code)) {
+                        continue;
+                    }
+
+                    // 保底奖品逻辑
+                    Boolean isGuarantee = json.getBoolean("isGuarantee");
+                    if (Boolean.TRUE.equals(isGuarantee) && jsonArray.size() - rounds.size() > 5) {
+                        continue;
+                    }
+                }
+
+                // APP跳过现金红包
+                if (param.getSource() == 3 && type == 1) {
+                    continue;
+                }
+
+                prizes.add(new Prize(type, amount, code, probability, couponId, null,null));
+
+            } catch (Exception e) {
+                log.warn("解析奖品配置失败,跳过: {}", JSON.toJSONString(o), e);
+            }
+        }
+
+        return prizes;
+    }
+
+    /**
+     * 保底转盘
+     */
+    private R drawTurntableGuarantee(FsCourseSendRewardUParam param, FsUser user, FsCourseWatchLog watchLog) {
+        log.debug("保底转盘 param: {}, user: {}, watchLog: {}",
+                JSON.toJSONString(param),JSON.toJSONString(user),JSON.toJSONString(watchLog));
+        return draw(param, user, watchLog, 5);
+    }
+
 }
 }
 
 

+ 64 - 0
fs-service/src/main/java/com/fs/his/config/AppConfig.java

@@ -4,6 +4,7 @@ import com.fs.course.vo.FsUserCourseVideoVO;
 import com.fs.his.domain.FsPackage;
 import com.fs.his.domain.FsPackage;
 import lombok.Data;
 import lombok.Data;
 
 
+import java.math.BigDecimal;
 import java.util.List;
 import java.util.List;
 
 
 @Data
 @Data
@@ -18,4 +19,67 @@ public class AppConfig {
     private String corpUrl; //APP客服配置 企业主体链接
     private String corpUrl; //APP客服配置 企业主体链接
     private Integer addIntegral; //玩一局游戏加多少积分
     private Integer addIntegral; //玩一局游戏加多少积分
     private Integer defaultRewardGold; //看视频获取多少金币
     private Integer defaultRewardGold; //看视频获取多少金币
+
+    private Integer isOpenWithdraw;//是否开启提现按钮 0关闭 1开启
+
+    private Integer withdrawRatio;//兑换佣金比例 1元=多少积分
+    private List<Integer> integralTypes;//允许兑换积分类型
+
+    private Integer isNew;//0:老商户 商家转账到零钱 1:新商户 商家转账
+
+    /**
+     * 商户号.
+     */
+    private String mchId;
+    /**
+     * 商户密钥.
+     */
+    private String mchKey;
+
+    /**
+     * p12证书文件的绝对路径或者以classpath:开头的类路径.
+     */
+    private String keyPath;
+
+    /**
+     * apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径.
+     */
+    private String privateKeyPath;
+
+    /**
+     * apiclient_cert.pem证书文件的绝对路径或者以classpath:开头的类路径.
+     */
+    private String privateCertPath;
+
+    /**
+     * apiV3 秘钥值.
+     */
+    private String apiV3Key;
+    /**
+     * 公钥ID
+     */
+    private String publicKeyId;
+
+    /**
+     * pub_key.pem证书文件的绝对路径或者以classpath:开头的类路径.
+     */
+    private String publicKeyPath;
+
+    private String notifyUrl;
+
+    private String withdrawalNotifyUrl;
+
+
+    //一次允许提现最大金额(元)
+    private BigDecimal maxApplicationAmount;
+
+    //一天允提现次数
+    private Integer withdrawNum;
+
+    //连续提现几天封控
+    private Integer limitDayNum;
+
+    //连续提现几天封控
+    private BigDecimal limitAmount;
+
 }
 }

+ 120 - 0
fs-service/src/main/java/com/fs/his/config/AppPageConfig.java

@@ -0,0 +1,120 @@
+package com.fs.his.config;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class AppPageConfig implements Serializable {
+
+    /**
+     * 主页面配置
+     */
+    private MainPage mainPage;
+
+    /**
+     * 子页面配置
+     */
+    private SubPage subPage;
+
+    @Data
+    public static class MainPage {
+        /**
+         * 健康
+         */
+        private String health;
+
+        /**
+         * 精选
+         */
+        private String featured;
+
+        /**
+         * 优选
+         */
+        private String preferred;
+    }
+
+    @Data
+    public static class SubPage {
+
+        /**
+         * 健康模块子页面
+         */
+        private Health health;
+
+        /**
+         * 精选模块子页面
+         */
+        private Featured featured;
+
+        /**
+         * 优选模块子页面
+         */
+        private Preferred preferred;
+    }
+
+    @Data
+    public static class Health {
+        /**
+         * 医疗服务
+         */
+        private String medicalService;
+
+        /**
+         * 健康管理
+         */
+        private String healthManagement;
+
+        /**
+         * 名家讲堂
+         */
+        private String famousLecture;
+
+        /**
+         * 学习中心
+         */
+        private String learningCenter;
+    }
+
+    @Data
+    public static class Featured {
+        /**
+         * 短视频
+         */
+        private String shortVideo;
+        private String shortVideoH5Url;
+
+        /**
+         * 直播
+         */
+        private String live;
+        private String liveH5Url;
+
+        /**
+         * 短剧
+         */
+        private String shortDrama;
+        private String shortDramaH5Url;
+
+        /**
+         * 游戏
+         */
+        private String game;
+        private String gameH5Url;
+
+        /**
+         * 任务广场
+         */
+        private String taskSquare;
+        private String taskSquareH5Url;
+    }
+
+    @Data
+    public static class Preferred {
+        /**
+         * 现金 + 积分购买
+         */
+        private String cashAndPointsPurchase;
+    }
+}

+ 56 - 0
fs-service/src/main/java/com/fs/his/domain/FsConsecutiveWithdrawRecord.java

@@ -0,0 +1,56 @@
+package com.fs.his.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 连续提现记录对象 fs_consecutive_withdraw_record
+ *
+ * @author fs
+ * @date 2026-02-04
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class FsConsecutiveWithdrawRecord extends BaseEntity{
+
+    @TableId
+    private Long id;
+
+    /** 用户ID */
+    @Excel(name = "用户ID",sort = 1)
+    private Long userId;
+
+    /** 连续天数 */
+    @Excel(name = "连续天数")
+    private Integer consecutiveDays;
+
+    /** 连续开始日期 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "连续开始日期", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date startDate;
+
+    /** 连续结束日期 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "连续结束日期", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date endDate;
+
+    /** 连续期间提现次数 */
+    @Excel(name = "连续期间提现次数")
+    private Integer withdrawCount;
+
+    /** 连续期间提现总金额 */
+    @Excel(name = "连续期间提现总金额")
+    private BigDecimal totalAmount;
+
+    /** 状态:1-有效 0-无效 */
+    private Long status;
+
+
+}

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

@@ -4,13 +4,20 @@ import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
 import lombok.Data;
 import lombok.Data;
+import lombok.NoArgsConstructor;
 
 
 import java.math.BigDecimal;
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
 import java.time.LocalDateTime;
 
 
 @TableName("fs_integral_cart")
 @TableName("fs_integral_cart")
 @Data
 @Data
+@Builder
+@NoArgsConstructor      // 无参构造
+@AllArgsConstructor
 public class FsIntegralCart {
 public class FsIntegralCart {
     /**
     /**
      * 主键ID
      * 主键ID
@@ -51,4 +58,9 @@ public class FsIntegralCart {
      */
      */
     @TableField(exist = false)
     @TableField(exist = false)
     private Boolean addToExist;
     private Boolean addToExist;
+
+    @ApiModelProperty("是否选中:1是,0否")
+    @TableField("is_selected")
+    private Boolean isSelected;
+
 }
 }

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

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.annotation.TableName;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
 import com.fs.common.core.domain.BaseEntity;
+import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 import lombok.Data;
 
 
 import java.math.BigDecimal;
 import java.math.BigDecimal;
@@ -130,4 +131,14 @@ public class FsIntegralOrder
     private Integer deliveryStatus;
     private Integer deliveryStatus;
 
 
     private String deliveryType;
     private String deliveryType;
+
+
+    @ApiModelProperty("购物车中的商品信息(多个)")
+    private String itemCartJson;
+    @ApiModelProperty("购物车中的商品对应积分(多个)")
+    private String integralByCart;
+    @ApiModelProperty("购物车中对应的商品编码(多个)")
+    private String barCodeCart;
+    @ApiModelProperty("商品对应的数量(多个)")
+    private String quantityCart;
 }
 }

+ 69 - 0
fs-service/src/main/java/com/fs/his/domain/FsIntegralRedPacketLog.java

@@ -0,0 +1,69 @@
+package com.fs.his.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+/**
+ * 积分佣金红包记录对象 fs_integral_red_packet_log
+ *
+ * @author fs
+ * @date 2026-01-22
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class FsIntegralRedPacketLog extends BaseEntity{
+
+    /** 日志Id */
+    @TableId
+    private Long logId;
+
+    /** 用户id */
+    @Excel(name = "会员ID",sort = 1)
+    private Long userId;
+
+    /** 转帐金额 */
+    @Excel(name = "转帐金额")
+    private BigDecimal amount;
+
+    /** 批次单号 */
+    @Excel(name = "批次单号")
+    private String outBatchNo;
+
+    /** 微信批次单号 */
+    @Excel(name = "微信批次单号")
+    private String batchId;
+
+    /** 状态
+     * -3取消中
+     * -2取消
+     * -1失败
+     * 0 发送中
+     * 1 已发送
+     * 2 转账已受理
+     * 3 转账处理中
+     * 4 待收款用户确认,可拉起微信收款确认页面进行收款确认
+     * 5 转账结果尚未明确,可拉起微信收款确认页面再次重试确认收款
+     */
+    @Excel(name = "状态",dictType = "wx_merchant_pay_status")
+    private String status;
+
+    /** appId */
+    private String appId;
+
+    /** 唤起收款参数 */
+    private String packageInfo;
+
+    /** 商户号 */
+    private String mchId;
+
+    /** 佣金退回状态 0:否 1:是 */
+    @Excel(name = "退佣金状态  0:否 1:是")
+    private Integer returnedStatus;
+
+
+}

+ 50 - 0
fs-service/src/main/java/com/fs/his/domain/FsUserActiveLog.java

@@ -0,0 +1,50 @@
+package com.fs.his.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Date;
+
+/**
+ * 活动签到记录对象 fs_user_active_log
+ *
+ * @author fs
+ * @date 2025-09-23
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class FsUserActiveLog extends BaseEntity{
+
+    /** $column.columnComment */
+    private Long id;
+
+    /** 活动名称 */
+    @Excel(name = "活动名称")
+    private String activeName;
+
+    /** 用户uid */
+    @Excel(name = "用户uid")
+    private Long userId;
+
+    /** 获得积分 */
+    @Excel(name = "获得积分")
+    private Long number;
+
+    /** 剩余积分 */
+    @Excel(name = "剩余积分")
+    private Long balance;
+
+    /** 签到说明 */
+    @Excel(name = "签到说明")
+    private String title;
+
+    @Excel(name = "创建时间")
+    private Date createTime;
+     @Excel(name = "修改时间")
+    private Date updateTime;
+
+
+
+}

+ 30 - 0
fs-service/src/main/java/com/fs/his/dto/ActiveConfigDTO.java

@@ -0,0 +1,30 @@
+package com.fs.his.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+@Data
+public class ActiveConfigDTO implements Serializable {
+
+
+    /** 活动名称 */
+    private String activeName;
+
+    /** 活动开始时间 */
+    private LocalDate activeStartTime;
+
+    /** 活动结束时间 */
+    private LocalDate activeEndTime;
+
+    /** 消费达标条件 */
+    private BigDecimal moneyTerm;
+
+    /** 活动奖励 */
+    private Long activeReward;
+
+    /** 是否开启(1 开启,0 关闭) */
+    private String isOpen;
+}

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

@@ -42,6 +42,7 @@ public enum FsUserIntegralLogTypeEnum {
     TYPE_30(30,"阅读文章获取积分"),
     TYPE_30(30,"阅读文章获取积分"),
     TYPE_31(31,"广告积分"),
     TYPE_31(31,"广告积分"),
     TYPE_32(32,"积分兑换佣金"),
     TYPE_32(32,"积分兑换佣金"),
+    TYPE_33(33,"福袋积分"),
     ;
     ;
 
 
 
 

+ 5 - 0
fs-service/src/main/java/com/fs/his/mapper/AdProfitDetailMapper.java

@@ -4,6 +4,9 @@ package com.fs.his.mapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.his.domain.AdProfitDetail;
 import com.fs.his.domain.AdProfitDetail;
 import com.fs.his.dto.AdProfitDetailDto;
 import com.fs.his.dto.AdProfitDetailDto;
+import com.fs.his.param.AdProfitDetailStatisticsParam;
+import com.fs.his.vo.AdProfitDetailStatisticsVo;
+import org.apache.ibatis.annotations.Param;
 
 
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
@@ -65,4 +68,6 @@ public interface AdProfitDetailMapper extends BaseMapper<AdProfitDetail>  {
     String getCompanyByUserId(String userId);
     String getCompanyByUserId(String userId);
 
 
     Map<String, Object> getWithFinishAndMayWithdraw();
     Map<String, Object> getWithFinishAndMayWithdraw();
+
+    List<AdProfitDetailStatisticsVo> statisticsList(@Param("param")AdProfitDetailStatisticsParam param);
 }
 }

+ 81 - 0
fs-service/src/main/java/com/fs/his/mapper/FsConsecutiveWithdrawRecordMapper.java

@@ -0,0 +1,81 @@
+package com.fs.his.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.his.domain.FsConsecutiveWithdrawRecord;
+import com.fs.his.param.FsConsecutiveWithdrawRecordParam;
+import com.fs.his.vo.FsConsecutiveWithdrawRecordVo;
+import org.apache.ibatis.annotations.Param;
+
+import java.time.LocalDate;
+import java.util.List;
+
+/**
+ * 连续提现记录Mapper接口
+ * 
+ * @author fs
+ * @date 2026-02-04
+ */
+public interface FsConsecutiveWithdrawRecordMapper extends BaseMapper<FsConsecutiveWithdrawRecord>{
+    /**
+     * 查询连续提现记录
+     * 
+     * @param id 连续提现记录主键
+     * @return 连续提现记录
+     */
+    FsConsecutiveWithdrawRecord selectFsConsecutiveWithdrawRecordById(Long id);
+
+    /**
+     * 查询连续提现记录列表
+     * 
+     * @param param 连续提现记录
+     * @return 连续提现记录集合
+     */
+    List<FsConsecutiveWithdrawRecordVo> selectFsConsecutiveWithdrawRecordList(FsConsecutiveWithdrawRecordParam param);
+
+    /**
+     * 新增连续提现记录
+     * 
+     * @param fsConsecutiveWithdrawRecord 连续提现记录
+     * @return 结果
+     */
+    int insertFsConsecutiveWithdrawRecord(FsConsecutiveWithdrawRecord fsConsecutiveWithdrawRecord);
+
+    /**
+     * 修改连续提现记录
+     * 
+     * @param fsConsecutiveWithdrawRecord 连续提现记录
+     * @return 结果
+     */
+    int updateFsConsecutiveWithdrawRecord(FsConsecutiveWithdrawRecord fsConsecutiveWithdrawRecord);
+
+    /**
+     * 删除连续提现记录
+     * 
+     * @param id 连续提现记录主键
+     * @return 结果
+     */
+    int deleteFsConsecutiveWithdrawRecordById(Long id);
+
+    /**
+     * 批量删除连续提现记录
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteFsConsecutiveWithdrawRecordByIds(Long[] ids);
+
+    /**
+     * 检查用户在某时间段是否已记录连续提现
+     */
+    boolean existsRecordInPeriod(@Param("userId") Long userId,
+                                 @Param("startDate") LocalDate startDate,
+                                 @Param("endDate") LocalDate endDate,
+                                 @Param("thresholdDays") int thresholdDays);
+
+    Integer countByUserAndPeriod(@Param("userId") Long userId,
+                                 @Param("startDate") LocalDate startDate,
+                                 @Param("endDate") LocalDate endDate);
+
+    FsConsecutiveWithdrawRecord selectCountByUserIdAndStartTime(@Param("userId") Long userId,
+                                                                @Param("startDate") LocalDate startDate);
+}

+ 6 - 0
fs-service/src/main/java/com/fs/his/mapper/FsIntegralCartMapper.java

@@ -2,7 +2,9 @@ package com.fs.his.mapper;
 
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.his.domain.FsIntegralCart;
 import com.fs.his.domain.FsIntegralCart;
+import com.fs.his.param.GetFsIntegralCartListParam;
 import com.fs.his.vo.FsIntegralCartVO;
 import com.fs.his.vo.FsIntegralCartVO;
+import com.fs.his.vo.GetFsIntegralCartListVo;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Param;
 
 
 import java.util.List;
 import java.util.List;
@@ -35,4 +37,8 @@ public interface FsIntegralCartMapper extends BaseMapper<FsIntegralCart> {
      * @return  删除数量
      * @return  删除数量
      */
      */
     int deleteCartByGoodsId(@Param("goodsId") Long goodsId);
     int deleteCartByGoodsId(@Param("goodsId") Long goodsId);
+
+    List<GetFsIntegralCartListVo> getFsIntegralCartList(@Param("param") GetFsIntegralCartListParam param, @Param("aLong") Long aLong, @Param("cartId") List<Long> cartId);
+
+    int updateQuantityAtomically(@Param("userId") Long userId, @Param("goodsId") Long goodsId, @Param("addQuantity") Integer addQuantity, @Param("maxQuantity") int maxQuantity);
 }
 }

+ 4 - 7
fs-service/src/main/java/com/fs/his/mapper/FsIntegralGoodsMapper.java

@@ -2,17 +2,12 @@ package com.fs.his.mapper;
 
 
 import com.fs.his.domain.FsIntegralGoods;
 import com.fs.his.domain.FsIntegralGoods;
 import com.fs.his.param.FsIntegralGoodsListUParam;
 import com.fs.his.param.FsIntegralGoodsListUParam;
-import com.fs.his.vo.FsGoodsVO;
-import com.fs.his.vo.FsIntegralGoodsChooseVO;
-import com.fs.his.vo.FsIntegralGoodsListUVO;
-import com.fs.his.vo.FsIntegralGoodsListVO;
+import com.fs.his.vo.*;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 import org.apache.ibatis.annotations.Select;
 import org.apache.ibatis.annotations.Update;
 import org.apache.ibatis.annotations.Update;
 
 
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
 
 /**
 /**
  * 积分商品Mapper接口
  * 积分商品Mapper接口
@@ -111,4 +106,6 @@ public interface FsIntegralGoodsMapper
      * 根据id集合查询积分商品列表
      * 根据id集合查询积分商品列表
      */
      */
     List<FsGoodsVO> getFsGoodsVOListByIds(@Param("goodsIds") List<Long> goodsIds);
     List<FsGoodsVO> getFsGoodsVOListByIds(@Param("goodsIds") List<Long> goodsIds);
+
+    List<FsIntegralGoodsVo> selectAllByGoodsIds(@Param("goodsIds") Set<Long> goodsIds);
 }
 }

+ 103 - 0
fs-service/src/main/java/com/fs/his/mapper/FsIntegralRedPacketLogMapper.java

@@ -0,0 +1,103 @@
+package com.fs.his.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.his.domain.FsIntegralRedPacketLog;
+import com.fs.his.param.FsIntegralRedPacketLogParam;
+import com.fs.his.vo.FsIntegralRedPacketLogVo;
+import org.apache.ibatis.annotations.Param;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 积分佣金红包记录Mapper接口
+ * 
+ * @author fs
+ * @date 2026-01-22
+ */
+public interface FsIntegralRedPacketLogMapper extends BaseMapper<FsIntegralRedPacketLog>{
+    /**
+     * 查询积分佣金红包记录
+     * 
+     * @param logId 积分佣金红包记录主键
+     * @return 积分佣金红包记录
+     */
+    FsIntegralRedPacketLog selectFsIntegralRedPacketLogByLogId(Long logId);
+
+    /**
+     * 查询积分佣金红包记录列表
+     * 
+     * @param fsIntegralRedPacketLog 积分佣金红包记录
+     * @return 积分佣金红包记录集合
+     */
+    List<FsIntegralRedPacketLog> selectFsIntegralRedPacketLogList(FsIntegralRedPacketLog fsIntegralRedPacketLog);
+
+    /**
+     * 新增积分佣金红包记录
+     * 
+     * @param fsIntegralRedPacketLog 积分佣金红包记录
+     * @return 结果
+     */
+    int insertFsIntegralRedPacketLog(FsIntegralRedPacketLog fsIntegralRedPacketLog);
+
+    /**
+     * 修改积分佣金红包记录
+     * 
+     * @param fsIntegralRedPacketLog 积分佣金红包记录
+     * @return 结果
+     */
+    int updateFsIntegralRedPacketLog(FsIntegralRedPacketLog fsIntegralRedPacketLog);
+
+    /**
+     * 删除积分佣金红包记录
+     * 
+     * @param logId 积分佣金红包记录主键
+     * @return 结果
+     */
+    int deleteFsIntegralRedPacketLogByLogId(Long logId);
+
+    /**
+     * 批量删除积分佣金红包记录
+     * 
+     * @param logIds 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteFsIntegralRedPacketLogByLogIds(Long[] logIds);
+
+    BigDecimal sumMoneyByUserId(@Param("userId") Long userId);
+
+    FsIntegralRedPacketLog selectFsIntegralRedPacketLogByBatchNo(@Param("outBatchNo") String outBatchNo);
+
+    void batchUpdate(@Param("list")List<FsIntegralRedPacketLog> list);
+
+    List<FsIntegralRedPacketLogVo> getList(FsIntegralRedPacketLogParam param);
+
+    Long countLogsByToday(@Param("userId") Long userId);
+
+    /**
+     * 查询时间段内有提现的用户
+     */
+
+    List<Long> selectUsersWithWithdrawInPeriod(@Param("startDate") LocalDate startDate,
+                                               @Param("endDate") LocalDate endDate,
+                                               @Param("limitAmount") BigDecimal limitAmount);
+
+    /**
+     * 查询用户提现日期列表
+     */
+    List<LocalDate> selectWithdrawDatesByUser(@Param("userId") Long userId,
+                                              @Param("startDate") LocalDate startDate,
+                                              @Param("endDate") LocalDate endDate);
+
+    /**
+     * 查询用户时间段内提现统计
+     */
+    Map<String, Object> selectWithdrawStatsByUserAndPeriod(
+            @Param("userId") Long userId,
+            @Param("startDate") LocalDate startDate,
+            @Param("endDate") LocalDate endDate);
+
+    BigDecimal sumMoneyByStatus(@Param("status")int status);
+}

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

@@ -78,4 +78,6 @@ public interface FsStoreOrderItemMapper
     List<FsStoreOrderItem> selectFsStoreOrderItemListByItemIds(@Param("itemIds") List<Long> itemIds);
     List<FsStoreOrderItem> selectFsStoreOrderItemListByItemIds(@Param("itemIds") List<Long> itemIds);
 
 
     BigDecimal selectPayPriceByYear(@Param("userId") String userId);
     BigDecimal selectPayPriceByYear(@Param("userId") String userId);
+
+    BigDecimal selectPayPriceAll(@Param("userId") Long userId);
 }
 }

+ 68 - 0
fs-service/src/main/java/com/fs/his/mapper/FsUserActiveLogMapper.java

@@ -0,0 +1,68 @@
+package com.fs.his.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.his.domain.FsUserActiveLog;
+import org.apache.ibatis.annotations.Param;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 活动签到记录Mapper接口
+ *
+ * @author fs
+ * @date 2025-09-23
+ */
+public interface FsUserActiveLogMapper extends BaseMapper<FsUserActiveLog>{
+    /**
+     * 查询活动签到记录
+     *
+     * @param id 活动签到记录主键
+     * @return 活动签到记录
+     */
+    FsUserActiveLog selectFsUserActiveLogById(Long id);
+
+    /**
+     * 查询活动签到记录列表
+     *
+     * @param fsUserActiveLog 活动签到记录
+     * @return 活动签到记录集合
+     */
+    List<FsUserActiveLog> selectFsUserActiveLogList(FsUserActiveLog fsUserActiveLog);
+
+    /**
+     * 新增活动签到记录
+     *
+     * @param fsUserActiveLog 活动签到记录
+     * @return 结果
+     */
+    int insertFsUserActiveLog(FsUserActiveLog fsUserActiveLog);
+
+    /**
+     * 修改活动签到记录
+     *
+     * @param fsUserActiveLog 活动签到记录
+     * @return 结果
+     */
+    int updateFsUserActiveLog(FsUserActiveLog fsUserActiveLog);
+
+    /**
+     * 删除活动签到记录
+     *
+     * @param id 活动签到记录主键
+     * @return 结果
+     */
+    int deleteFsUserActiveLogById(Long id);
+
+    /**
+     * 批量删除活动签到记录
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteFsUserActiveLogByIds(Long[] ids);
+
+    FsUserActiveLog selectTodayIsSign(Long userId);
+
+    List<FsUserActiveLog> selectByUserAndDateRange(@Param("userId") Long userId,@Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime);
+}

+ 3 - 1
fs-service/src/main/java/com/fs/his/mapper/FsUserIntegralLogsMapper.java

@@ -3,6 +3,7 @@ package com.fs.his.mapper;
 import com.fs.common.annotation.DataSource;
 import com.fs.common.annotation.DataSource;
 import com.fs.common.enums.DataSourceType;
 import com.fs.common.enums.DataSourceType;
 import com.fs.his.domain.FsUserIntegralLogs;
 import com.fs.his.domain.FsUserIntegralLogs;
+import com.fs.his.param.AdProfitDetailStatisticsParam;
 import com.fs.his.param.FsUserIntegralLogsListUParam;
 import com.fs.his.param.FsUserIntegralLogsListUParam;
 import com.fs.his.param.FsUserIntegralLogsParam;
 import com.fs.his.param.FsUserIntegralLogsParam;
 import com.fs.his.vo.ExchangeDetailVo;
 import com.fs.his.vo.ExchangeDetailVo;
@@ -146,7 +147,8 @@ public interface FsUserIntegralLogsMapper
 
 
     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);
 
 
-    @DataSource(DataSourceType.SHARDING)
+//    @DataSource(DataSourceType.SHARDING)
     List<ExchangeDetailVo> getExchangDetailList(ExchangeDetailVo detailVo);
     List<ExchangeDetailVo> getExchangDetailList(ExchangeDetailVo detailVo);
 
 
+    Long sumIntegralByLogTypeAndCreateTime(@Param("logType") Integer logType,@Param("param") AdProfitDetailStatisticsParam param);
 }
 }

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

@@ -0,0 +1,12 @@
+package com.fs.his.param;
+
+import lombok.Data;
+
+@Data
+public class AdProfitDetailStatisticsParam {
+    Integer type;//"类型 1今天 2昨天 3 本周 4 上周 5本月 6上月 7本季度 8上季度 9本年 10去年
+    String companyIds;//员工IDS 多个用,分割
+    Long[] companies;
+    String startTime;
+    String endTime;
+}

+ 26 - 0
fs-service/src/main/java/com/fs/his/param/AddGoodsIntoCartParam.java

@@ -0,0 +1,26 @@
+package com.fs.his.param;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+
+@Data
+public class AddGoodsIntoCartParam {
+
+    @NotNull(message = "积分商品ID不能为空")
+    @ApiModelProperty("积分商品ID")
+    private Long goodsId;
+
+    @Max(value = 99, message = "单商品最多添加99件")
+    @Min(value = 1, message = "商品数量至少为1")
+    @NotNull(message = "商品数量不能为空")
+    @ApiModelProperty("商品数量")
+    private Integer quantity;
+
+    @NotNull(message = "是否选中:1是,0否,不能为空")
+    @ApiModelProperty("是否选中:1是,0否")
+    private Boolean isSelected;
+}

+ 18 - 0
fs-service/src/main/java/com/fs/his/param/CreateOrderFromCartParm.java

@@ -0,0 +1,18 @@
+package com.fs.his.param;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+@Data
+public class CreateOrderFromCartParm {
+    @NotNull(message = "收货地址ID不能为空")
+    @ApiModelProperty("收货地址ID")
+    private Long addressId;
+
+    @NotNull(message = "积分商品ID不能为空")
+    @ApiModelProperty("积分商品ID")
+    private List<Long> goodsId;
+}

+ 9 - 0
fs-service/src/main/java/com/fs/his/param/FsConsecutiveWithdrawRecordParam.java

@@ -0,0 +1,9 @@
+package com.fs.his.param;
+
+import com.fs.his.domain.FsConsecutiveWithdrawRecord;
+import lombok.Data;
+
+@Data
+public class FsConsecutiveWithdrawRecordParam extends FsConsecutiveWithdrawRecord {
+    private String nickName;
+}

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

@@ -0,0 +1,13 @@
+package com.fs.his.param;
+
+import com.fs.his.domain.FsIntegralRedPacketLog;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class FsIntegralRedPacketLogParam extends FsIntegralRedPacketLog {
+    private String nickName;
+    private BigDecimal minAmount;
+    private BigDecimal maxAmount;
+}

+ 19 - 0
fs-service/src/main/java/com/fs/his/param/FsIntegralWithdrawalParam.java

@@ -0,0 +1,19 @@
+package com.fs.his.param;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class FsIntegralWithdrawalParam {
+    private Long userId;
+
+    //申请提现金额
+    private BigDecimal applicationAmount;
+
+    private String appId;
+
+    private Integer source;//来源 1:h5 2:彩虹汇医小程序 3:APP
+
+
+}

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

@@ -0,0 +1,17 @@
+package com.fs.his.param;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+@Data
+public class GetFsIntegralCartDetailsParm {
+    @ApiModelProperty("收货地址ID")
+    private Long addressId;
+
+    @NotNull(message = "购物车ID不能为空")
+    @ApiModelProperty("购物车ID")
+    private List<Long> cartId;
+}

+ 14 - 0
fs-service/src/main/java/com/fs/his/param/GetFsIntegralCartListParam.java

@@ -0,0 +1,14 @@
+package com.fs.his.param;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+public class GetFsIntegralCartListParam {
+    @ApiModelProperty("商品名称")
+    private String goodsName;
+    @ApiModelProperty(value = "当前页,默认为1")
+    private Integer pageNum = 1;
+    @ApiModelProperty(value = "页容量,默认为10")
+    private Integer pageSize = 10;
+}

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

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.R;
 import com.fs.his.domain.AdProfitDetail;
 import com.fs.his.domain.AdProfitDetail;
 import com.fs.his.dto.AdProfitDetailDto;
 import com.fs.his.dto.AdProfitDetailDto;
+import com.fs.his.param.AdProfitDetailStatisticsParam;
+import com.fs.his.vo.AdProfitDetailStatisticsVo;
 
 
 import java.util.List;
 import java.util.List;
 
 
@@ -63,4 +65,6 @@ public interface IAdProfitDetailService extends IService<AdProfitDetail>{
     int deleteAdProfitDetailById(Long id);
     int deleteAdProfitDetailById(Long id);
 
 
     R getWithFinishAndMayWithdraw();
     R getWithFinishAndMayWithdraw();
+
+    List<AdProfitDetailStatisticsVo> statisticsList(AdProfitDetailStatisticsParam param);
 }
 }

+ 66 - 0
fs-service/src/main/java/com/fs/his/service/IFsConsecutiveWithdrawRecordService.java

@@ -0,0 +1,66 @@
+package com.fs.his.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.his.domain.FsConsecutiveWithdrawRecord;
+import com.fs.his.param.FsConsecutiveWithdrawRecordParam;
+import com.fs.his.vo.FsConsecutiveWithdrawRecordVo;
+
+import java.util.List;
+
+/**
+ * 连续提现记录Service接口
+ * 
+ * @author fs
+ * @date 2026-02-04
+ */
+public interface IFsConsecutiveWithdrawRecordService extends IService<FsConsecutiveWithdrawRecord>{
+    /**
+     * 查询连续提现记录
+     * 
+     * @param id 连续提现记录主键
+     * @return 连续提现记录
+     */
+    FsConsecutiveWithdrawRecord selectFsConsecutiveWithdrawRecordById(Long id);
+
+    /**
+     * 查询连续提现记录列表
+     * 
+     * @param fsConsecutiveWithdrawRecord 连续提现记录
+     * @return 连续提现记录集合
+     */
+    List<FsConsecutiveWithdrawRecordVo> selectFsConsecutiveWithdrawRecordList(FsConsecutiveWithdrawRecordParam fsConsecutiveWithdrawRecord);
+
+    /**
+     * 新增连续提现记录
+     * 
+     * @param fsConsecutiveWithdrawRecord 连续提现记录
+     * @return 结果
+     */
+    int insertFsConsecutiveWithdrawRecord(FsConsecutiveWithdrawRecord fsConsecutiveWithdrawRecord);
+
+    /**
+     * 修改连续提现记录
+     * 
+     * @param fsConsecutiveWithdrawRecord 连续提现记录
+     * @return 结果
+     */
+    int updateFsConsecutiveWithdrawRecord(FsConsecutiveWithdrawRecord fsConsecutiveWithdrawRecord);
+
+    /**
+     * 批量删除连续提现记录
+     * 
+     * @param ids 需要删除的连续提现记录主键集合
+     * @return 结果
+     */
+    int deleteFsConsecutiveWithdrawRecordByIds(Long[] ids);
+
+    /**
+     * 删除连续提现记录信息
+     * 
+     * @param id 连续提现记录主键
+     * @return 结果
+     */
+    int deleteFsConsecutiveWithdrawRecordById(Long id);
+
+    void checkConsecutiveWithdrawUsers();
+}

+ 15 - 0
fs-service/src/main/java/com/fs/his/service/IFsIntegralCartService.java

@@ -1,8 +1,15 @@
 package com.fs.his.service;
 package com.fs.his.service;
 
 
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.common.core.domain.R;
 import com.fs.his.domain.FsIntegralCart;
 import com.fs.his.domain.FsIntegralCart;
+import com.fs.his.param.AddGoodsIntoCartParam;
+import com.fs.his.param.CreateOrderFromCartParm;
+import com.fs.his.param.GetFsIntegralCartDetailsParm;
+import com.fs.his.param.GetFsIntegralCartListParam;
 import com.fs.his.vo.FsIntegralCartVO;
 import com.fs.his.vo.FsIntegralCartVO;
+import com.fs.his.vo.GetFsIntegralCartDetailsVo;
+import com.fs.his.vo.GetFsIntegralCartListVo;
 
 
 import javax.validation.constraints.NotNull;
 import javax.validation.constraints.NotNull;
 import java.util.List;
 import java.util.List;
@@ -48,4 +55,12 @@ public interface IFsIntegralCartService extends IService<FsIntegralCart> {
      * @return  list
      * @return  list
      */
      */
     List<FsIntegralCartVO> getCartByIds(Long userId, List<Long> ids);
     List<FsIntegralCartVO> getCartByIds(Long userId, List<Long> ids);
+
+    List<GetFsIntegralCartListVo> getFsIntegralCartList(GetFsIntegralCartListParam param, Long aLong);
+
+    GetFsIntegralCartDetailsVo getFsIntegralCartDetails(GetFsIntegralCartDetailsParm param, Long userId);
+
+    Boolean addGoodsIntoCart(AddGoodsIntoCartParam param, Long userId);
+
+    R createOrderFromCart(CreateOrderFromCartParm param, Long aLong);
 }
 }

+ 74 - 0
fs-service/src/main/java/com/fs/his/service/IFsIntegralRedPacketLogService.java

@@ -0,0 +1,74 @@
+package com.fs.his.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.common.core.domain.R;
+import com.fs.his.domain.FsIntegralRedPacketLog;
+import com.fs.his.param.FsIntegralRedPacketLogParam;
+import com.fs.his.vo.FsIntegralRedPacketLogVo;
+import com.github.binarywang.wxpay.bean.transfer.TransferBillsNotifyResult;
+
+import java.util.List;
+
+/**
+ * 积分佣金红包记录Service接口
+ * 
+ * @author fs
+ * @date 2026-01-22
+ */
+public interface IFsIntegralRedPacketLogService extends IService<FsIntegralRedPacketLog>{
+    /**
+     * 查询积分佣金红包记录
+     * 
+     * @param logId 积分佣金红包记录主键
+     * @return 积分佣金红包记录
+     */
+    FsIntegralRedPacketLog selectFsIntegralRedPacketLogByLogId(Long logId);
+
+    /**
+     * 查询积分佣金红包记录列表
+     * 
+     * @param fsIntegralRedPacketLog 积分佣金红包记录
+     * @return 积分佣金红包记录集合
+     */
+    List<FsIntegralRedPacketLog> selectFsIntegralRedPacketLogList(FsIntegralRedPacketLog fsIntegralRedPacketLog);
+    List<FsIntegralRedPacketLogVo> getList(FsIntegralRedPacketLogParam param);
+
+    /**
+     * 新增积分佣金红包记录
+     * 
+     * @param fsIntegralRedPacketLog 积分佣金红包记录
+     * @return 结果
+     */
+    int insertFsIntegralRedPacketLog(FsIntegralRedPacketLog fsIntegralRedPacketLog);
+
+    /**
+     * 修改积分佣金红包记录
+     * 
+     * @param fsIntegralRedPacketLog 积分佣金红包记录
+     * @return 结果
+     */
+    int updateFsIntegralRedPacketLog(FsIntegralRedPacketLog fsIntegralRedPacketLog);
+
+    /**
+     * 批量删除积分佣金红包记录
+     * 
+     * @param logIds 需要删除的积分佣金红包记录主键集合
+     * @return 结果
+     */
+    int deleteFsIntegralRedPacketLogByLogIds(Long[] logIds);
+
+    /**
+     * 删除积分佣金红包记录信息
+     * 
+     * @param logId 积分佣金红包记录主键
+     * @return 结果
+     */
+    int deleteFsIntegralRedPacketLogByLogId(Long logId);
+
+    R syncRedPacket(String outBatchNo, String batchId);
+    R syncErrorRedPacket(TransferBillsNotifyResult.DecryptNotifyResult result);
+
+    FsIntegralRedPacketLog getRedPacketLogByCode(String orderCode);
+
+    void synchronizationWxMerchantPayStatus();
+}

+ 1 - 0
fs-service/src/main/java/com/fs/his/service/IFsStoreOrderService.java

@@ -281,4 +281,5 @@ public interface IFsStoreOrderService
 
 
     BigDecimal selectPayPriceByYear(String userId);
     BigDecimal selectPayPriceByYear(String userId);
 
 
+    BigDecimal selectPayPriceAll(Long userId);
 }
 }

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

@@ -8,6 +8,7 @@ import com.alibaba.fastjson.JSONObject;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.R;
 import com.fs.company.param.FsStoreStatisticsParam;
 import com.fs.company.param.FsStoreStatisticsParam;
 import com.fs.company.vo.FsStorePaymentStatisticsVO;
 import com.fs.company.vo.FsStorePaymentStatisticsVO;
+import com.fs.his.config.AppConfig;
 import com.fs.his.domain.FsStorePayment;
 import com.fs.his.domain.FsStorePayment;
 import com.fs.his.param.FsStorePaymentParam;
 import com.fs.his.param.FsStorePaymentParam;
 import com.fs.his.param.PayOrderParam;
 import com.fs.his.param.PayOrderParam;
@@ -95,6 +96,8 @@ public interface IFsStorePaymentService
 
 
     R sendRedPacket(WxSendRedPacketParam param);
     R sendRedPacket(WxSendRedPacketParam param);
 
 
+    R sendRedPacketV3ByApp(WxSendRedPacketParam param, AppConfig config);
+
     R sendRedPacketV3(WxSendRedPacketParam param);
     R sendRedPacketV3(WxSendRedPacketParam param);
 
 
     R sendRedPacketLimit(WxSendRedPacketParam param);
     R sendRedPacketLimit(WxSendRedPacketParam param);
@@ -143,4 +146,10 @@ public interface IFsStorePaymentService
     void synchronizePayStatus();
     void synchronizePayStatus();
 
 
     List<FsStorePayment> selectAllPayment();
     List<FsStorePayment> selectAllPayment();
+
+    R sendAppRedPacket(WxSendRedPacketParam packetParam);
+
+    R sendIntegralRedPacket(WxSendRedPacketParam param);
+
+    String integralV3TransferNotify(String notifyData, HttpServletRequest request);
 }
 }

+ 70 - 0
fs-service/src/main/java/com/fs/his/service/IFsUserActiveLogService.java

@@ -0,0 +1,70 @@
+package com.fs.his.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.common.core.domain.R;
+import com.fs.his.domain.FsUser;
+import com.fs.his.domain.FsUserActiveLog;
+
+import java.util.List;
+
+/**
+ * 活动签到记录Service接口
+ *
+ * @author fs
+ * @date 2025-09-23
+ */
+public interface IFsUserActiveLogService extends IService<FsUserActiveLog>{
+    /**
+     * 查询活动签到记录
+     *
+     * @param id 活动签到记录主键
+     * @return 活动签到记录
+     */
+    FsUserActiveLog selectFsUserActiveLogById(Long id);
+
+    /**
+     * 查询活动签到记录列表
+     *
+     * @param fsUserActiveLog 活动签到记录
+     * @return 活动签到记录集合
+     */
+    List<FsUserActiveLog> selectFsUserActiveLogList(FsUserActiveLog fsUserActiveLog);
+
+    /**
+     * 新增活动签到记录
+     *
+     * @param fsUserActiveLog 活动签到记录
+     * @return 结果
+     */
+    int insertFsUserActiveLog(FsUserActiveLog fsUserActiveLog);
+
+    /**
+     * 修改活动签到记录
+     *
+     * @param fsUserActiveLog 活动签到记录
+     * @return 结果
+     */
+    int updateFsUserActiveLog(FsUserActiveLog fsUserActiveLog);
+
+    /**
+     * 批量删除活动签到记录
+     *
+     * @param ids 需要删除的活动签到记录主键集合
+     * @return 结果
+     */
+    int deleteFsUserActiveLogByIds(Long[] ids);
+
+    /**
+     * 删除活动签到记录信息
+     *
+     * @param id 活动签到记录主键
+     * @return 结果
+     */
+    int deleteFsUserActiveLogById(Long id);
+
+    Long userActiveLogSign(FsUser fsUser);
+
+    R getUserActiveLog(FsUser fsUser);
+
+    boolean getUserActiveWindow(FsUser fsUser);
+}

+ 3 - 4
fs-service/src/main/java/com/fs/his/service/IFsUserIntegralLogsService.java

@@ -2,10 +2,7 @@ package com.fs.his.service;
 
 
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.R;
 import com.fs.his.domain.FsUserIntegralLogs;
 import com.fs.his.domain.FsUserIntegralLogs;
-import com.fs.his.param.FsUserAddIntegralParam;
-import com.fs.his.param.FsUserAddIntegralTemplateParam;
-import com.fs.his.param.FsUserIntegralLogsListUParam;
-import com.fs.his.param.FsUserIntegralLogsParam;
+import com.fs.his.param.*;
 import com.fs.his.vo.FsUserIntegralLogsListUVO;
 import com.fs.his.vo.FsUserIntegralLogsListUVO;
 import com.fs.his.vo.FsUserIntegralLogsListVO;
 import com.fs.his.vo.FsUserIntegralLogsListVO;
 
 
@@ -86,4 +83,6 @@ public interface IFsUserIntegralLogsService
 
 
     //app获取新人福利完成情况
     //app获取新人福利完成情况
     R getNewcomerBenefits(Long userId);
     R getNewcomerBenefits(Long userId);
+
+    Long sumIntegralByLogTypeAndCreateTime(Integer value, AdProfitDetailStatisticsParam param);
 }
 }

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

@@ -16,6 +16,7 @@ import com.fs.his.domain.FsUser;
 import com.fs.his.domain.FsUserAddress;
 import com.fs.his.domain.FsUserAddress;
 import com.fs.his.dto.FindUsersByDTO;
 import com.fs.his.dto.FindUsersByDTO;
 import com.fs.his.param.FindUserByParam;
 import com.fs.his.param.FindUserByParam;
+import com.fs.his.param.FsIntegralWithdrawalParam;
 import com.fs.his.param.FsUserParam;
 import com.fs.his.param.FsUserParam;
 import com.fs.his.vo.FsUserVO;
 import com.fs.his.vo.FsUserVO;
 import com.fs.his.vo.FsUserExportListVO;
 import com.fs.his.vo.FsUserExportListVO;
@@ -249,4 +250,6 @@ public interface IFsUserService
     R integralExchange(Long userId);
     R integralExchange(Long userId);
 
 
     R exchangDetail(Map<String, Object> params);
     R exchangDetail(Map<String, Object> params);
+
+    R withdrawal(FsIntegralWithdrawalParam param);
 }
 }

+ 70 - 0
fs-service/src/main/java/com/fs/his/service/impl/AdProfitDetailServiceImpl.java

@@ -4,16 +4,29 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.R;
 import com.fs.his.domain.AdProfitDetail;
 import com.fs.his.domain.AdProfitDetail;
 import com.fs.his.dto.AdProfitDetailDto;
 import com.fs.his.dto.AdProfitDetailDto;
+import com.fs.his.enums.FsUserIntegralLogTypeEnum;
 import com.fs.his.mapper.AdProfitDetailMapper;
 import com.fs.his.mapper.AdProfitDetailMapper;
+import com.fs.his.mapper.FsIntegralRedPacketLogMapper;
+import com.fs.his.param.AdProfitDetailStatisticsParam;
 import com.fs.his.service.IAdProfitDetailService;
 import com.fs.his.service.IAdProfitDetailService;
+import com.fs.his.service.IFsUserIntegralLogsService;
+import com.fs.his.vo.AdProfitDetailStatisticsVo;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 
 
 import java.math.BigDecimal;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.math.RoundingMode;
 import java.time.LocalDateTime;
 import java.time.LocalDateTime;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 
 /**
 /**
  * 广告分佣Service业务层处理
  * 广告分佣Service业务层处理
@@ -22,7 +35,15 @@ import java.util.Map;
  * @date 2025-11-27
  * @date 2025-11-27
  */
  */
 @Service
 @Service
+@Slf4j
 public class AdProfitDetailServiceImpl extends ServiceImpl<AdProfitDetailMapper, AdProfitDetail> implements IAdProfitDetailService {
 public class AdProfitDetailServiceImpl extends ServiceImpl<AdProfitDetailMapper, AdProfitDetail> implements IAdProfitDetailService {
+    @Autowired
+    private FsIntegralRedPacketLogMapper fsIntegralRedPacketLogMapper;
+    @Autowired
+    private IFsUserIntegralLogsService fsUserIntegralLogsService;
+    @Autowired
+    @Qualifier("threadPoolTaskExecutor")
+    private ThreadPoolTaskExecutor executor;
 
 
     /**
     /**
      * 查询广告分佣
      * 查询广告分佣
@@ -112,9 +133,58 @@ public class AdProfitDetailServiceImpl extends ServiceImpl<AdProfitDetailMapper,
         BigDecimal totalMayWithdraw = new BigDecimal(map.get("totalMayWithdraw").toString()).divide(BigDecimal.valueOf(100), 2, RoundingMode.DOWN);
         BigDecimal totalMayWithdraw = new BigDecimal(map.get("totalMayWithdraw").toString()).divide(BigDecimal.valueOf(100), 2, RoundingMode.DOWN);
         // 已提现总金额
         // 已提现总金额
         BigDecimal totalWithdrawFinish = new BigDecimal(map.get("totalWithdrawFinish").toString()).divide(BigDecimal.valueOf(100), 2, RoundingMode.DOWN);
         BigDecimal totalWithdrawFinish = new BigDecimal(map.get("totalWithdrawFinish").toString()).divide(BigDecimal.valueOf(100), 2, RoundingMode.DOWN);
+        // 提现中金额
+        BigDecimal withdrawMoney = fsIntegralRedPacketLogMapper.sumMoneyByStatus(0);
+        if (withdrawMoney != null) {
+            totalMayWithdraw = totalMayWithdraw.subtract(withdrawMoney);
+            totalWithdrawFinish = totalWithdrawFinish.subtract(withdrawMoney);
+            withdrawMoney = withdrawMoney.setScale(2, RoundingMode.UP);
+        }
         Map<String,Object> widthdrawMap = new HashMap<>();
         Map<String,Object> widthdrawMap = new HashMap<>();
         widthdrawMap.put("totalMayWithdraw",totalMayWithdraw);
         widthdrawMap.put("totalMayWithdraw",totalMayWithdraw);
         widthdrawMap.put("totalWithdrawFinish",totalWithdrawFinish);
         widthdrawMap.put("totalWithdrawFinish",totalWithdrawFinish);
+        widthdrawMap.put("withdrawMoney",withdrawMoney);
         return R.ok(widthdrawMap);
         return R.ok(widthdrawMap);
     }
     }
+
+    @Override
+    public List<AdProfitDetailStatisticsVo> statisticsList(AdProfitDetailStatisticsParam param) {
+        // 并行执行两个查询
+        CompletableFuture<List<AdProfitDetailStatisticsVo>> vosFuture = CompletableFuture
+                .supplyAsync(() -> baseMapper.statisticsList(param), executor);
+
+        CompletableFuture<BigDecimal> integralFuture = CompletableFuture
+                .supplyAsync(() -> {
+                    Long integral = fsUserIntegralLogsService.sumIntegralByLogTypeAndCreateTime(
+                            FsUserIntegralLogTypeEnum.TYPE_31.getValue(), param
+                    );
+                    return integral != null && integral > 0
+                            ? new BigDecimal(integral).divide(BigDecimal.valueOf(1000), 2, RoundingMode.HALF_UP)
+                            : BigDecimal.ZERO;
+                }, executor);
+
+        try {
+            // 等待两个都完成
+            CompletableFuture.allOf(vosFuture, integralFuture)
+                    .get(60, TimeUnit.SECONDS);
+
+            List<AdProfitDetailStatisticsVo> vos = vosFuture.getNow(null);
+            BigDecimal totalIntegral = integralFuture.getNow(BigDecimal.ZERO);
+
+            if (vos != null && !vos.isEmpty()) {
+                vos.get(0).setAmountYuan(totalIntegral);
+            }
+
+            return vos != null ? vos : Collections.emptyList();
+
+        } catch (TimeoutException e) {
+            log.error("查询超时", e);
+            vosFuture.cancel(true);
+            integralFuture.cancel(true);
+            throw new RuntimeException("查询超时,请稍后重试");
+        } catch (Exception e) {
+            log.error("查询异常", e);
+            throw new RuntimeException("查询执行异常", e);
+        }
+    }
 }
 }

+ 405 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsConsecutiveWithdrawRecordServiceImpl.java

@@ -0,0 +1,405 @@
+package com.fs.his.service.impl;
+
+import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.utils.DateUtils;
+import com.fs.his.config.AppConfig;
+import com.fs.his.config.IntegralConfig;
+import com.fs.his.domain.FsConsecutiveWithdrawRecord;
+import com.fs.his.mapper.FsConsecutiveWithdrawRecordMapper;
+import com.fs.his.mapper.FsIntegralRedPacketLogMapper;
+import com.fs.his.param.FsConsecutiveWithdrawRecordParam;
+import com.fs.his.service.IFsConsecutiveWithdrawRecordService;
+import com.fs.his.vo.FsConsecutiveWithdrawRecordVo;
+import com.fs.system.service.ISysConfigService;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 连续提现记录Service业务层处理
+ * 
+ * @author fs
+ * @date 2026-02-04
+ */
+@Service
+@Slf4j
+public class FsConsecutiveWithdrawRecordServiceImpl extends ServiceImpl<FsConsecutiveWithdrawRecordMapper, FsConsecutiveWithdrawRecord> implements IFsConsecutiveWithdrawRecordService {
+    @Autowired
+    private FsIntegralRedPacketLogMapper redPacketLogMapper;
+
+    @Autowired
+    private RedissonClient redissonClient;
+
+    @Autowired
+    private ISysConfigService configService;
+
+    /**
+     * 查询连续提现记录
+     * 
+     * @param id 连续提现记录主键
+     * @return 连续提现记录
+     */
+    @Override
+    public FsConsecutiveWithdrawRecord selectFsConsecutiveWithdrawRecordById(Long id)
+    {
+        return baseMapper.selectFsConsecutiveWithdrawRecordById(id);
+    }
+
+    /**
+     * 查询连续提现记录列表
+     * 
+     * @param param 连续提现记录
+     * @return 连续提现记录
+     */
+    @Override
+    public List<FsConsecutiveWithdrawRecordVo> selectFsConsecutiveWithdrawRecordList(FsConsecutiveWithdrawRecordParam param)
+    {
+        return baseMapper.selectFsConsecutiveWithdrawRecordList(param);
+    }
+
+    /**
+     * 新增连续提现记录
+     * 
+     * @param fsConsecutiveWithdrawRecord 连续提现记录
+     * @return 结果
+     */
+    @Override
+    public int insertFsConsecutiveWithdrawRecord(FsConsecutiveWithdrawRecord fsConsecutiveWithdrawRecord)
+    {
+        fsConsecutiveWithdrawRecord.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertFsConsecutiveWithdrawRecord(fsConsecutiveWithdrawRecord);
+    }
+
+    /**
+     * 修改连续提现记录
+     * 
+     * @param fsConsecutiveWithdrawRecord 连续提现记录
+     * @return 结果
+     */
+    @Override
+    public int updateFsConsecutiveWithdrawRecord(FsConsecutiveWithdrawRecord fsConsecutiveWithdrawRecord)
+    {
+        fsConsecutiveWithdrawRecord.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateFsConsecutiveWithdrawRecord(fsConsecutiveWithdrawRecord);
+    }
+
+    /**
+     * 批量删除连续提现记录
+     * 
+     * @param ids 需要删除的连续提现记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsConsecutiveWithdrawRecordByIds(Long[] ids)
+    {
+        return baseMapper.deleteFsConsecutiveWithdrawRecordByIds(ids);
+    }
+
+    /**
+     * 删除连续提现记录信息
+     * 
+     * @param id 连续提现记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsConsecutiveWithdrawRecordById(Long id)
+    {
+        return baseMapper.deleteFsConsecutiveWithdrawRecordById(id);
+    }
+    // 缓存已处理的用户和日期,避免重复查询
+    private final Set<String> processedUserPeriodCache = ConcurrentHashMap.newKeySet();
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void checkConsecutiveWithdrawUsers() {
+        if (!tryLockTask()) {
+            log.info("连续提现检查任务已在其他节点执行中,本次跳过");
+            return;
+        }
+        try {
+            log.info("========== 开始执行连续提现检查任务 ==========");
+            // 清空缓存
+            processedUserPeriodCache.clear();
+            executeCheck();
+            log.info("========== 连续提现检查任务执行完成 ==========");
+        } catch (Exception e) {
+            log.error("连续提现检查任务执行失败", e);
+            throw e; // 抛出异常让事务回滚
+        } finally {
+            unlockTask();
+        }
+    }
+
+
+    /**
+     * 查找连续提现期间
+     */
+    private List<ConsecutivePeriod> findConsecutivePeriods(List<LocalDate> dates) {
+        List<ConsecutivePeriod> periods = new ArrayList<>();
+
+        if (dates.isEmpty()) {
+            return periods;
+        }
+
+        // 去重并排序
+        Set<LocalDate> dateSet = new TreeSet<>(dates);
+        List<LocalDate> sortedDates = new ArrayList<>(dateSet);
+
+        LocalDate currentStart = sortedDates.get(0);
+        LocalDate previousDate = currentStart;
+        int currentStreak = 1;
+
+        for (int i = 1; i < sortedDates.size(); i++) {
+            LocalDate currentDate = sortedDates.get(i);
+            // 计算天数差
+            if (previousDate.plusDays(1).equals(currentDate)) {
+                // 连续
+                currentStreak++;
+            } else {
+                // 不连续,结束当前连续期间
+                if (currentStreak > 1) {
+                    periods.add(new ConsecutivePeriod(
+                            currentStart, previousDate, currentStreak));
+                }
+                // 开始新的连续期间
+                currentStart = currentDate;
+                currentStreak = 1;
+            }
+
+            previousDate = currentDate;
+        }
+
+        // 添加最后一个连续期间
+        if (currentStreak > 1) {
+            periods.add(new ConsecutivePeriod(
+                    currentStart, previousDate, currentStreak));
+        }
+
+        return periods;
+    }
+
+    /**
+     * 记录连续提现
+     */
+    private Boolean recordConsecutiveWithdraw(Long userId, ConsecutivePeriod period,BigDecimal limitAmount) {
+        // 1. 检查是否已记录
+        boolean exists = checkRecordExists(userId, period.getStartDate(), period.getEndDate());
+        if (exists) {
+            log.debug("记录已存在 - 用户: {}, 期间: {} 到 {}",
+                    userId, period.getStartDate(), period.getEndDate());
+            return false;
+        }
+        //查询开始时间是否一致 一致则更新
+        FsConsecutiveWithdrawRecord oldRecord = baseMapper.selectCountByUserIdAndStartTime(userId, period.getStartDate());
+
+        // 查询连续期间提现详情
+        Map<String, Object> withdrawStats = redPacketLogMapper.selectWithdrawStatsByUserAndPeriod(
+                userId, period.getStartDate(), period.getEndDate());
+
+        FsConsecutiveWithdrawRecord record = new FsConsecutiveWithdrawRecord();
+
+        record.setConsecutiveDays(period.getConsecutiveDays());
+
+        record.setEndDate(Date.from(period.getEndDate().atStartOfDay(ZoneId.systemDefault()).toInstant()));
+        record.setWithdrawCount(((Number) withdrawStats.get("count")).intValue());
+        Object totalAmount = withdrawStats.get("total_amount");
+        if (totalAmount == null || ((BigDecimal)totalAmount).compareTo(limitAmount)<0) {
+            log.info("用户UserId:{},提现:{},未达到阈值:{}",userId,totalAmount,limitAmount);
+            return false;
+        }
+        record.setTotalAmount((BigDecimal) withdrawStats.get("total_amount"));
+
+        int i = 0;
+        if (oldRecord != null) {
+            //更新
+            record.setId(oldRecord.getId());
+            record.setUpdateTime(DateUtils.getNowDate());
+            i = baseMapper.updateFsConsecutiveWithdrawRecord(record);
+        } else {
+            //新增
+            record.setUserId(userId);
+            record.setStartDate(Date.from(period.getStartDate().atStartOfDay(ZoneId.systemDefault()).toInstant()));
+            record.setCreateTime(DateUtils.getNowDate());
+            i = baseMapper.insertFsConsecutiveWithdrawRecord(record);
+        }
+        log.info("记录用户 {} 连续提现: {}天 ({} 到 {})",
+                userId, period.getConsecutiveDays(),
+                period.getStartDate(), period.getEndDate());
+        return i > 0;
+    }
+    /**
+     * 检查记录是否已存在
+     */
+    private boolean checkRecordExists(Long userId, LocalDate startDate, LocalDate endDate) {
+        Integer count = baseMapper.countByUserAndPeriod(userId,startDate,endDate);
+        return count != null && count > 0;
+    }
+
+
+    /**
+     * 连续期间内部类
+     */
+    @Data
+    @AllArgsConstructor
+    private static class ConsecutivePeriod {
+        private LocalDate startDate;
+        private LocalDate endDate;
+        private int consecutiveDays;
+    }
+
+    /**
+     * 执行连续提现检查
+     */
+    private void executeCheck() {
+
+        // 1.获取动态配置
+        String json = configService.selectConfigByKey("app.config");
+        AppConfig config = JSONUtil.toBean(json, AppConfig.class);
+        int checkRangeDays = 30;//检查时间范围的天数
+        Integer thresholdDays = config.getLimitDayNum();
+        BigDecimal limitAmount = config.getLimitAmount();
+        if (thresholdDays == null || thresholdDays <=0){
+            log.error("连续天数阈值配置无效: {}", thresholdDays);
+            return;
+        }
+        if (limitAmount == null || limitAmount.compareTo(BigDecimal.ZERO) <= 0){
+            log.error("连续累计金额阈值配置无效: {}", thresholdDays);
+            return;
+        }
+        log.info("当前配置: 连续天数阈值={}天, 检查范围={}天,累计金额阈值={}", thresholdDays, checkRangeDays, limitAmount);
+        // 2. 计算日期范围
+        LocalDate endDate = LocalDate.now().minusDays(1);  // 昨天
+//        LocalDate endDate = LocalDate.now();  // 今天
+        LocalDate startDate = endDate.minusDays(checkRangeDays - 1);
+        // 3. 查询最近有提现且累计金额大于大于阈值的用户
+        List<Long> recentWithdrawUsers = redPacketLogMapper.selectUsersWithWithdrawInPeriod(
+                startDate, endDate,limitAmount);
+        if (recentWithdrawUsers.isEmpty()) {
+            log.info("近期没有累计金额达到阈值用户提现");
+            return;
+        }
+        log.info("近期提现用户数量: {}", recentWithdrawUsers.size());
+        // 4. 批量检查每个用户
+        int batchSize = 100;
+        int qualifiedCount = 0;
+        int recordedCount = 0;
+
+        for (int i = 0; i < recentWithdrawUsers.size(); i += batchSize) {
+            int endIndex = Math.min(i + batchSize, recentWithdrawUsers.size());
+            List<Long> batchUsers = recentWithdrawUsers.subList(i, endIndex);
+
+            // 处理本批次用户
+            for (Long userId : batchUsers) {
+                try {
+                    int batchRecorded = processUserWithdrawRecord(
+                            userId, thresholdDays, startDate, endDate,limitAmount);
+                    if (batchRecorded > 0) {
+                        qualifiedCount++;
+                        recordedCount += batchRecorded;
+                    }
+                } catch (Exception e) {
+                    log.error("检查用户 {} 连续提现记录失败", userId, e);
+                }
+            }
+
+            log.info("已处理 {}/{} 个用户", endIndex, recentWithdrawUsers.size());
+        }
+        log.info("检查完成: 达标用户={}, 新记录数={}", qualifiedCount, recordedCount);
+    }
+
+    /**
+     * 处理单个用户提现记录
+     */
+    private int processUserWithdrawRecord(Long userId, int thresholdDays,
+                                          LocalDate startDate, LocalDate endDate,BigDecimal limitAmount) {
+        // 使用缓存键检查是否已处理过
+        String cacheKey = String.format("%s_%s_%s", userId, startDate, endDate);
+        if (processedUserPeriodCache.contains(cacheKey)) {
+            return 0;
+        }
+        // 1. 查询用户提现日期列表
+        List<LocalDate> withdrawDates = redPacketLogMapper.selectWithdrawDatesByUser(
+                userId, startDate, endDate);
+
+        if (withdrawDates.isEmpty() || withdrawDates.size() < thresholdDays) {
+            return 0;
+        }
+
+        // 2. 查找连续提现期间
+        List<ConsecutivePeriod> consecutivePeriods = findConsecutivePeriods(withdrawDates);
+
+        // 3. 检查是否有达到阈值的连续期间
+        int recordedCount = 0;
+        for (ConsecutivePeriod period : consecutivePeriods) {
+            if (period.getConsecutiveDays() >= thresholdDays) {
+                // 4. 记录到数据库
+                boolean success = recordConsecutiveWithdraw(userId, period,limitAmount);
+                if (success) {
+                    recordedCount++;
+                }
+            }
+        }
+        // 添加到缓存
+        if (!withdrawDates.isEmpty()) {
+            processedUserPeriodCache.add(cacheKey);
+        }
+        return recordedCount;
+    }
+
+    private static final String TASK_LOCK_KEY = "consecutive_withdraw_task_lock";
+    /**
+     * 获取分布式锁
+     */
+    private boolean tryLockTask() {
+        RLock lock = redissonClient.getLock(TASK_LOCK_KEY);
+
+        try {
+            // 尝试获取锁,最多等待5秒,锁持有时间30分钟
+            // 参数说明:
+            // waitTime: 等待锁的时间(秒)
+            // leaseTime: 锁自动释放时间(秒)
+            // unit: 时间单位
+            return lock.tryLock(5, 1800, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.warn("获取分布式锁时被中断", e);
+            return false;
+        } catch (Exception e) {
+            log.error("获取分布式锁失败", e);
+            return false;
+        }
+    }
+
+    /**
+     * 释放锁
+     */
+    private void unlockTask() {
+        RLock lock = redissonClient.getLock(TASK_LOCK_KEY);
+
+        try {
+            // 检查当前线程是否持有锁
+            if (lock.isHeldByCurrentThread()) {
+                lock.unlock();
+                log.debug("分布式锁释放成功: {}", TASK_LOCK_KEY);
+            } else {
+                log.debug("当前线程未持有锁,无需释放: {}", TASK_LOCK_KEY);
+            }
+        } catch (IllegalMonitorStateException e) {
+            log.warn("锁状态异常,可能已自动释放: {}", TASK_LOCK_KEY);
+        } catch (Exception e) {
+            log.error("释放分布式锁失败: {}", TASK_LOCK_KEY, e);
+        }
+    }
+}

+ 224 - 11
fs-service/src/main/java/com/fs/his/service/impl/FsIntegralCartServiceImpl.java

@@ -1,28 +1,40 @@
 package com.fs.his.service.impl;
 package com.fs.his.service.impl;
 
 
+import cn.hutool.json.JSONUtil;
 import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.core.domain.R;
 import com.fs.common.exception.CustomException;
 import com.fs.common.exception.CustomException;
-import com.fs.his.domain.FsIntegralCart;
-import com.fs.his.domain.FsIntegralGoods;
-import com.fs.his.domain.FsUser;
-import com.fs.his.mapper.FsIntegralCartMapper;
-import com.fs.his.mapper.FsIntegralGoodsMapper;
-import com.fs.his.mapper.FsUserMapper;
+import com.fs.common.exception.ServiceException;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.spring.SpringUtils;
+import com.fs.core.utils.OrderCodeUtils;
+import com.fs.his.domain.*;
+import com.fs.his.mapper.*;
+import com.fs.his.param.AddGoodsIntoCartParam;
+import com.fs.his.param.CreateOrderFromCartParm;
+import com.fs.his.param.GetFsIntegralCartDetailsParm;
+import com.fs.his.param.GetFsIntegralCartListParam;
 import com.fs.his.service.IFsIntegralCartService;
 import com.fs.his.service.IFsIntegralCartService;
-import com.fs.his.vo.FsIntegralCartVO;
+import com.fs.his.service.IFsUserIntegralLogsService;
+import com.fs.his.vo.*;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.ObjectUtils;
+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.dao.DuplicateKeyException;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.annotation.Transactional;
 
 
 import javax.annotation.Resource;
 import javax.annotation.Resource;
 import javax.validation.constraints.NotNull;
 import javax.validation.constraints.NotNull;
 import java.time.LocalDateTime;
 import java.time.LocalDateTime;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 
 @Slf4j
 @Slf4j
 @Service
 @Service
@@ -32,6 +44,14 @@ public class FsIntegralCartServiceImpl extends ServiceImpl<FsIntegralCartMapper,
     private FsIntegralGoodsMapper goodsMapper;
     private FsIntegralGoodsMapper goodsMapper;
     @Resource
     @Resource
     private FsUserMapper userMapper;
     private FsUserMapper userMapper;
+    @Autowired
+    private FsUserAddressMapper fsUserAddressMapper;
+    @Autowired
+    private FsIntegralGoodsMapper fsIntegralGoodsMapper;
+    @Autowired
+    private FsIntegralOrderMapper fsIntegralOrderMapper;
+    @Autowired
+    IFsUserIntegralLogsService integralLogsService;
 
 
     /**
     /**
      * 添加或修改购物车
      * 添加或修改购物车
@@ -123,4 +143,197 @@ public class FsIntegralCartServiceImpl extends ServiceImpl<FsIntegralCartMapper,
         params.put("ids", ids);
         params.put("ids", ids);
         return baseMapper.getCartsByMap(params);
         return baseMapper.getCartsByMap(params);
     }
     }
+
+    @Override
+    public List<GetFsIntegralCartListVo> getFsIntegralCartList(GetFsIntegralCartListParam param, Long aLong) {
+        return baseMapper.getFsIntegralCartList(param, aLong, null);
+    }
+
+    /**
+     * 获取用户购物车详情页
+     *
+     * @param param
+     * @param userId
+     * @return
+     */
+    @Override
+    public GetFsIntegralCartDetailsVo getFsIntegralCartDetails(GetFsIntegralCartDetailsParm param, Long userId) {
+        List<GetFsIntegralCartListVo> cartListVoList = baseMapper.getFsIntegralCartList(new GetFsIntegralCartListParam(), userId, param.getCartId());
+        GetFsIntegralCartDetailsVo vos = new GetFsIntegralCartDetailsVo();
+        ArrayList<GetCartGoodsDetailsVo> goodsDetailsVos = new ArrayList<>();
+        for (GetFsIntegralCartListVo listVo : cartListVoList) {
+            GetCartGoodsDetailsVo cartListVo = new GetCartGoodsDetailsVo();
+            BeanUtils.copyProperties(listVo, cartListVo);
+            cartListVo.setGoodsIntegralTotal(listVo.getGoodsIntegral() * listVo.getQuantity());
+            goodsDetailsVos.add(cartListVo);
+        }
+        if (ObjectUtils.isNotEmpty(param.getAddressId())) {
+            FsUserAddress address = fsUserAddressMapper.selectFsUserAddressByAddressId(param.getAddressId());
+            vos.setUserName(address.getRealName());
+            vos.setUserAddress(address.getProvince() + address.getCity() + address.getDistrict() + address.getDetail());
+            vos.setUserPhone(address.getPhone());
+            vos.setAddressId(address.getAddressId());
+        }
+        vos.setUserId(cartListVoList.get(0).getUserId());
+        vos.setUserIntegral(cartListVoList.get(0).getUserIntegral());
+        vos.setGoodsIntegralTotal(goodsDetailsVos.stream().filter(n -> ObjectUtils.isNotEmpty(n.getGoodsIntegralTotal())).mapToLong(GetCartGoodsDetailsVo::getGoodsIntegralTotal).sum());
+        vos.setGoodsDetailsVoList(goodsDetailsVos);
+        return vos;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean addGoodsIntoCart(AddGoodsIntoCartParam param, Long userId) {
+        // 查询现有购物车项
+        FsIntegralCart existingCart = baseMapper.selectOne(Wrappers.<FsIntegralCart>lambdaQuery().eq(FsIntegralCart::getUserId, userId).eq(FsIntegralCart::getGoodsId, param.getGoodsId()));
+        int finalQuantity;
+        // 查询商品最新状态
+        FsIntegralGoods freshGoods = fsIntegralGoodsMapper.selectFsIntegralGoodsByGoodsId(param.getGoodsId());
+        if (freshGoods == null || freshGoods.getStatus() != 1) {
+            try {
+                int deleteCount = baseMapper.delete(Wrappers.<FsIntegralCart>lambdaQuery().eq(FsIntegralCart::getUserId, userId).eq(FsIntegralCart::getGoodsId, param.getGoodsId()));
+                log.info("清除下架商品,userId:{}, goodsId:{},清除数量:{}", userId, param.getGoodsId(), deleteCount);
+            } catch (Exception e) {
+                log.error("清除下架商品失败,userId:{}, goodsId:{}", userId, param.getGoodsId(), e);
+            }
+            throw new ServiceException(String.format("商品[名称:%d]不存在或已下架", freshGoods.getGoodsName()));
+        }
+        int maxAllowed = Math.min((int) freshGoods.getStock().longValue(), 99);
+        if (existingCart != null) {
+            // 先做校验
+            finalQuantity = existingCart.getCartNum() + param.getQuantity();
+            if (finalQuantity > maxAllowed) {
+                if (existingCart.getCartNum() >= maxAllowed) {
+                    throw new ServiceException(String.format("商品已达最大购买数量%d件", maxAllowed));
+                }
+                int canAdd = maxAllowed - existingCart.getCartNum();
+                throw new ServiceException(String.format("最多还能购买%d件", canAdd));
+            }
+            int affectedRows = baseMapper.updateQuantityAtomically(userId, param.getGoodsId(), param.getQuantity(), maxAllowed);
+            if (affectedRows == 0) {
+                throw new ServiceException("添加失败,请重试");
+            }
+            return true;
+        } else {
+            // 新增购物车
+            if (param.getQuantity() > maxAllowed && param.getQuantity() > freshGoods.getStock()) {
+                throw new ServiceException(String.format("最多可购买%d件", freshGoods.getStock()));
+            }
+            try {
+                FsIntegralCart newCart = FsIntegralCart.builder().userId(userId).goodsId(param.getGoodsId()).cartNum(param.getQuantity()).isSelected(param.getIsSelected()).build();
+                return this.save(newCart);
+            } catch (DuplicateKeyException e) {
+                log.info("购物车并发插入,转为更新,userId:{}, goodsId:{}", userId, param.getGoodsId());
+                int affectedRows = baseMapper.updateQuantityAtomically(userId, param.getGoodsId(), param.getQuantity(), maxAllowed);
+                return affectedRows > 0;
+            }
+        }
+    }
+
+    /**
+     * 从购物车生成订单,兑换按钮(走的以前逻辑,没有重新设计)
+     *
+     * @param param
+     * @param userId
+     * @return
+     */
+    @Override
+    public R createOrderFromCart(CreateOrderFromCartParm param, Long userId) {
+        RedissonClient redissonClient = SpringUtils.getBean(RedissonClient.class);
+        String lockKey = "fsIntegralCartOrderCreate:" + userId;
+        RLock lock = redissonClient.getLock(lockKey);
+        try {
+            // 尝试获取锁,最多等待3秒,持有锁时间最多30秒
+            boolean isLocked = lock.tryLock(3, 10, TimeUnit.SECONDS);
+            if (!isLocked) {
+                return R.error("系统繁忙,请稍后再试");
+            }
+            FsUser user = userMapper.selectFsUserByUserId(userId);
+            FsUserAddress address = fsUserAddressMapper.selectFsUserAddressByAddressId(param.getAddressId());
+            List<FsIntegralGoodsVo> fsIntegralGoods = fsIntegralGoodsMapper.selectAllByGoodsIds(new HashSet<>(param.getGoodsId()));
+            List<FsIntegralCart> existingCart = baseMapper.selectList(Wrappers.<FsIntegralCart>lambdaQuery().eq(FsIntegralCart::getUserId, userId).in(FsIntegralCart::getGoodsId, param.getGoodsId()));
+            Map<Long, Integer> collect = existingCart.stream().collect(Collectors.groupingBy(FsIntegralCart::getGoodsId, Collectors.summingInt(FsIntegralCart::getCartNum)));
+            StringBuilder quantity = new StringBuilder();
+            for (FsIntegralGoodsVo fsIntegralGood : fsIntegralGoods) {
+                Integer integer = collect.get(fsIntegralGood.getGoodsId());
+                if (fsIntegralGood.getStock() < integer) {
+                    throw new ServiceException(String.format("%d库存不足,兑换失败", fsIntegralGood.getGoodsName()));
+                }
+                if (fsIntegralGood.getStatus() != 1) {
+                    this.remove(Wrappers.<FsIntegralCart>lambdaQuery().eq(FsIntegralCart::getUserId, userId).eq(FsIntegralCart::getGoodsId, fsIntegralGood.getGoodsId()));
+                    log.info("清除下架商品,userId:{}, goodsId:{}", userId, fsIntegralGood.getGoodsId());
+                    throw new ServiceException(String.format("商品[名称:%d]已下架,兑换失败", fsIntegralGood.getGoodsName()));
+                }
+                fsIntegralGood.setIntegralByNum(integer * fsIntegralGood.getIntegral());
+                fsIntegralGood.setQuantity(integer);
+                if (quantity.length() > 0) {
+                    quantity.append(",");
+                }
+                quantity.append(ObjectUtils.isNotEmpty(integer) ? integer : "0");
+            }
+            // 商品总的积分
+            Long goodsIntegral = fsIntegralGoods.stream().filter(n -> ObjectUtils.isNotEmpty(n.getIntegralByNum())).mapToLong(FsIntegralGoodsVo::getIntegralByNum).sum();
+            if (user.getIntegral() < goodsIntegral) {
+                throw new ServiceException("用户积分不足,兑换失败");
+            }
+            String barCode = fsIntegralGoods.stream().map(FsIntegralGoodsVo::getBarCode).collect(Collectors.joining(","));
+            String Integral = fsIntegralGoods.stream().map(m -> m.getIntegral().toString()).collect(Collectors.joining(","));
+            String orderSn = OrderCodeUtils.getOrderSn();
+            if (StringUtils.isEmpty(orderSn)) {
+                throw new ServiceException("订单生成失败,请重试");
+            }
+            FsIntegralOrder order = new FsIntegralOrder();
+            order.setOrderCode(orderSn);
+            order.setUserId(user.getUserId());
+            order.setStatus(1);
+            order.setBarCodeCart(barCode);
+            order.setIntegral(goodsIntegral.toString());
+            order.setIntegralByCart(Integral);
+            order.setItemCartJson(ObjectUtils.isNotEmpty(fsIntegralGoods) ? JSONUtil.toJsonStr(fsIntegralGoods) : null);
+            order.setUserName(address.getRealName());
+            order.setUserAddress(address.getProvince() + address.getCity() + address.getDistrict() + address.getDetail());
+            order.setUserPhone(address.getPhone());
+            order.setCreateTime(new Date());
+            order.setQuantityCart(quantity.toString());
+            if (fsIntegralOrderMapper.insertFsIntegralOrder(order) > 0) {
+                //写入日志
+                FsUser userMap = new FsUser();
+                userMap.setUserId(user.getUserId());
+                // 可消费积分
+                long consumer = user.getIntegral() - user.getWithdrawIntegral();
+                if (consumer < goodsIntegral) {
+                    // 扣除完可消费积分后,剩余的积分
+                    long extra = goodsIntegral - consumer;
+                    // 可提现积分扣除 剩余积分
+                    Long withdrawIntegral = user.getWithdrawIntegral() - extra;
+                    userMap.setIntegral(withdrawIntegral);
+                    userMap.setWithdrawIntegral(withdrawIntegral);
+                } else {
+                    userMap.setIntegral(user.getIntegral() - goodsIntegral);
+                }
+                userMapper.updateFsUser(userMap);
+                FsUserIntegralLogs logs = new FsUserIntegralLogs();
+                logs.setIntegral(-goodsIntegral);
+                logs.setUserId(order.getUserId());
+                logs.setBalance(userMap.getIntegral());
+                logs.setLogType(5);
+                logs.setBusinessId(order.getOrderId().toString());
+                logs.setCreateTime(new Date());
+                logs.setNickName(user.getNickName());
+                logs.setPhone(user.getPhone());
+                integralLogsService.insertFsUserIntegralLogs(logs);
+                //清空购物车对应商品
+                this.remove(Wrappers.<FsIntegralCart>lambdaQuery().eq(FsIntegralCart::getUserId, userId).in(FsIntegralCart::getGoodsId, param.getGoodsId()));
+                return R.ok("兑换成功").put("order", order);
+            } else {
+                return R.error("订单创建失败");
+            }
+        } catch (Exception e) {
+            return R.error(e.getMessage());
+        } finally {
+            if (lock.isHeldByCurrentThread()) {
+                lock.unlock();
+            }
+        }
+    }
 }
 }

+ 454 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsIntegralRedPacketLogServiceImpl.java

@@ -0,0 +1,454 @@
+package com.fs.his.service.impl;
+
+import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.his.config.AppConfig;
+import com.fs.his.config.IntegralConfig;
+import com.fs.his.domain.FsIntegralRedPacketLog;
+import com.fs.his.domain.FsUser;
+import com.fs.his.mapper.FsIntegralRedPacketLogMapper;
+import com.fs.his.mapper.FsUserMapper;
+import com.fs.his.param.FsIntegralRedPacketLogParam;
+import com.fs.his.service.IFsIntegralRedPacketLogService;
+import com.fs.his.vo.FsIntegralRedPacketLogVo;
+import com.fs.system.service.ISysConfigService;
+import com.github.binarywang.wxpay.bean.transfer.TransferBillsGetResult;
+import com.github.binarywang.wxpay.bean.transfer.TransferBillsNotifyResult;
+import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.TransferService;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+
+/**
+ * 积分佣金红包记录Service业务层处理
+ * 
+ * @author fs
+ * @date 2026-01-22
+ */
+@Service
+@Slf4j
+public class FsIntegralRedPacketLogServiceImpl extends ServiceImpl<FsIntegralRedPacketLogMapper, FsIntegralRedPacketLog> implements IFsIntegralRedPacketLogService {
+
+    @Autowired
+    private ISysConfigService configService;
+    @Autowired
+    private FsUserMapper fsUserMapper;
+    @Autowired
+    @Qualifier("threadPoolTaskExecutor")
+    private ThreadPoolTaskExecutor taskExecutor;
+
+    /**
+     * 查询积分佣金红包记录
+     * 
+     * @param logId 积分佣金红包记录主键
+     * @return 积分佣金红包记录
+     */
+    @Override
+    public FsIntegralRedPacketLog selectFsIntegralRedPacketLogByLogId(Long logId)
+    {
+        FsIntegralRedPacketLog log = baseMapper.selectFsIntegralRedPacketLogByLogId(logId);
+        if(log!=null && log.getOutBatchNo()!=null){
+            return getRedPacketLogByCode(log.getOutBatchNo());
+        }
+
+        return log;
+    }
+
+    /**
+     * 查询积分佣金红包记录列表
+     * 
+     * @param fsIntegralRedPacketLog 积分佣金红包记录
+     * @return 积分佣金红包记录
+     */
+    @Override
+    public List<FsIntegralRedPacketLog> selectFsIntegralRedPacketLogList(FsIntegralRedPacketLog fsIntegralRedPacketLog)
+    {
+        //1.
+        List<FsIntegralRedPacketLog> list = baseMapper.selectFsIntegralRedPacketLogList(fsIntegralRedPacketLog);
+        if(list!=null && !list.isEmpty()){
+            String json = configService.selectConfigByKey("his.integral");
+            IntegralConfig config = JSONUtil.toBean(json, IntegralConfig.class);
+
+            WxPayConfig payConfig = new WxPayConfig();
+            BeanUtils.copyProperties(config, payConfig);
+            WxPayService wxPayService = new WxPayServiceImpl();
+            wxPayService.setConfig(payConfig);
+            TransferService transferService = wxPayService.getTransferService();
+            Date oneMonthAgo = DateUtils.addMonths(new Date(), -1);
+            for (FsIntegralRedPacketLog redPacketLog : list) {
+                String outBatchNo = redPacketLog.getOutBatchNo();
+                if( "0".equals(redPacketLog.getStatus()) &&
+                        StringUtils.isNotBlank(outBatchNo) &&
+                        redPacketLog.getCreateTime().after(oneMonthAgo)){
+                    try {
+                        TransferBillsGetResult result = transferService.getBillsByOutBillNo(outBatchNo);
+                        if (result != null) {
+                            // 直接修改内存中的对象(前端能立即看到最新状态)
+                            syncLogToMemory(redPacketLog, result);
+                        }
+                    } catch (WxPayException e) {
+                        log.error("查询红包状态失败, outBatchNo: {}", outBatchNo, e);
+                    }
+                }
+            }
+        }
+
+        return list;
+    }
+
+
+    @Override
+    public List<FsIntegralRedPacketLogVo> getList(FsIntegralRedPacketLogParam param) {
+        return baseMapper.getList(param);
+    }
+
+    /**
+     * 同步状态到内存对象(即时返回给前端)
+     * 保留原remark,添加最新状态备注
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public void syncLogToMemory(FsIntegralRedPacketLog redPacketLog, TransferBillsGetResult result) {
+        String state = result.getState();
+        Date now = new Date();
+
+        switch (state) {
+            case "SUCCESS":
+                redPacketLog.setStatus("1");
+                redPacketLog.setBatchId(result.getTransferBillNo());
+                redPacketLog.setRemark("转账成功");
+                redPacketLog.setUpdateTime(now);
+                baseMapper.updateFsIntegralRedPacketLog(redPacketLog);
+                break;
+            case "FAIL":
+                redPacketLog.setStatus("-1");
+                redPacketLog.setRemark("失败原因:" + result.getFailReason());
+                redPacketLog.setUpdateTime(now);
+                updateUser(redPacketLog); // 回滚用户金额
+                redPacketLog.setReturnedStatus(1);
+                baseMapper.updateFsIntegralRedPacketLog(redPacketLog);
+                break;
+            case "CANCELLED":
+                redPacketLog.setStatus("-2");
+                redPacketLog.setRemark("转账撤销完成");
+                redPacketLog.setUpdateTime(now);
+                updateUser(redPacketLog); // 回滚用户金额
+                redPacketLog.setReturnedStatus(1);
+                baseMapper.updateFsIntegralRedPacketLog(redPacketLog);
+                break;
+            case "ACCEPTED":
+                redPacketLog.setStatus("2");
+                redPacketLog.setRemark("转账已受理");
+                break;
+            case "PROCESSING":
+                redPacketLog.setStatus("3");
+                redPacketLog.setRemark("转账处理中");
+                break;
+            case "WAIT_USER_CONFIRM":
+                redPacketLog.setStatus("4");
+                redPacketLog.setRemark("待收款用户确认,可拉起微信收款确认页面进行收款确认");
+                break;
+            case "TRANSFERING":
+                redPacketLog.setStatus("5");
+                redPacketLog.setRemark("转账结果尚未明确,可拉起微信收款确认页面再次重试确认收款");
+                break;
+            case "CANCELING":
+                redPacketLog.setStatus("-3");
+                redPacketLog.setRemark("商户撤销请求受理成功,该笔转账正在撤销中");
+                break;
+            default:
+                break;
+        }
+    }
+
+
+    /**
+     * 构建数据库更新对象(仅终态,且不覆盖原remark,只添加状态标记)
+     */
+    private FsIntegralRedPacketLog buildDBUpdateLog(FsIntegralRedPacketLog log, TransferBillsGetResult result) {
+        String state = result.getState();
+        Date now = new Date();
+        FsIntegralRedPacketLog updateLog = new FsIntegralRedPacketLog();
+        updateLog.setLogId(log.getLogId());
+        updateLog.setUpdateTime(now);
+
+        // 数据库中只保存简洁的状态标记,不保存完整的流程备注
+        switch (state) {
+            case "SUCCESS":
+                updateLog.setStatus("1");
+                updateLog.setBatchId(result.getTransferBillNo());
+                // 数据库remark保持原样或置空,不保存"转账成功"字样(避免重复)
+                break;
+            case "FAIL":
+                updateLog.setStatus("-1");
+                // 数据库只保存失败原因,不叠加历史备注
+                updateLog.setRemark("失败:" + result.getFailReason());
+                break;
+            case "CANCELLED":
+                updateLog.setStatus("-2");
+                updateLog.setRemark("转账已撤销");
+                break;
+            default:
+                return null;
+        }
+        return updateLog;
+    }
+
+    private boolean isTerminalStatus(String state) {
+        return "SUCCESS".equals(state) || "FAIL".equals(state) || "CANCELLED".equals(state);
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public void updateUser(FsIntegralRedPacketLog redPacketLog) {
+        Long userId = redPacketLog.getUserId();
+        FsUser fsUser = fsUserMapper.selectFsUserByUserId(userId);
+        if (fsUser == null) {
+            log.error("用户不存在, userId:{}", userId);
+            return;
+        }
+        FsUser updatefsUser = new FsUser();
+        updatefsUser.setUserId(userId);
+        BigDecimal fenWithdraw = redPacketLog.getAmount().multiply(BigDecimal.valueOf(100)).setScale(2, RoundingMode.HALF_DOWN);
+        BigDecimal mayWithdraw = fsUser.getMayWithdraw();
+        mayWithdraw = mayWithdraw.add(fenWithdraw);
+        updatefsUser.setMayWithdraw(mayWithdraw);
+        updatefsUser.setWithdrawFinish(fsUser.getWithdrawFinish().subtract(fenWithdraw));
+        fsUserMapper.updateFsUser(updatefsUser);
+        log.info("提现失败,回滚佣金,logId:{},参数:{}",redPacketLog.getLogId(), JSON.toJSONString(redPacketLog));
+    }
+
+    /**
+     * 新增积分佣金红包记录
+     * 
+     * @param fsIntegralRedPacketLog 积分佣金红包记录
+     * @return 结果
+     */
+    @Override
+    public int insertFsIntegralRedPacketLog(FsIntegralRedPacketLog fsIntegralRedPacketLog)
+    {
+        fsIntegralRedPacketLog.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertFsIntegralRedPacketLog(fsIntegralRedPacketLog);
+    }
+
+    /**
+     * 修改积分佣金红包记录
+     * 
+     * @param fsIntegralRedPacketLog 积分佣金红包记录
+     * @return 结果
+     */
+    @Override
+    public int updateFsIntegralRedPacketLog(FsIntegralRedPacketLog fsIntegralRedPacketLog)
+    {
+        fsIntegralRedPacketLog.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateFsIntegralRedPacketLog(fsIntegralRedPacketLog);
+    }
+
+    /**
+     * 批量删除积分佣金红包记录
+     * 
+     * @param logIds 需要删除的积分佣金红包记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsIntegralRedPacketLogByLogIds(Long[] logIds)
+    {
+        return baseMapper.deleteFsIntegralRedPacketLogByLogIds(logIds);
+    }
+
+    /**
+     * 删除积分佣金红包记录信息
+     * 
+     * @param logId 积分佣金红包记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsIntegralRedPacketLogByLogId(Long logId)
+    {
+        return baseMapper.deleteFsIntegralRedPacketLogByLogId(logId);
+    }
+
+    @Override
+    public R syncRedPacket(String outBatchNo, String batchId) {
+        FsIntegralRedPacketLog log = baseMapper.selectFsIntegralRedPacketLogByBatchNo(outBatchNo);
+        if (log!=null){
+            if ("1".equals(log.getStatus()) && batchId.equals(log.getBatchId())){
+                return R.ok();
+            }
+            log.setStatus("1");
+            log.setUpdateTime(new Date());
+            log.setBatchId(batchId);
+            baseMapper.updateFsIntegralRedPacketLog(log);
+            return R.ok();
+        }
+        return R.error("批次不存在");
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public R syncErrorRedPacket(TransferBillsNotifyResult.DecryptNotifyResult result) {
+        String state = result.getState();
+        if ("FAIL".equals(state) || "CANCELLED".equals(state)) {
+            String outBillNo = result.getOutBillNo();
+            if (StringUtils.isNotBlank(outBillNo)) {
+                FsIntegralRedPacketLog log = baseMapper.selectFsIntegralRedPacketLogByBatchNo(outBillNo);
+                if (log!=null){
+                    if ("-1".equals(log.getStatus()) || "-2".equals(log.getStatus())){
+                        return R.ok();
+                    }
+                    log.setStatus("FAIL".equals(state)?"-1":"-2");
+                    log.setUpdateTime(new Date());
+                    log.setRemark("失败原因:" +result.getFailReason());
+                    if (StringUtils.isNotBlank(log.getBatchId())){
+                        log.setBatchId(log.getBatchId());
+                    }
+                    updateUser(log);
+                    log.setReturnedStatus(1);
+                    baseMapper.updateFsIntegralRedPacketLog(log);
+                    return R.ok();
+                }
+                return R.error("批次不存在");
+            }
+        }
+        return R.error();
+    }
+
+    @Override
+    public FsIntegralRedPacketLog getRedPacketLogByCode(String orderCode) {
+        FsIntegralRedPacketLog redPacketLog = baseMapper.selectFsIntegralRedPacketLogByBatchNo(orderCode);
+        if (redPacketLog != null && "0".equals(redPacketLog.getStatus())) {
+            // 3. 获取微信配置
+            String json = configService.selectConfigByKey("app.config");
+            AppConfig config = JSONUtil.toBean(json, AppConfig.class);
+
+            WxPayConfig payConfig = new WxPayConfig();
+            BeanUtils.copyProperties(config, payConfig);
+            WxPayService wxPayService = new WxPayServiceImpl();
+            wxPayService.setConfig(payConfig);
+            TransferService transferService = wxPayService.getTransferService();
+            try {
+                redPacketLog = doExecute(transferService, redPacketLog);
+            } catch (Exception e) {
+                log.error("查询红包状态失败, outBatchNo: {}", redPacketLog.getOutBatchNo(), e);
+            }
+        }
+        return redPacketLog;
+    }
+
+    @Override
+    public void synchronizationWxMerchantPayStatus() {
+        FsIntegralRedPacketLogParam queryParam = new FsIntegralRedPacketLogParam();
+        Date oneMonthAgo = DateUtils.addMonths(new Date(), -1);
+        queryParam.setBeginTime(DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss",oneMonthAgo));
+        queryParam.setStatus("0");
+        List<FsIntegralRedPacketLog> list = baseMapper.selectFsIntegralRedPacketLogList(queryParam);
+        if (list!=null&& !list.isEmpty()){
+            String json = configService.selectConfigByKey("his.integral");
+            IntegralConfig config = JSONUtil.toBean(json, IntegralConfig.class);
+
+            WxPayConfig payConfig = new WxPayConfig();
+            BeanUtils.copyProperties(config, payConfig);
+            WxPayService wxPayService = new WxPayServiceImpl();
+            wxPayService.setConfig(payConfig);
+            TransferService transferService = wxPayService.getTransferService();
+
+            List<CompletableFuture<Void>> futures = list.stream()
+                    .map(redPacketLog -> CompletableFuture.runAsync(() -> {
+                        try {
+                            doExecute(transferService,redPacketLog);
+                        } catch (Exception e) {
+                            log.error("定时查询红包状态失败, redPacketLog: {}", JSONUtil.toJsonStr(redPacketLog), e);
+                        }
+                    }, taskExecutor))
+                    .collect(Collectors.toList());
+            // 6. 等待所有查询完成
+            try {
+                CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
+            } catch (Exception e) {
+                log.warn("定时查询微信状态超时,返回当前已获取的数据,剩余任务后台继续");
+            }
+        }
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public FsIntegralRedPacketLog doExecute(TransferService transferService, FsIntegralRedPacketLog redPacketLog) {
+        TransferBillsGetResult result = null;
+        try {
+            String outBatchNo = redPacketLog.getOutBatchNo();
+            if (StringUtils.isBlank(outBatchNo)) {
+                result = new TransferBillsGetResult();
+                result.setState("FAIL");
+                result.setFailReason("未生成订单号");
+            } else {
+                result = transferService.getBillsByOutBillNo(outBatchNo);
+            }
+
+            if (result != null) {
+                String state = result.getState();
+                Date now = new Date();
+                switch (state) {
+                    case "SUCCESS":
+                        redPacketLog.setStatus("1");
+                        redPacketLog.setBatchId(result.getTransferBillNo());
+                        redPacketLog.setRemark("转账成功");
+                        redPacketLog.setUpdateTime(now);
+                        baseMapper.updateFsIntegralRedPacketLog(redPacketLog);
+                        break;
+                    case "FAIL":
+                        redPacketLog.setStatus("-1");
+                        redPacketLog.setRemark("失败原因:" + result.getFailReason());
+                        redPacketLog.setUpdateTime(now);
+                        updateUser(redPacketLog); // 回滚用户金额
+                        redPacketLog.setReturnedStatus(1);
+                        baseMapper.updateFsIntegralRedPacketLog(redPacketLog);
+                        break;
+                    case "CANCELLED":
+                        redPacketLog.setStatus("-2");
+                        redPacketLog.setRemark("失败原因:转账撤销完成");
+                        redPacketLog.setUpdateTime(now);
+                        updateUser(redPacketLog); // 回滚用户金额
+                        redPacketLog.setReturnedStatus(1);
+                        baseMapper.updateFsIntegralRedPacketLog(redPacketLog);
+                    break;
+                    case "ACCEPTED":
+                        redPacketLog.setStatus("0");
+                        break;
+                    case "PROCESSING":
+                        redPacketLog.setStatus("0");
+                        break;
+                    case "WAIT_USER_CONFIRM":
+                        redPacketLog.setStatus("4");
+                        break;
+                    case "TRANSFERING":
+                        redPacketLog.setStatus("5");
+                        break;
+                    case "CANCELING":
+                        redPacketLog.setStatus("-3");
+                        break;
+                    default:
+                        break;
+                }
+            }
+        } catch (WxPayException e) {
+            throw new RuntimeException(e);
+        }
+        return redPacketLog;
+    }
+}

+ 5 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsStoreOrderServiceImpl.java

@@ -4626,4 +4626,9 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
         return fsStoreOrderItemMapper.selectPayPriceByYear(userId);
         return fsStoreOrderItemMapper.selectPayPriceByYear(userId);
     }
     }
 
 
+    @Override
+    public BigDecimal selectPayPriceAll(Long userId){
+        return fsStoreOrderItemMapper.selectPayPriceAll(userId);
+    }
+
 }
 }

+ 332 - 7
fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java

@@ -53,6 +53,8 @@ import com.fs.course.mapper.FsCoursePlaySourceConfigMapper;
 import com.fs.course.service.IFsCourseRedPacketLogService;
 import com.fs.course.service.IFsCourseRedPacketLogService;
 import com.fs.course.service.IFsUserCourseOrderService;
 import com.fs.course.service.IFsUserCourseOrderService;
 import com.fs.course.service.IFsUserVipOrderService;
 import com.fs.course.service.IFsUserVipOrderService;
+import com.fs.his.config.AppConfig;
+import com.fs.his.config.IntegralConfig;
 import com.fs.his.domain.*;
 import com.fs.his.domain.*;
 import com.fs.his.dto.PayConfigDTO;
 import com.fs.his.dto.PayConfigDTO;
 import com.fs.his.enums.FsStoreAfterSalesStatusEnum;
 import com.fs.his.enums.FsStoreAfterSalesStatusEnum;
@@ -258,6 +260,9 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
     @Autowired
     @Autowired
     private FsInquiryOrderMapper fsInquiryOrderMapper;
     private FsInquiryOrderMapper fsInquiryOrderMapper;
 
 
+    @Autowired
+    private IFsIntegralRedPacketLogService integralRedPacketLogService;
+
     /**
     /**
      * 红包账户锁
      * 红包账户锁
      */
      */
@@ -1279,7 +1284,6 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
         return fsStorePaymentMapper.selectFsStorePaymentExcelVOCount(fsStorePayment);
         return fsStorePaymentMapper.selectFsStorePaymentExcelVOCount(fsStorePayment);
     }
     }
 
 
-
     @Override
     @Override
     public R sendRedPacketV3(WxSendRedPacketParam param) {
     public R sendRedPacketV3(WxSendRedPacketParam param) {
         String json = configService.selectConfigByKey("redPacket.config");
         String json = configService.selectConfigByKey("redPacket.config");
@@ -1338,10 +1342,74 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
         }
         }
     }
     }
 
 
+    /**
+     *
+     * @param param
+     * @param config
+     * @param
+     * @return
+     */
+    @Override
+    public R sendRedPacketV3ByApp(WxSendRedPacketParam param,AppConfig config) {
+        WxPayConfig payConfig = new WxPayConfig();
+        BeanUtils.copyProperties(config, payConfig);
+        WxPayService wxPayService = new WxPayServiceImpl();
+        payConfig.setNotifyUrl(config.getNotifyUrl());
+        wxPayService.setConfig(payConfig);
+        TransferService transferService = wxPayService.getTransferService();
+
+        TransferBillsRequest request = new TransferBillsRequest();
+        request.setAppid(param.getAppId());
+        request.setOpenid(param.getOpenId());
+
+        String code =  OrderCodeUtils.getOrderSn();
+        if(StringUtils.isEmpty(code)){
+            return R.error("订单生成失败,请重试");
+        }
+//        String code = String.valueOf(IdUtil.getSnowflake(0, 0).nextId());
+        request.setOutBillNo("fsAppRed" + code);
+        if (param.getAmount() == null) {
+            return R.error();
+        }
+        Integer amount = WxPayUnifiedOrderRequest.yuanToFen(param.getAmount().toString());
+        request.setTransferAmount(amount);
+        request.setTransferRemark("积分提现");
+        request.setUserRecvPerception("活动奖励");
+        request.setNotifyUrl(config.getNotifyUrl());
+        request.setTransferSceneId("1000");
+
+        // 设置场景信息
+        List<TransferBillsRequest.TransferSceneReportInfo> transferSceneReportInfos = new ArrayList<>();
+        TransferBillsRequest.TransferSceneReportInfo info1 = new TransferBillsRequest.TransferSceneReportInfo();
+        info1.setInfoType("活动名称");
+        info1.setInfoContent("积分提现");
+        transferSceneReportInfos.add(info1);
+
+        TransferBillsRequest.TransferSceneReportInfo info2 = new TransferBillsRequest.TransferSceneReportInfo();
+        info2.setInfoType("奖励说明");
+        info2.setInfoContent("积分提现");
+        transferSceneReportInfos.add(info2);
+        request.setTransferSceneReportInfos(transferSceneReportInfos);
+
+
+        try {
+            logger.info("app商家转账开始:[param:{}]", request);
+            TransferBillsResult transferBillsResult = transferService.transferBills(request);
+            logger.info("Method...商家转账支付完成:[msg:{}]", transferBillsResult);
+            return R.ok("发送红包成功").put("data", transferBillsResult).put("mchId", config.getMchId())
+                    .put("package",transferBillsResult.getPackageInfo())
+                    .put("appId",param.getAppId())
+                    .put("orderCode",request.getOutBillNo());
+        } catch (Exception e) {
+            logger.error("app商家转账支付失败:参数: {} :原因: {}", request, e.getMessage(),e);
+            throw new RuntimeException(e);
+        }
+    }
+
 
 
     @Override
     @Override
     public String v3TransferNotify(String notifyData, HttpServletRequest request) {
     public String v3TransferNotify(String notifyData, HttpServletRequest request) {
-        logger.info("zyp \n【收到转账回调V3】:{}",notifyData);
+        logger.info("zyp \n【收到转账回调】:{}",notifyData);
         try {
         try {
             String json = configService.selectConfigByKey("redPacket.config");
             String json = configService.selectConfigByKey("redPacket.config");
             RedPacketConfig config = JSONUtil.toBean(json, RedPacketConfig.class);
             RedPacketConfig config = JSONUtil.toBean(json, RedPacketConfig.class);
@@ -1355,10 +1423,10 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
             signatureHeader.setNonce(request.getHeader("Wechatpay-Nonce"));
             signatureHeader.setNonce(request.getHeader("Wechatpay-Nonce"));
             signatureHeader.setSerial(request.getHeader("Wechatpay-Serial"));
             signatureHeader.setSerial(request.getHeader("Wechatpay-Serial"));
             signatureHeader.setSignature(request.getHeader("Wechatpay-Signature"));
             signatureHeader.setSignature(request.getHeader("Wechatpay-Signature"));
-            TransferBillsNotifyResult result = wxPayService.parseTransferBillsNotifyV3Result(notifyData,signatureHeader);
-            logger.info("到零钱回调1:{}",result.getResult());
-            if (result.getResult().getState().equals("SUCCESS")) {
-                R r = redPacketLogService.syncRedPacket(result.getResult().getOutBillNo(),result.getResult().getTransferBillNo());
+            WxPayTransferBatchesNotifyV3Result result = wxPayService.parseTransferBatchesNotifyV3Result(notifyData,signatureHeader);
+            logger.info("到零钱回调:{}",result.getResult());
+            if (result.getResult().getBatchStatus().equals("FINISHED") && result.getResult().getFailNum()==0) {
+                R r = redPacketLogService.syncRedPacket(result.getResult().getOutBatchNo(),result.getResult().getBatchId());
                 logger.info("result:{}",r);
                 logger.info("result:{}",r);
                 if (r.get("code").equals(200)){
                 if (r.get("code").equals(200)){
                     return WxPayNotifyResponse.success("处理成功");
                     return WxPayNotifyResponse.success("处理成功");
@@ -1369,7 +1437,6 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
                 return WxPayNotifyResponse.fail("");
                 return WxPayNotifyResponse.fail("");
             }
             }
         } catch (WxPayException e) {
         } catch (WxPayException e) {
-            e.printStackTrace();
             logger.error("zyp \n【转账回调异常】:{}", e.getReturnMsg());
             logger.error("zyp \n【转账回调异常】:{}", e.getReturnMsg());
             return WxPayNotifyResponse.fail(e.getMessage());
             return WxPayNotifyResponse.fail(e.getMessage());
         }
         }
@@ -1970,6 +2037,87 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
         return fsStorePaymentMapper.selectAllPayment();
         return fsStorePaymentMapper.selectAllPayment();
     }
     }
 
 
+    @Override
+    @Transactional
+    public R sendAppRedPacket(WxSendRedPacketParam param) {
+        //组合返回参数
+        R result = new R();
+        String json = configService.selectConfigByKey("app.config");
+        AppConfig config = JSONUtil.toBean(json, AppConfig.class);
+        if (config.getIsNew() != null && config.getIsNew() == 1) {
+            result = sendRedPacketV3ByApp(param, config);
+        } else {
+            result= sendRedPacketLegacy(param, config);
+        }
+
+        result.put("mchId", config.getMchId());
+        result.put("isNew",config.getIsNew());
+        logger.info("App提现返回:{}",result);
+        return result;
+    }
+
+    @Override
+    @Transactional
+    public R sendIntegralRedPacket(WxSendRedPacketParam param) {
+        //组合返回参数
+        R result = new R();
+        String json = configService.selectConfigByKey("app.config");
+        AppConfig config = JSONUtil.toBean(json, AppConfig.class);
+        if (config.getIsNew() != null && config.getIsNew() == 1) {
+            result = sendRedPacketV3Integral(param, config);
+        } else {
+            result= sendRedPacketLegacyIntegral(param, config);
+        }
+
+        result.put("mchId", config.getMchId());
+        result.put("isNew",config.getIsNew());
+        logger.info("积分提现返回:{}",result);
+        return result;
+    }
+
+    @Override
+    public String integralV3TransferNotify(String notifyData, HttpServletRequest request) {
+        logger.info("zyp \n【收到转账回调V3】:{}",notifyData);
+        try {
+            String json = configService.selectConfigByKey("app.config");
+            AppConfig config = JSONUtil.toBean(json, AppConfig.class);
+            //创建微信订单
+            WxPayConfig payConfig = new WxPayConfig();
+            BeanUtils.copyProperties(config,payConfig);
+            payConfig.setNotifyUrl(config.getWithdrawalNotifyUrl());
+            payConfig.setNotifyUrl(config.getWithdrawalNotifyUrl());
+            WxPayService wxPayService = new WxPayServiceImpl();
+            wxPayService.setConfig(payConfig);
+            SignatureHeader signatureHeader = new SignatureHeader();
+            signatureHeader.setTimeStamp(request.getHeader("Wechatpay-Timestamp"));
+            signatureHeader.setNonce(request.getHeader("Wechatpay-Nonce"));
+            signatureHeader.setSerial(request.getHeader("Wechatpay-Serial"));
+            signatureHeader.setSignature(request.getHeader("Wechatpay-Signature"));
+            TransferBillsNotifyResult result = wxPayService.parseTransferBillsNotifyV3Result(notifyData,signatureHeader);
+            logger.info("积分到零钱回调1:{}",result.getResult());
+            if (result == null){
+                return WxPayNotifyResponse.fail("");
+            }
+            if (result.getResult().getState().equals("SUCCESS")) {
+                R r = integralRedPacketLogService.syncRedPacket(result.getResult().getOutBillNo(),result.getResult().getTransferBillNo());
+                logger.info("result:{}",r);
+                if (r.get("code").equals(200)){
+                    return WxPayNotifyResponse.success("处理成功");
+                }else {
+                    return WxPayNotifyResponse.fail("");
+                }
+            }else {
+                //失败 退回佣金
+                integralRedPacketLogService.syncErrorRedPacket(result.getResult());
+                return WxPayNotifyResponse.success("处理成功");
+            }
+        } catch (WxPayException e) {
+            e.printStackTrace();
+            logger.error("zyp \n【转账回调异常】:{}", e.getReturnMsg());
+            return WxPayNotifyResponse.fail(e.getMessage());
+        }
+    }
+
     @Override
     @Override
     public R paymentByWxaCode(FsStorePaymentPayParam param) {
     public R paymentByWxaCode(FsStorePaymentPayParam param) {
         FsUser user = userMapper.selectFsUserById(param.getUserId());
         FsUser user = userMapper.selectFsUserById(param.getUserId());
@@ -2119,4 +2267,181 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
 
 
     }
     }
 
 
+    private R sendRedPacketLegacy(WxSendRedPacketParam param, AppConfig config) {
+        //如果服务号的配置存在,小程序红包接口可以使用服务号来发红包,重新赋值
+        //仅老商户支持
+        if (param.getOpenId()!=null && StringUtils.isNotEmpty(param.getAppId())){
+//            config.setAppId(param.getAppId());
+            param.setOpenId(param.getOpenId());
+        }
+        WxPayConfig payConfig = new WxPayConfig();
+        BeanUtils.copyProperties(config, payConfig);
+        payConfig.setNotifyUrl(config.getNotifyUrl());
+        WxPayService wxPayService = new WxPayServiceImpl();
+        wxPayService.setConfig(payConfig);
+        TransferService transferService = wxPayService.getTransferService();
+
+        TransferBatchesRequest request = new TransferBatchesRequest();
+//        request.setAppid(config.getAppId());
+
+
+        // todo 如果未配置负载均衡请还原原本的单号方式
+//        String code = IdUtil.getSnowflake(0, 0).nextIdStr();
+        String code =  OrderCodeUtils.getOrderSn();
+        if(StringUtils.isEmpty(code)){
+            return R.error("红包单号生成失败,请重试");
+        }
+        request.setOutBatchNo("fsIntegral" + code);
+        request.setBatchRemark("积分提现");
+        request.setBatchName("积分提现");
+        Integer amount = WxPayUnifiedOrderRequest.yuanToFen(param.getAmount().toString());
+        request.setTotalAmount(amount);
+        request.setTotalNum(1);
+        request.setNotifyUrl(config.getNotifyUrl());
+
+        ArrayList<TransferBatchesRequest.TransferDetail> transferDetailList = new ArrayList<>();
+        TransferBatchesRequest.TransferDetail transferDetail = new TransferBatchesRequest.TransferDetail();
+        transferDetail.setOpenid(param.getOpenId());
+        String code1 = IdUtil.getSnowflake(0, 0).nextIdStr();
+        transferDetail.setOutDetailNo("fsCourse" + code1);
+        transferDetail.setTransferAmount(amount);
+        transferDetail.setTransferRemark("积分提现成功!");
+        transferDetailList.add(transferDetail);
+        request.setTransferDetailList(transferDetailList);
+
+        try {
+            TransferBatchesResult transferBatchesResult = transferService.transferBatches(request);
+            return R.ok("积分提现成功").put("orderCode", transferBatchesResult.getOutBatchNo()).put("batchId", transferBatchesResult.getBatchId()).put("mchId", config.getMchId());
+        } catch (Exception e) {
+            logger.error("商家转账支付失败:参数: {} :原因: {}", com.alibaba.fastjson.JSON.toJSONString(param), e.getMessage(),e);
+            if (e instanceof WxPayException) {
+//            if (e instanceof WxPayException && "济南联志健康".equals(signProjectName)) {
+                WxPayException wxPayException = (WxPayException) e;
+                String customErrorMsg = wxPayException.getCustomErrorMsg();
+                if (null != customErrorMsg && customErrorMsg.startsWith("商户运营账户资金不足")) {
+                    return R.error("[积分提现] 账户余额不足,请联系管理员!");
+                }
+            }
+            throw new RuntimeException(e);
+        }
+    }
+
+    // 内部方法:处理新版本的发红包逻辑
+    private R sendRedPacketV3Integral(WxSendRedPacketParam param, AppConfig config) {
+
+        WxPayConfig payConfig = new WxPayConfig();
+        BeanUtils.copyProperties(config, payConfig);
+        payConfig.setNotifyUrl(config.getWithdrawalNotifyUrl());
+        payConfig.setAppId(param.getAppId());
+        WxPayService wxPayService = new WxPayServiceImpl();
+        wxPayService.setConfig(payConfig);
+        TransferService transferService = wxPayService.getTransferService();
+
+        TransferBillsRequest request = new TransferBillsRequest();
+        request.setAppid(param.getAppId());
+        request.setOpenid(param.getOpenId());
+
+        String code =  OrderCodeUtils.getOrderSn();
+        if(StringUtils.isEmpty(code)){
+            return R.error("订单生成失败,请重试");
+        }
+//        String code = String.valueOf(IdUtil.getSnowflake(0, 0).nextId());
+        request.setOutBillNo("fsIntegral" + code);
+        if (param.getAmount() == null) {
+            return R.error();
+        }
+        Integer amount = WxPayUnifiedOrderRequest.yuanToFen(param.getAmount().toString());
+        request.setTransferAmount(amount);
+        request.setTransferRemark("积分提现");
+        request.setUserRecvPerception("活动奖励");
+        request.setNotifyUrl(config.getNotifyUrl());
+        request.setTransferSceneId("1000");
+
+        // 设置场景信息
+        List<TransferBillsRequest.TransferSceneReportInfo> transferSceneReportInfos = new ArrayList<>();
+        TransferBillsRequest.TransferSceneReportInfo info1 = new TransferBillsRequest.TransferSceneReportInfo();
+        info1.setInfoType("活动名称");
+        info1.setInfoContent("积分提现");
+        transferSceneReportInfos.add(info1);
+
+        TransferBillsRequest.TransferSceneReportInfo info2 = new TransferBillsRequest.TransferSceneReportInfo();
+        info2.setInfoType("奖励说明");
+        info2.setInfoContent("积分提现");
+        transferSceneReportInfos.add(info2);
+        request.setTransferSceneReportInfos(transferSceneReportInfos);
+
+
+        try {
+            TransferBillsResult transferBillsResult = transferService.transferBills(request);
+            logger.info("商家转账支付完成:[msg:{}]", transferBillsResult);
+            return R.ok("发送红包成功").put("data", transferBillsResult).put("mchId", config.getMchId())
+                    .put("package",transferBillsResult.getPackageInfo())
+                    .put("appId",param.getAppId())
+                    .put("orderCode",request.getOutBillNo());
+        } catch (Exception e) {
+            logger.error("商家转账支付失败:参数: {} :原因: {}", com.alibaba.fastjson.JSON.toJSONString(param), e.getMessage(),e);
+            throw new RuntimeException(e);
+        }
+    }
+
+
+    private R sendRedPacketLegacyIntegral(WxSendRedPacketParam param, AppConfig config) {
+        //如果服务号的配置存在,小程序红包接口可以使用服务号来发红包,重新赋值
+        //仅老商户支持
+        if (param.getOpenId()!=null && StringUtils.isNotEmpty(param.getAppId())){
+//            config.setAppId(param.getAppId());
+            param.setOpenId(param.getOpenId());
+        }
+        WxPayConfig payConfig = new WxPayConfig();
+        BeanUtils.copyProperties(config, payConfig);
+        payConfig.setNotifyUrl(config.getWithdrawalNotifyUrl());
+        WxPayService wxPayService = new WxPayServiceImpl();
+        wxPayService.setConfig(payConfig);
+        TransferService transferService = wxPayService.getTransferService();
+
+        TransferBatchesRequest request = new TransferBatchesRequest();
+//        request.setAppid(config.getAppId());
+
+
+        // todo 如果未配置负载均衡请还原原本的单号方式
+//        String code = IdUtil.getSnowflake(0, 0).nextIdStr();
+        String code =  OrderCodeUtils.getOrderSn();
+        if(StringUtils.isEmpty(code)){
+            return R.error("红包单号生成失败,请重试");
+        }
+        request.setOutBatchNo("fsIntegral" + code);
+        request.setBatchRemark("积分提现");
+        request.setBatchName("积分提现");
+        Integer amount = WxPayUnifiedOrderRequest.yuanToFen(param.getAmount().toString());
+        request.setTotalAmount(amount);
+        request.setTotalNum(1);
+        request.setNotifyUrl(config.getNotifyUrl());
+
+        ArrayList<TransferBatchesRequest.TransferDetail> transferDetailList = new ArrayList<>();
+        TransferBatchesRequest.TransferDetail transferDetail = new TransferBatchesRequest.TransferDetail();
+        transferDetail.setOpenid(param.getOpenId());
+        String code1 = IdUtil.getSnowflake(0, 0).nextIdStr();
+        transferDetail.setOutDetailNo("fsCourse" + code1);
+        transferDetail.setTransferAmount(amount);
+        transferDetail.setTransferRemark("积分提现成功!");
+        transferDetailList.add(transferDetail);
+        request.setTransferDetailList(transferDetailList);
+
+        try {
+            TransferBatchesResult transferBatchesResult = transferService.transferBatches(request);
+            return R.ok("积分提现成功").put("orderCode", transferBatchesResult.getOutBatchNo()).put("batchId", transferBatchesResult.getBatchId()).put("mchId", config.getMchId());
+        } catch (Exception e) {
+            logger.error("商家转账支付失败:参数: {} :原因: {}", com.alibaba.fastjson.JSON.toJSONString(param), e.getMessage(),e);
+            if (e instanceof WxPayException) {
+//            if (e instanceof WxPayException && "济南联志健康".equals(signProjectName)) {
+                WxPayException wxPayException = (WxPayException) e;
+                String customErrorMsg = wxPayException.getCustomErrorMsg();
+                if (null != customErrorMsg && customErrorMsg.startsWith("商户运营账户资金不足")) {
+                    return R.error("[积分提现] 账户余额不足,请联系管理员!");
+                }
+            }
+            throw new RuntimeException(e);
+        }
+    }
+
 }
 }

+ 257 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsUserActiveLogServiceImpl.java

@@ -0,0 +1,257 @@
+package com.fs.his.service.impl;
+
+import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.core.domain.R;
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.DateUtils;
+import com.fs.his.domain.FsUser;
+import com.fs.his.domain.FsUserActiveLog;
+import com.fs.his.domain.FsUserIntegralLogs;
+import com.fs.his.dto.ActiveConfigDTO;
+import com.fs.his.mapper.FsUserActiveLogMapper;
+import com.fs.his.mapper.FsUserMapper;
+import com.fs.his.service.IFsStoreOrderService;
+import com.fs.his.service.IFsUserActiveLogService;
+import com.fs.his.service.IFsUserIntegralLogsService;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.service.ISysConfigService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 活动签到记录Service业务层处理
+ *
+ * @author fs
+ * @date 2025-09-23
+ */
+@Service
+public class FsUserActiveLogServiceImpl extends ServiceImpl<FsUserActiveLogMapper, FsUserActiveLog> implements IFsUserActiveLogService {
+    @Autowired
+    private IFsStoreOrderService storeOrderService;
+    @Autowired
+    private ISysConfigService configService;
+    @Autowired
+    private IFsUserIntegralLogsService userIntegralLogsService;
+    @Autowired
+    private FsUserMapper userMapper;
+    @Autowired
+    private FsUserActiveLogMapper userActiveLogMapper;
+    /**
+     * 查询活动签到记录
+     *
+     * @param id 活动签到记录主键
+     * @return 活动签到记录
+     */
+    @Override
+    public FsUserActiveLog selectFsUserActiveLogById(Long id)
+    {
+        return baseMapper.selectFsUserActiveLogById(id);
+    }
+
+    /**
+     * 查询活动签到记录列表
+     *
+     * @param fsUserActiveLog 活动签到记录
+     * @return 活动签到记录
+     */
+    @Override
+    public List<FsUserActiveLog> selectFsUserActiveLogList(FsUserActiveLog fsUserActiveLog)
+    {
+        return baseMapper.selectFsUserActiveLogList(fsUserActiveLog);
+    }
+
+    /**
+     * 新增活动签到记录
+     *
+     * @param fsUserActiveLog 活动签到记录
+     * @return 结果
+     */
+    @Override
+    public int insertFsUserActiveLog(FsUserActiveLog fsUserActiveLog)
+    {
+        fsUserActiveLog.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertFsUserActiveLog(fsUserActiveLog);
+    }
+
+    /**
+     * 修改活动签到记录
+     *
+     * @param fsUserActiveLog 活动签到记录
+     * @return 结果
+     */
+    @Override
+    public int updateFsUserActiveLog(FsUserActiveLog fsUserActiveLog)
+    {
+        fsUserActiveLog.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateFsUserActiveLog(fsUserActiveLog);
+    }
+
+    /**
+     * 批量删除活动签到记录
+     *
+     * @param ids 需要删除的活动签到记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsUserActiveLogByIds(Long[] ids)
+    {
+        return baseMapper.deleteFsUserActiveLogByIds(ids);
+    }
+
+    /**
+     * 删除活动签到记录信息
+     *
+     * @param id 活动签到记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsUserActiveLogById(Long id)
+    {
+        return baseMapper.deleteFsUserActiveLogById(id);
+    }
+
+    @Transactional
+    @Override
+    public Long userActiveLogSign(FsUser fsUser) {
+        //获取活动配置
+        SysConfig sysConfig= configService.selectConfigByConfigKey("active.config");
+        ActiveConfigDTO activeConfigDTO = JSONUtil.toBean(sysConfig.getConfigValue(), ActiveConfigDTO.class);
+        if ("0".equals(activeConfigDTO.getIsOpen())){
+            throw new CustomException("活动未开启");
+        }
+        // 判断今天是否在活动期间
+        LocalDate today = LocalDate.now();
+        if (today.isBefore(activeConfigDTO.getActiveStartTime())
+                || today.isAfter(activeConfigDTO.getActiveEndTime())) {
+            throw new CustomException("当前不在活动期间");
+        }
+        //查询用户消费额是否满足活动要求
+        BigDecimal totalPay = storeOrderService.selectPayPriceAll(fsUser.getUserId());
+        if (totalPay == null || totalPay.compareTo(activeConfigDTO.getMoneyTerm()) < 0) {
+            throw new CustomException("总消费金额未达到参与活动条件");
+        }
+        FsUserActiveLog userActiveLog = userActiveLogMapper.selectTodayIsSign(fsUser.getUserId());
+        if (userActiveLog!=null){
+            throw new CustomException("您今天已经领取过了");
+        }
+        //写入积分记录和活动记录表
+        FsUserActiveLog fsUserActiveLog = new FsUserActiveLog();
+        fsUserActiveLog.setActiveName(activeConfigDTO.getActiveName());
+        fsUserActiveLog.setTitle("活动奖励");
+        fsUserActiveLog.setNumber(activeConfigDTO.getActiveReward());
+        fsUserActiveLog.setBalance(fsUser.getIntegral());
+        fsUserActiveLog.setUserId(fsUser.getUserId());
+        fsUserActiveLog.setCreateTime(new Date());
+        baseMapper.insertFsUserActiveLog(fsUserActiveLog);
+        FsUserIntegralLogs logs = new FsUserIntegralLogs();
+        logs.setIntegral(activeConfigDTO.getActiveReward());
+        //logs.setBusinessType(fsUserActiveLog);
+        logs.setUserId(fsUser.getUserId());
+        logs.setBalance(fsUser.getIntegral());
+        logs.setLogType(25);
+        logs.setBusinessId(fsUserActiveLog.getId().toString());
+        logs.setCreateTime(new Date());
+        logs.setNickName(fsUser.getNickName());
+        logs.setPhone(fsUser.getPhone());
+        userIntegralLogsService.insertFsUserIntegralLogs(logs);
+        //用户积分增加
+        FsUser userMap =new  FsUser();
+        userMap.setIntegral(fsUser.getIntegral()+activeConfigDTO.getActiveReward());
+        userMap.setUserId(fsUser.getUserId());
+        //userMap.setSignNum(userSignNum);
+        Integer res =userMapper.updateFsUser(userMap);
+        if(res==0) {
+            throw new CustomException("签到失败");
+        }
+        return activeConfigDTO.getActiveReward();
+    }
+
+    @Override
+    public R getUserActiveLog(FsUser fsUser) {
+        SysConfig sysConfig = configService.selectConfigByConfigKey("active.config");
+        ActiveConfigDTO activeConfigDTO = JSONUtil.toBean(sysConfig.getConfigValue(), ActiveConfigDTO.class);
+
+        LocalDate start = activeConfigDTO.getActiveStartTime();
+        LocalDate end = activeConfigDTO.getActiveEndTime();
+
+        // 查询用户在活动期间的签到记录
+        List<FsUserActiveLog> logs = userActiveLogMapper.selectByUserAndDateRange(
+                fsUser.getUserId(),
+                LocalDateTime.of(start, LocalTime.MIN),
+                LocalDateTime.of(end, LocalTime.MAX)
+        );
+
+        // 转成 map,key=日期字符串,value=签到记录
+        Map<String, FsUserActiveLog> logMap = logs.stream()
+                .collect(Collectors.toMap(
+                        l -> l.getCreateTime().toInstant()
+                                .atZone(ZoneId.systemDefault())
+                                .toLocalDate()
+                                .toString(),
+                        Function.identity(),
+                        (a, b) -> a
+                ));
+
+        // 每天的签到状态
+        DateTimeFormatter dayFormatter = DateTimeFormatter.ofPattern("d");
+        List<Map<String, Object>> days = new ArrayList<>();
+        for (LocalDate d = start; !d.isAfter(end); d = d.plusDays(1)) {
+            FsUserActiveLog log = logMap.get(d.toString());
+            Map<String, Object> day = new HashMap<>();
+            day.put("date",  d.format(dayFormatter));
+            day.put("isSign", log != null);
+            day.put("number", log != null&&log.getNumber()!=null ? log.getNumber() : activeConfigDTO.getActiveReward());
+            day.put("title", log != null ? log.getTitle() : "");
+            days.add(day);
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("activeName", activeConfigDTO.getActiveName());
+        result.put("activeStartTime", start);
+        result.put("activeEndTime", end);
+        result.put("days", days);
+
+        return R.ok(result);
+    }
+
+    @Override
+    public boolean getUserActiveWindow(FsUser fsUser) {
+        //获取活动配置
+        SysConfig sysConfig= configService.selectConfigByConfigKey("active.config");
+        ActiveConfigDTO activeConfigDTO = JSONUtil.toBean(sysConfig.getConfigValue(), ActiveConfigDTO.class);
+        if ("0".equals(activeConfigDTO.getIsOpen())){
+            return false;
+        }
+        // 判断今天是否在活动期间
+        LocalDate today = LocalDate.now();
+        if (today.isBefore(activeConfigDTO.getActiveStartTime())
+                || today.isAfter(activeConfigDTO.getActiveEndTime())) {
+            return false;
+        }
+        //查询用户消费额是否满足活动要求
+        BigDecimal totalPay = storeOrderService.selectPayPriceAll(fsUser.getUserId());
+        if (totalPay == null || totalPay.compareTo(activeConfigDTO.getMoneyTerm()) < 0) {
+            return false;
+        }
+        //查询用户当天是否签到过
+        FsUserActiveLog userActiveLog = userActiveLogMapper.selectTodayIsSign(fsUser.getUserId());
+        if (userActiveLog!=null){
+            return false;
+        }
+        return true;
+    }
+
+
+}

+ 6 - 4
fs-service/src/main/java/com/fs/his/service/impl/FsUserIntegralLogsServiceImpl.java

@@ -16,10 +16,7 @@ import com.fs.his.mapper.AdProfitDetailMapper;
 import com.fs.his.mapper.FsUserIntegralLogsMapper;
 import com.fs.his.mapper.FsUserIntegralLogsMapper;
 import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.mapper.FsUserNewTaskMapper;
 import com.fs.his.mapper.FsUserNewTaskMapper;
-import com.fs.his.param.FsUserAddIntegralParam;
-import com.fs.his.param.FsUserAddIntegralTemplateParam;
-import com.fs.his.param.FsUserIntegralLogsListUParam;
-import com.fs.his.param.FsUserIntegralLogsParam;
+import com.fs.his.param.*;
 import com.fs.his.service.IFsUserIntegralLogsService;
 import com.fs.his.service.IFsUserIntegralLogsService;
 import com.fs.his.vo.FsUserIntegralLogsListUVO;
 import com.fs.his.vo.FsUserIntegralLogsListUVO;
 import com.fs.his.vo.FsUserIntegralLogsListVO;
 import com.fs.his.vo.FsUserIntegralLogsListVO;
@@ -636,4 +633,9 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
         map.put("isFinishFirstOrderPoint", isFinishFirstOrderPoint);
         map.put("isFinishFirstOrderPoint", isFinishFirstOrderPoint);
         return R.ok().put("data",map).put("isNewUser",isNewUser).put("createTime",createTime);
         return R.ok().put("data",map).put("isNewUser",isNewUser).put("createTime",createTime);
     }
     }
+
+    @Override
+    public Long sumIntegralByLogTypeAndCreateTime(Integer logType, AdProfitDetailStatisticsParam param) {
+        return fsUserIntegralLogsMapper.sumIntegralByLogTypeAndCreateTime(logType,param);
+    }
 }
 }

+ 207 - 4
fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java

@@ -9,6 +9,7 @@ import java.time.LocalDateTime;
 import java.util.*;
 import java.util.*;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 
 
@@ -29,6 +30,7 @@ import com.fs.common.core.domain.entity.SysDictData;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.enums.ImTypeEnum;
 import com.fs.common.enums.ImTypeEnum;
+import com.fs.common.exception.CustomException;
 import com.fs.common.param.LoginMaWxParam;
 import com.fs.common.param.LoginMaWxParam;
 import com.fs.common.utils.*;
 import com.fs.common.utils.*;
 import com.fs.company.cache.ICompanyTagCacheService;
 import com.fs.company.cache.ICompanyTagCacheService;
@@ -45,6 +47,7 @@ import com.fs.course.service.IFsUserCompanyUserService;
 import com.fs.course.vo.newfs.FsCourseAnalysisCountVO;
 import com.fs.course.vo.newfs.FsCourseAnalysisCountVO;
 import com.fs.course.vo.newfs.FsCourseAnalysisVO;
 import com.fs.course.vo.newfs.FsCourseAnalysisVO;
 import com.fs.course.vo.newfs.FsUserCourseVideoPageListVO;
 import com.fs.course.vo.newfs.FsUserCourseVideoPageListVO;
+import com.fs.his.config.AppConfig;
 import com.fs.his.config.IntegralConfig;
 import com.fs.his.config.IntegralConfig;
 import com.fs.his.domain.*;
 import com.fs.his.domain.*;
 import com.fs.his.domain.FsUserAddress;
 import com.fs.his.domain.FsUserAddress;
@@ -52,9 +55,7 @@ import com.fs.his.domain.FsUserIntegralLogs;
 import com.fs.his.dto.FindUsersByDTO;
 import com.fs.his.dto.FindUsersByDTO;
 import com.fs.his.enums.FsUserIntegralLogTypeEnum;
 import com.fs.his.enums.FsUserIntegralLogTypeEnum;
 import com.fs.his.mapper.*;
 import com.fs.his.mapper.*;
-import com.fs.his.param.FindUserByParam;
-import com.fs.his.param.FsUserAddIntegralTemplateParam;
-import com.fs.his.param.FsUserParam;
+import com.fs.his.param.*;
 import com.fs.his.service.*;
 import com.fs.his.service.*;
 import com.fs.his.utils.PhoneUtil;
 import com.fs.his.utils.PhoneUtil;
 import com.fs.his.utils.ProfitShareUtils;
 import com.fs.his.utils.ProfitShareUtils;
@@ -86,6 +87,7 @@ import com.fs.system.service.ISysConfigService;
 import com.fs.watch.domain.WatchUser;
 import com.fs.watch.domain.WatchUser;
 import com.fs.watch.domain.vo.FsUserAndCompanyAndDoctorVo;
 import com.fs.watch.domain.vo.FsUserAndCompanyAndDoctorVo;
 import com.fs.watch.service.WatchUserService;
 import com.fs.watch.service.WatchUserService;
+import com.github.binarywang.wxpay.bean.transfer.TransferBillsResult;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 import com.github.pagehelper.PageInfo;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
@@ -98,6 +100,8 @@ import org.apache.http.impl.client.CloseableHttpClient;
 import org.apache.http.impl.client.HttpClients;
 import org.apache.http.impl.client.HttpClients;
 import org.apache.http.util.Asserts;
 import org.apache.http.util.Asserts;
 import org.apache.http.util.EntityUtils;
 import org.apache.http.util.EntityUtils;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
 import org.slf4j.Logger;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.BeanUtils;
@@ -196,6 +200,14 @@ public class FsUserServiceImpl implements IFsUserService {
     private IFsUserIntegralLogsService fsUserIntegralLogsService;
     private IFsUserIntegralLogsService fsUserIntegralLogsService;
     @Autowired
     @Autowired
     private FsUserIntegralLogsMapper fsUserIntegralLogsMapper;
     private FsUserIntegralLogsMapper fsUserIntegralLogsMapper;
+    @Autowired
+    private FsIntegralRedPacketLogMapper integralRedPacketLogMapper;
+
+    @Autowired
+    private RedissonClient redissonClient;
+
+    @Autowired
+    private IFsStorePaymentService paymentService;
 
 
 
 
 
 
@@ -1534,6 +1546,24 @@ public class FsUserServiceImpl implements IFsUserService {
         return fsUserMapper.selectFsUserListLimit(fsUser);
         return fsUserMapper.selectFsUserListLimit(fsUser);
     }
     }
 
 
+    /**
+     * 处理用户与小程序的绑定
+     */
+    private void handleFsUserWxByAppid(FsUser user,String appId) {
+        FsUserWx fsUserWx = new FsUserWx();
+        fsUserWx.setType(1);
+        fsUserWx.setFsUserId(user.getUserId());
+        fsUserWx.setAppId(appId);
+        fsUserWx.setOpenId(user.getCourseMaOpenId());
+        fsUserWx.setUnionId(user.getUnionId());
+        fsUserWx.setCreateTime(new Date());
+        fsUserWx.setUpdateTime(new Date());
+        fsUserWxService.saveOrUpdateByUniqueKey(fsUserWx);
+
+        logger.info("zyp \n 【更新或插入用户与小程序{}的绑定关系】:{}", appId, user.getUserId());
+
+    }
+
     @Override
     @Override
     public void handleFsUserWx(FsUser user, LoginMaWxParam param, WxMaJscode2SessionResult session) {
     public void handleFsUserWx(FsUser user, LoginMaWxParam param, WxMaJscode2SessionResult session) {
         if (user == null) return;
         if (user == null) return;
@@ -1775,7 +1805,14 @@ public class FsUserServiceImpl implements IFsUserService {
         vo.setMayWithdraw(vo.getMayWithdraw().divide(BigDecimal.valueOf(100)));
         vo.setMayWithdraw(vo.getMayWithdraw().divide(BigDecimal.valueOf(100)));
         vo.setTotalCommission(vo.getTotalCommission().divide(BigDecimal.valueOf(100)));
         vo.setTotalCommission(vo.getTotalCommission().divide(BigDecimal.valueOf(100)));
         vo.setWithdrawFinish(vo.getWithdrawFinish().divide(BigDecimal.valueOf(100)));
         vo.setWithdrawFinish(vo.getWithdrawFinish().divide(BigDecimal.valueOf(100)));
-        vo.setWithdrawCash(new BigDecimal(vo.getWithdrawIntegral()).divide(BigDecimal.valueOf(1000),2,RoundingMode.DOWN));
+        //获取积分兑换佣金配置
+        String json = configService.selectConfigByKey("app.config");
+        AppConfig config = JSONUtil.toBean(json, AppConfig.class);
+        BigDecimal withdrawRatio = BigDecimal.valueOf(1000);
+        if (config != null && config.getWithdrawRatio()!=null) {
+            withdrawRatio = BigDecimal.valueOf(config.getIsOpenWithdraw());
+        }
+        vo.setWithdrawCash(new BigDecimal(vo.getWithdrawIntegral()).divide(withdrawRatio,2,RoundingMode.DOWN));
         return R.ok().put("data", vo);
         return R.ok().put("data", vo);
     }
     }
 
 
@@ -1848,4 +1885,170 @@ public class FsUserServiceImpl implements IFsUserService {
         return R.ok().put("data", rMap);
         return R.ok().put("data", rMap);
     }
     }
 
 
+    /**
+     * 用户提现
+     * @param param
+     */
+    @Override
+    @Transactional
+    public R withdrawal(FsIntegralWithdrawalParam param) {
+        Long userId = param.getUserId();
+        // 生成锁的key,基于用户ID和视频ID确保同一用户同一视频的请求被锁定
+        String lockKey = "reward_integral_lock:user:" + userId;
+        RLock lock = redissonClient.getLock(lockKey);
+
+        try {
+            // 尝试获取锁,等待时间5秒,锁过期时间30秒
+            boolean isLocked = lock.tryLock(5, 300, TimeUnit.SECONDS);
+            if (!isLocked) {
+                logger.warn("获取锁失败,用户ID:{}", userId);
+                return R.error("操作频繁,请稍后再试!");
+            }
+
+            logger.info("成功获取锁,开始处理奖励发放,用户ID:{}", userId);
+            return executeWithdrawal(param);
+
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            logger.error("获取锁被中断,用户ID:{}", userId, e);
+            return R.error("系统繁忙,请重试!");
+        } finally {
+            // 释放锁
+            if (lock.isHeldByCurrentThread()) {
+                lock.unlock();
+                logger.info("释放锁成功,用户ID:{}", userId);
+            }
+        }
+
+
+    }
+
+    private R executeWithdrawal(FsIntegralWithdrawalParam param){
+        //查询可提现
+        Long userId = param.getUserId();
+        FsUser fsUser = selectFsUserByUserId(userId);
+        if (fsUser == null) {
+            return R.error("用户不存在");
+        }
+        //拉黑用户不能提现
+        if (fsUser.getStatus() == 0){
+            return R.error("暂无法提现!");
+        }
+        BigDecimal mayWithdraw = fsUser.getMayWithdraw();
+        if (mayWithdraw == null || mayWithdraw.compareTo(BigDecimal.ZERO) <= 0) {
+            return R.error("用户无可提现金额");
+        }
+        BigDecimal applicationAmount = param.getApplicationAmount();
+        if (applicationAmount == null || applicationAmount.compareTo(BigDecimal.ZERO) <= 0) {
+            return R.error("用户提现金额错误");
+        }
+        String json = configService.selectConfigByKey("app.config");
+        AppConfig config = JSONUtil.toBean(json, AppConfig.class);
+        BigDecimal maxApplicationAmount = config.getMaxApplicationAmount();
+        if (maxApplicationAmount != null && maxApplicationAmount.compareTo(BigDecimal.ZERO) > 0 && applicationAmount.compareTo(maxApplicationAmount) > 0) {
+            return R.error("一次最多提现 "+ maxApplicationAmount + " 元");
+        }
+        if (mayWithdraw.compareTo(applicationAmount.multiply(BigDecimal.valueOf(100))) < 0) {
+            return R.error("用户可提现金额不足");
+        }
+        Integer withdrawNum = config.getWithdrawNum();
+        if (withdrawNum != null) {
+            Long count = integralRedPacketLogMapper.countLogsByToday(userId);
+            if (count != null && count >= withdrawNum) {
+                return R.error("当天最多领取 " + withdrawNum + "次 ,未领取成功的,可点击详情重新领取!");
+            }
+        }
+
+        // 保存log
+        FsIntegralRedPacketLog fsIntegralRedPacketLog = saveRedPacketLog(userId, applicationAmount,param.getAppId());
+        //来源是小程序切换openId
+        WxSendRedPacketParam packetParam = new WxSendRedPacketParam();
+        String openId = getOpenId(param, fsUser);
+        if (StringUtils.isBlank(openId)) {
+            fsIntegralRedPacketLog.setStatus("-1");
+            fsIntegralRedPacketLog.setRemark("失败原因:未使用微信登录");
+            fsIntegralRedPacketLog.setReturnedStatus(1);
+            integralRedPacketLogMapper.updateFsIntegralRedPacketLog(fsIntegralRedPacketLog);
+            return R.error("请重新使用微信登录");
+        }
+        packetParam.setOpenId(openId);
+        packetParam.setAmount(applicationAmount);
+        packetParam.setSource(param.getSource());
+        packetParam.setAppId(param.getAppId());
+        try {
+            //发送红包
+            R sendRedPacket = paymentService.sendIntegralRedPacket(packetParam);
+
+            if (sendRedPacket.get("code").equals(200)) {
+                TransferBillsResult transferBillsResult;
+                if (sendRedPacket.get("isNew").equals(1)) {
+                    transferBillsResult = (TransferBillsResult) sendRedPacket.get("data");
+                    fsIntegralRedPacketLog.setOutBatchNo(transferBillsResult.getOutBillNo());
+                    fsIntegralRedPacketLog.setBatchId(transferBillsResult.getTransferBillNo());
+                } else {
+                    fsIntegralRedPacketLog.setOutBatchNo(sendRedPacket.get("orderCode").toString());
+                    fsIntegralRedPacketLog.setBatchId(sendRedPacket.get("batchId").toString());
+                }
+                fsIntegralRedPacketLog.setUpdateTime(new Date());
+                Object aPackage = sendRedPacket.get("package");
+                if (aPackage != null) {
+                    fsIntegralRedPacketLog.setPackageInfo(aPackage.toString());
+                }
+                Object mchIdObj = sendRedPacket.get("mchId");
+                if (mchIdObj != null) {
+                    fsIntegralRedPacketLog.setMchId(mchIdObj.toString());
+                }
+                // 更新观看记录的奖励类型
+                integralRedPacketLogMapper.updateFsIntegralRedPacketLog(fsIntegralRedPacketLog);
+                //减少可兑换佣金
+                BigDecimal fenWithdraw = fsIntegralRedPacketLog.getAmount().multiply(BigDecimal.valueOf(100)).setScale(2, RoundingMode.HALF_UP);
+                mayWithdraw = mayWithdraw.subtract(fenWithdraw);
+                BigDecimal withdrawFinish = fsUser.getWithdrawFinish().add(fenWithdraw);
+                fsUser.setMayWithdraw(mayWithdraw);
+                fsUser.setWithdrawFinish(withdrawFinish);
+                log.info("佣金提现修改mayWithdraw:{},withdrawFinish:{}", mayWithdraw, fsUser.getWithdrawFinish());
+                fsUserMapper.updateFsUser(fsUser);
+                return sendRedPacket;
+            }
+        } catch (CustomException e) {
+            return R.error(e.getMessage());
+        }
+        return R.ok("发放成功");
+    }
+
+    private FsIntegralRedPacketLog saveRedPacketLog(Long userId, BigDecimal applicationAmount,String appId) {
+        FsIntegralRedPacketLog redPacketLog = new FsIntegralRedPacketLog();
+        redPacketLog.setUserId(userId);
+        redPacketLog.setAmount(applicationAmount);
+        redPacketLog.setStatus("0");
+        redPacketLog.setAppId(appId);
+        redPacketLog.setCreateTime(new Date());
+        integralRedPacketLogMapper.insertFsIntegralRedPacketLog(redPacketLog);
+        return redPacketLog;
+    }
+
+    /**
+     * 获取用户openId
+     */
+    private String getOpenId(FsIntegralWithdrawalParam param, FsUser user) {
+        if (param.getSource()==2){
+            FsUserWx fsUserWx = fsUserWxService.selectByAppIdAndUserId(param.getAppId(),user.getUserId(),1);
+            if (Objects.nonNull(fsUserWx) && StringUtils.isNotBlank(fsUserWx.getOpenId())) {
+                return fsUserWx.getOpenId();
+            }
+
+            if (StringUtils.isNotBlank(user.getCourseMaOpenId())) {
+                try {
+                    handleFsUserWxByAppid(user,param.getAppId());
+                } catch (Exception e){
+                    log.error("【更新或插入用户与小程序的绑定关系失败】:{}", user.getUserId(), e);
+                }
+                return user.getCourseMaOpenId();
+            }
+        } else if (param.getSource()==3){
+            return user.getAppOpenId();
+        }
+
+        return user.getMpOpenId();
+    }
 }
 }

+ 14 - 0
fs-service/src/main/java/com/fs/his/vo/AdProfitDetailStatisticsVo.java

@@ -0,0 +1,14 @@
+package com.fs.his.vo;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class AdProfitDetailStatisticsVo {
+    @Excel(name = "公司名称")
+    String name;
+    @Excel(name = "分佣金额")
+    BigDecimal amountYuan;
+}

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

@@ -0,0 +1,12 @@
+package com.fs.his.vo;
+
+import com.fs.common.annotation.Excel;
+import com.fs.his.domain.FsConsecutiveWithdrawRecord;
+import lombok.Data;
+
+@Data
+public class FsConsecutiveWithdrawRecordVo extends FsConsecutiveWithdrawRecord {
+    //用户昵称
+    @Excel(name = "用户昵称",sort = 2)
+    private String nickName;
+}

+ 50 - 0
fs-service/src/main/java/com/fs/his/vo/FsIntegralGoodsVo.java

@@ -0,0 +1,50 @@
+package com.fs.his.vo;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class FsIntegralGoodsVo {
+    private Long goodsId;
+
+    @ApiModelProperty("封面图")
+    private String imgUrl;
+
+    @ApiModelProperty("组图")
+    private String images;
+
+    @ApiModelProperty("商品名称")
+    private String goodsName;
+
+    @ApiModelProperty("原价")
+    private BigDecimal otPrice;
+
+    @ApiModelProperty("商品分类")
+    private Long goodsType;
+
+    @ApiModelProperty("状态")
+    private Long status;
+
+    @ApiModelProperty("单个商品积分")
+    private Long integral;
+
+    @ApiModelProperty("排序")
+    private Long sort;
+
+    @ApiModelProperty("库存")
+    private Long stock;
+
+    @ApiModelProperty("详情")
+    private String descs;
+
+    @ApiModelProperty("产品编码")
+    private String barCode;
+
+    @ApiModelProperty("加上数量的商品积分")
+    private Long integralByNum;
+
+    @ApiModelProperty("商品数量")
+    private Integer quantity;
+}

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

@@ -0,0 +1,13 @@
+package com.fs.his.vo;
+
+import com.fs.common.annotation.Excel;
+import com.fs.his.domain.FsIntegralRedPacketLog;
+import lombok.Data;
+
+@Data
+public class FsIntegralRedPacketLogVo extends FsIntegralRedPacketLog {
+    @Excel(name = "用户昵称",sort = 2)
+    private String nickName;
+
+
+}

+ 43 - 0
fs-service/src/main/java/com/fs/his/vo/GetCartGoodsDetailsVo.java

@@ -0,0 +1,43 @@
+package com.fs.his.vo;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+public class GetCartGoodsDetailsVo {
+    @ApiModelProperty("购物车ID")
+    private Long cartId;
+
+    @ApiModelProperty("商品id")
+    private Long goodsId;
+
+    @ApiModelProperty("封面图")
+    private String imgUrl;
+
+    @ApiModelProperty("组图")
+    private String images;
+
+    @ApiModelProperty("商品名称")
+    private String goodsName;
+
+    @ApiModelProperty("状态")
+    private Long status;
+
+    @ApiModelProperty("商品所需积分")
+    private Long goodsIntegral;
+
+    @ApiModelProperty("详情")
+    private String descs;
+
+    @ApiModelProperty("商品数量")
+    private Integer quantity;
+
+    @ApiModelProperty("是否选中:1是,0否")
+    private Integer isSelected;
+
+    @ApiModelProperty("创建时间(加入购物车时间)")
+    private String createTime;
+
+    @ApiModelProperty("商品所需积分之和")
+    private Long goodsIntegralTotal;
+}

+ 37 - 0
fs-service/src/main/java/com/fs/his/vo/GetFsIntegralCartDetailsVo.java

@@ -0,0 +1,37 @@
+package com.fs.his.vo;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+@Data
+public class GetFsIntegralCartDetailsVo {
+
+    @ApiModelProperty("地址id")
+    private Long addressId;
+
+    @ApiModelProperty("用户id")
+    private Long userId;
+
+    @ApiModelProperty("用户名称")
+    private String userName;
+
+    @ApiModelProperty("用户电话")
+    private String userPhone;
+
+    @ApiModelProperty("用户地址")
+    private String userAddress;
+
+    @ApiModelProperty("所有商品所需积分之和")
+    private Long goodsIntegralTotal;
+
+    @ApiModelProperty("用户的芳华币总数")
+    private BigDecimal userIntegral;
+
+    @ApiModelProperty("商品信息")
+    private List<GetCartGoodsDetailsVo> goodsDetailsVoList;
+
+
+}

+ 63 - 0
fs-service/src/main/java/com/fs/his/vo/GetFsIntegralCartListVo.java

@@ -0,0 +1,63 @@
+package com.fs.his.vo;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class GetFsIntegralCartListVo {
+    @ApiModelProperty("主键ID")
+    private Long cartId;
+
+    @ApiModelProperty("用户ID")
+    private Long userId;
+
+    @ApiModelProperty("积分商品ID")
+    private Long goodsId;
+
+    @ApiModelProperty("封面图")
+    private String imgUrl;
+
+    @ApiModelProperty("组图")
+    private String images;
+
+    @ApiModelProperty("商品名称")
+    private String goodsName;
+
+    @ApiModelProperty("原价")
+    private BigDecimal otPrice;
+
+    @ApiModelProperty("商品分类")
+    private Long goodsType;
+
+    @ApiModelProperty("状态")
+    private Long status;
+
+    @ApiModelProperty("商品所需积分")
+    private Long goodsIntegral;
+
+    @ApiModelProperty("库存")
+    private Long stock;
+
+    @ApiModelProperty("详情")
+    private String descs;
+
+    @ApiModelProperty("产品编码")
+    private String barCode;
+
+    @ApiModelProperty("商品数量")
+    private Integer quantity;
+
+    @ApiModelProperty("用户的芳华币总数")
+    private BigDecimal userIntegral;
+
+    @ApiModelProperty("是否选中:1是,0否")
+    private Integer isSelected;
+
+    @ApiModelProperty("创建时间(加入购物车时间)")
+    private String createTime;
+
+    @ApiModelProperty("更新时间")
+    private String updateTime;
+}

+ 2 - 1
fs-service/src/main/java/com/fs/qw/service/impl/LuckyBagServiceImpl.java

@@ -12,6 +12,7 @@ import com.fs.course.domain.LuckyBagCollectRecord;
 import com.fs.course.param.LuckyBagActualRewardsParam;
 import com.fs.course.param.LuckyBagActualRewardsParam;
 import com.fs.his.domain.FsUser;
 import com.fs.his.domain.FsUser;
 import com.fs.his.domain.FsUserIntegralLogs;
 import com.fs.his.domain.FsUserIntegralLogs;
+import com.fs.his.enums.FsUserIntegralLogTypeEnum;
 import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.param.FsReceiveLuckyBagParam;
 import com.fs.his.param.FsReceiveLuckyBagParam;
 import com.fs.his.service.impl.FsUserIntegralLogsServiceImpl;
 import com.fs.his.service.impl.FsUserIntegralLogsServiceImpl;
@@ -135,7 +136,7 @@ public class LuckyBagServiceImpl implements ILuckyBagService
 
 
         FsUserIntegralLogs fsUserIntegralLogs = new FsUserIntegralLogs();
         FsUserIntegralLogs fsUserIntegralLogs = new FsUserIntegralLogs();
         fsUserIntegralLogs.setUserId(userId);
         fsUserIntegralLogs.setUserId(userId);
-        fsUserIntegralLogs.setLogType(30); // 福袋获取获得芳华币
+        fsUserIntegralLogs.setLogType(FsUserIntegralLogTypeEnum.TYPE_33.getValue()); // 福袋获取获得芳华币
         fsUserIntegralLogs.setIntegral(coinAmount);
         fsUserIntegralLogs.setIntegral(coinAmount);
         fsUserIntegralLogs.setPhone(user.getPhone());
         fsUserIntegralLogs.setPhone(user.getPhone());
         fsUserIntegralLogs.setBalance(balance);
         fsUserIntegralLogs.setBalance(balance);

+ 17 - 2
fs-service/src/main/resources/mapper/company/CompanyRedPacketBalanceLogsMapper.xml

@@ -13,14 +13,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="balance"    column="balance"    />
         <result property="balance"    column="balance"    />
         <result property="logsType"    column="logs_type"    />
         <result property="logsType"    column="logs_type"    />
         <result property="status"    column="status"    />
         <result property="status"    column="status"    />
+        <result property="redPacketId"    column="red_packet_id"    />
     </resultMap>
     </resultMap>
 
 
     <sql id="selectCompanyRedPacketBalanceLogsVo">
     <sql id="selectCompanyRedPacketBalanceLogsVo">
-        select logs_id, company_id, money, remark, create_time, balance, logs_type, status from company_red_packet_balance_logs
+        select logs_id, company_id, money, remark, create_time, balance, logs_type, status,red_packet_id from company_red_packet_balance_logs
     </sql>
     </sql>
 
 
     <select id="selectCompanyRedPacketBalanceLogsList" parameterType="CompanyRedPacketBalanceLogs" resultMap="CompanyRedPacketBalanceLogsResult">
     <select id="selectCompanyRedPacketBalanceLogsList" parameterType="CompanyRedPacketBalanceLogs" resultMap="CompanyRedPacketBalanceLogsResult">
-        select l.logs_id, l.company_id, l.money, l.remark, l.create_time, l.balance, l.logs_type, l.status,c.company_name
+        select l.logs_id, l.company_id, l.money, l.remark, l.create_time, l.balance, l.logs_type, l.status,l.red_packet_id,c.company_name
         from
         from
         company_red_packet_balance_logs l
         company_red_packet_balance_logs l
         left join company c on c.company_id = l.company_id
         left join company c on c.company_id = l.company_id
@@ -40,6 +41,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <select id="getCompanyRedPacketBalance" resultType="com.fs.company.domain.Company">
     <select id="getCompanyRedPacketBalance" resultType="com.fs.company.domain.Company">
         SELECT * FROM company WHERE company_id = #{companyId}
         SELECT * FROM company WHERE company_id = #{companyId}
     </select>
     </select>
+    <select id="selectCompanyRedPacketBalanceLogsListByStatus"
+            resultType="com.fs.company.domain.CompanyRedPacketBalanceLogs">
+        <include refid="selectCompanyRedPacketBalanceLogsVo"/>  where logs_type = 15 and status = 0  and create_time &gt;= #{createSTime}  AND create_time &lt; #{createETime}
+    </select>
 
 
     <insert id="insertCompanyRedPacketBalanceLogs" parameterType="CompanyRedPacketBalanceLogs" useGeneratedKeys="true" keyProperty="logsId">
     <insert id="insertCompanyRedPacketBalanceLogs" parameterType="CompanyRedPacketBalanceLogs" useGeneratedKeys="true" keyProperty="logsId">
         insert into company_red_packet_balance_logs
         insert into company_red_packet_balance_logs
@@ -51,6 +56,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="balance != null">balance,</if>
             <if test="balance != null">balance,</if>
             <if test="logsType != null">logs_type,</if>
             <if test="logsType != null">logs_type,</if>
             <if test="status != null">status,</if>
             <if test="status != null">status,</if>
+            <if test="redPacketId != null">red_packet_id,</if>
          </trim>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="companyId != null">#{companyId},</if>
             <if test="companyId != null">#{companyId},</if>
@@ -60,6 +66,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="balance != null">#{balance},</if>
             <if test="balance != null">#{balance},</if>
             <if test="logsType != null">#{logsType},</if>
             <if test="logsType != null">#{logsType},</if>
             <if test="status != null">#{status},</if>
             <if test="status != null">#{status},</if>
+            <if test="redPacketId != null">#{redPacketId},</if>
          </trim>
          </trim>
     </insert>
     </insert>
 
 
@@ -73,9 +80,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="balance != null">balance = #{balance},</if>
             <if test="balance != null">balance = #{balance},</if>
             <if test="logsType != null">logs_type = #{logsType},</if>
             <if test="logsType != null">logs_type = #{logsType},</if>
             <if test="status != null">status = #{status},</if>
             <if test="status != null">status = #{status},</if>
+            <if test="redPacketId != null">red_packet_id = #{redPacketId},</if>
         </trim>
         </trim>
         where logs_id = #{logsId}
         where logs_id = #{logsId}
     </update>
     </update>
+    <update id="updateCompanyRedPacketBalanceLogsByRedPacketId">
+        update company_red_packet_balance_logs
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="status != null">status = #{status},</if>
+        </trim>
+        where red_packet_id = #{redPacketId} and logs_type = 15
+    </update>
 
 
     <delete id="deleteCompanyRedPacketBalanceLogsByLogsId" parameterType="Long">
     <delete id="deleteCompanyRedPacketBalanceLogsByLogsId" parameterType="Long">
         delete from company_red_packet_balance_logs where logs_id = #{logsId}
         delete from company_red_packet_balance_logs where logs_id = #{logsId}

+ 118 - 0
fs-service/src/main/resources/mapper/his/AdProfitDetailMapper.xml

@@ -130,6 +130,124 @@
             may_withdraw > 0.00
             may_withdraw > 0.00
            OR withdraw_finish > 0.00
            OR withdraw_finish > 0.00
     </select>
     </select>
+    <select id="statisticsList" resultType="com.fs.his.vo.AdProfitDetailStatisticsVo">
+        <!-- 1. 用户分润 -->
+        SELECT
+        '用户分润' AS `name`,
+        IFNULL(ROUND(SUM(a.user_money) / 100, 2), 0) AS amountYuan,
+        'summary' AS type,
+        1 AS sortOrder
+        FROM ad_profit_detail a
+        WHERE <include refid="timeCondition"/>
+
+        UNION ALL
+
+        <!-- 2. 互医分润 -->
+        SELECT
+        '互医分润' AS `name`,
+        IFNULL(ROUND(SUM(a.huyi_money) / 100, 2), 0) AS amountYuan,
+        'summary' AS type,
+        2 AS sortOrder
+        FROM ad_profit_detail a
+        WHERE <include refid="timeCondition"/>
+
+        UNION ALL
+
+        <!-- 3. 云联分润 -->
+        SELECT
+        '云联分润' AS `name`,
+        IFNULL(ROUND(SUM(a.yunlian_money) / 100, 2), 0) AS amountYuan,
+        'summary' AS type,
+        3 AS sortOrder
+        FROM ad_profit_detail a
+        WHERE <include refid="timeCondition"/>
+
+        UNION ALL
+
+        <!-- 4. 各公司分润 -->
+        SELECT
+        c.company_name AS `name`,
+        IFNULL(ROUND(SUM(a.company_money) / 100, 2), 0) AS amountYuan,
+        'company' AS type,
+        a.company_id + 100 AS sortOrder
+        FROM ad_profit_detail a
+        LEFT JOIN company c ON a.company_id = c.company_id
+        WHERE a.company_id IS NOT NULL
+        AND <include refid="timeCondition"/>
+        GROUP BY a.company_id, c.company_name
+
+        ORDER BY sortOrder, `name`
+    </select>
+
+    <sql id="timeCondition">
+        <choose>
+            <!-- type为null时,使用startTime和endTime -->
+            <when test="param.type == null">
+                a.create_time >= #{param.startTime}
+                AND a.create_time &lt; DATE_ADD(#{param.endTime}, INTERVAL 1 DAY)
+            </when>
+
+            <!-- 1. 今天 -->
+            <when test="param.type == 1">
+                a.create_time >= CURDATE()
+                AND a.create_time &lt; DATE_ADD(CURDATE(), INTERVAL 1 DAY)
+            </when>
+
+            <!-- 2. 昨天 -->
+            <when test="param.type == 2">
+                a.create_time >= DATE_SUB(CURDATE(), INTERVAL 1 DAY)
+                AND a.create_time &lt; CURDATE()
+            </when>
+
+            <!-- 3. 本周 -->
+            <when test="param.type == 3">
+                a.create_time >= DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY)
+                AND a.create_time &lt; DATE_ADD(DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY), INTERVAL 7 DAY)
+            </when>
+
+            <!-- 4. 上周 -->
+            <when test="param.type == 4">
+                a.create_time >= DATE_SUB(DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY), INTERVAL 7 DAY)
+                AND a.create_time &lt; DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY)
+            </when>
+
+            <!-- 5. 本月 -->
+            <when test="param.type == 5">
+                a.create_time >= DATE_FORMAT(CURDATE(), '%Y-%m-01')
+                AND a.create_time &lt; DATE_ADD(DATE_FORMAT(CURDATE(), '%Y-%m-01'), INTERVAL 1 MONTH)
+            </when>
+
+            <!-- 6. 上月 -->
+            <when test="param.type == 6">
+                a.create_time >= DATE_SUB(DATE_FORMAT(CURDATE(), '%Y-%m-01'), INTERVAL 1 MONTH)
+                AND a.create_time &lt; DATE_FORMAT(CURDATE(), '%Y-%m-01')
+            </when>
+
+            <!-- 7. 本季度 -->
+            <when test="param.type == 7">
+                a.create_time >= STR_TO_DATE(CONCAT(YEAR(CURDATE()), '-', QUARTER(CURDATE())*3-2, '-01'), '%Y-%m-%d')
+                AND a.create_time &lt; DATE_ADD(STR_TO_DATE(CONCAT(YEAR(CURDATE()), '-', QUARTER(CURDATE())*3-2, '-01'), '%Y-%m-%d'), INTERVAL 3 MONTH)
+            </when>
+
+            <!-- 8. 上季度 -->
+            <when test="param.type == 8">
+                a.create_time >= DATE_SUB(STR_TO_DATE(CONCAT(YEAR(CURDATE()), '-', QUARTER(CURDATE())*3-2, '-01'), '%Y-%m-%d'), INTERVAL 3 MONTH)
+                AND a.create_time &lt; STR_TO_DATE(CONCAT(YEAR(CURDATE()), '-', QUARTER(CURDATE())*3-2, '-01'), '%Y-%m-%d')
+            </when>
+
+            <!-- 9. 本年 -->
+            <when test="param.type == 9">
+                a.create_time >= STR_TO_DATE(CONCAT(YEAR(CURDATE()), '-01-01'), '%Y-%m-%d')
+                AND a.create_time &lt; DATE_ADD(STR_TO_DATE(CONCAT(YEAR(CURDATE()), '-01-01'), '%Y-%m-%d'), INTERVAL 1 YEAR)
+            </when>
+
+            <!-- 10. 去年 -->
+            <when test="param.type == 10">
+                a.create_time >= DATE_SUB(STR_TO_DATE(CONCAT(YEAR(CURDATE()), '-01-01'), '%Y-%m-%d'), INTERVAL 1 YEAR)
+                AND a.create_time &lt; STR_TO_DATE(CONCAT(YEAR(CURDATE()), '-01-01'), '%Y-%m-%d')
+            </when>
+        </choose>
+    </sql>
 
 
 
 
 </mapper>
 </mapper>

+ 137 - 0
fs-service/src/main/resources/mapper/his/FsConsecutiveWithdrawRecordMapper.xml

@@ -0,0 +1,137 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.his.mapper.FsConsecutiveWithdrawRecordMapper">
+    
+    <resultMap type="FsConsecutiveWithdrawRecord" id="FsConsecutiveWithdrawRecordResult">
+        <result property="id"    column="id"    />
+        <result property="userId"    column="user_id"    />
+        <result property="consecutiveDays"    column="consecutive_days"    />
+        <result property="startDate"    column="start_date"    />
+        <result property="endDate"    column="end_date"    />
+        <result property="withdrawCount"    column="withdraw_count"    />
+        <result property="totalAmount"    column="total_amount"    />
+        <result property="status"    column="status"    />
+        <result property="remark"    column="remark"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="updateTime"    column="update_time"    />
+    </resultMap>
+
+    <sql id="selectFsConsecutiveWithdrawRecordVo">
+        select id, user_id, consecutive_days, start_date, end_date, withdraw_count, total_amount, status, remark, create_time, update_time from fs_consecutive_withdraw_record
+    </sql>
+
+    <select id="selectFsConsecutiveWithdrawRecordList" parameterType="FsConsecutiveWithdrawRecord" resultType="com.fs.his.vo.FsConsecutiveWithdrawRecordVo">
+        select wr.*,fu.nick_name from fs_consecutive_withdraw_record wr
+        left join fs_user fu on fu.user_id = wr.user_id
+        <where>  
+            <if test="userId != null "> and wr.user_id = #{userId}</if>
+            <if test="consecutiveDays != null "> and wr.consecutive_days = #{consecutiveDays}</if>
+            <if test="startDate != null "> and wr.start_date = #{startDate}</if>
+            <if test="endDate != null "> and wr.end_date = #{endDate}</if>
+            <if test="withdrawCount != null "> and wr.withdraw_count = #{withdrawCount}</if>
+            <if test="totalAmount != null "> and wr.total_amount = #{totalAmount}</if>
+            <if test="status != null "> and wr.status = #{status}</if>
+            <if test="nickName != null and nickName != ''"> and fu.nick_name like concat(#{nickName},"%")</if>
+        </where>
+        order by wr.id desc
+    </select>
+
+    <select id="selectFsConsecutiveWithdrawRecordList_COUNT" parameterType="FsConsecutiveWithdrawRecord" resultType="Long">
+        select count(*) from fs_consecutive_withdraw_record wr
+        <if test="nickName != null and nickName != ''">
+            left join fs_user fu on fu.user_id = wr.user_id
+        </if>
+        <where>
+            <if test="userId != null "> and wr.user_id = #{userId}</if>
+            <if test="consecutiveDays != null "> and wr.consecutive_days = #{consecutiveDays}</if>
+            <if test="startDate != null "> and wr.start_date = #{startDate}</if>
+            <if test="endDate != null "> and wr.end_date = #{endDate}</if>
+            <if test="withdrawCount != null "> and wr.withdraw_count = #{withdrawCount}</if>
+            <if test="totalAmount != null "> and wr.total_amount = #{totalAmount}</if>
+            <if test="status != null "> and wr.status = #{status}</if>
+            <if test="nickName != null and nickName != ''"> and fu.nick_name like concat(#{nickName},"%")</if>
+        </where>
+    </select>
+    
+    <select id="selectFsConsecutiveWithdrawRecordById" parameterType="Long" resultMap="FsConsecutiveWithdrawRecordResult">
+        <include refid="selectFsConsecutiveWithdrawRecordVo"/>
+        where id = #{id}
+    </select>
+    <select id="existsRecordInPeriod" resultType="java.lang.Boolean">
+        SELECT COUNT(1) > 0 FROM fs_consecutive_withdraw_record
+        WHERE user_id = #{userId}
+        AND start_date >= #{startDate}
+        AND end_date &lt;= #{endDate}
+        AND consecutive_days >= #{thresholdDays}
+    </select>
+    <select id="countByUserAndPeriod" resultType="java.lang.Integer">
+                SELECT  * FROM fs_consecutive_withdraw_record
+                WHERE user_id = #{userId}
+                AND start_date = #{startDate}
+                AND end_date = #{endDate}
+    </select>
+    <select id="selectCountByUserIdAndStartTime" resultType="com.fs.his.domain.FsConsecutiveWithdrawRecord">
+        SELECT * FROM fs_consecutive_withdraw_record
+        WHERE user_id = #{userId}
+          AND start_date = #{startDate}
+        order by id desc limit 1
+    </select>
+
+    <insert id="insertFsConsecutiveWithdrawRecord" parameterType="FsConsecutiveWithdrawRecord" useGeneratedKeys="true" keyProperty="id">
+        insert into fs_consecutive_withdraw_record
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="userId != null">user_id,</if>
+            <if test="consecutiveDays != null">consecutive_days,</if>
+            <if test="startDate != null">start_date,</if>
+            <if test="endDate != null">end_date,</if>
+            <if test="withdrawCount != null">withdraw_count,</if>
+            <if test="totalAmount != null">total_amount,</if>
+            <if test="status != null">status,</if>
+            <if test="remark != null">remark,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="userId != null">#{userId},</if>
+            <if test="consecutiveDays != null">#{consecutiveDays},</if>
+            <if test="startDate != null">#{startDate},</if>
+            <if test="endDate != null">#{endDate},</if>
+            <if test="withdrawCount != null">#{withdrawCount},</if>
+            <if test="totalAmount != null">#{totalAmount},</if>
+            <if test="status != null">#{status},</if>
+            <if test="remark != null">#{remark},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+         </trim>
+    </insert>
+
+    <update id="updateFsConsecutiveWithdrawRecord" parameterType="FsConsecutiveWithdrawRecord">
+        update fs_consecutive_withdraw_record
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="userId != null">user_id = #{userId},</if>
+            <if test="consecutiveDays != null">consecutive_days = #{consecutiveDays},</if>
+            <if test="startDate != null">start_date = #{startDate},</if>
+            <if test="endDate != null">end_date = #{endDate},</if>
+            <if test="withdrawCount != null">withdraw_count = #{withdrawCount},</if>
+            <if test="totalAmount != null">total_amount = #{totalAmount},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="remark != null">remark = #{remark},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteFsConsecutiveWithdrawRecordById" parameterType="Long">
+        delete from fs_consecutive_withdraw_record where id = #{id}
+    </delete>
+
+    <delete id="deleteFsConsecutiveWithdrawRecordByIds" parameterType="String">
+        delete from fs_consecutive_withdraw_record where id in 
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>

+ 31 - 3
fs-service/src/main/resources/mapper/his/FsIntegralCartMapper.xml

@@ -12,7 +12,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             cash,
             cash,
             cart_num,
             cart_num,
             create_time,
             create_time,
-            update_time
+            update_time,
+            is_selected
         ) values (
         ) values (
             #{userId},
             #{userId},
             #{goodsId},
             #{goodsId},
@@ -20,7 +21,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             #{cash},
             #{cash},
             #{cartNum},
             #{cartNum},
             #{createTime},
             #{createTime},
-            #{updateTime}
+            #{updateTime},
+            #{isSelected}
         )
         )
         on duplicate key update
         on duplicate key update
             integral = values(integral),
             integral = values(integral),
@@ -36,6 +38,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                 </otherwise>
                 </otherwise>
             </choose>
             </choose>
     </insert>
     </insert>
+    <update id="updateQuantityAtomically">
+        UPDATE fs_integral_cart
+        SET quantity = LEAST(quantity + #{addQuantity}, #{maxQuantity}),
+            is_selected = 1,
+            update_time = NOW()
+        WHERE user_id = #{userId} AND goods_id = #{goodsId} AND quantity <![CDATA[<]]> #{maxQuantity}
+    </update>
 
 
     <select id="getCartCountByMap" resultType="java.lang.Integer">
     <select id="getCartCountByMap" resultType="java.lang.Integer">
         select
         select
@@ -65,7 +74,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             ig.stock,
             ig.stock,
             ic.cart_num,
             ic.cart_num,
             ic.create_time,
             ic.create_time,
-            ic.update_time
+            ic.update_time,
+            ic.is_selected
         from fs_integral_cart ic
         from fs_integral_cart ic
         inner join fs_integral_goods ig on ig.goods_id = ic.goods_id
         inner join fs_integral_goods ig on ig.goods_id = ic.goods_id
         <where>
         <where>
@@ -80,6 +90,24 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             </if>
             </if>
         </where>
         </where>
     </select>
     </select>
+    <select id="getFsIntegralCartList" resultType="com.fs.his.vo.GetFsIntegralCartListVo">
+        select fc.*,fg.*,fg.integral as goodsIntegral,fu.integral as userIntegral
+        from fs_integral_cart fc
+        left join fs_integral_goods fg on fc.goods_id = fg.goods_id
+        left join fs_user fu on fu.user_id = fc.user_id
+        <where>
+            <if test="aLong != null">
+                fc.user_id = #{aLong}
+            </if>
+            <if test="param.goodsName != null and param.goodsName != ''">
+                and fg.goods_name like concat('%', #{param.goodsName}, '%')
+            </if>
+            <if test="cartId != null and cartId.size() > 0">
+                and fc.cart_id in
+                <foreach collection="cartId" item="cartId" open="(" close=")" separator=",">#{cartId}</foreach>
+            </if>
+        </where>
+    </select>
 
 
     <delete id="deleteCartByGoodsId">
     <delete id="deleteCartByGoodsId">
         delete from fs_integral_cart where goods_id = #{goodsId}
         delete from fs_integral_cart where goods_id = #{goodsId}

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

@@ -173,4 +173,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             #{goodsId}
             #{goodsId}
         </foreach>
         </foreach>
     </select>
     </select>
+    <select id="selectAllByGoodsIds" resultType="com.fs.his.vo.FsIntegralGoodsVo">
+        select * from fs_integral_goods where goods_id in
+        <foreach item="goodsIds" collection="goodsIds" open="(" separator="," close=")">
+            #{goodsIds}
+        </foreach>
+    </select>
 </mapper>
 </mapper>

+ 266 - 0
fs-service/src/main/resources/mapper/his/FsIntegralRedPacketLogMapper.xml

@@ -0,0 +1,266 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.his.mapper.FsIntegralRedPacketLogMapper">
+    
+    <resultMap type="FsIntegralRedPacketLog" id="FsIntegralRedPacketLogResult">
+        <result property="logId"    column="log_id"    />
+        <result property="userId"    column="user_id"    />
+        <result property="amount"    column="amount"    />
+        <result property="outBatchNo"    column="out_batch_no"    />
+        <result property="batchId"    column="batch_id"    />
+        <result property="status"    column="status"    />
+        <result property="appId"    column="app_id"    />
+        <result property="remark"    column="remark"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="packageInfo"    column="package_info"    />
+        <result property="mchId"    column="mch_id"    />
+        <result property="returnedStatus"    column="returned_status"    />
+    </resultMap>
+
+    <sql id="selectFsIntegralRedPacketLogVo">
+        select log_id, user_id, amount, out_batch_no, batch_id, status,app_id, remark, create_time, update_time,
+               package_info,mch_id,returned_status from fs_integral_red_packet_log
+    </sql>
+
+    <select id="selectFsIntegralRedPacketLogList" parameterType="FsIntegralRedPacketLog" resultMap="FsIntegralRedPacketLogResult">
+        <include refid="selectFsIntegralRedPacketLogVo"/>
+        <where>  
+            <if test="userId != null "> and user_id = #{userId}</if>
+            <if test="amount != null "> and amount = #{amount}</if>
+            <if test="outBatchNo != null  and outBatchNo != ''"> and out_batch_no = #{outBatchNo}</if>
+            <if test="batchId != null  and batchId != ''"> and batch_id = #{batchId}</if>
+            <if test="status != null "> and status = #{status}</if>
+            <if test="appId != null "> and app_id = #{appId}</if>
+            <if test="mchId != null "> and mch_id = #{mchId}</if>
+            <if test="returnedStatus != null"> and returned_status = #{returnedStatus}</if>
+            <if test="beginTime != null and beginTime != ''">
+                and create_time >= #{beginTime}
+            </if>
+            <if test="endTime != null and endTime != ''">
+                and create_time &lt;= #{endTime}
+            </if>
+        </where>
+        order by log_id desc
+    </select>
+    
+    <select id="selectFsIntegralRedPacketLogByLogId" parameterType="Long" resultMap="FsIntegralRedPacketLogResult">
+        <include refid="selectFsIntegralRedPacketLogVo"/>
+        where log_id = #{logId}
+    </select>
+    <select id="sumMoneyByUserId" resultType="java.math.BigDecimal">
+        SELECT SUM(amount) FROM fs_integral_red_packet_log WHERE user_id = #{userId} AND `status` = 1
+    </select>
+    <select id="selectFsIntegralRedPacketLogByBatchNo" resultType="com.fs.his.domain.FsIntegralRedPacketLog">
+        select * from fs_integral_red_packet_log where out_batch_no = #{outBatchNo}
+    </select>
+    <select id="getList" resultType="com.fs.his.vo.FsIntegralRedPacketLogVo">
+        select pl.log_id, pl.user_id, pl.amount, pl.out_batch_no, pl.batch_id, pl.status,pl.app_id, pl.remark, pl.create_time, pl.update_time,
+        pl.package_info,pl.mch_id,fu.nick_name,pl.returned_status
+        from fs_integral_red_packet_log pl left join fs_user fu on fu.user_id = pl.user_id
+        <where>
+            <if test="userId != null "> and pl.user_id = #{userId}</if>
+            <if test="amount != null "> and pl.amount = #{amount}</if>
+            <if test="outBatchNo != null  and outBatchNo != ''"> and pl.out_batch_no = #{outBatchNo}</if>
+            <if test="batchId != null  and batchId != ''"> and pl.batch_id = #{batchId}</if>
+            <if test="status != null "> and pl.status = #{status}</if>
+            <if test="appId != null "> and pl.app_id = #{appId}</if>
+            <if test="mchId != null "> and pl.mch_id = #{mchId}</if>
+            <if test="returnedStatus != null"> and returned_status = #{returnedStatus}</if>
+            <if test="nickName != null and nickName != ''">
+                and fu.nick_name like concat(#{nickName},"%")
+            </if>
+            <if test="maxAmount != null ">
+                and pl.amount &lt;= #{maxAmount}
+            </if>
+            <if test="minAmount != null ">
+                and pl.amount >= #{minAmount}
+            </if>
+            <if test="beginTime != null and beginTime != ''">
+                and pl.create_time >= #{beginTime}
+            </if>
+            <if test="endTime != null and endTime != ''">
+                and pl.create_time &lt;= #{endTime}
+            </if>
+        </where>
+        order by pl.log_id desc
+    </select>
+
+    <select id="getList_COUNT" resultType="long">
+        select count(*) from fs_integral_red_packet_log pl
+        <if test="nickName != null and nickName != ''">
+            left join fs_user fu on fu.user_id = pl.user_id
+        </if>
+        <where>
+            <if test="userId != null "> and pl.user_id = #{userId}</if>
+            <if test="amount != null "> and pl.amount = #{amount}</if>
+            <if test="outBatchNo != null  and outBatchNo != ''"> and pl.out_batch_no = #{outBatchNo}</if>
+            <if test="batchId != null  and batchId != ''"> and pl.batch_id = #{batchId}</if>
+            <if test="status != null "> and pl.status = #{status}</if>
+            <if test="appId != null "> and pl.app_id = #{appId}</if>
+            <if test="mchId != null "> and pl.mch_id = #{mchId}</if>
+            <if test="returnedStatus != null"> and returned_status = #{returnedStatus}</if>
+            <if test="nickName != null and nickName != ''">
+                and fu.nick_name like concat(#{nickName},"%")
+            </if>
+            <if test="maxAmount != null ">
+                and pl.amount &lt;= #{maxAmount}
+            </if>
+            <if test="minAmount != null ">
+                and pl.amount >= #{minAmount}
+            </if>
+            <if test="beginTime != null and beginTime != ''">
+                and pl.create_time >= #{beginTime}
+            </if>
+            <if test="endTime != null and endTime != ''">
+                and pl.create_time &lt;= #{endTime}
+            </if>
+        </where>
+    </select>
+    <select id="countLogsByToday" resultType="java.lang.Long">
+        SELECT count(*)
+        FROM fs_integral_red_packet_log
+        WHERE user_id = #{userId}
+        AND status IN (0, 1)
+        AND create_time >= CURDATE()
+        AND create_time &lt; DATE_ADD(CURDATE(), INTERVAL 1 DAY);
+    </select>
+    <select id="selectUsersWithWithdrawInPeriod" resultType="java.lang.Long">
+        SELECT DISTINCT user_id
+        FROM fs_integral_red_packet_log
+        WHERE `status` IN (0, 1)
+        AND create_time >= #{startDate}
+        AND create_time &lt; DATE_ADD(#{endDate}, INTERVAL 1 DAY)
+        GROUP BY user_id
+        HAVING sum(amount) >= #{limitAmount}
+    </select>
+    <select id="selectWithdrawDatesByUser" resultType="java.time.LocalDate">
+        SELECT DISTINCT DATE(create_time) as withdraw_date
+        FROM fs_integral_red_packet_log
+        WHERE user_id = #{userId}
+        AND status IN (0, 1)
+        AND create_time >= #{startDate}
+        AND create_time &lt; DATE_ADD(#{endDate}, INTERVAL 1 DAY)
+        ORDER BY withdraw_date
+    </select>
+    <select id="selectWithdrawStatsByUserAndPeriod" resultType="java.util.Map">
+            SELECT
+              COUNT(*) as count,
+              SUM(amount) as total_amount
+            FROM fs_integral_red_packet_log
+            WHERE user_id = #{userId}
+            AND `status` IN (0, 1)
+            AND DATE(create_time) BETWEEN #{startDate} AND #{endDate}
+    </select>
+    <select id="sumMoneyByStatus" resultType="java.math.BigDecimal">
+        SELECT
+              SUM(amount)
+        FROM fs_integral_red_packet_log
+        WHERE
+          status = #{status}
+          AND returned_status = 0
+    </select>
+
+    <insert id="insertFsIntegralRedPacketLog" parameterType="FsIntegralRedPacketLog" useGeneratedKeys="true" keyProperty="logId">
+        insert into fs_integral_red_packet_log
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="userId != null">user_id,</if>
+            <if test="amount != null">amount,</if>
+            <if test="outBatchNo != null">out_batch_no,</if>
+            <if test="batchId != null">batch_id,</if>
+            <if test="status != null">status,</if>
+            <if test="appId != null">app_id,</if>
+            <if test="remark != null">remark,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="packageInfo != null">package_info,</if>
+            <if test="mchId != null">mch_id,</if>
+            <if test="returnedStatus != null">returned_status,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="userId != null">#{userId},</if>
+            <if test="amount != null">#{amount},</if>
+            <if test="outBatchNo != null">#{outBatchNo},</if>
+            <if test="batchId != null">#{batchId},</if>
+            <if test="status != null">#{status},</if>
+            <if test="appId != null">#{appId},</if>
+            <if test="remark != null">#{remark},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="packageInfo != null">#{packageInfo},</if>
+            <if test="mchId != null">#{mchId},</if>
+            <if test="returnedStatus != null">#{returnedStatus},</if>
+         </trim>
+    </insert>
+
+    <update id="updateFsIntegralRedPacketLog" parameterType="FsIntegralRedPacketLog">
+        update fs_integral_red_packet_log
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="userId != null">user_id = #{userId},</if>
+            <if test="amount != null">amount = #{amount},</if>
+            <if test="outBatchNo != null">out_batch_no = #{outBatchNo},</if>
+            <if test="batchId != null">batch_id = #{batchId},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="appId != null">app_id = #{appId},</if>
+            <if test="remark != null">remark = #{remark},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="packageInfo != null and packageInfo !=''">package_info = #{packageInfo},</if>
+            <if test="mchId != null and mchId != ''">mch_id = #{mchId},</if>
+            <if test="returnedStatus != null">returned_status = #{returnedStatus},</if>
+        </trim>
+        where log_id = #{logId}
+    </update>
+    <update id="batchUpdate">
+        UPDATE fs_integral_red_packet_log
+        SET
+        status = CASE log_id
+        <foreach collection="list" item="item">
+            WHEN #{item.logId} THEN #{item.status}
+        </foreach>
+        ELSE status
+        END,
+        update_time = CASE log_id
+        <foreach collection="list" item="item">
+            WHEN #{item.logId} THEN #{item.updateTime}
+        </foreach>
+        ELSE update_time
+        END,
+        batch_id = CASE log_id
+        <foreach collection="list" item="item">
+            WHEN #{item.logId} THEN #{item.batchId}
+        </foreach>
+        ELSE batch_id
+        END,
+        remark = CASE log_id
+        <foreach collection="list" item="item">
+            WHEN #{item.logId} THEN #{item.remark}
+        </foreach>
+        ELSE remark
+        END,
+        returned_status = CASE log_id
+        <foreach collection="list" item="item">
+            WHEN #{item.logId} THEN #{item.returnedStatus}
+        </foreach>
+        ELSE returned_status
+        END
+        WHERE log_id IN
+        <foreach collection="list" item="item" open="(" separator="," close=")">
+            #{item.logId}
+        </foreach>
+    </update>
+
+
+    <delete id="deleteFsIntegralRedPacketLogByLogId" parameterType="Long">
+        delete from fs_integral_red_packet_log where log_id = #{logId}
+    </delete>
+
+    <delete id="deleteFsIntegralRedPacketLogByLogIds" parameterType="String">
+        delete from fs_integral_red_packet_log where log_id in 
+        <foreach item="logId" collection="array" open="(" separator="," close=")">
+            #{logId}
+        </foreach>
+    </delete>
+</mapper>

+ 6 - 0
fs-service/src/main/resources/mapper/his/FsStoreOrderItemMapper.xml

@@ -55,6 +55,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         AND pay_time >= CONCAT(YEAR(CURDATE()), '-01-01')
         AND pay_time >= CONCAT(YEAR(CURDATE()), '-01-01')
         AND pay_time &lt; DATE_ADD(CONCAT(YEAR(CURDATE()), '-01-01'), INTERVAL 1 YEAR);
         AND pay_time &lt; DATE_ADD(CONCAT(YEAR(CURDATE()), '-01-01'), INTERVAL 1 YEAR);
     </select>
     </select>
+    <select id="selectPayPriceAll" resultType="java.math.BigDecimal">
+        SELECT SUM(pay_price) AS total_pay
+        FROM fs_store_order
+        WHERE status != 1 and status != -2
+          AND user_id = #{userId}
+    </select>
 
 
     <insert id="insertFsStoreOrderItem" parameterType="FsStoreOrderItem" useGeneratedKeys="true" keyProperty="itemId">
     <insert id="insertFsStoreOrderItem" parameterType="FsStoreOrderItem" useGeneratedKeys="true" keyProperty="itemId">
         insert into fs_store_order_item
         insert into fs_store_order_item

+ 100 - 0
fs-service/src/main/resources/mapper/his/FsUserActiveLogMapper.xml

@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.his.mapper.FsUserActiveLogMapper">
+
+    <resultMap type="FsUserActiveLog" id="FsUserActiveLogResult">
+        <result property="id"    column="id"    />
+        <result property="activeName"    column="active_name"    />
+        <result property="userId"    column="user_id"    />
+        <result property="number"    column="number"    />
+        <result property="balance"    column="balance"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="title"    column="title"    />
+    </resultMap>
+
+    <sql id="selectFsUserActiveLogVo">
+        select id, active_name, user_id, number, balance, create_time, update_time, title from fs_user_active_log
+    </sql>
+
+    <select id="selectFsUserActiveLogList" parameterType="FsUserActiveLog" resultMap="FsUserActiveLogResult">
+        <include refid="selectFsUserActiveLogVo"/>
+        <where>
+            <if test="activeName != null  and activeName != ''"> and active_name like concat('%', #{activeName}, '%')</if>
+            <if test="userId != null "> and user_id = #{userId}</if>
+            <if test="number != null "> and number = #{number}</if>
+            <if test="balance != null "> and balance = #{balance}</if>
+            <if test="title != null  and title != ''"> and title = #{title}</if>
+        </where>
+    </select>
+
+    <select id="selectFsUserActiveLogById" parameterType="Long" resultMap="FsUserActiveLogResult">
+        <include refid="selectFsUserActiveLogVo"/>
+        where id = #{id}
+    </select>
+    <select id="selectTodayIsSign" resultType="com.fs.his.domain.FsUserActiveLog">
+        SELECT *
+        FROM fs_user_active_log
+        WHERE user_id = #{userId}
+          AND DATE(create_time) = CURDATE();
+    </select>
+    <select id="selectByUserAndDateRange" resultType="com.fs.his.domain.FsUserActiveLog">
+        SELECT *
+        FROM fs_user_active_log
+        WHERE user_id = #{userId}
+        AND create_time &gt;= #{startTime}
+        AND create_time &lt; #{endTime} + INTERVAL 1 DAY;
+
+    </select>
+
+    <insert id="insertFsUserActiveLog" parameterType="FsUserActiveLog">
+        insert into fs_user_active_log
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="id != null">id,</if>
+            <if test="activeName != null">active_name,</if>
+            <if test="userId != null">user_id,</if>
+            <if test="number != null">number,</if>
+            <if test="balance != null">balance,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="title != null and title != ''">title,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="id != null">#{id},</if>
+            <if test="activeName != null">#{activeName},</if>
+            <if test="userId != null">#{userId},</if>
+            <if test="number != null">#{number},</if>
+            <if test="balance != null">#{balance},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="title != null and title != ''">#{title},</if>
+         </trim>
+    </insert>
+
+    <update id="updateFsUserActiveLog" parameterType="FsUserActiveLog">
+        update fs_user_active_log
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="activeName != null">active_name = #{activeName},</if>
+            <if test="userId != null">user_id = #{userId},</if>
+            <if test="number != null">number = #{number},</if>
+            <if test="balance != null">balance = #{balance},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="title != null and title != ''">title = #{title},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteFsUserActiveLogById" parameterType="Long">
+        delete from fs_user_active_log where id = #{id}
+    </delete>
+
+    <delete id="deleteFsUserActiveLogByIds" parameterType="String">
+        delete from fs_user_active_log where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>

+ 74 - 0
fs-service/src/main/resources/mapper/his/FsUserIntegralLogsMapper.xml

@@ -77,6 +77,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
           AND log_type = #{logType}
           AND log_type = #{logType}
         order by create_time desc
         order by create_time desc
     </select>
     </select>
+    <select id="sumIntegralByLogTypeAndCreateTime" resultType="java.lang.Long">
+        SELECT COALESCE(SUM(integral), 0) FROM fs_user_integral_logs where log_type = #{logType} and <include refid="timeCondition"/>
+    </select>
 
 
     <insert id="insertFsUserIntegralLogs" parameterType="FsUserIntegralLogs" useGeneratedKeys="true" keyProperty="id">
     <insert id="insertFsUserIntegralLogs" parameterType="FsUserIntegralLogs" useGeneratedKeys="true" keyProperty="id">
         insert into fs_user_integral_logs
         insert into fs_user_integral_logs
@@ -127,4 +130,75 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             #{id}
             #{id}
         </foreach>
         </foreach>
     </delete>
     </delete>
+
+
+    <sql id="timeCondition">
+        <choose>
+            <!-- type为null时,使用startTime和endTime -->
+            <when test="param.type == null">
+                a.create_time >= #{param.startTime}
+                AND a.create_time &lt; DATE_ADD(#{param.endTime}, INTERVAL 1 DAY)
+            </when>
+
+            <!-- 1. 今天 -->
+            <when test="param.type == 1">
+                a.create_time >= CURDATE()
+                AND a.create_time &lt; DATE_ADD(CURDATE(), INTERVAL 1 DAY)
+            </when>
+
+            <!-- 2. 昨天 -->
+            <when test="param.type == 2">
+                a.create_time >= DATE_SUB(CURDATE(), INTERVAL 1 DAY)
+                AND a.create_time &lt; CURDATE()
+            </when>
+
+            <!-- 3. 本周 -->
+            <when test="param.type == 3">
+                a.create_time >= DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY)
+                AND a.create_time &lt; DATE_ADD(DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY), INTERVAL 7 DAY)
+            </when>
+
+            <!-- 4. 上周 -->
+            <when test="param.type == 4">
+                a.create_time >= DATE_SUB(DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY), INTERVAL 7 DAY)
+                AND a.create_time &lt; DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY)
+            </when>
+
+            <!-- 5. 本月 -->
+            <when test="param.type == 5">
+                a.create_time >= DATE_FORMAT(CURDATE(), '%Y-%m-01')
+                AND a.create_time &lt; DATE_ADD(DATE_FORMAT(CURDATE(), '%Y-%m-01'), INTERVAL 1 MONTH)
+            </when>
+
+            <!-- 6. 上月 -->
+            <when test="param.type == 6">
+                a.create_time >= DATE_SUB(DATE_FORMAT(CURDATE(), '%Y-%m-01'), INTERVAL 1 MONTH)
+                AND a.create_time &lt; DATE_FORMAT(CURDATE(), '%Y-%m-01')
+            </when>
+
+            <!-- 7. 本季度 -->
+            <when test="param.type == 7">
+                a.create_time >= STR_TO_DATE(CONCAT(YEAR(CURDATE()), '-', QUARTER(CURDATE())*3-2, '-01'), '%Y-%m-%d')
+                AND a.create_time &lt; DATE_ADD(STR_TO_DATE(CONCAT(YEAR(CURDATE()), '-', QUARTER(CURDATE())*3-2, '-01'), '%Y-%m-%d'), INTERVAL 3 MONTH)
+            </when>
+
+            <!-- 8. 上季度 -->
+            <when test="param.type == 8">
+                a.create_time >= DATE_SUB(STR_TO_DATE(CONCAT(YEAR(CURDATE()), '-', QUARTER(CURDATE())*3-2, '-01'), '%Y-%m-%d'), INTERVAL 3 MONTH)
+                AND a.create_time &lt; STR_TO_DATE(CONCAT(YEAR(CURDATE()), '-', QUARTER(CURDATE())*3-2, '-01'), '%Y-%m-%d')
+            </when>
+
+            <!-- 9. 本年 -->
+            <when test="param.type == 9">
+                a.create_time >= STR_TO_DATE(CONCAT(YEAR(CURDATE()), '-01-01'), '%Y-%m-%d')
+                AND a.create_time &lt; DATE_ADD(STR_TO_DATE(CONCAT(YEAR(CURDATE()), '-01-01'), '%Y-%m-%d'), INTERVAL 1 YEAR)
+            </when>
+
+            <!-- 10. 去年 -->
+            <when test="param.type == 10">
+                a.create_time >= DATE_SUB(STR_TO_DATE(CONCAT(YEAR(CURDATE()), '-01-01'), '%Y-%m-%d'), INTERVAL 1 YEAR)
+                AND a.create_time &lt; STR_TO_DATE(CONCAT(YEAR(CURDATE()), '-01-01'), '%Y-%m-%d')
+            </when>
+        </choose>
+    </sql>
 </mapper>
 </mapper>

+ 10 - 1
fs-service/src/main/resources/mapper/his/FsUserMapper.xml

@@ -53,6 +53,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="appOpenId"    column="app_open_id"    />
         <result property="appOpenId"    column="app_open_id"    />
         <result property="appleKey"    column="apple_key"    />
         <result property="appleKey"    column="apple_key"    />
         <result property="historyApp"    column="history_app"    />
         <result property="historyApp"    column="history_app"    />
+        <result property="withdrawIntegral"    column="withdraw_integral"    />
+        <result property="withdrawFinish"    column="withdraw_finish"    />
+        <result property="totalCommission"    column="total_commission"    />
+
     </resultMap>
     </resultMap>
 
 
     <sql id="selectFsUserVo">
     <sql id="selectFsUserVo">
@@ -62,7 +66,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                tui_user_id, tui_time, tui_user_count, ma_open_id, mp_open_id, union_id, is_del, user_code,
                tui_user_id, tui_time, tui_user_count, ma_open_id, mp_open_id, union_id, is_del, user_code,
                remark, create_time, update_time, last_ip, balance,is_weixin_auth,parent_id,qw_user_id,app_id,
                remark, create_time, update_time, last_ip, balance,is_weixin_auth,parent_id,qw_user_id,app_id,
                company_id,company_user_id,is_promoter,now_money,brokerage_price,spread_user_id, spread_time,pay_count,
                company_id,company_user_id,is_promoter,now_money,brokerage_price,spread_user_id, spread_time,pay_count,
-               spread_count,user_type,app_open_id,apple_key,history_app from fs_user
+               spread_count,user_type,app_open_id,apple_key,history_app,withdraw_integral,total_commission,withdraw_finish
+               ,may_withdraw from fs_user
     </sql>
     </sql>
 
 
     <select id="selectFsUserList" parameterType="FsUser" resultMap="FsUserResult">
     <select id="selectFsUserList" parameterType="FsUser" resultMap="FsUserResult">
@@ -709,6 +714,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="appOpenId != null">app_open_id = #{appOpenId},</if>
             <if test="appOpenId != null">app_open_id = #{appOpenId},</if>
             <if test="appleKey != null">apple_key = #{appleKey},</if>
             <if test="appleKey != null">apple_key = #{appleKey},</if>
             <if test="historyApp != null and historyApp != ''">history_app = #{historyApp},</if>
             <if test="historyApp != null and historyApp != ''">history_app = #{historyApp},</if>
+            <if test="withdrawIntegral != null">withdraw_integral = #{withdrawIntegral},</if>
+            <if test="withdrawFinish != null">withdraw_finish = #{withdrawFinish},</if>
+            <if test="totalCommission != null">total_commission = #{totalCommission},</if>
+            <if test="mayWithdraw != null">may_withdraw = #{mayWithdraw},</if>
         </trim>
         </trim>
         where user_id = #{userId}
         where user_id = #{userId}
     </update>
     </update>

+ 7 - 0
fs-service/src/main/resources/mapper/his/InterestAiChatSessionMapper.xml

@@ -136,6 +136,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         inner join fs_interest_ai_role t3 on t3.role_id = t2.role_id
         inner join fs_interest_ai_role t3 on t3.role_id = t2.role_id
         where t2.session_id = #{sessionId}
         where t2.session_id = #{sessionId}
     </select>
     </select>
+    <select id="selectSessionIdByUserAndRole" resultType="java.lang.Integer">
+        select session_id from fs_interest_ai_session where user_id = #{userId} and role_id = #{roleId} limit 1
+    </select>
+    <select id="selectAiMsgBySessionIdAndMsg" resultType="java.lang.Integer">
+         <![CDATA[ select count(1) from fs_interest_ai_msg where session_id = #{sessionId} and content = #{message}
+                                                             and begin_time = #{beginTime}]]>
+    </select>
 
 
     <insert id="insertFsInterestAiSession" parameterType="FsInterestAiSession" useGeneratedKeys="true" keyProperty="sessionId">
     <insert id="insertFsInterestAiSession" parameterType="FsInterestAiSession" useGeneratedKeys="true" keyProperty="sessionId">
         insert into fs_interest_ai_session
         insert into fs_interest_ai_session

+ 18 - 0
fs-user-app/src/main/java/com/fs/app/controller/CommonController.java

@@ -41,6 +41,8 @@ import com.fs.event.TemplateEvent;
 import com.fs.event.TemplateListenEnum;
 import com.fs.event.TemplateListenEnum;
 import com.fs.event.WeixinTemplateService;
 import com.fs.event.WeixinTemplateService;
 import com.fs.framework.config.ServerConfig;
 import com.fs.framework.config.ServerConfig;
+import com.fs.his.config.AppConfig;
+import com.fs.his.config.AppPageConfig;
 import com.fs.his.config.FsSmsConfig;
 import com.fs.his.config.FsSmsConfig;
 import com.fs.his.config.FsSysConfig;
 import com.fs.his.config.FsSysConfig;
 import com.fs.his.domain.*;
 import com.fs.his.domain.*;
@@ -728,4 +730,20 @@ public class CommonController {
 	public R exchangDetail(@RequestBody Map<String,Object> params){
 	public R exchangDetail(@RequestBody Map<String,Object> params){
 		return userService.exchangDetail(params);
 		return userService.exchangDetail(params);
 	}
 	}
+
+	@ApiOperation("获取ios支付开关")
+	@GetMapping("/getAppPageConfig")
+	public R getAppPageConfig(){
+		String json = configService.selectConfigByKey("app.pageConfig");
+		AppPageConfig config = JSONUtil.toBean(json, AppPageConfig.class);
+		return R.ok().put("appPageConfig",config);
+	}
+
+	@ApiOperation("获取提现开关")
+	@GetMapping("/getAppWithdraw")
+	public R getAppWithdraw(){
+		String json = configService.selectConfigByKey("app.config");
+		AppConfig config = JSONUtil.toBean(json, AppConfig.class);
+		return R.ok().put("isOpenWithdraw",config.getIsOpenWithdraw()==null?0:config.getIsOpenWithdraw());
+	}
 }
 }

+ 98 - 0
fs-user-app/src/main/java/com/fs/app/controller/FsIntegralCartController.java

@@ -0,0 +1,98 @@
+package com.fs.app.controller;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.fs.app.annotation.Login;
+import com.fs.common.annotation.RepeatSubmit;
+import com.fs.common.core.domain.R;
+import com.fs.his.domain.FsIntegralCart;
+import com.fs.his.param.AddGoodsIntoCartParam;
+import com.fs.his.param.CreateOrderFromCartParm;
+import com.fs.his.param.GetFsIntegralCartDetailsParm;
+import com.fs.his.param.GetFsIntegralCartListParam;
+import com.fs.his.service.IFsIntegralCartService;
+import com.fs.his.vo.GetFsIntegralCartDetailsVo;
+import com.fs.his.vo.GetFsIntegralCartListVo;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * <p>
+ * 积分购物车表 前端控制器
+ * </p>
+ *
+ * @author chenshiyi
+ * @since 2026-01-13
+ */
+@RestController
+@RequestMapping("/app/integralCart")
+public class FsIntegralCartController extends AppBaseController {
+
+    @Autowired
+    private IFsIntegralCartService fsIntegralCartService;
+
+
+    @Login
+    @ApiOperation("获取用户购物车列表")
+    @PostMapping("/getFsIntegralCartList")
+    public R getFsIntegralCartList(@RequestBody GetFsIntegralCartListParam param) {
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        Long aLong = Long.valueOf(getUserId());
+        List<GetFsIntegralCartListVo> list = fsIntegralCartService.getFsIntegralCartList(param, aLong);
+        PageInfo<GetFsIntegralCartListVo> listPageInfo = new PageInfo<>(list);
+        return R.ok().put("data", listPageInfo);
+    }
+
+    @Login
+    @ApiOperation("获取用户购物车详情页")
+    @PostMapping("/getFsIntegralCartDetails")
+    public R getFsIntegralCartDetails(@RequestBody GetFsIntegralCartDetailsParm param) {
+        Long userId = Long.valueOf(getUserId());
+        GetFsIntegralCartDetailsVo list = fsIntegralCartService.getFsIntegralCartDetails(param, userId);
+        return R.ok().put("data", list);
+    }
+
+    @Login
+    @ApiOperation("添加商品到购物车")
+    @PostMapping("/addGoodsIntoCart")
+    public R addGoodsIntoCart(@RequestBody AddGoodsIntoCartParam param) {
+        Long aLong = Long.valueOf(getUserId());
+        Boolean b = fsIntegralCartService.addGoodsIntoCart(param, aLong);
+        return b ? R.ok("添加商品成功") : R.error("添加商品失败");
+    }
+
+
+    @Login
+    @ApiOperation("修改购物车商品数量")
+    @PutMapping("/putGoodsQuantityFromCart")
+    public R putGoodsQuantityFromCart(@RequestParam Integer quantity, @RequestParam Long cartId) {
+        Long aLong = Long.valueOf(getUserId());
+        FsIntegralCart cart = FsIntegralCart.builder().id(cartId).cartNum(quantity).build();
+        Boolean b = fsIntegralCartService.update(cart, Wrappers.<FsIntegralCart>lambdaQuery().eq(FsIntegralCart::getUserId, aLong).eq(FsIntegralCart::getId, cartId));
+        return b ? R.ok("修改商品成功") : R.error("修改商品失败");
+    }
+
+
+    @Login
+    @ApiOperation("删除购物车商品")
+    @DeleteMapping("/deleteGoodsFromCart/{cartIds}")
+    public R deleteGoodsFromCart(@PathVariable List<Long> cartIds) {
+        Long aLong = Long.valueOf(getUserId());
+        Boolean b = fsIntegralCartService.remove(Wrappers.<FsIntegralCart>lambdaQuery().eq(FsIntegralCart::getUserId, aLong).in(FsIntegralCart::getId, cartIds));
+        return b ? R.ok("删除商品成功") : R.error("删除商品失败");
+    }
+
+
+    @Login
+    @RepeatSubmit
+    @ApiOperation("从购物车生成订单(兑换按钮)")
+    @PostMapping("/createOrderFromCart")
+    public R createOrderFromCart(@RequestBody CreateOrderFromCartParm param) {
+        Long aLong = Long.valueOf(getUserId());
+        return fsIntegralCartService.createOrderFromCart(param, aLong);
+    }
+}

+ 95 - 0
fs-user-app/src/main/java/com/fs/app/controller/IntegralController.java

@@ -7,6 +7,7 @@ import com.alibaba.fastjson.JSON;
 import com.fs.app.annotation.Login;
 import com.fs.app.annotation.Login;
 import com.fs.common.annotation.RepeatSubmit;
 import com.fs.common.annotation.RepeatSubmit;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.course.service.IFsUserCourseVideoService;
@@ -28,6 +29,7 @@ import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
 
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletRequest;
+import java.math.BigDecimal;
 import java.util.*;
 import java.util.*;
 
 
 
 
@@ -53,6 +55,10 @@ public class IntegralController extends  AppBaseController {
     private ISysConfigService configService;
     private ISysConfigService configService;
     @Autowired
     @Autowired
     private IFsUserCourseVideoService fsUserCourseVideoService;
     private IFsUserCourseVideoService fsUserCourseVideoService;
+    @Autowired
+    private IFsUserActiveLogService fsUserActiveLogService;
+    @Autowired
+    private IFsIntegralRedPacketLogService fsIntegralRedPacketLogService;
     @ApiOperation("获取积分商品列表")
     @ApiOperation("获取积分商品列表")
     @GetMapping("/getIntegralGoodsList")
     @GetMapping("/getIntegralGoodsList")
     @Cacheable(value = "getIntegralGoodsList", key = "#param")
     @Cacheable(value = "getIntegralGoodsList", key = "#param")
@@ -308,4 +314,93 @@ public class IntegralController extends  AppBaseController {
         return integralOrderService.createCartOrder(param);
         return integralOrderService.createCartOrder(param);
     }
     }
 
 
+    @Login
+    @ApiOperation("活动签到窗口是否弹出")
+    @GetMapping("/getUserActiveWindow")
+    public R getUserActiveWindow(HttpServletRequest request){
+        FsUser user=userService.selectFsUserByUserId(Long.parseLong(getUserId()));
+        return R.ok().put("isOpen",fsUserActiveLogService.getUserActiveWindow(user));
+    }
+
+    @Login
+    @ApiOperation("获取用户活动签到记录")
+    @GetMapping("/getUserActiveLog")
+    public R getUserActiveLog(HttpServletRequest request){
+        FsUser user=userService.selectFsUserByUserId(Long.parseLong(getUserId()));
+        return fsUserActiveLogService.getUserActiveLog(user);
+        //获取签到配置
+//        String json=configService.selectConfigByKey("his.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("/withdrawal")
+    @RepeatSubmit
+    public R withdrawal(@RequestBody FsIntegralWithdrawalParam param){
+        String userId = getUserId();
+        if(userId == null){
+            return R.error("请先登录!");
+        }
+        BigDecimal applicationAmount = param.getApplicationAmount();
+        if(applicationAmount == null){
+            return R.error("提现金额不正确");
+        }
+        // 校验1:金额必须大于0
+        if (applicationAmount.compareTo(BigDecimal.ZERO) <= 0) {
+            return R.error("提现金额必须大于0");
+        }
+
+        // 校验2:最小单位为分,即最多保留两位小数
+        if (applicationAmount.scale() > 2) {
+            return R.error("提现金额最小单位为分,请输入正确保留两位小数的金额");
+        }
+        param.setUserId(Long.parseLong(userId));
+        return R.ok(userService.withdrawal(param));
+    }
+
+
+
+    /**
+     * 查询积分佣金红包记录列表
+     */
+    @Login
+    @GetMapping("/getRedPacketLogList")
+    public TableDataInfo list(FsIntegralRedPacketLog param)
+    {
+        String userId = getUserId();
+        if(userId == null){
+            return new TableDataInfo();
+        }
+        param.setUserId(Long.parseLong(userId));
+        startPage();
+        List<FsIntegralRedPacketLog> list = fsIntegralRedPacketLogService.selectFsIntegralRedPacketLogList(param);
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询积分佣金红包记录列表
+     */
+    @Login
+    @GetMapping("/getRedPacketLogByCode")
+    public R getRedPacketLogByCode(@RequestParam("orderCode") String orderCode)
+    {
+        String userId = getUserId();
+        if(userId == null){
+            return R.error("请先登录");
+        }
+        if(StringUtils.isBlank(orderCode)){
+            return R.error("缺少参数");
+        }
+        FsIntegralRedPacketLog redPacketLog = fsIntegralRedPacketLogService.getRedPacketLogByCode(orderCode);
+        if (redPacketLog != null) {
+            if (!Objects.equals(redPacketLog.getUserId(), Long.valueOf(userId))) {
+                return R.ok();
+            }
+        }
+        return R.ok().put("data", redPacketLog);
+    }
 }
 }

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

@@ -15,6 +15,7 @@ import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.his.domain.FsDoctor;
 import com.fs.his.domain.FsDoctor;
 import com.fs.his.domain.FsUser;
 import com.fs.his.domain.FsUser;
 import com.fs.his.dto.FindUsersByDTO;
 import com.fs.his.dto.FindUsersByDTO;
+import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.param.FindUserByParam;
 import com.fs.his.param.FindUserByParam;
 import com.fs.his.param.FsUserCouponUParam;
 import com.fs.his.param.FsUserCouponUParam;
 import com.fs.his.param.FsUserEditPushParam;
 import com.fs.his.param.FsUserEditPushParam;
@@ -57,8 +58,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
 
 
-import static com.fs.his.utils.PhoneUtil.decryptPhoneMk;
-import static com.fs.his.utils.PhoneUtil.encryptPhone;
+import static com.fs.his.utils.PhoneUtil.*;
 
 
 
 
 @Api("个人中心")
 @Api("个人中心")
@@ -84,6 +84,9 @@ public class UserController extends  AppBaseController {
     @Autowired
     @Autowired
     private IQwUserService qwUserService;
     private IQwUserService qwUserService;
 
 
+    @Autowired
+    private FsUserMapper userMapper;
+
 
 
     @Autowired
     @Autowired
     private IFsUserService fsUserService;
     private IFsUserService fsUserService;
@@ -391,4 +394,34 @@ public class UserController extends  AppBaseController {
         map.put("qwUser",qwUserVO);
         map.put("qwUser",qwUserVO);
         return R.ok().put("data",map);
         return R.ok().put("data",map);
     }
     }
+
+    @GetMapping("/findUser")
+    public R findUser(@Param("keyword") String keyword) {
+        Long userId = Long.parseLong(keyword);
+        FsUser user = null;
+        user = userService.selectFsUserByUserId(userId);
+        if(user == null){
+            user = findUserByPhone(keyword);
+        }
+        if (user != null) {
+            return R.ok().put("data", user);
+        } else {
+            return R.error("用户不存在!");
+        }
+    }
+
+    private FsUser findUserByPhone(String phone) {
+        // 先根据加密手机号查询用户
+        String jiami = (encryptPhone(phone));
+        FsUser user = userMapper.selectFsUserByPhoneLimitOne(jiami);
+        if (user == null) {
+            user = userMapper.selectFsUserByPhoneLimitOne(encryptPhoneOldKey(phone));
+        }
+        // 如果没有找到用户,再根据手机号查询
+        if (user == null) {
+            user = userMapper.selectFsUserByPhoneLimitOne(phone);
+
+        }
+        return user;
+    }
 }
 }

+ 5 - 0
fs-user-app/src/main/java/com/fs/app/controller/WxPayController.java

@@ -139,5 +139,10 @@ public class WxPayController {
         }
         }
     }
     }
 
 
+    @PostMapping( "/integralV3TransferNotify")
+    public String integralV3TransferNotify(@RequestBody String notifyData,HttpServletRequest request, HttpServletResponse response) throws Exception {
+        return paymentService.integralV3TransferNotify(notifyData,request);
+    }
+
 
 
 }
 }

+ 35 - 0
fs-user-app/src/main/java/com/fs/app/controller/aiChat/controller/AiDoctorController.java

@@ -0,0 +1,35 @@
+package com.fs.app.controller.aiChat.controller;
+
+import com.fs.aiChat.domain.DoctorAiChatMsg;
+import com.fs.aiChat.vo.DoctorAiVO;
+import com.fs.common.core.domain.R;
+import com.fs.his.service.IHisDoctorAiChatSessionService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping(value="/app/aiDoctor")
+@Api(tags = "AI医生")
+public class AiDoctorController {
+    @Autowired
+    private IHisDoctorAiChatSessionService doctorAiChatSessionService;
+    @ApiOperation(value = "历史会话")
+    @PostMapping("/chatListByUser")
+    public R chatListByUser(DoctorAiVO doctorAiVO) {
+        List<DoctorAiChatMsg> doctorAiChatMsgs = doctorAiChatSessionService.selectDoctorAiChatSessionListByUser(doctorAiVO);
+        return R.ok().put("data", doctorAiChatMsgs);
+    }
+
+//    @ApiOperation(value = "AI医生列表")
+//    @PostMapping("/doctorList")
+//    public R chatByUser(DoctorAiVO doctorAiVO) {
+//        String chat = doctorAiChatSessionService.chatByUser(doctorAiVO);
+//        return R.ok().put("data", chat);
+//    }
+}

+ 121 - 0
fs-user-app/src/main/java/com/fs/app/controller/aiChat/controller/InterestAiController.java

@@ -0,0 +1,121 @@
+package com.fs.app.controller.aiChat.controller;
+
+import com.fs.aiChat.domain.InterestAiChatMsg;
+import com.fs.aiChat.domain.InterestAiSession;
+import com.fs.aiChat.domain.SessionRoleInfo;
+import com.fs.aiChat.mapper.InterestAiChatSessionMapper;
+import com.fs.aiChat.param.InterestAiMessage;
+import com.fs.app.controller.AppBaseController;
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.his.domain.FsInterestAiSession;
+import com.fs.his.service.IHisInterestAiChatSessionService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+@Slf4j
+@RestController
+@RequestMapping(value = "/app/fsInterestAi")
+@Api(tags = "兴趣AI")
+public class InterestAiController extends AppBaseController {
+
+    @Autowired
+    private IHisInterestAiChatSessionService interestAiChatSessionService;
+
+    @Autowired
+    InterestAiChatSessionMapper interestAiChatSessionMapper;
+
+    /**
+     * 创建会话
+     *
+     * @param fsInterestAiSession
+     * @return
+     */
+    @PostMapping("/createSession")
+    @ApiOperation(value = "创建会话")
+    public R createSession(@RequestBody FsInterestAiSession fsInterestAiSession) {
+        FsInterestAiSession session = interestAiChatSessionService.createSession(fsInterestAiSession);
+        return R.ok().put("data", session);
+    }
+
+    /**
+     * 根据当前用户获取用户与所有AI角色的会话列表
+     *
+     * @param session
+     * @return
+     */
+    @GetMapping(value = "/getAllRolesListByUserId")
+    @ApiOperation(value = "根据当前用户获取用户的会话列表")
+    public R getAllRolesListByUserId(InterestAiSession session) {
+        String userId = getUserId();
+        log.info("拿到userId" + userId);
+        if (null != session && session.getUserId() == null) {
+            session.setUserId(Long.valueOf(userId));
+        }
+        startPage();
+        List<InterestAiSession> list = interestAiChatSessionService.getAllRolesListByUserId(session);
+        return R.ok().put("data", list);
+    }
+
+    /**
+     * 根据会话id获取会话的详细信息
+     *
+     * @param sessionId
+     * @return
+     */
+    @GetMapping(value = "/getSessionDetailInfoBySessionIdAndRoleId/{sessionId}/{roleId}")
+    @ApiOperation(value = "根据会话id获取会话的详细信息")
+    public R getSessionDetailInfoBySessionIdAndRoleId(@PathVariable(name = "sessionId") String sessionId,
+                                                      @PathVariable(name = "roleId") Long roleId) {
+
+
+        List<InterestAiChatMsg> chatList = new ArrayList<>();
+        SessionRoleInfo roleInfo = null;
+        if (null != roleId) {
+            roleInfo = interestAiChatSessionService.getSessionRoleInfoByRoleId(roleId);
+        }
+
+        if (StringUtils.isNotBlank(sessionId)) {
+            chatList = interestAiChatSessionService.getSessionChatList(sessionId);
+        }
+        return R.ok().put("roleInfo", roleInfo).put("chatList", chatList);
+    }
+
+    /**
+     * 获取推荐角色列表
+     *
+     * @return
+     */
+    @GetMapping(value = "/getRecommendRoleList")
+    @ApiOperation(value = "获取推荐角色列表")
+    public R getRecommendRoleList() {
+        String userId = getUserId();
+        List<SessionRoleInfo> roleList = interestAiChatSessionService.getRecommendRoleList(userId);
+        return R.ok().put("list", roleList);
+    }
+
+    @PostMapping(value = "/getAiJsonMsgCount")
+    @ApiOperation(value = "获取推荐角色列表")
+    public R getAiJsonMsgCount(@RequestBody InterestAiMessage message) {
+        Integer sessionId = interestAiChatSessionMapper.selectSessionIdByUserAndRole(message.getUserId(),message.getRoleId().intValue());
+        if (null == sessionId) {
+            return R.ok().put("count", 0);
+        }else{
+            message.setSessionId(Long.valueOf(sessionId));
+            Date time = message.getStartTime();
+            String dateString = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, time);
+            message.setBeginTime(dateString);
+            int count = interestAiChatSessionMapper.selectAiMsgBySessionIdAndMsg(message);
+            return R.ok().put("count", count);
+        }
+    }
+
+}

+ 14 - 1
fs-user-app/src/main/java/com/fs/app/controller/course/CourseFsUserController.java

@@ -166,7 +166,7 @@ public class CourseFsUserController extends AppBaseController {
             Long userId = Long.parseLong(getUserId());
             Long userId = Long.parseLong(getUserId());
             param.setUserId(userId);
             param.setUserId(userId);
         }
         }
-        String sourceType = request.getHeader("sourcetype");
+        String sourceType = request.getHeader("Sourcetype");
         if(StringUtils.isNotBlank(sourceType)){
         if(StringUtils.isNotBlank(sourceType)){
             if ("APP".equals(sourceType)) {
             if ("APP".equals(sourceType)) {
                 param.setSource(3);
                 param.setSource(3);
@@ -182,4 +182,17 @@ public class CourseFsUserController extends AppBaseController {
         logger.error("zyp \n【h5看课中途报错】:{}",msg);
         logger.error("zyp \n【h5看课中途报错】:{}",msg);
     }
     }
 
 
+    @Login
+    @ApiOperation("发送红包(以积分提现的形式)")
+    @PostMapping("/withdrawal")
+    @RepeatSubmit
+    public R withdrawal(@RequestBody FsCourseSendRewardUParam param){
+        String userId = getUserId();
+        if(userId == null){
+            return R.error("请先登录!");
+        }
+        param.setUserId(Long.parseLong(userId));
+        return R.ok(courseVideoService.withdrawal(param));
+    }
+
 }
 }

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

@@ -328,7 +328,7 @@ public class CourseQwController extends AppBaseController {
     @UserOperationLog(operationType = FsUserOperationEnum.SENDREWARD)
     @UserOperationLog(operationType = FsUserOperationEnum.SENDREWARD)
     public R sendReward(@RequestBody @Valid FsCourseSendRewardUParam param,HttpServletRequest request)
     public R sendReward(@RequestBody @Valid FsCourseSendRewardUParam param,HttpServletRequest request)
     {
     {
-        String sourceType = request.getHeader("sourcetype");
+        String sourceType = request.getHeader("Sourcetype");
         if(StringUtils.isNotBlank(sourceType)){
         if(StringUtils.isNotBlank(sourceType)){
             if ("APP".equals(sourceType)) {
             if ("APP".equals(sourceType)) {
                 param.setSource(3);
                 param.setSource(3);