☰
REA模型实战:用资源-事件-参与者重构业务数据建模
2026/10/11 10:57:56 网站建设 项目流程

看到“REA”这三个字母,如果你和我一样经常和会计系统、业务数据建模打交道,第一反应应该是那个经典的三元组:Resource-Event-Agent,资源、事件、参与者。我第一次正面接触它,是在一个图书电商项目的数据库评审会上。当时团队正为了“订单、库存、客户、财务流水怎么拆表”吵得不可开交,最后有人随口问了一句“为什么不用REA建模?”,会议室安静了几秒。后来我们真的按REA的思路重做了核心数据模型,才知道传统借贷记账法那些理所当然的表结构,到底让设计和沟通多绕了多少路。

这篇东西不准备抄教材,我会从一个具体的业务场景(图书销售)出发,把REA模型的三个实体究竟是什么、实体之间的关系怎么连、数据库表怎么建,以及我在实际落地过程中踩过的坑,全部过一遍。无论你是做业务需求分析、后端开发,还是维护着一套老ERP系统,这篇文章都值得留个收藏。它能帮你跳出“科目+分录”的思维惯性,真正从业务事件本身去理解数据。

1. 从“账本思维”到“事件思维”:REA到底在反叛什么

1.1 传统会计模型最让人头疼的四个地方

在讲REA之前,先聊聊我们大多数人熟悉的借贷记账法。小时候做会计实习,师傅教的第一件事就是“有借必有贷,借贷必相等”,所有业务最后都要变成会计分录。这套东西运行了几百年,支撑了无数企业的财务核算,但它用在现代业务系统里,会让人越来越别扭。

第一个别扭:围绕“科目”组织数据。销售收入、应收账款、库存商品……每个科目一张表,一旦你多了一个无法直接映射到科目的分析维度,比如“按促销活动看毛利”,就得在科目表上疯狂加辅助核算,或者在账表软件后面挂一堆自定义字段。时间长了,表结构成了一个打满补丁的棉袄。

第二个别扭:分录丢失了业务上下文。一笔销售收入的分录只告诉你“借:银行存款 100,贷:销售收入 100”,但它没告诉你这100块是哪笔订单带来的、客户是谁、销售员是谁、卖了哪几本书。这些信息要么散落在业务系统里,要么得靠一堆中间表去还原。你拿到了账,却看不懂业务。

第三个别扭:数据冗余高。为了同时满足财务和业务查询,很多人会把同样一份销售数据同时存在订单表、明细表、流水表、凭证表里。一旦某个数据变了,到处都要同步,漏了一处就是一笔坏账。

第四个别扭:系统之间难以联动。库存系统管库存,CRM管客户,财务管资金,三者靠接口互相喊话,每次都像两个地方工作风格不同的人在打越洋电话,效率全靠加班补。

这四件事加在一起,就构成了“用传统方式做现代业务数据模型”的主要困局。REA模型的初衷,就是用一套统一的结构去替代“科目+分录”这个组合。

1.2 REA的核心:一切从经济事件出发

REA这个名字看起来抽象,拆开之后其实很好理解。它让我悟了很久的一个道理是:业务系统的数据库不应该模仿会计凭证,而应该模仿业务本身。

先看三个词:

  • Resource(资源):企业里那些数量或价值可以被度量的东西。可以是实体的,比如仓库里的书、现金、设备;也可以是非实体的,比如版权、积分、服务工时。在REA里,资源不是账户,而是实实在在存在的经济对象。

  • Event(事件):引起资源增加或减少的行为。注意这个动词,“引起资源变化”是关键。比如“销售图书”就是一个事件,它同时减少了库存资源(书流出)和增加了资金资源(现金流入)。事件是REA模型的核心,它回答“什么时候、发生了什么”。

  • Agent(参与者):参与事件的人或组织。可以是企业的外部参与者(客户、供应商),也可以是内部参与者(销售员、仓管员、财务)。参与者回答“这个事是谁干的、和谁干的”。

