
第一次接触“rea”这个代号是在一次业务系统重构的前期调研里。项目文档上只写着三个小写字母我当时还以为是某个中间件的缩写。后来把 Resource、Event、Agent 三个词拼回去才反应过来这其实是一整套业务建模思想而且比很多人熟知的实体关系图更贴近业务的真实语义。这篇文章就围绕 rea也就是 REA 模型展开写给被订单、库存、资金对不上账困扰过的朋友也写给准备做业务系统建模、但不想一上来就掉进表结构细节里的同行。1. REA 模型Resource、Event、Agent 到底是什么1.1 三个字母背后的核心含义REA 的全称是 Resource-Event-Agent翻译过来就是“资源-事件-参与者”。很多人第一次看这三个词会觉得太抽象我当年也这样。换个方式理解任何一笔业务本质上都是“谁在什么时间对什么资源做了什么事情”。Resource 资源企业里能带来价值的对象。除了常见的商品、库存、现金还包括固定资产、服务能力、数字资产、知识文档。在 REA 里资源的核心特征是“能被事件改变状态”。比如库存商品会被出库事件减少会被入库事件增加。Event 事件业务中真实发生的动作。客户下单、仓库发货、客户签收、财务收款、供应商开票这些都是事件。事件是 REA 模型里最核心的记录单位它不是业务流程图上那个方框而是“确实发生了”的事实。Agent 参与者事件的执行人或对象。参与者分两类一类是内部参与者比如销售员、仓管员、财务人员另一类是外部参与者比如客户、供应商、承运商。把这三个要素放到一个业务场景里就会得到一组“谁参与了什么事、这件事动了哪些资源”的语义关系。这组关系一旦建立系统里流转的就不再是几张互相割裂的表格而是一条可以追踪到底的业务轨迹。1.2 三条最基础的关系规则REA 模型不是把 Resource、Event、Agent 三个表建起来就算完它最关键的是关系规则。最早提出这套思想的时候研究者总结了几条基本约束我实际用下来最常用的是这三条第一条每个经济事件至少涉及一个资源流出和一个资源流入。这句话可能不太好懂换个说法一笔业务如果只是“出货”而没有“收款或应收”那这笔业务在模型上是不完整的。现实中先货后款很常见但“先货”和“后款”必须通过事件配对关联起来而不是让它们各自躺在业务表里。第二条每个经济事件至少有一个内部参与者和一个外部参与者。这其实是强制的审计约束。发货事件必须有仓管员内部和客户外部如果没有外部参与者这笔货去哪儿了就没法回答。很多系统做不出完整的进销存追溯问题就出在这条规则上。第三条资源只有在事件发生时才能被记录或改变不允许凭空修改余额。这条规则和事件溯源思想很像。库存余额不是直接改数字而是通过“出库事件”“入库事件”“盘点事件”推导出来。这么做的好处非常直接每一笔余额变化都能找到原因。这三条规则看着简单但每一条都在对抗一种常见的偷懒设计。第一条对抗“只记过程不记结果”第二条对抗“只记账不记人”第三条对抗“余额随便改”。把这三条立在设计文档第一页后面建表、写查询、做对账都会顺畅很多。2. 为什么传统记账模型撑不起现代业务系统2.1 复式记账的盲区传统企业的核心系统大多围绕复式记账思想构建也就是大家熟悉的“有借必有贷借贷必相等”。这套体系在财务领域非常成熟但它有先天盲区它记录的是会计科目的结果不是业务过程的语义。举个例子。财务凭证上有一笔“借应收账款贷主营业务收入”它清楚地记录了金额和科目却无法回答几个业务人员最关心的问题这笔应收账款对应的商品是哪个批次的是哪位销售员签的合同物流单号是什么客户是否已经签收这些信息在传统凭证体系里完全没有位置只能通过凭证摘要、关联单据号去猜或者把额外的业务信息塞进备注字段。时间一长追踪链路就断了。还有一个更隐蔽的问题复式记账模型天然鼓励“冗余”。因为科目余额和业务明细是两套数据系统需要同时维护财务账和业务流水两边一旦对不上就只能靠月底对账去“找差异”。这个找差异的过程本质上是为模型缺陷买单。2.2 REA 把“流水日志”升级成“语义模型”REA 模型的思路是完全不同的。它不把业务事实强行折叠成借贷科目而是保留业务事件的本来面目某年某月某日某个客户从仓库提走了多少商品对应仓管员是谁之后的收款事件又是什么时候发生的。这样设计带来的直观变化是科目余额变成了可以由事件推导出来的查询结果而不是需要手工维护的一张表。所谓“主营业务收入”其实是“已发货事件”的金额聚合所谓“库存余额”其实是“入库事件数量减去出库事件数量”的实时计算。业务事件成为唯一的事实来源财务口径只是对这些事件的视图。用生活中的例子类比更容易理解传统的记账方式像一本只写汇总数字的账本REA 更像一本完整的操作日志加规则引擎。账本告诉你现在有多少钱操作日志告诉你每一分钱是怎么来的。在审计、追溯和对账场景里这个差别是本质性的。当然REA 模型不是为了取代复式记账而是让上层财务系统有更可靠的底层业务事实。我见过不少系统业务侧用事件表记录动作财务侧用科目表生成报表两层之间通过事件 ID 关联效果就非常理想。3. 用一个销售业务场景完整推演 REA 建模3.1 场景选取与实体识别理论讲多了容易飘直接拿一个具体的销售场景来推演。假设我在做一个模拟的食品贸易销售系统核心业务是客户下单仓库发货财务收款。很多企业级系统就是从这条主链起步的。第一步是识别资源。这个场景里的核心资源是“成品库存”也就是仓库里可以卖的商品。如果还要精细一点可以把“应收账款”也建模为资源但大多数情况下应收是发货事件与收款事件之间的配对关系产生的推导结果不需要单独建资源表。第二步是识别参与者。内部参与者包括销售员、仓管员、财务人员外部参与者主要是客户和承运商。有些场景里还把上游供应商算进来那是采购链路的事这个案例先不展开。第三步是识别事件。很多人一上来就把“销售订单”当作唯一事件这是最常见的建模错误。销售业务至少包含三个不同性质的事件销售下单事件客户承诺购买属于承诺类事件还没有实际资源流动。发货事件商品从仓库流向客户是实际资源流出事件。收款事件资金从客户流向企业是实际资源流入事件。这三个事件必须分开建模不能揉成一团。3.2 关键业务事件的定义与配对把事件拆开之后下一步是定义事件之间的关系。REA 模型特别强调事件配对。销售下单事件是“承诺”发货事件是对承诺的执行收款事件又与发货事件形成资源交换的配对。这种配对关系是审计追溯的命脉。我在建模的时候会专门维护一张“事件配对关系表”记录两张事件表之间的关联类型。比如发货事件与收款事件关联类型是“结算”销售订单事件与发货事件关联类型是“执行”。这里有个容易纠结的点客户付款可能分多次一次发货也可能对应多笔收款。所以发货事件和收款事件之间是多对多的配对关系。很多设计错误地在这个地方用外键直接挂一个“收款单号”导致一次分期付款只挂了最后一次金额。正确做法是维护一张独立的配对表每次卖方网的收款事件在配对表里追加一行。事件发生时资源的方向也要定义清楚。发货事件里商品是资源流出收款事件里资金是资源流入。方向不对后续做库存和收入统计时数据全反。3.3 内部参与者与外部参与者的角色划分参与者建模没有想象中简单。同一个客户可能既是下单人又是收货人同一笔业务里销售员可能同时是跟单人。所以参与者与事件的关系里必须带“角色字段”而不能只记录“参与者ID”。我在设计里常用的做法是事件参与者关联表增加 role 字段枚举值包括下单人、审批人、发货人、收货人、经办人等。这样做的好处是以后想知道“谁经手的这笔订单”或者“这批货签收人是谁”一条 SQL 就能查出来。外部参与者的判断标准也很关键。凡是企业边界之外的单位或个人都算外部参与者。客户和供应商一定是外部参与者运输公司的司机虽然具体到个人但他代表外部运输公司也算外部参与者。这条规则可以防止建模时把“承运司机”错当成内部仓管员导致权限和审计逻辑混乱。4. 把 REA 模型落成数据库表结构4.1 核心表结构设计概念模型讨论完毕就该动手建表了。 REA 模型落库后通常不是一张大表而是由资源表、事件表、参与者表、关联表组成的一组关系结构。下面是一个可以直接参考的表结构示例。资源表CREATE TABLE resource ( resource_id INT PRIMARY KEY, resource_name VARCHAR(128) NOT NULL, resource_type VARCHAR(32) NOT NULL, unit VARCHAR(20), spec VARCHAR(128) );事件表这里的 event_type 是核心字段对应销售、发货、收款等不同事件CREATE TABLE economic_event ( event_id INT PRIMARY KEY, event_type VARCHAR(32) NOT NULL, event_time TIMESTAMP NOT NULL, resource_id INT REFERENCES resource(resource_id), quantity DECIMAL(14,2), amount DECIMAL(14,2), direction VARCHAR(8) NOT NULL CHECK (direction IN (IN,OUT)) );参与者表CREATE TABLE agent ( agent_id INT PRIMARY KEY, agent_name VARCHAR(128) NOT NULL, agent_type VARCHAR(16) NOT NULL, external_flag BOOLEAN NOT NULL );事件参与者关联表注意 role 字段CREATE TABLE event_agent_link ( event_id INT REFERENCES economic_event(event_id), agent_id INT REFERENCES agent(agent_id), role VARCHAR(32) NOT NULL, PRIMARY KEY (event_id, agent_id, role) );事件配对表处理发货与收款之间的多对多关系CREATE TABLE event_pairing ( pair_id INT PRIMARY KEY, from_event_id INT REFERENCES economic_event(event_id), to_event_id INT REFERENCES economic_event(event_id), pair_type VARCHAR(32) NOT NULL );这套结构看着稍显复杂但每条记录都有明确作用。resource 表负责资源的定义economic_event 表负责记录业务事实agent 表负责人员和组织两张关联表建立事件与参与者、事件与事件之间的语义关系。4.2 核心查询语句示例表结构落实之后REA 的威力才真正体现出来因为大部分业务报表都可以通过简单查询从事件表推导。举几个最常用的例子。查询某段时间内每个客户的销售额SELECT a.agent_name, SUM(e.amount) AS sales_amount, SUM(e.quantity) AS sales_quantity FROM economic_event e JOIN event_agent_link el ON e.event_id el.event_id JOIN agent a ON el.agent_id a.agent_id WHERE e.event_type SHIPMENT AND e.direction OUT AND e.event_time 2025-01-01 AND el.role CUSTOMER GROUP BY a.agent_name ORDER BY sales_amount DESC;查询每个客户的应收余额本质上就是找已发货但未配对的收款额SELECT a.agent_name, SUM(e.amount) AS receivable_amount FROM economic_event e JOIN event_agent_link el ON e.event_id el.event_id JOIN agent a ON el.agent_id a.agent_id WHERE e.event_type SHIPMENT AND e.direction OUT AND NOT EXISTS ( SELECT 1 FROM event_pairing ep WHERE ep.from_event_id e.event_id ) GROUP BY a.agent_name;这两个查询背后没有余额表没有冗余汇总完全基于事件明细实时计算。数据量大了以后可以做物化视图或定期汇总但逻辑基础依然是事件表。4.3 与业务流程审批流的边界有人会问REA 模型里的事件表能不能直接替代工作流引擎我的观点是不能也没必要。REA 模型解决的是“业务事实如何记录”审批流解决的是“业务动作如何流转”。比如销售订单在审批通过之前只是草稿审批通过之后才成为有效的承诺事件。我通常的做法是工作流引擎负责订单状态的推进当订单状态变成“已确认”时系统再向 REA 事件表写入一条确认事件。这样REA 表里只保留具有业务效力的事实不给审批过程中的中间态留脏数据。这条边界很重要。如果错误地把“草稿”也写成事件后续统计会产生大量需要过滤的哑数据。反过来如果把审批状态硬塞进事件表事件参与者的角色含义会被扭曲。两边各司其职系统才干净。5. REA 建模的常见问题与排查心得5.1 高频问题速查表下面这个表是我在项目排查过程中整理出来的高频问题每一条都在实际场景里遇到至少一次。典型问题症状根因排查思路事件类型划分过粗查询销售数据时混入退款记录把发货、退款、收款都塞进同一个 event_type拆分为独立事件类型增加 direction 字段承诺事件和实际事件混用销售额和发货额对不上订单事件与发货事件存在同一事件表分离承诺事件表或显式定义事件类型缺少内部参与者无法追溯“谁发货的”只关注客户忽略仓管员补事件参与者关联表和角色字段多对多配对丢失分期收款后应收余额错误用外键直接挂单次收款改为独立事件配对表余额表手动维护库存与明细不一致直接修改余额字段余额改为事件聚合结果外部参与者判定错误承运司机被当成内部人员把组织外的人都当作内部参与者按企业边界重新确认参与者类型每次遇到账目对不齐先不要急着调余额回到事件明细里去查配对关系绝大多数问题的答案都在那里。5.2 我踩过的坑与避坑建议第一次落地 REA 模型时我对“事件”的理解过于狭窄以为只有发货、收款这种资源实际发生变化的才算事件。后来发现客户提交订单、销售修改价格、仓库盘点调整这些动作同样是业务事实的组成部分如果不记录后续追溯时缺了一大段上下文。尤其需要提醒的是“修改类事件”。现实中业务反复修改是常态比如订单数量从 100 改成 120。如果直接在原事件上更新数量和金额历史审计线索就断了。我的做法是原事件保持原样追加一条“更正事件”记录修改前后差异量并关联原事件 ID。这样任何时候回看都能还原业务的完整变化路径。另一个坑是我在权限设计上踩的。早期把 Agent 表和系统用户表混为同一张表结果外部客户、内部员工、系统管理员全挤在一个表里角色语义非常混乱。后来把系统账号表和业务 Agent 表拆开再通过映射表关联。系统账号负责登录认证业务 Agent 负责业务语义两者解耦之后权限设计清爽了很多。最后一点心得REA 模型对数据质量要求不低。它本质上是“事实驱动”的设计如果事件漏录、错录推导出的所有结果都会错。所以除了外键约束最好在应用层增加校验逻辑比如“发货事件必须有库存资源”、“收款事件必须能追溯到至少一个发货事件”。这些校验看似琐碎但能在源头挡住大量脏数据。我在实际项目里用 REA 模型重新梳理业务口径之后最明显的收益不是表结构变漂亮而是和业务部门沟通时有了共同语言。讨论“应收为什么多了”不再需要拉三张表拼数据打开事件追溯就能看到是哪个客户、哪批货、哪次收款还没配对。单独拿一个小技巧收尾建模初期在实体关系图上把每个事件的 direction 都标出来很多表结构缺陷在设计阶段就会自动暴露。