☰
REA模型:资源-事件-代理,重构业务建模与数据库设计方法论
2026/10/10 9:42:32 网站建设 项目流程

标题里的rea,不是某个英文单词少打了一个字母,它就是REA 模型的缩写:Resources-Events-Agents,资源-事件-代理。我第一次接触这套建模思想的时候,说实话第一反应是“这不是会计系统的东西吗,跟我做业务系统有什么关系”。但后来在几个真实项目里被踩坑教育过几次之后,我发现 REA 的价值被远远低估了。它不是一个只存在于论文里的学术模型,而是能直接指导我们画 ER 图、划分聚合、设计事件表的实战方法论。

这几年大家都在聊领域驱动设计(DDD)、事件溯源(Event Sourcing)、微服务拆分,动辄就上很重的架构概念。但很多同学在真正的建模现场还是拍脑袋,按直觉建表,结果后续需求一变更,表结构被改得面目全非。如果你也想找一个“看起来不难但后劲很大”的建模参考框架,REA 模型值得你花一个下午认真研究。

1. REA 模型到底解决什么问题

1.1 先从两个典型的建模现场说起

我先说一个很常见的场景。某电商平台要做订单履约系统,产品经理给了一堆需求:下单、支付、发货、退款、采购入库、盘点。大部分人第一反应是什么?先建订单表、订单明细表、商品表、用户表、支付表、物流表。再想想表之间的关系:订单和订单明细是一对多,商品和订单明细是一对多,用户和订单是一对多。看着好像挺顺,但需求继续往下推就出问题了。

比如“采购入库”和“订单发货”这两件事。从业务角度来看,一个是资源进来,一个是资源出去,它们都改变了库存数量。传统做法是各建各的表,然后在代码里靠一堆if分支去维护库存。但 REA 的做法是完全不同的,它先不急着定表结构,而是先把业务里的“资源”“事件”“代理”找出来。货物是资源,采购入库是一个事件,供应商和仓库管理员是代理。订单发货也是一个事件,客户和仓库操作员是代理。两类事件都作用于同一个资源“库存”,于是库存的变化规则自然就统一了。

另一个场景是制造企业的生产报工系统。老板想知道每一批产品的成本构成。如果你只是建一张生产记录表,记录“这个月生产了 A 型号 1000 台”,那根本回答不了成本追溯问题。REA 会告诉你:原材料是资源,投产领料是事件,把原材料加工成成品也是一个事件,工人和设备是事件代理。成本本质上是“资源—事件”之间的联系,通过事件把原料资源的输入量、成品资源的输出量、设备资源的消耗量关联起来,成本就自动有答案了。

1.2 三类核心元模型:资源、事件、代理

REA 的核心元素只有三个,但边界极其清晰。

资源(Resource),不是数据库表里的一行记录,而是“具有经济价值、可以被交换或使用的东西”。这里有一个关键点:资源不一定是有形的实物。商品、库存、现金是资源,技术专利、品牌授权、知识文档也可以作为资源,甚至“客户的一次购买承诺”都可以被视为一种资源。资源在 REA 里有两个方向:流入和流出,分别表示资源进入企业或离开企业。

事件(Event),是对资源产生增减影响的经济活动。它是整个模型的心脏。比如销售发货导致库存减少,收款导致现金增加,采购付款导致银行存款减少。事件有一个很重要的特性:它必须发生在某个时间点,是不可拆分的原子事实。为什么强调这一点?因为它和“状态”有本质区别,很多系统设计混乱,就是把人家的状态当成一张表来设计,而不是把状态看作一个事件推动的结果。

代理(Agent),是事件的参与者。单个“代理”可以是一个人,一个部门,一家客户公司,也可以是一个自动化系统。在落地建模时,通常把代理分为两类:内部代理和外部代理。内部代理是企业内部参与事件的人员或部门,外部代理是客户、供应商、银行、物流公司等。这样区分的好处是,权限设计和组织架构会自然引出来,比如仓库操作员只能触发出库事件,采购专员只能触发采购事件。

1.3 为什么说 REA 是“以事件为中心”的模型

