!286 合并 多租户功能

* add 新增 ruoyi-common-tenant 多租户模块 全框架适配多租户改动
* update 优化 隐藏页面主键
* remove 移除 缓存列表功能(多租户缓存功能繁杂多样 没有办法在页面管理)
* update 重构 全局缓存KEY 与 常用缓存KEY做区分
* update 重构 OssFactory 加载方式 改为每次比对配置做实例更新
* update 优化 SaTokenDao 改为 Bean 注入 便于扩展
* update 重构 项目初始化数据改为懒加载 不提供热加载
* update 重构 验证码开关使用配置文件(经调查少有动态开启需求)
* update 优化 启用 sqlserver 高版本语法 简化sql脚本语法
* update 优化 DataPermissionHelper 增加 开启/关闭 忽略数据权限功能
* update 优化 连接池增加 keepaliveTime 探活参数
* update 优化 调整连接池最长生命周期 防止出现警告
* update 优化 代码生成页面模板 校验不必要的表单数据
* add 新增 StringUtils splitTo 与 splitList 方法 优化业务代码
This commit is contained in:
疯狂的狮子Li
2023-02-16 09:06:10 +00:00
parent a8d5644f2e
commit 45ac0f23e1
187 changed files with 6486 additions and 2372 deletions

View File

@ -31,6 +31,7 @@
<module>ruoyi-common-sensitive</module>
<module>ruoyi-common-json</module>
<module>ruoyi-common-encrypt</module>
<module>ruoyi-common-tenant</module>
</modules>
<artifactId>ruoyi-common</artifactId>

View File

