feat(server): 完成 P0+P1 后端能力(工作流、RBAC、付款归档)

P0 MVP:

- 认证:login/refresh/logout/forgot/reset + tokenVersion

- RBAC:角色到权限映射 + requirePermission

- 发票池:upload/bind/signed-url + 判重(VALID_003/VALID_005)

- 报销:create/view/submit + 单号生成

- 工作流:approve + department_manager 解析

P1 扩展:

- 动作:reject/return/withdraw(含 comment)

- transfer:任务转审 + 链路上限 3

- pay:approved -> paid + 发票归档 used_complete

- 动态审批链:approval_rules 驱动

回归脚本:

- smoke-test.sh / regression-dedup.sh / regression-workflow-actions.sh
这个提交包含在:
2026-02-07 16:43:31 +08:00
当前提交 f527633716
共修改 44 个文件,包含 11100 行新增和 0 行删除
+150
查看文件
@@ -0,0 +1,150 @@
# hyb-oa 实施计划(Phase 1 / P0)
**日期**:2026-02-06
**范围**:报销系统及其依赖模块(员工/权限/工作流/附件)
## 0. 前置结论(不可再分叉)
- 技术栈:Fastify + Prisma + PostgreSQL(见 `docs/tech_stack_report.md`)
- 规格书:以 `docs/requirements-specification.md` 为单一事实源
## 1. 复用资产清单(从 shenzhou-business-suite 迁移到 hyb-oa)
| 资产 | 来源 | 复用方式 | 需要改造 |
| :--- | :--- | :--- | :--- |
| 工作流 4 表结构 | `shenzhou-business-suite/supabase/schema/70_workflow/01_workflow_02_tables.sql` | 转 Prisma schema | Supabase/RPC → Fastify service 事务 |
| `approval_rules` | 同上 | 转 Prisma schema + seed 3 条 | 管理 UI 放 P1(P0 先 DB/接口维护) |
| `approval_transfers` | 同上 | 转 Prisma schema | 与 `workflow_tasks` 联动,reason≥10 约束 |
| `document_sequences` | 同上 | 转 Prisma schema + seed 默认报销单号 | 生成逻辑改为服务端原子更新 |
| 权限码体系 | `shenzhou-business-suite/src/types/permissions.ts` | 复用命名与粒度 | 增加 `expense.proxy_submit/proxy_manage` |
| 工作流类型 | `shenzhou-business-suite/src/types/workflow.ts` | 参考 JSON 结构 | P0 只用线性审批链(后续扩展保留) |
| 报销表结构参考 | `shenzhou-business-suite/supabase/schema/20_finance/06_reimbursement_01_tables.sql` | 参考 forms/items/attachments 字段 | 补 `file_sha256`、签名 URL、判重例外审计 |
| 默认费用类型 seed | `shenzhou-business-suite/supabase/seed.sql` | 迁移 35 启用 + 3 legacy 禁用 | hyb-oa 额外启用 `utilities/rent` 形成 37 启用 |
| 存储安全基线思路 | `shenzhou-business-suite/supabase/migrations/20260131175000_storage_security_baseline.sql` | 迁移约束到 API 层 | P0 本地存储 + 签名 URL;P2 MinIO/OSS |
| UI 组件 | `shenzhou-business-suite/src/components/ui/*` | 实现阶段复制到 `client/src/` | 替换路由/样式变量;移除 Supabase 依赖 |
| 测试基建 | `playwright` + `vitest` 配置 | 实现阶段迁移 | P0 覆盖鉴权/工作流/判重核心路径 |
## 2. Phase 1(P0)里程碑拆解(按可开工顺序)
### Milestone 1:Prisma schema 草稿 + 评审
**目标**:把关键表结构确定下来(后续开发不再反复改 schema)。
**范围(至少)**:
- 组织/人员:`departments`、`employees`、`employee_roles`
- 权限:权限码为代码常量;角色集合落 `employee_roles`
- 报销:`reimbursement_forms`、`reimbursement_items`、`invoice_pool`(票据/附件池 + 绑定关系)
- OCR:`ocr_results`(OCR 去重与结果持久化)
- 费用类型:`expense_types`(含 37 默认启用 seed)
- 单号:`document_sequences`
- 工作流:`workflow_definitions`、`workflow_instances`、`workflow_tasks`、`workflow_history`
- 审批规则/转审:`approval_rules`、`approval_transfers`
- 认证与审计:`auth_sessions`、`audit_logs`
- 代提交授权:`delegation_grants`
**验收**:
- schema 与 `docs/requirements-specification.md` 一致
- 关键约束可实现(单号唯一、转审原因≥10、费用类型 code 唯一、软禁用等)
**评审截止(Schema Freeze)**:
- 截止:Milestone 1 合并后的 **2 个工作日**
- 截止后变更规则:
- 仅允许 `ADD COLUMN / ADD TABLE / ADD INDEX` 这类向前兼容变更
- `DROP/RENAME/CHANGE TYPE/CHANGE NULLABLE` 等破坏性变更必须走“变更申请”,并同步更新规格书与迁移计划
### Milestone 2:认证与会话(JWT + refresh cookie)
**范围**:
- `POST /api/auth/login|refresh|logout|forgot-password|reset-password`
- `auth_sessions`:refresh hash + deviceFingerprint/ip/UA + session 限额(5)
- `tokenVersion` 踢下线
**验收**:
- refresh rotation 生效;退出/重置能撤销会话;access 不落 localStorage
### Milestone 3A:发票池上传(本地存储)+ SHA256 + 签名 URL
**范围**:
- `POST /api/invoice-pool/upload`(multipart;入参 `file` + 可选 `owner_employee_id` + 可选 `ocr_type`;返回 `invoice_pool_id`)
- `POST /api/invoice-pool/:id/signed-url`(TTL=10m)
- 上传约束:类型白名单 + ≤10MB
- 哈希:计算并落库 `file_sha256`(作为 OCR 去重 key)
**验收**:
- 未授权用户无法获取签名 URL;过期不可访问;同文件哈希可用于 OCR 去重复用
### Milestone 3B:腾讯云 OCR + `ocr_results` 去重 + retry 策略
**范围**:
- OCR:调用腾讯云 OCR,保存“标准化结果”(rawResponse 仅日志不落库)
- 去重:`ocr_results` 以 `file_sha256 + ocr_provider + ocr_type` 唯一,命中时复用(不重复计费)
- 状态机:`invoice_pool` 从 `pending_ocr` → `available/ocr_failed`
- 重试:`POST /api/invoice-pool/:id/retry-ocr`(attempt<=3;间隔>=60s;错误码 `VALID_006/VALID_007`)
- 审计:`OCR_REQUESTED/OCR_SUCCEEDED/OCR_FAILED/OCR_RETRIED`
- 自动归档:`available/ocr_failed` 未绑定条目 90 天后自动归档为 `archived_reason=unused_timeout`
**验收**:
- 同一文件二次上传不重复计费;OCR 失败可重试且次数/频控生效;90 天游离自动归档生效且不参与全局判重;状态与审计一致
### Milestone 4:费用类型系统(动态 `expense_types`)
**范围**:
- `GET /api/expense-types?enabled=true`
- seed:默认启用 37 项 + legacy 3 项禁用
- 软禁用语义:禁用后不出现在下拉,但历史可展示
**验收**:
- 下拉加载启用类型;禁用类型不显示但历史单据不丢
### Milestone 5:单号生成(`document_sequences`)
**范围**:
- 报销单号:`BX-YYYYMMDD-0001`(daily reset)
- 生成逻辑:数据库原子递增(同事务写入 `reimbursement_forms.form_number`)
**验收**:
- 并发下无重复编号;失败回滚不产生脏占用
### Milestone 6:报销单 CRUD + 提交发起工作流
**范围**:
- 报销单:创建/更新/删除草稿/查看
- 发票池绑定/解绑:`POST /api/invoice-pool/:id/bind|unbind`(并发行锁;冲突 409/`VALID_005`;绑定时触发全局判重 409/`VALID_003`)
- 删除草稿:自动解绑其绑定的发票池条目并回到 `available`
- 提交:校验 items、票据/发票要求(发票池绑定)、判重;生成单号;启动工作流实例并创建首个任务
- 代提交:校验 `delegation_grants`(scope & 生效时间)
- 代提交授权(Delegation Grants)CRUD API(用于自助/代维护):
- `GET /api/delegation-grants`(默认返回与本人相关;`system.users` 可查询任意)
- `POST /api/delegation-grants`(本人创建:principal=自己;`system.users` 可代创建)
- `POST /api/delegation-grants/:id/revoke`(本人撤销;`system.users` 可代撤销)
**验收**:
- draft → submitted/reviewing;workflow_instance/task/history 创建完整
- delegation_grants 可被创建/查询/撤销;所有变更写入 `audit_logs`
### Milestone 7:审批动作(approve/reject/return/withdraw/transfer)
**范围**:
- `approval_rules` 匹配审批链(3 条默认规则)
- `POST /api/workflow/tasks/:taskId/approve|reject|return`
- `POST /api/workflow/tasks/:taskId/transfer`(reason≥10;链路≤3)
- 审计:动作写入 `workflow_history`;关键例外写 `audit_logs`
**验收**:
- 审批推进正确;转审三处证据齐全;状态与任务一致(status=reviewing 但节点来自 task)
### Milestone 8:付款标记 + 发票判重例外 + 发票池归档
**范围**:
- `POST /api/reimbursements/:id/pay`(需 `expense.finance.pay`)
- 付款完成联动:将该报销单绑定的发票池条目归档(`status=archived, archived_reason=used_complete`)
- 发票双策略判重 + 例外(仅 `finance_mgr`,需原单号+原因,原单 `approved/paid` 禁止)
**验收**:
- approved → paid;发票池条目自动归档为 `used_complete`;例外受限且审计完整
### Milestone 9:最小 CI 门槛(护栏)
**范围**:
- typecheck + lint(不改写源码)
- 最小集成测试:登录/发票池上传+OCR+绑定/提交/审批/转审/判重/付款标记/归档
- 安全回归:敏感字段脱敏测试(银行卡/身份证等字段在所有对外响应均为脱敏格式;审计日志禁止明文)
**验收**:
- CI 通过:typecheck + lint + 最小集成测试 + 脱敏测试全绿
## 3. Phase 2(扩展)
- OCR、企微/钉钉通知、报表导出
- 对象存储(MinIO/OSS)
- 工作流扩展到其他实体(请假/采购)
## 4. 风险与阻塞项(P0)
- 财务对“费用类型/科目默认值”仍可能需要微调:通过 `expense_types` 配置解决,避免改代码
- 转审/例外权限属于高风险动作:必须上线前完成审计与权限回归测试
文件差异内容过多而无法显示 加载差异
+66
查看文件
@@ -0,0 +1,66 @@
# hyb-oa 技术栈与复用报告(定案)
**日期**:2026-02-05
**适用范围**:OA 报销系统(Phase 1)
## 1. 定案结论(统一口径)
| 层级 | 选型 |
| :--- | :--- |
| 前端(OA) | React + Vite + Tailwind |
| 后端(OA API) | Fastify |
| ORM | Prisma |
| 数据库 | PostgreSQL |
| 认证 | JWT(access + refresh rotation),refresh 以 httpOnly Cookie 传输 |
| 权限 | RBAC(权限码) |
| 审计 | `workflow_history`(流程)+ `audit_logs`(安全与关键变更) |
| 部署 | Docker + Nginx(反向代理) |
## 2. 关键取舍(为什么这么选)
### 2.1 常驻 Node 服务场景:优先“稳定性/少踩坑”
本项目目标是云服务器常驻 Node 服务 + 长期演进,因此选型优先级为:
1) 工具链成熟度与可维护性
2) 团队上手成本与踩坑风险
3) 可观测性/审计/权限等生产配套
4) 性能(在常驻服务场景下,冷启动优势不显著)
### 2.2 Prisma vs Drizzle(定案:Prisma)
- Prisma:迁移范式统一、资料多、团队协作成本低;适合“新人不踩坑”
- Drizzle:更轻、更 SQL 直观,但更依赖团队 SQL/事务/索引纪律;在本项目“稳定性优先”的定义下不作为默认
### 2.3 Fastify vs Hono(定案:Fastify)
- Fastify 适合常驻服务:生态成熟、插件与生产实践丰富(鉴权、日志、schema 校验等)
- Hono 更偏极简/Edge/Serverless 方向;并非本项目的默认最优解
### 2.4 为什么不使用 Supabase(本项目定案)
本项目部署形态为“自建常驻 API + 自建权限/审计/工作流/文件存储”,Supabase 在此形态下会带来:
- 额外的运维与概念复杂度(RLS、RPC、Storage、Auth 等)
- 架构边界不清(哪些逻辑在 DB,哪些在服务层)
因此 hyb-oa 选择“Fastify + Prisma + PostgreSQL”作为单一事实源。
## 3. 复用资产清单(来自 `shenzhou-business-suite`)
> 原则:复用“成熟结构/约束/枚举/经验”,把改造集中在“Supabase 调用 → REST API 调用”。
| 资产 | 来源路径(参考) | 复用价值 | 备注/改造点 |
| :--- | :--- | :---: | :--- |
| 工作流引擎 4 表 + `approval_rules`/`approval_transfers`/`document_sequences` | `shenzhou-business-suite/supabase/schema/70_workflow/01_workflow_02_tables.sql` | ⭐⭐⭐⭐⭐ | 转 Prisma schema;工作流事务由 Fastify 服务层实现 |
| 工作流配置/类型(assignee_rule 等) | `shenzhou-business-suite/src/types/workflow.ts` | ⭐⭐⭐⭐ | 作为工作流 JSON 配置与契约参考 |
| 权限码体系 | `shenzhou-business-suite/src/types/permissions.ts` | ⭐⭐⭐⭐ | 复用命名与粒度;补 `expense.proxy_*` |
| 报销表结构参考(forms/items/attachments) | `shenzhou-business-suite/supabase/schema/20_finance/06_reimbursement_01_tables.sql` | ⭐⭐⭐⭐ | 字段设计参考;hyb-oa 简化并补 `file_sha256` 等 |
| 默认费用类型 seed(35 启用 + 3 legacy 禁用) | `shenzhou-business-suite/supabase/seed.sql` | ⭐⭐⭐⭐⭐ | hyb-oa 额外启用 `utilities/rent` 形成 37 启用 |
| 存储安全基线(类型/大小/访问控制) | `shenzhou-business-suite/supabase/migrations/20260131175000_storage_security_baseline.sql` | ⭐⭐⭐⭐ | 思路迁移到“本地存储 + 签名 URL” |
| UI 组件(shadcn/ui 风格) | `shenzhou-business-suite/src/components/ui/*` | ⭐⭐⭐⭐ | 实现阶段复制到 `hyb-oa/client/src/`(需统一路径与样式变量) |
| 报销前端页面结构 | `shenzhou-business-suite/src/pages/finance/reimbursement/*` | ⭐⭐⭐ | 需移除 Supabase 调用,替换为 OA API |
| 测试框架 | `shenzhou-business-suite/playwright.config.ts` + `vitest.config.ts` | ⭐⭐⭐ | 实现阶段迁移;补齐鉴权/工作流核心路径 |
## 4. 关键设计原则(写入规格书的那几条)
- 状态 vs 节点分离:报销单 `status` 固定 7 状态;审批节点由 `workflow_tasks` 驱动
- 配置驱动:`approval_rules`/`expense_types`/`document_sequences` 均可配置;P0 预置默认值即可生效
- 审计分层:
- `workflow_history`:流程动作与审批链路(权威)
- `audit_logs`:登录/敏感字段/配置变更/判重例外等安全关键事件
- 文件存储:P0 本地存储 + 签名 URL;P2 切换 MinIO/OSS(接口不变)
## 5. 与本仓库当前现状对齐(信息校验)
- `server/package.json` 已包含:`fastify`、`@fastify/jwt`、`prisma/@prisma/client`、`zod`
- `docker-compose.yml` 已提供 PostgreSQL 15(本地/开发用)