# 审批 + 表单引擎联合审视报告

> 生成时间：2026-04-30
> 调研范围：approval-engine / approval-center / form-engine / form-management 四模块的文档与代码
> 性质：纯静态调研，未起服务、未跑测试，未改任何代码

---

## 1. 模块概览

| 模块 | 一句话定位 | 主要资产 |
|------|-----------|---------|
| **approval-engine** | FFOA 工作流核心。流程定义/版本/实例/任务/Temporal 编排。 | 后端 `backend/src/engines/approval/`；表 `corp_approval.approval_*` 11 张；API 前缀 `/api/v1/approval`。 |
| **approval-center** | 用户运行时（发起/待办/我的）+ 管理员数据中心。 | 前端 `frontend/src/app/(modules)/approval-center/`、`(engines)/approval(s)`；后端复用 approval-engine 的 `admin/*` 路由。 |
| **form-engine** | 表单定义/版本/实例/翻译/模板的引擎层 CRUD。 | 后端 `backend/src/engines/form/form-engine/`；表 `platform_form.form_*` + `release_snapshots`；API `/forms/*` `/form-instances/*` `/form-templates/*` `/form-versions/*`。 |
| **form-management** | 设计态 + 运行时聚合层（前端唯一调用入口）。 | 后端 `backend/src/engines/form/form-management/`；前端 `(engines)/forms/` 全套页面 + `services/api/form-management.ts`；API `/api/v1/form-management/*`。 |

---

## 2. 文档↔代码差异清单

### 2.1 approval-engine

#### A1. 接口数量三处自相矛盾
- **类型**：不一致
- **位置**：
  - README:79 写 "总计：40 个接口"
  - `07-api.md`:13 写 "接口数量 47 个"
  - 实际 `approval.controller.ts` + `process-admin.controller.ts` 路由数 = **52 条**
- **现象**：三个数对不上，单凭文档无法定位"还差哪几个/多了哪几个"。
- **建议**：以代码为准重新清点 → 改文档（README + 07-api 同步）。
- **风险**：文档误导，影响 L0a/L0b 契约校验脚本基准。**契约面（API）**

#### A2. 状态机文档只覆盖"实例"，缺"任务"和动作语义
- **类型**：文档缺失
- **位置**：`04-state-machine.md`（仅 86 行，末尾"待确认项"已自承缺失）
- **现象**：`ApprovalTaskStatus`（10 个值）和 `ApprovalTaskAction`（24 个值）在 schema/06-data-model 已存在，但状态机文档完全不画；退回/加签/委托对实例状态的影响也明文未补。
- **建议**：补任务状态机 + 动作矩阵 + 超时/失败恢复策略 → 改文档。
- **风险**：**契约面（状态机）**。后续做"批量审批""减签""自动通过"会踩。

#### A3. `X-Idempotency-Key` 文档承诺幂等，代码只透传
- **类型**：不一致（已知 TODO）
- **位置**：`07-api.md`:40-44 写"启动流程接口可传入 X-Idempotency-Key 作为幂等键"；`approval.controller.ts:88` 只把 header 塞进 `dto.idempotencyKey`，全链路 `grep` 不到去重逻辑。
- **现象**：业务方按文档调，实际不防重复。
- **建议**：补幂等去重表/Redis 键，或文档明确"当前未做去重"。**用户决策**：上线前是否必须实现。
- **风险**：**契约面（API 行为）**。重复发起会出双单。

#### A4. ApprovalMode 前后端枚举映射有歧义
- **类型**：不一致
- **位置**：`approval-center/07-api.md`:320-329
- **现象**：前端 `SINGLE` 和 `OR` 都映射到后端 `OR_SIGN`，但 `SINGLE`（单人审批）和 `OR`（或签多人）业务语义不同；通过 `mapApprovalModeForWorkflow()` 转换后丢失原始意图。
- **建议**：要么后端拆出 `SINGLE` 枚举值，要么文档明确"单人审批 = 候选人列表只有 1 人的或签"。**用户决策**。
- **风险**：**契约面（枚举/状态机）**。

