4、客户列表API开发:GET请求实现客户分页查询、RESTful API设计规范、使用Postman进行接口测试

好,咱们进入实战环节。这一章我带你手写客户列表的API。说白了,就是让前端能通过一个GET请求,拿到分页后的客户数据。

你想想看,如果客户数据有10万条,一次性全返回,前端页面直接卡死。所以分页查询是必须的。我个人习惯,分页参数永远用 pagesize,简单明了。

4.1 RESTful API设计规范

先聊聊规范。RESTful API不是啥高深理论,它就是一套约定。我见过太多项目,API路径乱写,比如 /getCustomerList/queryAllCustomers。嗯,这样其实也能跑,但团队协作起来很痛苦。

我建议你遵循以下原则:

  • 资源用名词复数:比如 /api/customers,而不是 /api/getCustomer
  • HTTP动词表达操作:GET查、POST增、PUT改、DELETE删
  • 查询参数用于过滤和分页?page=1&size=10
  • 返回统一的数据结构:包含状态码、消息、数据体

我推荐的分页API路径:

GET /api/customers?page=1&size=10&keyword=张三

这样设计,语义清晰,前端也容易对接。

4.2 后端代码实现

我用Spring Boot来演示。为什么?因为我在项目中用了好几年,确实省心。先看Controller层:

@RestController
@RequestMapping("/api/customers")
public class CustomerController {

    @Autowired
    private CustomerService customerService;

    @GetMapping
    public Result<PageResult<CustomerVO>> list(
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(required = false) String keyword) {
        
        PageResult<CustomerVO> pageResult = customerService.listCustomers(page, size, keyword);
        return Result.success(pageResult);
    }
}

这里有个细节:@RequestParam(defaultValue = "1") 我强烈建议加上默认值。我曾经遇到前端没传page参数,后端直接报空指针,排查了半天才发现是参数没给默认值。

再看Service层:

@Service
public class CustomerServiceImpl implements CustomerService {

    @Autowired
    private CustomerMapper customerMapper;

    @Override
    public PageResult<CustomerVO> listCustomers(int page, int size, String keyword) {
        // 计算偏移量
        int offset = (page - 1) * size;
        
        // 查询总数
        long total = customerMapper.countCustomers(keyword);
        
        // 查询列表
        List<CustomerVO> list = customerMapper.selectPage(offset, size, keyword);
        
        // 封装结果
        PageResult<CustomerVO> pageResult = new PageResult<>();
        pageResult.setTotal(total);
        pageResult.setPage(page);
        pageResult.setSize(size);
        pageResult.setList(list);
        
        return pageResult;
    }
}

为什么用 offset = (page - 1) * size?因为前端传的page从1开始,而数据库的offset从0开始。这个坑我踩过,当时前端说“第一页数据不对”,我查了半天才发现是偏移量算错了。

Mapper层用MyBatis-Plus实现:

@Mapper
public interface CustomerMapper {

    Long countCustomers(@Param("keyword") String keyword);

    List<CustomerVO> selectPage(@Param("offset") int offset, 
                                 @Param("size") int size,
                                 @Param("keyword") String keyword);
}

对应的XML:

<select id="countCustomers" resultType="long">
    SELECT COUNT(*) FROM customer
    <where>
        <if test="keyword != null and keyword != ''">
            AND (name LIKE CONCAT('%', #{keyword}, '%')
                 OR phone LIKE CONCAT('%', #{keyword}, '%'))
        </if>
    </where>
</select>

<select id="selectPage" resultType="com.example.vo.CustomerVO">
    SELECT id, name, phone, email, create_time
    FROM customer
    <where>
        <if test="keyword != null and keyword != ''">
            AND (name LIKE CONCAT('%', #{keyword}, '%')
                 OR phone LIKE CONCAT('%', #{keyword}, '%'))
        </if>
    </where>
    ORDER BY id DESC
    LIMIT #{offset}, #{size}
</select>

小提示:分页查询一定要先查总数,再查列表。如果数据量特别大(百万级),可以考虑用ES或者搜索引擎来优化。不过对于大多数CRM系统,MySQL的分页完全够用。

4.3 统一返回结果封装

我习惯定义一个Result类,所有API都返回这个结构。这样前端统一处理,不用每个接口都写一遍解析逻辑。

public class Result<T> {
    private int code;      // 200成功,其他失败
    private String msg;    // 提示信息
    private T data;        // 实际数据

    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.code = 200;
        result.msg = "操作成功";
        result.data = data;
        return result;
    }

    public static <T> Result<T> error(int code, String msg) {
        Result<T> result = new Result<>();
        result.code = code;
        result.msg = msg;
        return result;
    }
}

PageResult分页结果封装:

public class PageResult<T> {
    private long total;    // 总记录数
    private int page;      // 当前页码
    private int size;      // 每页条数
    private List<T> list;  // 数据列表
    // getter/setter省略
}

4.4 使用Postman进行接口测试

代码写完了,怎么验证?我推荐Postman。它是我用过最顺手的API测试工具。

测试步骤:

  1. 新建请求:选择GET方法,输入URL http://localhost:8080/api/customers?page=1&size=5
  2. 添加请求头Content-Type: application/json(虽然GET请求不需要,但养成习惯)
  3. 点击Send,查看返回结果

正常返回示例:

{
    "code": 200,
    "msg": "操作成功",
    "data": {
        "total": 100,
        "page": 1,
        "size": 5,
        "list": [
            {
                "id": 1,
                "name": "张三",
                "phone": "13800138001",
                "email": "zhangsan@example.com",
                "createTime": "2024-01-15 10:30:00"
            },
            {
                "id": 2,
                "name": "李四",
                "phone": "13800138002",
                "email": "lisi@example.com",
                "createTime": "2024-01-15 11:00:00"
            }
        ]
    }
}

注意:测试时一定要验证边界情况:

  • page=0 或 page=-1 时,后端是否处理?
  • size=0 时,是否返回空列表?
  • keyword传空字符串时,是否返回全部数据?
  • 数据库没有数据时,total是否为0?

我曾经因为没测page=0的情况,上线后被用户反馈“点第一页没数据”,尴尬得很。

4.5 关键词搜索功能

分页查询里我加了keyword参数,支持按姓名和手机号模糊搜索。这个功能在客户管理里太常用了。

前端传参示例:

GET /api/customers?page=1&size=10&keyword=138

返回结果只包含手机号含“138”的客户。注意SQL里的 LIKE '%keyword%' 会导致全表扫描,如果数据量大,建议用全文索引或者ES。

4.6 常见问题与避坑指南

我曾经踩过的坑:

  • 分页总数不准确:count查询和列表查询的条件不一致,导致总数和列表对不上。解决办法:把条件抽取成公共方法。
  • 排序问题:分页查询一定要加ORDER BY,否则多次查询结果顺序可能不同。我习惯按id降序,最新的数据排前面。
  • SQL注入:keyword参数直接拼接到SQL里很危险。用MyBatis的 #{} 占位符,它会自动处理转义。

好了,这一章的内容就这些。你按照上面的代码实现,配合Postman测试,客户列表API就能跑起来了。下一章我们讲新增客户API,到时候会用到POST请求和参数校验,敬请期待。

课后练习:

  1. 在Postman中测试分页查询,验证page和size参数是否生效
  2. 尝试传入keyword参数,测试模糊搜索功能
  3. 修改size为0,观察后端返回结果是否符合预期