yuhongqi 1 týždeň pred
rodič
commit
be3a374d86
37 zmenil súbory, kde vykonal 2214 pridanie a 440 odobranie
  1. 1 1
      fs-admin/src/main/java/com/fs/live/controller/LiveController.java
  2. 1 1
      fs-admin/src/main/java/com/fs/live/controller/LiveUserRedRecordController.java
  3. 24 0
      fs-common/src/main/java/com/fs/common/constant/LiveKeysConstant.java
  4. 42 0
      fs-common/src/main/java/com/fs/common/core/redis/RedisCache.java
  5. 13 1
      fs-company/src/main/java/com/fs/company/controller/live/LiveRedConfController.java
  6. 6 0
      fs-live-socket/pom.xml
  7. 13 5
      fs-live-socket/src/main/java/com/fs/live/controller/LiveController.java
  8. 12 17
      fs-live-socket/src/main/java/com/fs/live/task/Task.java
  9. 35 34
      fs-live-socket/src/main/java/com/fs/live/websocket/auth/AuthHandler.java
  10. 2 0
      fs-live-socket/src/main/java/com/fs/live/websocket/constant/AttrConstant.java
  11. 58 188
      fs-live-socket/src/main/java/com/fs/live/websocket/handle/LiveChatHandler.java
  12. 9 0
      fs-live-socket/src/main/java/com/fs/live/websocket/hub/ClientReply.java
  13. 37 0
      fs-live-socket/src/main/java/com/fs/live/websocket/hub/LiveSocketHub.java
  14. 143 0
      fs-live-socket/src/main/java/com/fs/live/websocket/hub/LiveSocketHubImpl.java
  15. 97 97
      fs-live-socket/src/main/java/com/fs/live/websocket/service/NettyServerRunner.java
  16. 38 32
      fs-live-socket/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  17. 6 0
      fs-live-socket/src/main/resources/application.yml
  18. 4 2
      fs-service-system/src/main/java/com/fs/erp/service/impl/FsJstAftersalePushServiceImpl.java
  19. 5 2
      fs-service-system/src/main/java/com/fs/live/domain/LiveUserRedRecord.java
  20. 67 0
      fs-service-system/src/main/java/com/fs/live/mapper/LiveOrderOptMapper.java
  21. 4 0
      fs-service-system/src/main/java/com/fs/live/mapper/LiveRedConfMapper.java
  22. 19 0
      fs-service-system/src/main/java/com/fs/live/service/ILiveOrderOptService.java
  23. 5 0
      fs-service-system/src/main/java/com/fs/live/service/ILiveService.java
  24. 13 0
      fs-service-system/src/main/java/com/fs/live/service/impl/LiveGoodsServiceImpl.java
  25. 695 0
      fs-service-system/src/main/java/com/fs/live/service/impl/LiveOrderOptServiceImpl.java
  26. 51 17
      fs-service-system/src/main/java/com/fs/live/service/impl/LiveRedConfServiceImpl.java
  27. 140 19
      fs-service-system/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
  28. 4 0
      fs-service-system/src/main/java/com/fs/live/vo/LiveConfigVo.java
  29. 223 0
      fs-service-system/src/main/resources/mapper/live/LiveOrderOptMapper.xml
  30. 12 5
      fs-service-system/src/main/resources/mapper/live/LiveUserRedRecordMapper.xml
  31. 88 0
      fs-user-app/src/main/java/com/fs/app/controller/cacheLive/CacheLiveController.java
  32. 28 0
      fs-user-app/src/main/java/com/fs/app/controller/cacheLive/CacheLiveDataController.java
  33. 28 0
      fs-user-app/src/main/java/com/fs/app/controller/cacheLive/CacheLiveGiftController.java
  34. 112 0
      fs-user-app/src/main/java/com/fs/app/controller/cacheLive/CacheLiveGoodsController.java
  35. 42 0
      fs-user-app/src/main/java/com/fs/app/controller/cacheLive/CacheLiveVideoController.java
  36. 66 0
      fs-user-app/src/main/java/com/fs/app/controller/cacheLive/LiveOrderOptController.java
  37. 71 19
      fs-user-app/src/main/java/com/fs/app/facade/impl/LiveFacadeServiceImpl.java

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

@@ -175,7 +175,7 @@ public class LiveController extends BaseController {
     @Log(title = "直播", businessType = BusinessType.UPDATE)
     @PostMapping("/clearCache/{liveId}")
     public R clearCache(@PathVariable("liveId") Long liveId) {
-        return liveService.clearLiveCache(liveId);
+        return liveService.clearLiveCacheOnStatusChange(liveId);
     }
 
     /**

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

@@ -30,7 +30,7 @@ public class LiveUserRedRecordController extends BaseController
     /**
      * 查询直播用户红包记录列表
      */
-    @PreAuthorize("@ss.hasPermi('live:liveUserRedRecord:list')")
+    // @PreAuthorize("@ss.hasPermi('live:liveUserRedRecord:list')")
     @GetMapping("/list")
     public TableDataInfo list(LiveUserRedRecord liveUserRedRecord)
     {

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

@@ -35,4 +35,28 @@ public class LiveKeysConstant {
     public static final String PRODUCT_DETAIL_CACHE = "product:detail:%s"; //商品详情缓存
     public static final Integer PRODUCT_DETAIL_CACHE_EXPIRE = 300; //商品详情缓存过期时间(秒)
 
+    public static final String LIVE_GOODS_LIST_CACHE = "live:goods:list:%s"; //直播商品列表缓存
+    public static final Integer LIVE_GOODS_LIST_CACHE_EXPIRE = 60; //直播商品列表缓存过期时间(秒)
+    public static final String LIVE_SHOW_GOODS_CACHE = "live:goods:show:%s"; //当前讲解商品缓存
+    public static final Integer LIVE_SHOW_GOODS_CACHE_EXPIRE = 30; //当前讲解商品缓存过期时间(秒)
+    public static final String LIVE_VIDEO_CACHE = "live:video:%s:%s"; //直播视频缓存 liveId:videoType
+    public static final Integer LIVE_VIDEO_CACHE_EXPIRE = 300; //直播视频缓存过期时间(秒)
+
+    /** 置顶消息完整 key:live:config:{liveId}:topMsg */
+    public static final String LIVE_TOP_MSG_CACHE = "live:config:%s:" + TOP_MSG;
+
+    public static final String LIVE_GIFT_ACTIVE_CACHE = "live:gift:active"; //礼物列表缓存
+    public static final String LIVE_AUTO_TASK = "live:auto_task:%s";
+    public static final String LIVE_RED_TASK = "live:red_task:%s";
+    public static final String LIVE_LOTTERY_TASK = "live:lottery_task:%s";
+    public static final String LIVE_LIKE = "live:like:%s";
+    public static final String LIVE_COUPON_LIMIT = "live:coupon:limit:%s:%s"; // liveId:couponIssueId
+    public static final String LIVE_RED_REMAINING_LOTS = "live:red:remainingLots:%s";
+    public static final String LIVE_RED_REMAINING_NUM = "live:red:remainingNum:%s";
+    public static final String LIVE_RED_CLAIM = "live:red:claim:%s";
+
+    /** 订单 Opt:直播存在性 / 直播商品 */
+    public static final String OPT_LIVE_EXISTS = "opt:live:order:live:exists:%s";
+    public static final String OPT_LIVE_GOODS = "opt:live:order:goods:%s:%s";
+
 }

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

@@ -124,6 +124,48 @@ public class RedisCache
         return redisTemplate.delete(collection);
     }
 
+    /**
+     * 使用 SCAN 按模式删除 key(非阻塞,避免 KEYS)
+     *
+     * @param pattern Redis 匹配模式,如 live:goods:list:123:*
+     * @return 删除数量
+     */
+    public long deleteByPattern(final String pattern) {
+        if (pattern == null || pattern.isEmpty()) {
+            return 0L;
+        }
+        long deleted = 0L;
+        try {
+            Set<String> keys = (Set<String>) redisTemplate.execute((org.springframework.data.redis.core.RedisCallback<Set<String>>) connection -> {
+                Set<String> result = new java.util.HashSet<>();
+                try (org.springframework.data.redis.core.Cursor<byte[]> cursor = connection.scan(
+                        ScanOptions.scanOptions().match(pattern).count(200).build())) {
+                    while (cursor.hasNext()) {
+                        result.add(new String(cursor.next(), java.nio.charset.StandardCharsets.UTF_8));
+                    }
+                } catch (Exception e) {
+                    throw new RuntimeException(e);
+                }
+                return result;
+            });
+            if (keys != null && !keys.isEmpty()) {
+                Long count = redisTemplate.delete(keys);
+                deleted = count == null ? 0L : count;
+            }
+        } catch (Exception e) {
+            // 降级:少量环境 SCAN 异常时尝试 keys(仍尽量清掉)
+            try {
+                Set<String> keys = redisTemplate.keys(pattern);
+                if (keys != null && !keys.isEmpty()) {
+                    Long count = redisTemplate.delete(keys);
+                    deleted = count == null ? 0L : count;
+                }
+            } catch (Exception ignored) {
+            }
+        }
+        return deleted;
+    }
+
     /**
      * 缓存List数据
      *

+ 13 - 1
fs-company/src/main/java/com/fs/company/controller/live/LiveRedConfController.java

@@ -9,7 +9,9 @@ import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.core.security.SecurityUtils;
 import com.fs.live.domain.LiveRedConf;
+import com.fs.live.domain.LiveUserRedRecord;
 import com.fs.live.service.ILiveRedConfService;
+import com.fs.live.service.ILiveUserRedRecordService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
@@ -28,6 +30,8 @@ public class LiveRedConfController extends BaseController
 {
     @Autowired
     private ILiveRedConfService liveRedConfService;
+    @Autowired
+    private ILiveUserRedRecordService liveUserRedRecordService;
 
     /**
      * 查询直播红包记录配置列表
@@ -121,6 +125,14 @@ public class LiveRedConfController extends BaseController
         return liveRedConfService.start(redId, SecurityUtils.getLoginUser().getUser().getUserId());
     }
 
-
+    /**
+     * 查询红包领取记录
+     */
+    @GetMapping("/claimRecords")
+    public TableDataInfo claimRecords(LiveUserRedRecord liveUserRedRecord) {
+        startPage();
+        List<LiveUserRedRecord> list = liveUserRedRecordService.selectLiveUserRedRecordList(liveUserRedRecord);
+        return getDataTable(list);
+    }
 
 }

+ 6 - 0
fs-live-socket/pom.xml

@@ -115,6 +115,12 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-websocket</artifactId>
         </dependency>
+        <!-- Netty WebSocket 显式依赖(7116 端口) -->
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-all</artifactId>
+            <version>4.1.68.Final</version>
+        </dependency>
 
     </dependencies>
 

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

@@ -17,7 +17,7 @@ import com.fs.live.service.ILiveMsgService;
 import com.fs.live.service.ILiveService;
 import com.fs.live.service.ILiveVideoService;
 import com.fs.live.websocket.bean.SendMsgVo;