#### A5. ADMIN_APPROVE/REJECT 的 `targetUser` 仅前端推断
- **类型**：代码缺失
- **位置**：`approval-center/07-api.md`:331-334 已自承"当前前端通过推断 workaround，后端应在写入时持久化 targetUser"。
- **现象**：管理员代审批审计字段缺失，依赖前端传值，事后追溯不可靠。
- **建议**：后端 `admin-approval.service.ts` 写入 `task_logs.target_user_id` 时从 `task.assignee` 直接取 → 改代码。
- **风险**：**契约面（审计/权限）**。

#### A6. `InstanceStatus` 仍可能残留 `COMPLETED`
- **类型**：过时
- **位置**：`approval-center/07-api.md`:336-339 警告"前端不应使用 COMPLETED"；schema enum 已无此值。
- **现象**：旧前端代码或前端 i18n key 可能仍按 COMPLETED 渲染。需 grep 前端确认。
- **建议**：前端全局排查 `COMPLETED` 字符串 → 改代码 / 改 i18n。

---

### 2.2 approval-center

#### B1. 文档承诺的 `/approval-center/*` 前缀**根本不存在**
- **类型**：不一致（重大）
- **位置**：`form-management/07-api.md`:28-37 明文："前端永远只调用 `/form-management/...` 和 `/approval-center/...`"，但 `grep -E "@Controller\\(" backend/src/engines/approval/` 没有任何 `approval-center` 前缀的 controller。前端 `app/(modules)/approval-center/page.tsx` 实际混调 `/approval/admin/*`、`/approval/my/*`、`/form-management/*`。
- **现象**：架构契约（"引擎/聚合分层"）和实现不一致。前端直接打到 engine API。
- **建议**：**用户决策** —— 二选一：
  - (a) 真的拆出 `/approval-center/*` 聚合层 controller（工作量大）；
  - (b) 文档收编现状，承认 approval-center 前端直调 approval-engine，把 approval-engine API 的"用户态接口"标为 approval-center 入口。
- **风险**：**契约面（API 架构）**。决定后续所有 polish 走向。

#### B2. AdminAnalytics 接口路径属于审批引擎，但文档归在审批中心
- **类型**：过时
- **位置**：`approval-center/07-api.md`:14 "Base URL `/api/v1/approval`" 与文档主题"审批中心"分层不一致；7 个管理员接口实际在 `approval.controller.ts` 的 `/admin/*` 段。
- **现象**：与 B1 同源，分层口径混乱。
- **建议**：随 B1 一起决策 → 改文档。

---

### 2.3 form-engine

#### C1. `set-default` 路径与文档不一致
- **类型**：不一致
- **位置**：
  - `07-api.md`:66 写 `POST /forms/:formIdentifier/versions/:version/set-default`
  - 代码 `form-versions.controller.ts` 实际是 `@Post('_actions/set-default')`，body 里带 version
- **现象**：路径形态与示例都不同，前端按文档调会 404。
- **建议**：文档同步成 `_actions/set-default` 形式 → 改文档。**契约面（API）**

#### C2. 创建版本接口疑似缺失
- **类型**：代码缺失
- **位置**：`07-api.md`:61 列 "POST /forms/:formIdentifier/versions  创建版本"；代码 `form-versions.controller.ts` **无对应 POST 根路径**。
- **现象**：要么版本是别处自动创建（如保存设计时），要么真缺。
- **建议**：核对 `form-management/process-design` 链路是否隐式创建版本 → 改文档说明 / 补 API。
- **风险**：**契约面（API）**。

#### C3. 实例导出/统计两个 P0 接口是空 stub
- **类型**：代码缺失（已知 TODO）
- **位置**：
  - `form-instance.service.ts:679` `// TODO: 实现 Excel/CSV 导出逻辑`
  - `form-instance.service.ts:801` `// TODO: 根据 groupBy 参数实现不同的分组统计`
  - `form-instances.controller.ts:77` `// TODO: 实现实际的 Excel/CSV 文件生成`
- **现象**：`GET /form-instances/export` / `GET /form-instances/stats` 不返回真实数据。
- **建议**：补实现或在 API doc 标 🚧 → 改代码。
- **风险**：**功能缺失**。

