jzp 1 месяц назад
Родитель
Сommit
32fd5d16fb

+ 169 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/datasource/TenantDataSourceManager.java

@@ -0,0 +1,169 @@
+package com.fs.framework.datasource;
+
+import com.alibaba.druid.pool.DruidDataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.service.TenantInfoService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+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;
+
+/**
+ * 租户数据源管理器
+ * 负责动态创建和管理租户数据源
+ */
+@Component
+public class TenantDataSourceManager {
+
+    private static final Logger log = LoggerFactory.getLogger(TenantDataSourceManager.class);
+
+    @Resource
+    private DynamicDataSource dynamicDataSource;
+
+    @Resource
+    private TenantInfoService tenantInfoService;
+
+    /**
+     * 租户数据源缓存
+     */
+    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 tenantDs = createTenantDataSource(tenantInfo);
+                    TENANT_DS_CACHE.put(tenantKey, tenantDs);
+
+                    // 动态追加到已解析的数据源
+                    Map<Object, DataSource> resolvedMap = getResolvedDataSources();
+                    resolvedMap.put(tenantKey, tenantDs);
+                }
+            }
+        }
+
+        // ThreadLocal 切库
+        DynamicDataSourceContextHolder.setDataSourceType(tenantKey);
+    }
+
+    private String buildTenantKey(Long tenantId) {
+        return "tenant:" + tenantId;
+    }
+
+    /**
+     * 清理 ThreadLocal
+     */
+    public void clear() {
+        DynamicDataSourceContextHolder.clearDataSourceType();
+    }
+
+    /**
+     * 根据租户ID确保数据源已注册并切换(用于 Filter/拦截器等非登录场景)
+     * 解决 JVM 重启后 TENANT_DS_CACHE 被清空,导致 resolvedDataSources 中找不到租户数据源的问题
+     *
+     * @param tenantId 租户ID
+     */
+    public void ensureSwitchByTenantId(Long tenantId) {
+        String tenantKey = buildTenantKey(tenantId);
+
+        // 如果缓存中已有,直接切库
+        if (TENANT_DS_CACHE.containsKey(tenantKey)) {
+            DynamicDataSourceContextHolder.setDataSourceType(tenantKey);
+            log.debug("[TenantDS] 数据源已缓存,直接切换: {}", tenantKey);
+            return;
+        }
+
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        try {
+            TenantInfo tenantInfo = tenantInfoService.getById(tenantId);
+            if (tenantInfo == null) {
+                log.warn("[TenantDS] 租户ID={} 在主库中不存在,回退到主库", tenantId);
+                DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+                return;
+            }
+            if (!tenantInfo.getStatus().equals(1)) {
+                log.warn("[TenantDS] 租户ID={} 已禁用,回退到主库", tenantId);
+                DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+                return;
+            }
+            switchTenant(tenantInfo);
+            log.info("[TenantDS] 动态注册并切换数据源: key={}, url={}", tenantKey, tenantInfo.getDbUrl());
+        } catch (Exception e) {
+            log.error("[TenantDS] 动态注册租户数据源失败, tenantId={}, 回退到主库", tenantId, e);
+            DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        }
+    }
+
+    /**
+     * 获取租户数据源
+     */
+    public DataSource getTenantDataSource(Long tenantId) {
+        String tenantKey = buildTenantKey(tenantId);
+        if (!TENANT_DS_CACHE.containsKey(tenantKey)) {
+            DynamicDataSourceContextHolder.setDataSourceType("MASTER");
+            try {
+                TenantInfo tenantInfo = tenantInfoService.getById(tenantId);
+                if (tenantInfo != null && tenantInfo.getStatus().equals(1)) {
+                    DataSource tenantDs = createTenantDataSource(tenantInfo);
+                    TENANT_DS_CACHE.put(tenantKey, tenantDs);
+                    Map<Object, DataSource> resolvedMap = getResolvedDataSources();
+                    resolvedMap.put(tenantKey, tenantDs);
+                    return tenantDs;
+                }
+            } finally {
+                DynamicDataSourceContextHolder.clearDataSourceType();
+            }
+        }
+        return TENANT_DS_CACHE.get(tenantKey);
+    }
+
+    /**
+     * 创建租户数据源(MySQL + Druid)
+     */
+    private DataSource createTenantDataSource(TenantInfo tenant) {
+
+        DruidDataSource ds = new DruidDataSource();
+        ds.setUrl(tenant.getDbUrl());
+        ds.setUsername(tenant.getDbAccount());
+        ds.setPassword(tenant.getDbPwd());
+
+        // 统一 MySQL
+        ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
+
+        ds.setInitialSize(5);
+        ds.setMinIdle(10);
+        ds.setMaxActive(20);
+        ds.setMaxWait(60000);
+
+        return ds;
+    }
+
+    /**
+     * 反射获取 AbstractRoutingDataSource.resolvedDataSources
+     */
+    @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);
+        }
+    }
+}

