文件
hyb-oa/docs/requirements-specification.md
T

91 KiB
原始文件 Blame 文件历史

海音达 OA 系统 - 需求规格说明书

版本:1.4
日期:2026-02-06
范围:报销系统及其依赖模块(员工/权限/工作流/附件)

相关文档:


1. 项目概述

1.1 背景

海音达科技需要一套独立的 OA 系统,用于内部办公管理。Phase 1 聚焦报销系统及其依赖(员工管理、权限、工作流、附件/发票)。

1.2 目标(Phase 1)

  • 实现完整的费用报销流程:草稿 → 提交 → 审批(工作流任务)→ 通过 → 付款标记 → 完成
  • 审批规则可配置(金额区间 → 多级审批角色),P0 预置 3 条默认规则
  • 审批引擎采用通用 4 表式工作流(Definition/Instance/Task/History),后续扩展到请假/采购无需重构
  • 支持行政代提交(需授权)、发票附件上传、双策略判重、离线打款标记
  • 响应式 Web(PC/移动端)

1.3 技术栈(定案)

层级 选型
前端(OA) React + Vite + Tailwind
后端(OA API) Fastify + Prisma
数据库 PostgreSQL

1.4 复用资产(来自 shenzhou-business-suite)

目的:减少踩坑与重复造轮子。本文档的表结构/枚举/约束均以该项目的已验证实现为基线,再按 hyb-oa 目标做裁剪与补齐。