#### C4. 字段渲染器只覆盖 18/20+ 类型
- **类型**：代码缺失
- **位置**：`frontend/src/features/forms/components/FormFieldRenderer.tsx:749,759` 自承"简单数组类型/对象类型字段（暂未实现）"；`form-management/11-implementation-status.md`:197 写 "字段类型 PRD 20+ / 实际 18 / 缺地址选择、关联选择"。
- **现象**：嵌套数据类型在表单设计器里展示成兜底文案。
- **建议**：补 array/object 渲染器 → 改代码。
- **风险**：**UI 规格契约**。

---

### 2.4 form-management

#### D1. 多区域模型：FormDefinition 没有 regionId 列，文档写得像有
- **类型**：不一致（重大）
- **位置**：
  - `form-management/07-api.md`:54-117 整段架构说 "X-Region-Id 强制注入、按 regionId 过滤"
  - schema `platform_form.prisma` `FormDefinition` model 实际**没有 regionId 字段**，只有 `organizationId`
  - `form-management.service.ts:67-151` 真实实现是"先 where organizationId，再用 organization → region 映射在内存里二次过滤"
- **现象**：文档基于"FormDefinition 直接带 regionId"的心智模型在写，实际是"组织 → 区域"间接映射。性能（内存过滤）+ 心智模型双重 drift。
- **建议**：**用户决策**：
  - (a) 给 FormDefinition 加 regionId 列（schema 变更 + 迁移），跟文档对齐；
  - (b) 文档收编现状，描述"组织 → 区域"间接映射的实际策略。
- **风险**：**契约面（数据模型）**。直接影响所有 list/filter 接口性能与正确性。

#### D2. FormDefinition.status 文档枚举写 ACTIVE，实际是 PUBLISHED
- **类型**：不一致
- **位置**：
  - `form-management/07-api.md`:224 写 "DRAFT / ACTIVE / DISABLED / ARCHIVED"，示例响应 status 为 "ACTIVE"
  - schema 实际是 `FormStatus = DRAFT | PUBLISHED | DISABLED | ARCHIVED`
- **现象**：前端按文档 filter `status=ACTIVE` 会 0 命中。
- **建议**：文档全文搜 ACTIVE → PUBLISHED 替换 → 改文档。
- **风险**：**契约面（枚举）**。

#### D3. activeSnapshotId 列表字段被打 TODO，永远是 undefined
- **类型**：代码缺失
- **位置**：`form-management.service.ts:755` `activeSnapshotId: undefined, // TODO: 实现快照查询`
- **现象**：`GET /definitions` 文档示例返回 `activeSnapshotId: 'snap-001'`，实际所有记录都返回 undefined。前端展示"激活版本号"的列就是空。
- **建议**：补查 ReleaseSnapshot 关联 → 改代码。
- **风险**：**契约面（API 字段）**+ UI 受影响。

#### D4. 撤回流程不通知审批引擎（高危状态漂移）
- **类型**：代码缺失（P0）
- **位置**：`backend/src/engines/form/form-management/services/instance.service.ts:647-657`
  ```ts
  if (instance.approvalInstanceId) {
    // TODO: 调用审批引擎取消流程
    this.logger.log(`Approval process ${instance.approvalInstanceId} should be cancelled`);
  }
  ```
- **现象**：用户撤回表单实例时，FormInstance 改为 WITHDRAWN，但 ApprovalInstance 仍 RUNNING、Temporal Workflow 还在跑、审批人继续能审批。状态完全漂移。
- **建议**：调用 `approvalService.withdraw(...)` 真正终止流程 → 改代码。
- **风险**：**契约面（事件/状态机）**。**P0 必修**。

#### D5. Webhook 重试是空 TODO
- **类型**：代码缺失
- **位置**：`webhook.service.ts:294` `// TODO: 实现重试逻辑（可以使用队列服务）`
- **现象**：失败的 webhook 投递不重试。
- **建议**：接入 BullMQ / 现有 queue → 改代码。
- **风险**：可靠性。

#### D6. 实例服务的"权限检查"是空 TODO
- **类型**：代码缺失
- **位置**：`instance.service.ts:393` `// TODO: 添加更完善的权限检查`
- **现象**：实例修改类操作权限校验粗糙。
- **建议**：明确"当前粒度 + 缺失场景"，按需补 → 改代码。
- **风险**：**权限契约面**。

