Bläddra i källkod

Merge branch 'master' of http://1.14.104.71:10880/root/ylrz_his_scrm_java into 济南顺亿景

# Conflicts:
#	fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
#	fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
#	fs-service/src/main/java/com/fs/newAdv/domain/Lead.java
#	fs-service/src/main/java/com/fs/newAdv/service/impl/LeadServiceImpl.java
15376779826 3 månader sedan
förälder
incheckning
d7b30b34df
100 ändrade filer med 4751 tillägg och 472 borttagningar
  1. 1 1
      fs-ad-new-api/Dockerfile
  2. 3 4
      fs-ad-new-api/src/main/java/com/fs/app/controller/LandingPageController.java
  3. 14 10
      fs-ad-new-api/src/main/java/com/fs/app/controller/TestController.java
  4. 26 2
      fs-ad-new-api/src/main/java/com/fs/app/controller/TrackingController.java
  5. 17 6
      fs-ad-new-api/src/main/java/com/fs/app/controller/WeChatController.java
  6. 10 3
      fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeService.java
  7. 74 43
      fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeServiceImpl.java
  8. 3 1
      fs-ad-new-api/src/main/java/com/fs/app/facade/ConversionServiceImpl.java
  9. 2 14
      fs-ad-new-api/src/main/java/com/fs/app/mq/consumer/ConversionTrackingMessageConsumer.java
  10. 1 1
      fs-ad-new-api/src/main/java/com/fs/app/task/ConversionRetryTask.java
  11. 1 1
      fs-ad-new-api/src/main/java/com/fs/app/task/DataSyncTask.java
  12. 110 0
      fs-ad-new-api/src/main/java/com/fs/framework/aspectj/ControllerLogAspect.java
  13. 54 0
      fs-ad-new-api/src/main/java/com/fs/framework/aspectj/RocketMQTraceIdAspect.java
  14. 1 1
      fs-ad-new-api/src/main/resources/application.yml
  15. 5 5
      fs-admin/src/main/java/com/fs/company/controller/CompanyController.java
  16. 13 0
      fs-admin/src/main/java/com/fs/company/controller/CompanyDeptController.java
  17. 25 1
      fs-admin/src/main/java/com/fs/company/controller/CompanyUserController.java
  18. 7 0
      fs-admin/src/main/java/com/fs/course/controller/FsCourseRedPacketLogController.java
  19. 27 17
      fs-admin/src/main/java/com/fs/course/controller/FsCourseWatchLogController.java
  20. 18 1
      fs-admin/src/main/java/com/fs/course/controller/FsUserCoursePeriodController.java
  21. 19 1
      fs-admin/src/main/java/com/fs/course/controller/FsUserCourseVideoController.java
  22. 2 2
      fs-admin/src/main/java/com/fs/fastGpt/FastgptEventLogTotalController.java
  23. 20 0
      fs-admin/src/main/java/com/fs/fastGpt/GptRoleController.java
  24. 2 2
      fs-admin/src/main/java/com/fs/his/controller/FsIntegralOrderController.java
  25. 25 4
      fs-admin/src/main/java/com/fs/his/controller/FsStorePaymentController.java
  26. 51 8
      fs-admin/src/main/java/com/fs/his/task/CompanyBalanceTask.java
  27. 1 0
      fs-admin/src/main/java/com/fs/his/task/Task.java
  28. 2 2
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreAfterSalesScrmController.java
  29. 3 1
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreHealthOrderScrmController.java
  30. 18 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  31. 61 12
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStorePaymentScrmController.java
  32. 2 1
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreStatisticsScrmController.java
  33. 85 3
      fs-admin/src/main/java/com/fs/hisStore/task/LiveTask.java
  34. 38 3
      fs-admin/src/main/java/com/fs/hisStore/task/MallStoreTask.java
  35. 1 1
      fs-admin/src/main/java/com/fs/live/controller/LiveAfterSalesController.java
  36. 7 4
      fs-admin/src/main/java/com/fs/live/controller/OrderController.java
  37. 4 4
      fs-admin/src/main/java/com/fs/qw/controller/QwCompanyController.java
  38. 9 8
      fs-admin/src/main/java/com/fs/qw/controller/QwSopTempController.java
  39. 698 84
      fs-admin/src/main/java/com/fs/qw/controller/QwUserController.java
  40. 1 1
      fs-admin/src/main/resources/application.yml
  41. 2 0
      fs-common/src/main/java/com/fs/common/constant/LiveKeysConstant.java
  42. 12 0
      fs-common/src/main/java/com/fs/common/core/redis/RedisCache.java
  43. 367 0
      fs-common/src/main/java/com/fs/common/utils/HsCryptoUtil.java
  44. 154 0
      fs-common/src/main/java/com/fs/common/utils/StringToMapUtil.java
  45. 23 0
      fs-common/src/main/java/com/fs/common/utils/poi/ExcelUtil.java
  46. 7 6
      fs-company-app/src/main/java/com/fs/app/controller/FsUserController.java
  47. 0 1
      fs-company-app/src/main/java/com/fs/app/controller/FsUserCourseVideoController.java
  48. 1 1
      fs-company-app/src/main/java/com/fs/app/controller/WxCompanyUserController.java
  49. 38 0
      fs-company/src/main/java/com/fs/company/controller/common/CommonController.java
  50. 1 1
      fs-company/src/main/java/com/fs/company/controller/company/CompanyUserController.java
  51. 89 5
      fs-company/src/main/java/com/fs/company/controller/company/IndexStatisticsController.java
  52. 13 1
      fs-company/src/main/java/com/fs/company/controller/course/FsUserCoursePeriodController.java
  53. 6 5
      fs-company/src/main/java/com/fs/company/controller/live/LiveDataController.java
  54. 28 0
      fs-company/src/main/java/com/fs/company/controller/live/LiveMixLiuTestOpenController.java
  55. 8 4
      fs-company/src/main/java/com/fs/company/controller/live/OrderController.java
  56. 6 1
      fs-company/src/main/java/com/fs/company/controller/qw/QwAssignRuleController.java
  57. 5 4
      fs-company/src/main/java/com/fs/company/controller/qw/QwCustomerLinkController.java
  58. 13 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwSopTempController.java
  59. 6 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwUserController.java
  60. 1 0
      fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java
  61. 21 5
      fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  62. 2 1
      fs-company/src/main/java/com/fs/hisStore/controller/FsStoreStatisticsScrmController.java
  63. 25 4
      fs-company/src/main/java/com/fs/user/FsUserAdminController.java
  64. 5 0
      fs-live-app/src/main/java/com/fs/framework/aspectj/LiveWatchUserAspect.java
  65. 107 32
      fs-live-app/src/main/java/com/fs/live/task/Task.java
  66. 164 61
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  67. 1 1
      fs-live-app/src/main/resources/application.yml
  68. 1 1
      fs-quartz/src/main/java/com/fs/quartz/config/ScheduleConfig.java
  69. 6 0
      fs-qw-api/Dockerfile
  70. 5 22
      fs-qw-api/src/main/java/com/fs/app/service/QwDataCallbackService.java
  71. 91 9
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java
  72. 181 0
      fs-service/src/main/java/com/fs/aiSoundReplication/VoiceCloneController.java
  73. 23 0
      fs-service/src/main/java/com/fs/aiSoundReplication/config/OkHttpConfig.java
  74. 34 0
      fs-service/src/main/java/com/fs/aiSoundReplication/config/TtsConfig.java
  75. 27 0
      fs-service/src/main/java/com/fs/aiSoundReplication/config/VoiceCloneConfig.java
  76. 42 0
      fs-service/src/main/java/com/fs/aiSoundReplication/exception/ErrorCodeEnum.java
  77. 21 0
      fs-service/src/main/java/com/fs/aiSoundReplication/exception/VoiceCloneException.java
  78. 20 0
      fs-service/src/main/java/com/fs/aiSoundReplication/param/BaseResponse.java
  79. 33 0
      fs-service/src/main/java/com/fs/aiSoundReplication/param/StatusResponse.java
  80. 13 0
      fs-service/src/main/java/com/fs/aiSoundReplication/param/TrainingStatusRequest.java
  81. 64 0
      fs-service/src/main/java/com/fs/aiSoundReplication/param/TtsRequest.java
  82. 54 0
      fs-service/src/main/java/com/fs/aiSoundReplication/param/TtsResponse.java
  83. 12 0
      fs-service/src/main/java/com/fs/aiSoundReplication/param/UploadResponse.java
  84. 39 0
      fs-service/src/main/java/com/fs/aiSoundReplication/param/VoiceCloneRequest.java
  85. 57 0
      fs-service/src/main/java/com/fs/aiSoundReplication/service/TtsService.java
  86. 50 0
      fs-service/src/main/java/com/fs/aiSoundReplication/service/VoiceCloneService.java
  87. 488 0
      fs-service/src/main/java/com/fs/aiSoundReplication/service/impl/TtsServiceImpl.java
  88. 343 0
      fs-service/src/main/java/com/fs/aiSoundReplication/service/impl/VoiceCloneServiceImpl.java
  89. 44 0
      fs-service/src/main/java/com/fs/aiSoundReplication/util/FileToMultipartConverterUtil.java
  90. 47 0
      fs-service/src/main/java/com/fs/aiSoundReplication/util/FileUtil.java
  91. 3 1
      fs-service/src/main/java/com/fs/company/domain/CompanyRedPacketBalanceLogs.java
  92. 24 4
      fs-service/src/main/java/com/fs/company/mapper/CompanyMoneyLogsMapper.java
  93. 13 7
      fs-service/src/main/java/com/fs/company/mapper/CompanyRedPacketBalanceLogsMapper.java
  94. 6 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyUserMapper.java
  95. 99 0
      fs-service/src/main/java/com/fs/company/param/VcCompanyUser.java
  96. 7 2
      fs-service/src/main/java/com/fs/company/service/ICompanyService.java
  97. 322 34
      fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java
  98. 2 0
      fs-service/src/main/java/com/fs/course/domain/FsUserCoursePeriod.java
  99. 1 1
      fs-service/src/main/java/com/fs/course/mapper/FsCourseAnswerLogsMapper.java
  100. 13 6
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoMapper.java

+ 1 - 1
fs-ad-new-api/Dockerfile

@@ -1,4 +1,4 @@
-FROM openjdk:8-jre
+FROM anolis-registry.cn-zhangjiakou.cr.aliyuncs.com/openanolis/openjdk:8-8.6
 # java版本,最好使用openjdk,而不是类似于Java:1.8
 COPY ./target/fs-ad-new-api.jar fs-ad-new-api.jar
 # 向外暴露的接口,最好与项目yml文件中的端口一致

+ 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 - 6
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,12 +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.getInteger("expires_in")));
                     advMiniConfigService.updateById(advMiniConfig);
                 }
                 Map<String, Object> map = new HashMap<>();
                 Map<String, Object> map2 = new HashMap<>();
-                map2.put("path", "pages/home/productList");
+                map2.put("path", "/pages_ad/index");
                 map2.put("query", "traceId=" + traceId);
                 map2.put("env_version", "trial");
                 map.put("jump_wxa", map2);
@@ -84,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);
 }

+ 74 - 43
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,29 +136,29 @@ 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);
             }
         }
         // 模板缓存
-        Object ca = redisUtil.get(TEMPLATE_DATA + traceId);
+/*        Object ca = redisUtil.get(TEMPLATE_DATA + traceId);
         String templateData;
         if (ca != null) {
             templateData = String.valueOf(ca);
@@ -164,26 +169,34 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
             // 替换二维码链接
             updateQrCodeInTemplate(jsonObject, traceId, byId, byTraceId);
             templateData = JSONUtil.toJsonStr(jsonObject);
-        }
+        }*/
 
+        // 查询模板数据
+        LandingPageTemplate landingPageTemplate = landingPageTemplateService.getById(byId.getLaunchPageId());
+        JSONObject jsonObject = JSONUtil.parseObj(landingPageTemplate.getTemplateData());
+        // 替换二维码链接
+        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;
     }
 
