【苍穹外卖|Day02】后台接口自测闭环:Token、DTO 与 yml 配置

前言

Day02 要先把后台接口跑顺:请求从 Apifox 发出,带着 token 进入后端,参数封装成 DTO,Service 转成 Entity,再结合配置、异常处理、分页和日期格式这些公共机制完成一次接口自测。

这一篇不展开员工 CRUD,只整理这些后面写功能时会反复用到的基础动作。

一、看接口文档时先确认请求能不能进来

苍穹外卖项目先按端区分接口:

text 复制代码
管理端:/admin
用户端:/user

后台管理端接口都在 /admin 下面,用户端小程序接口都在 /user 下面。看接口文档时,我现在会先确认四件事:

要看什么 具体看哪里 容易错的地方
请求路径 /admin/** 还是 /user/** 管理端和用户端路径混用
请求方式 GETPOSTPUTDELETE 用错方式会直接 404 或 405
参数位置 body、query、path、header 明明传了参数,但后端接不到
必填/非必填 接口文档里的字段说明 非必填条件传空时 SQL 要能处理

这一步看起来简单,但能提前排掉很多低级问题。比如后台接口没带 token,业务代码还没执行,请求就可能被拦截器挡住。

二、Apifox 登录后自动携带 token

后台接口自测的第一步是让 token 自动带上。手动复制 token 不稳定,尤其是接口多了以后,很容易忘记换请求头。

1. 登录接口后置操作保存 token

员工登录成功后,在 Apifox 的登录接口后置操作里写:

javascript 复制代码
const res = pm.response.json();

if (res.code === 1 && res.data) {
  pm.environment.set("token", res.data.token || "");
  pm.environment.set("userId", String(res.data.id || ""));
  pm.environment.set("username", res.data.username || "");
  pm.environment.set("name", res.data.name || "");
}

然后在接口的前置操作加上

复制代码
const token = pm.environment.get("token");

if (token) {

  pm.request.headers.upsert({

    key: "token",

    value: token

  });

}

这样前端每次访问都回带着token

登录成功后,把后端返回的数据保存到环境变量里。后面接口不用再手动复制 token。

三、DTO、Entity、VO 先按数据流方向理解

Day02 里开始出现 DTO、Entity、VO。不要先背概念,先看数据流方向。

text 复制代码
DTO:前端 -> 后端
Entity:后端 -> 数据库
VO:后端 -> 前端

1. DTO:接收前端传来的字段

EmployeeDTO 是前端请求参数对象:

java 复制代码
@Data
public class EmployeeDTO implements Serializable {

    private Long id;
    private String username;
    private String name;
    private String phone;
    private String sex;
    private String idNumber;
}

DTO 的字段按照接口请求设计,不是按照数据库表完整设计。这里没有 passwordstatuscreateTimeupdateTime,因为这些字段不应该完全由前端决定。

看接口文档时,可以把 DTO 当作后端接收参数的落点:文档里传什么字段,先看 DTO 里有没有对应属性。

2. Entity:对应数据库字段

Employee 更接近数据库表:

java 复制代码
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Employee implements Serializable {

    private Long id;
    private String username;
    private String name;
    private String password;
    private String phone;
    private String sex;
    private String idNumber;
    private Integer status;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
    private Long createUser;
    private Long updateUser;
}

项目里 MyBatis 开启了驼峰命名映射:

yaml 复制代码
mybatis:
  configuration:
    map-underscore-to-camel-case: true

所以数据库里的 id_number 可以映射到 Java 里的 idNumbercreate_time 可以映射到 createTime

3. VO:控制返回给前端的数据

登录成功后返回的是 EmployeeLoginVO

java 复制代码
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "员工登录返回的数据格式")
public class EmployeeLoginVO implements Serializable {

    private Long id;
    private String userName;
    private String name;
    private String token;
}

VO 的意义是控制响应结构。后端不是把数据库对象原样返回,而是按前端真正需要的字段组装返回结果。

这一节最重要的判断方法是:

text 复制代码
接口传参先找 DTO
数据库字段先找 Entity
接口响应先找 VO

后面看一个功能时,先按这三个方向拆,会比直接翻 Controller 更稳。

四、DTO 转 Entity:Service 层补前端不能决定的字段

前端传来的 DTO 一般不会直接入库。Service 层会先把 DTO 转成 Entity,再补业务字段。

项目里常用这个写法:

java 复制代码
Employee employee = new Employee();
BeanUtils.copyProperties(employeeDTO, employee);

BeanUtils.copyProperties 会拷贝同名属性,比如 usernamenamephonesexidNumber

拷贝以后,还要补前端不能决定的字段。比如状态、默认密码、创建时间、创建人、更新时间、更新人。这些字段如果交给前端传,接口就不安全,也不符合后端统一管理的思路。

当前项目里,状态值放在常量类中:

java 复制代码
public class StatusConstant {
    public static final Integer ENABLE = 1;
    public static final Integer DISABLE = 0;
}

默认密码也放在常量类里。博客展示这类内容时建议脱敏:

java 复制代码
public class PasswordConstant {
    public static final String DEFAULT_PASSWORD = "******";
}

这样写的价值不是少写几行代码,而是把规则集中起来。以后状态值或者默认密码策略调整时,不需要到处找硬编码。

Builder 适合只给少数字段赋值

Employee 类上有 @Builder

java 复制代码
@Builder
public class Employee implements Serializable {
    // ...
}

只构造部分字段时,可以这样写:

java 复制代码
Employee employee = Employee.builder()
        .id(id)
        .status(status)
        .build();

这个写法表达得很明确:这次只关心 idstatus。如果某次更新只需要少数字段,Builder 会比先 new 再连续 set 更清楚。

五、application.ymlapplication-dev.yml:配置要看取值链路

项目配置不是只看某一个文件。当前项目的取值链路是:

text 复制代码
application.yml 激活 dev 环境
-> application.yml 中使用 ${...} 占位符
-> application-dev.yml 提供开发环境真实值

1. application.yml 负责公共结构

application.yml 里先指定当前环境:

yaml 复制代码
spring:
  profiles:
    active: dev

这表示项目启动时会加载 application-dev.yml

数据源配置里用了占位符:

yaml 复制代码
spring:
  datasource:
    driver-class-name: ${sky.datasource.driver-class-name}
    url: jdbc:mysql://${sky.datasource.host}:${sky.datasource.port}/${sky.datasource.database}?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
    username: ${sky.datasource.username}
    password: ${sky.datasource.password}

这里的 ${sky.datasource.host} 不是字面量。它会去当前激活的配置文件里找 sky.datasource.host

如果数据库连接失败,先不要只盯着 JDBC URL。可以按这个顺序查:

text 复制代码
spring.profiles.active 是否是 dev
-> application-dev.yml 里 sky.datasource.* 是否存在
-> yml 缩进是否正确
-> 数据库地址、端口、库名、账号、密码是否能连通

2. application-dev.yml 负责开发环境真实值

application-dev.yml 里放本地开发环境配置。写博客时要脱敏,不能把密码、AccessKey、Secret 原样贴出来。

可以这样展示:

yaml 复制代码
sky:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    host: localhost
    port: 3306
    database: sky_take_out
    username: root
    password: ******

  redis:
    host: localhost
    port: 6379
    password: ******
    database: 0

  aliyun:
    oss:
      endpoint: https://oss-cn-beijing.aliyuncs.com
      bucket-name: sky-take-out-***
      region: cn-beijing
      access-key-id: ******
      access-key-secret: ******

这里最需要注意的是密码和密钥。开发环境配置可以在本地使用真实值,但公开文章、截图、仓库里都要脱敏。

六、全局异常处理:把重复账号变成统一响应

笔记里提到一个 bug:账号已存在时,不能重复添加。

当前项目把数据库唯一约束异常交给全局异常处理器:

java 复制代码
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex) {

        String message = ex.getMessage();
        if (message.contains("Duplicate entry")) {
            String[] split = message.split(" ");
            String username = split[2];
            String msg = username + MessageConstant.ALREADY_EXISTS;
            return Result.error(msg);
        } else {
            return Result.error(MessageConstant.UNKNOWN_ERROR);
        }
    }
}

这样 Controller 和 Service 不需要到处写 try-catch。数据库抛出重复数据异常后,统一转换成 Result.error(...)

自测时可以用同一个账号提交两次。如果第二次返回"账号已存在"这类业务提示,说明异常已经被全局处理器接住了。若返回未知错误,可以继续查:

text 复制代码
数据库是否真的有唯一约束
-> 异常类型是否是 SQLIntegrityConstraintViolationException
-> 异常 message 中是否包含 Duplicate entry
-> MessageConstant.ALREADY_EXISTS 是否是预期文案

这个写法的边界也要知道:它依赖数据库异常文本。课程项目里适合理解全局异常处理;真实项目里,更稳的是结合错误码、约束名或提前业务校验。

七、ThreadLocal:当前操作人要能在线程里传下去

项目里有一个 BaseContext

java 复制代码
package com.sky.context;

public class BaseContext {

    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }

    public static Long getCurrentId() {
        return threadLocal.get();
    }

    public static void removeCurrentId() {
        threadLocal.remove();
    }
}

它用来保存当前请求里的登录用户 id。一次请求进来后,如果拦截器能从 token 中解析出员工 id,就可以放到 BaseContext 里。后面 Service、AOP 或自动填充字段时,再通过 BaseContext.getCurrentId() 获取当前操作人。

这条链路可以这样看:

text 复制代码
请求头携带 token
-> 拦截器解析 JWT
-> 得到当前员工 id
-> BaseContext.setCurrentId(empId)
-> 后续代码 BaseContext.getCurrentId()
-> 自动填充 createUser / updateUser

如果创建人、更新人没有写入数据库,不要只看实体类字段。应该沿着这条链路查:

  • 拦截器是否解析到了员工 id
  • 是否调用了 BaseContext.setCurrentId(...)
  • Mapper 方法上是否有自动填充注解
  • 自动填充切面里是否真的给字段赋值

ThreadLocal 还有一个容易忽略的点:线程会复用。请求结束后应及时清理,避免上一次请求的数据留在当前线程里。

八、Spring MVC 日期格式:版本敏感代码先查当前依赖

笔记里记录的卡点是:老师旧代码使用 MappingJackson2HttpMessageConverter,但当前项目版本不适合继续照搬。

当前项目使用 Spring Boot 4.0.6。通过 Context7 查询 Spring Framework 当前 Javadoc,可以看到 MappingJackson2HttpMessageConverter 构造器已经标记为 deprecated。项目里使用的是:

java 复制代码
new JacksonJsonHttpMessageConverter(new JacksonObjectMapper())

日期格式配置分成两块。

第一块:Spring MVC 参数绑定

java 复制代码
@Override
public void addFormatters(FormatterRegistry registry) {
    DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
    registrar.setDateTimeFormatter(JacksonObjectMapper.DATE_TIME_FORMATTER);
    registrar.registerFormatters(registry);
}

这块处理的是请求参数绑定时的时间格式。

第二块:JSON 序列化和反序列化

java 复制代码
@Override
public void configureMessageConverters(HttpMessageConverters.ServerBuilder converters) {
    log.info("配置消息转换器...");
    converters.withJsonConverter(new JacksonJsonHttpMessageConverter(new JacksonObjectMapper()));
}

JacksonObjectMapper 里统一定义了时间格式:

java 复制代码
public static final DateTimeFormatter DATE_TIME_FORMATTER =
        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

可以用接口返回里的 createTimeupdateTime 这类字段验证。如果返回格式是 yyyy-MM-dd HH:mm:ss,说明 JSON 时间格式配置生效。

这个问题给我的真正提醒是:版本相关代码不要直接复制旧教程。以后遇到类似问题,按这个顺序处理:

text 复制代码
先看当前项目 pom.xml
-> 再看当前代码已经用了哪个类
-> 再查当前官方文档
-> 最后决定是否替换旧写法

总结:

以上就是day02的笔记,我是程序猿乐锅,期待你的三连

相关推荐
心之伊始1 小时前
Spring Boot Actuator + Micrometer 自定义业务指标:不只是健康检查
java·架构·源码分析·csdn
冰暮流星1 小时前
javascript之对象的建立-使用Object
开发语言·javascript·ecmascript
Eason_LYC1 小时前
【GetShell 实战】CVE-2026-34486 Tomcat 加密拦截器绕过:从漏洞验证到反弹 Shell 全流程
java·渗透测试·tomcat·java反序列化·rce·远程代码执行漏洞·cve-2026-34486
qq_2518364571 小时前
基于java 税务管理系统设计与实现
java·开发语言
LuminousCPP1 小时前
从零开始学 C++|系列开篇:从 C 到 C++ 的衔接之路
开发语言·c++·笔记
超梦dasgg1 小时前
Java 生产环境分布式定时任务全解(实战落地版)
java·开发语言·分布式
Legendary_0081 小时前
18-30W 便携照明设备 USB-C PD 升级:选型与设计要点
c语言·开发语言
破土士V1 小时前
Java基础知识集合
java·开发语言
keykey6.1 小时前
从感知机到神经网络:深度学习的起源
开发语言·人工智能·深度学习·机器学习