Просмотр исходного кода

Merge branch 'master_exclusive_shop_20250718' of http://1.14.104.71:10880/root/rt_fhyx_java into master_exclusive_shop_20250718

15376779826 1 месяц назад
Родитель
Сommit
ca695bb08d
100 измененных файлов с 6126 добавлено и 496 удалено
  1. 36 7
      deploy.sh
  2. 6 0
      fs-admin/pom.xml
  3. 7 2
      fs-admin/src/main/java/com/fs/company/controller/CompanyProfitController.java
  4. 1 1
      fs-admin/src/main/java/com/fs/core/datasource/DynamicDataSourceContextHolder.java
  5. 62 32
      fs-admin/src/main/java/com/fs/core/interceptor/impl/SameUrlDataInterceptor.java
  6. 2 1
      fs-admin/src/main/java/com/fs/core/web/service/UserDetailsServiceImpl.java
  7. 22 2
      fs-admin/src/main/java/com/fs/live/controller/LiveController.java
  8. 106 5
      fs-admin/src/main/java/com/fs/live/controller/LiveDataController.java
  9. 30 0
      fs-admin/src/main/java/com/fs/live/controller/LiveFeedbackLogController.java
  10. 58 0
      fs-admin/src/main/java/com/fs/live/controller/LiveFeedbackTypeController.java
  11. 10 1
      fs-admin/src/main/java/com/fs/live/controller/LiveMsgController.java
  12. 99 7
      fs-admin/src/main/java/com/fs/live/controller/LiveOrderController.java
  13. 17 6
      fs-admin/src/main/java/com/fs/live/controller/LiveOrderPaymentController.java
  14. 48 0
      fs-admin/src/main/java/com/fs/live/controller/LiveVideoController.java
  15. 2 2
      fs-admin/src/main/java/com/fs/quartz/config/ScheduleConfig.java
  16. 11 5
      fs-admin/src/main/java/com/fs/store/controller/FsStoreHealthOrderController.java
  17. 10 6
      fs-admin/src/main/java/com/fs/store/controller/FsStoreOrderController.java
  18. 36 3
      fs-admin/src/main/java/com/fs/store/controller/FsStoreProductPackageController.java
  19. 1 0
      fs-admin/src/main/java/com/fs/store/controller/FsUserAddressController.java
  20. 151 10
      fs-admin/src/main/java/com/fs/task/LiveTask.java
  21. 228 47
      fs-admin/src/main/java/com/fs/task/StoreTask.java
  22. 11 0
      fs-admin/src/main/resources/application-dev.yml
  23. 1 1
      fs-admin/src/main/resources/application.yml
  24. 1 1
      fs-admin/src/test/java/com/fs/store/controller/FsStorePaymentControllerTest.java
  25. 1 1
      fs-api/src/main/java/com/fs/core/datasource/DynamicDataSourceContextHolder.java
  26. 42 0
      fs-common/src/main/java/com/fs/common/annotation/RateLimiter.java
  27. 5 0
      fs-common/src/main/java/com/fs/common/constant/Constants.java
  28. 7 0
      fs-common/src/main/java/com/fs/common/constant/LiveKeysConstant.java
  29. 17 0
      fs-common/src/main/java/com/fs/common/constant/RedisConstant.java
  30. 4 0
      fs-common/src/main/java/com/fs/common/core/domain/R.java
  31. 15 0
      fs-common/src/main/java/com/fs/common/core/redis/RedisCache.java
  32. 236 0
      fs-common/src/main/java/com/fs/common/core/redis/RedisCacheT.java
  33. 37 4
      fs-common/src/main/java/com/fs/common/core/redis/RedisUtil.java
  34. 139 0
      fs-common/src/main/java/com/fs/common/core/redis/service/StockDeductService.java
  35. 20 0
      fs-common/src/main/java/com/fs/common/enums/LimitType.java
  36. 73 0
      fs-common/src/main/java/com/fs/common/exception/ServiceException.java
  37. 237 5
      fs-common/src/main/java/com/fs/common/utils/PhoneUtils.java
  38. 10 0
      fs-common/src/main/java/com/fs/common/vo/LiveVo.java
  39. 123 0
      fs-company-app/src/main/java/com/fs/app/controller/StoreProductPackageController.java
  40. 1 1
      fs-company-app/src/main/java/com/fs/core/datasource/DynamicDataSourceContextHolder.java
  41. 4 0
      fs-company/src/main/java/com/fs/company/controller/live/LiveAfterSalesController.java
  42. 27 4
      fs-company/src/main/java/com/fs/company/controller/live/LiveController.java
  43. 105 0
      fs-company/src/main/java/com/fs/company/controller/live/LiveDataController.java
  44. 8 1
      fs-company/src/main/java/com/fs/company/controller/live/LiveMsgController.java
  45. 28 11
      fs-company/src/main/java/com/fs/company/controller/live/LiveOrderController.java
  46. 1 1
      fs-company/src/main/java/com/fs/core/datasource/DynamicDataSourceContextHolder.java
  47. 15 11
      fs-company/src/main/java/com/fs/store/controller/FsStoreOrderController.java
  48. 10 0
      fs-company/src/main/resources/application-dev.yml
  49. 184 0
      fs-live-mq/pom.xml
  50. 30 0
      fs-live-mq/src/main/java/com/fs/LiveSocketMqApplication.java
  51. 90 0
      fs-live-mq/src/main/java/com/fs/core/aspectj/LiveWatchUserAspect.java
  52. 58 0
      fs-live-mq/src/main/java/com/fs/core/aspectj/lock/DistributeLock.java
  53. 113 0
      fs-live-mq/src/main/java/com/fs/core/aspectj/lock/DistributeLockAspect.java
  54. 13 0
      fs-live-mq/src/main/java/com/fs/core/aspectj/lock/DistributeLockConstant.java
  55. 24 0
      fs-live-mq/src/main/java/com/fs/core/aspectj/lock/DistributeLockException.java
  56. 31 0
      fs-live-mq/src/main/java/com/fs/core/config/ApplicationConfig.java
  57. 123 0
      fs-live-mq/src/main/java/com/fs/core/config/DruidConfig.java
  58. 72 0
      fs-live-mq/src/main/java/com/fs/core/config/FastJson2JsonRedisSerializer.java
  59. 61 0
      fs-live-mq/src/main/java/com/fs/core/config/FilterConfig.java
  60. 109 0
      fs-live-mq/src/main/java/com/fs/core/config/MyBatisConfig.java
  61. 91 0
      fs-live-mq/src/main/java/com/fs/core/config/RedisConfig.java
  62. 66 0
      fs-live-mq/src/main/java/com/fs/core/config/ResourcesConfig.java
  63. 41 0
      fs-live-mq/src/main/java/com/fs/core/config/SecurityConfig.java
  64. 33 0
      fs-live-mq/src/main/java/com/fs/core/config/ServerConfig.java
  65. 124 0
      fs-live-mq/src/main/java/com/fs/core/config/SwaggerConfig.java
  66. 63 0
      fs-live-mq/src/main/java/com/fs/core/config/ThreadPoolConfig.java
  67. 77 0
      fs-live-mq/src/main/java/com/fs/core/config/properties/DruidProperties.java
  68. 27 0
      fs-live-mq/src/main/java/com/fs/core/datasource/DynamicDataSource.java
  69. 45 0
      fs-live-mq/src/main/java/com/fs/core/datasource/DynamicDataSourceContextHolder.java
  70. 56 0
      fs-live-mq/src/main/java/com/fs/core/interceptor/RepeatSubmitInterceptor.java
  71. 126 0
      fs-live-mq/src/main/java/com/fs/core/interceptor/impl/SameUrlDataInterceptor.java
  72. 53 0
      fs-live-mq/src/main/java/com/fs/core/security/SecurityUtils.java
  73. 45 0
      fs-live-mq/src/main/java/com/fs/mq/RocketMQConsumerService.java
  74. 1 0
      fs-live-mq/src/main/resources/META-INF/spring-devtools.properties
  75. 90 0
      fs-live-mq/src/main/resources/application-dev.yml
  76. 79 0
      fs-live-mq/src/main/resources/application-druid-test.yml
  77. 78 0
      fs-live-mq/src/main/resources/application-druid.yml
  78. 122 0
      fs-live-mq/src/main/resources/application.yml
  79. 2 0
      fs-live-mq/src/main/resources/banner.txt
  80. BIN
      fs-live-mq/src/main/resources/fx.jpg
  81. 36 0
      fs-live-mq/src/main/resources/i18n/messages.properties
  82. 15 0
      fs-live-mq/src/main/resources/mybatis/mybatis-config.xml
  83. BIN
      fs-live-mq/src/main/resources/qr.jpg
  84. BIN
      fs-live-mq/src/main/resources/simsunb.ttf
  85. 1 0
      fs-live-mq/src/main/resources/static/S8Zw463cFc.txt
  86. 21 0
      fs-live-mq/src/main/resources/templates/privacyPolicy.html
  87. 21 0
      fs-live-mq/src/main/resources/templates/userAgreement.html
  88. 61 0
      fs-live-mq/src/test/java/com/fs/core/security/BaseSpringBootTest.java
  89. 74 0
      fs-live-socket/src/main/java/com/fs/core/aspectj/DataSourceAspect.java
  90. 90 91
      fs-live-socket/src/main/java/com/fs/core/aspectj/LiveWatchUserAspect.java
  91. 15 0
      fs-live-socket/src/main/java/com/fs/core/config/RedisConfig.java
  92. 1 1
      fs-live-socket/src/main/java/com/fs/core/datasource/DynamicDataSourceContextHolder.java
  93. 1 1
      fs-live-socket/src/main/java/com/fs/live/controller/LiveController.java
  94. 330 62
      fs-live-socket/src/main/java/com/fs/live/task/Task.java
  95. 21 17
      fs-live-socket/src/main/java/com/fs/live/websocket/auth/WebSocketConfigurator.java
  96. 2 0
      fs-live-socket/src/main/java/com/fs/live/websocket/bean/SendMsgVo.java
  97. 8 14
      fs-live-socket/src/main/java/com/fs/live/websocket/handle/LiveChatHandler.java
  98. 1156 118
      fs-live-socket/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  99. 13 2
      fs-live-socket/src/main/resources/application-dev.yml
  100. 12 12
      fs-live-socket/src/main/resources/application.yml

+ 36 - 7
deploy.sh

@@ -5,7 +5,8 @@ declare -A SERVER_CONFIG=(
     # 服务名:IP地址
     ["fs-admin"]="162.14.71.71"
     ["fs-company"]="162.14.71.71"
-    ["fs-user-app"]="129.28.111.46"
+    ["fs-live-mq"]="162.14.71.71"
+#    ["fs-user-app"]="129.28.111.46"
     ["fs-api"]="139.155.112.25"
     ["fs-live-socket"]="118.24.135.139"
     ["fs-sync"]="139.155.112.25"
@@ -18,6 +19,7 @@ REMOTE_BASE_DIR="/home/software"
 # 本地 JAR 包路径
 LOCAL_FS_ADMIN_JAR="./fs-admin/target/fs-admin.jar"
 LOCAL_FS_COMPANY_JAR="./fs-company/target/fs-company.jar"
+LOCAL_FS_LIVE_MQ_JAR="./fs-live-mq/target/fs-live-mq.jar"
 LOCAL_FS_USER_APP_JAR="./fs-user-app/target/fs-user-app.jar"
 LOCAL_FS_API_APP_JAR="./fs-api/target/fs-api.jar"
 LOCAL_FS_LIVE_SOCKET_JAR="./fs-live-socket/target/fs-live-socket.jar"
@@ -110,7 +112,31 @@ deploy_jar() {
 
     echo ""
 }
-
+#         nohup java \
+#          -Xms40G \
+#          -Xmx40G \
+#          -Xss256k \
+#          -XX:+UseG1GC \
+#          -XX:G1HeapRegionSize=32M \
+#          -XX:InitiatingHeapOccupancyPercent=40 \
+#          -XX:MaxGCPauseMillis=200 \
+#          -XX:NewRatio=1 \
+#          -XX:SurvivorRatio=8 \
+#          -XX:MetaspaceSize=512m \
+#          -XX:MaxMetaspaceSize=1024m \
+#          -XX:+DoEscapeAnalysis \
+#          -XX:+EliminateAllocations \
+#          -XX:+UseStringDeduplication \
+#          -XX:+UseCompressedOops \
+#          -XX:+DisableExplicitGC \
+#          -XX:+ExplicitGCInvokesConcurrent \
+#          -XX:+HeapDumpOnOutOfMemoryError \
+#          -XX:HeapDumpPath=/data/dump/ \
+#          -Dfile.encoding=UTF-8 \
+#          -Djava.net.preferIPv4Stack=true \
+#          -Dsun.net.client.defaultConnectTimeout=30000 \
+#          -Dsun.net.client.defaultReadTimeout=30000 \
+#          -jar $app_name.jar \
 # 主要部署流程
 echo "开始多服务器分布式部署..."
 echo "部署配置:"
@@ -123,19 +149,22 @@ echo ""
 deploy_jar "$LOCAL_FS_ADMIN_JAR" "fs-admin" "fs-admin"
 
 # 部署 fs-company
-deploy_jar "$LOCAL_FS_COMPANY_JAR" "fs-company" "fs-company"
+#deploy_jar "$LOCAL_FS_COMPANY_JAR" "fs-company" "fs-company"
+
+# 部署 fs-live-mq
+#deploy_jar "$LOCAL_FS_COMPANY_JAR" "fs-live-mq" "fs-live-mq"
 
 # 部署 fs-user-app
-deploy_jar "$LOCAL_FS_USER_APP_JAR" "fs-user-app" "fs-user-app"
+#deploy_jar "$LOCAL_FS_USER_APP_JAR" "fs-user-app" "fs-user-app"
 
 # 部署 fs-api
-deploy_jar "$LOCAL_FS_API_APP_JAR" "fs-api" "fs-api"
+#deploy_jar "$LOCAL_FS_API_APP_JAR" "fs-api" "fs-api"
 
 # 部署 fs-live-socket
-deploy_jar "$LOCAL_FS_LIVE_SOCKET_JAR" "fs-live-socket" "fs-live-socket"
+#deploy_jar "$LOCAL_FS_LIVE_SOCKET_JAR" "fs-live-socket" "fs-live-socket"
 
 # 部署 fs-sync (注意:这里使用了不同的JAR命名方式)
-deploy_jar "$LOCAL_FS_SYNC_APP_JAR" "fs-sync" "fs-sync"
+#deploy_jar "$LOCAL_FS_SYNC_APP_JAR" "fs-sync" "fs-sync"
 
 echo "========================================"
 echo "分布式部署完成!"

+ 6 - 0
fs-admin/pom.xml

@@ -144,6 +144,12 @@
             <scope>test</scope>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.rocketmq</groupId>
+            <artifactId>rocketmq-spring-boot-starter</artifactId>
+            <version>2.2.3</version>
+        </dependency>
+
     </dependencies>
 
     <build>

+ 7 - 2
fs-admin/src/main/java/com/fs/company/controller/CompanyProfitController.java

@@ -193,14 +193,19 @@ public class CompanyProfitController extends BaseController
         Company company=companyService.selectCompanyByIdForUpdate(profit.getCompanyId());
         if(param.getStatus()==1){
             profit.setRemark(param.getRemark());
-            profit.setProfitStatus(3);
+            // 财务审核通过后直接设置为已完成(状态4),跳过待支付步骤
+            profit.setProfitStatus(4);
             profit.setUpdateTime(new Date());
+            // 如果传入了凭证,保存凭证
+            if(param.getImgUrl() != null && !param.getImgUrl().isEmpty()){
+                profit.setImgUrl(param.getImgUrl());
+            }
             companyProfitService.updateCompanyProfit(profit);
             CompanyProfitLogs logs=new CompanyProfitLogs();
             logs.setProfitId(profit.getProfitId());
             logs.setReason(param.getRemark());
             logs.setCreateTime(new Date());
-            logs.setTitle(loginUser.getUser().getNickName()+"审核通过");
+            logs.setTitle(loginUser.getUser().getNickName()+"财务审核通过");
             logsService.insertCompanyProfitLogs(logs);
         }
         else if(param.getStatus()==0){

+ 1 - 1
fs-admin/src/main/java/com/fs/core/datasource/DynamicDataSourceContextHolder.java

@@ -23,7 +23,7 @@ public class DynamicDataSourceContextHolder
      */
     public static void setDataSourceType(String dsType)
     {
-        log.info("切换到{}数据源", dsType);
+        
         CONTEXT_HOLDER.set(dsType);
     }
 

+ 62 - 32
fs-admin/src/main/java/com/fs/core/interceptor/impl/SameUrlDataInterceptor.java

@@ -1,18 +1,15 @@
 package com.fs.core.interceptor.impl;
 
-import java.util.Arrays;
-import java.util.List;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
 import javax.servlet.http.HttpServletRequest;
-
-import com.alibaba.fastjson.JSON;
-import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.beans.factory.annotation.Value;
-import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.data.redis.core.script.DefaultRedisScript;
 import org.springframework.stereotype.Component;
+import com.alibaba.fastjson.JSONObject;
 import com.fs.common.constant.Constants;
+import com.fs.common.core.redis.RedisCache;
 import com.fs.common.filter.RepeatedlyRequestWrapper;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.http.HttpHelper;
@@ -21,21 +18,21 @@ import com.fs.core.interceptor.RepeatSubmitInterceptor;
 /**
  * 判断请求url和数据是否和上一次相同,
  * 如果和上次相同,则是重复提交表单。 有效时间为10秒内。
+ *
  */
-@Slf4j
 @Component
 public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
 {
+    public final String REPEAT_PARAMS = "repeatParams";
+
+    public final String REPEAT_TIME = "repeatTime";
 
     // 令牌自定义标识
     @Value("${token.header}")
     private String header;
-    @Autowired
-    private RedisTemplate redisTemplate;
 
     @Autowired
-    @Qualifier("prevent-resubmit")
-    private DefaultRedisScript<Boolean> preventResubmitScript;
+    private RedisCache redisCache;
 
     /**
      * 间隔时间,单位:秒 默认10秒
@@ -44,16 +41,15 @@ public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
      */
     private int intervalTime = 10;
 
+    public void setIntervalTime(int intervalTime)
+    {
+        this.intervalTime = intervalTime;
+    }
 
     @SuppressWarnings("unchecked")
     @Override
     public boolean isRepeatSubmit(HttpServletRequest request)
     {
-       return this.isRepeatSubmit(request,this.intervalTime);
-    }
-
-    @Override
-    public boolean isRepeatSubmit(HttpServletRequest request, Integer intervalTime) {
         String nowParams = "";
         if (request instanceof RepeatedlyRequestWrapper)
         {
@@ -64,8 +60,12 @@ public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
         // body参数为空,获取Parameter的数据
         if (StringUtils.isEmpty(nowParams))
         {
-            nowParams = JSON.toJSONString(request.getParameterMap());
+            nowParams = JSONObject.toJSONString(request.getParameterMap());
         }
+        Map<String, Object> nowDataMap = new HashMap<String, Object>();
+        nowDataMap.put(REPEAT_PARAMS, nowParams);
+        nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());
+
         // 请求地址(作为存放cache的key值)
         String url = request.getRequestURI();
 
@@ -78,22 +78,52 @@ public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
 
         // 唯一标识(指定key + 消息头)
         String cache_repeat_key = Constants.REPEAT_SUBMIT_KEY + submitKey;
-        // 当前时间戳
-        long currentTimeMillis = System.currentTimeMillis();
 
-        // 执行Lua脚本
-        List<String> keys = Arrays.asList(cache_repeat_key, url);
+        Object sessionObj = redisCache.getCacheObject(cache_repeat_key);
+        if (sessionObj != null)
+        {
+            Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
+            if (sessionMap.containsKey(url))
+            {
+                Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
+                if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap))
+                {
+                    return true;
+                }
+            }
+        }
+        Map<String, Object> cacheMap = new HashMap<String, Object>();
+        cacheMap.put(url, nowDataMap);
+        redisCache.setCacheObject(cache_repeat_key, cacheMap, intervalTime, TimeUnit.SECONDS);
+        return false;
+    }
 
-        log.info("调用lua参数: {} {} {} {} {}",preventResubmitScript,keys,nowParams,currentTimeMillis,intervalTime);
+    @Override
+    public boolean isRepeatSubmit(HttpServletRequest request, Integer time) {
+        return false;
+    }
 
-        Boolean result = (Boolean) redisTemplate.execute(
-                preventResubmitScript,
-                keys,
-                nowParams,
-                currentTimeMillis,
-                intervalTime
-        );
+    /**
+     * 判断参数是否相同
+     */
+    private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap)
+    {
+        String nowParams = (String) nowMap.get(REPEAT_PARAMS);
+        String preParams = (String) preMap.get(REPEAT_PARAMS);
+        return nowParams.equals(preParams);
+    }
 
-        return result != null && result;
+    /**
+     * 判断两次间隔时间
+     */
+    private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap)
+    {
+        long time1 = (Long) nowMap.get(REPEAT_TIME);
+        long time2 = (Long) preMap.get(REPEAT_TIME);
+        if ((time1 - time2) < (this.intervalTime * 1000))
+        {
+            return true;
+        }
+        return false;
     }
 }

+ 2 - 1
fs-admin/src/main/java/com/fs/core/web/service/UserDetailsServiceImpl.java

@@ -1,6 +1,7 @@
 package com.fs.core.web.service;
 
 import com.fs.core.security.LoginUser;
+import com.fs.core.security.SecurityUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -17,7 +18,7 @@ import com.fs.system.service.ISysUserService;
 /**
  * 用户验证处理
  *
- 
+
  */
 @Service
 public class UserDetailsServiceImpl implements UserDetailsService

+ 22 - 2
fs-admin/src/main/java/com/fs/live/controller/LiveController.java

@@ -64,7 +64,7 @@ public class LiveController extends BaseController {
     @PreAuthorize("@ss.hasPermi('live:live:query')")
     @GetMapping(value = "/{liveId}")
     public AjaxResult getInfo(@PathVariable("liveId") Long liveId) {
-        return AjaxResult.success(liveService.selectLiveByLiveId(liveId));
+        return AjaxResult.success(liveService.selectLiveDbByLiveId(liveId));
     }
 
     /**
@@ -123,7 +123,7 @@ public class LiveController extends BaseController {
     /**
      * 批量删除视频
      */
-    @PreAuthorize("@ss.hasPermi('live:live:edit')")
+    @PreAuthorize("@ss.hasPermi('live:live:remove')")
     @PostMapping("/handleDeleteSelected")
     public R handleDeleteSelected(@RequestBody LiveListVo listVo) {
         return liveService.handleDeleteSelectedAdmin(listVo);
@@ -169,5 +169,25 @@ public class LiveController extends BaseController {
         return liveService.startLive(live);
     }
 
+    /**
+     * 清除直播间缓存
+     */
+    @Log(title = "直播", businessType = BusinessType.UPDATE)
+    @PostMapping("/clearCache/{liveId}")
+    public R clearCache(@PathVariable("liveId") Long liveId) {
+        return liveService.clearLiveCache(liveId);
+    }
+
+    /**
+     * 查询 his 平台大礼品奖励配置(rewardType=6)
+     */
+    @PreAuthorize("@ss.hasPermi('live:live:list')")
+    @GetMapping("/bigGiftReward/list")
+    public TableDataInfo listBigGiftReward(@RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
+                                           @RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
+                                           @RequestParam(value = "name", required = false) String name,
+                                           @RequestParam(value = "status", required = false) Long status) {
+        return liveService.listBigGiftRewards(pageNum, pageSize, name, status);
+    }
 
 }

+ 106 - 5
fs-admin/src/main/java/com/fs/live/controller/LiveDataController.java

@@ -1,22 +1,28 @@
 package com.fs.live.controller;
 
+import com.fs.common.annotation.Log;
+import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.core.security.SecurityUtils;
 import com.fs.live.domain.LiveData;
+import com.fs.live.param.LiveDataParam;
 import com.fs.live.service.ILiveDataService;
+import com.fs.live.vo.LiveDataListVo;
+import com.fs.live.vo.LiveUserDetailExportVO;
+import com.github.pagehelper.PageHelper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
+import javax.servlet.http.HttpServletRequest;
 import java.util.List;
 import java.util.Map;
 
 
 @RestController
-@RequestMapping("/live/liveData")
+@RequestMapping("/liveData/liveData")
 public class LiveDataController {
 
     @Autowired
@@ -77,5 +83,100 @@ public class LiveDataController {
         return R.ok(liveViewData);
     }
 
+    /**
+     * 查询新直播数据列表
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:list')")
+    @PostMapping("/listLiveData")
+    public R listLiveData(@RequestBody LiveDataParam param, HttpServletRequest request)
+    {
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        return liveDataService.listLiveData(param);
+    }
+
+    /**
+     * 导出直播数据列表(与列表查询相同筛选条件)
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:export')")
+    @Log(title = "直播数据列表", businessType = BusinessType.EXPORT)
+    @PostMapping("/exportLiveData")
+    public AjaxResult exportLiveData(@RequestBody LiveDataParam param) {
+        List<LiveDataListVo> list = liveDataService.exportLiveDataList(param);
+        if (list == null || list.isEmpty()) {
+            return AjaxResult.error("没有可导出的数据");
+        }
+        ExcelUtil<LiveDataListVo> util = new ExcelUtil<>(LiveDataListVo.class);
+        return util.exportExcel(list, "直播数据");
+    }
+
+    /**
+     * 查询直播间详情数据(SQL方式)
+     * @param liveId 直播间ID
+     * @return 详情数据
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @GetMapping("/getLiveDataDetailBySql")
+    public R getLiveDataDetailBySql(@RequestParam Long liveId) {
+        return liveDataService.getLiveDataDetailBySql(liveId);
+    }
+
+    /**
+     * 查询直播间用户详情列表(SQL方式)
+     * @param liveId 直播间ID
+     * @param pageNum 页码
+     * @param pageSize 每页数量
+     * @return 用户详情列表
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @GetMapping("/getLiveUserDetailListBySql")
+    public R getLiveUserDetailListBySql(@RequestParam Long liveId,
+                                        @RequestParam(required = false, defaultValue = "1") Integer pageNum,
+                                        @RequestParam(required = false, defaultValue = "10") Integer pageSize) {
+        PageHelper.startPage(pageNum, pageSize);
+        return liveDataService.getLiveUserDetailListBySql(liveId,null,null);
+    }
+
+    /**
+     * 查询直播间详情数据(查询数据服务器处理方式)
+     * @param liveId 直播间ID
+     * @return 详情数据
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @GetMapping("/getLiveDataDetailByServer")
+    public R getLiveDataDetailByServer(@RequestParam Long liveId) {
+        return liveDataService.getLiveDataDetailByServer(liveId);
+    }
+
+    /**
+     * 查询直播间用户详情列表(查询数据服务器处理方式)
+     * @param liveId 直播间ID
+     * @return 用户详情列表
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @GetMapping("/getLiveUserDetailListByServer")
+    public R getLiveUserDetailListByServer(@RequestParam Long liveId) {
+        return liveDataService.getLiveUserDetailListByServer(liveId);
+    }
+
+
+    /**
+     * 导出直播间用户详情数据
+     * @param liveId 直播间ID
+     * @return Excel文件
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:export')")
+    @Log(title = "直播间用户详情", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportLiveUserDetail")
+    public AjaxResult exportLiveUserDetail(@RequestParam Long liveId) {
+        List<LiveUserDetailExportVO> list = liveDataService.exportLiveUserDetail(liveId,null,null);
+        if (list == null || list.isEmpty()) {
+            return AjaxResult.error("未找到用户详情数据");
+        }
+
+        ExcelUtil<LiveUserDetailExportVO> util = new ExcelUtil<>(LiveUserDetailExportVO.class);
+        return util.exportExcel(list, "直播间用户详情数据");
+    }
+
+
 
 }

+ 30 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveFeedbackLogController.java

@@ -0,0 +1,30 @@
+package com.fs.live.controller;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.liveStream.param.LiveFeedbackLogParam;
+import com.fs.liveStream.service.ILiveFeedbackLogService;
+import com.fs.liveStream.vo.LiveFeedbackLogVO;
+import lombok.AllArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/live/liveFeedbackLog")
+@AllArgsConstructor
+public class LiveFeedbackLogController extends BaseController {
+
+    private final ILiveFeedbackLogService liveFeedbackLogService;
+
+    @PreAuthorize("@ss.hasPermi('live:liveFeedbackLog:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(LiveFeedbackLogParam liveFeedbackLog) {
+        startPage();
+        List<LiveFeedbackLogVO> list = liveFeedbackLogService.selectLiveFeedbackLogList(liveFeedbackLog);
+        return getDataTable(list);
+    }
+}

+ 58 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveFeedbackTypeController.java

@@ -0,0 +1,58 @@
+package com.fs.live.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.annotation.RepeatSubmit;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.liveStream.domain.LiveFeedbackType;
+import com.fs.liveStream.service.ILiveFeedbackTypeService;
+import lombok.AllArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/live/liveFeedbackType")
+@AllArgsConstructor
+public class LiveFeedbackTypeController extends BaseController {
+
+    private final ILiveFeedbackTypeService liveFeedbackTypeService;
+
+    @PreAuthorize("@ss.hasPermi('live:liveFeedbackType:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(LiveFeedbackType liveFeedbackType) {
+        List<LiveFeedbackType> list = liveFeedbackTypeService.selectLiveFeedbackTypeList(liveFeedbackType);
+        return getDataTable(list);
+    }
+
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return AjaxResult.success(liveFeedbackTypeService.selectLiveFeedbackTypeById(id));
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:liveFeedbackType:add')")
+    @Log(title = "直播反馈类型", businessType = BusinessType.INSERT)
+    @PostMapping
+    @RepeatSubmit(intervalTime = 500)
+    public AjaxResult add(@RequestBody LiveFeedbackType liveFeedbackType) {
+        return toAjax(liveFeedbackTypeService.insertLiveFeedbackType(liveFeedbackType));
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:liveFeedbackType:edit')")
+    @Log(title = "直播反馈类型", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody LiveFeedbackType liveFeedbackType) {
+        return toAjax(liveFeedbackTypeService.updateLiveFeedbackType(liveFeedbackType));
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:liveFeedbackType:remove')")
+    @Log(title = "直播反馈类型", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{typeIds}")
+    public AjaxResult remove(@PathVariable Long[] typeIds) {
+        return toAjax(liveFeedbackTypeService.deleteLiveFeedbackTypeByIds(typeIds));
+    }
+
+}

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

@@ -13,6 +13,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.List;
+import java.util.Arrays;
 
 /**
  * 直播讨论Controller
@@ -86,12 +87,20 @@ public class LiveMsgController extends BaseController
 
     /**
      * 删除直播讨论
+     * 只对数据库已有的消息(msgId>0)删库;临时 id(未入库)不删库,由聊天室服务在收到 WebSocket deleteMsg 时从队列移除并广播。
+     * 始终返回成功,以便前端发送 WebSocket deleteMsg 做广播。
      */
 //    @PreAuthorize("@ss.hasPermi('live:liveMsg:remove')")
     @Log(title = "直播讨论", businessType = BusinessType.DELETE)
 	@DeleteMapping("/{msgIds}")
     public AjaxResult remove(@PathVariable Long[] msgIds)
     {
-        return toAjax(liveMsgService.deleteLiveMsgByMsgIds(msgIds));
+        if (msgIds != null && msgIds.length > 0) {
+            Long[] realIds = Arrays.stream(msgIds).filter(id -> id != null && id > 0).toArray(Long[]::new);
+            if (realIds.length > 0) {
+                liveMsgService.deleteLiveMsgByMsgIds(realIds);
+            }
+        }
+        return AjaxResult.success();
     }
 }

+ 99 - 7
fs-admin/src/main/java/com/fs/live/controller/LiveOrderController.java

@@ -37,14 +37,24 @@ import com.fs.live.service.ILiveOrderPaymentService;
 import com.fs.live.service.ILiveOrderService;
 import com.fs.live.vo.LiveGoodsVo;
 import com.fs.live.vo.LiveOrderPaymentVo;
+import com.fs.live.vo.LiveOrderQueryVO;
 import com.fs.live.vo.LiveOrderVo;
+import com.fs.huifuPay.domain.HuiFuQueryOrderResult;
+import com.fs.huifuPay.sdk.opps.core.request.V2TradePaymentScanpayQueryRequest;
+import com.fs.huifuPay.service.HuiFuService;
+import com.fs.pay.pay.domain.OrderResult;
+import com.fs.pay.pay.dto.OrderQueryDTO;
+import com.fs.pay.pay.service.PayService;
 import com.fs.store.domain.FsStoreDelivers;
 import com.fs.store.domain.FsStoreOrder;
+import com.fs.store.domain.FsStorePayment;
 import com.fs.store.domain.FsStoreOrderStatus;
 import com.fs.store.domain.FsUser;
 import com.fs.store.mapper.FsStoreDeliversMapper;
+import com.fs.store.mapper.FsStoreProductMapper;
 import com.fs.store.mapper.FsWarehousesMapper;
 import com.fs.store.param.*;
+import com.fs.store.service.IFsStorePaymentService;
 import com.fs.store.service.IFsExpressService;
 import com.fs.store.service.IFsUserService;
 import com.fs.task.LiveTask;
@@ -60,6 +70,7 @@ import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletRequest;
+import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -97,35 +108,48 @@ public class LiveOrderController extends BaseController
     IErpOrderService erpOrderService;
     @Autowired
     private LiveTask liveTask;
+    @Autowired
+    private IFsStorePaymentService fsStorePaymentService;
+    @Autowired
+    private HuiFuService huiFuService;
+    @Autowired
+    private PayService ybPayService;
 
     /**
      * 查询订单列表
      */
     @PreAuthorize("@ss.hasPermi('live:liveOrder:list')")
     @GetMapping("/list")
-    public TableDataInfo list(LiveOrder liveOrder)
+    public TableDataInfo list(LiveOrderQueryVO liveOrder)
     {
         startPage();
         List<LiveOrder> list = liveOrderService.selectLiveOrderList(liveOrder);
         for (LiveOrder vo : list){
             vo.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
+            vo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
         }
         return getDataTable(list);
     }
 
 
+
     /**
      * 导出订单列表
      */
     @PreAuthorize("@ss.hasPermi('live:liveOrder:export')")
     @Log(title = "订单", businessType = BusinessType.EXPORT)
     @GetMapping("/export")
-    public AjaxResult export(LiveOrder liveOrder)
+    public AjaxResult export(LiveOrderQueryVO liveOrder)
     {
-        List<LiveOrder> list = liveOrderService.selectLiveOrderList(liveOrder);
+        List<LiveOrder> list = liveOrderService.selectLiveOrderListExport(liveOrder);
+
         for (LiveOrder order : list) {
-            order.setUserPhone(ParseUtils.parsePhone(order.getUserPhone()));
-            order.setUserAddress(ParseUtils.parseAddress(order.getUserAddress()));
+            if(StringUtils.isNotNull(order.getUserPhone())){
+                order.setUserPhone(ParseUtils.parsePhone(order.getUserPhone()));
+            }
+            if(StringUtils.isNotNull(order.getUserAddress())){
+                order.setUserAddress(ParseUtils.parseAddress(order.getUserAddress()));
+            }
         }
         ExcelUtil<LiveOrder> util = new ExcelUtil<LiveOrder>(LiveOrder.class);
         return util.exportExcel(list, "订单数据");
@@ -138,7 +162,10 @@ public class LiveOrderController extends BaseController
     @GetMapping(value = "/info/{orderId}")
     public AjaxResult getInfo(@PathVariable("orderId") String orderId)
     {
-        return AjaxResult.success(liveOrderService.selectLiveOrderByOrderId(orderId));
+        LiveOrder liveOrder = liveOrderService.selectLiveOrderByOrderId(orderId);
+        liveOrder.setUserPhone(ParseUtils.parsePhone(liveOrder.getUserPhone()));
+        liveOrder.setUserAddress(ParseUtils.parseAddress(liveOrder.getUserAddress()));
+        return AjaxResult.success(liveOrder);
     }
 
 
@@ -452,6 +479,71 @@ public class LiveOrderController extends BaseController
         return R.error("未查询到快递信息");
     }
 
-
+    /**
+     * 同步直播订单支付状态
+     * 仿照 FsStorePaymentController.returnPayStatus,仅处理直播订单
+     */
+    @Log(title = "同步直播订单状态", businessType = BusinessType.UPDATE)
+    @PreAuthorize("@ss.hasPermi('store:storePayment:payNotify')")
+    @PostMapping("/syncPayStatus")
+    public R syncPayStatus(@RequestBody FsStorePayment fsStorePayment) throws Exception {
+        LiveOrderPayment payment = orderPaymentService.selectLiveOrderPaymentByOrderCode(fsStorePayment.getTradeNo());
+        if (payment == null) {
+            return R.error("该付款记录没有查询到!");
+        }
+        if (payment.getBusinessType() != null && payment.getBusinessType() == 5) {
+            // 可提前校验为直播订单
+        }
+        String payMode = payment.getPayMode();
+        logger.info("同步直播订单状态 手动查询: {}", payment);
+
+        if ("hf".equals(payMode)) {
+            V2TradePaymentScanpayQueryRequest request = new V2TradePaymentScanpayQueryRequest();
+            request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
+            request.setOrgHfSeqId(payment.getTradeNo());
+            HuiFuQueryOrderResult o;
+            try {
+                o = huiFuService.queryOrder(request);
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+            logger.info("汇付返回: {}", o);
+            if ("00000000".equals(o.getResp_code()) && "S".equals(o.getTrans_stat())) {
+                String[] order = o.getOrg_req_seq_id().split("-");
+                if ("live".equals(order[0])) {
+                    liveOrderService.payConfirm(1, null, order[1], o.getOrg_hf_seq_id(), o.getOut_trans_id(), o.getParty_order_id());
+                    return R.ok();
+                }
+                return R.error("该交易单号不是直播订单,请使用同步订单状态功能");
+            }
+        } else if ("yb".equals(payMode)) {
+            OrderQueryDTO o = new OrderQueryDTO();
+            o.setUpOrderId(payment.getTradeNo());
+            OrderResult orderResult = ybPayService.getOrder(o);
+            if (orderResult != null) {
+                if ("4".equals(orderResult.getState())) {
+                    return R.error("订单未支付");
+                }
+                if ("5".equals(orderResult.getState())) {
+                    return R.error("订单已退款");
+                }
+                if ("0".equals(orderResult.getState())) {
+                    String[] order = orderResult.getLowOrderId().split("-");
+                    if ("100".equals(orderResult.getStatus())) {
+                        if ("live".equals(order[0])) {
+                            liveOrderService.payConfirm(1, null, order[1], o.getUpOrderId(), orderResult.getBankTrxId(), orderResult.getBankOrderId());
+                            return R.ok();
+                        }
+                        return R.error("该交易单号不是直播订单,请使用同步订单状态功能");
+                    }
+                    return R.error("请检查外部订单号");
+                }
+                return R.error("订单支付状态异常,联系财务检查");
+            }
+        } else if ("tzbk".equals(payMode)) {
+            return R.error("台州银行支付暂不支持直播订单同步");
+        }
+        return R.error("请检查外部订单号");
+    }
 
 }

+ 17 - 6
fs-admin/src/main/java/com/fs/live/controller/LiveOrderPaymentController.java

@@ -49,11 +49,16 @@ public class LiveOrderPaymentController extends BaseController
     @GetMapping
     public TableDataInfo listNew(LiveOrderPaymentVo liveOrderPayment)
     {
+        // 处理提交时间范围
+        if (liveOrderPayment.getCreateTimeRange() != null && !liveOrderPayment.getCreateTimeRange().isEmpty()) {
+            String[] timeRange = liveOrderPayment.getCreateTimeRange().split("--");
+            if (timeRange.length == 2) {
+                liveOrderPayment.setCreateBeginTime(timeRange[0]);
+                liveOrderPayment.setCreateEndTime(timeRange[1]);
+            }
+        }
         startPage();
         List<LiveOrderPaymentVo> list = liveOrderPaymentService.selectLiveOrderPaymentVoList(liveOrderPayment);
-        for (LiveOrderPaymentVo vo : list){
-            vo.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
-        }
         return getDataTable(list);
     }
 
@@ -65,11 +70,17 @@ public class LiveOrderPaymentController extends BaseController
     @GetMapping("/export")
     public AjaxResult export(LiveOrderPaymentVo liveOrderPayment)
     {
+        // 处理提交时间范围
+        if (liveOrderPayment.getCreateTimeRange() != null && !liveOrderPayment.getCreateTimeRange().isEmpty()) {
+            String[] timeRange = liveOrderPayment.getCreateTimeRange().split("--");
+            if (timeRange.length == 2) {
+                liveOrderPayment.setCreateBeginTime(timeRange[0]);
+                liveOrderPayment.setCreateEndTime(timeRange[1]);
+            }
+        }
         PageHelper.startPage(1, 10000, "");
         List<LiveOrderPaymentVo> list = liveOrderPaymentService.selectLiveOrderPaymentVoList(liveOrderPayment);
-        for (LiveOrderPaymentVo vo : list){
-            vo.setUserPhone(vo.getUserPhone() == null ? "" : vo.getUserPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
-        }
+
         ExcelUtil<LiveOrderPaymentVo> util = new ExcelUtil<LiveOrderPaymentVo>(LiveOrderPaymentVo.class);
         return util.exportExcel(list, "直播订单支付记录");
     }

+ 48 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveVideoController.java

@@ -150,4 +150,52 @@ public class LiveVideoController extends BaseController
         int result = liveVideoService.batchUpdateCategory(videoIds, category);
         return toAjax(result);
     }
+
+    /**
+     * 更新视频URL并重置转码状态
+     */
+    @Log(title = "更新视频URL", businessType = BusinessType.UPDATE)
+    @PutMapping("/updateVideoUrl")
+    public AjaxResult updateVideoUrl(@RequestBody Map<String, Object> params)
+    {
+        Object videoIdObj = params.get("videoId");
+        Object videoUrlObj = params.get("videoUrl");
+
+        if (videoIdObj == null) {
+            return AjaxResult.error("视频编号不能为空");
+        }
+        if (videoUrlObj == null || videoUrlObj.toString().trim().isEmpty()) {
+            return AjaxResult.error("视频URL不能为空");
+        }
+
+        Long videoId;
+        if (videoIdObj instanceof Number) {
+            videoId = ((Number) videoIdObj).longValue();
+        } else {
+            try {
+                videoId = Long.parseLong(videoIdObj.toString());
+            } catch (NumberFormatException e) {
+                return AjaxResult.error("视频编号格式错误");
+            }
+        }
+
+        String videoUrl = videoUrlObj.toString().trim();
+
+        int result = liveVideoService.updateVideoUrl(videoId, videoUrl);
+        return toAjax(result);
+    }
+
+    /**
+     * 批量新增直播视频
+     */
+    @Log(title = "批量新增直播视频", businessType = BusinessType.INSERT)
+    @PostMapping("/batchAdd")
+    public AjaxResult batchAdd(@RequestBody List<LiveVideo> liveVideoList)
+    {
+        if (liveVideoList == null || liveVideoList.isEmpty()) {
+            return AjaxResult.error("视频列表不能为空");
+        }
+        int result = liveVideoService.batchInsertLiveVideo(liveVideoList);
+        return toAjax(result);
+    }
 }

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

