在后台系统开发中,列表接口无疑是使用最频繁的接口类型之一:
几乎每个管理页面都需要依赖列表接口获取数据。然而,在许多实际项目中,列表接口的实现常常过于随意,例如直接返回一个简单的数组结构:
[
{ "id": 1, "name": "A" },
{ "id": 2, "name": "B" }
]
乍看之下这种写法足够简洁,前端也能正常解析。但随着项目复杂度提升,这种设计会暴露出诸多问题:
- 需要添加总条数(total)时,没有合适的字段位置
- 分页参数无法优雅扩展
- 额外的筛选条件或元数据无处存放
“直接返回 List” 的方式仅适用于最简单的演示场景。一旦项目涉及后台系统或B端业务,这种写法就会成为后期维护的负担。本文将深入探讨如何设计列表接口,以确保其具备良好的可扩展性与稳定性。

1. 为什么不能直接返回 List?
直接返回 List 最核心的问题在于缺乏结构化的包装。没有结构就意味着没有预留扩展空间,随着业务演进,你可能会遇到以下典型问题。
问题一:无法便捷添加总条数(total)
分页查询最基本的需求就是获知数据总量,从而计算总页数。如果接口一开始就返回裸 List,当需要加入 total 字段时,你只有两个选择:要么新增一个独立接口,要么破坏性修改现有接口结构。无论哪种方案,都会导致前端适配成本增加、旧版本兼容性断裂,最终使接口难以维护。
问题二:扩展字段无处安放
业务发展中经常需要返回一些辅助信息,例如:
- 当前用户对列表项的操作权限(如是否可编辑、删除)
- 查询时使用的筛选条件快照
- 数据更新时间或服务器时间戳
- 前端需要的状态枚举字典
这些字段都需要一个统一的容器来承载,而简单的 List 无法提供这样的容器。
问题三:前端分页处理困难
前端进行分页控制时,通常需要四个关键参数:当前页码(page)、每页大小(pageSize)、总条数(total)和数据列表(records)。直接返回 List 会迫使前端自行推算这些值,这种推算往往不可靠,且增加了前端的逻辑复杂度。
问题四:接口语义模糊,不利于演进
当返回值需要扩展时,整个接口不得不进行重构。换句话说,返回 List 等于在设计初期就锁死了接口的演进能力。

2. 标准的列表接口结构是怎样的?
一个健壮、可扩展且通用的列表接口返回结构应包含以下字段:
{
"list": [ ... ],
"total": 100,
"page": 1,
"pageSize": 10,
"extra": { ... }
}
各字段含义:
- list:当前页的数据记录集合
- total:符合查询条件的数据总条数
- page:当前页码,从1开始计数
- pageSize:每页显示的记录数
- extra:扩展信息对象,用于存放任何额外的业务字段(可选)
这是后台管理系统中最常见、也最具通用性的响应格式。

3. 统一封装为响应对象
在 Spring Boot 项目中,我们通常会定义一个通用的分页结果类 PageResult<T>:
@Data
public class PageResult<T> {
/** 当前页的数据列表 */
private List<T> list;
/** 总条数 */
private Long total;
/** 当前页码 */
private Integer page;
/** 每页大小 */
private Integer pageSize;
/** 扩展字段,可选 */
private Map<String, Object> extra;
public static <T> PageResult<T> of(List<T> list, long total, int page, int pageSize) {
PageResult<T> result = new PageResult<>();
result.setList(list);
result.setTotal(total);
result.setPage(page);
result.setPageSize(pageSize);
return result;
}
}
所有列表接口都统一返回 PageResult<T> 类型,可以极大提升项目内部的一致性,降低协作成本。

4. 与 MyBatis-Plus 分页插件协同工作
MyBatis-Plus 提供了开箱即用的分页支持,我们可以轻松地将其分页结果转换为我们的标准结构:
Page<User> page = userMapper.selectPage(
new Page<>(pageNum, pageSize),
wrapper
);
// 转换为 PageResult
PageResult<UserVO> result = PageResult.of(
userVOList,
page.getTotal(),
page.getCurrent(),
page.getSize()
);
在 Controller 层的统一写法如下:
@GetMapping("/list")
public Result<PageResult<UserVO>> list(UserQuery query) {
return Result.ok(userService.list(query));
}
通过这种标准化链路,前端永远能够以统一的方式解析分页数据。

一个优秀的列表接口,其扩展字段 extra 往往是业务灵活性的关键。例如,在一个订单列表接口中,extra 可以承载丰富的辅助信息:
{
"list": [...],
"total": 100,
"extra": {
"sumAmount": 25630.45,
"sumCount": 1230,
"statusList": [
{ "code": 1, "text": "待支付" },
{ "code": 2, "text": "已支付" }
]
}
}
extra 字段可以存放多种类型的数据:
- 聚合数据:如总金额、总订单数等统计信息
- 前端字典:如状态枚举列表,避免前端硬编码
- 查询上下文:当前使用的筛选条件,便于前端持久化
- 权限标记:当前用户对列表的操作权限点
- 服务器信息:如时间戳、数据版本等
extra 的存在使得接口能够在不改变主体结构的前提下持续演进,完美支持业务迭代。

6. 列表视图对象(VO)的设计原则
列表接口返回的数据不应直接使用数据库实体(Entity),而应该通过视图对象(VO)进行封装。例如:
@Data
public class UserListVO {
private Long id;
private String username;
private String mobile;
private String roleName;
private Integer status;
private String statusText;
}
使用 VO 的优势非常明显:
- 安全性:避免泄露敏感字段或数据库内部结构
- 灵活性:允许对原始数据进行加工或转换(如状态码转文本)
- 职责清晰:符合前后端分离架构思想,自然支持 BFF(Backend for Frontend)模式

7. 统一分页查询参数接收
许多项目中分页参数命名混乱,例如 page、size、pageNum、pageSize 等混用。建议统一定义一个分页查询参数类:
@Data
public class PageQuery {
private Integer page = 1;
private Integer pageSize = 10;
}
在 Controller 中规范使用:
@GetMapping("/list")
public Result<PageResult<UserVO>> list(PageQuery query) {
// 业务逻辑
}
这样设计可以确保前后端在分页参数传递上始终保持一致,减少不必要的沟通成本。
总结
直接返回 List 相当于在接口设计初期就放弃了扩展性;而返回结构化的分页对象,则能为接口的长期演进奠定坚实基础。
正确的列表接口设计应遵循以下原则:
- 统一结构:使用
PageResult 封装列表、总量、分页参数
- 预留扩展:通过
extra 字段承载业务扩展数据
- 视图隔离:采用 VO 对象返回数据,避免暴露底层细节
- 参数规范:使用统一的
PageQuery 接收分页参数
遵循这套规范,无论项目功能如何扩展,你的列表接口都能保持清晰的结构和强大的适应能力。