☰
REA建模指南:用资源-事件-代理重构进销存与业务系统
2026/10/11 8:30:48 网站建设 项目流程

最近在重构一套老进销存系统的时候,我在旧代码仓库里翻到一个文件夹,名字就三个字母:rea。同事说那大概是“资源(resource)”的随手缩写,但我盯着那三个字母看了很久,越看越觉得不像随手写的。后来我反应过来了——它真正对应的,是业务建模里一个被很多开发者低估的方法,REA,也就是 Resource-Events-Agents,资源-事件-代理模型。

REA 不是什么新编程框架,也不是新数据库,它是一套“把业务真实发生的事原原本本装进数据模型”的建模思路。跟传统会计科目表那一套不同,REA 的第一关注点不是“借方贷方怎么平”,而是“到底发生了什么”。这套思路特别适合做订单、库存、资金搅在一起的业务系统,比如电商、进销存、会员体系、物流调度。如果你正在被“表怎么设计才能既兼顾业务又方便查账”这件事折磨,这篇文章值得看完。

我不会堆理论,而是把从概念到落地的全过程写清楚:REA 到底是什么,为什么我放弃纯科目表,怎么在一个具体场景里一步步建出模型,模型建完以后表结构、查询、报表怎么写,以及我在真实项目里踩过的一些坑。

1. REA 模型的底层逻辑:资源、事件、代理究竟在描述什么

很多刚接触 REA 的人第一反应是:这三个词也太普通了,像是把实体关系换了个名字。但真正用过的人都知道,这三个词背后是把“记账思维”升级成“业务语义思维”的关键。我们一个个拆开看。

1.1 资源(Resources):不是账户,而是真实存在的经济对象

先说资源。在 REA 里,资源不是“应收账款”“应付账款”这种抽象科目,而是组织能控制、有价值、可以被经济事件“流入”或“流出”的具体对象。一件商品、仓库里的一批物料、一张购物卡、你银行卡里的余额,这些都是资源。

怎么判断一个东西算不算资源?就看它能不能被经济事件直接增减。只存在于账面上的纯记账元素,不算资源。举个例子,在线书店场景里,“书籍”是资源;“书籍销售收入”不是资源,因为销售收入是业务事件发生后的度量结果,不是真实对象。

这个区分非常关键。它逼着你把平时见惯了的会计科目重新翻译回物理世界的语言。传统表里你写“库存商品”科目,REA 里你写“图书”资源;传统表里你写“银行存款”科目,REA 里你写“资金”资源。表面上只是改名,实际上整个思维原点变了:你不再从“科目从哪里来”出发,而是从“业务里到底有哪些真实的东西在流动”出发。

1.2 事件(Events):不是流水,而是业务链上的动词

事件是 REA 里最核心的元素。如果说资源是名词,事件就是动词,比如销售、采购、收款、付款、发货、收货、退货。一个事件必须导致资源的流入或流出,否则它只是个备注,不是事件。

我自己建模时有个习惯:把业务里的“动词”全列出来,然后一个个问“这个动作改变哪个资源的状态”。能回答这个问题的才算事件。比如“客户提交订单”严格讲不是一个经济事件,因为它没有立刻让库存变少;真正的事件是“销售出库”,那一刻图书库存才真正减少。

事件还有一个容易被忽略的特征:时间轴属性。REA 本质上是一个沿着时间展开的模型,每个事件都带着发生顺序。这也是后来它能和事件溯源思想(Event Sourcing)产生共鸣的原因。你要统计“从下单到回款平均几天”,靠的就是事件之间的时间差,而不是靠某张表的固定时间字段。

1.3 代理(Agent):每一个动作都要有归因

代理是参与经济事件的人或组织。客户、供应商、公司内部的销售员、仓库保管员,都是代理。

代理要分两类,建模时必须区分:内部代理和外部代理。内部代理是你公司的人,比如销售专员、仓管员、财务专员;外部代理是客户、供应商、渠道商。为什么要分开?因为后面你做业绩统计、客户消费分析、供应商评价时,所有维度都挂在代理节点上。不分清楚,以后查询会特别难受。

