xw 22 godzin temu
rodzic
commit
e55b7492f2

+ 2 - 0
fs-company-app/src/main/java/com/fs/app/controller/FsUserCourseVideoController.java

@@ -235,8 +235,10 @@ public class FsUserCourseVideoController extends AppBaseController {
 
 
         R courseSortLink = fsUserCourseService.createCourseSortLink(fsCourseLinkCreateParam);
         R courseSortLink = fsUserCourseService.createCourseSortLink(fsCourseLinkCreateParam);
         String url = courseSortLink.get("url").toString();
         String url = courseSortLink.get("url").toString();
+        String linkId=courseSortLink.get("linkId").toString();
         Map<String, Object> map = new HashMap<>();
         Map<String, Object> map = new HashMap<>();
         map.put("url", url);
         map.put("url", url);
+        map.put("linkId", linkId);
         return R.ok(map);
         return R.ok(map);
     }
     }
 
 

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

@@ -40,4 +40,6 @@ public class FsCourseLinkCreateParam {
 
 
     private Long projectId;//项目ID
     private Long projectId;//项目ID
 
 
+    private String type; // 1-app
+
 }
 }

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

@@ -39,4 +39,8 @@ public class FsCourseSendRewardUParam implements Serializable
 
 
     private String code;
     private String code;
 
 
+    private Integer rewardType;//奖励类型 1红包 2积分
+
+    private Long watchLogId;
+
 }
 }

+ 3 - 0
fs-service/src/main/java/com/fs/course/param/newfs/FsCourseSortLinkParam.java

@@ -41,4 +41,7 @@ public class FsCourseSortLinkParam {
     @ApiModelProperty(value = "项目id")
     @ApiModelProperty(value = "项目id")
     private Long projectId;
     private Long projectId;
 
 
+    @ApiModelProperty(value = "app")
+    private String type;
+
 }
 }

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

@@ -281,4 +281,6 @@ public interface IFsUserCourseVideoService extends IService<FsUserCourseVideo> {
      * @return list
      * @return list
      */
      */
     List<OptionsVO> selectVideoOptionsByCourseId(Long courseId);
     List<OptionsVO> selectVideoOptionsByCourseId(Long courseId);
+
+    R withdrawal(FsCourseSendRewardUParam param);
 }
 }

+ 13 - 10
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseServiceImpl.java

@@ -138,7 +138,7 @@ public class FsUserCourseServiceImpl implements IFsUserCourseService
 
 
     private static final String userRealLink = "/pages/user/users/becomeVIP?";
     private static final String userRealLink = "/pages/user/users/becomeVIP?";
 
 