-import com.fs.live.websocket.service.WebSocketServer;
+import com.fs.live.websocket.hub.LiveSocketHub;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 import io.swagger.annotations.Api;
@@ -47,17 +47,21 @@ public class LiveController {
 	@Autowired
 	private ILiveVideoService videoService;
 	@Autowired
-	private WebSocketServer webSocketServer;
+	private LiveSocketHub liveSocketHub;
 
 
 	// 直播开播回调
 	@PostMapping("/startLiving")
 	public R checkLiving(HttpServletRequest request, @RequestBody  Map<String, String> params) {
 		log.info("请求参数:{}", params);
+		Long liveId = Long.valueOf(params.get("stream_id"));
 		SendMsgVo sendMsgVo = new SendMsgVo();
 		sendMsgVo.setMsg("开始直播");
 		sendMsgVo.setCmd("live_start");
-		webSocketServer.broadcastMessage(Long.valueOf(params.get("stream_id")), JSONObject.toJSONString(R.ok().put("data",sendMsgVo)));
+		liveSocketHub.broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data",sendMsgVo)));
+		// 推流开播:只清展示/cacheLive 缓存,保留已加载的 auto_task 与活动运行时
+		liveService.clearLiveCache(liveId);
+		liveService.asyncToCache();
 		return R.ok();
 //		{app=200149.push.tlivecloud.com, appid=1319721001, appname=live, channel_id=667, errcode=0,
 //		errmsg=, event_time=1753840982, event_type=1,
@@ -70,15 +74,19 @@ public class LiveController {
 	@PostMapping("/endLiving")
 	public R endLiving(HttpServletRequest request, @RequestBody  Map<String, String> params) {
 		log.info("请求参数:{}", params);
+		Long liveId = Long.valueOf(params.get("stream_id"));
 		SendMsgVo sendMsgVo = new SendMsgVo();
 		sendMsgVo.setMsg("结束直播");
 		sendMsgVo.setCmd("live_end");
-		webSocketServer.broadcastMessage(Long.valueOf(params.get("stream_id")), JSONObject.toJSONString(R.ok().put("data",sendMsgVo)));
+		liveSocketHub.broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data",sendMsgVo)));
 		Live live = new Live();
-		live.setLiveId(Long.valueOf(params.get("stream_id")));
+		live.setLiveId(liveId);
 		live.setStatus(3);
 		live.setFinishTime(LocalDateTime.now());
 		liveService.updateLiveEntity(live);
+		// updateLiveEntity 已清展示缓存;关播再清活动运行时 + 重建首页
+		liveService.clearLiveCacheOnStatusChange(liveId);
+		liveService.asyncToCache();
 		return R.ok();
 //		{app=200149.push.tlivecloud.com, appid=1319721001, appname=live, channel_id=673,
 //				errcode=1, errmsg=The push client actively stopped the push, event_time=1755571239,

+ 12 - 17
fs-live-socket/src/main/java/com/fs/live/task/Task.java

@@ -28,6 +28,7 @@ import com.fs.live.vo.LiveLotteryConfVo;
 import com.fs.live.vo.LiveLotteryProductListVo;
 import com.fs.live.vo.LotteryVo;
 import com.fs.live.websocket.bean.SendMsgVo;
+import com.fs.live.websocket.hub.LiveSocketHub;
 import com.fs.live.websocket.service.WebSocketServer;
 import com.fs.store.domain.FsExpress;
 import com.fs.store.domain.FsUser;
@@ -80,6 +81,8 @@ public class Task {
     @Autowired
     private ILiveRewardRecordService liveRewardRecordService;
     @Autowired
+    private LiveSocketHub liveSocketHub;
+    @Autowired
     private WebSocketServer webSocketServer;
     @Autowired
     private ILiveAutoTaskService liveAutoTaskService;
@@ -180,7 +183,9 @@ public class Task {
                 SendMsgVo sendMsgVo = new SendMsgVo();
                 sendMsgVo.setMsg("开始直播");
                 sendMsgVo.setCmd("live_start");
-                webSocketServer.broadcastMessage(live.getLiveId(), JSONObject.toJSONString(R.ok().put("data",sendMsgVo)));
+                liveSocketHub.broadcastMessage(live.getLiveId(), JSONObject.toJSONString(R.ok().put("data",sendMsgVo)));
+                // 开播:展示 + 活动运行时一并清理(含 cacheLive),再重建 auto_task
+                liveService.clearLiveCacheOnStatusChange(live.getLiveId());
                 List<LiveAutoTask> collect = liveAutoTasks.stream().filter(liveAutoTask -> liveAutoTask.getLiveId().equals(live.getLiveId())).collect(Collectors.toList());
                 if (!collect.isEmpty()) {
                     redisCache.redisTemplate.delete(key + live.getLiveId());
@@ -191,9 +196,6 @@ public class Task {
                         redisCache.redisTemplate.expire(key+live.getLiveId(), 1, TimeUnit.DAYS);
                     });
                 }
-                // 清理小程序缓存 和 直播标签缓存
-                String cacheKey = String.format(LiveKeysConstant.LIVE_DATA_CACHE, live.getLiveId());
-                redisCache.deleteObject(cacheKey);
                 liveWatchUserService.clearLiveFlagCache(live.getLiveId());
             }
             // 重新更新所有在直播的缓存
@@ -204,23 +206,16 @@ public class Task {
                 SendMsgVo sendMsgVo = new SendMsgVo();
                 sendMsgVo.setMsg("结束直播");
                 sendMsgVo.setCmd("live_end");
-                webSocketServer.broadcastMessage(live.getLiveId(), JSONObject.toJSONString(R.ok().put("data",sendMsgVo)));
-                List<LiveAutoTask> collect = liveAutoTasks.stream().filter(liveAutoTask -> liveAutoTask.getLiveId().equals(live.getLiveId())).collect(Collectors.toList());
-                if (!collect.isEmpty()) {
-                    collect.forEach(liveAutoTask -> {
-                        liveAutoTask.setCreateTime(null);
-                        liveAutoTask.setUpdateTime(null);
-                        redisCache.redisTemplate.opsForZSet().remove(key + live.getLiveId(), JSON.toJSONString(liveAutoTask),liveAutoTask.getAbsValue().getTime());
-                    });
-                }
+                liveSocketHub.broadcastMessage(live.getLiveId(), JSONObject.toJSONString(R.ok().put("data",sendMsgVo)));
+                // 关播:展示 + 红包/优惠券/抽奖/任务队列一并清理
+                liveService.clearLiveCacheOnStatusChange(live.getLiveId());
                 // 直播间关闭时,下架所有直播商品
                 try {
                     liveGoodsService.unshelfAllGoodsByLiveId(live.getLiveId());
                 } catch (Exception e) {
                     log.error("直播间关闭下架所有商品失败, liveId: {}", live.getLiveId(), e);
                 }
-                String cacheKey = String.format(LiveKeysConstant.LIVE_DATA_CACHE, live.getLiveId());
-                redisCache.deleteObject(cacheKey);
+                liveWatchUserService.clearLiveFlagCache(live.getLiveId());
                 webSocketServer.removeLikeCountCache(live.getLiveId());
             }
             // 重新更新所有在直播的缓存
@@ -347,7 +342,7 @@ public class Task {
                 sendMsgVo.setCmd("red");
                 sendMsgVo.setStatus(-1);
                 liveService.asyncToCacheLiveConfig(liveId);
-                webSocketServer.broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+                liveSocketHub.broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
             } catch (Exception e) {
                 log.error("[定时任务] 处理红包任务异常,liveKey: {}", liveKey, e);
             }
@@ -561,7 +556,7 @@ public class Task {
             sendMsgVo.setLiveId(liveLottery.getLiveId());
             sendMsgVo.setCmd("LotteryDetail");
             sendMsgVo.setData(JSON.toJSONString(lotteryVos));
-            webSocketServer.enqueueMessage(liveLottery.getLiveId(), JSONObject.toJSONString(R.ok().put("data", sendMsgVo)),true);
+            liveSocketHub.enqueueMessage(liveLottery.getLiveId(), JSONObject.toJSONString(R.ok().put("data", sendMsgVo)),true);
 
             liveService.asyncToCacheLiveConfig(liveLottery.getLiveId());
             // 删除缓存 同步抽奖记录

+ 35 - 34
fs-live-socket/src/main/java/com/fs/live/websocket/auth/AuthHandler.java

@@ -1,9 +1,8 @@
 package com.fs.live.websocket.auth;
 
+import com.fs.common.utils.spring.SpringUtils;
 import com.fs.live.utils.JwtUtils;
-import com.fs.live.utils.VerifyUtils;
 import com.fs.live.websocket.constant.AttrConstant;
-import com.fs.common.utils.spring.SpringUtils;
 import io.jsonwebtoken.Claims;
 import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandler;
@@ -18,6 +17,9 @@ import org.springframework.stereotype.Component;
 import java.util.List;
 import java.util.Map;
 
+/**
+ * Netty 握手认证,与 WebSocketConfigurator(JSR)对齐
+ */
 @Slf4j
 @Component
 @ChannelHandler.Sharable
@@ -32,12 +34,8 @@ public class AuthHandler extends ChannelInboundHandlerAdapter {
             String uri = req.uri();
             QueryStringDecoder decoder = new QueryStringDecoder(uri);
             Map<String, List<String>> parameterMap = decoder.parameters();
-            if (!parameterMap.containsKey(AttrConstant.LIVE_ID)) {
-                ctx.channel().writeAndFlush(new TextWebSocketFrame("Error: invalid parameters")).addListener(ChannelFutureListener.CLOSE);
-                return;
-            }
-
-            if (!parameterMap.containsKey(AttrConstant.USER_ID)) {
+            if (!parameterMap.containsKey(AttrConstant.LIVE_ID)
+                    || !parameterMap.containsKey(AttrConstant.USER_ID)) {
                 ctx.channel().writeAndFlush(new TextWebSocketFrame("Error: invalid parameters")).addListener(ChannelFutureListener.CLOSE);
                 return;
             }
@@ -51,41 +49,44 @@ public class AuthHandler extends ChannelInboundHandlerAdapter {
             Long liveId = Long.valueOf(parameterMap.get(AttrConstant.LIVE_ID).get(0));
             Long userId = Long.valueOf(parameterMap.get(AttrConstant.USER_ID).get(0));
 
-            // 验证 token
             if (parameterMap.containsKey(tokenKey)) {
-            String token = parameterMap.get(tokenKey).get(0);
-            Claims claims = jwtUtils.getClaimByToken(token);
-            if (claims == null || jwtUtils.isTokenExpired(claims.getExpiration())) {
-                ctx.channel().writeAndFlush(new TextWebSocketFrame("Error: invalid parameters")).addListener(ChannelFutureListener.CLOSE);
-                return;
-            }
-                // 将 userType 设置为 0(或根据实际业务逻辑设置)
-                ctx.channel().attr(AttrConstant.ATTR_USER_TYPE).set(0L);
+                String token = parameterMap.get(tokenKey).get(0);
+                Claims claims = jwtUtils.getClaimByToken(token);
+                if (claims == null || jwtUtils.isTokenExpired(claims.getExpiration())) {
+                    ctx.channel().writeAndFlush(new TextWebSocketFrame("Error: invalid token")).addListener(ChannelFutureListener.CLOSE);
+                    return;
+                }
             }
 
-            // 验证签名
-            if (parameterMap.containsKey(AttrConstant.SIGNATURE)) {
-                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);
-
+            long userType = 0L;
+            if (parameterMap.containsKey(AttrConstant.USER_TYPE)
+                    && parameterMap.get(AttrConstant.USER_TYPE) != null
+                    && !parameterMap.get(AttrConstant.USER_TYPE).isEmpty()
+                    && parameterMap.get(AttrConstant.USER_TYPE).get(0) != null) {
                 try {
-                    if (!VerifyUtils.verifySignature(liveId.toString(), userId.toString(), userTypeStr, timestampStr, signatureStr)) {
-                        ctx.channel().writeAndFlush(new TextWebSocketFrame("Error: invalid parameters")).addListener(ChannelFutureListener.CLOSE);
-                        return;
-                    }
-                    ctx.channel().attr(AttrConstant.ATTR_USER_TYPE).set(Long.parseLong(userTypeStr));
-                } catch (Exception e) {
-                    ctx.channel().writeAndFlush(new TextWebSocketFrame("Error: invalid parameters")).addListener(ChannelFutureListener.CLOSE);
-                    return;
+                    userType = Long.parseLong(parameterMap.get(AttrConstant.USER_TYPE).get(0));
+                } catch (NumberFormatException ignored) {
+                    userType = 0L;
                 }
             }
 
-            // 将 liveId 和 userId 保存到 Channel 属性中,供后续处理使用
             ctx.channel().attr(AttrConstant.ATTR_LIVE_ID).set(liveId);
             ctx.channel().attr(AttrConstant.ATTR_USER_ID).set(userId);
+            ctx.channel().attr(AttrConstant.ATTR_USER_TYPE).set(userType);
+
+            if (parameterMap.containsKey(AttrConstant.COMPANY_ID)) {
+                ctx.channel().attr(AttrConstant.ATTR_COMPANY_ID)
+                        .set(Long.valueOf(parameterMap.get(AttrConstant.COMPANY_ID).get(0)));
+            }
+            if (parameterMap.containsKey(AttrConstant.COMPANY_USER_ID)) {
+                ctx.channel().attr(AttrConstant.ATTR_COMPANY_USER_ID)
+                        .set(Long.valueOf(parameterMap.get(AttrConstant.COMPANY_USER_ID).get(0)));
+            }
+
+            // 去掉 query,避免 WebSocketServerProtocolHandler 路径校验失败
+            String path = uri.contains("?") ? uri.substring(0, uri.indexOf('?')) : uri;
+            req.setUri(path);
 
-            // 继续处理 WebSocket 握手
             ctx.pipeline().remove(this);
             ctx.fireChannelRead(req.retain());
         } else {
@@ -94,7 +95,7 @@ public class AuthHandler extends ChannelInboundHandlerAdapter {
     }
 
     @Override
-    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
         log.warn("webSocket 认证异常: {}", cause.getMessage(), cause);
         ctx.close();
     }

+ 2 - 0
fs-live-socket/src/main/java/com/fs/live/websocket/constant/AttrConstant.java

@@ -17,4 +17,6 @@ public class AttrConstant {
     public static final AttributeKey<Long> ATTR_LIVE_ID = AttributeKey.valueOf(LIVE_ID);
     public static final AttributeKey<Long> ATTR_USER_ID = AttributeKey.valueOf(USER_ID);
     public static final AttributeKey<Long> ATTR_USER_TYPE = AttributeKey.valueOf(USER_TYPE);
+    public static final AttributeKey<Long> ATTR_COMPANY_ID = AttributeKey.valueOf(COMPANY_ID);
+    public static final AttributeKey<Long> ATTR_COMPANY_USER_ID = AttributeKey.valueOf(COMPANY_USER_ID);
 }

+ 58 - 188
fs-live-socket/src/main/java/com/fs/live/websocket/handle/LiveChatHandler.java

@@ -1,83 +1,67 @@
 package com.fs.live.websocket.handle;
 
 import com.alibaba.fastjson.JSONObject;
-import com.fs.live.websocket.bean.SendMsgVo;
-import com.fs.live.websocket.constant.AttrConstant;
 import com.fs.common.core.domain.R;
 import com.fs.common.utils.spring.SpringUtils;
-import com.fs.live.domain.LiveMsg;
 import com.fs.live.domain.LiveWatchUser;
-import com.fs.live.service.ILiveMsgService;
 import com.fs.live.service.ILiveService;
 import com.fs.live.service.ILiveWatchUserService;
 import com.fs.live.vo.LiveWatchUserVO;
+import com.fs.live.websocket.bean.SendMsgVo;
+import com.fs.live.websocket.constant.AttrConstant;
+import com.fs.live.websocket.hub.LiveSocketHub;
+import com.fs.live.websocket.hub.LiveSocketHubImpl;
 import com.fs.store.domain.FsUser;
 import com.fs.store.service.IFsUserService;
 import io.netty.channel.*;
-import io.netty.channel.group.ChannelGroup;
-import io.netty.channel.group.DefaultChannelGroup;
 import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
 import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
-import io.netty.util.concurrent.GlobalEventExecutor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
 import java.util.Objects;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CopyOnWriteArrayList;
 
+/**
+ * Netty WebSocket 业务 Handler,连接逻辑与 JSR WebSocketServer 对齐
+ */
 @Component
 @ChannelHandler.Sharable
 @Slf4j
 public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
 
-    // 容器
-    private final static ConcurrentHashMap<Long, CopyOnWriteArrayList<Channel>> adminRooms = new ConcurrentHashMap<>();
-    private final static ConcurrentHashMap<Long, ConcurrentHashMap<Long, Channel>> rooms = new ConcurrentHashMap<>();
-    private final static ConcurrentHashMap<Long, ChannelGroup> roomGroups = new ConcurrentHashMap<>();
-    private final static ILiveService liveService = SpringUtils.getBean(ILiveService.class);
-    private final static ILiveWatchUserService liveWatchUserService = SpringUtils.getBean(ILiveWatchUserService.class);
-    private final static ILiveMsgService liveMsgService = SpringUtils.getBean(ILiveMsgService.class);
-    private final static IFsUserService fsUserService = SpringUtils.getBean(IFsUserService.class);
+    private final ILiveService liveService = SpringUtils.getBean(ILiveService.class);
+    private final ILiveWatchUserService liveWatchUserService = SpringUtils.getBean(ILiveWatchUserService.class);
+    private final IFsUserService fsUserService = SpringUtils.getBean(IFsUserService.class);
+
+    private LiveSocketHub hub() {
+        return SpringUtils.getBean(LiveSocketHub.class);
+    }
 
-    /**
-     * 处理握手
-     * @param ctx   连接
-     * @param evt   数据
-     * @throws Exception    异常
-     */
     @Override
     public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
-        // 处理 WebSocket 握手完成事件
         if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
             Long userId = ctx.channel().attr(AttrConstant.ATTR_USER_ID).get();
             Long liveId = ctx.channel().attr(AttrConstant.ATTR_LIVE_ID).get();
             Long userType = ctx.channel().attr(AttrConstant.ATTR_USER_TYPE).get();
+            if (userType == null) {
+                userType = 0L;
+            }
 
             if (Objects.isNull(liveService.selectLiveByLiveId(liveId))) {
                 ctx.channel().writeAndFlush(new TextWebSocketFrame("Error: 未找到直播间")).addListener(ChannelFutureListener.CLOSE);
                 return;
             }
 
-            Map<Long, Channel> room = getRoom(liveId);
-            List<Channel> adminRoom = getAdminRoom(liveId);
-            ChannelGroup roomGroup = getRoomGroup(liveId);
-            roomGroup.add(ctx.channel());
+            LiveSocketHubImpl hubImpl = (LiveSocketHubImpl) hub();
+            hubImpl.joinNettyFromChannel(ctx.channel());
 
             if (userType == 0) {
-
-
                 FsUser fsUser = fsUserService.selectWsFsUserById(userId);
                 if (Objects.isNull(fsUser)) {
                     ctx.channel().writeAndFlush(new TextWebSocketFrame("Error: 用户信息错误")).addListener(ChannelFutureListener.CLOSE);
                     return;
-                } // 加入房间
-                liveWatchUserService.join(fsUser,liveId, userId,"");
-                room.put(userId, ctx.channel());
-
+                }
+                liveWatchUserService.join(fsUser, liveId, userId, "");
                 LiveWatchUserVO liveWatchUserVO = liveWatchUserService.selectWatchUserByLiveIdAndUserId(liveId, userId);
 
                 SendMsgVo sendMsgVo = new SendMsgVo();
@@ -89,174 +73,60 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
                 sendMsgVo.setData(JSONObject.toJSONString(liveWatchUserVO));
                 sendMsgVo.setNickName(fsUser.getNickname());
                 sendMsgVo.setAvatar(fsUser.getAvatar());
-
-                // 广播连接消息
-                broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
-            } else if (userType == 1) {
-                adminRoom.add(ctx.channel());
+                hub().broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
             }
-
+        } else {
+            super.userEventTriggered(ctx, evt);
         }
     }
 
-    /**
-     * 获取房间
-     * @param liveId 直播间ID
-     * @return 容器
-     */
-    private CopyOnWriteArrayList<Channel> getAdminRoom(Long liveId) {
-        return adminRooms.computeIfAbsent(liveId, k -> new CopyOnWriteArrayList<>());
-    }
-
-    /**
-     * 获取房间
-     * @param liveId 直播间ID
-     * @return 容器
-     */
-    private ConcurrentHashMap<Long, Channel> getRoom(Long liveId) {
-        return rooms.computeIfAbsent(liveId, k -> new ConcurrentHashMap<>());
-    }
-
-    /**
-     * 获取房间用户组
-     * @param liveId 直播间ID
-     * @return  用户组
-     */
-    private ChannelGroup getRoomGroup(Long liveId) {
-        return roomGroups.computeIfAbsent(liveId, k -> new DefaultChannelGroup(GlobalEventExecutor.INSTANCE));
-    }
-
-    /**
-     * 发送广播
-     * @param liveId    直播间ID
-     * @param msg       消息
-     */
-    private void broadcastMessage(Long liveId, String msg) {
-        getRoomGroup(liveId).writeAndFlush(new TextWebSocketFrame(msg));
-    }
-
-    /**
-     * 发送指定消息
-     * @param channel   连接
-     * @param message   消息
-     */
-    private void sendMessage(Channel channel, String message) {
-        channel.writeAndFlush(new TextWebSocketFrame(message));
-    }
-
-    /**
-     * 接收消息
-     * @param channelHandlerContext 连接
-     * @param textWebSocketFrame    消息
-     * @throws Exception    异常
-     */
     @Override
-    protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
-        Long liveId = channelHandlerContext.channel().attr(AttrConstant.ATTR_LIVE_ID).get();
-        Long userType = channelHandlerContext.channel().attr(AttrConstant.ATTR_USER_TYPE).get();
-
-        SendMsgVo msg = JSONObject.parseObject( textWebSocketFrame.text(), SendMsgVo.class);
-        if(msg.isOn()) return;
-        try {
-            switch (msg.getCmd()) {
-                case "heartbeat":
-                    sendMessage(channelHandlerContext.channel(), JSONObject.toJSONString(R.ok().put("data", msg)));
-                    break;
-                case "sendMsg":
-                    LiveMsg liveMsg = new LiveMsg();
-                    liveMsg.setLiveId(msg.getLiveId());
-                    liveMsg.setUserId(msg.getUserId());
-                    liveMsg.setNickName(msg.getNickName());
-                    liveMsg.setAvatar(msg.getAvatar());
-                    liveMsg.setMsg(msg.getMsg());
-                    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(channelHandlerContext.channel(), JSONObject.toJSONString(R.error("你以被禁言")));
-                            return;
-                        }
-
-                        liveMsgService.insertLiveMsg(liveMsg);
-                    }
-
-                    msg.setOn(true);
-                    msg.setData(JSONObject.toJSONString(liveMsg));
-
-                    // 广播消息
-                    broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)));
-                    break;
-            }
-        } catch (Exception e) {
-            log.error("webSocket 消息处理失败 msg: {}", e.getMessage(), e);
+    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) {
+        Long userId = ctx.channel().attr(AttrConstant.ATTR_USER_ID).get();
+        Long liveId = ctx.channel().attr(AttrConstant.ATTR_LIVE_ID).get();
+        Long userType = ctx.channel().attr(AttrConstant.ATTR_USER_TYPE).get();
+        if (userType == null) {
+            userType = 0L;
         }
+        String text = frame.text();
+        Channel channel = ctx.channel();
+        hub().processClientMessage(liveId, userId, userType, text,
+                msg -> channel.writeAndFlush(new TextWebSocketFrame(msg)));
     }
 
-    /**
-     * 断开连接
-     * @param ctx   连接
-     * @throws Exception    异常
-     */
     @Override
-    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+    public void channelInactive(ChannelHandlerContext ctx) {
         Long userId = ctx.channel().attr(AttrConstant.ATTR_USER_ID).get();
         Long liveId = ctx.channel().attr(AttrConstant.ATTR_LIVE_ID).get();
         Long userType = ctx.channel().attr(AttrConstant.ATTR_USER_TYPE).get();
-
-        if (Objects.isNull(userId) || Objects.isNull(liveId) || Objects.isNull(userType)) {
-            return;
-        }
-
-        Map<Long, Channel> room = getRoom(liveId);
-        List<Channel> adminRoom = getAdminRoom(liveId);
-        ChannelGroup roomGroup = getRoomGroup(liveId);
-
-        if (userType == 0) {
-            FsUser fsUser = fsUserService.selectWsFsUserById(userId);
-            liveWatchUserService.close(liveId, userId);
-            room.remove(userId);
-
-            if (room.isEmpty()) {
-                rooms.remove(liveId);
-            }
-
-            LiveWatchUserVO liveWatchUserVO = liveWatchUserService.selectWatchUserByLiveIdAndUserId(liveId, userId);
-
-            SendMsgVo sendMsgVo = new SendMsgVo();
-            sendMsgVo.setLiveId(liveId);
-            sendMsgVo.setUserId(userId);
-            sendMsgVo.setUserType(userType);
-            sendMsgVo.setCmd("out");
-            sendMsgVo.setMsg("用户离开");
-            sendMsgVo.setData(JSONObject.toJSONString(liveWatchUserVO));
-            sendMsgVo.setNickName(fsUser.getNickname());
-            sendMsgVo.setAvatar(fsUser.getAvatar());
-
-            // 广播离开消息
-            broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
-        } else {
-            adminRoom.remove(ctx.channel());
-            if (adminRoom.isEmpty()) {
-                adminRooms.remove(liveId);
+        hub().leaveNetty(ctx.channel());
+        if (liveId != null && userId != null && (userType == null || userType == 0L)) {
+            try {
+                LiveWatchUser watchUser = liveWatchUserService.close(liveId, userId);
+                FsUser fsUser = fsUserService.selectWsFsUserById(userId);
+                SendMsgVo sendMsgVo = new SendMsgVo();
+                sendMsgVo.setLiveId(liveId);
+                sendMsgVo.setUserId(userId);
+                sendMsgVo.setUserType(0L);
+                sendMsgVo.setCmd("out");
+                sendMsgVo.setMsg("用户离开");
+                if (watchUser != null) {
+                    sendMsgVo.setData(JSONObject.toJSONString(watchUser));
+                }
+                if (fsUser != null) {
+                    sendMsgVo.setNickName(fsUser.getNickname());
+                    sendMsgVo.setAvatar(fsUser.getAvatar());
+                }
+                hub().enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)), false);
+            } catch (Exception e) {
+                log.warn("Netty 离开直播间处理失败: {}", e.getMessage());
             }
         }
