☰
REA业务建模实战:资源-事件-代理者模型重构进销存系统
2026/10/21 14:54:11 网站建设 项目流程

做业务系统的人,大概都遇到过一种尴尬:财务那边要一套账,运营那边要一套报表,研发手里那几张表结构却经常在两者之间反复横跳。数据是同一批数据,但口径一多,系统就成了一层层补丁堆出来的模型。我第一次真正理解 REA(Resources-Events-Agents,资源-事件-代理者)模型,就是在这种背景下。当时被拉去评审一个进销存系统的重构方案,某导师在会上说了一句话,我记到现在:"你们这张库存流水表,本质上是一张事件表,但你们一直在拿它当资源表用。"这句话我琢磨了很久,也促使我把 REA 从理论到落地完整啃了一遍。这篇文章就打算把那次项目里用 REA 做业务建模的完整思路、表结构设计、查询逻辑和踩过的坑,原原本本分享出来。

REA 不是什么新框架,它最早是会计信息系统领域里提出来的一套语义模型,后来被越来越多的人应用到企业系统、数据仓库、审计追踪的设计上。简单说,它主张把业务世界还原成三种东西:资源(Resource)、事件(Event)、代理者(Agent)。你不需要懂会计也能用,因为它的本质是回答一个问题:系统里到底发生了什么。如果你正在设计进销存、订单、账单、权限或者审计模型,这篇文章值得你花十分钟看完;哪怕你只是写一个小项目,理解这套思路也能帮你避开很多"改表结构改到崩溃"的坑。

1. 传统记账思维给系统设计埋了哪些坑

1.1 "借货、贷货"的思维惯性

接触 REA 之前,很多系统设计者习惯用会计科目或者单据类型来主导表结构设计。比如拿到一个"出库单",第一反应是建一张出库单主表、一张出库单明细表;拿到"入库单",又建一对主表和明细表。单据越堆越多,于是每次新增业务——比如加一个"调拨"或者"盘点"——就要再补两张表,或者拼命给现有表加类型字段。这个思路不能说是错的,但它把"业务动作"和"业务结果的记录载体"混在了一起。

我用一个生活化的类比来解释。传统记账思维像是给每个房间都装了独立电表:厨房一个、卧室一个、客厅一个,每个电表只记录自己房间的用电。问题是当你想知道"整栋房子今天一共用了多少电"时,你不得不把所有电表读数手动加一遍;如果某个电表坏了,或者你想单独问"空调那一路电用了多少",可能就无能为力了。REA 的思路更像是一个总闸日志:它记录每一个用电事件本身——什么时候、在哪条线路、流了多少电、是谁触发的。至于每个房间分摊多少,反而是可以随时重新计算的。

1.2 "发生什么"比"记成什么"更重要

REA 模型的核心主张,是把事件放在中心位置。所谓事件,就是业务世界里真实发生的动作:顾客下单、仓库发货、收到货款、退回商品、盘点调整。这些事件在时间轴上自然排序,构成了系统的"时间线"。而资源,也就是钱、货、服务、积分这些有价值的东西,是事件作用的对象;代理者,比如顾客、销售员、仓库管理员、供应商,是事件的发起方和接收方。

这个视角最大的好处在于:它不预设你将来要用什么科目、什么单据来承载业务,系统因此可以更贴近真实业务过程,也更容易适应业务规则的变化。举个例子,你原来把"发货"当成一张单据表,后来业务改成"先出库、后记账",甚至把发货拆成"拣货—交接—签收"三件独立的事,基于单据模式的表结构就得伤筋动骨。但如果底层是事件表,新增一个事件节点而已,历史数据完全不用动。

1.3 什么人适合认真学这套东西

先泼一盆冷水:如果你只是写一个轻量级的个人项目,比如一个小型记账本,直接用一张流水表就完全够用,不必上 REA。但一旦系统要同时服务财务、供应链、销售、运营这些不同口径的部门,或者你需要应对审计追溯、数据仓库建模、跨系统对账这类需求,REA 能帮你省掉大量"重建口径"的工单。这也是我把它定位成"给多业务口径系统建模的地基"的原因。

