diff --git a/src/main/java/com/webgame/webgamebackend/common/dto/ApiResponse.java b/src/main/java/com/webgame/webgamebackend/common/dto/ApiResponse.java index c8e207a..5c16fa0 100644 --- a/src/main/java/com/webgame/webgamebackend/common/dto/ApiResponse.java +++ b/src/main/java/com/webgame/webgamebackend/common/dto/ApiResponse.java @@ -9,10 +9,6 @@ public record ApiResponse(int code, String message, T data) { /** * 成功响应 - * - * @param data 响应数据 - * @param 数据类型 - * @return ApiResponse */ public static ApiResponse ok(T data) { return new ApiResponse<>(0, "success", data); @@ -20,11 +16,6 @@ public record ApiResponse(int code, String message, T data) { /** * 失败响应 - * - * @param code 业务状态码 - * @param message 错误消息 - * @param 数据类型 - * @return ApiResponse */ public static ApiResponse fail(int code, String message) { return new ApiResponse<>(code, message, null); diff --git a/src/main/java/com/webgame/webgamebackend/common/dto/account/LoginRequest.java b/src/main/java/com/webgame/webgamebackend/common/dto/account/LoginRequest.java index 33606bf..ee71a85 100644 --- a/src/main/java/com/webgame/webgamebackend/common/dto/account/LoginRequest.java +++ b/src/main/java/com/webgame/webgamebackend/common/dto/account/LoginRequest.java @@ -2,25 +2,18 @@ package com.webgame.webgamebackend.common.dto.account; import com.webgame.webgamebackend.common.dto.base.BaseVo; import jakarta.validation.constraints.NotBlank; -import lombok.Data; -import lombok.EqualsAndHashCode; +import jakarta.validation.constraints.NotNull; /** * 登录请求 */ -@Data -@EqualsAndHashCode(callSuper = true) -public class LoginRequest extends BaseVo { +public record LoginRequest( + @NotNull(message = "用户名不能为null") + @NotBlank(message = "用户名不能为空") + String username, - /** - * 用户名 - */ - @NotBlank(message = "用户名不能为空") - private String username; - - /** - * 密码 - */ - @NotBlank(message = "密码不能为空") - private String password; + @NotNull(message = "密码不能为null") + @NotBlank(message = "密码不能为空") + String password +) implements BaseVo { } diff --git a/src/main/java/com/webgame/webgamebackend/common/dto/account/LoginResponse.java b/src/main/java/com/webgame/webgamebackend/common/dto/account/LoginResponse.java index 654ad18..1955491 100644 --- a/src/main/java/com/webgame/webgamebackend/common/dto/account/LoginResponse.java +++ b/src/main/java/com/webgame/webgamebackend/common/dto/account/LoginResponse.java @@ -3,4 +3,5 @@ package com.webgame.webgamebackend.common.dto.account; /** * 登录/注册响应 */ -public record LoginResponse(String token) {} +public record LoginResponse(String token) { +} diff --git a/src/main/java/com/webgame/webgamebackend/common/dto/account/RegisterRequest.java b/src/main/java/com/webgame/webgamebackend/common/dto/account/RegisterRequest.java index a5c187f..209a036 100644 --- a/src/main/java/com/webgame/webgamebackend/common/dto/account/RegisterRequest.java +++ b/src/main/java/com/webgame/webgamebackend/common/dto/account/RegisterRequest.java @@ -2,28 +2,21 @@ package com.webgame.webgamebackend.common.dto.account; import com.webgame.webgamebackend.common.dto.base.BaseVo; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; -import lombok.Data; -import lombok.EqualsAndHashCode; /** * 注册请求 */ -@Data -@EqualsAndHashCode(callSuper = true) -public class RegisterRequest extends BaseVo { +public record RegisterRequest( + @NotNull(message = "用户名不能为null") + @NotBlank(message = "用户名不能为空") + @Size(min = 3, max = 32, message = "用户名长度3-32位") + String username, - /** - * 用户名 - */ - @NotBlank(message = "用户名不能为空") - @Size(min = 3, max = 32, message = "用户名长度3-32位") - private String username; - - /** - * 密码 - */ - @NotBlank(message = "密码不能为空") - @Size(min = 6, max = 64, message = "密码长度6-64位") - private String password; + @NotNull(message = "密码不能为null") + @NotBlank(message = "密码不能为空") + @Size(min = 6, max = 64, message = "密码长度6-64位") + String password +) implements BaseVo { } diff --git a/src/main/java/com/webgame/webgamebackend/common/dto/base/BaseVo.java b/src/main/java/com/webgame/webgamebackend/common/dto/base/BaseVo.java index b33a320..0316936 100644 --- a/src/main/java/com/webgame/webgamebackend/common/dto/base/BaseVo.java +++ b/src/main/java/com/webgame/webgamebackend/common/dto/base/BaseVo.java @@ -3,7 +3,7 @@ package com.webgame.webgamebackend.common.dto.base; import java.io.Serializable; /** - * 请求参数基类,所有入参 VO 均继承此类 + * 入参 VO 标记接口,所有请求参数 record 均实现此接口 */ -public abstract class BaseVo implements Serializable { +public interface BaseVo extends Serializable { } diff --git a/src/main/java/com/webgame/webgamebackend/common/dto/user/UserInfoResponse.java b/src/main/java/com/webgame/webgamebackend/common/dto/user/UserInfoResponse.java new file mode 100644 index 0000000..2ec9417 --- /dev/null +++ b/src/main/java/com/webgame/webgamebackend/common/dto/user/UserInfoResponse.java @@ -0,0 +1,31 @@ +package com.webgame.webgamebackend.common.dto.user; + +import com.webgame.webgamebackend.entities.UserEntity; + +import java.time.LocalDate; + +/** + * 用户信息响应 + */ +public record UserInfoResponse( + String nickName, + String avatar, + String signature, + Integer age, + Integer sex, + LocalDate birthday +) { + /** + * 从实体转换 + */ + public static UserInfoResponse from(UserEntity user) { + return new UserInfoResponse( + user.getNickName(), + user.getAvatar(), + user.getSignature(), + user.getAge(), + user.getSex(), + user.getBirthday() + ); + } +} diff --git a/src/main/java/com/webgame/webgamebackend/common/exception/ErrorCode.java b/src/main/java/com/webgame/webgamebackend/common/exception/ErrorCode.java index 97f5ed1..fc41a76 100644 --- a/src/main/java/com/webgame/webgamebackend/common/exception/ErrorCode.java +++ b/src/main/java/com/webgame/webgamebackend/common/exception/ErrorCode.java @@ -16,6 +16,9 @@ public enum ErrorCode { // ========== 账号模块 ========== USERNAME_EXISTS(1001, "用户名已存在"), BAD_CREDENTIALS(1002, "用户名或密码错误"), + + // ========== 用户模块 ========== + USER_NOT_FOUND(2001, "用户不存在"), ; private final int code; diff --git a/src/main/java/com/webgame/webgamebackend/common/handler/GlobalExceptionHandler.java b/src/main/java/com/webgame/webgamebackend/common/handler/GlobalExceptionHandler.java index cb54303..a93fc03 100644 --- a/src/main/java/com/webgame/webgamebackend/common/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/webgame/webgamebackend/common/handler/GlobalExceptionHandler.java @@ -1,12 +1,19 @@ package com.webgame.webgamebackend.common.handler; +import cn.dev33.satoken.exception.NotLoginException; +import cn.dev33.satoken.exception.NotPermissionException; +import cn.dev33.satoken.exception.NotRoleException; import com.webgame.webgamebackend.common.dto.ApiResponse; import com.webgame.webgamebackend.common.exception.BusinessException; import com.webgame.webgamebackend.common.exception.ErrorCode; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.resource.NoResourceFoundException; /** * 全局异常处理,统一返回 ApiResponse 格式 @@ -15,6 +22,8 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class GlobalExceptionHandler { + // ==================== 业务异常 ==================== + /** * 处理业务异常 */ @@ -24,18 +33,89 @@ public class GlobalExceptionHandler { return ApiResponse.fail(ex.getCode(), ex.getMessage()); } + // ==================== 参数校验异常 ==================== + /** - * 处理参数校验异常 + * 处理 @Valid 参数校验失败,合并所有字段的错误消息 */ @ExceptionHandler(MethodArgumentNotValidException.class) public ApiResponse handleValidation(MethodArgumentNotValidException ex) { String message = ex.getBindingResult().getFieldErrors().stream() - .findFirst() - .map(e -> e.getDefaultMessage()) + .map(e -> e.getField() + " " + e.getDefaultMessage()) + .reduce((a, b) -> a + "; " + b) .orElse(ErrorCode.BAD_REQUEST.getDefaultMessage()); return ApiResponse.fail(ErrorCode.BAD_REQUEST.getCode(), message); } + // ==================== Sa-Token 鉴权异常 ==================== + + /** + * 未登录 / token 无效 + */ + @ExceptionHandler(NotLoginException.class) + public ApiResponse handleNotLogin(NotLoginException ex) { + log.warn("未登录: {}", ex.getMessage()); + return ApiResponse.fail(401, "请先登录"); + } + + /** + * 无权限 + */ + @ExceptionHandler(NotPermissionException.class) + public ApiResponse handleNotPermission(NotPermissionException ex) { + log.warn("无权限: {}", ex.getMessage()); + return ApiResponse.fail(403, "无权限"); + } + + /** + * 无角色 + */ + @ExceptionHandler(NotRoleException.class) + public ApiResponse handleNotRole(NotRoleException ex) { + log.warn("无角色: {}", ex.getMessage()); + return ApiResponse.fail(403, "无权限"); + } + + // ==================== Spring MVC 异常 ==================== + + /** + * 请求方法不支持(如 GET 访问 POST 接口) + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ApiResponse handleMethodNotSupported(HttpRequestMethodNotSupportedException ex) { + log.warn("请求方法不支持: {}", ex.getMessage()); + return ApiResponse.fail(405, "请求方法不支持"); + } + + /** + * 请求体不可读(如 JSON 格式错误) + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + public ApiResponse handleMessageNotReadable(HttpMessageNotReadableException ex) { + log.warn("请求体解析失败: {}", ex.getMessage()); + return ApiResponse.fail(ErrorCode.BAD_REQUEST.getCode(), "请求体格式错误"); + } + + /** + * 路径参数类型不匹配 + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ApiResponse handleTypeMismatch(MethodArgumentTypeMismatchException ex) { + log.warn("参数类型不匹配: {}", ex.getMessage()); + return ApiResponse.fail(ErrorCode.BAD_REQUEST.getCode(), "参数类型错误"); + } + + /** + * 资源不存在(404) + */ + @ExceptionHandler(NoResourceFoundException.class) + public ApiResponse handleNoResourceFound(NoResourceFoundException ex) { + log.warn("资源不存在: {}", ex.getMessage()); + return ApiResponse.fail(404, "资源不存在"); + } + + // ==================== 兜底 ==================== + /** * 处理其他未捕获异常 */ diff --git a/src/main/java/com/webgame/webgamebackend/controller/UserController.java b/src/main/java/com/webgame/webgamebackend/controller/UserController.java new file mode 100644 index 0000000..a2d5963 --- /dev/null +++ b/src/main/java/com/webgame/webgamebackend/controller/UserController.java @@ -0,0 +1,31 @@ +package com.webgame.webgamebackend.controller; + +import com.webgame.webgamebackend.common.dto.ApiResponse; +import com.webgame.webgamebackend.common.dto.user.UserInfoResponse; +import com.webgame.webgamebackend.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 用户控制器 + */ +@Validated +@RestController +@RequestMapping("/user") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + /** + * 获取当前登录用户信息 + */ + @GetMapping("/info") + public ApiResponse info() { + UserInfoResponse result = userService.getCurrentUserInfo(); + return ApiResponse.ok(result); + } +} diff --git a/src/main/java/com/webgame/webgamebackend/repository/UserRepository.java b/src/main/java/com/webgame/webgamebackend/repository/UserRepository.java index 4773ccf..0b4e588 100644 --- a/src/main/java/com/webgame/webgamebackend/repository/UserRepository.java +++ b/src/main/java/com/webgame/webgamebackend/repository/UserRepository.java @@ -3,8 +3,15 @@ package com.webgame.webgamebackend.repository; import com.webgame.webgamebackend.entities.UserEntity; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + /** * 用户 Repository */ public interface UserRepository extends JpaRepository { + + /** + * 根据账号 ID 查询用户 + */ + Optional findByAccountId(String accountId); } diff --git a/src/main/java/com/webgame/webgamebackend/service/UserService.java b/src/main/java/com/webgame/webgamebackend/service/UserService.java new file mode 100644 index 0000000..32a8df5 --- /dev/null +++ b/src/main/java/com/webgame/webgamebackend/service/UserService.java @@ -0,0 +1,16 @@ +package com.webgame.webgamebackend.service; + +import com.webgame.webgamebackend.common.dto.user.UserInfoResponse; + +/** + * 用户服务接口 + */ +public interface UserService { + + /** + * 获取当前登录用户的资料 + * + * @return 用户信息 + */ + UserInfoResponse getCurrentUserInfo(); +} diff --git a/src/main/java/com/webgame/webgamebackend/service/impl/AccountServiceImpl.java b/src/main/java/com/webgame/webgamebackend/service/impl/AccountServiceImpl.java index 3e43caf..ff868c1 100644 --- a/src/main/java/com/webgame/webgamebackend/service/impl/AccountServiceImpl.java +++ b/src/main/java/com/webgame/webgamebackend/service/impl/AccountServiceImpl.java @@ -33,15 +33,15 @@ public class AccountServiceImpl implements AccountService { @Override public LoginResponse login(LoginRequest request) { - AccountEntity account = accountRepository.findByUsername(request.getUsername()) + AccountEntity account = accountRepository.findByUsername(request.username()) .orElseThrow(() -> new BusinessException(ErrorCode.BAD_CREDENTIALS)); - if (!passwordEncoder.matches(request.getPassword(), account.getPassword())) { + if (!passwordEncoder.matches(request.password(), account.getPassword())) { throw new BusinessException(ErrorCode.BAD_CREDENTIALS); } // Sa-Token 登录 StpUtil.login(account.getId()); String token = StpUtil.getTokenValue(); - log.info("用户登录成功: username={}", request.getUsername()); + log.info("用户登录成功: username={}", request.username()); return new LoginResponse(token); } @@ -49,13 +49,13 @@ public class AccountServiceImpl implements AccountService { @Transactional public LoginResponse register(RegisterRequest request) { // 检查用户名是否已存在 - if (accountRepository.existsByUsername(request.getUsername())) { + if (accountRepository.existsByUsername(request.username())) { throw new BusinessException(ErrorCode.USERNAME_EXISTS); } // 创建账号 AccountEntity account = new AccountEntity(); - account.setUsername(request.getUsername()); - account.setPassword(passwordEncoder.encode(request.getPassword())); + account.setUsername(request.username()); + account.setPassword(passwordEncoder.encode(request.password())); accountRepository.save(account); // 创建用户资料(默认值) UserEntity user = new UserEntity(); @@ -66,7 +66,7 @@ public class AccountServiceImpl implements AccountService { user.setAvatar(""); userRepository.save(user); - log.info("用户注册成功: username={}", request.getUsername()); + log.info("用户注册成功: username={}", request.username()); // 注册后自动登录 StpUtil.login(account.getId()); String token = StpUtil.getTokenValue(); diff --git a/src/main/java/com/webgame/webgamebackend/service/impl/UserServiceImpl.java b/src/main/java/com/webgame/webgamebackend/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..3db740e --- /dev/null +++ b/src/main/java/com/webgame/webgamebackend/service/impl/UserServiceImpl.java @@ -0,0 +1,31 @@ +package com.webgame.webgamebackend.service.impl; + +import cn.dev33.satoken.stp.StpUtil; +import com.webgame.webgamebackend.common.dto.user.UserInfoResponse; +import com.webgame.webgamebackend.common.exception.BusinessException; +import com.webgame.webgamebackend.common.exception.ErrorCode; +import com.webgame.webgamebackend.entities.UserEntity; +import com.webgame.webgamebackend.repository.UserRepository; +import com.webgame.webgamebackend.service.UserService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 用户服务实现 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class UserServiceImpl implements UserService { + + private final UserRepository userRepository; + + @Override + public UserInfoResponse getCurrentUserInfo() { + String accountId = StpUtil.getLoginIdAsString(); + UserEntity user = userRepository.findByAccountId(accountId) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + return UserInfoResponse.from(user); + } +}