ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

REA模型:从资源-事件-参与者视角重构业务数据建模

REA模型:从资源-事件-参与者视角重构业务数据建模

看到“rea”这个标题的时候,可能有人会以为是某个内部项目代号,或者是某个工具缩写。但我今天想聊的,是业务建模领域绕不开的一套经典语义模型——REA,也就是 Resource-Event-Agent,资源-事件-参与者。我最初接触到它是在做进销存系统重构的时候,当时传统的借贷记账模型在财务核算上没什么问题,但放到业务中台、流程追溯和数据仓库建设里就处处别扭,后来换了 REA 这套视角,很多纠缠不清的问题一下子变得清爽了。

这套模型特别适合这几类读者:正在做 ERP、供应链、订单中台或者会计信息系统设计的开发者;想给数据仓库搭一套能支撑审计追溯的底层语义模型的数据工程师;还有那些被“业务流程到底怎么建模”折磨的架构师。它不只是一个学术概念,真正用起来之后,你会发现它能直接指导表结构设计、状态机划分和接口语义定义。接下来的内容,我会把 REA 的底层逻辑、建模步骤、落地到数据库的方法,以及我自己踩过的坑一次讲清楚。

1. 为什么一谈业务建模就先想到REA:资源和账本背后的那套统一语言

先说个背景。企业里的业务系统五花八门,订单系统管单据,库存系统管数量,财务系统管借贷,CRM 管客户关系。表面上各司其职,但一旦要做集成、做跨系统对账、做数据仓库汇总,问题就来了:同一个业务动作,在订单系统里叫“已发货”,在库存系统里叫“出库”,在财务系统里叫“主营业务成本结转”,在物流系统里叫“运单揽收”。到底什么才是这件事的“真身”?

REA 模型最早是会计信息系统领域提出的,思路特别朴素:不管你们系统里用多少术语,企业的经营本质就是一遍遍发生“事件”,这些事件要么让某些“资源”增加,要么让某些“资源”减少,而且一定有人或组织在参与。你不需要纠结叫“发货”还是“出库”,只需要承认一个事实:有一件商品从仓库被转移出去了,这是事件;商品的库存数量下降,这是资源被消耗;操作人是仓管员,这是参与者。

为什么这个视角重要?因为它把业务建模从“账户语义”解放到了“事实语义”。传统借贷记账法是以“账户”为中心的,借什么、贷什么、记在哪个科目下,这套规则是给财务人员看的,它经过了非常高度的抽象和编码。而 REA 是以“经济事件”为中心的,更贴近业务现场的真实动作。财务上你可以把“发货”和“收入确认”放在两个科目,但业务事实其实是同一个连续过程的两端。用 REA 建模,你记录的是“发生了什么”,而不是“这个动作在账本上叫什么”。

我实际用下来的体会是,REA 特别适合作为企业数据中台的“公共语义层”。数据中台最怕的就是各个来源系统的字段各说各话,REA 给了你一套稳定的实体骨架:资源、事件、参与者,以及它们之间的关系。任何业务域都能映射到这三大类上,你只需要在细节层面做扩展,而不是每接一个系统就重新发明一套模型。比如销售域,资源是商品和现金,事件是销售和收款,参与者是客户、销售员、财务;采购域,资源是商品和现金,事件是采购和付款,参与者是供应商、采购员、财务。骨架完全一致,这就是它的通用性。

前阵子我接手一个库存与财务对账项目,业务方一直要求“保证账实相符”,但两边经常对不上。传统做法是大量调账和手工解释。我们花了大概一周时间,把几套系统的核心动作全部用 REA 重新梳理成事件流,发现很多对不上的地方根本不是数据错误,而是系统间对“事件发生时点”的定义不一样:一个系统在开单时扣库存,另一个在发货时确认成本。问题本质是建模视角不统一,而不是计算逻辑有问题。后来约定了一套标准事件口径,对账问题一下子少了七成。

2. 三大构件和四条关系:REA本体模型的核心拆解

要真正把 REA 用起来,不能只知道缩写,得把它的实体和关系吃透。这一节我会结合生活化的例子把核心内容讲清楚,因为这些东西直接决定你后面表怎么建、关联怎么落。

2.1 三大构件:经济资源、经济事件、经济参与者

