From 5fa2e08aad7bbcc93a870e9c355785d3a0f939d7 Mon Sep 17 00:00:00 2001
From: "Ray.Hao" <1490493387@qq.com>
Date: Wed, 3 Dec 2025 09:49:54 +0800
Subject: [PATCH 01/22] =?UTF-8?q?feat(config):=20=E6=94=AF=E6=8C=81?=
=?UTF-8?q?=E5=A4=9A=E6=95=B0=E6=8D=AE=E5=BA=93=E7=B1=BB=E5=9E=8B=E9=85=8D?=
=?UTF-8?q?=E7=BD=AE=E5=8F=8A=E5=8A=A8=E6=80=81=E5=88=86=E9=A1=B5=E6=96=B9?=
=?UTF-8?q?=E8=A8=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../com/youlai/boot/config/MybatisConfig.java | 18 +++++++++--
src/main/resources/application-dev.yml | 19 +++++++++++
src/main/resources/application-prod.yml | 32 +++++++++++++------
3 files changed, 56 insertions(+), 13 deletions(-)
diff --git a/src/main/java/com/youlai/boot/config/MybatisConfig.java b/src/main/java/com/youlai/boot/config/MybatisConfig.java
index a3711fbf..7cc08b39 100644
--- a/src/main/java/com/youlai/boot/config/MybatisConfig.java
+++ b/src/main/java/com/youlai/boot/config/MybatisConfig.java
@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionIntercepto
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.youlai.boot.plugin.mybatis.MyDataPermissionHandler;
import com.youlai.boot.plugin.mybatis.MyMetaObjectHandler;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@@ -21,16 +22,27 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
@EnableTransactionManagement
public class MybatisConfig {
+ @Value("${app.db-type:mysql}")
+ private String dbType;
+
/**
* 分页插件和数据权限插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
- //数据权限
+ // 数据权限
interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler()));
- //分页插件
- interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
+ // 分页插件,根据配置动态选择数据库类型
+ DbType mpDbType = DbType.MYSQL;
+ String type = dbType == null ? "mysql" : dbType.toLowerCase();
+ if ("postgres".equals(type) || "postgresql".equals(type)) {
+ mpDbType = DbType.POSTGRE_SQL;
+ } else if ("dm".equals(type) || "dameng".equals(type)) {
+ // 达梦更接近 Oracle 语法,这里选择 ORACLE 方言以获得较好兼容性
+ mpDbType = DbType.ORACLE;
+ }
+ interceptor.addInnerInterceptor(new PaginationInnerInterceptor(mpDbType));
return interceptor;
}
diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml
index 8af66727..54a00f90 100644
--- a/src/main/resources/application-dev.yml
+++ b/src/main/resources/application-dev.yml
@@ -1,13 +1,30 @@
server:
port: 8989
+# 数据库类型:用于 MyBatis-Plus 分页方言等(仅方言,不负责连接信息)
+app:
+ db-type: mysql # 可选:mysql | postgres | dm
+
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
+ # === MySQL 数据源(默认启用) ===
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://www.youlai.tech:3306/youlai_boot?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true
username: youlai
password: 123456
+
+ # === PostgreSQL 数据源示例(按需启用) ===
+ # driver-class-name: org.postgresql.Driver
+ # url: jdbc:postgresql://127.0.0.1:5432/youlai_boot
+ # username: postgres
+ # password: 123456
+
+ # === 达梦 DM 数据源示例(按需启用,注意按实际驱动与 URL 调整) ===
+ # driver-class-name: dm.jdbc.driver.DmDriver
+ # url: jdbc:dm://127.0.0.1:5236?schema=YOULAI_BOOT
+ # username: SYSDBA
+ # password: 123456
data:
redis:
database: 0
@@ -74,6 +91,8 @@ spring:
response-format:
type: json_object
+
+
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
global-config:
diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml
index fa9e9e63..b92bcc1a 100644
--- a/src/main/resources/application-prod.yml
+++ b/src/main/resources/application-prod.yml
@@ -4,10 +4,23 @@ server:
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
+ # === MySQL 数据源(默认启用) ===
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://www.youlai.tech:3306/youlai_boot?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true
username: youlai
password: 123456
+
+ # === PostgreSQL 数据源示例(按需启用) ===
+ # driver-class-name: org.postgresql.Driver
+ # url: jdbc:postgresql://127.0.0.1:5432/youlai_boot
+ # username: postgres
+ # password: 123456
+
+ # === 达梦 DM 数据源示例(按需启用,注意按实际驱动与 URL 调整) ===
+ # driver-class-name: dm.jdbc.driver.DmDriver
+ # url: jdbc:dm://127.0.0.1:5236?schema=YOULAI_BOOT
+ # username: SYSDBA
+ # password: 123456
data:
redis:
database: 11
@@ -95,19 +108,19 @@ mybatis-plus:
security:
session:
type: jwt # 会话方式 [jwt|redis-token]
- access-token-time-to-live: 7200 # 访问令牌 有效期(单位:秒),默认 2 小时,-1 表示永不过期
- refresh-token-time-to-live: 604800 # 刷新令牌有效期(单位:秒),默认 7 天,-1 表示永不过期
+ access-token-time-to-live: 7200 # 访问令牌 有效期(单位:秒),默认 2 小时,-1 表示永不过期
+ refresh-token-time-to-live: 604800 # 刷新令牌有效期(单位:秒),默认 7 天,-1 表示永不过期
jwt:
secret-key: SecretKey012345678901234567890123456789012345678901234567890123456789 # JWT密钥(HS256算法至少32字符)
redis-token:
allow-multi-login: true # 是否允许多设备登录
# 安全白名单路径,仅跳过 AuthorizationFilter 过滤器,还是会走 Spring Security 的其他过滤器(CSRF、CORS等)
ignore-urls:
- - /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录)
- - /api/v1/auth/captcha # 验证码获取接口
- - /api/v1/auth/refresh-token # 刷新令牌接口
+ - /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录)
+ - /api/v1/auth/captcha # 验证码获取接口
+ - /api/v1/auth/refresh-token # 刷新令牌接口
- //api/v1/auth/wx/miniapp/code-login # 微信小程序code登陆
- - /ws/** # WebSocket接口
+ - /ws/** # WebSocket接口
# 非安全端点路径,完全绕过 Spring Security 的过滤器
unsecured-urls:
- ${springdoc.swagger-ui.path}
@@ -173,7 +186,7 @@ springdoc:
api-docs:
path: /v3/api-docs
group-configs:
- - group: '系统管理'
+ - group: "系统管理"
paths-to-match: "/**"
packages-to-scan:
- com.youlai.boot.auth.controller
@@ -185,9 +198,9 @@ springdoc:
# knife4j 接口文档配置
knife4j:
# 是否开启 Knife4j 增强功能
- enable: true # 设置为 true 表示开启增强功能
+ enable: true # 设置为 true 表示开启增强功能
# 生产环境配置
- production: false # 设置为 true 表示在生产环境中不显示文档,为 false 表示显示文档(通常在开发环境中使用)
+ production: false # 设置为 true 表示在生产环境中不显示文档,为 false 表示显示文档(通常在开发环境中使用)
setting:
language: zh_cn
@@ -243,7 +256,6 @@ wx:
app-id: xxxxxx
app-secret: xxxxxx
-
# ==================== AI 命令系统配置 ====================
ai:
# 是否启用 AI 功能
From 96676f487eae4fce9418ff0f026f4b091f98201e Mon Sep 17 00:00:00 2001
From: "Ray.Hao" <1490493387@qq.com>
Date: Wed, 3 Dec 2025 20:59:38 +0800
Subject: [PATCH 02/22] =?UTF-8?q?refactor(auth):=20=E4=BC=98=E5=8C=96?=
=?UTF-8?q?=E4=BB=A4=E7=89=8C=E7=AE=A1=E7=90=86=E5=92=8C=E5=AE=89=E5=85=A8?=
=?UTF-8?q?=E6=80=A7=E9=AA=8C=E8=AF=81=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../auth/service/impl/AuthServiceImpl.java | 6 +-
.../boot/security/token/JwtTokenManager.java | 74 +++++++++----------
.../security/token/RedisTokenManager.java | 24 +++---
.../boot/security/util/SecurityUtils.java | 2 +-
4 files changed, 49 insertions(+), 57 deletions(-)
diff --git a/src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java b/src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java
index b19f68a1..259a26a5 100644
--- a/src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java
+++ b/src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java
@@ -152,10 +152,8 @@ public class AuthServiceImpl implements AuthService {
*/
@Override
public void logout() {
- String token = SecurityUtils.getTokenFromRequest();
- if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX )) {
- token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length());
- // 将JWT令牌加入黑名单
+ String token = SecurityUtils.getAccessToken();
+ if (StrUtil.isNotBlank(token)) {
tokenManager.invalidateToken(token);
// 清除Security上下文
SecurityContextHolder.clearContext();
diff --git a/src/main/java/com/youlai/boot/security/token/JwtTokenManager.java b/src/main/java/com/youlai/boot/security/token/JwtTokenManager.java
index bba10d7c..5f3f5397 100644
--- a/src/main/java/com/youlai/boot/security/token/JwtTokenManager.java
+++ b/src/main/java/com/youlai/boot/security/token/JwtTokenManager.java
@@ -132,49 +132,46 @@ public class JwtTokenManager implements TokenManager {
* @return 是否有效
*/
private boolean validateToken(String token, boolean validateRefreshToken) {
- try {
- JWT jwt = JWTUtil.parseToken(token);
- // 检查 Token 是否有效(验签 + 是否过期)
- boolean isValid = jwt.setKey(secretKey).validate(0);
+ JWT jwt = JWTUtil.parseToken(token);
+ // 检查 Token 是否有效(验签 + 是否过期)
+ boolean isValid = jwt.setKey(secretKey).validate(0);
- if (isValid) {
- JSONObject payloads = jwt.getPayloads();
- // 1. 校验刷新令牌类型(仅在校验刷新令牌场景启用)
- String jti = payloads.getStr(JWTPayload.JWT_ID);
- if (validateRefreshToken) {
- //刷新token需要校验token类别
- boolean isRefreshToken = payloads.getBool(JwtClaimConstants.TOKEN_TYPE);
- if (!isRefreshToken) {
- return false;
- }
- }
- // 2. 校验安全版本号(用于按用户维度失效历史 Token)
- Long userId = payloads.getLong(JwtClaimConstants.USER_ID);
- if (userId != null) {
- // 老版本 Token 可能没有 SECURITY_VERSION 声明,视为 0 版本
- Integer tokenVersionRaw = payloads.getInt(JwtClaimConstants.SECURITY_VERSION);
- int tokenVersion = tokenVersionRaw != null ? tokenVersionRaw : 0;
-
- String versionKey = StrUtil.format(RedisConstants.Auth.USER_SECURITY_VERSION, userId);
- Integer currentVersionRaw = (Integer) redisTemplate.opsForValue().get(versionKey);
- int currentVersion = currentVersionRaw != null ? currentVersionRaw : 0;
-
- // 如果当前版本号比 Token 携带的版本号新,则认为该 Token 已失效
- if (tokenVersion < currentVersion) {
- return false;
- }
- }
-
- // 3. 判断是否在黑名单中,如果在,则返回 false 标识Token无效
- if (Boolean.TRUE.equals(redisTemplate.hasKey(StrUtil.format(RedisConstants.Auth.BLACKLIST_TOKEN, jti)))) {
+ if (isValid) {
+ JSONObject payloads = jwt.getPayloads();
+ // 1. 校验刷新令牌类型(仅在校验刷新令牌场景启用)
+ String jti = payloads.getStr(JWTPayload.JWT_ID);
+ if (validateRefreshToken) {
+ //刷新token需要校验token类别
+ boolean isRefreshToken = payloads.getBool(JwtClaimConstants.TOKEN_TYPE);
+ if (!isRefreshToken) {
return false;
}
}
- return isValid;
- } catch (Exception gitignore) {
- // token 验证
+ // 2. 校验安全版本号(用于按用户维度失效历史 Token)
+ // 场景示例:用户修改密码、被管理员强制下线、手动“踢所有端”后,将用户安全版本号 +1,旧版本 Token 全部失效
+ Long userId = payloads.getLong(JwtClaimConstants.USER_ID);
+ if (userId != null) {
+ // 老版本 Token 可能没有 SECURITY_VERSION 声明,视为 0 版本
+ Integer tokenVersionRaw = payloads.getInt(JwtClaimConstants.SECURITY_VERSION);
+ int tokenVersion = tokenVersionRaw != null ? tokenVersionRaw : 0;
+
+ String versionKey = StrUtil.format(RedisConstants.Auth.USER_SECURITY_VERSION, userId);
+ Integer currentVersionRaw = (Integer) redisTemplate.opsForValue().get(versionKey);
+ int currentVersion = currentVersionRaw != null ? currentVersionRaw : 0;
+
+ // 如果当前版本号比 Token 携带的版本号新,则认为该 Token 已失效
+ if (tokenVersion < currentVersion) {
+ return false;
+ }
+ }
+
+ // 3. 判断是否在黑名单中,如果在,则返回 false 标识Token无效
+ // 场景示例:单点退出登录、后台手动注销某个会话、封禁账号后立即阻断当前 Token 等
+ if (Boolean.TRUE.equals(redisTemplate.hasKey(StrUtil.format(RedisConstants.Auth.BLACKLIST_TOKEN, jti)))) {
+ return false;
+ }
}
- return false;
+ return isValid;
}
/**
@@ -210,7 +207,6 @@ public class JwtTokenManager implements TokenManager {
// 永不过期的Token永久加入黑名单
redisTemplate.opsForValue().set(blacklistTokenKey, Boolean.TRUE);
}
- ;
}
/**
diff --git a/src/main/java/com/youlai/boot/security/token/RedisTokenManager.java b/src/main/java/com/youlai/boot/security/token/RedisTokenManager.java
index 7efefaae..9c899156 100644
--- a/src/main/java/com/youlai/boot/security/token/RedisTokenManager.java
+++ b/src/main/java/com/youlai/boot/security/token/RedisTokenManager.java
@@ -165,8 +165,8 @@ public class RedisTokenManager implements TokenManager {
*/
@Override
public void invalidateToken(String token) {
- OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(formatTokenKey(token));
- if (onlineUser != null) {
+ Object value = redisTemplate.opsForValue().get(formatTokenKey(token));
+ if (value instanceof OnlineUser onlineUser) {
Long userId = onlineUser.getUserId();
invalidateUserSessions(userId);
}
@@ -186,20 +186,18 @@ public class RedisTokenManager implements TokenManager {
// 1. 删除访问令牌相关
String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId);
Object accessTokenValue = redisTemplate.opsForValue().get(userAccessKey);
- Optional.of(accessTokenValue)
- .map(String.class::cast)
- .ifPresent(accessToken -> redisTemplate.delete(formatTokenKey(accessToken)));
+ if (accessTokenValue instanceof String accessToken) {
+ redisTemplate.delete(formatTokenKey(accessToken));
+ }
// 无论是否存在访问令牌映射,都尝试删除 userAccessKey
redisTemplate.delete(userAccessKey);
// 2. 删除刷新令牌相关
String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId);
Object refreshTokenValue = redisTemplate.opsForValue().get(userRefreshKey);
- Optional.of(refreshTokenValue)
- .map(String.class::cast)
- .ifPresent(refreshToken ->
- redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken))
- );
+ if (refreshTokenValue instanceof String refreshToken) {
+ redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken));
+ }
// 同样清理 userRefreshKey 本身
redisTemplate.delete(userRefreshKey);
}
@@ -237,9 +235,9 @@ public class RedisTokenManager implements TokenManager {
// 单设备登录控制,删除旧的访问令牌
if (!allowMultiLogin) {
Object oldAccessTokenValue = redisTemplate.opsForValue().get(userAccessKey);
- Optional.of(oldAccessTokenValue)
- .map(String.class::cast)
- .ifPresent(oldAccessToken -> redisTemplate.delete(formatTokenKey(oldAccessToken)));
+ if (oldAccessTokenValue instanceof String oldAccessToken) {
+ redisTemplate.delete(formatTokenKey(oldAccessToken));
+ }
}
// 存储访问令牌映射(用户ID -> 访问令牌),用于单设备登录控制删除旧的访问令牌和刷新令牌时删除旧令牌
setRedisValue(userAccessKey, accessToken, securityProperties.getSession().getAccessTokenTimeToLive());
diff --git a/src/main/java/com/youlai/boot/security/util/SecurityUtils.java b/src/main/java/com/youlai/boot/security/util/SecurityUtils.java
index 48372e53..94ce0434 100644
--- a/src/main/java/com/youlai/boot/security/util/SecurityUtils.java
+++ b/src/main/java/com/youlai/boot/security/util/SecurityUtils.java
@@ -113,7 +113,7 @@ public class SecurityUtils {
*
* @return Token 字符串
*/
- public static String getTokenFromRequest() {
+ public static String getAccessToken() {
ServletRequestAttributes servletRequestAttributes = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
if(Objects.isNull(servletRequestAttributes)) {
return null;
From e1d7f0371d303fd57ef6064efabae65a5f4e8df7 Mon Sep 17 00:00:00 2001
From: theo <971366405@qq.com>
Date: Thu, 4 Dec 2025 11:26:27 +0800
Subject: [PATCH 03/22] =?UTF-8?q?feat(database):=20=E6=94=AF=E6=8C=81?=
=?UTF-8?q?=E8=BE=BE=E6=A2=A6=E6=95=B0=E6=8D=AE=E5=BA=93=E9=80=82=E9=85=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 在多个Mapper XML文件中增加对达梦数据库的SQL语句适配
- 为AiCommandRecordMapper.xml中的model字段添加反引号避免关键字冲突
- 为LogMapper.xml、UserMapper.xml和DatabaseMapper.xml添加databaseId属性区分不同数据库
- 新增MybatisConfig配置类用于识别数据库类型并注册VendorDatabaseIdProvider
- pom.xml中引入达梦数据库驱动依赖DmJdbcDriver8
- 完善RoleMapper接口注释明确返回值类型
---
pom.xml | 6 +
.../com/youlai/boot/config/MybatisConfig.java | 17 +++
.../youlai/boot/system/mapper/LogMapper.java | 3 +-
.../youlai/boot/system/mapper/RoleMapper.java | 2 +-
.../mapper/ai/AiCommandRecordMapper.xml | 2 +-
.../mapper/codegen/DatabaseMapper.xml | 84 ++++++++++-
.../resources/mapper/system/LogMapper.xml | 136 +++++++++++++++---
.../resources/mapper/system/UserMapper.xml | 107 +++++++++++++-
8 files changed, 321 insertions(+), 36 deletions(-)
diff --git a/pom.xml b/pom.xml
index 12fb26ce..3a51ac4b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -274,6 +274,12 @@
1.0.0-M6
+
+ com.dameng
+ DmJdbcDriver8
+ 8.1.4.181
+
+
diff --git a/src/main/java/com/youlai/boot/config/MybatisConfig.java b/src/main/java/com/youlai/boot/config/MybatisConfig.java
index 7cc08b39..537cb7e5 100644
--- a/src/main/java/com/youlai/boot/config/MybatisConfig.java
+++ b/src/main/java/com/youlai/boot/config/MybatisConfig.java
@@ -7,11 +7,15 @@ import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionIntercepto
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.youlai.boot.plugin.mybatis.MyDataPermissionHandler;
import com.youlai.boot.plugin.mybatis.MyMetaObjectHandler;
+import org.apache.ibatis.mapping.DatabaseIdProvider;
+import org.apache.ibatis.mapping.VendorDatabaseIdProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
+import java.util.Properties;
+
/**
* mybatis-plus 配置类
*
@@ -57,4 +61,17 @@ public class MybatisConfig {
return globalConfig;
}
+ /**
+ * 数据库类型自动识别
+ */
+ @Bean
+ public DatabaseIdProvider databaseIdProvider() {
+ DatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
+ Properties properties = new Properties();
+ properties.setProperty("DM", "dm");
+ properties.setProperty("MySQL", "mysql");
+ databaseIdProvider.setProperties(properties);
+ return databaseIdProvider;
+ }
+
}
diff --git a/src/main/java/com/youlai/boot/system/mapper/LogMapper.java b/src/main/java/com/youlai/boot/system/mapper/LogMapper.java
index 50607b8a..a52ae257 100644
--- a/src/main/java/com/youlai/boot/system/mapper/LogMapper.java
+++ b/src/main/java/com/youlai/boot/system/mapper/LogMapper.java
@@ -1,13 +1,12 @@
package com.youlai.boot.system.mapper;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.system.model.bo.VisitCount;
import com.youlai.boot.system.model.bo.VisitStatsBO;
import com.youlai.boot.system.model.entity.Log;
import com.youlai.boot.system.model.query.LogPageQuery;
import com.youlai.boot.system.model.vo.LogPageVO;
-import com.youlai.boot.system.model.vo.VisitStatsVO;
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
diff --git a/src/main/java/com/youlai/boot/system/mapper/RoleMapper.java b/src/main/java/com/youlai/boot/system/mapper/RoleMapper.java
index 69a2cff4..7db98cfd 100644
--- a/src/main/java/com/youlai/boot/system/mapper/RoleMapper.java
+++ b/src/main/java/com/youlai/boot/system/mapper/RoleMapper.java
@@ -19,7 +19,7 @@ public interface RoleMapper extends BaseMapper {
* 获取最大范围的数据权限
*
* @param roles 角色编码集合
- * @return
+ * @return {@link Integer} – 数据权限范围
*/
Integer getMaximumDataScope(Set roles);
}
diff --git a/src/main/resources/mapper/ai/AiCommandRecordMapper.xml b/src/main/resources/mapper/ai/AiCommandRecordMapper.xml
index 0a065278..d23d1c26 100644
--- a/src/main/resources/mapper/ai/AiCommandRecordMapper.xml
+++ b/src/main/resources/mapper/ai/AiCommandRecordMapper.xml
@@ -45,7 +45,7 @@
acr.username,
acr.original_command,
acr.provider,
- acr.model,
+ acr.`model`,
acr.parse_success,
acr.function_calls,
acr.explanation,
diff --git a/src/main/resources/mapper/codegen/DatabaseMapper.xml b/src/main/resources/mapper/codegen/DatabaseMapper.xml
index d923000c..c2aefee0 100644
--- a/src/main/resources/mapper/codegen/DatabaseMapper.xml
+++ b/src/main/resources/mapper/codegen/DatabaseMapper.xml
@@ -2,10 +2,11 @@
+
-
-
diff --git a/src/main/resources/mapper/system/LogMapper.xml b/src/main/resources/mapper/system/LogMapper.xml
index 29d8c504..ad3a205b 100644
--- a/src/main/resources/mapper/system/LogMapper.xml
+++ b/src/main/resources/mapper/system/LogMapper.xml
@@ -2,10 +2,11 @@
+
-
-
+
+
SELECT
t1.id,
t1.module,
@@ -46,6 +47,48 @@
ORDER BY
t1.create_time DESC
+
+
+ SELECT
+ t1.id,
+ t1.module,
+ t1.content,
+ t1.request_uri,
+ t1.ip,
+ t1.province || ' ' || t1.city AS region,
+ t1.execution_time,
+ t1.browser || ' ' || t1.browser_version AS browser,
+ t1.os,
+ t1.create_time,
+ t2.nickname AS operator
+ FROM
+ sys_log t1
+ LEFT JOIN sys_user t2 ON t1.create_by = t2.id
+
+ t1.is_deleted = 0
+
+ AND (
+ t1.content LIKE concat('%',#{queryParams.keywords},'%')
+ OR
+ t1.ip LIKE concat('%',#{queryParams.keywords},'%')
+ OR
+ t2.nickname LIKE concat('%',#{queryParams.keywords},'%')
+ )
+
+
+
+
+ AND t1.create_time >= #{startDate}
+
+
+
+ AND t1.create_time <= #{endDate}
+
+
+
+ ORDER BY
+ t1.create_time DESC
+
@@ -75,44 +118,91 @@
DATE_FORMAT(create_time, '%Y-%m-%d')
-
-
+
+
SELECT
COUNT(CASE WHEN DATE(create_time) = CURDATE() THEN 1 END) AS todayCount,
COUNT(*) AS totalCount,
+ ROUND(
+ CASE
+ WHEN COUNT(CASE WHEN DATE(create_time) = CURDATE() - INTERVAL 1 DAY AND TIME(create_time) <= TIME(NOW()) THEN 1 END) = 0 THEN 0
+ ELSE
+ (COUNT(CASE WHEN DATE(create_time) = CURDATE() THEN 1 END) -
+ COUNT(CASE WHEN DATE(create_time) = CURDATE() - INTERVAL 1 DAY AND TIME(create_time) <= TIME(NOW()) THEN 1 END)) /
+ COUNT(CASE WHEN DATE(create_time) = CURDATE() - INTERVAL 1 DAY AND TIME(create_time) <= TIME(NOW()) THEN 1 END)
+ END,
+ 2) AS growthRate
+ FROM
+ sys_log
+ WHERE
+ is_deleted = 0
+
+
+
+ SELECT
+ COUNT(CASE WHEN TRUNC(create_time) = TRUNC(SYSDATE) THEN 1 END) AS todayCount,
+ COUNT(*) AS totalCount,
ROUND(
CASE
- WHEN COUNT(CASE WHEN DATE(create_time) = CURDATE() - INTERVAL 1 DAY AND TIME(create_time) <= TIME(NOW()) THEN 1 END) = 0 THEN 0
+ WHEN COUNT(CASE WHEN TRUNC(create_time) = TRUNC(SYSDATE) - 1
+ AND TO_CHAR(create_time, 'HH24:MI:SS') <= TO_CHAR(SYSDATE, 'HH24:MI:SS') THEN 1 END) = 0 THEN 0
ELSE
- (COUNT(CASE WHEN DATE(create_time) = CURDATE() THEN 1 END) -
- COUNT(CASE WHEN DATE(create_time) = CURDATE() - INTERVAL 1 DAY AND TIME(create_time) <= TIME(NOW()) THEN 1 END)) /
- COUNT(CASE WHEN DATE(create_time) = CURDATE() - INTERVAL 1 DAY AND TIME(create_time) <= TIME(NOW()) THEN 1 END)
+ (COUNT(CASE WHEN TRUNC(create_time) = TRUNC(SYSDATE) THEN 1 END) -
+ COUNT(CASE WHEN TRUNC(create_time) = TRUNC(SYSDATE) - 1
+ AND TO_CHAR(create_time, 'HH24:MI:SS') <= TO_CHAR(SYSDATE, 'HH24:MI:SS') THEN 1 END)) /
+ COUNT(CASE WHEN TRUNC(create_time) = TRUNC(SYSDATE) - 1
+ AND TO_CHAR(create_time, 'HH24:MI:SS') <= TO_CHAR(SYSDATE, 'HH24:MI:SS') THEN 1 END)
END,
2) AS growthRate
FROM
- sys_log
+ sys_log
WHERE
- is_deleted = 0
+ is_deleted = 0
-
-
- SELECT
- COUNT(DISTINCT CASE WHEN DATE(create_time) = CURDATE() THEN ip END) AS todayCount,
- COUNT(DISTINCT ip) AS totalCount,
- ROUND(
- CASE
- WHEN COUNT(DISTINCT CASE WHEN DATE(create_time) = CURDATE() - INTERVAL 1 DAY AND TIME(create_time) <= TIME(NOW()) THEN ip END) = 0 THEN 0
+
+
+ SELECT
+ COUNT(DISTINCT CASE WHEN DATE(create_time) = CURDATE() THEN ip END) AS todayCount,
+ COUNT(DISTINCT ip) AS totalCount,
+ ROUND(
+ CASE
+ WHEN COUNT(DISTINCT CASE WHEN DATE(create_time) = CURDATE() - INTERVAL 1 DAY AND TIME(create_time) <= TIME(NOW()) THEN ip END) = 0 THEN 0
ELSE
(COUNT(DISTINCT CASE WHEN DATE(create_time) = CURDATE() THEN ip END) -
- COUNT(DISTINCT CASE WHEN DATE(create_time) = CURDATE() - INTERVAL 1 DAY AND TIME(create_time) <= TIME(NOW()) THEN ip END)) /
+ COUNT(DISTINCT CASE WHEN DATE(create_time) = CURDATE() - INTERVAL 1 DAY AND TIME(create_time) <= TIME(NOW()) THEN ip END)) /
COUNT(DISTINCT CASE WHEN DATE(create_time) = CURDATE() - INTERVAL 1 DAY AND TIME(create_time) <= TIME(NOW()) THEN ip END)
END,
2) AS growthRate
- FROM
- sys_log
- WHERE
- is_deleted = 0
+ FROM
+ sys_log
+ WHERE
+ is_deleted = 0
+
+
+
+ SELECT COUNT(DISTINCT CASE WHEN TRUNC(create_time) = TRUNC(SYSDATE) THEN ip END) AS todayCount,
+ COUNT(DISTINCT ip) AS totalCount,
+ ROUND(
+ CASE
+ WHEN COUNT(DISTINCT CASE
+ WHEN TRUNC(create_time) = TRUNC(SYSDATE - 1)
+ THEN ip END) = 0
+ THEN 0
+ ELSE
+ (COUNT(DISTINCT CASE
+ WHEN TRUNC(create_time) = TRUNC(SYSDATE)
+ THEN ip END) -
+ COUNT(DISTINCT CASE
+ WHEN TRUNC(create_time) = TRUNC(SYSDATE - 1)
+ THEN ip END)) * 1.0 /
+ COUNT(DISTINCT CASE
+ WHEN TRUNC(create_time) = TRUNC(SYSDATE - 1)
+ THEN ip END)
+ END,
+ 2) AS growthRate
+ FROM sys_log
+ WHERE is_deleted = 0
diff --git a/src/main/resources/mapper/system/UserMapper.xml b/src/main/resources/mapper/system/UserMapper.xml
index 4e281a17..7ea2f9f7 100644
--- a/src/main/resources/mapper/system/UserMapper.xml
+++ b/src/main/resources/mapper/system/UserMapper.xml
@@ -2,10 +2,11 @@
+
-
-
+
+
SELECT
u.id,
u.username,
@@ -80,6 +81,82 @@
+
+
+ SELECT
+ u.id,
+ u.username,
+ u.nickname,
+ u.mobile,
+ u.gender,
+ u.avatar,
+ u.STATUS,
+ u.email,
+ d.NAME AS dept_name,
+ LISTAGG(r.name, ',') WITHIN GROUP (ORDER BY r.name) AS roleNames,
+ u.create_time
+ FROM
+ sys_user u
+ LEFT JOIN sys_dept d ON u.dept_id = d.id
+ LEFT JOIN sys_user_role sur ON u.id = sur.user_id
+ LEFT JOIN sys_role r ON sur.role_id = r.id
+
+ u.is_deleted = 0
+
+
+ AND NOT EXISTS (
+ SELECT
+ 1
+ FROM sys_user_role sur
+ INNER JOIN sys_role r ON sur.role_id = r.id
+ WHERE
+ sur.user_id = u.id
+ AND r.code = '${@com.youlai.boot.common.constant.SystemConstants@ROOT_ROLE_CODE}'
+ )
+
+
+ AND (
+ u.username LIKE CONCAT('%',#{queryParams.keywords},'%')
+ OR u.nickname LIKE CONCAT('%',#{queryParams.keywords},'%')
+ OR u.mobile LIKE CONCAT('%',#{queryParams.keywords},'%')
+ )
+
+
+ AND u.status = #{queryParams.status}
+
+
+ AND concat(',',concat(d.tree_path,',',d.id),',') like concat('%,',#{queryParams.deptId},',%')
+
+
+
+
+ AND u.create_time >= #{startDate}
+
+
+
+ AND u.create_time <= #{endDate}
+
+
+
+ AND sur.role_id IN
+
+ #{roleId}
+
+
+
+ GROUP BY
+ u.id
+
+
+
+ ORDER BY u.${queryParams.field} ${queryParams.direction}
+
+
+
+ ORDER BY u.update_time DESC, u.create_time DESC
+
+
+
@@ -224,8 +301,8 @@
GROUP BY u.id
-
-
+
+
SELECT
u.id,
u.username,
@@ -246,5 +323,27 @@
WHERE
u.id = #{userId} AND u.is_deleted = 0
+
+
+ SELECT
+ u.id,
+ u.username,
+ u.nickname,
+ u.mobile,
+ u.gender,
+ u.avatar,
+ u.STATUS,
+ u.email,
+ d.NAME AS deptName,
+ LISTAGG(r.name, ',') WITHIN GROUP (ORDER BY r.name) AS roleNames,
+ u.create_time
+ FROM
+ sys_user u
+ LEFT JOIN sys_dept d ON u.dept_id = d.id
+ LEFT JOIN sys_user_role sur ON u.id = sur.user_id
+ LEFT JOIN sys_role r ON sur.role_id = r.id
+ WHERE
+ u.id = #{userId} AND u.is_deleted = 0
+
From caf4f4e5c0b82ea6248460824387b496e55cb3a3 Mon Sep 17 00:00:00 2001
From: "Ray.Hao" <1490493387@qq.com>
Date: Sun, 7 Dec 2025 23:45:14 +0800
Subject: [PATCH 04/22] =?UTF-8?q?refactor:=20=E6=8E=A5=E5=8F=A3=E8=B7=AF?=
=?UTF-8?q?=E5=BE=84=E8=A7=84=E8=8C=83=E8=B0=83=E6=95=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../com/youlai/boot/system/controller/ConfigController.java | 2 +-
.../java/com/youlai/boot/system/controller/DeptController.java | 2 +-
src/main/resources/application-dev.yml | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/main/java/com/youlai/boot/system/controller/ConfigController.java b/src/main/java/com/youlai/boot/system/controller/ConfigController.java
index 1e8d446b..dbb4b81e 100644
--- a/src/main/java/com/youlai/boot/system/controller/ConfigController.java
+++ b/src/main/java/com/youlai/boot/system/controller/ConfigController.java
@@ -29,7 +29,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
@RestController
@RequiredArgsConstructor
@Tag(name = "08.系统配置")
-@RequestMapping("/api/v1/config")
+@RequestMapping("/api/v1/configs")
public class ConfigController {
private final ConfigService configService;
diff --git a/src/main/java/com/youlai/boot/system/controller/DeptController.java b/src/main/java/com/youlai/boot/system/controller/DeptController.java
index f1064353..bb001700 100644
--- a/src/main/java/com/youlai/boot/system/controller/DeptController.java
+++ b/src/main/java/com/youlai/boot/system/controller/DeptController.java
@@ -27,7 +27,7 @@ import java.util.List;
*/
@Tag(name = "05.部门接口")
@RestController
-@RequestMapping("/api/v1/dept")
+@RequestMapping("/api/v1/depts")
@RequiredArgsConstructor
public class DeptController {
diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml
index 54a00f90..df4c8e98 100644
--- a/src/main/resources/application-dev.yml
+++ b/src/main/resources/application-dev.yml
@@ -1,5 +1,5 @@
server:
- port: 8989
+ port: 8000
# 数据库类型:用于 MyBatis-Plus 分页方言等(仅方言,不负责连接信息)
app:
From 1f650fb4699efbc0411a56c7154ccb84f0245a7a Mon Sep 17 00:00:00 2001
From: "Ray.Hao" <1490493387@qq.com>
Date: Wed, 10 Dec 2025 11:52:55 +0800
Subject: [PATCH 05/22] =?UTF-8?q?refactor(ai):=20=E9=87=8D=E6=9E=84AI?=
=?UTF-8?q?=E5=91=BD=E4=BB=A4=E8=AE=B0=E5=BD=95=E6=A8=A1=E5=9D=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 将AiCommandRecord重命名为AiCommandLog
- 更新相关控制器、服务、映射器和实体类
---
sql/mysql/youlai_boot.sql | 44 ++----
.../ai/controller/AiCommandController.java | 16 +--
...ordMapper.java => AiCommandLogMapper.java} | 12 +-
...AiCommandRecord.java => AiCommandLog.java} | 54 ++-----
.../ai/model/query/AiCommandPageQuery.java | 14 +-
...mmandRecordVO.java => AiCommandLogVO.java} | 52 ++-----
...dService.java => AiCommandLogService.java} | 16 ++-
.../service/impl/AiCommandLogServiceImpl.java | 49 +++++++
.../impl/AiCommandRecordServiceImpl.java | 47 ------
.../ai/service/impl/AiCommandServiceImpl.java | 134 ++++++------------
.../mapper/ai/AiCommandLogMapper.xml | 96 +++++++++++++
.../mapper/ai/AiCommandRecordMapper.xml | 111 ---------------
12 files changed, 262 insertions(+), 383 deletions(-)
rename src/main/java/com/youlai/boot/platform/ai/mapper/{AiCommandRecordMapper.java => AiCommandLogMapper.java} (57%)
rename src/main/java/com/youlai/boot/platform/ai/model/entity/{AiCommandRecord.java => AiCommandLog.java} (58%)
rename src/main/java/com/youlai/boot/platform/ai/model/vo/{AiCommandRecordVO.java => AiCommandLogVO.java} (63%)
rename src/main/java/com/youlai/boot/platform/ai/service/{AiCommandRecordService.java => AiCommandLogService.java} (55%)
create mode 100644 src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandLogServiceImpl.java
delete mode 100644 src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandRecordServiceImpl.java
create mode 100644 src/main/resources/mapper/ai/AiCommandLogMapper.xml
delete mode 100644 src/main/resources/mapper/ai/AiCommandRecordMapper.xml
diff --git a/sql/mysql/youlai_boot.sql b/sql/mysql/youlai_boot.sql
index cd5c268a..dc52c6cd 100644
--- a/sql/mysql/youlai_boot.sql
+++ b/sql/mysql/youlai_boot.sql
@@ -574,53 +574,37 @@ INSERT INTO `sys_user_notice` VALUES (10, 10, 2, 1, NULL, now(), now(), 0);
-- ----------------------------
-- AI 命令记录表
-- ----------------------------
-DROP TABLE IF EXISTS `ai_command_record`;
-CREATE TABLE `ai_command_record` (
+DROP TABLE IF EXISTS `ai_command_log`;
+CREATE TABLE `ai_command_log` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` bigint DEFAULT NULL COMMENT '用户ID',
`username` varchar(64) DEFAULT NULL COMMENT '用户名',
`original_command` text COMMENT '原始命令',
- -- 解析相关字段
- `provider` varchar(32) DEFAULT NULL COMMENT 'AI供应商(qwen/openai/deepseek/gemini等)',
- `model` varchar(64) DEFAULT NULL COMMENT 'AI模型(qwen-plus/qwen-max/gpt-4-turbo等)',
- `parse_success` tinyint(1) DEFAULT NULL COMMENT '解析是否成功(0-失败, 1-成功)',
+ `ai_provider` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT 'AI 供应商(qwen/openai/deepseek/gemini等)',
+ `ai_model` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT 'AI 模型名称(qwen-plus/qwen-max/gpt-4-turbo等)',
+ `parse_status` tinyint DEFAULT '0' COMMENT '解析是否成功(0-失败, 1-成功)',
`function_calls` text COMMENT '解析出的函数调用列表(JSON)',
`explanation` varchar(500) DEFAULT NULL COMMENT 'AI的理解说明',
`confidence` decimal(3,2) DEFAULT NULL COMMENT '置信度(0.00-1.00)',
`parse_error_message` text COMMENT '解析错误信息',
`input_tokens` int DEFAULT NULL COMMENT '输入Token数量',
`output_tokens` int DEFAULT NULL COMMENT '输出Token数量',
- `total_tokens` int DEFAULT NULL COMMENT '总Token数量',
- `parse_time` bigint DEFAULT NULL COMMENT '解析耗时(毫秒)',
- -- 执行相关字段
+ `parse_duration_ms` int DEFAULT NULL COMMENT '解析耗时(毫秒)',
`function_name` varchar(255) DEFAULT NULL COMMENT '执行的函数名称',
`function_arguments` text COMMENT '函数参数(JSON)',
- `execute_status` varchar(20) DEFAULT NULL COMMENT '执行状态(pending-待执行, success-成功, failed-失败)',
- `execute_result` text COMMENT '执行结果(JSON)',
+ `execute_status` tinyint(1) DEFAULT NULL COMMENT '执行状态(0-待执行, 1-成功, -1-失败)',
`execute_error_message` text COMMENT '执行错误信息',
- `affected_rows` int DEFAULT NULL COMMENT '影响的记录数',
- `is_dangerous` tinyint(1) DEFAULT '0' COMMENT '是否危险操作(0-否, 1-是)',
- `requires_confirmation` tinyint(1) DEFAULT '0' COMMENT '是否需要确认(0-否, 1-是)',
- `user_confirmed` tinyint(1) DEFAULT NULL COMMENT '用户是否确认(0-否, 1-是)',
- `idempotency_key` varchar(128) DEFAULT NULL COMMENT '幂等性令牌(防止重复执行)',
- `execution_time` bigint DEFAULT NULL COMMENT '执行耗时(毫秒)',
- -- 通用字段
`ip_address` varchar(128) DEFAULT NULL COMMENT 'IP地址',
- `user_agent` varchar(512) DEFAULT NULL COMMENT '用户代理',
- `current_route` varchar(255) DEFAULT NULL COMMENT '当前页面路由',
- `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
- `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
- `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+ `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+ `update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
- UNIQUE KEY `uk_idempotency_key` (`idempotency_key`),
KEY `idx_user_id` (`user_id`),
KEY `idx_create_time` (`create_time`),
- KEY `idx_provider` (`provider`),
- KEY `idx_model` (`model`),
- KEY `idx_parse_success` (`parse_success`),
- KEY `idx_execute_status` (`execute_status`),
- KEY `idx_is_dangerous` (`is_dangerous`)
-) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='AI命令记录表';
+ KEY `idx_provider` (`ai_provider`),
+ KEY `idx_model` (`ai_model`),
+ KEY `idx_parse_success` (`parse_status`),
+ KEY `idx_execute_status` (`execute_status`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI 命令记录表';
SET FOREIGN_KEY_CHECKS = 1;
diff --git a/src/main/java/com/youlai/boot/platform/ai/controller/AiCommandController.java b/src/main/java/com/youlai/boot/platform/ai/controller/AiCommandController.java
index 464dfc19..f64b833f 100644
--- a/src/main/java/com/youlai/boot/platform/ai/controller/AiCommandController.java
+++ b/src/main/java/com/youlai/boot/platform/ai/controller/AiCommandController.java
@@ -7,8 +7,8 @@ import com.youlai.boot.platform.ai.model.dto.AiExecuteRequestDTO;
import com.youlai.boot.platform.ai.model.dto.AiParseRequestDTO;
import com.youlai.boot.platform.ai.model.dto.AiParseResponseDTO;
import com.youlai.boot.platform.ai.model.query.AiCommandPageQuery;
-import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO;
-import com.youlai.boot.platform.ai.service.AiCommandRecordService;
+import com.youlai.boot.platform.ai.model.vo.AiCommandLogVO;
+import com.youlai.boot.platform.ai.service.AiCommandLogService;
import com.youlai.boot.platform.ai.service.AiCommandService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -32,7 +32,7 @@ import org.springframework.web.bind.annotation.*;
public class AiCommandController {
private final AiCommandService aiCommandService;
- private final AiCommandRecordService recordService;
+ private final AiCommandLogService logService;
@Operation(summary = "解析自然语言命令")
@PostMapping("/parse")
@@ -72,17 +72,17 @@ public class AiCommandController {
@Operation(summary = "获取AI命令记录分页列表")
@GetMapping("/records")
- public PageResult getRecordPage(AiCommandPageQuery queryParams) {
- IPage page = recordService.getRecordPage(queryParams);
+ public PageResult getLogPage(AiCommandPageQuery queryParams) {
+ IPage page = logService.getLogPage(queryParams);
return PageResult.success(page);
}
@Operation(summary = "撤销命令执行")
- @PostMapping("/rollback/{recordId}")
+ @PostMapping("/rollback/{logId}")
public Result> rollbackCommand(
- @Parameter(description = "记录ID") @PathVariable String recordId
+ @Parameter(description = "记录ID") @PathVariable String logId
) {
- recordService.rollbackCommand(recordId);
+ logService.rollbackCommand(logId);
return Result.success("撤销成功");
}
diff --git a/src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandRecordMapper.java b/src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandLogMapper.java
similarity index 57%
rename from src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandRecordMapper.java
rename to src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandLogMapper.java
index 40e16903..7f6fbed8 100644
--- a/src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandRecordMapper.java
+++ b/src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandLogMapper.java
@@ -3,21 +3,23 @@ package com.youlai.boot.platform.ai.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.youlai.boot.platform.ai.model.entity.AiCommandRecord;
+import com.youlai.boot.platform.ai.model.entity.AiCommandLog;
import com.youlai.boot.platform.ai.model.query.AiCommandPageQuery;
-import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO;
+import com.youlai.boot.platform.ai.model.vo.AiCommandLogVO;
import org.apache.ibatis.annotations.Mapper;
/**
* AI 命令记录 Mapper
+ *
+ * @author Ray.Hao
+ * @since 3.0.0
*/
@Mapper
-public interface AiCommandRecordMapper extends BaseMapper {
+public interface AiCommandLogMapper extends BaseMapper {
/**
* 获取 AI 命令记录分页列表
*/
- IPage getRecordPage(Page page, AiCommandPageQuery queryParams);
+ IPage getLogPage(Page page, AiCommandPageQuery queryParams);
}
-
diff --git a/src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandRecord.java b/src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandLog.java
similarity index 58%
rename from src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandRecord.java
rename to src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandLog.java
index 8fed3d44..47e1f067 100644
--- a/src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandRecord.java
+++ b/src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandLog.java
@@ -8,15 +8,15 @@ import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/**
- * AI 命令记录实体(合并解析和执行记录)
+ * AI 命令记录实体
*
* @author Ray.Hao
* @since 3.0.0
*/
@Data
@EqualsAndHashCode(callSuper = true)
-@TableName("ai_command_record")
-public class AiCommandRecord extends BaseEntity {
+@TableName("ai_command_log")
+public class AiCommandLog extends BaseEntity {
/** 用户ID */
private Long userId;
@@ -30,13 +30,13 @@ public class AiCommandRecord extends BaseEntity {
// ==================== 解析相关字段 ====================
/** AI 供应商(qwen/openai/deepseek等) */
- private String provider;
+ private String aiProvider;
/** AI 模型(qwen-plus/qwen-max/gpt-4-turbo等) */
- private String model;
+ private String aiModel;
- /** 解析是否成功 */
- private Boolean parseSuccess;
+ /** 解析状态(0-失败, 1-成功) */
+ private Integer parseStatus;
/** 解析出的函数调用列表(JSON) */
private String functionCalls;
@@ -56,11 +56,8 @@ public class AiCommandRecord extends BaseEntity {
/** 输出 Token 数量 */
private Integer outputTokens;
- /** 总 Token 数量 */
- private Integer totalTokens;
-
/** 解析耗时(毫秒) */
- private Long parseTime;
+ private Integer parseDurationMs;
// ==================== 执行相关字段 ====================
@@ -70,46 +67,15 @@ public class AiCommandRecord extends BaseEntity {
/** 函数参数(JSON) */
private String functionArguments;
- /** 执行状态:pending, success, failed */
- private String executeStatus;
-
- /** 执行结果(JSON) */
- private String executeResult;
+ /** 执行状态(0-待执行, 1-成功, -1-失败) */
+ private Integer executeStatus;
/** 执行错误信息 */
private String executeErrorMessage;
- /** 影响的记录数 */
- private Integer affectedRows;
-
- /** 是否危险操作 */
- private Boolean isDangerous;
-
- /** 是否需要确认 */
- private Boolean requiresConfirmation;
-
- /** 用户是否确认 */
- private Boolean userConfirmed;
-
- /** 幂等性令牌(防止重复执行) */
- private String idempotencyKey;
-
- /** 执行耗时(毫秒) */
- private Long executionTime;
-
// ==================== 通用字段 ====================
/** IP 地址 */
private String ipAddress;
-
- /** 用户代理 */
- private String userAgent;
-
- /** 当前页面路由 */
- private String currentRoute;
-
- /** 备注 */
- private String remark;
}
-
diff --git a/src/main/java/com/youlai/boot/platform/ai/model/query/AiCommandPageQuery.java b/src/main/java/com/youlai/boot/platform/ai/model/query/AiCommandPageQuery.java
index 279b915d..0322b09e 100644
--- a/src/main/java/com/youlai/boot/platform/ai/model/query/AiCommandPageQuery.java
+++ b/src/main/java/com/youlai/boot/platform/ai/model/query/AiCommandPageQuery.java
@@ -21,19 +21,25 @@ public class AiCommandPageQuery extends BasePageQuery {
@Schema(description = "关键字(原始命令/函数名称/用户名)")
private String keywords;
- @Schema(description = "执行状态(pending-待执行, success-成功, failed-失败)")
- private String executeStatus;
+ @Schema(description = "执行状态(0-待执行, 1-成功, -1-失败)")
+ private Integer executeStatus;
@Schema(description = "用户ID")
private Long userId;
- @Schema(description = "是否危险操作")
- private Boolean isDangerous;
+ @Schema(description = "解析状态(0-失败, 1-成功)")
+ private Integer parseStatus;
@Schema(description = "创建时间范围")
private List createTime;
@Schema(description = "函数名称")
private String functionName;
+
+ @Schema(description = "AI供应商")
+ private String aiProvider;
+
+ @Schema(description = "AI模型")
+ private String aiModel;
}
diff --git a/src/main/java/com/youlai/boot/platform/ai/model/vo/AiCommandRecordVO.java b/src/main/java/com/youlai/boot/platform/ai/model/vo/AiCommandLogVO.java
similarity index 63%
rename from src/main/java/com/youlai/boot/platform/ai/model/vo/AiCommandRecordVO.java
rename to src/main/java/com/youlai/boot/platform/ai/model/vo/AiCommandLogVO.java
index 83df97ff..0481087d 100644
--- a/src/main/java/com/youlai/boot/platform/ai/model/vo/AiCommandRecordVO.java
+++ b/src/main/java/com/youlai/boot/platform/ai/model/vo/AiCommandLogVO.java
@@ -9,11 +9,14 @@ import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
- * AI命令记录VO(合并解析和执行记录)
+ * AI命令记录VO
+ *
+ * @author Ray.Hao
+ * @since 3.0.0
*/
@Data
@Schema(description = "AI命令记录VO")
-public class AiCommandRecordVO implements Serializable {
+public class AiCommandLogVO implements Serializable {
@Schema(description = "主键ID")
private String id;
@@ -30,13 +33,13 @@ public class AiCommandRecordVO implements Serializable {
// ==================== 解析相关字段 ====================
@Schema(description = "AI供应商")
- private String provider;
+ private String aiProvider;
@Schema(description = "AI模型")
- private String model;
+ private String aiModel;
- @Schema(description = "解析是否成功")
- private Boolean parseSuccess;
+ @Schema(description = "解析状态(0-失败, 1-成功)")
+ private Integer parseStatus;
@Schema(description = "解析出的函数调用列表(JSON)")
private String functionCalls;
@@ -56,11 +59,8 @@ public class AiCommandRecordVO implements Serializable {
@Schema(description = "输出Token数量")
private Integer outputTokens;
- @Schema(description = "总Token数量")
- private Integer totalTokens;
-
@Schema(description = "解析耗时(毫秒)")
- private Long parseTime;
+ private Integer parseDurationMs;
// ==================== 执行相关字段 ====================
@@ -70,41 +70,17 @@ public class AiCommandRecordVO implements Serializable {
@Schema(description = "函数参数(JSON)")
private String functionArguments;
- @Schema(description = "执行状态")
- private String executeStatus;
-
- @Schema(description = "执行结果(JSON)")
- private String executeResult;
+ @Schema(description = "执行状态(0-待执行, 1-成功, -1-失败)")
+ private Integer executeStatus;
@Schema(description = "执行错误信息")
private String executeErrorMessage;
- @Schema(description = "影响的记录数")
- private Integer affectedRows;
-
- @Schema(description = "是否危险操作")
- private Boolean isDangerous;
-
- @Schema(description = "是否需要确认")
- private Boolean requiresConfirmation;
-
- @Schema(description = "用户是否确认")
- private Boolean userConfirmed;
-
- @Schema(description = "执行耗时(毫秒)")
- private Long executionTime;
-
// ==================== 通用字段 ====================
@Schema(description = "IP地址")
private String ipAddress;
- @Schema(description = "用户代理")
- private String userAgent;
-
- @Schema(description = "当前页面路由")
- private String currentRoute;
-
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@@ -112,9 +88,5 @@ public class AiCommandRecordVO implements Serializable {
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
-
- @Schema(description = "备注")
- private String remark;
}
-
diff --git a/src/main/java/com/youlai/boot/platform/ai/service/AiCommandRecordService.java b/src/main/java/com/youlai/boot/platform/ai/service/AiCommandLogService.java
similarity index 55%
rename from src/main/java/com/youlai/boot/platform/ai/service/AiCommandRecordService.java
rename to src/main/java/com/youlai/boot/platform/ai/service/AiCommandLogService.java
index a4d9543a..4628881d 100644
--- a/src/main/java/com/youlai/boot/platform/ai/service/AiCommandRecordService.java
+++ b/src/main/java/com/youlai/boot/platform/ai/service/AiCommandLogService.java
@@ -2,14 +2,17 @@ package com.youlai.boot.platform.ai.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
-import com.youlai.boot.platform.ai.model.entity.AiCommandRecord;
+import com.youlai.boot.platform.ai.model.entity.AiCommandLog;
import com.youlai.boot.platform.ai.model.query.AiCommandPageQuery;
-import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO;
+import com.youlai.boot.platform.ai.model.vo.AiCommandLogVO;
/**
* AI 命令记录服务接口
+ *
+ * @author Ray.Hao
+ * @since 3.0.0
*/
-public interface AiCommandRecordService extends IService {
+public interface AiCommandLogService extends IService {
/**
* 获取命令记录分页列表
@@ -17,14 +20,13 @@ public interface AiCommandRecordService extends IService {
* @param queryParams 查询参数
* @return 命令记录分页列表
*/
- IPage getRecordPage(AiCommandPageQuery queryParams);
+ IPage getLogPage(AiCommandPageQuery queryParams);
/**
* 撤销命令执行
*
- * @param recordId 记录ID
+ * @param logId 记录ID
*/
- void rollbackCommand(String recordId);
+ void rollbackCommand(String logId);
}
-
diff --git a/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandLogServiceImpl.java b/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandLogServiceImpl.java
new file mode 100644
index 00000000..05f1143a
--- /dev/null
+++ b/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandLogServiceImpl.java
@@ -0,0 +1,49 @@
+package com.youlai.boot.platform.ai.service.impl;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.youlai.boot.platform.ai.mapper.AiCommandLogMapper;
+import com.youlai.boot.platform.ai.model.entity.AiCommandLog;
+import com.youlai.boot.platform.ai.model.query.AiCommandPageQuery;
+import com.youlai.boot.platform.ai.model.vo.AiCommandLogVO;
+import com.youlai.boot.platform.ai.service.AiCommandLogService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+/**
+ * AI 命令记录服务实现类
+ *
+ * @author Ray.Hao
+ * @since 3.0.0
+ */
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class AiCommandLogServiceImpl extends ServiceImpl
+ implements AiCommandLogService {
+
+ @Override
+ public IPage getLogPage(AiCommandPageQuery queryParams) {
+ Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize());
+ return this.baseMapper.getLogPage(page, queryParams);
+ }
+
+ @Override
+ public void rollbackCommand(String logId) {
+ AiCommandLog log = this.getById(logId);
+ if (log == null) {
+ throw new RuntimeException("命令记录不存在");
+ }
+
+ if (log.getExecuteStatus() == null || log.getExecuteStatus() != 1) {
+ throw new RuntimeException("只能撤销成功执行的命令");
+ }
+
+ // TODO: 实现具体的回滚逻辑
+ log.info("撤销命令执行: logId={}, function={}", logId, log.getFunctionName());
+ throw new UnsupportedOperationException("回滚功能尚未实现");
+ }
+}
+
diff --git a/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandRecordServiceImpl.java b/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandRecordServiceImpl.java
deleted file mode 100644
index 1a0b87a8..00000000
--- a/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandRecordServiceImpl.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.youlai.boot.platform.ai.service.impl;
-
-import com.baomidou.mybatisplus.core.metadata.IPage;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import com.youlai.boot.platform.ai.mapper.AiCommandRecordMapper;
-import com.youlai.boot.platform.ai.model.entity.AiCommandRecord;
-import com.youlai.boot.platform.ai.model.query.AiCommandPageQuery;
-import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO;
-import com.youlai.boot.platform.ai.service.AiCommandRecordService;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-
-/**
- * AI 命令记录服务实现类
- */
-@Service
-@Slf4j
-@RequiredArgsConstructor
-public class AiCommandRecordServiceImpl extends ServiceImpl
- implements AiCommandRecordService {
-
- @Override
- public IPage getRecordPage(AiCommandPageQuery queryParams) {
- Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize());
- return this.baseMapper.getRecordPage(page, queryParams);
- }
-
- @Override
- public void rollbackCommand(String recordId) {
- AiCommandRecord record = this.getById(recordId);
- if (record == null) {
- throw new RuntimeException("命令记录不存在");
- }
-
- if (!"success".equals(record.getExecuteStatus())) {
- throw new RuntimeException("只能撤销成功执行的命令");
- }
-
- // TODO: 实现具体的回滚逻辑
- log.info("撤销命令执行: recordId={}, function={}", recordId, record.getFunctionName());
- throw new UnsupportedOperationException("回滚功能尚未实现");
- }
-}
-
-
diff --git a/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandServiceImpl.java b/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandServiceImpl.java
index 72e8eb23..27197fa0 100644
--- a/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandServiceImpl.java
+++ b/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandServiceImpl.java
@@ -6,13 +6,12 @@ import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.json.JSONUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.youlai.boot.platform.ai.model.dto.AiExecuteRequestDTO;
import com.youlai.boot.platform.ai.model.dto.AiFunctionCallDTO;
import com.youlai.boot.platform.ai.model.dto.AiParseRequestDTO;
import com.youlai.boot.platform.ai.model.dto.AiParseResponseDTO;
-import com.youlai.boot.platform.ai.model.entity.AiCommandRecord;
-import com.youlai.boot.platform.ai.service.AiCommandRecordService;
+import com.youlai.boot.platform.ai.model.entity.AiCommandLog;
+import com.youlai.boot.platform.ai.service.AiCommandLogService;
import com.youlai.boot.platform.ai.service.AiCommandService;
import com.youlai.boot.platform.ai.tools.UserTools;
import com.youlai.boot.security.util.SecurityUtils;
@@ -51,7 +50,7 @@ public class AiCommandServiceImpl implements AiCommandService {
当无法识别命令时,success=false,并给出 error。
""";
- private final AiCommandRecordService recordService;
+ private final AiCommandLogService logService;
private final UserTools userTools;
private final ChatClient chatClient;
@@ -72,14 +71,13 @@ public class AiCommandServiceImpl implements AiCommandService {
String username = SecurityUtils.getUsername();
String ipAddress = JakartaServletUtil.getClientIP(httpRequest);
- AiCommandRecord record = new AiCommandRecord();
- record.setUserId(userId);
- record.setUsername(username);
- record.setOriginalCommand(command);
- record.setIpAddress(ipAddress);
- record.setCurrentRoute(request.getCurrentRoute());
- record.setProvider("spring-ai");
- record.setModel("auto");
+ AiCommandLog log = new AiCommandLog();
+ log.setUserId(userId);
+ log.setUsername(username);
+ log.setOriginalCommand(command);
+ log.setIpAddress(ipAddress);
+ log.setAiProvider("spring-ai");
+ log.setAiModel("auto");
String systemPrompt = buildSystemPrompt();
String userPrompt = buildUserPrompt(request);
@@ -97,19 +95,20 @@ public class AiCommandServiceImpl implements AiCommandService {
ParseResult parseResult = parseAiResponse(rawContent);
- record.setProvider(StrUtil.emptyToDefault(parseResult.provider(), "spring-ai"));
- record.setModel(StrUtil.emptyToDefault(parseResult.model(), "auto"));
- record.setParseSuccess(parseResult.success());
- record.setExplanation(parseResult.explanation());
- record.setFunctionCalls(JSONUtil.toJsonStr(parseResult.functionCalls()));
- record.setConfidence(parseResult.confidence() != null ? BigDecimal.valueOf(parseResult.confidence()) : null);
- record.setParseErrorMessage(parseResult.success() ? null : StrUtil.emptyToDefault(parseResult.error(), "解析失败"));
- record.setParseTime(System.currentTimeMillis() - startTime);
+ log.setAiProvider(StrUtil.emptyToDefault(parseResult.provider(), "spring-ai"));
+ log.setAiModel(StrUtil.emptyToDefault(parseResult.model(), "auto"));
+ log.setParseStatus(parseResult.success() ? 1 : 0);
+ log.setExplanation(parseResult.explanation());
+ log.setFunctionCalls(JSONUtil.toJsonStr(parseResult.functionCalls()));
+ log.setConfidence(parseResult.confidence() != null ? BigDecimal.valueOf(parseResult.confidence()) : null);
+ log.setParseErrorMessage(parseResult.success() ? null : StrUtil.emptyToDefault(parseResult.error(), "解析失败"));
+ long duration = System.currentTimeMillis() - startTime;
+ log.setParseDurationMs((int) duration);
- recordService.save(record);
+ logService.save(log);
AiParseResponseDTO response = AiParseResponseDTO.builder()
- .parseLogId(record.getId())
+ .parseLogId(log.getId())
.success(parseResult.success())
.functionCalls(parseResult.functionCalls())
.explanation(parseResult.explanation())
@@ -121,17 +120,17 @@ public class AiCommandServiceImpl implements AiCommandService {
if (!parseResult.success()) {
log.warn("❗️ AI 未能解析命令: {}", parseResult.error());
} else {
- log.info("✅ 解析成功,审计记录ID: {}", record.getId());
+ log.info("✅ 解析成功,审计记录ID: {}", log.getId());
}
return response;
} catch (Exception e) {
long duration = System.currentTimeMillis() - startTime;
- record.setParseSuccess(false);
- record.setFunctionCalls(JSONUtil.toJsonStr(Collections.emptyList()));
- record.setParseErrorMessage(e.getMessage());
- record.setParseTime(duration);
- recordService.save(record);
+ log.setParseStatus(0);
+ log.setFunctionCalls(JSONUtil.toJsonStr(Collections.emptyList()));
+ log.setParseErrorMessage(e.getMessage());
+ log.setParseDurationMs((int) duration);
+ logService.save(log);
log.error("❌ 解析命令失败: {}", e.getMessage(), e);
throw new RuntimeException("解析命令失败: " + e.getMessage(), e);
@@ -232,98 +231,59 @@ public class AiCommandServiceImpl implements AiCommandService {
AiFunctionCallDTO functionCall = request.getFunctionCall();
- // 判断是否为危险操作
- boolean isDangerous = isDangerousOperation(functionCall.getName());
-
// 根据解析日志ID获取审计记录,如果不存在则创建新记录
- AiCommandRecord record;
+ AiCommandLog log;
if (StrUtil.isNotBlank(request.getParseLogId())) {
// 更新已存在的审计记录(解析阶段已创建)
- record = recordService.getById(request.getParseLogId());
- if (record == null) {
+ log = logService.getById(request.getParseLogId());
+ if (log == null) {
throw new IllegalStateException("未找到对应的解析记录,ID: " + request.getParseLogId());
}
} else {
// 如果没有解析日志ID,创建新记录(兼容直接执行的情况)
- record = new AiCommandRecord();
- record.setUserId(userId);
- record.setUsername(username);
- record.setOriginalCommand(request.getOriginalCommand());
- record.setIpAddress(ipAddress);
- record.setCurrentRoute(request.getCurrentRoute());
- recordService.save(record);
+ log = new AiCommandLog();
+ log.setUserId(userId);
+ log.setUsername(username);
+ log.setOriginalCommand(request.getOriginalCommand());
+ log.setIpAddress(ipAddress);
+ logService.save(log);
}
// 更新执行相关字段
- record.setFunctionName(functionCall.getName());
- record.setFunctionArguments(JSONUtil.toJsonStr(functionCall.getArguments()));
- record.setIsDangerous(isDangerous);
- record.setRequiresConfirmation(request.getConfirmMode() != null &&
- "manual".equals(request.getConfirmMode()));
- record.setUserConfirmed(request.getUserConfirmed());
- record.setIdempotencyKey(request.getIdempotencyKey());
- record.setUserAgent(httpRequest.getHeader("User-Agent"));
- record.setExecuteStatus("pending");
+ log.setFunctionName(functionCall.getName());
+ log.setFunctionArguments(JSONUtil.toJsonStr(functionCall.getArguments()));
+ log.setExecuteStatus(0); // 0-待执行
try {
- // 幂等性检查
- if (StrUtil.isNotBlank(request.getIdempotencyKey())) {
- AiCommandRecord existing = recordService.getOne(
- new LambdaQueryWrapper()
- .eq(AiCommandRecord::getIdempotencyKey, request.getIdempotencyKey())
- .ne(AiCommandRecord::getId, record.getId()) // 排除当前记录
- );
- if (existing != null) {
- log.warn("⚠️ 检测到重复执行,幂等性令牌: {}", request.getIdempotencyKey());
- throw new IllegalStateException("该操作已执行,请勿重复提交");
- }
- }
-
// 🎯 执行具体的函数调用
Object result = executeFunctionCall(functionCall);
// 更新执行成功
- record.setExecuteStatus("success");
- record.setExecuteResult(JSONUtil.toJsonStr(result));
- record.setExecutionTime(System.currentTimeMillis() - startTime);
+ log.setExecuteStatus(1); // 1-成功
+ log.setExecuteErrorMessage(null);
// 更新审计记录
- recordService.updateById(record);
+ logService.updateById(log);
- log.info("✅ 命令执行成功,审计记录ID: {}", record.getId());
+ log.info("✅ 命令执行成功,审计记录ID: {}", log.getId());
return result;
} catch (Exception e) {
// 更新执行失败
- record.setExecuteStatus("failed");
- record.setExecuteErrorMessage(e.getMessage());
- record.setExecutionTime(System.currentTimeMillis() - startTime);
+ log.setExecuteStatus(-1); // -1-失败
+ log.setExecuteErrorMessage(e.getMessage());
// 更新审计记录
- recordService.updateById(record);
+ logService.updateById(log);
- log.error("❌ 命令执行失败,审计记录ID: {}", record.getId(), e);
+ log.error("❌ 命令执行失败,审计记录ID: {}", log.getId(), e);
// 抛出异常,由 Controller 统一处理
throw e;
}
}
- /**
- * 判断是否为危险操作
- */
- private boolean isDangerousOperation(String functionName) {
- String[] dangerousKeywords = {"delete", "remove", "drop", "truncate", "clear"};
- String lowerName = functionName.toLowerCase();
- for (String keyword : dangerousKeywords) {
- if (lowerName.contains(keyword)) {
- return true;
- }
- }
- return false;
- }
-
/**
* 执行具体的函数调用
*/
diff --git a/src/main/resources/mapper/ai/AiCommandLogMapper.xml b/src/main/resources/mapper/ai/AiCommandLogMapper.xml
new file mode 100644
index 00000000..f006c788
--- /dev/null
+++ b/src/main/resources/mapper/ai/AiCommandLogMapper.xml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SELECT
+ acl.id,
+ acl.user_id,
+ acl.username,
+ acl.original_command,
+ acl.ai_provider,
+ acl.ai_model,
+ acl.parse_status,
+ acl.function_calls,
+ acl.explanation,
+ acl.confidence,
+ acl.parse_error_message,
+ acl.input_tokens,
+ acl.output_tokens,
+ acl.parse_duration_ms,
+ acl.function_name,
+ acl.function_arguments,
+ acl.execute_status,
+ acl.execute_error_message,
+ acl.ip_address,
+ acl.create_time,
+ acl.update_time
+ FROM ai_command_log acl
+
+
+ (
+ acl.original_command LIKE CONCAT('%', #{queryParams.keywords}, '%')
+ OR acl.function_name LIKE CONCAT('%', #{queryParams.keywords}, '%')
+ OR acl.username LIKE CONCAT('%', #{queryParams.keywords}, '%')
+ )
+
+
+ AND acl.execute_status = #{queryParams.executeStatus}
+
+
+ AND acl.user_id = #{queryParams.userId}
+
+
+ AND acl.parse_status = #{queryParams.parseStatus}
+
+
+ AND acl.function_name = #{queryParams.functionName}
+
+
+ AND acl.ai_provider = #{queryParams.aiProvider}
+
+
+ AND acl.ai_model = #{queryParams.aiModel}
+
+
+ AND acl.create_time BETWEEN #{queryParams.createTime[0]} AND #{queryParams.createTime[1]}
+
+
+ ORDER BY acl.create_time DESC
+
+
+
+
diff --git a/src/main/resources/mapper/ai/AiCommandRecordMapper.xml b/src/main/resources/mapper/ai/AiCommandRecordMapper.xml
deleted file mode 100644
index d23d1c26..00000000
--- a/src/main/resources/mapper/ai/AiCommandRecordMapper.xml
+++ /dev/null
@@ -1,111 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- SELECT
- acr.id,
- acr.user_id,
- acr.username,
- acr.original_command,
- acr.provider,
- acr.`model`,
- acr.parse_success,
- acr.function_calls,
- acr.explanation,
- acr.confidence,
- acr.parse_error_message,
- acr.input_tokens,
- acr.output_tokens,
- acr.total_tokens,
- acr.parse_time,
- acr.function_name,
- acr.function_arguments,
- acr.execute_status,
- acr.execute_result,
- acr.execute_error_message,
- acr.affected_rows,
- acr.is_dangerous,
- acr.requires_confirmation,
- acr.user_confirmed,
- acr.execution_time,
- acr.ip_address,
- acr.user_agent,
- acr.current_route,
- acr.create_time,
- acr.update_time,
- acr.remark
- FROM ai_command_record acr
-
-
- (
- acr.original_command LIKE CONCAT('%', #{queryParams.keywords}, '%')
- OR acr.function_name LIKE CONCAT('%', #{queryParams.keywords}, '%')
- OR acr.username LIKE CONCAT('%', #{queryParams.keywords}, '%')
- )
-
-
- AND acr.execute_status = #{queryParams.executeStatus}
-
-
- AND acr.user_id = #{queryParams.userId}
-
-
- AND acr.is_dangerous = #{queryParams.isDangerous}
-
-
- AND acr.function_name = #{queryParams.functionName}
-
-
- AND acr.create_time BETWEEN #{queryParams.createTime[0]} AND #{queryParams.createTime[1]}
-
-
- ORDER BY acr.create_time DESC
-
-
-
-
-
From 1b46b60b3f7f4454731c1eeda312e72f0879096d Mon Sep 17 00:00:00 2001
From: "Ray.Hao" <1490493387@qq.com>
Date: Wed, 10 Dec 2025 11:57:25 +0800
Subject: [PATCH 06/22] =?UTF-8?q?refactor(ai):=20=E9=87=8D=E6=9E=84AI?=
=?UTF-8?q?=E5=91=BD=E4=BB=A4=E6=9C=8D=E5=8A=A1=E5=AE=9E=E7=8E=B0=E7=B1=BB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 重命名变量名 log 为 commandLog 以提高代码可读性
---
.../service/impl/AiCommandLogServiceImpl.java | 8 +-
.../ai/service/impl/AiCommandServiceImpl.java | 530 +++++++++---------
2 files changed, 269 insertions(+), 269 deletions(-)
diff --git a/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandLogServiceImpl.java b/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandLogServiceImpl.java
index 05f1143a..fb0eea05 100644
--- a/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandLogServiceImpl.java
+++ b/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandLogServiceImpl.java
@@ -32,17 +32,17 @@ public class AiCommandLogServiceImpl extends ServiceImpl result.getOutput().getText())
+ .orElse("");
+
+ ParseResult parseResult = parseAiResponse(rawContent);
+
+ commandLog.setAiProvider(StrUtil.emptyToDefault(parseResult.provider(), "spring-ai"));
+ commandLog.setAiModel(StrUtil.emptyToDefault(parseResult.model(), "auto"));
+ commandLog.setParseStatus(parseResult.success() ? 1 : 0);
+ commandLog.setExplanation(parseResult.explanation());
+ commandLog.setFunctionCalls(JSONUtil.toJsonStr(parseResult.functionCalls()));
+ commandLog.setConfidence(parseResult.confidence() != null ? BigDecimal.valueOf(parseResult.confidence()) : null);
+ commandLog.setParseErrorMessage(parseResult.success() ? null : StrUtil.emptyToDefault(parseResult.error(), "解析失败"));
+ long duration = System.currentTimeMillis() - startTime;
+ commandLog.setParseDurationMs((int) duration);
+
+ logService.save(commandLog);
+
+ AiParseResponseDTO response = AiParseResponseDTO.builder()
+ .parseLogId(commandLog.getId())
+ .success(parseResult.success())
+ .functionCalls(parseResult.functionCalls())
+ .explanation(parseResult.explanation())
+ .confidence(parseResult.confidence())
+ .error(parseResult.error())
+ .rawResponse(rawContent)
+ .build();
+
+ if (!parseResult.success()) {
+ log.warn("❗️ AI 未能解析命令: {}", parseResult.error());
+ } else {
+ log.info("✅ 解析成功,审计记录ID: {}", commandLog.getId());
+ }
+
+ return response;
+ } catch (Exception e) {
+ long duration = System.currentTimeMillis() - startTime;
+ commandLog.setParseStatus(0);
+ commandLog.setFunctionCalls(JSONUtil.toJsonStr(Collections.emptyList()));
+ commandLog.setParseErrorMessage(e.getMessage());
+ commandLog.setParseDurationMs((int) duration);
+ logService.save(commandLog);
+
+ log.error("❌ 解析命令失败: {}", e.getMessage(), e);
+ throw new RuntimeException("解析命令失败: " + e.getMessage(), e);
+ }
}
- Long userId = SecurityUtils.getUserId();
- String username = SecurityUtils.getUsername();
- String ipAddress = JakartaServletUtil.getClientIP(httpRequest);
-
- AiCommandLog log = new AiCommandLog();
- log.setUserId(userId);
- log.setUsername(username);
- log.setOriginalCommand(command);
- log.setIpAddress(ipAddress);
- log.setAiProvider("spring-ai");
- log.setAiModel("auto");
-
- String systemPrompt = buildSystemPrompt();
- String userPrompt = buildUserPrompt(request);
-
- try {
- log.info("📤 发送命令至 AI 模型: {}", command);
- ChatResponse chatResponse = chatClient.prompt()
- .system(systemPrompt)
- .user(userPrompt)
- .call().chatResponse();
-
- String rawContent = Optional.ofNullable(chatResponse.getResult())
- .map(result -> result.getOutput().getText())
- .orElse("");
-
- ParseResult parseResult = parseAiResponse(rawContent);
-
- log.setAiProvider(StrUtil.emptyToDefault(parseResult.provider(), "spring-ai"));
- log.setAiModel(StrUtil.emptyToDefault(parseResult.model(), "auto"));
- log.setParseStatus(parseResult.success() ? 1 : 0);
- log.setExplanation(parseResult.explanation());
- log.setFunctionCalls(JSONUtil.toJsonStr(parseResult.functionCalls()));
- log.setConfidence(parseResult.confidence() != null ? BigDecimal.valueOf(parseResult.confidence()) : null);
- log.setParseErrorMessage(parseResult.success() ? null : StrUtil.emptyToDefault(parseResult.error(), "解析失败"));
- long duration = System.currentTimeMillis() - startTime;
- log.setParseDurationMs((int) duration);
-
- logService.save(log);
-
- AiParseResponseDTO response = AiParseResponseDTO.builder()
- .parseLogId(log.getId())
- .success(parseResult.success())
- .functionCalls(parseResult.functionCalls())
- .explanation(parseResult.explanation())
- .confidence(parseResult.confidence())
- .error(parseResult.error())
- .rawResponse(rawContent)
- .build();
-
- if (!parseResult.success()) {
- log.warn("❗️ AI 未能解析命令: {}", parseResult.error());
- } else {
- log.info("✅ 解析成功,审计记录ID: {}", log.getId());
- }
-
- return response;
- } catch (Exception e) {
- long duration = System.currentTimeMillis() - startTime;
- log.setParseStatus(0);
- log.setFunctionCalls(JSONUtil.toJsonStr(Collections.emptyList()));
- log.setParseErrorMessage(e.getMessage());
- log.setParseDurationMs((int) duration);
- logService.save(log);
-
- log.error("❌ 解析命令失败: {}", e.getMessage(), e);
- throw new RuntimeException("解析命令失败: " + e.getMessage(), e);
- }
- }
-
- private String buildSystemPrompt() {
- return SYSTEM_PROMPT;
- }
-
- private String buildUserPrompt(AiParseRequestDTO request) {
- JSONObject payload = JSONUtil.createObj()
- .set("command", request.getCommand())
- .set("currentRoute", request.getCurrentRoute())
- .set("currentComponent", request.getCurrentComponent())
- .set("context", Optional.ofNullable(request.getContext()).orElse(Collections.emptyMap()))
- .set("availableFunctions", availableFunctions());
-
- return StrUtil.format("""
- 请根据以下上下文识别用户意图,并输出符合系统提示要求的 JSON:
- {}
- """, JSONUtil.toJsonPrettyStr(payload));
- }
-
- private List
*
* @author haoxr
* @since 2022/10/14
*/
@Component
+@RequiredArgsConstructor
public class MyMetaObjectHandler implements MetaObjectHandler {
+ @Autowired(required = false)
+ private TenantProperties tenantProperties;
+
/**
- * 新增填充创建时间
+ * 新增填充创建时间、更新时间和租户ID
*
* @param metaObject 元数据
*/
@@ -24,6 +35,27 @@ public class MyMetaObjectHandler implements MetaObjectHandler {
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
+
+ // 如果启用了多租户,自动填充租户ID
+ // 注意:由于 BaseEntity 中 tenantId 字段使用了 exist = false(避免单租户模式报错)
+ // 在启用多租户时,需要通过反射动态修改字段的 exist 属性,或者直接设置值
+ // 但 MyBatis-Plus 的字段映射是静态的,无法动态修改
+ // 因此,我们使用 strictInsertFill,它会自动处理字段映射
+ // 如果字段不存在(exist = false),strictInsertFill 会跳过,不会报错
+ if (tenantProperties != null && Boolean.TRUE.equals(tenantProperties.getEnabled())) {
+ Long tenantId = TenantContextHolder.getTenantId();
+ if (tenantId != null) {
+ // 使用数据库字段名(tenant_id)进行填充
+ // 注意:由于 exist = false,这个填充不会写入数据库
+ // 但多租户的数据隔离是通过 TenantLineHandler 自动添加 WHERE 条件实现的
+ // 所以这里只需要设置实体对象的属性值即可(用于业务逻辑)
+ String propertyName = "tenantId";
+ if (metaObject.hasGetter(propertyName)) {
+ // 直接设置值到实体对象,不依赖字段映射
+ metaObject.setValue(propertyName, tenantId);
+ }
+ }
+ }
}
/**
diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml
index df4c8e98..09335b47 100644
--- a/src/main/resources/application-dev.yml
+++ b/src/main/resources/application-dev.yml
@@ -10,19 +10,19 @@ spring:
type: com.alibaba.druid.pool.DruidDataSource
# === MySQL 数据源(默认启用) ===
driver-class-name: com.mysql.cj.jdbc.Driver
- url: jdbc:mysql://www.youlai.tech:3306/youlai_boot?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true
+ url: jdbc:mysql://www.youlai.tech:3306/youlai_admin?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true
username: youlai
password: 123456
# === PostgreSQL 数据源示例(按需启用) ===
# driver-class-name: org.postgresql.Driver
- # url: jdbc:postgresql://127.0.0.1:5432/youlai_boot
+ # url: jdbc:postgresql://127.0.0.1:5432/youlai_admin
# username: postgres
# password: 123456
# === 达梦 DM 数据源示例(按需启用,注意按实际驱动与 URL 调整) ===
# driver-class-name: dm.jdbc.driver.DmDriver
- # url: jdbc:dm://127.0.0.1:5236?schema=YOULAI_BOOT
+ # url: jdbc:dm://127.0.0.1:5236?schema=YOULAI_ADMIN
# username: SYSDBA
# password: 123456
data:
@@ -264,3 +264,32 @@ wx:
miniapp:
app-id: xxxxxx
app-secret: xxxxxx
+
+# ============================================
+# 多租户配置
+# ============================================
+# 说明:通过 youlai.tenant.enabled 控制是否启用多租户功能
+# 启用后,所有 SQL 查询会自动添加 tenant_id 过滤条件
+# ============================================
+youlai:
+ tenant:
+ # 是否启用多租户功能(默认:false)
+ # 设置为 true 启用多租户,设置为 false 禁用多租户(零成本切换)
+ enabled: false
+
+ # 租户字段名(默认:tenant_id)
+ column: tenant_id
+
+ # 默认租户ID(用于兼容旧数据,tenant_id 为 NULL 时使用)
+ default-tenant-id: 1
+
+ # 请求头中的租户ID字段名(默认:tenant-id)
+ header-name: tenant-id
+
+ # 忽略多租户过滤的表名列表(系统表、租户表等不需要租户隔离的表)
+ ignore-tables:
+ - sys_tenant # 租户表本身
+ - sys_dict # 字典表(通常共享)
+ - sys_dict_item # 字典项表(通常共享)
+ - sys_config # 系统配置表(通常共享)
+# ============================================
\ No newline at end of file
diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml
index b92bcc1a..2dcf4b99 100644
--- a/src/main/resources/application-prod.yml
+++ b/src/main/resources/application-prod.yml
@@ -6,19 +6,19 @@ spring:
type: com.alibaba.druid.pool.DruidDataSource
# === MySQL 数据源(默认启用) ===
driver-class-name: com.mysql.cj.jdbc.Driver
- url: jdbc:mysql://www.youlai.tech:3306/youlai_boot?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true
+ url: jdbc:mysql://www.youlai.tech:3306/youlai_admin?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true
username: youlai
password: 123456
# === PostgreSQL 数据源示例(按需启用) ===
# driver-class-name: org.postgresql.Driver
- # url: jdbc:postgresql://127.0.0.1:5432/youlai_boot
+ # url: jdbc:postgresql://127.0.0.1:5432/youlai_admin
# username: postgres
# password: 123456
# === 达梦 DM 数据源示例(按需启用,注意按实际驱动与 URL 调整) ===
# driver-class-name: dm.jdbc.driver.DmDriver
- # url: jdbc:dm://127.0.0.1:5236?schema=YOULAI_BOOT
+ # url: jdbc:dm://127.0.0.1:5236?schema=YOULAI_ADMIN
# username: SYSDBA
# password: 123456
data:
diff --git a/src/main/resources/mapper/ai/AiCommandLogMapper.xml b/src/main/resources/mapper/ai/AiCommandLogMapper.xml
index f006c788..55a12a2b 100644
--- a/src/main/resources/mapper/ai/AiCommandLogMapper.xml
+++ b/src/main/resources/mapper/ai/AiCommandLogMapper.xml
@@ -94,3 +94,4 @@
+
From 329b3551f77a393e22b190b103eb77863137c482 Mon Sep 17 00:00:00 2001
From: "Ray.Hao" <1490493387@qq.com>
Date: Wed, 10 Dec 2025 21:14:37 +0800
Subject: [PATCH 08/22] =?UTF-8?q?feat(tenant):=20=E5=AE=9E=E7=8E=B0?=
=?UTF-8?q?=E5=A4=9A=E7=A7=9F=E6=88=B7=E5=8A=9F=E8=83=BD=E6=94=AF=E6=8C=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
sql/mysql/tenant_add.sql | 186 +++++
sql/mysql/tenant_remove.sql | 111 +++
sql/mysql/youlai_admin.sql | 648 ++++++++++++++++++
.../boot/common/annotation/IgnoreTenant.java | 22 +
.../common/tenant/TenantContextHolder.java | 80 +++
.../config/property/TenantProperties.java | 62 ++
.../youlai/boot/core/aspect/TenantAspect.java | 46 ++
.../boot/core/filter/TenantContextFilter.java | 72 ++
.../plugin/mybatis/TenantLineHandler.java | 90 +++
.../system/controller/TenantController.java | 110 +++
.../boot/system/mapper/TenantMapper.java | 16 +
.../boot/system/mapper/UserTenantMapper.java | 16 +
.../boot/system/model/entity/Tenant.java | 71 ++
.../boot/system/model/entity/UserTenant.java | 34 +
.../youlai/boot/system/model/vo/TenantVO.java | 48 ++
.../boot/system/service/TenantService.java | 50 ++
.../service/impl/TenantServiceImpl.java | 125 ++++
17 files changed, 1787 insertions(+)
create mode 100644 sql/mysql/tenant_add.sql
create mode 100644 sql/mysql/tenant_remove.sql
create mode 100644 sql/mysql/youlai_admin.sql
create mode 100644 src/main/java/com/youlai/boot/common/annotation/IgnoreTenant.java
create mode 100644 src/main/java/com/youlai/boot/common/tenant/TenantContextHolder.java
create mode 100644 src/main/java/com/youlai/boot/config/property/TenantProperties.java
create mode 100644 src/main/java/com/youlai/boot/core/aspect/TenantAspect.java
create mode 100644 src/main/java/com/youlai/boot/core/filter/TenantContextFilter.java
create mode 100644 src/main/java/com/youlai/boot/plugin/mybatis/TenantLineHandler.java
create mode 100644 src/main/java/com/youlai/boot/system/controller/TenantController.java
create mode 100644 src/main/java/com/youlai/boot/system/mapper/TenantMapper.java
create mode 100644 src/main/java/com/youlai/boot/system/mapper/UserTenantMapper.java
create mode 100644 src/main/java/com/youlai/boot/system/model/entity/Tenant.java
create mode 100644 src/main/java/com/youlai/boot/system/model/entity/UserTenant.java
create mode 100644 src/main/java/com/youlai/boot/system/model/vo/TenantVO.java
create mode 100644 src/main/java/com/youlai/boot/system/service/TenantService.java
create mode 100644 src/main/java/com/youlai/boot/system/service/impl/TenantServiceImpl.java
diff --git a/sql/mysql/tenant_add.sql b/sql/mysql/tenant_add.sql
new file mode 100644
index 00000000..6e6a4f3d
--- /dev/null
+++ b/sql/mysql/tenant_add.sql
@@ -0,0 +1,186 @@
+-- ============================================
+-- 多租户支持 SQL 脚本(为现有系统添加多租户功能)
+-- ============================================
+-- 说明:此脚本用于为现有表添加 tenant_id 字段,启用多租户功能
+-- 适用场景:已有系统需要升级支持多租户
+-- 执行前请确保已备份数据库!
+-- ============================================
+
+USE youlai_admin;
+
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- ============================================
+-- 1. 创建租户表(如果不存在)
+-- ============================================
+DROP TABLE IF EXISTS `sys_tenant`;
+CREATE TABLE `sys_tenant` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '租户ID',
+ `name` varchar(100) NOT NULL COMMENT '租户名称',
+ `code` varchar(50) NOT NULL COMMENT '租户编码(唯一)',
+ `contact_name` varchar(50) DEFAULT NULL COMMENT '联系人姓名',
+ `contact_phone` varchar(20) DEFAULT NULL COMMENT '联系人电话',
+ `contact_email` varchar(100) DEFAULT NULL COMMENT '联系人邮箱',
+ `domain` varchar(100) DEFAULT NULL COMMENT '租户域名(用于域名识别)',
+ `logo` varchar(255) DEFAULT NULL COMMENT '租户Logo',
+ `status` tinyint DEFAULT '1' COMMENT '状态(1-正常 0-禁用)',
+ `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+ `expire_time` datetime DEFAULT NULL COMMENT '过期时间(NULL表示永不过期)',
+ `create_time` datetime COMMENT '创建时间',
+ `update_time` datetime COMMENT '更新时间',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_code` (`code`),
+ UNIQUE KEY `uk_domain` (`domain`),
+ KEY `idx_status` (`status`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='系统租户表';
+
+-- 插入默认租户
+INSERT INTO `sys_tenant` (`id`, `name`, `code`, `status`, `create_time`) VALUES
+(1, '默认租户', 'DEFAULT', 1, NOW());
+
+-- ============================================
+-- 2. 创建用户租户关联表(支持一个用户属于多个租户)
+-- ============================================
+DROP TABLE IF EXISTS `sys_user_tenant`;
+CREATE TABLE `sys_user_tenant` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `user_id` bigint NOT NULL COMMENT '用户ID',
+ `tenant_id` bigint NOT NULL COMMENT '租户ID',
+ `is_default` tinyint DEFAULT '0' COMMENT '是否默认租户(1-是 0-否)',
+ `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_user_tenant` (`user_id`, `tenant_id`),
+ KEY `idx_user_id` (`user_id`),
+ KEY `idx_tenant_id` (`tenant_id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户租户关联表(多租户模式)';
+
+-- ============================================
+-- 3. 为业务表添加 tenant_id 字段
+-- ============================================
+-- 注意:MySQL 5.7 不支持 IF NOT EXISTS,如果字段已存在会报错
+-- 建议先检查字段是否存在,或使用 MySQL 8.0+
+
+-- 用户表
+ALTER TABLE `sys_user`
+ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `id`,
+ADD INDEX `idx_tenant_id` (`tenant_id`);
+
+-- 更新现有数据的 tenant_id(设置为默认租户)
+UPDATE `sys_user` SET `tenant_id` = 1 WHERE `tenant_id` IS NULL;
+
+-- 角色表
+ALTER TABLE `sys_role`
+ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `id`,
+ADD INDEX `idx_tenant_id` (`tenant_id`);
+
+UPDATE `sys_role` SET `tenant_id` = 1 WHERE `tenant_id` IS NULL;
+
+-- 部门表
+ALTER TABLE `sys_dept`
+ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `id`,
+ADD INDEX `idx_tenant_id` (`tenant_id`);
+
+UPDATE `sys_dept` SET `tenant_id` = 1 WHERE `tenant_id` IS NULL;
+
+-- 通知公告表
+ALTER TABLE `sys_notice`
+ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `id`,
+ADD INDEX `idx_tenant_id` (`tenant_id`);
+
+UPDATE `sys_notice` SET `tenant_id` = 1 WHERE `tenant_id` IS NULL;
+
+-- 系统日志表
+ALTER TABLE `sys_log`
+ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `id`,
+ADD INDEX `idx_tenant_id` (`tenant_id`);
+
+UPDATE `sys_log` SET `tenant_id` = 1 WHERE `tenant_id` IS NULL;
+
+-- AI 命令记录表
+ALTER TABLE `ai_command_log`
+ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `id`,
+ADD INDEX `idx_tenant_id` (`tenant_id`);
+
+UPDATE `ai_command_log` SET `tenant_id` = 1 WHERE `tenant_id` IS NULL;
+
+-- 代码生成配置表(如果存在)
+-- ALTER TABLE `gen_config`
+-- ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `id`,
+-- ADD INDEX `idx_tenant_id` (`tenant_id`);
+-- UPDATE `gen_config` SET `tenant_id` = 1 WHERE `tenant_id` IS NULL;
+
+-- 代码生成字段配置表(如果存在)
+-- ALTER TABLE `gen_field_config`
+-- ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `id`,
+-- ADD INDEX `idx_tenant_id` (`tenant_id`);
+-- UPDATE `gen_field_config` SET `tenant_id` = 1 WHERE `tenant_id` IS NULL;
+
+-- ============================================
+-- 4. 初始化现有用户的租户关联(默认租户)
+-- ============================================
+INSERT INTO `sys_user_tenant` (`user_id`, `tenant_id`, `is_default`)
+SELECT `id`, 1, 1 FROM `sys_user` WHERE `is_deleted` = 0
+ON DUPLICATE KEY UPDATE `is_default` = 1;
+
+-- ============================================
+-- 5. 添加租户管理菜单和权限(仅在菜单不存在时添加)
+-- ============================================
+-- 租户管理主菜单(放在部门管理之后,字典管理之前,ID=6)
+INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`)
+VALUES (6, 1, '0,1', '租户管理', 1, 'Tenant', 'tenant', 'system/tenant/index', NULL, NULL, NULL, 1, 5, 'el-icon-OfficeBuilding', NULL, NOW(), NOW(), NULL)
+ON DUPLICATE KEY UPDATE `name` = '租户管理';
+
+-- 调整字典管理的排序(从6改为7)
+UPDATE `sys_menu` SET `sort` = 7 WHERE `id` = 7 AND `sort` = 6;
+
+-- 调整字典项的排序(从7改为8)
+UPDATE `sys_menu` SET `sort` = 8 WHERE `id` = 8 AND `sort` = 7;
+
+-- 调整系统日志的排序(从8改为9)
+UPDATE `sys_menu` SET `sort` = 9 WHERE `id` = 9 AND `sort` = 8;
+
+-- 调整系统配置的排序(从9改为10)
+UPDATE `sys_menu` SET `sort` = 10 WHERE `id` = 10 AND `sort` = 9;
+
+-- 调整通知公告的排序(从10改为11)
+UPDATE `sys_menu` SET `sort` = 11 WHERE `id` = 11 AND `sort` = 10;
+
+-- 租户管理权限按钮(ID: 141-145)
+INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`)
+VALUES
+(141, 6, '0,1,6', '租户查询', 4, NULL, '', NULL, 'sys:tenant:query', NULL, NULL, 1, 1, '', NULL, NOW(), NOW(), NULL),
+(142, 6, '0,1,6', '租户新增', 4, NULL, '', NULL, 'sys:tenant:add', NULL, NULL, 1, 2, '', NULL, NOW(), NOW(), NULL),
+(143, 6, '0,1,6', '租户编辑', 4, NULL, '', NULL, 'sys:tenant:edit', NULL, NULL, 1, 3, '', NULL, NOW(), NOW(), NULL),
+(144, 6, '0,1,6', '租户删除', 4, NULL, '', NULL, 'sys:tenant:delete', NULL, NULL, 1, 4, '', NULL, NOW(), NOW(), NULL),
+(145, 6, '0,1,6', '租户启用/禁用', 4, NULL, '', NULL, 'sys:tenant:status', NULL, NULL, 1, 5, '', NULL, NOW(), NOW(), NULL)
+ON DUPLICATE KEY UPDATE `name` = VALUES(`name`);
+
+-- 为系统管理员角色(role_id=2)分配租户管理菜单权限
+INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
+VALUES
+(2, 6),
+(2, 141),
+(2, 142),
+(2, 143),
+(2, 144),
+(2, 145)
+ON DUPLICATE KEY UPDATE `role_id` = VALUES(`role_id`);
+
+SET FOREIGN_KEY_CHECKS = 1;
+
+-- ============================================
+-- 脚本执行完成
+-- ============================================
+-- 执行完成后,请在 application.yml 中配置:
+-- youlai:
+-- tenant:
+-- enabled: true
+-- column: tenant_id
+-- default-tenant-id: 1
+-- header-name: tenant-id
+-- ignore-tables:
+-- - sys_tenant
+-- - sys_dict
+-- - sys_dict_item
+-- - sys_config
+-- ============================================
diff --git a/sql/mysql/tenant_remove.sql b/sql/mysql/tenant_remove.sql
new file mode 100644
index 00000000..d247a4f7
--- /dev/null
+++ b/sql/mysql/tenant_remove.sql
@@ -0,0 +1,111 @@
+-- ============================================
+-- 多租户移除脚本(移除多租户功能)
+-- ============================================
+-- 说明:此脚本用于移除多租户功能,删除 tenant_id 字段和相关表
+-- 适用场景:不再需要多租户功能,需要回退到单租户模式
+-- 执行前请确保已备份数据库!
+-- 警告:此操作不可逆,请谨慎执行!
+-- ============================================
+
+USE youlai_admin;
+
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- ============================================
+-- 1. 删除用户租户关联表
+-- ============================================
+DROP TABLE IF EXISTS `sys_user_tenant`;
+
+-- ============================================
+-- 2. 删除租户表(可选)
+-- ============================================
+-- 注意:如果将来可能再次启用多租户,建议保留此表
+-- 如需删除,取消下面的注释
+-- DROP TABLE IF EXISTS `sys_tenant`;
+
+-- ============================================
+-- 3. 移除业务表的 tenant_id 字段和索引
+-- ============================================
+-- 注意:如果字段不存在会报错,请根据实际情况调整
+
+-- 用户表
+ALTER TABLE `sys_user` DROP INDEX IF EXISTS `idx_tenant_id`;
+ALTER TABLE `sys_user` DROP COLUMN IF EXISTS `tenant_id`;
+
+-- 角色表
+ALTER TABLE `sys_role` DROP INDEX IF EXISTS `idx_tenant_id`;
+ALTER TABLE `sys_role` DROP COLUMN IF EXISTS `tenant_id`;
+
+-- 部门表
+ALTER TABLE `sys_dept` DROP INDEX IF EXISTS `idx_tenant_id`;
+ALTER TABLE `sys_dept` DROP COLUMN IF EXISTS `tenant_id`;
+
+-- 通知公告表
+ALTER TABLE `sys_notice` DROP INDEX IF EXISTS `idx_tenant_id`;
+ALTER TABLE `sys_notice` DROP COLUMN IF EXISTS `tenant_id`;
+
+-- 系统日志表
+ALTER TABLE `sys_log` DROP INDEX IF EXISTS `idx_tenant_id`;
+ALTER TABLE `sys_log` DROP COLUMN IF EXISTS `tenant_id`;
+
+-- AI 命令记录表
+ALTER TABLE `ai_command_log` DROP INDEX IF EXISTS `idx_tenant_id`;
+ALTER TABLE `ai_command_log` DROP COLUMN IF EXISTS `tenant_id`;
+
+-- 代码生成配置表(如果存在)
+-- ALTER TABLE `gen_config` DROP INDEX IF EXISTS `idx_tenant_id`;
+-- ALTER TABLE `gen_config` DROP COLUMN IF EXISTS `tenant_id`;
+
+-- 代码生成字段配置表(如果存在)
+-- ALTER TABLE `gen_field_config` DROP INDEX IF EXISTS `idx_tenant_id`;
+-- ALTER TABLE `gen_field_config` DROP COLUMN IF EXISTS `tenant_id`;
+
+-- 菜单表(如果之前添加了)
+-- ALTER TABLE `sys_menu` DROP INDEX IF EXISTS `idx_tenant_id`;
+-- ALTER TABLE `sys_menu` DROP COLUMN IF EXISTS `tenant_id`;
+
+-- ============================================
+-- 4. 删除租户管理菜单和权限
+-- ============================================
+-- 删除角色菜单关联
+DELETE FROM `sys_role_menu` WHERE `menu_id` IN (6, 141, 142, 143, 144, 145);
+
+-- 删除租户管理权限按钮
+DELETE FROM `sys_menu` WHERE `id` IN (141, 142, 143, 144, 145);
+
+-- 删除租户管理主菜单
+DELETE FROM `sys_menu` WHERE `id` = 6;
+
+-- 恢复字典管理的排序(从7改回6)
+UPDATE `sys_menu` SET `sort` = 6 WHERE `id` = 7 AND `sort` = 7;
+
+-- 恢复字典项的排序(从8改回7)
+UPDATE `sys_menu` SET `sort` = 7 WHERE `id` = 8 AND `sort` = 8;
+
+-- 恢复系统日志的排序(从9改回8)
+UPDATE `sys_menu` SET `sort` = 8 WHERE `id` = 9 AND `sort` = 9;
+
+-- 恢复系统配置的排序(从10改回9)
+UPDATE `sys_menu` SET `sort` = 9 WHERE `id` = 10 AND `sort` = 10;
+
+-- 恢复通知公告的排序(从11改回10)
+UPDATE `sys_menu` SET `sort` = 10 WHERE `id` = 11 AND `sort` = 11;
+
+SET FOREIGN_KEY_CHECKS = 1;
+
+-- ============================================
+-- 脚本执行完成
+-- ============================================
+-- 执行完成后,请执行以下操作:
+-- 1. 在 application.yml 中配置:
+-- youlai:
+-- tenant:
+-- enabled: false
+-- 2. 更新 BaseEntity.java,将 tenantId 字段的 exist 设置为 false
+-- 或移除 tenantId 字段(如果确定不再使用)
+-- ============================================
+-- 注意:
+-- 1. MySQL 5.7 不支持 IF EXISTS 语法,如果执行报错,请手动检查字段是否存在
+-- 2. 对于 MySQL 8.0+,可以使用上面的语法
+-- 3. 如果使用 MySQL 5.7,请先检查字段是否存在,再执行删除操作
+-- ============================================
diff --git a/sql/mysql/youlai_admin.sql b/sql/mysql/youlai_admin.sql
new file mode 100644
index 00000000..9a50c036
--- /dev/null
+++ b/sql/mysql/youlai_admin.sql
@@ -0,0 +1,648 @@
+
+# YouLai_Admin 数据库(MySQL 5.7 ~ MySQL 8.x)
+# Copyright (c) 2021-present, youlai.tech
+
+
+-- ----------------------------
+-- 1. 创建数据库
+-- ----------------------------
+CREATE DATABASE IF NOT EXISTS youlai_admin CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
+
+
+-- ----------------------------
+-- 2. 创建表 && 数据初始化
+-- ----------------------------
+USE youlai_admin;
+
+SET NAMES utf8mb4; # 设置字符集
+SET FOREIGN_KEY_CHECKS = 0; # 关闭外键检查,加快导入速度
+
+-- ----------------------------
+-- Table structure for sys_dept
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_dept`;
+CREATE TABLE `sys_dept` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
+ `name` varchar(100) NOT NULL COMMENT '部门名称',
+ `code` varchar(100) NOT NULL COMMENT '部门编号',
+ `parent_id` bigint DEFAULT 0 COMMENT '父节点id',
+ `tree_path` varchar(255) NOT NULL COMMENT '父节点id路径',
+ `sort` smallint DEFAULT 0 COMMENT '显示顺序',
+ `status` tinyint DEFAULT 1 COMMENT '状态(1-正常 0-禁用)',
+ `create_by` bigint NULL COMMENT '创建人ID',
+ `create_time` datetime NULL COMMENT '创建时间',
+ `update_by` bigint NULL COMMENT '修改人ID',
+ `update_time` datetime NULL COMMENT '更新时间',
+ `is_deleted` tinyint DEFAULT 0 COMMENT '逻辑删除标识(1-已删除 0-未删除)',
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_code`(`code` ASC) USING BTREE COMMENT '部门编号唯一索引'
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '部门管理表';
+
+-- ----------------------------
+-- Records of sys_dept
+-- ----------------------------
+INSERT INTO `sys_dept` VALUES (1, '有来技术', 'YOULAI', 0, '0', 1, 1, 1, NULL, 1, now(), 0);
+INSERT INTO `sys_dept` VALUES (2, '研发部门', 'RD001', 1, '0,1', 1, 1, 2, NULL, 2, now(), 0);
+INSERT INTO `sys_dept` VALUES (3, '测试部门', 'QA001', 1, '0,1', 1, 1, 2, NULL, 2, now(), 0);
+
+-- ----------------------------
+-- Table structure for sys_dict
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_dict`;
+CREATE TABLE `sys_dict` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键 ',
+ `dict_code` varchar(50) COMMENT '类型编码',
+ `name` varchar(50) COMMENT '类型名称',
+ `status` tinyint(1) DEFAULT '0' COMMENT '状态(0:正常;1:禁用)',
+ `remark` varchar(255) COMMENT '备注',
+ `create_time` datetime COMMENT '创建时间',
+ `create_by` bigint COMMENT '创建人ID',
+ `update_time` datetime COMMENT '更新时间',
+ `update_by` bigint COMMENT '修改人ID',
+ `is_deleted` tinyint DEFAULT '0' COMMENT '是否删除(1-删除,0-未删除)',
+ PRIMARY KEY (`id`) USING BTREE,
+ KEY `idx_dict_code` (`dict_code`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据字典类型表';
+-- ----------------------------
+-- Records of sys_dict
+-- ----------------------------
+INSERT INTO `sys_dict` VALUES (1, 'gender', '性别', 1, NULL, now() , 1,now(), 1,0);
+INSERT INTO `sys_dict` VALUES (2, 'notice_type', '通知类型', 1, NULL, now(), 1,now(), 1,0);
+INSERT INTO `sys_dict` VALUES (3, 'notice_level', '通知级别', 1, NULL, now(), 1,now(), 1,0);
+
+
+-- ----------------------------
+-- Table structure for sys_dict_item
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_dict_item`;
+CREATE TABLE `sys_dict_item` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
+ `dict_code` varchar(50) COMMENT '关联字典编码,与sys_dict表中的dict_code对应',
+ `value` varchar(50) COMMENT '字典项值',
+ `label` varchar(100) COMMENT '字典项标签',
+ `tag_type` varchar(50) COMMENT '标签类型,用于前端样式展示(如success、warning等)',
+ `status` tinyint DEFAULT '0' COMMENT '状态(1-正常,0-禁用)',
+ `sort` int DEFAULT '0' COMMENT '排序',
+ `remark` varchar(255) COMMENT '备注',
+ `create_time` datetime COMMENT '创建时间',
+ `create_by` bigint COMMENT '创建人ID',
+ `update_time` datetime COMMENT '更新时间',
+ `update_by` bigint COMMENT '修改人ID',
+ PRIMARY KEY (`id`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据字典项表';
+
+-- ----------------------------
+-- Records of sys_dict_item
+-- ----------------------------
+INSERT INTO `sys_dict_item` VALUES (1, 'gender', '1', '男', 'primary', 1, 1, NULL, now(), 1,now(),1);
+INSERT INTO `sys_dict_item` VALUES (2, 'gender', '2', '女', 'danger', 1, 2, NULL, now(), 1,now(),1);
+INSERT INTO `sys_dict_item` VALUES (3, 'gender', '0', '保密', 'info', 1, 3, NULL, now(), 1,now(),1);
+INSERT INTO `sys_dict_item` VALUES (4, 'notice_type', '1', '系统升级', 'success', 1, 1, '', now(), 1,now(),1);
+INSERT INTO `sys_dict_item` VALUES (5, 'notice_type', '2', '系统维护', 'primary', 1, 2, '', now(), 1,now(),1);
+INSERT INTO `sys_dict_item` VALUES (6, 'notice_type', '3', '安全警告', 'danger', 1, 3, '', now(), 1,now(),1);
+INSERT INTO `sys_dict_item` VALUES (7, 'notice_type', '4', '假期通知', 'success', 1, 4, '', now(), 1,now(),1);
+INSERT INTO `sys_dict_item` VALUES (8, 'notice_type', '5', '公司新闻', 'primary', 1, 5, '', now(), 1,now(),1);
+INSERT INTO `sys_dict_item` VALUES (9, 'notice_type', '99', '其他', 'info', 1, 99, '', now(), 1,now(),1);
+INSERT INTO `sys_dict_item` VALUES (10, 'notice_level', 'L', '低', 'info', 1, 1, '', now(), 1,now(),1);
+INSERT INTO `sys_dict_item` VALUES (11, 'notice_level', 'M', '中', 'warning', 1, 2, '', now(), 1,now(),1);
+INSERT INTO `sys_dict_item` VALUES (12, 'notice_level', 'H', '高', 'danger', 1, 3, '', now(), 1,now(),1);
+
+-- ----------------------------
+-- Table structure for sys_menu
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_menu`;
+CREATE TABLE `sys_menu` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
+ `parent_id` bigint NOT NULL COMMENT '父菜单ID',
+ `tree_path` varchar(255) COMMENT '父节点ID路径',
+ `name` varchar(64) NOT NULL COMMENT '菜单名称',
+ `type` char(1) NOT NULL COMMENT '菜单类型(C-目录 M-菜单 B-按钮)',
+ `route_name` varchar(255) COMMENT '路由名称(Vue Router 中用于命名路由)',
+ `route_path` varchar(128) COMMENT '路由路径(Vue Router 中定义的 URL 路径)',
+ `component` varchar(128) COMMENT '组件路径(组件页面完整路径,相对于 src/views/,缺省后缀 .vue)',
+ `perm` varchar(128) COMMENT '【按钮】权限标识',
+ `always_show` tinyint DEFAULT 0 COMMENT '【目录】只有一个子路由是否始终显示(1-是 0-否)',
+ `keep_alive` tinyint DEFAULT 0 COMMENT '【菜单】是否开启页面缓存(1-是 0-否)',
+ `visible` tinyint(1) DEFAULT 1 COMMENT '显示状态(1-显示 0-隐藏)',
+ `sort` int DEFAULT 0 COMMENT '排序',
+ `icon` varchar(64) COMMENT '菜单图标',
+ `redirect` varchar(128) COMMENT '跳转路径',
+ `create_time` datetime NULL COMMENT '创建时间',
+ `update_time` datetime NULL COMMENT '更新时间',
+ `params` varchar(255) NULL COMMENT '路由参数',
+ PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '系统菜单表';
+
+-- ----------------------------
+-- Records of sys_menu
+-- ----------------------------
+-- 顶级目录(1-10):平台/系统/代码生成/AI助手/文档/接口文档/组件/演示/多级/路由
+INSERT INTO `sys_menu` VALUES (1, 0, '0', '平台管理', 'C', '', '/platform', 'Layout', NULL, NULL, NULL, 1, 1, 'platform', '/platform/tenant', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2, 0, '0', '系统管理', 'C', '', '/system', 'Layout', NULL, NULL, NULL, 1, 2, 'system', '/system/user', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (3, 0, '0', '代码生成', 'C', '', '/gen', 'Layout', NULL, NULL, NULL, 1, 3, 'code', '/gen/index', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (4, 0, '0', 'AI助手', 'C', '', '/ai', 'Layout', NULL, NULL, NULL, 1, 4, 'platform', '/ai/command-record', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (5, 0, '0', '平台文档', 'C', '', '/doc', 'Layout', NULL, NULL, NULL, 1, 5, 'document', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (6, 0, '0', '接口文档', 'C', '', '/api', 'Layout', NULL, NULL, NULL, 1, 6, 'api', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (7, 0, '0', '组件封装', 'C', '', '/component', 'Layout', NULL, NULL, NULL, 1, 7, 'menu', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (8, 0, '0', '功能演示', 'C', '', '/function', 'Layout', NULL, NULL, NULL, 1, 8, 'menu', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (9, 0, '0', '多级菜单', 'C', NULL, '/multi-level', 'Layout', NULL, 1, NULL, 1, 9, 'cascader', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (10, 0, '0', '路由参数', 'C', '', '/route-param', 'Layout', NULL, NULL, NULL, 1, 10, 'el-icon-ElementPlus', '', now(), now(), NULL);
+
+-- 平台管理(平台方)
+INSERT INTO `sys_menu` VALUES (110, 1, '0,1', '租户管理', 'M', 'Tenant', 'tenant', 'system/tenant/index', NULL, NULL, 1, 1, 1, 'el-icon-OfficeBuilding', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (1101, 110, '0,1,110', '租户查询', 'B', NULL, '', NULL, 'sys:tenant:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (1102, 110, '0,1,110', '租户新增', 'B', NULL, '', NULL, 'sys:tenant:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (1103, 110, '0,1,110', '租户编辑', 'B', NULL, '', NULL, 'sys:tenant:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (1104, 110, '0,1,110', '租户删除', 'B', NULL, '', NULL, 'sys:tenant:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (1105, 110, '0,1,110', '租户启用/禁用', 'B', NULL, '', NULL, 'sys:tenant:change-status', NULL, NULL, 1, 5, '', NULL, now(), now(), NULL);
+
+-- 系统管理(租户侧)
+INSERT INTO `sys_menu` VALUES (210, 2, '0,2', '用户管理', 'M', 'User', 'user', 'system/user/index', NULL, NULL, 1, 1, 1, 'el-icon-User', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2101, 210, '0,2,210', '用户查询', 'B', NULL, '', NULL, 'sys:user:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2102, 210, '0,2,210', '用户新增', 'B', NULL, '', NULL, 'sys:user:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2103, 210, '0,2,210', '用户编辑', 'B', NULL, '', NULL, 'sys:user:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2104, 210, '0,2,210', '用户删除', 'B', NULL, '', NULL, 'sys:user:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2105, 210, '0,2,210', '重置密码', 'B', NULL, '', NULL, 'sys:user:reset-password', NULL, NULL, 1, 5, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2106, 210, '0,2,210', '用户导入', 'B', NULL, '', NULL, 'sys:user:import', NULL, NULL, 1, 6, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2107, 210, '0,2,210', '用户导出', 'B', NULL, '', NULL, 'sys:user:export', NULL, NULL, 1, 7, '', NULL, now(), now(), NULL);
+
+INSERT INTO `sys_menu` VALUES (220, 2, '0,2', '角色管理', 'M', 'Role', 'role', 'system/role/index', NULL, NULL, 1, 1, 2, 'role', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2201, 220, '0,2,220', '角色查询', 'B', NULL, '', NULL, 'sys:role:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2202, 220, '0,2,220', '角色新增', 'B', NULL, '', NULL, 'sys:role:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2203, 220, '0,2,220', '角色编辑', 'B', NULL, '', NULL, 'sys:role:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2204, 220, '0,2,220', '角色删除', 'B', NULL, '', NULL, 'sys:role:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
+
+INSERT INTO `sys_menu` VALUES (230, 2, '0,2', '菜单管理', 'M', 'SysMenu', 'menu', 'system/menu/index', NULL, NULL, 1, 1, 3, 'menu', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2301, 230, '0,2,230', '菜单查询', 'B', NULL, '', NULL, 'sys:menu:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2302, 230, '0,2,230', '菜单新增', 'B', NULL, '', NULL, 'sys:menu:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2303, 230, '0,2,230', '菜单编辑', 'B', NULL, '', NULL, 'sys:menu:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2304, 230, '0,2,230', '菜单删除', 'B', NULL, '', NULL, 'sys:menu:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
+
+INSERT INTO `sys_menu` VALUES (240, 2, '0,2', '部门管理', 'M', 'Dept', 'dept', 'system/dept/index', NULL, NULL, 1, 1, 4, 'tree', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2401, 240, '0,2,240', '部门查询', 'B', NULL, '', NULL, 'sys:dept:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2402, 240, '0,2,240', '部门新增', 'B', NULL, '', NULL, 'sys:dept:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2403, 240, '0,2,240', '部门编辑', 'B', NULL, '', NULL, 'sys:dept:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2404, 240, '0,2,240', '部门删除', 'B', NULL, '', NULL, 'sys:dept:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
+
+INSERT INTO `sys_menu` VALUES (250, 2, '0,2', '字典管理', 'M', 'Dict', 'dict', 'system/dict/index', NULL, NULL, 1, 1, 5, 'dict', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2501, 250, '0,2,250', '字典查询', 'B', NULL, '', NULL, 'sys:dict:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2502, 250, '0,2,250', '字典新增', 'B', NULL, '', NULL, 'sys:dict:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2503, 250, '0,2,250', '字典编辑', 'B', NULL, '', NULL, 'sys:dict:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2504, 250, '0,2,250', '字典删除', 'B', NULL, '', NULL, 'sys:dict:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
+
+INSERT INTO `sys_menu` VALUES (251, 250, '0,2,250,251', '字典项', 'M', 'DictItem', 'dict-item', 'system/dict/dict-item', NULL, 0, 1, 0, 6, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2511, 251, '0,2,250,251', '字典项查询', 'B', NULL, '', NULL, 'sys:dict-item:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2512, 251, '0,2,250,251', '字典项新增', 'B', NULL, '', NULL, 'sys:dict-item:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2513, 251, '0,2,250,251', '字典项编辑', 'B', NULL, '', NULL, 'sys:dict-item:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2514, 251, '0,2,250,251', '字典项删除', 'B', NULL, '', NULL, 'sys:dict-item:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
+
+INSERT INTO `sys_menu` VALUES (260, 2, '0,2', '系统日志', 'M', 'Log', 'log', 'system/log/index', NULL, 0, 1, 1, 7, 'document', NULL, now(), now(), NULL);
+
+INSERT INTO `sys_menu` VALUES (270, 2, '0,2', '系统配置', 'M', 'Config', 'config', 'system/config/index', NULL, 0, 1, 1, 8, 'setting', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2701, 270, '0,2,270', '系统配置查询', 'B', NULL, '', NULL, 'sys:config:list', 0, 1, 1, 1, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2702, 270, '0,2,270', '系统配置新增', 'B', NULL, '', NULL, 'sys:config:create', 0, 1, 1, 2, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2703, 270, '0,2,270', '系统配置修改', 'B', NULL, '', NULL, 'sys:config:update', 0, 1, 1, 3, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2704, 270, '0,2,270', '系统配置删除', 'B', NULL, '', NULL, 'sys:config:delete', 0, 1, 1, 4, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2705, 270, '0,2,270', '系统配置刷新', 'B', NULL, '', NULL, 'sys:config:refresh', 0, 1, 1, 5, '', NULL, now(), now(), NULL);
+
+INSERT INTO `sys_menu` VALUES (280, 2, '0,2', '通知公告', 'M', 'Notice', 'notice', 'system/notice/index', NULL, NULL, NULL, 1, 9, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2801, 280, '0,2,280', '通知查询', 'B', NULL, '', NULL, 'sys:notice:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2802, 280, '0,2,280', '通知新增', 'B', NULL, '', NULL, 'sys:notice:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2803, 280, '0,2,280', '通知编辑', 'B', NULL, '', NULL, 'sys:notice:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2804, 280, '0,2,280', '通知删除', 'B', NULL, '', NULL, 'sys:notice:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2805, 280, '0,2,280', '通知发布', 'B', NULL, '', NULL, 'sys:notice:publish', 0, 1, 1, 5, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2806, 280, '0,2,280', '通知撤回', 'B', NULL, '', NULL, 'sys:notice:revoke', 0, 1, 1, 6, '', NULL, now(), now(), NULL);
+
+-- 代码生成
+INSERT INTO `sys_menu` VALUES (310, 3, '0,3', '代码生成', 'M', 'Gen', 'gen', 'gen/index', NULL, NULL, 1, 1, 1, 'code', NULL, now(), now(), NULL);
+
+-- AI 助手
+INSERT INTO `sys_menu` VALUES (401, 4, '0,4', 'AI命令记录', 'M', 'AiCommandRecord', 'command-record', 'ai/command-record/index', NULL, NULL, 1, 1, 1, 'document', NULL, now(), now(), NULL);
+
+-- 平台文档(外链通过 route_path 识别)
+INSERT INTO `sys_menu` VALUES (501, 5, '0,5', '平台文档(外链)', 'M', NULL, 'https://juejin.cn/post/7228990409909108793', '', NULL, NULL, NULL, 1, 1, 'document', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (502, 5, '0,5', '后端文档', 'M', NULL, 'https://youlai.blog.csdn.net/article/details/145178880', '', NULL, NULL, NULL, 1, 2, 'document', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (503, 5, '0,5', '移动端文档', 'M', NULL, 'https://youlai.blog.csdn.net/article/details/143222890', '', NULL, NULL, NULL, 1, 3, 'document', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (504, 5, '0,5', '内部文档', 'M', NULL, 'internal-doc', 'demo/internal-doc', NULL, NULL, NULL, 1, 4, 'document', '', now(), now(), NULL);
+
+-- 接口文档
+INSERT INTO `sys_menu` VALUES (601, 6, '0,6', 'Apifox', 'M', 'Apifox', 'apifox', 'demo/api/apifox', NULL, NULL, 1, 1, 1, 'api', '', now(), now(), NULL);
+
+-- 组件封装
+INSERT INTO `sys_menu` VALUES (701, 7, '0,7', '富文本编辑器', 'M', 'WangEditor', 'wang-editor', 'demo/wang-editor', NULL, NULL, 1, 1, 2, '', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (702, 7, '0,7', '图片上传', 'M', 'Upload', 'upload', 'demo/upload', NULL, NULL, 1, 1, 3, '', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (703, 7, '0,7', '图标选择器', 'M', 'IconSelect', 'icon-select', 'demo/icon-select', NULL, NULL, 1, 1, 4, '', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (704, 7, '0,7', '字典组件', 'M', 'DictDemo', 'dict-demo', 'demo/dictionary', NULL, NULL, 1, 1, 4, '', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (705, 7, '0,7', '增删改查', 'M', 'Curd', 'curd', 'demo/curd/index', NULL, NULL, 1, 1, 0, '', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (706, 7, '0,7', '列表选择器', 'M', 'TableSelect', 'table-select', 'demo/table-select/index', NULL, NULL, 1, 1, 1, '', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (707, 7, '0,7', '拖拽组件', 'M', 'Drag', 'drag', 'demo/drag', NULL, NULL, NULL, 1, 5, '', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (708, 7, '0,7', '滚动文本', 'M', 'TextScroll', 'text-scroll', 'demo/text-scroll', NULL, NULL, NULL, 1, 6, '', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (709, 7, '0,7', '自适应表格操作列', 'M', 'AutoOperationColumn', 'operation-column', 'demo/auto-operation-column', NULL, NULL, 1, 1, 1, '', '', now(), now(), NULL);
+
+-- 功能演示
+INSERT INTO `sys_menu` VALUES (801, 8, '0,8', 'Websocket', 'M', 'WebSocket', '/function/websocket', 'demo/websocket', NULL, NULL, 1, 1, 1, '', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (802, 8, '0,8', 'Icons', 'M', 'IconDemo', 'icon-demo', 'demo/icons', NULL, NULL, 1, 1, 2, 'el-icon-Notification', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (803, 8, '0,8', '字典实时同步', 'M', 'DictSync', 'dict-sync', 'demo/dict-sync', NULL, NULL, NULL, 1, 3, '', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (804, 8, '0,8', 'VxeTable', 'M', 'VxeTable', 'vxe-table', 'demo/vxe-table/index', NULL, NULL, 1, 1, 4, 'el-icon-MagicStick', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (805, 8, '0,8', 'CURD单文件', 'M', 'CurdSingle', 'curd-single', 'demo/curd-single', NULL, NULL, 1, 1, 5, 'el-icon-Reading', '', now(), now(), NULL);
+
+-- 多级菜单示例
+INSERT INTO `sys_menu` VALUES (910, 9, '0,9', '菜单一级', 'C', NULL, 'multi-level1', 'Layout', NULL, 1, NULL, 1, 1, '', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (911, 910, '0,9,910', '菜单二级', 'C', NULL, 'multi-level2', 'Layout', NULL, 0, NULL, 1, 1, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (912, 911, '0,9,910,911', '菜单三级-1', 'M', NULL, 'multi-level3-1', 'demo/multi-level/children/children/level3-1', NULL, 0, 1, 1, 1, '', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (913, 911, '0,9,910,911', '菜单三级-2', 'M', NULL, 'multi-level3-2', 'demo/multi-level/children/children/level3-2', NULL, 0, 1, 1, 2, '', '', now(), now(), NULL);
+
+-- 路由参数
+INSERT INTO `sys_menu` VALUES (1001, 10, '0,10', '参数(type=1)', 'M', 'RouteParamType1', 'route-param-type1', 'demo/route-param', NULL, 0, 1, 1, 1, 'el-icon-Star', NULL, now(), now(), '{\"type\": \"1\"}');
+INSERT INTO `sys_menu` VALUES (1002, 10, '0,10', '参数(type=2)', 'M', 'RouteParamType2', 'route-param-type2', 'demo/route-param', NULL, 0, 1, 1, 2, 'el-icon-StarFilled', NULL, now(), now(), '{\"type\": \"2\"}');
+-- ============================================
+--- 系统配置权限按钮(ID: 901-905)
+--- 字典项权限按钮(ID: 701-704)
+-- ============================================
+-- 通知公告权限按钮(ID: 1101-1106)
+-- ============================================
+-- ============================================
+-- 字典项权限按钮(ID: 701-704)
+-- ============================================
+-- ============================================
+-- 租户管理权限按钮(ID: 501-505)
+-- ============================================
+
+
+-- ----------------------------
+-- Table structure for sys_role
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_role`;
+CREATE TABLE `sys_role` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `name` varchar(64) NOT NULL COMMENT '角色名称',
+ `code` varchar(32) NOT NULL COMMENT '角色编码',
+ `sort` int NULL COMMENT '显示顺序',
+ `status` tinyint(1) DEFAULT 1 COMMENT '角色状态(1-正常 0-停用)',
+ `data_scope` tinyint NULL COMMENT '数据权限(1-所有数据 2-部门及子部门数据 3-本部门数据 4-本人数据)',
+ `create_by` bigint NULL COMMENT '创建人 ID',
+ `create_time` datetime NULL COMMENT '创建时间',
+ `update_by` bigint NULL COMMENT '更新人ID',
+ `update_time` datetime NULL COMMENT '更新时间',
+ `is_deleted` tinyint(1) DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)',
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_name`(`name` ASC) USING BTREE COMMENT '角色名称唯一索引',
+ UNIQUE INDEX `uk_code`(`code` ASC) USING BTREE COMMENT '角色编码唯一索引'
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '系统角色表';
+
+-- ----------------------------
+-- Records of sys_role
+-- ----------------------------
+INSERT INTO `sys_role` VALUES (1, '超级管理员', 'ROOT', 1, 1, 1, NULL, now(), NULL, now(), 0);
+INSERT INTO `sys_role` VALUES (2, '系统管理员', 'ADMIN', 2, 1, 1, NULL, now(), NULL, NULL, 0);
+INSERT INTO `sys_role` VALUES (3, '访问游客', 'GUEST', 3, 1, 3, NULL, now(), NULL, now(), 0);
+INSERT INTO `sys_role` VALUES (4, '系统管理员1', 'ADMIN1', 4, 1, 1, NULL, now(), NULL, NULL, 0);
+INSERT INTO `sys_role` VALUES (5, '系统管理员2', 'ADMIN2', 5, 1, 1, NULL, now(), NULL, NULL, 0);
+INSERT INTO `sys_role` VALUES (6, '系统管理员3', 'ADMIN3', 6, 1, 1, NULL, now(), NULL, NULL, 0);
+INSERT INTO `sys_role` VALUES (7, '系统管理员4', 'ADMIN4', 7, 1, 1, NULL, now(), NULL, NULL, 0);
+INSERT INTO `sys_role` VALUES (8, '系统管理员5', 'ADMIN5', 8, 1, 1, NULL, now(), NULL, NULL, 0);
+INSERT INTO `sys_role` VALUES (9, '系统管理员6', 'ADMIN6', 9, 1, 1, NULL, now(), NULL, NULL, 0);
+INSERT INTO `sys_role` VALUES (10, '系统管理员7', 'ADMIN7', 10, 1, 1, NULL, now(), NULL, NULL, 0);
+INSERT INTO `sys_role` VALUES (11, '系统管理员8', 'ADMIN8', 11, 1, 1, NULL, now(), NULL, NULL, 0);
+INSERT INTO `sys_role` VALUES (12, '系统管理员9', 'ADMIN9', 12, 1, 1, NULL, now(), NULL, NULL, 0);
+
+-- ----------------------------
+-- Table structure for sys_role_menu
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_role_menu`;
+CREATE TABLE `sys_role_menu` (
+ `role_id` bigint NOT NULL COMMENT '角色ID',
+ `menu_id` bigint NOT NULL COMMENT '菜单ID',
+ UNIQUE INDEX `uk_roleid_menuid`(`role_id` ASC, `menu_id` ASC) USING BTREE COMMENT '角色菜单唯一索引'
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '角色菜单关联表';
+
+-- ============================================
+-- 系统管理员角色菜单权限(role_id=2)
+-- 顶级目录
+INSERT INTO `sys_role_menu` VALUES (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (2, 6), (2, 7), (2, 8), (2, 9), (2, 10);
+-- 平台管理
+INSERT INTO `sys_role_menu` VALUES (2, 110), (2, 1101), (2, 1102), (2, 1103), (2, 1104), (2, 1105);
+-- 系统管理
+INSERT INTO `sys_role_menu` VALUES (2, 210), (2, 2101), (2, 2102), (2, 2103), (2, 2104), (2, 2105), (2, 2106), (2, 2107);
+INSERT INTO `sys_role_menu` VALUES (2, 220), (2, 2201), (2, 2202), (2, 2203), (2, 2204);
+INSERT INTO `sys_role_menu` VALUES (2, 230), (2, 2301), (2, 2302), (2, 2303), (2, 2304);
+INSERT INTO `sys_role_menu` VALUES (2, 240), (2, 2401), (2, 2402), (2, 2403), (2, 2404);
+INSERT INTO `sys_role_menu` VALUES (2, 250), (2, 2501), (2, 2502), (2, 2503), (2, 2504);
+INSERT INTO `sys_role_menu` VALUES (2, 251), (2, 2511), (2, 2512), (2, 2513), (2, 2514);
+INSERT INTO `sys_role_menu` VALUES (2, 260);
+INSERT INTO `sys_role_menu` VALUES (2, 270), (2, 2701), (2, 2702), (2, 2703), (2, 2704), (2, 2705);
+INSERT INTO `sys_role_menu` VALUES (2, 280), (2, 2801), (2, 2802), (2, 2803), (2, 2804), (2, 2805), (2, 2806);
+-- 代码生成
+INSERT INTO `sys_role_menu` VALUES (2, 310);
+-- AI 助手
+INSERT INTO `sys_role_menu` VALUES (2, 401);
+-- 平台文档
+INSERT INTO `sys_role_menu` VALUES (2, 501), (2, 502), (2, 503), (2, 504);
+-- 接口文档
+INSERT INTO `sys_role_menu` VALUES (2, 601);
+-- 组件封装
+INSERT INTO `sys_role_menu` VALUES (2, 701), (2, 702), (2, 703), (2, 704), (2, 705), (2, 706), (2, 707), (2, 708), (2, 709);
+-- 功能演示 / 多级菜单
+INSERT INTO `sys_role_menu` VALUES (2, 801), (2, 802), (2, 803), (2, 804), (2, 805), (2, 910), (2, 911), (2, 912), (2, 913);
+-- 路由参数
+INSERT INTO `sys_role_menu` VALUES (2, 1001), (2, 1002);
+
+-- ----------------------------
+-- Table structure for sys_user
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_user`;
+CREATE TABLE `sys_user` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `username` varchar(64) COMMENT '用户名',
+ `nickname` varchar(64) COMMENT '昵称',
+ `gender` tinyint(1) DEFAULT 1 COMMENT '性别((1-男 2-女 0-保密)',
+ `password` varchar(100) COMMENT '密码',
+ `dept_id` int COMMENT '部门ID',
+ `avatar` varchar(255) COMMENT '用户头像',
+ `mobile` varchar(20) COMMENT '联系方式',
+ `status` tinyint(1) DEFAULT 1 COMMENT '状态(1-正常 0-禁用)',
+ `email` varchar(128) COMMENT '用户邮箱',
+ `create_time` datetime COMMENT '创建时间',
+ `create_by` bigint COMMENT '创建人ID',
+ `update_time` datetime COMMENT '更新时间',
+ `update_by` bigint COMMENT '修改人ID',
+ `is_deleted` tinyint(1) DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)',
+ `openid` char(28) COMMENT '微信 openid',
+ PRIMARY KEY (`id`) USING BTREE,
+ KEY `login_name` (`username`)
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '系统用户表';
+
+-- ----------------------------
+-- Records of sys_user
+-- ----------------------------
+INSERT INTO `sys_user` VALUES (1, 'root', '有来技术', 0, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', NULL, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345677', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0,NULL);
+INSERT INTO `sys_user` VALUES (2, 'admin', '系统管理员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345678', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0,NULL);
+INSERT INTO `sys_user` VALUES (3, 'test', '测试小用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345679', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0,NULL);
+
+-- ----------------------------
+-- Table structure for sys_user_role
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_user_role`;
+CREATE TABLE `sys_user_role` (
+ `user_id` bigint NOT NULL COMMENT '用户ID',
+ `role_id` bigint NOT NULL COMMENT '角色ID',
+ PRIMARY KEY (`user_id`, `role_id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '用户角色关联表';
+
+-- ----------------------------
+-- Records of sys_user_role
+-- ----------------------------
+INSERT INTO `sys_user_role` VALUES (1, 1);
+INSERT INTO `sys_user_role` VALUES (2, 2);
+INSERT INTO `sys_user_role` VALUES (3, 3);
+
+
+-- ----------------------------
+-- Table structure for sys_log
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_log`;
+CREATE TABLE `sys_log` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
+ `module` varchar(50) NOT NULL COMMENT '日志模块',
+ `request_method` varchar(64) NOT NULL COMMENT '请求方式',
+ `request_params` text COMMENT '请求参数(批量请求参数可能会超过text)',
+ `response_content` mediumtext COMMENT '返回参数',
+ `content` varchar(255) NOT NULL COMMENT '日志内容',
+ `request_uri` varchar(255) COMMENT '请求路径',
+ `method` varchar(255) COMMENT '方法名',
+ `ip` varchar(45) COMMENT 'IP地址',
+ `province` varchar(100) COMMENT '省份',
+ `city` varchar(100) COMMENT '城市',
+ `execution_time` bigint COMMENT '执行时间(ms)',
+ `browser` varchar(100) COMMENT '浏览器',
+ `browser_version` varchar(100) COMMENT '浏览器版本',
+ `os` varchar(100) COMMENT '终端系统',
+ `create_by` bigint COMMENT '创建人ID',
+ `create_time` datetime COMMENT '创建时间',
+ `is_deleted` tinyint DEFAULT '0' COMMENT '逻辑删除标识(1-已删除 0-未删除)',
+ PRIMARY KEY (`id`) USING BTREE,
+ KEY `idx_create_time` (`create_time`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COMMENT='系统操作日志表';
+
+-- ----------------------------
+-- Table structure for gen_config
+-- ----------------------------
+DROP TABLE IF EXISTS `gen_config`;
+CREATE TABLE `gen_config` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `table_name` varchar(100) NOT NULL COMMENT '表名',
+ `module_name` varchar(100) COMMENT '模块名',
+ `package_name` varchar(255) NOT NULL COMMENT '包名',
+ `business_name` varchar(100) NOT NULL COMMENT '业务名',
+ `entity_name` varchar(100) NOT NULL COMMENT '实体类名',
+ `author` varchar(50) NOT NULL COMMENT '作者',
+ `parent_menu_id` bigint COMMENT '上级菜单ID,对应sys_menu的id ',
+ `remove_table_prefix` varchar(20) COMMENT '要移除的表前缀,如: sys_',
+ `page_type` varchar(20) COMMENT '页面类型(classic|curd)',
+ `create_time` datetime COMMENT '创建时间',
+ `update_time` datetime COMMENT '更新时间',
+ `is_deleted` tinyint(4) DEFAULT 0 COMMENT '是否删除',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_tablename` (`table_name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代码生成配置表';
+
+-- ----------------------------
+-- Table structure for gen_field_config
+-- ----------------------------
+DROP TABLE IF EXISTS `gen_field_config`;
+CREATE TABLE `gen_field_config` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `config_id` bigint NOT NULL COMMENT '关联的配置ID',
+ `column_name` varchar(100) ,
+ `column_type` varchar(50) ,
+ `column_length` int ,
+ `field_name` varchar(100) NOT NULL COMMENT '字段名称',
+ `field_type` varchar(100) COMMENT '字段类型',
+ `field_sort` int COMMENT '字段排序',
+ `field_comment` varchar(255) COMMENT '字段描述',
+ `max_length` int ,
+ `is_required` tinyint(1) COMMENT '是否必填',
+ `is_show_in_list` tinyint(1) DEFAULT '0' COMMENT '是否在列表显示',
+ `is_show_in_form` tinyint(1) DEFAULT '0' COMMENT '是否在表单显示',
+ `is_show_in_query` tinyint(1) DEFAULT '0' COMMENT '是否在查询条件显示',
+ `query_type` tinyint COMMENT '查询方式',
+ `form_type` tinyint COMMENT '表单类型',
+ `dict_type` varchar(50) COMMENT '字典类型',
+ `create_time` datetime COMMENT '创建时间',
+ `update_time` datetime COMMENT '更新时间',
+ PRIMARY KEY (`id`),
+ KEY `config_id` (`config_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代码生成字段配置表';
+
+-- ----------------------------
+-- 系统配置表
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_config`;
+CREATE TABLE `sys_config` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `config_name` varchar(50) NOT NULL COMMENT '配置名称',
+ `config_key` varchar(50) NOT NULL COMMENT '配置key',
+ `config_value` varchar(100) NOT NULL COMMENT '配置值',
+ `remark` varchar(255) COMMENT '备注',
+ `create_time` datetime COMMENT '创建时间',
+ `create_by` bigint COMMENT '创建人ID',
+ `update_time` datetime COMMENT '更新时间',
+ `update_by` bigint COMMENT '更新人ID',
+ `is_deleted` tinyint(4) DEFAULT '0' NOT NULL COMMENT '逻辑删除标识(0-未删除 1-已删除)',
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB COMMENT='系统配置表';
+
+INSERT INTO `sys_config` VALUES (1, '系统限流QPS', 'IP_QPS_THRESHOLD_LIMIT', '10', '单个IP请求的最大每秒查询数(QPS)阈值Key', now(), 1, NULL, NULL, 0);
+
+-- ----------------------------
+-- 通知公告表
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_notice`;
+CREATE TABLE `sys_notice` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `title` varchar(50) COMMENT '通知标题',
+ `content` text COMMENT '通知内容',
+ `type` tinyint NOT NULL COMMENT '通知类型(关联字典编码:notice_type)',
+ `level` varchar(5) NOT NULL COMMENT '通知等级(字典code:notice_level)',
+ `target_type` tinyint NOT NULL COMMENT '目标类型(1: 全体, 2: 指定)',
+ `target_user_ids` varchar(255) COMMENT '目标人ID集合(多个使用英文逗号,分割)',
+ `publisher_id` bigint COMMENT '发布人ID',
+ `publish_status` tinyint DEFAULT '0' COMMENT '发布状态(0: 未发布, 1: 已发布, -1: 已撤回)',
+ `publish_time` datetime COMMENT '发布时间',
+ `revoke_time` datetime COMMENT '撤回时间',
+ `create_by` bigint NOT NULL COMMENT '创建人ID',
+ `create_time` datetime NOT NULL COMMENT '创建时间',
+ `update_by` bigint COMMENT '更新人ID',
+ `update_time` datetime COMMENT '更新时间',
+ `is_deleted` tinyint(1) DEFAULT '0' COMMENT '是否删除(0: 未删除, 1: 已删除)',
+ PRIMARY KEY (`id`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统通知公告表';
+
+INSERT INTO `sys_notice` VALUES (1, 'v3.0.0 版本发布 - 多租户功能上线', '🎉 新版本发布,主要更新内容:
1. 新增多租户功能,支持租户隔离和数据管理
2. 优化系统性能,提升响应速度
3. 完善权限管理,增强安全性
4. 修复已知问题,提升系统稳定性
', 1, 'H', 1, NULL, 1, 1, '2024-12-15 10:00:00', NULL, 1, '2024-12-15 10:00:00', 1, '2024-12-15 10:00:00', 0);
+INSERT INTO `sys_notice` VALUES (2, '系统维护通知 - 2024年12月20日', '⏰ 系统维护通知
系统将于 2024年12月20日(本周五)凌晨 2:00-4:00 进行例行维护升级。
维护期间系统将暂停服务,请提前做好数据备份工作。
给您带来的不便,敬请谅解!
', 2, 'H', 1, NULL, 1, 1, '2024-12-18 14:30:00', NULL, 1, '2024-12-18 14:30:00', 1, '2024-12-18 14:30:00', 0);
+INSERT INTO `sys_notice` VALUES (3, '安全提醒 - 防范钓鱼邮件', '⚠️ 安全提醒
近期发现有不法分子通过钓鱼邮件进行网络攻击,请大家提高警惕:
1. 不要点击来源不明的邮件链接
2. 不要下载可疑附件
3. 遇到可疑邮件请及时联系IT部门
4. 定期修改密码,使用强密码策略
', 3, 'H', 1, NULL, 1, 1, '2024-12-10 09:00:00', NULL, 1, '2024-12-10 09:00:00', 1, '2024-12-10 09:00:00', 0);
+INSERT INTO `sys_notice` VALUES (4, '元旦假期安排通知', '📅 元旦假期安排
根据国家法定节假日安排,公司元旦假期时间为:
2024年12月30日(周一)至 2025年1月1日(周三),共3天。
2024年12月29日(周日)正常上班。
祝大家元旦快乐,假期愉快!
', 4, 'M', 1, NULL, 1, 1, '2024-12-25 16:00:00', NULL, 1, '2024-12-25 16:00:00', 1, '2024-12-25 16:00:00', 0);
+INSERT INTO `sys_notice` VALUES (5, '新产品发布会邀请', '🎊 新产品发布会邀请
公司将于 2025年1月15日下午14:00 在总部会议室举办新产品发布会。
届时将展示最新研发的产品和技术成果,欢迎全体员工参加。
请各部门提前安排好工作,准时参加。
', 5, 'M', 1, NULL, 1, 1, '2024-12-28 11:00:00', NULL, 1, '2024-12-28 11:00:00', 1, '2024-12-28 11:00:00', 0);
+INSERT INTO `sys_notice` VALUES (6, 'v2.16.1 版本更新', '✨ 版本更新
v2.16.1 版本已发布,主要修复内容:
1. 修复 WebSocket 重复连接导致的后台线程阻塞问题
2. 优化通知公告功能,提升用户体验
3. 修复部分已知bug
建议尽快更新到最新版本。
', 1, 'M', 1, NULL, 1, 1, '2024-12-05 15:30:00', NULL, 1, '2024-12-05 15:30:00', 1, '2024-12-05 15:30:00', 0);
+INSERT INTO `sys_notice` VALUES (7, '年终总结会议通知', '📋 年终总结会议通知
各部门年终总结会议将于 2024年12月30日上午9:00 召开。
请各部门负责人提前准备好年度工作总结和下年度工作计划。
会议地点:总部大会议室
', 5, 'M', 2, '1,2', 1, 1, '2024-12-22 10:00:00', NULL, 1, '2024-12-22 10:00:00', 1, '2024-12-22 10:00:00', 0);
+INSERT INTO `sys_notice` VALUES (8, '系统功能优化完成', '✅ 系统功能优化
已完成以下功能优化:
1. 优化用户管理界面,提升操作体验
2. 增强数据导出功能,支持更多格式
3. 优化搜索功能,提升查询效率
4. 修复部分界面显示问题
', 1, 'L', 1, NULL, 1, 1, '2024-12-12 14:20:00', NULL, 1, '2024-12-12 14:20:00', 1, '2024-12-12 14:20:00', 0);
+INSERT INTO `sys_notice` VALUES (9, '员工培训计划', '📚 员工培训计划
为提升员工专业技能,公司将于 2025年1月8日-10日 组织技术培训。
培训内容:
1. 新技术框架应用
2. 代码规范与最佳实践
3. 系统架构设计
请各部门合理安排工作,确保培训顺利进行。
', 5, 'M', 1, NULL, 1, 1, '2024-12-20 09:30:00', NULL, 1, '2024-12-20 09:30:00', 1, '2024-12-20 09:30:00', 0);
+INSERT INTO `sys_notice` VALUES (10, '数据备份提醒', '💾 数据备份提醒
请各部门注意定期备份重要数据,建议每周至少备份一次。
备份方式:
1. 使用系统自带备份功能
2. 手动导出重要数据
3. 联系IT部门协助备份
数据安全,人人有责!
', 3, 'L', 1, NULL, 1, 1, '2024-12-08 08:00:00', NULL, 1, '2024-12-08 08:00:00', 1, '2024-12-08 08:00:00', 0);
+
+-- ----------------------------
+-- 用户通知公告表
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_user_notice`;
+CREATE TABLE `sys_user_notice` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
+ `notice_id` bigint NOT NULL COMMENT '公共通知id',
+ `user_id` bigint NOT NULL COMMENT '用户id',
+ `is_read` bigint DEFAULT '0' COMMENT '读取状态(0: 未读, 1: 已读)',
+ `read_time` datetime COMMENT '阅读时间',
+ `create_time` datetime NOT NULL COMMENT '创建时间',
+ `update_time` datetime COMMENT '更新时间',
+ `is_deleted` tinyint DEFAULT '0' COMMENT '逻辑删除(0: 未删除, 1: 已删除)',
+ PRIMARY KEY (`id`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户通知公告关联表';
+
+INSERT INTO `sys_user_notice` VALUES (1, 1, 2, 1, NULL, now(), now(), 0);
+INSERT INTO `sys_user_notice` VALUES (2, 2, 2, 1, NULL, now(), now(), 0);
+INSERT INTO `sys_user_notice` VALUES (3, 3, 2, 1, NULL, now(), now(), 0);
+INSERT INTO `sys_user_notice` VALUES (4, 4, 2, 1, NULL, now(), now(), 0);
+INSERT INTO `sys_user_notice` VALUES (5, 5, 2, 1, NULL, now(), now(), 0);
+INSERT INTO `sys_user_notice` VALUES (6, 6, 2, 1, NULL, now(), now(), 0);
+INSERT INTO `sys_user_notice` VALUES (7, 7, 2, 1, NULL, now(), now(), 0);
+INSERT INTO `sys_user_notice` VALUES (8, 8, 2, 1, NULL, now(), now(), 0);
+INSERT INTO `sys_user_notice` VALUES (9, 9, 2, 1, NULL, now(), now(), 0);
+INSERT INTO `sys_user_notice` VALUES (10, 10, 2, 1, NULL, now(), now(), 0);
+
+-- ----------------------------
+-- AI 命令记录表
+-- ----------------------------
+DROP TABLE IF EXISTS `ai_command_log`;
+CREATE TABLE `ai_command_log` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `user_id` bigint DEFAULT NULL COMMENT '用户ID',
+ `username` varchar(64) DEFAULT NULL COMMENT '用户名',
+ `original_command` text COMMENT '原始命令',
+ `ai_provider` varchar(32) DEFAULT NULL COMMENT 'AI 供应商(qwen/openai/deepseek/gemini等)',
+ `ai_model` varchar(64) DEFAULT NULL COMMENT 'AI 模型名称(qwen-plus/qwen-max/gpt-4-turbo等)',
+ `parse_status` tinyint DEFAULT '0' COMMENT '解析是否成功(0-失败, 1-成功)',
+ `function_calls` text COMMENT '解析出的函数调用列表(JSON)',
+ `explanation` varchar(500) DEFAULT NULL COMMENT 'AI的理解说明',
+ `confidence` decimal(3,2) DEFAULT NULL COMMENT '置信度(0.00-1.00)',
+ `parse_error_message` text COMMENT '解析错误信息',
+ `input_tokens` int DEFAULT NULL COMMENT '输入Token数量',
+ `output_tokens` int DEFAULT NULL COMMENT '输出Token数量',
+ `parse_duration_ms` int DEFAULT NULL COMMENT '解析耗时(毫秒)',
+ `function_name` varchar(255) DEFAULT NULL COMMENT '执行的函数名称',
+ `function_arguments` text COMMENT '函数参数(JSON)',
+ `execute_status` tinyint(1) DEFAULT NULL COMMENT '执行状态(0-待执行, 1-成功, -1-失败)',
+ `execute_error_message` text COMMENT '执行错误信息',
+ `ip_address` varchar(128) DEFAULT NULL COMMENT 'IP地址',
+ `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+ `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+ PRIMARY KEY (`id`),
+ KEY `idx_user_id` (`user_id`),
+ KEY `idx_create_time` (`create_time`),
+ KEY `idx_provider` (`ai_provider`),
+ KEY `idx_model` (`ai_model`),
+ KEY `idx_parse_success` (`parse_status`),
+ KEY `idx_execute_status` (`execute_status`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='AI 命令日志表';
+
+-- ----------------------------
+-- 租户表(多租户模式)
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_tenant`;
+CREATE TABLE `sys_tenant` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '租户ID',
+ `name` varchar(100) NOT NULL COMMENT '租户名称',
+ `code` varchar(50) NOT NULL COMMENT '租户编码(唯一)',
+ `contact_name` varchar(50) DEFAULT NULL COMMENT '联系人姓名',
+ `contact_phone` varchar(20) DEFAULT NULL COMMENT '联系人电话',
+ `contact_email` varchar(100) DEFAULT NULL COMMENT '联系人邮箱',
+ `domain` varchar(100) DEFAULT NULL COMMENT '租户域名(用于域名识别)',
+ `logo` varchar(255) DEFAULT NULL COMMENT '租户Logo',
+ `status` tinyint DEFAULT '1' COMMENT '状态(1-正常 0-禁用)',
+ `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+ `expire_time` datetime DEFAULT NULL COMMENT '过期时间(NULL表示永不过期)',
+ `create_time` datetime COMMENT '创建时间',
+ `update_time` datetime COMMENT '更新时间',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_code` (`code`),
+ UNIQUE KEY `uk_domain` (`domain`),
+ KEY `idx_status` (`status`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='系统租户表';
+
+-- ----------------------------
+-- Records of sys_tenant
+-- ----------------------------
+INSERT INTO `sys_tenant` VALUES (1, '默认租户', 'DEFAULT', '系统管理员', '18812345678', 'admin@youlai.tech', NULL, NULL, 1, '系统默认租户', NULL, now(), now());
+INSERT INTO `sys_tenant` VALUES (2, '演示租户', 'DEMO', '演示用户', '18812345679', 'demo@youlai.tech', 'demo.youlai.tech', NULL, 1, '演示租户', NULL, now(), now());
+
+-- ----------------------------
+-- 用户租户关联表(多租户模式)
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_user_tenant`;
+CREATE TABLE `sys_user_tenant` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `user_id` bigint NOT NULL COMMENT '用户ID',
+ `tenant_id` bigint NOT NULL COMMENT '租户ID',
+ `is_default` tinyint DEFAULT '0' COMMENT '是否默认租户(1-是 0-否)',
+ `create_time` datetime COMMENT '创建时间',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_user_tenant` (`user_id`, `tenant_id`),
+ KEY `idx_user_id` (`user_id`),
+ KEY `idx_tenant_id` (`tenant_id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户租户关联表(多租户模式)';
+
+-- ----------------------------
+-- Records of sys_user_tenant
+-- ----------------------------
+INSERT INTO `sys_user_tenant` VALUES (1, 1, 1, 1, now());
+INSERT INTO `sys_user_tenant` VALUES (2, 2, 1, 1, now());
+INSERT INTO `sys_user_tenant` VALUES (3, 2, 2, 0, now());
+
+
+SET FOREIGN_KEY_CHECKS = 1;
diff --git a/src/main/java/com/youlai/boot/common/annotation/IgnoreTenant.java b/src/main/java/com/youlai/boot/common/annotation/IgnoreTenant.java
new file mode 100644
index 00000000..a388c2e9
--- /dev/null
+++ b/src/main/java/com/youlai/boot/common/annotation/IgnoreTenant.java
@@ -0,0 +1,22 @@
+package com.youlai.boot.common.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 忽略多租户注解
+ *
+ * 标注在方法或类上,表示该方法或类下的所有方法忽略多租户过滤
+ * 适用于系统管理、租户管理等不需要租户隔离的场景
+ *
+ *
+ * @author Ray.Hao
+ * @since 3.0.0
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface IgnoreTenant {
+}
+
diff --git a/src/main/java/com/youlai/boot/common/tenant/TenantContextHolder.java b/src/main/java/com/youlai/boot/common/tenant/TenantContextHolder.java
new file mode 100644
index 00000000..fd674b31
--- /dev/null
+++ b/src/main/java/com/youlai/boot/common/tenant/TenantContextHolder.java
@@ -0,0 +1,80 @@
+package com.youlai.boot.common.tenant;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 租户上下文工具类
+ *
+ * 使用 ThreadLocal 存储当前线程的租户ID,确保线程安全
+ *
+ *
+ * @author Ray.Hao
+ * @since 3.0.0
+ */
+@Slf4j
+public class TenantContextHolder {
+
+ /**
+ * 租户ID线程本地变量
+ */
+ private static final ThreadLocal TENANT_ID_HOLDER = new ThreadLocal<>();
+
+ /**
+ * 忽略租户标志(用于某些场景下临时跳过租户过滤)
+ */
+ private static final ThreadLocal IGNORE_TENANT_HOLDER = new ThreadLocal<>();
+
+ /**
+ * 设置当前租户ID
+ *
+ * @param tenantId 租户ID
+ */
+ public static void setTenantId(Long tenantId) {
+ if (tenantId != null) {
+ TENANT_ID_HOLDER.set(tenantId);
+ log.debug("设置当前租户ID: {}", tenantId);
+ }
+ }
+
+ /**
+ * 获取当前租户ID
+ *
+ * @return 租户ID,如果未设置则返回 null
+ */
+ public static Long getTenantId() {
+ return TENANT_ID_HOLDER.get();
+ }
+
+ /**
+ * 设置忽略租户标志
+ *
+ * @param ignore 是否忽略
+ */
+ public static void setIgnoreTenant(boolean ignore) {
+ IGNORE_TENANT_HOLDER.set(ignore);
+ log.debug("设置忽略租户标志: {}", ignore);
+ }
+
+ /**
+ * 是否忽略租户
+ *
+ * @return true-忽略,false-不忽略
+ */
+ public static boolean isIgnoreTenant() {
+ Boolean ignore = IGNORE_TENANT_HOLDER.get();
+ return ignore != null && ignore;
+ }
+
+ /**
+ * 清除当前线程的租户上下文
+ *
+ * 必须在请求结束时调用,避免线程池复用导致的数据泄露
+ *
+ */
+ public static void clear() {
+ TENANT_ID_HOLDER.remove();
+ IGNORE_TENANT_HOLDER.remove();
+ log.debug("清除租户上下文");
+ }
+}
+
diff --git a/src/main/java/com/youlai/boot/config/property/TenantProperties.java b/src/main/java/com/youlai/boot/config/property/TenantProperties.java
new file mode 100644
index 00000000..460c2068
--- /dev/null
+++ b/src/main/java/com/youlai/boot/config/property/TenantProperties.java
@@ -0,0 +1,62 @@
+package com.youlai.boot.config.property;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 多租户配置属性
+ *
+ * @author Ray.Hao
+ * @since 3.0.0
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "youlai.tenant")
+public class TenantProperties {
+
+ /**
+ * 是否启用多租户功能
+ * 默认:false(不启用)
+ */
+ private Boolean enabled = false;
+
+ /**
+ * 租户字段名
+ * 默认:tenant_id
+ */
+ private String column = "tenant_id";
+
+ /**
+ * 默认租户ID(用于兼容旧数据,tenant_id 为 NULL 时使用)
+ * 默认:1
+ */
+ private Long defaultTenantId = 1L;
+
+ /**
+ * 忽略多租户过滤的表名列表
+ * 系统表、租户表等不需要租户隔离的表
+ */
+ private List ignoreTables = new ArrayList<>();
+
+ /**
+ * 请求头中的租户ID字段名
+ * 默认:tenant-id
+ */
+ private String headerName = "tenant-id";
+
+ /**
+ * 初始化默认忽略的表
+ */
+ public TenantProperties() {
+ // 系统表默认忽略多租户
+ ignoreTables.add("sys_tenant");
+ ignoreTables.add("sys_dict");
+ ignoreTables.add("sys_dict_item");
+ ignoreTables.add("sys_config");
+ }
+}
+
diff --git a/src/main/java/com/youlai/boot/core/aspect/TenantAspect.java b/src/main/java/com/youlai/boot/core/aspect/TenantAspect.java
new file mode 100644
index 00000000..0b50eeb5
--- /dev/null
+++ b/src/main/java/com/youlai/boot/core/aspect/TenantAspect.java
@@ -0,0 +1,46 @@
+package com.youlai.boot.core.aspect;
+
+import com.youlai.boot.common.annotation.IgnoreTenant;
+import com.youlai.boot.common.tenant.TenantContextHolder;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+/**
+ * 多租户切面
+ *
+ * 处理 @IgnoreTenant 注解,临时跳过租户过滤
+ *
+ *
+ * @author Ray.Hao
+ * @since 3.0.0
+ */
+@Aspect
+@Component
+@Order(1)
+@Slf4j
+@ConditionalOnProperty(prefix = "youlai.tenant", name = "enabled", havingValue = "true", matchIfMissing = false)
+public class TenantAspect {
+
+ /**
+ * 环绕通知:处理 @IgnoreTenant 注解
+ */
+ @Around("@annotation(ignoreTenant) || @within(ignoreTenant)")
+ public Object around(ProceedingJoinPoint joinPoint, IgnoreTenant ignoreTenant) throws Throwable {
+ try {
+ // 设置忽略租户标志
+ TenantContextHolder.setIgnoreTenant(true);
+ log.debug("方法 {} 忽略多租户过滤", joinPoint.getSignature().getName());
+ // 执行原方法
+ return joinPoint.proceed();
+ } finally {
+ // 恢复租户过滤
+ TenantContextHolder.setIgnoreTenant(false);
+ }
+ }
+}
+
diff --git a/src/main/java/com/youlai/boot/core/filter/TenantContextFilter.java b/src/main/java/com/youlai/boot/core/filter/TenantContextFilter.java
new file mode 100644
index 00000000..9e53282d
--- /dev/null
+++ b/src/main/java/com/youlai/boot/core/filter/TenantContextFilter.java
@@ -0,0 +1,72 @@
+package com.youlai.boot.core.filter;
+
+import com.youlai.boot.common.tenant.TenantContextHolder;
+import com.youlai.boot.config.property.TenantProperties;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+/**
+ * 租户上下文过滤器
+ *
+ * 从请求头中获取租户ID,设置到线程上下文
+ * 请求结束时自动清除上下文,避免线程池复用导致的数据泄露
+ *
+ *
+ * @author Ray.Hao
+ * @since 3.0.0
+ */
+@Slf4j
+@Component
+@Order(1) // 确保在其他过滤器之前执行
+@RequiredArgsConstructor
+@ConditionalOnProperty(prefix = "youlai.tenant", name = "enabled", havingValue = "true", matchIfMissing = false)
+public class TenantContextFilter extends OncePerRequestFilter {
+
+ private final TenantProperties tenantProperties;
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+ throws ServletException, IOException {
+
+ try {
+ // 从请求头获取租户ID
+ String tenantIdStr = request.getHeader(tenantProperties.getHeaderName());
+
+ if (StringUtils.hasText(tenantIdStr)) {
+ try {
+ Long tenantId = Long.parseLong(tenantIdStr);
+ TenantContextHolder.setTenantId(tenantId);
+ log.debug("从请求头获取租户ID: {}", tenantId);
+ } catch (NumberFormatException e) {
+ log.warn("租户ID格式错误: {}", tenantIdStr);
+ }
+ } else {
+ // 如果未提供租户ID,使用默认租户ID
+ Long defaultTenantId = tenantProperties.getDefaultTenantId();
+ if (defaultTenantId != null) {
+ TenantContextHolder.setTenantId(defaultTenantId);
+ log.debug("使用默认租户ID: {}", defaultTenantId);
+ }
+ }
+
+ // 继续执行过滤器链
+ filterChain.doFilter(request, response);
+
+ } finally {
+ // 请求结束时清除租户上下文,避免线程池复用导致的数据泄露
+ TenantContextHolder.clear();
+ }
+ }
+}
+
diff --git a/src/main/java/com/youlai/boot/plugin/mybatis/TenantLineHandler.java b/src/main/java/com/youlai/boot/plugin/mybatis/TenantLineHandler.java
new file mode 100644
index 00000000..1b33ba0c
--- /dev/null
+++ b/src/main/java/com/youlai/boot/plugin/mybatis/TenantLineHandler.java
@@ -0,0 +1,90 @@
+package com.youlai.boot.plugin.mybatis;
+
+import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
+import com.youlai.boot.common.tenant.TenantContextHolder;
+import com.youlai.boot.config.property.TenantProperties;
+import lombok.RequiredArgsConstructor;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.LongValue;
+import net.sf.jsqlparser.expression.NullValue;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * MyBatis-Plus 多租户处理器
+ *
+ * 实现 TenantLineHandler 接口,自动为 SQL 添加租户过滤条件
+ * 仅在启用多租户时注册(通过 @ConditionalOnProperty 控制)
+ *
+ *
+ * @author Ray.Hao
+ * @since 3.0.0
+ */
+@Component
+@RequiredArgsConstructor
+@ConditionalOnProperty(prefix = "youlai.tenant", name = "enabled", havingValue = "true", matchIfMissing = false)
+public class TenantLineHandler implements com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler {
+
+ private final TenantProperties tenantProperties;
+
+ /**
+ * 获取租户ID表达式
+ *
+ * 从 TenantContextHolder 获取当前租户ID
+ * 如果未设置或忽略租户,返回 NULL(不添加租户条件)
+ *
+ *
+ * @return 租户ID表达式
+ */
+ @Override
+ public Expression getTenantId() {
+ // 如果设置了忽略租户标志,返回 NULL(不添加租户条件)
+ if (TenantContextHolder.isIgnoreTenant()) {
+ return new NullValue();
+ }
+
+ // 获取当前租户ID
+ Long tenantId = TenantContextHolder.getTenantId();
+
+ // 如果未设置租户ID,使用默认租户ID
+ if (tenantId == null) {
+ tenantId = tenantProperties.getDefaultTenantId();
+ }
+
+ return new LongValue(tenantId);
+ }
+
+ /**
+ * 获取租户字段名
+ *
+ * @return 租户字段名
+ */
+ @Override
+ public String getTenantIdColumn() {
+ return tenantProperties.getColumn();
+ }
+
+ /**
+ * 判断表是否忽略多租户过滤
+ *
+ * 系统表、租户表等不需要租户隔离的表应返回 true
+ *
+ *
+ * @param tableName 表名
+ * @return true-忽略,false-不忽略
+ */
+ @Override
+ public boolean ignoreTable(String tableName) {
+ List ignoreTables = tenantProperties.getIgnoreTables();
+ if (ignoreTables == null || ignoreTables.isEmpty()) {
+ return false;
+ }
+
+ // 忽略表名匹配(不区分大小写)
+ return ignoreTables.stream()
+ .anyMatch(ignoreTable -> ignoreTable.equalsIgnoreCase(tableName));
+ }
+}
+
diff --git a/src/main/java/com/youlai/boot/system/controller/TenantController.java b/src/main/java/com/youlai/boot/system/controller/TenantController.java
new file mode 100644
index 00000000..dfd0ff3a
--- /dev/null
+++ b/src/main/java/com/youlai/boot/system/controller/TenantController.java
@@ -0,0 +1,110 @@
+package com.youlai.boot.system.controller;
+
+import com.youlai.boot.common.tenant.TenantContextHolder;
+import com.youlai.boot.core.web.Result;
+import com.youlai.boot.security.util.SecurityUtils;
+import com.youlai.boot.system.model.vo.TenantVO;
+import com.youlai.boot.system.service.TenantService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 租户管理控制器
+ *
+ * 提供租户切换、查询等功能
+ *
+ *
+ * @author Ray.Hao
+ * @since 3.0.0
+ */
+@Tag(name = "租户管理接口")
+@RestController
+@RequestMapping("/api/v1/tenant")
+@RequiredArgsConstructor
+@Slf4j
+@ConditionalOnProperty(prefix = "youlai.tenant", name = "enabled", havingValue = "true", matchIfMissing = false)
+public class TenantController {
+
+ private final TenantService tenantService;
+
+ /**
+ * 获取当前用户的租户列表
+ *
+ * 根据当前登录用户查询其所属的所有租户
+ *
+ *
+ * @return 租户列表
+ */
+ @Operation(summary = "获取当前用户的租户列表")
+ @GetMapping("/list")
+ public Result> getTenantList() {
+ Long userId = SecurityUtils.getUserId();
+ List tenantList = tenantService.getTenantListByUserId(userId);
+ log.info("获取用户 {} 的租户列表,共 {} 个租户", userId, tenantList.size());
+ return Result.success(tenantList);
+ }
+
+ /**
+ * 获取当前租户信息
+ *
+ * @return 当前租户信息
+ */
+ @Operation(summary = "获取当前租户信息")
+ @GetMapping("/current")
+ public Result getCurrentTenant() {
+ Long tenantId = TenantContextHolder.getTenantId();
+ if (tenantId == null) {
+ return Result.success(null);
+ }
+ TenantVO tenant = tenantService.getTenantById(tenantId);
+ return Result.success(tenant);
+ }
+
+ /**
+ * 切换租户
+ *
+ * 切换当前用户的租户上下文,需要验证用户是否有权限访问该租户
+ *
+ *
+ * @param tenantId 目标租户ID
+ * @return 切换结果
+ */
+ @Operation(summary = "切换租户")
+ @PostMapping("/switch/{tenantId}")
+ public Result switchTenant(
+ @Parameter(description = "租户ID") @PathVariable Long tenantId
+ ) {
+ Long userId = SecurityUtils.getUserId();
+ log.info("用户 {} 请求切换租户到 {}", userId, tenantId);
+
+ // 验证用户是否有权限访问该租户
+ boolean hasPermission = tenantService.hasTenantPermission(userId, tenantId);
+ if (!hasPermission) {
+ log.warn("用户 {} 无权限访问租户 {}", userId, tenantId);
+ return Result.failed("无权限访问该租户");
+ }
+
+ // 验证租户是否存在且正常
+ TenantVO tenant = tenantService.getTenantById(tenantId);
+ if (tenant == null) {
+ return Result.failed("租户不存在");
+ }
+ if (tenant.getStatus() == null || tenant.getStatus() != 1) {
+ return Result.failed("租户已禁用");
+ }
+
+ // 设置新的租户上下文
+ TenantContextHolder.setTenantId(tenantId);
+ log.info("用户 {} 成功切换租户到 {}", userId, tenantId);
+
+ return Result.success(tenant);
+ }
+}
+
diff --git a/src/main/java/com/youlai/boot/system/mapper/TenantMapper.java b/src/main/java/com/youlai/boot/system/mapper/TenantMapper.java
new file mode 100644
index 00000000..802e54ab
--- /dev/null
+++ b/src/main/java/com/youlai/boot/system/mapper/TenantMapper.java
@@ -0,0 +1,16 @@
+package com.youlai.boot.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.youlai.boot.system.model.entity.Tenant;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 租户 Mapper
+ *
+ * @author Ray.Hao
+ * @since 3.0.0
+ */
+@Mapper
+public interface TenantMapper extends BaseMapper {
+}
+
diff --git a/src/main/java/com/youlai/boot/system/mapper/UserTenantMapper.java b/src/main/java/com/youlai/boot/system/mapper/UserTenantMapper.java
new file mode 100644
index 00000000..9be70dc6
--- /dev/null
+++ b/src/main/java/com/youlai/boot/system/mapper/UserTenantMapper.java
@@ -0,0 +1,16 @@
+package com.youlai.boot.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.youlai.boot.system.model.entity.UserTenant;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 用户租户关联 Mapper
+ *
+ * @author Ray.Hao
+ * @since 3.0.0
+ */
+@Mapper
+public interface UserTenantMapper extends BaseMapper {
+}
+
diff --git a/src/main/java/com/youlai/boot/system/model/entity/Tenant.java b/src/main/java/com/youlai/boot/system/model/entity/Tenant.java
new file mode 100644
index 00000000..39cd414a
--- /dev/null
+++ b/src/main/java/com/youlai/boot/system/model/entity/Tenant.java
@@ -0,0 +1,71 @@
+package com.youlai.boot.system.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.youlai.boot.common.base.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * 租户实体
+ *
+ * @author Ray.Hao
+ * @since 3.0.0
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("sys_tenant")
+public class Tenant extends BaseEntity {
+
+ /**
+ * 租户名称
+ */
+ private String name;
+
+ /**
+ * 租户编码(唯一)
+ */
+ private String code;
+
+ /**
+ * 联系人姓名
+ */
+ private String contactName;
+
+ /**
+ * 联系人电话
+ */
+ private String contactPhone;
+
+ /**
+ * 联系人邮箱
+ */
+ private String contactEmail;
+
+ /**
+ * 租户域名(用于域名识别)
+ */
+ private String domain;
+
+ /**
+ * 租户Logo
+ */
+ private String logo;
+
+ /**
+ * 状态(1-正常 0-禁用)
+ */
+ private Integer status;
+
+ /**
+ * 备注
+ */
+ private String remark;
+
+ /**
+ * 过期时间(NULL表示永不过期)
+ */
+ private LocalDateTime expireTime;
+}
+
diff --git a/src/main/java/com/youlai/boot/system/model/entity/UserTenant.java b/src/main/java/com/youlai/boot/system/model/entity/UserTenant.java
new file mode 100644
index 00000000..46becb9d
--- /dev/null
+++ b/src/main/java/com/youlai/boot/system/model/entity/UserTenant.java
@@ -0,0 +1,34 @@
+package com.youlai.boot.system.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.youlai.boot.common.base.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 用户租户关联实体
+ *
+ * @author Ray.Hao
+ * @since 3.0.0
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("sys_user_tenant")
+public class UserTenant extends BaseEntity {
+
+ /**
+ * 用户ID
+ */
+ private Long userId;
+
+ /**
+ * 租户ID
+ */
+ private Long tenantId;
+
+ /**
+ * 是否默认租户(1-是 0-否)
+ */
+ private Integer isDefault;
+}
+
diff --git a/src/main/java/com/youlai/boot/system/model/vo/TenantVO.java b/src/main/java/com/youlai/boot/system/model/vo/TenantVO.java
new file mode 100644
index 00000000..884580cf
--- /dev/null
+++ b/src/main/java/com/youlai/boot/system/model/vo/TenantVO.java
@@ -0,0 +1,48 @@
+package com.youlai.boot.system.model.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 租户VO
+ *
+ * @author Ray.Hao
+ * @since 3.0.0
+ */
+@Data
+@Schema(description = "租户信息")
+public class TenantVO implements Serializable {
+
+ @Schema(description = "租户ID")
+ private Long id;
+
+ @Schema(description = "租户名称")
+ private String name;
+
+ @Schema(description = "租户编码")
+ private String code;
+
+ @Schema(description = "租户状态(1-正常 0-禁用)")
+ private Integer status;
+
+ @Schema(description = "联系人姓名")
+ private String contactName;
+
+ @Schema(description = "联系人电话")
+ private String contactPhone;
+
+ @Schema(description = "联系人邮箱")
+ private String contactEmail;
+
+ @Schema(description = "租户域名")
+ private String domain;
+
+ @Schema(description = "租户Logo")
+ private String logo;
+
+ @Schema(description = "是否默认租户")
+ private Boolean isDefault;
+}
+
diff --git a/src/main/java/com/youlai/boot/system/service/TenantService.java b/src/main/java/com/youlai/boot/system/service/TenantService.java
new file mode 100644
index 00000000..36dc36f7
--- /dev/null
+++ b/src/main/java/com/youlai/boot/system/service/TenantService.java
@@ -0,0 +1,50 @@
+package com.youlai.boot.system.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.youlai.boot.system.model.entity.Tenant;
+import com.youlai.boot.system.model.vo.TenantVO;
+
+import java.util.List;
+
+/**
+ * 租户服务接口
+ *
+ * @author Ray.Hao
+ * @since 3.0.0
+ */
+public interface TenantService extends IService {
+
+ /**
+ * 根据用户ID查询用户所属的租户列表
+ *
+ * @param userId 用户ID
+ * @return 租户列表
+ */
+ List getTenantListByUserId(Long userId);
+
+ /**
+ * 根据租户ID查询租户信息
+ *
+ * @param tenantId 租户ID
+ * @return 租户信息
+ */
+ TenantVO getTenantById(Long tenantId);
+
+ /**
+ * 根据域名查询租户ID
+ *
+ * @param domain 域名
+ * @return 租户ID
+ */
+ Long getTenantIdByDomain(String domain);
+
+ /**
+ * 验证用户是否有权限访问指定租户
+ *
+ * @param userId 用户ID
+ * @param tenantId 租户ID
+ * @return true-有权限,false-无权限
+ */
+ boolean hasTenantPermission(Long userId, Long tenantId);
+}
+
diff --git a/src/main/java/com/youlai/boot/system/service/impl/TenantServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/TenantServiceImpl.java
new file mode 100644
index 00000000..8d11aa49
--- /dev/null
+++ b/src/main/java/com/youlai/boot/system/service/impl/TenantServiceImpl.java
@@ -0,0 +1,125 @@
+package com.youlai.boot.system.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.youlai.boot.common.tenant.TenantContextHolder;
+import com.youlai.boot.system.mapper.TenantMapper;
+import com.youlai.boot.system.mapper.UserTenantMapper;
+import com.youlai.boot.system.model.entity.Tenant;
+import com.youlai.boot.system.model.entity.UserTenant;
+import com.youlai.boot.system.model.vo.TenantVO;
+import com.youlai.boot.system.service.TenantService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 租户服务实现类
+ *
+ * @author Ray.Hao
+ * @since 3.0.0
+ */
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class TenantServiceImpl extends ServiceImpl implements TenantService {
+
+ private final UserTenantMapper userTenantMapper;
+
+ @Override
+ public List getTenantListByUserId(Long userId) {
+ // 临时忽略租户过滤,查询所有租户
+ TenantContextHolder.setIgnoreTenant(true);
+ try {
+ // 查询用户关联的租户ID列表
+ List userTenants = userTenantMapper.selectList(
+ new LambdaQueryWrapper()
+ .eq(UserTenant::getUserId, userId)
+ );
+
+ if (userTenants.isEmpty()) {
+ return List.of();
+ }
+
+ // 提取租户ID列表
+ List tenantIds = userTenants.stream()
+ .map(UserTenant::getTenantId)
+ .collect(Collectors.toList());
+
+ // 查询租户信息
+ List tenants = this.list(
+ new LambdaQueryWrapper()
+ .in(Tenant::getId, tenantIds)
+ .eq(Tenant::getStatus, 1) // 只查询正常状态的租户
+ .orderByDesc(Tenant::getId)
+ );
+
+ // 转换为VO并标记默认租户
+ return tenants.stream().map(tenant -> {
+ TenantVO vo = new TenantVO();
+ BeanUtils.copyProperties(tenant, vo);
+ // 查找是否为默认租户
+ userTenants.stream()
+ .filter(ut -> ut.getTenantId().equals(tenant.getId()) && ut.getIsDefault() == 1)
+ .findFirst()
+ .ifPresent(ut -> vo.setIsDefault(true));
+ return vo;
+ }).collect(Collectors.toList());
+ } finally {
+ TenantContextHolder.setIgnoreTenant(false);
+ }
+ }
+
+ @Override
+ public TenantVO getTenantById(Long tenantId) {
+ TenantContextHolder.setIgnoreTenant(true);
+ try {
+ Tenant tenant = this.getById(tenantId);
+ if (tenant == null) {
+ return null;
+ }
+ TenantVO vo = new TenantVO();
+ BeanUtils.copyProperties(tenant, vo);
+ return vo;
+ } finally {
+ TenantContextHolder.setIgnoreTenant(false);
+ }
+ }
+
+ @Override
+ public Long getTenantIdByDomain(String domain) {
+ TenantContextHolder.setIgnoreTenant(true);
+ try {
+ Tenant tenant = this.getOne(
+ new LambdaQueryWrapper()
+ .eq(Tenant::getDomain, domain)
+ .eq(Tenant::getStatus, 1)
+ .last("LIMIT 1")
+ );
+ return tenant != null ? tenant.getId() : null;
+ } finally {
+ TenantContextHolder.setIgnoreTenant(false);
+ }
+ }
+
+ @Override
+ public boolean hasTenantPermission(Long userId, Long tenantId) {
+ TenantContextHolder.setIgnoreTenant(true);
+ try {
+ UserTenant userTenant = userTenantMapper.selectOne(
+ new LambdaQueryWrapper()
+ .eq(UserTenant::getUserId, userId)
+ .eq(UserTenant::getTenantId, tenantId)
+ .last("LIMIT 1")
+ );
+ return userTenant != null;
+ } finally {
+ TenantContextHolder.setIgnoreTenant(false);
+ }
+ }
+}
+
From 36d2db6dc559817bd4e02b1f33b1998891850497 Mon Sep 17 00:00:00 2001
From: "Ray.Hao" <1490493387@qq.com>
Date: Wed, 10 Dec 2025 21:16:37 +0800
Subject: [PATCH 09/22] =?UTF-8?q?feat(system):=20=E6=9B=B4=E6=96=B0?=
=?UTF-8?q?=E6=9D=83=E9=99=90=E6=A0=87=E8=AF=86=E7=AC=A6=E5=B9=B6=E4=BC=98?=
=?UTF-8?q?=E5=8C=96=E8=8F=9C=E5=8D=95=E7=B1=BB=E5=9E=8B=E6=9E=9A=E4=B8=BE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../system/controller/ConfigController.java | 4 +--
.../system/controller/DeptController.java | 4 +--
.../system/controller/DictController.java | 8 ++---
.../system/controller/MenuController.java | 8 ++---
.../system/controller/NoticeController.java | 8 ++---
.../system/controller/RoleController.java | 9 +++---
.../system/controller/UserController.java | 8 ++---
.../boot/system/enums/MenuTypeEnum.java | 29 ++++++++++---------
.../youlai/boot/system/model/entity/Menu.java | 4 +--
.../boot/system/model/form/MenuForm.java | 4 +--
.../youlai/boot/system/model/vo/MenuVO.java | 4 +--
.../system/service/impl/MenuServiceImpl.java | 28 +++++++++++-------
12 files changed, 65 insertions(+), 53 deletions(-)
diff --git a/src/main/java/com/youlai/boot/system/controller/ConfigController.java b/src/main/java/com/youlai/boot/system/controller/ConfigController.java
index dbb4b81e..a7df99ed 100644
--- a/src/main/java/com/youlai/boot/system/controller/ConfigController.java
+++ b/src/main/java/com/youlai/boot/system/controller/ConfigController.java
@@ -36,7 +36,7 @@ public class ConfigController {
@Operation(summary = "系统配置分页列表")
@GetMapping("/page")
- @PreAuthorize("@ss.hasPerm('sys:config:query')")
+ @PreAuthorize("@ss.hasPerm('sys:config:list')")
@Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING)
public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) {
IPage result = configService.page(configPageQuery);
@@ -45,7 +45,7 @@ public class ConfigController {
@Operation(summary = "新增系统配置")
@PostMapping
- @PreAuthorize("@ss.hasPerm('sys:config:add')")
+ @PreAuthorize("@ss.hasPerm('sys:config:create')")
@Log( value = "新增系统配置",module = LogModuleEnum.SETTING)
public Result> save(@RequestBody @Valid ConfigForm configForm) {
return Result.judge(configService.save(configForm));
diff --git a/src/main/java/com/youlai/boot/system/controller/DeptController.java b/src/main/java/com/youlai/boot/system/controller/DeptController.java
index bb001700..bb7fae27 100644
--- a/src/main/java/com/youlai/boot/system/controller/DeptController.java
+++ b/src/main/java/com/youlai/boot/system/controller/DeptController.java
@@ -52,7 +52,7 @@ public class DeptController {
@Operation(summary = "新增部门")
@PostMapping
- @PreAuthorize("@ss.hasPerm('sys:dept:add')")
+ @PreAuthorize("@ss.hasPerm('sys:dept:create')")
@RepeatSubmit
public Result> saveDept(
@Valid @RequestBody DeptForm formData
@@ -72,7 +72,7 @@ public class DeptController {
@Operation(summary = "修改部门")
@PutMapping(value = "/{deptId}")
- @PreAuthorize("@ss.hasPerm('sys:dept:edit')")
+ @PreAuthorize("@ss.hasPerm('sys:dept:update')")
public Result> updateDept(
@PathVariable Long deptId,
@Valid @RequestBody DeptForm formData
diff --git a/src/main/java/com/youlai/boot/system/controller/DictController.java b/src/main/java/com/youlai/boot/system/controller/DictController.java
index 45c64280..f2fb1d10 100644
--- a/src/main/java/com/youlai/boot/system/controller/DictController.java
+++ b/src/main/java/com/youlai/boot/system/controller/DictController.java
@@ -77,7 +77,7 @@ public class DictController {
@Operation(summary = "新增字典")
@PostMapping
- @PreAuthorize("@ss.hasPerm('sys:dict:add')")
+ @PreAuthorize("@ss.hasPerm('sys:dict:create')")
@RepeatSubmit
public Result> saveDict(@Valid @RequestBody DictForm formData) {
boolean result = dictService.saveDict(formData);
@@ -90,7 +90,7 @@ public class DictController {
@Operation(summary = "修改字典")
@PutMapping("/{id}")
- @PreAuthorize("@ss.hasPerm('sys:dict:edit')")
+ @PreAuthorize("@ss.hasPerm('sys:dict:update')")
public Result> updateDict(
@PathVariable Long id,
@RequestBody DictForm dictForm
@@ -148,7 +148,7 @@ public class DictController {
@Operation(summary = "新增字典项")
@PostMapping("/{dictCode}/items")
- @PreAuthorize("@ss.hasPerm('sys:dict-item:add')")
+ @PreAuthorize("@ss.hasPerm('sys:dict-item:create')")
@RepeatSubmit
public Result saveDictItem(
@PathVariable String dictCode,
@@ -177,7 +177,7 @@ public class DictController {
@Operation(summary = "修改字典项")
@PutMapping("/{dictCode}/items/{itemId}")
- @PreAuthorize("@ss.hasPerm('sys:dict-item:edit')")
+ @PreAuthorize("@ss.hasPerm('sys:dict-item:update')")
@RepeatSubmit
public Result> updateDictItem(
@PathVariable String dictCode,
diff --git a/src/main/java/com/youlai/boot/system/controller/MenuController.java b/src/main/java/com/youlai/boot/system/controller/MenuController.java
index c35199f4..e7e014a6 100644
--- a/src/main/java/com/youlai/boot/system/controller/MenuController.java
+++ b/src/main/java/com/youlai/boot/system/controller/MenuController.java
@@ -62,7 +62,7 @@ public class MenuController {
@Operation(summary = "菜单表单数据")
@GetMapping("/{id}/form")
- @PreAuthorize("@ss.hasPerm('sys:menu:edit')")
+ @PreAuthorize("@ss.hasPerm('sys:menu:update')")
public Result getMenuForm(
@Parameter(description = "菜单ID") @PathVariable Long id
) {
@@ -72,7 +72,7 @@ public class MenuController {
@Operation(summary = "新增菜单")
@PostMapping
- @PreAuthorize("@ss.hasPerm('sys:menu:add')")
+ @PreAuthorize("@ss.hasPerm('sys:menu:create')")
@RepeatSubmit
public Result> addMenu(@RequestBody MenuForm menuForm) {
boolean result = menuService.saveMenu(menuForm);
@@ -81,7 +81,7 @@ public class MenuController {
@Operation(summary = "修改菜单")
@PutMapping(value = "/{id}")
- @PreAuthorize("@ss.hasPerm('sys:menu:edit')")
+ @PreAuthorize("@ss.hasPerm('sys:menu:update')")
public Result> updateMenu(
@RequestBody MenuForm menuForm
) {
@@ -101,7 +101,7 @@ public class MenuController {
@Operation(summary = "修改菜单显示状态")
@PatchMapping("/{menuId}")
- @PreAuthorize("@ss.hasPerm('sys:menu:edit')")
+ @PreAuthorize("@ss.hasPerm('sys:menu:update')")
public Result> updateMenuVisible(
@Parameter(description = "菜单ID") @PathVariable Long menuId,
@Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible
diff --git a/src/main/java/com/youlai/boot/system/controller/NoticeController.java b/src/main/java/com/youlai/boot/system/controller/NoticeController.java
index 1568dbf1..1382aa5b 100644
--- a/src/main/java/com/youlai/boot/system/controller/NoticeController.java
+++ b/src/main/java/com/youlai/boot/system/controller/NoticeController.java
@@ -37,7 +37,7 @@ public class NoticeController {
@Operation(summary = "通知公告分页列表")
@GetMapping("/page")
- @PreAuthorize("@ss.hasPerm('sys:notice:query')")
+ @PreAuthorize("@ss.hasPerm('sys:notice:list')")
public PageResult getNoticePage(NoticePageQuery queryParams) {
IPage result = noticeService.getNoticePage(queryParams);
return PageResult.success(result);
@@ -45,7 +45,7 @@ public class NoticeController {
@Operation(summary = "新增通知公告")
@PostMapping
- @PreAuthorize("@ss.hasPerm('sys:notice:add')")
+ @PreAuthorize("@ss.hasPerm('sys:notice:create')")
public Result> saveNotice(@RequestBody @Valid NoticeForm formData) {
boolean result = noticeService.saveNotice(formData);
return Result.judge(result);
@@ -53,7 +53,7 @@ public class NoticeController {
@Operation(summary = "获取通知公告表单数据")
@GetMapping("/{id}/form")
- @PreAuthorize("@ss.hasPerm('sys:notice:edit')")
+ @PreAuthorize("@ss.hasPerm('sys:notice:update')")
public Result getNoticeForm(
@Parameter(description = "通知公告ID") @PathVariable Long id
) {
@@ -72,7 +72,7 @@ public class NoticeController {
@Operation(summary = "修改通知公告")
@PutMapping(value = "/{id}")
- @PreAuthorize("@ss.hasPerm('sys:notice:edit')")
+ @PreAuthorize("@ss.hasPerm('sys:notice:update')")
public Result updateNotice(
@Parameter(description = "通知公告ID") @PathVariable Long id,
@RequestBody @Validated NoticeForm formData
diff --git a/src/main/java/com/youlai/boot/system/controller/RoleController.java b/src/main/java/com/youlai/boot/system/controller/RoleController.java
index fef84c40..e178518d 100644
--- a/src/main/java/com/youlai/boot/system/controller/RoleController.java
+++ b/src/main/java/com/youlai/boot/system/controller/RoleController.java
@@ -55,7 +55,7 @@ public class RoleController {
@Operation(summary = "新增角色")
@PostMapping
- @PreAuthorize("@ss.hasPerm('sys:role:add')")
+ @PreAuthorize("@ss.hasPerm('sys:role:create')")
@RepeatSubmit
public Result> addRole(@Valid @RequestBody RoleForm roleForm) {
boolean result = roleService.saveRole(roleForm);
@@ -64,7 +64,7 @@ public class RoleController {
@Operation(summary = "获取角色表单数据")
@GetMapping("/{roleId}/form")
- @PreAuthorize("@ss.hasPerm('sys:role:edit')")
+ @PreAuthorize("@ss.hasPerm('sys:role:update')")
public Result getRoleForm(
@Parameter(description = "角色ID") @PathVariable Long roleId
) {
@@ -74,7 +74,7 @@ public class RoleController {
@Operation(summary = "修改角色")
@PutMapping(value = "/{id}")
- @PreAuthorize("@ss.hasPerm('sys:role:edit')")
+ @PreAuthorize("@ss.hasPerm('sys:role:update')")
public Result> updateRole(@Valid @RequestBody RoleForm roleForm) {
boolean result = roleService.saveRole(roleForm);
return Result.judge(result);
@@ -92,7 +92,7 @@ public class RoleController {
@Operation(summary = "修改角色状态")
@PutMapping(value = "/{roleId}/status")
- @PreAuthorize("@ss.hasPerm('sys:role:edit')")
+ @PreAuthorize("@ss.hasPerm('sys:role:update')")
public Result> updateRoleStatus(
@Parameter(description = "角色ID") @PathVariable Long roleId,
@Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status
@@ -112,6 +112,7 @@ public class RoleController {
@Operation(summary = "角色分配菜单权限")
@PutMapping("/{roleId}/menus")
+ @PreAuthorize("@ss.hasPerm('sys:role:assign')")
public Result assignMenusToRole(
@PathVariable Long roleId,
@RequestBody List menuIds
diff --git a/src/main/java/com/youlai/boot/system/controller/UserController.java b/src/main/java/com/youlai/boot/system/controller/UserController.java
index 644777df..db96814b 100644
--- a/src/main/java/com/youlai/boot/system/controller/UserController.java
+++ b/src/main/java/com/youlai/boot/system/controller/UserController.java
@@ -68,7 +68,7 @@ public class UserController {
@Operation(summary = "新增用户")
@PostMapping
- @PreAuthorize("@ss.hasPerm('sys:user:add')")
+ @PreAuthorize("@ss.hasPerm('sys:user:create')")
@RepeatSubmit
@Log(value = "新增用户", module = LogModuleEnum.USER)
public Result> saveUser(
@@ -80,7 +80,7 @@ public class UserController {
@Operation(summary = "获取用户表单数据")
@GetMapping("/{userId}/form")
- @PreAuthorize("@ss.hasPerm('sys:user:edit')")
+ @PreAuthorize("@ss.hasPerm('sys:user:update')")
@Log(value = "用户表单数据", module = LogModuleEnum.USER)
public Result getUserForm(
@Parameter(description = "用户ID") @PathVariable Long userId
@@ -91,7 +91,7 @@ public class UserController {
@Operation(summary = "修改用户")
@PutMapping(value = "/{userId}")
- @PreAuthorize("@ss.hasPerm('sys:user:edit')")
+ @PreAuthorize("@ss.hasPerm('sys:user:update')")
@Log(value = "修改用户", module = LogModuleEnum.USER)
public Result updateUser(
@Parameter(description = "用户ID") @PathVariable Long userId,
@@ -114,7 +114,7 @@ public class UserController {
@Operation(summary = "修改用户状态")
@PatchMapping(value = "/{userId}/status")
- @PreAuthorize("@ss.hasPerm('sys:user:edit')")
+ @PreAuthorize("@ss.hasPerm('sys:user:update')")
@Log(value = "修改用户状态", module = LogModuleEnum.USER)
public Result updateUserStatus(
@Parameter(description = "用户ID") @PathVariable Long userId,
diff --git a/src/main/java/com/youlai/boot/system/enums/MenuTypeEnum.java b/src/main/java/com/youlai/boot/system/enums/MenuTypeEnum.java
index d78fe5b0..8745ff7f 100644
--- a/src/main/java/com/youlai/boot/system/enums/MenuTypeEnum.java
+++ b/src/main/java/com/youlai/boot/system/enums/MenuTypeEnum.java
@@ -5,28 +5,31 @@ import com.youlai.boot.common.base.IBaseEnum;
import lombok.Getter;
/**
- * 菜单类型枚举
+ * 菜单类型枚举(char)
*
- * @author Ray.Hao
- * @since 2022/4/23 9:36
+ * C:目录
+ * M:菜单
+ * B:按钮
*/
@Getter
-public enum MenuTypeEnum implements IBaseEnum {
+public enum MenuTypeEnum implements IBaseEnum {
- NULL(0, null),
- MENU(1, "菜单"),
- CATALOG(2, "目录"),
- EXTLINK(3, "外链"),
- BUTTON(4, "按钮");
+ CATALOG("C", "目录"),
+ MENU("M", "菜单"),
+ BUTTON("B", "按钮");
- // Mybatis-Plus 提供注解表示插入数据库时插入该值
+ /**
+ * 数据库存储值
+ */
@EnumValue
- private final Integer value;
+ private final String value;
- // @JsonValue // 表示对枚举序列化时返回此字段
+ /**
+ * 友好名称
+ */
private final String label;
- MenuTypeEnum(Integer value, String label) {
+ MenuTypeEnum(String value, String label) {
this.value = value;
this.label = label;
}
diff --git a/src/main/java/com/youlai/boot/system/model/entity/Menu.java b/src/main/java/com/youlai/boot/system/model/entity/Menu.java
index 1e76b1e3..de3b2ca6 100644
--- a/src/main/java/com/youlai/boot/system/model/entity/Menu.java
+++ b/src/main/java/com/youlai/boot/system/model/entity/Menu.java
@@ -34,9 +34,9 @@ public class Menu {
private String name;
/**
- * 菜单类型(1-菜单;2-目录;3-外链;4-按钮权限)
+ * 菜单类型(C-目录 M-菜单 B-按钮)
*/
- private Integer type;
+ private String type;
/**
* 路由名称(Vue Router 中定义的路由名称)
diff --git a/src/main/java/com/youlai/boot/system/model/form/MenuForm.java b/src/main/java/com/youlai/boot/system/model/form/MenuForm.java
index 5d6bde07..dabc73ce 100644
--- a/src/main/java/com/youlai/boot/system/model/form/MenuForm.java
+++ b/src/main/java/com/youlai/boot/system/model/form/MenuForm.java
@@ -26,8 +26,8 @@ public class MenuForm {
@Schema(description = "菜单名称")
private String name;
- @Schema(description = "菜单类型(1-菜单 2-目录 3-外链 4-按钮)")
- private Integer type;
+ @Schema(description = "菜单类型(C-目录 M-菜单 B-按钮)")
+ private String type;
@Schema(description = "路由名称")
private String routeName;
diff --git a/src/main/java/com/youlai/boot/system/model/vo/MenuVO.java b/src/main/java/com/youlai/boot/system/model/vo/MenuVO.java
index 1b85a8b5..18a3ca52 100644
--- a/src/main/java/com/youlai/boot/system/model/vo/MenuVO.java
+++ b/src/main/java/com/youlai/boot/system/model/vo/MenuVO.java
@@ -19,8 +19,8 @@ public class MenuVO {
@Schema(description = "菜单名称")
private String name;
- @Schema(description="菜单类型")
- private Integer type;
+ @Schema(description="菜单类型(C-目录 M-菜单 B-按钮)")
+ private String type;
@Schema(description = "路由名称")
private String routeName;
diff --git a/src/main/java/com/youlai/boot/system/service/impl/MenuServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/MenuServiceImpl.java
index 4b2a644e..81120c24 100644
--- a/src/main/java/com/youlai/boot/system/service/impl/MenuServiceImpl.java
+++ b/src/main/java/com/youlai/boot/system/service/impl/MenuServiceImpl.java
@@ -205,19 +205,25 @@ public class MenuServiceImpl extends ServiceImpl implements Me
*/
private RouteVO toRouteVo(Menu menu) {
RouteVO routeVO = new RouteVO();
+ String routePath = menu.getRoutePath();
+ boolean externalLink = StrUtil.startWithAny(routePath, "http://", "https://");
+
// 获取路由名称
String routeName = menu.getRouteName();
if (StrUtil.isBlank(routeName)) {
- // 路由 name 需要驼峰,首字母大写
- routeName = StringUtils.capitalize(StrUtil.toCamelCase(menu.getRoutePath(), '-'));
+ // 外链不做驼峰转换,使用唯一占位,避免 http:// 被解析异常
+ routeName = externalLink
+ ? "ext-" + menu.getId()
+ : StringUtils.capitalize(StrUtil.toCamelCase(routePath, '-'));
}
// 根据name路由跳转 this.$router.push({name:xxx})
routeVO.setName(routeName);
// 根据path路由跳转 this.$router.push({path:xxx})
- routeVO.setPath(menu.getRoutePath());
+ routeVO.setPath(routePath);
routeVO.setRedirect(menu.getRedirect());
- routeVO.setComponent(menu.getComponent());
+ // 外链无组件
+ routeVO.setComponent(externalLink ? null : menu.getComponent());
RouteVO.Meta meta = new RouteVO.Meta();
meta.setTitle(menu.getName());
@@ -253,7 +259,9 @@ public class MenuServiceImpl extends ServiceImpl implements Me
@CacheEvict(cacheNames = "menu", key = "'routes'")
public boolean saveMenu(MenuForm menuForm) {
- Integer menuType = menuForm.getType();
+ String menuType = menuForm.getType();
+ boolean isExternalLink = MenuTypeEnum.MENU.getValue().equals(menuType)
+ && StrUtil.startWithAny(menuForm.getRoutePath(), "http://", "https://");
if (MenuTypeEnum.CATALOG.getValue().equals(menuType)) { // 如果是目录
String path = menuForm.getRoutePath();
@@ -261,8 +269,8 @@ public class MenuServiceImpl extends ServiceImpl implements Me
menuForm.setRoutePath("/" + path); // 一级目录需以 / 开头
}
menuForm.setComponent("Layout");
- } else if (MenuTypeEnum.EXTLINK.getValue().equals(menuType)) {
- // 外链菜单组件设置为 null
+ } else if (isExternalLink) {
+ // 外链菜单组件设置为 null,通过 routePath 判断外链
menuForm.setComponent(null);
}
if (Objects.equals(menuForm.getParentId(), menuForm.getId())) {
@@ -281,7 +289,7 @@ public class MenuServiceImpl extends ServiceImpl implements Me
entity.setParams(null);
}
// 新增类型为菜单时候 路由名称唯一
- if (MenuTypeEnum.MENU.getValue().equals(menuType)) {
+ if (MenuTypeEnum.MENU.getValue().equals(menuType) && !isExternalLink) {
Assert.isFalse(this.exists(new LambdaQueryWrapper