Bladeren bron

add:积分提现 风控 app看课发红包

ct 4 dagen geleden
bovenliggende
commit
20ad85fb34
40 gewijzigde bestanden met toevoegingen van 3666 en 124 verwijderingen
  1. 99 0
      fs-admin/src/main/java/com/fs/his/controller/FsConsecutiveWithdrawRecordController.java
  2. 69 0
      fs-admin/src/main/java/com/fs/his/controller/FsIntegralRedPacketLogController.java
  3. 19 4
      fs-admin/src/main/java/com/fs/his/task/CompanyBalanceTask.java
  4. 3 0
      fs-service/src/main/java/com/fs/company/domain/CompanyRedPacketBalanceLogs.java
  5. 5 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyRedPacketBalanceLogsMapper.java
  6. 4 2
      fs-service/src/main/java/com/fs/company/service/ICompanyService.java
  7. 177 24
      fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java
  8. 2 0
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java
  9. 649 72
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  10. 60 0
      fs-service/src/main/java/com/fs/his/config/AppConfig.java
  11. 56 0
      fs-service/src/main/java/com/fs/his/domain/FsConsecutiveWithdrawRecord.java
  12. 69 0
      fs-service/src/main/java/com/fs/his/domain/FsIntegralRedPacketLog.java
  13. 81 0
      fs-service/src/main/java/com/fs/his/mapper/FsConsecutiveWithdrawRecordMapper.java
  14. 103 0
      fs-service/src/main/java/com/fs/his/mapper/FsIntegralRedPacketLogMapper.java
  15. 3 1
      fs-service/src/main/java/com/fs/his/mapper/FsUserIntegralLogsMapper.java
  16. 18 0
      fs-service/src/main/java/com/fs/his/param/CreateOrderFromCartParm.java
  17. 9 0
      fs-service/src/main/java/com/fs/his/param/FsConsecutiveWithdrawRecordParam.java
  18. 13 0
      fs-service/src/main/java/com/fs/his/param/FsIntegralRedPacketLogParam.java
  19. 19 0
      fs-service/src/main/java/com/fs/his/param/FsIntegralWithdrawalParam.java
  20. 66 0
      fs-service/src/main/java/com/fs/his/service/IFsConsecutiveWithdrawRecordService.java
  21. 15 0
      fs-service/src/main/java/com/fs/his/service/IFsIntegralCartService.java
  22. 74 0
      fs-service/src/main/java/com/fs/his/service/IFsIntegralRedPacketLogService.java
  23. 9 0
      fs-service/src/main/java/com/fs/his/service/IFsStorePaymentService.java
  24. 3 4
      fs-service/src/main/java/com/fs/his/service/IFsUserIntegralLogsService.java
  25. 3 0
      fs-service/src/main/java/com/fs/his/service/IFsUserService.java
  26. 405 0
      fs-service/src/main/java/com/fs/his/service/impl/FsConsecutiveWithdrawRecordServiceImpl.java
  27. 454 0
      fs-service/src/main/java/com/fs/his/service/impl/FsIntegralRedPacketLogServiceImpl.java
  28. 332 7
      fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java
  29. 6 4
      fs-service/src/main/java/com/fs/his/service/impl/FsUserIntegralLogsServiceImpl.java
  30. 199 3
      fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java
  31. 12 0
      fs-service/src/main/java/com/fs/his/vo/FsConsecutiveWithdrawRecordVo.java
  32. 13 0
      fs-service/src/main/java/com/fs/his/vo/FsIntegralRedPacketLogVo.java
  33. 17 2
      fs-service/src/main/resources/mapper/company/CompanyRedPacketBalanceLogsMapper.xml
  34. 137 0
      fs-service/src/main/resources/mapper/his/FsConsecutiveWithdrawRecordMapper.xml
  35. 266 0
      fs-service/src/main/resources/mapper/his/FsIntegralRedPacketLogMapper.xml
  36. 74 0
      fs-service/src/main/resources/mapper/his/FsUserIntegralLogsMapper.xml
  37. 10 1
      fs-service/src/main/resources/mapper/his/FsUserMapper.xml
  38. 95 0
      fs-user-app/src/main/java/com/fs/app/controller/IntegralController.java
  39. 5 0
      fs-user-app/src/main/java/com/fs/app/controller/WxPayController.java
  40. 13 0
      fs-user-app/src/main/java/com/fs/app/controller/course/CourseFsUserController.java

