☰
REA业务建模:用资源-事件-主体重构订单与库存设计
2026/10/21 5:53:56 网站建设 项目流程

写业务系统的人,几乎都有过同样的经历:需求评审会上,产品经理说“就是一套标准的订单加库存管理”,你回到工位,第一件事打开数据库设计工具,开始画订单表、订单明细表、商品表、客户表、收款记录表。这种从“表”出发的设计直觉,支撑了无数系统的快速上线,可等到要加退货、要跨部门对账、要做行为审计的时候,最初那张订单表反而成了最难受的约束。

今天想聊的 REA,恰好是反着来的。它不问你“要建几张表”,而是先逼着你回答三个词:你的业务里有哪些资源,哪些事件会改变资源,哪些主体参与了改变。把这三件事想清楚,表结构是水到渠成的事。这篇文章我就把 REA 从原理到落地完整拆开,用一个真实可复现的案例,带你把模型一步步翻译成数据库表,再聊聊我在实际项目里踩过的坑。

1. REA从哪里来,回到什么问题

REA 缩写对应的英文是 Resource-Event-Agent,翻译过来就是“资源-事件-主体”。它最早不是程序员发明的,而是上世纪八十年代会计信息系统研究领域提出的概念。当时研究者发现,传统的复式记账法太执着于“借贷科目”了,一笔销售进来要记应收账款、要记收入、要记库存减少,每个科目都是单独的一张表,业务链条被人为切碎。于是有人换了一个角度问:如果不考虑会计科目,一笔经济业务最本质的构成是什么?答案就是资源、事件、主体这三样。

这套思路在当时是用来改进数据库建模的,所以 REA 本质上是一套业务建模框架,不是某个具体的软件或者中间件。近几年它又重新进入开发者视线,原因是事件驱动架构、领域驱动设计、以及各类需要多方协作的账本系统越来越流行,大家发现 REA 描述业务的方式,和事件溯源、不可变日志这些现代实践天然能接上。

我把 REA 解决的问题总结成一句大白话:它让“业务语义”而不是“数据表”成为系统设计的起点。

传统的表驱动设计,是先定义订单、订单明细、商品、客户这些名词,再把动词(下单、付款、发货)写成状态字段或者日志表;REA 反过来,先把动词当作一等公民,也就是“事件”本身要建模成核心实体,然后围绕事件去关联资源、关联主体。

REA 的核心假设有三条,理解了这三条,后面所有设计决策就都顺了:

  • 企业本质上是一张由经济主体参与的交换与转换网络。没有主体参与的活动,比如系统自身的定时任务,不应该进入业务事件模型。
  • 所有值得记录的业务活动,最终都体现为资源状态的改变。如果一个动作没有让任何资源发生变化,它就不具备业务价值,不该进入核心建模范围。
  • 每一次事件都必须能追溯到一个或多个责任主体。谁做的、谁授权的、谁接收的,这些信息是业务语义的一部分,不是可有可无的审计字段。

第一点解释了为什么 REA 适合描述供应链、交易、资金流这类多人协作场景;第二点会直接影响建模粒度;第三点则是 REA 后来在审计和合规场景里被反复提及的根本原因。如果你正在设计订单中心、库存中心、资金账本这类核心系统,REA 就是那种能让后期少返工的底层思维工具。

有人会问,REA 是不是就是换了种画法的新版 ER 图?我觉得有联系,但区别非常明显。ER 图关心的是有哪些实体、字段、外键;REA 关心的是“谁、在什么时间、把什么资源、从谁手里转移到了谁手里”这个完整链条。ER 图是静态视角,REA 是动态视角。这也是为什么一个 REA 模型可以比较自然地演进成事件流、消息、账本类系统。

2. 资源、事件、主体,三个词决定建模边界

REA 只有三个核心概念,听起来谁都懂,但我在实践中发现,绝大多数人刚上手时都会在这三个词上栽跟头。要么是把资源定义成数据库里的“字段”,要么是分不清事件和普通操作记录的区别,要么是漏掉了外部主体这一层。