用一句话概括REA:业务就是资源在参与者的推动下,通过一系列事件发生流动和转换。建模时,你先定义有什么资源,再定义资源怎么通过事件流动,最后定义谁负责参与这些事件。比把注意力放在“借什么科目、贷什么科目”上,逻辑要直白得多。

1.3 一个最简单的REA图景:书店通过“卖”这个事件,把书变成现金

我用一个日常场景帮你建立直觉。假设你经营一家小书店:

  • 资源:书架上的《三体》实体书、收银机里的现金。
  • 事件:书店向顾客“售出一本书”。
  • 参与者:顾客、收银员。

REA描述这个场景就是:

顾客这个参与者执行了“售书”事件,事件消耗了一个《三体》资源(书从库里去),同时另一个“收款”事件带来了现金资源。如果你愿意,售书事件和收款事件之间还可以有一个箭头,表示“销售之后应该收款”。整个链条没有一个“贷:销售收入”的字眼,但你完全能看懂业务发生了什么。

等你习惯了这种表达,再回头看那些二十多张表的旧系统,就会觉得REA的“反叛”是合理的——它把设计落点从结果记录挪到了过程还原。

2. 资源、事件、参与者之间的三类核心关系

2.1 资源与事件:“流入流出”是复式记录的原始形态

REA里面,资源和事件之间有一种天然的“进出关系”。一个事件会让某个资源增加,或者让某个资源减少。比如“采购”事件让库存图书资源增加,“售出”事件让库存图书资源减少。

这其实是复式记账最底层的逻辑:你不可能只让书没了,而不让别的资源增加。每一项资源流动,背后都至少有一个流入和一个流出事件。在REA建模中,我们会在资源和事件之间建立一个连接表,记录这条流动的方向(流入或流出)和数量单价。这样,资源余额就变成了一个推导结果:初始余额 + 所有流入数量 - 所有流出数量。

很多项目的库存查询以前都是靠一张“库存余额表”硬算,我说一个我在实际里看到的反面案例:有一次,某团队一周内两次盘库都差了十几件货。后来查出来是人工直接改了余额表字段,没走任何事件流。这种事情在REA模型里是不会发生的,因为余额不是一条可以随便改的记录,而是从事件流汇总出来的结果。谁想改库存,就必须增加一条“盘盈”或“盘亏”的事件,连带责任人和原因都留下,谁也赖不掉。

2.2 事件与事件:经济链中的先后因果

资源与事件的关系解决了“资源怎么变”,事件和事件的关系则解决了“为什么要变”。REA里最典型的一种事件关联叫“经济链”。用一个完整销售场景来体会:

顾客下一张订单(承诺事件)→ 仓库发货(实际销售事件)→ 顾客付款(收款事件)。这三个事件不是孤立的,它们之间有时间先后和因果关系。没有发货,就不该有收款;没有订单,发货就缺乏依据。

在REA模型中,这种“事件对事件”的关联通常用一个叫“claim”(债权债务)或“commitment”(承诺)的实体来记录。比如你先把货发给客户,但客户还没给钱,那就产生了一条“应收账款的债权”,它是销售事件和收款事件之间的空隙,等收款完成,这个债权就消失了。

我建议新手不要一上来就把订单、发货、发票、收款都建模成“事件”,更合理的入手点是先识别那些真正让资源流入或流出的事件,再去看哪些事件之间有前后依赖关系。订单、发票很多情况下不是资源流动本身,而是资源流动的凭证,可以处理成事件状态或附属单据,强行把它们都塞进REA的事件概念里,反而会把图弄得很难看。

2.3 参与者与事件:谁在什么岗位上干了什么

参与者和事件的关系最容易理解,也最容易做歪。一个销售事件里,参与者至少有两个:内部参与者(售货员)负责执行,外部参与者(客户)负责发生。这些信息如果只存在一张订单表里,往往会变成“下单人”、“审批人”、“发货人”等一堆固定字段。一旦将来冒出一个新角色,比如“补货人”、“售后客服”,你又得加字段或者新建表。

