refactor(backend): 重命名后端项目为 gym-manage-api,修改包名为 cn.novalon.gym.manage

This commit is contained in:
张翔
2026-04-17 18:35:50 +08:00
parent 666189b676
commit deb961c427
916 changed files with 108360 additions and 38328 deletions
@@ -0,0 +1,26 @@
package cn.novalon.gym.manage.app;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
@SpringBootApplication(scanBasePackages = "cn.novalon.gym.manage", exclude = {ReactiveUserDetailsServiceAutoConfiguration.class})
@EnableR2dbcRepositories(basePackages = {"cn.novalon.gym.manage.db.dao", "cn.novalon.gym.manage.sys.audit.repository"})
public class ManageApplication {
private static final Logger logger = LoggerFactory.getLogger(ManageApplication.class);
public static void main(String[] args) {
logger.info("应用程序启动中...");
logger.info("包扫描路径: cn.novalon.gym.manage");
// 使用简单的启动方式,避免自动配置问题
SpringApplication.run(ManageApplication.class, args);
logger.info("应用程序启动完成");
}
}
@@ -0,0 +1,42 @@
package cn.novalon.gym.manage.app;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 最小化应用程序启动类
* 避免复杂的自动配置问题,专注于核心功能
*/
@SpringBootApplication(
scanBasePackages = {
"cn.novalon.gym.manage.app.config",
"cn.novalon.gym.manage.app.controller",
"cn.novalon.gym.manage.app.service"
}
)
public class MinimalApplication {
private static final Logger logger = LoggerFactory.getLogger(MinimalApplication.class);
public static void main(String[] args) {
logger.info("最小化应用程序启动中...");
// 设置系统属性,避免自动配置问题
System.setProperty("spring.autoconfigure.exclude",
"org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration");
// 禁用复杂的自动配置
System.setProperty("spring.main.lazy-initialization", "true");
System.setProperty("spring.main.banner-mode", "off");
try {
SpringApplication.run(MinimalApplication.class, args);
logger.info("最小化应用程序启动完成");
} catch (Exception e) {
logger.error("应用程序启动失败: {}", e.getMessage());
e.printStackTrace();
}
}
}
@@ -0,0 +1,32 @@
package cn.novalon.gym.manage.app;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration;
/**
* 简化的应用程序启动类
* 避免复杂的自动配置问题
*/
@SpringBootApplication(
scanBasePackages = "cn.novalon.gym.manage.app",
exclude = {ReactiveUserDetailsServiceAutoConfiguration.class}
)
public class SimpleManageApplication {
private static final Logger logger = LoggerFactory.getLogger(SimpleManageApplication.class);
public static void main(String[] args) {
logger.info("简化版应用程序启动中...");
logger.info("包扫描路径: cn.novalon.gym.manage.app");
// 设置系统属性,避免自动配置问题
System.setProperty("spring.autoconfigure.exclude",
"org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration");
SpringApplication.run(SimpleManageApplication.class, args);
logger.info("简化版应用程序启动完成");
}
}
@@ -0,0 +1,57 @@
package cn.novalon.gym.manage.app.config;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* Jackson配置类
*
* 用于统一时间格式化配置
*
* @author 张翔
* @date 2026-03-26
*/
@Configuration
public class JacksonConfig {
private static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
@Bean
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
JavaTimeModule javaTimeModule = new JavaTimeModule();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_TIME_FORMAT);
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter));
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter));
objectMapper.registerModule(javaTimeModule);
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
return objectMapper;
}
@Bean
public Jackson2JsonEncoder jackson2JsonEncoder(ObjectMapper objectMapper) {
return new Jackson2JsonEncoder(objectMapper);
}
@Bean
public Jackson2JsonDecoder jackson2JsonDecoder(ObjectMapper objectMapper) {
return new Jackson2JsonDecoder(objectMapper);
}
}
@@ -0,0 +1,19 @@
package cn.novalon.gym.manage.app.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader;
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
@Configuration
public class MultipartConfig {
@Bean
public MultipartHttpMessageReader multipartHttpMessageReader() {
DefaultPartHttpMessageReader partReader = new DefaultPartHttpMessageReader();
partReader.setMaxHeadersSize(8192);
partReader.setMaxDiskUsagePerPart(10 * 1024 * 1024);
partReader.setEnableLoggingRequestDetails(true);
return new MultipartHttpMessageReader(partReader);
}
}
@@ -0,0 +1,60 @@
package cn.novalon.gym.manage.app.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.servers.Server;
import io.swagger.v3.oas.models.tags.Tag;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.List;
/**
* OpenAPI配置类
*
* @author 张翔
* @date 2026-03-14
*/
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Novalon Manage System API")
.version("1.0.0")
.description("Novalon 管理系统 RESTful API 文档")
.contact(new Contact()
.name("Novalon Team")
.email("support@novalon.cn"))
.license(new License()
.name("Apache 2.0")
.url("https://www.apache.org/licenses/LICENSE-2.0")))
.servers(List.of(
new Server().url("http://localhost:8084").description("开发环境"),
new Server().url("https://api.novalon.cn").description("生产环境")))
.tags(Arrays.asList(
new Tag().name("用户管理").description("用户相关操作"),
new Tag().name("角色管理").description("角色相关操作"),
new Tag().name("配置管理").description("系统配置相关操作"),
new Tag().name("字典管理").description("字典数据相关操作"),
new Tag().name("通知管理").description("系统通知相关操作"),
new Tag().name("文件管理").description("文件上传下载相关操作"),
new Tag().name("日志管理").description("操作日志相关操作"),
new Tag().name("认证管理").description("登录认证相关操作"),
new Tag().name("统计信息").description("系统统计相关操作")));
}
@Bean
public GroupedOpenApi allApi() {
return GroupedOpenApi.builder()
.group("all")
.pathsToMatch("/api/**")
.build();
}
}
@@ -0,0 +1,41 @@
package cn.novalon.gym.manage.app.config;
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
@Configuration
public class RateLimitConfig {
@Value("${rate.limit.limit-for-period:100}")
private int limitForPeriod;
@Value("${rate.limit.limit-refresh-period:1s}")
private Duration limitRefreshPeriod;
@Value("${rate.limit.timeout-duration:0}")
private Duration timeoutDuration;
@Bean
public RateLimiterRegistry rateLimiterRegistry() {
RateLimiterConfig config = RateLimiterConfig.custom()
.limitForPeriod(limitForPeriod)
.limitRefreshPeriod(limitRefreshPeriod)
.timeoutDuration(timeoutDuration)
.build();
return RateLimiterRegistry.of(config);
}
@Bean
@Qualifier("apiRateLimiter")
public RateLimiter apiRateLimiter(RateLimiterRegistry registry) {
return registry.rateLimiter("apiRateLimiter");
}
}
@@ -0,0 +1,198 @@
package cn.novalon.gym.manage.app.config;
import cn.novalon.gym.manage.sys.handler.auth.SysAuthHandler;
import cn.novalon.gym.manage.sys.handler.auth.PasswordDiagnosticHandler;
import cn.novalon.gym.manage.sys.handler.config.SysConfigHandler;
import cn.novalon.gym.manage.sys.handler.dictionary.DictionaryHandler;
import cn.novalon.gym.manage.sys.handler.dict.SysDictHandler;
import cn.novalon.gym.manage.sys.handler.log.SysLogHandler;
import cn.novalon.gym.manage.sys.handler.log.OperationLogHandler;
import cn.novalon.gym.manage.sys.handler.menu.MenuHandler;
import cn.novalon.gym.manage.sys.handler.role.SysRoleHandler;
import cn.novalon.gym.manage.sys.handler.permission.SysPermissionHandler;
import cn.novalon.gym.manage.sys.handler.stats.StatsHandler;
import cn.novalon.gym.manage.sys.handler.user.SysUserHandler;
import cn.novalon.gym.manage.notify.handler.SysNoticeHandler;
import cn.novalon.gym.manage.notify.handler.SysUserMessageHandler;
import cn.novalon.gym.manage.file.handler.SysFileHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
/**
* 系统路由配置类
*
* 文件定义:配置WebFlux函数式路由,将HTTP请求映射到对应的Handler方法
* 涉及业务:用户、角色、字典、菜单、公告、文件等所有RESTful API路由
* 算法:使用RouterFunctions.route()构建函数式路由规则
*
* @author 张翔
* @date 2026-03-13
*/
@Configuration
public class SystemRouter {
@Bean
public RouterFunction<ServerResponse> systemRoutes(
DictionaryHandler dictionaryHandler,
SysUserHandler userHandler,
MenuHandler menuHandler,
SysRoleHandler roleHandler,
SysConfigHandler configHandler,
SysLogHandler logHandler,
OperationLogHandler operationLogHandler,
SysAuthHandler authHandler,
StatsHandler statsHandler,
SysDictHandler dictHandler,
SysNoticeHandler noticeHandler,
SysUserMessageHandler messageHandler,
SysFileHandler fileHandler,
SysPermissionHandler permissionHandler,
PasswordDiagnosticHandler passwordDiagnosticHandler) {
return route()
// ========== 诊断路由 ==========
.GET("/api/diagnostic/password", passwordDiagnosticHandler::diagnose)
// ========== 字典路由 ==========
.GET("/api/dictionaries", dictionaryHandler::getAllDictionaries)
.GET("/api/dictionaries/{id}", dictionaryHandler::getDictionaryById)
.GET("/api/dictionaries/type/{type}", dictionaryHandler::getDictionariesByType)
.GET("/api/dictionaries/check/exists", dictionaryHandler::checkTypeAndCodeExists)
.POST("/api/dictionaries", dictionaryHandler::createDictionary)
.PUT("/api/dictionaries/{id}", dictionaryHandler::updateDictionary)
.DELETE("/api/dictionaries/{id}", dictionaryHandler::deleteDictionary)
// ========== 用户路由 ==========
.GET("/api/users", userHandler::getAllUsers)
.GET("/api/users/page", userHandler::getUsersByPage)
.GET("/api/users/count", userHandler::getUserCount)
.GET("/api/users/username/{username}", userHandler::getUserByUsername)
.GET("/api/users/check/username", userHandler::checkUsernameExists)
.GET("/api/users/check/email", userHandler::checkEmailExists)
.POST("/api/users", userHandler::createUser)
.GET("/api/users/{id}", userHandler::getUserById)
.PUT("/api/users/{id}", userHandler::updateUser)
.DELETE("/api/users/{id}", userHandler::deleteUser)
.POST("/api/users/{id}/action/change-password", userHandler::changePassword)
.POST("/api/users/{id}/action/logical-delete", userHandler::logicalDeleteUser)
.POST("/api/users/logical-delete", userHandler::logicalDeleteUsers)
.POST("/api/users/action/restore", userHandler::restoreUsers)
.POST("/api/users/{id}/action/restore", userHandler::restoreUser)
.GET("/api/users/{id}/roles", userHandler::getUserRoles)
.POST("/api/users/{id}/roles", userHandler::assignRoles)
// ========== 菜单路由 ==========
.GET("/api/menus", menuHandler::getAllMenus)
.GET("/api/menus/tree", menuHandler::getMenuTree)
.GET("/api/menus/{id}", menuHandler::getMenuById)
.POST("/api/menus", menuHandler::createMenu)
.PUT("/api/menus/{id}", menuHandler::updateMenu)
.DELETE("/api/menus/{id}", menuHandler::deleteMenu)
// ========== 角色路由 ==========
.GET("/api/roles", roleHandler::getAllRoles)
.GET("/api/roles/page", roleHandler::getRolesByPage)
.GET("/api/roles/count", roleHandler::getRoleCount)
.GET("/api/roles/name/{roleName}", roleHandler::getRoleByName)
.GET("/api/roles/check-name", roleHandler::checkNameExists)
.GET("/api/roles/{id}", roleHandler::getRoleById)
.POST("/api/roles", roleHandler::createRole)
.PUT("/api/roles/{id}", roleHandler::updateRole)
.DELETE("/api/roles/{id}", roleHandler::deleteRole)
.POST("/api/roles/{id}/restore", roleHandler::restoreRole)
.GET("/api/roles/{id}/permissions", permissionHandler::getPermissionsByRoleId)
.POST("/api/roles/{id}/permissions", permissionHandler::assignPermissionsToRole)
// ========== 配置路由 ==========
.GET("/api/config", configHandler::getAllConfigs)
.GET("/api/config/{id}", configHandler::getConfigById)
.GET("/api/config/key/{configKey}", configHandler::getConfigByKey)
.POST("/api/config", configHandler::createConfig)
.PUT("/api/config/{id}", configHandler::updateConfig)
.DELETE("/api/config/{id}", configHandler::deleteConfig)
// ========== 日志路由 ==========
.GET("/api/logs/login", logHandler::getAllLoginLogs)
.GET("/api/logs/login/page", logHandler::getLoginLogsByPage)
.GET("/api/logs/login/count", logHandler::getLoginLogCount)
.GET("/api/logs/login/today/count", logHandler::getTodayLoginCount)
.GET("/api/logs/login/recent", logHandler::getRecentLoginLogs)
.GET("/api/logs/login/{id}", logHandler::getLoginLogById)
.POST("/api/logs/login", logHandler::createLoginLog)
.GET("/api/logs/exception", logHandler::getAllExceptionLogs)
.GET("/api/logs/exception/page", logHandler::getExceptionLogsByPage)
.GET("/api/logs/exception/count", logHandler::getExceptionLogCount)
.GET("/api/logs/exception/{id}", logHandler::getExceptionLogById)
.POST("/api/logs/exception", logHandler::createExceptionLog)
.GET("/api/logs/operation", operationLogHandler::getAllOperationLogs)
.GET("/api/logs/operation/export", operationLogHandler::exportOperationLogs)
.GET("/api/logs/operation/page", operationLogHandler::getOperationLogsByPage)
.GET("/api/logs/operation/count", operationLogHandler::getOperationLogCount)
.GET("/api/logs/operation/{id}", operationLogHandler::getOperationLogById)
.POST("/api/logs/operation", operationLogHandler::createOperationLog)
// ========== 认证路由 ==========
.POST("/api/auth/login", authHandler::login)
.POST("/api/auth/register", authHandler::register)
.POST("/api/auth/logout", authHandler::logout)
// ========== 统计路由 ==========
.GET("/api/stats/overview", statsHandler::getOverview)
// ========== 数据字典路由 ==========
.GET("/api/dict/types", dictHandler::getAllDictTypes)
.GET("/api/dict/types/{id}", dictHandler::getDictTypeById)
.GET("/api/dict/types/type/{dictType}", dictHandler::getDictTypeByType)
.POST("/api/dict/types", dictHandler::createDictType)
.PUT("/api/dict/types/{id}", dictHandler::updateDictType)
.DELETE("/api/dict/types/{id}", dictHandler::deleteDictType)
.GET("/api/dict/data", dictHandler::getAllDictData)
.GET("/api/dict/data/type/{dictType}", dictHandler::getDictDataByType)
.GET("/api/dict/data/{id}", dictHandler::getDictDataById)
.POST("/api/dict/data", dictHandler::createDictData)
.PUT("/api/dict/data/{id}", dictHandler::updateDictData)
.DELETE("/api/dict/data/{id}", dictHandler::deleteDictData)
// ========== 公告路由 ==========
.GET("/api/notices", noticeHandler::getAllNotices)
.GET("/api/notices/{id}", noticeHandler::getNoticeById)
.GET("/api/notices/status/{status}", noticeHandler::getNoticesByStatus)
.POST("/api/notices", noticeHandler::createNotice)
.PUT("/api/notices/{id}", noticeHandler::updateNotice)
.DELETE("/api/notices/{id}", noticeHandler::deleteNotice)
// ========== 消息路由 ==========
.GET("/api/messages/user/{userId}", messageHandler::getMessagesByUser)
.GET("/api/messages/user/{userId}/unread", messageHandler::getUnreadCount)
.GET("/api/messages/user/{userId}/unread/list", messageHandler::getUnreadList)
.POST("/api/messages", messageHandler::createMessage)
.PUT("/api/messages/{id}/read", messageHandler::markAsRead)
.DELETE("/api/messages/{id}", messageHandler::deleteMessage)
// ========== 文件路由 ==========
.GET("/api/files", fileHandler::getAllFiles)
.GET("/api/files/{id}", fileHandler::getFileById)
.POST("/api/files/upload", fileHandler::uploadFile)
.GET("/api/files/{id}/download", fileHandler::downloadFile)
.GET("/api/files/download/{fileName}", fileHandler::downloadFileByName)
.GET("/api/files/{id}/preview", fileHandler::previewFile)
.GET("/api/files/preview/{fileName}", fileHandler::previewFileByName)
.DELETE("/api/files/{id}", fileHandler::deleteFile)
// ========== 权限路由 ==========
.GET("/api/permissions", permissionHandler::getAllPermissions)
.GET("/api/permissions/{id}", permissionHandler::getPermissionById)
.GET("/api/permissions/code/{code}", permissionHandler::getPermissionByCode)
.GET("/api/permissions/check-code", permissionHandler::checkCodeExists)
.GET("/api/permissions/count", permissionHandler::getPermissionCount)
.POST("/api/permissions", permissionHandler::createPermission)
.PUT("/api/permissions/{id}", permissionHandler::updatePermission)
.DELETE("/api/permissions/{id}", permissionHandler::deletePermission)
.build();
}
}
@@ -0,0 +1,20 @@
package cn.novalon.gym.manage.app.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.config.WebFluxConfigurer;
/**
* WebFlux配置类
*
* @author 张翔
* @date 2026-03-14
*/
@Configuration
public class WebFluxConfig implements WebFluxConfigurer {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().maxInMemorySize(16 * 1024 * 1024);
}
}
@@ -0,0 +1,5 @@
cn.novalon.manage.app.config.OpenApiConfig
cn.novalon.manage.app.config.WebFluxConfig
cn.novalon.manage.app.config.SystemRouter
cn.novalon.manage.app.config.MultipartConfig
cn.novalon.manage.app.config.RateLimitConfig
@@ -0,0 +1,22 @@
spring:
r2dbc:
url: r2dbc:postgresql://localhost:55432/manage_system
username: novalon
password: novalon123
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
validate-on-migrate: true
rate:
limit:
limit-for-period: 10000
limit-refresh-period: 1s
timeout-duration: 0
logging:
level:
cn.novalon.manage: DEBUG
org.springframework.r2dbc: DEBUG
org.springframework.web: TRACE
@@ -0,0 +1,36 @@
# 本地开发环境配置
spring:
config:
activate:
on-profile: local
r2dbc:
url: r2dbc:postgresql://localhost:55432/manage_system
username: novalon
password: novalon123
pool:
initial-size: 5
max-size: 20
max-idle-time: 10m
max-life-time: 30m
acquire-timeout: 3s
datasource:
url: jdbc:postgresql://localhost:55432/manage_system
username: novalon
password: novalon123
driver-class-name: org.postgresql.Driver
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
baseline-version: 0
validate-on-migrate: true
sql:
init:
mode: always
logging:
level:
cn.novalon.manage: DEBUG
org.springframework.r2dbc: DEBUG
cn.novalon.manage.db: DEBUG
org.flywaydb: DEBUG
@@ -0,0 +1,17 @@
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
metrics:
enabled: true
info:
env:
enabled: true
metrics:
export:
simple:
enabled: true
@@ -0,0 +1,12 @@
spring:
r2dbc:
url: r2dbc:postgresql://postgres:5432/novalon_manage
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
flyway:
enabled: true
logging:
level:
cn.novalon.manage: INFO
org.springframework.r2dbc: INFO
@@ -0,0 +1,62 @@
server:
port: 8084
spring:
application:
name: manage-app
r2dbc:
url: r2dbc:postgresql://localhost:55432/manage_system
username: novalon
password: novalon123
pool:
initial-size: 5
max-size: 20
max-idle-time: 30m
max-life-time: 1h
acquire-timeout: 5s
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
validate-on-migrate: true
sql:
init:
mode: never
security:
user:
name: disabled
password: disabled
management:
endpoints:
web:
exposure:
include: health,info,metrics,env,loggers
base-path: /actuator
endpoint:
health:
show-details: always
metrics:
tags:
application: ${spring.application.name}
environment: ${spring.profiles.active}
logging:
level:
cn.novalon.manage: DEBUG
org.springframework.r2dbc: DEBUG
cn.novalon.manage.db: DEBUG
org.flywaydb: INFO
springdoc:
api-docs:
path: /api-docs
enabled: true
swagger-ui:
path: /swagger-ui.html
enabled: true
tags-sorter: alpha
operations-sorter: alpha
show-actuator: false
default-consumes-media-type: application/json
default-produces-media-type: application/json
@@ -0,0 +1,68 @@
server:
port: 8084
spring:
application:
name: gym-manage-api
r2dbc:
url: r2dbc:postgresql://${DB_HOST:localhost}:${DB_PORT:55432}/${DB_NAME:manage_system}
username: ${DB_USERNAME:postgres}
password: ${DB_PASSWORD:postgres}
pool:
initial-size: 10
max-size: 50
max-idle-time: 30m
max-life-time: 1h
acquire-timeout: 5s
datasource:
url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:55432}/${DB_NAME:manage_system}
username: ${DB_USERNAME:postgres}
password: ${DB_PASSWORD:postgres}
driver-class-name: org.postgresql.Driver
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
baseline-version: 0
validate-on-migrate: true
security:
user:
name: disabled
password: disabled
management:
endpoints:
web:
exposure:
include: health,info,metrics,env,loggers
base-path: /actuator
endpoint:
health:
show-details: always
metrics:
tags:
application: ${spring.application.name}
environment: ${spring.profiles.active}
logging:
level:
cn.novalon.manage: DEBUG
org.springframework.r2dbc: DEBUG
cn.novalon.manage.db: DEBUG
jwt:
secret: ${JWT_SECRET:U2FsdGVkX1+vZ5Y9QmKxL8nN3rP7tW2jH4fG6dA8sB1cE5yN0zX3qV7wM4}
expiration: ${JWT_EXPIRATION:86400000}
springdoc:
api-docs:
path: /api-docs
enabled: true
swagger-ui:
path: /swagger-ui.html
enabled: true
tags-sorter: alpha
operations-sorter: alpha
show-actuator: false
default-consumes-media-type: application/json
default-produces-media-type: application/json
@@ -0,0 +1,30 @@
╔═══════════════════════════════════════════════════════════════════╗
║ ║
║ ███╗ ██╗ ██████╗ ██╗ ██╗ █████╗ ██╗ ██████╗ ███╗ ██╗ ║
║ ████╗ ██║██╔═══██╗██║ ██║██╔══██╗██║ ██╔═══██╗████╗ ██║ ║
║ ██╔██╗ ██║██║ ██║██║ ██║███████║██║ ██║ ██║██╔██╗ ██║ ║
║ ██║╚██╗██║██║ ██║╚██╗ ██╔╝██╔══██║██║ ██║ ██║██║╚██╗██║ ║
║ ██║ ╚████║╚██████╔╝ ╚████╔╝ ██║ ██║███████╗╚██████╔╝██║ ╚████║ ║
║ ╚═╝ ╚═══╝ ╚═════╝ ╚═══╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═══╝ ║
║ ║
║ ███╗ ███╗ █████╗ ███╗ ██╗ █████╗ ██████╗ ███████╗ ║
║ ████╗ ████║██╔══██╗████╗ ██║██╔══██╗██╔════╝ ██╔════╝ ║
║ ██╔████╔██║███████║██╔██╗ ██║███████║██║ ███╗█████╗ ║
║ ██║╚██╔╝██║██╔══██║██║╚██╗██║██╔══██║██║ ██║██╔══╝ ║
║ ██║ ╚═╝ ██║██║ ██║██║ ╚████║██║ ██║╚██████╔╝███████╗ ║
║ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ║
║ ║
║ ███████╗██╗ ██╗███████╗████████╗███████╗███╗ ███╗ ║
║ ██╔════╝╚██╗ ██╔╝██╔════╝╚══██╔══╝██╔════╝████╗ ████║ ║
║ ███████╗ ╚████╔╝ ███████╗ ██║ █████╗ ██╔████╔██║ ║
║ ╚════██║ ╚██╔╝ ╚════██║ ██║ ██╔══╝ ██║╚██╔╝██║ ║
║ ███████║ ██║ ███████║ ██║ ███████╗██║ ╚═╝ ██║ ║
║ ╚══════╝ ╚═╝ ╚══════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ ║
║ ║
╚═══════════════════════════════════════════════════════════════════╝
:: Novalon Manage System ::
Version: ${application.version:Unknown}
Spring Boot: ${spring-boot.version}
Java: ${java.version}
PID: ${PID}
@@ -0,0 +1,84 @@
-- H2数据库测试数据
-- 用于测试环境
-- 插入测试角色
MERGE INTO sys_role (id, role_name, role_key, role_sort, status, create_by, update_by)
KEY(id)
VALUES
(1, '超级管理员', 'admin', 1, 1, 'system', 'system'),
(2, '测试管理员', 'test_admin', 2, 1, 'system', 'system'),
(3, '普通用户', 'normal_user', 3, 1, 'system', 'system'),
(4, '访客', 'guest', 4, 1, 'system', 'system');
-- 插入测试用户
-- BCrypt哈希值对应明文密码: Test@123
MERGE INTO sys_user (id, username, password, email, phone, nickname, status, create_by, update_by)
KEY(id)
VALUES
(1, 'admin', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'admin@novalon.com', '13800138000', '超级管理员', 1, 'system', 'system'),
(2, 'testadmin', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'testadmin@novalon.com', '13800138001', '测试管理员', 1, 'system', 'system'),
(3, 'normaluser', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'normaluser@novalon.com', '13800138002', '普通用户', 1, 'system', 'system'),
(4, 'guestuser', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'guestuser@novalon.com', '13800138003', '访客用户', 1, 'system', 'system'),
(5, 'disableduser', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'disableduser@novalon.com', '13800138004', '禁用用户', 0, 'system', 'system'),
(10, 'e2e_test_user', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'e2e@test.com', '13900139000', 'E2E测试用户', 1, 'system', 'system');
-- 为用户分配角色
INSERT INTO user_role (user_id, role_id, created_by)
VALUES
(1, 1, 'system'),
(2, 2, 'system'),
(3, 3, 'system'),
(4, 4, 'system'),
(10, 1, 'system');
-- 插入测试菜单
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_by, update_by)
VALUES
(1, '系统管理', 0, 1, '/system', 'Layout', 'M', '1', '1', '', 'system', 'system', 'system'),
(2, '用户管理', 1, 1, 'user', 'system/user/index', 'C', '1', '1', 'system:user:list', 'user', 'system', 'system'),
(3, '角色管理', 1, 2, 'role', 'system/role/index', 'C', '1', '1', 'system:role:list', 'role', 'system', 'system'),
(4, '菜单管理', 1, 3, 'menu', 'system/menu/index', 'C', '1', '1', 'system:menu:list', 'menu', 'system', 'system'),
(5, '测试菜单', 0, 99, '/test', 'Layout', 'M', '1', '1', '', 'test', 'system', 'system'),
(6, '用户测试', 5, 1, 'user-test', 'system/user-test/index', 'C', '1', '1', 'system:user:test', 'user', 'system', 'system');
-- 插入测试权限
INSERT INTO sys_permission (id, permission_name, permission_code, resource, action, description, status, create_by, update_by)
VALUES
(1, '系统管理', 'system:manage', '/api/system', 'GET', '系统管理权限', 1, 'system', 'system'),
(2, '用户管理', 'system:user:manage', '/api/users', 'GET', '用户管理权限', 1, 'system', 'system'),
(3, '用户查询', 'system:user:list', '/api/users', 'GET', '用户查询权限', 1, 'system', 'system'),
(4, '用户新增', 'system:user:add', '/api/users', 'POST', '用户新增权限', 1, 'system', 'system'),
(5, '用户编辑', 'system:user:edit', '/api/users', 'PUT', '用户编辑权限', 1, 'system', 'system'),
(6, '用户删除', 'system:user:delete', '/api/users', 'DELETE', '用户删除权限', 1, 'system', 'system'),
(7, '测试权限', 'test:permission', '/api/test', 'GET', '测试权限', 1, 'system', 'system'),
(8, '用户测试权限', 'system:user:test', '/api/users/test', 'GET', '用户测试权限', 1, 'system', 'system');
-- 为角色分配权限
INSERT INTO sys_role_permission (role_id, permission_id, created_by, updated_by)
SELECT 1, id, 'system', 'system' FROM sys_permission
UNION ALL
SELECT 2, id, 'system', 'system' FROM sys_permission WHERE id IN (7, 8);
-- 插入字典类型
INSERT INTO sys_dict_type (id, dict_name, dict_type, status, remark, create_by, update_by)
VALUES
(1, '用户状态', 'user_status', '0', '用户状态列表', 'system', 'system'),
(2, '菜单状态', 'menu_status', '0', '菜单状态列表', 'system', 'system'),
(3, '角色状态', 'role_status', '0', '角色状态列表', 'system', 'system');
-- 插入字典数据
INSERT INTO sys_dict_data (id, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, update_by)
VALUES
(1, 1, '正常', '1', 'user_status', '', 'primary', 'Y', '0', 'system', 'system'),
(2, 2, '停用', '0', 'user_status', '', 'danger', 'N', '0', 'system', 'system'),
(3, 1, '正常', '0', 'menu_status', '', 'primary', 'Y', '0', 'system', 'system'),
(4, 2, '停用', '1', 'menu_status', '', 'danger', 'N', '0', 'system', 'system'),
(5, 1, '正常', '0', 'role_status', '', 'primary', 'Y', '0', 'system', 'system'),
(6, 2, '停用', '1', 'role_status', '', 'danger', 'N', '0', 'system', 'system');
-- 插入系统配置
INSERT INTO sys_config (id, config_name, config_key, config_value, config_type, create_by, update_by)
VALUES
(1, '用户管理-用户初始密码', 'sys.user.initPassword', '123456', 'Y', 'system', 'system'),
(2, '主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue', 'Y', 'system', 'system'),
(3, '用户自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', 'system', 'system');
@@ -0,0 +1,253 @@
-- H2数据库Schema for Integration Testing
-- Create用户表
CREATE TABLE IF NOT EXISTS sys_user (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
email VARCHAR(100),
phone VARCHAR(20),
nickname VARCHAR(100),
role_id BIGINT,
status INTEGER DEFAULT 1,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create角色表
CREATE TABLE IF NOT EXISTS sys_role (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
role_name VARCHAR(100) NOT NULL,
role_key VARCHAR(100) NOT NULL UNIQUE,
role_sort INTEGER DEFAULT 0,
status INTEGER DEFAULT 1,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create用户角色关联表
CREATE TABLE IF NOT EXISTS user_role (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(50),
CONSTRAINT fk_user_role_user FOREIGN KEY (user_id) REFERENCES sys_user(id) ON DELETE CASCADE,
CONSTRAINT fk_user_role_role FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE,
CONSTRAINT uk_user_role UNIQUE (user_id, role_id)
);
-- Create菜单表
CREATE TABLE IF NOT EXISTS sys_menu (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
menu_name VARCHAR(50) NOT NULL,
parent_id BIGINT DEFAULT 0,
order_num INTEGER DEFAULT 0,
path VARCHAR(200),
component VARCHAR(200),
menu_type VARCHAR(1) DEFAULT 'C',
visible VARCHAR(1) DEFAULT '1',
status VARCHAR(1) DEFAULT '1',
perms VARCHAR(100),
icon VARCHAR(100),
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create权限表
CREATE TABLE IF NOT EXISTS sys_permission (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
permission_name VARCHAR(100) NOT NULL,
permission_code VARCHAR(100) NOT NULL UNIQUE,
resource VARCHAR(200),
action VARCHAR(20),
description VARCHAR(500),
status INTEGER DEFAULT 1,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create角色权限关联表
CREATE TABLE IF NOT EXISTS sys_role_permission (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(50),
updated_by VARCHAR(50),
CONSTRAINT fk_role_permission_role FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE,
CONSTRAINT fk_role_permission_permission FOREIGN KEY (permission_id) REFERENCES sys_permission(id) ON DELETE CASCADE,
CONSTRAINT uk_role_permission UNIQUE (role_id, permission_id)
);
-- Create字典类型表
CREATE TABLE IF NOT EXISTS sys_dict_type (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
dict_name VARCHAR(100) NOT NULL,
dict_type VARCHAR(100) NOT NULL UNIQUE,
status VARCHAR(1) DEFAULT '0',
remark VARCHAR(500),
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create字典数据表
CREATE TABLE IF NOT EXISTS sys_dict_data (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
dict_sort INTEGER DEFAULT 0,
dict_label VARCHAR(100) NOT NULL,
dict_value VARCHAR(100) NOT NULL,
dict_type VARCHAR(100) NOT NULL,
css_class VARCHAR(100),
list_class VARCHAR(100),
is_default VARCHAR(1) DEFAULT 'N',
status VARCHAR(1) DEFAULT '0',
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create字典表(通用字典)
CREATE TABLE IF NOT EXISTS sys_dictionary (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
type VARCHAR(100) NOT NULL,
code VARCHAR(100) NOT NULL,
name VARCHAR(100) NOT NULL,
dict_value VARCHAR(500),
remark VARCHAR(500),
sort INTEGER DEFAULT 0,
create_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create系统配置表
CREATE TABLE IF NOT EXISTS sys_config (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
config_name VARCHAR(100) NOT NULL,
config_key VARCHAR(100) NOT NULL UNIQUE,
config_value VARCHAR(500) NOT NULL,
config_type VARCHAR(1) DEFAULT 'N',
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create登录日志表
CREATE TABLE IF NOT EXISTS sys_login_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50),
ip VARCHAR(50),
location VARCHAR(255),
browser VARCHAR(50),
os VARCHAR(50),
status VARCHAR(1),
message VARCHAR(255),
login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create异常日志表
CREATE TABLE IF NOT EXISTS sys_exception_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50),
title VARCHAR(100),
exception_name VARCHAR(100),
method_name VARCHAR(255),
method_params TEXT,
exception_msg TEXT,
exception_stack TEXT,
ip VARCHAR(50),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create操作日志表
CREATE TABLE IF NOT EXISTS operation_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50),
operation VARCHAR(100),
method VARCHAR(200),
params TEXT,
result TEXT,
ip VARCHAR(50),
duration BIGINT,
status VARCHAR(1) DEFAULT '0',
error_msg TEXT,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create系统公告表
CREATE TABLE IF NOT EXISTS sys_notice (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
notice_title VARCHAR(50) NOT NULL,
notice_type VARCHAR(1) NOT NULL,
notice_content TEXT,
status VARCHAR(1) DEFAULT '0',
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create用户消息表
CREATE TABLE IF NOT EXISTS sys_user_message (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
notice_id BIGINT,
message_title VARCHAR(255),
message_content TEXT,
is_read VARCHAR(1) DEFAULT '0',
read_time TIMESTAMP,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create文件管理表
CREATE TABLE IF NOT EXISTS sys_file (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
file_name VARCHAR(255) NOT NULL,
file_path VARCHAR(500) NOT NULL,
file_size BIGINT,
file_type VARCHAR(100),
file_extension VARCHAR(10),
storage_type VARCHAR(50),
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create索引
CREATE INDEX IF NOT EXISTS idx_user_role_user_id ON user_role(user_id);
CREATE INDEX IF NOT EXISTS idx_user_role_role_id ON user_role(role_id);
CREATE INDEX IF NOT EXISTS idx_sys_menu_parent_id ON sys_menu(parent_id);
CREATE INDEX IF NOT EXISTS idx_sys_dict_type ON sys_dict_data(dict_type);
CREATE INDEX IF NOT EXISTS idx_sys_login_log_username ON sys_login_log(username);
CREATE INDEX IF NOT EXISTS idx_operation_log_username ON operation_log(username);
@@ -0,0 +1,32 @@
package cn.novalon.gym.manage.app.config;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(MockitoExtension.class)
class MultipartConfigTest {
private MultipartConfig multipartConfig;
@BeforeEach
void setUp() {
multipartConfig = new MultipartConfig();
}
@Test
void testMultipartConfig() {
assertThat(multipartConfig).isNotNull();
}
@Test
void testMultipartHttpMessageReader() {
MultipartHttpMessageReader reader = multipartConfig.multipartHttpMessageReader();
assertThat(reader).isNotNull();
}
}
@@ -0,0 +1,50 @@
package cn.novalon.gym.manage.app.config;
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import java.lang.reflect.Field;
import java.time.Duration;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(MockitoExtension.class)
class RateLimitConfigTest {
@Test
void testRateLimiterRegistry() throws Exception {
RateLimitConfig rateLimitConfig = new RateLimitConfig();
setField(rateLimitConfig, "limitForPeriod", 100);
setField(rateLimitConfig, "limitRefreshPeriod", Duration.ofSeconds(1));
setField(rateLimitConfig, "timeoutDuration", Duration.ZERO);
RateLimiterRegistry registry = rateLimitConfig.rateLimiterRegistry();
assertThat(registry).isNotNull();
}
@Test
void testApiRateLimiter() throws Exception {
RateLimitConfig rateLimitConfig = new RateLimitConfig();
setField(rateLimitConfig, "limitForPeriod", 100);
setField(rateLimitConfig, "limitRefreshPeriod", Duration.ofSeconds(1));
setField(rateLimitConfig, "timeoutDuration", Duration.ZERO);
RateLimiterRegistry registry = rateLimitConfig.rateLimiterRegistry();
RateLimiter rateLimiter = rateLimitConfig.apiRateLimiter(registry);
assertThat(rateLimiter).isNotNull();
assertThat(rateLimiter.getName()).isEqualTo("apiRateLimiter");
}
private void setField(Object target, String fieldName, Object value) throws Exception {
Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
}
}
@@ -0,0 +1,68 @@
package cn.novalon.gym.manage.app.integration;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.test.context.ActiveProfiles;
import reactor.test.StepVerifier;
import java.time.Duration;
/**
* 数据库初始化验证测试
*
* 注意:此测试需要完整的数据库初始化,暂时禁用。
* TODO: 修复数据库初始化问题
*
* @author 张翔
* @date 2026-04-03
*/
@Disabled("暂时禁用:数据库初始化问题需要修复")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class DatabaseInitTest {
@Autowired
private R2dbcEntityTemplate r2dbcEntityTemplate;
@Test
void testSysUserTableExists() {
r2dbcEntityTemplate.getDatabaseClient()
.sql("SELECT COUNT(*) FROM sys_user")
.fetch()
.one()
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
}
@Test
void testOperationLogTableExists() {
r2dbcEntityTemplate.getDatabaseClient()
.sql("SELECT COUNT(*) FROM operation_log")
.fetch()
.one()
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
}
@Test
void testAllTablesCreated() {
r2dbcEntityTemplate.getDatabaseClient()
.sql("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'PUBLIC'")
.fetch()
.all()
.map(row -> row.get("TABLE_NAME"))
.collectList()
.as(StepVerifier::create)
.assertNext(tables -> {
System.out.println("Created tables: " + tables);
assert tables.contains("SYS_USER") : "SYS_USER table not found";
assert tables.contains("OPERATION_LOG") : "OPERATION_LOG table not found";
})
.verifyComplete();
}
}
@@ -0,0 +1,58 @@
package cn.novalon.gym.manage.app.integration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.test.context.ActiveProfiles;
import reactor.test.StepVerifier;
/**
* 手动创建表测试
*
* @author 张翔
* @date 2026-04-03
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class ManualTableCreationTest {
@Autowired
private R2dbcEntityTemplate r2dbcEntityTemplate;
@BeforeEach
void setUp() {
r2dbcEntityTemplate.getDatabaseClient()
.sql("CREATE TABLE IF NOT EXISTS operation_log (" +
"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
"username VARCHAR(50), " +
"operation VARCHAR(100), " +
"method VARCHAR(200), " +
"params TEXT, " +
"result TEXT, " +
"ip VARCHAR(50), " +
"duration BIGINT, " +
"status VARCHAR(1) DEFAULT '0', " +
"error_msg TEXT, " +
"create_by VARCHAR(50), " +
"update_by VARCHAR(50), " +
"created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " +
"updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " +
"deleted_at TIMESTAMP)")
.then()
.as(StepVerifier::create)
.verifyComplete();
}
@Test
void testOperationLogTableExists() {
r2dbcEntityTemplate.getDatabaseClient()
.sql("SELECT COUNT(*) FROM operation_log")
.fetch()
.one()
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
}
}
@@ -0,0 +1,70 @@
package cn.novalon.gym.manage.app.integration;
import cn.novalon.gym.manage.app.ManageApplication;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.reactive.server.WebTestClient;
/**
* 操作日志导出功能集成测试
*
* 注意:此测试存在超时问题,暂时禁用。
* TODO: 修复Excel导出的超时问题
*
* @author 张翔
* @date 2026-04-03
*/
@Disabled("暂时禁用:Excel导出功能存在超时问题,需要优化")
@SpringBootTest(
classes = ManageApplication.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
@ActiveProfiles("test")
class OperationLogExportIntegrationTest {
@Autowired
private WebTestClient webTestClient;
@Test
@WithMockUser(username = "admin", roles = {"ADMIN"})
void testExportOperationLogs_ShouldReturnExcelFile() {
webTestClient.get()
.uri("/api/logs/operation/export")
.accept(MediaType.APPLICATION_OCTET_STREAM)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_OCTET_STREAM)
.expectHeader().valueMatches("Content-Disposition", "attachment; filename=\"operation_logs_.*\\.xlsx\"")
.expectBody(byte[].class)
.value(bytes -> {
assert bytes != null;
assert bytes.length > 0;
assert bytes[0] == 0x50;
assert bytes[1] == 0x4B;
});
}
@Test
@WithMockUser(username = "admin", roles = {"ADMIN"})
void testExportOperationLogsWithKeyword_ShouldReturnFilteredExcel() {
webTestClient.get()
.uri(uriBuilder -> uriBuilder
.path("/api/logs/operation/export")
.queryParam("keyword", "test")
.build())
.accept(MediaType.APPLICATION_OCTET_STREAM)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_OCTET_STREAM)
.expectBody(byte[].class)
.value(bytes -> {
assert bytes != null;
assert bytes.length > 0;
});
}
}
@@ -0,0 +1,161 @@
package cn.novalon.gym.manage.app.integration;
import cn.novalon.gym.manage.sys.core.domain.OperationLog;
import cn.novalon.gym.manage.sys.core.service.IOperationLogService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.*;
/**
* 操作日志集成测试
*
* 注意:此测试需要完整的Spring上下文,暂时禁用。
* TODO: 优化集成测试配置
*
* @author 张翔
* @date 2026-04-03
*/
@Disabled("暂时禁用:集成测试配置需要优化")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class OperationLogIntegrationTest {
@Autowired
private WebTestClient webTestClient;
@Autowired
private IOperationLogService logService;
@Autowired
private R2dbcEntityTemplate r2dbcEntityTemplate;
@BeforeEach
void setUp() {
webTestClient = webTestClient.mutate()
.responseTimeout(Duration.ofSeconds(10))
.build();
r2dbcEntityTemplate.getDatabaseClient()
.sql("CREATE TABLE IF NOT EXISTS operation_log (" +
"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
"username VARCHAR(50), " +
"operation VARCHAR(100), " +
"method VARCHAR(200), " +
"params TEXT, " +
"result TEXT, " +
"ip VARCHAR(50), " +
"duration BIGINT, " +
"status VARCHAR(1) DEFAULT '0', " +
"error_msg TEXT, " +
"create_by VARCHAR(50), " +
"update_by VARCHAR(50), " +
"created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " +
"updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " +
"deleted_at TIMESTAMP)")
.then()
.as(StepVerifier::create)
.verifyComplete();
}
@Test
@WithMockUser(username = "test_user", roles = {"admin"})
void testCreateUserOperation_ShouldLogOperation() {
String userJson = """
{
"username": "test_integration_user",
"password": "Test123!@#",
"email": "test@example.com",
"phone": "13900139000",
"nickname": "集成测试用户"
}
""";
webTestClient.post()
.uri("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(userJson)
.exchange()
.expectStatus().isCreated()
.expectBody()
.jsonPath("$.id").exists()
.jsonPath("$.username").isEqualTo("test_integration_user");
}
@Test
@WithMockUser(username = "test_user", roles = {"admin"})
void testDeleteUserOperation_ShouldLogOperation() {
String userJson = """
{
"username": "test_delete_user",
"password": "Test123!@#",
"email": "delete@example.com",
"phone": "13900139001",
"nickname": "待删除用户"
}
""";
webTestClient.post()
.uri("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(userJson)
.exchange()
.expectStatus().isCreated()
.expectBody()
.jsonPath("$.id").value(id -> {
Long userId = Long.valueOf(id.toString());
webTestClient.delete()
.uri("/api/users/{id}", userId)
.exchange()
.expectStatus().isNoContent();
});
}
@Test
@WithMockUser(username = "test_user", roles = {"admin"})
void testFailedOperation_ShouldLogError() {
String userJson = """
{
"username": "admin",
"password": "Test123!@#",
"email": "duplicate@example.com",
"phone": "13900139002",
"nickname": "重复用户"
}
""";
webTestClient.post()
.uri("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(userJson)
.exchange()
.expectStatus().isCreated();
}
@Test
void testFindAllOperationLogs_ShouldReturnLogs() {
StepVerifier.create(logService.findAll().take(5))
.expectNextCount(0)
.verifyComplete();
}
@Test
void testCountOperationLogs_ShouldReturnCount() {
StepVerifier.create(logService.count())
.expectNextCount(1)
.verifyComplete();
}
}
@@ -0,0 +1,224 @@
package cn.novalon.gym.manage.app.integration;
import cn.novalon.gym.manage.common.util.StatusConstants;
import cn.novalon.gym.manage.sys.core.domain.SysUser;
import cn.novalon.gym.manage.sys.core.domain.SysRole;
import cn.novalon.gym.manage.sys.core.domain.UserRole;
import cn.novalon.gym.manage.sys.core.repository.ISysUserRepository;
import cn.novalon.gym.manage.sys.core.repository.ISysRoleRepository;
import cn.novalon.gym.manage.sys.core.repository.IUserRoleRepository;
import cn.novalon.gym.manage.sys.core.service.impl.SysUserService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.ActiveProfiles;
import reactor.test.StepVerifier;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.*;
/**
* 用户服务集成测试
*
* 使用PostgreSQL数据库进行集成测试
*
* 注意:此测试需要完整的Spring上下文,暂时禁用。
* TODO: 优化集成测试配置
*
* @author 张翔
* @date 2026-04-02
*/
@Disabled("暂时禁用:集成测试配置需要优化")
@SpringBootTest
@ActiveProfiles("test")
class SysUserServiceIntegrationTest {
@Autowired
private ISysUserRepository userRepository;
@Autowired
private ISysRoleRepository roleRepository;
@Autowired
private IUserRoleRepository userRoleRepository;
@Autowired
private R2dbcEntityTemplate r2dbcEntityTemplate;
@Autowired
private SysUserService userService;
@Autowired
private PasswordEncoder passwordEncoder;
@BeforeEach
void setUp() {
r2dbcEntityTemplate.delete(SysUser.class).all().block();
r2dbcEntityTemplate.delete(SysRole.class).all().block();
r2dbcEntityTemplate.delete(UserRole.class).all().block();
}
@Test
void testCreateAndFindUser() {
SysUser user = new SysUser();
user.setUsername("testuser");
user.setPassword("password123");
user.setEmail("test@example.com");
user.setNickname("Test User");
user.setPhone("13800138000");
StepVerifier.create(userService.createUser(user))
.expectNextMatches(createdUser -> {
assertNotNull(createdUser.getId());
assertEquals("testuser", createdUser.getUsername());
assertEquals("test@example.com", createdUser.getEmail());
assertTrue(createdUser.getPassword().startsWith("$2"));
assertEquals(StatusConstants.ENABLED, createdUser.getStatus());
return true;
})
.verifyComplete();
StepVerifier.create(userService.findByUsername("testuser"))
.expectNextMatches(foundUser -> {
assertEquals("testuser", foundUser.getUsername());
assertEquals("test@example.com", foundUser.getEmail());
return true;
})
.verifyComplete();
}
@Test
void testUpdateUser() {
SysUser user = new SysUser();
user.setUsername("updateuser");
user.setPassword("password123");
user.setEmail("update@example.com");
SysUser createdUser = userService.createUser(user).block();
assertNotNull(createdUser);
createdUser.setEmail("updated@example.com");
createdUser.setNickname("Updated User");
StepVerifier.create(userService.updateUser(createdUser))
.expectNextMatches(updatedUser -> {
assertEquals("updated@example.com", updatedUser.getEmail());
assertEquals("Updated User", updatedUser.getNickname());
return true;
})
.verifyComplete();
}
@Test
void testDeleteUser() {
SysUser user = new SysUser();
user.setUsername("deleteuser");
user.setPassword("password123");
user.setEmail("delete@example.com");
SysUser createdUser = userService.createUser(user).block();
assertNotNull(createdUser);
StepVerifier.create(userService.deleteUser(createdUser.getId()))
.verifyComplete();
StepVerifier.create(userService.findById(createdUser.getId()))
.verifyComplete();
}
@Test
void testChangePassword() {
SysUser user = new SysUser();
user.setUsername("pwduser");
user.setPassword("oldPassword");
user.setEmail("pwd@example.com");
SysUser createdUser = userService.createUser(user).block();
assertNotNull(createdUser);
StepVerifier.create(userService.changePassword(createdUser.getId(), "oldPassword", "newPassword"))
.expectNextMatches(updatedUser -> {
assertNotEquals(createdUser.getPassword(), updatedUser.getPassword());
assertTrue(passwordEncoder.matches("newPassword", updatedUser.getPassword()));
return true;
})
.verifyComplete();
}
@Test
void testAssignRolesToUser() {
SysRole role1 = new SysRole();
role1.setRoleName("Test Role 1");
role1.setRoleKey("test_role_1");
role1.setStatus(1);
SysRole role2 = new SysRole();
role2.setRoleName("Test Role 2");
role2.setRoleKey("test_role_2");
role2.setStatus(1);
SysRole createdRole1 = roleRepository.save(role1).block();
SysRole createdRole2 = roleRepository.save(role2).block();
assertNotNull(createdRole1);
assertNotNull(createdRole2);
SysUser user = new SysUser();
user.setUsername("roleuser");
user.setPassword("password123");
user.setEmail("role@example.com");
SysUser createdUser = userService.createUser(user).block();
assertNotNull(createdUser);
StepVerifier.create(userService.assignRolesToUser(createdUser.getId(),
Arrays.asList(createdRole1.getId(), createdRole2.getId())))
.verifyComplete();
StepVerifier.create(userRoleRepository.findByUserId(createdUser.getId()).collectList())
.expectNextMatches(userRoles -> {
assertEquals(2, userRoles.size());
return true;
})
.verifyComplete();
}
@Test
void testFindAllUsers() {
for (int i = 1; i <= 3; i++) {
SysUser user = new SysUser();
user.setUsername("user" + i);
user.setPassword("password" + i);
user.setEmail("user" + i + "@example.com");
userService.createUser(user).block();
}
StepVerifier.create(userService.findAll(false).collectList())
.expectNextMatches(users -> {
assertEquals(3, users.size());
return true;
})
.verifyComplete();
}
@Test
void testExistsByUsername() {
SysUser user = new SysUser();
user.setUsername("existinguser");
user.setPassword("password123");
user.setEmail("existing@example.com");
userService.createUser(user).block();
StepVerifier.create(userService.existsByUsername("existinguser"))
.expectNext(true)
.verifyComplete();
StepVerifier.create(userService.existsByUsername("nonexistinguser"))
.expectNext(false)
.verifyComplete();
}
}
@@ -0,0 +1,31 @@
spring:
r2dbc:
url: r2dbc:postgresql://localhost:55432/manage_system
username: novalon
password: novalon123
pool:
enabled: true
initial-size: 2
max-size: 10
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
validate-on-migrate: true
sql:
init:
mode: never
security:
enabled: false
jwt:
secret: test-secret-key-for-integration-testing
expiration: 86400000
logging:
level:
cn.novalon.manage: DEBUG
org.springframework.r2dbc: DEBUG
@@ -0,0 +1,80 @@
-- H2数据库测试数据
-- 用于测试环境
-- 插入测试角色
INSERT INTO sys_role (id, role_name, role_key, role_sort, status, create_by, update_by)
VALUES
(1, '超级管理员', 'admin', 1, 1, 'system', 'system'),
(2, '测试管理员', 'test_admin', 2, 1, 'system', 'system'),
(3, '普通用户', 'normal_user', 3, 1, 'system', 'system'),
(4, '访客', 'guest', 4, 1, 'system', 'system');
-- 插入测试用户
-- BCrypt哈希值对应明文密码: Test@123
INSERT INTO sys_user (id, username, password, email, phone, nickname, status, create_by, update_by)
VALUES
(1, 'admin', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'admin@novalon.com', '13800138000', '超级管理员', 1, 'system', 'system'),
(2, 'testadmin', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'testadmin@novalon.com', '13800138001', '测试管理员', 1, 'system', 'system'),
(3, 'normaluser', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'normaluser@novalon.com', '13800138002', '普通用户', 1, 'system', 'system'),
(4, 'guestuser', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'guestuser@novalon.com', '13800138003', '访客用户', 1, 'system', 'system'),
(5, 'disableduser', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'disableduser@novalon.com', '13800138004', '禁用用户', 0, 'system', 'system');
-- 为用户分配角色
INSERT INTO user_role (user_id, role_id, created_by)
VALUES
(1, 1, 'system'),
(2, 2, 'system'),
(3, 3, 'system'),
(4, 4, 'system');
-- 插入测试菜单
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, created_by, updated_by)
VALUES
(1, '系统管理', 0, 1, '/system', 'Layout', 'M', '1', '1', '', 'system', 'system', 'system'),
(2, '用户管理', 1, 1, 'user', 'system/user/index', 'C', '1', '1', 'system:user:list', 'user', 'system', 'system'),
(3, '角色管理', 1, 2, 'role', 'system/role/index', 'C', '1', '1', 'system:role:list', 'role', 'system', 'system'),
(4, '菜单管理', 1, 3, 'menu', 'system/menu/index', 'C', '1', '1', 'system:menu:list', 'menu', 'system', 'system'),
(5, '测试菜单', 0, 99, '/test', 'Layout', 'M', '1', '1', '', 'test', 'system', 'system'),
(6, '用户测试', 5, 1, 'user-test', 'system/user-test/index', 'C', '1', '1', 'system:user:test', 'user', 'system', 'system');
-- 插入测试权限
INSERT INTO sys_permission (id, permission_name, permission_key, permission_type, parent_id, status, created_by, updated_by)
VALUES
(1, '系统管理', 'system:manage', 'menu', 0, 1, 'system', 'system'),
(2, '用户管理', 'system:user:manage', 'menu', 1, 1, 'system', 'system'),
(3, '用户查询', 'system:user:list', 'button', 2, 1, 'system', 'system'),
(4, '用户新增', 'system:user:add', 'button', 2, 1, 'system', 'system'),
(5, '用户编辑', 'system:user:edit', 'button', 2, 1, 'system', 'system'),
(6, '用户删除', 'system:user:delete', 'button', 2, 1, 'system', 'system'),
(7, '测试权限', 'test:permission', 'menu', 0, 1, 'system', 'system'),
(8, '用户测试权限', 'system:user:test', 'button', 7, 1, 'system', 'system');
-- 为角色分配权限
INSERT INTO sys_role_permission (role_id, permission_id, created_by, updated_by)
SELECT 1, id, 'system', 'system' FROM sys_permission
UNION ALL
SELECT 2, id, 'system', 'system' FROM sys_permission WHERE id IN (7, 8);
-- 插入字典类型
INSERT INTO sys_dict_type (id, dict_name, dict_type, status, remark, created_by, updated_by)
VALUES
(1, '用户状态', 'user_status', '0', '用户状态列表', 'system', 'system'),
(2, '菜单状态', 'menu_status', '0', '菜单状态列表', 'system', 'system'),
(3, '角色状态', 'role_status', '0', '角色状态列表', 'system', 'system');
-- 插入字典数据
INSERT INTO sys_dict_data (id, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, created_by, updated_by)
VALUES
(1, 1, '正常', '1', 'user_status', '', 'primary', 'Y', '0', 'system', 'system'),
(2, 2, '停用', '0', 'user_status', '', 'danger', 'N', '0', 'system', 'system'),
(3, 1, '正常', '0', 'menu_status', '', 'primary', 'Y', '0', 'system', 'system'),
(4, 2, '停用', '1', 'menu_status', '', 'danger', 'N', '0', 'system', 'system'),
(5, 1, '正常', '0', 'role_status', '', 'primary', 'Y', '0', 'system', 'system'),
(6, 2, '停用', '1', 'role_status', '', 'danger', 'N', '0', 'system', 'system');
-- 插入系统配置
INSERT INTO sys_config (id, config_name, config_key, config_value, config_type, remark, created_by, updated_by)
VALUES
(1, '用户管理-用户初始密码', 'sys.user.initPassword', '123456', 'Y', '初始化用户密码', 'system', 'system'),
(2, '主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue', 'Y', '默认皮肤', 'system', 'system'),
(3, '用户自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', '是否开启验证码功能', 'system', 'system');
@@ -0,0 +1,76 @@
-- H2数据库Schema for Integration Testing
-- 创建用户表
CREATE TABLE IF NOT EXISTS sys_user (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
email VARCHAR(100),
phone VARCHAR(20),
nickname VARCHAR(100),
role_id BIGINT,
status INTEGER DEFAULT 1,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 创建角色表
CREATE TABLE IF NOT EXISTS sys_role (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
role_name VARCHAR(100) NOT NULL,
role_key VARCHAR(100) NOT NULL UNIQUE,
role_sort INTEGER DEFAULT 0,
status INTEGER DEFAULT 1,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 创建用户角色关联表
CREATE TABLE IF NOT EXISTS user_role (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(50),
CONSTRAINT fk_user_role_user FOREIGN KEY (user_id) REFERENCES sys_user(id) ON DELETE CASCADE,
CONSTRAINT fk_user_role_role FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE,
CONSTRAINT uk_user_role UNIQUE (user_id, role_id)
);
-- 创建索引
CREATE INDEX IF NOT EXISTS idx_user_role_user_id ON user_role(user_id);
CREATE INDEX IF NOT EXISTS idx_user_role_role_id ON user_role(role_id);
-- 创建审计日志表
CREATE TABLE IF NOT EXISTS audit_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
entity_type VARCHAR(100) NOT NULL,
entity_id BIGINT,
operation_type VARCHAR(20) NOT NULL,
operator VARCHAR(100),
operation_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
before_data CLOB,
after_data CLOB,
changed_fields CLOB,
ip_address VARCHAR(50),
user_agent CLOB,
description CLOB,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 创建审计日志索引
CREATE INDEX IF NOT EXISTS idx_audit_log_entity_type ON audit_log(entity_type);
CREATE INDEX IF NOT EXISTS idx_audit_log_entity_id ON audit_log(entity_id);
CREATE INDEX IF NOT EXISTS idx_audit_log_operation_type ON audit_log(operation_type);
CREATE INDEX IF NOT EXISTS idx_audit_log_operator ON audit_log(operator);
CREATE INDEX IF NOT EXISTS idx_audit_log_operation_time ON audit_log(operation_time);
CREATE INDEX IF NOT EXISTS idx_audit_log_entity ON audit_log(entity_type, entity_id);