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 @@ + - - SELECT t1.TABLE_NAME , t1.TABLE_COMMENT , @@ -32,8 +33,38 @@ ORDER BY CREATE_TIME DESC + + - SELECT TABLE_NAME , TABLE_COMMENT , @@ -46,8 +77,25 @@ TABLE_SCHEMA = (SELECT DATABASE()) AND TABLE_NAME = #{tableName} + + - SELECT COLUMN_NAME, DATA_TYPE, @@ -64,6 +112,32 @@ AND TABLE_NAME = #{tableName} ORDER BY ORDINAL_POSITION ASC - + + 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 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(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 + + + 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 @@ + + @@ -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 + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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> availableFunctions() { - return List.of( - Map.of( - "name", "updateUserNickname", - "description", "根据用户名更新用户昵称", - "requiredParameters", List.of("username", "nickname") - ) - ); - } - - private ParseResult parseAiResponse(String rawContent) { - if (StrUtil.isBlank(rawContent)) { - throw new IllegalStateException("AI 返回内容为空"); + private String buildSystemPrompt() { + return SYSTEM_PROMPT; } - try { - JSONObject jsonObject = JSONUtil.parseObj(rawContent); - boolean success = jsonObject.getBool("success", false); - String explanation = jsonObject.getStr("explanation"); - Double confidence = jsonObject.containsKey("confidence") ? jsonObject.getDouble("confidence") : null; - String error = jsonObject.getStr("error"); - String provider = jsonObject.getStr("provider"); - String model = jsonObject.getStr("model"); + 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()); - List functionCalls = toFunctionCallList(jsonObject.getJSONArray("functionCalls")); - - return new ParseResult(success, explanation, confidence, error, provider, model, functionCalls); - } catch (Exception ex) { - throw new IllegalStateException("无法解析 AI 响应: " + ex.getMessage(), ex); - } - } - - private List toFunctionCallList(JSONArray array) { - if (array == null || array.isEmpty()) { - return Collections.emptyList(); + return StrUtil.format(""" + 请根据以下上下文识别用户意图,并输出符合系统提示要求的 JSON: + {} + """, JSONUtil.toJsonPrettyStr(payload)); } - List result = new ArrayList<>(); - for (Object element : array) { - JSONObject functionJson = JSONUtil.parseObj(element); - Map arguments = Optional.ofNullable(functionJson.getJSONObject("arguments")) - .map(obj -> obj.toBean(new TypeReference>() { - })) - .orElse(Collections.emptyMap()); - - result.add(AiFunctionCallDTO.builder() - .name(functionJson.getStr("name")) - .description(functionJson.getStr("description")) - .arguments(arguments) - .build()); - } - return result; - } - - private record ParseResult( - boolean success, - String explanation, - Double confidence, - String error, - String provider, - String model, - List functionCalls - ) { - } - - @Override - public Object executeCommand(AiExecuteRequestDTO request, HttpServletRequest httpRequest) throws Exception { - long startTime = System.currentTimeMillis(); - - // 获取用户信息 - Long userId = SecurityUtils.getUserId(); - String username = SecurityUtils.getUsername(); - String ipAddress = JakartaServletUtil.getClientIP(httpRequest); - - AiFunctionCallDTO functionCall = request.getFunctionCall(); - - // 根据解析日志ID获取审计记录,如果不存在则创建新记录 - AiCommandLog log; - if (StrUtil.isNotBlank(request.getParseLogId())) { - // 更新已存在的审计记录(解析阶段已创建) - log = logService.getById(request.getParseLogId()); - if (log == null) { - throw new IllegalStateException("未找到对应的解析记录,ID: " + request.getParseLogId()); - } - } else { - // 如果没有解析日志ID,创建新记录(兼容直接执行的情况) - log = new AiCommandLog(); - log.setUserId(userId); - log.setUsername(username); - log.setOriginalCommand(request.getOriginalCommand()); - log.setIpAddress(ipAddress); - logService.save(log); + private List> availableFunctions() { + return List.of( + Map.of( + "name", "updateUserNickname", + "description", "根据用户名更新用户昵称", + "requiredParameters", List.of("username", "nickname") + ) + ); } - // 更新执行相关字段 - log.setFunctionName(functionCall.getName()); - log.setFunctionArguments(JSONUtil.toJsonStr(functionCall.getArguments())); - log.setExecuteStatus(0); // 0-待执行 + private ParseResult parseAiResponse(String rawContent) { + if (StrUtil.isBlank(rawContent)) { + throw new IllegalStateException("AI 返回内容为空"); + } - try { - // 🎯 执行具体的函数调用 - Object result = executeFunctionCall(functionCall); + try { + JSONObject jsonObject = JSONUtil.parseObj(rawContent); + boolean success = jsonObject.getBool("success", false); + String explanation = jsonObject.getStr("explanation"); + Double confidence = jsonObject.containsKey("confidence") ? jsonObject.getDouble("confidence") : null; + String error = jsonObject.getStr("error"); + String provider = jsonObject.getStr("provider"); + String model = jsonObject.getStr("model"); - // 更新执行成功 - log.setExecuteStatus(1); // 1-成功 - log.setExecuteErrorMessage(null); + List functionCalls = toFunctionCallList(jsonObject.getJSONArray("functionCalls")); - // 更新审计记录 - logService.updateById(log); - - log.info("✅ 命令执行成功,审计记录ID: {}", log.getId()); - - return result; - - } catch (Exception e) { - // 更新执行失败 - log.setExecuteStatus(-1); // -1-失败 - log.setExecuteErrorMessage(e.getMessage()); - - // 更新审计记录 - logService.updateById(log); - - log.error("❌ 命令执行失败,审计记录ID: {}", log.getId(), e); - - // 抛出异常,由 Controller 统一处理 - throw e; - } - } - - /** - * 执行具体的函数调用 - */ - private Object executeFunctionCall(AiFunctionCallDTO functionCall) { - String functionName = functionCall.getName(); - Map arguments = functionCall.getArguments(); - - log.info("🎯 执行函数: {}, 参数: {}", functionName, arguments); - - // 根据函数名称路由到不同的处理器 - switch (functionName) { - case "updateUserNickname": - return executeUpdateUserNickname(arguments); - default: - throw new UnsupportedOperationException("不支持的函数: " + functionName); - } - } - - /** - * 使用 Tool: 根据用户名更新用户昵称 - */ - private Object executeUpdateUserNickname(Map arguments) { - String username = (String) arguments.get("username"); - String nickname = (String) arguments.get("nickname"); - - log.info("🔧 [Tool] 更新用户昵称: username={}, nickname={}", username, nickname); - String resultMsg = userTools.updateUserNickname(username, nickname); - - boolean success = resultMsg != null && resultMsg.contains("成功"); - if (!success) { - throw new RuntimeException(resultMsg != null ? resultMsg : "更新用户昵称失败"); + return new ParseResult(success, explanation, confidence, error, provider, model, functionCalls); + } catch (Exception ex) { + throw new IllegalStateException("无法解析 AI 响应: " + ex.getMessage(), ex); + } } - return Map.of("username", username, "nickname", nickname, "message", resultMsg); - } + private List toFunctionCallList(JSONArray array) { + if (array == null || array.isEmpty()) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + for (Object element : array) { + JSONObject functionJson = JSONUtil.parseObj(element); + Map arguments = Optional.ofNullable(functionJson.getJSONObject("arguments")) + .map(obj -> obj.toBean(new TypeReference>() { + })) + .orElse(Collections.emptyMap()); + + result.add(AiFunctionCallDTO.builder() + .name(functionJson.getStr("name")) + .description(functionJson.getStr("description")) + .arguments(arguments) + .build()); + } + return result; + } + + private record ParseResult( + boolean success, + String explanation, + Double confidence, + String error, + String provider, + String model, + List functionCalls + ) { + } + + @Override + public Object executeCommand(AiExecuteRequestDTO request, HttpServletRequest httpRequest) throws Exception { + long startTime = System.currentTimeMillis(); + + // 获取用户信息 + Long userId = SecurityUtils.getUserId(); + String username = SecurityUtils.getUsername(); + String ipAddress = JakartaServletUtil.getClientIP(httpRequest); + + AiFunctionCallDTO functionCall = request.getFunctionCall(); + + // 根据解析日志ID获取审计记录,如果不存在则创建新记录 + AiCommandLog commandLog; + if (StrUtil.isNotBlank(request.getParseLogId())) { + // 更新已存在的审计记录(解析阶段已创建) + commandLog = logService.getById(request.getParseLogId()); + if (commandLog == null) { + throw new IllegalStateException("未找到对应的解析记录,ID: " + request.getParseLogId()); + } + } else { + // 如果没有解析日志ID,创建新记录(兼容直接执行的情况) + commandLog = new AiCommandLog(); + commandLog.setUserId(userId); + commandLog.setUsername(username); + commandLog.setOriginalCommand(request.getOriginalCommand()); + commandLog.setIpAddress(ipAddress); + logService.save(commandLog); + } + + // 更新执行相关字段 + commandLog.setFunctionName(functionCall.getName()); + commandLog.setFunctionArguments(JSONUtil.toJsonStr(functionCall.getArguments())); + commandLog.setExecuteStatus(0); // 0-待执行 + + try { + // 🎯 执行具体的函数调用 + Object result = executeFunctionCall(functionCall); + + // 更新执行成功 + commandLog.setExecuteStatus(1); // 1-成功 + commandLog.setExecuteErrorMessage(null); + + // 更新审计记录 + logService.updateById(commandLog); + + log.info("✅ 命令执行成功,审计记录ID: {}", commandLog.getId()); + + return result; + + } catch (Exception e) { + // 更新执行失败 + commandLog.setExecuteStatus(-1); // -1-失败 + commandLog.setExecuteErrorMessage(e.getMessage()); + + // 更新审计记录 + logService.updateById(commandLog); + + log.error("❌ 命令执行失败,审计记录ID: {}", commandLog.getId(), e); + + // 抛出异常,由 Controller 统一处理 + throw e; + } + } + + /** + * 执行具体的函数调用 + */ + private Object executeFunctionCall(AiFunctionCallDTO functionCall) { + String functionName = functionCall.getName(); + Map arguments = functionCall.getArguments(); + + log.info("🎯 执行函数: {}, 参数: {}", functionName, arguments); + + // 根据函数名称路由到不同的处理器 + switch (functionName) { + case "updateUserNickname": + return executeUpdateUserNickname(arguments); + default: + throw new UnsupportedOperationException("不支持的函数: " + functionName); + } + } + + /** + * 使用 Tool: 根据用户名更新用户昵称 + */ + private Object executeUpdateUserNickname(Map arguments) { + String username = (String) arguments.get("username"); + String nickname = (String) arguments.get("nickname"); + + log.info("🔧 [Tool] 更新用户昵称: username={}, nickname={}", username, nickname); + String resultMsg = userTools.updateUserNickname(username, nickname); + + boolean success = resultMsg != null && resultMsg.contains("成功"); + if (!success) { + throw new RuntimeException(resultMsg != null ? resultMsg : "更新用户昵称失败"); + } + + return Map.of("username", username, "nickname", nickname, "message", resultMsg); + } } From f16c1e62279b2daa4d223dbebfba18b3fe27a280 Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Wed, 10 Dec 2025 21:07:35 +0800 Subject: [PATCH 07/22] =?UTF-8?q?refactor(ai):=20=E9=87=8D=E6=9E=84AI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/mysql/youlai_boot.sql | 610 ------------------ .../youlai/boot/common/base/BaseEntity.java | 21 + .../com/youlai/boot/config/MybatisConfig.java | 21 + .../ai/mapper/AiCommandLogMapper.java | 1 + .../ai/model/entity/AiCommandLog.java | 1 + .../platform/ai/model/vo/AiCommandLogVO.java | 1 + .../ai/service/AiCommandLogService.java | 1 + .../service/impl/AiCommandLogServiceImpl.java | 1 + .../plugin/mybatis/MyMetaObjectHandler.java | 34 +- src/main/resources/application-dev.yml | 35 +- src/main/resources/application-prod.yml | 6 +- .../mapper/ai/AiCommandLogMapper.xml | 1 + 12 files changed, 116 insertions(+), 617 deletions(-) delete mode 100644 sql/mysql/youlai_boot.sql diff --git a/sql/mysql/youlai_boot.sql b/sql/mysql/youlai_boot.sql deleted file mode 100644 index dc52c6cd..00000000 --- a/sql/mysql/youlai_boot.sql +++ /dev/null @@ -1,610 +0,0 @@ - -# YouLai_Boot 数据库(MySQL 5.7 ~ MySQL 8.x) -# Copyright (c) 2021-present, youlai.tech - - --- ---------------------------- --- 1. 创建数据库 --- ---------------------------- -CREATE DATABASE IF NOT EXISTS youlai_boot CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; - - --- ---------------------------- --- 2. 创建表 && 数据初始化 --- ---------------------------- -use youlai_boot; - -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` tinyint NOT NULL COMMENT '菜单类型(1-菜单 2-目录 3-外链 4-按钮)', - `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 --- ---------------------------- -INSERT INTO `sys_menu` VALUES (1, 0, '0', '系统管理', 2, '', '/system', 'Layout', NULL, NULL, NULL, 1, 1, 'system', '/system/user', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (2, 1, '0,1', '用户管理', 1, 'User', 'user', 'system/user/index', NULL, NULL, 1, 1, 1, 'el-icon-User', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (3, 1, '0,1', '角色管理', 1, 'Role', 'role', 'system/role/index', NULL, NULL, 1, 1, 2, 'role', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (4, 1, '0,1', '菜单管理', 1, 'SysMenu', 'menu', 'system/menu/index', NULL, NULL, 1, 1, 3, 'menu', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (5, 1, '0,1', '部门管理', 1, 'Dept', 'dept', 'system/dept/index', NULL, NULL, 1, 1, 4, 'tree', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (6, 1, '0,1', '字典管理', 1, 'Dict', 'dict', 'system/dict/index', NULL, NULL, 1, 1, 5, 'dict', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (20, 0, '0', '多级菜单', 2, NULL, '/multi-level', 'Layout', NULL, 1, NULL, 1, 9, 'cascader', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (21, 20, '0,20', '菜单一级', 2, NULL, 'multi-level1', 'Layout', NULL, 1, NULL, 1, 1, '', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (22, 21, '0,20,21', '菜单二级', 2, NULL, 'multi-level2', 'Layout', NULL, 0, NULL, 1, 1, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (23, 22, '0,20,21,22', '菜单三级-1', 1, NULL, 'multi-level3-1', 'demo/multi-level/children/children/level3-1', NULL, 0, 1, 1, 1, '', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (24, 22, '0,20,21,22', '菜单三级-2', 1, NULL, 'multi-level3-2', 'demo/multi-level/children/children/level3-2', NULL, 0, 1, 1, 2, '', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (26, 0, '0', '平台文档', 2, '', '/doc', 'Layout', NULL, NULL, NULL, 1, 8, 'document', 'https://juejin.cn/post/7228990409909108793', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (30, 26, '0,26', '平台文档(外链)', 3, NULL, 'https://juejin.cn/post/7228990409909108793', '', NULL, NULL, NULL, 1, 2, 'document', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (31, 2, '0,1,2', '用户新增', 4, NULL, '', NULL, 'sys:user:add', NULL, NULL, 1, 1, '', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (32, 2, '0,1,2', '用户编辑', 4, NULL, '', NULL, 'sys:user:edit', NULL, NULL, 1, 2, '', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (33, 2, '0,1,2', '用户删除', 4, NULL, '', NULL, 'sys:user:delete', NULL, NULL, 1, 3, '', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (36, 0, '0', '组件封装', 2, NULL, '/component', 'Layout', NULL, NULL, NULL, 1, 10, 'menu', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (37, 36, '0,36', '富文本编辑器', 1, 'WangEditor', 'wang-editor', 'demo/wang-editor', NULL, NULL, 1, 1, 2, '', '', NULL, NULL, NULL); -INSERT INTO `sys_menu` VALUES (38, 36, '0,36', '图片上传', 1, 'Upload', 'upload', 'demo/upload', NULL, NULL, 1, 1, 3, '', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (39, 36, '0,36', '图标选择器', 1, 'IconSelect', 'icon-select', 'demo/icon-select', NULL, NULL, 1, 1, 4, '', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (40, 0, '0', '接口文档', 2, NULL, '/api', 'Layout', NULL, 1, NULL, 1, 7, 'api', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (41, 40, '0,40', 'Apifox', 1, 'Apifox', 'apifox', 'demo/api/apifox', NULL, NULL, 1, 1, 1, 'api', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (70, 3, '0,1,3', '角色新增', 4, NULL, '', NULL, 'sys:role:add', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (71, 3, '0,1,3', '角色编辑', 4, NULL, '', NULL, 'sys:role:edit', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (72, 3, '0,1,3', '角色删除', 4, NULL, '', NULL, 'sys:role:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (73, 4, '0,1,4', '菜单新增', 4, NULL, '', NULL, 'sys:menu:add', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (74, 4, '0,1,4', '菜单编辑', 4, NULL, '', NULL, 'sys:menu:edit', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (75, 4, '0,1,4', '菜单删除', 4, NULL, '', NULL, 'sys:menu:delete', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (76, 5, '0,1,5', '部门新增', 4, NULL, '', NULL, 'sys:dept:add', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (77, 5, '0,1,5', '部门编辑', 4, NULL, '', NULL, 'sys:dept:edit', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (78, 5, '0,1,5', '部门删除', 4, NULL, '', NULL, 'sys:dept:delete', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (79, 6, '0,1,6', '字典新增', 4, NULL, '', NULL, 'sys:dict:add', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (81, 6, '0,1,6', '字典编辑', 4, NULL, '', NULL, 'sys:dict:edit', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (84, 6, '0,1,6', '字典删除', 4, NULL, '', NULL, 'sys:dict:delete', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (88, 2, '0,1,2', '重置密码', 4, NULL, '', NULL, 'sys:user:reset-password', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (89, 0, '0', '功能演示', 2, NULL, '/function', 'Layout', NULL, NULL, NULL, 1, 12, 'menu', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (90, 89, '0,89', 'Websocket', 1, 'WebSocket', '/function/websocket', 'demo/websocket', NULL, NULL, 1, 1, 3, '', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (95, 36, '0,36', '字典组件', 1, 'DictDemo', 'dict-demo', 'demo/dictionary', NULL, NULL, 1, 1, 4, '', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (97, 89, '0,89', 'Icons', 1, 'IconDemo', 'icon-demo', 'demo/icons', NULL, NULL, 1, 1, 2, 'el-icon-Notification', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (102, 26, '0,26', 'document', 3, NULL, 'internal-doc', 'demo/internal-doc', NULL, NULL, NULL, 1, 1, 'document', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (105, 2, '0,1,2', '用户查询', 4, NULL, '', NULL, 'sys:user:query', 0, 0, 1, 0, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (106, 2, '0,1,2', '用户导入', 4, NULL, '', NULL, 'sys:user:import', NULL, NULL, 1, 5, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (107, 2, '0,1,2', '用户导出', 4, NULL, '', NULL, 'sys:user:export', NULL, NULL, 1, 6, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (108, 36, '0,36', '增删改查', 1, 'Curd', 'curd', 'demo/curd/index', NULL, NULL, 1, 1, 0, '', '', NULL, NULL, NULL); -INSERT INTO `sys_menu` VALUES (109, 36, '0,36', '列表选择器', 1, 'TableSelect', 'table-select', 'demo/table-select/index', NULL, NULL, 1, 1, 1, '', '', NULL, NULL, NULL); -INSERT INTO `sys_menu` VALUES (110, 0, '0', '路由参数', 2, NULL, '/route-param', 'Layout', NULL, 1, 1, 1, 11, 'el-icon-ElementPlus', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (111, 110, '0,110', '参数(type=1)', 1, '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 (112, 110, '0,110', '参数(type=2)', 1, 'RouteParamType2', 'route-param-type2', 'demo/route-param', NULL, 0, 1, 1, 2, 'el-icon-StarFilled', NULL, now(), now(), '{\"type\": \"2\"}'); -INSERT INTO `sys_menu` VALUES (117, 1, '0,1', '系统日志', 1, 'Log', 'log', 'system/log/index', NULL, 0, 1, 1, 6, 'document', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (118, 0, '0', '系统工具', 2, NULL, '/codegen', 'Layout', NULL, 0, 1, 1, 2, 'menu', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (119, 118, '0,118', '代码生成', 1, 'Codegen', 'codegen', 'codegen/index', NULL, 0, 1, 1, 1, 'code', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (120, 1, '0,1', '系统配置', 1, 'Config', 'config', 'system/config/index', NULL, 0, 1, 1, 7, 'setting', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (121, 120, '0,1,120', '系统配置查询', 4, NULL, '', NULL, 'sys:config:query', 0, 1, 1, 1, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (122, 120, '0,1,120', '系统配置新增', 4, NULL, '', NULL, 'sys:config:add', 0, 1, 1, 2, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (123, 120, '0,1,120', '系统配置修改', 4, NULL, '', NULL, 'sys:config:update', 0, 1, 1, 3, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (124, 120, '0,1,120', '系统配置删除', 4, NULL, '', NULL, 'sys:config:delete', 0, 1, 1, 4, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (125, 120, '0,1,120', '系统配置刷新', 4, NULL, '', NULL, 'sys:config:refresh', 0, 1, 1, 5, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (126, 1, '0,1', '通知公告', 1, 'Notice', 'notice', 'system/notice/index', NULL, NULL, NULL, 1, 9, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (127, 126, '0,1,126', '通知查询', 4, NULL, '', NULL, 'sys:notice:query', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (128, 126, '0,1,126', '通知新增', 4, NULL, '', NULL, 'sys:notice:add', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (129, 126, '0,1,126', '通知编辑', 4, NULL, '', NULL, 'sys:notice:edit', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (130, 126, '0,1,126', '通知删除', 4, NULL, '', NULL, 'sys:notice:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (133, 126, '0,1,126', '通知发布', 4, NULL, '', NULL, 'sys:notice:publish', 0, 1, 1, 5, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (134, 126, '0,1,126', '通知撤回', 4, NULL, '', NULL, 'sys:notice:revoke', 0, 1, 1, 6, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (135, 1, '0,1', '字典项', 1, 'DictItem', 'dict-item', 'system/dict/dict-item', NULL, 0, 1, 0, 6, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (136, 135, '0,1,135', '字典项新增', 4, NULL, '', NULL, 'sys:dict-item:add', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (137, 135, '0,1,135', '字典项编辑', 4, NULL, '', NULL, 'sys:dict-item:edit', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (138, 135, '0,1,135', '字典项删除', 4, NULL, '', NULL, 'sys:dict-item:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (139, 3, '0,1,3', '角色查询', 4, NULL, '', NULL, 'sys:role:query', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (140, 4, '0,1,4', '菜单查询', 4, NULL, '', NULL, 'sys:menu:query', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (141, 5, '0,1,5', '部门查询', 4, NULL, '', NULL, 'sys:dept:query', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (142, 6, '0,1,6', '字典查询', 4, NULL, '', NULL, 'sys:dict:query', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (143, 135, '0,1,135', '字典项查询', 4, NULL, '', NULL, 'sys:dict-item:query', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (144, 26, '0,26', '后端文档', 3, NULL, 'https://youlai.blog.csdn.net/article/details/145178880', '', NULL, NULL, NULL, 1, 3, 'document', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (145, 26, '0,26', '移动端文档', 3, NULL, 'https://youlai.blog.csdn.net/article/details/143222890', '', NULL, NULL, NULL, 1, 4, 'document', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (146, 36, '0,36', '拖拽组件', 1, 'Drag', 'drag', 'demo/drag', NULL, NULL, NULL, 1, 5, '', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (147, 36, '0,36', '滚动文本', 1, 'TextScroll', 'text-scroll', 'demo/text-scroll', NULL, NULL, NULL, 1, 6, '', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (148, 89, '0,89', '字典实时同步', 1, 'DictSync', 'dict-sync', 'demo/dict-sync', NULL, NULL, NULL, 1, 3, '', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (149, 89, '0,89', 'VxeTable', 1, 'VxeTable', 'vxe-table', 'demo/vxe-table/index', NULL, NULL, 1, 1, 0, 'el-icon-MagicStick', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (150, 36, '0,36', '自适应表格操作列', 1, 'AutoOperationColumn', 'operation-column', 'demo/auto-operation-column', NULL, NULL, 1, 1, 1, '', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (151, 89, '0,89', 'CURD单文件', 1, 'CurdSingle', 'curd-single', 'demo/curd-single', NULL, NULL, 1, 1, 7, 'el-icon-Reading', '', now(),now(), NULL); -INSERT INTO `sys_menu` VALUES (152, 0, '0', 'AI助手', 2, NULL, '/platform', 'Layout', NULL, NULL, NULL, 1, 13, 'platform', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (153, 152, '0,152', 'AI命令记录', 1, 'AiCommandRecord', 'command-record', 'ai/command-record/index', NULL, NULL, 1, 1, 1, 'document', NULL, now(), now(), NULL); - - --- ---------------------------- --- 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 = '角色和菜单关联表'; - --- ---------------------------- --- Records of sys_role_menu --- ---------------------------- -INSERT INTO `sys_role_menu` VALUES (2, 1); -INSERT INTO `sys_role_menu` VALUES (2, 2); -INSERT INTO `sys_role_menu` VALUES (2, 3); -INSERT INTO `sys_role_menu` VALUES (2, 4); -INSERT INTO `sys_role_menu` VALUES (2, 5); -INSERT INTO `sys_role_menu` VALUES (2, 6); -INSERT INTO `sys_role_menu` VALUES (2, 20); -INSERT INTO `sys_role_menu` VALUES (2, 21); -INSERT INTO `sys_role_menu` VALUES (2, 22); -INSERT INTO `sys_role_menu` VALUES (2, 23); -INSERT INTO `sys_role_menu` VALUES (2, 24); -INSERT INTO `sys_role_menu` VALUES (2, 26); -INSERT INTO `sys_role_menu` VALUES (2, 30); -INSERT INTO `sys_role_menu` VALUES (2, 31); -INSERT INTO `sys_role_menu` VALUES (2, 32); -INSERT INTO `sys_role_menu` VALUES (2, 33); -INSERT INTO `sys_role_menu` VALUES (2, 36); -INSERT INTO `sys_role_menu` VALUES (2, 37); -INSERT INTO `sys_role_menu` VALUES (2, 38); -INSERT INTO `sys_role_menu` VALUES (2, 39); -INSERT INTO `sys_role_menu` VALUES (2, 40); -INSERT INTO `sys_role_menu` VALUES (2, 41); -INSERT INTO `sys_role_menu` VALUES (2, 70); -INSERT INTO `sys_role_menu` VALUES (2, 71); -INSERT INTO `sys_role_menu` VALUES (2, 72); -INSERT INTO `sys_role_menu` VALUES (2, 73); -INSERT INTO `sys_role_menu` VALUES (2, 74); -INSERT INTO `sys_role_menu` VALUES (2, 75); -INSERT INTO `sys_role_menu` VALUES (2, 76); -INSERT INTO `sys_role_menu` VALUES (2, 77); -INSERT INTO `sys_role_menu` VALUES (2, 78); -INSERT INTO `sys_role_menu` VALUES (2, 79); -INSERT INTO `sys_role_menu` VALUES (2, 81); -INSERT INTO `sys_role_menu` VALUES (2, 84); -INSERT INTO `sys_role_menu` VALUES (2, 85); -INSERT INTO `sys_role_menu` VALUES (2, 86); -INSERT INTO `sys_role_menu` VALUES (2, 87); -INSERT INTO `sys_role_menu` VALUES (2, 88); -INSERT INTO `sys_role_menu` VALUES (2, 89); -INSERT INTO `sys_role_menu` VALUES (2, 90); -INSERT INTO `sys_role_menu` VALUES (2, 91); -INSERT INTO `sys_role_menu` VALUES (2, 95); -INSERT INTO `sys_role_menu` VALUES (2, 97); -INSERT INTO `sys_role_menu` VALUES (2, 102); -INSERT INTO `sys_role_menu` VALUES (2, 105); -INSERT INTO `sys_role_menu` VALUES (2, 106); -INSERT INTO `sys_role_menu` VALUES (2, 107); -INSERT INTO `sys_role_menu` VALUES (2, 108); -INSERT INTO `sys_role_menu` VALUES (2, 109); -INSERT INTO `sys_role_menu` VALUES (2, 110); -INSERT INTO `sys_role_menu` VALUES (2, 111); -INSERT INTO `sys_role_menu` VALUES (2, 112); -INSERT INTO `sys_role_menu` VALUES (2, 114); -INSERT INTO `sys_role_menu` VALUES (2, 115); -INSERT INTO `sys_role_menu` VALUES (2, 116); -INSERT INTO `sys_role_menu` VALUES (2, 117); -INSERT INTO `sys_role_menu` VALUES (2, 118); -INSERT INTO `sys_role_menu` VALUES (2, 119); -INSERT INTO `sys_role_menu` VALUES (2, 120); -INSERT INTO `sys_role_menu` VALUES (2, 121); -INSERT INTO `sys_role_menu` VALUES (2, 122); -INSERT INTO `sys_role_menu` VALUES (2, 123); -INSERT INTO `sys_role_menu` VALUES (2, 124); -INSERT INTO `sys_role_menu` VALUES (2, 125); -INSERT INTO `sys_role_menu` VALUES (2, 126); -INSERT INTO `sys_role_menu` VALUES (2, 127); -INSERT INTO `sys_role_menu` VALUES (2, 128); -INSERT INTO `sys_role_menu` VALUES (2, 129); -INSERT INTO `sys_role_menu` VALUES (2, 130); -INSERT INTO `sys_role_menu` VALUES (2, 131); -INSERT INTO `sys_role_menu` VALUES (2, 132); -INSERT INTO `sys_role_menu` VALUES (2, 133); -INSERT INTO `sys_role_menu` VALUES (2, 134); -INSERT INTO `sys_role_menu` VALUES (2, 135); -INSERT INTO `sys_role_menu` VALUES (2, 136); -INSERT INTO `sys_role_menu` VALUES (2, 137); -INSERT INTO `sys_role_menu` VALUES (2, 138); -INSERT INTO `sys_role_menu` VALUES (2, 139); -INSERT INTO `sys_role_menu` VALUES (2, 140); -INSERT INTO `sys_role_menu` VALUES (2, 141); -INSERT INTO `sys_role_menu` VALUES (2, 142); -INSERT INTO `sys_role_menu` VALUES (2, 143); -INSERT INTO `sys_role_menu` VALUES (2, 144); -INSERT INTO `sys_role_menu` VALUES (2, 145); -INSERT INTO `sys_role_menu` VALUES (2, 146); -INSERT INTO `sys_role_menu` VALUES (2, 147); -INSERT INTO `sys_role_menu` VALUES (2, 148); -INSERT INTO `sys_role_menu` VALUES (2, 149); -INSERT INTO `sys_role_menu` VALUES (2, 150); -INSERT INTO `sys_role_menu` VALUES (2, 151); - --- ---------------------------- --- 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, 'v2.12.0 新增系统日志,访问趋势统计功能。', '

1. 消息通知

2. 字典重构

3. 代码生成

', 1, 'L', 1, '2', 1, 1, now(), now(), 2, now(), 1, now(), 0); -INSERT INTO `sys_notice` VALUES (2, 'v2.13.0 新增菜单搜索。', '

1. 消息通知

2. 字典重构

3. 代码生成

', 1, 'L', 1, '2', 1, 1, now(), now(), 2, now(), 1, now(), 0); -INSERT INTO `sys_notice` VALUES (3, 'v2.14.0 新增个人中心。', '

1. 消息通知

2. 字典重构

3. 代码生成

', 1, 'L', 1, '2', 2, 1, now(), now(), 2, now(), 2, now(), 0); -INSERT INTO `sys_notice` VALUES (4, 'v2.15.0 登录页面改造。', '

1. 消息通知

2. 字典重构

3. 代码生成

', 1, 'L', 1, '2', 2, 1, now(), now(), 2, now(), 2, now(), 0); -INSERT INTO `sys_notice` VALUES (5, 'v2.16.0 通知公告、字典翻译组件。', '

1. 消息通知

2. 字典重构

3. 代码生成

', 1, 'L', 1, '2', 2, 1, now(), now(), 2, now(), 2, now(), 0); -INSERT INTO `sys_notice` VALUES (6, '系统将于本周六凌晨 2 点进行维护,预计维护时间为 2 小时。', '

1. 消息通知

2. 字典重构

3. 代码生成

', 2, 'H', 1, '2', 2, 1, now(), now(), 2, now(), 2, now(), 0); -INSERT INTO `sys_notice` VALUES (7, '最近发现一些钓鱼邮件,请大家提高警惕,不要点击陌生链接。', '

1. 消息通知

2. 字典重构

3. 代码生成

', 3, 'L', 1, '2', 2, 1, now(), now(), 2, now(), 2, now(), 0); -INSERT INTO `sys_notice` VALUES (8, '国庆假期从 10 月 1 日至 10 月 7 日放假,共 7 天。', '

1. 消息通知

2. 字典重构

3. 代码生成

', 4, 'L', 1, '2', 2, 1, now(), now(), 2, now(), 2, now(), 0); -INSERT INTO `sys_notice` VALUES (9, '公司将在 10 月 15 日举办新产品发布会,敬请期待。', '公司将在 10 月 15 日举办新产品发布会,敬请期待。', 5, 'H', 1, '2', 2, 1, now(), now(), 2, now(), 2, now(), 0); -INSERT INTO `sys_notice` VALUES (10, 'v2.16.1 版本发布。', 'v2.16.1 版本修复了 WebSocket 重复连接导致的后台线程阻塞问题,优化了通知公告。', 1, 'M', 1, '2', 2, 1, now(), now(), 2, now(), 2, now(), 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) 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数量', - `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 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI 命令记录表'; - - -SET FOREIGN_KEY_CHECKS = 1; diff --git a/src/main/java/com/youlai/boot/common/base/BaseEntity.java b/src/main/java/com/youlai/boot/common/base/BaseEntity.java index 9c6a2812..19bf36ab 100644 --- a/src/main/java/com/youlai/boot/common/base/BaseEntity.java +++ b/src/main/java/com/youlai/boot/common/base/BaseEntity.java @@ -13,6 +13,7 @@ import java.time.LocalDateTime; * 基础实体类 * *

实体类的基类,包含了实体类的公共属性,如创建时间、更新时间、逻辑删除标识等

+ *

多租户模式下,会自动添加 tenant_id 字段(通过 MyMetaObjectHandler 自动填充)

* * @author Ray * @since 2024/6/23 @@ -29,6 +30,26 @@ public class BaseEntity implements Serializable { @TableId(type = IdType.AUTO) private Long id; + /** + * 租户ID(多租户模式) + *

+ * 注意:此字段仅在启用多租户时生效 + * 通过 MyMetaObjectHandler 自动填充,无需手动设置 + * 如果不需要多租户,可以通过配置 youlai.tenant.enabled=false 禁用 + *

+ *

+ * 重要说明: + * 1. 默认使用 exist = false 标记字段不存在于数据库,避免单租户模式下报错 + * 2. 在启用多租户时,需要确保数据库表中有 tenant_id 字段 + * 3. 多租户的数据隔离主要通过 TenantLineHandler 自动添加 WHERE 条件实现 + * 4. 如果需要在 INSERT 时写入 tenant_id,请将 exist 改为 true 或移除 exist 属性 + * 5. 或者执行 add_tenant_column.sql 脚本为表添加 tenant_id 字段 + *

+ */ + @TableField(value = "tenant_id", exist = false) + @JsonInclude(value = JsonInclude.Include.NON_NULL) + private Long tenantId; + /** * 创建时间 */ diff --git a/src/main/java/com/youlai/boot/config/MybatisConfig.java b/src/main/java/com/youlai/boot/config/MybatisConfig.java index 537cb7e5..6cca57d5 100644 --- a/src/main/java/com/youlai/boot/config/MybatisConfig.java +++ b/src/main/java/com/youlai/boot/config/MybatisConfig.java @@ -5,11 +5,16 @@ import com.baomidou.mybatisplus.core.config.GlobalConfig; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; +import com.youlai.boot.config.property.TenantProperties; import com.youlai.boot.plugin.mybatis.MyDataPermissionHandler; import com.youlai.boot.plugin.mybatis.MyMetaObjectHandler; +import com.youlai.boot.plugin.mybatis.TenantLineHandler; import org.apache.ibatis.mapping.DatabaseIdProvider; import org.apache.ibatis.mapping.VendorDatabaseIdProvider; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.annotation.EnableTransactionManagement; @@ -29,14 +34,30 @@ public class MybatisConfig { @Value("${app.db-type:mysql}") private String dbType; + @Autowired(required = false) + private TenantLineHandler tenantLineHandler; + + @Autowired(required = false) + private TenantProperties tenantProperties; + /** * 分页插件和数据权限插件 + *

+ * 如果启用了多租户,则添加多租户插件(必须在最前面) + *

*/ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + + // 多租户插件(如果启用,必须在最前面) + if (tenantProperties != null && Boolean.TRUE.equals(tenantProperties.getEnabled()) && tenantLineHandler != null) { + interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(tenantLineHandler)); + } + // 数据权限 interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler())); + // 分页插件,根据配置动态选择数据库类型 DbType mpDbType = DbType.MYSQL; String type = dbType == null ? "mysql" : dbType.toLowerCase(); diff --git a/src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandLogMapper.java b/src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandLogMapper.java index 7f6fbed8..90bb3f32 100644 --- a/src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandLogMapper.java +++ b/src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandLogMapper.java @@ -23,3 +23,4 @@ public interface AiCommandLogMapper extends BaseMapper { IPage getLogPage(Page page, AiCommandPageQuery queryParams); } + diff --git a/src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandLog.java b/src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandLog.java index 47e1f067..9a55687f 100644 --- a/src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandLog.java +++ b/src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandLog.java @@ -79,3 +79,4 @@ public class AiCommandLog extends BaseEntity { private String ipAddress; } + diff --git a/src/main/java/com/youlai/boot/platform/ai/model/vo/AiCommandLogVO.java b/src/main/java/com/youlai/boot/platform/ai/model/vo/AiCommandLogVO.java index 0481087d..b35d3745 100644 --- a/src/main/java/com/youlai/boot/platform/ai/model/vo/AiCommandLogVO.java +++ b/src/main/java/com/youlai/boot/platform/ai/model/vo/AiCommandLogVO.java @@ -90,3 +90,4 @@ public class AiCommandLogVO implements Serializable { private LocalDateTime updateTime; } + diff --git a/src/main/java/com/youlai/boot/platform/ai/service/AiCommandLogService.java b/src/main/java/com/youlai/boot/platform/ai/service/AiCommandLogService.java index 4628881d..5b4bea19 100644 --- a/src/main/java/com/youlai/boot/platform/ai/service/AiCommandLogService.java +++ b/src/main/java/com/youlai/boot/platform/ai/service/AiCommandLogService.java @@ -30,3 +30,4 @@ public interface AiCommandLogService extends IService { 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 index fb0eea05..05ec8f4f 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 @@ -47,3 +47,4 @@ public class AiCommandLogServiceImpl extends ServiceImpl + * 支持自动填充创建时间、更新时间和租户ID + *

* * @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() .eq(Menu::getRouteName, entity.getRouteName()) .ne(menuForm.getId() != null, Menu::getId, menuForm.getId()) @@ -463,8 +471,8 @@ public class MenuServiceImpl extends ServiceImpl implements Me // 生成CURD按钮权限 String permPrefix = genConfig.getModuleName() + ":" + genConfig.getTableName().replace("_", "-") + ":"; - String[] actions = {"查询", "新增", "编辑", "删除"}; - String[] perms = {"query", "add", "edit", "delete"}; + String[] actions = {"查询", "新增", "修改", "删除"}; + String[] perms = {"list", "create", "update", "delete"}; for (int i = 0; i < actions.length; i++) { Menu button = new Menu(); From 47cabcbcfc580367c2f41747079759b0b9c9efa0 Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Thu, 11 Dec 2025 08:18:01 +0800 Subject: [PATCH 10/22] =?UTF-8?q?refactor:=20=E7=A7=9F=E6=88=B7=E9=87=8D?= =?UTF-8?q?=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/多租户用户管理改进说明.md | 228 ++++++++++++++++++ pom.xml | 10 + sql/mysql/tenant_add.sql | 25 +- .../common/tenant/TenantContextHolder.java | 9 +- .../boot/config/TenantDynamicFieldConfig.java | 76 ++++++ .../com/youlai/boot/config/WebMvcConfig.java | 23 ++ .../TenantValidationInterceptor.java | 86 +++++++ .../plugin/mybatis/MyMetaObjectHandler.java | 25 +- .../system/controller/TenantController.java | 63 +++-- .../system/mapper/TenantSwitchLogMapper.java | 15 ++ .../system/model/entity/TenantSwitchLog.java | 80 ++++++ .../boot/system/service/TenantService.java | 14 +- .../service/impl/TenantServiceImpl.java | 87 +++++++ .../system/service/impl/UserServiceImpl.java | 92 ++++++- 14 files changed, 789 insertions(+), 44 deletions(-) create mode 100644 docs/多租户用户管理改进说明.md create mode 100644 src/main/java/com/youlai/boot/config/TenantDynamicFieldConfig.java create mode 100644 src/main/java/com/youlai/boot/core/interceptor/TenantValidationInterceptor.java create mode 100644 src/main/java/com/youlai/boot/system/mapper/TenantSwitchLogMapper.java create mode 100644 src/main/java/com/youlai/boot/system/model/entity/TenantSwitchLog.java diff --git a/docs/多租户用户管理改进说明.md b/docs/多租户用户管理改进说明.md new file mode 100644 index 00000000..84417f78 --- /dev/null +++ b/docs/多租户用户管理改进说明.md @@ -0,0 +1,228 @@ +# 多租户用户管理改进说明 + +## 改进概述 + +本次改进实现了在用户管理中自动维护 `sys_user_tenant` 关联表,支持单租户和多租户两种模式的无缝切换。 + +## 核心改进 + +### 1. 用户新增时自动创建租户关联 + +**修改文件**: `UserServiceImpl.saveUser()` + +**逻辑**: +- 创建用户后,判断是否启用多租户(通过 `youlai.tenant.enabled` 配置) +- 如果启用,自动向 `sys_user_tenant` 表插入关联记录 +- 新用户默认设置为该租户的默认租户(`is_default=1`) + +```java +// 新增用户 +boolean result = this.save(entity); + +if (result) { + // 保存用户角色 + userRoleService.saveUserRoles(entity.getId(), userForm.getRoleIds()); + + // 如果启用多租户,保存用户租户关联 + if (Boolean.TRUE.equals(tenantProperties.getEnabled())) { + saveUserTenantRelation(entity.getId(), entity.getTenantId(), true); + } +} +``` + +### 2. 用户更新时同步租户关联 + +**修改文件**: `UserServiceImpl.updateUser()` + +**逻辑**: +- 比较用户的旧租户ID和新租户ID +- 如果租户发生变更: + - 删除旧的租户关联记录 + - 创建新的租户关联记录 + +```java +// 如果启用多租户且租户发生变更,更新用户租户关联 +if (Boolean.TRUE.equals(tenantProperties.getEnabled())) { + Long newTenantId = entity.getTenantId(); + if (newTenantId != null && !newTenantId.equals(oldTenantId)) { + // 删除旧的租户关联 + if (oldTenantId != null) { + userTenantMapper.delete(...); + } + // 保存新的租户关联 + saveUserTenantRelation(userId, newTenantId, true); + } +} +``` + +### 3. 用户删除时清理租户关联 + +**修改文件**: `UserServiceImpl.deleteUsers()` + +**逻辑**: +- 删除用户后,自动清理 `sys_user_tenant` 表中的关联记录 +- 避免产生孤立数据 + +```java +boolean result = this.removeByIds(ids); + +// 如果启用多租户,删除用户租户关联 +if (result && Boolean.TRUE.equals(tenantProperties.getEnabled())) { + for (Long userId : ids) { + userTenantMapper.delete(...); + log.info("删除用户租户关联:userId={}", userId); + } +} +``` + +### 4. 新增私有方法处理关联逻辑 + +**新增方法**: `saveUserTenantRelation()` + +**功能**: +- 检查关联是否已存在 +- 存在则更新 `is_default` 标识 +- 不存在则插入新记录 +- 添加详细日志记录 + +## 配置说明 + +### 启用多租户 + +在 `application-dev.yml` 中配置: + +```yaml +youlai: + tenant: + enabled: true # 设置为 true 启用多租户 + column: tenant_id + default-tenant-id: 1 +``` + +### 禁用多租户 + +```yaml +youlai: + tenant: + enabled: false # 设置为 false 禁用多租户 +``` + +当禁用多租户时: +- ✅ 不会自动创建/更新/删除 `sys_user_tenant` 记录 +- ✅ 只使用 `sys_user.tenant_id` 字段 +- ✅ 零成本切换,无需修改代码 + +## 数据库设计 + +### sys_user 表 +```sql +ALTER TABLE `sys_user` +ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `id`, +ADD INDEX `idx_tenant_id` (`tenant_id`); +``` + +### sys_user_tenant 表 +```sql +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 DEFAULT CHARSET=utf8mb4 COMMENT='用户租户关联表(多租户模式)'; +``` + +## 数据初始化 + +执行 SQL 脚本时,会自动为现有用户创建租户关联: + +```sql +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; +``` + +## 使用场景 + +### 场景1:单租户模式 +- 配置:`youlai.tenant.enabled = false` +- 用户只属于一个租户 +- 数据完全隔离 +- 不需要租户切换功能 + +### 场景2:多租户模式 +- 配置:`youlai.tenant.enabled = true` +- 用户可以属于多个租户 +- 可以在不同租户间切换 +- 通过 `sys_user_tenant` 表管理关联关系 + +## 关键优势 + +1. **自动化管理**: 创建/更新/删除用户时自动维护关联表 +2. **灵活切换**: 通过配置即可在单租户和多租户模式间切换 +3. **数据一致性**: 确保 `sys_user.tenant_id` 和 `sys_user_tenant` 表数据同步 +4. **幂等操作**: 支持重复执行,避免重复插入 +5. **完整日志**: 每次操作都有日志记录,便于追踪问题 + +## 注意事项 + +1. **事务处理**: 用户的增删改操作都已添加事务注解 `@Transactional` +2. **空值检查**: `saveUserTenantRelation()` 方法会检查参数是否为空 +3. **幂等性**: 插入前会检查记录是否已存在 +4. **配置优先**: 所有操作都基于 `tenantProperties.getEnabled()` 判断 + +## 测试建议 + +### 测试场景1:多租户模式下创建用户 +1. 设置 `youlai.tenant.enabled = true` +2. 在租户A下创建用户"张三" +3. 验证: + - `sys_user` 表插入记录,`tenant_id=A` + - `sys_user_tenant` 表插入记录,`user_id=张三, tenant_id=A, is_default=1` + +### 测试场景2:多租户模式下更新用户租户 +1. 将用户"张三"从租户A转移到租户B +2. 验证: + - `sys_user` 表更新,`tenant_id=B` + - `sys_user_tenant` 表删除旧记录 (A),插入新记录 (B) + +### 测试场景3:多租户模式下删除用户 +1. 删除用户"张三" +2. 验证: + - `sys_user` 表标记为删除 + - `sys_user_tenant` 表删除关联记录 + +### 测试场景4:单租户模式 +1. 设置 `youlai.tenant.enabled = false` +2. 创建/更新/删除用户 +3. 验证: + - 只操作 `sys_user` 表 + - 不操作 `sys_user_tenant` 表 + +## 修改文件清单 + +- ✅ `UserServiceImpl.java` - 添加多租户关联维护逻辑 +- ✅ `tenant_add.sql` - 数据库表结构和初始化脚本 +- ✅ `TenantProperties.java` - 多租户配置类(已存在) +- ✅ `UserTenantMapper.java` - MyBatis Mapper(已存在) +- ✅ `UserTenant.java` - 实体类(已存在) + +## 向后兼容性 + +- ✅ 默认配置为 `enabled: false`,不影响现有单租户系统 +- ✅ 现有代码无需修改,只需调整配置文件即可启用多租户 +- ✅ 数据库升级脚本支持多次执行(幂等) + +## 总结 + +本次改进完善了多租户用户管理机制,实现了: +- 自动维护用户租户关联关系 +- 支持单/多租户模式灵活切换 +- 保证数据一致性和完整性 +- 提供详细的操作日志 + +系统现在可以零成本在单租户和多租户模式间切换,只需修改配置文件即可。 diff --git a/pom.xml b/pom.xml index 3a51ac4b..93e3889a 100644 --- a/pom.xml +++ b/pom.xml @@ -59,6 +59,9 @@ 4.7.7.B 2.9.3 + + + 2.14.5 @@ -75,6 +78,13 @@ ${hutool.version} + + + com.alibaba + transmittable-thread-local + ${transmittable-thread-local.version} + + org.projectlombok diff --git a/sql/mysql/tenant_add.sql b/sql/mysql/tenant_add.sql index 6e6a4f3d..ec367255 100644 --- a/sql/mysql/tenant_add.sql +++ b/sql/mysql/tenant_add.sql @@ -39,7 +39,30 @@ INSERT INTO `sys_tenant` (`id`, `name`, `code`, `status`, `create_time`) VALUES (1, '默认租户', 'DEFAULT', 1, NOW()); -- ============================================ --- 2. 创建用户租户关联表(支持一个用户属于多个租户) +-- 2. 创建租户切换审计日志表 +-- ============================================ +DROP TABLE IF EXISTS `sys_tenant_switch_log`; +CREATE TABLE `sys_tenant_switch_log` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `user_id` bigint NOT NULL COMMENT '用户ID', + `username` varchar(64) COMMENT '用户名', + `from_tenant_id` bigint COMMENT '原租户ID', + `from_tenant_name` varchar(100) COMMENT '原租户名称', + `to_tenant_id` bigint NOT NULL COMMENT '目标租户ID', + `to_tenant_name` varchar(100) COMMENT '目标租户名称', + `switch_time` datetime NOT NULL COMMENT '切换时间', + `ip_address` varchar(50) COMMENT 'IP地址', + `user_agent` varchar(500) COMMENT '浏览器信息', + `status` tinyint DEFAULT '1' COMMENT '切换状态(1-成功 0-失败)', + `fail_reason` varchar(255) COMMENT '失败原因', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_switch_time` (`switch_time`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户切换审计日志表'; + +-- ============================================ +-- 3. 创建用户租户关联表(支持一个用户属于多个租户) -- ============================================ DROP TABLE IF EXISTS `sys_user_tenant`; CREATE TABLE `sys_user_tenant` ( diff --git a/src/main/java/com/youlai/boot/common/tenant/TenantContextHolder.java b/src/main/java/com/youlai/boot/common/tenant/TenantContextHolder.java index fd674b31..e80e55e6 100644 --- a/src/main/java/com/youlai/boot/common/tenant/TenantContextHolder.java +++ b/src/main/java/com/youlai/boot/common/tenant/TenantContextHolder.java @@ -1,11 +1,13 @@ package com.youlai.boot.common.tenant; +import com.alibaba.ttl.TransmittableThreadLocal; import lombok.extern.slf4j.Slf4j; /** * 租户上下文工具类 *

- * 使用 ThreadLocal 存储当前线程的租户ID,确保线程安全 + * 使用 TransmittableThreadLocal 存储当前线程的租户ID,确保线程安全 + * 支持异步任务、线程池、消息队列等场景的上下文传递 *

* * @author Ray.Hao @@ -16,13 +18,14 @@ public class TenantContextHolder { /** * 租户ID线程本地变量 + * 使用 TransmittableThreadLocal 支持父子线程和线程池场景的值传递 */ - private static final ThreadLocal TENANT_ID_HOLDER = new ThreadLocal<>(); + private static final TransmittableThreadLocal TENANT_ID_HOLDER = new TransmittableThreadLocal<>(); /** * 忽略租户标志(用于某些场景下临时跳过租户过滤) */ - private static final ThreadLocal IGNORE_TENANT_HOLDER = new ThreadLocal<>(); + private static final TransmittableThreadLocal IGNORE_TENANT_HOLDER = new TransmittableThreadLocal<>(); /** * 设置当前租户ID diff --git a/src/main/java/com/youlai/boot/config/TenantDynamicFieldConfig.java b/src/main/java/com/youlai/boot/config/TenantDynamicFieldConfig.java new file mode 100644 index 00000000..59d9cf05 --- /dev/null +++ b/src/main/java/com/youlai/boot/config/TenantDynamicFieldConfig.java @@ -0,0 +1,76 @@ +package com.youlai.boot.config; + +import com.baomidou.mybatisplus.core.metadata.TableFieldInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.youlai.boot.config.property.TenantProperties; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Field; +import java.util.List; + +/** + * 多租户动态字段配置 + *

+ * 在多租户模式启用时,动态修改 BaseEntity 中 tenant_id 字段的 exist 属性为 true + * 这样可以实现: + * - 单租户模式:tenant_id exist=false,不映射该字段,兼容没有该字段的表 + * - 多租户模式:tenant_id exist=true,自动填充租户ID到INSERT/UPDATE语句 + *

+ * + * @author Ray.Hao + * @since 3.0.0 + */ +@Slf4j +@Component +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "youlai.tenant", name = "enabled", havingValue = "true") +public class TenantDynamicFieldConfig implements InitializingBean { + + private final TenantProperties tenantProperties; + + @Override + public void afterPropertiesSet() { + log.info("多租户模式已启用,开始动态配置 tenant_id 字段映射..."); + + int modifiedCount = 0; + List tableInfos = TableInfoHelper.getTableInfos(); + + for (TableInfo tableInfo : tableInfos) { + // 检查是否是忽略的表 + String tableName = tableInfo.getTableName(); + if (tenantProperties.getIgnoreTables().contains(tableName)) { + log.debug("表 {} 在忽略列表中,跳过 tenant_id 字段配置", tableName); + continue; + } + + // 查找 tenant_id 字段 + TableFieldInfo tenantField = tableInfo.getFieldList().stream() + .filter(field -> tenantProperties.getColumn().equals(field.getColumn())) + .findFirst() + .orElse(null); + + if (tenantField != null) { + try { + // 通过反射修改 exist 属性为 true + Field existField = TableFieldInfo.class.getDeclaredField("exist"); + existField.setAccessible(true); + existField.set(tenantField, true); + + modifiedCount++; + log.debug("已为表 {} 启用 tenant_id 字段映射", tableName); + } catch (NoSuchFieldException | IllegalAccessException e) { + log.warn("修改表 {} 的 tenant_id 字段配置失败: {}", tableName, e.getMessage()); + } + } else { + log.warn("表 {} 未找到 tenant_id 字段,请检查实体类是否继承 BaseEntity", tableName); + } + } + + log.info("多租户字段配置完成,共修改 {} 张表", modifiedCount); + } +} diff --git a/src/main/java/com/youlai/boot/config/WebMvcConfig.java b/src/main/java/com/youlai/boot/config/WebMvcConfig.java index a8082ea9..fae163f3 100644 --- a/src/main/java/com/youlai/boot/config/WebMvcConfig.java +++ b/src/main/java/com/youlai/boot/config/WebMvcConfig.java @@ -7,17 +7,21 @@ import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import com.youlai.boot.core.interceptor.TenantValidationInterceptor; import jakarta.validation.Validation; import jakarta.validation.Validator; import jakarta.validation.ValidatorFactory; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.hibernate.validator.HibernateValidator; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.math.BigInteger; @@ -39,6 +43,9 @@ public class WebMvcConfig implements WebMvcConfigurer { private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + @Autowired(required = false) + private TenantValidationInterceptor tenantValidationInterceptor; + /** * 配置消息转换器 * @@ -78,6 +85,22 @@ public class WebMvcConfig implements WebMvcConfigurer { * @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory * @return Validator 实例 */ + /** + * 配置拦截器 + * + * @param registry 拦截器注册器 + */ + @Override + public void addInterceptors(InterceptorRegistry registry) { + // 注册租户校验拦截器(仅在多租户模式启用时生效) + if (tenantValidationInterceptor != null) { + registry.addInterceptor(tenantValidationInterceptor) + .addPathPatterns("/api/**") + .order(2); // 在认证拦截器之后执行 + log.info("租户校验拦截器已注册"); + } + } + @Bean public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) diff --git a/src/main/java/com/youlai/boot/core/interceptor/TenantValidationInterceptor.java b/src/main/java/com/youlai/boot/core/interceptor/TenantValidationInterceptor.java new file mode 100644 index 00000000..7ef57da7 --- /dev/null +++ b/src/main/java/com/youlai/boot/core/interceptor/TenantValidationInterceptor.java @@ -0,0 +1,86 @@ +package com.youlai.boot.core.interceptor; + +import com.youlai.boot.common.result.ResultCode; +import com.youlai.boot.common.tenant.TenantContextHolder; +import com.youlai.boot.config.property.TenantProperties; +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.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +/** + * 租户ID强制校验拦截器 + *

+ * 对于需要租户隔离的接口,强制要求携带有效的租户ID + * 防止恶意用户通过不携带租户ID来访问默认租户数据 + *

+ * + * @author Ray.Hao + * @since 3.0.0 + */ +@Slf4j +@Component +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "youlai.tenant", name = "enabled", havingValue = "true") +public class TenantValidationInterceptor implements HandlerInterceptor { + + private final TenantProperties tenantProperties; + + /** + * 白名单路径:这些路径不需要租户ID校验 + */ + private static final List WHITELIST_PATHS = Arrays.asList( + "/api/v1/auth/login", + "/api/v1/auth/logout", + "/api/v1/auth/captcha", + "/api/v1/tenant/list", + "/doc.html", + "/v3/api-docs", + "/swagger-ui", + "/favicon.ico", + "/error" + ); + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { + String requestPath = request.getRequestURI(); + + // 检查是否在白名单中 + if (isWhitelistPath(requestPath)) { + return true; + } + + // 检查租户ID是否存在 + Long tenantId = TenantContextHolder.getTenantId(); + if (tenantId == null) { + log.warn("请求路径 {} 缺少租户ID", requestPath); + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write(String.format( + "{\"code\":\"%s\",\"msg\":\"租户ID不能为空,请联系管理员\"}", + ResultCode.BAD_REQUEST.getCode() + )); + return false; + } + + // 可选:校验租户是否有效(需要注入 TenantService) + // 这里暂时只校验租户ID不为空 + + return true; + } + + /** + * 检查路径是否在白名单中 + */ + private boolean isWhitelistPath(String requestPath) { + return WHITELIST_PATHS.stream() + .anyMatch(requestPath::startsWith); + } +} diff --git a/src/main/java/com/youlai/boot/plugin/mybatis/MyMetaObjectHandler.java b/src/main/java/com/youlai/boot/plugin/mybatis/MyMetaObjectHandler.java index 14196424..23852ae0 100644 --- a/src/main/java/com/youlai/boot/plugin/mybatis/MyMetaObjectHandler.java +++ b/src/main/java/com/youlai/boot/plugin/mybatis/MyMetaObjectHandler.java @@ -28,6 +28,10 @@ public class MyMetaObjectHandler implements MetaObjectHandler { /** * 新增填充创建时间、更新时间和租户ID + *

+ * 多租户模式下,tenant_id 字段的 exist 属性会被 TenantDynamicFieldConfig 动态设置为 true, + * 因此这里的 strictInsertFill 可以正常工作 + *

* * @param metaObject 元数据 */ @@ -37,23 +41,16 @@ public class MyMetaObjectHandler implements MetaObjectHandler { 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) { + // 如果上下文中没有租户ID,使用默认租户ID + tenantId = tenantProperties.getDefaultTenantId(); + } if (tenantId != null) { - // 使用数据库字段名(tenant_id)进行填充 - // 注意:由于 exist = false,这个填充不会写入数据库 - // 但多租户的数据隔离是通过 TenantLineHandler 自动添加 WHERE 条件实现的 - // 所以这里只需要设置实体对象的属性值即可(用于业务逻辑) - String propertyName = "tenantId"; - if (metaObject.hasGetter(propertyName)) { - // 直接设置值到实体对象,不依赖字段映射 - metaObject.setValue(propertyName, tenantId); - } + // 使用 strictInsertFill 自动填充租户ID + // 注意:由于 TenantDynamicFieldConfig 已将 exist 设置为 true,这里可以正常填充 + this.strictInsertFill(metaObject, "tenantId", () -> tenantId, Long.class); } } } diff --git a/src/main/java/com/youlai/boot/system/controller/TenantController.java b/src/main/java/com/youlai/boot/system/controller/TenantController.java index dfd0ff3a..723c6b86 100644 --- a/src/main/java/com/youlai/boot/system/controller/TenantController.java +++ b/src/main/java/com/youlai/boot/system/controller/TenantController.java @@ -8,6 +8,7 @@ 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 jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -71,6 +72,7 @@ public class TenantController { * 切换租户 *

* 切换当前用户的租户上下文,需要验证用户是否有权限访问该租户 + * 并记录审计日志 *

* * @param tenantId 目标租户ID @@ -79,32 +81,49 @@ public class TenantController { @Operation(summary = "切换租户") @PostMapping("/switch/{tenantId}") public Result switchTenant( - @Parameter(description = "租户ID") @PathVariable Long tenantId + @Parameter(description = "租户ID") @PathVariable Long tenantId, + HttpServletRequest request ) { Long userId = SecurityUtils.getUserId(); - log.info("用户 {} 请求切换租户到 {}", userId, tenantId); + Long fromTenantId = TenantContextHolder.getTenantId(); + + log.info("用户 {} 请求切换租户:{} -> {}", userId, fromTenantId, tenantId); - // 验证用户是否有权限访问该租户 - boolean hasPermission = tenantService.hasTenantPermission(userId, tenantId); - if (!hasPermission) { - log.warn("用户 {} 无权限访问租户 {}", userId, tenantId); - return Result.failed("无权限访问该租户"); + try { + // 验证用户是否有权限访问该租户 + boolean hasPermission = tenantService.hasTenantPermission(userId, tenantId); + if (!hasPermission) { + log.warn("用户 {} 无权限访问租户 {}", userId, tenantId); + // 记录失败日志 + tenantService.recordTenantSwitch(userId, fromTenantId, tenantId, false, "无权限访问该租户", request); + return Result.failed("无权限访问该租户"); + } + + // 验证租户是否存在且正常 + TenantVO tenant = tenantService.getTenantById(tenantId); + if (tenant == null) { + tenantService.recordTenantSwitch(userId, fromTenantId, tenantId, false, "租户不存在", request); + return Result.failed("租户不存在"); + } + if (tenant.getStatus() == null || tenant.getStatus() != 1) { + tenantService.recordTenantSwitch(userId, fromTenantId, tenantId, false, "租户已禁用", request); + return Result.failed("租户已禁用"); + } + + // 设置新的租户上下文 + TenantContextHolder.setTenantId(tenantId); + + // 记录成功日志 + tenantService.recordTenantSwitch(userId, fromTenantId, tenantId, true, null, request); + + log.info("用户 {} 成功切换租户到 {}", userId, tenantId); + + return Result.success(tenant); + } catch (Exception e) { + log.error("用户 {} 切换租户失败", userId, e); + tenantService.recordTenantSwitch(userId, fromTenantId, tenantId, false, e.getMessage(), request); + return Result.failed("切换租户失败:" + e.getMessage()); } - - // 验证租户是否存在且正常 - 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/TenantSwitchLogMapper.java b/src/main/java/com/youlai/boot/system/mapper/TenantSwitchLogMapper.java new file mode 100644 index 00000000..62cd36ec --- /dev/null +++ b/src/main/java/com/youlai/boot/system/mapper/TenantSwitchLogMapper.java @@ -0,0 +1,15 @@ +package com.youlai.boot.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.youlai.boot.system.model.entity.TenantSwitchLog; +import org.apache.ibatis.annotations.Mapper; + +/** + * 租户切换审计日志 Mapper + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Mapper +public interface TenantSwitchLogMapper extends BaseMapper { +} diff --git a/src/main/java/com/youlai/boot/system/model/entity/TenantSwitchLog.java b/src/main/java/com/youlai/boot/system/model/entity/TenantSwitchLog.java new file mode 100644 index 00000000..d455d1e1 --- /dev/null +++ b/src/main/java/com/youlai/boot/system/model/entity/TenantSwitchLog.java @@ -0,0 +1,80 @@ +package com.youlai.boot.system.model.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 租户切换审计日志实体 + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Data +@TableName("sys_tenant_switch_log") +public class TenantSwitchLog { + + /** + * 主键ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 原租户ID + */ + private Long fromTenantId; + + /** + * 原租户名称 + */ + private String fromTenantName; + + /** + * 目标租户ID + */ + private Long toTenantId; + + /** + * 目标租户名称 + */ + private String toTenantName; + + /** + * 切换时间 + */ + private LocalDateTime switchTime; + + /** + * IP地址 + */ + private String ipAddress; + + /** + * 浏览器信息 + */ + private String userAgent; + + /** + * 切换状态(1-成功 0-失败) + */ + private Integer status; + + /** + * 失败原因 + */ + private String failReason; +} diff --git a/src/main/java/com/youlai/boot/system/service/TenantService.java b/src/main/java/com/youlai/boot/system/service/TenantService.java index 36dc36f7..130ad916 100644 --- a/src/main/java/com/youlai/boot/system/service/TenantService.java +++ b/src/main/java/com/youlai/boot/system/service/TenantService.java @@ -46,5 +46,17 @@ public interface TenantService extends IService { * @return true-有权限,false-无权限 */ boolean hasTenantPermission(Long userId, Long tenantId); -} + /** + * 记录租户切换审计日志 + * + * @param userId 用户ID + * @param fromTenantId 原租户ID + * @param toTenantId 目标租户ID + * @param success 是否成功 + * @param failReason 失败原因 + * @param request HTTP请求对象 + */ + void recordTenantSwitch(Long userId, Long fromTenantId, Long toTenantId, + boolean success, String failReason, jakarta.servlet.http.HttpServletRequest request); +} 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 index 8d11aa49..14bda285 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/TenantServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/TenantServiceImpl.java @@ -1,19 +1,26 @@ package com.youlai.boot.system.service.impl; +import cn.hutool.core.util.StrUtil; 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.TenantSwitchLogMapper; +import com.youlai.boot.system.mapper.UserMapper; import com.youlai.boot.system.mapper.UserTenantMapper; import com.youlai.boot.system.model.entity.Tenant; +import com.youlai.boot.system.model.entity.TenantSwitchLog; +import com.youlai.boot.system.model.entity.User; import com.youlai.boot.system.model.entity.UserTenant; import com.youlai.boot.system.model.vo.TenantVO; import com.youlai.boot.system.service.TenantService; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; +import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; @@ -29,6 +36,8 @@ import java.util.stream.Collectors; public class TenantServiceImpl extends ServiceImpl implements TenantService { private final UserTenantMapper userTenantMapper; + private final TenantSwitchLogMapper tenantSwitchLogMapper; + private final UserMapper userMapper; @Override public List getTenantListByUserId(Long userId) { @@ -121,5 +130,83 @@ public class TenantServiceImpl extends ServiceImpl impleme TenantContextHolder.setIgnoreTenant(false); } } + + @Override + public void recordTenantSwitch(Long userId, Long fromTenantId, Long toTenantId, + boolean success, String failReason, HttpServletRequest request) { + try { + // 临时忽略租户过滤,确保日志可以写入 + TenantContextHolder.setIgnoreTenant(true); + + // 创建审计日志 + TenantSwitchLog log = new TenantSwitchLog(); + log.setUserId(userId); + log.setFromTenantId(fromTenantId); + log.setToTenantId(toTenantId); + log.setSwitchTime(LocalDateTime.now()); + log.setStatus(success ? 1 : 0); + log.setFailReason(failReason); + + // 获取用户名 + if (userId != null) { + User user = userMapper.selectById(userId); + if (user != null) { + log.setUsername(user.getUsername()); + } + } + + // 获取租户名称 + if (fromTenantId != null) { + Tenant fromTenant = this.getById(fromTenantId); + if (fromTenant != null) { + log.setFromTenantName(fromTenant.getName()); + } + } + if (toTenantId != null) { + Tenant toTenant = this.getById(toTenantId); + if (toTenant != null) { + log.setToTenantName(toTenant.getName()); + } + } + + // 获取IP地址和User-Agent + if (request != null) { + log.setIpAddress(getIpAddress(request)); + log.setUserAgent(request.getHeader("User-Agent")); + } + + // 保存审计日志 + tenantSwitchLogMapper.insert(log); + } catch (Exception e) { + // 记录日志失败不应影响业务,仅记录错误 + Slf4j.getLogger(this.getClass()).error("记录租户切换日志失败", e); + } finally { + TenantContextHolder.setIgnoreTenant(false); + } + } + + /** + * 获取客户端IP地址 + */ + private String getIpAddress(HttpServletRequest request) { + String ip = request.getHeader("X-Forwarded-For"); + if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("X-Real-IP"); + } + if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + // 处理多级代理的情况 + if (ip != null && ip.contains(",")) { + ip = ip.split(",")[0].trim(); + } + return ip; + } } diff --git a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java index 749cfba1..401dc492 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java @@ -76,6 +76,10 @@ public class UserServiceImpl extends ServiceImpl implements Us private final UserConverter userConverter; + private final com.youlai.boot.config.property.TenantProperties tenantProperties; + + private final com.youlai.boot.system.mapper.UserTenantMapper userTenantMapper; + /** * 获取用户分页列表 * @@ -118,6 +122,7 @@ public class UserServiceImpl extends ServiceImpl implements Us * @return true|false */ @Override + @Transactional(rollbackFor = Exception.class) public boolean saveUser(UserForm userForm) { String username = userForm.getUsername(); @@ -139,6 +144,11 @@ public class UserServiceImpl extends ServiceImpl implements Us if (result) { // 保存用户角色 userRoleService.saveUserRoles(entity.getId(), userForm.getRoleIds()); + + // 如果启用多租户,保存用户租户关联 + if (Boolean.TRUE.equals(tenantProperties.getEnabled())) { + saveUserTenantRelation(entity.getId(), entity.getTenantId(), true); + } } return result; } @@ -151,7 +161,7 @@ public class UserServiceImpl extends ServiceImpl implements Us * @return true|false */ @Override - @Transactional + @Transactional(rollbackFor = Exception.class) public boolean updateUser(Long userId, UserForm userForm) { String username = userForm.getUsername(); @@ -162,6 +172,10 @@ public class UserServiceImpl extends ServiceImpl implements Us ); Assert.isTrue(count == 0, "用户名已存在"); + // 获取原用户信息,用于比较租户是否变更 + User oldUser = this.getById(userId); + Long oldTenantId = oldUser != null ? oldUser.getTenantId() : null; + // form -> entity User entity = userConverter.toEntity(userForm); entity.setUpdateBy(SecurityUtils.getUserId()); @@ -172,6 +186,23 @@ public class UserServiceImpl extends ServiceImpl implements Us if (result) { // 保存用户角色 userRoleService.saveUserRoles(entity.getId(), userForm.getRoleIds()); + + // 如果启用多租户且租户发生变更,更新用户租户关联 + if (Boolean.TRUE.equals(tenantProperties.getEnabled())) { + Long newTenantId = entity.getTenantId(); + if (newTenantId != null && !newTenantId.equals(oldTenantId)) { + // 删除旧的租户关联 + if (oldTenantId != null) { + userTenantMapper.delete( + new LambdaQueryWrapper() + .eq(com.youlai.boot.system.model.entity.UserTenant::getUserId, userId) + .eq(com.youlai.boot.system.model.entity.UserTenant::getTenantId, oldTenantId) + ); + } + // 保存新的租户关联 + saveUserTenantRelation(userId, newTenantId, true); + } + } } return result; } @@ -183,14 +214,28 @@ public class UserServiceImpl extends ServiceImpl implements Us * @return true|false */ @Override + @Transactional(rollbackFor = Exception.class) public boolean deleteUsers(String idsStr) { Assert.isTrue(StrUtil.isNotBlank(idsStr), "删除的用户数据为空"); // 逻辑删除 List ids = Arrays.stream(idsStr.split(",")) .map(Long::parseLong) .collect(Collectors.toList()); - return this.removeByIds(ids); - + + boolean result = this.removeByIds(ids); + + // 如果启用多租户,删除用户租户关联 + if (result && Boolean.TRUE.equals(tenantProperties.getEnabled())) { + for (Long userId : ids) { + userTenantMapper.delete( + new LambdaQueryWrapper() + .eq(com.youlai.boot.system.model.entity.UserTenant::getUserId, userId) + ); + log.info("删除用户租户关联:userId={}", userId); + } + } + + return result; } /** @@ -686,4 +731,45 @@ public class UserServiceImpl extends ServiceImpl implements Us return userConverter.toOptions(list); } + /** + * 保存用户租户关联关系 + *

+ * 仅在启用多租户时调用此方法 + *

+ * + * @param userId 用户ID + * @param tenantId 租户ID + * @param isDefault 是否为默认租户 + */ + private void saveUserTenantRelation(Long userId, Long tenantId, boolean isDefault) { + if (userId == null || tenantId == null) { + log.warn("用户ID或租户ID为空,跳过保存用户租户关联"); + return; + } + + // 检查关联是否已存在 + com.youlai.boot.system.model.entity.UserTenant existingRelation = userTenantMapper.selectOne( + new LambdaQueryWrapper() + .eq(com.youlai.boot.system.model.entity.UserTenant::getUserId, userId) + .eq(com.youlai.boot.system.model.entity.UserTenant::getTenantId, tenantId) + ); + + if (existingRelation != null) { + // 如果已存在,更新 is_default 标识 + if (isDefault && existingRelation.getIsDefault() != 1) { + existingRelation.setIsDefault(1); + userTenantMapper.updateById(existingRelation); + log.info("更新用户租户关联:userId={}, tenantId={}, isDefault=true", userId, tenantId); + } + } else { + // 不存在则新增 + com.youlai.boot.system.model.entity.UserTenant userTenant = new com.youlai.boot.system.model.entity.UserTenant(); + userTenant.setUserId(userId); + userTenant.setTenantId(tenantId); + userTenant.setIsDefault(isDefault ? 1 : 0); + userTenantMapper.insert(userTenant); + log.info("保存用户租户关联:userId={}, tenantId={}, isDefault={}", userId, tenantId, isDefault); + } + } + } From 51d8220a18937884b27c17ffd83eb57b40bc8746 Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Thu, 11 Dec 2025 21:13:52 +0800 Subject: [PATCH 11/22] =?UTF-8?q?refactor:=20=E5=A4=9A=E7=A7=9F=E6=88=B7?= =?UTF-8?q?=E5=BC=80=E5=8F=91=E5=92=8C=E4=BB=A3=E7=A0=81=E8=A7=84=E8=8C=83?= =?UTF-8?q?=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/mysql/tenant_add.sql | 105 ++---------- sql/mysql/tenant_remove.sql | 86 +++------- sql/mysql/youlai_admin.sql | 157 +++++++---------- .../boot/auth/controller/AuthController.java | 78 ++++++++- .../boot/auth/model/dto/LoginRequest.java | 35 ++++ .../youlai/boot/auth/model/vo/CaptchaVO.java | 4 +- .../boot/auth/model/vo/ChooseTenantVO.java | 27 +++ .../youlai/boot/auth/service/AuthService.java | 3 +- .../auth/service/impl/AuthServiceImpl.java | 15 +- .../common/constant/JwtClaimConstants.java | 5 + .../com/youlai/boot/config/MybatisConfig.java | 9 +- .../com/youlai/boot/config/WebMvcConfig.java | 23 --- .../config/property/TenantProperties.java | 3 + .../boot/core/filter/TenantContextFilter.java | 59 +++++-- .../TenantValidationInterceptor.java | 86 ---------- .../java/com/youlai/boot/core/web/Result.java | 8 + .../com/youlai/boot/core/web/ResultCode.java | 3 + .../ai/controller/AiCommandController.java | 16 +- ...Mapper.java => AiCommandRecordMapper.java} | 8 +- ...AiCommandLog.java => AiCommandRecord.java} | 4 +- .../ai/model/query/AiParseLogPageQuery.java | 39 ----- ...mmandLogVO.java => AiCommandRecordVO.java} | 3 +- ...rvice.java => AiCommandRecordService.java} | 8 +- ...l.java => AiCommandRecordServiceImpl.java} | 26 +-- .../ai/service/impl/AiCommandServiceImpl.java | 92 +++++----- .../boot/platform/ai/tools/UserTools.java | 38 ++-- .../codegen/controller/CodegenController.java | 12 +- .../codegen/converter/CodegenConverter.java | 32 ++-- ...gMapper.java => GenTableColumnMapper.java} | 6 +- ...nConfigMapper.java => GenTableMapper.java} | 6 +- .../entity/{GenConfig.java => GenTable.java} | 9 +- ...enFieldConfig.java => GenTableColumn.java} | 11 +- ...ervice.java => GenTableColumnService.java} | 4 +- ...onfigService.java => GenTableService.java} | 6 +- .../service/impl/CodegenServiceImpl.java | 50 +++--- .../impl/GenFieldConfigServiceImpl.java | 21 --- .../impl/GenTableColumnServiceImpl.java | 21 +++ ...viceImpl.java => GenTableServiceImpl.java} | 98 +++++------ .../plugin/mybatis/MyMetaObjectHandler.java | 5 +- ...eHandler.java => MyTenantLineHandler.java} | 2 +- .../filter/CaptchaValidationFilter.java | 151 ++++++++++++---- .../boot/security/model/OnlineUser.java | 5 + .../boot/security/model/SysUserDetails.java | 6 + .../security/model/UserAuthCredentials.java | 5 + .../security/service/PermissionService.java | 4 +- .../service/SysUserDetailsService.java | 3 + .../boot/security/token/JwtTokenManager.java | 2 + .../security/token/RedisTokenManager.java | 2 + .../system/controller/TenantController.java | 64 +++---- .../system/mapper/TenantSwitchLogMapper.java | 15 -- .../boot/system/mapper/UserTenantMapper.java | 16 -- .../system/model/entity/TenantSwitchLog.java | 80 --------- .../boot/system/model/entity/UserTenant.java | 34 ---- .../boot/system/service/MenuService.java | 4 +- .../boot/system/service/TenantService.java | 13 -- .../boot/system/service/UserService.java | 18 +- .../system/service/impl/MenuServiceImpl.java | 14 +- .../service/impl/TenantServiceImpl.java | 162 ++++++------------ .../system/service/impl/UserServiceImpl.java | 154 ++++++++--------- src/main/resources/application-dev.yml | 6 +- src/main/resources/codegen.yml | 4 +- ...ogMapper.xml => AiCommandRecordMapper.xml} | 72 ++++---- .../mapper/codegen/DatabaseMapper.xml | 4 +- .../mapper/codegen/GenConfigMapper.xml | 2 +- .../mapper/codegen/GenFieldConfigMapper.xml | 2 +- .../mapper/codegen/GenTableColumnMapper.xml | 7 + .../mapper/codegen/GenTableMapper.xml | 7 + 67 files changed, 922 insertions(+), 1157 deletions(-) create mode 100644 src/main/java/com/youlai/boot/auth/model/dto/LoginRequest.java create mode 100644 src/main/java/com/youlai/boot/auth/model/vo/ChooseTenantVO.java delete mode 100644 src/main/java/com/youlai/boot/core/interceptor/TenantValidationInterceptor.java rename src/main/java/com/youlai/boot/platform/ai/mapper/{AiCommandLogMapper.java => AiCommandRecordMapper.java} (60%) rename src/main/java/com/youlai/boot/platform/ai/model/entity/{AiCommandLog.java => AiCommandRecord.java} (95%) delete mode 100644 src/main/java/com/youlai/boot/platform/ai/model/query/AiParseLogPageQuery.java rename src/main/java/com/youlai/boot/platform/ai/model/vo/{AiCommandLogVO.java => AiCommandRecordVO.java} (97%) rename src/main/java/com/youlai/boot/platform/ai/service/{AiCommandLogService.java => AiCommandRecordService.java} (66%) rename src/main/java/com/youlai/boot/platform/ai/service/impl/{AiCommandLogServiceImpl.java => AiCommandRecordServiceImpl.java} (52%) rename src/main/java/com/youlai/boot/platform/codegen/mapper/{GenFieldConfigMapper.java => GenTableColumnMapper.java} (53%) rename src/main/java/com/youlai/boot/platform/codegen/mapper/{GenConfigMapper.java => GenTableMapper.java} (55%) rename src/main/java/com/youlai/boot/platform/codegen/model/entity/{GenConfig.java => GenTable.java} (89%) rename src/main/java/com/youlai/boot/platform/codegen/model/entity/{GenFieldConfig.java => GenTableColumn.java} (89%) rename src/main/java/com/youlai/boot/platform/codegen/service/{GenFieldConfigService.java => GenTableColumnService.java} (56%) rename src/main/java/com/youlai/boot/platform/codegen/service/{GenConfigService.java => GenTableService.java} (77%) delete mode 100644 src/main/java/com/youlai/boot/platform/codegen/service/impl/GenFieldConfigServiceImpl.java create mode 100644 src/main/java/com/youlai/boot/platform/codegen/service/impl/GenTableColumnServiceImpl.java rename src/main/java/com/youlai/boot/platform/codegen/service/impl/{GenConfigServiceImpl.java => GenTableServiceImpl.java} (66%) rename src/main/java/com/youlai/boot/plugin/mybatis/{TenantLineHandler.java => MyTenantLineHandler.java} (95%) delete mode 100644 src/main/java/com/youlai/boot/system/mapper/TenantSwitchLogMapper.java delete mode 100644 src/main/java/com/youlai/boot/system/mapper/UserTenantMapper.java delete mode 100644 src/main/java/com/youlai/boot/system/model/entity/TenantSwitchLog.java delete mode 100644 src/main/java/com/youlai/boot/system/model/entity/UserTenant.java rename src/main/resources/mapper/ai/{AiCommandLogMapper.xml => AiCommandRecordMapper.xml} (63%) create mode 100644 src/main/resources/mapper/codegen/GenTableColumnMapper.xml create mode 100644 src/main/resources/mapper/codegen/GenTableMapper.xml diff --git a/sql/mysql/tenant_add.sql b/sql/mysql/tenant_add.sql index ec367255..07305776 100644 --- a/sql/mysql/tenant_add.sql +++ b/sql/mysql/tenant_add.sql @@ -39,58 +39,24 @@ INSERT INTO `sys_tenant` (`id`, `name`, `code`, `status`, `create_time`) VALUES (1, '默认租户', 'DEFAULT', 1, NOW()); -- ============================================ --- 2. 创建租户切换审计日志表 --- ============================================ -DROP TABLE IF EXISTS `sys_tenant_switch_log`; -CREATE TABLE `sys_tenant_switch_log` ( - `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `user_id` bigint NOT NULL COMMENT '用户ID', - `username` varchar(64) COMMENT '用户名', - `from_tenant_id` bigint COMMENT '原租户ID', - `from_tenant_name` varchar(100) COMMENT '原租户名称', - `to_tenant_id` bigint NOT NULL COMMENT '目标租户ID', - `to_tenant_name` varchar(100) COMMENT '目标租户名称', - `switch_time` datetime NOT NULL COMMENT '切换时间', - `ip_address` varchar(50) COMMENT 'IP地址', - `user_agent` varchar(500) COMMENT '浏览器信息', - `status` tinyint DEFAULT '1' COMMENT '切换状态(1-成功 0-失败)', - `fail_reason` varchar(255) COMMENT '失败原因', - PRIMARY KEY (`id`), - KEY `idx_user_id` (`user_id`), - KEY `idx_switch_time` (`switch_time`), - KEY `idx_status` (`status`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户切换审计日志表'; - --- ============================================ --- 3. 创建用户租户关联表(支持一个用户属于多个租户) --- ============================================ -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 字段 +-- 2. 为业务表添加 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; +-- 修改 username 索引:从单列索引改为 (username, tenant_id) 组合唯一索引 +-- 这样同一租户内用户名唯一,不同租户可以有相同用户名 +DROP INDEX `login_name` ON `sys_user`; +ALTER TABLE `sys_user` +ADD UNIQUE KEY `uk_username_tenant` (`username`, `tenant_id`); + -- 角色表 ALTER TABLE `sys_role` ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `id`, @@ -120,53 +86,21 @@ 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` +ALTER TABLE `ai_command_record` 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; +UPDATE `ai_command_record` 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. 添加租户管理菜单和权限(仅在菜单不存在时添加) +-- 4. 添加租户管理菜单和权限(仅在菜单不存在时添加) -- ============================================ -- 租户管理主菜单(放在部门管理之后,字典管理之前,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`) @@ -190,20 +124,3 @@ VALUES 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 index d247a4f7..c179895b 100644 --- a/sql/mysql/tenant_remove.sql +++ b/sql/mysql/tenant_remove.sql @@ -12,60 +12,48 @@ USE youlai_admin; SET FOREIGN_KEY_CHECKS = 0; -- ============================================ --- 1. 删除用户租户关联表 --- ============================================ -DROP TABLE IF EXISTS `sys_user_tenant`; - --- ============================================ --- 2. 删除租户表(可选) +-- 1. 删除租户表(可选) -- ============================================ -- 注意:如果将来可能再次启用多租户,建议保留此表 -- 如需删除,取消下面的注释 -- DROP TABLE IF EXISTS `sys_tenant`; -- ============================================ --- 3. 移除业务表的 tenant_id 字段和索引 +-- 2. 移除业务表的 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_user` DROP INDEX `uk_username_tenant`; +-- 删除租户ID索引和字段 +ALTER TABLE `sys_user` DROP INDEX `idx_tenant_id`; +ALTER TABLE `sys_user` DROP COLUMN `tenant_id`; +-- 恢复原来的用户名唯一索引 +ALTER TABLE `sys_user` ADD UNIQUE KEY `login_name` (`username`); -- 角色表 -ALTER TABLE `sys_role` DROP INDEX IF EXISTS `idx_tenant_id`; -ALTER TABLE `sys_role` DROP COLUMN IF EXISTS `tenant_id`; +ALTER TABLE `sys_role` DROP INDEX `idx_tenant_id`; +ALTER TABLE `sys_role` DROP COLUMN `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_dept` DROP INDEX `idx_tenant_id`; +ALTER TABLE `sys_dept` DROP COLUMN `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_notice` DROP INDEX `idx_tenant_id`; +ALTER TABLE `sys_notice` DROP COLUMN `tenant_id`; -- 系统日志表 -ALTER TABLE `sys_log` DROP INDEX IF EXISTS `idx_tenant_id`; -ALTER TABLE `sys_log` DROP COLUMN IF EXISTS `tenant_id`; +ALTER TABLE `sys_log` DROP INDEX `idx_tenant_id`; +ALTER TABLE `sys_log` DROP COLUMN `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`; +ALTER TABLE `ai_command_record` DROP INDEX `idx_tenant_id`; +ALTER TABLE `ai_command_record` DROP COLUMN `tenant_id`; -- ============================================ --- 4. 删除租户管理菜单和权限 +-- 3. 删除租户管理菜单和权限 -- ============================================ -- 删除角色菜单关联 DELETE FROM `sys_role_menu` WHERE `menu_id` IN (6, 141, 142, 143, 144, 145); @@ -76,36 +64,4 @@ 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,请先检查字段是否存在,再执行删除操作 --- ============================================ +SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file diff --git a/sql/mysql/youlai_admin.sql b/sql/mysql/youlai_admin.sql index 9a50c036..423a091a 100644 --- a/sql/mysql/youlai_admin.sql +++ b/sql/mysql/youlai_admin.sql @@ -255,19 +255,6 @@ INSERT INTO `sys_menu` VALUES (913, 911, '0,9,910,911', '菜单三级-2', 'M', N -- 路由参数 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 @@ -353,6 +340,7 @@ INSERT INTO `sys_role_menu` VALUES (2, 1001), (2, 1002); DROP TABLE IF EXISTS `sys_user`; CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT, + `tenant_id` bigint DEFAULT 1 COMMENT '租户ID', `username` varchar(64) COMMENT '用户名', `nickname` varchar(64) COMMENT '昵称', `gender` tinyint(1) DEFAULT 1 COMMENT '性别((1-男 2-女 0-保密)', @@ -369,15 +357,16 @@ CREATE TABLE `sys_user` ( `is_deleted` tinyint(1) DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)', `openid` char(28) COMMENT '微信 openid', PRIMARY KEY (`id`) USING BTREE, - KEY `login_name` (`username`) + UNIQUE KEY `uk_username_tenant` (`username`, `tenant_id`), + KEY `idx_tenant_id` (`tenant_id`) ) 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); +INSERT INTO `sys_user` VALUES (1, 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, 1, '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, 1, '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 @@ -425,10 +414,10 @@ CREATE TABLE `sys_log` ( ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COMMENT='系统操作日志表'; -- ---------------------------- --- Table structure for gen_config +-- Table structure for gen_table -- ---------------------------- -DROP TABLE IF EXISTS `gen_config`; -CREATE TABLE `gen_config` ( +DROP TABLE IF EXISTS `gen_table`; +CREATE TABLE `gen_table` ( `id` bigint NOT NULL AUTO_INCREMENT, `table_name` varchar(100) NOT NULL COMMENT '表名', `module_name` varchar(100) COMMENT '模块名', @@ -447,12 +436,12 @@ CREATE TABLE `gen_config` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代码生成配置表'; -- ---------------------------- --- Table structure for gen_field_config +-- Table structure for gen_table_column -- ---------------------------- -DROP TABLE IF EXISTS `gen_field_config`; -CREATE TABLE `gen_field_config` ( +DROP TABLE IF EXISTS `gen_table_column`; +CREATE TABLE `gen_table_column` ( `id` bigint NOT NULL AUTO_INCREMENT, - `config_id` bigint NOT NULL COMMENT '关联的配置ID', + `table_id` bigint NOT NULL COMMENT '关联的表配置ID', `column_name` varchar(100) , `column_type` varchar(50) , `column_length` int , @@ -471,7 +460,7 @@ CREATE TABLE `gen_field_config` ( `create_time` datetime COMMENT '创建时间', `update_time` datetime COMMENT '更新时间', PRIMARY KEY (`id`), - KEY `config_id` (`config_id`) + KEY `idx_table_id` (`table_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代码生成字段配置表'; -- ---------------------------- @@ -559,60 +548,60 @@ 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 `ai_command_record`; +CREATE TABLE `ai_command_record` ( + `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`) + `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='系统租户表'; -- ---------------------------- @@ -621,28 +610,6 @@ CREATE TABLE `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/auth/controller/AuthController.java b/src/main/java/com/youlai/boot/auth/controller/AuthController.java index e4d169fc..ddfe2773 100644 --- a/src/main/java/com/youlai/boot/auth/controller/AuthController.java +++ b/src/main/java/com/youlai/boot/auth/controller/AuthController.java @@ -1,21 +1,33 @@ package com.youlai.boot.auth.controller; import com.youlai.boot.auth.model.vo.CaptchaVO; +import com.youlai.boot.auth.model.vo.ChooseTenantVO; +import com.youlai.boot.auth.model.dto.LoginRequest; import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDTO; import com.youlai.boot.common.enums.LogModuleEnum; +import com.youlai.boot.config.property.TenantProperties; import com.youlai.boot.core.web.Result; import com.youlai.boot.auth.service.AuthService; import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO; import com.youlai.boot.common.annotation.Log; +import com.youlai.boot.core.web.ResultCode; import com.youlai.boot.security.model.AuthenticationToken; +import com.youlai.boot.system.model.entity.User; +import com.youlai.boot.system.model.vo.TenantVO; +import com.youlai.boot.system.service.TenantService; +import com.youlai.boot.system.service.UserService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.*; +import java.util.List; +import java.util.stream.Collectors; + /** * 认证控制层 @@ -31,6 +43,10 @@ import org.springframework.web.bind.annotation.*; public class AuthController { private final AuthService authService; + private final UserService userService; + private final TenantService tenantService; + private final TenantProperties tenantProperties; + private final PasswordEncoder passwordEncoder; @Operation(summary = "获取验证码") @GetMapping("/captcha") @@ -42,12 +58,62 @@ public class AuthController { @Operation(summary = "账号密码登录") @PostMapping("/login") @Log(value = "登录", module = LogModuleEnum.LOGIN) - public Result login( - @Parameter(description = "用户名", example = "admin") @RequestParam String username, - @Parameter(description = "密码", example = "123456") @RequestParam String password - ) { - AuthenticationToken authenticationToken = authService.login(username, password); - return Result.success(authenticationToken); + public Result login(@RequestBody @Valid LoginRequest request) { + String username = request.getUsername(); + String password = request.getPassword(); + Long tenantId = request.getTenantId(); + + // 如果未启用多租户,直接登录 + if (tenantProperties == null || !Boolean.TRUE.equals(tenantProperties.getEnabled())) { + AuthenticationToken authenticationToken = authService.login(username, password, null); + return Result.success(authenticationToken); + } + + // 多租户模式:如果指定了租户ID,直接验证该租户下的密码 + if (tenantId != null) { + AuthenticationToken authenticationToken = authService.login(username, password, tenantId); + return Result.success(authenticationToken); + } + + // 多租户模式:未指定租户ID,查询该用户名在所有租户下的记录 + List users = userService.listUsersByUsername(username); + + if (users.isEmpty()) { + return Result.failed("用户不存在"); + } + + // 过滤出正常状态的用户 + List activeUsers = users.stream() + .filter(user -> user.getStatus() != null && user.getStatus() == 1) + .toList(); + + if (activeUsers.isEmpty()) { + return Result.failed("用户已被禁用"); + } + + // 如果只有1个租户,尝试验证该租户下的密码(兼容性) + if (activeUsers.size() == 1) { + User user = activeUsers.get(0); + // 登录(Spring Security 会验证密码) + AuthenticationToken authenticationToken = authService.login(username, password, user.getTenantId()); + return Result.success(authenticationToken); + } + + // 如果多个租户,返回 choose_tenant 响应(含 tenants 列表) + // 注意:此时不验证密码,直接返回租户列表让用户选择 + List tenants = activeUsers.stream() + .map(user -> tenantService.getTenantById(user.getTenantId())) + .filter(tenant -> tenant != null && (tenant.getStatus() == null || tenant.getStatus() == 1)) + .distinct() // 去重(理论上不会有重复,但保险起见) + .collect(Collectors.toList()); + + if (tenants.isEmpty()) { + return Result.failed("用户所属的租户均不可用"); + } + + // 返回 choose_tenant 响应 + ChooseTenantVO chooseTenantVO = new ChooseTenantVO(tenants); + return Result.failed(ResultCode.CHOOSE_TENANT, chooseTenantVO); } @Operation(summary = "短信验证码登录") diff --git a/src/main/java/com/youlai/boot/auth/model/dto/LoginRequest.java b/src/main/java/com/youlai/boot/auth/model/dto/LoginRequest.java new file mode 100644 index 00000000..de52a3cc --- /dev/null +++ b/src/main/java/com/youlai/boot/auth/model/dto/LoginRequest.java @@ -0,0 +1,35 @@ +package com.youlai.boot.auth.model.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; + +/** + * 登录请求参数 + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Schema(description = "登录请求参数") +@Data +public class LoginRequest { + + @Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "admin") + @NotBlank(message = "用户名不能为空") + private String username; + + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + @NotBlank(message = "密码不能为空") + private String password; + + @Schema(description = "验证码缓存ID", example = "captcha_id_123") + private String captchaId; + + @Schema(description = "验证码", example = "1234") + private String captchaCode; + + @Schema(description = "租户ID(可选,多租户模式下用于指定租户)", example = "1") + private Long tenantId; +} + diff --git a/src/main/java/com/youlai/boot/auth/model/vo/CaptchaVO.java b/src/main/java/com/youlai/boot/auth/model/vo/CaptchaVO.java index 3f42c300..a81fb3c6 100644 --- a/src/main/java/com/youlai/boot/auth/model/vo/CaptchaVO.java +++ b/src/main/java/com/youlai/boot/auth/model/vo/CaptchaVO.java @@ -15,8 +15,8 @@ import lombok.Data; @Builder public class CaptchaVO { - @Schema(description = "验证码缓存 Key") - private String captchaKey; + @Schema(description = "验证码缓存 ID") + private String captchaId; @Schema(description = "验证码图片Base64字符串") private String captchaBase64; diff --git a/src/main/java/com/youlai/boot/auth/model/vo/ChooseTenantVO.java b/src/main/java/com/youlai/boot/auth/model/vo/ChooseTenantVO.java new file mode 100644 index 00000000..15fdfdda --- /dev/null +++ b/src/main/java/com/youlai/boot/auth/model/vo/ChooseTenantVO.java @@ -0,0 +1,27 @@ +package com.youlai.boot.auth.model.vo; + +import com.youlai.boot.system.model.vo.TenantVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; + +/** + * 选择租户响应VO + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "选择租户响应") +public class ChooseTenantVO implements Serializable { + + @Schema(description = "租户列表") + private List tenants; +} + diff --git a/src/main/java/com/youlai/boot/auth/service/AuthService.java b/src/main/java/com/youlai/boot/auth/service/AuthService.java index 5fe2eadf..2adaf581 100644 --- a/src/main/java/com/youlai/boot/auth/service/AuthService.java +++ b/src/main/java/com/youlai/boot/auth/service/AuthService.java @@ -18,9 +18,10 @@ public interface AuthService { * * @param username 用户名 * @param password 密码 + * @param tenantId 租户ID(可选,多租户模式下用于指定租户) * @return 登录结果 */ - AuthenticationToken login(String username, String password); + AuthenticationToken login(String username, String password, Long tenantId); /** * 登出 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 259a26a5..1d740de1 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 @@ -21,6 +21,7 @@ import com.youlai.boot.security.model.WxMiniAppCodeAuthenticationToken; import com.youlai.boot.security.model.WxMiniAppPhoneAuthenticationToken; import com.youlai.boot.security.token.TokenManager; import com.youlai.boot.security.util.SecurityUtils; +import com.youlai.boot.common.tenant.TenantContextHolder; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; @@ -61,10 +62,16 @@ public class AuthServiceImpl implements AuthService { * * @param username 用户名 * @param password 密码 + * @param tenantId 租户ID(可选,多租户模式下用于指定租户) * @return 访问令牌 */ @Override - public AuthenticationToken login(String username, String password) { + public AuthenticationToken login(String username, String password, Long tenantId) { + // 如果指定了租户ID,需要先设置租户上下文,以便查询该租户下的用户 + if (tenantId != null) { + com.youlai.boot.common.tenant.TenantContextHolder.setTenantId(tenantId); + } + // 1. 创建用于密码认证的令牌(未认证) UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username.trim(), password); @@ -194,16 +201,16 @@ public class AuthServiceImpl implements AuthService { String imageBase64Data = captcha.getImageBase64Data(); // 验证码文本缓存至Redis,用于登录校验 - String captchaKey = IdUtil.fastSimpleUUID(); + String captchaId = IdUtil.fastSimpleUUID(); redisTemplate.opsForValue().set( - StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaKey), + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaId), captchaCode, captchaProperties.getExpireSeconds(), TimeUnit.SECONDS ); return CaptchaVO.builder() - .captchaKey(captchaKey) + .captchaId(captchaId) .captchaBase64(imageBase64Data) .build(); } diff --git a/src/main/java/com/youlai/boot/common/constant/JwtClaimConstants.java b/src/main/java/com/youlai/boot/common/constant/JwtClaimConstants.java index c0a84a94..4e2b3532 100644 --- a/src/main/java/com/youlai/boot/common/constant/JwtClaimConstants.java +++ b/src/main/java/com/youlai/boot/common/constant/JwtClaimConstants.java @@ -35,6 +35,11 @@ public interface JwtClaimConstants { */ String AUTHORITIES = "authorities"; + /** + * 租户ID + */ + String TENANT_ID = "tenantId"; + /** * 安全版本号,用于按用户失效历史令牌 */ diff --git a/src/main/java/com/youlai/boot/config/MybatisConfig.java b/src/main/java/com/youlai/boot/config/MybatisConfig.java index 6cca57d5..2a6166b0 100644 --- a/src/main/java/com/youlai/boot/config/MybatisConfig.java +++ b/src/main/java/com/youlai/boot/config/MybatisConfig.java @@ -9,12 +9,11 @@ import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerIntercept import com.youlai.boot.config.property.TenantProperties; import com.youlai.boot.plugin.mybatis.MyDataPermissionHandler; import com.youlai.boot.plugin.mybatis.MyMetaObjectHandler; -import com.youlai.boot.plugin.mybatis.TenantLineHandler; +import com.youlai.boot.plugin.mybatis.MyTenantLineHandler; import org.apache.ibatis.mapping.DatabaseIdProvider; import org.apache.ibatis.mapping.VendorDatabaseIdProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.annotation.EnableTransactionManagement; @@ -35,7 +34,7 @@ public class MybatisConfig { private String dbType; @Autowired(required = false) - private TenantLineHandler tenantLineHandler; + private MyTenantLineHandler myTenantLineHandler; @Autowired(required = false) private TenantProperties tenantProperties; @@ -51,8 +50,8 @@ public class MybatisConfig { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 多租户插件(如果启用,必须在最前面) - if (tenantProperties != null && Boolean.TRUE.equals(tenantProperties.getEnabled()) && tenantLineHandler != null) { - interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(tenantLineHandler)); + if (tenantProperties != null && Boolean.TRUE.equals(tenantProperties.getEnabled()) && myTenantLineHandler != null) { + interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(myTenantLineHandler)); } // 数据权限 diff --git a/src/main/java/com/youlai/boot/config/WebMvcConfig.java b/src/main/java/com/youlai/boot/config/WebMvcConfig.java index fae163f3..a8082ea9 100644 --- a/src/main/java/com/youlai/boot/config/WebMvcConfig.java +++ b/src/main/java/com/youlai/boot/config/WebMvcConfig.java @@ -7,21 +7,17 @@ import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; -import com.youlai.boot.core.interceptor.TenantValidationInterceptor; import jakarta.validation.Validation; import jakarta.validation.Validator; import jakarta.validation.ValidatorFactory; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.hibernate.validator.HibernateValidator; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.math.BigInteger; @@ -43,9 +39,6 @@ public class WebMvcConfig implements WebMvcConfigurer { private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - @Autowired(required = false) - private TenantValidationInterceptor tenantValidationInterceptor; - /** * 配置消息转换器 * @@ -85,22 +78,6 @@ public class WebMvcConfig implements WebMvcConfigurer { * @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory * @return Validator 实例 */ - /** - * 配置拦截器 - * - * @param registry 拦截器注册器 - */ - @Override - public void addInterceptors(InterceptorRegistry registry) { - // 注册租户校验拦截器(仅在多租户模式启用时生效) - if (tenantValidationInterceptor != null) { - registry.addInterceptor(tenantValidationInterceptor) - .addPathPatterns("/api/**") - .order(2); // 在认证拦截器之后执行 - log.info("租户校验拦截器已注册"); - } - } - @Bean public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) diff --git a/src/main/java/com/youlai/boot/config/property/TenantProperties.java b/src/main/java/com/youlai/boot/config/property/TenantProperties.java index 460c2068..66c9c04e 100644 --- a/src/main/java/com/youlai/boot/config/property/TenantProperties.java +++ b/src/main/java/com/youlai/boot/config/property/TenantProperties.java @@ -57,6 +57,9 @@ public class TenantProperties { ignoreTables.add("sys_dict"); ignoreTables.add("sys_dict_item"); ignoreTables.add("sys_config"); + // 代码生成表(平台共用,不做租户隔离) + ignoreTables.add("gen_table"); + ignoreTables.add("gen_table_column"); } } diff --git a/src/main/java/com/youlai/boot/core/filter/TenantContextFilter.java b/src/main/java/com/youlai/boot/core/filter/TenantContextFilter.java index 9e53282d..667e2b43 100644 --- a/src/main/java/com/youlai/boot/core/filter/TenantContextFilter.java +++ b/src/main/java/com/youlai/boot/core/filter/TenantContextFilter.java @@ -1,7 +1,10 @@ package com.youlai.boot.core.filter; +import com.youlai.boot.common.constant.SecurityConstants; import com.youlai.boot.common.tenant.TenantContextHolder; import com.youlai.boot.config.property.TenantProperties; +import com.youlai.boot.security.model.SysUserDetails; +import com.youlai.boot.security.token.TokenManager; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -10,6 +13,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.core.annotation.Order; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; @@ -34,39 +39,59 @@ import java.io.IOException; public class TenantContextFilter extends OncePerRequestFilter { private final TenantProperties tenantProperties; + private final TokenManager tokenManager; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { - // 从请求头获取租户ID - String tenantIdStr = request.getHeader(tenantProperties.getHeaderName()); + // 1) 优先从已认证用户中获取租户ID + Long tenantId = resolveTenantFromAuthentication(SecurityContextHolder.getContext().getAuthentication()); - 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 + // 2) 如果尚未获取到,尝试从 Token 中解析 + if (tenantId == null) { + tenantId = resolveTenantFromToken(request); + } + + // 3) 仍为空则使用默认租户 + if (tenantId == null) { Long defaultTenantId = tenantProperties.getDefaultTenantId(); if (defaultTenantId != null) { - TenantContextHolder.setTenantId(defaultTenantId); - log.debug("使用默认租户ID: {}", defaultTenantId); + tenantId = defaultTenantId; } } - // 继续执行过滤器链 - filterChain.doFilter(request, response); + if (tenantId != null) { + TenantContextHolder.setTenantId(tenantId); + log.debug("TenantContextFilter set tenantId: {}", tenantId); + } + filterChain.doFilter(request, response); } finally { - // 请求结束时清除租户上下文,避免线程池复用导致的数据泄露 TenantContextHolder.clear(); } } + + private Long resolveTenantFromAuthentication(Authentication authentication) { + if (authentication == null) { + return null; + } + Object principal = authentication.getPrincipal(); + if (principal instanceof SysUserDetails details) { + return details.getTenantId(); + } + return null; + } + + private Long resolveTenantFromToken(HttpServletRequest request) { + String authHeader = request.getHeader("Authorization"); + if (!StringUtils.hasText(authHeader) || !authHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + return null; + } + String token = authHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); + Authentication authentication = tokenManager.parseToken(token); + return resolveTenantFromAuthentication(authentication); + } } diff --git a/src/main/java/com/youlai/boot/core/interceptor/TenantValidationInterceptor.java b/src/main/java/com/youlai/boot/core/interceptor/TenantValidationInterceptor.java deleted file mode 100644 index 7ef57da7..00000000 --- a/src/main/java/com/youlai/boot/core/interceptor/TenantValidationInterceptor.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.youlai.boot.core.interceptor; - -import com.youlai.boot.common.result.ResultCode; -import com.youlai.boot.common.tenant.TenantContextHolder; -import com.youlai.boot.config.property.TenantProperties; -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.stereotype.Component; -import org.springframework.web.servlet.HandlerInterceptor; - -import java.io.IOException; -import java.util.Arrays; -import java.util.List; - -/** - * 租户ID强制校验拦截器 - *

- * 对于需要租户隔离的接口,强制要求携带有效的租户ID - * 防止恶意用户通过不携带租户ID来访问默认租户数据 - *

- * - * @author Ray.Hao - * @since 3.0.0 - */ -@Slf4j -@Component -@RequiredArgsConstructor -@ConditionalOnProperty(prefix = "youlai.tenant", name = "enabled", havingValue = "true") -public class TenantValidationInterceptor implements HandlerInterceptor { - - private final TenantProperties tenantProperties; - - /** - * 白名单路径:这些路径不需要租户ID校验 - */ - private static final List WHITELIST_PATHS = Arrays.asList( - "/api/v1/auth/login", - "/api/v1/auth/logout", - "/api/v1/auth/captcha", - "/api/v1/tenant/list", - "/doc.html", - "/v3/api-docs", - "/swagger-ui", - "/favicon.ico", - "/error" - ); - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { - String requestPath = request.getRequestURI(); - - // 检查是否在白名单中 - if (isWhitelistPath(requestPath)) { - return true; - } - - // 检查租户ID是否存在 - Long tenantId = TenantContextHolder.getTenantId(); - if (tenantId == null) { - log.warn("请求路径 {} 缺少租户ID", requestPath); - response.setStatus(HttpServletResponse.SC_BAD_REQUEST); - response.setContentType("application/json;charset=UTF-8"); - response.getWriter().write(String.format( - "{\"code\":\"%s\",\"msg\":\"租户ID不能为空,请联系管理员\"}", - ResultCode.BAD_REQUEST.getCode() - )); - return false; - } - - // 可选:校验租户是否有效(需要注入 TenantService) - // 这里暂时只校验租户ID不为空 - - return true; - } - - /** - * 检查路径是否在白名单中 - */ - private boolean isWhitelistPath(String requestPath) { - return WHITELIST_PATHS.stream() - .anyMatch(requestPath::startsWith); - } -} diff --git a/src/main/java/com/youlai/boot/core/web/Result.java b/src/main/java/com/youlai/boot/core/web/Result.java index 783be757..6e4e6ac6 100644 --- a/src/main/java/com/youlai/boot/core/web/Result.java +++ b/src/main/java/com/youlai/boot/core/web/Result.java @@ -56,6 +56,14 @@ public class Result implements Serializable { return result(resultCode.getCode(), StrUtil.isNotBlank(msg) ? msg : resultCode.getMsg(), null); } + public static Result failed(IResultCode resultCode, T data) { + return result(resultCode.getCode(), resultCode.getMsg(), data); + } + + public static Result failed(IResultCode resultCode, String msg, T data) { + return result(resultCode.getCode(), StrUtil.isNotBlank(msg) ? msg : resultCode.getMsg(), data); + } + private static Result result(IResultCode resultCode, T data) { return result(resultCode.getCode(), resultCode.getMsg(), data); } diff --git a/src/main/java/com/youlai/boot/core/web/ResultCode.java b/src/main/java/com/youlai/boot/core/web/ResultCode.java index d7ea897b..8ae2a22e 100644 --- a/src/main/java/com/youlai/boot/core/web/ResultCode.java +++ b/src/main/java/com/youlai/boot/core/web/ResultCode.java @@ -76,6 +76,9 @@ public enum ResultCode implements IResultCode, Serializable { USER_VERIFICATION_CODE_ATTEMPT_LIMIT_EXCEEDED("A0241", "用户验证码尝试次数超限"), USER_VERIFICATION_CODE_EXPIRED("A0242", "用户验证码过期"), + // 多租户登录 + CHOOSE_TENANT("A0250", "请选择登录租户"), + /** 二级宏观错误码 */ ACCESS_PERMISSION_EXCEPTION("A0300", "访问权限异常"), ACCESS_UNAUTHORIZED("A0301", "访问未授权"), 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 f64b833f..464dfc19 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.AiCommandLogVO; -import com.youlai.boot.platform.ai.service.AiCommandLogService; +import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO; +import com.youlai.boot.platform.ai.service.AiCommandRecordService; 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 AiCommandLogService logService; + private final AiCommandRecordService recordService; @Operation(summary = "解析自然语言命令") @PostMapping("/parse") @@ -72,17 +72,17 @@ public class AiCommandController { @Operation(summary = "获取AI命令记录分页列表") @GetMapping("/records") - public PageResult getLogPage(AiCommandPageQuery queryParams) { - IPage page = logService.getLogPage(queryParams); + public PageResult getRecordPage(AiCommandPageQuery queryParams) { + IPage page = recordService.getRecordPage(queryParams); return PageResult.success(page); } @Operation(summary = "撤销命令执行") - @PostMapping("/rollback/{logId}") + @PostMapping("/rollback/{recordId}") public Result rollbackCommand( - @Parameter(description = "记录ID") @PathVariable String logId + @Parameter(description = "记录ID") @PathVariable String recordId ) { - logService.rollbackCommand(logId); + recordService.rollbackCommand(recordId); return Result.success("撤销成功"); } diff --git a/src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandLogMapper.java b/src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandRecordMapper.java similarity index 60% rename from src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandLogMapper.java rename to src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandRecordMapper.java index 90bb3f32..e26bb6c6 100644 --- a/src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandLogMapper.java +++ b/src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandRecordMapper.java @@ -3,9 +3,9 @@ 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.AiCommandLog; +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.AiCommandLogVO; +import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO; import org.apache.ibatis.annotations.Mapper; /** @@ -15,12 +15,12 @@ import org.apache.ibatis.annotations.Mapper; * @since 3.0.0 */ @Mapper -public interface AiCommandLogMapper extends BaseMapper { +public interface AiCommandRecordMapper extends BaseMapper { /** * 获取 AI 命令记录分页列表 */ - IPage getLogPage(Page page, AiCommandPageQuery queryParams); + IPage getRecordPage(Page page, AiCommandPageQuery queryParams); } diff --git a/src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandLog.java b/src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandRecord.java similarity index 95% rename from src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandLog.java rename to src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandRecord.java index 9a55687f..1750970b 100644 --- a/src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandLog.java +++ b/src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandRecord.java @@ -15,8 +15,8 @@ import java.math.BigDecimal; */ @Data @EqualsAndHashCode(callSuper = true) -@TableName("ai_command_log") -public class AiCommandLog extends BaseEntity { +@TableName("ai_command_record") +public class AiCommandRecord extends BaseEntity { /** 用户ID */ private Long userId; diff --git a/src/main/java/com/youlai/boot/platform/ai/model/query/AiParseLogPageQuery.java b/src/main/java/com/youlai/boot/platform/ai/model/query/AiParseLogPageQuery.java deleted file mode 100644 index 065de26e..00000000 --- a/src/main/java/com/youlai/boot/platform/ai/model/query/AiParseLogPageQuery.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.youlai.boot.platform.ai.model.query; - -import com.youlai.boot.common.base.BasePageQuery; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Getter; -import lombok.Setter; - -import java.util.List; - -/** - * AI命令解析日志分页查询对象 - * - * @author Ray.Hao - * @since 3.0.0 - */ -@Schema(description = "AI命令解析日志分页查询对象") -@Getter -@Setter -public class AiParseLogPageQuery extends BasePageQuery { - - @Schema(description = "关键字(原始命令/用户名)") - private String keywords; - - @Schema(description = "解析是否成功(0-失败, 1-成功)") - private Boolean parseSuccess; - - @Schema(description = "用户ID") - private Long userId; - - @Schema(description = "AI提供商(qwen/openai/deepseek/gemini等)") - private String provider; - - @Schema(description = "AI模型(qwen-plus/qwen-max/gpt-4-turbo等)") - private String model; - - @Schema(description = "创建时间范围") - private List createTime; -} - diff --git a/src/main/java/com/youlai/boot/platform/ai/model/vo/AiCommandLogVO.java b/src/main/java/com/youlai/boot/platform/ai/model/vo/AiCommandRecordVO.java similarity index 97% rename from src/main/java/com/youlai/boot/platform/ai/model/vo/AiCommandLogVO.java rename to src/main/java/com/youlai/boot/platform/ai/model/vo/AiCommandRecordVO.java index b35d3745..088c5f9c 100644 --- a/src/main/java/com/youlai/boot/platform/ai/model/vo/AiCommandLogVO.java +++ b/src/main/java/com/youlai/boot/platform/ai/model/vo/AiCommandRecordVO.java @@ -16,7 +16,7 @@ import java.time.LocalDateTime; */ @Data @Schema(description = "AI命令记录VO") -public class AiCommandLogVO implements Serializable { +public class AiCommandRecordVO implements Serializable { @Schema(description = "主键ID") private String id; @@ -90,4 +90,3 @@ public class AiCommandLogVO implements Serializable { private LocalDateTime updateTime; } - diff --git a/src/main/java/com/youlai/boot/platform/ai/service/AiCommandLogService.java b/src/main/java/com/youlai/boot/platform/ai/service/AiCommandRecordService.java similarity index 66% rename from src/main/java/com/youlai/boot/platform/ai/service/AiCommandLogService.java rename to src/main/java/com/youlai/boot/platform/ai/service/AiCommandRecordService.java index 5b4bea19..5e94c87d 100644 --- a/src/main/java/com/youlai/boot/platform/ai/service/AiCommandLogService.java +++ b/src/main/java/com/youlai/boot/platform/ai/service/AiCommandRecordService.java @@ -2,9 +2,9 @@ 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.AiCommandLog; +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.AiCommandLogVO; +import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO; /** * AI 命令记录服务接口 @@ -12,7 +12,7 @@ import com.youlai.boot.platform.ai.model.vo.AiCommandLogVO; * @author Ray.Hao * @since 3.0.0 */ -public interface AiCommandLogService extends IService { +public interface AiCommandRecordService extends IService { /** * 获取命令记录分页列表 @@ -20,7 +20,7 @@ public interface AiCommandLogService extends IService { * @param queryParams 查询参数 * @return 命令记录分页列表 */ - IPage getLogPage(AiCommandPageQuery queryParams); + IPage getRecordPage(AiCommandPageQuery queryParams); /** * 撤销命令执行 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/AiCommandRecordServiceImpl.java similarity index 52% rename from src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandLogServiceImpl.java rename to src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandRecordServiceImpl.java index 05ec8f4f..33df604c 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/AiCommandRecordServiceImpl.java @@ -3,11 +3,11 @@ 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.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.AiCommandLogVO; -import com.youlai.boot.platform.ai.service.AiCommandLogService; +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; @@ -21,28 +21,28 @@ import org.springframework.stereotype.Service; @Service @Slf4j @RequiredArgsConstructor -public class AiCommandLogServiceImpl extends ServiceImpl - implements AiCommandLogService { +public class AiCommandRecordServiceImpl extends ServiceImpl + implements AiCommandRecordService { @Override - public IPage getLogPage(AiCommandPageQuery queryParams) { - Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); - return this.baseMapper.getLogPage(page, queryParams); + public IPage getRecordPage(AiCommandPageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + return this.baseMapper.getRecordPage(page, queryParams); } @Override public void rollbackCommand(String logId) { - AiCommandLog commandLog = this.getById(logId); - if (commandLog == null) { + AiCommandRecord commandRecord = this.getById(logId); + if (commandRecord == null) { throw new RuntimeException("命令记录不存在"); } - if (commandLog.getExecuteStatus() == null || commandLog.getExecuteStatus() != 1) { + if (commandRecord.getExecuteStatus() == null || commandRecord.getExecuteStatus() != 1) { throw new RuntimeException("只能撤销成功执行的命令"); } // TODO: 实现具体的回滚逻辑 - log.info("撤销命令执行: logId={}, function={}", logId, commandLog.getFunctionName()); + log.info("撤销命令执行: logId={}, function={}", logId, commandRecord.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 3be3ed68..c4b76980 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 @@ -10,8 +10,8 @@ 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.AiCommandLog; -import com.youlai.boot.platform.ai.service.AiCommandLogService; +import com.youlai.boot.platform.ai.model.entity.AiCommandRecord; +import com.youlai.boot.platform.ai.service.AiCommandRecordService; import com.youlai.boot.platform.ai.service.AiCommandService; import com.youlai.boot.platform.ai.tools.UserTools; import com.youlai.boot.security.util.SecurityUtils; @@ -50,7 +50,7 @@ public class AiCommandServiceImpl implements AiCommandService { 当无法识别命令时,success=false,并给出 error。 """; - private final AiCommandLogService logService; + private final AiCommandRecordService recordService; private final UserTools userTools; private final ChatClient chatClient; @@ -71,13 +71,13 @@ public class AiCommandServiceImpl implements AiCommandService { String username = SecurityUtils.getUsername(); String ipAddress = JakartaServletUtil.getClientIP(httpRequest); - AiCommandLog commandLog = new AiCommandLog(); - commandLog.setUserId(userId); - commandLog.setUsername(username); - commandLog.setOriginalCommand(command); - commandLog.setIpAddress(ipAddress); - commandLog.setAiProvider("spring-ai"); - commandLog.setAiModel("auto"); + AiCommandRecord commandRecord = new AiCommandRecord(); + commandRecord.setUserId(userId); + commandRecord.setUsername(username); + commandRecord.setOriginalCommand(command); + commandRecord.setIpAddress(ipAddress); + commandRecord.setAiProvider("spring-ai"); + commandRecord.setAiModel("auto"); String systemPrompt = buildSystemPrompt(); String userPrompt = buildUserPrompt(request); @@ -95,20 +95,20 @@ public class AiCommandServiceImpl implements AiCommandService { 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(), "解析失败")); + commandRecord.setAiProvider(StrUtil.emptyToDefault(parseResult.provider(), "spring-ai")); + commandRecord.setAiModel(StrUtil.emptyToDefault(parseResult.model(), "auto")); + commandRecord.setParseStatus(parseResult.success() ? 1 : 0); + commandRecord.setExplanation(parseResult.explanation()); + commandRecord.setFunctionCalls(JSONUtil.toJsonStr(parseResult.functionCalls())); + commandRecord.setConfidence(parseResult.confidence() != null ? BigDecimal.valueOf(parseResult.confidence()) : null); + commandRecord.setParseErrorMessage(parseResult.success() ? null : StrUtil.emptyToDefault(parseResult.error(), "解析失败")); long duration = System.currentTimeMillis() - startTime; - commandLog.setParseDurationMs((int) duration); + commandRecord.setParseDurationMs((int) duration); - logService.save(commandLog); + recordService.save(commandRecord); AiParseResponseDTO response = AiParseResponseDTO.builder() - .parseLogId(commandLog.getId()) + .parseLogId(commandRecord.getId()) .success(parseResult.success()) .functionCalls(parseResult.functionCalls()) .explanation(parseResult.explanation()) @@ -120,17 +120,17 @@ public class AiCommandServiceImpl implements AiCommandService { if (!parseResult.success()) { log.warn("❗️ AI 未能解析命令: {}", parseResult.error()); } else { - log.info("✅ 解析成功,审计记录ID: {}", commandLog.getId()); + log.info("✅ 解析成功,审计记录ID: {}", commandRecord.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); + commandRecord.setParseStatus(0); + commandRecord.setFunctionCalls(JSONUtil.toJsonStr(Collections.emptyList())); + commandRecord.setParseErrorMessage(e.getMessage()); + commandRecord.setParseDurationMs((int) duration); + recordService.save(commandRecord); log.error("❌ 解析命令失败: {}", e.getMessage(), e); throw new RuntimeException("解析命令失败: " + e.getMessage(), e); @@ -232,52 +232,52 @@ public class AiCommandServiceImpl implements AiCommandService { AiFunctionCallDTO functionCall = request.getFunctionCall(); // 根据解析日志ID获取审计记录,如果不存在则创建新记录 - AiCommandLog commandLog; + AiCommandRecord commandRecord ; if (StrUtil.isNotBlank(request.getParseLogId())) { // 更新已存在的审计记录(解析阶段已创建) - commandLog = logService.getById(request.getParseLogId()); - if (commandLog == null) { + commandRecord = recordService.getById(request.getParseLogId()); + if (commandRecord == null) { throw new IllegalStateException("未找到对应的解析记录,ID: " + request.getParseLogId()); } } else { // 如果没有解析日志ID,创建新记录(兼容直接执行的情况) - commandLog = new AiCommandLog(); - commandLog.setUserId(userId); - commandLog.setUsername(username); - commandLog.setOriginalCommand(request.getOriginalCommand()); - commandLog.setIpAddress(ipAddress); - logService.save(commandLog); + commandRecord = new AiCommandRecord(); + commandRecord.setUserId(userId); + commandRecord.setUsername(username); + commandRecord.setOriginalCommand(request.getOriginalCommand()); + commandRecord.setIpAddress(ipAddress); + recordService.save(commandRecord); } // 更新执行相关字段 - commandLog.setFunctionName(functionCall.getName()); - commandLog.setFunctionArguments(JSONUtil.toJsonStr(functionCall.getArguments())); - commandLog.setExecuteStatus(0); // 0-待执行 + commandRecord.setFunctionName(functionCall.getName()); + commandRecord.setFunctionArguments(JSONUtil.toJsonStr(functionCall.getArguments())); + commandRecord.setExecuteStatus(0); // 0-待执行 try { // 🎯 执行具体的函数调用 Object result = executeFunctionCall(functionCall); // 更新执行成功 - commandLog.setExecuteStatus(1); // 1-成功 - commandLog.setExecuteErrorMessage(null); + commandRecord.setExecuteStatus(1); // 1-成功 + commandRecord.setExecuteErrorMessage(null); // 更新审计记录 - logService.updateById(commandLog); + recordService.updateById(commandRecord); - log.info("✅ 命令执行成功,审计记录ID: {}", commandLog.getId()); + log.info("✅ 命令执行成功,审计记录ID: {}", commandRecord.getId()); return result; } catch (Exception e) { // 更新执行失败 - commandLog.setExecuteStatus(-1); // -1-失败 - commandLog.setExecuteErrorMessage(e.getMessage()); + commandRecord.setExecuteStatus(-1); // -1-失败 + commandRecord.setExecuteErrorMessage(e.getMessage()); // 更新审计记录 - logService.updateById(commandLog); + recordService.updateById(commandRecord); - log.error("❌ 命令执行失败,审计记录ID: {}", commandLog.getId(), e); + log.error("❌ 命令执行失败,审计记录ID: {}", commandRecord.getId(), e); // 抛出异常,由 Controller 统一处理 throw e; diff --git a/src/main/java/com/youlai/boot/platform/ai/tools/UserTools.java b/src/main/java/com/youlai/boot/platform/ai/tools/UserTools.java index 6cff249f..64d9e923 100644 --- a/src/main/java/com/youlai/boot/platform/ai/tools/UserTools.java +++ b/src/main/java/com/youlai/boot/platform/ai/tools/UserTools.java @@ -17,28 +17,28 @@ import org.springframework.ai.tool.annotation.ToolParam; @RequiredArgsConstructor public class UserTools { - private final UserService userService; + private final UserService userService; - @Tool(description = "根据关键字在用户列表中筛选用户") - public String queryUser( - @ToolParam(description = "搜索关键字,用于在列表中搜索筛选") String keywords - ) { - // 返回搜索关键字,前端会在用户列表页面进行筛选 - return "将在用户列表中搜索:" + keywords; - } + @Tool(description = "根据关键字在用户列表中筛选用户") + public String queryUser( + @ToolParam(description = "搜索关键字,用于在列表中搜索筛选") String keywords + ) { + // 返回搜索关键字,前端会在用户列表页面进行筛选 + return "将在用户列表中搜索:" + keywords; + } - @Tool(description = "根据用户名更新用户昵称") - public String updateUserNickname( - @ToolParam(description = "用户名") String username, - @ToolParam(description = "新的昵称") String nickname - ) { + @Tool(description = "根据用户名更新用户昵称") + public String updateUserNickname( + @ToolParam(description = "用户名") String username, + @ToolParam(description = "新的昵称") String nickname + ) { - boolean ok = userService.update(new LambdaUpdateWrapper() - .eq(User::getUsername, username) - .set(User::getNickname, nickname) - ); - return ok ? "用户昵称更新成功" : "用户昵称更新失败"; - } + boolean ok = userService.update(new LambdaUpdateWrapper() + .eq(User::getUsername, username) + .set(User::getNickname, nickname) + ); + return ok ? "用户昵称更新成功" : "用户昵称更新失败"; + } } diff --git a/src/main/java/com/youlai/boot/platform/codegen/controller/CodegenController.java b/src/main/java/com/youlai/boot/platform/codegen/controller/CodegenController.java index 497682f4..5485dced 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/controller/CodegenController.java +++ b/src/main/java/com/youlai/boot/platform/codegen/controller/CodegenController.java @@ -11,7 +11,7 @@ import com.youlai.boot.platform.codegen.model.query.TablePageQuery; import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVO; import com.youlai.boot.platform.codegen.model.vo.TablePageVO; import com.youlai.boot.common.annotation.Log; -import com.youlai.boot.platform.codegen.service.GenConfigService; +import com.youlai.boot.platform.codegen.service.GenTableService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -40,7 +40,7 @@ import java.util.List; public class CodegenController { private final CodegenService codegenService; - private final GenConfigService genConfigService; + private final GenTableService genTableService; private final CodegenProperties codegenProperties; @Operation(summary = "获取数据表分页列表") @@ -55,10 +55,10 @@ public class CodegenController { @Operation(summary = "获取代码生成配置") @GetMapping("/{tableName}/config") - public Result getGenConfigFormData( + public Result getGenTableFormData( @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName ) { - GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); + GenConfigForm formData = genTableService.getGenTableFormData(tableName); return Result.success(formData); } @@ -66,7 +66,7 @@ public class CodegenController { @PostMapping("/{tableName}/config") @Log(value = "生成代码", module = LogModuleEnum.OTHER) public Result saveGenConfig(@RequestBody GenConfigForm formData) { - genConfigService.saveGenConfig(formData); + genTableService.saveGenConfig(formData); return Result.success(); } @@ -75,7 +75,7 @@ public class CodegenController { public Result deleteGenConfig( @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName ) { - genConfigService.deleteGenConfig(tableName); + genTableService.deleteGenConfig(tableName); return Result.success(); } diff --git a/src/main/java/com/youlai/boot/platform/codegen/converter/CodegenConverter.java b/src/main/java/com/youlai/boot/platform/codegen/converter/CodegenConverter.java index 999d5fd1..4e9be4b9 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/converter/CodegenConverter.java +++ b/src/main/java/com/youlai/boot/platform/codegen/converter/CodegenConverter.java @@ -1,7 +1,7 @@ package com.youlai.boot.platform.codegen.converter; -import com.youlai.boot.platform.codegen.model.entity.GenConfig; -import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig; +import com.youlai.boot.platform.codegen.model.entity.GenTable; +import com.youlai.boot.platform.codegen.model.entity.GenTableColumn; import com.youlai.boot.platform.codegen.model.form.GenConfigForm; import org.mapstruct.Mapper; import org.mapstruct.Mapping; @@ -17,25 +17,25 @@ import java.util.List; @Mapper(componentModel = "spring") public interface CodegenConverter { - @Mapping(source = "genConfig.tableName", target = "tableName") - @Mapping(source = "genConfig.businessName", target = "businessName") - @Mapping(source = "genConfig.moduleName", target = "moduleName") - @Mapping(source = "genConfig.packageName", target = "packageName") - @Mapping(source = "genConfig.entityName", target = "entityName") - @Mapping(source = "genConfig.author", target = "author") - @Mapping(source = "genConfig.pageType", target = "pageType") - @Mapping(source = "genConfig.removeTablePrefix", target = "removeTablePrefix") + @Mapping(source = "genTable.tableName", target = "tableName") + @Mapping(source = "genTable.businessName", target = "businessName") + @Mapping(source = "genTable.moduleName", target = "moduleName") + @Mapping(source = "genTable.packageName", target = "packageName") + @Mapping(source = "genTable.entityName", target = "entityName") + @Mapping(source = "genTable.author", target = "author") + @Mapping(source = "genTable.pageType", target = "pageType") + @Mapping(source = "genTable.removeTablePrefix", target = "removeTablePrefix") @Mapping(source = "fieldConfigs", target = "fieldConfigs") - GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + GenConfigForm toGenConfigForm(GenTable genTable, List fieldConfigs); - List toGenFieldConfigForm(List fieldConfigs); + List toGenTableColumnForm(List fieldConfigs); - GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + GenConfigForm.FieldConfig toGenTableColumnForm(GenTableColumn genTableColumn); - GenConfig toGenConfig(GenConfigForm formData); + GenTable toGenTable(GenConfigForm formData); - List toGenFieldConfig(List fieldConfigs); + List toGenTableColumn(List fieldConfigs); - GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + GenTableColumn toGenTableColumn(GenConfigForm.FieldConfig fieldConfig); } \ No newline at end of file diff --git a/src/main/java/com/youlai/boot/platform/codegen/mapper/GenFieldConfigMapper.java b/src/main/java/com/youlai/boot/platform/codegen/mapper/GenTableColumnMapper.java similarity index 53% rename from src/main/java/com/youlai/boot/platform/codegen/mapper/GenFieldConfigMapper.java rename to src/main/java/com/youlai/boot/platform/codegen/mapper/GenTableColumnMapper.java index 14aaf2b1..de7728e1 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/mapper/GenFieldConfigMapper.java +++ b/src/main/java/com/youlai/boot/platform/codegen/mapper/GenTableColumnMapper.java @@ -1,17 +1,17 @@ package com.youlai.boot.platform.codegen.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig; +import com.youlai.boot.platform.codegen.model.entity.GenTableColumn; import org.apache.ibatis.annotations.Mapper; /** - * 代码生成字段配置访问层 + * 代码生成表字段配置访问层 * * @author Ray * @since 2.10.0 */ @Mapper -public interface GenFieldConfigMapper extends BaseMapper { +public interface GenTableColumnMapper extends BaseMapper { } diff --git a/src/main/java/com/youlai/boot/platform/codegen/mapper/GenConfigMapper.java b/src/main/java/com/youlai/boot/platform/codegen/mapper/GenTableMapper.java similarity index 55% rename from src/main/java/com/youlai/boot/platform/codegen/mapper/GenConfigMapper.java rename to src/main/java/com/youlai/boot/platform/codegen/mapper/GenTableMapper.java index ac3c79af..d7d2e750 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/mapper/GenConfigMapper.java +++ b/src/main/java/com/youlai/boot/platform/codegen/mapper/GenTableMapper.java @@ -1,17 +1,17 @@ package com.youlai.boot.platform.codegen.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.youlai.boot.platform.codegen.model.entity.GenConfig; +import com.youlai.boot.platform.codegen.model.entity.GenTable; import org.apache.ibatis.annotations.Mapper; /** - * 代码生成基础配置访问层 + * 代码生成表配置访问层 * * @author Ray * @since 2.10.0 */ @Mapper -public interface GenConfigMapper extends BaseMapper { +public interface GenTableMapper extends BaseMapper { } diff --git a/src/main/java/com/youlai/boot/platform/codegen/model/entity/GenConfig.java b/src/main/java/com/youlai/boot/platform/codegen/model/entity/GenTable.java similarity index 89% rename from src/main/java/com/youlai/boot/platform/codegen/model/entity/GenConfig.java rename to src/main/java/com/youlai/boot/platform/codegen/model/entity/GenTable.java index 13ad52f7..45d30606 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/model/entity/GenConfig.java +++ b/src/main/java/com/youlai/boot/platform/codegen/model/entity/GenTable.java @@ -7,15 +7,15 @@ import lombok.Getter; import lombok.Setter; /** - * 代码生成基础配置 + * 代码生成表配置 * * @author Ray * @since 2.10.0 */ -@TableName(value = "gen_config") +@TableName(value = "gen_table") @Getter @Setter -public class GenConfig extends BaseEntity { +public class GenTable extends BaseEntity { /** * 表名 @@ -61,4 +61,5 @@ public class GenConfig extends BaseEntity { * 要移除的表前缀,如: sys_ */ private String removeTablePrefix; -} \ No newline at end of file +} + diff --git a/src/main/java/com/youlai/boot/platform/codegen/model/entity/GenFieldConfig.java b/src/main/java/com/youlai/boot/platform/codegen/model/entity/GenTableColumn.java similarity index 89% rename from src/main/java/com/youlai/boot/platform/codegen/model/entity/GenFieldConfig.java rename to src/main/java/com/youlai/boot/platform/codegen/model/entity/GenTableColumn.java index 7c9bb91f..837bbc50 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/model/entity/GenFieldConfig.java +++ b/src/main/java/com/youlai/boot/platform/codegen/model/entity/GenTableColumn.java @@ -11,21 +11,21 @@ import lombok.Getter; import lombok.Setter; /** - * 字段生成配置实体 + * 代码生成表字段配置实体 * * @author Ray * @since 2.10.0 */ -@TableName(value = "gen_field_config") +@TableName(value = "gen_table_column") @Getter @Setter -public class GenFieldConfig extends BaseEntity { +public class GenTableColumn extends BaseEntity { /** - * 关联的配置ID + * 关联的表配置ID */ - private Long configId; + private Long tableId; /** * 列名 @@ -104,3 +104,4 @@ public class GenFieldConfig extends BaseEntity { */ private String dictType; } + diff --git a/src/main/java/com/youlai/boot/platform/codegen/service/GenFieldConfigService.java b/src/main/java/com/youlai/boot/platform/codegen/service/GenTableColumnService.java similarity index 56% rename from src/main/java/com/youlai/boot/platform/codegen/service/GenFieldConfigService.java rename to src/main/java/com/youlai/boot/platform/codegen/service/GenTableColumnService.java index fbfda4d4..11e9ad61 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/service/GenFieldConfigService.java +++ b/src/main/java/com/youlai/boot/platform/codegen/service/GenTableColumnService.java @@ -1,7 +1,7 @@ package com.youlai.boot.platform.codegen.service; import com.baomidou.mybatisplus.extension.service.IService; -import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig; +import com.youlai.boot.platform.codegen.model.entity.GenTableColumn; /** * 代码生成配置接口 @@ -9,6 +9,6 @@ import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig; * @author Ray * @since 2.10.0 */ -public interface GenFieldConfigService extends IService { +public interface GenTableColumnService extends IService { } diff --git a/src/main/java/com/youlai/boot/platform/codegen/service/GenConfigService.java b/src/main/java/com/youlai/boot/platform/codegen/service/GenTableService.java similarity index 77% rename from src/main/java/com/youlai/boot/platform/codegen/service/GenConfigService.java rename to src/main/java/com/youlai/boot/platform/codegen/service/GenTableService.java index 2731260e..135c0c3e 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/service/GenConfigService.java +++ b/src/main/java/com/youlai/boot/platform/codegen/service/GenTableService.java @@ -1,7 +1,7 @@ package com.youlai.boot.platform.codegen.service; import com.baomidou.mybatisplus.extension.service.IService; -import com.youlai.boot.platform.codegen.model.entity.GenConfig; +import com.youlai.boot.platform.codegen.model.entity.GenTable; import com.youlai.boot.platform.codegen.model.form.GenConfigForm; /** @@ -10,7 +10,7 @@ import com.youlai.boot.platform.codegen.model.form.GenConfigForm; * @author Ray * @since 2.10.0 */ -public interface GenConfigService extends IService { +public interface GenTableService extends IService { /** * 获取代码生成配置 @@ -18,7 +18,7 @@ public interface GenConfigService extends IService { * @param tableName 表名 * @return */ - GenConfigForm getGenConfigFormData(String tableName); + GenConfigForm getGenTableFormData(String tableName); /** * 保存代码生成配置 diff --git a/src/main/java/com/youlai/boot/platform/codegen/service/impl/CodegenServiceImpl.java b/src/main/java/com/youlai/boot/platform/codegen/service/impl/CodegenServiceImpl.java index aa4e507b..8e33a2df 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/service/impl/CodegenServiceImpl.java +++ b/src/main/java/com/youlai/boot/platform/codegen/service/impl/CodegenServiceImpl.java @@ -13,13 +13,13 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.youlai.boot.platform.codegen.enums.JavaTypeEnum; import com.youlai.boot.config.property.CodegenProperties; -import com.youlai.boot.platform.codegen.service.GenConfigService; -import com.youlai.boot.platform.codegen.service.GenFieldConfigService; +import com.youlai.boot.platform.codegen.service.GenTableService; +import com.youlai.boot.platform.codegen.service.GenTableColumnService; import com.youlai.boot.platform.codegen.service.CodegenService; import com.youlai.boot.core.exception.BusinessException; import com.youlai.boot.platform.codegen.mapper.DatabaseMapper; -import com.youlai.boot.platform.codegen.model.entity.GenConfig; -import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig; +import com.youlai.boot.platform.codegen.model.entity.GenTable; +import com.youlai.boot.platform.codegen.model.entity.GenTableColumn; import com.youlai.boot.platform.codegen.model.query.TablePageQuery; import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVO; import com.youlai.boot.platform.codegen.model.vo.TablePageVO; @@ -48,8 +48,8 @@ public class CodegenServiceImpl implements CodegenService { private final DatabaseMapper databaseMapper; private final CodegenProperties codegenProperties; - private final GenConfigService genConfigService; - private final GenFieldConfigService genFieldConfigService; + private final GenTableService genTableService; + private final GenTableColumnService genTableColumnService; /** * 数据表分页列表 @@ -77,16 +77,16 @@ public class CodegenServiceImpl implements CodegenService { List list = new ArrayList<>(); - GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() - .eq(GenConfig::getTableName, tableName) + GenTable genTable = genTableService.getOne(new LambdaQueryWrapper() + .eq(GenTable::getTableName, tableName) ); - if (genConfig == null) { + if (genTable == null) { throw new BusinessException("未找到表生成配置"); } - List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() - .eq(GenFieldConfig::getConfigId, genConfig.getId()) - .orderByAsc(GenFieldConfig::getFieldSort) + List fieldConfigs = genTableColumnService.list(new LambdaQueryWrapper() + .eq(GenTableColumn::getTableId, genTable.getId()) + .orderByAsc(GenTableColumn::getFieldSort) ); if (CollectionUtil.isEmpty(fieldConfigs)) { @@ -102,7 +102,7 @@ public class CodegenServiceImpl implements CodegenService { /* 1. 生成文件名 UserController */ // User Role Menu Dept - String entityName = genConfig.getEntityName(); + String entityName = genTable.getEntityName(); // Controller Service Mapper Entity String templateName = templateConfigEntry.getKey(); // .java .ts .vue @@ -114,9 +114,9 @@ public class CodegenServiceImpl implements CodegenService { /* 2. 生成文件路径 */ // 包名:com.youlai.boot - String packageName = genConfig.getPackageName(); + String packageName = genTable.getPackageName(); // 模块名:system - String moduleName = genConfig.getModuleName(); + String moduleName = genTable.getModuleName(); // 子包名:controller String subpackageName = templateConfig.getSubpackageName(); // 组合成文件路径:src/main/java/com/youlai/boot/system/controller @@ -126,8 +126,8 @@ public class CodegenServiceImpl implements CodegenService { /* 3. 生成文件内容 */ // 将模板文件中的变量替换为具体的值 生成代码内容 // 优先使用保存的 ui,没有则使用请求参数 - String finalType = StrUtil.blankToDefault(genConfig.getPageType(), pageType); - String content = getCodeContent(templateConfig, genConfig, fieldConfigs, finalType); + String finalType = StrUtil.blankToDefault(genTable.getPageType(), pageType); + String content = getCodeContent(templateConfig, genTable, fieldConfigs, finalType); previewVO.setContent(content); list.add(previewVO); @@ -215,29 +215,29 @@ public class CodegenServiceImpl implements CodegenService { * @param fieldConfigs 字段配置 * @return 代码内容 */ - private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs, String pageType) { + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenTable genTable, List fieldConfigs, String pageType) { Map bindMap = new HashMap<>(); - String entityName = genConfig.getEntityName(); + String entityName = genTable.getEntityName(); - bindMap.put("packageName", genConfig.getPackageName()); - bindMap.put("moduleName", genConfig.getModuleName()); + bindMap.put("packageName", genTable.getPackageName()); + bindMap.put("moduleName", genTable.getModuleName()); bindMap.put("subpackageName", templateConfig.getSubpackageName()); bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); bindMap.put("entityName", entityName); - bindMap.put("tableName", genConfig.getTableName()); - bindMap.put("author", genConfig.getAuthor()); + bindMap.put("tableName", genTable.getTableName()); + bindMap.put("author", genTable.getAuthor()); bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-test - bindMap.put("businessName", genConfig.getBusinessName()); + bindMap.put("businessName", genTable.getBusinessName()); bindMap.put("fieldConfigs", fieldConfigs); boolean hasLocalDateTime = false; boolean hasBigDecimal = false; boolean hasRequiredField = false; - for (GenFieldConfig fieldConfig : fieldConfigs) { + for (GenTableColumn fieldConfig : fieldConfigs) { if ("LocalDateTime".equals(fieldConfig.getFieldType())) { hasLocalDateTime = true; diff --git a/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenFieldConfigServiceImpl.java b/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenFieldConfigServiceImpl.java deleted file mode 100644 index e6dd938b..00000000 --- a/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenFieldConfigServiceImpl.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.youlai.boot.platform.codegen.service.impl; - -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.youlai.boot.platform.codegen.mapper.GenFieldConfigMapper; -import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig; -import com.youlai.boot.platform.codegen.service.GenFieldConfigService; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -/** - * 代码生成字段配置服务实现类 - * - * @author Ray - * @since 2.10.0 - */ -@Service -@RequiredArgsConstructor -public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { - - -} diff --git a/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenTableColumnServiceImpl.java b/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenTableColumnServiceImpl.java new file mode 100644 index 00000000..d74c0e57 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenTableColumnServiceImpl.java @@ -0,0 +1,21 @@ +package com.youlai.boot.platform.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.youlai.boot.platform.codegen.mapper.GenTableColumnMapper; +import com.youlai.boot.platform.codegen.model.entity.GenTableColumn; +import com.youlai.boot.platform.codegen.service.GenTableColumnService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 代码生成字段配置服务实现类 + * + * @author Ray.Hao + * @since 2.10.0 + */ +@Service +@RequiredArgsConstructor +public class GenTableColumnServiceImpl extends ServiceImpl implements GenTableColumnService { + + +} diff --git a/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenConfigServiceImpl.java b/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenTableServiceImpl.java similarity index 66% rename from src/main/java/com/youlai/boot/platform/codegen/service/impl/GenConfigServiceImpl.java rename to src/main/java/com/youlai/boot/platform/codegen/service/impl/GenTableServiceImpl.java index eaf59a40..b483652f 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenConfigServiceImpl.java +++ b/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenTableServiceImpl.java @@ -14,14 +14,14 @@ import com.youlai.boot.core.exception.BusinessException; import com.youlai.boot.config.property.CodegenProperties; import com.youlai.boot.platform.codegen.converter.CodegenConverter; import com.youlai.boot.platform.codegen.mapper.DatabaseMapper; -import com.youlai.boot.platform.codegen.mapper.GenConfigMapper; +import com.youlai.boot.platform.codegen.mapper.GenTableMapper; import com.youlai.boot.platform.codegen.model.bo.ColumnMetaData; import com.youlai.boot.platform.codegen.model.bo.TableMetaData; -import com.youlai.boot.platform.codegen.model.entity.GenConfig; -import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig; +import com.youlai.boot.platform.codegen.model.entity.GenTable; +import com.youlai.boot.platform.codegen.model.entity.GenTableColumn; import com.youlai.boot.platform.codegen.model.form.GenConfigForm; -import com.youlai.boot.platform.codegen.service.GenConfigService; -import com.youlai.boot.platform.codegen.service.GenFieldConfigService; +import com.youlai.boot.platform.codegen.service.GenTableService; +import com.youlai.boot.platform.codegen.service.GenTableColumnService; import com.youlai.boot.system.service.MenuService; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; @@ -40,11 +40,11 @@ import java.util.Objects; */ @Service @RequiredArgsConstructor -public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { +public class GenTableServiceImpl extends ServiceImpl implements GenTableService { private final DatabaseMapper databaseMapper; private final CodegenProperties codegenProperties; - private final GenFieldConfigService genFieldConfigService; + private final GenTableColumnService genTableColumnService; private final CodegenConverter codegenConverter; @Value("${spring.profiles.active}") @@ -59,64 +59,64 @@ public class GenConfigServiceImpl extends ServiceImpl(GenConfig.class) - .eq(GenConfig::getTableName, tableName) + GenTable genTable = this.getOne( + new LambdaQueryWrapper<>(GenTable.class) + .eq(GenTable::getTableName, tableName) .last("LIMIT 1") ); // 是否有代码生成配置 - boolean hasGenConfig = genConfig != null; + boolean hasGenTable = genTable != null; // 如果没有代码生成配置,则根据表的元数据生成默认配置 - if (genConfig == null) { + if (genTable == null) { TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); Assert.isTrue(tableMetadata != null, "未找到表元数据"); - genConfig = new GenConfig(); - genConfig.setTableName(tableName); + genTable = new GenTable(); + genTable.setTableName(tableName); // 表注释作为业务名称,去掉表字 例如:用户表 -> 用户 String tableComment = tableMetadata.getTableComment(); if (StrUtil.isNotBlank(tableComment)) { - genConfig.setBusinessName(tableComment.replace("表", "").trim()); + genTable.setBusinessName(tableComment.replace("表", "").trim()); } // 根据表名生成实体类名,支持去除前缀 例如:sys_user -> SysUser - String removePrefix = genConfig.getRemoveTablePrefix(); + String removePrefix = genTable.getRemoveTablePrefix(); String processedTable = tableName; if (StrUtil.isNotBlank(removePrefix) && StrUtil.startWith(tableName, removePrefix)) { processedTable = StrUtil.removePrefix(tableName, removePrefix); } - genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(processedTable)))); + genTable.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(processedTable)))); - genConfig.setPackageName(YouLaiBootApplication.class.getPackageName()); - genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 - genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); + genTable.setPackageName(YouLaiBootApplication.class.getPackageName()); + genTable.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 + genTable.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); } // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 - List genFieldConfigs = new ArrayList<>(); + List genTableColumns = new ArrayList<>(); // 获取表的列 List tableColumns = databaseMapper.getTableColumns(tableName); if (CollectionUtil.isNotEmpty(tableColumns)) { // 查询字段生成配置 - List fieldConfigList = genFieldConfigService.list( - new LambdaQueryWrapper() - .eq(GenFieldConfig::getConfigId, genConfig.getId()) - .orderByAsc(GenFieldConfig::getFieldSort) + List fieldConfigList = genTableColumnService.list( + new LambdaQueryWrapper() + .eq(GenTableColumn::getTableId, genTable.getId()) + .orderByAsc(GenTableColumn::getFieldSort) ); Integer maxSort = fieldConfigList.stream() - .map(GenFieldConfig::getFieldSort) + .map(GenTableColumn::getFieldSort) .filter(Objects::nonNull) // 过滤掉空值 .max(Integer::compareTo) .orElse(0); for (ColumnMetaData tableColumn : tableColumns) { // 根据列名获取字段生成配置 String columnName = tableColumn.getColumnName(); - GenFieldConfig fieldConfig = fieldConfigList.stream() + GenTableColumn fieldConfig = fieldConfigList.stream() .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) .findFirst() .orElseGet(() -> createDefaultFieldConfig(tableColumn)); @@ -130,16 +130,16 @@ public class GenConfigServiceImpl extends ServiceImpl genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); + List genTableColumns = codegenConverter.toGenTableColumn(formData.getFieldConfigs()); - if (CollectionUtil.isEmpty(genFieldConfigs)) { + if (CollectionUtil.isEmpty(genTableColumns)) { throw new BusinessException("字段配置不能为空"); } - genFieldConfigs.forEach(genFieldConfig -> { - genFieldConfig.setConfigId(genConfig.getId()); + genTableColumns.forEach(genTableColumn -> { + genTableColumn.setTableId(genTable.getId()); }); - genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); + genTableColumnService.saveOrUpdateBatch(genTableColumns); } /** @@ -208,15 +208,15 @@ public class GenConfigServiceImpl extends ServiceImpl() - .eq(GenConfig::getTableName, tableName)); + GenTable genTable = this.getOne(new LambdaQueryWrapper() + .eq(GenTable::getTableName, tableName)); - boolean result = this.remove(new LambdaQueryWrapper() - .eq(GenConfig::getTableName, tableName) + boolean result = this.remove(new LambdaQueryWrapper() + .eq(GenTable::getTableName, tableName) ); if (result) { - genFieldConfigService.remove(new LambdaQueryWrapper() - .eq(GenFieldConfig::getConfigId, genConfig.getId()) + genTableColumnService.remove(new LambdaQueryWrapper() + .eq(GenTableColumn::getTableId, genTable.getId()) ); } } diff --git a/src/main/java/com/youlai/boot/plugin/mybatis/MyMetaObjectHandler.java b/src/main/java/com/youlai/boot/plugin/mybatis/MyMetaObjectHandler.java index 23852ae0..fec93bc9 100644 --- a/src/main/java/com/youlai/boot/plugin/mybatis/MyMetaObjectHandler.java +++ b/src/main/java/com/youlai/boot/plugin/mybatis/MyMetaObjectHandler.java @@ -16,7 +16,7 @@ import java.time.LocalDateTime; * 支持自动填充创建时间、更新时间和租户ID *

* - * @author haoxr + * @author Ray.Hao * @since 2022/10/14 */ @Component @@ -50,7 +50,8 @@ public class MyMetaObjectHandler implements MetaObjectHandler { if (tenantId != null) { // 使用 strictInsertFill 自动填充租户ID // 注意:由于 TenantDynamicFieldConfig 已将 exist 设置为 true,这里可以正常填充 - this.strictInsertFill(metaObject, "tenantId", () -> tenantId, Long.class); + Long finalTenantId = tenantId; + this.strictInsertFill(metaObject, "tenantId", () -> finalTenantId, Long.class); } } } diff --git a/src/main/java/com/youlai/boot/plugin/mybatis/TenantLineHandler.java b/src/main/java/com/youlai/boot/plugin/mybatis/MyTenantLineHandler.java similarity index 95% rename from src/main/java/com/youlai/boot/plugin/mybatis/TenantLineHandler.java rename to src/main/java/com/youlai/boot/plugin/mybatis/MyTenantLineHandler.java index 1b33ba0c..5e63731f 100644 --- a/src/main/java/com/youlai/boot/plugin/mybatis/TenantLineHandler.java +++ b/src/main/java/com/youlai/boot/plugin/mybatis/MyTenantLineHandler.java @@ -25,7 +25,7 @@ import java.util.List; @Component @RequiredArgsConstructor @ConditionalOnProperty(prefix = "youlai.tenant", name = "enabled", havingValue = "true", matchIfMissing = false) -public class TenantLineHandler implements com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler { +public class MyTenantLineHandler implements TenantLineHandler { private final TenantProperties tenantProperties; diff --git a/src/main/java/com/youlai/boot/security/filter/CaptchaValidationFilter.java b/src/main/java/com/youlai/boot/security/filter/CaptchaValidationFilter.java index f0a08976..b4169424 100644 --- a/src/main/java/com/youlai/boot/security/filter/CaptchaValidationFilter.java +++ b/src/main/java/com/youlai/boot/security/filter/CaptchaValidationFilter.java @@ -2,38 +2,45 @@ package com.youlai.boot.security.filter; import cn.hutool.captcha.generator.CodeGenerator; import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.SecurityConstants; import com.youlai.boot.core.web.ResultCode; import com.youlai.boot.core.web.WebResponseHelper; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; import jakarta.servlet.http.HttpServletResponse; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.StreamUtils; import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; import java.io.IOException; - +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; /** * 图形验证码校验过滤器 - * - * @author haoxr - * @since 2022/10/1 */ public class CaptchaValidationFilter extends OncePerRequestFilter { - private static final RequestMatcher LOGIN_PATH_REQUEST_MATCHER = PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST,SecurityConstants.LOGIN_PATH); + private static final RequestMatcher LOGIN_PATH_REQUEST_MATCHER = PathPatternRequestMatcher.withDefaults() + .matcher(HttpMethod.POST, SecurityConstants.LOGIN_PATH); public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; - public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; + public static final String CAPTCHA_ID_PARAM_NAME = "captchaId"; private final RedisTemplate redisTemplate; - private final CodeGenerator codeGenerator; public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) { @@ -41,37 +48,111 @@ public class CaptchaValidationFilter extends OncePerRequestFilter { this.codeGenerator = codeGenerator; } - @Override - public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { - // 检验登录接口的验证码 - if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) { - // 请求中的验证码 - String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME); - // TODO 兼容没有验证码的版本(线上请移除这个判断) - if (StrUtil.isBlank(captchaCode)) { - chain.doFilter(request, response); - return; - } - // 缓存中的验证码 - String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); - String cacheVerifyCode = (String) redisTemplate.opsForValue().get( - StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey) - ); - if (cacheVerifyCode == null) { - WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); - } else { - // 验证码比对 - if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { - chain.doFilter(request, response); - } else { - WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR); - } - } - } else { - // 非登录接口放行 + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + + // 非登录接口直接放行 + if (!LOGIN_PATH_REQUEST_MATCHER.matches(request)) { chain.doFilter(request, response); + return; + } + + // 仅支持 JSON 登录 + String contentType = request.getContentType(); + if (contentType == null || !contentType.contains(MediaType.APPLICATION_JSON_VALUE)) { + WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + return; + } + + // 包装请求,确保下游还能读取 body + ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request); + + byte[] bodyBytes = StreamUtils.copyToByteArray(requestWrapper.getInputStream()); + String body = new String(bodyBytes, StandardCharsets.UTF_8); + String captchaCode = null; + String captchaId = null; + + if (StrUtil.isNotBlank(body)) { + JSONObject jsonObject = JSONUtil.parseObj(body); + captchaCode = jsonObject.getStr(CAPTCHA_CODE_PARAM_NAME); + captchaId = jsonObject.getStr(CAPTCHA_ID_PARAM_NAME); + } + + if (StrUtil.isBlank(captchaCode) || StrUtil.isBlank(captchaId)) { + WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + return; + } + + String cacheVerifyCode = (String) redisTemplate.opsForValue().get( + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaId) + ); + if (cacheVerifyCode == null) { + WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); + return; + } + + if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { + HttpServletRequest repeatableRequest = new RepeatableReadRequestWrapper(requestWrapper, bodyBytes); + chain.doFilter(repeatableRequest, response); + } else { + WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR); } } + /** + * Simple wrapper to allow repeated reads of the request body after we've parsed it here. + */ + private static class RepeatableReadRequestWrapper extends HttpServletRequestWrapper { + + private final byte[] cachedBody; + + RepeatableReadRequestWrapper(HttpServletRequest request, byte[] cachedBody) { + super(request); + this.cachedBody = cachedBody != null ? cachedBody : new byte[0]; + } + + @Override + public ServletInputStream getInputStream() { + ByteArrayInputStream bais = new ByteArrayInputStream(cachedBody); + return new ServletInputStream() { + @Override + public int read() { + return bais.read(); + } + + @Override + public boolean isFinished() { + return bais.available() == 0; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(jakarta.servlet.ReadListener readListener) { + // no-op + } + }; + } + + @Override + public BufferedReader getReader() { + return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8)); + } + + @Override + public int getContentLength() { + return cachedBody.length; + } + + @Override + public long getContentLengthLong() { + return cachedBody.length; + } + } } + + diff --git a/src/main/java/com/youlai/boot/security/model/OnlineUser.java b/src/main/java/com/youlai/boot/security/model/OnlineUser.java index 6dd72608..50d9ac7b 100644 --- a/src/main/java/com/youlai/boot/security/model/OnlineUser.java +++ b/src/main/java/com/youlai/boot/security/model/OnlineUser.java @@ -38,6 +38,11 @@ public class OnlineUser { */ private Integer dataScope; + /** + * 租户ID + */ + private Long tenantId; + /** * 角色权限集合 */ diff --git a/src/main/java/com/youlai/boot/security/model/SysUserDetails.java b/src/main/java/com/youlai/boot/security/model/SysUserDetails.java index 8a79831c..1112fcb3 100644 --- a/src/main/java/com/youlai/boot/security/model/SysUserDetails.java +++ b/src/main/java/com/youlai/boot/security/model/SysUserDetails.java @@ -56,6 +56,11 @@ public class SysUserDetails implements UserDetails { */ private Integer dataScope; + /** + * 租户ID + */ + private Long tenantId; + /** * 用户角色权限集合 */ @@ -73,6 +78,7 @@ public class SysUserDetails implements UserDetails { this.enabled = ObjectUtil.equal(user.getStatus(), 1); this.deptId = user.getDeptId(); this.dataScope = user.getDataScope(); + this.tenantId = user.getTenantId(); // 初始化角色权限集合 this.authorities = CollectionUtil.isNotEmpty(user.getRoles()) diff --git a/src/main/java/com/youlai/boot/security/model/UserAuthCredentials.java b/src/main/java/com/youlai/boot/security/model/UserAuthCredentials.java index e68d119d..df292fde 100644 --- a/src/main/java/com/youlai/boot/security/model/UserAuthCredentials.java +++ b/src/main/java/com/youlai/boot/security/model/UserAuthCredentials.java @@ -54,4 +54,9 @@ public class UserAuthCredentials { */ private Integer dataScope; + /** + * 租户ID(从登录上下文中获取) + */ + private Long tenantId; + } diff --git a/src/main/java/com/youlai/boot/security/service/PermissionService.java b/src/main/java/com/youlai/boot/security/service/PermissionService.java index 8b11ad4e..0ebd87e3 100644 --- a/src/main/java/com/youlai/boot/security/service/PermissionService.java +++ b/src/main/java/com/youlai/boot/security/service/PermissionService.java @@ -15,8 +15,8 @@ import java.util.*; /** * SpringSecurity 权限校验 * - * @author haoxr - * @since 2022/2/22 + * @author Ray.Hao + * @since 0.0.1 */ @Component("ss") @RequiredArgsConstructor diff --git a/src/main/java/com/youlai/boot/security/service/SysUserDetailsService.java b/src/main/java/com/youlai/boot/security/service/SysUserDetailsService.java index 213b698f..40f9be1d 100644 --- a/src/main/java/com/youlai/boot/security/service/SysUserDetailsService.java +++ b/src/main/java/com/youlai/boot/security/service/SysUserDetailsService.java @@ -1,5 +1,6 @@ package com.youlai.boot.security.service; +import com.youlai.boot.common.tenant.TenantContextHolder; import com.youlai.boot.security.model.SysUserDetails; import com.youlai.boot.security.model.UserAuthCredentials; import com.youlai.boot.system.service.UserService; @@ -37,6 +38,8 @@ public class SysUserDetailsService implements UserDetailsService { if (userAuthCredentials == null) { throw new UsernameNotFoundException(username); } + // 将当前上下文中的租户ID写入认证凭证,便于后续 Token 携带租户信息 + userAuthCredentials.setTenantId(TenantContextHolder.getTenantId()); return new SysUserDetails(userAuthCredentials); } catch (Exception e) { // 记录异常日志 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 5f3f5397..715801c7 100644 --- a/src/main/java/com/youlai/boot/security/token/JwtTokenManager.java +++ b/src/main/java/com/youlai/boot/security/token/JwtTokenManager.java @@ -91,6 +91,7 @@ public class JwtTokenManager implements TokenManager { userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围 + userDetails.setTenantId(payloads.getLong(JwtClaimConstants.TENANT_ID)); // 租户ID userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名 // 角色集合 @@ -275,6 +276,7 @@ public class JwtTokenManager implements TokenManager { payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围 + payload.put(JwtClaimConstants.TENANT_ID, userDetails.getTenantId()); // 租户ID // claims 中添加角色信息 Set roles = authentication.getAuthorities().stream() 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 9c899156..bd9dbaa0 100644 --- a/src/main/java/com/youlai/boot/security/token/RedisTokenManager.java +++ b/src/main/java/com/youlai/boot/security/token/RedisTokenManager.java @@ -61,6 +61,7 @@ public class RedisTokenManager implements TokenManager { user.getUsername(), user.getDeptId(), user.getDataScope(), + user.getTenantId(), user.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toSet()) @@ -268,6 +269,7 @@ public class RedisTokenManager implements TokenManager { userDetails.setUsername(onlineUser.getUsername()); userDetails.setDeptId(onlineUser.getDeptId()); userDetails.setDataScope(onlineUser.getDataScope()); + userDetails.setTenantId(onlineUser.getTenantId()); userDetails.setAuthorities(authorities); return userDetails; } diff --git a/src/main/java/com/youlai/boot/system/controller/TenantController.java b/src/main/java/com/youlai/boot/system/controller/TenantController.java index 723c6b86..f7cdf190 100644 --- a/src/main/java/com/youlai/boot/system/controller/TenantController.java +++ b/src/main/java/com/youlai/boot/system/controller/TenantController.java @@ -27,7 +27,7 @@ import java.util.List; */ @Tag(name = "租户管理接口") @RestController -@RequestMapping("/api/v1/tenant") +@RequestMapping("/api/v1/tenants") @RequiredArgsConstructor @Slf4j @ConditionalOnProperty(prefix = "youlai.tenant", name = "enabled", havingValue = "true", matchIfMissing = false) @@ -44,7 +44,7 @@ public class TenantController { * @return 租户列表 */ @Operation(summary = "获取当前用户的租户列表") - @GetMapping("/list") + @GetMapping public Result> getTenantList() { Long userId = SecurityUtils.getUserId(); List tenantList = tenantService.getTenantListByUserId(userId); @@ -72,14 +72,13 @@ public class TenantController { * 切换租户 *

* 切换当前用户的租户上下文,需要验证用户是否有权限访问该租户 - * 并记录审计日志 *

* * @param tenantId 目标租户ID * @return 切换结果 */ @Operation(summary = "切换租户") - @PostMapping("/switch/{tenantId}") + @PostMapping("/{tenantId}/switch") public Result switchTenant( @Parameter(description = "租户ID") @PathVariable Long tenantId, HttpServletRequest request @@ -89,41 +88,30 @@ public class TenantController { log.info("用户 {} 请求切换租户:{} -> {}", userId, fromTenantId, tenantId); - try { - // 验证用户是否有权限访问该租户 - boolean hasPermission = tenantService.hasTenantPermission(userId, tenantId); - if (!hasPermission) { - log.warn("用户 {} 无权限访问租户 {}", userId, tenantId); - // 记录失败日志 - tenantService.recordTenantSwitch(userId, fromTenantId, tenantId, false, "无权限访问该租户", request); - return Result.failed("无权限访问该租户"); - } - - // 验证租户是否存在且正常 - TenantVO tenant = tenantService.getTenantById(tenantId); - if (tenant == null) { - tenantService.recordTenantSwitch(userId, fromTenantId, tenantId, false, "租户不存在", request); - return Result.failed("租户不存在"); - } - if (tenant.getStatus() == null || tenant.getStatus() != 1) { - tenantService.recordTenantSwitch(userId, fromTenantId, tenantId, false, "租户已禁用", request); - return Result.failed("租户已禁用"); - } - - // 设置新的租户上下文 - TenantContextHolder.setTenantId(tenantId); - - // 记录成功日志 - tenantService.recordTenantSwitch(userId, fromTenantId, tenantId, true, null, request); - - log.info("用户 {} 成功切换租户到 {}", userId, tenantId); - - return Result.success(tenant); - } catch (Exception e) { - log.error("用户 {} 切换租户失败", userId, e); - tenantService.recordTenantSwitch(userId, fromTenantId, tenantId, false, e.getMessage(), request); - return Result.failed("切换租户失败:" + e.getMessage()); + // 验证用户是否有权限访问该租户 + boolean hasPermission = tenantService.hasTenantPermission(userId, tenantId); + if (!hasPermission) { + log.warn("用户 {} 无权限访问租户 {}", userId, tenantId); + return Result.failed("无权限访问该租户"); } + + // 验证租户是否存在且正常 + TenantVO tenant = tenantService.getTenantById(tenantId); + if (tenant == null) { + log.warn("用户 {} 尝试切换到不存在的租户 {}", userId, tenantId); + return Result.failed("租户不存在"); + } + if (tenant.getStatus() == null || tenant.getStatus() != 1) { + log.warn("用户 {} 尝试切换到已禁用的租户 {}", userId, tenantId); + return Result.failed("租户已禁用"); + } + + // 设置新的租户上下文 + TenantContextHolder.setTenantId(tenantId); + + log.info("用户 {} 成功切换租户:{} -> {}", userId, fromTenantId, tenantId); + + return Result.success(tenant); } } diff --git a/src/main/java/com/youlai/boot/system/mapper/TenantSwitchLogMapper.java b/src/main/java/com/youlai/boot/system/mapper/TenantSwitchLogMapper.java deleted file mode 100644 index 62cd36ec..00000000 --- a/src/main/java/com/youlai/boot/system/mapper/TenantSwitchLogMapper.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.youlai.boot.system.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.youlai.boot.system.model.entity.TenantSwitchLog; -import org.apache.ibatis.annotations.Mapper; - -/** - * 租户切换审计日志 Mapper - * - * @author Ray.Hao - * @since 3.0.0 - */ -@Mapper -public interface TenantSwitchLogMapper 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 deleted file mode 100644 index 9be70dc6..00000000 --- a/src/main/java/com/youlai/boot/system/mapper/UserTenantMapper.java +++ /dev/null @@ -1,16 +0,0 @@ -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/TenantSwitchLog.java b/src/main/java/com/youlai/boot/system/model/entity/TenantSwitchLog.java deleted file mode 100644 index d455d1e1..00000000 --- a/src/main/java/com/youlai/boot/system/model/entity/TenantSwitchLog.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.youlai.boot.system.model.entity; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -import java.time.LocalDateTime; - -/** - * 租户切换审计日志实体 - * - * @author Ray.Hao - * @since 3.0.0 - */ -@Data -@TableName("sys_tenant_switch_log") -public class TenantSwitchLog { - - /** - * 主键ID - */ - @TableId(type = IdType.AUTO) - private Long id; - - /** - * 用户ID - */ - private Long userId; - - /** - * 用户名 - */ - private String username; - - /** - * 原租户ID - */ - private Long fromTenantId; - - /** - * 原租户名称 - */ - private String fromTenantName; - - /** - * 目标租户ID - */ - private Long toTenantId; - - /** - * 目标租户名称 - */ - private String toTenantName; - - /** - * 切换时间 - */ - private LocalDateTime switchTime; - - /** - * IP地址 - */ - private String ipAddress; - - /** - * 浏览器信息 - */ - private String userAgent; - - /** - * 切换状态(1-成功 0-失败) - */ - private Integer status; - - /** - * 失败原因 - */ - private String failReason; -} 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 deleted file mode 100644 index 46becb9d..00000000 --- a/src/main/java/com/youlai/boot/system/model/entity/UserTenant.java +++ /dev/null @@ -1,34 +0,0 @@ -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/service/MenuService.java b/src/main/java/com/youlai/boot/system/service/MenuService.java index 6fbff3f3..389ec2bb 100644 --- a/src/main/java/com/youlai/boot/system/service/MenuService.java +++ b/src/main/java/com/youlai/boot/system/service/MenuService.java @@ -1,7 +1,7 @@ package com.youlai.boot.system.service; import com.baomidou.mybatisplus.extension.service.IService; -import com.youlai.boot.platform.codegen.model.entity.GenConfig; +import com.youlai.boot.platform.codegen.model.entity.GenTable; import com.youlai.boot.system.model.form.MenuForm; import com.youlai.boot.common.model.Option; import com.youlai.boot.system.model.entity.Menu; @@ -79,5 +79,5 @@ public interface MenuService extends IService { * @param parentMenuId 父菜单ID * @param genConfig 实体名 */ - void addMenuForCodegen(Long parentMenuId, GenConfig genConfig); + void addMenuForCodegen(Long parentMenuId, GenTable genTable); } diff --git a/src/main/java/com/youlai/boot/system/service/TenantService.java b/src/main/java/com/youlai/boot/system/service/TenantService.java index 130ad916..2d265cea 100644 --- a/src/main/java/com/youlai/boot/system/service/TenantService.java +++ b/src/main/java/com/youlai/boot/system/service/TenantService.java @@ -46,17 +46,4 @@ public interface TenantService extends IService { * @return true-有权限,false-无权限 */ boolean hasTenantPermission(Long userId, Long tenantId); - - /** - * 记录租户切换审计日志 - * - * @param userId 用户ID - * @param fromTenantId 原租户ID - * @param toTenantId 目标租户ID - * @param success 是否成功 - * @param failReason 失败原因 - * @param request HTTP请求对象 - */ - void recordTenantSwitch(Long userId, Long fromTenantId, Long toTenantId, - boolean success, String failReason, jakarta.servlet.http.HttpServletRequest request); } diff --git a/src/main/java/com/youlai/boot/system/service/UserService.java b/src/main/java/com/youlai/boot/system/service/UserService.java index 2ee3ca09..89cb5bd7 100644 --- a/src/main/java/com/youlai/boot/system/service/UserService.java +++ b/src/main/java/com/youlai/boot/system/service/UserService.java @@ -71,9 +71,25 @@ public interface UserService extends IService { * @param username 用户名 * @return {@link UserAuthCredentials} */ - UserAuthCredentials getAuthCredentialsByUsername(String username); + /** + * 根据用户名和租户ID获取认证信息(用于多租户登录) + * + * @param username 用户名 + * @param tenantId 租户ID + * @return {@link UserAuthCredentials} + */ + UserAuthCredentials getAuthCredentialsByUsernameAndTenant(String username, Long tenantId); + + /** + * 根据用户名查询该用户在所有租户下的记录(用于多租户登录时判断是否需要选择租户) + * + * @param username 用户名 + * @return 用户列表(每个租户一条记录) + */ + List listUsersByUsername(String username); + /** * 获取导出用户列表 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 81120c24..17ac96f7 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 @@ -10,7 +10,7 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import com.youlai.boot.platform.codegen.model.entity.GenConfig; +import com.youlai.boot.platform.codegen.model.entity.GenTable; import com.youlai.boot.security.util.SecurityUtils; import com.youlai.boot.system.converter.MenuConverter; import com.youlai.boot.system.mapper.MenuMapper; @@ -427,14 +427,14 @@ public class MenuServiceImpl extends ServiceImpl implements Me * 代码生成时添加菜单 * * @param parentMenuId 父菜单ID - * @param genConfig 实体名称 + * @param genTable 实体名称 */ @Override - public void addMenuForCodegen(Long parentMenuId, GenConfig genConfig) { + public void addMenuForCodegen(Long parentMenuId, GenTable genTable) { Menu parentMenu = this.getById(parentMenuId); Assert.notNull(parentMenu, "上级菜单不存在"); - String entityName = genConfig.getEntityName(); + String entityName = genTable.getEntityName(); long count = this.count(new LambdaQueryWrapper().eq(Menu::getRouteName, entityName)); if (count > 0) { @@ -453,11 +453,11 @@ public class MenuServiceImpl extends ServiceImpl implements Me Menu menu = new Menu(); menu.setParentId(parentMenuId); - menu.setName(genConfig.getBusinessName()); + menu.setName(genTable.getBusinessName()); menu.setRouteName(entityName); menu.setRoutePath(StrUtil.toSymbolCase(entityName, '-')); - menu.setComponent(genConfig.getModuleName() + "/" + StrUtil.toSymbolCase(entityName, '-') + "/index"); + menu.setComponent(genTable.getModuleName() + "/" + StrUtil.toSymbolCase(entityName, '-') + "/index"); menu.setType(MenuTypeEnum.MENU.getValue()); menu.setSort(sort); menu.setVisible(1); @@ -470,7 +470,7 @@ public class MenuServiceImpl extends ServiceImpl implements Me this.updateById(menu); // 生成CURD按钮权限 - String permPrefix = genConfig.getModuleName() + ":" + genConfig.getTableName().replace("_", "-") + ":"; + String permPrefix = genTable.getModuleName() + ":" + genTable.getTableName().replace("_", "-") + ":"; String[] actions = {"查询", "新增", "修改", "删除"}; String[] perms = {"list", "create", "update", "delete"}; 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 index 14bda285..b4dfed9c 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/TenantServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/TenantServiceImpl.java @@ -1,28 +1,22 @@ package com.youlai.boot.system.service.impl; -import cn.hutool.core.util.StrUtil; 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.TenantSwitchLogMapper; import com.youlai.boot.system.mapper.UserMapper; -import com.youlai.boot.system.mapper.UserTenantMapper; import com.youlai.boot.system.model.entity.Tenant; -import com.youlai.boot.system.model.entity.TenantSwitchLog; import com.youlai.boot.system.model.entity.User; -import com.youlai.boot.system.model.entity.UserTenant; import com.youlai.boot.system.model.vo.TenantVO; import com.youlai.boot.system.service.TenantService; -import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; -import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; +import java.util.stream.IntStream; /** * 租户服务实现类 @@ -35,8 +29,6 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class TenantServiceImpl extends ServiceImpl implements TenantService { - private final UserTenantMapper userTenantMapper; - private final TenantSwitchLogMapper tenantSwitchLogMapper; private final UserMapper userMapper; @Override @@ -44,21 +36,34 @@ public class TenantServiceImpl extends ServiceImpl impleme // 临时忽略租户过滤,查询所有租户 TenantContextHolder.setIgnoreTenant(true); try { - // 查询用户关联的租户ID列表 - List userTenants = userTenantMapper.selectList( - new LambdaQueryWrapper() - .eq(UserTenant::getUserId, userId) - ); - - if (userTenants.isEmpty()) { + // 先根据用户ID查询用户信息(获取 username) + User user = userMapper.selectById(userId); + if (user == null) { return List.of(); } - // 提取租户ID列表 - List tenantIds = userTenants.stream() - .map(UserTenant::getTenantId) + // 通过 username 查询该用户在所有租户下的记录,获取租户ID列表 + List users = userMapper.selectList( + new LambdaQueryWrapper() + .eq(User::getUsername, user.getUsername()) + .eq(User::getIsDeleted, 0) + ); + + if (users.isEmpty()) { + return List.of(); + } + + // 提取租户ID列表(去重) + List tenantIds = users.stream() + .map(User::getTenantId) + .filter(tenantId -> tenantId != null) + .distinct() .collect(Collectors.toList()); + if (tenantIds.isEmpty()) { + return List.of(); + } + // 查询租户信息 List tenants = this.list( new LambdaQueryWrapper() @@ -67,17 +72,19 @@ public class TenantServiceImpl extends ServiceImpl impleme .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()); + // 转换为VO,第一个租户作为默认租户 + return IntStream.range(0, tenants.size()) + .mapToObj(index -> { + Tenant tenant = tenants.get(index); + TenantVO vo = new TenantVO(); + BeanUtils.copyProperties(tenant, vo); + // 第一个租户作为默认租户 + if (index == 0) { + vo.setIsDefault(true); + } + return vo; + }) + .collect(Collectors.toList()); } finally { TenantContextHolder.setIgnoreTenant(false); } @@ -119,94 +126,25 @@ public class TenantServiceImpl extends ServiceImpl impleme 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) + // 先根据用户ID查询用户信息(获取 username) + User user = userMapper.selectById(userId); + if (user == null) { + return false; + } + + // 检查该 username 在指定租户下是否存在用户记录 + User tenantUser = userMapper.selectOne( + new LambdaQueryWrapper() + .eq(User::getUsername, user.getUsername()) + .eq(User::getTenantId, tenantId) + .eq(User::getIsDeleted, 0) .last("LIMIT 1") ); - return userTenant != null; + return tenantUser != null; } finally { TenantContextHolder.setIgnoreTenant(false); } } - @Override - public void recordTenantSwitch(Long userId, Long fromTenantId, Long toTenantId, - boolean success, String failReason, HttpServletRequest request) { - try { - // 临时忽略租户过滤,确保日志可以写入 - TenantContextHolder.setIgnoreTenant(true); - - // 创建审计日志 - TenantSwitchLog log = new TenantSwitchLog(); - log.setUserId(userId); - log.setFromTenantId(fromTenantId); - log.setToTenantId(toTenantId); - log.setSwitchTime(LocalDateTime.now()); - log.setStatus(success ? 1 : 0); - log.setFailReason(failReason); - - // 获取用户名 - if (userId != null) { - User user = userMapper.selectById(userId); - if (user != null) { - log.setUsername(user.getUsername()); - } - } - - // 获取租户名称 - if (fromTenantId != null) { - Tenant fromTenant = this.getById(fromTenantId); - if (fromTenant != null) { - log.setFromTenantName(fromTenant.getName()); - } - } - if (toTenantId != null) { - Tenant toTenant = this.getById(toTenantId); - if (toTenant != null) { - log.setToTenantName(toTenant.getName()); - } - } - - // 获取IP地址和User-Agent - if (request != null) { - log.setIpAddress(getIpAddress(request)); - log.setUserAgent(request.getHeader("User-Agent")); - } - - // 保存审计日志 - tenantSwitchLogMapper.insert(log); - } catch (Exception e) { - // 记录日志失败不应影响业务,仅记录错误 - Slf4j.getLogger(this.getClass()).error("记录租户切换日志失败", e); - } finally { - TenantContextHolder.setIgnoreTenant(false); - } - } - - /** - * 获取客户端IP地址 - */ - private String getIpAddress(HttpServletRequest request) { - String ip = request.getHeader("X-Forwarded-For"); - if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { - ip = request.getHeader("X-Real-IP"); - } - if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { - ip = request.getHeader("Proxy-Client-IP"); - } - if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { - ip = request.getHeader("WL-Proxy-Client-IP"); - } - if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { - ip = request.getRemoteAddr(); - } - // 处理多级代理的情况 - if (ip != null && ip.contains(",")) { - ip = ip.split(",")[0].trim(); - } - return ip; - } } diff --git a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java index 401dc492..b81bba24 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java @@ -18,6 +18,7 @@ import com.youlai.boot.security.model.UserAuthCredentials; import com.youlai.boot.security.service.PermissionService; import com.youlai.boot.security.token.TokenManager; import com.youlai.boot.security.util.SecurityUtils; +import com.youlai.boot.common.tenant.TenantContextHolder; import com.youlai.boot.platform.mail.service.MailService; import com.youlai.boot.system.converter.UserConverter; import com.youlai.boot.system.enums.DictCodeEnum; @@ -78,7 +79,6 @@ public class UserServiceImpl extends ServiceImpl implements Us private final com.youlai.boot.config.property.TenantProperties tenantProperties; - private final com.youlai.boot.system.mapper.UserTenantMapper userTenantMapper; /** * 获取用户分页列表 @@ -126,17 +126,26 @@ public class UserServiceImpl extends ServiceImpl implements Us public boolean saveUser(UserForm userForm) { String username = userForm.getUsername(); - - long count = this.count(new LambdaQueryWrapper().eq(User::getUsername, username)); - Assert.isTrue(count == 0, "用户名已存在"); - + // 实体转换 form->entity User entity = userConverter.toEntity(userForm); + + // 获取当前操作员的租户ID(新增用户时,租户ID由 MyMetaObjectHandler 自动填充) + Long tenantId = TenantContextHolder.getTenantId(); + Assert.notNull(tenantId, "租户ID不能为空"); + + // 检查同一租户下用户名是否已存在(新设计:用户名在租户内唯一) + long count = this.count(new LambdaQueryWrapper() + .eq(User::getUsername, username) + .eq(User::getTenantId, tenantId)); + Assert.isTrue(count == 0, "该租户下用户名已存在"); // 设置默认加密密码 String defaultEncryptPwd = passwordEncoder.encode(SystemConstants.DEFAULT_PASSWORD); entity.setPassword(defaultEncryptPwd); entity.setCreateBy(SecurityUtils.getUserId()); + + // 注意:租户ID由 MyMetaObjectHandler.insertFill() 自动填充,无需手动设置 // 新增用户 boolean result = this.save(entity); @@ -144,11 +153,6 @@ public class UserServiceImpl extends ServiceImpl implements Us if (result) { // 保存用户角色 userRoleService.saveUserRoles(entity.getId(), userForm.getRoleIds()); - - // 如果启用多租户,保存用户租户关联 - if (Boolean.TRUE.equals(tenantProperties.getEnabled())) { - saveUserTenantRelation(entity.getId(), entity.getTenantId(), true); - } } return result; } @@ -165,20 +169,32 @@ public class UserServiceImpl extends ServiceImpl implements Us public boolean updateUser(Long userId, UserForm userForm) { String username = userForm.getUsername(); + + // 获取原用户信息 + User oldUser = this.getById(userId); + Assert.notNull(oldUser, "用户不存在"); + + Long oldTenantId = oldUser.getTenantId(); + Long currentTenantId = TenantContextHolder.getTenantId(); + + // 验证:只能修改当前租户下的用户(防止跨租户修改) + Assert.isTrue(oldTenantId != null && oldTenantId.equals(currentTenantId), + "只能修改当前租户下的用户"); + // 检查同一租户下用户名是否已存在(排除当前用户) long count = this.count(new LambdaQueryWrapper() .eq(User::getUsername, username) + .eq(User::getTenantId, currentTenantId) .ne(User::getId, userId) ); - Assert.isTrue(count == 0, "用户名已存在"); - - // 获取原用户信息,用于比较租户是否变更 - User oldUser = this.getById(userId); - Long oldTenantId = oldUser != null ? oldUser.getTenantId() : null; + Assert.isTrue(count == 0, "该租户下用户名已存在"); // form -> entity User entity = userConverter.toEntity(userForm); entity.setUpdateBy(SecurityUtils.getUserId()); + + // 保持租户ID不变(不允许跨租户修改用户) + entity.setTenantId(oldTenantId); // 修改用户 boolean result = this.updateById(entity); @@ -186,23 +202,6 @@ public class UserServiceImpl extends ServiceImpl implements Us if (result) { // 保存用户角色 userRoleService.saveUserRoles(entity.getId(), userForm.getRoleIds()); - - // 如果启用多租户且租户发生变更,更新用户租户关联 - if (Boolean.TRUE.equals(tenantProperties.getEnabled())) { - Long newTenantId = entity.getTenantId(); - if (newTenantId != null && !newTenantId.equals(oldTenantId)) { - // 删除旧的租户关联 - if (oldTenantId != null) { - userTenantMapper.delete( - new LambdaQueryWrapper() - .eq(com.youlai.boot.system.model.entity.UserTenant::getUserId, userId) - .eq(com.youlai.boot.system.model.entity.UserTenant::getTenantId, oldTenantId) - ); - } - // 保存新的租户关联 - saveUserTenantRelation(userId, newTenantId, true); - } - } } return result; } @@ -224,16 +223,7 @@ public class UserServiceImpl extends ServiceImpl implements Us boolean result = this.removeByIds(ids); - // 如果启用多租户,删除用户租户关联 - if (result && Boolean.TRUE.equals(tenantProperties.getEnabled())) { - for (Long userId : ids) { - userTenantMapper.delete( - new LambdaQueryWrapper() - .eq(com.youlai.boot.system.model.entity.UserTenant::getUserId, userId) - ); - log.info("删除用户租户关联:userId={}", userId); - } - } + // 新设计:用户删除时,tenant_id 字段会随用户记录一起逻辑删除,无需额外处理 return result; } @@ -256,6 +246,46 @@ public class UserServiceImpl extends ServiceImpl implements Us return userAuthCredentials; } + @Override + public UserAuthCredentials getAuthCredentialsByUsernameAndTenant(String username, Long tenantId) { + // 临时忽略租户过滤,查询指定租户下的用户 + TenantContextHolder.setIgnoreTenant(true); + try { + // 先查询用户 + User user = this.getOne( + new LambdaQueryWrapper() + .eq(User::getUsername, username) + .eq(User::getTenantId, tenantId) + .eq(User::getIsDeleted, 0) + .last("LIMIT 1") + ); + if (user == null) { + return null; + } + // 设置租户上下文,然后查询认证信息(这样会包含该租户下的角色) + TenantContextHolder.setTenantId(tenantId); + return getAuthCredentialsByUsername(username); + } finally { + TenantContextHolder.setIgnoreTenant(false); + } + } + + @Override + public List listUsersByUsername(String username) { + // 临时忽略租户过滤,查询该用户名在所有租户下的记录 + TenantContextHolder.setIgnoreTenant(true); + try { + return this.list( + new LambdaQueryWrapper() + .eq(User::getUsername, username) + .eq(User::getIsDeleted, 0) + .orderByAsc(User::getTenantId) // 按租户ID排序,优先返回较小的租户ID + ); + } finally { + TenantContextHolder.setIgnoreTenant(false); + } + } + /** * 根据OpenID获取用户认证信息 * @@ -731,45 +761,5 @@ public class UserServiceImpl extends ServiceImpl implements Us return userConverter.toOptions(list); } - /** - * 保存用户租户关联关系 - *

- * 仅在启用多租户时调用此方法 - *

- * - * @param userId 用户ID - * @param tenantId 租户ID - * @param isDefault 是否为默认租户 - */ - private void saveUserTenantRelation(Long userId, Long tenantId, boolean isDefault) { - if (userId == null || tenantId == null) { - log.warn("用户ID或租户ID为空,跳过保存用户租户关联"); - return; - } - - // 检查关联是否已存在 - com.youlai.boot.system.model.entity.UserTenant existingRelation = userTenantMapper.selectOne( - new LambdaQueryWrapper() - .eq(com.youlai.boot.system.model.entity.UserTenant::getUserId, userId) - .eq(com.youlai.boot.system.model.entity.UserTenant::getTenantId, tenantId) - ); - - if (existingRelation != null) { - // 如果已存在,更新 is_default 标识 - if (isDefault && existingRelation.getIsDefault() != 1) { - existingRelation.setIsDefault(1); - userTenantMapper.updateById(existingRelation); - log.info("更新用户租户关联:userId={}, tenantId={}, isDefault=true", userId, tenantId); - } - } else { - // 不存在则新增 - com.youlai.boot.system.model.entity.UserTenant userTenant = new com.youlai.boot.system.model.entity.UserTenant(); - userTenant.setUserId(userId); - userTenant.setTenantId(tenantId); - userTenant.setIsDefault(isDefault ? 1 : 0); - userTenantMapper.insert(userTenant); - log.info("保存用户租户关联:userId={}, tenantId={}, isDefault={}", userId, tenantId, isDefault); - } - } } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 09335b47..38af018b 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -11,8 +11,8 @@ spring: # === MySQL 数据源(默认启用) === driver-class-name: com.mysql.cj.jdbc.Driver 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 + username: root + password: Youlai@2025 # === PostgreSQL 数据源示例(按需启用) === # driver-class-name: org.postgresql.Driver @@ -275,7 +275,7 @@ youlai: tenant: # 是否启用多租户功能(默认:false) # 设置为 true 启用多租户,设置为 false 禁用多租户(零成本切换) - enabled: false + enabled: true # 租户字段名(默认:tenant_id) column: tenant_id diff --git a/src/main/resources/codegen.yml b/src/main/resources/codegen.yml index 134ddfd4..ceaf271a 100644 --- a/src/main/resources/codegen.yml +++ b/src/main/resources/codegen.yml @@ -13,8 +13,8 @@ codegen: moduleName: system # 排除数据表 excludeTables: - - gen_config - - gen_field_config + - gen_table + - gen_table_column ## 模板配置 templateConfigs: API: diff --git a/src/main/resources/mapper/ai/AiCommandLogMapper.xml b/src/main/resources/mapper/ai/AiCommandRecordMapper.xml similarity index 63% rename from src/main/resources/mapper/ai/AiCommandLogMapper.xml rename to src/main/resources/mapper/ai/AiCommandRecordMapper.xml index 55a12a2b..1755acdc 100644 --- a/src/main/resources/mapper/ai/AiCommandLogMapper.xml +++ b/src/main/resources/mapper/ai/AiCommandRecordMapper.xml @@ -2,9 +2,9 @@ - + - + @@ -28,55 +28,55 @@ - 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 + acr.id, + acr.user_id, + acr.username, + acr.original_command, + acr.ai_provider, + acr.ai_model, + acr.parse_status, + acr.function_calls, + acr.explanation, + acr.confidence, + acr.parse_error_message, + acr.input_tokens, + acr.output_tokens, + acr.parse_duration_ms, + acr.function_name, + acr.function_arguments, + acr.execute_status, + acr.execute_error_message, + acr.ip_address, + acr.create_time, + acr.update_time + FROM ai_command_record acr ( - acl.original_command LIKE CONCAT('%', #{queryParams.keywords}, '%') - OR acl.function_name LIKE CONCAT('%', #{queryParams.keywords}, '%') - OR acl.username LIKE CONCAT('%', #{queryParams.keywords}, '%') + acr.original_command LIKE CONCAT('%', #{queryParams.keywords}, '%') + OR acr.function_name LIKE CONCAT('%', #{queryParams.keywords}, '%') + OR acr.username LIKE CONCAT('%', #{queryParams.keywords}, '%') ) - AND acl.execute_status = #{queryParams.executeStatus} + AND acr.execute_status = #{queryParams.executeStatus} - AND acl.user_id = #{queryParams.userId} + AND acr.user_id = #{queryParams.userId} - AND acl.parse_status = #{queryParams.parseStatus} + AND acr.parse_status = #{queryParams.parseStatus} - AND acl.function_name = #{queryParams.functionName} + AND acr.function_name = #{queryParams.functionName} - AND acl.ai_provider = #{queryParams.aiProvider} + AND acr.ai_provider = #{queryParams.aiProvider} - AND acl.ai_model = #{queryParams.aiModel} + AND acr.ai_model = #{queryParams.aiModel} - AND acl.create_time BETWEEN #{queryParams.createTime[0]} AND #{queryParams.createTime[1]} + AND acr.create_time BETWEEN #{queryParams.createTime[0]} AND #{queryParams.createTime[1]} - ORDER BY acl.create_time DESC + ORDER BY acr.create_time DESC diff --git a/src/main/resources/mapper/codegen/DatabaseMapper.xml b/src/main/resources/mapper/codegen/DatabaseMapper.xml index c2aefee0..0e402a8d 100644 --- a/src/main/resources/mapper/codegen/DatabaseMapper.xml +++ b/src/main/resources/mapper/codegen/DatabaseMapper.xml @@ -16,7 +16,7 @@ CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured FROM information_schema.tables t1 - LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name + LEFT JOIN gen_table t2 on t1.TABLE_NAME = t2.table_name WHERE t1.TABLE_SCHEMA = (SELECT DATABASE()) AND t1.table_type = 'BASE TABLE' @@ -46,7 +46,7 @@ ALL_TABLES t1 LEFT JOIN ALL_TAB_COMMENTS c ON t1.TABLE_NAME = c.TABLE_NAME AND t1.OWNER = c.OWNER LEFT JOIN ALL_OBJECTS o ON t1.TABLE_NAME = o.OBJECT_NAME AND t1.OWNER = o.OWNER AND o.OBJECT_TYPE = 'TABLE' - LEFT JOIN gen_config t2 ON t1.TABLE_NAME = t2.table_name + LEFT JOIN gen_table t2 ON t1.TABLE_NAME = t2.table_name WHERE t1.OWNER = 'YOULAI_BOOT' diff --git a/src/main/resources/mapper/codegen/GenConfigMapper.xml b/src/main/resources/mapper/codegen/GenConfigMapper.xml index 02709a91..0271fa20 100644 --- a/src/main/resources/mapper/codegen/GenConfigMapper.xml +++ b/src/main/resources/mapper/codegen/GenConfigMapper.xml @@ -2,6 +2,6 @@ - + diff --git a/src/main/resources/mapper/codegen/GenFieldConfigMapper.xml b/src/main/resources/mapper/codegen/GenFieldConfigMapper.xml index 0bd4db31..8cb86c3d 100644 --- a/src/main/resources/mapper/codegen/GenFieldConfigMapper.xml +++ b/src/main/resources/mapper/codegen/GenFieldConfigMapper.xml @@ -2,6 +2,6 @@ - + diff --git a/src/main/resources/mapper/codegen/GenTableColumnMapper.xml b/src/main/resources/mapper/codegen/GenTableColumnMapper.xml new file mode 100644 index 00000000..8cb86c3d --- /dev/null +++ b/src/main/resources/mapper/codegen/GenTableColumnMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/src/main/resources/mapper/codegen/GenTableMapper.xml b/src/main/resources/mapper/codegen/GenTableMapper.xml new file mode 100644 index 00000000..0271fa20 --- /dev/null +++ b/src/main/resources/mapper/codegen/GenTableMapper.xml @@ -0,0 +1,7 @@ + + + + + From d0b80d17e2b36a7ace0e13366b523f8ca14847c9 Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Fri, 12 Dec 2025 08:19:19 +0800 Subject: [PATCH 12/22] =?UTF-8?q?refactor:=20=E5=A4=9A=E7=A7=9F=E6=88=B7?= =?UTF-8?q?=E4=B8=B4=E6=97=B6=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/多租户表隔离策略.md | 288 ++++++++++++++++++++++++ sql/mysql/tenant_add.sql | 7 +- src/main/resources/application-dev.yml | 1 + src/main/resources/application-prod.yml | 30 +++ 4 files changed, 324 insertions(+), 2 deletions(-) create mode 100644 docs/多租户表隔离策略.md diff --git a/docs/多租户表隔离策略.md b/docs/多租户表隔离策略.md new file mode 100644 index 00000000..6b8e9c5f --- /dev/null +++ b/docs/多租户表隔离策略.md @@ -0,0 +1,288 @@ +# 多租户表隔离策略说明 + +## 📋 概述 + +本文档说明系统中各业务表的多租户隔离策略,帮助理解哪些表需要租户隔离,哪些表应该共享。 + +--- + +## 🎯 设计原则 + +### 1. **数据隔离**(Tenant Isolation) +- 租户私有数据必须严格隔离 +- 通过 `tenant_id` 字段实现 +- MyBatis-Plus 多租户插件自动添加过滤条件 + +### 2. **功能共享**(Feature Sharing) +- 系统功能定义应该标准化 +- 避免重复数据和维护成本 +- 通过角色和权限控制访问 + +### 3. **灵活配置**(Flexible Configuration) +- 通过配置文件控制隔离策略 +- 可随时调整隔离范围 +- 零成本切换单租户/多租户 + +--- + +## 📊 表隔离策略 + +### ✅ 需要租户隔离的表 + +这些表存储租户私有数据,必须添加 `tenant_id` 字段: + +| 表名 | 说明 | 隔离原因 | +|------|------|---------| +| `sys_user` | 用户表 | 用户属于特定租户,数据必须隔离 | +| `sys_role` | 角色表 | 角色是租户自定义的,不同租户角色不同 | +| `sys_dept` | 部门表 | 部门结构是租户私有的组织架构 | +| `sys_notice` | 通知公告表 | 通知是租户内部的信息 | +| `sys_log` | 系统日志表 | 日志记录租户的操作行为 | +| `sys_role_menu` | 角色菜单关联表 | 角色是租户隔离的,关联表也需要隔离 | +| `sys_user_role` | 用户角色关联表 | 用户和角色都是租户隔离的 | +| `ai_command_record` | AI命令记录表 | 命令记录是租户私有数据 | + +**实现方式**: +```sql +-- 添加 tenant_id 字段 +ALTER TABLE `sys_user` +ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `id`, +ADD INDEX `idx_tenant_id` (`tenant_id`); + +-- 初始化为默认租户 +UPDATE `sys_user` SET `tenant_id` = 1 WHERE `tenant_id` IS NULL; +``` + +--- + +### ❌ 不需要租户隔离的表 + +这些表存储系统公共数据,应该所有租户共享: + +| 表名 | 说明 | 共享原因 | +|------|------|---------| +| `sys_tenant` | 租户表 | 租户表本身不能隔离 | +| **`sys_menu`** | **菜单表** | **功能入口定义,标准化共享** | +| `sys_dict` | 字典表 | 系统字典通常是标准化的 | +| `sys_dict_item` | 字典项表 | 字典值应该统一 | +| `sys_config` | 系统配置表 | 系统级配置应该全局统一 | + +**配置方式**: +```yaml +youlai: + tenant: + enabled: true + ignore-tables: + - sys_tenant # 租户表本身 + - sys_menu # 菜单表(重点!) + - sys_dict # 字典表 + - sys_dict_item # 字典项表 + - sys_config # 系统配置表 +``` + +--- + +## 🔍 重点说明:为什么菜单不隔离? + +### 问题背景 +```sql +-- 错误示例:如果菜单隔离,会产生大量冗余 +租户A的菜单: + - 系统管理 → 用户管理 → 角色管理 +租户B的菜单: + - 系统管理 → 用户管理 → 角色管理 +租户C的菜单: + - 系统管理 → 用户管理 → 角色管理 +(完全相同的菜单定义重复了3次!) +``` + +### 推荐方案:菜单共享 + 角色控制 + +#### 1. **菜单定义共享** +``` +所有租户共享同一套菜单定义: +├─ 系统管理 +│ ├─ 用户管理 +│ ├─ 角色管理 +│ ├─ 菜单管理 +│ └─ 租户管理 +├─ 业务管理 +│ ├─ 订单管理 +│ └─ 商品管理 +``` + +#### 2. **权限通过角色控制** +```typescript +// 租户A的管理员角色 +角色:租户A管理员 +权限:系统管理、业务管理(全部菜单) + +// 租户A的普通员工角色 +角色:租户A员工 +权限:业务管理(部分菜单) + +// 租户B的管理员角色 +角色:租户B管理员 +权限:系统管理、业务管理(全部菜单) +``` + +#### 3. **优势** + +| 维度 | 菜单共享 | 菜单隔离 | +|------|---------|---------| +| **数据量** | ✅ 少量 | ❌ 大量冗余 | +| **升级维护** | ✅ 一次升级 | ❌ 需迁移所有租户 | +| **管理成本** | ✅ 低 | ❌ 高 | +| **功能一致性** | ✅ 保证统一 | ⚠️ 可能不一致 | +| **定制能力** | ⚠️ 通过角色实现 | ✅ 每租户独立 | + +--- + +## 💡 权限控制流程 + +### 用户访问菜单的流程 + +```mermaid +graph TD + A[用户登录] --> B[获取用户角色] + B --> C{角色是否有权限?} + C -->|是| D[显示菜单] + C -->|否| E[隐藏菜单] + + style A fill:#e1f5ff + style D fill:#d4edda + style E fill:#f8d7da +``` + +### 示例代码 + +```java +// 1. 菜单定义(所有租户共享) +sys_menu: + id: 1, name: "用户管理", perm: "sys:user:list" + +// 2. 租户A的角色(租户隔离) +sys_role (tenant_id=1): + id: 10, name: "管理员", tenant_id: 1 + +// 3. 角色菜单关联(租户隔离) +sys_role_menu (tenant_id=1): + role_id: 10, menu_id: 1, tenant_id: 1 + +// 查询时自动过滤 +SELECT t3.perm, t2.code +FROM sys_role_menu t1 +INNER JOIN sys_role t2 ON t1.role_id = t2.id + AND t2.tenant_id = 1 -- ✅ 角色租户过滤 +INNER JOIN sys_menu t3 ON t1.menu_id = t3.id + -- ❌ 菜单不需要租户过滤(通过 ignore-tables 配置) +WHERE t1.tenant_id = 1 -- ✅ 关联表租户过滤 +``` + +--- + +## 🔧 配置示例 + +### application-dev.yml + +```yaml +youlai: + tenant: + # 启用多租户 + enabled: true + + # 租户字段名 + column: tenant_id + + # 默认租户ID + default-tenant-id: 1 + + # 忽略多租户过滤的表(重点配置) + ignore-tables: + - sys_tenant # 租户表本身 + - sys_menu # 菜单表(所有租户共享) + - sys_dict # 字典表 + - sys_dict_item # 字典项表 + - sys_config # 系统配置表 +``` + +--- + +## ⚠️ 常见问题 + +### Q1: 如果需要为不同租户定制菜单怎么办? + +**A:** 有两种方案: + +#### 方案1: 通过角色权限控制(推荐) +``` +租户A看到:菜单A、B、C(通过角色权限配置) +租户B看到:菜单A、B(通过角色权限配置) +``` + +#### 方案2: 菜单隔离(不推荐) +```yaml +# 将 sys_menu 从 ignore-tables 中移除 +ignore-tables: + - sys_tenant + # - sys_menu # 注释掉,启用菜单隔离 + +# 然后执行 SQL 添加 tenant_id +ALTER TABLE sys_menu +ADD COLUMN tenant_id bigint DEFAULT 1; +``` + +--- + +### Q2: 如果后端报错 `Unknown column 't3.tenant_id'` 怎么办? + +**A:** 这个错误说明: +1. ❌ `sys_menu` 表没有 `tenant_id` 字段 +2. ❌ 但配置文件中没有将 `sys_menu` 添加到 `ignore-tables` +3. ✅ 解决方案:将 `sys_menu` 添加到 `ignore-tables`(本文档已说明) + +--- + +### Q3: 字典表需要隔离吗? + +**A:** 通常不需要,原因: +- 字典是系统标准配置(如:性别、状态等) +- 所有租户应该使用统一的字典定义 +- 如果需要租户级字典,可以单独创建 `tenant_dict` 表 + +--- + +## 📝 总结 + +### 核心原则 + +1. **数据隔离**:用户、角色、部门等业务数据必须隔离 +2. **功能共享**:菜单、字典、配置等系统定义应该共享 +3. **权限控制**:通过角色和权限实现访问控制 + +### 最佳实践 + +``` +✅ 推荐做法: +- 菜单定义共享 +- 角色租户隔离 +- 通过角色控制菜单访问权限 + +❌ 不推荐做法: +- 为每个租户复制菜单 +- 菜单和角色都隔离但逻辑相同 +- 升级时需要迁移所有租户的菜单 +``` + +--- + +## 🔗 相关文档 + +- [多租户用户管理改进说明](./多租户用户管理改进说明.md) +- [tenant_add.sql](../sql/mysql/tenant_add.sql) - 多租户SQL脚本 +- [TenantProperties.java](../src/main/java/com/youlai/boot/config/property/TenantProperties.java) - 配置类 + +--- + +**更新时间**:2025-12-12 +**版本**:v3.0.0 diff --git a/sql/mysql/tenant_add.sql b/sql/mysql/tenant_add.sql index 07305776..b1b4b0ee 100644 --- a/sql/mysql/tenant_add.sql +++ b/sql/mysql/tenant_add.sql @@ -41,8 +41,11 @@ INSERT INTO `sys_tenant` (`id`, `name`, `code`, `status`, `create_time`) VALUES -- ============================================ -- 2. 为业务表添加 tenant_id 字段 -- ============================================ --- 注意:MySQL 5.7 不支持 IF NOT EXISTS,如果字段已存在会报错 --- 建议先检查字段是否存在,或使用 MySQL 8.0+ +-- 注意事项: +-- 1. MySQL 5.7 不支持 IF NOT EXISTS,如果字段已存在会报错 +-- 2. 菜单表(sys_menu)不添加 tenant_id,所有租户共享菜单定义 +-- 权限控制通过角色实现(角色是租户隔离的) +-- 3. 建议先检查字段是否存在,或使用 MySQL 8.0+ -- 用户表:仅在不存在时添加列和索引,避免重复执行报错 ALTER TABLE `sys_user` diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 38af018b..cee52f9e 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -289,6 +289,7 @@ youlai: # 忽略多租户过滤的表名列表(系统表、租户表等不需要租户隔离的表) ignore-tables: - sys_tenant # 租户表本身 + - sys_menu # 菜单表(功能入口定义,所有租户共享) - sys_dict # 字典表(通常共享) - sys_dict_item # 字典项表(通常共享) - sys_config # 系统配置表(通常共享) diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 2dcf4b99..2bccb95d 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -318,3 +318,33 @@ ai: rate-limit: max-executions-per-minute: 10 max-executions-per-day: 100 + +# ============================================ +# 多租户配置 +# ============================================ +# 说明:通过 youlai.tenant.enabled 控制是否启用多租户功能 +# 启用后,所有 SQL 查询会自动添加 tenant_id 过滤条件 +# ============================================ +youlai: + tenant: + # 是否启用多租户功能(默认:false) + # 设置为 true 启用多租户,设置为 false 禁用多租户(零成本切换) + enabled: true + + # 租户字段名(默认: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_menu # 菜单表(功能入口定义,所有租户共享) + - sys_dict # 字典表(通常共享) + - sys_dict_item # 字典项表(通常共享) + - sys_config # 系统配置表(通常共享) +# ============================================ From 3f05f773515b95df6ea5212276b2c115a6fd3a0c Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Fri, 12 Dec 2025 22:26:43 +0800 Subject: [PATCH 13/22] =?UTF-8?q?feat:=20=E6=9D=83=E9=99=90=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E5=8A=A0=E8=BD=BD=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/mysql/tenant_add.sql | 7 + sql/mysql/tenant_remove.sql | 4 + .../boot/auth/controller/AuthController.java | 5 +- .../config/property/TenantProperties.java | 13 -- .../security/service/PermissionService.java | 26 ++- .../system/controller/TenantController.java | 17 +- .../boot/system/model/bo/RolePermsBO.java | 5 + .../youlai/boot/system/model/entity/User.java | 2 + .../boot/system/service/TenantService.java | 18 +- .../boot/system/service/UserService.java | 9 +- .../service/impl/RoleMenuServiceImpl.java | 167 +++++++++++++----- .../service/impl/TenantServiceImpl.java | 4 +- .../system/service/impl/UserServiceImpl.java | 6 +- src/main/resources/application-dev.yml | 12 +- .../mapper/system/RoleMenuMapper.xml | 6 +- 15 files changed, 210 insertions(+), 91 deletions(-) diff --git a/sql/mysql/tenant_add.sql b/sql/mysql/tenant_add.sql index b1b4b0ee..350bb7f1 100644 --- a/sql/mysql/tenant_add.sql +++ b/sql/mysql/tenant_add.sql @@ -67,6 +67,13 @@ ADD INDEX `idx_tenant_id` (`tenant_id`); UPDATE `sys_role` SET `tenant_id` = 1 WHERE `tenant_id` IS NULL; +-- 角色菜单关联表 +ALTER TABLE `sys_role_menu` +ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `role_id`, +ADD INDEX `idx_role_menu_tenant_id` (`tenant_id`); + +UPDATE `sys_role_menu` SET `tenant_id` = 1 WHERE `tenant_id` IS NULL; + -- 部门表 ALTER TABLE `sys_dept` ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `id`, diff --git a/sql/mysql/tenant_remove.sql b/sql/mysql/tenant_remove.sql index c179895b..1088c46d 100644 --- a/sql/mysql/tenant_remove.sql +++ b/sql/mysql/tenant_remove.sql @@ -36,6 +36,10 @@ ALTER TABLE `sys_user` ADD UNIQUE KEY `login_name` (`username`); ALTER TABLE `sys_role` DROP INDEX `idx_tenant_id`; ALTER TABLE `sys_role` DROP COLUMN `tenant_id`; +-- 角色菜单关联表 +ALTER TABLE `sys_role_menu` DROP INDEX `idx_role_menu_tenant_id`; +ALTER TABLE `sys_role_menu` DROP COLUMN `tenant_id`; + -- 部门表 ALTER TABLE `sys_dept` DROP INDEX `idx_tenant_id`; ALTER TABLE `sys_dept` DROP COLUMN `tenant_id`; diff --git a/src/main/java/com/youlai/boot/auth/controller/AuthController.java b/src/main/java/com/youlai/boot/auth/controller/AuthController.java index ddfe2773..e94ecc31 100644 --- a/src/main/java/com/youlai/boot/auth/controller/AuthController.java +++ b/src/main/java/com/youlai/boot/auth/controller/AuthController.java @@ -46,7 +46,6 @@ public class AuthController { private final UserService userService; private final TenantService tenantService; private final TenantProperties tenantProperties; - private final PasswordEncoder passwordEncoder; @Operation(summary = "获取验证码") @GetMapping("/captcha") @@ -75,8 +74,8 @@ public class AuthController { return Result.success(authenticationToken); } - // 多租户模式:未指定租户ID,查询该用户名在所有租户下的记录 - List users = userService.listUsersByUsername(username); + // 多租户模式:未指定租户ID,查询该用户名在所有租户下的账户 + List users = userService.findUserAcrossAllTenants(username); if (users.isEmpty()) { return Result.failed("用户不存在"); diff --git a/src/main/java/com/youlai/boot/config/property/TenantProperties.java b/src/main/java/com/youlai/boot/config/property/TenantProperties.java index 66c9c04e..a28a94fc 100644 --- a/src/main/java/com/youlai/boot/config/property/TenantProperties.java +++ b/src/main/java/com/youlai/boot/config/property/TenantProperties.java @@ -48,18 +48,5 @@ public class TenantProperties { */ private String headerName = "tenant-id"; - /** - * 初始化默认忽略的表 - */ - public TenantProperties() { - // 系统表默认忽略多租户 - ignoreTables.add("sys_tenant"); - ignoreTables.add("sys_dict"); - ignoreTables.add("sys_dict_item"); - ignoreTables.add("sys_config"); - // 代码生成表(平台共用,不做租户隔离) - ignoreTables.add("gen_table"); - ignoreTables.add("gen_table_column"); - } } diff --git a/src/main/java/com/youlai/boot/security/service/PermissionService.java b/src/main/java/com/youlai/boot/security/service/PermissionService.java index 0ebd87e3..1fc3dab1 100644 --- a/src/main/java/com/youlai/boot/security/service/PermissionService.java +++ b/src/main/java/com/youlai/boot/security/service/PermissionService.java @@ -3,6 +3,8 @@ package com.youlai.boot.security.service; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.StrUtil; import com.youlai.boot.common.constant.RedisConstants; +import com.youlai.boot.common.tenant.TenantContextHolder; +import com.youlai.boot.config.property.TenantProperties; import com.youlai.boot.security.util.SecurityUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -24,6 +26,7 @@ import java.util.*; public class PermissionService { private final RedisTemplate redisTemplate; + private final TenantProperties tenantProperties; /** * 判断当前登录用户是否拥有操作权限 @@ -67,21 +70,36 @@ public class PermissionService { /** - * 从缓存中获取角色权限列表 + * 构建租户权限缓存key + * + * @param tenantId 租户ID + * @return 缓存key + */ + private String buildRolePermsCacheKey(Long tenantId) { + if (!tenantProperties.getEnabled() || tenantId == null) { + return RedisConstants.System.ROLE_PERMS; + } + return RedisConstants.System.ROLE_PERMS + ":" + tenantId; + } + + /** + * 从缓存中获取角色权限列表(兼容单租户和多租户) * * @param roleCodes 角色编码集合 * @return 角色权限列表 */ public Set getRolePermsFormCache(Set roleCodes) { - // 检查输入是否为空 if (CollectionUtil.isEmpty(roleCodes)) { return Collections.emptySet(); } + // 获取当前租户ID并构建缓存Key + Long tenantId = TenantContextHolder.getTenantId(); + String cacheKey = buildRolePermsCacheKey(tenantId); + Set perms = new HashSet<>(); - // 从缓存中一次性获取所有角色的权限 Collection roleCodesAsObjects = new ArrayList<>(roleCodes); - List rolePermsList = redisTemplate.opsForHash().multiGet(RedisConstants.System.ROLE_PERMS, roleCodesAsObjects); + List rolePermsList = redisTemplate.opsForHash().multiGet(cacheKey, roleCodesAsObjects); for (Object rolePermsObj : rolePermsList) { if (rolePermsObj instanceof Set) { diff --git a/src/main/java/com/youlai/boot/system/controller/TenantController.java b/src/main/java/com/youlai/boot/system/controller/TenantController.java index f7cdf190..c899661f 100644 --- a/src/main/java/com/youlai/boot/system/controller/TenantController.java +++ b/src/main/java/com/youlai/boot/system/controller/TenantController.java @@ -43,12 +43,12 @@ public class TenantController { * * @return 租户列表 */ - @Operation(summary = "获取当前用户的租户列表") + @Operation(summary = "获取当前用户可访问的租户列表") @GetMapping - public Result> getTenantList() { + public Result> getAccessibleTenants() { Long userId = SecurityUtils.getUserId(); - List tenantList = tenantService.getTenantListByUserId(userId); - log.info("获取用户 {} 的租户列表,共 {} 个租户", userId, tenantList.size()); + List tenantList = tenantService.getAccessibleTenants(userId); + log.debug("用户 {} 可访问 {} 个租户", userId, tenantList.size()); return Result.success(tenantList); } @@ -88,11 +88,10 @@ public class TenantController { log.info("用户 {} 请求切换租户:{} -> {}", userId, fromTenantId, tenantId); - // 验证用户是否有权限访问该租户 - boolean hasPermission = tenantService.hasTenantPermission(userId, tenantId); - if (!hasPermission) { - log.warn("用户 {} 无权限访问租户 {}", userId, tenantId); - return Result.failed("无权限访问该租户"); + // 验证用户是否可以访问该租户 + if (!tenantService.canAccessTenant(userId, tenantId)) { + log.warn("用户 {} 无权访问租户 {}", userId, tenantId); + return Result.failed("无权访问该租户"); } // 验证租户是否存在且正常 diff --git a/src/main/java/com/youlai/boot/system/model/bo/RolePermsBO.java b/src/main/java/com/youlai/boot/system/model/bo/RolePermsBO.java index 198e8180..6deca02f 100644 --- a/src/main/java/com/youlai/boot/system/model/bo/RolePermsBO.java +++ b/src/main/java/com/youlai/boot/system/model/bo/RolePermsBO.java @@ -13,6 +13,11 @@ import java.util.Set; @Data public class RolePermsBO { + /** + * 租户ID + */ + private Long tenantId; + /** * 角色编码 */ diff --git a/src/main/java/com/youlai/boot/system/model/entity/User.java b/src/main/java/com/youlai/boot/system/model/entity/User.java index 3bea52be..b49e4a05 100644 --- a/src/main/java/com/youlai/boot/system/model/entity/User.java +++ b/src/main/java/com/youlai/boot/system/model/entity/User.java @@ -18,11 +18,13 @@ public class User extends BaseEntity { */ private String username; + /** * 昵称 */ private String nickname; + /** * 性别((1-男 2-女 0-保密) */ diff --git a/src/main/java/com/youlai/boot/system/service/TenantService.java b/src/main/java/com/youlai/boot/system/service/TenantService.java index 2d265cea..c1421fcb 100644 --- a/src/main/java/com/youlai/boot/system/service/TenantService.java +++ b/src/main/java/com/youlai/boot/system/service/TenantService.java @@ -15,12 +15,15 @@ import java.util.List; public interface TenantService extends IService { /** - * 根据用户ID查询用户所属的租户列表 + * 获取用户可访问的租户列表 + *

+ * 通过用户名查询该用户在所有租户下的账户,返回可访问的租户列表 + *

* * @param userId 用户ID - * @return 租户列表 + * @return 可访问的租户列表 */ - List getTenantListByUserId(Long userId); + List getAccessibleTenants(Long userId); /** * 根据租户ID查询租户信息 @@ -39,11 +42,14 @@ public interface TenantService extends IService { Long getTenantIdByDomain(String domain); /** - * 验证用户是否有权限访问指定租户 + * 检查用户是否可以访问指定租户 + *

+ * 验证该用户名在目标租户下是否存在账户 + *

* * @param userId 用户ID * @param tenantId 租户ID - * @return true-有权限,false-无权限 + * @return true-可访问,false-不可访问 */ - boolean hasTenantPermission(Long userId, Long tenantId); + boolean canAccessTenant(Long userId, Long tenantId); } diff --git a/src/main/java/com/youlai/boot/system/service/UserService.java b/src/main/java/com/youlai/boot/system/service/UserService.java index 89cb5bd7..2e3528e9 100644 --- a/src/main/java/com/youlai/boot/system/service/UserService.java +++ b/src/main/java/com/youlai/boot/system/service/UserService.java @@ -83,12 +83,15 @@ public interface UserService extends IService { UserAuthCredentials getAuthCredentialsByUsernameAndTenant(String username, Long tenantId); /** - * 根据用户名查询该用户在所有租户下的记录(用于多租户登录时判断是否需要选择租户) + * 跨租户查询用户账户列表 + *

+ * 查询该用户名在所有租户下的账户记录,用于多租户登录时判断是否需要选择租户 + *

* * @param username 用户名 - * @return 用户列表(每个租户一条记录) + * @return 用户账户列表(每个租户一条记录) */ - List listUsersByUsername(String username); + List findUserAcrossAllTenants(String username); /** diff --git a/src/main/java/com/youlai/boot/system/service/impl/RoleMenuServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/RoleMenuServiceImpl.java index 39465cc1..9c51f9a0 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/RoleMenuServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/RoleMenuServiceImpl.java @@ -3,10 +3,11 @@ package com.youlai.boot.system.service.impl; import cn.hutool.core.collection.CollectionUtil; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.youlai.boot.common.constant.RedisConstants; +import com.youlai.boot.common.tenant.TenantContextHolder; +import com.youlai.boot.config.property.TenantProperties; import com.youlai.boot.system.mapper.RoleMenuMapper; import com.youlai.boot.system.model.bo.RolePermsBO; import com.youlai.boot.system.model.entity.RoleMenu; -import com.youlai.boot.common.constant.SecurityConstants; import com.youlai.boot.system.service.RoleMenuService; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; @@ -18,7 +19,7 @@ import java.util.List; import java.util.Set; /** - * 角色菜单服务实现类 + * 角色菜单服务实现类(多租户优化版) * * @author Ray.Hao * @since 2.5.0 @@ -29,76 +30,159 @@ import java.util.Set; public class RoleMenuServiceImpl extends ServiceImpl implements RoleMenuService { private final RedisTemplate redisTemplate; + private final TenantProperties tenantProperties; /** - * 初始化权限缓存 + * 构建租户权限缓存key + * + * @param tenantId 租户ID + * @return 缓存key + * - 多租户开启: system:role:perms:{tenantId} + * - 多租户关闭: system:role:perms */ - @PostConstruct - public void initRolePermsCache() { - log.info("初始化权限缓存... "); - refreshRolePermsCache(); + private String buildRolePermsCacheKey(Long tenantId) { + // 判断是否启用多租户 + if (!tenantProperties.getEnabled() || tenantId == null) { + // 单租户模式或多租户未开启:使用原有Key + return RedisConstants.System.ROLE_PERMS; + } + // 多租户模式开启:Key按租户隔离 + return RedisConstants.System.ROLE_PERMS + ":" + tenantId; } /** - * 刷新权限缓存 + * 启动时初始化权限缓存 + */ + @PostConstruct + public void initRolePermsCache() { + log.info("开始初始化权限缓存..."); + + List allRolePermsList = this.baseMapper.getRolePermsList(null); + + if (CollectionUtil.isEmpty(allRolePermsList)) { + log.warn("权限数据为空,跳过缓存初始化"); + return; + } + + if (tenantProperties.getEnabled()) { + // 多租户模式:按租户分组缓存 + allRolePermsList.forEach(rolePerms -> { + Long tenantId = rolePerms.getTenantId(); + if (tenantId == null) { + log.warn("多租户模式下,角色[{}]缺少tenantId,跳过", rolePerms.getRoleCode()); + return; + } + String cacheKey = RedisConstants.System.ROLE_PERMS + ":" + tenantId; + String roleCode = rolePerms.getRoleCode(); + Set perms = rolePerms.getPerms(); + + if (CollectionUtil.isNotEmpty(perms)) { + redisTemplate.opsForHash().put(cacheKey, roleCode, perms); + } + }); + log.info("权限缓存初始化完成(多租户模式),共{}条数据", allRolePermsList.size()); + } else { + // 单租户模式:所有数据统一缓存 + String cacheKey = RedisConstants.System.ROLE_PERMS; + allRolePermsList.forEach(rolePerms -> { + String roleCode = rolePerms.getRoleCode(); + Set perms = rolePerms.getPerms(); + + if (CollectionUtil.isNotEmpty(perms)) { + redisTemplate.opsForHash().put(cacheKey, roleCode, perms); + } + }); + log.info("权限缓存初始化完成(单租户模式),共{}条数据", allRolePermsList.size()); + } + } + + /** + * 刷新当前租户权限缓存 */ @Override public void refreshRolePermsCache() { - // 清理权限缓存 - redisTemplate.opsForHash().delete(RedisConstants.System.ROLE_PERMS, "*"); - + Long tenantId = TenantContextHolder.getTenantId(); + String cacheKey = buildRolePermsCacheKey(tenantId); + + // 清理当前租户权限缓存 + redisTemplate.delete(cacheKey); + + // 重新加载当前租户权限 List list = this.baseMapper.getRolePermsList(null); if (CollectionUtil.isNotEmpty(list)) { list.forEach(item -> { String roleCode = item.getRoleCode(); Set perms = item.getPerms(); if (CollectionUtil.isNotEmpty(perms)) { - redisTemplate.opsForHash().put(RedisConstants.System.ROLE_PERMS, roleCode, perms); + redisTemplate.opsForHash().put(cacheKey, roleCode, perms); } }); } - } - - /** - * 刷新权限缓存 - */ - @Override - public void refreshRolePermsCache(String roleCode) { - // 清理权限缓存 - redisTemplate.opsForHash().delete(RedisConstants.System.ROLE_PERMS, roleCode); - - List list = this.baseMapper.getRolePermsList(roleCode); - if (CollectionUtil.isNotEmpty(list)) { - RolePermsBO rolePerms = list.get(0); - if (rolePerms == null) { - return; - } - - Set perms = rolePerms.getPerms(); - if (CollectionUtil.isNotEmpty(perms)) { - redisTemplate.opsForHash().put(RedisConstants.System.ROLE_PERMS, roleCode, perms); - } + + if (tenantId == null) { + log.info("权限缓存刷新完成(单租户模式)"); + } else { + log.info("租户[{}]权限缓存刷新完成", tenantId); } } /** - * 刷新权限缓存 (角色编码变更时调用) + * 刷新单个角色权限缓存 + */ + @Override + public void refreshRolePermsCache(String roleCode) { + Long tenantId = TenantContextHolder.getTenantId(); + String cacheKey = buildRolePermsCacheKey(tenantId); + + // 清理指定角色缓存 + redisTemplate.opsForHash().delete(cacheKey, roleCode); + + // 重新加载指定角色权限 + List list = this.baseMapper.getRolePermsList(roleCode); + if (CollectionUtil.isNotEmpty(list)) { + RolePermsBO rolePerms = list.get(0); + if (rolePerms != null) { + Set perms = rolePerms.getPerms(); + if (CollectionUtil.isNotEmpty(perms)) { + redisTemplate.opsForHash().put(cacheKey, roleCode, perms); + } + } + } + + if (tenantId == null) { + log.info("角色[{}]权限缓存刷新完成(单租户模式)", roleCode); + } else { + log.info("租户[{}]角色[{}]权限缓存刷新完成", tenantId, roleCode); + } + } + + /** + * 刷新权限缓存(角色编码变更时调用) */ @Override public void refreshRolePermsCache(String oldRoleCode, String newRoleCode) { + Long tenantId = TenantContextHolder.getTenantId(); + String cacheKey = buildRolePermsCacheKey(tenantId); + // 清理旧角色权限缓存 - redisTemplate.opsForHash().delete(RedisConstants.System.ROLE_PERMS, oldRoleCode); - + redisTemplate.opsForHash().delete(cacheKey, oldRoleCode); + // 添加新角色权限缓存 List list = this.baseMapper.getRolePermsList(newRoleCode); if (CollectionUtil.isNotEmpty(list)) { RolePermsBO rolePerms = list.get(0); - if (rolePerms == null) { - return; + if (rolePerms != null) { + Set perms = rolePerms.getPerms(); + if (CollectionUtil.isNotEmpty(perms)) { + redisTemplate.opsForHash().put(cacheKey, newRoleCode, perms); + } } - - Set perms = rolePerms.getPerms(); - redisTemplate.opsForHash().put(RedisConstants.System.ROLE_PERMS, newRoleCode, perms); + } + + if (tenantId == null) { + log.info("角色编码变更: {} -> {},权限缓存已更新(单租户模式)", oldRoleCode, newRoleCode); + } else { + log.info("租户[{}]角色编码变更: {} -> {},权限缓存已更新", tenantId, oldRoleCode, newRoleCode); } } @@ -110,6 +194,7 @@ public class RoleMenuServiceImpl extends ServiceImpl i */ @Override public Set getRolePermsByRoleCodes(Set roles) { + // 直接查询数据库(保持原有逻辑) return this.baseMapper.listRolePerms(roles); } 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 index b4dfed9c..072e2b0c 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/TenantServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/TenantServiceImpl.java @@ -32,7 +32,7 @@ public class TenantServiceImpl extends ServiceImpl impleme private final UserMapper userMapper; @Override - public List getTenantListByUserId(Long userId) { + public List getAccessibleTenants(Long userId) { // 临时忽略租户过滤,查询所有租户 TenantContextHolder.setIgnoreTenant(true); try { @@ -123,7 +123,7 @@ public class TenantServiceImpl extends ServiceImpl impleme } @Override - public boolean hasTenantPermission(Long userId, Long tenantId) { + public boolean canAccessTenant(Long userId, Long tenantId) { TenantContextHolder.setIgnoreTenant(true); try { // 先根据用户ID查询用户信息(获取 username) diff --git a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java index b81bba24..78626fb1 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java @@ -271,15 +271,15 @@ public class UserServiceImpl extends ServiceImpl implements Us } @Override - public List listUsersByUsername(String username) { - // 临时忽略租户过滤,查询该用户名在所有租户下的记录 + public List findUserAcrossAllTenants(String username) { + // 临时忽略租户过滤,查询该用户名在所有租户下的账户记录 TenantContextHolder.setIgnoreTenant(true); try { return this.list( new LambdaQueryWrapper() .eq(User::getUsername, username) .eq(User::getIsDeleted, 0) - .orderByAsc(User::getTenantId) // 按租户ID排序,优先返回较小的租户ID + .orderByAsc(User::getTenantId) ); } finally { TenantContextHolder.setIgnoreTenant(false); diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index cee52f9e..c93e4fa0 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -288,9 +288,11 @@ youlai: # 忽略多租户过滤的表名列表(系统表、租户表等不需要租户隔离的表) ignore-tables: - - sys_tenant # 租户表本身 - - sys_menu # 菜单表(功能入口定义,所有租户共享) - - sys_dict # 字典表(通常共享) - - sys_dict_item # 字典项表(通常共享) - - sys_config # 系统配置表(通常共享) + - sys_tenant # 租户表本身 + - sys_menu # 菜单表(功能入口定义,所有租户共享) + - sys_dict # 字典表(通常共享) + - sys_dict_item # 字典项表(通常共享) + - sys_config # 系统配置表(通常共享) + - gen_table # 代码生成表(平台共用) + - gen_table_column # 代码生成字段表(平台共用) # ============================================ \ No newline at end of file diff --git a/src/main/resources/mapper/system/RoleMenuMapper.xml b/src/main/resources/mapper/system/RoleMenuMapper.xml index 11fc527d..6fc89ea0 100644 --- a/src/main/resources/mapper/system/RoleMenuMapper.xml +++ b/src/main/resources/mapper/system/RoleMenuMapper.xml @@ -17,6 +17,7 @@ + @@ -26,8 +27,9 @@ SELECT - t2.tenant_id, t2.`code` role_code, t3.perm FROM From 8eaed3cfb791e25939aa45b834e730a7ef359cfd Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Thu, 18 Dec 2025 09:43:36 +0800 Subject: [PATCH 15/22] =?UTF-8?q?refactor:=20=E5=AE=9E=E4=BD=93=E5=91=BD?= =?UTF-8?q?=E5=90=8D=E8=A7=84=E8=8C=83=E8=B0=83=E6=95=B4=EF=BC=8C=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E7=94=9F=E6=88=90=E5=90=8C=E6=AD=A5=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- sql/mysql/youlai_admin.sql | 48 +-- .../boot/auth/controller/AuthController.java | 18 +- .../auth/model/dto/WxMiniAppCodeLoginDTO.java | 2 +- .../model/dto/WxMiniAppPhoneLoginDTO.java | 2 +- .../youlai/boot/auth/model/vo/CaptchaVO.java | 2 +- .../youlai/boot/auth/service/AuthService.java | 16 +- .../auth/service/impl/AuthServiceImpl.java | 31 +- .../com/youlai/boot/common/base/BaseVO.java | 21 -- .../com/youlai/boot/core/web/ResultCode.java | 4 +- .../ai/controller/AiAssistantController.java | 109 ++++++ .../ai/controller/AiCommandController.java | 93 ----- .../ai/mapper/AiAssistantRecordMapper.java | 22 ++ .../ai/mapper/AiCommandRecordMapper.java | 26 -- .../ai/model/dto/AiExecuteRequestDTO.java | 9 +- .../ai/model/dto/AiExecuteResponseDTO.java | 7 +- .../ai/model/dto/AiFunctionCallDTO.java | 4 +- .../ai/model/dto/AiParseRequestDTO.java | 5 +- .../ai/model/dto/AiParseResponseDTO.java | 7 +- .../ai/model/entity/AiAssistantRecord.java | 80 +++++ .../ai/model/entity/AiCommandRecord.java | 82 ----- .../ai/model/query/AiAssistantPageQuery.java | 44 +++ .../ai/model/query/AiCommandPageQuery.java | 45 --- .../ai/model/vo/AiAssistantRecordVo.java | 91 +++++ .../ai/model/vo/AiCommandRecordVO.java | 92 ----- .../ai/service/AiAssistantRecordService.java | 66 ++++ .../ai/service/AiCommandRecordService.java | 33 -- .../platform/ai/service/AiCommandService.java | 32 -- .../impl/AiAssistantRecordServiceImpl.java | 321 +++++++++++++++++ .../impl/AiCommandRecordServiceImpl.java | 50 --- .../ai/service/impl/AiCommandServiceImpl.java | 324 ------------------ .../codegen/controller/CodegenController.java | 12 +- .../platform/codegen/enums/JavaTypeEnum.java | 35 +- .../codegen/mapper/DatabaseMapper.java | 4 +- .../codegen/model/bo/ColumnMetaData.java | 2 +- .../codegen/model/vo/CodegenPreviewVO.java | 4 +- .../codegen/model/vo/TablePageVO.java | 2 +- .../codegen/service/CodegenService.java | 8 +- .../service/impl/CodegenServiceImpl.java | 101 ++++-- .../service/impl/GenTableServiceImpl.java | 5 +- .../controller/WebsocketController.java | 9 +- .../websocket/dto/DictChangeEvent.java | 15 + .../ChatMessage.java => dto/TextMessage.java} | 16 +- .../publisher/WebSocketPublisher.java | 61 ++++ .../service/impl/WebSocketServiceImpl.java | 262 ++------------ .../session/UserSessionRegistry.java | 103 ++++++ .../websocket/topic/WebSocketTopics.java | 15 + .../boot/security/model/SysUserDetails.java | 5 +- .../security/model/UserAuthCredentials.java | 45 +-- .../boot/security/model/UserAuthInfo.java | 58 ++++ .../provider/SmsAuthenticationProvider.java | 11 +- .../WxMiniAppCodeAuthenticationProvider.java | 17 +- .../WxMiniAppPhoneAuthenticationProvider.java | 14 +- .../service/SysUserDetailsService.java | 8 +- .../system/controller/ConfigController.java | 6 +- .../system/controller/DeptController.java | 6 +- .../system/controller/DictController.java | 18 +- .../boot/system/controller/LogController.java | 31 +- .../system/controller/MenuController.java | 12 +- .../system/controller/NoticeController.java | 20 +- .../system/controller/RoleController.java | 6 +- .../controller/StatisticsController.java | 47 +++ .../system/controller/UserController.java | 30 +- .../system/converter/ConfigConverter.java | 4 +- .../boot/system/converter/DeptConverter.java | 4 +- .../boot/system/converter/DictConverter.java | 4 +- .../system/converter/DictItemConverter.java | 4 +- .../boot/system/converter/MenuConverter.java | 4 +- .../system/converter/NoticeConverter.java | 12 +- .../boot/system/converter/RoleConverter.java | 4 +- .../boot/system/converter/UserConverter.java | 20 +- .../system/enums/NoticePublishStatusEnum.java | 2 +- .../system/handler/OnlineUserJobHandler.java | 7 +- .../system/listener/UserImportListener.java | 24 +- .../boot/system/mapper/DictItemMapper.java | 4 +- .../youlai/boot/system/mapper/DictMapper.java | 4 +- .../youlai/boot/system/mapper/LogMapper.java | 16 +- .../boot/system/mapper/NoticeMapper.java | 8 +- .../boot/system/mapper/RoleMenuMapper.java | 4 +- .../youlai/boot/system/mapper/UserMapper.java | 30 +- .../boot/system/mapper/UserNoticeMapper.java | 6 +- .../boot/system/mapper/UserRoleMapper.java | 2 +- .../youlai/boot/system/model/bo/NoticeBO.java | 2 +- .../boot/system/model/bo/RolePermsBO.java | 2 +- .../youlai/boot/system/model/bo/UserBO.java | 4 +- .../bo/{VisitCount.java => VisitCountBo.java} | 2 +- .../boot/system/model/bo/VisitStatsBO.java | 2 +- .../boot/system/model/dto/CurrentUserDTO.java | 2 +- .../boot/system/model/dto/DictEventDTO.java | 28 -- .../boot/system/model/dto/NoticeDTO.java | 3 +- .../boot/system/model/dto/UserExportDTO.java | 2 +- .../boot/system/model/dto/UserImportDTO.java | 2 +- .../boot/system/model/dto/UserSessionDTO.java | 6 +- .../youlai/boot/system/model/vo/ConfigVO.java | 4 +- .../youlai/boot/system/model/vo/DeptVO.java | 4 +- .../system/model/vo/DictItemOptionVO.java | 2 +- .../boot/system/model/vo/DictItemPageVO.java | 2 +- .../boot/system/model/vo/DictPageVO.java | 4 +- .../boot/system/model/vo/LogPageVO.java | 6 +- .../youlai/boot/system/model/vo/MenuVO.java | 4 +- .../boot/system/model/vo/NoticeDetailVO.java | 4 +- .../boot/system/model/vo/NoticePageVO.java | 2 +- .../boot/system/model/vo/RolePageVO.java | 2 +- .../youlai/boot/system/model/vo/RouteVO.java | 4 +- .../system/model/vo/UserNoticePageVO.java | 6 +- .../boot/system/model/vo/UserPageVO.java | 2 +- .../boot/system/model/vo/UserProfileVO.java | 2 +- .../boot/system/model/vo/VisitStatsVO.java | 2 +- .../boot/system/model/vo/VisitTrendVO.java | 6 +- .../boot/system/service/ConfigService.java | 4 +- .../boot/system/service/DeptService.java | 4 +- .../boot/system/service/DictItemService.java | 8 +- .../boot/system/service/DictService.java | 6 +- .../boot/system/service/LogService.java | 12 +- .../boot/system/service/MenuService.java | 10 +- .../boot/system/service/NoticeService.java | 12 +- .../boot/system/service/RoleService.java | 4 +- .../system/service/UserNoticeService.java | 6 +- .../system/service/UserOnlineService.java | 34 +- .../boot/system/service/UserService.java | 50 ++- .../service/WebSocketMessageService.java | 123 ------- .../service/impl/ConfigServiceImpl.java | 4 +- .../system/service/impl/DeptServiceImpl.java | 14 +- .../service/impl/DictItemServiceImpl.java | 20 +- .../system/service/impl/DictServiceImpl.java | 4 +- .../system/service/impl/LogServiceImpl.java | 33 +- .../system/service/impl/MenuServiceImpl.java | 52 +-- .../service/impl/NoticeServiceImpl.java | 47 +-- .../service/impl/RoleMenuServiceImpl.java | 14 +- .../system/service/impl/RoleServiceImpl.java | 6 +- .../service/impl/UserNoticeServiceImpl.java | 6 +- .../service/impl/UserRoleServiceImpl.java | 9 +- .../system/service/impl/UserServiceImpl.java | 84 ++--- src/main/resources/codegen.yml | 7 +- ...Mapper.xml => AiAssistantRecordMapper.xml} | 74 ++-- .../mapper/codegen/DatabaseMapper.xml | 4 +- .../mapper/system/DictItemMapper.xml | 2 +- .../resources/mapper/system/DictMapper.xml | 2 +- .../resources/mapper/system/LogMapper.xml | 16 +- .../resources/mapper/system/NoticeMapper.xml | 4 +- .../mapper/system/RoleMenuMapper.xml | 2 +- .../resources/mapper/system/UserMapper.xml | 18 +- .../mapper/system/UserNoticeMapper.xml | 2 +- .../mapper/system/UserRoleMapper.xml | 2 +- .../templates/codegen/api-types.ts.vm | 47 +++ .../resources/templates/codegen/api.ts.vm | 59 +--- .../templates/codegen/controller.java.vm | 28 +- .../templates/codegen/index.curd.vue.vm | 11 +- .../resources/templates/codegen/index.vue.vm | 13 +- .../templates/codegen/mapper.java.vm | 6 +- .../resources/templates/codegen/mapper.xml.vm | 2 +- .../templates/codegen/service.java.vm | 6 +- .../templates/codegen/serviceImpl.java.vm | 18 +- .../resources/templates/codegen/vo.java.vm | 2 +- .../boot/generator/SystemCodeGenerator.java | 8 +- src/test/resources/templates/bo.java.vm | 8 +- .../resources/templates/controller.java.vm | 6 +- .../resources/templates/converter.java.vm | 10 +- src/test/resources/templates/dto.java.vm | 10 +- src/test/resources/templates/mapper.java.vm | 4 +- src/test/resources/templates/mapper.xml.vm | 2 +- src/test/resources/templates/pageVo.java.vm | 10 +- src/test/resources/templates/service.java.vm | 4 +- .../resources/templates/serviceImpl.java.vm | 12 +- src/test/resources/templates/vo.java.vm | 8 +- 165 files changed, 1885 insertions(+), 2038 deletions(-) delete mode 100644 src/main/java/com/youlai/boot/common/base/BaseVO.java create mode 100644 src/main/java/com/youlai/boot/platform/ai/controller/AiAssistantController.java delete mode 100644 src/main/java/com/youlai/boot/platform/ai/controller/AiCommandController.java create mode 100644 src/main/java/com/youlai/boot/platform/ai/mapper/AiAssistantRecordMapper.java delete mode 100644 src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandRecordMapper.java create mode 100644 src/main/java/com/youlai/boot/platform/ai/model/entity/AiAssistantRecord.java delete mode 100644 src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandRecord.java create mode 100644 src/main/java/com/youlai/boot/platform/ai/model/query/AiAssistantPageQuery.java delete mode 100644 src/main/java/com/youlai/boot/platform/ai/model/query/AiCommandPageQuery.java create mode 100644 src/main/java/com/youlai/boot/platform/ai/model/vo/AiAssistantRecordVo.java delete mode 100644 src/main/java/com/youlai/boot/platform/ai/model/vo/AiCommandRecordVO.java create mode 100644 src/main/java/com/youlai/boot/platform/ai/service/AiAssistantRecordService.java delete mode 100644 src/main/java/com/youlai/boot/platform/ai/service/AiCommandRecordService.java delete mode 100644 src/main/java/com/youlai/boot/platform/ai/service/AiCommandService.java create mode 100644 src/main/java/com/youlai/boot/platform/ai/service/impl/AiAssistantRecordServiceImpl.java delete mode 100644 src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandRecordServiceImpl.java delete mode 100644 src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandServiceImpl.java create mode 100644 src/main/java/com/youlai/boot/platform/websocket/dto/DictChangeEvent.java rename src/main/java/com/youlai/boot/platform/websocket/{model/ChatMessage.java => dto/TextMessage.java} (52%) create mode 100644 src/main/java/com/youlai/boot/platform/websocket/publisher/WebSocketPublisher.java create mode 100644 src/main/java/com/youlai/boot/platform/websocket/session/UserSessionRegistry.java create mode 100644 src/main/java/com/youlai/boot/platform/websocket/topic/WebSocketTopics.java create mode 100644 src/main/java/com/youlai/boot/security/model/UserAuthInfo.java create mode 100644 src/main/java/com/youlai/boot/system/controller/StatisticsController.java rename src/main/java/com/youlai/boot/system/model/bo/{VisitCount.java => VisitCountBo.java} (90%) delete mode 100644 src/main/java/com/youlai/boot/system/model/dto/DictEventDTO.java delete mode 100644 src/main/java/com/youlai/boot/system/service/WebSocketMessageService.java rename src/main/resources/mapper/ai/{AiCommandRecordMapper.xml => AiAssistantRecordMapper.xml} (62%) create mode 100644 src/main/resources/templates/codegen/api-types.ts.vm diff --git a/Dockerfile b/Dockerfile index 842060dd..b9d182a5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM openjdk:17 # 维护者信息 -MAINTAINER youlai +LABEL maintainer="youlai " # 设置时区(Debian直接使用环境变量) ENV TZ=Asia/Shanghai diff --git a/sql/mysql/youlai_admin.sql b/sql/mysql/youlai_admin.sql index 6aa3c3f7..4b0d1f95 100644 --- a/sql/mysql/youlai_admin.sql +++ b/sql/mysql/youlai_admin.sql @@ -138,8 +138,8 @@ CREATE TABLE `sys_menu` ( -- ---------------------------- -- 顶级目录(1-9):系统/代码生成/AI助手/文档/接口文档/组件/演示/多级/路由 INSERT INTO `sys_menu` VALUES (1, 0, '0', '系统管理', 'C', '', '/system', 'Layout', NULL, NULL, NULL, 1, 1, 'system', '/system/user', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (2, 0, '0', '代码生成', 'C', '', '/gen', 'Layout', NULL, NULL, NULL, 1, 2, 'code', '/gen/index', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (3, 0, '0', 'AI助手', 'C', '', '/ai', 'Layout', NULL, NULL, NULL, 1, 3, 'platform', '/ai/command-record', now(), now(), NULL); +INSERT INTO `sys_menu` VALUES (2, 0, '0', '代码生成', 'C', '', '/codegen', 'Layout', NULL, NULL, NULL, 1, 2, 'code', '/codegen/index', now(), now(), NULL); +INSERT INTO `sys_menu` VALUES (3, 0, '0', 'AI助手', 'C', '', '/ai', 'Layout', NULL, NULL, NULL, 1, 3, 'ai', '/ai/command-record', now(), now(), NULL); INSERT INTO `sys_menu` VALUES (4, 0, '0', '平台文档', 'C', '', '/doc', 'Layout', NULL, NULL, NULL, 1, 4, 'document', '', now(), now(), NULL); INSERT INTO `sys_menu` VALUES (5, 0, '0', '接口文档', 'C', '', '/api', 'Layout', NULL, NULL, NULL, 1, 5, 'api', '', now(), now(), NULL); INSERT INTO `sys_menu` VALUES (6, 0, '0', '组件封装', 'C', '', '/component', 'Layout', NULL, NULL, NULL, 1, 6, 'menu', '', now(), now(), NULL); @@ -205,7 +205,7 @@ INSERT INTO `sys_menu` VALUES (2805, 280, '0,1,280', '通知发布', 'B', NULL, INSERT INTO `sys_menu` VALUES (2806, 280, '0,1,280', '通知撤回', 'B', NULL, '', NULL, 'sys:notice:revoke', 0, 1, 1, 6, '', NULL, now(), now(), NULL); -- 代码生成 -INSERT INTO `sys_menu` VALUES (310, 2, '0,2', '代码生成', 'M', 'Gen', 'gen', 'gen/index', NULL, NULL, 1, 1, 1, 'code', NULL, now(), now(), NULL); +INSERT INTO `sys_menu` VALUES (310, 2, '0,2', '代码生成', 'M', 'Codegen', 'codegen', 'codegen/index', NULL, NULL, 1, 1, 1, 'code', NULL, now(), now(), NULL); -- AI 助手 INSERT INTO `sys_menu` VALUES (401, 3, '0,3', 'AI命令记录', 'M', 'AiCommandRecord', 'command-record', 'ai/command-record/index', NULL, NULL, 1, 1, 1, 'document', NULL, now(), now(), NULL); @@ -537,8 +537,8 @@ 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_assistant_record`; +CREATE TABLE `ai_assistant_record` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `user_id` bigint DEFAULT NULL COMMENT '用户ID', `username` varchar(64) DEFAULT NULL COMMENT '用户名', @@ -565,40 +565,6 @@ CREATE TABLE `ai_command_record` ( KEY `idx_create_time` (`create_time`), KEY `idx_provider` (`ai_provider`), KEY `idx_model` (`ai_model`), - KEY `idx_parse_success` (`parse_status`), + KEY `idx_parse_status` (`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()); - - - -SET FOREIGN_KEY_CHECKS = 1; +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='AI 助手行为记录表(解析、执行、审计)'; diff --git a/src/main/java/com/youlai/boot/auth/controller/AuthController.java b/src/main/java/com/youlai/boot/auth/controller/AuthController.java index 1cb5a579..61dc05e0 100644 --- a/src/main/java/com/youlai/boot/auth/controller/AuthController.java +++ b/src/main/java/com/youlai/boot/auth/controller/AuthController.java @@ -1,12 +1,12 @@ package com.youlai.boot.auth.controller; -import com.youlai.boot.auth.model.vo.CaptchaVO; +import com.youlai.boot.auth.model.vo.CaptchaVo; import com.youlai.boot.auth.model.dto.LoginRequest; -import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDTO; +import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDto; import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.core.web.Result; import com.youlai.boot.auth.service.AuthService; -import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO; +import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDto; import com.youlai.boot.common.annotation.Log; import com.youlai.boot.security.model.AuthenticationToken; import io.swagger.v3.oas.annotations.Operation; @@ -37,8 +37,8 @@ public class AuthController { @Operation(summary = "获取验证码") @GetMapping("/captcha") - public Result getCaptcha() { - CaptchaVO captcha = authService.getCaptcha(); + public Result getCaptcha() { + CaptchaVo captcha = authService.getCaptcha(); return Result.success(captcha); } @@ -84,15 +84,15 @@ public class AuthController { @Operation(summary = "微信小程序登录(Code)") @PostMapping("/wx/miniapp/code-login") - public Result loginByWxMiniAppCode(@RequestBody @Valid WxMiniAppCodeLoginDTO loginDTO) { - AuthenticationToken token = authService.loginByWxMiniAppCode(loginDTO); + public Result loginByWxMiniAppCode(@RequestBody @Valid WxMiniAppCodeLoginDto loginDto) { + AuthenticationToken token = authService.loginByWxMiniAppCode(loginDto); return Result.success(token); } @Operation(summary = "微信小程序登录(手机号)") @PostMapping("/wx/miniapp/phone-login") - public Result loginByWxMiniAppPhone(@RequestBody @Valid WxMiniAppPhoneLoginDTO loginDTO) { - AuthenticationToken token = authService.loginByWxMiniAppPhone(loginDTO); + public Result loginByWxMiniAppPhone(@RequestBody @Valid WxMiniAppPhoneLoginDto loginDto) { + AuthenticationToken token = authService.loginByWxMiniAppPhone(loginDto); return Result.success(token); } diff --git a/src/main/java/com/youlai/boot/auth/model/dto/WxMiniAppCodeLoginDTO.java b/src/main/java/com/youlai/boot/auth/model/dto/WxMiniAppCodeLoginDTO.java index 69c653fb..b31c41c8 100644 --- a/src/main/java/com/youlai/boot/auth/model/dto/WxMiniAppCodeLoginDTO.java +++ b/src/main/java/com/youlai/boot/auth/model/dto/WxMiniAppCodeLoginDTO.java @@ -13,7 +13,7 @@ import jakarta.validation.constraints.NotBlank; */ @Schema(description = "微信小程序Code登录请求参数") @Data -public class WxMiniAppCodeLoginDTO { +public class WxMiniAppCodeLoginDto { @Schema(description = "微信小程序登录时获取的code", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank(message = "code不能为空") diff --git a/src/main/java/com/youlai/boot/auth/model/dto/WxMiniAppPhoneLoginDTO.java b/src/main/java/com/youlai/boot/auth/model/dto/WxMiniAppPhoneLoginDTO.java index a881f271..48c7ccea 100644 --- a/src/main/java/com/youlai/boot/auth/model/dto/WxMiniAppPhoneLoginDTO.java +++ b/src/main/java/com/youlai/boot/auth/model/dto/WxMiniAppPhoneLoginDTO.java @@ -13,7 +13,7 @@ import jakarta.validation.constraints.NotBlank; */ @Schema(description = "微信小程序手机号登录请求参数") @Data -public class WxMiniAppPhoneLoginDTO { +public class WxMiniAppPhoneLoginDto { @Schema(description = "微信小程序登录时获取的code", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank(message = "code不能为空") diff --git a/src/main/java/com/youlai/boot/auth/model/vo/CaptchaVO.java b/src/main/java/com/youlai/boot/auth/model/vo/CaptchaVO.java index a81fb3c6..f7db81e2 100644 --- a/src/main/java/com/youlai/boot/auth/model/vo/CaptchaVO.java +++ b/src/main/java/com/youlai/boot/auth/model/vo/CaptchaVO.java @@ -13,7 +13,7 @@ import lombok.Data; @Schema(description = "验证码信息") @Data @Builder -public class CaptchaVO { +public class CaptchaVo { @Schema(description = "验证码缓存 ID") private String captchaId; diff --git a/src/main/java/com/youlai/boot/auth/service/AuthService.java b/src/main/java/com/youlai/boot/auth/service/AuthService.java index 5fe2eadf..f0b6811c 100644 --- a/src/main/java/com/youlai/boot/auth/service/AuthService.java +++ b/src/main/java/com/youlai/boot/auth/service/AuthService.java @@ -1,9 +1,9 @@ package com.youlai.boot.auth.service; -import com.youlai.boot.auth.model.vo.CaptchaVO; -import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDTO; +import com.youlai.boot.auth.model.vo.CaptchaVo; +import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDto; import com.youlai.boot.security.model.AuthenticationToken; -import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO; +import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDto; /** * 认证服务接口 @@ -32,7 +32,7 @@ public interface AuthService { * * @return 验证码 */ - CaptchaVO getCaptcha(); + CaptchaVo getCaptcha(); /** * 刷新令牌 @@ -53,18 +53,18 @@ public interface AuthService { /** * 微信小程序Code登录 * - * @param loginDTO 登录参数 + * @param loginDto 登录参数 * @return 访问令牌 */ - AuthenticationToken loginByWxMiniAppCode(WxMiniAppCodeLoginDTO loginDTO); + AuthenticationToken loginByWxMiniAppCode(WxMiniAppCodeLoginDto loginDto); /** * 微信小程序手机号登录 * - * @param loginDTO 登录参数 + * @param loginDto 登录参数 * @return 访问令牌 */ - AuthenticationToken loginByWxMiniAppPhone(WxMiniAppPhoneLoginDTO loginDTO); + AuthenticationToken loginByWxMiniAppPhone(WxMiniAppPhoneLoginDto loginDto); /** * 发送短信验证码 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 dc32af31..2070efd4 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 @@ -5,9 +5,9 @@ import cn.hutool.captcha.CaptchaUtil; import cn.hutool.captcha.generator.CodeGenerator; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; -import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO; -import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDTO; -import com.youlai.boot.auth.model.vo.CaptchaVO; +import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDto; +import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDto; +import com.youlai.boot.auth.model.vo.CaptchaVo; import com.youlai.boot.auth.service.AuthService; import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.SecurityConstants; @@ -70,6 +70,11 @@ public class AuthServiceImpl implements AuthService { new UsernamePasswordAuthenticationToken(username.trim(), password); // 2. 执行认证(认证中) + // 说明:这里的认证流程由 Spring Security 提供的 AuthenticationManager 执行。 + // 默认情况下会委托给 DaoAuthenticationProvider: + // 1) retrieveUser(...):内部通过 UserDetailsService.loadUserByUsername(...) 获取用户信息(本项目为 SysUserDetailsService 实现) + // 2) additionalAuthenticationChecks(...):对比请求密码与用户存储密码(由 PasswordEncoder 完成匹配) + // 认证通过后返回已认证的 Authentication(principal 为 SysUserDetails,authorities 为角色/权限集合)。 Authentication authentication = authenticationManager.authenticate(authenticationToken); // 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证) @@ -166,7 +171,7 @@ public class AuthServiceImpl implements AuthService { * @return 验证码 */ @Override - public CaptchaVO getCaptcha() { + public CaptchaVo getCaptcha() { String captchaType = captchaProperties.getType(); int width = captchaProperties.getWidth(); @@ -202,7 +207,7 @@ public class AuthServiceImpl implements AuthService { TimeUnit.SECONDS ); - return CaptchaVO.builder() + return CaptchaVo.builder() .captchaId(captchaId) .captchaBase64(imageBase64Data) .build(); @@ -222,13 +227,13 @@ public class AuthServiceImpl implements AuthService { /** * 微信小程序Code登录 * - * @param loginDTO 登录参数 + * @param loginDto 登录参数 * @return 访问令牌 */ @Override - public AuthenticationToken loginByWxMiniAppCode(WxMiniAppCodeLoginDTO loginDTO) { + public AuthenticationToken loginByWxMiniAppCode(WxMiniAppCodeLoginDto loginDto) { // 1. 创建微信小程序认证令牌(未认证) - WxMiniAppCodeAuthenticationToken authenticationToken = new WxMiniAppCodeAuthenticationToken(loginDTO.getCode()); + WxMiniAppCodeAuthenticationToken authenticationToken = new WxMiniAppCodeAuthenticationToken(loginDto.getCode()); // 2. 执行认证(认证中) Authentication authentication = authenticationManager.authenticate(authenticationToken); @@ -243,16 +248,16 @@ public class AuthServiceImpl implements AuthService { /** * 微信小程序手机号登录 * - * @param loginDTO 登录参数 + * @param loginDto 登录参数 * @return 访问令牌 */ @Override - public AuthenticationToken loginByWxMiniAppPhone(WxMiniAppPhoneLoginDTO loginDTO) { + public AuthenticationToken loginByWxMiniAppPhone(WxMiniAppPhoneLoginDto loginDto) { // 创建微信小程序手机号认证Token WxMiniAppPhoneAuthenticationToken authenticationToken = new WxMiniAppPhoneAuthenticationToken( - loginDTO.getCode(), - loginDTO.getEncryptedData(), - loginDTO.getIv() + loginDto.getCode(), + loginDto.getEncryptedData(), + loginDto.getIv() ); // 执行认证 diff --git a/src/main/java/com/youlai/boot/common/base/BaseVO.java b/src/main/java/com/youlai/boot/common/base/BaseVO.java deleted file mode 100644 index bd1f540f..00000000 --- a/src/main/java/com/youlai/boot/common/base/BaseVO.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.youlai.boot.common.base; - -import lombok.Data; -import lombok.ToString; - -import java.io.Serial; -import java.io.Serializable; - -/** - * 视图对象基类 - * - * @author haoxr - * @since 2022/10/22 - */ -@Data -@ToString -public class BaseVO implements Serializable { - - @Serial - private static final long serialVersionUID = 1L; -} diff --git a/src/main/java/com/youlai/boot/core/web/ResultCode.java b/src/main/java/com/youlai/boot/core/web/ResultCode.java index d7ea897b..f40f506c 100644 --- a/src/main/java/com/youlai/boot/core/web/ResultCode.java +++ b/src/main/java/com/youlai/boot/core/web/ResultCode.java @@ -43,7 +43,7 @@ public enum ResultCode implements IResultCode, Serializable { VERIFICATION_CODE_INPUT_ERROR("A0130", "校验码输入错误"), SMS_VERIFICATION_CODE_INPUT_ERROR("A0131", "短信校验码输入错误"), EMAIL_VERIFICATION_CODE_INPUT_ERROR("A0132", "邮件校验码输入错误"), - VOICE_VERIFICATION_CODE_INPUT_ERROR("A0133", "语音校验码输入错误"), + VoICE_VERIFICATION_CODE_INPUT_ERROR("A0133", "语音校验码输入错误"), USER_CERTIFICATE_EXCEPTION("A0140", "用户证件异常"), USER_CERTIFICATE_TYPE_NOT_SELECTED("A0141", "用户证件类型未选择"), @@ -262,7 +262,7 @@ public enum ResultCode implements IResultCode, Serializable { /** 二级宏观错误码 */ NOTIFICATION_SERVICE_ERROR("C0500", "通知服务出错"), SMS_REMINDER_SERVICE_FAILED("C0501", "短信提醒服务失败"), - VOICE_REMINDER_SERVICE_FAILED("C0502", "语音提醒服务失败"), + VoICE_REMINDER_SERVICE_FAILED("C0502", "语音提醒服务失败"), EMAIL_REMINDER_SERVICE_FAILED("C0503", "邮件提醒服务失败"); diff --git a/src/main/java/com/youlai/boot/platform/ai/controller/AiAssistantController.java b/src/main/java/com/youlai/boot/platform/ai/controller/AiAssistantController.java new file mode 100644 index 00000000..72744ed2 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/controller/AiAssistantController.java @@ -0,0 +1,109 @@ +package com.youlai.boot.platform.ai.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.youlai.boot.core.web.PageResult; +import com.youlai.boot.core.web.Result; +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.AiAssistantPageQuery; +import com.youlai.boot.platform.ai.model.vo.AiAssistantRecordVo; +import com.youlai.boot.platform.ai.service.AiAssistantRecordService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * AI 助手控制器 + *

+ * 负责 AI 命令的解析、执行、记录管理及回滚操作, + * 表示一次 AI 助手完整的指令生命周期。 + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Tag(name = "AI 助手接口") +@RestController +@RequestMapping("/api/v1/ai/assistant") +@RequiredArgsConstructor +@Slf4j +public class AiAssistantController { + + private final AiAssistantRecordService aiAssistantRecordService; + + @Operation(summary = "解析自然语言命令") + @PostMapping("/parse") + public Result parseCommand( + @RequestBody AiParseRequestDto request, + HttpServletRequest httpRequest + ) { + log.info("收到 AI 命令解析请求: {}", request.getCommand()); + try { + AiParseResponseDto response = aiAssistantRecordService.parseCommand(request, httpRequest); + return Result.success(response); + } catch (Exception e) { + log.error("命令解析失败", e); + return Result.success(AiParseResponseDto.builder() + .success(false) + .error(e.getMessage()) + .build()); + } + } + + @Operation(summary = "执行已解析的命令") + @PostMapping("/execute") + public Result executeCommand( + @RequestBody AiExecuteRequestDto request, + HttpServletRequest httpRequest + ) { + log.info("收到 AI 命令执行请求: {}", request.getFunctionCall().getName()); + try { + Object result = aiAssistantRecordService.executeCommand(request, httpRequest); + return Result.success(result); + } catch (Exception e) { + log.error("命令执行失败", e); + return Result.failed(e.getMessage()); + } + } + + @Operation(summary = "获取 AI 命令记录分页列表") + @GetMapping("/records") + public PageResult getRecordPage(AiAssistantPageQuery queryParams) { + IPage page = aiAssistantRecordService.getRecordPage(queryParams); + return PageResult.success(page); + } + + @Operation(summary = "删除 AI 命令记录") + @DeleteMapping("/records/{ids}") + public Result deleteRecords( + @Parameter(description = "记录ID,多个以英文逗号(,)分割") + @PathVariable String ids + ) { + List idList = Arrays.stream(ids.split(",")) + .filter(s -> s != null && !s.isBlank()) + .map(String::trim) + .map(Long::valueOf) + .collect(Collectors.toList()); + + boolean removed = aiAssistantRecordService.deleteRecords(idList); + return Result.judge(removed); + } + + @Operation(summary = "撤销命令执行") + @PostMapping("/records/{recordId}/rollback") + public Result rollbackCommand( + @Parameter(description = "记录ID") + @PathVariable String recordId + ) { + aiAssistantRecordService.rollbackCommand(recordId); + return Result.success(); + } +} 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 deleted file mode 100644 index 464dfc19..00000000 --- a/src/main/java/com/youlai/boot/platform/ai/controller/AiCommandController.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.youlai.boot.platform.ai.controller; - -import com.baomidou.mybatisplus.core.metadata.IPage; -import com.youlai.boot.core.web.PageResult; -import com.youlai.boot.core.web.Result; -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.service.AiCommandService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletRequest; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.*; - -/** - * AI 命令控制器(基于 Spring AI) - * - * @author Ray.Hao - * @since 3.0.0 - */ -@Tag(name = "AI命令接口") -@RestController -@RequestMapping("/api/v1/ai/command") -@RequiredArgsConstructor -@Slf4j -public class AiCommandController { - - private final AiCommandService aiCommandService; - private final AiCommandRecordService recordService; - - @Operation(summary = "解析自然语言命令") - @PostMapping("/parse") - public Result parseCommand( - @RequestBody AiParseRequestDTO request, - HttpServletRequest httpRequest - ) { - log.info("收到AI命令解析请求: {}", request.getCommand()); - - try { - AiParseResponseDTO response = aiCommandService.parseCommand(request, httpRequest); - return Result.success(response); - } catch (Exception e) { - log.error("命令解析失败", e); - return Result.success(AiParseResponseDTO.builder() - .success(false) - .error(e.getMessage()) - .build()); - } - } - - @Operation(summary = "执行已解析的命令") - @PostMapping("/execute") - public Result executeCommand( - @RequestBody AiExecuteRequestDTO request, - HttpServletRequest httpRequest - ) { - log.info("收到AI命令执行请求: {}", request.getFunctionCall().getName()); - try { - Object result = aiCommandService.executeCommand(request, httpRequest); - return Result.success(result); - } catch (Exception e) { - log.error("命令执行失败", e); - return Result.failed(e.getMessage()); - } - } - - @Operation(summary = "获取AI命令记录分页列表") - @GetMapping("/records") - public PageResult getRecordPage(AiCommandPageQuery queryParams) { - IPage page = recordService.getRecordPage(queryParams); - return PageResult.success(page); - } - - @Operation(summary = "撤销命令执行") - @PostMapping("/rollback/{recordId}") - public Result rollbackCommand( - @Parameter(description = "记录ID") @PathVariable String recordId - ) { - recordService.rollbackCommand(recordId); - return Result.success("撤销成功"); - } - -} - - - - diff --git a/src/main/java/com/youlai/boot/platform/ai/mapper/AiAssistantRecordMapper.java b/src/main/java/com/youlai/boot/platform/ai/mapper/AiAssistantRecordMapper.java new file mode 100644 index 00000000..b5131587 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/mapper/AiAssistantRecordMapper.java @@ -0,0 +1,22 @@ +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.AiAssistantRecord; +import com.youlai.boot.platform.ai.model.query.AiAssistantPageQuery; +import com.youlai.boot.platform.ai.model.vo.AiAssistantRecordVo; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface AiAssistantRecordMapper extends BaseMapper { + + /** + * AI 助手行为记录分页列表 + * + * @param page 分页参数 + * @param queryParams 查询参数 + * @return 分页结果 + */ + IPage getRecordPage(Page page, AiAssistantPageQuery queryParams); +} diff --git a/src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandRecordMapper.java b/src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandRecordMapper.java deleted file mode 100644 index e26bb6c6..00000000 --- a/src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandRecordMapper.java +++ /dev/null @@ -1,26 +0,0 @@ -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.query.AiCommandPageQuery; -import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO; -import org.apache.ibatis.annotations.Mapper; - -/** - * AI 命令记录 Mapper - * - * @author Ray.Hao - * @since 3.0.0 - */ -@Mapper -public interface AiCommandRecordMapper extends BaseMapper { - - /** - * 获取 AI 命令记录分页列表 - */ - IPage getRecordPage(Page page, AiCommandPageQuery queryParams); -} - - diff --git a/src/main/java/com/youlai/boot/platform/ai/model/dto/AiExecuteRequestDTO.java b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiExecuteRequestDTO.java index 0dc54317..80a3058e 100644 --- a/src/main/java/com/youlai/boot/platform/ai/model/dto/AiExecuteRequestDTO.java +++ b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiExecuteRequestDTO.java @@ -3,13 +3,13 @@ package com.youlai.boot.platform.ai.model.dto; import lombok.Data; /** - * AI 命令执行请求 DTO + * AI 命令执行请求 Dto * * @author Ray.Hao * @since 3.0.0 */ @Data -public class AiExecuteRequestDTO { +public class AiExecuteRequestDto { /** * 关联的解析日志ID @@ -24,7 +24,7 @@ public class AiExecuteRequestDTO { /** * 要执行的函数调用 */ - private AiFunctionCallDTO functionCall; + private AiFunctionCallDto functionCall; /** * 确认模式:auto=自动执行, manual=需要用户确认 @@ -46,6 +46,3 @@ public class AiExecuteRequestDTO { */ private String currentRoute; } - - - diff --git a/src/main/java/com/youlai/boot/platform/ai/model/dto/AiExecuteResponseDTO.java b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiExecuteResponseDTO.java index 8796e58e..aa2c0371 100644 --- a/src/main/java/com/youlai/boot/platform/ai/model/dto/AiExecuteResponseDTO.java +++ b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiExecuteResponseDTO.java @@ -6,7 +6,7 @@ import lombok.Data; import lombok.NoArgsConstructor; /** - * AI 命令执行响应 DTO + * AI 命令执行响应 Dto * * @author Ray.Hao * @since 3.0.0 @@ -15,7 +15,7 @@ import lombok.NoArgsConstructor; @Builder @NoArgsConstructor @AllArgsConstructor -public class AiExecuteResponseDTO { +public class AiExecuteResponseDto { /** * 是否执行成功 @@ -57,6 +57,3 @@ public class AiExecuteResponseDTO { */ private String confirmationPrompt; } - - - diff --git a/src/main/java/com/youlai/boot/platform/ai/model/dto/AiFunctionCallDTO.java b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiFunctionCallDTO.java index 2b2368b1..c5aab5b1 100644 --- a/src/main/java/com/youlai/boot/platform/ai/model/dto/AiFunctionCallDTO.java +++ b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiFunctionCallDTO.java @@ -7,7 +7,7 @@ import lombok.NoArgsConstructor; import java.util.Map; /** - * AI 函数调用 DTO + * AI 函数调用 Dto * * @author Ray.Hao * @since 3.0.0 @@ -16,7 +16,7 @@ import java.util.Map; @Builder @NoArgsConstructor @AllArgsConstructor -public class AiFunctionCallDTO { +public class AiFunctionCallDto { /** * 函数名称 diff --git a/src/main/java/com/youlai/boot/platform/ai/model/dto/AiParseRequestDTO.java b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiParseRequestDTO.java index 4578a28a..35d7038a 100644 --- a/src/main/java/com/youlai/boot/platform/ai/model/dto/AiParseRequestDTO.java +++ b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiParseRequestDTO.java @@ -4,13 +4,13 @@ import lombok.Data; import java.util.Map; /** - * AI 解析请求 DTO + * AI 解析请求 Dto * * @author Ray.Hao * @since 3.0.0 */ @Data -public class AiParseRequestDTO { +public class AiParseRequestDto { /** * 用户输入的自然语言命令 @@ -32,4 +32,3 @@ public class AiParseRequestDTO { */ private Map context; } - diff --git a/src/main/java/com/youlai/boot/platform/ai/model/dto/AiParseResponseDTO.java b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiParseResponseDTO.java index 239dacc5..e8c9c372 100644 --- a/src/main/java/com/youlai/boot/platform/ai/model/dto/AiParseResponseDTO.java +++ b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiParseResponseDTO.java @@ -7,7 +7,7 @@ import lombok.NoArgsConstructor; import java.util.List; /** - * AI 解析响应 DTO + * AI 解析响应 Dto * * @author Ray.Hao * @since 3.0.0 @@ -16,7 +16,7 @@ import java.util.List; @Builder @NoArgsConstructor @AllArgsConstructor -public class AiParseResponseDTO { +public class AiParseResponseDto { /** * 解析日志ID(用于关联执行记录) @@ -31,7 +31,7 @@ public class AiParseResponseDTO { /** * 解析后的函数调用列表 */ - private List functionCalls; + private List functionCalls; /** * AI 的理解和说明 @@ -53,4 +53,3 @@ public class AiParseResponseDTO { */ private String rawResponse; } - diff --git a/src/main/java/com/youlai/boot/platform/ai/model/entity/AiAssistantRecord.java b/src/main/java/com/youlai/boot/platform/ai/model/entity/AiAssistantRecord.java new file mode 100644 index 00000000..6fa37b12 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/model/entity/AiAssistantRecord.java @@ -0,0 +1,80 @@ +package com.youlai.boot.platform.ai.model.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.youlai.boot.common.base.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; + +/** + * AI 助手行为记录实体 + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("ai_assistant_record") +public class AiAssistantRecord extends BaseEntity { + + /** 用户ID */ + private Long userId; + + /** 用户名 */ + private String username; + + /** 原始命令 */ + private String originalCommand; + + // ==================== 解析相关字段 ==================== + + /** AI 供应商(qwen/openai/deepseek等) */ + private String aiProvider; + + /** AI 模型(qwen-plus/qwen-max/gpt-4-turbo等) */ + private String aiModel; + + /** 解析状态(0-失败, 1-成功) */ + private Integer parseStatus; + + /** 解析出的函数调用列表(JSON) */ + private String functionCalls; + + /** AI 的理解说明 */ + private String explanation; + + /** 置信度(0.00-1.00) */ + private BigDecimal confidence; + + /** 解析错误信息 */ + private String parseErrorMessage; + + /** 输入 Token 数量 */ + private Integer inputTokens; + + /** 输出 Token 数量 */ + private Integer outputTokens; + + /** 解析耗时(毫秒) */ + private Integer parseDurationMs; + + // ==================== 执行相关字段 ==================== + + /** 执行的函数名称 */ + private String functionName; + + /** 函数参数(JSON) */ + private String functionArguments; + + /** 执行状态(0-待执行, 1-成功, -1-失败) */ + private Integer executeStatus; + + /** 执行错误信息 */ + private String executeErrorMessage; + + // ==================== 通用字段 ==================== + + /** IP 地址 */ + private String ipAddress; +} 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/AiCommandRecord.java deleted file mode 100644 index 1750970b..00000000 --- a/src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandRecord.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.youlai.boot.platform.ai.model.entity; - -import com.baomidou.mybatisplus.annotation.TableName; -import com.youlai.boot.common.base.BaseEntity; -import lombok.Data; -import lombok.EqualsAndHashCode; - -import java.math.BigDecimal; - -/** - * AI 命令记录实体 - * - * @author Ray.Hao - * @since 3.0.0 - */ -@Data -@EqualsAndHashCode(callSuper = true) -@TableName("ai_command_record") -public class AiCommandRecord extends BaseEntity { - - /** 用户ID */ - private Long userId; - - /** 用户名 */ - private String username; - - /** 原始命令 */ - private String originalCommand; - - // ==================== 解析相关字段 ==================== - - /** AI 供应商(qwen/openai/deepseek等) */ - private String aiProvider; - - /** AI 模型(qwen-plus/qwen-max/gpt-4-turbo等) */ - private String aiModel; - - /** 解析状态(0-失败, 1-成功) */ - private Integer parseStatus; - - /** 解析出的函数调用列表(JSON) */ - private String functionCalls; - - /** AI 的理解说明 */ - private String explanation; - - /** 置信度(0.00-1.00) */ - private BigDecimal confidence; - - /** 解析错误信息 */ - private String parseErrorMessage; - - /** 输入 Token 数量 */ - private Integer inputTokens; - - /** 输出 Token 数量 */ - private Integer outputTokens; - - /** 解析耗时(毫秒) */ - private Integer parseDurationMs; - - // ==================== 执行相关字段 ==================== - - /** 执行的函数名称 */ - private String functionName; - - /** 函数参数(JSON) */ - private String functionArguments; - - /** 执行状态(0-待执行, 1-成功, -1-失败) */ - private Integer executeStatus; - - /** 执行错误信息 */ - private String executeErrorMessage; - - // ==================== 通用字段 ==================== - - /** IP 地址 */ - private String ipAddress; -} - - diff --git a/src/main/java/com/youlai/boot/platform/ai/model/query/AiAssistantPageQuery.java b/src/main/java/com/youlai/boot/platform/ai/model/query/AiAssistantPageQuery.java new file mode 100644 index 00000000..4d5cc552 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/model/query/AiAssistantPageQuery.java @@ -0,0 +1,44 @@ +package com.youlai.boot.platform.ai.model.query; + +import com.youlai.boot.common.base.BasePageQuery; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * AI 助手行为记录分页查询对象 + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Schema(description = "AI 助手行为记录分页查询对象") +@Getter +@Setter +public class AiAssistantPageQuery extends BasePageQuery { + + @Schema(description = "关键字(原始命令/函数名称/用户名)") + private String keywords; + + @Schema(description = "执行状态(0-待执行, 1-成功, -1-失败)") + private Integer executeStatus; + + @Schema(description = "用户ID") + private Long userId; + + @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/query/AiCommandPageQuery.java b/src/main/java/com/youlai/boot/platform/ai/model/query/AiCommandPageQuery.java deleted file mode 100644 index 0322b09e..00000000 --- a/src/main/java/com/youlai/boot/platform/ai/model/query/AiCommandPageQuery.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.youlai.boot.platform.ai.model.query; - -import com.youlai.boot.common.base.BasePageQuery; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Getter; -import lombok.Setter; - -import java.util.List; - -/** - * AI命令记录分页查询对象 - * - * @author Ray.Hao - * @since 3.0.0 - */ -@Schema(description = "AI命令记录分页查询对象") -@Getter -@Setter -public class AiCommandPageQuery extends BasePageQuery { - - @Schema(description = "关键字(原始命令/函数名称/用户名)") - private String keywords; - - @Schema(description = "执行状态(0-待执行, 1-成功, -1-失败)") - private Integer executeStatus; - - @Schema(description = "用户ID") - private Long userId; - - @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/AiAssistantRecordVo.java b/src/main/java/com/youlai/boot/platform/ai/model/vo/AiAssistantRecordVo.java new file mode 100644 index 00000000..e72f4129 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/model/vo/AiAssistantRecordVo.java @@ -0,0 +1,91 @@ +package com.youlai.boot.platform.ai.model.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * AI 助手行为记录Vo + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Data +@Schema(description = "AI 助手行为记录Vo") +public class AiAssistantRecordVo implements Serializable { + + @Schema(description = "主键ID") + private String id; + + @Schema(description = "用户ID") + private Long userId; + + @Schema(description = "用户名") + private String username; + + @Schema(description = "原始命令") + private String originalCommand; + + // ==================== 解析相关字段 ==================== + + @Schema(description = "AI供应商") + private String aiProvider; + + @Schema(description = "AI模型") + private String aiModel; + + @Schema(description = "解析状态(0-失败, 1-成功)") + private Integer parseStatus; + + @Schema(description = "解析出的函数调用列表(JSON)") + private String functionCalls; + + @Schema(description = "AI的理解说明") + private String explanation; + + @Schema(description = "置信度") + private BigDecimal confidence; + + @Schema(description = "解析错误信息") + private String parseErrorMessage; + + @Schema(description = "输入Token数量") + private Integer inputTokens; + + @Schema(description = "输出Token数量") + private Integer outputTokens; + + @Schema(description = "解析耗时(毫秒)") + private Integer parseDurationMs; + + // ==================== 执行相关字段 ==================== + + @Schema(description = "执行的函数名称") + private String functionName; + + @Schema(description = "函数参数(JSON)") + private String functionArguments; + + @Schema(description = "执行状态(0-待执行, 1-成功, -1-失败)") + private Integer executeStatus; + + @Schema(description = "执行错误信息") + private String executeErrorMessage; + + // ==================== 通用字段 ==================== + + @Schema(description = "IP地址") + private String ipAddress; + + @Schema(description = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updateTime; +} 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/AiCommandRecordVO.java deleted file mode 100644 index 088c5f9c..00000000 --- a/src/main/java/com/youlai/boot/platform/ai/model/vo/AiCommandRecordVO.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.youlai.boot.platform.ai.model.vo; - -import com.fasterxml.jackson.annotation.JsonFormat; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -import java.io.Serializable; -import java.math.BigDecimal; -import java.time.LocalDateTime; - -/** - * AI命令记录VO - * - * @author Ray.Hao - * @since 3.0.0 - */ -@Data -@Schema(description = "AI命令记录VO") -public class AiCommandRecordVO implements Serializable { - - @Schema(description = "主键ID") - private String id; - - @Schema(description = "用户ID") - private Long userId; - - @Schema(description = "用户名") - private String username; - - @Schema(description = "原始命令") - private String originalCommand; - - // ==================== 解析相关字段 ==================== - - @Schema(description = "AI供应商") - private String aiProvider; - - @Schema(description = "AI模型") - private String aiModel; - - @Schema(description = "解析状态(0-失败, 1-成功)") - private Integer parseStatus; - - @Schema(description = "解析出的函数调用列表(JSON)") - private String functionCalls; - - @Schema(description = "AI的理解说明") - private String explanation; - - @Schema(description = "置信度") - private BigDecimal confidence; - - @Schema(description = "解析错误信息") - private String parseErrorMessage; - - @Schema(description = "输入Token数量") - private Integer inputTokens; - - @Schema(description = "输出Token数量") - private Integer outputTokens; - - @Schema(description = "解析耗时(毫秒)") - private Integer parseDurationMs; - - // ==================== 执行相关字段 ==================== - - @Schema(description = "执行的函数名称") - private String functionName; - - @Schema(description = "函数参数(JSON)") - private String functionArguments; - - @Schema(description = "执行状态(0-待执行, 1-成功, -1-失败)") - private Integer executeStatus; - - @Schema(description = "执行错误信息") - private String executeErrorMessage; - - // ==================== 通用字段 ==================== - - @Schema(description = "IP地址") - private String ipAddress; - - @Schema(description = "创建时间") - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - private LocalDateTime createTime; - - @Schema(description = "更新时间") - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - private LocalDateTime updateTime; -} - diff --git a/src/main/java/com/youlai/boot/platform/ai/service/AiAssistantRecordService.java b/src/main/java/com/youlai/boot/platform/ai/service/AiAssistantRecordService.java new file mode 100644 index 00000000..2e47a84a --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/service/AiAssistantRecordService.java @@ -0,0 +1,66 @@ +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.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.entity.AiAssistantRecord; +import com.youlai.boot.platform.ai.model.query.AiAssistantPageQuery; +import com.youlai.boot.platform.ai.model.vo.AiAssistantRecordVo; +import jakarta.servlet.http.HttpServletRequest; + +import java.util.List; + +/** + * AI 助手行为记录服务接口 + * + * 负责 AI 助手指令的解析/执行审计记录的分页查询、删除与回滚。 + * + * @author Ray.Hao + * @since 3.0.0 + */ +public interface AiAssistantRecordService extends IService { + + /** + * 解析自然语言命令。 + * + * @param request 解析请求参数 + * @param httpRequest HTTP 请求(用于获取 IP 等上下文) + * @return 解析结果(包含 functionCalls 等信息) + */ + AiParseResponseDto parseCommand(AiParseRequestDto request, HttpServletRequest httpRequest); + + /** + * 执行已解析的命令。 + * + * @param request 执行请求参数 + * @param httpRequest HTTP 请求(用于获取 IP 等上下文) + * @return 执行结果 + * @throws Exception 执行异常 + */ + Object executeCommand(AiExecuteRequestDto request, HttpServletRequest httpRequest) throws Exception; + + /** + * 获取 AI 助手行为记录分页列表 + * + * @param queryParams 查询参数 + * @return 分页列表 + */ + IPage getRecordPage(AiAssistantPageQuery queryParams); + + /** + * 删除 AI 助手行为记录。 + * + * @param ids 记录ID列表 + * @return 是否删除成功 + */ + boolean deleteRecords(List ids); + + /** + * 撤销命令执行 + * + * @param logId 记录ID + */ + void rollbackCommand(String logId); +} diff --git a/src/main/java/com/youlai/boot/platform/ai/service/AiCommandRecordService.java b/src/main/java/com/youlai/boot/platform/ai/service/AiCommandRecordService.java deleted file mode 100644 index 5e94c87d..00000000 --- a/src/main/java/com/youlai/boot/platform/ai/service/AiCommandRecordService.java +++ /dev/null @@ -1,33 +0,0 @@ -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.query.AiCommandPageQuery; -import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO; - -/** - * AI 命令记录服务接口 - * - * @author Ray.Hao - * @since 3.0.0 - */ -public interface AiCommandRecordService extends IService { - - /** - * 获取命令记录分页列表 - * - * @param queryParams 查询参数 - * @return 命令记录分页列表 - */ - IPage getRecordPage(AiCommandPageQuery queryParams); - - /** - * 撤销命令执行 - * - * @param logId 记录ID - */ - void rollbackCommand(String logId); -} - - diff --git a/src/main/java/com/youlai/boot/platform/ai/service/AiCommandService.java b/src/main/java/com/youlai/boot/platform/ai/service/AiCommandService.java deleted file mode 100644 index 1f5011bf..00000000 --- a/src/main/java/com/youlai/boot/platform/ai/service/AiCommandService.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.youlai.boot.platform.ai.service; - -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 jakarta.servlet.http.HttpServletRequest; - -/** - * AI 命令编排服务:负责对外的解析与执行编排 - */ -public interface AiCommandService { - - /** - * 解析自然语言命令 - */ - AiParseResponseDTO parseCommand(AiParseRequestDTO request, HttpServletRequest httpRequest); - - /** - * 执行已解析的命令 - * - * @param request 执行请求 - * @param httpRequest HTTP 请求 - * @return 执行结果数据(成功时返回) - * @throws Exception 执行失败时抛出异常 - */ - Object executeCommand(AiExecuteRequestDTO request, HttpServletRequest httpRequest) throws Exception; -} - - - - - diff --git a/src/main/java/com/youlai/boot/platform/ai/service/impl/AiAssistantRecordServiceImpl.java b/src/main/java/com/youlai/boot/platform/ai/service/impl/AiAssistantRecordServiceImpl.java new file mode 100644 index 00000000..a56b9709 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/service/impl/AiAssistantRecordServiceImpl.java @@ -0,0 +1,321 @@ +package com.youlai.boot.platform.ai.service.impl; + +import cn.hutool.core.lang.TypeReference; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.servlet.JakartaServletUtil; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +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.AiAssistantRecordMapper; +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.AiAssistantRecord; +import com.youlai.boot.platform.ai.model.query.AiAssistantPageQuery; +import com.youlai.boot.platform.ai.model.vo.AiAssistantRecordVo; +import com.youlai.boot.platform.ai.service.AiAssistantRecordService; +import com.youlai.boot.platform.ai.tools.UserTools; +import com.youlai.boot.security.util.SecurityUtils; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * AI 助手行为记录服务实现类 + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Service +@Slf4j +@RequiredArgsConstructor +public class AiAssistantRecordServiceImpl + extends ServiceImpl + implements AiAssistantRecordService { + + private static final String SYSTEM_PROMPT = """ + 你是一个智能的企业操作助手,需要将用户的自然语言命令解析成标准的函数调用。 + 请返回严格的 JSON 格式,包含字段: + - success: boolean + - explanation: string + - confidence: number (0-1) + - error: string + - provider: string + - model: string + - functionCalls: 数组,每个元素包含 name、description、arguments(对象) + 当无法识别命令时,success=false,并给出 error。 + """; + + private final UserTools userTools; + private final ChatClient chatClient; + + @Override + public AiParseResponseDto parseCommand(AiParseRequestDto request, HttpServletRequest httpRequest) { + long startTime = System.currentTimeMillis(); + String command = Optional.ofNullable(request.getCommand()).orElse("").trim(); + + if (StrUtil.isBlank(command)) { + return AiParseResponseDto.builder() + .success(false) + .error("命令不能为空") + .functionCalls(Collections.emptyList()) + .build(); + } + + Long userId = SecurityUtils.getUserId(); + String username = SecurityUtils.getUsername(); + String ipAddress = JakartaServletUtil.getClientIP(httpRequest); + + AiAssistantRecord commandRecord = new AiAssistantRecord(); + commandRecord.setUserId(userId); + commandRecord.setUsername(username); + commandRecord.setOriginalCommand(command); + commandRecord.setIpAddress(ipAddress); + commandRecord.setAiProvider("spring-ai"); + commandRecord.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); + + commandRecord.setAiProvider(StrUtil.emptyToDefault(parseResult.provider(), "spring-ai")); + commandRecord.setAiModel(StrUtil.emptyToDefault(parseResult.model(), "auto")); + commandRecord.setParseStatus(parseResult.success() ? 1 : 0); + commandRecord.setExplanation(parseResult.explanation()); + commandRecord.setFunctionCalls(JSONUtil.toJsonStr(parseResult.functionCalls())); + commandRecord.setConfidence(parseResult.confidence() != null ? BigDecimal.valueOf(parseResult.confidence()) : null); + commandRecord.setParseErrorMessage(parseResult.success() ? null : StrUtil.emptyToDefault(parseResult.error(), "解析失败")); + long duration = System.currentTimeMillis() - startTime; + commandRecord.setParseDurationMs((int) duration); + + this.save(commandRecord); + + return AiParseResponseDto.builder() + .parseLogId(commandRecord.getId()) + .success(parseResult.success()) + .functionCalls(parseResult.functionCalls()) + .explanation(parseResult.explanation()) + .confidence(parseResult.confidence()) + .error(parseResult.error()) + .rawResponse(rawContent) + .build(); + } catch (Exception e) { + long duration = System.currentTimeMillis() - startTime; + commandRecord.setParseStatus(0); + commandRecord.setFunctionCalls(JSONUtil.toJsonStr(Collections.emptyList())); + commandRecord.setParseErrorMessage(e.getMessage()); + commandRecord.setParseDurationMs((int) duration); + this.save(commandRecord); + + 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> availableFunctions() { + return List.of( + Map.of( + "name", "updateUserNickname", + "description", "根据用户名更新用户昵称", + "requiredParameters", List.of("username", "nickname") + ) + ); + } + + private ParseResult parseAiResponse(String rawContent) { + if (StrUtil.isBlank(rawContent)) { + throw new IllegalStateException("AI 返回内容为空"); + } + + try { + JSONObject jsonObject = JSONUtil.parseObj(rawContent); + boolean success = jsonObject.getBool("success", false); + String explanation = jsonObject.getStr("explanation"); + Double confidence = jsonObject.containsKey("confidence") ? jsonObject.getDouble("confidence") : null; + String error = jsonObject.getStr("error"); + String provider = jsonObject.getStr("provider"); + String model = jsonObject.getStr("model"); + + List functionCalls = toFunctionCallList(jsonObject.getJSONArray("functionCalls")); + + return new ParseResult(success, explanation, confidence, error, provider, model, functionCalls); + } catch (Exception ex) { + throw new IllegalStateException("无法解析 AI 响应: " + ex.getMessage(), ex); + } + } + + private List toFunctionCallList(JSONArray array) { + if (array == null || array.isEmpty()) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + for (Object element : array) { + JSONObject functionJson = JSONUtil.parseObj(element); + Map arguments = Optional.ofNullable(functionJson.getJSONObject("arguments")) + .map(obj -> obj.toBean(new TypeReference>() { + })) + .orElse(Collections.emptyMap()); + + result.add(AiFunctionCallDto.builder() + .name(functionJson.getStr("name")) + .description(functionJson.getStr("description")) + .arguments(arguments) + .build()); + } + return result; + } + + private record ParseResult( + boolean success, + String explanation, + Double confidence, + String error, + String provider, + String model, + List functionCalls + ) { + } + + @Override + public Object executeCommand(AiExecuteRequestDto request, HttpServletRequest httpRequest) throws Exception { + Long userId = SecurityUtils.getUserId(); + String username = SecurityUtils.getUsername(); + String ipAddress = JakartaServletUtil.getClientIP(httpRequest); + + AiFunctionCallDto functionCall = request.getFunctionCall(); + + AiAssistantRecord commandRecord; + if (StrUtil.isNotBlank(request.getParseLogId())) { + commandRecord = this.getById(request.getParseLogId()); + if (commandRecord == null) { + throw new IllegalStateException("未找到对应的解析记录,ID: " + request.getParseLogId()); + } + } else { + commandRecord = new AiAssistantRecord(); + commandRecord.setUserId(userId); + commandRecord.setUsername(username); + commandRecord.setOriginalCommand(request.getOriginalCommand()); + commandRecord.setIpAddress(ipAddress); + this.save(commandRecord); + } + + commandRecord.setFunctionName(functionCall.getName()); + commandRecord.setFunctionArguments(JSONUtil.toJsonStr(functionCall.getArguments())); + commandRecord.setExecuteStatus(0); + + try { + Object result = executeFunctionCall(functionCall); + commandRecord.setExecuteStatus(1); + commandRecord.setExecuteErrorMessage(null); + this.updateById(commandRecord); + log.info("✅ 命令执行成功,审计记录ID: {}", commandRecord.getId()); + return result; + } catch (Exception e) { + commandRecord.setExecuteStatus(-1); + commandRecord.setExecuteErrorMessage(e.getMessage()); + this.updateById(commandRecord); + log.error("❌ 命令执行失败,审计记录ID: {}", commandRecord.getId(), e); + throw e; + } + } + + private Object executeFunctionCall(AiFunctionCallDto functionCall) { + String functionName = functionCall.getName(); + Map arguments = functionCall.getArguments(); + + log.info("🎯 执行函数: {}, 参数: {}", functionName, arguments); + + switch (functionName) { + case "updateUserNickname": + return executeUpdateUserNickname(arguments); + default: + throw new UnsupportedOperationException("不支持的函数: " + functionName); + } + } + + private Object executeUpdateUserNickname(Map arguments) { + String username = (String) arguments.get("username"); + String nickname = (String) arguments.get("nickname"); + + log.info("🔧 [Tool] 更新用户昵称: username={}, nickname={}", username, nickname); + String resultMsg = userTools.updateUserNickname(username, nickname); + + boolean success = resultMsg != null && resultMsg.contains("成功"); + if (!success) { + throw new RuntimeException(resultMsg != null ? resultMsg : "更新用户昵称失败"); + } + + return Map.of("username", username, "nickname", nickname, "message", resultMsg); + } + + @Override + public IPage getRecordPage(AiAssistantPageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + return this.baseMapper.getRecordPage(page, queryParams); + } + + @Override + public boolean deleteRecords(List ids) { + return this.removeByIds(ids); + } + + @Override + public void rollbackCommand(String logId) { + AiAssistantRecord commandRecord = this.getById(logId); + if (commandRecord == null) { + throw new RuntimeException("命令记录不存在"); + } + + if (commandRecord.getExecuteStatus() == null || commandRecord.getExecuteStatus() != 1) { + throw new RuntimeException("只能撤销成功执行的命令"); + } + + log.info("撤销命令执行: logId={}, function={}", logId, commandRecord.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 33df604c..00000000 --- a/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandRecordServiceImpl.java +++ /dev/null @@ -1,50 +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 命令记录服务实现类 - * - * @author Ray.Hao - * @since 3.0.0 - */ -@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 logId) { - AiCommandRecord commandRecord = this.getById(logId); - if (commandRecord == null) { - throw new RuntimeException("命令记录不存在"); - } - - if (commandRecord.getExecuteStatus() == null || commandRecord.getExecuteStatus() != 1) { - throw new RuntimeException("只能撤销成功执行的命令"); - } - - // TODO: 实现具体的回滚逻辑 - log.info("撤销命令执行: logId={}, function={}", logId, commandRecord.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 deleted file mode 100644 index c4b76980..00000000 --- a/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandServiceImpl.java +++ /dev/null @@ -1,324 +0,0 @@ -package com.youlai.boot.platform.ai.service.impl; - -import cn.hutool.core.lang.TypeReference; -import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.servlet.JakartaServletUtil; -import cn.hutool.json.JSONUtil; -import cn.hutool.json.JSONArray; -import cn.hutool.json.JSONObject; -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.service.AiCommandService; -import com.youlai.boot.platform.ai.tools.UserTools; -import com.youlai.boot.security.util.SecurityUtils; -import jakarta.servlet.http.HttpServletRequest; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.ai.chat.client.ChatClient; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.stereotype.Service; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -/** - * AI 命令编排服务实现 - */ -@Service -@Slf4j -@RequiredArgsConstructor -public class AiCommandServiceImpl implements AiCommandService { - - private static final String SYSTEM_PROMPT = """ - 你是一个智能的企业操作助手,需要将用户的自然语言命令解析成标准的函数调用。 - 请返回严格的 JSON 格式,包含字段: - - success: boolean - - explanation: string - - confidence: number (0-1) - - error: string - - provider: string - - model: string - - functionCalls: 数组,每个元素包含 name、description、arguments(对象) - 当无法识别命令时,success=false,并给出 error。 - """; - - private final AiCommandRecordService recordService; - private final UserTools userTools; - private final ChatClient chatClient; - - @Override - public AiParseResponseDTO parseCommand(AiParseRequestDTO request, HttpServletRequest httpRequest) { - long startTime = System.currentTimeMillis(); - String command = Optional.ofNullable(request.getCommand()).orElse("").trim(); - - if (StrUtil.isBlank(command)) { - return AiParseResponseDTO.builder() - .success(false) - .error("命令不能为空") - .functionCalls(Collections.emptyList()) - .build(); - } - - Long userId = SecurityUtils.getUserId(); - String username = SecurityUtils.getUsername(); - String ipAddress = JakartaServletUtil.getClientIP(httpRequest); - - AiCommandRecord commandRecord = new AiCommandRecord(); - commandRecord.setUserId(userId); - commandRecord.setUsername(username); - commandRecord.setOriginalCommand(command); - commandRecord.setIpAddress(ipAddress); - commandRecord.setAiProvider("spring-ai"); - commandRecord.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); - - commandRecord.setAiProvider(StrUtil.emptyToDefault(parseResult.provider(), "spring-ai")); - commandRecord.setAiModel(StrUtil.emptyToDefault(parseResult.model(), "auto")); - commandRecord.setParseStatus(parseResult.success() ? 1 : 0); - commandRecord.setExplanation(parseResult.explanation()); - commandRecord.setFunctionCalls(JSONUtil.toJsonStr(parseResult.functionCalls())); - commandRecord.setConfidence(parseResult.confidence() != null ? BigDecimal.valueOf(parseResult.confidence()) : null); - commandRecord.setParseErrorMessage(parseResult.success() ? null : StrUtil.emptyToDefault(parseResult.error(), "解析失败")); - long duration = System.currentTimeMillis() - startTime; - commandRecord.setParseDurationMs((int) duration); - - recordService.save(commandRecord); - - AiParseResponseDTO response = AiParseResponseDTO.builder() - .parseLogId(commandRecord.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: {}", commandRecord.getId()); - } - - return response; - } catch (Exception e) { - long duration = System.currentTimeMillis() - startTime; - commandRecord.setParseStatus(0); - commandRecord.setFunctionCalls(JSONUtil.toJsonStr(Collections.emptyList())); - commandRecord.setParseErrorMessage(e.getMessage()); - commandRecord.setParseDurationMs((int) duration); - recordService.save(commandRecord); - - 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> availableFunctions() { - return List.of( - Map.of( - "name", "updateUserNickname", - "description", "根据用户名更新用户昵称", - "requiredParameters", List.of("username", "nickname") - ) - ); - } - - private ParseResult parseAiResponse(String rawContent) { - if (StrUtil.isBlank(rawContent)) { - throw new IllegalStateException("AI 返回内容为空"); - } - - try { - JSONObject jsonObject = JSONUtil.parseObj(rawContent); - boolean success = jsonObject.getBool("success", false); - String explanation = jsonObject.getStr("explanation"); - Double confidence = jsonObject.containsKey("confidence") ? jsonObject.getDouble("confidence") : null; - String error = jsonObject.getStr("error"); - String provider = jsonObject.getStr("provider"); - String model = jsonObject.getStr("model"); - - List functionCalls = toFunctionCallList(jsonObject.getJSONArray("functionCalls")); - - return new ParseResult(success, explanation, confidence, error, provider, model, functionCalls); - } catch (Exception ex) { - throw new IllegalStateException("无法解析 AI 响应: " + ex.getMessage(), ex); - } - } - - private List toFunctionCallList(JSONArray array) { - if (array == null || array.isEmpty()) { - return Collections.emptyList(); - } - - List result = new ArrayList<>(); - for (Object element : array) { - JSONObject functionJson = JSONUtil.parseObj(element); - Map arguments = Optional.ofNullable(functionJson.getJSONObject("arguments")) - .map(obj -> obj.toBean(new TypeReference>() { - })) - .orElse(Collections.emptyMap()); - - result.add(AiFunctionCallDTO.builder() - .name(functionJson.getStr("name")) - .description(functionJson.getStr("description")) - .arguments(arguments) - .build()); - } - return result; - } - - private record ParseResult( - boolean success, - String explanation, - Double confidence, - String error, - String provider, - String model, - List functionCalls - ) { - } - - @Override - public Object executeCommand(AiExecuteRequestDTO request, HttpServletRequest httpRequest) throws Exception { - long startTime = System.currentTimeMillis(); - - // 获取用户信息 - Long userId = SecurityUtils.getUserId(); - String username = SecurityUtils.getUsername(); - String ipAddress = JakartaServletUtil.getClientIP(httpRequest); - - AiFunctionCallDTO functionCall = request.getFunctionCall(); - - // 根据解析日志ID获取审计记录,如果不存在则创建新记录 - AiCommandRecord commandRecord ; - if (StrUtil.isNotBlank(request.getParseLogId())) { - // 更新已存在的审计记录(解析阶段已创建) - commandRecord = recordService.getById(request.getParseLogId()); - if (commandRecord == null) { - throw new IllegalStateException("未找到对应的解析记录,ID: " + request.getParseLogId()); - } - } else { - // 如果没有解析日志ID,创建新记录(兼容直接执行的情况) - commandRecord = new AiCommandRecord(); - commandRecord.setUserId(userId); - commandRecord.setUsername(username); - commandRecord.setOriginalCommand(request.getOriginalCommand()); - commandRecord.setIpAddress(ipAddress); - recordService.save(commandRecord); - } - - // 更新执行相关字段 - commandRecord.setFunctionName(functionCall.getName()); - commandRecord.setFunctionArguments(JSONUtil.toJsonStr(functionCall.getArguments())); - commandRecord.setExecuteStatus(0); // 0-待执行 - - try { - // 🎯 执行具体的函数调用 - Object result = executeFunctionCall(functionCall); - - // 更新执行成功 - commandRecord.setExecuteStatus(1); // 1-成功 - commandRecord.setExecuteErrorMessage(null); - - // 更新审计记录 - recordService.updateById(commandRecord); - - log.info("✅ 命令执行成功,审计记录ID: {}", commandRecord.getId()); - - return result; - - } catch (Exception e) { - // 更新执行失败 - commandRecord.setExecuteStatus(-1); // -1-失败 - commandRecord.setExecuteErrorMessage(e.getMessage()); - - // 更新审计记录 - recordService.updateById(commandRecord); - - log.error("❌ 命令执行失败,审计记录ID: {}", commandRecord.getId(), e); - - // 抛出异常,由 Controller 统一处理 - throw e; - } - } - - /** - * 执行具体的函数调用 - */ - private Object executeFunctionCall(AiFunctionCallDTO functionCall) { - String functionName = functionCall.getName(); - Map arguments = functionCall.getArguments(); - - log.info("🎯 执行函数: {}, 参数: {}", functionName, arguments); - - // 根据函数名称路由到不同的处理器 - switch (functionName) { - case "updateUserNickname": - return executeUpdateUserNickname(arguments); - default: - throw new UnsupportedOperationException("不支持的函数: " + functionName); - } - } - - /** - * 使用 Tool: 根据用户名更新用户昵称 - */ - private Object executeUpdateUserNickname(Map arguments) { - String username = (String) arguments.get("username"); - String nickname = (String) arguments.get("nickname"); - - log.info("🔧 [Tool] 更新用户昵称: username={}, nickname={}", username, nickname); - String resultMsg = userTools.updateUserNickname(username, nickname); - - boolean success = resultMsg != null && resultMsg.contains("成功"); - if (!success) { - throw new RuntimeException(resultMsg != null ? resultMsg : "更新用户昵称失败"); - } - - return Map.of("username", username, "nickname", nickname, "message", resultMsg); - } -} - - diff --git a/src/main/java/com/youlai/boot/platform/codegen/controller/CodegenController.java b/src/main/java/com/youlai/boot/platform/codegen/controller/CodegenController.java index 5485dced..05abcfd8 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/controller/CodegenController.java +++ b/src/main/java/com/youlai/boot/platform/codegen/controller/CodegenController.java @@ -8,8 +8,8 @@ import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.platform.codegen.service.CodegenService; import com.youlai.boot.platform.codegen.model.form.GenConfigForm; import com.youlai.boot.platform.codegen.model.query.TablePageQuery; -import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVO; -import com.youlai.boot.platform.codegen.model.vo.TablePageVO; +import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVo; +import com.youlai.boot.platform.codegen.model.vo.TablePageVo; import com.youlai.boot.common.annotation.Log; import com.youlai.boot.platform.codegen.service.GenTableService; import io.swagger.v3.oas.annotations.Operation; @@ -46,10 +46,10 @@ public class CodegenController { @Operation(summary = "获取数据表分页列表") @GetMapping("/table/page") @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER) - public PageResult getTablePage( + public PageResult getTablePage( TablePageQuery queryParams ) { - Page result = codegenService.getTablePage(queryParams); + Page result = codegenService.getTablePage(queryParams); return PageResult.success(result); } @@ -82,9 +82,9 @@ public class CodegenController { @Operation(summary = "获取预览生成代码") @GetMapping("/{tableName}/preview") @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) - public Result> getTablePreviewData(@PathVariable String tableName, + public Result> getTablePreviewData(@PathVariable String tableName, @RequestParam(value = "pageType", required = false, defaultValue = "classic") String pageType) { - List list = codegenService.getCodegenPreviewData(tableName, pageType); + List list = codegenService.getCodegenPreviewData(tableName, pageType); return Result.success(list); } diff --git a/src/main/java/com/youlai/boot/platform/codegen/enums/JavaTypeEnum.java b/src/main/java/com/youlai/boot/platform/codegen/enums/JavaTypeEnum.java index 44ea85c6..7474abb8 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/enums/JavaTypeEnum.java +++ b/src/main/java/com/youlai/boot/platform/codegen/enums/JavaTypeEnum.java @@ -27,9 +27,11 @@ public enum JavaTypeEnum { FLOAT("float", "Float", "number"), DOUBLE("double", "Double", "number"), DECIMAL("decimal", "BigDecimal", "number"), - DATE("date", "LocalDate", "Date"), - DATETIME("datetime", "LocalDateTime", "Date"), - TIMESTAMP("timestamp", "LocalDateTime", "Date"); + DATE("date", "LocalDate", "string"), + DATETIME("datetime", "LocalDateTime", "string"), + TIMESTAMP("timestamp", "LocalDateTime", "string"), + BOOLEAN("boolean", "Boolean", "boolean"), + BIT("bit", "Boolean", "boolean"); // 数据库类型 private final String dbType; @@ -61,11 +63,12 @@ public enum JavaTypeEnum { * @return 对应的Java类型 */ public static String getJavaTypeByColumnType(String columnType) { - JavaTypeEnum javaTypeEnum = typeMap.get(columnType); + String normalized = normalizeColumnType(columnType); + JavaTypeEnum javaTypeEnum = typeMap.get(normalized); if (javaTypeEnum != null) { return javaTypeEnum.getJavaType(); } - return null; + return "String"; } /** @@ -75,11 +78,31 @@ public enum JavaTypeEnum { * @return 对应的TypeScript类型 */ public static String getTsTypeByJavaType(String javaType) { + if (javaType == null) { + return "any"; + } for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { if (javaTypeEnum.getJavaType().equals(javaType)) { return javaTypeEnum.getTsType(); } } - return null; + return "any"; + } + + private static String normalizeColumnType(String columnType) { + if (columnType == null) { + return ""; + } + // Handle values like: varchar(255), bigint unsigned, INT + String normalized = columnType.trim().toLowerCase(); + int parenIndex = normalized.indexOf('('); + if (parenIndex > -1) { + normalized = normalized.substring(0, parenIndex); + } + // Remove modifiers + normalized = normalized.replace("unsigned", "").replace("zerofill", "").trim(); + // Collapse repeated spaces + normalized = normalized.replaceAll("\\s+", " "); + return normalized; } } diff --git a/src/main/java/com/youlai/boot/platform/codegen/mapper/DatabaseMapper.java b/src/main/java/com/youlai/boot/platform/codegen/mapper/DatabaseMapper.java index 3d742c63..5737b495 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/mapper/DatabaseMapper.java +++ b/src/main/java/com/youlai/boot/platform/codegen/mapper/DatabaseMapper.java @@ -5,7 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.youlai.boot.platform.codegen.model.bo.ColumnMetaData; import com.youlai.boot.platform.codegen.model.bo.TableMetaData; import com.youlai.boot.platform.codegen.model.query.TablePageQuery; -import com.youlai.boot.platform.codegen.model.vo.TablePageVO; +import com.youlai.boot.platform.codegen.model.vo.TablePageVo; import org.apache.ibatis.annotations.Mapper; import java.util.List; @@ -27,7 +27,7 @@ public interface DatabaseMapper extends BaseMapper { * @param queryParams * @return */ - Page getTablePage(Page page, TablePageQuery queryParams); + Page getTablePage(Page page, TablePageQuery queryParams); /** * 获取表字段列表 diff --git a/src/main/java/com/youlai/boot/platform/codegen/model/bo/ColumnMetaData.java b/src/main/java/com/youlai/boot/platform/codegen/model/bo/ColumnMetaData.java index 154947bc..835ab048 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/model/bo/ColumnMetaData.java +++ b/src/main/java/com/youlai/boot/platform/codegen/model/bo/ColumnMetaData.java @@ -3,7 +3,7 @@ package com.youlai.boot.platform.codegen.model.bo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; -@Schema(description = "数据表字段VO") +@Schema(description = "数据表字段Vo") @Data public class ColumnMetaData { diff --git a/src/main/java/com/youlai/boot/platform/codegen/model/vo/CodegenPreviewVO.java b/src/main/java/com/youlai/boot/platform/codegen/model/vo/CodegenPreviewVO.java index 749d21bf..deeb7289 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/model/vo/CodegenPreviewVO.java +++ b/src/main/java/com/youlai/boot/platform/codegen/model/vo/CodegenPreviewVO.java @@ -3,9 +3,9 @@ package com.youlai.boot.platform.codegen.model.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; -@Schema(description = "代码生成代码预览VO") +@Schema(description = "代码生成代码预览Vo") @Data -public class CodegenPreviewVO { +public class CodegenPreviewVo { @Schema(description = "生成文件路径") private String path; diff --git a/src/main/java/com/youlai/boot/platform/codegen/model/vo/TablePageVO.java b/src/main/java/com/youlai/boot/platform/codegen/model/vo/TablePageVO.java index 0b011181..2ac5184d 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/model/vo/TablePageVO.java +++ b/src/main/java/com/youlai/boot/platform/codegen/model/vo/TablePageVO.java @@ -6,7 +6,7 @@ import lombok.Data; @Schema(description = "表视图对象") @Data -public class TablePageVO { +public class TablePageVo { @Schema(description = "表名称", example = "sys_user") private String tableName; diff --git a/src/main/java/com/youlai/boot/platform/codegen/service/CodegenService.java b/src/main/java/com/youlai/boot/platform/codegen/service/CodegenService.java index 32c020d1..7fcd0e29 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/service/CodegenService.java +++ b/src/main/java/com/youlai/boot/platform/codegen/service/CodegenService.java @@ -2,8 +2,8 @@ package com.youlai.boot.platform.codegen.service; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.youlai.boot.platform.codegen.model.query.TablePageQuery; -import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVO; -import com.youlai.boot.platform.codegen.model.vo.TablePageVO; +import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVo; +import com.youlai.boot.platform.codegen.model.vo.TablePageVo; import java.util.List; @@ -21,7 +21,7 @@ public interface CodegenService { * @param queryParams 查询参数 * @return */ - Page getTablePage(TablePageQuery queryParams); + Page getTablePage(TablePageQuery queryParams); /** * 获取预览生成代码 @@ -29,7 +29,7 @@ public interface CodegenService { * @param tableName 表名 * @return */ - List getCodegenPreviewData(String tableName, String pageType); + List getCodegenPreviewData(String tableName, String pageType); /** * 下载代码 diff --git a/src/main/java/com/youlai/boot/platform/codegen/service/impl/CodegenServiceImpl.java b/src/main/java/com/youlai/boot/platform/codegen/service/impl/CodegenServiceImpl.java index 8e33a2df..24bc902d 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/service/impl/CodegenServiceImpl.java +++ b/src/main/java/com/youlai/boot/platform/codegen/service/impl/CodegenServiceImpl.java @@ -21,8 +21,8 @@ import com.youlai.boot.platform.codegen.mapper.DatabaseMapper; import com.youlai.boot.platform.codegen.model.entity.GenTable; import com.youlai.boot.platform.codegen.model.entity.GenTableColumn; import com.youlai.boot.platform.codegen.model.query.TablePageQuery; -import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVO; -import com.youlai.boot.platform.codegen.model.vo.TablePageVO; +import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVo; +import com.youlai.boot.platform.codegen.model.vo.TablePageVo; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -36,7 +36,11 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; /** - * 数据库服务实现类 + * 代码生成服务实现类。 + * + *

+ * 根据代码生成配置({@link CodegenProperties})与表/字段元数据,渲染模板并提供预览与下载能力。 + *

* * @author Ray * @since 2.10.0 @@ -57,8 +61,8 @@ public class CodegenServiceImpl implements CodegenService { * @param queryParams 查询参数 * @return 分页结果 */ - public Page getTablePage(TablePageQuery queryParams) { - Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + public Page getTablePage(TablePageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); // 设置排除的表 List excludeTables = codegenProperties.getExcludeTables(); queryParams.setExcludeTables(excludeTables); @@ -73,9 +77,9 @@ public class CodegenServiceImpl implements CodegenService { * @return 预览数据 */ @Override - public List getCodegenPreviewData(String tableName, String pageType) { + public List getCodegenPreviewData(String tableName, String pageType) { - List list = new ArrayList<>(); + List list = new ArrayList<>(); GenTable genTable = genTableService.getOne(new LambdaQueryWrapper() .eq(GenTable::getTableName, tableName) @@ -96,7 +100,7 @@ public class CodegenServiceImpl implements CodegenService { // 遍历模板配置 Map templateConfigs = codegenProperties.getTemplateConfigs(); for (Map.Entry templateConfigEntry : templateConfigs.entrySet()) { - CodegenPreviewVO previewVO = new CodegenPreviewVO(); + CodegenPreviewVo previewVo = new CodegenPreviewVo(); CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); @@ -110,7 +114,7 @@ public class CodegenServiceImpl implements CodegenService { // 文件名 UserController.java String fileName = getFileName(entityName, templateName, extension); - previewVO.setFileName(fileName); + previewVo.setFileName(fileName); /* 2. 生成文件路径 */ // 包名:com.youlai.boot @@ -121,26 +125,28 @@ public class CodegenServiceImpl implements CodegenService { String subpackageName = templateConfig.getSubpackageName(); // 组合成文件路径:src/main/java/com/youlai/boot/system/controller String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); - previewVO.setPath(filePath); + previewVo.setPath(filePath); /* 3. 生成文件内容 */ // 将模板文件中的变量替换为具体的值 生成代码内容 // 优先使用保存的 ui,没有则使用请求参数 String finalType = StrUtil.blankToDefault(genTable.getPageType(), pageType); String content = getCodeContent(templateConfig, genTable, fieldConfigs, finalType); - previewVO.setContent(content); + previewVo.setContent(content); - list.add(previewVO); + list.add(previewVo); } return list; } /** - * 生成文件名 + * 生成文件名。 * - * @param entityName 实体类名 UserController - * @param templateName 模板名 Entity - * @param extension 文件后缀 .java + *

部分模板需要使用约定的命名规则(例如前端 API 文件)。

+ * + * @param entityName 实体名(例如 User) + * @param templateName 模板名(例如 Entity、Controller、API) + * @param extension 文件后缀(例如 .java、.ts) * @return 文件名 */ private String getFileName(String entityName, String templateName, String extension) { @@ -149,8 +155,11 @@ public class CodegenServiceImpl implements CodegenService { } else if ("MapperXml".equals(templateName)) { return entityName + "Mapper" + extension; } else if ("API".equals(templateName)) { - // 生成 user-api.ts 命名 - return StrUtil.toSymbolCase(entityName, '-') + "-api" + extension; + // 生成 user.ts 命名 + return StrUtil.toSymbolCase(entityName, '-') + extension; + } else if ("API_TYPES".equals(templateName)) { + // 生成 types/api/user.ts + return StrUtil.toSymbolCase(entityName, '-') + extension; } else if ("VIEW".equals(templateName)) { return "index.vue"; } @@ -158,14 +167,14 @@ public class CodegenServiceImpl implements CodegenService { } /** - * 生成文件路径 + * 生成文件路径。 * - * @param templateName 模板名 Entity - * @param moduleName 模块名 system - * @param packageName 包名 com.youlai - * @param subPackageName 子包名 controller - * @param entityName 实体类名 UserController - * @return 文件路径 src/main/java/com/youlai/system/controller + * @param templateName 模板名 + * @param moduleName 模块名(例如 system) + * @param packageName 包名(例如 com.youlai.boot) + * @param subPackageName 子包名(例如 controller、service.impl、api、views) + * @param entityName 实体名(例如 User) + * @return 生成文件路径 */ private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { String path; @@ -183,6 +192,13 @@ public class CodegenServiceImpl implements CodegenService { + File.separator + subPackageName + File.separator + moduleName ); + } else if ("API_TYPES".equals(templateName)) { + // path = "src/types/api"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + "types" + + File.separator + "api" + ); } else if ("VIEW".equals(templateName)) { // path = "src/views/system/user"; path = (codegenProperties.getFrontendAppName() @@ -208,12 +224,13 @@ public class CodegenServiceImpl implements CodegenService { } /** - * 生成代码内容 + * 渲染模板,生成代码内容。 * * @param templateConfig 模板配置 - * @param genConfig 生成配置 + * @param genTable 表生成配置 * @param fieldConfigs 字段配置 - * @return 代码内容 + * @param pageType 前端页面类型 + * @return 渲染后的代码内容 */ private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenTable genTable, List fieldConfigs, String pageType) { @@ -228,8 +245,12 @@ public class CodegenServiceImpl implements CodegenService { bindMap.put("entityName", entityName); bindMap.put("tableName", genTable.getTableName()); bindMap.put("author", genTable.getAuthor()); - bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest - bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-test + String entityLowerCamel = StrUtil.lowerFirst(entityName); + String entityKebab = StrUtil.toSymbolCase(entityName, '-'); + String entityUpperSnake = StrUtil.toSymbolCase(entityName, '_').toUpperCase(); + bindMap.put("entityLowerCamel", entityLowerCamel); + bindMap.put("entityKebab", entityKebab); + bindMap.put("entityUpperSnake", entityUpperSnake); bindMap.put("businessName", genTable.getBusinessName()); bindMap.put("fieldConfigs", fieldConfigs); @@ -239,7 +260,11 @@ public class CodegenServiceImpl implements CodegenService { for (GenTableColumn fieldConfig : fieldConfigs) { - if ("LocalDateTime".equals(fieldConfig.getFieldType())) { + if (StrUtil.isBlank(fieldConfig.getFieldType())) { + fieldConfig.setFieldType(JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType())); + } + + if ("LocalDateTime".equals(fieldConfig.getFieldType()) || "LocalDate".equals(fieldConfig.getFieldType())) { hasLocalDateTime = true; } if ("BigDecimal".equals(fieldConfig.getFieldType())) { @@ -267,10 +292,11 @@ public class CodegenServiceImpl implements CodegenService { } /** - * 下载代码 + * 下载代码。 * - * @param tableNames 表名数组,支持多张表。 - * @return 压缩文件字节数组 + * @param tableNames 表名数组,支持多张表 + * @param ui 页面类型 + * @return zip 压缩文件字节数组 */ @Override public byte[] downloadCode(String[] tableNames, String ui) { @@ -292,15 +318,16 @@ public class CodegenServiceImpl implements CodegenService { } /** - * 根据表名生成代码并压缩到zip文件中 + * 根据表名生成代码并压缩到 zip 文件中。 * * @param tableName 表名 * @param zip 压缩文件输出流 + * @param ui 页面类型 */ private void generateAndZipCode(String tableName, ZipOutputStream zip, String ui) { - List codePreviewList = getCodegenPreviewData(tableName, ui); + List codePreviewList = getCodegenPreviewData(tableName, ui); - for (CodegenPreviewVO codePreview : codePreviewList) { + for (CodegenPreviewVo codePreview : codePreviewList) { String fileName = codePreview.getFileName(); String content = codePreview.getContent(); String path = codePreview.getPath(); diff --git a/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenTableServiceImpl.java b/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenTableServiceImpl.java index b483652f..c0bd7ebb 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenTableServiceImpl.java +++ b/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenTableServiceImpl.java @@ -161,9 +161,10 @@ public class GenTableServiceImpl extends ServiceImpl i fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName())); fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1); - if (fieldConfig.getColumnType().equals("date")) { + String columnType = StrUtil.blankToDefault(fieldConfig.getColumnType(), "").toLowerCase(); + if ("date".equals(columnType)) { fieldConfig.setFormType(FormTypeEnum.DATE); - } else if (fieldConfig.getColumnType().equals("datetime")) { + } else if ("datetime".equals(columnType) || "timestamp".equals(columnType)) { fieldConfig.setFormType(FormTypeEnum.DATE_TIME); } else { fieldConfig.setFormType(FormTypeEnum.INPUT); diff --git a/src/main/java/com/youlai/boot/platform/websocket/controller/WebsocketController.java b/src/main/java/com/youlai/boot/platform/websocket/controller/WebsocketController.java index 0a9f8966..01bb5187 100644 --- a/src/main/java/com/youlai/boot/platform/websocket/controller/WebsocketController.java +++ b/src/main/java/com/youlai/boot/platform/websocket/controller/WebsocketController.java @@ -1,12 +1,13 @@ package com.youlai.boot.platform.websocket.controller; -import com.youlai.boot.platform.websocket.model.ChatMessage; +import com.youlai.boot.platform.websocket.dto.TextMessage; +import com.youlai.boot.platform.websocket.publisher.WebSocketPublisher; +import com.youlai.boot.platform.websocket.topic.WebSocketTopics; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.messaging.handler.annotation.DestinationVariable; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; -import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -26,7 +27,7 @@ import java.security.Principal; @Slf4j public class WebsocketController { - private final SimpMessagingTemplate messagingTemplate; + private final WebSocketPublisher webSocketPublisher; /** @@ -58,7 +59,7 @@ public class WebsocketController { log.info("发送人:{}; 接收人:{}", sender, receiver); // 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting - messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message)); + webSocketPublisher.publishToUser(receiver, WebSocketTopics.USER_QUEUE_GREETING, new TextMessage(sender, message, System.currentTimeMillis())); } } diff --git a/src/main/java/com/youlai/boot/platform/websocket/dto/DictChangeEvent.java b/src/main/java/com/youlai/boot/platform/websocket/dto/DictChangeEvent.java new file mode 100644 index 00000000..4df538de --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/websocket/dto/DictChangeEvent.java @@ -0,0 +1,15 @@ +package com.youlai.boot.platform.websocket.dto; + +import lombok.Data; + +@Data +public class DictChangeEvent { + + private String dictCode; + private long timestamp; + + public DictChangeEvent(String dictCode) { + this.dictCode = dictCode; + this.timestamp = System.currentTimeMillis(); + } +} diff --git a/src/main/java/com/youlai/boot/platform/websocket/model/ChatMessage.java b/src/main/java/com/youlai/boot/platform/websocket/dto/TextMessage.java similarity index 52% rename from src/main/java/com/youlai/boot/platform/websocket/model/ChatMessage.java rename to src/main/java/com/youlai/boot/platform/websocket/dto/TextMessage.java index a94b0b11..53929687 100644 --- a/src/main/java/com/youlai/boot/platform/websocket/model/ChatMessage.java +++ b/src/main/java/com/youlai/boot/platform/websocket/dto/TextMessage.java @@ -1,25 +1,15 @@ -package com.youlai.boot.platform.websocket.model; +package com.youlai.boot.platform.websocket.dto; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -/** - * 系统消息体 - */ @Data @AllArgsConstructor @NoArgsConstructor -public class ChatMessage { +public class TextMessage { - /** - * 发送者 - */ private String sender; - - /** - * 消息内容 - */ private String content; - + private Long timestamp; } diff --git a/src/main/java/com/youlai/boot/platform/websocket/publisher/WebSocketPublisher.java b/src/main/java/com/youlai/boot/platform/websocket/publisher/WebSocketPublisher.java new file mode 100644 index 00000000..d6d04b7d --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/websocket/publisher/WebSocketPublisher.java @@ -0,0 +1,61 @@ +package com.youlai.boot.platform.websocket.publisher; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class WebSocketPublisher { + + private SimpMessagingTemplate messagingTemplate; + private final ObjectMapper objectMapper; + + @Autowired(required = false) + public void setMessagingTemplate(SimpMessagingTemplate messagingTemplate) { + this.messagingTemplate = messagingTemplate; + } + + public void publish(String destination, Object payload) { + if (messagingTemplate == null) { + log.warn("消息模板尚未初始化,无法发送消息: destination={}", destination); + return; + } + + try { + Object body = serializeIfNeeded(payload); + messagingTemplate.convertAndSend(destination, body); + } catch (Exception e) { + log.error("发送消息失败: destination={}", destination, e); + } + } + + public void publishToUser(String username, String destination, Object payload) { + if (messagingTemplate == null) { + log.warn("消息模板尚未初始化,无法发送用户消息: username={}, destination={}", username, destination); + return; + } + + try { + Object body = serializeIfNeeded(payload); + messagingTemplate.convertAndSendToUser(username, destination, body); + } catch (Exception e) { + log.error("发送用户消息失败: username={}, destination={}", username, destination, e); + } + } + + private Object serializeIfNeeded(Object payload) throws JsonProcessingException { + if (payload == null) { + return null; + } + if (payload instanceof String || payload instanceof Number || payload instanceof Boolean) { + return payload; + } + return objectMapper.writeValueAsString(payload); + } +} diff --git a/src/main/java/com/youlai/boot/platform/websocket/service/impl/WebSocketServiceImpl.java b/src/main/java/com/youlai/boot/platform/websocket/service/impl/WebSocketServiceImpl.java index 982f34ae..595b78a2 100644 --- a/src/main/java/com/youlai/boot/platform/websocket/service/impl/WebSocketServiceImpl.java +++ b/src/main/java/com/youlai/boot/platform/websocket/service/impl/WebSocketServiceImpl.java @@ -1,22 +1,15 @@ package com.youlai.boot.platform.websocket.service.impl; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.youlai.boot.system.model.dto.DictEventDTO; +import com.youlai.boot.platform.websocket.dto.DictChangeEvent; +import com.youlai.boot.platform.websocket.dto.TextMessage; +import com.youlai.boot.platform.websocket.publisher.WebSocketPublisher; +import com.youlai.boot.platform.websocket.session.UserSessionRegistry; import com.youlai.boot.platform.websocket.service.WebSocketService; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; +import com.youlai.boot.platform.websocket.topic.WebSocketTopics; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; /** * WebSocket 服务实现类 @@ -33,39 +26,12 @@ import java.util.stream.Collectors; @Slf4j public class WebSocketServiceImpl implements WebSocketService { - // ==================== 在线用户管理 ==================== - - /** - * 用户在线会话映射表 - * Key: 用户名 - * Value: 该用户的所有会话 ID 集合(支持多设备登录) - */ - private final Map> userSessionsMap = new ConcurrentHashMap<>(); + private final UserSessionRegistry userSessionRegistry; + private final WebSocketPublisher webSocketPublisher; - /** - * 会话详情映射表 - * Key: 会话 ID - * Value: 会话详细信息 - */ - private final Map sessionDetailsMap = new ConcurrentHashMap<>(); - - // ==================== 依赖注入 ==================== - - private SimpMessagingTemplate messagingTemplate; - private final ObjectMapper objectMapper; - - @Autowired - public WebSocketServiceImpl(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; - } - - /** - * 延迟注入 SimpMessagingTemplate,避免循环依赖 - */ - @Autowired(required = false) - public void setMessagingTemplate(SimpMessagingTemplate messagingTemplate) { - this.messagingTemplate = messagingTemplate; - log.info("✓ WebSocket 消息模板已初始化"); + public WebSocketServiceImpl(UserSessionRegistry userSessionRegistry, WebSocketPublisher webSocketPublisher) { + this.userSessionRegistry = userSessionRegistry; + this.webSocketPublisher = webSocketPublisher; } // ==================== 用户在线状态管理 ==================== @@ -88,16 +54,10 @@ public class WebSocketServiceImpl implements WebSocketService { return; } - // 添加会话到用户的会话集合中(支持多设备登录) - userSessionsMap.computeIfAbsent(username, k -> ConcurrentHashMap.newKeySet()) - .add(sessionId); + userSessionRegistry.userConnected(username, sessionId); - // 保存会话详情 - SessionInfo sessionInfo = new SessionInfo(username, sessionId, System.currentTimeMillis()); - sessionDetailsMap.put(sessionId, sessionInfo); - - int sessionCount = userSessionsMap.get(username).size(); - int totalOnlineUsers = userSessionsMap.size(); + int sessionCount = userSessionRegistry.getUserSessionCount(username); + int totalOnlineUsers = userSessionRegistry.getOnlineUserCount(); log.info("✓ 用户[{}]会话[{}]上线(该用户共 {} 个会话,系统总在线用户数:{})", username, sessionId, sessionCount, totalOnlineUsers); @@ -117,20 +77,9 @@ public class WebSocketServiceImpl implements WebSocketService { return; } - // 获取该用户的所有会话 - Set sessions = userSessionsMap.get(username); - if (sessions == null || sessions.isEmpty()) { - log.warn("用户[{}]下线:未找到会话记录", username); - return; - } + userSessionRegistry.userDisconnected(username); - // 移除所有会话详情(通常一次只断开一个会话,但这里做全量清理) - sessions.forEach(sessionDetailsMap::remove); - - // 移除用户的会话记录 - userSessionsMap.remove(username); - - int totalOnlineUsers = userSessionsMap.size(); + int totalOnlineUsers = userSessionRegistry.getOnlineUserCount(); log.info("✓ 用户[{}]下线(系统总在线用户数:{})", username, totalOnlineUsers); // 广播在线用户数变更 @@ -143,29 +92,8 @@ public class WebSocketServiceImpl implements WebSocketService { * @param sessionId 会话 ID */ public void removeSession(String sessionId) { - SessionInfo sessionInfo = sessionDetailsMap.remove(sessionId); - if (sessionInfo == null) { - return; - } - - String username = sessionInfo.getUsername(); - Set sessions = userSessionsMap.get(username); - - if (sessions != null) { - sessions.remove(sessionId); - - // 如果该用户没有其他会话了,移除用户记录 - if (sessions.isEmpty()) { - userSessionsMap.remove(username); - log.info("✓ 用户[{}]最后一个会话[{}]下线", username, sessionId); - } else { - log.info("✓ 用户[{}]会话[{}]下线(还剩 {} 个会话)", - username, sessionId, sessions.size()); - } - - // 广播在线用户数变更 - broadcastOnlineUserCount(); - } + userSessionRegistry.removeSession(sessionId); + broadcastOnlineUserCount(); } /** @@ -173,23 +101,8 @@ public class WebSocketServiceImpl implements WebSocketService { * * @return 在线用户信息列表 */ - public List getOnlineUsers() { - return userSessionsMap.entrySet().stream() - .map(entry -> { - String username = entry.getKey(); - Set sessions = entry.getValue(); - - // 获取该用户最早的登录时间 - long earliestLoginTime = sessions.stream() - .map(sessionDetailsMap::get) - .filter(info -> info != null) - .mapToLong(SessionInfo::getConnectTime) - .min() - .orElse(System.currentTimeMillis()); - - return new OnlineUserDTO(username, sessions.size(), earliestLoginTime); - }) - .collect(Collectors.toList()); + public List getOnlineUsers() { + return userSessionRegistry.getOnlineUsers(); } /** @@ -198,7 +111,7 @@ public class WebSocketServiceImpl implements WebSocketService { * @return 在线用户数(不是会话数) */ public int getOnlineUserCount() { - return userSessionsMap.size(); + return userSessionRegistry.getOnlineUserCount(); } /** @@ -207,7 +120,7 @@ public class WebSocketServiceImpl implements WebSocketService { * @return 所有在线会话的总数 */ public int getTotalSessionCount() { - return sessionDetailsMap.size(); + return userSessionRegistry.getTotalSessionCount(); } /** @@ -217,8 +130,7 @@ public class WebSocketServiceImpl implements WebSocketService { * @return 是否在线 */ public boolean isUserOnline(String username) { - Set sessions = userSessionsMap.get(username); - return sessions != null && !sessions.isEmpty(); + return userSessionRegistry.isUserOnline(username); } /** @@ -228,8 +140,7 @@ public class WebSocketServiceImpl implements WebSocketService { * @return 会话数量 */ public int getUserSessionCount(String username) { - Set sessions = userSessionsMap.get(username); - return sessions != null ? sessions.size() : 0; + return userSessionRegistry.getUserSessionCount(username); } /** @@ -246,18 +157,9 @@ public class WebSocketServiceImpl implements WebSocketService { * 广播在线用户数量变更(内部方法) */ private void broadcastOnlineUserCount() { - if (messagingTemplate == null) { - log.warn("消息模板尚未初始化,无法发送在线用户数量"); - return; - } - - try { - int count = getOnlineUserCount(); - messagingTemplate.convertAndSend("/topic/online-count", count); - log.debug("✓ 已广播在线用户数量: {}", count); - } catch (Exception e) { - log.error("广播在线用户数量失败", e); - } + int count = getOnlineUserCount(); + webSocketPublisher.publish(WebSocketTopics.TOPIC_ONLINE_COUNT, count); + log.debug("✓ 已广播在线用户数量: {}", count); } // ==================== 消息推送功能 ==================== @@ -274,30 +176,9 @@ public class WebSocketServiceImpl implements WebSocketService { return; } - DictEventDTO event = new DictEventDTO(dictCode); - sendDictChangeEvent(event); - } - - /** - * 发送字典变更事件 - * - * @param event 字典事件 - */ - private void sendDictChangeEvent(DictEventDTO event) { - if (messagingTemplate == null) { - log.warn("消息模板尚未初始化,无法发送字典更新通知"); - return; - } - - try { - String message = objectMapper.writeValueAsString(event); - messagingTemplate.convertAndSend("/topic/dict", message); - log.info("✓ 已广播字典变更通知: dictCode={}", event.getDictCode()); - } catch (JsonProcessingException e) { - log.error("字典事件序列化失败: dictCode={}", event.getDictCode(), e); - } catch (Exception e) { - log.error("发送字典变更通知失败: dictCode={}", event.getDictCode(), e); - } + DictChangeEvent event = new DictChangeEvent(dictCode); + webSocketPublisher.publish(WebSocketTopics.TOPIC_DICT, event); + log.info("✓ 已广播字典变更通知: dictCode={}", dictCode); } /** @@ -318,20 +199,8 @@ public class WebSocketServiceImpl implements WebSocketService { return; } - if (messagingTemplate == null) { - log.warn("消息模板尚未初始化,无法发送用户消息"); - return; - } - - try { - String messageJson = objectMapper.writeValueAsString(message); - messagingTemplate.convertAndSendToUser(username, "/queue/messages", messageJson); - log.info("✓ 已向用户[{}]发送通知", username); - } catch (JsonProcessingException e) { - log.error("消息序列化失败: username={}", username, e); - } catch (Exception e) { - log.error("向用户[{}]发送通知失败", username, e); - } + webSocketPublisher.publishToUser(username, WebSocketTopics.USER_QUEUE_MESSAGES, message); + log.info("✓ 已向用户[{}]发送通知", username); } /** @@ -345,71 +214,8 @@ public class WebSocketServiceImpl implements WebSocketService { return; } - if (messagingTemplate == null) { - log.warn("消息模板尚未初始化,无法发送广播消息"); - return; - } - - try { - SystemMessage systemMessage = new SystemMessage( - "系统通知", - message, - System.currentTimeMillis() - ); - String messageJson = objectMapper.writeValueAsString(systemMessage); - messagingTemplate.convertAndSend("/topic/public", messageJson); - log.info("✓ 已广播系统消息: {}", message); - } catch (JsonProcessingException e) { - log.error("系统消息序列化失败", e); - } catch (Exception e) { - log.error("广播系统消息失败", e); - } - } - - // ==================== 内部数据类 ==================== - - /** - * 会话信息 - */ - @Data - @AllArgsConstructor - @NoArgsConstructor - private static class SessionInfo { - /** 用户名 */ - private String username; - /** 会话 ID */ - private String sessionId; - /** 连接时间戳 */ - private long connectTime; - } - - /** - * 在线用户 DTO - */ - @Data - @AllArgsConstructor - @NoArgsConstructor - public static class OnlineUserDTO { - /** 用户名 */ - private String username; - /** 会话数量 */ - private int sessionCount; - /** 首次登录时间 */ - private long loginTime; - } - - /** - * 系统消息 - */ - @Data - @AllArgsConstructor - @NoArgsConstructor - public static class SystemMessage { - /** 发送者 */ - private String sender; - /** 消息内容 */ - private String content; - /** 时间戳 */ - private long timestamp; + TextMessage systemMessage = new TextMessage("系统通知", message, System.currentTimeMillis()); + webSocketPublisher.publish(WebSocketTopics.TOPIC_PUBLIC, systemMessage); + log.info("✓ 已广播系统消息: {}", message); } } diff --git a/src/main/java/com/youlai/boot/platform/websocket/session/UserSessionRegistry.java b/src/main/java/com/youlai/boot/platform/websocket/session/UserSessionRegistry.java new file mode 100644 index 00000000..34ee41da --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/websocket/session/UserSessionRegistry.java @@ -0,0 +1,103 @@ +package com.youlai.boot.platform.websocket.session; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +@Component +public class UserSessionRegistry { + + private final Map> userSessionsMap = new ConcurrentHashMap<>(); + private final Map sessionDetailsMap = new ConcurrentHashMap<>(); + + public void userConnected(String username, String sessionId) { + userSessionsMap.computeIfAbsent(username, k -> ConcurrentHashMap.newKeySet()).add(sessionId); + sessionDetailsMap.put(sessionId, new SessionInfo(username, sessionId, System.currentTimeMillis())); + } + + public void userDisconnected(String username) { + Set sessions = userSessionsMap.remove(username); + if (sessions == null) { + return; + } + sessions.forEach(sessionDetailsMap::remove); + } + + public void removeSession(String sessionId) { + SessionInfo sessionInfo = sessionDetailsMap.remove(sessionId); + if (sessionInfo == null) { + return; + } + + String username = sessionInfo.getUsername(); + Set sessions = userSessionsMap.get(username); + if (sessions == null) { + return; + } + + sessions.remove(sessionId); + if (sessions.isEmpty()) { + userSessionsMap.remove(username); + } + } + + public int getOnlineUserCount() { + return userSessionsMap.size(); + } + + public int getUserSessionCount(String username) { + Set sessions = userSessionsMap.get(username); + return sessions != null ? sessions.size() : 0; + } + + public int getTotalSessionCount() { + return sessionDetailsMap.size(); + } + + public boolean isUserOnline(String username) { + Set sessions = userSessionsMap.get(username); + return sessions != null && !sessions.isEmpty(); + } + + public List getOnlineUsers() { + return userSessionsMap.entrySet().stream() + .map(entry -> { + String username = entry.getKey(); + Set sessions = entry.getValue(); + long earliestLoginTime = sessions.stream() + .map(sessionDetailsMap::get) + .filter(info -> info != null) + .mapToLong(SessionInfo::getConnectTime) + .min() + .orElse(System.currentTimeMillis()); + + return new OnlineUserDto(username, sessions.size(), earliestLoginTime); + }) + .collect(Collectors.toList()); + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + private static class SessionInfo { + private String username; + private String sessionId; + private long connectTime; + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class OnlineUserDto { + private String username; + private int sessionCount; + private long loginTime; + } +} diff --git a/src/main/java/com/youlai/boot/platform/websocket/topic/WebSocketTopics.java b/src/main/java/com/youlai/boot/platform/websocket/topic/WebSocketTopics.java new file mode 100644 index 00000000..ca250f2f --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/websocket/topic/WebSocketTopics.java @@ -0,0 +1,15 @@ +package com.youlai.boot.platform.websocket.topic; + +public final class WebSocketTopics { + + private WebSocketTopics() { + } + + public static final String TOPIC_DICT = "/topic/dict"; + public static final String TOPIC_ONLINE_COUNT = "/topic/online-count"; + public static final String TOPIC_PUBLIC = "/topic/public"; + + public static final String USER_QUEUE_MESSAGES = "/queue/messages"; + public static final String USER_QUEUE_MESSAGE = "/queue/message"; + public static final String USER_QUEUE_GREETING = "/queue/greeting"; +} diff --git a/src/main/java/com/youlai/boot/security/model/SysUserDetails.java b/src/main/java/com/youlai/boot/security/model/SysUserDetails.java index 8a79831c..ba59ea8c 100644 --- a/src/main/java/com/youlai/boot/security/model/SysUserDetails.java +++ b/src/main/java/com/youlai/boot/security/model/SysUserDetails.java @@ -3,6 +3,7 @@ package com.youlai.boot.security.model; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.ObjectUtil; import com.youlai.boot.common.constant.SecurityConstants; +import com.youlai.boot.security.model.UserAuthInfo; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.security.core.GrantedAuthority; @@ -64,9 +65,9 @@ public class SysUserDetails implements UserDetails { /** * 构造函数:根据用户认证信息初始化用户详情对象 * - * @param user 用户认证信息对象 {@link UserAuthCredentials} + * @param user 用户认证信息对象 {@link UserAuthInfo} */ - public SysUserDetails(UserAuthCredentials user) { + public SysUserDetails(UserAuthInfo user) { this.userId = user.getUserId(); this.username = user.getUsername(); this.password = user.getPassword(); diff --git a/src/main/java/com/youlai/boot/security/model/UserAuthCredentials.java b/src/main/java/com/youlai/boot/security/model/UserAuthCredentials.java index e68d119d..2f6f7a7a 100644 --- a/src/main/java/com/youlai/boot/security/model/UserAuthCredentials.java +++ b/src/main/java/com/youlai/boot/security/model/UserAuthCredentials.java @@ -1,7 +1,6 @@ package com.youlai.boot.security.model; import lombok.Data; -import java.util.Set; /** * 用户认证凭证信息 @@ -10,48 +9,6 @@ import java.util.Set; * @since 2022/10/22 */ @Data -public class UserAuthCredentials { - - /** - * 用户ID - */ - private Long userId; - - /** - * 用户名 - */ - private String username; - - /** - * 昵称 - */ - private String nickname; - - /** - * 部门ID - */ - private Long deptId; - - /** - * 用户密码 - */ - private String password; - - /** - * 状态(1:启用;0:禁用) - */ - private Integer status; - - /** - * 用户所属的角色集合 - */ - private Set roles; - - /** - * 数据权限范围,用于控制用户可以访问的数据级别 - * - * @see com.youlai.boot.common.enums.DataScopeEnum - */ - private Integer dataScope; +public class UserAuthCredentials extends UserAuthInfo { } diff --git a/src/main/java/com/youlai/boot/security/model/UserAuthInfo.java b/src/main/java/com/youlai/boot/security/model/UserAuthInfo.java new file mode 100644 index 00000000..02cc7002 --- /dev/null +++ b/src/main/java/com/youlai/boot/security/model/UserAuthInfo.java @@ -0,0 +1,58 @@ +package com.youlai.boot.security.model; + +import lombok.Data; + +import java.util.Set; + +/** + * 用户认证信息 + *

+ * 用于登录认证过程中的用户信息承载,包含用户名、密码、状态、角色等与认证/授权相关的数据。 + *

+ * + * @author Ray.Hao + * @since 2025/12/16 + */ +@Data +public class UserAuthInfo { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 昵称 + */ + private String nickname; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 密码(加密后) + */ + private String password; + + /** + * 状态(1:启用 其它:禁用) + */ + private Integer status; + + /** + * 角色集合 + */ + private Set roles; + + /** + * 数据权限范围 + */ + private Integer dataScope; +} diff --git a/src/main/java/com/youlai/boot/security/provider/SmsAuthenticationProvider.java b/src/main/java/com/youlai/boot/security/provider/SmsAuthenticationProvider.java index 681f31e8..57b5f79b 100644 --- a/src/main/java/com/youlai/boot/security/provider/SmsAuthenticationProvider.java +++ b/src/main/java/com/youlai/boot/security/provider/SmsAuthenticationProvider.java @@ -6,7 +6,7 @@ import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.security.exception.CaptchaValidationException; import com.youlai.boot.security.model.SmsAuthenticationToken; import com.youlai.boot.security.model.SysUserDetails; -import com.youlai.boot.security.model.UserAuthCredentials; +import com.youlai.boot.security.model.UserAuthInfo; import com.youlai.boot.system.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; @@ -16,7 +16,6 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UsernameNotFoundException; - /** * 短信验证码认证 Provider * @@ -50,14 +49,14 @@ public class SmsAuthenticationProvider implements AuthenticationProvider { String inputVerifyCode = (String) authentication.getCredentials(); // 根据手机号获取用户信息 - UserAuthCredentials userAuthCredentials = userService.getAuthCredentialsByMobile(mobile); + UserAuthInfo userAuthInfo = userService.getAuthInfoByMobile(mobile); - if (userAuthCredentials == null) { + if (userAuthInfo == null) { throw new UsernameNotFoundException("用户不存在"); } // 检查用户状态是否有效 - if (ObjectUtil.notEqual(userAuthCredentials.getStatus(), 1)) { + if (ObjectUtil.notEqual(userAuthInfo.getStatus(), 1)) { throw new DisabledException("用户已被禁用"); } @@ -73,7 +72,7 @@ public class SmsAuthenticationProvider implements AuthenticationProvider { } // 构建认证后的用户详情信息 - SysUserDetails userDetails = new SysUserDetails(userAuthCredentials); + SysUserDetails userDetails = new SysUserDetails(userAuthInfo); // 创建已认证的 SmsAuthenticationToken return SmsAuthenticationToken.authenticated( diff --git a/src/main/java/com/youlai/boot/security/provider/WxMiniAppCodeAuthenticationProvider.java b/src/main/java/com/youlai/boot/security/provider/WxMiniAppCodeAuthenticationProvider.java index 3214df81..46fc17cf 100644 --- a/src/main/java/com/youlai/boot/security/provider/WxMiniAppCodeAuthenticationProvider.java +++ b/src/main/java/com/youlai/boot/security/provider/WxMiniAppCodeAuthenticationProvider.java @@ -5,7 +5,7 @@ import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import com.youlai.boot.security.model.SysUserDetails; -import com.youlai.boot.security.model.UserAuthCredentials; +import com.youlai.boot.security.model.UserAuthInfo; import com.youlai.boot.security.model.WxMiniAppCodeAuthenticationToken; import com.youlai.boot.system.service.UserService; import lombok.extern.slf4j.Slf4j; @@ -17,7 +17,6 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UsernameNotFoundException; - /** * 微信小程序Code认证Provider * @@ -30,13 +29,11 @@ public class WxMiniAppCodeAuthenticationProvider implements AuthenticationProvid private final UserService userService; private final WxMaService wxMaService; - public WxMiniAppCodeAuthenticationProvider(UserService userService, WxMaService wxMaService) { this.userService = userService; this.wxMaService = wxMaService; } - /** * 微信认证逻辑,参考 Spring Security 认证密码校验流程 * @@ -63,26 +60,26 @@ public class WxMiniAppCodeAuthenticationProvider implements AuthenticationProvid } // 根据微信 OpenID 查询用户信息 - UserAuthCredentials userAuthCredentials = userService.getAuthCredentialsByOpenId(openId); + UserAuthInfo userAuthInfo = userService.getAuthInfoByOpenId(openId); - if (userAuthCredentials == null) { + if (userAuthInfo == null) { // 用户不存在则注册 userService.registerOrBindWechatUser(openId); // 再次查询用户信息,确保用户注册成功 - userAuthCredentials = userService.getAuthCredentialsByOpenId(openId); - if (userAuthCredentials == null) { + userAuthInfo = userService.getAuthInfoByOpenId(openId); + if (userAuthInfo == null) { throw new UsernameNotFoundException("用户注册失败,请稍后重试"); } } // 检查用户状态是否有效 - if (ObjectUtil.notEqual(userAuthCredentials.getStatus(), 1)) { + if (ObjectUtil.notEqual(userAuthInfo.getStatus(), 1)) { throw new DisabledException("用户已被禁用"); } // 构建认证后的用户详情信息 - SysUserDetails userDetails = new SysUserDetails(userAuthCredentials); + SysUserDetails userDetails = new SysUserDetails(userAuthInfo); // 创建已认证的Token return WxMiniAppCodeAuthenticationToken.authenticated( diff --git a/src/main/java/com/youlai/boot/security/provider/WxMiniAppPhoneAuthenticationProvider.java b/src/main/java/com/youlai/boot/security/provider/WxMiniAppPhoneAuthenticationProvider.java index 2bcc933a..f7077764 100644 --- a/src/main/java/com/youlai/boot/security/provider/WxMiniAppPhoneAuthenticationProvider.java +++ b/src/main/java/com/youlai/boot/security/provider/WxMiniAppPhoneAuthenticationProvider.java @@ -6,7 +6,7 @@ import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import com.youlai.boot.security.model.SysUserDetails; -import com.youlai.boot.security.model.UserAuthCredentials; +import com.youlai.boot.security.model.UserAuthInfo; import com.youlai.boot.security.model.WxMiniAppPhoneAuthenticationToken; import com.youlai.boot.system.service.UserService; import lombok.extern.slf4j.Slf4j; @@ -78,28 +78,28 @@ public class WxMiniAppPhoneAuthenticationProvider implements AuthenticationProvi String phoneNumber = phoneNumberInfo.getPhoneNumber(); // 3. 根据手机号查询用户,不存在则创建新用户 - UserAuthCredentials userAuthCredentials = userService.getAuthCredentialsByMobile(phoneNumber); + UserAuthInfo userAuthInfo = userService.getAuthInfoByMobile(phoneNumber); - if (userAuthCredentials == null) { + if (userAuthInfo == null) { // 用户不存在,注册新用户 boolean registered = userService.registerUserByMobileAndOpenId(phoneNumber, openId); if (!registered) { throw new UsernameNotFoundException("用户注册失败"); } // 重新获取用户信息 - userAuthCredentials = userService.getAuthCredentialsByMobile(phoneNumber); + userAuthInfo = userService.getAuthInfoByMobile(phoneNumber); } else { // 用户存在,绑定openId(如果未绑定) - userService.bindUserOpenId(userAuthCredentials.getUserId(), openId); + userService.bindUserOpenId(userAuthInfo.getUserId(), openId); } // 4. 检查用户状态 - if (ObjectUtil.notEqual(userAuthCredentials.getStatus(), 1)) { + if (ObjectUtil.notEqual(userAuthInfo.getStatus(), 1)) { throw new DisabledException("用户已被禁用"); } // 5. 构建认证后的用户详情 - SysUserDetails userDetails = new SysUserDetails(userAuthCredentials); + SysUserDetails userDetails = new SysUserDetails(userAuthInfo); // 6. 创建已认证的Token return WxMiniAppPhoneAuthenticationToken.authenticated( diff --git a/src/main/java/com/youlai/boot/security/service/SysUserDetailsService.java b/src/main/java/com/youlai/boot/security/service/SysUserDetailsService.java index 213b698f..7b66208d 100644 --- a/src/main/java/com/youlai/boot/security/service/SysUserDetailsService.java +++ b/src/main/java/com/youlai/boot/security/service/SysUserDetailsService.java @@ -1,7 +1,7 @@ package com.youlai.boot.security.service; import com.youlai.boot.security.model.SysUserDetails; -import com.youlai.boot.security.model.UserAuthCredentials; +import com.youlai.boot.security.model.UserAuthInfo; import com.youlai.boot.system.service.UserService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -33,11 +33,11 @@ public class SysUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { try { - UserAuthCredentials userAuthCredentials = userService.getAuthCredentialsByUsername(username); - if (userAuthCredentials == null) { + UserAuthInfo userAuthInfo = userService.getAuthInfoByUsername(username); + if (userAuthInfo == null) { throw new UsernameNotFoundException(username); } - return new SysUserDetails(userAuthCredentials); + return new SysUserDetails(userAuthInfo); } catch (Exception e) { // 记录异常日志 log.error("认证异常:{}", e.getMessage()); 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 a7df99ed..f21bcb0d 100644 --- a/src/main/java/com/youlai/boot/system/controller/ConfigController.java +++ b/src/main/java/com/youlai/boot/system/controller/ConfigController.java @@ -7,7 +7,7 @@ import com.youlai.boot.core.web.Result; import com.youlai.boot.common.annotation.Log; import com.youlai.boot.system.model.form.ConfigForm; import com.youlai.boot.system.model.query.ConfigPageQuery; -import com.youlai.boot.system.model.vo.ConfigVO; +import com.youlai.boot.system.model.vo.ConfigVo; import com.youlai.boot.system.service.ConfigService; import io.swagger.v3.oas.annotations.Parameter; import jakarta.validation.Valid; @@ -38,8 +38,8 @@ public class ConfigController { @GetMapping("/page") @PreAuthorize("@ss.hasPerm('sys:config:list')") @Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING) - public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { - IPage result = configService.page(configPageQuery); + public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { + IPage result = configService.page(configPageQuery); return PageResult.success(result); } 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 bb7fae27..ed5e6964 100644 --- a/src/main/java/com/youlai/boot/system/controller/DeptController.java +++ b/src/main/java/com/youlai/boot/system/controller/DeptController.java @@ -6,7 +6,7 @@ import com.youlai.boot.common.model.Option; import com.youlai.boot.core.web.Result; import com.youlai.boot.system.model.form.DeptForm; import com.youlai.boot.system.model.query.DeptQuery; -import com.youlai.boot.system.model.vo.DeptVO; +import com.youlai.boot.system.model.vo.DeptVo; import com.youlai.boot.common.annotation.Log; import com.youlai.boot.system.service.DeptService; import io.swagger.v3.oas.annotations.Parameter; @@ -36,10 +36,10 @@ public class DeptController { @Operation(summary = "部门列表") @GetMapping @Log( value = "部门列表",module = LogModuleEnum.DEPT) - public Result> getDeptList( + public Result> getDeptList( DeptQuery queryParams ) { - List list = deptService.getDeptList(queryParams); + List list = deptService.getDeptList(queryParams); return Result.success(list); } 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 f2fb1d10..67736fc9 100644 --- a/src/main/java/com/youlai/boot/system/controller/DictController.java +++ b/src/main/java/com/youlai/boot/system/controller/DictController.java @@ -8,9 +8,9 @@ import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.system.model.form.DictItemForm; import com.youlai.boot.system.model.query.DictItemPageQuery; import com.youlai.boot.system.model.query.DictPageQuery; -import com.youlai.boot.system.model.vo.DictItemOptionVO; -import com.youlai.boot.system.model.vo.DictItemPageVO; -import com.youlai.boot.system.model.vo.DictPageVO; +import com.youlai.boot.system.model.vo.DictItemOptionVo; +import com.youlai.boot.system.model.vo.DictItemPageVo; +import com.youlai.boot.system.model.vo.DictPageVo; import com.youlai.boot.common.annotation.RepeatSubmit; import com.youlai.boot.system.model.form.DictForm; import com.youlai.boot.common.annotation.Log; @@ -51,10 +51,10 @@ public class DictController { @Operation(summary = "字典分页列表") @GetMapping("/page") @Log( value = "字典分页列表",module = LogModuleEnum.DICT) - public PageResult getDictPage( + public PageResult getDictPage( DictPageQuery queryParams ) { - Page result = dictService.getDictPage(queryParams); + Page result = dictService.getDictPage(queryParams); return PageResult.success(result); } @@ -128,21 +128,21 @@ public class DictController { //--------------------------------------------------- @Operation(summary = "字典项分页列表") @GetMapping("/{dictCode}/items/page") - public PageResult getDictItemPage( + public PageResult getDictItemPage( @PathVariable String dictCode, DictItemPageQuery queryParams ) { queryParams.setDictCode(dictCode); - Page result = dictItemService.getDictItemPage(queryParams); + Page result = dictItemService.getDictItemPage(queryParams); return PageResult.success(result); } @Operation(summary = "字典项列表") @GetMapping("/{dictCode}/items") - public Result> getDictItems( + public Result> getDictItems( @Parameter(description = "字典编码") @PathVariable String dictCode ) { - List list = dictItemService.getDictItems(dictCode); + List list = dictItemService.getDictItems(dictCode); return Result.success(list); } diff --git a/src/main/java/com/youlai/boot/system/controller/LogController.java b/src/main/java/com/youlai/boot/system/controller/LogController.java index 46e312c1..d494f005 100644 --- a/src/main/java/com/youlai/boot/system/controller/LogController.java +++ b/src/main/java/com/youlai/boot/system/controller/LogController.java @@ -2,20 +2,14 @@ package com.youlai.boot.system.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.youlai.boot.core.web.PageResult; -import com.youlai.boot.core.web.Result; 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.youlai.boot.system.model.vo.VisitTrendVO; +import com.youlai.boot.system.model.vo.LogPageVo; import com.youlai.boot.system.service.LogService; 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 org.springframework.web.bind.annotation.*; -import java.time.LocalDate; - /** * 日志控制层 * @@ -32,30 +26,11 @@ public class LogController { @Operation(summary = "日志分页列表") @GetMapping("/page") - public PageResult getLogPage( + public PageResult getLogPage( LogPageQuery queryParams ) { - Page result = logService.getLogPage(queryParams); + Page result = logService.getLogPage(queryParams); return PageResult.success(result); } - @Operation(summary = "获取访问趋势") - @GetMapping("/visit-trend") - public Result getVisitTrend( - @Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate, - @Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate - ) { - LocalDate start = LocalDate.parse(startDate); - LocalDate end = LocalDate.parse(endDate); - VisitTrendVO data = logService.getVisitTrend(start, end); - return Result.success(data); - } - - @Operation(summary = "获取访问统计") - @GetMapping("/visit-stats") - public Result getVisitStats() { - VisitStatsVO result = logService.getVisitStats(); - return Result.success(result); - } - } 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 e7e014a6..7fe0ffc6 100644 --- a/src/main/java/com/youlai/boot/system/controller/MenuController.java +++ b/src/main/java/com/youlai/boot/system/controller/MenuController.java @@ -7,8 +7,8 @@ import com.youlai.boot.common.model.Option; import com.youlai.boot.core.web.Result; import com.youlai.boot.system.model.form.MenuForm; import com.youlai.boot.system.model.query.MenuQuery; -import com.youlai.boot.system.model.vo.MenuVO; -import com.youlai.boot.system.model.vo.RouteVO; +import com.youlai.boot.system.model.vo.MenuVo; +import com.youlai.boot.system.model.vo.RouteVo; import com.youlai.boot.system.service.MenuService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -38,8 +38,8 @@ public class MenuController { @Operation(summary = "菜单列表") @GetMapping @Log(value = "菜单列表", module = LogModuleEnum.MENU) - public Result> getMenus(MenuQuery queryParams) { - List menuList = menuService.listMenus(queryParams); + public Result> getMenus(MenuQuery queryParams) { + List menuList = menuService.listMenus(queryParams); return Result.success(menuList); } @@ -55,8 +55,8 @@ public class MenuController { @Operation(summary = "当前用户菜单路由列表") @GetMapping("/routes") - public Result> getCurrentUserRoutes() { - List routeList = menuService.listCurrentUserRoutes(); + public Result> getCurrentUserRoutes() { + List routeList = menuService.listCurrentUserRoutes(); return Result.success(routeList); } 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 1382aa5b..af153f36 100644 --- a/src/main/java/com/youlai/boot/system/controller/NoticeController.java +++ b/src/main/java/com/youlai/boot/system/controller/NoticeController.java @@ -5,9 +5,9 @@ import com.youlai.boot.core.web.PageResult; import com.youlai.boot.core.web.Result; import com.youlai.boot.system.model.form.NoticeForm; import com.youlai.boot.system.model.query.NoticePageQuery; -import com.youlai.boot.system.model.vo.NoticeDetailVO; -import com.youlai.boot.system.model.vo.NoticePageVO; -import com.youlai.boot.system.model.vo.UserNoticePageVO; +import com.youlai.boot.system.model.vo.NoticeDetailVo; +import com.youlai.boot.system.model.vo.NoticePageVo; +import com.youlai.boot.system.model.vo.UserNoticePageVo; import com.youlai.boot.system.service.NoticeService; import com.youlai.boot.system.service.UserNoticeService; import io.swagger.v3.oas.annotations.Operation; @@ -38,8 +38,8 @@ public class NoticeController { @Operation(summary = "通知公告分页列表") @GetMapping("/page") @PreAuthorize("@ss.hasPerm('sys:notice:list')") - public PageResult getNoticePage(NoticePageQuery queryParams) { - IPage result = noticeService.getNoticePage(queryParams); + public PageResult getNoticePage(NoticePageQuery queryParams) { + IPage result = noticeService.getNoticePage(queryParams); return PageResult.success(result); } @@ -63,11 +63,11 @@ public class NoticeController { @Operation(summary = "阅读获取通知公告详情") @GetMapping("/{id}/detail") - public Result getNoticeDetail( + public Result getNoticeDetail( @Parameter(description = "通知公告ID") @PathVariable Long id ) { - NoticeDetailVO detailVO = noticeService.getNoticeDetail(id); - return Result.success(detailVO); + NoticeDetailVo detailVo = noticeService.getNoticeDetail(id); + return Result.success(detailVo); } @Operation(summary = "修改通知公告") @@ -120,10 +120,10 @@ public class NoticeController { @Operation(summary = "获取我的通知公告分页列表") @GetMapping("/my") - public PageResult getMyNoticePage( + public PageResult getMyNoticePage( NoticePageQuery queryParams ) { - IPage result = noticeService.getMyNoticePage(queryParams); + IPage result = noticeService.getMyNoticePage(queryParams); return PageResult.success(result); } } 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 e178518d..4546fe66 100644 --- a/src/main/java/com/youlai/boot/system/controller/RoleController.java +++ b/src/main/java/com/youlai/boot/system/controller/RoleController.java @@ -8,7 +8,7 @@ import com.youlai.boot.core.web.PageResult; import com.youlai.boot.core.web.Result; import com.youlai.boot.system.model.form.RoleForm; import com.youlai.boot.system.model.query.RolePageQuery; -import com.youlai.boot.system.model.vo.RolePageVO; +import com.youlai.boot.system.model.vo.RolePageVo; import com.youlai.boot.common.annotation.Log; import com.youlai.boot.system.service.RoleService; import io.swagger.v3.oas.annotations.Parameter; @@ -39,10 +39,10 @@ public class RoleController { @Operation(summary = "角色分页列表") @GetMapping("/page") @Log(value = "角色分页列表", module = LogModuleEnum.ROLE) - public PageResult getRolePage( + public PageResult getRolePage( RolePageQuery queryParams ) { - Page result = roleService.getRolePage(queryParams); + Page result = roleService.getRolePage(queryParams); return PageResult.success(result); } diff --git a/src/main/java/com/youlai/boot/system/controller/StatisticsController.java b/src/main/java/com/youlai/boot/system/controller/StatisticsController.java new file mode 100644 index 00000000..66091c1f --- /dev/null +++ b/src/main/java/com/youlai/boot/system/controller/StatisticsController.java @@ -0,0 +1,47 @@ +package com.youlai.boot.system.controller; + +import com.youlai.boot.core.web.Result; +import com.youlai.boot.system.model.vo.VisitStatsVo; +import com.youlai.boot.system.model.vo.VisitTrendVo; +import com.youlai.boot.system.service.LogService; +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 org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; + +/** + * 统计分析控制层 + * + * @author haoxr + * @since 2024-12-15 + */ +@Tag(name = "11.统计分析") +@RestController +@RequestMapping("/api/v1/statistics") +@RequiredArgsConstructor +public class StatisticsController { + + private final LogService logService; + + @Operation(summary = "访问趋势统计") + @GetMapping("/visits/trend") + public Result getVisitTrend( + @Parameter(description = "开始时间", example = "2024-01-01") @RequestParam String startDate, + @Parameter(description = "结束时间", example = "2024-12-31") @RequestParam String endDate + ) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + VisitTrendVo data = logService.getVisitTrend(start, end); + return Result.success(data); + } + + @Operation(summary = "访问概览统计") + @GetMapping("/visits/overview") + public Result getVisitOverview() { + VisitStatsVo result = logService.getVisitStats(); + return Result.success(result); + } +} 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 db96814b..b2b436e2 100644 --- a/src/main/java/com/youlai/boot/system/controller/UserController.java +++ b/src/main/java/com/youlai/boot/system/controller/UserController.java @@ -14,14 +14,14 @@ import com.youlai.boot.core.web.Result; import com.youlai.boot.common.util.ExcelUtils; import com.youlai.boot.security.util.SecurityUtils; import com.youlai.boot.system.listener.UserImportListener; -import com.youlai.boot.system.model.dto.UserExportDTO; -import com.youlai.boot.system.model.dto.UserImportDTO; +import com.youlai.boot.system.model.dto.UserExportDto; +import com.youlai.boot.system.model.dto.UserImportDto; import com.youlai.boot.system.model.entity.User; import com.youlai.boot.system.model.form.*; import com.youlai.boot.system.model.query.UserPageQuery; -import com.youlai.boot.system.model.dto.CurrentUserDTO; -import com.youlai.boot.system.model.vo.UserPageVO; -import com.youlai.boot.system.model.vo.UserProfileVO; +import com.youlai.boot.system.model.dto.CurrentUserDto; +import com.youlai.boot.system.model.vo.UserPageVo; +import com.youlai.boot.system.model.vo.UserProfileVo; import com.youlai.boot.system.service.UserService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -59,10 +59,10 @@ public class UserController { @Operation(summary = "用户分页列表") @GetMapping("/page") @Log(value = "用户分页列表", module = LogModuleEnum.USER) - public PageResult getUserPage( + public PageResult getUserPage( @Valid UserPageQuery queryParams ) { - IPage result = userService.getUserPage(queryParams); + IPage result = userService.getUserPage(queryParams); return PageResult.success(result); } @@ -130,9 +130,9 @@ public class UserController { @Operation(summary = "获取当前登录用户信息") @GetMapping("/me") @Log(value = "获取当前登录用户信息", module = LogModuleEnum.USER) - public Result getCurrentUser() { - CurrentUserDTO currentUserDTO = userService.getCurrentUserInfo(); - return Result.success(currentUserDTO); + public Result getCurrentUser() { + CurrentUserDto currentUserDto = userService.getCurrentUserInfo(); + return Result.success(currentUserDto); } @Operation(summary = "用户导入模板下载") @@ -160,7 +160,7 @@ public class UserController { @Log(value = "导入用户", module = LogModuleEnum.USER) public Result importUsers(MultipartFile file) throws IOException { UserImportListener listener = new UserImportListener(); - ExcelUtils.importExcel(file.getInputStream(), UserImportDTO.class, listener); + ExcelUtils.importExcel(file.getInputStream(), UserImportDto.class, listener); return Result.success(listener.getExcelResult()); } @@ -173,17 +173,17 @@ public class UserController { response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8)); - List exportUserList = userService.listExportUsers(queryParams); - EasyExcel.write(response.getOutputStream(), UserExportDTO.class).sheet("用户列表") + List exportUserList = userService.listExportUsers(queryParams); + EasyExcel.write(response.getOutputStream(), UserExportDto.class).sheet("用户列表") .doWrite(exportUserList); } @Operation(summary = "获取个人中心用户信息") @GetMapping("/profile") @Log(value = "获取个人中心用户信息", module = LogModuleEnum.USER) - public Result getUserProfile() { + public Result getUserProfile() { Long userId = SecurityUtils.getUserId(); - UserProfileVO userProfile = userService.getUserProfile(userId); + UserProfileVo userProfile = userService.getUserProfile(userId); return Result.success(userProfile); } diff --git a/src/main/java/com/youlai/boot/system/converter/ConfigConverter.java b/src/main/java/com/youlai/boot/system/converter/ConfigConverter.java index dc448ecd..03e01c5d 100644 --- a/src/main/java/com/youlai/boot/system/converter/ConfigConverter.java +++ b/src/main/java/com/youlai/boot/system/converter/ConfigConverter.java @@ -2,7 +2,7 @@ package com.youlai.boot.system.converter; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.youlai.boot.system.model.entity.Config; -import com.youlai.boot.system.model.vo.ConfigVO; +import com.youlai.boot.system.model.vo.ConfigVo; import com.youlai.boot.system.model.form.ConfigForm; import org.mapstruct.Mapper; @@ -15,7 +15,7 @@ import org.mapstruct.Mapper; @Mapper(componentModel = "spring") public interface ConfigConverter { - Page toPageVo(Page page); + Page toPageVo(Page page); Config toEntity(ConfigForm configForm); diff --git a/src/main/java/com/youlai/boot/system/converter/DeptConverter.java b/src/main/java/com/youlai/boot/system/converter/DeptConverter.java index 1ca1510e..9952f3f6 100644 --- a/src/main/java/com/youlai/boot/system/converter/DeptConverter.java +++ b/src/main/java/com/youlai/boot/system/converter/DeptConverter.java @@ -1,7 +1,7 @@ package com.youlai.boot.system.converter; import com.youlai.boot.system.model.entity.Dept; -import com.youlai.boot.system.model.vo.DeptVO; +import com.youlai.boot.system.model.vo.DeptVo; import com.youlai.boot.system.model.form.DeptForm; import org.mapstruct.Mapper; @@ -16,7 +16,7 @@ public interface DeptConverter { DeptForm toForm(Dept entity); - DeptVO toVo(Dept entity); + DeptVo toVo(Dept entity); Dept toEntity(DeptForm deptForm); diff --git a/src/main/java/com/youlai/boot/system/converter/DictConverter.java b/src/main/java/com/youlai/boot/system/converter/DictConverter.java index b15d23fc..9a844adb 100644 --- a/src/main/java/com/youlai/boot/system/converter/DictConverter.java +++ b/src/main/java/com/youlai/boot/system/converter/DictConverter.java @@ -2,7 +2,7 @@ package com.youlai.boot.system.converter; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.youlai.boot.system.model.entity.Dict; -import com.youlai.boot.system.model.vo.DictPageVO; +import com.youlai.boot.system.model.vo.DictPageVo; import com.youlai.boot.system.model.form.DictForm; import org.mapstruct.Mapper; @@ -15,7 +15,7 @@ import org.mapstruct.Mapper; @Mapper(componentModel = "spring") public interface DictConverter { - Page toPageVo(Page page); + Page toPageVo(Page page); DictForm toForm(Dict entity); diff --git a/src/main/java/com/youlai/boot/system/converter/DictItemConverter.java b/src/main/java/com/youlai/boot/system/converter/DictItemConverter.java index 99a354b0..ed0477e2 100644 --- a/src/main/java/com/youlai/boot/system/converter/DictItemConverter.java +++ b/src/main/java/com/youlai/boot/system/converter/DictItemConverter.java @@ -3,7 +3,7 @@ package com.youlai.boot.system.converter; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.youlai.boot.system.model.entity.DictItem; import com.youlai.boot.system.model.form.DictItemForm; -import com.youlai.boot.system.model.vo.DictPageVO; +import com.youlai.boot.system.model.vo.DictPageVo; import com.youlai.boot.common.model.Option; import org.mapstruct.Mapper; @@ -18,7 +18,7 @@ import java.util.List; @Mapper(componentModel = "spring") public interface DictItemConverter { - Page toPageVo(Page page); + Page toPageVo(Page page); DictItemForm toForm(DictItem entity); diff --git a/src/main/java/com/youlai/boot/system/converter/MenuConverter.java b/src/main/java/com/youlai/boot/system/converter/MenuConverter.java index a360ed4e..1dd49843 100644 --- a/src/main/java/com/youlai/boot/system/converter/MenuConverter.java +++ b/src/main/java/com/youlai/boot/system/converter/MenuConverter.java @@ -1,7 +1,7 @@ package com.youlai.boot.system.converter; import com.youlai.boot.system.model.entity.Menu; -import com.youlai.boot.system.model.vo.MenuVO; +import com.youlai.boot.system.model.vo.MenuVo; import com.youlai.boot.system.model.form.MenuForm; import org.mapstruct.Mapper; import org.mapstruct.Mapping; @@ -15,7 +15,7 @@ import org.mapstruct.Mapping; @Mapper(componentModel = "spring") public interface MenuConverter { - MenuVO toVo(Menu entity); + MenuVo toVo(Menu entity); @Mapping(target = "params", ignore = true) MenuForm toForm(Menu entity); diff --git a/src/main/java/com/youlai/boot/system/converter/NoticeConverter.java b/src/main/java/com/youlai/boot/system/converter/NoticeConverter.java index 5f3950cb..994868e0 100644 --- a/src/main/java/com/youlai/boot/system/converter/NoticeConverter.java +++ b/src/main/java/com/youlai/boot/system/converter/NoticeConverter.java @@ -1,11 +1,11 @@ package com.youlai.boot.system.converter; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.youlai.boot.system.model.bo.NoticeBO; +import com.youlai.boot.system.model.bo.NoticeBo; import com.youlai.boot.system.model.entity.Notice; import com.youlai.boot.system.model.form.NoticeForm; -import com.youlai.boot.system.model.vo.NoticeDetailVO; -import com.youlai.boot.system.model.vo.NoticePageVO; +import com.youlai.boot.system.model.vo.NoticeDetailVo; +import com.youlai.boot.system.model.vo.NoticePageVo; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings; @@ -30,9 +30,9 @@ public interface NoticeConverter{ }) Notice toEntity(NoticeForm formData); - NoticePageVO toPageVo(NoticeBO bo); + NoticePageVo toPageVo(NoticeBo bo); - Page toPageVo(Page noticePage); + Page toPageVo(Page noticePage); - NoticeDetailVO toDetailVO(NoticeBO noticeBO); + NoticeDetailVo toDetailVo(NoticeBo noticeBo); } diff --git a/src/main/java/com/youlai/boot/system/converter/RoleConverter.java b/src/main/java/com/youlai/boot/system/converter/RoleConverter.java index ddcfc930..bba6dbaf 100644 --- a/src/main/java/com/youlai/boot/system/converter/RoleConverter.java +++ b/src/main/java/com/youlai/boot/system/converter/RoleConverter.java @@ -2,7 +2,7 @@ package com.youlai.boot.system.converter; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.youlai.boot.system.model.entity.Role; -import com.youlai.boot.system.model.vo.RolePageVO; +import com.youlai.boot.system.model.vo.RolePageVo; import com.youlai.boot.common.model.Option; import com.youlai.boot.system.model.form.RoleForm; import org.mapstruct.Mapper; @@ -20,7 +20,7 @@ import java.util.List; @Mapper(componentModel = "spring") public interface RoleConverter { - Page toPageVo(Page page); + Page toPageVo(Page page); @Mappings({ @Mapping(target = "value", source = "id"), diff --git a/src/main/java/com/youlai/boot/system/converter/UserConverter.java b/src/main/java/com/youlai/boot/system/converter/UserConverter.java index 128d4773..ee9b2247 100644 --- a/src/main/java/com/youlai/boot/system/converter/UserConverter.java +++ b/src/main/java/com/youlai/boot/system/converter/UserConverter.java @@ -3,12 +3,12 @@ package com.youlai.boot.system.converter; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.youlai.boot.common.model.Option; import com.youlai.boot.system.model.entity.User; -import com.youlai.boot.system.model.dto.CurrentUserDTO; -import com.youlai.boot.system.model.vo.UserPageVO; -import com.youlai.boot.system.model.vo.UserProfileVO; -import com.youlai.boot.system.model.bo.UserBO; +import com.youlai.boot.system.model.dto.CurrentUserDto; +import com.youlai.boot.system.model.vo.UserPageVo; +import com.youlai.boot.system.model.vo.UserProfileVo; +import com.youlai.boot.system.model.bo.UserBo; import com.youlai.boot.system.model.form.UserForm; -import com.youlai.boot.system.model.dto.UserImportDTO; +import com.youlai.boot.system.model.dto.UserImportDto; import com.youlai.boot.system.model.form.UserProfileForm; import org.mapstruct.InheritInverseConfiguration; import org.mapstruct.Mapper; @@ -26,9 +26,9 @@ import java.util.List; @Mapper(componentModel = "spring") public interface UserConverter { - UserPageVO toPageVo(UserBO bo); + UserPageVo toPageVo(UserBo bo); - Page toPageVo(Page bo); + Page toPageVo(Page bo); UserForm toForm(User entity); @@ -38,12 +38,12 @@ public interface UserConverter { @Mappings({ @Mapping(target = "userId", source = "id") }) - CurrentUserDTO toCurrentUserDto(User entity); + CurrentUserDto toCurrentUserDto(User entity); - User toEntity(UserImportDTO vo); + User toEntity(UserImportDto vo); - UserProfileVO toProfileVo(UserBO bo); + UserProfileVo toProfileVo(UserBo bo); User toEntity(UserProfileForm formData); diff --git a/src/main/java/com/youlai/boot/system/enums/NoticePublishStatusEnum.java b/src/main/java/com/youlai/boot/system/enums/NoticePublishStatusEnum.java index bedd5e53..ecda82d1 100644 --- a/src/main/java/com/youlai/boot/system/enums/NoticePublishStatusEnum.java +++ b/src/main/java/com/youlai/boot/system/enums/NoticePublishStatusEnum.java @@ -16,7 +16,7 @@ public enum NoticePublishStatusEnum implements IBaseEnum { UNPUBLISHED(0, "未发布"), PUBLISHED(1, "已发布"), - REVOKED(-1, "已撤回"); + REVoKED(-1, "已撤回"); private final Integer value; diff --git a/src/main/java/com/youlai/boot/system/handler/OnlineUserJobHandler.java b/src/main/java/com/youlai/boot/system/handler/OnlineUserJobHandler.java index 39d1b00f..3fcb7a80 100644 --- a/src/main/java/com/youlai/boot/system/handler/OnlineUserJobHandler.java +++ b/src/main/java/com/youlai/boot/system/handler/OnlineUserJobHandler.java @@ -2,9 +2,10 @@ package com.youlai.boot.system.handler; import com.youlai.boot.system.service.UserOnlineService; +import com.youlai.boot.platform.websocket.publisher.WebSocketPublisher; +import com.youlai.boot.platform.websocket.topic.WebSocketTopics; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @@ -20,7 +21,7 @@ import org.springframework.stereotype.Component; public class OnlineUserJobHandler { private final UserOnlineService userOnlineService; - private final SimpMessagingTemplate messagingTemplate; + private final WebSocketPublisher webSocketPublisher; // 每3分钟统计一次在线用户数,减少服务器压力 @Scheduled(cron = "0 */3 * * * ?") @@ -28,7 +29,7 @@ public class OnlineUserJobHandler { log.info("定时任务:统计在线用户数"); // 推送在线用户数量到新主题 int count = userOnlineService.getOnlineUserCount(); - messagingTemplate.convertAndSend("/topic/online-count", count); + webSocketPublisher.publish(WebSocketTopics.TOPIC_ONLINE_COUNT, count); } } diff --git a/src/main/java/com/youlai/boot/system/listener/UserImportListener.java b/src/main/java/com/youlai/boot/system/listener/UserImportListener.java index a699fb54..66a23f29 100644 --- a/src/main/java/com/youlai/boot/system/listener/UserImportListener.java +++ b/src/main/java/com/youlai/boot/system/listener/UserImportListener.java @@ -14,7 +14,7 @@ import com.youlai.boot.common.enums.StatusEnum; import com.youlai.boot.core.web.ExcelResult; import com.youlai.boot.system.converter.UserConverter; import com.youlai.boot.system.enums.DictCodeEnum; -import com.youlai.boot.system.model.dto.UserImportDTO; +import com.youlai.boot.system.model.dto.UserImportDto; import com.youlai.boot.system.model.entity.*; import com.youlai.boot.system.service.*; import lombok.Getter; @@ -35,7 +35,7 @@ import java.util.stream.Collectors; * @since 2022/4/10 */ @Slf4j -public class UserImportListener extends AnalysisEventListener { +public class UserImportListener extends AnalysisEventListener { /** * Excel 导入结果 @@ -82,15 +82,15 @@ public class UserImportListener extends AnalysisEventListener { * 1. 数据校验;全字段校验 * 2. 数据持久化; * - * @param userImportDTO 一行数据,类似于 {@link AnalysisContext#readRowHolder()} + * @param userImportDto 一行数据,类似于 {@link AnalysisContext#readRowHolder()} */ @Override - public void invoke(UserImportDTO userImportDTO, AnalysisContext analysisContext) { - log.info("解析到一条用户数据:{}", JSONUtil.toJsonStr(userImportDTO)); + public void invoke(UserImportDto userImportDto, AnalysisContext analysisContext) { + log.info("解析到一条用户数据:{}", JSONUtil.toJsonStr(userImportDto)); boolean validation = true; String errorMsg = "第" + currentRow + "行数据校验失败:"; - String username = userImportDTO.getUsername(); + String username = userImportDto.getUsername(); if (StrUtil.isBlank(username)) { errorMsg += "用户名为空;"; validation = false; @@ -102,13 +102,13 @@ public class UserImportListener extends AnalysisEventListener { } } - String nickname = userImportDTO.getNickname(); + String nickname = userImportDto.getNickname(); if (StrUtil.isBlank(nickname)) { errorMsg += "用户昵称为空;"; validation = false; } - String mobile = userImportDTO.getMobile(); + String mobile = userImportDto.getMobile(); if (StrUtil.isBlank(mobile)) { errorMsg += "手机号码为空;"; validation = false; @@ -121,16 +121,16 @@ public class UserImportListener extends AnalysisEventListener { if (validation) { // 校验通过,持久化至数据库 - User entity = userConverter.toEntity(userImportDTO); + User entity = userConverter.toEntity(userImportDto); entity.setPassword(passwordEncoder.encode(SystemConstants.DEFAULT_PASSWORD)); // 默认密码 // 性别逆向翻译 根据字典标签得到字典值 - String genderLabel = userImportDTO.getGenderLabel(); + String genderLabel = userImportDto.getGenderLabel(); entity.setGender(getGenderValue(genderLabel)); // 角色解析 - String roleCodes = userImportDTO.getRoleCodes(); + String roleCodes = userImportDto.getRoleCodes(); List roleIds = getRoleIds(roleCodes); // 部门解析 - String deptCode = userImportDTO.getDeptCode(); + String deptCode = userImportDto.getDeptCode(); entity.setDeptId(getDeptId(deptCode)); boolean saveResult = userService.save(entity); diff --git a/src/main/java/com/youlai/boot/system/mapper/DictItemMapper.java b/src/main/java/com/youlai/boot/system/mapper/DictItemMapper.java index 1a0e1426..be5901cf 100644 --- a/src/main/java/com/youlai/boot/system/mapper/DictItemMapper.java +++ b/src/main/java/com/youlai/boot/system/mapper/DictItemMapper.java @@ -4,7 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.youlai.boot.system.model.entity.DictItem; import com.youlai.boot.system.model.query.DictItemPageQuery; -import com.youlai.boot.system.model.vo.DictItemPageVO; +import com.youlai.boot.system.model.vo.DictItemPageVo; import org.apache.ibatis.annotations.Mapper; /** @@ -19,7 +19,7 @@ public interface DictItemMapper extends BaseMapper { /** * 字典项分页列表 */ - Page getDictItemPage(Page page, DictItemPageQuery queryParams); + Page getDictItemPage(Page page, DictItemPageQuery queryParams); } diff --git a/src/main/java/com/youlai/boot/system/mapper/DictMapper.java b/src/main/java/com/youlai/boot/system/mapper/DictMapper.java index a40fbb29..d2c76fb8 100644 --- a/src/main/java/com/youlai/boot/system/mapper/DictMapper.java +++ b/src/main/java/com/youlai/boot/system/mapper/DictMapper.java @@ -4,7 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.youlai.boot.system.model.entity.Dict; import com.youlai.boot.system.model.query.DictPageQuery; -import com.youlai.boot.system.model.vo.DictPageVO; +import com.youlai.boot.system.model.vo.DictPageVo; import org.apache.ibatis.annotations.Mapper; /** @@ -23,7 +23,7 @@ public interface DictMapper extends BaseMapper { * @param queryParams 查询参数 * @return 字典分页列表 */ - Page getDictPage(Page page, DictPageQuery queryParams); + Page getDictPage(Page page, DictPageQuery queryParams); } 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 a52ae257..21fa6017 100644 --- a/src/main/java/com/youlai/boot/system/mapper/LogMapper.java +++ b/src/main/java/com/youlai/boot/system/mapper/LogMapper.java @@ -2,11 +2,11 @@ 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.bo.VisitCountBo; +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.LogPageVo; import org.apache.ibatis.annotations.Mapper; import java.util.List; @@ -24,7 +24,7 @@ public interface LogMapper extends BaseMapper { /** * 获取日志分页列表 */ - Page getLogPage(Page page, LogPageQuery queryParams); + Page getLogPage(Page page, LogPageQuery queryParams); /** * 统计浏览数(PV) @@ -32,7 +32,7 @@ public interface LogMapper extends BaseMapper { * @param startDate 开始日期 yyyy-MM-dd * @param endDate 结束日期 yyyy-MM-dd */ - List getPvCounts(String startDate, String endDate); + List getPvCounts(String startDate, String endDate); /** * 统计IP数 @@ -40,17 +40,17 @@ public interface LogMapper extends BaseMapper { * @param startDate 开始日期 yyyy-MM-dd * @param endDate 结束日期 yyyy-MM-dd */ - List getIpCounts(String startDate, String endDate); + List getIpCounts(String startDate, String endDate); /** * 获取浏览量(PV)统计 */ - VisitStatsBO getPvStats(); + VisitStatsBo getPvStats(); /** * 获取访问IP统计 */ - VisitStatsBO getUvStats(); + VisitStatsBo getUvStats(); } diff --git a/src/main/java/com/youlai/boot/system/mapper/NoticeMapper.java b/src/main/java/com/youlai/boot/system/mapper/NoticeMapper.java index 74ad716d..f46c270d 100644 --- a/src/main/java/com/youlai/boot/system/mapper/NoticeMapper.java +++ b/src/main/java/com/youlai/boot/system/mapper/NoticeMapper.java @@ -2,10 +2,10 @@ 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.NoticeBO; +import com.youlai.boot.system.model.bo.NoticeBo; import com.youlai.boot.system.model.entity.Notice; import com.youlai.boot.system.model.query.NoticePageQuery; -import com.youlai.boot.system.model.vo.NoticePageVO; +import com.youlai.boot.system.model.vo.NoticePageVo; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @@ -25,7 +25,7 @@ public interface NoticeMapper extends BaseMapper { * @param queryParams 查询参数 * @return 通知公告分页数据 */ - Page getNoticePage(Page page, NoticePageQuery queryParams); + Page getNoticePage(Page page, NoticePageQuery queryParams); /** * 获取阅读时通知公告详情 @@ -33,5 +33,5 @@ public interface NoticeMapper extends BaseMapper { * @param id 通知公告ID * @return 通知公告详情 */ - NoticeBO getNoticeDetail(@Param("id") Long id); + NoticeBo getNoticeDetail(@Param("id") Long id); } diff --git a/src/main/java/com/youlai/boot/system/mapper/RoleMenuMapper.java b/src/main/java/com/youlai/boot/system/mapper/RoleMenuMapper.java index 5f68f18e..111d4e53 100644 --- a/src/main/java/com/youlai/boot/system/mapper/RoleMenuMapper.java +++ b/src/main/java/com/youlai/boot/system/mapper/RoleMenuMapper.java @@ -1,7 +1,7 @@ package com.youlai.boot.system.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.youlai.boot.system.model.bo.RolePermsBO; +import com.youlai.boot.system.model.bo.RolePermsBo; import com.youlai.boot.system.model.entity.RoleMenu; import org.apache.ibatis.annotations.Mapper; @@ -28,7 +28,7 @@ public interface RoleMenuMapper extends BaseMapper { /** * 获取权限和拥有权限的角色列表 */ - List getRolePermsList(String roleCode); + List getRolePermsList(String roleCode); /** diff --git a/src/main/java/com/youlai/boot/system/mapper/UserMapper.java b/src/main/java/com/youlai/boot/system/mapper/UserMapper.java index 74af1bd5..db110363 100644 --- a/src/main/java/com/youlai/boot/system/mapper/UserMapper.java +++ b/src/main/java/com/youlai/boot/system/mapper/UserMapper.java @@ -2,13 +2,13 @@ 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.UserBO; +import com.youlai.boot.system.model.bo.UserBo; import com.youlai.boot.system.model.entity.User; import com.youlai.boot.system.model.query.UserPageQuery; import com.youlai.boot.system.model.form.UserForm; import com.youlai.boot.common.annotation.DataPermission; -import com.youlai.boot.security.model.UserAuthCredentials; -import com.youlai.boot.system.model.dto.UserExportDTO; +import com.youlai.boot.security.model.UserAuthInfo; +import com.youlai.boot.system.model.dto.UserExportDto; import org.apache.ibatis.annotations.Mapper; import java.util.List; @@ -30,7 +30,7 @@ public interface UserMapper extends BaseMapper { * @return 用户分页列表 */ @DataPermission(deptAlias = "u", userAlias = "u") - Page getUserPage(Page page, UserPageQuery queryParams); + Page getUserPage(Page page, UserPageQuery queryParams); /** * 获取用户表单详情 @@ -46,7 +46,11 @@ public interface UserMapper extends BaseMapper { * @param username 用户名 * @return 认证信息 */ - UserAuthCredentials getAuthCredentialsByUsername(String username); + UserAuthInfo getAuthInfoByUsername(String username); + + default UserAuthInfo getAuthCredentialsByUsername(String username) { + return getAuthInfoByUsername(username); + } /** * 根据微信openid获取用户认证信息 @@ -54,7 +58,11 @@ public interface UserMapper extends BaseMapper { * @param openid 微信openid * @return 认证信息 */ - UserAuthCredentials getAuthCredentialsByOpenId(String openid); + UserAuthInfo getAuthInfoByOpenId(String openid); + + default UserAuthInfo getAuthCredentialsByOpenId(String openid) { + return getAuthInfoByOpenId(openid); + } /** * 根据手机号获取用户认证信息 @@ -62,7 +70,11 @@ public interface UserMapper extends BaseMapper { * @param mobile 手机号 * @return 认证信息 */ - UserAuthCredentials getAuthCredentialsByMobile(String mobile); + UserAuthInfo getAuthInfoByMobile(String mobile); + + default UserAuthInfo getAuthCredentialsByMobile(String mobile) { + return getAuthInfoByMobile(mobile); + } /** * 获取导出用户列表 @@ -71,7 +83,7 @@ public interface UserMapper extends BaseMapper { * @return 导出用户列表 */ @DataPermission(deptAlias = "u", userAlias = "u") - List listExportUsers(UserPageQuery queryParams); + List listExportUsers(UserPageQuery queryParams); /** * 获取用户个人中心信息 @@ -79,6 +91,6 @@ public interface UserMapper extends BaseMapper { * @param userId 用户ID * @return 用户个人中心信息 */ - UserBO getUserProfile(Long userId); + UserBo getUserProfile(Long userId); } diff --git a/src/main/java/com/youlai/boot/system/mapper/UserNoticeMapper.java b/src/main/java/com/youlai/boot/system/mapper/UserNoticeMapper.java index e874cd3e..412ef549 100644 --- a/src/main/java/com/youlai/boot/system/mapper/UserNoticeMapper.java +++ b/src/main/java/com/youlai/boot/system/mapper/UserNoticeMapper.java @@ -5,8 +5,8 @@ import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.youlai.boot.system.model.entity.UserNotice; import com.youlai.boot.system.model.query.NoticePageQuery; -import com.youlai.boot.system.model.vo.NoticePageVO; -import com.youlai.boot.system.model.vo.UserNoticePageVO; +import com.youlai.boot.system.model.vo.NoticePageVo; +import com.youlai.boot.system.model.vo.UserNoticePageVo; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @@ -26,5 +26,5 @@ public interface UserNoticeMapper extends BaseMapper { * @param queryParams 查询参数 * @return 通知公告分页列表 */ - IPage getMyNoticePage(Page page, @Param("queryParams") NoticePageQuery queryParams); + IPage getMyNoticePage(Page page, @Param("queryParams") NoticePageQuery queryParams); } diff --git a/src/main/java/com/youlai/boot/system/mapper/UserRoleMapper.java b/src/main/java/com/youlai/boot/system/mapper/UserRoleMapper.java index 196c2a52..38b24507 100644 --- a/src/main/java/com/youlai/boot/system/mapper/UserRoleMapper.java +++ b/src/main/java/com/youlai/boot/system/mapper/UserRoleMapper.java @@ -18,5 +18,5 @@ public interface UserRoleMapper extends BaseMapper { * * @param roleId 角色ID */ - int countUsersForRole(Long roleId); + int countUsersByRoleId(Long roleId); } diff --git a/src/main/java/com/youlai/boot/system/model/bo/NoticeBO.java b/src/main/java/com/youlai/boot/system/model/bo/NoticeBO.java index 72175c47..23b85991 100644 --- a/src/main/java/com/youlai/boot/system/model/bo/NoticeBO.java +++ b/src/main/java/com/youlai/boot/system/model/bo/NoticeBO.java @@ -11,7 +11,7 @@ import java.time.LocalDateTime; * @since 2024-09-01 10:31 */ @Data -public class NoticeBO { +public class NoticeBo { /** * 通知ID diff --git a/src/main/java/com/youlai/boot/system/model/bo/RolePermsBO.java b/src/main/java/com/youlai/boot/system/model/bo/RolePermsBO.java index 198e8180..ff3484c8 100644 --- a/src/main/java/com/youlai/boot/system/model/bo/RolePermsBO.java +++ b/src/main/java/com/youlai/boot/system/model/bo/RolePermsBO.java @@ -11,7 +11,7 @@ import java.util.Set; * @since 2023/11/29 */ @Data -public class RolePermsBO { +public class RolePermsBo { /** * 角色编码 diff --git a/src/main/java/com/youlai/boot/system/model/bo/UserBO.java b/src/main/java/com/youlai/boot/system/model/bo/UserBO.java index 7fbc61ae..b6d901df 100644 --- a/src/main/java/com/youlai/boot/system/model/bo/UserBO.java +++ b/src/main/java/com/youlai/boot/system/model/bo/UserBO.java @@ -7,11 +7,11 @@ import java.time.LocalDateTime; /** * 用户持久化对象 * - * @author haoxr + * @author Ray.Hao * @since 2022/6/10 */ @Data -public class UserBO { +public class UserBo { /** * 用户ID diff --git a/src/main/java/com/youlai/boot/system/model/bo/VisitCount.java b/src/main/java/com/youlai/boot/system/model/bo/VisitCountBo.java similarity index 90% rename from src/main/java/com/youlai/boot/system/model/bo/VisitCount.java rename to src/main/java/com/youlai/boot/system/model/bo/VisitCountBo.java index a5e1b81c..ce6e7c34 100644 --- a/src/main/java/com/youlai/boot/system/model/bo/VisitCount.java +++ b/src/main/java/com/youlai/boot/system/model/bo/VisitCountBo.java @@ -9,7 +9,7 @@ import lombok.Data; * @since 2.10.0 */ @Data -public class VisitCount { +public class VisitCountBo { /** * 日期 yyyy-MM-dd diff --git a/src/main/java/com/youlai/boot/system/model/bo/VisitStatsBO.java b/src/main/java/com/youlai/boot/system/model/bo/VisitStatsBO.java index 2fba9179..5417f14e 100644 --- a/src/main/java/com/youlai/boot/system/model/bo/VisitStatsBO.java +++ b/src/main/java/com/youlai/boot/system/model/bo/VisitStatsBO.java @@ -14,7 +14,7 @@ import java.math.BigDecimal; */ @Getter @Setter -public class VisitStatsBO { +public class VisitStatsBo { @Schema(description = "今日访问量 (PV)") private Integer todayCount; diff --git a/src/main/java/com/youlai/boot/system/model/dto/CurrentUserDTO.java b/src/main/java/com/youlai/boot/system/model/dto/CurrentUserDTO.java index e29bd22b..e0044d25 100644 --- a/src/main/java/com/youlai/boot/system/model/dto/CurrentUserDTO.java +++ b/src/main/java/com/youlai/boot/system/model/dto/CurrentUserDTO.java @@ -13,7 +13,7 @@ import java.util.Set; */ @Schema(description ="当前登录用户对象") @Data -public class CurrentUserDTO { +public class CurrentUserDto { @Schema(description="用户ID") private Long userId; diff --git a/src/main/java/com/youlai/boot/system/model/dto/DictEventDTO.java b/src/main/java/com/youlai/boot/system/model/dto/DictEventDTO.java deleted file mode 100644 index b3cae343..00000000 --- a/src/main/java/com/youlai/boot/system/model/dto/DictEventDTO.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.youlai.boot.system.model.dto; - -import lombok.Data; - -/** - * 字典更新事件消息 - * - * @author Ray.Hao - * @since 3.0.0 - */ -@Data -public class DictEventDTO { - /** - * 字典编码 - */ - private String dictCode; - - /** - * 时间戳 - */ - private long timestamp; - - public DictEventDTO(String dictCode) { - this.dictCode = dictCode; - this.timestamp = System.currentTimeMillis(); - } -} - diff --git a/src/main/java/com/youlai/boot/system/model/dto/NoticeDTO.java b/src/main/java/com/youlai/boot/system/model/dto/NoticeDTO.java index fd9ac242..8c6fbe18 100644 --- a/src/main/java/com/youlai/boot/system/model/dto/NoticeDTO.java +++ b/src/main/java/com/youlai/boot/system/model/dto/NoticeDTO.java @@ -13,7 +13,7 @@ import java.time.LocalDateTime; * @since 2024-9-2 14:32:58 */ @Data -public class NoticeDTO { +public class NoticeDto { @Schema(description = "通知ID") private Long id; @@ -28,5 +28,4 @@ public class NoticeDTO { @JsonFormat(pattern = "yyyy-MM-dd HH:mm") private LocalDateTime publishTime; - } diff --git a/src/main/java/com/youlai/boot/system/model/dto/UserExportDTO.java b/src/main/java/com/youlai/boot/system/model/dto/UserExportDTO.java index 7c472c61..975ace9b 100644 --- a/src/main/java/com/youlai/boot/system/model/dto/UserExportDTO.java +++ b/src/main/java/com/youlai/boot/system/model/dto/UserExportDTO.java @@ -16,7 +16,7 @@ import java.time.LocalDateTime; @Data @ColumnWidth(20) -public class UserExportDTO { +public class UserExportDto { @ExcelProperty(value = "用户名") private String username; diff --git a/src/main/java/com/youlai/boot/system/model/dto/UserImportDTO.java b/src/main/java/com/youlai/boot/system/model/dto/UserImportDTO.java index 01357c7a..876b5798 100644 --- a/src/main/java/com/youlai/boot/system/model/dto/UserImportDTO.java +++ b/src/main/java/com/youlai/boot/system/model/dto/UserImportDTO.java @@ -10,7 +10,7 @@ import lombok.Data; * @since 2022/4/10 */ @Data -public class UserImportDTO { +public class UserImportDto { @ExcelProperty(value = "用户名") private String username; diff --git a/src/main/java/com/youlai/boot/system/model/dto/UserSessionDTO.java b/src/main/java/com/youlai/boot/system/model/dto/UserSessionDTO.java index 80cb44c1..f1903acb 100644 --- a/src/main/java/com/youlai/boot/system/model/dto/UserSessionDTO.java +++ b/src/main/java/com/youlai/boot/system/model/dto/UserSessionDTO.java @@ -6,13 +6,13 @@ import java.util.HashSet; import java.util.Set; /** - * 用户会话DTO + * 用户会话Dto * * @author Ray.Hao * @since 3.0.0 */ @Data -public class UserSessionDTO { +public class UserSessionDto { /** * 用户名 @@ -29,7 +29,7 @@ public class UserSessionDTO { */ private long lastActiveTime; - public UserSessionDTO(String username) { + public UserSessionDto(String username) { this.username = username; this.sessionIds = new HashSet<>(); this.lastActiveTime = System.currentTimeMillis(); diff --git a/src/main/java/com/youlai/boot/system/model/vo/ConfigVO.java b/src/main/java/com/youlai/boot/system/model/vo/ConfigVO.java index 12bf2f47..2343d745 100644 --- a/src/main/java/com/youlai/boot/system/model/vo/ConfigVO.java +++ b/src/main/java/com/youlai/boot/system/model/vo/ConfigVO.java @@ -18,8 +18,8 @@ import java.io.Serializable; @Data @Builder @EqualsAndHashCode(callSuper = false) -@Schema(description = "系统配置VO") -public class ConfigVO { +@Schema(description = "系统配置Vo") +public class ConfigVo { @Schema(description = "主键") private Long id; diff --git a/src/main/java/com/youlai/boot/system/model/vo/DeptVO.java b/src/main/java/com/youlai/boot/system/model/vo/DeptVO.java index d2249f4d..9b3d88c9 100644 --- a/src/main/java/com/youlai/boot/system/model/vo/DeptVO.java +++ b/src/main/java/com/youlai/boot/system/model/vo/DeptVO.java @@ -9,7 +9,7 @@ import java.util.List; @Schema(description = "部门视图对象") @Data -public class DeptVO { +public class DeptVo { @Schema(description = "部门ID") private Long id; @@ -30,7 +30,7 @@ public class DeptVO { private Integer status; @Schema(description = "子部门") - private List children; + private List children; @Schema(description = "创建时间") @JsonFormat(pattern = "yyyy-MM-dd HH:mm") diff --git a/src/main/java/com/youlai/boot/system/model/vo/DictItemOptionVO.java b/src/main/java/com/youlai/boot/system/model/vo/DictItemOptionVO.java index 8470f1b7..dc69f029 100644 --- a/src/main/java/com/youlai/boot/system/model/vo/DictItemOptionVO.java +++ b/src/main/java/com/youlai/boot/system/model/vo/DictItemOptionVO.java @@ -13,7 +13,7 @@ import lombok.Setter; @Schema(description = "字典项键值对象") @Getter @Setter -public class DictItemOptionVO { +public class DictItemOptionVo { @Schema(description = "字典项值") private String value; diff --git a/src/main/java/com/youlai/boot/system/model/vo/DictItemPageVO.java b/src/main/java/com/youlai/boot/system/model/vo/DictItemPageVO.java index 019da827..01529d44 100644 --- a/src/main/java/com/youlai/boot/system/model/vo/DictItemPageVO.java +++ b/src/main/java/com/youlai/boot/system/model/vo/DictItemPageVO.java @@ -14,7 +14,7 @@ import lombok.Setter; @Schema(description = "字典项分页对象") @Getter @Setter -public class DictItemPageVO { +public class DictItemPageVo { @Schema(description = "字典项ID") private Long id; diff --git a/src/main/java/com/youlai/boot/system/model/vo/DictPageVO.java b/src/main/java/com/youlai/boot/system/model/vo/DictPageVO.java index 6df5d53c..a4ecb826 100644 --- a/src/main/java/com/youlai/boot/system/model/vo/DictPageVO.java +++ b/src/main/java/com/youlai/boot/system/model/vo/DictPageVO.java @@ -9,7 +9,7 @@ import java.util.List; /** - * 字典分页VO + * 字典分页Vo * * @author Ray * @since 0.0.1 @@ -17,7 +17,7 @@ import java.util.List; @Schema(description = "字典分页对象") @Getter @Setter -public class DictPageVO { +public class DictPageVo { @Schema(description = "字典ID") private Long id; diff --git a/src/main/java/com/youlai/boot/system/model/vo/LogPageVO.java b/src/main/java/com/youlai/boot/system/model/vo/LogPageVO.java index 253c34c0..95944e89 100644 --- a/src/main/java/com/youlai/boot/system/model/vo/LogPageVO.java +++ b/src/main/java/com/youlai/boot/system/model/vo/LogPageVO.java @@ -9,14 +9,14 @@ import java.io.Serializable; import java.time.LocalDateTime; /** - * 系统日志分页VO + * 系统日志分页Vo * * @author Ray * @since 2.10.0 */ @Data -@Schema(description = "系统日志分页VO") -public class LogPageVO implements Serializable { +@Schema(description = "系统日志分页Vo") +public class LogPageVo implements Serializable { @Schema(description = "主键") private Long id; 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 18a3ca52..ee9cddfe 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 @@ -8,7 +8,7 @@ import java.util.List; @Schema(description ="菜单视图对象") @Data -public class MenuVO { +public class MenuVo { @Schema(description = "菜单ID") private Long id; @@ -48,6 +48,6 @@ public class MenuVO { @Schema(description = "子菜单") @JsonInclude(value = JsonInclude.Include.NON_NULL) - private List children; + private List children; } diff --git a/src/main/java/com/youlai/boot/system/model/vo/NoticeDetailVO.java b/src/main/java/com/youlai/boot/system/model/vo/NoticeDetailVO.java index c8a7a22b..30d2f852 100644 --- a/src/main/java/com/youlai/boot/system/model/vo/NoticeDetailVO.java +++ b/src/main/java/com/youlai/boot/system/model/vo/NoticeDetailVO.java @@ -7,13 +7,13 @@ import lombok.Data; import java.time.LocalDateTime; /** - * 阅读通知公告VO + * 阅读通知公告Vo * * @author Theo * @since 2024-9-8 01:25:06 */ @Data -public class NoticeDetailVO { +public class NoticeDetailVo { @Schema(description = "通知ID") private Long id; diff --git a/src/main/java/com/youlai/boot/system/model/vo/NoticePageVO.java b/src/main/java/com/youlai/boot/system/model/vo/NoticePageVO.java index 25de2efe..08224a46 100644 --- a/src/main/java/com/youlai/boot/system/model/vo/NoticePageVO.java +++ b/src/main/java/com/youlai/boot/system/model/vo/NoticePageVO.java @@ -18,7 +18,7 @@ import java.time.LocalDateTime; @Getter @Setter @Schema(description = "通知公告视图对象") -public class NoticePageVO implements Serializable { +public class NoticePageVo implements Serializable { @Serial private static final long serialVersionUID = 1L; diff --git a/src/main/java/com/youlai/boot/system/model/vo/RolePageVO.java b/src/main/java/com/youlai/boot/system/model/vo/RolePageVO.java index 86a1ce21..6c5e547a 100644 --- a/src/main/java/com/youlai/boot/system/model/vo/RolePageVO.java +++ b/src/main/java/com/youlai/boot/system/model/vo/RolePageVO.java @@ -8,7 +8,7 @@ import java.time.LocalDateTime; @Schema(description ="角色分页对象") @Data -public class RolePageVO { +public class RolePageVo { @Schema(description="角色ID") private Long id; diff --git a/src/main/java/com/youlai/boot/system/model/vo/RouteVO.java b/src/main/java/com/youlai/boot/system/model/vo/RouteVO.java index 8c786f02..88c73fcc 100644 --- a/src/main/java/com/youlai/boot/system/model/vo/RouteVO.java +++ b/src/main/java/com/youlai/boot/system/model/vo/RouteVO.java @@ -16,7 +16,7 @@ import java.util.Map; @Schema(description = "路由对象") @Data @JsonInclude(JsonInclude.Include.NON_EMPTY) -public class RouteVO { +public class RouteVo { @Schema(description = "路由路径", example = "user") private String path; @@ -59,5 +59,5 @@ public class RouteVO { } @Schema(description = "子路由列表") - private List children; + private List children; } diff --git a/src/main/java/com/youlai/boot/system/model/vo/UserNoticePageVO.java b/src/main/java/com/youlai/boot/system/model/vo/UserNoticePageVO.java index a21643fb..bb17ba2b 100644 --- a/src/main/java/com/youlai/boot/system/model/vo/UserNoticePageVO.java +++ b/src/main/java/com/youlai/boot/system/model/vo/UserNoticePageVO.java @@ -7,14 +7,14 @@ import lombok.Data; import java.time.LocalDateTime; /** - * 用户公告VO + * 用户公告Vo * * @author Theo * @since 2024-08-28 16:56 */ @Data -@Schema(description = "用户公告VO") -public class UserNoticePageVO { +@Schema(description = "用户公告Vo") +public class UserNoticePageVo { @Schema(description = "通知ID") private Long id; diff --git a/src/main/java/com/youlai/boot/system/model/vo/UserPageVO.java b/src/main/java/com/youlai/boot/system/model/vo/UserPageVO.java index 0baa4c5b..e45c11f2 100644 --- a/src/main/java/com/youlai/boot/system/model/vo/UserPageVO.java +++ b/src/main/java/com/youlai/boot/system/model/vo/UserPageVO.java @@ -14,7 +14,7 @@ import java.time.LocalDateTime; */ @Schema(description ="用户分页对象") @Data -public class UserPageVO { +public class UserPageVo { @Schema(description="用户ID") private Long id; diff --git a/src/main/java/com/youlai/boot/system/model/vo/UserProfileVO.java b/src/main/java/com/youlai/boot/system/model/vo/UserProfileVO.java index 614f917b..5bbcfa15 100644 --- a/src/main/java/com/youlai/boot/system/model/vo/UserProfileVO.java +++ b/src/main/java/com/youlai/boot/system/model/vo/UserProfileVO.java @@ -14,7 +14,7 @@ import java.util.Date; */ @Schema(description = "个人中心用户信息") @Data -public class UserProfileVO { +public class UserProfileVo { @Schema(description = "用户ID") private Long id; diff --git a/src/main/java/com/youlai/boot/system/model/vo/VisitStatsVO.java b/src/main/java/com/youlai/boot/system/model/vo/VisitStatsVO.java index 3ffd5dab..7c71a40b 100644 --- a/src/main/java/com/youlai/boot/system/model/vo/VisitStatsVO.java +++ b/src/main/java/com/youlai/boot/system/model/vo/VisitStatsVO.java @@ -15,7 +15,7 @@ import java.math.BigDecimal; @Schema(description = "访问量统计视图对象") @Getter @Setter -public class VisitStatsVO { +public class VisitStatsVo { @Schema(description = "今日独立访客数 (UV)") private Integer todayUvCount; diff --git a/src/main/java/com/youlai/boot/system/model/vo/VisitTrendVO.java b/src/main/java/com/youlai/boot/system/model/vo/VisitTrendVO.java index 42e8a2cb..25e5b784 100644 --- a/src/main/java/com/youlai/boot/system/model/vo/VisitTrendVO.java +++ b/src/main/java/com/youlai/boot/system/model/vo/VisitTrendVO.java @@ -8,15 +8,15 @@ import lombok.Setter; import java.util.List; /** - * 访问趋势VO + * 访问趋势Vo * * @author Ray.Hao * @since 2.3.0 */ -@Schema(description = "访问趋势VO") +@Schema(description = "访问趋势Vo") @Getter @Setter -public class VisitTrendVO { +public class VisitTrendVo { @Schema(description = "日期列表") private List dates; diff --git a/src/main/java/com/youlai/boot/system/service/ConfigService.java b/src/main/java/com/youlai/boot/system/service/ConfigService.java index 988db94d..68d24eb5 100644 --- a/src/main/java/com/youlai/boot/system/service/ConfigService.java +++ b/src/main/java/com/youlai/boot/system/service/ConfigService.java @@ -5,7 +5,7 @@ import com.baomidou.mybatisplus.extension.service.IService; import com.youlai.boot.system.model.entity.Config; import com.youlai.boot.system.model.form.ConfigForm; import com.youlai.boot.system.model.query.ConfigPageQuery; -import com.youlai.boot.system.model.vo.ConfigVO; +import com.youlai.boot.system.model.vo.ConfigVo; /** * 系统配置Service接口 @@ -20,7 +20,7 @@ public interface ConfigService extends IService { * @param sysConfigPageQuery 查询参数 * @return 系统配置分页列表 */ - IPage page(ConfigPageQuery sysConfigPageQuery); + IPage page(ConfigPageQuery sysConfigPageQuery); /** * 保存系统配置 diff --git a/src/main/java/com/youlai/boot/system/service/DeptService.java b/src/main/java/com/youlai/boot/system/service/DeptService.java index 850a948e..f724fb77 100644 --- a/src/main/java/com/youlai/boot/system/service/DeptService.java +++ b/src/main/java/com/youlai/boot/system/service/DeptService.java @@ -5,7 +5,7 @@ import com.youlai.boot.system.model.entity.Dept; import com.youlai.boot.common.model.Option; import com.youlai.boot.system.model.form.DeptForm; import com.youlai.boot.system.model.query.DeptQuery; -import com.youlai.boot.system.model.vo.DeptVO; +import com.youlai.boot.system.model.vo.DeptVo; import java.util.List; @@ -21,7 +21,7 @@ public interface DeptService extends IService { * * @return 部门列表 */ - List getDeptList(DeptQuery queryParams); + List getDeptList(DeptQuery queryParams); /** * 部门树形下拉选项 diff --git a/src/main/java/com/youlai/boot/system/service/DictItemService.java b/src/main/java/com/youlai/boot/system/service/DictItemService.java index 54d4d197..7439b980 100644 --- a/src/main/java/com/youlai/boot/system/service/DictItemService.java +++ b/src/main/java/com/youlai/boot/system/service/DictItemService.java @@ -5,8 +5,8 @@ import com.baomidou.mybatisplus.extension.service.IService; import com.youlai.boot.system.model.entity.DictItem; import com.youlai.boot.system.model.form.DictItemForm; import com.youlai.boot.system.model.query.DictItemPageQuery; -import com.youlai.boot.system.model.vo.DictItemOptionVO; -import com.youlai.boot.system.model.vo.DictItemPageVO; +import com.youlai.boot.system.model.vo.DictItemOptionVo; +import com.youlai.boot.system.model.vo.DictItemPageVo; import java.util.List; @@ -24,7 +24,7 @@ public interface DictItemService extends IService { * @param queryParams 查询参数 * @return 字典项分页列表 */ - Page getDictItemPage(DictItemPageQuery queryParams); + Page getDictItemPage(DictItemPageQuery queryParams); /** * 获取字典项列表 @@ -32,7 +32,7 @@ public interface DictItemService extends IService { * @param dictCode 字典编码 * @return 字典项列表 */ - List getDictItems(String dictCode); + List getDictItems(String dictCode); /** * 获取字典项表单 diff --git a/src/main/java/com/youlai/boot/system/service/DictService.java b/src/main/java/com/youlai/boot/system/service/DictService.java index 5ab06ab7..a22c7f91 100644 --- a/src/main/java/com/youlai/boot/system/service/DictService.java +++ b/src/main/java/com/youlai/boot/system/service/DictService.java @@ -6,8 +6,8 @@ import com.youlai.boot.common.model.Option; import com.youlai.boot.system.model.entity.Dict; import com.youlai.boot.system.model.form.DictForm; import com.youlai.boot.system.model.query.DictPageQuery; -import com.youlai.boot.system.model.vo.DictItemOptionVO; -import com.youlai.boot.system.model.vo.DictPageVO; +import com.youlai.boot.system.model.vo.DictItemOptionVo; +import com.youlai.boot.system.model.vo.DictPageVo; import java.util.List; @@ -25,7 +25,7 @@ public interface DictService extends IService { * @param queryParams 分页查询对象 * @return 字典分页列表 */ - Page getDictPage(DictPageQuery queryParams); + Page getDictPage(DictPageQuery queryParams); /** * 获取字典列表 diff --git a/src/main/java/com/youlai/boot/system/service/LogService.java b/src/main/java/com/youlai/boot/system/service/LogService.java index 8a621e66..58e13718 100644 --- a/src/main/java/com/youlai/boot/system/service/LogService.java +++ b/src/main/java/com/youlai/boot/system/service/LogService.java @@ -4,9 +4,9 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.youlai.boot.system.model.entity.Log; import com.baomidou.mybatisplus.extension.service.IService; 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.youlai.boot.system.model.vo.VisitTrendVO; +import com.youlai.boot.system.model.vo.LogPageVo; +import com.youlai.boot.system.model.vo.VisitStatsVo; +import com.youlai.boot.system.model.vo.VisitTrendVo; import java.time.LocalDate; import java.util.List; @@ -22,7 +22,7 @@ public interface LogService extends IService { /** * 获取日志分页列表 */ - Page getLogPage(LogPageQuery queryParams); + Page getLogPage(LogPageQuery queryParams); /** @@ -31,11 +31,11 @@ public interface LogService extends IService { * @param startDate 开始时间 * @param endDate 结束时间 */ - VisitTrendVO getVisitTrend(LocalDate startDate, LocalDate endDate); + VisitTrendVo getVisitTrend(LocalDate startDate, LocalDate endDate); /** * 获取访问统计 */ - VisitStatsVO getVisitStats(); + VisitStatsVo getVisitStats(); } diff --git a/src/main/java/com/youlai/boot/system/service/MenuService.java b/src/main/java/com/youlai/boot/system/service/MenuService.java index 389ec2bb..09457d22 100644 --- a/src/main/java/com/youlai/boot/system/service/MenuService.java +++ b/src/main/java/com/youlai/boot/system/service/MenuService.java @@ -6,8 +6,8 @@ import com.youlai.boot.system.model.form.MenuForm; import com.youlai.boot.common.model.Option; import com.youlai.boot.system.model.entity.Menu; import com.youlai.boot.system.model.query.MenuQuery; -import com.youlai.boot.system.model.vo.MenuVO; -import com.youlai.boot.system.model.vo.RouteVO; +import com.youlai.boot.system.model.vo.MenuVo; +import com.youlai.boot.system.model.vo.RouteVo; import java.util.List; import java.util.Set; @@ -23,7 +23,7 @@ public interface MenuService extends IService { /** * 获取菜单表格列表 */ - List listMenus(MenuQuery queryParams); + List listMenus(MenuQuery queryParams); /** * 获取菜单下拉列表 @@ -42,14 +42,14 @@ public interface MenuService extends IService { /** * 获取当前用户的菜单路由列表 */ - List listCurrentUserRoutes(); + List listCurrentUserRoutes(); /** * 获取当前用户的菜单路由列表(指定数据源) * * @param datasource 数据源名称,如:master(主库)、naiveui(NaiveUI数据库)、template(模板数据库) */ - List listCurrentUserRoutes(String datasource); + List listCurrentUserRoutes(String datasource); /** * 修改菜单显示状态 diff --git a/src/main/java/com/youlai/boot/system/service/NoticeService.java b/src/main/java/com/youlai/boot/system/service/NoticeService.java index 870a3825..96461527 100644 --- a/src/main/java/com/youlai/boot/system/service/NoticeService.java +++ b/src/main/java/com/youlai/boot/system/service/NoticeService.java @@ -5,9 +5,9 @@ import com.baomidou.mybatisplus.extension.service.IService; import com.youlai.boot.system.model.entity.Notice; import com.youlai.boot.system.model.form.NoticeForm; import com.youlai.boot.system.model.query.NoticePageQuery; -import com.youlai.boot.system.model.vo.NoticePageVO; -import com.youlai.boot.system.model.vo.UserNoticePageVO; -import com.youlai.boot.system.model.vo.NoticeDetailVO; +import com.youlai.boot.system.model.vo.NoticePageVo; +import com.youlai.boot.system.model.vo.UserNoticePageVo; +import com.youlai.boot.system.model.vo.NoticeDetailVo; /** * 通知公告服务类 @@ -22,7 +22,7 @@ public interface NoticeService extends IService { * * @return 通知公告分页列表 */ - IPage getNoticePage(NoticePageQuery queryParams); + IPage getNoticePage(NoticePageQuery queryParams); /** * 获取通知公告表单数据 @@ -79,7 +79,7 @@ public interface NoticeService extends IService { * @param id 通知公告ID * @return 通知公告详情 */ - NoticeDetailVO getNoticeDetail(Long id); + NoticeDetailVo getNoticeDetail(Long id); /** * 获取我的通知公告分页列表 @@ -87,5 +87,5 @@ public interface NoticeService extends IService { * @param queryParams 查询参数 * @return 通知公告分页列表 */ - IPage getMyNoticePage(NoticePageQuery queryParams); + IPage getMyNoticePage(NoticePageQuery queryParams); } diff --git a/src/main/java/com/youlai/boot/system/service/RoleService.java b/src/main/java/com/youlai/boot/system/service/RoleService.java index 43761877..030ed781 100644 --- a/src/main/java/com/youlai/boot/system/service/RoleService.java +++ b/src/main/java/com/youlai/boot/system/service/RoleService.java @@ -7,7 +7,7 @@ import com.youlai.boot.system.model.entity.Role; import com.youlai.boot.common.model.Option; import com.youlai.boot.system.model.form.RoleForm; import com.youlai.boot.system.model.query.RolePageQuery; -import com.youlai.boot.system.model.vo.RolePageVO; +import com.youlai.boot.system.model.vo.RolePageVo; import java.util.List; import java.util.Set; @@ -26,7 +26,7 @@ public interface RoleService extends IService { * @param queryParams * @return */ - Page getRolePage(RolePageQuery queryParams); + Page getRolePage(RolePageQuery queryParams); /** diff --git a/src/main/java/com/youlai/boot/system/service/UserNoticeService.java b/src/main/java/com/youlai/boot/system/service/UserNoticeService.java index ddf31c11..24326905 100644 --- a/src/main/java/com/youlai/boot/system/service/UserNoticeService.java +++ b/src/main/java/com/youlai/boot/system/service/UserNoticeService.java @@ -5,8 +5,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.IService; import com.youlai.boot.system.model.entity.UserNotice; import com.youlai.boot.system.model.query.NoticePageQuery; -import com.youlai.boot.system.model.vo.UserNoticePageVO; -import com.youlai.boot.system.model.vo.NoticePageVO; +import com.youlai.boot.system.model.vo.UserNoticePageVo; +import com.youlai.boot.system.model.vo.NoticePageVo; import java.util.List; @@ -31,5 +31,5 @@ public interface UserNoticeService extends IService { * @param queryParams 查询参数 * @return 我的通知公告分页列表 */ - IPage getMyNoticePage(Page page, NoticePageQuery queryParams); + IPage getMyNoticePage(Page page, NoticePageQuery queryParams); } diff --git a/src/main/java/com/youlai/boot/system/service/UserOnlineService.java b/src/main/java/com/youlai/boot/system/service/UserOnlineService.java index c0e2a67f..6ec80555 100644 --- a/src/main/java/com/youlai/boot/system/service/UserOnlineService.java +++ b/src/main/java/com/youlai/boot/system/service/UserOnlineService.java @@ -1,10 +1,9 @@ package com.youlai.boot.system.service; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.youlai.boot.platform.websocket.publisher.WebSocketPublisher; +import com.youlai.boot.platform.websocket.topic.WebSocketTopics; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; import java.util.List; @@ -26,11 +25,10 @@ public class UserOnlineService { // 在线用户映射表,key为用户名,value为用户在线信息 private final Map onlineUsers = new ConcurrentHashMap<>(); - private SimpMessagingTemplate messagingTemplate; + private final WebSocketPublisher webSocketPublisher; - @Autowired(required = false) - public void setMessagingTemplate(SimpMessagingTemplate messagingTemplate) { - this.messagingTemplate = messagingTemplate; + public UserOnlineService(WebSocketPublisher webSocketPublisher) { + this.webSocketPublisher = webSocketPublisher; } /** @@ -68,9 +66,9 @@ public class UserOnlineService { * * @return 在线用户名列表 */ - public List getOnlineUsers() { + public List getOnlineUsers() { return onlineUsers.values().stream() - .map(info -> new UserOnlineDTO(info.getUsername(), info.getLoginTime())) + .map(info -> new UserOnlineDto(info.getUsername(), info.getLoginTime())) .collect(Collectors.toList()); } @@ -97,11 +95,6 @@ public class UserOnlineService { * 通知所有客户端在线用户变更 */ private void notifyOnlineUsersChange() { - if (messagingTemplate == null) { - log.warn("消息模板尚未初始化,无法发送在线用户数量"); - return; - } - // 发送简化版数据(仅数量) sendOnlineUserCount(); } @@ -110,15 +103,10 @@ public class UserOnlineService { * 发送在线用户数量(简化版,不包含用户详情) */ private void sendOnlineUserCount() { - if (messagingTemplate == null) { - log.warn("消息模板尚未初始化,无法发送在线用户数量"); - return; - } - try { // 直接发送数量,更轻量 int count = onlineUsers.size(); - messagingTemplate.convertAndSend("/topic/online-count", count); + webSocketPublisher.publish(WebSocketTopics.TOPIC_ONLINE_COUNT, count); log.debug("已发送在线用户数量: {}", count); } catch (Exception e) { log.error("发送在线用户数量失败", e); @@ -136,10 +124,10 @@ public class UserOnlineService { } /** - * 用户在线DTO(用于返回给前端) + * 用户在线Dto(用于返回给前端) */ @Data - public static class UserOnlineDTO { + public static class UserOnlineDto { private final String username; private final long loginTime; } @@ -151,7 +139,7 @@ public class UserOnlineService { private static class OnlineUsersChangeEvent { private String type; private int count; - private List users; + private List users; private long timestamp; } } diff --git a/src/main/java/com/youlai/boot/system/service/UserService.java b/src/main/java/com/youlai/boot/system/service/UserService.java index e625db5f..a3a57c60 100644 --- a/src/main/java/com/youlai/boot/system/service/UserService.java +++ b/src/main/java/com/youlai/boot/system/service/UserService.java @@ -3,13 +3,13 @@ package com.youlai.boot.system.service; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.service.IService; import com.youlai.boot.common.model.Option; -import com.youlai.boot.security.model.UserAuthCredentials; -import com.youlai.boot.system.model.dto.CurrentUserDTO; -import com.youlai.boot.system.model.dto.UserExportDTO; +import com.youlai.boot.security.model.UserAuthInfo; +import com.youlai.boot.system.model.dto.CurrentUserDto; +import com.youlai.boot.system.model.dto.UserExportDto; import com.youlai.boot.system.model.entity.User; import com.youlai.boot.system.model.query.UserPageQuery; -import com.youlai.boot.system.model.vo.UserPageVO; -import com.youlai.boot.system.model.vo.UserProfileVO; +import com.youlai.boot.system.model.vo.UserPageVo; +import com.youlai.boot.system.model.vo.UserProfileVo; import com.youlai.boot.system.model.form.*; import java.util.List; @@ -25,9 +25,9 @@ public interface UserService extends IService { /** * 用户分页列表 * - * @return {@link IPage} 用户分页列表 + * @return {@link IPage} 用户分页列表 */ - IPage getUserPage(UserPageQuery queryParams); + IPage getUserPage(UserPageQuery queryParams); /** * 获取用户表单数据 @@ -69,33 +69,37 @@ public interface UserService extends IService { * 根据用户名获取认证信息 * * @param username 用户名 - * @return {@link UserAuthCredentials} + * @return {@link UserAuthInfo} */ - UserAuthCredentials getAuthCredentialsByUsername(String username); + UserAuthInfo getAuthInfoByUsername(String username); + + default UserAuthInfo getAuthCredentialsByUsername(String username) { + return getAuthInfoByUsername(username); + } /** * 获取导出用户列表 * * @param queryParams 查询参数 - * @return {@link List} 导出用户列表 + * @return {@link List} 导出用户列表 */ - List listExportUsers(UserPageQuery queryParams); + List listExportUsers(UserPageQuery queryParams); /** * 获取登录用户信息 * - * @return {@link CurrentUserDTO} 登录用户信息 + * @return {@link CurrentUserDto} 登录用户信息 */ - CurrentUserDTO getCurrentUserInfo(); + CurrentUserDto getCurrentUserInfo(); /** * 获取个人中心用户信息 * - * @return {@link UserProfileVO} 个人中心用户信息 + * @return {@link UserProfileVo} 个人中心用户信息 */ - UserProfileVO getUserProfile(Long userId); + UserProfileVo getUserProfile(Long userId); /** * 修改个人中心用户信息 @@ -165,10 +169,14 @@ public interface UserService extends IService { * 根据 openid 获取用户认证信息 * * @param openId 用户名 - * @return {@link UserAuthCredentials} + * @return {@link UserAuthInfo} */ - UserAuthCredentials getAuthCredentialsByOpenId(String openId); + UserAuthInfo getAuthInfoByOpenId(String openId); + + default UserAuthInfo getAuthCredentialsByOpenId(String openId) { + return getAuthInfoByOpenId(openId); + } /** * 根据微信 OpenID 注册或绑定用户 @@ -181,9 +189,13 @@ public interface UserService extends IService { * 根据手机号获取用户认证信息 * * @param mobile 手机号 - * @return {@link UserAuthCredentials} + * @return {@link UserAuthInfo} */ - UserAuthCredentials getAuthCredentialsByMobile(String mobile); + UserAuthInfo getAuthInfoByMobile(String mobile); + + default UserAuthInfo getAuthCredentialsByMobile(String mobile) { + return getAuthInfoByMobile(mobile); + } /** * 根据手机号和OpenID注册用户 diff --git a/src/main/java/com/youlai/boot/system/service/WebSocketMessageService.java b/src/main/java/com/youlai/boot/system/service/WebSocketMessageService.java deleted file mode 100644 index e183d7af..00000000 --- a/src/main/java/com/youlai/boot/system/service/WebSocketMessageService.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.youlai.boot.system.service; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.messaging.simp.SimpMessagingTemplate; -import org.springframework.stereotype.Service; - -/** - * WebSocket消息服务 - * - * @author Ray - * @since 3.0.0 - */ -@Service -@RequiredArgsConstructor -@Slf4j -public class WebSocketMessageService { - - private final SimpMessagingTemplate messagingTemplate; - private final ObjectMapper objectMapper; - - /** - * 字典事件类型 - */ - public enum DictEventType { - /** - * 字典更新 - */ - DICT_UPDATED, - - /** - * 字典删除 - */ - DICT_DELETED - } - - /** - * 字典事件消息 - */ - public static class DictEvent { - /** - * 事件类型 - */ - private String type; - - /** - * 字典编码 - */ - private String dictCode; - - /** - * 时间戳 - */ - private long timestamp; - - public DictEvent(DictEventType type, String dictCode) { - this.type = type.name(); - this.dictCode = dictCode; - this.timestamp = System.currentTimeMillis(); - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public String getDictCode() { - return dictCode; - } - - public void setDictCode(String dictCode) { - this.dictCode = dictCode; - } - - public long getTimestamp() { - return timestamp; - } - - public void setTimestamp(long timestamp) { - this.timestamp = timestamp; - } - } - - /** - * 向所有客户端发送字典更新事件 - * - * @param dictCode 字典编码 - */ - public void sendDictUpdatedEvent(String dictCode) { - DictEvent event = new DictEvent(DictEventType.DICT_UPDATED, dictCode); - sendDictEvent(event); - } - - /** - * 向所有客户端发送字典删除事件 - * - * @param dictCode 字典编码 - */ - public void sendDictDeletedEvent(String dictCode) { - DictEvent event = new DictEvent(DictEventType.DICT_DELETED, dictCode); - sendDictEvent(event); - } - - /** - * 发送字典事件消息 - * - * @param event 字典事件 - */ - private void sendDictEvent(DictEvent event) { - try { - String message = objectMapper.writeValueAsString(event); - messagingTemplate.convertAndSend("/topic/dict", message); - log.info("Sent dict event to clients: {}", message); - } catch (JsonProcessingException e) { - log.error("Failed to send dict event", e); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/youlai/boot/system/service/impl/ConfigServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/ConfigServiceImpl.java index e4e12c26..d0de5653 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/ConfigServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/ConfigServiceImpl.java @@ -11,7 +11,7 @@ import com.youlai.boot.system.mapper.ConfigMapper; import com.youlai.boot.system.model.entity.Config; import com.youlai.boot.system.model.form.ConfigForm; import com.youlai.boot.system.model.query.ConfigPageQuery; -import com.youlai.boot.system.model.vo.ConfigVO; +import com.youlai.boot.system.model.vo.ConfigVo; import com.youlai.boot.system.service.ConfigService; import com.youlai.boot.security.util.SecurityUtils; import jakarta.annotation.PostConstruct; @@ -54,7 +54,7 @@ public class ConfigServiceImpl extends ServiceImpl impleme * @return 系统配置分页列表 */ @Override - public IPage page(ConfigPageQuery configPageQuery) { + public IPage page(ConfigPageQuery configPageQuery) { Page page = new Page<>(configPageQuery.getPageNum(), configPageQuery.getPageSize()); String keywords = configPageQuery.getKeywords(); LambdaQueryWrapper query = new LambdaQueryWrapper() diff --git a/src/main/java/com/youlai/boot/system/service/impl/DeptServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/DeptServiceImpl.java index db0ec596..784b9024 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/DeptServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/DeptServiceImpl.java @@ -12,7 +12,7 @@ import com.youlai.boot.system.mapper.DeptMapper; import com.youlai.boot.system.model.entity.Dept; import com.youlai.boot.system.model.form.DeptForm; import com.youlai.boot.system.model.query.DeptQuery; -import com.youlai.boot.system.model.vo.DeptVO; +import com.youlai.boot.system.model.vo.DeptVo; import com.youlai.boot.common.constant.SystemConstants; import com.youlai.boot.common.enums.StatusEnum; import com.youlai.boot.common.model.Option; @@ -42,7 +42,7 @@ public class DeptServiceImpl extends ServiceImpl implements De * 获取部门列表 */ @Override - public List getDeptList(DeptQuery queryParams) { + public List getDeptList(DeptQuery queryParams) { // 查询参数 String keywords = queryParams.getKeywords(); Integer status = queryParams.getStatus(); @@ -83,14 +83,14 @@ public class DeptServiceImpl extends ServiceImpl implements De * @param deptList 部门列表 * @return 部门树形列表 */ - public List recurDeptList(Long parentId, List deptList) { + public List recurDeptList(Long parentId, List deptList) { return deptList.stream() .filter(dept -> dept.getParentId().equals(parentId)) .map(dept -> { - DeptVO deptVO = deptConverter.toVo(dept); - List children = recurDeptList(dept.getId(), deptList); - deptVO.setChildren(children); - return deptVO; + DeptVo deptVo = deptConverter.toVo(dept); + List children = recurDeptList(dept.getId(), deptList); + deptVo.setChildren(children); + return deptVo; }).toList(); } diff --git a/src/main/java/com/youlai/boot/system/service/impl/DictItemServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/DictItemServiceImpl.java index 7c1d8058..9cb89ff9 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/DictItemServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/DictItemServiceImpl.java @@ -8,8 +8,8 @@ import com.youlai.boot.system.mapper.DictItemMapper; import com.youlai.boot.system.model.entity.DictItem; import com.youlai.boot.system.model.form.DictItemForm; import com.youlai.boot.system.model.query.DictItemPageQuery; -import com.youlai.boot.system.model.vo.DictItemOptionVO; -import com.youlai.boot.system.model.vo.DictItemPageVO; +import com.youlai.boot.system.model.vo.DictItemOptionVo; +import com.youlai.boot.system.model.vo.DictItemPageVo; import com.youlai.boot.system.service.DictItemService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -36,10 +36,10 @@ public class DictItemServiceImpl extends ServiceImpl i * @return 字典项分页列表 */ @Override - public Page getDictItemPage(DictItemPageQuery queryParams) { + public Page getDictItemPage(DictItemPageQuery queryParams) { int pageNum = queryParams.getPageNum(); int pageSize = queryParams.getPageSize(); - Page page = new Page<>(pageNum, pageSize); + Page page = new Page<>(pageNum, pageSize); return this.baseMapper.getDictItemPage(page, queryParams); } @@ -51,7 +51,7 @@ public class DictItemServiceImpl extends ServiceImpl i * @param dictCode 字典编码 */ @Override - public List getDictItems(String dictCode) { + public List getDictItems(String dictCode) { return this.list( new LambdaQueryWrapper() .eq(DictItem::getDictCode, dictCode) @@ -59,11 +59,11 @@ public class DictItemServiceImpl extends ServiceImpl i .orderByAsc(DictItem::getSort) ).stream() .map(item -> { - DictItemOptionVO dictItemOptionVO = new DictItemOptionVO(); - dictItemOptionVO.setLabel(item.getLabel()); - dictItemOptionVO.setValue(item.getValue()); - dictItemOptionVO.setTagType(item.getTagType()); - return dictItemOptionVO; + DictItemOptionVo dictItemOptionVo = new DictItemOptionVo(); + dictItemOptionVo.setLabel(item.getLabel()); + dictItemOptionVo.setValue(item.getValue()); + dictItemOptionVo.setTagType(item.getTagType()); + return dictItemOptionVo; }).toList(); } diff --git a/src/main/java/com/youlai/boot/system/service/impl/DictServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/DictServiceImpl.java index 2573b4d3..e8fc8c5b 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/DictServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/DictServiceImpl.java @@ -12,7 +12,7 @@ import com.youlai.boot.system.model.entity.Dict; import com.youlai.boot.system.model.entity.DictItem; import com.youlai.boot.system.model.form.DictForm; import com.youlai.boot.system.model.query.DictPageQuery; -import com.youlai.boot.system.model.vo.DictPageVO; +import com.youlai.boot.system.model.vo.DictPageVo; import com.youlai.boot.system.service.DictItemService; import com.youlai.boot.system.service.DictService; import lombok.RequiredArgsConstructor; @@ -40,7 +40,7 @@ public class DictServiceImpl extends ServiceImpl implements Di * @param queryParams 分页查询对象 */ @Override - public Page getDictPage(DictPageQuery queryParams) { + public Page getDictPage(DictPageQuery queryParams) { // 查询参数 int pageNum = queryParams.getPageNum(); int pageSize = queryParams.getPageSize(); diff --git a/src/main/java/com/youlai/boot/system/service/impl/LogServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/LogServiceImpl.java index 035f91ca..224976cc 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/LogServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/LogServiceImpl.java @@ -3,17 +3,16 @@ package com.youlai.boot.system.service.impl; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.youlai.boot.system.mapper.LogMapper; -import com.youlai.boot.system.model.bo.VisitCount; -import com.youlai.boot.system.model.bo.VisitStatsBO; +import com.youlai.boot.system.model.bo.VisitCountBo; +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.youlai.boot.system.model.vo.VisitTrendVO; +import com.youlai.boot.system.model.vo.LogPageVo; +import com.youlai.boot.system.model.vo.VisitStatsVo; +import com.youlai.boot.system.model.vo.VisitTrendVo; import com.youlai.boot.system.service.LogService; import org.springframework.stereotype.Service; -import java.math.BigDecimal; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; @@ -37,7 +36,7 @@ public class LogServiceImpl extends ServiceImpl * @return 日志分页列表 */ @Override - public Page getLogPage(LogPageQuery queryParams) { + public Page getLogPage(LogPageQuery queryParams) { return this.baseMapper.getLogPage(new Page<>(queryParams.getPageNum(), queryParams.getPageSize()), queryParams); } @@ -50,8 +49,8 @@ public class LogServiceImpl extends ServiceImpl * @return */ @Override - public VisitTrendVO getVisitTrend(LocalDate startDate, LocalDate endDate) { - VisitTrendVO visitTrend = new VisitTrendVO(); + public VisitTrendVo getVisitTrend(LocalDate startDate, LocalDate endDate) { + VisitTrendVo visitTrend = new VisitTrendVo(); List dates = new ArrayList<>(); // 获取日期范围内的日期 @@ -62,12 +61,12 @@ public class LogServiceImpl extends ServiceImpl visitTrend.setDates(dates); // 获取访问量和访问 IP 数的统计数据 - List pvCounts = this.baseMapper.getPvCounts(dates.get(0) + " 00:00:00", dates.get(dates.size() - 1) + " 23:59:59"); - List ipCounts = this.baseMapper.getIpCounts(dates.get(0) + " 00:00:00", dates.get(dates.size() - 1) + " 23:59:59"); + List pvCounts = this.baseMapper.getPvCounts(dates.get(0) + " 00:00:00", dates.get(dates.size() - 1) + " 23:59:59"); + List ipCounts = this.baseMapper.getIpCounts(dates.get(0) + " 00:00:00", dates.get(dates.size() - 1) + " 23:59:59"); // 将统计数据转换为 Map - Map pvMap = pvCounts.stream().collect(Collectors.toMap(VisitCount::getDate, VisitCount::getCount)); - Map ipMap = ipCounts.stream().collect(Collectors.toMap(VisitCount::getDate, VisitCount::getCount)); + Map pvMap = pvCounts.stream().collect(Collectors.toMap(VisitCountBo::getDate, VisitCountBo::getCount)); + Map ipMap = ipCounts.stream().collect(Collectors.toMap(VisitCountBo::getDate, VisitCountBo::getCount)); // 匹配日期和访问量/访问 IP 数 List pvList = new ArrayList<>(); @@ -88,11 +87,11 @@ public class LogServiceImpl extends ServiceImpl * 访问量统计 */ @Override - public VisitStatsVO getVisitStats() { - VisitStatsVO result = new VisitStatsVO(); + public VisitStatsVo getVisitStats() { + VisitStatsVo result = new VisitStatsVo(); // 访客数统计(UV) - VisitStatsBO uvStats = this.baseMapper.getUvStats(); + VisitStatsBo uvStats = this.baseMapper.getUvStats(); if(uvStats!=null){ result.setTodayUvCount(uvStats.getTodayCount()); result.setTotalUvCount(uvStats.getTotalCount()); @@ -100,7 +99,7 @@ public class LogServiceImpl extends ServiceImpl } // 浏览量统计(PV) - VisitStatsBO pvStats = this.baseMapper.getPvStats(); + VisitStatsBo pvStats = this.baseMapper.getPvStats(); if(pvStats!=null){ result.setTodayPvCount(pvStats.getTodayCount()); result.setTotalPvCount(pvStats.getTotalCount()); 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 7a0d678d..fc77c1fa 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 @@ -17,8 +17,8 @@ import com.youlai.boot.system.mapper.MenuMapper; import com.youlai.boot.system.model.entity.Menu; import com.youlai.boot.system.model.form.MenuForm; import com.youlai.boot.system.model.query.MenuQuery; -import com.youlai.boot.system.model.vo.MenuVO; -import com.youlai.boot.system.model.vo.RouteVO; +import com.youlai.boot.system.model.vo.MenuVo; +import com.youlai.boot.system.model.vo.RouteVo; import com.youlai.boot.common.constant.SystemConstants; import com.youlai.boot.system.enums.MenuTypeEnum; import com.youlai.boot.common.enums.StatusEnum; @@ -55,7 +55,7 @@ public class MenuServiceImpl extends ServiceImpl implements Me * @param queryParams {@link MenuQuery} */ @Override - public List listMenus(MenuQuery queryParams) { + public List listMenus(MenuQuery queryParams) { List menus = this.list(new LambdaQueryWrapper() .like(StrUtil.isNotBlank(queryParams.getKeywords()), Menu::getName, queryParams.getKeywords()) .orderByAsc(Menu::getSort) @@ -88,15 +88,15 @@ public class MenuServiceImpl extends ServiceImpl implements Me * @param menuList 菜单列表 * @return 菜单列表 */ - private List buildMenuTree(Long parentId, List menuList) { + private List buildMenuTree(Long parentId, List menuList) { return CollectionUtil.emptyIfNull(menuList) .stream() .filter(menu -> menu.getParentId().equals(parentId)) .map(entity -> { - MenuVO menuVO = menuConverter.toVo(entity); - List children = buildMenuTree(entity.getId(), menuList); - menuVO.setChildren(children); - return menuVO; + MenuVo menuVo = menuConverter.toVo(entity); + List children = buildMenuTree(entity.getId(), menuList); + menuVo.setChildren(children); + return menuVo; }).toList(); } @@ -142,7 +142,7 @@ public class MenuServiceImpl extends ServiceImpl implements Me * 获取当前用户的菜单路由列表 */ @Override - public List listCurrentUserRoutes() { + public List listCurrentUserRoutes() { Set roleCodes = SecurityUtils.getRoles(); if (CollectionUtil.isEmpty(roleCodes)) { @@ -195,7 +195,7 @@ public class MenuServiceImpl extends ServiceImpl implements Me * - template: 模板项目菜单数据 */ @Override - public List listCurrentUserRoutes(String datasource) { + public List listCurrentUserRoutes(String datasource) { return listCurrentUserRoutes(); } @@ -207,17 +207,17 @@ public class MenuServiceImpl extends ServiceImpl implements Me * @param menuList 菜单列表 * @return 路由层级列表 */ - private List buildRoutes(Long parentId, List menuList) { - List routeList = new ArrayList<>(); + private List buildRoutes(Long parentId, List menuList) { + List routeList = new ArrayList<>(); for (Menu menu : menuList) { if (menu.getParentId().equals(parentId)) { - RouteVO routeVO = toRouteVo(menu); - List children = buildRoutes(menu.getId(), menuList); + RouteVo routeVo = toRouteVo(menu); + List children = buildRoutes(menu.getId(), menuList); if (!children.isEmpty()) { - routeVO.setChildren(children); + routeVo.setChildren(children); } - routeList.add(routeVO); + routeList.add(routeVo); } } @@ -225,10 +225,10 @@ public class MenuServiceImpl extends ServiceImpl implements Me } /** - * 根据RouteBO创建RouteVO + * 根据RouteBO创建RouteVo */ - private RouteVO toRouteVo(Menu menu) { - RouteVO routeVO = new RouteVO(); + private RouteVo toRouteVo(Menu menu) { + RouteVo routeVo = new RouteVo(); String routePath = menu.getRoutePath(); boolean externalLink = StrUtil.startWithAny(routePath, "http://", "https://"); @@ -241,15 +241,15 @@ public class MenuServiceImpl extends ServiceImpl implements Me : StringUtils.capitalize(StrUtil.toCamelCase(routePath, '-')); } // 根据name路由跳转 this.$router.push({name:xxx}) - routeVO.setName(routeName); + routeVo.setName(routeName); // 根据path路由跳转 this.$router.push({path:xxx}) - routeVO.setPath(routePath); - routeVO.setRedirect(menu.getRedirect()); + routeVo.setPath(routePath); + routeVo.setRedirect(menu.getRedirect()); // 外链无组件 - routeVO.setComponent(externalLink ? null : menu.getComponent()); + routeVo.setComponent(externalLink ? null : menu.getComponent()); - RouteVO.Meta meta = new RouteVO.Meta(); + RouteVo.Meta meta = new RouteVo.Meta(); meta.setTitle(menu.getName()); meta.setIcon(menu.getIcon()); meta.setHidden(StatusEnum.DISABLE.getValue().equals(menu.getVisible())); @@ -272,8 +272,8 @@ public class MenuServiceImpl extends ServiceImpl implements Me throw new RuntimeException("解析参数失败", e); } } - routeVO.setMeta(meta); - return routeVO; + routeVo.setMeta(meta); + return routeVo; } /** diff --git a/src/main/java/com/youlai/boot/system/service/impl/NoticeServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/NoticeServiceImpl.java index b4d70ea5..5efb5a70 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/NoticeServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/NoticeServiceImpl.java @@ -13,22 +13,23 @@ import com.youlai.boot.system.converter.NoticeConverter; import com.youlai.boot.system.enums.NoticePublishStatusEnum; import com.youlai.boot.system.enums.NoticeTargetEnum; import com.youlai.boot.system.mapper.NoticeMapper; -import com.youlai.boot.system.model.bo.NoticeBO; -import com.youlai.boot.system.model.dto.NoticeDTO; +import com.youlai.boot.system.model.bo.NoticeBo; +import com.youlai.boot.system.model.dto.NoticeDto; import com.youlai.boot.system.model.entity.Notice; import com.youlai.boot.system.model.entity.UserNotice; import com.youlai.boot.system.model.entity.User; import com.youlai.boot.system.model.form.NoticeForm; import com.youlai.boot.system.model.query.NoticePageQuery; -import com.youlai.boot.system.model.vo.NoticePageVO; -import com.youlai.boot.system.model.vo.UserNoticePageVO; -import com.youlai.boot.system.model.vo.NoticeDetailVO; +import com.youlai.boot.system.model.vo.NoticePageVo; +import com.youlai.boot.system.model.vo.UserNoticePageVo; +import com.youlai.boot.system.model.vo.NoticeDetailVo; import com.youlai.boot.system.service.NoticeService; import com.youlai.boot.system.service.UserNoticeService; import com.youlai.boot.system.service.UserOnlineService; import com.youlai.boot.system.service.UserService; +import com.youlai.boot.platform.websocket.publisher.WebSocketPublisher; +import com.youlai.boot.platform.websocket.topic.WebSocketTopics; import lombok.RequiredArgsConstructor; -import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -52,18 +53,18 @@ public class NoticeServiceImpl extends ServiceImpl impleme private final NoticeConverter noticeConverter; private final UserNoticeService userNoticeService; private final UserService userService; - private final SimpMessagingTemplate messagingTemplate; + private final WebSocketPublisher webSocketPublisher; private final UserOnlineService userOnlineService; /** * 获取通知公告分页列表 * * @param queryParams 查询参数 - * @return {@link IPage< NoticePageVO >} 通知公告分页列表 + * @return {@link IPage< NoticePageVo >} 通知公告分页列表 */ @Override - public IPage getNoticePage(NoticePageQuery queryParams) { - Page noticePage = this.baseMapper.getNoticePage( + public IPage getNoticePage(NoticePageQuery queryParams) { + Page noticePage = this.baseMapper.getNoticePage( new Page<>(queryParams.getPageNum(), queryParams.getPageSize()), queryParams ); @@ -214,19 +215,19 @@ public class NoticeServiceImpl extends ServiceImpl impleme Set receivers = targetUserList.stream().map(User::getUsername).collect(Collectors.toSet()); Set allOnlineUsers = userOnlineService.getOnlineUsers().stream() - .map(UserOnlineService.UserOnlineDTO::getUsername) + .map(UserOnlineService.UserOnlineDto::getUsername) .collect(Collectors.toSet()); // 找出在线用户的通知接收者 Set onlineReceivers = new HashSet<>(CollectionUtil.intersection(receivers, allOnlineUsers)); - NoticeDTO noticeDTO = new NoticeDTO(); - noticeDTO.setId(id); - noticeDTO.setTitle(notice.getTitle()); - noticeDTO.setType(notice.getType()); - noticeDTO.setPublishTime(notice.getPublishTime()); + NoticeDto noticeDto = new NoticeDto(); + noticeDto.setId(id); + noticeDto.setTitle(notice.getTitle()); + noticeDto.setType(notice.getType()); + noticeDto.setPublishTime(notice.getPublishTime()); - onlineReceivers.forEach(receiver -> messagingTemplate.convertAndSendToUser(receiver, "/queue/message", noticeDTO)); + onlineReceivers.forEach(receiver -> webSocketPublisher.publishToUser(receiver, WebSocketTopics.USER_QUEUE_MESSAGE, noticeDto)); } return publishResult; } @@ -249,7 +250,7 @@ public class NoticeServiceImpl extends ServiceImpl impleme throw new BusinessException("通知公告未发布或已撤回"); } - notice.setPublishStatus(NoticePublishStatusEnum.REVOKED.getValue()); + notice.setPublishStatus(NoticePublishStatusEnum.REVoKED.getValue()); notice.setRevokeTime(LocalDateTime.now()); notice.setUpdateBy(SecurityUtils.getUserId()); @@ -267,11 +268,11 @@ public class NoticeServiceImpl extends ServiceImpl impleme /** * * @param id 通知公告ID - * @return NoticeDetailVO 通知公告详情 + * @return NoticeDetailVo 通知公告详情 */ @Override - public NoticeDetailVO getNoticeDetail(Long id) { - NoticeBO noticeBO = this.baseMapper.getNoticeDetail(id); + public NoticeDetailVo getNoticeDetail(Long id) { + NoticeBo noticeBo = this.baseMapper.getNoticeDetail(id); // 更新用户通知公告的阅读状态 Long userId = SecurityUtils.getUserId(); userNoticeService.update(new LambdaUpdateWrapper() @@ -280,7 +281,7 @@ public class NoticeServiceImpl extends ServiceImpl impleme .eq(UserNotice::getIsRead, 0) .set(UserNotice::getIsRead, 1) ); - return noticeConverter.toDetailVO(noticeBO); + return noticeConverter.toDetailVo(noticeBo); } /** @@ -290,7 +291,7 @@ public class NoticeServiceImpl extends ServiceImpl impleme * @return 通知公告分页列表 */ @Override - public IPage getMyNoticePage(NoticePageQuery queryParams) { + public IPage getMyNoticePage(NoticePageQuery queryParams) { queryParams.setUserId(SecurityUtils.getUserId()); return userNoticeService.getMyNoticePage( new Page<>(queryParams.getPageNum(), queryParams.getPageSize()), diff --git a/src/main/java/com/youlai/boot/system/service/impl/RoleMenuServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/RoleMenuServiceImpl.java index 9304ce68..c9acaf8d 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/RoleMenuServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/RoleMenuServiceImpl.java @@ -4,7 +4,7 @@ import cn.hutool.core.collection.CollectionUtil; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.system.mapper.RoleMenuMapper; -import com.youlai.boot.system.model.bo.RolePermsBO; +import com.youlai.boot.system.model.bo.RolePermsBo; import com.youlai.boot.system.model.entity.RoleMenu; import com.youlai.boot.system.service.RoleMenuService; import jakarta.annotation.PostConstruct; @@ -36,7 +36,7 @@ public class RoleMenuServiceImpl extends ServiceImpl i public void initRolePermsCache() { log.info("开始初始化权限缓存..."); - List allRolePermsList = this.baseMapper.getRolePermsList(null); + List allRolePermsList = this.baseMapper.getRolePermsList(null); if (CollectionUtil.isEmpty(allRolePermsList)) { log.warn("权限数据为空,跳过缓存初始化"); @@ -67,7 +67,7 @@ public class RoleMenuServiceImpl extends ServiceImpl i redisTemplate.delete(cacheKey); // 重新加载权限 - List list = this.baseMapper.getRolePermsList(null); + List list = this.baseMapper.getRolePermsList(null); if (CollectionUtil.isNotEmpty(list)) { list.forEach(item -> { String roleCode = item.getRoleCode(); @@ -92,9 +92,9 @@ public class RoleMenuServiceImpl extends ServiceImpl i redisTemplate.opsForHash().delete(cacheKey, roleCode); // 重新加载指定角色权限 - List list = this.baseMapper.getRolePermsList(roleCode); + List list = this.baseMapper.getRolePermsList(roleCode); if (CollectionUtil.isNotEmpty(list)) { - RolePermsBO rolePerms = list.get(0); + RolePermsBo rolePerms = list.get(0); if (rolePerms != null) { Set perms = rolePerms.getPerms(); if (CollectionUtil.isNotEmpty(perms)) { @@ -117,9 +117,9 @@ public class RoleMenuServiceImpl extends ServiceImpl i redisTemplate.opsForHash().delete(cacheKey, oldRoleCode); // 添加新角色权限缓存 - List list = this.baseMapper.getRolePermsList(newRoleCode); + List list = this.baseMapper.getRolePermsList(newRoleCode); if (CollectionUtil.isNotEmpty(list)) { - RolePermsBO rolePerms = list.get(0); + RolePermsBo rolePerms = list.get(0); if (rolePerms != null) { Set perms = rolePerms.getPerms(); if (CollectionUtil.isNotEmpty(perms)) { diff --git a/src/main/java/com/youlai/boot/system/service/impl/RoleServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/RoleServiceImpl.java index 0efab740..b7654c75 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/RoleServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/RoleServiceImpl.java @@ -14,7 +14,7 @@ import com.youlai.boot.system.model.entity.Role; import com.youlai.boot.system.model.entity.RoleMenu; import com.youlai.boot.system.model.form.RoleForm; import com.youlai.boot.system.model.query.RolePageQuery; -import com.youlai.boot.system.model.vo.RolePageVO; +import com.youlai.boot.system.model.vo.RolePageVo; import com.youlai.boot.common.constant.SystemConstants; import com.youlai.boot.common.model.Option; import com.youlai.boot.security.util.SecurityUtils; @@ -48,10 +48,10 @@ public class RoleServiceImpl extends ServiceImpl implements Ro * 角色分页列表 * * @param queryParams 角色查询参数 - * @return {@link Page< RolePageVO >} – 角色分页列表 + * @return {@link Page< RolePageVo >} – 角色分页列表 */ @Override - public Page getRolePage(RolePageQuery queryParams) { + public Page getRolePage(RolePageQuery queryParams) { // 查询参数 int pageNum = queryParams.getPageNum(); int pageSize = queryParams.getPageSize(); diff --git a/src/main/java/com/youlai/boot/system/service/impl/UserNoticeServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/UserNoticeServiceImpl.java index aa82529c..b93d7211 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/UserNoticeServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/UserNoticeServiceImpl.java @@ -8,8 +8,8 @@ import com.youlai.boot.security.util.SecurityUtils; import com.youlai.boot.system.mapper.UserNoticeMapper; import com.youlai.boot.system.model.entity.UserNotice; import com.youlai.boot.system.model.query.NoticePageQuery; -import com.youlai.boot.system.model.vo.NoticePageVO; -import com.youlai.boot.system.model.vo.UserNoticePageVO; +import com.youlai.boot.system.model.vo.NoticePageVo; +import com.youlai.boot.system.model.vo.UserNoticePageVo; import com.youlai.boot.system.service.UserNoticeService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -51,7 +51,7 @@ public class UserNoticeServiceImpl extends ServiceImpl getMyNoticePage(Page page, NoticePageQuery queryParams) { + public IPage getMyNoticePage(Page page, NoticePageQuery queryParams) { return this.getBaseMapper().getMyNoticePage( new Page<>(queryParams.getPageNum(), queryParams.getPageSize()), queryParams diff --git a/src/main/java/com/youlai/boot/system/service/impl/UserRoleServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/UserRoleServiceImpl.java index 60a4fc2b..b3457551 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/UserRoleServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/UserRoleServiceImpl.java @@ -17,11 +17,16 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; +/** + * 用户角色业务实现类 + * + * @author Ray.Hao + * @since 0.0.1 + */ @Service @RequiredArgsConstructor public class UserRoleServiceImpl extends ServiceImpl implements UserRoleService { - private final TokenManager tokenManager; /** @@ -86,7 +91,7 @@ public class UserRoleServiceImpl extends ServiceImpl i */ @Override public boolean hasAssignedUsers(Long roleId) { - int count = this.baseMapper.countUsersForRole(roleId); + int count = this.baseMapper.countUsersByRoleId(roleId); return count > 0; } } diff --git a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java index abcc4c9a..70dbe215 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java @@ -14,7 +14,7 @@ import com.youlai.boot.core.exception.BusinessException; import com.youlai.boot.common.model.Option; import com.youlai.boot.platform.sms.enums.SmsTypeEnum; import com.youlai.boot.platform.sms.service.SmsService; -import com.youlai.boot.security.model.UserAuthCredentials; +import com.youlai.boot.security.model.UserAuthInfo; import com.youlai.boot.security.service.PermissionService; import com.youlai.boot.security.token.TokenManager; import com.youlai.boot.security.util.SecurityUtils; @@ -22,16 +22,16 @@ import com.youlai.boot.platform.mail.service.MailService; import com.youlai.boot.system.converter.UserConverter; import com.youlai.boot.system.enums.DictCodeEnum; import com.youlai.boot.system.mapper.UserMapper; -import com.youlai.boot.system.model.bo.UserBO; -import com.youlai.boot.system.model.dto.CurrentUserDTO; -import com.youlai.boot.system.model.dto.UserExportDTO; +import com.youlai.boot.system.model.bo.UserBo; +import com.youlai.boot.system.model.dto.CurrentUserDto; +import com.youlai.boot.system.model.dto.UserExportDto; import com.youlai.boot.system.model.entity.DictItem; import com.youlai.boot.system.model.entity.User; import com.youlai.boot.system.model.entity.UserRole; import com.youlai.boot.system.model.form.*; import com.youlai.boot.system.model.query.UserPageQuery; -import com.youlai.boot.system.model.vo.UserPageVO; -import com.youlai.boot.system.model.vo.UserProfileVO; +import com.youlai.boot.system.model.vo.UserPageVo; +import com.youlai.boot.system.model.vo.UserProfileVo; import com.youlai.boot.system.service.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -81,21 +81,21 @@ public class UserServiceImpl extends ServiceImpl implements Us * 获取用户分页列表 * * @param queryParams 查询参数 - * @return {@link IPage} 用户分页列表 + * @return {@link IPage} 用户分页列表 */ @Override - public IPage getUserPage(UserPageQuery queryParams) { + public IPage getUserPage(UserPageQuery queryParams) { // 参数构建 int pageNum = queryParams.getPageNum(); int pageSize = queryParams.getPageSize(); - Page page = new Page<>(pageNum, pageSize); + Page page = new Page<>(pageNum, pageSize); boolean isRoot = SecurityUtils.isRoot(); queryParams.setIsRoot(isRoot); // 查询数据 - Page userPage = this.baseMapper.getUserPage(page, queryParams); + Page userPage = this.baseMapper.getUserPage(page, queryParams); // 实体转换 return userConverter.toPageVo(userPage); @@ -208,18 +208,18 @@ public class UserServiceImpl extends ServiceImpl implements Us * 根据用户名获取认证凭证信息 * * @param username 用户名 - * @return 用户认证凭证信息 {@link UserAuthCredentials} + * @return 用户认证凭证信息 {@link UserAuthInfo} */ @Override - public UserAuthCredentials getAuthCredentialsByUsername(String username) { - UserAuthCredentials userAuthCredentials = this.baseMapper.getAuthCredentialsByUsername(username); - if (userAuthCredentials != null) { - Set roles = userAuthCredentials.getRoles(); + public UserAuthInfo getAuthInfoByUsername(String username) { + UserAuthInfo userAuthInfo = this.baseMapper.getAuthInfoByUsername(username); + if (userAuthInfo != null) { + Set roles = userAuthInfo.getRoles(); // 获取最大范围的数据权限 Integer dataScope = roleService.getMaximumDataScope(roles); - userAuthCredentials.setDataScope(dataScope); + userAuthInfo.setDataScope(dataScope); } - return userAuthCredentials; + return userAuthInfo; } @@ -230,18 +230,18 @@ public class UserServiceImpl extends ServiceImpl implements Us * @return 用户认证信息 */ @Override - public UserAuthCredentials getAuthCredentialsByOpenId(String openId) { + public UserAuthInfo getAuthInfoByOpenId(String openId) { if (StrUtil.isBlank(openId)) { return null; } - UserAuthCredentials userAuthCredentials = this.baseMapper.getAuthCredentialsByOpenId(openId); - if (userAuthCredentials != null) { - Set roles = userAuthCredentials.getRoles(); + UserAuthInfo userAuthInfo = this.baseMapper.getAuthInfoByOpenId(openId); + if (userAuthInfo != null) { + Set roles = userAuthInfo.getRoles(); // 获取最大范围的数据权限 Integer dataScope = roleService.getMaximumDataScope(roles); - userAuthCredentials.setDataScope(dataScope); + userAuthInfo.setDataScope(dataScope); } - return userAuthCredentials; + return userAuthInfo; } /** @@ -251,18 +251,18 @@ public class UserServiceImpl extends ServiceImpl implements Us * @return 用户认证信息 */ @Override - public UserAuthCredentials getAuthCredentialsByMobile(String mobile) { + public UserAuthInfo getAuthInfoByMobile(String mobile) { if (StrUtil.isBlank(mobile)) { return null; } - UserAuthCredentials userAuthCredentials = this.baseMapper.getAuthCredentialsByMobile(mobile); - if (userAuthCredentials != null) { - Set roles = userAuthCredentials.getRoles(); + UserAuthInfo userAuthInfo = this.baseMapper.getAuthInfoByMobile(mobile); + if (userAuthInfo != null) { + Set roles = userAuthInfo.getRoles(); // 获取最大范围的数据权限 Integer dataScope = roleService.getMaximumDataScope(roles); - userAuthCredentials.setDataScope(dataScope); + userAuthInfo.setDataScope(dataScope); } - return userAuthCredentials; + return userAuthInfo; } /** @@ -400,15 +400,15 @@ public class UserServiceImpl extends ServiceImpl implements Us * 获取导出用户列表 * * @param queryParams 查询参数 - * @return {@link List} 导出用户列表 + * @return {@link List} 导出用户列表 */ @Override - public List listExportUsers(UserPageQuery queryParams) { + public List listExportUsers(UserPageQuery queryParams) { boolean isRoot = SecurityUtils.isRoot(); queryParams.setIsRoot(isRoot); - List exportUsers = this.baseMapper.listExportUsers(queryParams); + List exportUsers = this.baseMapper.listExportUsers(queryParams); if (CollectionUtil.isNotEmpty(exportUsers)) { //获取性别的字典项 Map genderMap = dictItemService.list( @@ -438,10 +438,10 @@ public class UserServiceImpl extends ServiceImpl implements Us /** * 获取登录用户信息 * - * @return {@link CurrentUserDTO} 用户信息 + * @return {@link CurrentUserDto} 用户信息 */ @Override - public CurrentUserDTO getCurrentUserInfo() { + public CurrentUserDto getCurrentUserInfo() { String username = SecurityUtils.getUsername(); @@ -455,30 +455,30 @@ public class UserServiceImpl extends ServiceImpl implements Us User::getAvatar ) ); - // entity->VO - CurrentUserDTO userInfoVO = userConverter.toCurrentUserDto(user); + // entity->Vo + CurrentUserDto userInfoVo = userConverter.toCurrentUserDto(user); // 用户角色集合 Set roles = SecurityUtils.getRoles(); - userInfoVO.setRoles(roles); + userInfoVo.setRoles(roles); // 用户权限集合 if (CollectionUtil.isNotEmpty(roles)) { Set perms = permissionService.getRolePermsFormCache(roles); - userInfoVO.setPerms(perms); + userInfoVo.setPerms(perms); } - return userInfoVO; + return userInfoVo; } /** * 获取个人中心用户信息 * * @param userId 用户ID - * @return {@link UserProfileVO} 个人中心用户信息 + * @return {@link UserProfileVo} 个人中心用户信息 */ @Override - public UserProfileVO getUserProfile(Long userId) { - UserBO entity = this.baseMapper.getUserProfile(userId); + public UserProfileVo getUserProfile(Long userId) { + UserBo entity = this.baseMapper.getUserProfile(userId); return userConverter.toProfileVo(entity); } diff --git a/src/main/resources/codegen.yml b/src/main/resources/codegen.yml index ceaf271a..f8cf8111 100644 --- a/src/main/resources/codegen.yml +++ b/src/main/resources/codegen.yml @@ -1,4 +1,3 @@ - # 代码生成器配置 codegen: # 下载代码文件名称 @@ -21,6 +20,10 @@ codegen: templatePath: codegen/api.ts.vm subpackageName: api extension: .ts + API_TYPES: + templatePath: codegen/api-types.ts.vm + subpackageName: types + extension: .ts VIEW: templatePath: codegen/index.vue.vm subpackageName: views @@ -50,7 +53,7 @@ codegen: Form: templatePath: codegen/form.java.vm subpackageName: model.form - VO: + Vo: templatePath: codegen/vo.java.vm subpackageName: model.vo Entity: diff --git a/src/main/resources/mapper/ai/AiCommandRecordMapper.xml b/src/main/resources/mapper/ai/AiAssistantRecordMapper.xml similarity index 62% rename from src/main/resources/mapper/ai/AiCommandRecordMapper.xml rename to src/main/resources/mapper/ai/AiAssistantRecordMapper.xml index 1755acdc..eae36156 100644 --- a/src/main/resources/mapper/ai/AiCommandRecordMapper.xml +++ b/src/main/resources/mapper/ai/AiAssistantRecordMapper.xml @@ -2,9 +2,9 @@ - + - + @@ -28,55 +28,55 @@ - SELECT - acr.id, - acr.user_id, - acr.username, - acr.original_command, - acr.ai_provider, - acr.ai_model, - acr.parse_status, - acr.function_calls, - acr.explanation, - acr.confidence, - acr.parse_error_message, - acr.input_tokens, - acr.output_tokens, - acr.parse_duration_ms, - acr.function_name, - acr.function_arguments, - acr.execute_status, - acr.execute_error_message, - acr.ip_address, - acr.create_time, - acr.update_time - FROM ai_command_record acr + aar.id, + aar.user_id, + aar.username, + aar.original_command, + aar.ai_provider, + aar.ai_model, + aar.parse_status, + aar.function_calls, + aar.explanation, + aar.confidence, + aar.parse_error_message, + aar.input_tokens, + aar.output_tokens, + aar.parse_duration_ms, + aar.function_name, + aar.function_arguments, + aar.execute_status, + aar.execute_error_message, + aar.ip_address, + aar.create_time, + aar.update_time + FROM ai_assistant_record aar ( - acr.original_command LIKE CONCAT('%', #{queryParams.keywords}, '%') - OR acr.function_name LIKE CONCAT('%', #{queryParams.keywords}, '%') - OR acr.username LIKE CONCAT('%', #{queryParams.keywords}, '%') + aar.original_command LIKE CONCAT('%', #{queryParams.keywords}, '%') + OR aar.function_name LIKE CONCAT('%', #{queryParams.keywords}, '%') + OR aar.username LIKE CONCAT('%', #{queryParams.keywords}, '%') ) - AND acr.execute_status = #{queryParams.executeStatus} + AND aar.execute_status = #{queryParams.executeStatus} - AND acr.user_id = #{queryParams.userId} + AND aar.user_id = #{queryParams.userId} - AND acr.parse_status = #{queryParams.parseStatus} + AND aar.parse_status = #{queryParams.parseStatus} - AND acr.function_name = #{queryParams.functionName} + AND aar.function_name = #{queryParams.functionName} - AND acr.ai_provider = #{queryParams.aiProvider} + AND aar.ai_provider = #{queryParams.aiProvider} - AND acr.ai_model = #{queryParams.aiModel} + AND aar.ai_model = #{queryParams.aiModel} - AND acr.create_time BETWEEN #{queryParams.createTime[0]} AND #{queryParams.createTime[1]} + AND aar.create_time BETWEEN #{queryParams.createTime[0]} AND #{queryParams.createTime[1]} - ORDER BY acr.create_time DESC + ORDER BY aar.create_time DESC - - diff --git a/src/main/resources/mapper/codegen/DatabaseMapper.xml b/src/main/resources/mapper/codegen/DatabaseMapper.xml index 0e402a8d..7771dfd2 100644 --- a/src/main/resources/mapper/codegen/DatabaseMapper.xml +++ b/src/main/resources/mapper/codegen/DatabaseMapper.xml @@ -6,7 +6,7 @@ - SELECT t1.TABLE_NAME , t1.TABLE_COMMENT , @@ -34,7 +34,7 @@ CREATE_TIME DESC - SELECT t1.TABLE_NAME , c.COMMENTS AS TABLE_COMMENT , diff --git a/src/main/resources/mapper/system/DictItemMapper.xml b/src/main/resources/mapper/system/DictItemMapper.xml index ab6cfec2..7c76c7ce 100644 --- a/src/main/resources/mapper/system/DictItemMapper.xml +++ b/src/main/resources/mapper/system/DictItemMapper.xml @@ -5,7 +5,7 @@ - SELECT id, dict_code, diff --git a/src/main/resources/mapper/system/DictMapper.xml b/src/main/resources/mapper/system/DictMapper.xml index 670694b4..1624b764 100644 --- a/src/main/resources/mapper/system/DictMapper.xml +++ b/src/main/resources/mapper/system/DictMapper.xml @@ -4,7 +4,7 @@ "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> - SELECT t1.id, t1.name, diff --git a/src/main/resources/mapper/system/LogMapper.xml b/src/main/resources/mapper/system/LogMapper.xml index ad3a205b..e542abaa 100644 --- a/src/main/resources/mapper/system/LogMapper.xml +++ b/src/main/resources/mapper/system/LogMapper.xml @@ -6,7 +6,7 @@ - SELECT t1.id, t1.module, @@ -48,7 +48,7 @@ t1.create_time DESC - SELECT t1.id, t1.module, @@ -91,7 +91,7 @@ - SELECT COUNT(1) AS count, DATE_FORMAT(create_time,'%Y-%m-%d') AS date @@ -105,7 +105,7 @@ - SELECT COUNT(DISTINCT ip) AS count, DATE_FORMAT(create_time, '%Y-%m-%d') AS date @@ -119,7 +119,7 @@ - SELECT COUNT(CASE WHEN DATE(create_time) = CURDATE() THEN 1 END) AS todayCount, COUNT(*) AS totalCount, @@ -138,7 +138,7 @@ is_deleted = 0 - SELECT COUNT(CASE WHEN TRUNC(create_time) = TRUNC(SYSDATE) THEN 1 END) AS todayCount, COUNT(*) AS totalCount, @@ -161,7 +161,7 @@ - SELECT COUNT(DISTINCT CASE WHEN DATE(create_time) = CURDATE() THEN ip END) AS todayCount, COUNT(DISTINCT ip) AS totalCount, @@ -180,7 +180,7 @@ is_deleted = 0 - SELECT COUNT(DISTINCT CASE WHEN TRUNC(create_time) = TRUNC(SYSDATE) THEN ip END) AS todayCount, COUNT(DISTINCT ip) AS totalCount, ROUND( diff --git a/src/main/resources/mapper/system/NoticeMapper.xml b/src/main/resources/mapper/system/NoticeMapper.xml index 3e25389f..1dcc97ec 100644 --- a/src/main/resources/mapper/system/NoticeMapper.xml +++ b/src/main/resources/mapper/system/NoticeMapper.xml @@ -3,7 +3,7 @@ - SELECT t1.id, t1.title, @@ -42,7 +42,7 @@ - SELECT t1.id, t1.title, diff --git a/src/main/resources/mapper/system/RoleMenuMapper.xml b/src/main/resources/mapper/system/RoleMenuMapper.xml index 75403b12..31b59157 100644 --- a/src/main/resources/mapper/system/RoleMenuMapper.xml +++ b/src/main/resources/mapper/system/RoleMenuMapper.xml @@ -16,7 +16,7 @@ - + diff --git a/src/main/resources/mapper/system/UserMapper.xml b/src/main/resources/mapper/system/UserMapper.xml index 7ea2f9f7..49efb882 100644 --- a/src/main/resources/mapper/system/UserMapper.xml +++ b/src/main/resources/mapper/system/UserMapper.xml @@ -6,7 +6,7 @@ - SELECT u.id, u.username, @@ -82,7 +82,7 @@ - SELECT u.id, u.username, @@ -196,7 +196,7 @@ - + @@ -208,7 +208,7 @@ - SELECT t1.id userId, t1.username, @@ -226,7 +226,7 @@ - SELECT t1.id userId, t1.username, @@ -244,7 +244,7 @@ - SELECT t1.id userId, t1.username, @@ -261,7 +261,7 @@ - SELECT u.username, u.nickname, @@ -302,7 +302,7 @@ - SELECT u.id, u.username, @@ -324,7 +324,7 @@ u.id = #{userId} AND u.is_deleted = 0 - SELECT u.id, u.username, diff --git a/src/main/resources/mapper/system/UserNoticeMapper.xml b/src/main/resources/mapper/system/UserNoticeMapper.xml index c3364cb0..5153ac49 100644 --- a/src/main/resources/mapper/system/UserNoticeMapper.xml +++ b/src/main/resources/mapper/system/UserNoticeMapper.xml @@ -3,7 +3,7 @@ - SELECT t2.id, t2.title, diff --git a/src/main/resources/mapper/system/UserRoleMapper.xml b/src/main/resources/mapper/system/UserRoleMapper.xml index 87c7ddb2..d99bce0f 100644 --- a/src/main/resources/mapper/system/UserRoleMapper.xml +++ b/src/main/resources/mapper/system/UserRoleMapper.xml @@ -15,7 +15,7 @@ - SELECT count(*) FROM diff --git a/src/main/resources/templates/codegen/api-types.ts.vm b/src/main/resources/templates/codegen/api-types.ts.vm new file mode 100644 index 00000000..611fd522 --- /dev/null +++ b/src/main/resources/templates/codegen/api-types.ts.vm @@ -0,0 +1,47 @@ +/** + * ${entityName} $!{businessName}类型定义 + */ + +/** $!{businessName}分页查询参数 */ +export interface ${entityName}PageQuery extends PageQuery { +#foreach($fieldConfig in $fieldConfigs) + #if($fieldConfig.isShowInQuery) + #if("$!fieldConfig.fieldComment" != "") + /** ${fieldConfig.fieldComment} */ + #end + #if($fieldConfig.formType == "DATE" || $fieldConfig.formType == "DATE_TIME") + #if($fieldConfig.queryType == "BETWEEN") + ${fieldConfig.fieldName}?: [string, string]; + #else + ${fieldConfig.fieldName}?: ${fieldConfig.tsType}; + #end + #else + ${fieldConfig.fieldName}?: ${fieldConfig.tsType}; + #end + #end +#end +} + +/** $!{businessName}表单对象 */ +export interface ${entityName}Form { +#foreach($fieldConfig in $fieldConfigs) + #if($fieldConfig.isShowInForm) + #if("$!fieldConfig.fieldComment" != "") + /** ${fieldConfig.fieldComment} */ + #end + ${fieldConfig.fieldName}?: ${fieldConfig.tsType}; + #end +#end +} + +/** $!{businessName}分页对象 */ +export interface ${entityName}PageVo { +#foreach($fieldConfig in $fieldConfigs) + #if($fieldConfig.isShowInList) + #if("$!fieldConfig.fieldComment" != "") + /** ${fieldConfig.fieldComment} */ + #end + ${fieldConfig.fieldName}?: ${fieldConfig.tsType}; + #end +#end +} diff --git a/src/main/resources/templates/codegen/api.ts.vm b/src/main/resources/templates/codegen/api.ts.vm index a80445e8..944f9535 100644 --- a/src/main/resources/templates/codegen/api.ts.vm +++ b/src/main/resources/templates/codegen/api.ts.vm @@ -1,12 +1,13 @@ import request from "@/utils/request"; +import type { ${entityName}Form, ${entityName}PageQuery, ${entityName}PageVo } from "@/api/types"; -const ${entityName.toUpperCase()}_BASE_URL = "/api/v1/${kebabCaseEntityName}"; +const ${entityUpperSnake}_BASE_URL = "/api/v1/${entityKebab}"; const ${entityName}API = { /** 获取${businessName}分页数据 */ getPage(queryParams?: ${entityName}PageQuery) { - return request>({ - url: `${${entityName.toUpperCase()}_BASE_URL}/page`, + return request>({ + url: `${${entityUpperSnake}_BASE_URL}/page`, method: "get", params: queryParams, }); @@ -19,7 +20,7 @@ const ${entityName}API = { */ getFormData(id: number) { return request({ - url: `${${entityName.toUpperCase()}_BASE_URL}/${id}/form`, + url: `${${entityUpperSnake}_BASE_URL}/${id}/form`, method: "get", }); }, @@ -31,7 +32,7 @@ const ${entityName}API = { */ create(data: ${entityName}Form) { return request({ - url: `${${entityName.toUpperCase()}_BASE_URL}`, + url: `${${entityUpperSnake}_BASE_URL}`, method: "post", data, }); @@ -45,7 +46,7 @@ const ${entityName}API = { */ update(id: string, data: ${entityName}Form) { return request({ - url: `${${entityName.toUpperCase()}_BASE_URL}/${id}`, + url: `${${entityUpperSnake}_BASE_URL}/${id}`, method: "put", data, }); @@ -58,54 +59,10 @@ const ${entityName}API = { */ deleteByIds(ids: string) { return request({ - url: `${${entityName.toUpperCase()}_BASE_URL}/${ids}`, + url: `${${entityUpperSnake}_BASE_URL}/${ids}`, method: "delete", }); } } export default ${entityName}API; - -/** ${businessName}分页查询参数 */ -export interface ${entityName}PageQuery extends PageQuery { - #foreach($fieldConfig in $fieldConfigs) - #if($fieldConfig.isShowInQuery) - #if("$!fieldConfig.fieldComment" != "") - /** ${fieldConfig.fieldComment} */ - #end - #if($fieldConfig.formType == "DATE" || $fieldConfig.formType == "DATE_TIME") - #if($fieldConfig.queryType == "BETWEEN") - ${fieldConfig.fieldName}?: [string, string]; - #else - ${fieldConfig.fieldName}?: ${fieldConfig.tsType}; - #end - #else - ${fieldConfig.fieldName}?: ${fieldConfig.tsType}; - #end - #end - #end -} - -/** ${businessName}表单对象 */ -export interface ${entityName}Form { - #foreach($fieldConfig in $fieldConfigs) - #if($fieldConfig.isShowInForm) - #if("$!fieldConfig.fieldComment" != "") - /** ${fieldConfig.fieldComment} */ - #end - ${fieldConfig.fieldName}?: ${fieldConfig.tsType}; - #end - #end -} - -/** ${businessName}分页对象 */ -export interface ${entityName}PageVO { - #foreach($fieldConfig in $fieldConfigs) - #if($fieldConfig.isShowInList) - #if("$!fieldConfig.fieldComment" != "") - /** ${fieldConfig.fieldComment} */ - #end - ${fieldConfig.fieldName}?: ${fieldConfig.tsType}; - #end - #end -} diff --git a/src/main/resources/templates/codegen/controller.java.vm b/src/main/resources/templates/codegen/controller.java.vm index bdadcc47..768a0de8 100644 --- a/src/main/resources/templates/codegen/controller.java.vm +++ b/src/main/resources/templates/codegen/controller.java.vm @@ -6,7 +6,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import ${packageName}.${moduleName}.model.form.${entityName}Form; import ${packageName}.${moduleName}.model.query.${entityName}Query; -import ${packageName}.${moduleName}.model.vo.${entityName}VO; +import ${packageName}.${moduleName}.model.vo.${entityName}Vo; import com.baomidou.mybatisplus.core.metadata.IPage; import com.youlai.boot.core.web.PageResult; import com.youlai.boot.core.web.Result; @@ -26,56 +26,56 @@ import jakarta.validation.Valid; */ @Tag(name = "${businessName}接口") @RestController -@RequestMapping("/api/v1/${kebabCaseEntityName}") +@RequestMapping("/api/v1/${entityKebab}") @RequiredArgsConstructor public class ${entityName}Controller { - private final ${entityName}Service ${lowerFirstEntityName}Service; + private final ${entityName}Service ${entityLowerCamel}Service; @Operation(summary = "$!{businessName}分页列表") @GetMapping("/page") - @PreAuthorize("@ss.hasPerm('${moduleName}:${kebabCaseEntityName}:query')") - public PageResult<${entityName}VO> get${entityName}Page(${entityName}Query queryParams ) { - IPage<${entityName}VO> result = ${lowerFirstEntityName}Service.get${entityName}Page(queryParams); + @PreAuthorize("@ss.hasPerm('${moduleName}:${entityKebab}:query')") + public PageResult<${entityName}Vo> get${entityName}Page(${entityName}Query queryParams ) { + IPage<${entityName}Vo> result = ${entityLowerCamel}Service.get${entityName}Page(queryParams); return PageResult.success(result); } @Operation(summary = "新增${businessName}") @PostMapping - @PreAuthorize("@ss.hasPerm('${moduleName}:${kebabCaseEntityName}:add')") + @PreAuthorize("@ss.hasPerm('${moduleName}:${entityKebab}:add')") public Result save${entityName}(@RequestBody @Valid ${entityName}Form formData ) { - boolean result = ${lowerFirstEntityName}Service.save${entityName}(formData); + boolean result = ${entityLowerCamel}Service.save${entityName}(formData); return Result.judge(result); } @Operation(summary = "获取${businessName}表单数据") @GetMapping("/{id}/form") - @PreAuthorize("@ss.hasPerm('${moduleName}:${kebabCaseEntityName}:edit')") + @PreAuthorize("@ss.hasPerm('${moduleName}:${entityKebab}:edit')") public Result<${entityName}Form> get${entityName}Form( @Parameter(description = "$!{businessName}ID") @PathVariable Long id ) { - ${entityName}Form formData = ${lowerFirstEntityName}Service.get${entityName}FormData(id); + ${entityName}Form formData = ${entityLowerCamel}Service.get${entityName}FormData(id); return Result.success(formData); } @Operation(summary = "修改${businessName}") @PutMapping(value = "/{id}") - @PreAuthorize("@ss.hasPerm('${moduleName}:${kebabCaseEntityName}:edit')") + @PreAuthorize("@ss.hasPerm('${moduleName}:${entityKebab}:edit')") public Result update${entityName}( @Parameter(description = "$!{businessName}ID") @PathVariable Long id, @RequestBody @Validated ${entityName}Form formData ) { - boolean result = ${lowerFirstEntityName}Service.update${entityName}(id, formData); + boolean result = ${entityLowerCamel}Service.update${entityName}(id, formData); return Result.judge(result); } @Operation(summary = "删除${businessName}") @DeleteMapping("/{ids}") - @PreAuthorize("@ss.hasPerm('${moduleName}:${kebabCaseEntityName}:delete')") + @PreAuthorize("@ss.hasPerm('${moduleName}:${entityKebab}:delete')") public Result delete${entityName}s( @Parameter(description = "$!{businessName}ID,多个以英文逗号(,)分割") @PathVariable String ids ) { - boolean result = ${lowerFirstEntityName}Service.delete${entityName}s(ids); + boolean result = ${entityLowerCamel}Service.delete${entityName}s(ids); return Result.judge(result); } } diff --git a/src/main/resources/templates/codegen/index.curd.vue.vm b/src/main/resources/templates/codegen/index.curd.vue.vm index 193d106d..2d89bece 100644 --- a/src/main/resources/templates/codegen/index.curd.vue.vm +++ b/src/main/resources/templates/codegen/index.curd.vue.vm @@ -63,7 +63,8 @@