REA的处理方式是把“参与者”单独建模成一个表,再让事件和参与者之间通过一条关联去表达“角色”。同一张参与者表既能放客户,也能放员工,区别只是在关联记录里的角色字段不同。这样一来,如果业务有变化,只需增加新的参与者关联记录,不需要动表结构。我已经数不清自己曾经为了“销量Top10的销售员是谁”这个需求,在旧系统里翻了多少张表,REA用一条关联查询就出来了。

2.4 一张关系表看懂REA的连接结构

关系类型连接主体连接含义典型例子
资源-事件Resource ↔ Event资源通过事件发生增减图书通过“销售”流出库存
事件-事件Event ↔ Event事件间存在经济链和因果关系“销售”引出“收款”
事件-参与者Event ↔ Agent谁参与了事件、以什么角色“销售”由“客户”触发、“店员”执行

这张表不是REA的全部,但它是所有REA建模的骨架。无数条“资源-事件-参与者”的连接,像积木一样组合起来,就能还原整个企业业务流。

3. 用REA重构图书销售系统:一次完整的建模演练

3.1 先定义领域词汇表

建模最忌讳的是一上来就画图。我建议你学我,先拿一张白纸,把业务里所有名词和动词列一遍。以图书销售为例:

  • 资源类名词:图书(SKU维度)、现金、银行存款、预付卡余额。
  • 事件类动词:采购入库、退货给供应商、销售出库、客户退货、收款、付款、盘点调整。
  • 参与者类名词:客户、供应商、采购员、销售员、仓管员、财务专员。

这一步最大的价值不是列清单,而是逼着团队统一语言。如果两个人对“销售”这个词的理解不一致,一个认为是“下订单”,另一个认为是“收款”,那后面所有建模都会偏。

当时我们团队第一次做这个练习,发现仓库说的“入库事件”和采购说的“采购事件”其实指的是同一件事的两面。后来我们把事件统一成“采购收货”,才算把口径拉齐。这种教训在传统表驱动建模时几乎不会出现,因为你根本不需要分清“采购”和“入库”是不是一套业务动作。

3.2 用事件链把业务串起来

有了词汇表,就能画事件链了。一个最小可用图书电商系统,核心事件链大概是:

  1. 采购员向供应商下采购订单(这是一个承诺,可建模为商务事件)。
  2. 仓库收到书,发生“采购收货”事件,图书库存资源增加,应付供应商的债务(claim)增加。
  3. 客户下销售订单(客户承诺)。
  4. 仓管员发货,发生“销售出库”事件,图书库存资源减少,应收客户债权(claim)增加。
  5. 财务收到客户钱款,发生“收款”事件,现金资源增加,应收债权减少。
  6. 财务付钱给供应商,发生“付款”事件,银行存款资源减少,应付债务减少。

这条链跑通了,整个业务的资金与货物流就是闭环的。比对着会计凭证一借一贷地拆,清晰太多。

在建模文档里,你可以用这样的文字版结构去描述:

资源:图书(SKU为123456的那本书,数量=1,单价=59) ↓ 流出(qty=-1) 事件:销售出库(时间:2025-01-20 10:30) ↑ 参与(角色=客户) 代理人:客户A ← 后随于 事件:收款(时间:2025-01-20 11:00) ↑ 参与(角色=客户) 代理人:客户A → 流入(qty=+59) 资源:银行存款

这种描述方法我和同事都叫它“REA叙事线”,它能把业务原原本本讲出来。讲不出来,说明模型里缺了东西。

3.3 REA视图与传统三表设计的好与坏

对比维度传统设计(订单+明细+流水)REA模型(事件+链接表)
存储粒度订单头、订单行、流水行各管一摊资源、事件、参与者各自独立
业务语义靠字段命名和注释表达靠关系连接自然表达
应收应付靠科目余额和账龄分析靠事件之间的claim推导
扩展性加分析维度就改表/加字段加新事件类型或新链接即可
查询成本多表join且语义模糊关系固定且容易走索引