-    private static final String appRealLink = "/#/pages_course/videovip?course=";
+    private static final String appRealLink = "/#/appcourse/pages_course/videovip?course=";
     public static final String appShortLink = "/#/pages_course/videovip?s=";
     public static final String appShortLink = "/#/pages_course/videovip?s=";
 
 
     /**
     /**
@@ -597,7 +597,7 @@ public class FsUserCourseServiceImpl implements IFsUserCourseService
         BeanUtils.copyProperties(link, courseMap);
         BeanUtils.copyProperties(link, courseMap);
         courseMap.setProjectId(param.getProjectId());
         courseMap.setProjectId(param.getProjectId());
         String courseJson = JSON.toJSONString(courseMap);
         String courseJson = JSON.toJSONString(courseMap);
-        link.setRealLink(realLink + courseJson);
+        link.setRealLink(("1".equals(param.getType()) ?appRealLink:realLink) + courseJson);
 
 
         link.setCreateTime(new Date());
         link.setCreateTime(new Date());
 
 
@@ -605,14 +605,17 @@ public class FsUserCourseServiceImpl implements IFsUserCourseService
         Calendar calendar = getExpireDay(param, config, link.getCreateTime());
         Calendar calendar = getExpireDay(param, config, link.getCreateTime());
         link.setUpdateTime(calendar.getTime());
         link.setUpdateTime(calendar.getTime());
         int i = fsCourseLinkMapper.insertFsCourseLink(link);
         int i = fsCourseLinkMapper.insertFsCourseLink(link);
-        if (i > 0){
-//            String domainName = getDomainName(param.getCompanyUserId(), config);
-//            String sortLink = domainName + shortLink + link.getLink();
-//            return R.ok().put("url", sortLink).put("link", random);
-            String domainName = getDomainName(param.getCompanyUserId(), config);
-            String sortLink = domainName + link.getRealLink().replace("/#","");
-            sortLink = sortLink.replaceAll("\\\\", "");
-            return R.ok().put("url", sortLink).put("link", random);
+        if (i > 0) {
+            if ("1".equals(param.getType())) {
+                String domainName = getDomainName(param.getCompanyUserId(), config);
+                String sortLink = domainName + link.getRealLink().replace("/#", "");
+                sortLink = sortLink.replaceAll("\\\\", "");
+                return R.ok().put("url", sortLink).put("link", random).put("linkId", link.getLinkId());
+            } else {
+                String domainName = getDomainName(param.getCompanyUserId(), config);
+                String sortLink = domainName + shortLink + link.getLink();
+                return R.ok().put("url", sortLink).put("link", random).put("linkId", link.getLinkId());
+            }
         }
         }
         return R.error("生成链接失败!");
         return R.error("生成链接失败!");
     }
     }

+ 423 - 24
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -1644,7 +1644,12 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
             switch (config.getRewardType()) {
             switch (config.getRewardType()) {
                 // 红包奖励
                 // 红包奖励
                 case 1:
                 case 1:
-                    return sendRedPacketReward(param, user, watchLog, video, config);
+                    if (param.getSource() == 3){
+                        param.setWatchLogId(watchLog.getLogId());
+                        return withdrawal(param);
+                    } else {
+                        return sendRedPacketReward(param, user, watchLog, video, config);
+                    }
                 // 积分奖励
                 // 积分奖励
                 case 2:
                 case 2:
                     return sendIntegralReward(param, user, watchLog, config);
                     return sendIntegralReward(param, user, watchLog, config);
@@ -1758,7 +1763,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
             if (log.getRewardType() == 1) {
             if (log.getRewardType() == 1) {
                 FsCourseRedPacketLog fsCourseRedPacketLog = redPacketLogMapper.selectUserFsCourseRedPacketLog(param.getVideoId(), param.getUserId(), param.getPeriodId());
                 FsCourseRedPacketLog fsCourseRedPacketLog = redPacketLogMapper.selectUserFsCourseRedPacketLog(param.getVideoId(), param.getUserId(), param.getPeriodId());
                 if (fsCourseRedPacketLog != null && fsCourseRedPacketLog.getStatus() == 1) {
                 if (fsCourseRedPacketLog != null && fsCourseRedPacketLog.getStatus() == 1) {
-                    return R.error("已领取该课程奖励,不可重复领取!");
+                    return R.error("已领取该课程奖励,不可重复领取!").put("data",fsCourseRedPacketLog.getResult());
                 }
                 }
                 if (fsCourseRedPacketLog != null && fsCourseRedPacketLog.getStatus() == 0) {
                 if (fsCourseRedPacketLog != null && fsCourseRedPacketLog.getStatus() == 0) {
                     if (StringUtils.isNotEmpty(fsCourseRedPacketLog.getResult())) {
                     if (StringUtils.isNotEmpty(fsCourseRedPacketLog.getResult())) {
@@ -1780,17 +1785,28 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         String json = configService.selectConfigByKey("course.config");
         String json = configService.selectConfigByKey("course.config");
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
 
 
-        // 判断来源是否是app,如是app,则发放积分奖励
-        int sourceApp = 3;
-        if (sourceApp == param.getSource() && !CloudHostUtils.hasCloudHostName("中康")) {
-            return sendIntegralReward(param, user, log, config);
-        }
+//        // 判断来源是否是app,如是app,则发放积分奖励
+//        int sourceApp = 3;
+//        if (sourceApp == param.getSource() && !CloudHostUtils.hasCloudHostName("中康")) {
+//            return sendIntegralReward(param, user, log, config);
+//        }
 
 
         // 根据奖励类型发放不同奖励
         // 根据奖励类型发放不同奖励
         switch (config.getRewardType()) {
         switch (config.getRewardType()) {
             // 红包奖励
             // 红包奖励
             case 1:
             case 1:
-                return sendRedPacketRewardFsUser(param, user, log, video, config);
+                if (param.getSource()==3){
+                    WxSendRedPacketParam packetParam = new WxSendRedPacketParam();
+                    String openId = getOpenId(param, user);
+                    if (StringUtils.isBlank(openId)) {
+                        return R.error("请重新使用微信登录");
+                    }
+                    packetParam.setOpenId(openId);
+                    BeanUtils.copyProperties(param, packetParam);
+                    return sendAppRedPacket(packetParam, log,video, config);
+                } else {
+                    return sendRedPacketRewardFsUser(param, user, log, video, config);
+                }
             // 积分奖励
             // 积分奖励
             case 2:
             case 2:
                 return sendIntegralReward(param, user, log, config);
                 return sendIntegralReward(param, user, log, config);
@@ -2575,25 +2591,48 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
      * @param source    来源 1公众号 2小程序
      * @param source    来源 1公众号 2小程序
      * @return openId
      * @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;
     }
     }
 
 
     /**
     /**
@@ -4955,5 +4994,365 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     public List<OptionsVO> selectVideoOptionsByCourseId(Long courseId) {
     public List<OptionsVO> selectVideoOptionsByCourseId(Long courseId) {
         return fsUserCourseVideoMapper.selectVideoOptionsByCourseId(courseId);
         return fsUserCourseVideoMapper.selectVideoOptionsByCourseId(courseId);
     }
     }
+
+    /**
+     * 用户提现
+     * @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);
+            }
+        }
+    }
+
+    private R executeWithdrawal(FsCourseSendRewardUParam param){
+        log.info("进入用户判断");
+        FsUser user = fsUserMapper.selectFsUserByUserId(param.getUserId());
+        if (user == null) {
+            return R.error("未识别到用户信息");
+        }
+
+        FsCourseWatchLog watchLog = courseWatchLogMapper.getWatchCourseVideo(param.getUserId(), param.getVideoId(), param.getQwUserId(), param.getQwExternalId());
+        if (watchLog == null) {
+            watchLog = courseWatchLogMapper.getWatchCourseVideoByFsUser(param.getUserId(), param.getVideoId(), param.getCompanyUserId());
+        }
+        if (watchLog == null) {
+            return R.error("无记录");
+        }
+
+        if (watchLog.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 (watchLog.getRewardType() != null) {
+            if (watchLog.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 (watchLog.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, watchLog,video, config);
+            // 积分奖励
+            case 2:
+                return sendIntegralReward(param, user, watchLog, 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) {
+        // 仅手动发课(有营期)校验红包领取截止时间;自动发课不做该限制
+        if (log.getPeriodId() != null) {
+            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.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("答题成功!");
+        }
+    }
 }
 }
 
 

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

@@ -4,6 +4,7 @@ import com.fs.course.vo.FsUserCourseVideoVO;
 import com.fs.his.domain.FsPackage;
 import com.fs.his.domain.FsPackage;
 import lombok.Data;
 import lombok.Data;
 
 
+import java.math.BigDecimal;
 import java.util.List;
 import java.util.List;
 
 
 @Data
 @Data
@@ -13,4 +14,62 @@ public class AppConfig {
     private Long courseId;
     private Long courseId;
     private List<FsUserCourseVideoVO> fsCourse;
     private List<FsUserCourseVideoVO> fsCourse;
     private Integer unbindLimit;
     private Integer unbindLimit;
+
+    //积分提现商户配置
+    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;
 }
 }

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

@@ -7,6 +7,7 @@ import com.alibaba.fastjson.JSONObject;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.R;
 import com.fs.company.param.FsStoreStatisticsParam;
 import com.fs.company.param.FsStoreStatisticsParam;
 import com.fs.company.vo.FsStorePaymentStatisticsVO;
 import com.fs.company.vo.FsStorePaymentStatisticsVO;
+import com.fs.his.config.AppConfig;
 import com.fs.his.domain.FsStorePayment;
 import com.fs.his.domain.FsStorePayment;
 import com.fs.his.param.FsStorePaymentParam;
 import com.fs.his.param.FsStorePaymentParam;
 import com.fs.his.param.PayOrderParam;
 import com.fs.his.param.PayOrderParam;
@@ -119,6 +120,10 @@ public interface IFsStorePaymentService
 
 
     String v3TransferNotify(String notifyData, HttpServletRequest request);
     String v3TransferNotify(String notifyData, HttpServletRequest request);
 
 
+    /**
+     * App 商家转账 V3 回调(验签使用 his.AppRedPacket,与小程序 redPacket.config 隔离)
+     */
+    String v3TransferNotifyApp(String notifyData, HttpServletRequest request);
 
 
     String v3TransferNotifyWithCompanyId(Long companyId,String notifyData, HttpServletRequest request);
     String v3TransferNotifyWithCompanyId(Long companyId,String notifyData, HttpServletRequest request);
 
 