2. 资源-事件-代理者的三元结构,用大白话拆开

2.1 资源(Resource):跟着钱和物走

资源不是资产负债表上的科目,它指的是系统里被管理、被交换、有价值的东西。常见候选有商品库存、现金及等价物、应收账款、积分、服务时长、数字资产。判断一个对象是不是资源,有一个简单标准:它能不能被某个事件"改变数量或者归属"。比如一件商品,出库事件让它的数量减少;一笔现金,收款事件让它的余额增加。如果某个对象只是依附于其他对象的描述性信息,比如"订单备注",那它就不是资源。

这里要特别注意:资源和资源之间往往有父子关系。例如"商品"是一个资源,"某个批次的商品"也是一个资源,前者可以聚合后者。在 REA 建模里,资源体系完全可以做多层级,关键在于把层级关系当成资源与资源之间的关联,而不是用一张大宽表把层级信息全部拍扁。

2.2 事件(Event):系统的"时间线"

事件是 REA 里最富有启发性的概念。它记录的是业务过程中一个不可再细分的动作。怎么判断"不可再细分"?看两个动作能否被合并成一条记录而不丢失关键信息。还是以发货为例,"顾客下单"和"仓库发货"是两个不同时间点、不同责任主体做的事,必须拆成两个事件;而"录入订单"和"确认订单"如果发生在同一时间、同一个人、同一个系统界面,理论上可以合并成一条,但为了审计可追溯,通常还是建议拆开。

事件表在实际落地时,往往就是一张"流水总账"。任何业务表,只要它描述的是一个动作而不是一个静置的对象,都应该考虑用事件的逻辑来建模。我见过不少团队把"订单表"设计成什么都往里塞:金额、状态、地址、优惠信息,一个典型的"事件被对象化"问题,后面每加一个业务环节就要加一堆字段。

2.3 代理者(Agent):责任边界靠它画出来

代理者是事件的参与方,既可以是自然人,也可以是组织或角色。为什么要把组织单独拎出来?因为现实中经常出现"张三作为采购员替某公司采购"的场景,如果不区分"张三"和"某公司"这两个代理者维度,审计时根本说不清楚责任和权限。REA 里的事件关联建议至少保留两个代理者角色:"从某代理者"和"到某代理者",这样买卖双方、收发双方都能被完整记录。

有些系统把"客户"做成用户表里的一个类型字段,又另外建一张客户信息表,再去关联订单。字段越加越多,最后客户、供应商、内部员工三套身份混在一起。用 REA 去澄清责任边界时,你会发现代理者就是一类对象,和具体业务角色解耦,角色只是关系上的标签而已。

2.4 三类关系连起来,业务就变成了闭环

资源、事件、代理者不是三个孤立的概念,而是靠关系组织起来的。资源与事件之间是"参与"关系(出库事件减少了商品库存资源),事件与代理者之间是"执行/接收"关系(仓库管理员执行了出库,承运方接收了货物),资源和事件之间还可以有"转换"关系(生产事件把原材料资源转换成产成品资源)。把这些关系画清楚,系统里的业务流就不再是散落的表单,而是一张可以追溯每个动作"由谁做、对什么做、影响什么"的因果图。

3. 用一个电商案例完整走一遍 REA 建模

3.1 场景设定:模拟项目X的"下单—库存—回款"链路

为了让上面的概念落到实处,我拿一个模拟项目X来演示。这是一个小型电商系统,业务链路不算复杂:顾客浏览商品、下单、支付,仓库拣货并发货,顾客确认收货,平台与供应商结算货款。传统做法里,我们会设计商品表、订单表、订单明细表、支付单表、发货单表、结算单表。用 REA 视角走一遍,会发生什么变化?

先划元素。资源方面有"商品库存"和"资金"两类;事件方面涉及五个核心动作:顾客下单、顾客支付、仓库发货、顾客收货(触发平台确认收入)、平台向供应商付款;代理者包括顾客、平台自身、仓库管理员、供应商。注意,我没有把"订单"列为资源,因为订单不是一个有价值的东西,它是多个事件的组织外壳;真正有价值的资源是商品和资金。

3.2 从业务单据反推事件链

