初始化 V1

This commit is contained in:
陈博文
2025-06-10 15:04:49 +08:00
parent cbed9f13d1
commit b412a44ec7
1986 changed files with 1774358 additions and 59 deletions

View File

@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>yudao-framework</artifactId>
<groupId>cn.iocoder.cloud</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>多租户</description>
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<dependencies>
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-common</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-security</artifactId>
</dependency>
<!-- DB 相关 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-mybatis</artifactId>
</dependency>
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-redis</artifactId>
</dependency>
<!-- RPC 远程调用相关 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-rpc</artifactId>
<optional>true</optional>
</dependency>
<!-- Job 定时任务相关 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-job</artifactId>
<optional>true</optional>
</dependency>
<!-- 消息队列相关 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-mq</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<optional>true</optional>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 工具类相关 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,57 @@
package cn.iocoder.yudao.framework.tenant.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* 多租户配置
*
* @author 芋道源码
*/
@ConfigurationProperties(prefix = "yudao.tenant")
@Data
public class TenantProperties {
/**
* 租户是否开启
*/
private static final Boolean ENABLE_DEFAULT = true;
/**
* 是否开启
*/
private Boolean enable = ENABLE_DEFAULT;
/**
* 需要忽略多租户的请求
*
* 默认情况下,每个请求需要带上 tenant-id 的请求头。但是,部分请求是无需带上的,例如说短信回调、支付回调等 Open API
*/
private Set<String> ignoreUrls = new HashSet<>();
/**
* 需要忽略跨(切换)租户访问的请求
*
* 原因是:某些接口,访问的是个人信息,在跨租户是获取不到的!
*/
private Set<String> ignoreVisitUrls = Collections.emptySet();
/**
* 需要忽略多租户的表
*
* 即默认所有表都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟
*/
private Set<String> ignoreTables = Collections.emptySet();
/**
* 需要忽略多租户的 Spring Cache 缓存
*
* 即默认所有缓存都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟
*/
private Set<String> ignoreCaches = Collections.emptySet();
}

View File

@@ -0,0 +1,20 @@
package cn.iocoder.yudao.framework.tenant.config;
import cn.iocoder.yudao.framework.tenant.core.rpc.TenantRequestInterceptor;
import cn.iocoder.yudao.framework.common.biz.system.tenant.TenantCommonApi;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
@ConditionalOnProperty(prefix = "yudao.tenant", value = "enable", matchIfMissing = true) // 允许使用 yudao.tenant.enable=false 禁用多租户
@EnableFeignClients(clients = TenantCommonApi.class) // 主要是引入相关的 API 服务
public class YudaoTenantRpcAutoConfiguration {
@Bean
public TenantRequestInterceptor tenantRequestInterceptor() {
return new TenantRequestInterceptor();
}
}

View File

@@ -0,0 +1,32 @@
package cn.iocoder.yudao.framework.tenant.core.aop;
import cn.iocoder.yudao.framework.tenant.config.TenantProperties;
import java.lang.annotation.*;
/**
* 忽略租户,标记指定方法不进行租户的自动过滤
*
* 注意,只有 DB 的场景会过滤,其它场景暂时不过滤:
* 1、Redis 场景:因为是基于 Key 实现多租户的能力,所以忽略没有意义,不像 DB 是一个 column 实现的
* 2、MQ 场景:有点难以抉择,目前可以通过 Consumer 手动在消费的方法上,添加 @TenantIgnore 进行忽略
*
* 特殊:
* 1、如果添加到 Controller 类上,则该 URL 自动添加到 {@link TenantProperties#getIgnoreUrls()} 中
* 2、如果添加到 DO 实体类上,则它对应的表名“相当于”自动添加到 {@link TenantProperties#getIgnoreTables()} 中
*
* @author 芋道源码
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface TenantIgnore {
/**
* 是否开启忽略租户,默认为 true 开启
*
* 支持 Spring EL 表达式,如果返回 true 则满足条件,进行租户的忽略
*/
String enable() default "true";
}

View File

@@ -0,0 +1,41 @@
package cn.iocoder.yudao.framework.tenant.core.aop;
import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
/**
* 忽略多租户的 Aspect基于 {@link TenantIgnore} 注解实现,用于一些全局的逻辑。
* 例如说,一个定时任务,读取所有数据,进行处理。
* 又例如说,读取所有数据,进行缓存。
*
* 整体逻辑的实现,和 {@link TenantUtils#executeIgnore(Runnable)} 需要保持一致
*
* @author 芋道源码
*/
@Aspect
@Slf4j
public class TenantIgnoreAspect {
@Around("@annotation(tenantIgnore)")
public Object around(ProceedingJoinPoint joinPoint, TenantIgnore tenantIgnore) throws Throwable {
Boolean oldIgnore = TenantContextHolder.isIgnore();
try {
// 计算条件,满足的情况下,才进行忽略
Object enable = SpringExpressionUtils.parseExpression(tenantIgnore.enable());
if (Boolean.TRUE.equals(enable)) {
TenantContextHolder.setIgnore(true);
}
// 执行逻辑
return joinPoint.proceed();
} finally {
TenantContextHolder.setIgnore(oldIgnore);
}
}
}

