☰
REA建模实战:用资源、事件、代理取代状态字段
2026/10/21 14:24:46 网站建设 项目流程

有次评审一个供应链系统的状态设计,业务方反复跟我强调“订单就应该是订单,你给我拆成采购单、入库单、销售单、出库单这么多表干什么”。当时数据库里确实一堆以 status 字段为核心的业务表,程序里到处在判断“待支付变成已支付”“已支付变成已发货”,改到后面自己都分不清哪个状态是哪个节点产生的。后来我换了个建模视角,先把“订单”这个概念本身丢到一边,只问三个问题:哪些东西是被消耗或增加的,哪些动作导致了变化,以及谁有权利发起这些动作。这三个问题的答案,串起来就是 REA 的核心骨架:资源(Resource)、事件(Event)、代理(Agent)。这套模型最早在会计信息系统领域被作为一种语义建模方式提出来,后来逐渐被用到业务系统设计、领域驱动建模、财务对账系统里。这篇文章就是围绕 REA 做一次完整的建模实战拆解,以一套果蔬批发系统为例,讲清楚概念、建模步骤、物理表落地、对账机制,以及我踩过的坑。想把业务模型做得更贴近真实业务、少在状态上反复救火的读者,这篇文章应该能帮到你。

1. 从“订单不只是一张表”说起:REA建模的底层视角

1.1 传统关系建模的痒处:状态字段越改越糊

做业务系统久了会发现一个规律:凡是重度依赖状态字段的模块,维护成本一定高。原因倒不复杂——状态字段描述的是“结果快照”,而不是“变化过程”。你看到一条订单记录的 status = 3,只能说明它到了某个阶段,但说不清楚它经历了什么、是谁在什么时间让订单产生了变化、当时对库存和资金产生了哪些实际影响。

比如一个典型的订单表,里面会有下单时间、支付时间、发货时间、签收时间,这些时间字段其实是在用“多个快照”模拟事件发生顺序。一旦出现售后、部分退款、部分发货,状态就爆炸了。是我先退款再发货,还是先发货后退款?是两个独立动作,但传统表设计里往往只保留一个最终状态。

REA 的思路不是这样,它把“变化过程”放回建模的中心。事件是一等公民,状态只是事件累计的结果。你要知道某个订单现在是什么情况,就去查它关联过哪些事件,而不是读一个可能已经写乱的 status 字段。

1.2 REA 是什么:资源、事件、代理三角色

REA 的全称拆开是三个概念,Resource、Event、Agent,对应我在开头说的三个问题:

  • 资源:业务中会被创造、消耗、交换或转化的“有价值的东西”。现金是资源,库存果品是资源,固定资产是资源。它不是静态档案,而是必须会因业务动作产生增减的东西。
  • 事件:直接导致资源增减或权责关系变化的动作。采购入库、销售出库、收款、付款、报损耗,都是事件。
  • 代理:发起或参与事件的人、部门、企业。采购员是代理,供应商是代理,客户是代理,我们自己公司也是一类代理。

关键点在“事件即中心”。资源不自己消失,必须有一个事件让它消失;代理不直接改资源,代理是发起事件的主体。资源、事件、代理之间天然组成一张网状关系,而不是传统ER模型那种“表-表”的平面关系。

1.3 为什么会计领域会诞生这套模型

REA 的思想源头在会计信息系统。早期会计系统的数据模型往往围绕“科目”来建,借贷分录是核心。但科目思维有一个短板:它关注的是记账结果,而不是业务本身发生了什么。一笔应收账款的增加,背后可能是卖了一批菜,也可能是垫付了一笔运费,如果不绑定关联的业务事件,数据很容易变成一本糊涂账。

会计领域于是有人提出一个新问题:一套好的业务模型,能不能先不依赖借贷规则,先把“经济后果”描述清楚?结果就是 REA。资源对应经济资源,事件对应经济事件,代理对应经济主体,三者组成的关系能完整表达“谁在什么时间、以什么方式、改变了什么资源”,借贷分录反而成了从 REA 推导出来的记账实现。这一点后来被软件开发者看中,因为它天然适合描述需要审计、追溯、对账的业务系统。

