# 飞书报销审批后端闭环流程

更新时间：2026-06-12（补充审批通过后的打款核销接口）

## 结论

发起飞书审批不应由机器人在会话里直接临时调 OpenAPI 完成。正式流程已经收进公司财务系统后端：

1. 后端先创建/读取报销草稿；
2. 后端实时读取飞书统一报销审批模板；
3. 后端按模板字段生成审批预览；
4. 机器人把预览发给用户最终确认；
5. 用户确认后，后端接口负责：系统入库 → 以“当前和机器人对话的飞书用户”为发起人调飞书发起审批 → 把审批实例 ID 绑定到草稿和报销单；
6. 飞书 webhook 回调后，后端按审批实例 ID 回写数据库状态。

2026-06-09 已用真实测试审批验证该闭环可用：系统测试单 `RB20260609-97CB911E`、`RB20260609-F392C5DA`、`RB20260609-CC5EAD97` 均已通过飞书自动 callback 回写为 `APPROVED`。后端状态守卫已验证：后续无状态 `approval` 事件只记录为 `UNKNOWN` 审计，不覆盖已终态业务状态。

当前业务口径：

- 审批发起人不是固定机器人账号，也不是固定环境变量用户，而是当时在飞书里跟机器人对话并确认提交的用户；
- 现阶段使用 `公司统一报销申请（测试）` 模板做真实联调；流程走通后，该测试模板直接作为正式使用模板维护，不再另建一套重复模板；
- `FEISHU_APPROVAL_USER_ID/OPEN_ID` 只作为本地验证或网关无法传递会话用户时的兜底，不作为正式默认口径。

## 新增后端接口

### 1. 准备审批预览

```http
POST /api/reimbursements/approval/prepare
Authorization: Bearer <api-token>
Content-Type: application/json
```

两种用法：

- 传 `draft_id`：读取已有草稿并生成审批预览；
- 不传 `draft_id`：传 `intent=record_reimbursement` + `draft`，后端先创建草稿，再生成审批预览。

示例：

```json
{
  "intent": "record_reimbursement",
  "operator_subject_id": "sub_person_xxx",
  "draft": {
    "applicant_subject_id": "sub_person_xxx",
    "payee_subject_id": "sub_person_xxx",
    "default_owner_subject_id": "sub_company_yunhan",
    "project_name": "良渚业务",
    "items": [
      {
        "amount": "982.00",
        "occurred_at": "2026-05-07",
        "description": "良渚样品采购",
        "invoice_status": "INVOICED",
        "attachments": [
          { "file_name": "invoice.pdf", "feishu_file_token": "..." }
        ]
      }
    ]
  }
}
```

返回重点：

- `draft.id`：后续确认提交用；
- `approval_preview.fields`：机器人要发给用户确认的字段；
- `approval_preview.submit_ready`：是否具备发起审批条件；
- `approval_preview.missing_required_fields`：缺哪些模板必填字段；
- `approval_preview.template_source`：模板来源，正常应是 `feishu_openapi`。预览阶段允许本地映射兜底，但正式提交不允许靠本地映射兜底。

### 2. 用户确认后提交审批

```http
POST /api/reimbursements/approval/submit
Authorization: Bearer <api-token>
Content-Type: application/json
```

必须传 `confirmed: true`。

示例：

```json
{
  "draft_id": "drf_xxx",
  "confirmed": true,
  "approved_by": "user-confirmed-in-feishu-chat",
  "requester": {
    "feishu_user_id": "<当前对话飞书用户的 user_id>"
  }
}
```

后端执行顺序：

1. 实时读取飞书审批模板，不使用本地映射兜底；
2. 从请求体 `requester.feishu_user_id` / `requester.open_id` 读取当前飞书会话用户，作为审批发起人；
3. 校验草稿和审批模板必填字段；
4. 将草稿入库为 `reimbursement_order`，初始状态 `APPROVAL_SUBMITTING`；
5. 调用飞书 `POST /open-apis/approval/v4/instances` 创建审批实例；
6. 将返回的 `instance_code` 写回：
   - `intake_draft.approval_instance_id`
   - `reimbursement_order.approval_instance_id`
   - `reimbursement_order.approval_provider = feishu`
   - `reimbursement_order.approval_code`
7. 将状态更新为 `APPROVAL_PENDING`；
8. 写 `audit_log`。

如果飞书提交失败：