REA 的第一个核心实体是经济资源(Resource),指企业拥有或控制、具有经济价值的对象。它不限于实物商品,现金、应收账款、服务能力、软件许可证、积分资产都算。在建模时判断一个对象是不是资源,就一个问题:它能不能被一个经济事件增加或减少?比如“商品库存”能被入库增加、出库减少,是资源;“订单状态”不能被某个资源的增减直接表达,那它就不是资源,而是业务状态。

第二个核心实体是经济事件(Event),这是整个模型的发动机。经济事件是让经济资源的价值发生变化的活动,比如销售、采购、支付、收货、领用、退货。事件具备两个关键特征:一是发生在某个时间点,二是有明确的数量和金额信息可以落到某个资源上。你在建模时脑海里要有一个公式在转:每个事件一定有一个主资源,以及一个数量或者金额的变化方向,是增加还是减少。

第三个核心实体是经济参与者(Agent),指参与事件的人或组织,分为内部参与者(员工、部门、仓库)和外部参与者(客户、供应商、承运商)。参与者回答“这件事是谁做的、和谁做的”这一审计问题。很多团队建模时只关心资源和事件,把操作人、往来单位当成普通字段塞在事件表里,这在 REA 视角下是巨大的浪费,因为参与者是独立核心实体,它要和事件建立正式关系,才能支撑权责追溯。

2.2 四条关系:存量-流量、控制、职责、触发

光有三个实体还不够,REA 模型真正有价值的是定义了四条标准关系。

第一对是存量-流量关系,也就是资源与事件之间的关系。资源是存量,事件是流量;一个资源可以被多个事件影响,每个事件恰好作用于至少一个资源。举例来说,“咖啡豆库存”是资源,“采购入库”和“制作出杯”两个事件都直接改变它的数量。这条关系最重要的启示是:资源永远不会平白无故地变,它一定是由事件驱动的。你在设计 Redis 缓存或库存表时如果看到数量被直接 set 一个值,没有任何事件来源,那就要警惕了。

第二对是参与关系,也就是事件与参与者之间的关系。这是最简单的多对多:一个销售事件有客户参与,同时也有销售员参与;一个采购事件有供应商参与,也有采购员参与。注意参与关系应该落在“事件-参与者”上,而不是“资源-参与者”上。很多系统里把“客户-商品”直接建关联,这是不合理的,客户不是和商品直接产生关系,而是通过购买事件和商品产生法律关系。

第三对是控制关系,表示参与者可以支配哪些资源。比如仓管员控制着仓库库存,出纳控制着现金账户。控制关系往往成为权限系统的语义来源:你有权操作某类资源,意味着你有权参与某些事件。这个关系在企业建模中容易被忽略,等到做审批流、做数据权限时才发现到处补洞。

第四对是触发关系,表示事件与事件之间的因果或时序联系。比如销售事件触发收款事件,采购事件触发付款事件,发货事件触发物流事件。触发关系把独立的事件串成了完整的经营链条,这是 REA 能支撑流程追溯和审计的关键。在建模时,你需要明确每个事件的前置事件和后置事件,形成可回放的事件流。

这几个关系听起来简单,但真正搭建模型时,你能一直靠它们来校准设计。遇到不确定的字段归属,就问一句:这是在描述资源、事件还是参与者?又是在描述四类关系中的哪一种?答案明确,模型就不会跑偏。

3. 用一个电商出库流程,完整走一遍REA建模

理论部分说完了,咱们直接上手。我会用一套大多数人都接触过的电商销售出库流程做例子,带你从业务描述一步步推演出 REA 模型。这个过程和我实际工作中做业务域梳理的步骤完全一致。

3.1 第一步:圈定业务流程边界与目标

我们建模的范围是从“顾客下单”到“确认收货并完成收款”。如果范围再大,就会扯进营销、售后、供应商补货,模型复杂度会失控。第一次建模的人最容易犯的错就是贪大求全,我建议宁可先截取一个完整闭环,比如一笔订单从产生到回款,把这个闭环跑通、跑顺,再往外扩展。