-        roomGroup.remove(ctx.channel());
-        if (roomGroup.isEmpty()) {
-            roomGroups.remove(liveId);
-        }
-
-
     }
 
-    /**
-     * 连接异常
-     * @param ctx   连接
-     * @param cause 原因
-     * @throws Exception 异常
-     */
     @Override
-    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+        log.warn("Netty LiveChatHandler 异常: {}", cause.getMessage(), cause);
         ctx.close();
     }
 }

+ 9 - 0
fs-live-socket/src/main/java/com/fs/live/websocket/hub/ClientReply.java

@@ -0,0 +1,9 @@
+package com.fs.live.websocket.hub;
+
+/**
+ * 向当前客户端回写消息(JSR Session 或 Netty Channel)
+ */
+@FunctionalInterface
+public interface ClientReply {
+    void send(String message) throws Exception;
+}

+ 37 - 0
fs-live-socket/src/main/java/com/fs/live/websocket/hub/LiveSocketHub.java

@@ -0,0 +1,37 @@
+package com.fs.live.websocket.hub;
+
+/**
+ * 双端口(JSR-356 + Netty)统一广播入口
+ */
+public interface LiveSocketHub {
+
+    /**
+     * Netty 用户加入房间
+     */
+    void joinNetty(Long liveId, Long userId, Long userType, io.netty.channel.Channel channel);
+
+    /**
+     * Netty 连接断开
+     */
+    void leaveNetty(io.netty.channel.Channel channel);
+
+    /**
+     * 仅向 Netty 连接广播
+     */
+    void broadcastNetty(Long liveId, String message);
+
+    /**
+     * 向 JSR + Netty 双端口广播
+     */
+    void broadcastMessage(Long liveId, String message);
+
+    /**
+     * 入队广播(最终会走到双端口 broadcast)
+     */
+    boolean enqueueMessage(Long liveId, String message, boolean isAdmin);
+
+    /**
+     * 处理客户端消息(与 JSR onMessage 业务一致)
+     */
+    void processClientMessage(long liveId, long userId, long userType, String message, ClientReply reply);
+}

+ 143 - 0
fs-live-socket/src/main/java/com/fs/live/websocket/hub/LiveSocketHubImpl.java

