This commit is contained in:
77
2024-05-07 00:31:01 +08:00
parent f676e778cc
commit e421472e5d
406 changed files with 3042 additions and 1758 deletions

View File

@@ -0,0 +1,32 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-component</artifactId>
<version>4.8.2</version>
</parent>
<artifactId>ruoyi-component-tenant</artifactId>
<dependencies>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-component-redis</artifactId>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-component-satoken</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-component-mybatis</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,100 @@
package com.ruoyi.component.tenant.config;
import cn.dev33.satoken.dao.SaTokenDao;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.ruoyi.component.core.util.reflect.ReflectUtils;
import com.ruoyi.component.mybatis.config.MybatisPlusConfig;
import com.ruoyi.component.redis.config.RedisConfig;
import com.ruoyi.component.redis.properties.RedissonProperties;
import com.ruoyi.component.tenant.core.TenantSaTokenDao;
import com.ruoyi.component.tenant.handle.PlusTenantLineHandler;
import com.ruoyi.component.tenant.handle.TenantKeyPrefixHandler;
import com.ruoyi.component.tenant.manager.TenantSpringCacheManager;
import com.ruoyi.component.tenant.properties.TenantProperties;
import org.redisson.config.ClusterServersConfig;
import org.redisson.config.SingleServerConfig;
import org.redisson.spring.starter.RedissonAutoConfigurationCustomizer;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import java.util.ArrayList;
import java.util.List;
/**
* 租户配置类
*
* @author Lion Li
*/
@EnableConfigurationProperties(TenantProperties.class)
@AutoConfiguration(after = {RedisConfig.class, MybatisPlusConfig.class})
@ConditionalOnProperty(value = "tenant.enable", havingValue = "true")
public class TenantConfig {
/**
* 初始化租户配置
*/
@Bean
public boolean tenantInit(MybatisPlusInterceptor mybatisPlusInterceptor,
TenantProperties tenantProperties) {
List<InnerInterceptor> interceptors = new ArrayList<>();
// 多租户插件 必须放到第一位
interceptors.add(tenantLineInnerInterceptor(tenantProperties));
interceptors.addAll(mybatisPlusInterceptor.getInterceptors());
mybatisPlusInterceptor.setInterceptors(interceptors);
return true;
}
/**
* 多租户插件
*/
public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties tenantProperties) {
return new TenantLineInnerInterceptor(new PlusTenantLineHandler(tenantProperties));
}
@Bean
public RedissonAutoConfigurationCustomizer tenantRedissonCustomizer(RedissonProperties redissonProperties) {
return config -> {
TenantKeyPrefixHandler nameMapper = new TenantKeyPrefixHandler(redissonProperties.getKeyPrefix());
SingleServerConfig singleServerConfig = ReflectUtils.invokeGetter(config, "singleServerConfig");
if (ObjectUtil.isNotNull(singleServerConfig)) {
// 使用单机模式
// 设置多租户 redis key前缀
singleServerConfig.setNameMapper(nameMapper);
ReflectUtils.invokeSetter(config, "singleServerConfig", singleServerConfig);
}
ClusterServersConfig clusterServersConfig = ReflectUtils.invokeGetter(config, "clusterServersConfig");
// 集群配置方式 参考下方注释
if (ObjectUtil.isNotNull(clusterServersConfig)) {
// 设置多租户 redis key前缀
clusterServersConfig.setNameMapper(nameMapper);
ReflectUtils.invokeSetter(config, "clusterServersConfig", clusterServersConfig);
}
};
}
/**
* 多租户缓存管理器
*/
@Primary
@Bean
public CacheManager tenantCacheManager() {
return new TenantSpringCacheManager();
}
/**
* 多租户鉴权dao实现
*/
@Primary
@Bean
public SaTokenDao tenantSaTokenDao() {
return new TenantSaTokenDao();
}
}

View File

@@ -0,0 +1,21 @@
package com.ruoyi.component.tenant.core;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 租户基类
*
* @author Michelle.Chung
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class TenantEntity extends BaseEntity {
/**
* 租户编号
*/
private String tenantId;
}

View File

