Bläddra i källkod

1、迁移直播
2、调整错误

yys 1 månad sedan
förälder
incheckning
fac7525970
100 ändrade filer med 5449 tillägg och 206 borttagningar
  1. 3 0
      fs-common/src/main/java/com/fs/common/constant/LiveKeysConstant.java
  2. 16 0
      fs-common/src/main/java/com/fs/common/constant/RedisConstant.java
  3. 12 0
      fs-common/src/main/java/com/fs/common/core/redis/RedisCache.java
  4. 172 0
      fs-common/src/main/java/com/fs/common/core/redis/service/StockDeductService.java
  5. 2 2
      fs-company/src/main/resources/application.yml
  6. 5 0
      fs-live-app/src/main/java/com/fs/framework/aspectj/LiveWatchUserAspect.java
  7. 45 11
      fs-live-app/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  8. 26 11
      fs-live-app/src/main/java/com/fs/framework/config/RedisConfig.java
  9. 113 29
      fs-live-app/src/main/java/com/fs/live/task/Task.java
  10. 59 0
      fs-live-app/src/main/java/com/fs/live/utils/WebSocketRateLimiter.java
  11. 55 26
      fs-live-app/src/main/java/com/fs/live/websocket/auth/WebSocketConfigurator.java
  12. 63 0
      fs-live-app/src/main/java/com/fs/live/websocket/config/WebSocketSessionManager.java
  13. 501 97
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  14. 330 0
      fs-live-app/src/main/java/com/fs/live/websocket/test/WebSocketTestClient.java
  15. 138 0
      fs-live-mq/pom.xml
  16. 14 0
      fs-live-mq/src/main/java/com/fs/FSServletInitializer.java
  17. 27 0
      fs-live-mq/src/main/java/com/fs/FsLiveMqApplication.java
  18. 12 0
      fs-live-mq/src/main/java/com/fs/app/annotation/Login.java
  19. 15 0
      fs-live-mq/src/main/java/com/fs/app/annotation/LoginUser.java
  20. 125 0
      fs-live-mq/src/main/java/com/fs/app/controller/AdCallbackController.java
  21. 37 0
      fs-live-mq/src/main/java/com/fs/app/controller/CommonController.java
  22. 215 0
      fs-live-mq/src/main/java/com/fs/app/controller/MockAppController.java
  23. 51 0
      fs-live-mq/src/main/java/com/fs/app/exception/FSException.java
  24. 81 0
      fs-live-mq/src/main/java/com/fs/app/exception/FSExceptionHandler.java
  25. 49 0
      fs-live-mq/src/main/java/com/fs/app/mq/RocketMQConsumerService.java
  26. 182 0
      fs-live-mq/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java
  27. 73 0
      fs-live-mq/src/main/java/com/fs/framework/aspectj/DataSourceAspect.java
  28. 245 0
      fs-live-mq/src/main/java/com/fs/framework/aspectj/LogAspect.java
  29. 117 0
      fs-live-mq/src/main/java/com/fs/framework/aspectj/RateLimiterAspect.java
  30. 31 0
      fs-live-mq/src/main/java/com/fs/framework/config/ApplicationConfig.java
  31. 85 0
      fs-live-mq/src/main/java/com/fs/framework/config/CaptchaConfig.java
  32. 92 0
      fs-live-mq/src/main/java/com/fs/framework/config/DataSourceConfig.java
  33. 123 0
      fs-live-mq/src/main/java/com/fs/framework/config/DruidConfig.java
  34. 72 0
      fs-live-mq/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  35. 59 0
      fs-live-mq/src/main/java/com/fs/framework/config/FilterConfig.java
  36. 76 0
      fs-live-mq/src/main/java/com/fs/framework/config/KaptchaTextCreator.java
  37. 150 0
      fs-live-mq/src/main/java/com/fs/framework/config/MyBatisConfig.java
  38. 158 0
      fs-live-mq/src/main/java/com/fs/framework/config/RedisConfig.java
  39. 65 0
      fs-live-mq/src/main/java/com/fs/framework/config/ResourcesConfig.java
  40. 50 0
      fs-live-mq/src/main/java/com/fs/framework/config/SecurityConfig.java
  41. 33 0
      fs-live-mq/src/main/java/com/fs/framework/config/ServerConfig.java
  42. 121 0
      fs-live-mq/src/main/java/com/fs/framework/config/SwaggerConfig.java
  43. 63 0
      fs-live-mq/src/main/java/com/fs/framework/config/ThreadPoolConfig.java
  44. 77 0
      fs-live-mq/src/main/java/com/fs/framework/config/properties/DruidProperties.java
  45. 27 0
      fs-live-mq/src/main/java/com/fs/framework/datasource/DynamicDataSource.java
  46. 45 0
      fs-live-mq/src/main/java/com/fs/framework/datasource/DynamicDataSourceContextHolder.java
  47. 56 0
      fs-live-mq/src/main/java/com/fs/framework/interceptor/RepeatSubmitInterceptor.java
  48. 126 0
      fs-live-mq/src/main/java/com/fs/framework/interceptor/impl/SameUrlDataInterceptor.java
  49. 56 0
      fs-live-mq/src/main/java/com/fs/framework/manager/AsyncManager.java
  50. 40 0
      fs-live-mq/src/main/java/com/fs/framework/manager/ShutdownManager.java
  51. 103 0
      fs-live-mq/src/main/java/com/fs/framework/manager/factory/AsyncFactory.java
  52. 1 0
      fs-live-mq/src/main/resources/META-INF/spring-devtools.properties
  53. 37 0
      fs-live-mq/src/main/resources/i18n/messages.properties
  54. 93 0
      fs-live-mq/src/main/resources/logback.xml
  55. 15 0
      fs-live-mq/src/main/resources/mybatis/mybatis-config.xml
  56. 5 1
      fs-service/pom.xml
  57. 3 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java
  58. 14 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserCompanyUserMapper.java
  59. 54 0
      fs-service/src/main/java/com/fs/enums/ExceptionCodeEnum.java
  60. 13 0
      fs-service/src/main/java/com/fs/his/mapper/MerchantAppConfigMapper.java
  61. 1 1
      fs-service/src/main/java/com/fs/his/service/impl/FsPackageOrderServiceImpl.java
  62. 1 1
      fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java
  63. 1 0
      fs-service/src/main/java/com/fs/hisStore/config/StoreConfig.java
  64. 3 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStorePaymentScrm.java
  65. 45 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductPurchaseLimitScrm.java
  66. 6 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductScrm.java
  67. 76 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductPurchaseLimitScrmMapper.java
  68. 10 1
      fs-service/src/main/java/com/fs/hisStore/mapper/FsUserScrmMapper.java
  69. 91 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductPurchaseLimitScrmService.java
  70. 1 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  71. 1 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStorePaymentScrmServiceImpl.java
  72. 247 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductPurchaseLimitScrmServiceImpl.java
  73. 2 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java
  74. 24 17
      fs-service/src/main/java/com/fs/huifuPay/sdk/opps/core/utils/HuiFuUtils.java
  75. 6 0
      fs-service/src/main/java/com/fs/live/domain/Live.java
  76. 3 1
      fs-service/src/main/java/com/fs/live/domain/LiveCouponUser.java
  77. 7 2
      fs-service/src/main/java/com/fs/live/domain/LiveGoods.java
  78. 2 0
      fs-service/src/main/java/com/fs/live/domain/LiveOrderPayment.java
  79. 49 0
      fs-service/src/main/java/com/fs/live/domain/LiveSignRecord.java
  80. 2 0
      fs-service/src/main/java/com/fs/live/mapper/LiveAutoTaskMapper.java
  81. 9 0
      fs-service/src/main/java/com/fs/live/mapper/LiveCompletionPointsRecordMapper.java
  82. 7 0
      fs-service/src/main/java/com/fs/live/mapper/LiveCouponUserMapper.java
  83. 9 0
      fs-service/src/main/java/com/fs/live/mapper/LiveDataMapper.java
  84. 8 1
      fs-service/src/main/java/com/fs/live/mapper/LiveGoodsMapper.java
  85. 3 0
      fs-service/src/main/java/com/fs/live/mapper/LiveMapper.java
  86. 8 0
      fs-service/src/main/java/com/fs/live/mapper/LiveMsgMapper.java
  87. 1 0
      fs-service/src/main/java/com/fs/live/mapper/LiveOrderItemMapper.java
  88. 20 0
      fs-service/src/main/java/com/fs/live/mapper/LiveOrderMapper.java
  89. 11 1
      fs-service/src/main/java/com/fs/live/mapper/LiveOrderPaymentMapper.java
  90. 17 0
      fs-service/src/main/java/com/fs/live/mapper/LiveSignRecordMapper.java
  91. 2 0
      fs-service/src/main/java/com/fs/live/mapper/LiveTrafficLogMapper.java
  92. 6 1
      fs-service/src/main/java/com/fs/live/mapper/LiveUserFirstEntryMapper.java
  93. 3 0
      fs-service/src/main/java/com/fs/live/mapper/LiveVideoMapper.java
  94. 8 0
      fs-service/src/main/java/com/fs/live/mapper/LiveWatchLogMapper.java
  95. 3 0
      fs-service/src/main/java/com/fs/live/mapper/LiveWatchUserMapper.java
  96. 6 0
      fs-service/src/main/java/com/fs/live/param/MergedOrderQueryParam.java
  97. 2 0
      fs-service/src/main/java/com/fs/live/service/ILiveCompletionPointsRecordService.java
  98. 11 0
      fs-service/src/main/java/com/fs/live/service/ILiveDataService.java
  99. 2 1
      fs-service/src/main/java/com/fs/live/service/ILiveGoodsService.java
  100. 9 0
      fs-service/src/main/java/com/fs/live/service/ILiveMsgService.java

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

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

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

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

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

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

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

@@ -0,0 +1,172 @@
+package com.fs.common.core.redis.service;
+
+import com.fs.common.constant.RedisConstant;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Random;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.IntStream;
+
+/**
+ * 高并发库存扣减服务(Java 8 + Redis分布式锁)
+ */
+@Slf4j
+@Service
+public class StockDeductService {
+
+    // 注入RedisTemplate
+    public final RedisTemplate<String, Object> redisTemplate;
+
+    // 构造器注入(Spring 推荐,Java 8 支持)
+    public StockDeductService(RedisTemplate<String, Object> redisTemplate) {
+        this.redisTemplate = redisTemplate;
+    }
+
+    // 库存扣减Lua脚本(预编译,提升高并发性能)
+    private static final DefaultRedisScript<Long> STOCK_DEDUCT_SCRIPT;
+
+    // 库存扣减Lua脚本(优化后,增强健壮性)
+    static {
+        // 初始化库存扣减脚本
+        STOCK_DEDUCT_SCRIPT = new DefaultRedisScript<>();
+        STOCK_DEDUCT_SCRIPT.setScriptText("if redis.call('exists', KEYS[1]) ~= 1 then " + "return -2; " + "end " + "local stock_str = redis.call('get', KEYS[1]); " + "local stock = tonumber(stock_str); " + "if stock == nil then " + "return -3; " + "end " + "local deductNum_str = ARGV[1]; " + "local deductNum = tonumber(deductNum_str); " + "if deductNum == nil or deductNum <= 0 then " + "return -4; " + "end " + "if stock >= deductNum then " + "return redis.call('decrby', KEYS[1], deductNum); " + "else " + "return -1; " + "end");
+        STOCK_DEDUCT_SCRIPT.setResultType(Long.class);
+    }
+
+    /**
+     * 初始化商品库存(Redis)
+     *
+     * @param productId 商品ID
+     * @param initStock 初始库存
+     */
+    public void initStock(Long productId, Long liveId, Integer initStock) {
+        String stockKey = RedisConstant.STOCK_KEY_PREFIX + liveId + ":" + productId;
+        redisTemplate.opsForValue().set(stockKey, initStock);
+        log.info("商品" + productId + "库存初始化完成,初始库存:" + initStock);
+    }
+
+    /**
+     * 同步获取商品库存(基础版,适用于低并发/实时性要求高的场景)
+     *
+     * @param productId 商品ID
+     * @param liveId    直播间ID(贴合原有库存Key的拆分粒度)
+     * @return 库存数量(库存不存在/非数字时返回0)
+     */
+    public Integer getStock(Long productId, Long liveId) {
+        // 1. 参数合法性校验(避免空ID导致Redis Key异常)
+        if (productId == null || liveId == null) {
+            log.warn("获取库存失败:商品ID/直播间ID为空");
+            return 0;
+        }
+
+        // 2. 拼接库存Key(与初始化/扣减逻辑保持一致)
+        String stockKey = RedisConstant.STOCK_KEY_PREFIX + liveId + ":" + productId;
+
+        try {
+            // 3. 读取Redis库存值(Java 8 Optional处理空值)
+            Object stockObj = redisTemplate.opsForValue().get(stockKey);
+            Integer stock = Optional.ofNullable(stockObj)
+                    // 处理Redis值非数字的情况(如脏数据)
+                    .map(val -> {
+                        if (val instanceof Integer) {
+                            return (Integer) val;
+                        } else if (val instanceof String) {
+                            try {
+                                return Integer.parseInt((String) val);
+                            } catch (NumberFormatException e) {
+                                log.error("库存值格式异常,Key:{},值:{}", stockKey, val, e);
+                                return 0;
+                            }
+                        } else {
+                            log.error("库存值类型不支持,Key:{},类型:{}", stockKey, val.getClass().getName());
+                            return 0;
+                        }
+                    })
+                    // 库存Key不存在时返回0
+                    .orElse(0);
+
+            log.info("获取商品{}库存成功,直播间{},当前库存:{}", productId, liveId, stock);
+            return stock;
+        } catch (Exception e) {
+            // 4. 异常兜底(Redis连接异常/超时等场景)
+            log.error("获取商品{}库存异常,直播间{}", productId, liveId, e);
+            return 0;
+        }
+    }
+
+    /**
+     * 异步获取商品库存(高并发版,适配原有异步扣减逻辑)
+     *
+     * @param productId 商品ID
+     * @param liveId    直播间ID
+     * @return 异步结果:库存数量(异常/空值返回0)
+     */
+    public CompletableFuture<Integer> getStockAsync(Long productId, Long liveId) {
+        // 复用CompletableFuture异步化,避免主线程阻塞(Java 8特性)
+        return CompletableFuture.supplyAsync(() -> getStock(productId, liveId));
+    }
+
+    /**
+     * 高并发库存扣减(核心方法,落地Java 8特性)
+     *
+     * @param productId 商品ID
+     * @param deductNum 扣减数量(默认1)
+     * @return 扣减结果:true=成功,false=失败
+     */
+    public CompletableFuture<Boolean> deductStockAsync(Long productId, Long liveId, Integer deductNum, Long userId) {
+        // Java 8 CompletableFuture 异步处理,提升高并发吞吐量
+        return CompletableFuture.supplyAsync(() -> {
+            // 1. 参数校验(Java 8 Optional 空值处理)
+            Integer num = Optional.ofNullable(deductNum).orElse(1);
+            String stockKey = RedisConstant.STOCK_KEY_PREFIX + liveId + ":" + productId;
+            try {
+                // 5. 执行库存扣减Lua脚本(原子操作,防超卖)
+                // 新增日志:打印当前库存值和扣减数量
+                Integer currentStockStr = (Integer) redisTemplate.opsForValue().get(stockKey);
+                log.info("拿到锁成功 → 库存Key:{},当前库存值:{},扣减数量:{}", stockKey, currentStockStr, num);
+
+                // 执行库存扣减Lua脚本
+                Long remainingStock = redisTemplate.execute(STOCK_DEDUCT_SCRIPT, Collections.singletonList(stockKey), 1);
+
+                // 新增日志:打印Lua返回结果
+                log.info("Lua脚本返回值:{}", remainingStock);
+
+                // 6. 判断扣减结果
+                if (remainingStock >= 0) {
+                    log.info("商品{}库存扣减成功,剩余库存:{}", productId, remainingStock);
+                    return true;
+                } else {
+                    String errorMsg = "";
+                    switch (remainingStock.intValue()) {
+                        case -1:
+                            errorMsg = "库存不足";
+                            break;
+                        case -2:
+                            errorMsg = "库存Key不存在";
+                            break;
+                        case -3:
+                            errorMsg = "库存值非数字";
+                            break;
+                        case -4:
+                            errorMsg = "扣减数量无效";
+                            break;
+                        default:
+                            errorMsg = "未知错误,错误码:" + remainingStock;
+                    }
+                    log.info("商品{}扣减失败:{}", productId, errorMsg);
+                    return false;
+                }
+            }catch (Exception e) {
+                log.error("支付失败获取失败", e);
+                return false;
+            }
+        });
+    }
+}

+ 2 - 2
fs-company/src/main/resources/application.yml

@@ -3,9 +3,9 @@ server:
 # Spring配置
 spring:
   profiles:
-    active: dev
+#    active: dev
 #    active: druid-jnsyj-test
-#    active: druid-jnmy-test
+    active: druid-jnmy-test
 #    active: druid-jzzx-test
 #    active: druid-hdt
 #    active: druid-bjzm-test

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

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

+ 45 - 11
fs-live-app/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -1,11 +1,16 @@
 package com.fs.framework.config;
 
