docs(i3): add I3 design for M2 contracts and M10-F01 audit

Document REST shape, state machine, audit query, and Webhook DTO v0.1
alignment for iteration I3 (parallel tracks + product M2 P0).

Made-with: Cursor
This commit is contained in:
2026-04-06 21:29:17 +08:00
parent f94f03bcc2
commit 5b50bf0fd8
+397
View File
@@ -0,0 +1,397 @@
# 迭代 I3 设计说明 — M2 合同与行项 P0、M10-F01 审计、Webhook 事件 DTO v0.1
> **迭代定位**:与 [并行迭代索引](../PARALLEL_ITERATION_INDEX.md) 中 **I3** 一致 — 平台后端 **M2 合同 + 行项**、**M10-F01 关键字段变更日志**Webhook 侧 **事件 DTO v0.1** 与平台主键/枚举对齐,便于 I5 Callback 关联。
> **分支**`develop`(本仓库为契约与 SDK 工作区;平台运行时实现可在 `delivery-platform` 仓库,路径风格须与本仓 OpenAPI 一致)。
---
## 1. 上下文与引用文档
| 文档 | 路径 | 本迭代取用要点 |
| ------------------ | -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| 并行迭代索引 | [docs/engineering/PARALLEL_ITERATION_INDEX.md](../PARALLEL_ITERATION_INDEX.md) | I3 范围:M2 + M10-F01**I3 末**同步「合同状态枚举与非法迁移码」「M10-F01 字段」。 |
| 轨道 A(后端 + Webhook | [docs/engineering/tracks/01-backend-platform-webhook.md](../tracks/01-backend-platform-webhook.md) | I3:合同+行、状态机;审计;Webhook **事件 DTO 规范化**、与平台枚举对齐;契约:**合同/行 id** 供 Callback 关联。 |
| 产品模块与功能点 | [docs/chuangfei-platform-product-modules.md](../../chuangfei-platform-product-modules.md) | **M2-F01F04P0**:登记编辑、状态机、标的摘要、行项;**M10-F01(P0)**:关键字段变更日志(旧值/新值/人/时间)。 |
**现有 API 路径约定**(须保持一致):`services/delivery-platform-api` 中 Controller 使用 `**@RequestMapping("/api/v1/...")`**,例如 `/api/v1/customers``/api/v1/projects`。合同相关接口统一前缀 `**/api/v1/contracts`**。
**OpenAPI 单一事实来源**:契约快照路径为仓库根下 `[contracts/openapi/delivery-platform-api.json](../../../contracts/openapi/delivery-platform-api.json)`;新增/变更接口须更新该快照,并与 `OpenApiContractSnapshotTest` 对齐。
---
## 2. 领域模型:合同(Contract
### 2.1 实体职责
合同是「卖什么」的权威来源之一,关联 M1 客户与项目;行项为履约/授权上游锚点(与后续 M3/M4 衔接)。
### 2.2 字段(P0
| 字段 | 类型 | 约束 | 说明 |
| ------------------------- | --------------- | ------- | ------------------------------------------------------------------------ |
| `id` | int64 | 主键 | 与现有 `customers`/`projects` 一致用雪花或序列,API 中为 string 或 number 以 OpenAPI 为准。 |
| `contractNumber` | string | 必填,业务唯一 | 合同编号;唯一索引。 |
| `customerId` | int64 | 必填,FK | 指向客户。 |
| `projectId` | int64 | 必填,FK | 指向项目;须属于同一 `customerId`(服务端校验)。 |
| `signedAt` | date (ISO-8601) | 必填 | 签订日。 |
| `effectiveAt` | date | 必填 | 生效日。 |
| `endAt` | date | 可选 | 结束/到期日;与「终止」语义可并存(终止优先于到期展示逻辑由前端/报表约定)。 |
| `status` | enum | 必填 | 见 §3;**API 与 JSON 仅使用英文枚举名**。 |
| `createdAt` / `updatedAt` | timestamp | 系统字段 | 审计展示可与 M10-F01 互补。 |
### 2.3 状态枚举(对齐 BPM 语义)
| BPM 语义(展示/字典) | API 枚举值 `ContractStatus` |
| ------------- | ------------------------ |
| 草稿 | `DRAFT` |
| 待生效 | `PENDING_EFFECTIVE` |
| 生效 | `EFFECTIVE` |
| 变更中 | `CHANGING` |
| 终止 | `TERMINATED` |
字典表可增加 `dict_type = CONTRACT_STATUS``code` 与上表一致,`label_zh` 为左列中文。
### 2.4 编辑规则(与状态机联动)
- **仅 `DRAFT` 状态**允许对**合同头字段**(§2.2 中除 `id``status``createdAt``updatedAt` 外,是否含 `contractNumber` 由产品确认;P0 建议 **DRAFT 下编号可改**,一旦进入 `PENDING_EFFECTIVE` 则编号只读)及**行项**做增删改。
-`DRAFT` 下对合同头或行项的 `PUT`/`POST`/`DELETE`**409 Conflict**,错误码见 §4.3。
- 状态变更**仅允许**通过 `**POST .../transition`**(§5.4),禁止在普通 `PUT` 请求体中直接改 `status`(若传入与当前相同可忽略或 400,建议 **忽略** 幂等)。
---
## 3. 领域模型:合同行(Contract Line
### 3.1 字段(P0
| 字段 | 类型 | 约束 | 说明 |
| ------------- | --------------- | ----- | ------------------------------------------------ |
| `id` | int64 | 主键 | |
| `contractId` | int64 | 必填,FK | |
| `lineNo` | int32 | 必填 | 行号,从 1 递增;**同一合同内唯一**;用于展示与排序。 |
| `skuCode` | string | 条件 | `**skuCode``productName` 至少填一个**(另一个可为 null)。 |
| `productName` | string | 条件 | 无 SKU 时的产品/包名称。 |
| `quantity` | decimal 或 int64 | 必填 | 数量;小数与否由产品线约定,P0 建议 `number` JSON。 |
| `unitPrice` | decimal | 可选 | 单价;敏感字段可按角色脱敏(见 §9)。 |
| `termNotes` | string | 可选 | 期限/席位/交付与授权口径等说明(与 M2-F03 摘要同源数据)。 |
### 3.2 排序
列表接口默认按 `lineNo` 升序;`lineNo` 可由客户端指定,冲突时 **409**,错误码建议 `CONTRACT_LINE_NO_CONFLICT`
---
## 4. 状态机
### 4.1 允许迁移(P0
以下「当前状态 → 目标状态」为 **允许**;未列出的单向迁移视为 **禁止**
| 当前状态 | 允许的目标状态 |
| ------------------- | --------------------------------------- |
| `DRAFT` | `PENDING_EFFECTIVE``TERMINATED` |
| `PENDING_EFFECTIVE` | `EFFECTIVE``DRAFT`(撤回至草稿)、`TERMINATED` |
| `EFFECTIVE` | `CHANGING``TERMINATED` |
| `CHANGING` | `EFFECTIVE``TERMINATED` |
| `TERMINATED` | (终态,不允许任何迁出) |
**说明**
- `PENDING_EFFECTIVE``DRAFT`:用于「待生效前撤回修改」;撤回后恢复 §2.4 头行可编辑。
- `CHANGING``EFFECTIVE`:变更完成、回到生效。
- P0 **不**实现变更子版本表(M2-F07 为 P1)。`**CHANGING` 下合同头与行项与普通非草稿状态相同:禁止 `PUT`/`POST`/`DELETE` 行与头**,仅允许 `POST .../transition`(例如转至 `EFFECTIVE``TERMINATED`);若业务需要「变更中改行」,留待后续迭代专用变更 API。
### 4.2 非法迁移响应
- HTTP `**409 Conflict`**。
- 响应体(与平台统一错误结构对齐;若尚无 RFC 7807,则用 JSON)示例:
```json
{
"code": "CONTRACT_ILLEGAL_STATUS_TRANSITION",
"message": "不允许从当前状态转换到目标状态",
"currentStatus": "EFFECTIVE",
"targetStatus": "DRAFT"
}
```
- `code` 固定 `**CONTRACT_ILLEGAL_STATUS_TRANSITION**`,便于前端与 SDK 分支处理。
- 日志与 M10:建议记一条 `audit_log``action = STATUS_TRANSITION_DENIED`(见 §6)。
### 4.3 非草稿编辑冲突
在不允许编辑的状态下修改头或行:
- HTTP `**409 Conflict**`
- `code`: `**CONTRACT_NOT_EDITABLE_IN_STATUS**`(或细分头/行码,P0 可合并为一个码)。
```json
{
"code": "CONTRACT_NOT_EDITABLE_IN_STATUS",
"message": "仅草稿状态可编辑合同及行项",
"status": "EFFECTIVE"
}
```
---
## 5. REST API 设计
**前缀**`/api/v1`(与 [CustomerController](../../../services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/customer/CustomerController.java) 一致)。
**认证**:与现有 `/api/v1/customers` 相同(JWT 等);未登录 **401**
**分页**:列表接口与 customers 对齐:`page`(从 0 默认)、`size`(默认 20,最大 200)。
### 5.1 合同
| 方法 | 路径 | 说明 |
| ------ | -------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `GET` | `/api/v1/contracts` | 分页列表;查询参数:`page``size``customerId?``projectId?``status?``keyword?`(匹配 `contractNumber`)。 |
| `POST` | `/api/v1/contracts` | 创建;**初始状态必须为 `DRAFT`**(请求体可不传 `status`,服务端默认 `DRAFT`;若传其它值 **400**)。 |
| `GET` | `/api/v1/contracts/{contractId}` | 详情;含行项可内嵌或仅用行接口拉取(P0 建议详情 **内嵌 `lines`** 减少往返)。 |
| `PUT` | `/api/v1/contracts/{contractId}` | 更新头字段;**仅 `DRAFT`****不得**携带 `status` 变更(忽略或 400,建议 **400** `CONTRACT_STATUS_USE_TRANSITION_ENDPOINT`)。 |
**创建请求示例**
```json
{
"contractNumber": "HT-2026-0001",
"customerId": 1001,
"projectId": 2002,
"signedAt": "2026-04-01",
"effectiveAt": "2026-04-15",
"endAt": "2027-04-14"
}
```
**详情响应示例(内嵌行)**
```json
{
"id": 3001,
"contractNumber": "HT-2026-0001",
"customerId": 1001,
"projectId": 2002,
"signedAt": "2026-04-01",
"effectiveAt": "2026-04-15",
"endAt": "2027-04-14",
"status": "DRAFT",
"lines": [
{
"id": 4001,
"lineNo": 1,
"skuCode": "SKU-PRO-01",
"productName": null,
"quantity": 10,
"unitPrice": 1999.00,
"termNotes": "1 年订阅,100 席位"
}
],
"createdAt": "2026-04-06T10:00:00Z",
"updatedAt": "2026-04-06T10:00:00Z"
}
```
### 5.2 合同行 CRUD
| 方法 | 路径 | 说明 |
| -------- | ----------------------------------------------- | ---------------------------- |
| `GET` | `/api/v1/contracts/{contractId}/lines` | 行列表(可选,若详情已内嵌则可与产品取舍)。 |
| `POST` | `/api/v1/contracts/{contractId}/lines` | 新增行;**仅合同为 `DRAFT`**。 |
| `GET` | `/api/v1/contracts/{contractId}/lines/{lineId}` | 单行。 |
| `PUT` | `/api/v1/contracts/{contractId}/lines/{lineId}` | 更新;**仅 `DRAFT`**。 |
| `DELETE` | `/api/v1/contracts/{contractId}/lines/{lineId}` | 删除;**仅 `DRAFT`**;成功 **204**。 |
**创建/更新行请求体示例**
```json
{
"lineNo": 2,
"skuCode": null,
"productName": "企业旗舰包",
"quantity": 5,
"unitPrice": null,
"termNotes": "按项目交付"
}
```
### 5.3 状态迁移
| 方法 | 路径 | 说明 |
| ------ | ------------------------------------------- | ----------------------------------------------------------------------------- |
| `POST` | `/api/v1/contracts/{contractId}/transition` | 请求体指定目标状态;校验 §4.1;成功返回更新后的合同 DTO(或 204 + LocationP0 建议 **200 + 完整合同 JSON**)。 |
**请求体**
```json
{
"targetStatus": "PENDING_EFFECTIVE"
}
```
**成功**`200`body 为合同资源(含 `lines` 若详情惯例如此)。
---
## 6. M10-F01`audit_log` 表与读 API
### 6.1 表设计(建议名 `audit_log`
| 列名 | 类型 | 说明 |
| --------------- | ------------ | --------------------------------------------------------------------------------------------------------------- |
| `id` | bigserial PK | |
| `entity_type` | varchar(32) | 枚举:`**CUSTOMER`**、`**CONTRACT`**、`**CONTRACT_LINE**`(与产品 M10-F01「客户、合同、SN…」对齐;I3 落地三类,SN 后续迭代加 `LICENSE_SN` 等)。 |
| `entity_id` | int8 | 业务主键,与 `entity_type` 对应实体 id。 |
| `action` | varchar(64) | 如:`CREATE``UPDATE``DELETE``STATUS_TRANSITION``STATUS_TRANSITION_DENIED`。 |
| `field_name` | varchar(128) | 可选;字段级变更时记录英文名,如 `effectiveAt``status`。 |
| `old_value` | text | JSON 字符串;无则 NULL。 |
| `new_value` | text | JSON 字符串;无则 NULL。 |
| `actor_user_id` | int8 | 操作人用户 id。 |
| `created_at` | timestamptz | 不可改。 |
**索引**
- `(entity_type, entity_id, created_at DESC)` — 按对象拉时间线。
- 可选:`(actor_user_id, created_at DESC)` — 按人审计(M10-F02 预备)。
**写入时机(I3 最小集)**
- 合同:创建、头字段更新(`DRAFT`)、每次成功 `transition``field_name=status`,old/new 为枚举字符串)。
- 合同行:创建、更新、删除(`entity_type=CONTRACT_LINE``entity_id=lineId`)。
- 拒绝的非法迁移:可选记 `STATUS_TRANSITION_DENIED``new_value` 可存目标状态 JSON。
### 6.2 读 API
| 方法 | 路径 | 说明 |
| ----- | --------------- | ------- |
| `GET` | `/api/v1/audit` | 分页审计列表。 |
**查询参数(组合过滤)**
- `entityType` + `entityId`:精确到单一实体(如某合同、某行、某客户)。
- `**contractId`**:便捷范围 — 返回 `entity_type IN ('CONTRACT','CONTRACT_LINE')` 且(合同 id = contractId **或** 行所属 contractId = contractId)的记录;实现上可用 SQL `UNION` 或冗余 `contract_id` 列(**推荐冗余 `contract_id` 可空** 于 `audit_log` 以简化查询:合同与行写入时均填 `contract_id`,客户实体则只填 `entity_`*)。
**冗余列(可选但强烈推荐)**
| 列名 | 说明 |
| ------------- | ---------------------------------------------------------------------- |
| `contract_id` | 可空;`CONTRACT` 时等于 `entity_id``CONTRACT_LINE` 时为父合同 id`CUSTOMER` 可为空。 |
**响应项示例**
```json
{
"id": 900001,
"entityType": "CONTRACT",
"entityId": 3001,
"contractId": 3001,
"action": "STATUS_TRANSITION",
"fieldName": "status",
"oldValue": "\"DRAFT\"",
"newValue": "\"PENDING_EFFECTIVE\"",
"actorUserId": 42,
"createdAt": "2026-04-06T12:00:00Z"
}
```
说明:`old_value`/`new_value`**JSON text**(字符串加引号、对象则序列化),解析由前端或工具完成。
---
## 7. Webhook 事件 DTO v0.1Callback 关联预备)
**目标**:与平台合同/行主键对齐,便于 I5 Inbox 关联与幂等;**不**在本迭代要求完整比特 payload,仅 **最小信封**
### 7.1 建议信封字段(v0.1
| 字段 | 类型 | 必填 | 说明 |
| --------------- | ----------------- | --- | ----------------------------------------------- |
| `schemaVersion` | string | 是 | 固定 `**0.1`**(后续 `0.2``1.0` 递增)。 |
| `contractId` | int64 | 条件 | 与平台合同 id 一致;若事件仅到行级,仍建议带父 `contractId`。 |
| `lineIds` | array of int64 | 否 | 涉及的合同行 id 列表;无行级时可 `[]` 或省略。 |
| `eventType` | string | 否 | 预留,如 `contract.status.changed`(与 M5 字典统一可在 I5)。 |
| `occurredAt` | string (ISO-8601) | 否 | 事件发生时间。 |
**示例**
```json
{
"schemaVersion": "0.1",
"contractId": 3001,
"lineIds": [4001, 4002],
"eventType": "contract.status.changed",
"occurredAt": "2026-04-06T12:00:00Z"
}
```
**版本策略**Webhook 与平台共用 `schemaVersion` 语义;**I3 归档** JSON Schema 或本仓库 `contracts/` 下示例文件(与 [轨道 A 文档 §3](../tracks/01-backend-platform-webhook.md) 的 `schemaVersion` / `X-Event-Schema-Version` 一致)。
---
## 8. 安全与 RBAC(对齐产品粗粒度矩阵)
产品文档 [§13.3](../../chuangfei-platform-product-modules.md) 模块矩阵中 **M2 合同**、**M10 审计** 与角色关系如下(**R** 查看,**W** 新建编辑,**X** 导出)。用户提到的 `**DEVELOPER`** 在产品预置角色中为 `**DEV_SUPPORT`(研发/集成支撑)** — 实施时角色码二选一须与 IAM 统一,下文按矩阵描述 `**DEV_SUPPORT`**,若代码命名为 `DEVELOPER` 则与之对齐。
### 8.1 `SYS_ADMIN`
- **M2 合同**:矩阵为 **M / RWDX** — 含模块管理语义;企业可配置是否开放业务写。建议:**生产默认** `SYS_ADMIN` **仅 M11 管理面**,业务合同与 `**SALES`/`ORDER_SUPPORT`** 同权或按企业策略单独开 `contract:`* 权限码。
- **M10 审计**:矩阵为 **M**(管理面);若启用审计检索,建议单独挂 `audit:search`
### 8.2 `DEV_SUPPORT`(研发支撑 / 可与 `DEVELOPER` 对齐)
- **M2 合同**:**R**(只读)— 无合同创建/编辑/删除/导出,除非临时提权。
- **M10 审计**:**R** — 可检索审计(与矩阵「研发支撑」列一致)。
### 8.3 I3 API 权限码映射(建议)
| 接口 | 建议权限码 | 典型角色 |
| ---------------------- | -------------------------------------------------- | ----------------------------------------------------------------- |
| 合同与行 CRUD、`transition` | `contract:order:rw` | `SALES``ORDER_SUPPORT``SYS_ADMIN`(若开放业务写) |
| 合同只读列表/详情 | `contract:order:rw` 或细拆 `contract:order:read`Mid | 上表 + `DELIVERY``LICENSE_OPS``FINANCE_VIEW``EXEC_VIEW` 等只读列 |
| `GET /api/v1/audit` | `audit:search` | `COMPLIANCE``DEV_SUPPORT``FINANCE_VIEW`(导出另加 `audit:export` P1 |
**说明**:粗粒度阶段可将「读合同」与「写合同」合并为同一码,但 `**transition`** 必须与写权限同级或单独 `contract:order:transition`,避免只读角色误调;P0 可与 `contract:order:rw` 绑定。
---
## 9. OpenAPI 与交付物
- **快照路径**`[contracts/openapi/delivery-platform-api.json](../../../contracts/openapi/delivery-platform-api.json)`
- I3 完成定义:**上述路径、枚举、主要 DTO** 均须出现在该 OpenAPI 文件中,并与 `services/delivery-platform-api` 运行时 `/v3/api-docs``**OpenApiContractSnapshotTest`** 校验一致。
---
## 10. 修订记录
| 日期 | 说明 |
| ---------- | ---------------------------------------------------------- |
| 2026-04-06 | 初版:I3 合同/行项、状态机、REST、M10-F01、Webhook v0.1、RBAC、OpenAPI 引用。 |