1. REA 到底是什么,为什么值得学
你可能在很多业务建模的讨论里见过“REA”这三个字母:Resource、Event、Agent。如果只看名字,很容易把它当成会计凭证的另一种叫法,但我个人在几个流程重构项目里实际用过以后,觉得它更像一把把业务逻辑拆成可执行数据的解剖刀。REA 模型的核心思想,是把企业的每一项业务活动,都看作一次“事件”,而事件一头连接着“资源”,另一头连接着“代理人”。听起来有点抽象,但它解决的实际问题非常具体:传统系统往往把业务流程录成一张张表单,表单之间靠字符串、金额、备注硬关联,时间一长,数据就变成了一堆对不上的记录。而 REA 要求你先想清楚“谁、动用了什么资源、发生了什么事、产生了什么影响”,模型里每个对象都有明确语义,后续做报表、做审计、做预测都会轻松很多。
这套思路最早是从会计领域长出来的,因为它天生关注资源的增减变化。但它的适用范围远不止账务处理,凡是需要记录“谁做了什么、影响了什么资产”的业务场景,比如库存周转、订单履约、设备维护、会员积分,都可以用 REA 重新梳理。我在制造业、零售业、软件项目的建模里都试过,一旦把流程转成 REA 结构,最明显的收益是:业务方和技术方终于能对着同一张图,讨论“这个事件到底由谁发起、改的是哪份库存、影响的是哪条账目”,而不是各说各话。
这篇内容适合正在做业务流程梳理的产品经理、需要做数据库设计的技术人员,以及想把“业财一体化”做扎实的架构师。我会先从 REA 的核心概念讲起,再用一个门店销售场景演示从建模到落表的全过程,最后把我在实践里踩过的坑和排查思路一并整理出来。整篇文章不堆理论,只讲怎么用,另外也会解释每个关键动作背后的原因。
2. 三个核心概念:资源、事件、代理人
2.1 Resource 资源:企业要管理和保护的东西
Resource 在 REA 里不是“资源”这个词在中文语境下那么宽泛。它特指企业希望记录、控制、并且能够被事件消耗或生产的对象。现金、库存商品、固定资产、原材料、服务工时、专利授权,这些都可以是资源。判断一个对象是不是合格 Resource,有一个很朴素的标准:假如某天这个对象消失了或者变少了,系统里能不能找到一条对应的事件记录来解释它去哪了?如果能,说明它确实被当成资源来管理;如果不能,那它更像报表上的一个汇总数字,而不是建模意义上的资源。
比如门店里的“库存商品”是资源,因为每次卖货都会触发“销售事件”,减少对应的库存量;但是门店里的“客流人数”就不是资源,因为你很难把它对应到某个具体事件的输入或输出上,它更像一个度量指标。这个区分看似简单,实际建模中很多争论都发生在这里。开发说“订单表有状态、有金额,这不就是资源吗”,销售说“订单是我们跟客户之间的一次交易动作,不该当成资源”。用 REA 的尺子一量,订单明显是“事件”的载体,它记录的是销售这个动作发生的时间、参与方和涉及的资源,而不是需要被消耗或生产的东西。所以在动手画图之前,先圈定 Resource 清单,能省掉后面大量的返工。
2.2 Event 事件:业务发生的那个“瞬间事实”
Event 是 REA 模型里最核心、也最容易混淆的元素。它描述的是“某时某地发生了什么事情”,这个事必须能回答几个问题:发生在什么时间?涉及哪些资源?由谁参与?造成了资源如何变化?一次销售是一个 Event,一次采购是一个 Event,一次固定资产报废也是一个 Event。Event 最大的特点是不可再细分,或者说它代表的是业务上的一次“原子事实”。这跟数据库事务里的原子性有点像,不用纠结到每条商品流,但至少要落到“一次扫码成交”而不是“上午卖货这个活动”。
为什么要强调“原子”程度?因为后续做审计追溯时,最怕的就是事件粒度太大。比如你记录“今天门店销售总额 3 万元”,这不够;明天盘点发现库存少了 30 件,你没法知道是哪个时段、哪个收银员、哪些订单造成的。反过来事件粒度太细也不行,比如把一次销售里的每一个扫码动作都拆成独立事件,就会带来大量重复数据,而且很多动作之间没有独立的业务价值。我在实践中掌握的分寸是:只要事件的发生会影响资源数量或资源归属,就值得单独建模;如果只是过程中的中间状态,就保留为事件下的字段,而不是独立事件。
Event 在 REA 里还天然分为两条流:流入事件(比如采购收货、原材料入库、客户退货)和流出事件(比如销售出库、生产领料、设备调拨)。这一层区分特别重要,因为后续所有账务处理、库存计算,本质上都是对这些事件做累加和抵消。你不用像传统会计那样先做凭证再对账,REA 本身就是一本可以反推凭证的业务流水账。
2.3 Agent 代理人:谁参与了这件事
Agent 指的是参与事件的人或组织。它既可以是企业内部的角色:收银员、仓管员、生产班组长;也可以是企业外部的交易对手:客户、供应商、物流承运商。很多业务建模工具里也会画角色和部门,但 REA 对 Agent 有更严的要求:每个事件必须至少关联一个内部 Agent 和一个外部 Agent。为什么?因为业务永远要能回答“这件事谁负责的”和“这件事和谁发生的”。只记录“张三销售给李四”,才是一个完整的商业事实;只写“销售出库单”而没有经办人和客户,就无法支撑后续的责任追溯。
有一种常见做法是把 Agent 等同于“操作账号”,比如系统里记录 create_by、update_by。但 REA 里的 Agent 更强调业务身份而非系统身份。同一个员工从门店调到仓库,账号可能没变,但业务身份变了,他参与的“销售事件”和“盘盈事件”在建模上就应该区分内、外部角色。如果项目里已经有组织架构表,可以直接把 Agent 拆成“内部参与者表”和“外部参与者表”,再通过事件关联起来,这样既不会破坏 REA 结构,也能兼容现有系统。
2.4 关系与基数:模型是不是严谨,就看关系怎么定义
REA 模型强调事件与事件、事件与资源、事件与代理之间的“关系”,而且专门有一类双亲关系叫“经济互惠”:一笔资源的流出,必然对应一笔资源的流入。最典型的是“销售”这个流出事件,会对应“现金收款”这个流入事件。这两个事件可以不是同一时刻,也可能不是同一形态,但必须成对出现。这个约束保证了业务永远不会“凭空变出库存”或“凭空消失资产”。
建模时最容易忽略的是关系上的基数(一个事件最少对应几个资源、最多对应几个代理)。我见过不少模型,事件和代理画了一条线就完事了,但没有标 1 对多还是多对多。结果系统上线后,发现一个销售事件可以被多个收银员同时认领,或者一条订单明细对应了三种库存批次,数据直接乱掉。所以我在建模初期会用一套硬规矩:每个“流出事件”至少要对应一个“流入事件”,且事件与资源之间至少要有一条“减少”或“增加”的关联;事件与内部代理之间至少有一条“负责”关联,与外部代理之间至少有一条“参与”关联。画完图之后先数这三类关系,缺一条都不过关。
3. 用 REA 建模一场销售业务
3.1 场景设定:一家门店的“收银台业务”
为了把前面这些概念落到地上,我用一个我实际做过的案例来讲:一家连锁零售门店,每天有多个收银员在 POS 机上完成收款。传统系统里的记录大概是这样:销售流水表,字段有订单号、收银员、商品条码、数量、单价、应收金额、实收金额、支付方式、下单时间。看上去很完整,但当你需要回答“这个月库存差异为什么出现在哪个收银时段”时,这张表完全帮不上忙。因为销售流水只记录了“卖了什么”,而没有建模“库存减少”和“现金增加”这两个事件之间的关系。
用 REA 重新梳理之后,流程变成三条主线。第一,收银员扫描商品,产生“销售出库事件”,这个事件减少“门店库存资源”,同时也作为整个交易的“流出侧”。第二,顾客支付现金或扫码,产生“收款事件”,这个事件增加“现金和银行存款资源”,作为“流入侧”。第三,收银员和顾客分别是内部与外部代理,两个事件都必须各自记录参与者。整个模型里不再有“订单表”和“流水表”这种含混的对象,而是变成一张语义清晰的事件事实表。
3.2 步骤:从业务描述中识别三元组并画关系
我在梳理中会按四个步骤走,每一步都有检查点。
第一步,圈出所有能改变资源数量或资源的归属的动作。在这个场景里,“顾客进店浏览”不能改变资源,“收银员扫码”能改变,“顾客付款”能改变,“顾客退货”也能改变。把这些动作全部列出来。第二步,为每个动作找“资源方向”:销售扫码让“门店库存”这个资源减少;收款让“货币资金”资源增加。如果某个动作既不能增加资源也不能减少资源,那它大概率是流程步骤,而不是事件。第三步,为每个动作找代理:扫码销售必须知道是哪个收银员发起的、哪个顾客购买的;收款事件同样要关联收银员和顾客。这里的关键是,不能把“销售出库”和“收款”合并成一个“销售收款事件”,因为业务上它们可以分开发生:顾客先拿货后付款,或者先预付后提货。如果合并了,就无法精确描述中间状态了。
第四步,画事件之间的互惠关系。销售出库事件对应收款事件,两个事件通过销售订单号建立“业务关联”。这在 R1EA 模型里是核心骨架:一个流出事件必须有一个流入事件配对。在我们场景里,如果顾客是用会员积分抵扣了一部分金额,那还要多建模一个“积分消耗事件”,它同样作为流出事件,与收款事件的部分金额配对。这一步看起来会增加工作量,但我实际做下来,它对财务对账帮助极大,因为每一条退款都能原路追溯到它对应的原始销售事件。
3.3 把模型落成数据库表:一份可直接参考的表结构
模型画完之后,最关键的一步是把它转成可落地的表结构。我不建议完全照搬 UML 图到物理表,而是保留 REA 的语义,用三组核心表来表达。
第一组:事件表。统一设计一张“业务事件主表”,包含 event_id、event_type、event_time、internal_agent_id、external_agent_id、related_event_id 等字段。event_type 用来区分“销售出库”“收款”“退货”“退款”等具体动作;related_event_id 用来建立事件之间的互惠配对。这里有个实现细节:related_event_id 不能只是备注字段,必须保证非空或者通过外键约束建立,否则后续追溯会断链。第二组:资源变动表。每一条事件明细都要落到“资源变动明细表”,字段包括 event_id、resource_id、direction(增加/减少)、quantity_before、quantity_after、unit_cost。这张表是库存和财务计算的数据源,也在需要时直接支撑“两套账合一”。第三组:代理表和资源表。代理表用一张“参与者维度表”统一维护内部员工和组织机构;资源表则保留“资源编号”和“所属仓库/门店”等维度属性。
实际建表时我还会加一条“事件快照”机制:在事件发生的同时,把当时的资源单价和参与人角色快照下来。这样即使后来源数据被修改或删除,回溯时仍然能看到当时的事实。这个坑是我在审计追溯时踩过之后才补上的,后面“常见问题”里我会再展开。
4. 常见雷区与我的排查经验
4.1 把“活动”当成“事件”
我接手一个模拟项目时,团队把“采购询价”和“供应商评估”都建成了事件。乍一看没问题,它们确实有参与人和资源。但细想之后会发现,“询价”并没有让任何企业资源发生增减,它只是为后续“采购订单”做准备。如果把它当事件建入库存或资金账户,数据体系里就会出现大量“中性事件”,它们不增加也不减少资源,却占用了事件流的位置。更麻烦的是,月底对账时每一笔询价都要去做资源平衡检查,但根本没有可配对的资源变化,导致系统报出一堆假差异。
我的排查经验很简单:回到最开始那个定义,问一句“这个动作会让哪个资源数量发生永久变化?”。如果答案是没有,就把这个动作降级成事件前的状态字段。比如询价结果就是“采购订单”这个事件上的备注,而不是独立事件。真实业务里总有大量“动作”,但 REA 建模只关心能造成资源流改变的那几个时刻。这个区分做对了,整个模型的简洁度会立刻提升。
4.2 把“收款”和“销售出库”合并成一个事件
项目初期最容易出现的错误是,为了省事,把销售出库和收款合并成一条“销售记录”。在财务简单的小店里确实可以这样做,但一旦业务出现定金、赊销、分期付款、退款,合并模式就崩了——你没法表达“货已经发给客户了,钱还没到账”的中间状态。如果硬在合并事件表里塞一个“待收款金额”字段,系统最终会变成一堆手工调账。
正确的做法是把“销售出库”和“收款”拆成两个事件,通过 related_event_id 建立配对关系。拆开之后,你会发现“应收账款的账龄分析”这项需求变得非常简单:凡是存在销售出库事件但还没有对应收款事件(或收款金额小于出库金额)的,就是应收账款。“预收账款”同理,只要有收款事件但没有对应出库事件的,就是预收。这套逻辑不用额外做一堆对账代码,直接查事件配对关系就能得出结果。我后来在另一个项目里把原来 3000 多行的手工调账逻辑全部删掉,就靠这两个事件的关系。
4.3 资源表里库存余额与事件明细长期不一致
这是最隐蔽的一个坑。很多人建了事件表之后,又在“资源表”里直接维护一个 inventory_balance 字段,每次库存增减时手工更新。这会让资源表同时承担“事实记录”和“汇总快照”两个职责,一旦某条事件在后续被删除或修改,资源表的余额却没有回滚,系统就会出现“有流水没库存,或者有库存没流水”的对不齐。
我的处理办法是:资源表只维护标识和属性,不存储余额;余额一律通过资源变动明细表的汇总计算得到。如果为了查询性能想要一个实时余额,我会单独建一张“库存快照表”,每天定时生成,而不是在资源表里让它和明细强耦合。这样做的好处是,排查差异时可以先比事件明细总和与快照余额,如果一致,说明逻辑没错;如果不一致,只需要重算,不需要在“哪条更新漏了”上耗费时间。我在实际运维里靠这个机制,把月度盘点差异从平均 3 小时缩小到 20 分钟。
4.4 专属排查清单:出现差异先查这四处
我整理了一个速查表,遇到数据对不上的情况,按照顺序查这四个位置。第一,检查 related_event_id 是否为空,空值往往意味着互惠事件没有配对,导致资源流出和流入无法抵消。第二,检查 direction 字段是否被误用,比如把退货的“增加”录成了“减少”,会让净库存翻倍。第三,检查事件时间是否被修改过,特别是回溯型的补录单,如果时间晚于业务实际发生时间,不仅会影响日结,还会让快照价格失真。第四,检查代理表是否被物理删除,有时候员工离职后被清掉,历史事件找不到关联方,整个审计链条就断了。这四步查完,80% 的账实差异都能定位。
5. 从模型到系统:REA 的工程化落地
5.1 用事件表驱动接口和状态机
做过 B 端系统的人都知道,订单状态机是所有开发人员的噩梦。今天加一个“已取消”,明天加一个“已部分退款”,后天又加一个“已弃审”。如果用 REA 的思路重新看,订单状态其实可以不要放在订单表里,而是通过“最近一次事件类型”推导出来。比如“销售出库事件已完成,收款事件未完成”的状态就是“待收款”;“收款事件已完成,出库事件未完成”的状态就是“待发货”。每个状态不再是一堆枚举,而是两个事件事实的组合。
我在落地时会把“事件表”作为系统里最底层的持久化记录,然后通过一个轻量的“状态投影服务”对外提供当前状态。客户端查询时读投影,写操作只追加事件,不回写状态字段。这个设计一开始看起来额外绕了一圈,但它带来一个巨大的好处:所有修改都有完整的审计记录,而且不需要为每个状态写一堆并发控制的 if else。业务要新增“部分退款”时,只需要新增一个退款事件,并在投影规则里加一条推导逻辑,不需要改订单主表。
5.2 与现有财务凭证的衔接
很多团队不敢引入 REA,是因为公司财务已经用了传统借贷记账,担心 REA 无法对接。实际上 REA 和借贷记账不是对立关系,而是“母与子”。REA 事件表相当于一个完整的业务事实数据集,借贷凭证则可以通过一组映射规则自动生成。比如收到现金这个事件,在 REA 里表现为“收款事件增加现金资源”,映射到借贷凭证就是“借:银行存款;贷:主营业务收入”。这里的核心不是怎么记两行分录,而是要让每一个 REA 事件都能按规则转换成一组完整的借贷分录。
我在实际项目中会在 REA 事件表和财务凭证之间加一层“会计映射配置”。配置项里规定每个事件类型对应的借方科目和贷方科目,并支持多条件表达式。比如“销售出库事件 + 库存计价方法为移动加权平均”自动生成本次结转的成本金额。这样做以后,财务人员在系统里看到的不再是手工录入的凭证,而是从业务事件自动推演出来的凭证摘要。如果需要反向审计,也可以从一张凭证反查出它对应的销售出库事件、收银员、库存批次,这种双向追溯能力是传统凭证系统很难提供的。
5.3 实际使用中让我最顺手的一个技巧
聊几个具体的工具和习惯。第一,建模阶段不要一上来就用 ER 工具画物理表,我习惯先用白板或者在线协同画图工具,画出“事件-资源-代理”的三层图,而且只让箭头表示资源和事件的增减关系。一旦涉及一对多关联,我会在关系线上直接标出“1”和“*”,这是后续检查遗漏关系的重要线索。第二,代码层面我倾向于给所有事件表增加一个“事件编号”,用业务日期加序列号组成,比如 20250318-001,而不是依赖数据库自增主键。这样人在排查问题时,可以拿着编号和业务方对话,比 203815 这种数字友好得多。第三,每次模型调整后,我会把旧的事件数据做一次“存档标记”,而不是直接物理删除。REA 的价值建立在历史事实的完整性上,破坏历史就等于拆掉了审计的根基。
最后再分享一个我在多个项目里反复验证过的体会:REA 不是银弹,它无法帮你解决“业务流程本身混乱”的问题,但它能非常高效地暴露业务流程里那些含糊不清的地方。每当我遇到“这个订单既算销售又算预收”“这张出库单到底是补货还是调拨”这类争论,我就默认大家的建模语言不一致。拿出 REA 图,先重新整理事件、资源和代理,绝大多数分歧都会在对话中自己消失。真正实践一次,你会发现模型的价值不在那张图上,而在图背后逼着你把业务说清楚的过程里。