- 系统保留已经入库的草稿/报销单；
- 状态标记为 `APPROVAL_SUBMIT_FAILED`；
- `audit_log` 记录失败原因；
- 机器人应把真实错误反馈给用户，不要假装审批已提交。

## webhook 回写状态

已存在回调接口：

```http
POST /api/approval/feishu/callback
POST /api/webhooks/feishu/approval
```

回调流程：

1. 校验 Feishu callback verification token；
2. 从回调中提取 `instance_code / approval_instance_id` 和审批状态；
3. 根据 `intake_draft.approval_instance_id` 找到对应草稿；
4. 通过 `source_draft_id` 找到 `reimbursement_order`；
5. 更新状态：
   - `APPROVAL_PENDING`：审批中/发起审批；
   - `APPROVED`：审批通过；
   - `REJECTED`：拒绝/撤回/取消；
   - `PAID`：已打款，由打款核销接口流转，不由飞书回调直接写入；
6. 写入 `approval_callback_event` 防重复；
7. 写入 `audit_log`。

## 审批通过后的打款核销（APPROVED → PAID）

2026-06-12 起，`PAID` 不再是预留状态，由后端打款核销接口流转：

```http
POST /api/reimbursements/:id/payments
```

- 只接受 `APPROVED` 状态的报销单；金额不可超过未付金额；
- 每次打款自动创建一条 `fund_transaction` 真实资金流水（`direction=OUT`，`status=SETTLED`，来源类别 `src_company_repay_person`），或通过 `fund_transaction_id` 复用已有流水做合并打款核销；
- 核销关系写入 `reimbursement_payment`；累计核销达到整单金额时，报销单和来源草稿状态流转为 `PAID`，写 `paid_at`；
- 支持部分付款（未付清保持 `APPROVED`）与 `idempotency_key` 幂等；
- 全程写 `audit_log`；机器人侧对应 MCP 工具 `finance_record_reimbursement_payment`。

字段与表结构详细口径见：`docs/现行数据库表结构与看板字段口径.md` 第 3.3 节。

## 关键环境变量

```text
FEISHU_APP_ID
FEISHU_APP_SECRET
FEISHU_APPROVAL_USER_ID 或 FEISHU_APPROVAL_OPEN_ID
FEISHU_REIMBURSEMENT_APPROVAL_CODE（可选；不设时读取本地映射中的“公司统一报销申请（测试）”模板 code）
FEISHU_CALLBACK_VERIFICATION_TOKEN
FINANCE_API_TOKEN
DATABASE_URL
```

说明：

- 正式部署优先使用公司财务审批专用 App 变量 `COMPANY_FINANCE_FEISHU_APP_ID/SECRET` 获取 tenant_access_token；旧 `FEISHU_APP_ID/SECRET` 只作为历史兼容兜底，不应与 Hermes 聊天机器人凭据混用；
- 正式口径中，审批发起人来自提交请求里的当前飞书会话用户：`requester.feishu_user_id` / `requester.open_id`；
- `FEISHU_APPROVAL_USER_ID/OPEN_ID` 仅保留为本地验证或会话用户 ID 无法透传时的兜底；
- 正式提交阶段必须能实时读取飞书模板；
- 附件字段当前要求能提供飞书审批可用的附件 token，例如 `feishu_file_token` / `approval_file_token`。COS 归档附件只用于我们系统留痕，不能直接当作飞书审批附件 token。

## 当前已验证与仍未覆盖

已验证：

1. 后端提交接口可以使用当前飞书会话用户作为审批发起人；
2. 正式提交阶段可实时读取统一报销模板；
3. `amount`、日期、单选字段和 `attachmentV2` 控件格式已用真实实例验证；
4. webhook verification token 已与飞书后台一致，自动回调可到达公网 callback endpoint；
5. 审批通过后系统状态回写为 `APPROVED`，业务含义是“审批通过/待打款”；
6. 后续无状态 `approval` 事件被记录为 `UNKNOWN` 审计事件，但不会覆盖 `APPROVED`。

仍未覆盖：

1. 真实发票 OCR/结构化入库、发票号码去重和税额校验；
2. 审批拒绝、撤回、取消等异常分支的完整回归；
3. 打款核销接口已在本地实现并通过单元测试（2026-06-12），但尚未部署云端、尚未用真实打款验证。

验证记录：`docs/2026-06-09_报销审批自动回调闭环验证.md`
