feat: 重构项目结构并新增微信小程序认证模块

This commit is contained in:
Ray.Hao
2026-03-24 07:52:05 +08:00
parent 465e63c99d
commit 8188c82c3d
158 changed files with 1342 additions and 1562 deletions

View File

@@ -0,0 +1,43 @@
package com.youlai.boot.common.result;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* Excel导出响应结构体
*
* @author Theo
* @since 2025/1/14 11:46:08
*/
@Data
public class ExcelResult {
/**
* 响应码,来确定是否导入成功
*/
private String code;
/**
* 有效条数
*/
private Integer validCount;
/**
* 无效条数
*/
private Integer invalidCount;
/**
* 错误提示信息
*/
private List<String> messageList;
public ExcelResult() {
this.code = ResultCode.SUCCESS.getCode();
this.validCount = 0;
this.invalidCount = 0;
this.messageList = new ArrayList<>();
}
}

View File

@@ -0,0 +1,15 @@
package com.youlai.boot.common.result;
/**
* 响应码接口
*
* @author Ray.Hao
* @since 1.0.0
**/
public interface IResultCode {
String getCode();
String getMsg();
}

View File

@@ -0,0 +1,71 @@
package com.youlai.boot.common.result;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.Data;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;
/**
* 分页响应结构体
*
* @author Ray.Hao
* @since 2022/2/18
*/
@Data
public class PageResult<T> implements Serializable {
private String code;
private String msg;
private PageData<T> data;
/**
* 构建分页结果MyBatis-Plus {@link IPage})。
*
* <p>data 为当前页记录列表page 提供分页元信息。</p>
*/
public static <T> PageResult<T> success(IPage<T> page) {
PageResult<T> result = new PageResult<>();
result.setCode(ResultCode.SUCCESS.getCode());
result.setMsg(ResultCode.SUCCESS.getMsg());
List<T> records =
(page == null || page.getRecords() == null)
? Collections.emptyList()
: page.getRecords();
PageData<T> pageData = new PageData<>();
pageData.setList(records);
pageData.setTotal(page != null ? page.getTotal() : 0L);
result.setData(pageData);
return result;
}
/**
* 构建列表结果(无分页)。
*
* <p>page 置为 null用于与分页返回区分。</p>
*/
public static <T> PageResult<T> success(List<T> list) {
PageResult<T> result = new PageResult<>();
result.setCode(ResultCode.SUCCESS.getCode());
result.setMsg(ResultCode.SUCCESS.getMsg());
PageData<T> pageData = new PageData<>();
pageData.setList(list != null ? list : Collections.emptyList());
pageData.setTotal(0L);
result.setData(pageData);
return result;
}
@Data
public static class PageData<T> {
private List<T> list;
private long total;
}
}

View File

@@ -0,0 +1,118 @@
package com.youlai.boot.common.result;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.json.JSONUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import java.nio.charset.StandardCharsets;
/**
* 响应写入器
* <p>
* 用于在过滤器、Security处理器等无法使用 @RestControllerAdvice 的场景中统一写入HTTP响应。
* 支持写入成功响应和错误响应。
* 此类为工具类,所有方法均为静态方法,禁止实例化。
*
* @author Ray.Hao
* @since 2.0.0
*/
@Slf4j
public final class ResponseWriter {
/**
* 私有构造函数,防止实例化
*/
private ResponseWriter() {
throw new UnsupportedOperationException("工具类不允许实例化");
}
/**
* 写入成功响应
*
* @param response HttpServletResponse
* @param data 响应数据(可选)
*/
public static void writeSuccess(HttpServletResponse response, Object data) {
writeResult(response, Result.success(data), HttpStatus.OK.value());
}
/**
* 写入成功响应(无数据)
*
* @param response HttpServletResponse
*/
public static void writeSuccess(HttpServletResponse response) {
writeSuccess(response, null);
}
/**
* 写入错误响应
*
* @param response HttpServletResponse
* @param resultCode 响应结果码
*/
public static void writeError(HttpServletResponse response, ResultCode resultCode) {
writeError(response, resultCode, null);
}
/**
* 写入错误响应(带自定义消息)
*
* @param response HttpServletResponse
* @param resultCode 响应结果码
* @param message 自定义消息(可选,为 null 时使用 resultCode 的默认消息)
*/
public static void writeError(HttpServletResponse response, ResultCode resultCode, String message) {
Result<?> result = message == null
? Result.failed(resultCode)
: Result.failed(resultCode, message);
int httpStatus = mapHttpStatus(resultCode);
writeResult(response, result, httpStatus);
}
/**
* 写入响应结果(通用方法)
*
* @param response HttpServletResponse
* @param result 响应结果对象
* @param httpStatus HTTP状态码
*/
private static void writeResult(HttpServletResponse response, Result<?> result, int httpStatus) {
try {
// 设置HTTP状态码
response.setStatus(httpStatus);
// 设置响应编码和内容类型
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
// 写入响应
JakartaServletUtil.write(response,
JSONUtil.toJsonStr(result),
MediaType.APPLICATION_JSON_VALUE
);
} catch (Exception e) {
log.error("写入响应时发生未知异常: httpStatus={}, result={}", httpStatus, result, e);
}
}
/**
* 根据业务结果码映射HTTP状态码
*
* @param resultCode 业务结果码
* @return HTTP状态码
*/
private static int mapHttpStatus(ResultCode resultCode) {
return switch (resultCode) {
case ACCESS_UNAUTHORIZED,
ACCESS_TOKEN_INVALID,
REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value();
default -> HttpStatus.BAD_REQUEST.value();
};
}
}