2. 拆开REA的三个核心要素:资源、事件、代理到底指什么

2.1 资源:只有“会被经济事件改变”的才算

建模时最容易摸不清的就是资源。很多同事第一反应是“商品表就是资源表”,其实不准确。资源不是用于展示的商品档案,而是“参与经济存量和流量的实体”。水果的商品档案里有品名、条码、规格,这部分属于主数据;但库存水果才是资源,因为它会被入库事件增加、被出库事件减少。

判断一个东西是不是资源的办法很简单:问一句,它会被哪个事件改变?如果有人给你报了个损耗数,那么被损耗的东西是资源;如果只是描述性的、不会因事件而增减的信息,那就不是资源,归属主数据或其他维表。

还有一个容易忽略的知识点:现金和应收账款也算资源,而且相当重要。很多团队做库存系统时只盯着货物,不看资金,导致货款流向和业务流向对不上。REA 建模时,资金资源和货物资源要同时建模,因为它们会通过销售、采购这类事件发生联动。

2.2 事件:系统里唯一允许写资源的地方

事件是整个 REA 模型的操作入口,记住一条原则:任何资源的增减只能由事件触发,程序里不允许直接 update 资源表里的数量字段。这条规则听起来简单,落地后能避免一大类并发和一致性问题。

事件又分几种类型,常见的是:

  • 获取事件:从外部获得资源,比如采购入库、现金收款。
  • 转换事件:资源在内部发生形态或性质变化,比如把整箱苹果分装成小盒、把鲜果做成切盘。
  • 交换事件:资源在不同代理之间发生所有权转移,比如销售出库、采购退货。

我通常还会给事件加一个生命周期:计划态、执行态、完成态、补偿态。比如“今天要出库100斤”是计划事件,“实际称重出库95斤”是执行事件,“其中有5斤在搬运中损坏改成损耗事件”就是补偿或修正。这样比单一状态字段更能表达业务波折。

2.3 代理:权责人划分,内部代理和外部代理

代理是发起事件的“人”。不是指具体某个员工,而是员工所代表的角色或组织单位。代理需要区分内部和外部:内部代理是我们公司的岗位角色,比如采购员、仓管员、销售员;外部代理是业务对手,比如供应商、批发客户、物流承运方。

很多传统表设计把“供应商ID”“客户ID”挂在单据上就完事了,不区分发起人和责任人。REA 要求明确事件的控制关系:这个采购事件是谁发起的,归属哪个内部代理;这个销售事件指向哪个外部代理;收钱事件里,钱是从哪个客户身上收来的。这样权责边界清楚了,后续做权限、做对账、做责任追溯都有了依据。

2.4 三类事件和三种关系的组合方式

REA 虽然只有三个核心概念,但概念之间的关系是固定的三种,所以组合起来并不散乱:

  • 存量-流量关系:事件与资源之间的关系,描述资源被增加或减少。一个采购事件增加库存水果,一个销售事件减少库存水果,都是这类关系。
  • 二元关系:一个事件与另一个事件之间的双向配对,描述业务上的“一进一出”或“一前一后”。采购入库事件与付款事件配对,销售出库事件与收款事件配对。
  • 控制关系:事件与代理之间的关系,描述该事件由谁发起、对谁负责。

这三个关系的组合方式可以用这个表格说明:

关系类型连接的主体表达的业务含义典型例子
存量-流量关系事件 -> 资源资源因该事件是增加还是减少采购入库事件 -> 库存果品增加
二元关系事件 <-> 事件两个经济事件互为业务对应销售出库事件 <-> 收款事件
控制关系事件 -> 代理谁发起、谁受益、谁负责采购事件 -> 采购员;销售事件 -> 客户

