Pre Merge pull request !1126 from W-yf/cherry-pick-tenant

pull/1126/MERGE
W-yf 2025-12-20 08:37:05 +00:00 committed by Gitee
commit 8279759dda
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
85 changed files with 6718 additions and 1305 deletions

View File

@ -0,0 +1,3 @@
# Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references
.specstory/**

355
.env.example 100644
View File

@ -0,0 +1,355 @@
################################################################################
# Hair-Link 美发预约管理系统 - 环境变量配置模板
#
# 使用说明:
# 1. 复制此文件为 .env开发环境或 .env.production生产环境
# 2. 根据实际环境修改配置值
# 3. 确保 .env 文件已添加到 .gitignore不要提交到版本控制
# 4. 所有配置项都有默认值,可选择性覆盖
################################################################################
#==============================================================================
# 数据库配置 (Database Configuration)
#==============================================================================
# 主数据库主机地址
# 默认: localhost
HAIRLINK_DB_HOST=localhost
# 主数据库端口
# 默认: 3306
HAIRLINK_DB_PORT=3306
# 主数据库名称
# 默认: ry-vue
HAIRLINK_DB_NAME=ry-vue
# 主数据库连接参数
# 默认: useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
HAIRLINK_DB_PARAMS=useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
# 主数据库用户名
# 默认: root
HAIRLINK_DB_USERNAME=root
# 主数据库密码
# 默认: password
# ⚠️ 生产环境请务必修改为强密码
HAIRLINK_DB_PASSWORD=password
# 从数据库开关(读写分离)
# 可选值: true/false
# 默认: false
HAIRLINK_DB_SLAVE_ENABLED=false
# 从数据库主机地址(仅在启用从库时需要配置)
# 默认: (空)
# HAIRLINK_DB_SLAVE_HOST=slave-host
# 从数据库端口(仅在启用从库时需要配置)
# 默认: (空)
# HAIRLINK_DB_SLAVE_PORT=3306
# 从数据库名称(仅在启用从库时需要配置)
# 默认: (空)
# HAIRLINK_DB_SLAVE_NAME=ry-vue
# 从数据库连接参数(仅在启用从库时需要配置)
# 默认: (空)
# HAIRLINK_DB_SLAVE_PARAMS=useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
# 从数据库用户名(仅在启用从库时需要配置)
# 默认: (空)
# HAIRLINK_DB_SLAVE_USERNAME=root
# 从数据库密码(仅在启用从库时需要配置)
# 默认: (空)
# HAIRLINK_DB_SLAVE_PASSWORD=password
#------------------------------------------------------------------------------
# 数据库连接池配置 (Database Connection Pool)
#------------------------------------------------------------------------------
# 初始连接数
# 默认: 5
HAIRLINK_DB_INITIAL_SIZE=5
# 最小空闲连接数
# 默认: 10
HAIRLINK_DB_MIN_IDLE=10
# 最大活动连接数
# 默认: 20
HAIRLINK_DB_MAX_ACTIVE=20
# 获取连接等待超时时间(毫秒)
# 默认: 60000 (60秒)
HAIRLINK_DB_MAX_WAIT=60000
# 连接超时时间(毫秒)
# 默认: 30000 (30秒)
HAIRLINK_DB_CONNECT_TIMEOUT=30000
# 网络超时时间(毫秒)
# 默认: 60000 (60秒)
HAIRLINK_DB_SOCKET_TIMEOUT=60000
# 空闲连接检测间隔(毫秒)
# 默认: 60000 (60秒)
HAIRLINK_DB_TIME_BETWEEN_EVICTION=60000
# 连接在池中最小空闲时间(毫秒)
# 默认: 300000 (5分钟)
HAIRLINK_DB_MIN_EVICTABLE_IDLE_TIME=300000
# 连接在池中最大空闲时间(毫秒)
# 默认: 900000 (15分钟)
HAIRLINK_DB_MAX_EVICTABLE_IDLE_TIME=900000
#==============================================================================
# Redis 配置 (Redis Configuration)
#==============================================================================
# Redis 服务器地址
# 默认: localhost
HAIRLINK_REDIS_HOST=localhost
# Redis 服务器端口
# 默认: 6379
HAIRLINK_REDIS_PORT=6379
# Redis 数据库索引0-15
# 默认: 0
HAIRLINK_REDIS_DATABASE=0
# Redis 密码如果Redis未设置密码留空即可
# 默认: (空)
HAIRLINK_REDIS_PASSWORD=
# Redis 连接超时时间
# 默认: 10s
HAIRLINK_REDIS_TIMEOUT=10s
#------------------------------------------------------------------------------
# Redis 连接池配置 (Redis Connection Pool)
#------------------------------------------------------------------------------
# 连接池中的最小空闲连接
# 默认: 0
HAIRLINK_REDIS_MIN_IDLE=0
# 连接池中的最大空闲连接
# 默认: 8
HAIRLINK_REDIS_MAX_IDLE=8
# 连接池的最大数据库连接数
# 默认: 8
HAIRLINK_REDIS_MAX_ACTIVE=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
# 默认: -1ms
HAIRLINK_REDIS_MAX_WAIT=-1ms
#==============================================================================
# 应用服务配置 (Application Server Configuration)
#==============================================================================
# 应用服务器端口
# 默认: 8080
HAIRLINK_SERVER_PORT=8080
# 应用上下文路径
# 默认: /
# 示例: /api 则访问地址为 http://localhost:8080/api
HAIRLINK_CONTEXT_PATH=/
# Tomcat URI 编码
# 默认: UTF-8
HAIRLINK_SERVER_URI_ENCODING=UTF-8
# 连接数满后的排队数
# 默认: 1000
HAIRLINK_SERVER_ACCEPT_COUNT=1000
# Tomcat 最大线程数
# 默认: 800
HAIRLINK_SERVER_MAX_THREADS=800
# Tomcat 启动初始化的线程数
# 默认: 100
HAIRLINK_SERVER_MIN_SPARE_THREADS=100
#------------------------------------------------------------------------------
# 文件上传配置 (File Upload Configuration)
#------------------------------------------------------------------------------
# 文件上传存储路径
# 默认: ./upload (相对路径,自动适配操作系统)
# Windows示例: D:/hairlink/upload
# Linux示例: /home/hairlink/upload
HAIRLINK_UPLOAD_PATH=./upload
# 单个文件大小限制
# 默认: 10MB
HAIRLINK_MAX_FILE_SIZE=10MB
# 总上传文件大小限制
# 默认: 20MB
HAIRLINK_MAX_REQUEST_SIZE=20MB
#==============================================================================
# Token 安全配置 (Token Security Configuration)
#==============================================================================
# JWT Token 请求头名称
# 默认: Authorization
HAIRLINK_TOKEN_HEADER=Authorization
# JWT Token 密钥
# 默认: abcdefghijklmnopqrstuvwxyz
# ⚠️ 生产环境请务必修改为复杂的随机字符串建议32位以上
# 生成方法: openssl rand -base64 32
HAIRLINK_TOKEN_SECRET=abcdefghijklmnopqrstuvwxyz
# Token 过期时间(单位:分钟)
# 默认: 30
HAIRLINK_TOKEN_EXPIRE_TIME=30
#==============================================================================
# 用户密码策略配置 (Password Policy Configuration)
#==============================================================================
# 密码最大错误尝试次数
# 超过此次数将锁定账户
# 默认: 5
HAIRLINK_PASSWORD_MAX_RETRY=5
# 密码错误锁定时间(单位:分钟)
# 默认: 10
HAIRLINK_PASSWORD_LOCK_TIME=10
#==============================================================================
# 应用功能配置 (Application Features Configuration)
#==============================================================================
# 验证码类型
# 可选值: math(数学计算) / char(字符验证)
# 默认: math
HAIRLINK_CAPTCHA_TYPE=math
# IP地址获取开关
# 是否启用IP地址解析功能
# 可选值: true/false
# 默认: false
HAIRLINK_ADDRESS_ENABLED=false
# 热部署开关
# 开发环境建议开启,生产环境建议关闭
# 可选值: true/false
# 默认: true
HAIRLINK_DEVTOOLS_ENABLED=true
#==============================================================================
# XSS 防护配置 (XSS Protection Configuration)
#==============================================================================
# XSS 过滤开关
# 可选值: true/false
# 默认: true
HAIRLINK_XSS_ENABLED=true
# XSS 排除链接(多个用逗号分隔)
# 这些链接不进行XSS过滤
# 默认: /system/notice
HAIRLINK_XSS_EXCLUDES=/system/notice
# XSS 匹配链接(多个用逗号分隔)
# 只对这些链接进行XSS过滤
# 默认: /system/*,/monitor/*,/tool/*
HAIRLINK_XSS_URL_PATTERNS=/system/*,/monitor/*,/tool/*
#==============================================================================
# 防盗链配置 (Referer Protection Configuration)
#==============================================================================
# 防盗链开关
# 可选值: true/false
# 默认: false
HAIRLINK_REFERER_ENABLED=false
# 允许的域名列表(多个用逗号分隔)
# 默认: localhost,127.0.0.1
HAIRLINK_REFERER_ALLOWED_DOMAINS=localhost,127.0.0.1
#==============================================================================
# Druid 数据库监控配置 (Druid Monitor Configuration)
#==============================================================================
# Druid 监控后台管理员用户名
# 访问地址: http://localhost:8080/druid
# 默认: admin
# ⚠️ 生产环境请务必修改
HAIRLINK_DRUID_USERNAME=admin
# Druid 监控后台管理员密码
# 默认: 123456
# ⚠️ 生产环境请务必修改为强密码
HAIRLINK_DRUID_PASSWORD=123456
# 慢 SQL 记录阈值(单位:毫秒)
# 超过此时间的SQL将被记录为慢SQL
# 默认: 1000 (1秒)
HAIRLINK_DRUID_SLOW_SQL_MILLIS=1000
#==============================================================================
# 日志配置 (Logging Configuration)
#==============================================================================
# 日志文件存储路径
# 默认: ./logs (相对路径,自动适配操作系统)
# Windows示例: D:/hairlink/logs
# Linux示例: /home/hairlink/logs
HAIRLINK_LOG_PATH=./logs
# 日志文件保留天数
# 默认: 60
HAIRLINK_LOG_MAX_HISTORY=60
# com.ruoyi 包的日志级别
# 可选值: trace/debug/info/warn/error
# 默认: debug
HAIRLINK_LOG_LEVEL_RUOYI=debug
# Spring 框架的日志级别
# 可选值: trace/debug/info/warn/error
# 默认: warn
HAIRLINK_LOG_LEVEL_SPRING=warn
#==============================================================================
# Swagger API 文档配置 (Swagger Configuration)
#==============================================================================
# Swagger 开关
# 开发环境建议开启,生产环境建议关闭
# 可选值: true/false
# 默认: true
HAIRLINK_SWAGGER_ENABLED=true
# Swagger 请求前缀
# 默认: /dev-api
HAIRLINK_SWAGGER_PATH_MAPPING=/dev-api
################################################################################
# 配置优先级说明:
# 1. 环境变量 > YAML配置文件
# 2. 如果环境变量未设置将使用YAML中的默认值
# 3. 建议生产环境通过环境变量覆盖敏感配置
#
# 生产环境必须修改的配置项:
# - HAIRLINK_DB_PASSWORD: 数据库密码
# - HAIRLINK_TOKEN_SECRET: Token密钥建议32位以上随机字符串
# - HAIRLINK_DRUID_PASSWORD: Druid监控密码
# - HAIRLINK_REDIS_PASSWORD: Redis密码如果Redis设置了密码
# - HAIRLINK_DEVTOOLS_ENABLED: 建议设置为false
# - HAIRLINK_SWAGGER_ENABLED: 建议设置为false
################################################################################

44
.gitignore vendored
View File

@ -45,3 +45,47 @@ nbdist/
!*/build/*.java
!*/build/*.html
!*/build/*.xml
# 灵妙AI开发助手自动添加
.specstory/
log/
logs/
**/AGENTS.md
**/WARP.md
**/GEMINI.md
.bmad/
.claude/commands/bmad/
.cursor/rules/bmad/
.cursor/rules/root-prompt.mdc
# MCP反馈端口文件灵妙AI开发助手自动添加
.mcp-feedback-port
# 点思工坊工作流目录灵妙AI开发助手自动添加
.diansi/workflows
# 数据库数据目录灵妙AI开发助手自动添加
mysql_data/
redis_data/
pgvector_data/
# 进程ID文件目录灵妙AI开发助手自动添加
.pids/
# 本地设置文件灵妙AI开发助手自动添加
.claude/settings.local.json
######################################################################
# Environment Variables & Application Data
# 环境变量配置文件(包含敏感信息,不提交到版本控制)
.env
.env.local
.env.development
.env.test
.env.production
.env.*.local
# 上传文件目录
upload/
uploadPath/

22
docs/dev/README.md 100644
View File

@ -0,0 +1,22 @@
# 开发文档索引
本目录包含项目的开发相关文档。
## 文档列表
### 架构设计文档
- [TENANT_GUIDE.md](./TENANT_GUIDE.md) - 若依多租户架构实现指南
- 多租户SaaS架构概述与方案对比
- 核心组件实现原理TenantContext、拦截器、SQL改写
- 快速集成指南(依赖配置、代码集成、数据库改造)
- 使用指南与开发注意事项
- 最佳实践与性能优化
- 常见问题FAQ
## 文档规范
所有开发文档遵循以下规范:
- 文档命名格式:`{分类}_{英文大写}.md`,单词用 `_` 隔开
- 文档使用简体中文编写
- 言简意赅,避免长篇大论
- 及时更新本索引文件

File diff suppressed because it is too large Load Diff

23
pom.xml
View File

@ -8,9 +8,9 @@
<artifactId>ruoyi</artifactId>
<version>3.9.1</version>
<name>ruoyi</name>
<url>http://www.ruoyi.vip</url>
<description>若依管理系统</description>
<name>hair-link</name>
<url>http://www.example.com</url>
<description>Hair-Link 美发预约管理系统</description>
<properties>
<ruoyi.version>3.9.1</ruoyi.version>
@ -30,6 +30,9 @@
<poi.version>4.1.2</poi.version>
<velocity.version>2.3</velocity.version>
<jwt.version>0.9.1</jwt.version>
<!-- 多租户相关依赖版本 -->
<transmittable-thread-local.version>2.14.3</transmittable-thread-local.version>
<jsqlparser.version>4.6</jsqlparser.version>
<!-- override dependency version -->
<tomcat.version>9.0.112</tomcat.version>
<logback.version>1.2.13</logback.version>
@ -218,6 +221,20 @@
<version>${ruoyi.version}</version>
</dependency>
<!-- 多租户支持 - TransmittableThreadLocal -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>${transmittable-thread-local.version}</version>
</dependency>
<!-- SQL解析器 - JSqlParser -->
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>${jsqlparser.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@ -3,7 +3,7 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>ruoyi</artifactId>
<artifactId>hair-link</artifactId>
<groupId>com.ruoyi</groupId>
<version>3.9.1</version>
</parent>

View File

@ -16,15 +16,14 @@ public class RuoYiApplication
{
// System.setProperty("spring.devtools.restart.enabled", "false");
SpringApplication.run(RuoYiApplication.class, args);
System.out.println("(♥◠‿◠)ノ゙ 若依启动成功 ლ(´ڡ`ლ)゙ \n" +
" .-------. ____ __ \n" +
" | _ _ \\ \\ \\ / / \n" +
" | ( ' ) | \\ _. / ' \n" +
" |(_ o _) / _( )_ .' \n" +
" | (_,_).' __ ___(_ o _)' \n" +
" | |\\ \\ | || |(_,_)' \n" +
" | | \\ `' /| `-' / \n" +
" | | \\ / \\ / \n" +
" ''-' `'-' `-..-' ");
System.out.println("\n" +
" ███████╗██╗ ██╗██╗ ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗ ██████╗ \n" +
" ██╔════╝██║ ██║██║██╔════╝ ██║ ██║██╔══██╗████╗ ██║██╔════╝ \n" +
" ███████╗███████║██║██║ ███╗██║ ██║███████║██╔██╗ ██║██║ ███╗\n" +
" ╚════██║██╔══██║██║██║ ██║██║ ██║██╔══██║██║╚██╗██║██║ ██║\n" +
" ███████║██║ ██║██║╚██████╔╝╚██████╔╝██║ ██║██║ ╚████║╚██████╔╝\n" +
" ╚══════╝╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ \n" +
"\n" +
" 【 时 光 】 启动成功!\n");
}
}

View File

@ -49,7 +49,7 @@ public class SysLoginController
/**
*
*
*
* @param loginBody
* @return
*/
@ -59,7 +59,7 @@ public class SysLoginController
AjaxResult ajax = AjaxResult.success();
// 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
loginBody.getUuid());
loginBody.getUuid(), loginBody.getTenantCode());
ajax.put(Constants.TOKEN, token);
return ajax;
}

View File

@ -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<SysTenant> 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<SysTenant> list = sysTenantService.selectSysTenantList(sysTenant);
ExcelUtil<SysTenant> util = new ExcelUtil<SysTenant>(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));
}
}

View File

@ -0,0 +1,101 @@
package com.ruoyi.web.controller.system;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
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.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.system.domain.SysTenantPackage;
import com.ruoyi.system.service.ISysTenantPackageService;
/**
* Controller
*
* @author ruoyi
* @date 2025-12-19
*/
@RestController
@RequestMapping("/system/package")
public class SysTenantPackageController extends BaseController
{
@Autowired
private ISysTenantPackageService packageService;
/**
*
*/
@PreAuthorize("@ss.hasPermi('system:package:list')")
@GetMapping("/list")
public TableDataInfo list(SysTenantPackage sysTenantPackage)
{
startPage();
List<SysTenantPackage> list = packageService.selectPackageList(sysTenantPackage);
return getDataTable(list);
}
/**
*
*/
@PreAuthorize("@ss.hasPermi('system:package:query')")
@GetMapping(value = "/{packageId}")
public AjaxResult getInfo(@PathVariable("packageId") Long packageId)
{
return success(packageService.selectPackageById(packageId));
}
/**
*
*/
@PreAuthorize("@ss.hasPermi('system:package:add')")
@Log(title = "租户套餐", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysTenantPackage sysTenantPackage)
{
return toAjax(packageService.insertPackage(sysTenantPackage));
}
/**
*
*/
@PreAuthorize("@ss.hasPermi('system:package:edit')")
@Log(title = "租户套餐", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysTenantPackage sysTenantPackage)
{
return toAjax(packageService.updatePackage(sysTenantPackage));
}
/**
*
*/
@PreAuthorize("@ss.hasPermi('system:package:remove')")
@Log(title = "租户套餐", businessType = BusinessType.DELETE)
@DeleteMapping("/{packageIds}")
public AjaxResult remove(@PathVariable Long[] packageIds)
{
return toAjax(packageService.deletePackageByIds(packageIds));
}
/**
* ID
*/
@PreAuthorize("@ss.hasPermi('system:package:query')")
@GetMapping(value = "/{packageId}/menus")
public AjaxResult getPackageMenus(@PathVariable("packageId") Long packageId)
{
List<Long> menuIds = packageService.selectMenuIdsByPackageId(packageId);
return success(menuIds);
}
}

View File

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

View File

