# 合同管理 — 跨模块调用梳理(步骤 6) > **权威源码**:`shanxia/crm/backend/ruoyi-modules/ruoyi-crm/` > **日期**:2026-05-31 > **目的**:Python 重写 `CrmContractServiceImpl` 时,明确哪些金额字段由合同自身维护、哪些由回款/开票模块回调,避免漏实现或双轨不一致。 --- ## 1. 总览 ```mermaid flowchart LR subgraph inbound["入站:谁调用合同 Service"] Q[QuoteServiceImpl] O[OpportunityServiceImpl] P[PaymentServiceImpl] CC[CrmContractController] end subgraph contract["CrmContractServiceImpl"] insert[insertByBo / updateByBo] recv[updateReceivedAmount] inv[updateInvoiceAmount ⚠️ 未被调用] end subgraph outbound["出站:合同调用谁"] CP[ContractProductService] PP[PaymentPlanService.batchGenerate] SI[SupplierInfo / Customer / Contact / User] QT[Quote / Opportunity Mapper 写回] end subgraph invoice["CrmInvoiceServiceImpl"] inline[updateContractInvoiceAmount 私有方法] end Q -->|insertByBo| insert O -->|insertByBo + generateContractNo| insert P -->|queryById 校验| contract P -->|5 处回调| recv CC --> contract insert --> CP insert --> PP insert --> SI invoice -->|issue / invalid| inline inline -->|直接 UPDATE crm_contract| contract ``` | 合同金额字段 | 写入方式 | 当前门户 shanxia2.0 | |-------------|---------|---------------------| | `received_amount` / `unreceived_amount` | 回款模块回调 `updateReceivedAmount` | 回款模块**未移植**;列表/详情**只读展示** DB 字段 | | `invoice_amount` / `uninvoice_amount` | 开票模块私有 `updateContractInvoiceAmount`(**非** `updateInvoiceAmount`) | 开票模块**未移植**;同上只读 | --- ## 2. `updateReceivedAmount(contractId)` ### 2.1 实现逻辑(Java) 位置:`CrmContractServiceImpl.java` L1264–1295。 1. 查询 `crm_payment`:`contract_id = ?` 且 `del_flag = 0` 2. **对所有记录** `amount` 求和 → `receivedAmount`(**不过滤** `status`) 3. 读合同 `amount` 4. 更新合同:`received_amount = 求和`,`unreceived_amount = amount - received_amount` 5. `@Transactional`;合同不存在时 log error 并返回 `false` **与回款统计的差异**:`PaymentServiceImpl.statisticsOverview` 仅把 `status = confirmed` 计入金额;`updateReceivedAmount` **包含 pending / confirmed / invalid / rejected** 等所有未删记录。新增 pending 回款时也会立刻回调,而 `reject()` **不**回调(注释写「待确认未计入」,与 insert 行为矛盾)。Python 重写须**原样复刻**此行为,除非产品单独立项修正。 ### 2.2 触发点(`CrmPaymentServiceImpl`) | 方法 | 条件 | 说明 | |------|------|------| | `insertByBo` | `entity.contractId != null` | 插入后立即回调(状态为 `pending`) | | `updateByBo` | 更新成功且 `entity.contractId != null` | 仅更新**新** contractId;若改绑合同,**不**刷新旧合同 | | `deleteWithValidByIds` | 删除成功后,收集到的每个 `contractId` | 仅允许删 `pending` | | `confirm` | 确认成功且有关联合同 | | | `invalid` | 作废成功且有关联合同 | | | `reject` | — | **不调用** `updateReceivedAmount` | 插入前校验:`queryById(contractId)` 读 `unreceivedAmount`(空则取 `amount`),要求 `bo.amount <= unreceivedAmount`。 ### 2.3 门户现状 - `module-status.md`:回款管理(`crm_payment` + 计划表)**未开始** - 合同重写阶段:**实现** `updateReceivedAmount` 函数供将来回款模块 import;合同 CRUD **不**主动重算(除非 Java 同等逻辑在 insert 初始化 0) - 步骤 26 恢复 `quote.to_contract` 后,新合同 `received_amount = 0` 即可 --- ## 3. 开票金额:两套逻辑并存 ### 3.1 `updateInvoiceAmount(contractId)` — 接口存在、**无调用方** 位置:`CrmContractServiceImpl.java` L1981–2012。 1. 查 `crm_invoice`:`contract_id = ?`,`del_flag = 0` 2. 对 **`amount`(不含税)** 求和 3. `invoice_amount = 求和`,`uninvoice_amount = contract.amount - 求和` 全库 grep:**无任何 Service 调用**此方法(仅接口 + 实现定义)。 ### 3.2 `CrmInvoiceServiceImpl.updateContractInvoiceAmount` — **实际生效路径** 私有方法 L838–873;**直接** `contractMapper.updateById`,不经过 `ICrmContractService`。 | 发票方法 | 何时更新合同 | 累加字段 | 方向 | |---------|-------------|---------|------| | `insertByBo` | 否 | — | 申请中不计入 | | `updateByBo` | 否 | — | | | `approve` | 否 | — | 审批通过仍不计入 | | **`issue`** | 是 | **`totalAmount`(价税合计)** | `isAdd = true` | | **`invalid`** | 是(状态为 invoiced/delivered/received) | **`totalAmount`** | `isAdd = false` | | `express` / `receive` | 否 | — | | 新增/审批前校验:读合同 `uninvoiceAmount`,用本次 **`totalAmount`** 与剩余可开比较(`insertByBo` L131–141)。 **与 `updateInvoiceAmount` 的不一致**: | 维度 | `updateInvoiceAmount` | `updateContractInvoiceAmount` | |------|----------------------|------------------------------| | 汇总字段 | `invoice.amount` | 开票/作废时的 `totalAmount` | | 触发时机 | 全表重算(若被调用) | 增量加减 | | 是否在用 | 否 | 是 | Python 重写建议: - 合同模块保留 `update_invoice_amount()`(全量重算版,对齐 Java 接口,供运维/迁移脚本) - 开票模块移植时实现 **`update_contract_invoice_amount_increment`**,行为对齐 `CrmInvoiceServiceImpl` 私有方法 - **不要**在开票 `issue/invalid` 时改调全量重算,除非产品确认统一 ### 3.3 门户现状 - `crm_invoice` / `crm_invoice_item`:**未移植** - 已有 `crm_invoice_info` 是**客户开票信息**,与合同已开票金额无关 --- ## 4. 合同模块内部对金额字段的处理 ### 4.1 `insertByBo` - `received_amount` / `invoice_amount` 默认 `0` - `unreceived_amount = amount - received_amount` - `uninvoice_amount = amount - invoice_amount` - 若 BO 带 `paymentPlans` → `paymentPlanService.batchGenerate(contractId, plans)`(回款**计划**,非实收) ### 4.2 `updateByBo`(仅 draft) - 修改 `amount` 时:**保留**原 `received_amount` / `invoice_amount` - 重算 `unreceived_amount` / `uninvoice_amount` = 新 `amount` − 原已收/已开 ### 4.3 `renewContract` - 新合同:`received_amount = 0`,`invoice_amount = 0`,`unreceived/uninvoice = amount` ### 4.4 统计 API `statisticsOverview` / `statisticsTrend` 等对列表合同字段 **SUM**,不实时查 payment/invoice 子表。 --- ## 5. 其他入站调用(非金额) | 调用方 | 方法 | 用途 | |--------|------|------| | `CrmQuoteServiceImpl.toContract` | `contractService.insertByBo` | 报价转合同(非 HTTP `createFromQuote`) | | `CrmOpportunityServiceImpl` 赢单 | `generateContractNo` + `insertByBo` | 自动建 draft 合同,回写 `opp.contract_id` | | `CrmContractController` | 全套 REST | 见 `04-api-spec.md` | | `CrmPaymentServiceImpl` | `queryById` | 回款金额校验 | `createFromQuote` / `createFromOpportunity`(Controller 入口)内部也是 `insertByBo` + 更新 quote/opportunity 状态。 --- ## 6. 合同模块出站依赖(重写时一并注册) | 依赖 | 用途 | |------|------| | `ICrmContractProductService` | 产品明细 CRUD、续签复制 | | `CrmContractProductPriceTierMapper` | 框架合同调价区间 | | `ICrmContractApprovalService` | `canUserApprove` | | `ICrmPaymentPlanService` | 新建合同时 `batchGenerate` | | `CrmPaymentMapper` | 仅 `updateReceivedAmount` 读表 | | `CrmInvoiceMapper` | 仅 `updateInvoiceAmount` 读表(当前无外部触发) | | `CrmQuoteMapper` / `CrmOpportunityMapper` | createFrom* / 商机赢单 | | `CrmContractTemplateMapper` | createFromTemplate | | `ICrmSupplierInfoService` | 供方简称、编号规则 | | `ICrmProductService` | 商机转合同自动建产品 | | `UserService` | 负责人姓名 | | Customer / Contact Mapper | 冗余名称填充 | --- ## 7. 前端关联(步骤 7+ 适配时注意) | 位置 | 字段 | 说明 | |------|------|------| | `contract/index.vue` 列表/详情 | `receivedAmount`、`unreceivedAmount`、`invoiceAmount`、`uninvoiceAmount` | 来自合同 VO,非子表实时查询 | | `ExecuteDialog.vue` | 同上 + 回款率 | 打开执行弹窗前读合同详情 | | `InvalidDialog.vue` L111 | `res.data.invoicedAmount` | **字段名错误**:Java VO 为 `invoiceAmount`;作废前开票提示可能始终为 0,步骤 27 UI 适配时修正 | --- ## 8. shanxia2.0 实施策略 ### 阶段 D(合同后端重写) 1. 在 `crud/crm/contract.py` 实现 `update_received_amount(db, contract_id)`,逻辑对齐 §2.1 2. 同文件实现 `update_invoice_amount(db, contract_id)`(对齐 §3.1,预留接口) 3. `insert_by_bo` / `update_by_bo` 按 §4 维护四个金额字段 4. **不**在合同路由内实现回款/开票 CRUD ### 回款/开票模块移植后 | 模块 | 必须回调 | |------|---------| | 回款 | `insert` / `update` / `delete` / `confirm` / `invalid` → `update_received_amount`(**不含** `reject`) | | 开票 | `issue` / `invalid` → 增量更新(对齐 §3.2),或封装为 shared helper | ### 步骤 26 恢复 `quote.to_contract` 时调用新 `insert_by_bo`,无需触发回款/开票回调。 --- ## 9. Python 重写检查清单 - [ ] `update_received_amount`:payment 求和 **无 status 过滤**,`del_flag=0` - [ ] 回款 5 个触发点与 Java 一致;`reject` 不回调 - [ ] `update_invoice_amount` 全量重算实现但默认不被开票模块调用 - [ ] 开票模块:`issue/invalid` 用 **totalAmount** 增量更新(将来实现) - [ ] `update_by_bo` 改 `amount` 时不覆盖已收/已开累计值 - [ ] `renew_contract` 新合同金额字段清零 - [ ] `insert_by_bo` 可选 `payment_plans` → 调用 payment_plan 模块(若未移植则跳过或 stub) - [ ] 前端 `InvalidDialog`:`invoicedAmount` → `invoiceAmount` --- ## 变更记录 | 日期 | 说明 | |------|------| | 2026-05-31 | 步骤 6:梳理 updateReceivedAmount / 开票双轨 / 入站出站 / 门户策略 |