@@ -146,4 +151,8 @@ public interface IFsStorePaymentService
     void synchronizePayStatus();
     void synchronizePayStatus();
 
 
     List<FsStorePayment> selectAllPayment();
     List<FsStorePayment> selectAllPayment();
+
+    R sendAppRedPacket(WxSendRedPacketParam packetParam);
+
+    R sendRedPacketV3ByApp(WxSendRedPacketParam param, AppConfig config);
 }
 }

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

@@ -49,6 +49,7 @@ import com.fs.course.mapper.FsCoursePlaySourceConfigMapper;
 import com.fs.course.service.IFsCourseRedPacketLogService;
 import com.fs.course.service.IFsCourseRedPacketLogService;
 import com.fs.course.service.IFsUserCourseOrderService;
 import com.fs.course.service.IFsUserCourseOrderService;
 import com.fs.course.service.IFsUserVipOrderService;
 import com.fs.course.service.IFsUserVipOrderService;
+import com.fs.his.config.AppConfig;
 import com.fs.his.domain.*;
 import com.fs.his.domain.*;
 import com.fs.his.enums.PaymentMethodEnum;
 import com.fs.his.enums.PaymentMethodEnum;
 import com.fs.his.mapper.FsExportTaskMapper;
 import com.fs.his.mapper.FsExportTaskMapper;
