81 changed files with 160 additions and 3162 deletions
@ -1,76 +0,0 @@ |
|||
<?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.boot</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.boot</groupId> |
|||
<artifactId>yudao-common</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Web 相关 --> |
|||
<dependency> |
|||
<groupId>cn.iocoder.boot</groupId> |
|||
<artifactId>yudao-spring-boot-starter-security</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- DB 相关 --> |
|||
<dependency> |
|||
<groupId>cn.iocoder.boot</groupId> |
|||
<artifactId>yudao-spring-boot-starter-mybatis</artifactId> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>cn.iocoder.boot</groupId> |
|||
<artifactId>yudao-spring-boot-starter-redis</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Job 定时任务相关 --> |
|||
<dependency> |
|||
<groupId>cn.iocoder.boot</groupId> |
|||
<artifactId>yudao-spring-boot-starter-job</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- 消息队列相关 --> |
|||
<dependency> |
|||
<groupId>cn.iocoder.boot</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> |
|||
|
|||
<!-- 工具类相关 --> |
|||
<dependency> |
|||
<groupId>com.google.guava</groupId> |
|||
<artifactId>guava</artifactId> |
|||
</dependency> |
|||
</dependencies> |
|||
|
|||
</project> |
|||
@ -1,49 +0,0 @@ |
|||
package cn.iocoder.yudao.framework.tenant.config; |
|||
|
|||
import lombok.Data; |
|||
import org.springframework.boot.context.properties.ConfigurationProperties; |
|||
|
|||
import java.util.Collections; |
|||
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 = Collections.emptySet(); |
|||
|
|||
/** |
|||
* 需要忽略多租户的表 |
|||
* |
|||
* 即默认所有表都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟 |
|||
*/ |
|||
private Set<String> ignoreTables = Collections.emptySet(); |
|||
|
|||
/** |
|||
* 需要忽略多租户的 Spring Cache 缓存 |
|||
* |
|||
* 即默认所有缓存都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟 |
|||
*/ |
|||
private Set<String> ignoreCaches = Collections.emptySet(); |
|||
|
|||
} |
|||
@ -1,133 +0,0 @@ |
|||
package cn.iocoder.yudao.framework.tenant.config; |
|||
|
|||
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; |
|||
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; |
|||
import cn.iocoder.yudao.framework.redis.config.YudaoCacheProperties; |
|||
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnoreAspect; |
|||
import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor; |
|||
import cn.iocoder.yudao.framework.tenant.core.job.TenantJobAspect; |
|||
import cn.iocoder.yudao.framework.tenant.core.mq.rabbitmq.TenantRabbitMQInitializer; |
|||
import cn.iocoder.yudao.framework.tenant.core.mq.redis.TenantRedisMessageInterceptor; |
|||
import cn.iocoder.yudao.framework.tenant.core.mq.rocketmq.TenantRocketMQInitializer; |
|||
import cn.iocoder.yudao.framework.tenant.core.redis.TenantRedisCacheManager; |
|||
import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter; |
|||
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService; |
|||
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkServiceImpl; |
|||
import cn.iocoder.yudao.framework.tenant.core.web.TenantContextWebFilter; |
|||
import cn.iocoder.yudao.framework.web.config.WebProperties; |
|||
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; |
|||
import cn.iocoder.yudao.module.system.api.tenant.TenantApi; |
|||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; |
|||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; |
|||
import org.springframework.boot.autoconfigure.AutoConfiguration; |
|||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; |
|||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
|||
import org.springframework.boot.context.properties.EnableConfigurationProperties; |
|||
import org.springframework.boot.web.servlet.FilterRegistrationBean; |
|||
import org.springframework.context.annotation.Bean; |
|||
import org.springframework.context.annotation.Primary; |
|||
import org.springframework.data.redis.cache.BatchStrategies; |
|||
import org.springframework.data.redis.cache.RedisCacheConfiguration; |
|||
import org.springframework.data.redis.cache.RedisCacheManager; |
|||
import org.springframework.data.redis.cache.RedisCacheWriter; |
|||
import org.springframework.data.redis.connection.RedisConnectionFactory; |
|||
import org.springframework.data.redis.core.RedisTemplate; |
|||
|
|||
import java.util.Objects; |
|||
|
|||
@AutoConfiguration |
|||
@ConditionalOnProperty(prefix = "yudao.tenant", value = "enable", matchIfMissing = true) // 允许使用 yudao.tenant.enable=false 禁用多租户
|
|||
@EnableConfigurationProperties(TenantProperties.class) |
|||
public class YudaoTenantAutoConfiguration { |
|||
|
|||
@Bean |
|||
public TenantFrameworkService tenantFrameworkService(TenantApi tenantApi) { |
|||
return new TenantFrameworkServiceImpl(tenantApi); |
|||
} |
|||
|
|||
// ========== AOP ==========
|
|||
|
|||
@Bean |
|||
public TenantIgnoreAspect tenantIgnoreAspect() { |
|||
return new TenantIgnoreAspect(); |
|||
} |
|||
|
|||
// ========== DB ==========
|
|||
|
|||
@Bean |
|||
public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties properties, |
|||
MybatisPlusInterceptor interceptor) { |
|||
TenantLineInnerInterceptor inner = new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(properties)); |
|||
// 添加到 interceptor 中
|
|||
// 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定
|
|||
MyBatisUtils.addInterceptor(interceptor, inner, 0); |
|||
return inner; |
|||
} |
|||
|
|||
// ========== WEB ==========
|
|||
|
|||
@Bean |
|||
public FilterRegistrationBean<TenantContextWebFilter> tenantContextWebFilter() { |
|||
FilterRegistrationBean<TenantContextWebFilter> registrationBean = new FilterRegistrationBean<>(); |
|||
registrationBean.setFilter(new TenantContextWebFilter()); |
|||
registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER); |
|||
return registrationBean; |
|||
} |
|||
|
|||
// ========== Security ==========
|
|||
|
|||
@Bean |
|||
public FilterRegistrationBean<TenantSecurityWebFilter> tenantSecurityWebFilter(TenantProperties tenantProperties, |
|||
WebProperties webProperties, |
|||
GlobalExceptionHandler globalExceptionHandler, |
|||
TenantFrameworkService tenantFrameworkService) { |
|||
FilterRegistrationBean<TenantSecurityWebFilter> registrationBean = new FilterRegistrationBean<>(); |
|||
registrationBean.setFilter(new TenantSecurityWebFilter(tenantProperties, webProperties, |
|||
globalExceptionHandler, tenantFrameworkService)); |
|||
registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER); |
|||
return registrationBean; |
|||
} |
|||
|
|||
// ========== MQ ==========
|
|||
|
|||
@Bean |
|||
public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() { |
|||
return new TenantRedisMessageInterceptor(); |
|||
} |
|||
|
|||
@Bean |
|||
@ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate") |
|||
public TenantRabbitMQInitializer tenantRabbitMQInitializer() { |
|||
return new TenantRabbitMQInitializer(); |
|||
} |
|||
|
|||
@Bean |
|||
@ConditionalOnClass(name = "org.apache.rocketmq.spring.core.RocketMQTemplate") |
|||
public TenantRocketMQInitializer tenantRocketMQInitializer() { |
|||
return new TenantRocketMQInitializer(); |
|||
} |
|||
|
|||
// ========== Job ==========
|
|||
|
|||
@Bean |
|||
public TenantJobAspect tenantJobAspect(TenantFrameworkService tenantFrameworkService) { |
|||
return new TenantJobAspect(tenantFrameworkService); |
|||
} |
|||
|
|||
// ========== Redis ==========
|
|||
|
|||
@Bean |
|||
@Primary // 引入租户时,tenantRedisCacheManager 为主 Bean
|
|||
public RedisCacheManager tenantRedisCacheManager(RedisTemplate<String, Object> redisTemplate, |
|||
RedisCacheConfiguration redisCacheConfiguration, |
|||
YudaoCacheProperties yudaoCacheProperties, |
|||
TenantProperties tenantProperties) { |
|||
// 创建 RedisCacheWriter 对象
|
|||
RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory()); |
|||
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, |
|||
BatchStrategies.scan(yudaoCacheProperties.getRedisScanBatchSize())); |
|||
// 创建 TenantRedisCacheManager 对象
|
|||
return new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration, tenantProperties.getIgnoreCaches()); |
|||
} |
|||
|
|||
} |
|||
@ -1,18 +0,0 @@ |
|||
package cn.iocoder.yudao.framework.tenant.core.aop; |
|||
|
|||
import java.lang.annotation.*; |
|||
|
|||
/** |
|||
* 忽略租户,标记指定方法不进行租户的自动过滤 |
|||
* |
|||
* 注意,只有 DB 的场景会过滤,其它场景暂时不过滤: |
|||
* 1、Redis 场景:因为是基于 Key 实现多租户的能力,所以忽略没有意义,不像 DB 是一个 column 实现的 |
|||
* 2、MQ 场景:有点难以抉择,目前可以通过 Consumer 手动在消费的方法上,添加 @TenantIgnore 进行忽略 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Target({ElementType.METHOD}) |
|||
@Retention(RetentionPolicy.RUNTIME) |
|||
@Inherited |
|||
public @interface TenantIgnore { |
|||
} |
|||
@ -1,35 +0,0 @@ |
|||
package cn.iocoder.yudao.framework.tenant.core.aop; |
|||
|
|||
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 { |
|||
TenantContextHolder.setIgnore(true); |
|||
// 执行逻辑
|
|||
return joinPoint.proceed(); |
|||
} finally { |
|||
TenantContextHolder.setIgnore(oldIgnore); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -1,68 +0,0 @@ |
|||
package cn.iocoder.yudao.framework.tenant.core.context; |
|||
|
|||
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(); |
|||
} |
|||
|
|||
} |
|||
@ -1,21 +0,0 @@ |
|||
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; |
|||
|
|||
} |
|||
@ -1,44 +0,0 @@ |
|||
package cn.iocoder.yudao.framework.tenant.core.db; |
|||
|
|||
import cn.hutool.core.collection.CollUtil; |
|||
import cn.iocoder.yudao.framework.tenant.config.TenantProperties; |
|||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; |
|||
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.HashSet; |
|||
import java.util.Set; |
|||
|
|||
/** |
|||
* 基于 MyBatis Plus 多租户的功能,实现 DB 层面的多租户的功能 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
public class TenantDatabaseInterceptor implements TenantLineHandler { |
|||
|
|||
private final Set<String> ignoreTables = new HashSet<>(); |
|||
|
|||
public TenantDatabaseInterceptor(TenantProperties properties) { |
|||
// 不同 DB 下,大小写的习惯不同,所以需要都添加进去
|
|||
properties.getIgnoreTables().forEach(table -> { |
|||
ignoreTables.add(table.toLowerCase()); |
|||
ignoreTables.add(table.toUpperCase()); |
|||
}); |
|||
// 在 OracleKeyGenerator 中,生成主键时,会查询这个表,查询这个表后,会自动拼接 TENANT_ID 导致报错
|
|||
ignoreTables.add("DUAL"); |
|||
} |
|||
|
|||
@Override |
|||
public Expression getTenantId() { |
|||
return new LongValue(TenantContextHolder.getRequiredTenantId()); |
|||
} |
|||
|
|||
@Override |
|||
public boolean ignoreTable(String tableName) { |
|||
return TenantContextHolder.isIgnore() // 情况一,全局忽略多租户
|
|||
|| CollUtil.contains(ignoreTables, SqlParserUtils.removeWrapperSymbol(tableName)); // 情况二,忽略多租户的表
|
|||
} |
|||
|
|||
} |
|||
@ -1,14 +0,0 @@ |
|||
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 { |
|||
} |
|||
@ -1,59 +0,0 @@ |
|||
package cn.iocoder.yudao.framework.tenant.core.job; |
|||
|
|||
import cn.hutool.core.collection.CollUtil; |
|||
import cn.hutool.core.exceptions.ExceptionUtil; |
|||
import cn.hutool.core.util.StrUtil; |
|||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
|||
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService; |
|||
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.aspectj.lang.ProceedingJoinPoint; |
|||
import org.aspectj.lang.annotation.Around; |
|||
import org.aspectj.lang.annotation.Aspect; |
|||
|
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
|
|||
/** |
|||
* 多租户 JobHandler AOP |
|||
* 任务执行时,会按照租户逐个执行 Job 的逻辑 |
|||
* |
|||
* 注意,需要保证 JobHandler 的幂等性。因为 Job 因为某个租户执行失败重试时,之前执行成功的租户也会再次执行。 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Aspect |
|||
@RequiredArgsConstructor |
|||
@Slf4j |
|||
public class TenantJobAspect { |
|||
|
|||
private final TenantFrameworkService tenantFrameworkService; |
|||
|
|||
@Around("@annotation(tenantJob)") |
|||
public String around(ProceedingJoinPoint joinPoint, TenantJob tenantJob) { |
|||
// 获得租户列表
|
|||
List<Long> tenantIds = tenantFrameworkService.getTenantIds(); |
|||
if (CollUtil.isEmpty(tenantIds)) { |
|||
return null; |
|||
} |
|||
|
|||
// 逐个租户,执行 Job
|
|||
Map<Long, String> results = new ConcurrentHashMap<>(); |
|||
tenantIds.parallelStream().forEach(tenantId -> { |
|||
// TODO 芋艿:先通过 parallel 实现并行;1)多个租户,是一条执行日志;2)异常的情况
|
|||
TenantUtils.execute(tenantId, () -> { |
|||
try { |
|||
Object result = joinPoint.proceed(); |
|||
results.put(tenantId, StrUtil.toStringOrEmpty(result)); |
|||
} catch (Throwable e) { |
|||
log.error("[execute][租户({}) 执行 Job 发生异常", tenantId, e); |
|||
results.put(tenantId, ExceptionUtil.getRootCauseMessage(e)); |
|||
} |
|||
}); |
|||
}); |
|||
return JsonUtils.toJsonString(results); |
|||
} |
|||
|
|||
} |
|||
@ -1,37 +0,0 @@ |
|||
package cn.iocoder.yudao.framework.tenant.core.mq.kafka; |
|||
|
|||
import cn.hutool.core.util.StrUtil; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.boot.SpringApplication; |
|||
import org.springframework.boot.env.EnvironmentPostProcessor; |
|||
import org.springframework.core.env.ConfigurableEnvironment; |
|||
|
|||
/** |
|||
* 多租户的 Kafka 的 {@link EnvironmentPostProcessor} 实现类 |
|||
* |
|||
* Kafka Producer 发送消息时,增加 {@link TenantKafkaProducerInterceptor} 拦截器 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Slf4j |
|||
public class TenantKafkaEnvironmentPostProcessor implements EnvironmentPostProcessor { |
|||
|
|||
private static final String PROPERTY_KEY_INTERCEPTOR_CLASSES = "spring.kafka.producer.properties.interceptor.classes"; |
|||
|
|||
@Override |
|||
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { |
|||
// 添加 TenantKafkaProducerInterceptor 拦截器
|
|||
try { |
|||
String value = environment.getProperty(PROPERTY_KEY_INTERCEPTOR_CLASSES); |
|||
if (StrUtil.isEmpty(value)) { |
|||
value = TenantKafkaProducerInterceptor.class.getName(); |
|||
} else { |
|||
value += "," + TenantKafkaProducerInterceptor.class.getName(); |
|||
} |
|||
environment.getSystemProperties().put(PROPERTY_KEY_INTERCEPTOR_CLASSES, value); |
|||
} catch (NoClassDefFoundError ignore) { |
|||
// 如果触发 NoClassDefFoundError 异常,说明 TenantKafkaProducerInterceptor 类不存在,即没引入 kafka-spring 依赖
|
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -1,47 +0,0 @@ |
|||
package cn.iocoder.yudao.framework.tenant.core.mq.kafka; |
|||
|
|||
import cn.hutool.core.util.ReflectUtil; |
|||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; |
|||
import org.apache.kafka.clients.producer.ProducerInterceptor; |
|||
import org.apache.kafka.clients.producer.ProducerRecord; |
|||
import org.apache.kafka.clients.producer.RecordMetadata; |
|||
import org.apache.kafka.common.header.Headers; |
|||
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; |
|||
|
|||
import java.util.Map; |
|||
|
|||
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; |
|||
|
|||
/** |
|||
* Kafka 消息队列的多租户 {@link ProducerInterceptor} 实现类 |
|||
* |
|||
* 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 |
|||
* 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
public class TenantKafkaProducerInterceptor implements ProducerInterceptor<Object, Object> { |
|||
|
|||
@Override |
|||
public ProducerRecord<Object, Object> onSend(ProducerRecord<Object, Object> record) { |
|||
Long tenantId = TenantContextHolder.getTenantId(); |
|||
if (tenantId != null) { |
|||
Headers headers = (Headers) ReflectUtil.getFieldValue(record, "headers"); // private 属性,没有 get 方法,智能反射
|
|||
headers.add(HEADER_TENANT_ID, tenantId.toString().getBytes()); |
|||
} |
|||
return record; |
|||
} |
|||
|
|||
@Override |
|||
public void onAcknowledgement(RecordMetadata metadata, Exception exception) { |
|||
} |
|||
|
|||
@Override |
|||
public void close() { |
|||
} |
|||
|
|||
@Override |
|||
public void configure(Map<String, ?> configs) { |
|||
} |
|||
|
|||
} |
|||
@ -1,23 +0,0 @@ |
|||
package cn.iocoder.yudao.framework.tenant.core.mq.rabbitmq; |
|||
|
|||
import org.springframework.amqp.rabbit.core.RabbitTemplate; |
|||
import org.springframework.beans.BeansException; |
|||
import org.springframework.beans.factory.config.BeanPostProcessor; |
|||
|
|||
/** |
|||
* 多租户的 RabbitMQ 初始化器 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
public class TenantRabbitMQInitializer implements BeanPostProcessor { |
|||
|
|||
@Override |
|||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { |
|||
if (bean instanceof RabbitTemplate) { |
|||
RabbitTemplate rabbitTemplate = (RabbitTemplate) bean; |
|||
rabbitTemplate.addBeforePublishPostProcessors(new TenantRabbitMQMessagePostProcessor()); |
|||
} |
|||
return bean; |
|||
} |
|||
|
|||
} |
|||
@ -1,31 +0,0 @@ |
|||
package cn.iocoder.yudao.framework.tenant.core.mq.rabbitmq; |
|||
|
|||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; |
|||
import org.apache.kafka.clients.producer.ProducerInterceptor; |
|||
import org.springframework.amqp.AmqpException; |
|||
import org.springframework.amqp.core.Message; |
|||
import org.springframework.amqp.core.MessagePostProcessor; |
|||
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; |
|||
|
|||
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; |
|||
|
|||
/** |
|||
* RabbitMQ 消息队列的多租户 {@link ProducerInterceptor} 实现类 |
|||
* |
|||
* 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 |
|||
* 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
public class TenantRabbitMQMessagePostProcessor implements MessagePostProcessor { |
|||
|
|||
@Override |
|||
public Message postProcessMessage(Message message) throws AmqpException { |
|||
Long tenantId = TenantContextHolder.getTenantId(); |
|||
if (tenantId != null) { |
|||
message.getMessageProperties().getHeaders().put(HEADER_TENANT_ID, tenantId); |
|||
} |
|||
return message; |
|||
} |
|||
|
|||
} |
|||
@ -1,42 +0,0 @@ |
|||
package cn.iocoder.yudao.framework.tenant.core.mq.redis; |
|||
|
|||
import cn.hutool.core.util.StrUtil; |
|||
import cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor; |
|||
import cn.iocoder.yudao.framework.mq.redis.core.message.AbstractRedisMessage; |
|||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; |
|||
|
|||
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; |
|||
|
|||
/** |
|||
* 多租户 {@link AbstractRedisMessage} 拦截器 |
|||
* |
|||
* 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 |
|||
* 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
public class TenantRedisMessageInterceptor implements RedisMessageInterceptor { |
|||
|
|||
@Override |
|||
public void sendMessageBefore(AbstractRedisMessage message) { |
|||
Long tenantId = TenantContextHolder.getTenantId(); |
|||
if (tenantId != null) { |
|||
message.addHeader(HEADER_TENANT_ID, tenantId.toString()); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void consumeMessageBefore(AbstractRedisMessage message) { |
|||
String tenantIdStr = message.getHeader(HEADER_TENANT_ID); |
|||
if (StrUtil.isNotEmpty(tenantIdStr)) { |
|||
TenantContextHolder.setTenantId(Long.valueOf(tenantIdStr)); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void consumeMessageAfter(AbstractRedisMessage message) { |
|||
// 注意,Consumer 是一个逻辑的入口,所以不考虑原本上下文就存在租户编号的情况
|
|||
TenantContextHolder.clear(); |
|||
} |
|||
|
|||
} |
|||
@ -1,46 +0,0 @@ |
|||
package cn.iocoder.yudao.framework.tenant.core.mq.rocketmq; |
|||
|
|||
import cn.hutool.core.lang.Assert; |
|||
import cn.hutool.core.util.StrUtil; |
|||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; |
|||
import org.apache.rocketmq.client.hook.ConsumeMessageContext; |
|||
import org.apache.rocketmq.client.hook.ConsumeMessageHook; |
|||
import org.apache.rocketmq.common.message.MessageExt; |
|||
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; |
|||
|
|||
import java.util.List; |
|||
|
|||
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; |
|||
|
|||
/** |
|||
* RocketMQ 消息队列的多租户 {@link ConsumeMessageHook} 实现类 |
|||
* |
|||
* Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
public class TenantRocketMQConsumeMessageHook implements ConsumeMessageHook { |
|||
|
|||
@Override |
|||
public String hookName() { |
|||
return getClass().getSimpleName(); |
|||
} |
|||
|
|||
@Override |
|||
public void consumeMessageBefore(ConsumeMessageContext context) { |
|||
// 校验,消息必须是单条,不然设置租户可能不正确
|
|||
List<MessageExt> messages = context.getMsgList(); |
|||
Assert.isTrue(messages.size() == 1, "消息条数({})不正确", messages.size()); |
|||
// 设置租户编号
|
|||
String tenantId = messages.get(0).getUserProperty(HEADER_TENANT_ID); |
|||
if (StrUtil.isNotEmpty(tenantId)) { |
|||
TenantContextHolder.setTenantId(Long.parseLong(tenantId)); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void consumeMessageAfter(ConsumeMessageContext context) { |
|||
TenantContextHolder.clear(); |
|||
} |
|||
|
|||
} |
|||
@ -1,53 +0,0 @@ |
|||
package cn.iocoder.yudao.framework.tenant.core.mq.rocketmq; |
|||
|
|||
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; |
|||
import org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl; |
|||
import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl; |
|||
import org.apache.rocketmq.client.producer.DefaultMQProducer; |
|||
import org.apache.rocketmq.spring.core.RocketMQTemplate; |
|||
import org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer; |
|||
import org.springframework.beans.BeansException; |
|||
import org.springframework.beans.factory.config.BeanPostProcessor; |
|||
|
|||
/** |
|||
* 多租户的 RocketMQ 初始化器 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
public class TenantRocketMQInitializer implements BeanPostProcessor { |
|||
|
|||
@Override |
|||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { |
|||
if (bean instanceof DefaultRocketMQListenerContainer) { |
|||
DefaultRocketMQListenerContainer container = (DefaultRocketMQListenerContainer) bean; |
|||
initTenantConsumer(container.getConsumer()); |
|||
} else if (bean instanceof RocketMQTemplate) { |
|||
RocketMQTemplate template = (RocketMQTemplate) bean; |
|||
initTenantProducer(template.getProducer()); |
|||
} |
|||
return bean; |
|||
} |
|||
|
|||
private void initTenantProducer(DefaultMQProducer producer) { |
|||
if (producer == null) { |
|||
return; |
|||
} |
|||
DefaultMQProducerImpl producerImpl = producer.getDefaultMQProducerImpl(); |
|||
if (producerImpl == null) { |
|||
return; |
|||
} |
|||
producerImpl.registerSendMessageHook(new TenantRocketMQSendMessageHook()); |
|||
} |
|||
|
|||
private void initTenantConsumer(DefaultMQPushConsumer consumer) { |
|||
if (consumer == null) { |
|||
return; |
|||
} |
|||
DefaultMQPushConsumerImpl consumerImpl = consumer.getDefaultMQPushConsumerImpl(); |
|||
if (consumerImpl == null) { |
|||
return; |
|||
} |
|||
consumerImpl.registerConsumeMessageHook(new TenantRocketMQConsumeMessageHook()); |
|||
} |
|||
|
|||
} |
|||
@ -1,36 +0,0 @@ |
|||
package cn.iocoder.yudao.framework.tenant.core.mq.rocketmq; |
|||
|
|||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; |
|||
import org.apache.rocketmq.client.hook.SendMessageContext; |
|||
import org.apache.rocketmq.client.hook.SendMessageHook; |
|||
|
|||
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; |
|||
|
|||
/** |
|||
* RocketMQ 消息队列的多租户 {@link SendMessageHook} 实现类 |
|||
* |
|||
* Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
public class TenantRocketMQSendMessageHook implements SendMessageHook { |
|||
|
|||
@Override |
|||
public String hookName() { |
|||
return getClass().getSimpleName(); |
|||
} |
|||
|
|||
@Override |
|||
public void sendMessageBefore(SendMessageContext sendMessageContext) { |
|||
Long tenantId = TenantContextHolder.getTenantId(); |
|||
if (tenantId == null) { |
|||
return; |
|||
} |
|||
sendMessageContext.getMessage().putUserProperty(HEADER_TENANT_ID, tenantId.toString()); |
|||
} |
|||
|
|||
@Override |
|||
public void sendMessageAfter(SendMessageContext sendMessageContext) { |
|||
} |
|||
|
|||
} |
|||
@ -1,46 +0,0 @@ |
|||
package cn.iocoder.yudao.framework.tenant.core.redis; |
|||
|
|||
import cn.hutool.core.collection.CollUtil; |
|||
import cn.iocoder.yudao.framework.redis.core.TimeoutRedisCacheManager; |
|||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.cache.Cache; |
|||
import org.springframework.data.redis.cache.RedisCacheConfiguration; |
|||
import org.springframework.data.redis.cache.RedisCacheManager; |
|||
import org.springframework.data.redis.cache.RedisCacheWriter; |
|||
|
|||
import java.util.Set; |
|||
|
|||
/** |
|||
* 多租户的 {@link RedisCacheManager} 实现类 |
|||
* |
|||
* 操作指定 name 的 {@link Cache} 时,自动拼接租户后缀,格式为 name + ":" + tenantId + 后缀 |
|||
* |
|||
* @author airhead |
|||
*/ |
|||
@Slf4j |
|||
public class TenantRedisCacheManager extends TimeoutRedisCacheManager { |
|||
|
|||
private final Set<String> ignoreCaches; |
|||
|
|||
public TenantRedisCacheManager(RedisCacheWriter cacheWriter, |
|||
RedisCacheConfiguration defaultCacheConfiguration, |
|||
Set<String> ignoreCaches) { |
|||
super(cacheWriter, defaultCacheConfiguration); |
|||
this.ignoreCaches = ignoreCaches; |
|||
} |
|||
|
|||
@Override |
|||
public Cache getCache(String name) { |
|||
// 如果开启多租户,则 name 拼接租户后缀
|
|||
if (!TenantContextHolder.isIgnore() |
|||
&& TenantContextHolder.getTenantId() != null |
|||
&& !CollUtil.contains(ignoreCaches, name)) { |
|||
name = name + ":" + TenantContextHolder.getTenantId(); |
|||
} |
|||
|
|||
// 继续基于父方法
|
|||
return super.getCache(name); |
|||
} |
|||
|
|||
} |
|||
@ -1,117 +0,0 @@ |
|||
package cn.iocoder.yudao.framework.tenant.core.security; |
|||
|
|||
import cn.hutool.core.collection.CollUtil; |
|||
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; |
|||
import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
|||
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; |
|||
import cn.iocoder.yudao.framework.security.core.LoginUser; |
|||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; |
|||
import cn.iocoder.yudao.framework.tenant.config.TenantProperties; |
|||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; |
|||
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService; |
|||
import cn.iocoder.yudao.framework.web.config.WebProperties; |
|||
import cn.iocoder.yudao.framework.web.core.filter.ApiRequestFilter; |
|||
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.util.AntPathMatcher; |
|||
|
|||
import jakarta.servlet.FilterChain; |
|||
import jakarta.servlet.ServletException; |
|||
import jakarta.servlet.http.HttpServletRequest; |
|||
import jakarta.servlet.http.HttpServletResponse; |
|||
import java.io.IOException; |
|||
import java.util.Objects; |
|||
|
|||
/** |
|||
* 多租户 Security Web 过滤器 |
|||
* 1. 如果是登陆的用户,校验是否有权限访问该租户,避免越权问题。 |
|||
* 2. 如果请求未带租户的编号,检查是否是忽略的 URL,否则也不允许访问。 |
|||
* 3. 校验租户是合法,例如说被禁用、到期 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Slf4j |
|||
public class TenantSecurityWebFilter extends ApiRequestFilter { |
|||
|
|||
private final TenantProperties tenantProperties; |
|||
|
|||
private final AntPathMatcher pathMatcher; |
|||
|
|||
private final GlobalExceptionHandler globalExceptionHandler; |
|||
private final TenantFrameworkService tenantFrameworkService; |
|||
|
|||
public TenantSecurityWebFilter(TenantProperties tenantProperties, |
|||
WebProperties webProperties, |
|||
GlobalExceptionHandler globalExceptionHandler, |
|||
TenantFrameworkService tenantFrameworkService) { |
|||
super(webProperties); |
|||
this.tenantProperties = tenantProperties; |
|||
this.pathMatcher = new AntPathMatcher(); |
|||
this.globalExceptionHandler = globalExceptionHandler; |
|||
this.tenantFrameworkService = tenantFrameworkService; |
|||
} |
|||
|
|||
@Override |
|||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) |
|||
throws ServletException, IOException { |
|||
Long tenantId = TenantContextHolder.getTenantId(); |
|||
// 1. 登陆的用户,校验是否有权限访问该租户,避免越权问题。
|
|||
LoginUser user = SecurityFrameworkUtils.getLoginUser(); |
|||
if (user != null) { |
|||
// 如果获取不到租户编号,则尝试使用登陆用户的租户编号
|
|||
if (tenantId == null) { |
|||
tenantId = user.getTenantId(); |
|||
TenantContextHolder.setTenantId(tenantId); |
|||
// 如果传递了租户编号,则进行比对租户编号,避免越权问题
|
|||
} else if (!Objects.equals(user.getTenantId(), TenantContextHolder.getTenantId())) { |
|||
log.error("[doFilterInternal][租户({}) User({}/{}) 越权访问租户({}) URL({}/{})]", |
|||
user.getTenantId(), user.getId(), user.getUserType(), |
|||
TenantContextHolder.getTenantId(), request.getRequestURI(), request.getMethod()); |
|||
ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.FORBIDDEN.getCode(), |
|||
"您无权访问该租户的数据")); |
|||
return; |
|||
} |
|||
} |
|||
|
|||
// 如果非允许忽略租户的 URL,则校验租户是否合法
|
|||
if (!isIgnoreUrl(request)) { |
|||
// 2. 如果请求未带租户的编号,不允许访问。
|
|||
if (tenantId == null) { |
|||
log.error("[doFilterInternal][URL({}/{}) 未传递租户编号]", request.getRequestURI(), request.getMethod()); |
|||
ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), |
|||
"请求的租户标识未传递,请进行排查")); |
|||
return; |
|||
} |
|||
// 3. 校验租户是合法,例如说被禁用、到期
|
|||
try { |
|||
tenantFrameworkService.validTenant(tenantId); |
|||
} catch (Throwable ex) { |
|||
CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex); |
|||
ServletUtils.writeJSON(response, result); |
|||
return; |
|||
} |
|||
} else { // 如果是允许忽略租户的 URL,若未传递租户编号,则默认忽略租户编号,避免报错
|
|||
if (tenantId == null) { |
|||
TenantContextHolder.setIgnore(true); |
|||
} |
|||
} |
|||
|
|||
// 继续过滤
|
|||
chain.doFilter(request, response); |
|||
} |
|||
|
|||
private boolean isIgnoreUrl(HttpServletRequest request) { |
|||
// 快速匹配,保证性能
|
|||
if (CollUtil.contains(tenantProperties.getIgnoreUrls(), request.getRequestURI())) { |
|||
return true; |
|||
} |
|||
// 逐个 Ant 路径匹配
|
|||
for (String url : tenantProperties.getIgnoreUrls()) { |
|||
if (pathMatcher.match(url, request.getRequestURI())) { |
|||
return true; |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
} |
|||
@ -1,26 +0,0 @@ |
|||
package cn.iocoder.yudao.framework.tenant.core.service; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* Tenant 框架 Service 接口,定义获取租户信息 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
public interface TenantFrameworkService { |
|||
|
|||
/** |
|||
* 获得所有租户 |
|||
* |
|||
* @return 租户编号数组 |
|||
*/ |
|||
List<Long> getTenantIds(); |
|||
|
|||
/** |
|||
* 校验租户是否合法 |
|||
* |
|||
* @param id 租户编号 |
|||
*/ |
|||
void validTenant(Long id); |
|||
|
|||
} |
|||
@ -1,73 +0,0 @@ |
|||
package cn.iocoder.yudao.framework.tenant.core.service; |
|||
|
|||
import cn.iocoder.yudao.framework.common.exception.ServiceException; |
|||
import cn.iocoder.yudao.framework.common.util.cache.CacheUtils; |
|||
import cn.iocoder.yudao.module.system.api.tenant.TenantApi; |
|||
import com.google.common.cache.CacheLoader; |
|||
import com.google.common.cache.LoadingCache; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.SneakyThrows; |
|||
|
|||
import java.time.Duration; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* Tenant 框架 Service 实现类 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@RequiredArgsConstructor |
|||
public class TenantFrameworkServiceImpl implements TenantFrameworkService { |
|||
|
|||
private static final ServiceException SERVICE_EXCEPTION_NULL = new ServiceException(); |
|||
|
|||
private final TenantApi tenantApi; |
|||
|
|||
/** |
|||
* 针对 {@link #getTenantIds()} 的缓存 |
|||
*/ |
|||
private final LoadingCache<Object, List<Long>> getTenantIdsCache = CacheUtils.buildAsyncReloadingCache( |
|||
Duration.ofMinutes(1L), // 过期时间 1 分钟
|
|||
new CacheLoader<Object, List<Long>>() { |
|||
|
|||
@Override |
|||
public List<Long> load(Object key) { |
|||
return tenantApi.getTenantIdList(); |
|||
} |
|||
|
|||
}); |
|||
|
|||
/** |
|||
* 针对 {@link #validTenant(Long)} 的缓存 |
|||
*/ |
|||
private final LoadingCache<Long, ServiceException> validTenantCache = CacheUtils.buildAsyncReloadingCache( |
|||
Duration.ofMinutes(1L), // 过期时间 1 分钟
|
|||
new CacheLoader<Long, ServiceException>() { |
|||
|
|||
@Override |
|||
public ServiceException load(Long id) { |
|||
try { |
|||
tenantApi.validateTenant(id); |
|||
return SERVICE_EXCEPTION_NULL; |
|||
} catch (ServiceException ex) { |
|||
return ex; |
|||
} |
|||
} |
|||
|
|||
}); |
|||
|
|||
@Override |
|||
@SneakyThrows |
|||
public List<Long> getTenantIds() { |
|||
return getTenantIdsCache.get(Boolean.TRUE); |
|||
} |
|||
|
|||
@Override |
|||
public void validTenant(Long id) { |
|||
ServiceException serviceException = validTenantCache.getUnchecked(id); |
|||
if (serviceException != SERVICE_EXCEPTION_NULL) { |
|||
throw serviceException; |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -1,113 +0,0 @@ |
|||
package cn.iocoder.yudao.framework.tenant.core.util; |
|||
|
|||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; |
|||
|
|||
import java.util.Map; |
|||
import java.util.concurrent.Callable; |
|||
|
|||
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; |
|||
|
|||
/** |
|||
* 多租户 Util |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
public class TenantUtils { |
|||
|
|||
/** |
|||
* 使用指定租户,执行对应的逻辑 |
|||
* |
|||
* 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户 |
|||
* 当然,执行完成后,还是会恢复回去 |
|||
* |
|||
* @param tenantId 租户编号 |
|||
* @param runnable 逻辑 |
|||
*/ |
|||
public static void execute(Long tenantId, Runnable runnable) { |
|||
Long oldTenantId = TenantContextHolder.getTenantId(); |
|||
Boolean oldIgnore = TenantContextHolder.isIgnore(); |
|||
try { |
|||
TenantContextHolder.setTenantId(tenantId); |
|||
TenantContextHolder.setIgnore(false); |
|||
// 执行逻辑
|
|||
runnable.run(); |
|||
} finally { |
|||
TenantContextHolder.setTenantId(oldTenantId); |
|||
TenantContextHolder.setIgnore(oldIgnore); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 使用指定租户,执行对应的逻辑 |
|||
* |
|||
* 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户 |
|||
* 当然,执行完成后,还是会恢复回去 |
|||
* |
|||
* @param tenantId 租户编号 |
|||
* @param callable 逻辑 |
|||
* @return 结果 |
|||
*/ |
|||
public static <V> V execute(Long tenantId, Callable<V> callable) { |
|||
Long oldTenantId = TenantContextHolder.getTenantId(); |
|||
Boolean oldIgnore = TenantContextHolder.isIgnore(); |
|||
try { |
|||
TenantContextHolder.setTenantId(tenantId); |
|||
TenantContextHolder.setIgnore(false); |
|||
// 执行逻辑
|
|||
return callable.call(); |
|||
} catch (Exception e) { |
|||
throw new RuntimeException(e); |
|||
} finally { |
|||
TenantContextHolder.setTenantId(oldTenantId); |
|||
TenantContextHolder.setIgnore(oldIgnore); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 忽略租户,执行对应的逻辑 |
|||
* |
|||
* @param runnable 逻辑 |
|||
*/ |
|||
public static void executeIgnore(Runnable runnable) { |
|||
Boolean oldIgnore = TenantContextHolder.isIgnore(); |
|||
try { |
|||
TenantContextHolder.setIgnore(true); |
|||
// 执行逻辑
|
|||
runnable.run(); |
|||
} finally { |
|||
TenantContextHolder.setIgnore(oldIgnore); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 忽略租户,执行对应的逻辑 |
|||
* |
|||
* @param callable 逻辑 |
|||
* @return 结果 |
|||
*/ |
|||
public static <V> V executeIgnore(Callable<V> callable) { |
|||
Boolean oldIgnore = TenantContextHolder.isIgnore(); |
|||
try { |
|||
TenantContextHolder.setIgnore(true); |
|||
// 执行逻辑
|
|||
return callable.call(); |
|||
} catch (Exception e) { |
|||
throw new RuntimeException(e); |
|||
} finally { |
|||
TenantContextHolder.setIgnore(oldIgnore); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 将多租户编号,添加到 header 中 |
|||
* |
|||
* @param headers HTTP 请求 headers |
|||
* @param tenantId 租户编号 |
|||
*/ |
|||
public static void addTenantHeader(Map<String, String> headers, Long tenantId) { |
|||
if (tenantId != null) { |
|||
headers.put(HEADER_TENANT_ID, tenantId.toString()); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -1,37 +0,0 @@ |
|||
package cn.iocoder.yudao.framework.tenant.core.web; |
|||
|
|||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; |
|||
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; |
|||
import org.springframework.web.filter.OncePerRequestFilter; |
|||
|
|||
import jakarta.servlet.FilterChain; |
|||
import jakarta.servlet.ServletException; |
|||
import jakarta.servlet.http.HttpServletRequest; |
|||
import jakarta.servlet.http.HttpServletResponse; |
|||
import java.io.IOException; |
|||
|
|||
/** |
|||
* 多租户 Context Web 过滤器 |
|||
* 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号。 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
public class TenantContextWebFilter extends OncePerRequestFilter { |
|||
|
|||
@Override |
|||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) |
|||
throws ServletException, IOException { |
|||
// 设置
|
|||
Long tenantId = WebFrameworkUtils.getTenantId(request); |
|||
if (tenantId != null) { |
|||
TenantContextHolder.setTenantId(tenantId); |
|||
} |
|||
try { |
|||
chain.doFilter(request, response); |
|||
} finally { |
|||
// 清理
|
|||
TenantContextHolder.clear(); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -1,17 +0,0 @@ |
|||
/** |
|||
* 多租户,支持如下层面: |
|||
* 1. DB:基于 MyBatis Plus 多租户的功能实现。 |
|||
* 2. Redis:通过在 Redis Key 上拼接租户编号的方式,进行隔离。 |
|||
* 3. Web:请求 HTTP API 时,解析 Header 的 tenant-id 租户编号,添加到租户上下文。 |
|||
* 4. Security:校验当前登陆的用户,是否越权访问其它租户的数据。 |
|||
* 5. Job:在 JobHandler 执行任务时,会按照每个租户,都独立并行执行一次。 |
|||
* 6. MQ:在 Producer 发送消息时,Header 带上 tenant-id 租户编号;在 Consumer 消费消息时,将 Header 的 tenant-id 租户编号,添加到租户上下文。 |
|||
* 7. Async:异步需要保证 ThreadLocal 的传递性,通过使用阿里开源的 TransmittableThreadLocal 实现。相关的改造点,可见: |
|||
* 1)Spring Async: |
|||
* {@link cn.iocoder.yudao.framework.quartz.config.YudaoAsyncAutoConfiguration#threadPoolTaskExecutorBeanPostProcessor()} |
|||
* 2)Spring Security: |
|||
* TransmittableThreadLocalSecurityContextHolderStrategy |
|||
* 和 YudaoSecurityAutoConfiguration#securityContextHolderMethodInvokingFactoryBean() 方法 |
|||
* |
|||
*/ |
|||
package cn.iocoder.yudao.framework.tenant; |
|||
@ -1,275 +0,0 @@ |
|||
/* |
|||
* Copyright 2002-2023 the original author or authors. |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* https://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
|
|||
package org.springframework.messaging.handler.invocation; |
|||
|
|||
import java.lang.reflect.InvocationTargetException; |
|||
import java.lang.reflect.Method; |
|||
import java.lang.reflect.Type; |
|||
import java.util.Arrays; |
|||
|
|||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; |
|||
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; |
|||
import org.springframework.core.DefaultParameterNameDiscoverer; |
|||
import org.springframework.core.MethodParameter; |
|||
import org.springframework.core.ParameterNameDiscoverer; |
|||
import org.springframework.core.ResolvableType; |
|||
import org.springframework.lang.Nullable; |
|||
import org.springframework.messaging.Message; |
|||
import org.springframework.messaging.handler.HandlerMethod; |
|||
import org.springframework.util.ObjectUtils; |
|||
|
|||
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; |
|||
|
|||
/** |
|||
* Extension of {@link HandlerMethod} that invokes the underlying method with |
|||
* argument values resolved from the current HTTP request through a list of |
|||
* {@link HandlerMethodArgumentResolver}. |
|||
* |
|||
* 针对 rabbitmq-spring 和 kafka-spring,不存在合适的拓展点,可以实现 Consumer 消费前,读取 Header 中的 tenant-id 设置到 {@link TenantContextHolder} 中 |
|||
* TODO 芋艿:持续跟进,看看有没新的拓展点 |
|||
* |
|||
* @author Rossen Stoyanchev |
|||
* @author Juergen Hoeller |
|||
* @since 4.0 |
|||
*/ |
|||
public class InvocableHandlerMethod extends HandlerMethod { |
|||
|
|||
private static final Object[] EMPTY_ARGS = new Object[0]; |
|||
|
|||
|
|||
private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite(); |
|||
|
|||
private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); |
|||
|
|||
|
|||
/** |
|||
* Create an instance from a {@code HandlerMethod}. |
|||
*/ |
|||
public InvocableHandlerMethod(HandlerMethod handlerMethod) { |
|||
super(handlerMethod); |
|||
} |
|||
|
|||
/** |
|||
* Create an instance from a bean instance and a method. |
|||
*/ |
|||
public InvocableHandlerMethod(Object bean, Method method) { |
|||
super(bean, method); |
|||
} |
|||
|
|||
/** |
|||
* Construct a new handler method with the given bean instance, method name and parameters. |
|||
* @param bean the object bean |
|||
* @param methodName the method name |
|||
* @param parameterTypes the method parameter types |
|||
* @throws NoSuchMethodException when the method cannot be found |
|||
*/ |
|||
public InvocableHandlerMethod(Object bean, String methodName, Class<?>... parameterTypes) |
|||
throws NoSuchMethodException { |
|||
|
|||
super(bean, methodName, parameterTypes); |
|||
} |
|||
|
|||
|
|||
/** |
|||
* Set {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers} to use for resolving method argument values. |
|||
*/ |
|||
public void setMessageMethodArgumentResolvers(HandlerMethodArgumentResolverComposite argumentResolvers) { |
|||
this.resolvers = argumentResolvers; |
|||
} |
|||
|
|||
/** |
|||
* Set the ParameterNameDiscoverer for resolving parameter names when needed |
|||
* (e.g. default request attribute name). |
|||
* <p>Default is a {@link org.springframework.core.DefaultParameterNameDiscoverer}. |
|||
*/ |
|||
public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) { |
|||
this.parameterNameDiscoverer = parameterNameDiscoverer; |
|||
} |
|||
|
|||
|
|||
/** |
|||
* Invoke the method after resolving its argument values in the context of the given message. |
|||
* <p>Argument values are commonly resolved through |
|||
* {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}. |
|||
* The {@code providedArgs} parameter however may supply argument values to be used directly, |
|||
* i.e. without argument resolution. |
|||
* <p>Delegates to {@link #getMethodArgumentValues} and calls {@link #doInvoke} with the |
|||
* resolved arguments. |
|||
* @param message the current message being processed |
|||
* @param providedArgs "given" arguments matched by type, not resolved |
|||
* @return the raw value returned by the invoked method |
|||
* @throws Exception raised if no suitable argument resolver can be found, |
|||
* or if the method raised an exception |
|||
* @see #getMethodArgumentValues |
|||
* @see #doInvoke |
|||
*/ |
|||
@Nullable |
|||
public Object invoke(Message<?> message, Object... providedArgs) throws Exception { |
|||
Object[] args = getMethodArgumentValues(message, providedArgs); |
|||
if (logger.isTraceEnabled()) { |
|||
logger.trace("Arguments: " + Arrays.toString(args)); |
|||
} |
|||
// 注意:如下是本类的改动点!!!
|
|||
// 情况一:无租户编号的情况
|
|||
Long tenantId= parseTenantId(message); |
|||
if (tenantId == null) { |
|||
return doInvoke(args); |
|||
} |
|||
// 情况二:有租户的情况下
|
|||
return TenantUtils.execute(tenantId, () -> doInvoke(args)); |
|||
} |
|||
|
|||
private Long parseTenantId(Message<?> message) { |
|||
Object tenantId = message.getHeaders().get(HEADER_TENANT_ID); |
|||
if (tenantId == null) { |
|||
return null; |
|||
} |
|||
if (tenantId instanceof Long) { |
|||
return (Long) tenantId; |
|||
} |
|||
if (tenantId instanceof Number) { |
|||
return ((Number) tenantId).longValue(); |
|||
} |
|||
if (tenantId instanceof String) { |
|||
return Long.parseLong((String) tenantId); |
|||
} |
|||
if (tenantId instanceof byte[]) { |
|||
return Long.parseLong(new String((byte[]) tenantId)); |
|||
} |
|||
throw new IllegalArgumentException("未知的数据类型:" + tenantId); |
|||
} |
|||
|
|||
/** |
|||
* Get the method argument values for the current message, checking the provided |
|||
* argument values and falling back to the configured argument resolvers. |
|||
* <p>The resulting array will be passed into {@link #doInvoke}. |
|||
* @since 5.1.2 |
|||
*/ |
|||
protected Object[] getMethodArgumentValues(Message<?> message, Object... providedArgs) throws Exception { |
|||
MethodParameter[] parameters = getMethodParameters(); |
|||
if (ObjectUtils.isEmpty(parameters)) { |
|||
return EMPTY_ARGS; |
|||
} |
|||
|
|||
Object[] args = new Object[parameters.length]; |
|||
for (int i = 0; i < parameters.length; i++) { |
|||
MethodParameter parameter = parameters[i]; |
|||
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); |
|||
args[i] = findProvidedArgument(parameter, providedArgs); |
|||
if (args[i] != null) { |
|||
continue; |
|||
} |
|||
if (!this.resolvers.supportsParameter(parameter)) { |
|||
throw new MethodArgumentResolutionException( |
|||
message, parameter, formatArgumentError(parameter, "No suitable resolver")); |
|||
} |
|||
try { |
|||
args[i] = this.resolvers.resolveArgument(parameter, message); |
|||
} |
|||
catch (Exception ex) { |
|||
// Leave stack trace for later, exception may actually be resolved and handled...
|
|||
if (logger.isDebugEnabled()) { |
|||
String exMsg = ex.getMessage(); |
|||
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) { |
|||
logger.debug(formatArgumentError(parameter, exMsg)); |
|||
} |
|||
} |
|||
throw ex; |
|||
} |
|||
} |
|||
return args; |
|||
} |
|||
|
|||
/** |
|||
* Invoke the handler method with the given argument values. |
|||
*/ |
|||
@Nullable |
|||
protected Object doInvoke(Object... args) throws Exception { |
|||
try { |
|||
return getBridgedMethod().invoke(getBean(), args); |
|||
} |
|||
catch (IllegalArgumentException ex) { |
|||
assertTargetBean(getBridgedMethod(), getBean(), args); |
|||
String text = (ex.getMessage() == null || ex.getCause() instanceof NullPointerException) ? |
|||
"Illegal argument": ex.getMessage(); |
|||
throw new IllegalStateException(formatInvokeError(text, args), ex); |
|||
} |
|||
catch (InvocationTargetException ex) { |
|||
// Unwrap for HandlerExceptionResolvers ...
|
|||
Throwable targetException = ex.getTargetException(); |
|||
if (targetException instanceof RuntimeException runtimeException) { |
|||
throw runtimeException; |
|||
} |
|||
else if (targetException instanceof Error error) { |
|||
throw error; |
|||
} |
|||
else if (targetException instanceof Exception exception) { |
|||
throw exception; |
|||
} |
|||
else { |
|||
throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException); |
|||
} |
|||
} |
|||
} |
|||
|
|||
MethodParameter getAsyncReturnValueType(@Nullable Object returnValue) { |
|||
return new AsyncResultMethodParameter(returnValue); |
|||
} |
|||
|
|||
|
|||
private class AsyncResultMethodParameter extends AnnotatedMethodParameter { |
|||
|
|||
@Nullable |
|||
private final Object returnValue; |
|||
|
|||
private final ResolvableType returnType; |
|||
|
|||
public AsyncResultMethodParameter(@Nullable Object returnValue) { |
|||
super(-1); |
|||
this.returnValue = returnValue; |
|||
this.returnType = ResolvableType.forType(super.getGenericParameterType()).getGeneric(); |
|||
} |
|||
|
|||
protected AsyncResultMethodParameter(AsyncResultMethodParameter original) { |
|||
super(original); |
|||
this.returnValue = original.returnValue; |
|||
this.returnType = original.returnType; |
|||
} |
|||
|
|||
@Override |
|||
public Class<?> getParameterType() { |
|||
if (this.returnValue != null) { |
|||
return this.returnValue.getClass(); |
|||
} |
|||
if (!ResolvableType.NONE.equals(this.returnType)) { |
|||
return this.returnType.toClass(); |
|||
} |
|||
return super.getParameterType(); |
|||
} |
|||
|
|||
@Override |
|||
public Type getGenericParameterType() { |
|||
return this.returnType.getType(); |
|||
} |
|||
|
|||
@Override |
|||
public AsyncResultMethodParameter clone() { |
|||
return new AsyncResultMethodParameter(this); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -1,2 +0,0 @@ |
|||
org.springframework.boot.env.EnvironmentPostProcessor=\ |
|||
cn.iocoder.yudao.framework.tenant.core.mq.kafka.TenantKafkaEnvironmentPostProcessor |
|||
@ -1 +0,0 @@ |
|||
cn.iocoder.yudao.framework.tenant.config.YudaoTenantAutoConfiguration |
|||
@ -0,0 +1,26 @@ |
|||
<?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> |
|||
<groupId>cn.iocoder.boot</groupId> |
|||
<artifactId>yudao-module-alert</artifactId> |
|||
<version>${revision}</version> <!-- 1. 修改 version 为 ${revision} --> |
|||
</parent> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
<artifactId>yudao-module-alert-api</artifactId> |
|||
<packaging>jar</packaging> <!-- 2. 新增 packaging 为 jar --> |
|||
|
|||
<name>${project.artifactId}</name> <!-- 3. 新增 name 为 ${project.artifactId} --> |
|||
<description> <!-- 4. 新增 description 为该模块的描述 --> |
|||
alert 模块 API,暴露给其它模块调用 |
|||
</description> |
|||
|
|||
<dependencies> <!-- 5. 新增 yudao-common 依赖 --> |
|||
<dependency> |
|||
<groupId>cn.iocoder.boot</groupId> |
|||
<artifactId>yudao-common</artifactId> |
|||
</dependency> |
|||
</dependencies> |
|||
|
|||
</project> |
|||
@ -1,26 +0,0 @@ |
|||
package cn.iocoder.yudao.module.system.api.tenant; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 多租户的 API 接口 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
public interface TenantApi { |
|||
|
|||
/** |
|||
* 获得所有租户 |
|||
* |
|||
* @return 租户编号数组 |
|||
*/ |
|||
List<Long> getTenantIdList(); |
|||
|
|||
/** |
|||
* 校验租户是否合法 |
|||
* |
|||
* @param id 租户编号 |
|||
*/ |
|||
void validateTenant(Long id); |
|||
|
|||
} |
|||
@ -1,30 +0,0 @@ |
|||
package cn.iocoder.yudao.module.system.api.tenant; |
|||
|
|||
import cn.iocoder.yudao.module.system.service.tenant.TenantService; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
import jakarta.annotation.Resource; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 多租户的 API 实现类 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Service |
|||
public class TenantApiImpl implements TenantApi { |
|||
|
|||
@Resource |
|||
private TenantService tenantService; |
|||
|
|||
@Override |
|||
public List<Long> getTenantIdList() { |
|||
return tenantService.getTenantIdList(); |
|||
} |
|||
|
|||
@Override |
|||
public void validateTenant(Long id) { |
|||
tenantService.validTenant(id); |
|||
} |
|||
|
|||
} |
|||
@ -1,21 +0,0 @@ |
|||
### 获取租户编号 /admin-api/system/get-id-by-name |
|||
GET {{baseUrl}}/system/tenant/get-id-by-name?name=芋道源码 |
|||
|
|||
### 创建租户 /admin-api/system/tenant/create |
|||
POST {{baseUrl}}/system/tenant/create |
|||
Content-Type: application/json |
|||
Authorization: Bearer {{token}} |
|||
tenant-id: {{adminTenantId}} |
|||
|
|||
{ |
|||
"name": "芋道", |
|||
"contactName": "芋艿", |
|||
"contactMobile": "15601691300", |
|||
"status": 0, |
|||
"domain": "https://www.iocoder.cn", |
|||
"packageId": 110, |
|||
"expireTime": 1699545600000, |
|||
"accountCount": 20, |
|||
"username": "admin", |
|||
"password": "123321" |
|||
} |
|||
@ -1,123 +0,0 @@ |
|||
package cn.iocoder.yudao.module.system.controller.admin.tenant; |
|||
|
|||
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; |
|||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
|||
import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
|||
import cn.iocoder.yudao.framework.common.pojo.PageParam; |
|||
import cn.iocoder.yudao.framework.common.pojo.PageResult; |
|||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
|||
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; |
|||
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO; |
|||
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantRespVO; |
|||
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO; |
|||
import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO; |
|||
import cn.iocoder.yudao.module.system.service.tenant.TenantService; |
|||
import io.swagger.v3.oas.annotations.Operation; |
|||
import io.swagger.v3.oas.annotations.Parameter; |
|||
import io.swagger.v3.oas.annotations.tags.Tag; |
|||
import jakarta.annotation.Resource; |
|||
import jakarta.annotation.security.PermitAll; |
|||
import jakarta.servlet.http.HttpServletResponse; |
|||
import jakarta.validation.Valid; |
|||
import org.springframework.security.access.prepost.PreAuthorize; |
|||
import org.springframework.web.bind.annotation.*; |
|||
|
|||
import java.io.IOException; |
|||
import java.util.List; |
|||
|
|||
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; |
|||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
|||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; |
|||
|
|||
@Tag(name = "管理后台 - 租户") |
|||
@RestController |
|||
@RequestMapping("/system/tenant") |
|||
public class TenantController { |
|||
|
|||
@Resource |
|||
private TenantService tenantService; |
|||
|
|||
@GetMapping("/get-id-by-name") |
|||
@PermitAll |
|||
@Operation(summary = "使用租户名,获得租户编号", description = "登录界面,根据用户的租户名,获得租户编号") |
|||
@Parameter(name = "name", description = "租户名", required = true, example = "1024") |
|||
public CommonResult<Long> getTenantIdByName(@RequestParam("name") String name) { |
|||
TenantDO tenant = tenantService.getTenantByName(name); |
|||
return success(tenant != null ? tenant.getId() : null); |
|||
} |
|||
|
|||
@GetMapping({ "simple-list" }) |
|||
@PermitAll |
|||
@Operation(summary = "获取租户精简信息列表", description = "只包含被开启的租户,用于【首页】功能的选择租户选项") |
|||
public CommonResult<List<TenantRespVO>> getTenantSimpleList() { |
|||
List<TenantDO> list = tenantService.getTenantListByStatus(CommonStatusEnum.ENABLE.getStatus()); |
|||
return success(convertList(list, tenantDO -> |
|||
new TenantRespVO().setId(tenantDO.getId()).setName(tenantDO.getName()))); |
|||
} |
|||
|
|||
@GetMapping("/get-by-website") |
|||
@PermitAll |
|||
@Operation(summary = "使用域名,获得租户信息", description = "登录界面,根据用户的域名,获得租户信息") |
|||
@Parameter(name = "website", description = "域名", required = true, example = "www.iocoder.cn") |
|||
public CommonResult<TenantRespVO> getTenantByWebsite(@RequestParam("website") String website) { |
|||
TenantDO tenant = tenantService.getTenantByWebsite(website); |
|||
if (tenant == null || CommonStatusEnum.isDisable(tenant.getStatus())) { |
|||
return success(null); |
|||
} |
|||
return success(new TenantRespVO().setId(tenant.getId()).setName(tenant.getName())); |
|||
} |
|||
|
|||
@PostMapping("/create") |
|||
@Operation(summary = "创建租户") |
|||
@PreAuthorize("@ss.hasPermission('system:tenant:create')") |
|||
public CommonResult<Long> createTenant(@Valid @RequestBody TenantSaveReqVO createReqVO) { |
|||
return success(tenantService.createTenant(createReqVO)); |
|||
} |
|||
|
|||
@PutMapping("/update") |
|||
@Operation(summary = "更新租户") |
|||
@PreAuthorize("@ss.hasPermission('system:tenant:update')") |
|||
public CommonResult<Boolean> updateTenant(@Valid @RequestBody TenantSaveReqVO updateReqVO) { |
|||
tenantService.updateTenant(updateReqVO); |
|||
return success(true); |
|||
} |
|||
|
|||
@DeleteMapping("/delete") |
|||
@Operation(summary = "删除租户") |
|||
@Parameter(name = "id", description = "编号", required = true, example = "1024") |
|||
@PreAuthorize("@ss.hasPermission('system:tenant:delete')") |
|||
public CommonResult<Boolean> deleteTenant(@RequestParam("id") Long id) { |
|||
tenantService.deleteTenant(id); |
|||
return success(true); |
|||
} |
|||
|
|||
@GetMapping("/get") |
|||
@Operation(summary = "获得租户") |
|||
@Parameter(name = "id", description = "编号", required = true, example = "1024") |
|||
@PreAuthorize("@ss.hasPermission('system:tenant:query')") |
|||
public CommonResult<TenantRespVO> getTenant(@RequestParam("id") Long id) { |
|||
TenantDO tenant = tenantService.getTenant(id); |
|||
return success(BeanUtils.toBean(tenant, TenantRespVO.class)); |
|||
} |
|||
|
|||
@GetMapping("/page") |
|||
@Operation(summary = "获得租户分页") |
|||
@PreAuthorize("@ss.hasPermission('system:tenant:query')") |
|||
public CommonResult<PageResult<TenantRespVO>> getTenantPage(@Valid TenantPageReqVO pageVO) { |
|||
PageResult<TenantDO> pageResult = tenantService.getTenantPage(pageVO); |
|||
return success(BeanUtils.toBean(pageResult, TenantRespVO.class)); |
|||
} |
|||
|
|||
@GetMapping("/export-excel") |
|||
@Operation(summary = "导出租户 Excel") |
|||
@PreAuthorize("@ss.hasPermission('system:tenant:export')") |
|||
@ApiAccessLog(operateType = EXPORT) |
|||
public void exportTenantExcel(@Valid TenantPageReqVO exportReqVO, HttpServletResponse response) throws IOException { |
|||
exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); |
|||
List<TenantDO> list = tenantService.getTenantPage(exportReqVO).getList(); |
|||
// 导出 Excel
|
|||
ExcelUtils.write(response, "租户.xls", "数据", TenantRespVO.class, |
|||
BeanUtils.toBean(list, TenantRespVO.class)); |
|||
} |
|||
|
|||
} |
|||
@ -1,80 +0,0 @@ |
|||
package cn.iocoder.yudao.module.system.controller.admin.tenant; |
|||
|
|||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
|||
import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
|||
import cn.iocoder.yudao.framework.common.pojo.PageResult; |
|||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
|||
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages.*; |
|||
import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantPackageDO; |
|||
import cn.iocoder.yudao.module.system.service.tenant.TenantPackageService; |
|||
import io.swagger.v3.oas.annotations.tags.Tag; |
|||
import io.swagger.v3.oas.annotations.Parameter; |
|||
import io.swagger.v3.oas.annotations.Operation; |
|||
import org.springframework.security.access.prepost.PreAuthorize; |
|||
import org.springframework.validation.annotation.Validated; |
|||
import org.springframework.web.bind.annotation.*; |
|||
|
|||
import jakarta.annotation.Resource; |
|||
import jakarta.validation.Valid; |
|||
import java.util.List; |
|||
|
|||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
|||
|
|||
@Tag(name = "管理后台 - 租户套餐") |
|||
@RestController |
|||
@RequestMapping("/system/tenant-package") |
|||
@Validated |
|||
public class TenantPackageController { |
|||
|
|||
@Resource |
|||
private TenantPackageService tenantPackageService; |
|||
|
|||
@PostMapping("/create") |
|||
@Operation(summary = "创建租户套餐") |
|||
@PreAuthorize("@ss.hasPermission('system:tenant-package:create')") |
|||
public CommonResult<Long> createTenantPackage(@Valid @RequestBody TenantPackageSaveReqVO createReqVO) { |
|||
return success(tenantPackageService.createTenantPackage(createReqVO)); |
|||
} |
|||
|
|||
@PutMapping("/update") |
|||
@Operation(summary = "更新租户套餐") |
|||
@PreAuthorize("@ss.hasPermission('system:tenant-package:update')") |
|||
public CommonResult<Boolean> updateTenantPackage(@Valid @RequestBody TenantPackageSaveReqVO updateReqVO) { |
|||
tenantPackageService.updateTenantPackage(updateReqVO); |
|||
return success(true); |
|||
} |
|||
|
|||
@DeleteMapping("/delete") |
|||
@Operation(summary = "删除租户套餐") |
|||
@Parameter(name = "id", description = "编号", required = true) |
|||
@PreAuthorize("@ss.hasPermission('system:tenant-package:delete')") |
|||
public CommonResult<Boolean> deleteTenantPackage(@RequestParam("id") Long id) { |
|||
tenantPackageService.deleteTenantPackage(id); |
|||
return success(true); |
|||
} |
|||
|
|||
@GetMapping("/get") |
|||
@Operation(summary = "获得租户套餐") |
|||
@Parameter(name = "id", description = "编号", required = true, example = "1024") |
|||
@PreAuthorize("@ss.hasPermission('system:tenant-package:query')") |
|||
public CommonResult<TenantPackageRespVO> getTenantPackage(@RequestParam("id") Long id) { |
|||
TenantPackageDO tenantPackage = tenantPackageService.getTenantPackage(id); |
|||
return success(BeanUtils.toBean(tenantPackage, TenantPackageRespVO.class)); |
|||
} |
|||
|
|||
@GetMapping("/page") |
|||
@Operation(summary = "获得租户套餐分页") |
|||
@PreAuthorize("@ss.hasPermission('system:tenant-package:query')") |
|||
public CommonResult<PageResult<TenantPackageRespVO>> getTenantPackagePage(@Valid TenantPackagePageReqVO pageVO) { |
|||
PageResult<TenantPackageDO> pageResult = tenantPackageService.getTenantPackagePage(pageVO); |
|||
return success(BeanUtils.toBean(pageResult, TenantPackageRespVO.class)); |
|||
} |
|||
|
|||
@GetMapping({"/get-simple-list", "simple-list"}) |
|||
@Operation(summary = "获取租户套餐精简信息列表", description = "只包含被开启的租户套餐,主要用于前端的下拉选项") |
|||
public CommonResult<List<TenantPackageSimpleRespVO>> getTenantPackageList() { |
|||
List<TenantPackageDO> list = tenantPackageService.getTenantPackageListByStatus(CommonStatusEnum.ENABLE.getStatus()); |
|||
return success(BeanUtils.toBean(list, TenantPackageSimpleRespVO.class)); |
|||
} |
|||
|
|||
} |
|||
@ -1,32 +0,0 @@ |
|||
package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages; |
|||
|
|||
import cn.iocoder.yudao.framework.common.pojo.PageParam; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import lombok.ToString; |
|||
import org.springframework.format.annotation.DateTimeFormat; |
|||
|
|||
import java.time.LocalDateTime; |
|||
|
|||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; |
|||
|
|||
@Schema(description = "管理后台 - 租户套餐分页 Request VO") |
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@ToString(callSuper = true) |
|||
public class TenantPackagePageReqVO extends PageParam { |
|||
|
|||
@Schema(description = "套餐名", example = "VIP") |
|||
private String name; |
|||
|
|||
@Schema(description = "状态", example = "1") |
|||
private Integer status; |
|||
|
|||
@Schema(description = "备注", example = "好") |
|||
private String remark; |
|||
|
|||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) |
|||
@Schema(description = "创建时间") |
|||
private LocalDateTime[] createTime; |
|||
} |
|||
@ -1,31 +0,0 @@ |
|||
package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
|
|||
import java.time.LocalDateTime; |
|||
import java.util.Set; |
|||
|
|||
@Schema(description = "管理后台 - 租户套餐 Response VO") |
|||
@Data |
|||
public class TenantPackageRespVO { |
|||
|
|||
@Schema(description = "套餐编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") |
|||
private Long id; |
|||
|
|||
@Schema(description = "套餐名", requiredMode = Schema.RequiredMode.REQUIRED, example = "VIP") |
|||
private String name; |
|||
|
|||
@Schema(description = "状态,参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") |
|||
private Integer status; |
|||
|
|||
@Schema(description = "备注", example = "好") |
|||
private String remark; |
|||
|
|||
@Schema(description = "关联的菜单编号", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
private Set<Long> menuIds; |
|||
|
|||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
private LocalDateTime createTime; |
|||
|
|||
} |
|||
@ -1,35 +0,0 @@ |
|||
package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages; |
|||
|
|||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
|||
import cn.iocoder.yudao.framework.common.validation.InEnum; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
|
|||
import jakarta.validation.constraints.NotEmpty; |
|||
import jakarta.validation.constraints.NotNull; |
|||
import java.util.Set; |
|||
|
|||
@Schema(description = "管理后台 - 租户套餐创建/修改 Request VO") |
|||
@Data |
|||
public class TenantPackageSaveReqVO { |
|||
|
|||
@Schema(description = "套餐编号", example = "1024") |
|||
private Long id; |
|||
|
|||
@Schema(description = "套餐名", requiredMode = Schema.RequiredMode.REQUIRED, example = "VIP") |
|||
@NotEmpty(message = "套餐名不能为空") |
|||
private String name; |
|||
|
|||
@Schema(description = "状态,参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") |
|||
@NotNull(message = "状态不能为空") |
|||
@InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}") |
|||
private Integer status; |
|||
|
|||
@Schema(description = "备注", example = "好") |
|||
private String remark; |
|||
|
|||
@Schema(description = "关联的菜单编号", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
@NotNull(message = "关联的菜单编号不能为空") |
|||
private Set<Long> menuIds; |
|||
|
|||
} |
|||
@ -1,20 +0,0 @@ |
|||
package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
|
|||
import jakarta.validation.constraints.NotNull; |
|||
|
|||
@Schema(description = "管理后台 - 租户套餐精简 Response VO") |
|||
@Data |
|||
public class TenantPackageSimpleRespVO { |
|||
|
|||
@Schema(description = "套餐编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") |
|||
@NotNull(message = "套餐编号不能为空") |
|||
private Long id; |
|||
|
|||
@Schema(description = "套餐名", requiredMode = Schema.RequiredMode.REQUIRED, example = "VIP") |
|||
@NotNull(message = "套餐名不能为空") |
|||
private String name; |
|||
|
|||
} |
|||
@ -1,36 +0,0 @@ |
|||
package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant; |
|||
|
|||
import cn.iocoder.yudao.framework.common.pojo.PageParam; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import lombok.ToString; |
|||
import org.springframework.format.annotation.DateTimeFormat; |
|||
|
|||
import java.time.LocalDateTime; |
|||
|
|||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; |
|||
|
|||
@Schema(description = "管理后台 - 租户分页 Request VO") |
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@ToString(callSuper = true) |
|||
public class TenantPageReqVO extends PageParam { |
|||
|
|||
@Schema(description = "租户名", example = "芋道") |
|||
private String name; |
|||
|
|||
@Schema(description = "联系人", example = "芋艿") |
|||
private String contactName; |
|||
|
|||
@Schema(description = "联系手机", example = "15601691300") |
|||
private String contactMobile; |
|||
|
|||
@Schema(description = "租户状态(0正常 1停用)", example = "1") |
|||
private Integer status; |
|||
|
|||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) |
|||
@Schema(description = "创建时间") |
|||
private LocalDateTime[] createTime; |
|||
|
|||
} |
|||
@ -1,55 +0,0 @@ |
|||
package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant; |
|||
|
|||
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; |
|||
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; |
|||
import cn.iocoder.yudao.module.system.enums.DictTypeConstants; |
|||
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; |
|||
import com.alibaba.excel.annotation.ExcelProperty; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
|
|||
import java.time.LocalDateTime; |
|||
|
|||
@Schema(description = "管理后台 - 租户 Response VO") |
|||
@Data |
|||
@ExcelIgnoreUnannotated |
|||
public class TenantRespVO { |
|||
|
|||
@Schema(description = "租户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") |
|||
@ExcelProperty("租户编号") |
|||
private Long id; |
|||
|
|||
@Schema(description = "租户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") |
|||
@ExcelProperty("租户名") |
|||
private String name; |
|||
|
|||
@Schema(description = "联系人", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") |
|||
@ExcelProperty("联系人") |
|||
private String contactName; |
|||
|
|||
@Schema(description = "联系手机", example = "15601691300") |
|||
@ExcelProperty("联系手机") |
|||
private String contactMobile; |
|||
|
|||
@Schema(description = "租户状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") |
|||
@ExcelProperty(value = "状态", converter = DictConvert.class) |
|||
@DictFormat(DictTypeConstants.COMMON_STATUS) |
|||
private Integer status; |
|||
|
|||
@Schema(description = "绑定域名", example = "https://www.iocoder.cn") |
|||
private String website; |
|||
|
|||
@Schema(description = "租户套餐编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") |
|||
private Long packageId; |
|||
|
|||
@Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
private LocalDateTime expireTime; |
|||
|
|||
@Schema(description = "账号数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") |
|||
private Integer accountCount; |
|||
|
|||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
@ExcelProperty("创建时间") |
|||
private LocalDateTime createTime; |
|||
|
|||
} |
|||
@ -1,70 +0,0 @@ |
|||
package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant; |
|||
|
|||
import cn.hutool.core.util.ObjectUtil; |
|||
import com.fasterxml.jackson.annotation.JsonIgnore; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import org.hibernate.validator.constraints.Length; |
|||
|
|||
import jakarta.validation.constraints.AssertTrue; |
|||
import jakarta.validation.constraints.NotNull; |
|||
import jakarta.validation.constraints.Pattern; |
|||
import jakarta.validation.constraints.Size; |
|||
import java.time.LocalDateTime; |
|||
|
|||
@Schema(description = "管理后台 - 租户创建/修改 Request VO") |
|||
@Data |
|||
public class TenantSaveReqVO { |
|||
|
|||
@Schema(description = "租户编号", example = "1024") |
|||
private Long id; |
|||
|
|||
@Schema(description = "租户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") |
|||
@NotNull(message = "租户名不能为空") |
|||
private String name; |
|||
|
|||
@Schema(description = "联系人", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") |
|||
@NotNull(message = "联系人不能为空") |
|||
private String contactName; |
|||
|
|||
@Schema(description = "联系手机", example = "15601691300") |
|||
private String contactMobile; |
|||
|
|||
@Schema(description = "租户状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") |
|||
@NotNull(message = "租户状态") |
|||
private Integer status; |
|||
|
|||
@Schema(description = "绑定域名", example = "https://www.iocoder.cn") |
|||
private String website; |
|||
|
|||
@Schema(description = "租户套餐编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") |
|||
@NotNull(message = "租户套餐编号不能为空") |
|||
private Long packageId; |
|||
|
|||
@Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
@NotNull(message = "过期时间不能为空") |
|||
private LocalDateTime expireTime; |
|||
|
|||
@Schema(description = "账号数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") |
|||
@NotNull(message = "账号数量不能为空") |
|||
private Integer accountCount; |
|||
|
|||
// ========== 仅【创建】时,需要传递的字段 ==========
|
|||
|
|||
@Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao") |
|||
@Pattern(regexp = "^[a-zA-Z0-9]{4,30}$", message = "用户账号由 数字、字母 组成") |
|||
@Size(min = 4, max = 30, message = "用户账号长度为 4-30 个字符") |
|||
private String username; |
|||
|
|||
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") |
|||
@Length(min = 4, max = 16, message = "密码长度为 4-16 位") |
|||
private String password; |
|||
|
|||
@AssertTrue(message = "用户账号、密码不能为空") |
|||
@JsonIgnore |
|||
public boolean isUsernameValid() { |
|||
return id != null // 修改时,不需要传递
|
|||
|| (ObjectUtil.isAllNotEmpty(username, password)); // 新增时,必须都传递 username、password
|
|||
} |
|||
|
|||
} |
|||
@ -1,26 +0,0 @@ |
|||
package cn.iocoder.yudao.module.system.convert.tenant; |
|||
|
|||
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO; |
|||
import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserSaveReqVO; |
|||
import org.mapstruct.Mapper; |
|||
import org.mapstruct.factory.Mappers; |
|||
|
|||
/** |
|||
* 租户 Convert |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Mapper |
|||
public interface TenantConvert { |
|||
|
|||
TenantConvert INSTANCE = Mappers.getMapper(TenantConvert.class); |
|||
|
|||
default UserSaveReqVO convert02(TenantSaveReqVO bean) { |
|||
UserSaveReqVO reqVO = new UserSaveReqVO(); |
|||
reqVO.setUsername(bean.getUsername()); |
|||
reqVO.setPassword(bean.getPassword()); |
|||
reqVO.setNickname(bean.getContactName()).setMobile(bean.getContactMobile()); |
|||
return reqVO; |
|||
} |
|||
|
|||
} |
|||
@ -1,80 +0,0 @@ |
|||
package cn.iocoder.yudao.module.system.dal.dataobject.tenant; |
|||
|
|||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
|||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
|||
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; |
|||
import com.baomidou.mybatisplus.annotation.KeySequence; |
|||
import com.baomidou.mybatisplus.annotation.TableName; |
|||
import lombok.*; |
|||
|
|||
import java.time.LocalDateTime; |
|||
|
|||
/** |
|||
* 租户 DO |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@TableName(value = "system_tenant", autoResultMap = true) |
|||
@KeySequence("system_tenant_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@ToString(callSuper = true) |
|||
@Builder |
|||
@AllArgsConstructor |
|||
@NoArgsConstructor |
|||
public class TenantDO extends BaseDO { |
|||
|
|||
/** |
|||
* 套餐编号 - 系统 |
|||
*/ |
|||
public static final Long PACKAGE_ID_SYSTEM = 0L; |
|||
|
|||
/** |
|||
* 租户编号,自增 |
|||
*/ |
|||
private Long id; |
|||
/** |
|||
* 租户名,唯一 |
|||
*/ |
|||
private String name; |
|||
/** |
|||
* 联系人的用户编号 |
|||
* |
|||
* 关联 {@link AdminUserDO#getId()} |
|||
*/ |
|||
private Long contactUserId; |
|||
/** |
|||
* 联系人 |
|||
*/ |
|||
private String contactName; |
|||
/** |
|||
* 联系手机 |
|||
*/ |
|||
private String contactMobile; |
|||
/** |
|||
* 租户状态 |
|||
* |
|||
* 枚举 {@link CommonStatusEnum} |
|||
*/ |
|||
private Integer status; |
|||
/** |
|||
* 绑定域名 |
|||
*/ |
|||
private String website; |
|||
/** |
|||
* 租户套餐编号 |
|||
* |
|||
* 关联 {@link TenantPackageDO#getId()} |
|||
* 特殊逻辑:系统内置租户,不使用套餐,暂时使用 {@link #PACKAGE_ID_SYSTEM} 标识 |
|||
*/ |
|||
private Long packageId; |
|||
/** |
|||
* 过期时间 |
|||
*/ |
|||
private LocalDateTime expireTime; |
|||
/** |
|||
* 账号数量 |
|||
*/ |
|||
private Integer accountCount; |
|||
|
|||
} |
|||
@ -1,52 +0,0 @@ |
|||
package cn.iocoder.yudao.module.system.dal.dataobject.tenant; |
|||
|
|||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
|||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
|||
import com.baomidou.mybatisplus.annotation.KeySequence; |
|||
import com.baomidou.mybatisplus.annotation.TableField; |
|||
import com.baomidou.mybatisplus.annotation.TableName; |
|||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; |
|||
import lombok.*; |
|||
|
|||
import java.util.Set; |
|||
|
|||
/** |
|||
* 租户套餐 DO |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@TableName(value = "system_tenant_package", autoResultMap = true) |
|||
@KeySequence("system_tenant_package_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@ToString(callSuper = true) |
|||
@Builder |
|||
@AllArgsConstructor |
|||
@NoArgsConstructor |
|||
public class TenantPackageDO extends BaseDO { |
|||
|
|||
/** |
|||
* 套餐编号,自增 |
|||
*/ |
|||
private Long id; |
|||
/** |
|||
* 套餐名,唯一 |
|||
*/ |
|||
private String name; |
|||
/** |
|||
* 租户套餐状态 |
|||
* |
|||
* 枚举 {@link CommonStatusEnum} |
|||
*/ |
|||
private Integer status; |
|||
/** |
|||
* 备注 |
|||
*/ |
|||
private String remark; |
|||
/** |
|||
* 关联的菜单编号 |
|||
*/ |
|||
@TableField(typeHandler = JacksonTypeHandler.class) |
|||
private Set<Long> menuIds; |
|||
|
|||
} |
|||
@ -1,50 +0,0 @@ |
|||
package cn.iocoder.yudao.module.system.dal.mysql.tenant; |
|||
|
|||
import cn.iocoder.yudao.framework.common.pojo.PageResult; |
|||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
|||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
|||
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO; |
|||
import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO; |
|||
import org.apache.ibatis.annotations.Mapper; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 租户 Mapper |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Mapper |
|||
public interface TenantMapper extends BaseMapperX<TenantDO> { |
|||
|
|||
default PageResult<TenantDO> selectPage(TenantPageReqVO reqVO) { |
|||
return selectPage(reqVO, new LambdaQueryWrapperX<TenantDO>() |
|||
.likeIfPresent(TenantDO::getName, reqVO.getName()) |
|||
.likeIfPresent(TenantDO::getContactName, reqVO.getContactName()) |
|||
.likeIfPresent(TenantDO::getContactMobile, reqVO.getContactMobile()) |
|||
.eqIfPresent(TenantDO::getStatus, reqVO.getStatus()) |
|||
.betweenIfPresent(TenantDO::getCreateTime, reqVO.getCreateTime()) |
|||
.orderByDesc(TenantDO::getId)); |
|||
} |
|||
|
|||
default TenantDO selectByName(String name) { |
|||
return selectOne(TenantDO::getName, name); |
|||
} |
|||
|
|||
default TenantDO selectByWebsite(String website) { |
|||
return selectOne(TenantDO::getWebsite, website); |
|||
} |
|||
|
|||
default Long selectCountByPackageId(Long packageId) { |
|||
return selectCount(TenantDO::getPackageId, packageId); |
|||
} |
|||
|
|||
default List<TenantDO> selectListByPackageId(Long packageId) { |
|||
return selectList(TenantDO::getPackageId, packageId); |
|||
} |
|||
|
|||
default List<TenantDO> selectListByStatus(Integer status) { |
|||
return selectList(TenantDO::getStatus, status); |
|||
} |
|||
|
|||
} |
|||
@ -1,36 +0,0 @@ |
|||
package cn.iocoder.yudao.module.system.dal.mysql.tenant; |
|||
|
|||
import cn.iocoder.yudao.framework.common.pojo.PageResult; |
|||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
|||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
|||
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages.TenantPackagePageReqVO; |
|||
import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantPackageDO; |
|||
import org.apache.ibatis.annotations.Mapper; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 租户套餐 Mapper |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Mapper |
|||
public interface TenantPackageMapper extends BaseMapperX<TenantPackageDO> { |
|||
|
|||
default PageResult<TenantPackageDO> selectPage(TenantPackagePageReqVO reqVO) { |
|||
return selectPage(reqVO, new LambdaQueryWrapperX<TenantPackageDO>() |
|||
.likeIfPresent(TenantPackageDO::getName, reqVO.getName()) |
|||
.eqIfPresent(TenantPackageDO::getStatus, reqVO.getStatus()) |
|||
.likeIfPresent(TenantPackageDO::getRemark, reqVO.getRemark()) |
|||
.betweenIfPresent(TenantPackageDO::getCreateTime, reqVO.getCreateTime()) |
|||
.orderByDesc(TenantPackageDO::getId)); |
|||
} |
|||
|
|||
default List<TenantPackageDO> selectListByStatus(Integer status) { |
|||
return selectList(TenantPackageDO::getStatus, status); |
|||
} |
|||
|
|||
default TenantPackageDO selectByName(String name) { |
|||
return selectOne(TenantPackageDO::getName, name); |
|||
} |
|||
} |
|||
@ -1,72 +0,0 @@ |
|||
package cn.iocoder.yudao.module.system.service.tenant; |
|||
|
|||
import cn.iocoder.yudao.framework.common.pojo.PageResult; |
|||
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages.TenantPackagePageReqVO; |
|||
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages.TenantPackageSaveReqVO; |
|||
import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantPackageDO; |
|||
|
|||
import jakarta.validation.Valid; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 租户套餐 Service 接口 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
public interface TenantPackageService { |
|||
|
|||
/** |
|||
* 创建租户套餐 |
|||
* |
|||
* @param createReqVO 创建信息 |
|||
* @return 编号 |
|||
*/ |
|||
Long createTenantPackage(@Valid TenantPackageSaveReqVO createReqVO); |
|||
|
|||
/** |
|||
* 更新租户套餐 |
|||
* |
|||
* @param updateReqVO 更新信息 |
|||
*/ |
|||
void updateTenantPackage(@Valid TenantPackageSaveReqVO updateReqVO); |
|||
|
|||
/** |
|||
* 删除租户套餐 |
|||
* |
|||
* @param id 编号 |
|||
*/ |
|||
void deleteTenantPackage(Long id); |
|||
|
|||
/** |
|||
* 获得租户套餐 |
|||
* |
|||
* @param id 编号 |
|||
* @return 租户套餐 |
|||
*/ |
|||
TenantPackageDO getTenantPackage(Long id); |
|||
|
|||
/** |
|||
* 获得租户套餐分页 |
|||
* |
|||
* @param pageReqVO 分页查询 |
|||
* @return 租户套餐分页 |
|||
*/ |
|||
PageResult<TenantPackageDO> getTenantPackagePage(TenantPackagePageReqVO pageReqVO); |
|||
|
|||
/** |
|||
* 校验租户套餐 |
|||
* |
|||
* @param id 编号 |
|||
* @return 租户套餐 |
|||
*/ |
|||
TenantPackageDO validTenantPackage(Long id); |
|||
|
|||
/** |
|||
* 获得指定状态的租户套餐列表 |
|||
* |
|||
* @param status 状态 |
|||
* @return 租户套餐 |
|||
*/ |
|||
List<TenantPackageDO> getTenantPackageListByStatus(Integer status); |
|||
|
|||
} |
|||
@ -1,139 +0,0 @@ |
|||
package cn.iocoder.yudao.module.system.service.tenant; |
|||
|
|||
import cn.hutool.core.collection.CollUtil; |
|||
import cn.hutool.core.util.StrUtil; |
|||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
|||
import cn.iocoder.yudao.framework.common.pojo.PageResult; |
|||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
|||
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages.TenantPackagePageReqVO; |
|||
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.packages.TenantPackageSaveReqVO; |
|||
import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO; |
|||
import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantPackageDO; |
|||
import cn.iocoder.yudao.module.system.dal.mysql.tenant.TenantPackageMapper; |
|||
import com.baomidou.dynamic.datasource.annotation.DSTransactional; |
|||
import com.google.common.annotations.VisibleForTesting; |
|||
import org.springframework.context.annotation.Lazy; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.validation.annotation.Validated; |
|||
|
|||
import jakarta.annotation.Resource; |
|||
import java.util.List; |
|||
|
|||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
|||
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; |
|||
|
|||
/** |
|||
* 租户套餐 Service 实现类 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Service |
|||
@Validated |
|||
public class TenantPackageServiceImpl implements TenantPackageService { |
|||
|
|||
@Resource |
|||
private TenantPackageMapper tenantPackageMapper; |
|||
|
|||
@Resource |
|||
@Lazy // 避免循环依赖的报错
|
|||
private TenantService tenantService; |
|||
|
|||
@Override |
|||
public Long createTenantPackage(TenantPackageSaveReqVO createReqVO) { |
|||
// 校验套餐名是否重复
|
|||
validateTenantPackageNameUnique(null, createReqVO.getName()); |
|||
// 插入
|
|||
TenantPackageDO tenantPackage = BeanUtils.toBean(createReqVO, TenantPackageDO.class); |
|||
tenantPackageMapper.insert(tenantPackage); |
|||
// 返回
|
|||
return tenantPackage.getId(); |
|||
} |
|||
|
|||
@Override |
|||
@DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换
|
|||
public void updateTenantPackage(TenantPackageSaveReqVO updateReqVO) { |
|||
// 校验存在
|
|||
TenantPackageDO tenantPackage = validateTenantPackageExists(updateReqVO.getId()); |
|||
// 校验套餐名是否重复
|
|||
validateTenantPackageNameUnique(updateReqVO.getId(), updateReqVO.getName()); |
|||
// 更新
|
|||
TenantPackageDO updateObj = BeanUtils.toBean(updateReqVO, TenantPackageDO.class); |
|||
tenantPackageMapper.updateById(updateObj); |
|||
// 如果菜单发生变化,则修改每个租户的菜单
|
|||
if (!CollUtil.isEqualList(tenantPackage.getMenuIds(), updateReqVO.getMenuIds())) { |
|||
List<TenantDO> tenants = tenantService.getTenantListByPackageId(tenantPackage.getId()); |
|||
tenants.forEach(tenant -> tenantService.updateTenantRoleMenu(tenant.getId(), updateReqVO.getMenuIds())); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void deleteTenantPackage(Long id) { |
|||
// 校验存在
|
|||
validateTenantPackageExists(id); |
|||
// 校验正在使用
|
|||
validateTenantUsed(id); |
|||
// 删除
|
|||
tenantPackageMapper.deleteById(id); |
|||
} |
|||
|
|||
private TenantPackageDO validateTenantPackageExists(Long id) { |
|||
TenantPackageDO tenantPackage = tenantPackageMapper.selectById(id); |
|||
if (tenantPackage == null) { |
|||
throw exception(TENANT_PACKAGE_NOT_EXISTS); |
|||
} |
|||
return tenantPackage; |
|||
} |
|||
|
|||
private void validateTenantUsed(Long id) { |
|||
if (tenantService.getTenantCountByPackageId(id) > 0) { |
|||
throw exception(TENANT_PACKAGE_USED); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public TenantPackageDO getTenantPackage(Long id) { |
|||
return tenantPackageMapper.selectById(id); |
|||
} |
|||
|
|||
@Override |
|||
public PageResult<TenantPackageDO> getTenantPackagePage(TenantPackagePageReqVO pageReqVO) { |
|||
return tenantPackageMapper.selectPage(pageReqVO); |
|||
} |
|||
|
|||
@Override |
|||
public TenantPackageDO validTenantPackage(Long id) { |
|||
TenantPackageDO tenantPackage = tenantPackageMapper.selectById(id); |
|||
if (tenantPackage == null) { |
|||
throw exception(TENANT_PACKAGE_NOT_EXISTS); |
|||
} |
|||
if (tenantPackage.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { |
|||
throw exception(TENANT_PACKAGE_DISABLE, tenantPackage.getName()); |
|||
} |
|||
return tenantPackage; |
|||
} |
|||
|
|||
@Override |
|||
public List<TenantPackageDO> getTenantPackageListByStatus(Integer status) { |
|||
return tenantPackageMapper.selectListByStatus(status); |
|||
} |
|||
|
|||
|
|||
@VisibleForTesting |
|||
void validateTenantPackageNameUnique(Long id, String name) { |
|||
if (StrUtil.isBlank(name)) { |
|||
return; |
|||
} |
|||
TenantPackageDO tenantPackage = tenantPackageMapper.selectByName(name); |
|||
if (tenantPackage == null) { |
|||
return; |
|||
} |
|||
// 如果 id 为空,说明不用比较是否为相同 id 的用户
|
|||
if (id == null) { |
|||
throw exception(TENANT_PACKAGE_NAME_DUPLICATE); |
|||
} |
|||
if (!tenantPackage.getId().equals(id)) { |
|||
throw exception(TENANT_PACKAGE_NAME_DUPLICATE); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -1,138 +0,0 @@ |
|||
package cn.iocoder.yudao.module.system.service.tenant; |
|||
|
|||
import cn.iocoder.yudao.framework.common.pojo.PageResult; |
|||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; |
|||
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO; |
|||
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO; |
|||
import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO; |
|||
import cn.iocoder.yudao.module.system.service.tenant.handler.TenantInfoHandler; |
|||
import cn.iocoder.yudao.module.system.service.tenant.handler.TenantMenuHandler; |
|||
|
|||
import jakarta.validation.Valid; |
|||
import java.util.List; |
|||
import java.util.Set; |
|||
|
|||
/** |
|||
* 租户 Service 接口 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
public interface TenantService { |
|||
|
|||
/** |
|||
* 创建租户 |
|||
* |
|||
* @param createReqVO 创建信息 |
|||
* @return 编号 |
|||
*/ |
|||
Long createTenant(@Valid TenantSaveReqVO createReqVO); |
|||
|
|||
/** |
|||
* 更新租户 |
|||
* |
|||
* @param updateReqVO 更新信息 |
|||
*/ |
|||
void updateTenant(@Valid TenantSaveReqVO updateReqVO); |
|||
|
|||
/** |
|||
* 更新租户的角色菜单 |
|||
* |
|||
* @param tenantId 租户编号 |
|||
* @param menuIds 菜单编号数组 |
|||
*/ |
|||
void updateTenantRoleMenu(Long tenantId, Set<Long> menuIds); |
|||
|
|||
/** |
|||
* 删除租户 |
|||
* |
|||
* @param id 编号 |
|||
*/ |
|||
void deleteTenant(Long id); |
|||
|
|||
/** |
|||
* 获得租户 |
|||
* |
|||
* @param id 编号 |
|||
* @return 租户 |
|||
*/ |
|||
TenantDO getTenant(Long id); |
|||
|
|||
/** |
|||
* 获得租户分页 |
|||
* |
|||
* @param pageReqVO 分页查询 |
|||
* @return 租户分页 |
|||
*/ |
|||
PageResult<TenantDO> getTenantPage(TenantPageReqVO pageReqVO); |
|||
|
|||
/** |
|||
* 获得名字对应的租户 |
|||
* |
|||
* @param name 租户名 |
|||
* @return 租户 |
|||
*/ |
|||
TenantDO getTenantByName(String name); |
|||
|
|||
/** |
|||
* 获得域名对应的租户 |
|||
* |
|||
* @param website 域名 |
|||
* @return 租户 |
|||
*/ |
|||
TenantDO getTenantByWebsite(String website); |
|||
|
|||
/** |
|||
* 获得使用指定套餐的租户数量 |
|||
* |
|||
* @param packageId 租户套餐编号 |
|||
* @return 租户数量 |
|||
*/ |
|||
Long getTenantCountByPackageId(Long packageId); |
|||
|
|||
/** |
|||
* 获得使用指定套餐的租户数组 |
|||
* |
|||
* @param packageId 租户套餐编号 |
|||
* @return 租户数组 |
|||
*/ |
|||
List<TenantDO> getTenantListByPackageId(Long packageId); |
|||
|
|||
/** |
|||
* 获得指定状态的租户列表 |
|||
* |
|||
* @param status 状态 |
|||
* @return 租户列表 |
|||
*/ |
|||
List<TenantDO> getTenantListByStatus(Integer status); |
|||
|
|||
/** |
|||
* 进行租户的信息处理逻辑 |
|||
* 其中,租户编号从 {@link TenantContextHolder} 上下文中获取 |
|||
* |
|||
* @param handler 处理器 |
|||
*/ |
|||
void handleTenantInfo(TenantInfoHandler handler); |
|||
|
|||
/** |
|||
* 进行租户的菜单处理逻辑 |
|||
* 其中,租户编号从 {@link TenantContextHolder} 上下文中获取 |
|||
* |
|||
* @param handler 处理器 |
|||
*/ |
|||
void handleTenantMenu(TenantMenuHandler handler); |
|||
|
|||
/** |
|||
* 获得所有租户 |
|||
* |
|||
* @return 租户编号数组 |
|||
*/ |
|||
List<Long> getTenantIdList(); |
|||
|
|||
/** |
|||
* 校验租户是否合法 |
|||
* |
|||
* @param id 租户编号 |
|||
*/ |
|||
void validTenant(Long id); |
|||
|
|||
} |
|||
@ -1,311 +0,0 @@ |
|||
package cn.iocoder.yudao.module.system.service.tenant; |
|||
|
|||
import cn.hutool.core.collection.CollUtil; |
|||
import cn.hutool.core.lang.Assert; |
|||
import cn.hutool.core.util.ObjectUtil; |
|||
import cn.hutool.core.util.StrUtil; |
|||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
|||
import cn.iocoder.yudao.framework.common.pojo.PageResult; |
|||
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; |
|||
import cn.iocoder.yudao.framework.common.util.date.DateUtils; |
|||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
|||
import cn.iocoder.yudao.framework.tenant.config.TenantProperties; |
|||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; |
|||
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; |
|||
import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleSaveReqVO; |
|||
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO; |
|||
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO; |
|||
import cn.iocoder.yudao.module.system.convert.tenant.TenantConvert; |
|||
import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; |
|||
import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; |
|||
import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO; |
|||
import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantPackageDO; |
|||
import cn.iocoder.yudao.module.system.dal.mysql.tenant.TenantMapper; |
|||
import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum; |
|||
import cn.iocoder.yudao.module.system.enums.permission.RoleTypeEnum; |
|||
import cn.iocoder.yudao.module.system.service.permission.MenuService; |
|||
import cn.iocoder.yudao.module.system.service.permission.PermissionService; |
|||
import cn.iocoder.yudao.module.system.service.permission.RoleService; |
|||
import cn.iocoder.yudao.module.system.service.tenant.handler.TenantInfoHandler; |
|||
import cn.iocoder.yudao.module.system.service.tenant.handler.TenantMenuHandler; |
|||
import cn.iocoder.yudao.module.system.service.user.AdminUserService; |
|||
import com.baomidou.dynamic.datasource.annotation.DSTransactional; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.context.annotation.Lazy; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.validation.annotation.Validated; |
|||
|
|||
import jakarta.annotation.Resource; |
|||
import java.util.List; |
|||
import java.util.Objects; |
|||
import java.util.Set; |
|||
|
|||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
|||
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; |
|||
import static java.util.Collections.singleton; |
|||
|
|||
/** |
|||
* 租户 Service 实现类 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Service |
|||
@Validated |
|||
@Slf4j |
|||
public class TenantServiceImpl implements TenantService { |
|||
|
|||
@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") |
|||
@Autowired(required = false) // 由于 yudao.tenant.enable 配置项,可以关闭多租户的功能,所以这里只能不强制注入
|
|||
private TenantProperties tenantProperties; |
|||
|
|||
@Resource |
|||
private TenantMapper tenantMapper; |
|||
|
|||
@Resource |
|||
private TenantPackageService tenantPackageService; |
|||
@Resource |
|||
@Lazy // 延迟,避免循环依赖报错
|
|||
private AdminUserService userService; |
|||
@Resource |
|||
private RoleService roleService; |
|||
@Resource |
|||
private MenuService menuService; |
|||
@Resource |
|||
private PermissionService permissionService; |
|||
|
|||
@Override |
|||
public List<Long> getTenantIdList() { |
|||
List<TenantDO> tenants = tenantMapper.selectList(); |
|||
return CollectionUtils.convertList(tenants, TenantDO::getId); |
|||
} |
|||
|
|||
@Override |
|||
public void validTenant(Long id) { |
|||
TenantDO tenant = getTenant(id); |
|||
if (tenant == null) { |
|||
throw exception(TENANT_NOT_EXISTS); |
|||
} |
|||
if (tenant.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { |
|||
throw exception(TENANT_DISABLE, tenant.getName()); |
|||
} |
|||
if (DateUtils.isExpired(tenant.getExpireTime())) { |
|||
throw exception(TENANT_EXPIRE, tenant.getName()); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
@DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换
|
|||
public Long createTenant(TenantSaveReqVO createReqVO) { |
|||
// 校验租户名称是否重复
|
|||
validTenantNameDuplicate(createReqVO.getName(), null); |
|||
// 校验租户域名是否重复
|
|||
validTenantWebsiteDuplicate(createReqVO.getWebsite(), null); |
|||
// 校验套餐被禁用
|
|||
TenantPackageDO tenantPackage = tenantPackageService.validTenantPackage(createReqVO.getPackageId()); |
|||
|
|||
// 创建租户
|
|||
TenantDO tenant = BeanUtils.toBean(createReqVO, TenantDO.class); |
|||
tenantMapper.insert(tenant); |
|||
// 创建租户的管理员
|
|||
TenantUtils.execute(tenant.getId(), () -> { |
|||
// 创建角色
|
|||
Long roleId = createRole(tenantPackage); |
|||
// 创建用户,并分配角色
|
|||
Long userId = createUser(roleId, createReqVO); |
|||
// 修改租户的管理员
|
|||
tenantMapper.updateById(new TenantDO().setId(tenant.getId()).setContactUserId(userId)); |
|||
}); |
|||
return tenant.getId(); |
|||
} |
|||
|
|||
private Long createUser(Long roleId, TenantSaveReqVO createReqVO) { |
|||
// 创建用户
|
|||
Long userId = userService.createUser(TenantConvert.INSTANCE.convert02(createReqVO)); |
|||
// 分配角色
|
|||
permissionService.assignUserRole(userId, singleton(roleId)); |
|||
return userId; |
|||
} |
|||
|
|||
private Long createRole(TenantPackageDO tenantPackage) { |
|||
// 创建角色
|
|||
RoleSaveReqVO reqVO = new RoleSaveReqVO(); |
|||
reqVO.setName(RoleCodeEnum.TENANT_ADMIN.getName()).setCode(RoleCodeEnum.TENANT_ADMIN.getCode()) |
|||
.setSort(0).setRemark("系统自动生成"); |
|||
Long roleId = roleService.createRole(reqVO, RoleTypeEnum.SYSTEM.getType()); |
|||
// 分配权限
|
|||
permissionService.assignRoleMenu(roleId, tenantPackage.getMenuIds()); |
|||
return roleId; |
|||
} |
|||
|
|||
@Override |
|||
@DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换
|
|||
public void updateTenant(TenantSaveReqVO updateReqVO) { |
|||
// 校验存在
|
|||
TenantDO tenant = validateUpdateTenant(updateReqVO.getId()); |
|||
// 校验租户名称是否重复
|
|||
validTenantNameDuplicate(updateReqVO.getName(), updateReqVO.getId()); |
|||
// 校验租户域名是否重复
|
|||
validTenantWebsiteDuplicate(updateReqVO.getWebsite(), updateReqVO.getId()); |
|||
// 校验套餐被禁用
|
|||
TenantPackageDO tenantPackage = tenantPackageService.validTenantPackage(updateReqVO.getPackageId()); |
|||
|
|||
// 更新租户
|
|||
TenantDO updateObj = BeanUtils.toBean(updateReqVO, TenantDO.class); |
|||
tenantMapper.updateById(updateObj); |
|||
// 如果套餐发生变化,则修改其角色的权限
|
|||
if (ObjectUtil.notEqual(tenant.getPackageId(), updateReqVO.getPackageId())) { |
|||
updateTenantRoleMenu(tenant.getId(), tenantPackage.getMenuIds()); |
|||
} |
|||
} |
|||
|
|||
private void validTenantNameDuplicate(String name, Long id) { |
|||
TenantDO tenant = tenantMapper.selectByName(name); |
|||
if (tenant == null) { |
|||
return; |
|||
} |
|||
// 如果 id 为空,说明不用比较是否为相同名字的租户
|
|||
if (id == null) { |
|||
throw exception(TENANT_NAME_DUPLICATE, name); |
|||
} |
|||
if (!tenant.getId().equals(id)) { |
|||
throw exception(TENANT_NAME_DUPLICATE, name); |
|||
} |
|||
} |
|||
|
|||
private void validTenantWebsiteDuplicate(String website, Long id) { |
|||
if (StrUtil.isEmpty(website)) { |
|||
return; |
|||
} |
|||
TenantDO tenant = tenantMapper.selectByWebsite(website); |
|||
if (tenant == null) { |
|||
return; |
|||
} |
|||
// 如果 id 为空,说明不用比较是否为相同名字的租户
|
|||
if (id == null) { |
|||
throw exception(TENANT_WEBSITE_DUPLICATE, website); |
|||
} |
|||
if (!tenant.getId().equals(id)) { |
|||
throw exception(TENANT_WEBSITE_DUPLICATE, website); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
@DSTransactional |
|||
public void updateTenantRoleMenu(Long tenantId, Set<Long> menuIds) { |
|||
TenantUtils.execute(tenantId, () -> { |
|||
// 获得所有角色
|
|||
List<RoleDO> roles = roleService.getRoleList(); |
|||
roles.forEach(role -> Assert.isTrue(tenantId.equals(role.getTenantId()), "角色({}/{}) 租户不匹配", |
|||
role.getId(), role.getTenantId(), tenantId)); // 兜底校验
|
|||
// 重新分配每个角色的权限
|
|||
roles.forEach(role -> { |
|||
// 如果是租户管理员,重新分配其权限为租户套餐的权限
|
|||
if (Objects.equals(role.getCode(), RoleCodeEnum.TENANT_ADMIN.getCode())) { |
|||
permissionService.assignRoleMenu(role.getId(), menuIds); |
|||
log.info("[updateTenantRoleMenu][租户管理员({}/{}) 的权限修改为({})]", role.getId(), role.getTenantId(), menuIds); |
|||
return; |
|||
} |
|||
// 如果是其他角色,则去掉超过套餐的权限
|
|||
Set<Long> roleMenuIds = permissionService.getRoleMenuListByRoleId(role.getId()); |
|||
roleMenuIds = CollUtil.intersectionDistinct(roleMenuIds, menuIds); |
|||
permissionService.assignRoleMenu(role.getId(), roleMenuIds); |
|||
log.info("[updateTenantRoleMenu][角色({}/{}) 的权限修改为({})]", role.getId(), role.getTenantId(), roleMenuIds); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
@Override |
|||
public void deleteTenant(Long id) { |
|||
// 校验存在
|
|||
validateUpdateTenant(id); |
|||
// 删除
|
|||
tenantMapper.deleteById(id); |
|||
} |
|||
|
|||
private TenantDO validateUpdateTenant(Long id) { |
|||
TenantDO tenant = tenantMapper.selectById(id); |
|||
if (tenant == null) { |
|||
throw exception(TENANT_NOT_EXISTS); |
|||
} |
|||
// 内置租户,不允许删除
|
|||
if (isSystemTenant(tenant)) { |
|||
throw exception(TENANT_CAN_NOT_UPDATE_SYSTEM); |
|||
} |
|||
return tenant; |
|||
} |
|||
|
|||
@Override |
|||
public TenantDO getTenant(Long id) { |
|||
return tenantMapper.selectById(id); |
|||
} |
|||
|
|||
@Override |
|||
public PageResult<TenantDO> getTenantPage(TenantPageReqVO pageReqVO) { |
|||
return tenantMapper.selectPage(pageReqVO); |
|||
} |
|||
|
|||
@Override |
|||
public TenantDO getTenantByName(String name) { |
|||
return tenantMapper.selectByName(name); |
|||
} |
|||
|
|||
@Override |
|||
public TenantDO getTenantByWebsite(String website) { |
|||
return tenantMapper.selectByWebsite(website); |
|||
} |
|||
|
|||
@Override |
|||
public Long getTenantCountByPackageId(Long packageId) { |
|||
return tenantMapper.selectCountByPackageId(packageId); |
|||
} |
|||
|
|||
@Override |
|||
public List<TenantDO> getTenantListByPackageId(Long packageId) { |
|||
return tenantMapper.selectListByPackageId(packageId); |
|||
} |
|||
|
|||
@Override |
|||
public List<TenantDO> getTenantListByStatus(Integer status) { |
|||
return tenantMapper.selectListByStatus(status); |
|||
} |
|||
|
|||
@Override |
|||
public void handleTenantInfo(TenantInfoHandler handler) { |
|||
// 如果禁用,则不执行逻辑
|
|||
if (isTenantDisable()) { |
|||
return; |
|||
} |
|||
// 获得租户
|
|||
TenantDO tenant = getTenant(TenantContextHolder.getRequiredTenantId()); |
|||
// 执行处理器
|
|||
handler.handle(tenant); |
|||
} |
|||
|
|||
@Override |
|||
public void handleTenantMenu(TenantMenuHandler handler) { |
|||
// 如果禁用,则不执行逻辑
|
|||
if (isTenantDisable()) { |
|||
return; |
|||
} |
|||
// 获得租户,然后获得菜单
|
|||
TenantDO tenant = getTenant(TenantContextHolder.getRequiredTenantId()); |
|||
Set<Long> menuIds; |
|||
if (isSystemTenant(tenant)) { // 系统租户,菜单是全量的
|
|||
menuIds = CollectionUtils.convertSet(menuService.getMenuList(), MenuDO::getId); |
|||
} else { |
|||
menuIds = tenantPackageService.getTenantPackage(tenant.getPackageId()).getMenuIds(); |
|||
} |
|||
// 执行处理器
|
|||
handler.handle(menuIds); |
|||
} |
|||
|
|||
private static boolean isSystemTenant(TenantDO tenant) { |
|||
return Objects.equals(tenant.getPackageId(), TenantDO.PACKAGE_ID_SYSTEM); |
|||
} |
|||
|
|||
private boolean isTenantDisable() { |
|||
return tenantProperties == null || Boolean.FALSE.equals(tenantProperties.getEnable()); |
|||
} |
|||
|
|||
} |
|||
@ -1,21 +0,0 @@ |
|||
package cn.iocoder.yudao.module.system.service.tenant.handler; |
|||
|
|||
import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO; |
|||
|
|||
/** |
|||
* 租户信息处理 |
|||
* 目的:尽量减少租户逻辑耦合到系统中 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
public interface TenantInfoHandler { |
|||
|
|||
/** |
|||
* 基于传入的租户信息,进行相关逻辑的执行 |
|||
* 例如说,创建用户时,超过最大账户配额 |
|||
* |
|||
* @param tenant 租户信息 |
|||
*/ |
|||
void handle(TenantDO tenant); |
|||
|
|||
} |
|||
@ -1,21 +0,0 @@ |
|||
package cn.iocoder.yudao.module.system.service.tenant.handler; |
|||
|
|||
import java.util.Set; |
|||
|
|||
/** |
|||
* 租户菜单处理 |
|||
* 目的:尽量减少租户逻辑耦合到系统中 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
public interface TenantMenuHandler { |
|||
|
|||
/** |
|||
* 基于传入的租户菜单【全】列表,进行相关逻辑的执行 |
|||
* 例如说,返回可分配菜单的时候,可以移除多余的 |
|||
* |
|||
* @param menuIds 菜单列表 |
|||
*/ |
|||
void handle(Set<Long> menuIds); |
|||
|
|||
} |
|||
Loading…
Reference in new issue