2.1 资源:能被控制和有价值,缺一不可

REA 里的资源,指的是资源所有权或控制权的主体能够支配、并且具有经济价值的事物。判定标准就两条:能不能控制,有没有价值。

举个例子。一家咖啡店里的咖啡生豆、烘焙好的熟豆、库存现金、储值卡余额,这些显然是资源。一台可以重复使用的咖啡机,也是资源,因为它能被控制,也有价值。甚至连“咖啡师工作一小时”这种服务供给,在 REA 里也可以建模为资源,因为它具备被消耗、被交易、被度量的属性。

但是“品牌影响力”“客户满意度”这类概念,虽然很重要,却很难被某个主体控制,也不存在清晰的单位度量,所以不适合直接建模成资源。很多初学者在这里卡住,其实记住一句话就行:REA 资源的第一特性是“可被交换或消耗”,虚的概念不要往这里塞。

资源也不等于会计上的“资产”。资产是会计期末的一种存量表达,资源是业务流程中的一个语义节点。你甚至可以为了建模方便,把“客户剩余积分”定义成一种资源,这在会计上没有任何科目对应,但在 REA 里完全合理。

一个资源实体,典型的属性包括资源编号、名称、描述、规格、单位、当前数量、状态等。注意,资源表只回答“我们手里有什么”,不回答“这些状态是怎么变成这样的”。状态变化的过程,是事件要管的事。

2.2 事件:资源状态变化的原子记录

事件在 REA 里指“经济事件”,也就是能导致资源流入、流出或状态改变的业务动作。它是整个模型里最核心、也最容易设计过度的部分。

我们做订单系统时,大多数人会把“下单”“付款”“发货”做成订单表上的状态字段:订单状态从待支付改成已支付,从已支付改成已发货。字段一变,历史信息就被覆盖了。REA 的做法完全不同,它会把“支付成功”当成一个独立事件落库,把“发货完成”也当成一个独立事件落库,订单只是这些事件围绕的一个业务上下文。

事件必须满足三个特性:

  • 原子性:一个事件对应一个不可再分的业务动作。比如“扫码支付成功”就是一个事件,“同时收款和发货”如果能拆开看,就绝对不能合并成一个。
  • 可追溯性:每个事件都要有明确的发生时间、参与主体和关联资源,能回答“发生了什么、谁做的、涉及哪批资源”。
  • 业务意图性:事件要对应真实的业务规则,比如“有销售订单才允许出库事件”,模型里的事件不能被随意构造。

这里特别要强调一个高频误解:事件不是日志。操作日志记录的是“系统里发生了一次点击”,事件记录的是“资源发生了实质性的流入流出”。日志可以随便打,事件必须遵循业务规则。这也是为什么很多进销存系统后期对账困难的原因——它们只有流水日志,没有真正的业务事件语义。

2.3 主体:内部主体和外部主体不能混为一谈

主体就是参与事件的个人、部门或者组织。REA 把主体分成两类:

  • 内部主体:企业内部的人或部门,比如采购员、库管员、财务、咖啡师。
  • 外部主体:企业外部的交易对手,比如供应商、顾客、承运商。

为什么要刻意区分?因为很多时候,一个事件需要同时关联一个内部主体和一个外部主体才完整。比如“向供应商采购咖啡豆”这件事,内部主体是采购员,外部主体是供应商;“顾客付款”这件事,内部主体是收银员,外部主体是顾客。

如果模型里只记录“某某操作员创建了一个采购单”,却没有把供应商实体关联上,那这个模型就是残缺的,因为它丢失了业务语义中最关键的那半截交易关系。REA 强调,一个完整的经济事件,至少要有一个负责的内部主体和一个对应的外部主体。这也是后来在权限与合规场景里,REA 模型明显比常规表设计更好用的原因:责任主体一开始就进了模型,而不是事后靠日志猜。

2.4 三种关键关系,建模的真正的骨血

理解 REA 不能只看三个实体,还要看实体之间的三类关系。