#### D7. 11-implementation-status 列出但代码确认未补的项
- **类型**：代码缺失（已记录在文档）
- **位置**：`form-management/11-implementation-status.md`
- 已实测确认仍未补：
  - 表单列表 `status` filter 缺 DISABLED 选项（`forms/definitions/page.tsx:311-313`）
  - 概览页缺"本月提交/本月审批"统计卡片 + "统计分析"快捷入口
  - 设计器缺"保存并关闭"按钮
  - 表单设计器：条件显示、计算字段未实现
  - 流程设计器：流程模拟未实现
  - 版本管理：版本对比、回滚、分支均未实现
  - 翻译：批量导出、机器翻译未实现

---

## 3. 两引擎对接面分析

### 3.1 formKey 数据流

```
approval-center 列表
   ↓ 用户点"发起"或选表单
approval-center/submit/[formKey]/page.tsx (formKey from URL)
   ↓ getFormDefinitionByKey(formKey)        → /form-management/definitions?key=…
   ↓ getDesignData(formDefId)               → /form-management/definitions/:id/design
        返回 { formVersion, processVersion, snapshot? }
   ↓ previewProcessWithApprovers(formDefId, formData)
        → /form-management/definitions/:id/preview-process
        返回 { nodes: [{nodeId, name, approvers[], mode}] }
   ↓ FormRenderer 渲染 formVersion.schema (JSON Schema + uiSchema)
   ↓ createInstance / updateInstance (草稿) / submitInstance
        → /form-management/instances[/:id/submit]
   ↓ instance.service.submit():
        - 写 FormInstance.status = SUBMITTED → PENDING_APPROVAL
        - 调 approvalService.startApproval({
            processDefinitionKey: form.approvalProcessKey,
            businessType: 'FORM_INSTANCE',
            businessId: formInstance.id,
            variables: formData,
          })
        - 回写 FormInstance.approvalInstanceId / approvalStatus='RUNNING'
```

### 3.2 数据回写

```
Temporal workflow 走完
   ↓ approval-engine onProcessCompleted hook
   ↓ POST /api/v1/form-instances/_callbacks/approval-completed
        (form-engine 的 form-approval-callbacks.controller)
   ↓ 更新 FormInstance.approvalStatus / status / approvalEndTime
```

### 3.3 隐式约定（"能跑但没明确文档约束"）

| 隐式约定 | 风险 |
|---------|------|
| `FormInstance.approvalStatus` 是 schema 上的 **String 类型**，不是 enum，但 `approval-center/06-data-model.md`:27 写 `RUNNING/APPROVED/REJECTED/...`。callback 写入时无类型保护。 | 容易被脏值污染，无法做 enum 索引/校验。 |
| `businessType = 'FORM_INSTANCE'` 是硬编码常量，approval-engine 用它做 `BusinessTypeRegistry` 路由；但没文档登记"哪些 businessType 是合法的"。 | 后续业务模块要接 approval 时，要靠读代码摸索注册流程。 |
| `FormDefinition.approvalProcessKey` 是表单定义和审批流程定义的桥梁，但 schema 标注 `String?`（可空）；当为空时表单提交流程的行为没文档定义（应该不触发审批？还是报错？）。 | 行为不明确。 |
| 撤回链路：FormInstance.status='WITHDRAWN' 不通知审批引擎（D4）。 | **状态漂移 P0**。 |
| ReleaseSnapshot 是 form 的"发布快照"，绑定 formVersionId + processVersionId，但只有 `processVersionId String?`（可空）。"无审批表单"时 processVersionId 可空。设计文档没明确"无流程发布快照"的语义。 | 状态机隐含分支。 |
| `submit/[formKey]/page.tsx` 用的 `previewProcessWithApprovers` 是审批人**预览**，但提交后实际审批人由 Temporal workflow 内部解析。两次解析结果可能不一致（条件分支 + 数据变化）。 | 用户预期与实际执行漂移。 |
| `regionId` 的"自动注入"：FormInstance.regionId 来自提交时 user.regionId，FormDefinition 没有 regionId（D1 已述）。"用户跨区域"边界情况未覆盖。 | 多租户隔离漏洞潜在。 |
| ApprovalMode 前端 `SINGLE`/`OR` 都映射 OR_SIGN（A4），转换不可逆。 | 配置反向回显时可能错。 |

