
前阵子接手一个库存与财务对账的老项目业务那边天天喊“销售数据是准的但财务账面金额就是差几块钱”研发这边从SQL到缓存排查了一圈最后发现根本不是技术问题是业务模型从一开始就建歪了。重新梳理业务过程时我又把大学时学过的REA模型翻了出来用它把整个进货、销售、收付款流程重新建模了一遍很多纠缠不清的边界一下子变清晰了。很多做后端开发的朋友可能没听说过REA或者只在会计信息系统教材里扫过一眼。REA的全称是Resources-Events-Agents也就是“资源、事件、参与者”一套用本体论方式描述业务过程的建模方法。它既能用来做数据库表结构设计也能用来划分领域模型甚至和现在流行的事件溯源架构天然合拍。如果你正在为“业务和技术两张皮”头疼比如单据状态永远对不上、对账全靠手工 SQL 临时凑那这篇文章应该能给你一些不一样的思路。1. 当业务数据对不上账时我为什么重新翻出这个老模型1.1 从一次账实不符排查说起问题往往出在业务模型那个老项目的业务链路看起来很常规采购下单、供应商发货、仓库收货、财务付款销售下单、仓库发货、开票、收款。每张单都有自己的状态订单有“待审核、已确认、已完成”出库单有“待拣货、已出库、已取消”发票有“待开具、已开具、已核销”。表面上看每个状态机都自洽但串联起来问题就来了一张销售订单可以对应多张出库单一张出库单又可能部分开票部分收款采购那边还存在“货到了但发票没到票到了但钱没付”的错位。团队一开始的处理方式很原始在订单表上加了一堆冗余字段比如“已发货数量”“已结算金额”“剩余可退数量”每次发货、退款、开票都在多个表里来回更新。这种解法在业务量小的时候勉强能跑可一旦出现部分退货、折让、红冲、多币种结算冗余字段之间的约束就根本维护不住。最后的结果大家都猜得到业务说发了 100 件货财务说只确认了 80 件的收入两边都觉得自己没错错在系统“没记对账”。1.2 REA到底解决什么问题从会计信息系统走出来的通用业务建模方法REA模型最早是上世纪 80 年代一位会计信息系统学者提出的初衷很朴素当时的会计电算化系统本质上是把纸质凭证搬进电脑整个数据模型还是围绕“借”和“贷”两个方向设计的。学者们觉得与其模拟凭证不如直接描述业务本身的实质——一家企业到底拥有什么资源发生了哪些改变资源的事件这些事件由谁发起、谁受益。只要把这些记录清楚会计凭证反而是可以被派生出来的东西。这套思想后来逐渐走出会计领域变成一种通用的业务建模方法论。它最核心的要求是不要从“单据”出发要从“经济实质”出发。单据只是记录事件的载体真正有价值的是“哪个事件、动了哪项资源、谁在里面承担责任”。当你把这三件事用关系模型固定下来各种单据之间的对账关系就变得透明了。我当时重新看这个项目时才发现团队过去一直在给单据“打补丁”却从来没人问过这些单据背后的经济事件到底是什么。2. REA三要素逐层拆解从资源、事件、参与者到业务流程2.1 资源Resources先搞清楚“钱和货”到底指哪些东西REA里的资源并不是泛指一切数据而是指企业能够控制、并且希望通过经营过程获取或消耗的经济对象。最典型的就是现金、库存商品、原材料、固定资产也包括一些更抽象的东西比如应收账款、应付账款、预收款项。判断一个对象算不算REA资源有两个简单标准第一它是不是有限的、稀缺的第二企业能不能对它的流入流出做出计划和控制。举个例子“订单编号”本身不是资源因为订单可以无限生成不存在稀缺性但“库存商品”是资源因为每件货只有一个被卖掉就没了“应收账款”也是资源它代表着对客户的一笔债权企业会因为这笔权利而产生经济利益流入。识别资源时最容易犯的错是把“单据里的字段”当成资源比如把“订单金额”当成一项资源。其实订单金额只是销售事件的一个属性真正被消耗的资源是库存真正被创造的资源是应收账款或现金。2.2 事件Events一切改变资源状态的动作事件是REA模型的心脏。所谓经济事件就是会导致资源增加或减少的经营动作包括采购收货库存增加、销售发货库存减少、收到货款现金增加、应收减少、支付货款现金减少、应付减少等。REA特别强调事件的双向性在一次完整的经济交换中一方资源的流出必然对应另一方资源的流入。典型的销售业务包含两个互相关联的事件——销售事件货物流出企业和收款事件现金流入企业。实际业务系统里事件往往比单据更“细”。比如销售这个环节可能存在“下单”事件、“发货”事件、“签收”事件但从REA角度看真正影响资源的事件可能只有发货和收款。下单只是一个承诺并没有让库存资源发生转移。很多团队把“创建订单”和“扣减库存”绑在同一个操作里一旦出现支付失败或者超卖状态就乱了。如果从一开始就把“库存扣减事件”和“订单创建事件”分开建模这类问题会好处理得多。2.3 参与者Agents事件由谁发起、对谁负责参与者是指参与经济事件的个人或组织分为内部参与者和外部参与者。内部参与者是公司内部的部门或岗位比如采购员、仓管员、销售员外部参与者是客户、供应商、物流公司等。参与者与事件之间通常存在“发起”或“承担责任”的关系。在报销场景里员工是费用报销事件的发起者财务审核人是授权者这两个角色虽然都在系统里但权责完全不同。很多业务系统的权限设计其实就是在REA的参与者关系上做文章。比如“管货的不能管钱”反映到模型里就是仓库管理员不应该出现在收款事件的参与者列表里。如果你们的业务系统把参与者的权限直接固定在业务代码里一旦组织结构调整就要改代码那是建模时没有把参与者抽象出来。2.4 三个要素如何连成一幅业务全景图把资源、事件、参与者连接起来你就得到一张业务全景图。以一家小餐馆进货为例老板从供应商处采购一批蔬菜采购收货事件让“蔬菜”这项资源从供应商流向餐馆同时“现金”资源从餐馆流向供应商外部参与者是供应商内部参与者是采购员和出纳。整个过程围绕蔬菜和现金两条资源流展开中间由采购、收货、付款等事件衔接。这种图最大的好处是它能让你一眼看出“业务链条漏了哪一环”。比如很多系统只记录了进货事件却没有对应的付款事件结果财务永远不知道这笔钱到底付没付。REA强制要求每一次资源流出都必须有对应的资源流入这种约束天然逼着你把整个业务链画完整。3. 用REA重构一套进货-销售流程一步步建模演示3.1 场景定义一家贸易公司的一条完整业务链假设我们是一家做经销商批发生意的贸易公司最核心的一条业务链是向供应商采购商品、支付货款、向下游客户销售商品、收回货款。整个链路涉及六件事采购下单内部动作、供应商发货外部事件、仓库收货库存增加、支付货款现金减少、销售发货库存减少、收到货款现金增加。注意采购下单并不是一个经济事件因为下单时货还没到企业的资源没有发生任何变化。真正改变资源状态的是供应商发货和仓库收货其中仓库收货意味着企业获得了这批商品的控制权因此它才是需要建模的事件。同样销售订单创建也不是经济事件订单只是后续发货和收款这两个经济事件的前置信息。3.2 识别资源流与事件链先画链再建表我习惯先把资源流画成两条链货物链和资金链。货物链从供应商流向贸易公司再流向客户资金链则相反从客户流向贸易公司再流向供应商。每个资源流的节点上挂着对应的事件。资源流事件节点资源变化方向参与者货物链仓库收货库存商品增加供应商交付、仓管员接收货物链销售发货库存商品减少销售员发起、客户接收资金链收到货款银行存款增加、应收账款减少客户支付、出纳确认资金链支付货款银行存款减少、应付账款减少采购员发起、供应商收款这张表画完你会发现一个很有意思的点应收账款和应付账款并不是独立事件产生的它们是由货物事件和资金事件之间的“时间差”衍生出来的。如果发货的当天就收到钱系统中根本不需要存在应收账款这种资源。REA把这种衍生关系揭示出来以后你再去设计应收应付模块思路就会完全不一样。3.3 生成关系模型从REA到数据库表结构REA模型转成数据库设计时一般规则是每个经济事件一张表每类资源一张表每个参与者一张表然后用关联表表达事件与资源、事件与参与者之间的关系。以销售发货事件为例核心表可能长这样sales_shipment销售发货事件主表字段包括事件ID、客户ID、发货时间、关联订单号。inventory_item库存资源表包含商品ID、批次、库位、当前数量。shipment_item事件-资源关联表记录本次发货消耗了哪个批次库存、数量多少。receivable应收资源表记录这笔销售产生的债权。shipment_agent事件-参与者关联表记录发货员、客户、销售员。这里最关键的一个设计决策销售发货事件同时关联两份资源——库存的减少和应收款的增加。也就是说一个事件在关系模型里可以对应多行资源变动。很多人设计表结构时习惯把“发货单”和“应收单”分开建然后用程序同步两张单的状态一旦同步失败就产生脏数据。用REA思路这两个资源变动天然挂在同一个事件下要么一起成功要么一起失败就不存在同步问题。落实到代码上这通常意味着在一个事务里同时插入发货明细和应收明细然后用事件ID做关联。3.4 与科目分录对照REA不是要淘汰会计而是补上业务层有人可能会问REA和传统的会计科目、复式记账是不是冲突其实完全不冲突。REA描述的是业务事实层而会计科目是视图层。你可以把REA模型里的事件和资源变动映射成一条条会计分录。比如销售发货事件映射出来的分录是“借应收账款贷主营业务收入借主营业务成本贷库存商品”这些分录完全可以从REA模型中自动推导出来。明确了这层关系之后对账问题就降维了。以前对账是拿销售单据去对比财务凭证两边口径不同数据当然对不上。现在同一笔业务业务视图和财务视图都源于同一组事件只是在展示层做了不同的聚合对不平的概率会大大降低。我在那个老项目里重构之后第一件事就是把所有单据状态同步的问题全部停掉改成从事件表汇总出各类台账虽然前期改造工作量大但后续对账基本不再需要人肉介入。4. REA和事件溯源、领域建模的关系现代架构下REA的用武之地4.1 事件溯源架构与REA的共鸣最近几年事件溯源Event Sourcing在微服务和DDD社区里讨论很多。事件溯源的核心思想是不保存系统的当前状态而是保存一串不可变的事件当前状态由事件流回放得到。REA和事件溯源在哲学上惊人一致——两者都认为“事件才是业务事实的第一公民”状态只是事件的投影。很多团队实施事件溯源时最大的难题是判断“哪些操作值得作为事件保存”。如果事无巨细地记录鼠标点击、页面浏览事件流很快就会淹没在噪音里。REA恰好给出了一个筛选标准只有导致资源增减的经济事件才值得作为领域事件记录其他操作只是业务噪音。举个例子用户修改了订单里的收货地址这个操作要不要作为事件溯源的一个事件在REA视角下如果地址修改不影响任何资源的增减它就不算经济事件可以被放在订单聚合内部作为普通更新处理不必单独作为领域事件。4.2 用REA辅助领域建模聚合边界与业务不变量的来源做DDD时划分聚合边界往往靠直觉和经验容易把聚合切得过大或过碎。REA能提供一套相对客观的指导因为每个经济事件都关联着若干资源和参与者那么一个聚合的边界就可以围绕“一组必须保证一致性的相关事件与资源”来划定。比如“发货事件库存资源应收资源”三者必须在一个事务里变更它们就有很强的理由待在同一个聚合里而“客户主数据”虽然也是参与者信息但它不随每次发货变更就可以拆到独立的聚合中。业务不变量也可以从REA关系中直接推导。最常见的一个在任何时刻库存资源的当前值不能小于零。这个不变量在REA模型里体现为“库存消耗事件不能超出库存资源的存量”。我曾经在一个进销存系统里遇到超卖问题当时就是因为在代码里做了“先减库存后校验”如果先校验后在同一事务里扣减并且依赖数据库约束超卖根本不会发生。REA帮你想清楚了“资源是共享的、事件是并发的”所以你自然会去设置资源表的悲观锁或版本号而不是只在应用层做一次if判断。4.3 边界与误区REA不适合什么场景REA不是银弹它描述的是“经济资源的真实流动”所以它不太适合承载所有业务流程。比如审批流、任务流、状态机流转这些流程的主要目标是控制工作步骤的推进并不一定每步都在改变资源。你硬要把审批节点建模成经济事件只会让模型变得很别扭。另外REA也不负责权限控制。虽然参与者关系可以用来推导权限但实际系统里权限往往还涉及数据范围、临时授权、多级审批等复杂规则没必要全部塞进REA模型。我现在的做法是把REA作为核心领域模型负责呈现资源流动的事实审批流、权限、消息通知等作为外围功能通过事件监听器响应经济事件而不是混在同一个模型里。5. 落地REA时最容易踩的坑和我的实操建议5.1 坑一建模时只看单据不看经济实质这是最常见的错误。很多团队梳理流程时习惯把系统里已有的单据列出来然后给每张单据建一张表再给每张表加状态。这样做的结果就是模型跟着单据走单据之间靠状态机勾稽本质上还是“凭证思维”。REA要求你从事件出发反向审视单据到底是在记录什么事件。如果一张单据同时记录了多个事件比如“发货单”既记录了库存减少又记录了物流费用产生那就应该拆成多个事件而不是让一张表承载所有含义。我实际操作时有个笨办法拿现有单据把每个字段都问一遍“这描述的是哪个资源的哪个变化”。凡是答不上来的字段要么它只是冗余信息要么意味着某个事件被遗漏了。5.2 坑二资源定义过细或过粗资源粒度的把握直接决定了系统的灵活性。定义得太粗比如把所有库存都当成一项“存货”资源就无法区分不同批次、不同保质期的商品定义得太细比如把每个批号、每个仓位都单独建模事件关联的复杂度会爆炸。个人经验是资源粒度应该等于“业务上需要独立核算、独立追踪”的最小单元。如果业务要求按批次追溯那就把批次作为资源的关键属性如果不需要就别硬拆。还有一个常见问题是把“服务”也当成资源。服务本身不是有形资源但它会产生资源消耗和收入比如咨询服务消耗的是人工工时产出的是应收账款。这时可以把“劳务工时”或“服务能力”建模为资源或者干脆只建应收资源而不建模内部消耗。这取决于你是否需要核算项目的成本结构。5.3 坑三事件-资源关联维护不当事件与资源的关联需要同时记录“数量”和“金额”两个维度。有些团队只记金额不记数量比如收款事件只记录了收到多少钱却没有关联到具体是哪笔应收账款。一是销售部分收款、部分退款时无法精确冲销二是财务对账时无法区分这笔钱对应的是哪张订单。我的建议是所有货币类资源至少保留“金额”和“关联事件ID”两个字段库存类资源保留“数量”和“批次”信息。关联关系尽量在事件发生时确定不要在后续用定时任务去补。补数据的任务一旦出现延迟或失败对账就又要开始加班了。5.4 坑四把REA当成流程图来用REA模型看起来像流程图但它不是流程图。流程图的箭头表达的是“先后顺序”REA的关系表达的是“业务语义关联”。你会看到有人把REA图里的事件按时间顺序画成一条线然后试图在里面表达“如果库存不足则等待补货”这样的分支逻辑那就是用错了工具。REA擅长的是回答“系统要记录哪些事实”至于这些事件触发的流程规则应该交给状态机或者编排引擎去处理。所以落地时不要把REA图当成设计文档的唯一产物。我会把REA模型作为领域层的数据骨架在这个骨架上继续设计状态机、路由规则、消息事件。REA告诉你“有哪些业务事实”状态机告诉你“每个事实如何流转”两者职责不同配合起来才顺。5.5 我的落地清单与复盘体会如果让我重新梳理一次我会按下面这个清单推进先列出所有经济资源用稀缺性标准筛一遍和业务方确认哪些资源必须独立追踪。找出所有改变资源的事件务必补上对应的反向事件比如有“销售发货”就必有“收到货款”。为每个事件明确参与者和责任边界把权限需求整理出来。用资源流和资金流两条链验证模型是否闭环任何一个资源有增量无减量都要追查原因。将模型映射到数据库表结构和领域服务尽量让一个事件对应用一个事务边界。最后再补充流程状态机、权限、审批等外围设计。做这件事最花时间的不是写代码而是和业务方把“资源”“事件”的边界对齐。比如业务方口中的“退货”到底算销售事件的逆向事件还是一个新的换货事件如果算逆向事件是否需要原销售事件关联这些定义一旦确定后面所有关系表的设计就顺理成章了。我自己在这几个项目里最大的体会是REA并不是一个需要全量建完才能上线的重量级框架它更像一组“提问清单”。哪怕你只拿“资源-事件-参与者”这套问题去重新审一遍已有的表结构也能发现不少设计盲区。别指望一步到位挑一条最痛的链路试点比如销售到收款这条线先建模试试看跑通后再向采购、库存、成本等模块扩展。