Explorar o código

直播代码优化

yuhongqi hai 1 mes
pai
achega
6ce9b4e693
Modificáronse 100 ficheiros con 5195 adicións e 455 borrados
  1. 13 8
      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. 1 0
      fs-admin/src/main/java/com/fs/store/controller/FsUserAddressController.java
  6. 1 1
      fs-admin/src/main/java/com/fs/task/LiveTask.java
  7. 11 0
      fs-admin/src/main/resources/application-dev.yml
  8. 1 1
      fs-api/src/main/java/com/fs/core/datasource/DynamicDataSourceContextHolder.java
  9. 42 0
      fs-common/src/main/java/com/fs/common/annotation/RateLimiter.java
  10. 5 0
      fs-common/src/main/java/com/fs/common/constant/Constants.java
  11. 236 0
      fs-common/src/main/java/com/fs/common/core/redis/RedisCacheT.java
  12. 1 1
      fs-common/src/main/java/com/fs/common/core/redis/service/StockDeductService.java
  13. 20 0
      fs-common/src/main/java/com/fs/common/enums/LimitType.java
  14. 73 0
      fs-common/src/main/java/com/fs/common/exception/ServiceException.java
  15. 1 0
      fs-common/src/main/java/com/fs/common/vo/LiveVo.java
  16. 1 1
      fs-company-app/src/main/java/com/fs/core/datasource/DynamicDataSourceContextHolder.java
  17. 14 3
      fs-company/src/main/java/com/fs/company/controller/live/LiveController.java
  18. 1 1
      fs-company/src/main/java/com/fs/core/datasource/DynamicDataSourceContextHolder.java
  19. 10 0
      fs-company/src/main/resources/application-dev.yml
  20. 184 0
      fs-live-mq/pom.xml
  21. 30 0
      fs-live-mq/src/main/java/com/fs/LiveSocketMqApplication.java
  22. 90 0
      fs-live-mq/src/main/java/com/fs/core/aspectj/LiveWatchUserAspect.java
  23. 58 0
      fs-live-mq/src/main/java/com/fs/core/aspectj/lock/DistributeLock.java
  24. 113 0
      fs-live-mq/src/main/java/com/fs/core/aspectj/lock/DistributeLockAspect.java
  25. 13 0
      fs-live-mq/src/main/java/com/fs/core/aspectj/lock/DistributeLockConstant.java
  26. 24 0
      fs-live-mq/src/main/java/com/fs/core/aspectj/lock/DistributeLockException.java
  27. 31 0
      fs-live-mq/src/main/java/com/fs/core/config/ApplicationConfig.java
  28. 123 0
      fs-live-mq/src/main/java/com/fs/core/config/DruidConfig.java
  29. 72 0
      fs-live-mq/src/main/java/com/fs/core/config/FastJson2JsonRedisSerializer.java
  30. 61 0
      fs-live-mq/src/main/java/com/fs/core/config/FilterConfig.java
  31. 109 0
      fs-live-mq/src/main/java/com/fs/core/config/MyBatisConfig.java
  32. 91 0
      fs-live-mq/src/main/java/com/fs/core/config/RedisConfig.java
  33. 66 0
      fs-live-mq/src/main/java/com/fs/core/config/ResourcesConfig.java
  34. 41 0
      fs-live-mq/src/main/java/com/fs/core/config/SecurityConfig.java
  35. 33 0
      fs-live-mq/src/main/java/com/fs/core/config/ServerConfig.java
  36. 124 0
      fs-live-mq/src/main/java/com/fs/core/config/SwaggerConfig.java
  37. 63 0
      fs-live-mq/src/main/java/com/fs/core/config/ThreadPoolConfig.java
  38. 77 0
      fs-live-mq/src/main/java/com/fs/core/config/properties/DruidProperties.java
  39. 27 0
      fs-live-mq/src/main/java/com/fs/core/datasource/DynamicDataSource.java
  40. 45 0
      fs-live-mq/src/main/java/com/fs/core/datasource/DynamicDataSourceContextHolder.java
  41. 56 0
      fs-live-mq/src/main/java/com/fs/core/interceptor/RepeatSubmitInterceptor.java
  42. 126 0
      fs-live-mq/src/main/java/com/fs/core/interceptor/impl/SameUrlDataInterceptor.java
  43. 53 0
      fs-live-mq/src/main/java/com/fs/core/security/SecurityUtils.java
  44. 45 0
      fs-live-mq/src/main/java/com/fs/mq/RocketMQConsumerService.java
  45. 1 0
      fs-live-mq/src/main/resources/META-INF/spring-devtools.properties
  46. 90 0
      fs-live-mq/src/main/resources/application-dev.yml
  47. 79 0
      fs-live-mq/src/main/resources/application-druid-test.yml
  48. 78 0
      fs-live-mq/src/main/resources/application-druid.yml
  49. 122 0
      fs-live-mq/src/main/resources/application.yml
  50. 2 0
      fs-live-mq/src/main/resources/banner.txt
  51. BIN=BIN
      fs-live-mq/src/main/resources/fx.jpg
  52. 36 0
      fs-live-mq/src/main/resources/i18n/messages.properties
  53. 15 0
      fs-live-mq/src/main/resources/mybatis/mybatis-config.xml
  54. BIN=BIN
      fs-live-mq/src/main/resources/qr.jpg
  55. BIN=BIN
      fs-live-mq/src/main/resources/simsunb.ttf
  56. 1 0
      fs-live-mq/src/main/resources/static/S8Zw463cFc.txt
  57. 21 0
      fs-live-mq/src/main/resources/templates/privacyPolicy.html
  58. 21 0
      fs-live-mq/src/main/resources/templates/userAgreement.html
  59. 61 0
      fs-live-mq/src/test/java/com/fs/core/security/BaseSpringBootTest.java
  60. 74 0
      fs-live-socket/src/main/java/com/fs/core/aspectj/DataSourceAspect.java
  61. 90 90
      fs-live-socket/src/main/java/com/fs/core/aspectj/LiveWatchUserAspect.java
  62. 15 0
      fs-live-socket/src/main/java/com/fs/core/config/RedisConfig.java
  63. 1 1
      fs-live-socket/src/main/java/com/fs/core/datasource/DynamicDataSourceContextHolder.java
  64. 269 45
      fs-live-socket/src/main/java/com/fs/live/task/Task.java
  65. 21 17
      fs-live-socket/src/main/java/com/fs/live/websocket/auth/WebSocketConfigurator.java
  66. 2 2
      fs-live-socket/src/main/java/com/fs/live/websocket/handle/LiveChatHandler.java
  67. 936 121
      fs-live-socket/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  68. 13 2
      fs-live-socket/src/main/resources/application-dev.yml
  69. 7 7
      fs-live-socket/src/main/resources/application.yml
  70. 80 0
      fs-live-socket/src/test/java/com/fs/core/security/test.java
  71. 1 1
      fs-live-streamer/src/main/java/com/fs/framework/datasource/DynamicDataSourceContextHolder.java
  72. 6 6
      fs-live-streamer/src/main/resources/application-dev.yml
  73. 6 0
      fs-service-system/pom.xml
  74. 18 0
      fs-service-system/src/main/java/com/fs/common/param/LoginParam.java
  75. 45 18
      fs-service-system/src/main/java/com/fs/company/mapper/CompanyMoneyLogsMapper.java
  76. 6 57
      fs-service-system/src/main/java/com/fs/company/service/impl/CompanyMoneyLogsServiceImpl.java
  77. 4 2
      fs-service-system/src/main/java/com/fs/live/domain/LiveLotteryRegistration.java
  78. 1 0
      fs-service-system/src/main/java/com/fs/live/domain/LiveOrder.java
  79. 2 0
      fs-service-system/src/main/java/com/fs/live/mapper/LiveGoodsMapper.java
  80. 11 0
      fs-service-system/src/main/java/com/fs/live/mapper/LiveMsgMapper.java
  81. 2 0
      fs-service-system/src/main/java/com/fs/live/mapper/LiveOrderItemMapper.java
  82. 2 0
      fs-service-system/src/main/java/com/fs/live/mapper/LiveOrderMapper.java
  83. 4 1
      fs-service-system/src/main/java/com/fs/live/mapper/LiveUserFirstEntryMapper.java
  84. 29 1
      fs-service-system/src/main/java/com/fs/live/mapper/LiveWatchUserMapper.java
  85. 8 0
      fs-service-system/src/main/java/com/fs/live/service/ILiveMsgService.java
  86. 2 0
      fs-service-system/src/main/java/com/fs/live/service/ILiveOrderService.java
  87. 11 0
      fs-service-system/src/main/java/com/fs/live/service/ILiveWatchUserService.java
  88. 3 3
      fs-service-system/src/main/java/com/fs/live/service/impl/LiveAfterSalesServiceImpl.java
  89. 3 2
      fs-service-system/src/main/java/com/fs/live/service/impl/LiveAutoTaskServiceImpl.java
  90. 2 2
      fs-service-system/src/main/java/com/fs/live/service/impl/LiveDataServiceImpl.java
  91. 3 0
      fs-service-system/src/main/java/com/fs/live/service/impl/LiveGoodsServiceImpl.java
  92. 2 1
      fs-service-system/src/main/java/com/fs/live/service/impl/LiveLotteryConfServiceImpl.java
  93. 23 0
      fs-service-system/src/main/java/com/fs/live/service/impl/LiveMsgServiceImpl.java
  94. 447 34
      fs-service-system/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java
  95. 127 9
      fs-service-system/src/main/java/com/fs/live/service/impl/LiveRedConfServiceImpl.java
  96. 31 4
      fs-service-system/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
  97. 121 9
      fs-service-system/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java
  98. 21 1
      fs-service-system/src/main/java/com/fs/live/utils/redis/RedisBatchHandler.java
  99. 2 0
      fs-service-system/src/main/java/com/fs/live/vo/LiveAfterSalesVo.java
  100. 17 0
      fs-service-system/src/main/java/com/fs/live/vo/LiveGoodsUploadMqVo.java

+ 13 - 8
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"
@@ -97,7 +99,7 @@ deploy_jar() {
     echo "启动服务..."
     ssh -f "$REMOTE_USER@$remote_host" \
     "cd $REMOTE_BASE_DIR/$remote_dir && \
-     nohup java -jar -Dfile.encoding=UTF-8 $app_name.jar \
+     nohup java -jar  -Xms28g -Xmx28g -XX:+UseG1GC  -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=1024m -XX:MaxGCPauseMillis=200 -XX:+DisableExplicitGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump/    -Dfile.encoding=UTF-8 $app_name.jar \
      > $app_name.log 2>&1 &"
 
     # 检查进程是否启动成功
@@ -110,7 +112,7 @@ deploy_jar() {
 
     echo ""
 }
-
+#     nohup java -jar  -Xms28g -Xmx28g -XX:+UseG1GC  -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=1024m -XX:MaxGCPauseMillis=200 -XX:+DisableExplicitGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump/    -Dfile.encoding=UTF-8 $app_name.jar \
 # 主要部署流程
 echo "开始多服务器分布式部署..."
 echo "部署配置:"
@@ -120,22 +122,25 @@ done
 echo ""
 
 # 部署 fs-admin
-deploy_jar "$LOCAL_FS_ADMIN_JAR" "fs-admin" "fs-admin"
+#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"
 
 # 部署 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);
     }
 

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

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