这里有个实践中的细节:一个事件通常关联多个代理。一次销售事件,至少有两个代理参与——客户(买方)和销售员(卖方/经办人)。如果你在设计表结构时只给事件挂一个“客户ID”字段,后面再加销售员、审核人、配货员就会很痛苦。REA 的正确做法是用关联表,把代理和事件做成多对多,再用“角色”字段区分谁是买方、谁是经办人。

1.4 三种关系:存量-流量、参与、双向配对

REA 只有三种核心关系,却能覆盖几乎所有业务场景,这一点很神奇。三种关系分别是:

  • 存量-流量关系(stock-flow):事件与资源的关系。销售导致库存资源流出,采购导致库存资源流入。一进一出,资源的“存量”就被事件不断改写着。
  • 参与关系(participation):事件与代理的关系。谁参与了这件事,承担什么角色,都由这种关系描述。
  • 双向配对关系(duality):事件与事件的关系。最典型的是“销售事件”必须对应一个“收款事件”,这一对告诉你销售是按现金结算还是赊账。

画图的时候,千万别一上来就想外键、主键、索引,先把这三种关系在业务里理清楚,模型基本就立住了。表结构只是这些东西的物理投影而已。

2. 为什么我丢掉科目表:REA 和传统记账模型的正面拆解

做业务系统的人,很容易下意识把财务逻辑直接塞进数据库——建一张科目表,所有业务都往里面记一分录。早年我写进销存也这么干,后来发现不是应付不了账,是应付不了“查账”。业务系统真正需要的是还原过程,而会计系统最终需要的是能出报表的余额表,两者关注点本质上不一样。

2.1 传统科目表的三个痛点

第一,语义丢失。数据库里存的是一堆借贷分录,你问“这个客户为什么退货”,查不出来。因为它只记录了金额变动的结果,没有记录触发变动的业务原因。

第二,扩展困难。要新增一条业务线,就得加一堆科目和过渡科目,规则全硬编码在表结构里。业务一变,表结构跟着大改,改动风险极高。

第三,追溯麻烦。从库存余额一路查到原始凭证,中间隔了好几层,经常要跨好几张表做全扫描式的联查。审计要求高的时候,这套模型会让你加班加到怀疑人生。

2.2 REA 的语义完整性

REA 记录的是带语义的事件链。在 REA 里查“某客户从下单到付款的平均周期”,你不需要去猜,因为销售事件和收款事件本身就是两个独立的节点,并通过双向配对关系连在一起,你只需要算两个时间戳的差值就行。

再比如“哪些销售员手上的订单最容易发生退货”,这个问题在传统科目表里基本无解。但在 REA 里,退货也是一个事件,它和原销售事件之间可以通过资源批次去关联。你按销售员维度做聚合,马上就能得到答案。这种语义完整性,是科目表模式给不了的。

2.3 一个直观对比表

对比维度传统科目表模式REA 模型
核心对象会计科目、分录资源、事件、代理
记录重点金额的借贷变动业务事件的完整语义
业务可追溯性只能从科目余额反推事件链直接还原全过程
扩展新业务新增科目和过渡规则新增事件类型和关联关系
报表能力天然适配财务三大表需要投影层适配财务报表
适合场景纯财务核算复杂业务流系统、数据中台

你可能会问:那是不是可以完全抛弃科目表?我前面说了,不建议。落地的时候最稳妥的是“双轨制”:用 REA 管业务链路,用科目表做报表投影。REA 负责把真实发生的事件存清楚,然后通过一套映射规则,把事件转成财务需要的借贷分录。这套做法我会在第四章详细展开。

3. 六步建模法:用在线书店的 REA 项目把整条链路建出来

理论的利刃磨得再快,也得见血。我拿一个完整的在线书店场景,带你从头走一遍建模过程。别嫌这个例子普通,REA 的难点从来不在行业特殊,而在“事件找得对不对”。

3.1 业务场景描述