View File

@@ -0,0 +1,79 @@
package com.youlai.boot.common.result;
import cn.hutool.core.util.StrUtil;
import lombok.Data;
import java.io.Serializable;
/**
* 统一响应结构体
*
* @author Ray.Hao
* @since 2022/1/30
**/
@Data
public class Result<T> implements Serializable {
private String code;
private T data;
private String msg;
public static <T> Result<T> success() {
return success(null);
}
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(ResultCode.SUCCESS.getCode());
result.setMsg(ResultCode.SUCCESS.getMsg());
result.setData(data);
return result;
}
public static <T> Result<T> failed() {
return result(ResultCode.SYSTEM_ERROR.getCode(), ResultCode.SYSTEM_ERROR.getMsg(), null);
}
public static <T> Result<T> failed(String msg) {
return result(ResultCode.SYSTEM_ERROR.getCode(), msg, null);
}
public static <T> Result<T> judge(boolean status) {
if (status) {
return success();
} else {
return failed();
}
}
public static <T> Result<T> failed(IResultCode resultCode) {
return result(resultCode.getCode(), resultCode.getMsg(), null);
}
public static <T> Result<T> failed(IResultCode resultCode, String msg) {
return result(resultCode.getCode(), StrUtil.isNotBlank(msg) ? msg : resultCode.getMsg(), null);
}
public static <T> Result<T> failed(IResultCode resultCode, T data) {
return result(resultCode.getCode(), resultCode.getMsg(), data);
}
public static <T> Result<T> failed(IResultCode resultCode, String msg, T data) {
return result(resultCode.getCode(), StrUtil.isNotBlank(msg) ? msg : resultCode.getMsg(), data);
}
private static <T> Result<T> result(IResultCode resultCode, T data) {
return result(resultCode.getCode(), resultCode.getMsg(), data);
}
private static <T> Result<T> result(String code, String msg, T data) {
Result<T> result = new Result<>();
result.setCode(code);
result.setData(data);
result.setMsg(msg);
return result;
}
}

View File