@@ -0,0 +1,143 @@
+package com.fs.live.websocket.hub;
+
+import com.fs.common.utils.spring.SpringUtils;
+import com.fs.live.websocket.constant.AttrConstant;
+import com.fs.live.websocket.service.WebSocketServer;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * 双端口广播中枢:7114 JSR-356 + 7116 Netty 共用
+ */
+@Slf4j
+@Component
+public class LiveSocketHubImpl implements LiveSocketHub {
+
+    private final ConcurrentHashMap<Long, ConcurrentHashMap<Long, Channel>> nettyRooms = new ConcurrentHashMap<>();
+    private final ConcurrentHashMap<Long, CopyOnWriteArrayList<Channel>> nettyAdminRooms = new ConcurrentHashMap<>();
+    private final ConcurrentHashMap<String, Long[]> channelMeta = new ConcurrentHashMap<>();
+
+    private ConcurrentHashMap<Long, Channel> getNettyRoom(Long liveId) {
+        return nettyRooms.computeIfAbsent(liveId, k -> new ConcurrentHashMap<>());
+    }
+
+    private List<Channel> getNettyAdminRoom(Long liveId) {
+        return nettyAdminRooms.computeIfAbsent(liveId, k -> new CopyOnWriteArrayList<>());
+    }
+
+    @Override
+    public void joinNetty(Long liveId, Long userId, Long userType, Channel channel) {
+        if (liveId == null || userId == null || channel == null) {
+            return;
+        }
+        channelMeta.put(channel.id().asLongText(), new Long[]{liveId, userId, userType == null ? 0L : userType});
+        if (userType != null && userType == 1L) {
+            List<Channel> adminRoom = getNettyAdminRoom(liveId);
+            if (!adminRoom.contains(channel)) {
+                adminRoom.add(channel);
+            }
+        } else {
+            getNettyRoom(liveId).put(userId, channel);
+        }
+    }
+
+    @Override
+    public void leaveNetty(Channel channel) {
+        if (channel == null) {
+            return;
+        }
+        Long[] meta = channelMeta.remove(channel.id().asLongText());
+        if (meta == null) {
+            return;
+        }
+        Long liveId = meta[0];
+        Long userId = meta[1];
+        Long userType = meta[2];
+        if (userType != null && userType == 1L) {
+            List<Channel> adminRoom = nettyAdminRooms.get(liveId);
+            if (adminRoom != null) {
+                adminRoom.remove(channel);
+                if (adminRoom.isEmpty()) {
+                    nettyAdminRooms.remove(liveId);
+                }
+            }
+        } else {
+            ConcurrentHashMap<Long, Channel> room = nettyRooms.get(liveId);
+            if (room != null) {
+                room.remove(userId, channel);
+                if (room.isEmpty()) {
+                    nettyRooms.remove(liveId);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void broadcastNetty(Long liveId, String message) {
+        if (liveId == null || message == null) {
+            return;
+        }
+        ConcurrentHashMap<Long, Channel> room = nettyRooms.get(liveId);
+        if (room != null) {
+            room.forEach((uid, ch) -> writeNetty(ch, message));
+        }
+        List<Channel> adminRoom = nettyAdminRooms.get(liveId);
+        if (adminRoom != null) {
+            for (Channel ch : adminRoom) {
+                writeNetty(ch, message);
+            }
+        }
+    }
+
+    private void writeNetty(Channel channel, String message) {
+        if (channel == null || !channel.isActive()) {
+            return;
+        }
+        try {
+            channel.writeAndFlush(new TextWebSocketFrame(message));
+        } catch (Exception e) {
+            log.error("Netty 广播失败 channel={}", channel.id(), e);
+        }
+    }
+
+    @Override
+    public void broadcastMessage(Long liveId, String message) {
+        try {
+            WebSocketServer jsr = SpringUtils.getBean(WebSocketServer.class);
+            jsr.broadcastJsrOnly(liveId, message);
+        } catch (Exception e) {
+            log.error("JSR 广播失败 liveId={}", liveId, e);
+        }
+        broadcastNetty(liveId, message);
+    }
+
+    @Override
+    public boolean enqueueMessage(Long liveId, String message, boolean isAdmin) {
+        WebSocketServer jsr = SpringUtils.getBean(WebSocketServer.class);
+        return jsr.enqueueMessage(liveId, message, isAdmin);
+    }
+
+    @Override
+    public void processClientMessage(long liveId, long userId, long userType, String message, ClientReply reply) {
+        WebSocketServer jsr = SpringUtils.getBean(WebSocketServer.class);
+        jsr.processClientMessage(liveId, userId, userType, message, reply);
+    }
+
+    /**
+     * 从 Channel 属性读取并注册(握手完成后调用)
+     */
+    public void joinNettyFromChannel(Channel channel) {
+        Long liveId = channel.attr(AttrConstant.ATTR_LIVE_ID).get();
+        Long userId = channel.attr(AttrConstant.ATTR_USER_ID).get();
+        Long userType = channel.attr(AttrConstant.ATTR_USER_TYPE).get();
+        joinNetty(liveId, userId, userType == null ? 0L : userType, channel);
+    }
+}

+ 97 - 97
fs-live-socket/src/main/java/com/fs/live/websocket/service/NettyServerRunner.java

@@ -1,97 +1,97 @@
-package com.fs.live.websocket.service;//package com.fs.app.websocket.service;
-//
-//import com.fs.app.websocket.auth.AuthHandler;
-//import com.fs.app.websocket.handle.LiveChatHandler;
-//import io.netty.bootstrap.ServerBootstrap;
-//import io.netty.channel.*;
-//import io.netty.channel.nio.NioEventLoopGroup;
-//import io.netty.channel.socket.SocketChannel;
-//import io.netty.channel.socket.nio.NioServerSocketChannel;
-//import io.netty.handler.codec.http.HttpObjectAggregator;
-//import io.netty.handler.codec.http.HttpServerCodec;
-//import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
-//import lombok.extern.slf4j.Slf4j;
-//import org.springframework.beans.factory.annotation.Autowired;
-//import org.springframework.boot.CommandLineRunner;
-//import org.springframework.stereotype.Component;
-//
-//import javax.annotation.PreDestroy;
-//import java.util.Objects;
-//
-//@Slf4j
-//@Component
-//public class NettyServerRunner implements CommandLineRunner {
-//
-//    private static final int port = 17114;
-//    private EventLoopGroup bossGroup;
-//    private EventLoopGroup workerGroup;
-//    private Channel serverChannel;
-//    @Autowired
-//    private AuthHandler authHandler;
-//    @Autowired
-//    private LiveChatHandler liveChatHandler;
-//
-//    @Override
-//    public void run(String... args) throws Exception {
-//        new Thread(this::startServer).start();
-//    }
-//
-//    private void startServer() {
-//        bossGroup = new NioEventLoopGroup(); // 处理连接
-//        workerGroup = new NioEventLoopGroup(); // 处理I/O
-//        try {
-//            ServerBootstrap bootstrap = new ServerBootstrap();
-//            bootstrap.group(bossGroup, workerGroup)
-//                    .channel(NioServerSocketChannel.class)
-//                    .childHandler(new ChannelInitializer<SocketChannel>() {
-//
-//                        @Override
-//                        protected void initChannel(SocketChannel socketChannel) throws Exception {
-//                            ChannelPipeline pipeline = socketChannel.pipeline();
-//                            // 编解码
-//                            pipeline.addLast(new HttpServerCodec());
-//                            // 集合消息
-//                            pipeline.addLast(new HttpObjectAggregator(65536));
-//                            // 安全校验
-//                            pipeline.addLast(authHandler);
-//                            // websocket握手
-//                            pipeline.addLast(new WebSocketServerProtocolHandler("/app/webSocket", null, true, 65536, false, true));
-//                            // 自定义聊天
-//                            pipeline.addLast(liveChatHandler);
-//                        }
-//                    })
-//                    .option(ChannelOption.SO_BACKLOG, 1024)
-//                    .childOption(ChannelOption.SO_KEEPALIVE, true);
-//
-//            ChannelFuture future = bootstrap.bind(port).sync();
-//            serverChannel = future.channel();
-//            log.info("netty server started [{}]", port);
-//            serverChannel.closeFuture().sync();
-//        } catch (Exception e) {
-//            log.error("netty server error msg: {}", e.getMessage(), e);
-//        } finally {
-//            shutdown();
-//        }
-//    }
-//
-//    @PreDestroy
-//    public void destroy() {
-//        shutdown();
-//        log.info("netty server destroy");
-//    }
-//
-//    private void shutdown() {
-//        if (Objects.nonNull(bossGroup)) {
-//            bossGroup.shutdownGracefully();
-//        }
-//
-//        if (Objects.nonNull(workerGroup)) {
-//            workerGroup.shutdownGracefully();
-//        }
-//
-//        if (Objects.nonNull(serverChannel)) {
-//            serverChannel.close();
-//        }
-//        log.info("netty server stopped");
-//    }
-//}
+package com.fs.live.websocket.service;
+
+import com.fs.live.websocket.auth.AuthHandler;
+import com.fs.live.websocket.handle.LiveChatHandler;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.*;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PreDestroy;
+
+/**
+ * Netty WebSocket 服务,默认端口 7116,与 Tomcat JSR-356 7114 并存
+ */
+@Slf4j
+@Component
+public class NettyServerRunner implements CommandLineRunner {
+
+    @Value("${netty.websocket.port:7116}")
+    private int port;
+
+    @Value("${netty.websocket.path:/ws/app/webSocket}")
+    private String path;
+
+    private EventLoopGroup bossGroup;
+    private EventLoopGroup workerGroup;
+    private Channel serverChannel;
+
+    @Autowired
+    private AuthHandler authHandler;
+    @Autowired
+    private LiveChatHandler liveChatHandler;
+
+    @Override
+    public void run(String... args) {
+        new Thread(this::startServer, "netty-websocket-starter").start();
+    }
+
+    private void startServer() {
+        bossGroup = new NioEventLoopGroup(1);
+        workerGroup = new NioEventLoopGroup();
+        try {
+            ServerBootstrap bootstrap = new ServerBootstrap();
+            bootstrap.group(bossGroup, workerGroup)
+                    .channel(NioServerSocketChannel.class)
+                    .childHandler(new ChannelInitializer<SocketChannel>() {
+                        @Override
+                        protected void initChannel(SocketChannel socketChannel) {
+                            ChannelPipeline pipeline = socketChannel.pipeline();
+                            pipeline.addLast(new HttpServerCodec());
+                            pipeline.addLast(new HttpObjectAggregator(65536));
+                            pipeline.addLast(authHandler);
+                            pipeline.addLast(new WebSocketServerProtocolHandler(path, null, true, 65536, false, true));
+                            pipeline.addLast(liveChatHandler);
+                        }
+                    })
+                    .option(ChannelOption.SO_BACKLOG, 1024)
+                    .childOption(ChannelOption.SO_KEEPALIVE, true);
+
+            ChannelFuture future = bootstrap.bind(port).sync();
+            serverChannel = future.channel();
+            log.info("netty websocket server started port={} path={}", port, path);
+            serverChannel.closeFuture().sync();
+        } catch (Exception e) {
+            log.error("netty server error msg: {}", e.getMessage(), e);
+        } finally {
+            shutdown();
+        }
+    }
+
+    @PreDestroy
+    public void destroy() {
+        shutdown();
+        log.info("netty server destroy");
+    }
+
+    private void shutdown() {
+        if (serverChannel != null) {
+            serverChannel.close();
+        }
+        if (bossGroup != null) {
+            bossGroup.shutdownGracefully();
+        }
+        if (workerGroup != null) {
+            workerGroup.shutdownGracefully();
+        }
+    }
+}

+ 38 - 32
fs-live-socket/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -8,6 +8,8 @@ import com.fs.live.config.ProductionWordFilter;
 import com.fs.live.mapper.LiveCouponMapper;
 import com.fs.live.websocket.auth.WebSocketConfigurator;
 import com.fs.live.websocket.bean.SendMsgVo;
+import com.fs.live.websocket.hub.ClientReply;
+import com.fs.live.websocket.hub.LiveSocketHub;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.exception.BaseException;
@@ -352,9 +354,17 @@ public class WebSocketServer {
     @OnMessage
     public void onMessage(Session session,String message) throws IOException {
         Map<String, Object> userProperties = session.getUserProperties();
-
         long liveId = (long) userProperties.get("liveId");
         long userType = (long) userProperties.get("userType");
+        long userId = (long) userProperties.get("userId");
+        heartbeatCache.put(session.getId(), System.currentTimeMillis());
+        processClientMessage(liveId, userId, userType, message, msg -> sendMessage(session, msg));
+    }
+
+    /**
+     * 处理客户端消息(JSR / Netty 共用)
+     */
+    public void processClientMessage(long liveId, long userId, long userType, String message, ClientReply reply) {
         boolean isAdmin = false;
 
         SendMsgVo msg = JSONObject.parseObject(message, SendMsgVo.class);
@@ -363,20 +373,11 @@ public class WebSocketServer {
         try {
             switch (msg.getCmd()) {
                 case "heartbeat":
-                    // 更新心跳时间(使用putIfAbsent优化,减少不必要的更新)
-                    String sessionId = session.getId();
-                    heartbeatCache.put(sessionId, System.currentTimeMillis());
-                    
-                    // 异步发送心跳响应,避免阻塞主线程
                     HEARTBEAT_EXECUTOR.submit(() -> {
                         try {
-                            if (session.isOpen()) {
-                                String response = JSONObject.toJSONString(R.ok().put("data", msg));
-                                // 使用异步发送,不阻塞
-                                sendMessage(session,response);
-                            }
+                            String response = JSONObject.toJSONString(R.ok().put("data", msg));
+                            reply.send(response);
                         } catch (Exception ignored) {
-
                         }
                     });
                     break;
@@ -396,7 +397,7 @@ public class WebSocketServer {
                         String blockedUsersKey = String.format(MUTED_USERS_KEY, msg.getLiveId());
                         boolean isBlocked = redisCache.redisTemplate.opsForSet().isMember(blockedUsersKey, String.valueOf(msg.getUserId()));
                         if (isBlocked) {
-                            sendMessage(session, JSONObject.toJSONString(R.error("你已被禁言")));
+                            reply.send(JSONObject.toJSONString(R.error("你已被禁言")));
                             return;
                         }
                         Map<String, Integer> flagMap = liveWatchUserService.getLiveFlagWithCache(liveId);
@@ -415,7 +416,7 @@ public class WebSocketServer {
                                 flushLiveMsgQueue();
                             }
                         } catch (Exception e) {
-                            log.error("[聊天消息队列] 添加消息失败, liveId: {}, userId: {}, error: {}", 
+                            log.error("[聊天消息队列] 添加消息失败, liveId: {}, userId: {}, error: {}",
                                     liveMsg.getLiveId(), liveMsg.getUserId(), e.getMessage());
                         }
                     }
@@ -501,10 +502,8 @@ public class WebSocketServer {
         final Long resolvedMsgId = msgId;
         if (resolvedMsgId != null) {
             if (resolvedMsgId > 0) {
-                // 真实 msgId:本服务与后台可能不同库或后台未调用,此处统一删库(幂等),再广播
                 liveMsgService.deleteLiveMsgByMsgId(resolvedMsgId);
             } else {
-                // 临时 msgId:从队列移除,若已入库则按映射删库,再广播
                 liveMsgQueue.removeIf(m -> resolvedMsgId.equals(m.getMsgId()));
                 Long realId = tempIdToRealId.remove(resolvedMsgId);
                 if (realId != null) {
@@ -526,7 +525,6 @@ public class WebSocketServer {
         Integer status = jsonObject.getInteger("status");
         msg.setStatus(status);
         Long couponIssueId = jsonObject.getLong("couponIssueId");
-        // ①  检查  缓存是否存在  ② 如果是发布 放入缓存 ③ 删除缓存
         if (status == 1) {
             Object cacheObject = redisCache.getCacheObject(String.format(LiveKeysConstant.LIVE_COUPON_NUM , couponIssueId));
             if (cacheObject == null) {
@@ -541,7 +539,6 @@ public class WebSocketServer {
         enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)), true);
     }
 
-
     private void sendGoodsMessage(SendMsgVo msg) {
         JSONObject jsonObject = JSON.parseObject(msg.getData());
         Long goodsId = jsonObject.getLong("goodsId");
@@ -556,9 +553,6 @@ public class WebSocketServer {
         enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)), true);
     }
 
-    /**
-     * 处理红包变动消息
-     */
     private void processRed(Long liveId, SendMsgVo msg) {
         log.debug("redData: {}", msg);
         JSONObject jsonObject = JSON.parseObject(msg.getData());
@@ -572,9 +566,6 @@ public class WebSocketServer {
         }
     }
 
-    /**
-     * 处理抽奖变动消息
-     */
     private void processLottery(Long liveId, SendMsgVo msg) {
         log.debug("lotteryData: {}", msg);
         JSONObject jsonObject = JSON.parseObject(msg.getData());
@@ -797,36 +788,45 @@ public class WebSocketServer {
      * @param liveId   直播间ID
      * @param message  消息内容
      */
-    public void broadcastMessage(Long liveId, String message) {
+    /**
+     * 仅向 JSR-356 连接广播(不含 Netty)
+     */
+    public void broadcastJsrOnly(Long liveId, String message) {
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
         List<Session> adminRoom = getAdminRoom(liveId);
 
-
-        
-        // 收集普通用户房间的Session(去重)
         room.forEach((k, v) -> {
             if (v != null && v.isOpen()) {
                 try {
                     sendMessage(v, message);
                 } catch (Exception e) {
-                    // 单个Session发送失败不影响其他Session
                     log.error(e.getMessage());
                 }
             }
         });
-        
-        // 收集admin房间的Session(去重)
+
         adminRoom.forEach(v -> {
             if (v != null && v.isOpen()) {
                 try {
                     sendMessage(v, message);
                 } catch (Exception e) {
-                    // 单个Session发送失败不影响其他Session
                     log.error(e.getMessage());
                 }
             }
         });
+    }
 
+    /**
+     * 广播消息(JSR + Netty 双端口)
+     */
+    public void broadcastMessage(Long liveId, String message) {
+        broadcastJsrOnly(liveId, message);
+        try {
+            LiveSocketHub hub = SpringUtils.getBean(LiveSocketHub.class);
+            hub.broadcastNetty(liveId, message);
+        } catch (Exception e) {
+            log.debug("Netty 广播跳过: {}", e.getMessage());
+        }
     }
 
     /**
@@ -854,6 +854,12 @@ public class WebSocketServer {
                 log.error(e.getMessage());
             }
         });
+        try {
+            LiveSocketHub hub = SpringUtils.getBean(LiveSocketHub.class);
+            hub.broadcastNetty(liveId, message);
+        } catch (Exception e) {
+            log.debug("Netty 点赞广播跳过: {}", e.getMessage());
+        }
     }
 
     /**

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

@@ -26,6 +26,12 @@ fs:
 server:
   # 服务器的HTTP端口,默认为 7014  store 7114
   port: 7114
+
+# Netty WebSocket 独立端口(与 Tomcat JSR-356 7114 并存)
+netty:
+  websocket:
+    port: 7116
+    path: /ws/app/webSocket
   servlet:
     # 应用的访问路径
     context-path: /

+ 4 - 2
fs-service-system/src/main/java/com/fs/erp/service/impl/FsJstAftersalePushServiceImpl.java

@@ -185,7 +185,8 @@ public class FsJstAftersalePushServiceImpl implements FsJstAftersalePushService
             RefundItemDTO itemDTO = new RefundItemDTO();
             itemDTO.setSkuId(cartDTO.getBarCode());
             itemDTO.setQty(cartDTO.getNum());
-            itemDTO.setAmount(cartDTO.getPrice());
+            // 260727 需求需要按照给的钱 进行申请售后
+            itemDTO.setAmount(dto.getTotalAmount());
             itemDTO.setType("退货");
             refundItemDTOS.add(itemDTO);
         }
@@ -221,7 +222,8 @@ public class FsJstAftersalePushServiceImpl implements FsJstAftersalePushService
             RefundItemDTO itemDTO = new RefundItemDTO();
             itemDTO.setSkuId(cartDTO.getBarCode());
             itemDTO.setQty(cartDTO.getNum());
-            itemDTO.setAmount(cartDTO.getPrice());
+            // 260727 需求需要按照给的钱 进行申请售后
+            itemDTO.setAmount(dto.getTotalAmount());
             itemDTO.setType("退货");
             refundItemDTOS.add(itemDTO);
         }

+ 5 - 2
fs-service-system/src/main/java/com/fs/live/domain/LiveUserRedRecord.java

@@ -34,7 +34,10 @@ public class LiveUserRedRecord extends BaseEntity{
     @Excel(name = "芳华币数量")
     private Long integral;
 
+    /** 用户昵称(关联查询) */
+    @Excel(name = "用户昵称")
+    private String nickname;
 
-
-
+    /** 用户手机号(关联查询,脱敏展示) */
+    private String phone;
 }

+ 67 - 0
fs-service-system/src/main/java/com/fs/live/mapper/LiveOrderOptMapper.java

@@ -0,0 +1,67 @@
+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.domain.LiveGoods;
+import com.fs.live.domain.LiveOrder;
+import com.fs.live.domain.LiveOrderItem;
+import com.fs.live.domain.LiveOrderPayment;
+import com.fs.store.domain.FsShippingTemplates;
+import com.fs.store.domain.FsShippingTemplatesRegion;
+import com.fs.store.domain.FsStoreProduct;
+import com.fs.store.domain.FsStoreProductAttrValue;
+import com.fs.store.domain.FsUser;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
+
+import java.util.List;
+
+/**
+ * 直播订单高并发优化 Mapper(独立副本,不影响原 LiveOrderMapper)
+ */
+public interface LiveOrderOptMapper {
+
+    @DataSource(DataSourceType.SLAVE)
+    FsStoreProduct selectProductById(@Param("productId") Long productId);
+
+    @DataSource(DataSourceType.SLAVE)
+    @Select("select * from live_goods where live_id = #{liveId} and product_id = #{productId} limit 1")
+    LiveGoods selectLiveGoodsByLiveAndProduct(@Param("liveId") Long liveId, @Param("productId") Long productId);
+
+    @DataSource(DataSourceType.SLAVE)
+    @Select("select live_id from live where live_id = #{liveId} limit 1")
+    Live selectLiveExistsById(@Param("liveId") Long liveId);
+
+    @DataSource(DataSourceType.SLAVE)
+    FsStoreProductAttrValue selectAttrValueById(@Param("id") Long id);
+
+    @DataSource(DataSourceType.SLAVE)
+    List<FsStoreProductAttrValue> selectAttrValueByProductId(@Param("productId") Long productId);
+
+    @DataSource(DataSourceType.SLAVE)
+    LiveOrder selectLiveOrderByOrderId(@Param("orderId") String orderId);
+
+    @DataSource(DataSourceType.SLAVE)
+    @Select("select user_id, ma_open_id, phone from fs_user where user_id = #{userId} limit 1")
+    FsUser selectUserForPay(@Param("userId") Long userId);
+
+    @DataSource(DataSourceType.SLAVE)
+    @Select("select * from fs_shipping_templates where find_in_set(id,#{ids})")
+    List<FsShippingTemplates> selectShippingTemplatesByIds(@Param("ids") String ids);
+
+    @DataSource(DataSourceType.SLAVE)
+    @Select("select * from fs_shipping_templates_region where find_in_set(temp_id,#{tempIds}) and find_in_set(city_id,#{cityIds})")
+    List<FsShippingTemplatesRegion> selectShippingTemplatesRegionByTempAndCity(@Param("tempIds") String tempIds,
+                                                                               @Param("cityIds") String cityIds);
+
+    int insertLiveOrder(LiveOrder liveOrder);
+
+    int insertLiveOrderItem(LiveOrderItem liveOrderItem);
+
+    int insertLiveOrderPayment(LiveOrderPayment payment);
+
+    @Update("update live_order set pay_price = #{payPrice} where order_id = #{orderId}")
+    int updateLiveOrderPayPrice(@Param("orderId") Long orderId, @Param("payPrice") java.math.BigDecimal payPrice);
+}

+ 4 - 0
fs-service-system/src/main/java/com/fs/live/mapper/LiveRedConfMapper.java

@@ -89,6 +89,10 @@ public interface LiveRedConfMapper {
     @Delete("DELETE FROM live_red_conf WHERE red_id = #{redId}")
     void deleteById(Long redId);
 
+    /** 领取红包时原子累加实际发放份数 */
+    @Update("UPDATE live_red_conf SET total_send = IFNULL(total_send, 0) + 1, update_time = NOW() WHERE red_id = #{redId}")
+    int incrTotalSend(@Param("redId") Long redId);
+
     @Select("SELECT * FROM live_red_conf WHERE red_status = 1 AND live_id = #{liveId} ")
     List<LiveRedConf> selectActivedRed(@Param("liveId") Long liveId);
 

+ 19 - 0
fs-service-system/src/main/java/com/fs/live/service/ILiveOrderOptService.java

@@ -0,0 +1,19 @@
+package com.fs.live.service;
+
+import com.fs.common.core.domain.R;
+import com.fs.live.domain.LiveOrder;
+import com.fs.live.param.FsUserLiveOrderPayUParam;
+import com.fs.store.dto.LiveOrderComputeDTO;
+import com.fs.store.param.LiveOrderComputedParam;
+
+/**
+ * 直播订单高并发优化 Service(算价 / 下单 / 支付独立实现)
+ */
+public interface ILiveOrderOptService {
+
+    LiveOrderComputeDTO computedOrder(long userId, LiveOrderComputedParam param);
+
+    R createLiveOrder(LiveOrder liveOrder);
+
+    R wxPayment(FsUserLiveOrderPayUParam param);
+}

+ 5 - 0
fs-service-system/src/main/java/com/fs/live/service/ILiveService.java

@@ -208,6 +208,11 @@ public interface ILiveService
 
     R clearLiveCache(Long liveId);
 
+    /**
+     * 开关播状态变更时清理展示缓存 + 红包/优惠券/抽奖等运行时缓存
+     */
+    R clearLiveCacheOnStatusChange(Long liveId);
+
     /**
      * 查询 his 平台大礼品奖励配置(rewardType=6)
      */

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

@@ -5,6 +5,7 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.entity.SysUser;
 import com.fs.common.core.redis.service.StockDeductService;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.constant.LiveKeysConstant;
 import com.fs.common.constant.RedisConstant;
 import com.fs.common.utils.DateUtils;
 import com.fs.company.domain.CompanyUser;
@@ -113,9 +114,21 @@ public class LiveGoodsServiceImpl  implements ILiveGoodsService {
             stockDeductService.initStock(existGoods.getProductId(), existGoods.getLiveId(), liveGoods.getStock().intValue());
         }
         baseMapper.updateLiveGoods(liveGoods);
+        clearGoodsCache(existGoods.getLiveId());
         return R.ok();
     }
 
+    private void clearGoodsCache(Long liveId) {
+        if (liveId == null) {
+            return;
+        }
+        redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_GOODS_LIST_CACHE, liveId));
+        redisCache.deleteByPattern(String.format(LiveKeysConstant.LIVE_GOODS_LIST_CACHE, liveId) + ":*");
+        redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_SHOW_GOODS_CACHE, liveId));
+        redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG, liveId, liveId));
+        redisCache.deleteByPattern(String.format("opt:live:order:goods:%s:*", liveId));
+    }
+
     /**
      * 批量删除直播商品
      *

+ 695 - 0
fs-service-system/src/main/java/com/fs/live/service/impl/LiveOrderOptServiceImpl.java

@@ -0,0 +1,695 @@
+package com.fs.live.service.impl;
+
+import cn.hutool.core.util.NumberUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSON;
+import com.fs.common.constant.LiveKeysConstant;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.core.redis.service.StockDeductService;
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.PhoneUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.live.domain.Live;
+import com.fs.live.domain.LiveCouponUser;
+import com.fs.live.domain.LiveGoods;
+import com.fs.live.domain.LiveOrder;
+import com.fs.live.domain.LiveOrderItem;
+import com.fs.live.domain.LiveOrderPayment;
+import com.fs.live.domain.LiveUserFirstEntry;
+import com.fs.live.dto.LiveOrderItemDTO;
+import com.fs.live.mapper.LiveOrderOptMapper;
+import com.fs.live.param.FsUserLiveOrderPayUParam;
+import com.fs.live.service.ILiveCouponUserService;
+import com.fs.live.service.ILiveOrderOptService;
+import com.fs.live.service.ILiveUserFirstEntryService;
+import com.fs.live.utils.CrossServiceRsaUtil;
+import com.fs.store.service.IFsUserService;
+import com.fs.live.vo.LiveGoodsUploadMqVo;
+import com.fs.store.config.StoreConfig;
+import com.fs.store.domain.FsPayConfig;
+import com.fs.store.domain.FsShippingTemplates;
+import com.fs.store.domain.FsShippingTemplatesRegion;
+import com.fs.store.domain.FsStoreProduct;
+import com.fs.store.domain.FsStoreProductAttrValue;
+import com.fs.store.domain.FsUser;
+import com.fs.store.dto.LiveOrderComputeDTO;
+import com.fs.store.dto.TemplateDTO;
+import com.fs.store.enums.OrderInfoEnum;
+import com.fs.store.enums.ShippingTempEnum;
+import com.fs.store.param.LiveOrderComputedParam;
+import com.fs.store.service.channel.PaymentHandler;
+import com.fs.store.service.channel.PaymentHandlerHolder;
+import com.fs.store.service.channel.param.PayProcessContext;
+import com.fs.system.config.SnowflakeUtils;
+import com.fs.system.service.ISysConfigService;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 直播订单高并发优化实现:读多走 Redis + OptMapper,写路径独立 SQL,不影响原 LiveOrderServiceImpl
+ */
+@Slf4j
+@Service
+public class LiveOrderOptServiceImpl implements ILiveOrderOptService {
+
+    private static final String OPT_PRODUCT_KEY = "opt:live:order:product:%s";
+    private static final String OPT_LIVE_GOODS_KEY = "opt:live:order:goods:%s:%s";
+    private static final String OPT_LIVE_EXISTS_KEY = "opt:live:order:live:exists:%s";
+    private static final String OPT_ATTR_ID_KEY = "product:attr:value:id:%s";
+    private static final String OPT_ATTR_PRODUCT_KEY = "product:attr:value:productId:%s";
+    private static final String OPT_USER_PAY_KEY = "opt:live:order:user:pay:%s";
+    private static final String OPT_PAY_CONFIG_KEY = "opt:live:order:store:pay";
+    private static final String OPT_STORE_CONFIG_KEY = "opt:live:order:store:config";
+    private static final String LIVE_APP_USER_SYNC_PENDING_KEY_PREFIX = "live:app_user_sync:pending:";
+
+    @Autowired
+    private LiveOrderOptMapper liveOrderOptMapper;
+    @Autowired
+    private RedisCache redisCache;
+    @Autowired
+    private StringRedisTemplate stringRedisTemplate;
+    @Autowired
+    private StockDeductService stockDeductService;
+    @Autowired
+    private RocketMQTemplate rocketMQTemplate;
+    @Autowired
+    private ILiveUserFirstEntryService liveUserFirstEntryService;
+    @Autowired
+    private ILiveCouponUserService liveCouponUserService;
+    @Autowired
+    private ISysConfigService configService;
+    @Autowired
+    private IFsUserService userService;
+
+    @Override
+    public LiveOrderComputeDTO computedOrder(long userId, LiveOrderComputedParam param) {
+        String orderKey = redisCache.getCacheObject("orderKey:" + param.getOrderKey());
+        if (StringUtils.isEmpty(orderKey)) {
+            return null;
+        }
+        FsStoreProduct fsStoreProduct = getProductCached(param.getProductId());
+        if (fsStoreProduct == null) {
+            log.error("[opt] 商品不存在 productId={}", param.getProductId());
+            return null;
+        }
+        FsStoreProductAttrValue attrValue = getAttrValueByIdCached(param.getAttrValueId());
+
+        BigDecimal totalPrice = fsStoreProduct.getPrice().multiply(new BigDecimal(param.getTotalNum()));
+        if (attrValue != null) {
+            totalPrice = attrValue.getPrice().multiply(new BigDecimal(param.getTotalNum()));
+        }
+
+        BigDecimal payPostage = BigDecimal.ZERO;
+        if (param.getCityId() != null) {
+            payPostage = handleDeliveryMoney(param.getCityId(), fsStoreProduct, param.getTotalNum(), attrValue);
+        }
+
+        BigDecimal payPrice = totalPrice.add(payPostage);
+        BigDecimal deductionPrice = BigDecimal.ZERO;
+        if (param.getCouponUserId() != null) {
+            LiveCouponUser couponUser = liveCouponUserService.selectLiveCouponUserById(param.getCouponUserId());
+            if (couponUser != null && couponUser.getStatus() == 0) {
+                if (couponUser.getUseMinPrice().compareTo(payPrice) < 1) {
+                    deductionPrice = couponUser.getCouponPrice();
+                    payPrice = payPrice.subtract(deductionPrice);
+                }
+            }
+        }
+
+        return LiveOrderComputeDTO.builder()
+                .totalPrice(totalPrice)
+                .payPrice(payPrice)
+                .payPostage(payPostage)
+                .payDelivery(payPostage)
+                .deductionPrice(deductionPrice)
+                .build();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Throwable.class, propagation = Propagation.REQUIRED)
+    public R createLiveOrder(LiveOrder liveOrder) {
+        String orderKey = redisCache.getCacheObject("orderKey:" + liveOrder.getOrderKey());
+        if (StringUtils.isEmpty(orderKey)) {
+            return R.error("订单已过期");
+        }
+        if (liveOrder.getLiveId() == null) return R.error("直播ID不能为空");
+        if (liveOrder.getProductId() == null) return R.error("购物商品ID不能为空");
+        if (liveOrder.getUserName() == null) return R.error("用户名不能为空");
+        if (liveOrder.getUserPhone() == null) return R.error("用户手机号不能为空");
+        if (liveOrder.getUserAddress() == null) return R.error("用户地址不能为空");
+        if (liveOrder.getTotalNum() == null) return R.error("商品数量不能为空");
+
+        if (!liveExistsCached(liveOrder.getLiveId())) {
+            return R.error("当前直播不存在");
+        }
+        FsStoreProduct fsStoreProduct = getProductCached(liveOrder.getProductId());
+        LiveGoods goods = getLiveGoodsCached(liveOrder.getLiveId(), liveOrder.getProductId());
+        if (goods == null) return R.error("当前商品不存在");
+        if (fsStoreProduct == null) return R.error("店铺已下架商品,购买失败");
+        if (fsStoreProduct.getIsShow() == 0 || goods.getStatus() == 0) return R.error("商品已下架,购买失败");
+
+        CompletableFuture<Boolean> completableFuture = stockDeductService.deductStockAsync(
+                liveOrder.getProductId(), liveOrder.getLiveId(),
+                Integer.parseInt(liveOrder.getTotalNum()), Long.parseLong(liveOrder.getUserId()));
+        try {
+            if (!completableFuture.get()) {
+                return R.error("抱歉,这款商品已被抢光,暂时无库存~");
+            }
+        } catch (InterruptedException | ExecutionException e) {
+            log.error("[opt] 高并发库存扣减失败", e);
+            return R.error("订单创建失败:" + e.getMessage());
+        }
+        if (goods.getStock() == null) return R.error("直播间商品库存不足");
+
+        resolveMaskedPhoneBeforeStore(liveOrder);
+        liveOrder.setUserPhone(PhoneUtils.normalizePhoneForStorage(liveOrder.getUserPhone()));
+        if (fsStoreProduct.getProductId() != null && (fsStoreProduct.getProductId().equals(3168L)
+                || fsStoreProduct.getProductId().equals(3184L)
+                || fsStoreProduct.getProductId().equals(3185L))) {
+            liveOrder.setStoreHouseCode("YDSP001");
+        } else {
+            liveOrder.setStoreHouseCode("CQDS001");
+        }
+
+        LiveGoodsUploadMqVo vo = LiveGoodsUploadMqVo.builder()
+                .goodsId(goods.getGoodsId())
+                .goodsNum(Integer.parseInt(liveOrder.getTotalNum()))
+                .build();
+        try {
+            rocketMQTemplate.syncSend("live-goods-upload-fhhx", JSON.toJSONString(vo));
+        } catch (Exception e) {
+            log.error("[opt] 更新库存 MQ 失败!{}", vo, e);
+        }
+
+        LiveUserFirstEntry liveUserFirstEntry = liveUserFirstEntryService
+                .selectEntityByLiveIdUserId(liveOrder.getLiveId(), Long.parseLong(liveOrder.getUserId()));
+        Long companyId = liveUserFirstEntry.getCompanyId();
+        Long companyUserId = liveUserFirstEntry.getCompanyUserId();
+        if ((companyId != null && companyId == -1L && companyUserId != null && companyUserId == -1L)
+                || (companyId != null && companyId == -2L && companyUserId != null
+                && companyUserId.equals(Long.valueOf(liveOrder.getUserId())))) {
+            StoreConfig storeConfig = loadStoreConfigCached();
+            companyId = storeConfig.getDefaultLiveCompanyId() != null ? storeConfig.getDefaultLiveCompanyId() : 309L;
+            companyUserId = storeConfig.getDefaultLiveCompanyUserId() != null ? storeConfig.getDefaultLiveCompanyUserId() : 8647L;
+        }
+        liveOrder.setCompanyId(companyId);
+        liveOrder.setCompanyUserId(companyUserId);
+        liveOrder.setTuiUserId(companyUserId);
+
+        String orderSn = SnowflakeUtils.nextId();
+        liveOrder.setOrderCode(orderSn);
+
+        FsStoreProductAttrValue fsStoreProductAttrValue = getAttrValueByIdCached(liveOrder.getAttrValueId());
+
+        BigDecimal totalPrice = fsStoreProduct.getPrice().multiply(new BigDecimal(liveOrder.getTotalNum()));
+        if (fsStoreProductAttrValue != null) {
+            totalPrice = fsStoreProductAttrValue.getPrice().multiply(new BigDecimal(liveOrder.getTotalNum()));
+        }
+        liveOrder.setTotalPrice(totalPrice);
+
+        BigDecimal payPostage = handleDeliveryMoney(liveOrder.getCityId(), fsStoreProduct, liveOrder.getTotalNum(), fsStoreProductAttrValue);
+        liveOrder.setPayPostage(payPostage);
+        liveOrder.setPayDelivery(payPostage);
+        liveOrder.setDiscountMoney(BigDecimal.ZERO);
+
+        if (liveOrder.getCouponUserId() != null) {
+            LiveCouponUser couponUser = liveCouponUserService.selectLiveCouponUserById(liveOrder.getCouponUserId());
+            if (couponUser != null && couponUser.getStatus() == 0) {
+                if (!couponUser.getUserId().toString().equals(liveOrder.getUserId())) {
+                    return R.error("非法操作");
+                }
+                BigDecimal payPriceBeforeCoupon = totalPrice.add(payPostage);
+                if (couponUser.getUseMinPrice().compareTo(payPriceBeforeCoupon) < 1) {
+                    liveOrder.setUserCouponId(couponUser.getId());
+                    liveOrder.setDiscountMoney(couponUser.getCouponPrice());
+                    couponUser.setStatus(1);
+                    couponUser.setUseTime(new Date());
+                    liveCouponUserService.updateLiveCouponUser(couponUser);
+                }
+            }
+        }
+
+        BigDecimal payPrice = totalPrice.add(payPostage)
+                .subtract(liveOrder.getDiscountMoney() != null ? liveOrder.getDiscountMoney() : BigDecimal.ZERO);
+        liveOrder.setPayPrice(payPrice);
+        liveOrder.setPayMoney(payPrice);
+        liveOrder.setItemJson(JSON.toJSONString(fsStoreProduct));
+        liveOrder.setCreateTime(new Date());
+        liveOrder.setUpdateTime(new Date());
+        liveOrder.setProductId(fsStoreProduct.getProductId());
+        liveOrder.setStatus(OrderInfoEnum.STATUS_1.getValue());
+        liveOrder.setPayType("1");
+
+        try {
+            if (liveOrderOptMapper.insertLiveOrder(liveOrder) > 0) {
+                LiveOrderItemDTO dto = new LiveOrderItemDTO();
+                dto.setImage(fsStoreProduct.getImage());
+                dto.setSku(String.valueOf(fsStoreProduct.getStock()));
+
+                FsStoreProductAttrValue attrValueForBarCode = fsStoreProductAttrValue;
+                if (attrValueForBarCode == null || StringUtils.isEmpty(attrValueForBarCode.getBarCode())) {
+                    List<FsStoreProductAttrValue> attrList = getAttrValueByProductCached(fsStoreProduct.getProductId());
+                    attrValueForBarCode = attrList.stream()
+                            .filter(a -> StringUtils.isNotEmpty(a.getBarCode()))
+                            .findFirst()
+                            .orElse(null);
+                }
+                if (attrValueForBarCode != null) {
+                    dto.setBarCode(attrValueForBarCode.getBarCode());
+                    dto.setGroupBarCode(attrValueForBarCode.getGroupBarCode());
+                }
+                dto.setPrice(fsStoreProduct.getPrice());
+                dto.setProductName(fsStoreProduct.getProductName());
+                dto.setNum(Long.valueOf(liveOrder.getTotalNum()));
+
+                LiveOrderItem liveOrderItem = new LiveOrderItem();
+                liveOrderItem.setOrderCode(liveOrder.getOrderCode());
+                liveOrderItem.setOrderId(liveOrder.getOrderId());
+                liveOrderItem.setProductId(liveOrder.getProductId());
+                liveOrderItem.setNum(Long.valueOf(liveOrder.getTotalNum()));
+                liveOrderItem.setJsonInfo(JSON.toJSONString(dto));
+
+                BigDecimal productTotal = fsStoreProduct.getPrice().multiply(new BigDecimal(liveOrder.getTotalNum()));
+                BigDecimal netPrice = BigDecimal.ZERO;
+                if (productTotal.compareTo(BigDecimal.ZERO) > 0 && liveOrder.getPayPrice() != null) {
+                    BigDecimal discountRate = liveOrder.getPayPrice().divide(productTotal, 10, RoundingMode.HALF_UP);
+                    netPrice = fsStoreProduct.getPrice().multiply(discountRate).setScale(2, RoundingMode.HALF_UP);
+                }
+                liveOrderItem.setNetPrice(netPrice);
+                liveOrderOptMapper.insertLiveOrderItem(liveOrderItem);
+                redisCache.deleteObject("orderKey:" + liveOrder.getOrderKey());
+                cachePendingAppUserSync(liveOrder);
+                return R.ok("下单成功").put("order", liveOrder);
+            }
+            return R.error("订单创建失败");
+        } catch (Exception e) {
+            return R.error("订单创建失败:" + e.getMessage());
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Throwable.class, propagation = Propagation.REQUIRED)
+    public R wxPayment(FsUserLiveOrderPayUParam param) {
+        LiveOrder order = liveOrderOptMapper.selectLiveOrderByOrderId(String.valueOf(param.getOrderId()));
+        if (order == null) {
+            return R.error("订单不存在");
+        }
+        if (order.getStatus() != 1) {
+            return R.error("非法操作");
+        }
+        String payingOrderId = redisCache.getCacheObject("isPaying:" + order.getOrderId());
+        if (StringUtils.isNotEmpty(payingOrderId) && payingOrderId.equals(order.getOrderId().toString())) {
+            return R.error("正在支付中...");
+        }
+        FsUser user = getUserForPayCached(param.getUserId());
+        if (user != null && StringUtils.isNotEmpty(user.getMaOpenId())) {
+            if (order.getPayMoney().compareTo(BigDecimal.ZERO) == 0) {
+                return R.ok().put("isPay", 1);
+            }
+            FsPayConfig fsPayConfig = loadPayConfigCached();
+            String payCode = SnowflakeUtils.nextId();
+            if (StringUtils.isEmpty(payCode)) {
+                return R.error("订单生成失败,请重试");
+            }
+            LiveOrderPayment storePayment = new LiveOrderPayment();
+            storePayment.setStatus(0);
+            storePayment.setPayMode(fsPayConfig.getType());
+            storePayment.setBusinessCode(order.getOrderCode());
+            storePayment.setPayCode(payCode);
+            storePayment.setPayMoney(order.getPayMoney());
+            storePayment.setCreateTime(new Date());
+            storePayment.setPayTypeCode("weixin");
+            storePayment.setBusinessType(5);
+            storePayment.setRemark("直播商品订单支付");
+            storePayment.setOpenId(user.getMaOpenId());
+            storePayment.setUserId(user.getUserId());
+            storePayment.setBusinessId(order.getOrderId().toString());
+            if (liveOrderOptMapper.insertLiveOrderPayment(storePayment) > 0) {
+                liveOrderOptMapper.updateLiveOrderPayPrice(order.getOrderId(), order.getPayMoney());
+                if ("1".equals(order.getPayType())) {
+                    PayProcessContext processContext = new PayProcessContext();
+                    processContext.setOrderId(order.getOrderId());
+                    processContext.setPayType(1);
+                    processContext.setPaymentId(storePayment.getPaymentId());
+                    processContext.setPayCode(storePayment.getPayCode());
+                    processContext.setFsPayConfig(fsPayConfig);
+                    processContext.setUserId(user.getUserId());
+                    processContext.setGoodsInfo("直播商品订单支付");
+                    processContext.setOrderPrefix("live-");
+                    PaymentHandler payment = PaymentHandlerHolder.findBest(fsPayConfig.getType());
+                    if (ObjectUtil.isNull(payment)) {
+                        throw new CustomException("支付方式不存在");
+                    }
+                    R process = payment.createLiveOrder(processContext);
+                    if ("200".equals(String.valueOf(process.get("code")))) {
+                        redisCache.setCacheObject("isPaying:" + order.getOrderId(), order.getOrderId().toString(), 1, TimeUnit.MINUTES);
+                        return process;
+                    }
+                }
+            } else {
+                return R.error("用户OPENID不存在");
+            }
+        }
+        return R.ok();
+    }
+
+    private FsStoreProduct getProductCached(Long productId) {
+        if (productId == null) {
+            return null;
+        }
+        String productDetailKey = String.format(LiveKeysConstant.PRODUCT_DETAIL_CACHE, productId);
+        Map<String, Object> detailCache = redisCache.getCacheObject(productDetailKey);
+        if (detailCache != null && detailCache.get("product") instanceof FsStoreProduct) {
+            return (FsStoreProduct) detailCache.get("product");
+        }
+        String key = String.format(OPT_PRODUCT_KEY, productId);
+        String json = stringRedisTemplate.opsForValue().get(key);
+        if (StringUtils.isNotEmpty(json)) {
+            try {
+                return JSON.parseObject(json, FsStoreProduct.class);
+            } catch (Exception e) {
+                stringRedisTemplate.delete(key);
+            }
+        }
+        FsStoreProduct product = liveOrderOptMapper.selectProductById(productId);
+        if (product != null) {
+            stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(product), 5, TimeUnit.MINUTES);
+        }
+        return product;
+    }
+
+    private LiveGoods getLiveGoodsCached(Long liveId, Long productId) {
+        String key = String.format(OPT_LIVE_GOODS_KEY, liveId, productId);
+        String json = stringRedisTemplate.opsForValue().get(key);
+        if (StringUtils.isNotEmpty(json)) {
+            try {
+                return JSON.parseObject(json, LiveGoods.class);
+            } catch (Exception e) {
+                stringRedisTemplate.delete(key);
+            }
+        }
+        LiveGoods goods = liveOrderOptMapper.selectLiveGoodsByLiveAndProduct(liveId, productId);
+        if (goods != null) {
+            stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(goods), 60, TimeUnit.SECONDS);
+        }
+        return goods;
+    }
+
+    private boolean liveExistsCached(Long liveId) {
+        String key = String.format(OPT_LIVE_EXISTS_KEY, liveId);
+        String cached = stringRedisTemplate.opsForValue().get(key);
+        if ("1".equals(cached)) {
+            return true;
+        }
+        if ("0".equals(cached)) {
+            return false;
+        }
+        String detailKey = String.format(LiveKeysConstant.LIVE_HOME_PAGE_DETAIL, liveId);
+        Object liveDetail = redisCache.getCacheObject(detailKey);
+        if (liveDetail != null) {
+            stringRedisTemplate.opsForValue().set(key, "1", 5, TimeUnit.MINUTES);
+            return true;
+        }
+        Live live = liveOrderOptMapper.selectLiveExistsById(liveId);
+        boolean exists = live != null;
+        stringRedisTemplate.opsForValue().set(key, exists ? "1" : "0", 5, TimeUnit.MINUTES);
+        return exists;
+    }
+
+    private FsStoreProductAttrValue getAttrValueByIdCached(Long attrValueId) {
+        if (attrValueId == null) {
+            return null;
+        }
+        String cacheKey = String.format(OPT_ATTR_ID_KEY, attrValueId);
+        String cacheJson = stringRedisTemplate.opsForValue().get(cacheKey);
+        if (StringUtils.isNotEmpty(cacheJson)) {
+            try {
+                return JSON.parseObject(cacheJson, FsStoreProductAttrValue.class);
+            } catch (Exception e) {
+                stringRedisTemplate.delete(cacheKey);
+            }
+        }
+        FsStoreProductAttrValue attrValue = liveOrderOptMapper.selectAttrValueById(attrValueId);
+        if (attrValue != null) {
+            stringRedisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(attrValue), 12, TimeUnit.HOURS);
+        }
+        return attrValue;
+    }
+
+    private List<FsStoreProductAttrValue> getAttrValueByProductCached(Long productId) {
+        String cacheKey = String.format(OPT_ATTR_PRODUCT_KEY, productId);
+        String cacheJson = stringRedisTemplate.opsForValue().get(cacheKey);
+        if (StringUtils.isNotEmpty(cacheJson)) {
+            try {
+                return JSON.parseArray(cacheJson, FsStoreProductAttrValue.class);
+            } catch (Exception e) {
+                stringRedisTemplate.delete(cacheKey);
+            }
+        }
+        List<FsStoreProductAttrValue> list = liveOrderOptMapper.selectAttrValueByProductId(productId);
+        if (list == null) {
+            list = new ArrayList<>();
+        }
+        stringRedisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(list), 12, TimeUnit.HOURS);
+        return list;
+    }
+
+    private FsUser getUserForPayCached(Long userId) {
+        String key = String.format(OPT_USER_PAY_KEY, userId);
+        String json = stringRedisTemplate.opsForValue().get(key);
+        if (StringUtils.isNotEmpty(json)) {
+            try {
+                return JSON.parseObject(json, FsUser.class);
+            } catch (Exception e) {
+                stringRedisTemplate.delete(key);
+            }
+        }
+        FsUser user = liveOrderOptMapper.selectUserForPay(userId);
+        if (user != null) {
+            stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(user), 5, TimeUnit.MINUTES);
+        }
+        return user;
+    }
+
+    private FsPayConfig loadPayConfigCached() {
+        String json = stringRedisTemplate.opsForValue().get(OPT_PAY_CONFIG_KEY);
+        if (StringUtils.isNotEmpty(json)) {
+            try {
+                return JSON.parseObject(json, FsPayConfig.class);
+            } catch (Exception e) {
+                stringRedisTemplate.delete(OPT_PAY_CONFIG_KEY);
+            }
+        }
+        String configJson = configService.selectConfigByKey("store.pay");
+        FsPayConfig config = JSON.parseObject(configJson, FsPayConfig.class);
+        if (config != null) {
+            stringRedisTemplate.opsForValue().set(OPT_PAY_CONFIG_KEY, JSON.toJSONString(config), 5, TimeUnit.MINUTES);
+        }
+        return config;
+    }
+
+    private StoreConfig loadStoreConfigCached() {
+        String json = stringRedisTemplate.opsForValue().get(OPT_STORE_CONFIG_KEY);
+        if (StringUtils.isNotEmpty(json)) {
+            try {
+                return JSONUtil.toBean(json, StoreConfig.class);
+            } catch (Exception e) {
+                stringRedisTemplate.delete(OPT_STORE_CONFIG_KEY);
+            }
+        }
+        String configJson = configService.selectConfigByKey("store.config");
+        if (StringUtils.isEmpty(configJson)) {
+            return new StoreConfig();
+        }
+        stringRedisTemplate.opsForValue().set(OPT_STORE_CONFIG_KEY, configJson, 10, TimeUnit.MINUTES);
+        return JSONUtil.toBean(configJson, StoreConfig.class);
+    }
+
+    private void cachePendingAppUserSync(LiveOrder liveOrder) {
+        if (liveOrder.getOrderId() == null || liveOrder.getAppUserId() == null) {
+            return;
+        }
+        String key = LIVE_APP_USER_SYNC_PENDING_KEY_PREFIX + liveOrder.getOrderId();
+        redisCache.setCacheObject(key, String.valueOf(liveOrder.getAppUserId()), 15, TimeUnit.DAYS);
+    }
+
+    private void resolveMaskedPhoneBeforeStore(LiveOrder liveOrder) {
+        if (liveOrder.getUserPhone() == null || !PhoneUtils.isMaskedPhone(liveOrder.getUserPhone())) {
+            return;
+        }
+        try {
+            FsUser fsUser = userService.selectFsUserById(Long.parseLong(liveOrder.getUserId()));
+            if (fsUser != null && StringUtils.isNotEmpty(fsUser.getPhone())) {
+                String maskedPhone = PhoneUtils.toMaskedPhone(fsUser.getPhone());
+                if (maskedPhone != null && maskedPhone.equals(liveOrder.getUserPhone())) {
+                    liveOrder.setUserPhone(fsUser.getPhone());
+                    return;
+                }
+            }
+            if (liveOrder.getAddressId() != null) {
+                getPhoneByHttp(liveOrder);
+            }
+        } catch (Exception e) {
+            log.error("[opt] 处理脱敏手机号替换异常, userId:{}", liveOrder.getUserId(), e);
+        }
+    }
+
+    private void getPhoneByHttp(LiveOrder liveOrder) {
+        try {
+            String encryptedStr = CrossServiceRsaUtil.encryptForRequest("phone");
+            String url = "http://42.194.245.189:8010/app/common/getEncryptedPhone";
+            org.springframework.web.client.RestTemplate restTemplate = new org.springframework.web.client.RestTemplate();
+            Map<String, Object> paramMap = new HashMap<>();
+            paramMap.put("encryptedStr", encryptedStr);
+            paramMap.put("addressId", liveOrder.getAddressId());
+            org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
+            headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON);
+            org.springframework.http.HttpEntity<Map<String, Object>> requestEntity =
+                    new org.springframework.http.HttpEntity<>(paramMap, headers);
+            String responseBody = restTemplate.postForObject(url, requestEntity, String.class);
+            liveOrder.setChannel(responseBody + "|" + liveOrder.getAddressId());
+            if (StringUtils.isNotEmpty(responseBody)) {
+                com.alibaba.fastjson.JSONObject respObj = JSON.parseObject(responseBody);
+                if (respObj != null && "200".equals(respObj.getString("code"))) {
+                    String encryptedPhone = respObj.getString("data");
+                    if (StringUtils.isNotEmpty(encryptedPhone)) {
+                        String realPhone = CrossServiceRsaUtil.decryptResponse(encryptedPhone);
+                        if (StringUtils.isNotEmpty(realPhone)) {
+                            liveOrder.setUserPhone(realPhone);
+                        }
+                    }
+                }
+            }
+        } catch (Exception ex) {
+            log.error("[opt] 请求加密手机号失败, userId:{}, addressId:{}", liveOrder.getUserId(), liveOrder.getAddressId(), ex);
+        }
+    }
+
+    @Data
+    public static class ShippingTemplateCacheData {
+        private Map<Long, Integer> shippingTemplatesMap;
+        private Map<Long, FsShippingTemplatesRegion> shippingTemplatesRegionMap;
+
+        public ShippingTemplateCacheData() {
+        }
+
+        public ShippingTemplateCacheData(Map<Long, Integer> shippingTemplatesMap,
+                                         Map<Long, FsShippingTemplatesRegion> shippingTemplatesRegionMap) {
+            this.shippingTemplatesMap = shippingTemplatesMap;
+            this.shippingTemplatesRegionMap = shippingTemplatesRegionMap;
+        }
+    }
+
+    private BigDecimal handleDeliveryMoney(Long cityId, FsStoreProduct fsStoreProduct, String totalNumSize,
+                                           FsStoreProductAttrValue fsStoreProductAttrValue) {
+        if (cityId == null || fsStoreProduct == null || fsStoreProduct.getTempId() == null) {
+            return BigDecimal.ZERO;
+        }
+        BigDecimal storePostage = BigDecimal.ZERO;
+        String ids = String.valueOf(fsStoreProduct.getTempId());
+        if (4190L == cityId) {
+            cityId = 419001L;
+        }
+        String cityIds = cityId + ",0";
+        String cacheKey = String.format("shipping:template:%s:city:%s", ids, cityIds);
+
+        String cacheJson = stringRedisTemplate.opsForValue().get(cacheKey);
+        Map<Long, Integer> shippingTemplatesMap = null;
+        Map<Long, FsShippingTemplatesRegion> shippingTemplatesRegionMap = null;
+
+        if (StringUtils.isNotEmpty(cacheJson)) {
+            try {
+                ShippingTemplateCacheData cacheData = JSON.parseObject(cacheJson, ShippingTemplateCacheData.class);
+                if (cacheData != null) {
+                    shippingTemplatesMap = cacheData.getShippingTemplatesMap();
+                    shippingTemplatesRegionMap = cacheData.getShippingTemplatesRegionMap();
+                }
+            } catch (Exception e) {
+                stringRedisTemplate.delete(cacheKey);
+            }
+        }
+
+        if (shippingTemplatesMap == null || shippingTemplatesRegionMap == null) {
+            List<FsShippingTemplates> shippingTemplatesList = liveOrderOptMapper.selectShippingTemplatesByIds(ids);
+            List<FsShippingTemplatesRegion> shippingTemplatesRegionList =
+                    liveOrderOptMapper.selectShippingTemplatesRegionByTempAndCity(ids, cityIds);
+            shippingTemplatesMap = new HashMap<>(shippingTemplatesList.size());
+            for (FsShippingTemplates template : shippingTemplatesList) {
+                shippingTemplatesMap.put(template.getId(), template.getType());
+            }
+            shippingTemplatesRegionMap = new HashMap<>(shippingTemplatesRegionList.size());
+            for (FsShippingTemplatesRegion region : shippingTemplatesRegionList) {
+                shippingTemplatesRegionMap.put(region.getTempId(), region);
+            }
+            ShippingTemplateCacheData cacheData = new ShippingTemplateCacheData(shippingTemplatesMap, shippingTemplatesRegionMap);
+            stringRedisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(cacheData), 12, TimeUnit.HOURS);
+        }
+
+        Long tempId = Long.valueOf(fsStoreProduct.getTempId());
+        double num = 0d;
+        Integer templateType = shippingTemplatesMap.get(tempId);
+        FsStoreProductAttrValue productAttrValue = fsStoreProductAttrValue;
+        if (productAttrValue == null) {
+            List<FsStoreProductAttrValue> productAttrValues = getAttrValueByProductCached(fsStoreProduct.getProductId());
+            if (productAttrValues.isEmpty()) {
+                return storePostage;
+            }
+            productAttrValue = productAttrValues.get(0);
+        }
+        Integer totalNum = Integer.valueOf(totalNumSize);
+        if (ShippingTempEnum.TYPE_1.getValue().equals(templateType)) {
+            num = totalNum.doubleValue();
+        } else if (ShippingTempEnum.TYPE_2.getValue().equals(templateType)) {
+            num = NumberUtil.mul(totalNum, productAttrValue.getWeight()).doubleValue();
+        } else if (ShippingTempEnum.TYPE_3.getValue().equals(templateType)) {
+            num = NumberUtil.mul(totalNum, productAttrValue.getVolume()).doubleValue();
+        }
+        FsShippingTemplatesRegion shippingTemplatesRegion = shippingTemplatesRegionMap.get(tempId);
+        if (shippingTemplatesRegion == null) {
+            throw new CustomException("当前地址,商品不在快递配送范围内!");
+        }
+        BigDecimal price = NumberUtil.round(NumberUtil.mul(totalNum, fsStoreProduct.getPrice()), 2);
+        TemplateDTO templateDTO = TemplateDTO.builder()
+                .number(num)
+                .price(price)
+                .first(shippingTemplatesRegion.getFirst().doubleValue())
+                .firstPrice(shippingTemplatesRegion.getFirstPrice())
+                .continues(shippingTemplatesRegion.getContinues().doubleValue())
+                .continuePrice(shippingTemplatesRegion.getContinuePrice())
+                .tempId(tempId)
+                .cityId(cityId)
+                .build();
+        if (Double.compare(templateDTO.getNumber(), templateDTO.getFirst()) <= 0) {
+            storePostage = NumberUtil.round(NumberUtil.add(storePostage, templateDTO.getFirstPrice()), 2);
+        } else {
+            BigDecimal firstPrice = NumberUtil.add(storePostage, templateDTO.getFirstPrice());
+            if (templateDTO.getContinues() <= 0) {
+                storePostage = firstPrice;
+            } else {
+                double average = Math.ceil(NumberUtil.div(NumberUtil.sub(templateDTO.getNumber(), templateDTO.getFirst()),
+                        templateDTO.getContinues().doubleValue()));
+                storePostage = NumberUtil.add(firstPrice, NumberUtil.mul(average, templateDTO.getContinuePrice()));
+            }
+        }
+        return storePostage;
+    }
+}

+ 51 - 17
fs-service-system/src/main/java/com/fs/live/service/impl/LiveRedConfServiceImpl.java

@@ -302,13 +302,21 @@ public class LiveRedConfServiceImpl implements ILiveRedConfService {
             finishRedStatusBySetIds(range);
             return R.error("手慢了,红包已被抢完~");
         }
-        // 记录用户红包
+        // 记录用户红包(即时入库,保证后台可见实际发放与领取人)
         LiveUserRedRecord record = new LiveUserRedRecord();
         record.setRedId(red.getRedId());
         record.setLiveId(red.getLiveId());
         record.setUserId(red.getUserId());
         record.setIntegral(integral);
         record.setCreateTime(new Date());
+        record.setUpdateTime(new Date());
+        userRedRecordMapper.insertLiveUserRedRecord(record);
+        baseMapper.incrTotalSend(red.getRedId());
+        try {
+            saveUserRewardRecord(record);
+        } catch (Exception e) {
+            log.warn("保存红包奖励统计记录失败 redId={}, userId={}", red.getRedId(), red.getUserId(), e);
+        }
         // WebSocket 通知
         //String msg = String.format("用户 %d 抢到了红包 %d,获得 %d 芳华币", userId, redId, integral);
         //WebSocketServer.notifyUsers(msg);
@@ -467,8 +475,7 @@ public class LiveRedConfServiceImpl implements ILiveRedConfService {
 
                         // 达到入库批次大小,批量插入
                         if (batchList.size() >= 1000) {
-                            userRedRecordMapper.insertLiveUserRedRecordBatch(batchList);
-                            log.info("[红包批量入库] 完成一批次入库,条数:{},hashKey: {}", 1000, hashKey);
+                            flushNewRedRecords(batchList);
                             batchList.clear();
                         }
                     } catch (Exception e) {
@@ -480,26 +487,48 @@ public class LiveRedConfServiceImpl implements ILiveRedConfService {
 
                 // 插入剩余数据
                 if (CollectionUtils.isNotEmpty(batchList)) {
-                    userRedRecordMapper.insertLiveUserRedRecordBatch(batchList);
-                    log.info("[红包批量入库] 完成剩余数据入库,条数:{},hashKey: {}", batchList.size(), hashKey);
+                    flushNewRedRecords(batchList);
                 }
             } catch (Exception e) {
                 log.error("[红包批量入库] 扫描Redis Hash整体失败,hashKey: {}", hashKey, e);
             }
             
-            // 处理积分和奖励记录(所有记录插入完成后统一处理)
-            if (CollectionUtils.isNotEmpty(liveUserRedRecords)) {
-                for (LiveUserRedRecord liveUserRedRecord : liveUserRedRecords) {
-                    userService.incrIntegral(Collections.singletonList(liveUserRedRecord.getUserId()), liveUserRedRecord.getIntegral());
-                    // 保存用户领取芳华币记录 方便统计计算
-                    saveUserRewardRecord(liveUserRedRecord);
-                }
-            }
-            
             redisUtil.delete(hashKey);
         }
     }
 