-import com.alibaba.fastjson2.JSON;
-import com.alibaba.fastjson2.JSONReader;
-import com.alibaba.fastjson2.JSONWriter;
+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序列化
@@ -14,25 +19,54 @@ import org.springframework.data.redis.serializer.SerializationException;
  */
 public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 {
-    private final Class<T> clazz;
+    @SuppressWarnings("unused")
+    private ObjectMapper objectMapper = new ObjectMapper();
 
-    public FastJson2JsonRedisSerializer(Class<T> clazz) {
+    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) {
+    public byte[] serialize(T t) throws SerializationException
+    {
+        if (t == null)
+        {
             return new byte[0];
         }
-        return JSON.toJSONBytes(t, JSONWriter.Feature.WriteClassName);
+        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
     }
 
     @Override
-    public T deserialize(byte[] bytes) throws SerializationException {
-        if (bytes == null || bytes.length == 0) {
+    public T deserialize(byte[] bytes) throws SerializationException
+    {
+        if (bytes == null || bytes.length <= 0)
+        {
             return null;
         }
-        return JSON.parseObject(bytes, clazz, JSONReader.Feature.SupportAutoType, JSONReader.Feature.SupportClassForName);
+        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);
     }
 }

+ 26 - 11
fs-live-app/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -1,5 +1,10 @@
 package com.fs.framework.config;
 
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
 import org.springframework.cache.annotation.CachingConfigurerSupport;
 import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.context.annotation.Bean;
@@ -30,6 +35,11 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
+        serializer.setObjectMapper(mapper);
+
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -41,37 +51,36 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
-    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Integer> template = new RedisTemplate<>();
+    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-
-        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
-        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         template.afterPropertiesSet();
         return template;
     }
     @Bean
-    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
+    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Integer> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         template.afterPropertiesSet();
         return template;
@@ -85,6 +94,11 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
+        serializer.setObjectMapper(mapper);
+
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -96,6 +110,7 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
+
     @Bean
     public RedisTemplate<String, BigDecimal> redisTemplateForBigDecimal(RedisConnectionFactory connectionFactory) {
         RedisTemplate<String, BigDecimal> template = new RedisTemplate<>();

+ 113 - 29
fs-live-app/src/main/java/com/fs/live/task/Task.java

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

+ 59 - 0
fs-live-app/src/main/java/com/fs/live/utils/WebSocketRateLimiter.java

@@ -0,0 +1,59 @@
+package com.fs.live.utils;
+
+import com.google.common.util.concurrent.RateLimiter;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * WebSocket限流工具类
+ */
+public class WebSocketRateLimiter {
+
+    // 用户级别限流:每个用户每秒最多发送5条消息
+    private static final double USER_RATE_LIMIT = 5.0;
+    private static final ConcurrentHashMap<String, RateLimiter> userLimiters = new ConcurrentHashMap<>();
+
+    // 直播间级别限流:每个直播间每秒最多处理100条消息
+    private static final double ROOM_RATE_LIMIT = 100.0;
+    private static final ConcurrentHashMap<Long, RateLimiter> roomLimiters = new ConcurrentHashMap<>();
+
+    /**
+     * 检查用户是否被限流
+     * @param userId 用户ID
+     * @param liveId 直播间ID
+     * @return true-允许通过,false-被限流
+     */
+    public static boolean tryAcquire(Long userId, Long liveId) {
+        // 检查用户级别限流
+        String userKey = userId + "_" + liveId;
+        RateLimiter userLimiter = userLimiters.computeIfAbsent(
+                userKey,
+                k -> RateLimiter.create(USER_RATE_LIMIT)
+        );
+
+        if (!userLimiter.tryAcquire()) {
+            return false;
+        }
+
+        // 检查直播间级别限流
+        RateLimiter roomLimiter = roomLimiters.computeIfAbsent(
+                liveId,
+                k -> RateLimiter.create(ROOM_RATE_LIMIT)
+        );
+
+        return roomLimiter.tryAcquire();
+    }
+
+    /**
+     * 清理用户限流器
+     */
+    public static void removeUserLimiter(Long userId, Long liveId) {
+        userLimiters.remove(userId + "_" + liveId);
+    }
+
+    /**
+     * 清理直播间限流器
+     */
+    public static void removeRoomLimiter(Long liveId) {
+        roomLimiters.remove(liveId);
+    }
+}

+ 55 - 26
fs-live-app/src/main/java/com/fs/live/websocket/auth/WebSocketConfigurator.java

@@ -28,69 +28,98 @@ public class WebSocketConfigurator extends ServerEndpointConfig.Configurator {
     @Override
     public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
         Map<String, List<String>> parameterMap = request.getParameterMap();
+
+        // 1. 先检查必要参数是否存在
         if(!parameterMap.containsKey(AttrConstant.LIVE_ID)){
-            throw new BaseException("缺少必要的参数");
+            throw new BaseException("缺少必要的参数: liveId");
         }
         if(!parameterMap.containsKey(AttrConstant.USER_ID)){
-            throw new BaseException("缺少必要的参数");
+            throw new BaseException("缺少必要的参数: userId");
         }
 
         String tokenKey = jwtUtils.getHeader();
         if (!parameterMap.containsKey(tokenKey) && !parameterMap.containsKey(AttrConstant.SIGNATURE)) {
-            throw new BaseException("缺少必要的参数");
+            throw new BaseException("缺少必要的参数: token或signature");
         }
 
-        Long liveId = Long.valueOf(parameterMap.get(AttrConstant.LIVE_ID).get(0));
-        Long userId = Long.valueOf(parameterMap.get(AttrConstant.USER_ID).get(0));
+        // 2. 安全地解析LIVE_ID和USER_ID
+        Long liveId = safeParseLong(parameterMap.get(AttrConstant.LIVE_ID).get(0), AttrConstant.LIVE_ID);
+        Long userId = safeParseLong(parameterMap.get(AttrConstant.USER_ID).get(0), AttrConstant.USER_ID);
 
         Map<String, Object> userProperties = sec.getUserProperties();
         userProperties.put(AttrConstant.LIVE_ID, liveId);
         userProperties.put(AttrConstant.USER_ID, userId);
+
+        // 3. 安全地解析可选参数
         if (parameterMap.containsKey(AttrConstant.COMPANY_ID)) {
-            userProperties.put(AttrConstant.COMPANY_ID, Long.valueOf(parameterMap.get(AttrConstant.COMPANY_ID).get(0)));
+            userProperties.put(AttrConstant.COMPANY_ID,
+                    safeParseLong(parameterMap.get(AttrConstant.COMPANY_ID).get(0), AttrConstant.COMPANY_ID));
         }
         if (parameterMap.containsKey(AttrConstant.COMPANY_USER_ID)) {
-            userProperties.put(AttrConstant.COMPANY_USER_ID, Long.valueOf(parameterMap.get(AttrConstant.COMPANY_USER_ID).get(0)));
+            userProperties.put(AttrConstant.COMPANY_USER_ID,
+                    safeParseLong(parameterMap.get(AttrConstant.COMPANY_USER_ID).get(0), AttrConstant.COMPANY_USER_ID));
         }
         if (parameterMap.containsKey(AttrConstant.LOCATION)) {
             userProperties.put(AttrConstant.LOCATION, parameterMap.get(AttrConstant.LOCATION).get(0));
         }
         if (parameterMap.containsKey(AttrConstant.QW_USER_ID)) {
-            userProperties.put(AttrConstant.QW_USER_ID, Long.valueOf(parameterMap.get(AttrConstant.QW_USER_ID).get(0)));
+            userProperties.put(AttrConstant.QW_USER_ID,
+                    safeParseLong(parameterMap.get(AttrConstant.QW_USER_ID).get(0), AttrConstant.QW_USER_ID));
         }
         if (parameterMap.containsKey(AttrConstant.EXTERNAL_CONTACT_ID)) {
-            userProperties.put(AttrConstant.EXTERNAL_CONTACT_ID, Long.valueOf(parameterMap.get(AttrConstant.EXTERNAL_CONTACT_ID).get(0)));
+            userProperties.put(AttrConstant.EXTERNAL_CONTACT_ID,
+                    safeParseLong(parameterMap.get(AttrConstant.EXTERNAL_CONTACT_ID).get(0), AttrConstant.EXTERNAL_CONTACT_ID));
         }
 
-        // 验证token
+        // 4. 验证token
         if (parameterMap.containsKey(tokenKey)) {
             String token = parameterMap.get(tokenKey).get(0);
             Claims claims = jwtUtils.getClaimByToken(token);
             if(claims == null || jwtUtils.isTokenExpired(claims.getExpiration())){
-                throw new BaseException(jwtUtils.getHeader());
+                throw new BaseException("token无效或已过期");
             }
-
             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);
+        // 5. 修复关键问题:安全地处理USER_TYPE参数
+        if (parameterMap.containsKey(AttrConstant.USER_TYPE)) {
             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("缺少必要的参数");
+                // 检查是否为undefined或null
+                if (userTypeStr == null || userTypeStr.trim().isEmpty() ||
+                        "undefined".equals(userTypeStr) || "null".equals(userTypeStr)) {
+                    log.warn("USER_TYPE参数值为空或无效: {}", userTypeStr);
+                    // 可以根据业务需求设置默认值或抛出异常
+                    userProperties.put(AttrConstant.USER_TYPE, 0L); // 设置默认值
+                } else {
+                    userProperties.put(AttrConstant.USER_TYPE, Long.parseLong(userTypeStr));
                 }
-
-                userProperties.put(AttrConstant.USER_TYPE, Long.parseLong(userTypeStr));
-            } catch (Exception e) {
-                log.warn("webSocket连接验签失败 msg: {}", e.getMessage(), e);
-                throw new BaseException("缺少必要的参数");
+            } catch (NumberFormatException e) {
+                log.error("USER_TYPE参数格式错误: {}", userTypeStr, e);
+                // 根据业务需求处理:设置默认值或抛出异常
+                userProperties.put(AttrConstant.USER_TYPE, 0L); // 设置默认值
             }
+        } else {
+            log.warn("缺少USER_TYPE参数,使用默认值");
+            userProperties.put(AttrConstant.USER_TYPE, 0L); // 设置默认值
+        }
+
+        // 验证签名(注释掉的代码保持不变)
+        // ...
+    }
+
+    /**
+     * 安全地解析Long类型参数
+     */
+    private Long safeParseLong(String value, String paramName) {
+        if (value == null || value.trim().isEmpty() ||
+                "undefined".equals(value) || "null".equals(value)) {
+            throw new BaseException("参数 " + paramName + " 的值无效: " + value);
+        }
+        try {
+            return Long.valueOf(value);
+        } catch (NumberFormatException e) {
+            throw new BaseException("参数 " + paramName + " 格式错误: " + value);
         }
     }
 

+ 63 - 0
fs-live-app/src/main/java/com/fs/live/websocket/config/WebSocketSessionManager.java

@@ -0,0 +1,63 @@
+package com.fs.live.websocket.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import javax.websocket.Session;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * 直播间WebSocket连接管理器
+ * 核心:按直播间ID分组存储Session,线程安全,低锁竞争
+ */
+@Slf4j
+@Component
+public class WebSocketSessionManager {
+
+    /**
+     * 存储结构:直播间ID -> 该直播间的所有连接Session
+     * CopyOnWriteArraySet:读多写少场景,线程安全且遍历无锁
+     */
+    private static final Map<String, Set<Session>> ROOM_SESSIONS = new ConcurrentHashMap<>();
+
+    /**
+     * 添加连接(绑定直播间)
+     */
+    public void addSession(String roomId, Session session) {
+        // 不存在则创建空集合(ConcurrentHashMap的原子操作)
+        ROOM_SESSIONS.computeIfAbsent(roomId, k -> new CopyOnWriteArraySet<>()).add(session);
+        log.info("直播间[{}]新增连接,当前连接数:{}", roomId, ROOM_SESSIONS.get(roomId).size());
+    }
+
+    /**
+     * 移除连接(清理无效连接)
+     */
+    public void removeSession(String roomId, Session session) {
+        if (ROOM_SESSIONS.containsKey(roomId)) {
+            Set<Session> sessions = ROOM_SESSIONS.get(roomId);
+            sessions.remove(session);
+            // 直播间无连接时清空,释放内存
+            if (sessions.isEmpty()) {
+                ROOM_SESSIONS.remove(roomId);
+            }
+            log.info("直播间[{}]移除连接,当前连接数:{}", roomId, sessions.size());
+        }
+    }
+
+    /**
+     * 获取直播间所有连接
+     */
+    public Set<Session> getRoomSessions(String roomId) {
+        return ROOM_SESSIONS.getOrDefault(roomId, new CopyOnWriteArraySet<>());
+    }
+
+    /**
+     * 获取所有直播间ID
+     */
+    public Set<String> getAllRoomIds() {
+        return ROOM_SESSIONS.keySet();
+    }
+}

+ 501 - 97
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -36,11 +36,15 @@ import javax.websocket.*;
 import javax.websocket.server.ServerEndpoint;
 import java.io.EOFException;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
 import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 
@@ -68,6 +72,22 @@ public class WebSocketServer {
     // admin房间消息发送线程池(单线程,保证串行化)
     private final static ConcurrentHashMap<Long, ExecutorService> adminExecutors = new ConcurrentHashMap<>();
 
+    // 消息队列系统
+    // 每个直播间的消息队列,使用优先级队列支持管理员消息插队
+    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 = 10000;
+    // 消息队列最大大小:200MB
+    private final static long MAX_QUEUE_SIZE_BYTES = 200L * 1024L * 1024L; // 200MB
+    // 上下线消息采样率:10%
+    private final static double ENTRY_EXIT_SAMPLE_RATE = 0.1;
+
     private final RedisCache redisCache = SpringUtils.getBean(RedisCache.class);
     private final ILiveMsgService liveMsgService = SpringUtils.getBean(ILiveMsgService.class);
     private final ILiveService liveService = SpringUtils.getBean(ILiveService.class);
@@ -87,7 +107,7 @@ public class WebSocketServer {
     private static Random random = new Random();
 
     // Redis key 前缀:用户进入直播间时间
-    private static final String USER_ENTRY_TIME_KEY = "live:user:entry:time:%s:%s"; // liveId:userId
+    public static final String USER_ENTRY_TIME_KEY = "live:user:entry:time:%s:%s"; // liveId:userId
 
     // 直播间在线用户缓存
 //    private static final ConcurrentHashMap<Long, Integer> liveOnlineUsers = new ConcurrentHashMap<>();
@@ -109,7 +129,7 @@ public class WebSocketServer {
         if (live == null) {
             throw new BaseException("未找到直播间");
         }
-        long companyId = live.getCompanyId() == null ? -1L : live.getCompanyId();
+        long companyId = -1L;
         long companyUserId = -1L;
         if (!Objects.isNull(userProperties.get("companyId"))) {
             companyId = (long) userProperties.get("companyId");
@@ -130,7 +150,15 @@ public class WebSocketServer {
 
         // 记录连接信息 管理员不记录
         if (userType == 0) {
-            FsUserScrm fsUser = fsUserService.selectFsUserById(userId);
+            // 缓存用户信息,过期时间4小时
+            String userCacheKey = "fs:user:" + userId;
+            FsUserScrm fsUser = redisCache.getCacheObject(userCacheKey);
+            if (fsUser == null) {
+                fsUser = fsUserService.selectFsUserById(userId);
+                if (fsUser != null) {
+                    redisCache.setCacheObject(userCacheKey, fsUser, 4, TimeUnit.HOURS);
+                }
+            }
             if (Objects.isNull(fsUser)) {
                 throw new BaseException("用户信息错误");
             }
@@ -180,7 +208,8 @@ public class WebSocketServer {
                 redisCache.incr(UNIQUE_VIEWERS_KEY + liveId, 1);
             }
             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);
@@ -190,11 +219,19 @@ public class WebSocketServer {
                 sendMsgVo.setData(JSONObject.toJSONString(liveWatchUserVO));
                 sendMsgVo.setNickName(fsUser.getNickname());
                 sendMsgVo.setAvatar(fsUser.getAvatar());
-                // 广播连接消息
-                broadcastWebMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+                // 将上线消息加入队列
+                enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)), false);
             }
 
-            LiveUserFirstEntry liveUserFirstEntry = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveId, userId);
+            // 缓存用户首次进入记录,过期时间4小时
+            String liveUserFirstEntryCacheKey = "live:userFirstEntry:" + liveId + ":" + userId;
+            LiveUserFirstEntry liveUserFirstEntry = redisCache.getCacheObject(liveUserFirstEntryCacheKey);
+            if (liveUserFirstEntry == null) {
+                liveUserFirstEntry = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveId, userId);
+                if (liveUserFirstEntry != null) {
+                    redisCache.setCacheObject(liveUserFirstEntryCacheKey, liveUserFirstEntry, 4, TimeUnit.HOURS);
+                }
+            }
             // 如果用户连上了 socket,并且公司ID和销售ID大于0,更新 LiveWatchLog 的 logType
 
             if ((qwUserId > 0 && externalContactId > 0) || (liveUserFirstEntry != null && liveUserFirstEntry.getCompanyId() > 0 && liveUserFirstEntry.getCompanyUserId() > 0 )) {
@@ -224,10 +261,20 @@ public class WebSocketServer {
                 }
             } else {
                 // 这个用户A邀请用户b,b的业绩算a的销售的
-                if (companyUserId == -2L) {
-                    LiveUserFirstEntry clientB = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveId, companyUserId);
-                    companyId = clientB.getCompanyId();
-                    companyUserId = clientB.getCompanyUserId();
+                if (companyId == -2L) {
+                    // 缓存用户首次进入记录,过期时间4小时
+                    String clientBCacheKey = "live:userFirstEntry:" + liveId + ":" + companyUserId;
+                    LiveUserFirstEntry clientB = redisCache.getCacheObject(clientBCacheKey);
+                    if (clientB == null) {
+                        clientB = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveId, companyUserId);
+                        if (clientB != null) {
+                            redisCache.setCacheObject(clientBCacheKey, clientB, 4, TimeUnit.HOURS);
+                        }
+                    }
+                    if (clientB != null) {
+                        companyId = clientB.getCompanyId();
+                        companyUserId = clientB.getCompanyUserId();
+                    }
                 }
                 Date date = new Date();
                 liveUserFirstEntry = new LiveUserFirstEntry();
@@ -246,10 +293,10 @@ public class WebSocketServer {
                 }
                 liveUserFirstEntryService.insertLiveUserFirstEntry(liveUserFirstEntry);
             }
-            redisCache.setCacheObject( "live:user:first:entry:" + liveId + ":" + userId, liveUserFirstEntry,1, TimeUnit.HOURS);
+            redisCache.setCacheObject( "live:user:first:entry:" + liveId + ":" + userId, liveUserFirstEntry, 4, TimeUnit.HOURS);
 
             // 推送完课积分倒计时配置信息给前端
-            sendCompletionPointsConfigToUser(session, liveId, userId, live);
+//            sendCompletionPointsConfigToUser(session, liveId, userId, live);
 
 
         } else {
@@ -263,6 +310,15 @@ public class WebSocketServer {
         // 初始化心跳时间
         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);
+        }
+
     }
 
     //关闭连接时调用
@@ -286,7 +342,15 @@ public class WebSocketServer {
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
         List<Session> adminRoom = getAdminRoom(liveId);
         if (userType == 0) {
-            FsUserScrm fsUser = fsUserService.selectFsUserById(userId);
+            // 缓存用户信息,过期时间4小时
+            String userCacheKey = "fs:user:" + userId;
+            FsUserScrm fsUser = redisCache.getCacheObject(userCacheKey);
+            if (fsUser == null) {
+                fsUser = fsUserService.selectFsUserById(userId);
+                if (fsUser != null) {
+                    redisCache.setCacheObject(userCacheKey, fsUser, 4, TimeUnit.HOURS);
+                }
+            }
             if (Objects.isNull(fsUser)) {
                 throw new BaseException("用户信息错误");
             }
@@ -306,8 +370,8 @@ public class WebSocketServer {
             LiveWatchUser liveWatchUserVO = liveWatchUserService.close(fsUser,liveId, userId);
 
 
-            // 广播离开消息 添加一个概率问题 摇塞子,1-4 当为1的时候广播消息
-            if (1 == new Random().nextInt(10)) {
+            // 下线消息采样10%进入队列
+            if (random.nextDouble() < ENTRY_EXIT_SAMPLE_RATE) {
                 SendMsgVo sendMsgVo = new SendMsgVo();
                 sendMsgVo.setLiveId(liveId);
                 sendMsgVo.setUserId(userId);
@@ -317,7 +381,8 @@ public class WebSocketServer {
                 sendMsgVo.setData(JSONObject.toJSONString(liveWatchUserVO));
                 sendMsgVo.setNickName(fsUser.getNickname());
                 sendMsgVo.setAvatar(fsUser.getAvatar());
-                broadcastWebMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+                // 将下线消息加入队列
+                enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)), false);
             }
 
         } else {
@@ -335,6 +400,9 @@ public class WebSocketServer {
         // 清理Session相关资源
         heartbeatCache.remove(session.getId());
         sessionLocks.remove(session.getId());
+
+        // 检查并清理空的直播间资源
+        cleanupEmptyRoom(liveId);
     }
 
     //收到客户端信息
@@ -344,6 +412,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;
@@ -359,61 +428,72 @@ public class WebSocketServer {
 
 
 
-                    if (msg.getData() != null && !msg.getData().isEmpty()) {
-                        try {
-                            Long currentDuration = Long.parseLong(msg.getData());
-
-                            Live currentLive = liveService.selectLiveByLiveId(liveId);
-                            if (currentLive == null) {
-                                break;
-                            }
-
-
-                            // 判断直播是否已开始:status=2(直播中) 或 当前时间 >= 开播时间
-                            boolean isLiveStarted = false;
-                            if (currentLive.getStatus() != null && currentLive.getStatus() == 2) {
-                                // status=2 表示直播中
-                                isLiveStarted = true;
-                            } else if (currentLive.getStartTime() != null) {
-                                // 判断当前时间是否已超过开播时间
-                                LocalDateTime now = LocalDateTime.now();
-                                isLiveStarted = now.isAfter(currentLive.getStartTime()) || now.isEqual(currentLive.getStartTime());
-                            }
-
-                            // 使用Hash结构存储:一个直播间一个Hash,包含所有用户的时长
-                            String hashKey = "live:watch:duration:hash:" + liveId;
-                            String userIdField = String.valueOf(watchUserId);
-
-                            if (!isLiveStarted) {
-                                redisCache.hashDelete(hashKey, userIdField);
-                                log.debug("[心跳-观看时长] 直播未开始,清除预播时长, liveId={}, userId={}", liveId, watchUserId);
-                                break;
-                            }
-
-                            // 直播已开始,记录观看时长
-                            // 获取现有时长
-                            Object existingDuration = redisCache.hashGet(hashKey, userIdField);
-                            // 只有当新的时长更大时才更新
-                            if (existingDuration == null || currentDuration > Long.parseLong(existingDuration.toString())) {
-                                // 更新Hash中的用户时长
-                                redisCache.hashPut(hashKey, userIdField, currentDuration.toString());
-                                // 设置过期时间(2小时)
-                                redisCache.expire(hashKey, 2, TimeUnit.HOURS);
-
-                                checkAndSendCompletionPointsInRealTime(liveId, watchUserId, currentDuration);
-
-                            }
-                        } catch (Exception e) {
-                            log.error("[心跳-观看时长] 更新失败, liveId={}, userId={}, data={}",
-                                    liveId, watchUserId, msg.getData(), e);
-                        }
-                    }
+//                    if (msg.getData() != null && !msg.getData().isEmpty()) {
+//                        try {
+//                            Long currentDuration = Long.parseLong(msg.getData());
+//
+//                            Live currentLive = liveService.selectLiveByLiveId(liveId);
+//                            if (currentLive == null) {
+//                                break;
+//                            }
+//
+//
+//                            // 判断直播是否已开始:status=2(直播中) 或 当前时间 >= 开播时间
+//                            boolean isLiveStarted = false;
+//                            if (currentLive.getStatus() != null && currentLive.getStatus() == 2) {
+//                                // status=2 表示直播中
+//                                isLiveStarted = true;
+//                            } else if (currentLive.getStartTime() != null) {
+//                                // 判断当前时间是否已超过开播时间
+//                                LocalDateTime now = LocalDateTime.now();
+//                                isLiveStarted = now.isAfter(currentLive.getStartTime()) || now.isEqual(currentLive.getStartTime());
+//                            }
+//
+//                            // 使用Hash结构存储:一个直播间一个Hash,包含所有用户的时长
+//                            String hashKey = "live:watch:duration:hash:" + liveId;
+//                            String userIdField = String.valueOf(watchUserId);
+//
+//                            if (!isLiveStarted) {
+//                                redisCache.hashDelete(hashKey, userIdField);
+//                                log.debug("[心跳-观看时长] 直播未开始,清除预播时长, liveId={}, userId={}", liveId, watchUserId);
+//                                break;
+//                            }
+//
+//                            // 直播已开始,记录观看时长
+//                            // 获取现有时长
+//                            Object existingDuration = redisCache.hashGet(hashKey, userIdField);
+//                            // 只有当新的时长更大时才更新
+//                            if (existingDuration == null || currentDuration > Long.parseLong(existingDuration.toString())) {
+//                                // 更新Hash中的用户时长
+//                                redisCache.hashPut(hashKey, userIdField, currentDuration.toString());
+//                                // 设置过期时间(2小时)
+//                                redisCache.expire(hashKey, 2, TimeUnit.HOURS);
+//
+//                                checkAndSendCompletionPointsInRealTime(liveId, watchUserId, currentDuration);
+//
+//                            }
+//                        } catch (Exception e) {
+//                            log.error("[心跳-观看时长] 更新失败, liveId={}, userId={}, data={}",
+//                                    liveId, watchUserId, msg.getData(), e);
+//                        }
+//                    }
 
                     sendMessage(session, JSONObject.toJSONString(R.ok().put("data", msg)));
                     break;
                 case "sendMsg":
+                    // 参数校验
+                    if (liveMsgService == null) {
+                        log.error("[WebSocket-sendMsg] liveMsgService为null, liveId={}, userId={}", liveId, msg.getUserId());
+                        sendMessage(session, JSONObject.toJSONString(R.error("系统服务异常")));
+                        return;
+                    }
+
                     msg.setMsg(productionWordFilter.filter(msg.getMsg()).getFilteredText());
-                    if(StringUtils.isEmpty(msg.getMsg())) return;
+                    if(StringUtils.isEmpty(msg.getMsg())) {
+                        log.debug("[WebSocket-sendMsg] 消息内容为空, liveId={}, userId={}", liveId, msg.getUserId());
+                        return;
+                    }
+
                     liveMsg = new LiveMsg();
                     liveMsg.setLiveId(msg.getLiveId());
                     liveMsg.setUserId(msg.getUserId());
@@ -423,27 +503,48 @@ 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){
-                            sendMessage(session, JSONObject.toJSONString(R.error("你已被禁言")));
-                            return;
+                        try {
+                            List<LiveWatchUser> liveWatchUser = liveWatchUserService.getByLiveIdAndUserId(msg.getLiveId(), msg.getUserId());
+                            if(!liveWatchUser.isEmpty() && liveWatchUser.get(0).getMsgStatus() == 1){
+                                sendMessage(session, JSONObject.toJSONString(R.error("你已被禁言")));
+                                return;
+                            }
+                        } catch (Exception e) {
+                            log.error("[WebSocket-sendMsg] 检查禁言状态失败, liveId={}, userId={}, error={}",
+                                    liveId, msg.getUserId(), e.getMessage(), e);
                         }
 
-                        Map<String, Integer> flagMap = liveWatchUserService.getLiveFlagWithCache(liveId);
-                        Integer liveFlag = flagMap.get("liveFlag");
-                        Integer replayFlag = flagMap.get("replayFlag");
-                        liveMsg.setLiveFlag(liveFlag);
-                        liveMsg.setReplayFlag(replayFlag);
+                        try {
+                            Map<String, Integer> flagMap = liveWatchUserService.getLiveFlagWithCache(liveId);
+                            Integer liveFlag = flagMap.get("liveFlag");
+                            Integer replayFlag = flagMap.get("replayFlag");
+                            liveMsg.setLiveFlag(liveFlag);
+                            liveMsg.setReplayFlag(replayFlag);
+                        } catch (Exception e) {
+                            log.error("[WebSocket-sendMsg] 获取直播标志失败, liveId={}, error={}", liveId, e.getMessage(), e);
+                            // 设置默认值
+                            liveMsg.setLiveFlag(1);
+                            liveMsg.setReplayFlag(0);
+                        }
 
-                        liveMsgService.insertLiveMsg(liveMsg);
+                        try {
+                            liveMsgService.insertLiveMsg(liveMsg);
+                            log.debug("[WebSocket-sendMsg] 消息插入成功, liveId={}, userId={}, msgId={}",
+                                    liveId, msg.getUserId(), liveMsg.getMsgId());
+                        } catch (Exception e) {
+                            log.error("[WebSocket-sendMsg] 消息插入失败, liveId={}, userId={}, error={}",
+                                    liveId, msg.getUserId(), e.getMessage(), e);
+                        }
                     }
 
                     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 "sendNormalMsg":
                     msg.setMsg(productionWordFilter.filter(msg.getMsg()).getFilteredText());
                     if(StringUtils.isEmpty(msg.getMsg())) return;
@@ -475,8 +576,7 @@ public class WebSocketServer {
                     msg.setOn(true);
                     msg.setData(JSONObject.toJSONString(liveMsg));
                     msg.setCmd("sendMsg");
-                    // 广播消息
-                    broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)));
+                    enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)), true);
                     break;
                 case "sendPopMsg":
                     msg.setMsg(productionWordFilter.filter(msg.getMsg()).getFilteredText());
