Explorar el Código

卓美马甲功能

yuhongqi hace 5 días
padre
commit
ae0d8685ce
Se han modificado 28 ficheros con 460 adiciones y 38 borrados
  1. 1 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreAfterSalesScrmController.java
  2. 12 0
      fs-common/src/main/java/com/fs/common/constant/LiveKeysConstant.java
  3. 18 0
      fs-common/src/main/java/com/fs/common/core/redis/RedisCache.java
  4. 2 9
      fs-live-app/src/main/java/com/fs/live/task/Task.java
  5. 2 9
      fs-live-ws/src/main/java/com/fs/live/task/Task.java
  6. 53 0
      fs-live-ws/src/main/java/com/fs/live/ws/bean/WsConnectContext.java
  7. 6 0
      fs-live-ws/src/main/java/com/fs/live/ws/constant/WsAttrConstant.java
  8. 16 0
      fs-live-ws/src/main/java/com/fs/live/ws/handler/WsAuthHandler.java
  9. 13 2
      fs-live-ws/src/main/java/com/fs/live/ws/handler/WsLiveChatHandler.java
  10. 2 1
      fs-live-ws/src/main/java/com/fs/live/ws/service/ILiveWsConnectionService.java
  11. 101 2
      fs-live-ws/src/main/java/com/fs/live/ws/service/impl/LiveWsConnectionServiceImpl.java
  12. 13 2
      fs-live-ws/src/main/java/com/fs/live/ws/tomcat/TomcatLiveWsEndpoint.java
  13. 15 0
      fs-live-ws/src/main/java/com/fs/live/ws/tomcat/TomcatWsConfigurator.java
  14. 1 1
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderItemScrmMapper.java
  15. 3 3
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreAfterSalesVO.java
  16. 3 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderItemExportRefundZMVO.java
  17. 3 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderItemExportVO.java
  18. 2 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderItemExportZMVO.java
  19. 10 0
      fs-service/src/main/java/com/fs/live/domain/Live.java
  20. 5 0
      fs-service/src/main/java/com/fs/live/mapper/LiveMapper.java
  21. 68 8
      fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
  22. 7 0
      fs-service/src/main/java/com/fs/qw/mapper/QwSopSmsLogsMapper.java
  23. 5 0
      fs-service/src/main/java/com/fs/qw/service/IQwSopSmsLogsService.java
  24. 5 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwSopSmsLogsServiceImpl.java
  25. 2 0
      fs-service/src/main/resources/db/changelog/db.changelog-master.xml
  26. 67 0
      fs-service/src/main/resources/db/changelog/dictData/menu.sql
  27. 17 1
      fs-service/src/main/resources/mapper/live/LiveMapper.xml
  28. 8 0
      fs-service/src/main/resources/mapper/qw/QwSopSmsLogsMapper.xml

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

@@ -125,6 +125,7 @@ public class FsStoreAfterSalesScrmController extends BaseController
                                 zmvo.setPrice(vo.getPrice());
                                 zmvo.setCost(vo.getCost());
                                 zmvo.setPayMoney(vo.getPayMoney());
+                                zmvo.setCouponPrice(vo.getCouponPrice());
                                 zmvo.setPayPostage(vo.getTotalPostage());
                                 zmvo.setCateName(vo.getCateName());
                                 zmvo.setRealName(vo.getUserName());

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

@@ -58,6 +58,18 @@ public class LiveKeysConstant {
     public static final int TTL_LIVE_MSG = 30;
     public static final int TTL_LIVE_DATA = 30;
 
+    /** cacheLive 列表/分页缓存(不含 liveId,直播状态变更需整批失效) */
+    public static final String CACHE_LIVE_LIST_PATTERN = "live:cache:list:*";
+    public static final String LIVE_CONFIG_BY_ID_PATTERN = "live:config:%s:*";
+    public static final String CACHE_LIVE_GOODS_LIST_PATTERN = "live:cache:goods:list:%s:*";
+    public static final String CACHE_LIVE_STORE_PATTERN = "live:cache:goods:store:%s:*";
+    public static final String CACHE_LIVE_VIDEO_PATTERN = "live:cache:video:%s:*";
+    public static final String CACHE_LIVE_COUPON_LIST_PATTERN = "live:cache:coupon:list:%s:*";
+    public static final String CACHE_LIVE_COUPON_CUR_PATTERN = "live:cache:coupon:cur:%s:*";
+    public static final String CACHE_LIVE_LOTTERY_DETAIL_PATTERN = "live:cache:lottery:detail:%s:*";
+    public static final String CACHE_LIVE_COMPLETION_PATTERN = "live:cache:completion:*:%s:*";
+    public static final String CACHE_LIVE_RED_PACKET_PATTERN = "live:cache:redPacket:status:%s:*";
+
     /** 商品足迹异步写入队列 */
     public static final String FOOTPRINT_QUEUE_KEY = "live:footprint:queue";
     /** 足迹任务处理锁前缀,后缀为 taskId */

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

@@ -258,6 +258,24 @@ public class RedisCache
         });
     }
 
+    /**
+     * 按 pattern 删除匹配的 key(SCAN,避免 KEYS 阻塞)
+     *
+     * @param pattern 匹配模式,如 live:cache:list:*
+     * @return 删除数量
+     */
+    public long deleteByPattern(final String pattern)
+    {
+        if (pattern == null || pattern.isEmpty()) {
+            return 0L;
+        }
+        Set<String> keys = scan(pattern);
+        if (keys == null || keys.isEmpty()) {
+            return 0L;
+        }
+        return deleteObject(keys);
+    }
+
 
     public Boolean setIfAbsent(String key, String value, long timeout, TimeUnit unit) {
         return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit);

+ 2 - 9
fs-live-app/src/main/java/com/fs/live/task/Task.java

@@ -154,6 +154,8 @@ public class Task {
             for (Live live : liveList) {
                 liveService.updateLiveEntity(live);
             }
+            // 状态变更后统一重建首页列表;cacheLive Aside 已在 updateLiveEntity -> evictLiveAsideCache 中失效
+            liveService.asyncToCache();
         }
         String key = "live:auto_task:";
         if (!startLiveList.isEmpty()) {
@@ -171,9 +173,6 @@ public class Task {
                         redisCache.expire(key+live.getLiveId(), 1, TimeUnit.DAYS);
                     });
                 }
-                // 清理小程序缓存 和 直播标签缓存
-                String cacheKey = String.format(LiveKeysConstant.LIVE_DATA_CACHE, live.getLiveId());
-                redisCache.deleteObject(cacheKey);
                 liveWatchUserService.clearLiveFlagCache(live.getLiveId());
                 // 将开启的直播间信息写入Redis缓存,用于打标签定时任务
                 try {
@@ -203,8 +202,6 @@ public class Task {
                     log.error("写入直播间打标签缓存失败: liveId={}, error={}", live.getLiveId(), e.getMessage(), e);
                 }
             }
