资讯动态

REA业务建模:用资源-事件-主体重构订单与库存设计

发布时间:2026/10/21 5:53:57 来源:尧图企业网站定制
写业务系统的人几乎都有过同样的经历需求评审会上产品经理说“就是一套标准的订单加库存管理”你回到工位第一件事打开数据库设计工具开始画订单表、订单明细表、商品表、客户表、收款记录表。这种从“表”出发的设计直觉支撑了无数系统的快速上线可等到要加退货、要跨部门对账、要做行为审计的时候最初那张订单表反而成了最难受的约束。今天想聊的 REA恰好是反着来的。它不问你“要建几张表”而是先逼着你回答三个词你的业务里有哪些资源哪些事件会改变资源哪些主体参与了改变。把这三件事想清楚表结构是水到渠成的事。这篇文章我就把 REA 从原理到落地完整拆开用一个真实可复现的案例带你把模型一步步翻译成数据库表再聊聊我在实际项目里踩过的坑。1. REA从哪里来回到什么问题REA 缩写对应的英文是 Resource-Event-Agent翻译过来就是“资源-事件-主体”。它最早不是程序员发明的而是上世纪八十年代会计信息系统研究领域提出的概念。当时研究者发现传统的复式记账法太执着于“借贷科目”了一笔销售进来要记应收账款、要记收入、要记库存减少每个科目都是单独的一张表业务链条被人为切碎。于是有人换了一个角度问如果不考虑会计科目一笔经济业务最本质的构成是什么答案就是资源、事件、主体这三样。这套思路在当时是用来改进数据库建模的所以 REA 本质上是一套业务建模框架不是某个具体的软件或者中间件。近几年它又重新进入开发者视线原因是事件驱动架构、领域驱动设计、以及各类需要多方协作的账本系统越来越流行大家发现 REA 描述业务的方式和事件溯源、不可变日志这些现代实践天然能接上。我把 REA 解决的问题总结成一句大白话它让“业务语义”而不是“数据表”成为系统设计的起点。传统的表驱动设计是先定义订单、订单明细、商品、客户这些名词再把动词下单、付款、发货写成状态字段或者日志表REA 反过来先把动词当作一等公民也就是“事件”本身要建模成核心实体然后围绕事件去关联资源、关联主体。REA 的核心假设有三条理解了这三条后面所有设计决策就都顺了企业本质上是一张由经济主体参与的交换与转换网络。没有主体参与的活动比如系统自身的定时任务不应该进入业务事件模型。所有值得记录的业务活动最终都体现为资源状态的改变。如果一个动作没有让任何资源发生变化它就不具备业务价值不该进入核心建模范围。每一次事件都必须能追溯到一个或多个责任主体。谁做的、谁授权的、谁接收的这些信息是业务语义的一部分不是可有可无的审计字段。第一点解释了为什么 REA 适合描述供应链、交易、资金流这类多人协作场景第二点会直接影响建模粒度第三点则是 REA 后来在审计和合规场景里被反复提及的根本原因。如果你正在设计订单中心、库存中心、资金账本这类核心系统REA 就是那种能让后期少返工的底层思维工具。有人会问REA 是不是就是换了种画法的新版 ER 图我觉得有联系但区别非常明显。ER 图关心的是有哪些实体、字段、外键REA 关心的是“谁、在什么时间、把什么资源、从谁手里转移到了谁手里”这个完整链条。ER 图是静态视角REA 是动态视角。这也是为什么一个 REA 模型可以比较自然地演进成事件流、消息、账本类系统。2. 资源、事件、主体三个词决定建模边界REA 只有三个核心概念听起来谁都懂但我在实践中发现绝大多数人刚上手时都会在这三个词上栽跟头。要么是把资源定义成数据库里的“字段”要么是分不清事件和普通操作记录的区别要么是漏掉了外部主体这一层。2.1 资源能被控制和有价值缺一不可REA 里的资源指的是资源所有权或控制权的主体能够支配、并且具有经济价值的事物。判定标准就两条能不能控制有没有价值。举个例子。一家咖啡店里的咖啡生豆、烘焙好的熟豆、库存现金、储值卡余额这些显然是资源。一台可以重复使用的咖啡机也是资源因为它能被控制也有价值。甚至连“咖啡师工作一小时”这种服务供给在 REA 里也可以建模为资源因为它具备被消耗、被交易、被度量的属性。但是“品牌影响力”“客户满意度”这类概念虽然很重要却很难被某个主体控制也不存在清晰的单位度量所以不适合直接建模成资源。很多初学者在这里卡住其实记住一句话就行REA 资源的第一特性是“可被交换或消耗”虚的概念不要往这里塞。资源也不等于会计上的“资产”。资产是会计期末的一种存量表达资源是业务流程中的一个语义节点。你甚至可以为了建模方便把“客户剩余积分”定义成一种资源这在会计上没有任何科目对应但在 REA 里完全合理。一个资源实体典型的属性包括资源编号、名称、描述、规格、单位、当前数量、状态等。注意资源表只回答“我们手里有什么”不回答“这些状态是怎么变成这样的”。状态变化的过程是事件要管的事。2.2 事件资源状态变化的原子记录事件在 REA 里指“经济事件”也就是能导致资源流入、流出或状态改变的业务动作。它是整个模型里最核心、也最容易设计过度的部分。我们做订单系统时大多数人会把“下单”“付款”“发货”做成订单表上的状态字段订单状态从待支付改成已支付从已支付改成已发货。字段一变历史信息就被覆盖了。REA 的做法完全不同它会把“支付成功”当成一个独立事件落库把“发货完成”也当成一个独立事件落库订单只是这些事件围绕的一个业务上下文。事件必须满足三个特性原子性一个事件对应一个不可再分的业务动作。比如“扫码支付成功”就是一个事件“同时收款和发货”如果能拆开看就绝对不能合并成一个。可追溯性每个事件都要有明确的发生时间、参与主体和关联资源能回答“发生了什么、谁做的、涉及哪批资源”。业务意图性事件要对应真实的业务规则比如“有销售订单才允许出库事件”模型里的事件不能被随意构造。这里特别要强调一个高频误解事件不是日志。操作日志记录的是“系统里发生了一次点击”事件记录的是“资源发生了实质性的流入流出”。日志可以随便打事件必须遵循业务规则。这也是为什么很多进销存系统后期对账困难的原因——它们只有流水日志没有真正的业务事件语义。2.3 主体内部主体和外部主体不能混为一谈主体就是参与事件的个人、部门或者组织。REA 把主体分成两类内部主体企业内部的人或部门比如采购员、库管员、财务、咖啡师。外部主体企业外部的交易对手比如供应商、顾客、承运商。为什么要刻意区分因为很多时候一个事件需要同时关联一个内部主体和一个外部主体才完整。比如“向供应商采购咖啡豆”这件事内部主体是采购员外部主体是供应商“顾客付款”这件事内部主体是收银员外部主体是顾客。如果模型里只记录“某某操作员创建了一个采购单”却没有把供应商实体关联上那这个模型就是残缺的因为它丢失了业务语义中最关键的那半截交易关系。REA 强调一个完整的经济事件至少要有一个负责的内部主体和一个对应的外部主体。这也是后来在权限与合规场景里REA 模型明显比常规表设计更好用的原因责任主体一开始就进了模型而不是事后靠日志猜。2.4 三种关键关系建模的真正的骨血理解 REA 不能只看三个实体还要看实体之间的三类关系。第一类是库存流关系Stockflow连接资源与事件。它表示某个事件让某个资源的存量增加了还是减少了。比如“采购入库事件”让咖啡生豆资源增加“销售出库事件”让熟豆资源减少。库存流关系通常带一个方向标记流入为正流出为负。第二类是参与关系Participation连接主体与事件。它表示谁参与了这场业务动作。内部主体和外部主体都可以和同一事件建立参与关系只是角色不同。第三类是互换关系Exchange Duality连接事件与事件。它表示一个业务循环中资源流入事件和资源流出事件之间的配对。最典型的例子就是“销售事件”资源流出和“收款事件”资源流入互为配对。你把资源交付给顾客同时收到了对方的钱这两个事件构成一个完整的交换。基数规则直接决定业务约束。最基本的几条我整理在这张表里关系参与者典型基数说明库存流资源 - 事件一个事件可对应多条资源流一条资源流只能属于一个事件一次采购可能入库多种咖啡豆参与主体 - 事件一个事件至少关联一个内部主体和一个外部主体没有主体的裸事件不允许存在互换事件 - 事件一个流入事件可与一个或多个流出事件配对一笔收款可以核销多笔销售订单有朋友会问这些基数规则会不会太死板如果业务里有“匿名顾客自助下单”呢外部主体怎么处理通常的做法是把“未知顾客”或“散客”作为一条默认主体记录而不是让事件缺失主体。这个习惯非常关键因为一旦模型允许没有外部主体的事件后续所有对账和审计逻辑都得堆在应用代码里数据库层面就失控了。3. 一个咖啡店闭环的完整建模过程理论说得再多不如完整走一遍建模。我拿“咖啡店”这个场景来演示因为它足够小又覆盖了采购、生产转换、销售、收款多个环节。这一步大家可以照着思路套任何一个进销存或订单场景。3.1 第一步圈定要建模的业务闭环建模第一件事不是列表而是划范围。不要一上来就想把整个咖啡店的经营数据全塞进 REA 模型先圈定一个业务闭环。我选“采购-销售-收款”这条主链。咖啡店从供应商那里买咖啡生豆经过烘焙变成熟豆再制作成咖啡卖给顾客最后收到顾客付款。闭环里的资源变化有几个生豆增加、生豆减少、熟豆增加、熟豆减少、现金增加。3.2 第二步把业务名词归位到资源、事件、主体把刚才的业务流程拆分出来每个元素对号入座。资源包括咖啡生豆原材料烘焙熟豆半成品外带杯、牛奶、糖包辅料门店现金和电子账户余额资金咖啡师制作时长服务如果要把人工成本也建模进去事件包括采购入库事件向供应商购买生豆生豆流入烘焙事件生豆消耗、熟豆产出这是一个转换事件涉及两个资源流销售出库事件熟豆消耗、咖啡产品转交给顾客收款事件顾客付款现金或电子余额流入付款事件门店向供应商支付货款现金流出主体包括外部咖啡豆供应商、顾客内部采购员、烘焙师、咖啡师、收银员、财务这里注意一个细节烘焙事件是一个“转换”不是“交换”。它只有一个内部主体参与没有外部对手方它同时涉及一个资源的流出生豆减少和另一个资源的流入熟豆增加。REA 模型里这种“生产/转换”事件很常见。3.3 第三步把模型翻译成数据库表结构现在开始把上述概念落到表结构。我的习惯是先建事件相关表再建资源表最后补主体表。因为事件是整个模型的中枢。资源表CREATE TABLE resource ( resource_id INTEGER PRIMARY KEY, resource_code VARCHAR(32) NOT NULL UNIQUE, resource_name VARCHAR(128) NOT NULL, spec VARCHAR(64), uom VARCHAR(16) NOT NULL, -- 计量单位 current_qty DECIMAL(18, 2) NOT NULL DEFAULT 0, status VARCHAR(16) NOT NULL DEFAULT ACTIVE );主体表CREATE TABLE agent ( agent_id INTEGER PRIMARY KEY, agent_code VARCHAR(32) NOT NULL UNIQUE, agent_name VARCHAR(128) NOT NULL, agent_type VARCHAR(16) NOT NULL, -- INTERNAL / EXTERNAL contact_info VARCHAR(255) );事件表CREATE TABLE event ( event_id INTEGER PRIMARY KEY, event_code VARCHAR(32) NOT NULL UNIQUE, event_type VARCHAR(32) NOT NULL, -- PURCHASE / PRODUCE / SALE / RECEIPT / PAYMENT occurred_at TIMESTAMP NOT NULL, internal_agent_id INTEGER NOT NULL REFERENCES agent(agent_id), external_agent_id INTEGER REFERENCES agent(agent_id), note VARCHAR(255), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP );库存流关系表用来关联事件和资源同时区分方向CREATE TABLE stockflow ( stockflow_id INTEGER PRIMARY KEY, event_id INTEGER NOT NULL REFERENCES event(event_id), resource_id INTEGER NOT NULL REFERENCES resource(resource_id), direction SMALLINT NOT NULL, -- 1 流入, -1 流出 qty DECIMAL(18, 2) NOT NULL, unit_price DECIMAL(18, 2) -- 可选记录资源变动时的单价 );互换关系表把销售事件和收款事件配对CREATE TABLE exchange_pair( pair_id INTEGER PRIMARY KEY, inflow_event_id INTEGER NOT NULL REFERENCES event(event_id), outflow_event_id INTEGER NOT NULL REFERENCES event(event_id) );这个结构看起来多了一张表但它换来了一个非常大的好处资源变动的历史永远不会被覆盖。订单状态被改成“已取消”之后你还能看到曾经发生过一笔销售事件付款金额拆成三期就生成三条收款事件再通过 exchange_pair 全部挂到同一个销售事件下面。3.4 第四步写几条 SQL 验证模型是否好用如果模型正确常见业务问题的查询应该非常直接。查某天卖了多少杯咖啡可以这样SELECT DATE(sf.event_time) AS sale_date, SUM(sf.qty) AS total_cups FROM stockflow sf JOIN event e ON sf.event_id e.event_id WHERE e.event_type SALE AND sf.resource_id (SELECT resource_id FROM resource WHERE resource_code ROASTED_BEAN) AND DATE(e.occurred_at) CURRENT_DATE GROUP BY DATE(sf.event_time);查某个供应商最近一个月给我们供应了什么SELECT r.resource_name, SUM(sf.qty) AS total_in_qty FROM stockflow sf JOIN event e ON sf.event_id e.event_id JOIN resource r ON sf.resource_id r.resource_id WHERE e.event_type PURCHASE AND e.external_agent_id ( SELECT agent_id FROM agent WHERE agent_code SUPPLIER_001 ) AND e.occurred_at date_trunc(month, CURRENT_DATE) - INTERVAL 1 month GROUP BY r.resource_name;这类查询在传统的“订单表状态字段”设计里很难写因为你需要找销售的日志表去关联。而在 REA 模型里事件表就是天然的明细账stockflow 表就是天然的发生额查询和审计都变得非常机械化。3.5 这个模型怎么支持后续扩展咖啡店如果要加“退货”场景只需要新增一个“退货入库事件”和对应的“退款事件”销售退货会让资源重新流入退款会让资金流出两个事件再通过 exchange_pair 配对。不需要给订单表加“是否退货”字段也不需要动历史销售事件。如果要加“促销买一赠一”可以把赠品建模成同一销售事件下额外的库存流出只不过单价为零。原有结构不需要大改。这是 REA 模型最值钱的地方它能以新增事件的方式承载新业务而不是每加一个业务规则就改一次既有表结构。4. 为什么 REA 和现代事件驱动架构能接上写这一段是因为我最近一年多明显感觉到 REA 开始从理论书里走出来进入真实的工程实践。这不是偶然因为软件架构的流行方向变了。传统系统保存的是“最新状态”订单表里永远只有当前状态历史被覆盖。事件溯源架构保存的是“状态变化序列”想要任何时点的状态就把事件重放到那个时点。REA 的核心视图和这个思想完全一致资源表只是存量的物化视图真正权威的数据是事件表和库存流表。用术语来说事件表是事实数据资源表是投影数据。这样做的好处很明显。最直接的一点库存数据不准确的时候你不用去猜库存是怎么变成这样的而是直接把事件从第一个重放到最后一个逐步比对。某个环节录错了修正该事件的 qty后续所有结果自动跟着修正。这种能力在传统“库存表直接 UPDATE”的设计里是做不到的改了库存字段你就丢掉了原因。还有审计和合规场景。现在很多行业越来越看重操作留痕传统系统通常单独搞一张审计日志表记录的是“谁在什么时间操作了什么页面”。REA 根本不额外做这套东西因为业务事实本身就包含主体和事件。你查“这个月有哪些销售订单存在未经授权的折扣”可以直接从事件表、参与关系中查到责任人。审计日志在 REA 模型里不是附加功能而是核心模型的直接产物。另外每次我在设计涉及多个企业协作的系统时都会把 REA 模型拿出来铺在桌面上。传统单机系统的表结构是围绕一个企业内部权限设计的而多方协作系统里资源在不断的流入流出每个主体都要对资源变化负责REA 几乎就是为这种场景定制的。我去年参与过一个模拟的跨企业供应链数据平台最初大家拿出来的方案都是“订单表、对账单表、应付款表”结果一开工就卡在标准不统一A 公司的“已发货”到底对应 B 公司的“在途”还是“已收到”后来换成了 REA 思路把各家的业务都拆成“资源流事件主体”下游做对账时只需要匹配事件 ID 和库存流方向语义冲突一下少了很多。当然 REA 也不是万能的。它不擅长表达复杂的流程编排比如审批流、状态机这些还是要用工作流引擎。但它非常适合表达事实发生的记录和追溯这个定位和“账本”类系统是完全一致的。5. 落地时候最容易踩的坑理论模型讲得再好拿到真实项目里总会有几个反复出现的坑。我结合自己多次实际使用的经验把这些坑列出来希望你能少走点弯路。5.1 事件粒度太细或太粗后期都难受粒度选择是 REA 建模里最主观也最容易失控的一步。同样一笔销售有人用一个“销售事件”搞定有人把“接单”“出杯”“交付”拆成三个事件。我的建议是粒度的基准线是“你能不能独立地、有意义地回答一次资源变化”。咖啡店场景里“卖出一杯咖啡”对应熟豆减少一杯量、现金增加一杯价格这就足够了。如果你拆到“称豆子 20 克”和“热水注入”这种动作资源语义就被切得太碎后续没有人能够把这么多事件拼回一笔业务。反过来“售出商品”和“交付商品”如果明明发生在不同时间点、由不同人完成、对应不同风险责任就一定要拆成两个事件否则退货和售后永远不会清楚。判断标准很简单当你要回答“顾客是否已经拿到货”时如果同一个事件既能表示“已下单”又能表示“已交付”就说明粒度太粗了。5.2 事件 ID 幂等性必须提前设计真实系统不是单机玩具。对接第三方支付回调、多终端上传、消息队列重试同一个业务事件完全可能重复推送。如果不做防重每次重复推送都会产生一条库存流记录你的账永远会平白多出一份。我的做法是给事件表加一个 external_biz_no 字段存业务侧产生的唯一编号比如订单号加业务类型后缀然后对它建唯一索引。CREATE TABLE event ( ... external_biz_no VARCHAR(64) NOT NULL, CONSTRAINT uk_event_biz_no UNIQUE (external_biz_no) );每次写入事件之前先查询如果已经存在就跳过。这个改动成本极低但能省掉后续对账时大量“神走的库存”。这一条建议无论用不用 REA任何事件表设计都应该执行。5.3 资源余额不一定要实时死算新手很容易把 current_qty 当成事务里必须强一致更新的字段。每插入一条事件就立刻 UPDATE 资源表库存变少了就扣减。小数据量没问题但一旦库存操作并发上来资源表就变成热点行大量请求互相等待性能直线下降。更合理的做法有两个路径一是把资源余额当成一个可异步更新的投影事件插入后通过领域事件异步更新资源表二是在查询时通过事件表实时聚合得出余额资源表只存基础信息和上下限不参与即时事务。短期一致性的最终一致性策略在这个场景里完全够用。毕竟你要的是最终账面准确不是毫秒级同步。用 REA 建模最大的福利之一就是你不再需要靠“更新表”来计算余量——事件流本身就是最可靠的余额来源。5.4 主体和责任缺失是模型腐败的开始很多团队建 REA 模型时资源表和事件表都设计得很认真唯独在“内部主体”和“外部主体”上偷懒觉得“先记一笔再说”。这是模型腐败的开始。一个事件如果没有主体将来审计的时候就是一个黑洞。普通系统里你可以用日志去追查“谁干的”但 REA 的主旨就是责任归属一开始就必须进模型。没有责任人的事件要么用默认主体补上要么就不允许写入。我个人的落地习惯是给事件表加上两个字段creator_agent_id 和 owner_agent_id。creator 是操作执行人owner 是事件对应的业务责任人。很多系统里的越权和责任纠纷靠事件表追溯时一眼就能看出来。你甚至可以为了保证部分场景给事件表加操作类型字段让“谁创建、谁修改、谁审核”变成可查询的轮廓。5.5 从 REA 反向驱动表结构的实现清单最后分享一套我从 REA 落地到实际项目的完整步骤基本可以当 Checklist 用先圈定业务闭环范围不要求覆盖所有模块从一条有始有终的主链开始。列出闭环内所有资源严格用“可控制且有经济价值”这条标准筛掉虚概念。列出资源状态发生变化的事件用“是否原子、能否追溯、是否有业务语义”校验。为每个事件指定内部主体和外部主体缺主体的事件回头补上默认主体。在事件与事件之间建立互换关系配对让业务循环闭合。设计事件表加上 external_biz_no 唯一编号和创建人/责任人字段。设计库存流关系表把事件与资源通过方向连接起来。资源表只承担存量投影职责别让它替代事件表。设计查询和统计 SQL用真实业务问题验证一遍模型比如周销量、应收未收、库存余额。再进入实际接口、页面开发。我自己实际使用下来的体会是刚开始从 REA 思考问题时确实别扭人都习惯先画表但一旦在关键业务模块落地后续迭代新需求会特别省劲儿。早年间我做一个进销存项目因为没有用这套模型每次加退货和换货都像在给旧房子打补丁后来换了一版新系统从一开始就用事件和资源流来建模同样场景的改动量小了非常多。最后再分享一个小技巧也算是最实在的收尾不管你的系统最终用不用完整 REA都建议在所有核心业务表里保留责任主体字段哪怕只是把“操作人标识”单独拎出来建个索引。等到有一天你真正需要回答“这笔资源变化是谁负责”的时候你会回来感谢这个决定。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