☰
REA建模:资源-事件-代理如何重塑业务系统设计
2026/10/21 22:58:39 网站建设 项目流程

1. 这个叫“REA”的东西,到底是什么

做业务系统开发的朋友,多少会对建模这事头疼。接到一个新需求,第一反应往往是画表结构、定字段、写CRUD。订单、库存、流水、对账……一套下来,表不少,但每次需求一变,改起来就想骂人。我之前也这样,直到接触了REA建模思路,才发现原来业务系统的地基可以打得更扎实一些。

REA,全称是 Resource-Event-Agent,翻译过来就是“资源—事件—代理”。它最早是上世纪八十年代在会计信息系统领域提出来的一套企业业务建模思想,核心就一句话:经济活动中,是由“代理”发起“事件”,事件会影响“资源”的流入或流出。比如顾客下单买商品,顾客和店员是代理,下单是事件,商品和现金是资源。听起来很简单,但把这套模型真正落到业务系统设计里,整个系统的扩展力和可维护性完全不一样。

这篇博文重点围绕REA模型展开,从核心概念到实操建模,再配合一个真实的业务场景拆解全过程,帮你理解这套思路怎么用、为什么好用、以及用的时候最容易踩哪些坑。适合对领域建模、中后台系统设计感兴趣的开发者,也适合会计信息化方向的从业者。

2. 先理解REA的底层逻辑,再谈建模

2.1 三个核心要素的准确含义

传统设计里,我们习惯把“订单”当成一张表,把“库存”当成一张表,但REA不这么看。它把所有业务活动拆成三个最基本的构件:资源、事件、代理。

资源是指经济活动中被交换、被使用、被消耗的对象。它不一定是实物,可以是钱、是商品、是服务,甚至可以是积分、知识、版权。资源的特征是:你能够明确说出“这个资源现在有多少”、“在谁手里”、“处于什么状态”。经济资源必须是可以被计量的,没法计量的对象不适合当资源建模。

事件是资源变动的原因。注意,REA里的“事件”一定是一个已经发生或计划发生的具体业务行为,比如“产生销售订单”、“确认出库”、“收到货款”。事件是动词逻辑,它必须能回答“发生了什么”,并且一定会导致至少一个资源的数量或价值变化。事件粒度可以不同,但对于一个系统来说,你需要选择统一、清晰的业务语义。

代理是事件的发起者或参与者。可以是人,可以是部门,可以是系统本身,也可以是硬件设备。一个事件至少有两个相关方参与,才算完整的经济交换。比如“销售出库”这个事件,至少涉及“销售员”和“客户”两个代理角色。

这里最容易被忽略的是三者的关系:REA强调“事件”是中心和枢纽,资源通过事件实现增减,代理通过事件关联到资源和资源。而传统表结构里,“订单”表里直接存商品名、数量、金额、客户姓名、销售员姓名……所有信息拍平到一张表里。一旦业务规则变得复杂,这个模型就极其脆弱。

2.2 为什么REA要比传统模式更稳

我举个生活化的例子。传统记账式建模常见做法是给订单设计一张大宽表,字段包含商品名称、数量、单价、总价、客户、地址、销售员、收货状态、付款状态。表面看起来一步到位,用起来确实也方便——查订单全部信息一条SQL就能搞定。但问题是:如果客户买了两件商品,其中一件退货,另外一件换货,怎么处理?如果客户先付定金,再付尾款,分两期支付到两个不同账户,怎么记录?如果销售员离职了,历史订单归属如何调整?

这种场景一多,大宽表就开始发疯——要么字段冗余爆炸,要么到处加状态位,要么触发一堆自定义逻辑代码,改一个场景动不动影响三四个模块。REA的思路是:不做大宽表,而是把“发生了什么”这件事语义化表达出来。客户下单时,不直接更新商品表,而是生成一个“销售事件”;这个事件连接“客户代理”和“销售员代理”,同时连接“商品资源”的流出,以及“应收款资源”的流入。每一次业务动作,都是一条新的事件记录。

