feat: 用户相关api接口

This commit is contained in:
2026-06-15 18:23:41 +08:00
parent 567bee3e8f
commit 8477e49fa0
13 changed files with 233 additions and 56 deletions

View File

@@ -9,10 +9,6 @@ public record ApiResponse<T>(int code, String message, T data) {
/** /**
* 成功响应 * 成功响应
*
* @param data 响应数据
* @param <T> 数据类型
* @return ApiResponse
*/ */
public static <T> ApiResponse<T> ok(T data) { public static <T> ApiResponse<T> ok(T data) {
return new ApiResponse<>(0, "success", data); return new ApiResponse<>(0, "success", data);
@@ -20,11 +16,6 @@ public record ApiResponse<T>(int code, String message, T data) {
/** /**
* 失败响应 * 失败响应
*
* @param code 业务状态码
* @param message 错误消息
* @param <T> 数据类型
* @return ApiResponse
*/ */
public static <T> ApiResponse<T> fail(int code, String message) { public static <T> ApiResponse<T> fail(int code, String message) {
return new ApiResponse<>(code, message, null); return new ApiResponse<>(code, message, null);

View File

@@ -2,25 +2,18 @@ package com.webgame.webgamebackend.common.dto.account;
import com.webgame.webgamebackend.common.dto.base.BaseVo; import com.webgame.webgamebackend.common.dto.base.BaseVo;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import lombok.Data; import jakarta.validation.constraints.NotNull;
import lombok.EqualsAndHashCode;
/** /**
* 登录请求 * 登录请求
*/ */
@Data public record LoginRequest(
@EqualsAndHashCode(callSuper = true) @NotNull(message = "用户名不能为null")
public class LoginRequest extends BaseVo { @NotBlank(message = "用户名不能为空")
String username,
/** @NotNull(message = "密码不能为null")
* 用户名 @NotBlank(message = "密码不能为空")
*/ String password
@NotBlank(message = "用户名不能为空") ) implements BaseVo {
private String username;
/**
* 密码
*/
@NotBlank(message = "密码不能为空")
private String password;
} }

View File

@@ -3,4 +3,5 @@ package com.webgame.webgamebackend.common.dto.account;
/** /**
* 登录/注册响应 * 登录/注册响应
*/ */
public record LoginResponse(String token) {} public record LoginResponse(String token) {
}

View File

@@ -2,28 +2,21 @@ package com.webgame.webgamebackend.common.dto.account;
import com.webgame.webgamebackend.common.dto.base.BaseVo; import com.webgame.webgamebackend.common.dto.base.BaseVo;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
/** /**
* 注册请求 * 注册请求
*/ */
@Data public record RegisterRequest(
@EqualsAndHashCode(callSuper = true) @NotNull(message = "用户名不能为null")
public class RegisterRequest extends BaseVo { @NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 32, message = "用户名长度3-32位")
String username,
/** @NotNull(message = "密码不能为null")
* 用户名 @NotBlank(message = "密码不能为空")
*/ @Size(min = 6, max = 64, message = "密码长度6-64位")
@NotBlank(message = "用户名不能为空") String password
@Size(min = 3, max = 32, message = "用户名长度3-32位") ) implements BaseVo {
private String username;
/**
* 密码
*/
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 64, message = "密码长度6-64位")
private String password;
} }

View File

@@ -3,7 +3,7 @@ package com.webgame.webgamebackend.common.dto.base;
import java.io.Serializable; import java.io.Serializable;
/** /**
* 请求参数基类,所有入参 VO 均继承此类 * 入参 VO 标记接口,所有请求参数 record 均实现此接口
*/ */
public abstract class BaseVo implements Serializable { public interface BaseVo extends Serializable {
} }

View File

@@ -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()
);
}
}

View File

@@ -16,6 +16,9 @@ public enum ErrorCode {
// ========== 账号模块 ========== // ========== 账号模块 ==========
USERNAME_EXISTS(1001, "用户名已存在"), USERNAME_EXISTS(1001, "用户名已存在"),
BAD_CREDENTIALS(1002, "用户名或密码错误"), BAD_CREDENTIALS(1002, "用户名或密码错误"),
// ========== 用户模块 ==========
USER_NOT_FOUND(2001, "用户不存在"),
; ;
private final int code; private final int code;

View File

@@ -1,12 +1,19 @@
package com.webgame.webgamebackend.common.handler; 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.dto.ApiResponse;
import com.webgame.webgamebackend.common.exception.BusinessException; import com.webgame.webgamebackend.common.exception.BusinessException;
import com.webgame.webgamebackend.common.exception.ErrorCode; import com.webgame.webgamebackend.common.exception.ErrorCode;
import lombok.extern.slf4j.Slf4j; 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.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.resource.NoResourceFoundException;
/** /**
* 全局异常处理,统一返回 ApiResponse 格式 * 全局异常处理,统一返回 ApiResponse 格式
@@ -15,6 +22,8 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice @RestControllerAdvice
public class GlobalExceptionHandler { public class GlobalExceptionHandler {
// ==================== 业务异常 ====================
/** /**
* 处理业务异常 * 处理业务异常
*/ */
@@ -24,18 +33,89 @@ public class GlobalExceptionHandler {
return ApiResponse.fail(ex.getCode(), ex.getMessage()); return ApiResponse.fail(ex.getCode(), ex.getMessage());
} }
// ==================== 参数校验异常 ====================
/** /**
* 处理参数校验异常 * 处理 @Valid 参数校验失败,合并所有字段的错误消息
*/ */
@ExceptionHandler(MethodArgumentNotValidException.class) @ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResponse<Void> handleValidation(MethodArgumentNotValidException ex) { public ApiResponse<Void> handleValidation(MethodArgumentNotValidException ex) {
String message = ex.getBindingResult().getFieldErrors().stream() String message = ex.getBindingResult().getFieldErrors().stream()
.findFirst() .map(e -> e.getField() + " " + e.getDefaultMessage())
.map(e -> e.getDefaultMessage()) .reduce((a, b) -> a + "; " + b)
.orElse(ErrorCode.BAD_REQUEST.getDefaultMessage()); .orElse(ErrorCode.BAD_REQUEST.getDefaultMessage());
return ApiResponse.fail(ErrorCode.BAD_REQUEST.getCode(), message); return ApiResponse.fail(ErrorCode.BAD_REQUEST.getCode(), message);
} }
// ==================== Sa-Token 鉴权异常 ====================
/**
* 未登录 / token 无效
*/
@ExceptionHandler(NotLoginException.class)
public ApiResponse<Void> handleNotLogin(NotLoginException ex) {
log.warn("未登录: {}", ex.getMessage());
return ApiResponse.fail(401, "请先登录");
}
/**
* 无权限
*/
@ExceptionHandler(NotPermissionException.class)
public ApiResponse<Void> handleNotPermission(NotPermissionException ex) {
log.warn("无权限: {}", ex.getMessage());
return ApiResponse.fail(403, "无权限");
}
/**
* 无角色
*/
@ExceptionHandler(NotRoleException.class)
public ApiResponse<Void> handleNotRole(NotRoleException ex) {
log.warn("无角色: {}", ex.getMessage());
return ApiResponse.fail(403, "无权限");
}
// ==================== Spring MVC 异常 ====================
/**
* 请求方法不支持(如 GET 访问 POST 接口)
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ApiResponse<Void> handleMethodNotSupported(HttpRequestMethodNotSupportedException ex) {
log.warn("请求方法不支持: {}", ex.getMessage());
return ApiResponse.fail(405, "请求方法不支持");
}
/**
* 请求体不可读(如 JSON 格式错误)
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public ApiResponse<Void> handleMessageNotReadable(HttpMessageNotReadableException ex) {
log.warn("请求体解析失败: {}", ex.getMessage());
return ApiResponse.fail(ErrorCode.BAD_REQUEST.getCode(), "请求体格式错误");
}
/**
* 路径参数类型不匹配
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ApiResponse<Void> handleTypeMismatch(MethodArgumentTypeMismatchException ex) {
log.warn("参数类型不匹配: {}", ex.getMessage());
return ApiResponse.fail(ErrorCode.BAD_REQUEST.getCode(), "参数类型错误");
}
/**
* 资源不存在404
*/
@ExceptionHandler(NoResourceFoundException.class)
public ApiResponse<Void> handleNoResourceFound(NoResourceFoundException ex) {
log.warn("资源不存在: {}", ex.getMessage());
return ApiResponse.fail(404, "资源不存在");
}
// ==================== 兜底 ====================
/** /**
* 处理其他未捕获异常 * 处理其他未捕获异常
*/ */

View File

@@ -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<UserInfoResponse> info() {
UserInfoResponse result = userService.getCurrentUserInfo();
return ApiResponse.ok(result);
}
}