@@ -1168,6 +1169,50 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
         }
         }
     }
     }
 
 
+    @Override
+    public String v3TransferNotifyApp(String notifyData, HttpServletRequest request) {
+        logger.info("【App转账回调V3】收到回调,body={}", notifyData);
+        try {
+            String json = configService.selectConfigByKey("his.AppRedPacket");
+            if (StringUtils.isEmpty(json)) {
+                logger.error("【App转账回调V3】his.AppRedPacket 配置为空");
+                return WxPayNotifyResponse.fail("config empty");
+            }
+            AppConfig appConfig = JSONUtil.toBean(json, AppConfig.class);
+            logger.info("【App转账回调V3】验签配置 mchId={}, notifyUrl={}, requestUri={}",
+                    appConfig.getMchId(), appConfig.getNotifyUrl(), request.getRequestURI());
+            WxPayConfig payConfig = new WxPayConfig();
+            BeanUtils.copyProperties(appConfig, payConfig);
+            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("【App转账回调V3】解析成功,state={}, outBillNo={}, transferBillNo={}",
+                    result.getResult().getState(),
+                    result.getResult().getOutBillNo(),
+                    result.getResult().getTransferBillNo());
+            if (result.getResult().getState().equals("SUCCESS")) {
+                R r = redPacketLogService.syncRedPacket(result.getResult().getOutBillNo(), result.getResult().getTransferBillNo());
+                logger.info("【App转账回调V3】同步红包状态结果,code={}, msg={}", r.get("code"), r.get("msg"));
+                if (r.get("code").equals(200)) {
+                    return WxPayNotifyResponse.success("处理成功");
+                }
+                logger.warn("【App转账回调V3】同步失败,outBillNo={}", result.getResult().getOutBillNo());
+                return WxPayNotifyResponse.fail("");
+            }
+            logger.warn("【App转账回调V3】状态非SUCCESS,state={}, outBillNo={}",
+                    result.getResult().getState(), result.getResult().getOutBillNo());
+            return WxPayNotifyResponse.fail("");
+        } catch (WxPayException e) {
+            logger.error("【App转账回调V3】处理异常,returnMsg={}, message={}", e.getReturnMsg(), e.getMessage(), e);
+            return WxPayNotifyResponse.fail(e.getMessage());
+        }
+    }
+
     @Override
     @Override
     public String v3TransferNotifyWithCompanyId(Long companyId, String notifyData, HttpServletRequest request) {
     public String v3TransferNotifyWithCompanyId(Long companyId, String notifyData, HttpServletRequest request) {
         logger.info("分公司回调V3::companyId:{}",companyId);
         logger.info("分公司回调V3::companyId:{}",companyId);
@@ -1999,4 +2044,266 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
 
 
     }
     }
 
 