这种做法的好处是:业务规则可以被描述成事件之间的约束关系,而不是散落在代码分支里。比如“只有出库事件完成后,收款事件才是合法的”“退货事件发生后,对应的原销售事件状态自动标记为部分履行”。系统的行为规则和数据结构不再是两张皮,逻辑会清晰得多。

2.3 REA与传统复式记账法的对照关系

REA虽然源于会计领域,但它不完全等同于复式记账。复式记账关心的是“账怎么记”,REA关心的是“业务到底发生了什么”。这两者有关联,但层次不同。复式记账的核心是“有借必有贷,借贷必相等”,这是经济业务的符号化表达。而REA是面向业务语义的本体模型,你可以理解成两个维度:复式记账是结果,REA是过程。

举个例子,一笔销售业务,在复式记账里表现为借银行存款、贷主营业务收入,同时借主营业务成本、贷库存商品。而在REA里,这个业务至少会拆成几个事件:销售订单产生(同时关联商品流出承诺和应收款流入承诺)、商品出库(商品资源的实际流出)、收款完成(现金/银行存款资源的实际流入,应收款资源的冲销)。每一个事件都是一个独立的业务事实,系统可以围绕这些事件做审计、分析、追溯,而不需要依赖借贷两个字去反推业务语义。

所以我的观点是:如果你在一个业务系统里发现,无论怎么建模都绕不开大量的状态枚举和if-else分支,可以停下来想想,是不是从一开始就用错了地基。REA不一定适合所有系统,但凡是涉及多参与方、多资源形态、复杂业务规则的中后台系统,REA都值得认真考虑。

3. 核心建模步骤与方法拆解

3.1 确定经济资源范围

建模第一步是圈定资源。不是所有数据对象都应该作为“经济资源”建模,只有那些涉及“经济利益流动”的实体才算。具体的判别标准有三个:

第一,资源可以被计量,单位明确,比如“件”“元”“小时”。第二,资源的所有权或使用权会参与交换,比如商品、现金、服务工时。第三,资源的存量或价值需要被持续追踪。凡是满足这三个条件的,就应该设计为资源表;凡是只是描述性信息的,比如客户昵称、门店地址、商品图片,都不应当作为资源建模。

需要注意的是,在REA语境里,资源和“主数据”不完全等同。商品主数据里包含品牌、规格、条码等属性,但作为“经济资源”的商品,更关心的是可售数量、库存成本、所在仓位这类“可用于经济事件的数量维度”。实际建模时,可以把主数据单独保存,把经济资源的存量表单独设计,两者通过外键关联。

3.2 确定事件粒度与类型

事件是REA的灵魂,所以建好资源以后,第二件事就是把业务过程中“会发生什么”完整列出来。确定事件粒度有两条原则:一是“每个事件都必须能回答:谁、对什么、做了什么”,二是“事件与事件之间必须能通过资源形成因果链”。

还是以销售业务为例,完整事件链如下:

  • 客户下单:产生销售订单,同时承诺“交付商品”和“收到货款”;
  • 订单审核:销售订单状态变更,从草稿到已审核;
  • 出库发货:商品资源减少,在途库存增加;
  • 客户签收:在途库存减少,已交付库存增加;
  • 收款确认:现金/银行存款增加,应收款减少。

这里有个建模技巧:不要把所有变化都塞进一个事件里。订单审核和订单创建严格说是两个不同的业务事件,虽然都针对同一张订单,但发生时间不同、触达代理不同、业务含义不同。如果你把它们合并成一个状态字段,后续的审计追溯、时效统计、责任认定都会缺少关键信息。

我建议的做法是:事件表设计成两类,一类是“承诺事件”(比如订单创建、采购合同签订),一类是“履行事件”(比如出库、收款、入库)。承诺事件描述的是未来的经济义务,履行事件描述的是已经发生的事实。系统里做“未完成订单报表”,本质上是统计承诺事件关联的资源变动是否已经被对应的履行事件完全冲销。

3.3 确定代理角色以及关联关系

