瀏覽代碼

1.提交济世百康app看课红包和app直播订单支付

jzp 1 周之前
父節點
當前提交
7ffa26b9d0
共有 34 個文件被更改,包括 1751 次插入32 次删除
  1. 8 1
      fs-company/src/main/java/com/fs/company/controller/live/LiveController.java
  2. 1 1
      fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java
  3. 7 0
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  4. 1 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseRedPacketLog.java
  5. 63 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseReward.java
  6. 79 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseRewardRound.java
  7. 47 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseRewardVideoRelation.java
  8. 75 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseRewardRoundMapper.java
  9. 87 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseRewardVideoRelationMapper.java
  10. 7 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java
  11. 1 1
      fs-service/src/main/java/com/fs/course/param/FsCourseSendRewardUParam.java
  12. 2 0
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java
  13. 615 3
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  14. 37 0
      fs-service/src/main/java/com/fs/course/utils/luckyDraw/LotteryUtil.java
  15. 77 0
      fs-service/src/main/java/com/fs/course/utils/luckyDraw/Prize.java
  16. 49 0
      fs-service/src/main/java/com/fs/his/config/AppRedPacketConfig.java
  17. 2 0
      fs-service/src/main/java/com/fs/his/service/IFsStorePaymentService.java
  18. 135 0
      fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java
  19. 7 0
      fs-service/src/main/java/com/fs/live/mapper/LiveMapper.java
  20. 2 0
      fs-service/src/main/java/com/fs/live/service/ILiveService.java
  21. 3 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java
  22. 5 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
  23. 1 1
      fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java
  24. 43 6
      fs-service/src/main/java/com/fs/utils/ContentCheckUtil.java
  25. 3 3
      fs-service/src/main/resources/application-config-druid-jsbk.yml
  26. 48 0
      fs-service/src/main/resources/application-druid-jzzx.yml
  27. 138 0
      fs-service/src/main/resources/mapper/course/FsCourseRewardVideoRelationMapper.xml
  28. 153 0
      fs-service/src/main/resources/mapper/course/RewardRoundMapper.xml
  29. 20 0
      fs-service/src/main/resources/mapper/qw/QwSopSmsLogsMapper.xml
  30. 8 9
      fs-user-app/src/main/java/com/fs/app/controller/AppLoginController.java
  31. 13 0
      fs-user-app/src/main/java/com/fs/app/controller/course/CourseFsUserController.java
  32. 7 6
      fs-user-app/src/main/java/com/fs/app/controller/live/LiveOrderController.java
  33. 2 1
      fs-user-app/src/main/java/com/fs/app/controller/store/IndexScrmController.java
  34. 5 0
      fs-user-app/src/main/java/com/fs/app/controller/store/PayScrmController.java

+ 8 - 1
fs-company/src/main/java/com/fs/company/controller/live/LiveController.java

@@ -8,6 +8,7 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.model.LoginUser;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.http.HttpUtils;
@@ -69,7 +70,13 @@ public class LiveController extends BaseController
     public TableDataInfo listToLiveNoEnd(Live live)
     {
         startPage();
-        List<Live> list = liveService.listToLiveNoEnd(live);
+        List<Live> list = new ArrayList<>();
+        if (CloudHostUtils.hasCloudHostName("济世百康") ) {
+            //直播也发
+            list = liveService.listToLiveNoEndNew(live);
+        }else{
+            list = liveService.listToLiveNoEnd(live);
+        }
         return getDataTable(list);
     }
 

+ 1 - 1
fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java

@@ -763,7 +763,7 @@ public class IpadSendServer {
             sendShortLink = livePrefix + obj.getString("link");
         }
         //解析课程短链信息