如果你画过传统的实体关系图,会有一种感觉:实体之间的连线虽然清晰,但往往说不清“这条线到底表示什么”。订单和用户连线,是“属于”还是“创建”?商品和库存连线,是“包含”还是“对应”?而在 REA 里,关系不是拍脑袋定的,而是由三类联系严格驱动的。

第一类是资源—事件联系,表示资源因为某个事件发生了流入或流出。第二类是事件—代理联系,表示谁参与了这次经济活动。第三类是事件—事件联系,表示一个事件触发了另一个事件,这是 REA 中比较特殊也很有价值的部分。企业不是一座静止的仓库,而是一串流动的事件链,销售事件触发物流事件,物流事件触发发票事件。当模型中的“事件”成为主角,你再看那些业务发散的需求,心态会完全不一样。

所以我常说,REA 模型最厉害的地方,是它逼你先回答“发生了什么”以及“这个事件在改变哪个资源”,而不是先回答“要建一张什么表”。方向对了,后面设计多复杂都稳。

2. 从传统记账模型到 REA 模型的演进逻辑

2.1 传统会计模型到底局限在哪

很多懂点财务的读者可能敏锐地发现,REA 好像和“借贷记账”有关系。确实,REA 最早就是为了解决传统会计模型对业务语义表达能力不足的问题而提出的,后来才被推广到更广阔的信息系统建模领域。

传统会计模型的核心是借贷记账法。一张凭证上,借一笔贷一笔,平衡就结束。但这种表达方式有一个天然的短板:它只记录已经发生并影响会计科目的交易,却很难描述交易背后的“为什么”。比如仓库里发生了一次退货,会计凭证上可能只是“借:主营业务收入,贷:银行存款”或者“借:库存商品,贷:主营业务成本”。这个分录确实记录了金额,但它没有告诉我们客户是谁、退了哪个产品、为什么发生退货。你不要觉得这不重要,一旦你想做数据分析、做经营决策、做审计追溯,这些缺失的语境就是致命伤。

REA 的模型思路完全不同。它从“经济事件”本身出发,把记账分录拆成“哪个资源变多了,哪个资源变少了,谁参与了这件事”。贷方不再是某个虚的会计科目,而是实打实的资源流出;借方也不再是冷冰冰的科目数字,而是资源流入。这样设计出来的数据模型天然就能支持复杂的追溯,因为它保存的是完整的经济语义,而不是压缩后的记账符号。

2.2 REA 模型如何重构业务事实

用一个买咖啡的例子来说清楚这件事。你用手机支付了 20 元买了一杯咖啡。传统记账模型记录的是:银行存款减少 20 元,主营业务收入增加 20 元。而 REA 模型记录的是:现金资源流出 20 元,客户代理支付了这笔款项;咖啡产品资源流出 1 杯,咖啡店代理交付了产品;现金流出事件与产品流出事件之间存在“交换”联系,它们共同组成了一次完整的经济交换。

你看,同样一笔业务,后者包含的信息量级完全不同。它天然回答了几个业务问题:交易的对手是谁?交换的条件是什么?资源流转的方向是怎样的?把这些事实存下来,未来不管是要做税务审计、毛利分析、还是客户行为分析,都有了原始素材。尤其当你需要回答“这批库存为什么少了三件”的时候,REA 模型可以顺着事件链一路回溯到最初的采购、入库、出库、调拨记录,而不是被一句“业务调账”堵住嘴。

我在实际项目里的感受是:一个系统如果从设计之初就用 REA 的视角去沉淀业务事实,它的“数据底子”会厚实很多。很多报表需求不需要后期再到处取数拼接,因为你已经保留了最细粒度的业务真相。

2.3 REA 与数据库建模的映射关系

一定要注意的是,REA 是一个“领域模型”,不是物理表结构。拿到 REA 模型之后,你需要把它翻译成可落地的数据库设计。

通常的做法是:三张主表分别对应资源、事件、代理。资源表和事件表之间通过关联表连接,关联表里除了外键还要带上“方向”字段,例如inflow/outflow。事件表和代理表之间也通过关联表连接,关联表里带上代理角色,例如“下单员”“审批人”“承运方”。事件表和事件表之间如果存在触发关系,则可以建一张event_trigger关系表,记录父事件和子事件。