一旦这套关系画出来,整个业务系统的语义图基本成型。后面对账、审计、流程编排都从这张图上推导。

3. 实战建模:用REA做一套果蔬批发系统的领域模型

3.1 需求对象界定与领域名词收集

直接讲我做过的一个模拟项目,一套果蔬批发系统。业务背景很简单:一家批发商从农户手里收蔬菜水果,存入冷库,然后卖给周边餐饮门店和生鲜超市。要求能做采购入库、销售出库、损耗登记、收付款对账,还要能回答“某个客户还欠多少钱”“哪批菜的损耗率异常”这类问题。

建模第一步不是画表,而是收集领域名词。我把业务方常说的词全部列出来:农户、供应商、采购员、月结客户、现金客户、采购单、入库单、销售单、出库单、对账单、冷库库位、香蕉、苹果、损耗、分拣、整箱入库、拆零出库、预付款、货款、欠款。我一边听一边在纸上打标,哪些是资源,哪些是事件,哪些是代理。

这个过程很关键,因为业务方的语言里混着表名、单据名、实体对象名和报表指标,如果不先归到 REA 的三角色里,后面建出来的模型很容易变成“旧表换新名”。

3.2 识别资源、事件、代理

接下来把上面这些名词分类。

资源主要有两类,一类是货物资源:库存果品,单位可以是箱、斤。一类是资金资源:现金账户、应收账款、应付账款。不需要把每一批货单独定义为资源,因为批次、保质期这些属于事件或明细属性,而不是资源本身。

事件包括:采购入库事件(增加库存果品)、采购付款事件(减少现金或应付账款)、销售出库事件(减少库存果品)、销售收款事件(增加现金或应收账款)、损耗登记事件(减少库存果品)、分拣转换事件(整箱原料转为拆零商品)、库存盘点调整事件。

代理包括:内部代理是采购员、仓管员、销售员、财务员;外部代理是农户、客户、物流公司。需要注意的是,同一个主体在不同业务场景下可能承担不同角色,比如一个农户既是供应商,也可能直接开车来门店买货变成客户,所以代理角色要独立建模,不能把“供应商主体”和“客户主体”写成两个物理表。

3.3 实体关系落地

这段我用文字加表格来描述最终的关系结构,不用图,因为文字足够讲清楚。

  • resource 资源表:保存货物资源和资金资源的档案,product_name、unit、resource_type,其中 resource_type 区分“库存果品”和“资金账户”。
  • agent 代理表:保存内部角色和外部主体,agent_name、agent_type(内部/外部)、role_type。
  • economic_event 经济事件表:保存每个事件的元信息,event_type、event_time、description、关联发起代理、关联外部代理。
  • flow_detail 存量流量明细表:保存事件和资源的增减对应关系,direction 表示正向还是负向,quantity 和 amount 记录具体影响。
  • event_link 事件配对表:保存二元关系,即两个事件之间的业务勾稽关系,比如销售事件对应哪个收款事件。

资源、事件、代理的实体关系如下表展示:

实体对关系类型关系说明
resource <- economic_event存量-流量一个事件增减多个资源,一个资源被多个事件影响
agent <- economic_event控制一个代理发起多个事件,一个事件涉及多个代理
economic_event <-> economic_event二元两个事件通过 event_link 配对,形成业务闭环

3.4 转成物理表:SQL片段

有了关系结构,落成数据库表就不难了。以下是核心表的精简建表 DDL,完整字段做了裁剪,保留了主干:

create table resource ( id int primary key, resource_name varchar(50), resource_type varchar(20), -- 'goods' 货物 / 'money' 资金 unit varchar(10) ); create table agent ( id int primary key, agent_name varchar(50), agent_type varchar(10), -- 'internal' 内部 / 'external' 外部 role_type varchar(20) -- 采购员、仓管员、客户、供应商等 ); create table economic_event ( id int primary key, event_type varchar(30), -- purchase_in / sale_out / payment / receipt / loss ... event_time timestamp, internal_agent_id int, external_agent_id int, description varchar(255), foreign key (internal_agent_id) references agent(id), foreign key (external_agent_id) references agent(id) ); create table flow_detail ( id int primary key, event_id int, resource_id int, direction smallint, -- 1 增加 / -1 减少 quantity numeric(18, 3), amount numeric(18, 2), foreign key (event_id) references economic_event(id), foreign key (resource_id) references resource(id) ); create table event_link ( id int primary key, left_event_id int, right_event_id int, relation_type varchar(20), -- 'sales_receipt' 销售-收款 / 'purchase_payment' 采购-付款 foreign key (left_event_id) references economic_event(id), foreign key (right_event_id) references economic_event(id) );

建表之后有个很实际的好处:查询业务状态时,很少需要写复杂的状态判断 SQL,更多是聚合事件表。比如查“某个客户至今还剩多少欠款”,思路是找这个客户的销售事件总额,减去已经和它配对的收款事件总额,两者相减就是应收余额,而不是查客户档案上的某个余额字段。这种查询在 building 对账功能时尤其顺手。

3.5 关键查询:未对账单据

下面这个查询是销售对账的基础逻辑,找到已经做了销售出库、但还没有通过 event_link 配对上收款事件的单据:

select e.id as sale_event_id, e.event_time, a.agent_name as customer_name, sum(coalesce(fd.amount, 0)) as sale_amount from economic_event e left join event_link el on (el.left_event_id = e.id and relation_type = 'sales_receipt') join agent a on a.id = e.external_agent_id join flow_detail fd on fd.event_id = e.id where e.event_type = 'sale_out' and el.id is null group by e.id, e.event_time, a.agent_name;

这种写法在传统订单表里反而绕,因为传统表需要维护一个应收余额字段,再通过写在别的模块的收款单去更新它,很容易更新漏。REA 里应收余额不是一个被更新的数字,而是“已销售且未配对的销售事件”的聚合结果,天生不会产生更新遗漏。

4. 从模型到实现:聚合边界、状态机与对账机制设计

4.1 事件即聚合根:如何划分一致性边界

领域驱动设计里经常争论聚合根怎么划,REA 给了一个非常自然的答案:事件就是聚合根。一个采购入库事件,聚合它名下的所有 flow_detail,确保“入库单的明细行”和“库存增减明细”在同一个事务里被写入。一个销售出库事件同样如此。

不同事件之间不要强行做成一个大事务。采购入库事件和付款事件完全可能在时间上错开,今天入库,下个月才付款,如果把它们放在同一个聚合里,事务边界会扩大到难以维护。REA 的二元关系正好支持这种松耦合:两个事件各自成为聚合,通过 event_link 在后续关联。

这个设计带来的好处是并发能力提升。两个销售订单同时出库,操作的是不同的事件聚合并发写库,只要不是同时对同一个资源做相反方向的扣减,冲突概率就低。而传统“库存表余额字段”方案每次都要锁同一行,并发一高就出事。

4.2 状态不是字段,是一连串事件的结果

状态机在这个模型里变成了事件链的可视化结果。举个例子,一张销售单当前处于什么状态?不需要一个 status 字段,直接看它关联的事件:还没建销售事件,就是“草稿”;销售事件已经创建但还没和收款事件配对,就是“已发货待收款”;销售事件已经和收款事件完成配对,就是“已结清”。

这样的好处是状态的含义从数据库里“硬编码”变成了可推导的业务逻辑。如果想新增一个“部分收款”的状态,不需要改表加字段,只需要允许一个销售事件和多个收款事件通过 event_link 配对,状态表达式从“是否已配对”变成“配对了百分之多少”。这种演进能力是传统 status 字段很难做到的。

不过要把这套状态推导大规模落在代码里,建议给领域服务提供几个只读方法,比如 is_settled(event),settled_ratio(event),外部不要自己拼 SQL 去算。否则每个模块各算各的,逻辑就散掉了。

