feat(tenant): 完善多租户功能及相关数据结构
- 在登录流程中新增租户编码支持,允许用户通过租户编码登录。 - 更新SysLoginController和SysLoginService以处理租户信息。 - 在字典数据和字典类型实体中添加租户ID字段,支持租户级字典管理。 - 扩展字典数据和用户查询功能,支持按租户ID过滤。 - 更新前端登录页面,添加租户编码输入框,优化用户体验。 - 修改相关Mapper和Service以支持租户数据的插入和查询。pull/1126/head
parent
158e666ef2
commit
05adf0b31b
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,14 +53,22 @@ public class TenantConfig {
|
|||
"hl_order"
|
||||
);
|
||||
|
||||
/**
|
||||
* 混合模式表(系统级 + 租户级共存)
|
||||
* Reason: 这些表同时支持系统级数据(tenant_id=0)和租户级数据
|
||||
* 查询时使用 WHERE (tenant_id = ? OR tenant_id = 0)
|
||||
*/
|
||||
public static final List<String> HYBRID_TABLES = Arrays.asList(
|
||||
"sys_dict_type",
|
||||
"sys_dict_data"
|
||||
);
|
||||
|
||||
/**
|
||||
* 不需要租户隔离的表(白名单)
|
||||
* Reason: 这些表是全局共享的,所有租户共用
|
||||
*/
|
||||
public static final List<String> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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,6 +57,9 @@ public class SysLoginService
|
|||
@Autowired
|
||||
private ISysConfigService configService;
|
||||
|
||||
@Autowired
|
||||
private ISysTenantService tenantService;
|
||||
|
||||
/**
|
||||
* 登录验证
|
||||
*
|
||||
|
|
@ -62,9 +67,10 @@ public class SysLoginService
|
|||
* @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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -92,4 +92,14 @@ public interface SysDictDataMapper
|
|||
* @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<SysDictData> selectDictDataByTypeWithTenant(@Param("dictType") String dictType, @Param("tenantId") Long tenantId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,14 @@ public interface SysTenantMapper
|
|||
*/
|
||||
public SysTenant selectSysTenantByTenantId(Long tenantId);
|
||||
|
||||
/**
|
||||
* 通过租户编码查询租户信息
|
||||
*
|
||||
* @param tenantCode 租户编码
|
||||
* @return 租户信息
|
||||
*/
|
||||
public SysTenant selectSysTenantByTenantCode(String tenantCode);
|
||||
|
||||
/**
|
||||
* 查询租户信息列表
|
||||
*
|
||||
|
|
|
|||
|
|
@ -44,6 +44,15 @@ public interface SysUserMapper
|
|||
*/
|
||||
public SysUser selectUserByUserName(String userName);
|
||||
|
||||
/**
|
||||
* 通过用户名和租户ID查询用户
|
||||
*
|
||||
* @param userName 用户名
|
||||
* @param tenantId 租户ID
|
||||
* @return 用户对象信息
|
||||
*/
|
||||
public SysUser selectUserByUserNameAndTenantId(@Param("userName") String userName, @Param("tenantId") Long tenantId);
|
||||
|
||||
/**
|
||||
* 通过用户ID查询用户
|
||||
*
|
||||
|
|
@ -125,23 +134,26 @@ 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,14 @@ public interface ISysTenantService
|
|||
*/
|
||||
public SysTenant selectSysTenantByTenantId(Long tenantId);
|
||||
|
||||
/**
|
||||
* 通过租户编码查询租户信息
|
||||
*
|
||||
* @param tenantCode 租户编码
|
||||
* @return 租户信息
|
||||
*/
|
||||
public SysTenant selectSysTenantByTenantCode(String tenantCode);
|
||||
|
||||
/**
|
||||
* 查询租户信息列表
|
||||
*
|
||||
|
|
|
|||
|
|
@ -43,6 +43,15 @@ public interface ISysUserService
|
|||
*/
|
||||
public SysUser selectUserByUserName(String userName);
|
||||
|
||||
/**
|
||||
* 通过用户名和租户ID查询用户
|
||||
*
|
||||
* @param userName 用户名
|
||||
* @param tenantId 租户ID
|
||||
* @return 用户对象信息
|
||||
*/
|
||||
public SysUser selectUserByUserNameAndTenantId(String userName, Long tenantId);
|
||||
|
||||
/**
|
||||
* 通过用户ID查询用户
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -82,6 +83,10 @@ public class SysDictDataServiceImpl implements ISysDictDataService
|
|||
@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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,9 +134,14 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService
|
|||
|
||||
/**
|
||||
* 加载字典缓存数据
|
||||
* Reason: 启动时设置全局模式,跳过租户过滤
|
||||
*/
|
||||
@Override
|
||||
public void loadingDictCache()
|
||||
{
|
||||
// 启动时加载缓存,设置全局模式跳过租户过滤
|
||||
TenantContext.setIgnore(true);
|
||||
try
|
||||
{
|
||||
SysDictData dictData = new SysDictData();
|
||||
dictData.setStatus("0");
|
||||
|
|
@ -145,6 +151,11 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService
|
|||
DictUtils.setDictCache(entry.getKey(), entry.getValue().stream().sorted(Comparator.comparing(SysDictData::getDictSort)).collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
TenantContext.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空字典缓存数据
|
||||
|
|
@ -174,6 +185,10 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService
|
|||
@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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询租户信息列表
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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,6 +178,7 @@ 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;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
|
||||
<resultMap type="SysDictData" id="SysDictDataResult">
|
||||
<id property="dictCode" column="dict_code" />
|
||||
<result property="tenantId" column="tenant_id" />
|
||||
<result property="dictSort" column="dict_sort" />
|
||||
<result property="dictLabel" column="dict_label" />
|
||||
<result property="dictValue" column="dict_value" />
|
||||
|
|
@ -21,7 +22,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
</resultMap>
|
||||
|
||||
<sql id="selectDictDataVo">
|
||||
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
|
||||
</sql>
|
||||
|
||||
|
|
@ -95,6 +96,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
|
||||
<insert id="insertDictData" parameterType="SysDictData">
|
||||
insert into sys_dict_data(
|
||||
<if test="tenantId != null">tenant_id,</if>
|
||||
<if test="dictSort != null">dict_sort,</if>
|
||||
<if test="dictLabel != null and dictLabel != ''">dict_label,</if>
|
||||
<if test="dictValue != null and dictValue != ''">dict_value,</if>
|
||||
|
|
@ -107,6 +109,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
<if test="createBy != null and createBy != ''">create_by,</if>
|
||||
create_time
|
||||
)values(
|
||||
<if test="tenantId != null">#{tenantId},</if>
|
||||
<if test="dictSort != null">#{dictSort},</if>
|
||||
<if test="dictLabel != null and dictLabel != ''">#{dictLabel},</if>
|
||||
<if test="dictValue != null and dictValue != ''">#{dictValue},</if>
|
||||
|
|
@ -121,4 +124,20 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
)
|
||||
</insert>
|
||||
|
||||
<!-- 混合模式查询:合并系统字典和租户字典,租户优先 -->
|
||||
<select id="selectDictDataByTypeWithTenant" resultMap="SysDictDataResult">
|
||||
SELECT d1.* FROM sys_dict_data d1
|
||||
WHERE d1.dict_type = #{dictType}
|
||||
AND d1.status = '0'
|
||||
AND d1.tenant_id IN (0, #{tenantId})
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_dict_data d2
|
||||
WHERE d2.dict_type = d1.dict_type
|
||||
AND d2.dict_value = d1.dict_value
|
||||
AND d2.tenant_id = #{tenantId}
|
||||
AND d1.tenant_id = 0
|
||||
)
|
||||
ORDER BY d1.dict_sort ASC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
|
@ -6,6 +6,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
|
||||
<resultMap type="SysDictType" id="SysDictTypeResult">
|
||||
<id property="dictId" column="dict_id" />
|
||||
<result property="tenantId" column="tenant_id" />
|
||||
<result property="dictName" column="dict_name" />
|
||||
<result property="dictType" column="dict_type" />
|
||||
<result property="status" column="status" />
|
||||
|
|
@ -16,7 +17,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
</resultMap>
|
||||
|
||||
<sql id="selectDictTypeVo">
|
||||
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
|
||||
</sql>
|
||||
|
||||
|
|
@ -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')
|
||||
</if>
|
||||
</where>
|
||||
order by create_time desc
|
||||
</select>
|
||||
|
||||
<select id="selectDictTypeAll" resultMap="SysDictTypeResult">
|
||||
|
|
@ -86,6 +88,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
|
||||
<insert id="insertDictType" parameterType="SysDictType">
|
||||
insert into sys_dict_type(
|
||||
<if test="tenantId != null">tenant_id,</if>
|
||||
<if test="dictName != null and dictName != ''">dict_name,</if>
|
||||
<if test="dictType != null and dictType != ''">dict_type,</if>
|
||||
<if test="status != null">status,</if>
|
||||
|
|
@ -93,6 +96,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
<if test="createBy != null and createBy != ''">create_by,</if>
|
||||
create_time
|
||||
)values(
|
||||
<if test="tenantId != null">#{tenantId},</if>
|
||||
<if test="dictName != null and dictName != ''">#{dictName},</if>
|
||||
<if test="dictType != null and dictType != ''">#{dictType},</if>
|
||||
<if test="status != null">#{status},</if>
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
<if test="status != null and status != ''">status,</if>
|
||||
<if test="remark != null and remark != ''">remark,</if>
|
||||
<if test="createBy != null and createBy != ''">create_by,</if>
|
||||
<if test="tenantId != null">tenant_id,</if>
|
||||
create_time
|
||||
)values(
|
||||
<if test="roleId != null and roleId != 0">#{roleId},</if>
|
||||
|
|
@ -117,6 +118,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
<if test="status != null and status != ''">#{status},</if>
|
||||
<if test="remark != null and remark != ''">#{remark},</if>
|
||||
<if test="createBy != null and createBy != ''">#{createBy},</if>
|
||||
<if test="tenantId != null">#{tenantId},</if>
|
||||
sysdate()
|
||||
)
|
||||
</insert>
|
||||
|
|
|
|||
|
|
@ -43,6 +43,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
where tenant_id = #{tenantId}
|
||||
</select>
|
||||
|
||||
<select id="selectSysTenantByTenantCode" parameterType="String" resultMap="SysTenantResult">
|
||||
<include refid="selectSysTenantVo"/>
|
||||
where tenant_code = #{tenantCode} and del_flag = '0' and status = '0'
|
||||
</select>
|
||||
|
||||
<insert id="insertSysTenant" parameterType="SysTenant" useGeneratedKeys="true" keyProperty="tenantId">
|
||||
insert into sys_tenant
|
||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||
|
|
|
|||
|
|
@ -127,21 +127,26 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
where u.user_name = #{userName} and u.del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="selectUserByUserNameAndTenantId" resultMap="SysUserResult">
|
||||
<include refid="selectUserVo"/>
|
||||
where u.user_name = #{userName} and u.tenant_id = #{tenantId} and u.del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="selectUserById" parameterType="Long" resultMap="SysUserResult">
|
||||
<include refid="selectUserVo"/>
|
||||
where u.user_id = #{userId}
|
||||
</select>
|
||||
|
||||
<select id="checkUserNameUnique" parameterType="String" resultMap="SysUserResult">
|
||||
select user_id, user_name from sys_user where user_name = #{userName} and del_flag = '0' limit 1
|
||||
<select id="checkUserNameUnique" resultMap="SysUserResult">
|
||||
select user_id, user_name from sys_user where user_name = #{userName} and tenant_id = #{tenantId} and del_flag = '0' limit 1
|
||||
</select>
|
||||
|
||||
<select id="checkPhoneUnique" parameterType="String" resultMap="SysUserResult">
|
||||
select user_id, phonenumber from sys_user where phonenumber = #{phonenumber} and del_flag = '0' limit 1
|
||||
<select id="checkPhoneUnique" resultMap="SysUserResult">
|
||||
select user_id, phonenumber from sys_user where phonenumber = #{phonenumber} and tenant_id = #{tenantId} and del_flag = '0' limit 1
|
||||
</select>
|
||||
|
||||
<select id="checkEmailUnique" parameterType="String" resultMap="SysUserResult">
|
||||
select user_id, email from sys_user where email = #{email} and del_flag = '0' limit 1
|
||||
<select id="checkEmailUnique" resultMap="SysUserResult">
|
||||
select user_id, email from sys_user where email = #{email} and tenant_id = #{tenantId} and del_flag = '0' limit 1
|
||||
</select>
|
||||
|
||||
<insert id="insertUser" parameterType="SysUser" useGeneratedKeys="true" keyProperty="userId">
|
||||
|
|
@ -195,7 +200,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
<if test="loginDate != null">login_date = #{loginDate},</if>
|
||||
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
|
||||
<if test="remark != null">remark = #{remark},</if>
|
||||
<if test="tenantId != null">tenant_id = #{tenantId},</if>
|
||||
update_time = sysdate()
|
||||
</set>
|
||||
where user_id = #{userId}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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'])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,16 @@
|
|||
<div class="login">
|
||||
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
|
||||
<h3 class="title">{{title}}</h3>
|
||||
<el-form-item prop="tenantCode">
|
||||
<el-input
|
||||
v-model="loginForm.tenantCode"
|
||||
type="text"
|
||||
auto-complete="off"
|
||||
placeholder="租户编码(管理员留空)"
|
||||
>
|
||||
<svg-icon slot="prefix" icon-class="tree" class="el-input__icon input-icon" />
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
|
|
@ -73,6 +83,7 @@ export default {
|
|||
title: process.env.VUE_APP_TITLE,
|
||||
codeUrl: "",
|
||||
loginForm: {
|
||||
tenantCode: "",
|
||||
username: "admin",
|
||||
password: "admin123",
|
||||
rememberMe: false,
|
||||
|
|
|
|||
|
|
@ -158,6 +158,16 @@
|
|||
<!-- 添加或修改角色配置对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="所属租户" prop="tenantId" v-if="isSuperAdmin">
|
||||
<el-select v-model="form.tenantId" placeholder="请选择所属租户" clearable style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in tenantList"
|
||||
:key="item.tenantId"
|
||||
:label="item.tenantName"
|
||||
:value="item.tenantId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色名称" prop="roleName">
|
||||
<el-input v-model="form.roleName" placeholder="请输入角色名称" />
|
||||
</el-form-item>
|
||||
|
|
@ -254,6 +264,7 @@
|
|||
<script>
|
||||
import { listRole, getRole, delRole, addRole, updateRole, dataScope, changeRoleStatus, deptTreeSelect } from "@/api/system/role"
|
||||
import { treeselect as menuTreeselect, roleMenuTreeselect } from "@/api/system/menu"
|
||||
import { listTenant } from "@/api/system/tenant"
|
||||
|
||||
export default {
|
||||
name: "Role",
|
||||
|
|
@ -313,6 +324,8 @@ export default {
|
|||
menuOptions: [],
|
||||
// 部门列表
|
||||
deptOptions: [],
|
||||
// 租户列表
|
||||
tenantList: [],
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
|
|
@ -344,6 +357,13 @@ export default {
|
|||
created() {
|
||||
this.getList()
|
||||
},
|
||||
computed: {
|
||||
// 判断是否为超级管理员(拥有*:*:*权限)
|
||||
isSuperAdmin() {
|
||||
const permissions = this.$store.getters && this.$store.getters.permissions
|
||||
return permissions && permissions.includes('*:*:*')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 查询角色列表 */
|
||||
getList() {
|
||||
|
|
@ -361,6 +381,12 @@ export default {
|
|||
this.menuOptions = response.data
|
||||
})
|
||||
},
|
||||
/** 查询租户列表 */
|
||||
getTenantList() {
|
||||
listTenant({ status: '0' }).then(response => {
|
||||
this.tenantList = response.rows
|
||||
})
|
||||
},
|
||||
// 所有菜单节点数据
|
||||
getMenuAllCheckedKeys() {
|
||||
// 目前被选中的菜单节点
|
||||
|
|
@ -433,7 +459,8 @@ export default {
|
|||
deptIds: [],
|
||||
menuCheckStrictly: true,
|
||||
deptCheckStrictly: true,
|
||||
remark: undefined
|
||||
remark: undefined,
|
||||
tenantId: undefined
|
||||
}
|
||||
this.resetForm("form")
|
||||
},
|
||||
|
|
@ -501,6 +528,10 @@ export default {
|
|||
handleAdd() {
|
||||
this.reset()
|
||||
this.getMenuTreeselect()
|
||||
// 超级管理员获取租户列表
|
||||
if (this.isSuperAdmin) {
|
||||
this.getTenantList()
|
||||
}
|
||||
this.open = true
|
||||
this.title = "添加角色"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
-- ============================================
|
||||
-- Bug 修复脚本 - 2025-12-20
|
||||
-- ============================================
|
||||
|
||||
-- Bug 1: 修复基础套餐菜单配置(添加字典管理菜单)
|
||||
-- 原因:基础套餐缺少字典管理菜单(ID=105)及其子菜单权限
|
||||
|
||||
-- 1. 添加字典管理主菜单
|
||||
INSERT IGNORE INTO sys_package_menu (package_id, menu_id) VALUES (1, 105);
|
||||
|
||||
-- 2. 添加字典管理子菜单权限(1025-1029)
|
||||
INSERT IGNORE INTO sys_package_menu (package_id, menu_id) VALUES (1, 1025); -- 字典查询
|
||||
INSERT IGNORE INTO sys_package_menu (package_id, menu_id) VALUES (1, 1026); -- 字典新增
|
||||
INSERT IGNORE INTO sys_package_menu (package_id, menu_id) VALUES (1, 1027); -- 字典修改
|
||||
INSERT IGNORE INTO sys_package_menu (package_id, menu_id) VALUES (1, 1028); -- 字典删除
|
||||
INSERT IGNORE INTO sys_package_menu (package_id, menu_id) VALUES (1, 1029); -- 字典导出
|
||||
|
||||
-- 验证修复结果
|
||||
SELECT pm.package_id, pm.menu_id, m.menu_name
|
||||
FROM sys_package_menu pm
|
||||
LEFT JOIN sys_menu m ON pm.menu_id = m.menu_id
|
||||
WHERE pm.package_id = 1 AND pm.menu_id IN (105, 1025, 1026, 1027, 1028, 1029);
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
-- =============================================
|
||||
-- 字典表混合模式租户隔离迁移脚本
|
||||
-- 执行前请备份数据库
|
||||
-- =============================================
|
||||
|
||||
-- 1. 为 sys_dict_type 添加 tenant_id 字段
|
||||
ALTER TABLE sys_dict_type
|
||||
ADD COLUMN tenant_id BIGINT(20) NOT NULL DEFAULT 0 COMMENT '租户ID(0=系统级)' AFTER dict_id;
|
||||
|
||||
-- 2. 为 sys_dict_data 添加 tenant_id 字段
|
||||
ALTER TABLE sys_dict_data
|
||||
ADD COLUMN tenant_id BIGINT(20) NOT NULL DEFAULT 0 COMMENT '租户ID(0=系统级)' AFTER dict_code;
|
||||
|
||||
-- 3. 删除旧的唯一约束
|
||||
ALTER TABLE sys_dict_type DROP INDEX dict_type;
|
||||
|
||||
-- 4. 创建新的复合唯一约束(包含 tenant_id)
|
||||
ALTER TABLE sys_dict_type ADD UNIQUE INDEX uk_tenant_dict_type (tenant_id, dict_type);
|
||||
ALTER TABLE sys_dict_data ADD UNIQUE INDEX uk_tenant_dict_data (tenant_id, dict_type, dict_value);
|
||||
|
||||
-- 5. 添加索引优化查询
|
||||
CREATE INDEX idx_dict_type_tenant ON sys_dict_type(tenant_id);
|
||||
CREATE INDEX idx_dict_data_tenant ON sys_dict_data(tenant_id);
|
||||
Loading…
Reference in New Issue