这种映射关系看起来并不复杂,但它带来的好处是极高的稳定性。当新增一种业务类型时,如果这个新业务仍然属于“用某种资源交换另一种资源”的范畴,你就只需要在资源、事件、代理的引用字典上新增记录,而不是大幅调整表结构。这一点从长期演进角度来说,价值非常大。

3. 手把手做一个订单履约系统的 REA 建模

3.1 明确业务范围与候选对象清单

下面我用一个模拟项目来实操一遍,这个项目代号就叫“模拟订单履约系统”。假设我们要做的是标准的电商零售业务,涉及从供应商采购、商品入库、客户下单、支付、发货、退货这六条主线。

首先不要碰键盘,拿白板把业务事件流画一遍。我先列一遍候选事件:

  • 采购下单(向供应商下定单)
  • 采购入库(供应商送货后入库)
  • 销售下单(客户提交订单)
  • 收款(客户支付货款)
  • 发货(仓库打包发出)
  • 退货退款(客户退回商品,系统退回货款)

然后是候选资源:

  • 在库商品、在途采购商品、现金、银行存款、销售收入权益

这里有一个容易混淆的点,销售收入、应收账款的背后,其实是对“交换关系”的另一种理解,先不展开。候选代理有:

  • 客户、供应商、仓库管理员、采购专员、客服专员、财务审核人员

3.2 按资源、事件、代理三要素整理模型

接下来按 REA 的约定整理事件与资源流。所有事件被定义为“能够让资源流入企业或流出企业”;如果一个动作对资源不产生增减,那么它一般在 REA 模型中扮演“决策/承诺”的角色,而不是经济事件。销售订单在这个模型里更合适的定位是“承诺事件”,真正改变资源的是后续的“销售发货”。如果你一定要把销售订单当事件落地,也不影响大方向,但需要清楚它不直接产生资源流入流出。

整理后得到核心表意:

  • 采购入库事件,使“在库商品”资源增加,使“银行存款”潜在流出的义务增加。
  • 销售发货事件,使“在库商品”资源减少,使“银行存款”潜在流入的权利增加。
  • 收款事件,使“银行存款”资源实际增加。
  • 退货事件,使“在库商品”资源增加,使“银行存款”资源减少。
  • 事件之间的触发关系:销售下单触发收款、销售发货、退货退款;采购下单触发采购入库。

3.3 落地为数据模型:关键表和关系

我给出一个简化版的核心表结构。实际生产环境里字段要多很多,你可以在此基础上补充。

首先是resource表:

CREATE TABLE resource ( resource_id VARCHAR(32) PRIMARY KEY, resource_code VARCHAR(64), resource_name VARCHAR(128), resource_type VARCHAR(32) -- 枚举:goods / cash / receivable / payable );

然后是event表:

CREATE TABLE event ( event_id VARCHAR(32) PRIMARY KEY, event_type VARCHAR(32), -- 枚举:purchase_inbound / sales_outbound / receipt / return target_id VARCHAR(32), -- 关联到订单编号 occurred_at TIMESTAMP, note VARCHAR(500) );

关键是关联表,它才是 REA 思想的落点。

CREATE TABLE event_resource_link ( link_id BIGINT PRIMARY KEY, event_id VARCHAR(32), resource_id VARCHAR(32), direction VARCHAR(16), -- 枚举:inflow / outflow quantity NUMERIC(18,2) ); CREATE TABLE event_agent_link ( link_id BIGINT PRIMARY KEY, event_id VARCHAR(32), agent_id VARCHAR(32), role VARCHAR(32) ); CREATE TABLE event_trigger_link ( link_id BIGINT PRIMARY KEY, parent_event_id VARCHAR(32), child_event_id VARCHAR(32) );

你可能发现这里没有一张单独的“订单表”。订单本身的信息实际上被拆散在event、event_resource_link、event_agent_link里。这是 REA 比较激进的设计思路。如果你觉得这样让日常查询变复杂,也可以保留订单主表,但让它退化成一个“聚合外衣”,核心业务事实仍然放在 REA 关系表里。我更推荐后者,因为既照顾了开发习惯,又保住了 REA 的领域语义。

3.4 用代码表达核心模型

如果你团队里有喜欢写代码的伙伴,下一步可以先把模型类写出来。我常用 Python 的 dataclass 做原型,成本最低:

from dataclasses import dataclass from enum import Enum class FlowDirection(Enum): INFLOW = "inflow" OUTFLOW = "outflow" @dataclass class Event: event_id: str event_type: str occurred_at: str @dataclass class Resource: resource_id: str resource_type: str resource_name: str @dataclass class Agent: agent_id: str agent_type: str agent_name: str @dataclass class EventResourceLink: event: Event resource: Resource direction: FlowDirection quantity: float @dataclass class EventAgentLink: event: Event agent: Agent role: str

到这一步,模型已经可以指导服务层的实现了。比如处理“销售发货”事件时,代码会往event_resource_link插入一条“商品资源 outflow”的记录,同时往event_agent_link插入“客户”和“仓库操作员”两条参与记录。后续无论你要统计销售收入、计算库存余额、还是要做客户提货归档,都可以从这些基础事实出发,不会依赖某张状态表里那个可能被改来改去的字段。

我补充一个严格的红线提示:REA 模型里的“代理”不含任何网络通信概念。这里的所有“代理”都是业务参与方,比如人、部门、机构。和网络代理、流量代理没有任何关系,希望大家不要联想跑偏。

4. REA 与现代软件架构的结合:事件溯源和领域驱动设计

4.1 事件溯源视角下的 REA

我对事件溯源(Event Sourcing)一直有种“理论很美、落地很累”的感觉,但它和 REA 模型放在一起看,很多难点反而能解开。REA 本质上要求你把“事件”当作系统里不可变的事实记录下来,而事件溯源也要求你别去 UPDATE 状态,而是追加新事件。

比如库存这个资源,在传统系统里你存的是一个当前值“库存 100 件”。但 REA 视角下,你只需要存“采购入库 +50”“销售发货 -20”“盘点调整 +3”这一串事件,库存当前值只是一个投影。未来一旦对账有问题,你可以重放全部事件,而不是依赖某个可能出错的状态字段。

这里有一个实际好处:当你需要把单体系统拆成微服务的时候,事件溯源 + REA 的事件流天然就是消息队列里传递的领域事件。上游服务产出经济事件,下游服务订阅事件并更新自己的资源投影,彼此之间不需要同步接口调用。我用 REA 帮助团队梳理过几次事件名,效果立竿见影,大家争执“这个事件到底应该放哪个服务”的时间少了很多。

4.2 REA 如何辅助 DDD 中的聚合划分

领域驱动设计中最难的不是实体设计,而是聚合划分。到底哪些对象应该放在同一个聚合边界内,哪些对象只能通过聚合根访问?这个问题困扰了很多人,我见过不少团队在聚合设计阶段争论不休。REA 能提供一个比较纯粹的判断依据:事件与事件之间如果存在强触发关系,并且它们必须保持一致,那它们就应该落在同一个聚合里。

举个例子,订单聚合中“销售下单”“收款”“发货”三个事件,如果业务要求”不付款不能发货“,那么在 REA 看来,这三个事件就是一条强制性的事件链,它们必须在一个事务边界内,也就是同一个聚合。反过来说,如果业务允许先发货后付款,那”收款“事件就可以拆到别的聚合里,中间靠消息或定时任务来协调。你看,聚合边界不再是拍脑袋的”我觉得这个表应该放一起“,而是由事件之间的依赖强度决定。

4.3 微服务拆分时可以照抄的三条原则

结合 REA 做微服务拆分,我沉淀了三条经验,基本可以直接沿用。

第一条原则:资源是服务的账本,事件是服务的对外接口。每个微服务应该明确它掌管哪些资源,对外只暴露经济事件。订单服务暴露“销售下单”,库存服务暴露“发货”,财务服务暴露“收款”。服务之间不要互相暴露内部表,而是彼此消费事件。

第二条原则:根据资源生命周期划分服务边界。库存和商品绑定紧密,现金和支付绑定紧密,应收和客户信用绑定紧密。REA 说资源有生命周期,有流入流出,那服务边界就顺着资源的主人来切,谁最了解这个资源,谁就管理它的所有事件。

