Appearance
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 含有 editable、industryName、customerBusinessInfo 等聚合字段。
你在 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,再批量查跟进和合同”的思路。