第一类是库存流关系(Stockflow),连接资源与事件。它表示某个事件让某个资源的存量增加了还是减少了。比如“采购入库事件”让咖啡生豆资源增加,“销售出库事件”让熟豆资源减少。库存流关系通常带一个方向标记,流入为正,流出为负。

第二类是参与关系(Participation),连接主体与事件。它表示谁参与了这场业务动作。内部主体和外部主体都可以和同一事件建立参与关系,只是角色不同。

第三类是互换关系(Exchange Duality),连接事件与事件。它表示一个业务循环中,资源流入事件和资源流出事件之间的配对。最典型的例子,就是“销售事件”(资源流出)和“收款事件”(资源流入)互为配对。你把资源交付给顾客,同时收到了对方的钱,这两个事件构成一个完整的交换。

基数规则直接决定业务约束。最基本的几条我整理在这张表里:

关系参与者典型基数说明
库存流资源 -> 事件一个事件可对应多条资源流,一条资源流只能属于一个事件一次采购可能入库多种咖啡豆
参与主体 -> 事件一个事件至少关联一个内部主体和一个外部主体没有主体的裸事件不允许存在
互换事件 -> 事件一个流入事件可与一个或多个流出事件配对一笔收款可以核销多笔销售订单

有朋友会问,这些基数规则会不会太死板,如果业务里有“匿名顾客自助下单”呢,外部主体怎么处理?通常的做法是把“未知顾客”或“散客”作为一条默认主体记录,而不是让事件缺失主体。这个习惯非常关键,因为一旦模型允许没有外部主体的事件,后续所有对账和审计逻辑都得堆在应用代码里,数据库层面就失控了。

3. 一个咖啡店闭环的完整建模过程

理论说得再多,不如完整走一遍建模。我拿“咖啡店”这个场景来演示,因为它足够小,又覆盖了采购、生产转换、销售、收款多个环节。这一步大家可以照着思路套任何一个进销存或订单场景。

3.1 第一步:圈定要建模的业务闭环

建模第一件事不是列表,而是划范围。不要一上来就想把整个咖啡店的经营数据全塞进 REA 模型,先圈定一个业务闭环。

我选“采购-销售-收款”这条主链。咖啡店从供应商那里买咖啡生豆,经过烘焙变成熟豆,再制作成咖啡卖给顾客,最后收到顾客付款。闭环里的资源变化有几个:生豆增加、生豆减少、熟豆增加、熟豆减少、现金增加。

3.2 第二步:把业务名词归位到资源、事件、主体

把刚才的业务流程拆分出来,每个元素对号入座。

资源包括:

  • 咖啡生豆(原材料)
  • 烘焙熟豆(半成品)
  • 外带杯、牛奶、糖包(辅料)
  • 门店现金和电子账户余额(资金)
  • 咖啡师制作时长(服务,如果要把人工成本也建模进去)

事件包括:

  • 采购入库事件:向供应商购买生豆,生豆流入
  • 烘焙事件:生豆消耗、熟豆产出(这是一个转换事件,涉及两个资源流)
  • 销售出库事件:熟豆消耗、咖啡产品转交给顾客
  • 收款事件:顾客付款,现金或电子余额流入
  • 付款事件:门店向供应商支付货款,现金流出

主体包括:

  • 外部:咖啡豆供应商、顾客
  • 内部:采购员、烘焙师、咖啡师、收银员、财务

这里注意一个细节:烘焙事件是一个“转换”,不是“交换”。它只有一个内部主体参与,没有外部对手方;它同时涉及一个资源的流出(生豆减少)和另一个资源的流入(熟豆增加)。REA 模型里这种“生产/转换”事件很常见。

3.3 第三步:把模型翻译成数据库表结构

现在开始把上述概念落到表结构。我的习惯是先建事件相关表,再建资源表,最后补主体表。因为事件是整个模型的中枢。

资源表:

CREATE TABLE resource ( resource_id INTEGER PRIMARY KEY, resource_code VARCHAR(32) NOT NULL UNIQUE, resource_name VARCHAR(128) NOT NULL, spec VARCHAR(64), uom VARCHAR(16) NOT NULL, -- 计量单位 current_qty DECIMAL(18, 2) NOT NULL DEFAULT 0, status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' );

主体表:

CREATE TABLE agent ( agent_id INTEGER PRIMARY KEY, agent_code VARCHAR(32) NOT NULL UNIQUE, agent_name VARCHAR(128) NOT NULL, agent_type VARCHAR(16) NOT NULL, -- INTERNAL / EXTERNAL contact_info VARCHAR(255) );

事件表:

CREATE TABLE event ( event_id INTEGER PRIMARY KEY, event_code VARCHAR(32) NOT NULL UNIQUE, event_type VARCHAR(32) NOT NULL, -- PURCHASE / PRODUCE / SALE / RECEIPT / PAYMENT occurred_at TIMESTAMP NOT NULL, internal_agent_id INTEGER NOT NULL REFERENCES agent(agent_id), external_agent_id INTEGER REFERENCES agent(agent_id), note VARCHAR(255), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP );

库存流关系表,用来关联事件和资源,同时区分方向:

CREATE TABLE stockflow ( stockflow_id INTEGER PRIMARY KEY, event_id INTEGER NOT NULL REFERENCES event(event_id), resource_id INTEGER NOT NULL REFERENCES resource(resource_id), direction SMALLINT NOT NULL, -- 1 流入, -1 流出 qty DECIMAL(18, 2) NOT NULL, unit_price DECIMAL(18, 2) -- 可选,记录资源变动时的单价 );

互换关系表,把销售事件和收款事件配对:

CREATE TABLE exchange_pair( pair_id INTEGER PRIMARY KEY, inflow_event_id INTEGER NOT NULL REFERENCES event(event_id), outflow_event_id INTEGER NOT NULL REFERENCES event(event_id) );

这个结构看起来多了一张表,但它换来了一个非常大的好处:资源变动的历史永远不会被覆盖。订单状态被改成“已取消”之后,你还能看到曾经发生过一笔销售事件;付款金额拆成三期,就生成三条收款事件,再通过 exchange_pair 全部挂到同一个销售事件下面。

3.4 第四步:写几条 SQL 验证模型是否好用

如果模型正确,常见业务问题的查询应该非常直接。

查某天卖了多少杯咖啡,可以这样:

SELECT DATE(sf.event_time) AS sale_date, SUM(sf.qty) AS total_cups FROM stockflow sf JOIN event e ON sf.event_id = e.event_id WHERE e.event_type = 'SALE' AND sf.resource_id = (SELECT resource_id FROM resource WHERE resource_code = 'ROASTED_BEAN') AND DATE(e.occurred_at) = CURRENT_DATE GROUP BY DATE(sf.event_time);

查某个供应商最近一个月给我们供应了什么:

SELECT r.resource_name, SUM(sf.qty) AS total_in_qty FROM stockflow sf JOIN event e ON sf.event_id = e.event_id JOIN resource r ON sf.resource_id = r.resource_id WHERE e.event_type = 'PURCHASE' AND e.external_agent_id = ( SELECT agent_id FROM agent WHERE agent_code = 'SUPPLIER_001' ) AND e.occurred_at >= date_trunc('month', CURRENT_DATE) - INTERVAL '1 month' GROUP BY r.resource_name;

这类查询在传统的“订单表+状态字段”设计里很难写,因为你需要找销售的日志表去关联。而在 REA 模型里,事件表就是天然的明细账,stockflow 表就是天然的发生额,查询和审计都变得非常机械化。

3.5 这个模型怎么支持后续扩展

咖啡店如果要加“退货”场景,只需要新增一个“退货入库事件”和对应的“退款事件”,销售退货会让资源重新流入,退款会让资金流出,两个事件再通过 exchange_pair 配对。不需要给订单表加“是否退货”字段,也不需要动历史销售事件。

如果要加“促销买一赠一”,可以把赠品建模成同一销售事件下额外的库存流出,只不过单价为零。原有结构不需要大改。这是 REA 模型最值钱的地方:它能以新增事件的方式承载新业务,而不是每加一个业务规则就改一次既有表结构。

4. 为什么 REA 和现代事件驱动架构能接上

写这一段是因为我最近一年多,明显感觉到 REA 开始从理论书里走出来,进入真实的工程实践。这不是偶然,因为软件架构的流行方向变了。

传统系统保存的是“最新状态”,订单表里永远只有当前状态,历史被覆盖。事件溯源架构保存的是“状态变化序列”,想要任何时点的状态,就把事件重放到那个时点。REA 的核心视图和这个思想完全一致:资源表只是存量的物化视图,真正权威的数据是事件表和库存流表。

用术语来说,事件表是事实数据,资源表是投影数据。

这样做的好处很明显。最直接的一点,库存数据不准确的时候,你不用去猜库存是怎么变成这样的,而是直接把事件从第一个重放到最后一个,逐步比对。某个环节录错了,修正该事件的 qty,后续所有结果自动跟着修正。这种能力在传统“库存表直接 UPDATE”的设计里是做不到的,改了库存字段你就丢掉了原因。

还有审计和合规场景。现在很多行业越来越看重操作留痕,传统系统通常单独搞一张审计日志表,记录的是“谁在什么时间操作了什么页面”。REA 根本不额外做这套东西,因为业务事实本身就包含主体和事件。你查“这个月有哪些销售订单存在未经授权的折扣”,可以直接从事件表、参与关系中查到责任人。审计日志在 REA 模型里不是附加功能,而是核心模型的直接产物。

另外,每次我在设计涉及多个企业协作的系统时,都会把 REA 模型拿出来铺在桌面上。传统单机系统的表结构是围绕一个企业内部权限设计的,而多方协作系统里,资源在不断的流入流出,每个主体都要对资源变化负责,REA 几乎就是为这种场景定制的。

我去年参与过一个模拟的跨企业供应链数据平台,最初大家拿出来的方案都是“订单表、对账单表、应付款表”,结果一开工就卡在标准不统一:A 公司的“已发货”到底对应 B 公司的“在途”还是“已收到”?后来换成了 REA 思路,把各家的业务都拆成“资源流+事件+主体”,下游做对账时只需要匹配事件 ID 和库存流方向,语义冲突一下少了很多。

当然 REA 也不是万能的。它不擅长表达复杂的流程编排,比如审批流、状态机,这些还是要用工作流引擎。但它非常适合表达事实发生的记录和追溯,这个定位和“账本”类系统是完全一致的。

5. 落地时候最容易踩的坑

理论模型讲得再好,拿到真实项目里总会有几个反复出现的坑。我结合自己多次实际使用的经验,把这些坑列出来,希望你能少走点弯路。

5.1 事件粒度太细或太粗,后期都难受

粒度选择是 REA 建模里最主观也最容易失控的一步。同样一笔销售,有人用一个“销售事件”搞定,有人把“接单”“出杯”“交付”拆成三个事件。我的建议是,粒度的基准线是“你能不能独立地、有意义地回答一次资源变化”。

咖啡店场景里,“卖出一杯咖啡”对应熟豆减少一杯量、现金增加一杯价格,这就足够了。如果你拆到“称豆子 20 克”和“热水注入”这种动作,资源语义就被切得太碎,后续没有人能够把这么多事件拼回一笔业务。

反过来,“售出商品”和“交付商品”如果明明发生在不同时间点、由不同人完成、对应不同风险责任,就一定要拆成两个事件,否则退货和售后永远不会清楚。判断标准很简单:当你要回答“顾客是否已经拿到货”时,如果同一个事件既能表示“已下单”又能表示“已交付”,就说明粒度太粗了。

5.2 事件 ID 幂等性必须提前设计

真实系统不是单机玩具。对接第三方支付回调、多终端上传、消息队列重试,同一个业务事件完全可能重复推送。如果不做防重,每次重复推送都会产生一条库存流记录,你的账永远会平白多出一份。

我的做法是给事件表加一个 external_biz_no 字段,存业务侧产生的唯一编号,比如订单号加业务类型后缀,然后对它建唯一索引。

CREATE TABLE event ( ... external_biz_no VARCHAR(64) NOT NULL, CONSTRAINT uk_event_biz_no UNIQUE (external_biz_no) );

每次写入事件之前先查询,如果已经存在就跳过。这个改动成本极低,但能省掉后续对账时大量“神走的库存”。这一条建议无论用不用 REA,任何事件表设计都应该执行。

5.3 资源余额不一定要实时死算

新手很容易把 current_qty 当成事务里必须强一致更新的字段。每插入一条事件,就立刻 UPDATE 资源表,库存变少了就扣减。小数据量没问题,但一旦库存操作并发上来,资源表就变成热点行,大量请求互相等待,性能直线下降。

更合理的做法有两个路径:一是把资源余额当成一个可异步更新的投影,事件插入后通过领域事件异步更新资源表;二是在查询时通过事件表实时聚合得出余额,资源表只存基础信息和上下限,不参与即时事务。

短期一致性的最终一致性策略,在这个场景里完全够用。毕竟你要的是最终账面准确,不是毫秒级同步。用 REA 建模最大的福利之一,就是你不再需要靠“更新表”来计算余量——事件流本身就是最可靠的余额来源。

5.4 主体和责任缺失,是模型腐败的开始

很多团队建 REA 模型时,资源表和事件表都设计得很认真,唯独在“内部主体”和“外部主体”上偷懒,觉得“先记一笔再说”。这是模型腐败的开始。

一个事件如果没有主体,将来审计的时候就是一个黑洞。普通系统里你可以用日志去追查“谁干的”,但 REA 的主旨就是责任归属一开始就必须进模型。没有责任人的事件,要么用默认主体补上,要么就不允许写入。

我个人的落地习惯,是给事件表加上两个字段:creator_agent_id 和 owner_agent_id。creator 是操作执行人,owner 是事件对应的业务责任人。很多系统里的越权和责任纠纷,靠事件表追溯时一眼就能看出来。你甚至可以为了保证部分场景,给事件表加操作类型字段,让“谁创建、谁修改、谁审核”变成可查询的轮廓。

5.5 从 REA 反向驱动表结构的实现清单

最后,分享一套我从 REA 落地到实际项目的完整步骤,基本可以当 Checklist 用:

  1. 先圈定业务闭环范围,不要求覆盖所有模块,从一条有始有终的主链开始。
  2. 列出闭环内所有资源,严格用“可控制且有经济价值”这条标准筛掉虚概念。
  3. 列出资源状态发生变化的事件,用“是否原子、能否追溯、是否有业务语义”校验。
  4. 为每个事件指定内部主体和外部主体,缺主体的事件回头补上默认主体。
  5. 在事件与事件之间建立互换关系配对,让业务循环闭合。
  6. 设计事件表,加上 external_biz_no 唯一编号和创建人/责任人字段。
  7. 设计库存流关系表,把事件与资源通过方向连接起来。
  8. 资源表只承担存量投影职责,别让它替代事件表。
  9. 设计查询和统计 SQL,用真实业务问题验证一遍模型,比如周销量、应收未收、库存余额。
  10. 再进入实际接口、页面开发。

我自己实际使用下来的体会是,刚开始从 REA 思考问题时确实别扭,人都习惯先画表,但一旦在关键业务模块落地,后续迭代新需求会特别省劲儿。早年间我做一个进销存项目,因为没有用这套模型,每次加退货和换货都像在给旧房子打补丁;后来换了一版新系统,从一开始就用事件和资源流来建模,同样场景的改动量小了非常多。

最后再分享一个小技巧,也算是最实在的收尾:不管你的系统最终用不用完整 REA,都建议在所有核心业务表里保留责任主体字段,哪怕只是把“操作人标识”单独拎出来建个索引。等到有一天你真正需要回答“这笔资源变化是谁负责”的时候,你会回来感谢这个决定。

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

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

立即咨询