@@ -196,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;
@@ -221,7 +234,7 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
                                                Integer allocationRule,
                                                Long allocationRuleId,
                                                Lead byTraceId) {
-
+        log.info("开始获取广告二维码: {} {} {} {}", launchType, allocationRule, allocationRuleId, byTraceId);
         // 二维码
         String qrCode = "";
         if (allocationRule == 1) {
@@ -279,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();
 

+ 1 - 1
fs-ad-new-api/src/main/java/com/fs/app/task/ConversionRetryTask.java

@@ -37,7 +37,7 @@ public class ConversionRetryTask {
      * 转化回传重试任务
      * cron: 每10分钟执行
      */
-    @Scheduled(cron = "0 */5 * * * ?")
+    // @Scheduled(cron = "0 */5 * * * ?")
     @DistributeLock(scene = "task", key = "conversion_retry", waitTime = 0, errorMsg = "conversion_retry任务已执行")
     public void execute() {
         // 查询待重试的转化记录

+ 1 - 1
fs-ad-new-api/src/main/java/com/fs/app/task/DataSyncTask.java

@@ -59,7 +59,7 @@ public class DataSyncTask {
      * 数据同步任务->当日数据
      * cron: 每1小时统计站点数据
      */
-    @Scheduled(cron = "0 0/1 * * * ?")
+    @Scheduled(cron = "0 0 0/1 * * ?")
     @DistributeLock(scene = "task", key = "sync_today_data", waitTime = 0, errorMsg = "sync_today_data任务已执行")
     public void syncTodayData() throws InterruptedException {
         String batchNo = DateUtil.format(LocalDateTime.now(), "yyyy-MM-dd");

+ 110 - 0
fs-ad-new-api/src/main/java/com/fs/framework/aspectj/ControllerLogAspect.java

@@ -0,0 +1,110 @@
+
+package com.fs.framework.aspectj;
+
+import com.alibaba.fastjson.JSON;
+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.stereotype.Component;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+
+@Slf4j
+@Aspect
+@Component
+public class ControllerLogAspect {
+
+    @Pointcut("execution(* com.fs.app.controller..*.*(..))")
+    public void controllerPointcut() {
+    }
+
+    @Around("controllerPointcut()")
+    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
+        long startTime = System.currentTimeMillis();
+
+        String className = joinPoint.getTarget().getClass().getName();
+        String methodName = joinPoint.getSignature().getName();
+        String fullMethodName = className + "." + methodName;
+
+        Object[] args = joinPoint.getArgs();
+        String requestParams = getRequestParams(args);
+
+        log.info("========== 接口调用开始 ==========");
+        log.info("接口方法: {}", fullMethodName);
+        log.info("请求参数: {}", requestParams);
+
+        Object result = null;
+        try {
+            result = joinPoint.proceed();
+
+            long endTime = System.currentTimeMillis();
+            long costTime = endTime - startTime;
+
+            log.info("返回结果: {}", JSON.toJSONString(result));
+            log.info("接口耗时: {} ms", costTime);
+            log.info("========== 接口调用结束 ==========");
+
+            return result;
+        } catch (Throwable e) {
+            long endTime = System.currentTimeMillis();
+            long costTime = endTime - startTime;
+
+            log.error("接口异常: {}", e.getMessage());
+            log.error("接口耗时: {} ms", costTime);
+            log.error("========== 接口调用异常 ==========");
+
+            throw e;
+        }
+    }
+
+    private String getRequestParams(Object[] args) {
+        if (args == null || args.length == 0) {
+            return "无参数";
+        }
+
+        StringBuilder params = new StringBuilder();
+        for (int i = 0; i < args.length; i++) {
+            if (args[i] != null && !isFilterObject(args[i])) {
+                try {
+                    Object jsonObj = JSON.toJSON(args[i]);
+                    params.append(jsonObj.toString());
+                    if (i < args.length - 1) {
+                        params.append(", ");
+                    }
+                } catch (Exception e) {
+                    params.append(args[i].getClass().getSimpleName());
+                }
+            }
+        }
+
+        return params.length() > 0 ? params.toString() : "无参数";
+    }
+
+    private boolean isFilterObject(final Object o) {
+        Class<?> clazz = o.getClass();
+        if (clazz.isArray()) {
+            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
+        } else if (Collection.class.isAssignableFrom(clazz)) {
+            Collection collection = (Collection) o;
+            for (Iterator iter = collection.iterator(); iter.hasNext();) {
+                return iter.next() instanceof MultipartFile;
+            }
+        } else if (Map.class.isAssignableFrom(clazz)) {
+            Map map = (Map) o;
+            for (Iterator iter = map.entrySet().iterator(); iter.hasNext();) {
+                Map.Entry entry = (Map.Entry) iter.next();
+                return entry.getValue() instanceof MultipartFile;
+            }
+        }
+        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
+                || o instanceof BindingResult;
+    }
+}

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

+ 1 - 1
fs-ad-new-api/src/main/resources/application.yml

@@ -4,7 +4,7 @@ server:
 # Spring配置
 spring:
   profiles:
-    active: dev
+    active: druid-ylrz
 #    active: druid-ylrz
 
 #

+ 5 - 5
fs-admin/src/main/java/com/fs/company/controller/CompanyController.java

@@ -134,7 +134,7 @@ public class CompanyController extends BaseController
      * 新增企业
      */
     @PreAuthorize("@ss.hasPermi('company:company:add')")
-    @Log(title = "企业", businessType = BusinessType.INSERT)
+    @Log(title = "企业", businessType = BusinessType.INSERT, isStoreLog = true)
     @PostMapping
     public R add(@RequestBody Company company)
     {
@@ -153,7 +153,7 @@ public class CompanyController extends BaseController
      * 修改企业
      */
     @PreAuthorize("@ss.hasPermi('company:company:edit')")
-    @Log(title = "企业", businessType = BusinessType.UPDATE)
+    @Log(title = "企业", businessType = BusinessType.UPDATE, isStoreLog = true)
     @PutMapping
     public AjaxResult edit(@RequestBody Company company)
     {
@@ -186,7 +186,7 @@ public class CompanyController extends BaseController
      * 删除企业
      */
     @PreAuthorize("@ss.hasPermi('company:company:remove')")
-    @Log(title = "企业", businessType = BusinessType.DELETE)
+    @Log(title = "企业", businessType = BusinessType.DELETE, isStoreLog = true)
 	@DeleteMapping("/{companyIds}")
     public AjaxResult remove(@PathVariable Long[] companyIds)
     {
@@ -244,7 +244,7 @@ public class CompanyController extends BaseController
 
 
     @PreAuthorize("@ss.hasPermi('company:company:recharge')")
-    @Log(title = "企业转账", businessType = BusinessType.INSERT)
+    @Log(title = "企业转账", businessType = BusinessType.INSERT, isStoreLog = true)
     @PostMapping(value = "/recharge")
     @Transactional
     @RepeatSubmit
@@ -270,7 +270,7 @@ public class CompanyController extends BaseController
     }
 
     @PreAuthorize("@ss.hasPermi('company:company:deduct')")
-    @Log(title = "企业扣款", businessType = BusinessType.INSERT)
+    @Log(title = "企业扣款", businessType = BusinessType.INSERT, isStoreLog = true)
     @PostMapping(value = "/deduct")
     @Transactional
     @RepeatSubmit

+ 13 - 0
fs-admin/src/main/java/com/fs/company/controller/CompanyDeptController.java

@@ -100,4 +100,17 @@ public class CompanyDeptController extends BaseController
         List<CompanyDept> depts = companyDeptService.selectCompanyDeptList(dept);
         return AjaxResult.success(companyDeptService.buildDeptTreeSelect(depts));
     }
+
+    /**
+     * 获取部门下拉树列表
+     */
+    @GetMapping("/treeselectByCompanyId/{companyId}")
+    public AjaxResult treeselectByCompanyId(@PathVariable("companyId") Long companyId)
+    {
+        CompanyDept dept = new CompanyDept();
+        dept.setStatus("0");
+        dept.setCompanyId(companyId);
+        List<CompanyDept> depts = companyDeptService.selectCompanyDeptList(dept);
+        return AjaxResult.success(companyDeptService.buildDeptTreeSelect(depts));
+    }
 }

+ 25 - 1
fs-admin/src/main/java/com/fs/company/controller/CompanyUserController.java

@@ -11,12 +11,17 @@ import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.service.ICompanyUserService;
+import com.fs.his.vo.OptionsVO;
 import com.fs.qw.dto.UserProjectDTO;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 企业员工信息Controller
@@ -173,5 +178,24 @@ public class CompanyUserController extends BaseController
         List<CompanyUser> list = companyUserService.getCompanyUserList(user);
         return getDataTable(list);
     }
-
+    /**
+     * 根据销售名称模糊查询
+     * @param name  名称
+     * @return  list
+     */
+    @GetMapping("/getCompanyUserListLikeName")
+    public R getCompanyUserListLikeName(@RequestParam(required = false) String name,
+                                        @RequestParam(required = false, defaultValue = "1") Integer pageNum,
+                                        @RequestParam(required = false, defaultValue = "10") Integer pageSize,
+                                        @RequestParam(required = false) Long companyId) {
+        Map<String,Object> params = new HashMap<>();
+        params.put("nickName", name);
+        //查询多条数据传入公司
+//        if (pageSize>=200){
+        params.put("companyId", companyId);
+//        }
+        PageHelper.startPage(pageNum, pageSize);
+        List<OptionsVO> companyUserList = companyUserService.selectCompanyUserListByMap(params);
+        return R.ok().put("data", new PageInfo<>(companyUserList));
+    }
 }

+ 7 - 0
fs-admin/src/main/java/com/fs/course/controller/FsCourseRedPacketLogController.java

@@ -196,6 +196,13 @@ public class FsCourseRedPacketLogController extends BaseController
         return R.ok().put("list", optionsVOS);
     }
 
+    @GetMapping("/courseListByCompanyId/{companyId}")
+    public R courseListByCompanyId(@PathVariable("companyId") Long companyId)
+    {
+        List<OptionsVO> optionsVOS = fsUserCourseMapper.selectFsUserCourseByCompany(companyId);
+        return R.ok().put("list", optionsVOS);
+    }
+
     @GetMapping(value = "/videoList/{id}")
     public R videoList(@PathVariable("id") Long id)
     {

+ 27 - 17
fs-admin/src/main/java/com/fs/course/controller/FsCourseWatchLogController.java

@@ -2,17 +2,13 @@ package com.fs.course.controller;
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-import cn.hutool.core.collection.CollectionUtil;
-import com.fs.common.constant.HttpStatus;
 import com.fs.common.core.domain.R;
 import com.fs.common.exception.CustomException;
-import com.fs.common.utils.ServletUtils;
 import com.fs.course.param.FsCourseOverParam;
 import com.fs.course.param.FsCourseWatchLogListParam;
 import com.fs.course.param.FsCourseWatchLogStatisticsListParam;
+import com.fs.course.service.IFsUserCoursePeriodDaysService;
+import com.fs.course.service.IFsUserCoursePeriodService;
 import com.fs.course.vo.FsCourseOverVO;
 import com.fs.course.vo.FsCourseWatchLogListVO;
 import com.fs.course.vo.FsCourseWatchLogStatisticsListVO;
@@ -54,25 +50,39 @@ public class FsCourseWatchLogController extends BaseController
 
     @Autowired
     private IQwWatchLogService qwWatchLogService;
+    @Autowired
+    private IFsUserCoursePeriodDaysService userCoursePeriodDaysService;
+    @Autowired
+    private IFsUserCoursePeriodService userCoursePeriodService;
     /**
      * 查询短链课程看课记录列表
      */
     @PreAuthorize("@ss.hasPermi('course:courseWatchLog:list')")
-    @GetMapping("/list")
-    public TableDataInfo list(FsCourseWatchLogListParam param)
+    @PostMapping("/list")
+    public R list(@RequestBody FsCourseWatchLogListParam param)
     {
-        startPage();
-        if(CollectionUtil.isNotEmpty(param.getUserIds())){
-            param.setUserIds(param.getUserIds().stream()
-                    .filter(userId -> userId != null && userId.startsWith("user_"))
-                    .map(userId -> userId.substring(5))
-                    .collect(Collectors.toList())
-            );
+
+        if (param.getSendType()==1&& param.getPeriodETime()!=null && param.getPeriodSTime()!=null) {
+            List<Long> periodIds = userCoursePeriodDaysService.selectFsUserCoursePeriodDaysByTime(param.getPeriodSTime(), param.getPeriodETime());
+
+            if (!periodIds.isEmpty()){
+                List<Long> longs = userCoursePeriodService.selectFsUserCoursePeriodListByPeriodId(periodIds, param.getCompanyId());
+                if (!longs.isEmpty()){
+                    param.setPeriodIds(longs);
+                }else {
+                    return R.ok().put("data", new PageInfo<>());
+                }
+            }else {
+                return R.ok().put("data", new PageInfo<>());
+            }
+
         }
-        List<FsCourseWatchLogListVO> list = fsCourseWatchLogService.selectFsCourseWatchLogListVO(param);
-        return getDataTable(list);
+
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        return R.ok().put("data", new PageInfo<>(fsCourseWatchLogService.selectFsCourseWatchLogListVO(param)));
     }
 
+
     /**
      * 查询短链课程看课记录列表
      */

+ 18 - 1
fs-admin/src/main/java/com/fs/course/controller/FsUserCoursePeriodController.java

@@ -5,8 +5,10 @@ import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.entity.SysDictData;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.course.domain.FsUserCoursePeriod;
 import com.fs.course.domain.FsUserCoursePeriodDays;
@@ -63,7 +65,22 @@ public class FsUserCoursePeriodController extends BaseController {
         List<FsUserCoursePeriod> list = fsUserCoursePeriodService.selectFsUserCoursePeriodList(fsUserCoursePeriod);
         return getDataTable(list);
     }
-
+    /**
+     * @Description: 营期key value 值
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/11/18 14:59
+     */
+    @GetMapping("/listLabel/{companyId}")
+    public TableDataInfo listLabel(@PathVariable("companyId") Long companyId)
+    {
+        FsUserCoursePeriod fsUserCoursePeriod = new FsUserCoursePeriod();
+        fsUserCoursePeriod.setCompanyId(companyId + "");
+        startPage();
+        List<SysDictData> list = fsUserCoursePeriodService.selectFsUserCoursePeriodListLabel(fsUserCoursePeriod);
+        return getDataTable(list);
+    }
     @PreAuthorize("@ss.hasPermi('course:period:list')")
     @PostMapping("/page")
     @ApiOperation("自定义查询主列表分页")

+ 19 - 1
fs-admin/src/main/java/com/fs/course/controller/FsUserCourseVideoController.java

@@ -15,6 +15,7 @@ import com.fs.course.config.CourseConfig;
 import com.fs.course.domain.FsUserCourse;
 import com.fs.course.domain.FsUserCourseVideo;
 import com.fs.course.mapper.FsUserCourseVideoMapper;
+import com.fs.course.param.BatchEditCoverParam;
 import com.fs.course.param.BatchRedUpdate;
 import com.fs.course.param.BatchVideoSvae;
 import com.fs.course.param.CourseVideoUpdates;
@@ -23,12 +24,13 @@ import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.course.vo.FsUserCourseVideoChooseVO;
 import com.fs.framework.web.service.TokenService;
 import com.fs.his.vo.OptionsVO;
-import com.fs.qw.vo.SortDayVo;
 import com.fs.system.service.ISysConfigService;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
+import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.HashMap;
@@ -262,4 +264,20 @@ public class FsUserCourseVideoController extends BaseController
         List<FsUserCourseVideoChooseVO> list = fsUserCourseVideoService.getChooseCourseVideoListByMap(params);
         return R.ok().put("data", new PageInfo<>(list));
     }
+
+    @ApiOperation("视频下架")
+    @PreAuthorize("@ss.hasPermi('course:userCourseVideo:batchDown')")
+    @Log(title = "课堂视频", businessType = BusinessType.UPDATE)
+    @PostMapping("/batchDown/{videoIds}")
+    public AjaxResult batchDown(@PathVariable String[] videoIds) {
+        return toAjax(fsUserCourseVideoService.batchDown(videoIds));
+    }
+
+    @ApiOperation("批量修改视频封面图")
+    @PreAuthorize("@ss.hasPermi('course:userCourseVideo:batchEditCover')")
+    @Log(title = "课堂视频", businessType = BusinessType.UPDATE)
+    @PostMapping("/batchEditCover")
+    public AjaxResult batchEditCover(@Validated @RequestBody BatchEditCoverParam param) {
+        return toAjax(fsUserCourseVideoService.batchEditCover(param));
+    }
 }

+ 2 - 2
fs-admin/src/main/java/com/fs/fastGpt/FastgptEventLogTotalController.java

@@ -89,8 +89,8 @@ public class FastgptEventLogTotalController extends BaseController
     @GetMapping("/export")
     public AjaxResult export(FastgptEventLogTotal fastgptEventLogTotal)
     {
-        List<FastgptEventLogTotal> list = fastgptEventLogTotalService.selectFastgptEventLogTotalList(fastgptEventLogTotal);
-        ExcelUtil<FastgptEventLogTotal> util = new ExcelUtil<FastgptEventLogTotal>(FastgptEventLogTotal.class);
+        List<FastgptEventLogTotalVo> list = fastgptEventLogTotalService.selectFastgptEventLogTotalExport(fastgptEventLogTotal);
+        ExcelUtil<FastgptEventLogTotalVo> util = new ExcelUtil<FastgptEventLogTotalVo>(FastgptEventLogTotalVo.class);
         return util.exportExcel(list, "ai事件埋点统计数据");
     }
 

+ 20 - 0
fs-admin/src/main/java/com/fs/fastGpt/GptRoleController.java

@@ -6,6 +6,7 @@ import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.fastGpt.domain.FastGptRole;
 import com.fs.fastGpt.service.IFastGptRoleService;
@@ -49,6 +50,25 @@ public class GptRoleController extends BaseController
         return getDataTable(list);
     }
 
+    /**
+     * 查询应用列表
+     */
+    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptRole:newList')")
+    @GetMapping("/newList")
+    public TableDataInfo newList(FastGptRole fastGptRole)
+    {
+        startPage();
+        fastGptRole.setCompanyId(fastGptRole.getCompanyId());
+        List<FastGptRoleVO> list = fastGptRoleService.selectFastGptRoleListVONew(fastGptRole);
+        for (FastGptRoleVO fastGptRoleVO : list) {
+            String reminderWords = fastGptRoleVO.getReminderWords();
+            if (reminderWords!=null && reminderWords.length()>110) {
+                fastGptRoleVO.setReminderWords(reminderWords.substring(0,110)+"...");
+            }
+        }
+        return getDataTable(list);
+    }
+
     /**
      * 导出应用列表
      */

+ 2 - 2
fs-admin/src/main/java/com/fs/his/controller/FsIntegralOrderController.java

@@ -464,8 +464,8 @@ public class FsIntegralOrderController extends BaseController
     {
         ExcelUtil<FsIntegralOrderExcelVO> util = new ExcelUtil<>(FsIntegralOrderExcelVO.class);
         List<FsIntegralOrderExcelVO> list = util.importExcel(file.getInputStream());
-        String message = fsIntegralOrderService.importOrderStatusData(list);
-        return AjaxResult.success(message);
+        FsIntegralOrderImportResultVO result = fsIntegralOrderService.importOrderStatusData(list);
+        return AjaxResult.success(result);
     }
 
     @GetMapping("/importUpdateOrderTemplate")

+ 25 - 4
fs-admin/src/main/java/com/fs/his/controller/FsStorePaymentController.java

@@ -5,10 +5,10 @@ import java.util.List;
 
 import com.fs.common.core.domain.R;
 import com.fs.common.utils.SecurityUtils;
-import com.fs.his.domain.FsExportTask;
+import com.fs.his.domain.*;
 import com.fs.his.mapper.FsPrescribeMapper;
 import com.fs.his.param.FsStorePaymentParam;
-import com.fs.his.service.IFsExportTaskService;
+import com.fs.his.service.*;
 import com.fs.his.vo.FsStorePaymentExcelVO;
 import com.fs.his.vo.FsStorePaymentVO;
 import lombok.extern.slf4j.Slf4j;
@@ -26,8 +26,6 @@ import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.enums.BusinessType;
-import com.fs.his.domain.FsStorePayment;
-import com.fs.his.service.IFsStorePaymentService;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.common.core.page.TableDataInfo;
 
@@ -44,11 +42,17 @@ public class FsStorePaymentController extends BaseController
 {
     @Autowired
     private IFsStorePaymentService fsStorePaymentService;
+
+    @Autowired
+    private IFsStorePaymentErrorService fsStorePaymentErrorService;
     @Autowired
     FsPrescribeMapper fsPrescribeMapper;
 
     @Autowired
     private IFsExportTaskService exportTaskService;
+    @Autowired
+    private IFsPackageOrderService fsPackageOrderService;
+
     /**
      * 查询支付明细列表
      */
@@ -169,4 +173,21 @@ public class FsStorePaymentController extends BaseController
     {
         return toAjax(fsStorePaymentService.deleteFsStorePaymentByPaymentIds(paymentIds));
     }
+
+    /**
+     * 查询支付错误明细
+     */
+    @GetMapping("/error/list")
+    public TableDataInfo list(FsStorePaymentError fsStorePaymentError)
+    {
+        startPage();
+        List<FsStorePaymentError> list = fsStorePaymentErrorService.selectFsStorePaymentErrorList(fsStorePaymentError);
+        for (FsStorePaymentError vo : list){
+            if (vo.getBusinessType() != null && vo.getBusinessType()==3 &&  vo.getOrderId() != null) {
+                FsPackageOrder fsPackageOrder = fsPackageOrderService.selectFsPackageOrderByOrderId(vo.getOrderId());
+                vo.setOrderNo(fsPackageOrder.getOrderSn());
+            }
+        }
+        return getDataTable(list);
+    }
 }

+ 51 - 8
fs-admin/src/main/java/com/fs/his/task/CompanyBalanceTask.java

@@ -1,13 +1,15 @@
 package com.fs.his.task;
 
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
 import com.fs.company.service.ICompanyService;
-import com.fs.company.vo.RedPacketMoneyVO;
 import com.fs.course.service.BalanceRollbackErrorService;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 
-import java.util.List;
+import java.util.Date;
 
 /**
  * @description: 公司余额同步定时任务 (红包余额)
@@ -16,6 +18,7 @@ import java.util.List;
  * @version: 1.0
  */
 @Component("companyBalanceTask")
+@Slf4j
 public class CompanyBalanceTask {
 
 
@@ -58,19 +61,30 @@ public class CompanyBalanceTask {
      */
     public void initCompanyBalance() {
         balanceRollbackErrorService.initCompanyBalance();
-
     }
 
     /**
-     * @Description: 红包余额回滚(回滚的是客户没领取的红包),红包记录表中,两天没领取的记录不会再发送
+     * @Description: 优化成回滚前查询记录,一笔一笔回滚
      * @Param: 每天0点执行一次
      * @Return:
      * @Author xgb
      * @Date 2025/11/7 9:48
      */
-    public void rollbackRedPacketMoney() throws Exception {
-        // 这个地方真加的是company money字段 xgb 红包余额独立后这个方法弃用
-        companyService.rollbackRedPacketMoney();
+    public void rollbackRedPacketMoney(String time) throws Exception {
+        // 默认是前两天时间
+        String createSTime;
+        String createETime;
+        if (StringUtils.isNotBlank(time)) {
+            Date date = DateUtils.parseDate(time);
+            createSTime = time+" 00:00:00";
+            createETime = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD,DateUtils.addDays(date, 1))+" 00:00:00";
+        } else {
+            createSTime = DateUtils.parseDateToStr( DateUtils.YYYY_MM_DD,DateUtils.addDays(new Date(), -2))+" 00:00:00";
+            createETime = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD,DateUtils.addDays(new Date(), -1))+" 00:00:00";
+        }
+
+        // 这个地方真加的是company money字段 xgb
+        companyService.rollbackRedPacketMoney(createSTime, createETime);
     }
 
     /**
@@ -86,6 +100,35 @@ public class CompanyBalanceTask {
 
 
 
+    /**
+     * @Description: 更具批次号查询转账结果
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/12/25 10:31
+     */
+    public void checkMchTransferStatus(String outBatchNo,String companyIdStr) {
+        Long companyId=Long.parseLong(companyIdStr);
+        String result=companyService.checkMchTransferStatus(outBatchNo,companyId);
+        log.info("查询商户转账结果:{}",result);
+    }
+
+
+
+    /**
+     * @Description: 更具批次号查询转账结果
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/12/25 10:31
+     */
+    public void checkMchTransferStatusByBatchID(String batchId,String companyIdStr) {
+        Long companyId=Long.parseLong(companyIdStr);
+        R result=companyService.checkMchTransferStatusByBatchID(batchId,companyId);
+        log.info("查询商户转账结果:{}",result);
+    }
+
+
 
 
 }

+ 1 - 0
fs-admin/src/main/java/com/fs/his/task/Task.java

@@ -1146,6 +1146,7 @@ public class Task {
                 V2TradePaymentScanpayQueryRequest request = new V2TradePaymentScanpayQueryRequest();
                 request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
                 request.setOrgHfSeqId(payment.getTradeNo());
+                request.setAppId(payment.getAppId());
                 HuiFuQueryOrderResult o = null;
                 try {
                     o = huiFuService.queryOrder(request);

+ 2 - 2
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreAfterSalesScrmController.java

@@ -100,7 +100,7 @@ public class FsStoreAfterSalesScrmController extends BaseController
             return AjaxResult.error("请筛选数据导出");
         }
 
-        List<FsStoreAfterSalesVO> list = fsStoreAfterSalesService.selectFsStoreAfterSalesListVO(fsStoreAfterSales);
+        List<FsStoreAfterSalesVO> list = fsStoreAfterSalesService.selectFsStoreAfterSalesListVOExport(fsStoreAfterSales);
         if("北京卓美".equals(signProjectName)){
             List<FsStoreOrderItemExportRefundZMVO> zmvoList = list.stream()
                     .map(vo -> {
@@ -123,7 +123,7 @@ public class FsStoreAfterSalesScrmController extends BaseController
                             zmvo.setRealName(vo.getUserName());
                             zmvo.setUserPhone(vo.getUserPhone());
                             zmvo.setUserAddress(vo.getUserAddress());
-                            zmvo.setCreateTime(vo.getCreateTime());
+                            zmvo.setCreateTime(vo.getOrderCreateTime());
                             zmvo.setPayTime(vo.getOrderPayTime());
                             zmvo.setDeliverySn(vo.getOrderDeliverySn());
                             zmvo.setDeliveryName(vo.getOrderDeliveryName());

+ 3 - 1
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreHealthOrderScrmController.java

@@ -106,8 +106,10 @@ public class FsStoreHealthOrderScrmController extends BaseController {
         if (list != null) {
             LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
             for (FsStoreOrderVO vo : list) {
-                if(vo.getPhone()!=null){
+                if(StringUtils.isNotEmpty(vo.getPhone())){
                     vo.setPhone(vo.getPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                }
+                if (StringUtils.isNotEmpty(vo.getUserPhone())){
                     vo.setUserPhone(vo.getUserPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
                 }
                 if (CloudHostUtils.hasCloudHostName("康年堂")){

+ 18 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java

@@ -1,6 +1,7 @@
 package com.fs.hisStore.controller;
 
 import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
@@ -206,6 +207,8 @@ public class FsStoreOrderScrmController extends BaseController {
             for (FsStoreOrderVO vo : list) {
                 if(vo.getPhone()!=null){
                     vo.setPhone(vo.getPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                }
+                if(ObjectUtil.isNotEmpty(vo.getUserPhone())){
                     vo.setUserPhone(vo.getUserPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
                 }
                 if (CloudHostUtils.hasCloudHostName("康年堂")){
@@ -1119,6 +1122,21 @@ public class FsStoreOrderScrmController extends BaseController {
         return R.ok();
     }
 
+    @ApiOperation("批量审核订单")
+    @Log(title = "订单管理", businessType = BusinessType.UPDATE)
+    @PreAuthorize("@ss.hasPermi('store:storeOrder:batchAudit')")
+    @PostMapping("/batchAudit")
+    public R batchAuditOrder(@Validated @RequestBody FsStoreOrderBatchAuditParam param) {
+        if (param.getOrderIds() == null || param.getOrderIds().isEmpty()) {
+            return R.error("订单ID列表不能为空");
+        }
+        if (param.getIsAudit() == null) {
+            return R.error("审核状态不能为空");
+        }
+        int count = fsStoreOrderService.batchAuditOrder(param);
+        return R.ok("成功审核 " + count + " 条订单");
+    }
+
     private FsStoreOrderDf getDFInfo(String loginAccount) {
         //查询订单账户 判断是否存在该订单账户
         List<FsDfAccount> erpAccounts = fsDfAccountService.selectFsDfAccountList(null);

+ 61 - 12
fs-admin/src/main/java/com/fs/hisStore/controller/FsStorePaymentScrmController.java

@@ -1,5 +1,6 @@
 package com.fs.hisStore.controller;
 
+import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.alipay.api.AlipayApiException;
@@ -12,15 +13,20 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.CloudHostUtils;
+import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.ParseUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.common.utils.spring.SpringUtils;
 import com.fs.company.service.ICompanyService;
 import com.fs.config.cloud.CloudHostProper;
+import com.fs.course.domain.FsCoursePlaySourceConfig;
+import com.fs.course.service.IFsCoursePlaySourceConfigService;
 import com.fs.his.domain.FsHfpayConfig;
+import com.fs.his.domain.FsPayConfig;
+import com.fs.his.domain.MerchantAppConfig;
 import com.fs.his.mapper.FsHfpayConfigMapper;
-import com.fs.hisStore.domain.FsPayConfigScrm;
+import com.fs.his.service.IMerchantAppConfigService;
 import com.fs.huifuPay.domain.HuiFuQueryOrderResult;
 import com.fs.huifuPay.domain.HuiFuRefundResult;
 import com.fs.huifuPay.sdk.opps.core.request.V2TradePaymentScanpayQueryRequest;
@@ -32,6 +38,13 @@ import com.fs.hisStore.service.IFsStoreOrderScrmService;
 import com.fs.hisStore.service.IFsStorePaymentScrmService;
 import com.fs.hisStore.vo.FsStorePaymentVO;
 import com.fs.system.service.ISysConfigService;
+import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest;
+import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
+import com.github.binarywang.wxpay.bean.result.WxPayRefundQueryResult;
+import com.github.binarywang.wxpay.bean.result.WxPayRefundResult;
+import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.WxPayService;
 import com.ijpay.alipay.AliPayApi;
 import com.ijpay.alipay.AliPayApiConfig;
 import com.ijpay.alipay.AliPayApiConfigKit;
@@ -63,10 +76,16 @@ public class FsStorePaymentScrmController extends BaseController
     private IFsStorePaymentScrmService fsStorePaymentService;
     @Autowired
     private AliPayBean aliPayBean;
-
+    @Autowired
+    private WxPayService wxPayService;
     @Autowired
     private ICompanyService companyService;
-
+    @Autowired
+    private IFsCoursePlaySourceConfigService fsCoursePlaySourceConfigService;
+    @Autowired
+    private IMerchantAppConfigService merchantAppConfigService;
+    @Autowired
+    IFsStorePaymentScrmService paymentService;
     @Autowired
     private IFsStoreOrderScrmService orderService;
     @Autowired
@@ -135,6 +154,7 @@ public class FsStorePaymentScrmController extends BaseController
             V2TradePaymentScanpayQueryRequest request = new V2TradePaymentScanpayQueryRequest();
             request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
             request.setOrgHfSeqId(payment.getTradeNo());
+            request.setAppId(payment.getAppId());
             HuiFuQueryOrderResult o = null;
             try {
                 o = huiFuService.queryOrder(request);
@@ -193,16 +213,14 @@ public class FsStorePaymentScrmController extends BaseController
         }
 
         if(payment.getPayTypeCode().equals("weixin")){
-            String json;
-//            if (CloudHostUtils.hasCloudHostName("康年堂")){
-                json = configService.selectConfigByKey("his.pay");
-//            } else {
-//                json = configService.selectConfigByKey("store.pay");
-//            }
-            if (StringUtils.isBlank(json)) {
+            FsCoursePlaySourceConfig fsCoursePlaySourceConfig = fsCoursePlaySourceConfigService.selectCoursePlaySourceConfigByAppId(payment.getAppId());
+            MerchantAppConfig merchantAppConfig = merchantAppConfigService.selectMerchantAppConfigById(fsCoursePlaySourceConfig.getMerchantConfigId());
+
+            if (ObjectUtil.isEmpty(merchantAppConfig)) {
                 return R.error("缺少支付相关配置");
             }
-            FsPayConfigScrm fsPayConfig = JSON.parseObject(json, FsPayConfigScrm.class);
+            FsPayConfig fsPayConfig = JSON.parseObject(merchantAppConfig.getDataJson(), FsPayConfig.class);
+
             if (payment.getPayMode()!=null&&payment.getPayMode().equals("hf")){
                 String huifuId="";
                 FsHfpayConfigMapper fsHfpayConfigMapper = SpringUtils.getBean(FsHfpayConfigMapper.class);
@@ -228,6 +246,7 @@ public class FsStorePaymentScrmController extends BaseController
                 extendInfoMap.put("org_party_order_id", payment.getBankSerialNo());
                 request.setExtendInfo(extendInfoMap);
                 logger.info("请求参数:"+request);
+                request.setAppId(payment.getAppId());
                 HuiFuRefundResult refund = huiFuService.refund(request);
                 logger.info("退款:"+refund);
                 if((refund.getResp_code().equals("00000000")||refund.getResp_code().equals("00000100"))&&(refund.getTrans_stat().equals("S")||refund.getTrans_stat().equals("P"))){
@@ -247,7 +266,37 @@ public class FsStorePaymentScrmController extends BaseController
 
             }
             else if (payment.getPayMode()!=null&&payment.getPayMode().equals("wx")){
-
+                WxPayConfig payConfig = new WxPayConfig();
+                payConfig.setAppId(fsCoursePlaySourceConfig.getAppid());
+                payConfig.setMchId(fsPayConfig.getWxMchId());
+                payConfig.setMchKey(fsPayConfig.getWxMchKey());
+                payConfig.setKeyPath(fsPayConfig.getKeyPath());
+                payConfig.setSubAppId(org.apache.commons.lang3.StringUtils.trimToNull(null));
+                payConfig.setSubMchId(org.apache.commons.lang3.StringUtils.trimToNull(null));
+                wxPayService.setConfig(payConfig);
+                WxPayRefundRequest refundRequest = new WxPayRefundRequest();
+                refundRequest.setOutTradeNo("payment-"+payment.getPayCode());
+                refundRequest.setOutRefundNo("payment-"+payment.getPayCode());
+                refundRequest.setTotalFee(WxPayUnifiedOrderRequest.yuanToFen(payment.getPayMoney().toString()));
+                refundRequest.setRefundFee(WxPayUnifiedOrderRequest.yuanToFen(fsStorePayment.getRefundMoney().toString()));
+                try {
+                    WxPayRefundResult refundResult = wxPayService.refund(refundRequest);
+                    WxPayRefundQueryResult refundQueryResult = wxPayService.refundQuery("", refundResult.getOutTradeNo(), refundResult.getOutRefundNo(), refundResult.getRefundId());
+                    if(refundQueryResult!=null&&refundQueryResult.getResultCode().equals("SUCCESS")){
+                        FsStorePaymentScrm paymentMap=new FsStorePaymentScrm();
+                        paymentMap.setPaymentId(payment.getPaymentId());
+                        paymentMap.setStatus(-1);
+                        paymentMap.setRefundTime(DateUtils.getNowDate());
+                        paymentMap.setRefundMoney(fsStorePayment.getRefundMoney());
+                        paymentService.updateFsStorePayment(paymentMap);
+                        return R.ok("退款成功");
+                    }
+                    else {
+                        return R.error("退款请求失败"+refundQueryResult.getErrCodeDes());
+                    }
+                } catch (WxPayException e) {
+                    return R.error("退款请求失败"+e.getErrCodeDes());
+                }
             }
 
             //小雨点退款

+ 2 - 1
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreStatisticsScrmController.java

@@ -19,6 +19,7 @@ import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.Collectors;
@@ -76,7 +77,7 @@ public class FsStoreStatisticsScrmController extends BaseController
             List<JSONObject> jsonObjectList = storeOrderService.selectFsStoreOrderCounts(timeEntity.toMap());
             List<String> dates = jsonObjectList.stream().map(jsonObject -> jsonObject.getString("type")).collect(Collectors.toList());
             List<Integer> orderCount = jsonObjectList.stream().map(jsonObject -> jsonObject.getInteger("orderCount")).collect(Collectors.toList());
-            List<Integer> payPrice = jsonObjectList.stream().map(jsonObject -> jsonObject.getInteger("payPrice")).collect(Collectors.toList());
+            List<BigDecimal> payPrice = jsonObjectList.stream().map(jsonObject -> jsonObject.getBigDecimal("payPrice")).collect(Collectors.toList());
             //表格数据
             List<FsStoreOrderCountsVO> tableData = storeOrderService.selectFsStoreOrderCountsByDept(timeEntity.toMap(),param.getDeptId());
             return R.ok().put("dates",dates).put("orderCount",orderCount).put("payPrice",payPrice).put("tableData",tableData);

+ 85 - 3
fs-admin/src/main/java/com/fs/hisStore/task/LiveTask.java

@@ -164,6 +164,90 @@ public class LiveTask {
     @Autowired
     private FsJstAftersalePushScrmService fsJstAftersalePushScrmService;
 
+    /**
+     * 查询被拆分的订单,然后查询拆分订单的物流信息
+     */
+    public void querySplitOrderDelivery() {
+        try {
+            // 查询状态为6(被拆分)的订单
+            List<LiveOrder> splitOrders = liveOrderMapper.selectSplitOrders();
+            if (splitOrders == null || splitOrders.isEmpty()) {
+                log.debug("没有找到被拆分的订单");
+                return;
+            }
+
+            log.info("找到 {} 个被拆分的订单,开始查询拆分订单的物流信息", splitOrders.size());
+
+            IErpOrderService erpOrderService = getErpOrderService();
+            if (erpOrderService == null) {
+                log.warn("ERP服务未配置,无法查询拆分订单物流信息");
+                return;
+            }
+
+            for (LiveOrder splitOrder : splitOrders) {
+                try {
+                    // 查询该订单的所有拆分订单(通过原订单号查询)
+                    List<LiveOrder> childOrders = liveOrderMapper.selectChildOrdersByParentOrderCode(splitOrder.getOrderCode());
+                    if (childOrders == null || childOrders.isEmpty()) {
+                        log.debug("订单 {} 没有找到拆分订单", splitOrder.getOrderCode());
+                        continue;
+                    }
+
+                    // 遍历拆分订单,查询物流信息
+                    for (LiveOrder childOrder : childOrders) {
+                        if (StringUtils.isEmpty(childOrder.getExtendOrderId())) {
+                            log.debug("拆分订单 {} 没有扩展订单ID,跳过", childOrder.getOrderCode());
+                            continue;
+                        }
+
+                        // 查询ERP订单信息
+                        ErpOrderQueryRequert request = new ErpOrderQueryRequert();
+                        request.setCode(childOrder.getExtendOrderId());
+                        ErpOrderQueryResponse response = erpOrderService.getLiveOrder(request);
+
+                        if (!response.getSuccess()) {
+                            if ("429".equals(response.getCode())) {
+                                log.warn("ERP接口限流,停止查询");
+                                break;
+                            }
+                            log.warn("查询拆分订单物流信息失败, orderCode={}, error={}", childOrder.getOrderCode(), response.getCode());
+                            continue;
+                        }
+
+                        // 更新物流信息
+                        if (response.getOrders() != null && !response.getOrders().isEmpty()) {
+                            for (ErpOrderQuery orderQuery : response.getOrders()) {
+                                if (orderQuery.getDeliverys() != null && !orderQuery.getDeliverys().isEmpty()) {
+                                    for (ErpDeliverys delivery : orderQuery.getDeliverys()) {
+                                        if (delivery.getDelivery() && StringUtils.isNotEmpty(delivery.getMail_no())) {
+                                            // 更新订单物流信息
+                                            childOrder.setDeliverySn(delivery.getMail_no());
+                                            childOrder.setDeliveryCode(delivery.getExpress_code());
+                                            childOrder.setDeliveryName(delivery.getExpress_name());
+                                            if (childOrder.getStatus() == 2) { // 待发货状态
+                                                childOrder.setStatus(3); // 更新为待收货
+                                            }
+                                            childOrder.setUpdateTime(new Date());
+                                            liveOrderMapper.updateLiveOrder(childOrder);
+                                            log.info("拆分订单物流信息已更新, orderCode={}, deliverySn={}, expressName={}",
+                                                    childOrder.getOrderCode(), delivery.getMail_no(), delivery.getExpress_name());
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                } catch (Exception e) {
+                    log.error("处理拆分订单物流信息异常, orderCode={}", splitOrder.getOrderCode(), e);
+                }
+            }
+
+            log.info("拆分订单物流信息查询完成");
+        } catch (Exception e) {
+            log.error("查询拆分订单物流信息任务异常", e);
+        }
+    }
+
     // 聚水潭 推送售后信息
     public void pushJst(){
         fsJstAftersalePushScrmService.pushJst();
@@ -182,6 +266,7 @@ public class LiveTask {
                 V2TradePaymentScanpayQueryRequest request = new V2TradePaymentScanpayQueryRequest();
                 request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
                 request.setOrgHfSeqId(payment.getTradeNo());
+                request.setAppId(payment.getAppId());
                 HuiFuQueryOrderResult o = null;
                 try {
                     o = huiFuService.queryOrder(request);
@@ -254,9 +339,6 @@ public class LiveTask {
             request.setCode(order.getExtendOrderId());
             IErpOrderService erpOrderService = getErpOrderService();
             ErpOrderQueryResponse response = erpOrderService.getLiveOrder(request);
-            if(!response.getSuccess() && "429".equals(response.getCode())){
-                break;
-            }
             if (erpOrderService != dfOrderService) {
                 if (response.getOrders() != null && !response.getOrders().isEmpty()) {
                     for (ErpOrderQuery orderQuery : response.getOrders()) {

+ 38 - 3
fs-admin/src/main/java/com/fs/hisStore/task/MallStoreTask.java

@@ -34,7 +34,11 @@ import com.fs.hisStore.mapper.FsStorePaymentScrmMapper;
 import com.fs.hisStore.mapper.FsStoreProductAttrValueScrmMapper;
 import com.fs.hisStore.param.*;
 import com.fs.hisStore.service.*;
+import com.fs.huifuPay.domain.HuiFuQueryOrderResult;
+import com.fs.huifuPay.sdk.opps.core.request.V2TradePaymentScanpayQueryRequest;
+import com.fs.huifuPay.service.HuiFuService;
 import com.fs.live.domain.LiveOrder;
+import com.fs.live.domain.LiveOrderPayment;
 import com.fs.pay.pay.dto.OrderQueryDTO;
 import com.fs.pay.service.IPayService;
 import com.fs.store.config.StoreConfig;
@@ -50,6 +54,7 @@ import org.springframework.stereotype.Component;
 
 import java.math.BigDecimal;
 import java.text.ParseException;
+import java.text.SimpleDateFormat;
 import java.time.LocalTime;
 import java.util.ArrayList;
 import java.util.Date;
@@ -160,6 +165,39 @@ public class MallStoreTask
 
     //@Autowired
     //private IFsUserOnlineStateService fsUserOnlineStateService;
+    @Autowired
+    private HuiFuService huiFuService;
+
+    // 订单银行回调数据丢失补偿
+    public void recoveryBankOrder() {
+        // 查询出来最近30分钟的订单 待支付 未退款
+        List<FsStoreOrderScrm> list = fsStoreOrderMapper.selectBankOrder();
+        if(list == null || list.isEmpty()) return;
+        for (FsStoreOrderScrm order : list) {
+            List<FsStorePaymentScrm> orderPayments = fsStorePaymentMapper.selectNoPayByOrderId(order.getId());
+            if(orderPayments == null || orderPayments.isEmpty()) continue;
+            for (FsStorePaymentScrm payment : orderPayments) {
+                V2TradePaymentScanpayQueryRequest request = new V2TradePaymentScanpayQueryRequest();
+                request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
+                request.setOrgHfSeqId(payment.getTradeNo());
+                request.setAppId(payment.getAppId());
+                HuiFuQueryOrderResult o = null;
+                try {
+                    o = huiFuService.queryOrder(request);
+                } catch (Exception e) {
+                    log.error("查询失败:"+e.getMessage());
+                    continue;
+                }
+                log.info("汇付返回"+o);
+                if ("00000000".equals(o.getResp_code()) && "S".equals(o.getTrans_stat())) {
+                    String[] orderSpilt=o.getOrg_req_seq_id().split("-");
+                    if ("store".equals(orderSpilt[0])) {
+                        orderService.payConfirm(1, null, orderSpilt[1], o.getOrg_hf_seq_id(), o.getOut_trans_id(), o.getParty_order_id());
+                    }
+                }
+            }
+        }
+    }
 
     public void PushErp() throws ParseException {
         List<Long> ids;
@@ -229,9 +267,6 @@ public class MallStoreTask
             request.setCode(order.getExtendOrderId());
             IErpOrderService erpOrderService = getErpOrderService();
             ErpOrderQueryResponse response = erpOrderService.getScrmOrder(request);
-            if(!response.getSuccess() && "429".equals(response.getCode())){
-                break;
-            }
             if (erpOrderService != dfOrderService) {
                 if(response.getOrders()!=null && !response.getOrders().isEmpty()){
                     for(ErpOrderQuery orderQuery : response.getOrders()){

+ 1 - 1
fs-admin/src/main/java/com/fs/live/controller/LiveAfterSalesController.java

@@ -114,7 +114,7 @@ public class LiveAfterSalesController extends BaseController
     {
         PageHelper.clearPage();
         PageHelper.startPage(1, 10000, "");
-        List<LiveAfterSalesVo> list = liveAfterSalesService.selectLiveAfterSalesVoList(liveAfterSales);
+        List<LiveAfterSalesVo> list = liveAfterSalesService.selectLiveAfterSalesVoListExport(liveAfterSales);
         if("北京卓美".equals(signProjectName)){
             List<FsStoreOrderItemExportRefundZMVO> zmvoList = list.stream()
                     .map(vo -> {

+ 7 - 4
fs-admin/src/main/java/com/fs/live/controller/OrderController.java

@@ -51,7 +51,7 @@ public class OrderController extends BaseController
     @Autowired
     private IMergedOrderService mergedOrderService;
     // 设置最大导出数量限制为20000条
-    private static final int maxExportCount = 20000;
+    private static final int maxExportCount = 50000;
 
 
     @Autowired
@@ -88,6 +88,7 @@ public class OrderController extends BaseController
         // 先查询数据,限制查询20001条,用于判断是否超过限制
         PageHelper.startPage(1, maxExportCount + 1);
         List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+        list = list.stream().filter(item -> StringUtils.isNotEmpty(item.getBankTransactionId())).collect(Collectors.toList());
         
         // 如果查询结果超过20000条,返回错误提示
         if (list != null && list.size() > maxExportCount) {
@@ -126,6 +127,7 @@ public class OrderController extends BaseController
         // 先查询数据,限制查询20001条,用于判断是否超过限制
         PageHelper.startPage(1, maxExportCount + 1);
         List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+        list = list.stream().filter(item -> StringUtils.isNotEmpty(item.getBankTransactionId())).collect(Collectors.toList());
 
         // 如果查询结果超过20000条,返回错误提示
         if (list != null && list.size() > maxExportCount) {
@@ -235,20 +237,21 @@ public class OrderController extends BaseController
             MergedOrderExportVO exportVO = new MergedOrderExportVO();
             
             // 订单基本信息(参考 FsStoreOrderItemExportVO 的顺序)
+            exportVO.setOrderTypeName(vo.getOrderTypeName());
             exportVO.setOrderCode(vo.getOrderCode());
             exportVO.setStatus(vo.getStatus() != null ? String.valueOf(vo.getStatus()) : null);
             exportVO.setUserId(vo.getUserId());
             
             // 产品信息
-            exportVO.setProductName(vo.getProductName());
+            exportVO.setProductName(StringUtils.isEmpty(vo.getProductName()) ? "产品被删除" : vo.getProductName());
             exportVO.setBarCode(vo.getBarCode());
             exportVO.setProductSpec(StringUtils.isEmpty(vo.getProductSpec()) ? "默认" : vo.getProductSpec());
             exportVO.setTotalNum(vo.getTotalNum());
             exportVO.setPrice(vo.getTotalPrice()); // 产品价格使用订单总价
-            exportVO.setCost(vo.getCost());
+            exportVO.setCost(vo.getCost() != null ? vo.getCost() : BigDecimal.ZERO);
             exportVO.setFPrice(vo.getCost() != null ? vo.getCost().multiply(BigDecimal.valueOf(vo.getTotalNum())) : BigDecimal.ZERO); // 结算价,合并订单暂无此字段
             exportVO.setPayPostage(vo.getPayDelivery());
-            exportVO.setCateName(vo.getCateName());
+            exportVO.setCateName(StringUtils.isEmpty(vo.getCateName()) ? "产品被删除" : vo.getCateName());
             // 收货信息
             exportVO.setRealName(vo.getRealName());
             if (isPlainText) {

+ 4 - 4
fs-admin/src/main/java/com/fs/qw/controller/QwCompanyController.java

@@ -87,7 +87,7 @@ public class QwCompanyController extends BaseController
      * 导出企微主体列表
      */
     @PreAuthorize("@ss.hasPermi('qw:qwCompany:export')")
-    @Log(title = "企微主体", businessType = BusinessType.EXPORT)
+    @Log(title = "企微主体", businessType = BusinessType.EXPORT, isStoreLog = true)
     @GetMapping("/export")
     public AjaxResult export(QwCompany qwCompany)
     {
@@ -110,7 +110,7 @@ public class QwCompanyController extends BaseController
      * 新增企微主体
      */
     @PreAuthorize("@ss.hasPermi('qw:qwCompany:add')")
-    @Log(title = "企微主体", businessType = BusinessType.INSERT)
+    @Log(title = "企微主体", businessType = BusinessType.INSERT, isStoreLog = true)
     @PostMapping
     public AjaxResult add(@RequestBody QwCompany qwCompany)
     {
@@ -125,7 +125,7 @@ public class QwCompanyController extends BaseController
      * 修改企微主体
      */
     @PreAuthorize("@ss.hasPermi('qw:qwCompany:edit')")
-    @Log(title = "企微主体", businessType = BusinessType.UPDATE)
+    @Log(title = "企微主体", businessType = BusinessType.UPDATE, isStoreLog = true)
     @PutMapping
     public AjaxResult edit(@RequestBody QwCompany qwCompany)
     {
@@ -136,7 +136,7 @@ public class QwCompanyController extends BaseController
      * 删除企微主体
      */
     @PreAuthorize("@ss.hasPermi('qw:qwCompany:remove')")
-    @Log(title = "企微主体", businessType = BusinessType.DELETE)
+    @Log(title = "企微主体", businessType = BusinessType.DELETE, isStoreLog = true)
 	@DeleteMapping("/{ids}")
     public AjaxResult remove(@PathVariable Long[] ids)
     {

+ 9 - 8
fs-admin/src/main/java/com/fs/qw/controller/QwSopTempController.java

@@ -58,6 +58,15 @@ public class QwSopTempController extends BaseController
     {
 
 //        List<QwSopTemp> list = qwSopTempService.selectQwSopTempList(qwSopTemp);
+
+        if(qwSopTemp.getPageNum() == null) {
+            qwSopTemp.setPageNum(1);
+        }
+        if(qwSopTemp.getPageSize() == null) {
+            qwSopTemp.setPageSize(10);
+        }
+
+        PageHelper.startPage(qwSopTemp.getPageNum(), qwSopTemp.getPageSize());
         List<QwSopTemp> list = qwSopTempService.selectQwSopTempListNew(qwSopTemp);
         // 收集所有需要查询的用户ID
         Set<Long> userIds = list.stream()
@@ -87,14 +96,6 @@ public class QwSopTempController extends BaseController
             });
         }
 
-        if(qwSopTemp.getPageNum() == null) {
-            qwSopTemp.setPageNum(1);
-        }
-        if(qwSopTemp.getPageSize() == null) {
-            qwSopTemp.setPageSize(10);
-        }
-
-        PageHelper.startPage(qwSopTemp.getPageNum(), qwSopTemp.getPageSize());
 
         return R.ok().put("data",new PageInfo<>(list));
     }

+ 698 - 84
fs-admin/src/main/java/com/fs/qw/controller/QwUserController.java

@@ -1,32 +1,39 @@
 package com.fs.qw.controller;
 
+import cn.hutool.core.util.ObjectUtil;
 import com.alibaba.fastjson.JSON;
 import com.fs.common.annotation.Log;
 import com.fs.common.annotation.RepeatSubmit;
+import com.fs.common.constant.Constants;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.model.LoginUser;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
-import com.fs.common.utils.ServletUtils;
-import com.fs.company.domain.Company;
+import com.fs.common.exception.ServiceException;
+import com.fs.common.exception.user.UserPasswordNotMatchException;
+import com.fs.common.utils.MessageUtils;
+import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.mapper.CompanyUserMapper;
 import com.fs.company.service.ICompanyUserService;
+import com.fs.company.service.impl.CompanyDeptServiceImpl;
+import com.fs.fastGpt.domain.FastGptRole;
+import com.fs.fastGpt.mapper.FastGptRoleMapper;
+import com.fs.framework.manager.AsyncManager;
+import com.fs.framework.manager.factory.AsyncFactory;
 import com.fs.qw.domain.QwExternalContact;
-import com.fs.qw.domain.QwExternalContactTransferCompanyAudit;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwCompanyMapper;
 import com.fs.qw.mapper.QwExternalContactMapper;
-import com.fs.qw.param.QwFsUserParam;
-import com.fs.qw.param.QwUserBingParam;
-import com.fs.qw.param.QwUserListParam;
+import com.fs.qw.param.*;
 import com.fs.qw.service.IQwDeptService;
 import com.fs.qw.service.IQwExternalContactTransferCompanyAuditService;
 import com.fs.qw.service.IQwUserService;
 import com.fs.qw.vo.QwOptionsVO;
 import com.fs.qw.vo.QwUserVO;
+import com.fs.qw.vo.UpdateSendTypeVo;
 import com.fs.qwApi.domain.QwExternalContactAllListResult;
 import com.fs.qwApi.domain.inner.ExternalContact;
 import com.fs.qwApi.domain.inner.ExternalContactInfo;
@@ -34,14 +41,18 @@ import com.fs.qwApi.domain.inner.FollowInfo;
 import com.fs.qwApi.param.QwExternalListParam;
 import com.fs.qwApi.service.QwApiService;
 import com.fs.voice.utils.StringUtil;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
 import org.springframework.web.bind.annotation.*;
 
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
+import javax.annotation.Resource;
+import java.util.*;
 import java.util.stream.Collectors;
 
 /**
@@ -77,25 +88,622 @@ public class QwUserController extends BaseController {
     @Autowired
     private QwExternalContactMapper qwExternalContactMapper;
 
-    @GetMapping("/getQwUserAll")
-    public AjaxResult getQwUserAll(){
-        return AjaxResult.success(qwUserService.getQwUserAll());
+    @Autowired
+    private CompanyDeptServiceImpl companyDeptService;
+
+    @Resource
+    private AuthenticationManager authenticationManager;
+    @Autowired
+    private FastGptRoleMapper fastGptRoleMapper;
+
+    /**
+     * 查询企微员工列表
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:staffList')")
+    @GetMapping("/staffList")
+    public TableDataInfo staffList(QwUserListParam qwUser) {
+
+        // 添加企微部门查询条件
+        Long deptId = qwUser.getDeptId();
+        if(deptId!=null && qwUser.getCorpId()!=null){
+            List<Long> qwDeptIdList = new ArrayList<>();
+            if (deptId!=null){
+                qwDeptIdList.add(deptId);
+            }
+            // 本部门的下级部门
+            List<Long> deptList = qwUserService.selectDeptByParentId(deptId,qwUser.getCorpId());
+            if (!deptList.isEmpty()){
+                qwDeptIdList.addAll(deptList);
+            }
+            qwUser.setQwDeptIdList(qwDeptIdList);
+        }
+        startPage();
+        List<QwUserVO> list = qwUserService.selectQwUserListStaffVO(qwUser);
+        return getDataTable(list);
     }
 
     /**
-     * 获取企微信息
-     * **/
-    @GetMapping("/getQwUserInfo")
-    public R getQwUserInfo(QwFsUserParam param){
-        return R.ok().put("data",qwUserService.getQwUserInfo(param));
+     * 查询企微员工列表
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:staffListPost')")
+    @PostMapping("/staffListPost")
+    public TableDataInfo staffListPost(@RequestBody QwUserListParam qwUser) {
+        // 添加企微部门查询条件
+        Long deptId = qwUser.getDeptId();
+        if(deptId!=null && qwUser.getCorpId()!=null){
+            List<Long> qwDeptIdList = new ArrayList<>();
+            qwDeptIdList.add(deptId);
+            // 本部门的下级部门
+            List<Long> deptList = qwUserService.selectDeptByParentId(deptId,qwUser.getCorpId());
+            if (!deptList.isEmpty()){
+                qwDeptIdList.addAll(deptList);
+            }
+            qwUser.setQwDeptIdList(qwDeptIdList);
+        }
+        startPage();
+        List<QwUserVO> list = qwUserService.selectQwUserListStaffVO(qwUser);
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询我的企微员工列表
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:myStaffList')")
+    @GetMapping("/myStaffList")
+    public TableDataInfo myStaffList(QwUserListParam qwUser) {
+        startPage();
+        qwUser.setCompanyId(qwUser.getCompanyId());
+        List<QwUserVO> list = qwUserService.selectQwUserListStaffVO(qwUser);
+        return getDataTable(list);
+    }
+
+
+    /**
+     * 导出企微员工列表
+     * @param qwUser
+     * @return AjaxResult
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:export')")
+    @Log(title = "企微员工", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportStaff")
+    public AjaxResult export(QwUserListParam qwUser) {
+        qwUser.setCompanyId(qwUser.getCompanyId());
+        List<QwUserVO> list = qwUserService.selectQwUserListStaffVO(qwUser);
+        ExcelUtil<QwUserVO> util = new ExcelUtil<QwUserVO>(QwUserVO.class);
+        return util.exportExcel(list, "企微员工数据");
     }
 
-   @GetMapping("/getMyQwCompanyList")
-    public R getMyQwCompanyList()
+    /**
+     * 导出企微用户列表
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:export')")
+    @Log(title = "企微用户", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(QwUser qwUser) {
+        qwUser.setCompanyId(qwUser.getCompanyId());
+        List<QwUser> list = qwUserService.selectQwUserList(qwUser);
+        ExcelUtil<QwUser> util = new ExcelUtil<QwUser>(QwUser.class);
+        return util.exportExcel(list, "企微用户数据");
+    }
+
+
+    /**
+     * 查询我的部门 企业微信员工列表
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:myDepartList')")
+    @GetMapping("/myDepartList")
+    public TableDataInfo myDepartList(QwUserListParam qwUser)
     {
-        List<QwOptionsVO> list = qwUserService.selectQwCompanyListOptionsVOAll();
+
+        qwUser.setCompanyId(qwUser.getCompanyId());
+//        qwUser.setUserType(loginUser.getUser().getUserType());
+        List<Long> combinedList = new ArrayList<>();
+        //本部门
+        Long deptId = getLoginUser().getUser().getDeptId();
+        if (deptId!=null){
+            combinedList.add(deptId);
+        }
+        //本部门的下级部门
+        List<Long> deptList = companyDeptService.selectCompanyDeptByParentId(deptId);
+        if (!deptList.isEmpty()){
+            combinedList.addAll(deptList);
+        }
+
+        // 添加企微部门查询条件
+        Long qwDeptId = qwUser.getDeptId();
+        if(qwDeptId!=null && qwUser.getCorpId()!=null){
+            List<Long> qwDeptIdList = new ArrayList<>();
+            if (qwDeptId!=null){
+                qwDeptIdList.add(qwDeptId);
+            }
+            // 本部门的下级部门
+            List<Long> qwDeptList = qwUserService.selectDeptByParentId(qwDeptId,qwUser.getCorpId());
+            if (!qwDeptList.isEmpty()){
+                qwDeptIdList.addAll(qwDeptList);
+            }
+            qwUser.setQwDeptIdList(qwDeptIdList);
+        }
+
+
+        qwUser.setCuDeptIdList(combinedList);
+//        qwUser.setUserType(loginUser.getUser().getUserType());
+
+        startPage();
+        List<QwUserVO> list = qwUserService.selectQwUserListStaffVO(qwUser);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+    @PostMapping("/loginQwIpad")
+    public R loginQwIpad(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.loginQwIpad(loginParam);
+    }
+
+
+    /**
+     * 查询部门下的 企业微信账号
+     */
+    @PostMapping("/getQwUserByDept")
+    public R getQwUserByDept(@RequestBody QwUserByDeptParam deptParam){
+        deptParam.setCompanyId(deptParam.getCompanyId());
+        return R.ok().put("data",qwUserService.getQwUserByDept(deptParam)) ;
+    }
+
+
+
+
+    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+    @PostMapping("/getQwIpad")
+    @RepeatSubmit
+    public R getQwIpad(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.getQwIpad(loginParam);
+    }
+
+    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+    @PostMapping("/delQwIpad")
+    @RepeatSubmit
+    public R delQwIpad(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.delQwIpad(loginParam);
+    }
+
+    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+    @PostMapping("/qrCodeStatus")
+    public R qrCodeStatus(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.qrCodeStatus(loginParam);
+    }
+    //输入验证码
+    @PostMapping("/qrCodeVerify")
+    public R qrCodeVerify(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.qrCodeVerify(loginParam);
+    }
+
+    @PostMapping("/outLoginQwIpad")
+    public R outLoginQwIpad(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.outLoginQwIpad(loginParam);
+    }
+
+    @PostMapping("/twoCode")
+    public R twoCode(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.getTwoCode(loginParam);
+    }
+    @PostMapping("/twoCodeStatus")
+    public R TwoCodeStatus(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.getTwoCodeStatus(loginParam);
+    }
+
+    @PostMapping("/getQwIpadStatus")
+    public R getQwIpadStatus(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.getLoginQwIpadStatus(loginParam);
+    }
+    /**
+     * 直接授权key
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:authAppKey')")
+    @PostMapping("/authAppKey")
+    public R authAppKey(@RequestBody QwUser param){
+        return qwUserService.authAppKey(param);
+    }
+
+    /**
+     * 输入授权key
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:authAppKey')")
+    @PostMapping("/handleInputAuthAppKey")
+    public R handleInputAuthAppKey(@RequestBody QwUser param){
+        return qwUserService.handleInputAuthAppKey(param);
+    }
+
+
+    /**
+     * 登录企业微信(发起登录)
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+    @PostMapping("/loginQwCode")
+    public R loginQwCode(@RequestBody QwLoginParam loginParam){
+        return qwUserService.loginQwCode(loginParam);
+    }
+
+    /**
+     * 登录请求-刷新获取二维码
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+    @PostMapping("/loginQwCodeUrl")
+    public R loginQwCodeUrl(@RequestBody QwLoginParam loginParam){
+        return qwUserService.loginQwCodeUrl(loginParam);
+    }
+    /**
+     * 取redis里的登录二维码
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+    @PostMapping("/getQwCodeUrl")
+    public R getQwCodeUrl(@RequestBody QwLoginParam loginParam) throws InterruptedException {
+        return qwUserService.getQwCodeUrl(loginParam);
+    }
+
+    /**
+     * 登录企业微信(传输验证信息)
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+    @PostMapping("/loginQwCodeMsg")
+    public R loginQwCodeMsg(@RequestBody QwLoginParam loginParam){
+        return qwUserService.loginQwCodeMsg(loginParam);
+    }
+
+    /**
+     * 退出企业微信(退出插件)
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+    @PostMapping("/logoutQwLogout")
+    public R logoutQwLogout(@RequestBody QwLoginParam loginParam){
+        return qwUserService.logoutQwLogout(loginParam);
+    }
+
+//    /**
+//     * 企业微信(修改登录状态)
+//     */
+//    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+//    @PostMapping("/modifyLoginQwStatus")
+//    public R modifyLoginQwStatus(@RequestBody QwLoginParam loginParam){
+//        return qwUserService.modifyLoginQwStatus(loginParam);
+//    }
+//
+    /**
+     * 查询企业微信登录状态
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+    @PostMapping("/getLoginQwStatus")
+    public R getLoginQwStatus(@RequestBody QwLoginParam loginParam){
+        return qwUserService.getLoginQwStatus(loginParam);
+    }
+
+    @PutMapping
+    public AjaxResult updateUser(@RequestBody QwUser qwUser){
+        return toAjax(qwUserService.updateQwUser(qwUser));
+    }
+
+    /**
+     * 自动发课启用禁用
+     * @param qwUser
+     * @return
+     */
+    @PostMapping("/updateIsAuto")
+    @PreAuthorize("@ss.hasPermi('qw:user:isauto')")
+    public AjaxResult updateIsAuto(@RequestBody QwUser qwUser){
+        return toAjax(qwUserService.updateQwUser(qwUser));
+    }
+
+    /**
+     * 企业微信员工账号 绑定 云主机
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:loginIp')")
+    @Log(title = "绑定 云主机", businessType = BusinessType.INSERT)
+    @GetMapping("/qwBindCloudHost/{appKey}")
+    public R qwBindCloudHost(@PathVariable("appKey") String appKey){
+        return qwUserService.qwBindCloudHost(appKey);
+    }
+
+    /**
+     * 获取云主机的账密
+     *
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:cloudAP')")
+    @PostMapping("/selectCloudAP")
+    public R selectCloudAP(@RequestBody QwCloudAPParam param) throws Exception {
+        return qwUserService.selectCloudAP(param);
+    }
+
+    /**
+     * 根据销售账号密码 获取 他的所有企业微信账号以及云主机和账号密码
+     */
+    @PostMapping("/selectCloudByCompany")
+    public R selectCloudByCompany(@RequestBody QwCloudIPByCompanyParam param) throws Exception {
+
+        // 用户验证
+        Authentication authentication = null;
+        try
+        {
+            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
+            authentication = authenticationManager
+                    .authenticate(new UsernamePasswordAuthenticationToken(param.getCompanyAdmin(), param.getCompanyPassWord()));
+        }
+        catch (Exception e)
+        {
+            if (e instanceof BadCredentialsException)
+            {
+                AsyncManager.me().execute(AsyncFactory.recordLogininfor(param.getCompanyAdmin(), Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
+                throw new UserPasswordNotMatchException();
+            }
+            else
+            {
+                AsyncManager.me().execute(AsyncFactory.recordLogininfor(param.getCompanyAdmin(), Constants.LOGIN_FAIL, e.getMessage()));
+                throw new ServiceException(e.getMessage());
+            }
+        }
+        LoginUser loginUser=(LoginUser) authentication.getPrincipal();
+
+        return qwUserService.selectCloudByCompany(loginUser.getUser().getCompanyId(),loginUser.getUser().getUserId());
+    }
+
+
+    /**
+     * 企业微信员工账号 绑定 云主机
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:bindIp')")
+    @GetMapping("/qwBindCloudHostByIp/{appKey}/{IP}")
+    public R qwBindCloudHostByIp(@PathVariable("appKey") String appKey,@PathVariable("IP") String IP){
+        return qwUserService.qwBindCloudHostByIp(appKey,IP);
+    }
+
+    /**
+     * 企业微信员工账号 解除绑定 云主机
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:loginIpOut')")
+    @Log(title = "解除绑定 云主机", businessType = BusinessType.UPDATE)
+    @GetMapping("/qwUnbindCloudHost/{appKey}")
+    public R qwUnbindCloudHost(@PathVariable("appKey") String appKey){
+        return qwUserService.qwUnbindCloudHost(appKey);
+    }
+
+    /**
+     * 根据销售名称模糊查询
+     * @param qwUserName  名称
+     * @return  list
+     */
+    @GetMapping("/getQwUserListLikeName")
+    public R getQwUserListLikeName(@RequestParam(required = false) String qwUserName,
+                                   @RequestParam(required = false, defaultValue = "1") Integer pageNum,
+                                   @RequestParam(required = false, defaultValue = "10") Integer pageSize) {
+        Map<String,Object> params = new HashMap<>();
+        params.put("qwUserName", qwUserName);
+
+        PageHelper.startPage(pageNum, pageSize);
+        List<QwOptionsVO> qwUserList = companyUserService.selectQwUserListLikeName(params);
+        return R.ok().put("data", new PageInfo<>(qwUserList));
+    }
+
+    /**
+     * 查询企微用户列表
+     */
+//    @PreAuthorize("@ss.hasPermi('qw:user:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(QwUserListParam qwUser)
+    {
+        startPage();
+        qwUser.setCompanyId(qwUser.getCompanyId());
+        if (ObjectUtil.isNotEmpty(qwUser.getIsRemark())&&qwUser.getIsRemark().equals("1")){
+            qwUser.setCompanyUserId(getLoginUser().getUser().getUserId());
+        }else if (ObjectUtil.isNotEmpty(qwUser.getIsRemark())&&qwUser.getIsRemark().equals("2")){
+            qwUser.setDeptId(getLoginUser().getDeptId());
+            qwUser.setCorpId(null);
+        }
+
+        List<QwUserVO> list = qwUserService.selectQwUserListVO(qwUser);
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询企微用户列表
+     */
+
+    @GetMapping("/userList")
+    public TableDataInfo userList(QwUserListParam qwUser)
+    {
+        startPage();
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        qwUser.setCompanyId(loginUser.getCompany().getCompanyId());
+
+        List<QwUserVO> list = qwUserService.selectAllQwUserListVO(qwUser);
+        return getDataTable(list);
+    }
+    //    /**
+//     * 查询我的企微用户列表
+//     */
+//    @PreAuthorize("@ss.hasPermi('qw:user:myList')")
+//    @GetMapping("/myList")
+//    public TableDataInfo myList(QwUserParam qwUser)
+//    {
+//        startPage();
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        qwUser.setCompanyId(loginUser.getCompany().getCompanyId());
+//        qwUser.setCompanyUserId(loginUser.getCompany().getUserId());
+//        List<QwUserVO> list = qwUserService.selectQwUserListVO(qwUser);
+//        return getDataTable(list);
+//    }
+    @GetMapping("/getQwAllUserList")
+    public R getQwAllUserList(@RequestParam String corpId, @RequestParam Long companyId)
+    {
+        List<QwUserVO> list = companyUserService.selectCompanyQwUserList(corpId, companyId);
         return  R.ok().put("data",list);
     }
+    /**
+     * 查询企微用户列表-下拉框
+     */
+    @GetMapping("/qwList")
+    public TableDataInfo getQwList(QwUser qwUser)
+    {
+        startPage();
+
+        if(ObjectUtil.notEqual(qwUser.getDisableCompanyId(),1)){
+            qwUser.setCompanyId(qwUser.getCompanyId());
+        }
+        List<QwUser> list = qwUserService.selectQwUserList(qwUser);
+        return getDataTable(list);
+    }
+    /**
+     * 查询企微用户列表-下拉框
+     */
+    @PostMapping("/qwCompanyList")
+    public TableDataInfo qwCompanyList(@RequestBody QwUser qwUser)
+    {
+        startPage();
+        List<QwUser> list = qwUserService.selectQwUserList(qwUser);
+        return getDataTable(list);
+    }
+    /**
+     * 查询企微员工列表-用于员工管理绑定
+     */
+    @GetMapping("/getQwUserList")
+    public R getQwUserList()
+    {
+        QwUserParam qwUser = new QwUserParam();
+        List<String> strings = qwCompanyMapper.selectQwCompanyCorpIdListByCompanyId(getLoginUser().getUser().getCompanyId());
+        qwUser.setCorpId(strings);
+        if (strings==null||strings.size()==0){
+            return  R.ok().put("data",null);
+        }
+        qwUser.setIsDel(0);
+        List<QwUserVO> list = qwUserService.selectQwUserListBindVO(qwUser);
+        return  R.ok().put("data",list);
+    }
+
+    @GetMapping("/getMyQwUserList")
+    public R getMyQwUserList()
+    {
+        List<QwOptionsVO> list = qwUserService.selectQwUserListOptionsVOByCompanyUserId(getLoginUser().getUser().getUserId());
+        return  R.ok().put("data",list);
+    }
+    @GetMapping("/getMyQwCompanyList/{companyId}")
+    public R getMyQwCompanyList(@PathVariable Long companyId)
+    {
+        List<QwOptionsVO> list = qwUserService.selectQwCompanyListOptionsVOByCompanyId(companyId);
+        return  R.ok().put("data",list);
+    }
+    /**
+     * 获取企微用户详细信息
+     */
+
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(qwUserService.selectQwUserVOById(id));
+    }
+    /**
+     * 批量查询 企微用户详细信息
+     */
+    @GetMapping(value = "/getInfo/{ids}")
+    public AjaxResult getInfoByIds(@PathVariable("ids") Long[] ids)
+    {
+        return AjaxResult.success(qwUserService.selectQwUserVOByIds(ids));
+    }
+
+
+//    /**
+//     * 新增企微用户
+//     */
+//    @PreAuthorize("@ss.hasPermi('qw:user:add')")
+//    @Log(title = "企微用户", businessType = BusinessType.INSERT)
+//    @PostMapping
+//    public R add()
+//    {
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//
+//        return R.ok(qwUserService.syncQwUser(loginUser.getCompany().getCompanyId()));
+//    }
+
+
+
+    /**
+     * 同步企微用户
+     */
+    @RepeatSubmit
+    @PreAuthorize("@ss.hasPermi('qw:user:sync')")
+    @Log(title = "企微用户", businessType = BusinessType.INSERT)
+    @PostMapping("sync/{corpId}")
+    public R sync(@PathVariable String corpId)
+    {
+        List<String> strings = qwCompanyMapper.selectQwCompanyCorpIdListByCompanyId(getLoginUser().getUser().getCompanyId());
+        for (String string : strings) {
+
+            if (string.equals(corpId)){
+                qwUserService.syncQwUser(string);
+                qwDeptService.insertOrUpdateQwDept(string);
+                logger.info("同步完成");
+            }
+        }
+        return R.ok();
+    }
+    @RepeatSubmit
+    @PreAuthorize("@ss.hasPermi('qw:user:sync')")
+    @Log(title = "同步企微用户名称", businessType = BusinessType.INSERT)
+    @PostMapping("syncName/{corpId}")
+    public R syncName(@PathVariable String corpId)
+    {
+        List<String> strings = qwCompanyMapper.selectQwCompanyCorpIdListByCompanyId(getLoginUser().getUser().getCompanyId());
+        for (String string : strings) {
+            if (string.equals(corpId)){
+                qwUserService.syncQwUserName(string);
+            }
+        }
+        return R.ok();
+    }
+    /**
+     * 绑定AI客服
+     */
+//    @PreAuthorize("@ss.hasPermi('qw:user:bindAi')")
+    @Log(title = "企微用户", businessType = BusinessType.UPDATE)
+    @PutMapping("/bindAi")
+    @RepeatSubmit
+    public R edit(@RequestBody QwUserBindAi param)
+    {
+        QwUser qwUser=new QwUser();
+        qwUser.setId(param.getId());
+        qwUser.setFastGptRoleId(param.getFastGptRoleId());
+        FastGptRole role = fastGptRoleMapper.selectFastGptRoleByRoleId(param.getFastGptRoleId());
+
+        if (role.getBindCorpId()!=null){
+            if (role.getBindCorpId().equals(param.getCorpId())){
+                qwUserService.updateQwUser(qwUser);
+            }else {
+                return R.error("该角色已绑定其他企业");
+            }
+        }else {
+            int i = qwUserService.updateQwUser(qwUser);
+
+            FastGptRole fastGptRole=new FastGptRole();
+            fastGptRole.setRoleId(param.getFastGptRoleId());
+            fastGptRole.setBindCorpId(param.getCorpId());
+            fastGptRoleMapper.updateFastGptRole(fastGptRole);
+
+            if (i>0){
+                return R.ok();
+            }else {
+                return R.error("绑定失败");
+            }
+        }
+
+        return R.ok();
+    }
+
+    /**
+     * 解除应用绑定
+     */
+//    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptRole:relieve')")
+    @Log(title = "解除应用", businessType = BusinessType.UPDATE)
+    @GetMapping("/relieveFastGptRoleById/{id}")
+    public R relieveFastGptRoleById(@PathVariable("id") Long id)
+    {
+        return qwUserService.relieveFastGptRoleById(id);
+    }
 
     /**
      * 绑定企微用户
@@ -188,6 +796,7 @@ public class QwUserController extends BaseController {
         }
         return R.error("绑定失败");
 
+
     }
 
 
@@ -201,6 +810,52 @@ public class QwUserController extends BaseController {
         syncMyQwExternalContact(qu,null);
     }
 
+    /** 修改企微用户的欢迎语 */
+    @PostMapping("/weclomeQwUser")
+    public R weclomeQwUser(@RequestBody QwUser qwUser) throws Exception {
+
+
+        return  qwUserService.weclomeQwUser(qwUser);
+    }
+
+    /**
+     * 删除企微用户
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:remove')")
+    @Log(title = "企微用户", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(qwUserService.deleteQwUserByIds(ids));
+    }
+
+
+    /**
+     * 获取企业微信用户列表
+     */
+    @GetMapping("/qwUserList/{corpId}")
+    public TableDataInfo qwUserList(@PathVariable String corpId)
+    {
+
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        Long companyId = loginUser.getCompany().getCompanyId();
+
+        List<QwOptionsVO> list = qwUserService.selectQwUserListOptionsVO(corpId);
+        return getDataTable(list);
+    }
+    @GetMapping("/getQwUserAll")
+    public AjaxResult getQwUserAll(){
+        return AjaxResult.success(qwUserService.getQwUserAll());
+    }
+
+    /**
+     * 获取企微信息
+     * **/
+    @GetMapping("/getQwUserInfo")
+    public R getQwUserInfo(QwFsUserParam param){
+        return R.ok().put("data",qwUserService.getQwUserInfo(param));
+    }
+
     public R  syncMyQwExternalContact(QwUser qwUser,String getNextCursor )  {
 
         String qwUserId = qwUser.getQwUserId();
@@ -286,79 +941,38 @@ public class QwUserController extends BaseController {
         return R.ok();
     }
 
-
-    /**
-     * 同步企微用户
-     */
-    @RepeatSubmit
-    @PreAuthorize("@ss.hasPermi('qw:user:sync')")
-    @Log(title = "企微用户", businessType = BusinessType.INSERT)
-    @PostMapping("sync/{corpId}")
-    public R sync(@PathVariable("corpId") String corpId)
-    {
-
-        List<String> strings = qwCompanyMapper.selectQwCompanyCorpIdListByAll();
-        for (String string : strings) {
-
-            if (string.equals(corpId)){
-                qwUserService.syncQwUser(string);
-                qwDeptService.insertOrUpdateQwDept(string);
-                logger.info("同步完成");
-            }
-        }
-        return R.ok();
-    }
-
-    /**
-     * 获取企微用户详细信息
-     */
-
-    @GetMapping(value = "/{id}")
-    public AjaxResult getInfo(@PathVariable("id") Long id)
-    {
-        return AjaxResult.success(qwUserService.selectQwUserVOById(id));
-    }
-    /**
-     * 批量查询 企微用户详细信息
-     */
-    @GetMapping(value = "/getInfo/{ids}")
-    public AjaxResult getInfoByIds(@PathVariable("ids") Long[] ids)
-    {
-        return AjaxResult.success(qwUserService.selectQwUserVOByIds(ids));
+    //    /**
+//     * 重启云主机
+//     * @return
+//     */
+//    @PutMapping("/restartHost")
+//    public R restartCloudHost(@RequestParam String serverIp) {
+//        return qwUserService.restartCloudHost(serverIp);
+//    }
+    @PostMapping("/updateSendType")
+    public R updateSendType(@RequestBody UpdateSendTypeVo vo) {
+        return qwUserService.updateSendType(vo);
     }
 
-    @RepeatSubmit
-    @PreAuthorize("@ss.hasPermi('qw:user:sync')")
-    @Log(title = "同步企微用户名称", businessType = BusinessType.INSERT)
-    @PostMapping("syncName/{corpId}")
-    public R syncName(@PathVariable("corpId") String corpId)
-    {
-        List<String> strings = qwCompanyMapper.selectQwCompanyCorpIdListByAll();
-        for (String string : strings) {
-            if (string.equals(corpId)){
-                qwUserService.syncQwUserName(string);
-            }
-        }
+    @GetMapping("/changeVideoStatus")
+    public R changeVideoStatus(Long id) {
+        qwUserService.changeVideoStatus(id);
         return R.ok();
     }
 
-
-    /**
-     * 查询企微用户列表
-     */
-
-    @GetMapping("/userList")
-    public TableDataInfo userList(QwUserListParam qwUser)
+    @GetMapping("/companyQwUserlist")
+    public TableDataInfo companyQwUserlist(@RequestParam Long companyId,
+                                           @RequestParam String corpId,
+                                           @RequestParam(required = false) String nickName)
     {
         startPage();
-        List<QwUserVO> list = qwUserService.selectAllQwUserListVO(qwUser);
+        List<QwUserVO> list = qwUserService.selectQwUserListByCompanyIdAndCorpIdAndNickName(companyId, corpId, nickName);
         return getDataTable(list);
     }
 
-    @GetMapping("/getQwAllUserList")
-    public R getQwAllUserList(@RequestParam String corpId, @RequestParam Long companyId)
+    @GetMapping("/updateFastGptRoleStatusById/{id}")
+    public R updateFastGptRoleStatusById(@PathVariable Long id)
     {
-        List<QwUserVO> list = companyUserService.selectCompanyQwUserList(corpId, companyId);
-        return  R.ok().put("data",list);
+        return qwUserService.updateQwUserFastGptRoleStatusById(id);
     }
 }

+ 1 - 1
fs-admin/src/main/resources/application.yml

@@ -4,7 +4,7 @@ server:
 # Spring配置
 spring:
   profiles:
-#    active: druid-ylrz
+#    active: dev-test
 #    active: druid-hdt
 #    active: druid-yzt
 #    active: druid-sxjz-test

+ 2 - 0
fs-common/src/main/java/com/fs/common/constant/LiveKeysConstant.java

@@ -37,6 +37,8 @@ public class LiveKeysConstant {
     public static final Integer PRODUCT_DETAIL_CACHE_EXPIRE = 300; //商品详情缓存过期时间(秒)
 
     public static final String LIVE_TAG_MARK_CACHE = "live:tag:mark:%s"; //直播间打标签缓存,存储直播间ID、开始时间和视频时长
+    //记录用户观看直播间信息 直播间id、用户id、外部联系人id、qwUserId
+    public static final String LIVE_USER_WATCH_LOG_CACHE = "live:user:watch:log:%s:%s:%s:%s";
 
 
 }

+ 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);
+    }
+
     /**
      * 向哈希表中添加键值对
      *

+ 367 - 0
fs-common/src/main/java/com/fs/common/utils/HsCryptoUtil.java

@@ -0,0 +1,367 @@
+package com.fs.common.utils;
+
+import cn.hutool.json.JSONObject;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.DigestUtils;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * 红杉健康平台加解密工具类
+ */
+public class HsCryptoUtil {
+
+    private static final ObjectMapper objectMapper = new ObjectMapper();
+    private static final String AES_ALGORITHM = "AES/CBC/PKCS5Padding";
+    private static final String RSA_ALGORITHM = "RSA/ECB/PKCS1Padding";
+
+    /**
+     * 加密方法
+     * @param data 待加密的数据对象
+     * @param appSecret 应用密钥,用于AES加密的IV
+     * @param publicKeyStr RSA公钥(Base64编码)
+     * @return 加密结果Map,包含content、key、sign三个字段
+     */
+    public static Map<String, String> encrypt(JSONObject data, String appSecret, String publicKeyStr) {
+        try {
+//            公钥解码base64
+            String publicKey = new String( Base64.decodeBase64(publicKeyStr), StandardCharsets.UTF_8);
+            publicKey = publicKey.replace("-----BEGIN PUBLIC KEY-----\n", "").replace(
+                    "\n" +
+                            "-----END PUBLIC KEY-----\n", ""
+            );
+            // 1. 将对象转换为按ASCII码排序的Map
+
+            Map<String, Object> sortedParams = new TreeMap<>();
+            /*处理掉空值,否则转jsonString要报错*/
+            for (Map.Entry<String, Object> entry : data.entrySet()) {
+                if ( entry.getValue() != null && !(entry.getValue() instanceof cn.hutool.json.JSONNull)) {
+                    Object processedValue = processNestedObjects(entry.getValue());
+                    sortedParams.put(entry.getKey(), processedValue);
+                }
+            }
+            String contentStr = objectMapper.writeValueAsString(sortedParams);
+//            // 直接转换 JSONObject 为 Map
+//            Map<String, Object> sortedParams = convertToSortedMap(data);
+//
+//            // 2. 转换为JSON字符串(contentStr)
+//            String contentStr = objectMapper.writeValueAsString(sortedParams);
+
+            // 3. 生成32位随机密钥(randStr)
+            String randStr = generateRandStr(32);
+
+            // 4. AES-256-CBC加密contentStr
+            String encryptedContent = aesEncrypt(contentStr, randStr, appSecret);
+
+            // 5. 使用RSA公钥加密randStr
+            String encryptedKey = rsaEncrypt(randStr, publicKey);
+
+            // 6. 对contentStr进行MD5签名
+            String sign = DigestUtils.md5Hex(contentStr);
+
+            // 7. 构造返回结果
+            Map<String, String> result = new TreeMap<>();
+            result.put("content", encryptedContent);
+            result.put("key", encryptedKey);
+            result.put("sign", sign);
+
+            return result;
+        } catch (Exception e) {
+            throw new RuntimeException("加密失败", e);
+        }
+    }
+
+    /**
+     * 递归处理嵌套的JSON对象 的jsonNull和null
+     */
+    private static Object processNestedObjects(Object value) {
+        if (value instanceof cn.hutool.json.JSONObject) {
+            // 处理嵌套的JSONObject
+            cn.hutool.json.JSONObject jsonObj = (cn.hutool.json.JSONObject) value;
+            Map<String, Object> nestedMap = new TreeMap<>();
+            for (Map.Entry<String, Object> entry : jsonObj.entrySet()) {
+                if (entry.getValue() != null && !(entry.getValue() instanceof cn.hutool.json.JSONNull)) {
+                    nestedMap.put(entry.getKey(), processNestedObjects(entry.getValue()));
+                }
+            }
+            return nestedMap;
+        } else if (value instanceof cn.hutool.json.JSONArray) {
+            // 处理JSONArray
+            cn.hutool.json.JSONArray jsonArray = (cn.hutool.json.JSONArray) value;
+            List<Object> list = new ArrayList<>();
+            for (Object item : jsonArray) {
+                list.add(processNestedObjects(item));
+            }
+            return list;
+        } else {
+            // 基本类型直接返回
+            return value;
+        }
+    }
+
+    /**
+     * 将对象转换为按ASCII码排序的Map
+     */
+    private static Map<String, Object> convertToSortedMap(Object obj) throws Exception {
+        Map<String, Object> map = objectMapper.convertValue(obj, Map.class);
+        return new TreeMap<>(map);
+    }
+
+    /**
+     * 生成指定长度的随机字符串
+     */
+    private static String generateRandStr(int length) {
+        String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+        SecureRandom random = new SecureRandom();
+        StringBuilder sb = new StringBuilder(length);
+
+        for (int i = 0; i < length; i++) {
+            sb.append(chars.charAt(random.nextInt(chars.length())));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * AES-256-CBC加密
+     */
+    private static String aesEncrypt(String content, String randStr, String appSecret) throws Exception {
+        Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
+        SecretKeySpec keySpec = new SecretKeySpec(randStr.getBytes(), "AES");
+        // 使用appSecret的前16位作为IV
+        IvParameterSpec ivSpec = new IvParameterSpec(appSecret.substring(0, 16).getBytes());
+        cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
+        byte[] encrypted = cipher.doFinal(content.getBytes());
+        return Base64.encodeBase64String(encrypted);
+    }
+
+    /**
+     * RSA加密
+     */
+    private static String rsaEncrypt(String randStr, String publicKey) throws Exception {
+        byte[] decodedPublicKey = Base64.decodeBase64(publicKey);
+        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decodedPublicKey);
+        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+        PublicKey pubKey = keyFactory.generatePublic(keySpec);
+
+        Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
+        cipher.init(Cipher.ENCRYPT_MODE, pubKey);
+        byte[] encrypted = cipher.doFinal(randStr.getBytes());
+        return Base64.encodeBase64String(encrypted);
+    }
+
+    /**
+     * 解密方法
+     * @param encryptedContent AES加密的内容(Base64编码)
+     * @param encryptedKey RSA加密的密钥(Base64编码)
+     * @param sign 签名(MD5)
+     * @param appSecret 应用密钥,用于AES解密的IV
+     * @param privateKeyStr RSA私钥(PEM格式,Base64编码)
+     * @return 解密后的原始数据对象
+     */
+    public static Map<String, Object> decrypt(String encryptedContent,
+                                              String encryptedKey,
+                                              String sign,
+                                              String appSecret,
+                                              String privateKeyStr) {
+        try {
+            // 1. Base64解码私钥字符串,得到PEM格式的私钥
+            String privateKeyPEM = new String(
+                    Base64.decodeBase64(privateKeyStr),
+                    StandardCharsets.UTF_8
+            );
+            // 1. RSA解密获取AES密钥(randStr)
+            String randStr = rsaDecrypt(encryptedKey, privateKeyPEM);
+
+            // 2. AES解密获取contentStr
+            String contentStr = aesDecrypt(encryptedContent, randStr, appSecret);
+
+            // 3. 验证签名
+            String calculatedSign = DigestUtils.md5Hex(contentStr);
+            if (!calculatedSign.equals(sign)) {
+                throw new RuntimeException("签名验证失败,数据可能被篡改");
+            }
+
+            // 4. 将contentStr解析为Map
+            return objectMapper.readValue(contentStr, Map.class);
+
+        } catch (Exception e) {
+            throw new RuntimeException("解密失败", e);
+        }
+    }
+
+    /**
+     * 解密并验证的完整方法
+     * @param encryptedData 加密数据Map,包含content、key、sign三个字段
+     * @param appSecret 应用密钥
+     * @param privateKeyStr RSA私钥
+     * @return 解密后的原始数据Map
+     */
+    public static Map<String, Object> decrypt(JsonNode encryptedData,
+                                              String appSecret,
+                                              String privateKeyStr) {
+        return decrypt(
+                encryptedData.get("content").asText(),
+                encryptedData.get("key").asText(),
+                encryptedData.get("sign").asText(),
+                appSecret,
+                privateKeyStr
+        );
+    }
+
+    /**
+     * RSA解密 - 使用私钥解密AES密钥
+     */
+    private static String rsaDecrypt(String encryptedKey, String privateKeyStr) throws Exception {
+//    String privateKey = new String( Base64.decodeBase64(privateKeyStr), StandardCharsets.UTF_8);
+
+        // 处理PEM格式的私钥
+        String privateKeyPEM = privateKeyStr
+                .replace("-----BEGIN PRIVATE KEY-----", "")
+                .replace("-----END PRIVATE KEY-----", "")
+                .replaceAll("\\s+", "");
+
+        byte[] decodedPrivateKey = Base64.decodeBase64(privateKeyPEM);
+        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedPrivateKey);
+        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+        PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
+
+        Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
+        cipher.init(Cipher.DECRYPT_MODE, privateKey);
+
+        byte[] encryptedBytes = Base64.decodeBase64(encryptedKey);
+        byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
+
+        return new String(decryptedBytes, StandardCharsets.UTF_8);
+    }
+
+    /**
+     * AES-256-CBC解密
+     */
+    private static String aesDecrypt(String encryptedContent,
+                                     String randStr,
+                                     String appSecret) throws Exception {
+        Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
+        SecretKeySpec keySpec = new SecretKeySpec(randStr.getBytes(StandardCharsets.UTF_8), "AES");
+
+        // 使用appSecret的前16位作为IV(与加密时一致)
+        String iv = appSecret.length() >= 16 ? appSecret.substring(0, 16) : appSecret;
+        IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
+
+        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
+
+        byte[] encryptedBytes = Base64.decodeBase64(encryptedContent);
+        byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
+
+        return new String(decryptedBytes, StandardCharsets.UTF_8);
+    }
+
+    /**
+     * 验证签名(不进行解密)
+     */
+    public static boolean verifySignature(String contentStr, String sign) {
+        String calculatedSign = DigestUtils.md5Hex(contentStr);
+        return calculatedSign.equals(sign);
+    }
+
+    /**
+     * 单独解密AES密钥(用于调试)
+     */
+    public static String decryptKeyOnly(String encryptedKey, String privateKeyStr) throws Exception {
+        return rsaDecrypt(encryptedKey, privateKeyStr);
+    }
+
+    /**
+     * 单独解密内容(需要提供AES密钥)
+     */
+    public static String decryptContentOnly(String encryptedContent,
+                                            String randStr,
+                                            String appSecret) throws Exception {
+        return aesDecrypt(encryptedContent, randStr, appSecret);
+    }
+
+
+//    /**
+//     * 解密方法
+//     * @param encryptedData 加密的数据Map,包含content、key、sign三个字段
+//     * @param appSecret 应用密钥,用于AES解密的IV
+//     * @param privateKeyStr RSA私钥(Base64编码)
+//     * @return 解密后的原始数据对象
+//     */
+//    public static Map<String, Object> decrypt(JsonNode encryptedData, String appSecret, String privateKeyStr) {
+//        //            私钥解码base64
+//    String privateKey = new String( Base64.decodeBase64(privateKeyStr), StandardCharsets.UTF_8);
+
+//        privateKey = privateKey.replace("-----BEGIN PRIVATE KEY-----\n", "").replace(
+//                "\n" +
+//                        "-----END PRIVATE KEY-----", ""
+//        );
+//        try {
+//            String content = String.valueOf(encryptedData.get("content"));
+//            String key = String.valueOf(encryptedData.get("key"));
+//            String sign = String.valueOf(encryptedData.get("sign"));
+//
+//            // 1. 使用RSA私钥解密key字段,得到randStr
+//            String randStr = rsaDecrypt(key, privateKey);
+//
+//            // 2. 使用AES-256-CBC解密content字段
+//            String decryptedContent = aesDecrypt(content, randStr, appSecret);
+//
+//            // 3. 验证签名
+//            String calculatedSign = DigestUtils.md5Hex(decryptedContent);
+//            if (!calculatedSign.equals(sign)) {
+//                throw new RuntimeException("签名验证失败");
+//            }
+//
+//            // 4. 将解密后的JSON字符串转换为Map对象
+//            @SuppressWarnings("unchecked")
+//            Map<String, Object> result = objectMapper.readValue(decryptedContent, Map.class);
+//
+//            return result;
+//        } catch (Exception e) {
+//            throw new RuntimeException("解密失败", e);
+//        }
+//    }
+//
+//    /**
+//     * AES-256-CBC解密
+//     */
+//    private static String aesDecrypt(String content, String randStr, String appSecret) throws Exception {
+//        Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
+//        SecretKeySpec keySpec = new SecretKeySpec(randStr.getBytes(), "AES");
+//        // 使用appSecret的前16位作为IV
+//        IvParameterSpec ivSpec = new IvParameterSpec(appSecret.substring(0, 16).getBytes());
+//        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
+//        byte[] decrypted = cipher.doFinal(Base64.decodeBase64(content));
+//        return new String(decrypted);
+//    }
+//
+//    /**
+//     * RSA解密
+//     */
+//    private static String rsaDecrypt(String encryptedKey, String privateKey) throws Exception {
+//        byte[] decodedPrivateKey = Base64.decodeBase64(privateKey);
+//        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedPrivateKey);
+//        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+//        PrivateKey privKey = keyFactory.generatePrivate(keySpec);
+//
+//        Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
+//        cipher.init(Cipher.DECRYPT_MODE, privKey);
+//        byte[] decrypted = cipher.doFinal(Base64.decodeBase64(encryptedKey));
+//        return new String(decrypted);
+//    }
+}

+ 154 - 0
fs-common/src/main/java/com/fs/common/utils/StringToMapUtil.java

@@ -0,0 +1,154 @@
+package com.fs.common.utils;
+
+import cn.hutool.core.lang.TypeReference;
+import cn.hutool.json.JSONUtil;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class StringToMapUtil {
+    public static Map<String, Object> toMap(String input) {
+        if (input == null || input.trim().isEmpty()) {
+            return new HashMap<>();
+        }
+
+        try {
+            // 尝试JSON解析
+            return JSONUtil.toBean(input, new TypeReference<Map<String, Object>>() {}, false);
+        } catch (Exception e) {
+            // 如果失败,尝试Map.toString()格式解析
+            try {
+                // 检查是否是Map.toString()格式
+                if (input.trim().startsWith("{") && input.trim().endsWith("}") &&
+                        input.contains("=")) {
+                    return parseMapToString(input);
+                }
+            } catch (Exception ex) {
+                // 忽略,抛出原始异常
+            }
+            throw e;
+        }
+    }
+    /**
+     * 将Map.toString()格式的字符串转换为Map
+     * @param mapString 类似 {data={book_no=10381008622, ...}, msg=success, state=0} 的字符串
+     * @return 转换后的Map对象
+     */
+    private static Map<String, Object> parseMapToString(String mapString) {
+        Map<String, Object> resultMap = new HashMap<>();
+
+        // 移除外层大括号
+        String content = mapString.trim();
+        if (content.startsWith("{") && content.endsWith("}")) {
+            content = content.substring(1, content.length() - 1);
+        }
+
+        // 分割顶层键值对
+        List<String> pairs = splitTopLevelPairs(content);
+
+        for (String pair : pairs) {
+            int eqIndex = pair.indexOf('=');
+            if (eqIndex > 0) {
+                String key = pair.substring(0, eqIndex).trim();
+                String value = pair.substring(eqIndex + 1).trim();
+
+                if (value.startsWith("{") && value.endsWith("}")) {
+                    // 嵌套Map对象
+                    resultMap.put(key, parseNestedMap(value));
+                } else {
+                    // 简单值
+                    resultMap.put(key, value);
+                }
+            }
+        }
+
+        return resultMap;
+    }
+
+    /**
+     * 分割顶层键值对,避免分割嵌套对象内的逗号
+     */
+    private static List<String> splitTopLevelPairs(String content) {
+        List<String> pairs = new ArrayList<>();
+        int level = 0;
+        int start = 0;
+
+        for (int i = 0; i < content.length(); i++) {
+            char c = content.charAt(i);
+            if (c == '{') {
+                level++;
+            } else if (c == '}') {
+                level--;
+            } else if (c == ',' && level == 0) {
+                pairs.add(content.substring(start, i));
+                start = i + 1;
+            }
+        }
+
+        // 添加最后一个元素
+        if (start < content.length()) {
+            pairs.add(content.substring(start));
+        }
+
+        return pairs;
+    }
+
+    /**
+     * 解析嵌套的Map对象
+     */
+    private static Map<String, Object> parseNestedMap(String mapStr) {
+        Map<String, Object> nestedMap = new HashMap<>();
+
+        // 移除外层大括号
+        String content = mapStr.substring(1, mapStr.length() - 1);
+
+        List<String> pairs = splitTopLevelPairs(content);
+
+        for (String pair : pairs) {
+            int eqIndex = pair.indexOf('=');
+            if (eqIndex > 0) {
+                String key = pair.substring(0, eqIndex).trim();
+                String value = pair.substring(eqIndex + 1).trim();
+
+                // 尝试转换为合适的类型
+                nestedMap.put(key, convertValueType(value));
+            }
+        }
+
+        return nestedMap;
+    }
+
+    /**
+     * 根据值的特点转换为合适的类型
+     */
+    private static Object convertValueType(String value) {
+        // 数字
+        if (value.matches("-?\\d+")) {
+            try {
+                return Integer.parseInt(value);
+            } catch (NumberFormatException e) {
+                // 忽略,继续处理
+            }
+        }
+
+        // 布尔值
+        if ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value)) {
+            return Boolean.parseBoolean(value);
+        }
+
+        // 嵌套对象
+        if (value.startsWith("{") && value.endsWith("}")) {
+            return parseNestedMap(value);
+        }
+
+        // 数组或列表(简化处理)
+        if (value.startsWith("[") && value.endsWith("]")) {
+            return value.substring(1, value.length() - 1).split(",");
+        }
+
+        // 默认作为字符串
+        return value;
+    }
+}

+ 23 - 0
fs-common/src/main/java/com/fs/common/utils/poi/ExcelUtil.java

@@ -19,6 +19,8 @@ import java.util.Set;
 import java.util.UUID;
 import java.util.stream.Collectors;
 import javax.servlet.http.HttpServletResponse;
+
+import com.fs.common.core.domain.entity.SysDictData;
 import org.apache.poi.hssf.usermodel.HSSFClientAnchor;
 import org.apache.poi.hssf.usermodel.HSSFPicture;
 import org.apache.poi.hssf.usermodel.HSSFPictureData;
@@ -705,6 +707,27 @@ public class ExcelUtil<T>
             // 这里默认设了2-101列只能选择不能输入.
             setXSSFValidation(sheet, attr.combo(), 1, 100, column, column);
         }
+        // 如果设置了dictType属性,则从字典中获取选项并设置下拉列表
+        if (StringUtils.isNotEmpty(attr.dictType()))
+        {
+            try
+            {
+                List<SysDictData> dictDatas = DictUtils.getDictCache(attr.dictType());
+                if (dictDatas != null && !dictDatas.isEmpty())
+                {
+                    // 提取字典标签列表
+                    String[] dictLabels = dictDatas.stream()
+                            .map(SysDictData::getDictLabel)
+                            .toArray(String[]::new);
+                    // 这里默认设了2-101列只能选择不能输入.
+                    setXSSFValidation(sheet, dictLabels, 1, 100, column, column);
+                }
+            }
+            catch (Exception e)
+            {
+                log.warn("设置字典下拉选项失败,dictType: {}, error: {}", attr.dictType(), e.getMessage());
+            }
+        }
     }
 
     /**

+ 7 - 6
fs-company-app/src/main/java/com/fs/app/controller/FsUserController.java

@@ -45,10 +45,7 @@ import java.io.InputStream;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
+import java.util.*;
 
 import static com.fs.his.utils.PhoneUtil.encryptPhone;
 
@@ -216,7 +213,9 @@ public class FsUserController extends AppBaseController {
             @ApiParam(value = "类型,1-按完播率,2-按正确率", required = true) @RequestParam Integer type
     ) {
         long userId = Long.parseLong(getUserId());
-        return ResponseResult.ok(fsUserService.userRanking(userId, startTime, endTime, periodId, videoId, order, type));
+        // 中康的数据太多太卡不要这个
+        return ResponseResult.ok(Collections.emptyList());
+//        return ResponseResult.ok(fsUserService.userRanking(userId, startTime, endTime, periodId, videoId, order, type));
     }
 
     @Login
@@ -231,7 +230,9 @@ public class FsUserController extends AppBaseController {
             @ApiParam(value = "类型,1-按完播率,2-按正确率", required = true) @RequestParam Integer type
     ) {
         long userId = Long.parseLong(getUserId());
-        return ResponseResult.ok(fsUserService.courseRanking(userId, startTime, endTime, courseId, videoId, order, type));
+        // 中康的数据太多太卡不要这个
+        return ResponseResult.ok(Collections.emptyList());
+//        return ResponseResult.ok(fsUserService.courseRanking(userId, startTime, endTime, courseId, videoId, order, type));
     }
 
     @Login

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

@@ -170,7 +170,6 @@ public class FsUserCourseVideoController extends AppBaseController {
         } else {
             params.put("companyUserId", companyUser.getUserId());
         }
-
         PageHelper.startPage(pageNum, pageSize);
         List<FsUserCourseParticipationRecordVO> record = fsUserCourseService.getParticipationRecordByMap(params);
         return ResponseResult.ok(new PageInfo<>(record));

+ 1 - 1
fs-company-app/src/main/java/com/fs/app/controller/WxCompanyUserController.java

@@ -171,7 +171,7 @@ public class WxCompanyUserController extends AppBaseController {
      */
     private FsUser createUser(LoginMaWxParam param, WxMaJscode2SessionResult session, WxMaPhoneNumberInfo phoneNoInfo, Company company, CompanyUser companyUser) {
         FsUser user = new FsUser();
-        user.setStatus((company != null ? company.getFsUserIsDefaultBlack() : 0) == 1 ? 0 : 1);
+        user.setStatus(1);
         user.setUnionId(session.getUnionid() == null ? "" : session.getUnionid());
         user.setCreateTime(new Date());
         if (param.getAuthType() == 1 && phoneNoInfo != null) {

+ 38 - 0
fs-company/src/main/java/com/fs/company/controller/common/CommonController.java

@@ -6,10 +6,12 @@ import com.fs.common.constant.Constants;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
 import com.fs.common.exception.file.OssException;
+import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.file.FileUploadUtils;
 import com.fs.common.utils.file.FileUtils;
+import com.fs.company.service.ICompanyService;
 import com.fs.company.utils.AudioUtils;
 import com.fs.company.vo.WangUploadVO;
 import com.fs.course.service.ITencentCloudCosService;
@@ -40,6 +42,7 @@ import java.awt.image.BufferedImage;
 import java.io.ByteArrayOutputStream;
 import java.io.InputStream;
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
 
@@ -71,6 +74,9 @@ public class CommonController
     @Autowired
     private OpenIMService openIMService;
 
+    @Autowired
+    private ICompanyService companyService;
+
     @Autowired
     private TokenService tokenService;
 //    @Autowired
@@ -93,6 +99,38 @@ public class CommonController
 //        qwWorkTaskService.addQwWorkByConversionDay();
       return R.ok();
     }
+
+    @GetMapping("common/test2")
+    public R test2(String time) throws Exception
+    {
+        // 默认是前两天时间
+        String createSTime;
+        String createETime;
+        if (StringUtils.isNotBlank(time)) {
+            Date date = DateUtils.parseDate(time);
+            createSTime = time+" 00:00:00";
+            createETime = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD,DateUtils.addDays(date, 1))+" 00:00:00";
+        } else {
+            createSTime = DateUtils.parseDateToStr( DateUtils.YYYY_MM_DD,DateUtils.addDays(new Date(), -2))+" 00:00:00";
+            createETime = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD,DateUtils.addDays(new Date(), -1))+" 00:00:00";
+        }
+
+        // 这个地方真加的是company money字段 xgb
+        companyService.rollbackRedPacketMoney(createSTime, createETime);
+        return R.ok();
+    }
+
+    @GetMapping("common/test3")
+    public R test2(String companyIdStr,String outBatchNo) throws Exception
+    {
+        // 默认是前两天时间
+        Long companyId=Long.parseLong(companyIdStr);
+        R result=companyService.checkMchTransferStatusByBatchID(outBatchNo,companyId);
+        log.info("查询商户转账结果:{}",result);
+        return R.ok();
+    }
+
+
     /**
      * 通用下载请求
      *

+ 1 - 1
fs-company/src/main/java/com/fs/company/controller/company/CompanyUserController.java

@@ -199,7 +199,7 @@ public class CompanyUserController extends BaseController {
                     }
                     //是否绑定
                     if(QwStatusEnum.BOUND.getCode() == companyUserQwListVO.getQwStatus()){
-                        if(!companyUserQwListVO.getQwUserId().isEmpty()){
+                        if(!StringUtil.strIsNullOrEmpty(companyUserQwListVO.getQwUserId())){
                             Long[] ids = Arrays.stream(companyUserQwListVO.getQwUserId().split(","))
                                     .map(Long::parseLong)
                                     .toArray(Long[]::new);

+ 89 - 5
fs-company/src/main/java/com/fs/company/controller/company/IndexStatisticsController.java

@@ -3,6 +3,9 @@ package com.fs.company.controller.company;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.ServletUtils;
+import com.fs.company.domain.Company;
+import com.fs.company.service.ICompanyService;
+import com.fs.config.cloud.CloudHostProper;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
 import com.fs.statis.StatisticsRedisConstant;
@@ -12,7 +15,10 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
 
 import static com.fs.statis.StatisticsRedisConstant.*;
 
@@ -31,6 +37,11 @@ public class IndexStatisticsController {
     @Autowired
     private IStatisticsService statisticsService;
 
+    @Autowired
+    private ICompanyService companyService;
+
+    @Autowired
+    CloudHostProper cloudHostProper;
     /**
      * 分析概览
      */
@@ -94,12 +105,85 @@ public class IndexStatisticsController {
             userType = 0;
         }
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        Long companyId = loginUser.getCompany().getCompanyId();
-        param.setCompanyId(companyId);
 
-        String key = String.format("%s:%d:%d:%d", DATA_OVERVIEW_DEALER_CHARTS, type,userType,param.getCompanyId());
-        List<DeaMemberTopTenDTO> deaMemberTopTenDTOS = redisCache.getCacheObject(key);
-        return R.ok().put("data", deaMemberTopTenDTOS);
+        if("四川致医".equals(cloudHostProper.getCompanyName())){
+            Long companyId1 = loginUser.getCompany().getCompanyId();
+            Company company = loginUser.getCompany();
+            param.setCompanyId(companyId1);
+            List<WatchEndPlayTrendDTO> watchEndPlayTrendDTOS;
+
+            // 参考watchCourseTopTen方法的处理逻辑
+            if ((param.getCompanyId() == null && param.getDeptId() == null) || (param.getCompanyId() == null && param.getDeptId() == 1)){
+                String key = String.format("%s:%d:%d", DATA_OVERVIEW_DEALER_CHARTS, type,userType);
+                watchEndPlayTrendDTOS = redisCache.getCacheObject(key);
+            }else if(param.getCompanyId() != null){
+                String key = String.format("%s:%d:%d:%d", DATA_OVERVIEW_DEALER_CHARTS, type,userType,param.getCompanyId());
+                watchEndPlayTrendDTOS = redisCache.getCacheObject(key);
+            }else{
+                Long[] companyIds = companyService.selectCompanyList(company).stream().map(Company::getCompanyId).toArray(Long[]::new);
+                List<WatchEndPlayTrendDTO> tempDTOS = new ArrayList<>();
+                for(Long companyId : companyIds){
+                    String key = String.format("%s:%d:%d:%d", DATA_OVERVIEW_DEALER_CHARTS, type,userType,companyId);
+                    List<WatchEndPlayTrendDTO> companyData = redisCache.getCacheObject(key);
+                    if (companyData != null) {
+                        tempDTOS.addAll(companyData);
+                    }
+                }
+                // 根据startDate 和 x 分组,合并watchUserCount和completedUserCount 限制最多返回10条记录
+                watchEndPlayTrendDTOS = tempDTOS.stream()
+                        .collect(Collectors.groupingBy(
+                                dto -> dto.getStartDate() + ":" + dto.getX(),  // 根据startDate和x分组
+                                Collectors.reducing(new WatchEndPlayTrendDTO(), (dto1, dto2) -> {
+                                    // 合并watchUserCount和completedUserCount
+                                    WatchEndPlayTrendDTO result = new WatchEndPlayTrendDTO();
+                                    // 复制分组标识字段
+                                    if (dto2 != null && dto2.getStartDate() != null) {
+                                        result.setStartDate(dto2.getStartDate());
+                                    } else if (dto1 != null) {
+                                        result.setStartDate(dto1.getStartDate());
+                                    }
+
+                                    if (dto2 != null && dto2.getX() != null) {
+                                        result.setX(dto2.getX());
+                                    } else if (dto1 != null) {
+                                        result.setX(dto1.getX());
+                                    }
+
+                                    // 合并数值字段
+                                    result.setWatchUserCount(
+                                            (dto1 == null || dto1.getWatchUserCount() == null ? 0 : dto1.getWatchUserCount()) +
+                                                    (dto2 == null || dto2.getWatchUserCount() == null ? 0 : dto2.getWatchUserCount())
+                                    );
+
+                                    result.setCompletedUserCount(
+                                            (dto1 == null || dto1.getCompletedUserCount() == null ? 0 : dto1.getCompletedUserCount()) +
+                                                    (dto2 == null || dto2.getCompletedUserCount() == null ? 0 : dto2.getCompletedUserCount())
+                                    );
+
+                                    return result;
+                                })
+                        ))
+                        .values()
+                        .stream()
+                        .filter(Objects::nonNull)  // 过滤掉null值
+                        .limit(10)
+                        .sorted(Comparator.comparing(WatchEndPlayTrendDTO::getX))
+                        .collect(Collectors.toList());
+            }
+
+            if(watchEndPlayTrendDTOS == null){
+                watchEndPlayTrendDTOS = new ArrayList<>();
+            }
+
+            return R.ok().put("data", watchEndPlayTrendDTOS);
+        }else{
+            Long companyId = loginUser.getCompany().getCompanyId();
+            param.setCompanyId(companyId);
+
+            String key = String.format("%s:%d:%d:%d", DATA_OVERVIEW_DEALER_CHARTS, type,userType,param.getCompanyId());
+            List<DeaMemberTopTenDTO> deaMemberTopTenDTOS = redisCache.getCacheObject(key);
+            return R.ok().put("data", deaMemberTopTenDTOS);
+        }
     }
 
     /**

+ 13 - 1
fs-company/src/main/java/com/fs/company/controller/course/FsUserCoursePeriodController.java

@@ -31,6 +31,8 @@ import com.fs.framework.service.TokenService;
 import com.fs.his.vo.OptionsVO;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
+import com.hc.openapi.tool.fastjson.JSON;
+import com.hc.openapi.tool.fastjson.JSONObject;
 import io.swagger.annotations.ApiOperation;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -115,6 +117,15 @@ public class FsUserCoursePeriodController extends BaseController {
             } else {
                 vo.setIsNeedRegisterMember("0");
             }
+
+            // 看课休息判断
+            if(StringUtils.isNotBlank(vo.getIsOpenRestFlag())){
+                JSONObject  jsonObject= JSON.parseObject(vo.getIsOpenRestFlag());
+                vo.setIsOpenRestReminder(Integer.parseInt(jsonObject.get(currentCompanyId.toString()).toString()));
+            }else {
+                vo.setIsOpenRestReminder(null);
+            }
+
         }
         PageInfo<FsUserCoursePeriodVO> pageInfo = new PageInfo<>(list);
         Map<String, Object> result = new HashMap<>();
@@ -200,7 +211,8 @@ public class FsUserCoursePeriodController extends BaseController {
     @PutMapping("/updatePeriodIsOpenRestReminder")
     public AjaxResult updatePeriodIsOpenRestReminder(@RequestBody FsUserCoursePeriod fsUserCoursePeriod)
     {
-        return toAjax(fsUserCoursePeriodService.updatePeriod(fsUserCoursePeriod));
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return toAjax(fsUserCoursePeriodService.updatePeriod(fsUserCoursePeriod,loginUser.getCompany().getCompanyId()));
 
     }
 

+ 6 - 5
fs-company/src/main/java/com/fs/company/controller/live/LiveDataController.java

@@ -15,6 +15,7 @@ import com.fs.live.domain.LiveData;
 import com.fs.live.param.LiveDataParam;
 import com.fs.live.service.ILiveDataService;
 import com.fs.live.vo.ColumnsConfigVo;
+import com.fs.live.vo.LiveDataListVo;
 import com.fs.live.vo.LiveUserDetailExportVO;
 import com.github.pagehelper.PageHelper;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -61,7 +62,7 @@ public class LiveDataController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
     @GetMapping("/getLiveUserDetailListBySql")
-    public R getLiveUserDetailListBySql(@RequestParam Long liveId, 
+    public R getLiveUserDetailListBySql(@RequestParam Long liveId,
                                         @RequestParam(defaultValue = "1") Integer pageNum,
                                         @RequestParam(defaultValue = "100") Integer pageSize,
                                         HttpServletRequest request) {
@@ -163,11 +164,11 @@ public class LiveDataController extends BaseController
     @PreAuthorize("@ss.hasPermi('liveData:liveData:export')")
     @Log(title = "直播数据", businessType = BusinessType.EXPORT)
     @GetMapping("/export")
-    public AjaxResult export(LiveData liveData)
+    public AjaxResult export(LiveDataParam param)
     {
-        List<LiveData> list = liveDataService.selectLiveDataList(liveData);
-        ExcelUtil<LiveData> util = new ExcelUtil<LiveData>(LiveData.class);
-        return util.exportExcel(list, "直播数据数据");
+        List<LiveDataListVo> liveDataListVos = liveDataService.exportLiveData(param);
+        ExcelUtil<LiveDataListVo> util = new ExcelUtil<LiveDataListVo>(LiveDataListVo.class);
+        return util.exportExcel(liveDataListVos, "直播数据数据");
     }
 
     /**

+ 28 - 0
fs-company/src/main/java/com/fs/company/controller/live/LiveMixLiuTestOpenController.java

@@ -0,0 +1,28 @@
+package com.fs.company.controller.live;
+
+import com.fs.live.service.ILiveWatchUserService;
+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;
+
+/**
+ * @author MixLiu
+ * @date 2025/12/18 下午3:26)
+ */
+
+@RestController
+@RequestMapping("/live/LiveMixLiuTestOpen")
+public class LiveMixLiuTestOpenController {
+
+    @Autowired
+    private ILiveWatchUserService liveWatchUserService;
+
+    @GetMapping("/goToMarkUser/{liveId}")
+    public void goToMarkUser(@PathVariable Long liveId){
+        liveWatchUserService.qwTagMarkByLiveWatchLog(liveId);
+
+
+    }
+}

+ 8 - 4
fs-company/src/main/java/com/fs/company/controller/live/OrderController.java

@@ -52,7 +52,7 @@ public class OrderController extends BaseController
     @Autowired
     private IMergedOrderService mergedOrderService;
     // 设置最大导出数量限制为20000条
-    private static final int maxExportCount = 20000;
+    private static final int maxExportCount = 50000;
 
 
 
@@ -66,7 +66,8 @@ public class OrderController extends BaseController
         if(param.getOrderTypeFilter() == null || param.getOrderTypeFilter().equals("2")){
             return getDataTable(new ArrayList<>());
         }
-
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        param.setCompanyId(user.getCompanyId());
         startPage();
         List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
         for (MergedOrderVO vo : list) {
@@ -96,6 +97,7 @@ public class OrderController extends BaseController
         param.setCompanyId(user.getCompanyId());
         PageHelper.startPage(1, maxExportCount + 1);
         List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+        list = list.stream().filter(item -> StringUtils.isNotEmpty(item.getBankTransactionId())).collect(Collectors.toList());
 
         // 如果查询结果超过20000条,返回错误提示
         if (list != null && list.size() > maxExportCount) {
@@ -138,6 +140,7 @@ public class OrderController extends BaseController
         param.setCompanyId(user.getCompanyId());
         PageHelper.startPage(1, maxExportCount + 1);
         List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+        list = list.stream().filter(item -> StringUtils.isNotEmpty(item.getBankTransactionId())).collect(Collectors.toList());
 
         // 如果查询结果超过20000条,返回错误提示
         if (list != null && list.size() > maxExportCount) {
@@ -239,12 +242,13 @@ public class OrderController extends BaseController
             MergedOrderExportVO exportVO = new MergedOrderExportVO();
 
             // 订单基本信息(参考 FsStoreOrderItemExportVO 的顺序)
+            exportVO.setOrderTypeName(vo.getOrderTypeName());
             exportVO.setOrderCode(vo.getOrderCode());
             exportVO.setStatus(vo.getStatus() != null ? String.valueOf(vo.getStatus()) : null);
             exportVO.setUserId(vo.getUserId());
 
             // 产品信息
-            exportVO.setProductName(vo.getProductName());
+            exportVO.setProductName(StringUtils.isEmpty(vo.getProductName()) ? "产品被删除" : vo.getProductName());
             exportVO.setBarCode(vo.getBarCode());
             exportVO.setProductSpec(StringUtils.isEmpty(vo.getProductSpec()) ? "默认" : vo.getProductSpec());
             exportVO.setTotalNum(vo.getTotalNum());
@@ -252,7 +256,7 @@ public class OrderController extends BaseController
             exportVO.setCost(BigDecimal.ZERO);
             exportVO.setFPrice(BigDecimal.ZERO); // 结算价,合并订单暂无此字段
             exportVO.setPayPostage(vo.getPayDelivery());
-            exportVO.setCateName(vo.getCateName());
+            exportVO.setCateName(StringUtils.isEmpty(vo.getCateName()) ? "产品被删除" : vo.getCateName());
 
             // 收货信息
             exportVO.setRealName(vo.getRealName());

+ 6 - 1
fs-company/src/main/java/com/fs/company/controller/qw/QwAssignRuleController.java

@@ -1,5 +1,6 @@
 package com.fs.company.controller.qw;
 
+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;
@@ -39,10 +40,14 @@ public class QwAssignRuleController {
     public Result<IPage<QwAssignRule>> page(
             @RequestParam(defaultValue = "1") Long pageNum,
             @RequestParam(defaultValue = "10") Long pageSize,
-            @RequestParam(required = false) String ruleName
+            @RequestParam(required = false) String ruleName,
+            @RequestParam(required = false) Integer  status
     ) {
         Page<QwAssignRule> page = new Page<>(pageNum, pageSize);
         LambdaQueryWrapper<QwAssignRule> wrapper = new LambdaQueryWrapper<>();
+        if (ObjectUtil.isNotEmpty(status)){
+            wrapper.eq(QwAssignRule::getStatus,status);
+        }
         wrapper.like(StrUtil.isNotBlank(ruleName), QwAssignRule::getRuleName, ruleName);
         wrapper.orderByDesc(QwAssignRule::getCreateTime);
         IPage<QwAssignRule> result = qwAssignRuleService.page(page, wrapper);

+ 5 - 4
fs-company/src/main/java/com/fs/company/controller/qw/QwCustomerLinkController.java

@@ -17,6 +17,7 @@ import com.fs.qw.dto.QwCustomerLinkUserDto;
 import com.fs.qw.service.IQwCustomerLinkChannelService;
 import com.fs.qw.service.IQwCustomerLinkService;
 import com.fs.qw.service.IQwCustomerLinkUserService;
+import com.fs.qwApi.domain.QwLinkCreateResult;
 import com.fs.qwApi.param.QwLinkCreateParam;
 import com.fs.qwApi.service.QwApiService;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -110,11 +111,11 @@ public class QwCustomerLinkController {
             // qwApiService.linkUpdate(qwLinkCreateParam, qwGroupLiveCode.getCorpId());
             success = qwCustomerLinkService.updateById(bean);
         } else {
-/*            QwLinkCreateResult qwLinkCreateResult = qwApiService.linkCreate(qwLinkCreateParam, qwGroupLiveCode.getCorpId());
+            QwLinkCreateResult qwLinkCreateResult = qwApiService.linkCreate(qwLinkCreateParam, qwGroupLiveCode.getCorpId());
             bean.setLinkId(qwLinkCreateResult.getLinkId());
-            bean.setUrl(qwLinkCreateResult.getUrl());*/
-            bean.setLinkId(IdUtil.randomUUID());
-            bean.setUrl("https://work.weixin.qq.com/ca/" + IdUtil.randomUUID());
+            bean.setUrl(qwLinkCreateResult.getUrl());
+/*            bean.setLinkId(IdUtil.randomUUID());
+            bean.setUrl("https://work.weixin.qq.com/ca/" + IdUtil.randomUUID())*/;
             success = qwCustomerLinkService.save(bean);
         }
 

+ 13 - 0
fs-company/src/main/java/com/fs/company/controller/qw/QwSopTempController.java

@@ -20,6 +20,7 @@ import com.fs.qw.vo.SortDayVo;
 import com.fs.sop.domain.QwSop;
 import com.fs.sop.domain.QwSopTemp;
 import com.fs.sop.domain.QwSopTempDay;
+import com.fs.sop.params.BatchOpenOrCloseOfficialParam;
 import com.fs.sop.params.QwSopShareTempParam;
 import com.fs.sop.service.IQwSopTempService;
 import com.fs.sop.vo.UpdateRedVo;
@@ -362,4 +363,16 @@ public class QwSopTempController extends BaseController
     public R getSelectableRange(){
         return R.ok().put("data", qwSopTempService.getSelectableRange());
     }
+
+    /**
+     * sop模板update一键开关官方群发
+     * @param param
+     * @return
+     */
+    @PreAuthorize("@ss.hasPermi('qw:sopTemp:edit') or @ss.hasPermi('qw:sopTemp:myEdit') or @ss.hasPermi('qw:sopTemp:deptEdit')")
+    @Log(title = "sop模板update一键开关官方群发", businessType = BusinessType.UPDATE)
+    @PostMapping("/batchOpenOrCloseOfficial")
+    public R batchOpenOrCloseOfficial(@RequestBody BatchOpenOrCloseOfficialParam param){
+        return qwSopTempService.batchOpenOrCloseOfficial(param);
+    }
 }

+ 6 - 0
fs-company/src/main/java/com/fs/company/controller/qw/QwUserController.java

@@ -952,4 +952,10 @@ public class QwUserController extends BaseController
         List<QwUserVO> list = qwUserService.selectQwUserListByCompanyIdAndCorpIdAndNickName(companyId, corpId, nickName);
         return getDataTable(list);
     }
+
+    @GetMapping("/updateFastGptRoleStatusById/{id}")
+    public R updateFastGptRoleStatusById(@PathVariable Long id)
+    {
+        return qwUserService.updateQwUserFastGptRoleStatusById(id);
+    }
 }

+ 1 - 0
fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -132,6 +132,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/druid/**").anonymous()
                 .antMatchers("/qw/data/**").anonymous()
                 .antMatchers("/qw/user/selectCloudByCompany").anonymous()
+                .antMatchers("/live/LiveMixLiuTestOpen/**").anonymous()
                 // 除上面外的所有请求全部需要鉴权认证
                 .anyRequest().authenticated()
                 .and()

+ 21 - 5
fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java

@@ -30,19 +30,18 @@ import com.fs.hisStore.dto.ExpressInfoDTO;
 import com.fs.hisStore.dto.StoreOrderProductDTO;
 import com.fs.hisStore.enums.OrderLogEnum;
 import com.fs.hisStore.enums.ShipperCodeEnum;
-import com.fs.hisStore.param.FsStoreOrderBindCustomerParam;
-import com.fs.hisStore.param.FsStoreOrderCreateUserParam;
-import com.fs.hisStore.param.FsStoreOrderFinishParam;
-import com.fs.hisStore.param.FsStoreOrderParam;
+import com.fs.hisStore.param.*;
 import com.fs.hisStore.service.*;
 import com.fs.hisStore.vo.*;
 import com.fs.system.service.ISysConfigService;
+import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletRequest;
+import java.math.BigDecimal;
 import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -183,7 +182,7 @@ public class FsStoreOrderScrmController extends BaseController
                 if (vo.getUserAddress()!=null){
                     vo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
                 }
-
+                vo.setCost(BigDecimal.ZERO);
             }
         }
 
@@ -392,6 +391,7 @@ public class FsStoreOrderScrmController extends BaseController
                     catch (Exception e){
                     }
                 }
+                vo.setCost(BigDecimal.ZERO);
             }
         }
         ExcelUtil<FsStoreOrderItemExportVO> util = new ExcelUtil<FsStoreOrderItemExportVO>(FsStoreOrderItemExportVO.class);
@@ -472,4 +472,20 @@ public class FsStoreOrderScrmController extends BaseController
         Integer createSalesOrderType = config.getCreateSalesOrderType();
         return R.ok().put("createSalesOrderType",createSalesOrderType);
     }
+
+    @ApiOperation("批量审核订单")
+    @Log(title = "订单管理", businessType = BusinessType.UPDATE)
+    @PreAuthorize("@ss.hasPermi('store:storeOrder:batchAudit')")
+    @PostMapping("/batchAudit")
+    public R batchAuditOrder(@Validated @RequestBody FsStoreOrderBatchAuditParam param) {
+        if (param.getOrderIds() == null || param.getOrderIds().isEmpty()) {
+            return R.error("订单ID列表不能为空");
+        }
+        if (param.getIsAudit() == null) {
+            return R.error("审核状态不能为空");
+        }
+        int count = fsStoreOrderService.batchAuditOrder(param);
+        return R.ok("成功审核 " + count + " 条订单");
+    }
+
 }

+ 2 - 1
fs-company/src/main/java/com/fs/hisStore/controller/FsStoreStatisticsScrmController.java

@@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.Collectors;
@@ -80,7 +81,7 @@ public class FsStoreStatisticsScrmController extends BaseController
             List<JSONObject> jsonObjectList = storeOrderService.selectFsStoreOrderCounts(timeEntity.toMap());
             List<String> dates = jsonObjectList.stream().map(jsonObject -> jsonObject.getString("type")).collect(Collectors.toList());
             List<Integer> orderCount = jsonObjectList.stream().map(jsonObject -> jsonObject.getInteger("orderCount")).collect(Collectors.toList());
-            List<Integer> payPrice = jsonObjectList.stream().map(jsonObject -> jsonObject.getInteger("payPrice")).collect(Collectors.toList());
+            List<BigDecimal> payPrice = jsonObjectList.stream().map(jsonObject -> jsonObject.getBigDecimal("payPrice")).collect(Collectors.toList());
             return R.ok().put("list",list).put("dates",dates).put("orderCount",orderCount).put("payPrice",payPrice);
         }
         else {

+ 25 - 4
fs-company/src/main/java/com/fs/user/FsUserAdminController.java

@@ -9,14 +9,14 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ServletUtils;
-import com.fs.common.utils.StringUtils;
 import com.fs.company.cache.ICompanyUserCacheService;
+import com.fs.course.domain.FsUserCompanyUser;
 import com.fs.course.dto.BatchSendCourseDTO;
 import com.fs.course.param.FsCourseLinkCreateParam;
+import com.fs.course.service.IFsUserCompanyUserService;
 import com.fs.course.service.IFsUserCourseService;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
-
 import com.fs.his.domain.FsUser;
 import com.fs.his.service.IFsUserService;
 import com.fs.his.utils.PhoneUtil;
@@ -37,8 +37,6 @@ import org.springframework.web.bind.annotation.*;
 
 import java.util.Date;
 
-import static com.fs.his.utils.PhoneUtil.encryptPhone;
-
 @Api(tags = "会员管理接口")
 @RestController
 @Slf4j
@@ -64,6 +62,9 @@ public class FsUserAdminController extends BaseController {
     @Autowired
     private OpenIMService openIMService;
 
+    @Autowired
+    private IFsUserCompanyUserService fsUserCompanyUserService;
+
     @PreAuthorize("@ss.hasPermi('user:fsUser:list')")
     @PostMapping("/list")
     @ApiOperation("会员列表(与移动端使用的相同查询)")
@@ -146,6 +147,15 @@ public class FsUserAdminController extends BaseController {
         return AjaxResult.success(fsUserService.selectFsUserPageListVOByUserId(userId));
     }
 
+    /**
+     * 获取项目用户详细信息
+     */
+    @GetMapping(value = "/member/{id}")
+    public AjaxResult getMemberInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(fsUserService.selectFsMemberUserPageListVOById(id));
+    }
+
     /**
      * 修改用户
      */
@@ -157,6 +167,17 @@ public class FsUserAdminController extends BaseController {
         return toAjax(fsUserService.updateFsUser(fsUser));
     }
 
+    /**
+     * 修改用户
+     */
+    @PreAuthorize("@ss.hasPermi('user:fsUser:edit')")
+    @Log(title = "用户", businessType = BusinessType.UPDATE)
+    @PutMapping("/member")
+    public AjaxResult editMemberUser(@RequestBody FsUserCompanyUser fsUser)
+    {
+        return toAjax(fsUserCompanyUserService.updateFsUserCompanyUser(fsUser));
+    }
+
 
     @ApiOperation("后台会员批量发送课程消息")
     @PostMapping("/batchSendCourse")

+ 5 - 0
fs-live-app/src/main/java/com/fs/framework/aspectj/LiveWatchUserAspect.java

@@ -8,6 +8,9 @@ import org.aspectj.lang.JoinPoint;
 import org.aspectj.lang.annotation.AfterReturning;
 import org.aspectj.lang.annotation.Aspect;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
 import org.springframework.stereotype.Component;
 
 import java.util.Arrays;
@@ -17,11 +20,13 @@ import java.util.Set;
 @Aspect
 @Component
 @Slf4j
+@Order(Ordered.LOWEST_PRECEDENCE - 1)  // 调整切面优先级
 public class LiveWatchUserAspect {
 
 
 
     @Autowired
+    @Lazy
     private ILiveWatchUserService liveWatchUserService;
 
     @AfterReturning(pointcut = "execution(* com.fs.live.service.impl.LiveWatchUserServiceImpl.insertLiveWatchUser(..)) || " +

+ 107 - 32
fs-live-app/src/main/java/com/fs/live/task/Task.java

@@ -33,12 +33,14 @@ import javax.annotation.PostConstruct;
 import java.math.BigDecimal;
 import java.time.Instant;
 import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 import static com.fs.common.constant.LiveKeysConstant.*;
 import static com.fs.common.constant.LiveKeysConstant.LIVE_COUPON_NUM;
+import static com.fs.live.websocket.service.WebSocketServer.USER_ENTRY_TIME_KEY;
 
 @Component
 @AllArgsConstructor
@@ -184,17 +186,17 @@ public class Task {
                                 .mapToLong(LiveVideo::getDuration)
                                 .sum();
                     }
-                    
+
                     // 如果视频时长大于0,将直播间信息存入Redis
                     if (videoDuration > 0 && live.getStartTime() != null) {
                         Map<String, Object> tagMarkInfo = new HashMap<>();
                         tagMarkInfo.put("liveId", live.getLiveId());
                         tagMarkInfo.put("startTime", live.getStartTime().atZone(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli());
                         tagMarkInfo.put("videoDuration", videoDuration);
-                        
+
                         String tagMarkKey = String.format(LiveKeysConstant.LIVE_TAG_MARK_CACHE, live.getLiveId());
                         redisCache.setCacheObject(tagMarkKey, JSON.toJSONString(tagMarkInfo), 24, TimeUnit.HOURS);
-                        log.info("直播间开启,已加入打标签缓存: liveId={}, startTime={}, videoDuration={}", 
+                        log.info("直播间开启,已加入打标签缓存: liveId={}, startTime={}, videoDuration={}",
                                 live.getLiveId(), live.getStartTime(), videoDuration);
                     }
                 } catch (Exception e) {
@@ -415,7 +417,7 @@ public class Task {
         for (Live openRewardLive : openRewardLives) {
             String configJson = openRewardLive.getConfigJson();
             LiveWatchConfig config = JSON.parseObject(configJson, LiveWatchConfig.class);
-            if (config.getEnabled()) {
+            if (config.getEnabled() && 1 == config.getParticipateCondition()) {
                 List<LiveWatchUser> liveWatchUsers = liveWatchUserService.checkOnlineNoRewardUser(openRewardLive.getLiveId(), now);
                 if (liveWatchUsers == null || liveWatchUsers.isEmpty()) {
                     continue;
@@ -535,19 +537,19 @@ public class Task {
                 // 回放:使用 Redis 中的数据减去直播的数据,得到回放的数据
                 String likeKey = "live:like:" + liveData.getLiveId();
                 String totalViewsKey = TOTAL_VIEWS_KEY + liveData.getLiveId();
-                
+
                 // 从 Redis 获取总数据(直播+回放)
                 Long totalLikeCount = getAsLong(redisCache, likeKey);
                 Long totalViewCount = getAsLong(redisCache, totalViewsKey);
-                
+
                 // 获取数据库中直播的数据
                 Long liveLikeCount = liveData.getLikes() != null ? liveData.getLikes() : 0L;
                 Long liveViewCount = liveData.getTotalViews() != null ? liveData.getTotalViews() : 0L;
-                
+
                 // 回放数据 = Redis总数据 - 直播数据
                 Long replayLikeNum = totalLikeCount - liveLikeCount;
                 Long replayViewNum = totalViewCount - liveViewCount;
-                
+
                 // 确保回放数据不为负数
                 if (replayLikeNum < 0L) {
                     replayLikeNum = 0L;
@@ -555,7 +557,7 @@ public class Task {
                 if (replayViewNum < 0L) {
                     replayViewNum = 0L;
                 }
-                
+
                 // 更新回放数据
                 liveData.setReplayLikeNum(replayLikeNum);
                 liveData.setReplayViewNum(replayViewNum);
@@ -633,7 +635,7 @@ public class Task {
     }
 
     /**
-     * 定时扫描开启的直播间,检查是否到了打标签的时间
+     * 定时扫描开启的直播间,检查是否到了打标签的时间,然后把正在看直播的用户拆分为 直播用户和回放用户
      * 每10秒执行一次
      */
     @Scheduled(cron = "0/10 * * * * ?")
@@ -644,11 +646,11 @@ public class Task {
             // 获取所有打标签缓存的key
             String pattern = String.format(LiveKeysConstant.LIVE_TAG_MARK_CACHE, "*");
             Set<String> keys = redisCache.redisTemplate.keys(pattern);
-            
+
             if (keys == null || keys.isEmpty()) {
                 return;
             }
-            
+
             long currentTimeMillis = System.currentTimeMillis();
             LocalDateTime now = LocalDateTime.now();
             List<Long> processedLiveIds = new ArrayList<>();
@@ -660,19 +662,19 @@ public class Task {
                     if (cacheValue == null) {
                         continue;
                     }
-                    
+
                     String jsonStr = cacheValue.toString();
                     JSONObject tagMarkInfo = JSON.parseObject(jsonStr);
                     Long liveId = tagMarkInfo.getLong("liveId");
                     Long startTimeMillis = tagMarkInfo.getLong("startTime");
                     Long videoDuration = tagMarkInfo.getLong("videoDuration");
-                    
+
                     if (liveId == null || startTimeMillis == null || videoDuration == null || videoDuration <= 0) {
-                        log.warn("直播间打标签缓存信息不完整: key={}, liveId={}, startTime={}, videoDuration={}", 
+                        log.warn("直播间打标签缓存信息不完整: key={}, liveId={}, startTime={}, videoDuration={}",
                                 key, liveId, startTimeMillis, videoDuration);
                         continue;
                     }
-                    
+
                     // 查询直播间信息
                     Live live = liveService.selectLiveDbByLiveId(liveId);
                     if (live == null || live.getStartTime() == null) {
@@ -789,7 +791,7 @@ public class Task {
                     log.error("处理直播间打标签缓存异常: key={}, error={}", key, e.getMessage(), e);
                 }
             }
-            
+
             // 删除已处理的直播间缓存
             for (Long liveId : processedLiveIds) {
                 try {
@@ -812,12 +814,12 @@ public class Task {
     @DistributeLock(key = "scanLiveWatchUserStatus", scene = "task")
     public void scanLiveWatchUserStatus() {
         try {
+            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
             // 查询所有正在直播的直播间
             List<Live> activeLives = liveService.selectNoEndLiveList();
             if (activeLives == null || activeLives.isEmpty()) {
                 return;
             }
-
             for (Live live : activeLives) {
                 try {
                     Long liveId = live.getLiveId();
@@ -860,29 +862,37 @@ public class Task {
                             }
 
                             // 获取用户的在线观看时长
-                            Long onlineSeconds = user.getOnlineSeconds();
+                            String entryTimeKey = String.format(USER_ENTRY_TIME_KEY, liveId, userId);
+                            Long existingEntryTime = redisCache.getCacheObject(entryTimeKey);
+                            Long onlineSeconds = user.getOnlineSeconds() ==null ? 0L : user.getOnlineSeconds();
+                            if(null != existingEntryTime){
+                                onlineSeconds = onlineSeconds + ((System.currentTimeMillis() - existingEntryTime)/1000);
+                            }
                             if (onlineSeconds == null || onlineSeconds <= 0) {
                                 continue;
                             }
-                            
+
                             // 获取用户的 companyId 和 companyUserId
                             LiveUserFirstEntry liveUserFirstEntry =
                                     liveUserFirstEntryService.selectEntityByLiveIdUserIdWithCache(liveId, userId);
                             if (liveUserFirstEntry == null) {
                                 continue;
                             }
-                            
+
                             Long qwUserId = liveUserFirstEntry.getQwUserId();
                             Long externalContactId = liveUserFirstEntry.getExternalContactId();
 
                             if (qwUserId == null || qwUserId <= 0 || externalContactId == null || externalContactId <= 0) {
                                 continue;
                             }
-
+                            //更新最新用户活跃时间
+                            String liveUserWatchLogKey = String.format(LIVE_USER_WATCH_LOG_CACHE, liveId, userId,externalContactId,qwUserId);
+                            LocalDateTime now = LocalDateTime.now();
+                            redisCache.setCacheObject(liveUserWatchLogKey,formatter.format(now),5,TimeUnit.MINUTES);
                             // 使用 updateLiveWatchLogTypeByDuration 的逻辑更新观看记录状态
                             updateLiveWatchLogTypeByDuration(liveId, userId, qwUserId, externalContactId,
                                     onlineSeconds, totalVideoDuration, updateLog);
-                            
+
                         } catch (Exception e) {
                             log.error("处理用户观看记录状态异常: liveId={}, userId={}, error={}",
                                     liveId, user.getUserId(), e.getMessage(), e);
@@ -896,8 +906,11 @@ public class Task {
                             List<LiveWatchLog> batch = updateLog.subList(i, end);
                             liveWatchLogService.batchUpdateLiveWatchLog(batch);
                         }
+                        for (LiveWatchLog liveWatchLog : updateLog) {
+                            redisCache.setCacheObject("live:watch:log:cache:" + liveWatchLog.getLogId(), liveWatchLog, 1, TimeUnit.HOURS);
+                        }
                     }
-                    
+
                 } catch (Exception e) {
                     log.error("处理直播间观看记录状态异常: liveId={}, error={}",
                             live.getLiveId(), e.getMessage(), e);
@@ -923,7 +936,6 @@ public class Task {
             // 查询 LiveWatchLog
             LiveWatchLog queryLog = new LiveWatchLog();
             queryLog.setLiveId(liveId);
-            queryLog.setUserId(userId);
             queryLog.setQwUserId(String.valueOf(qwUserId));
             queryLog.setExternalContactId(exId);
 
@@ -940,13 +952,13 @@ public class Task {
                 boolean needUpdate = false;
                 Integer newLogType = log.getLogType();
 
-                // ① 如果在线时长 <= 3分钟,修改 logType 为 4(看课中断)
-                if (onlineSeconds <= 180) { // 3分钟 = 180秒
-                    newLogType = 4;
-                    needUpdate = true;
-                }
-                // ③ 如果直播视频 >= 40分钟,在线时长 >= 30分钟,logType 设置为 2(完课)
-                else if (totalVideoDuration >= 2400 && onlineSeconds >= 1800) { // 40分钟 = 2400秒,30分钟 = 1800秒
+                // ① 如果在线时长 <= 3分钟,修改 logType 为 4(看课中断) lmx-这个逻辑不合理,不能这样判定看课中断
+//                if (onlineSeconds <= 180) { // 3分钟 = 180秒
+//                    newLogType = 4;
+//                    needUpdate = true;
+//                } else
+                    // ③ 如果直播视频 >= 40分钟,在线时长 >= 30分钟,logType 设置为 2(完课)
+                if (totalVideoDuration >= 2400 && onlineSeconds >= 1800) { // 40分钟 = 2400秒,30分钟 = 1800秒
                     newLogType = 2;
                     log.setFinishTime(now);
                     needUpdate = true;
@@ -970,6 +982,69 @@ public class Task {
         }
     }
 
+    /**
+     * 每分钟扫描一次用户在线状态用于更新用户观看记录值
+     */
+    @Scheduled(cron = "0 0/1 * * * ?")
+    @DistributeLock(key = "updateLiveWatchUserStatus", scene = "task")
+    public void updateLiveWatchUserStatus() {
+        try {
+            Set<String> keys = redisCache.redisTemplate.keys("live:user:watch:log:*");
+            LocalDateTime now = LocalDateTime.now();
+            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+            List<LiveWatchLog> updateLog = new ArrayList<>();
+            if (keys != null && !keys.isEmpty()) {
+                for (String key : keys) {
+                    String[] split = key.split(":");
+                    String cacheTime = redisCache.getCacheObject(key);
+                    //判断缓存的值是否已经距离现在超过一分钟
+                    if (StringUtils.isNotBlank(cacheTime)) {
+                        try {
+                            LocalDateTime cachedDateTime = LocalDateTime.parse(cacheTime, formatter);
+                            // 比较时间,判断是否超过1分钟(60秒)
+                            long secondsBetween = java.time.Duration.between(cachedDateTime, now).getSeconds();
+                            if (secondsBetween >= 60) {
+                                // 距离上次记录已超过1分钟,更新状态为看课中断
+                                // 查询 LiveWatchLog
+                                LiveWatchLog queryLog = new LiveWatchLog();
+                                queryLog.setLiveId(Long.valueOf(split[4]));
+                                queryLog.setQwUserId(String.valueOf(split[7]));
+                                queryLog.setExternalContactId(Long.valueOf(split[6]));
+                                queryLog.setLogType(1);
+                                List<LiveWatchLog> logs = liveWatchLogService.selectLiveWatchLogList(queryLog);
+                                if (logs != null && !logs.isEmpty()) {
+                                    for (LiveWatchLog log : logs) {
+                                        if (log.getLogType() != null && log.getLogType() == 2) {
+                                            continue;
+                                        }
+                                        log.setLogType(4);
+                                        updateLog.add(log);
+                                    }
+                                }
+                            }
+                        } catch (Exception e) {
+                            log.error("解析缓存时间失败: cacheTime={}, error={}", cacheTime, e.getMessage());
+                        }
+                    }
+                }
+                // 批量插入回放用户数据
+                if (!updateLog.isEmpty()) {
+                    int batchSize = 500;
+                    for (int i = 0; i < updateLog.size(); i += batchSize) {
+                        int end = Math.min(i + batchSize, updateLog.size());
+                        List<LiveWatchLog> batch = updateLog.subList(i, end);
+                        liveWatchLogService.batchUpdateLiveWatchLog(batch);
+                    }
+                    for (LiveWatchLog liveWatchLog : updateLog) {
+                        redisCache.setCacheObject("live:watch:log:cache:" + liveWatchLog.getLogId(), liveWatchLog, 1, TimeUnit.HOURS);
+                    }
+                }
+            }
+        } catch (Exception ex) {
+            log.error("每分钟扫描一次用户在线状态用于更新用户观看记录值: error={}", ex.getMessage(), ex);
+        }
+    }
+
     /**
      * 批量同步Redis中的观看时长到数据库
      * 每2分钟执行一次,减少数据库压力

+ 164 - 61
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -36,7 +36,10 @@ import javax.websocket.*;
 import javax.websocket.server.ServerEndpoint;
 import java.io.EOFException;
 import java.io.IOException;
+import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
 import java.util.concurrent.*;
 import java.util.concurrent.locks.Lock;
@@ -83,9 +86,9 @@ public class WebSocketServer {
     private final ILiveVideoService liveVideoService = SpringUtils.getBean(ILiveVideoService.class);
     private final ILiveCompletionPointsRecordService completionPointsRecordService = SpringUtils.getBean(ILiveCompletionPointsRecordService.class);
     private static Random random = new Random();
-    
+
     // Redis key 前缀:用户进入直播间时间
-    private static final String USER_ENTRY_TIME_KEY = "live:user:entry:time:%s:%s"; // liveId:userId
+    public static final String USER_ENTRY_TIME_KEY = "live:user:entry:time:%s:%s"; // liveId:userId
 
     // 直播间在线用户缓存
 //    private static final ConcurrentHashMap<Long, Integer> liveOnlineUsers = new ConcurrentHashMap<>();
@@ -135,11 +138,17 @@ public class WebSocketServer {
 
             LiveWatchUser liveWatchUserVO = liveWatchUserService.join(fsUser,liveId, userId, location);
             room.put(userId, session);
-            
+
             // 存储用户进入直播间的时间到 Redis(用于计算在线时长)
+            // 如果已经存在进入时间,说明是重连,不应该覆盖,保持原来的进入时间
             String entryTimeKey = String.format(USER_ENTRY_TIME_KEY, liveId, userId);
-            redisCache.setCacheObject(entryTimeKey, System.currentTimeMillis(), 24, TimeUnit.HOURS);
-            
+            Long existingEntryTime = redisCache.getCacheObject(entryTimeKey);
+            if (existingEntryTime == null) {
+                // 首次连接,记录进入时间
+                redisCache.setCacheObject(entryTimeKey, System.currentTimeMillis(), 24, TimeUnit.HOURS);
+            }
+            // 如果是重连,不覆盖进入时间,保持原来的进入时间以便正确计算总时长
+
             // 直播间浏览量 +1
             redisCache.incr(PAGE_VIEWS_KEY + liveId, 1);
 
@@ -216,7 +225,7 @@ public class WebSocketServer {
                 }
             } else {
                 // 这个用户A邀请用户b,b的业绩算a的销售的
-                if (companyUserId == -2L) {
+                if (companyId == -2L) {
                     LiveUserFirstEntry clientB = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveId, companyUserId);
                     companyId = clientB.getCompanyId();
                     companyUserId = clientB.getCompanyUserId();
@@ -240,6 +249,9 @@ public class WebSocketServer {
             }
             redisCache.setCacheObject( "live:user:first:entry:" + liveId + ":" + userId, liveUserFirstEntry,1, TimeUnit.HOURS);
 
+            // 推送完课积分倒计时配置信息给前端
+            sendCompletionPointsConfigToUser(session, liveId, userId, live);
+
 
         } else {
             adminRoom.add(session);
@@ -347,7 +359,7 @@ public class WebSocketServer {
                     long watchUserId = (long) userProperties.get("userId");
 
 
-                    
+
                     if (msg.getData() != null && !msg.getData().isEmpty()) {
                         try {
                             Long currentDuration = Long.parseLong(msg.getData());
@@ -356,7 +368,8 @@ public class WebSocketServer {
                             if (currentLive == null) {
                                 break;
                             }
-                            
+
+
                             // 判断直播是否已开始:status=2(直播中) 或 当前时间 >= 开播时间
                             boolean isLiveStarted = false;
                             if (currentLive.getStatus() != null && currentLive.getStatus() == 2) {
@@ -364,22 +377,21 @@ public class WebSocketServer {
                                 isLiveStarted = true;
                             } else if (currentLive.getStartTime() != null) {
                                 // 判断当前时间是否已超过开播时间
-                                LocalDateTime now = java.time.LocalDateTime.now();
+                                LocalDateTime now = LocalDateTime.now();
                                 isLiveStarted = now.isAfter(currentLive.getStartTime()) || now.isEqual(currentLive.getStartTime());
                             }
-                            
-                            if (!isLiveStarted) {
-                                log.debug("[心跳-观看时长] 直播未开始(开播倒计时中),不统计观看时长, liveId={}, status={}, startTime={}", 
-                                        liveId, currentLive.getStatus(), currentLive.getStartTime());
-                                break;
-                            }
-                            
-                            log.debug("[心跳-观看时长] 直播已开始,统计观看时长, liveId={}, userId={}, duration={}秒", 
-                                    liveId, watchUserId, currentDuration);
-                            
+
                             // 使用Hash结构存储:一个直播间一个Hash,包含所有用户的时长
                             String hashKey = "live:watch:duration:hash:" + liveId;
                             String userIdField = String.valueOf(watchUserId);
+
+                            if (!isLiveStarted) {
+                                redisCache.hashDelete(hashKey, userIdField);
+                                log.debug("[心跳-观看时长] 直播未开始,清除预播时长, liveId={}, userId={}", liveId, watchUserId);
+                                break;
+                            }
+
+                            // 直播已开始,记录观看时长
                             // 获取现有时长
                             Object existingDuration = redisCache.hashGet(hashKey, userIdField);
                             // 只有当新的时长更大时才更新
@@ -393,11 +405,11 @@ public class WebSocketServer {
 
                             }
                         } catch (Exception e) {
-                            log.error("[心跳-观看时长] 更新失败, liveId={}, userId={}, data={}", 
+                            log.error("[心跳-观看时长] 更新失败, liveId={}, userId={}, data={}",
                                     liveId, watchUserId, msg.getData(), e);
                         }
                     }
-                    
+
                     sendMessage(session, JSONObject.toJSONString(R.ok().put("data", msg)));
                     break;
                 case "sendMsg":
@@ -738,7 +750,7 @@ public class WebSocketServer {
      */
     public void broadcastWebMessage(Long liveId, String message) {
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
-        
+
         if (room.isEmpty()) {
             return;
         }
@@ -864,7 +876,7 @@ public class WebSocketServer {
         for (Map.Entry<Long, ConcurrentHashMap<Long, Session>> roomEntry : rooms.entrySet()) {
             Long liveId = roomEntry.getKey();
             ConcurrentHashMap<Long, Session> room = roomEntry.getValue();
-            
+
             // 如果房间为空,跳过
             if (room.isEmpty()) {
                 continue;
@@ -876,12 +888,12 @@ public class WebSocketServer {
             for (Map.Entry<Long, Session> userEntry : room.entrySet()) {
                 Long userId = userEntry.getKey();
                 Session session = userEntry.getValue();
-                
+
                 if (session == null) {
                     toRemove.add(userId);
                     continue;
                 }
-                
+
                 Long lastHeartbeat = heartbeatCache.get(session.getId());
                 if (lastHeartbeat != null && (currentTime - lastHeartbeat) > HEARTBEAT_TIMEOUT) {
                     toRemove.add(userId);
@@ -951,11 +963,11 @@ public class WebSocketServer {
      */
     public void broadcastLikeMessage(Long liveId, String message) {
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
-        
+
         if (room.isEmpty()) {
             return;
         }
-        
+
         // 使用快照遍历,避免并发修改
         for (Map.Entry<Long, Session> entry : room.entrySet()) {
             Session session = entry.getValue();
@@ -1116,31 +1128,31 @@ public class WebSocketServer {
             // 从 Redis 获取用户进入时间
             String entryTimeKey = String.format(USER_ENTRY_TIME_KEY, liveId, userId);
             Long entryTime = redisCache.getCacheObject(entryTimeKey);
-            
+
             if (entryTime == null) {
                 // 如果没有进入时间记录,可能是旧数据,跳过
                 return;
             }
-            
+
             long currentTimeMillis = System.currentTimeMillis();
             Date now = new Date();
-            
+
             // 计算在线时长(秒)
             long durationSeconds = (currentTimeMillis - entryTime) / 1000;
-            
+
             if (durationSeconds <= 0) {
                 return;
             }
-            
+
             // 获取当前直播/回放状态
             Map<String, Integer> flagMap = liveWatchUserService.getLiveFlagWithCache(liveId);
             Integer currentLiveFlag = flagMap.get("liveFlag");
             Integer currentReplayFlag = flagMap.get("replayFlag");
-            
+
             // 查询用户记录
             LiveWatchUserEntry liveWatchUser = liveWatchUserService.selectLiveWatchAndCompanyUserByFlag(
                     liveId, userId, currentLiveFlag, currentReplayFlag);
-            
+
             if (liveWatchUser != null) {
                 // 累加在线时长
                 Long onlineSeconds = liveWatchUser.getOnlineSeconds();
@@ -1149,7 +1161,7 @@ public class WebSocketServer {
                 }
                 liveWatchUser.setOnlineSeconds(onlineSeconds + durationSeconds);
                 liveWatchUser.setUpdateTime(now);
-                
+
                 // 更新数据库
                 liveWatchUserService.updateLiveWatchUserEntry(liveWatchUser);
                 // 如果 LiveWatchUserEntry 存在,并且当前是直播状态(liveFlag = 1),更新 LiveWatchLog
@@ -1161,15 +1173,15 @@ public class WebSocketServer {
 //                            liveWatchUser.getOnlineSeconds());
 //                }
             }
-            
+
             // 删除 Redis 中的进入时间记录
             redisCache.deleteObject(entryTimeKey);
         } catch (Exception e) {
-            log.error("更新用户在线时长异常:liveId={}, userId={}, error={}", 
+            log.error("更新用户在线时长异常:liveId={}, userId={}, error={}",
                     liveId, userId, e.getMessage(), e);
         }
     }
-    
+
     /**
      * 在连接时更新 LiveWatchLog 的 logType
      * 如果 logType 类型不是 2,修改 logType 类型为 1(看课中)
@@ -1178,10 +1190,9 @@ public class WebSocketServer {
         try {
             LiveWatchLog queryLog = new LiveWatchLog();
             queryLog.setLiveId(liveId);
-            queryLog.setUserId(userId);
             queryLog.setQwUserId(String.valueOf(qwUserId));
             queryLog.setExternalContactId(externalContactId);
-            
+            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
             List<LiveWatchLog> logs = liveWatchLogService.selectLiveWatchLogList(queryLog);
             if (logs != null && !logs.isEmpty()) {
                 for (LiveWatchLog log : logs) {
@@ -1189,15 +1200,18 @@ public class WebSocketServer {
                     if (log.getLogType() == null || log.getLogType() != 2) {
                         log.setLogType(1);
                         liveWatchLogService.updateLiveWatchLog(log);
+                        String liveUserWatchLogKey = String.format(LIVE_USER_WATCH_LOG_CACHE, liveId, userId,externalContactId,qwUserId);
+                        LocalDateTime now = LocalDateTime.now();
+                        redisCache.setCacheObject(liveUserWatchLogKey,formatter.format(now),5,TimeUnit.MINUTES);
                     }
                 }
             }
         } catch (Exception e) {
-            log.error("更新 LiveWatchLog logType 异常(连接时):liveId={}, userId={}, error={}", 
+            log.error("更新 LiveWatchLog logType 异常(连接时):liveId={}, userId={}, error={}",
                     liveId, userId, e.getMessage(), e);
         }
     }
-    
+
     /**
      * 实时更新用户看课状态(在心跳时调用)
      * 在直播期间实时更新用户的看课状态,而不是等到关闭 WebSocket 或清理无效会话时才更新
@@ -1210,36 +1224,36 @@ public class WebSocketServer {
             // 获取当前直播/回放状态
             Map<String, Integer> flagMap = liveWatchUserService.getLiveFlagWithCache(liveId);
             Integer currentLiveFlag = flagMap.get("liveFlag");
-            
+
             // 只在直播状态(liveFlag = 1)时更新
             if (currentLiveFlag == null || currentLiveFlag != 1) {
                 return;
             }
-            
+
             // 获取用户的 companyId 和 companyUserId(使用带缓存的查询方法)
             LiveUserFirstEntry liveUserFirstEntry = liveUserFirstEntryService.selectEntityByLiveIdUserIdWithCache(liveId, userId);
             if (liveUserFirstEntry == null) {
                 return;
             }
-            
+
             Long companyId = liveUserFirstEntry.getCompanyId();
             Long companyUserId = liveUserFirstEntry.getCompanyUserId();
-            
+
             // 如果 companyId 和 companyUserId 有效,则更新看课状态
             if (companyId != null && companyId > 0 && companyUserId != null && companyUserId > 0) {
                 // 检查是否达到关键观看时长节点,在这些节点实时更新
                 // 关键节点:3分钟(180秒)、20分钟(1200秒)、30分钟(1800秒)
                 boolean isKeyDuration = (watchDuration == 180 || watchDuration == 1200 || watchDuration == 1800) ||
                                        (watchDuration > 180 && watchDuration % 60 == 0); // 每分钟更新一次
-                
+
                 // 使用 Redis 缓存控制更新频率,避免频繁更新数据库
                 // 策略:在关键节点立即更新,其他时候每60秒更新一次
                 String updateLockKey = "live:watch:log:update:lock:" + liveId + ":" + userId;
                 String lastUpdateKey = "live:watch:log:last:duration:" + liveId + ":" + userId;
-                
+
                 // 获取上次更新的时长
                 Long lastUpdateDuration = redisCache.getCacheObject(lastUpdateKey);
-                
+
                 // 如果达到关键节点,或者距离上次更新已超过60秒,则更新
                 boolean shouldUpdate = false;
                 if (isKeyDuration) {
@@ -1249,11 +1263,11 @@ public class WebSocketServer {
                     // 每60秒更新一次
                     shouldUpdate = true;
                 }
-                
+
                 if (shouldUpdate) {
                     // 使用分布式锁,避免并发更新(锁超时时间10秒)
                     Boolean canUpdate = redisCache.setIfAbsent(updateLockKey, "1", 10, TimeUnit.SECONDS);
-                    
+
                     if (Boolean.TRUE.equals(canUpdate)) {
                         // 异步更新,避免阻塞心跳处理
                         CompletableFuture.runAsync(() -> {
@@ -1262,7 +1276,7 @@ public class WebSocketServer {
                                 // 更新上次更新的时长
                                 redisCache.setCacheObject(lastUpdateKey, watchDuration, 2, TimeUnit.HOURS);
                             } catch (Exception e) {
-                                log.error("实时更新看课状态异常:liveId={}, userId={}, error={}", 
+                                log.error("实时更新看课状态异常:liveId={}, userId={}, error={}",
                                         liveId, userId, e.getMessage(), e);
                             } finally {
                                 // 释放锁
@@ -1273,11 +1287,11 @@ public class WebSocketServer {
                 }
             }
         } catch (Exception e) {
-            log.error("实时更新看课状态异常:liveId={}, userId={}, error={}", 
+            log.error("实时更新看课状态异常:liveId={}, userId={}, error={}",
                     liveId, userId, e.getMessage(), e);
         }
     }
-    
+
     /**
      * 根据在线时长更新 LiveWatchLog 的 logType
      * @param liveId 直播间ID
@@ -1286,7 +1300,7 @@ public class WebSocketServer {
      * @param companyUserId 销售ID
      * @param onlineSeconds 在线时长(秒)
      */
-    private void updateLiveWatchLogTypeByDuration(Long liveId, Long userId, Long companyId, 
+    private void updateLiveWatchLogTypeByDuration(Long liveId, Long userId, Long companyId,
                                                    Long companyUserId, Long onlineSeconds) {
         try {
             // 获取直播视频总时长(videoType = 1 的视频,使用带缓存的查询方法)
@@ -1298,14 +1312,13 @@ public class WebSocketServer {
                         .mapToLong(LiveVideo::getDuration)
                         .sum();
             }
-            
+
             // 查询 LiveWatchLog
             LiveWatchLog queryLog = new LiveWatchLog();
             queryLog.setLiveId(liveId);
-            queryLog.setUserId(userId);
             queryLog.setCompanyId(companyId);
             queryLog.setCompanyUserId(companyUserId);
-            
+
             List<LiveWatchLog> logs = liveWatchLogService.selectLiveWatchLogList(queryLog);
             if (logs == null || logs.isEmpty()) {
                 return;
@@ -1314,7 +1327,7 @@ public class WebSocketServer {
             for (LiveWatchLog log : logs) {
                 boolean needUpdate = false;
                 Integer newLogType = log.getLogType();
-                
+
                 // ① 如果在线时长 <= 3分钟,修改 logType 为 4(看课中断)
                 if (onlineSeconds <= 180) { // 3分钟 = 180秒
                     newLogType = 4;
@@ -1332,7 +1345,7 @@ public class WebSocketServer {
                     log.setFinishTime(now);
                     needUpdate = true;
                 }
-                
+
                 // 如果 logType 已经是 2(完课),不再更新
                 if (needUpdate && (log.getLogType() == null || log.getLogType() != 2)) {
                     log.setLogType(newLogType);
@@ -1340,7 +1353,7 @@ public class WebSocketServer {
                 }
             }
         } catch (Exception e) {
-            log.error("根据在线时长更新 LiveWatchLog logType 异常:liveId={}, userId={}, error={}", 
+            log.error("根据在线时长更新 LiveWatchLog logType 异常:liveId={}, userId={}, error={}",
                     liveId, userId, e.getMessage(), e);
         }
     }
@@ -1388,5 +1401,95 @@ public class WebSocketServer {
         }
     }
 
+    /**
+     * 向用户推送完课积分倒计时配置信息
+     * 在用户连接WebSocket时调用,让前端能够显示倒计时
+     * @param session WebSocket会话
+     * @param liveId 直播间ID
+     * @param userId 用户ID
+     * @param live 直播信息
+     */
+    private void sendCompletionPointsConfigToUser(Session session, Long liveId, Long userId, Live live) {
+        try {
+
+            boolean isLiveStarted = false;
+            if (live.getStatus() != null && live.getStatus() == 2) {
+                isLiveStarted = true;
+            } else if (live.getStartTime() != null) {
+                LocalDateTime now = LocalDateTime.now();
+                isLiveStarted = now.isAfter(live.getStartTime()) || now.isEqual(live.getStartTime());
+            }
+
+            if (!isLiveStarted) {
+                // 直播未开始,不推送完课配置
+                log.debug("[完课配置推送] 直播未开始,跳过推送, liveId={}, userId={}", liveId, userId);
+                return;
+            }
+
+            String configJson = live.getConfigJson();
+            if (configJson == null || configJson.isEmpty()) {
+                return;
+            }
+
+            JSONObject jsonConfig = JSON.parseObject(configJson);
+            boolean enabled = jsonConfig.getBooleanValue("enabled");
+            if (!enabled) {
+                return;
+            }
+
+            Integer completionRate = jsonConfig.getInteger("completionRate");
+            if (completionRate == null || completionRate <= 0 || completionRate > 100) {
+                return;
+            }
+
+            // 3. 计算完课所需观看时长
+            Long videoDuration = live.getDuration();
+            if (videoDuration == null || videoDuration <= 0) {
+                return;
+            }
+
+            // 完课所需时长(秒) = 视频总时长 × 完课比例 / 100
+            long requiredDuration = (long) Math.ceil(videoDuration * completionRate / 100.0);
+
+            // 4. 获取用户当前观看时长
+            String hashKey = "live:watch:duration:hash:" + liveId;
+            String userIdField = String.valueOf(userId);
+            Object existingDuration = redisCache.hashGet(hashKey, userIdField);
+            long currentDuration = existingDuration != null ? Long.parseLong(existingDuration.toString()) : 0L;
+
+            // 5. 检查今天是否已有完课记录
+            LocalDate today = LocalDate.now();
+            Date currentDate = Date.from(today.atStartOfDay(ZoneId.systemDefault()).toInstant());
+            LiveCompletionPointsRecord todayRecord = completionPointsRecordService.selectByUserAndDate(liveId, userId, currentDate);
+
+            boolean hasCompletedToday = (todayRecord != null);
+
+            // 6. 构建配置信息
+            JSONObject configData = new JSONObject();
+            configData.put("videoDuration", videoDuration);  // 视频总时长(秒)
+            configData.put("completionRate", completionRate);  // 完课比例(%)
+            configData.put("requiredDuration", requiredDuration);  // 完课所需时长(秒)
+            configData.put("currentDuration", currentDuration);  // 当前观看时长(秒)
+            configData.put("remainingDuration", Math.max(0, requiredDuration - currentDuration));  // 剩余时长(秒)
+            configData.put("hasCompletedToday", hasCompletedToday);  // 今天是否已完课
+
+            // 7. 推送配置消息
+            SendMsgVo sendMsgVo = new SendMsgVo();
+            sendMsgVo.setLiveId(liveId);
+            sendMsgVo.setUserId(userId);
+            sendMsgVo.setCmd("completionPointsConfig");
+            sendMsgVo.setMsg("完课积分配置");
+            sendMsgVo.setData(configData.toJSONString());
+
+            sendMessage(session, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+
+            log.debug("[完课配置推送] 推送成功, liveId={}, userId={}, 所需时长={}秒, 当前时长={}秒, 剩余={}秒",
+                    liveId, userId, requiredDuration, currentDuration, Math.max(0, requiredDuration - currentDuration));
+
+        } catch (Exception e) {
+            log.error("[完课配置推送] 推送失败, liveId={}, userId={}", liveId, userId, e);
+        }
+    }
+
 }
 

+ 1 - 1
fs-live-app/src/main/resources/application.yml

@@ -6,4 +6,4 @@ server:
 # Spring配置
 spring:
   profiles:
-    active: druid-bjzm-test
+    active: dev-test

+ 1 - 1
fs-quartz/src/main/java/com/fs/quartz/config/ScheduleConfig.java

@@ -49,7 +49,7 @@ public class ScheduleConfig
         // 可选,QuartzScheduler
         // 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了
         factory.setOverwriteExistingJobs(true);
-        // 设置自动启动,默认为true
+        // 设置自动启动,默认为true 切记调整为true
         factory.setAutoStartup(true);
 //        factory.setAutoStartup(false);
 

+ 6 - 0
fs-qw-api/Dockerfile

@@ -0,0 +1,6 @@
+FROM anolis-registry.cn-zhangjiakou.cr.aliyuncs.com/openanolis/openjdk:8-8.6
+# java版本,最好使用openjdk,而不是类似于Java:1.8
+COPY ./target/fs-qw-api.jar fs-qw-api.jar
+# 向外暴露的接口,最好与项目yml文件中的端口一致
+ENTRYPOINT ["java","-jar","fs-qw-api.jar"]
+# 执行启动命令java -jar

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

+ 91 - 9
fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java

@@ -694,6 +694,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         int type = content.getType();
         Long courseId = content.getCourseId();
         Long videoId = content.getVideoId();
+        Long liveId = content.getLiveId();
         Integer isOfficial = content.getIsOfficial() != null ? Integer.valueOf(content.getIsOfficial()) : 0;
 
 
@@ -746,13 +747,13 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
         if (StringUtils.isNotEmpty(logVo.getChatId())) {
             QwGroupChat groupChat = groupChatMap.get(logVo.getChatId());
-            ruleTimeVO.setSendType(6);
-            ruleTimeVO.setType(2);
             if (groupChat.getChatUserList() != null && !groupChat.getChatUserList().isEmpty()) {
                 QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, groupChat.getChatId(), groupChat.getName(), null, isOfficial, null,null);
+                ruleTimeVO.setSendType(6);
+                ruleTimeVO.setType(2);
                 handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
                         type, qwUserId, companyUserId, companyId, groupChat.getChatId(), welcomeText, qwUserName,
-                        null, true, miniAppId, groupChat,config, miniMap, null, sendMsgType,companies);
+                        null, true, miniAppId, groupChat,config, miniMap, null, sendMsgType,companies,liveId);
             }
 //            if (content.getIndex() == 0) {
 //                QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, groupChat.getChatId(), groupChat.getName(), null, isOfficial, null);
@@ -782,7 +783,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                     QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, contactId.getExternalContactId(), externalUserName, fsUserId, isOfficial, contactId.getExternalId(),contactId.getIsDaysNotStudy());
                     handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
                             type, qwUserId, companyUserId, companyId, externalId, welcomeText, qwUserName, fsUserId, false, miniAppId,
-                            null,config, miniMap, grade, sendMsgType,companies);
+                            null,config, miniMap, grade, sendMsgType,companies,liveId);
                 } catch (Exception e) {
                     log.error("处理 externalContactId {} 时发生异常: {}", contactId, e.getMessage(), e);
                 }
@@ -898,7 +899,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                                       String qwUserName, Long fsUserId, boolean isGroupChat, String miniAppId,
                                       QwGroupChat groupChat,CourseConfig config,
                                       Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
-                                      Integer grade, Integer sendMsgType ,List<Company> companies ) {
+                                      Integer grade, Integer sendMsgType ,List<Company> companies ,Long liveId) {
         switch (type) {
             case 1:
                 handleNormalMessage(sopLogs, content,companyUserId,companyId,isGroupChat,qwUserId,groupChat,externalId,logVo);
@@ -920,6 +921,9 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             case 7:
                 handleVoiceMessage(sopLogs, content, companyUserId);
                 break;
+            //直播间发送类型
+            case 20:
+                handleLiveMessage(sopLogs, content,companyUserId,companyId,isGroupChat,qwUserId,groupChat,externalId,logVo,liveId);
             default:
                 log.error("未知的消息类型 {},跳过处理。", type);
                 break;
@@ -1003,6 +1007,83 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         enqueueQwSopLogs(sopLogs);
     }
 
+    /**
+     * 处理直播消息
+     */
+    public void handleLiveMessage(QwSopLogs sopLogs,QwSopTempSetting.Content content, String companyUserId, String companyId,
+                                  boolean isGroupChat,String qwUserId,QwGroupChat groupChat,String externalId,SopUserLogsVo logVo,Long liveId){
+        // 深拷贝 Content 对象,避免使用 JSON
+        QwSopTempSetting.Content clonedContent = deepCopyContent(content);
+        if (clonedContent == null) {
+            log.error("Failed to clone content, skipping handleCourseMessage.");
+            return;
+        }
+        clonedContent.setLiveId(liveId);
+        List<QwSopTempSetting.Content.Setting> settings = clonedContent.getSetting();
+        if (settings == null || settings.isEmpty()) {
+            log.error("Cloned content settings are empty, skipping.");
+            return;
+        }
+
+        //直播发送类型
+        sopLogs.setSendType(20);
+
+        // 顺序处理每个 Setting,避免过多的并行导致线程开销
+        for (QwSopTempSetting.Content.Setting setting : settings) {
+            switch (setting.getContentType()) {
+                //直播小程序单独
+                case "12":
+                    clonedContent.setLiveId(setting.getLiveId());
+                    String sortLiveLink;
+                    sortLiveLink = "/pages_course/living.html?companyId=" + companyId + "&companyUserId=" + companyUserId + "&liveId=" + setting.getLiveId() + "&corpId=" + logVo.getCorpId()+"&qwUserId=" + qwUserId;
+                    String json = configService.selectConfigByKey("his.config");
+                    FSSysConfig sysConfig = JSON.parseObject(json, FSSysConfig.class);
+                    if (isGroupChat) {
+                        try {
+                            groupChat.getChatUserList().stream().filter(e -> e.getUserList() != null && !e.getUserList().isEmpty()).forEach(e -> {
+                                Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
+                                GroupUserExternalVo vo = userMap.get(groupChat.getOwner());
+                                if (vo != null && vo.getId() != null) {
+                                    sopLogs.setFsUserId(vo.getFsUserId());
+                                    //写入直播待看课记录
+                                    createLiveWatchLogAndEnQueue(companyId, companyUserId, vo.getId().toString(), setting.getLiveId(), sysConfig.getAppId(), 2, qwUserId,logVo.getCorpId());
+                                }
+                            });
+                            sortLiveLink += "&chatId=" + groupChat.getChatId();
+                        } catch (Exception e) {
+                            log.error("直播小程序群聊新增报错,{}", e.getMessage(), e);
+                        }
+                    } else {
+                        try {
+                            createLiveWatchLogAndEnQueue(companyId, companyUserId, externalId, setting.getLiveId(), sysConfig.getAppId(), 1, qwUserId,logVo.getCorpId());
+                            sortLiveLink += "&externalId=" + externalId;
+                        } catch (Exception e) {
+                            log.error("直播小程序个人新增报错,{}", e.getMessage(), e);
+                        }
+                    }
+
+                    String miniprogramLiveTitle = setting.getMiniprogramTitle();
+                    int maxLiveLength = 17;
+                    setting.setMiniprogramTitle(miniprogramLiveTitle.length() > maxLiveLength ? miniprogramLiveTitle.substring(0, maxLiveLength) + "..." : miniprogramLiveTitle);
+                    setting.setMiniprogramAppid(sysConfig.getAppId());
+                    setting.setMiniprogramPage(sortLiveLink);
+                    setting.setContentType("4");
+                    try {
+                        setting.setMiniprogramPicUrl(StringUtil.strIsNullOrEmpty(setting.getMiniprogramPicUrl()) ? "https://cos.his.cdwjyyh.com/fs/20250331/ec2b4e73be8048afbd526124a655ad56.png" : setting.getMiniprogramPicUrl());
+                    } catch (Exception e) {
+                        log.error("赋值-小程序封面地址失败-" + e);
+                    }
+
+                    break;
+                default:
+                    break;
+            }
+        }
+        sopLogs.setContentJson(JSON.toJSONString(clonedContent));
+
+        enqueueQwSopLogs(sopLogs);
+    }
+
     private void handleAIMessage(QwSopLogs sopLogs, QwSopTempSetting.Content content) {
         sopLogs.setContentJson(JSON.toJSONString(content));
         sopLogs.setSort(3);
@@ -1988,17 +2069,18 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     )
     public void batchInsertLiveWatchLog(List<LiveWatchLog> liveWatchLogToInsert) {
         try {
-            List<LiveWatchLog> lastInsertList = new ArrayList<>();
+            //更改为set 避免同一批生成的消息里面有重复数据 插入会报错
+            Set<LiveWatchLog> lastInsertSet = new HashSet<>();
             //判断是否存在数据 liveId + his_qw_external_contact_id + qwUserId 唯一
             for (LiveWatchLog liveWatchLog : liveWatchLogToInsert) {
                 //判断是否存在数据 存在的数据直接更新发送时间
                 if(liveWatchLogMapper.updateLiveWatchLogCondition(liveWatchLog) > 0){
                     continue;
                 }
-                lastInsertList.add(liveWatchLog);
+                lastInsertSet.add(liveWatchLog);
             }
-            if(!lastInsertList.isEmpty()){
-                liveWatchLogMapper.insertLiveWatchLogBatch(lastInsertList);
+            if(!lastInsertSet.isEmpty()){
+                liveWatchLogMapper.insertLiveWatchLogBatch(new ArrayList<>(lastInsertSet));
             }
 //            log.info("批量插入 LiveWatchLog 完成,共插入 {} 条记录。", liveWatchLogToInsert.size());
         } catch (Exception e) {

+ 181 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/VoiceCloneController.java

@@ -0,0 +1,181 @@
+package com.fs.aiSoundReplication;
+
+import com.fs.aiSoundReplication.param.StatusResponse;
+import com.fs.aiSoundReplication.param.TtsRequest;
+import com.fs.aiSoundReplication.param.TtsResponse;
+import com.fs.aiSoundReplication.param.UploadResponse;
+import com.fs.aiSoundReplication.service.TtsService;
+import com.fs.aiSoundReplication.service.VoiceCloneService;
+import com.fs.common.core.domain.R;
+import com.fs.fastgptApi.vo.AudioVO;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import java.io.File;
+import java.util.List;
+import java.util.UUID;
+
+@RestController
+@RequestMapping("/api/voice-clone")
+@Api(tags = "声音复刻API")
+public class VoiceCloneController {
+
+    @Autowired
+    private VoiceCloneService voiceCloneService;
+    @Autowired
+    private TtsService ttsService;
+
+    @PostMapping("/synthesize")
+    @ApiOperation("文本转语音")
+    public AudioVO synthesize(
+            @ApiParam(value = "TTS请求参数", required = true)
+            @RequestBody TtsRequest request) {
+        return ttsService.textToSpeech(request);
+    }
+
+    @PostMapping("/synthesize-simple")
+    @ApiOperation("简化版文本转语音")
+    public AudioVO synthesizeSimple(
+            @ApiParam(value = "要合成的文本", required = true)
+            @RequestParam String text,
+            @ApiParam(value = "音色ID", required = true)
+            @RequestParam String voiceType,
+            @ApiParam(value = "音频格式")
+            @RequestParam(required = false, defaultValue = "mp3") String format,
+            @ApiParam(value = "语速 (0-15)")
+            @RequestParam(required = false, defaultValue = "1") Integer speed
+    ) {
+
+        TtsRequest request = new TtsRequest(
+                "", "", voiceType, text); // AppID和Token会在Service中设置
+        request.setReqId(UUID.randomUUID().toString());
+        request.setFormat(format);
+        request.setSpeed(speed);
+        return ttsService.textToSpeech(request);
+    }
+
+//    @PostMapping("/synthesize-and-download")
+//    @ApiOperation("文本转语音并下载")
+//    public R synthesizeAndDownload(
+//            @ApiParam(value = "要合成的文本", required = true)
+//            @RequestParam String text,
+//            @ApiParam(value = "音色ID", required = true)
+//            @RequestParam String voiceType,
+//            HttpServletRequest httpRequest) {
+//
+//        TtsRequest ttsRequest = new TtsRequest("", "", voiceType, text);
+//        ttsRequest.setReqId(UUID.randomUUID().toString());
+//
+//        String url = ttsService.textToSpeechStream(ttsRequest);
+//
+//        return R.ok();
+//    }
+
+//    @PostMapping("/batch-synthesize")
+//    @ApiOperation("批量文本转语音")
+//    public ResponseEntity<List<File>> batchSynthesize(
+//            @ApiParam(value = "文本列表", required = true)
+//            @RequestBody List<String> texts,
+//            @ApiParam(value = "音色ID", required = true)
+//            @RequestParam String voiceType,
+//            @ApiParam(value = "是否打包下载")
+//            @RequestParam(required = false, defaultValue = "false") Boolean zip) {
+//
+//        List<File> audioFiles = ttsService.batchTextToSpeech(texts, voiceType);
+//
+//        if (zip && !audioFiles.isEmpty()) {
+//            // 这里可以添加ZIP打包逻辑
+//            // 返回ZIP文件的ResponseEntity
+//        }
+//
+//        return ResponseEntity.ok(audioFiles);
+//    }
+
+//    @PostMapping("/synthesize-with-params")
+//    @ApiOperation("带参数的文本转语音")
+//    public TtsResponse synthesizeWithParams(
+//            @ApiParam(value = "音色ID", required = true) @RequestParam String voiceType,
+//            @ApiParam(value = "文本内容", required = true) @RequestParam String text,
+//            @ApiParam(value = "语速 (0-15)") @RequestParam(required = false) Integer speed,
+//            @ApiParam(value = "音量 (0-15)") @RequestParam(required = false) Integer volume,
+//            @ApiParam(value = "音高 (0-15)") @RequestParam(required = false) Integer pitch,
+//            @ApiParam(value = "情感参数") @RequestParam(required = false) String emotion,
+//            @ApiParam(value = "说话风格") @RequestParam(required = false) String speakingStyle) {
+//
+//        TtsRequest request = new TtsRequest("", "", voiceType, text);
+//        request.setReqId(UUID.randomUUID().toString());
+//
+//        if (speed != null) request.setSpeed(speed);
+//        if (volume != null) request.setVolume(volume);
+//        if (pitch != null) request.setPitch(pitch);
+//        if (emotion != null) request.setEmotion(emotion);
+//        if (speakingStyle != null) request.setSpeakingStyle(speakingStyle);
+//
+//        return ttsService.textToSpeech(request);
+//    }
+
+    private String getContentType(String format) {
+        switch (format.toLowerCase()) {
+            case "mp3":
+                return "audio/mpeg";
+            case "wav":
+                return "audio/wav";
+            case "pcm":
+                return "audio/L16";
+            default:
+                return "application/octet-stream";
+        }
+    }
+    @PostMapping("/upload")
+    @ApiOperation("上传音频训练音色")
+    public UploadResponse uploadVoice(
+            @ApiParam(value = "音色ID", required = true) @RequestParam String speakerId,
+            @ApiParam(value = "音频文件", required = true) @RequestParam MultipartFile audioFile,
+            @ApiParam(value = "模型类型(1-ICL1.0, 4-ICL2.0)", defaultValue = "4")
+            @RequestParam(required = false) Integer modelType,
+            @ApiParam(value = "语种(0-中文, 1-英文)", defaultValue = "0")
+            @RequestParam(required = false) Integer language) {
+        return voiceCloneService.uploadVoice(speakerId, audioFile, modelType, language);
+    }
+
+    @GetMapping("/status/{speakerId}")
+    @ApiOperation("查询音色训练状态")
+    public StatusResponse getTrainingStatus(
+            @ApiParam(value = "音色ID", required = true)
+            @PathVariable String speakerId) {
+        return voiceCloneService.queryTrainingStatus(speakerId);
+    }
+
+//    @PostMapping("/upload-and-wait")
+//    @ApiOperation("上传并等待训练完成")
+//    public StatusResponse uploadAndWait(
+//            @ApiParam(value = "音色ID", required = true) @RequestParam String speakerId,
+//            @ApiParam(value = "音频文件", required = true) @RequestParam MultipartFile audioFile,
+//            @ApiParam(value = "模型类型", defaultValue = "4")
+//            @RequestParam(required = false) Integer modelType,
+//            @ApiParam(value = "语种", defaultValue = "0")
+//            @RequestParam(required = false) Integer language,
+//            @ApiParam(value = "最大等待时间(秒)", defaultValue = "600")
+//            @RequestParam(required = false) Integer maxWaitSeconds) {
+//
+//        // 1. 上传音频
+//        UploadResponse uploadResponse = voiceCloneService.uploadVoice(
+//                speakerId, audioFile, modelType, language);
+//
+//        // 2. 计算轮询参数
+//        int maxPollingTimes = maxWaitSeconds != null ? maxWaitSeconds * 1000 / 10000 : 60;
+//
+//        // 3. 轮询训练状态
+//        return voiceCloneService.pollTrainingStatus(
+//                uploadResponse.getSpeakerId(), maxPollingTimes, 10000L);
+//    }
+}

+ 23 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/config/OkHttpConfig.java

@@ -0,0 +1,23 @@
+package com.fs.aiSoundReplication.config;
+
+import okhttp3.ConnectionPool;
+import okhttp3.OkHttpClient;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.concurrent.TimeUnit;
+
+@Configuration
+public class OkHttpConfig {
+
+    @Bean
+    public OkHttpClient okHttpClient() {
+        return new OkHttpClient.Builder()
+                .connectTimeout(30, TimeUnit.SECONDS)
+                .readTimeout(60, TimeUnit.SECONDS)
+                .writeTimeout(60, TimeUnit.SECONDS)
+                .connectionPool(new ConnectionPool(10, 5, TimeUnit.MINUTES))
+                .retryOnConnectionFailure(true)
+                .build();
+    }
+}

+ 34 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/config/TtsConfig.java

@@ -0,0 +1,34 @@
+package com.fs.aiSoundReplication.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "voice.clone.tts")
+public class TtsConfig {
+    // HTTP TTS接口地址
+    private String httpUrl = "https://openspeech.bytedance.com/api/v1/tts";
+
+    // 默认参数
+    private String defaultFormat = "mp3";
+    private Integer defaultSampleRate = 24000;
+    private Integer defaultSpeed = 10;
+    private Integer defaultVolume = 10;
+    private Integer defaultPitch = 10;
+    private String defaultCluster = "volcano_icl";
+
+    // 文本长度限制
+    private Integer maxTextLength = 500; // 最大文本长度
+
+    // 重试配置
+    private Integer maxRetryTimes = 3;
+    private Long retryInterval = 2000L;
+
+    // 音频保存路径
+    private String audioSavePath = "./audio/";
+
+    // 是否自动保存音频文件
+    private Boolean autoSave = true;
+}

+ 27 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/config/VoiceCloneConfig.java

@@ -0,0 +1,27 @@
+package com.fs.aiSoundReplication.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "voice.clone")
+public class VoiceCloneConfig {
+    private String accessToken = "IVX_Rlt6r93upGb-_vy0QlxaK_dhQzDY";//正式环境需要换成公司豆包的信息
+    private String appId = "8243948690";//正式环境需要换成公司豆包的信息
+
+    // API地址
+    private String uploadUrl = "https://openspeech.bytedance.com/api/v1/mega_tts/audio/upload";
+    private String statusUrl = "https://openspeech.bytedance.com/api/v1/mega_tts/status";
+
+    // 资源ID - 根据模型类型选择
+    private String resourceIdIcl1 = "seed-icl-1.0";
+    private String resourceIdIcl2 = "seed-icl-2.0";
+
+    // 重试配置
+    private Integer maxRetryTimes = 3;
+    private Long retryInterval = 5000L; // 重试间隔5秒
+    private Long pollingInterval = 10000L; // 轮询间隔10秒
+    private Integer maxPollingTimes = 60; // 最多轮询60次,约10分钟
+}

+ 42 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/exception/ErrorCodeEnum.java

@@ -0,0 +1,42 @@
+package com.fs.aiSoundReplication.exception;
+
+import lombok.Getter;
+
+@Getter
+public enum ErrorCodeEnum {
+    SUCCESS(0, "成功"),
+    BAD_REQUEST_ERROR(1001, "请求参数有误"),
+    AUDIO_UPLOAD_ERROR(1101, "音频上传失败"),
+    ASR_ERROR(1102, "ASR转写失败"),
+    SID_ERROR(1103, "SID声纹检测失败"),
+    SID_FAIL_ERROR(1104, "声纹检测未通过"),
+    GET_AUDIO_DATA_ERROR(1105, "获取音频数据失败"),
+    SPEAKER_ID_DUPLICATION_ERROR(1106, "SpeakerID重复"),
+    SPEAKER_ID_NOT_FOUND_ERROR(1107, "SpeakerID未找到"),
+    AUDIO_CONVERT_ERROR(1108, "音频转码失败"),
+    WER_ERROR(1109, "WER检测错误"),
+    AED_ERROR(1111, "AED检测错误"),
+    SNR_ERROR(1112, "SNR检测错误"),
+    DENOISE_ERROR(1113, "降噪处理失败"),
+    AUDIO_QUALITY_ERROR(1114, "音频质量低"),
+    ASR_NO_SPEAKER_ERROR(1122, "未检测到人声"),
+    UPLOAD_LIMIT_ERROR(1123, "已达上传次数限制"),
+    UNKNOWN_ERROR(-1, "未知错误");
+
+    private final Integer code;
+    private final String message;
+
+    ErrorCodeEnum(Integer code, String message) {
+        this.code = code;
+        this.message = message;
+    }
+
+    public static ErrorCodeEnum fromCode(Integer code) {
+        for (ErrorCodeEnum errorCode : values()) {
+            if (errorCode.getCode().equals(code)) {
+                return errorCode;
+            }
+        }
+        return UNKNOWN_ERROR;
+    }
+}

+ 21 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/exception/VoiceCloneException.java

@@ -0,0 +1,21 @@
+package com.fs.aiSoundReplication.exception;
+
+import lombok.Getter;
+
+@Getter
+public class VoiceCloneException extends RuntimeException {
+    private final Integer errorCode;
+    private final String errorMessage;
+
+    public VoiceCloneException(Integer errorCode, String errorMessage) {
+        super(String.format("错误码: %d, 错误信息: %s", errorCode, errorMessage));
+        this.errorCode = errorCode;
+        this.errorMessage = errorMessage;
+    }
+
+    public VoiceCloneException(String message, Throwable cause) {
+        super(message, cause);
+        this.errorCode = -1;
+        this.errorMessage = message;
+    }
+}

+ 20 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/param/BaseResponse.java

@@ -0,0 +1,20 @@
+package com.fs.aiSoundReplication.param;
+
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class BaseResponse {
+    @JsonProperty("BaseResp")
+    private BaseResp baseResp;
+
+    @Data
+    public static class BaseResp {
+        @JsonProperty("StatusCode")
+        private Integer statusCode;
+
+        @JsonProperty("StatusMessage")
+        private String statusMessage;
+    }
+}

+ 33 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/param/StatusResponse.java

@@ -0,0 +1,33 @@
+package com.fs.aiSoundReplication.param;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class StatusResponse extends BaseResponse {
+    @JsonProperty("speaker_id")
+    private String speakerId;
+
+    private Integer status; // 0-NotFound, 1-Training, 2-Success, 3-Failed, 4-Active
+
+    @JsonProperty("create_time")
+    private Long createTime;
+
+    private String version;
+
+    @JsonProperty("demo_audio")
+    private String demoAudio;
+
+    public String getStatusText() {
+        switch (status) {
+            case 0: return "NotFound";
+            case 1: return "Training";
+            case 2: return "Success";
+            case 3: return "Failed";
+            case 4: return "Active";
+            default: return "Unknown";
+        }
+    }
+}

+ 13 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/param/TrainingStatusRequest.java

@@ -0,0 +1,13 @@
+package com.fs.aiSoundReplication.param;
+
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class TrainingStatusRequest {
+    private String appid;
+
+    @JsonProperty("speaker_id")
+    private String speakerId;
+}

+ 64 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/param/TtsRequest.java

@@ -0,0 +1,64 @@
+package com.fs.aiSoundReplication.param;
+
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Data
+public class TtsRequest {
+    @JsonProperty("appid")
+    private String appId;
+
+    @JsonProperty("token")
+    private String token;
+
+    @JsonProperty("cluster")
+    private String cluster = "volcano_icl"; // 声音复刻必须使用此cluster
+
+    @JsonProperty("voice_type")
+    private String voiceType; // 训练好的speaker_id
+
+    private String text; // 要合成的文本
+
+    private String format = "mp3"; // 音频格式: wav, mp3, pcm
+
+    private Integer sampleRate = 24000; // 采样率
+
+    private Integer speed = 1; // 语速 (0-15)
+
+    private Integer volume = 10; // 音量 (0-15)
+
+    private Integer pitch = 10; // 音高 (0-15)
+
+    @JsonProperty("audio_encode_type")
+    private String audioEncodeType = "raw"; // raw或wav
+
+    @JsonProperty("enable_subtitle")
+    private Boolean enableSubtitle = false; // 是否开启字幕
+
+    @JsonProperty("voice_id")
+    private String voiceId; // 音色ID (可选)
+
+    private String language = "zh"; // 语言: zh, en, ja等
+
+    @JsonProperty("reqid")
+    private String reqId; // 请求ID,需要保证唯一
+
+    @JsonProperty("emotion")
+    private String emotion; // 情感参数
+
+    @JsonProperty("speaking_style")
+    private String speakingStyle; // 说话风格
+
+    // 构造函数
+    public TtsRequest(String appId, String token, String voiceType, String text) {
+        this.appId = appId;
+        this.token = token;
+        this.voiceType = voiceType;
+        this.text = text;
+        this.reqId = java.util.UUID.randomUUID().toString();
+    }
+}

+ 54 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/param/TtsResponse.java

@@ -0,0 +1,54 @@
+package com.fs.aiSoundReplication.param;
+
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class TtsResponse extends BaseResponse {
+    private AudioData data;
+
+    @Data
+    public static class AudioData {
+        private String audio; // Base64编码的音频数据
+
+        private Double duration; // 音频时长(秒)
+
+        @JsonProperty("subtitle_info")
+        private SubtitleInfo subtitleInfo;
+
+        @JsonProperty("subtitle_url")
+        private String subtitleUrl; // 字幕文件URL
+    }
+
+    @Data
+    public static class SubtitleInfo {
+        @JsonProperty("word_list")
+        private List<WordInfo> wordList;
+    }
+
+    @Data
+    public static class WordInfo {
+        private String word; // 词语
+
+        private Double start; // 开始时间(秒)
+
+        private Double end; // 结束时间(秒)
+
+        @JsonProperty("phone_list")
+        private List<PhoneInfo> phoneList;
+    }
+
+    @Data
+    public static class PhoneInfo {
+        private String phone; // 音素
+
+        private Double start; // 开始时间(秒)
+
+        private Double end; // 结束时间(秒)
+    }
+}

+ 12 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/param/UploadResponse.java

@@ -0,0 +1,12 @@
+package com.fs.aiSoundReplication.param;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class UploadResponse extends BaseResponse {
+    @JsonProperty("speaker_id")
+    private String speakerId;
+}

+ 39 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/param/VoiceCloneRequest.java

@@ -0,0 +1,39 @@
+package com.fs.aiSoundReplication.param;
+
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class VoiceCloneRequest {
+    private String appid;
+
+    @JsonProperty("speaker_id")
+    private String speakerId;
+
+    private List<AudioInfo> audios;
+
+    private Integer source = 2; // 固定值2
+
+    private Integer language = 0; // 0-中文, 1-英文
+
+    @JsonProperty("model_type")
+    private Integer modelType = 4; // 默认使用ICL 2.0
+
+    @JsonProperty("extra_params")
+    private String extraParams = "{}";
+
+    @Data
+    public static class AudioInfo {
+        @JsonProperty("audio_bytes")
+        private String audioBytes; // Base64编码的音频
+
+        @JsonProperty("audio_format")
+        private String audioFormat; // wav, mp3等
+
+        private String text; // 可选,朗读文本用于校验
+    }
+}

+ 57 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/service/TtsService.java

@@ -0,0 +1,57 @@
+package com.fs.aiSoundReplication.service;
+
+import com.fs.aiSoundReplication.param.TtsRequest;
+import com.fs.aiSoundReplication.param.TtsResponse;
+import com.fs.fastgptApi.vo.AudioVO;
+import org.springframework.core.io.Resource;
+
+import java.io.File;
+import java.io.IOException;
+
+public interface TtsService {
+
+    /**
+     * 文本转语音
+     * @param request TTS请求参数
+     * @return TTS响应
+     */
+    AudioVO textToSpeech(TtsRequest request);
+
+    /**
+     * 简化版文本转语音
+     * @param text 要合成的文本
+     * @param voiceType 音色ID
+     * @return TTS响应
+     */
+//    TtsResponse textToSpeech(String text, String voiceType);
+
+    /**
+     * 文本转语音并保存为文件
+     * @param request TTS请求参数
+     * @param savePath 保存路径
+     * @return 保存的音频文件
+     */
+//    File textToSpeechAndSave(TtsRequest request, String savePath);
+
+    /**
+     * 文本转语音并获取字节数组
+     * @param request TTS请求参数
+     * @return 音频字节数组
+     */
+//    byte[] textToSpeechBytes(TtsRequest request);
+
+    /**
+     * 流式返回音频数据
+     * @param request TTS请求参数
+     * @return 音频资源
+     */
+//    String textToSpeechStream(TtsRequest request);
+
+    /**
+     * 批量文本转语音
+     * @param texts 文本列表
+     * @param voiceType 音色ID
+     * @return 音频文件列表
+     */
+//    java.util.List<File> batchTextToSpeech(java.util.List<String> texts, String voiceType);
+}

+ 50 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/service/VoiceCloneService.java

@@ -0,0 +1,50 @@
+package com.fs.aiSoundReplication.service;
+
+
+import com.fs.aiSoundReplication.param.StatusResponse;
+import com.fs.aiSoundReplication.param.UploadResponse;
+import org.springframework.web.multipart.MultipartFile;
+
+public interface VoiceCloneService {
+
+    /**
+     * 上传音频训练音色
+     * @param speakerId 音色ID
+     * @param audioFile 音频文件
+     * @param modelType 模型类型 1-ICL1.0, 2-DiT标准版, 3-DiT还原版, 4-ICL2.0
+     * @param language 语种 0-中文, 1-英文等
+     * @return 上传响应
+     */
+    UploadResponse uploadVoice(String speakerId, MultipartFile audioFile,
+                               Integer modelType, Integer language);
+
+    /**
+     * 上传音频训练音色(使用文件路径)
+     */
+    UploadResponse uploadVoiceByPath(String speakerId, String filePath,
+                                     Integer modelType, Integer language);
+
+    /**
+     * 查询音色训练状态
+     * @param speakerId 音色ID
+     * @return 状态响应
+     */
+    StatusResponse queryTrainingStatus(String speakerId);
+
+    /**
+     * 轮询音色训练状态
+     * @param speakerId 音色ID
+     * @param maxPollingTimes 最大轮询次数
+     * @param pollingInterval 轮询间隔(毫秒)
+     * @return 最终状态响应
+     */
+    StatusResponse pollTrainingStatus(String speakerId, Integer maxPollingTimes,
+                                      Long pollingInterval);
+
+    /**
+     * 获取资源ID(根据模型类型)
+     * @param modelType 模型类型
+     * @return 资源ID
+     */
+    String getResourceIdByModelType(Integer modelType);
+}

+ 488 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/service/impl/TtsServiceImpl.java

@@ -0,0 +1,488 @@
+package com.fs.aiSoundReplication.service.impl;
+
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fs.aiSoundReplication.config.TtsConfig;
+import com.fs.aiSoundReplication.config.VoiceCloneConfig;
+import com.fs.aiSoundReplication.exception.VoiceCloneException;
+import com.fs.aiSoundReplication.param.TtsRequest;
+import com.fs.aiSoundReplication.param.TtsResponse;
+import com.fs.aiSoundReplication.service.TtsService;
+import com.fs.fastgptApi.util.AudioUtils;
+import com.fs.fastgptApi.vo.AudioVO;
+import com.fs.system.oss.CloudStorageService;
+import com.fs.system.oss.OSSFactory;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.*;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.Base64;
+
+import static com.fs.fastgptApi.util.AudioUtils.getDurations;
+import static com.fs.fastgptApi.util.AudioUtils.transferAudioSilk;
+
+@Service
+@Slf4j
+public class TtsServiceImpl implements TtsService {
+
+    @Autowired
+    private TtsConfig ttsConfig;
+
+    @Autowired
+    private VoiceCloneConfig voiceCloneConfig;
+
+    @Autowired
+    private OkHttpClient okHttpClient;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    private final ExecutorService executorService = Executors.newFixedThreadPool(5);
+
+    private static final String AUTHORIZATION_HEADER = "Authorization";
+    private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
+
+    @Override
+    public AudioVO textToSpeech(TtsRequest request) {
+        try {
+            // 1. 参数校验
+            validateTtsRequest(request);
+
+            // 2. 设置默认值
+            setDefaultValues(request);
+
+            // 3. 构建请求体
+            String requestBody = buildRequestBody(request);
+
+            // 4. 构建HTTP请求
+            Request httpRequest = buildHttpRequest(requestBody);
+
+            // 5. 发送请求(带重试)
+            byte[] bytes = executeTtsRequest(httpRequest);
+
+            // 6. 检查音频数据
+            if (bytes == null || bytes.length == 0) {
+                throw new VoiceCloneException(-1, "音频数据为空");
+            }
+
+            // 7. 自动保存音频文件
+            // 创建临时文件
+            File tempFile = File.createTempFile("tts_", ".wav");
+            try (FileOutputStream fos = new FileOutputStream(tempFile)) {
+                fos.write(bytes);
+            }
+            // 上传到OSS
+            try (FileInputStream fileInputStream = new FileInputStream(tempFile)) {
+                //直接转silk然后传桶,返回url     优化-需要wav格式
+                CloudStorageService storage = OSSFactory.build();
+                String wavUrl = storage.uploadSuffix(fileInputStream, ".wav");
+//                AudioVO audioVO = AudioUtils.transferAudioSilkFromUrl(wavUrl, false);
+                Integer durations = getDurations(tempFile.getParent()+"\\"+tempFile.getName());
+                String silkUrl = transferAudioSilk(tempFile.getParent()+"\\", tempFile.getName(), false);
+                AudioVO audioVO = new AudioVO();
+                audioVO.setDuration(durations);
+                audioVO.setUrl(silkUrl);
+                audioVO.setWavUrl(wavUrl);
+                log.info("音频文件上传OSS成功: {}", audioVO.getUrl());
+                return audioVO;
+            } finally {
+                // 删除临时文件
+                tempFile.delete();
+            }
+
+        } catch (Exception e) {
+            log.error("TTS合成失败,reqId: {}, 错误: {}",
+                    request.getReqId(), e.getMessage());
+            throw e instanceof VoiceCloneException ?
+                    (VoiceCloneException) e :
+                    new VoiceCloneException("TTS合成失败", e);
+        }
+    }
+
+//    @Override
+//    public String textToSpeech(String text, String voiceType) {
+//        // 创建简化版请求
+//        TtsRequest request = new TtsRequest(
+//                voiceCloneConfig.getAppId(),
+//                voiceCloneConfig.getAccessToken(),
+//                voiceType,
+//                text
+//        );
+//
+//        // 设置默认参数
+//        request.setFormat(ttsConfig.getDefaultFormat());
+//        request.setSampleRate(ttsConfig.getDefaultSampleRate());
+//        request.setCluster(ttsConfig.getDefaultCluster());
+//
+//        return textToSpeech(request);
+//    }
+
+//    @Override
+//    public File textToSpeechAndSave(TtsRequest request, String savePath) {
+//        try {
+//            // 1. 执行TTS合成
+//            TtsResponse response = textToSpeech(request);
+//
+//            if (response.getData() == null || response.getData().getAudio() == null) {
+//                throw new VoiceCloneException(-1, "音频数据为空");
+//            }
+//
+//            // 2. 解码Base64音频数据
+//            byte[] audioBytes = Base64.getDecoder().decode(response.getData().getAudio());
+//
+//            // 3. 确定保存路径
+//            String finalSavePath = savePath != null ? savePath : ttsConfig.getAudioSavePath();
+//
+//            // 创建目录
+//            Path directory = Paths.get(finalSavePath);
+//            if (!Files.exists(directory)) {
+//                Files.createDirectories(directory);
+//            }
+//
+//            // 4. 生成文件名
+//            String fileName = String.format("%s_%s.%s",
+//                    request.getVoiceType(),
+//                    request.getReqId().substring(0, 8),
+//                    request.getFormat());
+//
+//            File audioFile = new File(finalSavePath, fileName);
+//
+//            // 5. 保存文件
+//            try (FileOutputStream fos = new FileOutputStream(audioFile)) {
+//                fos.write(audioBytes);
+//            }
+//
+//            log.info("音频文件保存成功: {}, 大小: {}KB",
+//                    audioFile.getAbsolutePath(), audioBytes.length / 1024);
+//
+//            return audioFile;
+//
+//        } catch (IOException e) {
+//            log.error("保存音频文件失败", e);
+//            throw new VoiceCloneException("保存音频文件失败", e);
+//        }
+//    }
+
+//    @Override
+//    public byte[] textToSpeechBytes(TtsRequest request) {
+//         textToSpeech(request);
+//    }
+
+//    @Override
+//    public String textToSpeechStream(TtsRequest request) {
+//        byte[] audioBytes = textToSpeechBytes(request);
+//
+//    }
+
+//    @Override
+//    public List<File> batchTextToSpeech(List<String> texts, String voiceType) {
+//        List<CompletableFuture<File>> futures = new ArrayList<>();
+//        List<File> results = new ArrayList<>();
+//
+//        for (int i = 0; i < texts.size(); i++) {
+//            final String text = texts.get(i);
+//            final int index = i;
+//
+//            CompletableFuture<File> future = CompletableFuture.supplyAsync(() -> {
+//                try {
+//                    TtsRequest request = new TtsRequest(
+//                            voiceCloneConfig.getAppId(),
+//                            voiceCloneConfig.getAccessToken(),
+//                            voiceType,
+//                            text
+//                    );
+//                    request.setReqId(String.format("batch_%s_%d",
+//                            UUID.randomUUID().toString().substring(0, 8), index));
+//
+//                    return textToSpeechAndSave(request, null);
+//                } catch (Exception e) {
+//                    log.error("批量TTS处理失败,文本索引: {}, 错误: {}", index, e.getMessage());
+//                    return null;
+//                }
+//            }, executorService);
+//
+//            futures.add(future);
+//        }
+//
+//        // 等待所有任务完成
+//        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+//
+//        // 收集结果
+//        for (CompletableFuture<File> future : futures) {
+//            try {
+//                File file = future.get();
+//                if (file != null) {
+//                    results.add(file);
+//                }
+//            } catch (Exception e) {
+//                log.error("获取批量处理结果失败", e);
+//            }
+//        }
+//
+//        log.info("批量TTS处理完成,成功: {}/{}", results.size(), texts.size());
+//        return results;
+//    }
+
+    // ============ 私有方法 ============
+
+    private void validateTtsRequest(TtsRequest request) {
+        if (request == null) {
+            throw new VoiceCloneException(1001, "请求参数不能为空");
+        }
+
+        if (request.getText() == null || request.getText().trim().isEmpty()) {
+            throw new VoiceCloneException(1001, "文本内容不能为空");
+        }
+
+        if (request.getText().length() > ttsConfig.getMaxTextLength()) {
+            throw new VoiceCloneException(1001,
+                    String.format("文本长度超过限制(%d字符)", ttsConfig.getMaxTextLength()));
+        }
+
+        if (request.getVoiceType() == null || request.getVoiceType().trim().isEmpty()) {
+            throw new VoiceCloneException(1001, "音色ID不能为空");
+        }
+
+        if (request.getReqId() == null || request.getReqId().trim().isEmpty()) {
+            request.setReqId(UUID.randomUUID().toString());
+        }
+    }
+
+    private void setDefaultValues(TtsRequest request) {
+        if (request.getAppId() == null || request.getAppId().equals("")) {
+            request.setAppId(voiceCloneConfig.getAppId());
+        }
+
+        if (request.getToken() == null || request.getToken().equals("")) {
+            request.setToken(voiceCloneConfig.getAccessToken());
+        }
+
+        if (request.getCluster() == null) {
+            request.setCluster(ttsConfig.getDefaultCluster());
+        }
+
+        if (request.getFormat() == null) {
+            request.setFormat(ttsConfig.getDefaultFormat());
+        }
+
+        if (request.getSampleRate() == null) {
+            request.setSampleRate(ttsConfig.getDefaultSampleRate());
+        }
+
+        if (request.getSpeed() == null) {
+            request.setSpeed(ttsConfig.getDefaultSpeed());
+        }
+
+        if (request.getVolume() == null) {
+            request.setVolume(ttsConfig.getDefaultVolume());
+        }
+
+        if (request.getPitch() == null) {
+            request.setPitch(ttsConfig.getDefaultPitch());
+        }
+    }
+
+    private String buildRequestBody(TtsRequest request) throws IOException {
+        Map<String, Object> requestBody = new HashMap<>();
+
+        // 必填参数
+        HashMap<String, Object> app = new HashMap<String, Object>() {{
+            put("appid", request.getAppId());
+            put("token", request.getToken());
+            put("cluster", request.getCluster());
+        }};
+        HashMap<String, Object> user = new HashMap<String, Object>() {{
+            put("uid","01");
+        }};
+        HashMap<String, Object> audio = new HashMap<String, Object>() {{
+            put("voice_type", request.getVoiceType());
+        }};
+        if (request.getFormat() != null)audio.put("encoding", request.getFormat());
+        if (request.getSpeed() != null)audio.put("speed_ratio", request.getSpeed());
+
+        HashMap<String, Object> requestMap = new HashMap<String, Object>() {{
+            put("reqid", request.getReqId());
+            put("text", request.getText());
+            put("operation","query");
+        }};
+
+        requestBody.put("app",app);
+        requestBody.put("user", user);
+        requestBody.put("audio", audio);
+        requestBody.put("request", requestMap);
+
+        // 可选参数
+//        if (request.getFormat() != null) {
+//            requestBody.put("format", request.getFormat());
+//        }
+
+//        if (request.getSampleRate() != null) {
+//            requestBody.put("sample_rate", request.getSampleRate());
+//        }
+
+//        if (request.getSpeed() != null) {
+//            requestBody.put("speed", request.getSpeed());
+//        }
+
+//        if (request.getVolume() != null) {
+//            requestBody.put("volume", request.getVolume());
+//        }
+
+//        if (request.getPitch() != null) {
+//            requestBody.put("pitch", request.getPitch());
+//        }
+
+//        if (request.getAudioEncodeType() != null) {
+//            requestBody.put("audio_encode_type", request.getAudioEncodeType());
+//        }
+
+//        if (request.getEnableSubtitle() != null) {
+//            requestBody.put("enable_subtitle", request.getEnableSubtitle());
+//        }
+
+//        if (request.getVoiceId() != null) {
+//            requestBody.put("voice_id", request.getVoiceId());
+//        }
+
+//        if (request.getLanguage() != null) {
+//            requestBody.put("language", request.getLanguage());
+//        }
+
+//        if (request.getEmotion() != null) {
+//            requestBody.put("emotion", request.getEmotion());
+//        }
+
+//        if (request.getSpeakingStyle() != null) {
+//            requestBody.put("speaking_style", request.getSpeakingStyle());
+//        }
+
+        return objectMapper.writeValueAsString(requestBody);
+    }
+
+    private Request buildHttpRequest(String requestBody) {
+        RequestBody body = RequestBody.create(JSON,requestBody );
+
+        return new Request.Builder()
+                .url(ttsConfig.getHttpUrl())
+                .post(body)
+                .addHeader(AUTHORIZATION_HEADER, "Bearer;" + voiceCloneConfig.getAccessToken())
+                .addHeader("Content-Type", "application/json")
+                .build();
+    }
+
+    private byte[] executeTtsRequest(Request httpRequest) {
+        IOException lastException = null;
+
+        for (int i = 0; i < ttsConfig.getMaxRetryTimes(); i++) {
+            try (Response response = okHttpClient.newCall(httpRequest).execute()) {
+                if (!response.isSuccessful()) {
+                    throw new IOException("HTTP请求失败,状态码: " + response.code());
+                }
+
+                String responseBody = response.body().string();
+                log.debug("TTS API响应: {}", responseBody);
+
+                Map<String, Object> responseMap = objectMapper.readValue(responseBody, Map.class);
+                Integer code = (Integer) responseMap.get("code");
+                if (code != null && code != 3000) {
+                    String message = (String) responseMap.get("message");
+                    throw new VoiceCloneException(code != null ? code : -1,
+                            String.format("TTS合成失败: %s (错误码: %d)", message, code != null ? code : -1));
+                }
+                // 获取data字段(base64编码的音频数据)
+                Object data = responseMap.get("data");
+                if (data == null) {
+                    throw new VoiceCloneException(-1, "TTS音频数据为空");
+                }
+
+                if (!(data instanceof String)) {
+                    throw new VoiceCloneException(-1, "TTS音频数据格式错误");
+                }
+
+                String base64Audio = (String) data;
+
+                // 解码base64音频数据
+                try {
+                    return Base64.getDecoder().decode(base64Audio);
+                } catch (IllegalArgumentException e) {
+                    log.error("Base64解码失败", e);
+                    throw new VoiceCloneException("音频数据解码失败", e);
+                }
+
+            } catch (IOException e) {
+                lastException = e;
+                log.warn("第{}次TTS请求失败: {}", i + 1, e.getMessage());
+
+                if (i < ttsConfig.getMaxRetryTimes() - 1) {
+                    try {
+                        Thread.sleep(ttsConfig.getRetryInterval());
+                    } catch (InterruptedException ie) {
+                        Thread.currentThread().interrupt();
+                        throw new VoiceCloneException("重试被中断", ie);
+                    }
+                }
+            }
+        }
+
+        throw new VoiceCloneException("TTS请求失败,达到最大重试次数", lastException);
+    }
+
+    private void checkTtsResponse(TtsResponse response) {
+        if (response == null || response.getBaseResp() == null) {
+            throw new VoiceCloneException(-1, "TTS响应数据异常");
+        }
+
+        Integer statusCode = response.getBaseResp().getStatusCode();
+        if (statusCode != 0) {
+            String errorMessage = response.getBaseResp().getStatusMessage();
+            throw new VoiceCloneException(statusCode,
+                    String.format("TTS合成失败: %s (错误码: %d)", errorMessage, statusCode));
+        }
+
+        if (response.getData() == null) {
+            throw new VoiceCloneException(-1, "TTS音频数据为空");
+        }
+
+        if (response.getData().getAudio() == null) {
+            throw new VoiceCloneException(-1, "Base64音频数据为空");
+        }
+    }
+
+    private void autoSaveAudio(String base64Audio, String reqId, String format) {
+        try {
+            // 解码音频
+            byte[] audioBytes = Base64.getDecoder().decode(base64Audio);
+
+            // 创建保存目录
+            Path saveDir = Paths.get(ttsConfig.getAudioSavePath());
+            if (!Files.exists(saveDir)) {
+                Files.createDirectories(saveDir);
+            }
+
+            // 生成文件名
+            String fileName = String.format("auto_save_%s.%s",
+                    reqId.substring(0, 8), format);
+            Path filePath = saveDir.resolve(fileName);
+
+            // 保存文件
+            Files.write(filePath, audioBytes);
+
+            log.debug("音频自动保存成功: {}", filePath);
+
+        } catch (Exception e) {
+            log.warn("音频自动保存失败: {}", e.getMessage());
+            // 不抛出异常,自动保存失败不影响主要功能
+        }
+    }
+}

+ 343 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/service/impl/VoiceCloneServiceImpl.java

@@ -0,0 +1,343 @@
+package com.fs.aiSoundReplication.service.impl;
+
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fs.aiSoundReplication.config.VoiceCloneConfig;
+import com.fs.aiSoundReplication.exception.ErrorCodeEnum;
+import com.fs.aiSoundReplication.exception.VoiceCloneException;
+import com.fs.aiSoundReplication.param.*;
+import com.fs.aiSoundReplication.service.VoiceCloneService;
+import com.fs.aiSoundReplication.util.FileUtil;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.util.*;
+
+@Service
+@Slf4j
+public class VoiceCloneServiceImpl implements VoiceCloneService {
+
+    @Autowired
+    private VoiceCloneConfig config;
+
+    @Autowired
+    private OkHttpClient okHttpClient;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    private static final String AUTHORIZATION_HEADER = "Authorization";
+    private static final String RESOURCE_ID_HEADER = "Resource-Id";
+
+    @Override
+    public UploadResponse uploadVoice(String speakerId, MultipartFile audioFile,
+                                      Integer modelType, Integer language) {
+        try {
+            // 1. 参数校验
+            validateUploadParams(speakerId, audioFile, modelType);
+
+            // 2. 构建请求
+            VoiceCloneRequest request = buildUploadRequest(speakerId, audioFile, modelType, language);
+            String requestBody = objectMapper.writeValueAsString(request);
+
+            // 3. 构建请求头
+            String resourceId = getResourceIdByModelType(modelType);
+            Request httpRequest = buildHttpRequest(config.getUploadUrl(), resourceId, requestBody);
+
+            // 4. 发送请求(带重试机制)
+            UploadResponse response = executeRequestWithRetry(httpRequest, UploadResponse.class);
+
+            // 5. 检查响应
+            checkResponse(response);
+
+            log.info("音色上传成功,speakerId: {}", response.getSpeakerId());
+            return response;
+
+        } catch (JsonProcessingException e) {
+            log.error("JSON序列化失败", e);
+            throw new VoiceCloneException("JSON序列化失败", e);
+        } catch (IOException e) {
+            log.error("文件处理失败", e);
+            throw new VoiceCloneException("文件处理失败", e);
+        }
+    }
+
+    @Override
+    public UploadResponse uploadVoiceByPath(String speakerId, String filePath,
+                                            Integer modelType, Integer language) {
+        try {
+            // 1. 参数校验
+            validateUploadParams(speakerId, null, modelType);
+
+            // 2. 读取文件并转换为Base64
+            String base64Audio = FileUtil.fileToBase64(filePath);
+            String fileExtension = FileUtil.getFileExtension(filePath);
+
+            // 3. 构建音频信息
+            VoiceCloneRequest.AudioInfo audioInfo = new VoiceCloneRequest.AudioInfo();
+            audioInfo.setAudioBytes(base64Audio);
+            audioInfo.setAudioFormat(fileExtension);
+
+            VoiceCloneRequest request = new VoiceCloneRequest();
+            request.setAppid(config.getAppId());
+            request.setSpeakerId(speakerId);
+            request.setAudios(Collections.singletonList(audioInfo));
+            request.setModelType(modelType);
+            request.setLanguage(language != null ? language : 0);
+            request.setSource(2);
+
+            // 4. 构建请求头并发送
+            String resourceId = getResourceIdByModelType(modelType);
+            String requestBody = objectMapper.writeValueAsString(request);
+            Request httpRequest = buildHttpRequest(config.getUploadUrl(), resourceId, requestBody);
+
+            UploadResponse response = executeRequestWithRetry(httpRequest, UploadResponse.class);
+            checkResponse(response);
+
+            log.info("音色上传成功(文件路径方式),speakerId: {}", response.getSpeakerId());
+            return response;
+
+        } catch (IOException e) {
+            log.error("文件处理失败", e);
+            throw new VoiceCloneException("文件处理失败", e);
+        }
+    }
+
+    @Override
+    public StatusResponse queryTrainingStatus(String speakerId) {
+        try {
+            // 1. 参数校验
+            if (speakerId == null || speakerId.trim().isEmpty()) {
+                throw new VoiceCloneException(1001, "speakerId不能为空");
+            }
+
+            // 2. 构建请求
+            TrainingStatusRequest request = new TrainingStatusRequest();
+            request.setAppid(config.getAppId());
+            request.setSpeakerId(speakerId);
+            String requestBody = objectMapper.writeValueAsString(request);
+
+            // 3. 根据历史记录确定资源ID,默认使用ICL 2.0
+            String resourceId = config.getResourceIdIcl2();
+            Request httpRequest = buildHttpRequest(config.getStatusUrl(), resourceId, requestBody);
+
+            // 4. 发送请求
+            StatusResponse response = executeRequest(httpRequest, StatusResponse.class);
+            checkResponse(response);
+
+            log.debug("训练状态查询成功,speakerId: {}, 状态: {}",
+                    response.getSpeakerId(), response.getStatusText());
+            return response;
+
+        } catch (IOException e) {
+            log.error("JSON序列化失败", e);
+            throw new VoiceCloneException("JSON序列化失败", e);
+        }
+    }
+
+    @Override
+    public StatusResponse pollTrainingStatus(String speakerId, Integer maxPollingTimes,
+                                             Long pollingInterval) {
+        if (maxPollingTimes == null) {
+            maxPollingTimes = config.getMaxPollingTimes();
+        }
+        if (pollingInterval == null) {
+            pollingInterval = config.getPollingInterval();
+        }
+
+        StatusResponse finalResponse = null;
+
+        for (int i = 0; i < maxPollingTimes; i++) {
+            try {
+                // 查询状态
+                StatusResponse response = queryTrainingStatus(speakerId);
+                finalResponse = response;
+
+                // 检查状态
+                Integer status = response.getStatus();
+                if (status == 2 || status == 4) {
+                    log.info("音色训练完成,speakerId: {}, 状态: {}",
+                            speakerId, response.getStatusText());
+                    return response;
+                } else if (status == 3) {
+                    log.error("音色训练失败,speakerId: {}", speakerId);
+                    throw new VoiceCloneException(ErrorCodeEnum.fromCode(status).getCode(),
+                            "训练失败,状态码: " + status);
+                } else if (status == 1) {
+                    log.info("训练中... (第{}次轮询)", i + 1);
+                    // 等待指定间隔
+                    Thread.sleep(pollingInterval);
+                } else {
+                    log.warn("未知状态,停止轮询,状态码: {}", status);
+                    return response;
+                }
+
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new VoiceCloneException("轮询被中断", e);
+            } catch (Exception e) {
+                log.error("第{}次轮询失败", i + 1, e);
+                // 非最后一次失败,继续尝试
+                if (i < maxPollingTimes - 1) {
+                    try {
+                        Thread.sleep(pollingInterval);
+                    } catch (InterruptedException ie) {
+                        Thread.currentThread().interrupt();
+                        throw new VoiceCloneException("轮询被中断", ie);
+                    }
+                } else {
+                    throw e instanceof VoiceCloneException ?
+                            (VoiceCloneException) e :
+                            new VoiceCloneException("轮询失败", e);
+                }
+            }
+        }
+
+        log.warn("达到最大轮询次数仍未完成,speakerId: {}", speakerId);
+        return finalResponse;
+    }
+
+    @Override
+    public String getResourceIdByModelType(Integer modelType) {
+        if (modelType == null) {
+            return config.getResourceIdIcl2(); // 默认使用ICL 2.0
+        }
+
+        switch (modelType) {
+            case 1: // ICL 1.0
+                return config.getResourceIdIcl1();
+            case 2: // DiT标准版(使用ICL 1.0)
+            case 3: // DiT还原版(使用ICL 1.0)
+                return config.getResourceIdIcl1();
+            case 4: // ICL 2.0
+                return config.getResourceIdIcl2();
+            default:
+                log.warn("未知的modelType: {},使用默认ICL 2.0", modelType);
+                return config.getResourceIdIcl2();
+        }
+    }
+
+    // ============ 私有方法 ============
+
+    private void validateUploadParams(String speakerId, MultipartFile audioFile,
+                                      Integer modelType) {
+        if (speakerId == null || speakerId.trim().isEmpty()) {
+            throw new VoiceCloneException(1001, "speakerId不能为空");
+        }
+
+        if (audioFile != null && audioFile.isEmpty()) {
+            throw new VoiceCloneException(1001, "音频文件不能为空");
+        }
+
+        if (modelType != null && modelType < 0 || modelType > 4) {
+            throw new VoiceCloneException(1001, "modelType参数错误,应为1-4");
+        }
+    }
+
+    private VoiceCloneRequest buildUploadRequest(String speakerId, MultipartFile audioFile,
+                                                 Integer modelType, Integer language) throws IOException {
+        VoiceCloneRequest request = new VoiceCloneRequest();
+        request.setAppid(config.getAppId());
+        request.setSpeakerId(speakerId);
+        request.setModelType(modelType != null ? modelType : 4);
+        request.setLanguage(language != null ? language : 0);
+        request.setSource(2);
+
+        // 构建音频信息
+        VoiceCloneRequest.AudioInfo audioInfo = new VoiceCloneRequest.AudioInfo();
+        audioInfo.setAudioBytes(FileUtil.multipartFileToBase64(audioFile));
+
+        // 获取文件扩展名
+        String originalFilename = audioFile.getOriginalFilename();
+        String fileExtension = FileUtil.getFileExtension(originalFilename);
+        if (fileExtension != null){
+            if (fileExtension.equals("m4a") || fileExtension.equals("pcm"))
+                audioInfo.setAudioFormat(fileExtension);
+        }
+        request.setAudios(Collections.singletonList(audioInfo));
+
+//        // 设置额外参数-降噪
+//        Map<String, Object> extraParams = new HashMap<>();
+//        // ICL 2.0默认关闭降噪以获得更多细节
+//        if (modelType == null || modelType == 4) {
+//            extraParams.put("enable_audio_denoise", false);
+//        } else {
+//            extraParams.put("enable_audio_denoise", true);
+//        }
+//        request.setExtraParams(objectMapper.writeValueAsString(extraParams));
+
+        return request;
+    }
+
+    private Request buildHttpRequest(String url, String resourceId, String requestBody) {
+        RequestBody body = RequestBody.create(
+                MediaType.get("application/json; charset=utf-8"),requestBody
+        );
+
+        return new Request.Builder()
+                .url(url)
+                .post(body)
+                .addHeader(AUTHORIZATION_HEADER, "Bearer;" + config.getAccessToken())
+                .addHeader(RESOURCE_ID_HEADER, resourceId)
+                .build();
+    }
+
+    private <T extends BaseResponse> T executeRequest(Request httpRequest, Class<T> responseType)
+            throws IOException {
+        try (Response response = okHttpClient.newCall(httpRequest).execute()) {
+            if (!response.isSuccessful()) {
+                throw new IOException("HTTP请求失败,状态码: " + response.code());
+            }
+
+            String responseBody = response.body().string();
+            log.debug("API响应: {}", responseBody);
+
+            return objectMapper.readValue(responseBody, responseType);
+        }
+    }
+
+    private <T extends BaseResponse> T executeRequestWithRetry(Request httpRequest,
+                                                               Class<T> responseType) {
+        IOException lastException = null;
+
+        for (int i = 0; i < config.getMaxRetryTimes(); i++) {
+            try {
+                return executeRequest(httpRequest, responseType);
+            } catch (IOException e) {
+                lastException = e;
+                log.warn("第{}次请求失败,准备重试: {}", i + 1, e.getMessage());
+
+                if (i < config.getMaxRetryTimes() - 1) {
+                    try {
+                        Thread.sleep(config.getRetryInterval());
+                    } catch (InterruptedException ie) {
+                        Thread.currentThread().interrupt();
+                        throw new VoiceCloneException("重试被中断", ie);
+                    }
+                }
+            }
+        }
+
+        throw new VoiceCloneException("请求失败,达到最大重试次数", lastException);
+    }
+
+    private void checkResponse(BaseResponse response) {
+        if (response == null || response.getBaseResp() == null) {
+            throw new VoiceCloneException(-1, "响应数据异常");
+        }
+
+        Integer statusCode = response.getBaseResp().getStatusCode();
+        if (statusCode != 0) {
+            String errorMessage = response.getBaseResp().getStatusMessage();
+            ErrorCodeEnum errorCode = ErrorCodeEnum.fromCode(statusCode);
+
+            throw new VoiceCloneException(statusCode,
+                    String.format("%s (错误码: %d)", errorCode.getMessage(), statusCode));
+        }
+    }
+}

+ 44 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/util/FileToMultipartConverterUtil.java

@@ -0,0 +1,44 @@
+package com.fs.aiSoundReplication.util;
+
+import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.web.multipart.MultipartFile;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+public class FileToMultipartConverterUtil {
+    /*file转成multipartFile*/
+    public static MultipartFile convert(File file) throws IOException {
+        if (file == null || !file.exists()) {
+            throw new IllegalArgumentException("文件不存在");
+        }
+
+        try (FileInputStream input = new FileInputStream(file)) {
+            return new MockMultipartFile(
+                    file.getName(),           // 文件名
+                    file.getName(),           // 原始文件名(通常与文件名相同)
+                    getContentType(file),     // 内容类型
+                    input                     // 文件输入流
+            );
+        }
+    }
+
+    private static String getContentType(File file) {
+        String fileName = file.getName();
+        if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) {
+            return "image/jpeg";
+        } else if (fileName.endsWith(".png")) {
+            return "image/png";
+        } else if (fileName.endsWith(".pdf")) {
+            return "application/pdf";
+        } else if (fileName.endsWith(".txt")) {
+            return "text/plain";
+        } else if (fileName.endsWith(".mp3")) {
+            return "audio/mpeg";
+        } else if (fileName.endsWith(".mp4")) {
+            return "video/mp4";
+        }
+        // 默认类型
+        return "application/octet-stream";
+    }
+}

+ 47 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/util/FileUtil.java

@@ -0,0 +1,47 @@
+package com.fs.aiSoundReplication.util;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Base64;
+
+@Slf4j
+public class FileUtil {
+
+    public static String fileToBase64(String filePath) throws IOException {
+        File file = new File(filePath);
+        if (!file.exists() || !file.isFile()) {
+            throw new IOException("文件不存在: " + filePath);
+        }
+
+        if (file.length() > 10 * 1024 * 1024) { // 10MB限制
+            throw new IOException("文件大小超过10MB限制");
+        }
+
+        byte[] bytes = Files.readAllBytes(file.toPath());
+        return Base64.getEncoder().encodeToString(bytes);
+    }
+
+    public static String getFileExtension(String fileName) {
+        if (fileName == null || !fileName.contains(".")) {
+            return "";
+        }
+        return fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
+    }
+
+    public static String multipartFileToBase64(MultipartFile file) throws IOException {
+        if (file.isEmpty()) {
+            throw new IOException("文件为空");
+        }
+
+        if (file.getSize() > 10 * 1024 * 1024) { // 10MB限制
+            throw new IOException("文件大小超过10MB限制");
+        }
+
+        byte[] bytes = file.getBytes();
+        return Base64.getEncoder().encodeToString(bytes);
+    }
+}

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

@@ -39,8 +39,10 @@ public class CompanyRedPacketBalanceLogs extends BaseEntity{
     @Excel(name = "类型")
     private Integer logsType;
 
-    /** 是否处理状态(0-初始化,1-已同步) */
+    /** 是否处理状态(0-初始化,1-已同步(收到红包回调)) */
     private Long status;
 
+    // 红包日志id
+    private Long redPacketId;
 
 }

+ 24 - 4
fs-service/src/main/java/com/fs/company/mapper/CompanyMoneyLogsMapper.java

@@ -201,10 +201,10 @@ public interface CompanyMoneyLogsMapper
             "select l.*,c.company_name,o.order_code,s.store_name,o.delivery_sn as delivery_id,p.pay_code,p.pay_type_code,o.package_name,o.package_second_name,io.order_sn,o.user_name,cu.nick_name as company_user_name \n" +
             "            from company_money_logs l  " +
             "left join company c on c.company_id=l.company_id " +
-            "left join fs_store_order o on (o.order_id=l.business_id and (l.logs_type=3 or  l.logs_type=4 or l.logs_type=5 or  l.logs_type=6 or  l.logs_type=13 or  l.logs_type=14))   " +
+            "left join fs_store_order_scrm o on (o.id=l.business_id and (l.logs_type=3 or  l.logs_type=4 or l.logs_type=5 or  l.logs_type=6 or  l.logs_type=13 or  l.logs_type=14))   " +
             "left join company_user cu on  cu.user_id = o.company_user_id   " +
-            "left join fs_store s on (o.store_id=s.store_id and (l.logs_type=3 or  l.logs_type=4 or l.logs_type=5 or  l.logs_type=6 or  l.logs_type=13 or  l.logs_type=14))   " +
-            "left join fs_store_payment p on ( p.`status`=1 and p.business_code=o.order_code and (l.logs_type=3 or  l.logs_type=4 or l.logs_type=5 or  l.logs_type=6 or  l.logs_type=13 or  l.logs_type=14))  " +
+            "left join fs_store_scrm s on (o.store_id=s.store_id and (l.logs_type=3 or  l.logs_type=4 or l.logs_type=5 or  l.logs_type=6 or  l.logs_type=13 or  l.logs_type=14))   " +
+            "left join fs_store_payment_scrm p on ( p.`status`=1 and p.business_code=o.order_code and (l.logs_type=3 or  l.logs_type=4 or l.logs_type=5 or  l.logs_type=6 or  l.logs_type=13 or  l.logs_type=14))  " +
             "left join fs_inquiry_order io on (io.order_id=l.business_id and (l.logs_type=12)) "+
             " where 1=1 " +
             "<if test = 'maps.companyId != null  '> " +
@@ -235,7 +235,7 @@ public interface CompanyMoneyLogsMapper
 //            "</script>"})
     @Select({"<script> " +
             "select l.*,o.*,c.company_name,cd.dept_name,cu.nick_name as company_user_name  FROM company_money_logs l " +
-            " LEFT JOIN fs_store_order o ON o.order_id=l.business_id and o.company_id =l.company_id  " +
+            " LEFT JOIN fs_store_order_scrm o ON o.id=l.business_id and o.company_id =l.company_id  " +
             " LEFT JOIN company c ON c.company_id= l.company_id " +
             " LEFT JOIN company_user cu ON cu.user_id=o.company_user_id " +
             " LEFT JOIN company_dept cd ON cd.dept_id=cu.dept_id   " +
@@ -292,6 +292,26 @@ public interface CompanyMoneyLogsMapper
 
     @Select("select * FROM company_money_logs where logs_type=12 AND business_id=#{id} limit 1")
     CompanyMoneyLogs selectCompanyMoneyByInquiry(Long id);
+
+    /**
+     * 批量更新公司金额日志
+     * @param list 日志列表
+     */
+    void batchUpdateCompanyMoneyLogs(@Param("list") List<CompanyMoneyLogs> list);
+
+    /**
+     * 查询所有分佣日志(logsType = 3)
+     * @return 分佣日志列表
+     */
+    @Select("SELECT * FROM company_money_logs WHERE logs_type = 3")
+    List<CompanyMoneyLogs> selectAllCommissionLogs();
+
+    /**
+     * 查询所有退佣日志(logsType = 4 或 5)
+     * @return 退佣日志列表
+     */
+    @Select("SELECT * FROM company_money_logs WHERE logs_type = 4 OR logs_type = 5")
+    List<CompanyMoneyLogs> selectAllRefundLogs();
     @Select({"<script> " +
             "select  count(1) from company_money_logs l" +
             " where 1=1 " +

+ 13 - 7
fs-service/src/main/java/com/fs/company/mapper/CompanyRedPacketBalanceLogsMapper.java

@@ -1,20 +1,22 @@
 package com.fs.company.mapper;
 
+import java.util.Date;
 import java.util.List;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.company.domain.Company;
 import com.fs.company.domain.CompanyRedPacketBalanceLogs;
+import org.apache.ibatis.annotations.Param;
 
 /**
  * 企业红包余额记录Mapper接口
- * 
+ *
  * @author fs
  * @date 2025-11-19
  */
 public interface CompanyRedPacketBalanceLogsMapper extends BaseMapper<CompanyRedPacketBalanceLogs>{
     /**
      * 查询企业红包余额记录
-     * 
+     *
      * @param logsId 企业红包余额记录主键
      * @return 企业红包余额记录
      */
@@ -22,7 +24,7 @@ public interface CompanyRedPacketBalanceLogsMapper extends BaseMapper<CompanyRed
 
     /**
      * 查询企业红包余额记录列表
-     * 
+     *
      * @param companyRedPacketBalanceLogs 企业红包余额记录
      * @return 企业红包余额记录集合
      */
@@ -30,7 +32,7 @@ public interface CompanyRedPacketBalanceLogsMapper extends BaseMapper<CompanyRed
 
     /**
      * 新增企业红包余额记录
-     * 
+     *
      * @param companyRedPacketBalanceLogs 企业红包余额记录
      * @return 结果
      */
@@ -38,7 +40,7 @@ public interface CompanyRedPacketBalanceLogsMapper extends BaseMapper<CompanyRed
 
     /**
      * 修改企业红包余额记录
-     * 
+     *
      * @param companyRedPacketBalanceLogs 企业红包余额记录
      * @return 结果
      */
@@ -46,7 +48,7 @@ public interface CompanyRedPacketBalanceLogsMapper extends BaseMapper<CompanyRed
 
     /**
      * 删除企业红包余额记录
-     * 
+     *
      * @param logsId 企业红包余额记录主键
      * @return 结果
      */
@@ -54,11 +56,15 @@ public interface CompanyRedPacketBalanceLogsMapper extends BaseMapper<CompanyRed
 
     /**
      * 批量删除企业红包余额记录
-     * 
+     *
      * @param logsIds 需要删除的数据主键集合
      * @return 结果
      */
     int deleteCompanyRedPacketBalanceLogsByLogsIds(Long[] logsIds);
 
     Company getCompanyRedPacketBalance(Long companyId);
+
+    void updateCompanyRedPacketBalanceLogsByRedPacketId(CompanyRedPacketBalanceLogs redLogs);
+
+    List<CompanyRedPacketBalanceLogs> selectCompanyRedPacketBalanceLogsListByStatus(@Param("createSTime") String createSTime,@Param("createETime") String createETime);
 }

+ 6 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyUserMapper.java

@@ -5,6 +5,7 @@ import com.fs.common.enums.DataSourceType;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.param.CompanyUserAreaParam;
 import com.fs.company.param.CompanyUserQwParam;
+import com.fs.company.param.VcCompanyUser;
 import com.fs.company.vo.*;
 import com.fs.his.vo.OptionsVO;
 import com.fs.qw.vo.CompanyUserQwVO;
@@ -351,4 +352,9 @@ public interface CompanyUserMapper
      */
     List<com.fs.hisStore.domain.FsUserScrm> selectBoundFsUsersByCompanyUserId(@Param("companyUserId") Long companyUserId);
 
+    CompanyUser selectCompanyUserByQwUserId(@Param("qwUserId") Long id);
+
+    VcCompanyUser selectVcCompanyUserByCompanyUserId(@Param("companyUserId")Long companyUserId);
+
+    int updateVcCompanyUser(@Param("vcCompanyUser") VcCompanyUser vcCompanyUser);
 }

+ 99 - 0
fs-service/src/main/java/com/fs/company/param/VcCompanyUser.java

@@ -0,0 +1,99 @@
+package com.fs.company.param;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+/**
+ * 公司用户音色表实体类
+ *
+ * @author
+ * @since 2024-XX-XX
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Accessors(chain = true)
+@TableName("vc_company_user")
+public class VcCompanyUser implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    /**
+     * 销售已上传声音次数(单个音色只给单个销售用,而且只能传5次)
+     */
+    @TableField("times")
+    private Integer times;
+
+    /**
+     * 上传豆包的音色id
+     */
+    @TableField("speaker_id")
+    private String speakerId;
+
+    /**
+     * 销售传入的声纹地址
+     */
+    @TableField("upload_url")
+    private String uploadUrl;
+
+    /**
+     * 上传的语音时长(秒)
+     */
+    @TableField("upload_time")
+    private Double uploadTime;
+
+    /**
+     * 用户id
+     */
+    @TableField("company_user_id")
+    private Long companyUserId;
+
+    /**
+     * 最后一次文字转语音生成的url
+     */
+    @TableField("latest_text_to_speech_url")
+    private String latestTextToSpeechUrl;
+
+    // 下面是可选添加的方法和字段
+
+    /**
+     * 判断是否还可以上传声音(最大5次)
+     * @return true: 可以上传,false: 已达上限
+     */
+    public boolean canUpload() {
+        return times == null || times < 5;
+    }
+
+    /**
+     * 增加上传次数
+     */
+    public void incrementTimes() {
+        if (times == null) {
+            times = 1;
+        } else {
+            times++;
+        }
+    }
+
+    /**
+     * 获取剩余可上传次数
+     */
+    public Integer getRemainingTimes() {
+        if (times == null) {
+            return 5;
+        }
+        return Math.max(0, 5 - times);
+    }
+}

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

@@ -2,6 +2,7 @@ package com.fs.company.service;
 
 import java.math.BigDecimal;
 import java.time.LocalTime;
+import java.util.Date;
 import java.util.List;
 
 import com.fs.common.core.domain.R;
@@ -177,7 +178,7 @@ public interface ICompanyService
 
     void redPacketTopUpCompany(Long companyId, BigDecimal money,String type);
 
-    void asyncRecordBalanceLog(Long companyId, BigDecimal money,Integer logType, BigDecimal balance, String remark);
+    void asyncRecordBalanceLog(Long companyId, BigDecimal money, Integer logType, BigDecimal balance, String remark, Long logId);
 
     void recordRedPacketBalance();
 
@@ -187,7 +188,7 @@ public interface ICompanyService
      */
     void batchUpdateCompany(List<Company> list);
 
-    void rollbackRedPacketMoney();
+    void rollbackRedPacketMoney(String createSTime, String createETime);
 
 
     List<CompanyVO> liveShowList(CompanyParam param);
@@ -197,4 +198,8 @@ public interface ICompanyService
     void addCompanyTuiLiveMoney(LiveOrder order);
 
     void subLiveCompanyMoney(LiveOrder order);
+
+    String checkMchTransferStatus(String outBatchNo,Long companyId);
+
+    R checkMchTransferStatusByBatchID(String batchId, Long companyId);
 }

+ 322 - 34
fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java

@@ -6,12 +6,12 @@ import java.util.*;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
-import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.fs.common.constant.FsConstants;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.exception.CustomException;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.SecurityUtils;
 import com.fs.common.utils.StringUtils;
@@ -19,11 +19,13 @@ import com.fs.company.domain.*;
 import com.fs.company.mapper.*;
 import com.fs.company.param.CompanyLiveShowParam;
 import com.fs.company.param.CompanyParam;
-import com.fs.company.service.ICompanyMiniappService;
-import com.fs.company.service.ICompanyProfitService;
-import com.fs.company.service.ICompanyRoleService;
+import com.fs.company.service.*;
 import com.fs.company.vo.*;
+import com.fs.course.config.CourseConfig;
+import com.fs.course.config.RedPacketConfig;
+import com.fs.course.domain.FsCourseRedPacketLog;
 import com.fs.course.mapper.FsCourseRedPacketLogMapper;
+import com.fs.course.service.IFsCourseRedPacketLogService;
 import com.fs.his.config.StoreConfig;
 import com.fs.his.domain.FsInquiryOrder;
 import com.fs.his.domain.FsStoreOrder;
@@ -41,18 +43,26 @@ import com.fs.store.config.CompanyMenuConfig;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.service.ISysConfigService;
+import com.github.binarywang.wxpay.bean.transfer.QueryTransferBatchesRequest;
+import com.github.binarywang.wxpay.bean.transfer.QueryTransferBatchesResult;
+import com.github.binarywang.wxpay.bean.transfer.TransferBillsGetResult;
+import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.TransferService;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
 import com.github.pagehelper.PageHelper;
 import com.google.gson.Gson;
 import org.apache.commons.collections4.CollectionUtils;
-import org.apache.commons.lang3.ObjectUtils;
 import org.redisson.api.RLock;
 import org.redisson.api.RedissonClient;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
-import com.fs.company.service.ICompanyService;
 import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.support.TransactionTemplate;
@@ -120,6 +130,7 @@ public class CompanyServiceImpl implements ICompanyService
     private TransactionTemplate transactionTemplate;
 
     @Autowired
+    @Lazy
     private ILiveService liveService;
     @Autowired
     private LiveOrderMapper liveOrderMapper;
@@ -127,6 +138,9 @@ public class CompanyServiceImpl implements ICompanyService
     @Autowired
     private CompanyRedPacketBalanceLogsMapper companyRedPacketBalanceLogsMapper;
 
+    @Autowired
+    private ICompanyConfigService companyConfigService;
+
 
     @Override
     public List<CompanyVO> liveShowList(CompanyParam param) {
@@ -184,6 +198,150 @@ public class CompanyServiceImpl implements ICompanyService
         }
     }
 
+    @Override
+    public String checkMchTransferStatus(String outBatchNo,Long companyId) {
+
+        // 查看红包发送配置
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig courseConfig = JSONUtil.toBean(json, CourseConfig.class);
+        RedPacketConfig config;
+        switch (courseConfig.getRedPacketMode()){// 1-总配置 2- 分公司配置
+            case 1:
+                json = configService.selectConfigByKey("redPacket.config");
+                config = JSONUtil.toBean(json, RedPacketConfig.class);
+                break;
+            case 2:
+                json = companyConfigService.selectRedPacketConfigByKey(companyId);
+                //如果分公司配置为空就走总后台的配置
+                if (StringUtils.isEmpty(json)){
+                    json = configService.selectConfigByKey("redPacket.config");
+                }
+                config = JSONUtil.toBean(json, RedPacketConfig.class);
+                break;
+            default:
+                throw new UnsupportedOperationException("当前红包模式不支持!");
+        }
+
+        WxPayConfig payConfig = new WxPayConfig();
+        BeanUtils.copyProperties(config, payConfig);
+
+        WxPayService wxPayService = new WxPayServiceImpl();
+        wxPayService.setConfig(payConfig);
+
+        TransferService transferService = wxPayService.getTransferService();
+
+        if (Objects.isNull(config.getIsNew()) || !Arrays.asList(0,1).contains(config.getIsNew())) {
+            logger.error("红包配置错误 isNew is err");
+            throw new CustomException("红包配置错误 isNew is err ");
+        }
+
+        try {
+            if (config.getIsNew() == 0) {
+                QueryTransferBatchesRequest request = new QueryTransferBatchesRequest();
+                request.setOutBatchNo(outBatchNo);
+                request.setNeedQueryDetail(true);
+                request.setOffset(0);
+                request.setLimit(20);
+                request.setDetailStatus("ALL");
+                QueryTransferBatchesResult result = transferService.transferBatchesOutBatchNo(request);
+                List<QueryTransferBatchesResult.TransferDetail> detailList = result.getTransferDetailList();
+                boolean isSuccess = detailList.stream().anyMatch(d -> "SUCCESS".equals(d.getDetailStatus()));
+                if (isSuccess) {
+                    return result.getTransferBatch().getBatchId();
+                }
+
+                boolean isFail = detailList.stream().anyMatch(d -> "FAIL".equals(d.getDetailStatus()));
+                if (isFail) {
+                    return "";
+                }
+            } else {
+                TransferBillsGetResult result = transferService.getBillsByOutBillNo(outBatchNo);
+                if ("SUCCESS".equals(result.getState())) {
+                    return result.getTransferBillNo();
+                } else if ("FAIL".equals(result.getState())) {
+                    return "";
+                } else if ("CANCELLED".equals(result.getState())) {
+                    return "";
+                }
+            }
+        } catch (WxPayException e) {
+            logger.error("查询转账单失败 err: {}", e.getMessage(), e);
+            throw new CustomException("查询转账单失败:" + e.getMessage());
+        }
+
+        throw new CustomException("转账处理中");
+    }
+
+    @Override
+    public R checkMchTransferStatusByBatchID(String batchId, Long companyId) {
+        // 获取配置信息
+        CourseConfig courseConfig = JSONUtil.toBean(configService.selectConfigByKey("course.config"), CourseConfig.class);
+
+        String json;
+        RedPacketConfig config = new RedPacketConfig();
+        // 根据红包模式获取配置
+        switch (courseConfig.getRedPacketMode()){
+            case 1:
+                json = configService.selectConfigByKey("redPacket.config");
+                config = JSONUtil.toBean(json, RedPacketConfig.class);
+                break;
+            case 2:
+                json = companyConfigService.selectRedPacketConfigByKey(companyId);
+                //如果分公司配置为空就走总后台的配置
+                if (StringUtils.isEmpty(json)){
+                    json = configService.selectConfigByKey("redPacket.config");
+                }
+                config = JSONUtil.toBean(json, RedPacketConfig.class);
+                break;
+            default:
+                throw new UnsupportedOperationException("当前红包模式不支持!");
+        }
+
+        //创建微信订单
+        WxPayConfig payConfig = new WxPayConfig();
+        BeanUtils.copyProperties(config,payConfig);
+        WxPayService wxPayService = new WxPayServiceImpl();
+        wxPayService.setConfig(payConfig);
+        TransferService transferService=wxPayService.getTransferService();
+
+        Map<String,Object> map = new HashMap<>();
+        map.put("status","待确认"); //
+        if (config.getIsNew() != null && config.getIsNew() == 1) {
+            try {
+                TransferBillsGetResult queryRedPacketResult = transferService.getBillsByTransferBillNo(batchId);
+                logger.info("FsCourseRedPacketLog-batchId:{},【红包处理】查询批次结果:{}",batchId,queryRedPacketResult.toString());
+                if(("SUCCESS").equals(queryRedPacketResult.getState())){
+                    map.put("status","success");
+                }else if(("FAIL").equals(queryRedPacketResult.getState())){
+                    map.put("status","fail");
+                }
+                return R.ok(map);
+            } catch (WxPayException e) {
+                logger.error(e.getMessage());
+                return R.error(e.getMessage());
+            }
+        } else {
+            QueryTransferBatchesRequest request = new QueryTransferBatchesRequest();
+            request.setBatchId(batchId);
+            request.setNeedQueryDetail(false);
+
+            try {
+                QueryTransferBatchesResult queryTransferBatchesResult = transferService.transferBatchesBatchId(request);
+                logger.info("FsCourseRedPacketLog-batchId,【红包处理】查询批次结果:{}",batchId,queryTransferBatchesResult.toString());
+                if(("FINISHED").equals(queryTransferBatchesResult.getTransferBatch().getBatchStatus())){
+                    map.put("status","success");
+                }else if(("CLOSED").equals(queryTransferBatchesResult.getTransferBatch().getBatchStatus())){
+                    map.put("status","fail");
+                }
+                return R.ok(map);
+            } catch (WxPayException e) {
+                logger.error(e.getMessage());
+                return R.error(e.getMessage());
+            }
+        }
+
+    }
+
     @Override
     public List<OptionsVO> selectAllCompanyList(Long deptId) {
         return companyMapper.selectAllCompanyList(deptId);
@@ -520,13 +678,23 @@ public class CompanyServiceImpl implements ICompanyService
                 orderMap.setTuiMoneyStatus(1);
                 storeOrderMapper.updateFsStoreOrder(orderMap);
                 // order.getPayRemain() 数据库实际没有这个字段了 直接使用 应付金额
-                BigDecimal money = order.getPayPrice();
-                company.setMoney(company.getMoney().add(money));
+                // 卓美,按照润天进行百分比进行分佣
+                String json =configService.selectConfigByKey("store.config");
+                com.fs.store.config.StoreConfig config= JSONUtil.toBean(json, com.fs.store.config.StoreConfig.class);
+                //支付金额-(订单金额*rate%)
+                BigDecimal tuiMoney = BigDecimal.ZERO;
+                if (config != null && config.getTuiMoneyRate() != null) {
+                    Double rate = config.getTuiMoneyRate() / 100d;
+                    tuiMoney = order.getTotalPrice().multiply(new BigDecimal(rate));
+                } else {
+                    tuiMoney = order.getPayPrice();
+                }
+                company.setMoney(company.getMoney().add(tuiMoney));
                 companyMapper.updateCompany(company);
                 CompanyMoneyLogs log=new CompanyMoneyLogs();
                 log.setCompanyId(company.getCompanyId());
                 log.setRemark("佣金入账");
-                log.setMoney(money);
+                log.setMoney(tuiMoney);
                 log.setLogsType(3);
                 log.setBalance(company.getMoney());
                 log.setCreateTime(new Date());
@@ -545,20 +713,34 @@ public class CompanyServiceImpl implements ICompanyService
         if(order.getCompanyId()>0){
             Company company=companyMapper.selectCompanyByIdForUpdate(order.getCompanyId());
             if(company!=null){
-                company.setMoney(company.getMoney().subtract(order.getTuiMoney()));
-                company.setTuiMoney(company.getTuiMoney().subtract(order.getTuiMoney()));
+                // 卓美,按照润天进行百分比进行分佣
+                String json =configService.selectConfigByKey("store.config");
+                com.fs.store.config.StoreConfig config= JSONUtil.toBean(json, com.fs.store.config.StoreConfig.class);
+                //支付金额-(订单金额*rate%)
+                BigDecimal tuiMoney = BigDecimal.ZERO;
+                if (config != null && config.getTuiMoneyRate() != null) {
+                    double rate = config.getTuiMoneyRate() / 100d;
+                    tuiMoney = order.getTotalPrice().multiply(new BigDecimal(rate));
+                } else {
+                    tuiMoney = order.getPayPrice();
+                }
+                company.setMoney(company.getMoney().add(tuiMoney));
                 companyMapper.updateCompany(company);
                 //写入日志
                 CompanyMoneyLogs log=new CompanyMoneyLogs();
                 log.setCompanyId(order.getCompanyId());
-                log.setRemark("订单佣金退款");
-                log.setMoney(order.getTuiMoney().multiply(new BigDecimal(-1)));
-                log.setLogsType(4);
+                log.setRemark("佣金入账");
+                log.setMoney(tuiMoney);
+                log.setLogsType(3);
                 log.setBalance(company.getMoney());
                 log.setCreateTime(new Date());
                 log.setBusinessId(order.getOrderId().toString());
                 moneyLogsMapper.insertCompanyMoneyLogs(log);
-
+                LiveOrder liveOrder = new LiveOrder();
+                liveOrder.setOrderId(order.getOrderId());
+                liveOrder.setTuiMoneyStatus(1);
+                liveOrder.setTuiMoney(tuiMoney);
+                liveOrderMapper.updateLiveOrder(liveOrder);
             }
         }
     }
@@ -1447,7 +1629,7 @@ public class CompanyServiceImpl implements ICompanyService
                                     // 记录余额变更日志
                                     String remark = "同步公司余额,差额: " + amount+"(正数为增加,负数为扣减)";
                                     // 实际不发生交易只是从缓存同步金额到数据库中 交易金额登记为0,备注清楚同步的金额
-                                    asyncRecordBalanceLog(company.getCompanyId(),new BigDecimal(0),17,redisMoney,remark);
+                                    asyncRecordBalanceLog(company.getCompanyId(),new BigDecimal(0),17,redisMoney,remark, null);
                                 }
                             }
                             return null;
@@ -1500,7 +1682,7 @@ public class CompanyServiceImpl implements ICompanyService
                 redisCache.setCacheObject(companyMoneyKey, newMoney.toString());
 
                 // 异步登记余额添加日志
-                asyncRecordBalanceLog(companyId,money,16,newMoney,"红包充值(负数为扣款)");
+                asyncRecordBalanceLog(companyId,money,16,newMoney,"红包充值(负数为扣款)", null);
 
             } else {
                 logger.error("获取redis锁失败,异常请求参数companyId:{},money:{},type:{}",companyId,money, type);
@@ -1526,15 +1708,17 @@ public class CompanyServiceImpl implements ICompanyService
 
     /**
      * 异步登记余额添加日志  xgb
+     *
      * @param companyId 公司ID
-     * @param money 变更金额
-     * @param balance 当前余额
-     * @param remark 备注信息
-     * @param logType 16-红包余额充值 15-红包余额扣除 17-同步公司余额
+     * @param money     变更金额
+     * @param logType   16-红包余额充值 15-红包余额扣除 17-同步公司余额
+     * @param balance   当前余额
+     * @param remark    备注信息
+     * @param logId
      */
     @Async
     @Override
-    public void asyncRecordBalanceLog(Long companyId, BigDecimal money,Integer logType, BigDecimal balance, String remark) {
+    public void asyncRecordBalanceLog(Long companyId, BigDecimal money, Integer logType, BigDecimal balance, String remark, Long logId) {
         try {
             CompanyRedPacketBalanceLogs log = new CompanyRedPacketBalanceLogs();
             log.setCompanyId(companyId);
@@ -1543,6 +1727,7 @@ public class CompanyServiceImpl implements ICompanyService
             log.setLogsType(logType); // 同步余额
             log.setBalance(balance);
             log.setCreateTime(new Date());
+            log.setRedPacketId(logId);
             companyRedPacketBalanceLogsMapper.insertCompanyRedPacketBalanceLogs(log);
         } catch (Exception e) {
             logger.error("异步登记红包余额日志失败 - 公司ID: {}, 金额: {}, 余额: {}, 备注: {}",
@@ -1573,7 +1758,7 @@ public class CompanyServiceImpl implements ICompanyService
                     // 实际不发生交易只是从缓存获取当天余额报错25小时 交易金额登记为0,备注清楚同步的金额
                     String remark = "时间:" + time +",当前公司余额,金额: " + moneyStr;
                     BigDecimal money = new BigDecimal(moneyStr);
-                    asyncRecordBalanceLog(company.getCompanyId(),new BigDecimal(0),18,money,remark);
+                    asyncRecordBalanceLog(company.getCompanyId(),new BigDecimal(0),18,money,remark, null);
                 }
                 return null;
             });
@@ -1597,18 +1782,121 @@ public class CompanyServiceImpl implements ICompanyService
      * @Author xgb
      * @Date 2025/11/7 9:53
      */
+//    @Override
+//    public void rollbackRedPacketMoney() {
+//        List<RedPacketMoneyVO> redPacketMoneyVOS = fsCourseRedPacketLogMapper.selectFsCourseAddRedPacketLogByCompany();
+//        for(RedPacketMoneyVO company:redPacketMoneyVOS){
+//            logger.info("红包余额回滚开始:{}",company);
+//        }
+//        Optional.ofNullable(redPacketMoneyVOS).ifPresent(list -> list.forEach(company -> {
+//
+//            if(company.getCompanyId()==null){
+//                logger.error("红包记录表中存在公司id为null的异常数据");
+//                return;
+//            }
+//            String companyMoneyKey = FsConstants.COMPANY_MONEY_KEY + company.getCompanyId();
+//            // 加锁,与看课发放红包的加锁保持一致
+//            RLock lock = redissonClient.getLock(FsConstants.COMPANY_MONEY_LOCK + company.getCompanyId());
+//            boolean lockAcquired = false;
+//            try {
+//                lockAcquired = lock.tryLock(3, 10, TimeUnit.SECONDS);
+//                if (lockAcquired) {
+//                    BigDecimal redisMoney;
+//                    // 获取当前余额
+//                    String moneyStr = redisCache.getCacheObject(companyMoneyKey);
+//                    if (StringUtils.isNotEmpty(moneyStr)) {
+//                        redisMoney = new BigDecimal(moneyStr);
+//                    }else {
+//                        logger.error("缓存公司id:{}的余额不存在,回滚金额{}",company.getCompanyId(),company.getMoney());
+//                        return;
+//                    }
+//                    BigDecimal newMoney = redisMoney.add(company.getMoney());
+//                    redisCache.setCacheObject(companyMoneyKey, newMoney.toString());
+//
+//                    String remark = "执行时间:"+DateUtils.getTime()+",T2天客户未领取红包退回,金额: " + company.getMoney();
+//                    asyncRecordBalanceLog(company.getCompanyId(),company.getMoney(),16,newMoney,remark, null);
+//                }
+//            } catch (Exception e) {
+//                logger.error("退回的红包同步增加到缓存和数据表,参数错误,请求异常,异常信息:{}", e.getMessage(), e);
+//            } finally {
+//                if (lockAcquired && lock.isHeldByCurrentThread()) {
+//                    try {
+//                        lock.unlock();
+//                    } catch (IllegalMonitorStateException e) {
+//                        logger.warn("尝试释放非当前线程持有的锁: companyId:{}", company.getCompanyId());
+//                    }
+//                }
+//            }
+//        }));
+//    }
+
+
+    /**
+     * @Description: 红包余额回滚(回滚的是客户没领取的红包),红包记录表中,两天没领取的记录不会再发送
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/12/25 9:32
+     */
     @Override
-    public void rollbackRedPacketMoney() {
-        List<RedPacketMoneyVO> redPacketMoneyVOS = fsCourseRedPacketLogMapper.selectFsCourseAddRedPacketLogByCompany();
-        for(RedPacketMoneyVO company:redPacketMoneyVOS){
-            logger.info("红包余额回滚开始:{}",company);
-        }
-        Optional.ofNullable(redPacketMoneyVOS).ifPresent(list -> list.forEach(company -> {
+    public void rollbackRedPacketMoney(String createSTime, String createETime) {
+        // 回滚前查询一下红包记录
+        List<CompanyRedPacketBalanceLogs> companyRedPacketBalanceLogsList = companyRedPacketBalanceLogsMapper.selectCompanyRedPacketBalanceLogsListByStatus(createSTime, createETime);
+
+        Optional.ofNullable(companyRedPacketBalanceLogsList).ifPresent(list -> list.forEach(company -> {
+
+            if(company.getRedPacketId()==null){// 无数据跳过
+                logger.info("红包记录未登记,流水{}",company.getLogsId());
+                return;
+            }
+
+            // 查询红包记录
+            FsCourseRedPacketLog redLogs = fsCourseRedPacketLogMapper.selectFsCourseRedPacketLogByLogId(company.getRedPacketId());
+            if(redLogs==null){
+                logger.error("未查询到红包记录,流水{}",company.getLogsId());
+                return;
+            }
 
             if(company.getCompanyId()==null){
-                logger.error("红包记录表中存在公司id为null的异常数据");
+                logger.error("红包记录表中存在公司id为null的异常数据,流水{}",company.getLogsId());
                 return;
             }
+
+            if(!StringUtils.isEmpty(redLogs.getBatchId())){
+                R result=checkMchTransferStatusByBatchID(redLogs.getBatchId(),redLogs.getCompanyId());
+                if("200".equals(result.get("code"))){
+                    FsCourseRedPacketLog update = new FsCourseRedPacketLog();
+                    update.setUpdateTime(new Date());
+                    update.setLogId(redLogs.getLogId());
+
+                    // 更新扣减状态
+                    CompanyRedPacketBalanceLogs redBalanceLogs = new CompanyRedPacketBalanceLogs();
+                    redBalanceLogs.setRedPacketId(company.getRedPacketId());
+
+                    if("success".equals(result.get("status"))){
+                        update.setStatus(1);
+                        fsCourseRedPacketLogMapper.updateFsCourseRedPacketLog(update);
+
+                        redBalanceLogs.setStatus(1L);
+                        companyRedPacketBalanceLogsMapper.updateCompanyRedPacketBalanceLogsByRedPacketId(redBalanceLogs);
+                        return;
+                    }else if("fail".equals(result.get("status"))){// 只对失败的部分进行回滚
+                        // 更新支付状态
+                        update.setStatus(2); // 已退回
+                        fsCourseRedPacketLogMapper.updateFsCourseRedPacketLog(update);
+
+                        redBalanceLogs.setStatus(2L);
+                        companyRedPacketBalanceLogsMapper.updateCompanyRedPacketBalanceLogsByRedPacketId(redBalanceLogs);
+                    }
+                }else {
+                    logger.info("商户转账状态查询失败,流水{}",company.getLogsId());
+                    return;
+                }
+            }else {
+                logger.error("红包记录表中存在商户批次号为null的异常数据,流水{}",company.getLogsId());
+                return;
+            }
+
             String companyMoneyKey = FsConstants.COMPANY_MONEY_KEY + company.getCompanyId();
             // 加锁,与看课发放红包的加锁保持一致
             RLock lock = redissonClient.getLock(FsConstants.COMPANY_MONEY_LOCK + company.getCompanyId());
@@ -1622,14 +1910,14 @@ public class CompanyServiceImpl implements ICompanyService
                     if (StringUtils.isNotEmpty(moneyStr)) {
                         redisMoney = new BigDecimal(moneyStr);
                     }else {
-                        logger.error("缓存公司id:{}的余额不存在,回滚金额{}",company.getCompanyId(),company.getMoney());
+                        logger.error("缓存公司id:{}的余额不存在,回滚金额{}",company.getCompanyId(),redLogs.getAmount());
                         return;
                     }
-                    BigDecimal newMoney = redisMoney.add(company.getMoney());
+                    BigDecimal newMoney = redisMoney.add(redLogs.getAmount());
                     redisCache.setCacheObject(companyMoneyKey, newMoney.toString());
 
-                    String remark = "执行时间:"+DateUtils.getTime()+",T2天客户未领取红包退回,金额: " + company.getMoney();
-                    asyncRecordBalanceLog(company.getCompanyId(),company.getMoney(),16,newMoney,remark);
+                    String remark = "执行时间:"+DateUtils.getTime()+",T2天客户未领取红包退回,金额: " + redLogs.getAmount();
+                    asyncRecordBalanceLog(company.getCompanyId(),redLogs.getAmount(),19,newMoney,remark, redLogs.getLogId());
                 }
             } catch (Exception e) {
                 logger.error("退回的红包同步增加到缓存和数据表,参数错误,请求异常,异常信息:{}", e.getMessage(), e);

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

@@ -122,4 +122,6 @@ public class FsUserCoursePeriod
 
     // 控制休息提示是否打开要暂停  0-关闭 1-打开 null-默认打开
     private Integer IsOpenRestReminder;
+    //  控制休息提示是否打开要暂停  0-关闭 1-打开 Json串 key值为companyId
+    private String isOpenRestFlag;
 }

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

@@ -131,7 +131,7 @@ public interface FsCourseAnswerLogsMapper
     int selectErrorCountByCourseVideo(@Param("videoId") Long videoId, @Param("userId") Long userId, @Param("qwUserId") String qwUserId,@Param("project") Long project);
 
     Long selectRedStatus(@Param("userId") Long userId, @Param("videoId") Long videoId, @Param("periodId") Long periodId);
-
+    Integer selectRedStatus2(@Param("userId") Long userId, @Param("videoId") Long videoId, @Param("periodId") Long periodId);
     List<FsCourseAnswerLogsListVO> selectFsCourseAnswerLogsListVONew(FsCourseAnswerLogsParam param);
 
     Long selectFsCourseAnswerLogsListVONewCount(FsCourseAnswerLogsParam param);

+ 13 - 6
fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoMapper.java

@@ -3,10 +3,7 @@ package com.fs.course.mapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.course.domain.FsUserCourseVideo;
 import com.fs.course.domain.FsVideoResource;
-import com.fs.course.param.CourseVideoUpdates;
-import com.fs.course.param.FsCourseListBySidebarParam;
-import com.fs.course.param.FsUserCourseVideoListUParam;
-import com.fs.course.param.FsUserCourseVideoParam;
+import com.fs.course.param.*;
 import com.fs.course.param.newfs.UserCourseVideoPageParam;
 import com.fs.course.vo.*;
 import com.fs.course.vo.newfs.FsUserCourseVideoPageListVO;
@@ -273,13 +270,23 @@ public interface FsUserCourseVideoMapper extends BaseMapper<FsUserCourseVideo> {
     @MapKey("videoId")
     Map<Long, FsUserCourseVideo> selectAllMap();
 
-    @Select("select * from fs_video_resource where line2 is not null and (job_id is null or job_id='')")
+    @Select("select * from fs_video_resource where video_url is not null and line2 is not null and (job_id is null or job_id='')")
     List<FsVideoResource> selectVideoByHuaWei();
 
-    @Select("select * from fs_video_resource where job_id is not null and  (hsy_vid is null or hsy_vid='')")
+    @Select("select * from fs_video_resource where video_url is not null and job_id is not null and  (hsy_vid is null or hsy_vid='')")
     List<FsVideoResource> selectVideoByJobId();
 
 
     @Select("select * from fs_video_resource where hsy_vid is not null")
     List<FsVideoResource> selectVideoByVid();
+
+    /**
+     * 下架
+     */
+    int batchDown(String[] videoIds);
+
+    /**
+     * 批量修改视频封面
+     */
+    int batchEditCover(BatchEditCoverParam param);
 }

Vissa filer visades inte eftersom för många filer har ändrats