
1. 项目概述三个字母里的业务数据建模入口第一次看到这个项目标题时项目组几个人都愣住了标题就“rea”三个小写字母没有附带任何业务文档。讨论了大半天后我们把思路收回到一个已经拖了两周的财务系统升级任务上——那一刻我突然意识到这三个字母完全可以落成“REA模型”也就是资源Resource、事件Event、行动者Agent组合成的业务语义建模框架。这个模型解决什么问题往大了说它能把一家公司的业务过程从“记账结果”还原成“业务实际发生的样子”。传统ERP里库存、应收、应付、采购、销售这些模块各有各的表模块之间靠字段兜着业务逻辑散落在接口里等你想做数据分析、跨部门流程梳理时经常要翻好几个月代码才能搞清楚一张表是怎么来的。而REA模型先从业务语义出发把每个环节拆成“谁Agent在什么事件Event里改变了什么资源Resource”这样建出来的数据模型天然能回答“这笔钱从哪里来、又变成什么了”。这篇文章适合正在做财务系统、ERP数据中台或者供应链数据分析的同学读也适合对业务建模感兴趣、想换一种视角看“流程”的人。我会按一次真实项目的节奏走一遍先拆解REA概念再做采购到付款的完整建模实操最后落到数据库表和踩坑记录上。整个过程不绕弯子可以直接拿着去试。提示REA模型虽然常被归类到会计信息系统AIS学术圈但它不只能做财务。凡是涉及“资源流动”的业务域比如电商库存、设备资产、订单履约都可以用同一套思路。2. 核心思路拆解REA模型到底在讲什么2.1 为什么老一套数据模型让人头疼很多业务系统的数据表是按“功能模块”建的采购订单一张表、收货单一张表、付款单一张表表之间再挂外键。表面上够用用的时候问题就来了。比如业务问你“某供应商的货这个月到了但还没付钱的一共多少”听起来很简单但你要先找采购订单再找收货单还要判断付款单有没有生成中间任何一处数据不一致结果就会差一截。更麻烦的是如果有人把“采购订单”和“收货单”混在一个流程里业务定义的边界不清晰下游的数据分析就只能靠猜。REA模型本质上是把“模块表”打散成三层语义要素资源、事件、行动者再把它们用业务因果关系连起来而不是靠操作菜单去组织数据。2.2 资源Resource是“有价值的东西”资源不是所有的名词必须满足一个条件它会被经济事件改变而且这种改变是业务关心的。典型的资源包括现金、存货、固定资产、服务工时。反例是“供应商”供应商虽然对业务很重要但它不是被销售、采购事件改变的对象它是行动者不应该被建模成资源。我踩过的一个坑是项目里有人把“合同”也当成资源。合同不会因为一笔订单而减少或增值真正被消耗的是合同对应的洪量额度或者履约服务所以后来我们拆成了“服务额度”资源合同本身成了约束条件。建议识别时多问一句“这笔业务发生后这个对象的价值/数量变了吗”变了才是资源。2.3 事件Event是改变资源控制的动作事件是REA模型里最核心也最容易被搞混的概念。它必须能直接对应到业务发生的那一刻比如“采购收货”“销售出库”“现金收款”而不是“审核”“批准”这种控制动作。为什么不能把“审核”算成事件因为审核不直接改变资源它改变的是流程状态。如果把控制动作也建模成事件模型里会出现大量不产生实际经济变化的“空事件”查询时还要反复排除。事件与事件之间会形成业务链采购订单只是承诺收货才让库存资源增加付款让现金资源减少应付增加。这里就有一个经验事件要按“是否最终生效”做区分比如“计划事件”和“执行事件”。REA模型里习惯把计划事件单独标记否则你会把一条采购申请和一次实际收货混在一个维度里。2.4 行动者Agent是事件的参与方行动者可以是人、部门、机构也可以是供应商、客户。它的关键作用是回答“谁来动这笔资源”。在一个完整的销售事件里至少有两个行动者销售员内部行动者和客户外部行动者。建模的时候最好把内部行动者和外部行动者分开建因为内部行动者往往是组织架构里的角色外部行动者则包含信用、地址、税率等属性混在一起会污染表结构。我的习惯是行动者表单独建一张用类型字段区分内部/外部事件与行动者的关联表同时记录“扮演的角色”比如”销售员“”审批人“”收货员”。这样后续做权限控制和组织绩效分析时就不用去猜字段含义了。2.5 三要素怎么串起来把三要素串起来就形成REA三元组行动者参与事件事件改变资源。这种表述看似简单实际建模时可以拆成两层关系资源与事件之间是多对多关系行动者与事件之间也是多对多关系。例如一次“销售出库”事件会减少库存商品资源同时增加“应收款”资源一个“收款”事件会减少现金资源同时减少应收款资源。事件还作为连接点把行动者、资源、以及前后事件串成一条完整链条。你会发现传统账本里的“应收账款”和“库存商品”在REA里都变成了不同时刻的资源状态而不是一张静态科目余额表。这种视角让我们能回答更多业务问题这批货对应哪次采购、哪个供应商、哪个采购员、收付款状态如何每一条都可以追溯到源头。这个“可追溯”是业务方最买账的价值点也是后面写SQL时实实在在的收益。3. 实操要点一个采购到付款流程的REA建模3.1 业务场景先定义到“能画出来”很多人一上来就列实体这是一种本末倒置。项目里我们第一步是选了一个高频业务——采购到付款圈定起点和终点从请购开始经历采购订单、收货、发票校验直到付款完成。画流程图时只画业务实际发生的事不画系统操作。这里有个经验先写叙事再建模。比如“采购员向供应商下订单仓库收到货物录入收货单财务拿到发票后核对数量价格到期后付款应付减少现金减少。”叙事里每个动词都可能对应一个事件每个名词都可能对应资源或行动者。先写文字再抽实体比直接画图清晰得多。3.2 识别出资源、事件、行动者对“采购到付款”流程我们最终识别如下要素类型识别结果说明资源采购数量对应的“商品”采购后形成的“应付账款”以及后续支付的“现金”商品是实物资源应付和现金是财务资源事件创建采购订单计划事件、收货入库库存事件、发票校验负债确认事件、支付货款现金减少事件事件必须直接改变资源状态行动者采购员、供应商、仓管员、财务审核人内部外部行动者要做区分这里最容易犯的错是把“发票校验”当普通流程跳过了。按传统模块思路发票校验只是财务里一张凭证单在REA模型里它其实是“确认应付”的关键事件直接影响负债资源必须单独建事件不能混在“付款”事件里。3.3 画关系与基数少一张关联表都会出问题识别出要素之后最关键的是明确基数。我按REA常用规则整理成下面这张表关系基数含义采购订单事件 → 商品资源多对多一张订单可包含多种商品一种商品可出现在多张订单收货事件 → 商品资源多对多一次收货可收多种商品一种商品可被多次收货采购订单事件 → 收货事件一对一或多对一一张订单可以对应多次收货但一次收货至少对应一张订单支付事件 → 应付资源多对多一笔付款可冲销多笔应付一笔应付也可被多次付款基数一旦画错后面建表会非常别扭。比如采购订单和收货单如果你把它们设计成一味的一对一遇到“一张订单分三次到货”的业务就彻底卡住反过来的多对多虽然让数据量变大但语义灵活业务再变也不会推翻模型。我的建议是宁可关联表多一点也不要为了查询“看起来简单”去压缩基数。3.4 事件链需要定义“计划事件”和“执行事件”REA模型里一张采购订单本身并不改变资源它只是“承诺未来会发生的事件”。如果你把订单也当作和收货一样的事件处理后续统计“实际库存”时会出错。正确做法是给事件表添加“事件类型”字段区分计划与执行然后用一种“事件连接”关系把它们串起来比如采购订单计划事件 → 收货执行事件采购订单 → 退款退货反向执行事件这个“事件连接”不是外键而是一张事件关系表用来表达“哪个计划引发了哪个执行”。做追溯分析时这张表是核心。实际编码时可以把连接的类别也存下来比如“计划关联”“冲销关联”避免将来分析时只靠猜。4. 落地实现从模型到数据库表4.1 物理表设计先从“事件表”开始概念模型完成后落地时不要按资源-事件-行动者三张表的错觉来设计。我通常做法是先建行动者表和资源表再建事件表中间用关联表把关系表达出来。agent_tb行动者表resource_tb资源表event_tb事件表event_resource_tb事件-资源关联表体现事件改变资源event_agent_tb事件-行动者关联表体现谁参与事件event_link_tb事件连接表体现计划执行、冲销等关系这套表结构与传统模块表最大的区别是所有业务动作都集中在事实表event_tb后续加需求不需要频繁修改表结构只要增加事件类型和关联数据就行。这一点在做财务系统升级时特别重要因为业务方经常变着法子加新型单据。4.2 核心建表SQL直接给出以采购收货事件为例我给出一个精简但能跑通的建表参考。字段命名按常见命名风格写大家根据自己的工程规范调整-- 行动者表 CREATE TABLE agent_tb ( agent_id BIGINT PRIMARY KEY, agent_type VARCHAR(20) NOT NULL, -- internal/external agent_name VARCHAR(100) NOT NULL, org_id BIGINT ); -- 资源表 CREATE TABLE resource_tb ( resource_id BIGINT PRIMARY KEY, resource_type VARCHAR(30) NOT NULL, -- goods/cash/receivable/payable resource_code VARCHAR(50) NOT NULL, resource_name VARCHAR(100) NOT NULL ); -- 事件表 CREATE TABLE event_tb ( event_id BIGINT PRIMARY KEY, event_no VARCHAR(50) NOT NULL, event_type VARCHAR(30) NOT NULL, -- purchase_order/receive/invoice/payment occur_time TIMESTAMP NOT NULL, fiscal_date DATE NOT NULL, event_status VARCHAR(20) NOT NULL ); -- 事件-资源关联表 CREATE TABLE event_resource_tb ( id BIGINT PRIMARY KEY, event_id BIGINT NOT NULL, resource_id BIGINT NOT NULL, quantity DECIMAL(18, 4) NOT NULL, unit_price DECIMAL(18, 4), direction SMALLINT NOT NULL, -- 1增加,-1减少 CONSTRAINT fk_er_event FOREIGN KEY (event_id) REFERENCES event_tb(event_id), CONSTRAINT fk_er_res FOREIGN KEY (resource_id) REFERENCES resource_tb(resource_id) ); -- 事件-行动者关联表 CREATE TABLE event_agent_tb ( id BIGINT PRIMARY KEY, event_id BIGINT NOT NULL, agent_id BIGINT NOT NULL, role_type VARCHAR(30) NOT NULL, -- buyer/vendor/warehouse_keeper/finance CONSTRAINT fk_ea_event FOREIGN KEY (event_id) REFERENCES event_tb(event_id), CONSTRAINT fk_ea_agent FOREIGN KEY (agent_id) REFERENCES agent_tb(agent_id) ); -- 事件连接表 CREATE TABLE event_link_tb ( id BIGINT PRIMARY KEY, from_event_id BIGINT NOT NULL, to_event_id BIGINT NOT NULL, link_type VARCHAR(20) NOT NULL, -- plan_to_execute / reverse CONSTRAINT fk_el_from FOREIGN KEY (from_event_id) REFERENCES event_tb(event_id), CONSTRAINT fk_el_to FOREIGN KEY (to_event_id) REFERENCES event_tb(event_id) );这里的direction字段很关键它表达的是“资源增加还是减少”。如果不区分方向后面做余额计算时就得靠事件类型去推断非常容易出错。建议在建表之初就强制约束任何一件改变资源数量的事件关联表里必须有明确的direction值。4.3 从事件明细到余额的聚合查询算“当前应付余额”不能直接读资源表因为资源表里没有“余额”字段。正确做法是对事件-资源关联表按资源做汇总SELECT r.resource_id, r.resource_name, SUM(er.quantity * er.unit_price * er.direction) AS balance FROM resource_tb r LEFT JOIN event_resource_tb er ON r.resource_id er.resource_id LEFT JOIN event_tb e ON er.event_id e.event_id WHERE r.resource_type payable AND e.event_status confirmed AND e.fiscal_date :stat_date GROUP BY r.resource_id, r.resource_name;这个查询看起来简单但两个细节必须注意。第一event_status必须是最终确认状态不能用未生效的草稿第二查余额时要过滤“计划事件”否则一张采购订单会提前把应付算进去。如果表设计时没有把计划/执行分开这个查询会非常痛苦。4.4 与会计科目表怎么整合REA模型与传统总账并行不冲突。实际操作中我在事件表里增加了一个voucher_id字段用来对应总账系统里的凭证号。这样既保留REA模型的业务语义又满足财务部门对外报送的需求。步骤是事件确认后根据事件类型生成日记账分录模板比如“收货”事件对应库存商品借方、应付账款贷方“付款”事件对应应付账款借方、银行存款贷方。注意REA模型负责“追溯业务事实”会计科目表负责“对外呈现结果”。两者不要做成一张表而是通过凭证号建立松耦合关联否则业务调整会直接影响财务编码后面升级又得返工。5. 常见问题与排查技巧实录5.1 事件多对多导致查询结果膨胀第一次按REA模型做报表统计时我发现数据量翻了几倍。原因很简单一个“收货”事件关联多行商品资源一个“付款”事件冲销多笔应付连接时不做去重就会出现笛卡尔积。排查后我给统计类查询统一加了一个原则先用事件维度汇总再关联资源维度不要一步join到底。比如统计“本月到货未付款”先去事件表过滤出收货事件再关联事件-资源表最后计算应付。如果一开始把付款表也同时连上一个收货事件被多次付款记录放大数字就会虚高。5.2 现有业务里找不到“资源”有不少业务只知道“事件”和“行动者”找不到明确资源典型例子是“客户咨询”。咨询事件发生前后客户账户里没有价值变化现金流也没动。这时候不要硬建模通常做法是把它归为“非资源事件”不参与资源平衡计算只在事件连接表里作为辅助信息。你看REA模型本来就不要求所有业务事件都改变资源非要塞进资源维度才会出问题。5.3 期初数据怎么进模型系统上线时原系统已经有库存余额和应付余额。那时候还没发生新的业务事件直接往事件表里补一条“期初导入”事件把所有存量资源挂上去。这样可以保持查询逻辑一致任何时点的余额都能用事件明细算出来而不是单独存一张期初表。这个操作的关键是“期初导入”事件要设置一个不影响业务发生的特殊日期比如上线前一天并且事件类型标记为opening_balance。如果后期需要回查业务历史还能清晰看到期初数据来自老系统而不是一条真实的采购事件。5.4 性能优化不能靠“少建关联表”REA模型的关联表很多查询天然比模块化表复杂所以一开始就要给大表做好索引。我的经验是三个必须索引事件表的fiscal_date和event_type事件资源表的event_id和resource_id事件连接表的from_event_id和to_event_id。业务查询一般围绕这几个键展开索引建好了亿级数据跑统计问题不大。如果报表特别依赖按商品汇总的余额也可以建物化视图定期刷新避免每次实时聚合。物化视图的刷新时间建议放在业务低峰期不要影响正常交易写入。5.5 团队不理解“事件链”怎么办这是实施中最常见的沟通问题。业务方老把“采购订单”当成核心单据认为事件表里必须体现订单状态。我的处理办法是先把“订单”当成计划事件再把“状态”设计成事件属性而不是单独建一张状态流转表。具体状态变化可以通过事件连接表来看下单计划事件、收货执行事件、退货反向事件事件链自然体现了状态流转。给团队培训时我反复用一句话概括忘掉你熟悉的功能菜单只关心“谁的什么动作改变了什么有价值的东西”。这句话落地后业务建模的争论少了一大半。5.6 遇到复杂场景怎么取舍如果你遇到的业务有“分批催收”“多次部分付款”基础REA模型反而更合适因为多对多天然支持部分冲销。真正难的是“资源形态变化”比如“原材料加工成成品”这涉及一个事件同时减少A资源、增加B资源。处理方式是用一个生产事件分别在事件资源关联表里挂两条记录一条direction为-1原材料一条direction为1产成品再用同一事件ID关联起来就可以追溯原料批次和成品批次的关系。不要试图建“生产任务表”那又会滑回模块化老路。6. 写在最后一点个人折腾心得从“rea”三个字母落地到一个可查询的REA数据模型整个过程最熬人的不是SQL怎么写而是逼着我们把业务定义清楚。传统表结构允许你掩盖模糊性比如“订单状态”字段不同部门理解不同REA模型不允许每个名词都要分清楚是资源、事件还是行动者每段业务都要写成“谁、做了什么、动了什么资源”。这个项目跑完后我把采购、销售、库存、应收应付都迁移到了REA模型上报表团队反馈最明显的感受是“终于不用再猜字段了。” 当然REA模型也不是万能的它更适合以“业务事实”为核心的记录和分析场景不适合做实时事务系统的主数据。假如你要做的是高并发交易入口还是让OLTP保持原样REA模型做分析侧的语义层会更稳。如果后面有人再丢给你一个看起来没头没尾的项目标题我的建议是别急着问细节先把它当成一次业务建模的入口把资源和事件拆清楚后面问题都会变得简单。