From 567bee3e8f69682d800a6a2ef65ebb79654af47b Mon Sep 17 00:00:00 2001 From: Azure <983547216@qq.com> Date: Mon, 15 Jun 2026 17:32:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9F=BA=E7=A1=80=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 6 ++ .../common/config/PasswordConfig.java | 17 +++++ .../common/dto/ApiResponse.java | 32 +++++++++ .../common/dto/account/LoginRequest.java | 26 +++++++ .../common/dto/account/LoginResponse.java | 6 ++ .../common/dto/account/RegisterRequest.java | 29 ++++++++ .../common/dto/base/BaseVo.java | 9 +++ .../common/exception/BusinessException.java | 28 ++++++++ .../common/exception/ErrorCode.java | 28 ++++++++ .../handler/GlobalExceptionHandler.java | 47 ++++++++++++ .../controller/AccountController.java | 29 +++++++- .../entities/AccountEntity.java | 2 +- .../repository/AccountRepository.java | 28 ++++++++ .../repository/UserRepository.java | 10 +++ .../service/AccountService.java | 22 +++++- .../service/impl/AccountServiceImpl.java | 72 ++++++++++++++++++- 16 files changed, 384 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/webgame/webgamebackend/common/config/PasswordConfig.java create mode 100644 src/main/java/com/webgame/webgamebackend/common/dto/ApiResponse.java create mode 100644 src/main/java/com/webgame/webgamebackend/common/dto/account/LoginRequest.java create mode 100644 src/main/java/com/webgame/webgamebackend/common/dto/account/LoginResponse.java create mode 100644 src/main/java/com/webgame/webgamebackend/common/dto/account/RegisterRequest.java create mode 100644 src/main/java/com/webgame/webgamebackend/common/dto/base/BaseVo.java create mode 100644 src/main/java/com/webgame/webgamebackend/common/exception/BusinessException.java create mode 100644 src/main/java/com/webgame/webgamebackend/common/exception/ErrorCode.java create mode 100644 src/main/java/com/webgame/webgamebackend/common/handler/GlobalExceptionHandler.java create mode 100644 src/main/java/com/webgame/webgamebackend/repository/AccountRepository.java create mode 100644 src/main/java/com/webgame/webgamebackend/repository/UserRepository.java diff --git a/pom.xml b/pom.xml index 7e04bd1..a9da9cc 100644 --- a/pom.xml +++ b/pom.xml @@ -101,6 +101,12 @@ 2.0.57 + + + org.springframework.security + spring-security-crypto + + org.springframework.boot diff --git a/src/main/java/com/webgame/webgamebackend/common/config/PasswordConfig.java b/src/main/java/com/webgame/webgamebackend/common/config/PasswordConfig.java new file mode 100644 index 0000000..eae5d45 --- /dev/null +++ b/src/main/java/com/webgame/webgamebackend/common/config/PasswordConfig.java @@ -0,0 +1,17 @@ +package com.webgame.webgamebackend.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +/** + * 密码加密配置 + */ +@Configuration +public class PasswordConfig { + + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/com/webgame/webgamebackend/common/dto/ApiResponse.java b/src/main/java/com/webgame/webgamebackend/common/dto/ApiResponse.java new file mode 100644 index 0000000..c8e207a --- /dev/null +++ b/src/main/java/com/webgame/webgamebackend/common/dto/ApiResponse.java @@ -0,0 +1,32 @@ +package com.webgame.webgamebackend.common.dto; + +/** + * 后端统一响应格式 + * + * @param 响应数据类型 + */ +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); + } + + /** + * 失败响应 + * + * @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 new file mode 100644 index 0000000..33606bf --- /dev/null +++ b/src/main/java/com/webgame/webgamebackend/common/dto/account/LoginRequest.java @@ -0,0 +1,26 @@ +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; + +/** + * 登录请求 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class LoginRequest extends BaseVo { + + /** + * 用户名 + */ + @NotBlank(message = "用户名不能为空") + private String username; + + /** + * 密码 + */ + @NotBlank(message = "密码不能为空") + private String password; +} 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 new file mode 100644 index 0000000..654ad18 --- /dev/null +++ b/src/main/java/com/webgame/webgamebackend/common/dto/account/LoginResponse.java @@ -0,0 +1,6 @@ +package com.webgame.webgamebackend.common.dto.account; + +/** + * 登录/注册响应 + */ +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 new file mode 100644 index 0000000..a5c187f --- /dev/null +++ b/src/main/java/com/webgame/webgamebackend/common/dto/account/RegisterRequest.java @@ -0,0 +1,29 @@ +package com.webgame.webgamebackend.common.dto.account; + +import com.webgame.webgamebackend.common.dto.base.BaseVo; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 注册请求 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class RegisterRequest extends BaseVo { + + /** + * 用户名 + */ + @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; +} 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 new file mode 100644 index 0000000..b33a320 --- /dev/null +++ b/src/main/java/com/webgame/webgamebackend/common/dto/base/BaseVo.java @@ -0,0 +1,9 @@ +package com.webgame.webgamebackend.common.dto.base; + +import java.io.Serializable; + +/** + * 请求参数基类,所有入参 VO 均继承此类 + */ +public abstract class BaseVo implements Serializable { +} diff --git a/src/main/java/com/webgame/webgamebackend/common/exception/BusinessException.java b/src/main/java/com/webgame/webgamebackend/common/exception/BusinessException.java new file mode 100644 index 0000000..b301bec --- /dev/null +++ b/src/main/java/com/webgame/webgamebackend/common/exception/BusinessException.java @@ -0,0 +1,28 @@ +package com.webgame.webgamebackend.common.exception; + +import lombok.Getter; + +/** + * 业务异常,Service 层抛出,由全局异常处理器转换为 ApiResponse + */ +@Getter +public class BusinessException extends RuntimeException { + + private final int code; + + /** + * 使用 ErrorCode 枚举创建异常,消息使用枚举默认值 + */ + public BusinessException(ErrorCode errorCode) { + super(errorCode.getDefaultMessage()); + this.code = errorCode.getCode(); + } + + /** + * 使用 ErrorCode 枚举 + 自定义消息创建异常 + */ + public BusinessException(ErrorCode errorCode, String message) { + super(message); + this.code = errorCode.getCode(); + } +} diff --git a/src/main/java/com/webgame/webgamebackend/common/exception/ErrorCode.java b/src/main/java/com/webgame/webgamebackend/common/exception/ErrorCode.java new file mode 100644 index 0000000..97f5ed1 --- /dev/null +++ b/src/main/java/com/webgame/webgamebackend/common/exception/ErrorCode.java @@ -0,0 +1,28 @@ +package com.webgame.webgamebackend.common.exception; + +import lombok.Getter; + +/** + * 业务错误码枚举,统一管理所有错误码和默认消息 + */ +@Getter +public enum ErrorCode { + + // ========== 通用 ========== + SUCCESS(0, "success"), + BAD_REQUEST(400, "参数校验失败"), + INTERNAL_ERROR(5000, "服务器内部错误"), + + // ========== 账号模块 ========== + USERNAME_EXISTS(1001, "用户名已存在"), + BAD_CREDENTIALS(1002, "用户名或密码错误"), + ; + + private final int code; + private final String defaultMessage; + + ErrorCode(int code, String defaultMessage) { + this.code = code; + this.defaultMessage = defaultMessage; + } +} diff --git a/src/main/java/com/webgame/webgamebackend/common/handler/GlobalExceptionHandler.java b/src/main/java/com/webgame/webgamebackend/common/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..cb54303 --- /dev/null +++ b/src/main/java/com/webgame/webgamebackend/common/handler/GlobalExceptionHandler.java @@ -0,0 +1,47 @@ +package com.webgame.webgamebackend.common.handler; + +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.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * 全局异常处理,统一返回 ApiResponse 格式 + */ +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + /** + * 处理业务异常 + */ + @ExceptionHandler(BusinessException.class) + public ApiResponse handleBusinessException(BusinessException ex) { + log.warn("业务异常: code={}, message={}", ex.getCode(), ex.getMessage()); + return ApiResponse.fail(ex.getCode(), ex.getMessage()); + } + + /** + * 处理参数校验异常 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public ApiResponse handleValidation(MethodArgumentNotValidException ex) { + String message = ex.getBindingResult().getFieldErrors().stream() + .findFirst() + .map(e -> e.getDefaultMessage()) + .orElse(ErrorCode.BAD_REQUEST.getDefaultMessage()); + return ApiResponse.fail(ErrorCode.BAD_REQUEST.getCode(), message); + } + + /** + * 处理其他未捕获异常 + */ + @ExceptionHandler(Exception.class) + public ApiResponse handleException(Exception ex) { + log.error("服务器内部错误", ex); + return ApiResponse.fail(ErrorCode.INTERNAL_ERROR.getCode(), ErrorCode.INTERNAL_ERROR.getDefaultMessage()); + } +} diff --git a/src/main/java/com/webgame/webgamebackend/controller/AccountController.java b/src/main/java/com/webgame/webgamebackend/controller/AccountController.java index 83145f2..929e364 100644 --- a/src/main/java/com/webgame/webgamebackend/controller/AccountController.java +++ b/src/main/java/com/webgame/webgamebackend/controller/AccountController.java @@ -1,22 +1,47 @@ package com.webgame.webgamebackend.controller; import cn.dev33.satoken.annotation.SaIgnore; +import com.webgame.webgamebackend.common.dto.ApiResponse; +import com.webgame.webgamebackend.common.dto.account.LoginRequest; +import com.webgame.webgamebackend.common.dto.account.LoginResponse; +import com.webgame.webgamebackend.common.dto.account.RegisterRequest; +import com.webgame.webgamebackend.service.AccountService; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; 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; +/** + * 账号控制器 + */ @Validated @RestController @RequestMapping("/account") @RequiredArgsConstructor public class AccountController { + private final AccountService accountService; + /** + * 登录 + */ @SaIgnore @PostMapping("/login") - public String login() { - return "login"; + public ApiResponse login(@Valid @RequestBody LoginRequest request) { + LoginResponse result = accountService.login(request); + return ApiResponse.ok(result); + } + + /** + * 注册 + */ + @SaIgnore + @PostMapping("/register") + public ApiResponse register(@Valid @RequestBody RegisterRequest request) { + LoginResponse result = accountService.register(request); + return ApiResponse.ok(result); } } diff --git a/src/main/java/com/webgame/webgamebackend/entities/AccountEntity.java b/src/main/java/com/webgame/webgamebackend/entities/AccountEntity.java index c4e91d3..507876e 100644 --- a/src/main/java/com/webgame/webgamebackend/entities/AccountEntity.java +++ b/src/main/java/com/webgame/webgamebackend/entities/AccountEntity.java @@ -30,7 +30,7 @@ public class AccountEntity extends UUIDBaseEntity { /** * 密码 */ - @Column(name = "password", nullable = false) + @Column(name = "password", nullable = false, length = 60) private String password; /** diff --git a/src/main/java/com/webgame/webgamebackend/repository/AccountRepository.java b/src/main/java/com/webgame/webgamebackend/repository/AccountRepository.java new file mode 100644 index 0000000..1b80bb6 --- /dev/null +++ b/src/main/java/com/webgame/webgamebackend/repository/AccountRepository.java @@ -0,0 +1,28 @@ +package com.webgame.webgamebackend.repository; + +import com.webgame.webgamebackend.entities.AccountEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +/** + * 账号 Repository + */ +public interface AccountRepository extends JpaRepository { + + /** + * 根据用户名查询账号 + * + * @param username 用户名 + * @return 账号实体 + */ + Optional findByUsername(String username); + + /** + * 判断用户名是否已存在 + * + * @param username 用户名 + * @return 是否存在 + */ + boolean existsByUsername(String username); +} diff --git a/src/main/java/com/webgame/webgamebackend/repository/UserRepository.java b/src/main/java/com/webgame/webgamebackend/repository/UserRepository.java new file mode 100644 index 0000000..4773ccf --- /dev/null +++ b/src/main/java/com/webgame/webgamebackend/repository/UserRepository.java @@ -0,0 +1,10 @@ +package com.webgame.webgamebackend.repository; + +import com.webgame.webgamebackend.entities.UserEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * 用户 Repository + */ +public interface UserRepository extends JpaRepository { +} diff --git a/src/main/java/com/webgame/webgamebackend/service/AccountService.java b/src/main/java/com/webgame/webgamebackend/service/AccountService.java index 9ac3811..12abdb7 100644 --- a/src/main/java/com/webgame/webgamebackend/service/AccountService.java +++ b/src/main/java/com/webgame/webgamebackend/service/AccountService.java @@ -1,9 +1,27 @@ package com.webgame.webgamebackend.service; +import com.webgame.webgamebackend.common.dto.account.LoginRequest; +import com.webgame.webgamebackend.common.dto.account.LoginResponse; +import com.webgame.webgamebackend.common.dto.account.RegisterRequest; + +/** + * 账号服务接口 + */ public interface AccountService { + /** * 登录 - * @return token + * + * @param request 登录请求 + * @return 登录结果(含 token) */ - public String login(); + LoginResponse login(LoginRequest request); + + /** + * 注册 + * + * @param request 注册请求 + * @return 注册结果(含 token,注册后自动登录) + */ + LoginResponse register(RegisterRequest request); } 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 9c4566a..3e43caf 100644 --- a/src/main/java/com/webgame/webgamebackend/service/impl/AccountServiceImpl.java +++ b/src/main/java/com/webgame/webgamebackend/service/impl/AccountServiceImpl.java @@ -1,14 +1,82 @@ package com.webgame.webgamebackend.service.impl; +import cn.dev33.satoken.stp.StpUtil; +import com.webgame.webgamebackend.common.dto.account.LoginRequest; +import com.webgame.webgamebackend.common.dto.account.LoginResponse; +import com.webgame.webgamebackend.common.dto.account.RegisterRequest; +import com.webgame.webgamebackend.common.exception.BusinessException; +import com.webgame.webgamebackend.common.exception.ErrorCode; +import com.webgame.webgamebackend.entities.AccountEntity; +import com.webgame.webgamebackend.entities.UserEntity; +import com.webgame.webgamebackend.repository.AccountRepository; +import com.webgame.webgamebackend.repository.UserRepository; import com.webgame.webgamebackend.service.AccountService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.concurrent.ThreadLocalRandom; + +/** + * 账号服务实现 + */ +@Slf4j @Service @RequiredArgsConstructor public class AccountServiceImpl implements AccountService { + + private final AccountRepository accountRepository; + private final UserRepository userRepository; + private final BCryptPasswordEncoder passwordEncoder; + @Override - public String login() { - return "login"; + public LoginResponse login(LoginRequest request) { + AccountEntity account = accountRepository.findByUsername(request.getUsername()) + .orElseThrow(() -> new BusinessException(ErrorCode.BAD_CREDENTIALS)); + if (!passwordEncoder.matches(request.getPassword(), account.getPassword())) { + throw new BusinessException(ErrorCode.BAD_CREDENTIALS); + } + // Sa-Token 登录 + StpUtil.login(account.getId()); + String token = StpUtil.getTokenValue(); + log.info("用户登录成功: username={}", request.getUsername()); + return new LoginResponse(token); + } + + @Override + @Transactional + public LoginResponse register(RegisterRequest request) { + // 检查用户名是否已存在 + if (accountRepository.existsByUsername(request.getUsername())) { + throw new BusinessException(ErrorCode.USERNAME_EXISTS); + } + // 创建账号 + AccountEntity account = new AccountEntity(); + account.setUsername(request.getUsername()); + account.setPassword(passwordEncoder.encode(request.getPassword())); + accountRepository.save(account); + // 创建用户资料(默认值) + UserEntity user = new UserEntity(); + user.setAccount(account); + user.setNickName("玩家" + generateRandomCode()); + user.setSex(0); + user.setSignature(""); + user.setAvatar(""); + userRepository.save(user); + + log.info("用户注册成功: username={}", request.getUsername()); + // 注册后自动登录 + StpUtil.login(account.getId()); + String token = StpUtil.getTokenValue(); + return new LoginResponse(token); + } + + /** + * 生成6位随机数字,用于默认昵称 + */ + private String generateRandomCode() { + return String.valueOf(ThreadLocalRandom.current().nextInt(100000, 1000000)); } }