概念先行
- DTO(Data Transfer Object)是什么
- 面向“数据传输”的中立模型,用来在系统间/层间传递数据(Controller⇄Service、服务A⇄服务B、RPC/消息等)。
- 强调输入/输出契约与序列化稳定性,通常不包含展示格式、也不带业务逻辑。
- 典型拆分:命令/变更类(Create/Update DTO)、查询类(Query DTO)、对外返回类(Response DTO)。
- VO(View Object)是什么
- 面向“展示/视图”的模型,专门为前端页面/客户端界面定制。
- 字段可以是组合/计算后的展示字段(如时间字符串、状态文案、头像全路径、是否关注等),为“好看好用”而服务。
- 只负责展示,不建议作为内部领域/持久化的输入。
什么时候用 DTO,什么时候用 VO
- 入参(从客户端到后端)优先用 DTO
- 如创建/修改/查询条件:
UserCreateDTO、UserUpdateDTO、UserQueryDTO
- 可携带校验注解(非空、格式、范围),清晰表述“接收什么数据”
- 出参(从后端到客户端)优先用 VO
- 如详情/列表项/分页结果:
UserDetailVO、UserListItemVO、UserProfileVO
- 可包含展示友好字段(格式化时间、枚举文案、组合字段)
简言之:输入→DTO,输出→VO。DTO更“中立契约”,VO更“贴近 UI”。
在你的项目里如何落地
- 现在已有:
UserInfo 是 PO(持久层实体,不建议直接暴露给前端)
UserInfoQuery 是查询模型(本质更像 Query DTO)
ResponseVO、PaginationResultVO 是“响应包装/分页结果 VO”
- 建议补齐:
- 新增入参 DTO:
UserInfoAddDTO、UserInfoUpdateDTO、UserInfoQueryDTO(或沿用 UserInfoQuery)
- 新增出参 VO:
UserInfoDetailVO、UserInfoListItemVO
- 映射关系:
- Controller 接 DTO → Service → 转 PO 落库
- Service 查到 PO → 转 VO 返回前端
- 转换工具:用
MapStruct(推荐,编译期生成)或现有 CopyTools
命名与分层建议
- 命名
- 入参:
XxxCreateDTO、XxxUpdateDTO、XxxQueryDTO
- 出参:
XxxDetailVO、XxxListItemVO
- 包结构:
controller/dto、controller/vo 或 interfaces/dto、interfaces/vo
- 分层
- Controller:仅收发 DTO/VO,不碰 PO
- Service:面向 PO、领域对象,边界使用 DTO/VO
- Mapper:只认 PO 与查询条件模型
常见误区
- 用 PO 直接当入参/出参(易暴露内部字段,如
password;耦合数据库变更)
- 用 VO 做入参(VO关注展示,往往包含只读字段,作为入参易混乱)
- 让 VO/DTO 混用或下沉到 Mapper 层(破坏分层职责)
- 出参未做格式化(让前端处理后端细节,如时间格式/枚举文案)
一个小决策清单(不纠结版)
- 这是“请求体/查询条件/命令”吗?→ 用 DTO
- 这是“返回给页面/APP的展示数据”吗?→ 用 VO
- 需格式化/组合字段、转文案吗?→ VO
- 需做字段合法性校验(后端接收时)吗?→ DTO
GQ Video中的VO、DTO、Query
简明区分与用法
-
VO(View Object)
-
定义:面向“返回给前端视图”的对象。
-
用法:Controller 返回时用 VO 封装结果,屏蔽内部实现与表结构。
-
在本项目中:
- 统一响应外壳:
com.easylive.entity.vo.ResponseVO
- 分页结果:
com.easylive.entity.vo.PaginationResultVO
- 部分业务返回也会自定义 VO(例如组合多个来源的展示数据)。
-
DTO(Data Transfer Object)
-
定义:服务/层间传输的数据对象,承载业务语义,避免直接暴露数据库实体。
-
用法:Service 层之间、缓存、搜索引擎、消息传递等使用;也可作为接口响应体(当含业务含义时)。
-
在本项目中(位置:
):
TokenUserInfoDto:登录后放入 Redis/Cookie 的用户会话数据
UserCountInfoDto:用户粉丝数、消息数等聚合结果
VideoInfoEsDto、VideoPlayInfoDto 等:用于搜索/播放等跨层传输的结构
-
Query(查询参数对象)
-
定义:封装“列表/分页/条件查询”的输入参数。
-
用法:Controller 组装 Query → 传 Service/Mapper 执行分页、排序、筛选;Query 通常继承/组合分页基础类。
-
在本项目中(位置:
1
| com.easylive.entity.query
|
):
BaseParam、SimplePage:分页与通用查询基类
VideoInfoPostQuery、UserInfoQuery 等:带 orderBy、pageNo/pageSize、筛选字段
项目中的典型调用链
- Controller 入参(原生参数/表单) → 组装 Query 或直接传简单参数
- Service 处理:
- 读写 DB:可能将 DO 转成 DTO 返回
- 组装统计结果:返回 DTO/VO
- Controller 出参:最终用
ResponseVO 包装,返回给前端
关键代码示例(均来自当前代码库)
1 2 3 4 5 6
| @RequestMapping("/checkCode") public ResponseVO<?> checkCode() { ... return getSuccessResponseVO(result); }
|
1 2 3 4 5 6 7
| import com.easylive.entity.dto.TokenUserInfoDto; import com.easylive.entity.dto.UserCountInfoDto; public ResponseVO<?> autoLogin(HttpServletResponse response) { TokenUserInfoDto tokenUserInfoDto = getTokenUserInfoDto(); ... return getSuccessResponseVO(tokenUserInfoDto); // 返回 DTO }
|
1 2 3 4 5 6 7 8 9 10 11 12
| @RequestMapping("/loadVideoList") public ResponseVO<?> loadVideoList(Integer status, Integer pageNo, String videoNameFuzzy) { VideoInfoPostQuery videoInfoQuery = new VideoInfoPostQuery(); videoInfoQuery.setPageNo(pageNo == null ? 1 : pageNo); videoInfoQuery.setPageSize(10); videoInfoQuery.setStatus(status); videoInfoQuery.setVideoNameFuzzy(videoNameFuzzy); videoInfoQuery.setOrderBy("create_time desc"); videoInfoQuery.setQueryCountInfo(true); PaginationResultVO<?> resultVO = videoInfoPostService.findListByPage(videoInfoQuery); return getSuccessResponseVO(resultVO); }
|
设计原则与最佳实践
- VO
- 只面向“输出”。避免把 DO(数据库实体)直接返回前端。
- 统一外层结构(本项目用
ResponseVO),便于前端处理统一的 code/msg/data。
- DTO
- 描述业务意义的数据载体。可跨层使用(Service ↔ 缓存/搜索/消息),避免 Controller 直接拼装复杂结构。
- 可承载多个来源的聚合信息,减少 Controller 逻辑。
- Query
- 只用于“输入的查询条件”,不承载业务结果。
- 与分页基类解耦(
SimplePage + BaseParam),避免每个 Query 重复分页字段。
- 命名上建议遵循
[业务对象]Query,字段尽量贴近筛选条件名称(如 xxxFuzzy、statusList、orderBy)。
快速判断口诀
- 我这是“返回给前端”的结构吗?是 → VO(外层统一用
ResponseVO)
- 我是在层与层之间传递业务数据?是 → DTO
- 我是“分页/筛选/排序”的输入条件?是 → Query
模版示例
下面给你一套可直接套用的模板示例(Query/DTO/VO/Controller/Service),你复制到对应包里改类名和字段即可。
1) Query 模板(用于分页筛选)
建议放在 com.easylive.entity.query 下,命名 FooQuery.java。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| package com.easylive.entity.query;
public class FooQuery extends BaseParam { private String fooId;
private String fooNameFuzzy;
private Integer status;
private String orderBy;
public String getFooId() { return fooId; } public void setFooId(String fooId) { this.fooId = fooId; }
public String getFooNameFuzzy() { return fooNameFuzzy; } public void setFooNameFuzzy(String fooNameFuzzy) { this.fooNameFuzzy = fooNameFuzzy; }
public Integer getStatus() { return status; } public void setStatus(Integer status) { this.status = status; }
public String getOrderBy() { return orderBy; } public void setOrderBy(String orderBy) { this.orderBy = orderBy; } }
|
用法要点:
- 分页字段继承自
BaseParam(已有 pageNo/pageSize/queryCountInfo 等)。
- 模糊查询后缀建议用
Fuzzy。
- 排序字段统一用
orderBy。
2) DTO 模板(跨层传输/业务语义)
建议放在 com.easylive.entity.dto 下,命名 FooDto.java。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| package com.easylive.entity.dto;
import java.io.Serializable; import java.util.List;
public class FooDto implements Serializable { private String fooId; private String fooName; private Integer status; private Long createTime;
private Integer relatedCount; private List<String> tags;
public String getFooId() { return fooId; } public void setFooId(String fooId) { this.fooId = fooId; }
public String getFooName() { return fooName; } public void setFooName(String fooName) { this.fooName = fooName; }
public Integer getStatus() { return status; } public void setStatus(Integer status) { this.status = status; }
public Long getCreateTime() { return createTime; } public void setCreateTime(Long createTime) { this.createTime = createTime; }
public Integer getRelatedCount() { return relatedCount; } public void setRelatedCount(Integer relatedCount) { this.relatedCount = relatedCount; }
public List<String> getTags() { return tags; } public void setTags(List<String> tags) { this.tags = tags; } }
|
用法要点:
- 不直接暴露数据库实体(DO/PO),DTO 聚合业务需要返回的数据即可。
- 可以包含多个来源数据的组合(统计/扩展)。
3) VO 模板(面向前端返回)
建议放在 com.easylive.entity.vo 下,命名 FooVO.java(可选)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package com.easylive.entity.vo;
import java.io.Serializable;
public class FooVO implements Serializable { private String id; private String name; private String statusText;
public String getId() { return id; } public void setId(String id) { this.id = id; }
public String getName() { return name; } public void setName(String name) { this.name = name; }
public String getStatusText() { return statusText; } public void setStatusText(String statusText) { this.statusText = statusText; } }
|
用法要点:
- 如果直接返回 DTO 也能满足需求,可不额外建 VO。
- 需要展示友好/脱敏/字段名重命名时,用 VO。
4) Service 模板(Query 入参,返回 DTO/分页)
接口(com.easylive.service.FooService):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package com.easylive.service;
import com.easylive.entity.dto.FooDto; import com.easylive.entity.query.FooQuery; import com.easylive.entity.vo.PaginationResultVO;
import java.util.List;
public interface FooService { PaginationResultVO<FooDto> findListByPage(FooQuery query);
List<FooDto> findListByParam(FooQuery query);
FooDto getById(String fooId); }
|
实现(com.easylive.service.impl.FooServiceImpl)里:
- 使用
FooQuery 传分页/筛选条件;
- 读取数据库实体后转为
FooDto;
- 分页返回
PaginationResultVO<FooDto>。
5) Controller 模板(入参→Query,出参→ResponseVO)
放在 com.easylive.controller 下,命名 FooController.java。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| package com.easylive.controller;
import com.easylive.annotation.GlobalInterceptor; import com.easylive.entity.dto.FooDto; import com.easylive.entity.query.FooQuery; import com.easylive.entity.vo.PaginationResultVO; import com.easylive.entity.vo.ResponseVO; import com.easylive.service.FooService; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController("fooController") @RequestMapping("/foo") public class FooController extends ABaseController {
@Resource private FooService fooService;
@RequestMapping("/list") @GlobalInterceptor public ResponseVO<?> list(Integer pageNo, Integer pageSize, String fooNameFuzzy, Integer status) { FooQuery query = new FooQuery(); query.setPageNo(pageNo == null ? 1 : pageNo); query.setPageSize(pageSize == null ? 10 : pageSize); query.setFooNameFuzzy(fooNameFuzzy); query.setStatus(status); query.setOrderBy("create_time desc"); query.setQueryCountInfo(true);
PaginationResultVO<FooDto> page = fooService.findListByPage(query); return getSuccessResponseVO(page); }
@RequestMapping("/detail") @GlobalInterceptor public ResponseVO<?> detail(String fooId) { FooDto dto = fooService.getById(fooId); return getSuccessResponseVO(dto); } }
|
说明:
- 入参转为
FooQuery,避免在 Controller 里写分页/筛选细节。
- 出参统一用
ResponseVO 包装;数据体是 DTO 或 PaginationResultVO<DTO>。
- 如需对前端展示再加工,新增
FooVO 并在 Controller 中把 DTO → VO。
6) 快速落地步骤
- 新建
FooQuery、FooDto、(可选)FooVO。
- Service:定义
findListByPage/findListByParam/getById,实现里把 DO 转 DTO。
- Controller:接 Query 入参、调用 Service、返回
ResponseVO。
- 前端:接收
ResponseVO 的 data 字段即可。