用于在业务流程中主动抛出可预期的错误,并携带业务错误码。
+ */ +package com.hertz.common.exception; + +public class BusinessException extends RuntimeException { + private final int code; + + public BusinessException(int code, String message) { + super(message); + this.code = code; + } + + public int getCode() { + return code; + } +} + diff --git a/src/main/java/com/hertz/common/exception/GlobalExceptionHandler.java b/src/main/java/com/hertz/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..f79aebd --- /dev/null +++ b/src/main/java/com/hertz/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,74 @@ +/** + * 全局异常处理器。 + * + *将各类异常统一转换为标准的接口响应结构,并设置对应的 HTTP 状态码。
+ */ +package com.hertz.common.exception; + +import com.hertz.common.api.ApiResponse; +import jakarta.validation.ConstraintViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.resource.NoResourceFoundException; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(BusinessException.class) + public ResponseEntity记录每次 HTTP 请求的方法、URI、响应状态码与耗时。
+ */ +package com.hertz.common.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@Slf4j +public class RequestLogFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) + throws ServletException, IOException { + + long startTime = System.currentTimeMillis(); + String method = request.getMethod(); + String uri = request.getRequestURI(); + + try { + filterChain.doFilter(request, response); + } finally { + long endTime = System.currentTimeMillis(); + long timeTaken = endTime - startTime; + int status = response.getStatus(); + + log.info("Request: [{}] {} | Status: {} | Time: {}ms", method, uri, status, timeTaken); + } + } +} diff --git a/src/main/java/com/hertz/config/AppPathResolver.java b/src/main/java/com/hertz/config/AppPathResolver.java new file mode 100644 index 0000000..0a5f451 --- /dev/null +++ b/src/main/java/com/hertz/config/AppPathResolver.java @@ -0,0 +1,31 @@ +/** + * 应用路径解析器。 + * + *用于将配置中的相对路径解析为基于应用工作目录的绝对路径。
+ */ +package com.hertz.config; + +import java.io.File; +import org.springframework.stereotype.Service; + +@Service +public class AppPathResolver { + + private final File baseDir; + + public AppPathResolver() { + String userDir = System.getProperty("user.dir"); + this.baseDir = userDir == null || userDir.isBlank() ? new File(".") : new File(userDir); + } + + public String resolve(String path) { + if (path == null || path.isBlank()) { + return path; + } + File file = new File(path); + if (file.isAbsolute()) { + return file.getAbsolutePath(); + } + return new File(baseDir, path).getAbsolutePath(); + } +} diff --git a/src/main/java/com/hertz/config/AppProperties.java b/src/main/java/com/hertz/config/AppProperties.java new file mode 100644 index 0000000..60bdfbf --- /dev/null +++ b/src/main/java/com/hertz/config/AppProperties.java @@ -0,0 +1,32 @@ +/** + * 应用自定义配置属性。 + * + *对应 application.yml 中以 {@code app} 为前缀的配置项。
+ */ +package com.hertz.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "app") +public class AppProperties { + + private Jwt jwt = new Jwt(); + private Upload upload = new Upload(); + + @Data + public static class Jwt { + private String secret; + private long expireSeconds; + } + + @Data + public static class Upload { + private String rootPath; + private String avatarPath; + private String knowledgePath = "knowledge/"; + } +} diff --git a/src/main/java/com/hertz/config/MybatisPlusConfig.java b/src/main/java/com/hertz/config/MybatisPlusConfig.java new file mode 100644 index 0000000..02af2c5 --- /dev/null +++ b/src/main/java/com/hertz/config/MybatisPlusConfig.java @@ -0,0 +1,22 @@ +/** + * MyBatis-Plus 配置。 + * + *主要用于注册分页等拦截器。
+ */ +package com.hertz.config; + +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MybatisPlusConfig { + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + var interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); + return interceptor; + } +} + diff --git a/src/main/java/com/hertz/config/WebMvcConfig.java b/src/main/java/com/hertz/config/WebMvcConfig.java new file mode 100644 index 0000000..d4d179e --- /dev/null +++ b/src/main/java/com/hertz/config/WebMvcConfig.java @@ -0,0 +1,33 @@ +/** + * Web MVC 配置。 + * + *用于配置静态资源映射,例如上传文件的访问路径。
+ */ +package com.hertz.config; + +import java.io.File; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.NonNull; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Value("${app.upload.root-path}") + private String uploadRootPath; + + private final AppPathResolver pathResolver; + + public WebMvcConfig(AppPathResolver pathResolver) { + this.pathResolver = pathResolver; + } + + @Override + public void addResourceHandlers(@NonNull ResourceHandlerRegistry registry) { + String absoluteUploadRootPath = new File(pathResolver.resolve(uploadRootPath)).getAbsolutePath().replace("\\", "/"); + registry.addResourceHandler("/uploads/**") + .addResourceLocations("file:" + absoluteUploadRootPath + "/"); + } +} diff --git a/src/main/java/com/hertz/modules/ai/config/VectorStoreConfig.java b/src/main/java/com/hertz/modules/ai/config/VectorStoreConfig.java new file mode 100644 index 0000000..8565b1d --- /dev/null +++ b/src/main/java/com/hertz/modules/ai/config/VectorStoreConfig.java @@ -0,0 +1,27 @@ +package com.hertz.modules.ai.config; + +import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.vectorstore.SimpleVectorStore; +import org.springframework.ai.vectorstore.VectorStore; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.File; + +@Configuration +public class VectorStoreConfig { + + @Value("${spring.ai.vectorstore.simple.store.path:vector-store.json}") + private String vectorStorePath; + + @Bean + public VectorStore vectorStore(EmbeddingModel embeddingModel) { + SimpleVectorStore simpleVectorStore = SimpleVectorStore.builder(embeddingModel).build(); + File file = new File(vectorStorePath); + if (file.exists() && file.isFile()) { + simpleVectorStore.load(file); + } + return simpleVectorStore; + } +} diff --git a/src/main/java/com/hertz/modules/ai/controller/AiController.java b/src/main/java/com/hertz/modules/ai/controller/AiController.java new file mode 100644 index 0000000..60810c6 --- /dev/null +++ b/src/main/java/com/hertz/modules/ai/controller/AiController.java @@ -0,0 +1,117 @@ +package com.hertz.modules.ai.controller; + +import com.hertz.modules.ai.dto.ChatRequest; +import com.hertz.modules.ai.entity.Conversation; +import com.hertz.modules.ai.entity.Message; +import com.hertz.modules.ai.service.AiService; +import com.hertz.modules.ai.service.ConversationService; +import com.hertz.common.api.ApiResponse; +import com.hertz.common.exception.BusinessException; +import com.hertz.security.SecurityUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Flux; + +import java.util.List; + +@RestController +@RequestMapping("/api/ai") +@RequiredArgsConstructor +public class AiController { + + private final AiService aiService; + private final ConversationService conversationService; + + @PostMapping("/chat") + public ApiResponse用于根据用户名加载用户信息,供认证与权限体系使用。
+ */ +package com.hertz.security; + +import com.hertz.modules.system.service.UserService; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + + private final UserService userService; + + public CustomUserDetailsService(UserService userService) { + this.userService = userService; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + var user = userService.findByUsername(username); + if (user == null) { + throw new UsernameNotFoundException("User not found: " + username); + } + // We are using JWT for auth, so this is mostly to satisfy Spring Security's default config + // or if we wanted to support Basic Auth/Form Login alongside JWT. + // For now, we return a minimal UserDetails implementation. + return org.springframework.security.core.userdetails.User + .withUsername(user.getUsername()) + .password(user.getPassword()) + .roles("USER") // Default role, actual authorities are loaded in JwtAuthFilter + .build(); + } +} diff --git a/src/main/java/com/hertz/security/JwtAuthFilter.java b/src/main/java/com/hertz/security/JwtAuthFilter.java new file mode 100644 index 0000000..de58917 --- /dev/null +++ b/src/main/java/com/hertz/security/JwtAuthFilter.java @@ -0,0 +1,67 @@ +/** + * JWT 认证过滤器。 + * + *从请求头 Authorization: Bearer token 中解析 JWT,并将认证信息写入 SecurityContext。
+ */ +package com.hertz.security; + +import com.hertz.modules.system.service.AuthzService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.stream.Collectors; +import org.springframework.http.HttpHeaders; +import org.springframework.lang.NonNull; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +@Component +public class JwtAuthFilter extends OncePerRequestFilter { + private final JwtService jwtService; + private final AuthzService authzService; + + public JwtAuthFilter(JwtService jwtService, AuthzService authzService) { + this.jwtService = jwtService; + this.authzService = authzService; + } + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain + ) throws ServletException, IOException { + var header = request.getHeader(HttpHeaders.AUTHORIZATION); + if (header == null || !header.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + var token = header.substring("Bearer ".length()).trim(); + try { + var claims = jwtService.parse(token); + var userId = claims.get("uid", Number.class).longValue(); + var username = claims.getSubject(); + var authorities = authzService.loadAuthorities(userId).stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + var auth = new UsernamePasswordAuthenticationToken(username, null, authorities); + auth.setDetails(userId); + SecurityContextHolder.getContext().setAuthentication(auth); + // System.out.println("DEBUG: JWT Auth Success for user: " + username); + } catch (Exception e) { + System.err.println("DEBUG: JWT Auth Failed: " + e.getMessage()); + e.printStackTrace(); + logger.error("JWT Authentication failed: " + e.getMessage(), e); + SecurityContextHolder.clearContext(); + } + + filterChain.doFilter(request, response); + } +} + diff --git a/src/main/java/com/hertz/security/JwtService.java b/src/main/java/com/hertz/security/JwtService.java new file mode 100644 index 0000000..4ed0ace --- /dev/null +++ b/src/main/java/com/hertz/security/JwtService.java @@ -0,0 +1,53 @@ +/** + * JWT 工具服务。 + * + *负责生成与解析 JWT,用于无状态认证。
+ */ +package com.hertz.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Date; +import javax.crypto.SecretKey; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class JwtService { + private final SecretKey key; + private final long expireSeconds; + + public JwtService( + @Value("${app.jwt.secret}") String secret, + @Value("${app.jwt.expire-seconds}") long expireSeconds + ) { + if (secret == null || secret.getBytes(StandardCharsets.UTF_8).length < 32) { + throw new IllegalArgumentException("app.jwt.secret 至少 32 字节"); + } + this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + this.expireSeconds = expireSeconds; + } + + public String createToken(long userId, String username) { + var now = Instant.now(); + return Jwts.builder() + .subject(username) + .claim("uid", userId) + .issuedAt(Date.from(now)) + .expiration(Date.from(now.plusSeconds(expireSeconds))) + .signWith(key) + .compact(); + } + + public Claims parse(String token) { + return Jwts.parser() + .verifyWith(key) + .build() + .parseSignedClaims(token) + .getPayload(); + } +} + diff --git a/src/main/java/com/hertz/security/SecurityConfig.java b/src/main/java/com/hertz/security/SecurityConfig.java new file mode 100644 index 0000000..c96395b --- /dev/null +++ b/src/main/java/com/hertz/security/SecurityConfig.java @@ -0,0 +1,65 @@ +/** + * Spring Security 配置。 + * + *配置无状态会话、跨域策略、接口访问规则,并注册 JWT 认证过滤器。
+ */ +package com.hertz.security; + +import jakarta.servlet.DispatcherType; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +@Configuration +@EnableMethodSecurity +public class SecurityConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + var config = new CorsConfiguration(); + config.addAllowedOriginPattern("*"); + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + config.setAllowCredentials(true); + + var source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } + + @Bean + public SecurityFilterChain filterChain( + HttpSecurity http, + JwtAuthFilter jwtAuthFilter + ) throws Exception { + return http + .csrf(csrf -> csrf.disable()) + .cors(Customizer.withDefaults()) + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll() + .requestMatchers("/api/auth/**", "/error", "/uploads/**").permitAll() + .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() + .anyRequest().authenticated() + ) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) + .build(); + } +} + diff --git a/src/main/java/com/hertz/security/SecurityUtils.java b/src/main/java/com/hertz/security/SecurityUtils.java new file mode 100644 index 0000000..7bb4017 --- /dev/null +++ b/src/main/java/com/hertz/security/SecurityUtils.java @@ -0,0 +1,30 @@ +/** + * 安全上下文工具类。 + * + *用于从 Spring Security 上下文中获取当前登录用户信息。
+ */ +package com.hertz.security; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +public final class SecurityUtils { + private SecurityUtils() { + } + + public static Long getCurrentUserId() { + return getCurrentUserId(SecurityContextHolder.getContext().getAuthentication()); + } + + public static Long getCurrentUserId(Authentication authentication) { + if (authentication == null) { + return null; + } + var details = authentication.getDetails(); + if (details instanceof Number n) { + return n.longValue(); + } + return null; + } +} + diff --git a/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000..fc7075b --- /dev/null +++ b/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,10 @@ +{ + "properties": [ + { + "name": "spring.ai.vectorstore.simple.store.path", + "type": "java.lang.String", + "description": "SimpleVectorStore 持久化存储路径(可为目录或文件路径,项目内按需解析)。" + } + ] +} + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..909fb28 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,57 @@ +server: + port: 8088 + +spring: + application: + name: Hertz-Springboot + sql: + init: + mode: never # 禁用 SQL 初始化(不自动执行 schema.sql / data.sql)可选:always(默认)、never + servlet: + multipart: + max-file-size: 2MB + max-request-size: 10MB + ai: + ollama: + base-url: http://localhost:11434 + chat: + # 如果需要快速响应,建议切换为标准模型,如: llama3.1, qwen2.5:7b, deepseek-llm:7b + model: deepseek-llm:7b + options: + temperature: 0.7 + embedding: + model: nomic-embed-text + vectorstore: + simple: + store: + path: ./uploads/knowledge/vector_store + datasource: + url: jdbc:mysql://localhost:3306/hertz_springboot?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai + username: root + password: 123456 + driver-class-name: com.mysql.cj.jdbc.Driver + data: + redis: + host: localhost + port: 6379 + password: + database: 0 + +mybatis-plus: + configuration: + map-underscore-to-camel-case: true + +app: + jwt: + secret: change-me-to-a-long-random-string-change-me-to-a-long-random-string + expire-seconds: 86400 + upload: + root-path: ./uploads + avatar-path: avatar/ + knowledge-path: knowledge/ + +management: + endpoints: + web: + exposure: + include: health,info,metrics diff --git a/src/main/resources/schema/ai_schema.sql b/src/main/resources/schema/ai_schema.sql new file mode 100644 index 0000000..bc03fef --- /dev/null +++ b/src/main/resources/schema/ai_schema.sql @@ -0,0 +1,26 @@ + +-- ---------------------------- +-- Table structure for conversations +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `ai_conversations` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT '用户ID', + `title` varchar(255) NOT NULL COMMENT '对话标题', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对话记录表'; + +-- ---------------------------- +-- Table structure for messages +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `ai_messages` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `conversation_id` bigint NOT NULL COMMENT '所属对话ID', + `role` enum('user','assistant') NOT NULL COMMENT '消息角色', + `content` text NOT NULL COMMENT '消息内容', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_conversation_id` (`conversation_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对话消息表'; diff --git a/src/main/resources/schema/knowledge_schema.sql b/src/main/resources/schema/knowledge_schema.sql new file mode 100644 index 0000000..a249fe6 --- /dev/null +++ b/src/main/resources/schema/knowledge_schema.sql @@ -0,0 +1,26 @@ +CREATE TABLE IF NOT EXISTS knowledge_base ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', + name VARCHAR(255) NOT NULL COMMENT '知识库名称', + description TEXT COMMENT '知识库描述', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + create_by BIGINT COMMENT '创建人ID' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库'; + +CREATE TABLE IF NOT EXISTS knowledge_document ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', + kb_id BIGINT NOT NULL COMMENT '知识库ID', + original_name VARCHAR(512) NOT NULL COMMENT '原始文件名', + stored_name VARCHAR(512) NOT NULL COMMENT '存储文件名', + stored_path VARCHAR(1024) NOT NULL COMMENT '存储相对路径', + content_type VARCHAR(255) COMMENT '文件类型', + size_bytes BIGINT NOT NULL COMMENT '文件大小(字节)', + sha256 VARCHAR(64) COMMENT '文件SHA256摘要', + status VARCHAR(32) NOT NULL COMMENT '处理状态(PROCESSING/READY/FAILED)', + chunk_count INT DEFAULT 0 COMMENT '分片数量', + error_message VARCHAR(1024) COMMENT '失败原因', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + deleted TINYINT(1) DEFAULT 0 COMMENT '是否删除(软删除标记)', + deleted_time DATETIME COMMENT '删除时间', + KEY idx_kb_id (kb_id), + KEY idx_kb_deleted (kb_id, deleted) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库文档'; diff --git a/src/main/resources/schema/monitor_schema.sql b/src/main/resources/schema/monitor_schema.sql new file mode 100644 index 0000000..3e1807f --- /dev/null +++ b/src/main/resources/schema/monitor_schema.sql @@ -0,0 +1,14 @@ +-- ---------------------------- +-- Table structure for sys_monitor_log +-- ---------------------------- +DROP TABLE IF EXISTS `sys_monitor_log`; +CREATE TABLE `sys_monitor_log` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `cpu_usage` double NOT NULL COMMENT 'CPU使用率(%)', + `memory_usage` double NOT NULL COMMENT '内存使用率(%)', + `memory_total` bigint NOT NULL COMMENT '总内存(字节)', + `memory_used` bigint NOT NULL COMMENT '已用内存(字节)', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录时间', + PRIMARY KEY (`id`), + KEY `idx_created_at` (`created_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统监控日志表'; diff --git a/src/main/resources/schema/schema.sql b/src/main/resources/schema/schema.sql new file mode 100644 index 0000000..ea0531e --- /dev/null +++ b/src/main/resources/schema/schema.sql @@ -0,0 +1,107 @@ +-- ---------------------------- +-- Table structure for sys_user +-- ---------------------------- +DROP TABLE IF EXISTS `sys_user`; +CREATE TABLE `sys_user` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID', + `username` varchar(50) NOT NULL COMMENT '用户名', + `password` varchar(100) NOT NULL COMMENT '加密密码', + `nickname` varchar(50) NOT NULL COMMENT '用户昵称', + `avatar_path` varchar(255) DEFAULT NULL COMMENT '头像路径', + `phone` varchar(20) DEFAULT NULL COMMENT '手机号', + `email` varchar(100) DEFAULT NULL COMMENT '邮箱', + `gender` tinyint(1) DEFAULT '0' COMMENT '0-未知 1-男 2-女', + `status` tinyint DEFAULT '1' COMMENT '0-禁用 1-启用', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_username` (`username`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表'; + +-- ---------------------------- +-- Table structure for sys_role +-- ---------------------------- +DROP TABLE IF EXISTS `sys_role`; +CREATE TABLE `sys_role` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID', + `role_key` varchar(50) NOT NULL COMMENT '角色标识', + `role_name` varchar(50) NOT NULL COMMENT '角色名称', + `status` tinyint DEFAULT '1' COMMENT '0-禁用 1-启用', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_role_key` (`role_key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统角色表'; + +-- ---------------------------- +-- 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 DEFAULT '0' COMMENT '父菜单ID', + `type` varchar(10) NOT NULL COMMENT 'D-目录 M-菜单 B-按钮', + `name` varchar(50) NOT NULL COMMENT '菜单名称', + `path` varchar(200) DEFAULT NULL COMMENT '路由路径', + `component` varchar(200) DEFAULT NULL COMMENT '组件路径', + `perms` varchar(100) DEFAULT NULL COMMENT '权限标识', + `icon` varchar(100) DEFAULT NULL COMMENT '菜单图标', + `sort` int DEFAULT '0' COMMENT '排序', + `visible` tinyint DEFAULT '1' COMMENT '0-隐藏 1-显示', + `status` tinyint DEFAULT '1' COMMENT '0-禁用 1-启用', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统菜单表'; + +-- ---------------------------- +-- Table structure for sys_user_role +-- ---------------------------- +DROP TABLE IF EXISTS `sys_user_role`; +CREATE TABLE `sys_user_role` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT '用户ID', + `role_id` bigint NOT NULL COMMENT '角色ID', + PRIMARY KEY (`id`), + KEY `idx_user_role` (`user_id`,`role_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表'; + +-- ---------------------------- +-- Table structure for sys_role_menu +-- ---------------------------- +DROP TABLE IF EXISTS `sys_role_menu`; +CREATE TABLE `sys_role_menu` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `role_id` bigint NOT NULL COMMENT '角色ID', + `menu_id` bigint NOT NULL COMMENT '菜单ID', + PRIMARY KEY (`id`), + KEY `idx_role_menu` (`role_id`,`menu_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色菜单关联表'; + +-- ---------------------------- +-- Init Data +-- ---------------------------- +INSERT INTO `sys_role` (`id`, `role_key`, `role_name`) VALUES + (1, 'ADMIN', '管理员'); + +INSERT INTO `sys_menu` (`id`, `parent_id`, `type`, `name`, `path`, `component`, `perms`, `icon`, `sort`) VALUES + (1, 0, 'M', '仪表盘', '/admin/dashboard', 'admin/Dashboard', NULL, 'DataLine', 0), + (2, 0, 'D', '系统管理', '/admin/system', NULL, NULL, 'Setting', 10), + (3, 2, 'M', '用户管理', '/admin/system/user', 'admin/system/User', 'system:user:view', 'User', 0), + (4, 2, 'M', '角色管理', '/admin/system/role', 'admin/system/Role', 'system:role:view', 'Tickets', 1), + (5, 2, 'M', '菜单管理', '/admin/system/menu', 'admin/system/Menu', 'system:menu:view', 'Menu', 2); + +INSERT INTO `sys_user` (`id`, `username`, `password`, `nickname`, `phone`, `email`, `gender`, `status`) VALUES + (1, 'hertz', '$2a$10$Gker6.ggCxG3wfZ13rE/Eu7aDnB.DX2JmP6h6vct30RTtBr9.q5Pq', '管理员', '18888888888', 'hertz@hertz.com', 1, 1), + (2, 'demo', '$2a$10$PSIz9pWXAwXfB32HWSxTjeGhVi0bixsSKxzeX8YAdKnRRXPxJC3Xe', '普通用户', '13888888888', 'demo@hertz.com', 1, 1); + +INSERT INTO `sys_user_role` (`user_id`, `role_id`) VALUES + (1, 1); + +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES + (1, 1), + (1, 2), + (1, 3), + (1, 4), + (1, 5); \ No newline at end of file diff --git a/src/test/java/com/hertz/HertzApplicationTests.java b/src/test/java/com/hertz/HertzApplicationTests.java new file mode 100644 index 0000000..6274ae5 --- /dev/null +++ b/src/test/java/com/hertz/HertzApplicationTests.java @@ -0,0 +1,13 @@ +package com.hertz; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class HertzApplicationTests { + + @Test + void contextLoads() { + } +} + diff --git a/ui/.env.dev b/ui/.env.dev new file mode 100644 index 0000000..1cdc56d --- /dev/null +++ b/ui/.env.dev @@ -0,0 +1 @@ +VITE_API_BASE=http://localhost:8088 \ No newline at end of file diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000..c9e0db4 --- /dev/null +++ b/ui/index.html @@ -0,0 +1,13 @@ + + + + + + +
+ Hertz Admin
+
+ Hertz Admin
+
+ Hertz Admin
+ 高效、安全、专业的解决方案
+
+
+ Hertz Admin
+ 开始您的旅程,今天就注册
+欢迎回到 Hertz Admin,今天也是充满活力的一天!
+
+ 现代化全栈权限管理系统解决方案
++ Hertz Admin 是一个基于 Spring Boot 3 和 Vue 3 构建的前后端分离权限管理系统。 + 它集成了最新的技术栈,提供了一套完整的 RBAC(基于角色的访问控制)解决方案,支持动态菜单、系统监控、AI 对话等功能。 + 旨在帮助开发者快速搭建企业级后台管理系统。 +
+
+ 基于 Spring Boot 3 与 Vue 3 的
现代化全栈权限管理系统
+
+ 极简设计,高效开发。集成 RBAC 权限、动态路由、AI 助手与实时监控,
为您提供开箱即用的企业级中后台解决方案。
+
细粒度的基于角色的访问控制,支持菜单、按钮级别的权限管理,保障系统安全。
+根据用户角色与权限动态生成侧边栏菜单与路由,无需手动配置前端路由表。
+采用 Element Plus 组件库,深度定制 iOS 风格设计,提供极致的用户体验。
+集成大语言模型,支持 RAG 知识库问答,为您的应用注入人工智能的活力。
+内置服务器状态监控,实时掌握 CPU、内存、JVM 及磁盘使用情况。
+前后端分离架构,标准 RESTful API,完善的代码规范,助您快速构建应用。
+