【短链接项目笔记】Day1 用户模块

前言

今天开始做一个短链接的项目,准备之后写到简历上,因此会认真的记录每天的学习情况,争取把里面的每个点都弄懂,也争取找到一些优化的内容。

1.什么是短链接

短链接(Short Link)是指将一个原始的长 URL(Uniform Resource Locator)通过特定的算法或服务转化为一个更短、易于记忆的 URL。短链接通常只包含几个字符,而原始的长 URL 可能会非常长。

短链接的原理非常简单,通过一个原始链接生成个相对短的链接,然后通过访问短链接跳转到原始链接。

2.功能分析

今天要实现的是用户模块,其功能主要包括以下几个方面:

  • 检查用户名是否存在
  • 注册用户
  • 修改用户
  • 根据用户名查询用户
  • 用户登录
  • 检查用户是否登录
  • 用户退出登录
  • 注销用户

3 数据持久层

持久层首先需要创建一张用户表,包含了用户的基本信息:用户名、密码、真实姓名、手机号、邮箱等,还包括一些数据的基本信息,比如创建时间、修改时间等。

创建完用户表后,就要进行一些基础配置,包括:引入持久层框架、持久层配置文件以及添加持久层接口扫描器

这边尤其需要说的是添加持久层接口扫描器 ,这个注解是加在启动类上的,它的作用是让你只写接口,不用写实现类,就可以操作数据库。

扫描器在项目启动时,会按照注解中的包名去寻找所有的接口文件,并查找对应的sql语句在哪(可能是XML文件,也可能是注解),然后通过JDK动态代理,在内存中生成一个虚拟的实现类,并放入IOC容器中,这样我们在Service层中注入时,注入的就是这个虚拟代理。

java 复制代码
@MapperScan("com.nageoffer.shortlink.admin.dao.mapper")

下面开始写代码。

用一个实体类UserDO来封装数据库的User:

java 复制代码
@TableName("t_user")
@Data
public class UserDO {
    /**
     * ID
     */
    private Long id;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 真实姓名
     */
    private String realName;

    /**
     * 手机号
     */
    private String phone;

    /**
     * 邮箱
     */
    private String mail;

    /**
     * 注销时间戳
     */
    private Long deletionTime;

    /**
     * 创建时间
     */
    private Date createTime;

    /**
     * 修改时间
     */
    private Date updateTime;

    /**
     * 删除标识 0:未删除 1:已删除
     */
    private Integer delFlag;
}

4.异常设计

4.1异常码说明

异常码的设计参考阿里巴巴开发手册(泰山版),其核心设计理念是:一看错误码,就知道是哪一端报错了

阿里规定,错误码不应该是简单的数字,而应该是一个5位的字符串。

4.2 异常码设计

手册将错误分为了三大类型,分别对应三个字母:
A:用户端错误 :用户(前端)的错,比如用户输入了空的用户名,或是密码长度不够等。
B:系统执行出错 :后端的错,例如空指针异常,数据库连接超时,或是代码有问题,这种比较严重 ,需要立即报警。
C:远程调用出错:别人的错,依赖的外部服务有问题,例如调用微信支付接口,结果微信返回超时。

  • IErrorCode.java
java 复制代码
/**
 * 平台错误码
 */
public interface IErrorCode {

    /**
     * 错误码
     */
    String code();

    /**
     * 错误信息
     */
    String message();
}
  • BaseErrorCode.java
java 复制代码
public enum BaseErrorCode implements IErrorCode {

    // ========== 一级宏观错误码 客户端错误 ==========
    CLIENT_ERROR("A000001", "用户端错误"),

    // ========== 二级宏观错误码 用户注册错误 ==========
    USER_REGISTER_ERROR("A000100", "用户注册错误"),
    USER_NAME_VERIFY_ERROR("A000110", "用户名校验失败"),
    USER_NAME_EXIST_ERROR("A000111", "用户名已存在"),
    USER_NAME_SENSITIVE_ERROR("A000112", "用户名包含敏感词"),
    USER_NAME_SPECIAL_CHARACTER_ERROR("A000113", "用户名包含特殊字符"),
    PASSWORD_VERIFY_ERROR("A000120", "密码校验失败"),
    PASSWORD_SHORT_ERROR("A000121", "密码长度不够"),
    PHONE_VERIFY_ERROR("A000151", "手机格式校验失败"),

    // ========== 二级宏观错误码 系统请求缺少幂等Token ==========
    IDEMPOTENT_TOKEN_NULL_ERROR("A000200", "幂等Token为空"),
    IDEMPOTENT_TOKEN_DELETE_ERROR("A000201", "幂等Token已被使用或失效"),

    // ========== 一级宏观错误码 系统执行出错 ==========
    SERVICE_ERROR("B000001", "系统执行出错"),
    // ========== 二级宏观错误码 系统执行超时 ==========
    SERVICE_TIMEOUT_ERROR("B000100", "系统执行超时"),

