概念先行

  • DTO(Data Transfer Object)是什么
  • 面向“数据传输”的中立模型,用来在系统间/层间传递数据(Controller⇄Service、服务A⇄服务B、RPC/消息等)。
  • 强调输入/输出契约与序列化稳定性,通常不包含展示格式、也不带业务逻辑。
  • 典型拆分:命令/变更类(Create/Update DTO)、查询类(Query DTO)、对外返回类(Response DTO)。
  • VO(View Object)是什么
  • 面向“展示/视图”的模型,专门为前端页面/客户端界面定制。
  • 字段可以是组合/计算后的展示字段(如时间字符串、状态文案、头像全路径、是否关注等),为“好看好用”而服务。
  • 只负责展示,不建议作为内部领域/持久化的输入。

什么时候用 DTO,什么时候用 VO

  • 入参(从客户端到后端)优先用 DTO
  • 如创建/修改/查询条件:UserCreateDTOUserUpdateDTOUserQueryDTO
  • 可携带校验注解(非空、格式、范围),清晰表述“接收什么数据”
  • 出参(从后端到客户端)优先用 VO
  • 如详情/列表项/分页结果:UserDetailVOUserListItemVOUserProfileVO
  • 可包含展示友好字段(格式化时间、枚举文案、组合字段)

简言之:输入→DTO,输出→VO。DTO更“中立契约”,VO更“贴近 UI”。

在你的项目里如何落地

  • 现在已有:
  • UserInfo 是 PO(持久层实体,不建议直接暴露给前端)
  • UserInfoQuery 是查询模型(本质更像 Query DTO)
  • ResponseVOPaginationResultVO 是“响应包装/分页结果 VO”
  • 建议补齐:
  • 新增入参 DTO:UserInfoAddDTOUserInfoUpdateDTOUserInfoQueryDTO(或沿用 UserInfoQuery
  • 新增出参 VO:UserInfoDetailVOUserInfoListItemVO
  • 映射关系:
    • Controller 接 DTO → Service → 转 PO 落库
    • Service 查到 PO → 转 VO 返回前端
  • 转换工具:用 MapStruct(推荐,编译期生成)或现有 CopyTools

命名与分层建议

  • 命名
  • 入参:XxxCreateDTOXxxUpdateDTOXxxQueryDTO
  • 出参:XxxDetailVOXxxListItemVO
  • 包结构:controller/dtocontroller/vointerfaces/dtointerfaces/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 层之间、缓存、搜索引擎、消息传递等使用;也可作为接口响应体(当含业务含义时)。

  • 在本项目中(位置:

    1
    com.easylive.entity.dto

    ):

    • TokenUserInfoDto:登录后放入 Redis/Cookie 的用户会话数据
    • UserCountInfoDto:用户粉丝数、消息数等聚合结果
    • VideoInfoEsDtoVideoPlayInfoDto 等:用于搜索/播放等跨层传输的结构
  • Query(查询参数对象)

  • 定义:封装“列表/分页/条件查询”的输入参数。

  • 用法:Controller 组装 Query → 传 Service/Mapper 执行分页、排序、筛选;Query 通常继承/组合分页基础类。

  • 在本项目中(位置:

    1
    com.easylive.entity.query

    ):

    • BaseParamSimplePage:分页与通用查询基类
    • VideoInfoPostQueryUserInfoQuery 等:带 orderBypageNo/pageSize、筛选字段

项目中的典型调用链

  • Controller 入参(原生参数/表单) → 组装 Query 或直接传简单参数
  • Service 处理:
  • 读写 DB:可能将 DO 转成 DTO 返回
  • 组装统计结果:返回 DTO/VO
  • Controller 出参:最终用 ResponseVO 包装,返回给前端

关键代码示例(均来自当前代码库)

  • Controller 返回 VO(统一响应)
1
2
3
4
5
6
// 所有接口返回统一的 ResponseVO 结构
@RequestMapping("/checkCode")
public ResponseVO<?> checkCode() {
...
return getSuccessResponseVO(result); // 外层是 ResponseVO
}
  • 使用 DTO 作为业务数据载体(登录态/统计)
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
}
  • 使用 Query 进行分页/筛选查询
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 包装分页 VO
}

设计原则与最佳实践

  • VO
  • 只面向“输出”。避免把 DO(数据库实体)直接返回前端。
  • 统一外层结构(本项目用 ResponseVO),便于前端处理统一的 code/msg/data。
  • DTO
  • 描述业务意义的数据载体。可跨层使用(Service ↔ 缓存/搜索/消息),避免 Controller 直接拼装复杂结构。
  • 可承载多个来源的聚合信息,减少 Controller 逻辑。
  • Query
  • 只用于“输入的查询条件”,不承载业务结果。
  • 与分页基类解耦(SimplePage + BaseParam),避免每个 Query 重复分页字段。
  • 命名上建议遵循 [业务对象]Query,字段尽量贴近筛选条件名称(如 xxxFuzzystatusListorderBy)。

快速判断口诀

  • 我这是“返回给前端”的结构吗?是 → 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;

// 排序:例如 "create_time desc"
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;

/**
* 承载业务语义的数据结构,用于 Service/缓存/消息/搜索之间传输。
*/
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;

/**
* 面向前端的展示对象。必要时和 DTO 区分,保证展示字段与命名更友好。
*/
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 包装;数据体是 DTOPaginationResultVO<DTO>
  • 如需对前端展示再加工,新增 FooVO 并在 Controller 中把 DTO → VO

6) 快速落地步骤

  1. 新建 FooQueryFooDto、(可选)FooVO
  2. Service:定义 findListByPage/findListByParam/getById,实现里把 DO 转 DTO。
  3. Controller:接 Query 入参、调用 Service、返回 ResponseVO
  4. 前端:接收 ResponseVOdata 字段即可。