资讯动态

REA建模:用资源-事件-参与者重构业务数据模型

发布时间:2026/10/11 11:51:58 来源:尧图企业网站定制
做了一段时间业务系统设计和数据建模之后我发现自己越来越依赖一个被很多人忽略的框架——REA全称叫 Resource-Event-Agent也就是“资源-事件-参与者”模型。第一次接触到这个概念的时候我还以为它只是会计信息系统的一个学术理论后来在实际项目里反复使用才发现这玩意完全就是一套把业务过程“翻译”成数据结构的方法论。今天这篇就围绕 REA 本身聊聊它的核心思路、建模方式、实操实现以及我在项目里踩过的那些坑。REA 解决的最核心问题是传统会计记账法和业务系统数据模型之间的割裂。过去的设计习惯是“业务一张表财务一张表”业务数据进入系统之后先落业务库月底或者每周再靠对账脚本往财务库迁移一旦业务规则变化两边经常对不上。而 REA 的思路是从一开始就把所有业务活动抽象成资源、事件和参与者三类要素财务数据只是这些要素的派生结果。换句话说它不要求你先想好科目和借贷而是要求你想清楚“业务到底发生了什么事”。这个模型特别适合三类人看一是业务分析师需要把业务流程拆成可以落地的数据结构二是后端开发尤其是做订单、库存、资金这类核心域时需要一个稳定的建模底座三是对会计信息系统感兴趣的技术人员想搞明白业务事件和财务凭证之间到底是什么关系。就算你是刚入门的新手把下面这套思路看完至少能在做数据表设计的时候有底气说出“我为什么要这么设计”。1. 内容整体设计与思路拆解1.1 REA 到底是什么为什么要拿它建模简单来说REA 最早是从会计信息系统领域里长出来的一个概念模型后来慢慢成为企业信息系统做业务建模的一种通用语言。它把业务流程里的每一个事实都拆成三个部分资源Resource、事件Event、参与者Agent。先解释一下这三个概念。资源是业务过程中被交换、被消耗、被生产出来的东西比如商品、现金、原材料、工时甚至可以是某种服务权益。关键点是资源一定有经济价值并且可以被衡量。事件是业务过程中实际发生的活动比如销售、采购、收款、领料、生产、入库。事件通常对应谓语动词强调的是“发生了什么”。参与者是参与事件的主体可以是公司内部的员工、部门也可以是外部的顾客、供应商。参与者决定了责任归属也解决了权限和追溯的问题。这个三元结构最大的价值在于它迫使你把关注点从“记账怎么记”转移到“业务事实怎么描述”。举个最典型的例子卖货收款。传统做法是先写一张销售单再写一张收款单最后月底由财务人员把它们“配平”。REA 视角下销售是一个事件收款是另一个事件“销售”这个事件连接了商品资源和顾客、销售员两个参与者“收款”这个事件连接了现金资源和顾客、收银员两个参与者中间再用一个事件依赖关系把“销售”和“收款”串起来。这样整个链路就是天然可追踪的不需要另外建一张“对账表”来做匹配。传统的借贷记账法相当于给每个业务动作贴上“借”和“贷”两个标签用既定的科目体系去归类REA 则完全不关心借贷方向只描述资源从哪里来、到哪里去、由谁参与、发生在什么时候。所以在实操中REA 特别适合作为业务数据库的底层模型财务凭证反而可以作为它的一个视图或派生表来看待。1.2 传统记账模型与 REA 的差异在哪里为了更直观看到差异我把两种建模思维放在一起对比。对比项传统借贷记账法REA 模型核心问题每笔交易如何记成借贷分录业务中发生了什么事实涉及什么资源数据主对象会计科目、凭证、分录资源、事件、参与者设计起点用科目表猜测业务归类用业务流程推导实体关系业务扩展新增业务需要新增科目或改科目映射新增业务就是新增事件事件挂到资源和参与者上审计追溯依赖凭证号和明细账反查每个事件本身就是事实数据天然可追溯系统定位财务核算系统业务数据库或中台数据模型这组对比不是说要彻底抛弃借贷记账。财务核算里有大量成熟的规则和报表体系没必要为了“先进”而推翻。我更推荐的做法是把 REA 作为业务系统的存储模型和传输模型在展示层再按需物化成财务视角的数据。某个企业级项目里我们把订单域、库存域、资金域全部按 REA 建模事务落到库里的第一天就是“事件”月底生成报表时再去关联资源状态。1.3 为什么这种设计能解决问题我实际用它落地过一个订单加支付的项目一开始团队按常规思路设计了订单表、支付表、退款表、账单表每张表都要维护两套状态机业务规则一变状态机就跟着到处打补丁。后来整体改成 REA 模型后问题立刻清晰很多。订单、支付、退款都变成事件商品库存和账户余额是资源四个事件围绕同一笔销售行为自然形成一张事件链不用再固守“订单状态”和“支付状态”那套二元视图。这个模型能解决这类问题核心原因有三点第一事实数据是只追加的。事件一旦发生就落库不做物理删除审计和追溯都有据可查。第二资源状态可以通过事件推导。商品库存由“入库事件”和“出库事件”推导账户余额由“收款事件”和“付款事件”推导不会出现业务库里一个数、财务库里另一个数的情况。第三参与者提供了天然权限边界。任何事件都可以挂上“谁操作”“谁承担”“谁收益”做权限和数据隔离的时候直接用参与者字段过滤就行。2. 核心细节解析与实操要点2.1 从业务过程到 REA 实体我一般拿到一个需求先不看原型图和数据库设计先做一件事把业务流程用“主谓宾”句式描述出来。比如“顾客购买商品”“仓库发出货物”“财务收取款项”这三句话直接决定后面建哪些表和哪些关系。主语就是参与者宾语就是资源谓语动作就是事件。这个过程里最容易犯的错是把某些概念性对象直接当成实体表。比如“订单”在 REA 里并不是一个事件本身而是一组事件的载体。一个订单可能包含下单事件、发货事件、收款事件、退款事件。如果你把“订单”定义为事件后续所有动作都往订单表上塞状态模型就会膨胀。正确的做法是把“订单”设计成一个业务聚合标识或者叫业务流程实例真正的事实落在子事件上。再强调一下资源的判定标准。我内部喜欢用一个土办法判断这个对象是否能被“消耗”“生产”或“交换”。商品能被卖掉现金能被支付原材料能被消耗工时能被分配这些都可以作为资源。如果某个对象只是起标识作用比如客户编号、合同编号那它不是资源更倾向于作为参与者属性或事件标识。2.2 三类关系怎么连REA 有三组核心关系基本覆盖了九成建模场景。第一组是资源-事件关系也叫存量-流动关系。它表达的是事件对资源的影响比如销售事件会减少商品库存采购事件会增加商品库存收款事件会增加现金资源付款事件会减少现金资源。在数据库里这类关系通常体现为事件表里带资源外键或者通过明细表连接。第二组是事件-事件关系。这个最关键常常被忽略。事件之间不是孤立存在的它们构成业务链路。比如“销售订单”事件和“收款”事件是兑换关系和“发货”事件是依赖关系。在模型里我会给事件表增加一个父事件标识或者单独建一个事件关联表把依赖类型写清楚。第三组是事件-参与者关系。每个事件至少有两个参与者一方承担义务另一方接收成果。销售事件的参与者是顾客和销售员采购事件的参与者是供应商和采购员。多对多的关系需要单独建连接表同时保留参与者的角色字段比如“买方”“卖方”“经办人”“审批人”。实操中的链铁律是一个完整业务循环必须从一个资源的流入开始到一个资源的流出结束。如果设计完发现有些事件没有连接任何资源那就意味着业务描述还不完整。2.3 建模时的三个常见姿态真正动手建模的时候我习惯分三个姿态去推进。第一个姿态是“业务事件普查”。找业务文档、访谈记录、系统原型图把能识别出来的所有动词全部列出来。销售、购买、支付、退款、入库、出库、领用、归还、调拨、盘点、审批能列多少列多少先不管会不会用到。然后逐个判断它是否满足事件的两个硬条件一是确实发生在某个时间点二是能通过影响资源产生价值变动。第二个姿态是“角色归属梳理”。把每个参与者标注清楚是内部还是外部并确定它在事件里的角色。这一步决定数据权限模型。比如顾客是外部参与者订单数据只允许相关顾客查询销售员是内部参与者通过销售员字段做业绩统计和权限隔离。第三个姿态是“过程完整性校验”。从某个资源出发沿事件走一遍闭环。比如从“现金”资源出发应该能看到收款事件、付款事件、退款事件这些事件又关联到对应订单或采购单。如果走到一半发现某个事件是“悬空”的没有上下游关联那大概率建模遗漏了业务规则。2.4 事件与账务、审批、单据如何共存REA 模型不代表你就不需要单据编号、审批流和会计凭证。它们只是在不同的层面各司其职。单据编号是业务标识解决的是“这个流程实例叫什么”比如订单号、付款单号。审批流解决的是“这个事件有没有被允许发生”审批状态可以作为事件属性也可以放在事件关联流程表里。会计凭证解决的是“这个事件如何进入财务核算”在 REA 里它完全可以作为一个派生对象由事件和资源状态聚合生成。我在设计里的一贯原则是事件表永远保留一个业务单据号字段但单据号不是主键只作为业务流程标识。主键用自增ID或雪花ID。这样既保证了业务消息的可读性又不会让主键被外部业务规则绑架一旦单据号规则变化数据表也不用重建。3. 实操过程与核心环节实现3.1 场景设定一个最小化的销售收款系统为了把前面那套理论落地我设计了一个足够小却足够完整的案例某公司销售商品并收款。整个业务过程拆成四个事件客户下单、仓库发货、客户付款、销售退款。涉及三个资源商品、库存、现金。涉及三个参与者客户、销售员、出纳。这个案例能跑通 REA 的绝大部分核心逻辑。我们一步步来建表、插入数据、查询结果最后把这套结构推演成财务视角的应收和收入数据。3.2 数据库表结构设计先定义资源表和参与者表。-- 商品资源表 CREATE TABLE product ( product_id BIGINT PRIMARY KEY, product_name VARCHAR(200), unit_price DECIMAL(12,2), status SMALLINT ); -- 现金资源表 CREATE TABLE cash_account ( account_id BIGINT PRIMARY KEY, account_name VARCHAR(100), balance DECIMAL(14,2) ); -- 客户参与者表 CREATE TABLE customer ( customer_id BIGINT PRIMARY KEY, customer_name VARCHAR(200), contact VARCHAR(100) ); -- 员工参与者表 CREATE TABLE employee ( employee_id BIGINT PRIMARY KEY, employee_name VARCHAR(200), dept_id VARCHAR(50) );再定义事件表。这里我没有把订单做成一张大而全的状态机表而是把订单作为流程标识用销售事件和发货事件分别承载业务事实。-- 销售事件表 CREATE TABLE sales_event ( event_id BIGINT PRIMARY KEY, order_no VARCHAR(50) NOT NULL, -- 业务单据号用于业务沟通 event_time TIMESTAMP NOT NULL, product_id BIGINT NOT NULL, quantity INT NOT NULL, unit_amount DECIMAL(12,2) NOT NULL, total_amount DECIMAL(12,2) NOT NULL, customer_id BIGINT NOT NULL, sales_emp_id BIGINT NOT NULL, related_refund_event BIGINT -- 如果是退货记录原销售事件ID ); -- 收款事件表 CREATE TABLE payment_event ( event_id BIGINT PRIMARY KEY, payment_no VARCHAR(50) NOT NULL, event_time TIMESTAMP NOT NULL, amount DECIMAL(12,2) NOT NULL, account_id BIGINT NOT NULL, customer_id BIGINT NOT NULL, cashier_emp_id BIGINT NOT NULL, sales_event_id BIGINT NOT NULL -- 和销售事件的关联 ); -- 发货事件表 CREATE TABLE shipment_event ( event_id BIGINT PRIMARY KEY, shipment_no VARCHAR(50) NOT NULL, event_time TIMESTAMP NOT NULL, sales_event_id BIGINT NOT NULL, product_id BIGINT NOT NULL, quantity INT NOT NULL, warehouse_emp_id BIGINT NOT NULL );你可能会问销售事件表里已经有了产品、数量、金额为什么还要单独建发货事件表原因在于业务事实的时间点不一样。销售订单往往先发生发货后移发货数量也可能分批次。如果一个表同时承载“合同确认”和“物流交付”两种事实就不得不增加大量可空字段和状态位。拆开后每个事件表都干净未来接仓储系统也只需要对接发货事件。3.3 事件链的数据写入过程插入一笔完整业务我一般按真实时序逐个事件插入。先插入销售事件再插入发货事件最后插入收款事件。-- 创建一笔销售订单事件 INSERT INTO sales_event ( event_id, order_no, event_time, product_id, quantity, unit_amount, total_amount, customer_id, sales_emp_id ) VALUES ( 1001, SO-202501-001, 2025-01-10 09:12:00, 501, 2, 300.00, 600.00, 801, 901 ); -- 仓库发货事件 INSERT INTO shipment_event ( event_id, shipment_no, event_time, sales_event_id, product_id, quantity, warehouse_emp_id ) VALUES ( 2001, SH-202501-001, 2025-01-10 14:30:00, 1001, 501, 2, 701 ); -- 收款事件 INSERT INTO payment_event ( event_id, payment_no, event_time, amount, account_id, customer_id, cashier_emp_id, sales_event_id ) VALUES ( 3001, PAY-202501-001, 2025-01-11 10:00:00, 600.00, 601, 801, 702, 1001 );事务这块必须注意一个事件的插入应该和它所引发的资源状态变更放在同一个数据库事务里。比如发货事件插入成功的同时应该实现商品库存资源的减少。虽然库存能在查询时通过计算获得但在高并发场景下直接计算代价太大通常会在 product 表上维护一个存量快照每次事件发生时做增量更新。3.4 查询与报表生成REA 模型的查询思路和传统业务系统不太一样。传统业务系统查订单列表直接查订单表。REA 里则通过事件关联把事实串起来。查询某个客户的全部销售事件并关联其收款情况SELECT s.order_no, s.event_time AS sales_time, s.total_amount, p.payment_no, p.event_time AS payment_time, p.amount AS payment_amount, p.amount - s.total_amount AS unpaid_amount FROM sales_event s LEFT JOIN payment_event p ON p.sales_event_id s.event_id WHERE s.customer_id 801 ORDER BY s.event_time;如果要计算某个时间段内的收入总额既然定价金额是固定值直接用销售事件汇总即可。但更符合 REA 精神的做法是看现金流入也就是收款事件汇总SELECT DATE(event_time) AS biz_date, SUM(amount) AS received_amount FROM payment_event WHERE event_time 2025-01-01 AND event_time 2025-02-01 GROUP BY DATE(event_time) ORDER BY biz_date;这两个查询分别解决了“企业确认了多少销售”和“企业实际收到多少钱”两个口径。传统系统经常把这两个数混在一个表里月底对账全靠人的经验判断。REA 模型天然把口径分开再在报表层合并数据质量会稳定很多。3.5 延伸如何从 REA 事件推导会计分录有些读者会问我做了 REA 模型但公司还得用财务软件怎么衔接这类场景我通常再做一张派生视图把事件转成凭证分录的中间结构。原则也很简单资源流入记借方资源流出记贷方销售事件的资源流出是商品对应的收入贷方则在报表层确认。一张比较常用的查询如下SELECT s.order_no, s.event_time, 库存商品 AS debit_account, s.total_amount AS debit_amount, 主营业务收入 AS credit_account, s.total_amount AS credit_amount FROM sales_event s UNION ALL SELECT p.payment_no, p.event_time, 银行存款 AS debit_account, p.amount AS debit_amount, 应收账款 AS credit_account, p.amount AS credit_amount FROM payment_event p;这里的行为逻辑是从事件表出发先把业务事实沉淀在库里再在查询阶段按财务口径做投影。这样做的好处是未来财务口径变了只需要调整视图或报表逻辑不需要重构底层表结构。我在项目里测试过这套方案对审计追溯特别友好因为每个凭证数据都能反查到事件ID和单据号。4. 常见问题与排查技巧实录4.1 事件之间没有建立关联对账查不齐这是做 REA 最容易犯的错误之一。最开始做数据库设计时销售事件和收款事件各自独立中间没有外键也没有关联表。当时笃定“反正查数据的时候可以根据客户和时间去匹配”结果一到月底同一个客户可能有多个订单、多笔部分付款根本没法精确对到每一笔销售。后来老老实实在 payment_event 表里增加 sales_event_id 字段强制要求每一笔收款必须关联到一个销售事件。如果是合并付款、混合付款再单独设计拆分关联表。这个问题的本质是事件-事件关系必须先明确再建表而不是建完表之后靠查询临时拼。先确定业务规则一单对一收、一单多收还是多单对一收。不确定的时候宁可多建关联表也不要省字段。4.2 库存和余额总是和历史单据对不上这个问题几乎每个项目都遇到过。最典型的情况是业务系统里退款的时候把支付记录硬删掉了结果资金流水出现空洞两边统计差一分钱。REA 模型明确要求事件不能物理删除只能追加反向事件。退款不应该把原有收款事件删掉而是新增一个退款事件金额为负数或者用正数配合事件类型字段来区分。我在项目里给出的规范做法是三条事件表只允许 insert不允许 update 关键字段或 delete。需要更正错误时使用补偿事件把“原事件作废”标记写入关联字段。资源余额用事件增量视图累加或者用物化视图定期刷新。做了这三条之后对账问题基本消失除非并发处理时顺序错了。并发场景下要给同一个资源加锁比如操作现金余额时必须锁住对应 account_id 的行。4.3 传统期望的“订单表”没了业务方不习惯还有一个偏管理的坑。业务方已经习惯了打开后台直接看到一张订单列表一页展示订单号、状态、金额。改成 REA 之后这些数据分散在多个事件表里业务方会觉得难以理解。我的处理方式是保留聚合查询视图。建立一个订单视角视图内部通过关联销售事件、发货事件、收款事件聚合出订单主状态。业务方看到的仍然是订单列表但底层已经是事件表。这样既维持了用户使用习惯又保障了数据模型的健壮性。这种做法的代价是需要多维护一层视图但换来的是底层模型不用妥协。多年的实际经验告诉我底层模型一旦被用户的习惯牵着走后面再改的成本会指数级上升。4.4 常见问题和解决方式速查现象可能原因解决思路收款无法对应订单事件未建立关联给事件表增加事件依赖关系字段或关联表库存对不上直接修改/删除资源记录改为事件追加资源状态由增量计算财务口径不一致订单金额和实际收款混存按事件分类查询一个口径一个口径独立确认状态无限增多把整个流程塞进一张状态表拆事件表每个事件描述一个业务事实权限混乱参与者没区分内外统一事件-参与者关系表增加角色字段追溯断链原事件被打补丁覆盖保留事件历史表新事件挂关联ID报表读取慢查询时逐级计算增加物化视图或事件汇总表按日/小时跑批5. 我踩过几次坑之后的实操体会5.1 不要用字段“微创新”去偏离模型刚开始理解 REA 的时候我也很想保留熟悉的东西于是设计了所谓的“订单事件表”把所有和订单有关的动作都挂到一张表用十来个状态字段去区分动作。表面上看没有脱离 REA 的框架实际上已经变成一张巨型事件表查询越来越慢规则越来越绕。后来改成每个动作一张独立事件表表和表之间通过关联字段连接结构清爽了很多。所以我想强调一个判断标准如果一个事件下还可能再拆分出不同时间点的事实那就必须拆表。一个事件对应一个时间点、一个资源影响、一个参与者集合。如果状态字段超过五个大概率事件设计已经错了。5.2 资源快照和事件流水要配合使用纯 REA 的极端做法是根本不存库存余额每次查询时把事件全部累加一遍。这在数据量小的时候没问题但线上系统压力一大就扛不住。我的做法是资源表里快照一个存量值每次事件发生时在同一个事务里做增量更新。这个快照不是严格必要的数据查询时可以用事件校验但日常访问性能就靠它顶住。这里有一个很重要的校准逻辑每个月或者每次对账时一定要做一次全量事件累加和快照值对比。不一致的话要优先相信事件流水再回溯是哪次并发更新丢了。5.3 后续如果要上事件溯源架构REA 是天然铺垫最近两三年我越来越明显感觉到REA 模型和事件溯源、CQRS 这类架构风格的匹配度极好。事件表本质上是事实流资源状态是投影。如果项目下一步计划引入消息队列或事件总线REA 模型完全可以直接把数据库里的事件表映射成消息事件流。参与者角色又能天然用来做字段级的权限控制。如果你现在的系统还在起步阶段我的建议是先把 REA 作为数据库建模的内核不需要一开始就上复杂的架构组件。把底层事实沉淀好将来上什么技术框架都是顺水推舟的事。最后再补充一句个人感受REA 不是银弹遇到极简的内部小工具或者纯报表系统杀鸡焉用牛刀。判断标准就是看业务的复杂度和链路深度如果你的业务里事件与事件之间存在长期依赖、资源状态会被多方口径使用REA 基本就是最适合你的建模起点。

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

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

免费获取报价 →
↑