    // ========== 一级宏观错误码 调用第三方服务出错 ==========
    REMOTE_ERROR("C000001", "调用第三方服务出错");

    private final String code;

    private final String message;

    BaseErrorCode(String code, String message) {
        this.code = code;
        this.message = message;
    }

    @Override
    public String code() {
        return code;
    }

    @Override
    public String message() {
        return message;
    }
}

4.3 异常设计

第一部分:拦截机制

在SpringBoot中,通过AOP 设计一个全局异常拦截器,一旦报错,异常都会向外抛出,全局异常拦截器会在异常到达前端之前抓住它,并把它格式化为一个JSON格式对象返回给前端。

java 复制代码
/**
 * 全局异常处理器
 *
 */
@Component
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 拦截参数验证异常
     */
    @SneakyThrows
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result validExceptionHandler(HttpServletRequest request, MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();

        FieldError firstFieldError = CollectionUtil.getFirst(bindingResult.getFieldErrors());
        String exceptionStr = Optional.ofNullable(firstFieldError)
                .map(FieldError::getDefaultMessage)
                .orElse(StrUtil.EMPTY);
        log.error("[{}] {} [ex] {}", request.getMethod(), getUrl(request), exceptionStr);
        return Results.failure(BaseErrorCode.CLIENT_ERROR.code(), exceptionStr);
    }

    /**
     * 拦截应用内抛出的异常
     */
    @ExceptionHandler(value = {AbstractException.class})
    public Result abstractException(HttpServletRequest request, AbstractException ex) {
        if (ex.getCause() != null) {
            log.error("[{}] {} [ex] {}", request.getMethod(), request.getRequestURL().toString(), ex.toString(), ex.getCause());
            return Results.failure(ex);
        }
        log.error("[{}] {} [ex] {}", request.getMethod(), request.getRequestURL().toString(), ex.toString());
        return Results.failure(ex);
    }

    /**
     * 拦截未捕获异常
     */
    @ExceptionHandler(value = Throwable.class)
    public Result defaultErrorHandler(HttpServletRequest request, Throwable throwable) {
        log.error("[{}] {} ", request.getMethod(), getUrl(request), throwable);
        return Results.failure();
    }

    private String getUrl(HttpServletRequest request) {
        if (StringUtils.isEmpty(request.getQueryString())) {
            return request.getRequestURL().toString();
        }
        return request.getRequestURL().toString() + "?" + request.getQueryString();
    }
}

第二部分:异常体系

首先定义一个抽象规约异常,它是所有自定义异常的父类,它通常继承自RunTimeException,要求所有子类必须包含两个核心要素:错误码和错误信息,其下面又有三大分支。

  • AbstractException.java
java 复制代码
@Getter
public abstract class AbstractException extends RuntimeException {

    public final String errorCode;

    public final String errorMessage;

    public AbstractException(String message, Throwable throwable, IErrorCode errorCode) {
        super(message, throwable);
        this.errorCode = errorCode.code();
        this.errorMessage = Optional.ofNullable(StringUtils.hasLength(message) ? message : null).orElse(errorCode.message());
    }
}
  • ClientException.java
java 复制代码
/**
 * 客户端异常
 */
public class ClientException extends AbstractException {

    public ClientException(IErrorCode errorCode) {
        this(null, null, errorCode);
    }

    public ClientException(String message) {
        this(message, null, BaseErrorCode.CLIENT_ERROR);
    }

    public ClientException(String message, IErrorCode errorCode) {
        this(message, null, errorCode);
    }

    public ClientException(String message, Throwable throwable, IErrorCode errorCode) {
        super(message, throwable, errorCode);
    }

    @Override
    public String toString() {
        return "ClientException{" +
                "code='" + errorCode + "'," +
                "message='" + errorMessage + "'" +
                '}';
    }
}
  • ServiceException.java
java 复制代码
/**
 * 服务端异常
 */
public class ServiceException extends AbstractException {

    public ServiceException(String message) {
        this(message, null, BaseErrorCode.SERVICE_ERROR);
    }

    public ServiceException(IErrorCode errorCode) {
        this(null, errorCode);
    }

    public ServiceException(String message, IErrorCode errorCode) {
        this(message, null, errorCode);
    }

    public ServiceException(String message, Throwable throwable, IErrorCode errorCode) {
        super(Optional.ofNullable(message).orElse(errorCode.message()), throwable, errorCode);
    }

    @Override
    public String toString() {
        return "ServiceException{" +
                "code='" + errorCode + "'," +
                "message='" + errorMessage + "'" +
                '}';
    }
}
  • RemoteException.java
java 复制代码
/**
 * 远程服务调用异常
 */
public class RemoteException extends AbstractException {

    public RemoteException(String message) {
        this(message, null, BaseErrorCode.REMOTE_ERROR);
    }

    public RemoteException(String message, IErrorCode errorCode) {
        this(message, null, errorCode);
    }