+ 218 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/datasource/TenantDataSourceUtil.java

@@ -0,0 +1,218 @@
+package com.fs.framework.datasource;
+
+import com.fs.common.config.RedisTenantContext;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.service.TenantInfoService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * 租户数据源切换工具类
+ * 用于在指定租户数据源下执行增删改查操作
+ */
+@Slf4j
+@Component
+public class TenantDataSourceUtil {
+
+    @Resource
+    private TenantDataSourceManager tenantDataSourceManager;
+
+    @Resource
+    private TenantInfoService tenantInfoService;
+
+    /**
+     * 在指定租户数据源下执行操作(无返回值)
+     *
+     * @param tenantId 租户ID
+     * @param action   要执行的操作
+     */
+    public void execute(Long tenantId, Runnable action) {
+        TenantInfo tenantInfo = getTenantInfo(tenantId);
+        if (tenantInfo == null) {
+            throw new IllegalArgumentException("租户不存在或已禁用,tenantId=" + tenantId);
+        }
+        execute(tenantInfo, action);
+    }
+
+    /**
+     * 在指定租户数据源下执行操作(无返回值)
+     *
+     * @param tenantInfo 租户信息
+     * @param action     要执行的操作
+     */
+    public void execute(TenantInfo tenantInfo, Runnable action) {
+        try {
+            // 切换到租户数据源
+            tenantDataSourceManager.switchTenant(tenantInfo);
+            // 切换Redis租户上下文
+            RedisTenantContext.setTenantId(tenantInfo.getId());
+            log.debug("[TenantDS] 已切换到租户数据源和Redis: tenantId={}, tenantCode={}",
+                    tenantInfo.getId(), tenantInfo.getTenantCode());
+
+            // 执行操作
+            action.run();
+
+        } finally {
+            // 清理数据源上下文
+            tenantDataSourceManager.clear();
+            // 清理Redis租户上下文
+            RedisTenantContext.clear();
+            log.debug("[TenantDS] 已清理租户数据源和Redis上下文");
+        }
+    }
+
+    /**
+     * 在指定租户数据源下执行操作(带返回值)
+     *
+     * @param tenantId 租户ID
+     * @param action   要执行的操作
+     * @param <T>      返回值类型
+     * @return 操作结果
+     */
+    public <T> T executeWithResult(Long tenantId, Supplier<T> action) {
+        TenantInfo tenantInfo = getTenantInfo(tenantId);
+        if (tenantInfo == null) {
+            throw new IllegalArgumentException("租户不存在或已禁用,tenantId=" + tenantId);
+        }
+        return executeWithResult(tenantInfo, action);
+    }
+
+    /**
+     * 在指定租户数据源下执行操作(带返回值)
+     *
+     * @param tenantInfo 租户信息
+     * @param action     要执行的操作
+     * @param <T>        返回值类型
+     * @return 操作结果
+     */
+    public <T> T executeWithResult(TenantInfo tenantInfo, Supplier<T> action) {
+        try {
+            // 切换到租户数据源
+            tenantDataSourceManager.switchTenant(tenantInfo);
+            // 切换Redis租户上下文
+            RedisTenantContext.setTenantId(tenantInfo.getId());
+            log.debug("[TenantDS] 已切换到租户数据源和Redis: tenantId={}, tenantCode={}",
+                    tenantInfo.getId(), tenantInfo.getTenantCode());
+
+            // 执行操作并返回结果
+            return action.get();
+
+        } finally {
+            // 清理数据源上下文
+            tenantDataSourceManager.clear();
+            // 清理Redis租户上下文
+            RedisTenantContext.clear();
+            log.debug("[TenantDS] 已清理租户数据源和Redis上下文");
+        }
+    }
+
+    /**
+     * 在指定租户数据源下执行操作(消费租户信息,无返回值)
+     *
+     * @param tenantId 租户ID
+     * @param action   要执行的操作,接收租户信息作为参数
+     */
+    public void executeWithTenant(Long tenantId, Consumer<TenantInfo> action) {
+        TenantInfo tenantInfo = getTenantInfo(tenantId);
+        if (tenantInfo == null) {
+            throw new IllegalArgumentException("租户不存在或已禁用,tenantId=" + tenantId);
+        }
+        executeWithTenant(tenantInfo, action);
+    }
+
+    /**
+     * 在指定租户数据源下执行操作(消费租户信息,无返回值)
+     *
+     * @param tenantInfo 租户信息
+     * @param action     要执行的操作,接收租户信息作为参数
+     */
+    public void executeWithTenant(TenantInfo tenantInfo, Consumer<TenantInfo> action) {
+        try {
+            // 切换到租户数据源
+            tenantDataSourceManager.switchTenant(tenantInfo);
+            // 切换Redis租户上下文
+            RedisTenantContext.setTenantId(tenantInfo.getId());
+            log.debug("[TenantDS] 已切换到租户数据源和Redis: tenantId={}, tenantCode={}",
+                    tenantInfo.getId(), tenantInfo.getTenantCode());
+
+            // 执行操作
+            action.accept(tenantInfo);
+
+        } finally {
+            // 清理数据源上下文
+            tenantDataSourceManager.clear();
+            // 清理Redis租户上下文
+            RedisTenantContext.clear();
+            log.debug("[TenantDS] 已清理租户数据源和Redis上下文");
+        }
+    }
+
+    /**
+     * 在指定租户数据源下执行操作(消费租户信息,带返回值)
+     *
+     * @param tenantId 租户ID
+     * @param action   要执行的操作,接收租户信息并返回结果
+     * @param <R>      返回值类型
+     * @return 操作结果
+     */
+    public <R> R executeWithTenantAndResult(Long tenantId, Function<TenantInfo, R> action) {
+        TenantInfo tenantInfo = getTenantInfo(tenantId);
+        if (tenantInfo == null) {
+            throw new IllegalArgumentException("租户不存在或已禁用,tenantId=" + tenantId);
+        }
+        return executeWithTenantAndResult(tenantInfo, action);
+    }
+
+    /**
+     * 在指定租户数据源下执行操作(消费租户信息,带返回值)
+     *
+     * @param tenantInfo 租户信息
+     * @param action     要执行的操作,接收租户信息并返回结果
+     * @param <R>        返回值类型
+     * @return 操作结果
+     */
+    public <R> R executeWithTenantAndResult(TenantInfo tenantInfo, Function<TenantInfo, R> action) {
+        try {
+            // 切换到租户数据源
+            tenantDataSourceManager.switchTenant(tenantInfo);
+            // 切换Redis租户上下文
+            RedisTenantContext.setTenantId(tenantInfo.getId());
+            log.debug("[TenantDS] 已切换到租户数据源和Redis: tenantId={}, tenantCode={}",
+                    tenantInfo.getId(), tenantInfo.getTenantCode());
+
+            // 执行操作并返回结果
+            return action.apply(tenantInfo);
+
+        } finally {
+            // 清理数据源上下文
+            tenantDataSourceManager.clear();
+            // 清理Redis租户上下文
+            RedisTenantContext.clear();
+            log.debug("[TenantDS] 已清理租户数据源和Redis上下文");
+        }
+    }
+
+    /**
+     * 获取租户信息
+     *
+     * @param tenantId 租户ID
+     * @return 租户信息,不存在或已禁用则返回null
+     */
+    private TenantInfo getTenantInfo(Long tenantId) {
+        TenantInfo tenantInfo = tenantInfoService.getById(tenantId);
+        if (tenantInfo == null) {
+            log.warn("[TenantDS] 租户不存在,tenantId={}", tenantId);
+            return null;
+        }
+        if (!Integer.valueOf(1).equals(tenantInfo.getStatus())) {
+            log.warn("[TenantDS] 租户已禁用,tenantId={}", tenantId);
+            return null;
+        }
+        return tenantInfo;
+    }
+}