资产 来源路径(参考) 在 hyb-oa 的复用方式
工作流 4 表结构 + approval_rules/approval_transfers/document_sequences shenzhou-business-suite/supabase/schema/70_workflow/01_workflow_02_tables.sql 转为 Prisma schema;服务端实现工作流事务与权限校验
工作流类型定义(assignee_rule 等) shenzhou-business-suite/src/types/workflow.ts 作为工作流 JSON 配置与服务层契约参考
权限码体系 shenzhou-business-suite/src/types/permissions.ts 复用命名与粒度;补齐行政代提交权限码
报销表结构参考(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 的约束与鉴权规则

2. 用户角色与权限模型

2.1 角色(role_code)与职责边界(P0)

角色(展示) role_code 职责/范围
普通员工 employee 创建/提交报销,查看/编辑本人草稿与记录
部门经理 manager 审批直属下属报销(工作流任务);可查看直属下属报销(P0:不递归)
总经理 ceo 审批大额报销(工作流任务),查看全公司数据(只读)
财务专员 finance 财务复核(工作流任务)、付款标记、票据管理(全量)
财务主管 finance_mgr 大额复核、发票判重例外、审批规则维护(P1 UI)
行政 administration 代提交(授权由 principal 本人或 hr/admin 维护;不含审批/付款)
人事 hr 员工与组织维护(不含财务/审批)
系统管理员 admin 系统设置、用户、审计(可授予 *)

说明:一个员工可同时拥有多个 role_code(例如 finance + admin),权限按并集生效。

2.2 权限码(当前实现)

命名规则:domain.action[_scope]。后端接口必须声明所需权限码并统一校验;前端仅用于菜单与按钮显隐,不作为最终授权依据。

2.2.1 报销 / 票据 / 系统基础权限

permission 说明
expense.create 创建报销单(草稿)
expense.view_own 查看自己的报销单
expense.view_all 查看所有报销单(数据范围由角色决定)
expense.edit_own 编辑自己的报销单(仅草稿/驳回)
expense.delete_own 删除自己的报销单(仅草稿)
expense.submit 提交报销单(发起工作流)
expense.withdraw 撤回报销单(将流程取消并回到草稿)
expense.approve 审批报销单(处理工作流任务)
expense.transfer 转审(转交当前待办任务)
expense.finance.pay 付款标记(approved -> paid)
expense.finance.override_duplicate 发票重复例外(需填原单号 + 原因)
expense.proxy_submit 行政代提交
expense.proxy_manage 代提交授权管理(principal 自管;hr/admin 可代维护)
invoice.upload 上传发票 / 附件
invoice.view_own 查看自己的发票 / 附件
invoice.manage 票据中心管理(可查看全量)
invoice.verify 票据验真与重试
system.settings 系统设置(审批规则 / 文档序列 / 通知渠道等配置入口)
system.users 用户与组织管理
system.users_sensitive 查看或维护员工敏感字段
system.audit.view 审计日志查看
report.dashboard.view 查看经营 / 审批看板
budget.view 查看预算池与预算余额
budget.manage 维护预算池与预算规则
masterdata.view 查看法体 / 成本中心 / 供应商等共享主数据
masterdata.manage 维护共享主数据
notification.channel.manage 通知渠道与模板配置
integration.manage 集成轨道管理、补发、重试、主数据同步
ai.assist 调用只读 AI 建议能力
* 超级权限(仅 admin 可授予)

2.2.2 已扩展业务域权限(P15-P22 当前实现)

  • leave.*:leave.create、leave.view_own、leave.view_all、leave.submit、leave.approve、leave.manage
  • procurement.*:procurement.create、procurement.view_own、procurement.view_all、procurement.submit、procurement.approve、procurement.manage
  • travel.*:travel.create、travel.view_own、travel.view_all、travel.submit、travel.approve、travel.manage、travel.payment.manage
  • asset.*:asset.view_own、asset.view_all、asset.manage、asset.transfer、asset.inventory

2.3 角色默认权限映射(当前实现)

默认角色权限以 server/src/constants/permissions.js 为单一事实源;下表仅描述当前主干中的默认映射与职责边界。

  • employee:报销自助权限 + 请假自助权限 + 采购自助权限 + 差旅自助权限 + asset.view_own
  • manager:在 employee 基础上 + expense.approve/transfer/view_all + leave.approve + procurement.approve/view_all + travel.approve/view_all + asset.view_all + report.dashboard.view + budget.view/manage
  • ceo:在经理审批视角基础上 + leave.view_all + masterdata.view + 公司级只读视角
  • finance:invoice.manage/verify + expense.approve/finance.pay/view_all + procurement.view_all + travel.view_all/payment.manage + asset.view_all + report.dashboard.view + budget.view + masterdata.view
  • finance_mgr:在 finance 基础上 + expense.finance.override_duplicate + procurement.manage + travel.manage/payment.manage + asset.manage/inventory + integration.manage + system.settings + system.audit.view + budget.manage + masterdata.manage
  • administration:expense.proxy_submit + 员工基础自助权限 + procurement.view_all/manage + travel.view_all/manage/payment.manage + asset.view_all/manage/transfer
  • hr:请假自助权限 + leave.view_all/manage + system.users/system.users_sensitive + expense.view_all + report.dashboard.view
  • admin:*

2.4 权限校验原则(实现约束)

  • 后端为单一事实源:所有关键动作必须在服务端校验权限码与数据范围;前端只负责界面显隐。
  • *_own 语义:
    • 对报销单(reimbursement),own 定义为:owner_id = me OR submitter_id = me(代提交场景)
    • 对采购单(purchase_request)与差旅申请(travel_request),own 定义为:applicant/applicant_employee_id = me OR submitter_id = me
    • 对请假单(leave_request),own 定义为:owner_id = me
    • 对资产卡片(asset_card),own 定义为:assignee_employee_id = me
  • view_all / manage 不是前端列表参数,而是服务端数据范围收敛规则:许多列表 / 详情接口只做 authenticate,实际权限在 handler 内按 view_own/view_all/manage 做二次判断。
  • 工作流审批范围由 workflow_tasks.assignee_id/assignee_role 决定:只有当前待办处理人 / 处理角色可执行 approve/reject/return;当前前端仅对报销任务暴露 transfer 入口。
  • travel.payment.manage 控制差旅支付运营面板(授权、交易、mark-paid、冻结建议、对账确认);integration.manage 控制主数据同步、ERP/IM 轨道查看增强、重试与补发。
  • 所有例外操作(判重例外、预算释放、主数据重试、集成补发等)都必须进入 audit_logs,并保留最小必要上下文。

3. 组织架构与部门模型

3.1 组织架构示例

总经理
├── 部门经理 A
│   ├── 员工 A1
│   └── 员工 A2
└── 部门经理 B
    ├── 员工 B1
    └── 员工 B2
  • 普通员工的直属上级 = 部门经理
  • 部门经理的直属上级 = 总经理

3.2 部门模型(P0)

  • 支持多级部门(parent_id)。
  • 部门归档约束(禁止硬删除):
    • 有子部门:禁止归档
    • 有在职员工:禁止归档
    • 归档后不可再分配新员工;组织树默认隐藏归档部门(可筛选查看)

4. 功能需求

4.1 员工管理

4.1.1 员工信息字段(核心)

字段 类型 必填 说明
工号 String ✅ 唯一标识
姓名 String ✅
手机号 String ✅ 登录标识(identifier 之一)
邮箱 String ✅ 登录标识(identifier 之一)/通知
部门 FK ✅ 关联部门表
职位 String ✅
直属上级 direct_manager_id FK ✅(顶层可空) 审批链与 department_manager 解析依赖;顶层角色(如 CEO)可为空
入职日期 Date ✅
状态 Enum ✅ 在职/离职
角色集合 M2M ✅ 通过 employee_roles(employee_id, role_code) 维护;权限按并集生效
银行卡号 String ✅ 敏感字段:加密存储/脱敏展示(见第 12 章)
开户行 String ✅
身份证号 String ❌ 敏感字段:加密存储/脱敏展示(见第 12 章)

4.1.2 功能点

  • 员工 CRUD(system.users,通常由 hr/admin 执行)
  • 组织架构树展示(含归档部门过滤)
  • 离职员工归档:离职后禁止登录并强制下线(见第 11 章 tokenVersion)

4.2 报销系统

4.2.1 报销单(Reimbursement Form)

字段 说明
报销单号 form_number 由单号规则生成(见第 8 章)
申请人 submitter_id 当前登录用户或代提交人
实际报销人 owner_id 费用归属人
部门 department_id 默认取 owner_id 所属部门
费用明细 items[] 多条,见下表
总金额 total_amount 自动汇总
报销日期 reimbursement_date 业务发生日期(整单)
状态 status 固定 6 状态(见 4.3.3)
备注 description 可选

4.2.2 报销明细(Reimbursement Item)

字段 说明
费用类型 expense_type_id 关联 expense_types(见第 9 章)
发生日期 expense_date 日期
金额 amount 数值
说明 description 必填(用于审计与复核)

附件(发票池条目)与明细的关联由 invoice_pool.linked_item_id 实现(可选),详见第 10 章。

费用类型不再是“21 种硬编码”,改为动态 expense_types;系统默认启用 37 项(见附录 A)。

4.2.3 代提交(Proxy Submit,P0)

  • 允许角色:administration(需权限 expense.proxy_submit)
  • 约束:代提交必须存在有效授权(由 delegation_grants 控制;授权由本人或 hr/admin 维护),否则拒绝提交/创建
  • 记录要求:
    • 报销单必须同时记录 submitter_id(代提交人)与 owner_id(实际报销人)
    • 审批与通知以 owner_id 为主,同时抄送 submitter_id

4.2.4 代提交授权(Delegation Grants,P0)

用于控制“行政可以为哪些员工代提交”,避免权限过大。P0 默认“创建即生效”,无需被授权人二次确认。

delegation_grants 表(建议字段):

字段 类型 说明
id UUID 主键
principal_employee_id UUID 被代提交人(员工)
delegate_employee_id UUID 代办人(行政)
scope String reimbursement_submit | all(P0;P1+ 可扩展其他 scope)
valid_from/valid_to DateTime 生效/失效时间(valid_to 可空表示长期)
revoked_at DateTime 撤销时间(可空)
created_at/created_by DateTime/UUID 创建时间/操作者

授权规则(P0 定案):

  • delegate_employee_id 必须包含 administration 角色
  • 授权生效条件:revoked_at is null 且当前时间在 [valid_from, valid_to] 范围内
  • 授权管理权限:
    • 员工本人可对“授予给行政的授权”执行创建/撤销(UI:个人设置 → 授权管理)
    • hr/admin 可代维护(用于补录/紧急处理),所有变更必须进入 audit_logs

4.3 审批与流程(业务视角)

4.3.1 默认审批链(P0 预置规则)

审批链不再硬编码在业务代码里,而是由 approval_rules 匹配得到(见第 6 章)。P0 预置 3 条规则:

  • 小额:[0, 500) → manager → finance
  • 中额:[500, 5000) → manager → finance_mgr
  • 大额:[5000, +∞) → manager → ceo → finance_mgr

4.3.2 审批操作(P0)

操作 触发者 结果
提交 submit 申请人/代提交人 生成单号,发起工作流实例与首个任务
通过 approve 当前任务处理人 完成当前任务并推进到下一任务/流程结束
驳回 reject 当前任务处理人 流程终止(instance=rejected),报销单状态变为 rejected(可编辑后再次 submit 进入新一轮)
退回 return 当前任务处理人 终止当前流程实例(instance=terminated),报销单回到 draft(可修改后重新 submit)
撤回 withdraw 申请人 取消当前流程实例(instance=cancelled),报销单回到 draft(仅在审批人未处理前)
转审 transfer 当前任务处理人 将当前任务转交他人;需记录原因(≥10 字)
付款标记 pay 财务 流程通过后,将 approved 标记为 paid

4.3.3 报销单数据状态机(固定 6 状态)

“待部门审批/待财务复核”等是任务节点文案,由工作流当前待办任务决定,不作为数据状态写入 DB。

draft
  └─ submit → reviewing
reviewing
  ├─ approve(最终) → approved
  ├─ reject → rejected
  ├─ return → draft
  └─ withdraw → draft
approved
  └─ pay → paid
paid (终态)
rejected (可由申请人编辑后再 submit 进入新一轮)
cancelled (作废/终止,终态)

4.3.4 return vs reject vs withdraw(P0 定案)

  • return(退回):用于“需要申请人修改后再提交”的场景。动作发生后:
    • 当前 workflow_instances 必须结束并标记为 terminated
    • 报销单状态回到 draft;申请人修改后需再次 submit 才会进入新一轮审批
    • 所有未完成任务必须被取消(workflow_tasks.status=cancelled),并写入 workflow_history(action=return)
  • reject(驳回):用于“本轮不通过”的场景。动作发生后:
    • 当前 workflow_instances 必须结束并标记为 rejected
    • 报销单状态变为 rejected(保留驳回意见);申请人可编辑后再次 submit 进入新一轮审批
    • 写入 workflow_history(action=reject)
  • withdraw(撤回):由申请人发起的“主动取消”。动作发生后:
    • 当前 workflow_instances 必须结束并标记为 cancelled
    • 报销单状态回到 draft
    • 约束:仅允许在审批人尚未处理前撤回(即当前实例内不存在已完成的审批任务)
    • 写入 workflow_history(action=withdraw)

4.4 发票池(Invoice Pool)与 OCR(P0)

目的:把“票据文件 + OCR 结果 + 发票元数据 + 绑定关系 + 生命周期状态”从报销单附件中解耦出来,形成可复用、可审计、可判重的单一事实源。

4.4.1 模块定位(P0)

  • invoice_pool 是票据/附件的单一事实源(含发票、行程单、火车票、出租车票等图片/PDF)。
  • 报销单不再直接管理“附件文件”,而是绑定 invoice_pool 条目(见 4.4.6)。
  • 判重与例外属于“使用/报销”层面的合规约束:P0 允许先入池,在绑定/提交阶段做硬校验(见 4.4.7)。

4.4.2 生命周期状态机(5 态,P0)

invoice_pool.status:

  • pending_ocr:已上传,OCR 未完成(或排队中)
  • ocr_failed:OCR 失败(可重试,或人工修正后转为 available)
  • available:可绑定(OCR 成功或人工确认/修正后)
  • bound:已绑定到某报销单(1:1)
  • archived:已归档(不可再绑定;P0 不提供恢复)

不变量(P0,必须由 DB + 服务端共同保证):

  • status = bound ⇒ linked_reimbursement_id is not null
  • status in (pending_ocr, ocr_failed, available) ⇒ linked_reimbursement_id is null
  • status = archived ⇒ 不允许再执行 bind
  • status = archived AND archived_reason = used_complete ⇒ linked_reimbursement_id is not null(用于追溯)
  • status = archived AND archived_reason in (unused_timeout, manual) ⇒ linked_reimbursement_id is null

4.4.3 归档原因(用于判重口径,P0)

invoice_pool.archived_reason(仅当 status=archived 时有意义):

  • used_complete:已被报销使用且流程完成后归档
  • unused_timeout:90 天未绑定自动归档(从未被使用)
  • manual:人工归档(清理/隐藏;从未被使用或不再使用)

判重口径(P0 定案):

  • 只有 status=bound 与 status=archived AND archived_reason=used_complete 的记录参与“全局判重历史”。
  • unused_timeout/manual 的归档记录不参与全局判重(避免“未使用的旧票据”阻塞新票据入库)。

4.4.4 90 天未绑定自动归档(P0)

  • 目标:控制“孤儿发票/票据”堆积。
  • 规则:对 status in (available, ocr_failed) 且未绑定的条目,超过 90 天自动转 archived 且 archived_reason=unused_timeout。
  • pending_ocr 超时策略(建议):先转 ocr_failed 再进入自动归档规则(超时阈值由实现配置)。

4.4.5 OCR(腾讯云,P0)

  • OCR 提供方:腾讯云(参考既有项目实现)。
  • OCR 结果必须持久化(用于复核/搜索/审计/复用);P0 不落库保存腾讯云 rawResponse(仅日志)。
  • ocr_type(P0 枚举):
    • vat(增值税发票;覆盖专票/普票/数电票,细分类型体现在标准化结果中)
    • train(火车票)
    • flight(机票行程单)
    • taxi(出租车票)
    • general(其他通用票据,默认值)
  • 交互策略:用户可选 ocr_type;默认 general。
  • 提交不阻塞:OCR 失败/未完成时,允许用户人工补齐关键字段后继续(UI 提示即可,不让流程卡死)。
  • 发票验真(税局/验真接口):P2,P0/P1 不做。

4.4.6 绑定/解绑规则(P0)

  • 绑定关系:一张 invoice_pool 条目只能绑定一个报销单(1:1)。
  • 绑定前置条件:status=available 且未绑定。
  • 绑定后:status=bound,写入 linked_reimbursement_id(可选 linked_item_id)。
  • 解绑后:清空 linked_reimbursement_id/linked_item_id,回到 available(P0 不自动恢复到 ocr_failed)。

与报销流程的联动(P0 定案):

  • 报销草稿被删除(仅 draft):必须自动解绑其绑定的发票池条目并回到 available。
  • 报销单进入 paid(完成节点):其绑定的发票池条目自动归档为 archived 且 archived_reason=used_complete。

4.4.7 判重与例外(P0)

  • 全局双策略判重(公司级):
    1. invoice_code + invoice_number(齐全时)
    2. file_sha256
  • 生效点:不阻塞“入池上传”;在 bind/submit 阶段做硬校验。
  • 例外(允许重复):仅 finance_mgr,必填原单号 + 原因(≥10 字),并写入审计;当原单处于 approved 或 paid 时禁止例外(见错误码 VALID_004)。

存储与签名 URL 访问规则见第 10 章;接口契约见第 13 章。


4.5 支付管理

  • 打款方式:线下打款,系统仅做“付款标记”
  • 付款状态:approved → paid
  • 付款记录:付款时间、操作人、银行流水号(可选)、备注

4.6 通知系统(P0)

场景 通知方式
待审批提醒 站内 + 邮件(P1:企微/钉钉)
审批结果 站内 + 邮件
付款完成 站内 + 邮件

4.7 请假管理

4.7.1 范围与边界

  • 当前请假域以 LeaveType、LeavePolicy、LeaveLedger、LeaveRequest 四层模型为核心,支持余额查询、列表/详情、草稿创建、提交、审批与撤回闭环。
  • 当前实现支持半天粒度(am/pm)请假、同人同时间段冲突阻断、余额强设闸、附件规则校验与审批后账本消耗。
  • 假期类型与年度额度由 HR 维护;法定节假日、调休日等共享主数据能力归入主数据域,不在本节单独展开日历算法。

4.7.2 角色与权限

  • employee:leave.create/view_own/submit,可发起、查看、提交和撤回本人请假。
  • manager、ceo:leave.approve,通过通用 workflow 待办处理请假审批。
  • hr:leave.view_all/manage,可维护假期类型、年度配额与全量查看请假单。

4.7.3 状态机 / 工作流语义

  • 请假单状态:draft -> reviewing -> approved/rejected;申请人 withdraw 或审批人 return 均回到 draft。
  • submit:创建 workflow 实例与首条待办,同时写入 LeaveLedger(entry_type=reserve) 冻结额度。
  • approved:消费冻结额度(consume);rejected、return、withdraw:释放冻结额度(release)。
  • 当前 workflow 复用通用 workflow_definitions / instances / tasks / history 四表,entity_type=leave,不引入独立请假审批引擎。

4.7.4 核心数据对象

对象 作用
LeaveType 假别定义,含是否必传附件、附件 MIME 白名单、排序等配置
LeavePolicy 假别对应年度额度与启用开关
LeaveRequest 请假申请主单,包含日期范围、半天粒度、附件信息与状态
LeaveLedger 假期余额账本,记录 grant/reserve/consume/release/adjust 五类流水

4.7.5 显式不做项

  • 不在请假域内单独实现复杂排班、倒班、加班换休或跨法域假期累计规则。
  • 不单独开放“请假转审”专用 UI;当前仅复用通用 workflow 的 approve/reject/return 能力。
  • 不把日历主数据、法定节假日策略和跨年结转算法写死到请假申请 API 中。

4.8 采购管理

4.8.1 范围与边界

  • 当前采购域覆盖 PurchaseRequest 草稿 / 提交 / 审批 / 撤回主流程,以及 PurchaseOrderArchive 的 PO / 合同归档闭环。
  • 采购二期已补齐预算 reserve/confirm/release 语义,并允许在 approved 采购单详情内完成归档留痕。
  • 采购域当前不进入收货、入库、三单匹配、供应商门户或发票核销。

4.8.2 角色与权限

  • employee:procurement.create/view_own/submit,可创建和提交本人的采购申请。
  • manager、ceo:procurement.approve,处理采购审批待办;具备 procurement.view_all 的角色可查看全量。
  • administration、finance_mgr:当前默认具备 procurement.manage,可执行归档留痕等运营动作。
  • integration.manage:用于查看或补发采购 ERP 轨道,不参与采购主业务编辑。

4.8.3 状态机 / 工作流语义

  • 采购申请状态:draft -> reviewing -> approved/rejected/cancelled。
  • submit:校验法体、成本中心、供应商、预算池等必填项,创建 workflow 并写预算预留。
  • approved:确认预算占用;rejected、return、withdraw:释放预算预留并回到草稿或终止实例。
  • PO / 合同归档仅允许在 approved 状态下执行,且归档不触发新的审批实例。

4.8.4 核心数据对象

对象 作用
PurchaseRequest 采购申请主单,承载申请人、法体、成本中心、供应商、预算池、总金额与状态
PurchaseRequestItem 采购明细,记录规格、数量、单价、金额与资产候选标记
PurchaseAttachment 采购附件,支持上传、删除、签名 URL 预览
PurchaseOrderArchive 已审批采购单的 PO / 合同归档记录

4.8.5 显式不做项

  • 不实现收货、入库、应付、发票核销、三单匹配或供应商协同门户。
  • 不把采购归档拆成独立后台模块;当前固定在采购详情内完成。
  • 不在核心采购表注入 ERP 私有字段;ERP 外部编号统一走集成层 binding 维护。

4.9 资产管理

4.9.1 范围与边界

  • 当前资产域覆盖分类、卡片、领用归还、人员 / 位置调拨、盘点任务与资产 ERP 摘要轨道。
  • 资产卡片可关联采购来源(approved 采购单),并维护法体、成本中心、位置、领用人、状态等台账信息。
  • 资产域当前不引入独立审批流;所有领用、归还、调拨、盘点都属于受控运营动作。

4.9.2 角色与权限

  • employee:asset.view_own,查看本人名下资产。
  • manager、ceo、finance:通常具备 asset.view_all,查看全量资产台账。
  • administration、finance_mgr:通常具备 asset.manage / asset.transfer,负责建卡、变更、领用归还与调拨。
  • asset.inventory:专用于盘点任务创建、启动、录入与完成;integration.manage 负责资产 ERP 轨道重试。

4.9.3 状态机 / 工作流语义

  • 资产卡片状态:in_stock、in_use、under_repair、retired、archived;状态变化由运营动作直接驱动,不经审批流。
  • 领用与归还通过 AssetAssignment 维护当前持有人;调拨通过 AssetTransfer 维护人员 / 位置变更历史。
  • 盘点任务状态:draft -> counting -> completed;盘点明细以 inventory_check_id + asset_id 唯一键幂等 upsert。
  • 资产 ERP 一期只在建卡和财务维度关键变更时触发摘要出站;领用、归还、位置调拨不触发 ERP 导出。

4.9.4 核心数据对象

对象 作用
AssetCategory 资产分类,绑定默认文档分类并控制是否启用
AssetCard 资产主卡片,记录编号、法体、成本中心、位置、领用人、状态等台账字段
AssetAssignment 资产领用 / 归还流水,维护当前 open assignment
AssetTransfer 资产人员 / 位置调拨历史
InventoryCheck / InventoryCheckItem 盘点任务与盘点结果明细

4.9.5 显式不做项

  • 不做折旧、净值、总账过账、会计科目自动分录等资产财务深化能力。
  • 不引入资产专属 workflow 审批实例;资产流转保持受控操作模型。
  • 不做原生扫码壳或仓储 WMS 深度集成;移动扫码与盘点增强由 PWA 路线承接。

4.10 差旅管理

4.10.1 范围与边界

  • 当前差旅域按三阶段交付:TRAVEL-1 申请与行程入站、TRAVEL-2 对公支付与发票绑定、TRAVEL-3 对账批次与 ERP 摘要桥。
  • 差旅申请主单承载出差目的、法体、成本中心、预算池、起止日期与行程段;支付域承接订单导入、企业卡授权 / 交易导入、mark-paid 与发票绑定;对账域承接批次快照、异常确认与 ERP 摘要桥接。
  • 当前差旅域不接真实商旅供应商 API,也不自动改写订单、申请或预算原值。

4.10.2 角色与权限

  • employee:travel.create/view_own/submit,创建和提交差旅申请。
  • manager、ceo:travel.approve,处理差旅审批待办;具备 travel.view_all 的角色可查看全量。
  • administration、finance_mgr:可持有 travel.manage 与 travel.payment.manage,用于支付运营、异常处理和对账确认。
  • integration.manage:负责 ERP 轨道、requeue 与主数据同步控制,不参与差旅主单编辑。

4.10.3 状态机 / 工作流语义

  • 差旅申请状态:draft -> reviewing -> approved/rejected;return 与 withdraw 均回到 draft。
  • submit:创建 workflow,并写预算预留;approved:确认预算;rejected / return / withdraw:释放预算。
  • TravelOrder 当前采用运行时字符串状态(如 imported/confirmed/paid)承载支付轨道,不作为独立公共枚举对外承诺;mark-paid 会写入付款时间、渠道、操作人并触发订单支付闭环。
  • TravelReconciliationBatch 状态固定为 draft/reconciled;异常明细必须人工 confirm 或 reopen,finalize 后只允许新建下一批,不回刷历史批次。

4.10.4 核心数据对象

对象 作用
TravelRequest 差旅申请主单,含申请人、法体、成本中心、预算池、目的与预计金额
TravelRequestSegment 行程段明细,含往返 / 中转角色、交通方式、起讫城市与时间
TravelOrder 导入或运营维护的差旅订单,承载支付确认与发票绑定
CardAuthorization / CardTransaction 企业卡授权与交易轨道,用于 authorize/settle/reverse 预算语义与支付明细
TravelReconciliationBatch / TravelReconciliationItem 差旅对账批次与异常明细快照

4.10.5 显式不做项

  • 不接真实商旅供应商出票、改签、退票、拒付、退款回调或银行清结算。
  • 不把 card_authorization / card_transaction 导入事实提升为长期稳定公共业务事件名。
  • 不做自动修账、自动改申请、自动冲销预算;对账只做差异发现与人工接受。

4.11 集成与外部适配

4.11.1 范围与边界

  • 当前集成层只通过边界适配核心域,不向报销、采购、资产、差旅等事务主表注入 ERP / IM 私有字段。
  • 已实现范围包括:共享主数据同步、IM-1 通知渠道、ERP-1 报销付款凭证切片、ERP-2 采购 commitment/archive、ERP-3 资产摘要、TRAVEL-3 批次级 erp.travel.export。
  • docs/integration-boundaries.md 与 docs/domain-events.md 是本章的边界与事件语义事实源;本规格只吸收已实现部分。

4.11.2 角色与权限

  • integration.manage:主数据同步、ERP / IM 轨道查看增强、补发、重试、手工触发。
  • 业务可读角色可以查看与自身实体相关的轨道摘要,但外部编号、重试与补发动作继续由 integration.manage 控制。
  • notification.channel.manage:维护通知渠道与模板;当前默认由 finance_mgr 持有。

4.11.3 状态机 / 工作流语义

  • 集成统一采用“业务事务提交成功 -> Outbox 异步派发 -> Binding / Track 回写”的交付模型;外部失败不回滚业务主事务。
  • IntegrationOutbox 负责 pending/processed/dead-letter 等投递状态、重试窗口与 dedupe_key 幂等;IntegrationBinding 负责本地实体与外部编号映射;IntegrationSyncState 负责主数据资源级同步状态。
  • 公开业务事件名保持最小集;erp.travel.export、erp.journal.export 等属于集成事件,不等同于新的业务领域事件名。
  • 当前后置 backlog 固定包括 IM-2/IM-3/IM-4、P22+VEH-1、P22+ASSET-FIN-1,不进入本轮已实现范围。

4.11.4 核心数据对象

对象 作用
IntegrationOutbox 统一出站事件队列,负责重试、死信与去重
IntegrationBinding provider / entity / localId / externalId 绑定关系
IntegrationSyncState 主数据同步状态、上次结果与错误摘要
NotificationChannel IM / 邮件等通知渠道配置与模板

4.11.5 显式不做项

  • 不直接在核心事务表写入第三方私有字段或回调状态列。
  • 不在本轮实现 IM-2/3/4、车辆管理、资产财务深化,也不把这些方向提前写入已完成事实源。
  • 不把 AI、MCP、PWA、生产化治理并入本章主体;这些能力继续由边界文档和运维文档承载。

5. 工作流引擎(Workflow,P0 采用)

采用 4 表式通用工作流,审批任务由 workflow_tasks 表驱动。P0 仅用线性审批链,但表结构支持后续扩展为条件分支/动作节点。

5.1 表结构(核心四表)

5.1.1 workflow_definitions(流程定义)

字段 类型 说明
id UUID 主键
name String 流程名称(如“报销 / 请假 / 采购 / 差旅审批流程”)
entity_type String 实体类型(如 reimbursement / leave / purchase_request / travel_request)
definition_json JSON 流程配置(见 5.2)
version Int 版本号(递增)
is_active Bool 是否启用

5.1.2 workflow_instances(流程实例)

字段 类型 说明
id UUID 主键
workflow_id UUID 关联 workflow_definitions
entity_type String 实体类型(如 reimbursement / leave / purchase_request / travel_request)
entity_id UUID 业务实体 ID(报销单 ID)
submitter_id UUID 发起人(提交者)
current_status String pending/approved/rejected/cancelled/terminated
current_step_id String 当前步骤 ID(与任务 step_id 对齐)
context_data JSON 实体快照(必须包含 owner_id、department_id;如 total_amount、命中 rule_id、审批链等)
started_at/completed_at DateTime 开始/结束时间

5.1.3 workflow_tasks(审批任务)

字段 类型 说明
id UUID 主键
instance_id UUID 关联 workflow_instances
step_id/step_name String 步骤标识与名称
assignee_id UUID 指派到具体人(可空)
assignee_role String 指派到角色(可空)
status String pending/completed/rejected/cancelled/skipped
outcome String approved/rejected/returned/start/transfer/skipped
comment String 审批意见/原因
due_date DateTime 截止时间(Phase 2 可启用超时)
created_at/completed_at DateTime 创建/完成时间
completed_by UUID 完成人

5.1.4 workflow_history(流程历史)

字段 类型 说明
id UUID 主键
instance_id UUID 关联实例(必填)
task_id UUID 关联任务(可空)
actor_id UUID 操作人(必填)
action String 动作(approve/reject/return/transfer/start/withdraw 等)
comment String 说明
payload JSON 扩展信息(如转审 from/to、判重例外引用原单号等)
created_at DateTime 时间戳(高精度)

5.2 流程定义 JSON 规范(WorkflowConfig)

参考 shenzhou-business-suite/src/types/workflow.ts。

{
  "steps": [
    {
      "id": "dept_manager",
      "name": "部门经理审批",
      "type": "approval",
      "assignee_rule": { "type": "department_manager" }
    },
    {
      "id": "ceo",
      "name": "总经理审批",
      "type": "approval",
      "assignee_rule": { "type": "role", "value": "ceo" }
    },
    {
      "id": "finance",
      "name": "财务复核",
      "type": "approval",
      "assignee_rule": { "type": "role", "value": "finance" }
    },
    {
      "id": "finance_mgr",
      "name": "财务主管复核",
      "type": "approval",
      "assignee_rule": { "type": "role", "value": "finance_mgr" }
    }
  ]
}

5.3 指派规则(assignee_rule)解析(P0)

支持类型:

  • specific_user:指派到指定用户 ID
  • role:指派到指定 role_code
  • department_manager:对报销 / 请假 / 采购 / 差旅四类审批实体,统一基于 owner_id 或 applicant 映射出的直属上级链向上寻找经理(最多 10 层;若经理=本人则继续向上;找不到则该步骤可被跳过并写入 history)
  • submitter_manager:基于 submitter_id 的直属上级链向上寻找经理(为未来其他流程预留;报销流程默认不使用该规则)

5.4 一致性与并发要求(实现约束)

  • 任务处理必须加锁:完成 workflow_tasks 时必须对该任务行做行级锁(防止重复审批)。
  • 单实例串行:同一个 workflow_instances 在任意时刻最多只有 1 条 pending 任务。
  • 幂等提交:同一 entity_type+entity_id 若存在未结束实例(非 approved/rejected/cancelled/terminated),再次提交应返回同一实例或拒绝重复提交。
  • 状态同步:
    • 创建首个任务后,报销单状态应进入 reviewing
    • 实例 approved → 报销单状态 approved
    • 实例 rejected → 报销单状态 rejected
    • 实例 cancelled(撤回)→ 报销单状态回到 draft
    • 实例 terminated(退回)→ 报销单状态回到 draft

5.5 动作→状态对照表(消歧义)

用于避免 workflow_tasks.status / outcome / workflow_instances.current_status / 报销 status 之间的实现分歧。

动作 workflow_tasks.status workflow_tasks.outcome workflow_instances.current_status 报销 status
提交 submit 首个 task=pending start pending reviewing
通过 approve(非最终) 当前=completed;下一=pending approved pending 保持 reviewing
通过 approve(最终) 当前=completed approved approved approved
驳回 reject 当前=rejected rejected rejected rejected
退回 return 当前=completed;其余 pending=cancelled returned terminated draft
撤回 withdraw 所有 pending=cancelled N/A cancelled draft
转审 transfer 保持 pending;assignee_id 更新 transfer 保持 pending 保持 reviewing
跳过(无审批人) 当前=skipped skipped pending(推进到下一) 保持 reviewing

6. 审批规则(Approval Rules,P0 可配置)

用于决定报销审批链的层级与角色,不在业务代码中硬编码金额阈值。

6.1 approval_rules 表(字段)

字段 类型 说明
id UUID 主键
name String 规则名(如“小额/中额/大额”)
min_amount Decimal 最小金额(含)
max_amount Decimal 最大金额(不含;NULL 表示无上限)
level1_role String 一级审批角色(role_code)
level2_role String 二级审批角色(可空)
level3_role String 三级审批角色(可空)
enabled Bool 启用
priority Int 优先级(越小越优先)

6.2 匹配算法(P0 定案)

  • amount >= min_amount
  • max_amount is null OR amount < max_amount
  • ORDER BY priority ASC, min_amount ASC LIMIT 1
  • 若无匹配:fallback 到“默认小额规则”

6.3 P0 预置 3 条规则(默认值)

name min_amount max_amount level1_role level2_role level3_role priority
小额 0 500 manager finance NULL 10
中额 500 5000 manager finance_mgr NULL 20
大额 5000 NULL manager ceo finance_mgr 30

6.4 审批角色 → 指派规则映射(消歧义,P0 定案)

approval_rules.level*_role 存的是 role_code;工作流落到 workflow_tasks 时,需要把部分 role_code 映射为“解析规则”(assignee_rule),避免把 manager 误解为“一个静态角色池”。

approval_rules.level*_role(role_code) 生成的工作流指派规则 workflow_tasks 落地形态
manager assignee_rule.type=department_manager(以 owner_id 为基准解析直属经理) 写入 assignee_id(解析到具体人);若找不到则该步骤可跳过并写入 history
其他 role_code(如 finance/finance_mgr/ceo) assignee_rule.type=role,value=role_code 写入 assignee_role=role_code(允许该角色下任一成员处理;行级锁保证只完成一次)

7. 转审(Transfer,P0)

7.1 approval_transfers 表(字段)

字段 类型 说明
id UUID 主键
reimbursement_id UUID 关联报销单
from_approver_id UUID 原审批人
to_approver_id UUID 新审批人
reason String 转审原因(至少 10 字)
status String active/cancelled
created_at/created_by DateTime/UUID 创建时间/操作者

7.2 业务规则(P0 定案)

  • 仅当前 workflow_tasks.status=pending 的实际处理人可转审
  • 转审后原审批人失去操作权(只读),新审批人成为任务 assignee_id
  • 转审链最大次数:3(超过需 admin 介入)
  • 转审必须落 3 处证据:
    1. 更新 workflow_tasks.assignee_id
    2. 插入 approval_transfers
    3. 插入 workflow_history,action=transfer,payload 含 from/to/reason

8. 单据编号规则(Document Sequences,P0)

8.1 document_sequences 表(字段)

字段 类型 说明
document_type String 文档类型(如 reimbursement)
prefix String 前缀(如 BX)
date_format String 日期格式(如 YYYYMMDD)
separator String 分隔符(如 -)
number_length Int 流水号长度(默认 4)
reset_period String none/daily/monthly/yearly
current_number Int 当前序号(原子递增)
last_reset_date DateTime 上次重置时间

约束:document_type 必须唯一(避免多条序列配置导致重复单号)。

8.2 P0 默认报销单号格式

  • document_type=reimbursement
  • prefix=BX
  • date_format=YYYYMMDD
  • separator=-
  • number_length=4
  • reset_period=daily

示例:BX-20260205-0001

8.3 并发安全要求(实现约束)

  • 编号生成必须数据库原子递增(等价于 UPDATE ... RETURNING),并在同一事务内写入报销单
  • form_number 必须全局唯一;失败必须回滚,禁止出现重复编号或“跳号后写入失败但编号已占用”的不可追溯状态
  • daily reset 的日期边界以公司本地时区为准(默认 Asia/Shanghai);实现时应统一使用显式时区计算,禁止依赖数据库/容器时区的隐式行为

9. 费用类型与科目映射(Expense Types,P0)

9.1 expense_types(动态费用类型)

字段至少包含:

  • code(全局唯一、不可修改)
  • name
  • description
  • category(用于 UI 分组;系统默认使用 travel/rd/production/office/business/assets/other)
  • sort_order
  • account_code_default(默认会计科目,可配置)
  • is_system_default(系统内置:不可删除、仅可禁用)
  • enabled(软禁用:不在录入下拉显示,但历史可展示)

9.2 系统默认费用类型(P0:37 项启用)

  • 基线:复用 shenzhou-business-suite/supabase/seed.sql 中已启用的 35 项
  • 补齐:启用 utilities(水电费)与 rent(房租租赁费)两项高频类型
  • 兼容预留:保留 3 个 legacy 类型(transport/meal/rd)为禁用状态,不在 P0 下拉显示

详见附录 A(37 项启用)与附录 B(legacy 禁用)。


10. 发票池与文件存储(P0:本地 + 签名 URL)

关键原则:文件与票据元数据以 invoice_pool 为单一事实源;报销单通过“绑定”使用发票池条目(见 4.4)。

10.1 P0 存储方案(本地存储)

  • 存储介质:本地磁盘(Docker volume)
  • DB 仅保存 storage_path,不保存公网 URL
  • 访问方式:后端生成签名 URL(TTL=10 分钟)用于浏览器预览/下载

10.2 安全基线(P0)

  • 文件类型白名单:application/pdf、image/jpeg、image/png、image/webp
  • 单文件大小:≤ 10MB
  • 权限:
    • 未绑定时:owner_employee_id、uploaded_by_employee_id、finance/finance_mgr、admin 可访问
    • 绑定后:除以上人员外,报销相关人员(owner/submitter/审批链/财务/admin)可访问
  • 文件元数据(用于判重/审计):
    • file_sha256(必填)
    • invoice_number/invoice_code(可选;齐全时参与判重)

10.3 P2 迁移(MinIO/OSS)

  • 替换为对象存储(S3 pre-signed URL)
  • 接口与 DB 字段保持不变(storage_path 语义不变)

10.4 invoice_pool 表结构(P0)

字段 类型 说明
id UUID 主键
owner_employee_id UUID 费用归属人(决定可见范围与绑定归属)
uploaded_by_employee_id UUID 上传人(可能是行政代上传)
storage_path String 本地存储路径(相对路径)
original_name String 原始文件名
mime_type String MIME 类型
file_size Int 文件大小(字节)
file_sha256 String 文件哈希(必填;用于判重与 OCR 去重)
status String pending_ocr/ocr_failed/available/bound/archived
archived_reason String used_complete/unused_timeout/manual(仅 archived 有意义)
archived_at DateTime 归档时间(可空)
linked_reimbursement_id UUID 绑定到报销单(可空;status=bound 必填)
linked_item_id UUID 绑定到费用明细(可空)
remarks String 备注/人工说明(可空)
ocr_provider String OCR 提供方(P0 固定 tencent)
ocr_type String vat/train/flight/taxi/general
ocr_result_id UUID 关联 ocr_results(可空)
ocr_attempts Int OCR 重试次数(默认 0)
ocr_last_error String OCR 最近一次错误(可空)
ocr_completed_at DateTime OCR 完成时间(可空)
invoice_code String 发票代码(可空)
invoice_number String 发票号码(可空)
invoice_date Date 开票日期(可空)
seller_name String 销售方名称(可空)
buyer_name String 购买方名称(可空)
amount Decimal 不含税金额(可空)
tax_amount Decimal 税额(可空)
total_amount Decimal 价税合计(可空)
is_duplicate_override Bool 是否为判重例外(默认 false)
override_ref_form_number String 例外引用的原单号(is_duplicate_override=true 时必填)
override_reason String 例外原因(is_duplicate_override=true 时必填,≥10 字)
overridden_by UUID 谁批准的例外(finance_mgr)
created_at/created_by DateTime/UUID 创建时间/操作者
updated_at/updated_by DateTime/UUID 更新时间/操作者

10.5 ocr_results(去重 OCR 结果表,P0)

目的:避免同一文件重复计费调用 OCR,同时持久化标准化结果用于复核与复用。

字段 类型 说明
id UUID 主键
file_sha256 String 文件哈希(与 invoice_pool.file_sha256 对齐)
ocr_provider String 提供方(P0 固定 tencent)
ocr_type String vat/train/flight/taxi/general
result_json JSON 标准化 OCR 结果(必须保存)
confidence Int 置信度(可空)
created_at DateTime 创建时间

唯一约束(P0 定案):

  • UNIQUE (file_sha256, ocr_provider, ocr_type)

10.6 索引与约束(P0 定案)

Prisma Schema 不能直接表达 partial unique index,需要通过 自定义 SQL migration 创建(prisma migrate dev --create-only 后编辑迁移 SQL)。

并发绑定保护(实现约束):

  • POST /api/invoice-pool/:id/bind 必须对目标 invoice_pool 行加行级锁(SELECT ... FOR UPDATE),避免并发双绑。

全局双策略判重(只对已使用历史生效,P0 定案):

  • UNIQUE (file_sha256) WHERE (status = 'bound' OR (status = 'archived' AND archived_reason = 'used_complete')) AND is_duplicate_override = false
  • UNIQUE (invoice_code, invoice_number) WHERE invoice_code IS NOT NULL AND invoice_number IS NOT NULL AND (status = 'bound' OR (status = 'archived' AND archived_reason = 'used_complete')) AND is_duplicate_override = false

例外字段完整性(DB check,防漏填):

  • 当 is_duplicate_override = true 时,override_ref_form_number、override_reason、overridden_by 必填,且 char_length(override_reason) >= 10

10.7 API 约定(P0)

  • 上传:POST /api/invoice-pool/upload(multipart;入参 file + 可选 owner_employee_id + 可选 ocr_type;默认 ocr_type=general)
    • 上传即创建 invoice_pool 记录并返回 invoice_pool_id
    • OCR 结果命中 ocr_results 时应复用(不重复计费)
  • 签名 URL:POST /api/invoice-pool/:id/signed-url(TTL=10m;服务端按 10.2 的规则鉴权)

11. 认证与会话(P0)

11.1 登录方式(P0)

  • 支持 手机号/邮箱 + 密码 登录
  • 验证码仅用于“找回/重置密码”(P0:邮件验证码;短信验证码/SSO 放 P1/P2)

11.2 密码策略(P0)

  • 长度:≥12
  • 复杂度:必须同时包含 大写 + 小写 + 数字(允许特殊字符但不强制)
  • 弱密码拦截:禁止命中弱密码库(Top 10k)
  • 错误次数锁定:15 分钟内失败 5 次 → 锁定 30 分钟;管理员可解锁
  • 首次登录:必须修改初始密码

11.3 Token 与会话(JWT + Refresh Rotation)

  • Access Token:
    • 过期:15 分钟
    • 传输:Authorization: Bearer <token>
    • 存储:仅内存(禁止 localStorage/sessionStorage)
  • Refresh Token:
    • 过期:7 天
    • 客户端存储:HttpOnly + Secure + SameSite=Strict Cookie
    • 轮换:每次 refresh 都签发新 refresh,旧 refresh 立即失效
    • 服务端存储:数据库保存 refresh 的哈希(不可明文),并绑定设备信息(deviceFingerprint/ipAddress/userAgent)
  • 强制下线:员工 tokenVersion + 1(密码重置/离职/管理员强制下线)使旧 access 立即失效

实现约束(P0 定案):

  • tokenVersion 立即失效机制:
    • Access Token 载荷必须包含 tokenVersion
    • 每次请求服务端必须校验 tokenVersion(从 DB 或缓存读取当前值)
    • 若 Token 中 tokenVersion < DB tokenVersion,立即返回 AUTH_004
  • Refresh 重放检测(防止 refresh 泄漏后被持续利用):
    • 每次 refresh 成功后,旧 refresh 的 hash 必须标记为已使用或直接删除
    • 若检测到"已使用的旧 refresh 被重放",说明可能存在泄漏:
      • 立即撤销该员工所有有效会话(revoked_at = now())
      • 员工 tokenVersion + 1
      • 写入 audit_logs(action=REFRESH_REUSE_DETECTED)
      • 返回 AUTH_005 并要求用户重新登录
  • Refresh 同站校验(额外防线):
    • POST /api/auth/refresh 若请求携带 Origin/Referer,则必须与本站域名匹配(按服务端配置的 APP_ORIGIN)
    • 不匹配则拒绝该请求并返回 AUTH_005,同时写入 audit_logs(action=AUTH_REFRESH_ORIGIN_MISMATCH)

11.4 Auth API(P0)

  • POST /api/auth/login → 返回 access;refresh 通过 Set-Cookie
  • POST /api/auth/refresh → 从 cookie 轮换 refresh,返回新 access
  • POST /api/auth/logout → 撤销当前会话并清 cookie
  • POST /api/auth/forgot-password(邮件验证码)
  • POST /api/auth/reset-password

11.5 auth_sessions(Refresh 会话表,P0)

目的:服务端可撤销 refresh、限制并发会话数、绑定设备信息以支持 Phase 2 异常登录检测。

字段 类型 说明
id UUID 主键
employee_id UUID 所属员工
refresh_token_hash String refresh 的哈希(不可明文)
device_fingerprint String 设备指纹(必填)
ip_address String 登录 IP(必填)
user_agent String UA(必填)
created_at DateTime 创建时间
last_seen_at DateTime 最近活跃时间(refresh 时更新)
expires_at DateTime 过期时间(= 7 天)
revoked_at DateTime 撤销时间(可空)

会话规则(P0 定案):

  • 同一员工最多 5 个有效会话(revoked_at is null AND expires_at > now());超过时撤销最旧会话
  • 退出登录:撤销当前会话(写 revoked_at)
  • 密码重置/离职/管理员强制下线:撤销该员工所有有效会话 + tokenVersion + 1

12. 数据安全与审计(P0)

12.1 敏感字段清单

  • 员工:银行卡号、身份证号
  • 报销:发票/附件文件(可能包含敏感信息)

12.2 脱敏展示规则

  • 银行卡:仅显示后 4 位(例:**** **** **** 1234)
  • 身份证:前 3 位 + 后 4 位(例:310***********1234)

12.3 加密存储(应用层加密)

  • 算法:AES-256-GCM
  • 每条记录随机 nonce
  • 密钥来自服务端环境变量(P0);后续可迁移 KMS
  • 明确:加密字段不支持按明文检索(禁止为“搜索”而明文存储)

12.4 审计分层(必须写清楚)

  • workflow_history:审批链路与流程动作的权威记录(谁在何时对哪个任务做了什么)
  • audit_logs(系统审计):登录/锁定/重置/敏感字段明文查看/权限配置变更/判重例外等安全关键事件

audit_logs 表(建议字段):

字段 类型 说明
id UUID 主键
request_id String 关联请求 ID
actor_id UUID 操作人
action String 动作码(如 AUTH_LOGIN_SUCCESS/INVOICE_DUPLICATE_OVERRIDE)
target_type String 目标类型(如 employee/reimbursement/attachment)
target_id UUID 目标 ID(可空)
ip/user_agent String 来源信息
extra JSON 扩展信息(含变更快照)
created_at DateTime 时间戳

对“敏感字段变更/授权变更/关键配置变更”,extra.changes 必须包含脱敏前后快照: { changes: { field: { before: masked, after: masked } } }(禁止记录明文)

保留期:至少 1 年。

12.5 审计动作码(P0 最小集)

目的:把“高风险动作”与“关键链路”落到可检索的审计事件上,便于排查与合规追溯。

OCR(发票识别)

  • OCR_REQUESTED:发起 OCR 请求(含 ocr_type、file_sha256)
  • OCR_SUCCEEDED:OCR 成功(含 ocr_result_id、confidence 摘要)
  • OCR_FAILED:OCR 失败(含错误摘要;禁止记录 rawResponse)
  • OCR_RETRIED:OCR 重试(含 attempts 与节流结果)

发票池(Invoice Pool)

  • INVOICE_POOL_UPLOADED:文件入池(owner/uploaded_by、mime/size/hash)
  • INVOICE_POOL_EDITED:人工修正关键字段(extra.changes 记录脱敏前后快照)
  • INVOICE_POOL_BOUND:绑定到报销单
  • INVOICE_POOL_UNBOUND:从报销单解绑
  • INVOICE_POOL_ARCHIVED:人工归档(archived_reason=manual)
  • INVOICE_POOL_AUTO_ARCHIVED:系统自动归档(archived_reason=unused_timeout/used_complete)

判重与例外

  • INVOICE_DUPLICATE_OVERRIDE:判重例外(必须包含引用原单号 + 原因摘要)

13. API 契约与错误码(P0)

本章为 API 契约唯一权威(P0 定案)。目的:前后端联调与测试可直接以此为准,不在实现阶段临场补协议。

13.1 通用响应结构(P0)

  • 所有响应必须返回 requestId(可放在响应头 x-request-id,并在 body 同步返回)
  • 失败响应结构:
{
  "requestId": "01J....",
  "error": {
    "code": "AUTH_001",
    "message": "用户名或密码错误",
    "details": {}
  }
}
  • 成功响应结构(对象):
{
  "requestId": "01J....",
  "data": {}
}
  • 成功响应结构(列表 + 分页):
{
  "requestId": "01J....",
  "data": [],
  "pageInfo": {
    "page": 1,
    "pageSize": 20,
    "total": 0,
    "totalPages": 0
  }
}

13.1.1 分页 / 排序 / 筛选约定(P0)

目的:前后端统一口径,避免“边开发边发明 query 参数”。

  • page:页码(从 1 开始;默认 1)
  • pageSize:每页数量(默认 20;最大 100)
  • sort:排序(格式:field:asc|desc;多个排序用逗号分隔,例如 sort=created_at:desc,total_amount:desc)
  • q:可选的关键字搜索(各接口自行决定是否支持)
  • 过滤参数:按接口逐项定义(P0 不引入通用 filter DSL)

13.1.2 字段类型约定(P0)

  • UUID:JSON 中一律为 string
  • Date:YYYY-MM-DD(业务日期)
  • DateTime:ISO 8601(建议带时区,例如 2026-02-06T09:00:00+08:00)
  • Decimal/Money:JSON 中一律为 string(禁止用 number,避免精度丢失)

13.2 核心业务 API(P0)

13.2.1 报销单(Reimbursements)CRUD

方法 路径 权限码 说明
POST /api/reimbursements expense.create 创建报销草稿(默认 owner_id=me, submitter_id=me;代提交场景可指定 owner,但必须存在有效授权)
GET /api/reimbursements expense.view_own(或 expense.view_all) 报销列表(分页/筛选/排序);服务端按权限自动收敛数据范围
GET /api/reimbursements/:id expense.view_own(或 expense.view_all) 报销详情(含 items + 已绑定的发票池条目摘要;可包含当前 pending task 用于“节点文案”展示)
PATCH /api/reimbursements/:id expense.edit_own 更新草稿/驳回单(仅允许 status in (draft, rejected))
DELETE /api/reimbursements/:id expense.delete_own 删除草稿(仅允许 status=draft;需删除 items;并自动解绑其绑定的发票池条目(回到 available;不删除发票池文件))

列表筛选建议(P0):

  • status:draft/reviewing/approved/paid/rejected/cancelled
  • formNumber:精确匹配
  • ownerId / submitterId / departmentId
  • reimbursementDateFrom / reimbursementDateTo(业务日期)
  • createdAtFrom / createdAtTo(时间戳)

13.2.2 审批待办(Workflow Tasks)

方法 路径 权限码 说明
GET /api/workflow/tasks 登录即可(服务端鉴权) 待办/已办列表(分页/筛选/排序);默认只返回“我可处理”的任务(assignee_id=me 或 assignee_role in myRoles)

列表筛选建议(P0):

  • status:默认 pending;可选 completed/rejected/cancelled/skipped
  • entityType:默认 reimbursement
  • reimbursementId(便于从报销详情页拉取当前任务/历史)

13.2.3 流程动作(Workflow / Reimbursement Actions)

方法 路径 权限码 说明
POST /api/reimbursements/:id/submit expense.submit 提交报销并发起工作流(生成单号、创建 instance/task/history)
POST /api/reimbursements/:id/withdraw expense.withdraw 撤回:取消流程实例并回到草稿(仅审批人未处理前)
POST /api/workflow/tasks/:taskId/approve expense.approve 通过当前任务并推进
POST /api/workflow/tasks/:taskId/reject expense.approve 驳回并终止流程
POST /api/workflow/tasks/:taskId/return expense.approve 退回:终止当前实例(instance=terminated)并回到草稿;申请人修改后需重新 submit 才会进入新一轮
POST /api/workflow/tasks/:taskId/transfer expense.transfer 转审(reason≥10,链≤3)
POST /api/reimbursements/:id/pay expense.finance.pay 付款标记(approved → paid)

13.2.4 员工与组织(Employees / Departments,P0)

方法 路径 权限码 说明
GET /api/me 登录即可 获取当前登录员工信息(含 role_codes;前端用于菜单与数据范围判断)
GET /api/employees system.users 员工列表(分页/筛选/排序)
GET /api/employees/:id system.users(或 expense.view_own 仅本人) 员工详情
POST /api/employees system.users 创建员工(初始密码策略见第 11 章;首次登录强制改密)
PATCH /api/employees/:id system.users 更新员工信息(敏感字段加密/脱敏与审计见第 12 章)
POST /api/employees/:id/archive system.users 员工离职/归档(禁止登录并强制下线,见 4.1.2/11.3)
GET /api/departments 登录即可 部门树/列表(默认隐藏归档部门,可通过 query 开启)
POST /api/departments system.users 创建部门
PATCH /api/departments/:id system.users 更新部门
POST /api/departments/:id/archive system.users 归档部门(归档约束见 3.2)

13.2.5 认证(Auth,P0)

方法 路径 权限码 说明
POST /api/auth/login N/A 登录:返回 access token;refresh token 通过 Set-Cookie 写入 HttpOnly cookie(见 11.3)
POST /api/auth/refresh N/A 刷新:轮换 refresh cookie,返回新 access token(refresh rotation + 重放检测见 11.3)
POST /api/auth/logout 登录即可 退出:撤销当前会话并清 cookie
POST /api/auth/forgot-password N/A 忘记密码:发送邮件验证码(P0)
POST /api/auth/reset-password N/A 重置密码:验证码校验 + tokenVersion + 1 强制下线(见 11.3)

13.2.6 发票池(Invoice Pool)+ OCR(P0)

方法 路径 权限码 说明
POST /api/invoice-pool/upload invoice.upload 上传票据文件入池(multipart;入参 file + 可选 owner_employee_id + 可选 ocr_type;默认 ocr_type=general;返回 invoice_pool_id + 初始 status)
GET /api/invoice-pool 登录即可(服务端鉴权) 发票池列表(分页/筛选/排序);默认只返回与本人相关(owner 或 uploaded_by),finance/admin 可查看全量
GET /api/invoice-pool/:id 登录即可(服务端鉴权) 发票池详情(含 OCR 摘要字段)
PATCH /api/invoice-pool/:id 登录即可(服务端鉴权) 更新备注/人工修正关键字段(如 invoice_code/number/date/amount);允许将 ocr_failed 条目“人工确认”转为 available
POST /api/invoice-pool/:id/retry-ocr 登录即可(服务端鉴权) 重试 OCR(仅 status=ocr_failed;次数/频控见下)
POST /api/invoice-pool/:id/bind 登录即可(服务端鉴权) 绑定到报销单(1:1;并发与判重硬校验在此发生)
POST /api/invoice-pool/:id/unbind 登录即可(服务端鉴权) 从报销单解绑(仅草稿/驳回可解绑;解绑后回到 available)
POST /api/invoice-pool/:id/archive 登录即可(服务端鉴权) 手工归档(仅未绑定;归档为 archived_reason=manual)
POST /api/invoice-pool/:id/signed-url 登录即可(服务端鉴权) 获取票据文件签名 URL(TTL=10m;服务端按第 10 章规则鉴权)

上传归属权限链(P0 定案):

  • 当 owner_employee_id != 当前登录用户 时:
    • 若当前登录用户持有 system.users(hr/admin)→ 允许
    • 若当前登录用户持有 expense.proxy_submit 且角色包含 administration → 必须存在有效 delegation_grants(principal=owner, delegate=当前用户, scope 包含 reimbursement_submit)
    • 否则拒绝(PERM_001)

绑定并发语义(P0 定案):

  • POST /api/invoice-pool/:id/bind 必须在事务内对目标 invoice_pool 行执行 SELECT ... FOR UPDATE(行级锁)。
  • 若已是 bound 或 linked_reimbursement_id 非空:返回 409 VALID_005(发票已被其他报销单占用/绑定)。
  • 若命中全局判重唯一约束:返回 409 VALID_003(发票判重命中)。

retry-ocr 次数与频控(P0 固定值):

  • 次数:ocr_attempts <= 3;超过返回 429 VALID_006(OCR 重试次数超限)。
  • 间隔:同一条目两次重试间隔 >= 60s;过于频繁返回 429 VALID_007(OCR 重试过于频繁)。

13.2.7 请假 API

方法 路径 权限码 说明
GET /api/leave-types leave.view_own 列出启用假别;leave.manage 可查看禁用项与配额配置
POST /api/leave-types leave.manage 创建假别与年度配额策略
PATCH /api/leave-types/:id leave.manage 更新假别启用状态、附件要求与年度额度
GET /api/leave-balances leave.view_own 查看当前员工请假余额;查询他人需 leave.view_all/leave.manage
GET /api/leave-requests leave.view_own(或 leave.view_all/leave.manage) 请假列表;服务端按本人或全量视角收敛
POST /api/leave-requests leave.create 创建请假草稿并校验基础日期 / 附件规则
GET /api/leave-requests/:id 登录即可(服务端鉴权) 获取请假详情;仅本人或具备全量权限的角色可读
POST /api/leave-requests/:id/submit leave.submit 提交请假申请,触发冲突校验、额度强设闸与 workflow 启动
POST /api/leave-requests/:id/withdraw leave.submit 在审批人未处理前撤回,请假状态回到 draft 并释放冻结额度

请假的 approve / reject / return 继续复用通用 workflow API,审批权限码为 leave.approve。

13.2.8 采购 API

方法 路径 权限码 说明
GET /api/purchase-requests 登录即可(服务端鉴权) 采购列表;按 procurement.view_own/view_all/manage 收敛 mine 视角
POST /api/purchase-requests procurement.create 创建采购草稿
GET /api/purchase-requests/:id 登录即可(服务端鉴权) 获取采购详情;仅申请人 / 提交人或可读角色可读
PATCH /api/purchase-requests/:id procurement.create 更新采购草稿;仅 draft 可编辑
POST /api/purchase-requests/:id/submit procurement.submit 提交采购申请并写预算预留
POST /api/purchase-requests/:id/withdraw procurement.submit 在审批人未处理前撤回,释放预算并回到 draft
GET /api/purchase-requests/:id/archive-order 登录即可(服务端鉴权) 查看 PO / 合同归档信息
POST /api/purchase-requests/:id/archive-order procurement.manage 在 approved 采购单上创建或更新归档记录
POST /api/purchase-requests/:id/attachments procurement.create 上传采购附件
DELETE /api/purchase-requests/:id/attachments/:attachmentId procurement.create 删除采购附件
POST /api/purchase-requests/:id/attachments/:attachmentId/signed-url 登录即可(服务端鉴权) 获取附件签名 URL
GET /api/purchase-requests/:id/attachments/:attachmentId/file 登录即可(服务端鉴权) 下载附件文件

采购 approve / reject / return 继续复用通用 workflow API,审批权限码为 procurement.approve。

13.2.9 资产与盘点 API

方法 路径 权限码 说明
GET /api/asset-categories 登录即可(服务端鉴权) 资产分类列表;按 asset.view_own/view_all/manage 收敛
POST /api/asset-categories asset.manage 创建资产分类
PATCH /api/asset-categories/:id asset.manage 更新资产分类
GET /api/assets/assignees 登录即可(服务端鉴权) 资产领用人候选查询;内部要求具备 asset.manage
GET /api/assets 登录即可(服务端鉴权) 资产卡片列表;按 asset.view_own/view_all/manage/transfer/inventory 收敛
POST /api/assets asset.manage 创建资产卡片;可在建卡时初始化领用状态
GET /api/assets/:id 登录即可(服务端鉴权) 资产详情
PATCH /api/assets/:id asset.manage 更新资产卡片;当前领用 / 归还 / 调拨语义在此受控落账
GET /api/inventory-checks asset.inventory 盘点任务列表
POST /api/inventory-checks asset.inventory 创建盘点任务
POST /api/inventory-checks/:id/start asset.inventory 启动盘点
POST /api/inventory-checks/:id/items asset.inventory 录入或覆盖盘点明细
POST /api/inventory-checks/:id/complete asset.inventory 完成盘点任务

13.2.10 差旅 API

方法 路径 权限码 说明
GET /api/travel-requests 登录即可(服务端鉴权) 差旅申请列表;按 travel.view_own/view_all/manage 收敛
POST /api/travel-requests travel.create 创建差旅草稿
GET /api/travel-requests/:id 登录即可(服务端鉴权) 差旅申请详情
PATCH /api/travel-requests/:id travel.create 更新差旅草稿
POST /api/travel-requests/:id/submit travel.submit 提交差旅申请并写预算预留
POST /api/travel-requests/:id/withdraw travel.submit 撤回差旅申请并释放预算
GET /api/travel-requests/:id/payment-track 登录即可(服务端鉴权) 查看订单支付轨道摘要
POST /api/travel/orders/import integration.manage 导入差旅订单
POST /api/travel-orders/:id/mark-paid travel.payment.manage 手工结算差旅订单并写支付凭证字段
GET /api/travel/authorizations travel.payment.manage 查看企业卡授权记录
POST /api/travel/authorizations/import integration.manage 导入企业卡授权
GET /api/travel/transactions travel.payment.manage 查看企业卡交易
POST /api/travel/transactions/import integration.manage 导入企业卡交易
POST /api/travel/transactions/:id/freeze-suggestion travel.payment.manage 标记冻结建议
GET /api/travel/reconciliation/batches 登录即可(服务端鉴权) 查看对账批次列表;服务端按差旅 / 集成读权限过滤
POST /api/travel/reconciliation/batches travel.payment.manage 创建对账批次快照
GET /api/travel/reconciliation/batches/:batchId 登录即可(服务端鉴权) 获取批次详情与 KPI
POST /api/travel/reconciliation/items/:itemId/confirm travel.payment.manage 确认异常明细
POST /api/travel/reconciliation/items/:itemId/reopen travel.payment.manage 重开异常明细
POST /api/travel/reconciliation/batches/:batchId/finalize travel.payment.manage 将批次从 draft 收口到 reconciled

差旅 approve / reject / return 继续复用通用 workflow API,审批权限码为 travel.approve。

13.2.11 集成 API

方法 路径 权限码 说明
GET /api/integrations/erp/master-data/status integration.manage 查看 ERP 主数据同步状态
POST /api/integrations/erp/master-data/sync integration.manage 手工触发主数据同步
GET /api/integrations/erp/journals/reimbursements/:reimbursementId/preview integration.manage 查看报销凭证切片预览
POST /api/integrations/erp/journals/reimbursements/:reimbursementId/export integration.manage 手工触发报销 ERP 导出
GET /api/integrations/erp/procurement/purchase-requests/:purchaseRequestId/track 登录即可(服务端鉴权) 查看采购 ERP 轨道;仅具备该采购单读权限的用户可见
POST /api/integrations/erp/procurement/purchase-requests/:purchaseRequestId/requeue integration.manage 补发或重试采购 ERP 轨道
GET /api/integrations/erp/assets/:assetId/track 登录即可(服务端鉴权) 查看资产 ERP 轨道
POST /api/integrations/erp/assets/:assetId/requeue integration.manage 补发或重试资产 ERP 轨道
GET /api/integrations/erp/travel/reconciliation-batches/:batchId/track 登录即可(服务端鉴权) 查看差旅对账批次 ERP 轨道
POST /api/integrations/erp/travel/reconciliation-batches/:batchId/requeue integration.manage 补发或重试差旅 ERP 摘要桥
POST /api/integrations/erp/sync/:reimbursementId integration.manage 兼容性手工同步入口

13.3 配置读取 API(P0 只读)

方法 路径 权限码 说明
GET /api/expense-types?enabled=true 登录即可 获取启用费用类型(下拉选项)
GET /api/approval-rules?enabled=true 登录即可 获取启用审批规则(便于运维/排查)
GET /api/document-sequences/reimbursement system.settings(可选) 查看报销单号规则配置

13.4 文件与签名 URL(P0)

上传与文件预览/下载统一归口到“发票池”模块,避免 files 与 invoice_pool 双源。

方法 路径 权限码 说明
POST /api/invoice-pool/upload invoice.upload 上传票据文件入池(见 13.2.6)
POST /api/invoice-pool/:id/signed-url 登录即可(服务端鉴权) 获取签名 URL(TTL=10m;服务端校验“是否与票据相关/是否有权限”)

13.5 Delegation Grants API(P0)

方法 路径 权限码 说明
GET /api/delegation-grants 登录即可 默认返回与本人相关(principal/delegate);system.users 可查询任意
POST /api/delegation-grants 登录即可 本人创建:principal=自己;代维护需 system.users;所有变更写入 audit_logs
POST /api/delegation-grants/:id/revoke 登录即可 本人撤销:principal=自己;代维护需 system.users;所有变更写入 audit_logs

实现约定(P0):

  • GET /api/delegation-grants 响应建议内嵌 principal/delegate 的员工摘要(如 id/employee_no/name/department_id),用于“行政代提交选择人员”场景,避免前端额外拉取员工目录。
  • 默认只返回“当前仍有效”的授权(revoked_at is null 且时间在有效期内);如需查看历史可通过 query 显式开启(例如 includeInactive=true)。

13.6 错误码(P0 最小集)

13.6.1 认证(Auth)

  • AUTH_001:用户名或密码错误(401)
  • AUTH_002:账号已锁定(423)
  • AUTH_003:账号已离职/禁用(403)
  • AUTH_004:Access Token 已过期/无效(401)
  • AUTH_005:Refresh Token 无效/已撤销(401)

13.6.2 权限(Permission)

  • PERM_001:无权限(403)
  • PERM_002:不在审批范围/非当前处理人(403)

13.6.3 校验(Validation)

  • VALID_001:参数校验失败(400)
  • VALID_002:转审原因不足 10 字(400)
  • VALID_003:发票判重命中(409)
  • VALID_004:发票重复例外被禁止(原单已打款/待打款)(409)
  • VALID_005:发票已被其他报销单占用/绑定(409)
  • VALID_006:OCR 重试次数超限(429)
  • VALID_007:OCR 重试过于频繁(429)

14. 非功能需求

类别 要求
响应式 PC + 移动端(响应式 Web)
性能 页面加载 <2s,API 响应 <500ms(P95)
安全 JWT(access+refresh rotation)、RBAC 权限码、审计日志、敏感字段加密与脱敏
部署 Docker + Nginx(反向代理),域名 oa.hyinda.com

15. 数据模型概览(高层)

erDiagram
    departments ||--o{ employees : contains
    employees ||--o{ employee_roles : assigns
    employees ||--o{ delegation_grants : principal
    employees ||--o{ delegation_grants : delegate
    employees ||--o{ auth_sessions : has
    employees ||--o{ audit_logs : writes

    employees ||--o{ reimbursement_forms : submits
    employees ||--o{ reimbursement_forms : owns
    reimbursement_forms ||--|{ reimbursement_items : has
    reimbursement_items }o--|| expense_types : uses
    reimbursement_forms ||--o{ invoice_pool : binds

    leave_types ||--o{ leave_policies : configures
    leave_types ||--o{ leave_requests : classifies
    employees ||--o{ leave_requests : owns
    leave_requests ||--o{ leave_ledgers : anchors
    employees ||--o{ leave_ledgers : accrues

    employees ||--o{ purchase_requests : applies
    purchase_requests ||--|{ purchase_request_items : has
    purchase_requests ||--o{ purchase_attachments : stores
    purchase_requests ||--o| purchase_order_archives : archives

    asset_categories ||--o{ asset_cards : classifies
    purchase_requests ||--o{ asset_cards : sources
    asset_cards ||--o{ asset_assignments : assigns
    asset_cards ||--o{ asset_transfers : transfers
    inventory_checks ||--o{ inventory_check_items : records
    asset_cards ||--o{ inventory_check_items : counted

    employees ||--o{ travel_requests : applies
    travel_requests ||--|{ travel_request_segments : has
    travel_requests ||--o{ travel_orders : imports
    travel_requests ||--o{ card_authorizations : authorizes
    travel_requests ||--o{ card_transactions : settles
    travel_reconciliation_batches ||--o{ travel_reconciliation_items : includes
    travel_orders ||--o{ travel_reconciliation_items : snapshots
    card_transactions ||--o{ travel_reconciliation_items : snapshots

    workflow_definitions ||--o{ workflow_instances : defines
    reimbursement_forms ||--o{ workflow_instances : starts
    leave_requests ||--o{ workflow_instances : starts
    purchase_requests ||--o{ workflow_instances : starts
    travel_requests ||--o{ workflow_instances : starts
    workflow_instances ||--o{ workflow_tasks : has
    workflow_instances ||--o{ workflow_history : logs

说明:

  • 上图为概念关系图,具体字段与索引以各章节表结构和 server/prisma/schema.prisma 为准。
  • IntegrationOutbox、IntegrationBinding、IntegrationSyncState、NotificationChannel 构成集成层私有模型,不回写核心事务表。

16. 迭代计划

状态口径说明(2026-03-13):

  • 以 docs/feature-matrix.md、docs/todo-reconciliation.md 与各已完成执行合同为实现状态事实源。
  • 本节用于规格分期表达;具体执行状态仍以 Issue CSV 和实现状态矩阵为准。
  • 本轮规格书正文新增覆盖请假 / 采购 / 资产 / 差旅 / 集成五大业务域;AI / MCP / PWA / 生产化治理继续以边界与运维文档承载。

Phase 1(核心 / P0)

  • 技术栈确定(Fastify + Prisma + PostgreSQL)
  • 员工与组织(含归档约束、角色集合)
  • 权限码体系与服务端校验(RBAC)
  • 工作流引擎(4 表)落库与服务层事务
  • approval_rules(预置 3 条)+ 审批链生效
  • 转审(reason≥10,链路≤3)
  • document_sequences 单号规则(BX-YYYYMMDD-0001)
  • 费用类型 expense_types(默认启用 37 项)+ 下拉动态加载
  • 发票池上传(本地存储 + 签名 URL)+ SHA256 + 腾讯云 OCR(结果持久化 / 去重 / 重试;不阻塞提交)+ 双策略判重(bind / submit 生效)+ 例外审计
  • 报销单 CRUD + 提交 / 撤回 / 审批 / 付款标记
  • 站内通知 + 邮件

Phase 2(基础增强 / P3-P14)

  • 审计回归、OCR mock 回归、全量回归入口与最小 CI 门禁
  • 验真、预算、看板、对象存储、通知渠道、集成框架等基础能力
  • 权限解耦、主数据(法体 / 成本中心 / 供应商)、假期日历与路线治理收口

Phase 3(请假 / P15B)

  • 请假类型、年度配额、余额账本与半天粒度模型
  • 请假申请列表 / 详情 / 提交 / 撤回与 workflow 审批闭环
  • 冲突日期阻断、余额强设闸、审批后 consume / release 台账语义

Phase 4(采购 / P17A-P17B)

  • 采购申请主流程、明细、附件与员工端页面
  • 采购预算 reserve/confirm/release 闭环
  • PO / 合同归档、详情区运营入口与 ERP 轨道挂件

Phase 5(资产 / P18A-P18B)

  • 资产分类、卡片、建卡与采购来源关联
  • 资产领用 / 归还 / 调拨与盘点任务闭环
  • 资产 ERP 摘要桥、详情轨道与只读同步状态

Phase 6(集成 / P19 + IM-1 + ERP-1/2/3)

  • 集成边界、领域事件、MCP 只读基线与路线治理冻结
  • IM-1 通知渠道、Outbox 派发与业务事件接入
  • ERP-1 报销凭证切片、ERP-2 采购 commitment / archive、ERP-3 资产摘要桥

Phase 7(差旅 / P20TRAVEL1-3)

  • TRAVEL-1:差旅申请、行程段、订单入站与预算语义
  • TRAVEL-2:企业卡授权 / 交易导入、订单 mark-paid、发票绑定与支付轨道
  • TRAVEL-3:对账批次快照、异常确认、Finalize 与 erp.travel.export 摘要桥

Phase 8(移动与生产化 / P21-P22)

  • 浏览器 PWA 资产扫码、盘点录入、移动审批 deep link 与离线草稿
  • 路由懒加载、E2E 补齐、benchmark 基线与生产化治理收口
  • 后置 backlog:IM-2/IM-3/IM-4、P22+VEH-1、P22+ASSET-FIN-1

附录 A:系统默认费用类型(P0:37 项启用)

来源:shenzhou-business-suite/supabase/seed.sql(启用 35 项)+ hyb-oa 补齐启用 utilities/rent(2 项)。

code name category sort_order account_code_default description
transport_long 长途交通费 travel 101 5102-01 飞机、火车、长途大巴等城市间交通费用
accommodation 住宿费 travel 102 5102-01 出差期间酒店、宾馆住宿费
meal_allowance 伙食补贴 travel 103 5102-01 出差期间伙食补助
transport_city 市内交通费 travel 104 5102-01 出差地或日常公事打车、公交费
rd_material 研发直接材料 rd 201 5102-07 研发专用的元器件、耗材、PCB打样等
rd_test 研发测试试验费 rd 202 5102-07 第三方检测、环境试验费
rd_service 研发技术服务费 rd 203 5102-07 外包开发、技术咨询服务
rd_sample 研发样品/样机费 rd 204 5102-07 原型机、模型制作费用
hydro_calibration 水声设备标定费 rd 205 5102-07 水听器、换能器等核心设备标定
test_site_rental 试验场地租赁费 rd 206 5102-07 水池、消声室、湖试场地租赁
rd_software 研发软件订阅 rd 207 5102-07 水声仿真、CAD设计软件(≤5000元)
rd_patent 专利申请/维护费 rd 208 5102-07 发明专利代理费、年费
raw_material 原材料 production 301 1403 生产用主材、辅材
prod_direct_material 生产成本-直接材料 production 302 5001-01 生产过程直接消耗的材料
mfg_consumable 制造费用-物料消耗 production 303 5101-02 车间消耗性物料
outsourcing 外协加工费 production 304 5001-03 外壳加工、组件代工等
mfg_repair 生产设备维修费 production 305 5101-03 车间仪器设备维修保养
office 办公费 office 401 5102-02 文具、纸张、打印耗材等
communication 通讯费 office 402 5102-03 话费、宽带、网络服务费
property 物业费 office 403 5102-15 物业管理、停车费
vehicle 车辆费 office 404 5102-10 油费、过路费、保养(私车公用需附协议)
welfare 福利费 office 405 5102-19 下午茶、团建、礼品(不超工资14%)
maintenance 维修费 office 406 5102-09 办公电脑、空调、设施维修
training 培训费 office 407 5102-05 员工外部培训、购书
software 软件服务费 office 408 5102-02 Office/Adobe/SaaS订阅(非研发)
express 快递物流费 office 409 5102-02 样品寄送、文件快递
utilities 水电费 office 410 5102-14 办公场地水电费、能耗费
rent 房租租赁费 office 411 5102-06 办公室/设备租赁费
entertainment 业务招待费 business 501 5102-04 客户宴请、礼品(注意限额)
meeting 会议费 business 502 5102-13 会议场地租赁、茶歇
marketing 市场推广费 business 503 5101-01 展会、画册制作、广告
consulting 咨询服务费 business 504 5102-08 法律顾问、咨询费
consulting_audit 代账/审计费 business 505 5102-08 记账报税、年度审计费
fixed_assets 固定资产 assets 601 1601 单价≥2000元 + 使用年限≥1年
low_value_consumables 低值易耗品 assets 602 1411 工具、小电器(<2000)
bank_fees 银行手续费 other 701 5103-01 转账手续费、账户维护费
other 其他合规费用 other 901 5102-99 无法归类的小额费用

附录 B:Legacy 费用类型(禁用,迁移预留)

code name enabled note
meal 旧-餐费 ❌ 兼容历史/迁移预留,P0 不在下拉显示
rd 旧-研发费 ❌ 兼容历史/迁移预留,P0 不在下拉显示
transport 旧-交通费 ❌ 兼容历史/迁移预留,P0 不在下拉显示

附录 C:P0 验收用例(最小集)

每条用例需覆盖:输入 → 状态流转 → 权限结果 → 日志记录点(workflow_history / audit_logs)。

  1. 审批规则匹配(3 档)

    • 输入:total_amount=100/1000/6000
    • 预期:分别命中“小额/中额/大额”规则,生成对应审批链(manager→finance / manager→finance_mgr / manager→ceo→finance_mgr)
  2. 工作流实例与任务创建

    • 输入:员工提交草稿
    • 预期:创建 workflow_instances(pending)+ 首个 workflow_tasks(pending)+ workflow_history(start);报销状态进入 reviewing
  3. 任务推进(approve)

    • 输入:审批人执行 approve
    • 预期:当前 task completed;创建下一 task(若存在);或实例 approved 并将报销状态置为 approved
  4. 驳回(reject) vs 退回(return)

    • 输入:审批人执行 reject/return(带 comment)
    • 预期(reject):实例 rejected;报销状态 rejected;history 记录 reject;申请人可编辑后再次 submit
    • 预期(return):实例 terminated;报销状态回到 draft;所有 pending task cancelled;history 记录 return;申请人修改后再次 submit
  5. 撤回(withdraw)

    • 输入:申请人在审批人未处理前撤回
    • 预期:实例 cancelled;所有 pending task cancelled;报销状态回到 draft;history 记录 withdraw
  6. 转审(transfer)约束与证据

    • 输入:当前审批人转审(reason 少于/不少于 10 字)
    • 预期:不足 10 字返回 VALID_002;满足时:
      • workflow_tasks.assignee_id 更新
      • 插入 approval_transfers
      • 插入 workflow_history(action=transfer,payload 含 from/to/reason)
  7. 状态 vs 节点一致性

    • 输入:报销单处于 reviewing
    • 预期:页面“当前节点文案”来自 workflow_tasks 的 pending 任务(而非 DB status 细分)
  8. 费用类型启用/禁用语义

    • 输入:禁用某费用类型
    • 预期:新建报销下拉不再显示;历史报销仍可展示该类型名称(不回退为 code)
  9. 发票池入池 + OCR 结果持久化 + 去重复用(ocr_results)

    • 输入:上传同一文件两次(同 file_sha256),OCR 类型相同
    • 预期:
      • 第二次上传命中 ocr_results(不重复计费调用 OCR)
      • invoice_pool 记录复用 ocr_result_id 并写入关键字段摘要(invoice_code/number/date/amount 等)
    • 日志:写 INVOICE_POOL_UPLOADED;如触发 OCR,写 OCR_REQUESTED/OCR_SUCCEEDED(禁止落库 rawResponse)
  10. OCR 失败与重试(次数/频控)

  • 输入:制造 OCR 失败 → 多次调用 POST /api/invoice-pool/:id/retry-ocr
  • 预期:
    • ocr_attempts 递增;失败条目保持 status=ocr_failed
    • 超过 3 次返回 VALID_006(429)
    • 60 秒内重复重试返回 VALID_007(429)
  • 日志:写 OCR_FAILED/OCR_RETRIED(错误摘要即可)
  1. 发票池绑定并发冲突(行锁 + 409)
  • 输入:两个报销草稿同时 bind 同一张发票池条目
  • 预期:一个成功,一个返回 VALID_005(409);服务端在 bind 事务内对 invoice_pool 行 SELECT ... FOR UPDATE
  1. 全局判重与例外(绑定/提交阶段硬校验)
  • 输入:已有“使用完成并归档”的发票(status=archived, archived_reason=used_complete),再尝试用相同 file_sha256 或相同 invoice_code+invoice_number 绑定/提交
  • 预期:
    • 普通用户返回 VALID_003(409)
    • finance_mgr 可执行判重例外,必填原单号+原因(≥10);若原单处于 approved/paid 则返回 VALID_004
  • 日志:例外必须写 audit_logs(INVOICE_DUPLICATE_OVERRIDE)
  1. 90 天游离自动归档不参与全局判重
  • 输入:一张发票池条目 90 天未绑定自动归档(archived_reason=unused_timeout),再上传/绑定同一张文件
  • 预期:不因“未使用的超期归档”触发全局判重拦截;仅当新条目被使用并进入 bound/used_complete 后才进入判重历史
  1. 签名 URL 鉴权与过期
  • 输入:非票据相关人员请求 POST /api/invoice-pool/:id/signed-url;或使用过期 URL
  • 预期:无权获取(403/PERM_001);过期访问失败;必要时写入 audit_logs
  1. 行政代上传授权链(owner ≠ uploaded_by)
  • 输入:行政为员工 A 上传票据(owner_employee_id=A, uploaded_by=行政)
  • 预期:
    • 有有效 delegation_grants(principal=A, delegate=行政, scope 含 reimbursement_submit)时允许上传
    • 无授权时拒绝(403/PERM_001)
  • 补充:hr/admin(持 system.users)可在无 delegation 的情况下代维护上传;所有代维护必须写入 audit_logs
  1. 敏感字段脱敏(masking)
  • 输入:读取/变更员工敏感字段(银行卡/身份证)并查看相关响应与审计
  • 预期:对外响应仅返回脱敏格式;audit_logs.extra.changes 仅包含脱敏前后快照(禁止明文)
  1. 代提交审批链解析(owner 基准)
  • 输入:行政为员工 A 代提交报销单(submitter=行政,owner=A)
  • 预期:首节点审批人按 A 的直属经理 解析(而非行政的直属经理);workflow_instances.context_data 包含 owner_id/department_id
  1. *_own 语义(owner 或 submitter)
  • 输入:行政查看/协助处理其代提交的单据;owner 编辑草稿/驳回单
  • 预期:当 owner_id=me OR submitter_id=me 时允许 view_own;draft/rejected 允许 edit_own;仅 draft 允许 delete_own;withdraw 仍需满足“审批人未处理前”等状态约束