View File

@@ -3,8 +3,15 @@ package com.webgame.webgamebackend.repository;
import com.webgame.webgamebackend.entities.UserEntity; import com.webgame.webgamebackend.entities.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
/** /**
* 用户 Repository * 用户 Repository
*/ */
public interface UserRepository extends JpaRepository<UserEntity, String> { public interface UserRepository extends JpaRepository<UserEntity, String> {
/**
* 根据账号 ID 查询用户
*/
Optional<UserEntity> findByAccountId(String accountId);
} }

View File

@@ -0,0 +1,16 @@
package com.webgame.webgamebackend.service;
import com.webgame.webgamebackend.common.dto.user.UserInfoResponse;
/**
* 用户服务接口
*/
public interface UserService {
/**
* 获取当前登录用户的资料
*
* @return 用户信息
*/
UserInfoResponse getCurrentUserInfo();
}

View File

@@ -33,15 +33,15 @@ public class AccountServiceImpl implements AccountService {
@Override @Override
public LoginResponse login(LoginRequest request) { public LoginResponse login(LoginRequest request) {
AccountEntity account = accountRepository.findByUsername(request.getUsername()) AccountEntity account = accountRepository.findByUsername(request.username())
.orElseThrow(() -> new BusinessException(ErrorCode.BAD_CREDENTIALS)); .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); throw new BusinessException(ErrorCode.BAD_CREDENTIALS);
} }
// Sa-Token 登录 // Sa-Token 登录
StpUtil.login(account.getId()); StpUtil.login(account.getId());
String token = StpUtil.getTokenValue(); String token = StpUtil.getTokenValue();
log.info("用户登录成功: username={}", request.getUsername()); log.info("用户登录成功: username={}", request.username());
return new LoginResponse(token); return new LoginResponse(token);
} }
@@ -49,13 +49,13 @@ public class AccountServiceImpl implements AccountService {
@Transactional @Transactional
public LoginResponse register(RegisterRequest request) { public LoginResponse register(RegisterRequest request) {
// 检查用户名是否已存在 // 检查用户名是否已存在
if (accountRepository.existsByUsername(request.getUsername())) { if (accountRepository.existsByUsername(request.username())) {
throw new BusinessException(ErrorCode.USERNAME_EXISTS); throw new BusinessException(ErrorCode.USERNAME_EXISTS);
} }
// 创建账号 // 创建账号
AccountEntity account = new AccountEntity(); AccountEntity account = new AccountEntity();
account.setUsername(request.getUsername()); account.setUsername(request.username());
account.setPassword(passwordEncoder.encode(request.getPassword())); account.setPassword(passwordEncoder.encode(request.password()));
accountRepository.save(account); accountRepository.save(account);
// 创建用户资料(默认值) // 创建用户资料(默认值)
UserEntity user = new UserEntity(); UserEntity user = new UserEntity();
@@ -66,7 +66,7 @@ public class AccountServiceImpl implements AccountService {
user.setAvatar(""); user.setAvatar("");
userRepository.save(user); userRepository.save(user);
log.info("用户注册成功: username={}", request.getUsername()); log.info("用户注册成功: username={}", request.username());
// 注册后自动登录 // 注册后自动登录
StpUtil.login(account.getId()); StpUtil.login(account.getId());
String token = StpUtil.getTokenValue(); String token = StpUtil.getTokenValue();

View File

@@ -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);
}
}