v1
This commit is contained in:
117
hertz_springboot/pom.xml
Normal file
117
hertz_springboot/pom.xml
Normal file
@@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.4.1</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.hertz</groupId>
|
||||
<artifactId>hertz-springboot-backend</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>hertz-springboot-backend</name>
|
||||
<description>Hertz 权限管理系统后端</description>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<maven.compiler.release>21</maven.compiler.release>
|
||||
<mybatis-plus.version>3.5.8</mybatis-plus.version>
|
||||
<jjwt.version>0.12.6</jjwt.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>${jjwt.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>${jjwt.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>${jjwt.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-enforcer-plugin</artifactId>
|
||||
<version>3.5.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>enforce-java</id>
|
||||
<goals>
|
||||
<goal>enforce</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<rules>
|
||||
<requireJavaVersion>
|
||||
<version>[21,)</version>
|
||||
</requireJavaVersion>
|
||||
</rules>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.hertz.common.api;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public record ApiResponse<T>(int code, String message, T data) {
|
||||
public static <T> ApiResponse<T> ok(T data) {
|
||||
return new ApiResponse<>(0, "ok", data);
|
||||
}
|
||||
|
||||
public static ApiResponse<Void> ok() {
|
||||
return new ApiResponse<>(0, "ok", null);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> fail(int code, String message) {
|
||||
return new ApiResponse<>(code, message, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ApiResponse<Void>> 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<Void> 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<Void> handleConstraintViolation(ConstraintViolationException e) {
|
||||
return ApiResponse.fail(40001, e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(AuthenticationException.class)
|
||||
@ResponseStatus(HttpStatus.UNAUTHORIZED)
|
||||
public ApiResponse<Void> handleAuthentication(AuthenticationException e) {
|
||||
return ApiResponse.fail(40100, "未登录或登录已过期");
|
||||
}
|
||||
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public ApiResponse<Void> handleAccessDenied(AccessDeniedException e) {
|
||||
return ApiResponse.fail(40300, "无权限");
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public ApiResponse<Void> handleException(Exception e) {
|
||||
return ApiResponse.fail(50000, "系统异常");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 + "/");
|
||||
}
|
||||
}
|
||||
@@ -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<AuthDtos.MeResponse> 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<AuthDtos.LoginResponse> 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<AuthDtos.MeResponse> 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<Void> 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<Void> updatePassword(@RequestBody @Valid AuthDtos.UpdatePasswordRequest req) {
|
||||
var userId = SecurityUtils.getCurrentUserId();
|
||||
if (userId == null) {
|
||||
throw new BusinessException(40100, "未登录或登录已过期");
|
||||
}
|
||||
userService.updatePassword(userId, req);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<UploadResult> 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) {
|
||||
}
|
||||
}
|
||||
@@ -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<List<MenuDto>> 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<IPage<SysMenu>> 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<Void> create(@RequestBody SysMenu menu) {
|
||||
menuService.saveMenu(menu);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@PreAuthorize("hasRole('ADMIN') or hasAuthority('system:menu:edit')")
|
||||
public ApiResponse<Void> update(@RequestBody SysMenu menu) {
|
||||
menuService.updateMenu(menu);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasRole('ADMIN') or hasAuthority('system:menu:remove')")
|
||||
public ApiResponse<Void> delete(@PathVariable("id") Long id) {
|
||||
menuService.deleteMenu(id);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SysRole>> list() {
|
||||
return ApiResponse.ok(roleService.listEnabledRoles());
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("hasRole('ADMIN') or hasAuthority('system:role:view')")
|
||||
public ApiResponse<IPage<SysRole>> 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<Void> create(@RequestBody SysRole role) {
|
||||
roleService.saveRole(role);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@PreAuthorize("hasRole('ADMIN') or hasAuthority('system:role:edit')")
|
||||
public ApiResponse<Void> update(@RequestBody SysRole role) {
|
||||
roleService.updateRole(role);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasRole('ADMIN') or hasAuthority('system:role:remove')")
|
||||
public ApiResponse<Void> delete(@PathVariable("id") Long id) {
|
||||
roleService.deleteRole(id);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/menus")
|
||||
@PreAuthorize("hasRole('ADMIN') or hasAuthority('system:role:view')")
|
||||
public ApiResponse<List<Long>> getRoleMenus(@PathVariable("id") Long id) {
|
||||
return ApiResponse.ok(roleService.getRoleMenuIds(id));
|
||||
}
|
||||
|
||||
public record UpdateRoleMenusRequest(List<Long> menuIds) {
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/menus")
|
||||
@PreAuthorize("hasRole('ADMIN') or hasAuthority('system:role:assign')")
|
||||
public ApiResponse<Void> updateRoleMenus(@PathVariable("id") Long id, @RequestBody UpdateRoleMenusRequest req) {
|
||||
roleService.updateRolePermissions(id, req.menuIds());
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<IPage<UserListItem>> 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.<UserListItem>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<String> 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<Void> 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<Void> 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<Void> delete(@PathVariable Long id) {
|
||||
userService.deleteUser(id);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/roles")
|
||||
@PreAuthorize("hasRole('ADMIN') or hasAuthority('system:user:view')")
|
||||
public ApiResponse<List<Long>> roleIds(@PathVariable("id") long userId) {
|
||||
return ApiResponse.ok(userService.getUserRoleIds(userId));
|
||||
}
|
||||
|
||||
public record UpdateRolesRequest(List<Long> roleIds) {
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/roles")
|
||||
@PreAuthorize("hasRole('ADMIN') or hasAuthority('system:user:assign')")
|
||||
public ApiResponse<Void> updateRoles(@PathVariable("id") long userId, @RequestBody UpdateRolesRequest req) {
|
||||
userService.updateUserRoles(userId, req == null ? List.of() : req.roleIds());
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
@@ -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<String> roles
|
||||
) {
|
||||
}
|
||||
|
||||
public record MeResponse(
|
||||
long userId,
|
||||
String username,
|
||||
String nickname,
|
||||
String avatarPath,
|
||||
String phone,
|
||||
String email,
|
||||
Integer gender,
|
||||
List<String> 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
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MenuDto> children = new ArrayList<>();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<SysMenu> {
|
||||
@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<SysMenu> 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<SysMenu> 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<String> 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<String> selectAllPerms();
|
||||
}
|
||||
|
||||
@@ -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<SysRole> {
|
||||
@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<String> 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<SysRole> selectRolesByUserId(@Param("userId") long userId);
|
||||
}
|
||||
|
||||
@@ -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<SysRoleMenu> {
|
||||
}
|
||||
@@ -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<SysUser> {
|
||||
}
|
||||
|
||||
@@ -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<SysUserRole> {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.hertz.system.service;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public interface AuthzService {
|
||||
Set<String> loadAuthorities(long userId);
|
||||
}
|
||||
|
||||
@@ -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<MenuDto> getMenuTreeByUserId(long userId);
|
||||
|
||||
IPage<SysMenu> pageMenus(int page, int size, String keyword);
|
||||
|
||||
void saveMenu(SysMenu menu);
|
||||
|
||||
void updateMenu(SysMenu menu);
|
||||
|
||||
void deleteMenu(Long id);
|
||||
}
|
||||
|
||||
@@ -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<SysRole> listEnabledRoles();
|
||||
|
||||
IPage<SysRole> pageRoles(int page, int size, String keyword);
|
||||
|
||||
void saveRole(SysRole role);
|
||||
|
||||
void updateRole(SysRole role);
|
||||
|
||||
void deleteRole(Long id);
|
||||
|
||||
void updateRolePermissions(Long roleId, List<Long> menuIds);
|
||||
|
||||
List<Long> getRoleMenuIds(Long roleId);
|
||||
}
|
||||
|
||||
@@ -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<SysUser> 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<Long> roleIds);
|
||||
|
||||
List<Long> getUserRoleIds(long userId);
|
||||
|
||||
List<com.hertz.system.entity.SysRole> getUserRoles(long userId);
|
||||
}
|
||||
|
||||
@@ -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<String> loadAuthorities(long userId) {
|
||||
var roleKeys = roleMapper.selectRoleKeysByUserId(userId);
|
||||
var authorities = new HashSet<String>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MenuDto> 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<Long, MenuDto>();
|
||||
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<MenuDto>();
|
||||
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<MenuDto> 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<SysMenu> pageMenus(int page, int size, String keyword) {
|
||||
var wrapper = new LambdaQueryWrapper<SysMenu>().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<SysMenu>().eq(SysMenu::getParentId, id)) > 0) {
|
||||
throw new BusinessException(400, "Has sub-menus, cannot delete");
|
||||
}
|
||||
menuMapper.deleteById(id);
|
||||
roleMenuMapper.delete(new LambdaQueryWrapper<SysRoleMenu>().eq(SysRoleMenu::getMenuId, id));
|
||||
}
|
||||
|
||||
private void sortRecursively(List<MenuDto> nodes, Comparator<MenuDto> comparator) {
|
||||
nodes.sort(comparator);
|
||||
for (var n : nodes) {
|
||||
if (n.getChildren() != null && !n.getChildren().isEmpty()) {
|
||||
sortRecursively(n.getChildren(), comparator);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SysRole> listEnabledRoles() {
|
||||
return roleMapper.selectList(new LambdaQueryWrapper<SysRole>()
|
||||
.eq(SysRole::getStatus, 1)
|
||||
.orderByAsc(SysRole::getId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<SysRole> pageRoles(int page, int size, String keyword) {
|
||||
var wrapper = new LambdaQueryWrapper<SysRole>().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<SysRole>().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<SysRoleMenu>().eq(SysRoleMenu::getRoleId, id));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void updateRolePermissions(Long roleId, List<Long> menuIds) {
|
||||
roleMenuMapper.delete(new LambdaQueryWrapper<SysRoleMenu>().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<Long> getRoleMenuIds(Long roleId) {
|
||||
return roleMenuMapper.selectList(new LambdaQueryWrapper<SysRoleMenu>().eq(SysRoleMenu::getRoleId, roleId))
|
||||
.stream().map(SysRoleMenu::getMenuId).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SysUser>()
|
||||
.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<SysUser>()
|
||||
.eq(SysUser::getUsername, username)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<SysUser> pageUsers(int page, int size, String keyword) {
|
||||
var wrapper = new LambdaQueryWrapper<SysUser>()
|
||||
.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<SysUser>().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<SysUserRole>().eq(SysUserRole::getUserId, id));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void updateUserRoles(long userId, List<Long> roleIds) {
|
||||
userRoleMapper.delete(new LambdaQueryWrapper<SysUserRole>().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<Long> getUserRoleIds(long userId) {
|
||||
var list = userRoleMapper.selectList(new LambdaQueryWrapper<SysUserRole>().eq(SysUserRole::getUserId, userId));
|
||||
return list.stream().map(SysUserRole::getRoleId).filter(Objects::nonNull).distinct().toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<com.hertz.system.entity.SysRole> getUserRoles(long userId) {
|
||||
return roleMapper.selectRolesByUserId(userId);
|
||||
}
|
||||
}
|
||||
|
||||
30
hertz_springboot/src/main/resources/application.yml
Normal file
30
hertz_springboot/src/main/resources/application.yml
Normal file
@@ -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/
|
||||
107
hertz_springboot/src/main/resources/schema.sql
Normal file
107
hertz_springboot/src/main/resources/schema.sql
Normal file
@@ -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);
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user