---

## 4. 已实现 / 缺失 / 待修 三象限清单

### ✅ 已实现（覆盖到位，无需动）

1. **流程定义/版本/实例三层模型** — schema 完整，CRUD 与版本管理跑通。
2. **Temporal 工作流集成** — Activity / workflow 目录齐全，BusinessTypeRegistry 解耦完成（changelog v2.2）。
3. **审批操作动作集** — approve/reject/return/forward/withdraw/approver-withdraw/add-sign/claim/unclaim/execute/remind/batch-remind 路由齐全（10+1+1）。
4. **任务查询 7 条**（待办/已办/我发起/抄送/统计/提醒/搜索）。
5. **管理员数据中心** — analytics + instances + 导出任务（含异步 task）+ settings，路由完备。
6. **抄送（CC）任务类型** — schema + service + my/cc 接口（v2.1）。
7. **表单引擎核心 CRUD** — Form/Version/Instance/Translation/Template/Snapshot/Webhook 全套。
8. **集成设计器 + 流程开关** — 表单与流程同页面配置，"启用审批流程"开关 UX。
9. **版本审核流程** — submit-review / review / pending-review，前后端全套。
10. **多语言翻译管理** — 增删改查 + 完整性检查 + JSON 批量导入。
11. **审批回调链路** — form-approval-callbacks.controller + approvalInstanceId 反向引用。

### ❌ 缺失（文档要求但代码没做 / 业务必备但两边都没有）

| # | 项 | 位置 | 优先级 | 理由 |
|---|----|------|--------|------|
| M1 | 表单撤回时通知审批引擎取消流程 | `instance.service.ts:650` TODO | **P0** | 状态漂移 P0，上线前必修。 |
| M2 | `/approval-center/*` 聚合层（或文档收编现状） | 不存在 | **P0** | 决策卡口，所有 polish 走向取决于此。 |
| M3 | 任务状态机 + 动作矩阵文档 | `04-state-machine.md` | P1 | 退回/加签/超时语义没文档，后续易踩。 |
| M4 | `X-Idempotency-Key` 真正幂等去重 | `approval.service.startApproval` | P1 | 文档承诺未实现，重复提交风险。 |
| M5 | 表单实例 Excel/CSV 导出 | `form-instance.service.ts:679` | P1 | 客户级需求，文档已列接口。 |
| M6 | 表单实例统计聚合（按 groupBy） | `form-instance.service.ts:801` | P1 | 概览页"本月提交/审批"卡片依赖。 |
| M7 | activeSnapshotId 在列表接口返回 | `form-management.service.ts:755` | P1 | 列表 UI 显示不全。 |
| M8 | 表单设计器条件显示字段 | FormDesigner | P1 | PRD 明文要求。 |
| M9 | Webhook 投递重试机制 | `webhook.service.ts:294` | P1 | 可靠性。 |
| M10 | 表单设计器：地址、关联两种字段类型 | FormFieldRenderer | P2 | 设计器完整性。 |
| M11 | array / object 字段渲染器实现 | `FormFieldRenderer.tsx:749,759` | P2 | 嵌套数据展示。 |
| M12 | DISABLED 状态筛选 dropdown | `forms/definitions/page.tsx:311` | P2 | 一行修。 |
| M13 | "保存并关闭"按钮 | 集成设计器 | P2 | UX 小项。 |
| M14 | 概览页"本月提交/审批"卡片 + 统计分析快捷入口 | `(engines)/forms/page.tsx` | P2 | 依赖 M6。 |
| M15 | 表单复制功能 | 表单详情 | P2 | 11-impl-status P2。 |
| M16 | 翻译批量导出 | 翻译管理 | P2 | 11-impl-status P2。 |
| M17 | 计算字段（公式） | FormDesigner | P2 | 11-impl-status P2。 |
| M18 | 流程设计器流程模拟 | DingProcessDesigner | P2 | 11-impl-status P2。 |
| M19 | 版本对比 / 回滚 / 分支 | 版本管理页 | P2/P3 | 11-impl-status P2-P3。 |
| M20 | 减签 / 流程暂停恢复 / 超时提醒 | approval-engine roadmap P1 | P2 | roadmap 待规划。 |

