diff --git a/backend/pom.xml b/backend/pom.xml
index 8274d359..4f433a24 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -24,6 +24,8 @@
1.18.30
6.2.0
0.12.3
+ 2.2.0
+ 5.8.23
@@ -140,6 +142,18 @@
${jjwt.version}
runtime
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ ${springdoc.version}
+
+
+
+ cn.hutool
+ hutool-all
+ ${hutool.version}
+
diff --git a/backend/src/main/java/com/qqchen/deploy/backend/api/UserApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/api/UserApiController.java
index 34a9ca91..bb6f8c2d 100644
--- a/backend/src/main/java/com/qqchen/deploy/backend/api/UserApiController.java
+++ b/backend/src/main/java/com/qqchen/deploy/backend/api/UserApiController.java
@@ -4,12 +4,15 @@ import com.qqchen.deploy.backend.common.controller.BaseController;
import com.qqchen.deploy.backend.common.api.Response;
import com.qqchen.deploy.backend.common.enums.ResponseCode;
import com.qqchen.deploy.backend.converter.UserConverter;
+import com.qqchen.deploy.backend.dto.request.LoginRequest;
+import com.qqchen.deploy.backend.dto.response.LoginResponse;
import com.qqchen.deploy.backend.entity.User;
import com.qqchen.deploy.backend.dto.query.UserQuery;
import com.qqchen.deploy.backend.dto.request.UserRequest;
import com.qqchen.deploy.backend.dto.response.UserResponse;
import com.qqchen.deploy.backend.dto.request.UserRegisterRequest;
import com.qqchen.deploy.backend.service.IUserService;
+import io.swagger.v3.oas.annotations.Operation;
import org.springframework.data.domain.Page;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
@@ -29,13 +32,18 @@ public class UserApiController extends BaseController login(@Validated @RequestBody LoginRequest request) {
+ return Response.success(userService.login(request));
+ }
+
@PostMapping("/register")
public Response register(@Validated @RequestBody UserRegisterRequest request) {
// 基础的注册逻辑
if (!request.getPassword().equals(request.getConfirmPassword())) {
return Response.error(ResponseCode.USER_NOT_FOUND);
}
-
User user = converter.toEntity(request);
User savedUser = userService.register(user);
return Response.success(converter.toResponse(savedUser));
diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/annotation/TenantFilter.java b/backend/src/main/java/com/qqchen/deploy/backend/common/annotation/TenantFilter.java
new file mode 100644
index 00000000..f725e0e7
--- /dev/null
+++ b/backend/src/main/java/com/qqchen/deploy/backend/common/annotation/TenantFilter.java
@@ -0,0 +1,17 @@
+package com.qqchen.deploy.backend.common.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface TenantFilter {
+ /**
+ * 是否启用租户过滤
+ */
+ boolean value() default true;
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/config/JacksonConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/common/config/JacksonConfig.java
new file mode 100644
index 00000000..ea732f76
--- /dev/null
+++ b/backend/src/main/java/com/qqchen/deploy/backend/common/config/JacksonConfig.java
@@ -0,0 +1,33 @@
+package com.qqchen.deploy.backend.common.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+@Configuration
+public class JacksonConfig {
+
+ private static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
+
+ @Bean
+ public ObjectMapper objectMapper() {
+ ObjectMapper objectMapper = new ObjectMapper();
+ JavaTimeModule javaTimeModule = new JavaTimeModule();
+
+ javaTimeModule.addSerializer(LocalDateTime.class,
+ new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_PATTERN)));
+
+ javaTimeModule.addDeserializer(LocalDateTime.class,
+ new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_TIME_PATTERN)));
+
+ objectMapper.registerModule(javaTimeModule);
+
+ return objectMapper;
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/context/TenantContext.java b/backend/src/main/java/com/qqchen/deploy/backend/common/context/TenantContext.java
new file mode 100644
index 00000000..9dd0d62c
--- /dev/null
+++ b/backend/src/main/java/com/qqchen/deploy/backend/common/context/TenantContext.java
@@ -0,0 +1,22 @@
+package com.qqchen.deploy.backend.common.context;
+
+public class TenantContext {
+ private static final ThreadLocal CURRENT_TENANT = new ThreadLocal<>();
+
+ public static void setTenantId(String tenantId) {
+ CURRENT_TENANT.set(tenantId);
+ }
+
+ public static String getTenantId() {
+ return CURRENT_TENANT.get();
+ }
+
+ public static void clear() {
+ CURRENT_TENANT.remove();
+ }
+
+ public static boolean isSystemTenant() {
+ String tenantId = getTenantId();
+ return tenantId == null || "0".equals(tenantId);
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/enums/ResponseCode.java b/backend/src/main/java/com/qqchen/deploy/backend/common/enums/ResponseCode.java
index 1049a7e1..66220825 100644
--- a/backend/src/main/java/com/qqchen/deploy/backend/common/enums/ResponseCode.java
+++ b/backend/src/main/java/com/qqchen/deploy/backend/common/enums/ResponseCode.java
@@ -13,9 +13,10 @@ public enum ResponseCode {
CONFLICT(409, "response.conflict"),
// 业务错误码
- USER_NOT_FOUND(1001, "user.not.found"),
- USERNAME_EXISTS(1002, "user.username.exists"),
- EMAIL_EXISTS(1003, "user.email.exists");
+ TENANT_NOT_FOUND(1001, "tenant.not.found"),
+ USER_NOT_FOUND(2001, "user.not.found"),
+ USERNAME_EXISTS(2002, "user.username.exists"),
+ EMAIL_EXISTS(2003, "user.email.exists");
private final int code;
diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/event/DomainEvent.java b/backend/src/main/java/com/qqchen/deploy/backend/common/event/DomainEvent.java
index 86cc5318..843fb688 100644
--- a/backend/src/main/java/com/qqchen/deploy/backend/common/event/DomainEvent.java
+++ b/backend/src/main/java/com/qqchen/deploy/backend/common/event/DomainEvent.java
@@ -6,9 +6,11 @@ import java.time.LocalDateTime;
@Getter
public abstract class DomainEvent {
+
private final String eventId;
+
private final LocalDateTime occurredOn;
-
+
protected DomainEvent() {
this.eventId = java.util.UUID.randomUUID().toString();
this.occurredOn = LocalDateTime.now();
diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/exception/BusinessException.java b/backend/src/main/java/com/qqchen/deploy/backend/common/exception/BusinessException.java
new file mode 100644
index 00000000..a1621bb1
--- /dev/null
+++ b/backend/src/main/java/com/qqchen/deploy/backend/common/exception/BusinessException.java
@@ -0,0 +1,22 @@
+package com.qqchen.deploy.backend.common.exception;
+
+import com.qqchen.deploy.backend.common.enums.ResponseCode;
+import com.qqchen.deploy.backend.common.utils.MessageUtils;
+import lombok.Getter;
+
+@Getter
+public class BusinessException extends RuntimeException {
+
+ private final ResponseCode errorCode;
+
+ public BusinessException(ResponseCode errorCode) {
+ super(MessageUtils.getMessage(errorCode.getMessageKey()));
+ this.errorCode = errorCode;
+ }
+
+
+ public BusinessException(ResponseCode errorCode, Throwable cause) {
+ super(MessageUtils.getMessage(errorCode.getMessageKey()), cause);
+ this.errorCode = errorCode;
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/qqchen/deploy/backend/common/exception/GlobalExceptionHandler.java
new file mode 100644
index 00000000..e0d6b388
--- /dev/null
+++ b/backend/src/main/java/com/qqchen/deploy/backend/common/exception/GlobalExceptionHandler.java
@@ -0,0 +1,32 @@
+package com.qqchen.deploy.backend.common.exception;
+
+import com.qqchen.deploy.backend.common.api.Response;
+import com.qqchen.deploy.backend.common.enums.ResponseCode;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@Slf4j
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+// @ExceptionHandler(AuthenticationException.class)
+// public ApiResult handleAuthenticationException(AuthenticationException e) {
+// log.error("认证异常", e);
+// if (e instanceof BadCredentialsException) {
+// return ApiResult.error(401, "用户名或密码错误");
+// }
+// return ApiResult.error(401, "认证失败:" + e.getMessage());
+// }
+
+ @ExceptionHandler(BusinessException.class)
+ public Response> handleApiException(BusinessException e) {
+ return Response.error(e.getErrorCode());
+ }
+
+ @ExceptionHandler(Exception.class)
+ public Response> handleException(Exception e) {
+ log.error("系统错误", e);
+ return Response.error(ResponseCode.ERROR);
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/filter/TenantFilter.java b/backend/src/main/java/com/qqchen/deploy/backend/common/filter/TenantFilter.java
new file mode 100644
index 00000000..6bc77fe6
--- /dev/null
+++ b/backend/src/main/java/com/qqchen/deploy/backend/common/filter/TenantFilter.java
@@ -0,0 +1,34 @@
+package com.qqchen.deploy.backend.common.filter;
+
+import com.qqchen.deploy.backend.common.context.TenantContext;
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+@Component
+@Order(1)
+public class TenantFilter implements Filter {
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest req = (HttpServletRequest) request;
+ try {
+ // 从请求头中获取租户ID
+ String tenantId = req.getHeader("X-Tenant-ID");
+ if (tenantId != null) {
+ TenantContext.setTenantId(tenantId);
+ }
+ chain.doFilter(request, response);
+ } finally {
+ // 清理租户上下文
+ TenantContext.clear();
+ }
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/interceptor/TenantInterceptor.java b/backend/src/main/java/com/qqchen/deploy/backend/common/interceptor/TenantInterceptor.java
new file mode 100644
index 00000000..37daf740
--- /dev/null
+++ b/backend/src/main/java/com/qqchen/deploy/backend/common/interceptor/TenantInterceptor.java
@@ -0,0 +1,54 @@
+package com.qqchen.deploy.backend.common.interceptor;
+
+import com.qqchen.deploy.backend.common.context.TenantContext;
+import com.qqchen.deploy.backend.common.enums.ResponseCode;
+import com.qqchen.deploy.backend.common.exception.BusinessException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import java.util.Arrays;
+import java.util.List;
+
+@Component
+public class TenantInterceptor implements HandlerInterceptor {
+
+ private final AntPathMatcher pathMatcher = new AntPathMatcher();
+
+ // 不需要进行租户隔离的路径
+ private final List excludePaths = Arrays.asList(
+ "/api/v1/users/login",
+ "/api/system/tenants/list",
+ "/swagger-ui/**",
+ "/v3/api-docs/**"
+ );
+
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
+ String path = request.getRequestURI();
+ // 如果是白名单路径,不进行租户校验
+ if (isExcludePath(path)) {
+ TenantContext.clear();
+ return true;
+ }
+ // 获取租户ID
+ String tenantId = request.getHeader("X-Tenant-ID");
+ if (tenantId == null || tenantId.trim().isEmpty()) {
+ throw new BusinessException(ResponseCode.TENANT_NOT_FOUND);
+ }
+ TenantContext.setTenantId(tenantId);
+ return true;
+ }
+
+ @Override
+ public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
+ TenantContext.clear();
+ }
+
+ private boolean isExcludePath(String path) {
+ return excludePaths.stream()
+ .anyMatch(pattern -> pathMatcher.match(pattern, path));
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/security/config/SecurityConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/common/security/config/SecurityConfig.java
index a615db86..bd6661a2 100644
--- a/backend/src/main/java/com/qqchen/deploy/backend/common/security/config/SecurityConfig.java
+++ b/backend/src/main/java/com/qqchen/deploy/backend/common/security/config/SecurityConfig.java
@@ -1,5 +1,8 @@
package com.qqchen.deploy.backend.common.security.config;
+import com.qqchen.deploy.backend.common.security.filter.JwtAuthenticationFilter;
+import com.qqchen.deploy.backend.common.security.handler.CustomAuthenticationEntryPoint;
+import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
@@ -14,6 +17,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
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;
@@ -22,8 +26,13 @@ import java.util.Arrays;
@Configuration
@EnableWebSecurity
+@RequiredArgsConstructor
public class SecurityConfig {
+ private final JwtAuthenticationFilter jwtAuthenticationFilter;
+
+ private final CustomAuthenticationEntryPoint authenticationEntryPoint;
+
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
@@ -31,10 +40,14 @@ public class SecurityConfig {
.csrf(csrf -> csrf.disable()) // 禁用CSRF
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 禁用session
+ .exceptionHandling(exceptions -> exceptions
+ .authenticationEntryPoint(authenticationEntryPoint)
+ )
.formLogin(form -> form.disable()) // 禁用form登录
.httpBasic(basic -> basic.disable()) // 禁用basic认证
.logout(logout -> logout.disable()) // 禁用logout
- .anonymous(anonymous -> {}) // 允许匿名访问
+ .anonymous(anonymous -> {
+ }) // 允许匿名访问
.authorizeHttpRequests(auth -> auth
// 公开接口
// .requestMatchers("/api/v1/users/register").permitAll()
@@ -47,7 +60,8 @@ public class SecurityConfig {
.anyRequest().permitAll()
// 生产环境建议改为需要认证
//.anyRequest().authenticated()
- );
+
+ ).addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/security/filter/JwtAuthenticationFilter.java b/backend/src/main/java/com/qqchen/deploy/backend/common/security/filter/JwtAuthenticationFilter.java
index 20e29325..35f79b4d 100644
--- a/backend/src/main/java/com/qqchen/deploy/backend/common/security/filter/JwtAuthenticationFilter.java
+++ b/backend/src/main/java/com/qqchen/deploy/backend/common/security/filter/JwtAuthenticationFilter.java
@@ -16,6 +16,8 @@ import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
@Slf4j
@Component
@@ -23,13 +25,25 @@ import java.io.IOException;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenUtil jwtTokenUtil;
-
private final UserDetailsService userDetailsService;
+ // 添加白名单路径
+ private static final List WHITELIST = Arrays.asList(
+ "/api/v1/users/login",
+ "/api/v1/users/register",
+ "/swagger-ui/**",
+ "/v3/api-docs/**"
+ );
+
+ @Override
+ protected boolean shouldNotFilter(HttpServletRequest request) {
+ String path = request.getServletPath();
+ return WHITELIST.stream().anyMatch(path::startsWith);
+ }
+
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
-
try {
String authHeader = request.getHeader("Authorization");
String token = null;
@@ -39,10 +53,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
token = authHeader.substring(7);
username = jwtTokenUtil.getUsernameFromToken(token);
}
-
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
-
if (jwtTokenUtil.validateToken(token, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
@@ -50,10 +62,10 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
+ chain.doFilter(request, response);
} catch (Exception e) {
log.error("Cannot set user authentication", e);
+ chain.doFilter(request, response);
}
-
- chain.doFilter(request, response);
}
}
\ No newline at end of file
diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/security/handler/CustomAuthenticationEntryPoint.java b/backend/src/main/java/com/qqchen/deploy/backend/common/security/handler/CustomAuthenticationEntryPoint.java
new file mode 100644
index 00000000..91505161
--- /dev/null
+++ b/backend/src/main/java/com/qqchen/deploy/backend/common/security/handler/CustomAuthenticationEntryPoint.java
@@ -0,0 +1,39 @@
+package com.qqchen.deploy.backend.common.security.handler;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.qqchen.deploy.backend.common.api.Response;
+import com.qqchen.deploy.backend.common.enums.ResponseCode;
+import com.qqchen.deploy.backend.common.utils.MessageUtils;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.http.MediaType;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.InternalAuthenticationServiceException;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+@Component
+public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ @Override
+ public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
+ response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+ response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+ Response> result;
+ if (authException instanceof BadCredentialsException) {
+ result = Response.error(ResponseCode.UNAUTHORIZED, MessageUtils.getMessage(ResponseCode.SUCCESS.getMessageKey()));
+ } else if (authException instanceof InternalAuthenticationServiceException) {
+ result = Response.error(ResponseCode.USER_NOT_FOUND, MessageUtils.getMessage(ResponseCode.USER_NOT_FOUND.getMessageKey()));
+ } else {
+ result = Response.error(ResponseCode.UNAUTHORIZED, MessageUtils.getMessage(ResponseCode.SUCCESS.getMessageKey()));
+ }
+
+ response.getWriter().write(objectMapper.writeValueAsString(result));
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/qqchen/deploy/backend/dto/request/LoginRequest.java b/backend/src/main/java/com/qqchen/deploy/backend/dto/request/LoginRequest.java
new file mode 100644
index 00000000..c095c577
--- /dev/null
+++ b/backend/src/main/java/com/qqchen/deploy/backend/dto/request/LoginRequest.java
@@ -0,0 +1,14 @@
+package com.qqchen.deploy.backend.dto.request;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class LoginRequest {
+
+ @NotBlank(message = "用户名不能为空")
+ private String username;
+
+ @NotBlank(message = "密码不能为空")
+ private String password;
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/qqchen/deploy/backend/dto/response/LoginResponse.java b/backend/src/main/java/com/qqchen/deploy/backend/dto/response/LoginResponse.java
new file mode 100644
index 00000000..8aa5937d
--- /dev/null
+++ b/backend/src/main/java/com/qqchen/deploy/backend/dto/response/LoginResponse.java
@@ -0,0 +1,12 @@
+package com.qqchen.deploy.backend.dto.response;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LoginResponse extends UserResponse {
+
+ private String token;
+
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/qqchen/deploy/backend/service/IUserService.java b/backend/src/main/java/com/qqchen/deploy/backend/service/IUserService.java
index f0b8b85f..de608ab9 100644
--- a/backend/src/main/java/com/qqchen/deploy/backend/service/IUserService.java
+++ b/backend/src/main/java/com/qqchen/deploy/backend/service/IUserService.java
@@ -1,6 +1,8 @@
package com.qqchen.deploy.backend.service;
import com.qqchen.deploy.backend.common.service.IBaseService;
+import com.qqchen.deploy.backend.dto.request.LoginRequest;
+import com.qqchen.deploy.backend.dto.response.LoginResponse;
import com.qqchen.deploy.backend.entity.User;
public interface IUserService extends IBaseService {
@@ -8,4 +10,6 @@ public interface IUserService extends IBaseService {
User findByUsername(String username);
boolean checkUsernameExists(String username);
boolean checkEmailExists(String email);
+
+ LoginResponse login(LoginRequest request);
}
\ No newline at end of file
diff --git a/backend/src/main/java/com/qqchen/deploy/backend/service/impl/CustomUserDetailsService.java b/backend/src/main/java/com/qqchen/deploy/backend/service/impl/CustomUserDetailsService.java
new file mode 100644
index 00000000..7896be87
--- /dev/null
+++ b/backend/src/main/java/com/qqchen/deploy/backend/service/impl/CustomUserDetailsService.java
@@ -0,0 +1,36 @@
+package com.qqchen.deploy.backend.service.impl;
+
+import com.qqchen.deploy.backend.entity.User;
+import com.qqchen.deploy.backend.repository.IUserRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Collections;
+
+@Service
+@RequiredArgsConstructor
+public class CustomUserDetailsService implements UserDetailsService {
+
+ private final IUserRepository userRepository; // 直接使用Repository
+
+ @Override
+ @Transactional(readOnly = true)
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ // 直接查询数据库,不要通过Controller或其他Service
+ return userRepository.findByUsernameAndDeletedFalse(username)
+ .map(this::createUserDetails)
+ .orElseThrow(() -> new UsernameNotFoundException("User not found"));
+ }
+
+ private UserDetails createUserDetails(User user) {
+ return org.springframework.security.core.userdetails.User
+ .withUsername(user.getUsername())
+ .password(user.getPassword())
+ .authorities(Collections.emptyList()) // 或者从数据库加载权限
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/qqchen/deploy/backend/service/impl/UserServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/service/impl/UserServiceImpl.java
index 3d679878..88edd9e1 100644
--- a/backend/src/main/java/com/qqchen/deploy/backend/service/impl/UserServiceImpl.java
+++ b/backend/src/main/java/com/qqchen/deploy/backend/service/impl/UserServiceImpl.java
@@ -1,19 +1,35 @@
package com.qqchen.deploy.backend.service.impl;
+import com.qqchen.deploy.backend.common.enums.ResponseCode;
+import com.qqchen.deploy.backend.common.exception.BusinessException;
+import com.qqchen.deploy.backend.common.security.util.JwtTokenUtil;
import com.qqchen.deploy.backend.common.service.impl.BaseServiceImpl;
+import com.qqchen.deploy.backend.dto.request.LoginRequest;
+import com.qqchen.deploy.backend.dto.response.LoginResponse;
import com.qqchen.deploy.backend.entity.User;
import com.qqchen.deploy.backend.repository.IUserRepository;
import com.qqchen.deploy.backend.service.IUserService;
-import jakarta.transaction.Transactional;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
@Service
-@Transactional
+@Slf4j
public class UserServiceImpl extends BaseServiceImpl implements IUserService {
private final IUserRepository userRepository;
+ @Resource
+ private AuthenticationManager authenticationManager;
+
+ @Resource
+ private JwtTokenUtil jwtTokenUtil;
+
@Autowired
public UserServiceImpl(IUserRepository userRepository) {
super(userRepository);
@@ -47,4 +63,24 @@ public class UserServiceImpl extends BaseServiceImpl implements IUse
return userRepository.existsByEmail(email);
}
+
+ @Override
+ public LoginResponse login(LoginRequest request) {
+ Authentication authentication = authenticationManager.authenticate(
+ new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
+ );
+ UserDetails userDetails = (UserDetails) authentication.getPrincipal();
+ String token = jwtTokenUtil.generateToken(userDetails);
+ User user = userRepository.findByUsernameAndDeletedFalse(userDetails.getUsername()).orElseThrow(() -> new BusinessException(ResponseCode.USER_NOT_FOUND));
+ LoginResponse response = new LoginResponse();
+ response.setId(user.getId());
+ response.setUsername(user.getUsername());
+ response.setNickname(user.getNickname());
+ response.setEmail(user.getEmail());
+ response.setPhone(user.getPhone());
+ response.setToken(token);
+ log.info("用户 {} ({}) 登录成功", user.getUsername(), user.getNickname());
+ return response;
+ }
+
}
\ No newline at end of file
diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml
index 7df400bd..ffd9f797 100644
--- a/backend/src/main/resources/application.yml
+++ b/backend/src/main/resources/application.yml
@@ -19,7 +19,7 @@ spring:
jdbc:
time_zone: Asia/Shanghai
mvc:
- log-request-details: true # \u6253\u5370\u8BF7\u6C42\u8BE6\u60C5
+ log-request-details: true
messages:
basename: messages
encoding: UTF-8
@@ -35,8 +35,8 @@ logging:
level:
org.springframework.web: DEBUG
org.springframework.context.i18n: DEBUG
- org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping: TRACE # \u6253\u5370\u6240\u6709\u6CE8\u518C\u7684\u63A5\u53E3\u8DEF\u5F84
- org.hibernate.type.descriptor.sql.BasicBinder: TRACE # \u663E\u793ASQL\u53C2\u6570
+ org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping: TRACE
+ org.hibernate.type.descriptor.sql.BasicBinder: TRACE
org.hibernate.type.descriptor.sql: TRACE
com.qqchen.deploy.backend.common.utils.EntityPathResolver: DEBUG
jwt:
diff --git a/backend/src/main/resources/messages.properties b/backend/src/main/resources/messages.properties
index 251a9a10..51c98249 100644
--- a/backend/src/main/resources/messages.properties
+++ b/backend/src/main/resources/messages.properties
@@ -7,6 +7,9 @@ response.forbidden=\u7981\u6B62\u8BBF\u95EE
response.not.found=\u8D44\u6E90\u672A\u627E\u5230
response.conflict=\u8D44\u6E90\u51B2\u7A81
+
+tenant.not.found=\u79DF\u6237\u4E0D\u5B58\u5728
+
user.not.found=\u7528\u6237\u4E0D\u5B58\u5728
user.username.exists=\u7528\u6237\u540D\u5DF2\u5B58\u5728
user.email.exists=\u90AE\u7BB1\u5DF2\u5B58\u5728
\ No newline at end of file
diff --git a/backend/src/main/resources/messages_en.properties b/backend/src/main/resources/messages_en.properties
index 899ab0e9..5ded67ea 100644
--- a/backend/src/main/resources/messages_en.properties
+++ b/backend/src/main/resources/messages_en.properties
@@ -7,6 +7,9 @@ response.forbidden=Forbidden
response.not.found=Resource Not Found
response.conflict=Resource Conflict
+
+tenant.not.found=Tenant not found
+
user.not.found=User not found
user.username.exists=Username already exists
user.email.exists=Email already exists
\ No newline at end of file
diff --git a/backend/src/main/resources/messages_zh_CN.properties b/backend/src/main/resources/messages_zh_CN.properties
index 251a9a10..fb120b64 100644
--- a/backend/src/main/resources/messages_zh_CN.properties
+++ b/backend/src/main/resources/messages_zh_CN.properties
@@ -7,6 +7,8 @@ response.forbidden=\u7981\u6B62\u8BBF\u95EE
response.not.found=\u8D44\u6E90\u672A\u627E\u5230
response.conflict=\u8D44\u6E90\u51B2\u7A81
+tenant.not.found=\u79DF\u6237\u4E0D\u5B58\u5728
+
user.not.found=\u7528\u6237\u4E0D\u5B58\u5728
user.username.exists=\u7528\u6237\u540D\u5DF2\u5B58\u5728
user.email.exists=\u90AE\u7BB1\u5DF2\u5B58\u5728
\ No newline at end of file