
前阵子给某公司做订单系统重构项目代号就叫 REA。三字母不是随手敲的拆开是 Resource-Event-Agent一套上世纪八十年代就提出的企业业务建模方法。我最初以为同事要搞什么复古风等真把这个模型落到数据库和代码里才发现它把业务真相和财务记账之间的鸿沟填得干干净净。这篇就从一个实际项目角度聊聊 REA 是什么、为什么它会让人用了就回不去、以及在真实系统里怎么落地。适合谁看如果你正在设计订单、库存、账务这类偏业务核心的系统或者被订单状态一堆、财务对不上、报表全靠凑的问题折磨过这篇文章应该能给你一套不一样的建模思路。1. REA 到底是什么从一个困惑说起1.1 代号的真实含义项目启动会那天产品经理甩出一张巨复杂的订单状态图什么待支付、已支付、待发货、已发货、已完成、已取消旁边还有一条退回分支整个图密密麻麻看得人头皮发麻。我说咱们能不能先把状态机放一边好好想想业务里到底发生了哪些事实。技术负责人点点头在白板上写了三个词Resource、Event、Agent。这就是 REA 的核心用资源、事件、主体三个原始概念去描述业务而不是用一张张订单一类类状态去堆模型。很多人第一次听 REA会以为它是某种新的数据库或者中间件。其实它是一种领域建模框架最早由学者在 1982 年提出最初是为了解决会计信息系统和企业共享数据环境之间的建模问题。后来这套框架被逐步推广到整个企业业务流程建模尤其是订单、库存、采购、销售这类经济交换场景。为什么会计领域的人会盯上建模因为传统会计系统天生是事后记录记账凭证、总账、明细账记录的是经济活动的结果而不是经济活动本身。REA 反其道而行之它先记录业务上真实发生的经济事件比如商品出库、客户付款再通过这些事件推导出账务数据。这个视角的切换正是它在今天依然有价值的原因。1.2 学术出身与工程价值REA 名字里的三个词任何一个单独拎出来都不稀奇但组合起来的威力在于它们之间的关系约束。这套模型的根基在于经济学中的交换行为一笔交易本质上是一方让渡资源、另一方获得资源而且这个交换必须是对偶的。你不可能只收钱不给货也不可能只发货不收钱哪怕先货后款在业务时间轴上最终也得闭合。REA 把这些朴素的商业逻辑显式地建模出来而不是藏在一堆状态字段里。在我的项目里REA 从一开始就被定位成业务事实层的建模工具而不是最终的数据存储范式。也就是说我们用 REA 来回答业务系统里应该记录什么、这些记录之间有什么关系至于底层用不用事件溯源、用不用物化视图那是另一个层面的决策。这个分层思路很关键我在后面的实操章节会展开讲。2. 传统建模方式的问题为什么订单财务系统总在返工2.1 发票中心的记账幻觉大部分订单系统是怎么建的围绕订单这个聚合根订单头、订单行、支付记录、发货记录然后造一个订单状态字段跟着流程流转。这套路很常见但埋了不少雷。第一个雷是凭证导向。很多系统把发票当成为最核心的业务凭证逻辑都围着发票转开票了才算交易成立冲红了才算交易取消。可发票本质上是税务与结算的中间载体不是业务发生的唯一事实。业务里明明货已经发了、钱已经收了只因为发票流程卡住系统状态就停在半山腰财务和业务永远在打架。REA 的做法是把发票降级为可选单据核心事实是经济事件本身。客户付了钱这是一个事件仓库发了货这是另一个事件两个事件发生在哪个时间点、先后顺序如何才是业务真相。发票只是从这些事件里派生出来的一张纸而已。2.2 状态字段堆出来的伪流程第二个雷是状态机失控。订单系统做久了订单状态字段能膨胀到十几个甚至几十个每个状态还有各种细分条件。我见过一张表里同时存在 PAY_STATUS、SHIP_STATUS、REFUND_STATUS 三个状态字段彼此还能互相矛盾。数据里一条订单显示已支付同时又显示退货中报表统计时只能靠一堆时空错乱的 SQL 硬掰。这种问题的根源在于状态是多个事实的聚合快照。你往状态里塞的信息越多状态之间的冲突概率越大。REA 不存当前状态它只存一串不可变的事件支付了、发货了、退货了、退款了。任何一个时刻你把这些事件按时间重放一遍当前状态自然就出来了。状态从存储对象降级为计算函数矛盾自然消失。2.3 账务与业务的两层皮第三个雷是业务库和财务库两张皮。订单系统一套数据财务系统另一套数据靠接口同步对不平了就人工调。某公司之前每个月月底都要花两天对账对不上就抽单子查效率极低。REA 之所以能解决这个问题是因为资源这个抽象同时涵盖了货物、资金、额度这些经济资源。系统每记录一个经济事件都会同时影响某种资源的流入或流出。货物的减少必然对应着客户应收账款或现金的增加这个对偶关系被建模约束住了财务数据不是后补的而是由业务事件推导出来的天然对得上。注意REA 并不是要让业务系统强行走借贷分录。它描述经济事实财务科目和借贷关系是派生视图。这个分离让业务团队和财务团队各取所需不再共用一张被两边改坏的宽表。3. 核心概念拆解资源、事件与主体3.1 Resource业务里真正流转的东西Resource 是经济资源业务里一切有价值、可以被交换、被消耗、被创造的东西。实体商品、数字服务、现金、应收款项、积分、运力甚至某些场景下的信息使用权都能建模成资源。识别资源有一个很实用的判断标准它是否参与流入或流出。仓库里的货会减少也会补货这是资源客户的钱会进来也会退款出去这是资源。反过来订单号、发票号这类纯粹用于标识的字符串不参与价值交换就不是资源。资源在数据库里通常表现为一张 ResourceType 字典加若干资源实例表。我的建议是在早期建模阶段先用逻辑层面的资源池来思考不要急着拆表。先搞清楚业务里有哪几种资源在流动再决定每类资源是一张独立表还是共用一张带类型的表。3.2 Event不可忽视的经济事实Event 是经济事件表示资源的流入接收或流出让渡。这是 REA 里最核心、也是最容易用错的词。我见过有人把一次用户点击也算成事件那纯粹是理解偏了。REA 里的 Event 必须满足一个条件它实际改变了一种经济资源的数量或归属。举个例子用户下单这件事在 REA 里不算严格意义上的 Event因为它只是表达了购买意愿商品还没有真正被转移。真正的事件是发货货从仓库资源池里流出是收款钱从客户的资源池流入企业。订单本身被建模成一组对偶事件的承诺而不是一个状态快照。Event 在设计上应该不可变immutable。一旦记录不允许修改或删除只能通过一个冲抵事件来撤销原事件。这项约束是 REA 与事件溯源结合的基础也是保证审计链条完整的关键。3.3 Agent谁在参与交换Agent 是参与经济事件的主体可以是人也可以是组织。内部主体比如销售员、仓库、公司本身外部主体比如客户、供应商、物流承运商。设置 Agent 的主要作用是明确权责归属每个事件是谁发起的、谁执行的、受益者是谁。在合规和审计场景这个信息比金额本身更重要。实际建模时Agent 通常只是一张主体表加事件参与关系表但别小看它多主体建模能力在对接供应链金融、多法人结算时是救命的。3.4 三类关系的业务含义REA 的真正精华不在三个基本概念而在它们之间的三种关系。存量-流量关系Stock-Flow事件与资源之间的关系。一个经济事件要么增加资源、要么减少资源。比如发货事件减少库存商品资源收款事件增加银行存款资源。关系上的字段就是数量、单价、金额这些计量属性。对偶关系Duality两个事件之间成对出现的关系。一笔完整的交易一定包含资源流出和资源流入两个对偶事件比如出货对应收款。这个关系是 REA 模型的约束核心也是它与传统借贷记账法最神似的地方。参与关系Participation事件与主体之间的关系。一个事件至少涉及两个主体一个内部主体、一个外部主体。比如销售发货事件外部主体是客户、内部主体是仓库双方都在事件上都留下记录。这三个关系说出来都不复杂但一旦把它们作为系统的骨架约束业务模型会变得异常牢固。每一个业务数据都能在上述六种节点三类概念加三类关系里找到自己的位置模棱两可的状态字段自然没了生存空间。4. 实操用一个电商订单系统把 REA 走通4.1 场景与建模范围我拿项目里一个典型的电商子场景举例客户在商城下单购买实体商品系统需要处理支付、发货、退货、退款还要求财务能按日清晰对账。建模前先约定边界本次只做订单交易域不碰采购入库和调拨。范围越小越容易走出模型等跑通后再往外扩。4.2 识别资源、事件、主体的四步法第一步列资源。这个场景下经济资源有三类库存商品、货币资金包括应收、可退换货权益。其中货币资金我建议拆成账户余额和银行账户两种资源因为自有资金和客户预付款在权属上完全不同。第二步列事件。我们按业务事实的时间线捋客户付款资金流入仓库发货商品流出客户签收商品所有权转移客户申请退货资源流出的反冲仓库验收退回商品商品流入财务退款资金流出这里注意下单和订单确认不是 Event它们只是交易的准备阶段。商品并未实际流动所以它们更适合建模为承诺或合同上下文。第三步列主体。外部主体是客户内部主体是商城运营主体还可能有一个虚拟的仓库部门作为执行主体。第四步对偶配平。发货商品流出必须能对上一个付款资金流入退货商品流入必须对上一个退款资金流出资金和货物在同一笔业务循环中双向流动构成完整闭环。4.3 数据库设计与代码映射逻辑模型定了物理落地反而简单。我们当时用了 MySQL采用一张核心事件表加资源流水表的方案CREATE TABLE economic_event ( event_id BIGINT PRIMARY KEY, event_type VARCHAR(64) NOT NULL, -- DELIVER, PAYMENT, RETURN, REFUND resource_type VARCHAR(64) NOT NULL, -- PRODUCT, CASH resource_qty DECIMAL(20,4) NOT NULL, -- 正数为流入负数为流出 counterparty_agent_id BIGINT NOT NULL, -- 外部主体 internal_agent_id BIGINT NOT NULL, -- 内部主体 related_event_id BIGINT NULL, -- 对偶事件ID验证 Duality occurred_at DATETIME(6) NOT NULL ); CREATE INDEX idx_event_resource_time ON economic_event(resource_type, occurred_at);代码层面我们给事件加了一层封装public class EconomicEvent { private String eventType; private ResourceAmount amount; private Agent counterparty; private Agent internal; private ListEconomicEventLink links; // 对偶关联 public void applyTo(ResourceLedger ledger) { ledger.post(this); } }写到这里我多说一句物理表结构可以简化但应用层必须守住两条铁律一是事件一旦提交不允许修改二是新增事件必须成对或允许缺省对偶但要有明确待配平标识。这两条是审计链的生命线。4.4 与传统订单模型的对比同样一个下单-付款-发货场景传统模型往往会画出三张主表加一张状态表字段 40 个起步。REA 模型只有一张事件表、一张资源流水表、两张主体表外加一个订单元数据表存下单快照和承诺关系。拿一个日常问题检验业务问今天到底发了多少货、收到了多少钱传统模型要在订单表里筛 ADD_TIME再去支付表和物流表里各自拉数据最后还要担心三个时间字段的时区不一致。REA 模型只需要查 economic_event 表里当天的 DELIVER 和 PAYMENT 事件sum 一下资源变动量即可。逻辑简单到让人怀疑是不是太简单但事实就是因为数据模型忠实反映了业务事实查询和统计都不再需要各种怪异的拼装。5. REA 与事件溯源、审计扩展的联动5.1 事件溯源天然契合 REA既然 REA 把一切业务事实都建模成不可变事件那它和事件溯源Event Sourcing简直是一对天生的搭档。事件溯源的核心思想是不以当前状态为存储真相而以事件日志为唯一真相状态只是事件的投影Projection。这两者的契合点在于REA 定义了事件应该长什么样事件溯源定义了事件应该怎么存。一个负责业务语义一个负责技术持久化各司其职。我们当时的落地方式是economic_event 表就是事件流Kafka 里同步发一套事件用于跨域通知ESElasticsearch里建立订单查询视图。业务查询都走投影视图审计对账走事件表两边各自优化。这样审计数据不可变、可追溯查询数据又保持了性能。5.2 扩展库存、税务与多主体场景REA 模型的一大优势是扩展性强。库存模块不需要单独建一套库存流水表库存资源本来就由入库事件和出库事件推导。税务上每笔发货事件带上税率字段销项税在月末汇总事件即可得出不用额外维护税务中间表。多主体场景更是 REA 的主场。某公司有几个法人实体共用一个商城系统时传统模型往往要把每个主体各做一套订单表然后靠大量配置字段区分。REA 把 Agent 作为事件的一个维度所有事件天然知道是哪个法人实体的资源在变动。月末合并报表时按 Agent 分组就能拆出各法人实体的经营数据不需要数据复制和迁移。5.3 闭环审计从凭证到事实的回归审计的时候最怕什么怕数据链路断。传统系统里业务单据和财务凭证分开存储审计人员要在两套系统里来回比对。REA 模型下业务事件就是原始凭证的数字化资源流水天然形成账本对偶关系保证每一笔资金流出都有对应的货物流入。审计需要的不是凭证而是可以从资源流水一路追踪到原始事件的能力。我在项目里给每条资源流水加了 event_id 溯源字段用户在前端看到的任何一条余额变动或者库存变动都能一路点回最初的经济事件。这功能做出来之后财务和客服再也没来找过程序员对过账。6. 实操中踩过的坑与排查心得6.1 最常犯的三个错误第一个错误把 REA 当 ERP 用。REA 是建模框架不是现成软件。有人以为建了 Resource 和 Event 两张表就算 REA 了忽略了关系约束结果就是表结构确实变了业务逻辑还是老的对账照样对不平。没有对偶约束和存量-流量约束就不能叫 REA。第二个错误事件的粒度把握不准。事件太粗比如把支付和退款混成一个事件会丢失业务细节事件太细比如把每个订单行都拆成一个独立事件又会让对偶关系爆炸。折中的做法是按业务上不可再分的动作来定义事件一个动作一个事件。第三个错误把事件设计成可变。一开始图方便退款时直接 UPDATE 原支付事件的金额结果审计时发现历史数据全乱套了。后来改成新增冲抵事件用负数金额表示反方向流动链路立刻清晰。这是我用真金白银换来的教训。6.2 判断建模是否正确的三个问题每次建模完成后我会拿三个问题自检每一笔资源的流入流出都能找到对应的经济事件吗如果存在凭空出现的库存或莫名消失的余额说明有事件没建模。每一个经济事件都能配对到对偶事件吗配不上的要么是业务流程有漏洞要么是事件识别不全。从任意资源流水出发能一路追踪到参与的主体吗追不到说明参与关系没建完整。这三个问题全部回答是模型基本就立住了。6.3 落地时的妥协与取舍REA 也不是银弹。它建模能力很强但对团队要求不低事件溯源带来的开发期多写一份投影代码成本也需要明确接受。我的建议是核心交易域用 REA周边非核心域不要硬套。比如商品信息维护、商家后台配置这类简单的 CRUD用传统模型更合适硬套 REA 只会给自己找麻烦。另外不要把状态字段彻底消灭。查询视图里仍然可以保留订单当前状态这个派生字段只是它的写入逻辑必须由事件投影生成而不是业务代码乱改。REA 是重建状态的唯一合法来源状态字段只是它的缓存。写在最后的经验我个人实际操作下来的体会是REA 最大的价值不是它有多炫酷而是它逼着你在建模之前老老实实地去理解业务到底在交换什么。传统建模方式往往会让你下意识地按界面和单据去画表结果业务一变表也跟着推倒重来。REA 把问题收敛到了资源和事件这个更稳定的层面业务换了个交互方式模型纹丝不动。如果你正好要设计订单、账务、库存这类系统不妨先从一个小场景入手练一次 REA 建模。不要一上来就想着全公司铺开先拿一个订单域走通资源、事件、主体、对偶这条链路感受一下状态字段消失之后查询和审计变得多清爽再决定要不要推广到其他领域。建模这件事慢就是快想清楚了再动手后面返工的债就会少很多。