@@ -0,0 +1,148 @@
package com.ruoyi.component.tenant.core;
import com.ruoyi.component.core.constant.GlobalConstants;
import com.ruoyi.component.redis.util.RedisUtils;
import com.ruoyi.component.satoken.core.PlusSaTokenDao;
import java.time.Duration;
import java.util.List;
/**
* SaToken 认证数据持久层 适配多租户
*
* @author Lion Li
*/
public class TenantSaTokenDao extends PlusSaTokenDao {
@Override
public String get(String key) {
return super.get(GlobalConstants.GLOBAL_REDIS_KEY + key);
}
@Override
public void set(String key, String value, long timeout) {
super.set(GlobalConstants.GLOBAL_REDIS_KEY + key, value, timeout);
}
/**
* 修修改指定key-value键值对 (过期时间不变)
*/
@Override
public void update(String key, String value) {
long expire = getTimeout(key);
// -2 = 无此键
if (expire == NOT_VALUE_EXPIRE) {
return;
}
this.set(key, value, expire);
}
/**
* 删除Value
*/
@Override
public void delete(String key) {
super.delete(GlobalConstants.GLOBAL_REDIS_KEY + key);
}
/**
* 获取Value的剩余存活时间 (单位: 秒)
*/
@Override
public long getTimeout(String key) {
return super.getTimeout(GlobalConstants.GLOBAL_REDIS_KEY + key);
}
/**
* 修改Value的剩余存活时间 (单位: 秒)
*/
@Override
public void updateTimeout(String key, long timeout) {
// 判断是否想要设置为永久
if (timeout == NEVER_EXPIRE) {
long expire = getTimeout(key);
if (expire == NEVER_EXPIRE) {
// 如果其已经被设置为永久,则不作任何处理
} else {
// 如果尚未被设置为永久那么再次set一次
this.set(key, this.get(key), timeout);
}
return;
}
RedisUtils.expire(GlobalConstants.GLOBAL_REDIS_KEY + key, Duration.ofSeconds(timeout));
}
/**
* 获取Object如无返空
*/
@Override
public Object getObject(String key) {
return super.getObject(GlobalConstants.GLOBAL_REDIS_KEY + key);
}
/**
* 写入Object并设定存活时间 (单位: 秒)
*/
@Override
public void setObject(String key, Object object, long timeout) {
super.setObject(GlobalConstants.GLOBAL_REDIS_KEY + key, object, timeout);
}
/**
* 更新Object (过期时间不变)
*/
@Override
public void updateObject(String key, Object object) {
long expire = getObjectTimeout(key);
// -2 = 无此键
if (expire == NOT_VALUE_EXPIRE) {
return;
}
this.setObject(key, object, expire);
}
/**
* 删除Object
*/
@Override
public void deleteObject(String key) {
super.deleteObject(GlobalConstants.GLOBAL_REDIS_KEY + key);
}
/**
* 获取Object的剩余存活时间 (单位: 秒)
*/
@Override
public long getObjectTimeout(String key) {
return super.getObjectTimeout(GlobalConstants.GLOBAL_REDIS_KEY + key);
}
/**
* 修改Object的剩余存活时间 (单位: 秒)
*/
@Override
public void updateObjectTimeout(String key, long timeout) {
// 判断是否想要设置为永久
if (timeout == NEVER_EXPIRE) {
long expire = getObjectTimeout(key);
if (expire == NEVER_EXPIRE) {
// 如果其已经被设置为永久,则不作任何处理
} else {
// 如果尚未被设置为永久那么再次set一次
this.setObject(key, this.getObject(key), timeout);
}
return;
}
RedisUtils.expire(GlobalConstants.GLOBAL_REDIS_KEY + key, Duration.ofSeconds(timeout));
}
/**
* 搜索数据
*/
@Override
public List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {
return super.searchData(GlobalConstants.GLOBAL_REDIS_KEY + prefix, keyword, start, size, sortType);
}
}

View File

@@ -0,0 +1,22 @@
package com.ruoyi.component.tenant.exception;
import com.ruoyi.component.core.exception.base.BaseException;
import java.io.Serializable;
/**
* 租户异常类
*
* @author Lion
*/
public class TenantException extends BaseException implements Serializable {
private static final long serialVersionUID = 1L;
public TenantException(String code, Object... args) {
super("tenant", code, args, null);
}
}

View File

