Skip to content

07:详情查询:VO 与聚合查询

1. 列表和详情不是同一个接口

列表追求:一次返回多条、字段较少、速度稳定。

详情追求:只返回一条、字段完整、可以聚合关联数据。

正式客户模块:

text
列表:POST /customerInfo/query/page
详情:POST /customerInfo/findById

不要为了省事,在列表接口中把所有联系人、地区、发票、银行、跟进记录全部查出来;这会让分页变慢。

2. Demo 的第一版详情

Controller 增加:

java
@PostMapping("/findById")
public Result<CustomerDetailVo> findById(@RequestBody Long id) {
    return Result.data(customerService.findById(id));
}

Service 实现流程:

text
1. mapper.selectById(id)
2. 为空则抛出“客户不存在”
3. Entity 转 CustomerDetailVo
4. 返回 VO

测试请求:

bash
curl -X POST 'http://127.0.0.1:7011/crm/web/client/customerInfo/findById' \
  -H 'Content-Type: application/json' \
  -d '1'

3. 第二版:聚合联系人

完成第五章的 demo_customer_contact 后,扩展 VO:

java
@Data
public class CustomerDetailVo {
    // 基础字段省略
    private List<CustomerContactVo> contacts;
}

查询顺序:

text
demo_customer            -> 查询客户主数据
demo_customer_contact    -> 按 customer_id 查询联系人
                         -> 组装进 CustomerDetailVo.contacts

不要把联系人表实体 DemoCustomerContact 直接返回。定义 CustomerContactVo,这能避免未来表字段变化直接影响接口。

4. 正式项目的详情为什么复杂

CustomerInfoServiceImpl.getCustomerInfoById 除了主表,还会处理:

  • 当前用户是否有编辑权限;
  • 公司性质字典名称;
  • 行业树路径;
  • 地区;
  • 工商信息;
  • 品牌和跟进统计;
  • 合同或首单时间等业务字段。

因此正式 CustomerInfoVo 含有 editableindustryNamecustomerBusinessInfo 等聚合字段。

你在 Demo 中不要一次模仿全部内容,只模仿“主表 + 联系人”的聚合模式即可。

5. Entity 到 VO 的转换

你可以手写:

java
public static CustomerDetailVo toDetailVo(DemoCustomer entity) {
    CustomerDetailVo vo = new CustomerDetailVo();
    vo.setId(entity.getId());
    vo.setCustomerName(entity.getCustomerName());
    vo.setContactPerson(entity.getContactPerson());
    vo.setContactPhone(entity.getContactPhone());
    vo.setBrandName(entity.getBrandName());
    vo.setCreatedAt(entity.getCreatedAt());
    return vo;
}

初期手写最容易理解。正式项目也有大量手写 Converter;后续才考虑 MapStruct、Orika 等自动映射工具。

6. 详情查询验收清单

  • id 存在时返回 200 和完整详情;
  • id 不存在时返回业务失败响应,而不是 null 或 500;
  • 返回的是 CustomerDetailVo,不是 Entity;
  • 有联系人时 contacts 是数组;没有联系人时约定返回空数组;
  • 调用详情接口不会影响列表分页接口。

7. 进阶问题

详情聚合过多时会有 N+1 查询问题。例如列表有 20 个客户,每个客户单独查一次联系人,会发出 21 条 SQL。

正确的列表聚合方式通常是:先查本页客户 id,再用 IN (ids) 一次查所有联系人,最后在 Java 中按 customerId 分组。当前 CRM 的客户分页也使用了“先取当前页 id,再批量查跟进和合同”的思路。

Lucking