这里不是说你必须弃用订单表。实际上很多REA落地项目仍会保留订单/发票等操作型单据,只是把底层核心模型换成REA结构。拿我们的项目来说,业务操作界面还是“下单”“发货”“开票”,但底层分析报表的数据源已经完全依赖REA事件流了,跑月报的速度比老系统快了一个量级。

3.4 从REA模型到查询意图的转换

一个很直观的好处:以前我问“这个季度ABC三本书一共卖了多少、分别卖给了谁、是谁卖的”,传统表要跨越订单表、客户表、员工表、商品表、库存流水表,很多人需要写一段“黄页一样长”的SQL。REA框架下答案一目了然:

  • 事件类型=“销售出库”
  • 资源链接=图书消耗行
  • 参与者关联=客户和销售员
  • 直接按时间范围过滤并分组

业务人员甚至能自己画出这个查询意图。这也解释了为什么很多数据分析框架底层都开始采用类似REA的思路去建模——它们要的不是记账,而是还原事件。

4. 落地到数据库:REA表结构设计与SQL实践

4.1 建表:宁可多建“连接表”,不要硬塞字段

REA建模到物理表阶段,我的习惯是采用这种五加一结构:资源表、事件表、参与者表,以及两张链接表,外加一张可选的claim表。下面给出最简版本:

-- 资源表 CREATE TABLE resource ( id BIGINT PRIMARY KEY, res_type_id BIGINT NOT NULL, -- 资源类型(图书、现金等) code VARCHAR(64) NOT NULL, -- 资源编码/ISBN name VARCHAR(128) NOT NULL, qty DECIMAL(18,4) NOT NULL DEFAULT 0, base_unit_price DECIMAL(18,4) ); -- 参与者表 CREATE TABLE agent ( id BIGINT PRIMARY KEY, agent_type VARCHAR(32) NOT NULL, -- CUSTOMER / SUPPLIER / STAFF name VARCHAR(128) NOT NULL, ext_info JSONB ); -- 事件表 CREATE TABLE event ( id BIGINT PRIMARY KEY, event_type VARCHAR(64) NOT NULL, -- SALE_OUT / PURCHASE_IN / CASH_RECEIPT ... happened_at TIMESTAMP NOT NULL, note TEXT ); -- 资源事件链接表(核心) CREATE TABLE res_link ( id BIGINT PRIMARY KEY, event_id BIGINT NOT NULL REFERENCES event(id), resource_id BIGINT NOT NULL REFERENCES resource(id), flow_type CHAR(3) NOT NULL CHECK (flow_type IN ('IN','OUT')), qty DECIMAL(18,4) NOT NULL DEFAULT 1, unit_price DECIMAL(18,4), CONSTRAINT uq_event_resource UNIQUE (event_id, resource_id, flow_type) ); -- 参与者事件链接表(核心) CREATE TABLE agent_link ( id BIGINT PRIMARY KEY, event_id BIGINT NOT NULL REFERENCES event(id), agent_id BIGINT NOT NULL REFERENCES agent(id), role_type VARCHAR(32) NOT NULL, -- SELLER / CUSTOMER / SIGNER CONSTRAINT uq_event_agent UNIQUE (event_id, agent_id, role_type) ); -- 债权债务关系表(可选) CREATE TABLE claim ( id BIGINT PRIMARY KEY, event_left_id BIGINT NOT NULL REFERENCES event(id), -- 例如销售事件 event_right_id BIGINT REFERENCES event(id), -- 例如收款事件 claim_type VARCHAR(32) NOT NULL, -- RECEIVABLE / PAYABLE amount DECIMAL(18,4) NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'OPEN' );

4.2 为什么用链接表而不是在事件表里写死外键

我第一次见到这种设计时觉得太繁琐:“一个事件居然要通过中间表关联参与者?”后来才明白,中间表换来的好处极其巨大。传统表设计里,一张销售表要好几列业务员ID、客户ID、审核员ID;到了下半场突然增加“介绍人ID”,你又得改表结构。而REA用agent_link表加一行数据就解决了,只是加一个角色,完全不用动表结构。