@@ -489,8 +589,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());
@@ -504,8 +603,7 @@ public class WebSocketServer {
                     liveMsg.setEndTime(DateUtils.addMinutes(new Date(),msg.getDuration()).toString());
                     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));
@@ -515,13 +613,13 @@ public class WebSocketServer {
                     msg.setOn(true);
                     liveWatchUserService.updateGlobalVisible(liveId, msg.getStatus());
                     liveService.updateGlobalVisible(liveId, msg.getStatus());
-                    // 广播消息
-                    broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)));
+                    // 管理员消息插队
+                    enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)), true);
                     break;
                 case "singleVisible":
                     liveWatchUserService.updateSingleVisible(liveId, msg.getStatus(),msg.getUserId());
-                    // 广播消息
-                    broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)));
+                    // 管理员消息插队
+                    enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)), true);
                     break;
                 case "sendGift":
                     break;
@@ -560,7 +658,8 @@ 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) {
@@ -580,7 +679,8 @@ 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);
     }
 
 
@@ -595,7 +695,8 @@ 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);
     }
 
     /**
@@ -609,7 +710,8 @@ 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);
         }
     }
 
@@ -624,7 +726,8 @@ 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);
         }
     }
 
@@ -713,7 +816,11 @@ public class WebSocketServer {
         if (session == null || !session.isOpen()) {
             return;
         }
-        session.getAsyncRemote().sendText(JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+        try {
+            sendMessage(session, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+        } catch (IOException e) {
+            log.error(e.getMessage());
+        }
     }
 
     private void sendBlockMessage(Long liveId, Long userId) {
@@ -1102,9 +1209,14 @@ public class WebSocketServer {
 //                    msg.setData(JSON.toJSONString(liveGoodsVo));
 //                    msg.setStatus(status);
 //                }
+            }else if (task.getTaskType() == 7L) {
+                // 签到
+                msg.setCmd("sign");
+                msg.setData(JSON.toJSONString(task.getContent()));
             }
             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());
         }
@@ -1191,7 +1303,7 @@ public class WebSocketServer {
             queryLog.setLiveId(liveId);
             queryLog.setQwUserId(String.valueOf(qwUserId));
             queryLog.setExternalContactId(externalContactId);
-
+            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
             List<LiveWatchLog> logs = liveWatchLogService.selectLiveWatchLogList(queryLog);
             if (logs != null && !logs.isEmpty()) {
                 for (LiveWatchLog log : logs) {
@@ -1199,6 +1311,9 @@ public class WebSocketServer {
                     if (log.getLogType() == null || log.getLogType() != 2) {
                         log.setLogType(1);
                         liveWatchLogService.updateLiveWatchLog(log);
+                        String liveUserWatchLogKey = String.format(LIVE_USER_WATCH_LOG_CACHE, liveId, userId,externalContactId,qwUserId);
+                        LocalDateTime now = LocalDateTime.now();
+                        redisCache.setCacheObject(liveUserWatchLogKey,formatter.format(now),5,TimeUnit.MINUTES);
                     }
                 }
             }
@@ -1487,5 +1602,294 @@ 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) {
+                            Thread.currentThread().interrupt();
+                            log.info("[消息队列] 消费者线程被中断, liveId={}", liveId);
+                            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 是否成功加入队列
+     */
+    private 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);
+        }
+    }
+
 }
 

+ 330 - 0
fs-live-app/src/main/java/com/fs/live/websocket/test/WebSocketTestClient.java