@ -6,34 +6,34 @@ spring:
druid:
# 主库数据源
master:
url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: password
url: jdbc:mysql://${HAIRLINK_DB_HOST:localhost}:${HAIRLINK_DB_PORT:3306}/${HAIRLINK_DB_NAME:ry-vue}?${HAIRLINK_DB_PARAMS:useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8}
username: ${HAIRLINK_DB_USERNAME:root}
password: ${HAIRLINK_DB_PASSWORD:password}
# 从库数据源
slave:
# 从数据源开关/默认关闭
enabled: false
url:
username:
password:
enabled: ${HAIRLINK_DB_SLAVE_ENABLED:false}
url: jdbc:mysql://${HAIRLINK_DB_SLAVE_HOST:}:${HAIRLINK_DB_SLAVE_PORT:}/${HAIRLINK_DB_SLAVE_NAME:}?${HAIRLINK_DB_SLAVE_PARAMS:}
username: ${HAIRLINK_DB_SLAVE_USERNAME:}
password: ${HAIRLINK_DB_SLAVE_PASSWORD:}
# 初始连接数
initialSize: 5
initialSize: ${HAIRLINK_DB_INITIAL_SIZE:5}
# 最小连接池数量
minIdle: 10
minIdle: ${HAIRLINK_DB_MIN_IDLE:10}
# 最大连接池数量
maxActive: 20
maxActive: ${HAIRLINK_DB_MAX_ACTIVE:20}
# 配置获取连接等待超时的时间
maxWait: 60000
maxWait: ${HAIRLINK_DB_MAX_WAIT:60000}
# 配置连接超时时间
connectTimeout: 30000
connectTimeout: ${HAIRLINK_DB_CONNECT_TIMEOUT:30000}
# 配置网络超时时间
socketTimeout: 60000
socketTimeout: ${HAIRLINK_DB_SOCKET_TIMEOUT:60000}
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
timeBetweenEvictionRunsMillis: ${HAIRLINK_DB_TIME_BETWEEN_EVICTION:60000}
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
minEvictableIdleTimeMillis: ${HAIRLINK_DB_MIN_EVICTABLE_IDLE_TIME:300000}
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
maxEvictableIdleTimeMillis: ${HAIRLINK_DB_MAX_EVICTABLE_IDLE_TIME:900000}
# 配置检测连接是否有效
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
@ -47,14 +47,14 @@ spring:
allow:
url-pattern: /druid/*
# 控制台管理用户名和密码
login-username: ruoyi
login-password: 123456
login-username: ${HAIRLINK_DRUID_USERNAME:admin}
login-password: ${HAIRLINK_DRUID_PASSWORD:123456}
filter:
stat:
enabled: true
# 慢SQL记录
log-slow-sql: true
slow-sql-millis: 1000
slow-sql-millis: ${HAIRLINK_DRUID_SLOW_SQL_MILLIS:1000}
merge-sql: true
wall:
config:

View File

@ -1,49 +1,49 @@
# 项目相关配置
ruoyi:
hairlink:
# 名称
name: RuoYi
name: Hair-Link
# 版本
version: 3.9.1
# 版权年份
copyrightYear: 2025
# 文件路径 示例( Windows配置D:/ruoyi/uploadPathLinux配置 /home/ruoyi/uploadPath
profile: D:/ruoyi/uploadPath
# 文件路径 示例( Windows配置D:/hairlink/uploadPathLinux配置 /home/hairlink/uploadPath
profile: ${HAIRLINK_UPLOAD_PATH:./upload}
# 获取ip地址开关
addressEnabled: false
addressEnabled: ${HAIRLINK_ADDRESS_ENABLED:false}
# 验证码类型 math 数字计算 char 字符验证
captchaType: math
captchaType: ${HAIRLINK_CAPTCHA_TYPE:math}
# 开发环境配置
server:
# 服务器的HTTP端口默认为8080
port: 8080
port: ${HAIRLINK_SERVER_PORT:8080}
servlet:
# 应用的访问路径
context-path: /
context-path: ${HAIRLINK_CONTEXT_PATH:/}
tomcat:
# tomcat的URI编码
uri-encoding: UTF-8
uri-encoding: ${HAIRLINK_SERVER_URI_ENCODING:UTF-8}
# 连接数满后的排队数默认为100
accept-count: 1000
accept-count: ${HAIRLINK_SERVER_ACCEPT_COUNT:1000}
threads:
# tomcat最大线程数默认为200
max: 800
max: ${HAIRLINK_SERVER_MAX_THREADS:800}
# Tomcat启动初始化的线程数默认值10
min-spare: 100
min-spare: ${HAIRLINK_SERVER_MIN_SPARE_THREADS:100}
# 日志配置
logging:
level:
com.ruoyi: debug
org.springframework: warn
com.ruoyi: ${HAIRLINK_LOG_LEVEL_RUOYI:debug}
org.springframework: ${HAIRLINK_LOG_LEVEL_SPRING:warn}
# 用户配置
user:
password:
# 密码最大错误次数
maxRetryCount: 5
maxRetryCount: ${HAIRLINK_PASSWORD_MAX_RETRY:5}
# 密码锁定时间默认10分钟
lockTime: 10
lockTime: ${HAIRLINK_PASSWORD_LOCK_TIME:10}
# Spring配置
spring:
@ -57,45 +57,45 @@ spring:
servlet:
multipart:
# 单个文件大小
max-file-size: 10MB
max-file-size: ${HAIRLINK_MAX_FILE_SIZE:10MB}
# 设置总上传的文件大小
max-request-size: 20MB
max-request-size: ${HAIRLINK_MAX_REQUEST_SIZE:20MB}
# 服务模块
devtools:
restart:
# 热部署开关
enabled: true
enabled: ${HAIRLINK_DEVTOOLS_ENABLED:true}
# redis 配置
redis:
# 地址
host: localhost
host: ${HAIRLINK_REDIS_HOST:localhost}
# 端口默认为6379
port: 6379
port: ${HAIRLINK_REDIS_PORT:6379}
# 数据库索引
database: 0
database: ${HAIRLINK_REDIS_DATABASE:0}
# 密码
password:
password: ${HAIRLINK_REDIS_PASSWORD:}
# 连接超时时间
timeout: 10s
timeout: ${HAIRLINK_REDIS_TIMEOUT:10s}
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
min-idle: ${HAIRLINK_REDIS_MIN_IDLE:0}
# 连接池中的最大空闲连接
max-idle: 8
max-idle: ${HAIRLINK_REDIS_MAX_IDLE:8}
# 连接池的最大数据库连接数
max-active: 8
max-active: ${HAIRLINK_REDIS_MAX_ACTIVE:8}
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
max-wait: ${HAIRLINK_REDIS_MAX_WAIT:-1ms}
# token配置
token:
# 令牌自定义标识
header: Authorization
header: ${HAIRLINK_TOKEN_HEADER:Authorization}
# 令牌密钥
secret: abcdefghijklmnopqrstuvwxyz
secret: ${HAIRLINK_TOKEN_SECRET:abcdefghijklmnopqrstuvwxyz}
# 令牌有效期默认30分钟
expireTime: 30
expireTime: ${HAIRLINK_TOKEN_EXPIRE_TIME:30}
# MyBatis配置
mybatis:
@ -115,22 +115,22 @@ pagehelper:
# Swagger配置
swagger:
# 是否开启swagger
enabled: true
enabled: ${HAIRLINK_SWAGGER_ENABLED:true}
# 请求前缀
pathMapping: /dev-api
pathMapping: ${HAIRLINK_SWAGGER_PATH_MAPPING:/dev-api}
# 防盗链配置
referer:
# 防盗链开关
enabled: false
enabled: ${HAIRLINK_REFERER_ENABLED:false}
# 允许的域名列表
allowed-domains: localhost,127.0.0.1,ruoyi.vip,www.ruoyi.vip
allowed-domains: ${HAIRLINK_REFERER_ALLOWED_DOMAINS:localhost,127.0.0.1}
# 防止XSS攻击
xss:
# 过滤开关
enabled: true
enabled: ${HAIRLINK_XSS_ENABLED:true}
# 排除链接(多个用逗号分隔)
excludes: /system/notice
excludes: ${HAIRLINK_XSS_EXCLUDES:/system/notice}
# 匹配链接
urlPatterns: /system/*,/monitor/*,/tool/*
urlPatterns: ${HAIRLINK_XSS_URL_PATTERNS:/system/*,/monitor/*,/tool/*}

View File

@ -1,24 +1,30 @@
Application Version: ${ruoyi.version}
Application Version: ${hairlink.version}
Spring Boot Version: ${spring-boot.version}
////////////////////////////////////////////////////////////////////
// _ooOoo_ //
// o8888888o //
// 88" . "88 //
// (| ^_^ |) //
// O\ = /O //
// ____/`---'\____ //
// .' \\| |// `. //
// / \\||| : |||// \ //
// / _||||| -:- |||||- \ //
// | | \\\ - /// | | //
// | \_| ''\---/'' | | //
// \ .-\__ `-` ___/-. / //
// ___`. .' /--.--\ `. . ___ //
// ."" '< `.___\_<|>_/___.' >'"". //
// | | : `- \`.;`\ _ /`;.`/ - ` : | | //
// \ \ `-. \_ __\ /__ _/ .-` / / //
// ========`-.____`-.___\_____/___.-`____.-'======== //
// `=---=' //
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //
// 佛祖保佑 永不宕机 永无BUG //
////////////////////////////////////////////////////////////////////
╔═══════════════════════╗
║ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ║
║ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ║
║ ▓▓▓▓▓▓▓▓▓▓▓▓▓ ║
╚══╗ ▓▓▓▓▓▓▓▓▓ ╔══╝
╲ ▓▓▓▓▓▓▓
╲ ▓▓▓▓▓
╲ ▓▓▓
╲ ▓
V
░ ╲
░░░ ╲
░░░░░ ╲
░░░░░░░ ╲
╔══╝ ░░░░░░░░░ ╚══╗
║ ░░░░░░░░░░░░░ ║
║ ░░░░░░░░░░░░░░░░░ ║
║ ░░░░░░░░░░░░░░░░░░░ ║
╚═══════════════════════╝
═══【时 光】═══

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 日志存放路径 -->
<property name="log.path" value="/home/ruoyi/logs" />
<property name="log.path" value="${HAIRLINK_LOG_PATH:-./logs}" />
<!-- 日志输出格式 -->
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
@ -20,7 +20,7 @@
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
<maxHistory>${HAIRLINK_LOG_MAX_HISTORY:-60}</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
@ -42,7 +42,7 @@
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
<maxHistory>${HAIRLINK_LOG_MAX_HISTORY:-60}</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
@ -64,7 +64,7 @@
<!-- 按天回滚 daily -->
<fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
<maxHistory>${HAIRLINK_LOG_MAX_HISTORY:-60}</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>

View File

@ -3,7 +3,7 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>ruoyi</artifactId>
<artifactId>hair-link</artifactId>
<groupId>com.ruoyi</groupId>
<version>3.9.1</version>
</parent>
@ -113,6 +113,12 @@
<artifactId>javax.servlet-api</artifactId>
</dependency>
<!-- 多租户支持 - TransmittableThreadLocal -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -5,11 +5,11 @@ import org.springframework.stereotype.Component;
/**
*
*
*
* @author ruoyi
*/
@Component
@ConfigurationProperties(prefix = "ruoyi")
@ConfigurationProperties(prefix = "hairlink")
public class RuoYiConfig
{
/** 项目名称 */

View File

@ -0,0 +1,144 @@
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<Long> SUPER_ADMIN_USER_IDS = Arrays.asList(
1L // admin 用户
);
/**
* URL
* Reason:
*/
public static final List<String> IGNORE_URLS = Arrays.asList(
"/login",
"/captchaImage",
"/logout",
"/register"
);
/**
*
* Reason:
*/
public static final List<String> 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: (tenant_id=0)
* 使 WHERE (tenant_id = ? OR tenant_id = 0)
*/
public static final List<String> HYBRID_TABLES = Arrays.asList(
"sys_dict_type",
"sys_dict_data"
);
/**
*
* Reason:
*/
public static final List<String> IGNORE_TABLES = Arrays.asList(
"sys_menu",
"sys_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);
}
/**
*
* Reason: (tenant_id=0)
*
* @param tableName
* @return true-false-
*/
public static boolean isHybridTable(String tableName) {
if (tableName == null) {
return false;
}
return HYBRID_TABLES.stream().anyMatch(tableName.toLowerCase()::equals);
}
}

View File

@ -0,0 +1,98 @@
package com.ruoyi.common.core.context;
import com.alibaba.ttl.TransmittableThreadLocal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* - ThreadLocalID
*
* Reason: 使TransmittableThreadLocal线
*/
public class TenantContext {
private static final Logger log = LoggerFactory.getLogger(TenantContext.class);
/**
* 线ThreadLocal
* Reason: 使线ThreadLocal
*/
private static final TransmittableThreadLocal<Long> TENANT_ID_HOLDER = new TransmittableThreadLocal<>();
/**
*
*/
private static final TransmittableThreadLocal<Boolean> 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;
}
}

View File

@ -38,6 +38,10 @@ public class BaseEntity implements Serializable
/** 备注 */
private String remark;
/** 租户ID */
@JsonIgnore
private Long tenantId;
/** 请求参数 */
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private Map<String, Object> 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;
}
}

View File

@ -22,6 +22,9 @@ public class SysDictData extends BaseEntity
@Excel(name = "字典编码", cellType = ColumnType.NUMERIC)
private Long dictCode;
/** 租户ID0=系统级) */
private Long tenantId;
/** 字典排序 */
@Excel(name = "字典排序", cellType = ColumnType.NUMERIC)
private Long dictSort;
@ -62,6 +65,16 @@ public class SysDictData extends BaseEntity
this.dictCode = dictCode;
}
public Long getTenantId()
{
return tenantId;
}
public void setTenantId(Long tenantId)
{
this.tenantId = tenantId;
}
public Long getDictSort()
{
return dictSort;
@ -158,6 +171,7 @@ public class SysDictData extends BaseEntity
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("dictCode", getDictCode())
.append("tenantId", getTenantId())
.append("dictSort", getDictSort())
.append("dictLabel", getDictLabel())
.append("dictValue", getDictValue())

View File

@ -22,6 +22,9 @@ public class SysDictType extends BaseEntity
@Excel(name = "字典主键", cellType = ColumnType.NUMERIC)
private Long dictId;
/** 租户ID0=系统级) */
private Long tenantId;
/** 字典名称 */
@Excel(name = "字典名称")
private String dictName;
@ -44,6 +47,16 @@ public class SysDictType extends BaseEntity
this.dictId = dictId;
}
public Long getTenantId()
{
return tenantId;
}
public void setTenantId(Long tenantId)
{
this.tenantId = tenantId;
}
@NotBlank(message = "字典名称不能为空")
@Size(min = 0, max = 100, message = "字典类型名称长度不能超过100个字符")
public String getDictName()
@ -83,6 +96,7 @@ public class SysDictType extends BaseEntity
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("dictId", getDictId())
.append("tenantId", getTenantId())
.append("dictName", getDictName())
.append("dictType", getDictType())
.append("status", getStatus())

View File

@ -93,6 +93,9 @@ public class SysUser extends BaseEntity
/** 角色ID */
private Long roleId;
/** 租户ID */
private Long tenantId;
public SysUser()
{
@ -312,6 +315,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)

View File

@ -27,6 +27,11 @@ public class LoginBody
*/
private String uuid;
/**
*
*/
private String tenantCode;
public String getUsername()
{
return username;
@ -66,4 +71,14 @@ public class LoginBody
{
this.uuid = uuid;
}
public String getTenantCode()
{
return tenantCode;
}
public void setTenantCode(String tenantCode)
{
this.tenantCode = tenantCode;
}
}

View File

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

View File

@ -8,6 +8,7 @@ import com.alibaba.fastjson2.JSONArray;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.core.domain.entity.SysDictData;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.core.context.TenantContext;
import com.ruoyi.common.utils.spring.SpringUtils;
/**
@ -206,12 +207,24 @@ public class DictUtils
/**
* cache key
*
* Reason: tenant_id=0 + tenant_id=ID
* +
*
* @param configKey
* @return key
*/
public static String getCacheKey(String configKey)
{
return CacheConstants.SYS_DICT_KEY + configKey;
// 超级管理员或全局模式使用全局缓存
if (TenantContext.isIgnore()) {
return CacheConstants.SYS_DICT_KEY + "global:" + configKey;
}
// 租户使用租户级缓存
Long tenantId = TenantContext.getTenantId();
if (tenantId != null) {
return CacheConstants.SYS_DICT_KEY + "tenant_" + tenantId + ":" + configKey;
}
// 兜底:无租户上下文时使用全局缓存
return CacheConstants.SYS_DICT_KEY + "global:" + configKey;
}
}

View File

@ -3,7 +3,7 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>ruoyi</artifactId>
<artifactId>hair-link</artifactId>
<groupId>com.ruoyi</groupId>
<version>3.9.1</version>
</parent>
@ -59,6 +59,12 @@
<artifactId>ruoyi-system</artifactId>
</dependency>
<!-- SQL解析器 - 用于MyBatis租户拦截器 -->
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
</dependency>
</dependencies>
</project>

View File

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

View File

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

View File

@ -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;
/**
* - LoginUsertenant_idTenantContext
*
* Reason: HTTPThreadLocal
*
*
* 1. preHandle()
* 2. SecurityContextLoginUser
* 3.
* -
* - tenant_idTenantContext
* 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已清除");
}
}

View File

@ -0,0 +1,450 @@
package com.ruoyi.framework.interceptor;
import java.sql.Connection;
import java.util.List;
import java.util.Properties;
import com.ruoyi.common.exception.ServiceException;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.ItemsList;
import net.sf.jsqlparser.expression.operators.relational.MultiExpressionList;
import net.sf.jsqlparser.expression.Parenthesis;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.insert.Insert;
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.select.SubSelect;
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;
/**
* MyBatisSQL - SQL tenant_id
*
* Reason: DataScopeSQL
*
*
* 1. MyBatisStatementHandler.prepare()
* 2. SQL
* 3. SELECT/UPDATE/DELETE +
* 4. 使JSQLParserSQL
* 5. WHERE tenant_id = ?
* 6. SQLBoundSql
*/
@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、INSERT
if (sqlCommandType != SqlCommandType.SELECT &&
sqlCommandType != SqlCommandType.UPDATE &&
sqlCommandType != SqlCommandType.DELETE &&
sqlCommandType != SqlCommandType.INSERT) {
return false;
}
try {
// 解析SQL获取主表名
Statement statement = CCJSqlParserUtil.parse(sql);
String mainTableName = getMainTableName(statement);
// 租户表或混合模式表都需要拦截
return mainTableName != null && (isTenantTable(mainTableName) || isHybridTable(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();
} else if (statement instanceof Insert) {
return ((Insert) statement).getTable().getName();
}
return null;
}
/**
*
*
* @param tableName
* @return true-false-
*/
private boolean isTenantTable(String tableName) {
return TenantConfig.needTenantFilter(tableName);
}
/**
*
* Reason: (tenant_id=0)
*
* @param tableName
* @return true-false-
*/
private boolean isHybridTable(String tableName) {
return TenantConfig.isHybridTable(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);
String tableName = getMainTableName(statement);
boolean isHybrid = isHybridTable(tableName);
if (statement instanceof Select) {
// 处理 SELECT 语句
Select selectStatement = (Select) statement;
PlainSelect plainSelect = (PlainSelect) selectStatement.getSelectBody();
// 获取主表别名避免多表JOIN时tenant_id歧义
String tableAlias = getMainTableAlias(plainSelect);
// 根据表类型构造不同的租户条件
net.sf.jsqlparser.expression.Expression tenantCondition;
if (isHybrid) {
// 混合模式:(tenant_id = ? OR tenant_id = 0)
tenantCondition = buildHybridTenantCondition(tenantId, tableAlias);
} else {
// 普通租户表tenant_id = ?
tenantCondition = buildTenantCondition(tenantId, tableAlias);
}
// 合并WHERE条件
if (plainSelect.getWhere() != null) {
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);
// 根据表类型构造不同的租户条件
// Reason: 混合模式表的UPDATE只能操作租户自己的数据不能修改系统级数据(tenant_id=0)
net.sf.jsqlparser.expression.Expression tenantCondition;
tenantCondition = buildTenantCondition(tenantId, tableAlias);
if (updateStatement.getWhere() != null) {
AndExpression andExpression = new AndExpression(tenantCondition, updateStatement.getWhere());
updateStatement.setWhere(andExpression);
} else {
updateStatement.setWhere(tenantCondition);
}
return updateStatement.toString();
}
else if (statement instanceof Delete) {
// 处理 DELETE 语句
Delete deleteStatement = (Delete) statement;
// 获取表别名
String tableAlias = getDeleteTableAlias(deleteStatement);
// 根据表类型构造不同的租户条件
// Reason: 混合模式表的DELETE只能操作租户自己的数据不能删除系统级数据(tenant_id=0)
net.sf.jsqlparser.expression.Expression tenantCondition;
tenantCondition = buildTenantCondition(tenantId, tableAlias);
if (deleteStatement.getWhere() != null) {
AndExpression andExpression = new AndExpression(tenantCondition, deleteStatement.getWhere());
deleteStatement.setWhere(andExpression);
} else {
deleteStatement.setWhere(tenantCondition);
}
return deleteStatement.toString();
}
else if (statement instanceof Insert) {
// 处理 INSERT 语句
return processInsert((Insert) statement, tenantId);
}
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;
}
/**
* ([.]tenant_id = ? OR [.]tenant_id = 0)
* Reason: (tenant_id=0)
*
* @param tenantId ID
* @param tableAlias null
* @return Parenthesis OR
*/
private Parenthesis buildHybridTenantCondition(Long tenantId, String tableAlias) {
// 构造 tenant_id = ?
EqualsTo tenantCondition = new EqualsTo();
if (tableAlias != null) {
tenantCondition.setLeftExpression(new Column(new Table(tableAlias), "tenant_id"));
} else {
tenantCondition.setLeftExpression(new Column("tenant_id"));
}
tenantCondition.setRightExpression(new LongValue(tenantId));
// 构造 tenant_id = 0
EqualsTo systemCondition = new EqualsTo();
if (tableAlias != null) {
systemCondition.setLeftExpression(new Column(new Table(tableAlias), "tenant_id"));
} else {
systemCondition.setLeftExpression(new Column("tenant_id"));
}
systemCondition.setRightExpression(new LongValue(0));
// 构造 (tenant_id = ? OR tenant_id = 0)
OrExpression orExpression = new OrExpression(tenantCondition, systemCondition);
return new Parenthesis(orExpression);
}
/**
* INSERT - tenant_id
*
* Reason: tenant_id
*
* @param insertStatement INSERT
* @param tenantId ID
* @return SQL
*/
private String processInsert(Insert insertStatement, Long tenantId) {
// 1. 获取表名
String tableName = insertStatement.getTable().getName();
// 2. 获取列定义
List<Column> columns = insertStatement.getColumns();
if (columns == null || columns.isEmpty()) {
// 无列名INSERT: INSERT INTO table VALUES (...)
log.warn("【租户INSERT】无列名语句无法自动填充tenant_id: {}", tableName);
return insertStatement.toString();
}
// 3. 检查是否已有tenant_id
boolean hasTenantId = columns.stream()
.anyMatch(col -> "tenant_id".equalsIgnoreCase(col.getColumnName()));
if (hasTenantId) {
log.debug("【租户INSERT】已包含tenant_id,跳过自动填充: {}", tableName);
return insertStatement.toString();
}
// 4. 自动添加tenant_id列
columns.add(new Column("tenant_id"));
// 5. 处理VALUES
ItemsList itemsList = insertStatement.getItemsList();
if (itemsList instanceof ExpressionList) {
// 单行INSERT
ExpressionList expressionList = (ExpressionList) itemsList;
expressionList.getExpressions().add(new LongValue(tenantId));
}
else if (itemsList instanceof MultiExpressionList) {
// 批量INSERT
MultiExpressionList multiList = (MultiExpressionList) itemsList;
for (ExpressionList expList : multiList.getExpressionLists()) {
expList.getExpressions().add(new LongValue(tenantId));
}
}
else if (itemsList instanceof SubSelect) {
// INSERT SELECT暂不支持
log.warn("【租户INSERT】INSERT SELECT语句暂不支持自动填充: {}", tableName);
return insertStatement.toString();
}
log.info("【租户INSERT拦截】自动填充tenant_id={}: {}", tenantId, tableName);
return insertStatement.toString();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 可以从配置文件读取租户表配置
}
}

View File