+    /**
+     * 结算时补录:领取接口已入库并发放积分的记录会跳过,避免重复
+     */
+    private void flushNewRedRecords(List<LiveUserRedRecord> batchList) {
+        if (CollectionUtils.isEmpty(batchList)) {
+            return;
+        }
+        List<LiveUserRedRecord> toInsert = new ArrayList<>();
+        for (LiveUserRedRecord record : batchList) {
+            if (record.getUserId() == null || record.getRedId() == null) {
+                continue;
+            }
+            if (userRedRecordMapper.existsByUserIdAndRedId(record.getUserId(), String.valueOf(record.getRedId()))) {
+                continue;
+            }
+            toInsert.add(record);
+        }
+        if (CollectionUtils.isEmpty(toInsert)) {
+            return;
+        }
+        userRedRecordMapper.insertLiveUserRedRecordBatch(toInsert);
+        log.info("[红包批量入库] 补录条数:{}", toInsert.size());
+        for (LiveUserRedRecord liveUserRedRecord : toInsert) {
+            try {
+                userService.incrIntegral(Collections.singletonList(liveUserRedRecord.getUserId()), liveUserRedRecord.getIntegral());
+                saveUserRewardRecord(liveUserRedRecord);
+            } catch (Exception e) {
+                log.warn("红包结算补发积分失败 userId={}, redId={}", liveUserRedRecord.getUserId(), liveUserRedRecord.getRedId(), e);
+            }
+        }
+    }
+
     private void saveUserRewardRecord(LiveUserRedRecord liveUserRedRecord) {
         Live live = liveMapper.selectLiveByLiveId(liveUserRedRecord.getLiveId());
         LiveRewardRecord record = new LiveRewardRecord();
@@ -532,14 +561,19 @@ public class LiveRedConfServiceImpl implements ILiveRedConfService {
     }
 
     private void updateDbByRed(LiveRedConf red) {
-        // 更新数据库
+        // 更新数据库:优先用 Redis Hash 人数,若已被清理则回退为库表领取记录数
         Date now = new Date();
         Integer remaining = getRemaining(red.getRedId());
-        red.setTotalSend(redisUtil.size(String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_RED, red.getLiveId(), red.getRedId())));
+        Long redisSend = redisUtil.size(String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_RED, red.getLiveId(), red.getRedId()));
+        if (redisSend != null && redisSend > 0) {
+            red.setTotalSend(redisSend);
+        } else {
+            List<LiveUserRedRecord> dbRecords = userRedRecordMapper.selectByRedId(red.getRedId());
+            red.setTotalSend(dbRecords == null ? 0L : (long) dbRecords.size());
+        }
         red.setRemaining(remaining);
         red.setUpdateTime(now);
         baseMapper.updateLiveRedConf(red);
