最新公众号管理平台后端
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4" />
|
||||
72
joju-framework/joju-spring-boot-starter-biz-tenant/pom.xml
Normal file
72
joju-framework/joju-spring-boot-starter-biz-tenant/pom.xml
Normal file
@@ -0,0 +1,72 @@
|
||||
<?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>com.jojubanking.boot</groupId>
|
||||
<artifactId>joju-framework</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>joju-spring-boot-starter-biz-tenant</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>多租户</description>
|
||||
<url>https://www.jojubanking.com</url>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.jojubanking.boot</groupId>
|
||||
<artifactId>joju-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Web 相关 -->
|
||||
<dependency>
|
||||
<groupId>com.jojubanking.boot</groupId>
|
||||
<artifactId>joju-spring-boot-starter-security</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- DB 相关 -->
|
||||
<dependency>
|
||||
<groupId>com.jojubanking.boot</groupId>
|
||||
<artifactId>joju-spring-boot-starter-mybatis</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.jojubanking.boot</groupId>
|
||||
<artifactId>joju-spring-boot-starter-redis</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Job 定时任务相关 -->
|
||||
<dependency>
|
||||
<groupId>com.jojubanking.boot</groupId>
|
||||
<artifactId>joju-spring-boot-starter-job</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 消息队列相关 -->
|
||||
<dependency>
|
||||
<groupId>com.jojubanking.boot</groupId>
|
||||
<artifactId>joju-spring-boot-starter-mq</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Test 测试相关 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 工具类相关 -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.jojubanking.boot.framework.tenant.config;
|
||||
|
||||
import cn.hutool.core.annotation.AnnotationUtil;
|
||||
import com.jojubanking.boot.framework.common.enums.WebFilterOrderEnum;
|
||||
import com.jojubanking.boot.framework.mybatis.core.util.MyBatisUtils;
|
||||
import com.jojubanking.boot.framework.quartz.core.handler.JobHandler;
|
||||
import com.jojubanking.boot.framework.tenant.core.aop.TenantIgnoreAspect;
|
||||
import com.jojubanking.boot.framework.tenant.core.db.TenantDatabaseInterceptor;
|
||||
import com.jojubanking.boot.framework.tenant.core.job.TenantJob;
|
||||
import com.jojubanking.boot.framework.tenant.core.job.TenantJobHandlerDecorator;
|
||||
import com.jojubanking.boot.framework.tenant.core.mq.TenantRedisMessageInterceptor;
|
||||
import com.jojubanking.boot.framework.tenant.core.security.TenantSecurityWebFilter;
|
||||
import com.jojubanking.boot.framework.tenant.core.service.TenantFrameworkService;
|
||||
import com.jojubanking.boot.framework.tenant.core.service.TenantFrameworkServiceImpl;
|
||||
import com.jojubanking.boot.framework.tenant.core.web.TenantContextWebFilter;
|
||||
import com.jojubanking.boot.framework.web.config.WebProperties;
|
||||
import com.jojubanking.boot.framework.web.core.handler.GlobalExceptionHandler;
|
||||
import com.jojubanking.boot.module.system.api.tenant.TenantApi;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
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.Configuration;
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "joju.tenant", value = "enable", matchIfMissing = true) // 允许使用 joju.tenant.enable=false 禁用多租户
|
||||
@EnableConfigurationProperties(TenantProperties.class)
|
||||
public class JojuTenantAutoConfiguration {
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
// ========== Job ==========
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
|
||||
public BeanPostProcessor jobHandlerBeanPostProcessor(TenantFrameworkService tenantFrameworkService) {
|
||||
return new BeanPostProcessor() {
|
||||
|
||||
@Override
|
||||
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (!(bean instanceof JobHandler)) {
|
||||
return bean;
|
||||
}
|
||||
// 有 TenantJob 注解的情况下,才会进行处理
|
||||
if (!AnnotationUtil.hasAnnotation(bean.getClass(), TenantJob.class)) {
|
||||
return bean;
|
||||
}
|
||||
|
||||
// 使用 TenantJobHandlerDecorator 装饰
|
||||
return new TenantJobHandlerDecorator(tenantFrameworkService, (JobHandler) bean);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.jojubanking.boot.framework.tenant.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 多租户配置
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "joju.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();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.jojubanking.boot.framework.tenant.core.aop;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 忽略租户,标记指定方法不进行租户的自动过滤
|
||||
*
|
||||
* 注意,只有 DB 的场景会过滤,其它场景暂时不过滤:
|
||||
* 1、Redis 场景:因为是基于 Key 实现多租户的能力,所以忽略没有意义,不像 DB 是一个 column 实现的
|
||||
* 2、MQ 场景:有点难以抉择,目前可以通过 Consumer 手动在消费的方法上,添加 @TenantIgnore 进行忽略
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@Target({ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
public @interface TenantIgnore {
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.jojubanking.boot.framework.tenant.core.aop;
|
||||
|
||||
import com.jojubanking.boot.framework.tenant.core.context.TenantContextHolder;
|
||||
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} 注解实现,用于一些全局的逻辑。
|
||||
* 例如说,一个定时任务,读取所有数据,进行处理。
|
||||
* 又例如说,读取所有数据,进行缓存。
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.jojubanking.boot.framework.tenant.core.context;
|
||||
|
||||
import com.alibaba.ttl.TransmittableThreadLocal;
|
||||
|
||||
/**
|
||||
* 多租户上下文 Holder
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
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 不存在租户编号"); // TODO TW:增加文档链接
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.jojubanking.boot.framework.tenant.core.db;
|
||||
|
||||
import com.jojubanking.boot.framework.mybatis.core.dataobject.BaseDO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* 拓展多租户的 BaseDO 基类
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Accessors(chain = true)
|
||||
public abstract class TenantBaseDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 多租户编号
|
||||
*/
|
||||
private Long tenantId;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.jojubanking.boot.framework.tenant.core.db;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.jojubanking.boot.framework.tenant.config.TenantProperties;
|
||||
import com.jojubanking.boot.framework.tenant.core.context.TenantContextHolder;
|
||||
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.LongValue;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 基于 MyBatis Plus 多租户的功能,实现 DB 层面的多租户的功能
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
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, tableName); // 情况二,忽略多租户的表
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.jojubanking.boot.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.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface TenantJob {
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.jojubanking.boot.framework.tenant.core.job;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.jojubanking.boot.framework.common.util.json.JsonUtils;
|
||||
import com.jojubanking.boot.framework.quartz.core.handler.JobHandler;
|
||||
import com.jojubanking.boot.framework.tenant.core.context.TenantContextHolder;
|
||||
import com.jojubanking.boot.framework.tenant.core.service.TenantFrameworkService;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 多租户 JobHandler 装饰器
|
||||
* 任务执行时,会按照租户逐个执行 Job 的逻辑
|
||||
*
|
||||
* 注意,需要保证 JobHandler 的幂等性。因为 Job 因为某个租户执行失败重试时,之前执行成功的租户也会再次执行。
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public class TenantJobHandlerDecorator implements JobHandler {
|
||||
|
||||
private final TenantFrameworkService tenantFrameworkService;
|
||||
/**
|
||||
* 被装饰的 Job
|
||||
*/
|
||||
private final JobHandler jobHandler;
|
||||
|
||||
@Override
|
||||
public final String execute(String param) throws Exception {
|
||||
// 获得租户列表
|
||||
List<Long> tenantIds = tenantFrameworkService.getTenantIds();
|
||||
if (CollUtil.isEmpty(tenantIds)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 逐个租户,执行 Job
|
||||
Map<Long, String> results = new ConcurrentHashMap<>();
|
||||
tenantIds.parallelStream().forEach(tenantId -> { // TODO TW:先通过 parallel 实现并行;1)多个租户,是一条执行日志;2)异常的情况
|
||||
try {
|
||||
// 设置租户
|
||||
TenantContextHolder.setTenantId(tenantId);
|
||||
// 执行 Job
|
||||
String result = jobHandler.execute(param);
|
||||
// 添加结果
|
||||
results.put(tenantId, result);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
TenantContextHolder.clear();
|
||||
}
|
||||
});
|
||||
return JsonUtils.toJsonString(results);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.jojubanking.boot.framework.tenant.core.mq;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.jojubanking.boot.framework.mq.core.interceptor.RedisMessageInterceptor;
|
||||
import com.jojubanking.boot.framework.mq.core.message.AbstractRedisMessage;
|
||||
import com.jojubanking.boot.framework.tenant.core.context.TenantContextHolder;
|
||||
|
||||
import static com.jojubanking.boot.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
|
||||
|
||||
/**
|
||||
* 多租户 {@link AbstractRedisMessage} 拦截器
|
||||
*
|
||||
* 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
|
||||
* 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.jojubanking.boot.framework.tenant.core.redis;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.jojubanking.boot.framework.redis.core.RedisKeyDefine;
|
||||
import com.jojubanking.boot.framework.tenant.core.context.TenantContextHolder;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* 多租户拓展的 RedisKeyDefine 实现类
|
||||
*
|
||||
* 由于 Redis 不同于 MySQL 有 column 字段,无法通过类似 WHERE tenant_id = ? 的方式过滤
|
||||
* 所以需要通过在 Redis Key 上增加后缀的方式,进行租户之间的隔离。具体的步骤是:
|
||||
* 1. 假设 Redis Key 是 user:%d,示例是 user:1;对应到多租户的 Redis Key 是 user:%d:%d,
|
||||
* 2. 在 Redis DAO 中,需要使用 {@link #formatKey(Object...)} 方法,进行 Redis Key 的格式化
|
||||
*
|
||||
* 注意,大多数情况下,并不用使用 TenantRedisKeyDefine 实现。主要的使用场景,还是 Redis Key 可能存在冲突的情况。
|
||||
* 例如说,租户 1 和 2 都有一个手机号作为 Key,则他们会存在冲突的问题
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
public class TenantRedisKeyDefine extends RedisKeyDefine {
|
||||
|
||||
/**
|
||||
* 多租户的 KEY 模板
|
||||
*/
|
||||
private static final String KEY_TEMPLATE_SUFFIX = ":%d";
|
||||
|
||||
public TenantRedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, Duration timeout) {
|
||||
super(memo, buildKeyTemplate(keyTemplate), keyType, valueType, timeout);
|
||||
}
|
||||
|
||||
public TenantRedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, TimeoutTypeEnum timeoutType) {
|
||||
super(memo, buildKeyTemplate(keyTemplate), keyType, valueType, timeoutType);
|
||||
}
|
||||
|
||||
private static String buildKeyTemplate(String keyTemplate) {
|
||||
return keyTemplate + KEY_TEMPLATE_SUFFIX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String formatKey(Object... args) {
|
||||
args = ArrayUtil.append(args, TenantContextHolder.getRequiredTenantId());
|
||||
return super.formatKey(args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.jojubanking.boot.framework.tenant.core.security;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.jojubanking.boot.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import com.jojubanking.boot.framework.common.pojo.CommonResult;
|
||||
import com.jojubanking.boot.framework.common.util.servlet.ServletUtils;
|
||||
import com.jojubanking.boot.framework.security.core.LoginUser;
|
||||
import com.jojubanking.boot.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.jojubanking.boot.framework.tenant.config.TenantProperties;
|
||||
import com.jojubanking.boot.framework.tenant.core.context.TenantContextHolder;
|
||||
import com.jojubanking.boot.framework.tenant.core.service.TenantFrameworkService;
|
||||
import com.jojubanking.boot.framework.web.config.WebProperties;
|
||||
import com.jojubanking.boot.framework.web.core.filter.ApiRequestFilter;
|
||||
import com.jojubanking.boot.framework.web.core.handler.GlobalExceptionHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 多租户 Security Web 过滤器
|
||||
* 1. 如果是登陆的用户,校验是否有权限访问该租户,避免越权问题。
|
||||
* 2. 如果请求未带租户的编号,检查是否是忽略的 URL,否则也不允许访问。
|
||||
* 3. 校验租户是合法,例如说被禁用、到期
|
||||
*
|
||||
* 校验用户访问的租户,是否是其所在的租户,
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.jojubanking.boot.framework.tenant.core.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Tenant 框架 Service 接口,定义获取租户信息
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
public interface TenantFrameworkService {
|
||||
|
||||
/**
|
||||
* 获得所有租户
|
||||
*
|
||||
* @return 租户编号数组
|
||||
*/
|
||||
List<Long> getTenantIds();
|
||||
|
||||
/**
|
||||
* 校验租户是否合法
|
||||
*
|
||||
* @param id 租户编号
|
||||
*/
|
||||
void validTenant(Long id);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.jojubanking.boot.framework.tenant.core.service;
|
||||
|
||||
import com.jojubanking.boot.framework.common.exception.ServiceException;
|
||||
import com.jojubanking.boot.framework.common.util.cache.CacheUtils;
|
||||
import com.jojubanking.boot.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 TW
|
||||
*/
|
||||
@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.getTenantIds();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* 针对 {@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.validTenant(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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.jojubanking.boot.framework.tenant.core.util;
|
||||
|
||||
import com.jojubanking.boot.framework.tenant.core.context.TenantContextHolder;
|
||||
|
||||
/**
|
||||
* 多租户 Util
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.jojubanking.boot.framework.tenant.core.web;
|
||||
|
||||
import com.jojubanking.boot.framework.tenant.core.context.TenantContextHolder;
|
||||
import com.jojubanking.boot.framework.web.core.util.WebFrameworkUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 多租户 Context Web 过滤器
|
||||
* 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号。
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 多租户,支持如下层面:
|
||||
* 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 com.jojubanking.boot.framework.quartz.config.JojuAsyncAutoConfiguration#threadPoolTaskExecutorBeanPostProcessor()}
|
||||
* 2)Spring Security:
|
||||
* TransmittableThreadLocalSecurityContextHolderStrategy
|
||||
* 和 JojuSecurityAutoConfiguration#securityContextHolderMethodInvokingFactoryBean() 方法
|
||||
*
|
||||
*/
|
||||
package com.jojubanking.boot.framework.tenant;
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
com.jojubanking.boot.framework.tenant.config.JojuTenantAutoConfiguration
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.jojubanking.boot.framework.tenant.core.redis;
|
||||
|
||||
import com.jojubanking.boot.framework.redis.core.RedisKeyDefine;
|
||||
import com.jojubanking.boot.framework.tenant.core.context.TenantContextHolder;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class TenantRedisKeyDefineTest {
|
||||
|
||||
@Test
|
||||
public void testFormatKey() {
|
||||
Long tenantId = 30L;
|
||||
TenantContextHolder.setTenantId(tenantId);
|
||||
// 准备参数
|
||||
TenantRedisKeyDefine define = new TenantRedisKeyDefine("", "user:%d:%d", RedisKeyDefine.KeyTypeEnum.HASH,
|
||||
Object.class, RedisKeyDefine.TimeoutTypeEnum.FIXED);
|
||||
Long userId = 10L;
|
||||
Integer userType = 1;
|
||||
|
||||
// 调用
|
||||
String key = define.formatKey(userId, userType);
|
||||
// 断言
|
||||
assertEquals("user:10:1:30", key);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
com.jojubanking.boot.framework.tenant.config.JojuTenantAutoConfiguration
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
#Generated by Maven
|
||||
#Wed Jan 14 17:48:36 CST 2026
|
||||
version=2.0.0-beta
|
||||
groupId=com.jojubanking.boot
|
||||
artifactId=joju-spring-boot-starter-biz-tenant
|
||||
@@ -0,0 +1,19 @@
|
||||
com\jojubanking\boot\framework\tenant\core\aop\TenantIgnore.class
|
||||
com\jojubanking\boot\framework\tenant\core\db\TenantDatabaseInterceptor.class
|
||||
com\jojubanking\boot\framework\tenant\config\TenantProperties.class
|
||||
com\jojubanking\boot\framework\tenant\core\service\TenantFrameworkServiceImpl.class
|
||||
com\jojubanking\boot\framework\tenant\config\JojuTenantAutoConfiguration$1.class
|
||||
com\jojubanking\boot\framework\tenant\core\mq\TenantRedisMessageInterceptor.class
|
||||
com\jojubanking\boot\framework\tenant\core\service\TenantFrameworkServiceImpl$1.class
|
||||
com\jojubanking\boot\framework\tenant\core\web\TenantContextWebFilter.class
|
||||
com\jojubanking\boot\framework\tenant\core\service\TenantFrameworkServiceImpl$2.class
|
||||
com\jojubanking\boot\framework\tenant\core\util\TenantUtils.class
|
||||
com\jojubanking\boot\framework\tenant\config\JojuTenantAutoConfiguration.class
|
||||
com\jojubanking\boot\framework\tenant\core\db\TenantBaseDO.class
|
||||
com\jojubanking\boot\framework\tenant\core\security\TenantSecurityWebFilter.class
|
||||
com\jojubanking\boot\framework\tenant\core\job\TenantJob.class
|
||||
com\jojubanking\boot\framework\tenant\core\context\TenantContextHolder.class
|
||||
com\jojubanking\boot\framework\tenant\core\job\TenantJobHandlerDecorator.class
|
||||
com\jojubanking\boot\framework\tenant\core\service\TenantFrameworkService.class
|
||||
com\jojubanking\boot\framework\tenant\core\redis\TenantRedisKeyDefine.class
|
||||
com\jojubanking\boot\framework\tenant\core\aop\TenantIgnoreAspect.class
|
||||
@@ -0,0 +1,17 @@
|
||||
D:\workspace\nxwj\掌医管理平台\jojuboot\joju-framework\joju-spring-boot-starter-biz-tenant\src\main\java\com\jojubanking\boot\framework\tenant\core\context\TenantContextHolder.java
|
||||
D:\workspace\nxwj\掌医管理平台\jojuboot\joju-framework\joju-spring-boot-starter-biz-tenant\src\main\java\com\jojubanking\boot\framework\tenant\core\util\TenantUtils.java
|
||||
D:\workspace\nxwj\掌医管理平台\jojuboot\joju-framework\joju-spring-boot-starter-biz-tenant\src\main\java\com\jojubanking\boot\framework\tenant\package-info.java
|
||||
D:\workspace\nxwj\掌医管理平台\jojuboot\joju-framework\joju-spring-boot-starter-biz-tenant\src\main\java\com\jojubanking\boot\framework\tenant\core\db\TenantBaseDO.java
|
||||
D:\workspace\nxwj\掌医管理平台\jojuboot\joju-framework\joju-spring-boot-starter-biz-tenant\src\main\java\com\jojubanking\boot\framework\tenant\core\security\TenantSecurityWebFilter.java
|
||||
D:\workspace\nxwj\掌医管理平台\jojuboot\joju-framework\joju-spring-boot-starter-biz-tenant\src\main\java\com\jojubanking\boot\framework\tenant\core\aop\TenantIgnoreAspect.java
|
||||
D:\workspace\nxwj\掌医管理平台\jojuboot\joju-framework\joju-spring-boot-starter-biz-tenant\src\main\java\com\jojubanking\boot\framework\tenant\core\service\TenantFrameworkService.java
|
||||
D:\workspace\nxwj\掌医管理平台\jojuboot\joju-framework\joju-spring-boot-starter-biz-tenant\src\main\java\com\jojubanking\boot\framework\tenant\config\JojuTenantAutoConfiguration.java
|
||||
D:\workspace\nxwj\掌医管理平台\jojuboot\joju-framework\joju-spring-boot-starter-biz-tenant\src\main\java\com\jojubanking\boot\framework\tenant\core\job\TenantJobHandlerDecorator.java
|
||||
D:\workspace\nxwj\掌医管理平台\jojuboot\joju-framework\joju-spring-boot-starter-biz-tenant\src\main\java\com\jojubanking\boot\framework\tenant\core\job\TenantJob.java
|
||||
D:\workspace\nxwj\掌医管理平台\jojuboot\joju-framework\joju-spring-boot-starter-biz-tenant\src\main\java\com\jojubanking\boot\framework\tenant\core\db\TenantDatabaseInterceptor.java
|
||||
D:\workspace\nxwj\掌医管理平台\jojuboot\joju-framework\joju-spring-boot-starter-biz-tenant\src\main\java\com\jojubanking\boot\framework\tenant\config\TenantProperties.java
|
||||
D:\workspace\nxwj\掌医管理平台\jojuboot\joju-framework\joju-spring-boot-starter-biz-tenant\src\main\java\com\jojubanking\boot\framework\tenant\core\service\TenantFrameworkServiceImpl.java
|
||||
D:\workspace\nxwj\掌医管理平台\jojuboot\joju-framework\joju-spring-boot-starter-biz-tenant\src\main\java\com\jojubanking\boot\framework\tenant\core\aop\TenantIgnore.java
|
||||
D:\workspace\nxwj\掌医管理平台\jojuboot\joju-framework\joju-spring-boot-starter-biz-tenant\src\main\java\com\jojubanking\boot\framework\tenant\core\redis\TenantRedisKeyDefine.java
|
||||
D:\workspace\nxwj\掌医管理平台\jojuboot\joju-framework\joju-spring-boot-starter-biz-tenant\src\main\java\com\jojubanking\boot\framework\tenant\core\web\TenantContextWebFilter.java
|
||||
D:\workspace\nxwj\掌医管理平台\jojuboot\joju-framework\joju-spring-boot-starter-biz-tenant\src\main\java\com\jojubanking\boot\framework\tenant\core\mq\TenantRedisMessageInterceptor.java
|
||||
@@ -0,0 +1 @@
|
||||
com\jojubanking\boot\framework\tenant\core\redis\TenantRedisKeyDefineTest.class
|
||||
@@ -0,0 +1 @@
|
||||
D:\workspace\nxwj\掌医管理平台\jojuboot\joju-framework\joju-spring-boot-starter-biz-tenant\src\test\java\com\jojubanking\boot\framework\tenant\core\redis\TenantRedisKeyDefineTest.java
|
||||
Binary file not shown.
Reference in New Issue
Block a user