Skip to content

12:客户主资料:创建、编辑、详情、删除与附属资料

1. 真实代码入口

当前文件 / 方法
API 输入hc-crm-center-api/.../form/CustomerInfoAddFormCustomerInfoEditForm
API 输出hc-crm-center-api/.../vo/CustomerInfoVo
Controllercontroller/web/client/CustomerInfoController
DTO / 转换dto/CustomerInfoDtoconverter/CustomerInfoConverter
业务实现service/impl/CustomerInfoServiceImpl
数据访问CustomerInfoMapperCustomerInfoProvider
主表customer_info

Controller 暴露的关键接口:

text
POST /customerInfo/save
POST /customerInfo/edit
POST /customerInfo/findById
POST /customerInfo/query/page?page=&size=
POST /customerInfo/del
POST /customerInfo/del/batch
POST /customerInfo/getSelectList
GET  /customerInfo/query/businessInfoList

2. 客户新增的真实业务顺序

源码方法:CustomerInfoServiceImpl.saveCustomerInfo(CustomerInfoDto),标注 @Transactional(rollbackFor = Exception.class)

它不是简单的 insert customer_info,真实顺序如下:

mermaid
sequenceDiagram
    participant UI as 前端
    participant C as CustomerInfoController
    participant S as CustomerInfoServiceImpl
    participant DB as 数据库
    participant R as 关联服务

    UI->>C: CustomerInfoAddForm
    C->>S: Form -> CustomerInfoDto
    S->>S: 校验非海外客户统一社会信用代码
    S->>R: 取当前用户主部门
    S->>DB: 保存 customer_info
    S->>DB: 写 customer_info_change_log
    S->>DB: 保存 customer_region
    S->>DB: 保存 customer_business_info
    S->>DB: 保存 customer_product_follow / customer_contact
    S->>DB: 保存 customer_follow_record / sys_file
    S->>S: 应用跟进截止时间规则
    S->>DB: 保存 customer_invoice / customer_bank_info
    S-->>C: true

逐步对应源码逻辑:

  1. checkCustomerLicense(dto):非海外客户必须填写 customerLicense
  2. ObjectUtils.copyProperties(dto, CustomerInfo.class):DTO 转主表实体。
  3. UserContext.getOpenId() + saasDeptMemberServer.getPrimaryDeptId(openId):取得创建人部门,写入 creator_department_id
  4. industryIds 只取最后一级,写入 customer_info.industry_id
  5. this.save(customerInfo):保存主表,得到 customerId。
  6. customerInfoChangeLogService.recordCreate(...):写客户基础信息变更日志。
  7. regions 的二维数组转换为多条 CustomerRegion,保存 customer_region
  8. DTO 转 CustomerBusinessInfo,保存 customer_business_info
  9. customerProductFollowService.saveCustomerProductFollow(...) 保存产品跟进与其联系人。
  10. 将每条跟进记录按产品拆分,保存 customer_follow_record;存在附件时写 sys_file
  11. 对新增的跟进记录调用 applyDeadlineRule(...),刷新产品跟进的保护截止时间。
  12. 保存 customer_invoicecustomer_bank_info,并各自写变更日志。

任何一步抛异常,事务应回滚,不能留下半个客户。

3. 创建 Form 中哪些字段决定业务分支

CustomerInfoAddForm 的关键字段不是“展示字段”,而是业务输入:

字段作用
customerName客户名称,Controller 先做同名检查
isOverseas + customerLicense决定统一社会信用代码是否必填
industryIds级联行业路径;最后一级写入主表
regions一对多地区记录
followAddItemForms为客户创建产品跟进和产品联系人
followRecordList创建跟进记录及附件
invoiceListbankInfoList创建开票和银行子表资料

这就是 Form、DTO 和 Entity 不能混用的原因:一个 Form 同时携带多个表的输入,CustomerInfo Entity 只对应主表。

4. 编辑客户:地区差量更新

源码方法:editCustomerInfo(CustomerInfoDto)

主要流程:

text
1. 校验统一社会信用代码
2. 按 id 查客户,不存在则失败
3. 更新 customer_info,并记录基础信息字段差异
4. 读取旧 customer_region
5. 将请求地区与旧地区按 regionType/province/city/district 比对
6. 保留相同地区;删除请求中不存在的地区;新增数据库没有的地区
7. 记录地区的新增/删除日志
8. customer_business_info 不存在则新建,存在则更新并记录差异

这比“先删所有地区再重建”复杂,但可以保留明确的变更日志、减少无效删除和新增。