@ -152,6 +152,13 @@
<version>${revision}</version>
</dependency>
<!-- 租户模块 -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-tenant</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@ -36,11 +36,6 @@ public class RuoYiConfig {
*/
private boolean demoEnabled;
/**
* 缓存懒加载
*/
private boolean cacheLazy;
/**
* 获取地址开关
*/

View File

@ -3,25 +3,15 @@ package com.ruoyi.common.core.constant;
/**
* 缓存的key 常量
*
* @author ruoyi
* @author Lion Li
*/
public interface CacheConstants {
/**
* 登录用户 redis key
*/
String LOGIN_TOKEN_KEY = "Authorization:login:token:";
/**
* 在线用户 redis key
*/
String ONLINE_TOKEN_KEY = "online_tokens:";
/**
* 验证码 redis key
*/
String CAPTCHA_CODE_KEY = "captcha_codes:";
/**
* 参数管理 cache key
*/
@ -32,18 +22,4 @@ public interface CacheConstants {
*/
String SYS_DICT_KEY = "sys_dict:";
/**
* 防重提交 redis key
*/
String REPEAT_SUBMIT_KEY = "repeat_submit:";
/**
* 限流 redis key
*/
String RATE_LIMIT_KEY = "rate_limit:";
/**
* 登录账户密码错误次数 redis key
*/
String PWD_ERR_CNT_KEY = "pwd_err_cnt:";
}

View File

@ -30,6 +30,11 @@ public interface CacheNames {
*/
String SYS_DICT = "sys_dict";
/**
* 租户
*/
String SYS_TENANT = GlobalConstants.GLOBAL_REDIS_KEY + "sys_tenant#30d";
/**
* 用户账户
*/

View File

@ -72,5 +72,10 @@ public interface Constants {
*/
String TOKEN = "token";
/**
* 顶级部门id
*/
Long TOP_PARENT_ID = 0L;
}

View File

@ -0,0 +1,39 @@
package com.ruoyi.common.core.constant;
/**
* 全局的key常量 (业务无关的key)
*
* @author Lion Li
*/
public interface GlobalConstants {
/**
* 全局 redis key (业务无关的key)
*/
String GLOBAL_REDIS_KEY = "global:";
/**
* 登录用户 redis key
*/
String LOGIN_TOKEN_KEY = GLOBAL_REDIS_KEY + "Authorization:login:token:";
/**
* 验证码 redis key
*/
String CAPTCHA_CODE_KEY = GLOBAL_REDIS_KEY + "captcha_codes:";
/**
* 防重提交 redis key
*/
String REPEAT_SUBMIT_KEY = GLOBAL_REDIS_KEY + "repeat_submit:";
/**
* 限流 redis key
*/
String RATE_LIMIT_KEY = GLOBAL_REDIS_KEY + "rate_limit:";
/**
* 登录账户密码错误次数 redis key
*/
String PWD_ERR_CNT_KEY = GLOBAL_REDIS_KEY + "pwd_err_cnt:";
}

View File

@ -0,0 +1,51 @@
package com.ruoyi.common.core.constant;
/**
* 租户常量信息
*
* @author Lion Li
*/
public interface TenantConstants {
/**
* 租户正常状态
*/
String NORMAL = "0";
/**
* 租户封禁状态
*/
String DISABLE = "1";
/**
* 校验返回结果码
*/
String PASS = "0";
String NOT_PASS = "1";
/**
* 超级管理员ID
*/
Long SUPER_ADMIN_ID = 1L;
/**
* 超级管理员角色 roleKey
*/
String SUPER_ADMIN_ROLE_KEY = "superadmin";
/**
* 租户管理员角色 roleKey
*/
String TENANT_ADMIN_ROLE_KEY = "admin";
/**
* 租户管理员角色名称
*/
String TENANT_ADMIN_ROLE_NAME = "管理员";
/**
* 默认租户ID
*/
String DEFAULT_TENANT_ID = "000000";
}

View File

@ -15,6 +15,12 @@ import jakarta.validation.constraints.NotBlank;
@Data
public class LoginBody {
/**
* 租户ID
*/
@NotBlank(message = "{tenant.number.not.blank}")
private String tenantId;
/**
* 用户名
*/

View File

@ -22,6 +22,11 @@ public class LoginUser implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 租户ID
*/
private String tenantId;
/**
* 用户ID
*/

View File

@ -13,6 +13,12 @@ import jakarta.validation.constraints.NotBlank;
@Data
public class SmsLoginBody {
/**
* 租户ID
*/
@NotBlank(message = "{tenant.number.not.blank}")
private String tenantId;
/**
* 用户名
*/

View File

@ -0,0 +1,30 @@
package com.ruoyi.common.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 用户状态
*
* @author LionLi
*/
@Getter
@AllArgsConstructor
public enum TenantStatus {
/**
* 正常
*/
OK("0", "正常"),
/**
* 停用
*/
DISABLE("1", "停用"),
/**
* 删除
*/
DELETED("2", "删除");
private final String code;
private final String info;
}

View File

@ -92,7 +92,7 @@ public class ServletUtils extends JakartaServletUtil {
public static Map<String, String> getParamMap(ServletRequest request) {
Map<String, String> params = new HashMap<>();
for (Map.Entry<String, String[]> entry : getParams(request).entrySet()) {
params.put(entry.getKey(), StringUtils.join(entry.getValue(), ","));
params.put(entry.getKey(), StringUtils.join(entry.getValue(), StringUtils.SEPARATOR));
}
return params;
}

View File

@ -42,7 +42,7 @@ public class StreamUtils {
* @return 拼接后的list
*/
public static <E> String join(Collection<E> collection, Function<E, String> function) {
return join(collection, function, ",");
return join(collection, function, StringUtils.SEPARATOR);
}
/**

View File

@ -1,16 +1,16 @@
package com.ruoyi.common.core.utils;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.StrUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.util.AntPathMatcher;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 字符串工具类
@ -20,6 +20,8 @@ import java.util.Set;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class StringUtils extends org.apache.commons.lang3.StringUtils {
public static final String SEPARATOR = ",";
/**
* 获取参数不为空值
*
@ -233,7 +235,7 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils {
/**
* 数字左边补齐0使之达到指定长度。注意如果数字转换为字符串后长度大于size则只保留 最后size个字符。
*
* @param num 数字对象
* @param num 数字对象
* @param size 字符串指定长度
* @return 返回数字的字符串格式,该字符串为指定长度。
*/
@ -244,9 +246,9 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils {
/**
* 字符串左补齐。如果原始字符串s长度大于size则只保留最后size个字符。
*
* @param s 原始字符串
* @param s 原始字符串
* @param size 字符串指定长度
* @param c 用于补齐的字符
* @param c 用于补齐的字符
* @return 返回指定长度的字符串,由原字符串左补齐或截取得到。
*/
public static String padl(final String s, final int size, final char c) {
@ -265,4 +267,55 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils {
return sb.toString();
}
/**
* 切分字符串(分隔符默认逗号)
*
* @param str 被切分的字符串
* @return 分割后的数据列表
*/
public static List<String> splitList(String str) {
return splitTo(str, Convert::toStr);
}
/**
* 切分字符串
*
* @param str 被切分的字符串
* @param separator 分隔符
* @return 分割后的数据列表
*/
public static List<String> splitList(String str, String separator) {
return splitTo(str, separator, Convert::toStr);
}
/**
* 切分字符串自定义转换(分隔符默认逗号)
*
* @param str 被切分的字符串
* @param mapper 自定义转换
* @return 分割后的数据列表
*/
public static <T> List<T> splitTo(String str, Function<? super Object, T> mapper) {
return splitTo(str, SEPARATOR, mapper);
}
/**
* 切分字符串自定义转换
*
* @param str 被切分的字符串
* @param separator 分隔符
* @param mapper 自定义转换
* @return 分割后的数据列表
*/
public static <T> List<T> splitTo(String str, String separator, Function<? super Object, T> mapper) {
if (isBlank(str)) {
return new ArrayList<>(0);
}
return StrUtil.split(str, separator)
.stream()
.filter(Objects::nonNull)
.map(mapper)
.collect(Collectors.toList());
}
}

View File

@ -1,5 +1,7 @@
package com.ruoyi.common.excel.annotation;
import com.ruoyi.common.core.utils.StringUtils;
import java.lang.annotation.*;
/**
@ -25,6 +27,6 @@ public @interface ExcelDictFormat {
/**
* 分隔符,读取字符串组内容
*/
String separator() default ",";
String separator() default StringUtils.SEPARATOR;
}

View File

@ -269,7 +269,7 @@ public class ExcelUtil {
*/
public static String convertByExp(String propertyValue, String converterExp, String separator) {
StringBuilder propertyString = new StringBuilder();
String[] convertSource = converterExp.split(",");
String[] convertSource = converterExp.split(StringUtils.SEPARATOR);
for (String item : convertSource) {
String[] itemArray = item.split("=");
if (StringUtils.containsAny(propertyValue, separator)) {
@ -298,7 +298,7 @@ public class ExcelUtil {
*/
public static String reverseByExp(String propertyValue, String converterExp, String separator) {
StringBuilder propertyString = new StringBuilder();
String[] convertSource = converterExp.split(",");
String[] convertSource = converterExp.split(StringUtils.SEPARATOR);
for (String item : convertSource) {
String[] itemArray = item.split("=");
if (StringUtils.containsAny(propertyValue, separator)) {

View File

@ -3,7 +3,7 @@ package com.ruoyi.common.idempotent.aspectj;
import cn.dev33.satoken.SaManager;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.SecureUtil;
import com.ruoyi.common.core.constant.CacheConstants;
import com.ruoyi.common.core.constant.GlobalConstants;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.utils.MessageUtils;
@ -57,7 +57,7 @@ public class RepeatSubmitAspect {
submitKey = SecureUtil.md5(submitKey + ":" + nowParams);
// 唯一标识指定key + url + 消息头)
String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey;
String cacheRepeatKey = GlobalConstants.REPEAT_SUBMIT_KEY + url + submitKey;
String key = RedisUtils.getCacheObject(cacheRepeatKey);
if (key == null) {
RedisUtils.setCacheObject(cacheRepeatKey, "", Duration.ofMillis(interval));

View File

@ -67,6 +67,7 @@ public class LogAspect {
// *========数据库日志=========*//
OperLogEvent operLog = new OperLogEvent();
operLog.setTenantId(LoginHelper.getTenantId());
operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
// 请求的地址
String ip = ServletUtils.getClientIP();

View File

@ -19,6 +19,11 @@ public class LogininforEvent implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 租户ID
*/
private String tenantId;
/**
* 用户账号
*/

View File

@ -23,6 +23,11 @@ public class OperLogEvent implements Serializable {
*/
private Long operId;
/**
* 租户ID
*/
private String tenantId;
/**
* 操作模块
*/

View File

@ -1,41 +0,0 @@
package com.ruoyi.common.mybatis.core.domain;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.util.ArrayList;
import java.util.List;
/**
* Tree基类
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class TreeEntity<T> extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 父菜单名称
*/
@TableField(exist = false)
private String parentName;
/**
* 父菜单ID
*/
private Long parentId;
/**
* 子部门
*/
@TableField(exist = false)
private List<T> children = new ArrayList<>();
}

View File

@ -6,7 +6,7 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.*;
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.toolkit.Db;
import com.ruoyi.common.core.utils.BeanCopyUtils;
@ -17,6 +17,9 @@ import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 自定义 Mapper 接口, 实现 自定义扩展
@ -193,4 +196,8 @@ public interface BaseMapperPlus<M, T, V> extends BaseMapper<T> {
return (P) voPage;
}
default <C> List<C> selectObjs(Wrapper<T> wrapper, Function<? super Object, C> mapper) {
return this.selectObjs(wrapper).stream().filter(Objects::nonNull).map(mapper).collect(Collectors.toList());
}
}

View File

@ -89,8 +89,8 @@ public class PageQuery implements Serializable {
// 兼容前端排序类型
isAsc = StringUtils.replaceEach(isAsc, new String[]{"ascending", "descending"}, new String[]{"asc", "desc"});
String[] orderByArr = orderBy.split(",");
String[] isAscArr = isAsc.split(",");
String[] orderByArr = orderBy.split(StringUtils.SEPARATOR);
String[] isAscArr = isAsc.split(StringUtils.SEPARATOR);
if (isAscArr.length != 1 && isAscArr.length != orderByArr.length) {
throw new ServiceException("排序参数有误");
}

View File

@ -1,4 +1,4 @@
package com.ruoyi.common.mybatis.helper;
package com.ruoyi.common.mybatis.handler;
import com.ruoyi.common.core.domain.R;
import lombok.extern.slf4j.Slf4j;

View File

@ -35,7 +35,6 @@ import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 数据权限过滤
@ -79,7 +78,7 @@ public class PlusDataPermissionHandler {
DataPermissionHelper.setVariable("user", currentUser);
}
// 如果是超级管理员,则不过滤数据
if (LoginHelper.isAdmin()) {
if (LoginHelper.isSuperAdmin()) {
return where;
}
String dataFilterSql = buildDataFilter(dataColumns, isSelect);

View File

@ -3,6 +3,8 @@ package com.ruoyi.common.mybatis.helper;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.context.model.SaStorage;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.plugins.IgnoreStrategy;
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@ -44,4 +46,19 @@ public class DataPermissionHelper {
}
throw new NullPointerException("data permission context type exception");
}
/**
* 开启忽略数据权限(开启后需手动调用 {@link #disableIgnore()} 关闭)
*/
public static void enableIgnore() {
InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().dataPermission(true).build());
}
/**
* 关闭忽略数据权限
*/
public static void disableIgnore() {
InterceptorIgnoreHelper.clearIgnoreStrategy();
}
}

View File

@ -183,6 +183,12 @@ public class OssClient {
return configKey;
}
/**
* 获取私有URL链接
*
* @param objectKey 对象KEY
* @param second 授权时间
*/
public String getPrivateUrl(String objectKey, Integer second) {
GeneratePresignedUrlRequest generatePresignedUrlRequest =
new GeneratePresignedUrlRequest(properties.getBucketName(), objectKey)
@ -192,6 +198,13 @@ public class OssClient {
return url.toString();
}
/**
* 检查配置是否相同
*/
public boolean checkPropertiesSame(OssProperties properties) {
return this.properties.equals(properties);
}
/**
* 获取当前桶权限类型
*

View File

@ -24,21 +24,6 @@ public class OssFactory {
private static final Map<String, OssClient> CLIENT_CACHE = new ConcurrentHashMap<>();
/**
* 初始化工厂
*/
public static void init() {
log.info("初始化OSS工厂");
RedisUtils.subscribe(OssConstant.DEFAULT_CONFIG_KEY, String.class, configKey -> {
OssClient client = getClient(configKey);
// 未初始化不处理
if (client != null) {
refresh(configKey);
log.info("订阅刷新OSS配置 => " + configKey);
}
});
}
/**
* 获取默认实例
*/
@ -55,25 +40,24 @@ public class OssFactory {
* 根据类型获取实例
*/
public static OssClient instance(String configKey) {
OssClient client = getClient(configKey);
if (client == null) {
refresh(configKey);
return getClient(configKey);
}
return client;
}
private static void refresh(String configKey) {
String json = CacheUtils.get(CacheNames.SYS_OSS_CONFIG, configKey);
if (json == null) {
throw new OssException("系统异常, '" + configKey + "'配置信息不存在!");
}
OssProperties properties = JsonUtils.parseObject(json, OssProperties.class);
CLIENT_CACHE.put(configKey, new OssClient(configKey, properties));
}
private static OssClient getClient(String configKey) {
return CLIENT_CACHE.get(configKey);
OssClient client = CLIENT_CACHE.get(configKey);
if (client == null) {
CLIENT_CACHE.put(configKey, new OssClient(configKey, properties));
log.info("创建OSS实例 key => {}", configKey);
return CLIENT_CACHE.get(configKey);
}
// 配置不相同则重新构建
if (!client.checkPropertiesSame(properties)) {
CLIENT_CACHE.put(configKey, new OssClient(configKey, properties));
log.info("重载OSS实例 key => {}", configKey);
return CLIENT_CACHE.get(configKey);
}
return client;
}
}

View File

@ -1,6 +1,6 @@
package com.ruoyi.common.ratelimiter.annotation;
import com.ruoyi.common.core.constant.CacheConstants;
import com.ruoyi.common.core.constant.GlobalConstants;
import com.ruoyi.common.ratelimiter.enums.LimitType;
import java.lang.annotation.*;
@ -17,7 +17,7 @@ public @interface RateLimiter {
/**
* 限流key
*/
String key() default CacheConstants.RATE_LIMIT_KEY;
String key() default GlobalConstants.RATE_LIMIT_KEY;
/**
* 限流时间,单位秒

View File

@ -1,8 +1,10 @@
package com.ruoyi.common.satoken.config;
import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.jwt.StpLogicJwtForSimple;
import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpLogic;
import com.ruoyi.common.satoken.core.dao.PlusSaTokenDao;
import com.ruoyi.common.satoken.core.service.SaPermissionImpl;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
@ -30,4 +32,12 @@ public class SaTokenConfig implements WebMvcConfigurer {
return new SaPermissionImpl();
}
/**
* 自定义dao层存储
*/
@Bean
public SaTokenDao saTokenDao() {
return new PlusSaTokenDao();
}
}

View File

@ -3,7 +3,6 @@ package com.ruoyi.common.satoken.core.dao;
import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.util.SaFoxUtil;
import com.ruoyi.common.redis.utils.RedisUtils;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.ArrayList;
@ -15,7 +14,6 @@ import java.util.List;
*
* @author Lion Li
*/
@Component
public class PlusSaTokenDao implements SaTokenDao {
/**

View File

@ -1,8 +1,11 @@
package com.ruoyi.common.satoken.utils;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONObject;
import com.ruoyi.common.core.constant.TenantConstants;
import com.ruoyi.common.core.constant.UserConstants;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.enums.DeviceType;
@ -12,6 +15,8 @@ import com.ruoyi.common.core.utils.StringUtils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.util.Set;
/**
* 登录鉴权助手
* <p>
@ -37,8 +42,7 @@ public class LoginHelper {
*/
public static void login(LoginUser loginUser) {
SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser);
StpUtil.login(loginUser.getLoginId());
setLoginUser(loginUser);
StpUtil.login(loginUser.getLoginId(), new SaLoginModel().setExtra(LOGIN_USER_KEY, loginUser));
}
/**
@ -49,15 +53,9 @@ public class LoginHelper {
*/
public static void loginByDevice(LoginUser loginUser, DeviceType deviceType) {
SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser);
StpUtil.login(loginUser.getLoginId(), deviceType.getDevice());
setLoginUser(loginUser);
}
/**
* 设置用户数据(多级缓存)
*/
public static void setLoginUser(LoginUser loginUser) {
StpUtil.getTokenSession().set(LOGIN_USER_KEY, loginUser);
StpUtil.login(loginUser.getLoginId(), new SaLoginModel()
.setDevice(deviceType.getDevice())
.setExtra(LOGIN_USER_KEY, loginUser));
}
/**
@ -68,7 +66,7 @@ public class LoginHelper {
if (loginUser != null) {
return loginUser;
}
loginUser = (LoginUser) StpUtil.getTokenSession().get(LOGIN_USER_KEY);
loginUser = ((JSONObject) StpUtil.getExtra(LOGIN_USER_KEY)).toBean(LoginUser.class);
SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser);
return loginUser;
}
@ -96,6 +94,19 @@ public class LoginHelper {
return loginUser.getUserId();
}
/**
* 获取租户ID
*/
public static String getTenantId() {
LoginUser loginUser;
try {
loginUser = getLoginUser();
} catch (Exception e) {
return null;
}
return loginUser.getTenantId();
}
/**
* 获取部门ID
*/
@ -119,17 +130,31 @@ public class LoginHelper {
}
/**
* 是否为管理员
* 是否为超级管理员
*
* @param userId 用户ID
* @return 结果
*/
public static boolean isAdmin(Long userId) {
public static boolean isSuperAdmin(Long userId) {
return UserConstants.SUPER_ADMIN_ID.equals(userId);
}
public static boolean isAdmin() {
return isAdmin(getUserId());
public static boolean isSuperAdmin() {
return isSuperAdmin(getUserId());
}
/**
* 是否为超级管理员
*
* @param rolePermission 角色权限标识组
* @return 结果
*/
public static boolean isTenantAdmin(Set<String> rolePermission) {
return rolePermission.contains(TenantConstants.TENANT_ADMIN_ROLE_KEY);
}
public static boolean isTenantAdmin() {
return isTenantAdmin(getLoginUser().getRolePermission());
}
}

View File

@ -1,2 +1 @@
com.ruoyi.common.satoken.core.dao.PlusSaTokenDao
com.ruoyi.common.satoken.config.SaTokenConfig

View File

@ -22,12 +22,6 @@
<artifactId>ruoyi-common-satoken</artifactId>
</dependency>
<!-- Sa-Token 权限认证, 在线文档http://sa-token.dev33.cn/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>${satoken.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -53,7 +53,7 @@ public class TencentSmsTemplate implements SmsTemplate {
throw new SmsException("模板ID不能为空");
}
SendSmsRequest req = new SendSmsRequest();
Set<String> set = Arrays.stream(phones.split(",")).map(p -> "+86" + p).collect(Collectors.toSet());
Set<String> set = Arrays.stream(phones.split(StringUtils.SEPARATOR)).map(p -> "+86" + p).collect(Collectors.toSet());
req.setPhoneNumberSet(ArrayUtil.toArray(set, String.class));
if (CollUtil.isNotEmpty(param)) {
req.setTemplateParamSet(ArrayUtil.toArray(param.values(), String.class));

View File

@ -0,0 +1,37 @@
<?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.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common-tenant</artifactId>
<description>
ruoyi-common-tenant 租户模块
</description>
<dependencies>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-mybatis</artifactId>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-redis</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
</dependency>
</dependencies>
</project>

View File

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

View File

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

View File

@ -0,0 +1,114 @@
package com.ruoyi.common.tenant.core;
import com.ruoyi.common.core.constant.GlobalConstants;
import com.ruoyi.common.satoken.core.dao.PlusSaTokenDao;
import java.util.List;
/**
* SaToken 认证数据持久层 适配多租户
*
* @author Lion Li
*/
public class TenantSaTokenDao extends PlusSaTokenDao {
@Override
public String get(String key) {
return super.get(GlobalConstants.GLOBAL_REDIS_KEY + key);
}
@Override
public void set(String key, String value, long timeout) {
super.set(GlobalConstants.GLOBAL_REDIS_KEY + key, value, timeout);
}
/**
* 修修改指定key-value键值对 (过期时间不变)
*/
@Override
public void update(String key, String value) {
super.update(GlobalConstants.GLOBAL_REDIS_KEY + key, value);
}
/**
* 删除Value
*/
@Override
public void delete(String key) {
super.delete(GlobalConstants.GLOBAL_REDIS_KEY + key);
}
/**
* 获取Value的剩余存活时间 (单位: 秒)
*/
@Override
public long getTimeout(String key) {
return super.getTimeout(GlobalConstants.GLOBAL_REDIS_KEY + key);
}
/**
* 修改Value的剩余存活时间 (单位: 秒)
*/
@Override
public void updateTimeout(String key, long timeout) {
super.updateTimeout(GlobalConstants.GLOBAL_REDIS_KEY + key, timeout);
}
/**
* 获取Object如无返空
*/
@Override
public Object getObject(String key) {
return super.getObject(GlobalConstants.GLOBAL_REDIS_KEY + key);
}
/**
* 写入Object并设定存活时间 (单位: 秒)
*/
@Override
public void setObject(String key, Object object, long timeout) {
super.setObject(GlobalConstants.GLOBAL_REDIS_KEY + key, object, timeout);
}
/**
* 更新Object (过期时间不变)
*/
@Override
public void updateObject(String key, Object object) {
super.updateObject(GlobalConstants.GLOBAL_REDIS_KEY + key, object);
}
/**
* 删除Object
*/
@Override
public void deleteObject(String key) {
super.deleteObject(GlobalConstants.GLOBAL_REDIS_KEY + key);
}
/**
* 获取Object的剩余存活时间 (单位: 秒)
*/
@Override
public long getObjectTimeout(String key) {
return super.getObjectTimeout(GlobalConstants.GLOBAL_REDIS_KEY + key);
}
/**
* 修改Object的剩余存活时间 (单位: 秒)
*/
@Override
public void updateObjectTimeout(String key, long timeout) {
super.updateObjectTimeout(GlobalConstants.GLOBAL_REDIS_KEY + key, timeout);
}
/**
* 搜索数据
*/
@Override
public List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {
return super.searchData(GlobalConstants.GLOBAL_REDIS_KEY + prefix, keyword, start, size, sortType);
}
}

View File

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

View File

@ -0,0 +1,57 @@
package com.ruoyi.common.tenant.handle;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.satoken.utils.LoginHelper;
import com.ruoyi.common.tenant.helper.TenantHelper;
import com.ruoyi.common.tenant.properties.TenantProperties;
import lombok.AllArgsConstructor;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.NullValue;
import java.util.List;
/**
* 自定义租户处理器
*
* @author Lion Li
*/
@AllArgsConstructor
public class PlusTenantLineHandler implements TenantLineHandler {
private final TenantProperties tenantProperties;
@Override
public Expression getTenantId() {
String tenantId = LoginHelper.getTenantId();
if (StringUtils.isBlank(tenantId)) {
return new NullValue();
}
String dynamicTenantId = TenantHelper.getDynamic();
if (StringUtils.isNotBlank(dynamicTenantId)) {
// 返回动态租户
return new LongValue(dynamicTenantId);
}
// 返回固定租户
return new LongValue(tenantId);
}
@Override
public boolean ignoreTable(String tableName) {
String tenantId = LoginHelper.getTenantId();
// 判断是否有租户
if (StringUtils.isNotBlank(tenantId)) {
// 不需要过滤租户的表
List<String> excludes = tenantProperties.getExcludes();
// 非业务表
excludes.addAll(List.of(
"gen_table",
"gen_table_column"
));
return excludes.contains(tableName);
}
return true;
}
}

View File

@ -0,0 +1,58 @@
package com.ruoyi.common.tenant.handle;
import com.ruoyi.common.core.constant.GlobalConstants;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.redis.handler.KeyPrefixHandler;
import com.ruoyi.common.tenant.helper.TenantHelper;
/**
* 多租户redis缓存key前缀处理
*
* @author Lion Li
*/
public class TenantKeyPrefixHandler extends KeyPrefixHandler {
public TenantKeyPrefixHandler(String keyPrefix) {
super(keyPrefix);
}
/**
* 增加前缀
*/
@Override
public String map(String name) {
if (StringUtils.isBlank(name)) {
return null;
}
if (StringUtils.contains(name, GlobalConstants.GLOBAL_REDIS_KEY)) {
return super.map(name);
}
String tenantId = TenantHelper.getTenantId();
if (StringUtils.startsWith(name, tenantId)) {
// 如果存在则直接返回
return super.map(name);
}
return super.map(tenantId + ":" + name);
}
/**
* 去除前缀
*/
@Override
public String unmap(String name) {
String unmap = super.unmap(name);
if (StringUtils.isBlank(unmap)) {
return null;
}
if (StringUtils.contains(name, GlobalConstants.GLOBAL_REDIS_KEY)) {
return super.unmap(name);
}
String tenantId = TenantHelper.getTenantId();
if (StringUtils.startsWith(unmap, tenantId)) {
// 如果存在则删除
return unmap.substring((tenantId + ":").length());
}
return unmap;
}
}

View File

@ -0,0 +1,112 @@
package com.ruoyi.common.tenant.helper;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.spring.SpringMVCUtil;
import com.alibaba.ttl.TransmittableThreadLocal;
import com.baomidou.mybatisplus.core.plugins.IgnoreStrategy;
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import com.ruoyi.common.core.constant.GlobalConstants;
import com.ruoyi.common.core.utils.SpringUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.redis.utils.RedisUtils;
import com.ruoyi.common.satoken.utils.LoginHelper;
import com.ruoyi.common.tenant.properties.TenantProperties;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 租户助手
*
* @author Lion Li
*/
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class TenantHelper {
private static final TenantProperties PROPERTIES = SpringUtils.getBean(TenantProperties.class);
private static final String DYNAMIC_TENANT_KEY = GlobalConstants.GLOBAL_REDIS_KEY + "dynamicTenant";
private static final ThreadLocal<String> TEMP_DYNAMIC_TENANT = new TransmittableThreadLocal<>();
/**
* 租户功能是否启用
*/
public static boolean isEnable() {
return PROPERTIES.getEnable();
}
/**
* 开启忽略租户(开启后需手动调用 {@link #disableIgnore()} 关闭)
*/
public static void enableIgnore() {
InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().tenantLine(true).build());
}
/**
* 关闭忽略租户
*/
public static void disableIgnore() {
InterceptorIgnoreHelper.clearIgnoreStrategy();
}
/**
* 设置动态租户(一直有效 需要手动清理)
* <p>
* 如果为非web环境 那么只在当前线程内生效
*/
public static void setDynamic(String tenantId) {
if (!SpringMVCUtil.isWeb()) {
TEMP_DYNAMIC_TENANT.set(tenantId);
return;
}
String cacheKey = DYNAMIC_TENANT_KEY + ":" + LoginHelper.getTenantId();
RedisUtils.setCacheObject(cacheKey, tenantId);
SaHolder.getStorage().set(cacheKey, tenantId);
}
/**
* 获取动态租户(一直有效 需要手动清理)
* <p>
* 如果为非web环境 那么只在当前线程内生效
*/
public static String getDynamic() {
if (!SpringMVCUtil.isWeb()) {
return TEMP_DYNAMIC_TENANT.get();
}
String cacheKey = DYNAMIC_TENANT_KEY + ":" + LoginHelper.getTenantId();
String tenantId = (String) SaHolder.getStorage().get(cacheKey);
if (StringUtils.isNotBlank(tenantId)) {
return tenantId;
}
tenantId = RedisUtils.getCacheObject(cacheKey);
SaHolder.getStorage().set(cacheKey, tenantId);
return tenantId;
}
/**
* 清除动态租户
*/
public static void clearDynamic() {
if (!SpringMVCUtil.isWeb()) {
TEMP_DYNAMIC_TENANT.remove();
return;
}
String cacheKey = DYNAMIC_TENANT_KEY + ":" + LoginHelper.getTenantId();
RedisUtils.deleteObject(cacheKey);
SaHolder.getStorage().delete(cacheKey);
}
/**
* 获取当前租户id(动态租户优先)
*/
public static String getTenantId() {
String tenantId = TenantHelper.getDynamic();
if (StringUtils.isBlank(tenantId)) {
tenantId = LoginHelper.getTenantId();
}
return tenantId;
}
}

View File

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

View File

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

View File

@ -30,7 +30,7 @@ public class FilterConfig {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setDispatcherTypes(DispatcherType.REQUEST);
registration.setFilter(new XssFilter());
registration.addUrlPatterns(StringUtils.split(xssProperties.getUrlPatterns(), ","));
registration.addUrlPatterns(StringUtils.split(xssProperties.getUrlPatterns(), StringUtils.SEPARATOR));
registration.setName("xssFilter");
registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
Map<String, String> initParameters = new HashMap<>();

View File

@ -14,6 +14,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "captcha")
public class CaptchaProperties {
private Boolean enable;
/**
* 验证码类型
*/

View File

@ -25,7 +25,7 @@ public class XssFilter implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
String tempExcludes = filterConfig.getInitParameter("excludes");
if (StringUtils.isNotEmpty(tempExcludes)) {
String[] url = tempExcludes.split(",");
String[] url = tempExcludes.split(StringUtils.SEPARATOR);
for (int i = 0; url != null && i < url.length; i++) {
excludes.add(url[i]);
}