假设我们要做这样一个系统:客户注册成为会员,在网上下单买书,可以用优惠券抵扣一部分金额;仓库接单后发货,客户确认收货;货款先进入平台账户,再结算给书店;客户可以申请退货退款。

一句话描述:卖书、发货、收钱、退货。就这么简单的业务,想变成 REA 模型,需要老老实实走六步。

3.2 六步建模法

第一步:穷举业务动词,找出所有经济事件。

把业务流程从头到尾过一遍,凡是“会改变资源状态”的动作都记下来。在线书店场景里,我列出来的是:销售出库、收款、退货、退款、采购入库、付款给供应商。优惠券抵扣不是事件,它是销售事件的一个属性,后面建模时挂在销售事件上就行。

第二步:把每个事件对应的资源标出来。

销售出库事件对应的资源是“图书库存”,方向是流出;收款事件对应的资源是“平台资金”,方向是流入;退货事件对应“图书库存”流入,同时对应“资金”流出。一个个标完,你会发现资源是有限的,大多数事件都落在库存和资金上。

第三步:把每个事件对应的代理标出来。

销售出库事件:客户是外部代理,销售专员(或系统自动处理人)是内部代理。收款事件:客户是付款方,财务专员是经办方。采购入库事件:供应商是外部代理,采购员是内部代理。代理必须要能回答“谁发起的”“谁经手的”“谁受益的”。

第四步:给事件配对双向关系。

销售出库和收款是一对配对,表示“卖了货、收了钱”。采购入库和付款给供应商是另一对配对,表示“进了货、付了钱”。退货和退款也是一对配对。注意,配对是有方向的,可能先款后货,也可能先货后款,模型里要保留两个方向,不能写死。

第五步:完整性检查。

每个事件是不是至少连了一个资源?每个事件是不是至少连了一个代理?没连资源的事件是伪事件,没连代理的事件是悬空事件。这一步能筛掉至少一半的建模错误。

第六步:把关系用文字图落下来,再翻译成表结构。

销售事件(SALE):

  • 流出资源:图书库存(资源)
  • 流入资源:应收账款承诺(为简化可暂时并入收款资源)
  • 参与代理:客户(外部)、销售专员(内部)
  • 双向配对:销售事件 ↔ 收款事件

收款事件(CASH_RECEIPT):

  • 流入资源:平台资金(资源)
  • 参与代理:客户(外部)、财务专员(内部)
  • 双向配对:收款事件 ↔ 销售事件

采购入库事件(PURCHASE):流出资源:采购承诺;流入资源:图书库存。 付款事件(CASH_PAYMENT):流出资源:平台资金。

文字图列出来后,数据表就水到渠成了。

3.3 表结构设计:从关系图到数据库

我直接给一套可以动手建的表结构,带注释:

CREATE TABLE resources ( resource_id INT PRIMARY KEY, resource_code VARCHAR(32) UNIQUE NOT NULL, resource_name VARCHAR(64) NOT NULL, resource_type VARCHAR(32) NOT NULL, -- 实物 / 资金 / 权益 created_at TIMESTAMP DEFAULT now() ); CREATE TABLE agents ( agent_id INT PRIMARY KEY, agent_code VARCHAR(32) UNIQUE NOT NULL, agent_name VARCHAR(64) NOT NULL, agent_type VARCHAR(16) NOT NULL, -- 内部 / 外部 agent_role VARCHAR(32) -- 客户 / 供应商 / 销售 / 仓管 ); CREATE TABLE economic_events ( event_id INT PRIMARY KEY, event_code VARCHAR(32) UNIQUE NOT NULL, event_type VARCHAR(32) NOT NULL, -- 销售 / 采购 / 收款 / 付款 / 退货 event_time TIMESTAMP NOT NULL, remark TEXT ); CREATE TABLE event_resource ( event_id INT REFERENCES economic_events(event_id), resource_id INT REFERENCES resources(resource_id), direction CHAR(1) NOT NULL, -- I=流入(增加) / O=流出(减少) quantity NUMERIC(18,4) NOT NULL DEFAULT 1, unit_price NUMERIC(18,2), PRIMARY KEY (event_id, resource_id) ); CREATE TABLE event_agent ( event_id INT REFERENCES economic_events(event_id), agent_id INT REFERENCES agents(agent_id), role_in_event VARCHAR(32) NOT NULL, -- 买方 / 卖方 / 经办人 / 审核人 PRIMARY KEY (event_id, agent_id) ); CREATE TABLE event_duality ( event_1_id INT REFERENCES economic_events(event_id), event_2_id INT REFERENCES economic_events(event_id), duality_type VARCHAR(32) NOT NULL, -- 销售-收款 / 采购-付款 / 退货-退款 PRIMARY KEY (event_1_id, event_2_id) );

