ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

REA业务建模:用资源-事件-代理人重构数据模型设计

REA业务建模:用资源-事件-代理人重构数据模型设计

刚转到做业务架构那两年,我一度被"借贷必相等"这几个字逼得夜夜失眠。明明前端业务很清晰:用户下单、仓库发货、财务收款,可一到后端的数据库模型里,全被抽象成一张张凭证分录,业务过程的信息被压扁成借贷两列。直到有前辈扔给我一份某高校实验室的建模文档,里面反复出现一个词——REA(Resources-Events-Agents,资源-事件-代理人)——我才意识到,原来早有人把"业务过程本身"当作建模的第一要素,而不是把财务科目当作宇宙中心。这篇文章我就围绕 REA 这个主题,把这些年的理解、落地经验、踩过的大坑,一次性聊透。

REA 不是什么新奇的编程框架,而是一套业务建模的思维范式。它从"资源、事件、代理人"三个基本概念出发,重新回答了那个几乎所有业务系统都要面对的问题:一套系统的数据模型,到底应该长成什么样,才能真正反映企业做了什么,而不只是账上记了什么。适合谁看?如果你在做 ERP、电商后台、供应链系统,或者只是对"领域建模到底怎么建"感到困惑的工程师、产品经理、业务分析师,这篇文章都能给你一套可复用的建模思路。

1. 先搞懂 REA 在解决什么:数据模型"重结果、轻过程"的老毛病

1.1 传统建模方式到底缺了什么

随便打开一个老牌业务系统的数据库,你会发现表结构高度相似:订单表、订单明细表、商品表、客户表,再加上一堆状态字段,比如订单状态、支付状态、发货状态。这种建模方式直观、好上手,可它有一个致命的隐含假设:系统只关心"当前是什么状态",不关心"它是怎么一步步变成这个状态的"。

举个例子。一个订单从"已下单"到"已支付"再到"已发货",传统表结构里通常就是几个状态字段被改来改去。但你有没有想过,"已支付"这个状态背后,其实蕴含着一个完整的业务事件:客户交出了一笔钱,公司获得了一笔收入,这笔钱从客户的账户流到了公司的账户。这些信息在传统模型里往往是割裂的——支付流水存在支付表,收入确认存在财务模块,订单状态存在订单表。三个地方各记各的,全靠开发者脑补它们之间的关系。

这正是我当年最头疼的地方。业务方跑来问:"这批订单到底有多少是真正收了钱的?"我的第一反应是要去 JOIN 好几张表,还要判断各种时间节点,稍有不慎就对不上账。而 REA 的思路从根本上改变了我看待这类问题的方式——把"发生了什么"当成核心,而不是把"现在是什么状态"当成核心。

1.2 一个洗车例子帮你跳过术语墙

如果觉得上面说得抽象,我用一个谁都懂的洗车场景来拆解 REA 的三个核心词。

  • 资源(Resource):洗车服务本身、洗车工的工作时间、洗车用的水蜡,这些都是资源。资源的核心特征是被企业控制、能够在业务事件中被消耗、转换或交换。
  • 事件(Event):"客户付了 30 元"是一个事件,"洗车工清洗了这辆车"是另一个事件。事件代表了资源的变化时刻。
  • 代理人(Agent):客户、洗车工、收银员,这些参与事件的人就是代理人。

REA 的精髓在于,它把"付钱"和"洗车"这两个事件通过某种关系连接起来,形成一个完整的业务闭环。传统建模里,你可能只关心"这个客户洗没洗完",而 REA 要求你回答:哪个资源被消耗了?哪个资源被获取了?谁参与了这件事?这件事和哪件事构成了交换关系?

一旦把这些脉络梳理清楚,数据模型就不再是一张张孤立的状态表,而是一张可以回溯历史、推导未来的关系网络。这也是我认为 REA 最值得学习的地方——它逼着你用"业务事件"的语言重新讲述企业运作的故事。

2. 借贷记账法的困局:财务视角和业务视角之间为什么总有翻译损耗

2.1 复式记账给了我们什么,又拿走了什么

会计领域用了几百年的复式记账法,核心是"有借必有贷,借贷必相等"。它很伟大,因为它第一次让企业的经济活动可以被系统地记录、核对。但如果你把复式记账法直接当作业务系统的建模基础,问题马上就来了——你会发现,账本上的每一笔记录都在为"财务报告"服务,而不是为"业务管理"服务。

我参与过一个模拟项目,需求是做一套内部报销系统。财务同事给的表格模板非常标准:借方科目、贷方科目、金额、摘要。开发团队按照这个模板建表,做出来之后却发现一个尴尬的场景:管理层问"这个部门这个月到底产生了哪些具体的业务活动",系统答不上来。因为报销单被强行拆成了借贷分录,摘要里写的"差旅费"过于笼统,具体去了哪里、见了谁、办了什么事,全被丢掉了。

复式记账法就像一台【数据压缩机】,把丰富多彩的业务过程压缩成几个科目代码。压缩的效率很高,但解压时损失的信息太多。A部门经理想知道"上个月那批设备采购,从申请到入库到底经历了哪几个环节",你在账本里找不到答案,因为账本只记录了"固定资产增加了、银行存款减少了",中间的沟通、审批、物流、验收全被抽象掉了。

2.2 REA 对复式记账法的态度:不是推翻,是补底

这里我要强调清楚,REA 并不是要否定复式记账,它的定位是记录比借贷科目更底层的业务事实。在 REA 模型里,你记录的是:谁(代理人)在什么时间(事件)把什么资源交给了另一家企业(代理人和资源的关系)。至于这笔业务在会计上该怎么借贷,那是可以从 REA 模型里推导出来的,但不需要你事先把它硬编码成一对借贷科目。

换句话说,传统的借贷关系视图是"数据模型必须长得像账本",REA 的出发点是"数据模型先把业务事实存好,财务视图只是一种查询结果"。这种分离特别有价值,因为财务规则可能会变(税率、科目编码规则),但业务事实不会变。

我用一张对比表总结一下两种思维方式的差异:

维度借贷记账法建模REA 建模
核心关注点资金与科目的增减变化资源在业务事件中的流转
建模起点会计科目表业务事件清单
信息的粒度粗,常丢失过程细节细,可回溯完整过程
财务规则变化时数据库结构可能要大改模型基本不动,只改推导视图
适合回答什么问题"账上还剩多少钱""这笔交易是怎么发生的,涉及哪些资源和人"

3. 物尽其用:把资源、事件、代理人三个构件逐一拆开看

3.1 资源不是"商品",也不是"资产"

很多人一听到资源,下意识就对应到"商品表"或"固定资产台账"。但在 REA 语境里,资源的定义要宽得多。资源是稀缺的、由企业控制的、能够在事件中被消耗或转换的东西。不只是商品,服务工时、生产能力、知识产权、现金,甚至客户好感度都可以建模成资源。

我参与设计的某跨平台系统里,曾经把"客服响应用户咨询的时长"作为资源建模,然后在事件表里记录每一次咨询消耗了多少"响应时长",再配合代理人的排班表做数据分析。这种建模方式在传统表结构里很难自然表达,但在 REA 框架下非常顺畅,因为一切都围绕"资源如何被消耗"展开。

关键判断准则:如果你发现在现有系统里,某样东西既不是人、不是事件,又确实在业务过程中发生了变化,那它大概率就是资源。

3.2 事件:一切模型的发动机

REA 里的事件不是泛指任何业务动作,而是特指引起资源数量或价值变化的那一瞬间。比如"客户下单"并不是事件,因为下单本身没有让任何资源发生实际转移;真正的事件是"仓库发出商品"和"财务收到货款"。

这里有一个很重要的区分:许诺事件( promise event)与实际事件(actual event)。签约、下单、确认订单,这些是对未来动作的承诺,不对应资源的即时流动。真正让资源"动起来"的,是实际事件。REA 模型要求你把这两类事件分开记录,因为它们的业务含义完全不同——承诺还没兑现,实际事件才是既成事实。

回到电商系统里,这就是为什么"订单表"不能替代"出库事件表"的原因。订单只代表客户有意愿买,不代表商品真的出了库、钱真的到了账。把承诺事件当作实际事件来处理,是业务系统数据失真最常见的原因之一。

3.3 代理人:事件的两端都有人

代理人这个概念最容易被忽略。很多数据模型里有"客户表"和"员工表",但很少有人意识到,人和事件的关系才是建模的关键。REA 要求每个事件都与代理人关联,而且事件里通常涉及两个代理人——内部代理人(inbound agent)和外部代理人(outbound agent)。

拿一次销售事件来说,内部代理人是负责发货或收款的本公司员工,外部代理人就是客户。如果没有这种区隔,系统的权限设计、责任追踪、提成计算就会非常混乱。我见过有系统把"操作员"字段当成万能字段,既能往里填客户 ID,又能填员工 ID,结果时间一长数据就彻底不可信了。REA 通过"每个事件必须有内部代理人和外部代理人"这个约束,从数据模型层面杜绝了这种混乱。

3.4 库存的本质:事件的增量,而非一张静态表

我很喜欢 REA 对库存的刻画方式。传统建模里,库存通常是一张记录"当前剩余数量"的表,每次出入库都去 UPDATE 这条记录。这样做的隐患是:库存表只是结果,不是过程。一旦有人误操作了库存数字,你根本不知道是哪个事件导致的。

REA 的做法是:不持久化"当前库存",而是持久化一系列入出库事件。库存量随时可以从事件历史中汇总出来,比如SUM(入库事件) - SUM(出库事件)。这不正是事件溯源的雏形吗?采用这种思路之后,库存的每一次变动都能追溯回去,数据对账变得异常轻松。事实上,很多现代业务的"库存流水表"设计,正是在不自觉地向 REA 靠拢。

4. 实操演练:用 REA 给电商订单系统建模,从识别事件到表结构落地

4.1 先给场景画一个圈

为了把建模过程讲清楚,我设定一个足够简单又足够典型的场景:一个普通电商平台的"现货订单"流程。业务链路是:客户提交订单 → 系统锁定库存 → 仓库发货 → 客户收货 → 财务收款。别急着打开画图工具,先做一件事——把业务过程中的所有"资源变动时刻"列出来。

  • 客户承诺购买(订单创建,这是许诺事件)
  • 仓库实际发出商品(出库事件,商品资源减少)
  • 客户实际支付货款(收款事件,现金资源增加)
  • 可能还有一笔应付给物流公司的运费的付款事件

所以,事件清单就是:下单、发货、收款、运费付款。注意"客户提交订单"在 REA 里更像"承诺",我们可以在事件表上用事件类型字段区分它和实际事件,但不能把两者混成同一类。

4.2 识别资源与代理人,把关系图推出来

在上述事件清单的基础上,逐项问自己三个问题:改变了什么资源?谁发起的?谁接收的?

  • 发货事件:减少的商品资源是"库存商品",内部代理人是仓库员工,外部代理人是客户。
  • 收款事件:增加的资源是"现金/银行存款",内部代理人是财务/收银,外部代理人是客户。
  • 运费付款事件:减少的资源是"现金",但这里交换到的资源是什么?是"物流运输服务"——它是一种即时消耗的服务资源,接收方是客户。

把所有关系和盘托出之后,你会发现一个漂亮的闭环:客户付出"现金"资源,换来"商品"和"物流服务"资源。这在传统电商系统的订单模型里其实是模糊的,但在 REA 模型里一目了然。

4.3 从模型到数据表:一份可以直接参考的表设计

建模思想的落地终究要反映到数据库结构上。下面这份表设计是从上述 REA 分析中推导出来的,方向供参考,具体字段视业务需要可以灵活增减。

-- 资源表:记录各种资源的主体信息 CREATE TABLE resource ( id BIGINT PRIMARY KEY, resource_type VARCHAR(50), -- 商品、现金、物流服务等 name VARCHAR(255), unit VARCHAR(20) ); -- 事件表:记录资源变动的实际事件与承诺事件 CREATE TABLE event ( id BIGINT PRIMARY KEY, event_type VARCHAR(50), -- pur/actual 区分承诺与实际 event_name VARCHAR(100), -- 下单、发货、收款等 occurred_at DATETIME, resource_out_id BIGINT, resource_in_id BIGINT, quantity_out DECIMAL(18,4), quantity_in DECIMAL(18,4) ); -- 代理人表:内部与外部代理人的统一视图 CREATE TABLE agent ( id BIGINT PRIMARY KEY, agent_type VARCHAR(20), -- internal/external name VARCHAR(255), customer_id BIGINT NULL, -- 如果关联到客户主数据 employee_id BIGINT NULL -- 如果关联到员工主数据 ); -- 事件-代理人关联表(多对多) CREATE TABLE event_agent ( id BIGINT PRIMARY KEY, event_id BIGINT NOT NULL, agent_id BIGINT NOT NULL, agent_role VARCHAR(20), -- inside/outside FOREIGN KEY (event_id) REFERENCES event(id), FOREIGN KEY (agent_id) REFERENCES agent(id) );

实际项目中我通常还会给事件表增加一列source_system_trace_id,用来记录该事件在原有系统里的单号。这一步很关键,因为 REA 建模后的系统往往要和老系统并行,需要双向追溯。说是"表设计",本质上你是在建立一张业务事实网络,而不是在维护状态快照。这一点必须反复强调。

4.4 这么建模,到底比传统方式强在哪

把同一套需求分别用"订单状态表"和"REA 事件表"实现之后,我看到了决定性的差异:统计分析能力。传统订单状态表适合回答"现在有多少待发货",但要回答"上周一共消耗了多少商品资源、按仓库维度看如何分布",就必须把订单表和库存流水 JOIN 起来,而且时间口径经常对不齐。REA 事件表天生就是流水账,任何资源的时间序列分析都只是加个 GROUP BY 和 WHERE 的事。

更实际的好处是数据链路透明。财务想看某笔订单的收入确认依据,直接从事件表找到对应的收款事件,再关联到对应的发货事件和资源记录,整个过程不需要经过大脑中的某种"代号翻译层"。

5. 建模中的经典陷阱:双边事件、实物与现金的流转、增量时点

