Appearance
12:客户主资料:创建、编辑、详情、删除与附属资料
1. 真实代码入口
| 层 | 当前文件 / 方法 |
|---|---|
| API 输入 | hc-crm-center-api/.../form/CustomerInfoAddForm、CustomerInfoEditForm |
| API 输出 | hc-crm-center-api/.../vo/CustomerInfoVo |
| Controller | controller/web/client/CustomerInfoController |
| DTO / 转换 | dto/CustomerInfoDto、converter/CustomerInfoConverter |
| 业务实现 | service/impl/CustomerInfoServiceImpl |
| 数据访问 | CustomerInfoMapper、CustomerInfoProvider |
| 主表 | 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/businessInfoList2. 客户新增的真实业务顺序
源码方法: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逐步对应源码逻辑:
checkCustomerLicense(dto):非海外客户必须填写customerLicense。ObjectUtils.copyProperties(dto, CustomerInfo.class):DTO 转主表实体。UserContext.getOpenId()+saasDeptMemberServer.getPrimaryDeptId(openId):取得创建人部门,写入creator_department_id。industryIds只取最后一级,写入customer_info.industry_id。this.save(customerInfo):保存主表,得到 customerId。customerInfoChangeLogService.recordCreate(...):写客户基础信息变更日志。- 将
regions的二维数组转换为多条CustomerRegion,保存customer_region。 - DTO 转
CustomerBusinessInfo,保存customer_business_info。 - 调
customerProductFollowService.saveCustomerProductFollow(...)保存产品跟进与其联系人。 - 将每条跟进记录按产品拆分,保存
customer_follow_record;存在附件时写sys_file。 - 对新增的跟进记录调用
applyDeadlineRule(...),刷新产品跟进的保护截止时间。 - 保存
customer_invoice和customer_bank_info,并各自写变更日志。
任何一步抛异常,事务应回滚,不能留下半个客户。
3. 创建 Form 中哪些字段决定业务分支
CustomerInfoAddForm 的关键字段不是“展示字段”,而是业务输入:
| 字段 | 作用 |
|---|---|
customerName | 客户名称,Controller 先做同名检查 |
isOverseas + customerLicense | 决定统一社会信用代码是否必填 |
industryIds | 级联行业路径;最后一级写入主表 |
regions | 一对多地区记录 |
followAddItemForms | 为客户创建产品跟进和产品联系人 |
followRecordList | 创建跟进记录及附件 |
invoiceList、bankInfoList | 创建开票和银行子表资料 |
这就是 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,除了在“创建客户”时被一次性保存外,也可以在客户详情页单独维护。
| 子领域 | 接口 Controller | Service 中的真实规则 | 关键表 |
|---|---|---|---|
| 产品联系人 | CustomerContactController | addContact、editCustomerContact,并支持普通/认领联系人分页 | customer_contact |
| 工商信息 | CustomerBusinessInfoController | findByCustomerId,创建/编辑客户时同步保存 | customer_business_info |
| 银行信息 | CustomerBankInfoController | isDuplicate、addBankInfo、editBankInfo,避免同客户重复账户 | customer_bank_info |
| 开票信息 | CustomerInvoiceController | addInvoice、editInvoice,并参与导入 | 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)
-> 银行账户唯一性校验
-> 开票信息
-> 工商资料
-> 变更日志每一个子表都要回答同样的问题:父记录不存在时是否允许写入、删除父记录时的规则、编辑时采用差量更新还是重建、是否要写日志。