第三条原则:尽量避免跨服务维护同一组事件链。如果“采购入库”这个事件同时影响库存、应付、供应商评价三个服务,那说明边界切得有问题,要么这个事件拆成三个细化事件,要么你应该引入一个流程编排服务。

这些方法论都不是零散的“架构秘诀”,它们都指回同一个主题:先认清楚业务里到底有哪些资源、事件、代理,再用这些元素去组织服务边界。架构是领域的翻译,不是技术的炫技。

5. 常见问题与踩坑复盘

5.1 建模时总把代理当成事件

第一个高频错误是混淆角色和动作。比如“客户”是代理,“下单”是事件。但在建模时,有人会把“客户订单”当成一个代理对象来用,这会导致关联表无法清晰表达“谁参与了什么资源变化”。我的判断技巧很简单:如果一个“对象”在业务句子里作主语,它通常是代理;如果作谓语,它通常是事件。“客户提交订单”这句话里,主语是客户,谓语是提交,宾语是订单。订单本身如果细化,它实际是一个由多行商品明细组成的承诺,不是一个代理。

如果你不小心把事件和代理搞混,后面的权限设计、审计追踪都会连锁出错。所以每建一个表,先用这个主语/谓语技巧自测一遍。

5.2 资源与状态边界不清

另一个常见问题是把“状态”当“资源”。库存是资源,但“库存状态”不是资源。订单是事件,但“订单状态”不是资源。商品可以处于“上架”“下架”“草稿”等多种状态,但 REA 里资源代表“有经济价值的可交换物”,状态只是资源在不同事件之间的存在表现。

如果把状态建模为资源,就会导致业务规则发散。正确做法是让状态通过事件自然推导,比如商品“上架”是一个事件,它把商品资源从内部草稿状态转换为可供销售状态。这样当你要问“从什么时候开始这个商品可以下单?”,答案就是一个事件时间,数据库里也能直接回溯。

5.3 一对多关系里的基数把握

还有一个容易被忽略的细节:资源-事件之间的数量关系并不总是一对多,很多场景是多对多。一个销售事件往往包含多个商品,一个商品也往往出现在多个销售事件中,所以event_resource_link是一条多对多关联表,而不只是event与resource上的主外键。如果你图省事,直接在资源表上挂一个event_id外键,那这个模型很快会因为复合商品、组合套餐、组装发货这类需求而炸开。

代理与事件之间也是多对多,一个事件有多个参与方,一个代理可以参与多个事件。角色信息也必须在关联表上保留,因为同一个客户在采购事件里是买方,在退货事件里是退货方,如果不存角色,审计的时候根本说不清楚。

5.4 几个可以反复使用的避坑经验

我单独把这几年用 REA 建模最容易翻车的几个点列出来,当成一份速查表。

  • 不要盲目把所有业务对象都塞进 REA 三张表;报表专用数据、运维元数据可以单独设计,REA 用于核心经济业务,不需要绑架全部系统。
  • 事件表的数据量增长非常快,所有事件记录几乎只增不改,建议导入归档分区机制,并且一二线环境上及时做好索引。
  • 关联表的“方向”字段不要吝啬,它才是资源流量分析的灵魂。有了方向,才能准确统计“资源流入总和”和“资源流出总和”,也才能对账。
  • 当业务方说“这里有个状态需要改一下”时,多问一句“到底是哪个事件导致了这个状态变化”。能不改字段,就不改字段;能新增事件,就新增事件。
  • 如果你所在团队已经有成熟的领域服务,不要推翻重来。REA 更适合用来做复盘和补漏,把现有系统里的核心业务对象逐一对照资源、事件、代理三类,往往能发现之前没想清楚的潜在问题。

这些经验,我建议你亲自在项目里试一遍。我在最初接触 REA 的时候,其实也走了弯路,为了追求“纯正的模型”把不少简单需求绕远了。后来才明白,REA 不是一个教条,它是一个用来帮助你理解业务的退路:当业务复杂到所有人争论不休时,退回到资源、事件、代理这三个词,往往能让大家重新对焦。建模的世界里没有银弹,但 REA 绝对是一把值得放进工具箱的耐用扳手。

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

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

立即咨询