feat(server): 完成 P2 后端能力(支付幂等、委派回收、OCR异步、审计)

这个提交包含在:
2026-02-07 21:54:29 +08:00
父节点 7ea25e9d72
当前提交 c4df9a419a
共修改 23 个文件,包含 2102 行新增和 70 行删除
+53 -5
查看文件
@@ -90,6 +90,9 @@ cd /home/shunli/Projects/hyb-oa/server
- `pay` 权限:非财务返回 `PERM_001`
- `pay` 状态校验:非 `approved/paid` 返回 `VALID_001`
- `pay` 幂等:重复付款返回 200(`archivedInvoiceCount` 可为 0)
- `pay` 幂等冲突:已付款后提交冲突凭证字段返回 `409 VALID_001`(`already_paid_conflict`)
- `delegation`:有效期内可代办并在 history 写入 delegation 证据
- `delegation` 过期:代理人返回 `PERM_002`,自动回收任务可撤销过期委派
### 4.4 一键全量回归
```bash
@@ -126,19 +129,62 @@ npm run dev
---
## 5. 关键环境变量
## 5. P2 API 摘要
### 5.1 付款标记(pay)
- `POST /api/reimbursements/:id/pay`
- 权限:`expense.finance.pay`
- 可选请求体:`paymentVoucherNo`、`paymentDate(YYYY-MM-DD)`、`paymentChannel(bank/alipay/wechat/cash/other)`
- 语义:
- `approved -> paid`:返回 200,并归档关联 `bound` 发票
- 已 `paid` 重复同值:返回 200(`payment.idempotent=true`)
- 已 `paid` 重复冲突值:返回 `409 VALID_001`,`details.payment=already_paid_conflict`
### 5.2 委派管理
- `POST /api/delegations`:创建委派(scope + validFrom/validTo)
- `GET /api/delegations/mine`:查看我发起/我接收的委派
- `POST /api/delegations/:id/revoke`:撤销委派
- 委派仅作用于 `assignee_id` 任务,不改写 `assignee_role` 语义。
### 5.3 OCR 异步状态查询
- 上传发票在 OCR 开启且 `ocr_type=vat` 时会进入 `pending_ocr`。
- `GET /api/invoice-pool/:id` 可轮询 OCR 处理状态(`pending_ocr/available/ocr_failed`)。
### 5.4 审计日志查询
- `GET /api/audit-logs`
- 权限:`system.audit.view`(finance_mgr/admin)
- 支持分页与筛选(actor/action/targetType/date range)。
### 5.5 定时任务(可手动执行)
```bash
cd /home/shunli/Projects/hyb-oa/server
npm run job:revoke-expired-delegations
npm run job:ocr-once
```
---
## 6. 关键环境变量
- `DATABASE_URL`: PostgreSQL 连接串
- `JWT_SECRET`: JWT & signed-url token 签名密钥
- `APP_TIMEZONE`: 默认 `Asia/Shanghai`
- `STORAGE_DIR`: 默认 `generated/storage`(相对 `server/`)
- `SIGNED_URL_TTL_SECONDS`: 默认 `600`
- `MAX_UPLOAD_SIZE_BYTES`: 默认 `10485760`(10MB)
- `DELEGATION_REVOKE_CRON`: 委派自动回收定时表达式(默认 `0 * * * *`)
- `OCR_PROVIDER`: `off` 或 `tencent`(默认 `off`)
- `OCR_BATCH_SIZE`: 每轮 OCR 认领数量(默认 `10`)
- `OCR_LOCK_TTL_SECONDS`: OCR 锁超时秒数(默认 `300`)
- `OCR_POLL_CRON`: OCR 轮询 cron(默认 `* * * * *`)
- `OCR_MAX_ATTEMPTS`: OCR 最大重试次数(默认 `3`)
- `OCR_TIMEOUT_MS`: 腾讯 OCR SDK 超时(默认 `5000`)
- `TENCENT_SECRET_ID` / `TENCENT_SECRET_KEY` / `TENCENT_OCR_REGION`: 腾讯 OCR 配置
> 生产提示:大表加索引需评估 `CREATE INDEX CONCURRENTLY`,避免锁表影响。
---
## 6. 常见失败排查
## 7. 常见失败排查
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
@@ -147,14 +193,16 @@ npm run dev
| signed-url 接口返回 403 | `JWT_SECRET` 不一致、token 过期、访问人无权限 | 统一服务密钥、确认 URL 在 TTL 内、检查账号角色 |
| 上传返回 `file_too_large` | 文件超 `MAX_UPLOAD_SIZE_BYTES` | 调整环境变量或使用更小文件 |
| 上传失败且报目录/权限错误 | `STORAGE_DIR` 不存在或不可写 | 创建目录并修正权限;Docker 需确认 volume 挂载 |
| OCR 长期停留 `pending_ocr` | `OCR_PROVIDER=off` 或密钥无效 | 检查 OCR 环境变量,手动执行 `npm run job:ocr-once` 验证 |
| 委派过期后仍可操作 | 自动回收任务未运行 | 检查 `DELEGATION_REVOKE_CRON`,手动执行 `npm run job:revoke-expired-delegations` |
---
## 7. 手工故障注入(非阻塞)
## 8. 手工故障注入(非阻塞)
### 7.1 `STORAGE_DIR` 不可写路径
### 8.1 `STORAGE_DIR` 不可写路径
- 临时将 `STORAGE_DIR` 指向只读目录,执行上传接口,确认错误可观测(日志+返回码)。
### 7.2 磁盘空间不足(ENOSPC)
### 8.2 磁盘空间不足(ENOSPC)
- 在受控环境中制造磁盘满条件后重试上传。
- 预期:错误路径可观测(日志中能定位到 I/O 错误),不要求纳入日常 CI 冒烟。