Files
shanxia2.0/docs/crm-migration/plans/contract/04-api-spec.md
T
2026-06-01 08:21:21 +08:00

265 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 合同管理 — 04 API 规格(重写移植版)
> **步骤 1 产出** | 日期:2026-05-31
> **权威源**:`shanxia/crm/backend/ruoyi-modules/ruoyi-crm/` + 快照 `docs/crm-migration/reference/java/contract/`
> **状态**:后端 ✅ 已重写(2026-06-01);前端 ✅ Fresh Mint 适配完成
> **验收清单**:`08-verify.md`
> **HTTP 约定**:**对齐原 CRM**(GET/PUT/DELETE/POST 与 Java Controller 一致),**不**采用旧门户「全 POST」包装
---
## 0. 路径与响应约定
### 0.1 原 CRM(Java / RuoYi)
| 项 | 值 |
|----|-----|
| Controller 前缀 | `/crm/contract`(及 `/crm/contract/approval` 等子模块) |
| 网关前缀 | 前端 `request` 通常走 `/prod-api`,完整路径如 `/prod-api/crm/contract/list` |
| 列表分页入参 | Query:`CrmContractBo` 字段 + `pageNum` / `pageSize` |
| 列表分页出参 | `TableDataInfo`:`{ rows, total }` |
| 单条出参 | `R<T>`:`{ code, msg, data }` |
| 详情 GET | `GET /crm/contract/{id}` → `data` 为 `CrmContractVo`(含 `products`、`canApprove`) |
### 0.2 shanxia2.0 Python 目标
| 项 | 值 |
|----|-----|
| Router 挂载 | `APIRouter(prefix="/web/crm")` → 完整前缀 **`/api/web/crm`** |
| 子路径 | 与 Java **相同**(如 `GET .../contract/list`,**不是** `POST .../contract/list`) |
| 租户 | 所有查询/写入带 `tenant_no`(Java 靠 TenantEntity;Python 从当前用户上下文取) |
| 主键 | `VARCHAR(64)` UUID(Java 为 `Long`;API 层接受 string,避免前端精度问题) |
| 列表响应 | 兼容 RuoYi:`{ rows/list, total }` 包在统一 `success_response` 的 `data` 内 |
| 导出 | `POST .../export`,响应 `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` |
### 0.3 前端(已拷贝 CRM)实际请求路径
拷贝页调用 **`/crm/contract/...`**(非 `/web/crm`)。步骤 7 机械适配时二选一:
1. **推荐**:Vite 增加 proxy:`/crm` → `{backend}/api/web/crm`(保留原 URL + 原 HTTP 动词)
2. 或改 `http` 的 `baseURL`,仍保持各函数 `method: 'get'|'put'` 与 Java 一致
---
## 1. 合同主模块 — `CrmContractController`
**Java**:`@RequestMapping("/crm/contract")`
**Service**:`ICrmContractService` / `CrmContractServiceImpl`
| # | HTTP | 路径 | Java Service 方法 | 请求体 / Query | 响应 data | 前端函数 | 后端 |
|---|------|------|-------------------|----------------|-----------|----------|------|
| C01 | **GET** | `/list` | `queryPageList` | Query: `CrmContractBo` + `pageNum`, `pageSize` | `{ rows, total }` | `listContract` | ❌ |
| C02 | **GET** | `/{id}` | `queryById` | Path: `id` | `CrmContractVo` | `getContract` | ❌ |
| C03 | **POST** | `/` | `insertByBo` | Body: `CrmContractBo`(AddGroup) | 新合同 `id` | `addContract` | ❌ |
| C04 | **PUT** | `/` | `updateByBo` | Body: `CrmContractBo`(EditGroup) | — | `updateContract` | ❌ |
| C05 | **DELETE** | `/{ids}` | `deleteWithValidByIds` | Path: 逗号或单 id | — | `delContract` | ❌ |
| C06 | **PUT** | `/submitApproval` | `submitApproval` | `{ id, approverIds[], remark? }` | — | `submitApproval` | ❌ |
| C07 | **PUT** | `/batchSubmitApproval` | `batchSubmitApproval` | `{ ids[], approverIds[], remark? }` | 成功条数 `int` | `batchSubmitApproval` | ❌ |
| C08 | **POST** | `/saveTemplate` | `saveTemplate` | `{ id, templateContent, settlementClauseDrafts? }` | — | `saveTemplate` | ❌ |
| C09 | **GET** | `/getTemplate/{id}` | `getTemplate` | Path: `id` | 富文本 HTML 字符串 | `getTemplate` | ❌ |
| C10 | **PUT** | `/withdrawApproval` | `withdrawApproval` | `{ id, remark? }` | — | `withdrawApproval` | ❌ |
| C11 | **PUT** | `/startExecute` | `startExecute` | `{ id, remark? }` | — | `startExecute` / `executeContract` | ❌ |
| C12 | **PUT** | `/finish` | `finishContract` | `{ id, remark? }` | — | `finishContract` / `completeContract` | ❌ |
| C13 | **PUT** | `/invalid` | `invalidContract` | `{ id, invalidReason, remark? }` | — | `invalidContract` | ❌ |
| C14 | **POST** | `/uploadFile` | `uploadFile` | `{ id, fileUrl }` | — | `uploadFile` | ❌ |
| C15 | **POST** | `/uploadAttachments` | `uploadAttachments` / `uploadAttachmentsWithCategory` | `{ id, attachments[]? \| ossIds? }` | — | `uploadAttachments` | ❌ |
| C16 | **POST** | `/export` | `queryList` + Excel | Body/Query: `CrmContractBo` | xlsx 流 | `exportContract` | ❌ |
| C17 | **GET** | `/statistics/overview` | `statisticsOverview` | Query: `ownerUserId?, beginTime?, endTime?` | Map 统计 | `getStatisticsOverview` | ❌ |
| C18 | **GET** | `/statistics/status` | `statisticsByStatus` | — | List Map | `getStatisticsByStatus` | ❌ |
| C19 | **GET** | `/statistics/type` | `statisticsByType` | — | List Map | `getStatisticsByType` | ❌ |
| C20 | **GET** | `/statistics/trend` | `statisticsTrend` | Query: `type=month`, `beginTime?`, `endTime?` | List Map | `getStatisticsTrend` | ❌ |
| C21 | **GET** | `/statistics/expiring` | `queryExpiring` | Query: `days=30` | `List<CrmContractVo>` | `getExpiringContracts` | ❌ |
| C22 | **POST** | `/renew/{parentContractId}` | `renewContract` | Path + Body: `CrmContractBo` | 新合同 id | `renewContract` | ❌ |
| C23 | **POST** | `/createFromOpportunity/{opportunityId}` | `createFromOpportunity` | Path + Body | — | `createFromOpportunity` | ❌ |
| C24 | **POST** | `/createFromQuote/{quoteId}` | `createFromQuote` | Path + Body | — | `createFromQuote` | ❌ |
| C25 | **PUT** | `/sign` | `signContract` | `{ id, actualSignDate, contractFile, remark? }` | — | (form 页可能用) | ❌ |
| C26 | **PUT** | `/batch/assign` | `batchAssignOwner` | `{ ids[], ownerUserId, remark? }` | — | (列表批量分配) | ❌ |
| C27 | **GET** | `/download/{id}` | `downloadContractPdf` | Path: `id` | 文件路径/URL 字符串 | (详情下载) | ❌ |
### 1.1 `CrmContractBo` 列表/筛选字段(Query)
与前端 `ContractQuery` 对齐:
| 字段 | 说明 |
|------|------|
| `contractNo`, `contractName`, `customerName` | 分别 like;**三者同值时 OR 联合搜索** |
| `customerId`, `opportunityId`, `quoteId` | 精确 |
| `contractType`, `status`, `approvalStatus` | 精确 |
| `excludeStatus` | `ne status`(列表默认排除 `invalid`) |
| `ownerUserId` | Tab「我的合同」 |
| `pendingApprovalUserId` | Tab「待审批」→ 子查询 `crm_contract_approval` |
| `approvedByUserId` | Tab「已审批」→ 历史审批记录反查 contract_id |
| `minAmount`, `maxAmount` | 金额区间(Java 在 `params` 中) |
| `signDateBegin`, `signDateEnd` | 签订日期 |
| `beginTime`, `endTime` | 创建时间 |
| `pageNum`, `pageSize` | 分页 |
### 1.2 `CrmContractVo` 关键返回字段
列表/详情需包含:`canApprove`(列表逐行计算)、`products`(详情)、`supplierName`(详情 enrichment)、回款/开票金额字段、`attachmentFiles`(JSON 或逗号 ossId)。
### 1.3 列表页统计卡片说明
**`index.vue` 未调用 C17**;Tab 顶栏统计由**当前页列表数据前端聚合**(`loadStatistics`)。C17–C21 仍须在 Service 层实现(其它页/报表可能使用)。
---
## 2. 合同审批 — `CrmContractApprovalController`
**Java**:`@RequestMapping("/crm/contract/approval")`
**Service**:`ICrmContractApprovalService`
| # | HTTP | 路径 | Service 方法 | 请求 | 前端 | 后端 |
|---|------|------|--------------|------|------|------|
| A01 | **GET** | `/list` | `queryPageList` | Query: `CrmContractApprovalBo` + 分页 | `listApproval` | ❌ |
| A02 | **GET** | `/{id}` | `queryById` | Path: `id` | `getApproval` | ❌ |
| A03 | **GET** | `/flow/{contractId}` | `queryListByContractId` | Path: `contractId` | `listContractApproval` / `getContractApprovalFlow` | ❌ |
| A04 | **POST** | `/pass` | `approvePass` | `{ id, approvalOpinion? }` | `approvePass` | ❌ |
| A05 | **POST** | `/reject` | `approveReject` | `{ id, approvalOpinion }` | `approveReject` | ❌ |
| A06 | **POST** | `/transfer` | `approveTransfer` | `{ id, targetApproverId, transferReason }` | `approveTransfer` | ❌ |
| A07 | **POST** | `/export` | `queryList` + Excel | Query/Bo | `exportApproval` | ❌ |
| A08 | **POST** | `/` | `approveContractByContractId` | `{ id: contractId, approvalResult, approvalOpinion, attachments?, remark? }` | `approveContract` / `rejectContract` | ❌ |
| A09 | **POST** | `/refreshNames` | `refreshApproverNames` | `{ contractId? }` | `refreshApproverNames` | ❌ |
**注意**:列表页 `ApprovalDialog` 使用 **A08**(按 **合同 id** + `approvalResult`);独立审批页可用 A04/A05(按 **审批记录 id**)。
---
## 3. 合同变更 — `CrmContractChangeController`
**Java**:`@RequestMapping("/crm/contract/change")`
**Service**:`ICrmContractChangeService`
| # | HTTP | 路径 | Service 方法 | 前端 | 后端 |
|---|------|------|--------------|------|------|
| H01 | **GET** | `/list` | `queryPageList` | `listChange` | ❌ |
| H02 | **GET** | `/{id}` | `queryById` | `getChange` | ❌ |
| H03 | **POST** | `/` | `insertByBo` | `addChange` | ❌ |
| H04 | **PUT** | `/` | `updateByBo` | `updateChange` | ❌ |
| H05 | **DELETE** | `/{ids}` | `deleteWithValidByIds` | `delChange` | ❌ |
| H06 | **POST** | `/submitApproval` | (Controller TODO 空实现) | `submitChangeApproval` | ❌ |
| H07 | **PUT** | `/withdraw/{id}` | `withdrawChange` | `withdrawChange` | ❌ |
| H08 | **PUT** | `/execute/{id}` | `executeChange` | `executeChange` | ❌ |
| H09 | **POST** | `/export` | `queryList` + Excel | `exportChange` | ❌ |
---
## 4. 合同模板库 — `CrmContractTemplateController`
**Java**:`@RequestMapping("/crm/contract/template")`
**与 C08/C09 区别**:此处是 **模板主数据**;C08/C09 是 **单份合同实例** 的富文本内容。
| # | HTTP | 路径 | Service 方法 | 前端 | 后端 |
|---|------|------|--------------|------|------|
| T01 | **GET** | `/list` | `queryPageList` | `listTemplate` | ❌ |
| T02 | **GET** | `/{id}` | `queryById` | `getTemplate`(template.ts) | ❌ |
| T03 | **POST** | `/` | `insertByBo` | `addTemplate` | ❌ |
| T04 | **PUT** | `/` | `updateByBo` | `updateTemplate` | ❌ |
| T05 | **DELETE** | `/{ids}` | `deleteWithValidByIds` | `delTemplate` | ❌ |
| T06 | **PUT** | `/enable/{id}` | `enableTemplate` | `enableTemplate` | ❌ |
| T07 | **PUT** | `/disable/{id}` | `disableTemplate` | `disableTemplate` | ❌ |
| T08 | **POST** | `/export` | `queryList` + Excel | `exportTemplate` | ❌ |
---
## 5. 合同产品 — `ICrmContractProductService`(无独立 Controller)
产品随合同 Bo 的 `products[]` 在 **C03/C04** 中批量写入;Service 另提供:
| Service 方法 | 用途 | 后端 |
|--------------|------|------|
| `queryListByContractId` | 详情/列表展开行 | ❌(合入 C02) |
| `insertByBo` / `updateByBo` / `deleteByIds` | 产品 CRUD | ❌ |
| `updateDeliveryStatus` | 交付进度 | ❌ |
框架合同 **调价区间** `crm_contract_product_price_tier` 在 `insertByBo` / 提交审批校验中处理(见 ServiceImpl)。
---
## 6. Service 层无 Controller 入口(内部 / 跨模块)
| Service 方法 | 调用方 | 说明 | 后端 |
|--------------|--------|------|------|
| `updateReceivedAmount(contractId)` | 回款 Service | 回写 `received_amount` / `unreceived_amount` | ❌ |
| `updateInvoiceAmount(contractId)` | 开票 Service | 回写 `invoice_amount` / `uninvoice_amount` | ❌ |
| `generateContractNo(supplierId, signDate)` | `insertByBo` 内部 | 供方 + 日期规则编号 | ❌ |
| `createFromTemplate(templateId, bo)` | 无 Controller | 若前端后续需要再暴露 | ❌ |
| `queryList(bo)` | C16 导出 | 同 C01 条件无分页 | ❌ |
**定时任务**:合同模块 **无** 专用 cron;到期提醒走 C21 查询接口。
---
## 7. 前端跨模块依赖(非 contract Controller)
| 用途 | HTTP | 路径 | 使用页面 |
|------|------|------|----------|
| 操作日志 | GET | `/monitor/operlog/list` | `detail.vue` |
| OSS 批量 | GET | `/resource/oss/listByIds/{ids}` | index / form / detail / Upload |
| OSS 下载 | GET | `/resource/oss/download/{ossId}` | 附件预览下载 |
| 商机名 | GET | `/crm/opportunity/{id}` | index 详情抽屉 / form |
| 报价单号 | GET | `/crm/quote/{id}` | index 详情抽屉 / form |
shanxia2.0 需确认上述路由是否已有 Python 等价实现或需同步移植。
---
## 8. Python 实现映射(计划)
| 层 | 目标文件 |
|----|----------|
| 路由 | `backend/app/api/crm_contract_routes.py` |
| 子路由 | `backend/app/api/crm_contract_sub_routes.py`(approval / change / template) |
| CRUD | `backend/app/crud/crm/contract.py` |
| 审批 | `backend/app/crud/crm/contract_approval.py` |
| 产品 | `backend/app/crud/crm/contract_product.py` |
| 变更 | `backend/app/crud/crm/contract_change.py` |
| 模板库 | `backend/app/crud/crm/contract_template.py` |
| Schema | `backend/app/schemas/crm/contract.py` 等 |
| 导出列 | `backend/app/utils/crm_export_columns.py`(`CONTRACT_*_EXPORT_COLUMNS` 已存在) |
| 注册 | `backend/app/api/crm.py` 末尾 `register_contract_routes` |
**翻译顺序**(对应断点步骤 15–26):C01+C02 → C03–C05 → C06–C07+A* → C10–C15 → C16–C21 → C22–C27 → H* / T* → 恢复 `quote.to_contract` → C24。
---
## 9. 权限码(Java 注释,实现时可对接 menuCode)
| 权限 | 接口组 |
|------|--------|
| `crm:contract:list` | C01, C17–C21 |
| `crm:contract:query` | C02, C09, C27 |
| `crm:contract:add` | C03, C23, C24 |
| `crm:contract:edit` | C04, C08, C10, C14–C15 |
| `crm:contract:remove` | C05 |
| `crm:contract:submit` | C06–C07 |
| `crm:contract:execute` | C11–C12 |
| `crm:contract:invalid` | C13 |
| `crm:contract:export` | C16 |
| `crm:contract:renew` | C22 |
| `crm:contract:sign` | C25 |
| `crm:contract:assign` | C26 |
| `crm:contract:approval:*` | A01–A09 |
| `crm:contract:change:*` | H01–H09 |
| `crm:contract:template:*` | T01–T08 |
---
## 10. 完成标准(重写后勾选)
- [ ] 27 个主接口 C01–C27 HTTP 动词与 Java 一致
- [ ] 9 个审批接口 A01–A09
- [ ] 9 个变更接口 H01–H09(H06 与 Java 行为对齐或明确实现)
- [ ] 8 个模板接口 T01–T08
- [ ] 内部方法 `updateReceivedAmount` / `updateInvoiceAmount` 可供回款/开票模块调用
- [ ] `createFromQuote` 恢复报价转合同
- [ ] 前端拷贝页在 proxy 配置下可直接调用,无需改为全 POST
---
## 变更记录
| 日期 | 说明 |
|------|------|
| 2026-05-31 | 步骤 1:自 Java Controller + Service 接口全量梳理;废弃旧「全 POST 门户」规格 |