-            // 重新更新所有在直播的缓存
-            liveService.asyncToCache();
         }
         if (!endLiveList.isEmpty()) {
             for (Live live : endLiveList) {
@@ -221,8 +218,6 @@ public class Task {
                         redisCache.redisTemplate.opsForZSet().remove(key + live.getLiveId(), JSON.toJSONString(liveAutoTask),liveAutoTask.getAbsValue().getTime());
                     });
                 }
-                String cacheKey = String.format(LiveKeysConstant.LIVE_DATA_CACHE, live.getLiveId());
-                redisCache.deleteObject(cacheKey);
                 webSocketServer.removeLikeCountCache(live.getLiveId());
 
                 // 删除打标签缓存
@@ -234,8 +229,6 @@ public class Task {
                     log.error("删除直播间打标签缓存失败: liveId={}, error={}", live.getLiveId(), e.getMessage(), e);
                 }
             }
-            // 重新更新所有在直播的缓存
-            liveService.asyncToCache();
         }
 
     }

+ 2 - 9
fs-live-ws/src/main/java/com/fs/live/task/Task.java

@@ -154,6 +154,8 @@ public class Task {
             for (Live live : liveList) {
                 liveService.updateLiveEntity(live);
             }
+            // 状态变更后统一重建首页列表;cacheLive Aside 已在 updateLiveEntity -> evictLiveAsideCache 中失效
+            liveService.asyncToCache();
         }
         String key = "live:auto_task:";
         if (!startLiveList.isEmpty()) {
@@ -171,9 +173,6 @@ public class Task {
                         redisCache.expire(key+live.getLiveId(), 1, TimeUnit.DAYS);
                     });
                 }
-                // 清理小程序缓存 和 直播标签缓存
-                String cacheKey = String.format(LiveKeysConstant.LIVE_DATA_CACHE, live.getLiveId());
-                redisCache.deleteObject(cacheKey);
                 liveWatchUserService.clearLiveFlagCache(live.getLiveId());
                 // 将开启的直播间信息写入Redis缓存,用于打标签定时任务
                 try {
@@ -203,8 +202,6 @@ public class Task {
                     log.error("写入直播间打标签缓存失败: liveId={}, error={}", live.getLiveId(), e.getMessage(), e);
                 }
             }
-            // 重新更新所有在直播的缓存
-            liveService.asyncToCache();
         }
         if (!endLiveList.isEmpty()) {
             for (Live live : endLiveList) {
@@ -221,8 +218,6 @@ public class Task {
                         redisCache.redisTemplate.opsForZSet().remove(key + live.getLiveId(), JSON.toJSONString(liveAutoTask),liveAutoTask.getAbsValue().getTime());
                     });
                 }
-                String cacheKey = String.format(LiveKeysConstant.LIVE_DATA_CACHE, live.getLiveId());
-                redisCache.deleteObject(cacheKey);
                 liveWsRoomBroadcastFacade.removeLikeCountCache(live.getLiveId());
 
                 // 删除打标签缓存
@@ -234,8 +229,6 @@ public class Task {
                     log.error("删除直播间打标签缓存失败: liveId={}, error={}", live.getLiveId(), e.getMessage(), e);
                 }
             }
-            // 重新更新所有在直播的缓存
-            liveService.asyncToCache();
         }
 
     }

+ 53 - 0
fs-live-ws/src/main/java/com/fs/live/ws/bean/WsConnectContext.java

@@ -0,0 +1,53 @@
+package com.fs.live.ws.bean;
+
+/**
+ * WS 进房上下文(销售归属等),Netty / Tomcat 共用。
+ */
+public class WsConnectContext {
+
+    private Long companyId = -1L;
+    private Long companyUserId = -1L;
+    private Long qwUserId = -1L;
+    private Long externalContactId = -1L;
+    private String location;
+
+    public Long getCompanyId() {
+        return companyId;
+    }
+
+    public void setCompanyId(Long companyId) {
+        this.companyId = companyId == null ? -1L : companyId;
+    }
+
+    public Long getCompanyUserId() {
+        return companyUserId;
+    }
+
+    public void setCompanyUserId(Long companyUserId) {
+        this.companyUserId = companyUserId == null ? -1L : companyUserId;
+    }
+
+    public Long getQwUserId() {
+        return qwUserId;
+    }
+
+    public void setQwUserId(Long qwUserId) {
+        this.qwUserId = qwUserId == null ? -1L : qwUserId;
+    }
+
+    public Long getExternalContactId() {
+        return externalContactId;
+    }
+
+    public void setExternalContactId(Long externalContactId) {
+        this.externalContactId = externalContactId == null ? -1L : externalContactId;
+    }
+
+    public String getLocation() {
+        return location;
+    }
+
+    public void setLocation(String location) {
+        this.location = location;
+    }
+}

+ 6 - 0
fs-live-ws/src/main/java/com/fs/live/ws/constant/WsAttrConstant.java

@@ -16,9 +16,15 @@ public final class WsAttrConstant {
     public static final String LOCATION = "location";
     public static final String COMPANY_ID = "companyId";
     public static final String COMPANY_USER_ID = "companyUserId";
+    public static final String QW_USER_ID = "qwUserId";
+    public static final String EXTERNAL_CONTACT_ID = "externalContactId";
 
     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<String> ATTR_LOCATION = AttributeKey.valueOf(LOCATION);
+    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);
+    public static final AttributeKey<Long> ATTR_QW_USER_ID = AttributeKey.valueOf(QW_USER_ID);
+    public static final AttributeKey<Long> ATTR_EXTERNAL_CONTACT_ID = AttributeKey.valueOf(EXTERNAL_CONTACT_ID);
 }

+ 16 - 0
fs-live-ws/src/main/java/com/fs/live/ws/handler/WsAuthHandler.java

@@ -63,12 +63,28 @@ public class WsAuthHandler extends ChannelInboundHandlerAdapter {
         if (params.containsKey(WsAttrConstant.LOCATION)) {
             ctx.channel().attr(WsAttrConstant.ATTR_LOCATION).set(params.get(WsAttrConstant.LOCATION).get(0));
         }
+        putLongAttr(ctx, params, WsAttrConstant.COMPANY_ID, WsAttrConstant.ATTR_COMPANY_ID);
+        putLongAttr(ctx, params, WsAttrConstant.COMPANY_USER_ID, WsAttrConstant.ATTR_COMPANY_USER_ID);
+        putLongAttr(ctx, params, WsAttrConstant.QW_USER_ID, WsAttrConstant.ATTR_QW_USER_ID);
+        putLongAttr(ctx, params, WsAttrConstant.EXTERNAL_CONTACT_ID, WsAttrConstant.ATTR_EXTERNAL_CONTACT_ID);
 
         ctx.pipeline().remove(this);
         ctx.fireChannelRead(req.retain());
         req.release();
     }
 