@@ -0,0 +1,56 @@
package com.ruoyi.component.tenant.handle;
import cn.hutool.core.collection.ListUtil;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.ruoyi.component.core.util.StringUtils;
import com.ruoyi.component.tenant.helper.TenantHelper;
import com.ruoyi.component.tenant.properties.TenantProperties;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.NullValue;
import net.sf.jsqlparser.expression.StringValue;
import java.util.List;
/**
* 自定义租户处理器
*
* @author Lion Li
*/
@Slf4j
@AllArgsConstructor
public class PlusTenantLineHandler implements TenantLineHandler {
private final TenantProperties tenantProperties;
@Override
public Expression getTenantId() {
String tenantId = TenantHelper.getTenantId();
if (StringUtils.isBlank(tenantId)) {
log.error("无法获取有效的租户id -> Null");
return new NullValue();
}
// 返回固定租户
return new StringValue(tenantId);
}
@Override
public boolean ignoreTable(String tableName) {
String tenantId = TenantHelper.getTenantId();
// 判断是否有租户
if (StringUtils.isNotBlank(tenantId)) {
// 不需要过滤租户的表
List<String> excludes = tenantProperties.getExcludes();
// 非业务表
List<String> tables = ListUtil.toList(
"gen_table",
"gen_table_column"
);
tables.addAll(excludes);
return tables.contains(tableName);
}
return true;
}
}

View File

@@ -0,0 +1,66 @@
package com.ruoyi.component.tenant.handle;
import com.ruoyi.component.core.constant.GlobalConstants;
import com.ruoyi.component.core.util.StringUtils;
import com.ruoyi.component.redis.handler.KeyPrefixHandler;
import com.ruoyi.component.tenant.helper.TenantHelper;
import lombok.extern.slf4j.Slf4j;
/**
* 多租户redis缓存key前缀处理
*
* @author Lion Li
*/
@Slf4j
public class TenantKeyPrefixHandler extends KeyPrefixHandler {
public TenantKeyPrefixHandler(String keyPrefix) {
super(keyPrefix);
}
/**
* 增加前缀
*/
@Override
public String map(String name) {
if (StringUtils.isBlank(name)) {
return null;
}
if (StringUtils.contains(name, GlobalConstants.GLOBAL_REDIS_KEY)) {
return super.map(name);
}
String tenantId = TenantHelper.getTenantId();
if (StringUtils.isBlank(tenantId)) {
log.error("无法获取有效的租户id -> Null");
}
if (StringUtils.startsWith(name, tenantId + "")) {
// 如果存在则直接返回
return super.map(name);
}
return super.map(tenantId + ":" + name);
}
/**
* 去除前缀
*/
@Override
public String unmap(String name) {
String unmap = super.unmap(name);
if (StringUtils.isBlank(unmap)) {
return null;
}
if (StringUtils.contains(name, GlobalConstants.GLOBAL_REDIS_KEY)) {
return super.unmap(name);
}
String tenantId = TenantHelper.getTenantId();
if (StringUtils.isBlank(tenantId)) {
log.error("无法获取有效的租户id -> Null");
}
if (StringUtils.startsWith(unmap, tenantId + "")) {
// 如果存在则删除
return unmap.substring((tenantId + ":").length());
}
return unmap;
}
}

View File