View File

@@ -0,0 +1,69 @@
package cn.iocoder.yudao.framework.tenant.core.context;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.DocumentEnum;
import com.alibaba.ttl.TransmittableThreadLocal;
/**
* 多租户上下文 Holder
*
* @author 芋道源码
*/
public class TenantContextHolder {
/**
* 当前租户编号
*/
private static final ThreadLocal<Long> TENANT_ID = new TransmittableThreadLocal<>();
/**
* 是否忽略租户
*/
private static final ThreadLocal<Boolean> IGNORE = new TransmittableThreadLocal<>();
/**
* 获得租户编号
*
* @return 租户编号
*/
public static Long getTenantId() {
return TENANT_ID.get();
}
/**
* 获得租户编号。如果不存在,则抛出 NullPointerException 异常
*
* @return 租户编号
*/
public static Long getRequiredTenantId() {
Long tenantId = getTenantId();
if (tenantId == null) {
throw new NullPointerException("TenantContextHolder 不存在租户编号!可参考文档:"
+ DocumentEnum.TENANT.getUrl());
}
return tenantId;
}
public static void setTenantId(Long tenantId) {
TENANT_ID.set(tenantId);
}
public static void setIgnore(Boolean ignore) {
IGNORE.set(ignore);
}
/**
* 当前是否忽略租户
*
* @return 是否忽略
*/
public static boolean isIgnore() {
return Boolean.TRUE.equals(IGNORE.get());
}
public static void clear() {
TENANT_ID.remove();
IGNORE.remove();
}
}

View File

@@ -0,0 +1,21 @@
package cn.iocoder.yudao.framework.tenant.core.db;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 拓展多租户的 BaseDO 基类
*
* @author 芋道源码
*/
@Data
@EqualsAndHashCode(callSuper = true)
public abstract class TenantBaseDO extends BaseDO {
/**
* 多租户编号
*/
private Long tenantId;
}

View File

@@ -0,0 +1,83 @@
package cn.iocoder.yudao.framework.tenant.core.db;
import cn.iocoder.yudao.framework.tenant.config.TenantProperties;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.toolkit.SqlParserUtils;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import java.util.HashMap;
import java.util.Map;
/**
* 基于 MyBatis Plus 多租户的功能,实现 DB 层面的多租户的功能
*
* @author 芋道源码
*/
public class TenantDatabaseInterceptor implements TenantLineHandler {
/**
* 忽略的表
*
* KEY表名
* VALUE是否忽略
*/
private final Map<String, Boolean> ignoreTables = new HashMap<>();
public TenantDatabaseInterceptor(TenantProperties properties) {
// 不同 DB 下,大小写的习惯不同,所以需要都添加进去
properties.getIgnoreTables().forEach(table -> {
addIgnoreTable(table, true);
});
// 在 OracleKeyGenerator 中,生成主键时,会查询这个表,查询这个表后,会自动拼接 TENANT_ID 导致报错
addIgnoreTable("DUAL", true);
}
@Override
public Expression getTenantId() {
return new LongValue(TenantContextHolder.getRequiredTenantId());
}
@Override
public boolean ignoreTable(String tableName) {
// 情况一,全局忽略多租户
if (TenantContextHolder.isIgnore()) {
return true;
}
// 情况二,忽略多租户的表
tableName = SqlParserUtils.removeWrapperSymbol(tableName);
Boolean ignore = ignoreTables.get(tableName.toLowerCase());
if (ignore == null) {
ignore = computeIgnoreTable(tableName);
synchronized (ignoreTables) {
addIgnoreTable(tableName, ignore);
}
}
return ignore;
}
private void addIgnoreTable(String tableName, boolean ignore) {
ignoreTables.put(tableName.toLowerCase(), ignore);
ignoreTables.put(tableName.toUpperCase(), ignore);
}
private boolean computeIgnoreTable(String tableName) {
// 找不到的表,说明不是 yudao 项目里的,不进行拦截(忽略租户)
TableInfo tableInfo = TableInfoHelper.getTableInfo(tableName);
if (tableInfo == null) {
return true;
}
// 如果继承了 TenantBaseDO 基类,显然不忽略租户
if (TenantBaseDO.class.isAssignableFrom(tableInfo.getEntityType())) {
return false;
}
// 如果添加了 @TenantIgnore 注解,显然也不忽略租户
TenantIgnore tenantIgnore = tableInfo.getEntityType().getAnnotation(TenantIgnore.class);
return tenantIgnore != null;
}
}

View File

@@ -0,0 +1,14 @@
package cn.iocoder.yudao.framework.tenant.core.job;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 多租户 Job 注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TenantJob {
}

Some files were not shown because too many files have changed in this diff Show More