265 lines
14 KiB
Markdown
265 lines
14 KiB
Markdown
# 合同管理 — 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 门户」规格 |
|