资讯动态

REA建模实战:资源事件代理模型如何搭稳进销存与对账体系

发布时间:2026/10/19 14:56:02 来源:尧图企业网站定制
如果你在系统设计笔记里看到“REA”三个字母很可能是在某个进销存、财务或供应链相关的讨论里。我最近恰好帮一个传统制造业客户梳理数据模型业务方被月底对账折磨得不轻采购系统、库存系统、销售系统各有一套台账合并之后账实不符财务只能逐笔翻凭证。后来我们终于把争论焦点收拢成三个英文词Resource、Event、Agent也就是 REA。这篇内容不是概念复读而是把我实际建模时的判断依据、表结构、踩过的坑一次讲清楚适合正在设计业务系统或做领域建模的开发者、产品经理和业务分析人员。1. 从一套让财务抓狂的借贷分录说起1.1 借贷记账为什么在业务单据上显得别扭传统进销存系统里底层数据大多是按“借/贷”思想设计的。我之前遇到的一个库存模块每次采购入库系统先生成一张“借库存商品、贷应付账款”的分录。仓管看“入库单”觉得货物多了财务看“分录”觉得应付多了同一个业务事实被拆成两套语言。等到月底核对入库单数量是5但分录金额用的是加权平均单价和当前采购单价对不上问题就来了。业务量小的时候手动调账还能忍。一旦SKU上千、供应商几十家、退货换货频繁这种“业务动作→会计科目”的转换链会变得极长。每个环节的换算规则都可能被业务方临时改写模型漂移几乎不可避免。我后来也想过直接优化会计分录但发现解决不了根本问题我们缺的不是更好的科目体系而是一个更贴近业务事实的中间模型。这是我转向REA模型的核心原因。1.2 REA的基本三分法资源、事件、代理REA模型的大思路其实不复杂。它认为任何一个业务系统都可以围绕三个关键词来建模资源会被业务事件增加或减少的东西例如库存商品、原材料、现金。事件业务发生的瞬间动作例如采购入库、销售出库、付款。代理参与并通过事件影响资源的个人或组织例如供应商、客户、仓库管理员。三分法的最大价值是让模型跟着“业务发生了什么”走而不是跟着“账上该怎么写”走。库管说“入库”REA里就是一个事件这个事件关联某个资源物料批次同时关联两个代理提供方供应商、接收方仓管。后续财务无论怎么定义科目这个原子事实都不会被破坏。1.3 为什么要把事件放在模型中心传统模型通常以“单据”或“对象”为中心REA偏偏把事件放在中心位置。实际原因是单据是人对事实的包装可能合并、可能拆分、可能事后补号而事件是业务世界里真实发生的一个动作一旦发生就不能消失。以事件为中心后续对账、审计、分析都围绕事实展开。我在项目里经常提醒团队“先别急着设计表把业务发生过的所有动作列出来给每个动作标上它改变了哪个资源、由哪些代理参与模型就已经出来一半了。”这句话听起来简单实操中能避免大量返工。2. 资源、事件、代理的边界判断该建模到什么程度2.1 资源的边界能不能被事件增减最常犯的错是把“任意业务实体”都当成资源。判断标准其实很朴素只有当这个对象会成为某类经济事件的增量或减量时它才是一个REA资源。我在各种需求评审中总结过一组常见候选对象| 候选对象 | 是否作为资源 | 判断理由 | | 库存商品 | 是 | 采购入库增加它销售出库减少它 | | 仓库 | 否 | 仓库本身不被事件“增减”除非专门做容量管理 | | 供应商 | 否 | 供应商是代理不是资源 | | 库存批次 | 是 | 批次库存会被入库、出库、报废事件增减 | | 合同 | 否 | 合同是承诺载体不是被增减的对象 |有一种例外值得注意如果你专门做仓储容量管理关心“可用库位”的增减那么库位容量可以建模为资源。我见过一个模拟项目把储位当资源处理空箱回库场景时非常顺手。这说明资源判断要结合业务事实而不是套规格。2.2 事件的粒度按行拆分还是按单合并事件粒度直接决定模型粗细。一张入库单可能包含多个物料、进入多个库位如果整张单作为一个事件后续想查“某批次在一号库位还剩多少”就无从下手。我建议按业务中最小的对账单元来拆事件。判断事件粒度是否合理可以问四个问题这个动作发生在什么时间它影响的是哪个资源实例哪个批次、哪个库位谁提供、谁接收、谁负责只保留这一条记录能否完整回答上面三个问题四个问题都能回答粒度就是合适的。同一张单据可以按物料、库位拆成若干事件同时保留单据号作为聚合字段供界面层拼装成一整张单据视图。这样做的好处是向上可以聚合向下可以追溯模型不会因为粒度过粗而失去灵活性。2.3 代理的角色内部代理和外部代理都要留下每个事件通常会涉及两类代理外部代理供应商、客户和内部代理采购员、仓管、业务员。REA模型中事件与代理的关系要成对出现谁提供资源谁接收资源都必须记录。实际项目中最容易遗漏的是内部代理很多人觉得“仓管是谁不重要”等到盘点差异需要追责时才发现缺少责任人信息。代理也不应该无脑建到“员工个人信息”的粒度。只有当需要按具体责任人追责时才把“人”挂到事件上如果只是做供应商账龄分析把外部代理的组织级信息挂到事件上就足够了。这个尺度拿捏比建模型本身更考验经验。3. 一个完整业务链的REA建模拆解采购入库到销售出库3.1 设定一个典型场景假设某公司做零部件进销存核心流程大致如下采购员向供应商下采购订单供应商发货仓管收货、检验、入库形成库存车间领料加工成半成品完工后入半成品库业务员接客户订单仓管按订单出库发货客户收货后付款公司收到货款。业务痛点概括成一句话月底账实不一致说不清某个批次物料到底消耗到了哪里。3.2 第一步先列事件再补资源和代理照我的做法先不看原来的库表和字段只用“事件”统一描述所有业务动作。列出主要事件后再给每个事件补充资源和代理| 事件 | 受影响资源 | 资源方向 | 内部代理 | 外部代理 | | 采购入库 | 原材料批次 | 增加 | 仓管接收 | 供应商提供 | | 生产领料 | 原材料批次 | 减少 | 车间接收 | 无 | | 完工入库 | 半成品/产成品 | 增加 | 车间提供、仓管接收 | 无 | | 销售出库 | 半成品/产成品 | 减少 | 仓管提供 | 客户接收 | | 付款 | 货币资金 | 减少 | 财务提供 | 供应商接收 | | 收款 | 货币资金 | 增加 | 财务接收 | 客户提供 |这张表写作时非常简单但它是整个系统的骨架。我还会额外加一列“原始单据号”方便后续把事件和采购单、销售单、收付款回执一一对应。3.3 第二步补齐承诺保证经济交换闭环仅仅列出事件还不够。一个完整的REA模型要求经济交换中必须同时存在“给出资源”和“收到资源”两个镜像事件否则经济链条是断的。比如采购供应商发货是资源流入我方付款是资源流出中间还应该有“应付承诺”把订单和后续付款关联起来。我当时的落地办法是在事件之外增加“承诺记录”订单、合同并将其与后续经济事件关联。这样账龄分析、供应商对账都能直接引用承诺数据不用靠人工猜测。库存模块的账实核对也因此简化了每一条库存余额变化都能追溯到事件以及事件对应的原始单据。3.4 第三步把模型翻译成听懂的人话最后一步把三元组关系翻译成业务语言。假设一批钢材3月1日入库100件3月5日出库30件用REA表达就是采购入库事件E1资源R1增加100件供应商A1提供仓管A2接收。销售出库事件E2资源R1减少30件仓管A3提供客户A4接收。累计剩余库存100 - 30 70件。把这张表放在项目评审会上业务方只要扫一眼就能理解。最直观的效果是双方不再因为“科目名称”争来争去而是直接看着事件流确认业务事实。这是REA带给我的最大红利。4. 落地设计与查询把REA变成数据库里的表4.1 用只增事件表替代库存余额表传统方案很喜欢设计一张“库存余额表”每次出入库直接更新余额查询时读余额简单直接。但余额表是状态快照一旦写错你很难知道它为什么错。REA落地则不同只追加不可变的事件记录余量通过事件计算得出。最小表结构分成四块资源表、代理表、事件表、关联表。关联表负责把事件与资源、事件与代理挂起来形成多对多关系。4.2 最小建表SQL示例我用结构相对清晰的PostgreSQL语法做个示例方便直接参考create table rea_resource ( id bigserial primary key, resource_type varchar(64) not null, name varchar(255) not null, unit varchar(32) ); create table rea_agent ( id bigserial primary key, agent_type varchar(64) not null, name varchar(255) not null ); create table rea_event ( id bigserial primary key, event_type varchar(64) not null, occurred_at timestamp not null, source_doc_no varchar(64) ); create table rea_event_resource ( event_id bigint not null references rea_event(id), resource_id bigint not null references rea_resource(id), direction smallint not null check (direction in (1, -1)), quantity numeric(18, 4) not null, primary key (event_id, resource_id) ); create table rea_event_agent ( event_id bigint not null references rea_event(id), agent_id bigint not null references rea_agent(id), role varchar(64) not null, primary key (event_id, agent_id) );这里 direction 用 1 表示资源增加-1 表示资源减少quantity 永远填正数。查询当前库存时直接对direction * quantity做求和即可。如果一个事件会对同一个资源实例产生多条不同的增减记录比如同一批原料在同一事件里既有报废又有转库可以给关联表加一个 seq 序号字段避免主键冲突。4.3 库存余量计算与逐笔追溯查询查询当前库存余量用一段带分组的SQL就能搞定select r.id, r.name, sum(er.quantity * er.direction) as balance_quantity from rea_event_resource er join rea_resource r on r.id er.resource_id group by r.id, r.name having sum(er.quantity * er.direction) 0;如果想针对某个批次做逐笔追溯把事件按时间排序重放即可select e.id, e.event_type, e.occurred_at, er.direction, er.quantity from rea_event e join rea_event_resource er on e.id er.event_id where er.resource_id 1001 order by e.occurred_at, e.id;生产环境数据量大时全表聚合会变慢。我建议定期用物化视图或批处理任务生成库存快照同时保留事件明细供追溯。查询层读快照审计层读事件明细两者互不干扰。4.4 不可变事件带来的审计优势事件不可变意味着任何库存异常都能被精确复盘。某批次库存出现负数逐条回放事件流立刻就能定位是入库漏录还是出库多录。对账时报表数据可以基于事件流重新计算不需要担心历史被覆盖。我在实际项目中尤其喜欢“单据号”字段它把事件和原始业务单据连接起来。财务问“这批货怎么少了”我们不再回答“可能是上月调账调的”而是直接给出事件意图链采购入库→生产领料→销售出库每一步都有据可查。5. 我见过的三个典型误用以及对应自查方法5.1 把一切对象都当成资源模型迅速膨胀有些同学学完REA后什么表都往资源里放客户建资源、合同建资源、仓库建资源。结果模型越来越大查询逻辑复杂到没人敢改。判断标准仍然是那条这个对象会被事件增加或减少吗客户不会因为销售事件变少一个合同不会因为付款事件变多一份所以它们都不是资源。资源必须是经济事件直接作用的载体。5.2 事件粒度拍脑袋一张单据等于一个事件把整张入库单当一个事件的做法短期看数据量小没毛病长期会卡在对账上。一旦要查“某个批次某天在某库位的结存”事件粒度太粗就查不出来只能重新改历史数据。我会坚持按最小对账单元去拆事件即使最初多出一些记录。事件拆细了后续聚合视图可以做得和过去完全一样但如果一开始拆粗了再想拆细就得翻工。5.3 只建模了交换没建模承诺还有个常见问题是只记录“入库”和“付款”忽略中间的订单承诺。结果就是应付款对账时财务要另外维护一张Excel台账因为系统里找不到“这笔应付对应哪个订单”。补齐承诺记录后整个链路变成订单承诺 → 采购入库 → 付款结算。这三个环节串起来账龄分析、供应商对账都是现成的数据根本不用额外加工。5.4 每次建模前过一遍自查清单我现在做REA建模前都会带着业务方过一张自查清单| 自查问题 | 通过标准 | | 这个对象会被事件增减吗 | 只有“会”才建模为资源 | | 事件能否回答时间、资源、代理、方向 | 完整回答才算粒度合适 | | 代理是否成对记录 | 不能只有提供方或接收方 | | 资源增减是否大致守恒 | 加总所有增减应与盘点数量对得上 | | 每个事件能否追溯到原始单据号 | 每行都有 source_doc_no |这套清单帮我拦下了不少“看起来很对但落地就会炸”的设计。如果你也在做类似的建模建议把它贴到需求文档首页。6. REA与事件溯源、领域驱动设计放在一起看6.1 事件溯源状态不是第一性过程才是事件溯源的核心是“用事件日志重建状态”REA几乎天然符合这种思想。REA的事件表本身就可以当作事实日志资源余量是事件流投射出来的状态。区别只在于事件溯源偏基础设施层关注如何重放REA偏业务建模层关注事件如何分类和关联。如果你已经用事件溯源做系统把REA概念套上去几乎无缝。资源对应聚合里的状态对象事件对应领域事件代理对应参与者。6.2 领域驱动设计REA是一种高效的分析模式做领域驱动设计时大家经常纠结聚合边界怎么划。REA能提供一套现成的分析骨架订单聚合可以映射为一个承诺事件履约过程映射为经济事件资源是聚合内需要保护的不变量。我开工作坊时会让团队先用REA画出事件流和代理关系再套DDD的战术建模。画完之后哪些对象该进哪个聚合、哪个事件该作为聚合根通常比硬想清晰得多。REA和DDD并不冲突反而像一个前置过滤器帮团队先统一业务语言。6.3 我什么时候选用REA什么时候明确不用| 系统类型 | 是否建议用REA | 原因 | | 进销存、财务、供应链 | 建议 | 强对账、强审计事件流能支撑复杂追溯 | | 简单内容后台 | 不建议 | 普通CRUD足够引入REA会增加大量成本 | | 高并发交易系统 | 看情况 | 建模结构可用但查询层要配合快照设计 | | 数据仓库贴源层 | 建议参考 | 事件流贴近原始终端事实比直接挂汇总表灵活 |没必要让所有模块都REA。我见过一种务实的混合做法核心库存与资金模块用REA建模订单展示层、用户界面查询仍用普通读模型或物化视图。这样既保住审计和追溯能力又不至于把每个页面都拖慢。最后说一个体感很强的事REA不是给你多一张表而是给你一个“必须先讨论清楚业务动作”的约束。我在实际项目里遇到争议时通常掏出一张白纸让所有业务方把动作写下来标上资源变化和两方代理。往往写不到一半争论就自然消失了。如果你正要改造一个对账混乱的系统可以先用Excel模拟一周的事件流把每个资源增减汇总算一遍确认守恒后再写建表语句。模型复不复杂不重要重要的是它能替你说话。

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

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

免费获取报价 →
↑