feat(tenant): 完善多租户功能及相关数据结构

- 在登录流程中新增租户编码支持,允许用户通过租户编码登录。
- 更新SysLoginController和SysLoginService以处理租户信息。
- 在字典数据和字典类型实体中添加租户ID字段,支持租户级字典管理。
- 扩展字典数据和用户查询功能,支持按租户ID过滤。
- 更新前端登录页面,添加租户编码输入框,优化用户体验。
- 修改相关Mapper和Service以支持租户数据的插入和查询。
pull/1126/head
9264yf 2025-12-20 16:15:47 +08:00
parent 158e666ef2
commit 05adf0b31b
29 changed files with 464 additions and 60 deletions

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -22,6 +22,9 @@ public class SysDictData extends BaseEntity
@Excel(name = "字典编码", cellType = ColumnType.NUMERIC)
private Long dictCode;
/** 租户ID0=系统级) */
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())

View File

@ -22,6 +22,9 @@ public class SysDictType extends BaseEntity
@Excel(name = "字典主键", cellType = ColumnType.NUMERIC)
private Long dictId;
/** 租户ID0=系统级) */
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())

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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
*

View File

@ -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

View File

@ -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);

View File

@ -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);
}

View File

@ -21,6 +21,14 @@ public interface SysTenantMapper
*/
public SysTenant selectSysTenantByTenantId(Long tenantId);
/**
*
*
* @param tenantCode
* @return
*/
public SysTenant selectSysTenantByTenantCode(String tenantCode);
/**
*
*

View File

@ -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);
}

View File

@ -20,6 +20,14 @@ public interface ISysTenantService
*/
public SysTenant selectSysTenantByTenantId(Long tenantId);
/**
*
*
* @param tenantCode
* @return
*/
public SysTenant selectSysTenantByTenantCode(String tenantCode);
/**
*
*

View File

@ -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
*

View File

@ -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)
{

View File

@ -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)
{

View File

@ -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);
}
/**
*
*

View File

@ -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;

View File

@ -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>

View File

@ -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') &lt;= 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>

View File

@ -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>

View File

@ -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=",">

View File

@ -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}

View File

@ -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',

View File

@ -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'])
}

View File

@ -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,

View File

@ -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 = "添加角色"
},

View File

@ -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);

View File

@ -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 '租户ID0=系统级)' 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 '租户ID0=系统级)' 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);