5.1 "事件必须一对对出现"的双边性陷阱

REA 中有个很容易理解错的概念:交换事件往往是双边(duality)的,即一个经济事件的完成,通常要以另一个经济事件作为交换。比如发货和收款是一对,采购入库和付款是一对。

但"一对一对"不代表你必须用一个父表把它们捆绑起来。有些开发者会把"发货单"和"收款单"强行做成主子表关系,一旦出现部分付款、分期发货就傻眼了。正确的做法是:两个事件各自独立建模,中间通过一个"交换关系表"关联,并且关联表上可以记录比例或分摊规则。

我踩过的一个具体坑是这样的:某系统的业务规则是"先发货后付款,允许客户分三次结清"。如果当初把发货事件和收款事件设计成一对一关系,整个表结构都得推翻。后来改成双边关系独立建模,才真正解决了"三笔收款对应一笔发货"的场景。记住,REA 说的双边是概念层面的依赖,不是数据表层面的一对一主外键。

5.2 钱和货不是两条平行线,而是一个闭环

另一个新手特别容易忽视的点:现金也是要建模成资源的。很多人在梳理模型时,很自然地把"商品出库"和"收到货款"拆成两套互不相关的子系统,商品归商品,资金归资金。但在 REA 的视角里,它们必须在同一个模型中形成闭环。

设想你在查一笔"预付款"订单:客户在 1 月份预付了 5000 元,公司 2 月份才发货。如果资金系统和仓库系统不在同一个模型框架内,财务那边已经确认了 5000 元收入,仓库这边却还挂着待发货,两边就永远对不上账。REA 把现金当作资源,把收款当事件,把发货当另一个事件,再用交换关系把它们连成环,任何时点的对账,本质上都是在环上做查询,自然不可能出现两边"打架"的情况。

5.3 库存的"事件增量"到底该选哪个时点

设计出库事件时,最容易纠结的一个细节是:入库和出库的增量应该记在哪个时点?是在"客户下单"时预先锁定,还是在"仓库实际发货"时才扣减?

从 REA 的角度回答这个问题非常清晰:承诺事件不改变资源,所以下单时你去改资源数量本身就是错的。正确做法是:下单时新增一条"承诺出库"的记录,但该记录不参与资源存量计算;真正出库时,再新增一条事件,资源存量才发生变化。至于"锁定库存"这种需求,本质上是对承诺事件的一个约束校验,不应该凌驾于实际事件之上。

我见过太多系统因为图省事,把"下单时扣库存"和"发货时扣库存"混在一起,结果一旦订单取消或者超卖,库存数据就神秘失联。坚持"承诺不改变资源"这条铁律,能帮你避开一大半库存对不上的问题。

6. REA 的延伸地带:从会计建模到企业本体,它和领域驱动设计那点事

6.1 现代企业本体与 REA 的承继关系

REA 不是停在教科书里的概念。二十世纪九十年代以来,学术界和企业建模领域在此基础上发展出了许多企业本体论(Enterprise Ontology),比如企业本体(EO)等,它们继承了很多 REA 的核心思想——从事件出发,从资源流转出发,把人(代理人)真正纳入模型的中心。我在做企业级中台建模时,经常会在概念模型里先画一张 REA 风格的事件网络图,再往上面套具体业务的实体和字段。这种方法论层面的先行,会让后面的系统设计稳很多。

6.2 与事件溯源(Event Sourcing)的神交

做后端的人一定听过事件溯源:不保存当前状态,而是保存状态变化的历史,需要时通过回放事件得到当前状态。REA 的事件思想和这件事有着内在的默契。我在落地一个"业务审计要求极高"的内部系统时,就直接把这两者结合了:数据库里只存事件,界面上展示的永远是一系列事件聚合后的投影。效果就是,任何一条数据都可以被追问"你的上一秒是什么",而且永远答得上。

6.3 什么场景不适合一上来就用 REA

当然,我不建议所有项目都立刻拥抱 REA。场景非常轻量、生命周期很短,比如一个内部小工具、一份表格管理后台,硬上 REA 只会让简单问题复杂化。另外,团队成员如果对"事件驱动"没有基本概念,学习成本也不可忽略。我自己的取舍标准是:只要系统未来要承担复杂的业务流转、对账审计、多系统交互,就值得花 1-2 天做一轮 REA 建模;否则用传统状态表也够用。

在实际应用中发现,REA 给我带来的最大改变不是某个具体技术,而是逼着我养成了一个习惯:动笔建表之前,先把"资源怎么流动、事件怎么发生、由谁参与"说清楚。有了这层底子,无论后面套用事件溯源、领域驱动设计还是微服务拆分,都不会跑偏。这是我认为 REA 真正值得花时间去琢磨的原因——它教给建模者的,不是一张表、两行代码,而是一套怎么看待业务世界的视角。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进