☰
从REA业务建模到数据库表设计:资源、事件与代理实战
2026/10/14 13:06:14 网站建设 项目流程

1. 从“rea”说起:我更愿意把它读成一套业务建模模型

如果你现在打开搜索引擎,输入“rea”三个字母,大概率会得到一堆八竿子打不着的联想词。但做业务建模和系统设计的人,看到这三个字母会立刻想起一套经典框架:REA,Resource、Event、Agent,资源、事件、代理。在我个人看来,这是为数不多“看起来冷门,实际能救命”的业务建模方式。它最初脱胎于会计信息系统领域,目的就是解决一个现实问题:怎样把一笔业务从头到尾记录成机器可理解、可追溯、可复核的事实,而不是塞进一堆借贷分录里。

REA 能做什么?一句话说清楚:它把所有业务抽象成“谁基于什么资源,和谁发生了什么事”。比如一笔蔬菜采购,传统系统里你只会看到“库存+200”,REA 系统里你会看到“某农户在某个时间向平台发运了一批土豆,平台接收并确认入库”这个完整事件。适合谁来读?后端开发、业务架构师、做 ERP/SCM/财务系统、供应链产品的人,都应该把 REA 当入门课。即使只做一个小工具,理解这套思路也能明显改善你对“状态”和“账目”的处理方式。

这篇文章不打算堆术语。我会把 REA 的每个概念用大白话拆开,然后给出一套可以在白板上直接画的表结构设计,最后聊几个落地时一定会遇到的坑。你可以把它当成一次“从概念到建表”的完整实操记录,照着搬思路,基本上不会走大弯路。

1.1 一个几十年前的老模型,为什么今天反而越来越有生命力

REA 本身并不是新东西。早期的会计学研究者面对传统复式记账的局限性时,就意识到:凡是有“借”和“贷”的账本,本质上是把复杂的业务压扁成两个数字。借贷记账再怎么平衡,也很难回答“这批货是从哪个供应商手里来的、当时对应的是哪份合同、后来又流转到哪些客户手里”这一类“业务因果关系”问题。REA 的出发点很朴素:不要从“借和贷”出发,从“业务事件”出发。

为什么今天它反而更有价值?因为现在的软件系统越来越强调事件驱动、数据中台、微服务、审计追踪。你如果用过 Kafka 之类的消息中间件,或者做过事件源Event Sourcing 架构,再看 REA 会瞬间反应过来:这不就是几十年前版本的“以事件为中心建模”吗。把业务拆成不可变的事件,再用事件去推导当前状态,这套思路在今天分布式系统里简直家常便饭。

还有一个很实际的趋势:合规和审计要求逐年变高,业务系统不能只用“结果”交差。客户问“为什么你的库存余额是 1200 而不是 1000”,如果你只有库存表,你得拉单据去推断;如果你有事件表,直接按时间线回放就能讲得一清二楚。REA 的价值在这种场景下会被无限放大。

1.2 一句话建立核心心智模型:给业务做 DNA 测序

REA 模型由资源、事件、代理三个核心概念构成。资源是可计量、可交换的价值载体,比如土豆、现金、积分、服务工时;事件是业务发生的最小原子单元,比如“发货”“收款”“消耗原材料”;代理是参与业务的内外部主体,比如供应商、客户、仓库、某位员工。

你可以这样理解:传统库存系统是在量身高体重,告诉你现在多少库存、多少金额;REA 是在给业务做 DNA 测序,把每一次基因片段(事件)原原本本记录下来,你想知道什么,就能基于真实发生过的片段去还原。REA 不同。资源、事件、代理的三角组合,本质上就是一套通用的业务语言。用这套语言描述任何业务,你会发现原来两个看起来完全不同的项目,在地基层面竟然长得一模一样。

2. REA 三要素为什么能放下整个业务世界

很多第一次接触 REA 的朋友会有质疑:“资源、事件、代理就这么三个词,能描述复杂的商业模式吗?”我一开始也有同样的怀疑,直到真拿它去做需求分析,才发现大多数系统的业务动作翻来覆去就那么几类,三要素完全接得住。下面把三个概念分别展开,讲清楚每个概念背后真正的含义。

2.1 资源:别把它当成普通“商品表”来设计