+    private void putLongAttr(ChannelHandlerContext ctx, Map<String, List<String>> params,
+                             String paramName, io.netty.util.AttributeKey<Long> attrKey) {
+        if (!params.containsKey(paramName)) {
+            return;
+        }
+        try {
+            ctx.channel().attr(attrKey).set(Long.parseLong(params.get(paramName).get(0)));
+        } catch (NumberFormatException ex) {
+            log.warn("WS 参数 {} 解析失败", paramName);
+        }
+    }
+
     /** 握手完成前必须返回 HTTP 响应,否则客户端会一直 pending */
     private void reject(ChannelHandlerContext ctx, FullHttpRequest req, HttpResponseStatus status, String reason) {
         byte[] body = reason.getBytes(CharsetUtil.UTF_8);

+ 13 - 2
fs-live-ws/src/main/java/com/fs/live/ws/handler/WsLiveChatHandler.java

@@ -1,5 +1,6 @@
 package com.fs.live.ws.handler;
 
+import com.fs.live.ws.bean.WsConnectContext;
 import com.fs.live.ws.constant.WsAttrConstant;
 import com.fs.live.ws.service.ILiveWsConnectionService;
 import com.fs.live.ws.service.ILiveWsMessageService;
@@ -39,8 +40,8 @@ public class WsLiveChatHandler extends SimpleChannelInboundHandler<TextWebSocket
             Long liveId = channel.attr(WsAttrConstant.ATTR_LIVE_ID).get();
             Long userId = channel.attr(WsAttrConstant.ATTR_USER_ID).get();
             Long userType = channel.attr(WsAttrConstant.ATTR_USER_TYPE).get();
-            String location = channel.attr(WsAttrConstant.ATTR_LOCATION).get();
             WsSender sender = new ChannelWsSender(channel);
+            WsConnectContext context = buildContext(channel);
 
             if (!connectionService.tryAcceptConnection()) {
                 sender.sendText("Error: 4503 节点连接已满");
@@ -49,7 +50,7 @@ public class WsLiveChatHandler extends SimpleChannelInboundHandler<TextWebSocket
             }
             connectionExecutor.execute(() -> {
                 try {
-                    connectionService.onConnected(liveId, userId, userType, sender, location);
+                    connectionService.onConnected(liveId, userId, userType, sender, context);
                 } catch (Exception ex) {
                     log.warn("WS 连接建立失败 liveId={}, userId={}", liveId, userId, ex);
                     sender.sendText("Error: " + ex.getMessage());
@@ -61,6 +62,16 @@ public class WsLiveChatHandler extends SimpleChannelInboundHandler<TextWebSocket
         super.userEventTriggered(ctx, evt);
     }
 
+    private WsConnectContext buildContext(Channel channel) {
+        WsConnectContext context = new WsConnectContext();
+        context.setLocation(channel.attr(WsAttrConstant.ATTR_LOCATION).get());
+        context.setCompanyId(channel.attr(WsAttrConstant.ATTR_COMPANY_ID).get());
+        context.setCompanyUserId(channel.attr(WsAttrConstant.ATTR_COMPANY_USER_ID).get());
+        context.setQwUserId(channel.attr(WsAttrConstant.ATTR_QW_USER_ID).get());
+        context.setExternalContactId(channel.attr(WsAttrConstant.ATTR_EXTERNAL_CONTACT_ID).get());
+        return context;
+    }
+
     @Override
     protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) {
         Channel channel = ctx.channel();

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

@@ -1,5 +1,6 @@
 package com.fs.live.ws.service;
 
+import com.fs.live.ws.bean.WsConnectContext;
 import com.fs.live.ws.session.WsSender;
 
 public interface ILiveWsConnectionService {
@@ -7,7 +8,7 @@ public interface ILiveWsConnectionService {
     /** @return true 允许连接;false 节点已满 */
     boolean tryAcceptConnection();
 
-    void onConnected(Long liveId, Long userId, Long userType, WsSender sender, String location);
+    void onConnected(Long liveId, Long userId, Long userType, WsSender sender, WsConnectContext context);
 
     void onDisconnected(Long liveId, Long userId, Long userType, WsSender sender);
 }

+ 101 - 2
fs-live-ws/src/main/java/com/fs/live/ws/service/impl/LiveWsConnectionServiceImpl.java

@@ -7,9 +7,12 @@ import com.fs.common.core.redis.RedisCache;
 import com.fs.hisStore.domain.FsUserScrm;
 import com.fs.hisStore.service.IFsUserScrmService;
 import com.fs.live.domain.Live;
+import com.fs.live.domain.LiveUserFirstEntry;
 import com.fs.live.domain.LiveWatchUser;
 import com.fs.live.service.ILiveService;
+import com.fs.live.service.ILiveUserFirstEntryService;
 import com.fs.live.service.ILiveWatchUserService;
+import com.fs.live.ws.bean.WsConnectContext;
 import com.fs.live.ws.bean.WsSendMsgVo;
 import com.fs.live.ws.config.LiveWsProperties;
 import com.fs.live.ws.constant.WsRedisKeys;
@@ -17,17 +20,20 @@ import com.fs.live.ws.service.ILiveWsBroadcastService;
 import com.fs.live.ws.service.ILiveWsConnectionService;
 import com.fs.live.ws.session.LiveWsRoomManager;
 import com.fs.live.ws.session.WsSender;
+import com.fs.newAdv.service.ILeadService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.util.Date;
 import java.util.concurrent.TimeUnit;
 
 @Service
 public class LiveWsConnectionServiceImpl implements ILiveWsConnectionService {
 
     private static final Logger log = LoggerFactory.getLogger(LiveWsConnectionServiceImpl.class);
+    private static final String FIRST_ENTRY_CACHE_KEY = "live:userFirstEntry:%s:%s";
 
     @Autowired
     private LiveWsProperties properties;
@@ -43,6 +49,10 @@ public class LiveWsConnectionServiceImpl implements ILiveWsConnectionService {
     private IFsUserScrmService fsUserService;
     @Autowired
     private RedisCache redisCache;
+    @Autowired
+    private ILiveUserFirstEntryService liveUserFirstEntryService;
+    @Autowired(required = false)
+    private ILeadService leadService;
 
     @Override
     public boolean tryAcceptConnection() {
@@ -50,7 +60,10 @@ public class LiveWsConnectionServiceImpl implements ILiveWsConnectionService {
     }
 
     @Override
-    public void onConnected(Long liveId, Long userId, Long userType, WsSender sender, String location) {
+    public void onConnected(Long liveId, Long userId, Long userType, WsSender sender, WsConnectContext context) {
+        if (context == null) {
+            context = new WsConnectContext();
+        }
         roomManager.incrementAndGetConnections();
         Live live = liveService.selectLiveByLiveId(liveId);
         if (live == null) {
@@ -64,7 +77,7 @@ public class LiveWsConnectionServiceImpl implements ILiveWsConnectionService {
                 roomManager.decrementAndGetConnections();
                 throw new IllegalStateException("用户信息错误");
             }
-            LiveWatchUser liveWatchUser = liveWatchUserService.join(fsUser, liveId, userId, location);
+            LiveWatchUser liveWatchUser = liveWatchUserService.join(fsUser, liveId, userId, context.getLocation());
             roomManager.bindUser(liveId, userId, sender);
 
             String entryTimeKey = String.format(WsRedisKeys.USER_ENTRY_TIME_KEY, liveId, userId);
@@ -74,6 +87,8 @@ public class LiveWsConnectionServiceImpl implements ILiveWsConnectionService {
             redisCache.incr(LiveKeysConstant.PAGE_VIEWS_KEY + liveId, 1);
             redisCache.incr(LiveKeysConstant.TOTAL_VIEWS_KEY + liveId, 1);
 
+            ensureLiveUserFirstEntry(liveId, userId, context);
+
             WsSendMsgVo sendMsgVo = buildEntryMsg(liveId, userId, userType, liveWatchUser, fsUser);
             broadcastService.broadcastToRoom(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
         } else {
@@ -81,6 +96,90 @@ public class LiveWsConnectionServiceImpl implements ILiveWsConnectionService {
         }
     }
 
+    /**
+     * 首次进房写入 live_user_first_entry;已存在则按销售扫码补全归属。
+     */
+    private void ensureLiveUserFirstEntry(Long liveId, Long userId, WsConnectContext context) {
+        long companyId = context.getCompanyId() == null ? -1L : context.getCompanyId();
+        long companyUserId = context.getCompanyUserId() == null ? -1L : context.getCompanyUserId();
+        long qwUserId = context.getQwUserId() == null ? -1L : context.getQwUserId();
+        long externalContactId = context.getExternalContactId() == null ? -1L : context.getExternalContactId();
+
+        String cacheKey = String.format(FIRST_ENTRY_CACHE_KEY, liveId, userId);
+        LiveUserFirstEntry liveUserFirstEntry = redisCache.getCacheObject(cacheKey);
+        if (liveUserFirstEntry == null) {
+            liveUserFirstEntry = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveId, userId);
+            if (liveUserFirstEntry != null) {
+                redisCache.setCacheObject(cacheKey, liveUserFirstEntry, 4, TimeUnit.HOURS);
+            }
+        }
+
+        try {
+            if (liveUserFirstEntry != null) {
+                // 首次无销售进入,后续扫码带销售再补全
+                Long existCompanyUserId = liveUserFirstEntry.getCompanyUserId();
+                if (existCompanyUserId != null && existCompanyUserId == -1L && companyUserId != -1L) {
+                    liveUserFirstEntry.setCompanyId(companyId);
+                    liveUserFirstEntry.setCompanyUserId(companyUserId);
+                    if (qwUserId != -1L) {
+                        liveUserFirstEntry.setQwUserId(qwUserId);
+                    }
+                    if (externalContactId != -1L) {
+                        liveUserFirstEntry.setExternalContactId(externalContactId);
+                    }
+                    liveUserFirstEntry.setUpdateTime(new Date());
+                    liveUserFirstEntryService.updateLiveUserFirstEntry(liveUserFirstEntry);
+                    redisCache.setCacheObject(cacheKey, liveUserFirstEntry, 4, TimeUnit.HOURS);
+                    redisCache.setCacheObject("live:user:first:entry:" + liveId + ":" + userId, liveUserFirstEntry, 4, TimeUnit.HOURS);
+                }
+            } else {
+                // 用户A邀请用户B:B 的业绩算 A 的销售
+                if (companyId == -2L) {
+                    String clientBCacheKey = String.format(FIRST_ENTRY_CACHE_KEY, liveId, companyUserId);
+                    LiveUserFirstEntry clientB = redisCache.getCacheObject(clientBCacheKey);
+                    if (clientB == null) {
+                        clientB = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveId, companyUserId);
+                        if (clientB != null) {
+                            redisCache.setCacheObject(clientBCacheKey, clientB, 4, TimeUnit.HOURS);
+                        }
+                    }
+                    if (clientB != null) {
+                        companyId = clientB.getCompanyId() == null ? -1L : clientB.getCompanyId();
+                        companyUserId = clientB.getCompanyUserId() == null ? -1L : clientB.getCompanyUserId();
+                    }
+                }
+                Date date = new Date();
+                liveUserFirstEntry = new LiveUserFirstEntry();
+                liveUserFirstEntry.setUserId(userId);
+                liveUserFirstEntry.setLiveId(liveId);
+                liveUserFirstEntry.setCompanyId(companyId);
+                liveUserFirstEntry.setCompanyUserId(companyUserId);
+                liveUserFirstEntry.setEntryDate(date);
+                liveUserFirstEntry.setFirstEntryTime(date);
+                liveUserFirstEntry.setUpdateTime(date);
+                if (qwUserId != -1L) {
+                    liveUserFirstEntry.setQwUserId(qwUserId);
+                }
+                if (externalContactId != -1L) {
+                    liveUserFirstEntry.setExternalContactId(externalContactId);
+                }
+                liveUserFirstEntryService.insertLiveUserFirstEntry(liveUserFirstEntry);
+                redisCache.setCacheObject(cacheKey, liveUserFirstEntry, 4, TimeUnit.HOURS);
+                redisCache.setCacheObject("live:user:first:entry:" + liveId + ":" + userId, liveUserFirstEntry, 4, TimeUnit.HOURS);
+
+                if (leadService != null) {
+                    try {
+                        leadService.enterLive(userId, liveId);
+                    } catch (Exception e) {
+                        log.warn("首次进房广告线索发送失败 liveId={}, userId={}", liveId, userId, e);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("写入 live_user_first_entry 失败 liveId={}, userId={}", liveId, userId, e);
+        }
+    }
+
     @Override
     public void onDisconnected(Long liveId, Long userId, Long userType, WsSender sender) {
         try {

+ 13 - 2
fs-live-ws/src/main/java/com/fs/live/ws/tomcat/TomcatLiveWsEndpoint.java

@@ -1,5 +1,6 @@
 package com.fs.live.ws.tomcat;
 
+import com.fs.live.ws.bean.WsConnectContext;
 import com.fs.live.ws.constant.WsAttrConstant;
 import com.fs.live.ws.service.ILiveWsConnectionService;
 import com.fs.live.ws.service.ILiveWsMessageService;
@@ -56,7 +57,6 @@ public class TomcatLiveWsEndpoint {
         Long liveId = (Long) props.get(WsAttrConstant.LIVE_ID);
         Long userId = (Long) props.get(WsAttrConstant.USER_ID);
         Long userType = (Long) props.get(WsAttrConstant.USER_TYPE);
-        String location = (String) props.get(WsAttrConstant.LOCATION);
         if (userType == null) {
             userType = 0L;
         }
@@ -70,10 +70,11 @@ public class TomcatLiveWsEndpoint {
             return;
         }
 
+        WsConnectContext context = buildContext(props);
         Long finalUserType = userType;
         Runnable joinTask = () -> {
             try {
-                connectionService.onConnected(liveId, userId, finalUserType, sender, location);
+                connectionService.onConnected(liveId, userId, finalUserType, sender, context);
             } catch (Exception ex) {
                 log.warn("Tomcat WS 连接建立失败 liveId={}, userId={}", liveId, userId, ex);
                 sender.sendText("Error: " + ex.getMessage());
@@ -87,6 +88,16 @@ public class TomcatLiveWsEndpoint {
         }
     }
 
+    private WsConnectContext buildContext(Map<String, Object> props) {
+        WsConnectContext context = new WsConnectContext();
+        context.setLocation((String) props.get(WsAttrConstant.LOCATION));
+        context.setCompanyId((Long) props.get(WsAttrConstant.COMPANY_ID));
+        context.setCompanyUserId((Long) props.get(WsAttrConstant.COMPANY_USER_ID));
+        context.setQwUserId((Long) props.get(WsAttrConstant.QW_USER_ID));
+        context.setExternalContactId((Long) props.get(WsAttrConstant.EXTERNAL_CONTACT_ID));
+        return context;
+    }
+
     @OnMessage
     public void onMessage(Session session, String payload) {
         Map<String, Object> props = session.getUserProperties();

+ 15 - 0
fs-live-ws/src/main/java/com/fs/live/ws/tomcat/TomcatWsConfigurator.java

@@ -40,5 +40,20 @@ public class TomcatWsConfigurator extends ServerEndpointConfig.Configurator {
         if (params.containsKey(WsAttrConstant.LOCATION)) {
             props.put(WsAttrConstant.LOCATION, params.get(WsAttrConstant.LOCATION).get(0));
         }
+        putLongProp(props, params, WsAttrConstant.COMPANY_ID);
+        putLongProp(props, params, WsAttrConstant.COMPANY_USER_ID);
+        putLongProp(props, params, WsAttrConstant.QW_USER_ID);
+        putLongProp(props, params, WsAttrConstant.EXTERNAL_CONTACT_ID);
+    }
+
+    private void putLongProp(Map<String, Object> props, Map<String, List<String>> params, String name) {
+        if (!params.containsKey(name)) {
+            return;
+        }
+        try {
+            props.put(name, Long.parseLong(params.get(name).get(0)));
+        } catch (NumberFormatException ignored) {
+            // ignore invalid query value
+        }
     }
 }

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

@@ -77,7 +77,7 @@ public interface FsStoreOrderItemScrmMapper
 
     @Select({"<script> " +
             "select i.*,o.user_id,u.nick_name,psps.cost,o.pay_postage,o.total_num,o.status,fspcs.cate_name, o.real_name,o.user_phone,o.user_address,o.create_time,o.pay_time,o.delivery_sn,o.delivery_name,o.delivery_id, c.company_name ,cu.nick_name as company_user_nick_name ,cu.phonenumber as company_usere_phonenumber,o.upload_time ,CASE WHEN o.certificates IS NULL OR o.certificates = '' THEN 0 ELSE 1 END AS is_upload,p.title as package_name,cts.name as scheduleName,sp_latest.pay_money, sp_latest.bank_transaction_id as bankTransactionId, o.delivery_send_time," +
-            " o.order_code, o.pay_price, o.pay_money, o.deduction_price,o.pay_delivery, o.order_type, o.refund_price as refundPrice, psps.price " +
+            " o.order_code, o.pay_price, o.pay_money, o.deduction_price,o.pay_delivery, o.coupon_price as couponPrice, o.order_type, o.refund_price as refundPrice, psps.price " +
             ", CASE o.is_audit WHEN 1 THEN '是' ELSE '否' END AS isAudit " +
             ", sas.audit_remark as auditRemark, sas.reason_level1_text as reasonValue1, sas.reason_level2_text as reasonValue2 " +
             ", o.delivery_type" +

+ 3 - 3
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreAfterSalesVO.java

@@ -121,6 +121,9 @@ public class FsStoreAfterSalesVO implements Serializable
     private BigDecimal totalPostage;
     @Excel(name="实付金额")
     private BigDecimal payPrice;
+    /** 优惠券金额(来自订单 coupon_price) */
+    @Excel(name = "优惠券金额", cellType = Excel.ColumnType.NUMERIC)
+    private BigDecimal couponPrice;
     //    @Excel(name="收货人姓名")
 //    private String realName;
     @Excel(name ="详细地址")
@@ -138,9 +141,6 @@ public class FsStoreAfterSalesVO implements Serializable
 
     private BigDecimal payMoney;
 
-    /** 直播优惠券金额(来自订单 coupon_price) */
-    private BigDecimal couponPrice;
-
     private String cateName;
     private Date orderCreateTime;
 

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderItemExportRefundZMVO.java

@@ -58,6 +58,9 @@ public class FsStoreOrderItemExportRefundZMVO implements Serializable  {
     @Excel(name = "实付金额",sort =91)
     private BigDecimal payMoney;
 
+    @Excel(name = "优惠券金额", cellType = Excel.ColumnType.NUMERIC, sort = 92)
+    private BigDecimal couponPrice;
+
     @Excel(name = "额外运费",sort =100)
     private BigDecimal payPostage;
     private Integer totalNum;

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderItemExportVO.java

@@ -54,6 +54,9 @@ public class FsStoreOrderItemExportVO implements Serializable
 
     private BigDecimal payMoney;
 
+    @Excel(name = "优惠券金额", cellType = Excel.ColumnType.NUMERIC)
+    private BigDecimal couponPrice;
+
     @Excel(name = "额外运费")
     private BigDecimal payPostage;
     private Integer totalNum;

+ 2 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderItemExportZMVO.java

@@ -56,6 +56,8 @@ public class FsStoreOrderItemExportZMVO implements Serializable {
     @Excel(name = "实付金额")
     private BigDecimal payMoney;
 
+    @Excel(name = "优惠券金额", cellType = Excel.ColumnType.NUMERIC)
+    private BigDecimal couponPrice;
 
     @Excel(name = "结算价")
     private BigDecimal FPrice;

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

@@ -136,6 +136,16 @@ public class   Live extends BaseEntity {
     private Long videoDuration;
     private Integer globalVisible;
 
+    /**
+     * 生活号马甲号ID
+     */
+    private Long vestId;
+
+    /**
+     * 可见范围 0私域(默认) 1公域(生活号置顶)
+     */
+    private Integer visibleScope;
+
     @TableField(exist = false)
     private List<LiveTagItemVO> liveTagList;
 

+ 5 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveMapper.java

@@ -271,4 +271,9 @@ public interface LiveMapper
 
     @Select("SELECT COUNT(1) FROM live WHERE is_del = 0 AND training_period_id = #{periodId}")
     int countLiveByTrainingPeriodId(@Param("periodId") Long periodId);
+
+    /**
+     * 生活号:进行中的公域直播,按开播时间升序
+     */
+    List<Live> selectPublicLivingList();
 }

+ 68 - 8
fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java

@@ -736,6 +736,12 @@ public class LiveServiceImpl implements ILiveService
         if(live.getLiveType() == 2 && StringUtils.isEmpty(live.getVideoUrl())) {
             throw new BaseException("录播必须上传视屏");
         }
+        if (live.getVisibleScope() == null) {
+            live.setVisibleScope(0);
+        }
+        if (Integer.valueOf(1).equals(live.getVisibleScope()) && live.getVestId() == null) {
+            throw new BaseException("公域直播必须绑定马甲号");
+        }
         live.setCreateTime(DateUtils.getNowDate());
         live.setIsDel(0);
         live.setIsAudit(0);
@@ -777,8 +783,8 @@ public class LiveServiceImpl implements ILiveService
     public int updateLiveEntity(Live live) {
         log.error("updateLiveEntity:"+ live.getLiveId());
         int result = baseMapper.updateLive( live);
-        // 清除缓存
-        clearLiveCache(live.getLiveId());
+        // 仅失效 Aside 缓存;首页列表由调用方(如开播定时任务)统一 asyncToCache,避免批量更新竞态
+        evictLiveAsideCache(live.getLiveId());
         return result;
     }
 
@@ -896,6 +902,11 @@ public class LiveServiceImpl implements ILiveService
             log.error("updateLive:"+ live.getLiveId());
             return -1;
         }
+        Integer scope = live.getVisibleScope() != null ? live.getVisibleScope() : exist.getVisibleScope();
+        Long vestId = live.getVestId() != null ? live.getVestId() : exist.getVestId();
+        if (Integer.valueOf(1).equals(scope) && vestId == null) {
+            throw new BaseException("公域直播必须绑定马甲号");
+        }
         live.setUpdateTime(DateUtils.getNowDate());
         List<LiveVideo> videos = liveVideoService.listByLiveId(live.getLiveId(), 1);
         if(!videos.isEmpty()){
@@ -1685,6 +1696,7 @@ public class LiveServiceImpl implements ILiveService
 
     /**
      * 清除直播间数据缓存(公开方法)
+     * 覆盖:小程序 live:data、详情/配置、以及 user-app cacheLive(live:cache:*)Aside 缓存
      * @param liveId 直播间ID
      * @return 结果
      */
@@ -1694,10 +1706,10 @@ public class LiveServiceImpl implements ILiveService
             return R.error("直播间ID不能为空");
         }
         try {
-            String cacheKey = String.format(LiveKeysConstant.LIVE_DATA_CACHE, liveId);
-            redisCache.deleteObject(cacheKey);
-            redisCache.hashDelete(LiveKeysConstant.LIVE_HOME_PAGE_DETAIL, String.valueOf(liveId));
-            log.debug("清除直播间缓存: liveId={}", liveId);
+            evictLiveAsideCache(liveId);
+            // 重建首页直播列表 ZSet,避免状态/上下架后仍读旧数据
+            asyncToCache();
+            log.info("清除直播间缓存成功: liveId={}", liveId);
             return R.ok("缓存清理成功");
         } catch (Exception e) {
             log.error("清除直播间缓存失败: liveId={}", liveId, e);
@@ -1705,6 +1717,52 @@ public class LiveServiceImpl implements ILiveService
         }
     }
 
+    /**
+     * 失效指定直播间及相关 cacheLive / 详情配置缓存(不重建首页列表)
+     */
+    private void evictLiveAsideCache(Long liveId) {
+        if (liveId == null) {
+            return;
+        }
+        String liveIdStr = String.valueOf(liveId);
+
+        // 精确 key
+        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_FLAG_CACHE, liveId));
+        redisCache.deleteObject(String.format(LiveKeysConstant.CACHE_LIVE_INFO, liveId));
+        redisCache.deleteObject(String.format(LiveKeysConstant.CACHE_LIVE_MSG_LIST, liveId));
+        redisCache.deleteObject(String.format(LiveKeysConstant.CACHE_LIVE_SHOW_GOODS, liveId));
+        redisCache.deleteObject(String.format(LiveKeysConstant.CACHE_LIVE_VIEW_DATA, liveId));
+        redisCache.deleteObject(String.format(LiveKeysConstant.CACHE_LIVE_RECENT_VIEWERS, liveId));
+        redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_COUPON_NUM, liveId));
+        redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_CURRENT_COUPON, liveId));
+        redisCache.deleteObject("live:cache:watch:user:" + liveIdStr);
+        redisCache.deleteObject("live:cache:data:list:" + liveIdStr);
+        redisCache.deleteObject("live:cache:data:get:" + liveIdStr);
+        redisCache.deleteObject("live:cache:order:user:" + liveIdStr);
+
+        // 兼容旧 Hash 详情(facade 使用未 format 的 key)
+        redisCache.hashDelete(LiveKeysConstant.LIVE_HOME_PAGE_DETAIL, liveIdStr);
+
+        // 模式匹配:配置与 cacheLive 分域
+        redisCache.deleteByPattern(String.format(LiveKeysConstant.LIVE_CONFIG_BY_ID_PATTERN, liveId));
+        redisCache.deleteByPattern(String.format(LiveKeysConstant.CACHE_LIVE_GOODS_LIST_PATTERN, liveId));
+        redisCache.deleteByPattern(String.format(LiveKeysConstant.CACHE_LIVE_STORE_PATTERN, liveId));
+        redisCache.deleteByPattern(String.format(LiveKeysConstant.CACHE_LIVE_VIDEO_PATTERN, liveId));
+        redisCache.deleteByPattern(String.format(LiveKeysConstant.CACHE_LIVE_COUPON_LIST_PATTERN, liveId));
+        redisCache.deleteByPattern(String.format(LiveKeysConstant.CACHE_LIVE_COUPON_CUR_PATTERN, liveId));
+        redisCache.deleteByPattern(String.format(LiveKeysConstant.CACHE_LIVE_LOTTERY_DETAIL_PATTERN, liveId));
+        redisCache.deleteByPattern(String.format(LiveKeysConstant.CACHE_LIVE_COMPLETION_PATTERN, liveId));
+        redisCache.deleteByPattern(String.format(LiveKeysConstant.CACHE_LIVE_RED_PACKET_PATTERN, liveId));
+        redisCache.deleteByPattern(String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_RED, liveId, "*"));
+        redisCache.deleteByPattern(String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_COUPON, liveId, "*"));
+        redisCache.deleteByPattern(String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_DRAW, liveId, "*"));
+
+        // 列表类缓存不含 liveId,任意直播变更都需整批失效
+        redisCache.deleteByPattern(LiveKeysConstant.CACHE_LIVE_LIST_PATTERN);
+    }
+
     /**
      * 批量清除直播间数据缓存
      * @param liveIds 直播间ID数组
@@ -1712,8 +1770,9 @@ public class LiveServiceImpl implements ILiveService
     private void clearLiveCache(Long[] liveIds) {
         if (liveIds != null && liveIds.length > 0) {
             for (Long liveId : liveIds) {
-                clearLiveCache(liveId);
+                evictLiveAsideCache(liveId);
             }
+            asyncToCache();
         }
     }
 
@@ -1724,8 +1783,9 @@ public class LiveServiceImpl implements ILiveService
     private void clearLiveCache(List<Long> liveIds) {
         if (liveIds != null && !liveIds.isEmpty()) {
             for (Long liveId : liveIds) {
-                clearLiveCache(liveId);
+                evictLiveAsideCache(liveId);
             }
+            asyncToCache();
         }
     }
 

+ 7 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwSopSmsLogsMapper.java

@@ -136,4 +136,11 @@ public interface QwSopSmsLogsMapper extends BaseMapper<QwSopSmsLogs>{
      * **/
     @DataSource(DataSourceType.SOP)
     List<QwSopSmsLogs> getQwSopSmsLogsStateList(@Param("serverIds") List<Long> serverIds);
+
+    /**
+     * 查询指定模板、发送时间之后的外部联系人ID(去重)
+     */
+    @DataSource(DataSourceType.SOP)
+    List<Long> selectContactIdsByTemplateAndSendTimeAfter(@Param("smsTemplateCode") String smsTemplateCode,
+                                                          @Param("sendTimeAfter") Date sendTimeAfter);
 }

+ 5 - 0
fs-service/src/main/java/com/fs/qw/service/IQwSopSmsLogsService.java

@@ -112,4 +112,9 @@ public interface IQwSopSmsLogsService extends IService<QwSopSmsLogs>{
      * @return lsit 服务数据
      * **/
     List<QwSopSmsLogs> getQwSopSmsLogsStateList(List<Long> serverIds);
+
+    /**
+     * 查询指定模板、发送时间之后的外部联系人ID(去重)
+     */
+    List<Long> selectContactIdsByTemplateAndSendTimeAfter(String smsTemplateCode, Date sendTimeAfter);
 }

+ 5 - 0
fs-service/src/main/java/com/fs/qw/service/impl/QwSopSmsLogsServiceImpl.java

@@ -145,4 +145,9 @@ public class QwSopSmsLogsServiceImpl extends ServiceImpl<QwSopSmsLogsMapper, QwS
     public List<QwSopSmsLogs> getQwSopSmsLogsStateList(List<Long> serverIds) {
         return baseMapper.getQwSopSmsLogsStateList(serverIds);
     }
+
+    @Override
+    public List<Long> selectContactIdsByTemplateAndSendTimeAfter(String smsTemplateCode, Date sendTimeAfter) {
+        return baseMapper.selectContactIdsByTemplateAndSendTimeAfter(smsTemplateCode, sendTimeAfter);
+    }
 }

+ 2 - 0
fs-service/src/main/resources/db/changelog/db.changelog-master.xml

@@ -11,7 +11,9 @@
 
     <include file="baseline/baseline.sql" relativeToChangelogFile="true"/>
     <include file="changes/20260613-live-user-add-is-del.sql" relativeToChangelogFile="true"/>
+    <include file="changes/20260722-life-account-video.sql" relativeToChangelogFile="true"/>
     <include file="table/live_group_type.sql" relativeToChangelogFile="true"/>
     <include file="dictData/dict_data_update.sql" relativeToChangelogFile="true"/>
+    <include file="dictData/menu.sql" relativeToChangelogFile="true"/>
 
 </databaseChangeLog>

+ 67 - 0
fs-service/src/main/resources/db/changelog/dictData/menu.sql

@@ -0,0 +1,67 @@
+--liquibase formatted sql
+
+--changeset auto:20260722-sys-menu-life
+--comment 总后台生活号菜单(挂在直播管理 2213 下)
+--preconditions onFail:MARK_RAN
+--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM sys_menu WHERE perms = 'life:vest:list'
+
+INSERT INTO `sys_menu`
+(`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
+VALUES
+('生活号', 2213, 20, 'life', NULL, NULL, 1, 0, 'M', '0', '0', '', 'peoples', 'admin', NOW(), '', NULL, '生活号目录');
+
+SET @life_dir_id = LAST_INSERT_ID();
+
+INSERT INTO `sys_menu`
+(`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
+VALUES
+('马甲号管理', @life_dir_id, 1, 'vest', 'life/vest/index', NULL, 1, 0, 'C', '0', '0', 'life:vest:list', 'user', 'admin', NOW(), '', NULL, '生活号马甲号'),
+('视频内容管理', @life_dir_id, 2, 'video', 'life/video/index', NULL, 1, 0, 'C', '0', '0', 'life:video:list', 'video', 'admin', NOW(), '', NULL, '生活号短视频');
+
+SET @vest_menu_id = (SELECT menu_id FROM sys_menu WHERE perms = 'life:vest:list' ORDER BY menu_id DESC LIMIT 1);
+SET @video_menu_id = (SELECT menu_id FROM sys_menu WHERE perms = 'life:video:list' ORDER BY menu_id DESC LIMIT 1);
+
+INSERT INTO `sys_menu`
+(`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
+VALUES
+('马甲号查询', @vest_menu_id, 1, '', NULL, NULL, 1, 0, 'F', '0', '0', 'life:vest:query', '#', 'admin', NOW(), '', NULL, ''),
+('马甲号新增', @vest_menu_id, 2, '', NULL, NULL, 1, 0, 'F', '0', '0', 'life:vest:add', '#', 'admin', NOW(), '', NULL, ''),
+('马甲号修改', @vest_menu_id, 3, '', NULL, NULL, 1, 0, 'F', '0', '0', 'life:vest:edit', '#', 'admin', NOW(), '', NULL, ''),
+('马甲号删除', @vest_menu_id, 4, '', NULL, NULL, 1, 0, 'F', '0', '0', 'life:vest:remove', '#', 'admin', NOW(), '', NULL, ''),
+('视频查询', @video_menu_id, 1, '', NULL, NULL, 1, 0, 'F', '0', '0', 'life:video:query', '#', 'admin', NOW(), '', NULL, ''),
+('视频新增', @video_menu_id, 2, '', NULL, NULL, 1, 0, 'F', '0', '0', 'life:video:add', '#', 'admin', NOW(), '', NULL, ''),
+('视频修改', @video_menu_id, 3, '', NULL, NULL, 1, 0, 'F', '0', '0', 'life:video:edit', '#', 'admin', NOW(), '', NULL, ''),
+('视频删除', @video_menu_id, 4, '', NULL, NULL, 1, 0, 'F', '0', '0', 'life:video:remove', '#', 'admin', NOW(), '', NULL, '');
+
+--changeset auto:20260722-company-menu-life
+--comment 公司端生活号菜单(挂在直播管理 2213 下)
+--preconditions onFail:MARK_RAN
+--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM company_menu WHERE perms = 'life:vest:list'
+
+INSERT INTO `company_menu`
+(`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `company_id`)
+VALUES
+('生活号', 2213, 20, 'life', NULL, 1, 0, 'M', '0', '0', '', 'peoples', 'admin', NOW(), '', NULL, '生活号目录', NULL);
+
+SET @c_life_dir_id = LAST_INSERT_ID();
+
+INSERT INTO `company_menu`
+(`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `company_id`)
+VALUES
+('马甲号管理', @c_life_dir_id, 1, 'vest', 'life/vest/index', 1, 0, 'C', '0', '0', 'life:vest:list', 'user', 'admin', NOW(), '', NULL, '生活号马甲号', NULL),
+('视频内容管理', @c_life_dir_id, 2, 'video', 'life/video/index', 1, 0, 'C', '0', '0', 'life:video:list', 'video', 'admin', NOW(), '', NULL, '生活号短视频', NULL);
+
+SET @c_vest_menu_id = (SELECT menu_id FROM company_menu WHERE perms = 'life:vest:list' ORDER BY menu_id DESC LIMIT 1);
+SET @c_video_menu_id = (SELECT menu_id FROM company_menu WHERE perms = 'life:video:list' ORDER BY menu_id DESC LIMIT 1);
+
+INSERT INTO `company_menu`
+(`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `company_id`)
+VALUES
+('马甲号查询', @c_vest_menu_id, 1, '', NULL, 1, 0, 'F', '0', '0', 'life:vest:query', '#', 'admin', NOW(), '', NULL, '', NULL),
+('马甲号新增', @c_vest_menu_id, 2, '', NULL, 1, 0, 'F', '0', '0', 'life:vest:add', '#', 'admin', NOW(), '', NULL, '', NULL),
+('马甲号修改', @c_vest_menu_id, 3, '', NULL, 1, 0, 'F', '0', '0', 'life:vest:edit', '#', 'admin', NOW(), '', NULL, '', NULL),
+('马甲号删除', @c_vest_menu_id, 4, '', NULL, 1, 0, 'F', '0', '0', 'life:vest:remove', '#', 'admin', NOW(), '', NULL, '', NULL),
+('视频查询', @c_video_menu_id, 1, '', NULL, 1, 0, 'F', '0', '0', 'life:video:query', '#', 'admin', NOW(), '', NULL, '', NULL),
+('视频新增', @c_video_menu_id, 2, '', NULL, 1, 0, 'F', '0', '0', 'life:video:add', '#', 'admin', NOW(), '', NULL, '', NULL),
+('视频修改', @c_video_menu_id, 3, '', NULL, 1, 0, 'F', '0', '0', 'life:video:edit', '#', 'admin', NOW(), '', NULL, '', NULL),
+('视频删除', @c_video_menu_id, 4, '', NULL, 1, 0, 'F', '0', '0', 'life:video:remove', '#', 'admin', NOW(), '', NULL, '', NULL);

+ 17 - 1
fs-service/src/main/resources/mapper/live/LiveMapper.xml

@@ -35,12 +35,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="liveCodeUrl"    column="live_code_url"    />
         <result property="globalVisible"    column="global_visible"    />
         <result property="liveGroupType"    column="live_group_type"    />
+        <result property="vestId"    column="vest_id"    />
+        <result property="visibleScope"    column="visible_scope"    />
     </resultMap>
 
     <sql id="selectLiveVo">
         select live_id, company_id, company_user_id, training_period_id, talent_id, live_name, is_audit, live_desc, show_type, status, anchor_id, live_type, start_time, finish_time,
                live_img_url, live_config, id_card_url, is_show, is_del, qw_qr_code, rtmp_url, flv_hls_url,
-               create_time, create_by, update_by, update_time, remark,config_json,global_visible,live_group_type from live
+               create_time, create_by, update_by, update_time, remark,config_json,global_visible,live_group_type,vest_id,visible_scope from live
     </sql>
 
     <select id="liveList" parameterType="Live" resultMap="LiveResult">
@@ -197,6 +199,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="idCardUrl != null">id_card_url,</if>
             <if test="globalVisible != null">global_visible,</if>
             <if test="liveGroupType != null">live_group_type,</if>
+            <if test="vestId != null">vest_id,</if>
+            <if test="visibleScope != null">visible_scope,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="companyId != null">#{companyId},</if>
@@ -228,6 +232,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="idCardUrl != null">#{idCardUrl},</if>
             <if test="globalVisible != null">#{globalVisible},</if>
             <if test="liveGroupType != null">#{liveGroupType},</if>
+            <if test="vestId != null">#{vestId},</if>
+            <if test="visibleScope != null">#{visibleScope},</if>
          </trim>
     </insert>
 
@@ -262,6 +268,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="idCardUrl != null">id_card_url = #{idCardUrl},</if>
             <if test="globalVisible != null">global_visible = #{globalVisible},</if>
             <if test="liveGroupType != null">live_group_type = #{liveGroupType},</if>
+            <if test="vestId != null">vest_id = #{vestId},</if>
+            <if test="visibleScope != null">visible_scope = #{visibleScope},</if>
         </trim>
         where live_id = #{liveId}
     </update>
@@ -457,5 +465,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         ORDER BY l.live_id
     </select>
 
+    <select id="selectPublicLivingList" resultMap="LiveResult">
+        <include refid="selectLiveVo"/>
+        where is_del = 0
+          and is_show = 1
+          and status = 2
+          and visible_scope = 1
+        order by start_time asc, live_id asc
+    </select>
 
 </mapper>

+ 8 - 0
fs-service/src/main/resources/mapper/qw/QwSopSmsLogsMapper.xml

@@ -267,4 +267,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         AND status = 1
         AND update_time  &lt;= DATE_SUB(NOW(), INTERVAL 40 MINUTE)
     </select>
+
+    <select id="selectContactIdsByTemplateAndSendTimeAfter" resultType="java.lang.Long">
+        select distinct contact_id
+        from qw_sop_sms_logs
+        where sms_template_code = #{smsTemplateCode}
+          and send_time &gt;= #{sendTimeAfter}
+          and contact_id is not null
+    </select>
 </mapper>