From 9a8ffdbb8f12030feafd7490571a98d3850ca745 Mon Sep 17 00:00:00 2001 From: 9264yf <691506722@qq.com> Date: Fri, 19 Dec 2025 21:21:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(tenant):=20=E5=AE=9E=E7=8E=B0=E5=A4=9A?= =?UTF-8?q?=E7=A7=9F=E6=88=B7=E6=95=B0=E6=8D=AE=E9=9A=94=E7=A6=BB=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增租户管理模块(SysTenant CRUD) - 实现 TenantContext 租户上下文(基于 TransmittableThreadLocal) - 新增 TenantInterceptor HTTP拦截器,自动识别当前租户 - 新增 TenantSqlInterceptor MyBatis 拦截器,自动注入 tenant_id 过滤条件 - 支持超级管理员全局模式,可查看所有租户数据 - 扩展 LoginUser/SysUser 支持租户字段 - 新增前端租户管理页面 --- pom.xml | 17 + .../system/SysTenantController.java | 106 +++ .../controller/system/SysUserController.java | 12 + ruoyi-common/pom.xml | 6 + .../com/ruoyi/common/config/TenantConfig.java | 122 +++ .../common/core/context/TenantContext.java | 98 +++ .../ruoyi/common/core/domain/BaseEntity.java | 14 + .../common/core/domain/entity/SysUser.java | 13 + .../common/core/domain/model/LoginUser.java | 15 + ruoyi-framework/pom.xml | 6 + .../ruoyi/framework/config/MyBatisConfig.java | 10 +- .../framework/config/ResourcesConfig.java | 10 +- .../interceptor/TenantInterceptor.java | 100 +++ .../interceptor/TenantSqlInterceptor.java | 319 +++++++ .../web/service/SysLoginService.java | 87 +- .../framework/web/service/TokenService.java | 21 +- .../com/ruoyi/system/domain/SysTenant.java | 163 ++++ .../ruoyi/system/mapper/SysTenantMapper.java | 63 ++ .../system/service/ISysTenantService.java | 62 ++ .../service/impl/SysTenantServiceImpl.java | 96 +++ .../mapper/system/SysTenantMapper.xml | 110 +++ .../resources/mapper/system/SysUserMapper.xml | 8 +- ruoyi-ui/src/api/system/tenant.js | 44 + ruoyi-ui/src/api/system/user.js | 3 + ruoyi-ui/src/views/system/tenant/index.vue | 346 ++++++++ ruoyi-ui/src/views/system/user/index.vue | 26 +- sql/hairlink_phase1.sql | 312 +++++++ sql/ry_20250522_with_tenant.sql | 792 ++++++++++++++++++ 28 files changed, 2949 insertions(+), 32 deletions(-) create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysTenantController.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/config/TenantConfig.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/context/TenantContext.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/TenantInterceptor.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/TenantSqlInterceptor.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTenant.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTenantMapper.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTenantService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTenantServiceImpl.java create mode 100644 ruoyi-system/src/main/resources/mapper/system/SysTenantMapper.xml create mode 100644 ruoyi-ui/src/api/system/tenant.js create mode 100644 ruoyi-ui/src/views/system/tenant/index.vue create mode 100644 sql/hairlink_phase1.sql create mode 100644 sql/ry_20250522_with_tenant.sql 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 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"