commit 6df6ab856b3bb94313084a6aece5a16c7a1913e1
Author: pony <1356137040@qq.com>
Date: Tue Jan 20 15:28:01 2026 +0800
v1
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f8d850d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,56 @@
+# --- Common ---
+.DS_Store
+Thumbs.db
+Desktop.ini
+*.log
+*.tmp
+*.bak
+*.swp
+
+# --- IDEs & Editors ---
+.idea/
+.vscode/
+*.iml
+*.ipr
+*.iws
+.classpath
+.project
+.settings/
+.factorypath
+
+# --- Java / Maven (Backend) ---
+target/
+*.class
+*.jar
+*.war
+*.ear
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# --- Node / Vue (Frontend) ---
+node_modules/
+dist/
+dist-ssr/
+coverage/
+*.local
+.npm/
+# Logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# --- Application Specific ---
+# Uploaded files
+uploads/
+# Ignore local environment override files if any
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
diff --git a/hertz_springboot/pom.xml b/hertz_springboot/pom.xml
new file mode 100644
index 0000000..6c61e00
--- /dev/null
+++ b/hertz_springboot/pom.xml
@@ -0,0 +1,117 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.4.1
+
+
+
+ com.hertz
+ hertz-springboot-backend
+ 0.0.1-SNAPSHOT
+ hertz-springboot-backend
+ Hertz 权限管理系统后端
+
+
+ 21
+ 21
+ 3.5.8
+ 0.12.6
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+ com.baomidou
+ mybatis-plus-spring-boot3-starter
+ ${mybatis-plus.version}
+
+
+
+ com.mysql
+ mysql-connector-j
+ runtime
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ io.jsonwebtoken
+ jjwt-api
+ ${jjwt.version}
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ ${jjwt.version}
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ ${jjwt.version}
+ runtime
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-enforcer-plugin
+ 3.5.0
+
+
+ enforce-java
+
+ enforce
+
+
+
+
+ [21,)
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/hertz_springboot/src/main/java/com/hertz/HertzApplication.java b/hertz_springboot/src/main/java/com/hertz/HertzApplication.java
new file mode 100644
index 0000000..e1c2645
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/HertzApplication.java
@@ -0,0 +1,14 @@
+package com.hertz;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+@MapperScan("com.hertz.**.mapper")
+public class HertzApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(HertzApplication.class, args);
+ }
+}
diff --git a/hertz_springboot/src/main/java/com/hertz/common/api/ApiResponse.java b/hertz_springboot/src/main/java/com/hertz/common/api/ApiResponse.java
new file mode 100644
index 0000000..279b6ed
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/common/api/ApiResponse.java
@@ -0,0 +1,19 @@
+package com.hertz.common.api;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record ApiResponse(int code, String message, T data) {
+ public static ApiResponse ok(T data) {
+ return new ApiResponse<>(0, "ok", data);
+ }
+
+ public static ApiResponse ok() {
+ return new ApiResponse<>(0, "ok", null);
+ }
+
+ public static ApiResponse fail(int code, String message) {
+ return new ApiResponse<>(code, message, null);
+ }
+}
+
diff --git a/hertz_springboot/src/main/java/com/hertz/common/exception/BusinessException.java b/hertz_springboot/src/main/java/com/hertz/common/exception/BusinessException.java
new file mode 100644
index 0000000..e6a1500
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/common/exception/BusinessException.java
@@ -0,0 +1,15 @@
+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/hertz_springboot/src/main/java/com/hertz/common/exception/GlobalExceptionHandler.java b/hertz_springboot/src/main/java/com/hertz/common/exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000..f34d956
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/common/exception/GlobalExceptionHandler.java
@@ -0,0 +1,59 @@
+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;
+
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ @ExceptionHandler(BusinessException.class)
+ public ResponseEntity> handleBusinessException(BusinessException e) {
+ var status = HttpStatus.BAD_REQUEST;
+ if (e.getCode() == 40100) {
+ status = HttpStatus.UNAUTHORIZED;
+ } else if (e.getCode() == 40300) {
+ status = HttpStatus.FORBIDDEN;
+ }
+ return ResponseEntity.status(status).body(ApiResponse.fail(e.getCode(), e.getMessage()));
+ }
+
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public ApiResponse handleMethodArgumentNotValid(MethodArgumentNotValidException e) {
+ var first = e.getBindingResult().getFieldErrors().stream().findFirst().orElse(null);
+ var message = first == null ? "参数错误" : first.getField() + " " + first.getDefaultMessage();
+ return ApiResponse.fail(40001, message);
+ }
+
+ @ExceptionHandler(ConstraintViolationException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public ApiResponse handleConstraintViolation(ConstraintViolationException e) {
+ return ApiResponse.fail(40001, e.getMessage());
+ }
+
+ @ExceptionHandler(AuthenticationException.class)
+ @ResponseStatus(HttpStatus.UNAUTHORIZED)
+ public ApiResponse handleAuthentication(AuthenticationException e) {
+ return ApiResponse.fail(40100, "未登录或登录已过期");
+ }
+
+ @ExceptionHandler(AccessDeniedException.class)
+ @ResponseStatus(HttpStatus.FORBIDDEN)
+ public ApiResponse handleAccessDenied(AccessDeniedException e) {
+ return ApiResponse.fail(40300, "无权限");
+ }
+
+ @ExceptionHandler(Exception.class)
+ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+ public ApiResponse handleException(Exception e) {
+ return ApiResponse.fail(50000, "系统异常");
+ }
+}
diff --git a/hertz_springboot/src/main/java/com/hertz/security/JwtAuthFilter.java b/hertz_springboot/src/main/java/com/hertz/security/JwtAuthFilter.java
new file mode 100644
index 0000000..f7bf564
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/security/JwtAuthFilter.java
@@ -0,0 +1,57 @@
+package com.hertz.security;
+
+import com.hertz.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.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(
+ HttpServletRequest request,
+ HttpServletResponse response,
+ 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);
+ } catch (Exception ignored) {
+ SecurityContextHolder.clearContext();
+ }
+
+ filterChain.doFilter(request, response);
+ }
+}
+
diff --git a/hertz_springboot/src/main/java/com/hertz/security/JwtService.java b/hertz_springboot/src/main/java/com/hertz/security/JwtService.java
new file mode 100644
index 0000000..87ee606
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/security/JwtService.java
@@ -0,0 +1,48 @@
+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/hertz_springboot/src/main/java/com/hertz/security/SecurityConfig.java b/hertz_springboot/src/main/java/com/hertz/security/SecurityConfig.java
new file mode 100644
index 0000000..fa24fd0
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/security/SecurityConfig.java
@@ -0,0 +1,58 @@
+package com.hertz.security;
+
+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
+ .requestMatchers("/api/auth/**", "/error", "/uploads/**").permitAll()
+ .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
+ .anyRequest().authenticated()
+ )
+ .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
+ .build();
+ }
+}
+
diff --git a/hertz_springboot/src/main/java/com/hertz/security/SecurityUtils.java b/hertz_springboot/src/main/java/com/hertz/security/SecurityUtils.java
new file mode 100644
index 0000000..bff1232
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/security/SecurityUtils.java
@@ -0,0 +1,25 @@
+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/hertz_springboot/src/main/java/com/hertz/system/config/MybatisPlusConfig.java b/hertz_springboot/src/main/java/com/hertz/system/config/MybatisPlusConfig.java
new file mode 100644
index 0000000..e8aa64a
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/config/MybatisPlusConfig.java
@@ -0,0 +1,17 @@
+package com.hertz.system.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/hertz_springboot/src/main/java/com/hertz/system/config/WebMvcConfig.java b/hertz_springboot/src/main/java/com/hertz/system/config/WebMvcConfig.java
new file mode 100644
index 0000000..c93dc66
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/config/WebMvcConfig.java
@@ -0,0 +1,19 @@
+package com.hertz.system.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+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;
+
+ @Override
+ public void addResourceHandlers(ResourceHandlerRegistry registry) {
+ registry.addResourceHandler("/uploads/**")
+ .addResourceLocations("file:" + uploadRootPath + "/");
+ }
+}
diff --git a/hertz_springboot/src/main/java/com/hertz/system/controller/AuthController.java b/hertz_springboot/src/main/java/com/hertz/system/controller/AuthController.java
new file mode 100644
index 0000000..8e40ee7
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/controller/AuthController.java
@@ -0,0 +1,109 @@
+package com.hertz.system.controller;
+
+import com.hertz.common.api.ApiResponse;
+import com.hertz.common.exception.BusinessException;
+import com.hertz.security.JwtService;
+import com.hertz.security.SecurityUtils;
+import com.hertz.system.dto.AuthDtos;
+import com.hertz.system.mapper.SysRoleMapper;
+import com.hertz.system.service.UserService;
+import jakarta.validation.Valid;
+import java.util.List;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/auth")
+public class AuthController {
+ private final UserService userService;
+ private final PasswordEncoder passwordEncoder;
+ private final JwtService jwtService;
+ private final SysRoleMapper roleMapper;
+
+ public AuthController(
+ UserService userService,
+ PasswordEncoder passwordEncoder,
+ JwtService jwtService,
+ SysRoleMapper roleMapper
+ ) {
+ this.userService = userService;
+ this.passwordEncoder = passwordEncoder;
+ this.jwtService = jwtService;
+ this.roleMapper = roleMapper;
+ }
+
+ @PostMapping("/register")
+ public ApiResponse register(@Valid @RequestBody AuthDtos.RegisterRequest req) {
+ var user = userService.register(req.username(), req.password(), req.nickname());
+ return ApiResponse.ok(new AuthDtos.MeResponse(
+ user.getId(),
+ user.getUsername(),
+ user.getNickname(),
+ user.getAvatarPath(),
+ user.getPhone(),
+ user.getEmail(),
+ user.getGender(),
+ List.of()
+ ));
+ }
+
+ @PostMapping("/login")
+ public ApiResponse login(@Valid @RequestBody AuthDtos.LoginRequest req) {
+ var user = userService.findByUsername(req.username());
+ if (user == null || user.getStatus() == null || user.getStatus() != 1) {
+ throw new BusinessException(40003, "用户名或密码错误");
+ }
+ if (!passwordEncoder.matches(req.password(), user.getPassword())) {
+ throw new BusinessException(40003, "用户名或密码错误");
+ }
+ var token = jwtService.createToken(user.getId(), user.getUsername());
+ var roles = roleMapper.selectRoleKeysByUserId(user.getId());
+ return ApiResponse.ok(new AuthDtos.LoginResponse(token, user.getId(), user.getUsername(), roles));
+ }
+
+ @GetMapping("/me")
+ public ApiResponse me(Authentication authentication) {
+ var userId = SecurityUtils.getCurrentUserId(authentication);
+ if (userId == null) {
+ throw new BusinessException(40100, "未登录或登录已过期");
+ }
+ var user = userService.findByUsername(authentication.getName());
+ var roles = roleMapper.selectRoleKeysByUserId(userId);
+ return ApiResponse.ok(new AuthDtos.MeResponse(
+ userId,
+ authentication.getName(),
+ user == null ? null : user.getNickname(),
+ user == null ? null : user.getAvatarPath(),
+ user == null ? null : user.getPhone(),
+ user == null ? null : user.getEmail(),
+ user == null ? null : user.getGender(),
+ roles
+ ));
+ }
+
+ @PostMapping("/profile")
+ public ApiResponse updateProfile(@RequestBody @Valid AuthDtos.UpdateProfileRequest req) {
+ var userId = SecurityUtils.getCurrentUserId();
+ if (userId == null) {
+ throw new BusinessException(40100, "未登录或登录已过期");
+ }
+ userService.updateProfile(userId, req);
+ return ApiResponse.ok();
+ }
+
+ @PostMapping("/password")
+ public ApiResponse updatePassword(@RequestBody @Valid AuthDtos.UpdatePasswordRequest req) {
+ var userId = SecurityUtils.getCurrentUserId();
+ if (userId == null) {
+ throw new BusinessException(40100, "未登录或登录已过期");
+ }
+ userService.updatePassword(userId, req);
+ return ApiResponse.ok();
+ }
+}
+
diff --git a/hertz_springboot/src/main/java/com/hertz/system/controller/FileController.java b/hertz_springboot/src/main/java/com/hertz/system/controller/FileController.java
new file mode 100644
index 0000000..ebe3f47
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/controller/FileController.java
@@ -0,0 +1,68 @@
+package com.hertz.system.controller;
+
+import com.hertz.common.api.ApiResponse;
+import com.hertz.common.exception.BusinessException;
+import java.io.File;
+import java.io.IOException;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.UUID;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+@RestController
+@RequestMapping("/api/common/file")
+public class FileController {
+
+ @Value("${app.upload.root-path}")
+ private String uploadRootPath;
+
+ @Value("${app.upload.avatar-path}")
+ private String avatarPath;
+
+ @PostMapping("/upload")
+ public ApiResponse upload(@RequestParam("file") MultipartFile file) {
+ if (file.isEmpty()) {
+ throw new BusinessException(400, "File cannot be empty");
+ }
+
+ String originalFilename = file.getOriginalFilename();
+ String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
+ if (!suffix.equalsIgnoreCase(".jpg") && !suffix.equalsIgnoreCase(".png") && !suffix.equalsIgnoreCase(".jpeg")) {
+ throw new BusinessException(400, "Only JPG/PNG formats are supported");
+ }
+
+ if (file.getSize() > 2 * 1024 * 1024) {
+ throw new BusinessException(400, "File size exceeds 2MB");
+ }
+
+ String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
+ String fileName = UUID.randomUUID().toString().replace("-", "") + suffix;
+ // Construct the relative path (e.g., avatar/2023/10/27/uuid.jpg)
+ // Note: avatarPath should end with /
+ String relativePath = avatarPath + datePath + "/" + fileName;
+ // Construct full path
+ String fullPath = uploadRootPath + File.separator + relativePath;
+
+ File dest = new File(fullPath);
+ if (!dest.getParentFile().exists()) {
+ dest.getParentFile().mkdirs();
+ }
+
+ try {
+ file.transferTo(dest);
+
+ // Return relative path. Frontend should prepend the base URL.
+ return ApiResponse.ok(new UploadResult("/uploads/" + relativePath, null));
+ } catch (IOException e) {
+ throw new BusinessException(500, "File upload failed: " + e.getMessage());
+ }
+ }
+
+ public record UploadResult(String url, String thumbUrl) {
+ }
+}
diff --git a/hertz_springboot/src/main/java/com/hertz/system/controller/MenuController.java b/hertz_springboot/src/main/java/com/hertz/system/controller/MenuController.java
new file mode 100644
index 0000000..df9187f
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/controller/MenuController.java
@@ -0,0 +1,72 @@
+package com.hertz.system.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.hertz.common.api.ApiResponse;
+import com.hertz.common.exception.BusinessException;
+import com.hertz.security.SecurityUtils;
+import com.hertz.system.dto.MenuDto;
+import com.hertz.system.entity.SysMenu;
+import com.hertz.system.service.MenuService;
+import java.util.List;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.Authentication;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/system/menus")
+public class MenuController {
+ private final MenuService menuService;
+
+ public MenuController(MenuService menuService) {
+ this.menuService = menuService;
+ }
+
+ @GetMapping("/tree")
+ public ApiResponse> tree(Authentication authentication) {
+ var userId = SecurityUtils.getCurrentUserId(authentication);
+ if (userId == null) {
+ throw new BusinessException(40100, "未登录或登录已过期");
+ }
+ return ApiResponse.ok(menuService.getMenuTreeByUserId(userId));
+ }
+
+ @GetMapping("/page")
+ @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:menu:view')")
+ public ApiResponse> page(
+ @RequestParam(defaultValue = "1") int page,
+ @RequestParam(defaultValue = "10") int size,
+ @RequestParam(required = false) String keyword
+ ) {
+ return ApiResponse.ok(menuService.pageMenus(page, size, keyword));
+ }
+
+ @PostMapping
+ @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:menu:add')")
+ public ApiResponse create(@RequestBody SysMenu menu) {
+ menuService.saveMenu(menu);
+ return ApiResponse.ok();
+ }
+
+ @PutMapping
+ @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:menu:edit')")
+ public ApiResponse update(@RequestBody SysMenu menu) {
+ menuService.updateMenu(menu);
+ return ApiResponse.ok();
+ }
+
+ @DeleteMapping("/{id}")
+ @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:menu:remove')")
+ public ApiResponse delete(@PathVariable("id") Long id) {
+ menuService.deleteMenu(id);
+ return ApiResponse.ok();
+ }
+}
+
diff --git a/hertz_springboot/src/main/java/com/hertz/system/controller/RoleController.java b/hertz_springboot/src/main/java/com/hertz/system/controller/RoleController.java
new file mode 100644
index 0000000..082f900
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/controller/RoleController.java
@@ -0,0 +1,81 @@
+package com.hertz.system.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.hertz.common.api.ApiResponse;
+import com.hertz.system.entity.SysRole;
+import com.hertz.system.service.RoleService;
+import java.util.List;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/system/roles")
+public class RoleController {
+ private final RoleService roleService;
+
+ public RoleController(RoleService roleService) {
+ this.roleService = roleService;
+ }
+
+ @GetMapping
+ @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:role:view')")
+ public ApiResponse> list() {
+ return ApiResponse.ok(roleService.listEnabledRoles());
+ }
+
+ @GetMapping("/page")
+ @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:role:view')")
+ public ApiResponse> page(
+ @RequestParam(defaultValue = "1") int page,
+ @RequestParam(defaultValue = "10") int size,
+ @RequestParam(required = false) String keyword
+ ) {
+ return ApiResponse.ok(roleService.pageRoles(page, size, keyword));
+ }
+
+ @PostMapping
+ @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:role:add')")
+ public ApiResponse create(@RequestBody SysRole role) {
+ roleService.saveRole(role);
+ return ApiResponse.ok();
+ }
+
+ @PutMapping
+ @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:role:edit')")
+ public ApiResponse update(@RequestBody SysRole role) {
+ roleService.updateRole(role);
+ return ApiResponse.ok();
+ }
+
+ @DeleteMapping("/{id}")
+ @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:role:remove')")
+ public ApiResponse delete(@PathVariable("id") Long id) {
+ roleService.deleteRole(id);
+ return ApiResponse.ok();
+ }
+
+ @GetMapping("/{id}/menus")
+ @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:role:view')")
+ public ApiResponse> getRoleMenus(@PathVariable("id") Long id) {
+ return ApiResponse.ok(roleService.getRoleMenuIds(id));
+ }
+
+ public record UpdateRoleMenusRequest(List menuIds) {
+ }
+
+ @PutMapping("/{id}/menus")
+ @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:role:assign')")
+ public ApiResponse updateRoleMenus(@PathVariable("id") Long id, @RequestBody UpdateRoleMenusRequest req) {
+ roleService.updateRolePermissions(id, req.menuIds());
+ return ApiResponse.ok();
+ }
+}
+
diff --git a/hertz_springboot/src/main/java/com/hertz/system/controller/UserController.java b/hertz_springboot/src/main/java/com/hertz/system/controller/UserController.java
new file mode 100644
index 0000000..4b23be6
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/controller/UserController.java
@@ -0,0 +1,153 @@
+package com.hertz.system.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.hertz.common.api.ApiResponse;
+import com.hertz.system.entity.SysUser;
+import com.hertz.system.service.UserService;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import java.time.LocalDateTime;
+import java.util.List;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/system/users")
+public class UserController {
+ private final UserService userService;
+
+ public UserController(UserService userService) {
+ this.userService = userService;
+ }
+
+ @GetMapping
+ @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:user:view')")
+ public ApiResponse> page(
+ @RequestParam(defaultValue = "1") @Min(1) int page,
+ @RequestParam(defaultValue = "10") @Min(1) @Max(200) int size,
+ @RequestParam(required = false) String keyword
+ ) {
+ var p = userService.pageUsers(page, size, keyword);
+ var dtoPage = Page.of(p.getCurrent(), p.getSize(), p.getTotal());
+ dtoPage.setRecords(p.getRecords().stream().map(u -> {
+ var roles = userService.getUserRoles(u.getId()).stream()
+ .map(com.hertz.system.entity.SysRole::getRoleName)
+ .toList();
+ return new UserListItem(
+ u.getId(),
+ u.getUsername(),
+ u.getNickname(),
+ u.getAvatarPath(),
+ u.getPhone(),
+ u.getEmail(),
+ u.getGender(),
+ u.getStatus(),
+ u.getCreatedAt(),
+ roles
+ );
+ }).toList());
+ return ApiResponse.ok(dtoPage);
+ }
+
+ public record UserListItem(
+ Long id,
+ String username,
+ String nickname,
+ String avatarPath,
+ String phone,
+ String email,
+ Integer gender,
+ Integer status,
+ LocalDateTime createdAt,
+ List roles
+ ) {
+ }
+
+ public record CreateUserRequest(
+ @NotBlank(message = "用户名不能为空") String username,
+ @NotBlank(message = "密码不能为空") String password,
+ @NotBlank(message = "昵称不能为空") String nickname,
+ String avatarPath,
+ String phone,
+ String email,
+ Integer gender,
+ Integer status
+ ) {}
+
+ @PostMapping
+ @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:user:add')")
+ public ApiResponse create(@RequestBody @Valid CreateUserRequest req) {
+ var u = new SysUser();
+ u.setUsername(req.username());
+ u.setPassword(req.password());
+ u.setNickname(req.nickname());
+ u.setAvatarPath(req.avatarPath());
+ u.setPhone(req.phone());
+ u.setEmail(req.email());
+ u.setGender(req.gender());
+ u.setStatus(req.status());
+ userService.createUser(u);
+ return ApiResponse.ok();
+ }
+
+ public record UpdateUserRequest(
+ Long id,
+ String password,
+ @NotBlank(message = "昵称不能为空") String nickname,
+ String avatarPath,
+ String phone,
+ String email,
+ Integer gender,
+ Integer status
+ ) {}
+
+ @PutMapping
+ @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:user:edit')")
+ public ApiResponse update(@RequestBody @Valid UpdateUserRequest req) {
+ var u = new SysUser();
+ u.setId(req.id());
+ u.setPassword(req.password());
+ u.setNickname(req.nickname());
+ u.setAvatarPath(req.avatarPath());
+ u.setPhone(req.phone());
+ u.setEmail(req.email());
+ u.setGender(req.gender());
+ u.setStatus(req.status());
+ userService.updateUser(u);
+ return ApiResponse.ok();
+ }
+
+ @DeleteMapping("/{id}")
+ @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:user:remove')")
+ public ApiResponse delete(@PathVariable Long id) {
+ userService.deleteUser(id);
+ return ApiResponse.ok();
+ }
+
+ @GetMapping("/{id}/roles")
+ @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:user:view')")
+ public ApiResponse> roleIds(@PathVariable("id") long userId) {
+ return ApiResponse.ok(userService.getUserRoleIds(userId));
+ }
+
+ public record UpdateRolesRequest(List roleIds) {
+ }
+
+ @PutMapping("/{id}/roles")
+ @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:user:assign')")
+ public ApiResponse updateRoles(@PathVariable("id") long userId, @RequestBody UpdateRolesRequest req) {
+ userService.updateUserRoles(userId, req == null ? List.of() : req.roleIds());
+ return ApiResponse.ok();
+ }
+}
diff --git a/hertz_springboot/src/main/java/com/hertz/system/dto/AuthDtos.java b/hertz_springboot/src/main/java/com/hertz/system/dto/AuthDtos.java
new file mode 100644
index 0000000..93ece57
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/dto/AuthDtos.java
@@ -0,0 +1,55 @@
+package com.hertz.system.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import java.util.List;
+
+public class AuthDtos {
+ public record LoginRequest(
+ @NotBlank String username,
+ @NotBlank String password
+ ) {
+ }
+
+ public record RegisterRequest(
+ @NotBlank String username,
+ @NotBlank String password,
+ String nickname
+ ) {
+ }
+
+ public record LoginResponse(
+ String token,
+ long userId,
+ String username,
+ List roles
+ ) {
+ }
+
+ public record MeResponse(
+ long userId,
+ String username,
+ String nickname,
+ String avatarPath,
+ String phone,
+ String email,
+ Integer gender,
+ List roles
+ ) {
+ }
+
+ public record UpdateProfileRequest(
+ @NotBlank(message = "昵称不能为空") String nickname,
+ String avatarPath,
+ String phone,
+ String email,
+ Integer gender
+ ) {
+ }
+
+ public record UpdatePasswordRequest(
+ @NotBlank(message = "旧密码不能为空") String oldPassword,
+ @NotBlank(message = "新密码不能为空") String newPassword
+ ) {
+ }
+}
+
diff --git a/hertz_springboot/src/main/java/com/hertz/system/dto/MenuDto.java b/hertz_springboot/src/main/java/com/hertz/system/dto/MenuDto.java
new file mode 100644
index 0000000..af84093
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/dto/MenuDto.java
@@ -0,0 +1,20 @@
+package com.hertz.system.dto;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Data;
+
+@Data
+public class MenuDto {
+ private Long id;
+ private Long parentId;
+ private String type;
+ private String name;
+ private String path;
+ private String component;
+ private String perms;
+ private String icon;
+ private Integer sort;
+ private List children = new ArrayList<>();
+}
+
diff --git a/hertz_springboot/src/main/java/com/hertz/system/entity/SysMenu.java b/hertz_springboot/src/main/java/com/hertz/system/entity/SysMenu.java
new file mode 100644
index 0000000..34d34b5
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/entity/SysMenu.java
@@ -0,0 +1,27 @@
+package com.hertz.system.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import java.time.LocalDateTime;
+import lombok.Data;
+
+@Data
+@TableName("sys_menu")
+public class SysMenu {
+ @TableId(type = IdType.AUTO)
+ private Long id;
+ private Long parentId;
+ private String type;
+ private String name;
+ private String path;
+ private String component;
+ private String perms;
+ private String icon;
+ private Integer sort;
+ private Integer visible;
+ private Integer status;
+ private LocalDateTime createdAt;
+ private LocalDateTime updatedAt;
+}
+
diff --git a/hertz_springboot/src/main/java/com/hertz/system/entity/SysRole.java b/hertz_springboot/src/main/java/com/hertz/system/entity/SysRole.java
new file mode 100644
index 0000000..d3610ce
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/entity/SysRole.java
@@ -0,0 +1,20 @@
+package com.hertz.system.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import java.time.LocalDateTime;
+import lombok.Data;
+
+@Data
+@TableName("sys_role")
+public class SysRole {
+ @TableId(type = IdType.AUTO)
+ private Long id;
+ private String roleKey;
+ private String roleName;
+ private Integer status;
+ private LocalDateTime createdAt;
+ private LocalDateTime updatedAt;
+}
+
diff --git a/hertz_springboot/src/main/java/com/hertz/system/entity/SysRoleMenu.java b/hertz_springboot/src/main/java/com/hertz/system/entity/SysRoleMenu.java
new file mode 100644
index 0000000..ebe428f
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/entity/SysRoleMenu.java
@@ -0,0 +1,16 @@
+package com.hertz.system.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+@Data
+@TableName("sys_role_menu")
+public class SysRoleMenu {
+ @TableId(type = IdType.AUTO)
+ private Long id;
+ private Long roleId;
+ private Long menuId;
+}
+
diff --git a/hertz_springboot/src/main/java/com/hertz/system/entity/SysUser.java b/hertz_springboot/src/main/java/com/hertz/system/entity/SysUser.java
new file mode 100644
index 0000000..1ba0a19
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/entity/SysUser.java
@@ -0,0 +1,28 @@
+package com.hertz.system.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import java.time.LocalDateTime;
+import lombok.Data;
+
+@Data
+@TableName("sys_user")
+public class SysUser {
+ @TableId(type = IdType.AUTO)
+ private Long id;
+ private String username;
+ private String password;
+ private String nickname;
+ private String avatarPath;
+ private String phone;
+ private String email;
+ /**
+ * 0-未知 1-男 2-女
+ */
+ private Integer gender;
+ private Integer status;
+ private LocalDateTime createdAt;
+ private LocalDateTime updatedAt;
+}
+
diff --git a/hertz_springboot/src/main/java/com/hertz/system/entity/SysUserRole.java b/hertz_springboot/src/main/java/com/hertz/system/entity/SysUserRole.java
new file mode 100644
index 0000000..33c8039
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/entity/SysUserRole.java
@@ -0,0 +1,16 @@
+package com.hertz.system.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+@Data
+@TableName("sys_user_role")
+public class SysUserRole {
+ @TableId(type = IdType.AUTO)
+ private Long id;
+ private Long userId;
+ private Long roleId;
+}
+
diff --git a/hertz_springboot/src/main/java/com/hertz/system/mapper/SysMenuMapper.java b/hertz_springboot/src/main/java/com/hertz/system/mapper/SysMenuMapper.java
new file mode 100644
index 0000000..8618c31
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/mapper/SysMenuMapper.java
@@ -0,0 +1,56 @@
+package com.hertz.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.hertz.system.entity.SysMenu;
+import java.util.List;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+@Mapper
+public interface SysMenuMapper extends BaseMapper {
+ @Select("""
+ SELECT DISTINCT m.*
+ FROM sys_menu m
+ INNER JOIN sys_role_menu rm ON rm.menu_id = m.id
+ INNER JOIN sys_user_role ur ON ur.role_id = rm.role_id
+ WHERE ur.user_id = #{userId}
+ AND m.status = 1
+ AND m.visible = 1
+ AND m.type IN ('D','M')
+ ORDER BY m.sort ASC, m.id ASC
+ """)
+ List selectMenusByUserId(@Param("userId") long userId);
+
+ @Select("""
+ SELECT m.*
+ FROM sys_menu m
+ WHERE m.status = 1
+ AND m.visible = 1
+ AND m.type IN ('D','M')
+ ORDER BY m.sort ASC, m.id ASC
+ """)
+ List selectAllVisibleMenus();
+
+ @Select("""
+ SELECT DISTINCT m.perms
+ FROM sys_menu m
+ INNER JOIN sys_role_menu rm ON rm.menu_id = m.id
+ INNER JOIN sys_user_role ur ON ur.role_id = rm.role_id
+ WHERE ur.user_id = #{userId}
+ AND m.status = 1
+ AND m.perms IS NOT NULL
+ AND m.perms <> ''
+ """)
+ List selectPermsByUserId(@Param("userId") long userId);
+
+ @Select("""
+ SELECT DISTINCT m.perms
+ FROM sys_menu m
+ WHERE m.status = 1
+ AND m.perms IS NOT NULL
+ AND m.perms <> ''
+ """)
+ List selectAllPerms();
+}
+
diff --git a/hertz_springboot/src/main/java/com/hertz/system/mapper/SysRoleMapper.java b/hertz_springboot/src/main/java/com/hertz/system/mapper/SysRoleMapper.java
new file mode 100644
index 0000000..13c30f5
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/mapper/SysRoleMapper.java
@@ -0,0 +1,29 @@
+package com.hertz.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.hertz.system.entity.SysRole;
+import java.util.List;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+@Mapper
+public interface SysRoleMapper extends BaseMapper {
+ @Select("""
+ SELECT r.role_key
+ FROM sys_role r
+ INNER JOIN sys_user_role ur ON ur.role_id = r.id
+ WHERE ur.user_id = #{userId} AND r.status = 1
+ """)
+ List selectRoleKeysByUserId(@Param("userId") long userId);
+
+ @Select("""
+ SELECT r.*
+ FROM sys_role r
+ INNER JOIN sys_user_role ur ON ur.role_id = r.id
+ WHERE ur.user_id = #{userId} AND r.status = 1
+ ORDER BY r.id ASC
+ """)
+ List selectRolesByUserId(@Param("userId") long userId);
+}
+
diff --git a/hertz_springboot/src/main/java/com/hertz/system/mapper/SysRoleMenuMapper.java b/hertz_springboot/src/main/java/com/hertz/system/mapper/SysRoleMenuMapper.java
new file mode 100644
index 0000000..b2e084c
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/mapper/SysRoleMenuMapper.java
@@ -0,0 +1,9 @@
+package com.hertz.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.hertz.system.entity.SysRoleMenu;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface SysRoleMenuMapper extends BaseMapper {
+}
diff --git a/hertz_springboot/src/main/java/com/hertz/system/mapper/SysUserMapper.java b/hertz_springboot/src/main/java/com/hertz/system/mapper/SysUserMapper.java
new file mode 100644
index 0000000..50de1f7
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/mapper/SysUserMapper.java
@@ -0,0 +1,10 @@
+package com.hertz.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.hertz.system.entity.SysUser;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface SysUserMapper extends BaseMapper {
+}
+
diff --git a/hertz_springboot/src/main/java/com/hertz/system/mapper/SysUserRoleMapper.java b/hertz_springboot/src/main/java/com/hertz/system/mapper/SysUserRoleMapper.java
new file mode 100644
index 0000000..1380349
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/mapper/SysUserRoleMapper.java
@@ -0,0 +1,10 @@
+package com.hertz.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.hertz.system.entity.SysUserRole;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface SysUserRoleMapper extends BaseMapper {
+}
+
diff --git a/hertz_springboot/src/main/java/com/hertz/system/service/AuthzService.java b/hertz_springboot/src/main/java/com/hertz/system/service/AuthzService.java
new file mode 100644
index 0000000..495e5cc
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/service/AuthzService.java
@@ -0,0 +1,8 @@
+package com.hertz.system.service;
+
+import java.util.Set;
+
+public interface AuthzService {
+ Set loadAuthorities(long userId);
+}
+
diff --git a/hertz_springboot/src/main/java/com/hertz/system/service/MenuService.java b/hertz_springboot/src/main/java/com/hertz/system/service/MenuService.java
new file mode 100644
index 0000000..95b1977
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/service/MenuService.java
@@ -0,0 +1,19 @@
+package com.hertz.system.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.hertz.system.dto.MenuDto;
+import com.hertz.system.entity.SysMenu;
+import java.util.List;
+
+public interface MenuService {
+ List getMenuTreeByUserId(long userId);
+
+ IPage pageMenus(int page, int size, String keyword);
+
+ void saveMenu(SysMenu menu);
+
+ void updateMenu(SysMenu menu);
+
+ void deleteMenu(Long id);
+}
+
diff --git a/hertz_springboot/src/main/java/com/hertz/system/service/RoleService.java b/hertz_springboot/src/main/java/com/hertz/system/service/RoleService.java
new file mode 100644
index 0000000..fd208ea
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/service/RoleService.java
@@ -0,0 +1,22 @@
+package com.hertz.system.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.hertz.system.entity.SysRole;
+import java.util.List;
+
+public interface RoleService {
+ List listEnabledRoles();
+
+ IPage pageRoles(int page, int size, String keyword);
+
+ void saveRole(SysRole role);
+
+ void updateRole(SysRole role);
+
+ void deleteRole(Long id);
+
+ void updateRolePermissions(Long roleId, List menuIds);
+
+ List getRoleMenuIds(Long roleId);
+}
+
diff --git a/hertz_springboot/src/main/java/com/hertz/system/service/UserService.java b/hertz_springboot/src/main/java/com/hertz/system/service/UserService.java
new file mode 100644
index 0000000..0cc4e6f
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/service/UserService.java
@@ -0,0 +1,31 @@
+package com.hertz.system.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.hertz.system.dto.AuthDtos;
+import com.hertz.system.entity.SysUser;
+import java.util.List;
+
+public interface UserService {
+ SysUser register(String username, String rawPassword, String nickname);
+
+ SysUser findByUsername(String username);
+
+ IPage pageUsers(int page, int size, String keyword);
+
+ void createUser(SysUser user);
+
+ void updateUser(SysUser user);
+
+ void updateProfile(Long userId, AuthDtos.UpdateProfileRequest req);
+
+ void updatePassword(Long userId, AuthDtos.UpdatePasswordRequest req);
+
+ void deleteUser(Long id);
+
+ void updateUserRoles(long userId, List roleIds);
+
+ List getUserRoleIds(long userId);
+
+ List getUserRoles(long userId);
+}
+
diff --git a/hertz_springboot/src/main/java/com/hertz/system/service/impl/AuthzServiceImpl.java b/hertz_springboot/src/main/java/com/hertz/system/service/impl/AuthzServiceImpl.java
new file mode 100644
index 0000000..5f98db9
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/service/impl/AuthzServiceImpl.java
@@ -0,0 +1,36 @@
+package com.hertz.system.service.impl;
+
+import com.hertz.system.mapper.SysMenuMapper;
+import com.hertz.system.mapper.SysRoleMapper;
+import com.hertz.system.service.AuthzService;
+import java.util.HashSet;
+import org.springframework.stereotype.Service;
+
+@Service
+public class AuthzServiceImpl implements AuthzService {
+ private final SysRoleMapper roleMapper;
+ private final SysMenuMapper menuMapper;
+
+ public AuthzServiceImpl(SysRoleMapper roleMapper, SysMenuMapper menuMapper) {
+ this.roleMapper = roleMapper;
+ this.menuMapper = menuMapper;
+ }
+
+ @Override
+ public HashSet loadAuthorities(long userId) {
+ var roleKeys = roleMapper.selectRoleKeysByUserId(userId);
+ var authorities = new HashSet();
+ for (var roleKey : roleKeys) {
+ if (roleKey != null && !roleKey.isBlank()) {
+ authorities.add("ROLE_" + roleKey);
+ }
+ }
+ if (roleKeys.stream().anyMatch(r -> "ADMIN".equalsIgnoreCase(r))) {
+ authorities.addAll(menuMapper.selectAllPerms());
+ } else {
+ authorities.addAll(menuMapper.selectPermsByUserId(userId));
+ }
+ return authorities;
+ }
+}
+
diff --git a/hertz_springboot/src/main/java/com/hertz/system/service/impl/MenuServiceImpl.java b/hertz_springboot/src/main/java/com/hertz/system/service/impl/MenuServiceImpl.java
new file mode 100644
index 0000000..69aabea
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/service/impl/MenuServiceImpl.java
@@ -0,0 +1,129 @@
+package com.hertz.system.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.hertz.common.exception.BusinessException;
+import com.hertz.system.dto.MenuDto;
+import com.hertz.system.entity.SysMenu;
+import com.hertz.system.entity.SysRoleMenu;
+import com.hertz.system.mapper.SysMenuMapper;
+import com.hertz.system.mapper.SysRoleMapper;
+import com.hertz.system.mapper.SysRoleMenuMapper;
+import com.hertz.system.service.MenuService;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class MenuServiceImpl implements MenuService {
+ private final SysMenuMapper menuMapper;
+ private final SysRoleMapper roleMapper;
+ private final SysRoleMenuMapper roleMenuMapper;
+
+ public MenuServiceImpl(SysMenuMapper menuMapper, SysRoleMapper roleMapper, SysRoleMenuMapper roleMenuMapper) {
+ this.menuMapper = menuMapper;
+ this.roleMapper = roleMapper;
+ this.roleMenuMapper = roleMenuMapper;
+ }
+
+ @Override
+ public List getMenuTreeByUserId(long userId) {
+ var roleKeys = roleMapper.selectRoleKeysByUserId(userId);
+ var menus = roleKeys.stream().anyMatch(r -> "ADMIN".equalsIgnoreCase(r))
+ ? menuMapper.selectAllVisibleMenus()
+ : menuMapper.selectMenusByUserId(userId);
+
+ var map = new HashMap();
+ for (var m : menus) {
+ var dto = new MenuDto();
+ dto.setId(m.getId());
+ dto.setParentId(m.getParentId());
+ dto.setType(m.getType());
+ dto.setName(m.getName());
+ dto.setPath(m.getPath());
+ dto.setComponent(m.getComponent());
+ dto.setPerms(m.getPerms());
+ dto.setIcon(m.getIcon());
+ dto.setSort(m.getSort());
+ map.put(dto.getId(), dto);
+ }
+
+ var roots = new ArrayList();
+ for (var dto : map.values()) {
+ var parentId = dto.getParentId() == null ? 0L : dto.getParentId();
+ if (parentId == 0L || !map.containsKey(parentId)) {
+ roots.add(dto);
+ } else {
+ map.get(parentId).getChildren().add(dto);
+ }
+ }
+
+ Comparator comparator = Comparator
+ .comparing((MenuDto d) -> d.getSort() == null ? 0 : d.getSort())
+ .thenComparing(d -> d.getId() == null ? 0 : d.getId());
+ sortRecursively(roots, comparator);
+ return roots;
+ }
+
+ @Override
+ public IPage pageMenus(int page, int size, String keyword) {
+ var wrapper = new LambdaQueryWrapper().orderByAsc(SysMenu::getSort);
+ if (keyword != null && !keyword.isBlank()) {
+ wrapper.like(SysMenu::getName, keyword);
+ }
+ return menuMapper.selectPage(Page.of(page, size), wrapper);
+ }
+
+ @Override
+ @Transactional
+ public void saveMenu(SysMenu menu) {
+ menu.setCreatedAt(java.time.LocalDateTime.now());
+ menu.setUpdatedAt(java.time.LocalDateTime.now());
+ menuMapper.insert(menu);
+ }
+
+ @Override
+ @Transactional
+ public void updateMenu(SysMenu menu) {
+ var existing = menuMapper.selectById(menu.getId());
+ if (existing == null) {
+ throw new BusinessException(404, "Menu not found");
+ }
+ existing.setParentId(menu.getParentId());
+ existing.setType(menu.getType());
+ existing.setName(menu.getName());
+ existing.setPath(menu.getPath());
+ existing.setComponent(menu.getComponent());
+ existing.setPerms(menu.getPerms());
+ existing.setIcon(menu.getIcon());
+ existing.setSort(menu.getSort());
+ existing.setVisible(menu.getVisible());
+ existing.setStatus(menu.getStatus());
+ existing.setUpdatedAt(java.time.LocalDateTime.now());
+ menuMapper.updateById(existing);
+ }
+
+ @Override
+ @Transactional
+ public void deleteMenu(Long id) {
+ if (menuMapper.selectCount(new LambdaQueryWrapper().eq(SysMenu::getParentId, id)) > 0) {
+ throw new BusinessException(400, "Has sub-menus, cannot delete");
+ }
+ menuMapper.deleteById(id);
+ roleMenuMapper.delete(new LambdaQueryWrapper().eq(SysRoleMenu::getMenuId, id));
+ }
+
+ private void sortRecursively(List nodes, Comparator comparator) {
+ nodes.sort(comparator);
+ for (var n : nodes) {
+ if (n.getChildren() != null && !n.getChildren().isEmpty()) {
+ sortRecursively(n.getChildren(), comparator);
+ }
+ }
+ }
+}
+
diff --git a/hertz_springboot/src/main/java/com/hertz/system/service/impl/RoleServiceImpl.java b/hertz_springboot/src/main/java/com/hertz/system/service/impl/RoleServiceImpl.java
new file mode 100644
index 0000000..1a67d6a
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/service/impl/RoleServiceImpl.java
@@ -0,0 +1,94 @@
+package com.hertz.system.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.hertz.common.exception.BusinessException;
+import com.hertz.system.entity.SysRole;
+import com.hertz.system.entity.SysRoleMenu;
+import com.hertz.system.mapper.SysRoleMapper;
+import com.hertz.system.mapper.SysRoleMenuMapper;
+import com.hertz.system.service.RoleService;
+import java.util.List;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class RoleServiceImpl implements RoleService {
+ private final SysRoleMapper roleMapper;
+ private final SysRoleMenuMapper roleMenuMapper;
+
+ public RoleServiceImpl(SysRoleMapper roleMapper, SysRoleMenuMapper roleMenuMapper) {
+ this.roleMapper = roleMapper;
+ this.roleMenuMapper = roleMenuMapper;
+ }
+
+ @Override
+ public List listEnabledRoles() {
+ return roleMapper.selectList(new LambdaQueryWrapper()
+ .eq(SysRole::getStatus, 1)
+ .orderByAsc(SysRole::getId));
+ }
+
+ @Override
+ public IPage pageRoles(int page, int size, String keyword) {
+ var wrapper = new LambdaQueryWrapper().orderByDesc(SysRole::getId);
+ if (keyword != null && !keyword.isBlank()) {
+ wrapper.and(w -> w.like(SysRole::getRoleName, keyword).or().like(SysRole::getRoleKey, keyword));
+ }
+ return roleMapper.selectPage(Page.of(page, size), wrapper);
+ }
+
+ @Override
+ @Transactional
+ public void saveRole(SysRole role) {
+ if (roleMapper.selectCount(new LambdaQueryWrapper().eq(SysRole::getRoleKey, role.getRoleKey())) > 0) {
+ throw new BusinessException(400, "Role key already exists");
+ }
+ role.setCreatedAt(java.time.LocalDateTime.now());
+ role.setUpdatedAt(java.time.LocalDateTime.now());
+ roleMapper.insert(role);
+ }
+
+ @Override
+ @Transactional
+ public void updateRole(SysRole role) {
+ var existing = roleMapper.selectById(role.getId());
+ if (existing == null) {
+ throw new BusinessException(404, "Role not found");
+ }
+ existing.setRoleName(role.getRoleName());
+ existing.setRoleKey(role.getRoleKey());
+ existing.setStatus(role.getStatus());
+ existing.setUpdatedAt(java.time.LocalDateTime.now());
+ roleMapper.updateById(existing);
+ }
+
+ @Override
+ @Transactional
+ public void deleteRole(Long id) {
+ roleMapper.deleteById(id);
+ roleMenuMapper.delete(new LambdaQueryWrapper().eq(SysRoleMenu::getRoleId, id));
+ }
+
+ @Override
+ @Transactional
+ public void updateRolePermissions(Long roleId, List menuIds) {
+ roleMenuMapper.delete(new LambdaQueryWrapper().eq(SysRoleMenu::getRoleId, roleId));
+ if (menuIds != null) {
+ menuIds.stream().distinct().forEach(menuId -> {
+ SysRoleMenu rm = new SysRoleMenu();
+ rm.setRoleId(roleId);
+ rm.setMenuId(menuId);
+ roleMenuMapper.insert(rm);
+ });
+ }
+ }
+
+ @Override
+ public List getRoleMenuIds(Long roleId) {
+ return roleMenuMapper.selectList(new LambdaQueryWrapper().eq(SysRoleMenu::getRoleId, roleId))
+ .stream().map(SysRoleMenu::getMenuId).toList();
+ }
+}
+
diff --git a/hertz_springboot/src/main/java/com/hertz/system/service/impl/UserServiceImpl.java b/hertz_springboot/src/main/java/com/hertz/system/service/impl/UserServiceImpl.java
new file mode 100644
index 0000000..73dd436
--- /dev/null
+++ b/hertz_springboot/src/main/java/com/hertz/system/service/impl/UserServiceImpl.java
@@ -0,0 +1,174 @@
+package com.hertz.system.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.hertz.common.exception.BusinessException;
+import com.hertz.system.entity.SysUser;
+import com.hertz.system.entity.SysUserRole;
+import com.hertz.system.mapper.SysRoleMapper;
+import com.hertz.system.mapper.SysUserMapper;
+import com.hertz.system.mapper.SysUserRoleMapper;
+import com.hertz.system.service.UserService;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class UserServiceImpl implements UserService {
+ private final SysUserMapper userMapper;
+ private final SysUserRoleMapper userRoleMapper;
+ private final SysRoleMapper roleMapper;
+ private final PasswordEncoder passwordEncoder;
+
+ public UserServiceImpl(SysUserMapper userMapper, SysUserRoleMapper userRoleMapper, SysRoleMapper roleMapper, PasswordEncoder passwordEncoder) {
+ this.userMapper = userMapper;
+ this.userRoleMapper = userRoleMapper;
+ this.roleMapper = roleMapper;
+ this.passwordEncoder = passwordEncoder;
+ }
+
+ @Override
+ public SysUser register(String username, String rawPassword, String nickname) {
+ var exists = userMapper.selectCount(new LambdaQueryWrapper()
+ .eq(SysUser::getUsername, username)) > 0;
+ if (exists) {
+ throw new BusinessException(40002, "用户名已存在");
+ }
+ var user = new SysUser();
+ user.setUsername(username);
+ user.setPassword(passwordEncoder.encode(rawPassword));
+ user.setNickname(nickname);
+ user.setStatus(1);
+ userMapper.insert(user);
+ return user;
+ }
+
+ @Override
+ public SysUser findByUsername(String username) {
+ return userMapper.selectOne(new LambdaQueryWrapper()
+ .eq(SysUser::getUsername, username)
+ .last("LIMIT 1"));
+ }
+
+ @Override
+ public IPage pageUsers(int page, int size, String keyword) {
+ var wrapper = new LambdaQueryWrapper()
+ .orderByDesc(SysUser::getId);
+ if (keyword != null && !keyword.isBlank()) {
+ wrapper.and(w -> w.like(SysUser::getUsername, keyword)
+ .or().like(SysUser::getNickname, keyword)
+ .or().like(SysUser::getPhone, keyword)
+ .or().like(SysUser::getEmail, keyword));
+ }
+ return userMapper.selectPage(Page.of(page, size), wrapper);
+ }
+
+ @Override
+ @Transactional
+ public void createUser(SysUser user) {
+ if (userMapper.selectCount(new LambdaQueryWrapper().eq(SysUser::getUsername, user.getUsername())) > 0) {
+ throw new BusinessException(400, "用户名已存在");
+ }
+ user.setPassword(passwordEncoder.encode(user.getPassword()));
+ user.setCreatedAt(java.time.LocalDateTime.now());
+ user.setUpdatedAt(java.time.LocalDateTime.now());
+ userMapper.insert(user);
+ }
+
+ @Override
+ @Transactional
+ public void updateUser(SysUser user) {
+ var existing = userMapper.selectById(user.getId());
+ if (existing == null) {
+ throw new BusinessException(404, "用户不存在");
+ }
+ existing.setNickname(user.getNickname());
+ existing.setPhone(user.getPhone());
+ existing.setEmail(user.getEmail());
+ existing.setGender(user.getGender());
+ existing.setStatus(user.getStatus());
+ existing.setAvatarPath(user.getAvatarPath());
+ existing.setUpdatedAt(java.time.LocalDateTime.now());
+
+ if (user.getPassword() != null && !user.getPassword().isBlank()) {
+ existing.setPassword(passwordEncoder.encode(user.getPassword()));
+ }
+
+ userMapper.updateById(existing);
+ }
+
+ @Override
+ @Transactional
+ public void updateProfile(Long userId, com.hertz.system.dto.AuthDtos.UpdateProfileRequest req) {
+ var existing = userMapper.selectById(userId);
+ if (existing == null) {
+ throw new BusinessException(404, "用户不存在");
+ }
+ existing.setNickname(req.nickname());
+ // Only update avatarPath if it's provided (not null and not empty)
+ // Actually, req.avatarPath() might be empty string if cleared, but here we assume if it is provided we update it.
+ // If the frontend sends null, we might want to skip update or clear it?
+ // Let's assume frontend sends the new path if updated.
+ if (req.avatarPath() != null) {
+ existing.setAvatarPath(req.avatarPath());
+ }
+ existing.setPhone(req.phone());
+ existing.setEmail(req.email());
+ existing.setGender(req.gender());
+ existing.setUpdatedAt(java.time.LocalDateTime.now());
+ userMapper.updateById(existing);
+ }
+
+ @Override
+ @Transactional
+ public void updatePassword(Long userId, com.hertz.system.dto.AuthDtos.UpdatePasswordRequest req) {
+ var existing = userMapper.selectById(userId);
+ if (existing == null) {
+ throw new BusinessException(404, "用户不存在");
+ }
+ if (!passwordEncoder.matches(req.oldPassword(), existing.getPassword())) {
+ throw new BusinessException(400, "旧密码错误");
+ }
+ existing.setPassword(passwordEncoder.encode(req.newPassword()));
+ existing.setUpdatedAt(java.time.LocalDateTime.now());
+ userMapper.updateById(existing);
+ }
+
+ @Override
+ @Transactional
+ public void deleteUser(Long id) {
+ userMapper.deleteById(id);
+ userRoleMapper.delete(new LambdaQueryWrapper().eq(SysUserRole::getUserId, id));
+ }
+
+ @Override
+ @Transactional
+ public void updateUserRoles(long userId, List roleIds) {
+ userRoleMapper.delete(new LambdaQueryWrapper().eq(SysUserRole::getUserId, userId));
+ if (roleIds == null || roleIds.isEmpty()) {
+ return;
+ }
+ for (var roleId : roleIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList())) {
+ var ur = new SysUserRole();
+ ur.setUserId(userId);
+ ur.setRoleId(roleId);
+ userRoleMapper.insert(ur);
+ }
+ }
+
+ @Override
+ public List getUserRoleIds(long userId) {
+ var list = userRoleMapper.selectList(new LambdaQueryWrapper().eq(SysUserRole::getUserId, userId));
+ return list.stream().map(SysUserRole::getRoleId).filter(Objects::nonNull).distinct().toList();
+ }
+
+ @Override
+ public List getUserRoles(long userId) {
+ return roleMapper.selectRolesByUserId(userId);
+ }
+}
+
diff --git a/hertz_springboot/src/main/resources/application.yml b/hertz_springboot/src/main/resources/application.yml
new file mode 100644
index 0000000..c4009ba
--- /dev/null
+++ b/hertz_springboot/src/main/resources/application.yml
@@ -0,0 +1,30 @@
+server:
+ port: 8080
+
+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
+ 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
+
+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: d:\LocalFile\hertz_springboot\uploads
+ avatar-path: avatar/
diff --git a/hertz_springboot/src/main/resources/schema.sql b/hertz_springboot/src/main/resources/schema.sql
new file mode 100644
index 0000000..ef388e8
--- /dev/null
+++ b/hertz_springboot/src/main/resources/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);
diff --git a/hertz_springboot/src/test/java/com/hertz/HertzApplicationTests.java b/hertz_springboot/src/test/java/com/hertz/HertzApplicationTests.java
new file mode 100644
index 0000000..6274ae5
--- /dev/null
+++ b/hertz_springboot/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/hertz_springboot_ui/.env.development b/hertz_springboot_ui/.env.development
new file mode 100644
index 0000000..e2da3fa
--- /dev/null
+++ b/hertz_springboot_ui/.env.development
@@ -0,0 +1,2 @@
+VITE_API_BASE=http://localhost:8080
+
diff --git a/hertz_springboot_ui/README.md b/hertz_springboot_ui/README.md
new file mode 100644
index 0000000..33895ab
--- /dev/null
+++ b/hertz_springboot_ui/README.md
@@ -0,0 +1,5 @@
+# Vue 3 + TypeScript + Vite
+
+This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `
+