@@ -8,8 +8,8 @@ import java.util.Properties;
 
 /**
  * 定时任务配置
- * 
- 
+ *
+
  */
 @Configuration
 public class ScheduleConfig

+ 11 - 5
fs-admin/src/main/java/com/fs/store/controller/FsStoreHealthOrderController.java

@@ -17,13 +17,16 @@ import com.fs.store.param.FsStoreOrderParam;
 import com.fs.store.service.*;
 import com.fs.store.vo.FsStoreOrderExportVO;
 import com.fs.store.vo.FsStoreOrderItemExportVO;
+import com.fs.store.vo.FsStoreOrderPlainExportVO;
 import com.fs.store.vo.FsStoreOrderVO;
+import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.util.ArrayList;
 import java.util.List;
 
 @RestController
@@ -98,20 +101,23 @@ public class FsStoreHealthOrderController extends BaseController {
         }
         param.setIsHealth("1");
         List<FsStoreOrderExportVO> list = fsStoreOrderService.selectFsStoreOrderListVOByExport(param);
+        List<FsStoreOrderPlainExportVO> exportList = new ArrayList<>();
         //对手机号脱敏
         if (list != null) {
             for (FsStoreOrderExportVO vo : list) {
+                FsStoreOrderPlainExportVO exportVo = new FsStoreOrderPlainExportVO();
+                BeanUtils.copyProperties(vo, exportVo);
                 if (vo.getPhone() != null) {
-                    vo.setPhone(vo.getPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                    exportVo.setPhone(vo.getPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
                 }
                 if (vo.getUserPhone() != null) {
-                    vo.setUserPhone(vo.getUserPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                    exportVo.setUserPhone(vo.getUserPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
                 }
-
+                exportList.add(exportVo);
             }
         }
-        ExcelUtil<FsStoreOrderExportVO> util = new ExcelUtil<FsStoreOrderExportVO>(FsStoreOrderExportVO.class);
-        return util.exportExcel(list, "订单数据");
+        ExcelUtil<FsStoreOrderPlainExportVO> util = new ExcelUtil<>(FsStoreOrderPlainExportVO.class);
+        return util.exportExcel(exportList, "订单数据");
     }
 
 

+ 10 - 6
fs-admin/src/main/java/com/fs/store/controller/FsStoreOrderController.java

@@ -207,22 +207,26 @@ public class FsStoreOrderController extends BaseController {
         }
         param.setNotHealth(1);
         List<FsStoreOrderExportVO> list = fsStoreOrderService.selectFsStoreOrderListVOByExport(param);
+        List<FsStoreOrderPlainExportVO> exportList = new ArrayList<>();
         //对手机号脱敏
         if (list != null) {
             for (FsStoreOrderExportVO vo : list) {
+                FsStoreOrderPlainExportVO exportVo = new FsStoreOrderPlainExportVO();
+                org.springframework.beans.BeanUtils.copyProperties(vo, exportVo);
                 if (vo.getPhone() != null) {
-                    vo.setPhone(vo.getPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                    exportVo.setPhone(vo.getPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
                 }
                 if (vo.getUserPhone() != null) {
-                    vo.setUserPhone(vo.getUserPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                    exportVo.setUserPhone(vo.getUserPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
                 }
                 if (vo.getUserAddress()!=null){
-                    vo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
+                    exportVo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
                 }
+                exportList.add(exportVo);
             }
         }
-        ExcelUtil<FsStoreOrderExportVO> util = new ExcelUtil<FsStoreOrderExportVO>(FsStoreOrderExportVO.class);
-        return util.exportExcel(list, "订单数据");
+        ExcelUtil<FsStoreOrderPlainExportVO> util = new ExcelUtil<>(FsStoreOrderPlainExportVO.class);
+        return util.exportExcel(exportList, "订单数据");
     }
 
 
@@ -375,7 +379,7 @@ public class FsStoreOrderController extends BaseController {
         ExpressInfoDTO expressInfoDTO = null;
         if (StringUtils.isNotEmpty(order.getDeliveryId())) {
             String lastFourNumber = "";
-            if (order.getDeliverySn().equals(ShipperCodeEnum.SF.getValue())) {
+            if (order.getDeliverySn().equals(ShipperCodeEnum.SF.getValue()) || order.getDeliverySn().equals(ShipperCodeEnum.ZTO.getValue())) {
                 lastFourNumber = order.getUserPhone();
                 if (lastFourNumber.length() == 11) {
                     lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);

+ 36 - 3
fs-admin/src/main/java/com/fs/store/controller/FsStoreProductPackageController.java

@@ -1,23 +1,27 @@
 package com.fs.store.controller;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import cn.hutool.json.JSONArray;
 import cn.hutool.json.JSONUtil;
-import com.fs.store.cache.IFsStoreProductCacheService;
 import com.fs.store.cache.impl.IFsStoreProductCacheServiceImpl;
+import com.fs.store.domain.FsShippingTemplates;
 import com.fs.store.domain.FsStoreProduct;
 import com.fs.store.domain.FsStoreProductAttrValue;
 import com.fs.store.dto.StoreOrderProductDTO;
 import com.fs.store.dto.StorePackageProductDTO;
 
+import com.fs.store.param.FsStoreProductPackageCloseSingleLinkModifyParam;
 import com.fs.store.param.FsStoreProductPackageModifyParam;
+import com.fs.store.service.IFsShippingTemplatesService;
 import com.fs.store.service.IFsStoreProductAttrValueService;
 import com.fs.store.service.IFsStoreProductService;
 import com.fs.store.vo.FsStoreProductPacketExportVO;
 import com.fs.store.vo.FsStoreProductPacketVO;
-import com.hc.openapi.tool.util.StringUtils;
+import com.fs.common.utils.StringUtils;
 import org.springframework.beans.BeanUtils;
 
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -56,6 +60,8 @@ public class FsStoreProductPackageController extends BaseController
     @Autowired
     private IFsStoreProductService storeProductService;
     @Autowired
+    private IFsShippingTemplatesService fsShippingTemplatesService;
+    @Autowired
     private IFsStoreProductCacheServiceImpl fsStoreProductCacheService;
 
     /**
@@ -80,6 +86,7 @@ public class FsStoreProductPackageController extends BaseController
     {
         List<FsStoreProductPacketVO> list = fsStoreProductPackageService.selectFsStoreProductPackageListVO(fsStoreProductPackage);
         List<FsStoreProductPacketExportVO> sorList=new ArrayList<>();
+        Map<Long, String> shippingTemplateNameCache = new HashMap<>();
 
         for(FsStoreProductPacketVO productPackage:list){
             //List<StoreOrderProductDTO> productList=new ArrayList<>();
@@ -101,6 +108,16 @@ public class FsStoreProductPackageController extends BaseController
                         packetExportVO.setSku(attrValue.getSku());
                         packetExportVO.setImage(attrValue.getImage());
                         packetExportVO.setProductName(product.getProductName());
+                        if (product.getTempId() != null) {
+                            Long tempId = product.getTempId().longValue();
+                            String templateName = shippingTemplateNameCache.computeIfAbsent(tempId, id -> {
+                                FsShippingTemplates tpl = fsShippingTemplatesService.selectFsShippingTemplatesById(id);
+                                return tpl != null && tpl.getName() != null ? tpl.getName() : "";
+                            });
+                            if (StringUtils.isNotEmpty(templateName)) {
+                                packetExportVO.setShippingTemplateName(templateName);
+                            }
+                        }
                         sorList.add(packetExportVO);
                         //productList.add(productDTO);
 
@@ -140,7 +157,7 @@ public class FsStoreProductPackageController extends BaseController
                     productDTO.setProductName(product.getProductName());
 
                     String warehouseCode = fsStoreProductCacheService.getWarehouseCodeByProductId(attrValue.getProductId());
-                    if(StringUtils.isNotBlank(warehouseCode)){
+                    if(StringUtils.isNotEmpty(warehouseCode)){
                         productDTO.setProdWareHouseCode(warehouseCode);
                     }
                     productList.add(productDTO);
@@ -161,6 +178,22 @@ public class FsStoreProductPackageController extends BaseController
         return toAjax(fsStoreProductPackageService.updateFsStoreProductPackages(param.getPackageIds(),param.getStatus()));
     }
 
+    /**
+     * 批量修改关闭单人链接
+     */
+    @PreAuthorize("@ss.hasPermi('store:storeProductPackage:edit')")
+    @Log(title = "商品组合套餐", businessType = BusinessType.UPDATE)
+    @PostMapping("/modifyCloseSingleLinkMore")
+    public AjaxResult modifyCloseSingleLinkMore(@RequestBody FsStoreProductPackageCloseSingleLinkModifyParam param) {
+        if (param.getPackageIds() == null || param.getPackageIds().length == 0) {
+            return AjaxResult.error("请选择套餐");
+        }
+        if (param.getCloseSingleLink() == null) {
+            return AjaxResult.error("请选择是否关闭单人链接");
+        }
+        return toAjax(fsStoreProductPackageService.updateCloseSingleLinkBatch(param.getPackageIds(), param.getCloseSingleLink()));
+    }
+
     /**
      * 新增商品组合套餐
      */

+ 1 - 0
fs-admin/src/main/java/com/fs/store/controller/FsUserAddressController.java

@@ -105,6 +105,7 @@ public class FsUserAddressController extends BaseController
     @GetMapping("/getAddressList")
     public R getAddressList(FsUserAddress fsUserAddress)
     {
+        logger.error("count:getAddressList");
         fsUserAddress.setIsDel(0);
         List<FsUserAddress> list = fsUserAddressService.selectFsUserAddressList(fsUserAddress);
         return R.ok().put("data", list);

+ 151 - 10
fs-admin/src/main/java/com/fs/task/LiveTask.java

@@ -2,40 +2,47 @@ package com.fs.task;
 
 import com.fs.common.annotation.QuartzRunnable;
 import com.fs.common.core.redis.RedisCache;
-import com.fs.company.mapper.CompanyUserMapper;
 import com.fs.erp.domain.ErpDeliverys;
 import com.fs.erp.domain.ErpOrderQuery;
 import com.fs.erp.dto.ErpOrderQueryRequert;
 import com.fs.erp.dto.ErpOrderQueryResponse;
 import com.fs.erp.service.FsJstAftersalePushService;
-import com.fs.erp.service.FsJstCodPushService;
-import com.fs.erp.service.IErpGoodsService;
 import com.fs.erp.service.IErpOrderService;
 import com.fs.erp.utils.ErpContextHolder;
+import com.fs.express.FsStoreDeliversService;
+import com.fs.common.utils.StringUtils;
+import com.fs.huifuPay.domain.HuiFuQueryOrderResult;
+import com.fs.huifuPay.sdk.opps.core.request.V2TradePaymentScanpayQueryRequest;
+import com.fs.live.domain.LiveOrderPayment;
+import com.fs.live.mapper.LiveOrderPaymentMapper;
+import com.fs.live.vo.LiveOrderPaymentVo;
+import com.fs.pay.pay.domain.OrderResult;
+import com.fs.pay.pay.dto.OrderQueryDTO;
+import com.fs.pay.pay.service.PayService;
+import com.fs.huifuPay.service.HuiFuService;
+import com.fs.live.utils.redis.RedisBatchHandler;
 import com.fs.live.domain.LiveAfterSales;
 import com.fs.live.domain.LiveOrder;
 import com.fs.live.service.*;
 import com.fs.store.domain.FsExpress;
-import com.fs.store.domain.FsStoreAfterSales;
-import com.fs.store.domain.FsStoreOrder;
 import com.fs.store.enums.OrderLogEnum;
 import com.fs.store.mapper.*;
-import com.fs.store.param.FsStoreAfterSalesAudit1Param;
 import com.fs.store.param.LiveAfterSalesAudit1Param;
 import com.fs.store.service.*;
-import com.fs.system.service.ISysConfigService;
-import com.fs.wx.mapper.FsWxExpressTaskMapper;
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang.ObjectUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 
 import java.text.ParseException;
+import java.text.SimpleDateFormat;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import static com.fs.live.utils.redis.RedisBatchHandler.CONSUME_INTERVAL;
 
 /**
  * 定时任务调度
@@ -48,6 +55,12 @@ public class LiveTask {
 
     @Autowired
     private ILiveOrderService liveOrderService;
+    @Autowired
+    private LiveOrderPaymentMapper liveOrderPaymentMapper;
+    @Autowired
+    private HuiFuService huiFuService;
+    @Autowired
+    private PayService ybPayService;
 
     @Autowired
     private ILiveAfterSalesService afterSalesService;
@@ -70,6 +83,75 @@ public class LiveTask {
 
     @Autowired
     public FsJstAftersalePushService fsJstAftersalePushService;
+    @Autowired
+    public RedisBatchHandler redisBatchHandler;
+    @Autowired
+    private IFsUserService fsUserService;
+    @Autowired
+    private RedisCache redisCache;
+
+    private static final String APP_USER_SYNC_TASK_LOCK_KEY = "task:live:app_user_sync:lock";
+    private static final long APP_USER_SYNC_TASK_LOCK_SECONDS = 600L;
+    /** 两个 APP 用户同步任务错峰启动间隔(秒) */
+    private static final long APP_USER_SYNC_TASK_STAGGER_SECONDS = 15L;
+
+    // 订单银行回调数据丢失补偿
+    public void recoveryBankOrder() {
+        // 查询出来最近15分钟的订单 待支付 未退款
+        List<LiveOrder> list = liveOrderService.selectBankOrder();
+        if (CollectionUtils.isEmpty(list)) {
+            return;
+        }
+        for (LiveOrder order : list) {
+            try {
+                LiveOrderPaymentVo payment = liveOrderPaymentMapper.selectLiveOrderPaymentByPaymentIdNew(order.getOrderId());
+
+                    if (payment == null || payment.getStatus() == null || payment.getStatus() != 0) {
+                        continue;
+                    }
+                    if (StringUtils.isEmpty(payment.getTradeNo()) || StringUtils.isEmpty(payment.getPayMode())) {
+                        continue;
+                    }
+                    String payMode = payment.getPayMode();
+                    if ("hf".equals(payMode)) {
+                        V2TradePaymentScanpayQueryRequest request = new V2TradePaymentScanpayQueryRequest();
+                        request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
+                        request.setOrgHfSeqId(payment.getTradeNo());
+                        HuiFuQueryOrderResult o = huiFuService.queryOrder(request);
+                        logger.info("直播订单补偿 汇付查询 orderId={} resp={}", order.getOrderId(), o);
+                        if (o != null && "00000000".equals(o.getResp_code()) && "S".equals(o.getTrans_stat())) {
+                            String[] parts = o.getOrg_req_seq_id().split("-");
+                            if (parts.length >= 2 && "live".equals(parts[0])) {
+                                liveOrderService.payConfirm(1, null, parts[1], o.getOrg_hf_seq_id(), o.getOut_trans_id(), o.getParty_order_id());
+                            }
+                        }
+                    } else if ("yb".equals(payMode)) {
+                        OrderQueryDTO q = new OrderQueryDTO();
+                        q.setUpOrderId(payment.getTradeNo());
+                        OrderResult orderResult = ybPayService.getOrder(q);
+                        if (orderResult == null) {
+                            continue;
+                        }
+                        if ("4".equals(orderResult.getState()) || "5".equals(orderResult.getState())) {
+                            continue;
+                        }
+                        if (!"0".equals(orderResult.getState()) || !"100".equals(orderResult.getStatus())) {
+                            continue;
+                        }
+                        String[] parts = orderResult.getLowOrderId().split("-");
+                        if (parts.length >= 2 && "live".equals(parts[0])) {
+                            liveOrderService.payConfirm(1, null, parts[1], payment.getTradeNo(), orderResult.getBankTrxId(), orderResult.getBankOrderId());
+                        }
+                    } else if ("tzbk".equals(payMode)) {
+                        logger.debug("直播订单补偿跳过台州银行支付 orderId={}", order.getOrderId());
+                    }
+
+
+            } catch (Exception e) {
+                logger.error("直播订单银行补偿失败 orderId={}", order.getOrderId(), e);
+            }
+        }
+    }
 
     /**
      * 超时订单自动取消
@@ -135,6 +217,56 @@ public class LiveTask {
     }
 
 
+    /**
+     * 同步近三天直播订单关联的APP用户数据
+     */
+    @QuartzRunnable(name = "同步APP用户数据")
+    public void syncAppUser() {
+        if (!tryAcquireAppUserSyncLock()) {
+            logger.debug("同步APP用户数据任务跳过,其他任务正在执行");
+            return;
+        }
+        try {
+            fsUserService.syncAppUsersForRecentLiveOrders(3);
+        } catch (Exception e) {
+            logger.error("同步APP用户数据失败", e);
+        } finally {
+            releaseAppUserSyncLock();
+        }
+    }
+
+    /**
+     * 从 Redis 拉取下单时缓存的待同步 APP 用户并同步
+     */
+    @QuartzRunnable(name = "同步APP用户数据(Redis)")
+    public void syncAppUserFromRedis() {
+        try {
+            Thread.sleep(APP_USER_SYNC_TASK_STAGGER_SECONDS * 1000);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            return;
+        }
+        if (!tryAcquireAppUserSyncLock()) {
+            logger.debug("Redis同步APP用户数据任务跳过,其他任务正在执行");
+            return;
+        }
+        try {
+            fsUserService.syncPendingAppUsersFromRedis();
+        } catch (Exception e) {
+            logger.error("Redis同步APP用户数据失败", e);
+        } finally {
+            releaseAppUserSyncLock();
+        }
+    }
+
+    private boolean tryAcquireAppUserSyncLock() {
+        return redisCache.setIfAbsent(APP_USER_SYNC_TASK_LOCK_KEY, "1", APP_USER_SYNC_TASK_LOCK_SECONDS, TimeUnit.SECONDS);
+    }
+
+    private void releaseAppUserSyncLock() {
+        redisCache.deleteObject(APP_USER_SYNC_TASK_LOCK_KEY);
+    }
+
         /**
          * 退款自动处理 24小时未审核自动审核通过 每小时执行一次
          */
@@ -171,7 +303,8 @@ public class LiveTask {
         }
     }
 
-
+    @Autowired
+    FsStoreDeliversService fsStoreDeliverService;
     /**
      * 同步物流状态
      */
@@ -180,6 +313,7 @@ public class LiveTask {
         List<Long> ids = liveOrderService.selectSyncExpressIds();
         for (Long id : ids) {
             liveOrderService.syncExpress(id);
+            fsStoreDeliverService.finishOrder(id,1);
         }
     }
 
@@ -199,4 +333,11 @@ public class LiveTask {
         }
 
     }
+    /**
+     * 更新流量
+     */
+    @Scheduled(fixedRate = CONSUME_INTERVAL)
+    public void insertLiveTrralog() {
+        redisBatchHandler.consumeBatchData();
+    }
 }

+ 228 - 47
fs-admin/src/main/java/com/fs/task/StoreTask.java

@@ -4,6 +4,20 @@ import cn.hutool.json.JSONArray;
 import cn.hutool.json.JSONUtil;
 import com.fs.common.annotation.QuartzRunnable;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.erp.service.impl.JSTErpOrderServiceImpl;
+import com.fs.huifuPay.domain.HuiFuQueryOrderResult;
+import com.fs.huifuPay.dto.*;
+import com.fs.huifuPay.sdk.opps.core.request.V2TradePaymentScanpayQueryRequest;
+import com.fs.huifuPay.service.HuiFuService;
+import com.fs.live.service.ILiveOrderService;
+import com.fs.pay.pay.domain.OrderResult;
+import com.fs.pay.pay.dto.OrderQueryDTO;
+import com.fs.pay.pay.service.PayService;
+import com.fs.store.domain.TzConfigInfoDTO;
+import com.fs.tzBank.TzBankService;
+import com.fs.tzBank.utils.TzConfigUtils;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.mapper.CompanyUserMapper;
 import com.fs.core.security.SecurityUtils;
@@ -28,9 +42,11 @@ import com.fs.store.vo.FsUserShareVO;
 import com.fs.system.service.ISysConfigService;
 import com.fs.wx.domain.FsWxExpressTask;
 import com.fs.wx.mapper.FsWxExpressTaskMapper;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.RateLimiter;
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang.ObjectUtils;
-import org.apache.commons.lang3.StringUtils;
+import org.apache.http.util.Asserts;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -40,11 +56,9 @@ import org.springframework.transaction.annotation.Transactional;
 
 import java.math.BigDecimal;
 import java.text.ParseException;
+import java.text.SimpleDateFormat;
 import java.time.LocalDateTime;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 
 import static com.fs.store.constants.StoreConstants.DELIVERY;
 
@@ -115,7 +129,14 @@ public class StoreTask {
 
     @Autowired
     private IFsStorePaymentService fsStorePaymentService;
-
+    @Autowired
+    private HuiFuService huiFuService;
+    @Autowired
+    private PayService ybPayService;
+    @Autowired
+    private TzBankService tzBankService;
+    @Autowired
+    private ILiveOrderService liveOrderService;
 
     @Autowired
     private FsWxExpressTaskMapper fsWxExpressTaskMapper;
@@ -129,6 +150,88 @@ public class StoreTask {
     @Autowired
     private FsJstCodPushService fsJstCodPushService;
 
+
+    // 订单银行回调数据丢失补偿(与 FsStorePaymentController.returnPayStatus 一致,仅处理商城近期待支付订单下的支付单)
+    public void recoveryBankOrder() {
+        List<FsStoreOrder> list = fsStoreOrderService.selectBankOrder();
+        if (CollectionUtils.isEmpty(list)) {
+            return;
+        }
+        for (FsStoreOrder storeOrder : list) {
+            try {
+                List<FsStorePayment> payments = fsStorePaymentService.selectFsStorePaymentByOrderIdNew(storeOrder.getId());
+                if (CollectionUtils.isEmpty(payments)) {
+                    continue;
+                }
+                for (FsStorePayment payment : payments) {
+                    if (payment.getStatus() == null || payment.getStatus() != 0) {
+                        continue;
+                    }
+                    if (StringUtils.isEmpty(payment.getTradeNo()) || StringUtils.isEmpty(payment.getPayMode())) {
+                        continue;
+                    }
+                    String payMode = payment.getPayMode();
+                    if ("hf".equals(payMode)) {
+                        V2TradePaymentScanpayQueryRequest request = new V2TradePaymentScanpayQueryRequest();
+                        request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
+                        request.setOrgHfSeqId(payment.getTradeNo());
+                        HuiFuQueryOrderResult o = huiFuService.queryOrder(request);
+                        logger.info("商城订单补偿 汇付查询 orderId={} resp={}", storeOrder.getId(), o);
+                        if (o != null && "00000000".equals(o.getResp_code()) && "S".equals(o.getTrans_stat())) {
+                            String[] parts = o.getOrg_req_seq_id().split("-");
+                            if (parts.length < 2) {
+                                continue;
+                            }
+                            if (parts[0].equals("store")) {
+                                orderService.payConfirm(1, null, parts[1], o.getOrg_hf_seq_id(), o.getOut_trans_id(), o.getParty_order_id());
+                            }
+                        }
+                    } else if ("yb".equals(payMode)) {
+                        OrderQueryDTO q = new OrderQueryDTO();
+                        q.setUpOrderId(payment.getTradeNo());
+                        OrderResult orderResult = ybPayService.getOrder(q);
+                        if (orderResult == null) {
+                            continue;
+                        }
+                        if ("4".equals(orderResult.getState()) || "5".equals(orderResult.getState())) {
+                            continue;
+                        }
+                        if (!"0".equals(orderResult.getState()) || !"100".equals(orderResult.getStatus())) {
+                            continue;
+                        }
+                        String[] parts = orderResult.getLowOrderId().split("-");
+                        if (parts.length < 2) {
+                            continue;
+                        }
+                        if (parts[0].equals("store")) {
+                            orderService.payConfirm(1, null, parts[1], payment.getTradeNo(), orderResult.getBankTrxId(), orderResult.getBankOrderId());
+                        }
+                    } else if ("tzbk".equals(payMode)) {
+                        FsStoreOrder fsOrder = orderService.selectFsStoreOrderById(payment.getOrderId());
+                        Asserts.notNull(fsOrder, "当前订单不存在!");
+                        RequestDTO<QueryOrderRestDTO> requestDTO = new RequestDTO<>();
+                        QueryOrderRestDTO queryOrderRestDTO = new QueryOrderRestDTO();
+                        queryOrderRestDTO.setOrderFlowNo(payment.getTradeNo());
+                        TzConfigInfoDTO tzConfigInfoDTO = TzConfigUtils.getConfig();
+                        queryOrderRestDTO.setPlatMerCstNo(tzConfigInfoDTO.getPlatMerCstNo());
+                        requestDTO.setReqBody(queryOrderRestDTO);
+                        requestDTO.setReqHeader(TzReqHeaderDTO.getInstance(payment.getPayCode()));
+                        TzReqResultDTO<QueryOrderInfoDTO> resultDTO = tzBankService.payQueryOrder(requestDTO);
+                        if (resultDTO == null || !"00000000".equals(resultDTO.getRetCode())) {
+                            logger.info("商城订单补偿 台州银行查询失败 orderId={} ret={}", storeOrder.getId(), resultDTO);
+                            continue;
+                        }
+                        QueryOrderInfoDTO body = resultDTO.getBody();
+                        orderService.payConfirm(1, fsOrder.getId(), payment.getPayCode(),
+                                payment.getTradeNo(), body.getChlTrxId(), payment.getTradeNo());
+                    }
+                }
+            } catch (Exception e) {
+                logger.error("商城订单银行补偿失败 orderId={}", storeOrder.getId(), e);
+            }
+        }
+    }
+
     /**
      * 推送jst售后单
      */
@@ -144,58 +247,133 @@ public class StoreTask {
     public void jstCodPush(){
         fsJstCodPushService.jstCodPush();
     }
-    /**
-     * 发货任务
-     */
-    @QuartzRunnable(name = "发货任务")
-    public void deliveryOp() {
-        List<FsStoreOrder> list = orderService.selectDeliverPenddingData();
-
-        for (FsStoreOrder order: list) {
-            String orderCode = order.getOrderCode();
-            ErpOrderQueryRequert request = new ErpOrderQueryRequert();
-            request.setCode(order.getExtendOrderId());
 
-            try{
-                // 根据仓库code找erp
-                if(com.fs.common.utils.StringUtils.isNotBlank(order.getStoreHouseCode())){
-                    String erp = fsWarehousesMapper.selectErpByCode(order.getStoreHouseCode());
-                    ErpContextHolder.setErpType(erp);
+    @Autowired
+    private JSTErpOrderServiceImpl jstErpOrderService;
+
+
+    private void jstErpOrderDelivery(List<Long> o_ids) throws InterruptedException {
+        if (org.springframework.util.CollectionUtils.isEmpty(o_ids)) return;
+
+        List<List<Long>> batches = Lists.partition(o_ids, 100);
+        List<String> extendIds = new ArrayList<>();
+        for (List<Long> batch : batches) {
+            int pageIndex = 1;
+            boolean has_next = false;
+            do {
+                Thread.sleep(300);
+                ErpOrderQueryRequert request = new ErpOrderQueryRequert();
+                request.setPage_size(100);
+                request.setPage_index(pageIndex);
+                request.setShop_id("18675245");
+                request.setO_ids(batch); // 设置当前批次
+
+                ErpOrderQueryResponse response = jstErpOrderService.getOrder(request);
+                has_next = response.hasNextSafe();
+                for (Long l : batch) {
+                    extendIds.add(l.toString());
                 }
-                ErpOrderQueryResponse response = erpOrderService.getOrder(request);
-                if (CollectionUtils.isNotEmpty(response.getOrders())) {
-                    for (ErpOrderQuery orderQuery : response.getOrders()) {
-                        if (CollectionUtils.isNotEmpty(orderQuery.getDeliverys())) {
-                            // 部分发货或者全部发货
-                            if (ObjectUtils.equals(orderQuery.getDelivery_state(), 1) || ObjectUtils.equals(orderQuery.getDelivery_state(), 2)) {
-
-                                orderStatusService.create(order.getId(), OrderLogEnum.DELIVERY_GOODS.getValue(),
-                                        OrderLogEnum.DELIVERY_GOODS.getDesc());
-
-                                for (ErpDeliverys delivery : orderQuery.getDeliverys()) {
-
-                                    FsExpress express=expressService.selectFsExpressByOmsCode(delivery.getExpress_code());
-                                    if(express == null){
-                                        logger.warn("当前express_code: {} 不存在!",delivery.getExpress_code());
+                orderService.updateUpdateTimeByExtendIds(extendIds);
+
+                if (response.getOrders() != null) {
+                    for(ErpOrderQuery orderQuery : response.getOrders()){
+                        if(orderQuery.getDeliverys()!=null&& !orderQuery.getDeliverys().isEmpty()){
+                            for(ErpDeliverys delivery:orderQuery.getDeliverys()){
+                                if(delivery.getDelivery()&& org.apache.commons.lang3.StringUtils.isNotEmpty(delivery.getMail_no())){
+                                    orderStatusService.create(Long.valueOf(orderQuery.getCode()), OrderLogEnum.DELIVERY_GOODS.getValue(),
+                                            OrderLogEnum.DELIVERY_GOODS.getDesc());
+                                    try {
+                                        for (ErpDeliverys deli : orderQuery.getDeliverys()) {
+                                            FsExpress express=expressService.selectFsExpressByOmsCode(deli.getExpress_code());
+                                            if(express == null){
+                                                logger.error("当前express_code: {} 不存在!",deli.getExpress_code());
+                                                continue;
+                                            }
+                                            if (deli.getDelivery()) {
+                                                orderService.deliveryOrder(orderQuery.getCode(), deli.getMail_no(),
+                                                        deli.getExpress_code(), deli.getExpress_name());
+                                            }
+                                        }
+                                    } catch (Exception e) {
+                                        logger.error("同步订单状态失败  {}",delivery.getMail_no(),e);
                                         continue;
                                     }
-
-                                    if (delivery.getDelivery()) {
-                                        orderService.deliveryOrder(orderCode, delivery.getMail_no(),
-                                                delivery.getExpress_code(), delivery.getExpress_name());
-                                    }
                                 }
-
-                                logger.info("订单 {} 发货信息同步成功",order.getOrderCode());
                             }
+
                         }
                     }
                 }
-            }catch (Exception e){
-                logger.error(String.format("[发货任务]调用erp查询接口失败!原因: %s",e));
-            }
+                pageIndex++;
+            } while (has_next);
+        }
+    }
+    /**
+     * 发货任务
+     */
+    @QuartzRunnable(name = "发货任务")
+    public void deliveryOp() throws InterruptedException {
+        List<FsStoreOrder> list = orderService.selectDeliverPenddingData();
 
+        //聚水潭
+        ArrayList<Long> o_ids = new ArrayList<>();
+        for (FsStoreOrder fsStoreOrder : list) {
+            o_ids.add(Long.parseLong(fsStoreOrder.getExtendOrderId()));
         }
+        if (!org.springframework.util.CollectionUtils.isEmpty(o_ids)){
+            jstErpOrderDelivery(o_ids);
+        }
+
+
+
+//        Date nowDate = DateUtils.getNowDate();
+//        for (FsStoreOrder order: list) {
+//            order.setUpdateTime(nowDate);
+//            orderService.updateFsStoreOrderDb(order);
+//            String orderCode = order.getOrderCode();
+//            ErpOrderQueryRequert request = new ErpOrderQueryRequert();
+//            request.setCode(order.getExtendOrderId());
+//
+//            try{
+//                // 根据仓库code找erp
+//                if(com.fs.common.utils.StringUtils.isNotBlank(order.getStoreHouseCode())){
+//                    String erp = fsWarehousesMapper.selectErpByCode(order.getStoreHouseCode());
+//                    ErpContextHolder.setErpType(erp);
+//                }
+//                ErpOrderQueryResponse response = erpOrderService.getOrder(request);
+//                if (CollectionUtils.isNotEmpty(response.getOrders())) {
+//                    for (ErpOrderQuery orderQuery : response.getOrders()) {
+//                        if (CollectionUtils.isNotEmpty(orderQuery.getDeliverys())) {
+//                            // 部分发货或者全部发货
+//                            if (ObjectUtils.equals(orderQuery.getDelivery_state(), 1) || ObjectUtils.equals(orderQuery.getDelivery_state(), 2)) {
+//
+//                                orderStatusService.create(order.getId(), OrderLogEnum.DELIVERY_GOODS.getValue(),
+//                                        OrderLogEnum.DELIVERY_GOODS.getDesc());
+//
+//                                for (ErpDeliverys delivery : orderQuery.getDeliverys()) {
+//
+//                                    FsExpress express=expressService.selectFsExpressByOmsCode(delivery.getExpress_code());
+//                                    if(express == null){
+//                                        logger.warn("当前express_code: {} 不存在!",delivery.getExpress_code());
+//                                        continue;
+//                                    }
+//
+//                                    if (delivery.getDelivery()) {
+//                                        orderService.deliveryOrder(orderCode, delivery.getMail_no(),
+//                                                delivery.getExpress_code(), delivery.getExpress_name());
+//                                    }
+//                                }
+//
+//                                logger.info("订单 {} 发货信息同步成功",order.getOrderCode());
+//                            }
+//                        }
+//                    }
+//                }
+//            }catch (Exception e){
+//                logger.error(String.format("[发货任务]调用erp查询接口失败!原因: %s",e));
+//            }
+//
+//        }
 
     }
 
@@ -503,6 +681,7 @@ public class StoreTask {
     }
 
 
+    private final RateLimiter rateLimiter = RateLimiter.create(1.3);
 
     /**
      * 更新发货状态
@@ -513,9 +692,11 @@ public class StoreTask {
 
         for (FsStoreOrder order : list) {
             try{
+                rateLimiter.acquire();
                 orderService.syncDeliveryOrder(order);
             }catch (Exception e) {
                 logger.error("获取订单是否发货失败!原因: ",e);
+
             }
         }
 

+ 11 - 0
fs-admin/src/main/resources/application-dev.yml

@@ -77,3 +77,14 @@ spring:
                 wall:
                     config:
                         multi-statement-allow: true
+
+rocketmq:
+    name-server: rmq-16b45v4p9n.rocketmq.cd.qcloud.tencenttdmq.com:8080 # RocketMQ NameServer 地址
+    producer:
+        group: my-producer-group
+        access-key: ak16b45v4p9n150d89395b3c # 替换为实际的 accessKey
+        secret-key: sk370fb48d869b152b # 替换为实际的 secretKey
+    consumer:
+        group: common-group
+        access-key: ak16b45v4p9n150d89395b3c # 替换为实际的 accessKey
+        secret-key: sk370fb48d869b152b # 替换为实际的 secretKey

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

@@ -35,7 +35,7 @@ server:
 # 日志配置
 logging:
   level:
-    com.fs: debug
+    com.fs: warn
     org.springframework: warn
 
 # Spring配置

+ 1 - 1
fs-admin/src/test/java/com/fs/store/controller/FsStorePaymentControllerTest.java

@@ -383,7 +383,7 @@ public class FsStorePaymentControllerTest {
     private ExpressTask expressTask;
 
     @Test
-    public void deliveryOp(){
+    public void deliveryOp() throws InterruptedException {
         storeTask.deliveryOp();
     }
 

+ 1 - 1
fs-api/src/main/java/com/fs/core/datasource/DynamicDataSourceContextHolder.java

@@ -23,7 +23,7 @@ public class DynamicDataSourceContextHolder
      */
     public static void setDataSourceType(String dsType)
     {
-        log.info("切换到{}数据源", dsType);
+        
         CONTEXT_HOLDER.set(dsType);
     }
 

+ 42 - 0
fs-common/src/main/java/com/fs/common/annotation/RateLimiter.java

@@ -0,0 +1,42 @@
+package com.fs.common.annotation;
+
+import com.fs.common.constant.Constants;
+import com.fs.common.enums.LimitType;
+
+import java.lang.annotation.*;
+
+/**
+ * 限流注解
+ * 
+
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RateLimiter
+{
+    /**
+     * 限流key
+     */
+    public String key() default Constants.RATE_LIMIT_KEY;
+
+    /**
+     * 限流时间,单位秒
+     */
+    public int time() default 60;
+
+    /**
+     * 限流次数
+     */
+    public int count() default 100;
+
+    /**
+     * 限流类型
+     */
+    public LimitType limitType() default LimitType.DEFAULT;
+
+    /**
+     * 消息提示
+     */
+    public String msg() default "访问过于频繁,请稍后再试";
+}

+ 5 - 0
fs-common/src/main/java/com/fs/common/constant/Constants.java

@@ -131,6 +131,11 @@ public class Constants
 
     public static final Integer PAGE_SIZE =10;
 
+    /**
+     * 限流 redis key
+     */
+    public static final String RATE_LIMIT_KEY = "rate_limit:";
+
     /**
      * 令牌前缀 企业端
      */

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

@@ -26,6 +26,13 @@ public class LiveKeysConstant {
     public static final String LIVE_HOME_PAGE_CONFIG_DRAW = "live:config:%s:draw:%s"; //抽奖记录
     public static final String TOP_MSG = "topMsg"; //抽奖记录
 
+    public static final String LIVE_FLAG_CACHE = "live:flag:%s"; //直播间直播/回放状态缓存
+    public static final Integer LIVE_FLAG_CACHE_EXPIRE = 300; //直播间状态缓存过期时间(秒)
 
+    public static final String LIVE_DATA_CACHE = "live:data:%s"; //直播间数据缓存
+    public static final Integer LIVE_DATA_CACHE_EXPIRE = 300; //直播间数据缓存过期时间(秒)
+
+    public static final String PRODUCT_DETAIL_CACHE = "product:detail:%s"; //商品详情缓存
+    public static final Integer PRODUCT_DETAIL_CACHE_EXPIRE = 300; //商品详情缓存过期时间(秒)
 
 }

+ 17 - 0
fs-common/src/main/java/com/fs/common/constant/RedisConstant.java

@@ -0,0 +1,17 @@
+package com.fs.common.constant;
+/**
+ * 库存与锁相关常量(Java 8 静态常量优化)
+ */
+public class RedisConstant {
+    // 库存Key前缀
+    public static final String STOCK_KEY_PREFIX = "product:stock:";
+    public static final String SALE_KEY_PREFIX = "product:sale:";
+    // 分布式锁Key前缀
+    public static final String LOCK_KEY_PREFIX = "product:lock:";
+    // 锁过期时间(30秒,避免死锁,大于业务执行时间)
+    public static final long LOCK_EXPIRE_SECONDS = 3L;
+    // 锁重试间隔(50毫秒,非阻塞重试,避免线程阻塞)
+    public static final long LOCK_RETRY_INTERVAL = 100L;
+    // 锁最大重试次数(3次,避免无限重试)
+    public static final int LOCK_MAX_RETRY = 20;
+}

+ 4 - 0
fs-common/src/main/java/com/fs/common/core/domain/R.java

@@ -53,4 +53,8 @@ public class R extends HashMap<String, Object> {
 		super.put(key, value);
 		return this;
 	}
+
+	public boolean isSuccess() {
+		return (Integer) get("code") == HttpStatus.SUCCESS;
+	}
 }

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

@@ -271,4 +271,19 @@ public class RedisCache
     public boolean setIfAbsent(final String key, final Object value, long timeout, TimeUnit unit) {
         return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit));
     }
+
+    /**
+     * 向哈希表中添加键值对
+     *
+     * @param key     哈希表键
+     * @param hashKey 哈希键
+     * @param value   哈希值
+     */
+    public void hashPut(String key, String hashKey, Object value) {
+        redisTemplate.opsForHash().put(key, hashKey, value);
+    }
+
+    public Long incr(final String key,final long delta) {
+        return redisTemplate.opsForValue().increment(key, delta);
+    }
 }

+ 236 - 0
fs-common/src/main/java/com/fs/common/core/redis/RedisCacheT.java

@@ -0,0 +1,236 @@
+package com.fs.common.core.redis;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.BoundSetOperations;
+import org.springframework.data.redis.core.HashOperations;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.stereotype.Component;
+
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * spring redis 工具类
+ *
+
+ **/
+@SuppressWarnings(value = { "unchecked", "rawtypes" })
+@Component
+public class RedisCacheT<T> {
+    @Autowired
+    public RedisTemplate redisTemplate;
+
+    /**
+     * 缓存基本的对象,Integer、String、实体类等
+     *
+     * @param key 缓存的键值
+     * @param value 缓存的值
+     */
+    public void setCacheObject(final String key, final T value)
+    {
+        redisTemplate.opsForValue().set(key, value);
+    }
+
+    /**
+     * 缓存基本的对象,Integer、String、实体类等
+     *
+     * @param key 缓存的键值
+     * @param value 缓存的值
+     * @param timeout 时间
+     * @param timeUnit 时间颗粒度
+     */
+    public void setCacheObject(final String key, final T value, final long timeout, final TimeUnit timeUnit)
+    {
+        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
+    }
+
+    /**
+     * 设置有效时间
+     *
+     * @param key Redis键
+     * @param timeout 超时时间
+     * @return true=设置成功;false=设置失败
+     */
+    public boolean expire(final String key, final long timeout)
+    {
+        return expire(key, timeout, TimeUnit.SECONDS);
+    }
+
+    /**
+     * 设置有效时间
+     *
+     * @param key Redis键
+     * @param timeout 超时时间
+     * @param unit 时间单位
+     * @return true=设置成功;false=设置失败
+     */
+    public boolean expire(final String key, final long timeout, final TimeUnit unit)
+    {
+        return redisTemplate.expire(key, timeout, unit);
+    }
+
+    /**
+     * 获得缓存的基本对象。
+     *
+     * @param key 缓存键值
+     * @return 缓存键值对应的数据
+     */
+    public T getCacheObject(final String key)
+    {
+        ValueOperations<String, T> operation = redisTemplate.opsForValue();
+        return operation.get(key);
+    }
+
+    /**
+     * 删除单个对象
+     *
+     * @param key
+     */
+    public boolean deleteObject(final String key)
+    {
+        return redisTemplate.delete(key);
+    }
+
+    /**
+     * 删除集合对象
+     *
+     * @param collection 多个对象
+     * @return
+     */
+    public long deleteObject(final Collection collection)
+    {
+        return redisTemplate.delete(collection);
+    }
+
+    /**
+     * 缓存List数据
+     *
+     * @param key 缓存的键值
+     * @param dataList 待缓存的List数据
+     * @return 缓存的对象
+     */
+    public long setCacheList(final String key, final List<T> dataList)
+    {
+        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
+        return count == null ? 0 : count;
+    }
+
+    /**
+     * 获得缓存的list对象
+     *
+     * @param key 缓存的键值
+     * @return 缓存键值对应的数据
+     */
+    public List<T> getCacheList(final String key)
+    {
+        return redisTemplate.opsForList().range(key, 0, -1);
+    }
+    public List<T> getCacheListByPattern(String pattern) {
+        Set<String> keys = redisTemplate.keys(pattern);
+        return redisTemplate.opsForValue().multiGet(keys);
+    }
+
+    /**
+     * 缓存Set
+     *
+     * @param key 缓存键值
+     * @param dataSet 缓存的数据
+     * @return 缓存数据的对象
+     */
+    public BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
+    {
+        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
+        Iterator<T> it = dataSet.iterator();
+        while (it.hasNext())
+        {
+            setOperation.add(it.next());
+        }
+        return setOperation;
+    }
+
+    /**
+     * 获得缓存的set
+     *
+     * @param key
+     * @return
+     */
+    public Set<T> getCacheSet(final String key)
+    {
+        return redisTemplate.opsForSet().members(key);
+    }
+
+    /**
+     * 缓存Map
+     *
+     * @param key
+     * @param dataMap
+     */
+    public void setCacheMap(final String key, final Map<String, T> dataMap)
+    {
+        if (dataMap != null) {
+            redisTemplate.opsForHash().putAll(key, dataMap);
+        }
+    }
+
+    /**
+     * 获得缓存的Map
+     *
+     * @param key
+     * @return
+     */
+    public Map<String, T> getCacheMap(final String key)
+    {
+        return redisTemplate.opsForHash().entries(key);
+    }
+
+    /**
+     * 往Hash中存入数据
+     *
+     * @param key Redis键
+     * @param hKey Hash键
+     * @param value 值
+     */
+    public void setCacheMapValue(final String key, final String hKey, final T value)
+    {
+        redisTemplate.opsForHash().put(key, hKey, value);
+    }
+
+    /**
+     * 获取Hash中的数据
+     *
+     * @param key Redis键
+     * @param hKey Hash键
+     * @return Hash中的对象
+     */
+    public T getCacheMapValue(final String key, final String hKey)
+    {
+        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
+        return opsForHash.get(key, hKey);
+    }
+
+    /**
+     * 获取多个Hash中的数据
+     *
+     * @param key Redis键
+     * @param hKeys Hash键集合
+     * @return Hash对象集合
+     */
+    public List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
+    {
+        return redisTemplate.opsForHash().multiGet(key, hKeys);
+    }
+
+    /**
+     * 获得缓存的基本对象列表
+     *
+     * @param pattern 字符串前缀
+     * @return 对象列表
+     */
+    public Collection<String> keys(final String pattern)
+    {
+        return redisTemplate.keys(pattern);
+    }
+
+
+}

+ 37 - 4
fs-common/src/main/java/com/fs/common/core/redis/RedisUtil.java

@@ -1,14 +1,13 @@
 package com.fs.common.core.redis;
 
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.Cursor;
 import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ScanOptions;
 import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.stereotype.Component;
 
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -233,6 +232,40 @@ public class RedisUtil {
         return redisTemplate.opsForHash().entries(key);
     }
 
+    /**
+     * 优化版:切片查询 Redis Hash 所有条目(分批 hscan)
+     * @param key Redis Hash 的 key
+     * @param batchSize 每次扫描的条数(建议 100-1000,根据业务调整)
+     * @return 完整的 Hash 键值对
+     */
+    public Map<Object, Object> hashEntriesByScan(String key, int batchSize) {
+        // 最终返回的完整Map(按需可改为边遍历边处理,不一次性存储)
+        Map<Object, Object> resultMap = new HashMap<>();
+
+        // 1. 构建 Scan 配置:count 指定每次扫描的建议条数(非绝对,Redis 内部优化)
+        ScanOptions scanOptions = ScanOptions.scanOptions()
+                .count(batchSize) // 每次扫描最多返回 batchSize 条
+                .match("*")       // 匹配所有字段(可指定前缀,如 "user_*" 过滤)
+                .build();
+
+        // 2. 获取游标,开始分批遍历(必须关闭游标,否则会泄漏 Redis 连接)
+        try (Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(key, scanOptions)) {
+            // 3. 迭代游标,分批读取数据
+            while (cursor.hasNext()) {
+                Map.Entry<Object, Object> entry = cursor.next();
+                resultMap.put(entry.getKey(), entry.getValue());
+
+                // 【可选】若不需要完整返回,可在此处边遍历边处理(比如写入数据库/缓存)
+                // processEntry(entry.getKey(), entry.getValue());
+            }
+        } catch (Exception e) {
+            // 异常处理:记录日志、告警,避免游标泄漏
+            throw new RuntimeException("Redis Hash 切片查询失败,key=" + key, e);
+        }
+
+        return resultMap;
+    }
+
     /**
      * 获取哈希表中数量
      *

+ 139 - 0
fs-common/src/main/java/com/fs/common/core/redis/service/StockDeductService.java

@@ -0,0 +1,139 @@
+package com.fs.common.core.redis.service;
+
+import com.fs.common.constant.RedisConstant;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Random;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.IntStream;
+
+/**
+ * 高并发库存扣减服务(Java 8 + Redis分布式锁)
+ */
+@Slf4j
+@Service
+public class StockDeductService {
+
+    // 注入RedisTemplate
+    public final RedisTemplate<String, Object> redisTemplate;
+
+    // 构造器注入(Spring 推荐,Java 8 支持)
+    public StockDeductService(RedisTemplate<String, Object> redisTemplate) {
+        this.redisTemplate = redisTemplate;
+    }
+
+    // 库存扣减Lua脚本(预编译,提升高并发性能)
+    private static final DefaultRedisScript<Long> STOCK_DEDUCT_SCRIPT;
+    // 锁释放Lua脚本(预编译)
+    private static final DefaultRedisScript<Long> LOCK_RELEASE_SCRIPT;
+
+    // 库存扣减Lua脚本(优化后,增强健壮性)
+    static {
+        // 初始化库存扣减脚本
+        STOCK_DEDUCT_SCRIPT = new DefaultRedisScript<>();
+        STOCK_DEDUCT_SCRIPT.setScriptText("if redis.call('exists', KEYS[1]) ~= 1 then " + "return -2; " + "end " + "local stock_str = redis.call('get', KEYS[1]); " + "local stock = tonumber(stock_str); " + "if stock == nil then " + "return -3; " + "end " + "local deductNum_str = ARGV[1]; " + "local deductNum = tonumber(deductNum_str); " + "if deductNum == nil or deductNum <= 0 then " + "return -4; " + "end " + "if stock >= deductNum then " + "return redis.call('decrby', KEYS[1], deductNum); " + "else " + "return -1; " + "end");
+        STOCK_DEDUCT_SCRIPT.setResultType(Long.class);
+
+        // 锁释放脚本保持不变
+        LOCK_RELEASE_SCRIPT = new DefaultRedisScript<>();
+        LOCK_RELEASE_SCRIPT.setScriptText("if redis.call('get', KEYS[1]) == ARGV[1] then " + "return redis.call('del', KEYS[1]) " + "else return 0 end");
+        LOCK_RELEASE_SCRIPT.setResultType(Long.class);
+    }
+
+    /**
+     * 初始化商品库存(Redis)
+     *
+     * @param productId 商品ID
+     * @param initStock 初始库存
+     */
+    public void initStock(Long productId, Long liveId, Integer initStock) {
+        String stockKey = RedisConstant.STOCK_KEY_PREFIX + liveId + ":" + productId;
+        redisTemplate.opsForValue().set(stockKey, initStock);
+        log.info("商品" + productId + "库存初始化完成,初始库存:" + initStock);
+    }
+
+    /**
+     * 高并发库存扣减(核心方法,落地Java 8特性)
+     *
+     * @param productId 商品ID
+     * @param deductNum 扣减数量(默认1)
+     * @return 扣减结果:true=成功,false=失败
+     */
+    public CompletableFuture<Boolean> deductStockAsync(Long productId, Long liveId, Integer deductNum, Long userId) {
+        // Java 8 CompletableFuture 异步处理,提升高并发吞吐量
+        return CompletableFuture.supplyAsync(() -> {
+            // 1. 参数校验(Java 8 Optional 空值处理)
+            Integer num = Optional.ofNullable(deductNum).orElse(1);
+            String stockKey = RedisConstant.STOCK_KEY_PREFIX + liveId + ":" + productId;
+            String lockKey = RedisConstant.LOCK_KEY_PREFIX + liveId + ":" + productId;
+
+            // 3. 尝试获取分布式锁(非阻塞重试,Java 8 Stream API 实现重试)
+// 3. 尝试获取分布式锁(优化:加入随机延迟,避免惊群效应)
+            boolean isLockAcquired = IntStream.range(0, RedisConstant.LOCK_MAX_RETRY).anyMatch(retryCount -> {
+                Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, userId, RedisConstant.LOCK_EXPIRE_SECONDS, TimeUnit.SECONDS);
+                if (Boolean.TRUE.equals(result)) {
+                    return true;
+                }
+                try {
+                    // 随机延迟:50ms~150ms,避免所有请求同时重试
+                    long randomDelay = RedisConstant.LOCK_RETRY_INTERVAL + new Random().nextInt(100);
+                    TimeUnit.MILLISECONDS.sleep(randomDelay);
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                    return false;
+                }
+                return false;
+            });
+            // 4. 未获取到锁,直接返回失败
+            if (!isLockAcquired) {
+                System.err.println("商品" + productId + "获取锁失败,高并发限流中");
+                return false;
+            }
+
+            try {
+                // 5. 执行库存扣减Lua脚本(原子操作,防超卖)
+                // 新增日志:打印当前库存值和扣减数量
+                Integer currentStockStr = (Integer) redisTemplate.opsForValue().get(stockKey);
+                log.info("拿到锁成功 → 库存Key:{},当前库存值:{},扣减数量:{}", stockKey, currentStockStr, num);
+
+                // 执行库存扣减Lua脚本
+                Long remainingStock = redisTemplate.execute(
+                        STOCK_DEDUCT_SCRIPT,
+                        Collections.singletonList(stockKey),
+                        1
+                );
+
+                // 新增日志:打印Lua返回结果
+                log.info("Lua脚本返回值:{}", remainingStock);
+
+                // 6. 判断扣减结果
+                if (remainingStock >= 0) {
+                    log.info("商品{}库存扣减成功,剩余库存:{}", productId, remainingStock);
+                    return true;
+                } else {
+                    String errorMsg = "";
+                    switch (remainingStock.intValue()) {
+                        case -1: errorMsg = "库存不足"; break;
+                        case -2: errorMsg = "库存Key不存在"; break;
+                        case -3: errorMsg = "库存值非数字"; break;
+                        case -4: errorMsg = "扣减数量无效"; break;
+                        default: errorMsg = "未知错误,错误码:" + remainingStock;
+                    }
+                    log.info("商品{}扣减失败:{}", productId, errorMsg);
+                    return false;
+                }
+            } finally {
+                // 7. 释放分布式锁(Lua脚本保证原子性,仅释放自己持有的锁)
+                redisTemplate.execute(LOCK_RELEASE_SCRIPT, Collections.singletonList(lockKey), userId);
+                log.info("商品{}锁释放成功,持有者:{}", productId, userId);
+            }
+        });
+    }
+}

+ 20 - 0
fs-common/src/main/java/com/fs/common/enums/LimitType.java

@@ -0,0 +1,20 @@
+package com.fs.common.enums;
+
+/**
+ * 限流类型
+ *
+
+ */
+
+public enum LimitType
+{
+    /**
+     * 默认策略全局限流
+     */
+    DEFAULT,
+
+    /**
+     * 根据请求者IP进行限流
+     */
+    IP
+}

+ 73 - 0
fs-common/src/main/java/com/fs/common/exception/ServiceException.java

@@ -0,0 +1,73 @@
+package com.fs.common.exception;
+
+/**
+ * 业务异常
+ * 
+
+ */
+public final class ServiceException extends RuntimeException
+{
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 错误码
+     */
+    private Integer code;
+
+    /**
+     * 错误提示
+     */
+    private String message;
+
+    /**
+     * 错误明细,内部调试错误
+     *
+     * 和 {@link CommonResult#getDetailMessage()} 一致的设计
+     */
+    private String detailMessage;
+
+    /**
+     * 空构造方法,避免反序列化问题
+     */
+    public ServiceException()
+    {
+    }
+
+    public ServiceException(String message)
+    {
+        this.message = message;
+    }
+
+    public ServiceException(String message, Integer code)
+    {
+        this.message = message;
+        this.code = code;
+    }
+
+    public String getDetailMessage()
+    {
+        return detailMessage;
+    }
+
+    public String getMessage()
+    {
+        return message;
+    }
+
+    public Integer getCode()
+    {
+        return code;
+    }
+
+    public ServiceException setMessage(String message)
+    {
+        this.message = message;
+        return this;
+    }
+
+    public ServiceException setDetailMessage(String detailMessage)
+    {
+        this.detailMessage = detailMessage;
+        return this;
+    }
+}

+ 237 - 5
fs-common/src/main/java/com/fs/common/utils/PhoneUtils.java

@@ -1,14 +1,246 @@
 package com.fs.common.utils;
 
 import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpException;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.domain.AjaxResult;
+import lombok.extern.slf4j.Slf4j;
 
+import static com.fs.common.core.domain.AjaxResult.CODE_TAG;
+import static com.fs.common.core.domain.AjaxResult.DATA_TAG;
+
+@Slf4j
 public class PhoneUtils {
-    public static String getLastFourNum(String phone) {
 
-            String lastFourNumber = phone;
-            if (lastFourNumber.length() == 11) {
-                lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
+    /**
+     * 解密
+     */
+    public static String decryptPhone(String encryptedText) {
+        try {
+            String text=null;
+            String url= "http://42.194.147.226:8000/app/common/genDecryption";
+            // 构建请求体 JSON
+            JSONObject jsonBody = new JSONObject();
+            jsonBody.put("content", encryptedText);
+
+            String result = HttpRequest.post(url)
+                    .body(jsonBody.toString())
+                    .execute()
+                    .body();
+            //解析返回
+            AjaxResult ajaxResult = JSONUtil.toBean(result, AjaxResult.class);
+
+            //取值返回参数
+            Object codeValue = ajaxResult.get(CODE_TAG);
+            Object dataValue = ajaxResult.get(DATA_TAG);
+
+            //判断是否成功
+            if ("200".equals(String.valueOf(codeValue))) {
+                text = String.valueOf(dataValue);
+            } else {
+                log.error("请求失败,状态码: " + codeValue);
+                return "";
             }
-            return lastFourNumber;
+
+            return text;
+        } catch (HttpException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 加密
+     */
+    public static String encryptPhone(String text) {
+        try {
+            String encryptedText=null;
+            String url= "http://42.194.147.226:8000/app/common/genEncryption";
+            // 构建请求体 JSON
+            JSONObject jsonBody = new JSONObject();
+            jsonBody.put("content", text);
+
+            String result = HttpRequest.post(url)
+                    .body(jsonBody.toString())
+                    .execute()
+                    .body();
+            //解析返回
+            AjaxResult ajaxResult = JSONUtil.toBean(result, AjaxResult.class);
+
+            //取值返回参数
+            Object codeValue = ajaxResult.get(CODE_TAG);
+            Object dataValue = ajaxResult.get(DATA_TAG);
+
+            //判断是否成功
+            if ("200".equals(String.valueOf(codeValue))) {
+                encryptedText = String.valueOf(dataValue);
+            } else {
+                log.error("请求失败,状态码: " + codeValue);
+                return "";
+            }
+
+            return encryptedText;
+        } catch (HttpException e) {
+            throw new RuntimeException(e);
+        }
+
+    }
+
+    /**
+     * 是否为11位明文手机号(纯数字)
+     */
+    public static boolean isPlainMobile11(String phone) {
+        return StrUtil.isNotBlank(phone) && phone.matches("^1\\d{10}$");
+    }
+
+    /**
+     * 是否为脱敏展示格式(如 177****5248)
+     */
+    public static boolean isMaskedPhone(String phone) {
+        return StrUtil.isNotBlank(phone) && phone.contains("****");
+    }
+
+    /**
+     * 解析为11位明文:已是明文直接返回;密文则解密;脱敏格式返回 null(需业务侧先解析)
+     */
+    public static String resolveToPlainPhone(String phone) {
+        if (StrUtil.isBlank(phone)) {
+            return phone;
+        }
+        if (isPlainMobile11(phone)) {
+            return phone;
+        }
+        if (isMaskedPhone(phone)) {
+            return null;
+        }
+        return tryDecryptToPlain(phone);
+    }
+
+    /**
+     * 将库中/用户表中的手机号转为脱敏格式,用于与前端传入的脱敏号比对
+     */
+    public static String toMaskedPhone(String phone) {
+        if (StrUtil.isBlank(phone)) {
+            return null;
+        }
+        if (isMaskedPhone(phone)) {
+            return phone;
+        }
+        String plain = resolveToPlainPhone(phone);
+        if (isPlainMobile11(plain)) {
+            return ParseUtils.parsePhone(plain);
+        }
+        return null;
+    }
+
+    /**
+     * 入库前统一处理:明文/外部密文 → 统一加密接口密文
+     */
+    public static String normalizePhoneForStorage(String phone) {
+        if (StrUtil.isBlank(phone)) {
+            return phone;
+        }
+        if (isMaskedPhone(phone)) {
+            log.warn("入库前仍为脱敏手机号,请先解析为明文: {}", phone);
+            return phone;
+        }
+        if (isPlainMobile11(phone)) {
+            return encryptPlainOrKeep(phone);
+        }
+        String plain = tryDecryptToPlain(phone);
+        if (isPlainMobile11(plain)) {
+            return encryptPlainOrKeep(plain);
+        }
+        return phone;
+    }
+
+    /**
+     * 无权限查看时:先解为明文再脱敏展示
+     */
+    public static String normalizePhoneForDisplay(String phone) {
+        if (StrUtil.isBlank(phone)) {
+            return phone;
+        }
+        if (isMaskedPhone(phone)) {
+            return phone;
+        }
+        String plain = resolveToPlainPhone(phone);
+        if (isPlainMobile11(plain)) {
+            return ParseUtils.parsePhone(plain);
+        }
+        return ParseUtils.parsePhone(phone);
+    }
+
+    /**
+     * ERP 上传用手机号:仅明文直传,其余解密为明文
+     */
+    public static String resolvePhoneForErp(String phone) {
+        if (StrUtil.isBlank(phone)) {
+            return phone;
+        }
+        if (isPlainMobile11(phone)) {
+            return phone;
+        }
+        if (isMaskedPhone(phone)) {
+            log.warn("ERP上传遇到脱敏手机号: {}", phone);
+            return phone;
+        }
+        String plain = tryDecryptToPlain(phone);
+        return StrUtil.isNotBlank(plain) ? plain : phone;
+    }
+
+    /**
+     * 从 ErpOrder 取收件人手机号并解析为 ERP 可上传的明文
+     */
+    public static String resolveErpOrderReceiverPhone(String receiverMobile, String receiverPhone) {
+        if (StrUtil.isNotBlank(receiverMobile)) {
+            return resolvePhoneForErp(receiverMobile);
+        }
+        if (StrUtil.isNotBlank(receiverPhone)) {
+            return resolvePhoneForErp(receiverPhone);
+        }
+        return "";
+    }
+
+    /**
+     * 快递查询用手机号后四位(顺丰/中通等)
+     */
+    public static String getLastFourNum(String phone) {
+        if (StrUtil.isBlank(phone)) {
+            return "";
+        }
+        String plain = resolveToPlainPhone(phone);
+        if (isPlainMobile11(plain)) {
+            return plain.substring(plain.length() - 4);
+        }
+        if (isMaskedPhone(phone) && phone.length() >= 4) {
+            return phone.substring(phone.length() - 4);
+        }
+        if (phone.length() >= 4) {
+            return phone.substring(phone.length() - 4);
+        }
+        return phone;
+    }
+
+    private static String tryDecryptToPlain(String phone) {
+        try {
+            String decrypted = decryptPhone(phone);
+            return isPlainMobile11(decrypted) ? decrypted : "";
+        } catch (Exception e) {
+            log.error("手机号解密失败", e);
+            return "";
+        }
+    }
+
+    private static String encryptPlainOrKeep(String plain) {
+        try {
+//            return plain;
+            String encrypted = encryptPhone(plain);
+            return StrUtil.isNotBlank(encrypted) ? encrypted : plain;
+        } catch (Exception e) {
+            log.error("手机号加密失败", e);
+            return plain;
+        }
     }
 }

+ 10 - 0
fs-common/src/main/java/com/fs/common/vo/LiveVo.java

@@ -20,6 +20,10 @@ public class LiveVo {
     private Integer status;
 
     private Long anchorId;
+    private Long companyId;
+
+    private Long videoFileSize;
+    private Long videoDuration;
 
     private Integer liveType;
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@@ -51,4 +55,10 @@ public class LiveVo {
     private String previewUrl;
     private Integer previewVideoType;
     private Long previewVideoId;
+
+    /** 是否开启直播完课积分功能 */
+    private Boolean completionPointsEnabled;
+
+    /** 今天是否已领取完课奖励 */
+    private Boolean todayRewardReceived;
 }

+ 123 - 0
fs-company-app/src/main/java/com/fs/app/controller/StoreProductPackageController.java

@@ -3,8 +3,16 @@ package com.fs.app.controller;
 
 import cn.hutool.json.JSONArray;
 import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSONObject;
 import com.fs.app.annotation.Login;
 import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.http.HttpUtils;
+import com.fs.company.domain.Company;
+import com.fs.company.domain.CompanyUser;
+import com.fs.company.service.ICompanyService;
+import com.fs.company.service.ICompanyUserService;
 import com.fs.store.domain.FsStoreProduct;
 import com.fs.store.domain.FsStoreProductAttrValue;
 import com.fs.store.domain.FsStoreProductPackage;
@@ -13,14 +21,19 @@ import com.fs.store.dto.StorePackageProductDTO;
 import com.fs.store.param.FsStoreProductPackageQueryParam;
 import com.fs.store.service.IFsStoreProductAttrValueService;
 import com.fs.store.service.IFsStoreProductPackageService;
+import com.fs.store.service.IFsStoreProductPackageUniqueCodeService;
 import com.fs.store.service.IFsStoreProductService;
 import com.fs.store.vo.FsStoreProductPacketVO;
+import com.fs.wx.miniapp.config.WxMaProperties;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
@@ -35,12 +48,24 @@ import java.util.List;
 @RequestMapping(value="/app/storeProductPackage")
 public class StoreProductPackageController extends  AppBaseController {
 
+    private static final Logger log = LoggerFactory.getLogger(StoreProductPackageController.class);
+
     @Autowired
     private IFsStoreProductPackageService productPackageService;
     @Autowired
     private IFsStoreProductAttrValueService attrValueService;
     @Autowired
     private IFsStoreProductService storeProductService;
+    @Autowired
+    private IFsStoreProductPackageUniqueCodeService productPackageUniqueCodeService;
+    @Autowired
+    private ICompanyService companyService;
+    @Autowired
+    private ICompanyUserService companyUserService;
+    @Autowired
+    private WxMaProperties properties;
+    @Autowired
+    private RedisCache redisCache;
     @Login
     @ApiOperation("获取套餐列表")
     @GetMapping("/getStoreProductPackage")
@@ -82,4 +107,102 @@ public class StoreProductPackageController extends  AppBaseController {
         return R.ok().put("data",storeProductPackage);
     }
 
+    @ApiOperation("套餐urlScheme")
+    @GetMapping("/getPackageAppletScheme")
+    public R getPackageAppletScheme(@RequestParam("packageId") Long packageId,
+                                    @RequestParam("companyId") Long companyId,
+                                    @RequestParam("companyUserId") Long companyUserId,
+                                    @RequestParam("path") String path) {
+        try {
+            if (StringUtils.isEmpty(path)) {
+                return R.error("path不能为空");
+            }
+            FsStoreProductPackage storeProductPackage = productPackageService.selectFsStoreProductPackageById(packageId);
+            if (storeProductPackage == null) {
+                return R.error("套餐不存在");
+            }
+            Company company = companyService.selectCompanyById(companyId);
+            if (company == null) {
+                return R.error("公司不存在");
+            }
+            CompanyUser companyUser = companyUserService.selectCompanyUserById(companyUserId);
+            if (companyUser == null) {
+                return R.error("未查询到销售信息,链接生成失败");
+            }
+            if (!companyId.equals(companyUser.getCompanyId())) {
+                return R.error("销售与公司信息不匹配");
+            }
+
+            R uniqueCodeResult = productPackageUniqueCodeService.createUniqueCode(packageId, companyId, companyUserId);
+            if (!uniqueCodeResult.isSuccess()) {
+                return uniqueCodeResult;
+            }
+            String uniqueCode = (String) uniqueCodeResult.get("uniqueCode");
+
+            String queryParam = "packageId=" + packageId + "&companyId=" + companyId + "&companyUserId=" + companyUserId + "&shareUniqueCode=" + uniqueCode;
+            String pageUrl = path.contains("?") ? path + "&" + queryParam : path + "?" + queryParam;
+            String appId = properties.getConfigs().get(0).getAppid();
+            String secret = properties.getConfigs().get(0).getSecret();
+
+            String cacheKey = "wx:access_token:" + appId;
+            String access_token = redisCache.getCacheObject(cacheKey);
+
+            if (StringUtils.isEmpty(access_token)) {
+                String rspStr = HttpUtils.sendGet("https://api.weixin.qq.com/cgi-bin/token", "grant_type=client_credential&" + "appid=" + appId + "&secret=" + secret);
+                JSONObject obj = JSONObject.parseObject(rspStr);
+                access_token = obj.getString("access_token");
+
+                if (StringUtils.isEmpty(access_token)) {
+                    log.error("获取微信 access_token 失败: {}", obj);
+                    return R.error("获取微信 access_token 失败");
+                }
+
+                redisCache.setCacheObject(cacheKey, access_token, 7200, java.util.concurrent.TimeUnit.SECONDS);
+                log.info("微信 access_token 已刷新并缓存,appId: {}", appId);
+            } else {
+                log.debug("从 Redis 缓存中获取 access_token,appId: {}", appId);
+            }
+
+            JSONObject jump_wxaObj = new JSONObject();
+            jump_wxaObj.put("page_url", pageUrl);
+            String paramStr = jump_wxaObj.toJSONString();
+            String postStr = HttpUtils.sendPost("https://api.weixin.qq.com/wxa/genwxashortlink?access_token=" + access_token, paramStr);
+            JSONObject obj = JSONObject.parseObject(postStr);
+
+            if (obj != null && (obj.getInteger("errcode") != null && obj.getInteger("errcode") == 40001)) {
+                log.warn("access_token 已失效,清除缓存并重新获取,appId: {}", appId);
+                redisCache.deleteObject(cacheKey);
+                String rspStr = HttpUtils.sendGet("https://api.weixin.qq.com/cgi-bin/token", "grant_type=client_credential&" + "appid=" + appId + "&secret=" + secret);
+                JSONObject tokenObj = JSONObject.parseObject(rspStr);
+                access_token = tokenObj.getString("access_token");
+                if (StringUtils.isNotEmpty(access_token)) {
+                    redisCache.setCacheObject(cacheKey, access_token, 7200, java.util.concurrent.TimeUnit.SECONDS);
+                    postStr = HttpUtils.sendPost("https://api.weixin.qq.com/wxa/genwxashortlink?access_token=" + access_token, paramStr);
+                    obj = JSONObject.parseObject(postStr);
+                }
+            }
+
+            return R.ok().put("result", obj).put("uniqueCode", uniqueCode);
+        } catch (Exception e) {
+            log.error("生成小程序 Scheme 失败", e);
+            return R.error("操作失败");
+        }
+    }
+
+    @Login
+    @ApiOperation("生成套餐分享唯一编码")
+    @PostMapping("/createUniqueCode")
+    public R createUniqueCode(@RequestParam("packageId") Long packageId,
+                              @RequestParam("companyId") Long companyId,
+                              @RequestParam("companyUserId") Long companyUserId) {
+        return productPackageUniqueCodeService.createUniqueCode(packageId, companyId, companyUserId);
+    }
+
+    @Login
+    @ApiOperation("校验套餐分享唯一编码是否已使用")
+    @GetMapping("/checkUniqueCode")
+    public R checkUniqueCode(@RequestParam("uniqueCode") String uniqueCode) {
+        return productPackageUniqueCodeService.checkUniqueCode(uniqueCode);
+    }
+
 }

+ 1 - 1
fs-company-app/src/main/java/com/fs/core/datasource/DynamicDataSourceContextHolder.java

@@ -23,7 +23,7 @@ public class DynamicDataSourceContextHolder
      */
     public static void setDataSourceType(String dsType)
     {
-        log.info("切换到{}数据源", dsType);
+        
         CONTEXT_HOLDER.set(dsType);
     }
 

+ 4 - 0
fs-company/src/main/java/com/fs/company/controller/live/LiveAfterSalesController.java

@@ -1,6 +1,7 @@
 package com.fs.company.controller.live;
 
 import com.fs.common.annotation.Log;
+import com.fs.common.config.LoginContextManager;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
@@ -55,6 +56,7 @@ public class LiveAfterSalesController extends BaseController
     public TableDataInfo list(LiveAfterSalesVo liveAfterSales)
     {
         startPage();
+        liveAfterSales.setCompanyId(LoginContextManager.getCurrentCompanyId());
         List<LiveAfterSalesVo> list = liveAfterSalesService.selectLiveAfterSalesVoList(liveAfterSales);
         for (LiveAfterSalesVo liveAfterSalesVo : list) {
             liveAfterSalesVo.setUserPhone(ParseUtils.parsePhone(liveAfterSalesVo.getUserPhone()));
@@ -70,6 +72,8 @@ public class LiveAfterSalesController extends BaseController
     @GetMapping("/export")
     public AjaxResult export(LiveAfterSalesVo liveAfterSales)
     {
+        liveAfterSales.setCompanyId(LoginContextManager.getCurrentCompanyId());
+
         PageHelper.startPage(1, 10000, "");
         List<LiveAfterSalesVo> list = liveAfterSalesService.selectLiveAfterSalesVoList(liveAfterSales);
         for (LiveAfterSalesVo liveAfterSalesVo : list) {

+ 27 - 4
fs-company/src/main/java/com/fs/company/controller/live/LiveController.java

@@ -22,6 +22,7 @@ import com.fs.live.service.ILiveService;
 import com.fs.live.vo.LiveListVo;
 import com.fs.system.oss.CloudStorageService;
 import com.fs.system.oss.OSSFactory;
+import com.fs.wx.miniapp.config.WxMaProperties;
 import com.google.common.reflect.TypeToken;
 import com.google.gson.Gson;
 import io.swagger.annotations.ApiOperation;
@@ -51,6 +52,8 @@ public class LiveController extends BaseController
     private TokenService tokenService;
     @Autowired
     private ILiveCompanyCodeService liveCompanyCodeService;
+    @Autowired
+    private WxMaProperties wxMaProperties;
 
     /**
      * 查询直播列表
@@ -254,7 +257,7 @@ public class LiveController extends BaseController
     /**
      * 批量删除视频
      */
-    @PreAuthorize("@ss.hasPermi('live:live:edit')")
+    @PreAuthorize("@ss.hasPermi('live:live:remove')")
     @PostMapping("/handleDeleteSelected")
     public R handleDeleteSelected(@RequestBody LiveListVo listVo) {
         setListCompanyId(listVo);
@@ -312,12 +315,20 @@ public class LiveController extends BaseController
     @GetMapping("/getWxaCodeUnLimit")
     @PreAuthorize("@ss.hasPermi('live:live:edit')")
     public R getWxaCodeUnLimit(@RequestParam(value = "liveId") Long liveId) {
+        // 从配置文件读取微信小程序配置
+        if (wxMaProperties == null || wxMaProperties.getConfigs() == null || wxMaProperties.getConfigs().isEmpty()) {
+            return R.error("微信小程序配置未找到");
+        }
+        WxMaProperties.Config config = wxMaProperties.getConfigs().get(0);
+        if (config == null || config.getAppid() == null || config.getSecret() == null) {
+            return R.error("微信小程序appid或secret未配置");
+        }
+
         String url="https://api.weixin.qq.com/cgi-bin/stable_token";
         HashMap<String, String> map = new HashMap<>();
         map.put("grant_type","client_credential");
-        // 芳华惠选
-        map.put("appid","wx4d225cc86cc7885d");
-        map.put("secret","f938f86cfebde0f4d34dad3b0d81b974");
+        map.put("appid", config.getAppid());
+        map.put("secret", config.getSecret());
         String accessToken = HttpUtils.endApi(url, null, map);
         // 创建Gson对象
         Gson gson = new Gson();
@@ -359,4 +370,16 @@ public class LiveController extends BaseController
 
         return R.ok().put("data", exist);
     }
+
+    /**
+     * 查询 his 平台大礼品奖励配置(rewardType=6)
+     */
+    @PreAuthorize("@ss.hasPermi('live:live:list')")
+    @GetMapping("/bigGiftReward/list")
+    public TableDataInfo listBigGiftReward(@RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
+                                           @RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
+                                           @RequestParam(value = "name", required = false) String name,
+                                           @RequestParam(value = "status", required = false) Long status) {
+        return liveService.listBigGiftRewards(pageNum, pageSize, name, status);
+    }
 }

+ 105 - 0
fs-company/src/main/java/com/fs/company/controller/live/LiveDataController.java

@@ -3,15 +3,21 @@ package com.fs.company.controller.live;
 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.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.CompanyUser;
 import com.fs.core.security.LoginUser;
 import com.fs.core.security.SecurityUtils;
 import com.fs.core.web.service.TokenService;
 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;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
@@ -35,6 +41,105 @@ public class LiveDataController extends BaseController
     @Autowired
     private TokenService tokenService;
 
+    /**
+     * 查询新直播数据列表
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:list')")
+    @PostMapping("/listLiveData")
+    public R listLiveData(@RequestBody LiveDataParam param, HttpServletRequest request)
+    {
+        param.setCompanyId(tokenService.getLoginUser(request).getUser().getCompanyId());
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        return liveDataService.listLiveData(param);
+    }
+
+    /**
+     * 导出直播数据列表(与列表查询相同筛选条件)
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:export')")
+    @Log(title = "直播数据列表", businessType = BusinessType.EXPORT)
+    @PostMapping("/exportLiveData")
+    public AjaxResult exportLiveData(@RequestBody LiveDataParam param, HttpServletRequest request) {
+        param.setCompanyId(tokenService.getLoginUser(request).getUser().getCompanyId());
+        List<LiveDataListVo> list = liveDataService.exportLiveDataList(param);
+        if (list == null || list.isEmpty()) {
+            return AjaxResult.error("没有可导出的数据");
+        }
+        ExcelUtil<LiveDataListVo> util = new ExcelUtil<>(LiveDataListVo.class);
+        return util.exportExcel(list, "直播数据");
+    }
+
+    /**
+     * 查询直播间详情数据(SQL方式)
+     * @param liveId 直播间ID
+     * @return 详情数据
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @GetMapping("/getLiveDataDetailBySql")
+    public R getLiveDataDetailBySql(@RequestParam Long liveId) {
+        return liveDataService.getLiveDataDetailBySql(liveId);
+    }
+
+    /**
+     * 查询直播间用户详情列表(SQL方式)
+     * @param liveId 直播间ID
+     * @param pageNum 页码
+     * @param pageSize 每页数量
+     * @param request HTTP请求
+     * @return 用户详情列表
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @GetMapping("/getLiveUserDetailListBySql")
+    public R getLiveUserDetailListBySql(@RequestParam Long liveId,
+                                        @RequestParam(required = false, defaultValue = "1") Integer pageNum,
+                                        @RequestParam(required = false, defaultValue = "10") Integer pageSize,
+                                        HttpServletRequest request) {
+        CompanyUser user = tokenService.getLoginUser(request).getUser();
+        PageHelper.startPage(pageNum, pageSize);
+        return liveDataService.getLiveUserDetailListBySql(liveId,user.getCompanyId(),user.getUserId());
+    }
+
+    /**
+     * 查询直播间详情数据(查询数据服务器处理方式)
+     * @param liveId 直播间ID
+     * @return 详情数据
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @GetMapping("/getLiveDataDetailByServer")
+    public R getLiveDataDetailByServer(@RequestParam Long liveId) {
+        return liveDataService.getLiveDataDetailByServer(liveId);
+    }
+
+    /**
+     * 查询直播间用户详情列表(查询数据服务器处理方式)
+     * @param liveId 直播间ID
+     * @return 用户详情列表
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @GetMapping("/getLiveUserDetailListByServer")
+    public R getLiveUserDetailListByServer(@RequestParam Long liveId) {
+        return liveDataService.getLiveUserDetailListByServer(liveId);
+    }
+
+
+    /**
+     * 导出直播间用户详情数据
+     * @param liveId 直播间ID
+     * @return Excel文件
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:export')")
+    @Log(title = "直播间用户详情", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportLiveUserDetail")
+    public AjaxResult exportLiveUserDetail(@RequestParam Long liveId, HttpServletRequest request) {
+        CompanyUser user = tokenService.getLoginUser(request).getUser();
+        List<LiveUserDetailExportVO> list = liveDataService.exportLiveUserDetail(liveId,user.getCompanyId(),user.getUserId());
+        if (list == null || list.isEmpty()) {
+            return AjaxResult.error("未找到用户详情数据");
+        }
+
+        ExcelUtil<LiveUserDetailExportVO> util = new ExcelUtil<>(LiveUserDetailExportVO.class);
+        return util.exportExcel(list, "直播间用户详情数据");
+    }
     /**
      * 查询直播数据列表
      */

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

@@ -14,6 +14,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.Arrays;
 import java.util.List;
 
 /**
@@ -95,7 +96,13 @@ public class LiveMsgController extends BaseController
 	@DeleteMapping("/{msgIds}")
     public AjaxResult remove(@PathVariable Long[] msgIds)
     {
-        return toAjax(liveMsgService.deleteLiveMsgByMsgIds(msgIds));
+        if (msgIds != null && msgIds.length > 0) {
+            Long[] realIds = Arrays.stream(msgIds).filter(id -> id != null && id > 0).toArray(Long[]::new);
+            if (realIds.length > 0) {
+                liveMsgService.deleteLiveMsgByMsgIds(realIds);
+            }
+        }
+        return AjaxResult.success();
     }
 
 }

+ 28 - 11
fs-company/src/main/java/com/fs/company/controller/live/LiveOrderController.java

@@ -2,6 +2,7 @@ package com.fs.company.controller.live;
 
 import cn.hutool.core.util.StrUtil;
 import com.fs.common.annotation.Log;
+import com.fs.common.config.LoginContextManager;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
@@ -28,7 +29,9 @@ import com.fs.live.service.ILiveOrderLogsService;
 import com.fs.live.service.ILiveOrderPaymentService;
 import com.fs.live.service.ILiveOrderService;
 import com.fs.live.vo.LiveGoodsVo;
+import com.fs.live.vo.LiveOrderQueryVO;
 import com.fs.live.vo.LiveOrderVo;
+import org.springframework.beans.BeanUtils;
 import com.fs.store.domain.*;
 import com.fs.store.param.FsStoreOrderBindCustomerParam;
 import com.fs.store.param.FsStoreOrderExpressParam;
@@ -135,9 +138,13 @@ public class LiveOrderController extends BaseController
     @PreAuthorize("@ss.hasPermi('live:liveOrder:queryAddress')")
     public R getAddress(@PathVariable("id") Long id)
     {
-        LiveOrder order = liveOrderService.selectLiveOrderByOrderId(String.valueOf(id));
-        String address = order.getUserAddress();
-        return R.ok().put("address",address);
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if (loginUser.getUser().getUserType().equals("00")){
+            LiveOrder order = liveOrderService.selectLiveOrderByOrderId(String.valueOf(id));
+            String address = order.getUserAddress();
+            return R.ok().put("address",address);
+        }
+        return R.error("无权查看");
     }
     @GetMapping(value = "/queryPhone/{id}")
     @PreAuthorize("@ss.hasPermi('live:liveOrder:queryPhone')")
@@ -185,14 +192,15 @@ public class LiveOrderController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('live:liveOrder:list')")
     @GetMapping("/list")
-    public TableDataInfo list(LiveOrder liveOrder)
+    public TableDataInfo list(LiveOrderQueryVO liveOrder)
     {
+        logger.info("查询订单列表: {}",liveOrder);
         startPage();
-        Long companyId = SecurityUtils.getLoginUser().getCompany().getCompanyId();
-        liveOrder.setCompanyId(companyId);
+        liveOrder.setCompanyId(LoginContextManager.getCurrentCompanyId());
         List<LiveOrder> list = liveOrderService.selectLiveOrderList(liveOrder);
         for (LiveOrder vo : list){
             vo.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
+            vo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
         }
         return getDataTable(list);
     }
@@ -203,15 +211,20 @@ public class LiveOrderController extends BaseController
     @PreAuthorize("@ss.hasPermi('live:liveOrder:export')")
     @Log(title = "订单", businessType = BusinessType.EXPORT)
     @GetMapping("/export")
-    public AjaxResult export(LiveOrder liveOrder)
+    public AjaxResult export(LiveOrderQueryVO liveOrder)
     {
-        List<LiveOrder> list = liveOrderService.selectLiveOrderList(liveOrder);
+        liveOrder.setCompanyId(LoginContextManager.getCurrentCompanyId());
+        List<LiveOrder> list = liveOrderService.selectLiveOrderListExport(liveOrder);
+        List<LiveOrderQueryVO> exportList = new java.util.ArrayList<>();
         for (LiveOrder order : list) {
             order.setUserPhone(ParseUtils.parsePhone(order.getUserPhone()));
             order.setUserAddress(ParseUtils.parseAddress(order.getUserAddress()));
+            LiveOrderQueryVO exportVo = new LiveOrderQueryVO();
+            BeanUtils.copyProperties(order, exportVo);
+            exportList.add(exportVo);
         }
-        ExcelUtil<LiveOrder> util = new ExcelUtil<LiveOrder>(LiveOrder.class);
-        return util.exportExcel(list, "订单数据");
+        ExcelUtil<LiveOrderQueryVO> util = new ExcelUtil<>(LiveOrderQueryVO.class);
+        return util.exportExcel(exportList, "订单数据");
     }
 
     /**
@@ -221,7 +234,11 @@ public class LiveOrderController extends BaseController
     @GetMapping(value = "/info/{orderId}")
     public AjaxResult getInfo(@PathVariable("orderId") String orderId)
     {
-        return AjaxResult.success(liveOrderService.selectLiveOrderByOrderId(orderId));
+        LiveOrder liveOrder = liveOrderService.selectLiveOrderByOrderId(orderId);
+            liveOrder.setUserPhone(ParseUtils.parsePhone(liveOrder.getUserPhone()));
+            liveOrder.setUserAddress(ParseUtils.parseAddress(liveOrder.getUserAddress()));
+
+        return AjaxResult.success(liveOrder);
     }
 
     /**

+ 1 - 1
fs-company/src/main/java/com/fs/core/datasource/DynamicDataSourceContextHolder.java

@@ -23,7 +23,7 @@ public class DynamicDataSourceContextHolder
      */
     public static void setDataSourceType(String dsType)
     {
-        log.info("切换到{}数据源", dsType);
+        
         CONTEXT_HOLDER.set(dsType);
     }
 

+ 15 - 11
fs-company/src/main/java/com/fs/store/controller/FsStoreOrderController.java

@@ -28,9 +28,11 @@ import com.fs.store.param.FsStoreOrderCreateUserParam;
 import com.fs.store.param.FsStoreOrderFinishParam;
 import com.fs.store.param.FsStoreOrderParam;
 import com.fs.store.service.*;
+import com.fs.store.vo.FsStoreOrderCompanyExportVO;
 import com.fs.store.vo.FsStoreOrderExportVO;
 import com.fs.store.vo.FsStoreOrderItemExportVO;
 import com.fs.store.vo.FsStoreOrderVO;
+import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
@@ -157,23 +159,25 @@ public class FsStoreOrderController extends BaseController
             param.setDeliveryImportTimeList(param.getDeliveryImportTimeRange().split("--"));
         }
         List<FsStoreOrderExportVO> list = fsStoreOrderService.selectFsStoreOrderListVOByExport(param);
-        //对手机号脱敏
-        if(list!=null){
-            for(FsStoreOrderExportVO vo:list){
+        List<FsStoreOrderCompanyExportVO> exportList = new java.util.ArrayList<>();
+        if (list != null) {
+            for (FsStoreOrderExportVO vo : list) {
+                FsStoreOrderCompanyExportVO exportVo = new FsStoreOrderCompanyExportVO();
+                BeanUtils.copyProperties(vo, exportVo);
                 if (vo.getPhone() != null) {
-                    vo.setPhone(vo.getPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                    exportVo.setPhone(vo.getPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
                 }
                 if (vo.getUserPhone() != null) {
-                    vo.setUserPhone(vo.getUserPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                    exportVo.setUserPhone(vo.getUserPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
                 }
-                if (vo.getUserAddress()!=null){
-                    vo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
+                if (vo.getUserAddress() != null) {
+                    exportVo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
                 }
-
+                exportList.add(exportVo);
             }
         }
-        ExcelUtil<FsStoreOrderExportVO> util = new ExcelUtil<FsStoreOrderExportVO>(FsStoreOrderExportVO.class);
-        return util.exportExcel(list,"订单数据");
+        ExcelUtil<FsStoreOrderCompanyExportVO> util = new ExcelUtil<>(FsStoreOrderCompanyExportVO.class);
+        return util.exportExcel(exportList, "订单数据");
     }
 
 
@@ -239,7 +243,7 @@ public class FsStoreOrderController extends BaseController
         if(StringUtils.isNotEmpty(order.getDeliveryId())){
 
             String lastFourNumber = "";
-            if (order.getDeliverySn().equals(ShipperCodeEnum.SF.getValue())) {
+            if (order.getDeliverySn().equals(ShipperCodeEnum.SF.getValue()) || order.getDeliverySn().equals(ShipperCodeEnum.ZTO.getValue())) {
                 lastFourNumber = order.getUserPhone();
                 if (lastFourNumber.length() == 11) {
                     lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);

+ 10 - 0
fs-company/src/main/resources/application-dev.yml

@@ -79,3 +79,13 @@ spring:
                         multi-statement-allow: true
 
 
+rocketmq:
+    name-server: rmq-16b45v4p9n.rocketmq.cd.qcloud.tencenttdmq.com:8080 # RocketMQ NameServer 地址
+    producer:
+        group: my-producer-group
+        access-key: ak16b45v4p9n150d89395b3c # 替换为实际的 accessKey
+        secret-key: sk370fb48d869b152b # 替换为实际的 secretKey
+    consumer:
+        group: common-group
+        access-key: ak16b45v4p9n150d89395b3c # 替换为实际的 accessKey
+        secret-key: sk370fb48d869b152b # 替换为实际的 secretKey

+ 184 - 0
fs-live-mq/pom.xml

@@ -0,0 +1,184 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>fs</artifactId>
+        <groupId>com.fs</groupId>
+        <version>1.1.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>fs-live-mq</artifactId>
+
+    <properties>
+        <maven.compiler.source>8</maven.compiler.source>
+        <maven.compiler.target>8</maven.compiler.target>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-thymeleaf</artifactId>
+        </dependency>
+        <!-- spring-boot-devtools -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-devtools</artifactId>
+            <optional>true</optional> <!-- 表示依赖不会传递 -->
+        </dependency>
+
+
+        <!-- swagger2-->
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger2</artifactId>
+        </dependency>
+
+        <!-- swagger2-UI-->
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger-ui</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>swagger-bootstrap-ui</artifactId>
+            <version>1.9.3</version>
+        </dependency>
+        <!-- Mysql驱动包 -->
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+        </dependency>
+
+        <!-- SpringBoot Web容器 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <!-- SpringBoot 拦截器 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+
+        <!-- 阿里数据库连接池 -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>druid-spring-boot-starter</artifactId>
+        </dependency>
+        <!-- 系统模块-->
+        <dependency>
+            <groupId>com.fs</groupId>
+            <artifactId>fs-service-system</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.github.tencentyun</groupId>
+            <artifactId>tls-sig-api-v2</artifactId>
+            <version>2.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.javen205</groupId>
+            <artifactId>IJPay-All</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.google.zxing</groupId>
+            <artifactId>javase</artifactId>
+            <version>3.4.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>1.7.30</version>
+            <scope>provided</scope>
+        </dependency>
+
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <!-- Redisson -->
+        <dependency>
+            <groupId>org.redisson</groupId>
+            <artifactId>redisson</artifactId>
+            <version>3.13.6</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.rocketmq</groupId>
+            <artifactId>rocketmq-spring-boot-starter</artifactId>
+            <version>2.2.3</version>
+        </dependency>
+
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.1.1.RELEASE</version>
+                <configuration>
+                    <fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-war-plugin</artifactId>
+                <version>3.1.0</version>
+                <configuration>
+                    <failOnMissingWebXml>false</failOnMissingWebXml>
+                    <warName>${project.artifactId}</warName>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>com.spotify</groupId>
+                <artifactId>docker-maven-plugin</artifactId>
+                <version>1.0.0</version>
+                <configuration>
+                    <imageName>${project.artifactId}:${project.version}</imageName>
+                    <baseImage>kdvolder/jdk8</baseImage>
+                    <maintainer>docker_maven docker_maven@email.com</maintainer>
+                    <workdir>/</workdir>
+                    <cmd>["java", "-version"]</cmd>
+                    <entryPoint>["java", "-jar", "${project.build.finalName}.jar"]</entryPoint>
+                    <!-- 这里是复制 jar 包到 docker 容器指定目录配置 -->
+                    <resources>
+                        <resource>
+                            <targetPath>/</targetPath>
+                            <directory>${project.build.directory}</directory>
+                            <include>${project.build.finalName}.jar</include>
+                        </resource>
+                    </resources>
+                    <dockerHost>http://8.140.143.122:2375</dockerHost>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <compilerArgs>
+                        <arg>-parameters</arg>
+                    </compilerArgs>
+                </configuration>
+            </plugin>
+        </plugins>
+        <finalName>${project.artifactId}</finalName>
+    </build>
+</project>

+ 30 - 0
fs-live-mq/src/main/java/com/fs/LiveSocketMqApplication.java

@@ -0,0 +1,30 @@
+package com.fs;
+
+import org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Import;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+/**
+ * 启动程序
+ */
+@EnableCaching
+@EnableAsync
+@Import({ RocketMQAutoConfiguration.class })
+@EnableScheduling
+@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
+public class LiveSocketMqApplication
+{
+    public static void main(String[] args)
+    {
+        // System.setProperty("spring.devtools.restart.enabled", "false");
+        SpringApplication.run(LiveSocketMqApplication.class, args);
+        System.out.println("Live-mq启动成功 \n" );
+    }
+
+
+}

+ 90 - 0
fs-live-mq/src/main/java/com/fs/core/aspectj/LiveWatchUserAspect.java

@@ -0,0 +1,90 @@
+//package com.fs.core.aspectj;
+//
+//import cn.hutool.core.util.ObjectUtil;
+//import com.fs.common.core.redis.RedisUtil;
+//import com.fs.live.domain.LiveWatchUser;
+//import com.fs.live.service.ILiveWatchUserService;
+//import lombok.extern.slf4j.Slf4j;
+//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.stereotype.Component;
+//
+//import java.util.Arrays;
+//import java.util.HashSet;
+//import java.util.Set;
+//
+//@Aspect
+//@Component
+//@Slf4j
+//public class LiveWatchUserAspect {
+//
+//    @Autowired
+//    private RedisUtil redisUtil;
+//
+//    @Autowired
+//    private ILiveWatchUserService liveWatchUserService;
+//
+//    @AfterReturning(pointcut = "execution(* com.fs.live.service.impl.LiveWatchUserServiceImpl.insertLiveWatchUser(..)) || " +
+//            "execution(* com.fs.live.service.impl.LiveWatchUserServiceImpl.updateLiveWatchUser(..)) || " +
+//            "execution(* com.fs.live.service.impl.LiveWatchUserServiceImpl.deleteLiveWatchUserById(..)) || " +
+//            "execution(* com.fs.live.service.impl.LiveWatchUserServiceImpl.deleteLiveWatchUserByIds(..))",
+//            returning = "result")
+//    public void afterLiveWatchUserOperation(JoinPoint joinPoint, Object result) {
+//        try {
+//            String methodName = joinPoint.getSignature().getName();
+//            Object[] args = joinPoint.getArgs();
+//            // 提取liveId并处理缓存更新
+//            Set<Long> liveIds = extractLiveIds(methodName, args);
+//            for (Long liveId : liveIds) {
+//                liveWatchUserService.asyncToCache(liveId);
+//            }
+//        } catch (Exception e) {
+//            log.error("执行直播观看用户变更后逻辑失败", e);
+//        }
+//    }
+//
+//    private Set<Long> extractLiveIds(String methodName, Object[] args) {
+//        Set<Long> liveIds = new HashSet<>();
+//        if (args == null || args.length == 0) {
+//            return liveIds;
+//        }
+//        switch (methodName) {
+//            case "insertLiveWatchUser":
+//            case "updateLiveWatchUser":
+//                // 参数是LiveWatchUser对象
+//                if (args[0] instanceof LiveWatchUser) {
+//                    LiveWatchUser liveWatchUser = (LiveWatchUser) args[0];
+//                    if (liveWatchUser.getLiveId() != null) {
+//                        liveIds.add(liveWatchUser.getLiveId());
+//                    }
+//                }
+//                break;
+//            case "deleteLiveWatchUserById":
+//                // 参数是Long类型的id,需要先查询获取liveId
+//                if (args[0] instanceof Long) {
+//                    LiveWatchUser liveWatchUser = liveWatchUserService.selectLiveWatchUserById((Long) args[0]);
+//                    if (ObjectUtil.isNotEmpty(liveWatchUser)) {
+//                        liveIds.add(liveWatchUser.getLiveId());
+//                    }
+//                }
+//                break;
+//            case "deleteLiveWatchUserByIds":
+//                // 参数是Long[]数组
+//                if (args[0] instanceof Long[]) {
+//                    Long[] ids = (Long[]) args[0];
+//                    LiveWatchUser liveWatchUser = liveWatchUserService.selectLiveWatchUserById(ids[0]);
+//                    if (ObjectUtil.isNotEmpty(liveWatchUser)) {
+//                        liveIds.add(liveWatchUser.getLiveId());
+//                    }
+//                }
+//                break;
+//            default:
+//                log.warn("未处理的方法: {}", methodName);
+//        }
+//        return liveIds;
+//    }
+//
+//
+//}

+ 58 - 0
fs-live-mq/src/main/java/com/fs/core/aspectj/lock/DistributeLock.java

@@ -0,0 +1,58 @@
+package com.fs.core.aspectj.lock;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 分布式锁注解
+ *
+ * @author Hollis
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface DistributeLock {
+
+    /**
+     * 锁的场景
+     *
+     * @return
+     */
+    public String scene();
+
+    /**
+     * 加锁的key,优先取key(),如果没有,则取keyExpression()
+     *
+     * @return
+     */
+    public String key() default DistributeLockConstant.NONE_KEY;
+
+    /**
+     * SPEL表达式:
+     * <pre>
+     *     #id
+     *     #insertResult.id
+     * </pre>
+     *
+     * @return
+     */
+    public String keyExpression() default DistributeLockConstant.NONE_KEY;
+
+    /**
+     * 超时时间,毫秒
+     * 默认情况下不设置超时时间,会自动续期
+     *
+     * @return
+     */
+    public int expireTime() default DistributeLockConstant.DEFAULT_EXPIRE_TIME;
+
+    public String errorMsg() default DistributeLockConstant.ERROR_MSG;
+
+    /**
+     * 加锁等待时长,毫秒
+     * 默认情况下不设置等待时长,会一直等待直到获取到锁
+     * @return
+     */
+    public int waitTime() default DistributeLockConstant.DEFAULT_WAIT_TIME;
+}

+ 113 - 0
fs-live-mq/src/main/java/com/fs/core/aspectj/lock/DistributeLockAspect.java

@@ -0,0 +1,113 @@
+package com.fs.core.aspectj.lock;
+
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.StandardReflectionParameterNameDiscoverer;
+import org.springframework.core.annotation.Order;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.Expression;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+import java.util.concurrent.TimeUnit;
+
+@Aspect
+@Component
+@Order(Integer.MIN_VALUE + 1)
+public class DistributeLockAspect {
+
+    private RedissonClient redissonClient;
+
+    public DistributeLockAspect(RedissonClient redissonClient) {
+        this.redissonClient = redissonClient;
+    }
+
+    private static final Logger LOG = LoggerFactory.getLogger(DistributeLockAspect.class);
+
+    @Around("@annotation(com.fs.core.aspectj.lock.DistributeLock)")
+    public Object process(ProceedingJoinPoint pjp) throws Throwable {
+        Object response = null;
+        Method method = ((MethodSignature) pjp.getSignature()).getMethod();
+        DistributeLock distributeLock = method.getAnnotation(DistributeLock.class);
+
+        String key = distributeLock.key();
+        if (DistributeLockConstant.NONE_KEY.equals(key)) {
+            if (DistributeLockConstant.NONE_KEY.equals(distributeLock.keyExpression())) {
+                throw new DistributeLockException("no lock key found...");
+            }
+            SpelExpressionParser parser = new SpelExpressionParser();
+            Expression expression = parser.parseExpression(distributeLock.keyExpression());
+
+            EvaluationContext context = new StandardEvaluationContext();
+            // 获取参数值
+            Object[] args = pjp.getArgs();
+
+            // 获取运行时参数的名称
+            StandardReflectionParameterNameDiscoverer discoverer
+                    = new StandardReflectionParameterNameDiscoverer();
+            String[] parameterNames = discoverer.getParameterNames(method);
+
+            // 将参数绑定到context中
+            if (parameterNames != null) {
+                for (int i = 0; i < parameterNames.length; i++) {
+                    context.setVariable(parameterNames[i], args[i]);
+                }
+            }
+
+            // 解析表达式,获取结果
+            key = String.valueOf(expression.getValue(context));
+        }
+
+        String scene = distributeLock.scene();
+
+        String lockKey = scene + "#" + key;
+
+        int expireTime = distributeLock.expireTime();
+        int waitTime = distributeLock.waitTime();
+        RLock rLock= redissonClient.getLock(lockKey);
+        try {
+            boolean lockResult = false;
+            if (waitTime == DistributeLockConstant.DEFAULT_WAIT_TIME) {
+                if (expireTime == DistributeLockConstant.DEFAULT_EXPIRE_TIME) {
+//                    LOG.info(String.format("lock for key : %s", lockKey));
+                    rLock.lock();
+                } else {
+//                    LOG.info(String.format("lock for key : %s , expire : %s", lockKey, expireTime));
+                    rLock.lock(expireTime, TimeUnit.MILLISECONDS);
+                }
+                lockResult = true;
+            } else {
+                if (expireTime == DistributeLockConstant.DEFAULT_EXPIRE_TIME) {
+//                    LOG.info(String.format("try lock for key : %s , wait : %s", lockKey, waitTime));
+                    lockResult = rLock.tryLock(waitTime, TimeUnit.MILLISECONDS);
+                } else {
+//                    LOG.info(String.format("try lock for key : %s , expire : %s , wait : %s", lockKey, expireTime, waitTime));
+                    lockResult = rLock.tryLock(waitTime, expireTime, TimeUnit.MILLISECONDS);
+                }
+            }
+
+            if (!lockResult) {
+//                LOG.warn(String.format("lock failed for key : %s , expire : %s", lockKey, expireTime));
+                throw new DistributeLockException(distributeLock.errorMsg());
+            }
+
+
+//            LOG.info(String.format("lock success for key : %s , expire : %s", lockKey, expireTime));
+            response = pjp.proceed();
+        }  finally {
+            if (rLock.isHeldByCurrentThread()) {
+                rLock.unlock();
+//                LOG.info(String.format("unlock for key : %s , expire : %s", lockKey, expireTime));
+            }
+        }
+        return response;
+    }
+}

+ 13 - 0
fs-live-mq/src/main/java/com/fs/core/aspectj/lock/DistributeLockConstant.java

@@ -0,0 +1,13 @@
+package com.fs.core.aspectj.lock;
+
+public class DistributeLockConstant {
+
+    public static final String NONE_KEY = "NONE";
+
+    public static final String DEFAULT_OWNER = "DEFAULT";
+
+    public static final int DEFAULT_EXPIRE_TIME = -1;
+
+    public static final int DEFAULT_WAIT_TIME = Integer.MAX_VALUE;
+    public static final String ERROR_MSG  = "请勿重复操作";
+}

+ 24 - 0
fs-live-mq/src/main/java/com/fs/core/aspectj/lock/DistributeLockException.java

@@ -0,0 +1,24 @@
+package com.fs.core.aspectj.lock;
+
+
+public class DistributeLockException extends RuntimeException {
+
+    public DistributeLockException() {
+    }
+
+    public DistributeLockException(String message) {
+        super(message);
+    }
+
+    public DistributeLockException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public DistributeLockException(Throwable cause) {
+        super(cause);
+    }
+
+    public DistributeLockException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+        super(message, cause, enableSuppression, writableStackTrace);
+    }
+}

+ 31 - 0
fs-live-mq/src/main/java/com/fs/core/config/ApplicationConfig.java

@@ -0,0 +1,31 @@
+package com.fs.core.config;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+
+import java.util.TimeZone;
+
+/**
+ * 程序注解配置
+ *
+ 
+ */
+@Configuration
+// 表示通过aop框架暴露该代理对象,AopContext能够访问
+@EnableAspectJAutoProxy(exposeProxy = true)
+// 指定要扫描的Mapper类的包的路径
+@MapperScan("com.fs.**.mapper")
+public class ApplicationConfig
+{
+    /**
+     * 时区配置
+     */
+    @Bean
+    public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization()
+    {
+        return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault());
+    }
+}

+ 123 - 0
fs-live-mq/src/main/java/com/fs/core/config/DruidConfig.java

@@ -0,0 +1,123 @@
+package com.fs.core.config;
+
+import com.alibaba.druid.pool.DruidDataSource;
+import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
+import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
+import com.alibaba.druid.util.Utils;
+import com.fs.common.enums.DataSourceType;
+import com.fs.common.utils.spring.SpringUtils;
+import com.fs.core.config.properties.DruidProperties;
+import com.fs.core.datasource.DynamicDataSource;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+
+import javax.servlet.*;
+import javax.sql.DataSource;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * druid 配置多数据源
+ * 
+
+ */
+@Configuration
+public class DruidConfig
+{
+    @Bean
+    @ConfigurationProperties("spring.datasource.druid.master")
+    public DataSource masterDataSource(DruidProperties druidProperties)
+    {
+        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
+        return druidProperties.dataSource(dataSource);
+    }
+
+    @Bean
+    @ConfigurationProperties("spring.datasource.druid.slave")
+    @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
+    public DataSource slaveDataSource(DruidProperties druidProperties)
+    {
+        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
+        return druidProperties.dataSource(dataSource);
+    }
+
+    @Bean(name = "dynamicDataSource")
+    @Primary
+    public DynamicDataSource dataSource(DataSource masterDataSource)
+    {
+        Map<Object, Object> targetDataSources = new HashMap<>();
+        targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
+        setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");
+        return new DynamicDataSource(masterDataSource, targetDataSources);
+    }
+    
+    /**
+     * 设置数据源
+     * 
+     * @param targetDataSources 备选数据源集合
+     * @param sourceName 数据源名称
+     * @param beanName bean名称
+     */
+    public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName)
+    {
+        try
+        {
+            DataSource dataSource = SpringUtils.getBean(beanName);
+            targetDataSources.put(sourceName, dataSource);
+        }
+        catch (Exception e)
+        {
+        }
+    }
+
+    /**
+     * 去除监控页面底部的广告
+     */
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Bean
+    @ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true")
+    public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties)
+    {
+        // 获取web监控页面的参数
+        DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
+        // 提取common.js的配置路径
+        String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
+        String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
+        final String filePath = "support/http/resources/js/common.js";
+        // 创建filter进行过滤
+        Filter filter = new Filter()
+        {
+            @Override
+            public void init(javax.servlet.FilterConfig filterConfig) throws ServletException
+            {
+            }
+            @Override
+            public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+                    throws IOException, ServletException
+            {
+                chain.doFilter(request, response);
+                // 重置缓冲区,响应头不会被重置
+                response.resetBuffer();
+                // 获取common.js
+                String text = Utils.readFromResource(filePath);
+                // 正则替换banner, 除去底部的广告信息
+                text = text.replaceAll("<a.*?banner\"></a><br/>", "");
+                text = text.replaceAll("powered.*?shrek.wang</a>", "");
+                response.getWriter().write(text);
+            }
+            @Override
+            public void destroy()
+            {
+            }
+        };
+        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
+        registrationBean.setFilter(filter);
+        registrationBean.addUrlPatterns(commonJsPattern);
+        return registrationBean;
+    }
+}

+ 72 - 0
fs-live-mq/src/main/java/com/fs/core/config/FastJson2JsonRedisSerializer.java

@@ -0,0 +1,72 @@
+package com.fs.core.config;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.parser.ParserConfig;
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.type.TypeFactory;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.serializer.SerializationException;
+import org.springframework.util.Assert;
+
+import java.nio.charset.Charset;
+
+/**
+ * Redis使用FastJson序列化
+ * 
+ 
+ */
+public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
+{
+    @SuppressWarnings("unused")
+    private ObjectMapper objectMapper = new ObjectMapper();
+
+    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
+
+    private Class<T> clazz;
+
+    static
+    {
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+    }
+
+    public FastJson2JsonRedisSerializer(Class<T> clazz)
+    {
+        super();
+        this.clazz = clazz;
+    }
+
+    @Override
+    public byte[] serialize(T t) throws SerializationException
+    {
+        if (t == null)
+        {
+            return new byte[0];
+        }
+        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
+    }
+
+    @Override
+    public T deserialize(byte[] bytes) throws SerializationException
+    {
+        if (bytes == null || bytes.length <= 0)
+        {
+            return null;
+        }
+        String str = new String(bytes, DEFAULT_CHARSET);
+
+        return JSON.parseObject(str, clazz);
+    }
+
+    public void setObjectMapper(ObjectMapper objectMapper)
+    {
+        Assert.notNull(objectMapper, "'objectMapper' must not be null");
+        this.objectMapper = objectMapper;
+    }
+
+    protected JavaType getJavaType(Class<?> clazz)
+    {
+        return TypeFactory.defaultInstance().constructType(clazz);
+    }
+}

+ 61 - 0
fs-live-mq/src/main/java/com/fs/core/config/FilterConfig.java

@@ -0,0 +1,61 @@
+package com.fs.core.config;
+
+import com.fs.common.filter.RepeatableFilter;
+import com.fs.common.filter.XssFilter;
+import com.fs.common.utils.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import javax.servlet.DispatcherType;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Filter配置
+ *
+ 
+ */
+@Configuration
+public class FilterConfig
+{
+    @Value("${xss.enabled}")
+    private String enabled;
+
+    @Value("${xss.excludes}")
+    private String excludes;
+
+    @Value("${xss.urlPatterns}")
+    private String urlPatterns;
+
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Bean
+    public FilterRegistrationBean xssFilterRegistration()
+    {
+        FilterRegistrationBean registration = new FilterRegistrationBean();
+        registration.setDispatcherTypes(DispatcherType.REQUEST);
+        registration.setFilter(new XssFilter());
+        registration.addUrlPatterns(StringUtils.split(urlPatterns, ","));
+        registration.setName("xssFilter");
+        registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
+        Map<String, String> initParameters = new HashMap<String, String>();
+        initParameters.put("excludes", excludes);
+        initParameters.put("enabled", enabled);
+        registration.setInitParameters(initParameters);
+        return registration;
+    }
+
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Bean
+    public FilterRegistrationBean someFilterRegistration()
+    {
+        FilterRegistrationBean registration = new FilterRegistrationBean();
+        registration.setFilter(new RepeatableFilter());
+        registration.addUrlPatterns("/*");
+        registration.setName("repeatableFilter");
+        registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
+        return registration;
+    }
+
+}

+ 109 - 0
fs-live-mq/src/main/java/com/fs/core/config/MyBatisConfig.java

@@ -0,0 +1,109 @@
+package com.fs.core.config;
+
+import org.apache.ibatis.io.VFS;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.mybatis.spring.SqlSessionFactoryBean;
+import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import org.springframework.core.io.DefaultResourceLoader;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
+import org.springframework.core.type.classreading.MetadataReader;
+import org.springframework.core.type.classreading.MetadataReaderFactory;
+import org.springframework.util.ClassUtils;
+
+import javax.sql.DataSource;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Mybatis支持*匹配扫描包
+ * 
+ 
+ */
+@Configuration
+public class MyBatisConfig
+{
+    @Autowired
+    private Environment env;
+
+    static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
+
+    public static String setTypeAliasesPackage(String typeAliasesPackage)
+    {
+        ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver();
+        MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);
+        List<String> allResult = new ArrayList<String>();
+        try
+        {
+            for (String aliasesPackage : typeAliasesPackage.split(","))
+            {
+                List<String> result = new ArrayList<String>();
+                aliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
+                        + ClassUtils.convertClassNameToResourcePath(aliasesPackage.trim()) + "/" + DEFAULT_RESOURCE_PATTERN;
+                Resource[] resources = resolver.getResources(aliasesPackage);
+                if (resources != null && resources.length > 0)
+                {
+                    MetadataReader metadataReader = null;
+                    for (Resource resource : resources)
+                    {
+                        if (resource.isReadable())
+                        {
+                            metadataReader = metadataReaderFactory.getMetadataReader(resource);
+                            try
+                            {
+                                result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName());
+                            }
+                            catch (ClassNotFoundException e)
+                            {
+                                e.printStackTrace();
+                            }
+                        }
+                    }
+                }
+                if (result.size() > 0)
+                {
+                    HashSet<String> hashResult = new HashSet<String>(result);
+                    allResult.addAll(hashResult);
+                }
+            }
+            if (allResult.size() > 0)
+            {
+                typeAliasesPackage = String.join(",", (String[]) allResult.toArray(new String[0]));
+            }
+            else
+            {
+                throw new RuntimeException("mybatis typeAliasesPackage 路径扫描错误,参数typeAliasesPackage:" + typeAliasesPackage + "未找到任何包");
+            }
+        }
+        catch (IOException e)
+        {
+            e.printStackTrace();
+        }
+        return typeAliasesPackage;
+    }
+
+    @Bean
+    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception
+    {
+        String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
+        String mapperLocations = env.getProperty("mybatis.mapperLocations");
+        String configLocation = env.getProperty("mybatis.configLocation");
+        typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
+        VFS.addImplClass(SpringBootVFS.class);
+
+        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
+        sessionFactory.setDataSource(dataSource);
+        sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
+        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
+        sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
+        return sessionFactory.getObject();
+    }
+}

+ 91 - 0
fs-live-mq/src/main/java/com/fs/core/config/RedisConfig.java

@@ -0,0 +1,91 @@
+package com.fs.core.config;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cache.annotation.CachingConfigurerSupport;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+/**
+ * redis配置
+ *
+
+ */
+@Configuration
+@EnableCaching
+public class RedisConfig extends CachingConfigurerSupport
+{
+
+    @Value("${spring.redis.host:localhost}")
+    private String host;
+
+    @Value("${spring.redis.port:6379}")
+    private int port;
+
+    @Value("${spring.redis.password:}")
+    private String password;
+
+    @Value("${spring.redis.database:0}")
+    private int database;
+
+    @Bean
+    @SuppressWarnings(value = { "unchecked", "rawtypes" })
+    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory)
+    {
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
+
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
+        serializer.setObjectMapper(mapper);
+
+        template.setValueSerializer(serializer);
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+        template.afterPropertiesSet();
+        return template;
+    }
+
+    /**
+     * 配置Redisson客户端
+     */
+    @Bean
+    public RedissonClient redissonClient() {
+        Config config = new Config();
+
+        // 构建Redis连接地址
+        String redisUrl = "redis://" + host + ":" + port;
+
+        // 单节点模式配置
+        if (password != null && !password.isEmpty()) {
+            config.useSingleServer()
+                    .setAddress(redisUrl)
+                    .setDatabase(database)
+                    .setPassword(password);
+        } else {
+            config.useSingleServer()
+                    .setAddress(redisUrl)
+                    .setDatabase(database);
+        }
+
+        // 连接池配置
+        config.useSingleServer()
+                .setConnectionMinimumIdleSize(8)
+                .setConnectionPoolSize(32)
+                .setIdleConnectionTimeout(10000);
+
+        return Redisson.create(config);
+    }
+}

+ 66 - 0
fs-live-mq/src/main/java/com/fs/core/config/ResourcesConfig.java

@@ -0,0 +1,66 @@
+package com.fs.core.config;
+
+import com.fs.common.config.FSConfig;
+import com.fs.common.constant.Constants;
+import com.fs.core.interceptor.RepeatSubmitInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * 通用配置
+ * 
+
+ */
+@Configuration
+public class ResourcesConfig implements WebMvcConfigurer
+{
+    @Autowired
+    private RepeatSubmitInterceptor repeatSubmitInterceptor;
+
+    @Override
+    public void addResourceHandlers(ResourceHandlerRegistry registry)
+    {
+        /** 本地文件上传路径 */
+        registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**").addResourceLocations("file:" + FSConfig.getProfile() + "/");
+
+        /** swagger配置 */
+        registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
+        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
+    }
+
+    /**
+     * 自定义拦截规则
+     */
+    @Override
+    public void addInterceptors(InterceptorRegistry registry)
+    {
+        registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
+    }
+
+    /**
+     * 跨域配置
+     */
+    @Bean
+    public CorsFilter corsFilter()
+    {
+        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        CorsConfiguration config = new CorsConfiguration();
+        config.setAllowCredentials(true);
+        // 设置访问源地址
+        config.addAllowedOrigin("*");
+        // 设置访问源请求头
+        config.addAllowedHeader("*");
+        // 设置访问源请求方法
+        config.addAllowedMethod("*");
+        // 对接口配置跨域设置
+        source.registerCorsConfiguration("/**", config);
+        return new CorsFilter(source);
+    }
+}

+ 41 - 0
fs-live-mq/src/main/java/com/fs/core/config/SecurityConfig.java

@@ -0,0 +1,41 @@
+package com.fs.core.config;
+
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+
+
+/**
+ * spring security配置
+ * 
+
+ */
+@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
+public class SecurityConfig extends WebSecurityConfigurerAdapter
+{
+
+    /**
+     * anyRequest          |   匹配所有请求路径
+     * access              |   SpringEl表达式结果为true时可以访问
+     * anonymous           |   匿名可以访问
+     * denyAll             |   用户不能访问
+     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
+     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
+     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
+     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
+     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
+     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
+     * permitAll           |   用户可以任意访问
+     * rememberMe          |   允许通过remember-me登录的用户访问
+     * authenticated       |   用户登录后可访问
+     */
+    @Override
+    protected void configure(HttpSecurity http) throws Exception {
+        http.authorizeRequests()
+                .antMatchers("/**").permitAll()
+                .anyRequest().authenticated()
+                .and().csrf().disable();
+    }
+
+}

+ 33 - 0
fs-live-mq/src/main/java/com/fs/core/config/ServerConfig.java

@@ -0,0 +1,33 @@
+package com.fs.core.config;
+
+import com.fs.common.utils.ServletUtils;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 服务相关配置
+ * 
+ 
+ */
+@Component
+public class ServerConfig
+{
+    /**
+     * 获取完整的请求路径,包括:域名,端口,上下文访问路径
+     * 
+     * @return 服务地址
+     */
+    public String getUrl()
+    {
+        HttpServletRequest request = ServletUtils.getRequest();
+        return getDomain(request);
+    }
+
+    public static String getDomain(HttpServletRequest request)
+    {
+        StringBuffer url = request.getRequestURL();
+        String contextPath = request.getServletContext().getContextPath();
+        return url.delete(url.length() - request.getRequestURI().length(), url.length()).append(contextPath).toString();
+    }
+}

+ 124 - 0
fs-live-mq/src/main/java/com/fs/core/config/SwaggerConfig.java

@@ -0,0 +1,124 @@
+package com.fs.core.config;
+import com.github.xiaoymin.swaggerbootstrapui.annotations.EnableSwaggerBootstrapUI;
+
+import com.fs.common.config.FSConfig;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import springfox.documentation.builders.ApiInfoBuilder;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.service.*;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spi.service.contexts.SecurityContext;
+import springfox.documentation.spring.web.plugins.Docket;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Swagger2的接口配置
+ * 
+
+ */
+@Configuration
+@EnableSwagger2
+@EnableSwaggerBootstrapUI
+public class SwaggerConfig
+{
+    /** 系统基础配置 */
+    @Autowired
+    private FSConfig fsConfig;
+
+    /** 是否开启swagger */
+    @Value("${swagger.enabled}")
+    private boolean enabled;
+
+    /** 设置请求的统一前缀 */
+    @Value("${swagger.pathMapping}")
+    private String pathMapping;
+
+    /**
+     * 创建API
+     */
+    @Bean
+    public Docket createRestApi()
+    {
+        return new Docket(DocumentationType.SWAGGER_2)
+                // 是否启用Swagger
+                .enable(enabled)
+                // 用来创建该API的基本信息,展示在文档的页面中(自定义展示的信息)
+                .apiInfo(apiInfo())
+                // 设置哪些接口暴露给Swagger展示
+                .select()
+                // 扫描所有有注解的api,用这种方式更灵活
+                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
+                // 扫描指定包中的swagger注解
+                // .apis(RequestHandlerSelectors.basePackage("com.fs.project.tool.swagger"))
+                // 扫描所有 .apis(RequestHandlerSelectors.any())
+                .paths(PathSelectors.any())
+                .build()
+                /* 设置安全模式,swagger可以设置访问token */
+                .securitySchemes(securitySchemes())
+                .securityContexts(securityContexts())
+                .pathMapping(pathMapping);
+    }
+
+    /**
+     * 安全模式,这里指定token通过Authorization头请求头传递
+     */
+    private List<ApiKey> securitySchemes()
+    {
+        List<ApiKey> apiKeyList = new ArrayList<ApiKey>();
+        apiKeyList.add(new ApiKey("Authorization", "AppToken", "header"));
+        return apiKeyList;
+    }
+
+    /**
+     * 安全上下文
+     */
+    private List<SecurityContext> securityContexts()
+    {
+        List<SecurityContext> securityContexts = new ArrayList<>();
+        securityContexts.add(
+                SecurityContext.builder()
+                        .securityReferences(defaultAuth())
+                        .forPaths(PathSelectors.regex("^(?!auth).*$"))
+                        .build());
+        return securityContexts;
+    }
+
+    /**
+     * 默认的安全上引用
+     */
+    private List<SecurityReference> defaultAuth()
+    {
+        AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
+        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
+        authorizationScopes[0] = authorizationScope;
+        List<SecurityReference> securityReferences = new ArrayList<>();
+        securityReferences.add(new SecurityReference("Authorization", authorizationScopes));
+        return securityReferences;
+    }
+
+    /**
+     * 添加摘要信息
+     */
+    private ApiInfo apiInfo()
+    {
+        // 用ApiInfoBuilder进行定制
+        return new ApiInfoBuilder()
+                // 设置标题
+                .title("标题:FS管理系统_接口文档")
+                // 描述
+                .description("描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...")
+                // 作者信息
+                .contact(new Contact(fsConfig.getName(), null, null))
+                // 版本
+                .version("版本号:" + fsConfig.getVersion())
+                .build();
+    }
+}

+ 63 - 0
fs-live-mq/src/main/java/com/fs/core/config/ThreadPoolConfig.java

@@ -0,0 +1,63 @@
+package com.fs.core.config;
+
+import com.fs.common.utils.Threads;
+import org.apache.commons.lang3.concurrent.BasicThreadFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * 线程池配置
+ *
+ 
+ **/
+@Configuration
+public class ThreadPoolConfig
+{
+    // 核心线程池大小
+    private int corePoolSize = 50;
+
+    // 最大可创建的线程数
+    private int maxPoolSize = 200;
+
+    // 队列最大长度
+    private int queueCapacity = 1000;
+
+    // 线程池维护线程所允许的空闲时间
+    private int keepAliveSeconds = 300;
+
+    @Bean(name = "threadPoolTaskExecutor")
+    public ThreadPoolTaskExecutor threadPoolTaskExecutor()
+    {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setMaxPoolSize(maxPoolSize);
+        executor.setCorePoolSize(corePoolSize);
+        executor.setQueueCapacity(queueCapacity);
+        executor.setKeepAliveSeconds(keepAliveSeconds);
+        // 线程池对拒绝任务(无线程可用)的处理策略
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        return executor;
+    }
+
+    /**
+     * 执行周期性或定时任务
+     */
+    @Bean(name = "scheduledExecutorService")
+    protected ScheduledExecutorService scheduledExecutorService()
+    {
+        return new ScheduledThreadPoolExecutor(corePoolSize,
+                new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build())
+        {
+            @Override
+            protected void afterExecute(Runnable r, Throwable t)
+            {
+                super.afterExecute(r, t);
+                Threads.printException(r, t);
+            }
+        };
+    }
+}

+ 77 - 0
fs-live-mq/src/main/java/com/fs/core/config/properties/DruidProperties.java

@@ -0,0 +1,77 @@
+package com.fs.core.config.properties;
+
+import com.alibaba.druid.pool.DruidDataSource;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * druid 配置属性
+ * 
+
+ */
+@Configuration
+public class DruidProperties
+{
+    @Value("${spring.datasource.druid.initialSize}")
+    private int initialSize;
+
+    @Value("${spring.datasource.druid.minIdle}")
+    private int minIdle;
+
+    @Value("${spring.datasource.druid.maxActive}")
+    private int maxActive;
+
+    @Value("${spring.datasource.druid.maxWait}")
+    private int maxWait;
+
+    @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}")
+    private int timeBetweenEvictionRunsMillis;
+
+    @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}")
+    private int minEvictableIdleTimeMillis;
+
+    @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}")
+    private int maxEvictableIdleTimeMillis;
+
+    @Value("${spring.datasource.druid.validationQuery}")
+    private String validationQuery;
+
+    @Value("${spring.datasource.druid.testWhileIdle}")
+    private boolean testWhileIdle;
+
+    @Value("${spring.datasource.druid.testOnBorrow}")
+    private boolean testOnBorrow;
+
+    @Value("${spring.datasource.druid.testOnReturn}")
+    private boolean testOnReturn;
+
+    public DruidDataSource dataSource(DruidDataSource datasource)
+    {
+        /** 配置初始化大小、最小、最大 */
+        datasource.setInitialSize(initialSize);
+        datasource.setMaxActive(maxActive);
+        datasource.setMinIdle(minIdle);
+
+        /** 配置获取连接等待超时的时间 */
+        datasource.setMaxWait(maxWait);
+
+        /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
+        datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
+
+        /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */
+        datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
+        datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
+
+        /**
+         * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
+         */
+        datasource.setValidationQuery(validationQuery);
+        /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */
+        datasource.setTestWhileIdle(testWhileIdle);
+        /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
+        datasource.setTestOnBorrow(testOnBorrow);
+        /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
+        datasource.setTestOnReturn(testOnReturn);
+        return datasource;
+    }
+}

+ 27 - 0
fs-live-mq/src/main/java/com/fs/core/datasource/DynamicDataSource.java

@@ -0,0 +1,27 @@
+package com.fs.core.datasource;
+
+import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
+
+import javax.sql.DataSource;
+import java.util.Map;
+
+/**
+ * 动态数据源
+ * 
+ 
+ */
+public class DynamicDataSource extends AbstractRoutingDataSource
+{
+    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources)
+    {
+        super.setDefaultTargetDataSource(defaultTargetDataSource);
+        super.setTargetDataSources(targetDataSources);
+        super.afterPropertiesSet();
+    }
+
+    @Override
+    protected Object determineCurrentLookupKey()
+    {
+        return DynamicDataSourceContextHolder.getDataSourceType();
+    }
+}

+ 45 - 0
fs-live-mq/src/main/java/com/fs/core/datasource/DynamicDataSourceContextHolder.java

@@ -0,0 +1,45 @@
+package com.fs.core.datasource;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 数据源切换处理
+ * 
+ 
+ */
+public class DynamicDataSourceContextHolder
+{
+    public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
+
+    /**
+     * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
+     *  所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
+     */
+    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
+
+    /**
+     * 设置数据源的变量
+     */
+    public static void setDataSourceType(String dsType)
+    {
+        
+        CONTEXT_HOLDER.set(dsType);
+    }
+
+    /**
+     * 获得数据源的变量
+     */
+    public static String getDataSourceType()
+    {
+        return CONTEXT_HOLDER.get();
+    }
+
+    /**
+     * 清空数据源变量
+     */
+    public static void clearDataSourceType()
+    {
+        CONTEXT_HOLDER.remove();
+    }
+}

+ 56 - 0
fs-live-mq/src/main/java/com/fs/core/interceptor/RepeatSubmitInterceptor.java

@@ -0,0 +1,56 @@
+package com.fs.core.interceptor;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.annotation.RepeatSubmit;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.utils.ServletUtils;
+import org.springframework.stereotype.Component;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.lang.reflect.Method;
+
+/**
+ * 防止重复提交拦截器
+ *
+
+ */
+@Component
+public abstract class RepeatSubmitInterceptor extends HandlerInterceptorAdapter
+{
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
+    {
+        if (handler instanceof HandlerMethod)
+        {
+            HandlerMethod handlerMethod = (HandlerMethod) handler;
+            Method method = handlerMethod.getMethod();
+            RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
+            if (annotation != null)
+            {
+                if (this.isRepeatSubmit(request))
+                {
+                    AjaxResult ajaxResult = AjaxResult.error("不允许重复提交,请稍后再试");
+                    ServletUtils.renderString(response, JSONObject.toJSONString(ajaxResult));
+                    return false;
+                }
+            }
+            return true;
+        }
+        else
+        {
+            return super.preHandle(request, response, handler);
+        }
+    }
+
+    /**
+     * 验证是否重复提交由子类实现具体的防重复提交的规则
+     *
+     * @param request
+     * @return
+     * @throws Exception
+     */
+    public abstract boolean isRepeatSubmit(HttpServletRequest request);
+}

+ 126 - 0
fs-live-mq/src/main/java/com/fs/core/interceptor/impl/SameUrlDataInterceptor.java

@@ -0,0 +1,126 @@
+package com.fs.core.interceptor.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.constant.Constants;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.filter.RepeatedlyRequestWrapper;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.http.HttpHelper;
+import com.fs.core.interceptor.RepeatSubmitInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 判断请求url和数据是否和上一次相同,
+ * 如果和上次相同,则是重复提交表单。 有效时间为10秒内。
+ * 
+
+ */
+@Component
+public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
+{
+    public final String REPEAT_PARAMS = "repeatParams";
+
+    public final String REPEAT_TIME = "repeatTime";
+
+    // 令牌自定义标识
+    @Value("${fs.jwt.header}")
+    private String header;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    /**
+     * 间隔时间,单位:秒 默认10秒
+     * 
+     * 两次相同参数的请求,如果间隔时间大于该参数,系统不会认定为重复提交的数据
+     */
+    private int intervalTime = 10;
+
+    public void setIntervalTime(int intervalTime)
+    {
+        this.intervalTime = intervalTime;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public boolean isRepeatSubmit(HttpServletRequest request)
+    {
+        String nowParams = "";
+        if (request instanceof RepeatedlyRequestWrapper)
+        {
+            RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
+            nowParams = HttpHelper.getBodyString(repeatedlyRequest);
+        }
+
+        // body参数为空,获取Parameter的数据
+        if (StringUtils.isEmpty(nowParams))
+        {
+            nowParams = JSONObject.toJSONString(request.getParameterMap());
+        }
+        Map<String, Object> nowDataMap = new HashMap<String, Object>();
+        nowDataMap.put(REPEAT_PARAMS, nowParams);
+        nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());
+
+        // 请求地址(作为存放cache的key值)
+        String url = request.getRequestURI();
+
+        // 唯一值(没有消息头则使用请求地址)
+        String submitKey = request.getHeader(header);
+        if (StringUtils.isEmpty(submitKey))
+        {
+            submitKey = url;
+        }
+
+        // 唯一标识(指定key + 消息头)
+        String cache_repeat_key = Constants.REPEAT_SUBMIT_KEY + submitKey;
+
+        Object sessionObj = redisCache.getCacheObject(cache_repeat_key);
+        if (sessionObj != null)
+        {
+            Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
+            if (sessionMap.containsKey(url))
+            {
+                Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
+                if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap))
+                {
+                    return true;
+                }
+            }
+        }
+        Map<String, Object> cacheMap = new HashMap<String, Object>();
+        cacheMap.put(url, nowDataMap);
+        redisCache.setCacheObject(cache_repeat_key, cacheMap, intervalTime, TimeUnit.SECONDS);
+        return false;
+    }
+
+    /**
+     * 判断参数是否相同
+     */
+    private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap)
+    {
+        String nowParams = (String) nowMap.get(REPEAT_PARAMS);
+        String preParams = (String) preMap.get(REPEAT_PARAMS);
+        return nowParams.equals(preParams);
+    }
+
+    /**
+     * 判断两次间隔时间
+     */
+    private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap)
+    {
+        long time1 = (Long) nowMap.get(REPEAT_TIME);
+        long time2 = (Long) preMap.get(REPEAT_TIME);
+        if ((time1 - time2) < (this.intervalTime * 1000))
+        {
+            return true;
+        }
+        return false;
+    }
+}

+ 53 - 0
fs-live-mq/src/main/java/com/fs/core/security/SecurityUtils.java

@@ -0,0 +1,53 @@
+package com.fs.core.security;
+
+import com.tencentcloudapi.yunjing.v20180228.models.MisAlarmNonlocalLoginPlacesRequest;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+
+
+/**
+ * 安全服务工具类
+ *
+
+ */
+public class SecurityUtils
+{
+
+    /**
+     * 生成BCryptPasswordEncoder密码
+     *
+     * @param password 密码
+     * @return 加密字符串
+     */
+    public static String encryptPassword(String password)
+    {
+
+        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
+        return passwordEncoder.encode(password);
+    }
+
+    /**
+     * 判断密码是否相同
+     *
+     * @param rawPassword 真实密码
+     * @param encodedPassword 加密后字符
+     * @return 结果
+     */
+    public static boolean matchesPassword(String rawPassword, String encodedPassword)
+    {
+        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
+        return passwordEncoder.matches(rawPassword, encodedPassword);
+    }
+
+    /**
+     * 是否为管理员
+     *
+     * @param userId 用户ID
+     * @return 结果
+     */
+    public static boolean isAdmin(Long userId)
+    {
+        return userId != null && 1L == userId;
+    }
+}

+ 45 - 0
fs-live-mq/src/main/java/com/fs/mq/RocketMQConsumerService.java

@@ -0,0 +1,45 @@
+package com.fs.mq;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.live.mapper.LiveGoodsMapper;
+import com.fs.live.vo.LiveGoodsUploadMqVo;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.ibatis.session.ExecutorType;
+import org.apache.ibatis.session.SqlSession;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
+import org.apache.rocketmq.spring.core.RocketMQListener;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+@AllArgsConstructor
+@RocketMQMessageListener(topic = "live-goods-upload-fhhx", consumerGroup = "${rocketmq.consumer.group}")
+public class RocketMQConsumerService implements RocketMQListener<String> {
+
+    private final SqlSessionFactory sqlSessionFactory;
+
+    @Override
+    public void onMessage(String message) {
+        LiveGoodsUploadMqVo vo = JSON.parseObject(message, LiveGoodsUploadMqVo.class);
+        if(vo == null || vo.getGoodsId() == null || vo.getGoodsNum() == null){
+            return;
+        }
+        // 2. 数据库事务:更新库存+销量(原子操作)
+        try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.SIMPLE)) {
+            LiveGoodsMapper mapper = session.getMapper(LiveGoodsMapper.class);
+            // 事务内更新:扣减可用库存 + 增加销量
+            int affected = mapper.updateStock(vo.getGoodsId(),vo.getGoodsNum());
+            if (affected > 0) {
+                session.commit();
+                // 标记消息已消费(过期时间=1天)
+                log.info("库存销量入库成功,skuId={}, deductNum={}", vo.getGoodsId(), vo.getGoodsNum());
+            } else {
+                log.error("库存销量入库失败(无匹配SKU),msg={}", vo);
+            }
+        } catch (Exception e) {
+            log.error("库存销量入库异常,msg={}", vo, e);
+        }
+    }
+}

+ 1 - 0
fs-live-mq/src/main/resources/META-INF/spring-devtools.properties

@@ -0,0 +1 @@
+restart.include.json=/com.alibaba.fastjson.*.jar

+ 90 - 0
fs-live-mq/src/main/resources/application-dev.yml

@@ -0,0 +1,90 @@
+# 数据源配置
+spring:
+    jackson:
+        time-zone: GMT+8 #如果有时区问题,设置时区
+    # redis 配置
+    redis:
+        # 地址
+        host: 10.0.0.8
+        # 端口,默认为6379
+        port: 6379
+        # 密码
+        password: Ylrz_1q2w3e4r5t6y
+        # 连接超时时间
+        timeout: 10s
+        lettuce:
+            pool:
+                # 连接池中的最小空闲连接
+                min-idle: 0
+                # 连接池中的最大空闲连接
+                max-idle: 8
+                # 连接池的最大数据库连接数
+                max-active: 8
+                # #连接池最大阻塞等待时间(使用负值表示没有限制)
+                max-wait: -1ms
+        database: 1
+    datasource:
+        type: com.alibaba.druid.pool.DruidDataSource
+        driverClassName: com.mysql.cj.jdbc.Driver
+        druid:
+            # 主库数据源
+            master:
+                url: jdbc:mysql://cd-cdb-7f33sq06.sql.tencentcdb.com:20329/fs_ffhx_shop?allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                username: root
+                password: Ylrz_1q2w3e4r5t6y
+            # 从库数据源
+            slave:
+                # 从数据源开关/默认关闭
+                url: jdbc:mysql://cd-cdb-7f33sq06.sql.tencentcdb.com:20329?allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                username: root
+                password: Ylrz_1q2w3e4r5t6y
+            # 初始连接数
+            initialSize: 5
+            # 最小连接池数量
+            minIdle: 10
+            # 最大连接池数量
+            maxActive: 20
+            # 配置获取连接等待超时的时间
+            maxWait: 60000
+            # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+            timeBetweenEvictionRunsMillis: 60000
+            # 配置一个连接在池中最小生存的时间,单位是毫秒
+            minEvictableIdleTimeMillis: 300000
+            # 配置一个连接在池中最大生存的时间,单位是毫秒
+            maxEvictableIdleTimeMillis: 900000
+            # 配置检测连接是否有效
+            validationQuery: SELECT 1 FROM DUAL
+            testWhileIdle: true
+            testOnBorrow: false
+            testOnReturn: false
+            webStatFilter:
+                enabled: true
+            statViewServlet:
+                enabled: true
+                # 设置白名单,不填则允许所有访问
+                allow:
+                url-pattern: /druid/*
+                # 控制台管理用户名和密码
+                login-username:
+                login-password:
+            filter:
+                stat:
+                    enabled: true
+                    # 慢SQL记录
+                    log-slow-sql: true
+                    slow-sql-millis: 1000
+                    merge-sql: true
+                wall:
+                    config:
+                        multi-statement-allow: true
+
+rocketmq:
+    name-server: rmq-16b45v4p9n.rocketmq.cd.qcloud.tencenttdmq.com:8080 # RocketMQ NameServer 地址
+    producer:
+        group: my-producer-group
+        access-key: ak16b45v4p9n150d89395b3c # 替换为实际的 accessKey
+        secret-key: sk370fb48d869b152b # 替换为实际的 secretKey
+    consumer:
+        group: common-group
+        access-key: ak16b45v4p9n150d89395b3c # 替换为实际的 accessKey
+        secret-key: sk370fb48d869b152b # 替换为实际的 secretKey

+ 79 - 0
fs-live-mq/src/main/resources/application-druid-test.yml

@@ -0,0 +1,79 @@
+# 数据源配置
+spring:
+    # redis 配置
+    redis:
+        # 地址
+        host: localhost
+        # 端口,默认为6379
+        port: 6379
+        # 密码
+        password:
+        # 连接超时时间
+        timeout: 10s
+        lettuce:
+            pool:
+                # 连接池中的最小空闲连接
+                min-idle: 0
+                # 连接池中的最大空闲连接
+                max-idle: 8
+                # 连接池的最大数据库连接数
+                max-active: 8
+                # #连接池最大阻塞等待时间(使用负值表示没有限制)
+                max-wait: -1ms
+        database: 1
+    datasource:
+        type: com.alibaba.druid.pool.DruidDataSource
+        driverClassName: com.mysql.cj.jdbc.Driver
+        druid:
+            # 主库数据源
+            master:
+                url: jdbc:mysql://139.186.77.83:3306/his?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                username: Rtroot
+                password: Rtroot
+            # 从库数据源
+            slave:
+                # 从数据源开关/默认关闭
+                enabled: false
+                url:
+                username:
+                password:
+            # 初始连接数
+            initialSize: 5
+            # 最小连接池数量
+            minIdle: 10
+            # 最大连接池数量
+            maxActive: 20
+            # 配置获取连接等待超时的时间
+            maxWait: 60000
+            # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+            timeBetweenEvictionRunsMillis: 60000
+            # 配置一个连接在池中最小生存的时间,单位是毫秒
+            minEvictableIdleTimeMillis: 300000
+            # 配置一个连接在池中最大生存的时间,单位是毫秒
+            maxEvictableIdleTimeMillis: 900000
+            # 配置检测连接是否有效
+            validationQuery: SELECT 1 FROM DUAL
+            testWhileIdle: true
+            testOnBorrow: false
+            testOnReturn: false
+            webStatFilter:
+                enabled: true
+            statViewServlet:
+                enabled: true
+                # 设置白名单,不填则允许所有访问
+                allow:
+                url-pattern: /druid/*
+                # 控制台管理用户名和密码
+                login-username:
+                login-password:
+            filter:
+                stat:
+                    enabled: true
+                    # 慢SQL记录
+                    log-slow-sql: true
+                    slow-sql-millis: 1000
+                    merge-sql: true
+                wall:
+                    config:
+                        multi-statement-allow: true
+

+ 78 - 0
fs-live-mq/src/main/resources/application-druid.yml

@@ -0,0 +1,78 @@
+# 数据源配置
+spring:
+    # redis 配置
+    redis:
+        # 地址 172.30.0.15
+        host: 127.0.0.1
+        # 端口,默认为6379
+        port: 6379
+        # 密码
+        password:
+        # 连接超时时间
+        timeout: 100s
+        lettuce:
+            pool:
+                # 连接池中的最小空闲连接
+                min-idle: 0
+                # 连接池中的最大空闲连接
+                max-idle: 500
+                # 连接池的最大数据库连接数
+                max-active: 500
+                # #连接池最大阻塞等待时间(使用负值表示没有限制)
+                max-wait: -1ms
+        database: 0
+    datasource:
+        type: com.alibaba.druid.pool.DruidDataSource
+        driverClassName: com.mysql.cj.jdbc.Driver
+        druid:
+            # 主库数据源
+            master:
+                url: jdbc:mysql://127.0.0.1:3306/fs_hospital?allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                username: root
+                password: 123456
+            # 从库数据源
+            slave:
+                # 从数据源开关/默认关闭
+                enabled: false
+                url:
+                username:
+                password:
+            # 初始连接数
+            initialSize: 500
+            # 最小连接池数量
+            minIdle: 2000
+            # 最大连接池数量
+            maxActive: 2000
+            # 配置获取连接等待超时的时间
+            maxWait: 60000
+            # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+            timeBetweenEvictionRunsMillis: 60000
+            # 配置一个连接在池中最小生存的时间,单位是毫秒
+            minEvictableIdleTimeMillis: 300000
+            # 配置一个连接在池中最大生存的时间,单位是毫秒
+            maxEvictableIdleTimeMillis: 900000
+            # 配置检测连接是否有效
+            validationQuery: SELECT 1 FROM DUAL
+            testWhileIdle: true
+            testOnBorrow: false
+            testOnReturn: false
+            webStatFilter:
+                enabled: true
+            statViewServlet:
+                enabled: true
+                # 设置白名单,不填则允许所有访问
+                allow:
+                url-pattern: /druid/*
+                # 控制台管理用户名和密码
+                login-username:
+                login-password:
+            filter:
+                stat:
+                    enabled: true
+                    # 慢SQL记录
+                    log-slow-sql: true
+                    slow-sql-millis: 1000
+                    merge-sql: true
+                wall:
+                    config:
+                        multi-statement-allow: true

+ 122 - 0
fs-live-mq/src/main/resources/application.yml

@@ -0,0 +1,122 @@
+# 项目相关配置
+fs:
+  # 名称
+  name: fs
+  # 版本
+  version: 1.1.0
+  # 版权年份
+  copyrightYear: 2020
+  # 实例演示开关
+  demoEnabled: false
+  # 文件路径 示例( Windows配置D:/fs/uploadPath,Linux配置 /home/fs/uploadPath)
+  profile: C:/fs/uploadPath
+  # 获取ip地址开关
+  addressEnabled: false
+  # 验证码类型 math 数组计算 char 字符验证
+  captchaType: math
+  # APP模块,是通过jwt认证的,如果要使用APP模块,则需要修改【加密秘钥】
+  jwt:
+    # 加密秘钥
+    secret: f4e2e52034348f86b67cde581c0f9eb5
+    # token有效时长,100天,单位秒
+    expire: 8640000
+    header: AppToken
+  url: https://api.yjf.runtzh.com
+# 开发环境配置
+server:
+  # 服务器的HTTP端口,默认为 7014  store 7114
+  port: 7115
+  servlet:
+    # 应用的访问路径
+    context-path: /
+    # 指定静态资源的路径
+    resources:
+      static-locations: classpath:/static/
+    #设定thymeleaf
+    thymeleaf:
+      #thymeleaf对html的检查过于严格,设置spring.thymeleaf.mode=LEGACYHTML5
+      mode: LEGACYHTML5
+      cache: false
+  tomcat:
+    # tomcat的URI编码
+    uri-encoding: UTF-8
+    # tomcat最大线程数,默认为200
+    max-threads: 5000
+    # Tomcat启动初始化的线程数,默认值25
+    min-spare-threads: 100
+    # 服务器在任何给定时间接受和处理的最大连接数。一旦达到限制,操作系统仍然可以接受基于“acceptCount”属性的连接。
+    max-connections: 30000
+    # 当所有可能的请求处理线程都在使用中时,传入连接请求的最大队列长度
+    accept-count: 1000
+    # 连接器在接受连接后等待显示请求 URI 行的时间。
+    connection-timeout: 20000
+    # 在关闭连接之前等待另一个 HTTP 请求的时间。如果未设置,则使用 connectionTimeout。设置为 -1 时不会超时。
+    keep-alive-timeout: 20000
+    # 在连接关闭之前可以进行流水线处理的最大HTTP请求数量。当设置为0或1时,禁用keep-alive和流水线处理。当设置为-1时,允许无限数量的流水线处理或keep-alive请求。
+    max-keep-alive-requests: 100
+
+# 日志配置
+logging:
+  level:
+#    com.fs: info
+    org.springframework: warn
+    org.springframework.web: info
+
+# Spring配置
+spring:
+  datasource:
+    druid:
+      stat-view-servlet:
+        enabled: false
+  # 资源信息
+  messages:
+    # 国际化资源文件路径
+    basename: i18n/messages
+  profiles:
+    active: dev
+    include: config
+  # 文件上传
+  servlet:
+     multipart:
+       # 单个文件大小
+       max-file-size:  10MB
+       # 设置总上传的文件大小
+       max-request-size:  20MB
+  # 服务模块
+  devtools:
+    restart:
+      # 热部署开关
+      enabled: true
+
+# MyBatis配置
+mybatis:
+    # 搜索指定包别名
+    typeAliasesPackage: com.fs.**.domain,com.fs.**.bo,com.fs.**.vo
+    # 配置mapper的扫描,找到所有的mapper.xml映射文件
+    mapperLocations: classpath*:mapper/**/*Mapper.xml
+    # 加载全局的配置文件
+    configLocation: classpath:mybatis/mybatis-config.xml
+
+# PageHelper分页插件
+pagehelper:
+  helperDialect: mysql
+  reasonable: false
+  supportMethodsArguments: true
+  params: count=countSql
+
+# Swagger配置
+swagger:
+  # 是否开启swagger
+  enabled: false
+  # 请求前缀
+  pathMapping: /
+
+# 防止XSS攻击
+xss:
+  # 过滤开关
+  enabled: true
+  # 排除链接(多个用逗号分隔)
+  excludes: /system/notice/*
+  # 匹配链接
+  urlPatterns: /system/*,/monitor/*,/tool/*
+

+ 2 - 0
fs-live-mq/src/main/resources/banner.txt

@@ -0,0 +1,2 @@
+Application Version: ${fs.version}
+Spring Boot Version: ${spring-boot.version}

BIN
fs-live-mq/src/main/resources/fx.jpg


+ 36 - 0
fs-live-mq/src/main/resources/i18n/messages.properties

@@ -0,0 +1,36 @@
+#错误消息
+not.null=* 必须填写
+user.jcaptcha.error=验证码错误
+user.jcaptcha.expire=验证码已失效
+user.not.exists=用户不存在/密码错误
+user.password.not.match=用户不存在/密码错误
+user.password.retry.limit.count=密码输入错误{0}次
+user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定10分钟
+user.password.delete=对不起,您的账号已被删除
+user.blocked=用户已封禁,请联系管理员
+role.blocked=角色已封禁,请联系管理员
+user.logout.success=退出成功
+
+length.not.valid=长度必须在{min}到{max}个字符之间
+
+user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头
+user.password.not.valid=* 5-50个字符
+ 
+user.email.not.valid=邮箱格式错误
+user.mobile.phone.number.not.valid=手机号格式错误
+user.login.success=登录成功
+user.notfound=请重新登录
+user.forcelogout=管理员强制退出,请重新登录
+user.unknown.error=未知错误,请重新登录
+
+##文件上传消息
+upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB!
+upload.filename.exceed.length=上传的文件名最长{0}个字符
+
+##权限
+no.permission=您没有数据的权限,请联系管理员添加权限 [{0}]
+no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}]
+no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}]
+no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}]
+no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}]
+no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}]

+ 15 - 0
fs-live-mq/src/main/resources/mybatis/mybatis-config.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE configuration
+PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-config.dtd">
+<configuration>
+	
+	<settings>
+		<setting name="cacheEnabled"             value="false" />  <!-- 全局映射器启用缓存 -->
+		<setting name="useGeneratedKeys"         value="true" />  <!-- 允许 JDBC 支持自动生成主键 -->
+		<setting name="defaultExecutorType"      value="REUSE" /> <!-- 配置默认的执行器 -->
+		<setting name="logImpl"                  value="SLF4J" /> <!-- 指定 MyBatis 所用日志的具体实现 -->
+		 <setting name="mapUnderscoreToCamelCase" value="true"/>
+	</settings>
+	
+</configuration>

BIN
fs-live-mq/src/main/resources/qr.jpg


BIN
fs-live-mq/src/main/resources/simsunb.ttf


+ 1 - 0
fs-live-mq/src/main/resources/static/S8Zw463cFc.txt

@@ -0,0 +1 @@
+641273d9479c4e0133bfacb9f669d39f

+ 21 - 0
fs-live-mq/src/main/resources/templates/privacyPolicy.html

@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:th="http://www.thymeleaf.org">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+    <title >隐私政策</title>
+    <link  rel="stylesheet"/>
+    <style>
+        body{
+            background-color:#F8F8F8;
+        }
+    </style>
+<body>
+<div th:utext="${privacyPolicy}" ></div>
+<script th:inline="javascript">
+
+</script>
+
+</body>
+</html>

+ 21 - 0
fs-live-mq/src/main/resources/templates/userAgreement.html

@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:th="http://www.thymeleaf.org">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+    <title >用户协议</title>
+    <link  rel="stylesheet"/>
+    <style>
+        body{
+            background-color:#F8F8F8;
+        }
+    </style>
+<body>
+<div th:utext="${userAgreement}" ></div>
+<script th:inline="javascript">
+
+</script>
+
+</body>
+</html>

+ 61 - 0
fs-live-mq/src/test/java/com/fs/core/security/BaseSpringBootTest.java

@@ -0,0 +1,61 @@
+package com.fs.core.security;
+
+import cn.hutool.core.date.DateUtil;
+import com.fs.common.utils.DateUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.runner.RunWith;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.util.CollectionUtils;
+
+import java.util.List;
+
+/**
+ * 测试基类
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@SpringBootTest
+public abstract class BaseSpringBootTest {
+
+    protected Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    private long time;
+
+    public long getTime() {
+        return time;
+    }
+
+    public void setTime(long time) {
+        this.time = time;
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        this.setTime(System.currentTimeMillis());
+        logger.info("==> 测试开始执行 <==");
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        logger.info("==> 测试执行完成,耗时:{} ms <==",
+                System.currentTimeMillis() - this.getTime());
+    }
+
+    /**
+     * 方法描述:打印list.
+     * 创建时间:2018-10-11 00:23:28
+     */
+    <T> void print(List<T> list) {
+        if (!CollectionUtils.isEmpty(list)) {
+            list.forEach(System.out::println);
+        }
+    }
+
+    void print(Object o) {
+        System.out.println(o.toString());
+    }
+
+}

+ 74 - 0
fs-live-socket/src/main/java/com/fs/core/aspectj/DataSourceAspect.java

@@ -0,0 +1,74 @@
+package com.fs.core.aspectj;
+
+import com.fs.common.annotation.DataSource;
+import com.fs.common.utils.StringUtils;
+import com.fs.core.datasource.DynamicDataSourceContextHolder;
+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.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import java.util.Objects;
+
+
+/**
+ * 多数据源处理
+ *
+
+ */
+@Aspect
+@Order(1)
+@Component
+public class DataSourceAspect
+{
+    protected Logger logger = LoggerFactory.getLogger(getClass());
+
+    @Pointcut("@annotation(com.fs.common.annotation.DataSource)"
+            + "|| @within(com.fs.common.annotation.DataSource)")
+    public void dsPointCut()
+    {
+
+    }
+
+    @Around("dsPointCut()")
+    public Object around(ProceedingJoinPoint point) throws Throwable
+    {
+        DataSource dataSource = getDataSource(point);
+
+        if (StringUtils.isNotNull(dataSource))
+        {
+            DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
+        }
+
+        try
+        {
+            return point.proceed();
+        }
+        finally
+        {
+            // 销毁数据源 在执行方法之后
+            DynamicDataSourceContextHolder.clearDataSourceType();
+        }
+    }
+
+    /**
+     * 获取需要切换的数据源
+     */
+    public DataSource getDataSource(ProceedingJoinPoint point)
+    {
+        MethodSignature signature = (MethodSignature) point.getSignature();
+        DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
+        if (Objects.nonNull(dataSource))
+        {
+            return dataSource;
+        }
+
+        return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
+    }
+}

+ 90 - 91
fs-live-socket/src/main/java/com/fs/core/aspectj/LiveWatchUserAspect.java

@@ -1,91 +1,90 @@
-package com.fs.core.aspectj;
-
-import cn.hutool.core.util.ObjectUtil;
-import com.fs.common.core.redis.RedisUtil;
-import com.fs.live.domain.LiveWatchUser;
-import com.fs.live.service.ILiveWatchUserService;
-import lombok.extern.slf4j.Slf4j;
-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.stereotype.Component;
-
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
-
-@Aspect
-@Component
-@Slf4j
-public class LiveWatchUserAspect {
-
-    @Autowired
-    private RedisUtil redisUtil;
-
-    @Autowired
-    private ILiveWatchUserService liveWatchUserService;
-
-    @AfterReturning(pointcut = "execution(* com.fs.live.service.impl.LiveWatchUserServiceImpl.insertLiveWatchUser(..)) || " +
-            "execution(* com.fs.live.service.impl.LiveWatchUserServiceImpl.updateLiveWatchUser(..)) || " +
-            "execution(* com.fs.live.service.impl.LiveWatchUserServiceImpl.deleteLiveWatchUserById(..)) || " +
-            "execution(* com.fs.live.service.impl.LiveWatchUserServiceImpl.deleteLiveWatchUserByIds(..))",
-            returning = "result")
-    public void afterLiveWatchUserOperation(JoinPoint joinPoint, Object result) {
-        try {
-            String methodName = joinPoint.getSignature().getName();
-            Object[] args = joinPoint.getArgs();
-            log.info("直播观看用户数据发生变化,方法: {}, 参数: {}", methodName, Arrays.toString(args));
-            // 提取liveId并处理缓存更新
-            Set<Long> liveIds = extractLiveIds(methodName, args);
-            for (Long liveId : liveIds) {
-                liveWatchUserService.asyncToCache(liveId);
-            }
-        } catch (Exception e) {
-            log.error("执行直播观看用户变更后逻辑失败", e);
-        }
-    }
-
-    private Set<Long> extractLiveIds(String methodName, Object[] args) {
-        Set<Long> liveIds = new HashSet<>();
-        if (args == null || args.length == 0) {
-            return liveIds;
-        }
-        switch (methodName) {
-            case "insertLiveWatchUser":
-            case "updateLiveWatchUser":
-                // 参数是LiveWatchUser对象
-                if (args[0] instanceof LiveWatchUser) {
-                    LiveWatchUser liveWatchUser = (LiveWatchUser) args[0];
-                    if (liveWatchUser.getLiveId() != null) {
-                        liveIds.add(liveWatchUser.getLiveId());
-                    }
-                }
-                break;
-            case "deleteLiveWatchUserById":
-                // 参数是Long类型的id,需要先查询获取liveId
-                if (args[0] instanceof Long) {
-                    LiveWatchUser liveWatchUser = liveWatchUserService.selectLiveWatchUserById((Long) args[0]);
-                    if (ObjectUtil.isNotEmpty(liveWatchUser)) {
-                        liveIds.add(liveWatchUser.getLiveId());
-                    }
-                }
-                break;
-            case "deleteLiveWatchUserByIds":
-                // 参数是Long[]数组
-                if (args[0] instanceof Long[]) {
-                    Long[] ids = (Long[]) args[0];
-                    LiveWatchUser liveWatchUser = liveWatchUserService.selectLiveWatchUserById(ids[0]);
-                    if (ObjectUtil.isNotEmpty(liveWatchUser)) {
-                        liveIds.add(liveWatchUser.getLiveId());
-                    }
-                }
-                break;
-            default:
-                log.warn("未处理的方法: {}", methodName);
-        }
-        return liveIds;
-    }
-
-
-}
+//package com.fs.core.aspectj;
+//
+//import cn.hutool.core.util.ObjectUtil;
+//import com.fs.common.core.redis.RedisUtil;
+//import com.fs.live.domain.LiveWatchUser;
+//import com.fs.live.service.ILiveWatchUserService;
+//import lombok.extern.slf4j.Slf4j;
+//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.stereotype.Component;
+//
+//import java.util.Arrays;
+//import java.util.HashSet;
+//import java.util.Set;
+//
+//@Aspect
+//@Component
+//@Slf4j
+//public class LiveWatchUserAspect {
+//
+//    @Autowired
+//    private RedisUtil redisUtil;
+//
+//    @Autowired
+//    private ILiveWatchUserService liveWatchUserService;
+//
+//    @AfterReturning(pointcut = "execution(* com.fs.live.service.impl.LiveWatchUserServiceImpl.insertLiveWatchUser(..)) || " +
+//            "execution(* com.fs.live.service.impl.LiveWatchUserServiceImpl.updateLiveWatchUser(..)) || " +
+//            "execution(* com.fs.live.service.impl.LiveWatchUserServiceImpl.deleteLiveWatchUserById(..)) || " +
+//            "execution(* com.fs.live.service.impl.LiveWatchUserServiceImpl.deleteLiveWatchUserByIds(..))",
+//            returning = "result")
+//    public void afterLiveWatchUserOperation(JoinPoint joinPoint, Object result) {
+//        try {
+//            String methodName = joinPoint.getSignature().getName();
+//            Object[] args = joinPoint.getArgs();
+//            // 提取liveId并处理缓存更新
+//            Set<Long> liveIds = extractLiveIds(methodName, args);
+//            for (Long liveId : liveIds) {
+//                liveWatchUserService.asyncToCache(liveId);
+//            }
+//        } catch (Exception e) {
+//            log.error("执行直播观看用户变更后逻辑失败", e);
+//        }
+//    }
+//
+//    private Set<Long> extractLiveIds(String methodName, Object[] args) {
+//        Set<Long> liveIds = new HashSet<>();
+//        if (args == null || args.length == 0) {
+//            return liveIds;
+//        }
+//        switch (methodName) {
+//            case "insertLiveWatchUser":
+//            case "updateLiveWatchUser":
+//                // 参数是LiveWatchUser对象
+//                if (args[0] instanceof LiveWatchUser) {
+//                    LiveWatchUser liveWatchUser = (LiveWatchUser) args[0];
+//                    if (liveWatchUser.getLiveId() != null) {
+//                        liveIds.add(liveWatchUser.getLiveId());
+//                    }
+//                }
+//                break;
+//            case "deleteLiveWatchUserById":
+//                // 参数是Long类型的id,需要先查询获取liveId
+//                if (args[0] instanceof Long) {
+//                    LiveWatchUser liveWatchUser = liveWatchUserService.selectLiveWatchUserById((Long) args[0]);
+//                    if (ObjectUtil.isNotEmpty(liveWatchUser)) {
+//                        liveIds.add(liveWatchUser.getLiveId());
+//                    }
+//                }
+//                break;
+//            case "deleteLiveWatchUserByIds":
+//                // 参数是Long[]数组
+//                if (args[0] instanceof Long[]) {
+//                    Long[] ids = (Long[]) args[0];
+//                    LiveWatchUser liveWatchUser = liveWatchUserService.selectLiveWatchUserById(ids[0]);
+//                    if (ObjectUtil.isNotEmpty(liveWatchUser)) {
+//                        liveIds.add(liveWatchUser.getLiveId());
+//                    }
+//                }
+//                break;
+//            default:
+//                log.warn("未处理的方法: {}", methodName);
+//        }
+//        return liveIds;
+//    }
+//
+//
+//}

+ 15 - 0
fs-live-socket/src/main/java/com/fs/core/config/RedisConfig.java

@@ -13,6 +13,7 @@ import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.data.redis.connection.RedisConnectionFactory;
 import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.data.redis.serializer.StringRedisSerializer;
 
 /**
@@ -45,6 +46,7 @@ public class RedisConfig extends CachingConfigurerSupport
         template.setConnectionFactory(connectionFactory);
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
+        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
 
         ObjectMapper mapper = new ObjectMapper();
         mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
@@ -54,6 +56,19 @@ public class RedisConfig extends CachingConfigurerSupport
         template.setValueSerializer(serializer);
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(stringRedisSerializer);
+        template.setKeySerializer(new StringRedisSerializer());
+        template.afterPropertiesSet();
+        return template;
+    }
+
+    /**
+     * 配置StringRedisTemplate(用于字符串序列化的Redis操作)
+     */
+    @Bean
+    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
+        StringRedisTemplate template = new StringRedisTemplate();
+        template.setConnectionFactory(connectionFactory);
         template.afterPropertiesSet();
         return template;
     }

+ 1 - 1
fs-live-socket/src/main/java/com/fs/core/datasource/DynamicDataSourceContextHolder.java

@@ -23,7 +23,7 @@ public class DynamicDataSourceContextHolder
      */
     public static void setDataSourceType(String dsType)
     {
-        log.info("切换到{}数据源", dsType);
+        
         CONTEXT_HOLDER.set(dsType);
     }
 

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

@@ -126,7 +126,7 @@ public class LiveController {
 
 		LinkedHashMap<String,Object> result = (LinkedHashMap<String,Object>) params.get("WorkflowExecution");
 		String string = result.get("Object").toString();
-		videoService.updateFinishStatus("https://fs-1319721001.cos.ap-chongqing.myqcloud.com/" + string.replace(".mp4", ".m3u8"));
+		videoService.updateFinishStatus(string.replace(".mp4", ".m3u8"));
 
 		return R.ok();
 //		{app=200149.push.tlivecloud.com, appid=1319721001, appname=live, channel_id=673,

+ 330 - 62
fs-live-socket/src/main/java/com/fs/live/task/Task.java

@@ -42,12 +42,16 @@ import org.apache.commons.lang.ObjectUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.Cursor;
+import org.springframework.data.redis.core.ScanOptions;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.PostConstruct;
+import java.io.IOException;
 import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
 import java.text.ParseException;
 import java.time.Instant;
 import java.time.LocalDateTime;
@@ -90,6 +94,9 @@ public class Task {
     @Autowired
     private ILiveCouponIssueService liveCouponIssueService;
 
+    @Autowired
+    private ILiveGoodsService liveGoodsService;
+
     @Autowired
     public FsJstAftersalePushService fsJstAftersalePushService;
     @Autowired
@@ -184,6 +191,10 @@ public class Task {
                         redisCache.redisTemplate.expire(key+live.getLiveId(), 1, TimeUnit.DAYS);
                     });
                 }
+                // 清理小程序缓存 和 直播标签缓存
+                String cacheKey = String.format(LiveKeysConstant.LIVE_DATA_CACHE, live.getLiveId());
+                redisCache.deleteObject(cacheKey);
+                liveWatchUserService.clearLiveFlagCache(live.getLiveId());
             }
             // 重新更新所有在直播的缓存
             liveService.asyncToCache();
@@ -202,54 +213,143 @@ public class Task {
                         redisCache.redisTemplate.opsForZSet().remove(key + live.getLiveId(), JSON.toJSONString(liveAutoTask),liveAutoTask.getAbsValue().getTime());
                     });
                 }
+                // 直播间关闭时,下架所有直播商品
+                try {
+                    liveGoodsService.unshelfAllGoodsByLiveId(live.getLiveId());
+                } catch (Exception e) {
+                    log.error("直播间关闭下架所有商品失败, liveId: {}", live.getLiveId(), e);
+                }
+                String cacheKey = String.format(LiveKeysConstant.LIVE_DATA_CACHE, live.getLiveId());
+                redisCache.deleteObject(cacheKey);
                 webSocketServer.removeLikeCountCache(live.getLiveId());
             }
             // 重新更新所有在直播的缓存
             liveService.asyncToCache();
         }
     }
+    /**
+     * 使用SCAN命令扫描Redis键(非阻塞方式)
+     * @param pattern 键模式,如 "live:lottery_task:*"
+     * @return 匹配的键集合
+     */
+    private Set<String> scanKeys(String pattern) {
+        try {
+            // 执行Redis回调并强制类型转换
+            Object executeResult = redisCache.redisTemplate.execute((org.springframework.data.redis.core.RedisCallback<Set<String>>) connection -> {
+                Set<String> result = new HashSet<>();
+                org.springframework.data.redis.core.Cursor<byte[]> cursor = connection.scan(
+                        org.springframework.data.redis.core.ScanOptions.scanOptions()
+                                .match(pattern)
+                                .count(100) // 每次扫描100个键
+                                .build()
+                );
+
+                try {
+                    if (cursor != null) {
+                        while (cursor.hasNext()) {
+                            result.add(new String(cursor.next(), StandardCharsets.UTF_8));
+                        }
+                    }
+                } finally {
+                    if (cursor != null) {
+                        try {
+                            cursor.close();
+                        } catch (Exception e) {
+                            log.error("[定时任务] Redis Cursor关闭失败,pattern: {}", pattern, e);
+                        }
+                    }
+                }
+                return result;
+            });
+
+            // 强制类型转换+非空判断,避免空指针和类型转换异常
+            return (executeResult != null) ? (Set<String>) executeResult : new HashSet<>();
+        } catch (Exception e) {
+            log.error("[定时任务] SCAN扫描键失败,pattern: {}", pattern, e);
+            return new HashSet<>();
+        }
+    }
+
     @Scheduled(cron = "0/1 * * * * ?")
     @DistributeLock(key = "liveLotteryTask", scene = "task")
     public void liveLotteryTask() {
         long currentTime = Instant.now().toEpochMilli(); // 当前时间戳(毫秒)
-        String lotteryKey = "live:lottery_task:*";
-        Set<String> allLiveKeys = redisCache.redisTemplate.keys(lotteryKey);
-        if (allLiveKeys != null && !allLiveKeys.isEmpty()) {
-            for (String liveKey : allLiveKeys) {
-                Set<String> range = redisCache.redisTemplate.opsForZSet().rangeByScore(liveKey, 0, currentTime);
-                if (range == null || range.isEmpty()) {
-                    continue;
+        
+        // 处理抽奖任务 - 使用SCAN非阻塞扫描
+        String lotteryKeyPrefix = "live:lottery_task:";
+        String lotteryKeyPattern = lotteryKeyPrefix + "*";
+        Set<String> allLotteryKeys = scanKeys(lotteryKeyPattern);
+        if (allLotteryKeys != null && !allLotteryKeys.isEmpty()) {
+            for (String liveKey : allLotteryKeys) {
+                try {
+                    // 从键中提取 liveId: "live:lottery_task:1147" -> "1147"
+                    String liveIdStr = liveKey.substring(lotteryKeyPrefix.length());
+                    Long liveId = Long.parseLong(liveIdStr);
+                    
+                    // 获取所有到期的抽奖任务(score <= currentTime)
+                    Set<String> range = redisCache.redisTemplate.opsForZSet().rangeByScore(liveKey, 0, currentTime);
+                    if (range == null || range.isEmpty()) {
+                        continue;
+                    }
+                    
+                    // 处理到期的抽奖任务
+                    processLotteryTask(range);
+                    
+                    // 删除已处理的任务
+                    redisCache.redisTemplate.opsForZSet().removeRangeByScore(liveKey, 0, currentTime);
+                    
+                    // 如果ZSet为空,删除整个键
+                    Long zsetSize = redisCache.redisTemplate.opsForZSet().size(liveKey);
+                    if (zsetSize != null && zsetSize == 0) {
+                        redisCache.redisTemplate.delete(liveKey);
+                    }
+                } catch (Exception e) {
+                    log.error("[定时任务] 处理抽奖任务异常,liveKey: {}", liveKey, e);
                 }
-                processLotteryTask(range);
-                redisCache.redisTemplate.opsForZSet()
-                        .removeRangeByScore(liveKey, 0, currentTime);
             }
         }
 
-        String redKey = "live:red_task:*";
-        allLiveKeys = redisCache.redisTemplate.keys(redKey);
-        if (allLiveKeys == null || allLiveKeys.isEmpty()) {
+        // 处理红包任务 - 使用SCAN非阻塞扫描
+        String redKeyPrefix = "live:red_task:";
+        String redKeyPattern = redKeyPrefix + "*";
+        Set<String> allRedKeys = scanKeys(redKeyPattern);
+        if (allRedKeys == null || allRedKeys.isEmpty()) {
             return;
         }
-        for (String liveKey : allLiveKeys) {
-            Set<String> range = redisCache.redisTemplate.opsForZSet().rangeByScore(liveKey, 0, currentTime);
-            if (range == null || range.isEmpty()) {
-                continue;
-            }
-
-            updateRedStatus(range);
-            redisCache.redisTemplate.opsForZSet()
-                    .removeRangeByScore(liveKey, 0, currentTime);
+        
+        for (String liveKey : allRedKeys) {
             try {
+                // 从键中提取 liveId: "live:red_task:1147" -> "1147"
+                String liveIdStr = liveKey.substring(redKeyPrefix.length());
+                Long liveId = Long.parseLong(liveIdStr);
+                
+                // 获取所有到期的红包任务(score <= currentTime)
+                Set<String> range = redisCache.redisTemplate.opsForZSet().rangeByScore(liveKey, 0, currentTime);
+                if (range == null || range.isEmpty()) {
+                    continue;
+                }
+                
+                // 更新红包状态为已结束
+                updateRedStatus(range);
+                
+                // 删除已处理的任务
+                redisCache.redisTemplate.opsForZSet().removeRangeByScore(liveKey, 0, currentTime);
+                
+                // 如果ZSet为空,删除整个键
+                Long zsetSize = redisCache.redisTemplate.opsForZSet().size(liveKey);
+                if (zsetSize != null && zsetSize == 0) {
+                    redisCache.redisTemplate.delete(liveKey);
+                }
+                
                 // 广播红包关闭消息
                 SendMsgVo sendMsgVo = new SendMsgVo();
-                sendMsgVo.setLiveId(Long.valueOf(liveKey));
+                sendMsgVo.setLiveId(liveId);
                 sendMsgVo.setCmd("red");
                 sendMsgVo.setStatus(-1);
-                liveService.asyncToCacheLiveConfig(Long.parseLong(liveKey));
-                webSocketServer.broadcastMessage(Long.valueOf(liveKey), JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+                liveService.asyncToCacheLiveConfig(liveId);
+                webSocketServer.broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
             } catch (Exception e) {
-                log.error("更新红包状态异常", e);
+                log.error("[定时任务] 处理红包任务异常,liveKey: {}", liveKey, e);
             }
         }
     }
@@ -270,28 +370,162 @@ public class Task {
             if(totalLots <= 0) continue;
             // 先将参与记录插入数据库
             String hashKey = String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_DRAW, liveLottery.getLiveId(), liveLottery.getLotteryId());
-            Map<Object, Object> hashEntries = redisUtil.hashEntries(hashKey);
+//            Map<Object, Object> hashEntries = redisUtil.hashEntries(hashKey);
+//            List<LiveLotteryRegistration> registrationList = new ArrayList<>();
+//            if (CollUtil.isNotEmpty(hashEntries)) {
+//                registrationList = hashEntries.values().stream()
+//                        .map(value -> JSONUtil.toBean(JSONUtil.parseObj(value), LiveLotteryRegistration.class))
+//                        .collect(Collectors.toList());
+//                liveLotteryRegistrationMapper.insertLiveLotteryRegistrationBatch(registrationList);
+//            }
+
+            // 1. 配置ScanOptions:分批扫描,匹配所有字段
+            ScanOptions scanOptions = ScanOptions.scanOptions()
+                    .count(1000)
+                    .match("*")
+                    .build();
             List<LiveLotteryRegistration> registrationList = new ArrayList<>();
-            if (CollUtil.isNotEmpty(hashEntries)) {
-                registrationList = hashEntries.values().stream()
-                        .map(value -> JSONUtil.toBean(JSONUtil.parseObj(value), LiveLotteryRegistration.class))
-                        .collect(Collectors.toList());
-                liveLotteryRegistrationMapper.insertLiveLotteryRegistrationBatch(registrationList);
+            try (
+                    // 2. 开启Cursor分批扫描,try-with-resources自动关闭游标,避免资源泄露
+                    Cursor<Map.Entry<Object, Object>> cursor = redisCache.redisTemplate.opsForHash().scan(hashKey, scanOptions)
+            ) {
+                // 3. 遍历游标,渐进式获取数据
+                while (cursor.hasNext()) {
+                    Map.Entry<Object, Object> entry = null;
+                    try {
+                        // 捕获游标读取/反序列化异常
+                        entry = cursor.next();
+                    } catch (Exception e) {
+                        log.warn("[最优批量入库] 读取Redis记录失败,跳过该条,hashKey: {}, error: {}", hashKey, e.getMessage());
+                        continue;
+                    }
+
+                    if (entry == null || entry.getValue() == null) {
+                        log.warn("[最优批量入库] 记录或值为null,跳过该条,hashKey: {}", hashKey);
+                        continue;
+                    }
+
+                    Object value = entry.getValue();
+
+                     try {
+                         // 4. 安全反序列化,单个记录失败不影响整体
+                         String jsonStr = value.toString();
+
+                         // 解析JSON对象,手动处理时间戳转换为Date
+                         com.alibaba.fastjson.JSONObject jsonObject = JSON.parseObject(jsonStr);
+                         LiveLotteryRegistration registration = new LiveLotteryRegistration();
+
+                         // 设置基本字段
+                         if (jsonObject.containsKey("liveId")) {
+                             registration.setLiveId(jsonObject.getLong("liveId"));
+                         }
+                         if (jsonObject.containsKey("userId")) {
+                             registration.setUserId(jsonObject.getLong("userId"));
+                         }
+                         if (jsonObject.containsKey("lotteryId")) {
+                             registration.setLotteryId(jsonObject.getLong("lotteryId"));
+                         }
+                         if (jsonObject.containsKey("isWin")) {
+                             registration.setIsWin(jsonObject.getLong("isWin"));
+                         }
+                         if (jsonObject.containsKey("rizeLevel")) {
+                             registration.setRizeLevel(jsonObject.getLong("rizeLevel"));
+                         }
+                         // 处理时间戳转换为Date
+                         if (jsonObject.containsKey("createTime")) {
+                             Object createTimeObj = jsonObject.get("createTime");
+                             if (createTimeObj instanceof Date) {
+                                 registration.setCreateTime((Date) createTimeObj);
+                             } else if (createTimeObj instanceof Number) {
+                                 registration.setCreateTime(new Date(((Number) createTimeObj).longValue()));
+                             } else if (createTimeObj instanceof String) {
+                                 try {
+                                     registration.setCreateTime(new Date(Long.parseLong((String) createTimeObj)));
+                                 } catch (NumberFormatException e) {
+                                     // 解析失败,使用当前时间
+                                     registration.setCreateTime(new Date());
+                                 }
+                             }
+                         }
+
+                         if (jsonObject.containsKey("updateTime")) {
+                             Object updateTimeObj = jsonObject.get("updateTime");
+                             if (updateTimeObj instanceof Date) {
+                                 registration.setUpdateTime((Date) updateTimeObj);
+                             } else if (updateTimeObj instanceof Number) {
+                                 registration.setUpdateTime(new Date(((Number) updateTimeObj).longValue()));
+                             } else if (updateTimeObj instanceof String) {
+                                 try {
+                                     registration.setUpdateTime(new Date(Long.parseLong((String) updateTimeObj)));
+                                 } catch (NumberFormatException e) {
+                                     // 解析失败,使用当前时间
+                                     registration.setUpdateTime(new Date());
+                                 }
+                             }
+                         }
+
+                         if (registration != null) {
+                             registrationList.add(registration);
+                             if (registration.getCreateTime() == null) {
+                                 registration.setCreateTime(now);
+                             }
+                             if (registration.getUpdateTime() == null) {
+                                 registration.setUpdateTime(now);
+                             }
+                         } else {
+                             log.warn("[最优批量入库] 转换后对象为null,跳过该条,hashKey: {}", hashKey);
+                             continue;
+                         }
+                        // 5. 达到入库批次大小,批量插入
+                        if (registrationList.size() >= 1000) {
+                            liveLotteryRegistrationMapper.insertLiveLotteryRegistrationBatch(registrationList);
+                            log.info("[最优批量入库] 完成一批次入库,条数:{},hashKey: {}", 1000, hashKey);
+                            registrationList.clear();
+                        }
+                    } catch (Exception e) {
+                        log.warn("[最优批量入库] 解析记录失败,跳过该条,hashKey: {}, field: , error: {}",
+                                hashKey, e.getMessage());
+                        log.error(e.getMessage());
+                        continue;
+                    }
+                }
+
+                // 6. 插入剩余数据
+                if (CollectionUtils.isNotEmpty(registrationList)) {
+                    liveLotteryRegistrationMapper.insertLiveLotteryRegistrationBatch(registrationList);
+                    log.info("[最优批量入库] 完成剩余数据入库,条数:{},hashKey: {}", registrationList.size(), hashKey);
+                }
+            } catch (Exception e) {
+                log.error("[最优批量入库] 扫描Redis Hash整体失败,hashKey: {}", hashKey, e);
+            }
+
+            // 直接从数据库随机查询指定数量的参与抽奖用户(优化:只查询需要的数量,不查询全部)
+            List<LiveWatchUser> winningUsers = liveWatchUserService.selectRandomLiveWatchAndRegisterUser(
+                    liveLottery.getLiveId(), liveLottery.getLotteryId(), totalLots);
+            if(winningUsers == null || winningUsers.isEmpty()){
+                log.error("随机查询中奖用户为空,liveId: {}, lotteryId: {}, totalLots: {}", 
+                        liveLottery.getLiveId(), liveLottery.getLotteryId(), totalLots);
+                continue;
+            }
+            
+            // 如果查询到的用户数量少于奖品数量,记录警告
+            if(winningUsers.size() < totalLots) {
+                log.warn("参与抽奖用户数量({})少于奖品数量({}),liveId: {}, lotteryId: {}", 
+                        winningUsers.size(), totalLots, liveLottery.getLiveId(), liveLottery.getLotteryId());
             }
 
-            // 查询在线用户 并且参与了抽奖的用户
-            List<LiveWatchUser> liveWatchUsers = liveWatchUserService.selectLiveWatchAndRegisterUser(liveLottery.getLiveId(),liveLottery.getLotteryId());
-            if(liveWatchUsers.isEmpty()) continue;
             LiveLotteryRegistration liveLotteryRegistration;
             // 收集中奖信息
             List<LotteryVo> lotteryVos = new ArrayList<>();
+            // 使用索引遍历中奖用户列表,按顺序分配奖品
+            int userIndex = 0;
             for (LiveLotteryProductListVo liveLotteryProductListVo : products) {
-                // 随机抽奖一个用户获取奖品
+                // 为每个产品分配指定数量的奖品
                 Long totalLotsPerProduct = liveLotteryProductListVo.getTotalLots();
-                for (int i = 0; i < totalLotsPerProduct && !liveWatchUsers.isEmpty(); i++) {
-                    // 随机选择一个用户
-                    int randomIndex = new Random().nextInt(liveWatchUsers.size());
-                    LiveWatchUser winningUser = liveWatchUsers.get(randomIndex);
+                for (int i = 0; i < totalLotsPerProduct && userIndex < winningUsers.size(); i++) {
+                    // 按顺序获取中奖用户(已经从数据库随机查询,无需再次随机)
+                    LiveWatchUser winningUser = winningUsers.get(userIndex);
+                    userIndex++;
 
                     // 创建中奖记录
                     LiveUserLotteryRecord record = new LiveUserLotteryRecord();
@@ -312,8 +546,7 @@ public class Task {
                     liveLotteryRegistration.setUpdateTime(now);
                     liveLotteryRegistration.setRizeLevel(liveLotteryProductListVo.getPrizeLevel());
                     liveLotteryRegistrationMapper.updateLiveLotteryRegistrationNoId(liveLotteryRegistration);
-                    // 从候选列表中移除该用户,确保每人只能中奖一次
-                    liveWatchUsers.remove(randomIndex);
+                    
                     LotteryVo lotteryVo = new LotteryVo();
                     lotteryVo.setUserId(winningUser.getUserId());
                     lotteryVo.setUserName(winningUser.getNickName());
@@ -328,7 +561,7 @@ public class Task {
             sendMsgVo.setLiveId(liveLottery.getLiveId());
             sendMsgVo.setCmd("LotteryDetail");
             sendMsgVo.setData(JSON.toJSONString(lotteryVos));
-            webSocketServer.broadcastMessage(liveLottery.getLiveId(), JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+            webSocketServer.enqueueMessage(liveLottery.getLiveId(), JSONObject.toJSONString(R.ok().put("data", sendMsgVo)),true);
 
             liveService.asyncToCacheLiveConfig(liveLottery.getLiveId());
             // 删除缓存 同步抽奖记录
@@ -371,6 +604,8 @@ public class Task {
         }
     }
 
+    // 已关闭:autoUpdateWatchReward方法已禁用,改为使用LiveCompletionPointsController中的倒计时功能
+    /*
     @Scheduled(cron = "0 0/1 * * * ?")
     @DistributeLock(key = "autoUpdateWatchReward", scene = "task")
     @Transactional
@@ -415,6 +650,7 @@ public class Task {
             }
         }
     }
+    */
     private void saveUserRewardRecord(Live live, List<Long> userIds,Long scoreAmount) {
         for (Long userId : userIds) {
             LiveRewardRecord record = new LiveRewardRecord();
@@ -491,39 +727,71 @@ public class Task {
             return;
         liveDatas.forEach(liveData ->{
 
-            Long resultLikeCount = getAsLong(redisCache, "live:like:" + liveData.getLiveId());
-            resultLikeCount = resultLikeCount > 0L ? resultLikeCount : liveData.getLikes();
-            redisCache.setCacheObject("live:like:" + liveData.getLiveId(), resultLikeCount.intValue());
-            liveData.setLikes(
-                    resultLikeCount
-            );
+            Map<String, Integer> flagMap = liveWatchUserService.getLiveFlagWithCache(liveData.getLiveId());
+            Integer liveFlag = flagMap.get("liveFlag");
+
+            // 判断是直播还是回放
+            if (liveFlag != null && liveFlag == 1) {
+                // 直播:更新 likes 和 totalViews
+                Long resultLikeCount = getAsLong(redisCache, "live:like:" + liveData.getLiveId());
+                resultLikeCount = resultLikeCount > 0L ? resultLikeCount : liveData.getLikes();
+                redisCache.setCacheObject("live:like:" + liveData.getLiveId(), resultLikeCount.intValue());
+                liveData.setLikes(resultLikeCount);
+
+                // 从 redis 获取数据,并提供默认值,避免 NPE
+                liveData.setPageViews(
+                        Math.max( liveData.getPageViews(), Optional.ofNullable(redisCache.incr(PAGE_VIEWS_KEY + liveData.getLiveId(),0)).orElse(0L))
+                );
+                liveData.setTotalViews(
+                        Math.max( liveData.getTotalViews(), Optional.ofNullable(redisCache.incr(TOTAL_VIEWS_KEY + liveData.getLiveId(),0)).orElse(0L))
+                );
+            } else {
+                // 回放:使用 Redis 中的数据减去直播的数据,得到回放的数据
+                String likeKey = "live:like:" + liveData.getLiveId();
+                String totalViewsKey = TOTAL_VIEWS_KEY + liveData.getLiveId();
 
-       /* for (Long liveId : liveIds) {
-            LiveData liveData = liveDataService.selectLiveDataByLiveId(liveId);
-            if (liveData == null) {
-                continue; // 防止空指针异常
-            }*/
+                // 从 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;
 
-            liveData.setPageViews(
-                    Math.max( liveData.getPageViews(), Optional.ofNullable(redisCache.increment(PAGE_VIEWS_KEY + liveData.getLiveId(),0)).orElse(0L))
-            );
-            liveData.setTotalViews(
-                    Math.max( liveData.getTotalViews(), Optional.ofNullable(redisCache.increment(TOTAL_VIEWS_KEY + liveData.getLiveId(),0)).orElse(0L))
-            );
+                // 回放数据 = Redis总数据 - 直播数据
+                Long replayLikeNum = totalLikeCount - liveLikeCount;
+                Long replayViewNum = totalViewCount - liveViewCount;
+
+                // 确保回放数据不为负数
+                if (replayLikeNum < 0L) {
+                    replayLikeNum = 0L;
+                }
+                if (replayViewNum < 0L) {
+                    replayViewNum = 0L;
+                }
+
+                // 更新回放数据
+                liveData.setReplayLikeNum(replayLikeNum);
+                liveData.setReplayViewNum(replayViewNum);
+
+                // 从 redis 获取数据,并提供默认值,避免 NPE
+                liveData.setPageViews(
+                        Math.max( liveData.getPageViews(), Optional.ofNullable(redisCache.incr(PAGE_VIEWS_KEY + liveData.getLiveId(),0)).orElse(0L))
+                );
+            }
             liveData.setUniqueVisitors(
                     /*Optional.ofNullable(redisCache.getCacheSet(UNIQUE_VISITORS_KEY + liveId))
                             .map(Set::size)  // 获取集合大小
                             .map(Long::valueOf)  // 转换为 Long 类型
                             .orElse(0L)*/
-                    Math.max( liveData.getUniqueVisitors(), Optional.ofNullable(redisCache.increment(UNIQUE_VISITORS_KEY + liveData.getLiveId(),0)).orElse(0L))
+                    Math.max( liveData.getUniqueVisitors(), Optional.ofNullable(redisCache.incr(UNIQUE_VISITORS_KEY + liveData.getLiveId(),0)).orElse(0L))
             );
             liveData.setUniqueViewers(
                     /*Optional.ofNullable(redisCache.getCacheSet(UNIQUE_VIEWERS_KEY + liveId))
                             .map(Set::size)  // 获取集合大小
                             .map(Long::valueOf)  // 转换为 Long 类型
                             .orElse(0L)*/
-                    Math.max( liveData.getUniqueViewers(), Optional.ofNullable(redisCache.increment(UNIQUE_VIEWERS_KEY + liveData.getLiveId(),0)).orElse(0L))
+                    Math.max( liveData.getUniqueViewers(), Optional.ofNullable(redisCache.incr(UNIQUE_VIEWERS_KEY + liveData.getLiveId(),0)).orElse(0L))
             );
             // 使用Set大小来获取最大同时在线人数
             String onlineUsersSetKey = ONLINE_USERS_SET_KEY + liveData.getLiveId();

+ 21 - 17
fs-live-socket/src/main/java/com/fs/live/websocket/auth/WebSocketConfigurator.java

@@ -64,25 +64,29 @@ public class WebSocketConfigurator extends ServerEndpointConfig.Configurator {
             userProperties.put(AttrConstant.USER_TYPE, 0L);
         }
 
-        // 验证签名
-        if (parameterMap.containsKey(AttrConstant.SIGNATURE)) {
-            String liveIdStr = parameterMap.get(AttrConstant.LIVE_ID).get(0);
-            String userIdStr = parameterMap.get(AttrConstant.USER_ID).get(0);
-            String userTypeStr = parameterMap.get(AttrConstant.USER_TYPE).get(0);
-            String timestampStr = parameterMap.get(AttrConstant.TIMESTAMP).get(0);
-            String signatureStr = parameterMap.get(AttrConstant.SIGNATURE).get(0);
+        String userTypeStr = parameterMap.get(AttrConstant.USER_TYPE).get(0);
 
-            try {
-                if (!VerifyUtils.verifySignature(liveIdStr, userIdStr, userTypeStr, timestampStr, signatureStr)) {
-                    throw new BaseException("缺少必要的参数");
-                }
+        userProperties.put(AttrConstant.USER_TYPE,null == userTypeStr ? 0 : Long.parseLong(userTypeStr));
 
-                userProperties.put(AttrConstant.USER_TYPE, Long.parseLong(userTypeStr));
-            } catch (Exception e) {
-                log.warn("webSocket连接验签失败 msg: {}", e.getMessage(), e);
-                throw new BaseException("缺少必要的参数");
-            }
-        }
+        // 验证签名
+//        if (parameterMap.containsKey(AttrConstant.SIGNATURE)) {
+//            String liveIdStr = parameterMap.get(AttrConstant.LIVE_ID).get(0);
+//            String userIdStr = parameterMap.get(AttrConstant.USER_ID).get(0);
+//            String userTypeStr = parameterMap.get(AttrConstant.USER_TYPE).get(0);
+//            String timestampStr = parameterMap.get(AttrConstant.TIMESTAMP).get(0);
+//            String signatureStr = parameterMap.get(AttrConstant.SIGNATURE).get(0);
+//
+//            try {
+//                if (!VerifyUtils.verifySignature(liveIdStr, userIdStr, userTypeStr, timestampStr, signatureStr)) {
+//                    throw new BaseException("缺少必要的参数");
+//                }
+//
+//                userProperties.put(AttrConstant.USER_TYPE, Long.parseLong(userTypeStr));
+//            } catch (Exception e) {
+//                log.warn("webSocket连接验签失败 msg: {}", e.getMessage(), e);
+//                throw new BaseException("缺少必要的参数");
+//            }
+//        }
     }
 
 }

+ 2 - 0
fs-live-socket/src/main/java/com/fs/live/websocket/bean/SendMsgVo.java

@@ -23,6 +23,8 @@ public class SendMsgVo {
     private Long userType;
     @ApiModelProperty("消息代码")
     private String cmd;
+    @ApiModelProperty("消息ID(删除时必传,用于删库或从待入库队列移除)")
+    private Long msgId;
     @ApiModelProperty("发送消息")
     private String msg;
     @ApiModelProperty("消息")

+ 8 - 14
fs-live-socket/src/main/java/com/fs/live/websocket/handle/LiveChatHandler.java

@@ -51,7 +51,6 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
      */
     @Override
     public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
-        log.debug("事件");
         // 处理 WebSocket 握手完成事件
         if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
             Long userId = ctx.channel().attr(AttrConstant.ATTR_USER_ID).get();
@@ -69,15 +68,15 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
             roomGroup.add(ctx.channel());
 
             if (userType == 0) {
-                // 加入房间
-                liveWatchUserService.join(liveId, userId);
-                room.put(userId, ctx.channel());
 
-                FsUser fsUser = fsUserService.selectFsUserByUserId(userId);
+
+                FsUser fsUser = fsUserService.selectWsFsUserById(userId);
                 if (Objects.isNull(fsUser)) {
                     ctx.channel().writeAndFlush(new TextWebSocketFrame("Error: 用户信息错误")).addListener(ChannelFutureListener.CLOSE);
                     return;
-                }
+                } // 加入房间
+                liveWatchUserService.join(fsUser,liveId, userId,"");
+                room.put(userId, ctx.channel());
 
                 LiveWatchUserVO liveWatchUserVO = liveWatchUserService.selectWatchUserByLiveIdAndUserId(liveId, userId);
 
@@ -97,7 +96,6 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
                 adminRoom.add(ctx.channel());
             }
 
-            log.debug("加入webSocket liveId: {}, userId: {}, 直播间人数: {}", liveId, userId, room.size());
         }
     }
 
@@ -154,7 +152,6 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
      */
     @Override
     protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
-        log.debug("接收到消息 data: {}", textWebSocketFrame.text());
         Long liveId = channelHandlerContext.channel().attr(AttrConstant.ATTR_LIVE_ID).get();
         Long userType = channelHandlerContext.channel().attr(AttrConstant.ATTR_USER_TYPE).get();
 
@@ -175,8 +172,8 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
                     liveMsg.setCreateTime(new Date());
 
                     if (userType == 0) {
-                        LiveWatchUser liveWatchUser = liveWatchUserService.getByLiveIdAndUserId(msg.getLiveId(), msg.getUserId());
-                        if(liveWatchUser.getMsgStatus() == 1){
+                        List<LiveWatchUser> liveWatchUser = liveWatchUserService.getByLiveIdAndUserId(msg.getLiveId(), msg.getUserId());
+                        if(!liveWatchUser.isEmpty() && liveWatchUser.get(0).getMsgStatus() == 1){
                             sendMessage(channelHandlerContext.channel(), JSONObject.toJSONString(R.error("你以被禁言")));
                             return;
                         }
@@ -203,7 +200,6 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
      */
     @Override
     public void channelInactive(ChannelHandlerContext ctx) throws Exception {
-        log.debug("断开连接");
         Long userId = ctx.channel().attr(AttrConstant.ATTR_USER_ID).get();
         Long liveId = ctx.channel().attr(AttrConstant.ATTR_LIVE_ID).get();
         Long userType = ctx.channel().attr(AttrConstant.ATTR_USER_TYPE).get();
@@ -217,7 +213,7 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
         ChannelGroup roomGroup = getRoomGroup(liveId);
 
         if (userType == 0) {
-            FsUser fsUser = fsUserService.selectFsUserByUserId(userId);
+            FsUser fsUser = fsUserService.selectWsFsUserById(userId);
             liveWatchUserService.close(liveId, userId);
             room.remove(userId);
 
@@ -250,7 +246,6 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
             roomGroups.remove(liveId);
         }
 
-        log.debug("断开webSocket liveId: {}, userId: {}, 直播间人数: {}", liveId, userId, room.size());
 
     }
 
@@ -262,7 +257,6 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
      */
     @Override
     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
-        log.error("连接异常 msg: {}", cause.getMessage(), cause);
         ctx.close();
     }
 }

+ 1156 - 118
fs-live-socket/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -18,6 +18,7 @@ import com.fs.live.service.*;
 import com.fs.live.vo.LiveGoodsVo;
 import com.fs.store.domain.FsUser;
 import com.fs.store.service.IFsUserService;
+import org.springframework.data.redis.core.StringRedisTemplate;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.time.DateUtils;
 import org.springframework.scheduling.annotation.Scheduled;
@@ -25,13 +26,15 @@ import org.springframework.stereotype.Component;
 
 import javax.websocket.*;
 import javax.websocket.server.ServerEndpoint;
-import java.io.EOFException;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.util.*;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.ThreadLocalRandom;
-import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.StampedLock;
 
 import static com.fs.common.constant.LiveKeysConstant.*;
 import static com.fs.common.constant.LiveKeysConstant.TOP_MSG;
@@ -47,11 +50,98 @@ public class WebSocketServer {
     private final static ConcurrentHashMap<Long, ConcurrentHashMap<Long, Session>> rooms = new ConcurrentHashMap<>();
     // 管理端连接
     private final static ConcurrentHashMap<Long, CopyOnWriteArrayList<Session>> adminRooms = new ConcurrentHashMap<>();
+
+    // Session发送锁,避免同一会话并发发送消息(使用StampedLock提升性能)
+    private final static ConcurrentHashMap<String, StampedLock> sessionLocks = new ConcurrentHashMap<>();
+    // 最大消息大小(字节):超过此大小将分片发送,默认64KB
+    private final static int MAX_MESSAGE_SIZE = 64 * 1024; // 64KB
+    // 分片大小(字节):每片大小
+    private final static int CHUNK_SIZE = 32 * 1024; // 32KB
+    // Session发送队列监控:记录每个Session的待发送消息数
+    private final static ConcurrentHashMap<String, AtomicLong> sessionQueueSizes = new ConcurrentHashMap<>();
+    // 心跳超时缓存:key=sessionId,value=最后心跳时间戳
+    private final static ConcurrentHashMap<String, Long> heartbeatCache = new ConcurrentHashMap<>();
+    // 心跳超时时间(毫秒):3分钟无心跳则认为超时
+    private final static long HEARTBEAT_TIMEOUT = 3 * 60 * 1000;
+    // admin房间消息发送线程池(单线程,保证串行化)
+    private final static ConcurrentHashMap<Long, ExecutorService> adminExecutors = new ConcurrentHashMap<>();
+    // 心跳响应专用线程池(高并发优化)
+    private static final ExecutorService HEARTBEAT_EXECUTOR = new ThreadPoolExecutor(
+            16,  // 核心线程数:根据32核CPU,设置为16
+            64,  // 最大线程数:高并发场景
+            60L, TimeUnit.SECONDS,  // 空闲线程存活时间
+            new LinkedBlockingQueue<>(10000),  // 队列容量:10000
+            new ThreadFactory() {
+                private final AtomicLong counter = new AtomicLong(0);
+                @Override
+                public Thread newThread(Runnable r) {
+                    Thread t = new Thread(r, "websocket-heartbeat-" + counter.incrementAndGet());
+                    t.setDaemon(true);
+                    return t;
+                }
+            },
+            new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略:调用者运行
+    );
+    // 1. 新增一个数据库异步操作线程池(在类中定义)
+    private static final ExecutorService DB_ASYNC_EXECUTOR = new ThreadPoolExecutor(
+            16,
+            64,
+            60L, TimeUnit.SECONDS,
+            new LinkedBlockingQueue<>(10000),
+            new ThreadFactory() {
+                private final AtomicLong counter = new AtomicLong(0);
+                @Override
+                public Thread newThread(Runnable r) {
+                    Thread t = new Thread(r, "websocket-db-async-" + counter.incrementAndGet());
+                    t.setDaemon(true);
+                    return t;
+                }
+            },
+            new ThreadPoolExecutor.CallerRunsPolicy()
+    );
+
+    // 消息队列系统
+    // 每个直播间的消息队列,使用优先级队列支持管理员消息插队
+    private final static ConcurrentHashMap<Long, PriorityBlockingQueue<QueueMessage>> messageQueues = new ConcurrentHashMap<>();
+    // 每个直播间的消费者线程
+    private final static ConcurrentHashMap<Long, Thread> consumerThreads = new ConcurrentHashMap<>();
+    // 每个直播间的消费者线程控制标志
+    private final static ConcurrentHashMap<Long, AtomicBoolean> consumerRunningFlags = new ConcurrentHashMap<>();
+    // 每个直播间队列的总大小(字节数)
+    private final static ConcurrentHashMap<Long, AtomicLong> queueSizes = new ConcurrentHashMap<>();
+    // 消息队列最大容量:10000
+    private final static int MAX_QUEUE_SIZE = 50000;
+    // 消息队列最大大小:200MB
+    private final static long MAX_QUEUE_SIZE_BYTES = 500L * 1024L * 1024L; // 200MB
+    // 上下线消息采样率:10%
+    private final static double ENTRY_EXIT_SAMPLE_RATE = 0.1;
+    // 聊天消息批量插入队列
+    private final static BlockingQueue<LiveMsg> liveMsgQueue = new LinkedBlockingQueue<>();
+    // 聊天消息批量插入阈值:500条
+    private final static int LIVE_MSG_BATCH_SIZE = 500;
+    // 聊天消息批量插入定时器间隔:5秒
+    private final static long LIVE_MSG_BATCH_INTERVAL = 5000; // 5秒
+    // Redis key:被禁言用户Set(按直播间)
+    private final static String BLOCKED_USERS_KEY = "live:blocked:users:%s";
+    private final static String MUTED_USERS_KEY = "live:muted:users:%s";
+
+    /** 临时消息ID生成器(负数,与数据库自增正数区分) */
+    private final static AtomicLong tempMsgIdCounter = new AtomicLong(0);
+    /** 已入库消息的 临时ID -> 数据库msgId 映射,用于删除时按临时ID找到真实ID */
+    private final static ConcurrentHashMap<Long, Long> tempIdToRealId = new ConcurrentHashMap<>();
+    private final static int TEMP_ID_MAP_MAX_SIZE = 100000;
+
     private final RedisCache redisCache = SpringUtils.getBean(RedisCache.class);
+    private final StringRedisTemplate stringRedisTemplate = SpringUtils.getBean(StringRedisTemplate.class);
     private final ILiveMsgService liveMsgService = SpringUtils.getBean(ILiveMsgService.class);
     private final ILiveService liveService = SpringUtils.getBean(ILiveService.class);
     private final ILiveWatchUserService liveWatchUserService = SpringUtils.getBean(ILiveWatchUserService.class);
     private final IFsUserService fsUserService = SpringUtils.getBean(IFsUserService.class);
+    
+    // 用户信息缓存Key前缀
+    private static final String USER_INFO_CACHE_KEY = "ws:user:info:%s";
+    // 用户信息缓存过期时间:12小时
+    private static final long USER_INFO_CACHE_EXPIRE_HOURS = 12;
     private final ILiveDataService liveDataService = SpringUtils.getBean(ILiveDataService.class);
     private final ProductionWordFilter productionWordFilter = SpringUtils.getBean(ProductionWordFilter.class);
     private final ILiveRedConfService liveRedConfService =  SpringUtils.getBean(ILiveRedConfService.class);
@@ -60,6 +150,10 @@ public class WebSocketServer {
     private final ILiveUserFirstEntryService liveUserFirstEntryService =  SpringUtils.getBean(ILiveUserFirstEntryService.class);
     private final ILiveCouponIssueService liveCouponIssueService =  SpringUtils.getBean(ILiveCouponIssueService.class);
     private final LiveCouponMapper liveCouponMapper = SpringUtils.getBean(LiveCouponMapper.class);
+    private static Random random = new Random();
+
+    // Redis key 前缀:用户进入直播间时间
+    public static final String USER_ENTRY_TIME_KEY = "live:user:entry:time:%s:%s";
 
     // 直播间在线用户缓存
 //    private static final ConcurrentHashMap<Long, Integer> liveOnlineUsers = new ConcurrentHashMap<>();
@@ -73,11 +167,12 @@ public class WebSocketServer {
         long liveId = (long) userProperties.get("liveId");
         long userId = (long) userProperties.get("userId");
         long userType = (long) userProperties.get("userType");
+        String location = (String) userProperties.get("location");
         Live live = liveService.selectLiveByLiveId(liveId);
         if (live == null) {
             throw new BaseException("未找到直播间");
         }
-        long companyId = live.getCompanyId() == null ? -1L : live.getCompanyId();
+        long companyId = -1L;
         long companyUserId = -1L;
         if (!Objects.isNull(userProperties.get("companyId"))) {
             companyId = (long) userProperties.get("companyId");
@@ -92,58 +187,40 @@ public class WebSocketServer {
 
         // 记录连接信息 管理员不记录
         if (userType == 0) {
-            FsUser fsUser = fsUserService.selectFsUserByUserId(userId);
+            // 使用缓存获取轻量级用户信息(只包含必要字段)
+//            LightweightUserInfo userInfo = getLightweightUserInfo(userId);
+//            FsUser fsUser = toFsUser(userInfo);
+            FsUser fsUser = fsUserService.selectWsFsUserById(userId);
             if (Objects.isNull(fsUser)) {
                 throw new BaseException("用户信息错误");
             }
 
-            LiveWatchUser liveWatchUserVO = liveWatchUserService.join(liveId, userId);
+            LiveWatchUser liveWatchUserVO = liveWatchUserService.join(fsUser,liveId, userId, location);
             room.put(userId, session);
-            // 直播间浏览量 +1
-            redisCache.increment(PAGE_VIEWS_KEY + liveId, 1);
-
-            // 累计观看人次 +1
-            redisCache.increment(TOTAL_VIEWS_KEY + liveId, 1);
-
-            // 记录在线人数
-            redisCache.increment(ONLINE_USERS_KEY + liveId, 1);
-            // 将用户ID添加到在线用户Set中
-            String onlineUsersSetKey = ONLINE_USERS_SET_KEY + liveId;
-            redisCache.redisTemplate.opsForSet().add(onlineUsersSetKey, String.valueOf(userId));
-            // 获取Set的大小作为当前在线人数
-            Long currentOnlineCount = redisCache.redisTemplate.opsForSet().size(onlineUsersSetKey);
-            //最大同时在线人数 - 使用Set大小来判断
-            Integer maxOnline = redisCache.getCacheObject(MAX_ONLINE_USERS_KEY + liveId);
-            int currentOnline = currentOnlineCount != null ? currentOnlineCount.intValue() : 0;
-            if (maxOnline == null || currentOnline > maxOnline) {
-                redisCache.setCacheObject(MAX_ONLINE_USERS_KEY + liveId, currentOnline);
-            }
 
-            // 判断是否是该直播间的首次访客(独立访客统计)
-            boolean isFirstVisit = redisCache.setIfAbsent(USER_VISIT_KEY + liveId + ":" + userId, 1, 1, TimeUnit.DAYS);
-            if (isFirstVisit) {
-
-                redisCache.increment(UNIQUE_VISITORS_KEY + liveId, 1);
-            }
-
-            // 判断是否是首次进入直播间的观众
-            boolean isFirstViewer = redisCache.setIfAbsent(UNIQUE_VIEWERS_KEY + liveId + ":" + userId, 1, 1, TimeUnit.DAYS);
-            if (isFirstViewer) {
-                redisCache.increment(UNIQUE_VIEWERS_KEY + liveId, 1);
-            }
+            // 存储用户进入直播间的时间到 Redis(用于计算在线时长)
+            // 如果已经存在进入时间,说明是重连,不应该覆盖,保持原来的进入时间
+            String entryTimeKey = String.format(USER_ENTRY_TIME_KEY, liveId, userId);
+            Long existingEntryTime = redisCache.getCacheObject(entryTimeKey);
+            
+            // 使用 Pipeline 批量执行 Redis 操作,减少网络交互次数
+            PipelineResult pipelineResult = executeUserJoinPipeline(liveId, userId, entryTimeKey, existingEntryTime);
 
             liveWatchUserVO.setMsgStatus(liveWatchUserVO.getMsgStatus());
-            SendMsgVo sendMsgVo = new SendMsgVo();
-            sendMsgVo.setLiveId(liveId);
-            sendMsgVo.setUserId(userId);
-            sendMsgVo.setUserType(userType);
-            sendMsgVo.setCmd("entry");
-            sendMsgVo.setMsg("用户进入");
-            sendMsgVo.setData(JSONObject.toJSONString(liveWatchUserVO));
-            sendMsgVo.setNickName(fsUser.getNickname());
-            sendMsgVo.setAvatar(fsUser.getAvatar());
-            // 广播连接消息
-            broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+            // 上线消息采样10%进入队列
+            if (random.nextDouble() < ENTRY_EXIT_SAMPLE_RATE) {
+                SendMsgVo sendMsgVo = new SendMsgVo();
+                sendMsgVo.setLiveId(liveId);
+                sendMsgVo.setUserId(userId);
+                sendMsgVo.setUserType(userType);
+                sendMsgVo.setCmd("entry");
+                sendMsgVo.setMsg("用户进入");
+                sendMsgVo.setData(JSONObject.toJSONString(liveWatchUserVO));
+                sendMsgVo.setNickName(fsUser.getNickname());
+                sendMsgVo.setAvatar(fsUser.getAvatar());
+                // 将上线消息加入队列
+                enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)), false);
+            }
 
             LiveUserFirstEntry liveUserFirstEntry = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveId, userId);
             if (liveUserFirstEntry != null) {
@@ -155,7 +232,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();
@@ -171,13 +248,28 @@ public class WebSocketServer {
                 liveUserFirstEntry.setUpdateTime( date);
                 liveUserFirstEntryService.insertLiveUserFirstEntry(liveUserFirstEntry);
             }
+//            redisCache.setCacheObject( "live:user:first:entry:" + liveId + ":" + userId, liveUserFirstEntry, 4, TimeUnit.HOURS);
 
 
         } else {
             adminRoom.add(session);
+            // 为admin房间创建单线程执行器,保证串行化发送
+            adminExecutors.computeIfAbsent(liveId, k -> Executors.newSingleThreadExecutor());
         }
 
-        log.debug("加入webSocket liveId: {}, userId: {}, 直播间人数: {}, 管理端人数: {}", liveId, userId, room.size(), adminRoom.size());
+        // 初始化Session锁(使用StampedLock提升性能)
+        sessionLocks.putIfAbsent(session.getId(), new StampedLock());
+        // 初始化心跳时间
+        heartbeatCache.put(session.getId(), System.currentTimeMillis());
+
+        // 如果有session,启动消费者线程
+        ConcurrentHashMap<Long, Session> tempRoom = getRoom(liveId);
+        List<Session> tempAdminRoom = getAdminRoom(liveId);
+        boolean hasSession = (tempRoom != null && !tempRoom.isEmpty()) ||
+                (tempAdminRoom != null && !tempAdminRoom.isEmpty());
+        if (hasSession) {
+            startConsumerThread(liveId);
+        }
     }
 
     //关闭连接时调用
@@ -192,11 +284,7 @@ public class WebSocketServer {
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
         List<Session> adminRoom = getAdminRoom(liveId);
         if (userType == 0) {
-            FsUser fsUser = fsUserService.selectFsUserByUserId(userId);
-            if (Objects.isNull(fsUser)) {
-                throw new BaseException("用户信息错误");
-            }
-
+            // close方法不需要fsUser参数,直接调用即可
             LiveWatchUser liveWatchUserVO = liveWatchUserService.close(liveId, userId);
             room.remove(userId);
 
@@ -205,28 +293,59 @@ public class WebSocketServer {
             }
 
 
-            // 直播间在线人数 -1
-            redisCache.increment(ONLINE_USERS_KEY + liveId, -1);
-            // 从在线用户Set中移除用户ID
-            String onlineUsersSetKey = ONLINE_USERS_SET_KEY + liveId;
-            redisCache.redisTemplate.opsForSet().remove(onlineUsersSetKey, String.valueOf(userId));
-            SendMsgVo sendMsgVo = new SendMsgVo();
-            sendMsgVo.setLiveId(liveId);
-            sendMsgVo.setUserId(userId);
-            sendMsgVo.setUserType(userType);
-            sendMsgVo.setCmd("out");
-            sendMsgVo.setMsg("用户离开");
-            sendMsgVo.setData(JSONObject.toJSONString(liveWatchUserVO));
-            sendMsgVo.setNickName(fsUser.getNickname());
-            sendMsgVo.setAvatar(fsUser.getAvatar());
-
-            // 广播离开消息
-            broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+            // 使用 Pipeline 批量执行 Redis 操作,减少网络交互次数
+            executeUserClosePipeline(liveId, userId);
+            // 下线消息采样10%进入队列
+            if (random.nextDouble() < ENTRY_EXIT_SAMPLE_RATE) {
+                // 从缓存获取用户信息(轻量级,只包含必要字段)
+                FsUser fsUser = fsUserService.selectWsFsUserById(userId);
+
+                SendMsgVo sendMsgVo = new SendMsgVo();
+                sendMsgVo.setLiveId(liveId);
+                sendMsgVo.setUserId(userId);
+                sendMsgVo.setUserType(userType);
+                sendMsgVo.setCmd("out");
+                sendMsgVo.setMsg("用户离开");
+                sendMsgVo.setData(JSONObject.toJSONString(liveWatchUserVO));
+                sendMsgVo.setNickName(fsUser.getNickname());
+                sendMsgVo.setAvatar(fsUser.getAvatar());
+                // 将下线消息加入队列
+                enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)), false);
+            }
         } else {
             adminRoom.remove(session);
+            // 如果admin房间为空,关闭并清理执行器
+            if (adminRoom.isEmpty()) {
+                ExecutorService executor = adminExecutors.remove(liveId);
+                if (executor != null) {
+                    executor.shutdown();
+                }
+                adminRooms.remove(liveId);
+            }
+        }
+        // 清理Session相关资源
+        String sessionId = session.getId();
+        heartbeatCache.remove(sessionId);
+        
+        // 清理Session锁(确保锁被移除,避免内存泄漏)
+        StampedLock lock = sessionLocks.remove(sessionId);
+        if (lock != null) {
+            // 尝试获取写锁并立即释放,确保没有线程持有该锁
+            try {
+                long stamp = lock.tryWriteLock(10, TimeUnit.MILLISECONDS);
+                if (stamp != 0) {
+                    lock.unlockWrite(stamp);
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
         }
+        
+        // 清理发送队列监控
+        sessionQueueSizes.remove(sessionId);
 
-        log.debug("离开webSocket liveId: {}, userId: {}, 直播间人数: {}, 管理端人数: {}", liveId, userId, room.size(), adminRoom.size());
+        // 检查并清理空的直播间资源
+        cleanupEmptyRoom(liveId);
     }
 
     //收到客户端信息
@@ -236,6 +355,7 @@ public class WebSocketServer {
 
         long liveId = (long) userProperties.get("liveId");
         long userType = (long) userProperties.get("userType");
+        boolean isAdmin = false;
 
         SendMsgVo msg = JSONObject.parseObject(message, SendMsgVo.class);
         if(msg.isOn()) return;
@@ -243,7 +363,22 @@ public class WebSocketServer {
         try {
             switch (msg.getCmd()) {
                 case "heartbeat":
-                    sendMessage(session, JSONObject.toJSONString(R.ok().put("data", msg)));
+                    // 更新心跳时间(使用putIfAbsent优化,减少不必要的更新)
+                    String sessionId = session.getId();
+                    heartbeatCache.put(sessionId, System.currentTimeMillis());
+                    
+                    // 异步发送心跳响应,避免阻塞主线程
+                    HEARTBEAT_EXECUTOR.submit(() -> {
+                        try {
+                            if (session.isOpen()) {
+                                String response = JSONObject.toJSONString(R.ok().put("data", msg));
+                                // 使用异步发送,不阻塞
+                                sendMessage(session,response);
+                            }
+                        } catch (Exception ignored) {
+
+                        }
+                    });
                     break;
                 case "sendMsg":
                     msg.setMsg(productionWordFilter.filter(msg.getMsg()).getFilteredText());
@@ -257,22 +392,51 @@ public class WebSocketServer {
                     liveMsg.setCreateTime(new Date());
 
                     if (userType == 0) {
-                        LiveWatchUser liveWatchUser = liveWatchUserService.getByLiveIdAndUserId(msg.getLiveId(), msg.getUserId());
-                        if(liveWatchUser.getMsgStatus() == 1){
+                        // 使用Redis Set检查用户是否被禁言
+                        String blockedUsersKey = String.format(MUTED_USERS_KEY, msg.getLiveId());
+                        boolean isBlocked = redisCache.redisTemplate.opsForSet().isMember(blockedUsersKey, String.valueOf(msg.getUserId()));
+                        if (isBlocked) {
                             sendMessage(session, JSONObject.toJSONString(R.error("你已被禁言")));
                             return;
                         }
-
-                        liveMsgService.insertLiveMsg(liveMsg);
+                        Map<String, Integer> flagMap = liveWatchUserService.getLiveFlagWithCache(liveId);
+                        Integer liveFlag = flagMap.get("liveFlag");
+                        Integer replayFlag = flagMap.get("replayFlag");
+                        liveMsg.setLiveFlag(liveFlag);
+                        liveMsg.setReplayFlag(replayFlag);
+                        // 分配临时 msgId(负数),便于前端删除未入库消息及广播删除
+                        long tempId = -tempMsgIdCounter.incrementAndGet();
+                        liveMsg.setMsgId(tempId);
+                        // 将消息加入批量插入队列
+                        try {
+                            liveMsgQueue.offer(liveMsg);
+                            // 如果队列超过阈值,立即触发批量插入
+                            if (liveMsgQueue.size() >= LIVE_MSG_BATCH_SIZE) {
+                                flushLiveMsgQueue();
+                            }
+                        } catch (Exception e) {
+                            log.error("[聊天消息队列] 添加消息失败, liveId: {}, userId: {}, error: {}", 
+                                    liveMsg.getLiveId(), liveMsg.getUserId(), e.getMessage());
+                        }
                     }
 
                     msg.setOn(true);
                     msg.setData(JSONObject.toJSONString(liveMsg));
 
-                    // 广播消息
-                    broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)));
+                    // 将消息加入队列(普通用户消息)
+                    isAdmin = (userType == 1);
+                    enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)), isAdmin);
                     break;
                 case "sendGift":
+                    liveMsg = new LiveMsg();
+                    liveMsg.setLiveId(msg.getLiveId());
+                    liveMsg.setUserId(msg.getUserId());
+                    liveMsg.setNickName(msg.getNickName());
+                    liveMsg.setAvatar(msg.getAvatar());
+                    liveMsg.setMsg(msg.getMsg());
+                    msg.setOn(true);
+                    msg.setData(JSONObject.toJSONString(liveMsg));
+                    enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)), true);
                     break;
                 case "sendTopMsg":
                     msg.setMsg(productionWordFilter.filter(msg.getMsg()).getFilteredText());
@@ -287,7 +451,7 @@ public class WebSocketServer {
                     msg.setOn(true);
                     msg.setData(JSONObject.toJSONString(liveMsg));
                     // 广播消息
-                    broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)));
+                    enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)), true);
                     // 放在当前活动里面
                     redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG, liveId, TOP_MSG));
                     redisCache.setCacheObject(String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG, liveId, TOP_MSG), JSONObject.toJSONString(liveMsg));
@@ -296,6 +460,9 @@ public class WebSocketServer {
                 case "blockUser":
                     sendBlockMessage(liveId, msg.getUserId());
                     break;
+                case "mutedUser":
+                    handleMutedUser(msg.getLiveId(), msg.getUserId(), msg.getStatus());
+                    break;
                 case "goods":
                     sendGoodsMessage(msg);
                     break;
@@ -323,12 +490,35 @@ public class WebSocketServer {
     }
 
     private void deleteMsg(long liveId,SendMsgVo msg) {
+        Long msgId = msg.getMsgId();
+        if (msgId == null && StringUtils.isNotEmpty(msg.getMsg())) {
+            try {
+                msgId = Long.parseLong(msg.getMsg().trim());
+            } catch (NumberFormatException ignored) {
+                // 兼容旧版:msg 可能是文本
+            }
+        }
+        final Long resolvedMsgId = msgId;
+        if (resolvedMsgId != null) {
+            if (resolvedMsgId > 0) {
+                // 真实 msgId:本服务与后台可能不同库或后台未调用,此处统一删库(幂等),再广播
+                liveMsgService.deleteLiveMsgByMsgId(resolvedMsgId);
+            } else {
+                // 临时 msgId:从队列移除,若已入库则按映射删库,再广播
+                liveMsgQueue.removeIf(m -> resolvedMsgId.equals(m.getMsgId()));
+                Long realId = tempIdToRealId.remove(resolvedMsgId);
+                if (realId != null) {
+                    liveMsgService.deleteLiveMsgByMsgId(realId);
+                }
+            }
+        }
         SendMsgVo sendMsgVo = new SendMsgVo();
         sendMsgVo.setLiveId(liveId);
         sendMsgVo.setUserType(0L);
         sendMsgVo.setCmd("deleteMsg");
         sendMsgVo.setMsg(msg.getMsg());
-        broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+        sendMsgVo.setMsgId(resolvedMsgId);
+        enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)), true);
     }
 
     private void processCoupon(long liveId, SendMsgVo msg) {
@@ -348,7 +538,7 @@ public class WebSocketServer {
         } else {
             redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_COUPON_NUM , couponIssueId));
         }
-        broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)));
+        enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)), true);
     }
 
 