这个流程涉及的角色包括:顾客、客服、仓管员、财务人员、物流公司。涉及的资源对象包括:可售商品库存、现金账户、应收账款。涉及的动作包括:顾客提交订单、仓库发货出库、物流送达顾客签收、财务确认收款。这里有一个容易混淆的地方:“顾客提交订单”算不算 REA 意义上的经济事件?严格看,它本身还没有让资源价值发生变化,更像是一个承诺或者业务意图。所以更合理的建模是把它作为流程的起点状态,而真正的第一个经济事件是“销售确认”或者“发货出库”。

3.2 第二步:逐事件识别资源增减与参与者责任

我们一个一个事件来对照。第一个关键事件是销售出库,操作角色是仓管员,外部参与是顾客所属订单,资源影响是“可售商品库存”减少,同时产生一笔“应收账款”资源的增加。对,这里的应收账款不是凭空出现的,它是由销售事件触发的债权性资源。第二个关键事件是物流送达并签收,参与者是物流公司和顾客,资源影响是“在途商品”减少、“已交付商品”或者直接说履约完成,同时“应收账款”仍然挂账。第三个关键事件是确认收款,参与者是财务和顾客,资源影响是“应收账款”减少、“现金账户”增加。

这里要注意,我把同一个业务动作拆成三个事件,而不是揉成一个“订单完成”大事件。原因是每个事件发生的时点不同、操作的参与者不同、影响的资源不同。这道边界切得准不准,决定了后面做分环节统计和审计时能不能拿到细粒度数据。细粒度数据和汇总数据并存没问题,但反过来从粗粒度倒推细粒度就难了。

为了便于大家理解,我把这个流程的 REA 建模结果整理成一张表:

经济事件前置触发事件影响的资源(方向)内部参与者外部参与者
销售出库订单提交商品库存(减少)、应收账款(增加)仓管员顾客
物流签收销售出库在途商品(减少)、履约进度(完成)—物流公司、顾客
确认收款物流签收应收账款(减少)、现金账户(增加)财务人员顾客

这张表就是整个模型的核心骨架。你可能发现这里少了很多传统订单系统里的字段,比如订单编号、商品名称、单价、税率。这些不是不重要,而是在 REA 视角下,它们是“事件发生时的快照信息”,应该挂在事件记录下面,而不是单独成为建模主体。商品名称挂在商品资源上,单价挂在“销售出库事件-商品资源”的关联记录里,订单编号则是事件链的分组标识。

3.3 第三步:校验模型能否回答业务问题

模型搭完之后,我们要做一次校验:拿业务上真实关心的几个问题来质问模型。第一个问题,“这个月卖了多少商品、卖了多少钱?”答案在“销售出库”事件里,按事件时间过滤、按商品资源聚合即可。第二个问题,“有多少应收账款还没收回来?”答案是把“销售出库事件产生的应收账款增加”和“确认收款事件对应的应收账款减少”做差。第三个问题,“某个仓管员经手了多少出库订单?”答案在“事件-参与者”关联里,按内部参与者过滤即可。

你会发现,这些问题刨根问底到最后,都落到了第 2 节说的四类关系上。这就是 REA 建模给人的底气和安全感:你不是在依据报表口径凑数,而是在依据业务事实回答问题。报表只是查询,模型才是地基。

4. 从概念到数据库:REA模型的表设计与关联落地

概念模型捋清楚之后,最现实的问题就是:怎么落到数据库里?我见过不少团队,REA 讲得头头是道,一打开建表脚本又回到了老一套,商品、订单、支付各建各的表,关系稀里糊涂。这一节我给出一套可落地的表结构设计通用范式,并说明每张表的职责边界。

4.1 通用基础表:事件表、资源表、参与者表

如果是在做通用业务中台,我会建议先建三张主表。第一张是经济事件主表(economic_event),承载所有事件的基本信息,字段包括事件ID、事件类型、事件时间、全局事务ID、来源系统标识、备注。事件类型用字典或者枚举,比如 SALE_OUT、CASH_RECEIPT、PURCHASE_IN。第二张是经济资源主表(economic_resource),字段包括资源ID、资源分类、资源编码、计量单位、当前存量、资源状态。第三张是经济参与者主表(economic_agent),字段包括参与者ID、参与者类型(内部/外部)、名称、证件号或统一信用代码。

