REA模型:以事件驱动重新定义企业业务建模与对账逻辑
做企业应用绕不开一个尴尬场景订单系统一切正常月底对账却怎么都对不上。我一开始的处理方式是翻存储过程、补接口、写临时SQL折腾了一周才找到根因原来是库存扣减和财务确认用的不是同一套业务口径。直到后来完整接触了REA模型Resource-Event-Agent资源-事件-代理我才意识到这类问题的本质是建模方式出了问题。这篇博文只围绕“rea”这三个字母展开把REA模型的思路、建模过程和我的踩坑经历完整说清楚适合正在做交易系统、库存系统、财务系统的设计师和开发者也适合第一次听说REA、想知道它到底怎么用的读者。我理解中的REA模型核心就一句话业务系统不应该围绕“对象”建模而应该围绕“事件”建模。这条思路解决了我上面提到的对账难题也让后续的扩展变得异常清晰。1. 从对账困境说起REA模型解决的是“业务过程”而不是“业务对象”1.1 “订单表”为什么总是差一条记录传统ER建模的三大痛点先说说我当年在的那个模拟项目。一套电商交易系统前端下单、库存扣减、支付回调、售后退款各自独立数据库里自然是orders、order_items、inventory、payments这几张表。单看每张表都没问题可一旦要回答“这笔订单到底让库存减少了多少”“退款应该回滚哪些数据”“某天的销售收入和资金流水为什么差了几块钱”就得把散落在多个模块里的代码逻辑全翻一遍。这种困境的根源在于传统ER建模先画实体再画关系本质上是在描述“业务对象”。而真实业务是过程同一个对象在不同时点有不同的状态和价值静态实体根本表达不了这种变化。具体来说有三个痛点时间维度丢失inventory表里只存当前库存量不存“哪次销售、哪个时间点、由谁触发”的变化过程。账面与实盘一旦不一致你只能拿当前值和期望值做比对很难倒查变化源头。因果链路断裂订单创建、出库、支付、退款这些动作相互影响但传统表结构里它们没有显式的因果连接。业务规则要求“先付款再出库”还是“先出库再付款”只存在于代码判断里。对账逻辑被迫写在应用层每一处金额核对、库存核对都要单独写逻辑换个业务场景就得重写一份口径还不一定一致。我后来意识到这不只是设计水平问题是“实体优先”这个出发点本身就容易走偏。REA模型把视角从“有什么东西”换成“发生了什么变化”正好补上这块短板。1.2 REA的核心思路一切业务都可以还原为“谁通过什么动作让什么资源发生了变化”REA模型最早由一位会计信息系统方向的研究者在八十年代初提出本意是解决企业信息系统与会计系统的统一建模问题。它把业务过程抽象成三个基础字这三个字拼起来就是标题里的reaResource资源企业关注其价值、需要管理其增减的事物。库存商品、现金、固定资产、积分余额都算资源。Event事件导致资源增减的行为。销售发货、采购入库、收款、退款、盘点调整都是事件。Agent代理事件的参与者包括企业内部代理运营人员、仓库管理员、出纳和外部代理客户、供应商。规则只有一条资源不会平白无故变化每次变化都由某个事件在某个时间点触发且必须有代理参与。这样一来数据库里任何一条资源记录都能沿着“哪个事件变动的、谁参与的”追问到底。对账不再是写一堆专项逻辑而是沿着事件链逐笔核验。用生活化点的类比去超市买东西收银台小票上会写明“什么商品、数量多少、单价多少、实收金额、收银员是谁”。小票上的每一行就是一个“事件”商品和现金是“资源”你和收银员是“代理”。超市不需要单独解释“为什么库存变少了”小票本身就是答案。REA模型就是把这种天然清晰的记账逻辑搬进业务流程设计里。2. 资源、事件、代理三大原语三个词各自承担什么职责2.1 事件是REA的中枢神经一切变化都由事件驱动REA模型里的核心对象不是资源是事件。资源只是事件作用的对象代理只是事件的参与者。一个业务过程往往是一串事件构成的“事件链”比如客户提交订单承诺事件锁定部分库存仓库拣货出库实物事件库存流出支付渠道收款资源事件资金流入售后发起退款逆事件资金流出、库存可能回补每相邻两个事件之间语义上存在因果关系。前一个事件为后一个事件提供了“依据”后一个事件对前一个事件产生“响应”。在表结构里事件之间可以用parent_event_id之类的字段关联形成一条完整链路。这个设计带来的直接好处是业务流程变更时不需要大改表结构只是调整事件的编排方式。我刚开始建事件表时犯过一个错总想着给事件区分类型开一张event_type字典表把“销售”“采购”“盘点”“调整”都塞进去。后来发现类型其实只是事件的属性核心还是“什么资源变动了多少、谁参与、何时发生”。过度分类反而让查询变得复杂因为不同事件类型后续加工逻辑是相同的都可以用“资源方向数量金额”来表达。2.2 资源与代理事件两侧的语义锚点事件不是孤立存在的它两侧分别锚定着资源和代理。这一点特别重要REA不是简单地把业务表改名而是把原来那些对象拆成“资源”和“代理”两类分别通过事件关联起来。Resource类型主要特征有价值、可计数、能区分增减方向。做建模时可以把“商品SKU”“现金账户”“优惠券额度”“仓储容量”都归为资源。注意资源的定义层级很灵活可以精细到某个SKU也可以粗放到某个品类取决于业务管理粒度。Agent类型主要特征能够发起或参与事件。内部Agent可以是运营、仓管、财务外部Agent可以是客户、供应商、物流商。数据库设计上Agent和具体的用户表、组织表并不冲突更像是一层业务角色的抽象层。举一个典型的双边事件销售发货。左侧是代理商客户获得商品资源右侧是企业获得应收资金。这两个动作不是两个事件而是同一个经济事件的“两个视角”。REA模型用“交换”关系表达这件事要求一个事件至少涉及两项资源流流出的是商品流入的是未来现金。这一点对财务自动记账特别有价值后面我会专门讲。2.3 关系类型的完整拼图经济交换、存量-流量、参与、授权再说清楚一点REA模型把对象间关系分成了四大类这个是我实际建模最受益的分类体系存量-流量关系Stock-Flow事件改变资源的量流入或流出。例如“出库事件减少库存资源”“收款事件增加资金资源”。参与关系Participation代理与事件的联系。例如“仓管员执行出库事件”“客户触发订单事件”。授权关系Authorization代理之间或者事件之间的批准链。例如“超过一定金额的退款需要财务主管授权授权本身也是一个事件”。交换关系Exchange一组事件组成一个完整的经济交换。例如“发货事件收款事件”构成一笔销售。这个分类的意义在于它逼着建模人把“审批”“授权”“调拨”这类动作也当成事件落库而不是藏在状态字段里。我在前面提到的对账问题很大程度就是退款审批过程只存在于工作流系统业务库没有记录结果财务侧无法核对“谁批准了这笔钱出去”。3. 电商订单场景的REA建模全流程从业务动词到双子表结构3.1 建模第一步不是画ER图是列业务动词清单我给不少同行的建议是REA建模一开始凡事先找动词不要找名词。当你说“用户下单”“仓库发货”“财务收款”的时候里面的“下单”“发货”“收款”就是候选事件。每个动词都要回答三个问题这个动作改变了哪种资源方向是流入还是流出谁发起谁参与谁有权批准发生后会产生什么新的业务状态我当时在一个电商项目里列出来的动词清单大致是提交订单、支付成功、发货出库、签收确认、申请退款、退款到账、售后退货、盘点盘盈、盘亏处理、库存锁定解锁。每一项都对应一个事件每个事件都在事件表里有一行记录。这里有个容易混淆的地方“提交订单”这个动作本身传统建模把它看作“生成一条订单记录”但在REA视角里它实际上是一个“承诺事件”。承诺事件不一定立刻改变实物资源但会改变“可用库存”的口径。我建议在库存管理上至少区分“物理库存”和“可用库存”前者由出库/入库类事件驱动后者由承诺/取消类事件驱动。把动词清单整理好后再根据事件之间的因果顺序连线最后形成的网络就是业务过程的骨架。画图工具用什么无所谓重要的是事件之间的依赖关系必须明确例如“退款到账”一定发生在“申请退款”之后但不必等待“售后退货”完成可以把两个事件分开记录、分开核算。3.2 事件表与事件行子表的双子表结构设计事件如何落库是我被问得最多的问题。我最终稳定使用的方案是“事件主表 事件行子表”的双子表结构类似于订单头和订单行的关系但语义换成通用事件模型。事件主表记录事件本身的信息核心字段如下字段说明示例event_id事件全局唯一IDEVT202408140001event_type事件类型编码SALE_ISSUE、PAYMENT_INevent_time事件发生时间2024-08-14 10:23:11agent_id发起或主要负责代理客户编号C10023parent_event_id上游触发事件EVT202408140032status事件状态DONE、VOID、PENDING事件主表只是壳真正的资源变动记录在事件行子表里字段说明示例line_id行IDL001event_id所属事件EVT202408140001resource_id资源IDSKU-8801flow_direction资源流向IN、OUTquantity变动数量2amount变动金额399.00unit计量单位件、元这样设计的最大好处是无论销售、采购、盘点还是退款都进入同一套事件模型。查询“某SKU的库存变化流水”只需要一条SQL对账也变成“从事件表里把某个时间段的某类事件聚合出来”逻辑完全统一。DDL大概长这样我简化了很多但核心结构就是两张表CREATE TABLE event_header ( event_id VARCHAR(32) PRIMARY KEY, event_type VARCHAR(32) NOT NULL, agent_id VARCHAR(32), parent_event_id VARCHAR(32), event_time TIMESTAMP NOT NULL, status VARCHAR(16) DEFAULT DONE ); CREATE TABLE event_line ( line_id VARCHAR(32) PRIMARY KEY, event_id VARCHAR(32) REFERENCES event_header(event_id), resource_id VARCHAR(32) NOT NULL, flow_direction CHAR(3) CHECK (flow_direction IN (IN, OUT)), quantity DECIMAL(18, 4), amount DECIMAL(18, 2), unit VARCHAR(16) );实际项目里还能加remark、source_system等字段但底子就这两张表足够了。资源当前值不要直接维护在一个“库存表”里而是通过事件聚合推导或者建一个物化视图定时刷新。这个取舍后面会再讲这里先记住一句话资源当前值是可推导的事件流是不可篡改的。3.3 与订单、支付、库存模块的对接event_id 成为贯穿各模块的公共主键双子表落库后还需要和原有的订单、支付、库存模块打通。我的做法是让event_id成为贯穿所有模块的公共标识订单表里的order_event_id关联订单提交事件支付流水表里的payment_event_id关联收款事件库存台账里的stock_change_event_id关联出库/入库事件。这一步做起来不难但收益很大。原先各模块自己维护状态现在通过event_id可以反查整条链路。比如账务发现某笔退款金额对不上就能从退款事件出发顺着parent_event_id找到原始支付事件再找到关联订单事件数据不一致发生在哪一段一目了然。在电商订单场景跑通REA后我最大的感受是系统的“状态”越来越少了取而代之的是“事件流”。订单状态、库存余额、账户余额都可以通过事件聚合算出来。虽然实现上需要一点查询优化但逻辑一致性和可追溯性提升了不止一个档次。这也为后面接财务、接审计打下了很好的基础。4. REA和现代架构的关系事件溯源、聚合边界与自动记账4.1 事件溯源与REA老模型和新思想的天然呼应这几年事件溯源Event Sourcing在微服务和DDD实践中越来越流行思路是“只保存业务事件通过重放事件重建状态”。我越用REA越觉得这两者本质上是一回事区别只是REA把事件的结构和语义规范得更严格而事件溯源更多在讨论基础设施层面的存储机制。把两者结合的一个非常自然的方式事件溯源的事件流里事件体按照REA的字段规范来设计resource_id、agent_id、flow_direction、quantity、amount作为标准字段。这样事件溯源系统的持久化事件天然就是一份可审计的业务流水。命令侧和查询侧分离后查询侧调用REA聚合视图就能得到当前库存、账户余额等状态。我实际搭过的某套交易核心就是这样做的写端只追加事件读端用物化视图维护汇总数据。由于事件不可变任何时刻都能重建任意时间点的业务快照这对财务审计是极大的便利。4.2 DDD聚合怎么切以“一次业务事件”为单位圈定边界使用REA建模还有一个直接收益就是能帮你决定DDD里的聚合边界到底怎么划。很多团队在领域建模时纠结“订单聚合到底要不要包含支付”“商品聚合要不要包含库存”其实回到REA思路就清楚了一次业务事件涉及的资源、代理、授权关系构成一个自然的聚合边界。例如“订单提交”这个事件涉及的资源是订单里的商品行和可用库存涉及的代理是客户涉及的数据对象是订单头和订单明细那这些对象就应该放在同一个聚合内。而“收款确认”事件虽然关联同一笔订单但它发生在不同的时间、由不同类型代理触发更适合作为独立聚合或通过领域事件异步协作。聚合划分的本质是确定事务边界和一致性边界。REA模型提供的“事件-资源-代理”视角比从对象关系出发去切聚合要直观得多也不容易把系统切成一个个贫血模型。4.3 REA驱动的自动记账从事件流推导借贷分录我后来做财务系统对接时发现REA模型最大的隐藏价值是能自动生成会计凭证。传统做法是业务侧在某个动作发生时写死借贷科目比如支付成功就贷“应收账款”借“银行存款”。问题在于这样的映射规则散落各处一旦口径调整就要改代码。在REA模型里经济事件天然包含“流出资源”和“流入资源”两个方向。把资源映射到会计科目把事件流向映射到借贷方向就能从事件流自动推导分录。以销售发货事件为例商品资源流出对应库存商品科目贷方发生额应收资源流入对应应收账款科目借方发生额退款事件则相反。这类规则是领域级的配置在资源-科目映射表里事件驱动引擎统一执行。这样财务凭证与业务事件严格同源对账报表直接由事件流聚合生成不再依赖业务代码里的离散逻辑。当然实际做自动记账还需要考虑计税、折扣分摊、多币种等问题但骨架是REA给的。我在某项目里把对账从“日终批量修复”提升到“实时事件核对”财务团队反馈的满意度变化非常大。5. 建模时最容易踩的坑以及我保留的几条实操建议5.1 三个常见误区把单据当事件、漏掉授权、不区分承诺事件先说说我自己和身边团队重复踩过的一些坑希望大家直接避过去。误区一把订单当事件。“订单创建”其实是一个行为产生的“结果”纸面单据本身不是事件动作才是。正确做法是把事件定义成“客户提交了订单”这个动词单据MO是事件的附属产物。这样区分的好处是作废、修改、履约都描述成不同的事件业务状态机就退化了能承载的流程变化更多。误区二漏掉授权关系。很多系统把审批流全部交给工作流引擎业务库只记录“审批通过”后的状态变化不记录审批过程本身。这违背了REA的授权关系原则。退款、调价、大额折扣、盘点差异调整这些动作都应该有对应的授权事件。否则真出事时你回答不了“这个操作是谁批准的、什么时间批准的”这类最基础的问题。误区三不区分承诺事件与执行事件。典型例子是库存锁定、优惠券冻结这类承诺动作不应该直接减少物理资源量而是应该占用“可用量”。如果混在一起一个订单最后取消库存回补逻辑就会写得非常别扭。5.2 三个我一直保留的实操建议从动词开始、能推导就不存字段、平滑迁移基于上面这些经验我总结了几条在团队里反复强调的建议按优先级排一下从动词清单开始而不是从ER图开始。建任何模块前先找业务里的动词把这些动词变成事件候选再根据“资源变动”来确认它们的定义是否清晰。这是成本最低的纠偏时机。能推导出来的值就不要落库成字段。库存余额、账户余额、某时间段收入这类聚合值都能从事件流推导。日常查询可以建物化视图或缓存但持久化的真相一定要只保留事件流。这样避免了多个“余额字段”口径冲突。存量系统不要一步到位迁移。如果项目里已经有传统表结构建议选一个对账敏感、流程闭环的子模块先按REA改造比如“库存盘点差异处理”或“退款流程”。跑通一个完整闭环后再逐步推广。我见过一次性推倒重来、拖了半年上不了线的案例太常见了。5.3 一个判断标准字段值是否能由其他事件推导最后补充一个很实用的判断标准我把它作为“建模是否正确”的试金石某个字段的值如果能从其他事件推导出来那它就不应该被当作独立字段存着应该作为派生值存在。例如订单金额可以从商品行事件聚合得出账户余额可以从收款/退款事件聚合得出。如果系统里出现了大量这种“本可推导却单独存储”的字段就说明事件边界划分还不够完整后续大概率会出现一致性问题。用这个标准回看很多老系统坏味道会特别明显订单表存了库存扣减状态、支付表存了对账结果、日志表存了业务结论。本质上都是把事件流打散到了静态字段里。结尾一点个人体会我做了几次REA建模后最明显的感受是系统的边界感和可追溯性都变强了。以前写代码遇到“用户状态和服务状态不知道谁本谁末”时总要反复拉扯而现在只要问一句“哪次事件产生了哪个状态”答案就清晰了状态本身反而成了次要的东西。最后再分享一个落地时的小技巧事件表的event_type命名别贪图简短建议直接按照“业务域_动作_资源方向”的格式例如sales_issued_out、payment_received_in、refund_sent_out。前期命名稍微长一点没关系后续做聚合查询和报表时你会发现这种命名带来的辨识度比一切注释都管用。如果你想进一步探索REA模型还可以往供应链计划、多租户会计核算、业财一体化这几个方向延伸每一项都能单独展开说不少细节先把这篇里的方法用起来后面再看哪里需要加深。