@@ -195,7 +195,7 @@ public class LiveTask {
 
     }
     /**
-     * 更新发货状态
+     * 更新流量
      */
     @Scheduled(fixedRate = CONSUME_INTERVAL)
     public void insertLiveTrralog() {

+ 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-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:";
+
     /**
      * 令牌前缀 企业端
      */

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

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

@@ -55,7 +55,7 @@ public class StockDeductService {
      */
     public void initStock(Long productId, Long liveId, Integer initStock) {
         String stockKey = RedisConstant.STOCK_KEY_PREFIX + liveId + ":" + productId;
-        redisTemplate.opsForValue().set(stockKey, initStock, 24 * 60 * 60, TimeUnit.SECONDS);
+        redisTemplate.opsForValue().set(stockKey, initStock);
         log.info("商品" + productId + "库存初始化完成,初始库存:" + initStock);
     }
 

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

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

@@ -20,6 +20,7 @@ public class LiveVo {
     private Integer status;
 
     private Long anchorId;
+    private Long companyId;
 
     private Long videoFileSize;
     private Long videoDuration;

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

+ 14 - 3
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;
 
     /**
      * 查询直播列表
@@ -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();

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

+ 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=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=BIN
fs-live-mq/src/main/resources/qr.jpg


BIN=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 - 90
fs-live-socket/src/main/java/com/fs/core/aspectj/LiveWatchUserAspect.java

@@ -1,90 +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;
-    }
-
-
-}
+//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);
     }
 

+ 269 - 45
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;
@@ -184,6 +188,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 +210,137 @@ public class Task {
                         redisCache.redisTemplate.opsForZSet().remove(key + live.getLiveId(), JSON.toJSONString(liveAutoTask),liveAutoTask.getAbsValue().getTime());
                     });
                 }
+                String cacheKey = String.format(LiveKeysConstant.LIVE_DATA_CACHE, live.getLiveId());
+                redisCache.deleteObject(cacheKey);
                 webSocketServer.removeLikeCountCache(live.getLiveId());
             }
             // 重新更新所有在直播的缓存
             liveService.asyncToCache();
         }
     }
-//    @Scheduled(cron = "0/1 * * * * ?")
-//    @DistributeLock(key = "liveLotteryTask", scene = "task")
+    /**
+     * 使用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 +361,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 +537,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 +552,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());
             // 删除缓存 同步抽奖记录

+ 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 - 2
fs-live-socket/src/main/java/com/fs/live/websocket/handle/LiveChatHandler.java

@@ -70,7 +70,7 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
             if (userType == 0) {
 
 
-                FsUser fsUser = fsUserService.selectFsUserByUserId(userId);
+                FsUser fsUser = fsUserService.selectWsFsUserById(userId);
                 if (Objects.isNull(fsUser)) {
                     ctx.channel().writeAndFlush(new TextWebSocketFrame("Error: 用户信息错误")).addListener(ChannelFutureListener.CLOSE);
                     return;
@@ -213,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);
 

+ 936 - 121
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,12 +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.*;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
+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.locks.StampedLock;
 
 import static com.fs.common.constant.LiveKeysConstant.*;
 import static com.fs.common.constant.LiveKeysConstant.TOP_MSG;
@@ -47,20 +51,90 @@ public class WebSocketServer {
     // 管理端连接
     private final static ConcurrentHashMap<Long, CopyOnWriteArrayList<Session>> adminRooms = new ConcurrentHashMap<>();
 
-    // Session发送锁,避免同一会话并发发送消息
-    private final static ConcurrentHashMap<String, Lock> sessionLocks = 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;
+    // 聊天消息批量插入定时器间隔:10秒
+    private final static long LIVE_MSG_BATCH_INTERVAL = 10000; // 10秒
+    // Redis key:被禁言用户Set(按直播间)
+    private final static String BLOCKED_USERS_KEY = "live:blocked:users:%s";
 
     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);
@@ -71,6 +145,9 @@ public class WebSocketServer {
     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<>();
 
@@ -103,48 +180,28 @@ 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(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());
-            if (1 == random.nextInt(10)) {
+            // 上线消息采样10%进入队列
+            if (random.nextDouble() < ENTRY_EXIT_SAMPLE_RATE) {
                 SendMsgVo sendMsgVo = new SendMsgVo();
                 sendMsgVo.setLiveId(liveId);
                 sendMsgVo.setUserId(userId);
@@ -154,8 +211,8 @@ public class WebSocketServer {
                 sendMsgVo.setData(JSONObject.toJSONString(liveWatchUserVO));
                 sendMsgVo.setNickName(fsUser.getNickname());
                 sendMsgVo.setAvatar(fsUser.getAvatar());
-                // 广播连接消息
-                broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+                // 将上线消息加入队列
+                enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)), false);
             }
 
             LiveUserFirstEntry liveUserFirstEntry = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveId, userId);
@@ -193,10 +250,19 @@ public class WebSocketServer {
             adminExecutors.computeIfAbsent(liveId, k -> Executors.newSingleThreadExecutor());
         }
 
-        // 初始化Session锁
-        sessionLocks.putIfAbsent(session.getId(), new ReentrantLock());
+        // 初始化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);
+        }
     }
 
     //关闭连接时调用
@@ -211,11 +277,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);
 
@@ -224,13 +286,13 @@ 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));
-            // 广播离开消息 添加一个概率问题 摇塞子,1-4 当为1的时候广播消息
-            if (1 == new Random().nextInt(10)) {
+            // 使用 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);
@@ -240,7 +302,8 @@ public class WebSocketServer {
                 sendMsgVo.setData(JSONObject.toJSONString(liveWatchUserVO));
                 sendMsgVo.setNickName(fsUser.getNickname());
                 sendMsgVo.setAvatar(fsUser.getAvatar());
-                broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+                // 将下线消息加入队列
+                enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)), false);
             }
         } else {
             adminRoom.remove(session);
@@ -254,9 +317,28 @@ public class WebSocketServer {
             }
         }
         // 清理Session相关资源
-        heartbeatCache.remove(session.getId());
-        sessionLocks.remove(session.getId());
+        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);
 
+        // 检查并清理空的直播间资源
+        cleanupEmptyRoom(liveId);
     }
 
     //收到客户端信息
@@ -266,6 +348,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;
@@ -273,9 +356,22 @@ public class WebSocketServer {
         try {
             switch (msg.getCmd()) {
                 case "heartbeat":
-                    // 更新心跳时间
-                    heartbeatCache.put(session.getId(), System.currentTimeMillis());
-                    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());
@@ -289,8 +385,10 @@ public class WebSocketServer {
                     liveMsg.setCreateTime(new Date());
 
                     if (userType == 0) {
-                        List<LiveWatchUser> liveWatchUser = liveWatchUserService.getByLiveIdAndUserId(msg.getLiveId(), msg.getUserId());
-                        if(!liveWatchUser.isEmpty() && liveWatchUser.get(0).getMsgStatus() == 1){
+                        // 使用Redis Set检查用户是否被禁言
+                        String blockedUsersKey = String.format(BLOCKED_USERS_KEY, msg.getLiveId());
+                        boolean isBlocked = redisCache.redisTemplate.opsForSet().isMember(blockedUsersKey, String.valueOf(msg.getUserId()));
+                        if (isBlocked) {
                             sendMessage(session, JSONObject.toJSONString(R.error("你已被禁言")));
                             return;
                         }
@@ -299,14 +397,25 @@ public class WebSocketServer {
                         Integer replayFlag = flagMap.get("replayFlag");
                         liveMsg.setLiveFlag(liveFlag);
                         liveMsg.setReplayFlag(replayFlag);
-                        liveMsgService.insertLiveMsg(liveMsg);
+                        // 将消息加入批量插入队列
+                        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();
@@ -317,7 +426,7 @@ public class WebSocketServer {
                     liveMsg.setMsg(msg.getMsg());
                     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);
                     break;
                 case "sendTopMsg":
                     msg.setMsg(productionWordFilter.filter(msg.getMsg()).getFilteredText());
@@ -332,7 +441,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));
@@ -373,7 +482,7 @@ public class WebSocketServer {
         sendMsgVo.setUserType(0L);
         sendMsgVo.setCmd("deleteMsg");
         sendMsgVo.setMsg(msg.getMsg());
-        broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+        enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)), true);
     }
 
     private void processCoupon(long liveId, SendMsgVo msg) {
@@ -393,7 +502,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);
     }
 
 
@@ -408,7 +517,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);
     }
 
     /**
@@ -423,7 +532,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);
         }
     }
 
@@ -439,7 +548,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);
         }
     }
 
@@ -471,28 +580,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 {
         if (session == null || !session.isOpen()) {
             return;
         }
 
-        // 获取Session锁
-        Lock lock = sessionLocks.get(session.getId());
+        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(session.getId(), k -> new ReentrantLock());
+            lock = sessionLocks.computeIfAbsent(sessionId, k -> new StampedLock());
         }
 
-        // 使用锁保证同一Session的消息串行发送
-        lock.lock();
+        // 使用写锁保证同一Session的消息串行发送
+        // 注意:只对写入操作加锁,读取操作(如session.isOpen())在锁外进行
+        long stamp = lock.writeLock();
         try {
-            if (session.isOpen()) {
-                session.getAsyncRemote().sendText(message);
+            // 双重检查,确保连接仍然打开(在锁内再次检查,避免锁期间连接关闭)
+            if (!session.isOpen()) {
+                return;
+            }
+            
+            try {
+                // 使用同步发送,确保前一次写入完成后再发起新写入
+                session.getBasicRemote().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 {
-            lock.unlock();
+            // 确保锁在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) {
@@ -505,11 +719,15 @@ 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);
     }
 
     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);
@@ -519,12 +737,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  消息内容
      */
@@ -532,72 +750,138 @@ public class WebSocketServer {
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
         List<Session> adminRoom = getAdminRoom(liveId);
 
-        // 普通用户房间:并行发送
+        // 使用Set去重,避免同一Session被多次处理
+        Set<Session> uniqueSessions = new HashSet<>();
+        
+        // 收集普通用户房间的Session(去重)
         room.forEach((k, v) -> {
-            if (v.isOpen()) {
-                sendWithRetry(v,message,1);
+            if (v != null && v.isOpen()) {
+                uniqueSessions.add(v);
+            }
+        });
+        
+        // 收集admin房间的Session(去重)
+        adminRoom.forEach(v -> {
+            if (v != null && v.isOpen()) {
+                uniqueSessions.add(v);
             }
         });
 
-        // admin房间:串行发送,使用单线程执行器
-        if (!adminRoom.isEmpty()) {
-            ExecutorService executor = adminExecutors.get(liveId);
-            if (executor != null && !executor.isShutdown()) {
-                executor.submit(() -> {
-                    for (Session session : adminRoom) {
-                        if (session.isOpen()) {
-                            sendWithRetry(session, message, 1);
-                        }
-                    }
-                });
-            } else {
-                // 如果执行器不存在或已关闭,直接发送
-                adminRoom.forEach(v -> {
-                    if (v.isOpen()) {
-                        sendWithRetry(v, message, 1);
-                    }
-                });
+        // 并行发送给所有唯一Session
+        uniqueSessions.parallelStream().forEach(session -> {
+            try {
+                sendWithRetry(session, message, 1);
+            } 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,1);
+            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) {
@@ -690,7 +974,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());
         }
@@ -741,6 +1025,7 @@ public class WebSocketServer {
                 String valueStr = cacheObject.toString().trim();
                 current = Integer.parseInt(valueStr);
             } catch (NumberFormatException e) {
+                log.error(e.getMessage());
                 continue;
             }
             Integer last = lastLikeCountCache.getOrDefault(liveId, 0);
@@ -756,6 +1041,71 @@ public class WebSocketServer {
         lastLikeCountCache.keySet().removeIf(liveId -> !activeLiveIds.contains(liveId));
     }
 
+    @Scheduled(fixedRate = LIVE_MSG_BATCH_INTERVAL) // 每10秒执行一次
+    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());
+                    });
+        }
+    }
+
+    /**
+     * 批量插入聊天消息队列
+     */
+    private void flushLiveMsgQueue() {
+        if (liveMsgQueue.isEmpty()) {
+            return;
+        }
+        
+        List<LiveMsg> batchList = new ArrayList<>();
+        // 从队列中取出所有消息(最多500条)
+        int count = liveMsgQueue.drainTo(batchList, LIVE_MSG_BATCH_SIZE);
+        
+        if (count > 0) {
+            try {
+                int inserted = liveMsgService.insertLiveMsgBatch(batchList);
+                log.debug("[聊天消息批量插入] 成功插入 {} 条消息", inserted);
+            } catch (Exception e) {
+                log.error("[聊天消息批量插入] 插入失败, 条数: {}, error: {}", count, e.getMessage(), e);
+                // 插入失败,将消息重新放回队列(避免消息丢失)
+                batchList.forEach(msg -> {
+                    try {
+                        liveMsgQueue.offer(msg);
+                    } catch (Exception ex) {
+                        log.error("[聊天消息队列] 重新入队失败, liveId: {}, userId: {}", msg.getLiveId(), msg.getUserId());
+                    }
+                });
+            }
+        }
+    }
+
     /**
      * 定时清理无效会话(每分钟执行一次)
      * 检查心跳超时的会话并关闭
@@ -824,4 +1174,469 @@ public class WebSocketServer {
             }
         }
     }
+    /**
+     * 消息队列包装类,支持优先级(管理员消息优先级更高)
+     */
+    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

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

+ 80 - 0
fs-live-socket/src/test/java/com/fs/core/security/test.java

@@ -0,0 +1,80 @@
+package com.fs.core.security;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.live.domain.LiveLotteryRegistration;
+
+import java.util.Date;
+
+public class test {
+
+    public static void main(String[] args) {
+        String jsonStr = "{\"@type\":\"com.alibaba.fastjson.JSONObject\",\"rizeLevel\":-1L,\"updateTime\":new Date(1770029021011),\"params\":{\"@type\":\"com.alibaba.fastjson.JSONObject\"},\"liveId\":1147L,\"userId\":3L,\"isWin\":0L,\"lotteryId\":2219L,\"createTime\":new Date(1770029021011)}";
+
+        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());
+                }
+            }
+        }
+        
+        // 保证 createTime 不为空
+        if (registration.getCreateTime() == null) {
+            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());
+                }
+            }
+        }
+        
+        // 保证 updateTime 不为空
+        if (registration.getUpdateTime() == null) {
+            registration.setUpdateTime(new Date());
+        }
+        
+        System.out.println("registration: " + registration);
+        System.out.println("createTime: " + registration.getCreateTime());
+        System.out.println("updateTime: " + registration.getUpdateTime());
+    }
+}

+ 1 - 1
fs-live-streamer/src/main/java/com/fs/framework/datasource/DynamicDataSourceContextHolder.java

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

+ 6 - 6
fs-live-streamer/src/main/resources/application-dev.yml

@@ -82,13 +82,13 @@ spring:
 
 
 rocketmq:
-  name-server: rmq-1243b25nj.rocketmq.gz.public.tencenttdmq.com:8080 # RocketMQ NameServer 地址
+  name-server: rmq-16b45v4p9n.rocketmq.cd.qcloud.tencenttdmq.com:8080 # RocketMQ NameServer 地址
   producer:
     group: my-producer-group
-    access-key: ak1243b25nj17d4b2dc1a03 # 替换为实际的 accessKey
-    secret-key: sk08a7ea1f9f4b0237 # 替换为实际的 secretKey
+    access-key: ak16b45v4p9n150d89395b3c # 替换为实际的 accessKey
+    secret-key: sk370fb48d869b152b # 替换为实际的 secretKey
   consumer:
-    group: test-group
-    access-key: ak1243b25nj17d4b2dc1a03 # 替换为实际的 accessKey
-    secret-key: sk08a7ea1f9f4b0237 # 替换为实际的 secretKey
+    group: common-group
+    access-key: ak16b45v4p9n150d89395b3c # 替换为实际的 accessKey
+    secret-key: sk370fb48d869b152b # 替换为实际的 secretKey
 

+ 6 - 0
fs-service-system/pom.xml

@@ -192,6 +192,12 @@
             <version>3.1.1</version>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.rocketmq</groupId>
+            <artifactId>rocketmq-spring-boot-starter</artifactId>
+            <version>2.2.3</version>
+        </dependency>
+
         <dependency>
             <groupId>org.springframework.retry</groupId>
             <artifactId>spring-retry</artifactId>

+ 18 - 0
fs-service-system/src/main/java/com/fs/common/param/LoginParam.java

@@ -0,0 +1,18 @@
+package com.fs.common.param;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import java.io.Serializable;
+
+@Data
+public class LoginParam implements Serializable {
+    @NotBlank(message = "code参数缺失")
+    private String code;
+    private String encryptedData;
+    private String iv;
+    private String rawData;
+    private String signature;
+    private String appId;
+    private Integer authType;
+}

+ 45 - 18
fs-service-system/src/main/java/com/fs/company/mapper/CompanyMoneyLogsMapper.java

