资讯动态

REA建模实战:用资源、事件、参与者统一业务与财务口径

发布时间:2026/10/21 14:26:59 来源:尧图企业网站定制
去年我接手了一个内部代号叫“REA”的项目。项目本身一句话就能说清楚把公司散落多年的订单、库存、财务三套系统整合成一套口径统一的业务底座。真正值得复盘的是我们在这个项目里把建模框架也选成了REA——Resources、Events、Agents也就是资源、事件、参与者。做业务系统的同学应该都有这种体会订单系统说“订单已创建”库存系统说“物料已出库”财务系统说“收入未确认”三套系统各说各话后端联调全靠口头对齐报表口径全靠人工核对。REA框架最大的价值就是提供了一套统一的业务语言任何业务都还原成“谁参与者对什么资源做了什么事件”。不搞花活不炫技就是把业务最底层的逻辑理清楚。这篇文章我会从原理讲到落地包括我们设计的表结构、建模步骤、一段库存核算的SQL以及整个过程中踩过的几个大坑。适合正在做中台、供应链、财务一体化改造的业务架构师和后端开发者。如果你只是刚开始接触业务建模也能看我会把基础概念用最小白的例子解释清楚。1. REA到底在讲什么1.1 三个原语资源、事件、参与者先说资源。资源在REA里特指“在业务中被交换、生产、消耗或使用的有价值对象”。商品是资源现金是资源库存储备是资源甚至客户的积分额度也是资源。判断标准很简单它在业务规则里会不会被增加、减少、转移如果会它就是资源如果不会它可能只是一个属性或者一个参与者的标签。事件是REA里最核心也最容易理解错的概念。它不是系统的操作日志而是“业务事实的发生”。比如“张三用支付宝付款买了十斤苹果”这是事件。“服务器在18点03分收到POST请求”这不是业务事件这是技术日志。事件有方向资源的增加、减少、交换都是事件的结果。拿买菜举例我去摊位买了二斤排骨这个“买菜”事件里排骨是资源我是外部参与者摊主是内部参与者现金是从我流向摊主的资源。就这么简单一个模型覆盖了买卖双方的完整视角。参与者是发起事件的人或组织。客户、供应商、员工、仓库管理员、物流公司都属于参与者。REA里有一个很重要的原则要把“内部参与者”和“外部参与者”分开。内部参与者是你自己组织里的人外部参与者是客户、供应商这些有业务往来的对象。为什么要分开因为权限和职责不同内部参与者对事件负有操作责任外部参与者只决定事件的属主关系。后面做连接表的时候这个区分会直接体现在字段设计上。1.2 传统建模的问题出在哪传统做法是画E-R图定义订单表、库存表、科目表。看起来清晰实际上一旦流程变长问题就来了。订单表里存的是“当前状态”比如order_status从pending变成shipped状态覆盖了历史你根本不知道它是哪天从pending变成shipped的也不知道是谁操作了这个变更。更麻烦的是如果业务规则变化“已发货”的定义可能需要拆分出“已出库”和“已签收”存量数据怎么办只能写一堆乱七八糟的迁移脚本。库存表更典型。每天晚上定时任务的库存快照覆盖前一天的数据盘点和追溯只能靠另外一套日志系统。时间一长业务日志和系统日志混在一起谁也说不清哪个是可信的数据源。我们之前查一笔历史库存异常要从订单日志、WMS操作日志、接口调用日志三个地方拼线索耗费了整整两天。财务系统反而是相对“稳”的。财务上有科目余额表每一笔记账凭证更新一次余额。余额是结果凭证是原因。结果会覆盖原因但凭证不会丢。所以财务系统出了差错永远可以从凭证重新推一遍余额。REA的思路和财务记账一脉相承业务系统也应该像记账一样只记录“发生了什么”而不是只记录“现在是什么”。这是整个REA建模的底层信念。1.3 为什么REA能打通业务与财务理解了“结果会覆盖原因”这个问题就能明白REA打通业务和财务的原理。REA把业务过程建模成事件序列事件在改变资源状态的同时本身天然可推导财务影响。也就是说业务的事实和财务的事实是同一份事实不需要再单独做一套财务解释。我们以前做对账是拿业务订单表和财务凭证表做映射映射规则散落在几十个存储过程里。REA改造之后财务人员可以直接查业务事件表“哪些发运事件已经发生但对应的收款事件还没发生”这一行数据就是应收账款。业务语言和财务语言第一次对齐了连带着把“两套账”的问题也解决了。这个收益不是省了多少人力而是从根源上消除了数据漂移的可能性。2. 从订单到账本REA模型的完整推演2.1 一个最小闭环长什么样用一个标准的电商订单履约闭环来拆解。整个流程涉及四个关键事件客户下单、仓库出库、客户付款、供应商补货。拆成REA事件大概是这样客户下单生成销售事件资源是“承诺库存”方向是减少库存被占用。仓库出库生成发运事件资源是“可售库存”方向是减少。客户付款生成收款事件资源是“现金”方向是增加。供应商补货生成采购收货事件资源是“库存”方向是增加。这四个事件之间不是孤立的。付款事件要关联到对应的发运事件发运事件要关联到对应的销售事件采购事件和销售事件之间可能通过“批次”“库位”间接关联。REA建模的精髓在于不只记录每个事件还要记录事件之间的因果链。因果链一旦断了对账就变成猜谜。2.2 两条关键连接规则事件和资源的关系是多对多。一个销售事件可能包含三种商品一个资源也可能被多个事件触及比如同一批库存既被盘点事件触及又被出库事件触及。事件和参与者的关系也是多对多一个客户可以参与多个销售事件一个销售事件也会包含多个内部参与者比如经手的销售员和审批人。这里有一个容易踩的坑连接表不能无脑建。你需要在连接表上补充“角色”字段。比如event_agent这张表除了事件ID和参与者ID还要有一个role字段表示这个参与者在这件事里是“买家”还是“经办人”还是“审批人”。如果不加角色字段后面想查“某客户的订单是由哪个员工处理的”就得靠猜。这个角色字段本质上就是业务关系的事实。2.3 从模型到财务报表我直接给一段查询应收账款的逻辑应收等于已完成发运事件所对应的销售金额减去已收款事件的金额。在REA模型下这个查询就是两个sum相减不需要join任何凭证表。更妙的是资产负债表的核心项——存货、现金、应收、应付——都能从同一套事件表通过不同的过滤条件推导出来。这意味着什么呢财务系统的“记账凭证”可以不再是独立数据而是业务事件的一种视图。这一点让不少财务同事一开始很不适应但一旦理解“事件就是凭证的一种形态”对账工作会大幅减轻。根源上消除两套数据的漂移比事后无限次对账要有效得多。3. 实操REA在库存核算系统中的落地3.1 表结构设计我们用的是PostgreSQL。核心表就四张agents、resources、events外加两张连接表event_resource、event_agent。这是REA模型最经典的落地形态。agents表存参与者id、name、agent_type内部/外部、以及可扩展的维度字段。resources表存资源id、name、resource_type商品/现金/库存/额度、unit计量单位。events表存事件id、event_type销售/发运/收款/采购/盘点、occurred_at业务发生时间、业务单号。event_resource表存事件与资源的关系event_id、resource_id、quantity_change正数代表增加负数代表减少、unit_price。event_agent表存事件与参与者的关系event_id、agent_id、role。这里最重要的设计决策是events表和event_resource表只允许插入不允许更新和删除。库存盘点错了怎么办当天复盘再插入一个盘点事件而不是去修改原来的事件。这是事件溯源的基本纪律也是整个系统不会烂掉的底线。如果有同事提需求说“帮我把这条数据的数量改一下”一定要拒绝改成“新增一条冲销事件”。3.2 建模五步法第一步圈资源。把业务里所有有价值、会被增减转移的对象列出来标上计量单位。第二步识别事件。逐条过业务流程凡是改变资源数量或价值的动作都标记为事件。第三步定参与者。区分内外明确每个事件的参与角色。第四步建连接。把事件和资源、事件和参与者连起来保证每个事件至少有一个资源变动和一个参与者。第五步校验模型。反过来问自己任何一个资源数量的变化都能找到对应事件吗任何一个参与者发起的动作都有事件记录吗两个问题都回答“能”模型就基本成立。这五步看似简单实际上每一次建模讨论到最后都会变成业务规则的辩论。比如“取消订单”算不算一个事件我们内部辩论过很久。最后结论是不算新事件它应该作为原销售事件的一个状态变更记录同时引入一个“订单取消”的资源变动类型来处理。这种辩论恰恰是REA建模最有价值的地方它逼着团队把业务规则说清楚而不是让规则藏在流程图的泳道里。3.3 库存余额查询库存余额不用维护“当前库存”字段直接查事件就能得到。核心SQL示意如下select r.name as resource_name, sum(er.quantity_change) as current_balance from event_resource er join resources r on er.resource_id r.id join events e on er.event_id e.id where e.occurred_at now() group by r.name;这段SQL背后的思想是当前余额等于所有历史事件的代数和。第一次上线时需要把旧系统的存量库存做成一个“期初事件”插入进来之后的每一天系统余额都能自动演进。我们实际运行时给event_resource表加了索引并按事件时间做了分区几百万事件量下查询依然在毫秒级。如果哪天发现余额不对不是改数据而是补齐缺失的事件这就把“修数据”变成了“补事实”。4. REA与DDD、事件溯源的关系4.1 给DDD补上业务语义层很多团队现在做领域驱动设计用聚合、实体、值对象表达业务行为这是好东西。但DDD本身有一个空白它对“业务行为的财务后果”缺乏统一表达。一个聚合可以很好地处理“订单创建”的逻辑但“这笔订单对库存和现金分别意味着什么”DDD没有自然答案。REA在这个位置补上了。具体做法是一个DDD聚合对应若干个REA事件。订单聚合创建时同时抛出“销售事件”和“承诺库存减少事件”两个领域事件支付聚合完成后抛出“收款事件”。这样既享受DDD带来的代码组织能力又享受REA带来的业务口径统一。二者不是竞争关系REA更像是DDD之上的一层业务语义层。4.2 与事件溯源天然契合事件溯源是一种数据存储模式不存当前状态只存状态变更序列。REA的事件恰好是天然的“业务事实序列”两者结合非常顺。我们最终没有引入复杂的事件溯源框架而是直接用了一张append-only的events表配合领域事件发布。数据量上来后通过事件回放就能重建任意时间点的业务状态这在审计场景里特别好用。这里有个重要区别得提醒事件溯源里的“事件”可以是技术性的意图比如“下单请求已接收”REA的“事件”必须是业务事实比如“货款已收讫”。如果混用会把技术噪音带进业务模型。我们在events表上强制区分了这两种类型技术事件只进日志不业务事件表。这个分类一旦模糊模型的纯净度就没了。4.3 团队落地要点落地最大的阻力不是技术是习惯迁移。我们开了三轮建模工作坊要求业务、产品、开发一起画REA图。画图过程极其痛苦因为每个人对“事件”的理解都不同。财务觉得“记账”才叫事件运营觉得“用户点击”就叫事件仓库觉得“货架移动”才算事件。最后统一了一个规则凡是会改变资源数量或所有权的事件才进入模型。资源不动就不是业务事件。规则确立后建模速度快了很多争议大幅减少。术语表也特别重要。我们发现“商品”“产品”“物料”“SKU”在不同系统里指代几乎相同的东西但用词混乱导致连接表建得乱七八糟。后来强制统一用“商品”作为唯一资源名并在资源表里加了一个别名映射字段才把问题压下去。术语不统一建出来的模型就是一堆带伤的表后面每一个查询都要为此付出代价。5. 常见问题与避坑实录5.1 资源误判常见错误之一是把所有名词都当成资源。“客户联系方式”不是资源它只是参与者属性“优惠券模板”不是资源但“已经发放给客户的优惠券实例”是资源。判定的标准只有一个在业务规则中它是否会被生产、消耗、转移或兑换。这个标准体现在代码上就是它是否出现在event_resource表的quantity_change字段里。出现了就是资源没出现就不是简单粗暴但可靠。5.2 事件和状态纠缠不清还有一种错误是把状态当事件。有人设计了一张“状态变更事件表”记录order_status从1改成2。这是错的。事件必须是“发生了什么”不是“变成了什么”。正确做法是记录“订单支付成功”这个事实至于该订单当前是“已支付”状态那是从事件推导出来的查询结果。这样做的价值在于未来如果需要同时支持“已支付但已取消”的历史状态不用改历史数据只要重新推导即可。5.3 多对多连接的性能问题REA模型多对多连接一旦数据量大连表查询容易越写越重。我们的应对方案有两个。第一event_resource表按事件时间做分区配合资源ID的索引常规查询走索引就能覆盖。第二对于高频聚合查询比如按天汇总库存用物化视图预先计算。REA的严谨性体现在底层的原始事件上物化视图只是性能优化不会破坏模型的一致性。千万别为了性能去update事件那等于把账本重新变成了状态表。5.4 问题速查表症状原因解决办法业务口径和财务口径对不上没有用事件统一事实把业务过程和财务影响建模到同一事件表状态字段太多历史无法追溯用状态覆盖事实改为append-only事件状态由事件推导模型复杂难以推广把非资源也建模成资源严格用“是否增减转移”判断资源查询性能不足多对多连接无索引分区加上物化视图团队概念混乱术语不统一建立资源唯一命名和别名映射做这个REA项目之前我一度以为业务系统复杂是因为功能多、流程长。做完之后我才意识到复杂是因为团队对“业务事实”的定义不统一。REA给我的最大启发不是它的表结构多优雅而是它用一个极其朴素的框架逼着所有人回答三个问题你的资源是什么你的事件是哪一条事实参与这件事的人和组织是谁这三个问题回答清楚后面所有系统设计都只是工程执行。踩过几次坑之后我现在接任何核心系统需求都会先问一句这里的业务变化是不是已经在用事件来表达如果答案是否定的那后面一定会有一笔糊涂账在等着。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