你在 Demo 的第一版可先使用“删旧联系人再插入新联系人”;理解后再做差量更新。

5. 客户详情:不是主表直出

源码方法:getCustomerInfoById(Long id)

详情至少聚合:

text
customer_info
  + 当前用户是否可编辑(当前跟进人或其部门领导)
  + 公司性质字典名称
  + 行业名称和行业祖先路径
  + 地区
  + 工商信息
  + 品牌、签约/跟进统计等

所以返回的是 CustomerInfoVo,而不是 CustomerInfo Entity。

6. 客户分页:主表查询后再批量聚合

queryPage(QueryModel) 的做法:

text
1. 没传 header 时补默认表头
2. 注入数据权限:创建人、创建部门
3. CustomerInfoProvider 执行主表分页
4. 提取当前页 customerId
5. 批量查跟进产品、产品、合同、地区、成员名称、行业
6. 将合作阶段、跟进产品、商务、最近跟进、成交产品等补到 records
7. 返回 QueryResultModel

重点:它只对当前页 id 批量查询关联数据,避免一个客户一条 SQL 的 N+1 问题。

7. 删除客户的真实约束

源码 deleteCustomerInfoById

text
1. 客户不存在 -> 失败
2. 统计该客户是否存在 customer_product_follow
3. 只要存在跟进产品 -> 禁止删除
4. 写 customer_info_change_log 删除记录
5. removeById(customer)

因此删除规则不是“前端点删除就删”。它先保护已进入跟进流程的客户数据。

8. Demo 应实现的客户业务缩减版

按顺序实现,不要一步照搬全部子表:

text
版本 1:demo_customer
  - 名称唯一
  - 新增、编辑、详情、分页、删除

版本 2:demo_customer_region
  - 新增时保存多地区
  - 编辑时做“删旧再建”

版本 3:demo_customer_contact
  - 新增时主表 + 多联系人同一事务
  - 详情聚合联系人

版本 4:demo_customer_change_log
  - 新增、编辑、删除写审计记录

版本 5:demo_customer_product_follow
  - 有跟进记录时禁止删除客户

每完成一个版本,都用数据库查询确认:成功时所有表完整写入;失败时所有表都没有新增数据。

9. 阅读源码练习

依次在当前项目打开:

text
CustomerInfoController.saveCustomerInfo
CustomerInfoConverter.convertFormToDto
CustomerInfoServiceImpl.saveCustomerInfo
CustomerInfoServiceImpl.editCustomerInfo
CustomerInfoServiceImpl.getCustomerInfoById
CustomerInfoServiceImpl.deleteCustomerInfoById
CustomerInfoProvider.queryPageList
pginit.sql 中 customer_info / customer_region / customer_business_info

能够解释这条链路后,才开始实现 Demo 的客户主资料业务。

10. 客户附属资料不是重复功能

客户子资料各自有独立 Controller 和 Service,除了在“创建客户”时被一次性保存外,也可以在客户详情页单独维护。

子领域接口 ControllerService 中的真实规则关键表
产品联系人CustomerContactControlleraddContacteditCustomerContact,并支持普通/认领联系人分页customer_contact
工商信息CustomerBusinessInfoControllerfindByCustomerId,创建/编辑客户时同步保存customer_business_info
银行信息CustomerBankInfoControllerisDuplicateaddBankInfoeditBankInfo,避免同客户重复账户customer_bank_info
开票信息CustomerInvoiceControlleraddInvoiceeditInvoice,并参与导入customer_invoice
客户地区CustomerRegionController创建、编辑客户时由主 Service 统一差量维护customer_region
客户行业关系CustomerIndustryController行业关联资料维护customer_industry
客户品牌关系CustomerBrandRelationController客户与品牌的扩展关系customer_brand_relation
变更历史CustomerInfoChangeLogController查询由主业务写入的字段变化customer_info_change_log

学习时先区分两个概念:

text
customer_contact:客户-产品维度的联系人,归属到某条产品跟进
contract_contact:合同维度的联系人,归属到某份合同

它们都叫“联系人”,但业务所属、外键和生命周期不同,不能混用。

11. 子资料在 Demo 中的正确落地顺序

text
客户主表 CRUD
  -> 客户地区(一对多)
  -> 客户-产品联系人(一对多,必须关联 productFollowId)
  -> 银行账户唯一性校验
  -> 开票信息
  -> 工商资料
  -> 变更日志

每一个子表都要回答同样的问题:父记录不存在时是否允许写入、删除父记录时的规则、编辑时采用差量更新还是重建、是否要写日志。

Lucking