@@ -92,10 +92,18 @@ public interface CompanyMoneyLogsMapper
 
 
     @Select({"<script> " +
-            "select l.*,c.company_name ,o.order_code,o.user_phone,p.bank_transaction_id,p.pay_type_code,p.pay_code " +
-            "from company_money_logs l left join company c on c.company_id=l.company_id   " +
-            "left join fs_store_order o on o.id=l.business_id " +
-            "left join fs_store_payment p on o.id=p.order_id "+
+            "select l.*,c.company_name " +
+            ",COALESCE(so.order_code, lo.order_code) as order_code " +
+            ",COALESCE(so.user_phone, lo.user_phone) as user_phone " +
+            ",COALESCE(sp.bank_transaction_id, lp.bank_transaction_id) as bank_transaction_id " +
+            ",COALESCE(sp.pay_type_code, lp.pay_type_code) as pay_type_code " +
+            ",COALESCE(sp.pay_code, lp.pay_code) as pay_code " +
+            "from company_money_logs l " +
+            "left join company c on c.company_id=l.company_id   " +
+            "left join fs_store_order so on (so.id=l.business_id and l.type=0) " +
+            "left join live_order lo on (lo.order_id=l.business_id and l.type=1) " +
+            "left join fs_store_payment sp on (so.id=sp.order_id and l.type=0) " +
+            "left join live_order_payment lp on (lo.order_id=lp.business_id and lp.status=1 and l.type=1) " +
             "where  (l.logs_type=4 || l.logs_type=5)  " +
             "<if test = 'maps.companyId != null  '> " +
             "and l.company_id = #{maps.companyId}" +
@@ -106,17 +114,20 @@ public interface CompanyMoneyLogsMapper
             "<if test = 'maps.businessId != null  '> " +
             "and l.business_id = #{maps.businessId}" +
             "</if>" +
+            "<if test = 'maps.type != null  '> " +
+            "and l.type = #{maps.type}" +
+            "</if>" +
             "<if test = 'maps.orderCode != null and maps.orderCode != \"\"  '> " +
-            "and o.order_code = #{maps.orderCode}" +
+            "and (so.order_code = #{maps.orderCode} or lo.order_code = #{maps.orderCode})" +
             "</if>" +
             "<if test = 'maps.tradeCode != null and maps.tradeCode != \"\"  '> " +
-            "and p.bank_transaction_id = #{maps.tradeCode}" +
+            "and (sp.bank_transaction_id = #{maps.tradeCode} or lp.bank_transaction_id = #{maps.tradeCode})" +
             "</if>" +
             "<if test = 'maps.payCode != null and maps.payCode != \"\"  '> " +
-            "and p.pay_code = #{maps.payCode}" +
+            "and (sp.pay_code = #{maps.payCode} or lp.pay_code = #{maps.payCode})" +
             "</if>" +
             "<if test = 'maps.userPhone != null and maps.userPhone != \"\"  '> " +
-            "and o.user_phone = #{maps.userPhone}" +
+            "and (so.user_phone = #{maps.userPhone} or lo.user_phone = #{maps.userPhone})" +
             "</if>" +
             "<if test = 'maps.beginTime != null and maps.beginTime != \"\" '> " +
             "and date_format(l.create_time,'%y%m%d') &gt;= date_format(#{maps.beginTime},'%y%m%d') " +
@@ -129,9 +140,12 @@ public interface CompanyMoneyLogsMapper
     List<CompanyMoneyLogsVO> selectCompanyMoneyLogsMallVOList(@Param("maps") CompanyMoneyLogsParam companyMoneyLogs);
 
     @Select({"<script> " +
-            "select l.*,c.company_name ,p.bank_transaction_id,p.pay_type_code " +
+            "select l.*,c.company_name " +
+            ",COALESCE(sp.bank_transaction_id, lp.bank_transaction_id) as bank_transaction_id " +
+            ",COALESCE(sp.pay_type_code, lp.pay_type_code) as pay_type_code " +
             "from company_money_logs l left join company c on c.company_id=l.company_id   " +
-            "left join fs_store_payment p on p.payment_id=l.business_id "+
+            "left join fs_store_payment sp on (sp.payment_id=l.business_id and l.type=0) " +
+            "left join live_order_payment lp on (lp.payment_id=l.business_id and lp.status=1 and l.type=1) " +
             "where  (l.logs_type=8 || l.logs_type=9)  " +
             "<if test = 'maps.companyId != null  '> " +
             "and l.company_id = #{maps.companyId}" +
@@ -142,14 +156,15 @@ public interface CompanyMoneyLogsMapper
             "<if test = 'maps.businessId != null  '> " +
             "and l.business_id = #{maps.businessId}" +
             "</if>" +
-
+            "<if test = 'maps.type != null  '> " +
+            "and l.type = #{maps.type}" +
+            "</if>" +
             "<if test = 'maps.payCode != null and maps.payCode != \"\"  '> " +
-            "and p.pay_code = #{maps.payCode}" +
+            "and (sp.pay_code = #{maps.payCode} or lp.pay_code = #{maps.payCode})" +
             "</if>" +
             "<if test = 'maps.tradeCode != null and maps.tradeCode != \"\"  '> " +
-            "and p.bank_transaction_id = #{maps.tradeCode}" +
+            "and (sp.bank_transaction_id = #{maps.tradeCode} or lp.bank_transaction_id = #{maps.tradeCode})" +
             "</if>" +
-
             "<if test = 'maps.beginTime != null and maps.beginTime != \"\" '> " +
             "and date_format(l.create_time,'%y%m%d') &gt;= date_format(#{maps.beginTime},'%y%m%d') " +
             "</if>" +
@@ -193,10 +208,22 @@ public interface CompanyMoneyLogsMapper
     List<CompanyMoneyLogsExport1VO> selectCompanyMoneyLogsExport1VOList(@Param("maps")CompanyMoneyLogs companyMoneyLogs);
 
     @Select({"<script> " +
-            "select l.*,c.company_name,p.pay_code,p.pay_type_code,p.bank_transaction_id  " +
-            ",(select group_concat(dd.dept_name separator  ',') from company_dept dd where p.dept_id=dd.dept_id or find_in_set(dd.dept_id,d.ancestors)) as dept_name"+
-            " from company_money_logs l left join company c on c.company_id=l.company_id left join fs_store_payment p on p.payment_id=l.business_id " +
-            " left join company_dept d on d.dept_id=p.dept_id "+
+            "select l.*,c.company_name " +
+            ",COALESCE(so.order_code, lo.order_code) as orderCode " +
+            ",COALESCE(sp.pay_code, lp.pay_code) as pay_code " +
+            ",COALESCE(sp.pay_type_code, lp.pay_type_code) as pay_type_code " +
+            ",COALESCE(sp.bank_transaction_id, lp.bank_transaction_id) as bank_transaction_id " +
+            ",(select group_concat(dd.dept_name separator  ',') from company_dept dd " +
+            "  where (sp.dept_id=dd.dept_id or find_in_set(dd.dept_id,d.ancestors)) and l.type=0 " +
+            "  or (lo.dept_id=dd.dept_id or find_in_set(dd.dept_id,ld.ancestors)) and l.type=1) as dept_name " +
+            "from company_money_logs l " +
+            "left join company c on c.company_id=l.company_id " +
+            "left join fs_store_payment sp on (sp.payment_id=l.business_id and l.type=0) " +
+            "left join fs_store_order so on (so.id=sp.order_id and l.type=0) " +
+            "left join live_order_payment lp on (lp.payment_id=l.business_id and lp.status=1 and l.type=1) " +
+            "left join live_order lo on (lo.order_id=lp.business_id and l.type=1) " +
+            "left join company_dept d on (d.dept_id=sp.dept_id and l.type=0) " +
+            "left join company_dept ld on (ld.dept_id=lo.dept_id and l.type=1) " +
             "where (l.logs_type=8||l.logs_type=9) " +
             "<if test = 'maps.companyId != null  '> " +
             "and l.company_id = #{maps.companyId}" +

+ 6 - 57
fs-service-system/src/main/java/com/fs/company/service/impl/CompanyMoneyLogsServiceImpl.java

@@ -2,17 +2,11 @@ package com.fs.company.service.impl;
 
 import java.math.BigDecimal;
 import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.stream.Collectors;
 
-import cn.hutool.core.util.ObjectUtil;
 import com.fs.common.utils.DateUtils;
 import com.fs.company.param.CompanyMoneyLogsParam;
 import com.fs.company.param.CompanyStoreOrderMoneyLogsListParam;
 import com.fs.company.vo.*;
-import com.fs.live.domain.LiveOrder;
-import com.fs.live.mapper.LiveOrderMapper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import com.fs.company.mapper.CompanyMoneyLogsMapper;
@@ -31,9 +25,6 @@ public class CompanyMoneyLogsServiceImpl implements ICompanyMoneyLogsService
     @Autowired
     private CompanyMoneyLogsMapper companyMoneyLogsMapper;
 
-    @Autowired
-    private LiveOrderMapper liveOrderMapper;
-
     /**
      * 查询企业账户记录
      *
@@ -129,62 +120,20 @@ public class CompanyMoneyLogsServiceImpl implements ICompanyMoneyLogsService
 
     @Override
     public List<CompanyMoneyLogsExportVO> selectCompanyMoneyLogsExportVOList(CompanyMoneyLogs companyMoneyLogs) {
-        List<CompanyMoneyLogsExportVO> companyMoneyLogsExportVOS = companyMoneyLogsMapper.selectCompanyMoneyLogsExportVOList(companyMoneyLogs);
-        if (ObjectUtil.isEmpty(companyMoneyLogsExportVOS)){
-            return companyMoneyLogsExportVOS;
-        }
-        List<String> businessIdList =
-                companyMoneyLogsExportVOS.stream()
-                        .filter(vo -> Integer.valueOf(1).equals(vo.getType()))
-                        .map(CompanyMoneyLogsExportVO::getBusinessId)
-                        .filter(Objects::nonNull)
-                        .distinct()
-                        .collect(Collectors.toList());
-        if (ObjectUtil.isEmpty(businessIdList)) {
-            return companyMoneyLogsExportVOS;
-        }
-        Map<String, String> orderCodeMap = liveOrderMapper.selectOrderCodeMapByOrderIds(businessIdList);
-
-        for (CompanyMoneyLogsExportVO item : companyMoneyLogsExportVOS) {
-            // 如果是直播订单
-            if(Integer.valueOf(1).equals(item.getType())) {
-                String orderCode = orderCodeMap.get(item.getBusinessId());
-                if (ObjectUtil.isNotNull(orderCode)) {
-                    item.setOrderCode(orderCode);
-                }
-            }
-        }
-        return companyMoneyLogsExportVOS;
+        // SQL 查询已经根据 type 字段关联了商城订单和直播订单表,直接返回结果
+        return companyMoneyLogsMapper.selectCompanyMoneyLogsExportVOList(companyMoneyLogs);
     }
 
     @Override
     public List<CompanyMoneyLogsExport1VO> selectCompanyMoneyLogsExport1VOList(CompanyMoneyLogs companyMoneyLogs) {
-        List<CompanyMoneyLogsExport1VO> companyMoneyLogsExport1VOS = companyMoneyLogsMapper.selectCompanyMoneyLogsExport1VOList(companyMoneyLogs);
-        for (CompanyMoneyLogsExport1VO item : companyMoneyLogsExport1VOS) {
-            // 如果是直播订单
-            if(ObjectUtil.equal(item.getType(),1)) {
-                String orderCode = liveOrderMapper.selectLiveOrderCodeByOrderId(item.getBusinessId());
-                if(ObjectUtil.isNotNull(orderCode)) {
-                    item.setOrderCode(orderCode);
-                }
-            }
-        }
-        return companyMoneyLogsExport1VOS;
+        // SQL 查询已经根据 type 字段关联了商城订单和直播订单表,直接返回结果
+        return companyMoneyLogsMapper.selectCompanyMoneyLogsExport1VOList(companyMoneyLogs);
     }
 
     @Override
     public List<CompanyMoneyLogsExport2VO> selectCompanyMoneyLogsExport2VOList(CompanyMoneyLogs companyMoneyLogs) {
-        List<CompanyMoneyLogsExport2VO> companyMoneyLogsExport2VOS = companyMoneyLogsMapper.selectCompanyMoneyLogsExport2VOList(companyMoneyLogs);
-        for (CompanyMoneyLogsExport2VO item : companyMoneyLogsExport2VOS) {
-            // 如果是直播订单
-            if(ObjectUtil.equal(item.getType(),1)) {
-                String orderCode = liveOrderMapper.selectLiveOrderCodeByOrderId(item.getBusinessId());
-                if(ObjectUtil.isNotNull(orderCode)) {
-                    item.setOrderCode(orderCode);
-                }
-            }
-        }
-        return companyMoneyLogsExport2VOS;
+        // SQL 查询已经根据 type 字段关联了商城订单和直播订单表,直接返回结果
+        return companyMoneyLogsMapper.selectCompanyMoneyLogsExport2VOList(companyMoneyLogs);
     }
 
     @Override

+ 4 - 2
fs-service-system/src/main/java/com/fs/live/domain/LiveLotteryRegistration.java

@@ -5,6 +5,8 @@ import com.fs.common.core.domain.BaseEntity;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 
+import java.io.Serializable;
+
 /**
  * 直播抽奖登记对象 live_lottery_registration
  *
@@ -12,8 +14,8 @@ import lombok.EqualsAndHashCode;
  * @date 2025-07-17
  */
 @Data
-@EqualsAndHashCode(callSuper = true)
-public class LiveLotteryRegistration extends BaseEntity{
+@EqualsAndHashCode(callSuper = false)
+public class LiveLotteryRegistration extends BaseEntity implements Serializable {
 
     /** 登记ID */
     @Excel(name = "登记ID")

+ 1 - 0
fs-service-system/src/main/java/com/fs/live/domain/LiveOrder.java

@@ -384,6 +384,7 @@ public class LiveOrder extends BaseEntity {
     private Long customerId;
     private Long couponUserId;
     private Long recordId;
+    private Long attrValueId;
 
 
 }

+ 2 - 0
fs-service-system/src/main/java/com/fs/live/mapper/LiveGoodsMapper.java

@@ -173,4 +173,6 @@ public interface LiveGoodsMapper {
      * @return 结果
      */
     int updateBatchById(@Param("list") List<LiveGoods> liveGoodsList);
+
+    int updateStock(@Param("goodsId") Long goodsId, @Param("goodsNum") Integer goodsNum);
 }

+ 11 - 0
fs-service-system/src/main/java/com/fs/live/mapper/LiveMsgMapper.java

@@ -1,6 +1,8 @@
 package com.fs.live.mapper;
 
 
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
 import com.fs.live.domain.LiveMsg;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
@@ -73,5 +75,14 @@ public interface LiveMsgMapper
     public int deleteLiveMsgByMsgIds(Long[] msgIds);
 
     @Select("select * from live_msg where live_id = #{liveId} order by create_time desc limit 30")
+    @DataSource(DataSourceType.SLAVE)
     List<LiveMsg> listRecentMsg(@Param("liveId")Long liveId);
+
+    /**
+     * 批量新增直播讨论
+     *
+     * @param liveMsgList 直播讨论列表
+     * @return 结果
+     */
+    int insertLiveMsgBatch(@Param("list") List<LiveMsg> liveMsgList);
 }

+ 2 - 0
fs-service-system/src/main/java/com/fs/live/mapper/LiveOrderItemMapper.java

@@ -93,4 +93,6 @@ public interface LiveOrderItemMapper {
 
     @Select("select * from live_order_item where order_id= #{orderId}")
     List<LiveOrderItem> selectCheckedByOrderId(@Param("orderId") Long orderId);
+
+    void insertLiveOrderItemTest(LiveOrderItem liveOrderItem);
 }

+ 2 - 0
fs-service-system/src/main/java/com/fs/live/mapper/LiveOrderMapper.java

@@ -130,4 +130,6 @@ public interface LiveOrderMapper {
 
     @Select("SELECT * FROM live_order WHERE live_id= #{liveId}")
     List<LiveOrder> selectOrderByLiveId(@Param("liveId") Long liveId);
+
+    int insertLiveOrderTest(LiveOrder liveOrder);
 }

+ 4 - 1
fs-service-system/src/main/java/com/fs/live/mapper/LiveUserFirstEntryMapper.java

@@ -1,6 +1,8 @@
 package com.fs.live.mapper;
 
 
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
 import com.fs.live.domain.LiveUserFirstEntry;
 import com.fs.live.vo.LiveUserFirstProfit;
 import com.fs.live.vo.LiveUserEntryDetail;
@@ -70,7 +72,8 @@ public interface LiveUserFirstEntryMapper {
 
     List<LiveUserFirstProfit> selectLiveProfitList(LiveUserFirstProfit liveUserFirstProfit);
 
-    @Select("select * from live_user_first_entry where live_id=#{liveId} and user_id=#{userId}")
+    @Select("select * from live_user_first_entry where user_id=#{userId} and live_id=#{liveId} ")
+    @DataSource(DataSourceType.SLAVE)
     LiveUserFirstEntry selectEntityByLiveIdUserId(@Param("liveId") long liveId,@Param("userId") long userId);
 
     /**

+ 29 - 1
fs-service-system/src/main/java/com/fs/live/mapper/LiveWatchUserMapper.java

@@ -62,6 +62,22 @@ public interface LiveWatchUserMapper {
      */
     int updateLiveWatchUser(LiveWatchUser liveWatchUser);
 
+    /**
+     * 更新用户进入直播间时的字段(只更新:update_time, online, location)
+     *
+     * @param liveWatchUser 直播间观看用户
+     * @return 结果
+     */
+    int updateLiveWatchUserOnJoin(LiveWatchUser liveWatchUser);
+
+    /**
+     * 更新用户离开直播间时的字段(只更新:update_time, online, online_seconds)
+     *
+     * @param liveWatchUser 直播间观看用户
+     * @return 结果
+     */
+    int updateLiveWatchUserOnClose(LiveWatchUser liveWatchUser);
+
     /**
      * 删除直播间观看用户
      *
@@ -103,9 +119,21 @@ public interface LiveWatchUserMapper {
     @Select("select a.*,fu.nickname as nick_name from (select lws.* from live_watch_user lws where live_id=#{liveId} and online = 0 and " +
             "user_id in (select user_id from live_lottery_registration where live_id = #{liveId} and lottery_id=#{lotteryId} and registration_id >= " +
             "(SELECT FLOOR(RAND() * (SELECT MAX(registration_id) FROM live_lottery_registration)))) ) a left join fs_user fu on fu.user_id = a.user_id")
-    @DataSource(DataSourceType.SLAVE)
     List<LiveWatchUser> selectLiveWatchAndRegisterUser(@Param("liveId") Long liveId,@Param("lotteryId") Long lotteryId);
 
+    /**
+     * 随机查询指定数量的参与抽奖用户(用于抽奖分配)
+     * @param liveId 直播间ID
+     * @param lotteryId 抽奖ID
+     * @param limit 查询数量限制
+     * @return 随机用户列表
+     */
+    @Select("select a.*,fu.nickname as nick_name from (select lws.* from live_watch_user lws where live_id=#{liveId} and live_flag=#{liveFlag}  and online = 0 and " +
+            "user_id in (select user_id from live_lottery_registration where live_id = #{liveId} and lottery_id=#{lotteryId})) a " +
+            "left join fs_user fu on fu.user_id = a.user_id " +
+            "order by RAND() limit #{limit}")
+    List<LiveWatchUser> selectRandomLiveWatchAndRegisterUser(@Param("liveId") Long liveId, @Param("lotteryId") Long lotteryId, @Param("limit") Integer limit, @Param("liveFlag") Integer liveFlag);
+
     @Select("select * from live_watch_user where live_id = #{liveId} and user_id = #{userId}")
     LiveWatchUser selectUserByLiveIdAndUserId(@Param("liveId") long liveId,@Param("userId")  long userId);
 

+ 8 - 0
fs-service-system/src/main/java/com/fs/live/service/ILiveMsgService.java

@@ -67,4 +67,12 @@ public interface ILiveMsgService
      * @return
      */
     List<LiveMsg> listRecentMsg(Long id);
+
+    /**
+     * 批量新增直播讨论
+     *
+     * @param liveMsgList 直播讨论列表
+     * @return 结果
+     */
+    int insertLiveMsgBatch(List<LiveMsg> liveMsgList);
 }

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

@@ -214,4 +214,6 @@ public interface ILiveOrderService {
     R payConfirmReward(LiveOrder liveOrder);
 
     void initStock();
+
+    R createLiveOrderTest(LiveOrder param);
 }

+ 11 - 0
fs-service-system/src/main/java/com/fs/live/service/ILiveWatchUserService.java

@@ -112,8 +112,19 @@ public interface ILiveWatchUserService {
 
     List<LiveWatchUser> selectLiveWatchAndRegisterUser(Long liveId, Long lotteryId);
 
+    /**
+     * 随机查询指定数量的参与抽奖用户(用于抽奖分配)
+     * @param liveId 直播间ID
+     * @param lotteryId 抽奖ID
+     * @param limit 查询数量限制
+     * @return 随机用户列表
+     */
+    List<LiveWatchUser> selectRandomLiveWatchAndRegisterUser(Long liveId, Long lotteryId, Integer limit);
+
 
     List<LiveWatchUserVO> asyncToCache(Long liveId);
 
     R liveUserTotals(LiveWatchUser liveWatchUser);
+
+    void clearLiveFlagCache(Long liveId);
 }

+ 3 - 3
fs-service-system/src/main/java/com/fs/live/service/impl/LiveAfterSalesServiceImpl.java

@@ -455,7 +455,7 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
         LiveAfterSalesLogs logs = new LiveAfterSalesLogs();
         logs.setChangeTime(new DateTime());
         logs.setChangeType(2);
-        FsUser user = userService.selectFsUserByUserId(storeAfterSales.getUserId());
+        FsUser user = userService.selectWsFsUserById(storeAfterSales.getUserId());
         logs.setOperator(user.getNickname());
         logs.setStoreAfterSalesId(storeAfterSales.getId());
         logs.setChangeMessage(OrderInfoEnum.STATUS_2.getDesc());
@@ -571,7 +571,7 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
         LiveAfterSalesLogs logs = new LiveAfterSalesLogs();
         logs.setChangeTime(new DateTime());
         logs.setChangeType(0);
-        FsUser user = userService.selectFsUserByUserId(Long.valueOf(order.getUserId()));
+        FsUser user = userService.selectWsFsUserById(Long.parseLong(order.getUserId()));
         logs.setOperator(user.getNickname());
         logs.setStoreAfterSalesId(storeAfterSales.getId());
         logs.setChangeMessage(LiveAfterSalesStatusEnum.STATUS_0.getDesc());
@@ -638,7 +638,7 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
         LiveAfterSalesLogs logs = new LiveAfterSalesLogs();
         logs.setChangeTime(new DateTime());
         logs.setChangeType(5);
-        FsUser user = userService.selectFsUserByUserId(Long.valueOf(order.getUserId()));
+        FsUser user = userService.selectWsFsUserById(Long.parseLong(order.getUserId()));
         logs.setOperator(user.getNickname());
         logs.setStoreAfterSalesId(storeAfterSales.getId());
         logs.setChangeMessage(OrderInfoEnum.REFUND_STATUS_1.getDesc());

+ 3 - 2
fs-service-system/src/main/java/com/fs/live/service/impl/LiveAutoTaskServiceImpl.java

@@ -4,6 +4,7 @@ import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.time.ZoneId;
 import java.util.*;
+import java.util.concurrent.TimeUnit;
 
 import cn.hutool.core.util.ObjectUtil;
 import com.alibaba.fastjson.JSON;
@@ -188,7 +189,7 @@ public class LiveAutoTaskServiceImpl implements ILiveAutoTaskService {
             liveAutoTask.setUpdateTime(null);
             liveAutoTask.setCreateTime(null);
             redisCache.redisTemplate.opsForZSet().add("live:auto_task:" + live.getLiveId(), JSON.toJSONString(liveAutoTask),liveAutoTask.getAbsValue().getTime());
-            redisCache.redisTemplate.expire("live:auto_task:" + live.getLiveId(), 30, java.util.concurrent.TimeUnit.MINUTES);
+            redisCache.redisTemplate.expire("live:auto_task:" + live.getLiveId(), 1, TimeUnit.DAYS);
         }
 
 
@@ -225,7 +226,7 @@ public class LiveAutoTaskServiceImpl implements ILiveAutoTaskService {
             liveAutoTask.setUpdateTime(null);
             liveAutoTask.setCreateTime(null);
             redisCache.redisTemplate.opsForZSet().add("live:auto_task:" + live.getLiveId(), JSON.toJSONString(liveAutoTask),liveAutoTask.getAbsValue().getTime());
-            redisCache.redisTemplate.expire("live:auto_task:" + live.getLiveId(), 30, java.util.concurrent.TimeUnit.MINUTES);
+            redisCache.redisTemplate.expire("live:auto_task:" + live.getLiveId(), 1, TimeUnit.DAYS);
         }
         return R.ok();
     }

+ 2 - 2
fs-service-system/src/main/java/com/fs/live/service/impl/LiveDataServiceImpl.java

@@ -810,7 +810,7 @@ public class LiveDataServiceImpl implements ILiveDataService {
                 LiveUserDetailVo vo = new LiveUserDetailVo();
                 vo.setUserId(userId);
                 // 查询用户信息
-                FsUser user = fsUserMapper.selectFsUserByUserId(userId);
+                FsUser user = fsUserMapper.selectFsUserById(userId);
                 if (user != null) {
                     vo.setUserName(user.getNickname() != null ? user.getNickname() : (user.getNickname() != null ? user.getNickname() : "未知用户"));
                 } else {
@@ -855,7 +855,7 @@ public class LiveDataServiceImpl implements ILiveDataService {
             LiveUserDetailVo userDetail = userDetailMap.computeIfAbsent(userId, k -> {
                 LiveUserDetailVo vo = new LiveUserDetailVo();
                 vo.setUserId(userId);
-                FsUser user = fsUserMapper.selectFsUserByUserId(userId);
+                FsUser user = fsUserMapper.selectFsUserById(userId);
                 if (user != null) {
                     vo.setUserName(user.getNickname() != null ? user.getNickname() : (user.getNickname() != null ? user.getNickname() : "未知用户"));
                 } else {

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

@@ -288,6 +288,9 @@ public class LiveGoodsServiceImpl  implements ILiveGoodsService {
                 .collect(Collectors.toList());
 //
 //        // 批量插入
+        liveGoodsList.forEach(e -> {
+            stockDeductService.initStock(e.getProductId(), liveId, e.getStock().intValue());
+        });
         baseMapper.insertLiveGoodsList(liveGoodsList);
         return R.ok();
     }

+ 2 - 1
fs-service-system/src/main/java/com/fs/live/service/impl/LiveLotteryConfServiceImpl.java

@@ -27,6 +27,7 @@ import org.springframework.transaction.annotation.Transactional;
 
 import java.time.LocalDateTime;
 import java.util.*;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 /**
@@ -111,7 +112,7 @@ public class LiveLotteryConfServiceImpl implements ILiveLotteryConfService {
             LocalDateTime localDateTime = LocalDateTime.now().plusMinutes(liveLotteryConf.getDuration());
             double score = localDateTime.atZone(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli();
             redisCache.redisTemplate.opsForZSet().add(cacheKey, String.valueOf(liveLotteryConf.getLotteryId()), score);
-            redisCache.redisTemplate.expire(cacheKey, 30, java.util.concurrent.TimeUnit.MINUTES);
+            redisCache.redisTemplate.expire(cacheKey, 1, TimeUnit.DAYS);
         } else {
             redisCache.deleteObject(cacheKey);
         }

+ 23 - 0
fs-service-system/src/main/java/com/fs/live/service/impl/LiveMsgServiceImpl.java

@@ -9,6 +9,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.util.Collections;
+import java.util.Date;
 import java.util.List;
 
 /**
@@ -103,4 +104,26 @@ public class LiveMsgServiceImpl implements ILiveMsgService
     public List<LiveMsg> listRecentMsg(Long liveId) {
         return liveMsgMapper.listRecentMsg(liveId);
     }
+
+    /**
+     * 批量新增直播讨论
+     *
+     * @param liveMsgList 直播讨论列表
+     * @return 结果
+     */
+    @Override
+    public int insertLiveMsgBatch(List<LiveMsg> liveMsgList) {
+        if (liveMsgList == null || liveMsgList.isEmpty()) {
+            return 0;
+        }
+
+        Date nowDate = DateUtils.getNowDate();
+        // 设置创建时间
+        liveMsgList.forEach(msg -> {
+            if (msg.getCreateTime() == null) {
+                msg.setCreateTime(nowDate);
+            }
+        });
+        return liveMsgMapper.insertLiveMsgBatch(liveMsgList);
+    }
 }

+ 447 - 34
fs-service-system/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java

@@ -24,10 +24,12 @@ import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 
 import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson.annotation.JSONType;
 import com.fs.common.config.FSSysConfig;
 import com.fs.common.config.LoginContextManager;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.core.redis.RedisUtil;
 import com.fs.common.core.redis.service.StockDeductService;
 import com.fs.common.event.TemplateBean;
 import com.fs.common.event.TemplateEvent;
@@ -73,10 +75,12 @@ import com.fs.system.config.SnowflakeUtils;
 import com.fs.system.service.ISysConfigService;
 import com.fs.wx.domain.FsWxExpressTask;
 import com.fs.wx.mapper.FsWxExpressTaskMapper;
+import lombok.Data;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang.ObjectUtils;
 import org.apache.http.util.Asserts;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.ApplicationEventPublisher;
@@ -175,6 +179,9 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
     @Autowired
     private ApplicationEventPublisher publisher;
 
+    @Autowired
+    private RocketMQTemplate rocketMQTemplate;
+
     @Value("${fsConfig.omsCode}")
     private String deliverOmsCode;
 
@@ -1626,8 +1633,39 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         BigDecimal payPrice = BigDecimal.ZERO;
         BigDecimal payDelivery = BigDecimal.ZERO;
         BigDecimal totalPrice = fsStoreProduct.getPrice().multiply(new BigDecimal(param.getTotalNum()));
+        FsStoreProductAttrValue fsStoreProductAttrValueForDelivery = null;
+        if (!Objects.isNull(param.getAttrValueId())) {
+            // 查询商品属性值,使用缓存
+            Long attrValueId = param.getAttrValueId();
+            String attrValueCacheKey = String.format("product:attr:value:id:%s", attrValueId);
+            String attrValueCacheJson = stringRedisTemplate.opsForValue().get(attrValueCacheKey);
+            
+            if (StringUtils.isNotEmpty(attrValueCacheJson)) {
+                try {
+                    // 缓存命中,解析 JSON 字符串
+                    fsStoreProductAttrValueForDelivery = JSON.parseObject(attrValueCacheJson, FsStoreProductAttrValue.class);
+                } catch (Exception e) {
+                    log.warn("[订单计算] 商品属性值缓存数据解析失败,重新查询数据库,attrValueId: {}, error: {}", attrValueId, e.getMessage());
+                    // 解析失败,清除缓存,重新查询
+                    stringRedisTemplate.delete(attrValueCacheKey);
+                    attrValueCacheJson = null;
+                }
+            }
+            
+            if (fsStoreProductAttrValueForDelivery == null) {
+                // 缓存未命中或解析失败,查询数据库
+                fsStoreProductAttrValueForDelivery = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueById(attrValueId);
+                
+                // 将结果存入缓存,设置过期时间为12小时
+                if (fsStoreProductAttrValueForDelivery != null) {
+                    String jsonStr = JSON.toJSONString(fsStoreProductAttrValueForDelivery);
+                    stringRedisTemplate.opsForValue().set(attrValueCacheKey, jsonStr, 12, TimeUnit.HOURS);
+                }
+            }
+        }
+        
         if (param.getCityId() != null) {
-            payDelivery = handleDeliveryMoney(param.getCityId(), fsStoreProduct, param.getTotalNum());
+            payDelivery = handleDeliveryMoney(param.getCityId(), fsStoreProduct, param.getTotalNum(), fsStoreProductAttrValueForDelivery);
             totalPrice = totalPrice.add(payDelivery);
         }
         return LiveOrderComputeDTO.builder().payPrice(payPrice)
@@ -1649,10 +1687,35 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
             log.error("商品不存在");
             return null;
         }
-        // todo yhq 计算价格接口 是否传入了规格参数
         FsStoreProductAttrValue fsStoreProductAttrValue = null;
         if (!Objects.isNull(param.getAttrValueId())) {
-            fsStoreProductAttrValue = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueById(param.getAttrValueId());
+            // 查询商品属性值,使用缓存
+            Long attrValueId = param.getAttrValueId();
+            String attrValueCacheKey = String.format("product:attr:value:id:%s", attrValueId);
+            String attrValueCacheJson = stringRedisTemplate.opsForValue().get(attrValueCacheKey);
+            
+            if (StringUtils.isNotEmpty(attrValueCacheJson)) {
+                try {
+                    // 缓存命中,解析 JSON 字符串
+                    fsStoreProductAttrValue = JSON.parseObject(attrValueCacheJson, FsStoreProductAttrValue.class);
+                } catch (Exception e) {
+                    log.warn("[订单计算] 商品属性值缓存数据解析失败,重新查询数据库,attrValueId: {}, error: {}", attrValueId, e.getMessage());
+                    // 解析失败,清除缓存,重新查询
+                    stringRedisTemplate.delete(attrValueCacheKey);
+                    attrValueCacheJson = null;
+                }
+            }
+            
+            if (fsStoreProductAttrValue == null) {
+                // 缓存未命中或解析失败,查询数据库
+                fsStoreProductAttrValue = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueById(attrValueId);
+                
+                // 将结果存入缓存,设置过期时间为12小时
+                if (fsStoreProductAttrValue != null) {
+                    String jsonStr = JSON.toJSONString(fsStoreProductAttrValue);
+                    stringRedisTemplate.opsForValue().set(attrValueCacheKey, jsonStr, 12, TimeUnit.HOURS);
+                }
+            }
         }
         BigDecimal totalPrice = BigDecimal.ZERO;
         BigDecimal payPrice = fsStoreProduct.getPrice().multiply(new BigDecimal(param.getTotalNum()));
@@ -1663,7 +1726,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         BigDecimal payDelivery = BigDecimal.ZERO;
         BigDecimal deductionPrice = BigDecimal.ZERO;
         if (param.getCityId() != null) {
-            payDelivery = handleDeliveryMoney(param.getCityId(), fsStoreProduct, param.getTotalNum());
+            payDelivery = handleDeliveryMoney(param.getCityId(), fsStoreProduct, param.getTotalNum(), fsStoreProductAttrValue);
             payPrice = payPrice.add(payDelivery);
         }
 
@@ -1863,7 +1926,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
     public R getExpress(LiveOrder order) {
         //顺丰轨迹查询处理
         String lastFourNumber = "";
-        if (order.getDeliverySn().equals(ShipperCodeEnum.SF.getValue())) {
+        if (order.getDeliveryCode().equals(ShipperCodeEnum.SF.getValue())  || order.getDeliveryCode().equals(ShipperCodeEnum.ZTO.getValue())) {
             lastFourNumber = PhoneUtils.getLastFourNum(order.getUserPhone());
         }
         ExpressInfoDTO dto = expressService.getExpressInfo(order.getOrderCode(), order.getDeliveryCode(), order.getDeliverySn(), lastFourNumber);
@@ -1885,34 +1948,127 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         }
     }
 
-    private BigDecimal handleDeliveryMoney(Long cityId, FsStoreProduct fsStoreProduct, String totalNumSize) {
+    /**
+     * 运费模板缓存数据包装类
+     */
+    @Data
+    public static class ShippingTemplateCacheData {
+        private Map<Long, Integer> shippingTemplatesMap;
+        private Map<Long, FsShippingTemplatesRegion> shippingTemplatesRegionMap;
+        
+        public ShippingTemplateCacheData() {
+        }
+        
+        public ShippingTemplateCacheData(Map<Long, Integer> shippingTemplatesMap, 
+                                        Map<Long, FsShippingTemplatesRegion> shippingTemplatesRegionMap) {
+            this.shippingTemplatesMap = shippingTemplatesMap;
+            this.shippingTemplatesRegionMap = shippingTemplatesRegionMap;
+        }
+    }
+
+    @Autowired
+    private RedisUtil redisUtil;
+
+    @Autowired
+    private org.springframework.data.redis.core.StringRedisTemplate stringRedisTemplate;
+
+    private BigDecimal handleDeliveryMoney(Long cityId, FsStoreProduct fsStoreProduct, String totalNumSize, FsStoreProductAttrValue fsStoreProductAttrValue) {
         BigDecimal storePostage = BigDecimal.ZERO;
-        List<Long> citys = new ArrayList<>();
-        citys.add(cityId);
-        citys.add(0l);
+        // 优化:直接字符串拼接,避免创建List和Stream中间对象
         String ids = String.valueOf(fsStoreProduct.getTempId());
-        List<FsShippingTemplates> shippingTemplatesList = shippingTemplatesService.selectFsShippingTemplatesByIds(ids);
-        String cityIds = String.join(",", citys.stream()
-                .map(String::valueOf).collect(Collectors.toList()));
-        List<FsShippingTemplatesRegion> shippingTemplatesRegionList = shippingTemplatesRegionService.selectFsShippingTemplatesRegionListByTempIdsAndCityIds(ids, cityIds);
-        Map<Long, Integer> shippingTemplatesMap = shippingTemplatesList
-                .stream()
-                .collect(Collectors.toMap(FsShippingTemplates::getId,
-                        FsShippingTemplates::getType));
-        //提取运费模板有相同值覆盖
-        Map<Long, FsShippingTemplatesRegion> shippingTemplatesRegionMap =
-                shippingTemplatesRegionList.stream()
-                        .collect(Collectors.toMap(FsShippingTemplatesRegion::getTempId,
-                                YxShippingTemplatesRegion -> YxShippingTemplatesRegion,
-                                (key1, key2) -> key2));
+        String cityIds = cityId + ",0";
+        
+        // 构建缓存 key:基于 tempId 和 cityIds
+        String cacheKey = String.format("shipping:template:%s:city:%s", ids, cityIds);
+        
+        // 先查缓存(使用 JSON 字符串存储,避免 FastJSON autoType 问题)
+        String cacheJson = stringRedisTemplate.opsForValue().get(cacheKey);
+        Map<Long, Integer> shippingTemplatesMap = null;
+        Map<Long, FsShippingTemplatesRegion> shippingTemplatesRegionMap = null;
+        
+        if (StringUtils.isNotEmpty(cacheJson)) {
+            try {
+                // 缓存命中,解析 JSON 字符串
+                ShippingTemplateCacheData cacheData = JSON.parseObject(cacheJson, ShippingTemplateCacheData.class);
+                if (cacheData != null) {
+                    shippingTemplatesMap = cacheData.getShippingTemplatesMap();
+                    shippingTemplatesRegionMap = cacheData.getShippingTemplatesRegionMap();
+                }
+            } catch (Exception e) {
+                log.warn("[运费计算] 缓存数据解析失败,重新查询数据库,cacheKey: {}, error: {}", cacheKey, e.getMessage());
+                // 解析失败,清除缓存,重新查询
+                stringRedisTemplate.delete(cacheKey);
+                cacheJson = null;
+            }
+        }
+        
+        if (shippingTemplatesMap == null || shippingTemplatesRegionMap == null) {
+            // 缓存未命中或解析失败,查询数据库并计算
+            List<FsShippingTemplates> shippingTemplatesList = shippingTemplatesService.selectFsShippingTemplatesByIds(ids);
+            List<FsShippingTemplatesRegion> shippingTemplatesRegionList = shippingTemplatesRegionService.selectFsShippingTemplatesRegionListByTempIdsAndCityIds(ids, cityIds);
+            
+            // 优化:使用传统for循环替代Stream操作,减少对象创建
+            shippingTemplatesMap = new HashMap<>(shippingTemplatesList.size());
+            for (FsShippingTemplates template : shippingTemplatesList) {
+                shippingTemplatesMap.put(template.getId(), template.getType());
+            }
+            
+            // 优化:使用传统for循环替代Stream操作,减少对象创建
+            shippingTemplatesRegionMap = new HashMap<>(shippingTemplatesRegionList.size());
+            for (FsShippingTemplatesRegion region : shippingTemplatesRegionList) {
+                // 相同tempId时,后面的值覆盖前面的值
+                shippingTemplatesRegionMap.put(region.getTempId(), region);
+            }
+            
+            // 将计算结果放入缓存(使用 JSON 字符串存储),设置过期时间为12小时
+            ShippingTemplateCacheData cacheData = new ShippingTemplateCacheData(shippingTemplatesMap, shippingTemplatesRegionMap);
+            String jsonStr = JSON.toJSONString(cacheData);
+            stringRedisTemplate.opsForValue().set(cacheKey, jsonStr, 12, TimeUnit.HOURS);
+        }
         Long tempId = Long.valueOf(fsStoreProduct.getTempId());
         double num = 0d;
         Integer templateType = shippingTemplatesMap.get(tempId);
-        List<FsStoreProductAttrValue> productAttrValues = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueByProductId(fsStoreProduct.getProductId());
-        if (productAttrValues == null || productAttrValues.isEmpty()) {
-            return storePostage;
+        
+        // 如果传入的 fsStoreProductAttrValue 不为空,直接使用;如果为空,查询数据库
+        FsStoreProductAttrValue productAttrValue = fsStoreProductAttrValue;
+        
+        if (productAttrValue == null) {
+            // 查询商品属性值,使用缓存
+            Long productId = fsStoreProduct.getProductId();
+            String productAttrCacheKey = String.format("product:attr:value:productId:%s", productId);
+            String productAttrCacheJson = stringRedisTemplate.opsForValue().get(productAttrCacheKey);
+            List<FsStoreProductAttrValue> productAttrValues = null;
+            
+            if (StringUtils.isNotEmpty(productAttrCacheJson)) {
+                try {
+                    // 缓存命中,解析 JSON 字符串
+                    productAttrValues = JSON.parseArray(productAttrCacheJson, FsStoreProductAttrValue.class);
+                } catch (Exception e) {
+                    log.warn("[运费计算] 商品属性值缓存数据解析失败,重新查询数据库,productId: {}, error: {}", productId, e.getMessage());
+                    // 解析失败,清除缓存,重新查询
+                    stringRedisTemplate.delete(productAttrCacheKey);
+                    productAttrCacheJson = null;
+                }
+            }
+            
+            if (productAttrValues == null) {
+                // 缓存未命中或解析失败,查询数据库
+                productAttrValues = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueByProductId(productId);
+                
+                // 将结果存入缓存(包括空 list),设置过期时间为12小时
+                if (productAttrValues == null) {
+                    productAttrValues = new ArrayList<>(); // 确保不为 null,空数据也缓存空 list
+                }
+                String jsonStr = JSON.toJSONString(productAttrValues);
+                stringRedisTemplate.opsForValue().set(productAttrCacheKey, jsonStr, 12, TimeUnit.HOURS);
+            }
+            
+            if (productAttrValues.isEmpty()) {
+                return storePostage;
+            }
+            
+            productAttrValue = productAttrValues.get(0);
         }
-        FsStoreProductAttrValue productAttrValue = productAttrValues.get(0);
         Integer totalNum = Integer.valueOf(totalNumSize);
         // TYPE_1: 按件数计算
         if (ShippingTempEnum.TYPE_1.getValue().equals(templateType)) {
@@ -2140,7 +2296,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
     public R syncExpress(Long id) {
         LiveOrder order = baseMapper.selectLiveOrderByOrderId(String.valueOf(id));
         String lastFourNumber = "";
-        if (order.getDeliveryCode().equals(ShipperCodeEnum.SF.getValue())) {
+        if (order.getDeliveryCode().equals(ShipperCodeEnum.SF.getValue()) || order.getDeliveryCode().equals(ShipperCodeEnum.ZTO.getValue())) {
             lastFourNumber = order.getUserPhone();
             if (lastFourNumber.length() == 11) {
                 lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
@@ -2164,7 +2320,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         }
         if ("0".equals(dto.getStateEx()) && "0".equals(dto.getState())) {
             lastFourNumber = "19923690275";
-            if (order.getDeliveryCode().equals(ShipperCodeEnum.SF.getValue())) {
+            if (order.getDeliveryCode().equals(ShipperCodeEnum.SF.getValue()) || order.getDeliveryCode().equals(ShipperCodeEnum.ZTO.getValue())) {
                 lastFourNumber = order.getUserPhone();
                 if (lastFourNumber.length() == 11) {
                     lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
@@ -2473,6 +2629,215 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         });
     }
 
+    @Override
+    public R createLiveOrderTest(LiveOrder liveOrder) {
+        String orderKey = redisCache.getCacheObject("orderKey:" + liveOrder.getOrderKey());
+        if (StringUtils.isEmpty(orderKey)) {
+            return R.error("订单已过期");
+        }
+        if (liveOrder.getLiveId() == null) return R.error("直播ID不能为空");
+        if (liveOrder.getProductId() == null) return R.error("购物商品ID不能为空");
+        if (liveOrder.getUserName() == null) return R.error("用户名不能为空");
+        if (liveOrder.getUserPhone() == null) return R.error("用户手机号不能为空");
+        if (liveOrder.getUserAddress() == null) return R.error("用户地址不能为空");
+        if (liveOrder.getTotalNum() == null) return R.error("商品数量不能为空");
+
+        Live live = liveService.selectLiveByLiveId(liveOrder.getLiveId());
+        if (live == null) return R.error("当前直播不存在");
+        FsStoreProduct fsStoreProduct = fsStoreProductService.selectFsStoreProductById(liveOrder.getProductId());
+        LiveGoods goods = liveGoodsMapper.selectLiveGoodsByProductId(liveOrder.getLiveId(), liveOrder.getProductId());
+        if (goods == null) return R.error("当前商品不存在");
+        if (fsStoreProduct == null) return R.error("店铺已下架商品,购买失败");
+        if (fsStoreProduct.getIsShow() == 0 || goods.getStatus() == 0) return R.error("商品已下架,购买失败");
+//        if(fsStoreProduct.getStock() < Integer.parseInt(liveOrder.getTotalNum()) || goods.getStock() < Integer.parseInt(liveOrder.getTotalNum())) return R.error("抱歉,这款商品已被抢光,暂时无库存~");
+//
+//        String configJson = configService.selectConfigByKey("store.config");
+//        if (org.apache.commons.lang3.StringUtils.isNotEmpty(configJson)) {
+//            com.fs.store.config.StoreConfig config = com.alibaba.fastjson.JSON.parseObject(configJson, com.fs.store.config.StoreConfig.class);
+//            if (config != null && Boolean.TRUE.equals(config.getCheckStock())) {
+//
+//            }
+//        }
+//        FsStoreProductAttrValueScrm attrValue = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueById(liveOrder.getAttrValueId());
+
+
+        // 更改店铺库存
+//        fsStoreProduct.setStock(fsStoreProduct.getStock()-Integer.parseInt(liveOrder.getTotalNum()));
+//        fsStoreProductService.updateFsStoreProduct(fsStoreProduct);
+        CompletableFuture<Boolean> completableFuture = stockDeductService.deductStockAsync(liveOrder.getProductId(), liveOrder.getLiveId(), Integer.parseInt(liveOrder.getTotalNum()), Long.parseLong(liveOrder.getUserId()));
+        try {
+            log.info("{}, 商品REDIS 库存扣减成功!", goods.getLiveId());
+            if (!completableFuture.get()) {
+                return R.error("抱歉,这款商品已被抢光,暂时无库存~");
+            }
+            log.info("{}, 商品REDIS 库存扣减成功!", goods.getLiveId());
+        } catch (InterruptedException | ExecutionException e) {
+            log.error("高并发处理失败", e);
+            return R.error("订单创建失败:" + e.getMessage());
+        }
+        if (goods.getStock() == null) return R.error("直播间商品库存不足");
+        // 更新直播间库存
+//        LiveGoods liveGoods = new LiveGoods();
+//        liveGoods.setGoodsId(goods.getGoodsId());
+//        liveGoods.setStock(goods.getStock() - Integer.parseInt(liveOrder.getTotalNum()));
+//        liveGoods.setSales(goods.getSales() + Integer.parseInt(liveOrder.getTotalNum()));
+//        liveGoodsMapper.updateLiveGoods(goods);
+//        LiveGoods liveGoods = new LiveGoods();
+//        liveGoods.setGoodsId(goods.getGoodsId());
+//        liveGoods.setStock(goods.getStock() - Integer.parseInt(liveOrder.getTotalNum()));
+//        liveGoods.setSales(goods.getSales() + Integer.parseInt(liveOrder.getTotalNum()));
+//        log.info("商品库存修改添加队列:{}", goods.getGoodsId());
+//        try {
+//            boolean offered = liveGoodsQueue.offer(liveGoods, 5, TimeUnit.SECONDS);
+//            if (!offered) {
+//                log.error("liveGoodsStock 队列已满,无法添加日志: {}", JSON.toJSONString(liveGoods));
+//                // 处理队列已满的情况,例如记录到失败队列或持久化存储
+//            }
+//        } catch (InterruptedException e) {
+//            Thread.currentThread().interrupt();
+//            log.error("插入 liveGoodsStock 队列时被中断: {}", e.getMessage(), e);
+//        }
+
+        //判断是否是三种特定产品
+        if (fsStoreProduct.getProductId() != null && (fsStoreProduct.getProductId().equals(3168L)
+                || fsStoreProduct.getProductId().equals(3184L)
+                || fsStoreProduct.getProductId().equals(3185L))) {
+            liveOrder.setStoreHouseCode("YDSP001");
+        } else {
+            liveOrder.setStoreHouseCode("CQDS001");
+        }
+        LiveGoodsUploadMqVo vo = LiveGoodsUploadMqVo.builder().goodsId(goods.getGoodsId()).goodsNum(Integer.parseInt(liveOrder.getTotalNum())).build();
+        try {
+            log.info("订单提交MQ:{}", vo);
+            rocketMQTemplate.syncSend("live-goods-upload-fhhx", JSON.toJSONString(vo));
+        }catch (Exception e){
+            log.error("更新库存失败!{}", vo, e);
+        }
+
+        LiveUserFirstEntry liveUserFirstEntry = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveOrder.getLiveId(), Long.parseLong(liveOrder.getUserId()));
+        liveOrder.setCompanyId(liveUserFirstEntry.getCompanyId());
+        liveOrder.setCompanyUserId(liveUserFirstEntry.getCompanyUserId());
+        liveOrder.setTuiUserId(liveUserFirstEntry.getCompanyUserId());
+
+        String orderSn = SnowflakeUtils.nextId();
+        log.info("订单生成:" + orderSn);
+        liveOrder.setOrderCode(orderSn);
+
+        // 查询商品属性值,使用缓存
+        FsStoreProductAttrValue fsStoreProductAttrValue = null;
+        if (!Objects.isNull(liveOrder.getAttrValueId())) {
+            Long attrValueId = liveOrder.getAttrValueId();
+            String attrValueCacheKey = String.format("product:attr:value:id:%s", attrValueId);
+            String attrValueCacheJson = stringRedisTemplate.opsForValue().get(attrValueCacheKey);
+
+            if (StringUtils.isNotEmpty(attrValueCacheJson)) {
+                try {
+                    // 缓存命中,解析 JSON 字符串
+                    fsStoreProductAttrValue = JSON.parseObject(attrValueCacheJson, FsStoreProductAttrValue.class);
+                } catch (Exception e) {
+                    log.warn("[订单创建] 商品属性值缓存数据解析失败,重新查询数据库,attrValueId: {}, error: {}", attrValueId, e.getMessage());
+                    // 解析失败,清除缓存,重新查询
+                    stringRedisTemplate.delete(attrValueCacheKey);
+                    attrValueCacheJson = null;
+                }
+            }
+
+            if (fsStoreProductAttrValue == null) {
+                // 缓存未命中或解析失败,查询数据库
+                fsStoreProductAttrValue = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueById(attrValueId);
+
+                // 将结果存入缓存,设置过期时间为12小时
+                if (fsStoreProductAttrValue != null) {
+                    String jsonStr = JSON.toJSONString(fsStoreProductAttrValue);
+                    stringRedisTemplate.opsForValue().set(attrValueCacheKey, jsonStr, 12, TimeUnit.HOURS);
+                }
+            }
+        }
+
+        BigDecimal payPrice = fsStoreProduct.getPrice().multiply(new BigDecimal(liveOrder.getTotalNum()));
+        if (fsStoreProductAttrValue != null) {
+            payPrice = fsStoreProductAttrValue.getPrice().multiply(new BigDecimal(liveOrder.getTotalNum()));
+        }
+        // 直播不需要服务费 0915 1735 左
+//        String config=configService.selectConfigByKey("store.config");
+//        StoreConfig storeConfig= JSONUtil.toBean(config,StoreConfig.class);
+//        BigDecimal serviceFee=new BigDecimal(0);
+//        if(storeConfig.getServiceFee()!=null){
+//            if(liveOrder.getCompanyUserId()==null||liveOrder.getCompanyUserId()==0){
+//                serviceFee=storeConfig.getServiceFee();
+//            }
+//        }
+//        payPrice = payPrice.add(serviceFee);
+        // 生成
+        BigDecimal deliveryMoney = handleDeliveryMoney(liveOrder.getCityId(), fsStoreProduct, liveOrder.getTotalNum(), fsStoreProductAttrValue);
+        payPrice = payPrice.add(deliveryMoney);
+        liveOrder.setDiscountMoney(BigDecimal.ZERO);
+
+        //优惠券处理
+        if (liveOrder.getCouponUserId() != null) {
+            LiveCouponUser couponUser = liveCouponUserService.selectLiveCouponUserById(liveOrder.getCouponUserId());
+            if (couponUser != null && couponUser.getStatus() == 0) {
+                if (!couponUser.getUserId().toString().equals(liveOrder.getUserId())) {
+                    return R.error("非法操作");
+                }
+                if (couponUser.getUseMinPrice().compareTo(payPrice) < 1) {
+                    liveOrder.setUserCouponId(couponUser.getId());
+                    liveOrder.setDiscountMoney(couponUser.getCouponPrice());
+                    //更新优惠券状态
+                    couponUser.setStatus(1);
+                    couponUser.setUseTime(new Date());
+                    liveCouponUserService.updateLiveCouponUser(couponUser);
+                }
+            }
+        }
+
+        liveOrder.setItemJson(JSON.toJSONString(fsStoreProduct));
+        liveOrder.setCreateTime(new Date());
+        liveOrder.setUpdateTime(new Date());
+        liveOrder.setPayDelivery(deliveryMoney);
+        liveOrder.setProductId(fsStoreProduct.getProductId());
+        liveOrder.setStatus(OrderInfoEnum.STATUS_1.getValue());
+        liveOrder.setPayType("1");
+        liveOrder.setTotalPrice(payPrice);
+        liveOrder.setPayMoney(liveOrder.getTotalPrice().subtract(liveOrder.getDiscountMoney()));
+        try {
+            if (baseMapper.insertLiveOrderTest(liveOrder) > 0) {
+                LiveOrderItemDTO dto = new LiveOrderItemDTO();
+                dto.setImage(fsStoreProduct.getImage());
+                dto.setSku(String.valueOf(fsStoreProduct.getStock()));
+
+                // 如果已经有规格信息,使用它;否则查询所有规格并过滤出有条码的
+                FsStoreProductAttrValue attrValueForBarCode = fsStoreProductAttrValue;
+                if (attrValueForBarCode == null || StringUtils.isEmpty(attrValueForBarCode.getBarCode())) {
+                    attrValueForBarCode = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueByProductId(fsStoreProduct.getProductId()).stream().filter(attrValue -> StringUtils.isNotEmpty(attrValue.getBarCode())).findFirst().orElse(null);
+                }
+                if (attrValueForBarCode != null) {
+                    dto.setBarCode(attrValueForBarCode.getBarCode());
+                    dto.setGroupBarCode(attrValueForBarCode.getGroupBarCode());
+                }
+
+                dto.setPrice(fsStoreProduct.getPrice());
+                dto.setProductName(fsStoreProduct.getProductName());
+                dto.setNum(Long.valueOf(liveOrder.getTotalNum()));
+
+                LiveOrderItem liveOrderItem = new LiveOrderItem();
+                liveOrderItem.setOrderCode(liveOrder.getOrderCode());
+                liveOrderItem.setOrderId(liveOrder.getOrderId());
+                liveOrderItem.setProductId(liveOrder.getProductId());
+                liveOrderItem.setNum(Long.valueOf(liveOrder.getTotalNum()));
+                liveOrderItem.setJsonInfo(JSON.toJSONString(dto));
+                liveOrderItemMapper.insertLiveOrderItemTest(liveOrderItem);
+                redisCache.deleteObject("orderKey:" + liveOrder.getOrderKey());
+                return R.ok("下单成功").put("order", liveOrder);
+            } else {
+                return R.error("订单创建失败");
+            }
+        } catch (Exception e) {
+            // 异常处理
+            return R.error("订单创建失败:" + e.getMessage());
+        }
+    }
+
     @Autowired
     ILiveService liveService;
 
@@ -2558,6 +2923,13 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         } else {
             liveOrder.setStoreHouseCode("CQDS001");
         }
+        LiveGoodsUploadMqVo vo = LiveGoodsUploadMqVo.builder().goodsId(goods.getGoodsId()).goodsNum(Integer.parseInt(liveOrder.getTotalNum())).build();
+        try {
+            log.info("订单提交MQ:{}", vo);
+            rocketMQTemplate.syncSend("live-goods-upload-fhhx", JSON.toJSONString(vo));
+        }catch (Exception e){
+            log.error("更新库存失败!{}", vo, e);
+        }
 
         LiveUserFirstEntry liveUserFirstEntry = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveOrder.getLiveId(), Long.parseLong(liveOrder.getUserId()));
         liveOrder.setCompanyId(liveUserFirstEntry.getCompanyId());
@@ -2567,7 +2939,44 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         String orderSn = SnowflakeUtils.nextId();
         log.info("订单生成:" + orderSn);
         liveOrder.setOrderCode(orderSn);
+        
+        // 查询商品属性值,使用缓存
+        FsStoreProductAttrValue fsStoreProductAttrValue = null;
+        if (!Objects.isNull(liveOrder.getAttrValueId())) {
+            Long attrValueId = liveOrder.getAttrValueId();
+            String attrValueCacheKey = String.format("product:attr:value:id:%s", attrValueId);
+            String attrValueCacheJson = stringRedisTemplate.opsForValue().get(attrValueCacheKey);
+            
+            if (StringUtils.isNotEmpty(attrValueCacheJson)) {
+                try {
+                    // 缓存命中,解析 JSON 字符串
+                    fsStoreProductAttrValue = JSON.parseObject(attrValueCacheJson, FsStoreProductAttrValue.class);
+                } catch (Exception e) {
+                    log.warn("[订单创建] 商品属性值缓存数据解析失败,重新查询数据库,attrValueId: {}, error: {}", attrValueId, e.getMessage());
+                    // 解析失败,清除缓存,重新查询
+                    stringRedisTemplate.delete(attrValueCacheKey);
+                    attrValueCacheJson = null;
+                }
+            }
+            
+            if (fsStoreProductAttrValue == null) {
+                // 缓存未命中或解析失败,查询数据库
+                fsStoreProductAttrValue = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueById(attrValueId);
+                
+                // 将结果存入缓存,设置过期时间为12小时
+                if (fsStoreProductAttrValue != null) {
+                    String jsonStr = JSON.toJSONString(fsStoreProductAttrValue);
+                    stringRedisTemplate.opsForValue().set(attrValueCacheKey, jsonStr, 12, TimeUnit.HOURS);
+                }
+            }
+        }
+        
+        // 计算价格:如果有规格,使用规格价格;否则使用商品价格
         BigDecimal payPrice = fsStoreProduct.getPrice().multiply(new BigDecimal(liveOrder.getTotalNum()));
+        if (fsStoreProductAttrValue != null) {
+            payPrice = fsStoreProductAttrValue.getPrice().multiply(new BigDecimal(liveOrder.getTotalNum()));
+        }
+        
         // 直播不需要服务费 0915 1735 左
 //        String config=configService.selectConfigByKey("store.config");
 //        StoreConfig storeConfig= JSONUtil.toBean(config,StoreConfig.class);
@@ -2579,7 +2988,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
 //        }
 //        payPrice = payPrice.add(serviceFee);
         // 生成
-        BigDecimal deliveryMoney = handleDeliveryMoney(liveOrder);
+        BigDecimal deliveryMoney = handleDeliveryMoney(liveOrder.getCityId(), fsStoreProduct, liveOrder.getTotalNum(), fsStoreProductAttrValue);
         payPrice = payPrice.add(deliveryMoney);
         liveOrder.setDiscountMoney(BigDecimal.ZERO);
 
@@ -2616,10 +3025,14 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                 dto.setImage(fsStoreProduct.getImage());
                 dto.setSku(String.valueOf(fsStoreProduct.getStock()));
 
-                FsStoreProductAttrValue fsStoreProductAttrValue = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueByProductId(fsStoreProduct.getProductId()).stream().filter(attrValue -> StringUtils.isNotEmpty(attrValue.getBarCode())).findFirst().orElse(null);
-                if (fsStoreProductAttrValue != null) {
-                    dto.setBarCode(fsStoreProductAttrValue.getBarCode());
-                    dto.setGroupBarCode(fsStoreProductAttrValue.getGroupBarCode());
+                // 如果已经有规格信息,使用它;否则查询所有规格并过滤出有条码的
+                FsStoreProductAttrValue attrValueForBarCode = fsStoreProductAttrValue;
+                if (attrValueForBarCode == null || StringUtils.isEmpty(attrValueForBarCode.getBarCode())) {
+                    attrValueForBarCode = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueByProductId(fsStoreProduct.getProductId()).stream().filter(attrValue -> StringUtils.isNotEmpty(attrValue.getBarCode())).findFirst().orElse(null);
+                }
+                if (attrValueForBarCode != null) {
+                    dto.setBarCode(attrValueForBarCode.getBarCode());
+                    dto.setGroupBarCode(attrValueForBarCode.getGroupBarCode());
                 }
 
                 dto.setPrice(fsStoreProduct.getPrice());

+ 127 - 9
fs-service-system/src/main/java/com/fs/live/service/impl/LiveRedConfServiceImpl.java

@@ -4,6 +4,8 @@ package com.fs.live.service.impl;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.json.JSONUtil;
+import cn.hutool.json.JSONObject;
+import com.alibaba.fastjson.JSON;
 import com.fs.common.constant.LiveKeysConstant;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
@@ -19,13 +21,15 @@ import com.fs.live.param.RedPO;
 import com.fs.live.service.ILiveRedConfService;
 import com.fs.live.domain.LiveAutoTask;
 import com.fs.live.service.ILiveAutoTaskService;
-import cn.hutool.json.JSONObject;
 import com.fs.store.domain.FsUser;
 import com.fs.store.service.IFsUserService;
 import com.fs.store.service.impl.FsUserServiceImpl;
+import org.apache.commons.collections4.CollectionUtils;
 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.data.redis.core.script.DefaultRedisScript;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -127,7 +131,7 @@ public class LiveRedConfServiceImpl implements ILiveRedConfService {
             LocalDateTime localDateTime = LocalDateTime.now().plusMinutes(liveRedConf.getDuration());
             double score = localDateTime.atZone(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli();
             redisCache.redisTemplate.opsForZSet().add(cacheKey, String.valueOf(liveRedConf.getRedId()), score);
-            redisCache.redisTemplate.expire(cacheKey, 30, TimeUnit.MINUTES);
+            redisCache.redisTemplate.expire(cacheKey, 1, TimeUnit.DAYS);
         } else {
             // 其他
             redisCache.deleteObject(REDPACKET_REMAININGLOTS_KEY + liveRedConf.getRedId());
@@ -308,7 +312,7 @@ public class LiveRedConfServiceImpl implements ILiveRedConfService {
         // WebSocket 通知
         //String msg = String.format("用户 %d 抢到了红包 %d,获得 %d 芳华币", userId, redId, integral);
         //WebSocketServer.notifyUsers(msg);
-        redisUtil.hashPut(String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_RED, red.getLiveId(), red.getRedId()), String.valueOf(red.getUserId()), JSONUtil.toJsonStr(record));
+        redisUtil.hashPut(String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_RED, red.getLiveId(), red.getRedId()), String.valueOf(red.getUserId()), com.alibaba.fastjson.JSON.toJSON(record));
         FsUser fsUser = new FsUser();
         fsUser.setUserId(red.getUserId());
         fsUser.setIntegral(new BigDecimal(integral));
@@ -365,19 +369,133 @@ public class LiveRedConfServiceImpl implements ILiveRedConfService {
             // 更新数据库
             updateDbByRed(liveRedConf);
             String hashKey = String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_RED, liveRedConf.getLiveId(), liveRedConf.getRedId());
-            Map<Object, Object> hashEntries = redisUtil.hashEntries(hashKey);
+            
+            // 使用非阻塞的 scan 方式扫描 Redis Hash
+            ScanOptions scanOptions = ScanOptions.scanOptions()
+                    .count(1000)
+                    .match("*")
+                    .build();
             List<LiveUserRedRecord> liveUserRedRecords = new ArrayList<>();
-            if (CollUtil.isNotEmpty(hashEntries)) {
-                liveUserRedRecords = hashEntries.values().stream()
-                        .map(value -> JSONUtil.toBean(JSONUtil.parseObj(value), LiveUserRedRecord.class))
-                        .collect(Collectors.toList());
-                userRedRecordMapper.insertLiveUserRedRecordBatch(liveUserRedRecords);
+            List<LiveUserRedRecord> batchList = new ArrayList<>();
+            Date now = new Date();
+            
+            try (
+                    // 开启Cursor分批扫描,try-with-resources自动关闭游标,避免资源泄露
+                    Cursor<Map.Entry<Object, Object>> cursor = redisCache.redisTemplate.opsForHash().scan(hashKey, scanOptions)
+            ) {
+                // 遍历游标,渐进式获取数据
+                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 {
+                        // 解析JSON对象,手动处理时间戳转换为Date
+                        String jsonStr = value.toString();
+                        com.alibaba.fastjson.JSONObject jsonObject = JSON.parseObject(jsonStr);
+                        LiveUserRedRecord record = new LiveUserRedRecord();
+
+                        // 设置基本字段
+                        if (jsonObject.containsKey("redId")) {
+                            record.setRedId(jsonObject.getLong("redId"));
+                        }
+                        if (jsonObject.containsKey("liveId")) {
+                            record.setLiveId(jsonObject.getLong("liveId"));
+                        }
+                        if (jsonObject.containsKey("userId")) {
+                            record.setUserId(jsonObject.getLong("userId"));
+                        }
+                        if (jsonObject.containsKey("integral")) {
+                            record.setIntegral(jsonObject.getLong("integral"));
+                        }
+
+                        // 处理时间戳转换为Date
+                        if (jsonObject.containsKey("createTime")) {
+                            Object createTimeObj = jsonObject.get("createTime");
+                            if (createTimeObj instanceof Date) {
+                                record.setCreateTime((Date) createTimeObj);
+                            } else if (createTimeObj instanceof Number) {
+                                record.setCreateTime(new Date(((Number) createTimeObj).longValue()));
+                            } else if (createTimeObj instanceof String) {
+                                try {
+                                    record.setCreateTime(new Date(Long.parseLong((String) createTimeObj)));
+                                } catch (NumberFormatException e) {
+                                    // 解析失败,使用当前时间
+                                    record.setCreateTime(now);
+                                }
+                            }
+                        }
+
+                        if (jsonObject.containsKey("updateTime")) {
+                            Object updateTimeObj = jsonObject.get("updateTime");
+                            if (updateTimeObj instanceof Date) {
+                                record.setUpdateTime((Date) updateTimeObj);
+                            } else if (updateTimeObj instanceof Number) {
+                                record.setUpdateTime(new Date(((Number) updateTimeObj).longValue()));
+                            } else if (updateTimeObj instanceof String) {
+                                try {
+                                    record.setUpdateTime(new Date(Long.parseLong((String) updateTimeObj)));
+                                } catch (NumberFormatException e) {
+                                    // 解析失败,使用当前时间
+                                    record.setUpdateTime(now);
+                                }
+                            }
+                        }
+
+                        // 确保时间字段不为空
+                        if (record.getCreateTime() == null) {
+                            record.setCreateTime(now);
+                        }
+                        if (record.getUpdateTime() == null) {
+                            record.setUpdateTime(now);
+                        }
+
+                        liveUserRedRecords.add(record);
+                        batchList.add(record);
+
+                        // 达到入库批次大小,批量插入
+                        if (batchList.size() >= 1000) {
+                            userRedRecordMapper.insertLiveUserRedRecordBatch(batchList);
+                            log.info("[红包批量入库] 完成一批次入库,条数:{},hashKey: {}", 1000, hashKey);
+                            batchList.clear();
+                        }
+                    } catch (Exception e) {
+                        log.warn("[红包批量入库] 解析记录失败,跳过该条,hashKey: {}, error: {}",
+                                hashKey, e.getMessage());
+                        continue;
+                    }
+                }
+
+                // 插入剩余数据
+                if (CollectionUtils.isNotEmpty(batchList)) {
+                    userRedRecordMapper.insertLiveUserRedRecordBatch(batchList);
+                    log.info("[红包批量入库] 完成剩余数据入库,条数:{},hashKey: {}", batchList.size(), hashKey);
+                }
+            } catch (Exception e) {
+                log.error("[红包批量入库] 扫描Redis Hash整体失败,hashKey: {}", hashKey, e);
+            }
+            
+            // 处理积分和奖励记录(所有记录插入完成后统一处理)
+            if (CollectionUtils.isNotEmpty(liveUserRedRecords)) {
                 for (LiveUserRedRecord liveUserRedRecord : liveUserRedRecords) {
                     userService.incrIntegral(Collections.singletonList(liveUserRedRecord.getUserId()), liveUserRedRecord.getIntegral());
                     // 保存用户领取芳华币记录 方便统计计算
                     saveUserRewardRecord(liveUserRedRecord);
                 }
             }
+            
             redisUtil.delete(hashKey);
         }
     }

+ 31 - 4
fs-service-system/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java

@@ -5,6 +5,7 @@ import cn.hutool.core.util.ObjectUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 
+import com.fs.common.core.redis.service.StockDeductService;
 import com.fs.common.vo.LiveVo;
 import com.fs.common.constant.LiveKeysConstant;
 import com.fs.common.core.domain.R;
@@ -78,6 +79,8 @@ public class LiveServiceImpl implements ILiveService
 
     @Autowired
     private ILiveGoodsService liveGoodsService;
+    @Autowired
+    private StockDeductService stockDeductService;
 
     @Autowired
     private ILiveRedConfService liveRedConfService;
@@ -152,11 +155,30 @@ public class LiveServiceImpl implements ILiveService
         LiveGoodsVo liveGoodsVo = liveGoodsService.showGoods(liveId);
         List<LiveRedConf> liveRedConfs = liveRedConfService.selectActivedRed(liveId);
         List<LiveLotteryConfVo> liveLotteryConfs = liveLotteryConfService.selectActivedLottery(liveId);
-        List<Long> lotteryIds = liveLotteryConfs.stream().map(LiveLotteryConfVo::getLotteryId).collect(Collectors.toList());
+        
+        // 优化:使用传统for循环替代Stream操作,减少对象创建
+        List<Long> lotteryIds = new ArrayList<>();
+        if (liveLotteryConfs != null && !liveLotteryConfs.isEmpty()) {
+            for (LiveLotteryConfVo conf : liveLotteryConfs) {
+                if (conf.getLotteryId() != null) {
+                    lotteryIds.add(conf.getLotteryId());
+                }
+            }
+        }
+        
         if (!lotteryIds.isEmpty()) {
             List<LiveLotteryProductListVo> products = liveLotteryProductConfMapper.selectLiveLotteryProductConfByLotteryIds(lotteryIds);
-            for (LiveLotteryConfVo liveLotteryConf : liveLotteryConfs) {
-                liveLotteryConf.setProducts(products.stream().filter(product -> product.getLotteryId().equals(liveLotteryConf.getLotteryId())).collect(Collectors.toList()));
+            // 优化:使用传统for循环替代Stream操作,减少对象创建
+            if (liveLotteryConfs != null && products != null) {
+                for (LiveLotteryConfVo liveLotteryConf : liveLotteryConfs) {
+                    List<LiveLotteryProductListVo> matchedProducts = new ArrayList<>();
+                    for (LiveLotteryProductListVo product : products) {
+                        if (product.getLotteryId() != null && product.getLotteryId().equals(liveLotteryConf.getLotteryId())) {
+                            matchedProducts.add(product);
+                        }
+                    }
+                    liveLotteryConf.setProducts(matchedProducts);
+                }
             }
         }
         return LiveConfigVo.builder().liveRedConfs(liveRedConfs).liveLotteryConfs(liveLotteryConfs).liveGoodsVo(liveGoodsVo).build();
@@ -784,6 +806,11 @@ public class LiveServiceImpl implements ILiveService
             redisCache.redisTemplate.expire("live:auto_task:"+live.getLiveId(), 1, TimeUnit.DAYS);
         });
 
+        String cacheKey = String.format(LiveKeysConstant.LIVE_DATA_CACHE, live.getLiveId());
+        redisCache.deleteObject(cacheKey);
+        String cacheKey2 = String.format(LiveKeysConstant.LIVE_FLAG_CACHE, live.getLiveId());
+        redisCache.deleteObject(cacheKey2);
+
         return R.ok();
     }
 
@@ -1058,7 +1085,7 @@ public class LiveServiceImpl implements ILiveService
                 newGoods.setStock(goodsMap.containsKey(liveGoods.getProductId())
                         ? goodsMap.get(liveGoods.getProductId()).getStock() : 0);
                 liveGoodsService.insertLiveGoods(newGoods);
-
+                stockDeductService.initStock(newGoods.getProductId(), newLiveId, newGoods.getStock().intValue());
                 goodsIdMapping.put(liveGoods.getGoodsId(), newGoods.getGoodsId());
 
                 // 复制商品推送任务(taskType=1)

+ 121 - 9
fs-service-system/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java

@@ -15,6 +15,7 @@ import com.fs.live.domain.Live;
 import com.fs.live.domain.LiveWatchUser;
 import com.fs.live.mapper.LiveMapper;
 import com.fs.live.mapper.LiveWatchUserMapper;
+import com.fs.live.service.ILiveService;
 import com.fs.live.service.ILiveWatchUserService;
 import com.fs.live.vo.LiveWatchUserStatistics;
 import com.fs.live.vo.LiveWatchUserVO;
@@ -190,10 +191,14 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
         return result;
     }
 
+    @Autowired
+    ILiveService liveService;
+
     @Override
     public LiveWatchUser join(FsUser fsUser,long liveId, long userId, String location) {
         // 查询直播间信息
-        Live live = liveMapper.selectLiveByLiveId(liveId);
+        // 缓存直播间信息,过期时间4小时
+        Live live = liveService.selectLiveByLiveId(liveId);
         if (live == null) {
             throw new RuntimeException("直播间不存在");
         }
@@ -204,17 +209,22 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
         Integer liveFlag = flagMap.get("liveFlag");
         Integer replayFlag = flagMap.get("replayFlag");
 
+        if (live.getLiveType() == 1) {
+            liveFlag = 1;
+            replayFlag = 0;
+        }
+
         // 使用唯一索引查询:live_id, user_id, live_flag, replay_flag
         LiveWatchUser liveWatchUser = baseMapper.selectByUniqueIndex(liveId, userId, liveFlag, replayFlag);
 
         if (liveWatchUser != null) {
-            // 存在则更新
+            // 存在则更新(只更新:update_time, online, location)
             liveWatchUser.setUpdateTime(now);
             liveWatchUser.setOnline(0);
             if (StringUtils.isNotEmpty(location)) {
                 liveWatchUser.setLocation(location);
             }
-            baseMapper.updateLiveWatchUser(liveWatchUser);
+            baseMapper.updateLiveWatchUserOnJoin(liveWatchUser);
         } else {
             // 不存在则插入
             liveWatchUser = new LiveWatchUser();
@@ -237,10 +247,14 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
 //        redisCache.hashPut(hashKey, String.valueOf(userId), JSON.toJSONString(liveWatchUser));
         return liveWatchUser;
     }
+
+    private static final String USER_ENTRY_TIME_KEY = "live:user:entry:time:%s:%s";
+
     @Override
     public LiveWatchUser close(long liveId, long userId) {
         // 查询直播间信息
-        Live live = liveMapper.selectLiveByLiveId(liveId);
+        // 缓存直播间信息,过期时间4小时
+        Live live = liveService.selectLiveByLiveId(liveId);
         if (live == null) {
             throw new RuntimeException("直播间不存在");
         }
@@ -249,22 +263,100 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
         Map<String, Integer> flagMap = getLiveFlagWithCache(liveId);
         Integer liveFlag = flagMap.get("liveFlag");
         Integer replayFlag = flagMap.get("replayFlag");
+        if (live.getLiveType() == 1) {
+            liveFlag = 1;
+            replayFlag = 0;
+        }
 
         // 使用唯一索引查询:live_id, user_id, live_flag, replay_flag
         LiveWatchUser liveWatchUser = baseMapper.selectByUniqueIndex(liveId, userId, liveFlag, replayFlag);
+        if (liveWatchUser == null) {
+            return null;
+        }
+        // 从 Redis 获取用户进入时间
+        String entryTimeKey = String.format(USER_ENTRY_TIME_KEY, liveId, userId);
         // 设置在线时长
         try {
-            Long onlineSeconds = liveWatchUser.getOnlineSeconds();
-            if(onlineSeconds == null) onlineSeconds = 0L;
-            liveWatchUser.setOnlineSeconds(onlineSeconds + (System.currentTimeMillis() - liveWatchUser.getUpdateTime().getTime()) / 1000);
+            // 如果不在直播中(status != 2),不计算在线时长,保持原有 onlineSeconds
+            if (live.getStatus() != null && live.getStatus() == 2) {
+                // 在直播中,计算在线时长
+                Object entryTimeObj = redisCache.getCacheObject(entryTimeKey);
+                Long entryTime = null;
+                if (entryTimeObj != null) {
+                    if (entryTimeObj instanceof Long) {
+                        entryTime = (Long) entryTimeObj;
+                    } else if (entryTimeObj instanceof String) {
+                        try {
+                            entryTime = Long.parseLong((String) entryTimeObj);
+                        } catch (NumberFormatException e) {
+                            log.error("无法解析进入时间字符串为Long: {}", entryTimeObj);
+                        }
+                    } else if (entryTimeObj instanceof Number) {
+                        entryTime = ((Number) entryTimeObj).longValue();
+                    }
+                }
+
+                long currentTimeMillis = System.currentTimeMillis();
+                if (entryTime == null) {
+                    // 如果没有进入时间记录,可使用用户更新时间
+                    if (liveWatchUser.getUpdateTime() == null) {
+                        entryTime = currentTimeMillis;
+                    } else {
+                        entryTime = liveWatchUser.getUpdateTime().getTime();
+                    }
+                }
+
+                // 获取开播时间和关播时间(转换为毫秒时间戳)
+                long startTimeMillis = 0;
+                long finishTimeMillis = Long.MAX_VALUE;
+                
+                if (live.getStartTime() != null) {
+                    // LocalDateTime 转换为毫秒时间戳
+                    startTimeMillis = live.getStartTime().atZone(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli();
+                }
+                
+                if (live.getFinishTime() != null) {
+                    // LocalDateTime 转换为毫秒时间戳
+                    finishTimeMillis = live.getFinishTime().atZone(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli();
+                }
+
+                // 如果进入时间小于开播时间,进入时间按开播时间计算
+                if (startTimeMillis > 0 && entryTime < startTimeMillis) {
+                    entryTime = startTimeMillis;
+                }
+
+                // 如果退出时间超过关播时间,退出时间按关播时间计算
+                long exitTime = currentTimeMillis;
+                if (exitTime > finishTimeMillis) {
+                    exitTime = finishTimeMillis;
+                }
+
+                // 计算在线时长(秒)
+                long durationSeconds = (exitTime - entryTime) / 1000;
+                // 确保时长不为负数
+                if (durationSeconds < 0) {
+                    durationSeconds = 0;
+                }
+
+                Long onlineSeconds = liveWatchUser.getOnlineSeconds();
+                if(onlineSeconds == null) onlineSeconds = 0L;
+                liveWatchUser.setOnlineSeconds(onlineSeconds + durationSeconds);
+            }
         } catch (Exception e) {
-            log.error("设置在线时长异常:{}", e.getMessage());
+            log.error("设置在线时长异常:{}", e.getMessage(), e);
         }
+        // 更新用户离开直播间时的字段(只更新:update_time, online, online_seconds)
         liveWatchUser.setUpdateTime(DateUtils.getNowDate());
         liveWatchUser.setOnline(1);
-        baseMapper.updateLiveWatchUser(liveWatchUser);
+        // 确保 onlineSeconds 不为 null
+        if (liveWatchUser.getOnlineSeconds() == null) {
+            liveWatchUser.setOnlineSeconds(0L);
+        }
+        baseMapper.updateLiveWatchUserOnClose(liveWatchUser);
         String hashKey  = String.format(LiveKeysConstant.LIVE_WATCH_USERS, liveId);
         redisUtil.hashDelete(hashKey, String.valueOf(userId));
+        // 删除 Redis 中的进入时间记录
+        redisCache.deleteObject(entryTimeKey);
         return liveWatchUser;
     }
 
@@ -288,9 +380,16 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
     public int changeUserState(Long liveId, Long userId) {
         List<LiveWatchUser> liveWatchUser = getByLiveIdAndUserId(liveId, userId);
         if (Objects.nonNull(liveWatchUser)) {
+            String msgKey = String.format("live:blocked:users:%s", liveId);
             for (LiveWatchUser watchUser : liveWatchUser) {
                 watchUser.setMsgStatus(Math.abs(1 - watchUser.getMsgStatus()));
                 watchUser.setUpdateTime(DateUtils.getNowDate());
+                if (watchUser.getMsgStatus() == 0) {
+                    redisCache.redisTemplate.opsForSet().remove(msgKey, userId);
+                } else {
+                    redisCache.redisTemplate.opsForSet().add(msgKey, userId);
+                }
+
                 baseMapper.updateLiveWatchUser(watchUser);
             }
             return 1;
@@ -338,6 +437,13 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
         return baseMapper.selectLiveWatchAndRegisterUser(liveId, lotteryId);
     }
 
+    @Override
+    public List<LiveWatchUser> selectRandomLiveWatchAndRegisterUser(Long liveId, Long lotteryId, Integer limit) {
+        Map<String, Integer> liveFlagWithCache = getLiveFlagWithCache(liveId);
+        Integer liveFlag = liveFlagWithCache.get("liveFlag");
+        return baseMapper.selectRandomLiveWatchAndRegisterUser(liveId, lotteryId, limit,liveFlag);
+    }
+
     @Override
     public List<LiveWatchUserVO> asyncToCache(Long liveId) {
         LiveWatchUser liveWatchUser = new LiveWatchUser();
@@ -385,4 +491,10 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
         return R.ok().put("data", liveWatchUserStatistics);
     }
 
+    @Override
+    public void clearLiveFlagCache(Long liveId) {
+        String cacheKey = String.format(LiveKeysConstant.LIVE_FLAG_CACHE, liveId);
+        redisCache.deleteObject(cacheKey);
+    }
+
 }

+ 21 - 1
fs-service-system/src/main/java/com/fs/live/utils/redis/RedisBatchHandler.java

@@ -10,7 +10,9 @@ import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 
 @Slf4j
@@ -50,11 +52,29 @@ public class RedisBatchHandler {
         List<LiveTrafficLog> batchList = jsonList.stream()
                 .map(json -> JSON.parseObject(json, LiveTrafficLog.class))
                 .collect(Collectors.toList());
+        int size = batchList.size();
+
+        Map<String, LiveTrafficLog> uniqueMap = batchList.stream()
+                .collect(Collectors.toMap(
+                        // 分组key:uuId(为null的话用一个特殊值,避免分组异常)
+                        log -> log.getUuId()== null ? "NULL_UUID_" + System.currentTimeMillis() : log.getUuId(),
+                        // value:当前实体
+                        log -> log,
+                        // 合并规则:如果key重复,保留internetTraffic更大的那个
+                        (log1, log2) -> {
+                            // 处理internetTraffic为null的情况,视为0
+                            long traffic1 = log1.getInternetTraffic() == null ? 0L : log1.getInternetTraffic();
+                            long traffic2 = log2.getInternetTraffic() == null ? 0L : log2.getInternetTraffic();
+                            // 比较流量,返回更大的那个;相等时返回任意一个(这里返回log2)
+                            return traffic2 > traffic1 ? log2 : log1;
+                        }
+                ));
+        batchList = new ArrayList<>(uniqueMap.values());
         try {
             // 批量写入数据库
             liveTrafficLogMapper.batchInsert(batchList);
             // 删除已消费的数据
-            redisTemplate.opsForList().trim(BATCH_QUEUE_KEY, batchList.size(), -1);
+            redisTemplate.opsForList().trim(BATCH_QUEUE_KEY, size, -1);
         } catch (Exception e) {
             // 消费失败:记录日志,不删除Redis数据(重试)
             log.error("Redis批量消费失败,将重试", e);

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

@@ -138,6 +138,8 @@ public class LiveAfterSalesVo {
     @Excel(name = "提交时间",dateFormat = "yyyy-MM-dd HH:mm:ss")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private Date createTime;
+    private Date beginTime;
+    private Date endTime;
 
 
 

+ 17 - 0
fs-service-system/src/main/java/com/fs/live/vo/LiveGoodsUploadMqVo.java

@@ -0,0 +1,17 @@
+package com.fs.live.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class LiveGoodsUploadMqVo {
+
+    private Long goodsId;
+    private Integer goodsNum;
+
+}

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio