如果你最近在技术群里看到 rea 这三个字母,第一反应多半是:又有什么网络热词火了?或者,哪个前端框架名字只打了一半?在我熟悉的业务建模圈子里,REA 其实是 Resource-Event-Agent 的缩写,翻成大白话就是“资源-事件-代理”。它不是什么新出的开源工具,而是一套存在了很多年的语义建模框架,专门用来描述一个组织到底是怎么创造、交换和使用经济价值的。我第一次认真看 REA,是被一张发工资的事件图吸引的:整张图只有三个核心概念,却把我一直头疼的“订单、库存、财务口径对不上”的问题讲得明明白白。所以这篇内容,我想把 REA 的核心逻辑、建模步骤、落地表结构和常见坑一起整理出来,给做企业系统、数据中台、财务集成的朋友一个可以直接拿去用的思考框架。
1. rea 是什么:三个关键词组成一张业务底稿
1.1 资源:先搞清楚什么值得被持续追踪
资源是 REA 里最容易理解,也最容易被理解偏的一个概念。简单说,资源就是“企业认为有价值、并且愿意为它分配控制权的东西”。常见的有现金、商品、原材料、服务、知识产权,也包括员工提供的劳动。一个东西值不值得被建模成资源,我一般看三个条件:第一,它对参与交易的双方都有真实价值;第二,它能被某个代理控制或转移;第三,它的数量、归属、状态变化是可以用事件明确记录的。满足这三个条件,才算一个合格的资源。
有了这个判断,很多模糊的实体就能快速归类。比如有人想把“供应商”也建成资源,这其实有点别扭——供应商是交易对手方,是代理,不是被交换的标的物。你把代理当资源建,后面统计资源增减时逻辑就会乱。反过来,也有人想把“客户满意度”建成资源,但满意度不是一次性事件能明确增减的东西,硬建进去只会让表结构变得空泛。实际操作时,我习惯和业务方逐个核对资源清单,反复问一句:这个东西的控制权真的会在不同主体之间转移吗?如果会,那它大概率是资源;如果它只是一段状态描述,那它很可能只是某个事件或代理的属性。
1.2 事件:记录真实业务动作,而不是账本上的凭证
事件是 REA 的灵魂。一次采购确认、一次收货、一次付款、一次退货,都是事件。每个事件都发生在具体时间点,并且会导致一个或多个资源流入或流出。REA 通常把流入叫增量事件,把流出叫减量事件。一个交易动作可以同时产生一种资源的增量和另一种资源的减量,比如用现金买商品:现金减少、商品增加,这对应两个事件,但都源于同一笔交易。
为什么强调“事件”而不是“记账凭证”?因为凭证是会计系统为了人工记录方便而设计的抽象,一张凭证可以塞进多个业务动作,也可能被人为拆分成好几张。事件才是业务真实发生的动作序列,自带时间、参与者、涉及资源、数量、方向,这套信息比借贷分录完整得多。做系统设计时,事件往往也是天然的幂等边界:同一个事件重复推送到消费端,只需要按事件唯一编号去重,后面的资源更新不会出现重复累加。我见过不少对账事故,根源就是各系统拿凭证号、单据号、流水号互相映射,映射出偏差后谁都说不清真正发生了什么。如果底层一开始就用事件,这套问题的复杂度会小很多。
1.3 代理:人、部门、系统,只要扮演角色就算
代理是参与事件的主体,可以是一个人、一个部门、一家公司,也可以是一套自动化系统。在 REA 模型里,一个事件至少要关联两类代理,一类代表资源流出方,一类代表资源流入方;如果事件是内部资源调拨,则通常涉及一个发起代理和一个接收代理。这也解释了为什么 REA 特别适合做跨主体协同:每一笔资源变动都能回答“谁给谁、基于什么动作”,而不是只有一维金额。
这里要特别提醒一点:建模关心的是“角色”,不是组织架构。同一个员工既可以是采购申请人,也可以是收货确认人,还可以是资产领用人。在物理表里不要简单写一个“员工ID”,更好的做法是“内部代理ID+角色类型”的组合。这样未来组织调整或岗位合并时,业务语义不会跟着部门撤销一起失效。换句话说,你维护的是业务事实,而不是一张随时变化的组织花名册。
2. 发工资这个例子,能解释清楚 REA 的八成逻辑
2.1 传统借贷分录到底缺了什么
假设某公司要发当月工资 50 万。传统会计会做一个分录:借记“应付职工薪酬”,贷记“银行存款”。这个分录干净、平衡,但它丢掉的信息量非常大:这 50 万对应哪些员工?他们提供了多少小时劳动?这些劳动算不算公司获得的资源?从分录里只能看到钱少了,看不到员工劳动这个“资源”其实流入了公司。对于需要按项目、按部门、按产品线核算人力成本的企业来说,这个维度缺失是很要命的。
很多团队为了补上这道缺口,会在凭证摘要里写“X月工资”,在辅助核算里挂部门,甚至单独维护一套 Excel 记录每个人的工时。结果就是:财务系统知道钱付了,业务系统知道劳动发生了,两套系统各说各话,月底对账时靠人工扯半天。问题不在工具,而在建模时一开始没有给“劳动服务”一个合适的位置。
2.2 用 REA 把发工资重讲一遍
用 REA 建模就很直接。公司付出的资源是银行存款,发生的是一次减量事件;换回来的资源是员工提供的劳动服务,发生的是一次增量事件。参与的代理有两类:员工是外部代理,公司是内部代理。事实是:在某个月末,公司和员工之间发生了一组关联事件——劳动服务流入公司,工资资金流出公司。这两个事件不一定在同一秒完成,但月度结算场景里可以把它们建模为一组相互引用的事件。
这么建模最大的好处是显式。把劳动服务也建为资源后,人力成本的归集就有了自然的载体。以后想算“某项目投入了多少人力成本”,不需要再去逆推工资表,直接查劳动服务资源关联的事件流,按事件携带的项目维度聚合就行。很多团队想引入 REA,就是冲着这种“一个事实追到底”的能力来的。
2.3 一张最小可落地的表结构
如果只做一套最基础的表,我会先建三张:资源表、事件表、事件资源关联表。
CREATE TABLE rea_resource ( resource_id BIGINT PRIMARY KEY, resource_name VARCHAR(128), resource_type VARCHAR(64) ); CREATE TABLE rea_event ( event_id BIGINT PRIMARY KEY, event_type VARCHAR(64), happened_at TIMESTAMP, internal_agent BIGINT, external_agent BIGINT, source_note VARCHAR(255) ); CREATE TABLE rea_event_resource_flow ( flow_id BIGINT PRIMARY KEY, event_id BIGINT, resource_id BIGINT, direction CHAR(1), quantity DECIMAL(18,4), unit VARCHAR(32), monetary_amt DECIMAL(18,2) );资源表定义被追踪的对象,事件表记录业务动作和参与代理,关联表记录每个事件导致哪些资源发生增减。发工资时,插入两条事件资源流:一条是劳动服务的增量和一条是银行存款的减量,再通过事件表里的内部代理、外部代理字段判断参与方。后续不管要算现金支出、人力成本还是按段时间的资源流水,都可以在这套结构上直接聚合。相比传统“科目+辅助核算”,资源维度是完全开放的,新增一种资源就是插一行,不需要调整科目树的层级。
3. 从借贷记账到 REA:数字化协同需要一张“事实网”,不是一张“凭证表”
3.1 借贷记账法是纸张时代的优秀产物
我不反对借贷记账法。复式记账在历史上是巨大进步,每笔分录都记录两个方向,天然有平衡校验,几百年来靠它撑起了整个商业世界的核算体系。但它的设计前提是:交易信息要压缩成一对金额,写进纸张分类账,方便人工登记和核对。进入数字化协同时代,这道压缩过程把大量业务上下文丢失了。
最典型的就是电商订单里的一单多品、运费分摊、优惠券抵扣、积分支付。在这一堆对象面前,借贷分录只能先用一条“主营业务收入”、几条“其他应付款”把它压平。业务系统里明明是商品、物流、营销三个不同域的事实,到了财务系统只剩金额。结果就是报表能对上,但中间逻辑全靠单据号硬拼,每次月底对账都像在玩拼图。
3.2 REA 不排斥会计,但把“可审计”的口径放大了
REA 同样强调可审计,每个事件都对应资源增减,代理与资源都明确,不可篡改。但它的表达方式不是借贷两行,而是一条条带语义的事件。这些事件不只在财务系统里存在,它在采购、库存、销售、结算等系统之间流动,成为唯一事实源。
这样带来一个关键改变:企业外部供应商、客户,如果也按同一套事件语义提供数据,双方的对账就能从“导出报表再比对”变成“比对事件流”。采购方收到供应商的发货事件,供应商系统里也记录了同一个发货事件,两边约定好事件编号映射,差异项会非常显眼。这比传统对账高效很多,因为你在比对的是同一层事实,而不是各自加工的产物。
3.3 传统分录与 REA 事件流的对比
| 对比维度 | 传统借贷分录 | REA 事件流 |
|---|---|---|
| 记录对象 | 金额与科目 | 业务事件与资源增减 |
| 可追踪性 | 依赖摘要和辅助核算 | 事件自带代理、时间、资源 |
| 跨系统协同 | 靠对账和映射 | 事件即公共语义 |
| 扩展成本 | 科目结构调整成本高 | 新增资源或事件类型即可 |
| 平衡方式 | 借贷恒等式 | 资源增减守恒,按事件流校验 |
表格只是概括。落到工程上,最大的差异是扩展成本。传统模型里加一个核算维度,通常要动科目表、凭证模板、报表公式;REA 模型里加一种资源或一种事件类型,只是新增字典行和新的聚合口径,老数据完全不用迁移。这一点对业务变化快的公司来说非常值钱。
4. 实操一遍:把一段“采购赠品”的需求改造成 REA 模型
4.1 拿到手的需求原文
假设某公司市场部要采购一批定制赠品,用于线下活动。需求方的原话是:“先提采购申请,老板审批通过后向供应商下订单;供应商发货,仓库收货验货,验货合格后入库;财务根据发票和入库单付款;如果赠品有质量问题,还要退货退款。”
这段话看着不长,但里面混着审批流程、库存动作、资金动作、质量异常分支。如果直接照字面意思去设计表,很容易把“审批状态”“订单状态”“入库状态”全部塞进一张大表,然后每个状态用一堆字段记录,系统还没上线就被状态组合数搞晕。
4.2 把动作拆成预期事件,把承诺和事实分开
第一步不是画 ER 图,而是先拆事实、契约和状态。拆出来的结果大概是:
- 契约:采购申请单、采购订单,这两类都是“承诺”,不是动作。
- 事实事件:确认订单、供应商发货、仓库验收入库、财务付款、退货确认、供应商退款。
- 资源:赠品存货、银行存款、质量损失等。
- 代理:采购员、仓库管理员、财务人员、供应商。
拆的时候最关键的是把“确认订单”和“收到货”分开。订单只是对未来的承诺,只有等到发货和收货事件发生,资源的控制权才开始真正转移。很多系统把订单状态机当成业务事件的全部,结果报表里想追溯“货物实际到库时间”,只能去翻状态变更日志,而状态变更往往只记了“从待发货改成已发货”,数量没有、交接代理没有、对应的供应商发货事件也没有。这样一来,业务过程就完全失去了可审计性。
4.3 给契约单独建两张表
在事件表、资源表的基础上,我会补两张契约表和一张关联表。
CREATE TABLE rea_contract ( contract_id BIGINT PRIMARY KEY, contract_type VARCHAR(64), status VARCHAR(32), agreed_date DATE, internal_agent BIGINT, external_agent BIGINT ); CREATE TABLE rea_contract_line ( line_id BIGINT PRIMARY KEY, contract_id BIGINT, resource_id BIGINT, planned_qty DECIMAL(18,4), actual_qty DECIMAL(18,4) ); CREATE TABLE rea_event_contract_link ( link_id BIGINT PRIMARY KEY, event_id BIGINT, contract_id BIGINT, line_id BIGINT );这样,承诺、事实、资源三个维度被彻底分开。要回答“这张订单进度如何”,对比契约行的 planned_qty 和实际事件的汇总即可;要回答“仓库今天实际收了多少货”,按入库事件聚合;要回答“这个月给这家供应商付了多少钱”,按付款事件聚合。三个问题可以独立计算,也能通过事件-契约关联表随时合并。整条链路的血缘关系是一张可以下钻的网,而不是一张需要反复 ETL 的宽表。
4.4 一份自检清单
实际操作中,光看案例是不够的,把一个新业务域映射成 REA 时,我会逐条过这份清单:
- 这个动作是否真的改变了某项资源的数量或归属?如果没有,它可能只是状态变更,不是业务事件。
- 这项资源在动作前后分别由哪个代理控制?能不能明确指到内部代理和外部代理?
- 这个动作除了金额之外,是否有业务必须记录的属性,比如数量、批次、库位、关联合同号?
- 这个动作能不能重复发生且每次需要单独记录?能,那它大概率是事件。
- 这个实体是已经发生的动作,还是只是一个未来预期?预期放入契约,动作放入事件。
这份清单几乎每个建模工作坊都能用,尤其适合刚接触 REA 的团队。我第一次带人做类似梳理时,大家最大的收获不是学会了三张表,而是学会了克制:不急着设计字段,先把“哪些是承诺、哪些是事实、哪些是状态”分清。
5. 三个最容易把模型带偏的边界问题
5.1 内部调拨不是资源增减,而是控制权转移
仓库A把货调拨到仓库B,很多团队习惯做一个调出事件和一个调入事件,把总库存先减掉再加回来。站在 REA 的边界看,货并没有离开企业的整体控制范围,库存总量没变,变化的是具体控制位置或控制部门。这时候更适合把“仓位/库位”设成资源粒度的一部分,或者把调拨建模成一个内部代理之间的转移事件,而不是一增一减两个方向相反的事件。
我见过一个真实踩坑的案例。某供应链系统把内外仓调拨影射为两次出入库,每个月总库存表都比实物库存多出一截。排查了很多天,最后发现是调拨单被重复累加了。REA 里的增量、减量概念会逼你先回答一个问题:这个资源到底有没有离开企业的控制边界?没离开,就不该同时发生增和减。想清楚这个问题,很多奇怪的统计偏差能直接从源头避免。
5.2 把“订单”当成事件来处理
这是新团队最容易犯的错误。业务上有个“订单”,就把它当成一个事件写进事件表。结果产品经理问“这个月的未完成订单都是哪些”,研发得在事件表里用状态字段过滤;问“每个订单实际兑现了多少货”,研发又要去关联一堆后续事件。订单本质上是对未来交换的承诺,它包含计划数量和期望日期,可以用一个或多个未来事件来描述。
正确做法是让事件只记录真实发生的动作。确认订单这个瞬间,资源还没增减,所以不应生成资源流;只有收到货、付款这些确定事件发生时,才在事件表里落一行。如果你非要在事件表里放一个“订单确认事件”,那记得它没有资源增减,只能被当作业务日志。别把它和真正的资源流混在一起,不然查询口径会变得非常拧巴。
5.3 代理和参与者的粒度不一致
供应商的送货司机、仓库里的搬运工、系统接口机器人,这些人或系统要不要都建模成代理?理想情况是“业务责任主体”才算代理。司机只是受供应商委托送货,责任主体仍然是供应商;但如果你的业务需要追踪到具体经办人的绩效,比如要考核采购员的议价表现、仓管员的收货及时率,那把这些内部经办人建模为代理也合理。
关键问题是粒度必须全模型一致,不要在事件A里用部门ID,在事件B里用员工ID,最后统计分析时根本无法对齐。比较好的做法是建一张“参与方角色表”,把内部代理ID、外部代理ID、角色类型统一管理起来。这样即使粒度变化,也只是在角色表里增加一种职责类型,已有的历史事件不会受影响。
这三个坑基本是每个 REA 建模团队第一次复盘时都会撞上的,提前留个心,能省下一轮需求返工。
6. 事件溯源、数据中台,为什么让 REA 突然又香了
6.1 事件溯源架构和 REA 有天然的契合点
这几年不少团队在做事件溯源,系统里的每次状态变更都被记成一条 event,但很多人只记了“按钮被点击”“状态从A变成B”,事件与业务指标之间没有稳定关联。REA 恰好给事件补上了语义要求:事件必须指向资源增减、必须关联代理。做事件消息结构设计时,如果能带上 resource_id、direction、quantity、internal_agent、external_agent,消费方就不需要猜这条事件到底影响了什么,按资源维度直接聚合就好。
这不是把模型变复杂,而是让事件可计算。比如你收到的不是“order_created”,而是“goods_shipped”事件,payload 里明白写着资源编号、数量、发货代理、收货代理,那么下游系统可以独立验证数据、生成库存台账、计算应付暂估。事件的可信度一下子就上来了。
6.2 跨系统协同:事件成为公共语义后,对账成本大幅下降
多系统集成时,最常见的问题是各系统对同一个业务动作表达不一致。采购系统说“已发货”,仓库系统说“待收货”,财务系统说“在途”,三个口径全靠映射表。如果大家统一采用同一套事件语义,发货事件在采购系统和仓库系统里都是同一个概念,事件编号也天然可以关联。供应链上下游之间的交接明细、差异核对,会变得比现在清晰得多。
当然,推动外部供应商和客户都按这套事件标准输出,是需要商务和技术双重投入的。但即便先只在企业内部各系统之间统一事件语义,也已经能解决一大部分“本地口径打架”的问题。REA 在这里的价值不是给你一套通信协议,而是给你一套大家都认的业务词汇表。
6.3 落地建议:别一开始就设计大宽表
把 REA 落到物理模型时,最大的诱惑是按业务模块建大宽表:采购一张大宽表,库存一张大宽表,财务一张大宽表,后面每个新需求都加新字段,最终表宽得吓人,业务一变化就四处打补丁。REA 的思路反过来:先把事件流水表做成唯一事实源,资源表、代理表作为维度,其它业务宽表都只是针对特定查询的物化视图。运维成本低很多,新增指标也只是增加一个聚合视图。
我习惯在梳理订单和库存域的时候,拿一张白纸先画三列:资源有哪些,事件有哪些,代理有哪些。画完再回头对应现有的数据库表结构,通常会发现,旧设计里至少有一半字段都在弥补当初没把事件建模清楚的债。先把这三列理清,后续的系统改造会顺很多。