这一步我建议逐张单据过一遍,每张业务单据都想清楚:这张单子是在描述一个动作,还是在描述一组状态。"发货单"显然是在描述"发货"这个动作,所以它应该对应一个事件;"订单"则更像是把下单、支付、收货这些事件聚合在一起的一个业务根对象,数据库里仍然可以保留一张订单主表作为聚合头,但它不应该承担"记录业务动作"的全部职责。

把事件拆出来之后,接下来要为每个事件找到两个代理者——谁发起、谁接收——以及受影响的那一项资源。比如"顾客下单"事件:发起方是顾客,接收方是平台,但它直接影响的是商品库存吗?并不是。下单本身还没改变库存,它改变的其实是顾客和平台之间的一项合约关系。你可以把"订单应付/应收"建模为一项资源,也可以暂时让下单事件不直接作用于商品资源。这个区分在后面建模资金时会显得尤为重要。

3.3 建模结果与表关系示例

我用一张表格把核心事件的归属说清楚:

事件发起代理者接收代理者影响的资源事件结果
提交订单顾客平台应收/应付合约合约增加
完成支付顾客平台资金账户资金平台资金增加
仓库发货仓库管理员承运方/顾客商品库存库存减少
确认收货顾客平台应收/应付合约合约完结,确认收入

有了这张表,数据库里的事件表就可以统一设计成一张"动作流水表",每行记录一个事件,包含事件类型、发生时间、发起代理者、接收代理者、关联资源、数量、金额等信息。不同业务类型之间的事件差异,用类型字段区分,或者再用事件扩展表存放各自的附加属性,事件表本身保持精简。

4. 落地到数据模型:表结构、外键与典型查询

4.1 事件表怎么设计才不变成垃圾桶

很多人看到"把所有事件放进一张表",第一反应是它会不会变成一张无法维护的大杂烩。确实会,如果不做约束的话。我的经验是,事件表只承载"公共动作属性":事件ID、所属聚合(比如订单号)、事件类型、事件时间、发起代理者、接收代理者、关联主资源、变化数量、变化金额、备注。各业务特有的扩展信息放到扩展表里,通过事件ID一对一关联。

这里要特意提醒一个细节:事件表中"数量"和"金额"必须设置严格的符号约定。例如"增加资源"记正数,"减少资源"记负数;"流入资金"记正数,"流出资金"记负数。这样一组 SUM 即可统计出任何资源的余额变化,避免在代码里为每种业务各写一套加减逻辑。这是我吃过亏的地方:一开始没有统一符号约定,后面所有统计查询都变成一条业务一套 SQL,维护成本高到想哭。

4.2 资源表与事件表怎么关联

资源表存的是"当前状态",事件表存的是"历史动作",这两个概念千万不要合并成一张表。以库存为例,商品库存资源表里维护的是"当前可用数量",每个出库/入库事件都往事件表里写一笔带正负号的数量记录;把事件表按时间累加,就能随时回放出任意时间点的库存快照。这正是"事件溯源"思想在业务系统里的一种落地方案,REA 和事件溯源天然契合。

表与表的基本关联可以这样设计:事件表里有多列代理者外键,分别指向代理者表的不同实例;资源表与事件表通过"关联资源"外键相连。实际操作中,有人把代理者和资源统一放到一张"业务对象"大表里,再用类型区分;也有人分成几张业务身份表,事件表里用多个可空外键指向它们。两种方案我都试过,单表简单但类型容易混乱,分表清晰但关联稍重。团队规模不大时,我更推荐分表。

4.3 典型查询:库存、毛利、对账怎么算

建模之后,很多查询都变得非常直接。比如统计某天某商品的库存余额:

select resource_id, sum(qty_change) as balance from rea_event where event_time <= '2025-06-30 23:59:59' and event_type in ('入库', '出库', '盘点调整') group by resource_id;

再比如按代理者统计一段时间内的销售额:

select agent_id, sum(amount) as sales_amount from rea_event where event_type = '确认收入' and event_time between '2025-01-01 00:00:00' and '2025-06-30 23:59:59' group by agent_id;