@@ -0,0 +1,330 @@
+package com.fs.live.websocket.test;
+
+import javax.websocket.*;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Scanner;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * WebSocket 测试客户端
+ * 使用 @ClientEndpoint 注解方式
+ */
+@ClientEndpoint
+public class WebSocketTestClient {
+
+    private Session session;
+    private CountDownLatch closeLatch = new CountDownLatch(1);
+    private boolean connected = false;
+    private Throwable connectionError = null;
+
+    /**
+     * 连接成功回调
+     */
+    @OnOpen
+    public void onOpen(Session session) {
+        this.session = session;
+        this.connected = true;
+
+        System.out.println("\n✅ WebSocket 连接成功!");
+        System.out.println("Session ID: " + session.getId());
+        System.out.println("服务器地址: " + session.getRequestURI());
+        System.out.println("请求参数: " + session.getRequestParameterMap());
+        System.out.println("\n等待消息...");
+    }
+
+    /**
+     * 收到消息回调
+     */
+    @OnMessage
+    public void onMessage(String message) {
+        System.out.println("\n📨 收到消息: ");
+        System.out.println(message);
+        System.out.print("> ");
+    }
+
+    /**
+     * 连接关闭回调
+     */
+    @OnClose
+    public void onClose(CloseReason closeReason) {
+        System.out.println("\n❌ WebSocket 连接关闭");
+        System.out.println("关闭原因: " + closeReason.getReasonPhrase());
+        System.out.println("关闭代码: " + closeReason.getCloseCode());
+        this.connected = false;
+        closeLatch.countDown();
+    }
+
+    /**
+     * 错误回调
+     */
+    @OnError
+    public void onError(Throwable throwable) {
+        System.err.println("\n⚠️ WebSocket 错误:");
+        this.connectionError = throwable;
+        throwable.printStackTrace();
+    }
+
+    /**
+     * 发送消息
+     */
+    public void sendMessage(String message) {
+        if (session != null && session.isOpen()) {
+            try {
+                session.getBasicRemote().sendText(message);
+                System.out.println("\n📤 发送消息: " + message);
+            } catch (IOException e) {
+                System.err.println("发送消息失败: " + e.getMessage());
+                e.printStackTrace();
+            }
+        } else {
+            System.err.println("连接未打开或已关闭");
+        }
+    }
+
+    /**
+     * 关闭连接
+     */
+    public void close() {
+        if (session != null && session.isOpen()) {
+            try {
+                session.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "测试结束"));
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    /**
+     * 检查是否已连接
+     */
+    public boolean isConnected() {
+        return connected && session != null && session.isOpen();
+    }
+
+    /**
+     * 获取连接错误
+     */
+    public Throwable getConnectionError() {
+        return connectionError;
+    }
+
+    /**
+     * 等待连接关闭
+     */
+    public boolean awaitClose(long timeout, TimeUnit unit) throws InterruptedException {
+        return closeLatch.await(timeout, unit);
+    }
+
+    public static void main(String[] args) {
+        WebSocketContainer container = null;
+        WebSocketTestClient client = null;
+
+        try {
+            // ==================== 配置区域 ====================
+            String host = "localhost";
+            int port = 7114;
+
+            // 直播间和用户配置
+            long liveId = 27;        // ⚠️ 修改为实际存在的直播间ID
+            long userId = 856648;   // Token 中的用户ID(sub字段)
+            long userType = 0;      // 0=普通用户, 1=管理员
+
+            // JWT Token(从 Postman 生成)
+            String jwtToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI4NTY2NDgiLCJpYXQiOjE3NzU5NTc2OTEsImV4cCI6MTgwNzQ5MzY5MX0.keB7XqAQcGhpsKjKbaIhmFhxf2443jHUBRvRSaLjqi-igUaPGA37dUBLJ65R89h6g-7pGr-4Cn99GhvnmkpdpQ";
+            String jwtHeader = "AppToken";  // JWT header 参数名
+
+            // 可选参数
+            Long companyId = -1L;
+            Long companyUserId = -1L;
+            String location = "";
+            // ================================================
+
+            // 构建 WebSocket URL
+            StringBuilder urlBuilder = new StringBuilder();
+            urlBuilder.append(String.format("ws://%s:%d/ws/app/webSocket?liveId=%d&userId=%d&userType=%d",
+                    host, port, liveId, userId, userType));
+
+            // 添加 JWT Token
+            urlBuilder.append("&").append(jwtHeader).append("=").append(jwtToken);
+
+            // 添加可选参数
+            if (location != null && !location.isEmpty()) {
+                urlBuilder.append("&location=").append(location);
+            }
+            if (companyId != null && companyId != -1L) {
+                urlBuilder.append("&companyId=").append(companyId);
+            }
+            if (companyUserId != null && companyUserId != -1L) {
+                urlBuilder.append("&companyUserId=").append(companyUserId);
+            }
+
+            String wsUrl = urlBuilder.toString();
+
+            System.out.println("===========================================");
+            System.out.println("WebSocket 连接测试");
+            System.out.println("===========================================");
+            System.out.println("测试时间: " + new java.util.Date());
+            System.out.println("连接地址: " + wsUrl);
+            System.out.println("用户ID: " + userId);
+            System.out.println("直播间ID: " + liveId);
+            System.out.println("用户类型: " + (userType == 0 ? "普通用户" : "管理员"));
+            System.out.println("JWT Header: " + jwtHeader);
+            System.out.println("JWT Token: " + jwtToken.substring(0, Math.min(50, jwtToken.length())) + "...");
+            System.out.println("===========================================\n");
+
+            // 创建 WebSocket 容器
+            container = ContainerProvider.getWebSocketContainer();
+
+            // 设置容器配置
+            container.setDefaultMaxSessionIdleTimeout(60000);
+            container.setDefaultMaxBinaryMessageBufferSize(1024 * 1024);
+            container.setDefaultMaxTextMessageBufferSize(1024 * 1024);
+
+            System.out.println("✓ WebSocket 容器创建成功");
+
+            // 创建客户端实例
+            client = new WebSocketTestClient();
+            System.out.println("✓ 客户端实例创建成功\n");
+
+            System.out.println("正在连接到: " + wsUrl);
+            System.out.println("请稍候...\n");
+
+            // 连接到 WebSocket 服务器
+            long startTime = System.currentTimeMillis();
+            try {
+                container.connectToServer(client, new URI(wsUrl));
+                long connectTime = System.currentTimeMillis() - startTime;
+                System.out.println("✓ 连接请求已发送 (耗时: " + connectTime + "ms)");
+            } catch (Exception e) {
+                long failTime = System.currentTimeMillis() - startTime;
+                System.err.println("✗ 连接请求失败 (耗时: " + failTime + "ms)");
+                throw e;
+            }
+
+            // 等待连接建立(最多等待5秒)
+            System.out.println("等待服务器响应...");
+            for (int i = 0; i < 50; i++) {
+                Thread.sleep(100);
+                if (client.isConnected()) {
+                    break;
+                }
+                if (i % 10 == 0 && i > 0) {
+                    System.out.print(".");
+                }
+            }
+            System.out.println();
+
+            // 检查连接状态
+            if (!client.isConnected()) {
+                System.err.println("\n❌ 连接未能建立!");
+                if (client.getConnectionError() != null) {
+                    System.err.println("连接错误:");
+                    client.getConnectionError().printStackTrace();
+                }
+                System.err.println("\n请检查:");
+                System.err.println("1. fs-live-app 服务是否已启动?");
+                System.err.println("2. 服务是否运行在端口 " + port + "?");
+                System.err.println("3. 数据库中是否存在 live_id=" + liveId + " 的直播间?");
+                System.err.println("4. 数据库中是否存在 user_id=" + userId + " 的用户?");
+                return;
+            }
+
+            Scanner scanner = new Scanner(System.in);
+            System.out.println("\n✅ 连接成功!请输入要发送的消息:");
+            System.out.println("   - 输入 'heartbeat' 发送心跳消息");
+            System.out.println("   - 输入其他文本发送聊天消息");
+            System.out.println("   - 输入 'quit' 或 'exit' 退出\n");
+
+            while (true) {
+                System.out.print("> ");
+                String input = scanner.nextLine();
+
+                if ("quit".equalsIgnoreCase(input) || "exit".equalsIgnoreCase(input)) {
+                    break;
+                }
+
+                if (input.trim().isEmpty()) {
+                    continue;
+                }
+
+                if ("heartbeat".equalsIgnoreCase(input)) {
+                    String heartbeatMsg = String.format(
+                            "{\"cmd\":\"heartbeat\",\"liveId\":%d,\"userId\":%d,\"userType\":%d}",
+                            liveId, userId, userType
+                    );
+                    client.sendMessage(heartbeatMsg);
+                } else {
+                    String chatMsg = String.format(
+                            "{\"cmd\":\"sendMsg\",\"liveId\":%d,\"userId\":%d,\"userType\":%d,\"nickName\":\"测试用户\",\"avatar\":\"\",\"msg\":\"%s\"}",
+                            liveId, userId, userType, input
+                    );
+                    client.sendMessage(chatMsg);
+                }
+            }
+
+            scanner.close();
+
+        } catch (Exception e) {
+            System.err.println("\n❌ 连接失败: " + e.getClass().getSimpleName() + ": " + e.getMessage());
+
+            // 打印完整堆栈
+            System.err.println("\n📋 完整异常堆栈:");
+            e.printStackTrace();
+
+            // 分析错误原因
+            System.err.println("\n💡 错误分析:");
+            System.err.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
+
+            String errorMsg = e.getMessage();
+            if (errorMsg != null) {
+                if (errorMsg.contains("500")) {
+                    System.err.println("🔴 HTTP 500 错误 - 服务器内部错误");
+                    System.err.println("   可能原因:");
+                    System.err.println("   1. 服务器端握手处理抛出异常");
+                    System.err.println("   2. 数据库中不存在对应的 liveId 或 userId");
+                    System.err.println("   3. JWT Token 验证失败");
+                    System.err.println("   4. Spring Bean 注入失败");
+                    System.err.println();
+                    System.err.println("   ✅ 解决方案:");
+                    System.err.println("   1. 查看 fs-live-app 的控制台输出");
+                    System.err.println("   2. 查看日志文件: fs-live-app/logs/");
+                    System.err.println("   3. 确认 liveId=27 是否存在于数据库");
+                    System.err.println("   4. 确认 userId=856648 是否存在于数据库");
+
+                } else if (errorMsg.contains("404")) {
+                    System.err.println("🔴 HTTP 404 错误 - 路径不存在");
+                    System.err.println("   可能原因: WebSocket 端点路径错误");
+                    System.err.println("   正确路径: /ws/app/webSocket");
+
+                } else if (errorMsg.contains("401") || errorMsg.contains("403")) {
+                    System.err.println("🔴 HTTP " + (errorMsg.contains("401") ? "401" : "403") + " 错误 - 认证失败");
+                    System.err.println("   可能原因: JWT Token 无效或过期");
+                    System.err.println("   解决方案: 重新生成 Token");
+
+                } else if (errorMsg.contains("Connection refused") || errorMsg.contains("ConnectException")) {
+                    System.err.println("🔴 连接被拒绝");
+                    System.err.println("   可能原因: 服务未启动或端口错误");
+                    System.err.println("   解决方案:");
+                    System.err.println("   1. 启动 fs-live-app 服务");
+                    System.err.println("   2. 确认服务运行在端口 7114");
+                    System.err.println("   3. 访问 http://localhost:7114 测试");
+
+                } else if (errorMsg.contains("timeout") || errorMsg.contains("Timeout")) {
+                    System.err.println("🔴 连接超时");
+                    System.err.println("   可能原因: 网络问题或服务器无响应");
+                    System.err.println("   解决方案: 检查网络连接和服务器状态");
+                }
+            }
+
+            System.err.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
+
+        } finally {
+            if (client != null) {
+                client.close();
+            }
+            System.out.println("\n测试结束");
+        }
+    }
+}

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

@@ -0,0 +1,138 @@
+<?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>
+    <description>
+       直播消费
+    </description>
+    <dependencies>
+
+        <!-- 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>
+
+
+        <!-- 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.github.penggle</groupId>
+            <artifactId>kaptcha</artifactId>
+            <exclusions>
+                <exclusion>
+                    <artifactId>javax.servlet-api</artifactId>
+                    <groupId>javax.servlet</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <!-- 获取系统信息 -->
+        <dependency>
+            <groupId>com.github.oshi</groupId>
+            <artifactId>oshi-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fs</groupId>
+            <artifactId>fs-service</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.tencentyun</groupId>
+            <artifactId>tls-sig-api-v2</artifactId>
+            <version>2.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-websocket</artifactId>
+            <version>5.1.10.RELEASE</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </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>
+        </plugins>
+        <finalName>${project.artifactId}</finalName>
+    </build>
+
+</project>

+ 14 - 0
fs-live-mq/src/main/java/com/fs/FSServletInitializer.java

@@ -0,0 +1,14 @@
+package com.fs;
+
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+
+
+public class FSServletInitializer extends SpringBootServletInitializer
+{
+    @Override
+    protected SpringApplicationBuilder configure(SpringApplicationBuilder application)
+    {
+        return application.sources(FsLiveMqApplication.class);
+    }
+}

+ 27 - 0
fs-live-mq/src/main/java/com/fs/FsLiveMqApplication.java

@@ -0,0 +1,27 @@
+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.context.annotation.Import;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+/**
+ * 启动程序
+ */
+@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
+@Import({ RocketMQAutoConfiguration.class })
+@EnableTransactionManagement
+@EnableAsync
+@EnableScheduling
+public class FsLiveMqApplication
+{
+    public static void main(String[] args)
+    {
+        SpringApplication.run(FsLiveMqApplication.class, args);
+        System.out.println("live-mq启动成功");
+    }
+}

+ 12 - 0
fs-live-mq/src/main/java/com/fs/app/annotation/Login.java

@@ -0,0 +1,12 @@
+package com.fs.app.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * app登录效验
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Login {
+}

+ 15 - 0
fs-live-mq/src/main/java/com/fs/app/annotation/LoginUser.java

@@ -0,0 +1,15 @@
+package com.fs.app.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 登录用户信息
+ */
+@Target(ElementType.PARAMETER)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface LoginUser {
+
+}

+ 125 - 0
fs-live-mq/src/main/java/com/fs/app/controller/AdCallbackController.java

@@ -0,0 +1,125 @@
+package com.fs.app.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fs.ad.domain.AdHtmlTemplate;
+import com.fs.ad.domain.AdSite;
+import com.fs.ad.mapper.AdHtmlTemplateMapper;
+import com.fs.ad.service.IAdAccountService;
+import com.fs.ad.service.IAdHtmlClickLogService;
+import com.fs.ad.service.IAdSiteService;
+import com.fs.baidu.api.BaiduApis;
+import com.fs.baidu.service.IBdAccountService;
+import com.fs.baidu.vo.ad.AdBaiduClickCallbackVo;
+import com.fs.baidu.vo.ad.AdDyClickCallbackVo;
+import com.fs.baidu.vo.ad.AdIqiyiClickCallbackVo;
+import com.fs.baidu.vo.ad.AdYouKuClickCallbackVo;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.R;
+import com.fs.qw.domain.QwWorkLink;
+import com.fs.qw.service.IQwWorkLinkService;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 知识库Controller
+ *
+ * @author fs
+ * @date 2024-04-21
+ */
+@Slf4j
+@AllArgsConstructor
+@RestController
+@RequestMapping("/baidu")
+public class AdCallbackController extends BaseController {
+
+    private final AdHtmlTemplateMapper adHtmlTemplateMapper;
+    private final IAdHtmlClickLogService adHtmlClickLogService;
+    private final BaiduApis baiduApis;
+    private final IBdAccountService bdAccountService;
+    private final IAdSiteService adSiteService;
+    private final IAdAccountService adAccountService;
+    private final IQwWorkLinkService qwWorkLinkService;
+
+    //百度-页面点击接口
+//    @GetMapping("/callback")
+//    public R callback(String url, String bdVid, String t, Long id) {
+//        if(id == null || StringUtils.isEmpty(bdVid)) {
+//            return R.ok();
+//        }
+//        adHtmlClickLogService.addLog(url, id, bdVid, t);
+//        return R.ok();
+//    }
+    // 百度点击监听返回接口
+//    @GetMapping("/clickCallback")
+//    public R clickCallback(AdClickCallbackVo vo){
+//        log.info("百度监听地址返回数据:{}", JSON.toJSONString(vo));
+//        adHtmlClickLogService.setLogBaiDu(vo, "67", 0);
+//        return R.ok();
+//    }
+    @GetMapping("/getTemplateByNo")
+    public R getTemplateByNo(String no){
+        AdHtmlTemplate htmlUrl = adHtmlTemplateMapper.selectOne(new QueryWrapper<AdHtmlTemplate>().eq("no", no));
+        if(htmlUrl == null){
+            return R.error("错误编号");
+        }
+        return R.ok().put("data",  htmlUrl);
+    }
+    @GetMapping("/getTemplateById")
+    public R getTemplateById(Long id){
+        AdSite site = adSiteService.getById(id);
+        if(site.getWorkId() != null){
+            QwWorkLink byId = qwWorkLinkService.getById(site.getWorkId());
+            if(byId != null){
+                site.setWorkUrl(byId.getUrl());
+            }
+        }
+        AdHtmlTemplate htmlUrl = adHtmlTemplateMapper.selectById(site.getTemplateId());
+        if(htmlUrl == null){
+            return R.error("错误编号");
+        }
+        return R.ok().put("site", site).put("data",  htmlUrl);
+    }
+//    @GetMapping("/syncPlan")
+//    public R syncPlan(){
+//        baiduApis.listAccount(1L);
+//        return R.ok();
+//    }
+
+
+    @GetMapping("/baiduClickCallbackApi")
+    public R baiduClickCallbackApi(AdBaiduClickCallbackVo vo){
+        log.info("百度点击监听地址返回数据:{}", JSON.toJSONString(vo));
+        adHtmlClickLogService.setLogBaiDuApi(vo);
+        return R.ok();
+    }
+    @GetMapping("/baiduClickCallback")
+    public R baiduClickCallback(AdBaiduClickCallbackVo vo){
+        log.info("百度监听地址返回数据:{}", JSON.toJSONString(vo));
+        adHtmlClickLogService.setLogBaiDu(vo);
+        return R.ok();
+    }
+    @GetMapping("/youkuClickCallback")
+    public R youkuClickCallback(AdYouKuClickCallbackVo vo){
+        log.info("优酷监听地址返回数据:{}", JSON.toJSONString(vo));
+        adHtmlClickLogService.setLogYouKu(vo);
+        return R.ok();
+    }
+    @GetMapping("/iqiyiClickCallback")
+    public R iqiyiClickCallback(AdIqiyiClickCallbackVo vo){
+        log.info("爱奇艺监听地址返回数据:{}", JSON.toJSONString(vo));
+        adHtmlClickLogService.setLogIqiyi(vo);
+        return R.ok();
+    }
+    @GetMapping("/dyClickCallback")
+    public R dyClickCallback(AdDyClickCallbackVo vo){
+        log.info("抖音监听地址返回数据:{}", JSON.toJSONString(vo));
+        adHtmlClickLogService.setLogDy(vo);
+        return R.ok();
+    }
+
+
+}

+ 37 - 0
fs-live-mq/src/main/java/com/fs/app/controller/CommonController.java

@@ -0,0 +1,37 @@
+package com.fs.app.controller;
+
+
+import com.alibaba.fastjson.JSON;
+import com.fs.ad.enums.AdUploadType;
+import com.fs.ad.service.IAdHtmlClickLogService;
+import com.fs.common.core.domain.R;
+import com.fs.qw.vo.AdUploadVo;
+import io.swagger.annotations.Api;
+import jdk.nashorn.internal.ir.annotations.Ignore;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+
+@Slf4j
+@Api("公共接口")
+@RestController
+@AllArgsConstructor
+@Ignore
+@RequestMapping(value="/app/common")
+public class CommonController {
+    private IAdHtmlClickLogService adHtmlClickLogService;
+    private RocketMQTemplate rocketMQTemplate;
+
+    @GetMapping("/testSend")
+    public R testSend(String id){
+        AdUploadVo build = AdUploadVo.builder().state(id).type(AdUploadType.ADD_WX).build();
+        rocketMQTemplate.syncSend("ad-upload", JSON.toJSONString(build));
+//        adHtmlClickLogService.upload(id, AdUploadType.ADD_WX, e -> {});
+        return R.ok();
+    }
+
+}

+ 215 - 0
fs-live-mq/src/main/java/com/fs/app/controller/MockAppController.java

@@ -0,0 +1,215 @@
+package com.fs.app.controller;
+
+import com.fs.baidu.domain.BdApi;
+import com.fs.baidu.service.IBdApiService;
+import com.fs.baidu.utils.SignService;
+import com.fs.huifuPay.sdk.opps.core.exception.BasePayException;
+import com.fs.huifuPay.sdk.opps.core.utils.HttpClientUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.json.JSONObject;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeMap;
+
+@Slf4j
+@RestController
+@RequestMapping("/baiduBack")
+public class MockAppController {
+    private static final String APPID = "appId";
+    private static final String AUTH_CODE = "authCode";
+    private static final String USERID = "userId";
+    private static final String TIMESTAMP = "timestamp";
+    private static final String SIGNATURE = "signature";
+    private static final String STATE = "state";
+    private static final String ACCESSTOKEN_URL = "https://u.baidu.com/oauth/accessToken";
+
+    @Autowired
+    private IBdApiService bdApiService; // 开发者自定义
+    @Autowired
+    private SignService signService; // 开发者可自定义
+
+    @GetMapping("/callback")
+    public String mockApp(HttpServletRequest request) throws BasePayException {
+
+        // 获取请求参数
+        Map<String, String> params = new HashMap<>();
+        this.fillParams(params, request);
+        log.info("callback: params = {}", JSONObject.valueToString(params));
+        int userIdInt = 0;
+        try {
+            userIdInt = Integer.parseInt(params.get(USERID));
+        } catch (Exception e) {
+            return this.getResponseJson(600011, "参数错误", new Object());
+        }
+        // 对签名内容进行判空
+        if (StringUtils.isBlank(params.get(SIGNATURE))) {
+            return this.getResponseJson(600011, "参数错误", new Object());
+        }
+
+        // 获取应用对应的密钥
+        BdApi app = bdApiService.getAppByAppId(params.get(APPID));
+        String sk = app.getAppSecretKey();
+        // 开发者进行验签
+        // 检查状态码
+//        if (!this.checkState(params.get(APPID), app.getAppUserId(), params.get(STATE))) {
+//            log.info("callback: state check fail");
+//            return this.getResponseJson(600011, "状态码错误", new Object());
+//        }
+
+        // 签名验证
+        if (!this.checkSignature(params, sk)) {
+            log.info("callback: signature check fail");
+            return this.getResponseJson(600011, "签名错误", new Object());
+        }
+
+        // 调用接口换取授权令牌
+        String response = this.getAccessToken(params, sk, userIdInt);
+        log.info("callback:getAccesstoken, response={}", response);
+        String accessToken = null;
+        String refreshToken = null;
+        int uid;
+
+        if (response.length() > 0) {
+            JSONObject res = new JSONObject(response);
+            int code = (int) res.get("code");
+            if (code == 0) {
+                JSONObject data = res.getJSONObject("data");
+                accessToken = (String) data.get("accessToken");
+                // 注释部分为 accessToken 其他信息,各应用开发者酌情使用即可
+                 refreshToken = (String) data.get("refreshToken");
+                String openId = data.getString("openId");
+                int expiresIn = data.getInt("expiresIn");
+                int refreshExpiresIn = data.getInt("refreshExpiresIn");
+                app.setAccessToken(accessToken);
+                app.setRefreshToken(refreshToken);
+                app.setExpiresIn(expiresIn);
+                app.setOpenId(openId);
+                app.setRefreshExpiresIn(refreshExpiresIn);
+                app.setAuthCode(params.get(AUTH_CODE));
+                bdApiService.updateById(app);
+
+                // TODO 相关信息落库处理
+            } else {
+                return this.getResponseJson(600011, "未获取到 access_token", new Object());
+            }
+        }
+        Map<String, String> data = new HashMap<>();
+        data.put("accessToken", accessToken);
+        return this.getResponseJson(0, "success", data);
+    }
+
+    /**
+     * 填充请求参数,开发者可用实体代替map
+     *
+     * @param params
+     * @param request
+     */
+    private void fillParams(Map<String, String> params, HttpServletRequest request) {
+        params.put(APPID, request.getParameter(APPID));
+        params.put(AUTH_CODE, request.getParameter(AUTH_CODE));
+        params.put(USERID, request.getParameter(USERID));
+        params.put(TIMESTAMP, request.getParameter(TIMESTAMP));
+        params.put(SIGNATURE, request.getParameter(SIGNATURE));
+        params.put(STATE, request.getParameter(STATE));
+    }
+
+    /**
+     * 校验状态码是否符合预期
+     *
+     * @param appId  应用ID
+     * @param userId 创建应用的ID
+     * @param state  请求参数中的状态码
+     * @return
+     */
+    private boolean checkState(String appId, Long userId, String state) {
+        // md5util 开发者自行开发即可
+        String newState = md5(appId.concat("_").concat(String.valueOf(userId)));
+        return newState.equals(state);
+    }
+    public static String md5(String input) {
+        try {
+            MessageDigest md = MessageDigest.getInstance("MD5");
+            byte[] messageDigest = md.digest(input.getBytes());
+            StringBuilder hexString = new StringBuilder();
+            for (byte b : messageDigest) {
+                String hex = Integer.toHexString(0xff & b);
+                if (hex.length() == 1) {
+                    hexString.append('0');
+                }
+                hexString.append(hex);
+            }
+            return hexString.toString();
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException(e);
+        }
+    }
+    /**
+     * 签名验证方法
+     *
+     * @param params
+     * @param sk
+     * @return
+     */
+    private boolean checkSignature(Map<String, String> params, String sk) {
+        // 获取验签字符串:按key自然排序,拼接成 json 字符串,
+        // 示例:str1 = {"appId": xxxxx, "authCode": xxx, "state": xxx,"timestamp": xxx}
+        TreeMap<String, Object> map = new TreeMap<>();
+        map.put(APPID, params.get(APPID));
+        map.put(AUTH_CODE, params.get(AUTH_CODE));
+        map.put(USERID, params.get(USERID));
+        map.put(STATE, params.get(STATE));
+        map.put(TIMESTAMP, params.get(TIMESTAMP));
+        // 根据上述签名算法对接收到的参数签名
+        String sign = signService.paramsSign(sk, map);
+        log.info("callback: signature = {}", sign);
+        // 判断签名和URL传参签名是否一致
+        return params.get(SIGNATURE).equals(sign);
+    }
+
+    /**
+     * 换取授权令牌
+     *
+     * @param params
+     * @param sk
+     * @param userIdInt 授权账户ID
+     * @return
+     */
+    private String getAccessToken(Map<String, String> params, String sk, int userIdInt) throws BasePayException {
+        Map<String, Object> requestMap = new HashMap<>();
+        requestMap.put(APPID, params.get(APPID));
+        requestMap.put("secretKey", sk);
+        requestMap.put(AUTH_CODE, params.get(AUTH_CODE));
+        requestMap.put("grantType", "access_token");
+        requestMap.put("userId", userIdInt);
+
+        String paramsJson = JSONObject.valueToString(requestMap);
+        // 开发者自行增加异常判断
+        return HttpClientUtils.httpPostJson(ACCESSTOKEN_URL, new HashMap<>(), paramsJson);
+    }
+
+    /**
+     * 封装返回json串
+     *
+     * @param code
+     * @param message
+     * @param data
+     * @return
+     */
+    private String getResponseJson(int code, String message, Object data) {
+        // 封装返回字段的map
+        Map<String, Object> responseMap = new HashMap<>();
+        responseMap.put("code", code);
+        responseMap.put("message", message);
+        responseMap.put("data", data);
+        return JSONObject.valueToString(responseMap);
+    }
+}

+ 51 - 0
fs-live-mq/src/main/java/com/fs/app/exception/FSException.java

@@ -0,0 +1,51 @@
+package com.fs.app.exception;
+
+/**
+ * 自定义异常
+ */
+public class FSException extends RuntimeException {
+	private static final long serialVersionUID = 1L;
+	
+    private String msg;
+    private int code = 500;
+    
+    public FSException(String msg) {
+		super(msg);
+		this.msg = msg;
+	}
+	
+	public FSException(String msg, Throwable e) {
+		super(msg, e);
+		this.msg = msg;
+	}
+	
+	public FSException(String msg, int code) {
+		super(msg);
+		this.msg = msg;
+		this.code = code;
+	}
+	
+	public FSException(String msg, int code, Throwable e) {
+		super(msg, e);
+		this.msg = msg;
+		this.code = code;
+	}
+
+	public String getMsg() {
+		return msg;
+	}
+
+	public void setMsg(String msg) {
+		this.msg = msg;
+	}
+
+	public int getCode() {
+		return code;
+	}
+
+	public void setCode(int code) {
+		this.code = code;
+	}
+	
+	
+}

+ 81 - 0
fs-live-mq/src/main/java/com/fs/app/exception/FSExceptionHandler.java

@@ -0,0 +1,81 @@
+package com.fs.app.exception;
+
+
+
+
+import com.fs.common.core.domain.R;
+import com.fs.common.exception.CustomException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.validation.BindException;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.servlet.NoHandlerFoundException;
+
+
+/**
+ * 异常处理器
+ */
+@RestControllerAdvice
+public class FSExceptionHandler {
+	private Logger logger = LoggerFactory.getLogger(getClass());
+
+	/**
+	 * 处理自定义异常
+	 */
+	@ExceptionHandler(FSException.class)
+	public R handleRRException(FSException e){
+		R r = new R();
+		r.put("code", e.getCode());
+		r.put("msg", e.getMessage());
+
+		return r;
+	}
+
+	@ExceptionHandler(NoHandlerFoundException.class)
+	public R handlerNoFoundException(Exception e) {
+		logger.error(e.getMessage(), e);
+		return R.error(404, "路径不存在,请检查路径是否正确");
+	}
+
+	@ExceptionHandler(DuplicateKeyException.class)
+	public R handleDuplicateKeyException(DuplicateKeyException e){
+		logger.error(e.getMessage(), e);
+		return R.error("数据库中已存在该记录");
+	}
+
+
+	@ExceptionHandler(Exception.class)
+	public R handleException(Exception e){
+		logger.error(e.getMessage(), e);
+		return R.error();
+	}
+	@ExceptionHandler(AccessDeniedException.class)
+	public R handleAccessDeniedException(AccessDeniedException e){
+		logger.error(e.getMessage(), e);
+		return R.error("没有权限");
+	}
+
+	@ExceptionHandler(BindException.class)
+	public R bindExceptionHandler(BindException e) {
+		FieldError error = e.getFieldError();
+		String message = String.format("%s",  error.getDefaultMessage());
+		return R.error(message);
+	}
+
+	@ExceptionHandler(MethodArgumentNotValidException.class)
+	public R exceptionHandler(MethodArgumentNotValidException e) {
+		FieldError error = e.getBindingResult().getFieldError();
+		String message = String.format("%s",  error.getDefaultMessage());
+		return R.error(message);
+	}
+	@ExceptionHandler(CustomException.class)
+	public R handleException(CustomException e){
+
+		return R.error(e.getMessage());
+	}
+}

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

@@ -0,0 +1,49 @@
+package com.fs.app.mq;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.ad.service.IAdHtmlClickLogService;
+import com.fs.common.utils.StringUtils;
+import com.fs.live.mapper.LiveGoodsMapper;
+import com.fs.live.vo.LiveGoodsUploadMqVo;
+import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.vo.AdUploadVo;
+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", 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);
+        }
+    }
+}

+ 182 - 0
fs-live-mq/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java

@@ -0,0 +1,182 @@
+package com.fs.framework.aspectj;
+
+import com.fs.common.annotation.DataScope;
+import com.fs.common.core.domain.BaseEntity;
+import com.fs.common.core.domain.entity.SysRole;
+import com.fs.common.core.domain.entity.SysUser;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.utils.SecurityUtils;
+import com.fs.common.utils.StringUtils;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+
+/**
+ * 数据过滤处理
+ *
+
+ */
+@Aspect
+@Component
+public class DataScopeAspect
+{
+    /**
+     * 全部数据权限
+     */
+    public static final String DATA_SCOPE_ALL = "1";
+
+    /**
+     * 自定数据权限
+     */
+    public static final String DATA_SCOPE_CUSTOM = "2";
+
+    /**
+     * 部门数据权限
+     */
+    public static final String DATA_SCOPE_DEPT = "3";
+
+    /**
+     * 部门及以下数据权限
+     */
+    public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";
+
+    /**
+     * 仅本人数据权限
+     */
+    public static final String DATA_SCOPE_SELF = "5";
+
+    /**
+     * 数据权限过滤关键字
+     */
+    public static final String DATA_SCOPE = "dataScope";
+
+    // 配置织入点
+    @Pointcut("@annotation(com.fs.common.annotation.DataScope)")
+    public void dataScopePointCut()
+    {
+    }
+
+    @Before("dataScopePointCut()")
+    public void doBefore(JoinPoint point) throws Throwable
+    {
+        clearDataScope(point);
+        handleDataScope(point);
+    }
+
+    protected void handleDataScope(final JoinPoint joinPoint)
+    {
+        // 获得注解
+        DataScope controllerDataScope = getAnnotationLog(joinPoint);
+        if (controllerDataScope == null)
+        {
+            return;
+        }
+        // 获取当前的用户
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        if (StringUtils.isNotNull(loginUser))
+        {
+            SysUser currentUser = loginUser.getUser();
+            // 如果是超级管理员,则不过滤数据
+            if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin())
+            {
+                dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),
+                        controllerDataScope.userAlias());
+            }
+        }
+    }
+
+    /**
+     * 数据范围过滤
+     *
+     * @param joinPoint 切点
+     * @param user 用户
+     * @param userAlias 别名
+     */
+    public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias)
+    {
+        StringBuilder sqlString = new StringBuilder();
+
+        for (SysRole role : user.getRoles())
+        {
+            String dataScope = role.getDataScope();
+            if (DATA_SCOPE_ALL.equals(dataScope))
+            {
+                sqlString = new StringBuilder();
+                break;
+            }
+            else if (DATA_SCOPE_CUSTOM.equals(dataScope))
+            {
+                sqlString.append(StringUtils.format(
+                        " OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,
+                        role.getRoleId()));
+            }
+            else if (DATA_SCOPE_DEPT.equals(dataScope))
+            {
+                sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
+            }
+            else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))
+            {
+                sqlString.append(StringUtils.format(
+                        " OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
+                        deptAlias, user.getDeptId(), user.getDeptId()));
+            }
+            else if (DATA_SCOPE_SELF.equals(dataScope))
+            {
+                if (StringUtils.isNotBlank(userAlias))
+                {
+                    sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
+                }
+                else
+                {
+                    // 数据权限为仅本人且没有userAlias别名不查询任何数据
+                    sqlString.append(" OR 1=0 ");
+                }
+            }
+        }
+
+        if (StringUtils.isNotBlank(sqlString.toString()))
+        {
+            Object params = joinPoint.getArgs()[0];
+            if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+            {
+                BaseEntity baseEntity = (BaseEntity) params;
+                baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
+            }
+        }
+    }
+
+    /**
+     * 是否存在注解,如果存在就获取
+     */
+    private DataScope getAnnotationLog(JoinPoint joinPoint)
+    {
+        Signature signature = joinPoint.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method method = methodSignature.getMethod();
+
+        if (method != null)
+        {
+            return method.getAnnotation(DataScope.class);
+        }
+        return null;
+    }
+
+    /**
+     * 拼接权限sql前先清空params.dataScope参数防止注入
+     */
+    private void clearDataScope(final JoinPoint joinPoint)
+    {
+        Object params = joinPoint.getArgs()[0];
+        if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+        {
+            BaseEntity baseEntity = (BaseEntity) params;
+            baseEntity.getParams().put(DATA_SCOPE, "");
+        }
+    }
+}