资源也一样,你永远不知道以后这个资源会不会同时出现在多个事件环节里。用中间表,事件和资源之间就是多对多关系,未来扩展“这个销售事件消耗了三条库存批次”也只是一个链接表里放三行而已。在业务模型不稳定、变化快的项目里,这种设计能把开发成本压得很低。

4.3 一个最常用的查询:过去30天的销售额

假设我们想知道过去30天按图书分组的销售收入。这个查询会同时用到event、res_link和resource三张表:

SELECT r.code AS book_code, r.name AS book_name, SUM(rl.qty) AS total_qty, SUM(rl.qty * rl.unit_price) AS total_revenue FROM event e JOIN res_link rl ON rl.event_id = e.id AND rl.flow_type = 'OUT' JOIN resource r ON r.id = rl.resource_id WHERE e.event_type = 'SALE_OUT' AND e.happened_at >= CURRENT_DATE - INTERVAL '30 day' GROUP BY r.code, r.name ORDER BY total_revenue DESC;

这里有个细节我一直强调:单价不要写在resource表里,要写在res_link表里。商品标价会变,但某个历史销售事件发生当时的成交单价,是那个事件的事实属性。如果把单价写在资源主表上,一旦促销调价,历史报表全乱套。res_link里的unit_price就像快照,锁定了事件的现场。这也是REA里“事件是事实”思想的一个重要体现。

4.4 维护一致性:让数据库别把垃圾数据放进来

REA模型表结构不难建,难的是约束。我的工程清单里至少包含这几个:

  • 每个res_link记录,必须有对应的event和resource,用外键保证。
  • 每个agent_link记录,必须有对应的event和agent,同理。
  • flow_type只能IN或OUT,用CHECK约束。
  • 一个资源发生OUT时,该资源的当前qty必须大于等于流出量,这个在数据库层面可以用触发器,在应用层则必须做乐观锁或事务。我们当初为了省事把库存检查放到了应用层,结果并发高时有超卖,后来加了一个数据库触发器才稳。
  • 事件happened_at不能为空,且逻辑上应该与res_link里的时间一致。

如果团队还没准备好上复杂触发器,先把外键和CHECK做对,再在应用层做严格的事务边界。REA模型本身不会帮你解决并发,但它让业务约束更容易定位到“某个事件”上,排查问题不用再扫一屋子表了。

5. 我在REA建模过程中踩过的五个坑,以及排查清单

5.1 坑一:把“科目”当成“资源”

这是从会计思维转过来最容易犯的错误。有人会说“应收账款”是个资源吧?客户欠我钱,这难道不是资产吗?但在REA模型里,“应收账款”不是资源,它是销售事件和收款事件之间的未完成状态,也就是claim。如果你把应收款建模成一个资源,就会出现两个资源对同一笔债权做冗余存储,对账的时候永远有一边对不平。

更准确的说法是,应收款是“已经发生了销售事件但尚未发生收款事件”的中间事实,它应该通过查询事件链推导出来。一开始我们用REA时,有人坚持要保留“应收账款表”,后来发现这张表实际上是事件间状态的缓存,于是干脆把它去掉,应收款全部由SQL实时计算出来,反而更准确了。

5.2 坑二:事件粒度一个不对,后面全是灾难

事件粒度是个老问题。一次销售订单里有三本书,这算一个事件还是三个事件?我的建议是:事件粒度取决于你分析的最小业务单元。如果你需要回答“每种书的销量”,那一次“售出”就应该在res_link里拆成三行,而事件本身可以是一个,链接表承载明细。这是推荐做法:核心事件表登记“谁、什么时候、什么动作”,资源链接表登记“哪些资源受到影响,各自数量单价”。

反过来,如果某业务场景里同时发生“收取现金”和“复核销售单”,不要把它组合成一个事件。事件要尽量保持单一、原子,便于复用和统计。我们曾经把“销售并收款”合并成一个事件,省了片刻的编码时间,结果一个促销活动是“先收款后发货”,一个活动是“先发货后收款”,后面不得不把事件拆开重来。所以,事件合并一时爽,业务分析火葬场,这是真话。

5.3 坑三:把订单、发票、合同硬塞进“事件”里