这种写法有个很实在的好处:事件表里存的是原始动作,只要设置不同的查询条件,就能同时回答财务、运营、审计三方的不同口径;而不是每换一个报表口径,就要改表结构或重新导数据。很多同事第一次看到这套查询时都觉得"太简单了",但简单恰恰说明建模方向对了。

5. 实践中的坑与经验:哪些地方别照搬 REA

5.1 最容易搞混的:把"资源"当"事件"、把"事件"当"单据"

第一次用 REA 建模的朋友,最容易把"商品"这类静置对象当成事件,又把"发货单"这类单据当成事件。把商品当事件,问题在于对象的"当前数量"是状态,不是可以按时间线查询的动作;把发货单直接映射成事件,问题在于一张单据往往是对多个动作的包装,直接映射会丢失动作之间的时间差和责任划分。

我的判断方法是:每次先问自己——"这一行记录,表示的是一次时间点上的一个动作,还是此刻某个东西的状态?"如果是前者,它就是事件;如果是后者,大概率是资源。再问一句:"这个动作有没有明确的发起方和接收方?"如果没有,那很可能建模粒度太粗,或者它只是一个大动作里的辅助步骤,不应该单独成事件。

5.2 资金类资源怎么建模

资金是 REA 实践里最容易翻车的点,问题通常出在"应收""应付""余额"这些概念被混成一张表。按 REA 的做法,"顾客支付"事件会同时影响两个资源:顾客侧资金减少、平台侧资金增加。而"应收/应付"最好建模成一项合约类资源,它不是资金的子类,而是连接履约状态的一种待结转权利义务。只有把"钱实际在哪"和"谁还欠谁多少"分成两类资源,对账逻辑才能写清楚。

具体到表结构,我建议资金账户表只反映"真实可用的资金余额",所有变动都来自严格校验过的支付/退款事件;另建一张"应收应付合约表",记录订单维度上的债权债务,定期或按需做核销。这种拆分乍看有点绕,但分摊成本极低,还能彻底避免"账面余额和实际余额永远差一分钱"的悲剧。

5.3 哪些业务场景不适合 REA

REA 不是万能模板,它最大的短板在于模型抽象度高,团队成员水平参差时反而会增加沟通成本。比如一次纯展示性质的需求,只需要把几个第三方接口的返回结果透传到前端,就没有必要硬套 REA;又比如一个生命周期很短的临时活动,建一堆事件表纯属浪费。我自己的判断标准是三条:系统未来有没有明确的审计需求、有没有跨渠道多口径的需求、有没有跨部门对账的需求。只要有其中一项,REA 都值得引入;如果都没有,用简约的表单驱动设计反而更快。

再补充一点:不要试图用 REA 消灭所有"订单表"这类聚合实体。订单作为一组事件的聚合组织者,仍然有存在的价值,只是它的身份从"存储业务的容器"变成了"关联多个事件的根对象",两者并不矛盾。

5.4 落地推进的个人建议

如果团队之前没接触过这类模型,不建议一上来就全量重构。我第二次实践 REA 时学乖了:先挑选业务链路里最容易出问题的部分,通常是"库存+回款"这对组合,用 REA 重新建一套事件流水,然后让报表、对账、审计三个场景都基于这套流水开发,跑通后再逐步把更多业务纳入进来。这样既降低了改革阻力,也能用实际效果说服其他成员。

还有一点容易被忽略:事件表数据量会长得很快,必须从第一天就规划归档策略,比如按月分区、冷热分离,不要让一张原始事件表占用主库大量空间。事件数据的价值在于可以被反复查询,查询性能与存储成本之间需要持续做平衡。

其实在我个人的实际操作体会里,REA 给我最大的启发不是什么高深理论,而是"先想清楚业务里每一次到底发生了什么,再去谈怎么存、怎么算"。在系统越来越复杂的今天,这种把业务还原成资源—事件—代理者因果网络的方法,反而让设计变得简单了。如果你也正在为库存对账、审计追溯这类问题头疼,不妨拿手头一个真实业务链路试一次 REA 建模。哪怕最终不重构,光是把事件链完整画出来,就足以让很多隐藏的设计问题现形。

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

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

立即咨询