
敲下rea这三个字母不同背景的人反应完全不一样前端工程师会想到 React 的早期缩写做学术检索的会想到某个期刊编号而在业务系统设计这个语境里它代表的是 Resource-Event-Agent一套从会计信息系统里长出来的领域建模框架。我最早接触它是在做一个进销存模拟项目的重构时被折腾得够呛传统订单表一路叠加字段最后连自己都看不懂那张十几列的表到底在表达什么。后来照着 REA 的思路重新梳理很多纠缠不清的关系一下就顺了。这篇内容不是教科书式的概念科普而是我实际把一套进销存系统用 REA 重新建模的记录。我会从三个核心词讲起再用一段完整的建表 SQL 演示怎么落地最后把我在实操中踩过的坑和排查思路全部摊开。如果你正在为业务系统改一个流程就要动一次表结构而头疼或者想给自己的领域模型找一个更稳的内核这篇文章应该能给你一些能直接抄作业的思路。1. 这个rea到底是什么凭什么解决建模焦虑1.1 三个字母背后的建模框架REA 全称 Resource-Event-Agent翻译过来就是资源-事件-代理。它最早出现在会计信息系统领域上世纪八十年代末研究会计信息化的一批学者发现传统财务软件把凭证账簿当成核心来建模但凭证只是业务活动的记录结果不是业务活动本身。如果系统只围着凭证转业务规则一变凭证格式一变整个数据结构就得跟着抖三抖。于是他们换了一个角度不再从单据出发而是从真实世界里发生了什么出发。一笔生意本质上就是一些资源货物、钱、服务在几个代理客户、销售员、仓库之间发生流动。资源、事件、代理这三个词就是 REA 的全部骨架。听起来简单但把它作为数据模型的内核之后系统的稳定性会明显好很多——因为真实世界的经济活动规律不会轻易变而单据格式却会经常改。1.2 传统ER建模为什么越改越乱大多数业务系统早期都喜欢这么建表一张订单表字段包括订单号、客户、商品、数量、单价、金额、状态、创建时间、销售人员、发货时间、收款时间……看起来一切正常直到业务方提需求现在客户可以先付定金发货后付尾款退款的时候还要分原路退回和余额退回。你开始往订单表里加字段定金金额、尾款金额、退款方式。过两天又来一个需求一个订单可能包含多个发货批次。你又开始拆子表。问题就出在这里订单表本质上是一张单据表单据是业务流程里的人为约定今天可以长这样明天可以长那样。你把模型建立在单据上相当于把地基打在了流沙上。而 REA 的思路是绕过单据直接对准业务里不可再分的经济事件。单据可以有几十种形态但货物发出和款项收到这两件事在任何商贸场景里都不会变。1.3 REA和复式记账抄了会计几百年的作业很多人第一次看到 REA 的三个概念会觉得这不就是会计里的资产、分录、往来单位吗确实REA 深度借鉴了复式记账的思想。复式记账最厉害的一点是每一笔经济业务至少涉及两个账户有借必有贷借贷必相等。它保证了账目的完整性和可追溯性——你永远可以顺着一条分录追到业务源头。REA 把这个思想搬进了业务系统建模每一个经济事件也必然涉及资源流入和资源流出两个方向。销售发货这个事件一方面让库存商品流出去另一方面让应收账款流进来或者直接让现金流入视结算方式而定。这种成对出现的资源流动在 REA 里叫双效性Duality它是整个框架最核心的约束。理解了这个再看后面的表设计就会明白为什么每张流水表都带着 in/out 方向字段。2. 三个核心概念拆开揉碎2.1 Resource能被交换、被使用、有价值的东西资源是 REA 里最好理解的一个词但它比直觉上的商品要宽得多。凡是企业里可以被交换、被消耗、被使用的有价值的东西都是资源。库存商品是资源现金是资源应收账款是资源甚至一项专利、一次咨询服务、一个月的云服务器带宽都可以建模成资源。在实际建模时我习惯给资源加一个类型字段比如 INVENTORY库存类、CASH现金类、RECEIVABLE应收类、SERVICE服务类。为什么这样分因为不同资源的计量方式不一样库存看数量、看单位现金看币种、看金额服务看时长、看次数。类型字段可以帮你在一张统一的资源表里兼容这些差异而不需要为每种资源单独建一张表。这里有一个重要认知资源描述的是可用的东西不是发生的行为。商品库存是资源而盘点库存这个动作不是资源它是事件。2.2 Event业务里真正发生的事而不是单据事件是 REA 的灵魂也是最容易被搞混的概念。判断一个东西是不是事件最简单的标准是它在时间轴上是否只发生一次且不可逆。销售发货是一次性的事件发生在某个具体时刻订单状态是可变的不是事件只是状态。事件是不可变的发生之后只能追加更正事件不能修改它本身。为什么要强调不可变因为事件是业务系统的审计基础。我在模拟项目里把事件表设计成了只追加模式没有 update 语句只有 insert。如果业务上确实录错了就新记一条更正事件把错误事件挂上作废标记。这个设计一开始看起来很麻烦但后面做对账、追溯、排查问题的时候简直是一把利器。单据可以改但真实世界发生过的事情不能改——这是 REA 的基本信念。2.3 Agent谁参与、谁负责、谁受益代理就是参与事件的人或组织既可以是企业内部的人也可以是外部的客户、供应商、物流方。传统订单表里只会有一个操作员字段顶多再加一个客户再多就不知道该往哪放。REA 的做法不同事件和代理是多对多的关系一个发货事件涉及销售员谁卖的、仓管员谁发的、客户谁收的、司机谁送的。你不要把这几个角色塞成事件表的四个外键而是用一张关联表让每个代理在事件中扮演一个具体的角色。这样做的好处是扩展性。比如后面要加一个审批人不需要改事件表只要在关联表里多插一条记录角色叫 APPROVER 就行。这个设计在处理多方参与的业务时特别省心也是我从传统字段堆叠转向 REA 的最直接原因之一。2.4 双效性一笔事件必然有成对的资源流动把资源、事件、代理单独拿出来只是一个静态的分类法真正让 REA 转起来的是双效性。每一笔经济事件至少会引起两笔资源流动一个方向是流入另一个方向是流出。拿销售发货举例库存商品流出应收账款流入。拿采购收货举例库存商品流入应付账款增加相当于负债流出的反向。拿收款举例现金流入应收账款流出。在表结构上我会用一张event_resource流水表来表达这种双效性同一个事件 ID下面挂两行流水一行 direction 是 OUT一行 direction 是 IN。这两行之间的价值金额必须相等就像复式记账里的借贷必相等。这个约束保证了业务数据的守恒不会出现货发了但钱没人管的断链情况。你可以在应用层写校验也可以直接在数据库事务里保证。2.5 存量与流量给业务加上时间轴REA 还有一个常被忽略但特别实用的视角存量Stock与流量Flow。资源表里记录的是某一个时间点的存量快照而事件资源流水记录的是流量变化。当前库存不是一个字段它是一个查询结果期初库存加上所有流入减去所有流出。这个视角天然契合财务和仓储的流水账思路。我后来把这种设计延伸到库存查询上发现一个好处你不需要维护实时库存字段只需要在每次事件发生时写入资源流水任何时候想要某个时点的库存按时间过滤求和就行。这在传统订单表方案里很难做到因为订单表的数量字段会被各种状态覆盖来覆盖去最后根本说不清某个时刻的真实库存是多少。REA 的流水模式把历史与当前统一到了一个模型里。3. 亲手把一个进销存系统改成REA建模3.1 业务场景与破局目标我在某团队做过的模拟项目X是一个小型商贸公司的进销存系统业务范围包括销售订单、发货、收款、采购入库、付款。旧系统就是典型的订单大表设计问题在第一节已经说过。重构时我的目标很明确核心业务数据全部基于 REA 建模不再允许出现订单状态付款状态这类不断加枚举值的字段。我给自己定了一个约束任何业务动作落到数据库里只有三类东西——新建一条事件、给事件配资源流水、给事件配代理参与关系。单据只是事件的查询视图不是存储主体。这样想了之后整个建表过程非常顺。3.2 一笔销售单背后藏着哪些REA要素拿一笔最常见的销售业务来拆客户下单仓库发货财务收款。先找资源货物本身是库存资源客户应付但还没付的钱是应收账款资源客户付款之后变成现金/银行存款资源。再找事件发货是一个事件收款是另一个事件。下单这个动作在 REA 严格分类里属于承诺Commitment可以单独建模也可以先并入发货事件看你对承诺追溯的需求强不强。我在模拟项目里为了控制复杂度把销售下单当作一个轻量事件主事件还是发货和收款。再找代理发货事件涉及销售员、仓库管理员、客户收款事件涉及财务、客户。这些参与关系统统放进事件与代理的关联表用角色区分。3.3 从REA模型到建表SQL下面是模拟项目X里经过简化的核心表结构SQL 尽量用通用写法实际使用时可以根据你的数据库微调-- 资源表一切有价值的东西 create table resource ( id bigint primary key, resource_no varchar(64) not null unique, name varchar(128) not null, resource_type varchar(32) not null, -- INVENTORY / CASH / RECEIVABLE / SERVICE unit varchar(16) -- 单位 ); -- 代理表参与业务的人或组织 create table agent ( id bigint primary key, agent_no varchar(64) not null unique, name varchar(128) not null, agent_type varchar(32) not null -- INTERNAL / EXTERNAL ); -- 经济事件表业务真实发生的事件 create table economic_event ( id bigint primary key, event_no varchar(64) not null unique, event_type varchar(32) not null, -- SALE_SHIP / CASH_RECEIPT / PURCHASE_RECV / CASH_PAY occurred_at timestamp not null, -- 业务发生时间 parent_event_id bigint null -- 可选关联前序事件用于追溯 ); -- 事件-资源流水表表达双效性 create table event_resource ( id bigint primary key, event_id bigint not null references economic_event(id), resource_id bigint not null references resource(id), flow_direction varchar(8) not null, -- IN / OUT quantity decimal(18, 2) not null, unit_value decimal(18, 2) not null, constraint uq_event_resource unique (event_id, resource_id, flow_direction) ); -- 事件-代理关联表表达谁参与了事件、扮演什么角色 create table event_agent ( id bigint primary key, event_id bigint not null references economic_event(id), agent_id bigint not null references agent(id), role varchar(32) not null, -- SALES / WAREHOUSE / CUSTOMER / FINANCE constraint uq_event_agent unique (event_id, agent_id, role) );这几张表加起来就是 REA 建模的完整落地。你可能会问商品名称、客户名称、联系方式这些业务属性放哪答案是放在资源表和代理表的扩展信息表里或者直接作为属性字段加在这两张主表上但不要破坏事件只记录行为的边界。3.4 跑通一笔完整业务的查询与会算表建好了现在把一笔销售业务完整过一遍。第一步发货事件。假设卖给客户 A 一批货商品 X 有 10 件每件 100 元-- 创建发货事件 insert into economic_event (id, event_no, event_type, occurred_at) values (1, EVT-20250101-001, SALE_SHIP, now()); -- 发货事件引起两笔资源流动库存流出 应收账款流入 insert into event_resource (id, event_id, resource_id, flow_direction, quantity, unit_value) values (1, 1, 101, OUT, 10, 100); insert into event_resource (id, event_id, resource_id, flow_direction, quantity, unit_value) values (2, 1, 201, IN, 1000, 1); -- 应收账款 1000 -- 发货事件关联代理 insert into event_agent (id, event_id, agent_id, role) values (1, 1, 501, SALES), (2, 1, 502, WAREHOUSE), (3, 1, 601, CUSTOMER);第二步收款事件。客户实际付了 1000 元insert into economic_event (id, event_no, event_type, occurred_at) values (2, EVT-20250105-002, CASH_RECEIPT, now()); insert into event_resource (id, event_id, resource_id, flow_direction, quantity, unit_value) values (3, 2, 301, IN, 1000, 1), -- 现金流入 1000 (4, 2, 201, OUT, 1000, 1); -- 应收账款减少 1000 insert into event_agent (id, event_id, agent_id, role) values (4, 2, 503, FINANCE), (5, 2, 601, CUSTOMER);现在想查当前还欠多少钱只需要对应收账款资源做 SUM(IN) - SUM(OUT)select coalesce(sum(case when flow_direction IN then quantity else -quantity end), 0) as receivable_balance from event_resource where resource_id 201;结果为零说明这笔订单已经结清。如果客户只付了 500查出来的余额就是 500账目一目了然。这种查询方式不需要任何状态字段完全靠资源流水推导非常干净。3.5 给事件补上不可变和审计约束REA 的落地不只是建表还要在应用层守住几条约束否则模型会被用坏。第一事件表只允许插入和标记作废不允许物理删除和修改。作废用单独字段void_reason或关联一条更正事件。第二同一个事件 ID 下IN 方向的总价值必须等于 OUT 方向的总价值这个校验在写入事件资源时用数据库事务保证应用层也做一道防线。第三事件编号必须有唯一约束防止重复入账。第四occurred_at记录的是业务发生时间不是系统插入时间很多对账问题都出在时间语义不清上。我在实际操作中还加了一个扩展表用来记录事件附带的单据信息比如发票号、物流单号。这样既保住了事件表的纯粹性又不丢失单据层面的业务细节。4. 常见问题、避坑经验与适用边界4.1 事件和资源永远分不清三个判断标准这是 REA 实操里最常见的问题我在给团队讲这个模型时几乎每个人都会先经历一段看什么都像资源的时期。我的判断标准有三个能不能被持有、是不是一次性的、改了之后是修正还是追溯。资源可以被持有和转移事件只能发生一次资源的状态会变化事件在时间轴上不可逆你可以修改库存数量但你不能修改一次发货行为只能再发一条冲销事件。举个例子运费是资源还是事件如果你们把运费当成商品一样计价、收取、支付那就当成资源如果它只是发货事件附带的一个费用信息那就作为事件资源表里的一条 IN 流水即可不用单独立资源。没有绝对正确的答案关键是全团队用同一套标准保持一致。4.2 一个事件对应多个代理怎么办在传统表设计里一个操作员字段解决不了多方参与的问题你可能会想在事件表里加sales_id、warehouse_id、customer_id好几个外键。短时间没问题但一旦出现新的角色又要改表加字段。REA 的标准做法是用event_agent关联表角色变成一个可枚举的字符串而不是固定列。实际项目中还有一种情况同一个代理在事件里扮演两个角色比如某个员工既负责销售又负责审核。这时关联表里就会出现同一 agent 的两行记录role 分别为 SALES 和 APPROVER。这正好说明多对多关联表的表达力比固定字段强得多你永远不需要为角色组合调整表结构。4.3 REA不是万能药这些场景我劝你别硬上虽然 REA 很好用它也有明显的适用范围。如果业务本质不是有价值资源的双向交换REA 会显得很绕。比如纯内容管理系统、任务协作工具、简单的信息登记系统这些场景没有清晰的经济事件硬要套 REA 只会增加理解成本和开发成本。另外如果系统已经有非常成熟稳定的单据流而且业务方明确几十年都不打算改流程那传统 ER 建模加状态机也够用。REA 解决的是变化频繁、需要强追溯、数据守恒要求高的问题它所带来的通用性是有代价的查询需要 join 更多表开发人员需要重新理解模型。任何一个框架都有边界想清楚再用比盲目追新重要得多。4.4 和DDD、微服务怎么和平共处有人会问REA 是一种数据库设计方法和领域驱动设计DDD冲突吗我的经验是不冲突甚至可以互补。DDD 强调用限界上下文划分领域边界REA 正好可以作为核心域里的一层稳定内核。比如订单上下文、库存上下文、财务上下文之间可以都用 REA 风格的事件表然后通过事件总线同步而不是直接共享数据库表。我在微服务项目里通常这么做每个服务内部保留自己的 REA 事件表对外通过领域事件发布发货事件收款事件下游服务只读自己关心的资源流水。这样既保证了核心事件的一致性又把 REA 的实时存量查询能力限制在服务内部避免跨服务 join 导致耦合。4.5 我的几条独家实操心得最后分享几条不写进任何文档的经验每一条都是真金白银换来的第一不要把资源表的unit_value字段当成最终交易价格的唯一来源。折扣、促销、阶梯价这些业务规则应该放在事件扩展表里或者由独立的报价服务计算否则资源流水里的单价会变得很尴尬。第二事件表一定加parent_event_id。比如发货是由订单触发的退款是对收款的部分冲销不记录前序关系时间久了根本无法回答这笔钱是哪笔订单带来的这种问题。第三前面说的库存查询可以用流水 SUM 推导这个方案在小数据量下很舒服但数据量大了之后一定要给event_resource加时间分区索引不然月底对账时一条大查询能把数据库压垮。第四团队里每个人接受 REA 的节奏不一样不要直接扔文档最好的方式是拿一条真业务在白板上圈出资源、事件、代理大家一起圈一次比读十篇博客都管用。我在重构那个模拟项目X之后最直观的感受是业务方再来提需求我不再害怕又要加字段改状态了。新需求来了先问一句这是新资源、新事件还是新代理角色想清楚这个建模就跟搭积木一样每次只在局部加一块而不是把整栋房子推倒重来。REA 不是银弹但如果你想找一个能扛住业务变化的建模内核它值得你花一个下午好好试一试。