From a50dc8af8a1c44de1b2ef7d8f7808d2f1ad35991 Mon Sep 17 00:00:00 2001 From: 9264yf <691506722@qq.com> Date: Wed, 17 Dec 2025 15:37:31 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat=EF=BC=9Ahair-link=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursorindexingignore | 3 + .env.example | 355 ++++++ .gitignore | 44 + pom.xml | 8 +- ruoyi-admin/pom.xml | 2 +- .../main/java/com/ruoyi/RuoYiApplication.java | 19 +- .../src/main/resources/application-druid.yml | 38 +- .../src/main/resources/application.yml | 76 +- ruoyi-admin/src/main/resources/banner.txt | 52 +- ruoyi-admin/src/main/resources/logback.xml | 8 +- ruoyi-common/pom.xml | 2 +- .../com/ruoyi/common/config/RuoYiConfig.java | 4 +- ruoyi-framework/pom.xml | 2 +- ruoyi-generator/pom.xml | 2 +- ruoyi-quartz/pom.xml | 2 +- ruoyi-system/pom.xml | 2 +- ruoyi-ui/.env.development | 4 +- ruoyi-ui/.env.production | 4 +- ruoyi-ui/package.json | 10 +- ruoyi-ui/src/components/RuoYi/Doc/index.vue | 21 - ruoyi-ui/src/components/RuoYi/Git/index.vue | 21 - ruoyi-ui/src/layout/components/Navbar.vue | 14 +- ruoyi-ui/src/settings.js | 2 +- ruoyi-ui/src/views/index.vue | 1016 +---------------- ruoyi-ui/src/views/login.vue | 2 +- ruoyi-ui/vue.config.js | 6 +- 26 files changed, 528 insertions(+), 1191 deletions(-) create mode 100644 .cursorindexingignore create mode 100644 .env.example delete mode 100644 ruoyi-ui/src/components/RuoYi/Doc/index.vue delete mode 100644 ruoyi-ui/src/components/RuoYi/Git/index.vue diff --git a/.cursorindexingignore b/.cursorindexingignore new file mode 100644 index 000000000..953908e73 --- /dev/null +++ b/.cursorindexingignore @@ -0,0 +1,3 @@ + +# Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references +.specstory/** diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..16f2f07a7 --- /dev/null +++ b/.env.example @@ -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 +################################################################################ diff --git a/.gitignore b/.gitignore index 0c0da3e07..4342bbffb 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/pom.xml b/pom.xml index 66fc545cc..d50117569 100644 --- a/pom.xml +++ b/pom.xml @@ -5,12 +5,12 @@ 4.0.0 com.ruoyi - ruoyi + hair-link 3.9.0 - ruoyi - http://www.ruoyi.vip - 若依管理系统 + hair-link + http://www.example.com + Hair-Link 美发预约管理系统 3.9.0 diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml index 434b5f07d..4e1863e77 100644 --- a/ruoyi-admin/pom.xml +++ b/ruoyi-admin/pom.xml @@ -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"> - ruoyi + hair-link com.ruoyi 3.9.0 diff --git a/ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java b/ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java index e3c56ee54..4dacd4485 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java @@ -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"); } } diff --git a/ruoyi-admin/src/main/resources/application-druid.yml b/ruoyi-admin/src/main/resources/application-druid.yml index bcfad3eae..7a3afb098 100644 --- a/ruoyi-admin/src/main/resources/application-druid.yml +++ b/ruoyi-admin/src/main/resources/application-druid.yml @@ -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: diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index d15705c48..79c54a9e3 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -1,49 +1,49 @@ # 项目相关配置 -ruoyi: +hairlink: # 名称 - name: RuoYi + name: Hair-Link # 版本 version: 3.9.0 # 版权年份 copyrightYear: 2025 - # 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath) - profile: D:/ruoyi/uploadPath + # 文件路径 示例( Windows配置D:/hairlink/uploadPath,Linux配置 /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/*} diff --git a/ruoyi-admin/src/main/resources/banner.txt b/ruoyi-admin/src/main/resources/banner.txt index 94662592f..d369f11e5 100644 --- a/ruoyi-admin/src/main/resources/banner.txt +++ b/ruoyi-admin/src/main/resources/banner.txt @@ -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 // -//////////////////////////////////////////////////////////////////// \ No newline at end of file + + ╔═══════════════════════╗ + ║ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ║ + ║ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ║ + ║ ▓▓▓▓▓▓▓▓▓▓▓▓▓ ║ + ╚══╗ ▓▓▓▓▓▓▓▓▓ ╔══╝ + ╲ ▓▓▓▓▓▓▓ ╱ + ╲ ▓▓▓▓▓ ╱ + ╲ ▓▓▓ ╱ + ╲ ▓ ╱ + ╲ ╱ + ╲ ╱ + ╲ ╱ + V + ╱ ╲ + ╱ ╲ + ╱ ╲ + ╱ ░ ╲ + ╱ ░░░ ╲ + ╱ ░░░░░ ╲ + ╱ ░░░░░░░ ╲ + ╔══╝ ░░░░░░░░░ ╚══╗ + ║ ░░░░░░░░░░░░░ ║ + ║ ░░░░░░░░░░░░░░░░░ ║ + ║ ░░░░░░░░░░░░░░░░░░░ ║ + ╚═══════════════════════╝ + + ═══【时 光】═══ diff --git a/ruoyi-admin/src/main/resources/logback.xml b/ruoyi-admin/src/main/resources/logback.xml index d69a57207..f2456ead1 100644 --- a/ruoyi-admin/src/main/resources/logback.xml +++ b/ruoyi-admin/src/main/resources/logback.xml @@ -1,7 +1,7 @@ - + @@ -20,7 +20,7 @@ ${log.path}/sys-info.%d{yyyy-MM-dd}.log - 60 + ${HAIRLINK_LOG_MAX_HISTORY:-60} ${log.pattern} @@ -42,7 +42,7 @@ ${log.path}/sys-error.%d{yyyy-MM-dd}.log - 60 + ${HAIRLINK_LOG_MAX_HISTORY:-60} ${log.pattern} @@ -64,7 +64,7 @@ ${log.path}/sys-user.%d{yyyy-MM-dd}.log - 60 + ${HAIRLINK_LOG_MAX_HISTORY:-60} ${log.pattern} diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml index 6e6b98808..f71e8c74c 100644 --- a/ruoyi-common/pom.xml +++ b/ruoyi-common/pom.xml @@ -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"> - ruoyi + hair-link com.ruoyi 3.9.0 diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java b/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java index eac3da152..1afd5a96c 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java @@ -5,11 +5,11 @@ import org.springframework.stereotype.Component; /** * 读取项目相关配置 - * + * * @author ruoyi */ @Component -@ConfigurationProperties(prefix = "ruoyi") +@ConfigurationProperties(prefix = "hairlink") public class RuoYiConfig { /** 项目名称 */ diff --git a/ruoyi-framework/pom.xml b/ruoyi-framework/pom.xml index ae1c59c3b..cc18dacdc 100644 --- a/ruoyi-framework/pom.xml +++ b/ruoyi-framework/pom.xml @@ -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"> - ruoyi + hair-link com.ruoyi 3.9.0 diff --git a/ruoyi-generator/pom.xml b/ruoyi-generator/pom.xml index 031399292..15c437383 100644 --- a/ruoyi-generator/pom.xml +++ b/ruoyi-generator/pom.xml @@ -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"> - ruoyi + hair-link com.ruoyi 3.9.0 diff --git a/ruoyi-quartz/pom.xml b/ruoyi-quartz/pom.xml index 4806634a4..61311fb27 100644 --- a/ruoyi-quartz/pom.xml +++ b/ruoyi-quartz/pom.xml @@ -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"> - ruoyi + hair-link com.ruoyi 3.9.0 diff --git a/ruoyi-system/pom.xml b/ruoyi-system/pom.xml index ba8b1a8b6..efff398a8 100644 --- a/ruoyi-system/pom.xml +++ b/ruoyi-system/pom.xml @@ -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"> - ruoyi + hair-link com.ruoyi 3.9.0 diff --git a/ruoyi-ui/.env.development b/ruoyi-ui/.env.development index 18b2a3ed4..1e45be0b0 100644 --- a/ruoyi-ui/.env.development +++ b/ruoyi-ui/.env.development @@ -1,10 +1,10 @@ # 页面标题 -VUE_APP_TITLE = 若依管理系统 +VUE_APP_TITLE = Hair-Link # 开发环境配置 ENV = 'development' -# 若依管理系统/开发环境 +# Hair-Link/开发环境 VUE_APP_BASE_API = '/dev-api' # 路由懒加载 diff --git a/ruoyi-ui/.env.production b/ruoyi-ui/.env.production index cb064ec84..33d631330 100644 --- a/ruoyi-ui/.env.production +++ b/ruoyi-ui/.env.production @@ -1,8 +1,8 @@ # 页面标题 -VUE_APP_TITLE = 若依管理系统 +VUE_APP_TITLE = Hair-Link # 生产环境配置 ENV = 'production' -# 若依管理系统/生产环境 +# Hair-Link/生产环境 VUE_APP_BASE_API = '/prod-api' diff --git a/ruoyi-ui/package.json b/ruoyi-ui/package.json index 5df0cedfc..ee3f8a662 100644 --- a/ruoyi-ui/package.json +++ b/ruoyi-ui/package.json @@ -1,8 +1,8 @@ { - "name": "ruoyi", + "name": "hair-link", "version": "3.9.0", - "description": "若依管理系统", - "author": "若依", + "description": "Hair-Link 美发预约管理系统", + "author": "时光", "license": "MIT", "scripts": { "dev": "vue-cli-service serve", @@ -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", diff --git a/ruoyi-ui/src/components/RuoYi/Doc/index.vue b/ruoyi-ui/src/components/RuoYi/Doc/index.vue deleted file mode 100644 index cc829b243..000000000 --- a/ruoyi-ui/src/components/RuoYi/Doc/index.vue +++ /dev/null @@ -1,21 +0,0 @@ - - - \ No newline at end of file diff --git a/ruoyi-ui/src/components/RuoYi/Git/index.vue b/ruoyi-ui/src/components/RuoYi/Git/index.vue deleted file mode 100644 index 517c44865..000000000 --- a/ruoyi-ui/src/components/RuoYi/Git/index.vue +++ /dev/null @@ -1,21 +0,0 @@ - - - \ No newline at end of file diff --git a/ruoyi-ui/src/layout/components/Navbar.vue b/ruoyi-ui/src/layout/components/Navbar.vue index 1d10b91a0..5f9137087 100644 --- a/ruoyi-ui/src/layout/components/Navbar.vue +++ b/ruoyi-ui/src/layout/components/Navbar.vue @@ -9,14 +9,6 @@ @@ -1062,11 +56,6 @@ export default { // 版本号 version: "3.9.0" } - }, - methods: { - goTarget(href) { - window.open(href, "_blank") - } } } @@ -1134,4 +123,3 @@ export default { } } - diff --git a/ruoyi-ui/src/views/login.vue b/ruoyi-ui/src/views/login.vue index f979f68b5..aa298711d 100644 --- a/ruoyi-ui/src/views/login.vue +++ b/ruoyi-ui/src/views/login.vue @@ -56,7 +56,7 @@ diff --git a/ruoyi-ui/vue.config.js b/ruoyi-ui/vue.config.js index a6bfb3b87..1a16d8800 100644 --- a/ruoyi-ui/vue.config.js +++ b/ruoyi-ui/vue.config.js @@ -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, // 压缩文件格式 From 9a8ffdbb8f12030feafd7490571a98d3850ca745 Mon Sep 17 00:00:00 2001 From: 9264yf <691506722@qq.com> Date: Fri, 19 Dec 2025 21:21:39 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat(tenant):=20=E5=AE=9E=E7=8E=B0=E5=A4=9A?= =?UTF-8?q?=E7=A7=9F=E6=88=B7=E6=95=B0=E6=8D=AE=E9=9A=94=E7=A6=BB=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增租户管理模块(SysTenant CRUD) - 实现 TenantContext 租户上下文(基于 TransmittableThreadLocal) - 新增 TenantInterceptor HTTP拦截器,自动识别当前租户 - 新增 TenantSqlInterceptor MyBatis 拦截器,自动注入 tenant_id 过滤条件 - 支持超级管理员全局模式,可查看所有租户数据 - 扩展 LoginUser/SysUser 支持租户字段 - 新增前端租户管理页面 --- pom.xml | 17 + .../system/SysTenantController.java | 106 +++ .../controller/system/SysUserController.java | 12 + ruoyi-common/pom.xml | 6 + .../com/ruoyi/common/config/TenantConfig.java | 122 +++ .../common/core/context/TenantContext.java | 98 +++ .../ruoyi/common/core/domain/BaseEntity.java | 14 + .../common/core/domain/entity/SysUser.java | 13 + .../common/core/domain/model/LoginUser.java | 15 + ruoyi-framework/pom.xml | 6 + .../ruoyi/framework/config/MyBatisConfig.java | 10 +- .../framework/config/ResourcesConfig.java | 10 +- .../interceptor/TenantInterceptor.java | 100 +++ .../interceptor/TenantSqlInterceptor.java | 319 +++++++ .../web/service/SysLoginService.java | 87 +- .../framework/web/service/TokenService.java | 21 +- .../com/ruoyi/system/domain/SysTenant.java | 163 ++++ .../ruoyi/system/mapper/SysTenantMapper.java | 63 ++ .../system/service/ISysTenantService.java | 62 ++ .../service/impl/SysTenantServiceImpl.java | 96 +++ .../mapper/system/SysTenantMapper.xml | 110 +++ .../resources/mapper/system/SysUserMapper.xml | 8 +- ruoyi-ui/src/api/system/tenant.js | 44 + ruoyi-ui/src/api/system/user.js | 3 + ruoyi-ui/src/views/system/tenant/index.vue | 346 ++++++++ ruoyi-ui/src/views/system/user/index.vue | 26 +- sql/hairlink_phase1.sql | 312 +++++++ sql/ry_20250522_with_tenant.sql | 792 ++++++++++++++++++ 28 files changed, 2949 insertions(+), 32 deletions(-) create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysTenantController.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/config/TenantConfig.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/context/TenantContext.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/TenantInterceptor.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/TenantSqlInterceptor.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTenant.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTenantMapper.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTenantService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTenantServiceImpl.java create mode 100644 ruoyi-system/src/main/resources/mapper/system/SysTenantMapper.xml create mode 100644 ruoyi-ui/src/api/system/tenant.js create mode 100644 ruoyi-ui/src/views/system/tenant/index.vue create mode 100644 sql/hairlink_phase1.sql create mode 100644 sql/ry_20250522_with_tenant.sql diff --git a/pom.xml b/pom.xml index d50117569..200de75c0 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,9 @@ 4.1.2 2.3 0.9.1 + + 2.14.3 + 4.6 9.0.108 1.2.13 @@ -218,6 +221,20 @@ ${ruoyi.version} + + + com.alibaba + transmittable-thread-local + ${transmittable-thread-local.version} + + + + + com.github.jsqlparser + jsqlparser + ${jsqlparser.version} + + diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysTenantController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysTenantController.java new file mode 100644 index 000000000..b6e383945 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysTenantController.java @@ -0,0 +1,106 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import com.ruoyi.system.domain.SysTenant; +import com.ruoyi.system.service.ISysTenantService; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.common.core.page.TableDataInfo; + +/** + * 租户信息Controller + * + * @author W-yf + * @date 2025-12-19 + */ +@RestController +@RequestMapping("/link/tenant") +public class SysTenantController extends BaseController +{ + @Autowired + private ISysTenantService sysTenantService; + + /** + * 查询租户信息列表 + */ + @PreAuthorize("@ss.hasPermi('link:tenant:list')") + @GetMapping("/list") + public TableDataInfo list( + SysTenant sysTenant) + { + startPage(); + List list = sysTenantService.selectSysTenantList(sysTenant); + return getDataTable(list); + } + + /** + * 导出租户信息列表 + */ + @PreAuthorize("@ss.hasPermi('link:tenant:export')") + @Log(title = "租户信息", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, SysTenant sysTenant) + { + List list = sysTenantService.selectSysTenantList(sysTenant); + ExcelUtil util = new ExcelUtil(SysTenant.class); + util.exportExcel(response, list, "租户信息数据"); + } + + /** + * 获取租户信息详细信息 + */ + @PreAuthorize("@ss.hasPermi('link:tenant:query')") + @GetMapping(value = "/{tenantId}") + public AjaxResult getInfo(@PathVariable("tenantId") Long tenantId) + { + return success(sysTenantService.selectSysTenantByTenantId(tenantId)); + } + + /** + * 新增租户信息 + */ + @PreAuthorize("@ss.hasPermi('link:tenant:add')") + @Log(title = "租户信息", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody SysTenant sysTenant) + { + return toAjax(sysTenantService.insertSysTenant(sysTenant)); + } + + /** + * 修改租户信息 + */ + @PreAuthorize("@ss.hasPermi('link:tenant:edit')") + @Log(title = "租户信息", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody SysTenant sysTenant) + { + return toAjax(sysTenantService.updateSysTenant(sysTenant)); + } + + /** + * 删除租户信息 + */ + @PreAuthorize("@ss.hasPermi('link:tenant:remove')") + @Log(title = "租户信息", businessType = BusinessType.DELETE) + @DeleteMapping("/{tenantIds}") + public AjaxResult remove(@PathVariable Long[] tenantIds) + { + return toAjax(sysTenantService.deleteSysTenantByTenantIds(tenantIds)); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java index 11790f9ba..5fabb38e8 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java @@ -27,9 +27,11 @@ import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.system.domain.SysTenant; import com.ruoyi.system.service.ISysDeptService; import com.ruoyi.system.service.ISysPostService; import com.ruoyi.system.service.ISysRoleService; +import com.ruoyi.system.service.ISysTenantService; import com.ruoyi.system.service.ISysUserService; /** @@ -53,6 +55,9 @@ public class SysUserController extends BaseController @Autowired private ISysPostService postService; + @Autowired + private ISysTenantService tenantService; + /** * 获取用户列表 */ @@ -113,6 +118,13 @@ public class SysUserController extends BaseController List roles = roleService.selectRoleAll(); ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList())); ajax.put("posts", postService.selectPostAll()); + // 如果是超级管理员,返回租户列表 + if (SysUser.isAdmin(SecurityUtils.getUserId())) + { + SysTenant query = new SysTenant(); + query.setStatus("0"); + ajax.put("tenants", tenantService.selectSysTenantList(query)); + } return ajax; } diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml index f71e8c74c..af6506b9d 100644 --- a/ruoyi-common/pom.xml +++ b/ruoyi-common/pom.xml @@ -119,6 +119,12 @@ javax.servlet-api + + + com.alibaba + transmittable-thread-local + + \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/config/TenantConfig.java b/ruoyi-common/src/main/java/com/ruoyi/common/config/TenantConfig.java new file mode 100644 index 000000000..323b5431b --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/config/TenantConfig.java @@ -0,0 +1,122 @@ +package com.ruoyi.common.config; + +import java.util.Arrays; +import java.util.List; + +/** + * 租户配置类 + * + * Reason: 集中管理租户相关配置,包括超级管理员列表 + */ +public class TenantConfig { + + /** + * 超级管理员用户ID列表 + * Reason: 使用固定用户ID判断,简单可靠,避免角色权限体系的复杂度 + * + * 超级管理员拥有以下特权: + * 1. 登录后自动进入全局模式 + * 2. 可以查看所有租户的数据 + * 3. 所有操作都会记录审计日志 + */ + public static final List SUPER_ADMIN_USER_IDS = Arrays.asList( + 1L // admin 用户 + ); + + /** + * 忽略租户过滤的URL路径(不需要租户隔离的接口) + * Reason: 登录、验证码等公共接口不需要租户上下文 + */ + public static final List IGNORE_URLS = Arrays.asList( + "/login", + "/captchaImage", + "/logout", + "/register" + ); + + /** + * 需要进行租户隔离的表名列表 + * Reason: 明确定义需要拦截的表,避免误拦截 + */ + public static final List TENANT_TABLES = Arrays.asList( + // 若依框架表 + "sys_user", + "sys_dept", + "sys_role", + "sys_post", + "sys_notice", + // HairLink业务表 + "hl_customer", + "hl_member_card", + "hl_service", + "hl_staff", + "hl_order" + ); + + /** + * 不需要租户隔离的表(白名单) + * Reason: 这些表是全局共享的,所有租户共用 + */ + public static final List IGNORE_TABLES = Arrays.asList( + "sys_menu", + "sys_dict_type", + "sys_dict_data", + "sys_config", + "sys_user_role", + "sys_role_menu", + "sys_role_dept", + "sys_tenant", + "sys_logininfor", + "sys_oper_log", + "sys_job", + "sys_job_log" + ); + + /** + * 判断是否是超级管理员 + * + * @param userId 用户ID + * @return true-超级管理员,false-普通用户 + */ + public static boolean isSuperAdmin(Long userId) { + if (userId == null) { + return false; + } + return SUPER_ADMIN_USER_IDS.contains(userId); + } + + /** + * 判断URL是否需要忽略租户过滤 + * + * @param url 请求URL + * @return true-忽略,false-需要过滤 + */ + public static boolean isIgnoreUrl(String url) { + if (url == null) { + return false; + } + return IGNORE_URLS.stream().anyMatch(url::contains); + } + + /** + * 判断表是否需要租户隔离 + * + * @param tableName 表名 + * @return true-需要隔离,false-不需要 + */ + public static boolean needTenantFilter(String tableName) { + if (tableName == null) { + return false; + } + + String lowerTableName = tableName.toLowerCase(); + + // 白名单表不需要过滤 + if (IGNORE_TABLES.stream().anyMatch(lowerTableName::equals)) { + return false; + } + + // 租户表需要过滤 + return TENANT_TABLES.stream().anyMatch(lowerTableName::equals); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/context/TenantContext.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/context/TenantContext.java new file mode 100644 index 000000000..0927a3c66 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/context/TenantContext.java @@ -0,0 +1,98 @@ +package com.ruoyi.common.core.context; + +import com.alibaba.ttl.TransmittableThreadLocal; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 租户上下文 - 基于ThreadLocal实现租户ID的传递 + * + * Reason: 使用TransmittableThreadLocal支持线程池场景下的上下文传递 + */ +public class TenantContext { + + private static final Logger log = LoggerFactory.getLogger(TenantContext.class); + + /** + * 支持父子线程传递的ThreadLocal + * Reason: 若依框架使用了异步任务和线程池,普通ThreadLocal会丢失上下文 + */ + private static final TransmittableThreadLocal TENANT_ID_HOLDER = new TransmittableThreadLocal<>(); + + /** + * 忽略租户过滤的标志(用于超级管理员或系统任务) + */ + private static final TransmittableThreadLocal IGNORE_TENANT = new TransmittableThreadLocal<>(); + + /** + * 设置当前租户ID + * + * @param tenantId 租户ID + */ + public static void setTenantId(Long tenantId) { + if (tenantId == null) { + log.warn("尝试设置NULL租户ID,已拒绝"); + return; + } + TENANT_ID_HOLDER.set(tenantId); + log.debug("TenantContext设置租户ID: {}", tenantId); + } + + /** + * 获取当前租户ID + * + * @return 租户ID + */ + public static Long getTenantId() { + return TENANT_ID_HOLDER.get(); + } + + /** + * 清除租户上下文 + * Reason: 请求结束后必须清除,避免线程池复用时的上下文污染 + */ + public static void clear() { + TENANT_ID_HOLDER.remove(); + IGNORE_TENANT.remove(); + log.trace("TenantContext已清除"); + } + + /** + * 设置忽略租户过滤(超级管理员专用) + * + * @param ignore 是否忽略租户过滤 + */ + public static void setIgnore(boolean ignore) { + IGNORE_TENANT.set(ignore); + if (ignore) { + log.warn("租户过滤已禁用,当前处于全局模式!请谨慎操作"); + } else { + log.info("租户过滤已启用"); + } + } + + /** + * 是否忽略租户过滤 + * + * @return true-忽略(全局模式),false-正常过滤 + */ + public static boolean isIgnore() { + Boolean ignore = IGNORE_TENANT.get(); + return ignore != null && ignore; + } + + /** + * 获取当前租户ID(如果不存在则抛出异常) + * + * @return 租户ID + * @throws com.ruoyi.common.exception.ServiceException 租户ID不存在时抛出 + */ + public static Long getRequiredTenantId() { + Long tenantId = getTenantId(); + if (tenantId == null && !isIgnore()) { + log.error("【安全警告】租户上下文丢失,当前操作被拦截"); + throw new RuntimeException("租户上下文丢失,请重新登录"); + } + return tenantId; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java index 67269ff6b..e995d07ca 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java @@ -38,6 +38,10 @@ public class BaseEntity implements Serializable /** 备注 */ private String remark; + /** 租户ID */ + @JsonIgnore + private Long tenantId; + /** 请求参数 */ @JsonInclude(JsonInclude.Include.NON_EMPTY) private Map params; @@ -115,4 +119,14 @@ public class BaseEntity implements Serializable { this.params = params; } + + public Long getTenantId() + { + return tenantId; + } + + public void setTenantId(Long tenantId) + { + this.tenantId = tenantId; + } } diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java index cf4e5cb48..06a10d741 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java @@ -92,6 +92,9 @@ public class SysUser extends BaseEntity /** 角色ID */ private Long roleId; + /** 租户ID */ + private Long tenantId; + public SysUser() { @@ -310,6 +313,16 @@ public class SysUser extends BaseEntity this.roleId = roleId; } + public Long getTenantId() + { + return tenantId; + } + + public void setTenantId(Long tenantId) + { + this.tenantId = tenantId; + } + @Override public String toString() { return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java index 95e169cf3..31bad0223 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java @@ -26,6 +26,11 @@ public class LoginUser implements UserDetails */ private Long deptId; + /** + * 租户ID + */ + private Long tenantId; + /** * 用户唯一标识 */ @@ -258,6 +263,16 @@ public class LoginUser implements UserDetails this.user = user; } + public Long getTenantId() + { + return tenantId; + } + + public void setTenantId(Long tenantId) + { + this.tenantId = tenantId; + } + @Override public Collection getAuthorities() { diff --git a/ruoyi-framework/pom.xml b/ruoyi-framework/pom.xml index cc18dacdc..855729583 100644 --- a/ruoyi-framework/pom.xml +++ b/ruoyi-framework/pom.xml @@ -59,6 +59,12 @@ ruoyi-system + + + com.github.jsqlparser + jsqlparser + + \ No newline at end of file diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java index e30fe74fe..c72b69002 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java @@ -23,10 +23,11 @@ import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.util.ClassUtils; import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.interceptor.TenantSqlInterceptor; /** * Mybatis支持*匹配扫描包 - * + * * @author ruoyi */ @Configuration @@ -35,6 +36,9 @@ public class MyBatisConfig @Autowired private Environment env; + @Autowired + private TenantSqlInterceptor tenantSqlInterceptor; + static final String DEFAULT_RESOURCE_PATTERN = "**/*.class"; public static String setTypeAliasesPackage(String typeAliasesPackage) @@ -127,6 +131,10 @@ public class MyBatisConfig sessionFactory.setTypeAliasesPackage(typeAliasesPackage); sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ","))); sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation)); + + // 【多租户】注册MyBatis租户SQL拦截器 + sessionFactory.setPlugins(tenantSqlInterceptor); + return sessionFactory.getObject(); } } \ No newline at end of file diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java index 12d354946..7f73082b6 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java @@ -14,10 +14,11 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import com.ruoyi.common.config.RuoYiConfig; import com.ruoyi.common.constant.Constants; import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor; +import com.ruoyi.framework.interceptor.TenantInterceptor; /** * 通用配置 - * + * * @author ruoyi */ @Configuration @@ -26,6 +27,9 @@ public class ResourcesConfig implements WebMvcConfigurer @Autowired private RepeatSubmitInterceptor repeatSubmitInterceptor; + @Autowired + private TenantInterceptor tenantInterceptor; + @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { @@ -45,6 +49,10 @@ public class ResourcesConfig implements WebMvcConfigurer @Override public void addInterceptors(InterceptorRegistry registry) { + // 【多租户】租户拦截器 - 必须在第一位,优先设置租户上下文 + registry.addInterceptor(tenantInterceptor).addPathPatterns("/**"); + + // 防重复提交拦截器 registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**"); } diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/TenantInterceptor.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/TenantInterceptor.java new file mode 100644 index 000000000..275009dc1 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/TenantInterceptor.java @@ -0,0 +1,100 @@ +package com.ruoyi.framework.interceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import com.ruoyi.common.config.TenantConfig; +import com.ruoyi.common.core.context.TenantContext; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.SecurityUtils; + +/** + * 租户拦截器 - 从LoginUser中提取tenant_id并设置到TenantContext + * + * Reason: 每个HTTP请求都需要识别当前租户,并将租户信息存入ThreadLocal + * + * 工作流程: + * 1. 请求进入 → preHandle() + * 2. 从SecurityContext获取LoginUser + * 3. 判断是否是超级管理员 + * - 是:设置忽略租户过滤(全局模式) + * - 否:提取tenant_id并设置到TenantContext + * 4. 请求结束 → afterCompletion() → 清除TenantContext + */ +@Component +public class TenantInterceptor implements HandlerInterceptor { + + private static final Logger log = LoggerFactory.getLogger(TenantInterceptor.class); + + /** + * 请求处理前的拦截 + */ + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + + String requestUri = request.getRequestURI(); + + // 判断是否是忽略URL(如登录、验证码接口) + if (TenantConfig.isIgnoreUrl(requestUri)) { + log.trace("忽略URL租户过滤: {}", requestUri); + return true; + } + + try { + // 从SecurityContext获取当前登录用户 + LoginUser loginUser = SecurityUtils.getLoginUser(); + + if (loginUser != null) { + Long userId = loginUser.getUserId(); + + // 判断是否是超级管理员 + if (TenantConfig.isSuperAdmin(userId)) { + // 超级管理员:忽略租户过滤(全局模式) + TenantContext.setIgnore(true); + log.warn("【超级管理员】用户 {} (ID:{}) 进入全局模式,可查看所有租户数据", + loginUser.getUsername(), userId); + } + else if (loginUser.getTenantId() != null) { + // 普通用户:设置租户ID + TenantContext.setTenantId(loginUser.getTenantId()); + log.debug("【租户用户】用户 {} (ID:{}) 进入租户 {} 模式", + loginUser.getUsername(), userId, loginUser.getTenantId()); + } + else { + // 未分配租户的普通用户(不允许访问) + log.error("【安全警告】用户 {} (ID:{}) 未分配租户且非超级管理员,拒绝访问", + loginUser.getUsername(), userId); + throw new ServiceException("用户未分配租户,请联系管理员"); + } + } else { + // 未登录状态(可能是匿名访问或登录接口) + log.trace("未获取到LoginUser,跳过租户拦截: {}", requestUri); + } + + } catch (ServiceException e) { + // 业务异常直接抛出 + throw e; + } catch (Exception e) { + // 其他异常(可能是登录接口或匿名访问) + log.trace("获取租户上下文失败: {}, URI: {}", e.getMessage(), requestUri); + } + + return true; + } + + /** + * 请求完成后的清理 + * Reason: 必须清除ThreadLocal,避免线程池复用时的上下文污染 + */ + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, + Object handler, Exception ex) throws Exception { + TenantContext.clear(); + log.trace("请求完成,TenantContext已清除"); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/TenantSqlInterceptor.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/TenantSqlInterceptor.java new file mode 100644 index 000000000..79a1fa934 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/TenantSqlInterceptor.java @@ -0,0 +1,319 @@ +package com.ruoyi.framework.interceptor; + +import java.sql.Connection; +import java.util.Properties; + +import com.ruoyi.common.exception.ServiceException; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.expression.operators.relational.EqualsTo; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.select.FromItem; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.statement.update.Update; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.SqlCommandType; +import org.apache.ibatis.plugin.Interceptor; +import org.apache.ibatis.plugin.Intercepts; +import org.apache.ibatis.plugin.Invocation; +import org.apache.ibatis.plugin.Plugin; +import org.apache.ibatis.plugin.Signature; +import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.reflection.SystemMetaObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import com.ruoyi.common.config.TenantConfig; +import com.ruoyi.common.core.context.TenantContext; + +/** + * MyBatis租户SQL拦截器 - 自动在SQL中添加 tenant_id 过滤条件 + * + * Reason: 借鉴若依DataScope模式,通过SQL解析实现租户隔离 + * + * 工作原理: + * 1. 拦截MyBatis的StatementHandler.prepare()方法 + * 2. 获取原始SQL和命令类型 + * 3. 判断是否需要拦截(SELECT/UPDATE/DELETE + 租户表) + * 4. 使用JSQLParser解析SQL + * 5. 在WHERE子句中添加 tenant_id = ? 条件 + * 6. 将改写后的SQL设置回BoundSql + */ +@Component +@Intercepts({ + @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}) +}) +public class TenantSqlInterceptor implements Interceptor { + + private static final Logger log = LoggerFactory.getLogger(TenantSqlInterceptor.class); + + @Override + public Object intercept(Invocation invocation) throws Throwable { + StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); + MetaObject metaObject = SystemMetaObject.forObject(statementHandler); + + MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); + SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType(); + + BoundSql boundSql = statementHandler.getBoundSql(); + String originalSql = boundSql.getSql(); + + // 判断是否需要拦截 + if (!shouldIntercept(sqlCommandType, originalSql)) { + return invocation.proceed(); + } + + // 超级管理员忽略租户过滤 + if (TenantContext.isIgnore()) { + log.debug("【全局模式】跳过租户过滤: {}", originalSql.substring(0, Math.min(100, originalSql.length()))); + return invocation.proceed(); + } + + // 获取租户ID + Long tenantId = TenantContext.getTenantId(); + if (tenantId == null) { + log.error("【安全警告】租户ID为空且非全局模式,SQL被拦截: {}", + originalSql.substring(0, Math.min(100, originalSql.length()))); + throw new ServiceException("租户上下文丢失,请重新登录"); + } + + // 解析并修改SQL + try { + String modifiedSql = processSql(originalSql, tenantId, sqlCommandType); + if (!modifiedSql.equals(originalSql)) { + metaObject.setValue("delegate.boundSql.sql", modifiedSql); + log.debug("【租户SQL拦截】租户ID: {}, 原SQL: {}", tenantId, + originalSql.substring(0, Math.min(100, originalSql.length()))); + } + } catch (Exception e) { + log.error("租户SQL解析失败: {}, 原SQL: {}", e.getMessage(), + originalSql.substring(0, Math.min(100, originalSql.length()))); + // SQL解析失败不影响执行,记录日志后继续 + } + + return invocation.proceed(); + } + + /** + * 判断是否需要拦截 + * + * 核心逻辑:通过SQL解析获取主表名,只有主表是租户表时才拦截 + * 这样可以避免: + * 1. 字符串子串匹配导致的误判(如 sys_role_menu 误匹配 sys_role) + * 2. JOIN表被错误过滤(只对主表添加租户条件) + * + * @param sqlCommandType SQL命令类型 + * @param sql 原始SQL + * @return true-需要拦截,false-不需要 + */ + private boolean shouldIntercept(SqlCommandType sqlCommandType, String sql) { + // 只拦截 SELECT、UPDATE、DELETE + if (sqlCommandType != SqlCommandType.SELECT && + sqlCommandType != SqlCommandType.UPDATE && + sqlCommandType != SqlCommandType.DELETE) { + return false; + } + + try { + // 解析SQL获取主表名 + Statement statement = CCJSqlParserUtil.parse(sql); + String mainTableName = getMainTableName(statement); + + // 只有主表是租户表时才拦截 + return mainTableName != null && isTenantTable(mainTableName); + } catch (Exception e) { + // SQL解析失败,不拦截(避免影响正常业务) + log.debug("SQL解析失败,跳过租户拦截: {}", e.getMessage()); + return false; + } + } + + /** + * 从SQL语句中获取主表名 + * + * @param statement SQL语句 + * @return 主表名,解析失败返回null + */ + private String getMainTableName(Statement statement) { + if (statement instanceof Select) { + PlainSelect plainSelect = (PlainSelect) ((Select) statement).getSelectBody(); + FromItem fromItem = plainSelect.getFromItem(); + if (fromItem instanceof Table) { + return ((Table) fromItem).getName(); + } + } else if (statement instanceof Update) { + return ((Update) statement).getTable().getName(); + } else if (statement instanceof Delete) { + return ((Delete) statement).getTable().getName(); + } + return null; + } + + /** + * 判断表名是否是租户表 + * + * @param tableName 表名 + * @return true-是租户表,false-不是 + */ + private boolean isTenantTable(String tableName) { + return TenantConfig.TENANT_TABLES.stream() + .anyMatch(t -> t.equalsIgnoreCase(tableName)); + } + + /** + * 处理SQL - 添加租户过滤条件 + * + * @param sql 原始SQL + * @param tenantId 租户ID + * @param sqlCommandType SQL类型 + * @return 改写后的SQL + */ + private String processSql(String sql, Long tenantId, SqlCommandType sqlCommandType) throws Exception { + Statement statement = CCJSqlParserUtil.parse(sql); + + if (statement instanceof Select) { + // 处理 SELECT 语句 + Select selectStatement = (Select) statement; + PlainSelect plainSelect = (PlainSelect) selectStatement.getSelectBody(); + + // 获取主表别名,避免多表JOIN时tenant_id歧义 + String tableAlias = getMainTableAlias(plainSelect); + + // 构造 tenant_id = ? 条件(带表别名) + EqualsTo tenantCondition = buildTenantCondition(tenantId, tableAlias); + + // 合并WHERE条件 + if (plainSelect.getWhere() != null) { + AndExpression andExpression = new AndExpression(tenantCondition, plainSelect.getWhere()); + plainSelect.setWhere(andExpression); + } else { + plainSelect.setWhere(tenantCondition); + } + + return selectStatement.toString(); + } + else if (statement instanceof Update) { + // 处理 UPDATE 语句 + Update updateStatement = (Update) statement; + + // 获取表别名 + String tableAlias = getUpdateTableAlias(updateStatement); + EqualsTo tenantCondition = buildTenantCondition(tenantId, tableAlias); + + if (updateStatement.getWhere() != null) { + AndExpression andExpression = new AndExpression(tenantCondition, updateStatement.getWhere()); + updateStatement.setWhere(andExpression); + } else { + updateStatement.setWhere(tenantCondition); + } + + return updateStatement.toString(); + } + else if (statement instanceof Delete) { + // 处理 DELETE 语句 + Delete deleteStatement = (Delete) statement; + + // 获取表别名 + String tableAlias = getDeleteTableAlias(deleteStatement); + EqualsTo tenantCondition = buildTenantCondition(tenantId, tableAlias); + + if (deleteStatement.getWhere() != null) { + AndExpression andExpression = new AndExpression(tenantCondition, deleteStatement.getWhere()); + deleteStatement.setWhere(andExpression); + } else { + deleteStatement.setWhere(tenantCondition); + } + + return deleteStatement.toString(); + } + + return sql; + } + + /** + * 获取SELECT语句主表的别名或表名 + * + * @param plainSelect SELECT语句 + * @return 表别名或表名,用于构造 tenant_id 条件 + */ + private String getMainTableAlias(PlainSelect plainSelect) { + FromItem fromItem = plainSelect.getFromItem(); + if (fromItem instanceof Table) { + Table table = (Table) fromItem; + if (table.getAlias() != null) { + return table.getAlias().getName(); + } + return table.getName(); + } + return null; + } + + /** + * 获取UPDATE语句的表别名或表名 + * + * @param updateStatement UPDATE语句 + * @return 表别名或表名 + */ + private String getUpdateTableAlias(Update updateStatement) { + Table table = updateStatement.getTable(); + if (table != null) { + if (table.getAlias() != null) { + return table.getAlias().getName(); + } + return table.getName(); + } + return null; + } + + /** + * 获取DELETE语句的表别名或表名 + * + * @param deleteStatement DELETE语句 + * @return 表别名或表名 + */ + private String getDeleteTableAlias(Delete deleteStatement) { + Table table = deleteStatement.getTable(); + if (table != null) { + if (table.getAlias() != null) { + return table.getAlias().getName(); + } + return table.getName(); + } + return null; + } + + /** + * 构造租户过滤条件:[表别名.]tenant_id = ? + * + * @param tenantId 租户ID + * @param tableAlias 表别名(可为null) + * @return EqualsTo 表达式 + */ + private EqualsTo buildTenantCondition(Long tenantId, String tableAlias) { + EqualsTo equalsTo = new EqualsTo(); + if (tableAlias != null) { + equalsTo.setLeftExpression(new Column(new Table(tableAlias), "tenant_id")); + } else { + equalsTo.setLeftExpression(new Column("tenant_id")); + } + equalsTo.setRightExpression(new LongValue(tenantId)); + return equalsTo; + } + + @Override + public Object plugin(Object target) { + return Plugin.wrap(target, this); + } + + @Override + public void setProperties(Properties properties) { + // 可以从配置文件读取租户表配置 + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java index a7795f323..14ab896f3 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java @@ -1,6 +1,8 @@ package com.ruoyi.framework.web.service; import javax.annotation.Resource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; @@ -22,6 +24,7 @@ import com.ruoyi.common.utils.DateUtils; import com.ruoyi.common.utils.MessageUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.common.core.context.TenantContext; import com.ruoyi.framework.manager.AsyncManager; import com.ruoyi.framework.manager.factory.AsyncFactory; import com.ruoyi.framework.security.context.AuthenticationContextHolder; @@ -30,12 +33,13 @@ import com.ruoyi.system.service.ISysUserService; /** * 登录校验方法 - * + * * @author ruoyi */ @Component public class SysLoginService { + private static final Logger log = LoggerFactory.getLogger(SysLoginService.class); @Autowired private TokenService tokenService; @@ -62,41 +66,74 @@ public class SysLoginService */ public String login(String username, String password, String code, String uuid) { - // 验证码校验 - validateCaptcha(username, code, uuid); - // 登录前置校验 - loginPreCheck(username, password); - // 用户验证 - Authentication authentication = null; + // 【多租户】登录阶段:临时忽略租户过滤 + // Reason: 登录时需要查询用户表获取tenant_id,形成先有鸡还是先有蛋的问题 + // 因此登录阶段临时设置全局模式,允许查询和更新用户表 try { - UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); - AuthenticationContextHolder.setContext(authenticationToken); - // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername - authentication = authenticationManager.authenticate(authenticationToken); - } - catch (Exception e) - { - if (e instanceof BadCredentialsException) + TenantContext.setIgnore(true); + log.debug("【多租户】登录阶段:临时设置全局模式"); + + // 验证码校验 + validateCaptcha(username, code, uuid); + // 登录前置校验 + loginPreCheck(username, password); + // 用户验证 + Authentication authentication = null; + try { - AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); - throw new UserPasswordNotMatchException(); + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); + AuthenticationContextHolder.setContext(authenticationToken); + // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername + authentication = authenticationManager.authenticate(authenticationToken); + } + catch (Exception e) + { + if (e instanceof BadCredentialsException) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); + throw new UserPasswordNotMatchException(); + } + else + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage())); + throw new ServiceException(e.getMessage()); + } + } + finally + { + AuthenticationContextHolder.clearContext(); + } + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"))); + LoginUser loginUser = (LoginUser) authentication.getPrincipal(); + recordLoginInfo(loginUser.getUserId()); + + // 【多租户】从SysUser获取tenant_id并设置到LoginUser + if (loginUser.getUser() != null && loginUser.getUser().getTenantId() != null) + { + loginUser.setTenantId(loginUser.getUser().getTenantId()); + log.info("【多租户】用户 {} 登录成功,租户ID: {}", username, loginUser.getTenantId()); + } + else if (!com.ruoyi.common.config.TenantConfig.isSuperAdmin(loginUser.getUserId())) + { + // 非超级管理员必须有租户ID + log.error("用户 {} 未分配租户且非超级管理员,拒绝登录", username); + throw new ServiceException("用户未分配租户,请联系管理员"); } else { - AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage())); - throw new ServiceException(e.getMessage()); + log.warn("【多租户】超级管理员 {} 登录,无需租户ID", username); } + + // 生成token + return tokenService.createToken(loginUser); } finally { - AuthenticationContextHolder.clearContext(); + // 【多租户】登录完成,清除全局模式标记 + TenantContext.clear(); + log.debug("【多租户】登录流程结束,清除全局模式"); } - AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"))); - LoginUser loginUser = (LoginUser) authentication.getPrincipal(); - recordLoginInfo(loginUser.getUserId()); - // 生成token - return tokenService.createToken(loginUser); } /** diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java index e5179a3fa..5a9a8bbde 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java @@ -56,7 +56,7 @@ public class TokenService /** * 获取用户身份信息 - * + * * @return 用户信息 */ public LoginUser getLoginUser(HttpServletRequest request) @@ -72,6 +72,15 @@ public class TokenService String uuid = (String) claims.get(Constants.LOGIN_USER_KEY); String userKey = getTokenKey(uuid); LoginUser user = redisCache.getCacheObject(userKey); + + // 【多租户】从JWT中提取tenant_id并设置到LoginUser + if (user != null && claims.get("tenant_id") != null) + { + Long tenantId = claims.get("tenant_id", Long.class); + user.setTenantId(tenantId); + log.debug("从JWT中提取租户ID: {}", tenantId); + } + return user; } catch (Exception e) @@ -107,7 +116,7 @@ public class TokenService /** * 创建令牌 - * + * * @param loginUser 用户信息 * @return 令牌 */ @@ -121,6 +130,14 @@ public class TokenService Map claims = new HashMap<>(); claims.put(Constants.LOGIN_USER_KEY, token); claims.put(Constants.JWT_USERNAME, loginUser.getUsername()); + + // 【多租户】将tenant_id存入JWT + if (loginUser.getTenantId() != null) + { + claims.put("tenant_id", loginUser.getTenantId()); + log.debug("JWT中存储租户ID: {}", loginUser.getTenantId()); + } + return createToken(claims); } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTenant.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTenant.java new file mode 100644 index 000000000..a6c8b1cac --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTenant.java @@ -0,0 +1,163 @@ +package com.ruoyi.system.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 租户信息对象 sys_tenant + * + * @author W-yf + * @date 2025-12-19 + */ +public class SysTenant extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 租户ID */ + private Long tenantId; + + /** 租户名称 */ + @Excel(name = "租户名称") + private String tenantName; + + /** 租户编码(唯一标识) */ + @Excel(name = "租户编码", readConverterExp = "唯=一标识") + private String tenantCode; + + /** 联系人 */ + @Excel(name = "联系人") + private String contactName; + + /** 联系电话 */ + @Excel(name = "联系电话") + private String contactPhone; + + /** 过期时间(NULL表示永久) */ + @Excel(name = "过期时间", readConverterExp = "N=ULL表示永久") + private Date expireTime; + + /** 套餐ID(预留) */ + @Excel(name = "套餐ID", readConverterExp = "预=留") + private Long packageId; + + /** 状态(0正常 1停用) */ + @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** 删除标志(0存在 2删除) */ + private String delFlag; + + public void setTenantId(Long tenantId) + { + this.tenantId = tenantId; + } + + public Long getTenantId() + { + return tenantId; + } + + public void setTenantName(String tenantName) + { + this.tenantName = tenantName; + } + + public String getTenantName() + { + return tenantName; + } + + public void setTenantCode(String tenantCode) + { + this.tenantCode = tenantCode; + } + + public String getTenantCode() + { + return tenantCode; + } + + public void setContactName(String contactName) + { + this.contactName = contactName; + } + + public String getContactName() + { + return contactName; + } + + public void setContactPhone(String contactPhone) + { + this.contactPhone = contactPhone; + } + + public String getContactPhone() + { + return contactPhone; + } + + public void setExpireTime(Date expireTime) + { + this.expireTime = expireTime; + } + + public Date getExpireTime() + { + return expireTime; + } + + public void setPackageId(Long packageId) + { + this.packageId = packageId; + } + + public Long getPackageId() + { + return packageId; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getStatus() + { + return status; + } + + public void setDelFlag(String delFlag) + { + this.delFlag = delFlag; + } + + public String getDelFlag() + { + return delFlag; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("tenantId", getTenantId()) + .append("tenantName", getTenantName()) + .append("tenantCode", getTenantCode()) + .append("contactName", getContactName()) + .append("contactPhone", getContactPhone()) + .append("expireTime", getExpireTime()) + .append("packageId", getPackageId()) + .append("status", getStatus()) + .append("delFlag", getDelFlag()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTenantMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTenantMapper.java new file mode 100644 index 000000000..d73d7d2de --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTenantMapper.java @@ -0,0 +1,63 @@ +package com.ruoyi.system.mapper; + +import com.ruoyi.system.domain.SysTenant; + +import java.util.List; + + +/** + * 租户信息Mapper接口 + * + * @author W-yf + * @date 2025-12-19 + */ +public interface SysTenantMapper +{ + /** + * 查询租户信息 + * + * @param tenantId 租户信息主键 + * @return 租户信息 + */ + public SysTenant selectSysTenantByTenantId(Long tenantId); + + /** + * 查询租户信息列表 + * + * @param sysTenant 租户信息 + * @return 租户信息集合 + */ + public List selectSysTenantList(SysTenant sysTenant); + + /** + * 新增租户信息 + * + * @param sysTenant 租户信息 + * @return 结果 + */ + public int insertSysTenant(SysTenant sysTenant); + + /** + * 修改租户信息 + * + * @param sysTenant 租户信息 + * @return 结果 + */ + public int updateSysTenant(SysTenant sysTenant); + + /** + * 删除租户信息 + * + * @param tenantId 租户信息主键 + * @return 结果 + */ + public int deleteSysTenantByTenantId(Long tenantId); + + /** + * 批量删除租户信息 + * + * @param tenantIds 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteSysTenantByTenantIds(Long[] tenantIds); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTenantService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTenantService.java new file mode 100644 index 000000000..6c2ef01dc --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTenantService.java @@ -0,0 +1,62 @@ +package com.ruoyi.system.service; + +import com.ruoyi.system.domain.SysTenant; + +import java.util.List; + +/** + * 租户信息Service接口 + * + * @author W-yf + * @date 2025-12-19 + */ +public interface ISysTenantService +{ + /** + * 查询租户信息 + * + * @param tenantId 租户信息主键 + * @return 租户信息 + */ + public SysTenant selectSysTenantByTenantId(Long tenantId); + + /** + * 查询租户信息列表 + * + * @param sysTenant 租户信息 + * @return 租户信息集合 + */ + public List selectSysTenantList(SysTenant sysTenant); + + /** + * 新增租户信息 + * + * @param sysTenant 租户信息 + * @return 结果 + */ + public int insertSysTenant(SysTenant sysTenant); + + /** + * 修改租户信息 + * + * @param sysTenant 租户信息 + * @return 结果 + */ + public int updateSysTenant(SysTenant sysTenant); + + /** + * 批量删除租户信息 + * + * @param tenantIds 需要删除的租户信息主键集合 + * @return 结果 + */ + public int deleteSysTenantByTenantIds(Long[] tenantIds); + + /** + * 删除租户信息信息 + * + * @param tenantId 租户信息主键 + * @return 结果 + */ + public int deleteSysTenantByTenantId(Long tenantId); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTenantServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTenantServiceImpl.java new file mode 100644 index 000000000..81741c31d --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTenantServiceImpl.java @@ -0,0 +1,96 @@ +package com.ruoyi.system.service.impl; + +import java.util.List; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.system.domain.SysTenant; +import com.ruoyi.system.mapper.SysTenantMapper; +import com.ruoyi.system.service.ISysTenantService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * 租户信息Service业务层处理 + * + * @author W-yf + * @date 2025-12-19 + */ +@Service +public class SysTenantServiceImpl implements ISysTenantService +{ + @Autowired + private SysTenantMapper sysTenantMapper; + + /** + * 查询租户信息 + * + * @param tenantId 租户信息主键 + * @return 租户信息 + */ + @Override + public SysTenant selectSysTenantByTenantId(Long tenantId) + { + return sysTenantMapper.selectSysTenantByTenantId(tenantId); + } + + /** + * 查询租户信息列表 + * + * @param sysTenant 租户信息 + * @return 租户信息 + */ + @Override + public List selectSysTenantList(SysTenant sysTenant) + { + return sysTenantMapper.selectSysTenantList(sysTenant); + } + + /** + * 新增租户信息 + * + * @param sysTenant 租户信息 + * @return 结果 + */ + @Override + public int insertSysTenant(SysTenant sysTenant) + { + sysTenant.setCreateTime(DateUtils.getNowDate()); + return sysTenantMapper.insertSysTenant(sysTenant); + } + + /** + * 修改租户信息 + * + * @param sysTenant 租户信息 + * @return 结果 + */ + @Override + public int updateSysTenant(SysTenant sysTenant) + { + sysTenant.setUpdateTime(DateUtils.getNowDate()); + return sysTenantMapper.updateSysTenant(sysTenant); + } + + /** + * 批量删除租户信息 + * + * @param tenantIds 需要删除的租户信息主键 + * @return 结果 + */ + @Override + public int deleteSysTenantByTenantIds(Long[] tenantIds) + { + return sysTenantMapper.deleteSysTenantByTenantIds(tenantIds); + } + + /** + * 删除租户信息信息 + * + * @param tenantId 租户信息主键 + * @return 结果 + */ + @Override + public int deleteSysTenantByTenantId(Long tenantId) + { + return sysTenantMapper.deleteSysTenantByTenantId(tenantId); + } +} diff --git a/ruoyi-system/src/main/resources/mapper/system/SysTenantMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysTenantMapper.xml new file mode 100644 index 000000000..179123646 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysTenantMapper.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + select tenant_id, tenant_name, tenant_code, contact_name, contact_phone, expire_time, package_id, status, del_flag, create_by, create_time, update_by, update_time, remark from sys_tenant + + + + + + + + insert into sys_tenant + + tenant_name, + tenant_code, + contact_name, + contact_phone, + expire_time, + package_id, + status, + del_flag, + create_by, + create_time, + update_by, + update_time, + remark, + + + #{tenantName}, + #{tenantCode}, + #{contactName}, + #{contactPhone}, + #{expireTime}, + #{packageId}, + #{status}, + #{delFlag}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + #{remark}, + + + + + update sys_tenant + + tenant_name = #{tenantName}, + tenant_code = #{tenantCode}, + contact_name = #{contactName}, + contact_phone = #{contactPhone}, + expire_time = #{expireTime}, + package_id = #{packageId}, + status = #{status}, + del_flag = #{delFlag}, + create_by = #{createBy}, + create_time = #{createTime}, + update_by = #{updateBy}, + update_time = #{updateTime}, + remark = #{remark}, + + where tenant_id = #{tenantId} + + + + delete from sys_tenant where tenant_id = #{tenantId} + + + + delete from sys_tenant where tenant_id in + + #{tenantId} + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml index 49de1cab1..6aeffca7b 100644 --- a/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml @@ -7,6 +7,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -48,7 +49,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - select u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.pwd_update_date, u.create_by, u.create_time, u.remark, + select u.user_id, u.dept_id, u.tenant_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.pwd_update_date, u.create_by, u.create_time, u.remark, d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.status as dept_status, r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status from sys_user u @@ -58,7 +59,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + del_flag = '0' + and package_name like concat('%', #{packageName}, '%') + and package_code = #{packageCode} + and status = #{status} + + + + + + + insert into sys_tenant_package + + package_name, + package_code, + status, + del_flag, + create_by, + create_time, + update_by, + update_time, + remark, + + + #{packageName}, + #{packageCode}, + #{status}, + #{delFlag}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + #{remark}, + + + + + update sys_tenant_package + + package_name = #{packageName}, + package_code = #{packageCode}, + status = #{status}, + del_flag = #{delFlag}, + create_by = #{createBy}, + create_time = #{createTime}, + update_by = #{updateBy}, + update_time = #{updateTime}, + remark = #{remark}, + + where package_id = #{packageId} + + + + update sys_tenant_package set del_flag = '2' where package_id = #{packageId} + + + + update sys_tenant_package set del_flag = '2' where package_id in + + #{packageId} + + + + + + + + + insert into sys_package_menu(package_id, menu_id) values + + (#{packageId}, #{menuId}) + + + + + + delete from sys_package_menu where package_id = #{packageId} + + + diff --git a/ruoyi-ui/src/api/system/package.js b/ruoyi-ui/src/api/system/package.js new file mode 100644 index 000000000..8c5520956 --- /dev/null +++ b/ruoyi-ui/src/api/system/package.js @@ -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' + }) +} diff --git a/ruoyi-ui/src/views/system/package/index.vue b/ruoyi-ui/src/views/system/package/index.vue new file mode 100644 index 000000000..fbb5a6ac5 --- /dev/null +++ b/ruoyi-ui/src/views/system/package/index.vue @@ -0,0 +1,376 @@ + + + + + diff --git a/ruoyi-ui/src/views/system/tenant/index.vue b/ruoyi-ui/src/views/system/tenant/index.vue index 1c51aa5ee..4af837d38 100644 --- a/ruoyi-ui/src/views/system/tenant/index.vue +++ b/ruoyi-ui/src/views/system/tenant/index.vue @@ -41,13 +41,15 @@ placeholder="请选择过期时间"> - - + + + + 搜索 @@ -113,7 +115,11 @@ {{ parseTime(scope.row.expireTime, '{y}-{m}-{d}') }} - + + + @@ -167,11 +173,18 @@ placeholder="请选择过期时间"> - - - - - + + + + {{ pkg.packageName }} + {{ pkg.remark }} + + @@ -187,6 +200,7 @@