REA模式,全称Resource-Event-Agent(资源-事件-代理),是业务系统领域建模时一个绕不开的经典框架。我第一次接触它是在设计一套进销存系统的过程中,当时正被库存流水和财务凭证对不上的问题折磨,一位做财务系统多年的同行跟我说:“别老想着怎么把业务塞进凭证表,你该反过来,让业务自己长出账来。”他给我把REA的核心画了一遍,我才发现自己之前做的所谓业务模型,本质上是把“会计结果”当成了“业务起点”。
REA能解决什么问题?最直白地说,它把任何一笔生意抽象成“谁、在什么时间、对什么资源、做了什么事件”,并进一步把这些事件用“双效性”串成完整的价值链。这样一来,库存、成本、应收应付、收入确认这些东西,不再是靠人肉写分录推导,而是从业务事件本身自动映射出来。如果你正在设计ERP、电商后台、记账软件、供应链系统,或者任何需要同时管住“钱、货、人”三方的系统,REA能让你摆脱传统“科目余额表+凭证”的老路子,建出来的模型抗业务变化的能力会强很多。
这篇内容适合三类人:被财务对账和库存差异折腾过的后端开发,正在做业务建模的架构师或产品经理,以及刚入门领域驱动设计、想知道“业务事件到底该怎么落表”的读者。我会拆开讲REA到底在讲什么、为什么要这么设计,再带一套可以直接搬走的建模实操方案。
1. 为什么传统记账式建模总在翻车
很多业务系统在设计数据模型时,第一反应是去参考会计凭证。毕竟业务最终要反映到钱上,那直接把“借方科目、贷方科目、金额、摘要”抄过来不就行了?表面上看省事,但实际上从第一步就走歪了。
凭证是会计人员对业务进行“加工”后的产品,不是业务本身。你拿着加工后的结果去做系统底层的领域模型,等于把“解释权”固化在了表结构里。举个例子,销售退货这个业务,在凭证上可能是“借:主营业务收入,贷:应收账款”,同时“借:库存商品,贷:主营业务成本”。如果系统只按这套凭证结构建模,那新增一种业务类型时,就要新写一套凭证映射逻辑,业务规则散落在十几个if判断里,时间一长谁也说不清这个系统到底怎么运转的。
更麻烦的是,凭证式建模天然把“业务过程”和“会计结果”绑死了。可现实中,业务是先发生,会计是后处理,中间有时间差。比如货已经发出去了,但发票还没开,按凭证式建模,库存已经减少,收入却还没有入账,月末一结账就出现“已出库未开票”这种让人头皮发麻的对账项。REA的逻辑完全不同:先记录“发货”这个真实发生的事件,再单独记录“开票”这个事件,两者通过业务关系关联,账务处理反而成为可以被推导出来的结果。
再说一个我一直觉得很重要的问题:传统模型很难回答“为什么”。库存表里少了两件货,凭证上是“借:主营业务成本,贷:库存商品”,看起来平了,但如果你想知道“这2件货到底是被谁、在哪个订单、用什么价格卖出去的”,凭证结构给不了线索。REA保留了完整的业务痕迹,每一个资源变动背后都有对应的事件和参与者,等于系统自己记录了完整的“来龙去脉”。
我并不是说会计科目没用了。恰恰相反,REA和会计科目是兼容的,只是位置变了:科目从“模型的骨架”退位成“对业务事件结果的标准化输出”。建模的时候先看业务本质,等到需要出具报表时,再用规则从REA模型里转录成会计凭证。这就是“让业务长出账来”,而不是“让业务去拼凑账”。
2. 三要素拆解:Resource、Event、Agent到底指什么
REA名字里三个单词就是它的全部构成,但每个词的含义都跟日常理解不太一样,值得逐个抠清楚。
2.1 Resource:被管理和交换的东西
Resource(资源)在REA里不是泛泛的物质资源,而是“企业能够控制、并且愿意去交换或消耗的经济资源”。它有三个关键特征:有价值、可被控制、会因事件而变动。
库存商品是Resource,现金是Resource,应收账款也是Resource。你可能会说,应收账款不是“钱”啊,怎么也能算资源?在REA的视角下,应收账款代表了一项“未来获取现金的权利”,它同样有价值、可以被企业控制,而且会因为销售事件产生、因收款事件减少,所以它符合经济资源的定义。这个概念看似简单,但把“钱的权利”和“钱本身”都当成同一类东西,模型统一性会好很多。
实际操作中,很多人会把“固定资产卡片”和“库存明细”分开建模,这没有问题,但从REA角度看它们都是Resource,区别只是“跟随的存量字段和生命周期规则不同”。我建议在设计表结构时,给Resource设计一个通用的基础结构,再通过子类去扩展专有属性,而不是上来就分成完全独立的两套表。这样后续新增资源类型时,公共行为(比如入账、出账、盘点)可以复用。
2.2 Event:业务发生的原始痕迹
Event(事件)是REA里最核心的概念,它表示“资源发生了什么变化”。销售是事件,采购是事件,收款是事件,领料是事件。事件的核心特征是“已经发生”和“带有时间属性”,它是最原始的、不可再拆分的业务痕迹。
这里要特别注意一个原则:不要记录“将要发生”或“计划发生”的事,只记录“已经发生”的事。我见过有人把订单审核通过也当成Event来建模,这其实是把“业务流程状态”和“经济事件”混淆了。订单审核通过本身没有让任何资源发生增减,它只是一个审批动作。真正的事件是后续的出库、发货、收款。如果把这些非经济动作也塞进Event表,模型会被大量无效事件污染,双效性配对会变得很困难。
事件还应该记录“事件发生的时间点”而不是“记录入系统的时间点”。这两个时间经常不同,比如周日卖出的货,周一才录入系统。业务分析、财务核算通常都以事件时间为准,所以Event表里最好同时保留业务时间和系统时间两个字段,以免后续对账时被时间口径坑到。
2.3 Agent:参与事件的人或组织
Agent(代理)指参与到事件中的个人或组织。它不光是“谁做了这件事”,还包括“谁受益于这件事”和“谁承担了对应的义务”。比如销售事件里有企业自己的销售员,也有外部客户;采购事件里有企业员工,也有供应商。
Agent在REA里分成内部Agent和外部Agent。内部Agent,比如本公司的销售员、仓库管理员、采购员,他们通常跟薪酬、绩效考核相关;外部Agent,比如客户、供应商,他们跟应收应付相关。这个区分在建模和后续分析时非常有用,因为内部Agent的数据一般存在企业自己的员工表里,外部Agent的数据则要维护在客户或供应商的主数据中。
有一个容易被忽略的点:同一个组织在不同场景下可能扮演不同角色。某家公司在这个项目里是你的供应商,在另一个项目里又可能是你的客户。如果把“角色”直接钉在Agent实体上,就会出现一张“既是客户又是供应商”的脏主数据。更合理的做法是把Agent作为基础实体,用事件来标记它在特定场景下扮演的角色。也就是说,“角色”不是Agent的属性,而是Agent与Event关联时的上下文属性。
2.4 三要素之外的连接:增量和减量
有了三要素,还要把它们的关系落到“数量”上。REA中,Resource与Event之间的关联分两个方向:流入资源的Event是“增量事件”,流出资源的Event是“减量事件”。这里的“流入/流出”指的是资源流向企业。
举个采购例子:采购入库事件让库存资源增加,这就是一个增量事件;采购付款事件让现金资源减少,这是一个减量事件。同一笔采购业务,既产生了库存增量,也产生了现金减量,通过同一个采购订单把它们耦合在一起。
建模时,我强烈建议显式地给增量、减量加上方向字段或类型标记,因为后续所有库存结余、往来余额、毛利计算都要靠这个方向来判断加减。不要试图通过“金额正负号”猜方向,那条路看起来省事,实际做复杂报表时会把自己绕晕。我在项目中统一约定:增量记为正,减量记为负,并在事件上同时标注“资源方向”和“价值方向”,价值方向用于记账(借方/贷方),资源方向用于库存,两者独立又协同。
3. 双效性:REA能撑起完整业务链的关键
理解了三个基本概念,REA还不能闭环。让整个系统真正转起来的是“双效性”,也就是Economy Duality。它的意思是:任何一个经济事件,一定能找到另一个与之配对的事件,两者形成“给出一种资源、获得另一种资源”的完整经济交换。
比如销售给客户一批货,对应的销售事件是“库存商品流出”,那一定会有一个“应收账款流入”事件来配对。后续客户付钱,这个收款事件是“现金流入”,同时配对一个“应收账款减少”事件。整个链条就像拉链一样,一个增事件对应一个减事件,环环相扣。
很多第一次接触REA的人会问:为什么一定要搞双效性?我的理解是,双效性让系统能够自动验证业务完整性。如果只记录“发货”而不记录对应的“应收”,价值链就是断的,系统可以主动报警,而不是等到月底财务对账才发现差异。这种结构天然支持“业务与账务一体化”,因为它从根上保证了每一笔资源变动都有经济上的来路和去处。
双效性在建模里还有一个高阶技巧:当某个业务事件看起来找不到配对事件时,不要硬凑,去看看自己是不是漏了业务环节。最常见的例子是“免费赠送”。如果把赠品作为单纯的“库存减少”事件,它在双效性上找不到对应的增量配对,这通常说明“赠送”这个业务背后还跟着“营销费用增加”或其他价值交换。REA会逼你把业务想完整,这恰恰是它的价值所在。
在实际设计里,我会把双效性落地为一个名为“EventPair”的关联表,字段包括“原事件ID”“配对事件ID”“配对类型”。这样既能表达一对一的标准经济交换,也能表达一对多的复杂场景,比如一次销售对应多次分期收款,通过配对分组字段做聚合即可。
4. 实操:从零用REA建模一个“进货→销售→收款”业务
理论说太多容易飘,这一节我完整演示一遍怎么把REA套到常见业务上。这里以一家小商贸公司为例,业务链条是:向供应商进货 → 货入仓 → 卖给客户 → 客户付款 → 向供应商付款。
4.1 第一步:列出完整的业务事件流
不要先想表结构,先按时间顺序把业务环节写出来:
- 采购员向供应商下采购订单,确认进货意向。
- 供应商发货,仓库收货,商品入库。
- 财务收到供应商发票,确认应付账款。
- 销售员向客户销售商品,商品出库。
- 给客户开具销售发票,确认应收账款。
- 客户支付货款,款项到账。
- 企业向供应商支付货款。
注意,我特意把“下采购订单”和“收货入库”分开了,因为订单只是“计划”,入库才是“事件”。在REA模型中,计划类数据可以作为前置于事件的单据存在,但不进入核心事件表。理论上系统可以先建“采购订单单据”来记录计划,当收货发生时,再由收货事件去关联该计划数据。
4.2 第二步:识别每个事件的三要素
把上面每个环节拆成Resource、Event、Agent三列:
采购收货环节:
- Resource:库存商品(增加)
- Event:采购入库
- Agent:内部Agent是仓库管理员,外部Agent是供应商。
销售出库环节:
- Resource:库存商品(减少)
- Event:销售出库
- Agent:内部Agent是销售员,外部Agent是客户。
收款环节:
- Resource:现金/银行存款(增加)
- Event:收款
- Agent:内部Agent是财务出纳,外部Agent是客户。
付款环节:
- Resource:现金/银行存款(减少)
- Event:付款
- Agent:内部Agent是财务出纳,外部Agent是供应商。
把这四组拆完,你会发现业务里头最重要的东西已经全部浮现了。后面的设计就是把这些内容翻译成表结构。
4.3 第三步:用双效性配对事件
接下来做经济事件的配对。采购入库事件对应的配对事件是“应付账款增加”,但这里有一个建模选择:可以直接把“应付账款增加”理解为一个虚拟资源的变化,也可以单独建一个“应付确认”事件来承载。
我推荐前一种做法,也就是把“应付/应收”作为Resource的子类型,用事件直接改变它的存量。销售出库事件配对一个“应收账款增加”事件;收款事件配对一个“应收账款减少”事件。这样价值链非常干净。
配好对之后,整个业务链条可以这样表述:
- 采购事件:库存商品增加 ↔ 应付账款增加
- 付款事件:应付账款减少 ↔ 银行存款减少
- 销售事件:库存商品减少 ↔ 应收账款增加
- 收款事件:应收账款减少 ↔ 银行存款增加
到这里,整个经济循环就闭环了。系统在任意时刻只要聚合这些配对事件,就能天然算出库存余额、应收余额、应付余额和现金余额。
4.4 第四步:落成可执行的表结构
前面分析完,数据库设计就顺理成章了。我习惯按四张核心表来落地:
- res_resource:资源表,存放资源类型、名称、初始存量等。
- agt_agent:代理表,存放内部/外部主体信息。
- evt_event:事件表,存放业务事件的属性(事件类型、发生时间、关联资源、关联代理、方向、金额等)。
- evt_event_pair:事件配对表,存放双效性配对关系。
以下是一份可直接参考的简化建表示例:
-- 资源表 CREATE TABLE res_resource ( id BIGINT PRIMARY KEY, resource_type VARCHAR(32) NOT NULL, -- 库存商品/现金/应收账款/应付账款 resource_name VARCHAR(128) NOT NULL, unit VARCHAR(16), -- 计量单位 is_active BOOLEAN DEFAULT TRUE ); -- 代理表 CREATE TABLE agt_agent ( id BIGINT PRIMARY KEY, agent_type VARCHAR(16) NOT NULL, -- 内部/外部 agent_name VARCHAR(128) NOT NULL ); -- 事件表 CREATE TABLE evt_event ( id BIGINT PRIMARY KEY, event_type VARCHAR(32) NOT NULL, -- 采购入库/销售出库/收款/付款 event_time TIMESTAMP NOT NULL, -- 业务发生时间 system_time TIMESTAMP NOT NULL, -- 系统录入时间 resource_id BIGINT NOT NULL, -- 关联资源 agent_id BIGINT NOT NULL, -- 主要参与者 quantity NUMERIC(18,4) NOT NULL, -- 数量方向:正为流入,负为流出 amount NUMERIC(18,2) NOT NULL, -- 价值方向 counterparty_id BIGINT, -- 外部交易对手,可空 remark VARCHAR(512) ); -- 事件配对表 CREATE TABLE evt_event_pair ( id BIGINT PRIMARY KEY, left_event_id BIGINT NOT NULL, right_event_id BIGINT NOT NULL, pair_type VARCHAR(32) NOT NULL, -- 采购配对/销售配对/收付款配对 group_no VARCHAR(32) -- 用于一对多分组的标识 );这套结构看着简单,但实际跑起来非常稳。库存余额就是对该资源所有事件的quantity求和,应收余额就是对“应收账款”资源所有事件的amount求和。月末对账不再是比对一堆凭证,而是直接对着原始事件查流水,哪里多了少了,一眼定位到具体业务环节。
4.5 一个完整的事件流示例
为了让你更直观地看到事件如何“流”过这套模型,我构造一条虚构数据,来看从进货到收款的全过程。
初始状态:库存商品0件,现金100万元,无应收应付。
第一步,向供应商进货100件,单价10元。
- 事件A:采购入库,resource_id指向库存商品,quantity=+100,amount=1000元。
- 事件B:应付增加,resource_id指向应付账款,amount=+1000元。
- 配对表:left=A,right=B,pair_type=采购配对。
第二步,向供应商付款,全额支付。
- 事件C:付款,resource_id指向银行存款,quantity=-1000元。
- 事件D:应付减少,resource_id指向应付账款,amount=-1000元。
- 配对表:left=C,right=D,pair_type=付款配对。
第三步,向客户销售60件,单价20元。
- 事件E:销售出库,resource_id指向库存商品,quantity=-60,amount基于成本价计算(比如按10元成本,则amount=-600元)。
- 事件F:应收增加,resource_id指向应收账款,amount=+1200元。
- 配对表:left=E,right=F,pair_type=销售配对。
第四步,客户付款80件货款,按单件20元算共1600元。 等等,这里数据需要改一下。客户买了60件,货款应该是60×20=1200元。客户全额付款1200元。
- 事件G:收款,resource_id指向银行存款,quantity=+1200元。
- 事件H:应收减少,resource_id指向应收账款,amount=-1200元。
- 配对表:left=G,right=H,pair_type=收付款配对。
这时看各资源余额:
- 库存商品:+100 - 60 = 40件。
- 银行存款:100万 - 1000 + 1200 = 100万零200元。
- 应收账款:0元。
- 应付账款:0元。
整个过程在事件表里能完整追溯:哪一批货进来的,哪一批货出去的,钱从哪里来、到哪里去,一目了然。这就是REA在实操层面带来的最大改变。
5. 落地避坑:这些问题我几乎每个项目都会遇到
REA理论听上去很顺,真正落地时坑不少。下面是我在几个项目里真实踩过、也调整过的点。
5.1 别把“流程审批”混进“业务事件”
这是新手最容易犯的错。系统都会有审批流,比如“销售订单审核”“采购申请批准”,有人为了图省事,直接把审批动作也记成Event。Event一旦混入非经济动作,双效性就乱了,库存和金额的聚合查询会被大量无意义的记录干扰。
我的处理原则是:只录入会引起Resource增减的事件。审批、流转、审核这些动作放在独立的流程表中,与核心事件表解耦。如果确实需要知道“这笔销售是谁审批的”,通过流程表反查就好,没必要污染事件表。
5.2 资源和事件的方向别用“感觉”定义
在代码里,增量和减量一定要显式定义,最好在常量层就定清楚。比如“入库=IN=+1”“出库=OUT=-1”,并在事件写入时结合资源类型的属性校验方向是否正确。不要想当然地在页面展示时通过颜色和正负号偷偷转换,数据库里保持一致的符号约定,所有统计逻辑都基于同一套方向定义。
另一个相关的小坑是“金额方向”和“数量方向”可能不一致。入库是数量+1但金额可能是+,而退货入库在财务上是冲减,数量方向与金额方向在不同业务语义下有不同的组合。建议在事件表里分开存“quantity”和“amount”,不要合成一个字段,否则销售退货这类复杂业务会把余额算错。
5.3 应付应收不要只盯“余额”,要看“事件流”
很多人用传统模型时,养成了只看应收余额的习惯。但在REA模型里,应收余额是聚合出来的结果,更建议去做“事件流画像”。比如查询某客户的应收账款时,不只看余额是多少,还要看到底是哪些销售事件产生了它、哪些收款事件冲销了它、哪些事件还悬在那里。
这样做的直接好处就是,月底对账时财务抛给你一个差异,你能顺着事件流就找到那张异常单据。传统表结构要给财务导一堆明细,REA模型的查询逻辑天然支持穿透式追溯,这在和业务对线时太省心了。
5.4 资源主数据一定要提前治理
REA对主数据的要求其实比传统凭证式建模更高,尤其是Resource表。如果资源类型没有统一编码,后续事件关联时会出现同一物料在采购单写“A4复印纸”、在销售单写“A4打印纸”这类问题。
我建议在项目启动阶段就定义好资源目录和编码规范,资源主数据的变更走审批流程。别看这和REA没关系,Resource治理不好,后面所有事件聚合都会失真。这件事在传统模型里最多造成报表难看,在REA里会造成整个存量数据的污染,代价是完全不同的。
5.5 双效性不是一对一的死绑定
设计双效性关联时,不要下意识认为一个事件只能配对一个事件。分期收款、分批入库都是天然的“一对多”场景。这里我更建议用配对表去解耦,而不是在事件表里加“pair_id”字段。
曾经在分销项目中遇到客户要求“一次采购分成三个月付款”,如果只在事件表存pair_id,根本无法表达“三个付款事件对应一个采购事件”的多重关系。后来改成独立的event_pair表,加一个group_no字段,把三次付款归到同一组,问题就解决了。所以模型一定要为“多重性”留余地。
6. 扩展思路:REA能做的远比记账多
REA虽然起源于会计领域,但它本质是一套“用事件串起资源与主体的通用建模方法”。一旦接受了这个思维,能做的就不止财务系统。
在电商后台,可以用REA统一处理“商品上架、下单、支付、发货、退货、退款”的全链路。你把“订单”视为一个虚拟资源,“支付成功”视为一个事件,“发货”视为一个事件,“确认收货”和“退款”也是事件。这样做的好处是,所有资金和货物流转都沉淀成事件流,风控、运营和财务共用同一套数据基础,不用各建各的报表口径。
在供应链协同平台,REA可以用来表达多个参与方之间的资源流转。比如分销商向品牌商订货、物流公司承运、消费者签收,每一段都是“资源+事件+代理”。通过REA把跨组织的经济交换统一建模,各方对账时就能看到同一套事件流,而不是各拿各的Excel表来回扯皮。
还有更复杂的业务场景,比如会员积分系统。把“积分”抽象为一种资源,“积分发放”“积分消耗”“积分过期”“积分购买权益”都是事件,会员是Agent。这样积分账本、过期预警、成本核算都变得非常清晰,不需要在一堆优惠券代码里翻逻辑。
我身边越来越多同行开始尝试用REA思维替代“科目+凭证”的建模框架,尤其是在快速迭代的互联网业务中,REA这种“先理清业务本质、再倒推账务结果”的方式,容错性和扩展性都比传统做法强太多。
最后分享一个我自己的体会:REA上手最难的其实不是表结构设计,而是转变思维方式。刚开始建模时,总会下意识去找“科目”,找“凭证”,老觉得没有“借:库存商品,贷:应付账款”心里不踏实。等真正在事件表里看到业务数据自然流淌、月底报表一键算平的时候,就会明白所谓“让业务长出账来”到底有多爽。如果你也在被业务建模和财务对账反复折磨,真心建议给REA一次机会,从一个小模块开始试点,切身体验一下“从事件看业务”的视角变化。这套模式不会让你立刻写出更花哨的代码,但它会让你设计的系统在业务变了十次之后,依然稳如磐石。