+ 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;
 
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
 import com.fs.company.service.ICompanyService;
 import com.fs.company.vo.RedPacketMoneyVO;
 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.stereotype.Component;
 
+import java.util.Date;
 import java.util.List;
 
 /**
@@ -62,15 +65,27 @@ public class CompanyBalanceTask {
     }
 
     /**
-     * @Description: 红包余额回滚(回滚的是客户没领取的红包),红包记录表中,两天没领取的记录不会再发送
+     * @Description: 优化成回滚前查询记录,一笔一笔回滚
      * @Param: 每天0点执行一次
      * @Return:
      * @Author xgb
      * @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);
     }
 
     /**

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

@@ -42,5 +42,8 @@ public class CompanyRedPacketBalanceLogs extends BaseEntity{
     /** 是否处理状态(0-初始化,1-已同步) */
     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.fs.company.domain.Company;
 import com.fs.company.domain.CompanyRedPacketBalanceLogs;
+import org.apache.ibatis.annotations.Param;
 
 /**
  * 企业红包余额记录Mapper接口
@@ -61,4 +62,8 @@ public interface CompanyRedPacketBalanceLogsMapper extends BaseMapper<CompanyRed
     int deleteCompanyRedPacketBalanceLogsByLogsIds(Long[] logsIds);
 
     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 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();
 
@@ -187,7 +187,7 @@ public interface ICompanyService
      */
     void batchUpdateCompany(List<Company> list);
 
-    void rollbackRedPacketMoney();
+    void rollbackRedPacketMoney(String createSTime, String createETime);
 
 
     List<CompanyVO> liveShowList(CompanyParam param);
@@ -197,4 +197,6 @@ public interface ICompanyService
     void addCompanyTuiLiveMoney(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.param.CompanyLiveShowParam;
 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.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.his.config.AppConfig;
 import com.fs.his.config.StoreConfig;
 import com.fs.his.domain.FsInquiryOrder;
 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.mapper.SysConfigMapper;
 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.google.gson.Gson;
 import org.apache.commons.collections4.CollectionUtils;
@@ -49,10 +60,10 @@ import org.redisson.api.RLock;
 import org.redisson.api.RedissonClient;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
-import com.fs.company.service.ICompanyService;
 import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.support.TransactionTemplate;
@@ -127,6 +138,12 @@ public class CompanyServiceImpl implements ICompanyService
     @Autowired
     private CompanyRedPacketBalanceLogsMapper companyRedPacketBalanceLogsMapper;
 
+    @Autowired
+    private ICompanyConfigService companyConfigService;
+
+    @Autowired
+    private WxOpenProperties openProperties;
+
 
     @Override
     public List<CompanyVO> liveShowList(CompanyParam param) {
@@ -1448,7 +1465,7 @@ public class CompanyServiceImpl implements ICompanyService
                                     // 记录余额变更日志
                                     String remark = "同步公司余额,差额: " + amount+"(正数为增加,负数为扣减)";
                                     // 实际不发生交易只是从缓存同步金额到数据库中 交易金额登记为0,备注清楚同步的金额
-                                    asyncRecordBalanceLog(company.getCompanyId(),new BigDecimal(0),17,redisMoney,remark);
+                                    asyncRecordBalanceLog(company.getCompanyId(),new BigDecimal(0),17,redisMoney,remark, 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: 红包充值
      * @Param: type 充值类型 1 充值 2 扣款
@@ -1501,7 +1525,7 @@ public class CompanyServiceImpl implements ICompanyService
                 redisCache.setCacheObject(companyMoneyKey, newMoney.toString());
 
                 // 异步登记余额添加日志
-                asyncRecordBalanceLog(companyId,money,16,newMoney,"红包充值(负数为扣款)");
+                asyncRecordBalanceLog(companyId,money,16,newMoney,"红包充值(负数为扣款)", null);
 
             } else {
                 logger.error("获取redis锁失败,异常请求参数companyId:{},money:{},type:{}",companyId,money, type);
@@ -1527,15 +1551,17 @@ public class CompanyServiceImpl implements ICompanyService
 
     /**
      * 异步登记余额添加日志  xgb
+     *
      * @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
     @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 {
             CompanyRedPacketBalanceLogs log = new CompanyRedPacketBalanceLogs();
             log.setCompanyId(companyId);
@@ -1544,6 +1570,7 @@ public class CompanyServiceImpl implements ICompanyService
             log.setLogsType(logType); // 同步余额
             log.setBalance(balance);
             log.setCreateTime(new Date());
+            log.setRedPacketId(logId);
             companyRedPacketBalanceLogsMapper.insertCompanyRedPacketBalanceLogs(log);
         } catch (Exception e) {
             logger.error("异步登记红包余额日志失败 - 公司ID: {}, 金额: {}, 余额: {}, 备注: {}",
@@ -1574,7 +1601,7 @@ public class CompanyServiceImpl implements ICompanyService
                     // 实际不发生交易只是从缓存获取当天余额报错25小时 交易金额登记为0,备注清楚同步的金额
                     String remark = "时间:" + time +",当前公司余额,金额: " + 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;
             });
@@ -1596,20 +1623,67 @@ public class CompanyServiceImpl implements ICompanyService
      * @Param:
      * @Return:
      * @Author xgb
-     * @Date 2025/11/7 9:53
+     * @Date 2025/12/25 9:32
      */
     @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){
-                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;
             }
+
             String companyMoneyKey = FsConstants.COMPANY_MONEY_KEY + 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)) {
                         redisMoney = new BigDecimal(moneyStr);
                     }else {
-                        logger.error("缓存公司id:{}的余额不存在,回滚金额{}",company.getCompanyId(),company.getMoney());
+                        logger.error("缓存公司id:{}的余额不存在,回滚金额{}",company.getCompanyId(),redLogs.getAmount());
                         return;
                     }