-        String coursePrefix = "/pages/courseAnswer/index?link=";
+        String coursePrefix = "/courseH5/pages/course/learning?course=";
         if (miniProgramPage.startsWith(coursePrefix)) {
             JSONObject obj = JSONObject.parseObject(miniProgramPage.substring(coursePrefix.length()));
             sendShortLink = coursePrefix + obj.getString("link");

+ 7 - 0
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -6,6 +6,7 @@ import com.alibaba.fastjson.JSONObject;
 import com.fs.common.constant.LiveKeysConstant;
 import com.fs.common.core.redis.RedisCacheT;
 import com.fs.common.exception.base.BaseException;
+import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.date.DateUtil;
 import com.fs.his.domain.FsUser;
 import com.fs.his.service.IFsUserService;
@@ -26,6 +27,7 @@ import com.fs.live.domain.*;
 import com.fs.live.service.*;
 import com.fs.live.vo.LiveGoodsVo;
 import com.fs.newAdv.service.ILeadService;
+import com.fs.utils.ContentCheckUtil;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.time.DateUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -417,6 +419,11 @@ public class WebSocketServer {
         long liveId = (long) userProperties.get("liveId");
         long userType = (long) userProperties.get("userType");
         boolean isAdmin = false;
+        if (CloudHostUtils.hasCloudHostName("济世百康") ) {
+            if(!ContentCheckUtil.checkText(message)){
+                return;
+            }
+        }
 
         SendMsgVo msg = JSONObject.parseObject(message, SendMsgVo.class);
         if(msg.isOn()) return;

+ 1 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseRedPacketLog.java

@@ -76,4 +76,5 @@ public class FsCourseRedPacketLog extends BaseEntity
     //商户号
     private String mchId;
 
+    private Integer watchType;
 }

+ 63 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseReward.java

@@ -0,0 +1,63 @@
+package com.fs.course.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 奖励配置对象 fs_course_reward
+ *
+ * @author 杨衍生
+ * @date 2025-09-02
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class FsCourseReward extends BaseEntity{
+
+    /** 主键ID */
+    private Long id;
+
+    /** 奖励名称 */
+    @Excel(name = "奖励名称")
+    private String name;
+
+    /** 奖励描述 */
+    @Excel(name = "奖励描述")
+    private String description;
+
+    /** 奖励类型 (1:宝箱, 2:红包, 3:积分, 4:转盘, 5:保底转盘) */
+    @Excel(name = "奖励类型 (1:宝箱, 2:红包, 3:积分, 4:转盘, 5:保底转盘)")
+    private Long rewardType;
+
+    /** 状态 (0:禁用, 1:启用) */
+    @Excel(name = "状态 (0:禁用, 1:启用)")
+    private Long status;
+
+    /** 期望值 */
+    @Excel(name = "期望值")
+    private String expectedValue;
+
+    /** 创建人ID */
+    @Excel(name = "创建人ID")
+    private Long createId;
+
+    /** 实际获得的奖励内容 */
+    @Excel(name = "实际获得的奖励内容")
+    private String actualRewards;
+
+    /**
+     * 关闭宝箱url
+     */
+    private String closeChestUrl;
+
+    /**
+     * 开启宝箱url
+     */
+    private String openChestUrl;
+
+    /**
+     * 奖励id
+     */
+    private Long [] rewardIds;
+}

+ 79 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseRewardRound.java

@@ -0,0 +1,79 @@
+package com.fs.course.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 奖励领取记录对象 reward_round
+ *
+ * @author 杨衍生
+ * @date 2025-09-02
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class FsCourseRewardRound extends BaseEntity{
+
+    /** 主键ID */
+    private Long id;
+
+    /** 奖励ID */
+    @Excel(name = "奖励ID")
+    private Long rewardId;
+
+    /** 领取用户ID */
+    @Excel(name = "领取用户ID")
+    private Long userId;
+    private String userIdByName;
+
+    /** 奖励类型 */
+    @Excel(name = "奖励类型")
+    private Long rewardType;
+
+    /** 公司ID */
+    @Excel(name = "公司ID")
+    private Long companyId;
+    private String companyName;
+
+    /** 实际领取到的奖励 */
+    @Excel(name = "实际领取到的奖励")
+    private String actualRewards;
+
+    /** 创建人ID */
+    @Excel(name = "创建人ID")
+    private Long createId;
+
+    /** 奖励状态 (0:已作废, 1:已领取, 2:已过期) */
+    @Excel(name = "奖励状态 (0:已作废, 1:已领取, 2:已过期)")
+    private Long status;
+
+    /** 规则id */
+    @Excel(name = "规则id")
+    private String ruleId;
+
+    /** 看课记录id */
+    @Excel(name = "看课记录id")
+    private Long watchId;
+
+    /** 奖励规则关联id */
+    @Excel(name = "奖励规则关联id")
+    private Long rewardVideoRelationId;
+    /**
+     * 时长
+     */
+    private String second;
+    /**
+     * 小节id
+     */
+    private Long videoId;
+    private String qwUserId;
+    private Long qwExternalId;
+
+    private Integer linkType;
+
+    /**
+     * 奖励商品ID
+     */
+    private Long goodsId;
+}

+ 47 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseRewardVideoRelation.java

@@ -0,0 +1,47 @@
+package com.fs.course.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 奖励与视频小节关联关系对象 fs_course_reward_video_relation
+ *
+ * @author fs
+ * @date 2025-09-03
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class FsCourseRewardVideoRelation extends BaseEntity{
+
+    /** 关联关系ID */
+    private Long id;
+
+    /** 关联reward_.id */
+    @Excel(name = "关联reward_.id")
+    private Long rewardId;
+
+    /** 关联视频小节ID */
+    @Excel(name = "关联视频小节ID")
+    private Long videoSectionId;
+
+    /** 标志百分比 */
+    @Excel(name = "标志百分比")
+    private String mark;
+
+
+    /** 创建人ID */
+    @Excel(name = "创建人ID")
+    private Long createId;
+
+    /** 创建人ID */
+    @Excel(name = "创建人ID")
+    private Long updateId;
+    /** 公司ID */
+    @Excel(name = "公司ID")
+    private Long companyId;
+
+    private Integer type;
+
+}

+ 75 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseRewardRoundMapper.java

@@ -0,0 +1,75 @@
+package com.fs.course.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.vo.RedPacketMoneyVO;
+import com.fs.course.domain.FsCourseRewardRound;
+import org.apache.ibatis.annotations.Param;
+
+import java.time.LocalDate;
+import java.util.List;
+
+/**
+ * 奖励领取记录Mapper接口
+ *
+ * @author 杨衍生
+ * @date 2025-09-02
+ */
+public interface FsCourseRewardRoundMapper extends BaseMapper<FsCourseRewardRound>{
+    /**
+     * 查询奖励领取记录
+     *
+     * @param id 奖励领取记录主键
+     * @return 奖励领取记录
+     */
+    FsCourseRewardRound selectFsCourseRewardRoundById(Long id);
+
+    /**
+     * 查询奖励领取记录列表
+     *
+     * @param rewardRound 奖励领取记录
+     * @return 奖励领取记录集合
+     */
+    List<FsCourseRewardRound> selectFsCourseRewardRoundList(FsCourseRewardRound rewardRound);
+
+    /**
+     * 新增奖励领取记录
+     *
+     * @param rewardRound 奖励领取记录
+     * @return 结果
+     */
+    int insertFsCourseRewardRound(FsCourseRewardRound rewardRound);
+
+    /**
+     * 修改奖励领取记录
+     *
+     * @param rewardRound 奖励领取记录
+     * @return 结果
+     */
+    int updateFsCourseRewardRound(FsCourseRewardRound rewardRound);
+
+    /**
+     * 删除奖励领取记录
+     *
+     * @param id 奖励领取记录主键
+     * @return 结果
+     */
+    int deleteFsCourseRewardRoundById(Long id);
+
+    /**
+     * 批量删除奖励领取记录
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteFsCourseRewardRoundByIds(Long[] ids);
+
+    List<RedPacketMoneyVO> selectFsCourseRewardRoundByAmount(@Param("start") String start,@Param("end") String end);
+
+    List<RedPacketMoneyVO> selectFsCourseRewardRoundByAmountForLuckyBag(@Param("start") String start,@Param("end") String end);
+
+    /**
+     * 查询1000条指定状态且小于指定时间的数据
+     */
+    List<FsCourseRewardRound> get1kByStatusAndLtDate(@Param("status") int status, @Param("endTime") LocalDate endTime);
+
+}

+ 87 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseRewardVideoRelationMapper.java

@@ -0,0 +1,87 @@
+package com.fs.course.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.course.domain.FsCourseReward;
+import com.fs.course.domain.FsCourseRewardVideoRelation;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+
+/**
+ * 奖励与视频小节关联关系Mapper接口
+ *
+ * @author fs
+ * @date 2025-09-03
+ */
+public interface FsCourseRewardVideoRelationMapper extends BaseMapper<FsCourseRewardVideoRelation>{
+    /**
+     * 查询奖励与视频小节关联关系
+     *
+     * @param id 奖励与视频小节关联关系主键
+     * @return 奖励与视频小节关联关系
+     */
+    FsCourseRewardVideoRelation selectFsCourseRewardVideoRelationById(Long id);
+
+    /**
+     * 查询奖励与视频小节关联关系列表
+     *
+     * @param fsCourseRewardVideoRelation 奖励与视频小节关联关系
+     * @return 奖励与视频小节关联关系集合
+     */
+    List<FsCourseRewardVideoRelation> selectFsCourseRewardVideoRelationList(FsCourseRewardVideoRelation fsCourseRewardVideoRelation);
+
+    /**
+     * 新增奖励与视频小节关联关系
+     *
+     * @param fsCourseRewardVideoRelation 奖励与视频小节关联关系
+     * @return 结果
+     */
+    int insertFsCourseRewardVideoRelation(FsCourseRewardVideoRelation fsCourseRewardVideoRelation);
+
+    /**
+     * 修改奖励与视频小节关联关系
+     *
+     * @param fsCourseRewardVideoRelation 奖励与视频小节关联关系
+     * @return 结果
+     */
+    int updateFsCourseRewardVideoRelation(FsCourseRewardVideoRelation fsCourseRewardVideoRelation);
+
+    /**
+     * 删除奖励与视频小节关联关系
+     *
+     * @param id 奖励与视频小节关联关系主键
+     * @return 结果
+     */
+    int deleteFsCourseRewardVideoRelationById(Long id);
+
+    /**
+     * 批量删除奖励与视频小节关联关系
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteFsCourseRewardVideoRelationByIds(Long[] ids);
+
+    /**
+     * 根据公司ID、小节ID和类型删除关系
+     */
+    void deleteByTypes(@Param("videoId") Long videoId, @Param("companyId") Long companyId, @Param("types") List<Integer> types);
+
+    /**
+     * 根据公司ID、小节ID和类型查询奖励ID
+     */
+    Long getRewardIdByCompanyIdAndVideoIdAndType(@Param("companyId") Long companyId, @Param("videoId") Long videoId, @Param("type") int type);
+
+    /**
+     * 根据公司ID、小节ID和类型查询奖励配置
+     */
+    FsCourseReward getRewardByCompanyIdAndVideoIdAndType(@Param("companyId") Long companyId, @Param("videoId") Long videoId, @Param("type") Integer type);
+
+    List<FsCourseRewardVideoRelation> selectFsCourseRewardVideoRelationListByType(FsCourseRewardVideoRelation fsCourseRewardVideoRelation);
+
+    /**
+     * 根据公司ID、小节ID和奖励ID查询配置关系
+     */
+    FsCourseRewardVideoRelation selectByCompanyIdAndVideoIdAndRewardId(@Param("companyId") Long companyId, @Param("videoId") Long videoId, @Param("rewardId") Long rewardId);
+}

+ 7 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java

@@ -910,6 +910,13 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
      */
     List<AppSalesWatchLogReportVO> selectAppDeptOrderStats(FsCourseWatchLogStatisticsListParam param);
 
+    @Select("select * from fs_course_watch_log " +
+            "where video_id = #{videoId} " +
+            "and company_user_id = #{companyUserId} " +
+            "and user_id = #{userId}  order by create_time desc limit 1")
+    FsCourseWatchLog getWatchCourseVideoByFsUserNew(@Param("userId") Long userId, @Param("videoId") Long videoId, @Param("companyUserId") Long companyUserId);
+
+
     /**
      * 获取过期数据
      * **/

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

@@ -36,7 +36,7 @@ public class FsCourseSendRewardUParam implements Serializable
     private Long periodId;
     @NotBlank(message = "小程序参数不能为空")
     private String appId; //前端传来的小程序的appid
-
+    private Integer rewardType; //奖励类型 1红包 2积分 3随机转盘 4保底转盘
     private String code;
 
 }

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

