diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 00000000..1297ea88 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,519 @@ +# hyb-oa 系统架构概览 + +**版本**:1.1 +**日期**:2026-02-09 +**目的**:帮助新成员快速理解系统"怎么做"的全貌(并提供演进方向)。 + +--- + +## 阅读指南:As‑Is vs To‑Be + +- **As‑Is(现状)**:以当前仓库代码、试运行部署包为准,用于回归、排障、交接。 +- **To‑Be(前瞻)**:面向 P5+ 的演进目标,用于指导架构升级;所有未来项都会明确标注 **[To‑Be/未实现]**,避免误解为已交付。 + +--- + +## 1. 系统架构(部署视图) + +### 1.1 部署视图(As‑Is / 单机试运行) + +> 现状特点:**单机可运行、同域可部署、作业同进程**(node-cron),外部依赖最小。 + +```mermaid +graph TB + subgraph Client["客户端"] + Browser["浏览器 (React SPA)"] + end + + subgraph Node["单机(dev/trialrun)"] + Proxy["可选:Nginx/Caddy\nTLS/反代/限流/上传体积控制"] + App["Fastify Server\n(API + 可选静态托管 client/dist)"] + Jobs["同进程 Jobs (node-cron)\nOCR Job / 委派回收 Job"] + PG["PostgreSQL (15+/16)"] + Storage["本地存储 (票据文件)"] + end + + subgraph External["外部服务(可选)"] + TencentOCR["腾讯云 OCR"] + end + + Browser -.->|可选 HTTPS| Proxy + Browser -->|dev 可直连| App + Proxy -->|/api/*| App + Proxy -->|/*| App + + App -->|Prisma| PG + Jobs -->|Prisma| PG + App -->|文件读写| Storage + Jobs -->|文件读写| Storage + Jobs -->|API 调用| TencentOCR +``` + +**说明** +- `PostgreSQL` 版本口径:开发 compose 可为 15,试运行包建议 16(两者均可)。 +- `Proxy`(Nginx/Caddy)是可选项:试运行/上线建议配置(TLS、上传限制、限流),开发联调可跳过。 +- `App` 可通过 `SERVE_CLIENT_DIST=true` 托管 `client/dist`,实现同域 cookie 与 SPA fallback。 +- `Jobs` 现状与 `App` 同进程(node-cron);未来可拆分独立 Worker(见 To‑Be)。 + +### 1.2 部署视图(To‑Be / 可扩展但仍可单机) + +> 目标:在不改变“单机可跑”前提下,逐步具备“可横向扩展、可观测、可运维”的形态。 + +```mermaid +graph TB + subgraph Client["客户端"] + Browser["浏览器 (React SPA)"] + end + + subgraph Edge["边缘层 [To‑Be/未实现]"] + Proxy["Nginx/Caddy/WAF\nTLS/反代/限流/上传策略"] + end + + subgraph App["应用层"] + API["Fastify API (stateless)\n可选静态托管 client/dist"] + Worker["Job Worker [To‑Be/未实现]\nOCR / 委派回收 / 清理作业"] + Queue["队列/Redis [To‑Be/未实现]\nBullMQ/Streams(可选)"] + end + + subgraph Data["数据层"] + PG["PostgreSQL\nPgBouncer [To‑Be/未实现]"] + ObjectStorage["对象存储 [To‑Be/未实现]\nS3/OSS/MinIO"] + end + + subgraph Obs["可观测性 [To‑Be/未实现]"] + Logs["结构化日志聚合\nLoki/ELK"] + Metrics["metrics\nPrometheus"] + end + + subgraph External["外部服务"] + TencentOCR["腾讯云 OCR"] + end + + Browser --> Proxy + Proxy --> API + + API --> PG + Worker --> PG + + API --> ObjectStorage + Worker --> ObjectStorage + + Worker <--> Queue + Worker --> TencentOCR + + API --> Logs + Worker --> Logs + API --> Metrics + Worker --> Metrics +``` + +--- + +## 2. 核心业务流程(报销审批 / As‑Is) + +> 核心链路:**票据上传 →(异步)OCR → 绑定票据 → 提交报销 → 工作流审批 → 财务付款 → 票据归档**。 + +```mermaid +sequenceDiagram + participant E as 员工 + participant S as Server(Fastify) + participant J as OCR Job(node-cron) + participant O as 腾讯云OCR + participant WF as 工作流引擎 + participant M as 经理 + participant F as 财务 + + E->>S: 上传票据 (invoice_pool: pending_ocr/available) + S-->>E: 返回 invoiceId + status + + J->>S: 认领 pending_ocr (FOR UPDATE SKIP LOCKED + 原子回写锁) + J->>O: OCR 识别(可 mock/off) + O-->>J: 识别结果/错误 + J->>S: 更新 invoice_pool (available/ocr_failed) + + E->>S: GET /api/invoice-pool/:id (轮询/刷新) + S-->>E: 返回最新 status 与字段 + + E->>S: 绑定票据到报销单 (available -> bound) + E->>S: 提交报销 (draft -> reviewing) + S->>WF: 创建 workflow_instance + 首个 task + WF-->>M: 待办 task(assignee_id 或 assignee_role) + + M->>S: approve / return / reject / transfer + WF->>WF: 推进流程并写 workflow_history + + F->>S: pay (approved -> paid) + S->>S: bound 票据 -> archived (used_complete) + S->>S: 写 audit_logs(关键运营/安全动作) +``` + +**证据链边界(现状)** +- `workflow_history`:流程证据(针对某个 workflow_instance/task 的动作轨迹),用于还原审批过程。 +- `audit_logs`:合规/安全证据(登录、委派回收、用户管理等跨域操作),用于审计与风控追踪。 + +--- + +## 3. 数据模型(ER 图 / As‑Is) + +> 说明:为可读性仅列关键字段与关系;具体字段以 Prisma schema 为准。 + +### 3.1 组织 / 权限 / 会话 / 审计 + +```mermaid +erDiagram + Department ||--o{ Employee : contains + Department ||--o{ Department : children + + Employee ||--o{ EmployeeRole : has + Employee ||--o{ AuthSession : sessions + Employee ||--o{ PasswordResetCode : resets + Employee ||--o{ AuditLog : writes + Employee ||--o{ DelegationGrant : principal + Employee ||--o{ DelegationGrant : delegate + + Department { + uuid id PK + string name + uuid parent_id FK + } + + Employee { + uuid id PK + string employee_no UK + uuid department_id FK + uuid direct_manager_id FK + string status + int token_version + } + + EmployeeRole { + uuid employee_id PK,FK + string role_code PK + } + + AuthSession { + uuid id PK + uuid employee_id FK + string device_fingerprint + datetime expires_at + datetime revoked_at + } + + PasswordResetCode { + uuid id PK + uuid employee_id FK + datetime expires_at + datetime used_at + } + + DelegationGrant { + uuid id PK + uuid principal_employee_id FK + uuid delegate_employee_id FK + datetime valid_from + datetime valid_to + datetime revoked_at + } + + AuditLog { + uuid id PK + string request_id + uuid actor_id FK + string action + string target_type + uuid target_id + datetime created_at + } +``` + +### 3.2 报销与票据(含 OCR 缓存) + +```mermaid +erDiagram + Employee ||--o{ ReimbursementForm : owns + Employee ||--o{ ReimbursementForm : submits + Department ||--o{ ReimbursementForm : belongs + + ReimbursementForm ||--o{ ReimbursementItem : contains + ExpenseType ||--o{ ReimbursementItem : categorizes + + Employee ||--o{ InvoicePool : owns + Employee ||--o{ InvoicePool : uploads + ReimbursementForm ||--o{ InvoicePool : links + ReimbursementItem ||--o{ InvoicePool : linksItem + OcrResult ||--o{ InvoicePool : cachedFor + + ReimbursementForm { + uuid id PK + string form_number UK + uuid owner_id FK + uuid submitter_id FK + uuid department_id FK + string status + decimal total_amount + string payment_voucher_no + } + + ReimbursementItem { + uuid id PK + uuid reimbursement_id FK + uuid expense_type_id FK + decimal amount + string description + } + + ExpenseType { + uuid id PK + string code UK + string name + string category + } + + InvoicePool { + uuid id PK + uuid owner_employee_id FK + uuid uploaded_by_employee_id FK + string storage_path + string file_sha256 + string status + uuid linked_reimbursement_id FK + uuid linked_item_id FK + uuid ocr_result_id FK + } + + OcrResult { + uuid id PK + string file_sha256 + string ocr_provider + string ocr_type + json result_json + } +``` + +**关系备注(关键纠偏)** +- `InvoicePool.linked_reimbursement_id / linked_item_id / ocr_result_id` 均为 **可空字段**(票据可长期处于 available/pending_ocr 未绑定状态)。 + +### 3.3 工作流引擎(含转审证据) + +```mermaid +erDiagram + WorkflowDefinition ||--o{ WorkflowInstance : creates + ReimbursementForm ||--o{ WorkflowInstance : instances + WorkflowInstance ||--o{ WorkflowTask : tasks + WorkflowInstance ||--o{ WorkflowHistory : history + WorkflowTask ||--o{ WorkflowHistory : taskHistory + + Employee ||--o{ WorkflowTask : assigned + Employee ||--o{ WorkflowTask : completed + Employee ||--o{ WorkflowHistory : actions + + ReimbursementForm ||--o{ ApprovalTransfer : transfers + Employee ||--o{ ApprovalTransfer : from + Employee ||--o{ ApprovalTransfer : to + + WorkflowDefinition { + uuid id PK + string entity_type + int version + bool is_active + json definition_json + } + + WorkflowInstance { + uuid id PK + uuid workflow_id FK + string entity_type + uuid entity_id FK + uuid submitter_id FK + string current_status + string current_step_id + json context_data + } + + WorkflowTask { + uuid id PK + uuid instance_id FK + string step_id + string step_name + uuid assignee_id FK + string assignee_role + string status + string outcome + } + + WorkflowHistory { + uuid id PK + uuid instance_id FK + uuid task_id FK + uuid actor_id FK + string action + json payload + } + + ApprovalTransfer { + uuid id PK + uuid reimbursement_id FK + uuid from_approver_id FK + uuid to_approver_id FK + string reason + string status + } + + ApprovalRule { + uuid id PK + string name + decimal min_amount + decimal max_amount + string level1_role + string level2_role + string level3_role + bool enabled + int priority + } +``` + +**关系备注(关键纠偏)** +- `ReimbursementForm -> WorkflowInstance` 是 **0..n**(同一报销单可能多次提交产生多个实例)。 +- `WorkflowTask.assignee_id` 与 `assignee_role` 在语义上通常 **互斥**(实现允许为空/组合以支持候选/角色任务)。 +- `WorkflowHistory.task_id` 在实现中为 **可空字段**(例如非 task 维度的历史事件)。 +- `ApprovalRule` 是规则配置表:用于构建审批步骤,通常以 `workflow_history.payload` 等形式被引用(并非强制外键关联)。 + +### 3.4 单号序列(系统表) + +```mermaid +erDiagram + DocumentSequence { + string document_type PK + string prefix + string separator + int current_number + string reset_period + datetime updated_at + } +``` + +--- + +## 4. 状态机 + +### 4.1 报销单状态(As‑Is) + +```mermaid +stateDiagram-v2 + [*] --> draft: create + draft --> reviewing: submit + reviewing --> draft: return / withdraw + reviewing --> rejected: reject + reviewing --> approved: final approve + approved --> paid: pay + rejected --> draft: edit & resubmit + paid --> [*] +``` + +### 4.2 报销单状态(To‑Be/未实现) + +> 预留方向:补齐“作废/取消”语义,支持运营或系统终止(例如提交后发现异常、关账等)。 + +```mermaid +stateDiagram-v2 + [*] --> draft + draft --> reviewing: submit + reviewing --> cancelled: cancel [To‑Be] + approved --> cancelled: cancel [To‑Be] + cancelled --> [*] +``` + +### 4.3 票据池状态(As‑Is) + +```mermaid +stateDiagram-v2 + [*] --> pending_ocr: upload (OCR enabled) + [*] --> available: upload (OCR off/mock) + pending_ocr --> available: OCR success + pending_ocr --> ocr_failed: OCR failed / duplicate + available --> bound: bind + bound --> archived: pay (used_complete) +``` + +### 4.4 票据池状态(To‑Be/未实现) + +> 预留方向:增加“解绑、超时归档、人工纠错”能力(需要额外 API/作业/运营策略支撑)。 + +```mermaid +stateDiagram-v2 + bound --> available: unbind [To‑Be] + available --> archived: unused_timeout [To‑Be] + ocr_failed --> available: manual_fix [To‑Be] +``` + +--- + +## 5. 目录结构(简化视图) + +``` +hyb-oa/ +├── client/ # 前端 (React + Vite + Tailwind) +│ ├── src/ +│ │ ├── api/ # API 封装 +│ │ ├── auth/ # 会话与权限映射 +│ │ ├── components/ # UI 组件 +│ │ ├── layouts/ # 布局 +│ │ ├── pages/ # 页面 +│ │ ├── query/ # React Query 统一封装 +│ │ ├── router/ # 路由与守卫 +│ │ └── styles/ # Token/基础样式 +│ └── tests/e2e/ # Playwright E2E +│ +├── server/ # 后端 (Fastify + Prisma) +│ ├── src/ +│ │ ├── routes/ # API 路由 +│ │ ├── services/ # 业务逻辑 (OCR 等) +│ │ ├── jobs/ # 定时任务 (OCR/委派回收) +│ │ └── utils/ # 工具函数 +│ ├── prisma/ # 数据库 Schema & 迁移 +│ └── scripts/ # 回归脚本 +│ +├── deploy/trialrun/ # 试运行部署包 +├── docs/ # 文档 (需求/实施/架构) +├── issues/ # Issue CSV 执行合同 +└── scripts/ # 门禁/备份恢复脚本 +``` + +--- + +## 6. 技术栈速览(As‑Is) + +| 层级 | 技术 | 说明 | +|---|---|---| +| 前端 | React 18 + Vite + Tailwind | SPA;配套 React Query、Radix primitives、Playwright E2E | +| 后端 | Fastify + Prisma | Node.js;JWT + refresh cookie;RBAC 权限 | +| 数据库 | PostgreSQL 15+(试运行建议 16) | Prisma ORM;关键队列/作业依赖 SQL 锁语义 | +| OCR | 腾讯云(可 mock/off) | 异步作业 + 并发锁;失败降级不阻塞上传 | +| 部署 | Docker Compose | 单机试运行;可选反代同域 | +| CI | GitHub Actions | 最小门禁 + 回归 + E2E(按开关分层执行) | + +--- + +## 7. 相关文档 + +- [需求规格说明书](./requirements-specification.md) - 完整的 P0 需求定义 +- [实施计划](./implementation_plan.md) - 里程碑与验收标准 +- [技术栈报告](./tech_stack_report.md) - 选型依据 + +--- + +## 8. 对标与演进(不造轮子) + +> 本节只列“可借鉴能力清单”,不代表已实现。 + +### 8.1 工作流(Camunda / Flowable / Activiti 思想) +- **Candidate / Claim / Unclaim**:候选人/候选组与认领机制(减少“角色任务抢占冲突”,提升可控性)。 +- **SLA / Due date / Escalation**:任务期限与超时升级(试运行后期高频诉求)。 +- **History & Audit 分层**:流程历史(workflow_history)与合规审计(audit_logs)职责边界清晰。 + +### 8.2 费用报销(Odoo / ERPNext 的单据分层) +- **业务单据 vs 付款凭证/对账**:先保持 `payment_voucher_no/payment_date/channel` 的轻量字段;后续可演进独立 `payment_transactions` 与对账状态,不影响现有 API。 + +### 8.3 合规审计(企业级通用实践) +- **统一 request_id/trace_id**:贯穿 API 与作业日志(便于追踪、复盘、举证)。 +- **最小可观测性**:ready/metrics/log 聚合(先最小可用,后续再引入 OpenTelemetry 等体系化方案)。