这套表结构看着简单,但能覆盖的业务查询远超你的想象。核心秘密就在 event_resource 和 event_agent 两张关联表里:它们把多对多关系真正打开了。传统系统里那种“一笔订单表带一个客户ID和一个仓库ID”的设计,在这里变成了事件和代理的动态关联,加角色、加参与者都是加一行数据的事,不用动表结构。

3.4 映射到 ORM 时的注意点

如果你用 ORM 框架,别把 REA 关系生搬硬套成纯粹的嵌套对象。我常用的做法是:事件作为聚合根,资源和代理作为被引用对象。读取时按事件维度 join,写入时先写事件,再批量写关联表。这样既保住 REA 的语义,又不会让 ORM 的 Lazy Loading 把性能拖垮。

4. 模型落地之后:查询、报表和中间层到底怎么写

模型建完只是第一步,真正考验 REA 的是“能不能把需要的数取出来”。这一章我直接给查询思路和 SQL 示例。

4.1 把事件链查出来:一单从销售到收款的完整路径

REA 最舒服的查询场景,就是还原一条完整的事件链。比如你想看某笔销售什么时候发生、什么时候回款、间隔多久,用 duality 表一查就出来:

SELECT sale_event.event_code AS sale_code, sale_event.event_time AS sale_time, receipt_event.event_time AS receipt_time, EXTRACT(DAY FROM (receipt_event.event_time - sale_event.event_time)) AS gap_days FROM event_duality d JOIN economic_events sale_event ON sale_event.event_id = d.event_1_id JOIN economic_events receipt_event ON receipt_event.event_id = d.event_2_id WHERE d.duality_type = 'SALE_RECEIPT' AND sale_event.event_time >= '2025-01-01';

这套查询在传统科目表模式里很难写,因为你根本不知道哪笔收款对应哪笔销售。REA 里 duality 关系就是干这个的。

4.2 按代理做分析:客户贡献度和销售员业绩

代理节点是天然的维度表。你想统计每个客户的采购额和回款额,可以这么写:

SELECT a.agent_name AS customer_name, SUM(CASE WHEN e.event_type = 'SALE' THEN er.quantity * er.unit_price ELSE 0 END) AS sale_amount, SUM(CASE WHEN e.event_type = 'CASH_RECEIPT' THEN er.quantity * er.unit_price ELSE 0 END) AS receipt_amount FROM agents a JOIN event_agent ea ON ea.agent_id = a.agent_id JOIN economic_events e ON e.event_id = ea.event_id JOIN event_resource er ON er.event_id = e.event_id WHERE a.agent_type = 'CUSTOMER' AND e.event_time >= '2025-01-01' AND e.event_time < '2025-02-01' GROUP BY a.agent_name;

这里有个细节:客户和销售员都是代理,如果要在同一套数据里既按“客户”分组又按“销售员”分组,你得在 event_agent 里靠 role_in_event 区分。这也是为什么我建议代理关联表一定要保留“角色”字段。没有它,一个事件关联多个代理的时候,分析维度会直接乱掉。

4.3 双轨落地:把 REA 事件投影成财务报表

财务同事不关心你的事件链,他们要看科目余额表、利润表。这时候就要在 REA 层和报表层之间加一个投影层,也就是把事件按照预定义规则“翻译”成凭证。