+ 73 - 0
fs-live-mq/src/main/java/com/fs/framework/aspectj/DataSourceAspect.java

@@ -0,0 +1,73 @@
+package com.fs.framework.aspectj;
+
+import com.fs.common.annotation.DataSource;
+import com.fs.common.utils.StringUtils;
+import com.fs.framework.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);
+    }
+}

+ 245 - 0
fs-live-mq/src/main/java/com/fs/framework/aspectj/LogAspect.java

@@ -0,0 +1,245 @@
+package com.fs.framework.aspectj;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.enums.BusinessStatus;
+import com.fs.common.enums.HttpMethod;
+import com.fs.common.utils.SecurityUtils;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.ip.IpUtils;
+
+import com.fs.framework.manager.AsyncManager;
+import com.fs.framework.manager.factory.AsyncFactory;
+import com.fs.system.domain.SysOperLog;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.AfterThrowing;
+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.stereotype.Component;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.servlet.HandlerMapping;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * 操作日志记录处理
+ * 
+
+ */
+@Aspect
+@Component
+public class LogAspect
+{
+    private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
+
+    // 配置织入点
+    @Pointcut("@annotation(com.fs.common.annotation.Log)")
+    public void logPointCut()
+    {
+    }
+
+    /**
+     * 处理完请求后执行
+     *
+     * @param joinPoint 切点
+     */
+    @AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
+    public void doAfterReturning(JoinPoint joinPoint, Object jsonResult)
+    {
+        handleLog(joinPoint, null, jsonResult);
+    }
+
+    /**
+     * 拦截异常操作
+     * 
+     * @param joinPoint 切点
+     * @param e 异常
+     */
+    @AfterThrowing(value = "logPointCut()", throwing = "e")
+    public void doAfterThrowing(JoinPoint joinPoint, Exception e)
+    {
+        handleLog(joinPoint, e, null);
+    }
+
+    protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult)
+    {
+        try
+        {
+            // 获得注解
+            Log controllerLog = getAnnotationLog(joinPoint);
+            if (controllerLog == null)
+            {
+                return;
+            }
+
+            // 获取当前的用户
+            LoginUser loginUser = SecurityUtils.getLoginUser();
+
+            // *========数据库日志=========*//
+            SysOperLog operLog = new SysOperLog();
+            operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
+            // 请求的地址
+            String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
+            operLog.setOperIp(ip);
+            // 返回参数
+            operLog.setJsonResult(JSON.toJSONString(jsonResult));
+
+            operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());
+            if (loginUser != null)
+            {
+                operLog.setOperName(loginUser.getUsername());
+            }
+
+            if (e != null)
+            {
+                operLog.setStatus(BusinessStatus.FAIL.ordinal());
+                operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
+            }
+            // 设置方法名称
+            String className = joinPoint.getTarget().getClass().getName();
+            String methodName = joinPoint.getSignature().getName();
+            operLog.setMethod(className + "." + methodName + "()");
+            // 设置请求方式
+            operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
+            // 处理设置注解上的参数
+            getControllerMethodDescription(joinPoint, controllerLog, operLog);
+            // 保存数据库
+            AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
+        }
+        catch (Exception exp)
+        {
+            // 记录本地异常日志
+            log.error("==前置通知异常==");
+            log.error("异常信息:{}", exp.getMessage());
+            exp.printStackTrace();
+        }
+    }
+
+    /**
+     * 获取注解中对方法的描述信息 用于Controller层注解
+     * 
+     * @param log 日志
+     * @param operLog 操作日志
+     * @throws Exception
+     */
+    public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog) throws Exception
+    {
+        // 设置action动作
+        operLog.setBusinessType(log.businessType().ordinal());
+        // 设置标题
+        operLog.setTitle(log.title());
+        // 设置操作人类别
+        operLog.setOperatorType(log.operatorType().ordinal());
+        // 是否需要保存request,参数和值
+        if (log.isSaveRequestData())
+        {
+            // 获取参数的信息,传入到数据库中。
+            setRequestValue(joinPoint, operLog);
+        }
+    }
+
+    /**
+     * 获取请求的参数,放到log中
+     * 
+     * @param operLog 操作日志
+     * @throws Exception 异常
+     */
+    private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog) throws Exception
+    {
+        String requestMethod = operLog.getRequestMethod();
+        if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod))
+        {
+            String params = argsArrayToString(joinPoint.getArgs());
+            operLog.setOperParam(StringUtils.substring(params, 0, 2000));
+        }
+        else
+        {
+            Map<?, ?> paramsMap = (Map<?, ?>) ServletUtils.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
+            operLog.setOperParam(StringUtils.substring(paramsMap.toString(), 0, 2000));
+        }
+    }
+
+    /**
+     * 是否存在注解,如果存在就获取
+     */
+    private Log getAnnotationLog(JoinPoint joinPoint) throws Exception
+    {
+        Signature signature = joinPoint.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method method = methodSignature.getMethod();
+
+        if (method != null)
+        {
+            return method.getAnnotation(Log.class);
+        }
+        return null;
+    }
+
+    /**
+     * 参数拼装
+     */
+    private String argsArrayToString(Object[] paramsArray)
+    {
+        String params = "";
+        if (paramsArray != null && paramsArray.length > 0)
+        {
+            for (int i = 0; i < paramsArray.length; i++)
+            {
+                if (StringUtils.isNotNull(paramsArray[i]) && !isFilterObject(paramsArray[i]))
+                {
+                    Object jsonObj = JSON.toJSON(paramsArray[i]);
+                    params += jsonObj.toString() + " ";
+                }
+            }
+        }
+        return params.trim();
+    }
+
+    /**
+     * 判断是否需要过滤的对象。
+     * 
+     * @param o 对象信息。
+     * @return 如果是需要过滤的对象,则返回true;否则返回false。
+     */
+    @SuppressWarnings("rawtypes")
+    public boolean isFilterObject(final Object o)
+    {
+        Class<?> clazz = o.getClass();
+        if (clazz.isArray())
+        {
+            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
+        }
+        else if (Collection.class.isAssignableFrom(clazz))
+        {
+            Collection collection = (Collection) o;
+            for (Iterator iter = collection.iterator(); iter.hasNext();)
+            {
+                return iter.next() instanceof MultipartFile;
+            }
+        }
+        else if (Map.class.isAssignableFrom(clazz))
+        {
+            Map map = (Map) o;
+            for (Iterator iter = map.entrySet().iterator(); iter.hasNext();)
+            {
+                Map.Entry entry = (Map.Entry) iter.next();
+                return entry.getValue() instanceof MultipartFile;
+            }
+        }
+        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
+                || o instanceof BindingResult;
+    }
+}

+ 117 - 0
fs-live-mq/src/main/java/com/fs/framework/aspectj/RateLimiterAspect.java

@@ -0,0 +1,117 @@
+package com.fs.framework.aspectj;
+
+import com.fs.common.annotation.RateLimiter;
+import com.fs.common.enums.LimitType;
+import com.fs.common.exception.ServiceException;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.ip.IpUtils;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.RedisScript;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 限流处理
+ *
+
+ */
+@Aspect
+@Component
+public class RateLimiterAspect
+{
+    private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);
+
+    private RedisTemplate<Object, Object> redisTemplate;
+
+    private RedisScript<Long> limitScript;
+
+    @Autowired
+    public void setRedisTemplate1(RedisTemplate<Object, Object> redisTemplate)
+    {
+        this.redisTemplate = redisTemplate;
+    }
+
+    @Autowired
+    public void setLimitScript(RedisScript<Long> limitScript)
+    {
+        this.limitScript = limitScript;
+    }
+
+    // 配置织入点
+    @Pointcut("@annotation(com.fs.common.annotation.RateLimiter)")
+    public void rateLimiterPointCut()
+    {
+    }
+
+    @Before("rateLimiterPointCut()")
+    public void doBefore(JoinPoint point) throws Throwable
+    {
+        RateLimiter rateLimiter = getAnnotationRateLimiter(point);
+        String key = rateLimiter.key();
+        int time = rateLimiter.time();
+        int count = rateLimiter.count();
+
+        String combineKey = getCombineKey(rateLimiter, point);
+        List<Object> keys = Collections.singletonList(combineKey);
+        try
+        {
+            Long number = redisTemplate.execute(limitScript, keys, count, time);
+            if (StringUtils.isNull(number) || number.intValue() > count)
+            {
+                throw new ServiceException("访问过于频繁,请稍后再试");
+            }
+            log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), key);
+        }
+        catch (ServiceException e)
+        {
+            throw e;
+        }
+        catch (Exception e)
+        {
+            throw new RuntimeException("服务器限流异常,请稍后再试");
+        }
+    }
+
+    /**
+     * 是否存在注解,如果存在就获取
+     */
+    private RateLimiter getAnnotationRateLimiter(JoinPoint joinPoint)
+    {
+        Signature signature = joinPoint.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method method = methodSignature.getMethod();
+
+        if (method != null)
+        {
+            return method.getAnnotation(RateLimiter.class);
+        }
+        return null;
+    }
+
+    public String getCombineKey(RateLimiter rateLimiter, JoinPoint point)
+    {
+        StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
+        if (rateLimiter.limitType() == LimitType.IP)
+        {
+            stringBuffer.append(IpUtils.getIpAddr(ServletUtils.getRequest()));
+        }
+        MethodSignature signature = (MethodSignature) point.getSignature();
+        Method method = signature.getMethod();
+        Class<?> targetClass = method.getDeclaringClass();
+        stringBuffer.append("-").append(targetClass.getName()).append("- ").append(method.getName());
+        return stringBuffer.toString();
+    }
+}

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

@@ -0,0 +1,31 @@
+package com.fs.framework.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());
+    }
+}

+ 85 - 0
fs-live-mq/src/main/java/com/fs/framework/config/CaptchaConfig.java

@@ -0,0 +1,85 @@
+package com.fs.framework.config;
+
+import com.google.code.kaptcha.impl.DefaultKaptcha;
+import com.google.code.kaptcha.util.Config;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.Properties;
+
+import static com.google.code.kaptcha.Constants.*;
+
+/**
+ * 验证码配置
+ * 
+
+ */
+@Configuration
+public class CaptchaConfig
+{
+    @Bean(name = "captchaProducer")
+    public DefaultKaptcha getKaptchaBean()
+    {
+        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
+        Properties properties = new Properties();
+        // 是否有边框 默认为true 我们可以自己设置yes,no
+        properties.setProperty(KAPTCHA_BORDER, "yes");
+        // 验证码文本字符颜色 默认为Color.BLACK
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black");
+        // 验证码图片宽度 默认为200
+        properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
+        // 验证码图片高度 默认为50
+        properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
+        // 验证码文本字符大小 默认为40
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38");
+        // KAPTCHA_SESSION_KEY
+        properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode");
+        // 验证码文本字符长度 默认为5
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
+        // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
+        // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
+        properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
+        Config config = new Config(properties);
+        defaultKaptcha.setConfig(config);
+        return defaultKaptcha;
+    }
+
+    @Bean(name = "captchaProducerMath")
+    public DefaultKaptcha getKaptchaBeanMath()
+    {
+        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
+        Properties properties = new Properties();
+        // 是否有边框 默认为true 我们可以自己设置yes,no
+        properties.setProperty(KAPTCHA_BORDER, "yes");
+        // 边框颜色 默认为Color.BLACK
+        properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90");
+        // 验证码文本字符颜色 默认为Color.BLACK
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue");
+        // 验证码图片宽度 默认为200
+        properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
+        // 验证码图片高度 默认为50
+        properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
+        // 验证码文本字符大小 默认为40
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35");
+        // KAPTCHA_SESSION_KEY
+        properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath");
+        // 验证码文本生成器
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.fs.framework.config.KaptchaTextCreator");
+        // 验证码文本字符间距 默认为2
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3");
+        // 验证码文本字符长度 默认为5
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6");
+        // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
+        // 验证码噪点颜色 默认为Color.BLACK
+        properties.setProperty(KAPTCHA_NOISE_COLOR, "white");
+        // 干扰实现类
+        properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
+        // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
+        properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
+        Config config = new Config(properties);
+        defaultKaptcha.setConfig(config);
+        return defaultKaptcha;
+    }
+}

+ 92 - 0
fs-live-mq/src/main/java/com/fs/framework/config/DataSourceConfig.java

@@ -0,0 +1,92 @@
+package com.fs.framework.config;
+
+import com.alibaba.druid.pool.DruidDataSource;
+import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
+import com.alibaba.druid.util.Utils;
+import com.fs.common.enums.DataSourceType;
+import com.fs.framework.datasource.DynamicDataSource;
+import org.springframework.beans.factory.annotation.Qualifier;
+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;
+
+@Configuration
+public class DataSourceConfig {
+
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.sop.druid.master")
+    public DataSource sopDataSource() {
+        return new DruidDataSource();
+    }
+
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.mysql.druid.master")
+    public DataSource masterDataSource() {
+        return new DruidDataSource();
+    }
+
+
+
+    @Bean
+    @Primary
+    public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("sopDataSource") DataSource sopDataSource) {
+        Map<Object, Object> targetDataSources = new HashMap<>();
+        targetDataSources.put(DataSourceType.SOP.name(), sopDataSource);
+        return new DynamicDataSource(masterDataSource, targetDataSources);
+    }
+
+    /**
+     * 去除监控页面底部的广告
+     */
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Bean
+    @ConditionalOnProperty(name = "spring.datasource.mysql.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;
+    }
+}

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

@@ -0,0 +1,123 @@
+package com.fs.framework.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.framework.config.properties.DruidProperties;
+import com.fs.framework.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.mysql.druid.master")
+    public DataSource masterDataSource(DruidProperties druidProperties)
+    {
+        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
+        return druidProperties.dataSource(dataSource);
+    }
+
+    @Bean
+    @ConfigurationProperties("spring.datasource.mysql.druid.slave")
+    @ConditionalOnProperty(prefix = "spring.datasource.mysql.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.mysql.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/framework/config/FastJson2JsonRedisSerializer.java

@@ -0,0 +1,72 @@
+package com.fs.framework.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);
+    }
+}

+ 59 - 0
fs-live-mq/src/main/java/com/fs/framework/config/FilterConfig.java

@@ -0,0 +1,59 @@
+package com.fs.framework.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.autoconfigure.condition.ConditionalOnProperty;
+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
+@ConditionalOnProperty(value = "xss.enabled", havingValue = "true")
+public class FilterConfig
+{
+    @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);
+        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;
+    }
+
+}

+ 76 - 0
fs-live-mq/src/main/java/com/fs/framework/config/KaptchaTextCreator.java

@@ -0,0 +1,76 @@
+package com.fs.framework.config;
+
+import com.google.code.kaptcha.text.impl.DefaultTextCreator;
+
+import java.util.Random;
+
+/**
+ * 验证码文本生成器
+ * 
+
+ */
+public class KaptchaTextCreator extends DefaultTextCreator
+{
+    private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(",");
+
+    @Override
+    public String getText()
+    {
+        Integer result = 0;
+        Random random = new Random();
+        int x = random.nextInt(10);
+        int y = random.nextInt(10);
+        StringBuilder suChinese = new StringBuilder();
+        int randomoperands = (int) Math.round(Math.random() * 2);
+        if (randomoperands == 0)
+        {
+            result = x * y;
+            suChinese.append(CNUMBERS[x]);
+            suChinese.append("*");
+            suChinese.append(CNUMBERS[y]);
+        }
+        else if (randomoperands == 1)
+        {
+            if (!(x == 0) && y % x == 0)
+            {
+                result = y / x;
+                suChinese.append(CNUMBERS[y]);
+                suChinese.append("/");
+                suChinese.append(CNUMBERS[x]);
+            }
+            else
+            {
+                result = x + y;
+                suChinese.append(CNUMBERS[x]);
+                suChinese.append("+");
+                suChinese.append(CNUMBERS[y]);
+            }
+        }
+        else if (randomoperands == 2)
+        {
+            if (x >= y)
+            {
+                result = x - y;
+                suChinese.append(CNUMBERS[x]);
+                suChinese.append("-");
+                suChinese.append(CNUMBERS[y]);
+            }
+            else
+            {
+                result = y - x;
+                suChinese.append(CNUMBERS[y]);
+                suChinese.append("-");
+                suChinese.append(CNUMBERS[x]);
+            }
+        }
+        else
+        {
+            result = x + y;
+            suChinese.append(CNUMBERS[x]);
+            suChinese.append("+");
+            suChinese.append(CNUMBERS[y]);
+        }
+        suChinese.append("=?@" + result);
+        return suChinese.toString();
+    }
+}

+ 150 - 0
fs-live-mq/src/main/java/com/fs/framework/config/MyBatisConfig.java

@@ -0,0 +1,150 @@
+package com.fs.framework.config;
+
+import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
+import com.fs.common.utils.StringUtils;
+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.Arrays;
+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;
+    }
+
+    public Resource[] resolveMapperLocations(String[] mapperLocations)
+    {
+        ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
+        List<Resource> resources = new ArrayList<Resource>();
+        if (mapperLocations != null)
+        {
+            for (String mapperLocation : mapperLocations)
+            {
+                try
+                {
+                    Resource[] mappers = resourceResolver.getResources(mapperLocation);
+                    resources.addAll(Arrays.asList(mappers));
+                }
+                catch (IOException e)
+                {
+                    // ignore
+                }
+            }
+        }
+        return resources.toArray(new Resource[resources.size()]);
+    }
+
+//    @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(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
+//        sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
+//        return sessionFactory.getObject();
+//    }
+    @Bean
+    public SqlSessionFactory sqlSessionFactorys(DataSource dataSource) throws Exception
+    {
+        String typeAliasesPackage = env.getProperty("mybatis-plus.typeAliasesPackage");
+        String mapperLocations = env.getProperty("mybatis-plus.mapperLocations");
+        String configLocation = env.getProperty("mybatis-plus.configLocation");
+        typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
+        VFS.addImplClass(SpringBootVFS.class);
+
+        final MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
+        sessionFactory.setDataSource(dataSource);
+        sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
+        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
+        sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
+        return sessionFactory.getObject();
+    }
+}

+ 158 - 0
fs-live-mq/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -0,0 +1,158 @@
+package com.fs.framework.config;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
+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.core.script.DefaultRedisScript;
+import org.springframework.data.redis.serializer.GenericToStringSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+import java.math.BigDecimal;
+
+/**
+ * redis配置
+ *
+
+ */
+@Configuration
+@EnableCaching
+public class RedisConfig extends CachingConfigurerSupport
+{
+    @Bean
+    @SuppressWarnings(value = { "unchecked", "rawtypes" })
+    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
+    {
+        RedisTemplate<Object, 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.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
+        serializer.setObjectMapper(mapper);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+        template.setValueSerializer(serializer);
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(serializer);
+
+        template.afterPropertiesSet();
+        return template;
+    }
+    @Bean
+    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        template.afterPropertiesSet();
+        return template;
+    }
+    @Bean
+    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Integer> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
+
+        template.afterPropertiesSet();
+        return template;
+    }
+
+    @Bean
+    @SuppressWarnings(value = { "unchecked", "rawtypes" })
+    public RedisTemplate<String, Object> redisTemplateForObject(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.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
+        serializer.setObjectMapper(mapper);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+        template.setValueSerializer(serializer);
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(serializer);
+
+        template.afterPropertiesSet();
+        return template;
+    }
+    @Bean
+    public RedisTemplate<String, BigDecimal> redisTemplateForBigDecimal(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, BigDecimal> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(BigDecimal.class));
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(new GenericToStringSerializer<>(BigDecimal.class));
+
+        template.afterPropertiesSet();
+        return template;
+    }
+    @Bean
+    public DefaultRedisScript<Long> limitScript()
+    {
+        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
+        redisScript.setScriptText(limitScriptText());
+        redisScript.setResultType(Long.class);
+        return redisScript;
+    }
+
+    /**
+     * 限流脚本
+     */
+    private String limitScriptText()
+    {
+        return "local key = KEYS[1]\n" +
+                "local count = tonumber(ARGV[1])\n" +
+                "local time = tonumber(ARGV[2])\n" +
+                "local current = redis.call('get', key);\n" +
+                "if current and tonumber(current) > count then\n" +
+                "    return current;\n" +
+                "end\n" +
+                "current = redis.call('incr', key)\n" +
+                "if tonumber(current) == 1 then\n" +
+                "    redis.call('expire', key, time)\n" +
+                "end\n" +
+                "return current;";
+    }
+}

