资讯动态

REA模型实战:从事件流重构业务数据建模,解决单据表难题

发布时间:2026/10/21 14:20:41 来源:尧图企业网站定制
做了十年业务系统设计我大概有四五年时间一直被一个问题反复卡住单据模型写起来快但一遇到业务规则调整就要动表结构流水账模型倒是灵活可月底对账、库存核算、成本分摊这些场景又变成一场灾难。直到我认真研究并实践了 REAResources-Events-Agents资源—事件—参与者模型很多困扰才开始真正解开。这篇文章不是教科书复述而是把这套模型放进真实项目里怎么用、为什么能用、踩过哪些坑做一个完整复盘。适合正在设计 ERP、进销存、财务系统、订单中台或者正被复杂业务规则折磨的后端工程师和架构师阅读。我第一次接触 REA 是在研究会计信息系统的发展历史时当时的第一反应是这不就是把复式记账翻译成对象模型吗直到把它落地到库存和资金管理项目里才意识到它的价值根本不在记账层面而在业务语义的统一表达。下面我从头展开。1. 为什么我放弃了单据表优先的传统建模方式先交代一下背景。某一次我需要为一套商贸管理系统重构核心数据模型业务覆盖采购、仓储、销售、财务对账。我翻了旧代码发现系统里最核心的就是一堆单据表采购订单表、采购入库单表、销售订单表、销售出库单表、收款单表、付款单表、费用单表。每张单都有状态字段从草稿到审核到完成再到作废。表面看很直观但实际上已经让开发团队苦不堪言。1.1 单据模型的三宗罪第一宗罪是业务规则一调整就要改表结构或状态机。比如原本一张销售出库单只有出库完成一种终态后来财务要支持先出库后开票那就得在出库单上再加一个已开票标记再后来要支持部分退货又得引入退货单并和原出库单建立关联。每加一种业务形态状态枚举、接口校验、报表查询就都要跟着动一遍。系统上线越久这些散落在各层代码里的分支判断就越难梳理。第二宗罪是单据之间靠人工关联很难保证一致性。典型场景销售出库 100 件客户退了 30 件换货 20 件这几个动作分别落在出库单、退货单、换货单上。月底对账时业务员和财务经常发现同一笔订单的库存扣减、应收冲销、发票金额三方对不上因为每个单据都是独立流程没有一条清晰的经济轨迹把它们串起来。第三宗罪是资产负债表、损益表等财务报告没法直接从业务数据生成。业务系统里的单据存的是订单金额实收金额这类业务口径数据而财务系统需要的是收入确认时点应收账款余额存货成本这类会计口径数据。为了让两者能对上团队不得不写大量转换逻辑而这些转换逻辑恰恰又是最容易出错的部分。1.2 流水账模型的另一个极端被单据模型折磨了半年后我尝试过把系统改成一刀切流水账所有业务动作统一写成一条流水记录字段包括业务类型、金额、数量、关联对象。这种方式确实灵活新增业务几乎不用改表结构但很快就暴露了新问题。流水账缺乏语义结构。一条库存减少的流水是为了销售、退货报废还是内部调拨系统里虽然在业务类型字段上做了枚举但枚举值越来越多查询报表时到处是CASE WHEN实际维护成本比单据模型还高。更重要的是流水账把库存变动和销售收入割裂成两条独立记录缺少了这一次销售同时导致库存减少和应收账款增加的天然关联导致月底核算时仍然要对账只是对账粒度从单据变成了分录。那段时间我反复琢磨一个问题我们到底缺的是更好的表设计还是更底层的业务抽象方法答案其实是后者。单据表和流水账试图直接从业务形态跳到数据库表中间少了一个业务语义建模的层次。而 REA 模型提供的正是这个缺失的中间层。2. REA 的三大核心概念资源、事件、参与者REA 最早是会计信息系统领域提出的企业建模框架核心思想非常朴素任何企业经济活动都可以抽象成资源—事件—参与者三类元素以及它们之间的关系。它不再以会计科目或单据为基本抽象单位而是以真实世界的经济现象为建模出发点。2.1 资源、事件、参与者到底指什么**资源Resource**是指企业所控制的、具有经济价值的对象。注意它不限于现金和存货还包括应收账款、固定资产、劳务工时、软件许可证、品牌的授权使用等一切能被交易或消耗的东西。资源的本质特征有两个可被企业控制且能带来或体现经济价值。**事件Event**是经济活动中可以观察到的、改变资源状态或控制关系的动作。比如销售出库、采购入库、支付货款、计提折旧、确认收货这些都是事件。事件是 REA 模型的核心因为时间是连续的企业状态的变化只能通过事件来记录。**参与者Agent**是参与事件的个人或组织。参与者分两类内部参与者和外部参与者。内部参与者指本企业的人或部门比如销售员、仓管员、采购部外部参与者指交易对手或关联方比如客户、供应商、银行、物流承运商。这三者的关系可以这样理解**事件改变了资源的流向或归属参与者则驱动着事件发生并承担相应的责任与权利。**REA 模型里还有一个非常重要的约束叫双重性指的是每一个经济事件都必须同时涉及至少一个资源的流入和一个资源的流出。换句话说企业里不会出现只有出没有进的经济事件一进一出的配对关系就是经济事件的天然完整性约束。2.2 用一组实际例子理解 REA我用一个最普通的图书销售场景来拆解顾客下单后图书出库这是一个销售发货事件。它导致库存图书这个资源的数量减少流出同时导致应收账款这个资源的余额增加流入。顾客支付书款这是一个收款事件。它导致 现金资源增加流入同时应收账款减少流出。仓库采购新书入库这是采购收货事件库存图书增加流入应付账款也增加流出。向供应商付款这是付款事件应付账款减少流入方现金减少流出方。把上面的事件画成图你会发现每一条事件都同时牵着两头的资源而参与者顾客、销售员、供应商、出纳挂在不同的事件边上。这种结构天然回答了谁在什么时间做了什么动作导致什么资源发生什么变化。我看到这个模型时的第一反应是这不就是我们每天在业务流程图里画的那些箭头吗对REA 的厉害之处就在于把业务流程中那些大家凭经验画出来的关系提炼成了一套有严格约束的建模语言让这些关系真正变得可校验、可推导、可落地到数据库。2.3 REA 与复式记账的关系很多人听说 REA 是会计领域来的会误以为它是复式记账的另一种表达。其实 REA 是比复式记账更底层的一层抽象。复式记账的每一条分录本质上是已经按借方/贷方套上了会计科目这个桶而 REA 模型先记录经济事件本身再通过规则推导出应该记到哪些科目。打个通俗的比方复式记账是已经拍好照片的相册每张照片都做了分类归档REA 是电影胶片它记录的是连续发生的过程。只要你拿到了完整的事件流随时可以从任意时点回放、重算出相册里的任何一张照片。这个差异在业务系统设计里意义很大因为企业的业务规则会比会计科目变得更快把事实和解释分层一套事件数据可以灵活支撑多种口径的财务报告和管理报表。3. 用 REA 建模一个完整的从订单到回款流程理论讲完了我们落地走一遍。我以一家做机电设备分销的公司为例完整演示怎么从一个真实的业务流程推导出 REA 模型。这个流程包括客户询价、销售订单、发货、客户签收、开票、收款还有后续可能的退货。3.1 先把业务流程拆成原汁原味的事件流建模的第一步不是画类图而是把公司实际发生的业务动作逐条列出来。我当时是拉着业务部的老同事连续聊了两个下午把各业务环节从头到尾盘了一遍。得到的事件列表大致是这样客户向销售员提交采购申请询价/下单意向销售员录入销售订单锁定商品和价格仓库按销售订单拣货并发货客户签收货物确认数量与质量无误财务依据销售订单和签收记录开具发票客户按约定账期支付货款财务收到款项并在系统确认到账这就是最原始的业务事件流。注意这里面已经出现了既是动作、又可能存在中间状态的内容销售订单在正式发货前只是一个承诺在发货后才变成实际经济结果开票和收款在时间上往往也不一致。这些在 REA 里需要区分清楚因为它们会在后续建模中对应到不同的资源流。3.2 识别每个事件涉及的资源与参与者带着事件列表我开始给每个事件标注资源和参与者。这一步骤是整个建模过程里最需要业务判断力的部分。销售订单这个事件比较特殊。在 REA 模型里订单本质上是承诺事件它还没有导致资源实际流出但已经形成了未来的资源流约定。因此它涉及两类对象作为资源的存货待发售货物和作为新增资源的客户订单承诺一种应收性质的约定权利。参与者是销售员内部和客户外部。发货事件要明确对应存货出库。这时库存商品资源减少同时在途商品资源增加参与者是仓管员和承运商。客户签收事件则是在途商品转为客户已收货商品实际上客户签收意味着交货义务履行完成此时应收账款资源增加。开票事件在 REA 里往往不单独改变资源数量而是改变资源属性未开票应收变成已开票应收但它仍然是独立事件因为发票状态本身影响税务和财务入账。收款事件是资金流入现金资源增加应收账款资源减少参与者是客户和出纳。逐步梳理完后我得到了一张包含资源和事件关系的完整图谱。每个事件前面都有一套入流和出流的资源配对销售发货事件存货流出应收账款流入、收款事件现金流入应收账款流出。这种成对出现的双重性保证了我不会漏掉任何一边的资源变化。3.3 抽象出可复用的模式做完一整个流程的建模后我发现了重要的规律不同业务场景里事件的资源流向模式是高度相似的。采购流程和销售流程在结构上完全对称。销售是存货流出、应收流入、收现流入采购是存货流入、应付流出、付现流出。一个是镜像一个是反向。有了这个发现我就把采购、销售、收付款、退换货这些流程抽象成几个通用的经济事件模板后续要接新的业务线只需要往里套资源实体和参与者而不用改动事件框架本身。这也正是 REA 相比单据模型最大的优势单据模型下销售单和采购单是两个完全不同的表字段和状态机都不相通REA 模型下它们其实是同一套事件机制的镜像表达建模语言是统一的。当时我把这套模型给后端团队讲清楚后团队里一个新同学只花了两天就完成了新增寄售业务的建模这在以前的单据模式下至少要两周。4. 把 REA 落地到数据库表结构、约束与性能设计模型画得再漂亮最终还是要落到数据库表设计上。这一部分我分享实际项目中的表结构设计思路、核心约束以及性能上的关键取舍。4.1 五张核心表可覆盖绝大部分业务我实践中看到一个非常稳定的模式只要设计好五张基础表就能承载绝大多数交易类业务。这五张表分别是资源表、参与者表、事件类型表、事件表、事件资源明细表一台事件对应多条资源增减变动。-- 资源表存储所有可被控制的资源 CREATE TABLE res_resource ( id BIGSERIAL PRIMARY KEY, code VARCHAR(50) UNIQUE NOT NULL, name VARCHAR(200) NOT NULL, type VARCHAR(50) NOT NULL, -- INVENTORY / CASH / RECEIVABLE / PAYABLE / WORK_IN_PROCESS unit_price NUMERIC(18,4), status SMALLINT DEFAULT 1 ); -- 参与者表存储内部参与者和外部参与者 CREATE TABLE act_agent ( id BIGSERIAL PRIMARY KEY, code VARCHAR(50) UNIQUE NOT NULL, name VARCHAR(200) NOT NULL, agent_type VARCHAR(50) NOT NULL, -- INTERNAL / EXTERNAL external_type VARCHAR(50) -- CUSTOMER / SUPPLIER / CARRIER / BANK ); -- 事件类型表定义经济事件模板 CREATE TABLE ety_event_type ( id BIGSERIAL PRIMARY KEY, code VARCHAR(50) UNIQUE NOT NULL, name VARCHAR(200) NOT NULL, in_resource_type VARCHAR(50), out_resource_type VARCHAR(50) ); -- 事件表记录每个具体事件的发生 CREATE TABLE evt_event ( id BIGSERIAL PRIMARY KEY, event_code VARCHAR(50) UNIQUE NOT NULL, event_type_id BIGINT NOT NULL REFERENCES ety_event_type(id), occurred_at TIMESTAMPTZ NOT NULL, internal_agent_id BIGINT REFERENCES act_agent(id), external_agent_id BIGINT REFERENCES act_agent(id), note TEXT ); -- 事件资源明细表记录事件对资源的增减影响 CREATE TABLE evl_event_line ( id BIGSERIAL PRIMARY KEY, event_id BIGINT NOT NULL REFERENCES evt_event(id), resource_id BIGINT NOT NULL REFERENCES res_resource(id), direction SMALLINT NOT NULL, -- 1流入, -1流出 quantity NUMERIC(18,4) NOT NULL, unit_price NUMERIC(18,4), amount NUMERIC(18,4) GENERATED ALWAYS AS (quantity * unit_price) STORED );这个结构的核心思路是一次事件一行记录事件明细表达资源的增减。你可能会问为什么不把资源和事件的关系放到事件表里两个字段而是要用一张明细表因为实际业务里一个事件经常涉及多种资源。比如采购收货事件通常同时增加库存商品和增加应付账款销售发货事件同时减少库存和增加应收账款。因此事件和资源之间的多对多关系就需要一张关联表来表达。4.2 库存余额怎么算REA 模型天然是面向事件流的它不保存余额余额永远由事件明细实时聚合计算。这也是 REA 的一个结构性优点余额永远不会和业务事实不一致因为余额是从事实推导出来的。但实时聚合在数据量大了以后会有性能问题。比如一个做了三年的商贸系统销售发货事件可能达到几百万条库存查询如果每次都用SUM(...) GROUP BY会慢到不可接受。我当时的做法是引入快照 增量重算机制。每批次业务发生后定时任务计算截至某个时点的资源余额保存为快照用户在查询库存时先取最近的快照再叠加快照之后发生的事件明细增量。快照粒度可以根据业务需求选择按天、按小时甚至实时更新。这样一个千万级事件量的系统库存查询的响应时间能稳定在几百毫秒以内。4.3 事件应该只增不改这是铁律在 REA 落地的过程中最重要的一条经验就是事件一旦写入就不允许修改和删除。业务上如果发生了输入错误正确的处理方式是新增一个反向事件来冲销而不是直接UPDATE原事件。我见过一个团队在这上面摔过跟头。他们允许运营同学直接编辑已确认的销售发货事件结果导致库存历史数据链断裂月底对账怎么都对不上。后来统一改成冲销事件流程后类似问题就再没有出现过。这条铁律背后有两个原因。第一事件是经济事实的原始记录事实是历史的一部分不应该被抹去第二审计追溯需要一条完好的证据链如果上游事件随意改变整条链上的后续事件都失去意义。这和区块链思想很像链上的每个区块只增不改靠新区块来修正共识状态。IT 系统里的经济事件记录也天然应该有这个属性。4.4 事件的不可变性对报表系统是很大的好消息因为事件不可变报表系统可以非常放心地做物化视图、做数据缓存、做时间维度上的任意切片。我当时把财务月报、库存汇总全部改成了基于事件流的物化视图每周重算一次配合增量日志把原来需要跑半小时的月末报表压缩到了五分钟以内。这在旧的单据模型下是几乎不可能的——因为单据会被无规律地修改数据仓库方根本不敢信任主系统的表数据。5. 三个生产项目里踩过的坑以及我给新人的建议理论讲完表结构也给了但我必须把真实项目中踩过的几个坑也一并讲出来不然你照搬模型可能会遇到完全想不到的问题。这些坑都不是 REA 理论本身的问题而是落地时容易忽视的边界条件。5.1 坑一把非经济事件也硬塞进事件模型第一个坑发生在第一个项目上线一个月后。业务方提了一个需求要在系统里记录客户投诉处理过程包括客服受理、内部流转、责任判定、回访关闭等环节。当时为了追求模型一致性我把投诉受理也建模成了 REA 事件结果发现它既不产生资源流入也不产生资源流出与双重性原则完全冲突硬编码下去只会让所有统计逻辑变得莫名其妙。后来我把这类流程性数据从 REA 事件中剥离出来回归到独立的流程工单模型。REA 的事件模型只保留真正产生经济资源变化的动作其他操作类、流程类的状态流转放在事件表旁边的辅助表里即可。你可以在事件表上增加一个可空字段关联到非经济类的业务流程记录但不能让流程记录伪装成经济事件。这个边界想清楚后系统的语义纯度立刻提高了。5.2 坑二过早引入承诺与履行的双层结构会造成过度设计REA 语义严格区分承诺事件比如销售订单和执行事件比如发货、收款这种区分在学术上有完整的理论支撑。但如果在项目一开始就把所有流程都拆成承诺/履行两层系统的复杂度会立刻翻倍。我的实践建议是分阶段引入第一版先只做执行层模型订单作为独立单据存在发货和收款建为 REA 事件等到系统运行稳定、团队理解了 REA 的价值之后再把订单也升级成正式的承诺事件与后续的发货、收款事件建立完整关联。这样做的好处是风险可控不会因为团队对 REA 理解不够而导致第一版就直接瘫掉。5.3 坑三多币种和多计量单位没有在建模时提前处理我在第二个项目里吃了大亏。项目有多币种业务但最初建模时所有金额字段只设计了币种代码没有把币种提升为资源的属性维度。导致后来做合并报表时美元金额和人民币金额不能直接汇总所有查询都得临时换算大量代码围绕汇率重算打转。教训是如果业务可能涉及多币种或者多计量单位必须提前把币种、单位设计成资源的属性并且定义好币种间的汇率关系表。最好在事件明细表里直接存原币金额和本币金额两个字段而不是等需求来了再加。这种事前期改模型是一天的事后期改数据是一季度的事。5.4 给新人推荐的落地路径如果读者自己也想在项目里实践 REA我个人的建议步骤如下先找一个业务量不大的模块试水比如收款或付款单一流程把它映射成事件模型同时保留旧的实现以做对照。然后写一套双向校验程序每天用旧逻辑计算财务余额用 REA 事件流重算同样的余额两者做差异对账。我对这个做法极其推荐因为它是检验模型正确性的最直接手段。只要差异为零说明你的事件建模和旧的业务口径完全一致接下来就可以放心地逐步迁移。当迁移完成之后你还可以做一件事来降维打击旧系统把对账逻辑本身删掉。旧系统需要一张一张单子去核对是因为数据孤岛导致的对账需求REA 模型天然把资源的增减和风险的转移绑定在同一条事件链上对账逻辑实际上变成了多余的确定性的计算而不是像以前一样是多方数据源的模糊归因。6. 从会计域扩展到了更广的业务场景最后我想聊聊 REA 模型在我后续项目中的外延。它并不只适用于会计、财务、进销存凡是需要准确描述价值流动的系统都可以借鉴这套思想。6.1 租赁业务的资产状态管理在某租赁系统项目中我把设备从可用库存到出租中到维修中再到报废的每次状态变化都建模成了事件每个事件都关联资源变体和参与者。在旧系统里资产状态是一个可随意修改的字段经常出现资产还在维修中却已被业务员租给客户的乌龙。改成 REA 事件流之后资产状态永远是从事件链上推导出来的设备被租出那一刻状态变换就是原子性的乌龙再也没有出现过。6.2 会员积分体系的账务化另一个有意思的案例是会员积分系统。积分本质上是企业发行的一种负债型资源用户获得积分和消耗积分都是经济事件。把积分体系用 REA 建模后积分的发放、过期、兑换、作废全部成为可追溯的事件流每一笔积分余额都可以追溯到是哪一次消费行为产生的。风控团队甚至提出过反馈系统的积分核算比某些财务模块还严格。所以我的最终体会是REA 不是一套固定的表结构而是一种思维框架。它教你用资源如何流动、事件如何发生、谁参与其中的方式去审查业务。只要抓住了这个框架无论面对什么系统你都能够更容易地画出那张经济活动图谱也更有把握地设计出既健壮又灵活的数据模型。如果你打算在下一个项目里尝试 REA我的建议很简单不要一上来就推翻现有系统而是先在一个小模块里证明它的价值用双重校验保证新旧口径一致再逐步扩大范围。相信我一旦体验过从事件流直接推导出资产负债表而不是靠一堆转换程序硬凑数字你就很难回头再去做那种到处是状态分支的单据系统了。

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

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

免费获取报价 →
↑