@@ -283,4 +283,6 @@ public interface IFsUserCourseVideoService extends IService<FsUserCourseVideo> {
      * @return
      */
     R createAppFd(LuckyBagCollectRecord param);
+
+    R withdrawal(FsCourseSendRewardUParam param);
 }

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

@@ -49,13 +49,15 @@ import com.fs.course.service.IFsCourseLinkService;
 import com.fs.course.service.IFsUserCompanyBindService;
 import com.fs.course.service.IFsUserCompanyUserService;
 import com.fs.course.service.IFsUserCourseVideoService;
+import com.fs.course.utils.luckyDraw.LotteryUtil;
+import com.fs.course.utils.luckyDraw.Prize;
 import com.fs.course.vo.*;
 import com.fs.course.vo.newfs.*;
 import com.fs.enums.ExceptionCodeEnum;
 import com.fs.his.config.AppConfig;
-import com.fs.his.domain.FsUser;
-import com.fs.his.domain.FsUserIntegralLogs;
-import com.fs.his.domain.FsUserWx;
+import com.fs.his.domain.*;
+import com.fs.his.mapper.FsCouponMapper;
+import com.fs.his.mapper.FsUserCouponMapper;
 import com.fs.his.mapper.FsUserIntegralLogsMapper;
 import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.param.WxSendRedPacketParam;
@@ -238,6 +240,14 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     private IFsUserCompanyUserService userCompanyUserService;
     @Autowired
     private SysDictDataMapper dictDataMapper;
+    @Autowired
+    private FsCourseRewardVideoRelationMapper videoRelationMapper;
+    @Autowired
+    private FsCourseRewardRoundMapper roundMapper;
+    @Autowired
+    private FsUserCouponMapper fsUserCouponMapper;
+    @Autowired
+    private FsCouponMapper fsCouponMapper;
 
     @Autowired
     private FsCourseAnswerLogsMapper courseAnswerLogsMapper;
@@ -4912,5 +4922,607 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
 
 
 