代理就是“谁参与了事件”。REA里有一个比较严格的要求:事件必须和至少两个代理产生关联,才能构成一个完整的经济交换。这是因为业务系统的本质是在协调多方的利益流动。如果设计出来的事件只关联了一个代理,那你就要检查一下:是不是漏了另一方?比如“销售出库”不可能只有销售员,没有客户;“采购入库”不可能只有仓库管理员,没有供应商。

代理的粒度设计也要注意。有些建模者会把“某公司”作为一个代理,把“该公司里的某个具体经办人”也作为一个代理。这样设计合理吗?分情况。如果业务关注的是公司与公司之间的债务关系,那代理设计成公司级就够了;如果还要追踪具体经办人的业绩和责任,那应该再加一层个人代理,并且让“公司代理”与“所属员工代理”建立组织关系。但不要单靠一个代理对象属性去表达,建议单独建代理关系表。

在我的实操中,代理表设计通常包含一个代理类型,用来区分“内部代理”和“外部代理”。内部代理对应员工、部门,外部代理对应客户、供应商、合作伙伴。事件表里至少留两个关联字段:事件发起方代理ID、事件接收方代理ID。如果存在第三方角色,比如物流承运商、担保方、平台方,可以额外建立事件参与关系表,而不是无脑加字段。

4. 实操示例:一个订单系统的REA建模全过程

4.1 场景设定与需求范围

假设现在要为某电商公司的订单模块重新设计底层数据结构,需求包括:客户下单购买商品、后台审核订单、仓库发货、客户签收、财务确认收款。特殊要求有:支持同一订单包含多个商品;支持部分退货;支持先付定金再付尾款;支持订单归属人所在部门调整时追溯变更日志。

如果按照传统大宽表设计,字段会爆炸到很难收拾。现在用REA来梳理,整个过程分几步走。

4.2 具体表结构设计与关系编码

一、资源表设计

商品资源表resource_product,主键是商品ID,关键字段是库存数量、锁定库存、可售数量。特别注意:库存数量不是唯一的数据源,它应当由“入库事件合计—出库事件合计”算出来,而不是每次直接写UPDATE。之所以要这样,是为了追溯每一次库存变动的来源。

资金资源表resource_fund,主键是资金账户ID,字段包括账户类型、余额、可用额度。订单的应收款不是存一个订单总额字段,而是单独采用“应收资源”建模:用一个resource_receivable表,记录客户应付款余额的变化。

二、事件表设计

事件主表event_record,字段包括事件ID、事件类型、事件时间、关联资源ID、来源代理ID、目标代理ID、事项说明。注意:事件类型不是用字符串乱填,而是用枚举固定:订单创建、订单审核、出库、签收、退货、收款、退款。

事件明细表event_detail,用于一对多场景。一个订单事件可能涉及多个商品资源,主表和明细表分离,保证一个事件对应多个资源明细。

资金事件表fund_event,专门记录付款、退款等资金类事件。字段包括资金事件ID、事件ID、资金账户ID、金额方向、金额、关联应收资源ID。

三、代理表设计

代理主表agent,字段包括代理ID、代理名称、代理类型(客户/供应商/员工/部门)、状态。代理关系表agent_relationship,用来描述员工属于哪个部门、客户归属于哪个销售员。

四、关系说明

每张资源表都必须回答“当前库存来自哪些事件”。每张事件表都必须回答“这个事件涉及哪些资源和哪些代理”。从上到下三层关系清晰。订单查询不再是一条订单主表扫描,而是通过“订单创建事件”找到关联资源明细,再通过资源明细反查商品信息、代理信息。

4.3 典型业务流转的落地实现

以一个两件商品的订单为例:

第一步,客户下单。系统插入一条“订单创建”事件记录,事件发起代理为客户,接收代理为销售部。事件明细插入两行,分别关联商品A和商品B的resource_product,数量各为1。此时商品A、B的锁定库存增加,可售库存减少。

第二步,后台审核。插入“订单审核”事件记录,代理是审核员。不要把审核状态直接UPDATE在订单表上,因为订单表已经被拆掉了,状态的本质就是“是否存在对应事件记录”。同理,查询一张订单是否已审核,就查有没有对应事件。