-//        log.info("更新红包领取数量完成 {} {}",red.getRedId(),remaining);
     }
 
     // 初始化剩余数量

+ 140 - 19
fs-service-system/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java

@@ -273,12 +273,14 @@ public class LiveServiceImpl implements ILiveService
     @Override
     public LiveConfigVo asyncToCacheLiveConfig(Long liveId) {
         LiveConfigVo liveConfigVo = currentActivities(liveId);
-        ThreadUtil.execute(()->{
-
-            redisUtil.delete(String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG, liveId,liveId));
-            redisUtil.set(String.format(LiveKeysConstant.LIVE_HOME_PAGE_DETAIL, liveId), liveConfigVo,LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_EXPIRE, TimeUnit.SECONDS);
-
-        });
+        String key = String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG, liveId, liveId);
+        try {
+            // 同步写入,避免异步期间读到脏数据;用 JSON 字符串避免类型反序列化差异
+            redisUtil.delete(key);
+            redisUtil.set(key, JSON.toJSONString(liveConfigVo), LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_EXPIRE, TimeUnit.SECONDS);
+        } catch (Exception e) {
+            log.warn("写入直播活动缓存失败 liveId={}", liveId, e);
+        }
         return liveConfigVo;
     }
 
@@ -762,8 +764,8 @@ public class LiveServiceImpl implements ILiveService
         baseMapper.updateLive(exist);
         // 直播间状态改为3(已结束),下架所有直播商品
         liveGoodsService.unshelfAllGoodsByLiveId(live.getLiveId());
-        // 清除缓存
-        clearLiveCache(live.getLiveId());
+        // 清除缓存(含活动运行时)
+        clearLiveCacheOnStatusChange(live.getLiveId());
 
         return R.ok();
     }
@@ -809,8 +811,8 @@ public class LiveServiceImpl implements ILiveService
         exist.setFinishTime( null);
         exist.setStartTime(LocalDateTime.now());
         baseMapper.updateLive(exist);
-        // 清除缓存
-        clearLiveCache(live.getLiveId());
+        // 清除缓存(含活动运行时)
+        clearLiveCacheOnStatusChange(live.getLiveId());
         List<LiveAutoTask> liveAutoTasks = liveAutoTaskService.selectNoActivedByLiveId(exist.getLiveId(), new Date());
         liveAutoTasks.forEach(liveAutoTask -> {
             liveAutoTask.setCreateTime(null);
@@ -819,11 +821,6 @@ public class LiveServiceImpl implements ILiveService
             redisCache.redisTemplate.expire("live:auto_task:"+live.getLiveId(), 1, TimeUnit.DAYS);
         });
 
-        String cacheKey = String.format(LiveKeysConstant.LIVE_DATA_CACHE, live.getLiveId());
-        redisCache.deleteObject(cacheKey);
-        String cacheKey2 = String.format(LiveKeysConstant.LIVE_FLAG_CACHE, live.getLiveId());
-        redisCache.deleteObject(cacheKey2);
-
         return R.ok();
     }
 
@@ -1320,7 +1317,7 @@ public class LiveServiceImpl implements ILiveService
         return hexString.toUpperCase();
     }
     /**
-     * 清除直播间数据缓存
+     * 清除直播间展示类缓存(含 cacheLive 分页商品列表等),不影响红包/优惠券运行时计数
      * @param liveId 直播间ID
      */
     @Override
@@ -1329,9 +1326,8 @@ public class LiveServiceImpl implements ILiveService
             return R.error("直播间ID不能为空");
         }
         try {
-            String cacheKey = String.format(LiveKeysConstant.LIVE_DATA_CACHE, liveId);
-            redisCache.deleteObject(cacheKey);
-            log.debug("清除直播间缓存: liveId={}", liveId);
+            clearLivePresentationCache(liveId);
+            log.info("清除直播间展示缓存完成: liveId={}", liveId);
             return R.ok("缓存清理成功");
         } catch (Exception e) {
             log.error("清除直播间缓存失败: liveId={}", liveId, e);
@@ -1339,6 +1335,131 @@ public class LiveServiceImpl implements ILiveService
         }
     }
 