学REA时,教科书上经常出现“订单(Order)”这种东西。于是很多人把订单建成一个事件,可订单本身并不会让任何资源发生增减,它只是承诺或者意图。REA里专门有一个概念叫“承诺事件(Commitment Event)”,比如销售订单、采购订单,它描述的是未来可能发生资源流动,而不是资源目前已经流动。

遇到这种情况,我建议在落地时把订单、合同作为普通单据表,独立于REA事件流,然后在适当时机生成真正的事件记录。比如订单确认后,当仓库发货时,再插入一条“销售出库”事件,并可以用订单号作为业务参考,关联到单据。这样既保留了业务操作习惯,又不破坏REA事件流的纯粹性。把订单直接做成事件,会导致query里到处是“订单状态”这种五味杂陈的字段,REA的干净感就没了。

5.4 坑四:参与者和用户不分

很多团队把agent表直接设计成“用户表”,里面放着登录账号、密码、手机号。也许项目早期这样没问题,但很快就会发现:同一个客户可能既是小程序用户又是线下门店会员;同一个员工可能既是仓管员又是客服专员。参与者和用户是两个维度,agent是业务参与角色,user是访问系统的身份。它们可以是同一个实体,但不应该混在同一张表里。

合理的处理是:agent表只放业务角色和业务属性;认证和授权相关的字段做成单独的用户表,两者通过外键关联。这样REA模型才能跨系统复用——供应商供货员也可以成为agent,但他并不是你的系统用户。当年我们没分,后面给供应商开放门户时差点把用户表结构拆到崩溃。

5.5 坑五:忽略事件链的时间次序

REA的连接看起来都是无向的,但业务本质上有强时间顺序:先采购后销售,先发货后收款。如果你在设计表时没有保留事件发生时间,或者用数据库里无意义的创建时间来替代真实业务时间,那么统计“某个时间点的库存”“某个周期的应收余额”时,你就会拿到一个错误答案。

我的做法是两套时间并行:事件表必须有一个业务时间happened_at,记录真实业务发生时点;另外可以有一个created_at记录系统录入时间。查询时一律用happened_at,不因系统批次导致统计失真。还有一个容易忽略的点:在claim表里,事件left和right的顺序不能倒。可以建立触发器或者应用层校验,确保claim的起始事件时间早于结束事件时间,否则账期就乱了。

5.6 REA建模质量检查清单

把这套模型做完后,别急着上线,先用下面这些问题对模型进行一次“目检”:

  • 每个资源是否都至少有一个投入和一个产出路径?如果某个资源只有流入没有流出,或者只有流出没有流入,那么它要么是边界(比如初始库存),要么模型里漏了事件。
  • 每个经济事件是否至少连接了一个参与者?如果一件事连“谁做的”都说不清,它不叫事件,只能算状态变化。
  • 是否存在只记录金额而没有记录数量的资源流动?金额和数量最好成对出现,用比让后续各种维度分析包括成本和毛利时会非常痛苦。
  • 是否能用“谁在什么时间,通过什么动作,使什么资源发生什么变化”来描述每个事件?念出来不顺,就说明事件定义有问题。
  • 现金、库存、应收的所有增删改查,是否都通过事件流完成而没有任何直接改余额表的口子?

这个清单我们团队后来直接复制到了每个项目的交付验收标准里。每次上线前跑一遍,至少能拦下一半以上逻辑错误。

最后再分享一个小技巧:模型建完以后,试着挑一条核心业务链,用自然语言整个念一遍——“客户在10点30分通过店员买走了一本《三体》,图书库存减少一本,应收款增加59元;11点客户付款,现金增加59元,应收款清零。”如果这段话通顺、没有任何地方需要你硬解释,那你的REA模型基本就是健康的。REA最厉害的地方不是那张ER图,而是它逼着每个人把报表里冷冰冰的数字还原成“什么资源、在什么事件中、被谁转化成什么资源”的动态故事。这种视角,对做数据中台、写分析报表、甚至在团队里跟业务对需求的人来说,都比任何时髦方法论都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询