@@ -0,0 +1,189 @@
package com.ruoyi.component.tenant.helper;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.convert.Convert;
import com.alibaba.ttl.TransmittableThreadLocal;
import com.baomidou.mybatisplus.core.plugins.IgnoreStrategy;
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import com.ruoyi.component.satoken.utils.LoginHelper;
import com.ruoyi.component.core.constant.GlobalConstants;
import com.ruoyi.component.core.util.StringUtils;
import com.ruoyi.component.core.util.spring.SpringUtils;
import com.ruoyi.component.redis.util.RedisUtils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.function.Supplier;
/**
* 租户助手
*
* @author Lion Li
*/
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class TenantHelper {
private static final String DYNAMIC_TENANT_KEY = GlobalConstants.GLOBAL_REDIS_KEY + "dynamicTenant";
private static final ThreadLocal<String> TEMP_DYNAMIC_TENANT = new TransmittableThreadLocal<>();
/**
* 租户功能是否启用
*/
public static boolean isEnable() {
return Convert.toBool(SpringUtils.getProperty("tenant.enable"), false);
}
/**
* 开启忽略租户(开启后需手动调用 {@link #disableIgnore()} 关闭)
*/
public static void enableIgnore() {
InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().tenantLine(true).build());
}
/**
* 关闭忽略租户
*/
public static void disableIgnore() {
InterceptorIgnoreHelper.clearIgnoreStrategy();
}
/**
* 在忽略租户中执行
*
* @param handle 处理执行方法
*/
public static void ignore(Runnable handle) {
enableIgnore();
try {
handle.run();
} finally {
disableIgnore();
}
}
/**
* 在忽略租户中执行
*
* @param handle 处理执行方法
*/
public static <T> T ignore(Supplier<T> handle) {
enableIgnore();
try {
return handle.get();
} finally {
disableIgnore();
}
}
/**
* 设置动态租户(一直有效 需要手动清理)
* <p>
* 如果为未登录状态下 那么只在当前线程内生效
*/
public static void setDynamic(String tenantId) {
if (!isEnable()) {
return;
}
if (!isLogin()) {
TEMP_DYNAMIC_TENANT.set(tenantId);
return;
}
String cacheKey = DYNAMIC_TENANT_KEY + ":" + LoginHelper.getUserId();
RedisUtils.setCacheObject(cacheKey, tenantId);
SaHolder.getStorage().set(cacheKey, tenantId);
}
/**
* 获取动态租户(一直有效 需要手动清理)
* <p>
* 如果为未登录状态下 那么只在当前线程内生效
*/
public static String getDynamic() {
if (!isEnable()) {
return null;
}
if (!isLogin()) {
return TEMP_DYNAMIC_TENANT.get();
}
String cacheKey = DYNAMIC_TENANT_KEY + ":" + LoginHelper.getUserId();
String tenantId = (String) SaHolder.getStorage().get(cacheKey);
if (StringUtils.isNotBlank(tenantId)) {
return tenantId;
}
tenantId = RedisUtils.getCacheObject(cacheKey);
SaHolder.getStorage().set(cacheKey, tenantId);
return tenantId;
}
/**
* 清除动态租户
*/
public static void clearDynamic() {
if (!isEnable()) {
return;
}
if (!isLogin()) {
TEMP_DYNAMIC_TENANT.remove();
return;
}
String cacheKey = DYNAMIC_TENANT_KEY + ":" + LoginHelper.getUserId();
RedisUtils.deleteObject(cacheKey);
SaHolder.getStorage().delete(cacheKey);
}
/**
* 在动态租户中执行
*
* @param handle 处理执行方法
*/
public static void dynamic(String tenantId, Runnable handle) {
setDynamic(tenantId);
try {
handle.run();
} finally {
clearDynamic();
}
}
/**
* 在动态租户中执行
*
* @param handle 处理执行方法
*/
public static <T> T dynamic(String tenantId, Supplier<T> handle) {
setDynamic(tenantId);
try {
return handle.get();
} finally {
clearDynamic();
}
}
/**
* 获取当前租户id(动态租户优先)
*/
public static String getTenantId() {
if (!isEnable()) {
return null;
}
String tenantId = TenantHelper.getDynamic();
if (StringUtils.isBlank(tenantId)) {
tenantId = LoginHelper.getTenantId();
}
return tenantId;
}
private static boolean isLogin() {
try {
StpUtil.checkLogin();
return true;
} catch (Exception e) {
return false;
}
}
}

View File

@@ -0,0 +1,32 @@
package com.ruoyi.component.tenant.manager;
import com.ruoyi.component.core.constant.GlobalConstants;
import com.ruoyi.component.core.util.StringUtils;
import com.ruoyi.component.redis.manager.PlusSpringCacheManager;
import com.ruoyi.component.tenant.helper.TenantHelper;
import org.springframework.cache.Cache;
/**
* 重写 cacheName 处理方法 支持多租户
*
* @author Lion Li
*/
public class TenantSpringCacheManager extends PlusSpringCacheManager {
public TenantSpringCacheManager() {
}
@Override
public Cache getCache(String name) {
if (StringUtils.contains(name, GlobalConstants.GLOBAL_REDIS_KEY)) {
return super.getCache(name);
}
String tenantId = TenantHelper.getTenantId();
if (StringUtils.startsWith(name, tenantId)) {
// 如果存在则直接返回
return super.getCache(name);
}
return super.getCache(tenantId + ":" + name);
}
}

View File

@@ -0,0 +1,27 @@
package com.ruoyi.component.tenant.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
/**
* 租户 配置属性
*
* @author Lion Li
*/
@Data
@ConfigurationProperties(prefix = "tenant")
public class TenantProperties {
/**
* 是否启用
*/
private Boolean enable;
/**
* 排除表
*/
private List<String> excludes;
}