    public RemoteException(String message, Throwable throwable, IErrorCode errorCode) {
        super(message, throwable, errorCode);
    }

    @Override
    public String toString() {
        return "RemoteException{" +
                "code='" + errorCode + "'," +
                "message='" + errorMessage + "'" +
                '}';
    }
}

5.全局统一返回实体

如果没有统一的返回实体,那么不同的接口返回的数据就是千奇百怪的,查询用户会返回一个User对象,删除用户又会返回一个Boolean对象,前端会无法统一判断请求是否成功,也不知道如何去找错误提示。因此需要一个Result< T >对象,将返回值标准化。

  • Result.java
java 复制代码
/**
 * 全局返回对象
 */
@Data
@Accessors(chain = true)
public class Result<T> implements Serializable {

    @Serial
    private static final long serialVersionUID = 5679018624309023727L;

    /**
     * 正确返回码
     */
    public static final String SUCCESS_CODE = "0";

    /**
     * 返回码
     */
    private String code;

    /**
     * 返回消息
     */
    private String message;

    /**
     * 响应数据
     */
    private T data;

    /**
     * 请求ID
     */
    private String requestId;

    public boolean isSuccess() {
        return SUCCESS_CODE.equals(code);
    }
}

Result对象是一个纯粹的数据容器,它定义了返回值包含哪些字段(code、msg、data)。
:这里的@Accessors(chain = true)注解是一个好用的工具,它允许链式调用,如:new Result().setCode("0").setMessage("OK")

  • Results.java
java 复制代码
/**
 * 全局返回对象构造器
 */
public final class Results {

    /**
     * 构造成功响应
     */
    public static Result<Void> success() {
        return new Result<Void>()
                .setCode(Result.SUCCESS_CODE);
    }

    /**
     * 构造带返回数据的成功响应
     */
    public static <T> Result<T> success(T data) {
        return new Result<T>()
                .setCode(Result.SUCCESS_CODE)
                .setData(data);
    }

    /**
     * 构建服务端失败响应
     */
    public static Result<Void> failure() {
        return new Result<Void>()
                .setCode(BaseErrorCode.SERVICE_ERROR.code())
                .setMessage(BaseErrorCode.SERVICE_ERROR.message());
    }

    /**
     * 通过 {@link AbstractException} 构建失败响应
     */
    public static Result<Void> failure(AbstractException abstractException) {
        String errorCode = Optional.ofNullable(abstractException.getErrorCode())
                .orElse(BaseErrorCode.SERVICE_ERROR.code());
        String errorMessage = Optional.ofNullable(abstractException.getErrorMessage())
                .orElse(BaseErrorCode.SERVICE_ERROR.message());
        return new Result<Void>()
                .setCode(errorCode)
                .setMessage(errorMessage);
    }

    /**
     * 通过 errorCode、errorMessage 构建失败响应
     */
    public static Result<Void> failure(String errorCode, String errorMessage) {
        return new Result<Void>()
                .setCode(errorCode)
                .setMessage(errorMessage);
    }
}

Results类是一个工具类,用于构造,它负责生产结果。它与Result类进行了分离,好处是把复杂的创建逻辑和Result的属性分离开来,不让Result类显的很臃肿。

6.用户敏感信息脱敏展示

对于用户的一些敏感信息,如手机、身份证号等,我们是需要进行脱敏展示的,这里只针对手机号进行脱敏展示,使用序列化器。

java 复制代码
/**
 * 手机号脱敏反序列化
 */
public class PhoneDesensitizationSerializer extends JsonSerializer<String> {

    @Override
    public void serialize(String phone, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        String phoneDesensitization = DesensitizedUtil.mobilePhone(phone);
        jsonGenerator.writeString(phoneDesensitization);
    }
}

另外要在返回对象实体类的对应属性上打上标签:

java 复制代码
	/**
     * 手机号
     */
    @JsonSerialize(using = PhoneDesensitizationSerializer.class)
    private String phone;

使用apifox进行测试,发现完美达成预期目标:

相关推荐
一念一花一世界8 小时前
Arbess从基础到实践(23) - 集成GitLab+Hadess实现Java项目构建并上传制品
java·gitlab·cicd·arbess·制品库
啃火龙果的兔子8 小时前
Java 学习路线及学习周期
java·开发语言·学习
Selegant8 小时前
Quarkus vs Spring Boot:谁更适合云原生时代的 Java 开发?
java·spring boot·云原生
ss2738 小时前
SpringBoot+Vue宠物商城系统
java
梦里不知身是客118 小时前
spark的统一内存管理机制
java·大数据·spark
济南壹软网络科技有限公司8 小时前
高并发电商实战:基于Java生态的多元化盲盒系统技术实现方案
java·开发语言·开源·盲盒源码·盲盒定制开发
大白的编程日记.8 小时前
【计算网络学习笔记】TCP套接字介绍和使用
网络·笔记·学习
色空大师8 小时前
【linux查看日志】
java·linux·运维·日志