4.3 对账与补偿:REA天然的双向查验

我见过太多把对账做成“凌晨批量任务比对两张表”的系统,查出来不平也不知道问题从哪一步开始。REA 模式下,对账不需要批次任务累加比对,而是结构性的:每一笔销售事件必须对应至少一笔收款事件,每一笔采购事件必须对应至少一笔付款事件。对账就是检查 event_link 里是否有未闭合的孤点。

实际业务里有大量非标准情况:客户先付定金,后面分批提货,最后少提了5斤要求退款,这些在 REA 里都是事件组合问题。定金是一笔资金资源增加事件;分批提货是多个销售出库事件;退款是一笔资金资源减少事件。用 event_link 把多个事件互相配对,就能表达“一笔款配多笔货”“一笔货被多次付款”的复杂情况。

关键是 REA 把“权责关系”和“事件时效”绑定在事件树上,天然支持补偿操作。做错了就用反向事件去冲,而不是去修改历史事件的字段。这样审计线索是完整的,不会出现“昨晚批量脚本改错了3个月前的订单但没人知道”的尴尬局面。

4.4 落地时的读模型:允许为了报表做物化

讲到这里可能有人会问:如果所有查询都靠聚合事件,报表性能会不会很吃力?会,所以成熟的落地方案是 CQRS 思路:写模型用事件表,保持干净和可追溯;读模型专门做物化投影,生成库存余额表、应收报表、销售汇总表,用异步方式更新。

什么意思?就是把 REA 当成可信数据源,报表和列表查询从物化表里读取。比如凌晨或每次事件写入后,用一段聚合逻辑生成“今日库存快照”和“客户应收汇总表”。物化表和事件表同时存在,到期对账发现不一致时,永远以事件表为准,重建物化表即可。

我强烈建议不要在写路径上直接去更新各种汇总字段,否则 REA 的追溯优势会被吞掉,退化成传统“订单表+余额表”的老路。正确姿势是事件表只追加,物化表可以批量重建。这个架构虽然在初期看起来多一张表,但维护成本远低于反复修补状态字段。

5. REA建模的高发翻车点与适用边界判断

5.1 翻车一:把一切状态流转都塞进事件

有同事一开始相当兴奋,把“用户点击了商品详情”“购物车加了一件商品”“浏览页面”全都建模成事件,加了海量事件表,最后仓库里堆了几千万条无意义记录。这里要分清楚:REA 关注的是经济事件,也就是能改变资源、产生权责关系的动作。一个商品被加入购物车不改变所有权,也不产生资金增减,它是业务行为但不是经济事件,不需要进 REA 模型。

判断标准还是老办法:这个动作是否让某个资源的存量发生变化?是否让代理之间的权责发生变化?都没有的话,请放到行为日志或风控系统里,别塞进领域模型。事件定义太多太杂,会让核心模型失去主心骨。

5.2 翻车二:资源粒度定错,不是每个批次都要成为资源

果蔬批发里经常遇到批次问题:这一批苹果进价2元,那一批进价3元,能不能把每一批都建成一个资源?如果这么建,资源表会膨胀到无法维护,而且“批”和“货”的边界会被无限拆分,后面任何跨批次的业务逻辑都变得极其难写。

正确的做法是保持资源的可替换性:资源是一类货物,比如“苹果”就是资源,批次、产地、进价是事件或明细的属性。两种不同进价的苹果混在一起出库时,出库事件里记录数量即可,成本核算通过明细金额或者先进先出/加权平均规则计算,不应该靠物理拆分资源表来实现。记住:资源关心的是“可被事件改变”,不是“每一件实物都要一个编号”。

5.3 翻车三:忘了外部代理,账对不上

如果只做仓库进销存,忽略外部代理还能凑合,但一旦涉及应收应付,这个坑立刻炸开。我见过一个库存系统,销售出库事件都建了,却没有登记外部客户代理,结果财务对账时根本不知道哪笔销售对应哪个客户的货款,只能对着几张零散的Excel发挥想象力。

