Skip to content

16:合同:编号、关联明细、审批与客户联动

1. 合同业务的定位

合同是客户产品跟进进入成交阶段后的核心单据。它必须连接客户、跟进产品、联系人、结算规则、垫资用途、业务模式和附件,并通过审批结果反向影响跟进截止时间与客户首单成交日期。

2. 关键代码与表

内容当前代码
合同主业务ContractServiceImplcontract
合同联系人ContractContactServiceImplcontract_contact
合同关联客户ContractCustomerRelationServiceImplcontract_customer_relation
结算规则ContractSettlementRuleServiceImplcontract_settlement_rule
垫资用途ContractAdvancePaymentPurposeServiceImplcontract_advance_payment_purpose
业务模式ContractBusinessModelServiceImplcontract_business_model
编号序列ContractNumberSequenceServiceImplcontract_number_sequence
附件SysFileServiceImplsys_file

3. 合同新增的真实业务结构

入口:ContractController -> Form -> ContractDto -> ContractServiceImpl.insertContract

真实业务的总体顺序:

text
1. 校验关联的客户产品跟进是否允许提交合同
2. 补齐当前用户、部门、客户、产品等上下文
3. 根据产品前缀与年份获取安全递增的合同编号
4. 保存 contract 主表
5. 批量保存合同联系人
6. 批量保存除主客户以外的关联客户
7. 保存结算规则、垫资用途、业务模式
8. 按来源类型保存合同附件
9. 发起 BPM 审批,回写 flowId、流程状态、开始时间
10. 审批状态变化时应用跟进截止时间规则

它比项目评估多了“编号并发安全”和“客户首单时间联动”两个重点。

4. 合同编号为什么不能简单用 count + 1

ContractNumberSequenceServiceImpl.getNextSequenceNumber(productPrefix, year) 使用唯一键冲突重试和乐观锁语义来获取下一个序号。

错误实现:

text
SELECT COUNT(*) -> 100
生成编号 -> 101

两个请求并发时可能都生成 101。

正确目标:同一产品前缀和年份下,每个并发请求只能得到一个不同序号。Demo 可以先用数据库唯一索引 + 重试,理解并发下的唯一性。

5. 合同关联明细的共同模式

多个 Service 都遵循:

text
batchInsertXxx(contractId, list)
batchUpdateXxx(contractId, list)
getXxxByContractId(contractId)

其中 ContractCustomerRelationServiceImpl 有特殊规则:主客户已存在于 contract 主表中,关联客户表只保存非主客户,且去重。

这类规则应放在关联 Service,不应让 Controller 自己过滤。

6. 审批回调和客户联动

ContractServiceImpl.updateFlowProgress(flowId, status) 接收流程变化。审批通过后,典型联动包括:

text
更新合同流程状态
  -> 对关联产品跟进调用 applyDeadlineRule
  -> 首次成交时调用 CustomerInfoService.updateSigningDateOnFirstContractApproved
  -> customer_info.signing_date 只在为空时写入,保留最早成交日期

这意味着合同审批不是合同模块内部事件;它会改变客户和跟进模块的数据。

7. Demo 的合同实现顺序

第一版只实现:

text
demo_contract
  id
  contract_no
  customer_id
  product_follow_id
  status
  signing_date

demo_contract_contact

业务步骤:

text
1. 只能针对 FOLLOWING 状态的产品跟进创建合同
2. 生成唯一 contractNo
3. 保存合同和联系人,使用事务
4. 模拟审批通过
5. 将产品跟进 phase 改为 VIP
6. 若 customer.signingDate 为空,写入合同签约日期

第二版再增加结算规则、垫资用途和多客户关联。

8. 必测用例

  • 同一并发场景下合同编号不重复;
  • 跟进不存在或不在跟进中时不能创建合同;
  • 联系人保存失败时合同主表回滚;
  • 审批通过后跟进状态/阶段按规则变化;
  • 第二份合同通过时不会覆盖客户更早的首次成交日期;
  • 详情能查到联系人、规则、附件和关联客户。

9. 源码阅读顺序

text
ContractController
ContractAddForm / ContractDto / ContractVo
ContractServiceImpl.insertContract
ContractServiceImpl.updateContract
ContractServiceImpl.findById
ContractServiceImpl.updateFlowProgress
ContractServiceImpl.applyDeadlineRule
ContractNumberSequenceServiceImpl.getNextSequenceNumber
ContractContactServiceImpl
ContractCustomerRelationServiceImpl
pginit.sql 中 contract 和全部 contract_* 表

Lucking