最近在重构一套老进销存系统的时候我在旧代码仓库里翻到一个文件夹名字就三个字母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我的建议是先别急着画图找一个最不起眼的小场景把动词列全再说。等你那份事件清单能开口讲话的时候模型的一半已经立住了后面不过是在给现实世界做投影而已。