@@ -363,7 +553,7 @@ public class WebSocketServer {
         liveService.asyncToCacheLiveConfig(liveId);
         msg.setLiveId(liveId);
         msg.setData(JSONObject.toJSONString(liveGoods));
-        broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)));
+        enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)), true);
     }
 
     /**
@@ -378,7 +568,7 @@ public class WebSocketServer {
         if (Objects.nonNull(liveRedConf)) {
             liveService.asyncToCacheLiveConfig(liveId);
             msg.setData(JSONObject.toJSONString(liveRedConf));
-            broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)));
+            enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)), true);
         }
     }
 
@@ -394,7 +584,7 @@ public class WebSocketServer {
         if (Objects.nonNull(liveLotteryConf)) {
             liveService.asyncToCacheLiveConfig(liveId);
             msg.setData(JSONObject.toJSONString(liveLotteryConf));
-            broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)));
+            enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)), true);
         }
     }
 
@@ -404,12 +594,7 @@ public class WebSocketServer {
         try {
             this.onClose(session);
         } catch (Exception e) {
-            log.error("webSocket 错误 onError", e);
-        }
-        if (throwable instanceof EOFException) {
-            log.info("WebSocket连接被客户端正常关闭(EOF),sessionId: {}", session.getId());
-        } else {
-            log.error("WebSocket连接错误", throwable);
+            log.error("webSocket 错误处理失败", e);
         }
     }
 
@@ -431,9 +616,133 @@ public class WebSocketServer {
         return adminRooms.computeIfAbsent(liveId, k -> new CopyOnWriteArrayList<>());
     }
 
-    //发送消息
+    /**
+     * 发送消息(线程安全,支持并发写入保护)
+     * 只对写入操作加锁,确保同一Session的消息串行发送
+     * 
+     * @param session WebSocket会话
+     * @param message 消息内容
+     * @throws IOException 发送失败时抛出异常
+     */
     public void sendMessage(Session session, String message) throws IOException {
-        session.getAsyncRemote().sendText(message);
+        if (session == null || !session.isOpen()) {
+            return;
+        }
+
+        String sessionId = session.getId();
+        
+        // 检查消息大小,超大消息分片发送
+//        byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8);
+//        if (messageBytes.length > MAX_MESSAGE_SIZE) {
+//            sendMessageInChunks(session, message, messageBytes);
+//            return;
+//        }
+
+        // 获取Session锁(使用StampedLock提升性能)
+        StampedLock lock = sessionLocks.get(sessionId);
+        if (lock == null) {
+            // 如果锁不存在,创建一个新锁
+            lock = sessionLocks.computeIfAbsent(sessionId, k -> new StampedLock());
+        }
+
+        // 使用写锁保证同一Session的消息串行发送
+        // 注意:只对写入操作加锁,读取操作(如session.isOpen())在锁外进行
+        long stamp = lock.writeLock();
+        try {
+            // 双重检查,确保连接仍然打开(在锁内再次检查,避免锁期间连接关闭)
+            if (!session.isOpen()) {
+                return;
+            }
+            
+            try {
+                // 使用同步发送,确保前一次写入完成后再发起新写入
+                session.getAsyncRemote().sendText(message);
+            } catch (IllegalStateException e) {
+                log.error(e.getMessage());
+                // TEXT_FULL_WRITING状态,说明前一次写入未完成
+                String errorMsg = e.getMessage();
+                if (errorMsg != null && errorMsg.contains("TEXT_FULL_WRITING")) {
+                    // 等待一小段时间后重试
+                    try {
+                        Thread.sleep(10);
+                        if (session.isOpen()) {
+                            session.getBasicRemote().sendText(message);
+                        }
+                    } catch (InterruptedException ie) {
+                        Thread.currentThread().interrupt();
+                        throw new IOException("发送消息被中断", ie);
+                    }
+                } else {
+                    throw new IOException("WebSocket连接异常", e);
+                }
+            } catch (Exception e) {
+                log.error(e.getMessage());
+                // 连接已断开或其他异常
+                String errorMsg = e.getMessage();
+                if (errorMsg != null && (
+                    errorMsg.contains("Broken pipe") || 
+                    errorMsg.contains("Connection reset") ||
+                    errorMsg.contains("Connection closed"))) {
+                    // 静默处理,避免日志刷屏
+                    return;
+                }
+                throw new IOException("WebSocket发送失败", e);
+            }
+        } finally {
+            // 确保锁在finally中释放,避免死锁
+            lock.unlockWrite(stamp);
+        }
+    }
+
+    /**
+     * 分片发送超大消息(安全处理UTF-8字符边界)
+     */
+    private void sendMessageInChunks(Session session, String message, byte[] messageBytes) throws IOException {
+        String sessionId = session.getId();
+        int totalSize = messageBytes.length;
+        
+        log.warn("[超大消息分片] sessionId: {}, 消息大小: {} bytes, 将分片发送", 
+                sessionId, totalSize);
+        
+        int offset = 0;
+        int chunkIndex = 0;
+        
+        while (offset < totalSize) {
+            // 计算当前分片的结束位置
+            int chunkEnd = Math.min(offset + CHUNK_SIZE, totalSize);
+            
+            // 如果不在字符串末尾,需要找到UTF-8字符边界
+            if (chunkEnd < totalSize) {
+                // 从chunkEnd向前查找,找到完整的UTF-8字符边界
+                // UTF-8字符的第一个字节:0xxxxxxx 或 110xxxxx 或 1110xxxx 或 11110xxx
+                // UTF-8字符的后续字节:10xxxxxx
+                while (chunkEnd > offset && (messageBytes[chunkEnd] & 0xC0) == 0x80) {
+                    chunkEnd--;
+                }
+            }
+            
+            // 提取分片(确保不截断UTF-8字符)
+            String chunk = new String(messageBytes, offset, chunkEnd - offset, StandardCharsets.UTF_8);
+            
+            // 每个分片都通过sendMessage发送,确保串行化和锁保护
+            sendMessage(session, chunk);
+            
+            offset = chunkEnd;
+            chunkIndex++;
+            
+            // 分片之间稍作延迟,避免缓冲区溢出
+            if (offset < totalSize) {
+                try {
+                    Thread.sleep(5);
+                } catch (InterruptedException e) {
+                    log.error(e.getMessage());
+                    Thread.currentThread().interrupt();
+                    throw new IOException("分片发送被中断", e);
+                }
+            }
+        }
+        
+        log.debug("[超大消息分片] sessionId: {}, 完成分片发送, 分片数: {}", sessionId, chunkIndex);
     }
 
     public void sendIntegralMessage(Long liveId, Long userId,Long scoreAmount) {
@@ -446,11 +755,30 @@ public class WebSocketServer {
         sendMsgVo.setData(String.valueOf(scoreAmount));
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
         Session session = room.get(userId);
-        if(Objects.isNull( session)) return;
-        session.getAsyncRemote().sendText(JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+        if(Objects.isNull(session) || !session.isOpen()) return;
+        sendWithRetry(session, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)), 1);
+    }
+
+    /**
+     * 处理禁言/解禁:将用户ID加入或移出直播间禁言 Redis Set,发送消息时据此判断是否允许发言
+     */
+    private void handleMutedUser(Long liveId, Long userId, Integer status) {
+        if (liveId == null || userId == null) {
+            return;
+        }
+        String mutedUsersKey = String.format(MUTED_USERS_KEY, liveId);
+        if (status != null && status == 1) {
+            redisCache.redisTemplate.opsForSet().add(mutedUsersKey, String.valueOf(userId));
+        } else {
+            redisCache.redisTemplate.opsForSet().remove(mutedUsersKey, String.valueOf(userId));
+        }
     }
 
     private void sendBlockMessage(Long liveId, Long userId) {
+        // 将被禁言用户添加到Redis Set中
+        String blockedUsersKey = String.format(BLOCKED_USERS_KEY, liveId);
+        redisCache.redisTemplate.opsForSet().add(blockedUsersKey, String.valueOf(userId));
+
         SendMsgVo sendMsgVo = new SendMsgVo();
         sendMsgVo.setLiveId(liveId);
         sendMsgVo.setUserId(userId);
@@ -460,12 +788,12 @@ public class WebSocketServer {
         sendMsgVo.setData(null);
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
         Session session = room.get(userId);
-        if(Objects.isNull( session)) return;
-        session.getAsyncRemote().sendText(JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+        if(Objects.isNull(session) || !session.isOpen()) return;
+        sendWithRetry(session, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)), 1);
     }
 
     /**
-     * 广播消息
+     * 广播消息(优化:添加Session去重,避免同一Session重复发送)
      * @param liveId   直播间ID
      * @param message  消息内容
      */
