1. 从“rea”三个字母说起为什么这个缩写值得细究只抛三个字母“rea”出来不同领域的人大概率会想到完全不同的东西做外贸的会联想到某个港口代码做教育的会想到开放教育资源做英语翻译的会下意识把它当成“read”或“real”的截断写法。但在企业应用系统这个圈子里我最先想到的永远是另一个实体REAResource-Event-Agent资源-事件-代理模型。这是我在做进销存、财务核算、业务中台项目时反复用到的一套建模思路也是一旦用顺了就不太想退回传统“科目表流水表”的思维框架。REA最早出现在会计信息系统研究领域最初解决的是“钱与货怎么在系统里被真实表达”的问题。传统的复式记账让我们知道库存商品增加了、银行存款减少了却说不清“是谁在什么时间通过什么业务动作让这两个账户同时发生变化”。REA把这一切还原为三个要素业务中流的资源业务中发生的事件以及参与事件的代理。任何一笔经济业务都可以用“代理在某个事件中改变资源的存量或所有权”来描述。这篇文章适合三类读者一类是做企业核心系统设计的技术人员尤其是负责库存、订单、资金模块的人一类是准备用领域驱动设计或事件溯源重构旧系统的架构师REA和这两套方法论天然合拍还有一类是财务后端研发或数据分析工程师想知道业务事件应该如何结构化落地才能让后续的审计、统计、分析不再靠拼接SQL找补。我会从概念拆解开始逐步带到一个真实的“零售进销存”建模案例再分享我在实际项目中踩过坑之后沉淀下来的排查清单和建模纪律。文中的表和SQL结构可以直接拿去复刻但请一定先理解每张表存在的理由再考虑照抄代码的问题。2. 核心概念资源、事件、代理与三类关系2.1 三个实体画板经济资源、经济事件、经济代理先看三个对象在REA里的精确定义。经济资源Economic Resource不是数据库里字段意义上的“资源”而是业务上有价值、可以被消耗或获取的东西。库存里的手机、账上的现金、客户欠下的应收账款、固定资产、甚至知识产权都可以是资源。资源的特征是“能带来经济利益”通常可以量化、可以计价参与事件中的流入或流出。经济事件Economic Event是业务发生的那一瞬间。采购入库、销售出库、收到货款、支付供应商货款这些都是事件。事件有两个重要的性质第一事件不可凭空存在它必须涉及至少一个资源的变化第二事件必须发生在某个时间点并且是已经发生的事实而不是计划或预测。经济代理Economic Agent是参与事件的人或组织。客户、供应商、员工、部门、仓库管理员、快递公司都是代理。代理可以内部也可以外部但在REA模型里一个事件同一个代理通常绑定一种“职责”比如销售员负责开单、仓管员负责发货、客户负责付钱职责本身也是一条独立信息。打个日常生活的比方。你去便利店买一瓶水付钱拿走。这瓶水是资源你的扫码付款是资源销售发生的那一刻是事件你和店主是代理。REA不关心流水号先写还是后写它关心的是谁、在什么时间、通过哪个动作、让什么资源发生了增加或减少、又让什么资源发生了相反的变化。2.2 三条关键关系线存量-流量、触发、职责三个实体两两之间构成三种关系这也是REA模型最容易让人混乱的地方。第一是存量-流量关系Stock-Flow连接经济事件和经济资源。库存、现金这类资源的余额是“存量”事件对资源数量造成的影响叫“流量”。销售事件让库存商品这个资源减少这就是一条“流出”存量-流量关系收款事件让现金资源增加这就是一条“流入”存量-流量关系。文件名或表单里常见的“出货单”“入库单”“付款单”本质都是在描述若干条存量-流量关系。第二是触发关系Trigger连接两个经济事件。典型场景是“销售订单”这个事件触发了“销售发运”事件“销售发运”又触发了“销售收款”事件。触发关系表达的是业务前后顺序和因果逻辑它不是数据库外键而是一种业务编排关系。第三是职责关系Custody/Participation连接经济事件和经济代理。客户购买、仓管员发货、财务收款、供应商交货这些都是“参与”关系。这里特别要区分“内部代理”和“外部代理”因为很多报表关心的恰恰是内部责任归属比如哪些损耗要算在哪个仓管员头上。三种关系用一张表格可以概括得更清楚关系类型连接实体一句话解释基数示例存量-流量事件 - 资源事件增减了资源一个销售事件减少多种商品Trigger事件 - 事件一事件引起后一事件一个收货事件触发多个付款事件职责/参与事件 - 代理代理参与了事件一个销售事件有客户、销售员、仓管员三个人参与2.3 一个完整的REA业务片段怎么读用一个最简单的“销售收款”场景把上述概念串起来读一遍。资源“库存商品”和“银行存款”是两种经济资源。事件“销售出库”和“收到货款”是两个经济事件。代理“客户”“销售员”“财务人员”是三个代理。关系连接起来就是这样客户代理参与销售出库事件职责是“买方”销售员代理也参与销售出库事件职责是“经办人”销售出库事件让库存商品资源减少存量-流量流入流出销售出库事件触发收到货款事件Trigger关系财务人员参与收到货款事件职责是“收款人”收到货款事件让银行存款资源增加存量-流量关系流入。这个片段里没有任何一张“会计凭证”出现但业务的全貌已经被完整记录下来了。它比传统流水表多出来的地方是语义系统不再只知道“库存-1、银行1”还知道这两条数量变化是因为谁和谁在什么环节发生了交易。3. REA与传统复式记账不是替代而是语义补全3.1 传统科目表在表达上的短板传统复式记账系统里核心资产是一张科目表会计科目被人为分成资产、负债、权益、成本、损益五类然后每一笔业务都写成借贷分录。对应到软件里最终落地的往往是“凭证表 分录明细表”借贷平衡靠借贷金额之和相等来保证。这种模型在财务核算上非常成熟却有两个明显的短板。第一它把“业务事实”压缩成了“账户增减”原始的业务上下文需要从凭证摘要或备注字段里猜。第二它很难回答“这笔销售对应哪次进货”“哪个业务员贡献了多少毛利”这类跨模块问题因为科目表的视角是账户不是资源、事件和代理。我在实际项目里见过太多这种“科目表僵化”的情况库存模块想要追踪某个批次的移动轨迹财务模块却只看到两级科目下的库存余额。两边数据对着不上最后只能加一堆冗余中间表把业务对象和财务科目硬绑起来。整个过程既脆弱又难维护。3.2 REA如何补全审计与分析的叙事REA把复式记账里隐藏的“谁在做什么、资源从哪里流向哪里”显式化。实现之后审计人员不需要对着凭证摘要猜业务链路系统里本身就存在一条从“采购事件 - 付款事件 - 库存资源增加”的完整链条。分析场景也轻松很多。比如想分析“某仓库某类商品每月的资金占用成本”传统做法是从财务库存科目取余额再按商品分类去库存表匹配费时费力。REA模型直接从“资源事件流量代理职责”查询就可以得到“哪些商品在哪个期间内进出、经手人是谁”的数据。因为模型本身就保存了这些语义关系查询只需按图索引不需再做关联猜测。需要说明的是REA并不要求推翻复式记账。实际项目中最稳妥的做法是“双轨并行”业务系统使用REA来表达真实业务事件和资源变动日终或实时通过派生规则生成会计凭证满足财务核算要求。这样既保留了财务合规性也获得了业务语义层的可扩展性。3.3 和领域驱动设计、事件溯源的关系现在很多团队做微服务拆分或系统重构时会谈到事件溯源Event Sourcing和领域驱动设计DDD。如果细看REA和这两套思想有很强的互补性。事件溯源把“状态变更”本身当作事实记录下来系统状态由事件序列回放得到。REA里的“经济事件”恰好就是这种事实记录销售出库是事件收款是事件。事件溯源要求事件是不可变的、有明确时间和业务含义的这跟REA对事件的定义几乎一致。DDD里讲聚合、实体、值对象REA可以帮你在识别聚合边界时提供方向哪些资源、事件、代理应该落在同一个聚合内哪些应该分离。例如订单聚合通常包含订单事件、客户代理、商品资源信息但付款聚合通常独立存在只通过Trigger关系连接因为资金生命周期和订单生命周期并不完全一致。这样一来微服务的服务划分就有了非常自然的边界依据。4. 实操用REA为零售进销存系统建模4.1 确定业务范围与关键对象我拿一个比较典型的“零售进销存”案例做演示。业务场景是一家小型零售企业有采购进货、销售出货、库存管理和供应商/客户结算功能。目标是设计一个既能支撑前端业务又能为财务核算和后续报表分析提供结构化数据的核心数据模型。首先圈定业务范围不包含生产加工不包含复杂的多仓调拨只关注“从供应商买进来、卖给客户、收付款、看库存余额”这四条主线。边界越清晰REA建模越不容易被无关概念干扰。第二步是列出业务中出现的所有候选对象先不决策它们属于哪一类只做头脑风暴式收集。候选名单大致是商品、供应商、采购订单、采购入库单、应付账款、客户、销售订单、销售出库单、应收账款、收到的钱、付出去的钱、业务员、仓库管理员、财务审核人。这些候选对象在REA里会被归类到资源、事件、代理三种类型中。4.2 设计资源、事件、代理三张主表现在把候选对象归类到三类表中。商品属于经济资源因为它具有价值且会被减少或增加。现金和银行存款属于经济资源因为收款和付款会改变它们。应收和应付也可以归为资源它们在未来会变成现金流入或流出。采购订单、采购入库、销售订单、销售出库、收款、付款、退货、退款这些全部是经济事件。事件表需要区分“计划事件”和“实际事件”这是我在项目里吃过亏才意识到的一点采购订单是计划采购入库才是实际发生销售订单是计划销售出库才是实际发生。两类事件跑在不同的生命周期流程中不能混在同一个业务状态机上处理。客户、供应商、业务员、仓管员、财务人员这些全部是经济代理。代理表不需要区分“我方员工”和“外部伙伴”用一个“代理类型”字段标出来就好了因为分析时既可能按员工分组也可能按客户或供应商分组。4.3 建立存量-流量关系与触发关系主体表设计完之后真正的建模重点在关系表。存量-流量关系表要记录“哪个事件增减了哪个资源”以及“增减了多少”。这张表必须支持“一个事件涉及多个资源”比如一张销售出库单可以销售多款商品所以事件和资源是多对多关系需要一张独立的关联表。触发关系表要记录事件之间的因果。采购入库事件触发应付事件销售出库事件触发收款事件销售退货事件触发退款事件。这些触发关系就是后续做状态机推演和流程仿真的基础。职责关系表要记录“哪些代理参与了哪个事件、各自扮演什么角色”。一张销售出库单涉及客户、销售员、仓库员三个角色。这张表往往是最容易被草率设计的地方一般情况下只加一个经办人字段就草草收场后续做业绩归属和岗位责任分析时才发现信息不够。4.4 落地为关系数据库表结构下面给出一个可以直接套用的四张核心表结构。它不是完整系统但覆盖了REA建模的主要场景适合作为起点。-- 经济资源表 CREATE TABLE economic_resource ( resource_id BIGINT PRIMARY KEY, resource_name VARCHAR(128) NOT NULL, resource_type VARCHAR(32) NOT NULL, -- STOCK/CASH/AR/AP/FIXED_ASSET unit VARCHAR(16), -- 计量单位 status VARCHAR(16) DEFAULT ACTIVE ); -- 经济事件表 CREATE TABLE economic_event ( event_id BIGINT PRIMARY KEY, event_no VARCHAR(64) NOT NULL UNIQUE, -- 业务单号幂等关键 event_type VARCHAR(32) NOT NULL, -- SALES_RETURN/PURCHASE/RECEIVE/PAY... occurred_at TIMESTAMP NOT NULL, remark VARCHAR(512) ); -- 经济代理表 CREATE TABLE economic_agent ( agent_id BIGINT PRIMARY KEY, agent_name VARCHAR(128) NOT NULL, agent_type VARCHAR(32) NOT NULL -- INTERNAL/EXTERNAL ); -- 资源-事件关联表存量-流量 CREATE TABLE stock_flow_line ( line_id BIGINT PRIMARY KEY, event_id BIGINT NOT NULL REFERENCES economic_event(event_id), resource_id BIGINT NOT NULL REFERENCES economic_resource(resource_id), direction SMALLINT NOT NULL, -- 1 增加资源-1 减少资源 quantity DECIMAL(18,4) NOT NULL, unit_price DECIMAL(18,4), -- 事件发生时点的价值 CHECK (quantity 0) );触发关系用一张事件关联表来维护这里我把它单独列出来CREATE TABLE event_link ( link_id BIGINT PRIMARY KEY, from_event_id BIGINT NOT NULL REFERENCES economic_event(event_id), to_event_id BIGINT NOT NULL REFERENCES economic_event(event_id), link_type VARCHAR(32) NOT NULL, -- TRIGGER/REVERSE/COMPENSATE UNIQUE(from_event_id, to_event_id, link_type) ); CREATE TABLE participation_link ( participation_id BIGINT PRIMARY KEY, event_id BIGINT NOT NULL REFERENCES economic_event(event_id), agent_id BIGINT NOT NULL REFERENCES economic_agent(agent_id), role VARCHAR(32) NOT NULL, -- SELLER/CUSTOMER/WAREHOUSE_KEEPER... UNIQUE(event_id, agent_id, role) );有一点要注意economic_event里不能把时间和金额塞成一堆业务字段。事件唯一身份由event_no保证业务动作本身由event_type和关系表描述。金额不直接存在事件主表里而是出现在stock_flow_line中因为“资源价格”是跟具体资源流入流出绑定的属性不是事件本身的属性。4.5 用建模结果指导业务流程校验建模完成后还要做一轮流程走查把每个业务场景按REA元素逐一手动推演一遍确保没有遗漏。比如“销售出货-收款”的正常流程推演下来是这样销售员代理参与销售出库事件事件销售出库事件减少库存商品资源存量-流量线同时触发收款事件Trigger线财务人员代理参与收款事件参与线收款事件增加现金资源存量-流量线。逻辑通顺。再把“销售退货-退款”推演一遍客户代理发起销售退货事件事件销售退货事件增加库存商品资源存量-流量线同时触发退款事件Trigger线退款事件减少现金资源存量-流量线。逻辑同样闭合。如果某天发现流程走查不闭合比如“采购入库后没有触发应付事件”说明业务环节缺失或事件没有被建模完全。就在这一步把它补上不要让问题流入开发阶段。5. 常见误区、踩坑与我的排查办法5.1 误区一把REA事件做成“流水账”表比较多的情况是团队听说要记事件就直接设计了一张字段非常宽的大表单时间、单据号、商品、数量、单价、金额、税率、客户、销售员、仓库、备注、扩展字段1、扩展字段2……这其实还是流水表思维只是把表名从biz_bill换成了event。事件的本质是“业务事实的类型”它的特征信息应该通过关系线去表达而不是全部堆在事件字段里。如果一个“销售出库”事件同时要记录三种商品的数量和单价宽表就会出现大量空字段或重复字段。正确的做法是用economic_event stock_flow_line一对多关联结构一张事件表管事实一张资源关联表管资源明细。我自己的判断标准很简单同一事件涉及多个资源时如果表设计不能自然扩展就是走错了方向。用宽表硬撑多资源后期对账和明细级分析就是个灾难。5.2 误区二在资源上存放冗余关联另一个容易踩的坑是在资源表上直接放置“最后事件ID”或者“最近价格”这类字段。有些人为了查询方便会在商品资源表上加“最近销售时间”“最近采购价”“最后经手人”字段。这类字段看着能救急实际上在REA模型里属于严重冗余。资源是存量事件是流量两者应该完全分离。如果资源表上加了“最后采购价”那么当某个事件被冲销或修正时资源表上的这个字段就必须联动修改一旦漏改就会出现“资源状态是对的、但该事件的记录保持不一”的脏数据。如果你真的很关心实时余额或最新价格正确做法是做一张资源余额视图或单独一张“当前快照表”由事件流水日终或事件提交时重算。快照表可以冗余核心资源表不能冗余。5.3 误区三代理只想到客户和供应商很多初做REA的人会把代理局限在外部角色上客户、供应商、承运人。但实际业务里内部代理的角色信息也非常关键。拿一次销售退货来说客户是外部代理处理退货的客服、审核退款的财务、检查退回商品的仓管这些内部代理同样记录在参与关系里。如果系统不做这类信息后续想看“哪个仓管员审核退货最多”“谁手上的退货异常率高”就完全无据可查。代理不是简单的人名而是“角色人”的组合。同一个员工既可能是销售员也可能是仓管员这两个身份在不同的参与关系里要能区分开。所以参与关系里一定要有role字段不能只靠agent_id关联。5.4 实战排查清单与压测注意事项在做REA模型上线前的数据校验时我通常按下面这个清单检查是否存在没有资源变化的事件一个事件至少连接一条存量-流量线如果某个事件连不到资源表业务含义存疑需要确认是否真的是事件。是否存在没有参与者的事件所有事件都应该有内部或外部代理参与完全无主的事件不符合业务事实。存量-流量线的方向是否与事件类型匹配销售类事件里库存商品的流量方向必须是减少现金或应收类资源的方向必须是增加。触发关系是否形成闭环死循环比如付款事件又反过来触发销售出库事件这种错误会让状态机永远跑不完。event_no的唯一性和幂等性是否落实重复提交事件是线上系统最容易遇到的风险数据库唯一索引必须加上。性能上REA关系表的量级可能增长得很快。一张stock_flow_line表在业务活跃的场景下一年会上百万甚至上千万行。所以两个关键索引必须从一开始就建好一个是(event_id, resource_id)联合索引用于按事件查资源明细另一个是(resource_id, occurred_at)索引用于按资源时间范围查流量。如果查询还有代理维度再在participation_link上增加(agent_id, event_id)联合索引。事件量特别大的时候考虑按业务线分表或按时间分区。比如库存流量表按月分区事件表按业务类型分表都是常见做法。REA模型本身不绑定任何存储方案表结构清晰之后上关系库还是上列存完全看查询模式。6. 几句个人体会关于REA建模的边界用了几年REA我最大的感受是它没有把问题变复杂而是把复杂问题说得更接近事实了。数据库设计最怕的不是表多而是表之间的关系不清楚。REA给了一套非常稳固的“关系语法”资源、事件、代理之间的三类关系能覆盖绝大部分业务场景。只要关系语法正确后期加需求、加分析维度都是顺着模型扩而不是推翻重来。REA也有明显不适合的场景。比如一个只有单一表单、无多方参与的轻量业务直接建一张业务流水表可能效率更高硬套REA显得臃肿。再比如强流程审批类系统REA关心的是“发生了什么”不关心“审批状态机怎么流转”这部分仍然需要工作流引擎来补位。我在实际项目中还有一个很深的体会REA模型不能只由产品经理或研发单方面推动。它最好由业务分析师、财务人员和后端架构师共同参与梳理因为资源清单往往来自财务视角事件清单来自业务流程视角代理清单来自组织管理视角。三方对齐之后产出的模型才真正经得起业务检验。如果只是技术团队闭门做REA很容易把模型设计成理论正确的空中楼阁。如果你现在正在重构一个进销存或者财务核心模块不妨先用REA把现有系统的核心交易梳理一遍。不需要推翻现有数据库只需要画出资源、事件、代理三类图再对照现有表结构看哪些语义丢失了。往往做完这一步系统重构的方向就会变得比想象中清晰不少。最后分享一个小技巧REA建模时可以把“事件”想象成一段影片的定格画面。资源是画面里流动的物品代理是画面里出现的人触发关系是画面切换的因果。每次建模我都在心里播放业务的心智模型作为校验。这样完成的表设计通常不需要翻文档就能直接读业务也经得起后续无数次查数和分析的折腾。把这个方法保持住你手里的业务系统就不会只是“一套能跑的程序”而是一个真正能被理解、被审计、被演化的结构化业务记忆。这是REA模型留给项目最大的价值也是我至今仍愿意在各种项目里反复使用它的原因。
阅读完成 · 觉得有帮助?