+ 2 - 11
fs-qw-api/pom.xml

@@ -105,12 +105,12 @@
 
     <build>
         <plugins>
-            <!--<plugin>
+            <plugin>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-maven-plugin</artifactId>
                 <version>2.1.1.RELEASE</version>
                 <configuration>
-                    <fork>true</fork> &lt;!&ndash; 如果没有该配置,devtools不会生效 &ndash;&gt;
+                    <fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
                 </configuration>
                 <executions>
                     <execution>
@@ -119,15 +119,6 @@
                         </goals>
                     </execution>
                 </executions>
-            </plugin>-->
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-compiler-plugin</artifactId>
-                <configuration>
-                    <source>${java.version}</source>
-                    <target>${java.version}</target>
-                    <encoding>${project.build.sourceEncoding}</encoding>
-                </configuration>
             </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>

+ 1 - 1
fs-service/src/main/java/com/fs/qw/service/impl/QwUserServiceImpl.java

@@ -1319,7 +1319,7 @@ public class QwUserServiceImpl implements IQwUserService
         WxWorkSetCallbackUrlDTO wxWorkSetCallbackUrlDTO = new WxWorkSetCallbackUrlDTO();
 
         System.out.println("回调地址"+"http://saasqwapimsg.ylrzcloud.com/msg/callback/"+serverId + "/"+loginParam.getTenantId());
-        wxWorkSetCallbackUrlDTO.setUrl("http://saasqwapimsg.ylrzcloud.com/msg/callback/"+serverId+ "/"+loginParam.getTenantId());
+        wxWorkSetCallbackUrlDTO.setUrl("http://cn-hk-bgp-4.ofalias.net:55081/msg/callback/"+serverId+ "/"+loginParam.getTenantId());
         wxWorkSetCallbackUrlDTO.setUuid(data.getUuid());
         wxWorkService.SetCallbackUrl(wxWorkSetCallbackUrlDTO,serverId);