From 05adf0b31b98b75fc71bdbe0090339497bc17952 Mon Sep 17 00:00:00 2001 From: 9264yf <691506722@qq.com> Date: Sat, 20 Dec 2025 16:15:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(tenant):=20=E5=AE=8C=E5=96=84=E5=A4=9A?= =?UTF-8?q?=E7=A7=9F=E6=88=B7=E5=8A=9F=E8=83=BD=E5=8F=8A=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在登录流程中新增租户编码支持,允许用户通过租户编码登录。 - 更新SysLoginController和SysLoginService以处理租户信息。 - 在字典数据和字典类型实体中添加租户ID字段,支持租户级字典管理。 - 扩展字典数据和用户查询功能,支持按租户ID过滤。 - 更新前端登录页面,添加租户编码输入框,优化用户体验。 - 修改相关Mapper和Service以支持租户数据的插入和查询。 --- .../controller/system/SysLoginController.java | 4 +- .../com/ruoyi/common/config/TenantConfig.java | 26 ++++++- .../core/domain/entity/SysDictData.java | 14 ++++ .../core/domain/entity/SysDictType.java | 14 ++++ .../common/core/domain/model/LoginBody.java | 15 ++++ .../com/ruoyi/common/utils/DictUtils.java | 17 ++++- .../interceptor/TenantSqlInterceptor.java | 76 +++++++++++++++++-- .../web/service/SysLoginService.java | 27 ++++++- .../web/service/UserDetailsServiceImpl.java | 20 ++++- .../system/mapper/SysDictDataMapper.java | 12 ++- .../ruoyi/system/mapper/SysTenantMapper.java | 10 ++- .../ruoyi/system/mapper/SysUserMapper.java | 22 ++++-- .../system/service/ISysTenantService.java | 10 ++- .../ruoyi/system/service/ISysUserService.java | 11 ++- .../service/impl/SysDictDataServiceImpl.java | 7 +- .../service/impl/SysDictTypeServiceImpl.java | 27 +++++-- .../service/impl/SysTenantServiceImpl.java | 14 +++- .../service/impl/SysUserServiceImpl.java | 29 +++++-- .../mapper/system/SysDictDataMapper.xml | 25 +++++- .../mapper/system/SysDictTypeMapper.xml | 8 +- .../resources/mapper/system/SysRoleMapper.xml | 2 + .../mapper/system/SysTenantMapper.xml | 5 ++ .../resources/mapper/system/SysUserMapper.xml | 24 +++--- ruoyi-ui/src/api/login.js | 5 +- ruoyi-ui/src/store/modules/user.js | 11 ++- ruoyi-ui/src/views/login.vue | 11 +++ ruoyi-ui/src/views/system/role/index.vue | 33 +++++++- sql/bug_fix_20251220.sql | 22 ++++++ sql/dict_tenant_migration.sql | 23 ++++++ 29 files changed, 464 insertions(+), 60 deletions(-) create mode 100644 sql/bug_fix_20251220.sql create mode 100644 sql/dict_tenant_migration.sql diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java index caebb3972..7e308d85b 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java @@ -49,7 +49,7 @@ public class SysLoginController /** * 登录方法 - * + * * @param loginBody 登录信息 * @return 结果 */ @@ -59,7 +59,7 @@ public class SysLoginController AjaxResult ajax = AjaxResult.success(); // 生成令牌 String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(), - loginBody.getUuid()); + loginBody.getUuid(), loginBody.getTenantCode()); ajax.put(Constants.TOKEN, token); return ajax; } diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/config/TenantConfig.java b/ruoyi-common/src/main/java/com/ruoyi/common/config/TenantConfig.java index 323b5431b..06c7af38c 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/config/TenantConfig.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/config/TenantConfig.java @@ -53,14 +53,22 @@ public class TenantConfig { "hl_order" ); + /** + * 混合模式表(系统级 + 租户级共存) + * Reason: 这些表同时支持系统级数据(tenant_id=0)和租户级数据 + * 查询时使用 WHERE (tenant_id = ? OR tenant_id = 0) + */ + public static final List HYBRID_TABLES = Arrays.asList( + "sys_dict_type", + "sys_dict_data" + ); + /** * 不需要租户隔离的表(白名单) * Reason: 这些表是全局共享的,所有租户共用 */ public static final List IGNORE_TABLES = Arrays.asList( "sys_menu", - "sys_dict_type", - "sys_dict_data", "sys_config", "sys_user_role", "sys_role_menu", @@ -119,4 +127,18 @@ public class TenantConfig { // 租户表需要过滤 return TENANT_TABLES.stream().anyMatch(lowerTableName::equals); } + + /** + * 判断表是否为混合模式表 + * Reason: 混合模式表同时支持系统级数据(tenant_id=0)和租户级数据 + * + * @param tableName 表名 + * @return true-混合模式表,false-非混合模式表 + */ + public static boolean isHybridTable(String tableName) { + if (tableName == null) { + return false; + } + return HYBRID_TABLES.stream().anyMatch(tableName.toLowerCase()::equals); + } } diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictData.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictData.java index e79eaa20c..540bac8f1 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictData.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictData.java @@ -22,6 +22,9 @@ public class SysDictData extends BaseEntity @Excel(name = "字典编码", cellType = ColumnType.NUMERIC) private Long dictCode; + /** 租户ID(0=系统级) */ + private Long tenantId; + /** 字典排序 */ @Excel(name = "字典排序", cellType = ColumnType.NUMERIC) private Long dictSort; @@ -62,6 +65,16 @@ public class SysDictData extends BaseEntity this.dictCode = dictCode; } + public Long getTenantId() + { + return tenantId; + } + + public void setTenantId(Long tenantId) + { + this.tenantId = tenantId; + } + public Long getDictSort() { return dictSort; @@ -158,6 +171,7 @@ public class SysDictData extends BaseEntity public String toString() { return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) .append("dictCode", getDictCode()) + .append("tenantId", getTenantId()) .append("dictSort", getDictSort()) .append("dictLabel", getDictLabel()) .append("dictValue", getDictValue()) diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictType.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictType.java index 0befbf434..76c694790 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictType.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictType.java @@ -22,6 +22,9 @@ public class SysDictType extends BaseEntity @Excel(name = "字典主键", cellType = ColumnType.NUMERIC) private Long dictId; + /** 租户ID(0=系统级) */ + private Long tenantId; + /** 字典名称 */ @Excel(name = "字典名称") private String dictName; @@ -44,6 +47,16 @@ public class SysDictType extends BaseEntity this.dictId = dictId; } + public Long getTenantId() + { + return tenantId; + } + + public void setTenantId(Long tenantId) + { + this.tenantId = tenantId; + } + @NotBlank(message = "字典名称不能为空") @Size(min = 0, max = 100, message = "字典类型名称长度不能超过100个字符") public String getDictName() @@ -83,6 +96,7 @@ public class SysDictType extends BaseEntity public String toString() { return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) .append("dictId", getDictId()) + .append("tenantId", getTenantId()) .append("dictName", getDictName()) .append("dictType", getDictType()) .append("status", getStatus()) diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java index 583b7a61e..e566716c5 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java @@ -27,6 +27,11 @@ public class LoginBody */ private String uuid; + /** + * 租户编码 + */ + private String tenantCode; + public String getUsername() { return username; @@ -66,4 +71,14 @@ public class LoginBody { this.uuid = uuid; } + + public String getTenantCode() + { + return tenantCode; + } + + public void setTenantCode(String tenantCode) + { + this.tenantCode = tenantCode; + } } diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java index 8204f1338..854566839 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java @@ -6,6 +6,7 @@ import com.alibaba.fastjson2.JSONArray; import com.ruoyi.common.constant.CacheConstants; import com.ruoyi.common.core.domain.entity.SysDictData; import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.core.context.TenantContext; import com.ruoyi.common.utils.spring.SpringUtils; /** @@ -228,12 +229,24 @@ public class DictUtils /** * 设置cache key - * + * Reason: 字典数据支持混合模式(系统级tenant_id=0 + 租户级tenant_id=租户ID) + * 每个租户的缓存包含:系统级字典 + 该租户的字典,需要按租户隔离缓存 + * * @param configKey 参数键 * @return 缓存键key */ public static String getCacheKey(String configKey) { - return CacheConstants.SYS_DICT_KEY + configKey; + // 超级管理员或全局模式使用全局缓存 + if (TenantContext.isIgnore()) { + return CacheConstants.SYS_DICT_KEY + "global:" + configKey; + } + // 租户使用租户级缓存 + Long tenantId = TenantContext.getTenantId(); + if (tenantId != null) { + return CacheConstants.SYS_DICT_KEY + "tenant_" + tenantId + ":" + configKey; + } + // 兜底:无租户上下文时使用全局缓存 + return CacheConstants.SYS_DICT_KEY + "global:" + configKey; } } diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/TenantSqlInterceptor.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/TenantSqlInterceptor.java index 7258d15ba..e6f5b61bf 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/TenantSqlInterceptor.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/TenantSqlInterceptor.java @@ -7,10 +7,12 @@ import java.util.Properties; import com.ruoyi.common.exception.ServiceException; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.expression.operators.conditional.OrExpression; import net.sf.jsqlparser.expression.operators.relational.EqualsTo; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.expression.operators.relational.ItemsList; import net.sf.jsqlparser.expression.operators.relational.MultiExpressionList; +import net.sf.jsqlparser.expression.Parenthesis; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; @@ -110,7 +112,7 @@ public class TenantSqlInterceptor implements Interceptor { /** * 判断是否需要拦截 * - * 核心逻辑:通过SQL解析获取主表名,只有主表是租户表时才拦截 + * 核心逻辑:通过SQL解析获取主表名,只有主表是租户表或混合模式表时才拦截 * 这样可以避免: * 1. 字符串子串匹配导致的误判(如 sys_role_menu 误匹配 sys_role) * 2. JOIN表被错误过滤(只对主表添加租户条件) @@ -133,8 +135,8 @@ public class TenantSqlInterceptor implements Interceptor { Statement statement = CCJSqlParserUtil.parse(sql); String mainTableName = getMainTableName(statement); - // 只有主表是租户表时才拦截 - return mainTableName != null && isTenantTable(mainTableName); + // 租户表或混合模式表都需要拦截 + return mainTableName != null && (isTenantTable(mainTableName) || isHybridTable(mainTableName)); } catch (Exception e) { // SQL解析失败,不拦截(避免影响正常业务) log.debug("SQL解析失败,跳过租户拦截: {}", e.getMessage()); @@ -175,6 +177,17 @@ public class TenantSqlInterceptor implements Interceptor { return TenantConfig.needTenantFilter(tableName); } + /** + * 判断表名是否是混合模式表 + * Reason: 混合模式表同时支持系统级数据(tenant_id=0)和租户级数据 + * + * @param tableName 表名 + * @return true-是混合模式表,false-不是 + */ + private boolean isHybridTable(String tableName) { + return TenantConfig.isHybridTable(tableName); + } + /** * 处理SQL - 添加租户过滤条件 * @@ -185,6 +198,8 @@ public class TenantSqlInterceptor implements Interceptor { */ private String processSql(String sql, Long tenantId, SqlCommandType sqlCommandType) throws Exception { Statement statement = CCJSqlParserUtil.parse(sql); + String tableName = getMainTableName(statement); + boolean isHybrid = isHybridTable(tableName); if (statement instanceof Select) { // 处理 SELECT 语句 @@ -194,8 +209,15 @@ public class TenantSqlInterceptor implements Interceptor { // 获取主表别名,避免多表JOIN时tenant_id歧义 String tableAlias = getMainTableAlias(plainSelect); - // 构造 tenant_id = ? 条件(带表别名) - EqualsTo tenantCondition = buildTenantCondition(tenantId, tableAlias); + // 根据表类型构造不同的租户条件 + net.sf.jsqlparser.expression.Expression tenantCondition; + if (isHybrid) { + // 混合模式:(tenant_id = ? OR tenant_id = 0) + tenantCondition = buildHybridTenantCondition(tenantId, tableAlias); + } else { + // 普通租户表:tenant_id = ? + tenantCondition = buildTenantCondition(tenantId, tableAlias); + } // 合并WHERE条件 if (plainSelect.getWhere() != null) { @@ -213,7 +235,11 @@ public class TenantSqlInterceptor implements Interceptor { // 获取表别名 String tableAlias = getUpdateTableAlias(updateStatement); - EqualsTo tenantCondition = buildTenantCondition(tenantId, tableAlias); + + // 根据表类型构造不同的租户条件 + // Reason: 混合模式表的UPDATE只能操作租户自己的数据,不能修改系统级数据(tenant_id=0) + net.sf.jsqlparser.expression.Expression tenantCondition; + tenantCondition = buildTenantCondition(tenantId, tableAlias); if (updateStatement.getWhere() != null) { AndExpression andExpression = new AndExpression(tenantCondition, updateStatement.getWhere()); @@ -230,7 +256,11 @@ public class TenantSqlInterceptor implements Interceptor { // 获取表别名 String tableAlias = getDeleteTableAlias(deleteStatement); - EqualsTo tenantCondition = buildTenantCondition(tenantId, tableAlias); + + // 根据表类型构造不同的租户条件 + // Reason: 混合模式表的DELETE只能操作租户自己的数据,不能删除系统级数据(tenant_id=0) + net.sf.jsqlparser.expression.Expression tenantCondition; + tenantCondition = buildTenantCondition(tenantId, tableAlias); if (deleteStatement.getWhere() != null) { AndExpression andExpression = new AndExpression(tenantCondition, deleteStatement.getWhere()); @@ -319,6 +349,38 @@ public class TenantSqlInterceptor implements Interceptor { return equalsTo; } + /** + * 构造混合模式租户过滤条件:([表别名.]tenant_id = ? OR [表别名.]tenant_id = 0) + * Reason: 混合模式表同时支持系统级数据(tenant_id=0)和租户级数据 + * + * @param tenantId 租户ID + * @param tableAlias 表别名(可为null) + * @return Parenthesis 表达式(带括号的OR条件) + */ + private Parenthesis buildHybridTenantCondition(Long tenantId, String tableAlias) { + // 构造 tenant_id = ? + EqualsTo tenantCondition = new EqualsTo(); + if (tableAlias != null) { + tenantCondition.setLeftExpression(new Column(new Table(tableAlias), "tenant_id")); + } else { + tenantCondition.setLeftExpression(new Column("tenant_id")); + } + tenantCondition.setRightExpression(new LongValue(tenantId)); + + // 构造 tenant_id = 0 + EqualsTo systemCondition = new EqualsTo(); + if (tableAlias != null) { + systemCondition.setLeftExpression(new Column(new Table(tableAlias), "tenant_id")); + } else { + systemCondition.setLeftExpression(new Column("tenant_id")); + } + systemCondition.setRightExpression(new LongValue(0)); + + // 构造 (tenant_id = ? OR tenant_id = 0) + OrExpression orExpression = new OrExpression(tenantCondition, systemCondition); + return new Parenthesis(orExpression); + } + /** * 处理INSERT语句 - 自动填充tenant_id * diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java index 14ab896f3..e66dc1a25 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java @@ -30,6 +30,8 @@ import com.ruoyi.framework.manager.factory.AsyncFactory; import com.ruoyi.framework.security.context.AuthenticationContextHolder; import com.ruoyi.system.service.ISysConfigService; import com.ruoyi.system.service.ISysUserService; +import com.ruoyi.system.service.ISysTenantService; +import com.ruoyi.system.domain.SysTenant; /** * 登录校验方法 @@ -55,16 +57,20 @@ public class SysLoginService @Autowired private ISysConfigService configService; + @Autowired + private ISysTenantService tenantService; + /** * 登录验证 - * + * * @param username 用户名 * @param password 密码 * @param code 验证码 * @param uuid 唯一标识 + * @param tenantCode 租户编码 * @return 结果 */ - public String login(String username, String password, String code, String uuid) + public String login(String username, String password, String code, String uuid, String tenantCode) { // 【多租户】登录阶段:临时忽略租户过滤 // Reason: 登录时需要查询用户表获取tenant_id,形成先有鸡还是先有蛋的问题 @@ -78,6 +84,23 @@ public class SysLoginService validateCaptcha(username, code, uuid); // 登录前置校验 loginPreCheck(username, password); + + // 【多租户】通过租户编码查询租户ID并设置到上下文 + Long tenantId = null; + if (StringUtils.isNotEmpty(tenantCode)) + { + SysTenant tenant = tenantService.selectSysTenantByTenantCode(tenantCode); + if (tenant == null) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, "租户不存在或已停用")); + throw new ServiceException("租户不存在或已停用"); + } + tenantId = tenant.getTenantId(); + // 将租户ID设置到上下文,供 UserDetailsServiceImpl 使用 + TenantContext.setTenantId(tenantId); + log.info("【多租户】登录租户编码: {}, 租户ID: {}", tenantCode, tenantId); + } + // 用户验证 Authentication authentication = null; try diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java index e294fbd2e..e788fe2ce 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java @@ -7,6 +7,7 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; +import com.ruoyi.common.core.context.TenantContext; import com.ruoyi.common.core.domain.entity.SysUser; import com.ruoyi.common.core.domain.model.LoginUser; import com.ruoyi.common.enums.UserStatus; @@ -27,7 +28,7 @@ public class UserDetailsServiceImpl implements UserDetailsService @Autowired private ISysUserService userService; - + @Autowired private SysPasswordService passwordService; @@ -37,7 +38,22 @@ public class UserDetailsServiceImpl implements UserDetailsService @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - SysUser user = userService.selectUserByUserName(username); + // 【多租户】从上下文获取租户ID,用于查询指定租户的用户 + Long tenantId = TenantContext.getTenantId(); + SysUser user; + if (tenantId != null) + { + // 有租户ID时,查询指定租户的用户 + user = userService.selectUserByUserNameAndTenantId(username, tenantId); + log.debug("【多租户】查询用户: {}, 租户ID: {}", username, tenantId); + } + else + { + // 无租户ID时(超级管理员登录),查询所有用户 + user = userService.selectUserByUserName(username); + log.debug("【多租户】超级管理员登录,查询用户: {}", username); + } + if (StringUtils.isNull(user)) { log.info("登录用户:{} 不存在.", username); diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictDataMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictDataMapper.java index 92f799e55..f6fe1e53a 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictDataMapper.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictDataMapper.java @@ -86,10 +86,20 @@ public interface SysDictDataMapper /** * 同步修改字典类型 - * + * * @param oldDictType 旧字典类型 * @param newDictType 新旧字典类型 * @return 结果 */ public int updateDictDataType(@Param("oldDictType") String oldDictType, @Param("newDictType") String newDictType); + + /** + * 混合模式查询:根据字典类型和租户ID查询字典数据 + * Reason: 合并系统字典(tenant_id=0)和租户字典,租户优先 + * + * @param dictType 字典类型 + * @param tenantId 租户ID + * @return 字典数据集合信息 + */ + public List selectDictDataByTypeWithTenant(@Param("dictType") String dictType, @Param("tenantId") Long tenantId); } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTenantMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTenantMapper.java index d73d7d2de..76f32d7b8 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTenantMapper.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTenantMapper.java @@ -15,12 +15,20 @@ public interface SysTenantMapper { /** * 查询租户信息 - * + * * @param tenantId 租户信息主键 * @return 租户信息 */ public SysTenant selectSysTenantByTenantId(Long tenantId); + /** + * 通过租户编码查询租户信息 + * + * @param tenantCode 租户编码 + * @return 租户信息 + */ + public SysTenant selectSysTenantByTenantCode(String tenantCode); + /** * 查询租户信息列表 * diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java index 043e51f46..411d67259 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java @@ -38,12 +38,21 @@ public interface SysUserMapper /** * 通过用户名查询用户 - * + * * @param userName 用户名 * @return 用户对象信息 */ public SysUser selectUserByUserName(String userName); + /** + * 通过用户名和租户ID查询用户 + * + * @param userName 用户名 + * @param tenantId 租户ID + * @return 用户对象信息 + */ + public SysUser selectUserByUserNameAndTenantId(@Param("userName") String userName, @Param("tenantId") Long tenantId); + /** * 通过用户ID查询用户 * @@ -123,25 +132,28 @@ public interface SysUserMapper /** * 校验用户名称是否唯一 - * + * * @param userName 用户名称 + * @param tenantId 租户ID * @return 结果 */ - public SysUser checkUserNameUnique(String userName); + public SysUser checkUserNameUnique(@Param("userName") String userName, @Param("tenantId") Long tenantId); /** * 校验手机号码是否唯一 * * @param phonenumber 手机号码 + * @param tenantId 租户ID * @return 结果 */ - public SysUser checkPhoneUnique(String phonenumber); + public SysUser checkPhoneUnique(@Param("phonenumber") String phonenumber, @Param("tenantId") Long tenantId); /** * 校验email是否唯一 * * @param email 用户邮箱 + * @param tenantId 租户ID * @return 结果 */ - public SysUser checkEmailUnique(String email); + public SysUser checkEmailUnique(@Param("email") String email, @Param("tenantId") Long tenantId); } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTenantService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTenantService.java index 6c2ef01dc..542c7ce3b 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTenantService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTenantService.java @@ -14,12 +14,20 @@ public interface ISysTenantService { /** * 查询租户信息 - * + * * @param tenantId 租户信息主键 * @return 租户信息 */ public SysTenant selectSysTenantByTenantId(Long tenantId); + /** + * 通过租户编码查询租户信息 + * + * @param tenantCode 租户编码 + * @return 租户信息 + */ + public SysTenant selectSysTenantByTenantCode(String tenantCode); + /** * 查询租户信息列表 * diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java index 5cf9f7cd3..506363dc8 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java @@ -37,12 +37,21 @@ public interface ISysUserService /** * 通过用户名查询用户 - * + * * @param userName 用户名 * @return 用户对象信息 */ public SysUser selectUserByUserName(String userName); + /** + * 通过用户名和租户ID查询用户 + * + * @param userName 用户名 + * @param tenantId 租户ID + * @return 用户对象信息 + */ + public SysUser selectUserByUserNameAndTenantId(String userName, Long tenantId); + /** * 通过用户ID查询用户 * diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictDataServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictDataServiceImpl.java index de244f8b4..334319e93 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictDataServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictDataServiceImpl.java @@ -3,6 +3,7 @@ package com.ruoyi.system.service.impl; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import com.ruoyi.common.core.context.TenantContext; import com.ruoyi.common.core.domain.entity.SysDictData; import com.ruoyi.common.utils.DictUtils; import com.ruoyi.system.mapper.SysDictDataMapper; @@ -75,13 +76,17 @@ public class SysDictDataServiceImpl implements ISysDictDataService /** * 新增保存字典数据信息 - * + * * @param data 字典数据信息 * @return 结果 */ @Override public int insertDictData(SysDictData data) { + // 自动填充 tenant_id(混合模式:普通租户填充租户ID,超级管理员填充0表示系统级) + Long tenantId = TenantContext.getTenantId(); + data.setTenantId(tenantId != null ? tenantId : 0L); + int row = dictDataMapper.insertDictData(data); if (row > 0) { diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java index b6c4c5650..b608ce4a1 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java @@ -9,6 +9,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.context.TenantContext; import com.ruoyi.common.core.domain.entity.SysDictData; import com.ruoyi.common.core.domain.entity.SysDictType; import com.ruoyi.common.exception.ServiceException; @@ -133,16 +134,26 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService /** * 加载字典缓存数据 + * Reason: 启动时设置全局模式,跳过租户过滤 */ @Override public void loadingDictCache() { - SysDictData dictData = new SysDictData(); - dictData.setStatus("0"); - Map> dictDataMap = dictDataMapper.selectDictDataList(dictData).stream().collect(Collectors.groupingBy(SysDictData::getDictType)); - for (Map.Entry> entry : dictDataMap.entrySet()) + // 启动时加载缓存,设置全局模式跳过租户过滤 + TenantContext.setIgnore(true); + try { - DictUtils.setDictCache(entry.getKey(), entry.getValue().stream().sorted(Comparator.comparing(SysDictData::getDictSort)).collect(Collectors.toList())); + SysDictData dictData = new SysDictData(); + dictData.setStatus("0"); + Map> dictDataMap = dictDataMapper.selectDictDataList(dictData).stream().collect(Collectors.groupingBy(SysDictData::getDictType)); + for (Map.Entry> entry : dictDataMap.entrySet()) + { + DictUtils.setDictCache(entry.getKey(), entry.getValue().stream().sorted(Comparator.comparing(SysDictData::getDictSort)).collect(Collectors.toList())); + } + } + finally + { + TenantContext.clear(); } } @@ -167,13 +178,17 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService /** * 新增保存字典类型信息 - * + * * @param dict 字典类型信息 * @return 结果 */ @Override public int insertDictType(SysDictType dict) { + // 自动填充 tenant_id(混合模式:普通租户填充租户ID,超级管理员填充0表示系统级) + Long tenantId = TenantContext.getTenantId(); + dict.setTenantId(tenantId != null ? tenantId : 0L); + int row = dictTypeMapper.insertDictType(dict); if (row > 0) { diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTenantServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTenantServiceImpl.java index 81741c31d..966c9aab6 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTenantServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTenantServiceImpl.java @@ -22,7 +22,7 @@ public class SysTenantServiceImpl implements ISysTenantService /** * 查询租户信息 - * + * * @param tenantId 租户信息主键 * @return 租户信息 */ @@ -32,6 +32,18 @@ public class SysTenantServiceImpl implements ISysTenantService return sysTenantMapper.selectSysTenantByTenantId(tenantId); } + /** + * 通过租户编码查询租户信息 + * + * @param tenantCode 租户编码 + * @return 租户信息 + */ + @Override + public SysTenant selectSysTenantByTenantCode(String tenantCode) + { + return sysTenantMapper.selectSysTenantByTenantCode(tenantCode); + } + /** * 查询租户信息列表 * diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java index 5ce9fdaff..5f2d66bfc 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java @@ -107,7 +107,7 @@ public class SysUserServiceImpl implements ISysUserService /** * 通过用户名查询用户 - * + * * @param userName 用户名 * @return 用户对象信息 */ @@ -117,6 +117,19 @@ public class SysUserServiceImpl implements ISysUserService return userMapper.selectUserByUserName(userName); } + /** + * 通过用户名和租户ID查询用户 + * + * @param userName 用户名 + * @param tenantId 租户ID + * @return 用户对象信息 + */ + @Override + public SysUser selectUserByUserNameAndTenantId(String userName, Long tenantId) + { + return userMapper.selectUserByUserNameAndTenantId(userName, tenantId); + } + /** * 通过用户ID查询用户 * @@ -165,7 +178,8 @@ public class SysUserServiceImpl implements ISysUserService /** * 校验用户名称是否唯一 - * + * Reason: Bug Fix - 添加 tenant_id 参数,支持不同租户创建同名用户 + * * @param user 用户信息 * @return 结果 */ @@ -173,7 +187,8 @@ public class SysUserServiceImpl implements ISysUserService public boolean checkUserNameUnique(SysUser user) { Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId(); - SysUser info = userMapper.checkUserNameUnique(user.getUserName()); + Long tenantId = user.getTenantId(); + SysUser info = userMapper.checkUserNameUnique(user.getUserName(), tenantId); if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) { return UserConstants.NOT_UNIQUE; @@ -183,6 +198,7 @@ public class SysUserServiceImpl implements ISysUserService /** * 校验手机号码是否唯一 + * Reason: Bug Fix - 添加 tenant_id 参数,支持不同租户使用同一手机号 * * @param user 用户信息 * @return @@ -191,7 +207,8 @@ public class SysUserServiceImpl implements ISysUserService public boolean checkPhoneUnique(SysUser user) { Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId(); - SysUser info = userMapper.checkPhoneUnique(user.getPhonenumber()); + Long tenantId = user.getTenantId(); + SysUser info = userMapper.checkPhoneUnique(user.getPhonenumber(), tenantId); if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) { return UserConstants.NOT_UNIQUE; @@ -201,6 +218,7 @@ public class SysUserServiceImpl implements ISysUserService /** * 校验email是否唯一 + * Reason: Bug Fix - 添加 tenant_id 参数,支持不同租户使用同一邮箱 * * @param user 用户信息 * @return @@ -209,7 +227,8 @@ public class SysUserServiceImpl implements ISysUserService public boolean checkEmailUnique(SysUser user) { Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId(); - SysUser info = userMapper.checkEmailUnique(user.getEmail()); + Long tenantId = user.getTenantId(); + SysUser info = userMapper.checkEmailUnique(user.getEmail(), tenantId); if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) { return UserConstants.NOT_UNIQUE; diff --git a/ruoyi-system/src/main/resources/mapper/system/SysDictDataMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysDictDataMapper.xml index c5e1da9d0..8435b54a3 100644 --- a/ruoyi-system/src/main/resources/mapper/system/SysDictDataMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/system/SysDictDataMapper.xml @@ -6,6 +6,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -19,9 +20,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - + - select dict_code, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark + select dict_code, tenant_id, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark from sys_dict_data @@ -95,6 +96,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" insert into sys_dict_data( + tenant_id, dict_sort, dict_label, dict_value, @@ -107,6 +109,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" create_by, create_time )values( + #{tenantId}, #{dictSort}, #{dictLabel}, #{dictValue}, @@ -120,5 +123,21 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" sysdate() ) - + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysDictTypeMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysDictTypeMapper.xml index 554db5441..295103eee 100644 --- a/ruoyi-system/src/main/resources/mapper/system/SysDictTypeMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/system/SysDictTypeMapper.xml @@ -6,6 +6,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -14,9 +15,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - + - select dict_id, dict_name, dict_type, status, create_by, create_time, remark + select dict_id, tenant_id, dict_name, dict_type, status, create_by, create_time, remark from sys_dict_type @@ -39,6 +40,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and date_format(create_time,'%Y%m%d') <= date_format(#{params.endTime},'%Y%m%d') + order by create_time desc + + insert into sys_tenant diff --git a/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml index 6aeffca7b..0af7d41eb 100644 --- a/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml @@ -126,22 +126,27 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" where u.user_name = #{userName} and u.del_flag = '0' - + + + - + select user_id, user_name from sys_user where user_name = #{userName} and tenant_id = #{tenantId} and del_flag = '0' limit 1 - - + select user_id, phonenumber from sys_user where phonenumber = #{phonenumber} and tenant_id = #{tenantId} and del_flag = '0' limit 1 - - + select user_id, email from sys_user where email = #{email} and tenant_id = #{tenantId} and del_flag = '0' limit 1 @@ -195,7 +200,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" login_date = #{loginDate}, update_by = #{updateBy}, remark = #{remark}, - tenant_id = #{tenantId}, update_time = sysdate() where user_id = #{userId} diff --git a/ruoyi-ui/src/api/login.js b/ruoyi-ui/src/api/login.js index 9f333cb3c..15e992673 100644 --- a/ruoyi-ui/src/api/login.js +++ b/ruoyi-ui/src/api/login.js @@ -1,12 +1,13 @@ import request from '@/utils/request' // 登录方法 -export function login(username, password, code, uuid) { +export function login(username, password, code, uuid, tenantCode) { const data = { username, password, code, - uuid + uuid, + tenantCode } return request({ url: '/login', diff --git a/ruoyi-ui/src/store/modules/user.js b/ruoyi-ui/src/store/modules/user.js index 83a7d625f..1bf17ba79 100644 --- a/ruoyi-ui/src/store/modules/user.js +++ b/ruoyi-ui/src/store/modules/user.js @@ -47,8 +47,9 @@ const user = { const password = userInfo.password const code = userInfo.code const uuid = userInfo.uuid + const tenantCode = userInfo.tenantCode return new Promise((resolve, reject) => { - login(username, password, code, uuid).then(res => { + login(username, password, code, uuid, tenantCode).then(res => { setToken(res.token) commit('SET_TOKEN', res.token) resolve() @@ -67,9 +68,13 @@ const user = { if (!isHttp(avatar)) { avatar = (isEmpty(avatar)) ? defAva : process.env.VUE_APP_BASE_API + avatar } - if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组 - commit('SET_ROLES', res.roles) + // 设置权限(独立于角色,解决跨租户角色关联时permissions无法设置的问题) + if (res.permissions && res.permissions.length > 0) { commit('SET_PERMISSIONS', res.permissions) + } + // 设置角色 + if (res.roles && res.roles.length > 0) { + commit('SET_ROLES', res.roles) } else { commit('SET_ROLES', ['ROLE_DEFAULT']) } diff --git a/ruoyi-ui/src/views/login.vue b/ruoyi-ui/src/views/login.vue index aa298711d..535580a76 100644 --- a/ruoyi-ui/src/views/login.vue +++ b/ruoyi-ui/src/views/login.vue @@ -2,6 +2,16 @@