端到端业务系统案例:报销、审批与支付账务
用一条真实业务链,把对象建模、状态机、接口契约、幂等、对账、权限审计和生产修复接起来
阅读边界: 这页不是报销、审批或支付账务的领域专题替代品,而是一张工程案例页。它关心的是:当一笔费用从提交、审批、预算、支付、回单、凭证到归档走完时,系统边界怎样设计才不会在生产里散架。
一句话判断: 端到端业务系统的难点不在“每个模块都能跑”,而在跨模块状态、资金动作、账务证据、权限审计和异常修复能否形成一条可解释的链。
费用事实 / 报销单
审批流
预算占用
支付申请
支付执行
回单回写
凭证 / 账务
归档 / 审计
异常补偿
| 阶段 | 核心事实 | 工程风险 | 最低设计要求 |
| 费用事实 | 票据、附件、项目、人员、供应商、收款账户 | 事实只存在图片或备注里,后续无法校验 | 关键事实结构化,附件有哈希、版本和来源链 |
| 审批流 | 谁在什么权限下同意了什么金额和用途 | 审批状态和业务状态分裂 | 审批结果只推进允许的业务状态,不直接替代支付或记账结果 |
| 预算占用 | 预算指标、项目额度、占用金额、释放规则 | 审批通过但预算未占,或驳回后预算未释放 | 占用、释放、撤销和重提都要有明确幂等键 |
| 支付执行 | 支付指令、银行流水、回单、失败原因 | 超时未知、重复提交、回调延迟、状态悬挂 | 支付订单状态机、外部查询补偿、重复提交防护 |
| 账务凭证 | 会计分录、凭证号、规则版本、来源单据 | 钱付了但凭证缺失,或凭证无法解释来源 | 账务分录可追到业务单、支付结果和规则版本 |
| 归档审计 | 附件、审批轨迹、回单、凭证、修复记录 | 事后追问时只剩导出文件和聊天记录 | 形成不可随意覆盖的证据包和审计事件 |
expense_fact
费用事实,承接票据、合同、行程、项目、人员和附件来源,回答“这笔钱为什么发生”。
reimbursement_order
报销主单,负责金额、收款人、费用归属、状态和跨对象关联,不直接承包所有附件与审批明细。
workflow_instance
审批实例,记录节点、参与人、动作、意见和权限快照,输出审批结论但不伪装成业务终态。
budget_hold
预算占用对象,记录指标、额度、占用、释放、冲销和失败原因,是预算正确性的核心账本。
payment_order
支付订单,负责外部资金动作的幂等、状态、回调、查询和重试,不让报销单直接调用银行接口。
payment_result
支付结果,保存银行或支付通道返回的权威结果、回单、错误码和查询时间线。
ledger_entry
账务分录和凭证来源链,记录业务单、支付结果、会计科目、规则版本和凭证号。
audit_event
审计事件,记录每一次提交、审批、支付、修复、重放和人工操作,让事后追问有证据。
draft
submitted
approving
rejected
approved
budget_held
payment_pending
paying
paid
accounting_posted
archived
failed
cancelled
| 状态边界 | 允许动作 | 禁止偷懒 |
| 审批中 | 撤回、驳回、转交、加签、审批通过 | 不能提前生成真实支付指令;最多做预校验 |
| 已审批 | 预算占用、支付申请创建、重新校验权限和额度 | 不能把“审批通过”展示成“已付款” |
| 预算已占用 | 创建支付订单、释放预算、进入异常队列 | 不能在支付失败后忘记释放或保留占用原因 |
| 支付中 | 等待回调、主动查询、超时转人工、幂等重试 | 不能用简单失败覆盖“外部结果未知” |
| 已支付 | 生成凭证、回单归档、发起对账 | 不能再允许普通撤回或驳回,只能走冲正 / 订正流程 |
| 已记账 / 已归档 | 订正、冲正、补充归档、审计查询 | 不能直接改历史金额或覆盖原附件 |
| 工程缝 | 典型坏味道 | 更稳的设计 |
| 审批状态 vs 业务状态 | 审批引擎显示通过,但业务单仍不可支付,前台解释不清 | 审批结果是事件,业务单按状态机消费事件并记录失败原因 |
| 预算占用 / 释放 | 驳回、撤回、支付失败后额度卡死 | 预算占用做成独立对象,所有释放动作可幂等、可对账 |
| 支付幂等 | 页面重复点击、接口超时重试导致重复付款 | 以业务单号、支付批次、收款账户、金额构造幂等键,并隔离外部未知态 |
| 账务来源链 | 凭证生成了,但财务追不到是哪张单、哪次支付、哪个规则版本 | 凭证保存来源对象、规则版本、操作人和生成批次 |
| 附件 / 归档完整性 | 附件被覆盖、导出包和线上数据不一致 | 附件版本化、哈希校验、归档批次和导出快照都要入审计 |
| 权限和审计证据 | 只知道“系统自动处理”,不知道当时谁有权、为什么能操作 | 关键动作保存 actor、role、permission_snapshot、policy_version 和审批证据 |
| 故障场景 | 如果没设计会怎样 | 处理剧本 |
| 审批通过但预算占用失败 | 用户以为能付,财务端无法继续 | 业务单进入 approved 后的待处理异常,展示预算失败原因,可重试或退回补正 |
| 预算已占但支付失败 | 额度长期被锁,报销单卡在中间态 | 区分可重试失败、外部未知、明确失败;明确失败释放预算,未知态先查权威结果 |
| 支付成功但回调延迟 | 系统重复发起支付,造成二次付款风险 | 支付中状态先主动查询外部结果,禁止在未知窗口内新建同幂等键支付 |
| 支付重复提交 | 同一笔报销被多次付款 | 支付订单唯一约束、幂等表、通道侧请求号和人工复核共同兜底 |
| 凭证生成失败 | 钱已经出去,但账务系统缺凭证 | 支付成功后建立待记账队列,凭证生成失败可重试、可补录、可对账 |
| 下游状态已变后用户撤回 | 撤回动作把支付或账务历史直接抹掉 | 超过边界后禁止普通撤回,改走红冲、退款、订正或人工审批流程 |
| 字段 | 用途 | 不要缺在哪里 |
business_id / trace_id | 把报销、审批、预算、支付、账务串成一条排障线 | 日志、消息、API 请求、后台任务、人工修复单 |
workflow_instance_id | 定位审批版本、节点、操作者和权限快照 | 审批回调、业务单状态变更、审计事件 |
payment_order_id | 定位资金动作、通道请求和外部权威结果 | 支付请求、支付回调、主动查询、对账任务 |
ledger_entry_id | 定位凭证、分录、规则版本和账务修复 | 自动凭证、账务对账、关账检查 |
status_timeline | 解释什么时候从哪个状态变到哪个状态 | 前台查询、客服解释、审计复盘、事故分析 |
retry_count / repair_batch_id | 区分自动重试、人工修复和批量补偿 | 异步任务、对账补偿、生产数据修复 |
API 契约
每个跨系统调用是否定义请求号、幂等键、状态码、错误分类、超时语义和回调验签。
数据契约
报销、审批、预算、支付、凭证之间是否有稳定字段、枚举版本和兼容策略。
幂等与补偿
重复提交、重复消息、重复回调、重复修复是否不会产生二次资金或账务副作用。
权限与审计
关键动作是否保存操作者、角色、权限快照、审批证据和人工修复理由。
发布与回滚
状态机、凭证规则、支付通道变更是否有灰度、回滚、双写对账和旧数据兼容方案。
生产修复
是否有 dry-run、影响范围、审批、执行记录、复核对账和禁止直接改终态的规则。
| 交付证据 | 必须交出什么 | 缺失后的风险 |
| 链路状态图 | 报销单、审批实例、预算占用、支付订单、支付结果、账务凭证和归档状态之间的推进关系、终态和异常态。 | 模块各自正确,但跨模块状态解释不通,客服、财务和研发各说一套。 |
| 资金动作清单 | 哪些动作会真正触发付款、退款、冲正、预算释放或凭证生成;每个动作的幂等键、权限和审批要求。 | 重复点击、重试、补偿或人工修复可能产生二次资金副作用。 |
| 权威来源说明 | 审批结论、预算余额、支付结果、银行回单、账务凭证分别以哪个系统或对象为权威。 | 发生差异时不知道听谁的,补偿动作容易把错误状态继续扩散。 |
| 异常剧本与 owner | 预算失败、支付未知、重复支付、凭证失败、附件缺失、权限争议等场景的处理队列、SLA、owner 和升级路径。 | 异常只靠研发临时查库,业务等待时间不可控,责任边界模糊。 |
| 审计与归档包 | 提交事实、审批轨迹、权限快照、支付回单、账务凭证、修复记录、导出快照和附件哈希。 | 事后审计只能靠聊天记录和截图,无法证明当时谁有权、钱为什么付、凭证从哪里来。 |
| 上线门禁 | 最低要求 | 失败时动作 |
| 状态机门禁 | 所有终态、可补偿态、人工介入态和禁止回退边界都经过评审,并有测试样例覆盖。 | 阻断上线,先补状态迁移表和失败路径用例。 |
| 支付幂等门禁 | 支付请求号、业务幂等键、通道请求号、回调验签和主动查询策略可被复核。 | 不允许接入真实支付通道,只能停留在沙箱或灰度环境。 |
| 对账闭环门禁 | 业务单、支付结果、银行流水、账务凭证和预算占用有定时对账、差异分类和处理队列。 | 不能宣告闭环,必须补对账任务和差异 owner。 |
| 权限审计门禁 | 高风险动作有角色、权限快照、审批证据、操作原因和不可覆盖审计事件。 | 冻结高风险入口,先补权限证据和审计写入。 |
| 修复演练门禁 | 至少演练一条支付未知、一条凭证失败、一条预算卡死的 dry-run 修复路径,并留复核记录。 | 上线范围收窄或推迟,避免生产第一次遇到异常时靠手工救火。 |