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