REA 里的资源,指的是业务中真正在流动的价值载体。它可以是实体货品,比如一吨玉米;也可以是虚体,比如一段咨询工时、一笔现金、一个数字会员权益。关键在于“可计量”和“可交换”这两个特征。系统里能明确说出数量的对象,才配叫资源;没有数量概念的对象,顶多算分类信息。

我踩过这样一个坑:早期设计一个供应链平台时,把资源表做成了问宽表,想尽力保存每种商品独有的属性,结果资源表膨胀得离谱。后来按 REA 思路重新拆解,只保留通用字段:资源编码、资源名称、计量单位、计量精度。真正商品特有的规格、产地、保质期,挪到资源扩展属性表里去。这样资源表本身非常稳定,扩展信息通过外键关联,反而更经得起需求变更。

设计资源的关键是“可追溯粒度”要定义清楚。以土豆为例,如果库存里只有单一维度“土豆 500 千克”,那后续根本没法知道哪批次是哪天到的、属于哪个供应商。你至少要把“批次”作为资源的一个唯一标识记录。真正的操作里面,就是加上一个batch_no字段,不然以后盘点差异根本没法盯。

2.2 事件:系统里最不该被修改的记录

事件是 REA 模型的心脏,指实际发生的业务动作。可以把事件理解成“流水账”里的那行原始凭证,它记录的是事实,不是意图。客户下了订单,那只是“意图”;订单对应的货真正发出去,才构成一个“提供事件”;资金真正到账,才构成一个“获得事件”。

REA 强调一个完整的经济业务通常表现为双向对等的行为:一方向另一方提供资源,另一方则以某种形式反哺资源(比如付钱)。这个成对出现的情况在会计上叫“互换”。比如一次采购:供应商提供了土豆(一方提供资源),平台同时产生一笔应付义务或直接付款(另一方提供资金资源)。设计表结构的时候不需要强行把一个事件拆成两行,而是要有一个清晰的“事件对”回链关系,能定位到这笔业务里交易双方各自贡献了什么。

事件施加约束:只追加、不修改、不物理删除。听起来我有点“强迫症”,其实是有原因的。事件如果允许修改,后续对账时的可信度就塌了。即使偶尔发现数据录错,也应该新增一条反向事件来做冲销,比如“入库 200 千克”录成了“入库 2000 千克”,就新增一条“入库 -1800 千克”修正事件,把错误痕迹留在线下而不是藏在 update 语句里。

2.3 代理:把内部部门和外部客户放在同一张表里

代理就是参与事件的“人”,但这个“人”是广义的。外部代理包括供应商、客户、物流商;内部代理包括采购部、仓库、门店、负责操作业务的员工。很多系统会把“客户表”和“部门表”分开维护,这在 REA 里是没有必要的,因为它们在一个业务事件里身份是对称的。

框里举例说明:用户今天从你这儿买了一批米,他是客户;明天他发起退货,他又是退货方。你如果单独建一张用户表、再建一张供应商表、再建一张门店表,同一主体分散在不同地方,后续做对账时 join 到怀疑人生。REA 的做法是统一抽象成agent或party表,用一个agent_type字段区分“内部代理”还是“外部代理”,再分别维护不同的角色信息,比如客户要关联销售负责人,供应商要关联结算账期。主体统一,关系清晰,也方便将来扩展新角色。

3. 为什么传统记账法在数字化业务里越来越不够用

复式记账法有两个明显优势:账目平衡、体系成熟。这也是它统治财务系统数百年的原因。但它有一个天然的短板:对“业务语义”的记录几乎为零。你可能觉得这有点夸张,但仔细想想,很多系统里订单、库存、收付款各自是独立的子系统,账面余额能对上,却没有人能回答“这笔钱对应哪一单”。

3.1 借贷记账法能回答“平衡吗”,却答不出“发生了什么”

举一个实际中的场景。你接手了一个遗留财务系统,看到一条会计分录:借银行存款 100 万,贷应收账款 100 万。你能确定这笔钱是哪个客户打来的吗?对应的是哪张发票?如果客户同时欠好几笔货款,你根本说不清这 100 万冲的是 3 月那张还是 5 月那张。借贷记账法擅长分类汇总,天然会把“谁”“为什么”这些信息挡在科目两个维度之外。

传统 ERP 会靠“订单、发货单、发票、收款单”多类单据协作,然后用序号耦合。好处是财务规范,坏处是开发人员在业务链路里加一个判断条件,就得同步改多个模块。数据一变,几个模块各自交易记录,对不上账就成了常态。这还不是人的问题,是模型的问题:你没有把“业务事件”本身当一等公民来存储。

