diff --git a/pom.xml b/pom.xml
index d50117569..200de75c0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -30,6 +30,9 @@
4.1.2
2.3
0.9.1
+
+ 2.14.3
+ 4.6
9.0.108
1.2.13
@@ -218,6 +221,20 @@
${ruoyi.version}
+
+
+ com.alibaba
+ transmittable-thread-local
+ ${transmittable-thread-local.version}
+
+
+
+
+ com.github.jsqlparser
+ jsqlparser
+ ${jsqlparser.version}
+
+
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysTenantController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysTenantController.java
new file mode 100644
index 000000000..b6e383945
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysTenantController.java
@@ -0,0 +1,106 @@
+package com.ruoyi.web.controller.system;
+
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+
+import com.ruoyi.system.domain.SysTenant;
+import com.ruoyi.system.service.ISysTenantService;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.common.core.page.TableDataInfo;
+
+/**
+ * 租户信息Controller
+ *
+ * @author W-yf
+ * @date 2025-12-19
+ */
+@RestController
+@RequestMapping("/link/tenant")
+public class SysTenantController extends BaseController
+{
+ @Autowired
+ private ISysTenantService sysTenantService;
+
+ /**
+ * 查询租户信息列表
+ */
+ @PreAuthorize("@ss.hasPermi('link:tenant:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(
+ SysTenant sysTenant)
+ {
+ startPage();
+ List list = sysTenantService.selectSysTenantList(sysTenant);
+ return getDataTable(list);
+ }
+
+ /**
+ * 导出租户信息列表
+ */
+ @PreAuthorize("@ss.hasPermi('link:tenant:export')")
+ @Log(title = "租户信息", businessType = BusinessType.EXPORT)
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, SysTenant sysTenant)
+ {
+ List list = sysTenantService.selectSysTenantList(sysTenant);
+ ExcelUtil util = new ExcelUtil(SysTenant.class);
+ util.exportExcel(response, list, "租户信息数据");
+ }
+
+ /**
+ * 获取租户信息详细信息
+ */
+ @PreAuthorize("@ss.hasPermi('link:tenant:query')")
+ @GetMapping(value = "/{tenantId}")
+ public AjaxResult getInfo(@PathVariable("tenantId") Long tenantId)
+ {
+ return success(sysTenantService.selectSysTenantByTenantId(tenantId));
+ }
+
+ /**
+ * 新增租户信息
+ */
+ @PreAuthorize("@ss.hasPermi('link:tenant:add')")
+ @Log(title = "租户信息", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@RequestBody SysTenant sysTenant)
+ {
+ return toAjax(sysTenantService.insertSysTenant(sysTenant));
+ }
+
+ /**
+ * 修改租户信息
+ */
+ @PreAuthorize("@ss.hasPermi('link:tenant:edit')")
+ @Log(title = "租户信息", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@RequestBody SysTenant sysTenant)
+ {
+ return toAjax(sysTenantService.updateSysTenant(sysTenant));
+ }
+
+ /**
+ * 删除租户信息
+ */
+ @PreAuthorize("@ss.hasPermi('link:tenant:remove')")
+ @Log(title = "租户信息", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{tenantIds}")
+ public AjaxResult remove(@PathVariable Long[] tenantIds)
+ {
+ return toAjax(sysTenantService.deleteSysTenantByTenantIds(tenantIds));
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java
index 11790f9ba..5fabb38e8 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java
@@ -27,9 +27,11 @@ import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.system.domain.SysTenant;
import com.ruoyi.system.service.ISysDeptService;
import com.ruoyi.system.service.ISysPostService;
import com.ruoyi.system.service.ISysRoleService;
+import com.ruoyi.system.service.ISysTenantService;
import com.ruoyi.system.service.ISysUserService;
/**
@@ -53,6 +55,9 @@ public class SysUserController extends BaseController
@Autowired
private ISysPostService postService;
+ @Autowired
+ private ISysTenantService tenantService;
+
/**
* 获取用户列表
*/
@@ -113,6 +118,13 @@ public class SysUserController extends BaseController
List roles = roleService.selectRoleAll();
ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList()));
ajax.put("posts", postService.selectPostAll());
+ // 如果是超级管理员,返回租户列表
+ if (SysUser.isAdmin(SecurityUtils.getUserId()))
+ {
+ SysTenant query = new SysTenant();
+ query.setStatus("0");
+ ajax.put("tenants", tenantService.selectSysTenantList(query));
+ }
return ajax;
}
diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml
index f71e8c74c..af6506b9d 100644
--- a/ruoyi-common/pom.xml
+++ b/ruoyi-common/pom.xml
@@ -119,6 +119,12 @@
javax.servlet-api
+
+
+ com.alibaba
+ transmittable-thread-local
+
+
\ No newline at end of file
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
new file mode 100644
index 000000000..323b5431b
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/config/TenantConfig.java
@@ -0,0 +1,122 @@
+package com.ruoyi.common.config;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 租户配置类
+ *
+ * Reason: 集中管理租户相关配置,包括超级管理员列表
+ */
+public class TenantConfig {
+
+ /**
+ * 超级管理员用户ID列表
+ * Reason: 使用固定用户ID判断,简单可靠,避免角色权限体系的复杂度
+ *
+ * 超级管理员拥有以下特权:
+ * 1. 登录后自动进入全局模式
+ * 2. 可以查看所有租户的数据
+ * 3. 所有操作都会记录审计日志
+ */
+ public static final List SUPER_ADMIN_USER_IDS = Arrays.asList(
+ 1L // admin 用户
+ );
+
+ /**
+ * 忽略租户过滤的URL路径(不需要租户隔离的接口)
+ * Reason: 登录、验证码等公共接口不需要租户上下文
+ */
+ public static final List IGNORE_URLS = Arrays.asList(
+ "/login",
+ "/captchaImage",
+ "/logout",
+ "/register"
+ );
+
+ /**
+ * 需要进行租户隔离的表名列表
+ * Reason: 明确定义需要拦截的表,避免误拦截
+ */
+ public static final List TENANT_TABLES = Arrays.asList(
+ // 若依框架表
+ "sys_user",
+ "sys_dept",
+ "sys_role",
+ "sys_post",
+ "sys_notice",
+ // HairLink业务表
+ "hl_customer",
+ "hl_member_card",
+ "hl_service",
+ "hl_staff",
+ "hl_order"
+ );
+
+ /**
+ * 不需要租户隔离的表(白名单)
+ * 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",
+ "sys_role_dept",
+ "sys_tenant",
+ "sys_logininfor",
+ "sys_oper_log",
+ "sys_job",
+ "sys_job_log"
+ );
+
+ /**
+ * 判断是否是超级管理员
+ *
+ * @param userId 用户ID
+ * @return true-超级管理员,false-普通用户
+ */
+ public static boolean isSuperAdmin(Long userId) {
+ if (userId == null) {
+ return false;
+ }
+ return SUPER_ADMIN_USER_IDS.contains(userId);
+ }
+
+ /**
+ * 判断URL是否需要忽略租户过滤
+ *
+ * @param url 请求URL
+ * @return true-忽略,false-需要过滤
+ */
+ public static boolean isIgnoreUrl(String url) {
+ if (url == null) {
+ return false;
+ }
+ return IGNORE_URLS.stream().anyMatch(url::contains);
+ }
+
+ /**
+ * 判断表是否需要租户隔离
+ *
+ * @param tableName 表名
+ * @return true-需要隔离,false-不需要
+ */
+ public static boolean needTenantFilter(String tableName) {
+ if (tableName == null) {
+ return false;
+ }
+
+ String lowerTableName = tableName.toLowerCase();
+
+ // 白名单表不需要过滤
+ if (IGNORE_TABLES.stream().anyMatch(lowerTableName::equals)) {
+ return false;
+ }
+
+ // 租户表需要过滤
+ return TENANT_TABLES.stream().anyMatch(lowerTableName::equals);
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/context/TenantContext.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/context/TenantContext.java
new file mode 100644
index 000000000..0927a3c66
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/context/TenantContext.java
@@ -0,0 +1,98 @@
+package com.ruoyi.common.core.context;
+
+import com.alibaba.ttl.TransmittableThreadLocal;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 租户上下文 - 基于ThreadLocal实现租户ID的传递
+ *
+ * Reason: 使用TransmittableThreadLocal支持线程池场景下的上下文传递
+ */
+public class TenantContext {
+
+ private static final Logger log = LoggerFactory.getLogger(TenantContext.class);
+
+ /**
+ * 支持父子线程传递的ThreadLocal
+ * Reason: 若依框架使用了异步任务和线程池,普通ThreadLocal会丢失上下文
+ */
+ private static final TransmittableThreadLocal TENANT_ID_HOLDER = new TransmittableThreadLocal<>();
+
+ /**
+ * 忽略租户过滤的标志(用于超级管理员或系统任务)
+ */
+ private static final TransmittableThreadLocal IGNORE_TENANT = new TransmittableThreadLocal<>();
+
+ /**
+ * 设置当前租户ID
+ *
+ * @param tenantId 租户ID
+ */
+ public static void setTenantId(Long tenantId) {
+ if (tenantId == null) {
+ log.warn("尝试设置NULL租户ID,已拒绝");
+ return;
+ }
+ TENANT_ID_HOLDER.set(tenantId);
+ log.debug("TenantContext设置租户ID: {}", tenantId);
+ }
+
+ /**
+ * 获取当前租户ID
+ *
+ * @return 租户ID
+ */
+ public static Long getTenantId() {
+ return TENANT_ID_HOLDER.get();
+ }
+
+ /**
+ * 清除租户上下文
+ * Reason: 请求结束后必须清除,避免线程池复用时的上下文污染
+ */
+ public static void clear() {
+ TENANT_ID_HOLDER.remove();
+ IGNORE_TENANT.remove();
+ log.trace("TenantContext已清除");
+ }
+
+ /**
+ * 设置忽略租户过滤(超级管理员专用)
+ *
+ * @param ignore 是否忽略租户过滤
+ */
+ public static void setIgnore(boolean ignore) {
+ IGNORE_TENANT.set(ignore);
+ if (ignore) {
+ log.warn("租户过滤已禁用,当前处于全局模式!请谨慎操作");
+ } else {
+ log.info("租户过滤已启用");
+ }
+ }
+
+ /**
+ * 是否忽略租户过滤
+ *
+ * @return true-忽略(全局模式),false-正常过滤
+ */
+ public static boolean isIgnore() {
+ Boolean ignore = IGNORE_TENANT.get();
+ return ignore != null && ignore;
+ }
+
+ /**
+ * 获取当前租户ID(如果不存在则抛出异常)
+ *
+ * @return 租户ID
+ * @throws com.ruoyi.common.exception.ServiceException 租户ID不存在时抛出
+ */
+ public static Long getRequiredTenantId() {
+ Long tenantId = getTenantId();
+ if (tenantId == null && !isIgnore()) {
+ log.error("【安全警告】租户上下文丢失,当前操作被拦截");
+ throw new RuntimeException("租户上下文丢失,请重新登录");
+ }
+ return tenantId;
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java
index 67269ff6b..e995d07ca 100644
--- a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java
@@ -38,6 +38,10 @@ public class BaseEntity implements Serializable
/** 备注 */
private String remark;
+ /** 租户ID */
+ @JsonIgnore
+ private Long tenantId;
+
/** 请求参数 */
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private Map params;
@@ -115,4 +119,14 @@ public class BaseEntity implements Serializable
{
this.params = params;
}
+
+ public Long getTenantId()
+ {
+ return tenantId;
+ }
+
+ public void setTenantId(Long tenantId)
+ {
+ this.tenantId = tenantId;
+ }
}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
index cf4e5cb48..06a10d741 100644
--- a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
@@ -92,6 +92,9 @@ public class SysUser extends BaseEntity
/** 角色ID */
private Long roleId;
+ /** 租户ID */
+ private Long tenantId;
+
public SysUser()
{
@@ -310,6 +313,16 @@ public class SysUser extends BaseEntity
this.roleId = roleId;
}
+ public Long getTenantId()
+ {
+ return tenantId;
+ }
+
+ public void setTenantId(Long tenantId)
+ {
+ this.tenantId = tenantId;
+ }
+
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java
index 95e169cf3..31bad0223 100644
--- a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java
@@ -26,6 +26,11 @@ public class LoginUser implements UserDetails
*/
private Long deptId;
+ /**
+ * 租户ID
+ */
+ private Long tenantId;
+
/**
* 用户唯一标识
*/
@@ -258,6 +263,16 @@ public class LoginUser implements UserDetails
this.user = user;
}
+ public Long getTenantId()
+ {
+ return tenantId;
+ }
+
+ public void setTenantId(Long tenantId)
+ {
+ this.tenantId = tenantId;
+ }
+
@Override
public Collection extends GrantedAuthority> getAuthorities()
{
diff --git a/ruoyi-framework/pom.xml b/ruoyi-framework/pom.xml
index cc18dacdc..855729583 100644
--- a/ruoyi-framework/pom.xml
+++ b/ruoyi-framework/pom.xml
@@ -59,6 +59,12 @@
ruoyi-system
+
+
+ com.github.jsqlparser
+ jsqlparser
+
+
\ No newline at end of file
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java
index e30fe74fe..c72b69002 100644
--- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java
@@ -23,10 +23,11 @@ import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.ClassUtils;
import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.framework.interceptor.TenantSqlInterceptor;
/**
* Mybatis支持*匹配扫描包
- *
+ *
* @author ruoyi
*/
@Configuration
@@ -35,6 +36,9 @@ public class MyBatisConfig
@Autowired
private Environment env;
+ @Autowired
+ private TenantSqlInterceptor tenantSqlInterceptor;
+
static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
public static String setTypeAliasesPackage(String typeAliasesPackage)
@@ -127,6 +131,10 @@ public class MyBatisConfig
sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
+
+ // 【多租户】注册MyBatis租户SQL拦截器
+ sessionFactory.setPlugins(tenantSqlInterceptor);
+
return sessionFactory.getObject();
}
}
\ No newline at end of file
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java
index 12d354946..7f73082b6 100644
--- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java
@@ -14,10 +14,11 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor;
+import com.ruoyi.framework.interceptor.TenantInterceptor;
/**
* 通用配置
- *
+ *
* @author ruoyi
*/
@Configuration
@@ -26,6 +27,9 @@ public class ResourcesConfig implements WebMvcConfigurer
@Autowired
private RepeatSubmitInterceptor repeatSubmitInterceptor;
+ @Autowired
+ private TenantInterceptor tenantInterceptor;
+
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry)
{
@@ -45,6 +49,10 @@ public class ResourcesConfig implements WebMvcConfigurer
@Override
public void addInterceptors(InterceptorRegistry registry)
{
+ // 【多租户】租户拦截器 - 必须在第一位,优先设置租户上下文
+ registry.addInterceptor(tenantInterceptor).addPathPatterns("/**");
+
+ // 防重复提交拦截器
registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/TenantInterceptor.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/TenantInterceptor.java
new file mode 100644
index 000000000..275009dc1
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/TenantInterceptor.java
@@ -0,0 +1,100 @@
+package com.ruoyi.framework.interceptor;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+import com.ruoyi.common.config.TenantConfig;
+import com.ruoyi.common.core.context.TenantContext;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.SecurityUtils;
+
+/**
+ * 租户拦截器 - 从LoginUser中提取tenant_id并设置到TenantContext
+ *
+ * Reason: 每个HTTP请求都需要识别当前租户,并将租户信息存入ThreadLocal
+ *
+ * 工作流程:
+ * 1. 请求进入 → preHandle()
+ * 2. 从SecurityContext获取LoginUser
+ * 3. 判断是否是超级管理员
+ * - 是:设置忽略租户过滤(全局模式)
+ * - 否:提取tenant_id并设置到TenantContext
+ * 4. 请求结束 → afterCompletion() → 清除TenantContext
+ */
+@Component
+public class TenantInterceptor implements HandlerInterceptor {
+
+ private static final Logger log = LoggerFactory.getLogger(TenantInterceptor.class);
+
+ /**
+ * 请求处理前的拦截
+ */
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
+ throws Exception {
+
+ String requestUri = request.getRequestURI();
+
+ // 判断是否是忽略URL(如登录、验证码接口)
+ if (TenantConfig.isIgnoreUrl(requestUri)) {
+ log.trace("忽略URL租户过滤: {}", requestUri);
+ return true;
+ }
+
+ try {
+ // 从SecurityContext获取当前登录用户
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+
+ if (loginUser != null) {
+ Long userId = loginUser.getUserId();
+
+ // 判断是否是超级管理员
+ if (TenantConfig.isSuperAdmin(userId)) {
+ // 超级管理员:忽略租户过滤(全局模式)
+ TenantContext.setIgnore(true);
+ log.warn("【超级管理员】用户 {} (ID:{}) 进入全局模式,可查看所有租户数据",
+ loginUser.getUsername(), userId);
+ }
+ else if (loginUser.getTenantId() != null) {
+ // 普通用户:设置租户ID
+ TenantContext.setTenantId(loginUser.getTenantId());
+ log.debug("【租户用户】用户 {} (ID:{}) 进入租户 {} 模式",
+ loginUser.getUsername(), userId, loginUser.getTenantId());
+ }
+ else {
+ // 未分配租户的普通用户(不允许访问)
+ log.error("【安全警告】用户 {} (ID:{}) 未分配租户且非超级管理员,拒绝访问",
+ loginUser.getUsername(), userId);
+ throw new ServiceException("用户未分配租户,请联系管理员");
+ }
+ } else {
+ // 未登录状态(可能是匿名访问或登录接口)
+ log.trace("未获取到LoginUser,跳过租户拦截: {}", requestUri);
+ }
+
+ } catch (ServiceException e) {
+ // 业务异常直接抛出
+ throw e;
+ } catch (Exception e) {
+ // 其他异常(可能是登录接口或匿名访问)
+ log.trace("获取租户上下文失败: {}, URI: {}", e.getMessage(), requestUri);
+ }
+
+ return true;
+ }
+
+ /**
+ * 请求完成后的清理
+ * Reason: 必须清除ThreadLocal,避免线程池复用时的上下文污染
+ */
+ @Override
+ public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
+ Object handler, Exception ex) throws Exception {
+ TenantContext.clear();
+ log.trace("请求完成,TenantContext已清除");
+ }
+}
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
new file mode 100644
index 000000000..79a1fa934
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/TenantSqlInterceptor.java
@@ -0,0 +1,319 @@
+package com.ruoyi.framework.interceptor;
+
+import java.sql.Connection;
+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.relational.EqualsTo;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.schema.Column;
+import net.sf.jsqlparser.schema.Table;
+import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.statement.delete.Delete;
+import net.sf.jsqlparser.statement.select.FromItem;
+import net.sf.jsqlparser.statement.select.PlainSelect;
+import net.sf.jsqlparser.statement.select.Select;
+import net.sf.jsqlparser.statement.update.Update;
+import org.apache.ibatis.executor.statement.StatementHandler;
+import org.apache.ibatis.mapping.BoundSql;
+import org.apache.ibatis.mapping.MappedStatement;
+import org.apache.ibatis.mapping.SqlCommandType;
+import org.apache.ibatis.plugin.Interceptor;
+import org.apache.ibatis.plugin.Intercepts;
+import org.apache.ibatis.plugin.Invocation;
+import org.apache.ibatis.plugin.Plugin;
+import org.apache.ibatis.plugin.Signature;
+import org.apache.ibatis.reflection.MetaObject;
+import org.apache.ibatis.reflection.SystemMetaObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import com.ruoyi.common.config.TenantConfig;
+import com.ruoyi.common.core.context.TenantContext;
+
+/**
+ * MyBatis租户SQL拦截器 - 自动在SQL中添加 tenant_id 过滤条件
+ *
+ * Reason: 借鉴若依DataScope模式,通过SQL解析实现租户隔离
+ *
+ * 工作原理:
+ * 1. 拦截MyBatis的StatementHandler.prepare()方法
+ * 2. 获取原始SQL和命令类型
+ * 3. 判断是否需要拦截(SELECT/UPDATE/DELETE + 租户表)
+ * 4. 使用JSQLParser解析SQL
+ * 5. 在WHERE子句中添加 tenant_id = ? 条件
+ * 6. 将改写后的SQL设置回BoundSql
+ */
+@Component
+@Intercepts({
+ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
+})
+public class TenantSqlInterceptor implements Interceptor {
+
+ private static final Logger log = LoggerFactory.getLogger(TenantSqlInterceptor.class);
+
+ @Override
+ public Object intercept(Invocation invocation) throws Throwable {
+ StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
+ MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
+
+ MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
+ SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
+
+ BoundSql boundSql = statementHandler.getBoundSql();
+ String originalSql = boundSql.getSql();
+
+ // 判断是否需要拦截
+ if (!shouldIntercept(sqlCommandType, originalSql)) {
+ return invocation.proceed();
+ }
+
+ // 超级管理员忽略租户过滤
+ if (TenantContext.isIgnore()) {
+ log.debug("【全局模式】跳过租户过滤: {}", originalSql.substring(0, Math.min(100, originalSql.length())));
+ return invocation.proceed();
+ }
+
+ // 获取租户ID
+ Long tenantId = TenantContext.getTenantId();
+ if (tenantId == null) {
+ log.error("【安全警告】租户ID为空且非全局模式,SQL被拦截: {}",
+ originalSql.substring(0, Math.min(100, originalSql.length())));
+ throw new ServiceException("租户上下文丢失,请重新登录");
+ }
+
+ // 解析并修改SQL
+ try {
+ String modifiedSql = processSql(originalSql, tenantId, sqlCommandType);
+ if (!modifiedSql.equals(originalSql)) {
+ metaObject.setValue("delegate.boundSql.sql", modifiedSql);
+ log.debug("【租户SQL拦截】租户ID: {}, 原SQL: {}", tenantId,
+ originalSql.substring(0, Math.min(100, originalSql.length())));
+ }
+ } catch (Exception e) {
+ log.error("租户SQL解析失败: {}, 原SQL: {}", e.getMessage(),
+ originalSql.substring(0, Math.min(100, originalSql.length())));
+ // SQL解析失败不影响执行,记录日志后继续
+ }
+
+ return invocation.proceed();
+ }
+
+ /**
+ * 判断是否需要拦截
+ *
+ * 核心逻辑:通过SQL解析获取主表名,只有主表是租户表时才拦截
+ * 这样可以避免:
+ * 1. 字符串子串匹配导致的误判(如 sys_role_menu 误匹配 sys_role)
+ * 2. JOIN表被错误过滤(只对主表添加租户条件)
+ *
+ * @param sqlCommandType SQL命令类型
+ * @param sql 原始SQL
+ * @return true-需要拦截,false-不需要
+ */
+ private boolean shouldIntercept(SqlCommandType sqlCommandType, String sql) {
+ // 只拦截 SELECT、UPDATE、DELETE
+ if (sqlCommandType != SqlCommandType.SELECT &&
+ sqlCommandType != SqlCommandType.UPDATE &&
+ sqlCommandType != SqlCommandType.DELETE) {
+ return false;
+ }
+
+ try {
+ // 解析SQL获取主表名
+ Statement statement = CCJSqlParserUtil.parse(sql);
+ String mainTableName = getMainTableName(statement);
+
+ // 只有主表是租户表时才拦截
+ return mainTableName != null && isTenantTable(mainTableName);
+ } catch (Exception e) {
+ // SQL解析失败,不拦截(避免影响正常业务)
+ log.debug("SQL解析失败,跳过租户拦截: {}", e.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * 从SQL语句中获取主表名
+ *
+ * @param statement SQL语句
+ * @return 主表名,解析失败返回null
+ */
+ private String getMainTableName(Statement statement) {
+ if (statement instanceof Select) {
+ PlainSelect plainSelect = (PlainSelect) ((Select) statement).getSelectBody();
+ FromItem fromItem = plainSelect.getFromItem();
+ if (fromItem instanceof Table) {
+ return ((Table) fromItem).getName();
+ }
+ } else if (statement instanceof Update) {
+ return ((Update) statement).getTable().getName();
+ } else if (statement instanceof Delete) {
+ return ((Delete) statement).getTable().getName();
+ }
+ return null;
+ }
+
+ /**
+ * 判断表名是否是租户表
+ *
+ * @param tableName 表名
+ * @return true-是租户表,false-不是
+ */
+ private boolean isTenantTable(String tableName) {
+ return TenantConfig.TENANT_TABLES.stream()
+ .anyMatch(t -> t.equalsIgnoreCase(tableName));
+ }
+
+ /**
+ * 处理SQL - 添加租户过滤条件
+ *
+ * @param sql 原始SQL
+ * @param tenantId 租户ID
+ * @param sqlCommandType SQL类型
+ * @return 改写后的SQL
+ */
+ private String processSql(String sql, Long tenantId, SqlCommandType sqlCommandType) throws Exception {
+ Statement statement = CCJSqlParserUtil.parse(sql);
+
+ if (statement instanceof Select) {
+ // 处理 SELECT 语句
+ Select selectStatement = (Select) statement;
+ PlainSelect plainSelect = (PlainSelect) selectStatement.getSelectBody();
+
+ // 获取主表别名,避免多表JOIN时tenant_id歧义
+ String tableAlias = getMainTableAlias(plainSelect);
+
+ // 构造 tenant_id = ? 条件(带表别名)
+ EqualsTo tenantCondition = buildTenantCondition(tenantId, tableAlias);
+
+ // 合并WHERE条件
+ if (plainSelect.getWhere() != null) {
+ AndExpression andExpression = new AndExpression(tenantCondition, plainSelect.getWhere());
+ plainSelect.setWhere(andExpression);
+ } else {
+ plainSelect.setWhere(tenantCondition);
+ }
+
+ return selectStatement.toString();
+ }
+ else if (statement instanceof Update) {
+ // 处理 UPDATE 语句
+ Update updateStatement = (Update) statement;
+
+ // 获取表别名
+ String tableAlias = getUpdateTableAlias(updateStatement);
+ EqualsTo tenantCondition = buildTenantCondition(tenantId, tableAlias);
+
+ if (updateStatement.getWhere() != null) {
+ AndExpression andExpression = new AndExpression(tenantCondition, updateStatement.getWhere());
+ updateStatement.setWhere(andExpression);
+ } else {
+ updateStatement.setWhere(tenantCondition);
+ }
+
+ return updateStatement.toString();
+ }
+ else if (statement instanceof Delete) {
+ // 处理 DELETE 语句
+ Delete deleteStatement = (Delete) statement;
+
+ // 获取表别名
+ String tableAlias = getDeleteTableAlias(deleteStatement);
+ EqualsTo tenantCondition = buildTenantCondition(tenantId, tableAlias);
+
+ if (deleteStatement.getWhere() != null) {
+ AndExpression andExpression = new AndExpression(tenantCondition, deleteStatement.getWhere());
+ deleteStatement.setWhere(andExpression);
+ } else {
+ deleteStatement.setWhere(tenantCondition);
+ }
+
+ return deleteStatement.toString();
+ }
+
+ return sql;
+ }
+
+ /**
+ * 获取SELECT语句主表的别名或表名
+ *
+ * @param plainSelect SELECT语句
+ * @return 表别名或表名,用于构造 tenant_id 条件
+ */
+ private String getMainTableAlias(PlainSelect plainSelect) {
+ FromItem fromItem = plainSelect.getFromItem();
+ if (fromItem instanceof Table) {
+ Table table = (Table) fromItem;
+ if (table.getAlias() != null) {
+ return table.getAlias().getName();
+ }
+ return table.getName();
+ }
+ return null;
+ }
+
+ /**
+ * 获取UPDATE语句的表别名或表名
+ *
+ * @param updateStatement UPDATE语句
+ * @return 表别名或表名
+ */
+ private String getUpdateTableAlias(Update updateStatement) {
+ Table table = updateStatement.getTable();
+ if (table != null) {
+ if (table.getAlias() != null) {
+ return table.getAlias().getName();
+ }
+ return table.getName();
+ }
+ return null;
+ }
+
+ /**
+ * 获取DELETE语句的表别名或表名
+ *
+ * @param deleteStatement DELETE语句
+ * @return 表别名或表名
+ */
+ private String getDeleteTableAlias(Delete deleteStatement) {
+ Table table = deleteStatement.getTable();
+ if (table != null) {
+ if (table.getAlias() != null) {
+ return table.getAlias().getName();
+ }
+ return table.getName();
+ }
+ return null;
+ }
+
+ /**
+ * 构造租户过滤条件:[表别名.]tenant_id = ?
+ *
+ * @param tenantId 租户ID
+ * @param tableAlias 表别名(可为null)
+ * @return EqualsTo 表达式
+ */
+ private EqualsTo buildTenantCondition(Long tenantId, String tableAlias) {
+ EqualsTo equalsTo = new EqualsTo();
+ if (tableAlias != null) {
+ equalsTo.setLeftExpression(new Column(new Table(tableAlias), "tenant_id"));
+ } else {
+ equalsTo.setLeftExpression(new Column("tenant_id"));
+ }
+ equalsTo.setRightExpression(new LongValue(tenantId));
+ return equalsTo;
+ }
+
+ @Override
+ public Object plugin(Object target) {
+ return Plugin.wrap(target, this);
+ }
+
+ @Override
+ public void setProperties(Properties properties) {
+ // 可以从配置文件读取租户表配置
+ }
+}
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 a7795f323..14ab896f3 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
@@ -1,6 +1,8 @@
package com.ruoyi.framework.web.service;
import javax.annotation.Resource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
@@ -22,6 +24,7 @@ import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.ip.IpUtils;
+import com.ruoyi.common.core.context.TenantContext;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.framework.security.context.AuthenticationContextHolder;
@@ -30,12 +33,13 @@ import com.ruoyi.system.service.ISysUserService;
/**
* 登录校验方法
- *
+ *
* @author ruoyi
*/
@Component
public class SysLoginService
{
+ private static final Logger log = LoggerFactory.getLogger(SysLoginService.class);
@Autowired
private TokenService tokenService;
@@ -62,41 +66,74 @@ public class SysLoginService
*/
public String login(String username, String password, String code, String uuid)
{
- // 验证码校验
- validateCaptcha(username, code, uuid);
- // 登录前置校验
- loginPreCheck(username, password);
- // 用户验证
- Authentication authentication = null;
+ // 【多租户】登录阶段:临时忽略租户过滤
+ // Reason: 登录时需要查询用户表获取tenant_id,形成先有鸡还是先有蛋的问题
+ // 因此登录阶段临时设置全局模式,允许查询和更新用户表
try
{
- UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
- AuthenticationContextHolder.setContext(authenticationToken);
- // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
- authentication = authenticationManager.authenticate(authenticationToken);
- }
- catch (Exception e)
- {
- if (e instanceof BadCredentialsException)
+ TenantContext.setIgnore(true);
+ log.debug("【多租户】登录阶段:临时设置全局模式");
+
+ // 验证码校验
+ validateCaptcha(username, code, uuid);
+ // 登录前置校验
+ loginPreCheck(username, password);
+ // 用户验证
+ Authentication authentication = null;
+ try
{
- AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
- throw new UserPasswordNotMatchException();
+ UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
+ AuthenticationContextHolder.setContext(authenticationToken);
+ // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
+ authentication = authenticationManager.authenticate(authenticationToken);
+ }
+ catch (Exception e)
+ {
+ if (e instanceof BadCredentialsException)
+ {
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
+ throw new UserPasswordNotMatchException();
+ }
+ else
+ {
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
+ throw new ServiceException(e.getMessage());
+ }
+ }
+ finally
+ {
+ AuthenticationContextHolder.clearContext();
+ }
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
+ LoginUser loginUser = (LoginUser) authentication.getPrincipal();
+ recordLoginInfo(loginUser.getUserId());
+
+ // 【多租户】从SysUser获取tenant_id并设置到LoginUser
+ if (loginUser.getUser() != null && loginUser.getUser().getTenantId() != null)
+ {
+ loginUser.setTenantId(loginUser.getUser().getTenantId());
+ log.info("【多租户】用户 {} 登录成功,租户ID: {}", username, loginUser.getTenantId());
+ }
+ else if (!com.ruoyi.common.config.TenantConfig.isSuperAdmin(loginUser.getUserId()))
+ {
+ // 非超级管理员必须有租户ID
+ log.error("用户 {} 未分配租户且非超级管理员,拒绝登录", username);
+ throw new ServiceException("用户未分配租户,请联系管理员");
}
else
{
- AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
- throw new ServiceException(e.getMessage());
+ log.warn("【多租户】超级管理员 {} 登录,无需租户ID", username);
}
+
+ // 生成token
+ return tokenService.createToken(loginUser);
}
finally
{
- AuthenticationContextHolder.clearContext();
+ // 【多租户】登录完成,清除全局模式标记
+ TenantContext.clear();
+ log.debug("【多租户】登录流程结束,清除全局模式");
}
- AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
- LoginUser loginUser = (LoginUser) authentication.getPrincipal();
- recordLoginInfo(loginUser.getUserId());
- // 生成token
- return tokenService.createToken(loginUser);
}
/**
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java
index e5179a3fa..5a9a8bbde 100644
--- a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java
@@ -56,7 +56,7 @@ public class TokenService
/**
* 获取用户身份信息
- *
+ *
* @return 用户信息
*/
public LoginUser getLoginUser(HttpServletRequest request)
@@ -72,6 +72,15 @@ public class TokenService
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
String userKey = getTokenKey(uuid);
LoginUser user = redisCache.getCacheObject(userKey);
+
+ // 【多租户】从JWT中提取tenant_id并设置到LoginUser
+ if (user != null && claims.get("tenant_id") != null)
+ {
+ Long tenantId = claims.get("tenant_id", Long.class);
+ user.setTenantId(tenantId);
+ log.debug("从JWT中提取租户ID: {}", tenantId);
+ }
+
return user;
}
catch (Exception e)
@@ -107,7 +116,7 @@ public class TokenService
/**
* 创建令牌
- *
+ *
* @param loginUser 用户信息
* @return 令牌
*/
@@ -121,6 +130,14 @@ public class TokenService
Map claims = new HashMap<>();
claims.put(Constants.LOGIN_USER_KEY, token);
claims.put(Constants.JWT_USERNAME, loginUser.getUsername());
+
+ // 【多租户】将tenant_id存入JWT
+ if (loginUser.getTenantId() != null)
+ {
+ claims.put("tenant_id", loginUser.getTenantId());
+ log.debug("JWT中存储租户ID: {}", loginUser.getTenantId());
+ }
+
return createToken(claims);
}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTenant.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTenant.java
new file mode 100644
index 000000000..a6c8b1cac
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTenant.java
@@ -0,0 +1,163 @@
+package com.ruoyi.system.domain;
+
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 租户信息对象 sys_tenant
+ *
+ * @author W-yf
+ * @date 2025-12-19
+ */
+public class SysTenant extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 租户ID */
+ private Long tenantId;
+
+ /** 租户名称 */
+ @Excel(name = "租户名称")
+ private String tenantName;
+
+ /** 租户编码(唯一标识) */
+ @Excel(name = "租户编码", readConverterExp = "唯=一标识")
+ private String tenantCode;
+
+ /** 联系人 */
+ @Excel(name = "联系人")
+ private String contactName;
+
+ /** 联系电话 */
+ @Excel(name = "联系电话")
+ private String contactPhone;
+
+ /** 过期时间(NULL表示永久) */
+ @Excel(name = "过期时间", readConverterExp = "N=ULL表示永久")
+ private Date expireTime;
+
+ /** 套餐ID(预留) */
+ @Excel(name = "套餐ID", readConverterExp = "预=留")
+ private Long packageId;
+
+ /** 状态(0正常 1停用) */
+ @Excel(name = "状态", readConverterExp = "0=正常,1=停用")
+ private String status;
+
+ /** 删除标志(0存在 2删除) */
+ private String delFlag;
+
+ public void setTenantId(Long tenantId)
+ {
+ this.tenantId = tenantId;
+ }
+
+ public Long getTenantId()
+ {
+ return tenantId;
+ }
+
+ public void setTenantName(String tenantName)
+ {
+ this.tenantName = tenantName;
+ }
+
+ public String getTenantName()
+ {
+ return tenantName;
+ }
+
+ public void setTenantCode(String tenantCode)
+ {
+ this.tenantCode = tenantCode;
+ }
+
+ public String getTenantCode()
+ {
+ return tenantCode;
+ }
+
+ public void setContactName(String contactName)
+ {
+ this.contactName = contactName;
+ }
+
+ public String getContactName()
+ {
+ return contactName;
+ }
+
+ public void setContactPhone(String contactPhone)
+ {
+ this.contactPhone = contactPhone;
+ }
+
+ public String getContactPhone()
+ {
+ return contactPhone;
+ }
+
+ public void setExpireTime(Date expireTime)
+ {
+ this.expireTime = expireTime;
+ }
+
+ public Date getExpireTime()
+ {
+ return expireTime;
+ }
+
+ public void setPackageId(Long packageId)
+ {
+ this.packageId = packageId;
+ }
+
+ public Long getPackageId()
+ {
+ return packageId;
+ }
+
+ public void setStatus(String status)
+ {
+ this.status = status;
+ }
+
+ public String getStatus()
+ {
+ return status;
+ }
+
+ public void setDelFlag(String delFlag)
+ {
+ this.delFlag = delFlag;
+ }
+
+ public String getDelFlag()
+ {
+ return delFlag;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+ .append("tenantId", getTenantId())
+ .append("tenantName", getTenantName())
+ .append("tenantCode", getTenantCode())
+ .append("contactName", getContactName())
+ .append("contactPhone", getContactPhone())
+ .append("expireTime", getExpireTime())
+ .append("packageId", getPackageId())
+ .append("status", getStatus())
+ .append("delFlag", getDelFlag())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .append("remark", getRemark())
+ .toString();
+ }
+}
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
new file mode 100644
index 000000000..d73d7d2de
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTenantMapper.java
@@ -0,0 +1,63 @@
+package com.ruoyi.system.mapper;
+
+import com.ruoyi.system.domain.SysTenant;
+
+import java.util.List;
+
+
+/**
+ * 租户信息Mapper接口
+ *
+ * @author W-yf
+ * @date 2025-12-19
+ */
+public interface SysTenantMapper
+{
+ /**
+ * 查询租户信息
+ *
+ * @param tenantId 租户信息主键
+ * @return 租户信息
+ */
+ public SysTenant selectSysTenantByTenantId(Long tenantId);
+
+ /**
+ * 查询租户信息列表
+ *
+ * @param sysTenant 租户信息
+ * @return 租户信息集合
+ */
+ public List selectSysTenantList(SysTenant sysTenant);
+
+ /**
+ * 新增租户信息
+ *
+ * @param sysTenant 租户信息
+ * @return 结果
+ */
+ public int insertSysTenant(SysTenant sysTenant);
+
+ /**
+ * 修改租户信息
+ *
+ * @param sysTenant 租户信息
+ * @return 结果
+ */
+ public int updateSysTenant(SysTenant sysTenant);
+
+ /**
+ * 删除租户信息
+ *
+ * @param tenantId 租户信息主键
+ * @return 结果
+ */
+ public int deleteSysTenantByTenantId(Long tenantId);
+
+ /**
+ * 批量删除租户信息
+ *
+ * @param tenantIds 需要删除的数据主键集合
+ * @return 结果
+ */
+ public int deleteSysTenantByTenantIds(Long[] tenantIds);
+}
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
new file mode 100644
index 000000000..6c2ef01dc
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTenantService.java
@@ -0,0 +1,62 @@
+package com.ruoyi.system.service;
+
+import com.ruoyi.system.domain.SysTenant;
+
+import java.util.List;
+
+/**
+ * 租户信息Service接口
+ *
+ * @author W-yf
+ * @date 2025-12-19
+ */
+public interface ISysTenantService
+{
+ /**
+ * 查询租户信息
+ *
+ * @param tenantId 租户信息主键
+ * @return 租户信息
+ */
+ public SysTenant selectSysTenantByTenantId(Long tenantId);
+
+ /**
+ * 查询租户信息列表
+ *
+ * @param sysTenant 租户信息
+ * @return 租户信息集合
+ */
+ public List selectSysTenantList(SysTenant sysTenant);
+
+ /**
+ * 新增租户信息
+ *
+ * @param sysTenant 租户信息
+ * @return 结果
+ */
+ public int insertSysTenant(SysTenant sysTenant);
+
+ /**
+ * 修改租户信息
+ *
+ * @param sysTenant 租户信息
+ * @return 结果
+ */
+ public int updateSysTenant(SysTenant sysTenant);
+
+ /**
+ * 批量删除租户信息
+ *
+ * @param tenantIds 需要删除的租户信息主键集合
+ * @return 结果
+ */
+ public int deleteSysTenantByTenantIds(Long[] tenantIds);
+
+ /**
+ * 删除租户信息信息
+ *
+ * @param tenantId 租户信息主键
+ * @return 结果
+ */
+ public int deleteSysTenantByTenantId(Long tenantId);
+}
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
new file mode 100644
index 000000000..81741c31d
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTenantServiceImpl.java
@@ -0,0 +1,96 @@
+package com.ruoyi.system.service.impl;
+
+import java.util.List;
+import com.ruoyi.common.utils.DateUtils;
+import com.ruoyi.system.domain.SysTenant;
+import com.ruoyi.system.mapper.SysTenantMapper;
+import com.ruoyi.system.service.ISysTenantService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * 租户信息Service业务层处理
+ *
+ * @author W-yf
+ * @date 2025-12-19
+ */
+@Service
+public class SysTenantServiceImpl implements ISysTenantService
+{
+ @Autowired
+ private SysTenantMapper sysTenantMapper;
+
+ /**
+ * 查询租户信息
+ *
+ * @param tenantId 租户信息主键
+ * @return 租户信息
+ */
+ @Override
+ public SysTenant selectSysTenantByTenantId(Long tenantId)
+ {
+ return sysTenantMapper.selectSysTenantByTenantId(tenantId);
+ }
+
+ /**
+ * 查询租户信息列表
+ *
+ * @param sysTenant 租户信息
+ * @return 租户信息
+ */
+ @Override
+ public List selectSysTenantList(SysTenant sysTenant)
+ {
+ return sysTenantMapper.selectSysTenantList(sysTenant);
+ }
+
+ /**
+ * 新增租户信息
+ *
+ * @param sysTenant 租户信息
+ * @return 结果
+ */
+ @Override
+ public int insertSysTenant(SysTenant sysTenant)
+ {
+ sysTenant.setCreateTime(DateUtils.getNowDate());
+ return sysTenantMapper.insertSysTenant(sysTenant);
+ }
+
+ /**
+ * 修改租户信息
+ *
+ * @param sysTenant 租户信息
+ * @return 结果
+ */
+ @Override
+ public int updateSysTenant(SysTenant sysTenant)
+ {
+ sysTenant.setUpdateTime(DateUtils.getNowDate());
+ return sysTenantMapper.updateSysTenant(sysTenant);
+ }
+
+ /**
+ * 批量删除租户信息
+ *
+ * @param tenantIds 需要删除的租户信息主键
+ * @return 结果
+ */
+ @Override
+ public int deleteSysTenantByTenantIds(Long[] tenantIds)
+ {
+ return sysTenantMapper.deleteSysTenantByTenantIds(tenantIds);
+ }
+
+ /**
+ * 删除租户信息信息
+ *
+ * @param tenantId 租户信息主键
+ * @return 结果
+ */
+ @Override
+ public int deleteSysTenantByTenantId(Long tenantId)
+ {
+ return sysTenantMapper.deleteSysTenantByTenantId(tenantId);
+ }
+}
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysTenantMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysTenantMapper.xml
new file mode 100644
index 000000000..179123646
--- /dev/null
+++ b/ruoyi-system/src/main/resources/mapper/system/SysTenantMapper.xml
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ select tenant_id, tenant_name, tenant_code, contact_name, contact_phone, expire_time, package_id, status, del_flag, create_by, create_time, update_by, update_time, remark from sys_tenant
+
+
+
+
+
+
+
+ insert into sys_tenant
+
+ tenant_name,
+ tenant_code,
+ contact_name,
+ contact_phone,
+ expire_time,
+ package_id,
+ status,
+ del_flag,
+ create_by,
+ create_time,
+ update_by,
+ update_time,
+ remark,
+
+
+ #{tenantName},
+ #{tenantCode},
+ #{contactName},
+ #{contactPhone},
+ #{expireTime},
+ #{packageId},
+ #{status},
+ #{delFlag},
+ #{createBy},
+ #{createTime},
+ #{updateBy},
+ #{updateTime},
+ #{remark},
+
+
+
+
+ update sys_tenant
+
+ tenant_name = #{tenantName},
+ tenant_code = #{tenantCode},
+ contact_name = #{contactName},
+ contact_phone = #{contactPhone},
+ expire_time = #{expireTime},
+ package_id = #{packageId},
+ status = #{status},
+ del_flag = #{delFlag},
+ create_by = #{createBy},
+ create_time = #{createTime},
+ update_by = #{updateBy},
+ update_time = #{updateTime},
+ remark = #{remark},
+
+ where tenant_id = #{tenantId}
+
+
+
+ delete from sys_tenant where tenant_id = #{tenantId}
+
+
+
+ delete from sys_tenant where tenant_id in
+
+ #{tenantId}
+
+
+
\ No newline at end of file
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
index 49de1cab1..6aeffca7b 100644
--- a/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
@@ -7,6 +7,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+
@@ -48,7 +49,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
- select u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.pwd_update_date, u.create_by, u.create_time, u.remark,
+ select u.user_id, u.dept_id, u.tenant_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.pwd_update_date, u.create_by, u.create_time, u.remark,
d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.status as dept_status,
r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status
from sys_user u
@@ -58,7 +59,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"