Procházet zdrojové kódy

Merge remote-tracking branch 'refs/remotes/origin/Payment-Configuration' into Payment-Configuration-more-app

xgb před 1 týdnem
rodič
revize
eaf565c2bb
65 změnil soubory, kde provedl 2205 přidání a 443 odebrání
  1. 3 4
      fs-ad-new-api/src/main/java/com/fs/app/controller/LandingPageController.java
  2. 14 10
      fs-ad-new-api/src/main/java/com/fs/app/controller/TestController.java
  3. 26 2
      fs-ad-new-api/src/main/java/com/fs/app/controller/TrackingController.java
  4. 17 7
      fs-ad-new-api/src/main/java/com/fs/app/controller/WeChatController.java
  5. 10 3
      fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeService.java
  6. 67 42
      fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeServiceImpl.java
  7. 3 1
      fs-ad-new-api/src/main/java/com/fs/app/facade/ConversionServiceImpl.java
  8. 2 14
      fs-ad-new-api/src/main/java/com/fs/app/mq/consumer/ConversionTrackingMessageConsumer.java
  9. 54 0
      fs-ad-new-api/src/main/java/com/fs/framework/aspectj/RocketMQTraceIdAspect.java
  10. 51 0
      fs-admin/src/main/java/com/fs/task/FsCourseBackupTask.java
  11. 12 0
      fs-common/src/main/java/com/fs/common/core/redis/RedisCache.java
  12. 15 0
      fs-company-app/src/main/java/com/fs/app/controller/FsUserCourseVideoController.java
  13. 5 22
      fs-qw-api/src/main/java/com/fs/app/service/QwDataCallbackService.java
  14. 15 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseAnswerLogsMapper.java
  15. 13 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseRedPacketLogMapper.java
  16. 13 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java
  17. 5 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseAnswerLogsService.java
  18. 5 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseRedPacketLogService.java
  19. 6 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseWatchLogService.java
  20. 8 0
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java
  21. 197 3
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseAnswerLogsServiceImpl.java
  22. 195 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseRedPacketLogServiceImpl.java
  23. 201 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java
  24. 40 1
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  25. 2 2
      fs-service/src/main/java/com/fs/his/domain/FsIntegralOrder.java
  26. 30 0
      fs-service/src/main/java/com/fs/his/enums/FsIntegralOrderStatusEnum.java
  27. 1 0
      fs-service/src/main/java/com/fs/his/enums/FsUserIntegralLogTypeEnum.java
  28. 2 0
      fs-service/src/main/java/com/fs/live/service/ILiveCompletionPointsRecordService.java
  29. 26 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveAfterSalesServiceImpl.java
  30. 114 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionPointsRecordServiceImpl.java
  31. 12 12
      fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java
  32. 171 84
      fs-service/src/main/java/com/fs/live/service/impl/LiveRedConfServiceImpl.java
  33. 2 2
      fs-service/src/main/java/com/fs/live/vo/LiveAfterSalesVo.java
  34. 19 0
      fs-service/src/main/java/com/fs/live/vo/LiveDataListVo.java
  35. 1 0
      fs-service/src/main/java/com/fs/newAdv/constant/MqTopicConstant.java
  36. 16 7
      fs-service/src/main/java/com/fs/newAdv/domain/Lead.java
  37. 12 0
      fs-service/src/main/java/com/fs/newAdv/dto/req/QwExternalIdBindTrackReq.java
  38. 10 0
      fs-service/src/main/java/com/fs/newAdv/dto/req/TraceIdDto.java
  39. 12 0
      fs-service/src/main/java/com/fs/newAdv/dto/req/updateNickNameReq.java
  40. 8 8
      fs-service/src/main/java/com/fs/newAdv/enums/SystemEventTypeEnum.java
  41. 2 1
      fs-service/src/main/java/com/fs/newAdv/event/ConversionEventListener.java
  42. 2 3
      fs-service/src/main/java/com/fs/newAdv/integration/adapter/BaiduAdapter.java
  43. 4 5
      fs-service/src/main/java/com/fs/newAdv/integration/adapter/OPPOAdapter.java
  44. 2 3
      fs-service/src/main/java/com/fs/newAdv/integration/adapter/OceanEngineAdapter.java
  45. 1 2
      fs-service/src/main/java/com/fs/newAdv/integration/adapter/TencentAdapter.java
  46. 1 2
      fs-service/src/main/java/com/fs/newAdv/integration/adapter/VIVOAdapter.java
  47. 16 9
      fs-service/src/main/java/com/fs/newAdv/integration/client/AbstractApiClient.java
  48. 1 1
      fs-service/src/main/java/com/fs/newAdv/integration/client/advertiser/BaiduApiClient.java
  49. 13 11
      fs-service/src/main/java/com/fs/newAdv/integration/client/advertiser/OPPOApiClient.java
  50. 21 2
      fs-service/src/main/java/com/fs/newAdv/integration/client/advertiser/OceanEngineApiClient.java
  51. 1 1
      fs-service/src/main/java/com/fs/newAdv/integration/client/advertiser/VIVOApiClient.java
  52. 20 6
      fs-service/src/main/java/com/fs/newAdv/service/ILeadService.java
  53. 124 60
      fs-service/src/main/java/com/fs/newAdv/service/impl/LeadServiceImpl.java
  54. 1 1
      fs-service/src/main/java/com/fs/newAdv/service/impl/SiteServiceImpl.java
  55. 12 0
      fs-service/src/main/java/com/fs/newAdv/vo/ConversionParmVo.java
  56. 19 5
      fs-service/src/main/java/com/fs/qw/service/impl/CustomerTransferApprovalServiceImpl.java
  57. 43 1
      fs-service/src/main/resources/mapper/course/FsCourseAnswerLogsMapper.xml
  58. 64 0
      fs-service/src/main/resources/mapper/course/FsCourseRedPacketLogMapper.xml
  59. 21 9
      fs-service/src/main/resources/mapper/course/FsCourseTrafficLogMapper.xml
  60. 64 0
      fs-service/src/main/resources/mapper/course/FsCourseWatchLogMapper.xml
  61. 1 0
      fs-service/src/main/resources/mapper/live/LiveAfterSalesMapper.xml
  62. 15 1
      fs-user-app/src/main/java/com/fs/app/controller/course/CourseQwController.java
  63. 134 94
      fs-user-app/src/main/java/com/fs/app/controller/live/LiveCompletionPointsController.java
  64. 197 0
      fs-user-app/src/main/java/com/fs/app/controller/live/LiveOrderController.java
  65. 11 2
      fs-user-app/src/main/java/com/fs/app/controller/store/WxUserScrmController.java

+ 3 - 4
fs-ad-new-api/src/main/java/com/fs/app/controller/LandingPageController.java