这三张主表是最顶层的主数据。实际业务中会有大量衍生的子类型表,比如商品资源表、现金账户表、客户表、供应商表,都分别通过外键挂到主表上。这样既保证公共模型统一,又保留业务的差异化扩展能力。我用这套方案调整过好几个系统,最直接的感受是后续接新业务域的时候,不需要再推翻主表,只需要新增子类型表。

4.2 关联表:事件资源表、事件参与表、事件链路表

真正让 REA 活起来的是三张关联表,也是很多设计师容易省略的地方。

第一张是事件资源关联表(event_resource),记录每个事件对每个资源的影响明细。核心字段包括关联ID、事件ID、资源ID、变动方向(增加/减少)、变动数量、变动金额、业务发生单价。这里要特别注意:金额和单价是“快照”,必须当场记录,不能事后去查当前价格,因为供应链场景里同一商品在不同批次采购时价格可能不同,你要还原的是事件发生时的口径。

第二张是事件参与者关联表(event_agent),记录每个事件的参与者。一个事件可以关联多个参与者,字段包括关联ID、事件ID、参与者ID、参与角色。这里的角色是语义化的,比如“操作人”“审批人”“往来单位”。不直接把参与者ID做在外键上,而是放进关联表,是为了支持一个事件有多个相关方的现实情况,也方便后续做权限或责任追溯时灵活查询。

第三张是事件触发关系表(event_link),记录事件之间的指向关系,字段包括关联ID、前置事件ID、后置事件ID、触发类型。触发类型可以是“直接驱动”“时间约束”“凭证引用”。有些场景里后置事件不一定马上发生,比如销售出库之后可能一个月才收款,所以表里最好带上期望间隔时间或者计划触发时间,方便做流程预警。

下面我用简化的 DDL 示意这几张表的关联要点:

CREATE TABLE economic_event ( event_id BIGINT PRIMARY KEY, event_type VARCHAR(64) NOT NULL, event_time DATETIME NOT NULL, source_system VARCHAR(64), remark VARCHAR(512) ); CREATE TABLE event_resource ( id BIGINT PRIMARY KEY, event_id BIGINT NOT NULL, resource_id BIGINT NOT NULL, change_direction TINYINT NOT NULL COMMENT '1-增加,2-减少', change_qty DECIMAL(18, 4) NOT NULL, change_amount DECIMAL(18, 2) NOT NULL, unit_price DECIMAL(18, 4), INDEX idx_event (event_id), INDEX idx_resource (resource_id) ); CREATE TABLE event_agent ( id BIGINT PRIMARY KEY, event_id BIGINT NOT NULL, agent_id BIGINT NOT NULL, role_type VARCHAR(64) NOT NULL, INDEX idx_agent (agent_id) ); CREATE TABLE event_link ( id BIGINT PRIMARY KEY, prev_event_id BIGINT NOT NULL, next_event_id BIGINT NOT NULL, trigger_type VARCHAR(32) NOT NULL, INDEX idx_prev (prev_event_id), INDEX idx_next (next_event_id) );

4.3 状态与快照:如何在事件流上支撑查询性能

看到这里,很多人会问:如果所有业务数据都走事件表,查询当前库存、当前客户余额这类频繁高频的读取怎么办?总不能每次跑一遍事件汇总吧?答案是在事件流之上建立状态投影表。比如商品库存表可以保留一个冗余字段 current_qty,由事件驱动的汇总任务异步更新。订单聚合表也可以保留当前状态字段,由事件表驱动状态机迁移。

状态投影表不是 REA 模型的破坏,而是它在工程上的必要补充。事件表是事实的源头,投影表是面向查询的缓存。二者通过一个不可回滚的“事件版本号”或“全局事件ID”保持一致。用数据库事务或者消息队列把事件消费到投影表的时候,一定要遵循先写事件表、再改状态的顺序,否则一旦失败就会出现状态和事件对不上的情况。我在实践中的经验是:把事件表当作账本,只准追加、不准修改删除;把投影表当作草稿纸,随时可以由账本重算。想明白这一点,很多数据一致性的争论就消停了。

5. 几个容易翻车的建模细节与我的避坑记录

REA 在概念上不难,难的是建模过程中那些“好像对、实则错”的细节。下面这几条都是我在项目里实际踩过的坑,每一条都对应过一段难受的返工经历。

