yh 4 месяцев назад
Родитель
Сommit
f9d7b0d0ef
17 измененных файлов с 590 добавлено и 207 удалено
  1. 13 0
      fs-live-app/src/main/java/com/fs/live/config/TenantPrincipal.java
  2. 79 0
      fs-live-app/src/main/java/com/fs/live/datasource/TenantDataSourceManager.java
  3. 21 18
      fs-live-app/src/main/java/com/fs/live/task/LiveCompletionPointsTask.java
  4. 80 2
      fs-live-app/src/main/java/com/fs/live/task/Task.java
  5. 121 0
      fs-live-app/src/main/java/com/fs/live/task/TenantTaskRunner.java
  6. 15 10
      fs-live-app/src/main/java/com/fs/live/websocket/auth/AuthHandler.java
  7. 113 0
      fs-live-app/src/main/java/com/fs/live/websocket/auth/TenantChannelContext.java
  8. 6 0
      fs-live-app/src/main/java/com/fs/live/websocket/constant/AttrConstant.java
  9. 95 157
      fs-live-app/src/main/java/com/fs/live/websocket/handle/LiveChatHandler.java
  10. 5 1
      fs-live-app/src/main/resources/application.yml
  11. 10 10
      fs-service/src/main/java/com/fs/core/utils/OrderCodeUtils.java
  12. 3 1
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreOrderScrmService.java
  13. 10 2
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  14. 2 2
      fs-user-app/src/main/java/com/fs/FsUserAppApplication.java
  15. 3 3
      fs-user-app/src/main/java/com/fs/app/controller/store/PayScrmController.java
  16. 1 1
      fs-user-app/src/main/java/com/fs/app/controller/store/StoreOrderScrmController.java
  17. 13 0
      fs-user-app/src/main/java/com/fs/framework/filter/AppTenantSwitchFilter.java

+ 13 - 0
fs-live-app/src/main/java/com/fs/live/config/TenantPrincipal.java

@@ -0,0 +1,13 @@
+package com.fs.live.config;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * SaaS 多租户身份标识,存入 SecurityContext,供 TenantKeyRedisSerializer 读取 tenantId。
+ */
+@Getter
+@AllArgsConstructor
+public class TenantPrincipal {
+    private final Long tenantId;
+}

+ 79 - 0
fs-live-app/src/main/java/com/fs/live/datasource/TenantDataSourceManager.java

@@ -0,0 +1,79 @@
+package com.fs.live.datasource;
+
+import com.alibaba.druid.pool.DruidDataSource;
+import com.fs.framework.datasource.DynamicDataSource;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import com.fs.tenant.domain.TenantInfo;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import javax.sql.DataSource;
+import java.lang.reflect.Field;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * SaaS 多租户数据源管理器(fs-live-app 本地版)。
+ * 按需动态创建租户 DruidDataSource,并注册到 DynamicDataSource.resolvedDataSources。
+ */
+@Component
+public class TenantDataSourceManager {
+
+    @Resource
+    private DynamicDataSource dynamicDataSource;
+
+    private static final Map<String, DataSource> TENANT_DS_CACHE = new ConcurrentHashMap<>();
+
+    /**
+     * 切换到指定租户数据源(不存在则动态创建)。
+     */
+    public void switchTenant(TenantInfo tenantInfo) {
+        String tenantKey = buildTenantKey(tenantInfo.getId());
+        if (!TENANT_DS_CACHE.containsKey(tenantKey)) {
+            synchronized (this) {
+                if (!TENANT_DS_CACHE.containsKey(tenantKey)) {
+                    DataSource ds = createTenantDataSource(tenantInfo);
+                    TENANT_DS_CACHE.put(tenantKey, ds);
+                    getResolvedDataSources().put(tenantKey, ds);
+                }
+            }
+        }
+        DynamicDataSourceContextHolder.setDataSourceType(tenantKey);
+    }
+
+    /**
+     * 清理当前线程的数据源标记。
+     */
+    public void clear() {
+        DynamicDataSourceContextHolder.clearDataSourceType();
+    }
+
+    private String buildTenantKey(Long tenantId) {
+        return "tenant:" + tenantId;
+    }
+
+    private DataSource createTenantDataSource(TenantInfo tenant) {
+        DruidDataSource ds = new DruidDataSource();
+        ds.setUrl(tenant.getDbUrl());
+        ds.setUsername(tenant.getDbAccount());
+        ds.setPassword(tenant.getDbPwd());
+        ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
+        ds.setInitialSize(5);
+        ds.setMinIdle(5);
+        ds.setMaxActive(20);
+        ds.setMaxWait(60000);
+        return ds;
+    }
+
+    @SuppressWarnings("unchecked")
+    private Map<Object, DataSource> getResolvedDataSources() {
+        try {
+            Field field = org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource.class
+                    .getDeclaredField("resolvedDataSources");
+            field.setAccessible(true);
+            return (Map<Object, DataSource>) field.get(dynamicDataSource);
+        } catch (Exception e) {
+            throw new IllegalStateException("获取 resolvedDataSources 失败", e);
+        }
+    }
+}

+ 21 - 18
fs-live-app/src/main/java/com/fs/live/task/LiveCompletionPointsTask.java

@@ -1,23 +1,17 @@
 package com.fs.live.task;
 
-import com.alibaba.fastjson.JSONObject;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.live.domain.Live;
-import com.fs.live.domain.LiveCompletionPointsRecord;
 import com.fs.live.service.ILiveCompletionPointsRecordService;
 import com.fs.live.service.ILiveService;