@ -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,20 +24,24 @@ 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;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.system.service.ISysUserService;
import com.ruoyi.system.service.ISysTenantService;
import com.ruoyi.system.domain.SysTenant;
/**
*
*
*
* @author ruoyi
*/
@Component
public class SysLoginService
{
private static final Logger log = LoggerFactory.getLogger(SysLoginService.class);
@Autowired
private TokenService tokenService;
@ -51,52 +57,106 @@ public class SysLoginService
@Autowired
private ISysConfigService configService;
@Autowired
private ISysTenantService tenantService;
/**
*
*
*
* @param username
* @param password
* @param code
* @param uuid
* @param tenantCode
* @return
*/
public String login(String username, String password, String code, String uuid)
public String login(String username, String password, String code, String uuid, String tenantCode)
{
// 验证码校验
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);
// 【多租户】通过租户编码查询租户ID并设置到上下文
Long tenantId = null;
if (StringUtils.isNotEmpty(tenantCode))
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
throw new UserPasswordNotMatchException();
SysTenant tenant = tenantService.selectSysTenantByTenantCode(tenantCode);
if (tenant == null)
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, "租户不存在或已停用"));
throw new ServiceException("租户不存在或已停用");
}
tenantId = tenant.getTenantId();
// 将租户ID设置到上下文供 UserDetailsServiceImpl 使用
TenantContext.setTenantId(tenantId);
log.info("【多租户】登录租户编码: {}, 租户ID: {}", tenantCode, tenantId);
}
// 用户验证
Authentication authentication = null;
try
{
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);
}
/**

View File

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

View File

@ -7,6 +7,7 @@ import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.ruoyi.common.core.context.TenantContext;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.enums.UserStatus;
@ -27,7 +28,7 @@ public class UserDetailsServiceImpl implements UserDetailsService
@Autowired
private ISysUserService userService;
@Autowired
private SysPasswordService passwordService;
@ -37,7 +38,22 @@ public class UserDetailsServiceImpl implements UserDetailsService
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
SysUser user = userService.selectUserByUserName(username);
// 【多租户】从上下文获取租户ID用于查询指定租户的用户
Long tenantId = TenantContext.getTenantId();
SysUser user;
if (tenantId != null)
{
// 有租户ID时查询指定租户的用户
user = userService.selectUserByUserNameAndTenantId(username, tenantId);
log.debug("【多租户】查询用户: {}, 租户ID: {}", username, tenantId);
}
else
{
// 无租户ID时超级管理员登录查询所有用户
user = userService.selectUserByUserName(username);
log.debug("【多租户】超级管理员登录,查询用户: {}", username);
}
if (StringUtils.isNull(user))
{
log.info("登录用户:{} 不存在.", username);

View File

@ -3,7 +3,7 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>ruoyi</artifactId>
<artifactId>hair-link</artifactId>
<groupId>com.ruoyi</groupId>
<version>3.9.1</version>
</parent>

View File

@ -3,7 +3,7 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>ruoyi</artifactId>
<artifactId>hair-link</artifactId>
<groupId>com.ruoyi</groupId>
<version>3.9.1</version>
</parent>

View File

@ -3,7 +3,7 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>ruoyi</artifactId>
<artifactId>hair-link</artifactId>
<groupId>com.ruoyi</groupId>
<version>3.9.1</version>
</parent>

View File

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

View File

@ -0,0 +1,92 @@
package com.ruoyi.system.domain;
import com.ruoyi.common.core.domain.BaseEntity;
/**
* sys_tenant_package
*
* @author ruoyi
* @date 2025-12-19
*/
public class SysTenantPackage extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 套餐ID */
private Long packageId;
/** 套餐名称 */
private String packageName;
/** 套餐编码 */
private String packageCode;
/** 状态0正常 1停用 */
private String status;
/** 删除标志0存在 2删除 */
private String delFlag;
/** 菜单ID列表非数据库字段 */
private Long[] menuIds;
public void setPackageId(Long packageId)
{
this.packageId = packageId;
}
public Long getPackageId()
{
return packageId;
}
public void setPackageName(String packageName)
{
this.packageName = packageName;
}
public String getPackageName()
{
return packageName;
}
public void setPackageCode(String packageCode)
{
this.packageCode = packageCode;
}
public String getPackageCode()
{
return packageCode;
}
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;
}
public Long[] getMenuIds()
{
return menuIds;
}
public void setMenuIds(Long[] menuIds)
{
this.menuIds = menuIds;
}
}

View File

@ -86,10 +86,20 @@ public interface SysDictDataMapper
/**
*
*
*
* @param oldDictType
* @param newDictType
* @return
*/
public int updateDictDataType(@Param("oldDictType") String oldDictType, @Param("newDictType") String newDictType);
/**
* ID
* Reason: (tenant_id=0)
*
* @param dictType
* @param tenantId ID
* @return
*/
public List<SysDictData> selectDictDataByTypeWithTenant(@Param("dictType") String dictType, @Param("tenantId") Long tenantId);
}

View File

@ -0,0 +1,71 @@
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 tenantCode
* @return
*/
public SysTenant selectSysTenantByTenantCode(String tenantCode);
/**
*
*
* @param sysTenant
* @return
*/
public List<SysTenant> 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);
}

View File

@ -0,0 +1,87 @@
package com.ruoyi.system.mapper;
import java.util.List;
import com.ruoyi.system.domain.SysTenantPackage;
import org.apache.ibatis.annotations.Param;
/**
* Mapper
*
* @author ruoyi
* @date 2025-12-19
*/
public interface SysTenantPackageMapper
{
/**
*
*
* @param packageId
* @return
*/
public SysTenantPackage selectPackageById(Long packageId);
/**
*
*
* @param sysTenantPackage
* @return
*/
public List<SysTenantPackage> selectPackageList(SysTenantPackage sysTenantPackage);
/**
*
*
* @param sysTenantPackage
* @return
*/
public int insertPackage(SysTenantPackage sysTenantPackage);
/**
*
*
* @param sysTenantPackage
* @return
*/
public int updatePackage(SysTenantPackage sysTenantPackage);
/**
*
*
* @param packageId
* @return
*/
public int deletePackageById(Long packageId);
/**
*
*
* @param packageIds
* @return
*/
public int deletePackageByIds(Long[] packageIds);
/**
* ID
*
* @param packageId ID
* @return ID
*/
public List<Long> selectMenuIdsByPackageId(Long packageId);
/**
*
*
* @param packageId ID
* @param menuIds ID
* @return
*/
public int batchInsertPackageMenu(@Param("packageId") Long packageId, @Param("menuIds") Long[] menuIds);
/**
*
*
* @param packageId ID
* @return
*/
public int deletePackageMenuByPackageId(Long packageId);
}

View File

@ -38,12 +38,21 @@ public interface SysUserMapper
/**
*
*
*
* @param userName
* @return
*/
public SysUser selectUserByUserName(String userName);
/**
* ID
*
* @param userName
* @param tenantId ID
* @return
*/
public SysUser selectUserByUserNameAndTenantId(@Param("userName") String userName, @Param("tenantId") Long tenantId);
/**
* ID
*
@ -123,25 +132,28 @@ public interface SysUserMapper
/**
*
*
*
* @param userName
* @param tenantId ID
* @return
*/
public SysUser checkUserNameUnique(String userName);
public SysUser checkUserNameUnique(@Param("userName") String userName, @Param("tenantId") Long tenantId);
/**
*
*
* @param phonenumber
* @param tenantId ID
* @return
*/
public SysUser checkPhoneUnique(String phonenumber);
public SysUser checkPhoneUnique(@Param("phonenumber") String phonenumber, @Param("tenantId") Long tenantId);
/**
* email
*
* @param email
* @param tenantId ID
* @return
*/
public SysUser checkEmailUnique(String email);
public SysUser checkEmailUnique(@Param("email") String email, @Param("tenantId") Long tenantId);
}

View File

@ -0,0 +1,71 @@
package com.ruoyi.system.service;
import java.util.List;
import com.ruoyi.system.domain.SysTenantPackage;
/**
* Service
*
* @author ruoyi
* @date 2025-12-19
*/
public interface ISysTenantPackageService
{
/**
*
*
* @param packageId
* @return
*/
public SysTenantPackage selectPackageById(Long packageId);
/**
*
*
* @param sysTenantPackage
* @return
*/
public List<SysTenantPackage> selectPackageList(SysTenantPackage sysTenantPackage);
/**
*
*
* @param sysTenantPackage
* @return
*/
public int insertPackage(SysTenantPackage sysTenantPackage);
/**
*
*
* @param sysTenantPackage
* @return
*/
public int updatePackage(SysTenantPackage sysTenantPackage);
/**
*
*
* @param packageIds
* @return
*/
public int deletePackageByIds(Long[] packageIds);
/**
*
*
* @param packageId
* @return
*/
public int deletePackageById(Long packageId);
/**
* ID
*
* Reason:
*
* @param packageId ID
* @return ID
*/
public List<Long> selectMenuIdsByPackageId(Long packageId);
}

View File

@ -0,0 +1,70 @@
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 tenantCode
* @return
*/
public SysTenant selectSysTenantByTenantCode(String tenantCode);
/**
*
*
* @param sysTenant
* @return
*/
public List<SysTenant> 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);
}

View File

@ -37,12 +37,21 @@ public interface ISysUserService
/**
*
*
*
* @param userName
* @return
*/
public SysUser selectUserByUserName(String userName);
/**
* ID
*
* @param userName
* @param tenantId ID
* @return
*/
public SysUser selectUserByUserNameAndTenantId(String userName, Long tenantId);
/**
* ID
*

View File

@ -3,6 +3,7 @@ package com.ruoyi.system.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.common.core.context.TenantContext;
import com.ruoyi.common.core.domain.entity.SysDictData;
import com.ruoyi.common.utils.DictUtils;
import com.ruoyi.system.mapper.SysDictDataMapper;
@ -75,13 +76,17 @@ public class SysDictDataServiceImpl implements ISysDictDataService
/**
*
*
*
* @param data
* @return
*/
@Override
public int insertDictData(SysDictData data)
{
// 自动填充 tenant_id混合模式普通租户填充租户ID超级管理员填充0表示系统级
Long tenantId = TenantContext.getTenantId();
data.setTenantId(tenantId != null ? tenantId : 0L);
int row = dictDataMapper.insertDictData(data);
if (row > 0)
{

View File

@ -9,6 +9,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.context.TenantContext;
import com.ruoyi.common.core.domain.entity.SysDictData;
import com.ruoyi.common.core.domain.entity.SysDictType;
import com.ruoyi.common.exception.ServiceException;
@ -133,16 +134,26 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService
/**
*
* Reason:
*/
@Override
public void loadingDictCache()
{
SysDictData dictData = new SysDictData();
dictData.setStatus("0");
Map<String, List<SysDictData>> dictDataMap = dictDataMapper.selectDictDataList(dictData).stream().collect(Collectors.groupingBy(SysDictData::getDictType));
for (Map.Entry<String, List<SysDictData>> entry : dictDataMap.entrySet())
// 启动时加载缓存,设置全局模式跳过租户过滤
TenantContext.setIgnore(true);
try
{
DictUtils.setDictCache(entry.getKey(), entry.getValue().stream().sorted(Comparator.comparing(SysDictData::getDictSort)).collect(Collectors.toList()));
SysDictData dictData = new SysDictData();
dictData.setStatus("0");
Map<String, List<SysDictData>> dictDataMap = dictDataMapper.selectDictDataList(dictData).stream().collect(Collectors.groupingBy(SysDictData::getDictType));
for (Map.Entry<String, List<SysDictData>> entry : dictDataMap.entrySet())
{
DictUtils.setDictCache(entry.getKey(), entry.getValue().stream().sorted(Comparator.comparing(SysDictData::getDictSort)).collect(Collectors.toList()));
}
}
finally
{
TenantContext.clear();
}
}
@ -167,13 +178,17 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService
/**
*
*
*
* @param dict
* @return
*/
@Override
public int insertDictType(SysDictType dict)
{
// 自动填充 tenant_id混合模式普通租户填充租户ID超级管理员填充0表示系统级
Long tenantId = TenantContext.getTenantId();
dict.setTenantId(tenantId != null ? tenantId : 0L);
int row = dictTypeMapper.insertDictType(dict);
if (row > 0)
{

View File

@ -8,6 +8,8 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.common.constant.Constants;
@ -16,14 +18,18 @@ import com.ruoyi.common.core.domain.TreeSelect;
import com.ruoyi.common.core.domain.entity.SysMenu;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.SysTenant;
import com.ruoyi.system.domain.vo.MetaVo;
import com.ruoyi.system.domain.vo.RouterVo;
import com.ruoyi.system.mapper.SysMenuMapper;
import com.ruoyi.system.mapper.SysRoleMapper;
import com.ruoyi.system.mapper.SysRoleMenuMapper;
import com.ruoyi.system.service.ISysMenuService;
import com.ruoyi.system.service.ISysTenantService;
import com.ruoyi.system.service.ISysTenantPackageService;
/**
*
@ -33,6 +39,8 @@ import com.ruoyi.system.service.ISysMenuService;
@Service
public class SysMenuServiceImpl implements ISysMenuService
{
private static final Logger log = LoggerFactory.getLogger(SysMenuServiceImpl.class);
public static final String PREMISSION_STRING = "perms[\"{0}\"]";
@Autowired
@ -44,6 +52,12 @@ public class SysMenuServiceImpl implements ISysMenuService
@Autowired
private SysRoleMenuMapper roleMenuMapper;
@Autowired
private ISysTenantService tenantService;
@Autowired
private ISysTenantPackageService packageService;
/**
*
*
@ -58,7 +72,7 @@ public class SysMenuServiceImpl implements ISysMenuService
/**
*
*
*
* @param menu
* @return
*/
@ -75,6 +89,8 @@ public class SysMenuServiceImpl implements ISysMenuService
{
menu.getParams().put("userId", userId);
menuList = menuMapper.selectMenuListByUserId(menu);
// 应用租户套餐过滤
menuList = applyPackageFilter(menuList);
}
return menuList;
}
@ -123,7 +139,7 @@ public class SysMenuServiceImpl implements ISysMenuService
/**
* ID
*
*
* @param userId
* @return
*/
@ -138,6 +154,8 @@ public class SysMenuServiceImpl implements ISysMenuService
else
{
menus = menuMapper.selectMenuTreeByUserId(userId);
// 应用租户套餐过滤
menus = applyPackageFilter(menus);
}
return getChildPerms(menus, 0);
}
@ -532,7 +550,7 @@ public class SysMenuServiceImpl implements ISysMenuService
/**
*
*
*
* @return
*/
public String innerLinkReplaceEach(String path)
@ -540,4 +558,61 @@ public class SysMenuServiceImpl implements ISysMenuService
return StringUtils.replaceEach(path, new String[] { Constants.HTTP, Constants.HTTPS, Constants.WWW, ".", ":" },
new String[] { "", "", "", "/", "/" });
}
/**
*
*
* Reason:
*
*
* @param menus
* @return
*/
private List<SysMenu> applyPackageFilter(List<SysMenu> menus)
{
try
{
// 1. 获取当前租户
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser == null || loginUser.getTenantId() == null)
{
return menus; // 无租户不过滤
}
// 2. 获取租户信息
SysTenant tenant = tenantService.selectSysTenantByTenantId(loginUser.getTenantId());
if (tenant == null || tenant.getPackageId() == null)
{
log.warn("租户 {} 未配置套餐,不限制菜单", loginUser.getTenantId());
return menus;
}
// 3. 获取套餐菜单ID集合
List<Long> packageMenuIds = packageService.selectMenuIdsByPackageId(tenant.getPackageId());
// 4. 空列表表示高级套餐(不限制)
if (packageMenuIds == null || packageMenuIds.isEmpty())
{
log.debug("租户 {} 使用高级套餐,不限制菜单", loginUser.getTenantId());
return menus;
}
// 5. 过滤:只保留套餐内菜单
Set<Long> packageMenuIdSet = new HashSet<>(packageMenuIds);
List<SysMenu> filteredMenus = menus.stream()
.filter(menu -> packageMenuIdSet.contains(menu.getMenuId()))
.collect(Collectors.toList());
log.debug("租户 {} 套餐过滤: {} → {} 个菜单",
loginUser.getTenantId(), menus.size(), filteredMenus.size());
return filteredMenus;
}
catch (Exception e)
{
log.error("应用租户套餐过滤失败,降级为不过滤", e);
return menus; // 容错:失败不影响正常功能
}
}
}

View File

@ -0,0 +1,124 @@
package com.ruoyi.system.service.impl;
import java.util.List;
import com.ruoyi.common.utils.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.system.mapper.SysTenantPackageMapper;
import com.ruoyi.system.domain.SysTenantPackage;
import com.ruoyi.system.service.ISysTenantPackageService;
/**
* Service
*
* @author ruoyi
* @date 2025-12-19
*/
@Service
public class SysTenantPackageServiceImpl implements ISysTenantPackageService
{
@Autowired
private SysTenantPackageMapper tenantPackageMapper;
/**
*
*
* @param packageId
* @return
*/
@Override
public SysTenantPackage selectPackageById(Long packageId)
{
return tenantPackageMapper.selectPackageById(packageId);
}
/**
*
*
* @param sysTenantPackage
* @return
*/
@Override
public List<SysTenantPackage> selectPackageList(SysTenantPackage sysTenantPackage)
{
return tenantPackageMapper.selectPackageList(sysTenantPackage);
}
/**
*
*
* @param sysTenantPackage
* @return
*/
@Override
@Transactional
public int insertPackage(SysTenantPackage sysTenantPackage)
{
sysTenantPackage.setCreateTime(DateUtils.getNowDate());
int rows = tenantPackageMapper.insertPackage(sysTenantPackage);
// 保存菜单关联
if (sysTenantPackage.getMenuIds() != null && sysTenantPackage.getMenuIds().length > 0)
{
tenantPackageMapper.batchInsertPackageMenu(sysTenantPackage.getPackageId(), sysTenantPackage.getMenuIds());
}
return rows;
}
/**
*
*
* @param sysTenantPackage
* @return
*/
@Override
@Transactional
public int updatePackage(SysTenantPackage sysTenantPackage)
{
sysTenantPackage.setUpdateTime(DateUtils.getNowDate());
// 先删除旧的菜单关联
tenantPackageMapper.deletePackageMenuByPackageId(sysTenantPackage.getPackageId());
// 再插入新的菜单关联
if (sysTenantPackage.getMenuIds() != null && sysTenantPackage.getMenuIds().length > 0)
{
tenantPackageMapper.batchInsertPackageMenu(sysTenantPackage.getPackageId(), sysTenantPackage.getMenuIds());
}
return tenantPackageMapper.updatePackage(sysTenantPackage);
}
/**
*
*
* @param packageIds
* @return
*/
@Override
public int deletePackageByIds(Long[] packageIds)
{
return tenantPackageMapper.deletePackageByIds(packageIds);
}
/**
*
*
* @param packageId
* @return
*/
@Override
public int deletePackageById(Long packageId)
{
return tenantPackageMapper.deletePackageById(packageId);
}
/**
* ID
*
* @param packageId ID
* @return ID
*/
@Override
public List<Long> selectMenuIdsByPackageId(Long packageId)
{
return tenantPackageMapper.selectMenuIdsByPackageId(packageId);
}
}

View File

@ -0,0 +1,108 @@
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 tenantCode
* @return
*/
@Override
public SysTenant selectSysTenantByTenantCode(String tenantCode)
{
return sysTenantMapper.selectSysTenantByTenantCode(tenantCode);
}
/**
*
*
* @param sysTenant
* @return
*/
@Override
public List<SysTenant> 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);
}
}

View File