-                    BigDecimal newMoney = redisMoney.add(company.getMoney());
+                    BigDecimal newMoney = redisMoney.add(redLogs.getAmount());
                     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) {
                 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/service/IFsUserCourseVideoService.java

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

+ 649 - 72
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -297,6 +297,9 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     @Autowired
     private FsUserSignMapper fsUserSignMapper;
 
+    @Autowired
+    private FsCourseRewardVideoRelationMapper videoRelationMapper;
+
 
     /**
      * 查询课堂视频
@@ -1538,9 +1541,11 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
             if (param.getSource() != 3 && config.getMiniAppAuthType() == 2 && StringUtil.strIsNullOrEmpty(user.getMpOpenId())) {
                 return R.error(401, "授权后可继续!");
             }
-
+            // 根据奖励类型发放不同奖励 //todo
+            if ((param.getUserId() == 42 || param.getUserId() == 41 || param.getUserId() == 816473) && (param.getSource() == 3)){
+                return withdrawal(param);
+            }
             log.info("奖励类型:{}", config.getRewardType());
-            // 根据奖励类型发放不同奖励
             switch (config.getRewardType()) {
                 // 红包奖励
                 case 1:
@@ -1930,7 +1935,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
 
                 // 异步登记余额扣减日志
                 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());
 
                 return sendRedPacket;
@@ -2041,11 +2046,6 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         packetParam.setOpenId(user.getMpOpenId());
         // 来源是小程序切换openId
         if (param.getSource() == 2) {
-            //处理多小程序问题
-//            Company company = companyMapper.selectCompanyById(param.getCompanyId());
-//            if (company.getCourseMiniAppId()==null){
-//                return R.error("销售公司参数错误,未绑定小程序");
-//            }
             if (user.getMpOpenId() != null && !isNewWxMerchant) {
                 packetParam.setOpenId(user.getMpOpenId());
             } else {
@@ -2193,7 +2193,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
 
                     // 异步登记余额扣减日志
                     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;
 
@@ -2212,48 +2212,48 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                     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 {
@@ -2394,31 +2394,49 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
 
     /**
      * 获取用户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 +2452,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         FsUser userMap = new FsUser();
         userMap.setUserId(user.getUserId());
         userMap.setIntegral(user.getIntegral() + config.getAnswerIntegral());
+        userMap.setWithdrawIntegral(user.getWithdrawIntegral() + config.getAnswerIntegral());
         fsUserMapper.updateFsUser(userMap);
 
         // 记录积分日志
@@ -4853,6 +4872,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) {
         int maxRetry = 3;
@@ -5037,6 +5094,39 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         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 +5160,492 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         return R.ok("奖励发放成功");
     }
 
+    private R executeWithdrawal(FsCourseSendRewardUParam param){
+        log.info("进入用户判断");
+        FsUser user = fsUserMapper.selectFsUserByUserId(param.getUserId());
+        if (user == null) {
+            return R.error("未识别到用户信息");
+        }
+
+        FsCourseWatchLog 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) {
+            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);
+    }
+
 }
 

+ 60 - 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 lombok.Data;
 
+import java.math.BigDecimal;
 import java.util.List;
 
 @Data
@@ -18,4 +19,63 @@ public class AppConfig {
     private String corpUrl; //APP客服配置 企业主体链接
     private Integer addIntegral; //玩一局游戏加多少积分
     private Integer defaultRewardGold; //看视频获取多少金币
+
+    //积分提现商户配置
+    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;
+
 }

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

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

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

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

+ 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.enums.DataSourceType;
 import com.fs.his.domain.FsUserIntegralLogs;
+import com.fs.his.param.AdProfitDetailStatisticsParam;
 import com.fs.his.param.FsUserIntegralLogsListUParam;
 import com.fs.his.param.FsUserIntegralLogsParam;
 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);
 
-    @DataSource(DataSourceType.SHARDING)
+//    @DataSource(DataSourceType.SHARDING)
     List<ExchangeDetailVo> getExchangDetailList(ExchangeDetailVo detailVo);
 
+    Long sumIntegralByLogTypeAndCreateTime(@Param("logType") Integer logType,@Param("param") AdProfitDetailStatisticsParam param);
 }

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

+ 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;
 
 import com.baomidou.mybatisplus.extension.service.IService;
+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.vo.FsIntegralCartVO;
+import com.fs.his.vo.GetFsIntegralCartDetailsVo;
+import com.fs.his.vo.GetFsIntegralCartListVo;
 
 import javax.validation.constraints.NotNull;
 import java.util.List;
@@ -48,4 +55,12 @@ public interface IFsIntegralCartService extends IService<FsIntegralCart> {
      * @return  list
      */
     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();
+}

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