第三步,仓库发货。只发了商品A,没发商品B,则插入“出库”事件,明细只关联商品A。此时商品A的锁定库存减掉,商品B的锁定库存保留,等待下次发货。

第四步,签收确认。插入“签收”事件,商品A在途状态转为已交付。

第五步,客户付款。客户分两笔付钱,先付定金,再付尾款。资金表fund_event插入两行记录,金额分别为定金和尾款,方向都为收入。应收资源resource_receivable.余额字段随两笔付款逐步减少,直到归零。

整个过程没有任何一个UPDATE硬改业务状态,所有动作都是插入新事件。数据分析时,“某订单还差多少尾款”这个事情,不再需要去查一个current_status字段,而是通过计算应收资源余额即可。

4.4 常见误区:承诺与履行的混淆

我见过不少人学完REA以后,建模时把“订单确认”和“订单发货”混成一个事件,搞了两张资源表关联同一事件。表面看也没错,但查询“哪些订单已确认但未发货”时,分分钟写出十层嵌套子查询,维护性极差。

正确思路是按照上面说的,把承诺事件、履行事件分开。订单创建是承诺,出库、收款、退货是履行。一个承诺事件可以对应多个履行事件(分批出库);一个履行事件也可以对应多个承诺事件(合并支付多个订单)。这种“多对多”的事件关系,恰恰是REA模型最有表现力的地方。

在设计事件表时,额外加一个“履行了哪个承诺事件”的关联字段,即related_event_id。比如出库事件里填上它履行的订单创建事件ID。虽然看起来只是加了一个字段,但它能让系统顺着事件链回溯业务全貌,做审计时极其舒服。

5. 常见问题与排查技巧实录

5.1 建完模型后,查询太复杂怎么办

这是REA建模后最常见的一个吐槽。以前一个订单一条记录,现在一个订单一堆事件记录,查个列表都要JOIN一堆表。这个问题确实存在,但解决方式不是打回原形,而是增加“读模型”或者说查询模型。

我建议在REA事件表之外,单独建一套只读的查询投影表,比如订单视图冗余表。写入时通过事件表落库,读取时直接查投影表。投影表相当于把事件链路“展开”成业务流程视角,方便列表页展示。只要保证写入侧事件表是唯一的数据权威来源,投影表定时同步或事件触发同步,就不会出现数据不一致的问题。这才是事件驱动架构常见的CQRS思路,REA和它是天然搭的。

这种方案的好处很明显:事件表保证了业务的完整追溯和规则清晰,投影表保证了列表查询的速度。两边各司其职,不要为了查询优化去破坏事件建模。

5.2 两个代理之间的关系如何建模

客户可能同时是供应商,员工也可能就是客户(内部购买)。这样模糊的关系用代理类型字段区分是很别扭的。我的建议是不要试图把一个代理实体钉死为某一种类型,而是让代理表本身保持中性,让代理关系表去表达“在这个业务语境下,他是以什么角色出现的”。

具体来说,代理关系表结构可以设计为:关系ID、代理ID、关系类型、关联代理ID、生效时间、失效时间。比如某客户同时也向公司供货,那就插入两条关系记录:一条“客户—供应商”关系,一条“员工—内部购买”关系。

这个设计带来的好处是,未来增加新的参与方类型时不需要改表结构,只需要新增关系类型枚举。这也是REA“本体驱动”思路的延伸:先定义业务中最稳固的概念,变化的部分用关联去承接,而不是为每个变化都加一个硬编码字段。

5.3 退货换货场景怎么建模

退货的本质是“资源的反向流动”:商品资源从客户流向公司,资金资源从公司流向客户。建模时插入一条“退货事件”,代理是客户和售后专员,资源关联是商品“流入”和资金“流出”。如果商品已贬值或损坏,还需要设计一条“商品价值调整事件”,调整商品资源的计量价值。

换货的场景要注意一点:换货不是简单的退货加新订单,它是“跨事件”的资源交换,建模时要完整表述为两条独立事件链,并增加一个“等价交换关系表”把退货事件和新订单事件关联起来,便于后续对账和成本核算。如果你在系统里直接把换货做成一个status,后续所有统计都会缺胳膊少腿。