建议从建模型第一天就把外部代理补齐,哪怕当前没有财务需求。供应商、客户、承运商都是外部代理,把它挂在事件上成本很低,回头建对账模块就不需要再补数据。还有一点,内部代理和外部代理在表设计上要留 agent_type 字段,名称可以重复,但角色要区分清楚,否则后面做数据隔离时无从下手。

5.4 REA vs 传统ER建模:该不该用、用在哪儿

REA 不是万能的,它也有不合适的场景。我做过的项目里,那种面向终端、CRUD 简单、业务方只关心“结果数据录进去能查出来”的内勤型系统,用传统 ER 建模反而更快,没必要引入事件语义来增加复杂度。

但在以下场景,REA 价值明显更大:需要财务对账、审计追溯的进销存和供应链系统;业务流程长、涉及多方代理协同的平台;极容易发生状态分叉、退货退款、部分结算的交易系统。判断方法很简单:如果业务方反复追问“这个数据是哪笔业务产生的”“这两个单子为什么对不上”,REA 大概率适合你。

对比维度传统ER建模REA建模
建模视角实体对象和关系经济事件与资源流转
状态表达状态字段快照事件链推导
追溯能力弱,靠日志补偿强,结构天然支持审计
对账能力需要额外开发比对逻辑事件配对即对账
变更响应加字段、改枚举加事件、改配对关系
学习成本低中高

6. 复盘:一项真实供应链项目中REA的落地体验

6.1 第一次试点的混乱与纠正

我在一个模拟的农产品供应链项目里第一次完整落地 REA,开头两周团队是痛苦的。大家已经习惯把“订单表”“库存表”当成思考原点,我让他们先写事件流,再找资源,他们写出来的东西还是一股订单味:“创建订单事件”“修改订单事件”“删除订单事件”,这根本不是经济事件。

后来我换了个引导方式:不要从已有单据反推,而是逼自己站在业务现场描述事实。“农户送来了50箱香蕉,仓管员验收后放进冷库;客户来拉货,销售员称了30箱拿走;月底财务给农户打款。”把这类事实逐条拆成“谁对什么资源做了什么”,模型才慢慢正过来。所以试点阶段的混乱不可怕,关键是别急着写代码,先在事件流上反复磨。

6.2 REA带来的最大红利:业务语言对齐

这个项目做到中期,我发现 REA 最大的价值不在数据库设计,而在沟通。业务方和我讨论“某个客户为什么还有欠款”时,我只需要指着一串事件问他:哪笔销售你没收款?哪笔收款还没配对到销售?他能直接回答,而不是对着状态字段猜。

后来业务方在评审会上甚至自己会用“事件”说话了,主动说“损耗要单独记一笔事件,不能直接改库存”。这种业务语言和模型语言的一致性,是传统 ER 模型很难带来的。因为 REA 的记录对象是“业务真相”而不是“软件数据对象”,双方看到的是同一张图。

6.3 小技巧:先列事件流,再找资源和代理

如果只让我留一条实操建议,就是这句话:先列事件流,再找资源和代理。不要从数据表开始,也不要从页面原型开始,先把业务过程中发生的事情按时间顺序列出来。采购入库、销售出库、收款、付款、损耗、盘点调整,按事实写下来。

事件流列完了,资源自然浮现——被这些事件增加或减少的东西就是资源;代理也自然浮现——谁发起这些事件、谁和我们对等交易就是代理。最后再用二元关系把前后事件配起来,整个模型就闭环了。我照着这个顺序重做了多次建模,速度和准确率都比“先画表关系”高得多。

如果你现在正被一堆 status 字段和月底对账折磨,不妨找一个最小的模块先试点,只建模一个采购入库和对应付款事件,把流程跑通。REA 不需要整体切换,拿下一块阵地再扩大,效果会更稳。

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

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

立即咨询