+ 65 - 0
fs-live-mq/src/main/java/com/fs/framework/config/ResourcesConfig.java

@@ -0,0 +1,65 @@
+package com.fs.framework.config;
+
+import com.fs.common.config.FSConfig;
+import com.fs.common.constant.Constants;
+import com.fs.framework.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/**").addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/");
+    }
+
+    /**
+     * 自定义拦截规则
+     */
+    @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);
+    }
+}

+ 50 - 0
fs-live-mq/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -0,0 +1,50 @@
+package com.fs.framework.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.BeanIds;
+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();
+    }
+
+    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
+    @Override
+    public AuthenticationManager authenticationManagerBean() throws Exception {
+        return super.authenticationManagerBean();
+    }
+
+
+}

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

@@ -0,0 +1,33 @@
+package com.fs.framework.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();
+    }
+}

+ 121 - 0
fs-live-mq/src/main/java/com/fs/framework/config/SwaggerConfig.java

@@ -0,0 +1,121 @@
+package com.fs.framework.config;
+
+import com.fs.common.config.FSConfig;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.models.auth.In;
+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 java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Swagger2的接口配置
+ * 
+
+ */
+@Configuration
+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", "Authorization", "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/framework/config/ThreadPoolConfig.java

@@ -0,0 +1,63 @@
+package com.fs.framework.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/framework/config/properties/DruidProperties.java

@@ -0,0 +1,77 @@
+package com.fs.framework.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.mysql.druid.initialSize}")
+    private int initialSize;
+
+    @Value("${spring.datasource.mysql.druid.minIdle}")
+    private int minIdle;
+
+    @Value("${spring.datasource.mysql.druid.maxActive}")
+    private int maxActive;
+
+    @Value("${spring.datasource.mysql.druid.maxWait}")
+    private int maxWait;
+
+    @Value("${spring.datasource.mysql.druid.timeBetweenEvictionRunsMillis}")
+    private int timeBetweenEvictionRunsMillis;
+
+    @Value("${spring.datasource.mysql.druid.minEvictableIdleTimeMillis}")
+    private int minEvictableIdleTimeMillis;
+
+    @Value("${spring.datasource.mysql.druid.maxEvictableIdleTimeMillis}")
+    private int maxEvictableIdleTimeMillis;
+
+    @Value("${spring.datasource.mysql.druid.validationQuery}")
+    private String validationQuery;
+
+    @Value("${spring.datasource.mysql.druid.testWhileIdle}")
+    private boolean testWhileIdle;
+
+    @Value("${spring.datasource.mysql.druid.testOnBorrow}")
+    private boolean testOnBorrow;
+
+    @Value("${spring.datasource.mysql.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/framework/datasource/DynamicDataSource.java

@@ -0,0 +1,27 @@
+package com.fs.framework.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/framework/datasource/DynamicDataSourceContextHolder.java

@@ -0,0 +1,45 @@
+package com.fs.framework.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)
+    {
+//        log.info("切换到{}数据源", 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/framework/interceptor/RepeatSubmitInterceptor.java

@@ -0,0 +1,56 @@
+package com.fs.framework.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/framework/interceptor/impl/SameUrlDataInterceptor.java

@@ -0,0 +1,126 @@
+package com.fs.framework.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.framework.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("${token.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 cacheRepeatKey = Constants.REPEAT_SUBMIT_KEY + submitKey;
+
+        Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);
+        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(cacheRepeatKey, 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;
+    }
+}

+ 56 - 0
fs-live-mq/src/main/java/com/fs/framework/manager/AsyncManager.java

@@ -0,0 +1,56 @@
+package com.fs.framework.manager;
+
+import com.fs.common.utils.Threads;
+import com.fs.common.utils.spring.SpringUtils;
+
+import java.util.TimerTask;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 异步任务管理器
+ * 
+
+ */
+public class AsyncManager
+{
+    /**
+     * 操作延迟10毫秒
+     */
+    private final int OPERATE_DELAY_TIME = 10;
+
+    /**
+     * 异步操作任务调度线程池
+     */
+    private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");
+
+    /**
+     * 单例模式
+     */
+    private AsyncManager(){}
+
+    private static AsyncManager me = new AsyncManager();
+
+    public static AsyncManager me()
+    {
+        return me;
+    }
+
+    /**
+     * 执行任务
+     * 
+     * @param task 任务
+     */
+    public void execute(TimerTask task)
+    {
+        executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * 停止任务线程池
+     */
+    public void shutdown()
+    {
+        Threads.shutdownAndAwaitTermination(executor);
+    }
+}

+ 40 - 0
fs-live-mq/src/main/java/com/fs/framework/manager/ShutdownManager.java

@@ -0,0 +1,40 @@
+package com.fs.framework.manager;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PreDestroy;
+
+/**
+ * 确保应用退出时能关闭后台线程
+ *
+
+ */
+@Component
+public class ShutdownManager
+{
+    private static final Logger logger = LoggerFactory.getLogger("sys-user");
+
+    @PreDestroy
+    public void destroy()
+    {
+        shutdownAsyncManager();
+    }
+
+    /**
+     * 停止异步执行任务
+     */
+    private void shutdownAsyncManager()
+    {
+        try
+        {
+            logger.info("====关闭后台任务任务线程池====");
+            AsyncManager.me().shutdown();
+        }
+        catch (Exception e)
+        {
+            logger.error(e.getMessage(), e);
+        }
+    }
+}

+ 103 - 0
fs-live-mq/src/main/java/com/fs/framework/manager/factory/AsyncFactory.java

@@ -0,0 +1,103 @@
+package com.fs.framework.manager.factory;
+
+import com.fs.common.constant.Constants;
+import com.fs.common.utils.LogUtils;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.ip.AddressUtils;
+import com.fs.common.utils.ip.IpUtils;
+import com.fs.common.utils.spring.SpringUtils;
+import com.fs.system.domain.SysLogininfor;
+import com.fs.system.domain.SysOperLog;
+import com.fs.system.service.ISysLogininforService;
+import com.fs.system.service.ISysOperLogService;
+import eu.bitwalker.useragentutils.UserAgent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.TimerTask;
+
+/**
+ * 异步工厂(产生任务用)
+ * 
+
+ */
+public class AsyncFactory
+{
+    private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user");
+
+    /**
+     * 记录登录信息
+     * 
+     * @param username 用户名
+     * @param status 状态
+     * @param message 消息
+     * @param args 列表
+     * @return 任务task
+     */
+    public static TimerTask recordLogininfor(final String username, final String status, final String message,
+            final Object... args)
+    {
+        final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
+        final String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
+        return new TimerTask()
+        {
+            @Override
+            public void run()
+            {
+                String address = AddressUtils.getRealAddressByIP(ip);
+                StringBuilder s = new StringBuilder();
+                s.append(LogUtils.getBlock(ip));
+                s.append(address);
+                s.append(LogUtils.getBlock(username));
+                s.append(LogUtils.getBlock(status));
+                s.append(LogUtils.getBlock(message));
+                // 打印信息到日志
+                sys_user_logger.info(s.toString(), args);
+                // 获取客户端操作系统
+                String os = userAgent.getOperatingSystem().getName();
+                // 获取客户端浏览器
+                String browser = userAgent.getBrowser().getName();
+                // 封装对象
+                SysLogininfor logininfor = new SysLogininfor();
+                logininfor.setUserName(username);
+                logininfor.setIpaddr(ip);
+                logininfor.setLoginLocation(address);
+                logininfor.setBrowser(browser);
+                logininfor.setOs(os);
+                logininfor.setMsg(message);
+                // 日志状态
+                if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER))
+                {
+                    logininfor.setStatus(Constants.SUCCESS);
+                }
+                else if (Constants.LOGIN_FAIL.equals(status))
+                {
+                    logininfor.setStatus(Constants.FAIL);
+                }
+                // 插入数据
+                SpringUtils.getBean(ISysLogininforService.class).insertLogininfor(logininfor);
+            }
+        };
+    }
+
+    /**
+     * 操作日志记录
+     * 
+     * @param operLog 操作日志信息
+     * @return 任务task
+     */
+    public static TimerTask recordOper(final SysOperLog operLog)
+    {
+        return new TimerTask()
+        {
+            @Override
+            public void run()
+            {
+                // 远程查询操作地点
+                operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));
+                SpringUtils.getBean(ISysOperLogService.class).insertOperlog(operLog);
+            }
+        };
+    }
+}

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

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

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

@@ -0,0 +1,37 @@
+#错误消息
+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.register.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}]

+ 93 - 0
fs-live-mq/src/main/resources/logback.xml

@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+    <!-- 日志存放路径 -->
+	<property name="log.path" value="/home/fs-ai-websocket/logs" />
+    <!-- 日志输出格式 -->
+	<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
+
+	<!-- 控制台输出 -->
+	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
+	</appender>
+
+	<!-- 系统日志输出 -->
+	<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
+	    <file>${log.path}/sys-info.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+			<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
+			<!-- 日志最大的历史 60天 -->
+			<maxHistory>60</maxHistory>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>INFO</level>
+            <!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+            <!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+	</appender>
+
+	<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+	    <file>${log.path}/sys-error.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
+			<!-- 日志最大的历史 60天 -->
+			<maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>ERROR</level>
+			<!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+			<!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+	<!-- 用户访问日志输出  -->
+    <appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>${log.path}/sys-user.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 按天回滚 daily -->
+            <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <!-- 日志最大的历史 60天 -->
+            <maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+    </appender>
+
+	<!-- 系统模块日志级别控制  -->
+	<logger name="com.fs" level="info" />
+	<!-- Spring日志级别控制  -->
+	<logger name="org.springframework" level="warn" />
+
+	<root level="info">
+		<appender-ref ref="console" />
+	</root>
+
+	<!--系统操作日志-->
+    <root level="info">
+        <appender-ref ref="file_info" />
+        <appender-ref ref="file_error" />
+    </root>
+
+	<!--系统用户操作日志-->
+    <logger name="sys-user" level="info">
+        <appender-ref ref="sys-user"/>
+    </logger>
+</configuration>

+ 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="true" />  <!-- 全局映射器启用缓存 -->
+		<setting name="useGeneratedKeys"         value="true" />  <!-- 允许 JDBC 支持自动生成主键 -->
+		<setting name="defaultExecutorType"      value="REUSE" /> <!-- 配置默认的执行器 -->
+		<setting name="logImpl"                  value="SLF4J" /> <!-- 指定 MyBatis 所用日志的具体实现 -->
+		 <setting name="mapUnderscoreToCamelCase" value="true"/>
+	</settings>
+	
+</configuration>

+ 5 - 1
fs-service/pom.xml

@@ -301,7 +301,11 @@
             <artifactId>volc-sdk-java</artifactId>
             <version>1.0.250</version>
         </dependency>
-
+        <dependency>
+            <groupId>org.springframework.retry</groupId>
+            <artifactId>spring-retry</artifactId>
+            <version>1.3.1</version>
+        </dependency>
         <!-- 支付宝-->
         <dependency>
             <groupId>com.alipay.sdk</groupId>

+ 3 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java

@@ -62,6 +62,8 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Propagation;
@@ -131,6 +133,7 @@ public class CompanyServiceImpl implements ICompanyService
     private TransactionTemplate transactionTemplate;
 
     @Autowired
+    @Lazy
     private ILiveService liveService;
     @Autowired
     private LiveOrderMapper liveOrderMapper;

+ 14 - 0
fs-service/src/main/java/com/fs/course/mapper/FsUserCompanyUserMapper.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.course.domain.FsUserCompanyUser;
 import com.fs.qw.dto.FsUserTransferParamDTO;
 import com.fs.qw.dto.UserProjectDTO;
+import com.fs.store.param.h5.FsUserPageListParam;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 
@@ -103,5 +104,18 @@ public interface FsUserCompanyUserMapper extends BaseMapper<FsUserCompanyUser>{
      * @return 删除的记录数
      */
     int batchDeleteByIds(@Param("userIds") List<Long> userIds);
+    /**
+     * 根据用户id查询用户id
+     * @param
+     * @return
+     */
+    List<Long> selectIdListByCompanyUserId(FsUserPageListParam param);
+
+    /**
+     * 根据userId查询最开始的数据
+     * @param userId
+     * @return
+     */
+    FsUserCompanyUser selectFsUserCompanyUserByUserId(@Param("userId") Long userId);
 
 }

+ 54 - 0
fs-service/src/main/java/com/fs/enums/ExceptionCodeEnum.java

@@ -0,0 +1,54 @@
+package com.fs.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum ExceptionCodeEnum {
+
+    // ============ 用户相关错误 (400-419) ============
+    USER_NOT_FOUND(401, "当前用户信息不存在,请重新授权"),
+
+    // ============ 会员相关错误 (420-439) ============
+    USER_BLACKLISTED(421, "已被拉黑,请联系管理员"),
+
+    // ============ 销售相关错误 (440-459) ============
+    SALES_NOT_FOUND(441, "当前销售不存在"),
+    SALES_FORBIDDEN_BIND(442, "当前销售禁止绑定会员,请联系销售!"),
+    CONTACT_SALES_FOR_INVITATION(443, "请联系销售发送邀请链接成为会员!"),
+
+    // ============ 会员关系错误 (460-479) ============
+    MEMBER_DISABLED(461, "会员被停用,无权限,请联系客服!"),
+    USER_ALREADY_OTHER_SALES_MEMBER(462, "该用户(%s)已成为其他销售会员"),
+    USER_COMPANY_OTHER_SALES_MEMBER(463, "该用户(%s)已在公司%s成为其他销售会员"),
+
+    // ============ 课程相关错误 (480-499) ============
+    COURSE_CONFIG_ERROR(481, "课程配置错误,项目归属为空,课程ID: %s"),
+    WATCH_LATEST_COURSE(482, "请观看最新的课程项目"),
+    EXCEED_COURSE_LIMIT(483, "超过项目看课数量限制"),
+    ALREADY_WATCHED_OTHER_SALES_COURSE(484, "已看过其他销售分享的此课程,不能重复观看"),
+
+    // ============ 参数相关错误 (500-519) ============
+    PARAM_ERROR(501, "参数错误!"),
+    GROUP_PARAM_ERROR(502, "群参数异常"),
+    LIVE_PARAM_ERROR(503, "直播参数错误"),
+
+    // ============ 系统相关错误 (520-539) ============
+    SERVICE_UNAVAILABLE(521, "服务暂时不可用,请联系管理员");
+
+    private final Integer code;
+    private final String description;
+
+    ExceptionCodeEnum(Integer code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    /**
+     * 获取格式化后的描述信息
+     * @param args 格式化参数
+     * @return 格式化后的描述
+     */
+    public String getFormattedDescription(Object... args) {
+        return String.format(description, args);
+    }
+}

+ 13 - 0
fs-service/src/main/java/com/fs/his/mapper/MerchantAppConfigMapper.java

@@ -2,7 +2,10 @@ package com.fs.his.mapper;
 
 import java.util.List;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
 import com.fs.his.domain.MerchantAppConfig;
+import org.apache.ibatis.annotations.Param;
 
 /**
  * 商户应用配置Mapper接口
@@ -71,4 +74,14 @@ public interface MerchantAppConfigMapper extends BaseMapper<MerchantAppConfig>{
      * @return 结果
      */
     int deleteMerchantAppConfigByIds(Long[] ids);
+
+
+    /**
+     * 根据appId和支付类型查询商户信息
+     *
+     * @param appId
+     * @param payType
+     * @return
+     */
+    MerchantAppConfig selectMerchantAppConfigByAppId(@Param("appId") String appId, @Param("payType") String payType);
 }

+ 1 - 1
fs-service/src/main/java/com/fs/his/service/impl/FsPackageOrderServiceImpl.java