3.2 REA 的最大差异化:事件即事实,余额是推导出来的

REA 核心思想是“事件即事实”。系统不直接存“当前库存 500 千克”,而是存“某日入库 200 千克,某日出库 150 千克,某日退货 50 千克”。任何一个时间点的库存,都可以通过对事件汇总推导得出。用这种方式设计,你 时刻可以用同一套数据回答余量、增量、减量三个问题。

这会带来一个额外好处:应用逻辑大幅简化。你不必在多个状态字段之间来回同步,比如“待发货”“已发货”“已签收”;只需根据事件流去计算当前状态即可。状态只是一个派生值,不是根本记录。当我第一次把“库存余额”改成“库存快照+事件流水”的方式后,线上对账问题一下子少了一大半。

3.3 REA 为什么天然适合当下的事件溯源与分布式账本

今天的软件架构频繁提到事件溯源、只追加存储、CQRS,说白了都是把“状态变化过程”保留下来。REA 本来就要求事件不可变,并要求每个事件都必须连接资源和双方代理,这种结构天然带审计和追责基因。做可信供应链、大宗贸易平台、积分发放系统这类高强对账的业务,REA 建模的容错力确实比纯 CRUD 高出一个级别。

如果你未来要接区块链存证或可信凭证类系统,REA 事件表几乎可以直接对应链上存证的数据项。事件ID、资源批次ID、双方代理ID、数量、发生时间、关联凭证哈希,字段都现成。所以我说它老但没老掉牙,至今仍是业务层地基里最值得学的模板之一。

4. 核心实操:一个农产品流通平台的 REA 落地设计

概念讲再多,不如直接画表。这里我模拟一个农产品交易平台的简化设计:上游是农户/供应商,平台做撮合和中转,下游是生鲜门店。平台要支持供应商供货、库存入库、门店下单、平台发货、资金结算等流程。下面看一眼怎么用 REA 把数据库骨架搭出来。

4.1 场景假设:三方角色、一种核心资源、四类核心事件

参与者:某农户(供货方)、某平台(运营方)、某生鲜门店(采购方)。核心资源:土豆。核心事件:供货入库、销售出库、支付货款、确认收款。平台不允许直接跨过自身去处理门店和农户的账,所以平台作为内部代理出现在每一条事件链路上,这样的好处是清清楚楚掌握流水。

如果按传统方式,你可能先建供应商表、门店表、采购单表、销售单表、出入库单表、应收应付表,看起来挺好,但查一个事情要跨五六张单。REA 模式下,表结构能收敛为:某一资源表、某一代理表、某一事件表、以及可持续扩展的事件明细表。

4.2 数据库表设计:三张核心表按这个思路建

资源表建议这样做:

CREATE TABLE resource ( resource_id BIGINT PRIMARY KEY COMMENT '资源ID', resource_code VARCHAR(64) NOT NULL COMMENT '资源编码,如POTATO-001', resource_name VARCHAR(128) NOT NULL COMMENT '资源名称,如土豆', base_unit VARCHAR(32) NOT NULL COMMENT '基本计量单位,如千克', precision_limit INT NOT NULL DEFAULT 0 COMMENT '数量精度', category_id BIGINT COMMENT ‘扩展分类ID’ );

没必要把所有商品属性堆在这里,产地、等级、包装规格全部放到扩展表。资源表负责回答问题:“这个资源是什么、用什么单位计量”,仅此而已。

代理表设计:

CREATE TABLE agent ( agent_id BIGINT PRIMARY KEY, agent_type VARCHAR(32) NOT NULL COMMENT 'INTERNAL/EXTERNAL', agent_name VARCHAR(128) NOT NULL, tax_id VARCHAR(64) NULL, contact_info VARCHAR(255) NULL );

内部代理可能是“平台仓库”,外部代理可能是农户或门店。如果后续有需要,也可以加一列 parent_agent_id 做层级。

事件表是核心:

CREATE TABLE economic_event ( event_id BIGINT PRIMARY KEY, event_type VARCHAR(32) NOT NULL COMMENT 'INBOUND/OUTBOUND/PAYMENT/RECEIPT', resource_id BIGINT NOT NULL, quantity DECIMAL(18,4) NOT NULL, from_agent_id BIGINT NOT NULL, to_agent_id BIGINT NOT NULL, occurred_at TIMESTAMP NOT NULL, ref_order_sn VARCHAR(64) NULL, ref_voucher_no VARCHAR(64) NULL, event_status VARCHAR(32) DEFAULT 'VALID' COMMENT ‘VALID/CANCELLED’, create_by BIGINT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );

事件表里保存 from 和 to,是从资源流动角度看的“谁拿出资源、谁接收资源”。事件行的 event_type 不要理解成“出入库类型”,建议理解成“经济行为类型”,这样后续扩展维护会轻松很多。

4.3 从业务动作到事件的映射表

平时做需求评审时,我会准备一张事件映射表,直接当场确认。格局例子如下:

业务动作事件类型涉及资源从代理到代理关键字段
农户向平台供货入库入库提供土豆批次 XX农户平台仓库车辆、批次号
平台向门店销售发货销售出库土豆批次 XX平台仓库门店订单号
门店向平台支付货款支付接入货币/资金门店平台支付流水号
平台向农户结算货款结算支出货币/资金平台农户结算单号

这张表贴在项目白板上,比任何数据库注释都管用。因为一个事件只要明确了 from、to、resource、quantity,发生时间,它就真正描述了业务中一个“原子事实”。很多团队建表反复修改,就是因为在设计初期没做这一步。

4.4 一个完整事件流示例:从入库到结算全过程

假设农户在某日向平台供应 200kg 土豆,平台同日把其中 150kg 卖给门店,门店随后打款 1500 元,平台隔日向农户支付 900 元。实际会生成这样几条事件记录:

事件ID类型资源数量来源目标发生时间
E1001入库提供土豆批次 2025-03-01200kg农户平台03-01 09:00
E1002销售出库土豆批次 2025-03-01150kg平台门店03-02 10:00
E1003收款获款人民币资金1500元门店平台03-02 14:00
E1004付款提供人民币资金900元平台农户03-03 11:00

从这个流水里,任何人都能轻松推导出:平台持有土豆 50kg,平台银行存款余额为收款减付款后的净额,农户待结算余额已更新。这不是事后拍脑袋,而是把每个动作都落成可追踪事实。生产能力再复杂,底层也逃不出这些事件组合。

5. 从模型到系统:落地 REA 模型必须避开的坑

纸上谈兵容易,把 REA 推进真实项目的时候,我遇到过的坑还是不少。如果只记住几个教训,我建议你先记住下面三条:意图与事实的分离、资源粒度决策、多对多分摊关系。

5.1 先别全量应用 REA,MVP 阶段只做三张表

有一些强力推荐,会建议你把合同、承诺、资源组合等全部 REA 元素一次性建模,说是要让模型完整引入。我自己的态度是:如果你在做中小型系统,初期全量应用会把自己绕晕。比如合同独立建模有很强的理论价值,但业务还没跑通前,你最好先把 event 表做好。把“发生什么”记录对,比花时间去抽象“未来会承诺什么”更重要。

我建议的 MVP 范围:一张资源表、一张代理表、一只 event 表,就够了。所有业务环节,先统一变成 event 流水。后续真需要承诺机制,再新增细分表,主链路不会伤到。

5.2 订单/合同是意图,事件才是事实

这是我见过最普遍踩坑的问题,甚至不少系统已经按 REA 思想做了事件表,却还是把订单、发货单、对账单一股脑塞进去。订单不等于事实。客户下单“要 1000 件货”,最后实际分三批发货,每批 300、300、400。如果照订单表的 1000 件去汇总“已订未交”,你会发现数据时对时就错。

正确做法维持边界:订单属“意图”域,把订单号、期望交付时间、期望数量放在订单表;事件属“事实”域,把每批实发数量、实收数量放在事件表。订单与事件之间用订单号关联,但计算库存或应收款时,只基于事件表聚合。这样“预测”和“实际”两种数都能量得干净。

5.3 多对多分摊关系别漏掉

一个真实场景:门店一次性付了 5000 元,对应三张采购单。事件映射时难道要把一条支付事件拆成三行?可以是可以,但拆完你会失去原始凭证的完整性。所以更稳的方法是保留一条支付事件总金额,同时加一张分摊明细表,把一条支付事件关联到多张订单,并记录每单分摊金额。