+
+    /**
+     * 用户提现
+     * @param param
+     * @return
+     */
+    @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);
+            }
+        }
+    }
+
+    private R executeWithdrawal(FsCourseSendRewardUParam param) {
+        log.info("进入用户判断");
+        FsUser user = fsUserMapper.selectFsUserByUserId(param.getUserId());
+        if (user == null) {
+            return R.error("未识别到用户信息");
+        }
+
+        FsCourseWatchLog log = courseWatchLogMapper.getWatchCourseVideoByFsUserNew(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 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 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);
+    }
+
+    /**
+     * 抽奖
+     */
+    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 = buildPrizes(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 sendCoupon(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 R sendCoupon(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("奖励发放成功");
+    }
+
+    private List<Prize> buildPrizes(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));
+
+            } catch (Exception e) {
+                log.warn("解析奖品配置失败,跳过: {}", JSON.toJSONString(o), e);
+            }
+        }
+
+        return prizes;
+    }
+
+
+    /**
+     * 安全解析概率值
+     */
+    private double parseProbability(String probabilityStr) {
+        try {
+            if (StringUtils.isBlank(probabilityStr)) {
+                return -1;
+            }
+
+            // 移除百分号并转换
+            String cleanStr = probabilityStr.replace("%", "").trim();
+            return Double.parseDouble(cleanStr);
+        } catch (NumberFormatException e) {
+            log.error("概率格式错误: {}", probabilityStr, e);
+            return -1;
+        }
+    }
+
+
+    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());
+                    redPacketLog.setWatchType(1);
+
+                    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("答题成功!");
+        }
+    }
+
+    /**
+     * 获取用户openId
+     */
+    private String getOpenId(FsCourseSendRewardUParam 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 {
+                    handleFsUserWx(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();
+    }
 }
 

+ 37 - 0
fs-service/src/main/java/com/fs/course/utils/luckyDraw/LotteryUtil.java

@@ -0,0 +1,37 @@
+package com.fs.course.utils.luckyDraw;
+
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+
+public class LotteryUtil {
+
+    /**
+     * 根据奖品配置抽奖
+     *
+     * @param prizes 奖品列表
+     * @return 中奖的奖品
+     */
+    public static Prize draw(List<Prize> prizes) {
+        if (prizes == null || prizes.isEmpty()) {
+            return null;
+        }
+
+        // 计算总概率和
+        double total = prizes.stream().mapToDouble(Prize::getProbability).sum();
+
+        // [0, total) 之间取随机数
+        double random = ThreadLocalRandom.current().nextDouble() * total;
+
+        // 逐步累加找到落点
+        double sum = 0;
+        for (Prize prize : prizes) {
+            sum += prize.getProbability();
+            if (random < sum) {
+                return prize;
+            }
+        }
+
+        // 理论上不会到这里
+        return prizes.get(prizes.size() - 1);
+    }
+}

+ 77 - 0
fs-service/src/main/java/com/fs/course/utils/luckyDraw/Prize.java

@@ -0,0 +1,77 @@
+package com.fs.course.utils.luckyDraw;
+
+
+public class Prize {
+
+    private Integer type;
+    private String amount;
+    private String code;
+    private double probability;
+    private String couponId;
+    private String goodsId;
+
+    public Prize(Integer type, String amount, String code, double probability, String couponId, String goodsId) {
+        this.type = type;
+        this.amount = amount;
+        this.code = code;
+        this.probability = probability;
+        this.couponId = couponId;
+        this.goodsId = goodsId;
+    }
+
+    public Prize(Integer type, String amount, String code, double probability, String couponId) {
+        this.type = type;
+        this.amount = amount;
+        this.code = code;
+        this.probability = probability;
+        this.couponId = couponId;
+    }
+
+    public Integer getType() {
+        return type;
+    }
+
+    public void setType(Integer type) {
+        this.type = type;
+    }
+
+    public String getAmount() {
+        return amount;
+    }
+
+    public void setAmount(String amount) {
+        this.amount = amount;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public void setCode(String code) {
+        this.code = code;
+    }
+
+    public double getProbability() {
+        return probability;
+    }
+
+    public void setProbability(double probability) {
+        this.probability = probability;
+    }
+
+    public String getCouponId() {
+        return couponId;
+    }
+
+    public void setCouponId(String couponId) {
+        this.couponId = couponId;
+    }
+
+    public String getGoodsId() {
+        return goodsId;
+    }
+
+    public void setGoodsId(String goodsId) {
+        this.goodsId = goodsId;
+    }
+}

+ 49 - 0
fs-service/src/main/java/com/fs/his/config/AppRedPacketConfig.java

@@ -0,0 +1,49 @@
+package com.fs.his.config;
+
+import lombok.Data;
+
+@Data
+public class AppRedPacketConfig {
+    //积分提现商户配置
+    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;
+}

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

@@ -142,4 +142,6 @@ public interface IFsStorePaymentService
     void synchronizePayStatus();
 
     List<FsStorePayment> selectAllPayment();
+
+    R sendAppRedPacket(WxSendRedPacketParam packetParam);
 }

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

@@ -45,6 +45,7 @@ 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.AppRedPacketConfig;
 import com.fs.his.domain.*;
 import com.fs.his.dto.PayConfigDTO;
 import com.fs.his.enums.PaymentMethodEnum;
@@ -1774,6 +1775,82 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
         return fsStorePaymentMapper.selectAllPayment();
     }
 
+    @Override
+    @Transactional
+    public R sendAppRedPacket(WxSendRedPacketParam param) {
+        //组合返回参数
+        R result = new R();
+        String json = configService.selectConfigByKey("his.AppRedPacket");
+        AppRedPacketConfig config = JSONUtil.toBean(json, AppRedPacketConfig.class);
+        if (config.getIsNew() != null && config.getIsNew() == 1) {
+            result = sendRedPacketV3(param, config);
+        } else {
+            result= sendRedPacketLegacy(param, config);
+        }
+
+        result.put("mchId", config.getMchId());
+        result.put("isNew",config.getIsNew());
+        logger.info("App提现返回:{}",result);
+        return result;
+    }
+
+    // 内部方法:处理新版本的发红包逻辑
+    private R sendRedPacketV3(WxSendRedPacketParam param,AppRedPacketConfig config) {
+
+        WxPayConfig payConfig = new WxPayConfig();
+        BeanUtils.copyProperties(config, payConfig);
+        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("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 R paymentByWxaCode(FsStorePaymentPayParam param) {
         FsUser user = userMapper.selectFsUserById(param.getUserId());
@@ -1925,4 +2002,62 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
 
     }
 
+    private R sendRedPacketLegacy(WxSendRedPacketParam param, AppRedPacketConfig 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);
+        WxPayService wxPayService = new WxPayServiceImpl();
+        wxPayService.setConfig(payConfig);
+        TransferService transferService = wxPayService.getTransferService();
+
+        TransferBatchesRequest request = new TransferBatchesRequest();
+        request.setAppid(param.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);
+        }
+    }
+
 }

+ 7 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveMapper.java

@@ -251,4 +251,11 @@ public interface LiveMapper
     @Select("SELECT live_id FROM live WHERE finish_time >= DATE_SUB(NOW(), INTERVAL #{days} DAY) " +
             "AND finish_time <= NOW() AND is_del = 0 AND is_audit = 1 AND status IN (3, 4)")
     List<Long> selectLiveIdsByFinishTimeWithinDays(@Param("days") int days);
+
+    @Select({"<script>" +
+            " SELECT * FROM live WHERE is_audit = 1 and is_del = 0 and status in (1,2,4)" +
+            "  <if test='live.liveName!=null' > and live_name like concat('%',#{live.liveName},'%') </if> " +
+            " order by create_time desc" +
+            " </script>"})
+    List<Live> listToLiveNoEndNew(@Param("live") Live live);
 }

+ 2 - 0
fs-service/src/main/java/com/fs/live/service/ILiveService.java

@@ -230,4 +230,6 @@ public interface ILiveService
     List<Live> selectLiveListNew(Live live);
 
     R createAppLink(CompanyUser user, Long liveId, String corpId);
+
+    List<Live> listToLiveNoEndNew(Live live);
 }

+ 3 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java

@@ -4396,6 +4396,9 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         liveOrder.setPayType("1");
         liveOrder.setTotalPrice(payPrice);
         liveOrder.setPayPrice(payPrice.subtract(liveOrder.getDiscountMoney()));