+ 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.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.FsUserIntegralLogsListVO;
 
@@ -86,4 +83,6 @@ public interface IFsUserIntegralLogsService
 
     //app获取新人福利完成情况
     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.dto.FindUsersByDTO;
 import com.fs.his.param.FindUserByParam;
+import com.fs.his.param.FsIntegralWithdrawalParam;
 import com.fs.his.param.FsUserParam;
 import com.fs.his.vo.FsUserVO;
 import com.fs.his.vo.FsUserExportListVO;
@@ -249,4 +250,6 @@ public interface IFsUserService
     R integralExchange(Long userId);
 
     R exchangDetail(Map<String, Object> params);
+
+    R withdrawal(FsIntegralWithdrawalParam param);
 }

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

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

+ 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.IFsUserCourseOrderService;
 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.dto.PayConfigDTO;
 import com.fs.his.enums.FsStoreAfterSalesStatusEnum;
@@ -256,6 +258,9 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
     @Autowired
     FsInquiryOrderMapper inquiryOrderMapper;
 
+    @Autowired
+    private IFsIntegralRedPacketLogService integralRedPacketLogService;
+
     /**
      * 红包账户锁
      */
@@ -1262,7 +1267,6 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
         return fsStorePaymentMapper.selectFsStorePaymentExcelVOCount(fsStorePayment);
     }
 
-
     @Override
     public R sendRedPacketV3(WxSendRedPacketParam param) {
         String json = configService.selectConfigByKey("redPacket.config");
@@ -1321,10 +1325,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
     public String v3TransferNotify(String notifyData, HttpServletRequest request) {
-        logger.info("zyp \n【收到转账回调V3】:{}",notifyData);
+        logger.info("zyp \n【收到转账回调】:{}",notifyData);
         try {
             String json = configService.selectConfigByKey("redPacket.config");
             RedPacketConfig config = JSONUtil.toBean(json, RedPacketConfig.class);
@@ -1338,10 +1406,10 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
             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.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);
                 if (r.get("code").equals(200)){
                     return WxPayNotifyResponse.success("处理成功");
@@ -1352,7 +1420,6 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
                 return WxPayNotifyResponse.fail("");
             }
         } catch (WxPayException e) {
-            e.printStackTrace();
             logger.error("zyp \n【转账回调异常】:{}", e.getReturnMsg());
             return WxPayNotifyResponse.fail(e.getMessage());
         }
@@ -1953,6 +2020,87 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
         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
     public R paymentByWxaCode(FsStorePaymentPayParam param) {
         FsUser user = userMapper.selectFsUserById(param.getUserId());
@@ -2102,4 +2250,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);
+        }
+    }
+
 }