分摊表可以通用化,核心字段:event_id、ref_type(销售订单、采购订单、发票)、ref_id、allocated_amount。这个表的存在会让后续应收应付核销变得非常灵活,不用天天埋着头改 main event 表。

5.4 并发写入和重复扣减:用幂等键守住资源余额

资源余额是推导出来的,但如果两个服务并发往事件表里插入“出库 100 件”,会造成数量双花事故。解决办法:第一,事件表对一个业务幂等键建立唯一索引,常见幂等键可以是“单据类型+业务单据号+行号+动作类型”,比如ORDER_READ_20250302_001_IN。第二,插入事件时附加状态字段,从 PENDING 改为 CONFIRMED 通常只能发生一次,后续重复请求会被拒绝。

我在实际项目里还会做一层“资源快照”:每隔一段时间把某事件之后的余额结果存起来,存储查询快表。小团队一开始不需要做太复杂,但并发重复问题一定要在设计评审时提出来,否则下面采购环节就会踩到余额负数问题。

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

下面这些是我手持 event 表排查线上问题时的经验。看起来很像零碎技巧,但实操中价值很大。

6.1 问题速查表

常见问题可能原因解决办法
库存余额为负事件缺失或重复出库按资源批次查询所有事件,找到重复录入或人工更正记录
对账不平订单与事件混在一起汇总确认订单状态与事件状态分离,只按事件表算应收应付
同一笔支付关联多笔订单错误分摊关系没管理增加分摊明细表,用裂变关系替代在事件表上改金额
重复事件导致库存双算无幂等控制业务幂等键建唯一索引,重复请求直接返回旧结果
报表查询慢每次查询都扫事件表新建余额汇总快照表,定期从事件流回放生成快照

这张表我每次验收模型阶段都会拿来对照。如果你根据类似症状排查,能省不少时间。

6.2 事件重复录入怎么办:幂等设计两种解法

解法一:业务流水号唯一索引。假设“采购入库单号”是 LB20250301001 的“入库”动作只能发生一次,就在 economic_event 表建立关联业务单号+业务动作类型的唯一索引。第二次插入同样数据会被数据库挡下。

解法二:事件去重表。你可以单独建一张event_dedup表,专门记录已处理的业务流水号。插入事件前先判断这个流水号是否处理过。这个方法适合事件表之前还有其他下游通知或异步任务,需要保证整个处理链路幂等。两种方案可以同时用,数据库唯一索引兜底,应用层返回友好提示。

6.3 如何从事件流里查“这笔采购为什么缺货”

如果用户问你“我采购的土豆到了为什么库存少了 800kg”,不要急着怀疑库存表,先写一条事件追溯查询:

SELECT event_type, from_agent_id, to_agent_id, quantity, occurred_at, ref_order_sn FROM economic_event WHERE resource_id = 10001 ORDER BY occurred_at, event_id;

拿到完整事件序列后,你会直观看到所有和该资源有关的事件。可能发现是某两笔订单被并成同一批出库,也可能是发生了退货冲销,总之顺着时间回顾比在库存表里瞎猜准得多。

6.4 我在实际项目中的几个小习惯

第一,接到新业务需求的第一步,不是建订单表,而是画事件流:把资源用圆圈表示,把代理用方框表示,中间连线就是事件。亲手画一遍比空想管用。

第二,事件表里留冗余字段,不要怕多。经常查询时,同事会把代理名称、资源名称直接冗余到事件表,这样省去大量 join,也不会因为源表改名而依赖被破坏。但要设计好同步点。

第三,事件不能物理删除。如果确实录错了,新增一条方向相反的事件,再标记原事件为 CANCELLED,这样后续审计能看到完整来龙去脉。我无数次靠这个习惯把历史问题解释清楚。

第四,接口命名尽量用动词短语,比如deliverResource()、receivePayment(),而不是updateStatus()。字段名和接口名一旦传播起来,团队认知就会慢慢统一到 REA 的语义层面。

写到这里,REA 的骨架和落地要点基本就清楚了。我自己现在的习惯是,接任何需求都会先把三个词写在便利贴上:资源是什么?事件是什么?代理是谁?等这些问题都有了明确答案,数据库的表结构往往也呼之欲出了。你下次做一个看似和财务无关的小系统时,也不妨先用 REA 过一遍,多半会有一种“早该这么做”的体会。

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

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

立即咨询