资讯动态

REA模型:用资源、事件与参与者重构业务系统设计

发布时间:2026/10/20 17:25:38 来源:尧图企业网站定制
看到“rea”这个代号的第一反应我以为是哪个懒人把项目名打错了。直到把三个字母补全为 Resource、Event、Agent我才意识到这其实是一套业务建模思想。它不是什么开源框架也不是最新的网络热词而是一套在会计与业务系统设计中被反复验证的语义模型。最近我在重构一套进销存系统时就用它理清了那些纠缠不清的订单、库存和流水今天把这段实践和思考完整拆出来。如果你正在做订单系统、库存台账、财务对账或者一块后台管理模块多半会遇到同一类痛点表结构越加越多、业务口径每个人说得都不一样、月底对账总是差几块钱还找不到原因。REA 模型恰好能帮你在设计源头把“业务事实”和“统计口径”分开让系统在很长一段时间内改需求不伤筋动骨。下面这篇文章适合后端开发者、系统架构师也适合跟研发沟通的产品经理。1. 拿到“rea”这个代号我第一反应是查字典1.1 三个字母背后的完整定义REA 拆开是 Resource、Event、Agent中文通常翻译成资源、事件、参与者。它最早是会计信息系统领域用来描述经济业务的一套语义模型核心思想很简单商业活动本质上就是一群参与者Agent通过经济事件Event去交换或转换资源Resource。这句话看起来像废话但真往业务里一放很多传统设计就会变形。举个例子一张销售订单传统表设计会把客户、商品、数量、金额、业务员、备注全部塞进一张订单表里。当时觉得挺顺后来发现同一客户可能在不同区域下订单同一商品在不同活动里价格不同订单上还要挂各种审批状态、物流状态、开票状态。表越拆越多但业务逻辑还是绕成一团。用 REA 的思路看订单并不是核心。核心是“发货”这个事件谁把什么资源发给了谁以及“收款”这个事件谁从谁那里收到了现金。订单只是这两个事件之间的一个承诺不应该过度承担建模职责。把目光从“表单”挪到“事件”上设计思路立刻就不一样了。1.2 为什么偏偏选它做业务系统我之前在某公司负责一套跨平台进销存系统最头疼的不是功能做不出来而是对账对不上。采购部看库存财务看应付销售看毛利都是同一笔业务三个部门能产出三个数字。后来回看底层表问题很清晰库存表存了当前数量订单表存了订单状态收款表存了到账记录这些表各自维护“余额”只要一个环节漏更对账就崩。REA 模型给了一个很极端的约束余额和汇总一律不落表只记录事件本身。库存数量不是字段而是所有“入库”事件减去所有“出库”事件之后计算出来的结果应收账款不是一张表里的某个数字而是“发货”事件和“收款”事件对比出来的差额。听起来会增加查询负担但在现代数据库和缓存面前这点计算代价远小于长期维护不一致账本的代价。我当时决定把旧系统重构核心就是引入 REA 模型。现在回想这个决定是在跟业务方确认了“到底什么是一次完整业务”之后才做的。先有业务共识再有模型设计REA 只是把共识固化下来。2. 核心概念拆解资源、事件、参与者怎么搭配2.1 资源有价值的、可追踪的东西资源是“有价值并且需要追踪去向”的对象可以是实体的商品、现金也可以是虚拟的应收账款、积分、服务工时。每个资源都应该有一个可识别的主体比如具体到某一批次的商品或者某一张面额的抵用券。判断一个东西是不是资源有个很实用的标准看它会不会在业务中被“消耗”或“增加”。库存商品会被卖出而减少现金会因收款而增加这些是资源。而“销售订单号”或“审批记录”虽然也很重要但它们描述的是业务的凭证和流程本身不会被增减所以不应作为资源建模。我做模型时习惯给资源加一个“资源规格”的维度。比如同一款手机有不同颜色和存储容量它们在业务上可能是同一种商品但库存追踪时必须区分规格。于是拆成资源主表和资源规格表事件与规格关联。这样既不会在商品维度上含糊也不会把型号参数重复塞进每张事件表。2.2 事件让资源发生流动的动作事件是 REA 模型里最核心的部分也是我建议大家最先确定的元素。事件记录的是“发生了什么”不记录“还剩多少”。采购入库、销售出库、收付款、领用、盘点调整这些都是事件。事件天然分成两个方向一类让资源流入组织比如采购入库、预收款另一类让资源流出组织比如销售发货、现金付款。一个完整业务循环通常由一对相反方向的事件组成。卖货这个业务就是商品流出事件和现金流入事件共同完成采购则是商品流入事件和现金流出事件共同完成。事件需要记录的关键信息不是余额而是“时点”和“量”。什么时候发生涉及多少资源数量按什么单价成交。单价和总额可以记录但它们是事件发生时的业务事实不是动态计算出来的汇总。事件表一旦写入原则上就不应该更新或删除只能追加一条反向冲正事件这一点对后续对账和审计特别重要。刚开始建模的人经常会问那“订单”应该放在哪里我的处理方式是把订单看作一个“承诺”或“契约”它单独有一张契约表用来关联后续发生的多个事件。订单本身不是事件不是资源但它连接了未来的发货事件和收款事件。表结构上契约与事件是弱关联一个发货事件可能对应一张订单也可能对应多张订单合并发货。2.3 参与者谁参与了这件事参与者是“执行或参与事件的人或组织”包括内部参与者和外部参与者。内部参与者可以是销售员、仓管员、采购员外部参与者可以是客户、供应商、承运商。同一个事件可能涉及多个参与者一张销售发货单仓管员负责出库货车司机负责运输客户负责签收。在传统表设计里参与者经常被当成冗余字段写在单据表上比如“业务员”字段、”客户名称”字段。一旦一个事件有多角色参与字段就塞不下了。REA 的处理是单独建一张参与者关联表每个事件通过关联表连接多个参与者并且在关联表上标明角色。我建议参与者表用“组织人员”两层。客户、供应商这类组织级参与者与内部员工这类人员级参与者分开。事件与参与者的关联统一记录“参与者ID、事件ID、角色类型”不要嫌多一张表麻烦。后续做业绩统计、客户分析、物流追溯时这张关联表会派上大用场。3. 建模实操从模糊需求到 REA 实战落地3.1 第一步先画业务循环不急着建表我重构进销存系统时第一步不是打开数据库画 ER 图而是拿白板列出业务循环。以销售模块为例完整循环大概是顾客询价、下订单仓库按订单发货财务向客户收款客户收货后可能退货财务对账、结转成本这个循环里真正让资源发生流动的动作是发货、收款、退货、付款。订单在这里只是一个前奏不应该被当成主事件来处理。紧接着要识别每个事件里有哪些资源在流动。发货事件流动的是库存商品收款事件流动的是现金退货事件流动的是退回商品付款事件流动的是现金或银行存款。最后再给每个事件补上参与者发货事件的内部参与者是仓管员外部参与者是承运商和客户收款事件的外部参与者是客户内部参与者是财务人员。这一步我不建议直接画成数据库表而是用文字清单把“谁、在什么时间、通过什么动作、改变了什么资源”描述出来。描述得越清楚后面建表越顺利。很多时候业务对账对不上不是缺字段而是这一步没想透。3.2 第二步把模型落成可执行的库表画完业务循环再转成表结构就顺理成章了。下面是我在项目里实际用过的核心表结构简化版命名上保留了 REA 的语义痕迹。-- 资源表记录资源名称、类型、计量单位 CREATE TABLE resource_specs ( id BIGINT PRIMARY KEY, name VARCHAR(100) NOT NULL, category VARCHAR(50), unit VARCHAR(20) ); -- 资源实例表可追踪的具体资源批次 CREATE TABLE resources ( id BIGINT PRIMARY KEY, spec_id BIGINT NOT NULL REFERENCES resource_specs(id), batch_no VARCHAR(50), initial_qty NUMERIC(18,3) NOT NULL DEFAULT 0 ); -- 事件主表记录业务事件本身 CREATE TABLE events ( id BIGINT PRIMARY KEY, event_type VARCHAR(50) NOT NULL, occurred_at TIMESTAMP NOT NULL, recorded_at TIMESTAMP NOT NULL, ref_doc_no VARCHAR(50), status VARCHAR(20) NOT NULL DEFAULT VALID ); -- 事件资源明细事件涉及的具体资源及数量 CREATE TABLE event_items ( event_id BIGINT NOT NULL REFERENCES events(id), resource_id BIGINT NOT NULL REFERENCES resources(id), direction VARCHAR(10) NOT NULL CHECK (direction IN (IN, OUT)), quantity NUMERIC(18,3) NOT NULL, unit_price NUMERIC(18,6), amount NUMERIC(18,2) ); -- 参与者表 CREATE TABLE agents ( id BIGINT PRIMARY KEY, agent_type VARCHAR(20) NOT NULL, name VARCHAR(100) NOT NULL ); -- 事件参与者关联表 CREATE TABLE event_agents ( event_id BIGINT NOT NULL REFERENCES events(id), agent_id BIGINT NOT NULL REFERENCES agents(id), role_type VARCHAR(30) NOT NULL );event_items 表里的 direction 字段非常重要它标明这笔明细是流入还是流出。查询某一时刻的库存只需要把资源的所有事件明细按方向累加即可。这也是 REA 模型最舒服的地方没有冗余的“当前库存”字段逻辑上不会出现库存表和流水表不一致的问题。amount 字段刻意保留在明细层而不是事件主表因为同一个事件可能同时涉及多种资源比如一张发货单里包含手机和耳机不能只算一个总金额。unit_price 和 amount 是业务发生时的快照后续价格调整不应该修改这里的值只能通过冲正事件修正。3.3 第三步查询口径与 API 设计建表之后第一个要写的查询就是“某时间段内的销售明细和毛利”。销售毛利不再依赖单独的报表表而是通过事件明细去计算发货事件中的商品数量乘以成本价得到销售成本收款事件中的实收金额得到销售收入。两者都来源于事件统计口径天然一致。查询某个客户的应收账款也不需要建应收账款表直接把这个客户参与的事件拿出来比较发货事件发生的应收减去收款事件发生的实收差额就是当前欠款。需要注意查询时要用 event_agents 关联表去限定参与者而不是在事件表里放一个“客户ID”字段。这样既支持一笔订单多个参与方也不会因为参与者扩展而改表结构。API 设计上对外提供的写入接口应该围绕“事件”展开比如 submitShipmentsubmitPaymentsubmitReturn。而查询接口围绕“资源余额”和“事件列表”展开比如 getInventorygetReceivable。写入端绝不接受前端传一个“最新库存数量”直接更新库存数量只可能是后端根据事件计算出来的结果。这个约束在前后端接口文档里要写清楚得让前端同学明白提交的永远是业务事件不是业务状态。4. 我在业务系统里踩过的四个经典坑4.1 坑一把订单当成了资源我第一版设计时为了图省事把订单也放进了资源表管理。当时想的理由很充分订单有价值也需要追踪状态。结果没几天就出问题了销售统计时把订单金额和收款金额混在一起算客户付了钱之后订单变成历史数据不断叠加对账越对越乱。后来才意识到订单的价值是“未来可能发生的交易”它是契约不是已经被消耗或获取的资源。订单可以和“应收款”这样的资源建立关系但订单本身不应该进入资源表。如果你在建表时发现一个对象既不是库存也不是现金却总想在资源表里给它留一行先停下来问一句它真的会被“增加”或“减少”吗如果只是状态流转它就应该放在契约或凭证层。4.2 坑二金额和数量的归属搞不清楚传统单据表喜欢把整单合计金额直接写在主表上这在 REA 模型里特别别扭。一笔销售发货单如果包含三种商品明细金额分别不同还有一个整单折扣那么“整单金额”到底属于哪个资源如果硬放在事件主表统计时很容易重复计算。我的解决办法是事件主表只保留事件本身的时点、类型、凭证号明细表记录每种资源对应的数量、单价、金额涉及整单折扣时再单独建一张“折扣”资源或折扣明细把它视为一种负向资源变动。这样每一种价值的增减都有对应的资源统计毛利时能拆开看商品收入、折扣影响、退款影响业务方再问差异时也查得清楚。4.3 坑三退货和取消直接删数据旧系统里遇到退货第一选择是把原订单明细删除或者给状态字段改成“已作废”。这在 REA 模型里是绝对的禁区。事件是历史事实不能因为后来退货就否认当初发生过发货。如果删除事件后续库存计算和审计日志都会出现不可解释的缺口。正确做法是新增一条反向事件记录退货数量、退款金额、退货参与者再通过一个冲正事件组字段把原发货事件和退货事件关联起来。汇总时原事件照常保留但计算净库存和净应收时会同时考虑正向事件与冲正事件。这个过程很像财务上的红字冲销虽然初期觉得多写了代码但后续只要业务方问“为什么这笔单发生过又退了”你随时可以拿出完整事件链。4.4 坑四参与者表关联后统计翻倍引入 event_agents 表后有一个很典型的 SQL 问题。比如统计销售发货总金额原本直接 from events 就行后来为了把参与人带出来join 了 event_agents结果因为有多个参与者发货金额被重复计数。排查后意识到当一张关联表的一侧记录数是 M另一侧是 Njoin 后就会产生 M 乘 N 的重复。解决办法是统计时先聚合 event_items再单独查询 event_agents 作为维度表或者对关联表先按角色去重。一个事件可能有业务员、仓管员、司机、客户四个人参与但金额只能按事件明细算一次不能让参与者数量放大金额。这个坑很隐蔽建议在写报表 SQL 时养成“金额先聚合、参与人后关联”的习惯。5. 让 REA 和常见开发方式共存5.1 与领域驱动设计的配合很多人看到事件、资源、参与者的结构第一反应是它和事件溯源、DDD 很像。确实如此。REA 可以看成是业务领域模型的一种骨架事件表对应领域事件资源对应聚合根里的实体参与者对应领域内的角色。用 DDD 落地 REA 时我会把每个完整业务循环看作一个聚合边界。比如销售域聚合里发货事件和收款事件是核心订单契约是外围。写入流程先创建契约再发生资源流出事件再发生资源流入事件事件的不可变性正好满足事件溯源的要求。这样设计的好处是聚合边界清晰不会因为一个客户把销售和收款强行放成一个巨型聚合。5.2 与复式记账的衔接团队里有财务背景的人会问REA 和借贷记账法是什么关系我的理解是复式记账是一种展示规则REA 是更底层的业务事实模型。一笔销售发货在借贷表里是“借应收账款贷库存商品”在 REA 里就是两个事件库存流出事件和应收增加事件。实际落地时不必把传统凭证表丢掉。可以保留凭证表但凭证的来源应该是事件而不是手工填写。事件发生后由规则引擎自动生成会计凭证确保账务和业务流水一一对应。这样既符合财务审查习惯也保留 REA 的扩展能力。5.3 最小落地模块建议如果要从零开始引入 REA不建议一口气把所有业务都重构。我建议先选一个边界清晰、对账问题最严重的模块试点比如库存或应收账款。最小模块清单只需要五张表资源规格表资源实例表事件主表事件资源明细表事件参与者关联表不需要一开始就设计复杂的契约表、关联规则表。先把“事件不可修改”这个原则守住把余额查询改成实时计算就跑通了一个最小闭环。等业务认可这套逻辑再逐步把订单、合同、契约加入模型扩展起来会非常自然。6. 我的一点实践心得折腾这套 REA 设计之后我对业务系统的看法改变了不少。过去拿到需求第一反应是“要建几张表、分几个模块”现在变成“谁在什么时候对什么资源做了什么”以及“这件事的参与者和方向是什么”。表结构反而成了最后一步。如果真想在自己的项目里尝试 REA我建议从一张“事件清单”开始把各个岗位口中的业务动作全列出来然后逐个问这个动作有没有让某个资源增加或减少如果答案是否定的那它大概率不是核心事件只是一个附属流程。把所有核心事件理清楚资源表、参与者表自然会浮出水面。还有一点很重要不要在高并发场景里无脑实时计算所有余额。如果查询量上来了可以建立汇总表或快照表但更新汇总表的逻辑一定要由事件驱动而不是业务代码里随手更新。汇总表只是缓存事件表才是唯一的事实来源。只要守住这个底线REA 模型给系统带来的长期收益远大于初期那点建模成本。

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

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

免费获取报价 →
↑