去年我接手了一个内部代号叫“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给我的最大启发不是它的表结构多优雅,而是它用一个极其朴素的框架逼着所有人回答三个问题:你的资源是什么?你的事件是哪一条事实?参与这件事的人和组织是谁?这三个问题回答清楚,后面所有系统设计都只是工程执行。踩过几次坑之后,我现在接任何核心系统需求,都会先问一句:这里的业务变化,是不是已经在用事件来表达?如果答案是否定的,那后面一定会有一笔糊涂账在等着。