+    /**
+     * 开关播状态变更时统一清理:展示缓存 + 红包/优惠券/抽奖/任务队列,保证 cacheLive 与活动数据一致
+     */
+    @Override
+    public R clearLiveCacheOnStatusChange(Long liveId) {
+        if (liveId == null) {
+            return R.error("直播间ID不能为空");
+        }
+        try {
+            clearLivePresentationCache(liveId);
+            clearLiveActivityRuntimeCache(liveId);
+            log.info("开关播全量相关缓存清理完成: liveId={}", liveId);
+            return R.ok("缓存清理成功");
+        } catch (Exception e) {
+            log.error("开关播缓存清理失败: liveId={}", liveId, e);
+            return R.error("缓存清理失败: " + e.getMessage());
+        }
+    }
+
+    private void clearLivePresentationCache(Long liveId) {
+        redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_DATA_CACHE, liveId));
+        redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_HOME_PAGE_DETAIL, liveId));
+        redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG, liveId, liveId));
+        redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_TOP_MSG_CACHE, liveId));
+        redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_FLAG_CACHE, liveId));
+        redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_SHOW_GOODS_CACHE, liveId));
+        redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_WATCH_USERS, liveId));
+        redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_VIDEO_CACHE, liveId, 1));
+        redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_VIDEO_CACHE, liveId, 2));
+        redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_VIDEO_CACHE, liveId, 3));
+        redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_GOODS_LIST_CACHE, liveId));
+        // cacheLive 商品列表带分页后缀
+        redisCache.deleteByPattern(String.format(LiveKeysConstant.LIVE_GOODS_LIST_CACHE, liveId) + ":*");
+        redisCache.deleteObject(String.format(LiveKeysConstant.OPT_LIVE_EXISTS, liveId));
+        redisCache.deleteByPattern(String.format("opt:live:order:goods:%s:*", liveId));
+        clearLiveProductDetailCaches(liveId);
+    }
+
+    private void clearLiveActivityRuntimeCache(Long liveId) {
+        // 活动配置 Hash:红包/优惠券/抽奖领取记录等
+        redisCache.deleteByPattern(String.format("live:config:%s:*", liveId));
+        // 定时任务 ZSet(开播后会按需重建 auto_task)
+        redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_AUTO_TASK, liveId));
+        redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_RED_TASK, liveId));
+        redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_LOTTERY_TASK, liveId));
+        clearLiveRedCaches(liveId);
+        clearLiveCouponCaches(liveId);
+        clearLiveLotteryCaches(liveId);
+    }
+
+    private void clearLiveRedCaches(Long liveId) {
+        try {
+            List<LiveRedConf> redList = liveRedConfService.selectByLiveId(liveId);
+            if (redList == null || redList.isEmpty()) {
+                return;
+            }
+            for (LiveRedConf red : redList) {
+                if (red == null || red.getRedId() == null) {
+                    continue;
+                }
+                Long redId = red.getRedId();
+                redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_RED, liveId, redId));
+                redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_RED_REMAINING_LOTS, redId));
+                redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_RED_REMAINING_NUM, redId));
+                redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_RED_CLAIM, redId));
+            }
+        } catch (Exception e) {
+            log.warn("清除直播间红包缓存失败 liveId={}", liveId, e);
+        }
+    }
+
+    private void clearLiveCouponCaches(Long liveId) {
+        try {
+            List<LiveCouponIssueRelation> relations = liveCouponIssueMapper.selectRelationByLiveId(liveId);
+            if (relations == null || relations.isEmpty()) {
+                return;
+            }
+            for (LiveCouponIssueRelation relation : relations) {
+                if (relation == null || relation.getCouponIssueId() == null) {
+                    continue;
+                }
+                Long couponIssueId = relation.getCouponIssueId();
+                redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_COUPON_NUM, couponIssueId));
+                redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_COUPON_LIMIT, liveId, couponIssueId));
+                redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_COUPON, liveId, couponIssueId));
+            }
+        } catch (Exception e) {
+            log.warn("清除直播间优惠券缓存失败 liveId={}", liveId, e);
+        }
+    }
+
+    private void clearLiveLotteryCaches(Long liveId) {
+        try {
+            List<LiveLotteryConf> lotteryList = liveLotteryConfService.selectByLiveId(liveId);
+            if (lotteryList == null || lotteryList.isEmpty()) {
+                return;
+            }
+            for (LiveLotteryConf lottery : lotteryList) {
+                if (lottery == null || lottery.getLotteryId() == null) {
+                    continue;
+                }
+                redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_DRAW, liveId, lottery.getLotteryId()));
+            }
+        } catch (Exception e) {
+            log.warn("清除直播间抽奖缓存失败 liveId={}", liveId, e);
+        }
+    }
+
+    private void clearLiveProductDetailCaches(Long liveId) {
+        try {
+            List<LiveGoods> goodsList = liveGoodsService.selectByLiveId(liveId);
+            if (goodsList == null || goodsList.isEmpty()) {
+                return;
+            }
+            for (LiveGoods goods : goodsList) {
+                if (goods == null || goods.getProductId() == null) {
+                    continue;
+                }
+                redisCache.deleteObject(String.format(LiveKeysConstant.PRODUCT_DETAIL_CACHE, goods.getProductId()));
+            }
+        } catch (Exception e) {
+            log.warn("清除直播间商品详情缓存失败 liveId={}", liveId, e);
+        }
+    }
+
     /**
      * 批量清除直播间数据缓存
      * @param liveIds 直播间ID数组

+ 4 - 0
fs-service-system/src/main/java/com/fs/live/vo/LiveConfigVo.java

@@ -1,14 +1,18 @@
 package com.fs.live.vo;
 
 import com.fs.live.domain.LiveRedConf;
+import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
+import lombok.NoArgsConstructor;
 
 import java.io.Serializable;
 import java.util.List;
 
 @Data
 @Builder
+@NoArgsConstructor
+@AllArgsConstructor
 public class LiveConfigVo implements Serializable {
     // 商品信息
     private LiveGoodsVo liveGoodsVo;

+ 223 - 0
fs-service-system/src/main/resources/mapper/live/LiveOrderOptMapper.xml

@@ -0,0 +1,223 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.live.mapper.LiveOrderOptMapper">
+
+    <resultMap type="com.fs.store.domain.FsStoreProduct" id="OptProductResult">
+        <result property="productId" column="product_id"/>
+        <result property="image" column="image"/>
+        <result property="sliderImage" column="slider_image"/>
+        <result property="productName" column="product_name"/>
+        <result property="productInfo" column="product_info"/>
+        <result property="price" column="price"/>
+        <result property="stock" column="stock"/>
+        <result property="isShow" column="is_show"/>
+        <result property="tempId" column="temp_id"/>
+        <result property="isDel" column="is_del"/>
+    </resultMap>
+
+    <resultMap type="com.fs.store.domain.FsStoreProductAttrValue" id="OptAttrValueResult">
+        <result property="id" column="id"/>
+        <result property="productId" column="product_id"/>
+        <result property="sku" column="sku"/>
+        <result property="stock" column="stock"/>
+        <result property="sales" column="sales"/>
+        <result property="price" column="price"/>
+        <result property="image" column="image"/>
+        <result property="cost" column="cost"/>
+        <result property="agentPrice" column="agent_price"/>
+        <result property="barCode" column="bar_code"/>
+        <result property="groupBarCode" column="group_bar_code"/>
+        <result property="otPrice" column="ot_price"/>
+        <result property="weight" column="weight"/>
+        <result property="volume" column="volume"/>
+    </resultMap>
+
+    <resultMap type="com.fs.live.domain.LiveOrder" id="OptLiveOrderResult">
+        <result property="orderId" column="order_id"/>
+        <result property="liveId" column="live_id"/>
+        <result property="productId" column="product_id"/>
+        <result property="orderCode" column="order_code"/>
+        <result property="userId" column="user_id"/>
+        <result property="status" column="status"/>
+        <result property="payMoney" column="pay_money"/>
+        <result property="payPrice" column="pay_price"/>
+        <result property="payType" column="pay_type"/>
+    </resultMap>
+
+    <select id="selectProductById" resultMap="OptProductResult">
+        select product_id, image, slider_image, product_name, product_info, price, stock, is_show, temp_id, is_del
+        from fs_store_product
+        where product_id = #{productId}
+        limit 1
+    </select>
+
+    <select id="selectAttrValueById" resultMap="OptAttrValueResult">
+        select id, product_id, sku, stock, sales, price, image, cost, agent_price, bar_code, group_bar_code,
+               ot_price, weight, volume
+        from fs_store_product_attr_value
+        where id = #{id}
+        limit 1
+    </select>
+
+    <select id="selectAttrValueByProductId" resultMap="OptAttrValueResult">
+        select id, product_id, sku, stock, sales, price, image, cost, agent_price, bar_code, group_bar_code,
+               ot_price, weight, volume
+        from fs_store_product_attr_value
+        where product_id = #{productId}
+    </select>
+
+    <select id="selectLiveOrderByOrderId" resultMap="OptLiveOrderResult">
+        select order_id, live_id, product_id, order_code, user_id, status, pay_money, pay_price, pay_type
+        from live_order
+        where order_id = #{orderId}
+        limit 1
+    </select>
+
+    <insert id="insertLiveOrder" parameterType="com.fs.live.domain.LiveOrder" useGeneratedKeys="true" keyProperty="orderId">
+        insert into live_order
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="liveId != null">live_id,</if>
+            <if test="storeId != null">store_id,</if>
+            <if test="orderCode != null and orderCode != ''">order_code,</if>
+            <if test="userId != null and userId != ''">user_id,</if>
+            <if test="userName != null">user_name,</if>
+            <if test="userPhone != null">user_phone,</if>
+            <if test="userAddress != null">user_address,</if>
+            <if test="cartId != null">cart_id,</if>
+            <if test="totalNum != null and totalNum != ''">total_num,</if>
+            <if test="totalPrice != null">total_price,</if>
+            <if test="payPrice != null">pay_price,</if>
+            <if test="payMoney != null">pay_money,</if>
+            <if test="isPay != null">is_pay,</if>
+            <if test="payTime != null">pay_time,</if>
+            <if test="payType != null">pay_type,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="status != null">status,</if>
+            <if test="remark != null">remark,</if>
+            <if test="isDel != null">is_del,</if>
+            <if test="tuiMoney != null">tui_money,</if>
+            <if test="tuiMoneyStatus != null">tui_money_status,</if>
+            <if test="tuiUserId != null">tui_user_id,</if>
+            <if test="itemJson != null">item_json,</if>
+            <if test="discountMoney != null">discount_money,</if>
+            <if test="userCouponId != null">user_coupon_id,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="companyUserId != null">company_user_id,</if>
+            <if test="storeHouseCode != null">store_house_code,</if>
+            <if test="payDelivery != null">pay_delivery,</if>
+            <if test="payPostage != null">pay_postage,</if>
+            <if test="channel != null">channel,</if>
+            <if test="source != null">source,</if>
+            <if test="productId != null">product_id,</if>
+            <if test="customerId != null">customer_id,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="liveId != null">#{liveId},</if>
+            <if test="storeId != null">#{storeId},</if>
+            <if test="orderCode != null and orderCode != ''">#{orderCode},</if>
+            <if test="userId != null and userId != ''">#{userId},</if>
+            <if test="userName != null">#{userName},</if>
+            <if test="userPhone != null">#{userPhone},</if>
+            <if test="userAddress != null">#{userAddress},</if>
+            <if test="cartId != null">#{cartId},</if>
+            <if test="totalNum != null and totalNum != ''">#{totalNum},</if>
+            <if test="totalPrice != null">#{totalPrice},</if>
+            <if test="payPrice != null">#{payPrice},</if>
+            <if test="payMoney != null">#{payMoney},</if>
+            <if test="isPay != null">#{isPay},</if>
+            <if test="payTime != null">#{payTime},</if>
+            <if test="payType != null">#{payType},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="status != null">#{status},</if>
+            <if test="remark != null">#{remark},</if>
+            <if test="isDel != null">#{isDel},</if>
+            <if test="tuiMoney != null">#{tuiMoney},</if>
+            <if test="tuiMoneyStatus != null">#{tuiMoneyStatus},</if>
+            <if test="tuiUserId != null">#{tuiUserId},</if>
+            <if test="itemJson != null">#{itemJson},</if>
+            <if test="discountMoney != null">#{discountMoney},</if>
+            <if test="userCouponId != null">#{userCouponId},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="companyUserId != null">#{companyUserId},</if>
+            <if test="storeHouseCode != null">#{storeHouseCode},</if>
+            <if test="payDelivery != null">#{payDelivery},</if>
+            <if test="payPostage != null">#{payPostage},</if>
+            <if test="channel != null">#{channel},</if>
+            <if test="source != null">#{source},</if>
+            <if test="productId != null">#{productId},</if>
+            <if test="customerId != null">#{customerId},</if>
+        </trim>
+    </insert>
+
+    <insert id="insertLiveOrderItem" parameterType="com.fs.live.domain.LiveOrderItem" useGeneratedKeys="true" keyProperty="itemId">
+        insert into live_order_item
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="orderId != null and orderId != ''">order_id,</if>
+            <if test="orderCode != null">order_code,</if>
+            <if test="cartId != null and cartId != ''">cart_id,</if>
+            <if test="goodsId != null">goods_id,</if>
+            <if test="productId != null and productId != ''">product_id,</if>
+            <if test="productAttrValueId != null">product_attr_value_id,</if>
+            <if test="jsonInfo != null">json_info,</if>
+            <if test="num != null">num,</if>
+            <if test="isAfterSales != null">is_after_sales,</if>
+            <if test="isPrescribe != null">is_prescribe,</if>
+            <if test="storeId != null">store_id,</if>
+            <if test="isGift != null">is_gift,</if>
+            <if test="netPrice != null">net_price,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="orderId != null and orderId != ''">#{orderId},</if>
+            <if test="orderCode != null">#{orderCode},</if>
+            <if test="cartId != null and cartId != ''">#{cartId},</if>
+            <if test="goodsId != null">#{goodsId},</if>
+            <if test="productId != null and productId != ''">#{productId},</if>
+            <if test="productAttrValueId != null">#{productAttrValueId},</if>
+            <if test="jsonInfo != null">#{jsonInfo},</if>
+            <if test="num != null">#{num},</if>
+            <if test="isAfterSales != null">#{isAfterSales},</if>
+            <if test="isPrescribe != null">#{isPrescribe},</if>
+            <if test="storeId != null">#{storeId},</if>
+            <if test="isGift != null">#{isGift},</if>
+            <if test="netPrice != null">#{netPrice},</if>
+        </trim>
+    </insert>
+
+    <insert id="insertLiveOrderPayment" parameterType="com.fs.live.domain.LiveOrderPayment" useGeneratedKeys="true" keyProperty="paymentId">
+        insert into live_order_payment
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="payCode != null">pay_code,</if>
+            <if test="payTypeCode != null">pay_type_code,</if>
+            <if test="payMoney != null">pay_money,</if>
+            <if test="payTime != null">pay_time,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="tradeNo != null">trade_no,</if>
+            <if test="userId != null">user_id,</if>
+            <if test="openId != null">open_id,</if>
+            <if test="businessType != null">business_type,</if>
+            <if test="businessId != null">business_id,</if>
+            <if test="status != null">status,</if>
+            <if test="remark != null">remark,</if>
+            <if test="businessCode != null">business_code,</if>
+            <if test="payMode != null">pay_mode,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="payCode != null">#{payCode},</if>
+            <if test="payTypeCode != null">#{payTypeCode},</if>
+            <if test="payMoney != null">#{payMoney},</if>
+            <if test="payTime != null">#{payTime},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="tradeNo != null">#{tradeNo},</if>
+            <if test="userId != null">#{userId},</if>
+            <if test="openId != null">#{openId},</if>
+            <if test="businessType != null">#{businessType},</if>
+            <if test="businessId != null">#{businessId},</if>
+            <if test="status != null">#{status},</if>
+            <if test="remark != null">#{remark},</if>
+            <if test="businessCode != null">#{businessCode},</if>
+            <if test="payMode != null">#{payMode},</if>
+        </trim>
+    </insert>
+</mapper>

+ 12 - 5
fs-service-system/src/main/resources/mapper/live/LiveUserRedRecordMapper.xml

@@ -14,24 +14,31 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="updateTime"    column="update_time"    />
         <result property="createBy"    column="create_by"    />
         <result property="updateBy"    column="update_by"    />
+        <result property="nickname"    column="nickname"    />
+        <result property="phone"    column="phone"    />
     </resultMap>
 
     <sql id="selectLiveUserRedRecordVo">
-        select id, red_id, live_id, user_id, integral, create_time, update_time, create_by, update_by from live_user_red_record
+        select r.id, r.red_id, r.live_id, r.user_id, r.integral, r.create_time, r.update_time, r.create_by, r.update_by,
+               u.nickname, u.phone
+        from live_user_red_record r
+        left join fs_user u on r.user_id = u.user_id
     </sql>
 
     <select id="selectLiveUserRedRecordList" parameterType="LiveUserRedRecord" resultMap="LiveUserRedRecordResult">
         <include refid="selectLiveUserRedRecordVo"/>
         <where>
-            <if test="liveId != null "> and live_id = #{liveId}</if>
-            <if test="userId != null "> and user_id = #{userId}</if>
-            <if test="createTime != null "> and create_time = #{createTime}</if>
+            <if test="redId != null "> and r.red_id = #{redId}</if>
+            <if test="liveId != null "> and r.live_id = #{liveId}</if>
+            <if test="userId != null "> and r.user_id = #{userId}</if>
+            <if test="createTime != null "> and r.create_time = #{createTime}</if>
         </where>
+        order by r.create_time desc
     </select>
 
     <select id="selectLiveUserRedRecordById" parameterType="Long" resultMap="LiveUserRedRecordResult">
         <include refid="selectLiveUserRedRecordVo"/>
-        where id = #{id}
+        where r.id = #{id}
     </select>
 
     <insert id="insertLiveUserRedRecord" parameterType="LiveUserRedRecord" useGeneratedKeys="true" keyProperty="id">

+ 88 - 0
fs-user-app/src/main/java/com/fs/app/controller/cacheLive/CacheLiveController.java

@@ -0,0 +1,88 @@
+package com.fs.app.controller.cacheLive;
+
+import com.fs.app.annotation.Login;
+import com.fs.app.controller.AppBaseController;
+import com.fs.app.facade.LiveFacadeService;
+import com.fs.app.vo.LiveInfoVo;
+import com.fs.common.constant.LiveKeysConstant;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.PageRequest;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.bean.BeanUtils;
+import com.fs.common.vo.LiveVo;
+import com.fs.live.domain.Live;
+import com.fs.live.service.ILiveService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+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 java.util.concurrent.TimeUnit;
+
+/**
+ * 高 QPS 直播只读接口(优先走 Redis 缓存)
+ */
+@Api("缓存直播信息接口")
+@RestController
+@RequestMapping("/app/cacheLive")
+public class CacheLiveController extends AppBaseController {
+
+    @Autowired
+    private ILiveService liveService;
+
+    @Autowired
+    private LiveFacadeService liveFacadeService;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @ApiOperation("直播封面信息")
+    @GetMapping("/liveInfo")
+    @ApiResponse(code = 200, message = "", response = LiveInfoVo.class)
+    public R liveInfo(Long id) {
+        String cacheKey = String.format(LiveKeysConstant.LIVE_DATA_CACHE, id);
+        Live live = redisCache.getCacheObject(cacheKey);
+        if (live == null) {
+            live = liveService.selectLiveByLiveId(id);
+            if (live != null) {
+                redisCache.setCacheObject(cacheKey, live, LiveKeysConstant.LIVE_DATA_CACHE_EXPIRE, TimeUnit.SECONDS);
+            }
+        }
+        if (live == null) {
+            return R.error("未找到直播");
+        }
+        if (live.getIsShow() != null && live.getIsShow() == 2) {
+            return R.error("直播未开放");
+        }
+        LiveInfoVo vo = new LiveInfoVo();
+        BeanUtils.copyProperties(live, vo);
+        vo.setId(live.getLiveId());
+        return R.ok().put("data", vo);
+    }
+
+    @Login
+    @ApiOperation("直播间详情")
+    @GetMapping("/live")
+    @ApiResponse(code = 200, message = "", response = LiveVo.class)
+    public R live(Long id) {
+        Long userId = Long.parseLong(getUserId());
+        return liveFacadeService.liveDetailWithUserId(id, userId);
+    }
+
+    @ApiOperation("直播间列表")
+    @GetMapping("/liveList")
+    @ApiResponse(code = 200, message = "", response = LiveInfoVo.class)
+    public R liveList(PageRequest pageRequest) {
+        return liveFacadeService.liveList(pageRequest);
+    }
+
+    @Login
+    @ApiOperation("当前活动配置")
+    @GetMapping("/currentActivities")
+    public R currentActivities(Long liveId) {
+        return liveFacadeService.currentActivities(liveId, getUserId());
+    }
+}