@@ -1155,7 +1155,7 @@ public class FsPackageOrderServiceImpl implements IFsPackageOrderService
                         o.setAppId(appId);
                         //公司分账
                         try {
-                            HuiFuUtils.doDiv(o,fsPackageOrder.getCompanyId());
+                            HuiFuUtils.doDiv(o,fsPackageOrder.getCompanyId(),storePayment.getMerchantId());
                             //存储分账明细
                             HuiFuUtils.saveDivItem(o, fsPackageOrder.getOrderSn(), storePayment.getPayCode());
                         } catch (Exception e) {

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

@@ -2237,7 +2237,7 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
                 o.setAppId(appId);
                 //公司分账
                 try {
-                    HuiFuUtils.doDiv(o,company.getCompanyId());
+                    HuiFuUtils.doDiv(o,company.getCompanyId(),storePayment.getMerchantId());
                     //存储分账明细
                     HuiFuUtils.saveDivItem(o, storePayment.getPayCode(), storePayment.getPayCode());
                 } catch (Exception e) {

+ 1 - 0
fs-service/src/main/java/com/fs/hisStore/config/StoreConfig.java

@@ -28,4 +28,5 @@ public class StoreConfig implements Serializable {
     private Boolean isWeChatShipping;//是否开启微信发货
     private Boolean scanCodeDiscountEnabled;//是否开启扫码立减金
     private BigDecimal scanCodeDiscountAmount;//扫码立减金额
+    private Boolean checkStock;//是否检查库存,默认关闭
 }

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStorePaymentScrm.java

@@ -104,4 +104,7 @@ public class FsStorePaymentScrm extends BaseEntity
     //小程序appId(用于多汇付支付/退款)
     private String appId;
 
+    // 商户配置ID (用于多汇付支付/退款) 切换汇付后需要查询历史汇付配置信息退款
+    private Long merConfigId;
+
 }

+ 45 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductPurchaseLimitScrm.java

@@ -0,0 +1,45 @@
+package com.fs.hisStore.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 商品限购对象 fs_store_product_purchase_limit
+ *
+ * @author fs
+ * @date 2024-01-01
+ */
+@Data
+public class FsStoreProductPurchaseLimitScrm implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 限购ID */
+    private Long id;
+
+    /** 商品ID */
+    @Excel(name = "商品ID")
+    private Long productId;
+
+    /** 用户ID */
+    @Excel(name = "用户ID")
+    private Long userId;
+
+    /** 已购买数量 */
+    @Excel(name = "已购买数量")
+    private Integer num;
+
+    /** 创建时间 */
+    @Excel(name = "创建时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 更新时间 */
+    @Excel(name = "更新时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+
+}
+

+ 6 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductScrm.java

@@ -343,4 +343,10 @@ public class FsStoreProductScrm extends BaseEntity
     @Excel(name = "所属小程序app_id")
     private String appIds;
 
+    /** 限购数量 */
+    @Excel(name = "限购数量")
+    private Integer purchaseLimit;
+
+    /** 过滤商品id */
+    private Long[] excludeProductIds;
 }

+ 76 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductPurchaseLimitScrmMapper.java

@@ -0,0 +1,76 @@
+package com.fs.hisStore.mapper;
+
+import java.util.List;
+
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.hisStore.domain.FsStoreProductPurchaseLimitScrm;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 商品限购Mapper接口
+ *
+ * @author fs
+ * @date 2024-01-01
+ */
+public interface FsStoreProductPurchaseLimitScrmMapper
+{
+    /**
+     * 查询商品限购
+     *
+     * @param id 商品限购ID
+     * @return 商品限购
+     */
+    public FsStoreProductPurchaseLimitScrm selectFsStoreProductPurchaseLimitById(Long id);
+
+    /**
+     * 查询商品限购列表
+     *
+     * @param fsStoreProductPurchaseLimit 商品限购
+     * @return 商品限购集合
+     */
+    public List<FsStoreProductPurchaseLimitScrm> selectFsStoreProductPurchaseLimitList(FsStoreProductPurchaseLimitScrm fsStoreProductPurchaseLimit);
+
+    /**
+     * 根据商品ID和用户ID查询限购记录
+     *
+     * @param productId 商品ID
+     * @param userId 用户ID
+     * @return 商品限购
+     */
+    @DataSource(DataSourceType.SLAVE)
+    public FsStoreProductPurchaseLimitScrm selectByProductIdAndUserId(@Param("productId") Long productId, @Param("userId") Long userId);
+
+    /**
+     * 新增商品限购
+     *
+     * @param fsStoreProductPurchaseLimit 商品限购
+     * @return 结果
+     */
+    public int insertFsStoreProductPurchaseLimit(FsStoreProductPurchaseLimitScrm fsStoreProductPurchaseLimit);
+
+    /**
+     * 修改商品限购
+     *
+     * @param fsStoreProductPurchaseLimit 商品限购
+     * @return 结果
+     */
+    public int updateFsStoreProductPurchaseLimit(FsStoreProductPurchaseLimitScrm fsStoreProductPurchaseLimit);
+
+    /**
+     * 删除商品限购
+     *
+     * @param id 商品限购ID
+     * @return 结果
+     */
+    public int deleteFsStoreProductPurchaseLimitById(Long id);
+
+    /**
+     * 批量删除商品限购
+     *
+     * @param ids 需要删除的数据ID
+     * @return 结果
+     */
+    public int deleteFsStoreProductPurchaseLimitByIds(Long[] ids);
+}
+

+ 10 - 1
fs-service/src/main/java/com/fs/hisStore/mapper/FsUserScrmMapper.java

@@ -124,7 +124,16 @@ public interface FsUserScrmMapper
     @Update("update fs_user set pay_count=pay_count+1" +
             " where user_id=#{userId}")
     int incPayCount(Long userId);
-    @Select("select * from fs_user where phone=#{phone}")
+
+    /**
+     * 增加用户余额
+     * @param userId 用户ID
+     * @param integral 增加的积分
+     * @return 结果
+     */
+    @Update("update fs_user set integral = IFNULL(integral, 0) + #{integral} where user_id = #{userId}")
+    int incrIntegral(@Param("userId") Long userId, @Param("integral") BigDecimal integral);
+    @Select("select * from fs_user where phone=#{phone} and is_del =0")
     FsUserScrm selectFsUserByPhone(String phone);
 
     /**

+ 91 - 0
fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductPurchaseLimitScrmService.java

@@ -0,0 +1,91 @@
+package com.fs.hisStore.service;
+
+import java.util.List;
+import com.fs.hisStore.domain.FsStoreProductPurchaseLimitScrm;
+
+/**
+ * 商品限购Service接口
+ *
+ * @author fs
+ * @date 2024-01-01
+ */
+public interface IFsStoreProductPurchaseLimitScrmService
+{
+    /**
+     * 查询商品限购
+     *
+     * @param id 商品限购ID
+     * @return 商品限购
+     */
+    public FsStoreProductPurchaseLimitScrm selectFsStoreProductPurchaseLimitById(Long id);
+
+    /**
+     * 查询商品限购列表
+     *
+     * @param fsStoreProductPurchaseLimit 商品限购
+     * @return 商品限购集合
+     */
+    public List<FsStoreProductPurchaseLimitScrm> selectFsStoreProductPurchaseLimitList(FsStoreProductPurchaseLimitScrm fsStoreProductPurchaseLimit);
+
+    /**
+     * 根据商品ID和用户ID查询限购记录
+     *
+     * @param productId 商品ID
+     * @param userId 用户ID
+     * @return 商品限购
+     */
+    public FsStoreProductPurchaseLimitScrm selectByProductIdAndUserId(Long productId, Long userId);
+
+    /**
+     * 新增商品限购
+     *
+     * @param fsStoreProductPurchaseLimit 商品限购
+     * @return 结果
+     */
+    public int insertFsStoreProductPurchaseLimit(FsStoreProductPurchaseLimitScrm fsStoreProductPurchaseLimit);
+
+    /**
+     * 修改商品限购
+     *
+     * @param fsStoreProductPurchaseLimit 商品限购
+     * @return 结果
+     */
+    public int updateFsStoreProductPurchaseLimit(FsStoreProductPurchaseLimitScrm fsStoreProductPurchaseLimit);
+
+    /**
+     * 批量删除商品限购
+     *
+     * @param ids 需要删除的商品限购ID
+     * @return 结果
+     */
+    public int deleteFsStoreProductPurchaseLimitByIds(Long[] ids);
+
+    /**
+     * 删除商品限购信息
+     *
+     * @param id 商品限购ID
+     * @return 结果
+     */
+    public int deleteFsStoreProductPurchaseLimitById(Long id);
+
+    /**
+     * 增加用户限购数量
+     *
+     * @param productId 商品ID
+     * @param userId 用户ID
+     * @param num 增加的数量
+     * @return 结果
+     */
+    public int increasePurchaseLimit(Long productId, Long userId, Integer num);
+
+    /**
+     * 减少用户限购数量
+     *
+     * @param productId 商品ID
+     * @param userId 用户ID
+     * @param num 减少的数量
+     * @return 结果
+     */
+    public int decreasePurchaseLimit(Long productId, Long userId, Integer num);
+}
+

+ 1 - 1
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -4324,7 +4324,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                     o.setGoodsDesc("商城订单支付");
                     o.setAppId(param.getAppId());
                     try {
-                        HuiFuUtils.doDiv(o,order.getCompanyId());
+                        HuiFuUtils.doDiv(o,order.getCompanyId(),storePayment.getMerConfigId());
                         //存储分账明细
                         HuiFuUtils.saveDivItem(o, order.getOrderCode(), storePayment.getPayCode());
                     } catch (Exception e) {

+ 1 - 1
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStorePaymentScrmServiceImpl.java

@@ -986,7 +986,7 @@ public class FsStorePaymentScrmServiceImpl implements IFsStorePaymentScrmService
                 o.setAppId(appId);
                 //公司分账
                 try {
-                    HuiFuUtils.doDiv(o,company.getCompanyId());
+                    HuiFuUtils.doDiv(o,company.getCompanyId(),storePayment.getMerConfigId());
                     //存储分账明细
                     HuiFuUtils.saveDivItem(o, storePayment.getPayCode(), storePayment.getPayCode());
                 } catch (Exception e) {

+ 247 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductPurchaseLimitScrmServiceImpl.java

@@ -0,0 +1,247 @@
+package com.fs.hisStore.service.impl;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import com.fs.common.constant.LiveKeysConstant;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.core.redis.RedisCacheT;
+import com.fs.common.utils.DateUtils;
+import com.fs.hisStore.domain.FsStoreProductAttrScrm;
+import com.fs.hisStore.domain.FsStoreProductAttrValueScrm;
+import com.fs.hisStore.domain.FsStoreProductPurchaseLimitScrm;
+import com.fs.hisStore.domain.FsStoreProductScrm;
+import com.fs.hisStore.mapper.FsStoreProductAttrScrmMapper;
+import com.fs.hisStore.mapper.FsStoreProductAttrValueScrmMapper;
+import com.fs.hisStore.mapper.FsStoreProductPurchaseLimitScrmMapper;
+import com.fs.hisStore.mapper.FsStoreProductScrmMapper;
+import com.fs.hisStore.service.IFsStoreProductPurchaseLimitScrmService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * 商品限购Service业务层处理
+ *
+ * @author fs
+ * @date 2024-01-01
+ */
+@Service
+public class FsStoreProductPurchaseLimitScrmServiceImpl implements IFsStoreProductPurchaseLimitScrmService {
+    @Autowired
+    private FsStoreProductPurchaseLimitScrmMapper fsStoreProductPurchaseLimitMapper;
+
+    @Autowired
+    private RedisCache redisCache;
+    @Autowired
+    private RedisCacheT<FsStoreProductPurchaseLimitScrm> redisCacheT;
+    @Autowired
+    private FsStoreProductScrmMapper fsStoreProductScrmMapper;
+    @Autowired
+    private FsStoreProductAttrScrmMapper fsStoreProductAttrScrmMapper;
+    @Autowired
+    private FsStoreProductAttrValueScrmMapper fsStoreProductAttrValueScrmMapper;
+
+
+    /**
+     * 查询商品限购
+     *
+     * @param id 商品限购ID
+     * @return 商品限购
+     */
+    @Override
+    public FsStoreProductPurchaseLimitScrm selectFsStoreProductPurchaseLimitById(Long id) {
+        return fsStoreProductPurchaseLimitMapper.selectFsStoreProductPurchaseLimitById(id);
+    }
+
+    /**
+     * 查询商品限购列表
+     *
+     * @param fsStoreProductPurchaseLimit 商品限购
+     * @return 商品限购
+     */
+    @Override
+    public List<FsStoreProductPurchaseLimitScrm> selectFsStoreProductPurchaseLimitList(FsStoreProductPurchaseLimitScrm fsStoreProductPurchaseLimit) {
+        return fsStoreProductPurchaseLimitMapper.selectFsStoreProductPurchaseLimitList(fsStoreProductPurchaseLimit);
+    }
+
+    /**
+     * 根据商品ID和用户ID查询限购记录
+     *
+     * @param productId 商品ID
+     * @param userId    用户ID
+     * @return 商品限购
+     */
+    @Override
+    public FsStoreProductPurchaseLimitScrm selectByProductIdAndUserId(Long productId, Long userId) {
+        String key = "live:order:limit:" + productId + ":" + userId;
+        FsStoreProductPurchaseLimitScrm scrm = redisCacheT.getCacheObject(key);
+        if(scrm != null){
+            return scrm;
+        }
+        scrm = fsStoreProductPurchaseLimitMapper.selectByProductIdAndUserId(productId, userId);
+        redisCacheT.setCacheObject(key, scrm);
+        return scrm;
+    }
+
+    /**
+     * 新增商品限购
+     *
+     * @param fsStoreProductPurchaseLimit 商品限购
+     * @return 结果
+     */
+    @Override
+    public int insertFsStoreProductPurchaseLimit(FsStoreProductPurchaseLimitScrm fsStoreProductPurchaseLimit) {
+        fsStoreProductPurchaseLimit.setCreateTime(DateUtils.getNowDate());
+        return fsStoreProductPurchaseLimitMapper.insertFsStoreProductPurchaseLimit(fsStoreProductPurchaseLimit);
+    }
+
+    /**
+     * 修改商品限购
+     *
+     * @param fsStoreProductPurchaseLimit 商品限购
+     * @return 结果
+     */
+    @Override
+    public int updateFsStoreProductPurchaseLimit(FsStoreProductPurchaseLimitScrm fsStoreProductPurchaseLimit) {
+        fsStoreProductPurchaseLimit.setUpdateTime(DateUtils.getNowDate());
+        return fsStoreProductPurchaseLimitMapper.updateFsStoreProductPurchaseLimit(fsStoreProductPurchaseLimit);
+    }
+
+    /**
+     * 批量删除商品限购
+     *
+     * @param ids 需要删除的商品限购ID
+     * @return 结果
+     */
+    @Override
+    public int deleteFsStoreProductPurchaseLimitByIds(Long[] ids) {
+        return fsStoreProductPurchaseLimitMapper.deleteFsStoreProductPurchaseLimitByIds(ids);
+    }
+
+    /**
+     * 删除商品限购信息
+     *
+     * @param id 商品限购ID
+     * @return 结果
+     */
+    @Override
+    public int deleteFsStoreProductPurchaseLimitById(Long id) {
+        return fsStoreProductPurchaseLimitMapper.deleteFsStoreProductPurchaseLimitById(id);
+    }
+
+    /**
+     * 增加用户限购数量
+     *
+     * @param productId 商品ID
+     * @param userId    用户ID
+     * @param num       增加的数量
+     * @return 结果
+     */
+    @Override
+    public int increasePurchaseLimit(Long productId, Long userId, Integer num) {
+        String cacheKey = String.format(LiveKeysConstant.PRODUCT_DETAIL_CACHE, productId);
+        Map<String, Object> cachedData = redisCache.getCacheObject(cacheKey);
+
+        FsStoreProductScrm product;
+        List<FsStoreProductAttrScrm> productAttr;
+        List<FsStoreProductAttrValueScrm> productValues;
+
+        if (cachedData != null) {
+            // 从缓存中获取数据
+            product = (FsStoreProductScrm) cachedData.get("product");
+        } else {
+            // 缓存中没有,从数据库查询
+            product = fsStoreProductScrmMapper.selectFsStoreProductById(productId);
+            if (product == null) {
+                return -1;
+            }
+            productAttr = fsStoreProductAttrScrmMapper.selectFsStoreProductAttrByProductId(productId);
+            productValues = fsStoreProductAttrValueScrmMapper.selectFsStoreProductAttrValueByProductId(productId);
+
+            // 将数据存入缓存
+            Map<String, Object> cacheData = new HashMap<>();
+            cacheData.put("product", product);
+            cacheData.put("productAttr", productAttr);
+            cacheData.put("productValues", productValues);
+            redisCache.setCacheObject(cacheKey, cacheData, LiveKeysConstant.PRODUCT_DETAIL_CACHE_EXPIRE, TimeUnit.SECONDS);
+        }
+
+        if (product != null && product.getPurchaseLimit() != null && product.getPurchaseLimit() > 0) {
+            FsStoreProductPurchaseLimitScrm limit = selectByProductIdAndUserId(productId, userId);
+            String key = "live:order:limit:" + productId + ":" + userId;
+            if (limit == null) {
+                // 创建新记录
+                limit = new FsStoreProductPurchaseLimitScrm();
+                limit.setProductId(productId);
+                limit.setUserId(userId);
+                limit.setNum(num);
+                redisCacheT.setCacheObject(key, limit);
+                return insertFsStoreProductPurchaseLimit(limit);
+            } else {
+                // 更新现有记录
+                limit.setNum(limit.getNum() + num);
+                redisCacheT.setCacheObject(key, limit);
+                return updateFsStoreProductPurchaseLimit(limit);
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * 减少用户限购数量
+     *
+     * @param productId 商品ID
+     * @param userId    用户ID
+     * @param num       减少的数量
+     * @return 结果
+     */
+    @Override
+    public int decreasePurchaseLimit(Long productId, Long userId, Integer num) {
+        String cacheKey = String.format(LiveKeysConstant.PRODUCT_DETAIL_CACHE, productId);
+        Map<String, Object> cachedData = redisCache.getCacheObject(cacheKey);
+
+        FsStoreProductScrm product;
+        List<FsStoreProductAttrScrm> productAttr;
+        List<FsStoreProductAttrValueScrm> productValues;
+
+        if (cachedData != null) {
+            // 从缓存中获取数据
+            product = (FsStoreProductScrm) cachedData.get("product");
+        } else {
+            // 缓存中没有,从数据库查询
+            product = fsStoreProductScrmMapper.selectFsStoreProductById(productId);
+            if (product == null) {
+                return -1;
+            }
+            productAttr = fsStoreProductAttrScrmMapper.selectFsStoreProductAttrByProductId(productId);
+            productValues = fsStoreProductAttrValueScrmMapper.selectFsStoreProductAttrValueByProductId(productId);
+
+            // 将数据存入缓存
+            Map<String, Object> cacheData = new HashMap<>();
+            cacheData.put("product", product);
+            cacheData.put("productAttr", productAttr);
+            cacheData.put("productValues", productValues);
+            redisCache.setCacheObject(cacheKey, cacheData, LiveKeysConstant.PRODUCT_DETAIL_CACHE_EXPIRE, TimeUnit.SECONDS);
+        }
+        if (product != null && product.getPurchaseLimit() != null && product.getPurchaseLimit() > 0) {
+            FsStoreProductPurchaseLimitScrm limit = selectByProductIdAndUserId(productId, userId);
+            if (limit != null) {
+                int newNum = limit.getNum() - num;
+                if (newNum <= 0) {
+                    // 如果数量为0或负数,删除记录
+                    return deleteFsStoreProductPurchaseLimitById(limit.getId());
+                } else {
+                    // 更新数量
+                    limit.setNum(newNum);
+                    return updateFsStoreProductPurchaseLimit(limit);
+                }
+            }
+            return 0;
+        }
+        return -1;
+    }
+}
+

+ 2 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java

@@ -59,6 +59,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import com.fs.hisStore.service.IFsStoreProductScrmService;
@@ -121,6 +122,7 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
     private FsStoreProductCategoryScrmMapper fsStoreProductCategoryScrmMapper;
 
     @Autowired
+    @Lazy
     private ILiveService liveService;
 
     @Autowired

+ 24 - 17
fs-service/src/main/java/com/fs/huifuPay/sdk/opps/core/utils/HuiFuUtils.java

@@ -6,28 +6,22 @@ import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.spring.SpringUtils;
 import com.fs.company.domain.CompanyDivItem;
-import com.fs.company.mapper.CompanyDivConfigMapper;
 import com.fs.company.param.CompanyAcctInfo;
 import com.fs.company.service.ICompanyDivConfigService;
 import com.fs.company.service.ICompanyDivItemService;
-import com.fs.company.service.impl.CompanyDivConfigServiceImpl;
 import com.fs.company.vo.CompanyDivConfigVo;
-import com.fs.his.domain.FsPackageOrder;
-import com.fs.his.domain.FsPayConfig;
-import com.fs.his.domain.FsStoreOrder;
-import com.fs.his.domain.FsStorePayment;
+import com.fs.course.domain.FsCoursePlaySourceConfig;
+import com.fs.course.mapper.FsCoursePlaySourceConfigMapper;
+import com.fs.his.domain.*;
 import com.fs.his.mapper.FsStorePaymentMapper;
+import com.fs.his.mapper.MerchantAppConfigMapper;
 import com.fs.hisStore.domain.FsStorePaymentScrm;
 import com.fs.hisStore.mapper.FsStorePaymentScrmMapper;
 import com.fs.huifuPay.domain.HuiFuConfirmOrder;
 import com.fs.huifuPay.domain.HuiFuCreateOrder;
 import com.fs.huifuPay.domain.HuiFuQueryOrderResult;
 import com.fs.huifuPay.service.HuiFuService;
-import com.fs.system.domain.SysConfig;
-import com.fs.system.mapper.SysConfigMapper;
-import com.google.gson.Gson;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
@@ -46,12 +40,19 @@ public class HuiFuUtils {
      *
      * @param huiFuCreateOrder
      * @param companyId
+     * @param merConfigId
      */
-    public static void doDiv(HuiFuCreateOrder huiFuCreateOrder, Long companyId) throws Exception {
+    public static void doDiv(HuiFuCreateOrder huiFuCreateOrder, Long companyId, Long merConfigId) throws Exception {
         //默认汇付账户
-        SysConfigMapper sysConfigMapper = SpringUtils.getBean(SysConfigMapper.class);
-        SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey("his.pay");
-        FsPayConfig fsPayConfig = new Gson().fromJson(sysConfig.getConfigValue(), FsPayConfig.class);
+        FsCoursePlaySourceConfigMapper fsCoursePlaySourceConfigMapper = SpringUtils.getBean(FsCoursePlaySourceConfigMapper.class);
+        MerchantAppConfigMapper merchantAppConfigMapper = SpringUtils.getBean(MerchantAppConfigMapper.class);
+        FsCoursePlaySourceConfig fsCoursePlaySourceConfig = fsCoursePlaySourceConfigMapper.selectCoursePlaySourceConfigByAppId(huiFuCreateOrder.getAppId());
+        Long merConfigIds = fsCoursePlaySourceConfig.getMerchantConfigId();
+        if(merConfigId!=null){
+            merConfigIds=merConfigId;
+        }
+        MerchantAppConfig merchantAppConfig = merchantAppConfigMapper.selectMerchantAppConfigById(merConfigIds);
+        FsPayConfig fsPayConfig = JSON.parseObject(merchantAppConfig.getDataJson(), FsPayConfig.class);
         String defaultHuifuId = fsPayConfig.getHuifuId();
         //查询是否开启分账
         if (companyId != null) {
@@ -230,9 +231,15 @@ public class HuiFuUtils {
             if (payPrice.compareTo(reMoney) > 0) {
                 //部分退款
                 if (companyDivItem != null) {
-                    SysConfigMapper sysConfigMapper = SpringUtils.getBean(SysConfigMapper.class);
-                    SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey("his.pay");
-                    FsPayConfig fsPayConfig = new Gson().fromJson(sysConfig.getConfigValue(), FsPayConfig.class);
+                    FsCoursePlaySourceConfigMapper fsCoursePlaySourceConfigMapper = SpringUtils.getBean(FsCoursePlaySourceConfigMapper.class);
+                    MerchantAppConfigMapper merchantAppConfigMapper = SpringUtils.getBean(MerchantAppConfigMapper.class);
+                    FsCoursePlaySourceConfig fsCoursePlaySourceConfig = fsCoursePlaySourceConfigMapper.selectCoursePlaySourceConfigByAppId(payment.getAppId());
+                    Long merConfigId = fsCoursePlaySourceConfig.getMerchantConfigId();
+                    if(payment.getMerchantId()!=null){
+                        merConfigId=payment.getMerchantId();
+                    }
+                    MerchantAppConfig merchantAppConfig = merchantAppConfigMapper.selectMerchantAppConfigById(merConfigId);
+                    FsPayConfig fsPayConfig = JSON.parseObject(merchantAppConfig.getDataJson(), FsPayConfig.class);
                     String defaultHuiFuId = fsPayConfig.getHuifuId(); //默认汇付id
 
                     String detail = companyDivItem.getDetail();

+ 6 - 0
fs-service/src/main/java/com/fs/live/domain/Live.java

@@ -131,6 +131,12 @@ public class   Live extends BaseEntity {
     private Long videoDuration;
     private Integer globalVisible;
 
+    private String roomPassword;
+
+    // 是否需要秘钥
+    @TableField(exist = false)
+    private Boolean isNeedPassword;
+
     @TableField(exist = false)
     private List<LiveTagItemVO> liveTagList;
 }

+ 3 - 1
fs-service/src/main/java/com/fs/live/domain/LiveCouponUser.java

@@ -73,5 +73,7 @@ public class LiveCouponUser extends BaseEntity
     @Excel(name = "商品ID")
     private Long goodsId;
 
-
+    //非数据库实体 只是返回
+    private String nickname;
+    private String phone;
 }

+ 7 - 2
fs-service/src/main/java/com/fs/live/domain/LiveGoods.java

@@ -1,8 +1,12 @@
 package com.fs.live.domain;
 
 
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
+import com.fs.common.core.domain.BaseEntityTow;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 
@@ -13,10 +17,10 @@ import lombok.EqualsAndHashCode;
  * @date 2025-07-08
  */
 @Data
-@EqualsAndHashCode(callSuper = true)
 public class LiveGoods extends BaseEntity{
 
     /** ID */
+    @TableId(type = IdType.AUTO)
     private Long goodsId;
 
     /** 直播ID */
@@ -60,8 +64,9 @@ public class LiveGoods extends BaseEntity{
     private Long sort;
 
     /** 商品名称搜索关键字*/
-
+    @TableField(exist = false)
     private String keywords;
+    @TableField(exist = false)
     private String productName;
 
 

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

@@ -118,4 +118,6 @@ public class LiveOrderPayment extends BaseEntity{
 
     //小程序appId(用于多汇付支付/退款)
     private String appId;
+
+    private Long merConfigId;
 }

+ 49 - 0
fs-service/src/main/java/com/fs/live/domain/LiveSignRecord.java

@@ -0,0 +1,49 @@
+package com.fs.live.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 直播签到记录对象 live_sign_record
+ *
+ * @author ylrz
+ * @date 2026-04-07
+ */
+@Data
+@TableName("live_sign_record")
+public class LiveSignRecord implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 直播ID */
+    @Excel(name = "直播ID")
+    private Long liveId;
+
+    /** 用户ID */
+    @Excel(name = "用户ID")
+    private Long userId;
+
+    /** 用户名称 */
+    @Excel(name = "用户名称")
+    private String userName;
+
+    /** 签到序号(第几次签到) */
+    @Excel(name = "签到序号")
+    private String signNo;
+
+    /** 签到时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "签到时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+}
+

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

@@ -91,4 +91,6 @@ public interface LiveAutoTaskMapper {
 
     @Select("select * from live_auto_task where live_id= #{liveId} and abs_value > #{nowDate} and task_type in (1,2,4) and finish_status = 1 order by abs_value ")
     List<LiveAutoTask> consoleList(@Param("liveId") Long liveId,@Param("nowDate") Date nowDate);
+
+    LiveAutoTask selectLastSignTaskByLiveId(Long liveId);
 }

+ 9 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveCompletionPointsRecordMapper.java

@@ -1,5 +1,7 @@
 package com.fs.live.mapper;
 
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
 import com.fs.live.domain.LiveCompletionPointsRecord;
 import org.apache.ibatis.annotations.Param;
 
@@ -33,6 +35,13 @@ public interface LiveCompletionPointsRecordMapper {
      */
     LiveCompletionPointsRecord selectLatestByUser(@Param("userId") Long userId);
 
+    /**
+     * 查询用户在某直播间最近一次完课记录(不限制日期)
+     */
+    @DataSource(DataSourceType.SLAVE)
+    LiveCompletionPointsRecord selectLatestByUserAndLiveId(@Param("liveId") Long liveId, 
+                                                            @Param("userId") Long userId);
+
     /**
      * 查询用户未领取的完课记录列表
      */

+ 7 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveCouponUserMapper.java

@@ -1,6 +1,9 @@
 package com.fs.live.mapper;
 
 import java.util.List;
+
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
 import com.fs.live.domain.LiveCouponUser;
 import com.fs.live.param.CouponPO;
 import org.apache.ibatis.annotations.Param;
@@ -20,6 +23,7 @@ public interface LiveCouponUserMapper
      * @param id 优惠券发放记录ID
      * @return 优惠券发放记录
      */
+    @DataSource(DataSourceType.SLAVE)
     public LiveCouponUser selectLiveCouponUserById(Long id);
 
     /**
@@ -71,5 +75,8 @@ public interface LiveCouponUserMapper
             " and (lcu.goods_id= #{coupon.goodsId} or lcu.goods_id is null or lcu.goods_id = 0)" +
             " </if>" +
             "</script>")
+    @DataSource(DataSourceType.SLAVE)
     List<LiveCouponUser> curCoupon(@Param("coupon") CouponPO coupon);
+
+    int refundCoupon(Long couponUserId);
 }

+ 9 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveDataMapper.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.LiveData;
 import com.fs.live.vo.LiveDashBoardDataVo;
 import com.fs.live.vo.LiveDataDetailVo;
@@ -140,6 +142,7 @@ public interface LiveDataMapper {
             "FROM " +
             "    live_data ld " +
             "where ld.live_id=#{liveId}")
+    @DataSource(DataSourceType.SLAVE)
     Map<String, Integer> selectDashboardCount(@Param("liveId") Long liveId);
 
     /**
@@ -147,6 +150,7 @@ public interface LiveDataMapper {
      * @param liveIds 直播间ID列表
      * @return 统计数据
      */
+    @DataSource(DataSourceType.SLAVE)
     LiveDataStatisticsVo selectLiveDataStatistics(@Param("liveIds") List<Long> liveIds);
 
     /**
@@ -154,6 +158,7 @@ public interface LiveDataMapper {
      * @param liveIds 直播间ID列表
      * @return 列表数据
      */
+    @DataSource(DataSourceType.SLAVE)
     List<LiveDataListVo> selectLiveDataListByLiveIds(@Param("liveIds") List<Long> liveIds);
 
     /**
@@ -161,6 +166,7 @@ public interface LiveDataMapper {
      * @param liveId 直播间ID
      * @return 详情数据
      */
+    @DataSource(DataSourceType.SLAVE)
     LiveDataDetailVo selectLiveDataDetailBySql(@Param("liveId") Long liveId);
 
     /**
@@ -168,5 +174,8 @@ public interface LiveDataMapper {
      * @param liveId 直播间ID
      * @return 用户详情列表
      */
+    @DataSource(DataSourceType.SLAVE)
     List<LiveUserDetailVo> selectLiveUserDetailListBySql(@Param("liveId") Long liveId,@Param("companyId") Long companyId,@Param("companyUserId") Long companyUserId);
+
+    List<LiveUserDetailVo> selectLiveUserDetailListBySqlNew(Long liveId);
 }

+ 8 - 1
fs-service/src/main/java/com/fs/live/mapper/LiveGoodsMapper.java

@@ -1,5 +1,8 @@
 package com.fs.live.mapper;
 
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
 import com.fs.live.domain.LiveGoods;
 import com.fs.live.domain.LiveOrder;
 import com.fs.live.vo.LiveGoodsListVo;
@@ -17,7 +20,7 @@ import java.util.Map;
  * @author fs
  * @date 2025-07-08
  */
-public interface LiveGoodsMapper {
+public interface LiveGoodsMapper extends BaseMapper<LiveGoods> {
     /**
      * 查询直播商品
      *
@@ -90,6 +93,7 @@ public interface LiveGoodsMapper {
      * @param liveGoods 直播商品
      * @return 商品信息集合
      */
+    @DataSource(DataSourceType.SLAVE)
     List<LiveGoodsVo> selectProductListByLiveId(LiveGoods liveGoods);
 
     /**
@@ -112,6 +116,7 @@ public interface LiveGoodsMapper {
     List<LiveGoodsVo> selectProductListByOrder(LiveOrder liveOrder);
 
     @Select("select * from live_goods where live_id = #{liveId} and product_id = #{productId}")
+    @DataSource(DataSourceType.SLAVE)
     LiveGoods selectLiveGoodsByProductId(@Param("liveId") Long liveId,@Param("productId") Long productId);
 
     /**
@@ -157,4 +162,6 @@ public interface LiveGoodsMapper {
      */
     @Update("update live_goods set status = #{status} where goods_id = #{goodsId}")
     void updateLiveGoodsStatus(@Param("goodsId") Long goodsId, @Param("status") Integer status);
+
+    int updateStock(@Param("goodsId") Long goodsId, @Param("goodsNum") Integer goodsNum);
 }

+ 3 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveMapper.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.Live;
 import com.fs.live.param.LiveDataParam;
 import com.fs.live.vo.LiveListVo;
@@ -24,6 +26,7 @@ public interface LiveMapper
      * @param liveId 直播主键
      * @return 直播
      */
+    @DataSource(DataSourceType.SLAVE)
     public Live selectLiveByLiveId(Long liveId);
 
     /**

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

@@ -84,4 +84,12 @@ public interface LiveMsgMapper
     Map<String, BigDecimal> selectDashboardCount(@Param("liveId") Long liveId);
 
     List<LiveMsg> selectLiveMsgSingleList(LiveMsg liveMsg);
+
+    /**
+     * 查询直播评论用于导出
+     *
+     * @param liveId 直播ID
+     * @return 评论列表
+     */
+    List<com.fs.live.vo.LiveMsgExportVO> selectLiveMsgForExport(@Param("liveId") Long liveId);
 }

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

@@ -41,6 +41,7 @@ public interface LiveOrderItemMapper {
      * @return 结果
      */
     int insertLiveOrderItem(LiveOrderItem liveOrderItem);
+    int insertLiveOrderItemTest(LiveOrderItem liveOrderItem);
 
     /**
      * 修改订单详情

+ 20 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveOrderMapper.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.hisStore.vo.FsStoreOrderItemExportZMVO;
 import com.fs.live.domain.LiveOrder;
 import com.fs.live.dto.LiveOrderDeliveryNoteDTO;
@@ -41,6 +43,7 @@ public interface LiveOrderMapper {
      * @param liveOrder 订单
      * @return 订单集合
      */
+    @DataSource(DataSourceType.SLAVE)
     List<LiveOrder> selectLiveOrderList(LiveOrder liveOrder);
 
     /**
@@ -50,6 +53,7 @@ public interface LiveOrderMapper {
      * @return 结果
      */
     int insertLiveOrder(LiveOrder liveOrder);
+    int insertLiveOrderTest(LiveOrder liveOrder);
 
     /**
      * 修改订单
@@ -103,14 +107,17 @@ public interface LiveOrderMapper {
             "</where> " +
             "order by create_time desc" +
             "</script>"})
+    @DataSource(DataSourceType.SLAVE)
     List<LiveOrderListVo> selectLiveOrderListVo(@Param("userId") String userId,@Param("status") Integer status);
 
     @Select("select * from live_order where `status` = 3 AND TIMESTAMPDIFF(HOUR, start_time, NOW()) >= 48  ")
     List<LiveOrder> selectLiveOrderByFinish();
 
     @Select("select * from live_order where `status` = 1 and extend_order_id is not null and (delivery_sn is null or delivery_code = '') and refund_status = 0 and is_pay = 1 order by update_time ")
+    @DataSource(DataSourceType.SLAVE)
     List<LiveOrder> selectUpdateExpress();
 
+
     @Select("select order_id from live_order where `status` = 2 and delivery_code is not null and delivery_sn is not null and is_pay = 1")
     List<Long> selectSyncExpressIds();
 
@@ -159,6 +166,13 @@ public interface LiveOrderMapper {
     String selectLiveOrderProductStatistics(@Param("maps")LiveOrderParam param);
 
     List<LiveOrder> selectLiveOrderInId(@Param("ids") Long[] ids);
+
+    /**
+     * 根据订单ID列表批量查询订单
+     * @param orderIds 订单ID列表
+     * @return 订单列表
+     */
+    List<LiveOrder> selectLiveOrderByOrderIds(@Param("orderIds") List<Long> orderIds);
     @Select({"<script> " +
             "select o.*,cts.name as scheduleName,u.nickname,u.phone,cc.push_code,cc.create_time as customer_create_time," +
             "cc.source,cc.customer_code, c.company_name ,cu.nick_name as company_user_nick_name ," +
@@ -473,6 +487,12 @@ public interface LiveOrderMapper {
      */
     void batchUpdateInOrderCode(@Param("list") List<LiveOrderDeliveryNoteDTO> dtoList);
 
+    /**
+     * 批量更新订单channel字段
+     * @param maps 订单ID和channel的映射列表,每个map包含orderId和channel
+     */
+    void batchUpdateChannelByOrderIds(@Param("maps") List<Map<String, Object>> maps);
+
     @Select("SELECT * FROM live_order WHERE user_id=#{userId} LIMIT 1")
     LiveOrder selectOrderByUserIdLimit1(@Param("userId") Long userId);
 

+ 11 - 1
fs-service/src/main/java/com/fs/live/mapper/LiveOrderPaymentMapper.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.LiveOrderPayment;
 import com.fs.live.vo.LiveOrderPaymentVo;
 import org.apache.ibatis.annotations.Param;
@@ -22,6 +24,7 @@ public interface LiveOrderPaymentMapper {
      * @param paymentId 支付明细主键
      * @return 支付明细
      */
+    @DataSource(DataSourceType.SLAVE)
     LiveOrderPayment selectLiveOrderPaymentByPaymentId(Long paymentId);
 
     /**
@@ -67,7 +70,7 @@ public interface LiveOrderPaymentMapper {
     @Select("select * from live_order_payment where pay_code=#{payCode}")
     LiveOrderPayment selectLiveOrderPaymentByPaymentCode(String payCode);
 
-    @Select("select * from live_order_payment where business_type=#{type} and  business_id=#{businessId} and status=1")
+    @Select("select * from live_order_payment where (business_type=#{type} or business_type=9)  and  business_id=#{businessId} and status=1")
     List<LiveOrderPayment> selectLiveOrderPaymentByPay(@Param("type")int type, @Param("businessId") Long businessId);
 
     @Select("select * from live_order_payment where   business_id=#{businessId} and status=1")
@@ -82,4 +85,11 @@ public interface LiveOrderPaymentMapper {
 
     @Select("select * from live_order_payment where business_id= #{orderId} and status=1 order by create_time desc limit 1")
     LiveOrderPayment selectLiveOrderLatestPayByOrderId(@Param("orderId") Long orderId);
+
+    /**
+     * 根据交易单号列表批量查询支付记录
+     * @param bankTransactionIds 交易单号列表
+     * @return 支付记录列表
+     */
+    List<LiveOrderPayment> selectByBankTransactionIds(@Param("bankTransactionIds") List<String> bankTransactionIds);
 }

+ 17 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveSignRecordMapper.java

@@ -0,0 +1,17 @@
+package com.fs.live.mapper;
+
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.live.domain.LiveSignRecord;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 直播签到记录Mapper接口
+ *
+ * @author ylrz
+ * @date 2026-04-07
+ */
+@Mapper
+public interface LiveSignRecordMapper extends BaseMapper<LiveSignRecord> {
+
+}

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

@@ -83,4 +83,6 @@ public interface LiveTrafficLogMapper
     List<LiveTrafficLogListVO> selectTrafficByCompany(@Param("maps") LiveTrafficLogParam param);
 
     void insertOrUpdateLiveTrafficLog(LiveTrafficLog trafficLog);
+
+    void batchInsert(@Param("list") List<LiveTrafficLog> list);
 }

+ 6 - 1
fs-service/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.LiveDashBoardDataVo;
 import com.fs.live.vo.LiveUserFirstProfit;
@@ -71,10 +73,11 @@ public interface LiveUserFirstEntryMapper {
 
     @Select("select count(*) from live_user_first_entry where user_id=#{userId} and DATE(entry_date)=DATE(#{now})")
     int selectTodayEntry(@Param("userId") long userId,@Param("now") Date now);
-
+    @DataSource(DataSourceType.SLAVE)
     List<LiveUserFirstProfit> selectLiveProfitList();
 
     @Select("select * from live_user_first_entry where live_id=#{liveId} and user_id=#{userId}")
+    @DataSource(DataSourceType.SLAVE)
     LiveUserFirstEntry selectEntityByLiveIdUserId(@Param("liveId") long liveId,@Param("userId") long userId);
 
     @Select("SELECT  " +
@@ -90,6 +93,7 @@ public interface LiveUserFirstEntryMapper {
             "ORDER BY  " +
             "  invite_num DESC   " +
             "  LIMIT 10")
+    @DataSource(DataSourceType.SLAVE)
     List<LiveUserFirstVo> selectDashboardInviteCount(@Param("liveId") Long liveId);
 
     @Select("SELECT  " +
@@ -104,6 +108,7 @@ public interface LiveUserFirstEntryMapper {
             "  lufe.company_user_id   " +
             "ORDER BY  " +
             "  invite_num DESC   ")
+    @DataSource(DataSourceType.SLAVE)
     List<LiveUserFirstVo> inviteList(@Param("liveId") Long liveId);
 
     @Select("SELECT  sum(case when company_user_id > 0 then 1 else 0 end ) as shareUrlNum, " +

+ 3 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveVideoMapper.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.LiveVideo;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
@@ -83,6 +85,7 @@ public interface LiveVideoMapper
     List<LiveVideo> selectByLiveId(@Param("liveId")Long liveId);
 
     @Select("select * from live_video where live_id = #{liveId} and video_type = #{videoType}")
+    @DataSource(DataSourceType.SLAVE)
     List<LiveVideo> selectByLiveIdAndType(@Param("liveId")Long liveId,@Param("videoType") Integer videoType);
 
     @Select("select * from live_video where live_id = #{liveId}")

+ 8 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveWatchLogMapper.java

@@ -72,6 +72,14 @@ public interface LiveWatchLogMapper extends BaseMapper<LiveWatchLog> {
 
     List<LiveWatchLog> selectLiveWatchLogByLiveId(@Param("liveId")Long liveId);
 
+    /**
+     * 根据直播间ID和外部联系人ID查询看课记录
+     * @param liveId 直播间ID
+     * @param externalContactId 外部联系人ID
+     * @return 看课记录
+     */
+    LiveWatchLog selectLiveWatchLogByLiveIdAndExternalId(@Param("liveId")Long liveId, @Param("externalContactId")Long externalContactId);
+
     List<LiveWatchLogListVO> selectLiveWatchLogListInfo(LiveWatchLog liveWatchLog);
 
     /**

+ 3 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveWatchUserMapper.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.LiveWatchUser;
 import com.fs.live.vo.LiveDashBoardDataVo;
 import com.fs.live.vo.LiveWatchUserEntry;
@@ -148,6 +150,7 @@ public interface LiveWatchUserMapper {
     @Select("select lufe.company_id,lufe.company_user_id,lwu.* from live_watch_user lwu" +
             " left join live_user_first_entry lufe on lwu.live_id = lufe.live_id and lwu.user_id = lufe.user_id" +
             " where lwu.live_id = #{liveId} and lwu.user_id = #{userId} and lwu.live_flag = #{liveFlag} and lwu.replay_flag = #{replayFlag} limit 1 ")
+    @DataSource(DataSourceType.SLAVE)
     LiveWatchUserEntry selectLiveWatchAndCompanyUserByFlag(@Param("liveId") Long liveId,@Param("userId") Long userId,@Param("liveFlag") Integer liveFlag,@Param("replayFlag") Integer replayFlag);
 
     /**

+ 6 - 0
fs-service/src/main/java/com/fs/live/param/MergedOrderQueryParam.java

@@ -120,5 +120,11 @@ public class MergedOrderQueryParam extends BaseQueryParam implements Serializabl
     private String erpPhoneNumber;
     /** 汇付商户订单号 */
     private String hfshh;
+    
+    /** 分页偏移量(在外部计算后传入,不在SQL中计算) */
+    private Integer offset;
+
+    /** 分页偏移量(在外部计算后传入,不在SQL中计算) */
+    private Integer exportFlag;
 }
 

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

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

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

@@ -167,4 +167,15 @@ public interface ILiveDataService {
      * @return 导出VO列表
      */
     List<LiveUserDetailExportVO> exportLiveUserDetail(Long liveId, Long companyId, Long companyUserId);
+
+    List<LiveDataListVo> exportLiveData(LiveDataParam param);
+
+    /**
+     * 获取直播间用户详情列表(SQL方式)
+     * @param liveId 直播间ID
+     * @return 用户详情列表
+     */
+    R getLiveUserDetailListBySqlNew(Long liveId);
+
+    List<LiveUserDetailExportVO> exportLiveUserDetailNew(Long liveId);
 }

+ 2 - 1
fs-service/src/main/java/com/fs/live/service/ILiveGoodsService.java

@@ -1,6 +1,7 @@
 package com.fs.live.service;
 
 
+import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.entity.SysUser;
 import com.fs.company.domain.CompanyUser;
@@ -17,7 +18,7 @@ import java.util.Map;
  * @author fs
  * @date 2025-07-08
  */
-public interface ILiveGoodsService {
+public interface ILiveGoodsService extends IService<LiveGoods> {
     /**
      * 查询直播商品
      *

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

@@ -69,4 +69,13 @@ public interface ILiveMsgService
     List<LiveMsg> listRecentMsg(Long id);
 
     List<LiveMsg> selectLiveMsgSingleList(LiveMsg liveMsg);
+
+    /**
+     * 导出直播评论
+     *
+     * @param liveId 直播ID
+     * @param userId 用户ID(用于Redis加锁)
+     * @return 评论列表
+     */
+    List<com.fs.live.vo.LiveMsgExportVO> exportLiveMsgComments(Long liveId, Long userId);
 }

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