news 2026/10/21 5:27:00

REA模式:用事件驱动重构业务系统的建模思路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
REA模式:用事件驱动重构业务系统的建模思路

做业务系统这么多年,我见过太多把账算错、把业务流程写死的项目。很多团队不是在开发时遇到困难,而是在开发半年后、业务规则一变,整个表结构就撑不住了。今天想认真聊聊 REA 模式——Resource-Event-Agent,资源-事件-参与者。这是我从会计领域里借鉴来的一套业务建模思路,不是什么框架,也不是某个开源库,但它能帮你把采购、销售、库存、资金流转这类场景,从头到尾梳理得明明白白。

这篇内容适合正在做订单系统、ERP、进销存、财务模块的同学,也适合任何对领域建模感兴趣的开发者。我尽量把这套模式讲透:它为什么能替代传统的“单据流”建模思路,三个核心对象到底怎么用,实际落地时表结构怎么设计,以及我在真实项目里踩过的那些坑。看完你大概率会重新审视自己项目里的流水账表结构。

1. REA 模式到底是什么

1.1 从一次采购业务说起

我先抛一个特别常见的场景:公司要采购一批原材料。

大多数团队的做法,是建三张表。第一张是采购单,记录供应商、总金额、下单日期;第二张是入库单,记录物料名称、数量、批次;第三张是付款单,记录钱付了多少、什么时候付的。三张表靠“单号”字段互相引用来回串。

这个模型刚上线时一切正常,但真实业务永远比“看起来”复杂。比如供应商退货了怎么办?一批货里有一部分是赠送的、一部分需要付款怎么办?同一个采购单分批入库、跨月付款怎么办?再往后,财务要求成本核算到每个批次,业务要求追溯每一笔库存变化的来源,这套“单号关联”的结构就开始撑不住了。

你会发现表里的字段越堆越多:是否预付、是否含税、是否红冲、关联原单类型、冲销原因……每一个新字段,都是一次业务规则被硬塞进数据结构的痕迹。到后来,谁也不敢轻易动这些表,因为动了就不知道会牵连多少关联查询。

REA 模式对这个问题的回答非常直接:不要围绕单据建模,围绕事件建模。单据只是事件的凭证,业务真相藏在事件的因果关系里。

1.2 REA 与传统复式记账模型的本质区别

传统会计的复式记账法,用“借”“贷”两列记录每一笔价值流转。每一笔分录必须借贷平衡,这在会计上是严谨且完备的。但如果直接把复式记账的思维搬进业务系统,很容易出问题——系统里全是“科目余额”“借贷方向”这类抽象的财务对象,业务上下文被稀释得干干净净。采购员打开系统,看到的不是“这批货从哪个供应商进来的”,而是“借:原材料,贷:应付账款”。信息没错,但人看不懂,后续的逻辑推导也无从谈起。

REA 模型的雏形,恰恰是对会计模型的再抽象。McCarthy 在 1982 年提出了一套观点:任何经济活动,都可以用三类对象来描述——参与经济活动的资源(Resource),资源价值发生变化的事件(Event),以及参与事件的个人或组织(Agent)。采购、销售、支付、收货、领料、报废,这些真实业务动作直接成为模型的中心对象,语义完整保留,而不是被压扁成一组抽象科目。

对比一下就很清楚:

维度传统单据模型复式记账模型REA 模型
中心对象单据会计科目业务事件
语义完整性中等,依赖单据关联低,业务被抽象成借贷高,事件即业务动作
扩展能力弱,加规则要加字段弱,宏观但不贴近业务强,新事件就是新表
适用人群开发、业务财务开发、业务、财务都能理解

所以 REA 不是要推翻会计理论,而是把会计“价值流转”的严谨内核,嫁接到业务系统需要的“语义完整”表达上。理解了这一层,后面设计模型时就自然知道该把什么放在中心位置。

2. 三个核心概念拆解

2.1 Resource:业务里的“资源”

Resource 很好理解,就是业务中被交换、被消耗、被生产的有价值的东西。在采购场景里,原材料是资源;在销售场景里,成品是资源;在劳务场景里,工时是资源;在资金场景里,现金是资源。资源可以是实物,也可以是虚拟的,比如积分、优惠券、服务时长。

有一个关键点必须强调:资源不等于库存台账里的“物料主数据”。物料主数据描述的是“这个东西是什么”,而 REA 里的 Resource 描述的是“当前这个具体批次的资源有多少价值”。同一款物料,不同批次的采购成本可能不同,存放位置可能不同,对应的资源记录就应该区分开。这是后面做成本核算、批次追溯的基础。