5.1 把状态当成事件,事件流被污染

最典型的问题是把“订单状态变更”当成经济事件来建模。比如有人会给 REA 事件表单独增加“状态变更”事件类型,把订单从待发货改成已发货记录成一个事件。这看着合理,实际上把两类东西混在一起了:状态是业务对象的属性投影,而事件是资源增减的驱动源。状态变更应该作为事件发生的结果来描述,而不是作为一个独立的经济事件。

举个例子,仓库完成“销售出库”这个经济事件之后,订单状态自然会变成“已发货”。状态是被推导出来的,而不是另一个事件。如果强行增加“状态变更”事件,后面做对账和追溯时会产生二义性:到底是先有状态事件还是先有出库事件?数据口径会变得非常混乱。我建议在实现时把状态机只放在投影表上,所有状态的迁移必须有来源事件ID作为依据,不允许通过接口直接改状态字段。

5.2 硬化资源与软性资源的计量单位不统一

商品库存可以用件、箱、吨、个计量,现金账户用元,应收账款用元,积分资源用个。这些资源在 REA 模型里都会走 event_resource 关联表,如果change_qty字段直接用业务单位,跨资源聚合会比较吃力。

我的做法是在资源主表里增加一个“标准计量单位”字段,同时把change_qty统一折算为标准单位数量,另存一份change_qty_original保留原始业务单位数量。比如一件商品一箱装 12 个,标准单位用“个”,但仓库操作的时候扫描的是“箱”,事件记录中原始数量是 2,标准数量是 24,这样汇总报表时直接按标准数量求和即可,不会出现数量单位错乱的问题。这个坑在项目上线后暴露时,修起来远比建模阶段多花数倍精力。

5.3 事件与参与者关系上一对一思维顽固

很多企微、审批流场景里,业务天然的“一个事件只有一个操作人”思维会渗透到模型设计里。比如销售出库事件,建表时直接放了一个operator_id字段,后来业务要求同时记录“审核人”“拣货人”“复核人”时,只能不断加字段,一次迭代加一个。这是典型的参与者关系没建模好。

正确的做法是第 4 节说的 event_agent 关联表,参与者是一组而不是单一字段。后续如果想扩展“客户经理”“质量抽检人”,直接插入关联记录,不需要改表结构。设计阶段多做一个关联表,后续能省掉 N 次数据库迁移。这是我在项目里最有体感的“设计救后期”案例之一。

5.4 只建增量不见减量,资源账对不平

REA 事件是对资源增减的驱动,但很多初版系统只记录了增加资源的动作,比如采购入库、收款,对于消耗或减少资源的动作没有建模成事件。最典型的场景是库存“报损”和“盘点差异”,业务上本来应该是一个减少库存的经济事件,但为了省事,系统直接改库存表的数值。

结果就是:资源表里数量对不上,盘点差异说不清来源,财务审计要求逐一追溯每个差异时无从下手。正确的道路是任何数量的变动都必须具备对应事件,哪怕盘亏也要建一个“库存盘亏”事件,注明责任人、审批单、原因。只有坚持“资源无事件不变”这条铁律,模型才能经得起审计。

5.5 事件时间与业务时间的混淆

事件表里我建议至少设计四个时间字段:事件发生时间(业务现场的真实动作时间)、系统记录时间(数据写入数据库的时间)、业务归属期间(比如属于哪个账期或月份)、计划触发时间(如果是前置计划类事件)。很多团队只保留一个create_time,后果是财务月结时,上月底已经发货但本月初才录入系统的单子,归属哪一期成了争议。REA 里事件时间应当由业务动作决定,不能由数据库的落库时间决定。没有这个意识,月末对账永远有一批“差异单据”要人工解释。

最后再分享一个我一直在用的检查方法:每建完一个 REA 模型,我会找业务方随机挑一个资源,然后从当前存量出发,一路追问导致它变化的全部事件,直到能够完全还原这个数字的由来。如果中间有任何一段说不清楚,或者要借助系统外的手工表格,说明这个模型还有缺口。建模本质上是在给企业的经营数字打地基,地基稳了,上面的报表、指标、决策才不会歪。

RELATED READING

延伸阅读

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