
如果你在搜索框里敲下 rea 这三个字母会得到一堆互不相干的结果有人名、地名、公司名的缩写某个产品线代号甚至一个注册表键值。但只要你是在做企业信息系统、财务数字化、订单中台这类工作看到 rea 时第一反应大概率应该是同一个词——REA 本体模型。我接触 REA 是因为一个特别典型的现场某小型网上书店项目订单库、财务库、物流库各建一套订单状态自己维护一套财务科目自己维护一套商品库存又单独一套。结果月底对账财务说销售金额对不上库房说库存数量对不上技术说每个系统都按自己的逻辑记了谁也没错但合在一起就是不对。后来我们换了一种思路——不再从科目、从页面、从状态机出发而是从业务到底发生了什么出发去做统一建模这就是 REA。这篇文章把我对 REA 的理解、完整的建模过程、物理表设计、与复式记账的关系以及实际落地中踩过的坑梳理一遍。内容偏向企业业务建模和数据架构适合做电商订单、库存、财务体系或者准备重构老旧交易系统、想统一多套系统口径的开发者、业务分析师、数据架构师参考。1. 为什么传统的借贷记账模型在业务系统里越来越不顺手1.1 复式记账擅长平账却不回答业务发生了什么复式记账这套机制很伟大它用有借必有贷、借贷必相等把企业的一笔笔经济活动压缩成会计科目上的增减记录保证了账目的勾稽关系。但它本质上解决的是记录问题不是理解问题。它记录的是结果——钱和物的账面变化却没有结构性地记录这笔变化到底来自哪一段业务流程。举个例子。某个订单在系统里的真实过程是顾客下单、支付货款、仓库发货、顾客收货。放在传统账务里这中间会产生销售收入凭证、成本结转凭证、银行收款凭证可能还有库存减少凭证。四张凭证之间有没有血缘关系在科目表里看不出来。你只能根据时间、金额、单号去反推。一旦出现部分退款、部分发货、跨期收款反推的匹配工作就会变得极困难。我见过很多业务系统就是用记账凭证作为中心来建的加一个销售模块就往凭证表里插一行加一个库存模块再插一行。系统越来越多凭证表变成一个大杂烩谁也说不清哪一行对应哪个真实事件。1.2 科目树越长越复杂业务口径越来越分裂财务人员习惯用科目树描述一切库存商品、主营业务收入、应收账款、银行存款、应付账款。科目树本身没有错可是它离业务太远。业务人员看的是订单已支付包裹已签收商品已入库这两种语言之间没有一个稳定的中间结构。在系统集成时这种分裂会直接变成一张越来越大的映射表订单系统的已收款对应财务系统的银行存款增加物流系统的出库对应财务系统的库存减少每个接口都要手工对齐一次。曾经有个项目映射表里的口径规则从最初的三十多条膨胀到两百多条最后维护映射表的人离职新来的人根本不敢动那套规则因为每条规则背后都有说不清道不明的历史原因。问题的根源在于科目是财务视角的篮子它天然不关注一个订单怎么变成收款和出库的完整因果链。而业务系统需要的是因果链否则就无法回答为什么库存少了为什么应收账款多了这个数字。1.3 REA 的解法把业务事实而不是记账凭证作为建模中心REA 的全称是 Resource-Event-Agent中文一般叫资源-事件-参与方。这套模型最初是二十世纪八十年代会计信息系统研究者提出的一种企业本体建模方法。它的核心主张非常直接描述企业经济活动时不应当以会计科目、凭证、分录为中心而应当以业务事件为中心围绕事件去连接资源发生了什么变化和参与方是谁发生的、对谁发生。它跟普通 ER 建模有什么区别ER 建模往往是从页面、表单、业务对象出发的比如订单表商品表用户表它是为了满足一个具体系统界面的存取需求。REA 则是从语义出发的它先回答系统中到底有哪些真实发生的经济事件再回答每个事件改变了哪些资源、涉及了哪些参与方。页面可以变、状态字段可以加但销售发货这个事件和库存商品减少这个事实永远不变。这正是 REA 在多个系统之间做统一口径时最好用的地方。2. REA 三原语资源、事件、参与方以及它们之间的配对关系2.1 什么是资源不只是库存商品REA 里的 Resource 指一切有价值、可以被交换、被消耗、被生产的东西。注意它不是数据库里的某个业务主数据那么简单。图书库存是资源现金是资源应收账款是资源给顾客提供的咨询服务也是资源甚至用户积分都可以当成资源建模。这里有一个重要区分资源是价值载体而商品主数据只是资源的描述属性。比如一本《某某导论》它的书名、ISBN、出版社是主数据属性但当这本书从供应商那里采购入库、又被顾客买走时它作为库存商品参与了一次次经济事件这才是 REA 说的资源。把两者分开你后续建模会顺很多——主数据表怎么改不影响事件表的语义事件表怎么扩展也不用改主数据。2.2 什么是事件价值链上最小的经济事实Event 是 REA 的核心原语它表示某件事真实发生了并改变了资源的状态。命名习惯上事件一定要用动词宾语比如销售图书收到货款采购入库支付货款发出商品。一个合格的事件具备三个特征原子性不可再拆分、时间性有一个准确的发生时间、资源影响至少让一种资源发生流入或流出。判断一个操作能不能算是事件可以问自己如果不记这条流水后面会不会有对不上的账如果会那它多半就是事件。要特别小心那些看起来像事件、实际不是事件的操作比如创建订单就不是经济事件——它只是表达了一个未来的承诺真正的事件是后续的收款发货。再比如查询库存也绝不是事件。把非事件混进事件流是所有 REA 实施里最常见的坑之一。2.3 什么是参与方责任主体与交易对手Agent 指参与经济事件的个人或组织它解决这事是谁干的、对谁干的这个问题。一般分为两层内部参与方店员、仓库管理员、销售部门和外部参与方顾客、供应商、物流公司。为什么参与方要单独建表、单独建模因为同样一个销售图书事件站在书店内部看是店员执行的站在外部看是顾客购买的。一个事件很可能同时涉及多个参与方而且同一个参与方在不同事件里扮演不同角色。如果不把参与方抽象出来而是在订单表里塞一个顾客ID在出库表里塞一个库管员ID那么跨流程审计的时候就很难回答谁在什么时间对谁做了什么。2.4 三组核心配对关系REA 模型最重要的是原语之间的配对关系不是原语本身。见下表配对关系名称含义例子Resource ↔ Event存量-流量关系资源通过事件发生流入或流出流入减流出得到当前存量图书库存因销售发货事件而减少Event ↔ Event交换配对Duality一个流出资源的付出事件必然对应一个流入资源的获得事件体现价值守恒发货流出了图书同时收款流入了现金Event ↔ Agent控制关系事件由某个参与方执行或作用于某个参与方记录责任主体收款事件由店员操作顾客付款Agent ↔ Agent内部与外部配对企业内部参与方与外部参与方构成交易双方店里店员与顾客构成买卖关系这四组关系里新手最需要理解的是交换配对Duality。它的意思是真实世界里没有无缘无故的得与失一个企业之所以愿意流出一本书是因为它收到了钱之所以愿意付钱是因为它收到了货。REA 把这种一进一出绑定成一对系统的价值守恒自然就有了。传统复式记账里的借贷必相等本质上也来自这同一件事。3. 用网上书店订单跑一遍从事件链到快照模型的完整建模过程3.1 场景和边界网上书店的最小交易闭环我拿一个虚构的模拟项目——某小型网上书店的正向销售链路来演示。业务范围先框定为顾客浏览商品、下单、支付、书店发货、顾客收货。暂不考虑采购、退货、赠品、积分抵扣这些可以在掌握正向链路之后再扩展。画 REA 图时最忌讳一上来就想把所有业务都建模。正确的做法是先划一条端到端的最小价值流把它走通再向两侧扩展。我们这个例子里最小闭环就是收钱—给货—钱货兑现。3.2 一步一步识别资源、事件、参与方第一步列事件。顺着业务时间线走一遍顾客下单这是个承诺暂时不作为经济事件单独建模顾客支付货款事件收款仓库拣货并发货事件销售发货顾客签收从 REA 角度看这个动作更多是物流确认核心事件已经完成第二步列资源。看每个事件改变了什么东西收款事件流入的资源是现金/银行存款发货事件流出的资源是库存商品图书这笔交易的应收账款也跟着动态变化第三步列参与方顾客外部参与方书店店员或收银员内部参与方库房管理员内部参与方第四步配对。把一进一出的事件用 Duality 配对发货图书流出↔ 收款现金流入。这样整条事件链就是顾客下单形成承诺→ 顾客付款现金流入应收账款消失→ 仓库发货库存商品流出→ 交易完成。用普通文字描述这段事件链很简单但把它画成建模结构时它其实是一个由事件—资源—参与方组成的关系网而不是一张订单表的几行状态。3.3 REA 快照任一时刻的资源和义务REA 还有一个很实用的概念叫快照Snapshot。简单说在任意一个时间点把每个资源的所有流入减去所有流出得到当时的资源存量再把所有尚待履行的承诺比如还没发货的订单汇总成义务。这样你就得到了一个不依赖会计科目的资产负债表式视图。拿书店来说要回答今天书店值多少钱不需要去翻财务系统从 REA 事件流就能算出来库存商品余额 采购入库总量 − 发货出库总量银行存款余额 收款总额 − 付款总额应收账款余额 已销售未收款金额应付账款余额 已收货未付款金额净资产 所有资源余额 − 所有义务余额。这就是快照的威力。科目表会随着会计准则变但流入减去流出这个算法永远不变。这是 REA 在很多账务审计、链上对账系统里被反复使用的根本原因。4. 从 REA 概念到数据表一套可落地的物理表设计4.1 用一组核心表把语义固定下来概念层和数据层之间需要一套稳定的映射。我实践下来最顺手的是一组事件导向的表核心表结构如下participant参与方表存内部和外部人员/组织。resource资源表定义图书、现金、应收账款等资源类型。event事件主表每行就是一个原子经济事件。event_resource事件与资源的关联表记录某个事件让资源流入或流出了多少。event_participant事件与参与方的关联表记录每个事件里各参与方的角色。commitment承诺表表达订单、合同这类未来要发生的事件并与后续实际事件挂接。在设计上我不是很建议把流入和流出拆成两张表比如 inflow 表和 outflow 表。拆表看起来清晰但在做汇总余额时会非常痛苦每个资源余额都要把两张表 full join 一遍。更好的做法是在 event_resource 里放一个 direction 字段1 表示流入-1 表示流出后续聚合就是一行 SUM。4.2 SQL 建表脚本与关键字段解释下面是简化版建表脚本去掉了一些审计字段保留核心字段CREATE TABLE participant ( id INT PRIMARY KEY, participant_no VARCHAR(32) NOT NULL, participant_type TINYINT NOT NULL, -- 1内部参与方2外部参与方 name VARCHAR(128) NOT NULL ); CREATE TABLE resource ( id INT PRIMARY KEY, resource_code VARCHAR(32) NOT NULL, resource_name VARCHAR(128) NOT NULL, is_asset TINYINT NOT NULL DEFAULT 1, -- 1正向资源0义务 UNIQUE KEY uk_resource_code (resource_code) ); CREATE TABLE event ( id BIGINT PRIMARY KEY, event_type VARCHAR(64) NOT NULL, -- 例如 sale, receipt, shipment event_no VARCHAR(32) NOT NULL, occurred_at DATETIME NOT NULL, description VARCHAR(255) ); CREATE TABLE event_resource ( event_id BIGINT NOT NULL, resource_id INT NOT NULL, direction TINYINT NOT NULL, -- 1流入-1流出 quantity DECIMAL(18,4) NOT NULL, unit_price DECIMAL(18,4) NULL, PRIMARY KEY (event_id, resource_id) ); CREATE TABLE event_participant ( event_id BIGINT NOT NULL, participant_id INT NOT NULL, role VARCHAR(32) NOT NULL, -- buyer, seller, warehouse_operator PRIMARY KEY (event_id, participant_id, role) ); CREATE TABLE commitment ( id BIGINT PRIMARY KEY, commitment_type VARCHAR(64) NOT NULL, -- order, contract resource_id INT NOT NULL, promise_quantity DECIMAL(18,4) NOT NULL, due_date DATE NULL, actual_event_id BIGINT NULL, -- 实际发生的事件 status TINYINT NOT NULL -- 0未完成1已兑现2已取消 );4.3 一笔交易如何落库销售与收款的两次写入模拟一笔销售顾客购买一本定价 45 元的书先收款后发货。第一步写入收款事件INSERT INTO event (event_type, event_no, occurred_at) VALUES (receipt, RCV-2025-0001, 2025-01-08 14:00:00); INSERT INTO event_resource (event_id, resource_id, direction, quantity, unit_price) VALUES (LAST_INSERT_ID(), 1, 1, 45.00, 45.00); -- resource_id1 表示现金 INSERT INTO event_participant (event_id, participant_id, role) VALUES (LAST_INSERT_ID(), 7, buyer); -- 顾客付款第二步写入发货事件INSERT INTO event (event_type, event_no, occurred_at) VALUES (shipment, SHP-2025-0001, 2025-01-08 15:30:00); INSERT INTO event_resource (event_id, resource_id, direction, quantity, unit_price) VALUES (LAST_INSERT_ID(), 2, -1, 1, 45.00); -- resource_id2 表示库存图书 INSERT INTO event_participant (event_id, participant_id, role) VALUES (LAST_INSERT_ID(), 9, operator); -- 仓库管理员操作注意这里的关键点不要在 resource 表上直接 UPDATE当前余额。余额应该由事件流汇总而来。这个思路和事件溯源Event Sourcing一脉相承好处是任何时间点的余额都能还原坏处是历史事件量大了之后查询会变慢后面我会讲工程上的折中方法。4.4 从明细事件还原任意时点的资源余额要算某个截止时间点的库存余额用下面这类聚合查询SELECT r.resource_code, r.resource_name, COALESCE(SUM(CASE WHEN er.direction 1 THEN er.quantity ELSE 0 END), 0) AS total_inflow, COALESCE(SUM(CASE WHEN er.direction -1 THEN er.quantity ELSE 0 END), 0) AS total_outflow, COALESCE(SUM(er.direction * er.quantity), 0) AS balance FROM resource r LEFT JOIN event_resource er ON er.resource_id r.id LEFT JOIN event e ON e.id er.event_id WHERE e.occurred_at 2025-01-31 23:59:59 GROUP BY r.resource_code, r.resource_name;在数据量上来之后可以建一个日终余额物化表每天凌晨把所有资源的期初余额加上当日流入流出落一张 resource_daily_balance 表。查询某天余额就直接读这张表追溯某天凌晨的具体流水再回到明细表。相当于用日汇总作为快照缓存既保留完整事件链又避免每次实时全表聚合。这套表结构在真正项目里运行之后最大的感受是历史口径变得非常好追溯。以前财务问这个数字哪来的你得从结果反查好几张表现在直接从事件链正着推下来谁都赖不掉。5. 当 REA 遇到复式记账语义层与记录层怎么配合5.1 一张对照表REA 不是来推翻财务的很多人一听 REA 就紧张觉得是要颠覆会计体系。其实不是。更准确地说REA 是语义层复式记账是记录与呈现层两者解决的是不同层面的问题。维度REA 本体建模传统复式记账建模中心经济事件会计凭证核心对象资源、事件、参与方科目、分录平衡机制资源流入流出配对Duality有借必有贷、借贷必相等回答的问题业务到底发生了什么、谁参与账上增减了多少钱报表产出可以由事件链推导任意视图直接产出三大报表对系统的要求需要完整的事件流记录只关心凭证级别的数据在实际工程里REA 更像一个中间层它把前端多变的业务状态和后端严格的财务科目解耦开。前端随便你怎么改页面只要事件没变财务口径就不受影响财务科目怎么调整也不需要动业务侧的事件定义。5.2 从 REA 事件链生成借贷分录同一笔销售REA 的事件和复式记账的分录是可以互相翻译的。上面那笔收款 45 元、发货一本成本 20 元的书在复式记账里对应的分录是借银行存款 45贷主营业务收入 45借主营业务成本 20贷库存商品 20在 REA 视角里第一组分录对应的语义是收款事件让现金资源流入第二组分录对应的语义是发货事件让库存资源流出。也就是说只要你在系统中维护了 REA 事件链就可以通过一组映射规则自动生成借贷分录。这正是很多财务中台项目的实际做法核心交易用 REA 记录真实业务期末通过一个会计引擎把事件翻译成总账凭证。5.3 实际项目里的双轨演进路线如果你的公司已经有一套运行多年的财务系统我不建议立刻把财务系统推倒重来。更稳妥的路线是先只对核心交易链路做 REA 建模比如收入、收款、发货、采购、付款。让 REA 事件流作为业务中台的统一出口各业务系统只负责产生事件。会计引擎消费事件流按映射规则生成传统借贷凭证写入原有财务系统。这样业务侧得到了一个稳定统一的事实层财务侧仍然使用它们熟悉的凭证和科目两边的改动量都最小。我们在这个模式下踩过的最大的坑是映射规则没有做版本管理——改了一条映射历史凭证也跟着变了。后来给所有映射规则加了生效版本和生效时间才彻底解决。6. 实施 REA 入过的坑关于识别、命名和上线节奏的提醒6.1 把 REA 当成普通的 ER 建模来用是最常见的误用很多团队觉得 REA 很简单不就是多建几张表吗于是照着订单表、用户表、库存表硬套。结果 resource 表变成了商品主数据表event 表变成了操作日志表整个建模失去了语义能力。要判断你的实现是不是真 REA可以看两件事第一事件表里每一行是不是一条不可再拆的真实经济事实第二是不是能仅凭事件流不经任何业务状态表算出资源余额。如果答案都是是那说明建模方向对了如果做不到那多半又退化成了普通的 CRUD 表设计。6.2 事件识别最容易出问题的三个点我总结了三个高频失误都出自真实项目里的教训。第一个是把审批当事件。比如订单审核通过它只是内部控制流程并没有导致任何资源流入或流出。把它记成事件会让事件流里混入大量无意义数据余额计算还得特判过滤。审批信息应该挂在参与方或控制关系上而不是作为独立经济事件。第二个是不顾原子性把多个动作合并成一个事件。比如收款并发货写成一条记录。表面省事实际上这两个动作可能发生在不同时间也可能一个成功一个失败。一旦拆开前一条已经入账后一条还没发货账务就对不上了。REA 建模的铁律是一个事件只做一件事。第三个是事件命名太随意。有人把事件写成销售订单完成出库单创建这种语义模糊的名字。我建议统一用动词业务对象后缀的结构比如销售发货收款到账采购入库看到名字就能知道资源怎么动、系统能不能自动映射。命名混乱在事件量小的时候没感觉一旦接入会计引擎每一条映射规则都在为命名买单。6.3 落地节奏和协作建议REA 建模最理想的上手方式是从一条真实存在、让团队痛苦最久的端到端流程开始而不是从一张全公司业务全景图开始。比如销售收款对不上就先把收入、收款、应收、发货这个闭环建模跑通跑通后再向采购侧、库存侧扩展。建模过程里一定要把业务、财务、开发拉到同一个会议里。业务提供流程事实财务校验价值守恒和专业判断开发负责把事件写入和余额计算做成通用机制三方缺一不可。我们第一次建模会议差点吵起来就是因为业务说付款就是订单完成财务坚持付款和发货是两件事后来是用 REA 事件链把流程拆开大家才终于对齐。最后再分享一个我个人的体会刚开始引入 REA 时不要追求一步到位更不要指望马上替换掉所有旧系统。先把一套真实的、让人头痛的交易链路用 REA 重新描述一遍你会立刻感受到业务发生了什么这件事终于有了一致的答案。有了这个正反馈后面再推广到全公司阻力会小很多。如果你正在被多套系统之间口径不一致、对账对不上折磨不妨从一个小闭环开始试试。