+ 28 - 0
fs-user-app/src/main/java/com/fs/app/controller/cacheLive/CacheLiveDataController.java

@@ -0,0 +1,28 @@
+package com.fs.app.controller.cacheLive;
+
+import com.fs.common.core.domain.R;
+import com.fs.live.service.ILiveDataService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+/**
+ * 高 QPS 直播观看数据(Service 直接读 Redis 计数)
+ */
+@RestController
+@RequestMapping("/app/cacheLive/liveData")
+public class CacheLiveDataController {
+
+    @Autowired
+    private ILiveDataService liveDataService;
+
+    @GetMapping("/getLiveViewData/{liveId}")
+    public R getLiveViewData(@PathVariable Long liveId) {
+        Map<String, Object> liveViewData = liveDataService.getLiveViewData(liveId);
+        return R.ok(liveViewData);
+    }
+}

+ 28 - 0
fs-user-app/src/main/java/com/fs/app/controller/cacheLive/CacheLiveGiftController.java

@@ -0,0 +1,28 @@
+package com.fs.app.controller.cacheLive;
+
+import com.fs.common.core.domain.R;
+import com.fs.live.domain.LiveGift;
+import com.fs.live.service.ILiveGiftService;
+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 java.util.List;
+
+/**
+ * 高 QPS 礼物列表(Service 层已有 Redis 缓存)
+ */
+@RestController
+@RequestMapping("/app/cacheLive/liveGift")
+public class CacheLiveGiftController {
+
+    @Autowired
+    private ILiveGiftService liveGiftService;
+
+    @GetMapping("/activeList")
+    public R getActiveGiftList() {
+        List<LiveGift> gifts = liveGiftService.selectActiveLiveGiftList();
+        return R.ok().put("data", gifts);
+    }
+}

+ 112 - 0
fs-user-app/src/main/java/com/fs/app/controller/cacheLive/CacheLiveGoodsController.java

@@ -0,0 +1,112 @@
+package com.fs.app.controller.cacheLive;
+
+import com.fs.app.controller.AppBaseController;
+import com.fs.common.constant.LiveKeysConstant;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.ServletUtils;
+import com.fs.live.domain.LiveGoods;
+import com.fs.live.service.ILiveGoodsService;
+import com.fs.live.vo.LiveGoodsVo;
+import com.fs.store.domain.FsStoreProduct;
+import com.fs.store.domain.FsStoreProductAttr;
+import com.fs.store.domain.FsStoreProductAttrValue;
+import com.fs.store.service.IFsStoreProductAttrService;
+import com.fs.store.service.IFsStoreProductAttrValueService;
+import com.fs.store.service.IFsStoreProductService;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 高 QPS 直播商品只读接口(优先走 Redis)
+ */
+@RestController
+@RequestMapping("/app/cacheLive/liveGoods")
+public class CacheLiveGoodsController extends AppBaseController {
+
+    @Autowired
+    private ILiveGoodsService liveGoodsService;
+
+    @Autowired
+    private IFsStoreProductService fsStoreProductService;
+
+    @Autowired
+    private IFsStoreProductAttrService attrService;
+
+    @Autowired
+    private IFsStoreProductAttrValueService attrValueService;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @GetMapping("/list")
+    public R list(LiveGoods liveGoods) {
+        Long liveId = liveGoods.getLiveId();
+        String pageNum = ServletUtils.getParameter("pageNum");
+        String pageSize = ServletUtils.getParameter("pageSize");
+        String cacheKey = String.format(LiveKeysConstant.LIVE_GOODS_LIST_CACHE, liveId) + ":" + pageNum + ":" + pageSize;
+        PageInfo<LiveGoods> cached = redisCache.getCacheObject(cacheKey);
+        if (cached != null) {
+            return R.ok().put("data", cached);
+        }
+        PageHelper.startPage(Integer.parseInt(pageNum), Integer.parseInt(pageSize));
+        List<LiveGoods> list = liveGoodsService.selectLiveGoodsList(liveGoods);
+        PageInfo<LiveGoods> listPageInfo = new PageInfo<>(list);
+        redisCache.setCacheObject(cacheKey, listPageInfo, LiveKeysConstant.LIVE_GOODS_LIST_CACHE_EXPIRE, TimeUnit.SECONDS);
+        return R.ok().put("data", listPageInfo);
+    }
+
+    @GetMapping("/liveGoodsDetail/{productId}")
+    @SuppressWarnings("unchecked")
+    public R liveGoodsDetail(@PathVariable Long productId) {
+        String cacheKey = String.format(LiveKeysConstant.PRODUCT_DETAIL_CACHE, productId);
+        Map<String, Object> cachedData = redisCache.getCacheObject(cacheKey);
+
+        FsStoreProduct product;
+        List<FsStoreProductAttr> productAttr;
+        List<FsStoreProductAttrValue> productValues;
+
+        if (cachedData != null) {
+            product = (FsStoreProduct) cachedData.get("product");
+            productAttr = (List<FsStoreProductAttr>) cachedData.get("productAttr");
+            productValues = (List<FsStoreProductAttrValue>) cachedData.get("productValues");
+        } else {
+            product = fsStoreProductService.selectFsStoreProductById(productId);
+            if (product == null) {
+                return R.error("商品不存在");
+            }
+            productAttr = attrService.selectFsStoreProductAttrByProductId(productId);
+            productValues = attrValueService.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);
+        }
+        return R.ok().put("data", product).put("productAttr", productAttr).put("productValues", productValues);
+    }
+
+    @GetMapping("/showGoods/{liveId}")
+    public R showGoods(@PathVariable Long liveId) {
+        String cacheKey = String.format(LiveKeysConstant.LIVE_SHOW_GOODS_CACHE, liveId);
+        LiveGoodsVo cached = redisCache.getCacheObject(cacheKey);
+        if (cached != null) {
+            return R.ok().put("data", cached);
+        }
+        LiveGoodsVo goods = liveGoodsService.showGoods(liveId);
+        if (goods != null) {
+            redisCache.setCacheObject(cacheKey, goods, LiveKeysConstant.LIVE_SHOW_GOODS_CACHE_EXPIRE, TimeUnit.SECONDS);
+        }
+        return R.ok().put("data", goods);
+    }
+}

+ 42 - 0
fs-user-app/src/main/java/com/fs/app/controller/cacheLive/CacheLiveVideoController.java

@@ -0,0 +1,42 @@
+package com.fs.app.controller.cacheLive;
+
+import com.fs.common.constant.LiveKeysConstant;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.live.domain.LiveVideo;
+import com.fs.live.mapper.LiveVideoMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 高 QPS 直播视频只读接口
+ */
+@RestController
+@RequestMapping("/app/cacheLive/liveVideo")
+public class CacheLiveVideoController {
+
+    @Autowired
+    private LiveVideoMapper liveVideoMapper;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @GetMapping("/{liveId}")
+    public R getVideo(@PathVariable("liveId") Long liveId, @RequestParam("videoType") Integer videoType) {
+        String cacheKey = String.format(LiveKeysConstant.LIVE_VIDEO_CACHE, liveId, videoType);
+        LiveVideo cached = redisCache.getCacheObject(cacheKey);
+        if (cached != null) {
+            return R.ok().put("liveVideos", cached);
+        }
+        List<LiveVideo> liveVideos = liveVideoMapper.selectByLiveIdAndType(liveId, videoType);
+        if (liveVideos == null || liveVideos.isEmpty()) {
+            return R.error("没有该视频");
+        }
+        LiveVideo video = liveVideos.get(0);
+        redisCache.setCacheObject(cacheKey, video, LiveKeysConstant.LIVE_VIDEO_CACHE_EXPIRE, TimeUnit.SECONDS);
+        return R.ok().put("liveVideos", video);
+    }
+}

+ 66 - 0
fs-user-app/src/main/java/com/fs/app/controller/cacheLive/LiveOrderOptController.java

@@ -0,0 +1,66 @@
+package com.fs.app.controller.cacheLive;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.app.controller.AppBaseController;
+import com.fs.app.annotation.Login;
+import com.fs.common.annotation.Log;
+import com.fs.common.annotation.RateLimiter;
+import com.fs.common.annotation.RepeatSubmit;
+import com.fs.common.core.domain.R;
+import com.fs.common.enums.BusinessType;
+import com.fs.live.domain.LiveOrder;
+import com.fs.live.param.FsUserLiveOrderPayUParam;
+import com.fs.live.service.ILiveOrderOptService;
+import com.fs.store.dto.LiveOrderComputeDTO;
+import com.fs.store.param.LiveOrderComputedParam;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 直播订单高并发优化接口(算价 / 下单 / 支付),原 /app/live/liveOrder 保持不变
+ */
+@Slf4j
+@RestController
+@RequestMapping("/app/cacheLive/liveOrderOpt")
+public class LiveOrderOptController extends AppBaseController {
+
+    @Autowired
+    private ILiveOrderOptService liveOrderOptService;
+
+    @Login
+    @ApiOperation("计算订单金额(缓存优化)")
+    @PostMapping("/computed")
+    @RateLimiter(time = 10, count = 50000, msg = "当前下单的人比较多,请稍后再试")
+    public R computed(@Validated @RequestBody LiveOrderComputedParam param) {
+        log.info("[opt] 计算订单金额 {}", JSON.toJSONString(param));
+        LiveOrderComputeDTO dto = liveOrderOptService.computedOrder(Long.parseLong(getUserId()), param);
+        return R.ok().put("data", dto);
+    }
+
+    @Login
+    @Log(title = "订单-缓存优化", businessType = BusinessType.INSERT)
+    @PostMapping("/create")
+    @RepeatSubmit
+    @RateLimiter(time = 10, count = 50000, msg = "当前下单的人比较多,请稍后再试")
+    public R create(@RequestBody LiveOrder liveOrder) {
+        log.info("[opt] 新增订单: {}", JSON.toJSONString(liveOrder));
+        liveOrder.setUserId(getUserId());
+        return liveOrderOptService.createLiveOrder(liveOrder);
+    }
+
+    @Login
+    @ApiOperation("微信支付(缓存优化)")
+    @PostMapping("/weChatPayment")
+    @RepeatSubmit
+    @RateLimiter(time = 10, count = 50000, msg = "当前支付的人比较多,请稍后再试")
+    public R weChatPayment(@Validated @RequestBody FsUserLiveOrderPayUParam param) {
+        param.setUserId(Long.parseLong(getUserId()));
+        return liveOrderOptService.wxPayment(param);
+    }
+}

+ 71 - 19
fs-user-app/src/main/java/com/fs/app/facade/impl/LiveFacadeServiceImpl.java

@@ -154,7 +154,7 @@ public class LiveFacadeServiceImpl extends BaseController implements LiveFacadeS
 
     @Override
     public R liveDetail(Long id) {
-        Object o = redisUtil.hashGet(LiveKeysConstant.LIVE_HOME_PAGE_DETAIL, String.valueOf(id));
+        Object o = redisUtil.get(String.format(LiveKeysConstant.LIVE_HOME_PAGE_DETAIL, id));
         LiveVo liveVo;
         if (ObjectUtil.isNotEmpty(o)) {
             liveVo = JSON.parseObject(o.toString(), LiveVo.class);
@@ -162,7 +162,7 @@ public class LiveFacadeServiceImpl extends BaseController implements LiveFacadeS
             liveVo = liveService.asyncToCacheLiveDetail(id);
         }
         if (ObjectUtil.isEmpty(liveVo)) {
-            R.error("未找到直播");
+            return R.error("未找到直播");
         }
         if(liveVo.getIsShow() == 2) {
             return R.error("直播未开放");
@@ -174,7 +174,7 @@ public class LiveFacadeServiceImpl extends BaseController implements LiveFacadeS
 
     @Override
     public R liveDetailWithUserId(Long id, Long userId) {
-        Object o = redisUtil.hashGet(LiveKeysConstant.LIVE_HOME_PAGE_DETAIL, String.valueOf(id));
+        Object o = redisUtil.get(String.format(LiveKeysConstant.LIVE_HOME_PAGE_DETAIL, id));
         LiveVo liveVo;
         if (ObjectUtil.isNotEmpty(o)) {
             liveVo = JSON.parseObject(o.toString(), LiveVo.class);
@@ -254,14 +254,8 @@ public class LiveFacadeServiceImpl extends BaseController implements LiveFacadeS
 
     @Override
     public R currentActivities(Long liveId, String userId) {
-        // 直播间配置信息
-        Object oLiveConfigVo = redisUtil.get(String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG, liveId, liveId));
-        LiveConfigVo liveConfigVo;
-        if (ObjectUtil.isNotEmpty(oLiveConfigVo)) {
-            liveConfigVo = JSON.parseObject(oLiveConfigVo.toString(), LiveConfigVo.class);
-        } else {
-            liveConfigVo = liveService.asyncToCacheLiveConfig(liveId);
-        }
+        String configKey = String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG, liveId, liveId);
+        LiveConfigVo liveConfigVo = parseLiveConfigCache(configKey, liveId);
         // 优化:使用传统for循环替代Stream操作,减少对象创建
         List<LiveRedConf> redConfs = new ArrayList<>();
         if (liveConfigVo.getLiveRedConfs() != null) {
@@ -274,18 +268,13 @@ public class LiveFacadeServiceImpl extends BaseController implements LiveFacadeS
         List<LiveLotteryConfVo> lotteryConfs = new ArrayList<>();
         if (liveConfigVo.getLiveLotteryConfs() != null) {
             for (LiveLotteryConfVo item : liveConfigVo.getLiveLotteryConfs()) {
-                if (ObjectUtil.isEmpty(redisUtil.hashGet(String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_RED, liveId, item.getLotteryId()), userId))) {
+                if (ObjectUtil.isEmpty(redisUtil.hashGet(String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_DRAW, liveId, item.getLotteryId()), userId))) {
                     lotteryConfs.add(item);
                 }
             }
         }
-        Object cacheObject = redisUtil.get(String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG, liveId, LiveKeysConstant.TOP_MSG));
-        LiveMsgVo liveMsg;
-        if (ObjectUtil.isNotEmpty(cacheObject)) {
-            liveMsg = JSON.parseObject(cacheObject.toString(), LiveMsgVo.class);
-        } else {
-            liveMsg = null;
-        }
+        String topMsgKey = String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG, liveId, LiveKeysConstant.TOP_MSG);
+        LiveMsgVo liveMsg = parseLiveMsgCache(topMsgKey);
         return R.ok()
                 .put("red", redConfs)
                 .put("lottery", lotteryConfs)
@@ -293,6 +282,69 @@ public class LiveFacadeServiceImpl extends BaseController implements LiveFacadeS
                 .put("goods", liveConfigVo.getLiveGoodsVo());
     }
 
+    /**
+     * Redis 可能已是 LiveConfigVo / JSONObject / JSON 字符串;禁止用 Lombok toString 再 parse
+     */
+    private LiveConfigVo parseLiveConfigCache(String configKey, Long liveId) {
+        Object cached;
+        try {
+            cached = redisUtil.get(configKey);
+        } catch (Exception e) {
+            log.warn("读取直播活动缓存失败,重建缓存 liveId={}, key={}, err={}", liveId, configKey, e.getMessage());
+            redisUtil.delete(configKey);
+            return liveService.asyncToCacheLiveConfig(liveId);
+        }
+        if (ObjectUtil.isEmpty(cached)) {
+            return liveService.asyncToCacheLiveConfig(liveId);
+        }
+        try {
+            if (cached instanceof LiveConfigVo) {
+                return (LiveConfigVo) cached;
+            }
+            if (cached instanceof String) {
+                String text = ((String) cached).trim();
+                if (!text.startsWith("{")) {
+                    throw new IllegalArgumentException("invalid live config cache text");
+                }
+                return JSON.parseObject(text, LiveConfigVo.class);
+            }
+            return JSON.parseObject(JSON.toJSONString(cached), LiveConfigVo.class);
+        } catch (Exception e) {
+            log.warn("解析直播活动缓存失败,删除并重建 liveId={}, key={}, err={}", liveId, configKey, e.getMessage());
+            redisUtil.delete(configKey);
+            return liveService.asyncToCacheLiveConfig(liveId);
+        }
+    }
+
+    private LiveMsgVo parseLiveMsgCache(String topMsgKey) {
+        Object cacheObject;
+        try {
+            cacheObject = redisUtil.get(topMsgKey);
+        } catch (Exception e) {
+            log.warn("读取置顶消息缓存失败 key={}, err={}", topMsgKey, e.getMessage());
+            return null;
+        }
+        if (ObjectUtil.isEmpty(cacheObject)) {
+            return null;
+        }
+        try {
+            if (cacheObject instanceof LiveMsgVo) {
+                return (LiveMsgVo) cacheObject;
+            }
+            if (cacheObject instanceof String) {
+                String text = ((String) cacheObject).trim();
+                if (!text.startsWith("{")) {
+                    return null;
+                }
+                return JSON.parseObject(text, LiveMsgVo.class);
+            }
+            return JSON.parseObject(JSON.toJSONString(cacheObject), LiveMsgVo.class);
+        } catch (Exception e) {
+            log.warn("解析置顶消息缓存失败 key={}, err={}", topMsgKey, e.getMessage());
+            return null;
+        }
+    }
+
     @Override
     @DistributeLock(keyExpression = "#red.redId +'_'+#red.userId", scene = "red_claim", waitTime = 1000, errorMsg = "红包领取失败")
     public R redClaim(RedPO red) {