我在实际项目里通常把 Resource 拆成两层:

  • Resource Type:资源的静态定义,比如“物料编码”“物料名称”“规格型号”;
  • Resource Instance:资源的动态状态,比如“批次 A 的 500 个物料”“这批物料的单位成本”。

很多团队只建了 Type 层,没有 Instance 层,结果库存数量一变动,只能去改那张“总数”数字。改总数意味着丢失历史,历史丢了,后面任何对账、审计、分析都无从谈起。资源实例化,是 REA 落地时第一件要做对的事。

2.2 Event:真正记录价值的动作

Event 是 REA 模式里最核心、也是最容易理解错的对象。它表示一次让资源发生变化的经济事件。采购订单本身不是 Event,订单只是“意图”;真正的事件是“货物实际到达仓库并验收”这一动作,它让原材料资源增加了。类似的,“财务批准付款”是一个事件,它让现金资源减少了。

为什么这么区分?因为订单可能被取消,审批可能被驳回,但已经发生的“收货”和“付款”是一个不可回退的事实。系统里的历史数据,应该由事实事件构成,而不是由意向单据构成。

在设计 Event 时我一般会在内部再细分几个子类型,这是实践经验而非 REA 理论原生的要求:

  • 交易类事件:对应资源所有权的转移,比如销售发货、采购收货、资产调拨;
  • 转换类事件:对应资源形态的变化,比如生产领料、加工完成、物料报废;
  • 使用类事件:对应资源的消耗,比如工时投入、设备使用、费用报销。

这个细分让我在写业务规则时更清晰。交易类事件涉及双方的 Agent,转换类事件通常只涉及内部 Agent,使用类事件则往往触发成本归集。分清楚之后,每个事件需要关联什么字段、触发什么后续流程,就顺理成章了。

2.3 Agent:内外参与者的边界

Agent 是事件的参与方。它不是简单的“用户表”,而是一个具备明确职责边界的角色。在 REA 模型里,Agent 通常分成两类:内部 Agent 和外部 Agent。内部 Agent 可能是“采购部”“仓库管理员”“销售员”,外部 Agent 则是“供应商”“客户”“物流承运方”。

不少项目把 Agent 概念忽略了,直接在建表时只留一个“create_by”字段记录操作人。这在简单系统里没问题,可一旦业务涉及多组织、多角色协同,比如总部采购、分公司收货、第三方代管库存,“谁对哪个事件负责”就成了一笔糊涂账。REA 的做法,是把 Agent 作为事件的一个显式关联维度,让每一笔价值变动都能追溯到责任人。

我再补充一个值得留意的点:Agent 与“操作人”是两个概念。Event 里有一个字段记录“操作系统的人”和“本机 IP”,这属于审计日志范畴;而 Agent 关联的是“业务上承担责任的角色”。我见过一个项目把这两者混在一起,结果员工离职后,历史采购单的供应商关联全部悬空。供应商、客户这些外部 Agent 应该有自己的主数据表和生命周期,不能绑定在某个系统账号上。

3. 用 REA 重构一个真实业务场景

3.1 场景定义与模型设计

前面说得再多,不如拿一个完整场景过一遍。假设我们正在开发一个简单的进销存系统,核心业务是:从供应商采购商品入库、向客户销售商品出库、定期给供应商付款、向客户收款。

按照传统建模,我们需要采购单、采购入库单、销售单、销售出库单、付款单、收款单,至少六张表,加上一堆关联关系。按照 REA 建模,我们只需要三个层面的对象:

  • 资源:商品库存实例、现金账户;
  • 事件:采购收货、销售发货、付款、收款;
  • 参与者:供应商、客户、采购员、销售员、仓库管理员。

然后按照 REA 的核心原则组关系。采购收货事件增加商品库存资源,同时欠供应商一笔“应付”——这里引入了一个 REA 里的关键概念叫“债务/债权(Claim)”。它不是独立的对象,而是两个事件的差额推导出来的关系:采购收货的增量、付款的减量,两者之差就是当前应付余额。

这个设计有个天然的好处:应付余额不是存储字段,而是由事件链推导出来的计算值。传统模式下,应付余额存在供应商表的“应付款余额”字段里,每次收付款都要 update 一次,一旦程序出错或者并发写入,余额就对不上了。REA 模式下,余额永远可以从事件数据里重新算出来,对账时重新跑一遍事件流就能核对。

3.2 从模型到数据库表结构

模型设计完成后,落成数据库表就顺理成章了。核心表大致如下:

-- 资源类型表 CREATE TABLE resource_type ( id BIGINT PRIMARY KEY, code VARCHAR(64) NOT NULL, name VARCHAR(128) NOT NULL, unit VARCHAR(32) ); -- 资源实例表(批次级别) CREATE TABLE resource_instance ( id BIGINT PRIMARY KEY, type_id BIGINT NOT NULL REFERENCES resource_type(id), external_ref VARCHAR(128), status VARCHAR(16), quantity DECIMAL(18,4), unit_cost DECIMAL(18,4) ); -- 事件表(这是核心,统一一张表还是拆分,取决于业务复杂度) CREATE TABLE economic_event ( id BIGINT PRIMARY KEY, event_type VARCHAR(32) NOT NULL, -- PURCHASE_RECEIPT / SALE_SHIPMENT / PAYMENT / RECEIPT event_time TIMESTAMP NOT NULL, resource_id BIGINT REFERENCES resource_instance(id), from_agent_id BIGINT, to_agent_id BIGINT, quantity DECIMAL(18,4), unit_price DECIMAL(18,4), prev_event_id BIGINT REFERENCES economic_event(id) ); -- 代理表 CREATE TABLE agent ( id BIGINT PRIMARY KEY, agent_type VARCHAR(16) NOT NULL, -- INTERNAL / EXTERNAL name VARCHAR(128) NOT NULL, parent_id BIGINT );

看到上面的prev_event_id了吗?这是 REA 落地时一个很重要的设计——事件之间通过“前序事件”指针建立因果链。同一笔采购分三次收货,三个收货事件通过前序指针串起来;付款事件通过前序指针关联到对应的收货事件。这样做有很多好处,最实用的一个就是“红冲”变得极其简单:不用去改原事件,只需要新增一个负向事件,并把它的前序指向原事件。错误历史被完整保留,审计时有据可查。

补充一句,上面给的是一张统一事件表的写法。业务不复杂时这样最省事;如果各类型事件差异很大,比如销售事件要记录物流单号、生产事件要记录工单号,我会选择按事件类型拆子表,但公共字段仍然遵循同一套命名规范。拆还是不分,以“事件公共字段占比”为标准:如果公共字段能覆盖 70% 的列,就用单表加类型字段;否则就拆。

3.3 查询与报表如何落地

REA 模型的查询逻辑,跟传统模型不一样。传统模型的报表,直接 select 各张单据表然后关联;REA 模型的报表,是把事件流做聚合推导。

举个例子,“某供应商当前应付款余额”的查询逻辑是:

SELECT agent_id, SUM(CASE WHEN event_type = 'PURCHASE_RECEIPT' THEN quantity * unit_price ELSE 0 END) AS owed, SUM(CASE WHEN event_type = 'PAYMENT' THEN amount ELSE 0 END) AS paid FROM economic_event WHERE agent_id = ? GROUP BY agent_id;

这个查询的效率,在事件量级不大的时候没问题。一旦单条事件量超过百万,我会引入“余额快照表”或者“物化视图”做预聚合。注意:快照不是为了替代事件流,而是为了查询性能。事件流仍然是唯一真相源,快照数据随时可以从事件流重建。这个原则一定不能颠倒。

再说库存报表。传统模型下,库存余额是一个字段,扣减时直接 update;REA 模型下,当前库存 = 所有入库事件数量之和 - 所有出库事件数量之和。这样每次盘点、对账,都可以用一条 SQL 重新计算。我参与过一个项目,就是因为用了 REA 的思路,盘点差异定位从“全表扫查”变成了“按事件时间范围重算”,效率提升了几个量级。

4. 我在实践里踩过的坑

4.1 容易搞混的概念:从 REA 角度看“单据”

在实际项目里被问到最多的问题就是:“那我的采购单怎么办?我的销售合同怎么办?”我的回答是:单据在 REA 里作为“意图凭证”存在,存放单据头、明细、审批记录、附件等,但它不是价值的直接载体。价值变动由事件承载,单据与事件之间是弱关联,一张单据可以对应多个事件,一个事件也可以由多张单据触发。

这样做的好处,是当出现“先发货后补单”这类业务动作时,系统不至于卡死。发货事件已经发生并记录在案,补录的销售单只是补充说明。很多 ERP 项目里让人头疼的“单据完成状态同步”,在 REA 模型下直接消失了——事件已经记录,单据只是文案。

4.2 常见问题速查与避坑清单

问题表现排查方向
库存余额对不上账面数量与实际库存不一致检查是否绕过 Event 直接改资源数量
应付余额漂移供应商对账单总有差额检查是否重复记录付款事件,或漏掉退货负向事件
红冲混乱新单据数据张冠李戴检查前序事件指针是否准确指向原事件
报表无法追溯明细账看不清某批次来源检查 Resource 是否做了实例级拆分
并发导致重复记账同一个事件被保存两次检查事件表是否有业务唯一键约束