@@ -473,55 +801,138 @@ public class WebSocketServer {
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
         List<Session> adminRoom = getAdminRoom(liveId);
 
+
+        
+        // 收集普通用户房间的Session(去重)
         room.forEach((k, v) -> {
-            if (v.isOpen()) {
-                sendWithRetry(v,message,7);
+            if (v != null && v.isOpen()) {
+                try {
+                    sendMessage(v, message);
+                } catch (Exception e) {
+                    // 单个Session发送失败不影响其他Session
+                    log.error(e.getMessage());
+                }
             }
         });
+        
+        // 收集admin房间的Session(去重)
         adminRoom.forEach(v -> {
-            if (v.isOpen()) {
-                sendWithRetry(v,message,7);
+            if (v != null && v.isOpen()) {
+                try {
+                    sendMessage(v, message);
+                } catch (Exception e) {
+                    // 单个Session发送失败不影响其他Session
+                    log.error(e.getMessage());
+                }
             }
         });
+
     }
 
     /**
-     * 广播点赞消息
+     * 广播点赞消息(优化:添加Session去重)
      * @param liveId   直播间ID
      * @param message  消息内容
      */
     public void broadcastLikeMessage(Long liveId, String message) {
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
+        
+        // 使用Set去重,避免同一Session被多次处理
+        Set<Session> uniqueSessions = new HashSet<>();
         room.forEach((k, v) -> {
-            if (v.isOpen()) {
-                sendWithRetry(v,message,7);
+            if (v != null && v.isOpen()) {
+                uniqueSessions.add(v);
+            }
+        });
+        
+        // 并行发送给所有唯一Session
+        uniqueSessions.parallelStream().forEach(session -> {
+            try {
+                sendWithRetry(session, message, 1);
+            } catch (Exception e) {
+                // 单个Session发送失败不影响其他Session
+                log.error(e.getMessage());
             }
         });
     }
 
+    /**
+     * 带重试机制的消息发送(统一使用sendMessage,避免直接调用getAsyncRemote)
+     * 解决心跳机制与业务消息并发冲突问题
+     */
     private void sendWithRetry(Session session, String message, int maxRetries) {
+        if (session == null || !session.isOpen()) {
+            return;
+        }
+        
+        String sessionId = session.getId();
+        
+        // 更新发送队列监控
+        sessionQueueSizes.computeIfAbsent(sessionId, k -> new AtomicLong(0)).incrementAndGet();
+        
         int attempts = 0;
         while (attempts < maxRetries) {
             try {
-                if(session.isOpen()) {
-                    session.getAsyncRemote().sendText(message);
+                // 使用sendMessage统一发送,确保锁机制生效,避免并发冲突
+                sendMessage(session, message);
+                
+                // 发送成功,更新监控
+                AtomicLong queueSize = sessionQueueSizes.get(sessionId);
+                if (queueSize != null) {
+                    queueSize.decrementAndGet();
                 }
                 return;  // 发送成功,退出
-            } catch (Exception e) {
-                if (e.getMessage() != null && e.getMessage().contains("TEXT_FULL_WRITING")) {
-                    attempts++;
+            } catch (IOException e) {
+                log.error(e.getMessage());
+                String errorMsg = e.getMessage();
+                
+                // 连接已断开,清理资源并返回
+                if (errorMsg != null && (
+                    errorMsg.contains("Broken pipe") || 
+                    errorMsg.contains("Connection reset") ||
+                    errorMsg.contains("Connection closed") ||
+                    errorMsg.contains("连接已断开"))) {
+                    // 清理监控
+                    sessionQueueSizes.remove(sessionId);
+                    return;
+                }
+                
+                // TEXT_FULL_WRITING或其他可重试错误
+                attempts++;
+                if (attempts < maxRetries) {
                     try {
-                        TimeUnit.MILLISECONDS.sleep(ThreadLocalRandom.current().nextInt(5, 100));
+                        // 指数退避策略
+                        long delay = Math.min(50L * (1L << attempts), 500L);
+                        TimeUnit.MILLISECONDS.sleep(ThreadLocalRandom.current().nextInt(5, (int)delay));
                     } catch (InterruptedException ie) {
+                        log.error(e.getMessage());
                         Thread.currentThread().interrupt();
                         break;
                     }
-                } else {
-                    throw e;
+                }
+            } catch (Exception e) {
+                log.error(e.getMessage());
+                // 其他异常,记录日志
+                if (attempts == 0) {
+                    log.warn("[发送消息重试] 失败,sessionId: {}, attempts: {}, error: {}", 
+                            sessionId, attempts + 1, e.getMessage());
+                }
+                attempts++;
+                if (attempts >= maxRetries) {
+                    break;
                 }
             }
         }
-        log.info("超过重试次数, 消息 {}",message);
+        
+        // 超过重试次数,清理监控
+        AtomicLong queueSize = sessionQueueSizes.get(sessionId);
+        if (queueSize != null) {
+            queueSize.decrementAndGet();
+        }
+        
+        if (attempts >= maxRetries) {
+            log.debug("[发送消息] 超过重试次数, sessionId: {}, attempts: {}", sessionId, attempts);
+        }
     }
 
     public void handleAutoTask(LiveAutoTask task) {
@@ -614,7 +1025,7 @@ public class WebSocketServer {
 
             }
             msg.setStatus(1);
-            broadcastMessage(task.getLiveId(), JSONObject.toJSONString(R.ok().put("data", msg)));
+            enqueueMessage(task.getLiveId(), JSONObject.toJSONString(R.ok().put("data", msg)), true);
         } catch (Exception e) {
             log.error("定时任务执行异常:{}", e.getMessage());
         }
@@ -629,6 +1040,27 @@ public class WebSocketServer {
         lastLikeCountCache.remove(liveId);
     }
 
+    @Scheduled(fixedRate = 2000)// 每2秒执行一次
+    public void broadcastUserNumMessage() {
+        // 遍历每个直播间
+        for (Map.Entry<Long, ConcurrentHashMap<Long, Session>> entry : rooms.entrySet()) {
+            Long liveId = entry.getKey();
+            ConcurrentHashMap<Long, Session> room = entry.getValue();
+
+            // 统计当前直播间的在线人数
+            int onlineCount = room.size();
+
+            // 构造消息
+            SendMsgVo sendMsgVo = new SendMsgVo();
+            sendMsgVo.setLiveId(liveId);
+            sendMsgVo.setCmd("userCount");
+            sendMsgVo.setData(String.valueOf(onlineCount));
+
+            // 广播当前直播间的在线人数
+            broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+        }
+    }
+
 
     @Scheduled(fixedRate = 300)// 每0.3分钟执行一次
     public void broadcastLikeMessage() {
@@ -644,7 +1076,7 @@ public class WebSocketServer {
                 String valueStr = cacheObject.toString().trim();
                 current = Integer.parseInt(valueStr);
             } catch (NumberFormatException e) {
-                log.error("点赞数格式错误,liveId: {}, value: {}", liveId, cacheObject, e);
+                log.error(e.getMessage());
                 continue;
             }
             Integer last = lastLikeCountCache.getOrDefault(liveId, 0);
@@ -659,4 +1091,610 @@ public class WebSocketServer {
         }
         lastLikeCountCache.keySet().removeIf(liveId -> !activeLiveIds.contains(liveId));
     }
+
+    @Scheduled(fixedRate = LIVE_MSG_BATCH_INTERVAL) // 每5秒执行一次
+    public void batchInsertLiveMsg() {
+        flushLiveMsgQueue();
+    }
+
+    /**
+     * 监控Session发送队列积压情况(每分钟执行一次)
+     */
+    @Scheduled(fixedRate = 60000) // 每分钟执行一次
+    public void monitorSessionQueues() {
+        long totalQueued = sessionQueueSizes.values().stream()
+                .mapToLong(AtomicLong::get)
+                .sum();
+        
+        // 统计积压严重的Session(队列长度>10)
+        long highQueueCount = sessionQueueSizes.values().stream()
+                .filter(size -> size.get() > 10)
+                .count();
+        
+        if (totalQueued > 100 || highQueueCount > 0) {
+            log.warn("[Session队列监控] 总积压消息数: {}, 高积压Session数: {}", 
+                    totalQueued, highQueueCount);
+            
+            // 输出积压最严重的5个Session
+            sessionQueueSizes.entrySet().stream()
+                    .filter(entry -> entry.getValue().get() > 10)
+                    .sorted((a, b) -> Long.compare(b.getValue().get(), a.getValue().get()))
+                    .limit(5)
+                    .forEach(entry -> {
+                        log.warn("[Session队列监控] sessionId: {}, 积压消息数: {}", 
+                                entry.getKey(), entry.getValue().get());
+                    });
+        }
+    }
+
+    /**
+     * 批量插入聊天消息队列:逐条插入以获取每条消息的 msgId,并维护临时ID到真实ID的映射,便于删除时按临时ID删库。
+     */
+    private void flushLiveMsgQueue() {
+        if (liveMsgQueue.isEmpty()) {
+            return;
+        }
+
+        List<LiveMsg> batchList = new ArrayList<>();
+        int count = liveMsgQueue.drainTo(batchList, LIVE_MSG_BATCH_SIZE);
+
+        if (count > 0) {
+            for (LiveMsg liveMsg : batchList) {
+                try {
+                    Long tempId = liveMsg.getMsgId();
+                    liveMsg.setMsgId(null);
+                    liveMsgService.insertLiveMsg(liveMsg);
+                    Long realId = liveMsg.getMsgId();
+                    if (tempId != null && realId != null) {
+                        tempIdToRealId.put(tempId, realId);
+                        if (tempIdToRealId.size() > TEMP_ID_MAP_MAX_SIZE) {
+                            tempIdToRealId.clear();
+                        }
+                    }
+                } catch (Exception e) {
+                    log.error("[聊天消息批量插入] 单条插入失败, liveId: {}, userId: {}, error: {}",
+                            liveMsg.getLiveId(), liveMsg.getUserId(), e.getMessage(), e);
+                    try {
+                        liveMsgQueue.offer(liveMsg);
+                    } catch (Exception ex) {
+                        log.error("[聊天消息队列] 重新入队失败, liveId: {}, userId: {}", liveMsg.getLiveId(), liveMsg.getUserId());
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 定时清理无效会话(每分钟执行一次)
+     * 检查心跳超时的会话并关闭
+     */
+    @Scheduled(fixedRate = 60000) // 每分钟执行一次
+    public void cleanInactiveSessions() {
+        long currentTime = System.currentTimeMillis();
+        int cleanedCount = 0;
+
+        // 遍历所有直播间
+        for (Map.Entry<Long, ConcurrentHashMap<Long, Session>> roomEntry : rooms.entrySet()) {
+            Long liveId = roomEntry.getKey();
+            ConcurrentHashMap<Long, Session> room = roomEntry.getValue();
+
+            // 检查普通用户会话
+            List<Long> toRemove = new ArrayList<>();
+            room.forEach((userId, session) -> {
+                Long lastHeartbeat = heartbeatCache.get(session.getId());
+                if (lastHeartbeat != null && (currentTime - lastHeartbeat) > HEARTBEAT_TIMEOUT) {
+                    toRemove.add(userId);
+                    try {
+                        if (session.isOpen()) {
+                            session.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "心跳超时"));
+                        }
+                    } catch (Exception e) {
+                        log.error("关闭超时会话失败: sessionId={}, liveId={}, userId={}",
+                                session.getId(), liveId, userId, e);
+                    }
+                }
+            });
+
+            // 移除超时的会话
+            toRemove.forEach(room::remove);
+            cleanedCount += toRemove.size();
+        }
+
+        // 检查admin房间
+        for (Map.Entry<Long, CopyOnWriteArrayList<Session>> adminEntry : adminRooms.entrySet()) {
+            Long liveId = adminEntry.getKey();
+            CopyOnWriteArrayList<Session> adminRoom = adminEntry.getValue();
+
+            List<Session> toRemoveAdmin = new ArrayList<>();
+            for (Session session : adminRoom) {
+                Long lastHeartbeat = heartbeatCache.get(session.getId());
+                if (lastHeartbeat != null && (currentTime - lastHeartbeat) > HEARTBEAT_TIMEOUT) {
+                    toRemoveAdmin.add(session);
+                    try {
+                        if (session.isOpen()) {
+                            session.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "心跳超时"));
+                        }
+                    } catch (Exception e) {
+                        log.error("关闭admin超时会话失败: sessionId={}, liveId={}",
+                                session.getId(), liveId, e);
+                    }
+                }
+            }
+
+            // 移除超时的admin会话
+            toRemoveAdmin.forEach(adminRoom::remove);
+            cleanedCount += toRemoveAdmin.size();
+        }
+
+        if (cleanedCount > 0) {
+            if (random.nextInt(10) == 1) {
+                log.info("已清理 {} 个无效会话", cleanedCount);
+            }
+        }
+    }
+    /**
+     * 消息队列包装类,支持优先级(管理员消息优先级更高)
+     */
+    private static class QueueMessage implements Comparable<QueueMessage> {
+        private final String message;
+        private final long timestamp;
+        private final int priority; // 0=普通消息, 1=管理员消息(优先级更高)
+        private final long sequence; // 序列号,用于相同优先级消息的FIFO排序
+        private final long sizeBytes; // 消息大小(字节数)
+
+        private static final AtomicLong sequenceGenerator = new AtomicLong(0);
+
+        public QueueMessage(String message, boolean isAdmin) {
+            this.message = message;
+            this.timestamp = System.currentTimeMillis();
+            this.priority = isAdmin ? 1 : 0;
+            this.sequence = sequenceGenerator.getAndIncrement();
+            // 计算消息大小(UTF-8编码)
+            this.sizeBytes = message != null ? message.getBytes(StandardCharsets.UTF_8).length : 0;
+        }
+
+        public String getMessage() {
+            return message;
+        }
+
+        public long getSizeBytes() {
+            return sizeBytes;
+        }
+
+        @Override
+        public int compareTo(QueueMessage other) {
+            // 优先级高的先处理(管理员消息)
+            int priorityCompare = Integer.compare(other.priority, this.priority);
+            if (priorityCompare != 0) {
+                return priorityCompare;
+            }
+            // 相同优先级按序列号排序(FIFO)
+            return Long.compare(this.sequence, other.sequence);
+        }
+    }
+
+    /**
+     * 获取或创建消息队列
+     */
+    private PriorityBlockingQueue<QueueMessage> getMessageQueue(Long liveId) {
+        return messageQueues.computeIfAbsent(liveId, k -> new PriorityBlockingQueue<>());
+    }
+
+    /**
+     * 启动消费者线程(如果还没有启动)
+     */
+    private void startConsumerThread(Long liveId) {
+        consumerRunningFlags.computeIfAbsent(liveId, k -> new AtomicBoolean(false));
+        AtomicBoolean runningFlag = consumerRunningFlags.get(liveId);
+
+        // 如果线程已经在运行,直接返回
+        if (runningFlag.get()) {
+            return;
+        }
+
+        // 尝试启动消费者线程
+        synchronized (consumerRunningFlags) {
+            if (runningFlag.compareAndSet(false, true)) {
+                Thread consumerThread = new Thread(() -> {
+                    PriorityBlockingQueue<QueueMessage> queue = getMessageQueue(liveId);
+                    log.info("[消息队列] 启动消费者线程, liveId={}", liveId);
+
+                    while (runningFlag.get()) {
+                        try {
+                            // 检查是否还有session,如果没有则退出
+                            ConcurrentHashMap<Long, Session> room = rooms.get(liveId);
+                            List<Session> adminRoom = adminRooms.get(liveId);
+
+                            boolean hasSession = (room != null && !room.isEmpty()) ||
+                                    (adminRoom != null && !adminRoom.isEmpty());
+
+                            if (!hasSession) {
+                                log.info("[消息队列] 直播间无session,停止消费者线程, liveId={}", liveId);
+                                break;
+                            }
+
+                            // 从队列中取消息,最多等待1秒
+                            QueueMessage queueMessage = queue.poll(1, TimeUnit.SECONDS);
+                            if (queueMessage != null) {
+                                // 更新队列大小(减少)
+                                AtomicLong currentSize = queueSizes.get(liveId);
+                                if (currentSize != null) {
+                                    currentSize.addAndGet(-queueMessage.getSizeBytes());
+                                }
+                                // 广播消息
+                                broadcastMessageFromQueue(liveId, queueMessage.getMessage());
+                            }
+                        } catch (InterruptedException e) {
+                            log.error(e.getMessage());
+                            Thread.currentThread().interrupt();
+                            break;
+                        } catch (Exception e) {
+                            log.error("[消息队列] 消费消息异常, liveId={}", liveId, e);
+                        }
+                    }
+
+                    // 清理资源
+                    runningFlag.set(false);
+                    consumerThreads.remove(liveId);
+                    log.info("[消息队列] 消费者线程已停止, liveId={}", liveId);
+                }, "MessageConsumer-" + liveId);
+
+                consumerThread.setDaemon(true);
+                consumerThread.start();
+                consumerThreads.put(liveId, consumerThread);
+            }
+        }
+    }
+
+    /**
+     * 停止消费者线程
+     */
+    private void stopConsumerThread(Long liveId) {
+        AtomicBoolean runningFlag = consumerRunningFlags.get(liveId);
+        if (runningFlag != null) {
+            runningFlag.set(false);
+        }
+        Thread consumerThread = consumerThreads.remove(liveId);
+        if (consumerThread != null) {
+            consumerThread.interrupt();
+        }
+    }
+
+    /**
+     * 将消息加入队列
+     * @param liveId 直播间ID
+     * @param message 消息内容
+     * @param isAdmin 是否是管理员消息(管理员消息会插队)
+     * @return 是否成功加入队列
+     */
+    public boolean enqueueMessage(Long liveId, String message, boolean isAdmin) {
+        PriorityBlockingQueue<QueueMessage> queue = getMessageQueue(liveId);
+        AtomicLong currentSize = queueSizes.computeIfAbsent(liveId, k -> new AtomicLong(0));
+
+        // 计算新消息的大小
+        long messageSize = message != null ? message.getBytes(StandardCharsets.UTF_8).length : 0;
+
+        // 检查队列条数限制
+        if (!isAdmin && queue.size() >= MAX_QUEUE_SIZE) {
+            log.warn("[消息队列] 队列条数已满,丢弃消息, liveId={}, queueSize={}", liveId, queue.size());
+            return false;
+        }
+
+        // 检查队列大小限制(200MB)
+        long newTotalSize = currentSize.get() + messageSize;
+        if (newTotalSize > MAX_QUEUE_SIZE_BYTES) {
+            if (!isAdmin) {
+                // 普通消息超过大小限制,直接丢弃
+                log.warn("[消息队列] 队列大小超过限制,丢弃普通消息, liveId={}, currentSize={}MB, messageSize={}KB",
+                        liveId, currentSize.get() / (1024.0 * 1024.0), messageSize / 1024.0);
+                return false;
+            } else {
+                // 管理员消息:需要移除一些普通消息以腾出空间
+                long needToFree = newTotalSize - MAX_QUEUE_SIZE_BYTES;
+                long freedSize = removeMessagesToFreeSpace(queue, currentSize, needToFree, true);
+                if (freedSize < needToFree) {
+                    log.warn("[消息队列] 无法释放足够空间,管理员消息可能无法入队, liveId={}, needToFree={}KB, freed={}KB",
+                            liveId, needToFree / 1024.0, freedSize / 1024.0);
+                    // 即使空间不足,也尝试入队(可能会超过限制,但管理员消息优先级高)
+                }
+            }
+        }
+
+        // 如果是管理员消息且队列条数已满,移除一个普通消息
+        if (isAdmin && queue.size() >= MAX_QUEUE_SIZE) {
+            // 由于是优先级队列,普通消息(priority=0)会在队列末尾
+            // 尝试移除一个普通消息,为管理员消息腾出空间
+            QueueMessage removed = null;
+            Iterator<QueueMessage> iterator = queue.iterator();
+            while (iterator.hasNext()) {
+                QueueMessage msg = iterator.next();
+                if (msg.priority == 0) {
+                    removed = msg;
+                    break;
+                }
+            }
+            if (removed != null) {
+                queue.remove(removed);
+                currentSize.addAndGet(-removed.getSizeBytes());
+                log.debug("[消息队列] 管理员消息插队,移除普通消息, liveId={}", liveId);
+            } else {
+                // 如果没有普通消息,移除队列末尾的消息(可能是最早的管理员消息)
+                // 这种情况很少发生,因为管理员消息通常较少
+                log.warn("[消息队列] 队列条数已满且无普通消息可移除, liveId={}", liveId);
+            }
+        }
+
+        QueueMessage queueMessage = new QueueMessage(message, isAdmin);
+        queue.offer(queueMessage);
+        currentSize.addAndGet(messageSize);
+
+        // 如果有session,确保消费者线程在运行
+        ConcurrentHashMap<Long, Session> room = rooms.get(liveId);
+        List<Session> adminRoom = adminRooms.get(liveId);
+        boolean hasSession = (room != null && !room.isEmpty()) ||
+                (adminRoom != null && !adminRoom.isEmpty());
+
+        if (hasSession) {
+            startConsumerThread(liveId);
+        }
+
+        return true;
+    }
+
+    /**
+     * 移除消息以释放空间
+     * @param queue 消息队列
+     * @param currentSize 当前队列大小(原子变量)
+     * @param needToFree 需要释放的空间(字节数)
+     * @param onlyRemoveNormal 是否只移除普通消息(true=只移除普通消息,false=可以移除任何消息)
+     * @return 实际释放的空间(字节数)
+     */
+    private long removeMessagesToFreeSpace(PriorityBlockingQueue<QueueMessage> queue,
+                                           AtomicLong currentSize,
+                                           long needToFree,
+                                           boolean onlyRemoveNormal) {
+        long freedSize = 0;
+        List<QueueMessage> toRemove = new ArrayList<>();
+
+        // 收集需要移除的消息(优先移除普通消息)
+        Iterator<QueueMessage> iterator = queue.iterator();
+        while (iterator.hasNext() && freedSize < needToFree) {
+            QueueMessage msg = iterator.next();
+            if (!onlyRemoveNormal || msg.priority == 0) {
+                toRemove.add(msg);
+                freedSize += msg.getSizeBytes();
+            }
+        }
+
+        // 如果只移除普通消息但空间还不够,可以移除管理员消息
+        if (onlyRemoveNormal && freedSize < needToFree) {
+            iterator = queue.iterator();
+            while (iterator.hasNext() && freedSize < needToFree) {
+                QueueMessage msg = iterator.next();
+                if (msg.priority == 1 && !toRemove.contains(msg)) {
+                    toRemove.add(msg);
+                    freedSize += msg.getSizeBytes();
+                }
+            }
+        }
+
+        // 移除消息并更新大小
+        for (QueueMessage msg : toRemove) {
+            if (queue.remove(msg)) {
+                currentSize.addAndGet(-msg.getSizeBytes());
+            }
+        }
+
+        if (freedSize > 0) {
+            log.info("[消息队列] 释放队列空间, removedCount={}, freedSize={}KB",
+                    toRemove.size(), freedSize / 1024.0);
+        }
+
+        return freedSize;
+    }
+
+    /**
+     * 从队列中消费消息并广播
+     */
+    private void broadcastMessageFromQueue(Long liveId, String message) {
+        broadcastMessage(liveId, message);
+    }
+
+    /**
+     * 检查并清理空的直播间资源
+     */
+    private void cleanupEmptyRoom(Long liveId) {
+        ConcurrentHashMap<Long, Session> room = rooms.get(liveId);
+        List<Session> adminRoom = adminRooms.get(liveId);
+
+        boolean hasSession = (room != null && !room.isEmpty()) ||
+                (adminRoom != null && !adminRoom.isEmpty());
+
+        if (!hasSession) {
+            // 停止消费者线程
+            stopConsumerThread(liveId);
+            // 清理消息队列
+            messageQueues.remove(liveId);
+            consumerRunningFlags.remove(liveId);
+            queueSizes.remove(liveId);
+            log.info("[消息队列] 清理空直播间资源, liveId={}", liveId);
+        }
+    }
+
+
+    /**
+     * Redis Pipeline 批量操作结果包装类
+     */
+    private static class PipelineResult {
+        Long currentOnlineCount;
+        Integer maxOnline;
+        boolean isFirstVisit;
+        boolean isFirstViewer;
+    }
+
+    /**
+     * 批量执行用户进入直播间的 Redis 操作(使用 Pipeline 提升性能)
+     *
+     * @param liveId 直播间ID
+     * @param userId 用户ID
+     * @param entryTimeKey 进入时间Key
+     * @param existingEntryTime 已存在的进入时间
+     * @return PipelineResult 包含批量操作的结果
+     */
+    private PipelineResult executeUserJoinPipeline(Long liveId, Long userId, String entryTimeKey, Long existingEntryTime) {
+        PipelineResult result = new PipelineResult();
+        
+        // 使用 Pipeline 批量执行 Redis 操作
+        List<Object> pipelineResults = redisCache.redisTemplate.executePipelined(
+                (org.springframework.data.redis.core.RedisCallback<Object>) connection -> {
+                    // 1. 设置进入时间(如果不存在)
+                    if (existingEntryTime == null) {
+                        connection.setEx(
+                                redisCache.redisTemplate.getKeySerializer().serialize(entryTimeKey),
+                                24 * 3600, // 24小时
+                                redisCache.redisTemplate.getValueSerializer().serialize(System.currentTimeMillis())
+                        );
+                    }
+                    
+                    // 2. 批量递增操作
+                    String pageViewsKey = PAGE_VIEWS_KEY + liveId;
+                    String totalViewsKey = TOTAL_VIEWS_KEY + liveId;
+                    String onlineUsersKey = ONLINE_USERS_KEY + liveId;
+                    
+                    connection.incr(redisCache.redisTemplate.getKeySerializer().serialize(pageViewsKey));
+                    connection.incr(redisCache.redisTemplate.getKeySerializer().serialize(totalViewsKey));
+                    connection.incr(redisCache.redisTemplate.getKeySerializer().serialize(onlineUsersKey));
+                    
+                    // 3. Set 操作:添加用户到在线用户Set
+                    String onlineUsersSetKey = ONLINE_USERS_SET_KEY + liveId;
+                    connection.sAdd(
+                            redisCache.redisTemplate.getKeySerializer().serialize(onlineUsersSetKey),
+                            redisCache.redisTemplate.getValueSerializer().serialize(String.valueOf(userId))
+                    );
+                    
+                    // 4. 获取Set大小
+                    connection.sCard(redisCache.redisTemplate.getKeySerializer().serialize(onlineUsersSetKey));
+                    
+                    // 5. 获取最大在线人数
+                    String maxOnlineKey = MAX_ONLINE_USERS_KEY + liveId;
+                    connection.get(redisCache.redisTemplate.getKeySerializer().serialize(maxOnlineKey));
+                    
+                    // 6. 判断是否首次访客
+                    String userVisitKey = USER_VISIT_KEY + liveId + ":" + userId;
+                    connection.setNX(
+                            redisCache.redisTemplate.getKeySerializer().serialize(userVisitKey),
+                            redisCache.redisTemplate.getValueSerializer().serialize(1)
+                    );
+                    connection.expire(
+                            redisCache.redisTemplate.getKeySerializer().serialize(userVisitKey),
+                            86400 // 1天
+                    );
+                    
+                    // 7. 判断是否首次观众
+                    String uniqueViewersUserKey = UNIQUE_VIEWERS_KEY + liveId + ":" + userId;
+                    connection.setNX(
+                            redisCache.redisTemplate.getKeySerializer().serialize(uniqueViewersUserKey),
+                            redisCache.redisTemplate.getValueSerializer().serialize(1)
+                    );
+                    connection.expire(
+                            redisCache.redisTemplate.getKeySerializer().serialize(uniqueViewersUserKey),
+                            86400 // 1天
+                    );
+                    
+                    return null; // Pipeline 模式下返回值会被忽略
+                }
+        );
+        
+        // 解析 Pipeline 结果
+        try {
+            int index = 0;
+            if (existingEntryTime == null) {
+                index++; // 跳过 setEx 结果
+            }
+            index += 3; // 跳过 3 个 incr 结果
+            index++; // 跳过 sAdd 结果
+            
+            // 获取 Set 大小
+            if (index < pipelineResults.size() && pipelineResults.get(index) != null) {
+                result.currentOnlineCount = ((Number) pipelineResults.get(index)).longValue();
+            }
+            index++;
+            
+            // 获取最大在线人数
+            if (index < pipelineResults.size() && pipelineResults.get(index) != null) {
+                try {
+                    String maxOnlineStr = pipelineResults.get(index).toString();
+                    result.maxOnline = Integer.parseInt(maxOnlineStr);
+                } catch (Exception e) {
+                    // 解析失败,忽略
+                    log.error(e.getMessage());
+                }
+            }
+            index++;
+            
+            // 判断是否首次访客
+            if (index < pipelineResults.size() && pipelineResults.get(index) != null) {
+                result.isFirstVisit = Boolean.TRUE.equals(pipelineResults.get(index));
+                if (result.isFirstVisit) {
+                    // 首次访客,需要递增独立访客数
+                    redisCache.increment(UNIQUE_VISITORS_KEY + liveId, 1);
+                }
+            }
+            index += 2; // 跳过 expire 结果
+            
+            // 判断是否首次观众
+            if (index < pipelineResults.size() && pipelineResults.get(index) != null) {
+                result.isFirstViewer = Boolean.TRUE.equals(pipelineResults.get(index));
+                if (result.isFirstViewer) {
+                    // 首次观众,需要递增独立观众数
+                    redisCache.increment(UNIQUE_VIEWERS_KEY + liveId, 1);
+                }
+            }
+            
+            // 更新最大在线人数(如果需要)
+            if (result.currentOnlineCount != null) {
+                int currentOnline = result.currentOnlineCount.intValue();
+                if (result.maxOnline == null || currentOnline > result.maxOnline) {
+                    redisCache.setCacheObject(MAX_ONLINE_USERS_KEY + liveId, currentOnline);
+                }
+            }
+        } catch (Exception e) {
+            log.error(e.getMessage());
+        }
+        
+        return result;
+    }
+
+    /**
+     * 批量执行用户离开直播间的 Redis 操作(使用 Pipeline 提升性能)
+     *
+     * @param liveId 直播间ID
+     * @param userId 用户ID
+     */
+    private void executeUserClosePipeline(Long liveId, Long userId) {
+        // 使用 Pipeline 批量执行 Redis 操作
+        redisCache.redisTemplate.executePipelined(
+                (org.springframework.data.redis.core.RedisCallback<Object>) connection -> {
+                    // 1. 在线人数 -1
+                    String onlineUsersKey = ONLINE_USERS_KEY + liveId;
+                    connection.incrBy(
+                            redisCache.redisTemplate.getKeySerializer().serialize(onlineUsersKey),
+                            -1
+                    );
+                    
+                    // 2. 从在线用户Set中移除用户ID
+                    String onlineUsersSetKey = ONLINE_USERS_SET_KEY + liveId;
+                    connection.sRem(
+                            redisCache.redisTemplate.getKeySerializer().serialize(onlineUsersSetKey),
+                            redisCache.redisTemplate.getValueSerializer().serialize(String.valueOf(userId))
+                    );
+                    
+                    // 3. 删除进入时间记录
+                    String entryTimeKey = String.format(USER_ENTRY_TIME_KEY, liveId, userId);
+                    connection.del(redisCache.redisTemplate.getKeySerializer().serialize(entryTimeKey));
+                    
+                    return null; // Pipeline 模式下返回值会被忽略
+                }
+        );
+    }
 }

+ 13 - 2
fs-live-socket/src/main/resources/application-dev.yml

@@ -34,6 +34,7 @@ spring:
                 password: Ylrz_1q2w3e4r5t6y
             # 从库数据源
             slave:
+                enabled: true
                 # 从数据源开关/默认关闭
                 url: jdbc:mysql://10.0.0.3:3306/fs_ffhx_shop?allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                 username: root
@@ -41,9 +42,9 @@ spring:
             # 初始连接数
             initialSize: 5
             # 最小连接池数量
-            minIdle: 10
+            minIdle: 20
             # 最大连接池数量
-            maxActive: 20
+            maxActive: 128
             # 配置获取连接等待超时的时间
             maxWait: 60000
             # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
@@ -77,3 +78,13 @@ spring:
                 wall:
                     config:
                         multi-statement-allow: true
+rocketmq:
+    name-server: rmq-16b45v4p9n.rocketmq.cd.qcloud.tencenttdmq.com:8080 # RocketMQ NameServer 地址
+    producer:
+        group: my-producer-group
+        access-key: ak16b45v4p9n150d89395b3c # 替换为实际的 accessKey
+        secret-key: sk370fb48d869b152b # 替换为实际的 secretKey
+    consumer:
+        group: common-group
+        access-key: ak16b45v4p9n150d89395b3c # 替换为实际的 accessKey
+        secret-key: sk370fb48d869b152b # 替换为实际的 secretKey

+ 12 - 12
fs-live-socket/src/main/resources/application.yml

@@ -41,24 +41,24 @@ server:
     # tomcat的URI编码
     uri-encoding: UTF-8
     # tomcat最大线程数,默认为200
-    max-threads: 5000
+    max-threads: 2000
     # Tomcat启动初始化的线程数,默认值25
-    min-spare-threads: 100
-    # 服务器在任何给定时间接受和处理的最大连接数。一旦达到限制,操作系统仍然可以接受基于“acceptCount”属性的连接。
-    max-connections: 30000
-    # 当所有可能的请求处理线程都在使用中时,传入连接请求的最大队列长度
+    min-spare-threads: 2000
+    #    # 服务器在任何给定时间接受和处理的最大连接数。一旦达到限制,操作系统仍然可以接受基于“acceptCount”属性的连接。
+    max-connections: 10000
+    #    # 当所有可能的请求处理线程都在使用中时,传入连接请求的最大队列长度
     accept-count: 1000
-    # 连接器在接受连接后等待显示请求 URI 行的时间。
-    connection-timeout: 20000
-    # 在关闭连接之前等待另一个 HTTP 请求的时间。如果未设置,则使用 connectionTimeout。设置为 -1 时不会超时。
-    keep-alive-timeout: 20000
-    # 在连接关闭之前可以进行流水线处理的最大HTTP请求数量。当设置为0或1时,禁用keep-alive和流水线处理。当设置为-1时,允许无限数量的流水线处理或keep-alive请求。
-    max-keep-alive-requests: 100
+#    # 连接器在接受连接后等待显示请求 URI 行的时间。
+#    connection-timeout: 20000
+#    # 在关闭连接之前等待另一个 HTTP 请求的时间。如果未设置,则使用 connectionTimeout。设置为 -1 时不会超时。
+#    keep-alive-timeout: 300000
+#    # 在连接关闭之前可以进行流水线处理的最大HTTP请求数量。当设置为0或1时,禁用keep-alive和流水线处理。当设置为-1时,允许无限数量的流水线处理或keep-alive请求。
+#    max-keep-alive-requests: 10000
 
 # 日志配置
 logging:
   level:
-    com.fs: info
+    com.fs: error
     org.springframework: warn
     org.springframework.web: info
 

Некоторые файлы не были показаны из-за большого количества измененных файлов