-import com.fs.live.websocket.bean.SendMsgVo;
-import com.fs.live.websocket.service.WebSocketServer;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
 
 /**
  * 直播完课积分定时任务
@@ -33,10 +27,13 @@ public class LiveCompletionPointsTask {
     private ILiveCompletionPointsRecordService completionPointsRecordService;
 
     @Autowired
-    private WebSocketServer webSocketServer;
+    private ILiveService liveService;
 
     @Autowired
-    private ILiveService liveService;
+    private TenantTaskRunner tenantTaskRunner;
+
+    @Value("${saas.task.enabled}")
+    private boolean saasTaskEnabled;
 
     /**
      * 定时检查观看时长并创建完课记录(兜底机制)
@@ -45,10 +42,18 @@ public class LiveCompletionPointsTask {
      */
     @Scheduled(cron = "0 */1 * * * ?")
     public void checkCompletionStatus() {
+        if (saasTaskEnabled && !TenantTaskRunner.isInTenantExecution()) {
+            tenantTaskRunner.runForEachTenant("checkCompletionStatus", this::doCheckCompletionStatus);
+            return;
+        }
+        doCheckCompletionStatus();
+    }
+
+    private void doCheckCompletionStatus() {
         try {
             // 只查询开启了完课积分配置的直播间
             List<Live> activeLives = liveService.selectLiveListWithCompletionPointsEnabled();
-            
+
             if (activeLives == null || activeLives.isEmpty()) {
                 log.debug("当前没有开启完课积分的直播间");
                 return;
@@ -57,28 +62,26 @@ public class LiveCompletionPointsTask {
             for (Live live : activeLives) {
                 try {
                     Long liveId = live.getLiveId();
-                    
+
                     // 使用Hash结构获取该直播间所有用户的观看时长
                     String hashKey = "live:watch:duration:hash:" + liveId;
                     Map<Object, Object> userDurations = redisCache.hashEntries(hashKey);
-                    
-                    if (userDurations == null || userDurations.isEmpty()) {
 
+                    if (userDurations == null || userDurations.isEmpty()) {
                         continue;
                     }
-                    // 3. 逐个用户处理
+
+                    // 逐个用户处理
                     for (Map.Entry<Object, Object> entry : userDurations.entrySet()) {
                         try {
                             Long userId = Long.parseLong(entry.getKey().toString());
-                            Long duration = Long.parseLong(entry.getValue().toString());  // 从 Redis 直接获取观看时长
-                            
+                            Long duration = Long.parseLong(entry.getValue().toString());
                             completionPointsRecordService.checkAndCreateCompletionRecord(liveId, userId, duration);
-
                         } catch (Exception e) {
                             log.error("处理用户完课状态失败, liveId={}, userId={}", liveId, entry.getKey(), e);
                         }
                     }
-                    
+
                 } catch (Exception e) {
                     log.error("处理直播间完课状态失败, liveId={}", live.getLiveId(), e);
                 }

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

@@ -20,11 +20,12 @@ import com.fs.live.vo.LiveLotteryProductListVo;
 import com.fs.live.vo.LotteryVo;
 import com.fs.live.websocket.bean.SendMsgVo;
 import com.fs.live.websocket.service.WebSocketServer;
-import lombok.AllArgsConstructor;
+import lombok.RequiredArgsConstructor;
 import org.apache.commons.collections4.CollectionUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.annotation.Transactional;
@@ -43,7 +44,7 @@ import static com.fs.common.constant.LiveKeysConstant.LIVE_COUPON_NUM;
 import static com.fs.live.websocket.service.WebSocketServer.USER_ENTRY_TIME_KEY;
 
 @Component
-@AllArgsConstructor
+@RequiredArgsConstructor
 public class Task {
 
     private static final Logger log = LoggerFactory.getLogger(Task.class);
@@ -82,10 +83,23 @@ public class Task {
 
     @Autowired
     public FsJstAftersalePushService fsJstAftersalePushService;
+    @Autowired
+    private TenantTaskRunner tenantTaskRunner;
+
+    @Value("${saas.task.enabled}")
+    private boolean saasTaskEnabled;
 
     @Scheduled(cron = "0 0/1 * * * ?")
     @DistributeLock(key = "updateLiveStatusByTime", scene = "task")
     public void updateLiveStatusByTime() {
+        if (saasTaskEnabled && !TenantTaskRunner.isInTenantExecution()) {
+            tenantTaskRunner.runForEachTenant("updateLiveStatusByTime", this::doUpdateLiveStatusByTime);
+            return;
+        }
+        doUpdateLiveStatusByTime();
+    }
+
+    private void doUpdateLiveStatusByTime() {
         List<Live> list = liveService.selectNoEndLiveList();
         if (list.isEmpty())
             return;
@@ -242,6 +256,14 @@ public class Task {
     @Scheduled(cron = "0/1 * * * * ?")
     @DistributeLock(key = "liveLotteryTask", scene = "task")
     public void liveLotteryTask() {
+        if (saasTaskEnabled && !TenantTaskRunner.isInTenantExecution()) {
+            tenantTaskRunner.runForEachTenant("liveLotteryTask", this::doLiveLotteryTask);
+            return;
+        }
+        doLiveLotteryTask();
+    }
+
+    private void doLiveLotteryTask() {
         long currentTime = Instant.now().toEpochMilli(); // 当前时间戳(毫秒)
         String lotteryKey = "live:lottery_task:*";
         Set<String> allLiveKeys = redisCache.redisTemplate.keys(lotteryKey);
@@ -372,6 +394,14 @@ public class Task {
     @Scheduled(cron = "0/1 * * * * ?")
     @DistributeLock(key = "liveAutoTask", scene = "task")
     public void liveAutoTask() {
+        if (saasTaskEnabled && !TenantTaskRunner.isInTenantExecution()) {
+            tenantTaskRunner.runForEachTenant("liveAutoTask", this::doLiveAutoTask);
+            return;
+        }
+        doLiveAutoTask();
+    }
+
+    private void doLiveAutoTask() {
         long currentTime = Instant.now().toEpochMilli(); // 当前时间戳(毫秒)
 
         Set<String> allLiveKeys = redisCache.redisTemplate.keys("live:auto_task:*");
@@ -405,6 +435,14 @@ public class Task {
     @DistributeLock(key = "autoUpdateWatchReward", scene = "task")
     @Transactional
     public void autoUpdateWatchReward() {
+        if (saasTaskEnabled && !TenantTaskRunner.isInTenantExecution()) {
+            tenantTaskRunner.runForEachTenant("autoUpdateWatchReward", this::doAutoUpdateWatchReward);
+            return;
+        }
+        doAutoUpdateWatchReward();
+    }
+
+    private void doAutoUpdateWatchReward() {
 
         // 1.查询所有直播中的直播间
         List<Live> lives = liveService.liveList();
@@ -510,6 +548,14 @@ public class Task {
      */
     @Scheduled(cron = "0 0/1 * * * ?")// 每分钟执行一次
     public void syncLiveDataToDB() {
+        if (saasTaskEnabled && !TenantTaskRunner.isInTenantExecution()) {
+            tenantTaskRunner.runForEachTenant("syncLiveDataToDB", this::doSyncLiveDataToDB);
+            return;
+        }
+        doSyncLiveDataToDB();
+    }
+
+    private void doSyncLiveDataToDB() {
         List<LiveData> liveDatas = liveDataService.getAllLiveDatas(); // 获取所有正在直播的直播间数据
         if(liveDatas == null)
             return;
@@ -631,6 +677,14 @@ public class Task {
     @Scheduled(cron = "0/5 * * * * ?")
     @DistributeLock(key = "updateRedQuantityNum", scene = "task")
     public void updateRedQuantityNum() {
+        if (saasTaskEnabled && !TenantTaskRunner.isInTenantExecution()) {
+            tenantTaskRunner.runForEachTenant("updateRedQuantityNum", this::doUpdateRedQuantityNum);
+            return;
+        }
+        doUpdateRedQuantityNum();
+    }
+
+    private void doUpdateRedQuantityNum() {
         liveRedConfService.updateRedQuantityNum();
     }
 
@@ -641,6 +695,14 @@ public class Task {
     @Scheduled(cron = "0/10 * * * * ?")
     @DistributeLock(key = "scanLiveTagMark", scene = "task")
     public void scanLiveTagMark() {
+        if (saasTaskEnabled && !TenantTaskRunner.isInTenantExecution()) {
+            tenantTaskRunner.runForEachTenant("scanLiveTagMark", this::doScanLiveTagMark);
+            return;
+        }
+        doScanLiveTagMark();
+    }
+
+    private void doScanLiveTagMark() {
         try {
 
             // 获取所有打标签缓存的key
@@ -813,6 +875,14 @@ public class Task {
     @Scheduled(cron = "0/30 * * * * ?")
     @DistributeLock(key = "scanLiveWatchUserStatus", scene = "task")
     public void scanLiveWatchUserStatus() {
+        if (saasTaskEnabled && !TenantTaskRunner.isInTenantExecution()) {
+            tenantTaskRunner.runForEachTenant("scanLiveWatchUserStatus", this::doScanLiveWatchUserStatus);
+            return;
+        }
+        doScanLiveWatchUserStatus();
+    }
+
+    private void doScanLiveWatchUserStatus() {
         try {
             DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
             // 查询所有正在直播的直播间
@@ -988,6 +1058,14 @@ public class Task {
     @Scheduled(cron = "0 0/1 * * * ?")
     @DistributeLock(key = "updateLiveWatchUserStatus", scene = "task")
     public void updateLiveWatchUserStatus() {
+        if (saasTaskEnabled && !TenantTaskRunner.isInTenantExecution()) {
+            tenantTaskRunner.runForEachTenant("updateLiveWatchUserStatus", this::doUpdateLiveWatchUserStatus);
+            return;
+        }
+        doUpdateLiveWatchUserStatus();
+    }
+
+    private void doUpdateLiveWatchUserStatus() {
         try {
             Set<String> keys = redisCache.redisTemplate.keys("live:user:watch:log:*");
             LocalDateTime now = LocalDateTime.now();

+ 121 - 0
fs-live-app/src/main/java/com/fs/live/task/TenantTaskRunner.java

@@ -0,0 +1,121 @@
+package com.fs.live.task;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.enums.DataSourceType;
+import com.fs.common.utils.StringUtils;
+import com.fs.config.saas.ProjectConfig;
+import com.fs.core.config.TenantConfigContext;
+import com.fs.live.config.TenantPrincipal;
+import com.fs.live.datasource.TenantDataSourceManager;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.mapper.SysConfigMapper;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.service.TenantInfoService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * SaaS 模式下按租户执行定时任务。
+ * 从主库查启用且未过期的租户,逐租户切库、加载项目配置、设置 SecurityContext 后执行传入逻辑。
+ */
+@Slf4j
+@Component
+public class TenantTaskRunner {
+
+    /** ThreadLocal 防止任务方法递归二次分发 */
+    private static final ThreadLocal<Boolean> IN_TENANT_EXECUTION = ThreadLocal.withInitial(() -> false);
+
+    @Resource
+    private TenantDataSourceManager tenantDataSourceManager;
+    @Resource
+    private TenantInfoService tenantInfoService;
+    @Resource
+    private SysConfigMapper sysConfigMapper;
+
+    public static boolean isInTenantExecution() {
+        return Boolean.TRUE.equals(IN_TENANT_EXECUTION.get());
+    }
+
+    /**
+     * 对每个启用且未过期的租户执行无参逻辑(任务名用于日志)。
+     */
+    public void runForEachTenant(String taskName, Runnable action) {
+        List<TenantInfo> tenants = getValidTenants();
+        if (tenants == null || tenants.isEmpty()) {
+            log.debug("[SaaS Live Task] 无有效租户,跳过任务: {}", taskName);
+            return;
+        }
+        for (TenantInfo tenant : tenants) {
+            runForOneTenant(tenant, taskName, action);
+        }
+    }
+
+    private List<TenantInfo> getValidTenants() {
+        try {
+            DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+            TenantInfo query = new TenantInfo();
+            query.setStatus(1);
+            List<TenantInfo> tenants = tenantInfoService.selectTenantInfoList(query);
+            if (tenants == null || tenants.isEmpty()) {
+                return Collections.emptyList();
+            }
+            Date now = new Date();
+            return tenants.stream()
+                    .filter(t -> t.getExpireTime() == null || !t.getExpireTime().before(now))
+                    .collect(Collectors.toList());
+        } finally {
+            DynamicDataSourceContextHolder.clearDataSourceType();
+        }
+    }
+
+    private void runForOneTenant(TenantInfo tenant, String taskName, Runnable action) {
+        String dsKey = "tenant:" + tenant.getId();
+        try {
+            // 切换租户数据源
+            tenantDataSourceManager.switchTenant(tenant);
+            log.info("[SaaS Live Task] 切换数据源 dataSource={}, tenantId={}, tenantCode={}, task={}",
+                    dsKey, tenant.getId(), tenant.getTenantCode(), taskName);
+
+            // 加载租户项目配置
+            SysConfig cfg = sysConfigMapper.selectConfigByConfigKey("projectConfig");
+            if (cfg != null && StringUtils.isNotBlank(cfg.getConfigValue())) {
+                TenantConfigContext.set(JSONObject.parseObject(cfg.getConfigValue()));
+            } else {
+                TenantConfigContext.set(null);
+            }
+            ProjectConfig.loadTenantConfigsFromContext();
+
+            // 设置租户 SecurityContext,让 TenantKeyRedisSerializer 自动拼租户前缀
+            SecurityContextHolder.getContext().setAuthentication(
+                    new UsernamePasswordAuthenticationToken(
+                            new TenantPrincipal(tenant.getId()),
+                            null,
+                            Collections.emptyList()
+                    )
+            );
+
+            // 标记当前线程正在租户执行上下文中,防止递归分发
+            IN_TENANT_EXECUTION.set(true);
+            action.run();
+
+        } catch (Exception e) {
+            log.error("[SaaS Live Task] 租户 tenantId={}, tenantCode={} 执行任务 {} 异常",
+                    tenant.getId(), tenant.getTenantCode(), taskName, e);
+        } finally {
+            IN_TENANT_EXECUTION.remove();
+            ProjectConfig.clearTenantConfigs();
+            TenantConfigContext.clear();
+            DynamicDataSourceContextHolder.clearDataSourceType();
+            SecurityContextHolder.clearContext();
+        }
+    }
+}

+ 15 - 10
fs-live-app/src/main/java/com/fs/live/websocket/auth/AuthHandler.java

@@ -24,6 +24,7 @@ import java.util.Map;
 public class AuthHandler extends ChannelInboundHandlerAdapter {
 
     private final JwtUtils jwtUtils = SpringUtils.getBean(JwtUtils.class);
+    private final TenantChannelContext tenantChannelContext = SpringUtils.getBean(TenantChannelContext.class);
 
     @Override
     public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
@@ -32,11 +33,11 @@ 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)) {
                 ctx.channel().writeAndFlush(new TextWebSocketFrame("Error: invalid parameters")).addListener(ChannelFutureListener.CLOSE);
                 return;
@@ -53,13 +54,12 @@ public class AuthHandler extends ChannelInboundHandlerAdapter {
 
             // 验证 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(或根据实际业务逻辑设置)
+                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;
+                }
                 ctx.channel().attr(AttrConstant.ATTR_USER_TYPE).set(0L);
             }
 
@@ -68,7 +68,6 @@ public class AuthHandler extends ChannelInboundHandlerAdapter {
                 String userTypeStr = parameterMap.get(AttrConstant.USER_TYPE).get(0);
                 String timestampStr = parameterMap.get(AttrConstant.TIMESTAMP).get(0);
                 String signatureStr = parameterMap.get(AttrConstant.SIGNATURE).get(0);
-
                 try {
                     if (!VerifyUtils.verifySignature(liveId.toString(), userId.toString(), userTypeStr, timestampStr, signatureStr)) {
                         ctx.channel().writeAndFlush(new TextWebSocketFrame("Error: invalid parameters")).addListener(ChannelFutureListener.CLOSE);
@@ -81,10 +80,16 @@ public class AuthHandler extends ChannelInboundHandlerAdapter {
                 }
             }
 
-            // 将 liveId 和 userId 保存到 Channel 属性中,供后续处理使用
+            // 将 liveId 和 userId 保存到 Channel 属性中
             ctx.channel().attr(AttrConstant.ATTR_LIVE_ID).set(liveId);
             ctx.channel().attr(AttrConstant.ATTR_USER_ID).set(userId);
 
+            // SaaS:解析 tenantCode 并绑定租户到 Channel
+            if (parameterMap.containsKey(AttrConstant.TENANT_CODE)) {
+                String tenantCode = parameterMap.get(AttrConstant.TENANT_CODE).get(0);
+                tenantChannelContext.bindTenant(ctx.channel(), tenantCode);
+            }
+
             // 继续处理 WebSocket 握手
             ctx.pipeline().remove(this);
             ctx.fireChannelRead(req.retain());

+ 113 - 0
fs-live-app/src/main/java/com/fs/live/websocket/auth/TenantChannelContext.java

@@ -0,0 +1,113 @@
+package com.fs.live.websocket.auth;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.enums.DataSourceType;
+import com.fs.common.utils.StringUtils;
+import com.fs.config.saas.ProjectConfig;
+import com.fs.core.config.TenantConfigContext;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import com.fs.live.config.TenantPrincipal;
+import com.fs.live.datasource.TenantDataSourceManager;
+import com.fs.live.websocket.constant.AttrConstant;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.mapper.SysConfigMapper;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.service.TenantInfoService;
+import io.netty.channel.Channel;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * SaaS WebSocket 租户上下文工具:
+ * 1. 握手时从 URL 参数解析 tenantCode,查主库获取租户,将 tenantId/dsKey 写入 Channel 属性。
+ * 2. 每次业务处理前调用 activate(),从 Channel 属性切换数据源 + 设置 SecurityContext。
+ * 3. 业务处理完成后调用 clear() 清理线程变量。
+ */
+@Slf4j
+@Component
+public class TenantChannelContext {
+
+    @Value("${saas.task.enabled:false}")
+    private boolean saasEnabled;
+
+    @Resource
+    private TenantInfoService tenantInfoService;
+    @Resource
+    private TenantDataSourceManager tenantDataSourceManager;
+    @Resource
+    private SysConfigMapper sysConfigMapper;
+
+    /**
+     * 握手阶段:解析 tenantCode,将租户信息绑定到 Channel。
+     * 若未启用 SaaS 或未传 tenantCode,不做任何处理(兼容单库模式)。
+     */
+    public void bindTenant(Channel channel, String tenantCode) {
+        if (!saasEnabled || StringUtils.isBlank(tenantCode)) return;
+        try {
+            DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+            TenantInfo query = new TenantInfo();
+            query.setTenantCode(tenantCode);
+            List<TenantInfo> list = tenantInfoService.selectTenantInfoList(query);
+            if (list == null || list.isEmpty()) {
+                log.warn("[SaaS WS] tenantCode={} 未找到租户", tenantCode);
+                return;
+            }
+            TenantInfo tenant = list.get(0);
+            if (!Integer.valueOf(1).equals(tenant.getStatus())) return;
+            if (tenant.getExpireTime() != null && tenant.getExpireTime().before(new Date())) return;
+            channel.attr(AttrConstant.ATTR_TENANT_ID).set(tenant.getId());
+            channel.attr(AttrConstant.ATTR_DS_KEY).set("tenant:" + tenant.getId());
+            // 预先确保数据源已注册
+            tenantDataSourceManager.switchTenant(tenant);
+            log.info("[SaaS WS] 绑定租户 tenantId={}, tenantCode={} 到 Channel", tenant.getId(), tenantCode);
+        } finally {
+            DynamicDataSourceContextHolder.clearDataSourceType();
+        }
+    }
+
+    /**
+     * 业务处理前:从 Channel 属性激活租户数据源 + SecurityContext。
+     * 若 Channel 未绑定租户,不做切换(单库模式)。
+     */
+    public void activate(Channel channel) {
+        if (!saasEnabled) return;
+        Long tenantId = channel.attr(AttrConstant.ATTR_TENANT_ID).get();
+        String dsKey = channel.attr(AttrConstant.ATTR_DS_KEY).get();
+        if (tenantId == null || StringUtils.isBlank(dsKey)) return;
+        DynamicDataSourceContextHolder.setDataSourceType(dsKey);
+        SecurityContextHolder.getContext().setAuthentication(
+                new UsernamePasswordAuthenticationToken(
+                        new TenantPrincipal(tenantId), null, Collections.emptyList()));
+        // 加载项目配置
+        try {
+            SysConfig cfg = sysConfigMapper.selectConfigByConfigKey("projectConfig");
+            if (cfg != null && StringUtils.isNotBlank(cfg.getConfigValue())) {
+                TenantConfigContext.set(JSONObject.parseObject(cfg.getConfigValue()));
+            } else {
+                TenantConfigContext.set(null);
+            }
+            ProjectConfig.loadTenantConfigsFromContext();
+        } catch (Exception e) {
+            log.warn("[SaaS WS] 加载租户项目配置失败 tenantId={}", tenantId, e);
+        }
+    }
+
+    /**
+     * 业务处理后:清理线程变量,避免线程复用串库。
+     */
+    public void clear() {
+        if (!saasEnabled) return;
+        try { ProjectConfig.clearTenantConfigs(); } catch (Exception ignored) {}
+        TenantConfigContext.clear();
+        DynamicDataSourceContextHolder.clearDataSourceType();
+        SecurityContextHolder.clearContext();
+    }
+}

+ 6 - 0
fs-live-app/src/main/java/com/fs/live/websocket/constant/AttrConstant.java

@@ -15,10 +15,16 @@ public class AttrConstant {
     public static final String LOCATION = "location";
     public static final String QW_USER_ID = "qwUserId";
     public static final String EXTERNAL_CONTACT_ID = "externalContactId";
+    /** SaaS: URL 参数中的租户编码 */
+    public static final String TENANT_CODE = "tenantCode";
 
     // 定义 AttributeKey 保存必要参数
     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(USER_TYPE);
+    /** SaaS: Channel 上绑定的租户ID,-1 表示未启用多租户 */
+    public static final AttributeKey<Long> ATTR_TENANT_ID = AttributeKey.newInstance("tenantId");
+    /** SaaS: Channel 上绑定的数据源 key,如 "tenant:1" */
+    public static final AttributeKey<String> ATTR_DS_KEY = AttributeKey.newInstance("dsKey");
 }

+ 95 - 157
fs-live-app/src/main/java/com/fs/live/websocket/handle/LiveChatHandler.java

@@ -1,8 +1,6 @@
 package com.fs.live.websocket.handle;
 
 import com.alibaba.fastjson.JSONObject;
-import com.fs.his.domain.FsUser;
-import com.fs.his.service.IFsUserService;
 import com.fs.hisStore.domain.FsUserScrm;
 import com.fs.hisStore.service.IFsUserScrmService;
 import com.fs.live.websocket.bean.SendMsgVo;
@@ -14,7 +12,7 @@ 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.auth.TenantChannelContext;
 import io.netty.channel.*;
 import io.netty.channel.group.ChannelGroup;
 import io.netty.channel.group.DefaultChannelGroup;
@@ -36,7 +34,6 @@ import java.util.concurrent.CopyOnWriteArrayList;
 @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<>();
@@ -44,128 +41,68 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
     private final static ILiveWatchUserService liveWatchUserService = SpringUtils.getBean(ILiveWatchUserService.class);
     private final static ILiveMsgService liveMsgService = SpringUtils.getBean(ILiveMsgService.class);
     private final static IFsUserScrmService fsUserService = SpringUtils.getBean(IFsUserScrmService.class);
+    private final static TenantChannelContext tenantChannelContext = SpringUtils.getBean(TenantChannelContext.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 (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());
-
-            if (userType == 0) {
-
-
-                FsUserScrm fsUser = fsUserService.selectFsUserByUserId(userId);
-                // 加入房间
-                LiveWatchUser liveWatchUser = liveWatchUserService.joinWithoutLocation(fsUser,liveId, userId);
-                room.put(userId, ctx.channel());
-                if (Objects.isNull(fsUser)) {
-                    ctx.channel().writeAndFlush(new TextWebSocketFrame("Error: 用户信息错误")).addListener(ChannelFutureListener.CLOSE);
+            try {
+                tenantChannelContext.activate(ctx.channel());
+                if (Objects.isNull(liveService.selectLiveByLiveId(liveId))) {
+                    ctx.channel().writeAndFlush(new TextWebSocketFrame("Error: 未找到直播间")).addListener(ChannelFutureListener.CLOSE);
                     return;
                 }
-
-
-                SendMsgVo sendMsgVo = new SendMsgVo();
-                sendMsgVo.setLiveId(liveId);
-                sendMsgVo.setUserId(userId);
-                sendMsgVo.setUserType(userType);
-                sendMsgVo.setCmd("entry");
-                sendMsgVo.setMsg("用户进入");
-                sendMsgVo.setData(JSONObject.toJSONString(liveWatchUser));
-                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());
+                Map<Long, Channel> room = getRoom(liveId);
+                List<Channel> adminRoom = getAdminRoom(liveId);
+                ChannelGroup roomGroup = getRoomGroup(liveId);
+                roomGroup.add(ctx.channel());
+                if (userType == 0) {
+                    FsUserScrm fsUser = fsUserService.selectFsUserByUserId(userId);
+                    LiveWatchUser liveWatchUser = liveWatchUserService.joinWithoutLocation(fsUser, liveId, userId);
+                    room.put(userId, ctx.channel());
+                    if (Objects.isNull(fsUser)) {
+                        ctx.channel().writeAndFlush(new TextWebSocketFrame("Error: 用户信息错误")).addListener(ChannelFutureListener.CLOSE);
+                        return;
+                    }
+                    SendMsgVo sendMsgVo = new SendMsgVo();
+                    sendMsgVo.setLiveId(liveId);
+                    sendMsgVo.setUserId(userId);
+                    sendMsgVo.setUserType(userType);
+                    sendMsgVo.setCmd("entry");
+                    sendMsgVo.setMsg("用户进入");
+                    sendMsgVo.setData(JSONObject.toJSONString(liveWatchUser));
+                    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());
+                }
+            } finally {
+                tenantChannelContext.clear();
             }
-
         }
     }
 
-    /**
-     * 获取房间
-     * @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;
+    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame textWebSocketFrame) throws Exception {
+        Long liveId = ctx.channel().attr(AttrConstant.ATTR_LIVE_ID).get();
+        Long userType = ctx.channel().attr(AttrConstant.ATTR_USER_TYPE).get();
+        SendMsgVo msg = JSONObject.parseObject(textWebSocketFrame.text(), SendMsgVo.class);
+        if (msg.isOn()) return;
         try {
+            tenantChannelContext.activate(ctx.channel());
             switch (msg.getCmd()) {
                 case "heartbeat":
-                    sendMessage(channelHandlerContext.channel(), JSONObject.toJSONString(R.ok().put("data", msg)));
+                    sendMessage(ctx.channel(), JSONObject.toJSONString(R.ok().put("data", msg)));
                     break;
                 case "sendMsg":
                     LiveMsg liveMsg = new LiveMsg();
@@ -175,96 +112,97 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
                     liveMsg.setAvatar(msg.getAvatar());
                     liveMsg.setMsg(msg.getMsg());
                     liveMsg.setCreateTime(new Date());
-
                     if (userType == 0) {
                         Map<String, Integer> liveFlagWithCache = liveWatchUserService.getLiveFlagWithCache(liveId);
-                        LiveWatchUser liveWatchUser = liveWatchUserService.selectLiveWatchUserByFlag(msg.getLiveId(), msg.getUserId(), liveFlagWithCache.get("liveFlag"),  liveFlagWithCache.get("replayFlag"));
-                        if(liveWatchUser != null && liveWatchUser.getMsgStatus() == 1){
-                            sendMessage(channelHandlerContext.channel(), JSONObject.toJSONString(R.error("你以被禁言")));
+                        LiveWatchUser liveWatchUser = liveWatchUserService.selectLiveWatchUserByFlag(
+                                msg.getLiveId(), msg.getUserId(),
+                                liveFlagWithCache.get("liveFlag"), liveFlagWithCache.get("replayFlag"));
+                        if (liveWatchUser != null && liveWatchUser.getMsgStatus() == 1) {
+                            sendMessage(ctx.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);
+        } finally {
+            tenantChannelContext.clear();
         }
     }
 
     /**
      * 断开连接
-     * @param ctx   连接
-     * @throws Exception    异常
      */
     @Override
     public void channelInactive(ChannelHandlerContext ctx) throws Exception {
-
         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) {
-            FsUserScrm fsUser = fsUserService.selectFsUserByUserId(userId);
-            LiveWatchUser close = liveWatchUserService.close(fsUser,liveId, userId);
-            room.remove(userId);
-
-            if (room.isEmpty()) {
-                rooms.remove(liveId);
-            }
-
-
-
-            SendMsgVo sendMsgVo = new SendMsgVo();
-            sendMsgVo.setLiveId(liveId);
-            sendMsgVo.setUserId(userId);
-            sendMsgVo.setUserType(userType);
-            sendMsgVo.setCmd("out");
-            sendMsgVo.setMsg("用户离开");
-            sendMsgVo.setData(JSONObject.toJSONString(close));
-            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);
+        try {
+            tenantChannelContext.activate(ctx.channel());
+            Map<Long, Channel> room = getRoom(liveId);
+            List<Channel> adminRoom = getAdminRoom(liveId);
+            ChannelGroup roomGroup = getRoomGroup(liveId);
+            if (userType == 0) {
+                FsUserScrm fsUser = fsUserService.selectFsUserByUserId(userId);
+                LiveWatchUser close = liveWatchUserService.close(fsUser, liveId, userId);
+                room.remove(userId);
+                if (room.isEmpty()) rooms.remove(liveId);
+                SendMsgVo sendMsgVo = new SendMsgVo();
+                sendMsgVo.setLiveId(liveId);
+                sendMsgVo.setUserId(userId);
+                sendMsgVo.setUserType(userType);
+                sendMsgVo.setCmd("out");
+                sendMsgVo.setMsg("用户离开");
+                sendMsgVo.setData(JSONObject.toJSONString(close));
+                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);
             }
+            roomGroup.remove(ctx.channel());
+            if (roomGroup.isEmpty()) roomGroups.remove(liveId);
+        } finally {
+            tenantChannelContext.clear();
         }
-        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 {
         log.error("连接异常 msg: {}", cause.getMessage(), cause);
         ctx.close();
     }
+
+    private CopyOnWriteArrayList<Channel> getAdminRoom(Long liveId) {
+        return adminRooms.computeIfAbsent(liveId, k -> new CopyOnWriteArrayList<>());
+    }
+
+    private ConcurrentHashMap<Long, Channel> getRoom(Long liveId) {
+        return rooms.computeIfAbsent(liveId, k -> new ConcurrentHashMap<>());
+    }
+
+    private ChannelGroup getRoomGroup(Long liveId) {
+        return roomGroups.computeIfAbsent(liveId, k -> new DefaultChannelGroup(GlobalEventExecutor.INSTANCE));
+    }
+
+    private void broadcastMessage(Long liveId, String msg) {
+        getRoomGroup(liveId).writeAndFlush(new TextWebSocketFrame(msg));
+    }
+
+    private void sendMessage(Channel channel, String message) {
+        channel.writeAndFlush(new TextWebSocketFrame(message));
+    }
 }

+ 5 - 1
fs-live-app/src/main/resources/application.yml

@@ -13,4 +13,8 @@ server:
 # Spring配置
 spring:
   profiles:
-    active: druid-bjzm-test
+    active: dev
+
+saas:
+  task:
+    enabled: true

+ 10 - 10
fs-service/src/main/java/com/fs/core/utils/OrderCodeUtils.java

@@ -44,16 +44,16 @@ public class OrderCodeUtils {
 
     }
     public static String getOrderSn(){
-        String url= FSConfig.getCommonApi()+ "/app/common/genOrderCode";
-//        String url= "42.194.245.189:8010/app/common/genOrderCode";
-        String json = HttpRequest.get(url)
-                .execute().body();
-        OrderCodeVO vo= JSONUtil.toBean(json, OrderCodeVO.class);
-        if(vo.getCode()==200){
-            return vo.getOrderCode();
-        }
-        else return null;
-//        return OrderCodeUtils.genOrderSn();
+//        String url= FSConfig.getCommonApi()+ "/app/common/genOrderCode";
+////        String url= "42.194.245.189:8010/app/common/genOrderCode";
+//        String json = HttpRequest.get(url)
+//                .execute().body();
+//        OrderCodeVO vo= JSONUtil.toBean(json, OrderCodeVO.class);
+//        if(vo.getCode()==200){
+//            return vo.getOrderCode();
+//        }
+//        else return null;
+        return OrderCodeUtils.genOrderSn();
 
     }
 

+ 3 - 1
fs-service/src/main/java/com/fs/hisStore/service/IFsStoreOrderScrmService.java

@@ -29,6 +29,8 @@ import com.fs.hisStore.vo.*;
 
 import com.fs.his.vo.FsPrescribeVO;
 
+import javax.servlet.http.HttpServletRequest;
+
 /**
  * 订单Service接口
  *
@@ -295,7 +297,7 @@ public interface IFsStoreOrderScrmService
      * */
     FsStoreOrderAmountScrmStatsVo selectFsStoreOrderAmountScrmStats(FsStoreOrderAmountScrmStatsQueryDto queryDto);
 
-    R pay(FsStoreOrderPayParam param);
+    R pay(FsStoreOrderPayParam param, HttpServletRequest request);
 
     void cancelPay(FsStoreOrderPayParam param);
 

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

@@ -150,6 +150,7 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.interceptor.TransactionAspectSupport;
 
 import javax.annotation.PostConstruct;
+import javax.servlet.http.HttpServletRequest;
 import java.lang.reflect.Field;
 import java.math.BigDecimal;
 import java.nio.charset.Charset;
@@ -405,6 +406,12 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     @Autowired
     private FsUserCompanyPackageScrmMapper fsUserCompanyPackageScrmMapper;
 
+    /**
+     * 租户编码请求头名称。
+     * 前端需在每次请求中携带该头,才能启用多租户能力。
+     */
+    public final String HEADER_TENANT_CODE = "X-Tenant-Code";
+
     @PostConstruct
     public void initErpServiceMap() {
         erpServiceMap = new HashMap<>();
@@ -4370,7 +4377,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     }
     @Override
     @Transactional(rollbackFor = Throwable.class,propagation = Propagation.REQUIRED)
-    public R pay(FsStoreOrderPayParam param) {
+    public R pay(FsStoreOrderPayParam param, HttpServletRequest request) {
         FsStoreOrderScrm order=this.selectFsStoreOrderById(param.getOrderId());
         if(order==null){
             return R.error("订单不存在");
@@ -4474,10 +4481,11 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 fsStorePaymentMapper.insertFsStorePayment(storePayment);
 
                 if (merchantAppConfig.getMerchantType().equals("hf")){
+                    String tenantCode = request.getHeader(HEADER_TENANT_CODE);
                     HuiFuCreateOrder o = new HuiFuCreateOrder();
                     o.setTradeType("T_MINIAPP");
                     o.setOpenid(user.getMaOpenId());
-                    o.setReqSeqId("store-"+storePayment.getPayCode());
+                    o.setReqSeqId(tenantCode +"-store-"+storePayment.getPayCode());
                     o.setTransAmt(storePayment.getPayMoney().toString());
                     o.setGoodsDesc("商城订单支付");
                     o.setAppId(param.getAppId());

+ 2 - 2
fs-user-app/src/main/java/com/fs/FsUserAppApplication.java

@@ -15,12 +15,12 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
 @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
 @EnableTransactionManagement
 @EnableSwaggerBootstrapUI
-@EnableScheduling
+//@EnableScheduling
 public class FsUserAppApplication
 {
     public static void main(String[] args)
     {
-        // System.setProperty("spring.devtools.restart.enabled", "false");
+         System.setProperty("spring.devtools.restart.enabled", "false");
         SpringApplication.run(FsUserAppApplication.class, args);
         System.out.println("AppAPI启动成功");
     }

+ 3 - 3
fs-user-app/src/main/java/com/fs/app/controller/store/PayScrmController.java

@@ -79,14 +79,14 @@ public class PayScrmController {
         logger.info("汇付支付回调:"+o);
         if(o.getResp_code().equals("00000000") && o.getNotify_type().equals("1")){
             String[] order=o.getReq_seq_id().split("-");
-            switch (order[0]) {
+            switch (order[1]) {
                 case "store":
                     try {
-                        HuiFuUtils.updateDivItem(order[1]);
+                        HuiFuUtils.updateDivItem(order[2]);
                     } catch (Exception e) {
                         logger.error("-------分账明细回调错误{}", e.getMessage());
                     }
-                    return orderService.payConfirm(1,null,order[1], o.getHf_seq_id(),o.getOut_trans_id(),o.getParty_order_id());
+                    return orderService.payConfirm(1,null,order[2], o.getHf_seq_id(),o.getOut_trans_id(),o.getParty_order_id());
                 case "store_remain":
                     try {
                         HuiFuUtils.updateDivItem(order[1]);

+ 1 - 1
fs-user-app/src/main/java/com/fs/app/controller/store/StoreOrderScrmController.java

@@ -235,7 +235,7 @@ public class StoreOrderScrmController extends AppBaseController {
                 return R.error("订单正在处理中,请勿重复提交");
             }
 
-            result = orderService.pay(param);
+            result = orderService.pay(param, request);
 
         } catch (InterruptedException e) {
             logger.error("获取支付锁的过程被中断, 订单号: {}", orderId, e);

+ 13 - 0
fs-user-app/src/main/java/com/fs/framework/filter/AppTenantSwitchFilter.java

@@ -8,6 +8,7 @@ import com.fs.core.config.TenantConfigContext;
 import com.fs.framework.datasource.DynamicDataSourceContextHolder;
 import com.fs.framework.datasource.TenantDataSourceManager;
 import com.fs.framework.security.TenantPrincipal;
+import com.fs.huifuPay.domain.HuiFuResult;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
 import com.fs.tenant.domain.TenantInfo;
@@ -18,6 +19,7 @@ import org.springframework.core.annotation.Order;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.stereotype.Component;
+import org.springframework.util.AntPathMatcher;
 import org.springframework.web.filter.OncePerRequestFilter;
 
 import javax.servlet.FilterChain;
@@ -74,6 +76,17 @@ public class AppTenantSwitchFilter extends OncePerRequestFilter {
         String tenantCode = request.getHeader(HEADER_TENANT_CODE);
 
         try {
+            String loginPath = "/store/app/pay/hfPayNotify";
+            AntPathMatcher pathMatcher = new AntPathMatcher();
+            String requestPath = request.getRequestURI();
+            // 汇付回调接口,从参数里面拿租户编码
+            if (pathMatcher.match(loginPath, requestPath)) {
+                String respData = request.getParameter("resp_data");
+                HuiFuResult huiFuResult = JSONObject.parseObject(respData, HuiFuResult.class);
+                String[] strings = huiFuResult.getReq_seq_id().split("-");
+                tenantCode = strings[0];
+            }
+
             if (StringUtils.isBlank(tenantCode)) {
                 SysConfig cfg = sysConfigMapper.selectConfigByConfigKey("projectConfig");
                 if (cfg != null && StringUtils.isNotBlank(cfg.getConfigValue())) {