前言
Day02 要先把后台接口跑顺:请求从 Apifox 发出,带着 token 进入后端,参数封装成 DTO,Service 转成 Entity,再结合配置、异常处理、分页和日期格式这些公共机制完成一次接口自测。
这一篇不展开员工 CRUD,只整理这些后面写功能时会反复用到的基础动作。
一、看接口文档时先确认请求能不能进来
苍穹外卖项目先按端区分接口:
text
管理端:/admin
用户端:/user
后台管理端接口都在 /admin 下面,用户端小程序接口都在 /user 下面。看接口文档时,我现在会先确认四件事:
| 要看什么 | 具体看哪里 | 容易错的地方 |
|---|---|---|
| 请求路径 | /admin/** 还是 /user/** |
管理端和用户端路径混用 |
| 请求方式 | GET、POST、PUT、DELETE |
用错方式会直接 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 的字段按照接口请求设计,不是按照数据库表完整设计。这里没有 password、status、createTime、updateTime,因为这些字段不应该完全由前端决定。
看接口文档时,可以把 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 里的 idNumber,create_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 会拷贝同名属性,比如 username、name、phone、sex、idNumber。
拷贝以后,还要补前端不能决定的字段。比如状态、默认密码、创建时间、创建人、更新时间、更新人。这些字段如果交给前端传,接口就不安全,也不符合后端统一管理的思路。
当前项目里,状态值放在常量类中:
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();
这个写法表达得很明确:这次只关心 id 和 status。如果某次更新只需要少数字段,Builder 会比先 new 再连续 set 更清楚。
五、application.yml 和 application-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");
可以用接口返回里的 createTime、updateTime 这类字段验证。如果返回格式是 yyyy-MM-dd HH:mm:ss,说明 JSON 时间格式配置生效。
这个问题给我的真正提醒是:版本相关代码不要直接复制旧教程。以后遇到类似问题,按这个顺序处理:
text
先看当前项目 pom.xml
-> 再看当前代码已经用了哪个类
-> 再查当前官方文档
-> 最后决定是否替换旧写法
总结:
以上就是day02的笔记,我是程序猿乐锅,期待你的三连