@@ -31,18 +31,17 @@ public class LandingPageController {
      * 落地页访问
      */
     @PostMapping("/h5/home")
-    public Result<LandingIndexRes> track(
+    public Result<LandingIndexRes> h5Home(
             @RequestBody LandingIndexReq req) {
-        log.info("落地页访问追踪:req={}", req);
         // 查询落地页模板
-        return Result.success(facadeService.getLandingIndexBySiteId(req.getAllParams()));
+        return Result.success(facadeService.getLandingIndexBySiteId(req.getViewUrl(),req.getAllParams()));
     }
 
     /**
      * 小程序访问
      */
     @PostMapping("/mini/home")
-    public Result<LandingIndexRes> track(@RequestBody WeChatLandingIndexReq req) {
+    public Result<LandingIndexRes> miniHome(@RequestBody WeChatLandingIndexReq req) {
         return Result.success(facadeService.getWxLandingIndexBySiteId(req));
     }
 

+ 14 - 10
fs-ad-new-api/src/main/java/com/fs/app/controller/TestController.java

@@ -1,15 +1,13 @@
 package com.fs.app.controller;
 
+import com.fs.newAdv.dto.req.TraceIdDto;
 import com.fs.newAdv.integration.client.advertiser.BaiduApiClient;
 import com.fs.newAdv.enums.SystemEventTypeEnum;
 import com.fs.newAdv.event.ConversionEventPublisher;
 import com.fs.newAdv.service.IPromotionAccountService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
 /**
  * 广告商监测链接
@@ -28,15 +26,21 @@ public class TestController {
     @Autowired
     private IPromotionAccountService promotionAccountService;
 
-    @GetMapping("/test1/{traceId}")
-    public void test1(@PathVariable("traceId") String traceId) {
+    @PostMapping("/test1")
+    public void test1(@RequestBody TraceIdDto traceId) {
         log.info("模拟 当日加群 事件完成");
-        conversionEventPublisher.publishConversionEvent(traceId, SystemEventTypeEnum.GROUP_TODAY);
+        conversionEventPublisher.publishConversionEvent(traceId.getTraceId(), SystemEventTypeEnum.GROUP_TODAY);
     }
 
-    @GetMapping("/test2/{traceId}")
-    public void test2(@PathVariable("traceId") String traceId) {
+    @PostMapping("/test2")
+    public void test2(@RequestBody TraceIdDto traceId) {
         log.info("模拟 当日加微 事件完成");
-        conversionEventPublisher.publishConversionEvent(traceId, SystemEventTypeEnum.WEI_CHAT_TODAY);
+        conversionEventPublisher.publishConversionEvent(traceId.getTraceId(), SystemEventTypeEnum.WEI_CHAT_TODAY);
+    }
+
+    @PostMapping("/test3")
+    public void test3(@RequestBody TraceIdDto traceId) {
+        log.info("模拟 微信授权 事件完成");
+        conversionEventPublisher.publishConversionEvent(traceId.getTraceId(), SystemEventTypeEnum.AUTH_TODAY_CREATE);
     }
 }

+ 26 - 2
fs-ad-new-api/src/main/java/com/fs/app/controller/TrackingController.java

@@ -1,6 +1,10 @@
 package com.fs.app.controller;
 
 import com.fs.app.facade.CallbackProcessingFacadeService;
+import com.fs.app.facade.IConversionService;
+import com.fs.common.result.Result;
+import com.fs.newAdv.dto.req.QwExternalIdBindTrackReq;
+import com.fs.newAdv.enums.SystemEventTypeEnum;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
@@ -22,6 +26,8 @@ public class TrackingController {
     private CallbackProcessingFacadeService facadeService;
 
 
+    @Autowired
+    private IConversionService conversionService;
     /**
      * 监测链接端口
      *
@@ -29,14 +35,14 @@ public class TrackingController {
      * @param response  HTTP响应
      */
     @GetMapping("/click/{advertiserCode}")
-    public void trackBaidu(
+    public void track(
             @RequestParam Map<String, String> allParams,
             @PathVariable("advertiserCode") Long advertiserCode,
             HttpServletResponse response) {
         log.info("接收监测请求 | params={}", allParams);
         try {
             // 2. 保存点击追踪记录
-            facadeService.saveClickTrace(advertiserCode,allParams);
+            facadeService.saveClickTrace(advertiserCode, allParams);
             // 3. 返回 200 OK
             response.setStatus(HttpServletResponse.SC_OK);
         } catch (Exception e) {
@@ -45,4 +51,22 @@ public class TrackingController {
         }
     }
 
+    /**
+     * 绑定企微用户与线索
+     */
+    @PostMapping("/bind/qwTrack")
+    public Result<String> qwExternalIdBindTrack(@RequestBody QwExternalIdBindTrackReq req) {
+        facadeService.qwExternalIdBindTrack(req);
+        return Result.success();
+    }
+
+    /**
+     * 绑定企微用户与线索
+     */
+    @PostMapping("/bind/qwTrack/test/{id1}/{id2}")
+    public Result<String> qwExternalIdBindTrack1(@PathVariable String id1, @PathVariable String id2) {
+        boolean success = conversionService.reportTrackingToAdvertiser(SystemEventTypeEnum.getByCode(id1), id2);
+        return Result.success();
+    }
+
 }

+ 17 - 7
fs-ad-new-api/src/main/java/com/fs/app/controller/WeChatController.java

@@ -9,14 +9,12 @@ import com.fs.app.facade.CallbackProcessingFacadeService;
 import com.fs.common.constant.SystemConstant;
 import com.fs.common.result.Result;
 import com.fs.newAdv.domain.AdvMiniConfig;
+import com.fs.newAdv.dto.req.updateNickNameReq;
 import com.fs.newAdv.service.IAdvMiniConfigService;
 import com.fs.wx.miniapp.config.WxMaProperties;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
 import java.time.LocalDateTime;
 import java.util.HashMap;
@@ -50,7 +48,8 @@ public class WeChatController {
             try {
                 String access_token = advMiniConfig.getAccessToken();
                 // 判断token是否过期
-                if (advMiniConfig.getExpiresIn().isBefore(LocalDateTime.now().plusMinutes(10))) {
+                if (LocalDateTime.now().plusMinutes(10).isAfter(advMiniConfig.getExpiresIn())) {
+                    // 提前10分钟刷新Token
                     HttpResponse execute2 = HttpRequest.get("https://api.weixin.qq.com/cgi-bin/token")
                             .form("grant_type", "client_credential")
                             .form("appid", advMiniConfig.getAppId())
@@ -59,13 +58,14 @@ public class WeChatController {
                             .execute();
                     JSONObject obj = JSONObject.parseObject(execute2.body());
                     access_token = obj.getString("access_token");
+                    log.info("getSchemeUrl:{}", obj);
                     advMiniConfig.setAccessToken(access_token);
-                    advMiniConfig.setExpiresIn(LocalDateTime.now().plusSeconds(obj.getLong("expires_in")));
+                    advMiniConfig.setExpiresIn(LocalDateTime.now().plusSeconds(obj.getInteger("expires_in")));
                     advMiniConfigService.updateById(advMiniConfig);
                 }
                 Map<String, Object> map = new HashMap<>();
                 Map<String, Object> map2 = new HashMap<>();
-                map2.put("path", "/pages/shopping/productDetails");
+                map2.put("path", "/pages_ad/index");
                 map2.put("query", "traceId=" + traceId);
                 map2.put("env_version", "trial");
                 map.put("jump_wxa", map2);
@@ -85,4 +85,14 @@ public class WeChatController {
         }
         return Result.success("");
     }
+
+    /**
+     * 更新用户昵称
+     * @return
+     */
+    @PostMapping("/updateNickName")
+    public Result<String> updateNickName(@RequestBody updateNickNameReq req) {
+        facadeService.updateNickName(req);
+        return Result.success("");
+    }
 }

+ 10 - 3
fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeService.java

@@ -1,6 +1,8 @@
 package com.fs.app.facade;
 
+import com.fs.newAdv.dto.req.QwExternalIdBindTrackReq;
 import com.fs.newAdv.dto.req.WeChatLandingIndexReq;
+import com.fs.newAdv.dto.req.updateNickNameReq;
 import com.fs.newAdv.dto.res.LandingIndexRes;
 
 import java.util.Map;
@@ -10,20 +12,25 @@ public interface CallbackProcessingFacadeService {
 
     /**
      * 链化追踪点击
+     *
      * @param allParams
      */
-    void saveClickTrace(Long advertiserCode,Map<String,String> allParams);
+    void saveClickTrace(Long advertiserCode, Map<String, String> allParams);
+
     /**
      * 根据站点ID获取落地页信息
      *
-     * @param siteId
      * @return
      */
-    LandingIndexRes getLandingIndexBySiteId(Map<String, String> allParams);
+    LandingIndexRes getLandingIndexBySiteId(String viewUrl,Map<String, String> allParams);
 
     //----------------------code回调---------------------------------
     void gdtGetAuthCode(Integer code, Long state);
 
 
     LandingIndexRes getWxLandingIndexBySiteId(WeChatLandingIndexReq req);
+
+    void qwExternalIdBindTrack(QwExternalIdBindTrackReq req);
+
+    void updateNickName(updateNickNameReq req);
 }

+ 67 - 42
fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeServiceImpl.java

@@ -1,22 +1,24 @@
 package com.fs.app.facade;
 
+import cn.hutool.core.util.IdUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.json.JSONArray;
 import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
-import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
-import com.fs.newAdv.integration.adapter.IAdvertiserAdapter;
-import com.fs.newAdv.integration.factory.AdvertiserHandlerFactory;
-import com.fs.common.exception.base.BusinessException;
 import com.fs.common.utils.RedisUtil;
+import com.fs.common.utils.SnowflakeUtil;
 import com.fs.newAdv.domain.LandingPageTemplate;
 import com.fs.newAdv.domain.Lead;
 import com.fs.newAdv.domain.Site;
+import com.fs.newAdv.dto.req.QwExternalIdBindTrackReq;
 import com.fs.newAdv.dto.req.WeChatLandingIndexReq;
+import com.fs.newAdv.dto.req.updateNickNameReq;
 import com.fs.newAdv.dto.res.LandingIndexRes;
 import com.fs.newAdv.enums.AdvertiserTypeEnum;
+import com.fs.newAdv.integration.adapter.IAdvertiserAdapter;
+import com.fs.newAdv.integration.factory.AdvertiserHandlerFactory;
 import com.fs.newAdv.service.ILandingPageTemplateService;
 import com.fs.newAdv.service.ILeadService;
 import com.fs.newAdv.service.ISiteService;
@@ -72,50 +74,53 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
     public void saveClickTrace(Long advertiserCode, Map<String, String> allParams) {
         IAdvertiserAdapter advertiserAdapter = handlerFactory.getAdapter(AdvertiserTypeEnum.getByCode(advertiserCode));
         Lead lead = advertiserAdapter.adaptCallbackData(allParams);
-        Lead byTraceId = leadService.getByTraceId(lead.getTraceId());
-        if (ObjectUtil.isNotEmpty(byTraceId)) {
-            throw new BusinessException("监测信息已存在: " + lead.getTraceId());
+        Lead byClickId = leadService.getByClickId(lead.getTraceId());
+        if (ObjectUtil.isNotEmpty(byClickId)) {
+            log.info("线索已存在:{}", lead.getTraceId());
+            return;
         }
         lead.setStatus(0);
         lead.setClickTrigger(1);
         lead.setTraceRawParams(JSONUtil.toJsonStr(allParams));
+        lead.setTraceId(SnowflakeUtil.randomUUID());
         boolean saved = leadService.save(lead);
         if (!saved) {
             log.error("线索保存失败:{}", lead);
-            throw new RuntimeException("线索保存失败");
         }
     }
 
     private String getTraceIdByAdvertiser(AdvertiserTypeEnum byCode, Map<String, String> allParams) {
-        String traceId;
+        String clickId;
         switch (byCode) {
             case OCEANENGINE:
             case TENCENT:
+                clickId = allParams.get("clickid");
+                break;
             case OPPO:
-                traceId = allParams.get("click_id");
+                clickId = allParams.get("tid");
                 break;
             case BAIDU:
-                traceId = allParams.get("bd_vid");
+                clickId = allParams.get("bd_vid");
                 break;
             case VIVO:
-                traceId = allParams.get("requestId");
+                clickId = allParams.get("requestId");
                 break;
             case IQIYI:
-                traceId = allParams.get("traceId");
+                clickId = allParams.get("traceId");
                 break;
             default:
-                traceId = "ylrz_test";
+                clickId = "ylrz_test";
         }
-        if (StrUtil.isEmpty(traceId)) {
-            traceId = "ylrz_test";
+        if (StrUtil.isEmpty(clickId)) {
+            clickId = "ylrz_test" + IdUtil.randomUUID();
         }
 
-        return traceId;
+        return clickId;
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public LandingIndexRes getLandingIndexBySiteId(Map<String, String> allParams) {
+    public LandingIndexRes getLandingIndexBySiteId(String viewUrl, Map<String, String> allParams) {
         // 站点信息
         String paramsSiteId = allParams.get("siteId");
         if (ObjectUtil.isEmpty(paramsSiteId)) {
@@ -131,25 +136,25 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
         // 广告商信息
         Long advertiserId = byId.getAdvertiserId();
         // 访问链路id
-        String traceId = getTraceIdByAdvertiser(Objects.requireNonNull(AdvertiserTypeEnum.getByCode(advertiserId)), allParams);
+        String clickId = getTraceIdByAdvertiser(Objects.requireNonNull(AdvertiserTypeEnum.getByCode(advertiserId)), allParams);
         // 线索信息
-        Lead byTraceId = leadService.getByTraceId(traceId);
-        boolean isNewLead = ObjectUtil.isEmpty(byTraceId);
+        Lead lead = leadService.getByClickId(clickId);
+        boolean isNewLead = ObjectUtil.isEmpty(lead);
         if (isNewLead) {
             IAdvertiserAdapter advertiserAdapter = handlerFactory.getAdapter(AdvertiserTypeEnum.getByCode(advertiserId));
-            byTraceId = advertiserAdapter.adaptCallbackData(allParams);
-            byTraceId.setAdvertiserId(advertiserId);
-            byTraceId.setSiteId(siteId);
-            byTraceId.setTraceId(traceId);
+            lead = advertiserAdapter.adaptCallbackData(allParams);
+            lead.setAdvertiserId(advertiserId);
+            lead.setSiteId(siteId);
+            lead.setClickId(clickId);
             // 设置站点和落地页的关联
-            setSiteByIdeaId(siteId, byTraceId.getIdeaId());
+            setSiteByIdeaId(siteId, lead.getIdeaId());
         } else {
             // 检查站点和广告商信息是否异常
-            if (!Objects.equals(byTraceId.getSiteId(), siteId)) {
-                log.info("落地页站点信息异常:{}---{}", byTraceId.getSiteId(), siteId);
+            if (!Objects.equals(lead.getSiteId(), siteId)) {
+                log.info("落地页站点信息异常:{}---{}", lead.getSiteId(), siteId);
             }
-            if (!Objects.equals(byTraceId.getAdvertiserId(), advertiserId)) {
-                log.info("落地页广告商信息异常:{}---{}", byTraceId.getAdvertiserId(), advertiserId);
+            if (!Objects.equals(lead.getAdvertiserId(), advertiserId)) {
+                log.info("落地页广告商信息异常:{}---{}", lead.getAdvertiserId(), advertiserId);
             }
         }
         // 模板缓存
@@ -170,26 +175,28 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
         LandingPageTemplate landingPageTemplate = landingPageTemplateService.getById(byId.getLaunchPageId());
         JSONObject jsonObject = JSONUtil.parseObj(landingPageTemplate.getTemplateData());
         // 替换二维码链接
-        updateQrCodeInTemplate(jsonObject, traceId, byId, byTraceId);
+        updateQrCodeInTemplate(jsonObject, byId, lead);
         String templateData = JSONUtil.toJsonStr(jsonObject);
 
         // 保存或更新 线索信息
         LocalDateTime now = LocalDateTime.now();
-        byTraceId.setLandingPageRawParams(JSONUtil.toJsonStr(allParams));
-        byTraceId.setLandingPageTrigger(1);
-        byTraceId.setLandingPageTs(now);
-        byTraceId.setUpdateTime(now);
+        lead.setLandingPageRawParams(JSONUtil.toJsonStr(allParams));
+        lead.setLandingPageTrigger(1);
+        lead.setLandingPageTs(now);
+        lead.setUpdateTime(now);
+        lead.setViewUrl(viewUrl);
         if (isNewLead) {
-            leadService.save(byTraceId);
+            lead.setTraceId(SnowflakeUtil.randomUUID());
+            leadService.save(lead);
         } else {
-            leadService.updateById(byTraceId);
+            leadService.updateById(lead);
         }
 
         // 封装返回结果
         LandingIndexRes res = new LandingIndexRes();
-        redisUtil.set(TEMPLATE_DATA + traceId, templateData, 24, TimeUnit.HOURS);
+        redisUtil.set(TEMPLATE_DATA + lead.getTraceId(), templateData, 24, TimeUnit.HOURS);
         res.setTemplateData(templateData);
-        res.setTraceId(byTraceId.getTraceId());
+        res.setTraceId(lead.getTraceId());
         return res;
     }
 
@@ -202,7 +209,7 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
     /**
      * 更新模板中的二维码信息
      */
-    private void updateQrCodeInTemplate(JSONObject templateData, String traceId, Site site, Lead lead) {
+    private void updateQrCodeInTemplate(JSONObject templateData, Site site, Lead lead) {
         JSONArray configList = templateData.getJSONArray("configList");
         if (configList == null || configList.isEmpty()) {
             return;
@@ -227,7 +234,7 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
                                                Integer allocationRule,
                                                Long allocationRuleId,
                                                Lead byTraceId) {
-        log.info("开始获取广告二维码: {} {} {} {}", launchType, allocationRule, allocationRuleId ,byTraceId);
+        log.info("开始获取广告二维码: {} {} {} {}", launchType, allocationRule, allocationRuleId, byTraceId);
         // 二维码
         String qrCode = "";
         if (allocationRule == 1) {
@@ -285,10 +292,28 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
     @Override
     public LandingIndexRes getWxLandingIndexBySiteId(WeChatLandingIndexReq req) {
         // 更新授权页访问信息记录
-        leadService.updateAuthIndex(req.getTraceId(),req.getType());
+        leadService.updateAuthIndex(req.getTraceId(), req.getType());
         String templateData = String.valueOf(redisUtil.get(TEMPLATE_DATA + req.getTraceId()));
         LandingIndexRes landingIndexRes = new LandingIndexRes();
         landingIndexRes.setTemplateData(templateData);
         return landingIndexRes;
     }
+
+    @Override
+    public void qwExternalIdBindTrack(QwExternalIdBindTrackReq req) {
+        // 广告线索处理
+        leadService.updateAddMemberLead(req.getQwExternalId(), req.getUnionid());
+    }
+
+    @Override
+    public void updateNickName(updateNickNameReq req) {
+        Lead byTraceId = leadService.getByTraceId(req.getTraceId());
+        if (ObjectUtil.isEmpty(byTraceId)) {
+            log.error("更新昵称失败,未找到线索:{}", req);
+        }
+        Lead update = new Lead();
+        update.setId(byTraceId.getId());
+        update.setWeiChatName(req.getNickName());
+        leadService.updateById(update);
+    }
 }

+ 3 - 1
fs-ad-new-api/src/main/java/com/fs/app/facade/ConversionServiceImpl.java

@@ -96,7 +96,7 @@ public class ConversionServiceImpl implements IConversionService {
             return false;
         }
 
-        Map<String, Object> params = JSONUtil.toBean(lead.getTraceRawParams(), Map.class);
+        Map<String, Object> params = JSONUtil.toBean(lead.getLandingPageRawParams(), Map.class);
         // 构建回传参数
         Map<String, Object> conversionData = new HashMap<>();
         // ------------------------------通用参数----------
@@ -109,6 +109,8 @@ public class ConversionServiceImpl implements IConversionService {
         conversionData.put("traceId", traceId);
         conversionData.put("eventType", advertiserEventType.getAdvertiserEventType());
         conversionData.put("timestamp", System.currentTimeMillis() / 1000);
+        conversionData.put("viewUrl", lead.getViewUrl());
+
         // 添加平台适应参数
         IAdvertiserAdapter adapter = advertiserHandlerFactory.getAdapter(AdvertiserTypeEnum.getByCode(callbackAccount.getAdvertiserId()));
         adapter.uploadConversionData(conversionData, params);

+ 2 - 14
fs-ad-new-api/src/main/java/com/fs/app/mq/consumer/ConversionTrackingMessageConsumer.java

@@ -2,17 +2,13 @@ package com.fs.app.mq.consumer;
 
 import com.fs.app.facade.IConversionService;
 import com.fs.common.annotation.DistributeLock;
-import com.fs.common.utils.RedisUtil;
-import com.fs.common.utils.TraceIdUtil;
 import com.fs.newAdv.constant.ConversionTrackingMessage;
 import com.fs.newAdv.constant.MqTopicConstant;
 import com.fs.newAdv.enums.SystemEventTypeEnum;
-import com.fs.newAdv.mapper.ConversionLogMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.rocketmq.spring.annotation.ConsumeMode;
 import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
 import org.apache.rocketmq.spring.core.RocketMQListener;
-import org.jboss.logging.MDC;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
@@ -25,23 +21,17 @@ import org.springframework.stereotype.Component;
 @Component
 @RocketMQMessageListener(
         topic = MqTopicConstant.CONVERSION_TRACKING_TOPIC,
-        consumerGroup = MqTopicConstant.CONVERSION_TRACKING_TOPIC_CONSUMER_GROUP,
+        consumerGroup = MqTopicConstant.CONVERSION_TRACKING_GROUP,
         // 并发消费模式(多线程并发消费,线程数由RocketMQ自动管理)
         consumeMode = ConsumeMode.CONCURRENTLY,
         // 最大重试次数(RocketMQ默认16次)
-        maxReconsumeTimes = 16
+        maxReconsumeTimes = 3
 )
 public class ConversionTrackingMessageConsumer implements RocketMQListener<ConversionTrackingMessage> {
 
     @Autowired
     private IConversionService conversionService;
 
-    @Autowired
-    private RedisUtil redisUtil;
-
-    @Autowired
-    private ConversionLogMapper conversionLogMapper;
-
     /**
      * 消费转化消息
      *
@@ -50,8 +40,6 @@ public class ConversionTrackingMessageConsumer implements RocketMQListener<Conve
     @Override
     @DistributeLock(scene = "mq", keyExpression = "#message.traceId", waitTime = 0, errorMsg = "重复消费")
     public void onMessage(ConversionTrackingMessage message) {
-        TraceIdUtil.put(message.getTrackId());
-
         String traceId = message.getTraceId();
         SystemEventTypeEnum eventType = message.getEventType();
 

+ 54 - 0
fs-ad-new-api/src/main/java/com/fs/framework/aspectj/RocketMQTraceIdAspect.java

@@ -0,0 +1,54 @@
+package com.fs.framework.aspectj;
+
+import com.fs.common.utils.TraceIdUtil;
+import com.fs.newAdv.constant.ConversionTrackingMessage;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+/**
+ * RocketMQ消费者链路ID切面
+ * 在消费消息前设置链路ID到MDC,保证整个消费链路的日志可追踪
+ *
+ * @author zhangqin
+ */
+@Aspect
+@Component
+@Slf4j
+@Order(1) // 优先于 DistributeLockAspect 执行
+public class RocketMQTraceIdAspect {
+
+    @Pointcut("execution(* com.fs.app.mq.consumer.*.onMessage(..))")
+    public void mqConsumerPointcut() {
+    }
+
+    @Around("mqConsumerPointcut()")
+    public Object around(ProceedingJoinPoint pjp) throws Throwable {
+        String trackId = null;
+        try {
+            // 从消息参数中提取 trackId
+            Object[] args = pjp.getArgs();
+            if (args != null && args.length > 0) {
+                Object message = args[0];
+                if (message instanceof ConversionTrackingMessage) {
+                    trackId = ((ConversionTrackingMessage) message).getTrackId();
+                }
+            }
+            // 设置链路ID到MDC
+            if (trackId != null && !trackId.isEmpty()) {
+                TraceIdUtil.put(trackId);
+            } else {
+                // 如果没有 trackId,则生成一个新的
+                TraceIdUtil.init();
+            }
+            return pjp.proceed();
+        } finally {
+            // 清理MDC,避免线程复用时链路ID污染
+            TraceIdUtil.clear();
+        }
+    }
+}

+ 51 - 0
fs-admin/src/main/java/com/fs/task/FsCourseBackupTask.java

@@ -0,0 +1,51 @@
+package com.fs.task;
+
+import com.fs.course.domain.FsCourseWatchLog;
+import com.fs.course.service.IFsCourseAnswerLogsService;
+import com.fs.course.service.IFsCourseRedPacketLogService;
+import com.fs.course.service.IFsCourseWatchLogService;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * 备份看课记录,答题记录,红包记录数据
+ */
+@AllArgsConstructor
+@Component("fsCourseBackupTask")
+@Slf4j
+public class FsCourseBackupTask {
+
+    @Autowired
+    private IFsCourseWatchLogService fsCourseWatchLogService;
+
+    @Autowired
+    private IFsCourseAnswerLogsService fsCourseAnswerLogsService;
+
+    @Autowired
+    private IFsCourseRedPacketLogService fsCourseRedPacketLogService;
+
+    /**
+     * 备份看课记录
+     */
+    public void backupWatchLog(){
+        fsCourseWatchLogService.backupWatchLog();
+    }
+
+    /**
+     * 备份答题记录
+     */
+    public void backupAnswerLog(){
+        fsCourseAnswerLogsService.backupAnswerLog();
+    }
+
+    /**
+     * 备份红包记录
+     */
+    public void backupRedPacketLog(){
+        fsCourseRedPacketLogService.backupRedPacketLog();
+    }
+}

+ 12 - 0
fs-common/src/main/java/com/fs/common/core/redis/RedisCache.java

@@ -410,6 +410,18 @@ public class RedisCache
         redisTemplate.opsForHash().put(key, hashKey, value);
     }
 
+    /**
+     * 向哈希表中添加键值对(仅当字段不存在时,原子操作,保证幂等性)
+     *
+     * @param key     哈希表键
+     * @param hashKey 哈希键
+     * @param value   哈希值
+     * @return true=设置成功(字段不存在),false=字段已存在
+     */
+    public Boolean hashPutIfAbsent(String key, String hashKey, Object value) {
+        return redisTemplate.opsForHash().putIfAbsent(key, hashKey, value);
+    }
+
     /**
      * 向哈希表中添加键值对
      *

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

@@ -5,6 +5,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fs.app.annotation.Login;
 import com.fs.app.config.ImageStorageConfig;
 import com.fs.common.annotation.Log;
+import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.ResponseResult;
 import com.fs.common.enums.BusinessType;
@@ -79,6 +80,10 @@ public class FsUserCourseVideoController extends AppBaseController {
 
     @Autowired
     private IFsCourseWatchLogService fsCourseWatchLogService;
+    @Autowired
+    private IFsCourseAnswerLogsService fsCourseAnswerLogsService;
+    @Autowired
+    private IFsCourseRedPacketLogService fsCourseRedPacketLogService;
 
     @Autowired
     private OpenIMService openIMService;
@@ -399,4 +404,14 @@ public class FsUserCourseVideoController extends AppBaseController {
         return imMsgSendLogService.deleteFsImMsgSendLogAndDetail(logId);
     }
 
+
+
+    @GetMapping("/backupLog")
+    public AjaxResult backupLog()
+    {
+        fsCourseWatchLogService.backupWatchLog();
+        fsCourseAnswerLogsService.backupAnswerLog();
+        fsCourseRedPacketLogService.backupRedPacketLog();
+        return null;
+    }
 }

+ 5 - 22
fs-qw-api/src/main/java/com/fs/app/service/QwDataCallbackService.java

@@ -228,32 +228,13 @@ public class QwDataCallbackService {
                                     String qwApiExternal = redisCache.getCacheObject(cacheKey);
                                     if (StringUtil.strIsNullOrEmpty(qwApiExternal)) {
                                         try {
-                                            String externalUserID = root.getElementsByTagName("ExternalUserID").item(0).getTextContent();
-                                            String userID = root.getElementsByTagName("UserID").item(0).getTextContent();
                                             // 5. 新增用户
-                                            qwExternalContactService.insertQwExternalContactByExternalUserId(externalUserID,userID,null,corpId,State,WelcomeCode);
+                                            qwExternalContactService.insertQwExternalContactByExternalUserId(root.getElementsByTagName("ExternalUserID").item(0).getTextContent(),root.getElementsByTagName("UserID").item(0).getTextContent(),null,corpId,State,WelcomeCode);
                                             // 6. 业务逻辑执行成功后,写入 Redis 缓存(有效期 10 分钟)
                                             redisCache.setCacheObject(cacheKey, "1", 10, TimeUnit.MINUTES);
 
                                             // 广告线索处理
-                                            leadService.updateAddMemberLead(externalUserID,userID,corpId,State)
-                                                    .thenAccept(result -> {
-                                                        if (StrUtil.isNotEmpty(result)) {
-                                                            QwExternalContact qwExternalContact = qwExternalContactMapper.selectQwExternalByExternalIdAndCompanyIdToIdAndFs(externalUserID, userID, corpId);
-                                                            if (qwExternalContact == null){
-                                                                try {
-                                                                    Thread.sleep(2000);
-                                                                } catch (InterruptedException e) {
-                                                                    throw new RuntimeException(e);
-                                                                }
-                                                                qwExternalContact = qwExternalContactMapper.selectQwExternalByExternalIdAndCompanyIdToIdAndFs(externalUserID, userID, corpId);
-                                                            }
-                                                            QwExternalContact temp = new QwExternalContact();
-                                                            temp.setId(qwExternalContact.getId());
-                                                            temp.setTraceId(result);
-                                                            qwExternalContactMapper.updateById(temp);
-                                                        }
-                                                    });
+                                            leadService.updateAddMemberLead(root.getElementsByTagName("ExternalUserID").item(0).getTextContent(),root.getElementsByTagName("UserID").item(0).getTextContent(),corpId,State);
                                         } catch (Exception e) {
                                             // 7. 业务逻辑失败时,删除缓存
                                             redisCache.deleteObject(cacheKey);
@@ -278,6 +259,8 @@ public class QwDataCallbackService {
                             break;
                         case "del_follow_user":
                             qwExternalContactService.deletefollowUserByExternalUserId(root.getElementsByTagName("ExternalUserID").item(0).getTextContent(),root.getElementsByTagName("UserID").item(0).getTextContent(),corpId);
+                            // 广告线索处理
+                            leadService.updateDeleteMemberLead(root.getElementsByTagName("ExternalUserID").item(0).getTextContent(),root.getElementsByTagName("UserID").item(0).getTextContent(),corpId);
                             break;
                         case "transfer_fail":
                             qwExternalContactService.transferFailByExternalUserId(root.getElementsByTagName("ExternalUserID").item(0).getTextContent(),root.getElementsByTagName("UserID").item(0).getTextContent(),corpId,root.getElementsByTagName("FailReason").item(0).getTextContent());
@@ -431,7 +414,7 @@ public class QwDataCallbackService {
                                     if (qwGroupChatUserOld==null) {
                                         qwGroupChatUserService.insertQwGroupChatUser(qwGroupChatUser);
                                         // 群员入群 广告判断记录
-                                        leadService.updateGroupAddMemberLead(qwGroupChatUser.getUserId(),qwGroupChatUser.getChatId(),qwGroupChatUser.getCorpId(),qwGroupChatUser.getUnionid());
+                                        leadService.updateGroupAddMemberLead(qwGroupChatUser.getName(),qwGroupChatUser.getChatId(),qwGroupChatUser.getCorpId(),qwGroupChatUser.getUnionid());
                                     }else {
                                         qwGroupChatUserOld.setIsOut(1L);
                                         qwGroupChatUserOld.setCorpId(corpId);

+ 15 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseAnswerLogsMapper.java

@@ -138,4 +138,19 @@ public interface FsCourseAnswerLogsMapper
 
     @Select("select * from fs_course_answer_logs where video_id = #{videoId} and user_id = #{userId} and is_right = 1 limit 1")
     FsCourseAnswerLogs selectRightLogByCourseVideoIsOpen(@Param("videoId") Long videoId,@Param("userId") Long userId);
+
+
+    /**
+     * 备份答题数据
+     * @param logs
+     * @return
+     */
+    int batchInsert(@Param("list") List<FsCourseAnswerLogs> logs);
+
+
+    /**
+     * 批量删除ID列表
+     * @return 删除的行数
+     */
+    int batchDeleteByIds(@Param("ids") List<Long> ids);
 }

+ 13 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseRedPacketLogMapper.java

@@ -189,4 +189,17 @@ public interface FsCourseRedPacketLogMapper
     List<CourseRedPacketStatisticsDTO> statistics(CourseRedPacketStatisticsParam param);
 
     List<FsCourseRedPacketLog> selectFsCourseRedPacketLogListBySending(@Param("maps") Map<String, Object> map);
+
+    /**
+     * 批量备份红包记录表
+     * @param logs
+     * @return
+     */
+    int batchInsert(@Param("list") List<FsCourseRedPacketLog> logs);
+
+    /**
+     * 批量删除ID列表
+     * @return 删除的行数
+     */
+    int batchDeleteByIds(@Param("ids") List<Long> ids);
 }

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

@@ -719,4 +719,17 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
             "</script>"
     })
     List<Long> getExContactIdsIdsByWatchLogIds(@Param("watchLogIds")List<Long> watchLogIds);
+
+    /**
+     * 批量插入到目标表
+     * @param logs 日志列表
+     * @return 插入成功的条数
+     */
+    int batchInsert(@Param("list") List<FsCourseWatchLog> logs);
+
+    /**
+     * 批量删除ID列表
+     * @return 删除的行数
+     */
+    int batchDeleteByIds(@Param("ids") List<Long> ids);
 }

+ 5 - 0
fs-service/src/main/java/com/fs/course/service/IFsCourseAnswerLogsService.java

@@ -66,4 +66,9 @@ public interface IFsCourseAnswerLogsService
     public List<FsCourseAnswerLogsListVO> selectFsCourseAnswerLogsListVONew(FsCourseAnswerLogsParam param);
 
     public Long selectFsCourseAnswerLogsListVONewCount(FsCourseAnswerLogsParam param);
+
+    /**
+     * 备份答题记录
+     */
+    void backupAnswerLog();
 }

+ 5 - 0
fs-service/src/main/java/com/fs/course/service/IFsCourseRedPacketLogService.java

@@ -89,4 +89,9 @@ public interface IFsCourseRedPacketLogService
     void queryRedPacketResult(String startTime, String endTime);
 
     R getBillsByTransferBillNo(String batchId);
+
+    /**
+     * 备份红包记录
+     */
+    void backupRedPacketLog();
 }

+ 6 - 0
fs-service/src/main/java/com/fs/course/service/IFsCourseWatchLogService.java

@@ -154,4 +154,10 @@ public interface IFsCourseWatchLogService extends IService<FsCourseWatchLog> {
      * @return
      */
     List<Long> getExContactIdsIdsByWatchLogIds(List<Long> watchLogIds);
+
+
+    /**
+     * 备份看课记录
+     */
+    void backupWatchLog();
 }

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

@@ -257,4 +257,12 @@ public interface IFsUserCourseVideoService extends IService<FsUserCourseVideo> {
     int batchEditCover(BatchEditCoverParam param);
 
     R createZcMiniLink(FsCourseLinkMiniParam param);
+
+    /**
+     * 注册信息
+     *
+     * @param param
+     * @return
+     */
+    R registerQwFsUser(FsUserCourseVideoAddKfUParam param);
 }

+ 197 - 3
fs-service/src/main/java/com/fs/course/service/impl/FsCourseAnswerLogsServiceImpl.java

@@ -8,6 +8,7 @@ import com.fs.company.cache.ICompanyUserCacheService;
 import com.fs.company.domain.Company;
 import com.fs.company.domain.CompanyUser;
 import com.fs.course.domain.FsCourseAnswerLogs;
+import com.fs.course.domain.FsCourseRedPacketLog;
 import com.fs.course.domain.FsUserCourseVideo;
 import com.fs.course.mapper.FsCourseAnswerLogsMapper;
 import com.fs.course.param.FsCourseAnswerLogsParam;
@@ -17,14 +18,15 @@ import com.fs.course.vo.FsCourseAnswerLogsListVO;
 import com.fs.his.domain.FsUser;
 import com.fs.his.service.IFsUserService;
 import com.fs.store.service.cache.IFsUserCacheService;
+import com.github.pagehelper.PageHelper;
 import com.hc.openapi.tool.util.StringUtils;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
+import java.text.SimpleDateFormat;
+import java.util.*;
 import java.util.stream.Collectors;
 
 /**
@@ -34,6 +36,7 @@ import java.util.stream.Collectors;
  * @date 2024-10-26
  */
 @Service
+@Slf4j
 @RequiredArgsConstructor
 public class FsCourseAnswerLogsServiceImpl implements IFsCourseAnswerLogsService
 {
@@ -201,4 +204,195 @@ public class FsCourseAnswerLogsServiceImpl implements IFsCourseAnswerLogsService
         return fsCourseAnswerLogsMapper.selectFsCourseAnswerLogsListVONewCount(param);
     }
 
+    @Override
+    public void backupAnswerLog() {
+        log.info("开始执行数据迁移任务");
+
+        // 检查是否在凌晨3点之后
+//        if (shouldStopAt3AM()) {
+//            log.info("已到凌晨3点,停止数据迁移任务");
+//            return;
+//        }
+
+        // 设置查询条件:获取12月之前的数据
+        FsCourseAnswerLogs queryLog = createQueryCondition();
+
+        // 分页参数配置
+        int pageSize = 1000; // 每批次处理数量,可根据性能调整
+        int pageNum = 1;
+        int totalProcessed = 0;
+        boolean hasMoreData = true;
+
+        try {
+            while (hasMoreData) {
+                // 检查是否到凌晨3点
+               /* if (shouldStopAt3AM()) {
+                    log.info("已到凌晨3点,停止数据迁移任务,已处理{}条数据", totalProcessed);
+                    break;
+                }*/
+
+                log.info("开始查询第{}批数据,查询条件:beginTime={}, endTime={}",
+                        pageNum, queryLog.getBeginTime(), queryLog.getEndTime());
+
+                // 使用分页查询
+                PageHelper.startPage(pageNum, pageSize);
+                List<FsCourseAnswerLogs> fsCourseAnswerLogs = fsCourseAnswerLogsMapper.selectFsCourseAnswerLogsList(queryLog);
+
+                if (fsCourseAnswerLogs == null || fsCourseAnswerLogs.isEmpty()) {
+                    log.info("所有12月之前的数据已迁移完成");
+                    hasMoreData = false;
+                    continue;
+                }
+
+                log.info("第{}批查询到{}条数据,开始处理", pageNum, fsCourseAnswerLogs.size());
+
+                // 批量处理数据(迁移到目标表并删除源表数据)
+                int processedCount = processBatchData(fsCourseAnswerLogs);
+                totalProcessed += processedCount;
+
+                log.info("第{}批数据处理完成,成功迁移{}条数据,累计迁移{}条",
+                        pageNum, processedCount, totalProcessed);
+
+                pageNum++;
+
+                // 清理分页参数,避免影响下次查询
+                PageHelper.clearPage();
+
+                // 小批量提交后稍作休息,避免数据库压力过大
+                if (hasMoreData) {
+                    Thread.sleep(100);
+                }
+            }
+
+            log.info("数据迁移任务完成,总计迁移{}条数据", totalProcessed);
+
+        } catch (InterruptedException e) {
+            log.warn("数据迁移任务被中断,已处理{}条数据", totalProcessed);
+            Thread.currentThread().interrupt();
+        } catch (Exception e) {
+            log.error("数据迁移过程中发生异常,已处理{}条数据", totalProcessed, e);
+        } finally {
+            // 确保清理分页参数
+            PageHelper.clearPage();
+        }
+    }
+
+    /**
+     * 创建查询条件 - 获取所有12月之前的数据
+     */
+    private FsCourseAnswerLogs createQueryCondition() {
+        FsCourseAnswerLogs queryLog = new FsCourseAnswerLogs();
+
+        // 设置开始时间为系统允许的最早时间(可根据实际情况调整)
+        queryLog.setBeginTime(getEarliestTimeString());
+
+        // 设置结束时间为今年12月1日之前(包含历史所有年份的12月之前数据)
+        queryLog.setEndTime(getDecemberFirstTimeString());
+
+        return queryLog;
+    }
+
+    /**
+     * 批量处理日志数据
+     * @return 成功处理的数据条数
+     */
+    private int processBatchData(List<FsCourseAnswerLogs> logs) {
+        if (logs == null || logs.isEmpty()) {
+            return 0;
+        }
+
+        int successCount = 0;
+
+        try {
+            // 1. 批量插入到目标表
+            log.debug("开始批量插入数据到目标表,数量:{}", logs.size   ());
+            int insertCount = fsCourseAnswerLogsMapper.batchInsert(logs);
+            log.debug("批量插入完成,插入{}条数据", insertCount);
+
+            if (insertCount > 0) {
+                // 2. 获取成功插入的数据ID
+                List<Long> idsToDelete = logs.stream()
+                        .limit(insertCount) // 只删除成功插入的数据
+                        .map(FsCourseAnswerLogs::getLogId)
+                        .filter(Objects::nonNull)
+                        .collect(Collectors.toList());
+
+                if (!idsToDelete.isEmpty()) {
+                    // 3. 批量删除源表数据
+                    log.debug("开始批量删除源表数据,数量:{}", idsToDelete.size());
+                    int deleteCount = fsCourseAnswerLogsMapper.batchDeleteByIds(idsToDelete);
+                    log.debug("批量删除完成,删除{}条数据", deleteCount);
+
+                    // 返回成功处理的数量(以插入成功为准)
+                    successCount = insertCount;
+                }
+            }
+
+        } catch (Exception e) {
+            log.error("批量处理数据时发生异常", e);
+            // 这里可以根据需要添加重试逻辑
+            // retryProcessBatchData(logs);
+        }
+
+        return successCount;
+    }
+
+    /**
+     * 检查是否应该停止(凌晨3点之后)
+     */
+    private boolean shouldStopAt3AM() {
+        Calendar calendar = Calendar.getInstance();
+        int hour = calendar.get(Calendar.HOUR_OF_DAY);
+        int minute = calendar.get(Calendar.MINUTE);
+
+        // 凌晨3点及之后返回true
+        return hour >= 3;
+    }
+
+    /**
+     * 获取最早时间字符串(String格式)
+     */
+    private String getEarliestTimeString() {
+        try {
+            // 这里可以根据业务设置一个最早的开始时间
+            // 例如:如果只迁移最近3年的数据
+            Calendar calendar = Calendar.getInstance();
+            calendar.add(Calendar.YEAR, -3); // 3年前
+            calendar.set(Calendar.MONTH, 0);
+            calendar.set(Calendar.DAY_OF_MONTH, 1);
+            calendar.set(Calendar.HOUR_OF_DAY, 0);
+            calendar.set(Calendar.MINUTE, 0);
+            calendar.set(Calendar.SECOND, 0);
+            calendar.set(Calendar.MILLISECOND, 0);
+
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            return sdf.format(calendar.getTime());
+        } catch (Exception e) {
+            log.warn("设置最早时间失败,使用默认值null", e);
+            return null; // 返回null表示不限制最早时间
+        }
+    }
+
+    /**
+     * 获取今年12月1日之前的时间字符串
+     */
+    private String getDecemberFirstTimeString() {
+        try {
+            Calendar calendar = Calendar.getInstance();
+            int currentYear = calendar.get(Calendar.YEAR);
+
+            // 获取当前年份的12月1日
+            calendar.set(currentYear, Calendar.DECEMBER, 1, 0, 0, 0);
+            calendar.set(Calendar.MILLISECOND, 0);
+
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            return sdf.format(calendar.getTime());
+        } catch (Exception e) {
+            log.error("获取12月1日时间失败", e);
+            // 返回一个很早的时间作为兜底
+            return "2020-12-01 00:00:00";
+        }
+    }
+
+
 }

+ 195 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCourseRedPacketLogServiceImpl.java

@@ -1,6 +1,7 @@
 package com.fs.course.service.impl;
 
 import java.math.BigDecimal;
+import java.text.SimpleDateFormat;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
@@ -40,6 +41,8 @@ import com.github.binarywang.wxpay.exception.WxPayException;
 import com.github.binarywang.wxpay.service.TransferService;
 import com.github.binarywang.wxpay.service.WxPayService;
 import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
+import com.github.pagehelper.PageHelper;
+import lombok.extern.slf4j.Slf4j;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.BeanUtils;
@@ -57,6 +60,7 @@ import org.springframework.transaction.annotation.Transactional;
  * @date 2024-10-24
  */
 @Service
+@Slf4j
 public class FsCourseRedPacketLogServiceImpl implements IFsCourseRedPacketLogService
 {
     private static final Logger logger = LoggerFactory.getLogger(FsCourseRedPacketLogServiceImpl.class);
@@ -553,4 +557,195 @@ public class FsCourseRedPacketLogServiceImpl implements IFsCourseRedPacketLogSer
         }
 
     }
+
+    @Override
+    public void backupRedPacketLog() {
+        log.info("开始执行数据迁移任务");
+
+       /* // 检查是否在凌晨3点之后
+        if (shouldStopAt3AM()) {
+            log.info("已到凌晨3点,停止数据迁移任务");
+            return;
+        }*/
+
+        // 设置查询条件:获取12月之前的数据
+        FsCourseRedPacketLog queryLog = createQueryCondition();
+
+        // 分页参数配置
+        int pageSize = 1000; // 每批次处理数量,可根据性能调整
+        int pageNum = 1;
+        int totalProcessed = 0;
+        boolean hasMoreData = true;
+
+        try {
+            while (hasMoreData) {
+                // 检查是否到凌晨3点
+              /*  if (shouldStopAt3AM()) {
+                    log.info("已到凌晨3点,停止数据迁移任务,已处理{}条数据", totalProcessed);
+                    break;
+                }*/
+
+                log.info("开始查询第{}批数据,查询条件:beginTime={}, endTime={}",
+                        pageNum, queryLog.getBeginTime(), queryLog.getEndTime());
+
+                // 使用分页查询
+                PageHelper.startPage(pageNum, pageSize);
+                List<FsCourseRedPacketLog> fsCourseRedPacketLogs = fsCourseRedPacketLogMapper.selectFsCourseRedPacketLogList(queryLog);
+
+                if (fsCourseRedPacketLogs == null || fsCourseRedPacketLogs.isEmpty()) {
+                    log.info("所有12月之前的数据已迁移完成");
+                    hasMoreData = false;
+                    continue;
+                }
+
+                log.info("第{}批查询到{}条数据,开始处理", pageNum, fsCourseRedPacketLogs.size());
+
+                // 批量处理数据(迁移到目标表并删除源表数据)
+                int processedCount = processBatchData(fsCourseRedPacketLogs);
+                totalProcessed += processedCount;
+
+                log.info("第{}批数据处理完成,成功迁移{}条数据,累计迁移{}条",
+                        pageNum, processedCount, totalProcessed);
+
+                pageNum++;
+
+                // 清理分页参数,避免影响下次查询
+                PageHelper.clearPage();
+
+                // 小批量提交后稍作休息,避免数据库压力过大
+                if (hasMoreData) {
+                    Thread.sleep(100);
+                }
+            }
+
+            log.info("数据迁移任务完成,总计迁移{}条数据", totalProcessed);
+
+        } catch (InterruptedException e) {
+            log.warn("数据迁移任务被中断,已处理{}条数据", totalProcessed);
+            Thread.currentThread().interrupt();
+        } catch (Exception e) {
+            log.error("数据迁移过程中发生异常,已处理{}条数据", totalProcessed, e);
+        } finally {
+            // 确保清理分页参数
+            PageHelper.clearPage();
+        }
+    }
+
+    /**
+     * 创建查询条件 - 获取所有12月之前的数据
+     */
+    private FsCourseRedPacketLog createQueryCondition() {
+        FsCourseRedPacketLog queryLog = new FsCourseRedPacketLog();
+
+        // 设置开始时间为系统允许的最早时间(可根据实际情况调整)
+        queryLog.setBeginTime(getEarliestTimeString());
+
+        // 设置结束时间为今年12月1日之前(包含历史所有年份的12月之前数据)
+        queryLog.setEndTime(getDecemberFirstTimeString());
+
+        return queryLog;
+    }
+
+    /**
+     * 批量处理日志数据
+     * @return 成功处理的数据条数
+     */
+    private int processBatchData(List<FsCourseRedPacketLog> logs) {
+        if (logs == null || logs.isEmpty()) {
+            return 0;
+        }
+
+        int successCount = 0;
+
+        try {
+            // 1. 批量插入到目标表
+            log.debug("开始批量插入数据到目标表,数量:{}", logs.size   ());
+            int insertCount = fsCourseRedPacketLogMapper.batchInsert(logs);
+            log.debug("批量插入完成,插入{}条数据", insertCount);
+
+            if (insertCount > 0) {
+                // 2. 获取成功插入的数据ID
+                List<Long> idsToDelete = logs.stream()
+                        .limit(insertCount) // 只删除成功插入的数据
+                        .map(FsCourseRedPacketLog::getLogId)
+                        .filter(Objects::nonNull)
+                        .collect(Collectors.toList());
+
+                if (!idsToDelete.isEmpty()) {
+                    // 3. 批量删除源表数据
+                    log.debug("开始批量删除源表数据,数量:{}", idsToDelete.size());
+                    int deleteCount = fsCourseRedPacketLogMapper.batchDeleteByIds(idsToDelete);
+                    log.debug("批量删除完成,删除{}条数据", deleteCount);
+
+                    // 返回成功处理的数量(以插入成功为准)
+                    successCount = insertCount;
+                }
+            }
+
+        } catch (Exception e) {
+            log.error("批量处理数据时发生异常", e);
+            // 这里可以根据需要添加重试逻辑
+            // retryProcessBatchData(logs);
+        }
+
+        return successCount;
+    }
+
+    /**
+     * 检查是否应该停止(凌晨3点之后)
+     */
+    private boolean shouldStopAt3AM() {
+        Calendar calendar = Calendar.getInstance();
+        int hour = calendar.get(Calendar.HOUR_OF_DAY);
+        int minute = calendar.get(Calendar.MINUTE);
+
+        // 凌晨3点及之后返回true
+        return hour >= 3;
+    }
+
+    /**
+     * 获取最早时间字符串(String格式)
+     */
+    private String getEarliestTimeString() {
+        try {
+            // 这里可以根据业务设置一个最早的开始时间
+            // 例如:如果只迁移最近3年的数据
+            Calendar calendar = Calendar.getInstance();
+            calendar.add(Calendar.YEAR, -3); // 3年前
+            calendar.set(Calendar.MONTH, 0);
+            calendar.set(Calendar.DAY_OF_MONTH, 1);
+            calendar.set(Calendar.HOUR_OF_DAY, 0);
+            calendar.set(Calendar.MINUTE, 0);
+            calendar.set(Calendar.SECOND, 0);
+            calendar.set(Calendar.MILLISECOND, 0);
+
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            return sdf.format(calendar.getTime());
+        } catch (Exception e) {
+            log.warn("设置最早时间失败,使用默认值null", e);
+            return null; // 返回null表示不限制最早时间
+        }
+    }
+
+    /**
+     * 获取今年12月1日之前的时间字符串
+     */
+    private String getDecemberFirstTimeString() {
+        try {
+            Calendar calendar = Calendar.getInstance();
+            int currentYear = calendar.get(Calendar.YEAR);
+
+            // 获取当前年份的12月1日
+            calendar.set(currentYear, Calendar.DECEMBER, 1, 0, 0, 0);
+            calendar.set(Calendar.MILLISECOND, 0);
+
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            return sdf.format(calendar.getTime());
+        } catch (Exception e) {
+            log.error("获取12月1日时间失败", e);
+            // 返回一个很早的时间作为兜底
+            return "2020-12-01 00:00:00";
+        }
+    }
+
 }

+ 201 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java

@@ -51,6 +51,7 @@ import com.fs.store.service.cache.IFsUserCacheService;
 import com.fs.store.service.cache.IFsUserCourseCacheService;
 import com.fs.system.service.ISysConfigService;
 import com.fs.tag.service.FsTagUpdateService;
+import com.github.pagehelper.PageHelper;
 import com.hc.openapi.tool.util.StringUtils;
 import org.apache.commons.collections4.CollectionUtils;
 import org.slf4j.Logger;
@@ -63,6 +64,7 @@ import org.springframework.transaction.annotation.Transactional;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
+import java.text.SimpleDateFormat;
 import java.time.Duration;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
@@ -1459,6 +1461,205 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         return fsCourseWatchLogMapper.getExContactIdsIdsByWatchLogIds(watchLogIds);
     }
 
+    @Override
+    public void backupWatchLog() {
+        log.info("开始执行数据迁移任务");
+
+        // 检查是否在凌晨3点之后
+/*        if (shouldStopAt3AM()) {
+            log.info("已到凌晨3点,停止数据迁移任务");
+            return;
+        }*/
+
+        // 设置查询条件:获取12月之前的数据
+        FsCourseWatchLog queryLog = createQueryCondition();
+
+        // 分页参数配置
+        int pageSize = 1000; // 每批次处理数量,可根据性能调整
+        int pageNum = 1;
+        int totalProcessed = 0;
+        boolean hasMoreData = true;
+
+        try {
+            while (hasMoreData) {
+                // 检查是否到凌晨3点
+               /* if (shouldStopAt3AM()) {
+                    log.info("已到凌晨3点,停止数据迁移任务,已处理{}条数据", totalProcessed);
+                    break;
+                }*/
+
+                log.info("开始查询第{}批数据,查询条件:beginTime={}, endTime={}",
+                        pageNum, queryLog.getBeginTime(), queryLog.getEndTime());
+
+                // 使用分页查询
+                PageHelper.startPage(pageNum, pageSize);
+                List<FsCourseWatchLog> fsCourseWatchLogs = fsCourseWatchLogMapper.selectFsCourseWatchLogList(queryLog);
+
+                if (fsCourseWatchLogs == null || fsCourseWatchLogs.isEmpty()) {
+                    log.info("所有12月之前的数据已迁移完成");
+                    hasMoreData = false;
+                    continue;
+                }
+
+                log.info("第{}批查询到{}条数据,开始处理", pageNum, fsCourseWatchLogs.size());
+
+                // 批量处理数据(迁移到目标表并删除源表数据)
+                int processedCount = processBatchData(fsCourseWatchLogs);
+                totalProcessed += processedCount;
+
+                log.info("第{}批数据处理完成,成功迁移{}条数据,累计迁移{}条",
+                        pageNum, processedCount, totalProcessed);
+
+                pageNum++;
+
+                // 清理分页参数,避免影响下次查询
+                PageHelper.clearPage();
+
+                // 小批量提交后稍作休息,避免数据库压力过大
+                if (hasMoreData) {
+                    Thread.sleep(100);
+                }
+            }
+
+            log.info("数据迁移任务完成,总计迁移{}条数据", totalProcessed);
+
+        } catch (InterruptedException e) {
+            log.warn("数据迁移任务被中断,已处理{}条数据", totalProcessed);
+            Thread.currentThread().interrupt();
+        } catch (Exception e) {
+            log.error("数据迁移过程中发生异常,已处理{}条数据", totalProcessed, e);
+        } finally {
+            // 确保清理分页参数
+            PageHelper.clearPage();
+        }
+    }
+
+    /**
+     * 创建查询条件 - 获取所有12月之前的数据
+     */
+    private FsCourseWatchLog createQueryCondition() {
+        FsCourseWatchLog queryLog = new FsCourseWatchLog();
+
+        // 设置开始时间为系统允许的最早时间(可根据实际情况调整)
+        queryLog.setBeginTime(getEarliestTimeString());
+
+        // 设置结束时间为今年12月1日之前(包含历史所有年份的12月之前数据)
+        queryLog.setEndTime(getLastYearDecemberFirstTimeString());
+
+        return queryLog;
+    }
+
+    /**
+     * 批量处理日志数据
+     * @return 成功处理的数据条数
+     */
+    private int processBatchData(List<FsCourseWatchLog> logs) {
+        if (logs == null || logs.isEmpty()) {
+            return 0;
+        }
+
+        int successCount = 0;
+
+        try {
+            // 1. 批量插入到目标表
+            log.debug("开始批量插入数据到目标表,数量:{}", logs.size());
+            int insertCount = fsCourseWatchLogMapper.batchInsert(logs);
+            log.debug("批量插入完成,插入{}条数据", insertCount);
+
+            if (insertCount > 0) {
+                // 2. 获取成功插入的数据ID
+                List<Long> idsToDelete = logs.stream()
+                        .limit(insertCount) // 只删除成功插入的数据
+                        .map(FsCourseWatchLog::getLogId)
+                        .filter(Objects::nonNull)
+                        .collect(Collectors.toList());
+
+                if (!idsToDelete.isEmpty()) {
+                    // 3. 批量删除源表数据
+                    log.debug("开始批量删除源表数据,数量:{}", idsToDelete.size());
+                    int deleteCount = fsCourseWatchLogMapper.batchDeleteByIds(idsToDelete);
+                    log.debug("批量删除完成,删除{}条数据", deleteCount);
+
+                    // 返回成功处理的数量(以插入成功为准)
+                    successCount = insertCount;
+                }
+            }
+
+        } catch (Exception e) {
+            log.error("批量处理数据时发生异常", e);
+            // 这里可以根据需要添加重试逻辑
+            // retryProcessBatchData(logs);
+        }
+
+        return successCount;
+    }
+
+    /**
+     * 检查是否应该停止(凌晨3点之后)
+     */
+    private boolean shouldStopAt3AM() {
+        Calendar calendar = Calendar.getInstance();
+        int hour = calendar.get(Calendar.HOUR_OF_DAY);
+        int minute = calendar.get(Calendar.MINUTE);
+
+        // 凌晨3点及之后返回true
+        return hour >= 3;
+    }
+
+    /**
+     * 获取最早时间字符串(String格式)
+     */
+    private String getEarliestTimeString() {
+        try {
+            // 这里可以根据业务设置一个最早的开始时间
+            // 例如:如果只迁移最近3年的数据
+            Calendar calendar = Calendar.getInstance();
+            calendar.add(Calendar.YEAR, -3); // 3年前
+            calendar.set(Calendar.MONTH, 0);
+            calendar.set(Calendar.DAY_OF_MONTH, 1);
+            calendar.set(Calendar.HOUR_OF_DAY, 0);
+            calendar.set(Calendar.MINUTE, 0);
+            calendar.set(Calendar.SECOND, 0);
+            calendar.set(Calendar.MILLISECOND, 0);
+
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            return sdf.format(calendar.getTime());
+        } catch (Exception e) {
+            log.warn("设置最早时间失败,使用默认值null", e);
+            return null; // 返回null表示不限制最早时间
+        }
+    }
+
+    /**
+     * 获取去年12月1日之前的时间字符串
+     * 注意:这个是时间上限,查询条件是小于这个时间的数据
+     */
+    private String getLastYearDecemberFirstTimeString() {
+        try {
+            Calendar calendar = Calendar.getInstance();
+            int currentYear = calendar.get(Calendar.YEAR);
+
+            // 🔴 关键修改:去年12月1日,不是今年
+            int lastYear = currentYear - 1;
+
+            // 设置去年12月1日 00:00:00
+            calendar.set(lastYear, Calendar.DECEMBER, 1, 0, 0, 0);
+            calendar.set(Calendar.MILLISECOND, 0);
+
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            String result = sdf.format(calendar.getTime());
+
+            log.info("📅 设置查询结束时间:去年12月1日 = {}", result);
+            return result;
+
+        } catch (Exception e) {
+            log.error("获取去年12月1日时间失败", e);
+            // 兜底:返回前年12月1日,确保能查到历史数据
+            return (Calendar.getInstance().get(Calendar.YEAR) - 2) + "-12-01 00:00:00";
+        }
+    }
+
+
 
 
 }

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

@@ -145,6 +145,8 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     @Autowired
     private FsUserCourseVideoMapper fsUserCourseVideoMapper;
     @Autowired
+    private FsCoursePlaySourceConfigMapper fsCoursePlaySourceConfigMapper;
+    @Autowired
     private QwGroupChatMapper qwGroupChatMapper;
     @Autowired
     private QwGroupChatUserMapper qwGroupChatUserMapper;
@@ -4502,7 +4504,16 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         news.put("miniprogramAppid", qwCompany.getMiniAppId());
         news.put("miniprogramTitle", "点击注册");
         news.put("miniprogramPage", link.getRealLink());
-        news.put("miniprogramPicUrl", "https://cos.his.cdwjyyh.com/fs/20251008/15512254ec6747949f45071f0338df79.png");
+        FsCoursePlaySourceConfig fsCoursePlaySourceConfig = fsCoursePlaySourceConfigMapper.selectCoursePlaySourceConfigByAppId(qwCompany.getMiniAppId());
+        if (ObjectUtils.isEmpty(fsCoursePlaySourceConfig)) {
+            String json = configService.selectConfigByKey("course.config");
+            CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+
+            news.put("miniprogramPicUrl", config.getSidebarImageUrl());
+        }else {
+            news.put("miniprogramPicUrl",fsCoursePlaySourceConfig.getImg());
+
+        }
 
         return R.ok().put("data",news);
     }
@@ -4530,6 +4541,34 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
 
         return link;
     }
+    @Override
+    public R registerQwFsUser(FsUserCourseVideoAddKfUParam param) {
+        logger.info("zyp \n【判断添加客服】:{}", param);
+
+        // 参数校验
+        if (param == null) {
+            return R.error("参数不能为空");
+        }
 
+        // 查询用户
+        FsUser fsUser = fsUserMapper.selectFsUserByUserId(param.getUserId());
+
+        // 用户不存在唤起重新授权
+        if (fsUser == null) {
+            return R.error(401, "用户不存在");
+        }
+
+        if (fsUser.getStatus() != null && fsUser.getStatus() == 0) {
+            return R.error("会员被停用,无权限,请联系客服!");
+        }
+
+        // 处理群聊逻辑
+        if (param.getChatId() != null) {
+            return handleGroupChatLogic(param,fsUser);
+        }
+
+        // 处理普通外部联系人逻辑
+        return handleExternalContactLogic(param, fsUser);
+    }
 }
 

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

@@ -126,8 +126,8 @@ public class FsIntegralOrder
     private Date createTime;
     @TableField(exist = false)
     private Date updateTime;
-
+    @TableField(exist = false)
     private Integer deliveryStatus;
-
+    @TableField(exist = false)
     private String deliveryType;
 }

+ 30 - 0
fs-service/src/main/java/com/fs/his/enums/FsIntegralOrderStatusEnum.java

@@ -0,0 +1,30 @@
+package com.fs.his.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.stream.Stream;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public enum FsIntegralOrderStatusEnum {
+    STATUS_1(1,"待发货"),
+    STATUS_2(2,"待收货"),
+    STATUS_3(3,"已完成"),
+    STATUS_4(4,"待支付"),
+    STATUS_6(5,"已取消");
+
+
+
+    private Integer value;
+    private String desc;
+
+    public static FsIntegralOrderStatusEnum toType(int value) {
+        return Stream.of(FsIntegralOrderStatusEnum.values())
+                .filter(p -> p.value == value)
+                .findAny()
+                .orElse(null);
+    }
+}

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

@@ -34,6 +34,7 @@ public enum FsUserIntegralLogTypeEnum {
     TYPE_24(24, "付费课程订阅"),
     TYPE_25(25, "直播完课积分"),
     TYPE_26(26, "直播红包积分"),
+    TYPE_27(27, "积分订单取消退回积分"),
     ;
 
 

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

@@ -50,4 +50,6 @@ public interface ILiveCompletionPointsRecordService {
      * @return 完课记录
      */
     LiveCompletionPointsRecord selectByUserAndDate(Long liveId, Long userId, Date date);
+
+    LiveCompletionPointsRecord createCompletionRecord(Long liveId, Long userId);
 }

+ 26 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveAfterSalesServiceImpl.java

@@ -421,6 +421,32 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
             log.info("申请售后订单锁获取成功,订单号:{}", param.getOrderCode());
 
             LiveOrder order=liveOrderService.selectOrderIdByOrderCode(param.getOrderCode());
+            
+            // 查询配置:是否删除历史售后数据
+            try {
+                String deleteAfterSalesConfig = configService.selectConfigByKey("delete_after_sales");
+                if (StringUtils.isNotEmpty(deleteAfterSalesConfig) && "true".equalsIgnoreCase(deleteAfterSalesConfig.trim())) {
+                    // 查询历史售后数据,将 is_del 设置为 1
+                    if (order != null && order.getOrderId() != null) {
+                        LiveAfterSales queryAfterSales = new LiveAfterSales();
+                        queryAfterSales.setOrderId(order.getOrderId());
+                        List<LiveAfterSales> historyAfterSalesList = baseMapper.selectLiveAfterSalesList(queryAfterSales);
+                        if (historyAfterSalesList != null && !historyAfterSalesList.isEmpty()) {
+                            for (LiveAfterSales historyAfterSales : historyAfterSalesList) {
+                                if (historyAfterSales.getIsDel() != null && historyAfterSales.getIsDel() == 0) {
+                                    LiveAfterSales updateAfterSales = new LiveAfterSales();
+                                    updateAfterSales.setId(historyAfterSales.getId());
+                                    updateAfterSales.setIsDel(1);
+                                    baseMapper.updateLiveAfterSales(updateAfterSales);
+                                    log.info("删除历史售后数据,售后ID:{},订单ID:{}", historyAfterSales.getId(), order.getOrderId());
+                                }
+                            }
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                log.error("查询或更新历史售后数据失败", e);
+            }
             if(!order.getUserId().equals(userId)){
                 throw new CustomException("非法操作");
             }

+ 114 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionPointsRecordServiceImpl.java

@@ -269,6 +269,120 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
         return recordMapper.selectByUserAndDate(liveId, userId, date);
     }
 
+    @Override
+    public LiveCompletionPointsRecord createCompletionRecord(Long liveId, Long userId) {
+        try {
+            // 1. 获取直播信息和配置
+            Live live = liveService.selectLiveByLiveId(liveId);
+            if (live == null) {
+
+                return null;
+            }
+
+            // 2. 从数据库获取完课积分配置
+            CompletionPointsConfig config = getCompletionPointsConfig(live);
+
+            // 检查是否开启完课积分功能
+            if (!config.isEnabled()) {
+
+                return null;
+            }
+
+            // 检查配置完整性
+            Integer completionRate = config.getCompletionRate();
+            int[] pointsConfig = config.getPointsConfig();
+
+            if (completionRate == null || pointsConfig == null || pointsConfig.length == 0) {
+
+                return null;
+            }
+
+            // 3. 获取观看时长(如果为null,则从数据库累计直播+回放时长)
+            // 4.这个时间和liveWatchUser分开算
+            long actualWatchDuration = 0L;
+
+            // 4. 获取视频总时长(秒)
+            Long videoDuration = live.getDuration();
+            if (videoDuration == null || videoDuration <= 0) {
+
+                return null;
+            }
+
+            // 5. 计算完课比例
+            BigDecimal watchRate = BigDecimal.valueOf(actualWatchDuration)
+                    .multiply(BigDecimal.valueOf(100))
+                    .divide(BigDecimal.valueOf(videoDuration), 2, RoundingMode.HALF_UP);
+
+            // 限制完课比例最大值为100.00%(防止数据库字段溢出)
+            if (watchRate.compareTo(BigDecimal.valueOf(100)) > 0) {
+                watchRate = BigDecimal.valueOf(100);
+            }
+
+//            // 6. 判断是否达到完课标准
+//            if (watchRate.compareTo(BigDecimal.valueOf(completionRate)) < 0) {
+//
+//                return null;
+//            }
+
+            // 7. 检查今天是否已有完课记录
+            LocalDate today = LocalDate.now();
+            Date currentDate = Date.from(today.atStartOfDay(ZoneId.systemDefault()).toInstant());
+
+            LiveCompletionPointsRecord todayRecord = recordMapper.selectByUserAndDate(liveId, userId, currentDate);
+            if (todayRecord != null) {
+
+                return todayRecord;
+            }
+
+            // 7. 查询最近一次完课记录(不限直播间),计算连续天数
+            LiveCompletionPointsRecord latestRecord = recordMapper.selectLatestByUser(userId);
+            int continuousDays = 1;
+
+            if (latestRecord != null) {
+                LocalDate lastDate = latestRecord.getCurrentCompletionDate()
+                        .toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+
+                long daysBetween = ChronoUnit.DAYS.between(lastDate, today);
+
+                if (daysBetween == 0) {
+                    continuousDays = latestRecord.getContinuousDays();
+
+                } else if (daysBetween == 1) {
+                    // 昨天完课了,连续天数+1
+                    continuousDays = latestRecord.getContinuousDays() + 1;
+                } else {
+                    // 中断了,重新开始
+                    continuousDays = 1;
+                }
+            }
+
+            // 8. 计算积分
+            int points = calculatePoints(continuousDays, pointsConfig);
+
+            // 9. 创建完课记录
+            LiveCompletionPointsRecord record = new LiveCompletionPointsRecord();
+            record.setLiveId(liveId);
+            record.setUserId(userId);
+            record.setWatchDuration(actualWatchDuration);
+            record.setVideoDuration(videoDuration);
+            record.setCompletionRate(watchRate);
+            record.setContinuousDays(continuousDays);
+            record.setPointsAwarded(points);
+            record.setCurrentCompletionDate(currentDate);
+            record.setReceiveStatus(0); // 未领取
+
+            if (latestRecord != null) {
+                record.setLastCompletionDate(latestRecord.getCurrentCompletionDate());
+            }
+
+            recordMapper.insertRecord(record);
+            return record;
+        } catch (Exception e) {
+            log.error("检查并创建完课记录失败, liveId={}, userId={}", liveId, userId, e);
+            throw e;
+        }
+    }
+
     /**
      * 从直播配置中获取完课积分配置
      */

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

@@ -737,14 +737,16 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
             }
 
             // 佣金处理
-            if (order.getCompanyUserId() == -1L) {
-                companyService.addCompanyTuiLiveMoney(order);
-            } else {
-                // 目前是一级佣金
-                FsStoreProduct product = JSONUtil.toBean(order.getItemJson(), FsStoreProduct.class);
-                List<FsStoreProductAttrValueScrm> productAttrValues = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueByProductId(product.getProductId());
-                if (productAttrValues != null && !productAttrValues.isEmpty()) {
-                    userService.addTuiLiveMoney(order, productAttrValues);
+            if (order.getCompanyUserId() != null || order.getCompanyId() != null) {
+                if (order.getCompanyUserId() == -1L) {
+                    companyService.addCompanyTuiLiveMoney(order);
+                } else {
+                    // 目前是一级佣金
+                    FsStoreProduct product = JSONUtil.toBean(order.getItemJson(), FsStoreProduct.class);
+                    List<FsStoreProductAttrValueScrm> productAttrValues = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueByProductId(product.getProductId());
+                    if (productAttrValues != null && !productAttrValues.isEmpty()) {
+                        userService.addTuiLiveMoney(order, productAttrValues);
+                    }
                 }
             }
 
@@ -797,10 +799,8 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                         List<LiveWatchLog> logs = liveWatchLogService.selectLiveWatchLogList(queryLog);
                         if (logs != null && !logs.isEmpty()) {
                             for (LiveWatchLog log : logs) {
-                                if (log.getLogType() == null || log.getLogType() != 2) {
-                                    log.setLiveBuy(1);
-                                    liveWatchLogService.updateLiveWatchLog(log);
-                                }
+                                log.setLiveBuy(1);
+                                liveWatchLogService.updateLiveWatchLog(log);
                             }
                         }
                     }

+ 171 - 84
fs-service/src/main/java/com/fs/live/service/impl/LiveRedConfServiceImpl.java

@@ -71,6 +71,7 @@ public class LiveRedConfServiceImpl implements ILiveRedConfService {
     private static final String REDPACKET_REMAININGLOTS_KEY = "live:red:remainingLots:";
     private static final String REDPACKET_REMAININGNUM_KEY = "live:red:remainingNum:";
     private static final String REDPACKET_CLAIM_KEY = "live:red:claim:";
+    private static final String REDPACKET_CONF_CACHE_KEY = "live:red:conf:"; // 红包配置缓存
 
     @Autowired
     private LiveRedConfMapper baseMapper;
@@ -130,10 +131,17 @@ public class LiveRedConfServiceImpl implements ILiveRedConfService {
             double score = localDateTime.atZone(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli();
             redisCache.redisTemplate.opsForZSet().add(cacheKey, String.valueOf(liveRedConf.getRedId()), score);
             redisCache.redisTemplate.expire(cacheKey, 30, TimeUnit.MINUTES);
+            
+            // 将红包配置缓存到 Redis(用于高并发查询)
+            String redConfCacheKey = REDPACKET_CONF_CACHE_KEY + liveRedConf.getRedId();
+            redisCache.setCacheObject(redConfCacheKey, JSONUtil.toJsonStr(liveRedConf), liveRedConf.getDuration().intValue() + 5, TimeUnit.MINUTES);
+            log.info("红包配置已缓存到 Redis,redId: {}, liveId: {}", liveRedConf.getRedId(), liveRedConf.getLiveId());
         } else {
             // 其他
             redisCache.deleteObject(REDPACKET_REMAININGLOTS_KEY + liveRedConf.getRedId());
             redisCache.deleteObject(cacheKey);
+            // 删除红包配置缓存
+            redisCache.deleteObject(REDPACKET_CONF_CACHE_KEY + liveRedConf.getRedId());
             redStatusUpdate(CollUtil.newHashSet(liveRedConf.getRedId()));
         }
         return baseMapper.updateLiveRedConf(liveRedConf);
@@ -226,95 +234,151 @@ public class LiveRedConfServiceImpl implements ILiveRedConfService {
         baseMapper.deleteById(redId);
     }
 
-    /**
-     * 用户领取红包
-     */
+
     @Override
-    @Transactional
     public R claimRedPacket(RedPO red) {
-        // String claimKey = REDPACKET_CLAIM_KEY + red.getRedId();
-        Object o = redisCache.hashGet(String.format(LiveKeysConstant. LIVE_HOME_PAGE_CONFIG_RED, red.getLiveId(), red.getRedId()), String.valueOf(red.getUserId()));
-        if (ObjectUtil.isNotEmpty(o)) {
-            return R.error("您已经领取过红包了!");
-        }
-        /*try {*/
-/*            //获取红包锁w
-            if (!tryLock(claimKey, red.getUserId().toString(), 5)) {
-                return R.error("您已经领取过红包了!");
-            }*/
-
-        LiveRedConf conf = baseMapper.selectLiveRedConfByRedId(red.getRedId());
-        if (conf == null || conf.getRedStatus() != 1) {
-            return R.error("手慢了,红包已结束~");
-        }
-        //redis剩余红包数
-        // 平均分 暂时不适用redis 记录红包数
-        Long integral = calculateIntegralAverage(conf);
-        if (0L == integral) {
-            return R.error("手慢了,红包被抢完了~");
-        }
-
-        // 更新数据库
-/*
-        Date now = new Date();
-        conf.setTotalSend(conf.getTotalSend() + 1);
-        conf.setRemaining(Math.toIntExact(conf.getTotalLots() - conf.getTotalSend()));
-        conf.setUpdateTime(now);
-        baseMapper.updateLiveRedConf(conf);
-*/
-
-        // 最后更新缓存
-        if (getRemaining(red.getRedId()) <= 0 || !decreaseRemainingLotsIfPossible(red.getRedId())) {
-            LiveRedConf liveRedConf = new LiveRedConf();
-            liveRedConf.setRedId(red.getRedId());
-            liveRedConf.setRedStatus(2L);
-            baseMapper.updateLiveRedConf(liveRedConf);
-            Set<String> range = CollUtil.newHashSet(String.valueOf(red.getRedId()));
-            finishRedStatusBySetIds(range);
-            return R.error("手慢了,红包已被抢完~");
-        }
-        // 记录用户红包
+//             * 1. 使用 Redis HSETNX 原子操作保证幂等性(每个用户只能领取一次)
+//     * 2. 从 Redis 读取红包配置(提高响应速度)
+//     * 3. 使用 Redis 原子操作减少剩余数量
+//                * 4. 异步更新数据库
+        String redisKey = String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_RED, red.getLiveId(), red.getRedId());
+        String userIdStr = String.valueOf(red.getUserId());
+        
+        // 1. 使用 Redis HSETNX 原子操作保证幂等性(每个用户只能领取一次)
+        // 先尝试在 Redis 中标记用户已领取(原子操作,保证高并发安全)
         LiveUserRedRecord record = new LiveUserRedRecord();
         record.setRedId(red.getRedId());
         record.setLiveId(red.getLiveId());
         record.setUserId(red.getUserId());
-        record.setIntegral(integral);
         record.setCreateTime(new Date());
-        // 更新用户余额
-        BigDecimal balanceAmount = BigDecimal.valueOf(integral);
-        int updateResult = fsUserScrmMapper.incrIntegral(red.getUserId(), balanceAmount);
-        if (updateResult <= 0) {
-            log.error("更新用户余额失败,userId: {}, balance: {}", red.getUserId(), balanceAmount);
-            return R.error("更新用户余额失败");
+        
+        // 使用 HSETNX 原子操作:如果字段不存在则设置,返回 true;如果已存在则返回 false
+        Boolean claimed = redisCache.hashPutIfAbsent(redisKey, userIdStr, "claimed");
+        if (Boolean.FALSE.equals(claimed)) {
+            // 用户已经领取过(Redis 中已存在记录)
+            log.debug("用户 {} 已领取过红包 redId: {}(Redis 检查)", red.getUserId(), red.getRedId());
+            return R.error("您已经领取过红包了!");
+        } else {
+            redisCache.expire(redisKey, 24, TimeUnit.HOURS);
         }
+        
+        try {
+            // 2. 从 Redis 读取红包配置(优先从缓存读取,提高响应速度)
+            String redConfCacheKey = REDPACKET_CONF_CACHE_KEY + red.getRedId();
+            Object confCache = redisCache.getCacheObject(redConfCacheKey);
+            LiveRedConf conf = null;
+            
+            if (confCache != null) {
+                try {
+                    conf = JSONUtil.toBean(confCache.toString(), LiveRedConf.class);
+                    log.debug("从 Redis 缓存读取红包配置,redId: {}", red.getRedId());
+                } catch (Exception e) {
+                    log.warn("从 Redis 缓存解析红包配置失败,从数据库读取,redId: {}", red.getRedId(), e);
+                }
+            }
+            
+            // 如果 Redis 中没有配置,从数据库读取并缓存
+            if (conf == null) {
+                conf = baseMapper.selectLiveRedConfByRedId(red.getRedId());
+                if (conf != null && conf.getRedStatus() == 1) {
+                    // 缓存到 Redis
+                    redisCache.setCacheObject(redConfCacheKey, JSONUtil.toJsonStr(conf), conf.getDuration().intValue() + 5, TimeUnit.MINUTES);
+                    log.debug("从数据库读取红包配置并缓存到 Redis,redId: {}", red.getRedId());
+                }
+            }
+            
+            // 验证红包状态
+            if (conf == null || conf.getRedStatus() != 1) {
+                // 回滚:删除 Redis 中的标记
+                redisCache.hashDelete(redisKey, userIdStr);
+                return R.error("手慢了,红包已结束~");
+            }
+            
+            // 3. 使用 Redis 原子操作减少剩余数量
+            if (getRemaining(red.getRedId()) <= 0 || !decreaseRemainingLotsIfPossible(red.getRedId())) {
+                // 回滚:删除 Redis 中的标记
+                redisCache.hashDelete(redisKey, userIdStr);
+                // 更新红包状态为已结束
+                LiveRedConf liveRedConf = new LiveRedConf();
+                liveRedConf.setRedId(red.getRedId());
+                liveRedConf.setRedStatus(2L);
+                baseMapper.updateLiveRedConf(liveRedConf);
+                // 删除配置缓存
+                redisCache.deleteObject(redConfCacheKey);
+                return R.error("手慢了,红包已被抢完~");
+            }
+            
+            // 计算积分(平均分)
+            Long integral = calculateIntegralAverage(conf);
+            if (0L == integral) {
+                // 回滚:删除 Redis 中的标记
+                redisCache.hashDelete(redisKey, userIdStr);
+                return R.error("手慢了,红包被抢完了~");
+            }
+            
+            record.setIntegral(integral);
+            
+            // 4. 更新用户积分(同步操作,保证数据一致性)
+            BigDecimal balanceAmount = BigDecimal.valueOf(integral);
+            int updateResult = fsUserScrmMapper.incrIntegral(red.getUserId(), balanceAmount);
+            if (updateResult <= 0) {
+                // 回滚:删除 Redis 中的标记和恢复剩余数量
+                redisCache.hashDelete(redisKey, userIdStr);
+                // 恢复剩余数量
+                String remainingKey = REDPACKET_REMAININGLOTS_KEY + red.getRedId();
+                redisCache.redisTemplate.opsForValue().increment(remainingKey);
+                log.error("更新用户余额失败,userId: {}, balance: {}", red.getUserId(), balanceAmount);
+                return R.error("更新用户余额失败");
+            }
+            
+            // 5. 更新 Redis 缓存中的记录(包含完整信息)
+            record.setCreateTime(new Date());
+            redisCache.hashPut(redisKey, userIdStr, JSONUtil.toJsonStr(record));
+            
+            // 6. 异步更新数据库(提高响应速度,不阻塞用户)
+            // 查询用户当前余额(用于积分日志)
+            com.fs.hisStore.domain.FsUserScrm user = fsUserScrmMapper.selectFsUserById(red.getUserId());
+            Long currentIntegral = user.getIntegral() != null ? user.getIntegral() : 0L;
+            
+
+            final LiveUserRedRecord finalRecord = record;
+            final LiveRedConf finalConf = conf;
+            final Long finalIntegral = integral;
+            final Long finalCurrentIntegral = currentIntegral;
+            
+        try {
+            // 插入红包记录
+            userRedRecordMapper.insertLiveUserRedRecord(finalRecord);
+
+            // 添加积分变动记录
+            FsUserIntegralLogs integralLogs = new FsUserIntegralLogs();
+            integralLogs.setUserId(red.getUserId());
+            integralLogs.setIntegral(finalIntegral);
+            integralLogs.setBalance(finalCurrentIntegral);
+            integralLogs.setLogType(FsUserIntegralLogTypeEnum.TYPE_26.getValue());
+            integralLogs.setBusinessId(String.valueOf(red.getRedId()));
+            integralLogs.setBusinessType(Math.toIntExact(finalConf.getRedId()));
+            integralLogs.setStatus(0);
+            integralLogs.setCreateTime(new Date());
+            fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLogs);
+
+            log.debug("异步更新数据库成功,userId: {}, redId: {}, integral: {}", red.getUserId(), red.getRedId(), finalIntegral);
+        } catch (Exception e) {
+            // 数据库更新失败不影响用户领取(已更新积分),记录日志即可
+            log.error("异步更新数据库失败,userId: {}, redId: {}, integral: {}", red.getUserId(), red.getRedId(), finalIntegral, e);
+        }
+
+            
 
-        // 查询用户当前余额
-        com.fs.hisStore.domain.FsUserScrm user = fsUserScrmMapper.selectFsUserById(red.getUserId());
-        Long currentIntegral = user.getIntegral() != null ? user.getIntegral() : 0L;
-        Long newIntegral = currentIntegral + integral;
-
-        // 添加余额变动记录
-        FsUserIntegralLogs integralLogs = new FsUserIntegralLogs();
-        integralLogs.setUserId(red.getUserId());
-        integralLogs.setIntegral(integral);
-        integralLogs.setBalance(newIntegral);
-        integralLogs.setLogType(FsUserIntegralLogTypeEnum.TYPE_26.getValue()); // 3表示分享获得积分,可根据实际情况调整
-        integralLogs.setBusinessId(String.valueOf(red.getRedId()));
-        integralLogs.setBusinessType(Math.toIntExact(conf.getRedId())); // 1表示直播红包
-        integralLogs.setStatus(0);
-        integralLogs.setCreateTime(new Date());
-        fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLogs);
-
-        // WebSocket 通知
-        //String msg = String.format("用户 %d 抢到了红包 %d,获得 %d 芳华币", userId, redId, integral);
-        //WebSocketServer.notifyUsers(msg);
-        redisCache.hashPut(String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_RED, red.getLiveId(), red.getRedId()), String.valueOf(red.getUserId()), JSONUtil.toJsonStr(record));
-        return R.ok("恭喜您成功抢到" + integral + "芳华币");
-/*        } catch (Exception e) {
-            e.printStackTrace();
-            log.error("抢红包异常:" + e.getMessage());
-        }*/
-        // return R.error("抢红包异常");
+            
+            return R.ok("恭喜您成功抢到" + integral + "芳华币");
+            
+        } catch (Exception e) {
+            // 发生异常,回滚 Redis 标记
+            redisCache.hashDelete(redisKey, userIdStr);
+            log.error("领取红包异常,userId: {}, redId: {}", red.getUserId(), red.getRedId(), e);
+            return R.error("领取红包失败,请稍后重试");
+        }
     }
 
     @Override
@@ -358,6 +422,9 @@ public class LiveRedConfServiceImpl implements ILiveRedConfService {
         // 插入抽奖记录
         for (Long id : redIds) {
             LiveRedConf liveRedConf = baseMapper.selectLiveRedConfByRedId(id);
+            if (liveRedConf == null) {
+                continue;
+            }
             // 更新数据库
             updateDbByRed(liveRedConf);
             String hashKey = String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_RED, liveRedConf.getLiveId(), liveRedConf.getRedId());
@@ -367,11 +434,31 @@ public class LiveRedConfServiceImpl implements ILiveRedConfService {
                 liveUserRedRecords = hashEntries.values().stream()
                         .map(value -> JSONUtil.toBean(JSONUtil.parseObj(value), LiveUserRedRecord.class))
                         .collect(Collectors.toList());
-                userRedRecordMapper.insertLiveUserRedRecordBatch(liveUserRedRecords);
+                
+                // 过滤掉已经存在于数据库中的记录(避免重复增加积分)
+                List<LiveUserRedRecord> newRecords = new ArrayList<>();
                 for (LiveUserRedRecord liveUserRedRecord : liveUserRedRecords) {
-                    userService.incrIntegral(Collections.singletonList(liveUserRedRecord.getUserId()), liveUserRedRecord.getIntegral());
-                    // 保存用户领取芳华币记录 方便统计计算
-                    saveUserRewardRecord(liveUserRedRecord);
+                    // 检查数据库中是否已存在该记录(使用 redId 和 userId 组合查询)
+                    LiveUserRedRecord queryRecord = new LiveUserRedRecord();
+                    queryRecord.setUserId(liveUserRedRecord.getUserId());
+                    queryRecord.setRedId(liveUserRedRecord.getRedId());
+                    List<LiveUserRedRecord> existingRecords = userRedRecordMapper.selectLiveUserRedRecordList(queryRecord);
+                    
+                    // 如果不存在,则添加到新记录列表
+                    if (existingRecords == null || existingRecords.isEmpty()) {
+                        newRecords.add(liveUserRedRecord);
+                    }
+                }
+                
+                // 只插入新记录(这些记录是从 Redis 同步过来的,但还没有插入数据库)
+                // 注意:积分增加逻辑已移除,现在只能通过 claimRedPacket 方法领取红包并增加积分
+                if (CollUtil.isNotEmpty(newRecords)) {
+                    try {
+                        userRedRecordMapper.insertLiveUserRedRecordBatch(newRecords);
+                        log.info("批量插入红包记录成功,redId: {}, 记录数: {}", id, newRecords.size());
+                    } catch (Exception e) {
+                        log.error("批量插入红包记录失败,redId: {}", id, e);
+                    }
                 }
             }
             redisCache.deleteObject(hashKey);

+ 2 - 2
fs-service/src/main/java/com/fs/live/vo/LiveAfterSalesVo.java

@@ -160,10 +160,10 @@ public class LiveAfterSalesVo {
 
     @Excel(name ="产品名称")
     private String productName;
-
+    
     /** 产品名称查询参数(用于搜索) */
     private String productNameQuery;
-
+    
     @Excel(name ="产品编码")
     private String productBarCode;
     @Excel(name ="规格")

+ 19 - 0
fs-service/src/main/java/com/fs/live/vo/LiveDataListVo.java

@@ -1,6 +1,7 @@
 package com.fs.live.vo;
 
 import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
 import lombok.Data;
 
 import java.math.BigDecimal;
@@ -15,59 +16,77 @@ import java.util.Date;
 @Data
 public class LiveDataListVo {
     /** 直播ID */
+    @Excel(name = "直播ID")
     private Long liveId;
 
     /** 直播名称 */
+    @Excel(name = "直播名称")
     private String liveName;
 
     /** 直播类型 1直播,2录播,3直播回放 */
+    @Excel(name = "直播类型 1直播,2录播,3直播回放")
     private Integer liveType;
 
     /** 直播状态 1未开播 2直播中 3已结束 4直播回放中 */
+    @Excel(name = "直播状态 1未开播 2直播中 3已结束 4直播回放中")
     private Integer status;
 
     /** 开始时间 */
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "开始时间", width = 30, dateFormat = "yyyy-MM-dd")
     private Date startTime;
 
     /** 结束时间 */
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "结束时间", width = 30, dateFormat = "yyyy-MM-dd")
     private Date finishTime;
 
     /** 累计观看人数 */
+    @Excel(name = "累计观看人数")
     private Long totalViewers = 0L;
 
     /** 直播观看人数 */
+    @Excel(name = "直播观看人数")
     private Long liveViewers = 0L;
 
     /** 回放观看人数 */
+    @Excel(name = "回放观看人数")
     private Long playbackViewers = 0L;
 
     /** 直播平均时长(秒) */
+    @Excel(name = "直播平均时长")
     private Long liveAvgDuration = 0L;
 
     /** 回放平均时长(秒) */
+    @Excel(name = "回放平均时长")
     private Long playbackAvgDuration = 0L;
 
     /** 累计完课人数 */
+    @Excel(name = "累计完课人数")
     private Long totalCompletedCourses = 0L;
 
     /** 直播完课人数 */
+    @Excel(name = "直播完课人数")
     private Long liveCompletedCourses = 0L;
 
     /** 回放完课人数 */
+    @Excel(name = "回放完课人数")
     private Long playbackCompletedCourses = 0L;
 
     /** GMV(总销售额) */
+    @Excel(name = "总销售额")
     private BigDecimal gmv = BigDecimal.ZERO;
 
     /** 付费人数 */
+    @Excel(name = "付费人数")
     private Long paidUsers = 0L;
 
     /** 付费单数 */
+    @Excel(name = "付费单数")
     private Long paidOrders = 0L;
 
     /** 销量统计 */
+    @Excel(name = "销量统计")
     private Long salesCount = 0L;
 }
 

+ 1 - 0
fs-service/src/main/java/com/fs/newAdv/constant/MqTopicConstant.java

@@ -24,4 +24,5 @@ public class MqTopicConstant {
      * 转化追踪consumer
      */
     public static final String CONVERSION_TRACKING_TOPIC_CONSUMER_GROUP = "conversion-topic-tracking-consumer-group";
+    public static final String CONVERSION_TRACKING_GROUP = "conversion-tracking-group";
 }

+ 16 - 7
fs-service/src/main/java/com/fs/newAdv/domain/Lead.java

@@ -31,10 +31,12 @@ public class Lead implements Serializable {
     private Long siteId;
 
     /**
-     * 链路id唯一(clickId)
+     * 链路id唯一
      */
     private String traceId;
 
+    private String clickId;
+
     /**
      * 来源平台
      */
@@ -76,16 +78,15 @@ public class Lead implements Serializable {
     /**
      * 外部联系人id 添加人id
      */
-    private String externalUserId;
-    /**
-     * 属于用户id 被添加的id
-     */
-    private String userId;
-    private String corpId;
+    private Long externalId;
     /**
      * 群Id
      */
     private String chatId;
+    /**
+     * 企微主题Id
+     */
+    private String corpId;
     /**
      * 是否添加企微 1是 0否
      */
@@ -104,6 +105,10 @@ public class Lead implements Serializable {
      */
     private String openid;
     private String unionid;
+    /**
+     * 微信名称 unionid不存在时关联
+     */
+    private String weiChatName;
     /**
      * 小程序授权1是 0否
      */
@@ -179,5 +184,9 @@ public class Lead implements Serializable {
      * 落地页访问时间
      */
     private LocalDateTime landingPageTs;
+    /**
+     * 落地页原始Url
+     */
+    private String viewUrl;
 }
 

+ 12 - 0
fs-service/src/main/java/com/fs/newAdv/dto/req/QwExternalIdBindTrackReq.java

@@ -0,0 +1,12 @@
+package com.fs.newAdv.dto.req;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class QwExternalIdBindTrackReq implements Serializable {
+    private static final long serialVersionUID = 1L;
+    private Long qwExternalId;
+    private String unionid;
+}

+ 10 - 0
fs-service/src/main/java/com/fs/newAdv/dto/req/TraceIdDto.java

@@ -0,0 +1,10 @@
+package com.fs.newAdv.dto.req;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class TraceIdDto implements Serializable {
+    private String traceId;
+}

+ 12 - 0
fs-service/src/main/java/com/fs/newAdv/dto/req/updateNickNameReq.java

@@ -0,0 +1,12 @@
+package com.fs.newAdv.dto.req;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class updateNickNameReq implements Serializable {
+    private static final long serialVersionUID = 1L;
+    private String nickName;
+    private String traceId;
+}

+ 8 - 8
fs-service/src/main/java/com/fs/newAdv/enums/SystemEventTypeEnum.java

@@ -5,14 +5,14 @@ import lombok.Getter;
 @Getter
 public enum SystemEventTypeEnum {
 
-    GROUP_TODAY("1", "当日加群"),
-    WEI_CHAT_TODAY("2", "当日加微"),
-    TO_CLASS_AND_GROUP_TODAY("3", "直播到课且当日加群"),
-    TO_CLASS_AND_WEI_CHAT_TODAY("4", "直播到课且当日加微"),
-    BUY_ORDER("5", "商品购买订单"),
-    AUTH_TODAY_CREATE("6", "微信授权且当日创建"),
-    COMPLETE_CLASS_AND_GROUP_TODAY("7", "直播完课且当日加群"),
-    COMPLETE_CLASS_AND_WEI_CHAT_TODAY("8", "直播完课且当日加微");
+    GROUP_TODAY("event1", "当日加群"),
+    WEI_CHAT_TODAY("event2", "当日加微"),
+    TO_CLASS_AND_GROUP_TODAY("event3", "直播到课且当日加群"),
+    TO_CLASS_AND_WEI_CHAT_TODAY("event4", "直播到课且当日加微"),
+    BUY_ORDER("event5", "商品购买订单"),
+    AUTH_TODAY_CREATE("event6", "微信授权且当日创建"),
+    COMPLETE_CLASS_AND_GROUP_TODAY("event7", "直播完课且当日加群"),
+    COMPLETE_CLASS_AND_WEI_CHAT_TODAY("event8", "直播完课且当日加微");
 
     private final String code;
     private final String description;

+ 2 - 1
fs-service/src/main/java/com/fs/newAdv/event/ConversionEventListener.java

@@ -58,11 +58,12 @@ public class ConversionEventListener {
                 new SendCallback() {
                     @Override
                     public void onSuccess(SendResult sendResult) {
+                        log.error("转化消息发送成功:{}", trackId);
                     }
 
                     @Override
                     public void onException(Throwable e) {
-                        log.error("转化消息发送失败:{}", trackId);
+                        log.error("转化消息发送失败:{}", trackId,e);
                         // TODO: 记录到数据库,后续补偿
                     }
                 }

+ 2 - 3
fs-service/src/main/java/com/fs/newAdv/integration/adapter/BaiduAdapter.java

@@ -28,9 +28,8 @@ public class BaiduAdapter implements IAdvertiserAdapter {
      */
     @Override
     public Lead adaptCallbackData(Map<String, String> rawData) {
-        log.info("百度数据适配:{}", rawData);
         Lead lead = new Lead();
-        lead.setTraceId(rawData.get("bd_vid"));
+        lead.setClickId(rawData.get("bd_vid"));
         lead.setAdvertiserId(AdvertiserTypeEnum.BAIDU.getCode());
         lead.setIp(rawData.get("ip"));
         lead.setIdeaId(rawData.get("aid"));
@@ -54,7 +53,7 @@ public class BaiduAdapter implements IAdvertiserAdapter {
 
     @Override
     public void uploadConversionData(Map<String, Object> conversionData, Map<String, Object> params) {
-        conversionData.put("logidUrl",params.get("logidUrl"));
+
     }
 }
 

+ 4 - 5
fs-service/src/main/java/com/fs/newAdv/integration/adapter/OPPOAdapter.java

@@ -29,10 +29,9 @@ public class OPPOAdapter implements IAdvertiserAdapter {
      */
     @Override
     public Lead adaptCallbackData(Map<String, String> rawData) {
-        log.info("腾讯数据适配:{}", rawData);
         Lead lead = new Lead();
-        lead.setTraceId(rawData.get("request_id"));
-        lead.setAdvertiserId(AdvertiserTypeEnum.TENCENT.getCode());
+        lead.setClickId(rawData.get("request_id"));
+        lead.setAdvertiserId(AdvertiserTypeEnum.OPPO.getCode());
         lead.setIp(rawData.get("ip"));
         if (StrUtil.isNotEmpty(rawData.get("dynamic_creative_id"))){
             lead.setIdeaId(rawData.get("dynamic_creative_id"));
@@ -50,7 +49,7 @@ public class OPPOAdapter implements IAdvertiserAdapter {
                     Instant.ofEpochMilli(Long.parseLong(rawData.get("click_time"))),
                     ZoneId.systemDefault()));
         } catch (Exception e) {
-            log.error("时间转换异常", e);
+            log.error("时间转换异常");
         }
         return lead;
     }
@@ -63,8 +62,8 @@ public class OPPOAdapter implements IAdvertiserAdapter {
     @Override
     public void uploadConversionData(Map<String, Object> conversionData, Map<String, Object> params) {
         // ------------------------------oppo参数----------
-        conversionData.put("tid", params.get("tid"));
         conversionData.put("lbid", params.get("lbid"));
+        conversionData.put("pageId", params.get("pageId"));
     }
 }
 

+ 2 - 3
fs-service/src/main/java/com/fs/newAdv/integration/adapter/OceanEngineAdapter.java

@@ -29,9 +29,8 @@ public class OceanEngineAdapter implements IAdvertiserAdapter {
      */
     @Override
     public Lead adaptCallbackData(Map<String, String> rawData) {
-        log.info("巨量引擎数据适配:{}", rawData);
         Lead lead = new Lead();
-        lead.setTraceId(rawData.get("track_id"));
+        lead.setClickId(rawData.get("track_id"));
         lead.setAdvertiserId(AdvertiserTypeEnum.OCEANENGINE.getCode());
         if (StrUtil.isNotEmpty(rawData.get("cid"))) {
             lead.setIdeaId(rawData.get("cid"));
@@ -52,7 +51,7 @@ public class OceanEngineAdapter implements IAdvertiserAdapter {
                     Instant.ofEpochMilli(Long.parseLong(rawData.get("ts"))),
                     ZoneId.systemDefault()));
         } catch (Exception e) {
-            log.error("时间转换异常", e);
+            log.error("时间转换异常");
         }
         return lead;
     }

+ 1 - 2
fs-service/src/main/java/com/fs/newAdv/integration/adapter/TencentAdapter.java

@@ -28,9 +28,8 @@ public class TencentAdapter implements IAdvertiserAdapter {
      */
     @Override
     public Lead adaptCallbackData(Map<String, String> rawData) {
-        log.info("腾讯数据适配:{}", rawData);
         Lead lead = new Lead();
-        lead.setTraceId(rawData.get("request_id"));
+        lead.setClickId(rawData.get("request_id"));
         lead.setAdvertiserId(AdvertiserTypeEnum.TENCENT.getCode());
         lead.setIp(rawData.get("ip"));
         if (StrUtil.isNotEmpty(rawData.get("dynamic_creative_id"))){

+ 1 - 2
fs-service/src/main/java/com/fs/newAdv/integration/adapter/VIVOAdapter.java

@@ -28,9 +28,8 @@ public class VIVOAdapter implements IAdvertiserAdapter {
      */
     @Override
     public Lead adaptCallbackData(Map<String, String> rawData) {
-        log.info("VIVO适配:{}", rawData);
         Lead lead = new Lead();
-        lead.setTraceId(rawData.get("requestId"));
+        lead.setClickId(rawData.get("requestId"));
         lead.setAdvertiserId(AdvertiserTypeEnum.VIVO.getCode());
         lead.setIp(rawData.get("ip"));
         lead.setIdeaId(rawData.get("creativeId"));

+ 16 - 9
fs-service/src/main/java/com/fs/newAdv/integration/client/AbstractApiClient.java

@@ -11,7 +11,6 @@ import com.fs.newAdv.service.IApiCallLogService;
 import com.fs.newAdv.service.IPromotionAccountService;
 import com.fs.newAdv.vo.AccessTokenVo;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.poi.ss.formula.functions.T;
 import org.springframework.beans.factory.annotation.Autowired;
 
 import java.time.LocalDateTime;
@@ -39,21 +38,29 @@ public abstract class AbstractApiClient implements IApiClient {
     protected boolean executeWithLog(AdvertiserTypeEnum advertiserType, String apiUrl, Map<String, Object> params, ApiCall action) {
         long start = System.currentTimeMillis();
         boolean callStatus = false;
-        String responseBody = "";
+        JSONObject jsonObject = null;
         try {
             log.info("[{}] 调用开始, 参数: {}", apiUrl, params);
             HttpResponse result = action.call();
-            JSONObject jsonObject = JSONUtil.parseObj(result.body());
-            Integer code = (Integer) jsonObject.get("code");
-            if (ObjectUtil.isNotEmpty(code) && (code == 0 || code == 200)) {
+            jsonObject = JSONUtil.parseObj(result.body());
+            // 通用状态
+            Integer code = jsonObject.getInt("code");
+            // 百度状态
+            Integer status = -1;
+            JSONObject header = jsonObject.getJSONObject("header");
+            if (ObjectUtil.isNotEmpty(header)) {
+                status = header.getInt("status");
+            }
+            if ((ObjectUtil.isNotEmpty(code) && (code == 0 || code == 200 ))
+            || (ObjectUtil.isNotEmpty(status) && status == 0)
+            ) {
                 callStatus = true;
             }
-            responseBody = JSONUtil.toJsonStr(jsonObject);
-            log.info("[{}] 调用成功, 耗时: {} ms, 返回结果: {}", apiUrl, System.currentTimeMillis() - start, result);
+            log.info("[{}] 调用成功, 耗时: {} ms, 返回结果: {}", apiUrl, System.currentTimeMillis() - start, jsonObject);
         } catch (Exception e) {
             log.error("[{}] 调用失败, 耗时: {} ms, 错误信息: {}", apiUrl, System.currentTimeMillis() - start, e.getMessage(), e);
         }
-        saveApiCallLog(advertiserType, apiUrl, params, responseBody, callStatus, start);
+        saveApiCallLog(advertiserType, apiUrl, params, jsonObject != null ? JSONUtil.toJsonStr(jsonObject) : null, callStatus, start);
         return callStatus;
     }
 
@@ -74,7 +81,7 @@ public abstract class AbstractApiClient implements IApiClient {
     protected String getAccessToken(Long promotionAccountId) {
         PromotionAccount byId = promotionAccountService.getById(promotionAccountId);
         // 判断token是否过期 提前1小时刷新
-        if (byId.getExpireTime().isBefore(LocalDateTime.now().plusHours(1))) {
+        if (ObjectUtil.isEmpty(byId.getExpireTime()) || byId.getExpireTime().isBefore(LocalDateTime.now().plusHours(1))) {
             // 获取请求参数
             IApiClient apiClient = advertiserHandlerFactory.getApiClient(AdvertiserTypeEnum.getByCode(byId.getAdvertiserId()));
             IAccessTokenClient tokenClient = (IAccessTokenClient) apiClient;

+ 1 - 1
fs-service/src/main/java/com/fs/newAdv/integration/client/advertiser/BaiduApiClient.java

@@ -85,7 +85,7 @@ public class BaiduApiClient extends AbstractApiClient implements IAccessTokenCli
         Map<String, Object> conversion = new HashMap<>();
 
         // 点击ID(必填)
-        String logidUrl = (String) conversionData.get("logidUrl");
+        String logidUrl = (String) conversionData.get("viewUrl");
         if (StrUtil.isBlank(logidUrl)) {
             throw new ThirdPartyException("落地页不能为空");
         }

+ 13 - 11
fs-service/src/main/java/com/fs/newAdv/integration/client/advertiser/OPPOApiClient.java

@@ -8,10 +8,13 @@ import com.fs.newAdv.domain.SiteStatistics;
 import com.fs.newAdv.enums.AdvertiserTypeEnum;
 import com.fs.newAdv.integration.client.AbstractApiClient;
 import com.fs.common.constant.SystemConstant;
+import com.google.common.hash.Hashing;
+import com.google.common.io.BaseEncoding;
 import lombok.extern.slf4j.Slf4j;
 import me.chanjar.weixin.common.util.crypto.SHA1;
 import org.springframework.stereotype.Component;
 
+import java.nio.charset.Charset;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -45,15 +48,15 @@ public class OPPOApiClient extends AbstractApiClient {
             // 构建请求参数
             Map<String, Object> requestBody = buildConversionParams(conversionData);
             Long timestamp = (Long) conversionData.get("timestamp");
-            String ownerId = (String) conversionData.get("ownerId");
+            String ownerId = (String) conversionData.get("adAccountId");
             String appId = (String) conversionData.get("appId");
-            String appKey = (String) conversionData.get("appKey");
-            String sign = SHA1.gen(appId + appKey + timestamp);
-            String token = Base64.encode(ownerId + "," + appId + "," + timestamp + "," + sign);
+            String appKey = (String) conversionData.get("appSecret");
+            String sign = Hashing.sha1().hashString(appId + appKey + timestamp, Charset.defaultCharset()).toString();
+            String token = BaseEncoding.base64().encode((ownerId + "," + appId + "," + timestamp + "," + sign).getBytes());
             // 发送HTTP请求
             return HttpRequest.post(CONVERSION_API_URL)
                     .header("Content-Type", "application/json")
-                    .header("Authorization", "Bearer  " + token)
+                    .header("Authorization", "Bearer " + token)
                     .body(JSONUtil.toJsonStr(requestBody))
                     .timeout(SystemConstant.API_TIMEOUT)
                     .execute();
@@ -70,18 +73,17 @@ public class OPPOApiClient extends AbstractApiClient {
         Map<String, Object> params = new HashMap<>();
 
         // 落地页Id:投放广告到投放
-        params.put("pageId", conversionData.get("traceId"));
+        params.put("pageId", conversionData.get("pageId"));
         // 广告主id:对应广告主自提供
-        params.put("adAccountId", conversionData.get("adAccountId"));
+        params.put("ownerId", conversionData.get("adAccountId"));
         // 用户IP:广告主收集
-        // params.put("ip", conversionData.get("ip"));
-        params.put("ip", "192.168.1.1");
+        params.put("ip", "127.0.0.1");
         // traceId:播放时追加在url上
-        params.put("tid", conversionData.get("tid"));
+        params.put("tid", conversionData.get("traceId"));
         // 流量号:播放时追加在URL上
         params.put("lbid ", conversionData.get("lbid"));
         // 事件
-        params.put("transformType ", conversionData.get("eventType"));
+        params.put("transformType", conversionData.get("eventType"));
         return params;
     }
 

+ 21 - 2
fs-service/src/main/java/com/fs/newAdv/integration/client/advertiser/OceanEngineApiClient.java

@@ -18,8 +18,11 @@ import com.fs.newAdv.vo.AccessTokenVo;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.http.client.utils.URIBuilder;
 import org.springframework.stereotype.Component;
+import org.springframework.web.util.UriComponentsBuilder;
 
 import java.math.BigDecimal;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
 import java.time.LocalDateTime;
 import java.util.Arrays;
 import java.util.Collections;
@@ -63,9 +66,16 @@ public class OceanEngineApiClient extends AbstractApiClient implements IAccessTo
         return executeWithLog(AdvertiserTypeEnum.OCEANENGINE, CONVERSION_API_URL, conversionData, () -> {
             // 构建请求参数
             Map<String, Object> requestBody = buildConversionParams(conversionData);
+/*            UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(CONVERSION_API_URL)
+                    .queryParam("callback", requestBody.get("traceId")) // 核心参数,监测链接透传的内容
+                    .queryParam("event_type", requestBody.get("event_type"))
+                    .queryParam("conv_time", requestBody.get("conv_time"));
+            URI uri = builder.build().encode(StandardCharsets.UTF_8).toUri();
+            log.info("准备调用巨量引擎回传接口, URL: {}", uri);*/
+
             // 发送HTTP请求
             return HttpRequest.get(CONVERSION_API_URL)
-                    .form(JSONUtil.toJsonStr(requestBody))
+                    .form(requestBody)
                     .timeout(SystemConstant.API_TIMEOUT)
                     .execute();
         });
@@ -94,6 +104,7 @@ public class OceanEngineApiClient extends AbstractApiClient implements IAccessTo
 
     @Override
     public SiteStatistics getDataReport(PromotionAccount account, String ideaId, String startDate, String endDate) {
+        log.info("开始获取头条报表数据:{} {} {} {}",account.getId(), ideaId,startDate,endDate);
         // 构建请求参数
         Map<String, Object> map = new HashMap<>();
         map.put("advertiser_id", Long.valueOf(account.getAdAccountId()));
@@ -142,8 +153,15 @@ public class OceanEngineApiClient extends AbstractApiClient implements IAccessTo
                 .header("Access-Token", getAccessToken(account.getId()))
                 .timeout(SystemConstant.API_TIMEOUT)
                 .execute();
-        JSONObject jsonObject = JSONUtil.parseObj(execute.body());
+        String body = execute.body();
+        log.info("头条报表数据返回:{} ",body);
+        JSONObject jsonObject = JSONUtil.parseObj(body);
         JSONObject data = jsonObject.getJSONObject("data");
+        int code = jsonObject.getInt("code");
+        if (code != 0){
+            log.error("头条报表数据返回异常:{} ",body);
+            return new SiteStatistics();
+        }
         JSONArray rows = data.getJSONArray("rows");
         JSONObject jsonObject1 = rows.getJSONObject(0);
         JSONObject jsonObject2 = jsonObject1.getJSONObject("metrics");
@@ -200,5 +218,6 @@ public class OceanEngineApiClient extends AbstractApiClient implements IAccessTo
                 .expireTime(LocalDateTime.now().plusSeconds(data.getLong("refresh_token_expires_in")))
                 .build();
     }
+
 }
 

+ 1 - 1
fs-service/src/main/java/com/fs/newAdv/integration/client/advertiser/VIVOApiClient.java

@@ -82,7 +82,7 @@ public class VIVOApiClient extends AbstractApiClient {
 
     @Override
     public AdvertiserTypeEnum getAdvertiserType() {
-        return AdvertiserTypeEnum.OPPO;
+        return AdvertiserTypeEnum.VIVO;
     }
 
     @Override

+ 20 - 6
fs-service/src/main/java/com/fs/newAdv/service/ILeadService.java

@@ -16,6 +16,8 @@ import java.util.concurrent.CompletableFuture;
 public interface ILeadService extends IService<Lead> {
 
     Lead getByTraceId(String traceId);
+    Lead getByClickId(String clickId);
+    Lead getByExternalId(Long externalId);
 
     /**
      * 用户加企业群线索处理
@@ -28,12 +30,17 @@ public interface ILeadService extends IService<Lead> {
 
     /**
      * 用户添加企业微信线索处理
-     * @param externalUserID
-     * @param userID
-     * @param corpId
-     * @param state
      */
-    CompletableFuture<String> updateAddMemberLead(String externalUserID, String userID, String corpId, String state);
+    void updateAddMemberLead(Long externalId, String unionId);
+    /**
+     * 用户添加企业微信线索处理
+     */
+    void updateAddMemberLead(String externalUserID,String userID,String corpId,String State);
+
+    /**
+     * 用户删除企业微信线索处理
+     */
+    void updateDeleteMemberLead(String externalUserID,String userID,String corpId);
     /**
      * 小程序授权线索处理
      * @param traceId
@@ -41,7 +48,14 @@ public interface ILeadService extends IService<Lead> {
      * @param mpOpenId
      * @param phone
      */
-    void weChatAuthorizationLead(String traceId, String unionId, String maOpenId, String phone);
+    void weChatAuthorizationLead(String traceId, String unionId, String maOpenId, String phone,Long userId);
+
+    /**
+     * 小程序获取头像昵称线索处理
+     * @param traceId
+     * @param weiChatName
+     */
+    void weChatNameLead(String traceId, String weiChatName);
 
     /**
      * 小程序授权落地页访问线索处理

+ 124 - 60
fs-service/src/main/java/com/fs/newAdv/service/impl/LeadServiceImpl.java

@@ -1,6 +1,7 @@
 package com.fs.newAdv.service.impl;
 
 import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -11,17 +12,14 @@ import com.fs.newAdv.mapper.LeadMapper;
 import com.fs.newAdv.service.ILeadService;
 import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.mapper.QwExternalContactMapper;
-import com.fs.qwApi.domain.QwExternalContactResult;
-import com.fs.qwApi.service.QwApiService;
+import com.fs.qw.service.IQwExternalContactService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
 import java.time.LocalDate;
-import java.util.Collections;
 import java.util.List;
-import java.util.concurrent.CompletableFuture;
 
 /**
  * 域名管理Service实现类
@@ -36,9 +34,9 @@ public class LeadServiceImpl extends ServiceImpl<LeadMapper, Lead> implements IL
 
     @Autowired
     private ConversionEventPublisher conversionEventPublisher;
-
     @Autowired
-    private QwApiService qwApiService;
+    private IQwExternalContactService qwExternalContactService;
+
     @Autowired
     private QwExternalContactMapper qwExternalContactMapper;
 
@@ -52,27 +50,50 @@ public class LeadServiceImpl extends ServiceImpl<LeadMapper, Lead> implements IL
         return list.isEmpty() ? null : list.get(0);
     }
 
+    @Override
+    public Lead getByClickId(String clickId) {
+        List<Lead> list = this.list(new LambdaQueryWrapper<Lead>()
+                .eq(Lead::getClickId, clickId));
+        if (list.size() > 1) {
+            log.error("查询投流点击信息不唯一:{}", clickId);
+        }
+        return list.isEmpty() ? null : list.get(0);
+    }
+
+    @Override
+    public Lead getByExternalId(Long externalId) {
+        List<Lead> list = this.list(new LambdaQueryWrapper<Lead>()
+                .eq(Lead::getExternalId, externalId));
+        if (list.size() > 1) {
+            log.error("查询投流外部联系人信息不唯一:{}", externalId);
+        }
+        return list.isEmpty() ? null : list.get(0);
+    }
+
     @Override
     @Async
-    public void updateGroupAddMemberLead(String userId, String chatId, String corpId, String unionid) {
+    public void updateGroupAddMemberLead(String name, String chatId, String corpId, String unionid) {
+        log.info("用户加群线索信息:{} {} {} {}", name, chatId, corpId, unionid);
+        LambdaQueryWrapper<Lead> last = new LambdaQueryWrapper<Lead>();
+        if (StrUtil.isNotEmpty(unionid)) {
+            last.eq(Lead::getUnionid, unionid);
+        } else if (StrUtil.isNotEmpty(name)) {
+            last.eq(Lead::getWeiChatName, name);
+        }
+        last.eq(Lead::getAddContactQw, 0).last("LIMIT 1");
         // 末次归因逻辑
-        Lead lead = this.getOne(new LambdaQueryWrapper<Lead>()
-                .eq(Lead::getUnionid, unionid)
-                .eq(Lead::getAddContactQw, 0)
-                .last("LIMIT 1"));
+        Lead lead = this.getOne(last);
         if (lead != null) {
-            lead.setCorpId(corpId);
             lead.setChatId(chatId);
-            lead.setUserId(userId);
             lead.setAddContactQw(1);
+            lead.setCorpId(corpId);
             this.updateById(lead);
-
             if (ObjectUtil.isNotEmpty(lead.getLandingPageTs()) && lead.getLandingPageTs().toLocalDate().isEqual(LocalDate.now())) {
                 // 当日加群事件回调
                 conversionEventPublisher.publishConversionEvent(lead.getTraceId(), SystemEventTypeEnum.GROUP_TODAY);
             }
         } else {
-            log.info("用户加群线索信息不存在:{}", userId);
+            log.info("用户加群线索信息不存在:{}", name);
         }
 
 
@@ -80,65 +101,108 @@ public class LeadServiceImpl extends ServiceImpl<LeadMapper, Lead> implements IL
 
     @Override
     @Async
-    public CompletableFuture<String> updateAddMemberLead(String externalUserID, String userID, String corpId, String state) {
-        QwExternalContactResult externalContactResult = qwApiService.getExternalcontact(externalUserID, corpId);
-        String unionid = externalContactResult.getExternal_contact().getUnionid();
+    public void updateAddMemberLead(Long externalId, String unionid) {
+        QwExternalContact qwExternalContact = qwExternalContactService.selectQwExternalContactById(externalId);
+        if (qwExternalContact == null) {
+            log.info("外部联系人信息不存在:{}", externalId);
+            return;
+        }
+        qwExternalContact.setUnionid(unionid);
+        this.updateAddMemberLead(qwExternalContact);
+    }
+
+    @Override
+    public void updateAddMemberLead(String externalUserID, String userID, String corpId, String State) {
+        QwExternalContact qwExternalContact = qwExternalContactMapper.selectQwExternalByExternalIdAndCompanyIdToIdAndFs(externalUserID, userID, corpId);
+        if (qwExternalContact == null) {
+            log.info("外部联系人信息不存在:{} {} {}", externalUserID, userID, corpId);
+            return;
+        }
+        this.updateAddMemberLead(qwExternalContact);
+    }
+
+    @Override
+    public void updateDeleteMemberLead(String externalUserID, String userID, String corpId) {
+        QwExternalContact qwExternalContact = qwExternalContactMapper.selectQwExternalByExternalIdAndCompanyIdToIdAndFs(externalUserID, userID, corpId);
+        if (qwExternalContact != null) {
+            Lead byExternalId = this.getByExternalId(qwExternalContact.getId());
+            if (ObjectUtil.isEmpty(byExternalId)) {
+                return;
+            }
+            log.info("用户删除微线索信息:{}", byExternalId.getTraceId());
+            Lead tempLead = new Lead();
+            tempLead.setId(byExternalId.getId());
+            tempLead.setWechatDelete(1);
+            this.updateById(tempLead);
+        }
+
+    }
+
+    private void updateAddMemberLead(QwExternalContact qwExternalContact) {
+        log.info("用户加微线索信息:{}", qwExternalContact);
+        LambdaQueryWrapper<Lead> last = new LambdaQueryWrapper<Lead>();
+        if (StrUtil.isNotEmpty(qwExternalContact.getUnionid())) {
+            last.eq(Lead::getUnionid, qwExternalContact.getUnionid());
+        } else if (StrUtil.isNotEmpty(qwExternalContact.getName())) {
+            last.eq(Lead::getWeiChatName, qwExternalContact.getName());
+        }
+        last.eq(Lead::getAddContactQwGroup, 0).last("LIMIT 1");
         // 末次归因逻辑
-        Lead lead = this.getOne(new LambdaQueryWrapper<Lead>()
-                .eq(Lead::getUnionid, unionid)
-                .eq(Lead::getAddContactQwGroup, 0)
-                .last("LIMIT 1"));
+        Lead lead = this.getOne(last);
         if (lead != null) {
-            lead.setExternalUserId(externalUserID);
-            lead.setCorpId(corpId);
-            lead.setUserId(userID);
+            lead.setExternalId(qwExternalContact.getId());
             lead.setAddContactQwGroup(1);
             this.updateById(lead);
-
             // 绑定企微用户线索关系
-            QwExternalContact qwExternalContact = qwExternalContactMapper.selectQwExternalByExternalIdAndCompanyIdToIdAndFs(externalUserID, userID, corpId);
-            if (qwExternalContact != null){
-                QwExternalContact temp = new QwExternalContact();
-                temp.setId(qwExternalContact.getId());
-                temp.setTraceId(lead.getTraceId());
-                qwExternalContactMapper.updateById(temp);
-            }else {
-                log.info("广告归因企微用户信息不存在:{} {} {} ", externalUserID, userID,corpId);
-            }
+            QwExternalContact temp = new QwExternalContact();
+            temp.setId(qwExternalContact.getId());
+            temp.setTraceId(lead.getTraceId());
+            qwExternalContactMapper.updateById(temp);
             if (ObjectUtil.isNotEmpty(lead.getLandingPageTs()) && lead.getLandingPageTs().toLocalDate().isEqual(LocalDate.now())) {
                 // 当日加微事件回调
                 conversionEventPublisher.publishConversionEvent(lead.getTraceId(), SystemEventTypeEnum.WEI_CHAT_TODAY);
             }
-            return CompletableFuture.completedFuture(lead.getTraceId());
+
         } else {
-            log.info("广告归因线索不存在:{} {} {}", externalUserID, userID,corpId);
+            log.info("广告归因线索不存在:{}", qwExternalContact);
+        }
+    }
+
+    @Override
+    @Async
+    public void weChatAuthorizationLead(String traceId, String unionId, String maOpenId, String phone, Long userId) {
+        log.info("用户微信授权线索信息:{} {} {} {} {}", traceId, unionId, phone, maOpenId, userId);
+        Lead byTraceId = this.getByTraceId(traceId);
+        if (byTraceId == null) {
+            return;
+        }
+        this.update(new LambdaUpdateWrapper<Lead>()
+                .eq(Lead::getTraceId, traceId)
+                .set(ObjectUtil.isNotEmpty(unionId), Lead::getUnionid, unionId)
+                .set(ObjectUtil.isNotEmpty(phone), Lead::getPhone, phone)
+                .set(ObjectUtil.isNotEmpty(maOpenId), Lead::getOpenid, maOpenId)
+                .set(ObjectUtil.isNotEmpty(userId), Lead::getSystemUserId, userId)
+                .set(Lead::getMiniAuth, 1));
+        if (ObjectUtil.isNotEmpty(byTraceId.getLandingPageTs()) && byTraceId.getLandingPageTs().toLocalDate().isEqual(LocalDate.now())) {
+            // 微信授权且当日创建事件
+            log.info("用户微信授权线索事件回传:{}", traceId);
+            conversionEventPublisher.publishConversionEvent(traceId, SystemEventTypeEnum.AUTH_TODAY_CREATE);
         }
-        return CompletableFuture.completedFuture(null);
     }
 
     @Override
     @Async
-    public void weChatAuthorizationLead(String traceId, String unionId, String maOpenId, String phone) {
-       try{
-           log.info("用户微信授权线索信息:{}", traceId);
-           Lead byTraceId = this.getByTraceId(traceId);
-           if (byTraceId == null) {
-               return;
-           }
-           this.update(new LambdaUpdateWrapper<Lead>()
-                   .eq(Lead::getTraceId, traceId)
-                   .set(ObjectUtil.isNotEmpty(unionId),Lead::getUnionid, unionId)
-                   .set(ObjectUtil.isNotEmpty(phone),Lead::getPhone, phone)
-                   .set(ObjectUtil.isNotEmpty(maOpenId),Lead::getOpenid, maOpenId)
-                   .set(Lead::getMiniAuth, 1));
-           if (ObjectUtil.isNotEmpty(byTraceId.getLandingPageTs()) && byTraceId.getLandingPageTs().toLocalDate().isEqual(LocalDate.now())) {
-               // 微信授权且当日创建事件
-               log.info("用户微信授权线索事件回传:{}", traceId);
-               conversionEventPublisher.publishConversionEvent(traceId, SystemEventTypeEnum.AUTH_TODAY_CREATE);
-           }
-       }catch (Exception e){
-           e.printStackTrace();
-       }
+    public void weChatNameLead(String traceId, String weiChatName) {
+        log.info("用户微信昵称线索信息:{} {}", traceId, weiChatName);
+        Lead byTraceId = this.getByTraceId(traceId);
+        if (byTraceId == null || StrUtil.isEmpty(weiChatName)) {
+            return;
+        }
+        this.update(new LambdaUpdateWrapper<Lead>()
+                .eq(Lead::getTraceId, traceId)
+                .isNull(Lead::getWeiChatName)
+                .set(Lead::getWeiChatName, weiChatName)
+        );
     }
 
     @Override
@@ -161,7 +225,7 @@ public class LeadServiceImpl extends ServiceImpl<LeadMapper, Lead> implements IL
         boolean update = this.update(new LambdaUpdateWrapper<Lead>()
                 .eq(Lead::getTraceId, traceId)
                 .set(Lead::getMiniLaunchIndexCount, 1));
-        if (!update){
+        if (!update) {
             log.error("发起进入小程序失败:{}", traceId);
         }
     }

+ 1 - 1
fs-service/src/main/java/com/fs/newAdv/service/impl/SiteServiceImpl.java

@@ -25,7 +25,7 @@ public class SiteServiceImpl extends ServiceImpl<SiteMapper, Site> implements IS
     @Transactional(rollbackFor = Exception.class)
     public void createSite(Site site) {
         this.save(site);
-        site.setSiteUrl("https://" + site.getLaunchDomain() + "/#/pages/index/index?siteId=" + site.getId());
+        site.setSiteUrl("https://" + site.getLaunchDomain() + "/pages/index/index?siteId=" + site.getId());
         this.updateById(site);
     }
 

+ 12 - 0
fs-service/src/main/java/com/fs/newAdv/vo/ConversionParmVo.java

@@ -0,0 +1,12 @@
+package com.fs.newAdv.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 回传通用参数类
+ */
+@Data
+public class ConversionParmVo implements Serializable {
+}

+ 19 - 5
fs-service/src/main/java/com/fs/qw/service/impl/CustomerTransferApprovalServiceImpl.java

@@ -186,13 +186,27 @@ public class CustomerTransferApprovalServiceImpl implements ICustomerTransferApp
 
     private String safeFormat(String format, Object... args) {
         try {
-            Object[] safeArgs = Arrays.stream(args)
-                    .map(arg -> Objects.toString(arg, ""))
-                    .toArray();
-            return String.format(format, safeArgs);
+            // 尝试直接格式化
+            return String.format(format, args);
         } catch (Exception e) {
             log.warn("字符串格式化异常: format={}, args={}", format, Arrays.toString(args), e);
-            return Arrays.toString(args);
+
+            // 尝试将所有参数转为字符串再格式化
+            try {
+                Object[] stringArgs = Arrays.stream(args)
+                        .map(arg -> arg == null ? "null" : arg.toString())
+                        .toArray();
+                // 将 %d 替换为 %s
+                String safeFormat = format.replace("%d", "%s");
+                return String.format(safeFormat, stringArgs);
+            } catch (Exception ex) {
+                // 如果还是失败,返回最简单的格式
+                StringBuilder result = new StringBuilder(format);
+                for (Object arg : args) {
+                    result.append(" ").append(arg);
+                }
+                return result.toString();
+            }
         }
     }
 

+ 43 - 1
fs-service/src/main/resources/mapper/course/FsCourseAnswerLogsMapper.xml

@@ -33,6 +33,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="companyUserId != null "> and company_user_id = #{companyUserId}</if>
             <if test="companyId != null "> and company_id = #{companyId}</if>
             <if test="qwUserId != null "> and qw_user_id = #{qwUserId}</if>
+            <if test="beginTime != null "> and create_time &gt;= #{beginTime}</if>
+            <if test="endTime != null "> and create_time &lt;= #{endTime}</if>
         </where>
     </select>
 
@@ -213,5 +215,45 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             and period_id = #{periodId}
         </if>
     </select>
-
+    <insert id="batchInsert" parameterType="java.util.List">
+        insert into fs_course_answer_logs_1
+        (
+        log_id,
+        user_id,
+        video_id,
+        is_right,
+        create_time,
+        company_id,
+        company_user_id,
+        qw_user_id,
+        course_id,
+        question_json,
+        watch_log_id,
+        period_id
+        )
+        VALUES
+        <foreach collection="list" item="log" separator=",">
+            (
+            #{log.logId},
+            #{log.userId},
+            #{log.videoId},
+            #{log.isRight},
+            #{log.createTime},
+            #{log.companyId},
+            #{log.companyUserId},
+            #{log.qwUserId},
+            #{log.courseId},
+            #{log.questionJson},
+            #{log.watchLogId},
+            #{log.periodId}
+            )
+        </foreach>
+    </insert>
+    <delete id="batchDeleteByIds" parameterType="java.util.List">
+        DELETE FROM fs_course_answer_logs
+        WHERE log_id IN
+        <foreach collection="ids" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
 </mapper>

+ 64 - 0
fs-service/src/main/resources/mapper/course/FsCourseRedPacketLogMapper.xml

@@ -41,6 +41,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="amount != null "> and amount = #{amount}</if>
             <if test="status != null "> and `status` = #{status}</if>
             <if test="qwUserId != null  and qwUserId != ''"> and qw_user_id = #{qwUserId}</if>
+            <if test="beginTime != null "> and create_time &gt;= #{beginTime}</if>
+            <if test="endTime != null "> and create_time &lt;= #{endTime}</if>
         </where>
     </select>
 
@@ -216,4 +218,66 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <include refid="selectFsCourseRedPacketLogVo"/> where status != 1 and create_time &gt;= #{maps.startTime} and create_time &lt;= #{maps.endTime}
 
     </select>
+
+
+    <insert id="batchInsert" parameterType="java.util.List">
+        insert into fs_course_red_packet_log_1
+        (
+        log_id,
+        course_id,
+        user_id,
+        video_id,
+        company_user_id,
+        company_id,
+        amount,
+        create_time,
+        qw_user_id,
+        out_batch_no,
+        status,
+        update_time,
+        watch_log_id,
+        remark,
+        period_id,
+        result,
+        batch_id,
+        app_id,
+        acc_balance_before,
+        acc_balance_after,
+        mch_id
+        )
+        VALUES
+        <foreach collection="list" item="log" separator=",">
+            (
+            #{log.logId},
+            #{log.courseId},
+            #{log.userId},
+            #{log.videoId},
+            #{log.companyUserId},
+            #{log.companyId},
+            #{log.amount},
+            #{log.createTime},
+            #{log.qwUserId},
+            #{log.outBatchNo},
+            #{log.status},
+            #{log.updateTime},
+            #{log.watchLogId},
+            #{log.remark},
+            #{log.periodId},
+            #{log.result},
+            #{log.batchId},
+            #{log.appId},
+            #{log.accBalanceBefore},
+            #{log.accBalanceAfter},
+            #{log.mchId}
+            )
+        </foreach>
+    </insert>
+
+    <delete id="batchDeleteByIds" parameterType="java.util.List">
+        DELETE FROM fs_course_red_packet_log
+        WHERE log_id IN
+        <foreach collection="ids" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
 </mapper>

+ 21 - 9
fs-service/src/main/resources/mapper/course/FsCourseTrafficLogMapper.xml

@@ -137,6 +137,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="companyId != null">company_id,</if>
             <if test="courseId != null">course_id,</if>
             <if test="uuId != null">uu_id,</if>
+            <if test="project != null">project,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="userId != null">#{userId},</if>
@@ -149,6 +150,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="companyId != null">#{companyId},</if>
             <if test="courseId != null">#{courseId},</if>
             <if test="uuId != null">#{uuId},</if>
+            <if test="project != null">#{project},</if>
          </trim>
     </insert>
 
@@ -165,6 +167,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="companyId != null">company_id,</if>
             <if test="courseId != null">course_id,</if>
             <if test="uuId != null">uu_id,</if>
+            <if test="project != null">project,</if>
         </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="userId != null">#{userId},</if>
@@ -177,6 +180,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="companyId != null">#{companyId},</if>
             <if test="courseId != null">#{courseId},</if>
             <if test="uuId != null">#{uuId},</if>
+            <if test="project != null">#{project},</if>
         </trim>
         on duplicate key update
         <trim suffixOverrides=",">
@@ -247,26 +251,34 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </insert>
 
     <select id="selectTrafficNew" resultType="com.fs.course.vo.FsCourseTrafficLogListVO">
-        select company_id,project,course_id,SUM(internet_traffic) AS total_internet_traffic
-        ,DATE_FORMAT(create_time, '%Y-%m-%d') AS `month`  from fs_course_traffic_log
+        select a.company_id,
+        <if test="tabType!=null and tabType=='project'">
+            b.project,
+        </if>
+
+        a.course_id,SUM(a.internet_traffic) AS total_internet_traffic
+        ,DATE_FORMAT(a.create_time, '%Y-%m-%d') AS `month`  from fs_course_traffic_log a
+        <if test="tabType!=null and tabType=='project'">
+            left join fs_user_course b on a.course_id = b.course_id
+        </if>
         <where>
             <if test="startDate != null and endDate != null">
-                and DATE_FORMAT(create_time, '%Y-%m-%d') between #{startDate} AND #{endDate}
+                and DATE_FORMAT(a.create_time, '%Y-%m-%d') between #{startDate} AND #{endDate}
             </if>
             <if test="companyId !=null">
-                and company_id = #{companyId}
+                and a.company_id = #{companyId}
             </if>
             <if test="courseId != null">
-                and course_id = ${courseId}
+                and a.course_id = ${courseId}
             </if>
             <if test="project != null">
-                and project = ${project}
+                and b.project = ${project}
             </if>
             <if test="common == null">
-                AND company_id IS not NULL
+                AND a.company_id IS not NULL
             </if>
             <if test="common != null ">
-                AND company_id IS NULL
+                AND a.company_id IS NULL
             </if>
         </where>
 
@@ -274,7 +286,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             group by company_id,`month`,course_id,project
         </if>
         <if test="tabType!=null and tabType=='project'">
-            group by project,`month`
+            group by b.project,`month`
         </if>
         <if test="tabType!=null and tabType=='course'">
             group by course_id,`month`

+ 64 - 0
fs-service/src/main/resources/mapper/course/FsCourseWatchLogMapper.xml

@@ -51,6 +51,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="campPeriodTime != null "> and camp_period_time = #{campPeriodTime}</if>
             <if test="project != null "> and project = #{project}</if>
             <if test="watchType != null "> and watch_type = #{watchType}</if>
+            <if test="beginTime != null "> and create_time &gt;= #{beginTime}</if>
+            <if test="endTime != null "> and create_time &lt;= #{endTime}</if>
         </where>
     </select>
 
@@ -1120,4 +1122,66 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         DATE(o.create_time)
         ) AS t
     </select>
+
+
+
+
+    <insert id="batchInsert" parameterType="java.util.List">
+        INSERT INTO fs_course_watch_log_1 (
+        log_id,
+        user_id,
+        video_id,
+        log_type,
+        create_time,
+        update_time,
+        qw_external_contact_id,
+        duration,
+        qw_user_id,
+        company_user_id,
+        company_id,
+        course_id,
+        send_type,
+        reward_type,
+        sop_id,
+        camp_period_time,
+        project,
+        period_id,
+        im_msg_send_detail_id,
+        watch_type
+        )
+        VALUES
+        <foreach collection="list" item="log" separator=",">
+            (
+            #{log.logId},  <!-- 🔴 如果需要保留原id -->
+            #{log.userId},
+            #{log.videoId},
+            #{log.logType},
+            #{log.createTime},
+            #{log.updateTime},
+            #{log.qwExternalContactId},
+            #{log.duration},
+            #{log.qwUserId},
+            #{log.companyUserId},
+            #{log.companyId},
+            #{log.courseId},
+            #{log.sendType},
+            #{log.rewardType},
+            #{log.sopId},
+            #{log.campPeriodTime},
+            #{log.project},
+            #{log.periodId},
+            #{log.imMsgSendDetailId},
+            #{log.watchType}
+            )
+        </foreach>
+    </insert>
+
+
+    <delete id="batchDeleteByIds" parameterType="java.util.List">
+        DELETE FROM fs_course_watch_log
+        WHERE log_id IN
+        <foreach collection="ids" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
 </mapper>

+ 1 - 0
fs-service/src/main/resources/mapper/live/LiveAfterSalesMapper.xml

@@ -80,6 +80,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
         <where>
             <if test="hfOrderCode != null and hfOrderCode != ''"> and lop.pay_code = #{hfOrderCode}</if>
+            <if test="bankTransactionId != null and bankTransactionId != ''"> and lop.bank_transaction_id = #{bankTransactionId}</if>
             <if test="liveId != null and liveId != ''"> and las.live_id = #{liveId}</if>
             <if test="companyUserNickName != null and companyUserNickName != ''"> and cu.nick_name like concat(#{companyUserNickName},'%')</if>
             <if test="storeId != null and storeId != ''"> and las.store_id = #{storeId}</if>

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

@@ -25,6 +25,7 @@ import com.fs.his.service.IFsIntegralGoodsService;
 import com.fs.sop.domain.QwSop;
 import com.fs.sop.service.IQwSopService;
 import com.fs.system.service.ISysConfigService;
+import com.fs.voice.utils.StringUtil;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 import com.hc.openapi.tool.fastjson.JSON;
@@ -237,10 +238,15 @@ public class CourseQwController extends AppBaseController {
     @PostMapping("/sendReward")
     @RepeatSubmit
     @UserOperationLog(operationType = FsUserOperationEnum.SENDREWARD)
-    public R sendReward(@RequestBody @Valid FsCourseSendRewardUParam param)
+    public R sendReward(@RequestBody FsCourseSendRewardUParam param)
     {
         param.setUserId(Long.parseLong(getUserId()));
         logger.info("【发放奖励】3:{}",param);
+
+        if ( param.getSource()!=3  && StringUtil.strIsNullOrEmpty(param.getAppId())){
+            return R.error("appId不能为空");
+        }
+
         return courseVideoService.sendReward(param);
     }
 
@@ -486,5 +492,13 @@ public class CourseQwController extends AppBaseController {
         fastgptEventLogTotalService.eventLogTotals(startTime,endTime);
         return R.ok();
     }
+    @Login
+    @ApiOperation("注册链接接口")
+    @PostMapping("/registerQwFsUser")
+    public R registerQwFsUser(@RequestBody FsUserCourseVideoAddKfUParam param) {
+        Long userId = Long.parseLong(getUserId());
+        param.setUserId(userId);
+        return courseVideoService.registerQwFsUser(param);
+    }
 
 }

+ 134 - 94
fs-user-app/src/main/java/com/fs/app/controller/live/LiveCompletionPointsController.java

@@ -16,7 +16,12 @@ import com.fs.live.domain.LiveCompletionPointsRecord;
 import com.fs.live.mapper.LiveCompletionPointsRecordMapper;
 import com.fs.live.service.ILiveCompletionPointsRecordService;
 import com.fs.live.service.ILiveService;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.*;
 
 import java.math.BigDecimal;
@@ -27,6 +32,7 @@ import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
 
 /**
  * 直播完课积分Controller
@@ -35,6 +41,8 @@ import java.util.Map;
 @RequestMapping("/app/live/completion")
 public class LiveCompletionPointsController extends AppBaseController {
 
+    private static final Logger logger = LoggerFactory.getLogger(LiveCompletionPointsController.class);
+
     @Autowired
     private ILiveCompletionPointsRecordService completionPointsRecordService;
 
@@ -50,6 +58,9 @@ public class LiveCompletionPointsController extends AppBaseController {
     @Autowired
     private LiveCompletionPointsRecordMapper completionPointsRecordMapper;
 
+    @Autowired
+    private RedissonClient redissonClient;
+
     /**
      * 领取完课积分
      */
@@ -87,20 +98,20 @@ public class LiveCompletionPointsController extends AppBaseController {
     @GetMapping("/info")
     public R getInfo(@RequestParam Long liveId) {
         Long userId = Long.parseLong(getUserId());
-        
+
         // 1. 获取用户积分余额
         FsUser user = fsUserService.selectFsUserByUserId(userId);
         Long integral = user != null && user.getIntegral() != null ? user.getIntegral() : 0L;
-        
+
         // 2. 获取完课记录列表(包含已领取和未领取)
         List<LiveCompletionPointsRecord> records = completionPointsRecordService.getUserRecords(liveId, userId);
-        
+
         // 3. 统计信息
         long totalPoints = records.stream()
                 .filter(r -> r.getReceiveStatus() == 1)
                 .mapToLong(LiveCompletionPointsRecord::getPointsAwarded)
                 .sum();
-        
+
         long unreceivedCount = records.stream()
                 .filter(r -> r.getReceiveStatus() == 0)
                 .count();
@@ -111,7 +122,7 @@ public class LiveCompletionPointsController extends AppBaseController {
         result.put("totalDays", records.size());  // 累计看直播天数
         result.put("unreceivedCount", unreceivedCount);  // 未领取记录数
         result.put("records", records);  // 完课记录列表
-        
+
         return R.ok().put("data", result);
     }
 
@@ -121,7 +132,7 @@ public class LiveCompletionPointsController extends AppBaseController {
     @PostMapping("/test/create")
     public R testCreateRecord(@RequestParam Long liveId, @RequestParam(required = false) Long watchDuration) {
         Long userId = Long.parseLong(getUserId());
-        
+
         try {
             // 调用完课记录创建方法(watchDuration为null时会自动从数据库累计)
             completionPointsRecordService.checkAndCreateCompletionRecord(liveId, userId, watchDuration);
@@ -139,7 +150,7 @@ public class LiveCompletionPointsController extends AppBaseController {
     @GetMapping("/remaining-time")
     public R getRemainingTime(@RequestParam Long liveId) {
         Long userId = Long.parseLong(getUserId());
-        
+
         try {
             // 1. 获取直播间信息
             Live live = liveService.selectLiveByLiveId(liveId);
@@ -149,20 +160,18 @@ public class LiveCompletionPointsController extends AppBaseController {
 
             // 2. 查询当前用户和当前直播间的最近一次完课记录(不限制日期)
             LiveCompletionPointsRecord record = completionPointsRecordMapper.selectLatestByUserAndLiveId(liveId, userId);
-            
+
             // 3. 如果没有记录,查询直播间配置并生成记录
             if (record == null) {
-                completionPointsRecordService.checkAndCreateCompletionRecord(liveId, userId, null);
-                // 重新查询
-                record = completionPointsRecordMapper.selectLatestByUserAndLiveId(liveId, userId);
+                record = completionPointsRecordService.createCompletionRecord(liveId, userId);
             }
 
             // 4. 计算剩余时长
             RemainingTimeVO vo = new RemainingTimeVO();
             Long videoDuration = live.getDuration() != null ? live.getDuration() : 0L;
-            Long watchDuration = record != null && record.getWatchDuration() != null 
+            Long watchDuration = record != null && record.getWatchDuration() != null
                     ? record.getWatchDuration() : 0L;
-            
+
             vo.setVideoDuration(videoDuration);
             if (record != null) {
                 vo.setCompletionRate(record.getCompletionRate());
@@ -170,7 +179,7 @@ public class LiveCompletionPointsController extends AppBaseController {
             vo.setWatchDuration(watchDuration);
             vo.setRemainingTime(Math.max(0, videoDuration - watchDuration));
             vo.setHasReceived(record != null && record.getReceiveStatus() != null && record.getReceiveStatus() == 1);
-            
+
             return R.ok().put("data", vo);
         } catch (Exception e) {
             return R.error("查询失败: " + e.getMessage());
@@ -183,9 +192,10 @@ public class LiveCompletionPointsController extends AppBaseController {
      * 更新用户看课completionPointsRecordService看课记录里面的时长
      */
     @PostMapping("/update-watch-duration")
+    @Transactional
     public R updateWatchDuration(@RequestParam Long liveId, @RequestParam Long watchDuration) {
         Long userId = Long.parseLong(getUserId());
-        
+
         try {
             // 1. 获取直播间信息
             Live live = liveService.selectLiveByLiveId(liveId);
@@ -196,7 +206,7 @@ public class LiveCompletionPointsController extends AppBaseController {
             // 2. 判断当前时间是否在直播期间(状态为2,直播中)
             boolean isLiveInProgress = false;
             LocalDateTime now = LocalDateTime.now();
-            
+
             if (live.getStatus() != null && live.getStatus() == 2) {
                 // status=2 表示直播中
                 isLiveInProgress = true;
@@ -212,20 +222,20 @@ public class LiveCompletionPointsController extends AppBaseController {
 
             // 3. 查询当前直播间的完课记录(不限制日期)
             LiveCompletionPointsRecord record = completionPointsRecordMapper.selectLatestByUserAndLiveId(liveId, userId);
-            
+
             // 4. 计算看课时长
             Date updateTime = null;
             if (record != null && record.getUpdateTime() != null) {
                 updateTime = record.getUpdateTime();
             }
-            
+
             // 判断更新时间与直播间开始时间的关系
-            Date startTime = live.getStartTime() != null 
+            Date startTime = live.getStartTime() != null
                     ? java.sql.Timestamp.valueOf(live.getStartTime()) : null;
-            
+
             Date currentTime = new Date();
             long timeDiff = 0L;
-            
+
             if (updateTime != null && startTime != null) {
                 if (updateTime.before(startTime)) {
                     // 更新时间小于直播间开始时间,使用直播间开始时间进行计算
@@ -238,7 +248,7 @@ public class LiveCompletionPointsController extends AppBaseController {
                 // 没有更新记录,使用直播间开始时间计算
                 timeDiff = (currentTime.getTime() - startTime.getTime()) / 1000; // 转换为秒
             }
-            
+
             // 5. 如果请求传入的时间大于这个时间差,就使用计算出的看课时长,否则使用请求传入的时长
             Long finalWatchDuration;
             if (watchDuration > timeDiff) {
@@ -248,18 +258,19 @@ public class LiveCompletionPointsController extends AppBaseController {
                 // 否则使用请求传入的时长
                 finalWatchDuration = watchDuration;
             }
-            
+
             // 6. 更新完课记录中的看课时长
             if (record == null) {
                 // 如果没有记录,先创建记录
-                completionPointsRecordService.checkAndCreateCompletionRecord(liveId, userId, finalWatchDuration);
-                record = completionPointsRecordMapper.selectLatestByUserAndLiveId(liveId, userId);
+                record = completionPointsRecordService.createCompletionRecord(liveId, userId);
+                record.setWatchDuration(finalWatchDuration);
+                completionPointsRecordMapper.updateRecord(record);
             } else {
                 // 更新现有记录的看课时长
-                Long currentWatchDuration = record.getWatchDuration() != null 
+                Long currentWatchDuration = record.getWatchDuration() != null
                         ? record.getWatchDuration() : 0L;
                 record.setWatchDuration(currentWatchDuration + finalWatchDuration);
-                
+
                 // 重新计算完课比例
                 Long videoDuration = live.getDuration();
                 if (videoDuration != null && videoDuration > 0) {
@@ -271,15 +282,18 @@ public class LiveCompletionPointsController extends AppBaseController {
                     }
                     record.setCompletionRate(completionRate);
                 }
-                
-                completionPointsRecordMapper.updateRecord(record);
+
+                int updateResult = completionPointsRecordMapper.updateRecord(record);
+                if (updateResult <= 0) {
+                    return R.error("更新看课时间失败");
+                }
             }
 
             UpdateWatchDurationVO vo = new UpdateWatchDurationVO();
             vo.setWatchDuration(finalWatchDuration);
-            vo.setTotalWatchDuration(record != null && record.getWatchDuration() != null 
+            vo.setTotalWatchDuration(record != null && record.getWatchDuration() != null
                     ? record.getWatchDuration() : finalWatchDuration);
-            
+
             return R.ok().put("data", vo);
         } catch (Exception e) {
             return R.error("更新失败: " + e.getMessage());
@@ -294,81 +308,107 @@ public class LiveCompletionPointsController extends AppBaseController {
      * 没达到,返回报错
      */
     @PostMapping("/receive-points")
-    @RepeatSubmit
     public R receivePoints(@RequestParam Long liveId) {
         Long userId = Long.parseLong(getUserId());
-        
+
+        // 创建唯一锁,确保同一个 liveId 和 userId 只能有一个线程在执行
+        String lockKey = String.format("receivePoints:liveId:%d:userId:%d", liveId, userId);
+        RLock lock = redissonClient.getLock(lockKey);
+
         try {
-            // 1. 查询当前用户和当前直播间的最近一次完课记录(不限制日期)
-            LiveCompletionPointsRecord record = completionPointsRecordMapper.selectLatestByUserAndLiveId(liveId, userId);
-            
-            if (record == null) {
-                return R.error("您还没有看课记录,无法领取积分");
+            // 尝试获取锁,等待时间0秒,锁持有时间15秒
+            boolean locked = lock.tryLock(0, 15, TimeUnit.SECONDS);
+            if (!locked) {
+                logger.warn("获取领取积分锁失败,liveId: {}, userId: {}", liveId, userId);
+                return R.error("系统繁忙,请稍后重试");
             }
 
-            // 2. 获取直播间信息和配置
-            Live live = liveService.selectLiveByLiveId(liveId);
-            if (live == null) {
-                return R.error("直播间不存在");
-            }
+            try {
+                // 1. 查询当前用户和当前直播间的最近一次完课记录(不限制日期)
+                LiveCompletionPointsRecord record = completionPointsRecordMapper.selectLatestByUserAndLiveId(liveId, userId);
 
-            // 3. 检查看课记录里面的时长是否达到完课标准
-            Long watchDuration = record.getWatchDuration();
-            if (watchDuration == null || watchDuration <= 0) {
-                return R.error("您的看课时长不足,无法领取积分");
-            }
+                if (record == null) {
+                    return R.error("您还没有看课记录,无法领取积分");
+                }
 
-            // 4. 检查完课比例是否达到标准
-            BigDecimal completionRate = record.getCompletionRate();
-            if (completionRate == null) {
-                // 重新计算完课比例
-                Long videoDuration = live.getDuration();
-                if (videoDuration == null || videoDuration <= 0) {
-                    return R.error("直播间视频时长配置错误");
+                // 2. 获取直播间信息和配置
+                Live live = liveService.selectLiveByLiveId(liveId);
+                if (live == null) {
+                    return R.error("直播间不存在");
                 }
-                completionRate = BigDecimal.valueOf(watchDuration)
-                        .multiply(BigDecimal.valueOf(100))
-                        .divide(BigDecimal.valueOf(videoDuration), 2, java.math.RoundingMode.HALF_UP);
-                if (completionRate.compareTo(BigDecimal.valueOf(100)) > 0) {
-                    completionRate = BigDecimal.valueOf(100);
+
+                // 3. 检查看课记录里面的时长是否达到完课标准
+                Long watchDuration = record.getWatchDuration();
+                if (watchDuration == null || watchDuration <= 0) {
+                    return R.error("您的看课时长不足,无法领取积分");
                 }
-                record.setCompletionRate(completionRate);
-            }
 
-            // 5. 从直播间配置获取完课标准
-            String configJson = live.getConfigJson();
-            Integer requiredCompletionRate = null;
-            if (configJson != null && !configJson.isEmpty()) {
-                try {
-                    com.alibaba.fastjson.JSONObject jsonConfig = com.alibaba.fastjson.JSON.parseObject(configJson);
-                    requiredCompletionRate = jsonConfig.getInteger("completionRate");
-                } catch (Exception e) {
-                    // 解析失败,忽略
+                // 4. 检查完课比例是否达到标准
+                BigDecimal completionRate = record.getCompletionRate();
+                if (completionRate == null) {
+                    // 重新计算完课比例
+                    Long videoDuration = live.getDuration();
+                    if (videoDuration == null || videoDuration <= 0) {
+                        return R.error("直播间视频时长配置错误");
+                    }
+                    completionRate = BigDecimal.valueOf(watchDuration)
+                            .multiply(BigDecimal.valueOf(100))
+                            .divide(BigDecimal.valueOf(videoDuration), 2, java.math.RoundingMode.HALF_UP);
+                    if (completionRate.compareTo(BigDecimal.valueOf(100)) > 0) {
+                        completionRate = BigDecimal.valueOf(100);
+                    }
+                    record.setCompletionRate(completionRate);
                 }
-            }
 
-            // 6. 判断是否达到完课标准
-            if (requiredCompletionRate != null && completionRate.compareTo(BigDecimal.valueOf(requiredCompletionRate)) < 0) {
-                return R.error("您的完课比例未达到标准(" + requiredCompletionRate + "%),当前完课比例:" + completionRate + "%");
-            }
+                // 5. 从直播间配置获取完课标准
+                String configJson = live.getConfigJson();
+                Integer requiredCompletionRate = null;
+                if (configJson != null && !configJson.isEmpty()) {
+                    try {
+                        com.alibaba.fastjson.JSONObject jsonConfig = com.alibaba.fastjson.JSON.parseObject(configJson);
+                        requiredCompletionRate = jsonConfig.getInteger("completionRate");
+                    } catch (Exception e) {
+                        // 解析失败,忽略
+                    }
+                }
 
-            // 7. 检查是否已领取
-            if (record.getReceiveStatus() != null && record.getReceiveStatus() == 1) {
-                return R.error("该完课积分已领取");
-            }
+                // 6. 判断是否达到完课标准
+                if (requiredCompletionRate != null && completionRate.compareTo(BigDecimal.valueOf(requiredCompletionRate)) < 0) {
+                    return R.error("您的完课比例未达到标准(" + requiredCompletionRate + "%),当前完课比例:" + completionRate + "%");
+                }
 
-            // 8. 领取积分(更新看课记录的领取状态,给用户加积分)
-            LiveCompletionPointsRecord receivedRecord = completionPointsRecordService.receiveCompletionPoints(record.getId(), userId);
+                // 7. 检查是否已领取
+                if (record.getReceiveStatus() != null && record.getReceiveStatus() == 1) {
+                    return R.error("该完课积分已领取");
+                }
 
-            ReceivePointsVO vo = new ReceivePointsVO();
-            vo.setRecord(receivedRecord);
-            vo.setPoints(receivedRecord.getPointsAwarded());
-            vo.setContinuousDays(receivedRecord.getContinuousDays());
-            
-            return R.ok().put("data", vo);
-        } catch (BaseException e) {
-            return R.error(e.getMessage());
+                // 8. 领取积分(更新看课记录的领取状态,给用户加积分)
+                LiveCompletionPointsRecord receivedRecord = completionPointsRecordService.receiveCompletionPoints(record.getId(), userId);
+
+                ReceivePointsVO vo = new ReceivePointsVO();
+                vo.setRecord(receivedRecord);
+                vo.setPoints(receivedRecord.getPointsAwarded());
+                vo.setContinuousDays(receivedRecord.getContinuousDays());
+
+                return R.ok().put("data", vo);
+            } catch (BaseException e) {
+                return R.error(e.getMessage());
+            } catch (Exception e) {
+                logger.error("领取积分失败,liveId: {}, userId: {}", liveId, userId, e);
+                return R.error("领取失败: " + e.getMessage());
+            } finally {
+                // 释放锁
+                if (lock.isHeldByCurrentThread()) {
+                    lock.unlock();
+                    logger.debug("领取积分锁已释放,liveId: {}, userId: {}", liveId, userId);
+                }
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            logger.error("获取领取积分锁被中断,liveId: {}, userId: {}", liveId, userId, e);
+            return R.error("系统繁忙,请稍后重试");
         } catch (Exception e) {
+            logger.error("领取积分异常,liveId: {}, userId: {}", liveId, userId, e);
             return R.error("领取失败: " + e.getMessage());
         }
     }
@@ -380,13 +420,13 @@ public class LiveCompletionPointsController extends AppBaseController {
     @GetMapping("/integral-logs")
     public R getIntegralLogs(@RequestParam(required = false) Integer type) {
         Long userId = Long.parseLong(getUserId());
-        
+
         try {
             FsUserIntegralLogs query = new FsUserIntegralLogs();
             query.setUserId(userId);
-            
+
             List<FsUserIntegralLogs> logs = fsUserIntegralLogsService.selectFsUserIntegralLogsList(query);
-            
+
             // 如果指定了类型,进行过滤
             if (type != null) {
                 if (type == 1) {
@@ -397,7 +437,7 @@ public class LiveCompletionPointsController extends AppBaseController {
                     logs.removeIf(log -> log.getIntegral() == null || log.getIntegral() >= 0);
                 }
             }
-            
+
             return R.ok().put("data", logs);
         } catch (Exception e) {
             return R.error("查询失败: " + e.getMessage());

+ 197 - 0
fs-user-app/src/main/java/com/fs/app/controller/live/LiveOrderController.java

@@ -35,7 +35,10 @@ import com.fs.his.domain.FsPayConfig;
 import com.fs.his.domain.MerchantAppConfig;
 import com.fs.his.mapper.MerchantAppConfigMapper;
 import com.fs.hisStore.domain.FsUserScrm;
+import com.fs.hisStore.enums.AfterSalesStatusEnum;
 import com.fs.hisStore.enums.OrderInfoEnum;
+import com.fs.hisStore.mapper.FsStorePaymentScrmMapper;
+import com.fs.hisStore.service.IFsStoreAfterSalesScrmService;
 import com.fs.hisStore.param.*;
 import com.fs.hisStore.service.IFsStoreOrderScrmService;
 import com.fs.hisStore.service.IFsUserScrmService;
@@ -44,9 +47,12 @@ import com.fs.huifuPay.domain.HuiFuQueryOrderResult;
 import com.fs.huifuPay.domain.HuifuCreateOrderResult;
 import com.fs.huifuPay.sdk.opps.core.request.V2TradePaymentScanpayQueryRequest;
 import com.fs.huifuPay.service.HuiFuService;
+import com.fs.live.domain.LiveAfterSales;
 import com.fs.live.domain.LiveOrder;
 import com.fs.live.domain.LiveOrderPayment;
 import com.fs.live.dto.LiveOrderComputeDTO;
+import com.fs.live.enums.LiveAfterSalesStatusEnum;
+import com.fs.live.enums.LiveOrderCancleReason;
 import com.fs.live.mapper.LiveOrderPaymentMapper;
 import com.fs.live.param.FsMyLiveOrderQueryParam;
 import com.fs.live.param.LiveOrderComputedParam;
@@ -85,7 +91,9 @@ import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletRequest;
 import java.math.BigDecimal;
+import java.sql.Timestamp;
 import java.text.SimpleDateFormat;
+import java.time.LocalDateTime;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
 
@@ -132,6 +140,12 @@ public class LiveOrderController extends AppBaseController
     IPayService ybPayService;
     @Autowired
     private HuiFuService huiFuService;
+    @Autowired
+    private IFsStoreOrderScrmService fsStoreOrderScrmService;
+    @Autowired
+    private IFsStoreAfterSalesScrmService fsStoreAfterSalesScrmService;
+    @Autowired
+    private FsStorePaymentScrmMapper fsStorePaymentScrmMapper;
 
 
 
@@ -946,5 +960,188 @@ public class LiveOrderController extends AppBaseController
     }
 
 
+    @ApiOperation("批量生成售后订单并退款")
+//    @PostMapping("/batchCreateAfterSalesAndRefund")
+    public R batchCreateAfterSalesAndRefund(@RequestBody List<String> payCodes) {
+        if (payCodes == null || payCodes.isEmpty()) {
+            return R.error("支付订单号列表不能为空");
+        }
+
+        List<Map<String, Object>> results = new ArrayList<>();
+
+        for (String payCode : payCodes) {
+            Map<String, Object> result = new HashMap<>();
+            result.put("payCode", payCode);
+
+            try {
+                // 先查询支付记录
+                // 先尝试查询商城订单支付记录
+                FsStorePaymentScrm storePayment = fsStorePaymentScrmMapper.selectFsStorePaymentByCode(payCode.split("-")[1]);
+
+                if (storePayment != null) {
+                    // 商城订单支付记录
+                    result.put("paymentType", "store");
+                    result.put("paymentId", storePayment.getPaymentId());
+
+                    // 根据支付记录查询订单
+                    FsStoreOrderScrm storeOrder = null;
+                    if (storePayment.getOrderId() != null) {
+                        storeOrder = fsStoreOrderScrmService.selectFsStoreOrderById(storePayment.getOrderId());
+                    } else if (StringUtils.isNotEmpty(storePayment.getBusinessOrderId())) {
+                        // 如果没有orderId,尝试通过businessOrderId查询
+                        storeOrder = fsStoreOrderScrmService.selectFsStoreOrderByOrderCode(storePayment.getBusinessOrderId());
+                    }
+
+                    if (storeOrder == null) {
+                        result.put("success", false);
+                        result.put("message", "根据支付记录未找到对应的订单");
+                        results.add(result);
+                        continue;
+                    }
+
+                    result.put("orderCode", storeOrder.getOrderCode());
+                    result.put("orderId", storeOrder.getId());
+
+                    // 查询售后记录
+                    FsStoreAfterSalesScrm queryAfterSales = new FsStoreAfterSalesScrm();
+                    queryAfterSales.setOrderCode(storeOrder.getOrderCode());
+                    List<FsStoreAfterSalesScrm> existingAfterSales = fsStoreAfterSalesScrmService.selectFsStoreAfterSalesList(queryAfterSales);
+
+                    FsStoreAfterSalesScrm afterSales = null;
+                    if (existingAfterSales == null || existingAfterSales.isEmpty()) {
+                        // 创建售后记录
+                        afterSales = new FsStoreAfterSalesScrm();
+                        afterSales.setOrderCode(storeOrder.getOrderCode());
+                        afterSales.setRefundAmount(storeOrder.getPayMoney());
+                        afterSales.setServiceType(0); // 0仅退款
+                        afterSales.setReasons("系统自动生成售后订单");
+                        afterSales.setExplains("系统自动生成售后订单");
+                        afterSales.setExplainImg(null);
+                        afterSales.setStatus(AfterSalesStatusEnum.STATUS_4.getValue()); // 4财务已审核(退款成功)
+                        afterSales.setSalesStatus(3); // 3已完成
+                        afterSales.setCreateTime(Timestamp.valueOf(LocalDateTime.now()));
+                        afterSales.setIsDel(0);
+                        afterSales.setUserId(storeOrder.getUserId());
+                        afterSales.setOrderStatus(storeOrder.getStatus());
+                        afterSales.setCompanyId(storeOrder.getCompanyId());
+                        afterSales.setCompanyUserId(storeOrder.getCompanyUserId());
+                        if (storeOrder.getPackageJson() != null) {
+                            afterSales.setPackageJson(storeOrder.getPackageJson());
+                        }
+                        if (storeOrder.getIsPackage() != null) {
+                            afterSales.setIsPackage(storeOrder.getIsPackage());
+                        }
+                        fsStoreAfterSalesScrmService.insertFsStoreAfterSales(afterSales);
+                        result.put("afterSalesCreated", true);
+                    } else {
+                        afterSales = existingAfterSales.get(0);
+                        result.put("afterSalesCreated", false);
+                        result.put("afterSalesId", afterSales.getId());
+                    }
+
+                    // 修改订单状态为退款成功
+                    storeOrder.setStatus(OrderInfoEnum.STATUS_NE2.getValue()); // -2退款成功
+                    storeOrder.setRefundStatus(OrderInfoEnum.REFUND_STATUS_2.getValue()); // 2已退款
+                    storeOrder.setRefundPrice(storeOrder.getPayMoney());
+                    fsStoreOrderScrmService.updateFsStoreOrder(storeOrder);
+
+                    result.put("success", true);
+                    result.put("message", "处理成功");
+                    result.put("afterSalesId", afterSales != null ? afterSales.getId() : null);
+
+                } else {
+                    // 尝试查询直播订单支付记录
+                    LiveOrderPayment livePayment = liveOrderPaymentMapper.selectLiveOrderPaymentByPaymentCode(payCode.split("-")[1]);
+
+                    if (livePayment != null) {
+                        // 直播订单支付记录
+                        result.put("paymentType", "live");
+                        result.put("paymentId", livePayment.getPaymentId());
+
+                        // 根据支付记录查询订单
+                        LiveOrder liveOrder = null;
+                        if (StringUtils.isNotEmpty(livePayment.getBusinessId())) {
+                            // businessId 是订单ID(Long类型转String)
+                            try {
+                                Long orderId = Long.parseLong(livePayment.getBusinessId());
+                                liveOrder = orderService.selectLiveOrderByOrderId(String.valueOf(orderId));
+                            } catch (NumberFormatException e) {
+                                // 如果不是数字,可能是订单号
+                                liveOrder = orderService.selectLiveOrderByOrderId(livePayment.getBusinessId());
+                            }
+                        } else if (StringUtils.isNotEmpty(livePayment.getBusinessCode())) {
+                            // 通过businessCode(订单号)查询
+                            liveOrder = orderService.selectLiveOrderByOrderId(livePayment.getBusinessCode());
+                        }
+
+                        if (liveOrder == null) {
+                            result.put("success", false);
+                            result.put("message", "根据支付记录未找到对应的订单");
+                            results.add(result);
+                            continue;
+                        }
+
+                        result.put("orderCode", liveOrder.getOrderCode());
+                        result.put("orderId", liveOrder.getOrderId());
+
+                        // 查询售后记录
+                        LiveAfterSales queryAfterSales = new LiveAfterSales();
+                        queryAfterSales.setOrderId(liveOrder.getOrderId());
+                        List<LiveAfterSales> existingAfterSales = liveAfterSalesService.selectLiveAfterSalesList(queryAfterSales);
+
+                        LiveAfterSales afterSales = null;
+                        if (existingAfterSales == null || existingAfterSales.isEmpty()) {
+                            // 创建售后记录
+                            afterSales = new LiveAfterSales();
+                            afterSales.setOrderId(liveOrder.getOrderId());
+                            afterSales.setRefundAmount(liveOrder.getPayMoney());
+                            afterSales.setRefundType(0); // 0仅退款
+                            afterSales.setReasons("系统自动生成售后订单");
+                            afterSales.setExplains("系统自动生成售后订单");
+                            afterSales.setExplainImg(null);
+                            afterSales.setStatus(LiveAfterSalesStatusEnum.STATUS_4.getValue()); // 4财务已审核(退款成功)
+                            afterSales.setSalesStatus(3); // 3已完成
+                            afterSales.setCreateTime(Timestamp.valueOf(LocalDateTime.now()));
+                            afterSales.setIsDel(0);
+                            afterSales.setUserId(Long.valueOf(liveOrder.getUserId()));
+                            afterSales.setOrderStatus(liveOrder.getStatus());
+                            afterSales.setCompanyId(liveOrder.getCompanyId());
+                            afterSales.setCompanyUserId(liveOrder.getCompanyUserId());
+                            liveAfterSalesService.insertLiveAfterSales(afterSales);
+                            result.put("afterSalesCreated", true);
+                        } else {
+                            afterSales = existingAfterSales.get(0);
+                            result.put("afterSalesCreated", false);
+                            result.put("afterSalesId", afterSales.getId());
+                        }
+
+                        // 修改订单状态为退款成功
+                        liveOrder.setStatus(OrderInfoEnum.STATUS_NE2.getValue()); // -2退款成功
+                        liveOrder.setRefundStatus(String.valueOf(OrderInfoEnum.REFUND_STATUS_2.getValue())); // 2已退款
+                        liveOrder.setRefundMoney(liveOrder.getPayMoney());
+                        liveOrder.setRefundTime(new Date());
+                        orderService.updateLiveOrder(liveOrder);
+
+                        result.put("success", true);
+                        result.put("message", "处理成功");
+                        result.put("afterSalesId", afterSales != null ? afterSales.getId() : null);
+
+                    } else {
+                        result.put("success", false);
+                        result.put("message", "未找到对应的支付记录");
+                    }
+                }
+
+            } catch (Exception e) {
+                logger.error("处理支付订单失败: " + payCode, e);
+                result.put("success", false);
+                result.put("message", "处理失败: " + e.getMessage());
+            }
+
+            results.add(result);
+        }
+
+        return R.ok().put("results", results);
+    }
 
 }

+ 11 - 2
fs-user-app/src/main/java/com/fs/app/controller/store/WxUserScrmController.java

@@ -5,6 +5,7 @@ import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
 import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
 import cn.binarywang.wx.miniapp.bean.WxMaUserInfo;
 import cn.hutool.core.date.DateTime;
+import cn.hutool.core.util.ObjectUtil;
 import com.fs.app.annotation.Login;
 import com.fs.app.controller.AppBaseController;
 import com.fs.app.utils.JwtUtils;
@@ -237,13 +238,18 @@ public class WxUserScrmController extends AppBaseController {
     @ApiOperation("获取微信小程序用户信息")
     @PostMapping("/getWeixinInfo")
     public R getWeixinInfo(@RequestBody LoginMpWxParam param) {
-        final WxMaService wxService = WxMaConfiguration.getMaService(maProperties.getConfigs().get(0).getAppid());
+        String appid = maProperties.getConfigs().get(0).getAppid();
+        if (ObjectUtil.isNotEmpty(param.getTraceId()) && ObjectUtil.isNotEmpty(param.getAppId())){
+            appid = param.getAppId();
+        }
+        final WxMaService wxService = WxMaConfiguration.getMaService(appid);
         try {
             WxMaJscode2SessionResult session = wxService.getUserService().getSessionInfo(param.getCode());
             // 用户信息校验
             if (!wxService.getUserService().checkUserInfo(session.getSessionKey(), param.getRawData(), param.getSignature())) {
                 return R.error("user check failed");
             }
+
             // 解密用户信息
             WxMaUserInfo userInfo = wxService.getUserService().getUserInfo(session.getSessionKey(), param.getEncryptedData(), param.getIv());
             FsUserScrm user=userService.selectFsUserById(Long.parseLong(getUserId()));
@@ -251,6 +257,9 @@ public class WxUserScrmController extends AppBaseController {
             user.setAvatar(userInfo.getAvatarUrl());
             user.setIsWeixinAuth(1);
             userService.updateFsUser(user);
+
+            // 广告线索
+            leadService.weChatNameLead(param.getTraceId(), userInfo.getNickName());
             return R.ok();
         } catch (WxErrorException e) {
             e.printStackTrace();
@@ -371,7 +380,7 @@ public class WxUserScrmController extends AppBaseController {
             userService.handleFsUserWx(user,loginMaWxParam,session);
             String token = jwtUtils.generateToken(user.getUserId());
             // 广告线索
-            leadService.weChatAuthorizationLead(param.getTraceId(), session.getUnionid(),session.getOpenid(),user.getPhone());
+            leadService.weChatAuthorizationLead(param.getTraceId(), session.getUnionid(),session.getOpenid(),user.getPhone(),user.getUserId());
             return R.ok("登录成功").put("token",token).put("user", user);
         } catch (WxErrorException e) {
             //this.logger.error(e.getMessage(), e);