+        if(CloudHostUtils.hasCloudHostName("济世百康")) {
+            liveOrder.setPayMoney(payPrice.subtract(liveOrder.getDiscountMoney()));
+        }
         try {
             if (baseMapper.insertLiveOrder(liveOrder) > 0) {
                 LiveOrderItemDTO dto = new LiveOrderItemDTO();

+ 5 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java

@@ -376,6 +376,11 @@ public class LiveServiceImpl implements ILiveService
         return R.ok().put("realLink","康好健康"+InvitationCode);
     }
 
+    @Override
+    public List<Live> listToLiveNoEndNew(Live live) {
+        return baseMapper.listToLiveNoEndNew(live);
+    }
+
 
     /**
      * 查询直播

+ 1 - 1
fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java

@@ -2558,7 +2558,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
         FsCourseRealLink courseMap = new FsCourseRealLink();
         BeanUtils.copyProperties(link, courseMap);
         String courseJson = JSON.toJSONString(courseMap);
-        String realLinkFull = appRealLink + courseJson;
+        String realLinkFull = REAL_LINK_PREFIX + courseJson;
         link.setRealLink(realLinkFull);
         Date updateTime = createUpdateTime(setting, sendTime, config);
         link.setUpdateTime(updateTime);

+ 43 - 6
fs-user-app/src/main/java/com/fs/app/utils/ContentCheckUtil.java → fs-service/src/main/java/com/fs/utils/ContentCheckUtil.java

@@ -1,10 +1,15 @@
-package com.fs.app.utils;
+package com.fs.utils;
 
+import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.fs.common.core.domain.R;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.service.ISysConfigService;
 import lombok.Data;
 import lombok.extern.slf4j.Slf4j;
 import okhttp3.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
 
 import javax.crypto.Mac;
 import javax.crypto.spec.SecretKeySpec;
@@ -20,7 +25,15 @@ import java.util.function.Supplier;
  * 内容审核
  */
 @Slf4j
-public final class ContentCheckUtil {
+@Component
+public class ContentCheckUtil {
+
+    private static ISysConfigService configService;
+
+    @Autowired
+    public void setISysConfigService(ISysConfigService service) {
+        configService = service;
+    }
 
     private static final OkHttpClient client = new OkHttpClient();
 
@@ -71,8 +84,20 @@ public final class ContentCheckUtil {
      *
      * @param text 待审核内容
      */
-    public static R checkText(String text) {
-        return checkText(text, DEFAULT_REGION);
+    public static Boolean checkText(String text) {
+        SysConfig config = configService.selectConfigByConfigKey("tx.config");
+        JSONObject json = JSON.parseObject(config.getConfigValue());
+        Boolean status = json.getBoolean("txtStatus");
+        if (status == null) {
+            return true;
+        }else{
+            if(status.equals(true)){
+                R r = checkText(text, DEFAULT_REGION);
+                return r.get("success").toString().equals("true");
+            }else{
+                return true;
+            }
+        }
     }
 
     /**
@@ -80,8 +105,20 @@ public final class ContentCheckUtil {
      *
      * @param fileUrl 图片所在网络地址
      */
-    public static R checkImage(String fileUrl) {
-        return checkImage(fileUrl, DEFAULT_REGION);
+    public static Boolean checkImage(String fileUrl) {
+        SysConfig config = configService.selectConfigByConfigKey("tx.config");
+        JSONObject json = JSON.parseObject(config.getConfigValue());
+        Boolean status = json.getBoolean("imgStatus");
+        if (status == null) {
+            return true;
+        }else{
+            if(status.equals(true)){
+                R r = checkImage(fileUrl, DEFAULT_REGION);
+                return r.get("success").toString().equals("true");
+            }else{
+                return true;
+            }
+        }
     }
 
     /**

+ 3 - 3
fs-service/src/main/resources/application-config-druid-jsbk.yml

@@ -64,8 +64,8 @@ watch:
   password3: v9xsKuqn_$d2y
 
 fs :
-  commonApi: http://172.16.16.4:8010
-  h5CommonApi: http://172.16.16.4:8010
+  commonApi: http://127.0.0.1:8010
+  h5CommonApi: http://127.0.0.1:8010
   jwt:
     # 加密秘钥
     secret: f4y2x52034348j86b67cde58fcsf5625
@@ -96,7 +96,7 @@ ipad:
   aiApi: http://49.232.181.28:3000/api
   wxIpadUrl:
   voiceApi:
-  commonApi:
+  commonApi: http://127.0.0.1:8010
 wx_miniapp_temp:
   pay_order_temp_id:
   inquiry_temp_id:

+ 48 - 0
fs-service/src/main/resources/application-druid-jzzx.yml

@@ -141,6 +141,54 @@ spring:
                     wall:
                         config:
                             multi-statement-allow: true
+        easycall:
+            type: com.alibaba.druid.pool.DruidDataSource
+            driverClassName: com.mysql.cj.jdbc.Driver
+            druid:
+                # 主库数据源
+                master:
+                    url: jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: root
+                    password: easycallcenter365
+                # 初始连接数
+                initialSize: 5
+                # 最小连接池数量
+                minIdle: 10
+                # 最大连接池数量
+                maxActive: 20
+                # 配置获取连接等待超时的时间
+                maxWait: 60000
+                # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+                timeBetweenEvictionRunsMillis: 60000
+                # 配置一个连接在池中最小生存的时间,单位是毫秒
+                minEvictableIdleTimeMillis: 300000
+                # 配置一个连接在池中最大生存的时间,单位是毫秒
+                maxEvictableIdleTimeMillis: 900000
+                # 配置检测连接是否有效
+                validationQuery: SELECT 1 FROM DUAL
+                testWhileIdle: true
+                testOnBorrow: false
+                testOnReturn: false
+                webStatFilter:
+                    enabled: true
+                statViewServlet:
+                    enabled: true
+                    # 设置白名单,不填则允许所有访问
+                    allow:
+                    url-pattern: /druid/*
+                    # 控制台管理用户名和密码
+                    login-username: fs
+                    login-password: 123456
+                filter:
+                    stat:
+                        enabled: true
+                        # 慢SQL记录
+                        log-slow-sql: true
+                        slow-sql-millis: 1000
+                        merge-sql: true
+                    wall:
+                        config:
+                            multi-statement-allow: true
 rocketmq:
     name-server: rmq-1243b25nj.rocketmq.gz.public.tencenttdmq.com:8080 # RocketMQ NameServer 地址
     producer:

+ 138 - 0
fs-service/src/main/resources/mapper/course/FsCourseRewardVideoRelationMapper.xml

@@ -0,0 +1,138 @@
+<?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.course.mapper.FsCourseRewardVideoRelationMapper">
+
+    <resultMap type="FsCourseRewardVideoRelation" id="FsCourseRewardVideoRelationResult">
+        <result property="id"    column="id"    />
+        <result property="rewardId"    column="reward_id"    />
+        <result property="videoSectionId"    column="video_section_id"    />
+        <result property="mark"    column="mark"    />
+        <result property="createId"    column="create_id"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="updateId"    column="update_id"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="companyId"    column="company_id"    />
+    </resultMap>
+
+    <sql id="selectFsCourseRewardVideoRelationVo">
+        select id, reward_id, company_id,video_section_id, mark, create_id, create_time, update_id, update_time from fs_course_reward_video_relation
+    </sql>
+
+    <select id="selectFsCourseRewardVideoRelationList" parameterType="FsCourseRewardVideoRelation" resultMap="FsCourseRewardVideoRelationResult">
+        <include refid="selectFsCourseRewardVideoRelationVo"/>
+        <where>
+            <if test="rewardId != null  and rewardId != ''"> and reward_id = #{rewardId}</if>
+            <if test="videoSectionId != null  and videoSectionId != ''"> and video_section_id = #{videoSectionId}</if>
+            <if test="mark != null "> and mark = #{mark}</if>
+            <if test="createId != null "> and create_id = #{createId}</if>
+            <if test="updateId != null "> and update_id = #{updateId}</if>
+        </where>
+    </select>
+
+    <select id="selectFsCourseRewardVideoRelationById" parameterType="String" resultMap="FsCourseRewardVideoRelationResult">
+        <include refid="selectFsCourseRewardVideoRelationVo"/>
+        where id = #{id}
+    </select>
+
+    <select id="getRewardIdByCompanyIdAndVideoIdAndType" resultType="java.lang.Long">
+        select cr.id
+        from fs_course_reward_video_relation crvr
+        inner join fs_course_reward cr on cr.id = crvr.reward_id and cr.reward_type = #{type}
+        where crvr.company_id = #{companyId} and crvr.video_section_id = #{videoId} and cr.status = 1
+        limit 1
+    </select>
+
+    <select id="getRewardByCompanyIdAndVideoIdAndType" resultType="com.fs.course.domain.FsCourseReward">
+        select cr.*
+        from fs_course_reward cr
+        inner join fs_course_reward_video_relation crvr on cr.id = crvr.reward_id
+        where crvr.company_id = #{companyId} and crvr.video_section_id = #{videoId} and cr.reward_type = #{type} and cr.status = 1
+        limit 1
+    </select>
+
+    <insert id="insertFsCourseRewardVideoRelation" parameterType="FsCourseRewardVideoRelation" useGeneratedKeys="true" keyProperty="id">
+        insert into fs_course_reward_video_relation
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="rewardId != null and rewardId != ''">reward_id,</if>
+            <if test="videoSectionId != null and videoSectionId != ''">video_section_id,</if>
+            <if test="mark != null">mark,</if>
+            <if test="createId != null">create_id,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateId != null">update_id,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="companyId != null">company_id,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="rewardId != null and rewardId != ''">#{rewardId},</if>
+            <if test="videoSectionId != null and videoSectionId != ''">#{videoSectionId},</if>
+            <if test="mark != null">#{mark},</if>
+            <if test="createId != null">#{createId},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateId != null">#{updateId},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="companyId != null">#{companyId},</if>
+         </trim>
+    </insert>
+
+    <update id="updateFsCourseRewardVideoRelation" parameterType="FsCourseRewardVideoRelation">
+        update fs_course_reward_video_relation
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="rewardId != null and rewardId != ''">reward_id = #{rewardId},</if>
+            <if test="videoSectionId != null and videoSectionId != ''">video_section_id = #{videoSectionId},</if>
+            <if test="mark != null">mark = #{mark},</if>
+            <if test="createId != null">create_id = #{createId},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="updateId != null">update_id = #{updateId},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteFsCourseRewardVideoRelationById" parameterType="String">
+        delete from fs_course_reward_video_relation where id = #{id}
+    </delete>
+
+    <delete id="deleteFsCourseRewardVideoRelationByIds" parameterType="String">
+        delete from fs_course_reward_video_relation where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <delete id="deleteByTypes">
+        delete crvr from fs_course_reward_video_relation crvr
+        inner join fs_course_reward cr on cr.id = crvr.reward_id
+        where crvr.video_section_id = #{videoId} and crvr.company_id = #{companyId}
+        and cr.reward_type in
+        <foreach item="type" collection="types" open="(" separator="," close=")">
+            #{type}
+        </foreach>
+    </delete>
+
+
+    <select id="selectFsCourseRewardVideoRelationListByType" resultMap="FsCourseRewardVideoRelationResult">
+        select crvr.id, crvr.reward_id, crvr.company_id,crvr.video_section_id, crvr.mark, crvr.create_id, crvr.create_time, crvr.update_id, crvr.update_time
+        from fs_course_reward_video_relation crvr
+                 inner join fs_course_reward cr on cr.id = crvr.reward_id
+            <where>
+                <if test="rewardId != null  and rewardId != ''"> and crvr.reward_id = #{rewardId}</if>
+                <if test="type != null  and type != ''">   and cr.reward_type = #{type}</if>
+                <if test="videoSectionId != null  and videoSectionId != ''"> and video_section_id = #{videoSectionId}</if>
+                <if test="mark != null "> and crvr.mark = #{mark}</if>
+                <if test="companyId != null ">and crvr.company_id = #{companyId}</if>
+                <if test="createId != null "> and crvr.create_id = #{createId}</if>
+                <if test="updateId != null "> and crvr.update_id = #{updateId}</if>
+            </where>
+    </select>
+
+    <select id="selectByCompanyIdAndVideoIdAndRewardId" resultType="com.fs.course.domain.FsCourseRewardVideoRelation">
+        select
+            *
+        from fs_course_reward_video_relation
+        where company_id = #{companyId} and video_section_id = #{videoId} and reward_id = #{rewardId}
+    </select>
+
+</mapper>

+ 153 - 0
fs-service/src/main/resources/mapper/course/RewardRoundMapper.xml

@@ -0,0 +1,153 @@
+<?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.course.mapper.FsCourseRewardRoundMapper">
+
+    <resultMap type="FsCourseRewardRound" id="FsCourseRewardRoundResult">
+        <result property="id"    column="id"    />
+        <result property="rewardId"    column="reward_id"    />
+        <result property="userId"    column="user_id"    />
+        <result property="rewardType"    column="reward_type"    />
+        <result property="companyId"    column="company_id"    />
+        <result property="actualRewards"    column="actual_rewards"    />
+        <result property="createId"    column="create_id"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="status"    column="status"    />
+        <result property="rewardVideoRelationId"    column="reward_video_relation_id"    />
+        <result property="watchId"    column="watch_id"    />
+        <result property="ruleId"    column="rule_id"    />
+        <result property="goodsId"    column="goods_id"    />
+    </resultMap>
+
+    <sql id="selectFsCourseRewardRoundVo">
+        select id, reward_id, user_id, reward_type, company_id, actual_rewards, create_id, create_time, update_time,
+               status,  watch_id, rule_id, reward_video_relation_id, goods_id
+        from fs_course_reward_round
+    </sql>
+
+    <select id="selectFsCourseRewardRoundList" parameterType="FsCourseRewardRound" resultMap="FsCourseRewardRoundResult">
+        <include refid="selectFsCourseRewardRoundVo"/>
+        <where>
+            status &lt;&gt; 0
+            <if test="rewardId != null "> and reward_id = #{rewardId}</if>
+            <if test="userId != null "> and user_id = #{userId}</if>
+            <if test="rewardType != null "> and reward_type = #{rewardType}</if>
+            <if test="companyId != null "> and company_id = #{companyId}</if>
+            <if test="actualRewards != null  and actualRewards != ''"> and actual_rewards = #{actualRewards}</if>
+            <if test="createId != null "> and create_id = #{createId}</if>
+            <if test="status != null "> and status = #{status}</if>
+            <if test="rewardVideoRelationId != null "> and reward_video_relation_id = #{rewardVideoRelationId}</if>
+            <if test="watchId != null "> and watch_id = #{watchId}</if>
+            <if test="ruleId != null "> and rule_id = #{ruleId}</if>
+            <if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
+                and date_format(create_time,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')
+            </if>
+            <if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
+                and date_format(create_time,'%y%m%d') &lt;= date_format(#{params.endTime},'%y%m%d')
+            </if>
+        </where>
+        order by id desc
+    </select>
+
+    <select id="selectFsCourseRewardRoundById" parameterType="Long" resultMap="FsCourseRewardRoundResult">
+        <include refid="selectFsCourseRewardRoundVo"/>
+        where id = #{id}
+    </select>
+
+    <insert id="insertFsCourseRewardRound" parameterType="FsCourseRewardRound" useGeneratedKeys="true" keyProperty="id">
+        insert into fs_course_reward_round
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="rewardId != null">reward_id,</if>
+            <if test="userId != null">user_id,</if>
+            <if test="rewardType != null">reward_type,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="actualRewards != null and actualRewards != ''">actual_rewards,</if>
+            <if test="createId != null">create_id,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="status != null">status,</if>
+            <if test="rewardVideoRelationId != null">reward_video_relation_id,</if>
+            <if test="watchId != null">watch_id,</if>
+            <if test="ruleId != null">rule_id,</if>
+            <if test="goodsId != null">goods_id,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="rewardId != null">#{rewardId},</if>
+            <if test="userId != null">#{userId},</if>
+            <if test="rewardType != null">#{rewardType},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="actualRewards != null and actualRewards != ''">#{actualRewards},</if>
+            <if test="createId != null">#{createId},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="status != null">#{status},</if>
+            <if test="rewardVideoRelationId != null">#{rewardVideoRelationId},</if>
+            <if test="watchId != null">#{watchId},</if>
+            <if test="ruleId != null">#{ruleId},</if>
+            <if test="goodsId != null">#{goodsId},</if>
+         </trim>
+    </insert>
+
+    <update id="updateFsCourseRewardRound" parameterType="FsCourseRewardRound">
+        update fs_course_reward_round
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="rewardId != null">reward_id = #{rewardId},</if>
+            <if test="userId != null">user_id = #{userId},</if>
+            <if test="rewardType != null">reward_type = #{rewardType},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="actualRewards != null and actualRewards != ''">actual_rewards = #{actualRewards},</if>
+            <if test="createId != null">create_id = #{createId},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="rewardVideoRelationId != null">reward_video_relation_id = #{rewardVideoRelationId},</if>
+            <if test="watchId != null">watch_id = #{watchId},</if>
+            <if test="ruleId != null">rule_id = #{ruleId},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteFsCourseRewardRoundById" parameterType="Long">
+        delete from fs_course_reward_round where id = #{id}
+    </delete>
+
+    <delete id="deleteFsCourseRewardRoundByIds" parameterType="String">
+        delete from fs_course_reward_round where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+
+    <select id="selectFsCourseRewardRoundByAmount" resultType="com.fs.company.vo.RedPacketMoneyVO">
+        select company_id ,sum(actual_rewards)/1000 as money from fs_course_reward_round
+        <where>
+            status =1 and reward_type=1
+            <if test="start != null "> and create_time &gt;= #{start} </if>
+            <if test="end != null "> and create_time &lt;= #{end}</if>
+        </where>
+        group by company_id
+    </select>
+
+
+    <select id="selectFsCourseRewardRoundByAmountForLuckyBag" resultType="com.fs.company.vo.RedPacketMoneyVO">
+        select company_id ,sum(nullif(coin_amount,0) )/1000 as money from lucky_bag_collect_record
+        <where>
+            collect_type =1
+            <if test="start != null "> and collect_time &gt;= #{start} </if>
+            <if test="end != null "> and collect_time &lt;= #{end}</if>
+        </where>
+        group by company_id
+    </select>
+
+    <select id="get1kByStatusAndLtDate" resultMap="FsCourseRewardRoundResult">
+        <include refid="selectFsCourseRewardRoundVo"/>
+        where status = #{status} and create_time &lt; #{endTime}
+        order by id asc
+        limit 1000
+    </select>
+
+
+</mapper>

+ 20 - 0
fs-service/src/main/resources/mapper/qw/QwSopSmsLogsMapper.xml

@@ -241,4 +241,24 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             )
         </foreach>
     </insert>
+
+    <select id="getQwSopSmsLogsStateList" resultType="com.fs.qw.domain.QwSopSmsLogs">
+        SELECT
+        id,
+        fs_user_id,
+        content,
+        sms_template_code,
+        sop_log_id,
+        server_id,
+        sms_index
+        FROM
+        qw_sop_sms_logs
+        WHERE
+        server_id IN
+        <foreach collection="serverIds" item="serverId" open="(" separator="," close=")">
+            #{serverId}
+        </foreach>
+        AND status = 1
+        AND update_time  &lt;= DATE_SUB(NOW(), INTERVAL 40 MINUTE)
+    </select>
 </mapper>

+ 8 - 9
fs-user-app/src/main/java/com/fs/app/controller/AppLoginController.java

@@ -4,7 +4,6 @@ package com.fs.app.controller;
 import cn.hutool.core.collection.CollectionUtil;
 import cn.hutool.core.date.DateTime;
 import cn.hutool.core.util.ObjectUtil;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.fs.app.annotation.Login;
 import com.fs.app.param.*;
 import com.fs.app.utils.WxUtil;
@@ -17,6 +16,7 @@ import com.fs.common.core.redis.RedisCache;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.exception.ServiceException;
 import com.fs.common.service.ISmsService;
+import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.ip.IpUtils;
 import com.fs.common.utils.sign.Md5Utils;
@@ -26,25 +26,19 @@ import com.fs.course.domain.LuckyBag;
 import com.fs.course.domain.LuckyBagCollectRecord;
 import com.fs.course.mapper.FsCoursePlaySourceConfigMapper;
 import com.fs.course.service.IFsUserCourseVideoService;
-import com.fs.his.config.FsSysConfig;
 import com.fs.his.domain.FsUser;
-import com.fs.his.domain.FsUserNewTask;
 import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.service.IFsUserCouponService;
 import com.fs.his.service.IFsUserNewTaskService;
 import com.fs.his.service.IFsUserService;
-import com.fs.his.utils.ConfigUtil;
 import com.fs.his.vo.FsUserRegisterParam;
 import com.fs.im.dto.OpenImResponseDTO;
 import com.fs.im.service.OpenIMService;
 import com.fs.qw.service.ILuckyBagService;
-import com.fs.watch.domain.WatchDeviceSetup;
-import com.fs.watch.domain.WatchUser;
-import com.fs.watch.service.WatchUserService;
+import com.fs.utils.ContentCheckUtil;
 import io.netty.util.internal.StringUtil;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
-import lombok.Synchronized;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.ibatis.annotations.Param;
@@ -55,7 +49,6 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
-import java.time.LocalDateTime;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
 
@@ -292,6 +285,9 @@ public class AppLoginController extends AppBaseController{
             String nickname = userInfo.get("nickname").toString();
             Integer sex = (Integer) userInfo.get("sex");
             String avatar = userInfo.get("headimgurl").toString();
+            if (CloudHostUtils.hasCloudHostName("济世百康") ) {
+                avatar = ContentCheckUtil.checkImage(avatar)?avatar:"https://cos.his.cdwjyyh.com/fs/20240926/420728ee06e54575ba82665dedb4756b.png";
+            }
             FsUser user = userService.selectFsUserByUnionid(unionid);
 
             Map<String, Object> map = new HashMap<>();
@@ -604,6 +600,9 @@ public class AppLoginController extends AppBaseController{
             String nickname = userInfo.get("nickname").toString();
             Integer sex = (Integer) userInfo.get("sex");
             String avatar = userInfo.get("headimgurl").toString();
+            if (CloudHostUtils.hasCloudHostName("济世百康") ) {
+                avatar = ContentCheckUtil.checkImage(avatar)?avatar:"https://cos.his.cdwjyyh.com/fs/20240926/420728ee06e54575ba82665dedb4756b.png";
+            }
             FsUser user = findUserByPhone(param.getPhone());
             if (user!=null && StringUtils.isEmpty(user.getUnionId())){
                 FsUser userByUnionId = userMapper.selectFsUserByUnionid(unionid);

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

@@ -273,4 +273,17 @@ public class CourseFsUserController extends AppBaseController {
         return courseLinkService.getLinkInfo(param.getLogId());
     }
 
+    @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));
+    }
+
 }

+ 7 - 6
fs-user-app/src/main/java/com/fs/app/controller/live/LiveOrderController.java

@@ -16,10 +16,7 @@ import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.exception.CustomException;
-import com.fs.common.utils.IpUtil;
-import com.fs.common.utils.ParseUtils;
-import com.fs.common.utils.ServletUtils;
-import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.*;
 import com.fs.common.utils.ip.IpUtils;
 import com.fs.core.config.WxMaConfiguration;
 import com.fs.core.utils.OrderCodeUtils;
@@ -343,7 +340,11 @@ public class LiveOrderController extends AppBaseController
         String userId= getUserId();
         log.info("开始创建订单,登录用户id:{}", userId);
         param.setUserId(userId);
-        return orderService.createStoreOrder(param);
+        if(CloudHostUtils.hasCloudHostName("济世百康")){
+            return  orderService.createLiveOrder(param);
+        }else{
+            return orderService.createStoreOrder(param);
+        }
     }
     @Login
     @ApiOperation("创建订单测试")
@@ -532,7 +533,7 @@ public class LiveOrderController extends AppBaseController
             }
         }
         FsUserScrm user=userService.selectFsUserById(Long.valueOf(order.getUserId()));
-        if(user!=null&& StringUtils.isNotEmpty(user.getMaOpenId())){
+        if(user!=null){
             //已改价处理
             if(order.getIsEditMoney()!=null&&order.getIsEditMoney()==1){
                 //改过价不做处理

+ 2 - 1
fs-user-app/src/main/java/com/fs/app/controller/store/IndexScrmController.java

@@ -160,7 +160,8 @@ public class IndexScrmController extends AppBaseController {
 		List<FsAdvListQueryVO> advList=advService.selectFsAdvListQuery(1);
 		List<FsStoreProductListQueryVO> newProductList=productService.selectFsStoreProductNewQuery(10, request.getParameter("appId"));
 		List<FsStoreProductListQueryVO> hotProductList=productService.selectFsStoreProductHotQuery(12, request.getParameter("appId"));
-		IndexVO vo=IndexVO.builder().articleCateList(articleCateList).advList(advList).newProductList(newProductList).hotProductList(hotProductList).build();
+		List<FsStoreProductListQueryVO> tuiProductList=productService.selectFsStoreProductHotQuery(13, request.getParameter("appId"));
+		IndexVO vo=IndexVO.builder().articleCateList(articleCateList).advList(advList).tuiProductList(tuiProductList).newProductList(newProductList).hotProductList(hotProductList).build();
 		return R.ok().put("data", vo);
 	}
 

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

@@ -5,6 +5,7 @@ import com.alibaba.fastjson.JSON;
 import com.fs.common.config.FSSysConfig;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.CloudHostUtils;
 import com.fs.company.service.ICompanyService;
 import com.fs.company.service.ICompanyUserService;
 import com.fs.erp.service.IErpOrderService;
@@ -81,6 +82,10 @@ public class PayScrmController {
             switch (order[0]) {
                 case "store":
                 case "live":
+                    if (CloudHostUtils.hasCloudHostName("济世百康") ) {
+                        liveOrderService.payConfirm(1,null,order[1], o.getHf_seq_id(),o.getOut_trans_id(),o.getParty_order_id());
+                        break;
+                    }
                     try {
                         HuiFuUtils.updateDivItem(order[1]);
                     } catch (Exception e) {