feat(biji-houdaun): 实现用户注册、登录、 Markdown 文件和图片上传功能

- 新增用户注册、登录接口及服务实现
- 添加 Markdown 文件创建、更新接口及服务实现
- 实现图片上传、获取接口及服务实现
- 集成 Snowflake ID 生成器
- 添加全局异常处理和统一返回结果封装
- 配置跨域访问和静态资源处理
- 实现基础的 XSS 防护
This commit is contained in:
ikmkj
2025-06-16 20:20:08 +08:00
parent 8a4bf2d245
commit 1ef2e116a6
27 changed files with 1049 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
package com.test.bijihoudaun.common.advice;
import com.test.bijihoudaun.common.exception.BaseException;
import com.test.bijihoudaun.common.response.R;
import com.test.bijihoudaun.common.response.ResultCode;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import java.util.List;
import java.util.stream.Collectors;
/**
* 全局异常处理器
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理业务异常
*/
@ExceptionHandler(BaseException.class)
public R<Void> handleBaseException(BaseException e, HttpServletRequest request) {
return R.fail(e.getResultCode().getCode(), e.getMessage());
}
/**
* 处理文件大小超出限制异常
*/
@ExceptionHandler(MaxUploadSizeExceededException.class)
public R<String> handleFileSizeLimitExceeded() {
return R.fail("文件大小超过限制");
}
/**
* 处理参数校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public R<List<String>> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
List<String> errors = e.getBindingResult().getFieldErrors()
.stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.toList());
return R.fail(ResultCode.VALIDATE_FAILED.getCode(), errors.get(0));
}
/**
* 处理绑定异常
*/
@ExceptionHandler(BindException.class)
public R<List<String>> handleBindException(BindException e) {
List<String> errors = e.getBindingResult().getFieldErrors()
.stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.toList());
return R.fail(ResultCode.VALIDATE_FAILED.getCode(), errors.get(0));
}
/**
* 处理其他异常
*/
@ExceptionHandler(Exception.class)
public R<Void> handleException(Exception e) {
return R.fail(ResultCode.FAILED.getCode(), "系统繁忙,请稍后再试");
}
}

View File

@@ -0,0 +1,32 @@
package com.test.bijihoudaun.common.exception;
import com.test.bijihoudaun.common.response.ResultCode;
import lombok.Getter;
/**
* 基础业务异常类
*/
@Getter
public class BaseException extends RuntimeException {
private final ResultCode resultCode;
public BaseException(ResultCode resultCode) {
super(resultCode.getMsg());
this.resultCode = resultCode;
}
public BaseException(ResultCode resultCode, String message) {
super(message);
this.resultCode = resultCode;
}
public BaseException(ResultCode resultCode, Throwable cause) {
super(resultCode.getMsg(), cause);
this.resultCode = resultCode;
}
public BaseException(ResultCode resultCode, String message, Throwable cause) {
super(message, cause);
this.resultCode = resultCode;
}
}

View File

@@ -0,0 +1,87 @@
package com.test.bijihoudaun.common.response;
import lombok.Data;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import java.io.Serializable;
import java.net.URI;
/**
* 统一返回结果类
* @param <T> 数据类型
*/
@Data
public class R<T> implements Serializable {
private static final long serialVersionUID = 1L;
private Integer code; // 状态码
private String msg; // 消息
private T data; // 数据
// 成功返回
public static <T> R<T> success() {
return success(null);
}
public static <T> R<T> success(T data) {
R<T> r = new R<>();
r.setCode(ResultCode.SUCCESS.getCode());
r.setMsg(ResultCode.SUCCESS.getMsg());
r.setData(data);
return r;
}
// 分页成功返回
public static <T> R<T> pageSuccess(long total, T data) {
PageR<T> r = new PageR<>();
r.setCode(ResultCode.SUCCESS.getCode());
r.setMsg(ResultCode.SUCCESS.getMsg());
r.setData(data);
r.setTotal(total);
return r;
}
// 失败返回
public static <T> R<T> fail() {
return fail(ResultCode.FAILED);
}
public static <T> R<T> fail(ResultCode resultCode) {
return fail(resultCode.getCode(), resultCode.getMsg());
}
public static <T> R<T> fail(String msg) {
return fail(ResultCode.FAILED.getCode(), msg);
}
public static <T> R<T> fail(Integer code, String msg) {
R<T> r = new R<>();
r.setCode(code);
r.setMsg(msg);
return r;
}
// 转换为ProblemDetail
public static ProblemDetail toProblemDetail(ResultCode resultCode, HttpStatus status) {
ProblemDetail problemDetail = ProblemDetail.forStatus(status);
problemDetail.setType(URI.create("https://api.test.com/errors/" + resultCode.name().toLowerCase()));
problemDetail.setTitle(resultCode.getMsg());
problemDetail.setDetail(resultCode.getMsg());
problemDetail.setProperty("code", resultCode.getCode());
return problemDetail;
}
// 分页返回结果类
private static class PageR<T> extends R<T> {
private long total;
public long getTotal() {
return total;
}
public void setTotal(long total) {
this.total = total;
}
}
}

View File

@@ -0,0 +1,51 @@
package com.test.bijihoudaun.common.response;
import lombok.Getter;
/**
* 返回状态码枚举
*/
@Getter
public enum ResultCode {
// 基础状态码
SUCCESS(200, "操作成功"),
FAILED(500, "操作失败"),
VALIDATE_FAILED(400, "参数校验失败"),
UNAUTHORIZED(401, "未授权"),
FORBIDDEN(403, "禁止访问"),
NOT_FOUND(404, "资源不存在"),
// 参数相关错误
PARAM_IS_INVALID(1001, "参数无效"),
PARAM_IS_BLANK(1002, "参数为空"),
PARAM_TYPE_BIND_ERROR(1003, "参数类型错误"),
PARAM_NOT_COMPLETE(1004, "参数缺失"),
// 加密相关错误
ENCRYPTION_FAILED(2001, "加密失败"),
DECRYPTION_FAILED(2002, "解密失败"),
KEY_GENERATION_FAILED(2003, "密钥生成失败"),
INVALID_KEY_FORMAT(2004, "密钥格式无效"),
// 编码相关
BASE64_ENCODE_FAILED(4001, "Base64编码失败"),
BASE64_DECODE_FAILED(4002, "Base64解码失败"),
// ID生成相关
ID_GENERATION_FAILED(3001, "ID生成失败"),
UUID_GENERATION_FAILED(3002, "UUID生成失败"),
SNOWFLAKE_PARAM_INVALID(3003, "雪花ID参数无效"), // 用于workerId/datacenterId越界
CLOCK_BACKWARD_ERROR(3004, "系统时钟回拨异常"), // 处理时钟回拨场景
SEQUENCE_OVERFLOW(3005, "ID序列号溢出"); // 序列号超过4095时的异常
private final Integer code;
private final String msg;
ResultCode(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
}