### ⚠️ 待修（已实现但有 bug / 走不通 / 契约不一致）

| # | 项 | 位置 | 优先级 | 理由 |
|---|----|------|--------|------|
| F1 | FormDefinition 多区域模型与文档不一致（schema 无 regionId，文档当作有） | D1 | **P0** | 数据模型契约面，影响所有列表 API。决策路径选 (a) 加列 / (b) 改文档。 |
| F2 | API 接口数量三处对不齐（README 40 / 07-api 47 / 实测 ~52） | A1 | P1 | 重新清点 → 同步文档。 |
| F3 | `FormDefinition.status` 文档写 ACTIVE，实际 PUBLISHED | D2 | P1 | 文档全文替换。 |
| F4 | `set-default` 路径文档写 `/versions/:version/set-default`，实际 `_actions/set-default` | C1 | P1 | 文档同步。 |
| F5 | 创建版本接口在文档列出但代码缺 POST 根路径 | C2 | P1 | 核对 → 文档/代码二选一对齐。 |
| F6 | ApprovalMode 前端 `SINGLE` 和 `OR` 都映射 OR_SIGN，丢语义 | A4 | P2 | 用户决策枚举设计。 |
| F7 | ADMIN_APPROVE/REJECT 的 targetUser 仅前端推断，后端未持久化 | A5 | P2 | 审计字段补写。 |
| F8 | InstanceStatus 前端可能仍残留 COMPLETED | A6 | P2 | grep 排查。 |
| F9 | `FormInstance.approvalStatus` schema String 而非 enum，无类型保护 | 3.3 隐式约定 | P2 | 改为 enum 或文档锁口径。 |
| F10 | `previewProcessWithApprovers` 与实际 workflow 解析可能不一致 | 3.3 隐式约定 | P2 | 文档说明 or 抽公共解析器。 |
| F11 | 实例服务权限检查是 TODO 占位 | `instance.service.ts:393` | P2 | 明确粒度补齐。 |
| F12 | "用户跨区域"边界（user.regionId 与 form 的 organizationId.region 不一致）行为不明 | 3.3 隐式约定 | P2 | 测试 + 文档锁定。 |
| F13 | DTO 缺少 swagger 装饰器（多个 controller `// TODO: Install @nestjs/swagger`） | 多处 form-engine controller | P3 | 仅影响 OpenAPI 自动生成。 |

---

## 5. 建议下一步（按优先级）

> 给用户用来定优先级（步骤 4 用）

1. **【P0 决策卡口】M2 / B1**：先决定 `/approval-center/*` 是真聚合层、还是文档收编现状。这一条决定后续 50% 的 polish 改动方向。我建议 **(b) 文档收编现状**——因为已经跑通的链路重命名收益不大。
2. **【P0 必修】M1 / D4**：撤回流程通知审批引擎取消 workflow。这是一个具体 bug fix，1 人天内可完成 + 必须配集成测试覆盖。
3. **【P0 决策卡口】F1 / D1**：FormDefinition 是否要加 regionId 列。这是 schema 变更，会触发 `prisma migrate` + 跨模块改动。建议 **(a) 加列**——目前的"组织 → 区域"内存过滤在数据量上来后会成性能债。
4. **【契约对齐批次】F2 / F3 / F4 / F5 / A1 / A2**：以代码为准重写 `07-api.md` 和 `04-state-machine.md`，并跑一次 `testing/scripts/contract-check.ts` 看 L0a/L0b 的真实差异。这是后续测试的前置。
5. **【功能补齐第一波】M5 / M6 / M7 / M9**：导出 + 统计 + activeSnapshotId + Webhook 重试。这四项加起来是概览页和列表页"看起来缺数据/不可靠"的核心原因，补完后整体观感"可正式使用"。

---

## 整体健康度评级

🟡 **黄色（可投入但有明显风险）**

理由：核心链路（设计→发布→提交→审批→回写）已跑通且代码组织合理；但存在 1 个 P0 状态漂移 bug（撤回不通知审批引擎）+ 2 个 P0 架构决策卡口（approval-center 前缀、FormDefinition regionId），加上 5+ 处文档↔代码契约不一致。建议优先解决 P0 三项后转绿，再批量推 polish。