@ -107,7 +107,7 @@ public class SysUserServiceImpl implements ISysUserService
/**
*
*
*
* @param userName
* @return
*/
@ -117,6 +117,19 @@ public class SysUserServiceImpl implements ISysUserService
return userMapper.selectUserByUserName(userName);
}
/**
* ID
*
* @param userName
* @param tenantId ID
* @return
*/
@Override
public SysUser selectUserByUserNameAndTenantId(String userName, Long tenantId)
{
return userMapper.selectUserByUserNameAndTenantId(userName, tenantId);
}
/**
* ID
*
@ -165,7 +178,8 @@ public class SysUserServiceImpl implements ISysUserService
/**
*
*
* Reason: Bug Fix - tenant_id
*
* @param user
* @return
*/
@ -173,7 +187,8 @@ public class SysUserServiceImpl implements ISysUserService
public boolean checkUserNameUnique(SysUser user)
{
Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId();
SysUser info = userMapper.checkUserNameUnique(user.getUserName());
Long tenantId = user.getTenantId();
SysUser info = userMapper.checkUserNameUnique(user.getUserName(), tenantId);
if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue())
{
return UserConstants.NOT_UNIQUE;
@ -183,6 +198,7 @@ public class SysUserServiceImpl implements ISysUserService
/**
*
* Reason: Bug Fix - tenant_id 使
*
* @param user
* @return
@ -191,7 +207,8 @@ public class SysUserServiceImpl implements ISysUserService
public boolean checkPhoneUnique(SysUser user)
{
Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId();
SysUser info = userMapper.checkPhoneUnique(user.getPhonenumber());
Long tenantId = user.getTenantId();
SysUser info = userMapper.checkPhoneUnique(user.getPhonenumber(), tenantId);
if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue())
{
return UserConstants.NOT_UNIQUE;
@ -201,6 +218,7 @@ public class SysUserServiceImpl implements ISysUserService
/**
* email
* Reason: Bug Fix - tenant_id 使
*
* @param user
* @return
@ -209,7 +227,8 @@ public class SysUserServiceImpl implements ISysUserService
public boolean checkEmailUnique(SysUser user)
{
Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId();
SysUser info = userMapper.checkEmailUnique(user.getEmail());
Long tenantId = user.getTenantId();
SysUser info = userMapper.checkEmailUnique(user.getEmail(), tenantId);
if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue())
{
return UserConstants.NOT_UNIQUE;

View File

@ -6,6 +6,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<resultMap type="SysDictData" id="SysDictDataResult">
<id property="dictCode" column="dict_code" />
<result property="tenantId" column="tenant_id" />
<result property="dictSort" column="dict_sort" />
<result property="dictLabel" column="dict_label" />
<result property="dictValue" column="dict_value" />
@ -19,9 +20,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
</resultMap>
<sql id="selectDictDataVo">
select dict_code, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark
select dict_code, tenant_id, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark
from sys_dict_data
</sql>
@ -95,6 +96,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<insert id="insertDictData" parameterType="SysDictData">
insert into sys_dict_data(
<if test="tenantId != null">tenant_id,</if>
<if test="dictSort != null">dict_sort,</if>
<if test="dictLabel != null and dictLabel != ''">dict_label,</if>
<if test="dictValue != null and dictValue != ''">dict_value,</if>
@ -107,6 +109,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="createBy != null and createBy != ''">create_by,</if>
create_time
)values(
<if test="tenantId != null">#{tenantId},</if>
<if test="dictSort != null">#{dictSort},</if>
<if test="dictLabel != null and dictLabel != ''">#{dictLabel},</if>
<if test="dictValue != null and dictValue != ''">#{dictValue},</if>
@ -120,5 +123,21 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
sysdate()
)
</insert>
<!-- 混合模式查询:合并系统字典和租户字典,租户优先 -->
<select id="selectDictDataByTypeWithTenant" resultMap="SysDictDataResult">
SELECT d1.* FROM sys_dict_data d1
WHERE d1.dict_type = #{dictType}
AND d1.status = '0'
AND d1.tenant_id IN (0, #{tenantId})
AND NOT EXISTS (
SELECT 1 FROM sys_dict_data d2
WHERE d2.dict_type = d1.dict_type
AND d2.dict_value = d1.dict_value
AND d2.tenant_id = #{tenantId}
AND d1.tenant_id = 0
)
ORDER BY d1.dict_sort ASC
</select>
</mapper>

View File

@ -6,6 +6,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<resultMap type="SysDictType" id="SysDictTypeResult">
<id property="dictId" column="dict_id" />
<result property="tenantId" column="tenant_id" />
<result property="dictName" column="dict_name" />
<result property="dictType" column="dict_type" />
<result property="status" column="status" />
@ -14,9 +15,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
</resultMap>
<sql id="selectDictTypeVo">
select dict_id, dict_name, dict_type, status, create_by, create_time, remark
select dict_id, tenant_id, dict_name, dict_type, status, create_by, create_time, remark
from sys_dict_type
</sql>
@ -39,6 +40,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
and date_format(create_time,'%Y%m%d') &lt;= date_format(#{params.endTime},'%Y%m%d')
</if>
</where>
order by create_time desc
</select>
<select id="selectDictTypeAll" resultMap="SysDictTypeResult">
@ -86,6 +88,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<insert id="insertDictType" parameterType="SysDictType">
insert into sys_dict_type(
<if test="tenantId != null">tenant_id,</if>
<if test="dictName != null and dictName != ''">dict_name,</if>
<if test="dictType != null and dictType != ''">dict_type,</if>
<if test="status != null">status,</if>
@ -93,6 +96,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="createBy != null and createBy != ''">create_by,</if>
create_time
)values(
<if test="tenantId != null">#{tenantId},</if>
<if test="dictName != null and dictName != ''">#{dictName},</if>
<if test="dictType != null and dictType != ''">#{dictType},</if>
<if test="status != null">#{status},</if>

View File

@ -105,6 +105,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="status != null and status != ''">status,</if>
<if test="remark != null and remark != ''">remark,</if>
<if test="createBy != null and createBy != ''">create_by,</if>
<if test="tenantId != null">tenant_id,</if>
create_time
)values(
<if test="roleId != null and roleId != 0">#{roleId},</if>
@ -117,6 +118,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="status != null and status != ''">#{status},</if>
<if test="remark != null and remark != ''">#{remark},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if>
<if test="tenantId != null">#{tenantId},</if>
sysdate()
)
</insert>

View File

@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.system.mapper.SysTenantMapper">
<resultMap type="SysTenant" id="SysTenantResult">
<result property="tenantId" column="tenant_id" />
<result property="tenantName" column="tenant_name" />
<result property="tenantCode" column="tenant_code" />
<result property="contactName" column="contact_name" />
<result property="contactPhone" column="contact_phone" />
<result property="expireTime" column="expire_time" />
<result property="packageId" column="package_id" />
<result property="status" column="status" />
<result property="delFlag" column="del_flag" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
</resultMap>
<sql id="selectSysTenantVo">
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
</sql>
<select id="selectSysTenantList" parameterType="SysTenant" resultMap="SysTenantResult">
<include refid="selectSysTenantVo"/>
<where>
<if test="tenantName != null and tenantName != ''"> and tenant_name like concat('%', #{tenantName}, '%')</if>
<if test="tenantCode != null and tenantCode != ''"> and tenant_code = #{tenantCode}</if>
<if test="contactName != null and contactName != ''"> and contact_name like concat('%', #{contactName}, '%')</if>
<if test="contactPhone != null and contactPhone != ''"> and contact_phone = #{contactPhone}</if>
<if test="expireTime != null "> and expire_time = #{expireTime}</if>
<if test="packageId != null "> and package_id = #{packageId}</if>
<if test="status != null and status != ''"> and status = #{status}</if>
</where>
</select>
<select id="selectSysTenantByTenantId" parameterType="Long" resultMap="SysTenantResult">
<include refid="selectSysTenantVo"/>
where tenant_id = #{tenantId}
</select>
<select id="selectSysTenantByTenantCode" parameterType="String" resultMap="SysTenantResult">
<include refid="selectSysTenantVo"/>
where tenant_code = #{tenantCode} and del_flag = '0' and status = '0'
</select>
<insert id="insertSysTenant" parameterType="SysTenant" useGeneratedKeys="true" keyProperty="tenantId">
insert into sys_tenant
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="tenantName != null and tenantName != ''">tenant_name,</if>
<if test="tenantCode != null and tenantCode != ''">tenant_code,</if>
<if test="contactName != null">contact_name,</if>
<if test="contactPhone != null">contact_phone,</if>
<if test="expireTime != null">expire_time,</if>
<if test="packageId != null">package_id,</if>
<if test="status != null">status,</if>
<if test="delFlag != null">del_flag,</if>
<if test="createBy != null">create_by,</if>
<if test="createTime != null">create_time,</if>
<if test="updateBy != null">update_by,</if>
<if test="updateTime != null">update_time,</if>
<if test="remark != null">remark,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="tenantName != null and tenantName != ''">#{tenantName},</if>
<if test="tenantCode != null and tenantCode != ''">#{tenantCode},</if>
<if test="contactName != null">#{contactName},</if>
<if test="contactPhone != null">#{contactPhone},</if>
<if test="expireTime != null">#{expireTime},</if>
<if test="packageId != null">#{packageId},</if>
<if test="status != null">#{status},</if>
<if test="delFlag != null">#{delFlag},</if>
<if test="createBy != null">#{createBy},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateBy != null">#{updateBy},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="remark != null">#{remark},</if>
</trim>
</insert>
<update id="updateSysTenant" parameterType="SysTenant">
update sys_tenant
<trim prefix="SET" suffixOverrides=",">
<if test="tenantName != null and tenantName != ''">tenant_name = #{tenantName},</if>
<if test="tenantCode != null and tenantCode != ''">tenant_code = #{tenantCode},</if>
<if test="contactName != null">contact_name = #{contactName},</if>
<if test="contactPhone != null">contact_phone = #{contactPhone},</if>
<if test="expireTime != null">expire_time = #{expireTime},</if>
<if test="packageId != null">package_id = #{packageId},</if>
<if test="status != null">status = #{status},</if>
<if test="delFlag != null">del_flag = #{delFlag},</if>
<if test="createBy != null">create_by = #{createBy},</if>
<if test="createTime != null">create_time = #{createTime},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="remark != null">remark = #{remark},</if>
</trim>
where tenant_id = #{tenantId}
</update>
<delete id="deleteSysTenantByTenantId" parameterType="Long">
delete from sys_tenant where tenant_id = #{tenantId}
</delete>
<delete id="deleteSysTenantByTenantIds" parameterType="String">
delete from sys_tenant where tenant_id in
<foreach item="tenantId" collection="array" open="(" separator="," close=")">
#{tenantId}
</foreach>
</delete>
</mapper>

View File

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.system.mapper.SysTenantPackageMapper">
<resultMap type="SysTenantPackage" id="SysTenantPackageResult">
<result property="packageId" column="package_id" />
<result property="packageName" column="package_name" />
<result property="packageCode" column="package_code" />
<result property="status" column="status" />
<result property="delFlag" column="del_flag" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
</resultMap>
<sql id="selectPackageVo">
select package_id, package_name, package_code, status, del_flag, create_by, create_time, update_by, update_time, remark from sys_tenant_package
</sql>
<select id="selectPackageList" parameterType="SysTenantPackage" resultMap="SysTenantPackageResult">
<include refid="selectPackageVo"/>
<where>
del_flag = '0'
<if test="packageName != null and packageName != ''"> and package_name like concat('%', #{packageName}, '%')</if>
<if test="packageCode != null and packageCode != ''"> and package_code = #{packageCode}</if>
<if test="status != null and status != ''"> and status = #{status}</if>
</where>
</select>
<select id="selectPackageById" parameterType="Long" resultMap="SysTenantPackageResult">
<include refid="selectPackageVo"/>
where package_id = #{packageId} and del_flag = '0'
</select>
<insert id="insertPackage" parameterType="SysTenantPackage" useGeneratedKeys="true" keyProperty="packageId">
insert into sys_tenant_package
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="packageName != null and packageName != ''">package_name,</if>
<if test="packageCode != null and packageCode != ''">package_code,</if>
<if test="status != null">status,</if>
<if test="delFlag != null">del_flag,</if>
<if test="createBy != null">create_by,</if>
<if test="createTime != null">create_time,</if>
<if test="updateBy != null">update_by,</if>
<if test="updateTime != null">update_time,</if>
<if test="remark != null">remark,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="packageName != null and packageName != ''">#{packageName},</if>
<if test="packageCode != null and packageCode != ''">#{packageCode},</if>
<if test="status != null">#{status},</if>
<if test="delFlag != null">#{delFlag},</if>
<if test="createBy != null">#{createBy},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateBy != null">#{updateBy},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="remark != null">#{remark},</if>
</trim>
</insert>
<update id="updatePackage" parameterType="SysTenantPackage">
update sys_tenant_package
<trim prefix="SET" suffixOverrides=",">
<if test="packageName != null and packageName != ''">package_name = #{packageName},</if>
<if test="packageCode != null and packageCode != ''">package_code = #{packageCode},</if>
<if test="status != null">status = #{status},</if>
<if test="delFlag != null">del_flag = #{delFlag},</if>
<if test="createBy != null">create_by = #{createBy},</if>
<if test="createTime != null">create_time = #{createTime},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="remark != null">remark = #{remark},</if>
</trim>
where package_id = #{packageId}
</update>
<update id="deletePackageById" parameterType="Long">
update sys_tenant_package set del_flag = '2' where package_id = #{packageId}
</update>
<update id="deletePackageByIds" parameterType="Long">
update sys_tenant_package set del_flag = '2' where package_id in
<foreach item="packageId" collection="array" open="(" separator="," close=")">
#{packageId}
</foreach>
</update>
<!-- 查询套餐包含的菜单ID列表 -->
<select id="selectMenuIdsByPackageId" parameterType="Long" resultType="Long">
select menu_id from sys_package_menu where package_id = #{packageId}
</select>
<!-- 批量新增套餐菜单关联 -->
<insert id="batchInsertPackageMenu">
insert into sys_package_menu(package_id, menu_id) values
<foreach item="menuId" collection="menuIds" separator=",">
(#{packageId}, #{menuId})
</foreach>
</insert>
<!-- 删除套餐菜单关联 -->
<delete id="deletePackageMenuByPackageId" parameterType="Long">
delete from sys_package_menu where package_id = #{packageId}
</delete>
</mapper>

View File

@ -7,6 +7,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<resultMap type="SysUser" id="SysUserResult">
<id property="userId" column="user_id" />
<result property="deptId" column="dept_id" />
<result property="tenantId" column="tenant_id" />
<result property="userName" column="user_name" />
<result property="nickName" column="nick_name" />
<result property="email" column="email" />
@ -48,7 +49,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap>
<sql id="selectUserVo">
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"
</sql>
<select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult">
select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader from sys_user u
select u.user_id, u.dept_id, u.tenant_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader from sys_user u
left join sys_dept d on u.dept_id = d.dept_id
where u.del_flag = '0'
<if test="userId != null and userId != 0">
@ -125,22 +126,27 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<include refid="selectUserVo"/>
where u.user_name = #{userName} and u.del_flag = '0'
</select>
<select id="selectUserByUserNameAndTenantId" resultMap="SysUserResult">
<include refid="selectUserVo"/>
where u.user_name = #{userName} and u.tenant_id = #{tenantId} and u.del_flag = '0'
</select>
<select id="selectUserById" parameterType="Long" resultMap="SysUserResult">
<include refid="selectUserVo"/>
where u.user_id = #{userId}
</select>
<select id="checkUserNameUnique" parameterType="String" resultMap="SysUserResult">
select user_id, user_name from sys_user where user_name = #{userName} and del_flag = '0' limit 1
<select id="checkUserNameUnique" resultMap="SysUserResult">
select user_id, user_name from sys_user where user_name = #{userName} and tenant_id = #{tenantId} and del_flag = '0' limit 1
</select>
<select id="checkPhoneUnique" parameterType="String" resultMap="SysUserResult">
select user_id, phonenumber from sys_user where phonenumber = #{phonenumber} and del_flag = '0' limit 1
<select id="checkPhoneUnique" resultMap="SysUserResult">
select user_id, phonenumber from sys_user where phonenumber = #{phonenumber} and tenant_id = #{tenantId} and del_flag = '0' limit 1
</select>
<select id="checkEmailUnique" parameterType="String" resultMap="SysUserResult">
select user_id, email from sys_user where email = #{email} and del_flag = '0' limit 1
<select id="checkEmailUnique" resultMap="SysUserResult">
select user_id, email from sys_user where email = #{email} and tenant_id = #{tenantId} and del_flag = '0' limit 1
</select>
<insert id="insertUser" parameterType="SysUser" useGeneratedKeys="true" keyProperty="userId">
@ -158,6 +164,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="pwdUpdateDate != null">pwd_update_date,</if>
<if test="createBy != null and createBy != ''">create_by,</if>
<if test="remark != null and remark != ''">remark,</if>
<if test="tenantId != null">tenant_id,</if>
create_time
)values(
<if test="userId != null and userId != ''">#{userId},</if>
@ -173,6 +180,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="pwdUpdateDate != null">#{pwdUpdateDate},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if>
<if test="remark != null and remark != ''">#{remark},</if>
<if test="tenantId != null">#{tenantId},</if>
sysdate()
)
</insert>

View File

@ -1,10 +1,10 @@
# 页面标题
VUE_APP_TITLE = 若依管理系统
VUE_APP_TITLE = Hair-Link
# 开发环境配置
ENV = 'development'
# 若依管理系统/开发环境
# Hair-Link/开发环境
VUE_APP_BASE_API = '/dev-api'
# 路由懒加载

View File

@ -1,8 +1,8 @@
# 页面标题
VUE_APP_TITLE = 若依管理系统
VUE_APP_TITLE = Hair-Link
# 生产环境配置
ENV = 'production'
# 若依管理系统/生产环境
# Hair-Link/生产环境
VUE_APP_BASE_API = '/prod-api'

View File

@ -19,10 +19,6 @@
"admin-template",
"management-system"
],
"repository": {
"type": "git",
"url": "https://gitee.com/y_project/RuoYi-Vue.git"
},
"dependencies": {
"@riophae/vue-treeselect": "0.4.0",
"axios": "0.28.1",

View File

@ -1,12 +1,13 @@
import request from '@/utils/request'
// 登录方法
export function login(username, password, code, uuid) {
export function login(username, password, code, uuid, tenantCode) {
const data = {
username,
password,
code,
uuid
uuid,
tenantCode
}
return request({
url: '/login',

View File

@ -0,0 +1,52 @@
import request from '@/utils/request'
// 查询套餐列表
export function listPackage(query) {
return request({
url: '/system/package/list',
method: 'get',
params: query
})
}
// 查询套餐详细
export function getPackage(packageId) {
return request({
url: '/system/package/' + packageId,
method: 'get'
})
}
// 新增套餐
export function addPackage(data) {
return request({
url: '/system/package',
method: 'post',
data: data
})
}
// 修改套餐
export function updatePackage(data) {
return request({
url: '/system/package',
method: 'put',
data: data
})
}
// 删除套餐
export function delPackage(packageId) {
return request({
url: '/system/package/' + packageId,
method: 'delete'
})
}
// 查询套餐关联的菜单ID列表
export function getPackageMenus(packageId) {
return request({
url: '/system/package/' + packageId + '/menus',
method: 'get'
})
}

View File

@ -0,0 +1,44 @@
import request from '@/utils/request'
// 查询租户信息列表
export function listTenant(query) {
return request({
url: '/link/tenant/list',
method: 'get',
params: query
})
}
// 查询租户信息详细
export function getTenant(tenantId) {
return request({
url: '/link/tenant/' + tenantId,
method: 'get'
})
}
// 新增租户信息
export function addTenant(data) {
return request({
url: '/link/tenant',
method: 'post',
data: data
})
}
// 修改租户信息
export function updateTenant(data) {
return request({
url: '/link/tenant',
method: 'put',
data: data
})
}
// 删除租户信息
export function delTenant(tenantId) {
return request({
url: '/link/tenant/' + tenantId,
method: 'delete'
})
}

View File

@ -134,3 +134,6 @@ export function deptTreeSelect() {
method: 'get'
})
}
// 导出租户列表查询方法
export { listTenant } from '@/api/system/tenant'

View File

@ -1,21 +0,0 @@
<template>
<div>
<svg-icon icon-class="question" @click="goto" />
</div>
</template>
<script>
export default {
name: 'RuoYiDoc',
data() {
return {
url: 'http://doc.ruoyi.vip/ruoyi-vue'
}
},
methods: {
goto() {
window.open(this.url)
}
}
}
</script>

View File

@ -1,21 +0,0 @@
<template>
<div>
<svg-icon icon-class="github" @click="goto" />
</div>
</template>
<script>
export default {
name: 'RuoYiGit',
data() {
return {
url: 'https://gitee.com/y_project/RuoYi-Vue'
}
},
methods: {
goto() {
window.open(this.url)
}
}
}
</script>

View File

@ -12,14 +12,6 @@
<template v-if="device!=='mobile'">
<search id="header-search" class="right-menu-item" />
<el-tooltip content="源码地址" effect="dark" placement="bottom">
<ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
</el-tooltip>
<el-tooltip content="文档地址" effect="dark" placement="bottom">
<ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" />
</el-tooltip>
<screenfull id="screenfull" class="right-menu-item hover-effect" />
<el-tooltip content="布局大小" effect="dark" placement="bottom">
@ -59,8 +51,6 @@ import Hamburger from '@/components/Hamburger'
import Screenfull from '@/components/Screenfull'
import SizeSelect from '@/components/SizeSelect'
import Search from '@/components/HeaderSearch'
import RuoYiGit from '@/components/RuoYi/Git'
import RuoYiDoc from '@/components/RuoYi/Doc'
export default {
emits: ['setLayout'],
@ -72,9 +62,7 @@ export default {
Hamburger,
Screenfull,
SizeSelect,
Search,
RuoYiGit,
RuoYiDoc
Search
},
computed: {
...mapGetters([

View File

@ -52,5 +52,5 @@ module.exports = {
/**
* 底部版权文本内容
*/
footerContent: 'Copyright © 2018-2025 RuoYi. All Rights Reserved.'
footerContent: 'Copyright © 2025 时光. All Rights Reserved.'
}

View File