REA 事件投影成会计凭证的逻辑
销售事件(图书库存流出)借:营业成本,贷:库存商品(按成本金额)
销售事件(应收债权形成)借:应收账款,贷:主营业务收入(按售价金额)
收款事件(资金流入)借:银行存款,贷:应收账款
采购入库事件借:库存商品,贷:应付账款 / 银行存款
退货退款事件反向处理对应凭证

具体实现上,可以用定时任务或者物化视图,把 REA 原始事件按映射规则聚合,写入一张财务科目余额表。这样维护起来特别舒服:业务变更时改映射规则,不动历史事件;财务报表要追溯时,又能从余额表一路看到具体事件。

我见过一个项目用事件流 + 规则引擎做投影,历史凭证全部由事件回放生成,审计的人来了,直接翻事件流,连原始凭证都对得上。这套方案能实现的前提,就是底层用的是 REA 这种保留完整语义的模型。

5. 真实项目里的边界感:哪些地方 REA 好用,哪些别硬上

REA 不是银弹。用了一两个月后,我总结出它的几个边界和实战中的教训,每一条都是付过学费的。

5.1 过度建模:承诺层不是第一优先级

REA 完整理论里有一个“承诺”概念,用来建模“订单已经下了但还没发生实际资源交换”的状态。比如客户提交订单,承诺要买一本书;仓库还没出库,库存还没减少。

很多团队一上来就把承诺层建得很细,结果模型复杂到没人愿意维护。我的建议是:第一迭代先只建模“已经发生的经济事件”,订单这种承诺状态放到事件属性里先存着,等模型跑稳了,再去抽象承诺层。先把已经发生的经济事件建模,承诺层留到第二迭代再考虑。

5.2 你不需要推翻科目表

这是最容易走偏的地方。有人说 REA 是“会计系统的终结者”,真不是这样。REA 解决的是业务语义记录问题,而财务报表仍然需要借贷平衡这些规则。落地时最稳的架构是双轨:REA 作为链路层,负责真实事件;科目表作为报表层,负责出具财务结果;中间用投影规则连通。这样既保留语义能力,又不让传统财务流程产生排异反应。

5.3 和事件溯源思想的异同

做微服务的人一看到事件就兴奋,以为 REA 就是事件溯源。两者有关联,但不是一个层面的东西。事件溯源是架构模式,关心的是“怎么通过重放事件恢复系统状态”;REA 是领域建模方法,关心的是“业务语义怎么表达、资源怎么增减、谁来参与”。实际系统里可以结合:底层事件流存储原始事件,REA 模型作为逻辑视图,专门服务查询和分析。但别把事件边界直接当 REA 事件边界用,否则建模会变形。

5.4 团队认知门槛的取舍

如果团队里全是传统 ERP 思路的人,切 REA 的学习成本不低。我经历过一段阵痛期:程序员习惯了一张订单一个表,不理解为什么要拆事件、资源、代理三张主力表。我的做法是渐进式落地,新模块用 REA 建模,老模块保持原样,中间用适配层透出数据。新模块跑通、数据分析同事真真切切体会到查询快感后,再慢慢把老模块搬过来。全量重构一次,风险太高,不划算。

另外还有一个很实用的排查经验:模型里出现“悬空事件”时,先别急着调代码,回到事件清单重新过一遍业务动词。大多数建模错误都来源于动词找得不准,比如把“确认订单”当成事件,把“提交退货申请”当成事件。真正的事件一定同时改变资源和代理之间的实际经济交换。

最后再说一点个人体会。这次重构最大的收获,不是“用上了新模型”,而是逼着自己把业务从头到尾用“动词”捋了一遍。你一旦认真统计业务里到底有哪些“改变资源”的动作,会发现很多以前被流水号淹没的规则,原来一直躺在那里没人管。REA 给你的不是更花哨的技术,而是一副能看清业务全景的眼镜。

如果你想在业务系统里试 REA,我的建议是先别急着画图,找一个最不起眼的小场景,把动词列全再说。等你那份事件清单能开口讲话的时候,模型的一半已经立住了,后面不过是在给现实世界做投影而已。

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

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

立即咨询