@@ -0,0 +1,155 @@
package com.youlai.boot.common.result;
import java.io.Serializable;
/**
* 响应码枚举
* <p>
* 参考《阿里巴巴 Java 开发手册》错误码设计建议:
* 00000 表示成功。
* A**** 表示用户端错误(如参数错误、认证失败等)。
* B**** 表示当前系统执行出错(如系统超时等)。
* C**** 表示调用第三方服务出错(如中间件、数据库等外部依赖)。
* <p>
* 错误码位数与号段说明:
* - 错误码为字符串类型,共 5 位错误产生来源A/B/C + 四位数字编号。
* - 四位数字编号范围 0001~9999大类之间建议按步长 100 预留号段(如 A0200、A0300、A0400
* - 错误码后三位编号与 HTTP 状态码无关。
* <p>
* 说明:
* - 本项目仅保留实际使用的错误码,并在 A/B/C 各保留少量示例,避免枚举无限膨胀。
* - 如需扩展业务错误码,建议在对应宏观分类下按场景划分号段并保持全局唯一。
* <p>
* 附表(节选):错误码列表(示例/项目使用项)
* <pre>
* | 错误码 | 中文描述 | 说明 |
* |-------|----------------------|------------------|
* | 00000 | 成功 | 正常执行后的返回 |
* | A0001 | 用户端错误 | 一级宏观错误码 |
* | A0100 | 用户注册错误 | 二级宏观错误码 |
* | A0101 | 用户未同意隐私协议 | 二级宏观错误码 |
* | A0200 | 用户登录异常 | 二级宏观错误码 |
* | A0201 | 用户账户不存在 | 二级宏观错误码 |
* | A0202 | 用户账户被冻结 | 二级宏观错误码 |
* | A0230 | 访问令牌无效或已过期 | 令牌校验失败 |
* | A0241 | 用户验证码尝试次数超限 | 二级宏观错误码 |
* | A0300 | 访问权限异常 | 二级宏观错误码 |
* | A0301 | 访问未授权 | 二级宏观错误码 |
* | A0400 | 用户请求参数错误 | 二级宏观错误码 |
* | A0410 | 请求必填参数为空 | 二级宏观错误码 |
* | A0500 | 用户请求服务异常 | 二级宏观错误码 |
* | A0502 | 请求并发数超出限制 | 二级宏观错误码 |
* | A0506 | 请勿重复提交 | 二级宏观错误码 |
* | B0001 | 系统执行出错 | 一级宏观错误码 |
* | B0100 | 系统执行超时 | 二级宏观错误码 |
* | C0001 | 调用第三方服务出错 | 一级宏观错误码 |
* | C0113 | 接口不存在 | 二级宏观错误码 |
* | C0300 | 数据库服务出错 | 二级宏观错误码 |
* </pre>
*
* @author Ray.Hao
* @since 2020/6/23
**/
public enum ResultCode implements IResultCode, Serializable {
SUCCESS("00000", "成功"),
/** 一级宏观错误码:用户端错误(由客户端输入/认证/权限/请求方式等引起,需客户端配合修正) */
USER_ERROR("A0001", "用户端错误"),
/** 二级宏观错误码:用户端具体错误(按号段细分,便于定位是注册/登录/令牌/参数/防重等问题) */
/** A01xx用户注册错误 */
USER_REGISTRATION_ERROR("A0100", "用户注册错误"),
USER_NOT_AGREE_PRIVACY_AGREEMENT("A0101", "用户未同意隐私协议"),
/** A013x校验码输入错误 */
VERIFICATION_CODE_INPUT_ERROR("A0130", "校验码输入错误"),
/** A02xx用户登录异常 */
USER_LOGIN_EXCEPTION("A0200", "用户登录异常"),
ACCOUNT_NOT_FOUND("A0201", "用户账户不存在"),
ACCOUNT_FROZEN("A0202", "用户账户被冻结"),
USER_PASSWORD_ERROR("A0210", "用户名或密码错误"),
/** A023x令牌无效或已过期 */
ACCESS_TOKEN_INVALID("A0230", "访问令牌无效或已过期"),
REFRESH_TOKEN_INVALID("A0231", "刷新令牌无效或已过期"),
/** A024x验证码错误 */
USER_VERIFICATION_CODE_ERROR("A0240", "验证码错误"),
USER_VERIFICATION_CODE_ATTEMPT_LIMIT_EXCEEDED("A0241", "用户验证码尝试次数超限"),
USER_VERIFICATION_CODE_EXPIRED("A0242", "用户验证码过期"),
/** A03xx访问权限异常 */
ACCESS_PERMISSION_EXCEPTION("A0300", "访问权限异常"),
ACCESS_UNAUTHORIZED("A0301", "访问未授权"),
/** A04xx用户请求参数错误 */
USER_REQUEST_PARAMETER_ERROR("A0400", "用户请求参数错误"),
INVALID_USER_INPUT("A0402", "无效的用户输入"),
REQUEST_REQUIRED_PARAMETER_IS_EMPTY("A0410", "请求必填参数为空"),
PARAMETER_FORMAT_MISMATCH("A0421", "参数格式不匹配"),
/** A05xx用户请求服务异常 */
USER_REQUEST_SERVICE_EXCEPTION("A0500", "用户请求服务异常"),
REQUEST_CONCURRENCY_LIMIT_EXCEEDED("A0502", "请求并发数超出限制"),
DUPLICATE_SUBMISSION("A0506", "请勿重复提交"),
/** A07xx文件处理异常 */
UPLOAD_FILE_EXCEPTION("A0700", "上传文件异常"),
DELETE_FILE_EXCEPTION("A0710", "删除文件异常"),
/** 一级宏观错误码:系统端错误(服务端内部异常/超时/不可用等,需后端排查修复) */
SYSTEM_ERROR("B0001", "系统执行出错"),
/** 二级宏观错误码:系统端具体错误(按号段细分,便于定位超时/限流/资源耗尽等) */
SYSTEM_EXECUTION_TIMEOUT("B0100", "系统执行超时"),
/** 一级宏观错误码:第三方服务错误(外部依赖/中间件/数据库等引起,需检查依赖健康与配置) */
THIRD_PARTY_SERVICE_ERROR("C0001", "调用第三方服务出错"),
/** 二级宏观错误码:第三方服务具体错误(按号段细分,便于定位是接口不存在/数据库异常等) */
INTERFACE_NOT_EXIST("C0113", "接口不存在"),
DATABASE_SERVICE_ERROR("C0300", "数据库服务出错"),
DATABASE_EXECUTION_SYNTAX_ERROR("C0313", "数据库执行语法错误"),
INTEGRITY_CONSTRAINT_VIOLATION("C0342", "违反了完整性约束"),
DATABASE_ACCESS_DENIED("C0351", "演示环境已禁用数据库写入功能请本地部署修改数据库链接或开启Mock模式进行体验");
private final String code;
private final String msg;
ResultCode(String code, String msg) {
this.code = code;
this.msg = msg;
}
@Override
public String getCode() {
return code;
}
@Override
public String getMsg() {
return msg;
}
@Override
public String toString() {
return "{" +
"\"code\":\"" + code + '\"' +
", \"msg\":\"" + msg + '\"' +
'}';
}
public static ResultCode getValue(String code) {
for (ResultCode value : values()) {
if (value.getCode().equals(code)) {
return value;
}
}
return SYSTEM_ERROR; // 默认系统执行错误
}
}