@ -47,8 +47,9 @@ const user = {
const password = userInfo.password
const code = userInfo.code
const uuid = userInfo.uuid
const tenantCode = userInfo.tenantCode
return new Promise((resolve, reject) => {
login(username, password, code, uuid).then(res => {
login(username, password, code, uuid, tenantCode).then(res => {
setToken(res.token)
commit('SET_TOKEN', res.token)
resolve()
@ -67,9 +68,13 @@ const user = {
if (!isHttp(avatar)) {
avatar = (isEmpty(avatar)) ? defAva : process.env.VUE_APP_BASE_API + avatar
}
if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
commit('SET_ROLES', res.roles)
// 设置权限独立于角色解决跨租户角色关联时permissions无法设置的问题
if (res.permissions && res.permissions.length > 0) {
commit('SET_PERMISSIONS', res.permissions)
}
// 设置角色
if (res.roles && res.roles.length > 0) {
commit('SET_ROLES', res.roles)
} else {
commit('SET_ROLES', ['ROLE_DEFAULT'])
}

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,16 @@
<div class="login">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">{{title}}</h3>
<el-form-item prop="tenantCode">
<el-input
v-model="loginForm.tenantCode"
type="text"
auto-complete="off"
placeholder="租户编码(管理员留空)"
>
<svg-icon slot="prefix" icon-class="tree" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
@ -56,7 +66,7 @@
</el-form>
<!-- 底部 -->
<div class="el-login-footer">
<span>{{ footerContent }}</span>
<span>Copyright © 2025 时光. All Rights Reserved.</span>
</div>
</div>
</template>
@ -75,6 +85,7 @@ export default {
footerContent: defaultSettings.footerContent,
codeUrl: "",
loginForm: {
tenantCode: "",
username: "admin",
password: "admin123",
rememberMe: false,

View File

@ -0,0 +1,376 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="套餐名称" prop="packageName">
<el-input
v-model="queryParams.packageName"
placeholder="请输入套餐名称"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="套餐编码" prop="packageCode">
<el-input
v-model="queryParams.packageCode"
placeholder="请输入套餐编码"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
<el-option label="正常" value="0" />
<el-option label="停用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery"></el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery"></el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['system:package:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-edit"
size="mini"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['system:package:edit']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['system:package:remove']"
>删除</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="packageList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="套餐ID" align="center" prop="packageId" width="80" />
<el-table-column label="套餐名称" align="center" prop="packageName" :show-overflow-tooltip="true" />
<el-table-column label="套餐编码" align="center" prop="packageCode" :show-overflow-tooltip="true" />
<el-table-column label="状态" align="center" prop="status" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.status === '0' ? 'success' : 'danger'">
{{ scope.row.status === '0' ? '正常' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['system:package:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['system:package:remove']"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改套餐对话框 -->
<el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="套餐名称" prop="packageName">
<el-input v-model="form.packageName" placeholder="请输入套餐名称" />
</el-form-item>
<el-form-item label="套餐编码" prop="packageCode">
<el-input v-model="form.packageCode" placeholder="请输入套餐编码" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio label="0">正常</el-radio>
<el-radio label="1">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="菜单权限">
<el-checkbox v-model="menuExpand" @change="handleCheckedTreeExpand($event, 'menu')">/</el-checkbox>
<el-checkbox v-model="menuNodeAll" @change="handleCheckedTreeNodeAll($event, 'menu')">/</el-checkbox>
<el-checkbox v-model="form.menuCheckStrictly" @change="handleCheckedTreeConnect($event, 'menu')"></el-checkbox>
<el-tree
class="tree-border"
:data="menuOptions"
show-checkbox
ref="menu"
node-key="id"
:check-strictly="!form.menuCheckStrictly"
empty-text="加载中,请稍候"
:props="defaultProps"
></el-tree>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listPackage, getPackage, addPackage, updatePackage, delPackage, getPackageMenus } from "@/api/system/package"
import { treeselect as menuTreeselect } from "@/api/system/menu"
export default {
name: "Package",
data() {
return {
//
loading: true,
//
ids: [],
//
single: true,
//
multiple: true,
//
showSearch: true,
//
total: 0,
//
packageList: [],
//
title: "",
//
open: false,
//
menuOptions: [],
//
menuExpand: false,
//
menuNodeAll: false,
//
queryParams: {
pageNum: 1,
pageSize: 10,
packageName: null,
packageCode: null,
status: null
},
//
form: {},
//
defaultProps: {
children: "children",
label: "label"
},
//
rules: {
packageName: [
{ required: true, message: "套餐名称不能为空", trigger: "blur" }
],
packageCode: [
{ required: true, message: "套餐编码不能为空", trigger: "blur" }
]
}
}
},
created() {
this.getList()
},
methods: {
/** 查询套餐列表 */
getList() {
this.loading = true
listPackage(this.queryParams).then(response => {
this.packageList = response.rows
this.total = response.total
this.loading = false
})
},
/** 查询菜单树结构 */
getMenuTreeselect() {
menuTreeselect().then(response => {
this.menuOptions = response.data
})
},
//
cancel() {
this.open = false
this.reset()
},
//
reset() {
if (this.$refs.menu != undefined) {
this.$refs.menu.setCheckedKeys([])
}
this.menuExpand = false
this.menuNodeAll = false
this.form = {
packageId: null,
packageName: null,
packageCode: null,
status: "0",
menuCheckStrictly: true,
remark: null
}
this.resetForm("form")
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm")
this.handleQuery()
},
//
handleSelectionChange(selection) {
this.ids = selection.map(item => item.packageId)
this.single = selection.length !== 1
this.multiple = !selection.length
},
// /
handleCheckedTreeExpand(value, type) {
if (type == 'menu') {
let treeList = this.menuOptions
for (let i = 0; i < treeList.length; i++) {
this.$refs.menu.store.nodesMap[treeList[i].id].expanded = value
}
}
},
// /
handleCheckedTreeNodeAll(value, type) {
if (type == 'menu') {
this.$refs.menu.setCheckedNodes(value ? this.menuOptions : [])
}
},
//
handleCheckedTreeConnect(value, type) {
if (type == 'menu') {
this.form.menuCheckStrictly = value ? true : false
}
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.getMenuTreeselect()
this.open = true
this.title = "添加套餐"
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
this.getMenuTreeselect()
const packageId = row.packageId || this.ids
getPackage(packageId).then(response => {
this.form = response.data
this.form.menuCheckStrictly = true
this.open = true
this.title = "修改套餐"
//
this.$nextTick(() => {
getPackageMenus(packageId).then(res => {
let checkedKeys = res.data
checkedKeys.forEach((v) => {
this.$nextTick(() => {
this.$refs.menu.setChecked(v, true, false)
})
})
})
})
})
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
// ID
this.form.menuIds = this.getMenuAllCheckedKeys()
if (this.form.packageId != null) {
updatePackage(this.form).then(response => {
this.$modal.msgSuccess("修改成功")
this.open = false
this.getList()
})
} else {
addPackage(this.form).then(response => {
this.$modal.msgSuccess("新增成功")
this.open = false
this.getList()
})
}
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
const packageIds = row.packageId || this.ids
this.$modal.confirm('是否确认删除套餐编号为"' + packageIds + '"的数据项?').then(function() {
return delPackage(packageIds)
}).then(() => {
this.getList()
this.$modal.msgSuccess("删除成功")
}).catch(() => {})
},
//
getMenuAllCheckedKeys() {
//
let checkedKeys = this.$refs.menu.getCheckedKeys()
//
let halfCheckedKeys = this.$refs.menu.getHalfCheckedKeys()
checkedKeys.unshift.apply(checkedKeys, halfCheckedKeys)
return checkedKeys
}
}
}
</script>
<style scoped>
.tree-border {
margin-top: 5px;
border: 1px solid #e5e6e7;
background: #FFFFFF none;
border-radius: 4px;
max-height: 300px;
overflow-y: auto;
}
</style>

View File

@ -158,6 +158,16 @@
<!-- 添加或修改角色配置对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="所属租户" prop="tenantId" v-if="isSuperAdmin">
<el-select v-model="form.tenantId" placeholder="请选择所属租户" clearable style="width: 100%">
<el-option
v-for="item in tenantList"
:key="item.tenantId"
:label="item.tenantName"
:value="item.tenantId"
/>
</el-select>
</el-form-item>
<el-form-item label="角色名称" prop="roleName">
<el-input v-model="form.roleName" placeholder="请输入角色名称" />
</el-form-item>
@ -254,6 +264,7 @@
<script>
import { listRole, getRole, delRole, addRole, updateRole, dataScope, changeRoleStatus, deptTreeSelect } from "@/api/system/role"
import { treeselect as menuTreeselect, roleMenuTreeselect } from "@/api/system/menu"
import { listTenant } from "@/api/system/tenant"
export default {
name: "Role",
@ -313,6 +324,8 @@ export default {
menuOptions: [],
//
deptOptions: [],
//
tenantList: [],
//
queryParams: {
pageNum: 1,
@ -344,6 +357,13 @@ export default {
created() {
this.getList()
},
computed: {
// *:*:*
isSuperAdmin() {
const permissions = this.$store.getters && this.$store.getters.permissions
return permissions && permissions.includes('*:*:*')
}
},
methods: {
/** 查询角色列表 */
getList() {
@ -361,6 +381,12 @@ export default {
this.menuOptions = response.data
})
},
/** 查询租户列表 */
getTenantList() {
listTenant({ status: '0' }).then(response => {
this.tenantList = response.rows
})
},
//
getMenuAllCheckedKeys() {
//
@ -433,7 +459,8 @@ export default {
deptIds: [],
menuCheckStrictly: true,
deptCheckStrictly: true,
remark: undefined
remark: undefined,
tenantId: undefined
}
this.resetForm("form")
},
@ -501,6 +528,10 @@ export default {
handleAdd() {
this.reset()
this.getMenuTreeselect()
//
if (this.isSuperAdmin) {
this.getTenantList()
}
this.open = true
this.title = "添加角色"
},

View File

@ -0,0 +1,375 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="租户名称" prop="tenantName">
<el-input
v-model="queryParams.tenantName"
placeholder="请输入租户名称"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="租户编码" prop="tenantCode">
<el-input
v-model="queryParams.tenantCode"
placeholder="请输入租户编码"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="联系人" prop="contactName">
<el-input
v-model="queryParams.contactName"
placeholder="请输入联系人"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="联系电话" prop="contactPhone">
<el-input
v-model="queryParams.contactPhone"
placeholder="请输入联系电话"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="过期时间" prop="expireTime">
<el-date-picker clearable
v-model="queryParams.expireTime"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择过期时间">
</el-date-picker>
</el-form-item>
<el-form-item label="租户套餐" prop="packageId">
<el-select v-model="queryParams.packageId" placeholder="请选择套餐" clearable>
<el-option
v-for="pkg in packageList"
:key="pkg.packageId"
:label="pkg.packageName"
:value="pkg.packageId"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery"></el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery"></el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['link:tenant:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-edit"
size="mini"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['link:tenant:edit']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['link:tenant:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['link:tenant:export']"
>导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="tenantList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="租户ID" align="center" prop="tenantId" />
<el-table-column label="租户名称" align="center" prop="tenantName" />
<el-table-column label="租户编码" align="center" prop="tenantCode" />
<el-table-column label="联系人" align="center" prop="contactName" />
<el-table-column label="联系电话" align="center" prop="contactPhone" />
<el-table-column label="过期时间" align="center" prop="expireTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.expireTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="租户套餐" align="center" prop="packageId">
<template slot-scope="scope">
<span>{{ getPackageName(scope.row.packageId) }}</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['link:tenant:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['link:tenant:remove']"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改租户信息对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="租户名称" prop="tenantName">
<el-input v-model="form.tenantName" placeholder="请输入租户名称" />
</el-form-item>
<el-form-item label="租户编码" prop="tenantCode">
<el-input v-model="form.tenantCode" placeholder="请输入租户编码" />
</el-form-item>
<el-form-item label="联系人" prop="contactName">
<el-input v-model="form.contactName" placeholder="请输入联系人" />
</el-form-item>
<el-form-item label="联系电话" prop="contactPhone">
<el-input v-model="form.contactPhone" placeholder="请输入联系电话" />
</el-form-item>
<el-form-item label="过期时间" prop="expireTime">
<el-date-picker clearable
v-model="form.expireTime"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择过期时间">
</el-date-picker>
</el-form-item>
<el-form-item label="租户套餐" prop="packageId">
<el-select v-model="form.packageId" placeholder="请选择套餐" clearable style="width: 100%">
<el-option
v-for="pkg in packageList"
:key="pkg.packageId"
:label="pkg.packageName"
:value="pkg.packageId"
>
<span>{{ pkg.packageName }}</span>
<span style="color: #8492a6; font-size: 12px; margin-left: 10px">{{ pkg.remark }}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listTenant, getTenant, delTenant, addTenant, updateTenant } from "@/api/system/tenant"
import { listPackage } from "@/api/system/package"
export default {
name: "Tenant",
data() {
return {
//
loading: true,
//
ids: [],
//
single: true,
//
multiple: true,
//
showSearch: true,
//
total: 0,
//
tenantList: [],
//
packageList: [],
//
title: "",
//
open: false,
//
queryParams: {
pageNum: 1,
pageSize: 10,
tenantName: null,
tenantCode: null,
contactName: null,
contactPhone: null,
expireTime: null,
packageId: null,
status: null,
},
//
form: {},
//
rules: {
tenantName: [
{ required: true, message: "租户名称不能为空", trigger: "blur" }
],
tenantCode: [
{ required: true, message: "租户编码不能为空", trigger: "blur" }
],
}
}
},
created() {
this.getList()
this.getPackageList()
},
methods: {
/** 查询租户信息列表 */
getList() {
this.loading = true
listTenant(this.queryParams).then(response => {
this.tenantList = response.rows
this.total = response.total
this.loading = false
})
},
/** 查询套餐列表 */
getPackageList() {
listPackage({ status: '0' }).then(response => {
this.packageList = response.rows
})
},
/** 根据套餐ID获取套餐名称 */
getPackageName(packageId) {
if (!packageId) return '-'
const pkg = this.packageList.find(p => p.packageId === packageId)
return pkg ? pkg.packageName : '-'
},
//
cancel() {
this.open = false
this.reset()
},
//
reset() {
this.form = {
tenantId: null,
tenantName: null,
tenantCode: null,
contactName: null,
contactPhone: null,
expireTime: null,
packageId: null,
status: null,
delFlag: null,
createBy: null,
createTime: null,
updateBy: null,
updateTime: null,
remark: null
}
this.resetForm("form")
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm")
this.handleQuery()
},
//
handleSelectionChange(selection) {
this.ids = selection.map(item => item.tenantId)
this.single = selection.length!==1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.open = true
this.title = "添加租户信息"
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const tenantId = row.tenantId || this.ids
getTenant(tenantId).then(response => {
this.form = response.data
this.open = true
this.title = "修改租户信息"
})
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.tenantId != null) {
updateTenant(this.form).then(response => {
this.$modal.msgSuccess("修改成功")
this.open = false
this.getList()
})
} else {
addTenant(this.form).then(response => {
this.$modal.msgSuccess("新增成功")
this.open = false
this.getList()
})
}
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
const tenantIds = row.tenantId || this.ids
this.$modal.confirm('是否确认删除租户信息编号为"' + tenantIds + '"的数据项?').then(function() {
return delTenant(tenantIds)
}).then(() => {
this.getList()
this.$modal.msgSuccess("删除成功")
}).catch(() => {})
},
/** 导出按钮操作 */
handleExport() {
this.download('link/tenant/export', {
...this.queryParams
}, `tenant_${new Date().getTime()}.xlsx`)
}
}
}
</script>

View File

@ -165,6 +165,15 @@
</el-form-item>
</el-col>
</el-row>
<el-row v-if="isSuperAdmin">
<el-col :span="12">
<el-form-item label="所属租户" prop="tenantId">
<el-select v-model="form.tenantId" placeholder="请选择租户" clearable>
<el-option v-for="item in tenantOptions" :key="item.tenantId" :label="item.tenantName" :value="item.tenantId" :disabled="item.status == '1'"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="备注">
@ -246,6 +255,8 @@ export default {
postOptions: [],
//
roleOptions: [],
//
tenantOptions: [],
//
form: {},
defaultProps: {
@ -317,6 +328,12 @@ export default {
}
}
},
computed: {
//
isSuperAdmin() {
return this.$store.getters.id === 1
}
},
watch: {
//
deptName(val) {
@ -400,7 +417,8 @@ export default {
status: "0",
remark: undefined,
postIds: [],
roleIds: []
roleIds: [],
tenantId: undefined
}
this.resetForm("form")
},
@ -442,6 +460,9 @@ export default {
getUser().then(response => {
this.postOptions = response.posts
this.roleOptions = response.roles
if (response.tenants) {
this.tenantOptions = response.tenants
}
this.open = true
this.title = "添加用户"
this.form.password = this.initPassword
@ -455,6 +476,9 @@ export default {
this.form = response.data
this.postOptions = response.posts
this.roleOptions = response.roles
if (response.tenants) {
this.tenantOptions = response.tenants
}
this.$set(this.form, "postIds", response.postIds)
this.$set(this.form, "roleIds", response.roleIds)
this.open = true

View File

@ -7,7 +7,7 @@ function resolve(dir) {
const CompressionPlugin = require('compression-webpack-plugin')
const name = process.env.VUE_APP_TITLE || '若依管理系统' // 网页标题
const name = process.env.VUE_APP_TITLE || 'Hair-Link' // 网页标题
const baseUrl = 'http://localhost:8080' // 后端接口
@ -19,7 +19,7 @@ const port = process.env.port || process.env.npm_config_port || 80 // 端口
module.exports = {
// 部署生产环境和开发环境下的URL。
// 默认情况下Vue CLI 会假设你的应用是被部署在一个域名的根路径上
// 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。
// 例如 https://www.example.com/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.example.com/admin/,则设置 baseUrl 为 /admin/。
publicPath: process.env.NODE_ENV === "production" ? "/" : "/",
// 在npm run build 或 yarn build 时 生成文件的目录名称要和baseUrl的生产环境路径一致默认dist
outputDir: 'dist',
@ -65,7 +65,7 @@ module.exports = {
}
},
plugins: [
// http://doc.ruoyi.vip/ruoyi-vue/other/faq.html#使用gzip解压缩静态文件
// 使用gzip解压缩静态文件
new CompressionPlugin({
cache: false, // 不启用文件缓存
test: /\.(js|css|html|jpe?g|png|gif|svg)?$/i, // 压缩文件格式

View File

@ -0,0 +1,22 @@
-- ============================================
-- Bug 修复脚本 - 2025-12-20
-- ============================================
-- Bug 1: 修复基础套餐菜单配置(添加字典管理菜单)
-- 原因基础套餐缺少字典管理菜单ID=105及其子菜单权限
-- 1. 添加字典管理主菜单
INSERT IGNORE INTO sys_package_menu (package_id, menu_id) VALUES (1, 105);
-- 2. 添加字典管理子菜单权限1025-1029
INSERT IGNORE INTO sys_package_menu (package_id, menu_id) VALUES (1, 1025); -- 字典查询
INSERT IGNORE INTO sys_package_menu (package_id, menu_id) VALUES (1, 1026); -- 字典新增
INSERT IGNORE INTO sys_package_menu (package_id, menu_id) VALUES (1, 1027); -- 字典修改
INSERT IGNORE INTO sys_package_menu (package_id, menu_id) VALUES (1, 1028); -- 字典删除
INSERT IGNORE INTO sys_package_menu (package_id, menu_id) VALUES (1, 1029); -- 字典导出
-- 验证修复结果
SELECT pm.package_id, pm.menu_id, m.menu_name
FROM sys_package_menu pm
LEFT JOIN sys_menu m ON pm.menu_id = m.menu_id
WHERE pm.package_id = 1 AND pm.menu_id IN (105, 1025, 1026, 1027, 1028, 1029);

View File

@ -0,0 +1,23 @@
-- =============================================
-- 字典表混合模式租户隔离迁移脚本
-- 执行前请备份数据库
-- =============================================
-- 1. 为 sys_dict_type 添加 tenant_id 字段
ALTER TABLE sys_dict_type
ADD COLUMN tenant_id BIGINT(20) NOT NULL DEFAULT 0 COMMENT '租户ID0=系统级)' AFTER dict_id;
-- 2. 为 sys_dict_data 添加 tenant_id 字段
ALTER TABLE sys_dict_data
ADD COLUMN tenant_id BIGINT(20) NOT NULL DEFAULT 0 COMMENT '租户ID0=系统级)' AFTER dict_code;
-- 3. 删除旧的唯一约束
ALTER TABLE sys_dict_type DROP INDEX dict_type;
-- 4. 创建新的复合唯一约束(包含 tenant_id
ALTER TABLE sys_dict_type ADD UNIQUE INDEX uk_tenant_dict_type (tenant_id, dict_type);
ALTER TABLE sys_dict_data ADD UNIQUE INDEX uk_tenant_dict_data (tenant_id, dict_type, dict_value);
-- 5. 添加索引优化查询
CREATE INDEX idx_dict_type_tenant ON sys_dict_type(tenant_id);
CREATE INDEX idx_dict_data_tenant ON sys_dict_data(tenant_id);

View File

@ -0,0 +1,312 @@
-- ============================
-- HairLink Phase 1 数据库初始化脚本
-- 版本1.0.0
-- 创建日期2025-12-19
-- 说明包含P1阶段核心业务表顾客管理、订单管理、基础配置
-- 表数量6个核心业务表
-- 依赖:需先执行 ry_20250522.sql若依框架基础表
-- ============================
-- ----------------------------
-- 1、顾客档案表
-- ----------------------------
drop table if exists hl_customer;
create table hl_customer (
customer_id bigint(20) not null auto_increment comment '顾客ID',
tenant_id bigint(20) default null comment '租户ID',
dept_id bigint(20) default null comment '归属门店',
-- [P1] 基础信息
customer_name varchar(50) default '散客' comment '姓名',
phone varchar(11) default null comment '手机号(兼容散客为空)',
gender char(1) default '2' comment '性别 0男 1女 2未知',
-- [P2] 资产快照(预留)
member_level char(1) default '0' comment '会员等级 0普通 1银卡 2金卡',
balance decimal(10,2) default 0.00 comment '储值余额',
-- [P3] C端连接预留
wx_openid varchar(64) default null comment '小程序OpenID',
points int(11) default 0 comment '积分',
-- 标准字段
create_by varchar(64) default '',
create_time datetime default null,
update_by varchar(64) default '',
update_time datetime default null,
remark varchar(500) default null,
primary key (customer_id),
index idx_tenant (tenant_id),
index idx_phone (phone),
index idx_openid (wx_openid)
) engine=innodb auto_increment=100 comment = '顾客档案表';
-- ----------------------------
-- 2、会员权益卡表
-- ----------------------------
drop table if exists hl_member_card;
create table hl_member_card (
card_id bigint(20) not null auto_increment comment '卡ID',
tenant_id bigint(20) default null comment '租户ID',
customer_id bigint(20) not null comment '顾客ID',
-- [P1] 计次卡逻辑
card_name varchar(100) default '' comment '卡名称',
total_count int(11) default 0 comment '总次数',
remain_count int(11) default 0 comment '剩余次数',
-- [P2] 储值卡/期限卡逻辑(预留)
card_type char(1) default '0' comment '卡类型 0计次 1储值 2期限',
total_amount decimal(10,2) default 0.00 comment '总充值金额',
remain_amount decimal(10,2) default 0.00 comment '剩余金额',
gift_amount decimal(10,2) default 0.00 comment '赠送金额',
expire_time datetime default null comment '过期时间',
status char(1) default '0' comment '状态 0正常 1停用 2耗尽',
create_by varchar(64) default '',
create_time datetime default null,
update_by varchar(64) default '',
update_time datetime default null,
primary key (card_id),
index idx_tenant (tenant_id),
index idx_customer (customer_id)
) engine=innodb auto_increment=100 comment = '会员权益卡表';
-- ----------------------------
-- 3、服务项目表
-- ----------------------------
drop table if exists hl_service;
create table hl_service (
service_id bigint(20) not null auto_increment comment '服务ID',
tenant_id bigint(20) default null comment '租户ID',
service_name varchar(100) not null comment '服务名称',
service_code varchar(50) default null comment '服务编码',
price decimal(10,2) default 0.00 comment '标准价格',
duration int(11) default 30 comment '服务时长(分钟)',
category varchar(50) default null comment '服务分类',
status char(1) default '0' comment '状态 0正常 1停用',
sort_order int(11) default 0 comment '排序',
create_by varchar(64) default '',
create_time datetime default null,
update_by varchar(64) default '',
update_time datetime default null,
remark varchar(500) default null,
primary key (service_id),
index idx_tenant (tenant_id),
unique key uk_code (service_code)
) engine=innodb auto_increment=100 comment = '服务项目表';
-- ----------------------------
-- 4、员工扩展表
-- ----------------------------
drop table if exists hl_staff;
create table hl_staff (
staff_id bigint(20) not null auto_increment comment '员工ID',
user_id bigint(20) not null comment '关联sys_user用户ID',
tenant_id bigint(20) default null comment '租户ID',
dept_id bigint(20) default null comment '所属门店',
staff_name varchar(50) not null comment '员工姓名',
phone varchar(11) default null comment '手机号',
position varchar(50) default '理发师' comment '职位',
level char(1) default '1' comment '级别 1初级 2中级 3高级',
-- [P2] 业绩相关(预留)
commission_rate decimal(5,2) default 30.00 comment '提成比例',
base_salary decimal(10,2) default 0.00 comment '底薪',
status char(1) default '0' comment '状态 0在职 1离职',
entry_date date default null comment '入职日期',
create_by varchar(64) default '',
create_time datetime default null,
update_by varchar(64) default '',
update_time datetime default null,
primary key (staff_id),
index idx_user (user_id),
index idx_dept (dept_id)
) engine=innodb auto_increment=100 comment = '员工扩展表';
-- ----------------------------
-- 5、业务订单表
-- ----------------------------
drop table if exists hl_order;
create table hl_order (
order_id bigint(20) not null auto_increment comment '订单ID',
order_no varchar(32) not null comment '订单编号',
tenant_id bigint(20) default null comment '租户ID',
dept_id bigint(20) default null comment '门店ID',
customer_id bigint(20) not null comment '顾客ID',
-- [P1] 基础交易
pay_amount decimal(10,2) default 0.00 comment '实收/实扣金额',
pay_method char(1) default '0' comment '支付方式 0现金 1微信 2卡扣次',
pay_status char(1) default '1' comment '支付状态 1已付',
-- [P2] 复杂交易(预留)
order_type char(1) default '0' comment '订单类型 0服务 1办卡 2零售',
total_amount decimal(10,2) default 0.00 comment '原价总额',
discount_amount decimal(10,2) default 0.00 comment '优惠金额',
-- [P3] 在线支付(预留)
transaction_id varchar(64) default null comment '微信支付流水号',
create_by varchar(64) default '',
create_time datetime default null,
primary key (order_id),
unique key uk_order_no (order_no),
index idx_tenant (tenant_id),
index idx_customer (customer_id),
index idx_create_time (create_time)
) engine=innodb auto_increment=1000 comment = '业务订单表';
-- ----------------------------
-- 6、订单明细表
-- ----------------------------
drop table if exists hl_order_detail;
create table hl_order_detail (
detail_id bigint(20) not null auto_increment comment '明细ID',
order_id bigint(20) not null comment '订单ID',
-- [P1] 核心内容
item_id bigint(20) not null comment '项目ID',
item_name varchar(100) not null comment '项目名称',
price decimal(10,2) default 0.00 comment '单价',
quantity int(11) default 1 comment '数量',
staff_id bigint(20) default null comment '主理发师ID',
deduct_card_id bigint(20) default null comment '关联扣次卡ID',
-- [P2] 类型扩展(预留)
item_type char(1) default '0' comment '项目类型 0服务 1产品',
is_stock_deducted char(1) default '0' comment '是否已扣库 0否 1是',
primary key (detail_id),
index idx_order (order_id)
) engine=innodb auto_increment=1000 comment = '订单明细表';
-- ----------------------------
-- 外键约束
-- ----------------------------
-- 会员卡 -> 顾客
ALTER TABLE hl_member_card
ADD CONSTRAINT fk_card_customer
FOREIGN KEY (customer_id) REFERENCES hl_customer(customer_id);
-- 订单 -> 顾客
ALTER TABLE hl_order
ADD CONSTRAINT fk_order_customer
FOREIGN KEY (customer_id) REFERENCES hl_customer(customer_id);
-- 订单明细 -> 订单(级联删除)
ALTER TABLE hl_order_detail
ADD CONSTRAINT fk_detail_order
FOREIGN KEY (order_id) REFERENCES hl_order(order_id) ON DELETE CASCADE;
-- 订单明细 -> 会员卡(可选)
ALTER TABLE hl_order_detail
ADD CONSTRAINT fk_detail_card
FOREIGN KEY (deduct_card_id) REFERENCES hl_member_card(card_id);
-- ----------------------------
-- 初始化-服务项目表数据
-- ----------------------------
INSERT INTO hl_service (service_name, service_code, price, duration, category, status, sort_order, create_by, create_time) VALUES
('男士剪发', 'MAN_CUT', 38.00, 30, '基础服务', '0', 1, 'admin', sysdate()),
('女士剪发', 'WOMAN_CUT', 58.00, 45, '基础服务', '0', 2, 'admin', sysdate()),
('烫发', 'PERM', 288.00, 120, '造型服务', '0', 3, 'admin', sysdate()),
('染发', 'DYE', 268.00, 120, '造型服务', '0', 4, 'admin', sysdate()),
('洗剪吹', 'WASH_CUT_BLOW', 68.00, 60, '套餐服务', '0', 5, 'admin', sysdate());
-- ----------------------------
-- 测试数据-顾客档案
-- ----------------------------
INSERT INTO hl_customer (customer_name, phone, gender, member_level, balance, create_by, create_time, remark) VALUES
('张三', '13800138001', '0', '0', 0.00, 'admin', sysdate(), '测试会员顾客1'),
('李四', '13800138002', '1', '0', 0.00, 'admin', sysdate(), '测试会员顾客2'),
('散客', NULL, '2', '0', 0.00, 'admin', sysdate(), '测试散客');
-- ----------------------------
-- 测试数据-员工扩展表
-- ----------------------------
INSERT INTO hl_staff (user_id, staff_name, phone, position, level, commission_rate, status, entry_date, create_by, create_time) VALUES
(1, '王师傅', '13900139001', '高级理发师', '3', 30.00, '0', '2024-01-01', 'admin', sysdate()),
(2, '赵师傅', '13900139002', '理发师', '2', 30.00, '0', '2024-06-01', 'admin', sysdate());
-- ----------------------------
-- 测试数据-会员卡
-- ----------------------------
-- 为张三创建一张10次理发卡
INSERT INTO hl_member_card (customer_id, card_name, card_type, total_count, remain_count, status, create_by, create_time) VALUES
(100, '理发计次卡', '0', 10, 10, '0', 'admin', sysdate());
-- 为李四创建一张20次理发卡
INSERT INTO hl_member_card (customer_id, card_name, card_type, total_count, remain_count, status, create_by, create_time) VALUES
(101, '理发计次卡', '0', 20, 19, '0', 'admin', sysdate());
-- ----------------------------
-- 测试数据-订单及明细
-- ----------------------------
-- 订单1散客现金支付剪发
INSERT INTO hl_order (order_no, customer_id, pay_amount, pay_method, pay_status, order_type, total_amount, create_by, create_time) VALUES
('20251219000001', 102, 38.00, '0', '1', '0', 38.00, 'admin', sysdate());
INSERT INTO hl_order_detail (order_id, item_id, item_name, price, quantity, staff_id, item_type) VALUES
(1000, 100, '男士剪发', 38.00, 1, 100, '0');
-- 订单2李四会员卡扣次烫发
INSERT INTO hl_order (order_no, customer_id, pay_amount, pay_method, pay_status, order_type, total_amount, create_by, create_time) VALUES
('20251219000002', 101, 288.00, '2', '1', '0', 288.00, 'admin', sysdate());
INSERT INTO hl_order_detail (order_id, item_id, item_name, price, quantity, staff_id, deduct_card_id, item_type) VALUES
(1001, 102, '烫发', 288.00, 1, 100, 101, '0');
-- ----------------------------
-- 初始化完成提示
-- ----------------------------
-- ============================
-- HairLink Phase 1 数据库初始化完成!
--
-- 已创建表:
-- 1. hl_customer (顾客档案表)
-- 2. hl_member_card (会员权益卡表)
-- 3. hl_service (服务项目表)
-- 4. hl_staff (员工扩展表)
-- 5. hl_order (业务订单表)
-- 6. hl_order_detail (订单明细表)
--
-- 已添加外键约束4个
--
-- 已初始化数据:
-- - 服务项目5条
-- - 测试顾客3条
-- - 测试员工2条
-- - 测试会员卡2条
-- - 测试订单2条含订单明细
--
-- 下一步:
-- 1. 在MySQL中执行本脚本
-- 2. 验证表结构和数据
-- 3. 开始后端代码开发
-- ============================

View File

@ -0,0 +1,792 @@
-- ============================
-- 若依框架基础表 + 多租户支持
-- 版本1.0.0_with_tenant
-- 创建日期2025-12-19
-- 说明:在若依框架基础表创建时就包含多租户支持,无需后续ALTER TABLE
-- 改动说明:
-- 1. 新增 sys_tenant 租户管理表
-- 2. 为 sys_user, sys_dept, sys_role, sys_post, sys_notice, sys_config 添加 tenant_id 字段
-- 3. 为 tenant_id 字段创建索引
-- 4. 初始化数据设置默认租户ID
-- ============================
-- ----------------------------
-- 0、租户信息表 (新增)
-- ----------------------------
drop table if exists sys_tenant;
create table sys_tenant (
tenant_id bigint(20) not null auto_increment comment '租户ID',
tenant_name varchar(50) not null comment '租户名称',
tenant_code varchar(50) not null comment '租户编码(唯一标识)',
contact_name varchar(50) default null comment '联系人',
contact_phone varchar(20) default null comment '联系电话',
expire_time datetime default null comment '过期时间NULL表示永久',
package_id bigint(20) default null comment '套餐ID预留',
status char(1) default '0' comment '状态0正常 1停用',
del_flag char(1) default '0' comment '删除标志0存在 2删除',
create_by varchar(64) default '' comment '创建者',
create_time datetime default null comment '创建时间',
update_by varchar(64) default '' comment '更新者',
update_time datetime default null comment '更新时间',
remark varchar(500) default null comment '备注',
primary key (tenant_id),
unique key uk_tenant_code (tenant_code)
) engine=innodb auto_increment=1000 comment = '租户信息表';
-- ----------------------------
-- 初始化-租户信息表数据
-- ----------------------------
insert into sys_tenant (tenant_name, tenant_code, contact_name, contact_phone, status, create_by, create_time, remark) values
('默认租户', 'DEFAULT', '系统管理员', '13800138000', '0', 'admin', sysdate(), '系统默认租户,现有数据归属此租户'),
('测试租户A', 'TENANT_A', '张三', '13800138001', '0', 'admin', sysdate(), '测试租户A,用于多租户隔离测试'),
('测试租户B', 'TENANT_B', '李四', '13800138002', '0', 'admin', sysdate(), '测试租户B,用于跨租户访问测试');
-- ----------------------------
-- 1、部门表 (已添加 tenant_id)
-- ----------------------------
drop table if exists sys_dept;
create table sys_dept (
dept_id bigint(20) not null auto_increment comment '部门id',
tenant_id bigint(20) default null comment '租户ID',
parent_id bigint(20) default 0 comment '父部门id',
ancestors varchar(50) default '' comment '祖级列表',
dept_name varchar(30) default '' comment '部门名称',
order_num int(4) default 0 comment '显示顺序',
leader varchar(20) default null comment '负责人',
phone varchar(11) default null comment '联系电话',
email varchar(50) default null comment '邮箱',
status char(1) default '0' comment '部门状态0正常 1停用',
del_flag char(1) default '0' comment '删除标志0代表存在 2代表删除',
create_by varchar(64) default '' comment '创建者',
create_time datetime comment '创建时间',
update_by varchar(64) default '' comment '更新者',
update_time datetime comment '更新时间',
primary key (dept_id),
index idx_tenant_id (tenant_id)
) engine=innodb auto_increment=200 comment = '部门表';
-- ----------------------------
-- 初始化-部门表数据
-- ----------------------------
set @default_tenant_id = (select tenant_id from sys_tenant where tenant_code = 'DEFAULT');
insert into sys_dept values(100, @default_tenant_id, 0, '0', '若依科技', 0, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null);
insert into sys_dept values(101, @default_tenant_id, 100, '0,100', '深圳总公司', 1, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null);
insert into sys_dept values(102, @default_tenant_id, 100, '0,100', '长沙分公司', 2, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null);
insert into sys_dept values(103, @default_tenant_id, 101, '0,100,101', '研发部门', 1, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null);
insert into sys_dept values(104, @default_tenant_id, 101, '0,100,101', '市场部门', 2, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null);
insert into sys_dept values(105, @default_tenant_id, 101, '0,100,101', '测试部门', 3, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null);
insert into sys_dept values(106, @default_tenant_id, 101, '0,100,101', '财务部门', 4, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null);
insert into sys_dept values(107, @default_tenant_id, 101, '0,100,101', '运维部门', 5, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null);
insert into sys_dept values(108, @default_tenant_id, 102, '0,100,102', '市场部门', 1, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null);
insert into sys_dept values(109, @default_tenant_id, 102, '0,100,102', '财务部门', 2, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null);
-- ----------------------------
-- 2、用户信息表 (已添加 tenant_id)
-- ----------------------------
drop table if exists sys_user;
create table sys_user (
user_id bigint(20) not null auto_increment comment '用户ID',
tenant_id bigint(20) default null comment '租户ID',
dept_id bigint(20) default null comment '部门ID',
user_name varchar(30) not null comment '用户账号',
nick_name varchar(30) not null comment '用户昵称',
user_type varchar(2) default '00' comment '用户类型00系统用户',
email varchar(50) default '' comment '用户邮箱',
phonenumber varchar(11) default '' comment '手机号码',
sex char(1) default '0' comment '用户性别0男 1女 2未知',
avatar varchar(100) default '' comment '头像地址',
password varchar(100) default '' comment '密码',
status char(1) default '0' comment '账号状态0正常 1停用',
del_flag char(1) default '0' comment '删除标志0代表存在 2代表删除',
login_ip varchar(128) default '' comment '最后登录IP',
login_date datetime comment '最后登录时间',
pwd_update_date datetime comment '密码最后更新时间',
create_by varchar(64) default '' comment '创建者',
create_time datetime comment '创建时间',
update_by varchar(64) default '' comment '更新者',
update_time datetime comment '更新时间',
remark varchar(500) default null comment '备注',
primary key (user_id),
index idx_tenant_id (tenant_id),
index idx_tenant_user (tenant_id, user_name)
) engine=innodb auto_increment=100 comment = '用户信息表';
-- ----------------------------
-- 初始化-用户信息表数据
-- ----------------------------
insert into sys_user values(1, @default_tenant_id, 103, 'admin', '若依', '00', 'ry@163.com', '15888888888', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', sysdate(), sysdate(), 'admin', sysdate(), '', null, '管理员');
insert into sys_user values(2, @default_tenant_id, 105, 'ry', '若依', '00', 'ry@qq.com', '15666666666', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', sysdate(), sysdate(), 'admin', sysdate(), '', null, '测试员');
-- ----------------------------
-- 3、岗位信息表 (已添加 tenant_id)
-- ----------------------------
drop table if exists sys_post;
create table sys_post
(
post_id bigint(20) not null auto_increment comment '岗位ID',
tenant_id bigint(20) default null comment '租户ID',
post_code varchar(64) not null comment '岗位编码',
post_name varchar(50) not null comment '岗位名称',
post_sort int(4) not null comment '显示顺序',
status char(1) not null comment '状态0正常 1停用',
create_by varchar(64) default '' comment '创建者',
create_time datetime comment '创建时间',
update_by varchar(64) default '' comment '更新者',
update_time datetime comment '更新时间',
remark varchar(500) default null comment '备注',
primary key (post_id),
index idx_tenant_id (tenant_id)
) engine=innodb comment = '岗位信息表';
-- ----------------------------
-- 初始化-岗位信息表数据
-- ----------------------------
insert into sys_post values(1, @default_tenant_id, 'ceo', '董事长', 1, '0', 'admin', sysdate(), '', null, '');
insert into sys_post values(2, @default_tenant_id, 'se', '项目经理', 2, '0', 'admin', sysdate(), '', null, '');
insert into sys_post values(3, @default_tenant_id, 'hr', '人力资源', 3, '0', 'admin', sysdate(), '', null, '');
insert into sys_post values(4, @default_tenant_id, 'user', '普通员工', 4, '0', 'admin', sysdate(), '', null, '');
-- ----------------------------
-- 4、角色信息表 (已添加 tenant_id)
-- ----------------------------
drop table if exists sys_role;
create table sys_role (
role_id bigint(20) not null auto_increment comment '角色ID',
tenant_id bigint(20) default null comment '租户ID',
role_name varchar(30) not null comment '角色名称',
role_key varchar(100) not null comment '角色权限字符串',
role_sort int(4) not null comment '显示顺序',
data_scope char(1) default '1' comment '数据范围1全部数据权限 2自定数据权限 3本部门数据权限 4本部门及以下数据权限',
menu_check_strictly tinyint(1) default 1 comment '菜单树选择项是否关联显示',
dept_check_strictly tinyint(1) default 1 comment '部门树选择项是否关联显示',
status char(1) not null comment '角色状态0正常 1停用',
del_flag char(1) default '0' comment '删除标志0代表存在 2代表删除',
create_by varchar(64) default '' comment '创建者',
create_time datetime comment '创建时间',
update_by varchar(64) default '' comment '更新者',
update_time datetime comment '更新时间',
remark varchar(500) default null comment '备注',
primary key (role_id),
index idx_tenant_id (tenant_id)
) engine=innodb auto_increment=100 comment = '角色信息表';
-- ----------------------------
-- 初始化-角色信息表数据
-- ----------------------------
insert into sys_role values('1', @default_tenant_id, '超级管理员', 'admin', 1, 1, 1, 1, '0', '0', 'admin', sysdate(), '', null, '超级管理员');
insert into sys_role values('2', @default_tenant_id, '普通角色', 'common', 2, 2, 1, 1, '0', '0', 'admin', sysdate(), '', null, '普通角色');
-- ----------------------------
-- 5、菜单权限表
-- ----------------------------
drop table if exists sys_menu;
create table sys_menu (
menu_id bigint(20) not null auto_increment comment '菜单ID',
menu_name varchar(50) not null comment '菜单名称',
parent_id bigint(20) default 0 comment '父菜单ID',
order_num int(4) default 0 comment '显示顺序',
path varchar(200) default '' comment '路由地址',
component varchar(255) default null comment '组件路径',
query varchar(255) default null comment '路由参数',
route_name varchar(50) default '' comment '路由名称',
is_frame int(1) default 1 comment '是否为外链0是 1否',
is_cache int(1) default 0 comment '是否缓存0缓存 1不缓存',
menu_type char(1) default '' comment '菜单类型M目录 C菜单 F按钮',
visible char(1) default 0 comment '菜单状态0显示 1隐藏',
status char(1) default 0 comment '菜单状态0正常 1停用',
perms varchar(100) default null comment '权限标识',
icon varchar(100) default '#' comment '菜单图标',
create_by varchar(64) default '' comment '创建者',
create_time datetime comment '创建时间',
update_by varchar(64) default '' comment '更新者',
update_time datetime comment '更新时间',
remark varchar(500) default '' comment '备注',
primary key (menu_id)
) engine=innodb auto_increment=2000 comment = '菜单权限表';
-- ----------------------------
-- 初始化-菜单信息表数据
-- ----------------------------
-- 一级菜单
insert into sys_menu values('1', '系统管理', '0', '1', 'system', null, '', '', 1, 0, 'M', '0', '0', '', 'system', 'admin', sysdate(), '', null, '系统管理目录');
insert into sys_menu values('2', '系统监控', '0', '2', 'monitor', null, '', '', 1, 0, 'M', '0', '0', '', 'monitor', 'admin', sysdate(), '', null, '系统监控目录');
insert into sys_menu values('3', '系统工具', '0', '3', 'tool', null, '', '', 1, 0, 'M', '0', '0', '', 'tool', 'admin', sysdate(), '', null, '系统工具目录');
insert into sys_menu values('4', '若依官网', '0', '4', 'http://ruoyi.vip', null, '', '', 0, 0, 'M', '0', '0', '', 'guide', 'admin', sysdate(), '', null, '若依官网地址');
-- 二级菜单
insert into sys_menu values('100', '用户管理', '1', '1', 'user', 'system/user/index', '', '', 1, 0, 'C', '0', '0', 'system:user:list', 'user', 'admin', sysdate(), '', null, '用户管理菜单');
insert into sys_menu values('101', '角色管理', '1', '2', 'role', 'system/role/index', '', '', 1, 0, 'C', '0', '0', 'system:role:list', 'peoples', 'admin', sysdate(), '', null, '角色管理菜单');
insert into sys_menu values('102', '菜单管理', '1', '3', 'menu', 'system/menu/index', '', '', 1, 0, 'C', '0', '0', 'system:menu:list', 'tree-table', 'admin', sysdate(), '', null, '菜单管理菜单');
insert into sys_menu values('103', '部门管理', '1', '4', 'dept', 'system/dept/index', '', '', 1, 0, 'C', '0', '0', 'system:dept:list', 'tree', 'admin', sysdate(), '', null, '部门管理菜单');
insert into sys_menu values('104', '岗位管理', '1', '5', 'post', 'system/post/index', '', '', 1, 0, 'C', '0', '0', 'system:post:list', 'post', 'admin', sysdate(), '', null, '岗位管理菜单');
insert into sys_menu values('105', '字典管理', '1', '6', 'dict', 'system/dict/index', '', '', 1, 0, 'C', '0', '0', 'system:dict:list', 'dict', 'admin', sysdate(), '', null, '字典管理菜单');
insert into sys_menu values('106', '参数设置', '1', '7', 'config', 'system/config/index', '', '', 1, 0, 'C', '0', '0', 'system:config:list', 'edit', 'admin', sysdate(), '', null, '参数设置菜单');
insert into sys_menu values('107', '通知公告', '1', '8', 'notice', 'system/notice/index', '', '', 1, 0, 'C', '0', '0', 'system:notice:list', 'message', 'admin', sysdate(), '', null, '通知公告菜单');
insert into sys_menu values('108', '日志管理', '1', '9', 'log', '', '', '', 1, 0, 'M', '0', '0', '', 'log', 'admin', sysdate(), '', null, '日志管理菜单');
insert into sys_menu values('109', '在线用户', '2', '1', 'online', 'monitor/online/index', '', '', 1, 0, 'C', '0', '0', 'monitor:online:list', 'online', 'admin', sysdate(), '', null, '在线用户菜单');
insert into sys_menu values('110', '定时任务', '2', '2', 'job', 'monitor/job/index', '', '', 1, 0, 'C', '0', '0', 'monitor:job:list', 'job', 'admin', sysdate(), '', null, '定时任务菜单');
insert into sys_menu values('111', '数据监控', '2', '3', 'druid', 'monitor/druid/index', '', '', 1, 0, 'C', '0', '0', 'monitor:druid:list', 'druid', 'admin', sysdate(), '', null, '数据监控菜单');
insert into sys_menu values('112', '服务监控', '2', '4', 'server', 'monitor/server/index', '', '', 1, 0, 'C', '0', '0', 'monitor:server:list', 'server', 'admin', sysdate(), '', null, '服务监控菜单');
insert into sys_menu values('113', '缓存监控', '2', '5', 'cache', 'monitor/cache/index', '', '', 1, 0, 'C', '0', '0', 'monitor:cache:list', 'redis', 'admin', sysdate(), '', null, '缓存监控菜单');
insert into sys_menu values('114', '缓存列表', '2', '6', 'cacheList', 'monitor/cache/list', '', '', 1, 0, 'C', '0', '0', 'monitor:cache:list', 'redis-list', 'admin', sysdate(), '', null, '缓存列表菜单');
insert into sys_menu values('115', '表单构建', '3', '1', 'build', 'tool/build/index', '', '', 1, 0, 'C', '0', '0', 'tool:build:list', 'build', 'admin', sysdate(), '', null, '表单构建菜单');
insert into sys_menu values('116', '代码生成', '3', '2', 'gen', 'tool/gen/index', '', '', 1, 0, 'C', '0', '0', 'tool:gen:list', 'code', 'admin', sysdate(), '', null, '代码生成菜单');
insert into sys_menu values('117', '系统接口', '3', '3', 'swagger', 'tool/swagger/index', '', '', 1, 0, 'C', '0', '0', 'tool:swagger:list', 'swagger', 'admin', sysdate(), '', null, '系统接口菜单');
-- 三级菜单
insert into sys_menu values('500', '操作日志', '108', '1', 'operlog', 'monitor/operlog/index', '', '', 1, 0, 'C', '0', '0', 'monitor:operlog:list', 'form', 'admin', sysdate(), '', null, '操作日志菜单');
insert into sys_menu values('501', '登录日志', '108', '2', 'logininfor', 'monitor/logininfor/index', '', '', 1, 0, 'C', '0', '0', 'monitor:logininfor:list', 'logininfor', 'admin', sysdate(), '', null, '登录日志菜单');
-- 用户管理按钮
insert into sys_menu values('1000', '用户查询', '100', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:query', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1001', '用户新增', '100', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:add', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1002', '用户修改', '100', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:edit', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1003', '用户删除', '100', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:remove', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1004', '用户导出', '100', '5', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:export', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1005', '用户导入', '100', '6', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:import', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1006', '重置密码', '100', '7', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:resetPwd', '#', 'admin', sysdate(), '', null, '');
-- 角色管理按钮
insert into sys_menu values('1007', '角色查询', '101', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:query', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1008', '角色新增', '101', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:add', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1009', '角色修改', '101', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:edit', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1010', '角色删除', '101', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:remove', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1011', '角色导出', '101', '5', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:export', '#', 'admin', sysdate(), '', null, '');
-- 菜单管理按钮
insert into sys_menu values('1012', '菜单查询', '102', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:menu:query', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1013', '菜单新增', '102', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:menu:add', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1014', '菜单修改', '102', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:menu:edit', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1015', '菜单删除', '102', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:menu:remove', '#', 'admin', sysdate(), '', null, '');
-- 部门管理按钮
insert into sys_menu values('1016', '部门查询', '103', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:dept:query', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1017', '部门新增', '103', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:dept:add', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1018', '部门修改', '103', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:dept:edit', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1019', '部门删除', '103', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:dept:remove', '#', 'admin', sysdate(), '', null, '');
-- 岗位管理按钮
insert into sys_menu values('1020', '岗位查询', '104', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:query', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1021', '岗位新增', '104', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:add', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1022', '岗位修改', '104', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:edit', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1023', '岗位删除', '104', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:remove', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1024', '岗位导出', '104', '5', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:export', '#', 'admin', sysdate(), '', null, '');
-- 字典管理按钮
insert into sys_menu values('1025', '字典查询', '105', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:query', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1026', '字典新增', '105', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:add', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1027', '字典修改', '105', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:edit', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1028', '字典删除', '105', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:remove', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1029', '字典导出', '105', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:export', '#', 'admin', sysdate(), '', null, '');
-- 参数设置按钮
insert into sys_menu values('1030', '参数查询', '106', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:query', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1031', '参数新增', '106', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:add', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1032', '参数修改', '106', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:edit', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1033', '参数删除', '106', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:remove', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1034', '参数导出', '106', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:export', '#', 'admin', sysdate(), '', null, '');
-- 通知公告按钮
insert into sys_menu values('1035', '公告查询', '107', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:notice:query', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1036', '公告新增', '107', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:notice:add', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1037', '公告修改', '107', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:notice:edit', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1038', '公告删除', '107', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:notice:remove', '#', 'admin', sysdate(), '', null, '');
-- 操作日志按钮
insert into sys_menu values('1039', '操作查询', '500', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:operlog:query', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1040', '操作删除', '500', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:operlog:remove', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1041', '日志导出', '500', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:operlog:export', '#', 'admin', sysdate(), '', null, '');
-- 登录日志按钮
insert into sys_menu values('1042', '登录查询', '501', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:query', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1043', '登录删除', '501', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:remove', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1044', '日志导出', '501', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:export', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1045', '账户解锁', '501', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:unlock', '#', 'admin', sysdate(), '', null, '');
-- 在线用户按钮
insert into sys_menu values('1046', '在线查询', '109', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:online:query', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1047', '批量强退', '109', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:online:batchLogout', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1048', '单条强退', '109', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:online:forceLogout', '#', 'admin', sysdate(), '', null, '');
-- 定时任务按钮
insert into sys_menu values('1049', '任务查询', '110', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:query', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1050', '任务新增', '110', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:add', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1051', '任务修改', '110', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:edit', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1052', '任务删除', '110', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:remove', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1053', '状态修改', '110', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:changeStatus', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1054', '任务导出', '110', '6', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:export', '#', 'admin', sysdate(), '', null, '');
-- 代码生成按钮
insert into sys_menu values('1055', '生成查询', '116', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:query', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1056', '生成修改', '116', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:edit', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1057', '生成删除', '116', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:remove', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1058', '导入代码', '116', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:import', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1059', '预览代码', '116', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:preview', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1060', '生成代码', '116', '6', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:code', '#', 'admin', sysdate(), '', null, '');
-- ----------------------------
-- 6、用户和角色关联表 用户N-1角色
-- ----------------------------
drop table if exists sys_user_role;
create table sys_user_role (
user_id bigint(20) not null comment '用户ID',
role_id bigint(20) not null comment '角色ID',
primary key(user_id, role_id)
) engine=innodb comment = '用户和角色关联表';
-- ----------------------------
-- 初始化-用户和角色关联表数据
-- ----------------------------
insert into sys_user_role values ('1', '1');
insert into sys_user_role values ('2', '2');
-- ----------------------------
-- 7、角色和菜单关联表 角色1-N菜单
-- ----------------------------
drop table if exists sys_role_menu;
create table sys_role_menu (
role_id bigint(20) not null comment '角色ID',
menu_id bigint(20) not null comment '菜单ID',
primary key(role_id, menu_id)
) engine=innodb comment = '角色和菜单关联表';
-- ----------------------------
-- 初始化-角色和菜单关联表数据
-- ----------------------------
insert into sys_role_menu values ('2', '1');
insert into sys_role_menu values ('2', '2');
insert into sys_role_menu values ('2', '3');
insert into sys_role_menu values ('2', '4');
insert into sys_role_menu values ('2', '100');
insert into sys_role_menu values ('2', '101');
insert into sys_role_menu values ('2', '102');
insert into sys_role_menu values ('2', '103');
insert into sys_role_menu values ('2', '104');
insert into sys_role_menu values ('2', '105');
insert into sys_role_menu values ('2', '106');
insert into sys_role_menu values ('2', '107');
insert into sys_role_menu values ('2', '108');
insert into sys_role_menu values ('2', '109');
insert into sys_role_menu values ('2', '110');
insert into sys_role_menu values ('2', '111');
insert into sys_role_menu values ('2', '112');
insert into sys_role_menu values ('2', '113');
insert into sys_role_menu values ('2', '114');
insert into sys_role_menu values ('2', '115');
insert into sys_role_menu values ('2', '116');
insert into sys_role_menu values ('2', '117');
insert into sys_role_menu values ('2', '500');
insert into sys_role_menu values ('2', '501');
insert into sys_role_menu values ('2', '1000');
insert into sys_role_menu values ('2', '1001');
insert into sys_role_menu values ('2', '1002');
insert into sys_role_menu values ('2', '1003');
insert into sys_role_menu values ('2', '1004');
insert into sys_role_menu values ('2', '1005');
insert into sys_role_menu values ('2', '1006');
insert into sys_role_menu values ('2', '1007');
insert into sys_role_menu values ('2', '1008');
insert into sys_role_menu values ('2', '1009');
insert into sys_role_menu values ('2', '1010');
insert into sys_role_menu values ('2', '1011');
insert into sys_role_menu values ('2', '1012');
insert into sys_role_menu values ('2', '1013');
insert into sys_role_menu values ('2', '1014');
insert into sys_role_menu values ('2', '1015');
insert into sys_role_menu values ('2', '1016');
insert into sys_role_menu values ('2', '1017');
insert into sys_role_menu values ('2', '1018');
insert into sys_role_menu values ('2', '1019');
insert into sys_role_menu values ('2', '1020');
insert into sys_role_menu values ('2', '1021');
insert into sys_role_menu values ('2', '1022');
insert into sys_role_menu values ('2', '1023');
insert into sys_role_menu values ('2', '1024');
insert into sys_role_menu values ('2', '1025');
insert into sys_role_menu values ('2', '1026');
insert into sys_role_menu values ('2', '1027');
insert into sys_role_menu values ('2', '1028');
insert into sys_role_menu values ('2', '1029');
insert into sys_role_menu values ('2', '1030');
insert into sys_role_menu values ('2', '1031');
insert into sys_role_menu values ('2', '1032');
insert into sys_role_menu values ('2', '1033');
insert into sys_role_menu values ('2', '1034');
insert into sys_role_menu values ('2', '1035');
insert into sys_role_menu values ('2', '1036');
insert into sys_role_menu values ('2', '1037');
insert into sys_role_menu values ('2', '1038');
insert into sys_role_menu values ('2', '1039');
insert into sys_role_menu values ('2', '1040');
insert into sys_role_menu values ('2', '1041');
insert into sys_role_menu values ('2', '1042');
insert into sys_role_menu values ('2', '1043');
insert into sys_role_menu values ('2', '1044');
insert into sys_role_menu values ('2', '1045');
insert into sys_role_menu values ('2', '1046');
insert into sys_role_menu values ('2', '1047');
insert into sys_role_menu values ('2', '1048');
insert into sys_role_menu values ('2', '1049');
insert into sys_role_menu values ('2', '1050');
insert into sys_role_menu values ('2', '1051');
insert into sys_role_menu values ('2', '1052');
insert into sys_role_menu values ('2', '1053');
insert into sys_role_menu values ('2', '1054');
insert into sys_role_menu values ('2', '1055');
insert into sys_role_menu values ('2', '1056');
insert into sys_role_menu values ('2', '1057');
insert into sys_role_menu values ('2', '1058');
insert into sys_role_menu values ('2', '1059');
insert into sys_role_menu values ('2', '1060');
-- ----------------------------
-- 8、角色和部门关联表 角色1-N部门
-- ----------------------------
drop table if exists sys_role_dept;
create table sys_role_dept (
role_id bigint(20) not null comment '角色ID',
dept_id bigint(20) not null comment '部门ID',
primary key(role_id, dept_id)
) engine=innodb comment = '角色和部门关联表';
-- ----------------------------
-- 初始化-角色和部门关联表数据
-- ----------------------------
insert into sys_role_dept values ('2', '100');
insert into sys_role_dept values ('2', '101');
insert into sys_role_dept values ('2', '105');
-- ----------------------------
-- 9、用户与岗位关联表 用户1-N岗位
-- ----------------------------
drop table if exists sys_user_post;
create table sys_user_post
(
user_id bigint(20) not null comment '用户ID',
post_id bigint(20) not null comment '岗位ID',
primary key (user_id, post_id)
) engine=innodb comment = '用户与岗位关联表';
-- ----------------------------
-- 初始化-用户与岗位关联表数据
-- ----------------------------
insert into sys_user_post values ('1', '1');
insert into sys_user_post values ('2', '2');
-- ----------------------------
-- 10、操作日志记录
-- ----------------------------
drop table if exists sys_oper_log;
create table sys_oper_log (
oper_id bigint(20) not null auto_increment comment '日志主键',
title varchar(50) default '' comment '模块标题',
business_type int(2) default 0 comment '业务类型0其它 1新增 2修改 3删除',
method varchar(200) default '' comment '方法名称',
request_method varchar(10) default '' comment '请求方式',
operator_type int(1) default 0 comment '操作类别0其它 1后台用户 2手机端用户',
oper_name varchar(50) default '' comment '操作人员',
dept_name varchar(50) default '' comment '部门名称',
oper_url varchar(255) default '' comment '请求URL',
oper_ip varchar(128) default '' comment '主机地址',
oper_location varchar(255) default '' comment '操作地点',
oper_param varchar(2000) default '' comment '请求参数',
json_result varchar(2000) default '' comment '返回参数',
status int(1) default 0 comment '操作状态0正常 1异常',
error_msg varchar(2000) default '' comment '错误消息',
oper_time datetime comment '操作时间',
cost_time bigint(20) default 0 comment '消耗时间',
primary key (oper_id),
key idx_sys_oper_log_bt (business_type),
key idx_sys_oper_log_s (status),
key idx_sys_oper_log_ot (oper_time)
) engine=innodb auto_increment=100 comment = '操作日志记录';
-- ----------------------------
-- 11、字典类型表
-- ----------------------------
drop table if exists sys_dict_type;
create table sys_dict_type
(
dict_id bigint(20) not null auto_increment comment '字典主键',
dict_name varchar(100) default '' comment '字典名称',
dict_type varchar(100) default '' comment '字典类型',
status char(1) default '0' comment '状态0正常 1停用',
create_by varchar(64) default '' comment '创建者',
create_time datetime comment '创建时间',
update_by varchar(64) default '' comment '更新者',
update_time datetime comment '更新时间',
remark varchar(500) default null comment '备注',
primary key (dict_id),
unique (dict_type)
) engine=innodb auto_increment=100 comment = '字典类型表';
insert into sys_dict_type values(1, '用户性别', 'sys_user_sex', '0', 'admin', sysdate(), '', null, '用户性别列表');
insert into sys_dict_type values(2, '菜单状态', 'sys_show_hide', '0', 'admin', sysdate(), '', null, '菜单状态列表');
insert into sys_dict_type values(3, '系统开关', 'sys_normal_disable', '0', 'admin', sysdate(), '', null, '系统开关列表');
insert into sys_dict_type values(4, '任务状态', 'sys_job_status', '0', 'admin', sysdate(), '', null, '任务状态列表');
insert into sys_dict_type values(5, '任务分组', 'sys_job_group', '0', 'admin', sysdate(), '', null, '任务分组列表');
insert into sys_dict_type values(6, '系统是否', 'sys_yes_no', '0', 'admin', sysdate(), '', null, '系统是否列表');
insert into sys_dict_type values(7, '通知类型', 'sys_notice_type', '0', 'admin', sysdate(), '', null, '通知类型列表');
insert into sys_dict_type values(8, '通知状态', 'sys_notice_status', '0', 'admin', sysdate(), '', null, '通知状态列表');
insert into sys_dict_type values(9, '操作类型', 'sys_oper_type', '0', 'admin', sysdate(), '', null, '操作类型列表');
insert into sys_dict_type values(10, '系统状态', 'sys_common_status', '0', 'admin', sysdate(), '', null, '登录状态列表');
-- ----------------------------
-- 12、字典数据表
-- ----------------------------
drop table if exists sys_dict_data;
create table sys_dict_data
(
dict_code bigint(20) not null auto_increment comment '字典编码',
dict_sort int(4) default 0 comment '字典排序',
dict_label varchar(100) default '' comment '字典标签',
dict_value varchar(100) default '' comment '字典键值',
dict_type varchar(100) default '' comment '字典类型',
css_class varchar(100) default null comment '样式属性(其他样式扩展)',
list_class varchar(100) default null comment '表格回显样式',
is_default char(1) default 'N' comment '是否默认Y是 N否',
status char(1) default '0' comment '状态0正常 1停用',
create_by varchar(64) default '' comment '创建者',
create_time datetime comment '创建时间',
update_by varchar(64) default '' comment '更新者',
update_time datetime comment '更新时间',
remark varchar(500) default null comment '备注',
primary key (dict_code)
) engine=innodb auto_increment=100 comment = '字典数据表';
insert into sys_dict_data values(1, 1, '', '0', 'sys_user_sex', '', '', 'Y', '0', 'admin', sysdate(), '', null, '性别男');
insert into sys_dict_data values(2, 2, '', '1', 'sys_user_sex', '', '', 'N', '0', 'admin', sysdate(), '', null, '性别女');
insert into sys_dict_data values(3, 3, '未知', '2', 'sys_user_sex', '', '', 'N', '0', 'admin', sysdate(), '', null, '性别未知');
insert into sys_dict_data values(4, 1, '显示', '0', 'sys_show_hide', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '显示菜单');
insert into sys_dict_data values(5, 2, '隐藏', '1', 'sys_show_hide', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '隐藏菜单');
insert into sys_dict_data values(6, 1, '正常', '0', 'sys_normal_disable', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '正常状态');
insert into sys_dict_data values(7, 2, '停用', '1', 'sys_normal_disable', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '停用状态');
insert into sys_dict_data values(8, 1, '正常', '0', 'sys_job_status', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '正常状态');
insert into sys_dict_data values(9, 2, '暂停', '1', 'sys_job_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '停用状态');
insert into sys_dict_data values(10, 1, '默认', 'DEFAULT', 'sys_job_group', '', '', 'Y', '0', 'admin', sysdate(), '', null, '默认分组');
insert into sys_dict_data values(11, 2, '系统', 'SYSTEM', 'sys_job_group', '', '', 'N', '0', 'admin', sysdate(), '', null, '系统分组');
insert into sys_dict_data values(12, 1, '', 'Y', 'sys_yes_no', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '系统默认是');
insert into sys_dict_data values(13, 2, '', 'N', 'sys_yes_no', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '系统默认否');
insert into sys_dict_data values(14, 1, '通知', '1', 'sys_notice_type', '', 'warning', 'Y', '0', 'admin', sysdate(), '', null, '通知');
insert into sys_dict_data values(15, 2, '公告', '2', 'sys_notice_type', '', 'success', 'N', '0', 'admin', sysdate(), '', null, '公告');
insert into sys_dict_data values(16, 1, '正常', '0', 'sys_notice_status', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '正常状态');
insert into sys_dict_data values(17, 2, '关闭', '1', 'sys_notice_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '关闭状态');
insert into sys_dict_data values(18, 99, '其他', '0', 'sys_oper_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, '其他操作');
insert into sys_dict_data values(19, 1, '新增', '1', 'sys_oper_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, '新增操作');
insert into sys_dict_data values(20, 2, '修改', '2', 'sys_oper_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, '修改操作');
insert into sys_dict_data values(21, 3, '删除', '3', 'sys_oper_type', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '删除操作');
insert into sys_dict_data values(22, 4, '授权', '4', 'sys_oper_type', '', 'primary', 'N', '0', 'admin', sysdate(), '', null, '授权操作');
insert into sys_dict_data values(23, 5, '导出', '5', 'sys_oper_type', '', 'warning', 'N', '0', 'admin', sysdate(), '', null, '导出操作');
insert into sys_dict_data values(24, 6, '导入', '6', 'sys_oper_type', '', 'warning', 'N', '0', 'admin', sysdate(), '', null, '导入操作');
insert into sys_dict_data values(25, 7, '强退', '7', 'sys_oper_type', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '强退操作');
insert into sys_dict_data values(26, 8, '生成代码', '8', 'sys_oper_type', '', 'warning', 'N', '0', 'admin', sysdate(), '', null, '生成操作');
insert into sys_dict_data values(27, 9, '清空数据', '9', 'sys_oper_type', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '清空操作');
insert into sys_dict_data values(28, 1, '成功', '0', 'sys_common_status', '', 'primary', 'N', '0', 'admin', sysdate(), '', null, '正常状态');
insert into sys_dict_data values(29, 2, '失败', '1', 'sys_common_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '停用状态');
-- ----------------------------
-- 13、参数配置表 (已添加 tenant_id)
-- ----------------------------
drop table if exists sys_config;
create table sys_config (
config_id int(5) not null auto_increment comment '参数主键',
tenant_id bigint(20) default null comment '租户IDNULL表示全局配置',
config_name varchar(100) default '' comment '参数名称',
config_key varchar(100) default '' comment '参数键名',
config_value varchar(500) default '' comment '参数键值',
config_type char(1) default 'N' comment '系统内置Y是 N否',
create_by varchar(64) default '' comment '创建者',
create_time datetime comment '创建时间',
update_by varchar(64) default '' comment '更新者',
update_time datetime comment '更新时间',
remark varchar(500) default null comment '备注',
primary key (config_id),
index idx_tenant_id (tenant_id)
) engine=innodb auto_increment=100 comment = '参数配置表';
-- 注意sys_config 的初始化数据 tenant_id 保持 NULL表示全局配置
insert into sys_config values(1, NULL, '主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue', 'Y', 'admin', sysdate(), '', null, '蓝色 skin-blue、绿色 skin-green、紫色 skin-purple、红色 skin-red、黄色 skin-yellow' );
insert into sys_config values(2, NULL, '用户管理-账号初始密码', 'sys.user.initPassword', '123456', 'Y', 'admin', sysdate(), '', null, '初始化密码 123456' );
insert into sys_config values(3, NULL, '主框架页-侧边栏主题', 'sys.index.sideTheme', 'theme-dark', 'Y', 'admin', sysdate(), '', null, '深色主题theme-dark浅色主题theme-light' );
insert into sys_config values(4, NULL, '账号自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', 'admin', sysdate(), '', null, '是否开启验证码功能true开启false关闭');
insert into sys_config values(5, NULL, '账号自助-是否开启用户注册功能', 'sys.account.registerUser', 'false', 'Y', 'admin', sysdate(), '', null, '是否开启注册用户功能true开启false关闭');
insert into sys_config values(6, NULL, '用户登录-黑名单列表', 'sys.login.blackIPList', '', 'Y', 'admin', sysdate(), '', null, '设置登录IP黑名单限制多个匹配项以;分隔,支持匹配(*通配、网段)');
insert into sys_config values(7, NULL, '用户管理-初始密码修改策略', 'sys.account.initPasswordModify', '1', 'Y', 'admin', sysdate(), '', null, '0初始密码修改策略关闭没有任何提示1提醒用户如果未修改初始密码则在登录时就会提醒修改密码对话框');
insert into sys_config values(8, NULL, '用户管理-账号密码更新周期', 'sys.account.passwordValidateDays', '0', 'Y', 'admin', sysdate(), '', null, '密码更新周期填写数字数据初始化值为0不限制若修改必须为大于0小于365的正整数如果超过这个周期登录系统时则在登录时就会提醒修改密码对话框');
-- ----------------------------
-- 14、系统访问记录
-- ----------------------------
drop table if exists sys_logininfor;
create table sys_logininfor (
info_id bigint(20) not null auto_increment comment '访问ID',
user_name varchar(50) default '' comment '用户账号',
ipaddr varchar(128) default '' comment '登录IP地址',
login_location varchar(255) default '' comment '登录地点',
browser varchar(50) default '' comment '浏览器类型',
os varchar(50) default '' comment '操作系统',
status char(1) default '0' comment '登录状态0成功 1失败',
msg varchar(255) default '' comment '提示消息',
login_time datetime comment '访问时间',
primary key (info_id),
key idx_sys_logininfor_s (status),
key idx_sys_logininfor_lt (login_time)
) engine=innodb auto_increment=100 comment = '系统访问记录';
-- ----------------------------
-- 15、定时任务调度表
-- ----------------------------
drop table if exists sys_job;
create table sys_job (
job_id bigint(20) not null auto_increment comment '任务ID',
job_name varchar(64) default '' comment '任务名称',
job_group varchar(64) default 'DEFAULT' comment '任务组名',
invoke_target varchar(500) not null comment '调用目标字符串',
cron_expression varchar(255) default '' comment 'cron执行表达式',
misfire_policy varchar(20) default '3' comment '计划执行错误策略1立即执行 2执行一次 3放弃执行',
concurrent char(1) default '1' comment '是否并发执行0允许 1禁止',
status char(1) default '0' comment '状态0正常 1暂停',
create_by varchar(64) default '' comment '创建者',
create_time datetime comment '创建时间',
update_by varchar(64) default '' comment '更新者',
update_time datetime comment '更新时间',
remark varchar(500) default '' comment '备注信息',
primary key (job_id, job_name, job_group)
) engine=innodb auto_increment=100 comment = '定时任务调度表';
insert into sys_job values(1, '系统默认(无参)', 'DEFAULT', 'ryTask.ryNoParams', '0/10 * * * * ?', '3', '1', '1', 'admin', sysdate(), '', null, '');
insert into sys_job values(2, '系统默认(有参)', 'DEFAULT', 'ryTask.ryParams(\'ry\')', '0/15 * * * * ?', '3', '1', '1', 'admin', sysdate(), '', null, '');
insert into sys_job values(3, '系统默认(多参)', 'DEFAULT', 'ryTask.ryMultipleParams(\'ry\', true, 2000L, 316.50D, 100)', '0/20 * * * * ?', '3', '1', '1', 'admin', sysdate(), '', null, '');
-- ----------------------------
-- 16、定时任务调度日志表
-- ----------------------------
drop table if exists sys_job_log;
create table sys_job_log (
job_log_id bigint(20) not null auto_increment comment '任务日志ID',
job_name varchar(64) not null comment '任务名称',
job_group varchar(64) not null comment '任务组名',
invoke_target varchar(500) not null comment '调用目标字符串',
job_message varchar(500) comment '日志信息',
status char(1) default '0' comment '执行状态0正常 1失败',
exception_info varchar(2000) default '' comment '异常信息',
create_time datetime comment '创建时间',
primary key (job_log_id)
) engine=innodb comment = '定时任务调度日志表';
-- ----------------------------
-- 17、通知公告表 (已添加 tenant_id)
-- ----------------------------
drop table if exists sys_notice;
create table sys_notice (
notice_id int(4) not null auto_increment comment '公告ID',
tenant_id bigint(20) default null comment '租户ID',
notice_title varchar(50) not null comment '公告标题',
notice_type char(1) not null comment '公告类型1通知 2公告',
notice_content longblob default null comment '公告内容',
status char(1) default '0' comment '公告状态0正常 1关闭',
create_by varchar(64) default '' comment '创建者',
create_time datetime comment '创建时间',
update_by varchar(64) default '' comment '更新者',
update_time datetime comment '更新时间',
remark varchar(255) default null comment '备注',
primary key (notice_id),
index idx_tenant_id (tenant_id)
) engine=innodb auto_increment=10 comment = '通知公告表';
-- ----------------------------
-- 初始化-公告信息表数据
-- ----------------------------
insert into sys_notice values('1', @default_tenant_id, '温馨提醒2018-07-01 若依新版本发布啦', '2', '新版本内容', '0', 'admin', sysdate(), '', null, '管理员');
insert into sys_notice values('2', @default_tenant_id, '维护通知2018-07-01 若依系统凌晨维护', '1', '维护内容', '0', 'admin', sysdate(), '', null, '管理员');
-- ----------------------------
-- 18、代码生成业务表
-- ----------------------------
drop table if exists gen_table;
create table gen_table (
table_id bigint(20) not null auto_increment comment '编号',
table_name varchar(200) default '' comment '表名称',
table_comment varchar(500) default '' comment '表描述',
sub_table_name varchar(64) default null comment '关联子表的表名',
sub_table_fk_name varchar(64) default null comment '子表关联的外键名',
class_name varchar(100) default '' comment '实体类名称',
tpl_category varchar(200) default 'crud' comment '使用的模板crud单表操作 tree树表操作',
tpl_web_type varchar(30) default '' comment '前端模板类型element-ui模版 element-plus模版',
package_name varchar(100) comment '生成包路径',
module_name varchar(30) comment '生成模块名',
business_name varchar(30) comment '生成业务名',
function_name varchar(50) comment '生成功能名',
function_author varchar(50) comment '生成功能作者',
gen_type char(1) default '0' comment '生成代码方式0zip压缩包 1自定义路径',
gen_path varchar(200) default '/' comment '生成路径(不填默认项目路径)',
options varchar(1000) comment '其它生成选项',
create_by varchar(64) default '' comment '创建者',
create_time datetime comment '创建时间',
update_by varchar(64) default '' comment '更新者',
update_time datetime comment '更新时间',
remark varchar(500) default null comment '备注',
primary key (table_id)
) engine=innodb auto_increment=1 comment = '代码生成业务表';
-- ----------------------------
-- 19、代码生成业务表字段
-- ----------------------------
drop table if exists gen_table_column;
create table gen_table_column (
column_id bigint(20) not null auto_increment comment '编号',
table_id bigint(20) comment '归属表编号',
column_name varchar(200) comment '列名称',
column_comment varchar(500) comment '列描述',
column_type varchar(100) comment '列类型',
java_type varchar(500) comment 'JAVA类型',
java_field varchar(200) comment 'JAVA字段名',
is_pk char(1) comment '是否主键1是',
is_increment char(1) comment '是否自增1是',
is_required char(1) comment '是否必填1是',
is_insert char(1) comment '是否为插入字段1是',
is_edit char(1) comment '是否编辑字段1是',
is_list char(1) comment '是否列表字段1是',
is_query char(1) comment '是否查询字段1是',
query_type varchar(200) default 'EQ' comment '查询方式(等于、不等于、大于、小于、范围)',
html_type varchar(200) comment '显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)',
dict_type varchar(200) default '' comment '字典类型',
sort int comment '排序',
create_by varchar(64) default '' comment '创建者',
create_time datetime comment '创建时间',
update_by varchar(64) default '' comment '更新者',
update_time datetime comment '更新时间',
primary key (column_id)
) engine=innodb auto_increment=1 comment = '代码生成业务表字段';
-- ============================
-- 初始化完成提示
-- ============================
-- ============================
-- 若依框架 + 多租户支持 初始化完成!
--
-- 已完成改造:
-- 1. 新增 sys_tenant 租户管理表
-- 2. 为 6 张表添加 tenant_id 字段:
-- - sys_user (用户表)
-- - sys_dept (部门表)
-- - sys_role (角色表)
-- - sys_post (岗位表)
-- - sys_notice (通知公告表)
-- - sys_config (参数配置表NULL表示全局配置)
-- 3. 创建必要的索引:
-- - sys_user: idx_tenant_id, idx_tenant_user(tenant_id, user_name)
-- - 其他表: idx_tenant_id
-- 4. 初始化数据已设置默认租户ID
--
-- 下一步:
-- 1. 执行 hairlink_phase1.sql (业务表)
-- 2. 开发后端租户隔离逻辑:
-- - TenantContext (租户上下文)
-- - TenantInterceptor (HTTP拦截器)
-- - TenantSqlInterceptor (MyBatis拦截器)
-- ============================

View File

@ -0,0 +1,120 @@
-- ============================================
-- 若依多租户 - 唯一索引改造脚本
-- 版本: 1.0
-- 日期: 2025-12-19
-- 警告: 执行前务必备份数据库!
-- ============================================
-- ============================================
-- 第一步:检查是否存在重复数据(必须先执行)
-- ============================================
-- 检查 sys_user 表重复数据
SELECT tenant_id, user_name, COUNT(*)
FROM sys_user
WHERE del_flag = '0'
GROUP BY tenant_id, user_name
HAVING COUNT(*) > 1;
-- 如有输出,需先处理重复数据
-- 检查 sys_role 表重复数据
SELECT tenant_id, role_key, COUNT(*)
FROM sys_role
WHERE del_flag = '0'
GROUP BY tenant_id, role_key
HAVING COUNT(*) > 1;
-- 如有输出,需先处理重复数据
-- 检查 sys_post 表重复数据
-- 注意sys_post 表没有 del_flag 字段
SELECT tenant_id, post_code, COUNT(*)
FROM sys_post
GROUP BY tenant_id, post_code
HAVING COUNT(*) > 1;
-- 如有输出,需先处理重复数据
-- ============================================
-- 第二步:执行唯一索引改造
-- ============================================
-- 1. sys_user表用户名改为租户内唯一
-- 说明:不同租户可以有同名用户(如都有 admin
-- 安全删除旧索引兼容MySQL 5.7
SET @index_exists = (SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'sys_user' AND index_name = 'uk_user_name');
SET @sql = IF(@index_exists > 0, 'ALTER TABLE sys_user DROP INDEX uk_user_name', 'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 创建新的联合唯一索引
ALTER TABLE sys_user ADD UNIQUE KEY uk_tenant_user_name (tenant_id, user_name);
-- 2. sys_role表角色标识改为租户内唯一
-- 说明:不同租户可以有同标识角色(如都有 role_admin
SET @index_exists = (SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'sys_role' AND index_name = 'uk_role_key');
SET @sql = IF(@index_exists > 0, 'ALTER TABLE sys_role DROP INDEX uk_role_key', 'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
ALTER TABLE sys_role ADD UNIQUE KEY uk_tenant_role_key (tenant_id, role_key);
-- 3. sys_post表岗位编码改为租户内唯一
-- 说明:不同租户可以有同编码岗位(如都有 ceo
SET @index_exists = (SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'sys_post' AND index_name = 'uk_post_code');
SET @sql = IF(@index_exists > 0, 'ALTER TABLE sys_post DROP INDEX uk_post_code', 'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
ALTER TABLE sys_post ADD UNIQUE KEY uk_tenant_post_code (tenant_id, post_code);
-- ============================================
-- 第三步:验证索引创建
-- ============================================
-- 验证 sys_user 索引
SHOW INDEX FROM sys_user WHERE Key_name = 'uk_tenant_user_name';
-- 验证 sys_role 索引
SHOW INDEX FROM sys_role WHERE Key_name = 'uk_tenant_role_key';
-- 验证 sys_post 索引
SHOW INDEX FROM sys_post WHERE Key_name = 'uk_tenant_post_code';
-- ============================================
-- 执行结果说明
-- ============================================
-- 每个表应该显示索引的两条记录tenant_id 和对应的业务字段)
-- Seq_in_index = 1: tenant_id
-- Seq_in_index = 2: user_name / role_key / post_code
-- ============================================
-- 回滚脚本(仅供紧急情况使用)
-- ============================================
/*
-- 警告:回滚将恢复到单租户模式,可能导致数据冲突!
-- 回滚 sys_user 索引
ALTER TABLE sys_user DROP INDEX IF EXISTS uk_tenant_user_name;
ALTER TABLE sys_user ADD UNIQUE KEY uk_user_name (user_name);
-- 回滚 sys_role 索引
ALTER TABLE sys_role DROP INDEX IF EXISTS uk_tenant_role_key;
ALTER TABLE sys_role ADD UNIQUE KEY uk_role_key (role_key);
-- 回滚 sys_post 索引
ALTER TABLE sys_post DROP INDEX IF EXISTS uk_tenant_post_code;
ALTER TABLE sys_post ADD UNIQUE KEY uk_post_code (post_code);
*/
-- ============================================
-- 执行注意事项
-- ============================================
-- 1. 选择业务低峰期执行预计停机10-30分钟
-- 2. 执行前完整备份数据库mysqldump -u root -p database_name > backup.sql
-- 3. 务必先执行第一步的重复数据检查
-- 4. 如有重复数据,需先清理或合并
-- 5. 大表索引创建较慢,耐心等待不要中断
-- 6. 准备回滚脚本以备不时之需
-- 7. 执行完毕后验证应用功能正常

View File

@ -0,0 +1,204 @@
-- ============================================
-- 租户套餐功能 - 数据库脚本
-- 版本: 1.0
-- 日期: 2025-12-19
-- 说明: 实现租户套餐功能,通过套餐控制租户的菜单权限
-- ============================================
-- ============================================
-- 第一步:创建租户套餐表
-- ============================================
CREATE TABLE IF NOT EXISTS sys_tenant_package (
package_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '套餐ID',
package_name VARCHAR(50) NOT NULL COMMENT '套餐名称',
package_code VARCHAR(50) NOT NULL COMMENT '套餐编码',
status CHAR(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
del_flag CHAR(1) DEFAULT '0' COMMENT '删除标志(0存在 2删除)',
create_by VARCHAR(64) DEFAULT '' COMMENT '创建者',
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
update_by VARCHAR(64) DEFAULT '' COMMENT '更新者',
update_time DATETIME DEFAULT NULL COMMENT '更新时间',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (package_id),
UNIQUE KEY uk_package_code (package_code)
) ENGINE=InnoDB AUTO_INCREMENT=1 COMMENT='租户套餐表';
-- ============================================
-- 第二步:创建套餐菜单关联表
-- ============================================
CREATE TABLE IF NOT EXISTS sys_package_menu (
package_id BIGINT(20) NOT NULL COMMENT '套餐ID',
menu_id BIGINT(20) NOT NULL COMMENT '菜单ID',
PRIMARY KEY (package_id, menu_id),
INDEX idx_package_id (package_id) -- 为package_id添加索引用户要求
) ENGINE=InnoDB COMMENT='套餐菜单关联表';
-- ============================================
-- 第三步修改sys_tenant表增加套餐字段
-- ============================================
-- 检查列是否存在,避免重复执行报错
SELECT COUNT(*) INTO @col_exists
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'sys_tenant'
AND column_name = 'package_id';
-- 只有列不存在时才添加
SET @sql = IF(@col_exists = 0,
'ALTER TABLE sys_tenant ADD COLUMN package_id BIGINT(20) DEFAULT NULL COMMENT ''套餐ID'' AFTER tenant_code',
'SELECT ''Column package_id already exists'' AS message');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 添加索引(忽略已存在的情况)
-- 注意:如果索引已存在会报错,可忽略该错误继续执行
SET @index_exists = (SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'sys_tenant' AND index_name = 'idx_package_id');
SET @sql = IF(@index_exists = 0, 'ALTER TABLE sys_tenant ADD INDEX idx_package_id (package_id)', 'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- ============================================
-- 第四步:初始化套餐数据
-- ============================================
INSERT INTO sys_tenant_package (package_name, package_code, status, create_by, create_time, remark) VALUES
('基础套餐', 'BASIC', '0', 'admin', NOW(), '用户、部门管理'),
('标准套餐', 'STANDARD', '0', 'admin', NOW(), '增加角色、菜单、字典管理'),
('高级套餐', 'PREMIUM', '0', 'admin', NOW(), '全部功能,不限制菜单');
-- ============================================
-- 第五步:初始化套餐菜单关联(基础套餐)
-- ============================================
-- 基础套餐:只包含用户、部门、岗位、字典管理
INSERT INTO sys_package_menu (package_id, menu_id) VALUES
-- 系统管理模块
(1, 1), -- 系统管理
-- 菜单项
(1, 100), -- 用户管理
(1, 101), -- 部门管理
(1, 103), -- 岗位管理
(1, 104), -- 字典管理
-- 用户管理按钮
(1, 1000), (1, 1001), (1, 1002), (1, 1003), (1, 1004), (1, 1005), (1, 1006),
-- 部门管理按钮
(1, 1016), (1, 1017), (1, 1018), (1, 1019),
-- 岗位管理按钮
(1, 1020), (1, 1021), (1, 1022), (1, 1023), (1, 1024),
-- 字典管理按钮
(1, 1025), (1, 1026), (1, 1027), (1, 1028), (1, 1029);
-- ============================================
-- 第六步:初始化套餐菜单关联(标准套餐)
-- ============================================
-- 标准套餐:增加角色、菜单管理
INSERT INTO sys_package_menu (package_id, menu_id) VALUES
-- 系统管理模块
(2, 1), -- 系统管理
-- 菜单项
(2, 100), (2, 101), (2, 102), (2, 103), (2, 104), (2, 105), -- 用户/部门/角色/岗位/字典/菜单
-- 用户管理按钮
(2, 1000), (2, 1001), (2, 1002), (2, 1003), (2, 1004), (2, 1005), (2, 1006),
-- 角色管理按钮
(2, 1007), (2, 1008), (2, 1009), (2, 1010), (2, 1011),
-- 菜单管理按钮
(2, 1012), (2, 1013), (2, 1014), (2, 1015),
-- 部门管理按钮
(2, 1016), (2, 1017), (2, 1018), (2, 1019),
-- 岗位管理按钮
(2, 1020), (2, 1021), (2, 1022), (2, 1023), (2, 1024),
-- 字典管理按钮
(2, 1025), (2, 1026), (2, 1027), (2, 1028), (2, 1029);
-- ============================================
-- 第七步:高级套餐不插入关联数据
-- ============================================
-- 说明:高级套餐 (package_id=3) 不插入 sys_package_menu 数据
-- 代码逻辑:查询结果为空列表时,表示"不限制菜单",用户可以看到所有菜单
-- ============================================
-- 第八步:关联现有租户到套餐
-- ============================================
-- 根据实际租户情况调整(使用安全的更新方式)
UPDATE sys_tenant SET package_id = 1 WHERE tenant_code = 'DEFAULT' AND package_id IS NULL;
UPDATE sys_tenant SET package_id = 2 WHERE tenant_code = 'TENANT_A' AND package_id IS NULL;
UPDATE sys_tenant SET package_id = 3 WHERE tenant_code = 'TENANT_B' AND package_id IS NULL;
-- ============================================
-- 验证数据
-- ============================================
-- 验证套餐表
SELECT * FROM sys_tenant_package;
-- 验证套餐菜单关联(基础套餐)
SELECT COUNT(*) AS '基础套餐菜单数' FROM sys_package_menu WHERE package_id = 1;
-- 验证套餐菜单关联(标准套餐)
SELECT COUNT(*) AS '标准套餐菜单数' FROM sys_package_menu WHERE package_id = 2;
-- 验证套餐菜单关联(高级套餐)
SELECT COUNT(*) AS '高级套餐菜单数应为0' FROM sys_package_menu WHERE package_id = 3;
-- 验证租户套餐关联
SELECT tenant_id, tenant_name, tenant_code, package_id
FROM sys_tenant
WHERE package_id IS NOT NULL;
-- ============================================
-- 回滚脚本(仅供紧急情况使用)
-- ============================================
/*
-- 警告:回滚将删除所有套餐数据和配置!
-- 1. 清除租户的套餐关联
UPDATE sys_tenant SET package_id = NULL;
-- 2. 删除套餐菜单关联表
DROP TABLE IF EXISTS sys_package_menu;
-- 3. 删除套餐表
DROP TABLE IF EXISTS sys_tenant_package;
-- 4. 删除租户表的套餐字段
ALTER TABLE sys_tenant DROP COLUMN IF EXISTS package_id;
ALTER TABLE sys_tenant DROP INDEX IF EXISTS idx_package_id;
*/
-- ============================================
-- 使用说明
-- ============================================
/*
1. ** (BASIC)**
-
-
-
2. ** (STANDARD)**
-
- + +
-
3. ** (PREMIUM)**
-
-
- sys_package_menu "全部菜单可见"
- =
-
-
*/

View File

@ -0,0 +1,32 @@
-- ============================================
-- 套餐管理菜单 - 插入脚本
-- 版本: 1.0
-- 日期: 2025-12-20
-- ============================================
-- 1. 套餐管理菜单放在系统管理下parent_id = 1
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES (2006, '套餐管理', 1, 10, 'package', 'system/package/index', NULL, 'Package', 1, 0, 'C', '0', '0', 'system:package:list', 'component', 'admin', NOW(), '', NULL, '租户套餐管理菜单');
-- 2. 套餐管理按钮权限
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES (2007, '套餐查询', 2006, 1, '', NULL, NULL, '', 1, 0, 'F', '0', '0', 'system:package:query', '#', 'admin', NOW(), '', NULL, '');
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES (2008, '套餐新增', 2006, 2, '', NULL, NULL, '', 1, 0, 'F', '0', '0', 'system:package:add', '#', 'admin', NOW(), '', NULL, '');
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES (2009, '套餐修改', 2006, 3, '', NULL, NULL, '', 1, 0, 'F', '0', '0', 'system:package:edit', '#', 'admin', NOW(), '', NULL, '');
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES (2010, '套餐删除', 2006, 4, '', NULL, NULL, '', 1, 0, 'F', '0', '0', 'system:package:remove', '#', 'admin', NOW(), '', NULL, '');
-- 3. 给超级管理员角色分配权限role_id = 1
INSERT INTO sys_role_menu (role_id, menu_id) VALUES (1, 2006);
INSERT INTO sys_role_menu (role_id, menu_id) VALUES (1, 2007);
INSERT INTO sys_role_menu (role_id, menu_id) VALUES (1, 2008);
INSERT INTO sys_role_menu (role_id, menu_id) VALUES (1, 2009);
INSERT INTO sys_role_menu (role_id, menu_id) VALUES (1, 2010);
-- 验证
SELECT * FROM sys_menu WHERE menu_id >= 2006;