+
+    @Override
+    @Transactional
+    public R sendAppRedPacket(WxSendRedPacketParam param) {
+        //组合返回参数
+        R result = new R();
+        String json = configService.selectConfigByKey("his.AppRedPacket");
+        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;
+    }
+
+    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);
+        }
+    }
+
+
+    /**
+     *
+     * @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);
+        }
+    }
+
 }
 }

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

@@ -42,6 +42,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 import org.springframework.web.multipart.MultipartFile;
 
 
+import javax.servlet.http.HttpServletRequest;
 import javax.validation.Valid;
 import javax.validation.Valid;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map;
@@ -184,12 +185,18 @@ public class CourseFsUserController extends AppBaseController {
     @PostMapping("/sendReward")
     @PostMapping("/sendReward")
     @UserOperationLog(operationType = FsUserOperationEnum.SENDREWARD)
     @UserOperationLog(operationType = FsUserOperationEnum.SENDREWARD)
     @RepeatSubmit
     @RepeatSubmit
-    public R sendReward(@RequestBody FsCourseSendRewardUParam param)
+    public R sendReward(@RequestBody FsCourseSendRewardUParam param, HttpServletRequest request)
     {
     {
         if (ObjectUtil.isEmpty(param.getUserId())){
         if (ObjectUtil.isEmpty(param.getUserId())){
             Long userId = Long.parseLong(getUserId());
             Long userId = Long.parseLong(getUserId());
             param.setUserId(userId);
             param.setUserId(userId);
         }
         }
+        String sourceType = request.getHeader("Sourcetype");
+        if(StringUtils.isNotBlank(sourceType)){
+            if ("APP".equals(sourceType)) {
+                param.setSource(3);
+            }
+        }
         logger.info("zyp \n【发放奖励】2:{}",param);
         logger.info("zyp \n【发放奖励】2:{}",param);
         return courseVideoService.sendRewardByFsUser(param);
         return courseVideoService.sendRewardByFsUser(param);
     }
     }
@@ -200,6 +207,20 @@ public class CourseFsUserController extends AppBaseController {
         logger.error("zyp \n【h5看课中途报错】:{}",msg);
         logger.error("zyp \n【h5看课中途报错】:{}",msg);
     }
     }
 
 
+
+    @Login
+    @ApiOperation("发送红包(以积分提现的形式)")
+    @PostMapping("/withdrawal")
+    @RepeatSubmit
+    public R withdrawal(@RequestBody FsCourseSendRewardUParam param){
+        String userId = getUserId();
+        if(userId == null){
+            return R.error("请先登录!");
+        }
+        param.setUserId(Long.parseLong(userId));
+        return R.ok(courseVideoService.withdrawal(param));
+    }
+
     /**
     /**
      * 添加appId到用户的appId列表中(如果不存在)
      * 添加appId到用户的appId列表中(如果不存在)
      * @param currentAppIds 当前用户已有的appId列表(逗号分隔)
      * @param currentAppIds 当前用户已有的appId列表(逗号分隔)

+ 8 - 0
fs-user-app/src/main/java/com/fs/app/controller/course/CourseTransferController.java

@@ -50,6 +50,14 @@ public class CourseTransferController {
         return paymentService.v3TransferNotify(notifyData,request);
         return paymentService.v3TransferNotify(notifyData,request);
     }
     }
 
 
+    /**
+     * App 商家转账 V3 回调(验签使用 his.AppRedPacket;请在微信侧 notifyUrl 指向本地址)
+     */
+    @PostMapping("/v3TransferNotifyApp")
+    public String v3TransferNotifyApp(@RequestBody String notifyData, HttpServletRequest request, HttpServletResponse response) throws Exception {
+        return paymentService.v3TransferNotifyApp(notifyData, request);
+    }
+
     @GetMapping("/test")
     @GetMapping("/test")
     public void test(){
     public void test(){
         iSopUserLogsInfoService.updateSopUserInfoByExternalId(1L,123456L);
         iSopUserLogsInfoService.updateSopUserInfoByExternalId(1L,123456L);