+ 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.FsUserMapper;
 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.vo.FsUserIntegralLogsListUVO;
 import com.fs.his.vo.FsUserIntegralLogsListVO;
@@ -636,4 +633,9 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
         map.put("isFinishFirstOrderPoint", isFinishFirstOrderPoint);
         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);
+    }
 }

+ 199 - 3
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.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
 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.redis.RedisCache;
 import com.fs.common.enums.ImTypeEnum;
+import com.fs.common.exception.CustomException;
 import com.fs.common.param.LoginMaWxParam;
 import com.fs.common.utils.*;
 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.FsCourseAnalysisVO;
 import com.fs.course.vo.newfs.FsUserCourseVideoPageListVO;
+import com.fs.his.config.AppConfig;
 import com.fs.his.config.IntegralConfig;
 import com.fs.his.domain.*;
 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.enums.FsUserIntegralLogTypeEnum;
 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.utils.PhoneUtil;
 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.vo.FsUserAndCompanyAndDoctorVo;
 import com.fs.watch.service.WatchUserService;
+import com.github.binarywang.wxpay.bean.transfer.TransferBillsResult;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 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.util.Asserts;
 import org.apache.http.util.EntityUtils;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.BeanUtils;
@@ -196,6 +200,14 @@ public class FsUserServiceImpl implements IFsUserService {
     private IFsUserIntegralLogsService fsUserIntegralLogsService;
     @Autowired
     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);
     }
 
+    /**
+     * 处理用户与小程序的绑定
+     */
+    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
     public void handleFsUserWx(FsUser user, LoginMaWxParam param, WxMaJscode2SessionResult session) {
         if (user == null) return;
@@ -1848,4 +1878,170 @@ public class FsUserServiceImpl implements IFsUserService {
         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();
+    }
 }

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

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

+ 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="logsType"    column="logs_type"    />
         <result property="status"    column="status"    />
+        <result property="redPacketId"    column="red_packet_id"    />
     </resultMap>
 
     <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>
 
     <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
         company_red_packet_balance_logs l
         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 * FROM company WHERE company_id = #{companyId}
     </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 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="logsType != null">logs_type,</if>
             <if test="status != null">status,</if>
+            <if test="redPacketId != null">red_packet_id,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <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="logsType != null">#{logsType},</if>
             <if test="status != null">#{status},</if>
+            <if test="redPacketId != null">#{redPacketId},</if>
          </trim>
     </insert>
 
@@ -73,9 +80,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="balance != null">balance = #{balance},</if>
             <if test="logsType != null">logs_type = #{logsType},</if>
             <if test="status != null">status = #{status},</if>
+            <if test="redPacketId != null">red_packet_id = #{redPacketId},</if>
         </trim>
         where logs_id = #{logsId}
     </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 from company_red_packet_balance_logs where logs_id = #{logsId}

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

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

+ 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}
         order by create_time desc
     </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 into fs_user_integral_logs
@@ -127,4 +130,75 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             #{id}
         </foreach>
     </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>

+ 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="appleKey"    column="apple_key"    />
         <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>
 
     <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,
                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,
-               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>
 
     <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="appleKey != null">apple_key = #{appleKey},</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>
         where user_id = #{userId}
     </update>

+ 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.common.annotation.RepeatSubmit;
 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.StringUtils;
 import com.fs.course.service.IFsUserCourseVideoService;
@@ -28,6 +29,7 @@ import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletRequest;
+import java.math.BigDecimal;
 import java.util.*;
 
 
@@ -53,6 +55,10 @@ public class IntegralController extends  AppBaseController {
     private ISysConfigService configService;
     @Autowired
     private IFsUserCourseVideoService fsUserCourseVideoService;
+    @Autowired
+    private IFsUserActiveLogService fsUserActiveLogService;
+    @Autowired
+    private IFsIntegralRedPacketLogService fsIntegralRedPacketLogService;
     @ApiOperation("获取积分商品列表")
     @GetMapping("/getIntegralGoodsList")
     @Cacheable(value = "getIntegralGoodsList", key = "#param")
@@ -308,4 +314,93 @@ public class IntegralController extends  AppBaseController {
         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);
+    }
 }

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

+ 13 - 0
fs-user-app/src/main/java/com/fs/app/controller/course/CourseFsUserController.java

@@ -182,4 +182,17 @@ public class CourseFsUserController extends AppBaseController {
         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));
+    }
+
 }