5.4 如何控制建模粒度,避免过度设计

REA最大的坑之一就是事件拆得太细,导致一个简单查询要跑几十张表。比如“用户点击了商品详情页”这个行为,就不该作为经济事件记录到REA体系里——它不涉及经济资源的流入流出。

怎么划分粒度?我的建议是看一个事件是否同时满足三个条件:一是涉及至少一个资源,二是存在经济价值增减,三是能够明确识别出至少两个代理。如果三个条件一个不满足,就说明它是子事件、描述性信息、或者压根不属于REA关注的业务行为,别往核心事件表里塞。经验法则是:把系统里的事件分成“核心经济事件”和“业务过程痕迹”,前者走REA,后者走操作日志,两者数据分开存储,避免污染。

5.5 与现有系统的兼容过渡

如果已经有老系统在跑,不建议一次性把所有模块推倒重构。比较稳妥的路径是“渐进式替换”:选择业务规则最复杂的模块,比如订单或财务模块,先把新模型用起来,跑通之后再逐步扩展。

另一个兼容技巧是:在老系统和新系统之间加一层“事件桥接器”。老系统的每一次状态变更,由桥接器翻译成REA事件写入新模型。这样新系统可以在不中断业务的情况下,逐步积累完整的事件历史数据。实测下来,这是从传统建模向REA迁移成本最低的一条路,也是我比较推荐的做法。

6. 从REA延伸出去的思考

6.1 REA与事件溯源的天然契合

事件溯源的核心思路是“不保存当前状态,只保存导致当前状态的事件序列”。这和REA几乎是一拍即合。REA定义了哪些事件是有业务意义的,事件溯源定义了这些事件如何存储、如何重放、如何恢复状态。

我在实际做后端架构时,把两者结合得很紧密:REA负责业务语义层,保证团队沟通时说的是同一套业务语言;事件溯源负责技术存储层,保证系统具备完整重建任一时刻状态的能力。这样的组合,让系统在审计、合规、追溯这些需求上天然满足,不需要额外写一堆补偿逻辑。

6.2 REA与领域驱动设计的配合

领域驱动设计(DDD)强调“以业务事件为中心构建领域模型”,这一点上与REA高度一致。DDD的聚合根、值对象、领域事件,在REA框架下能很自然地找到对应:聚合根可以是一个完整的经济交换过程,值对象可以是商品资源的基础属性,领域事件就是REA里的业务事件。

如果团队已经采用DDD做业务架构,那在概念模型层引入REA几乎是无缝衔接的。用REA来统一“资源—事件—代理”的词汇表,用DDD来落地领域模型的代码实现,两者搭配起来非常顺手。

6.3 我个人在落地REA时的几点体会

最后再分享一些个人感受。REA不是一个脱离实际的学术玩具,它是一套已经在上世纪就被验证过的企业建模方法论。但在国内的技术讨论里,聊到这个概念的人不算多,更多开发者习惯了“表驱动设计”的模式,遇到复杂业务便拼命加状态位,结果系统越做越僵硬。

我自己经历过一次被逼着重构的痛苦。当时接手一个财务结算系统,旧代码全是状态判断、操作日志、数据补丁,几乎没法继续加需求。后来用了REA重新梳理,虽然前期多花了一段时间,但后期迭代速度明显加快,业务方提需求时,开发团队也能很快判断该改哪张表、加什么事件、关联哪些代理。这种“把业务逻辑具象化”的体验,是传统表驱动设计很难带来的。

如果你想在项目里尝试这套思路,我的建议是从一个模块做起,最好是订单或库存这种资源流动明显的模块。先画出完整的“资源—事件—代理”图,再转成表结构,最后用事件表做数据写入。过程中多问自己几个为什么,比如“这个状态的变化是不是代表着某个资源的流动”,能帮你快速校正建模方向。

我自己到现在还在不断复盘REA模型在不同场景里的边界,比如订阅制服务、租赁业务、预付费余额这类复杂场景。每一次调整都会有新的体会。如果你正在设计一套中长期演进的业务系统,REA值得你花一个下午好好研究一下。

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

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

立即咨询