避坑清单里我最想强调三点:

第一,资源数量不能直接在 resource_instance 表里 update,必须通过事件来变更。哪怕加一个“盘点调整”事件类型也好,一定要走事件。这是 REA 的铁律,破坏一次,后面就全失信了。

第二,事件必须有业务唯一性约束。比如“同一个供应商、同一时间、同一资源批次、同一金额”的付款事件不应该重复出现。实际应用中我发现,仅靠自增 ID 做主键不能防重,要在逻辑层做唯一性校验,或者建联合唯一索引。这是很多项目上线后才爆雷的地方。

第三,Agent 主数据要独立生命周期管理。不要为了省事把供应商、客户直接挂在订单表里。供应商改名、合并、停用,都是主数据操作,与具体的事件表解耦。我见过某公司因为供应商改名导致历史应付账全部查不到,最后只能人工对账,教训非常惨痛。

4.3 什么时候别用 REA

我不想把 REA 包装成万能药。说实话,它也有不适用的时候。

如果你正在做的系统,业务规则极其简单,就是“一张订单对一次发货对一次收款”,不存在分批、跨期、退货、冲销这些情况,那用传统单据模型反而更轻量。REA 的建模和维护成本摆在那里,事件链、前序指针、资源实例拆分,都是额外的复杂性。简单场景不需要杀鸡用牛刀。

另外,如果你的团队对领域建模不熟,也没有预算做长期迭代,REA 的学习曲线会成为一个隐性成本。它不是那种“第一天学完第二天就上手”的模式,至少要经历一个完整的建模、落地、复盘循环,才能真正体会到它带来的好处。团队全员(至少核心成员)愿意投入学习,我才建议推进。

如果是大型商业 ERP 的核心领域、多组织复杂业务、严格的财务审计要求——这类场景我强烈建议优先考虑 REA 思路。它和这些场景的匹配度,几乎是量身定做的。

5. 从一个更广的视角看 REA 的价值

最后说点个人体会。我最初接触 REA 是十年前,当时只觉得它是一个会计领域的抽象模型,离写代码太远。后来在一个多组织、多币种、多税制的复杂项目里被传统模型逼迫到墙角,才重新翻出 REA 的思路,越用越觉得它的设计哲学高明。

REA 本质上回答了一个问题:系统真正的“事实”到底是什么?传统设计把事实散落在单据、字段、状态流转里,而 REA 把所有事实收敛为一条不可变的事件流。资源是事件的结果,余额是事件的推导,单据是事件的注释。这种把“事实”与“解释”分离的思想,让系统在面对需求变化时有了极强的韧性。

如果你正在准备设计一个新系统,或者打算重构一个已经有些僵硬的旧系统,我建议你花一周时间做一次 REA 建模练习。不需要找真实的复杂业务,就用“便利店进货”这种简单场景,把资源、事件、参与者三层画出来,再对照你现有的表结构看看差距。这个练习的成本很低,但对建模思维的冲击非常大。

我个人在实际操作中的体会是:REA 模式不是为了让你第一次建模就完美,而是为了让你在第二次、第三次面对需求变化时,有地方可以退。事件链是永远可以追溯的,余额是永远可以重算的,这就是它比“写死字段”的方式多出来的底层安全感。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 5:26:55

经典蓝牙的协议介绍

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/21 5:25:29

RAG+LangChain实战:从私有知识库问答到大模型落地

现在很多做实际业务的人一看到“大模型”三个字,第一反应都是“我能不能拿它来问答我的私有文档”。真把这个需求做一遍就会发现,直接丢给大模型硬答,结果基本不能看——要么胡说八道,要么答非所问,要么就是“我不知道…

作者头像 李华
网站建设 2026/10/21 5:25:26

加密课程设计实战:AES、RSA与数字签名代码全解析

简介:这是面向高校网络安全课程设计的一套加密技术资料包,主要服务于网络工程、信息安全、计算机等专业需要完成“加密技术”相关课题的本科生。资源以RSA加密算法为核心,既提供了完整的课程设计报告,又附带了对应的C语言源程序&a…

作者头像 李华
网站建设 2026/10/21 5:25:07

深入理解Containerd:容器运行时架构、实操与Kubernetes实践

1. 为什么Containerd值得单独拎出来讲很多刚接触云原生的人,一上来就被Kubernetes、Docker、Containerd、CRI-O这一堆名词绕晕。我见过不少项目里的同事,明明宿主机上装着Docker,结果发现Kubernetes集群用的运行时根本不是它,而是…

作者头像 李华