资讯动态

REA模型实战:用资源-事件-代理统一领域建模与事件驱动架构

发布时间:2026/10/14 15:11:11 来源:尧图企业网站定制
前阵子我在整理自己的领域建模工具箱时把一个叫“rea”的小项目翻了出来。这个项目最早源于我给某供应链团队做账务系统评审时的冲突场景业务方画了一堆ER图和状态机开发同学按图建了表结果一到对接财务系统各种“这个字段该放哪”“这个事件到底算不算落账”的争吵持续了两个星期。后来我用REAResource-Event-Agent资源-事件-代理模型重新梳理业务把资源、事件、代理三个核心构造块作为唯一主线再配合这个小项目做语义校验和模型导出才算把流程理顺。这篇文章就把我从选型、建模到代码落地的完整经验整理出来适合正在做领域建模、事件驱动架构、财务系统、进销存或供应链系统的开发者和架构师参考也适合想入门DDD但总觉得建模知识零散的人。如果只问REA是什么网上的资料大多会告诉你它来自会计学领域是一套关于资源、事件、代理的语义建模框架。但在真实代码里它更像一份“业务动词清单”把业务规则中的关键动词显式建模为事件把单据、物品、资金建模为资源把客户、员工、供应商建模为代理。这套视角在应对复杂交易场景时尤其好用。下面我会直接把我写“rea”时的目录结构、校验逻辑、踩坑记录一起拿出来聊能直接抄作业的部分就尽量给到细节。1. 项目背景与建模痛点为什么我非要自研REA建模小工具1.1 建模工具与代码实现之间始终存在一道断层我最早接触这套思路的时候也以为不过是把ER图换了个符号。真到实战项目里才发现问题不在符号而在团队认知。举个例子某进销存系统里销售团队在讨论“一笔销售的收款”。有人画状态图把“待收款”“已收款”当成订单状态有人把“付款单”当成主记录还有人认为收款就是“现金余额的变动”。这样一来同一个业务动作在不同模块里就有了三种名字接口对接时自然会出现“我这个字段是状态还是金额”的争议。ER图擅长描述“哪些表有哪些字段”但不太擅长描述业务发生的顺序和职责边界。状态机图虽然能描述流转却又常常把资源的状态变化混在一起。REA模型的第一个价值就是给业务语义划出清晰的边界Resource是“被管理和交换的对象”Event是“改变资源的那一刻”Agent是“谁发起了这个改变”。这样的边界一旦统一后面建表、写接口、做事件映射都会顺着一个固定思路走至少不会再因为同一个东西有三种叫法而吵架。1.2 REA模型是什么用零食店场景理解Resource、Event、Agent要理解REA模型要解决的核心问题可以做一个类比把企业想象成一家餐厅Resource就是厨房里的食材、现金和菜品Event是“采购入库”“顾客点单”“厨师出餐”“顾客付款”这些动作Agent是顾客、厨师、收银员、供应商。ER图能画出食材表和订单表的外键关系但很难回答“为什么顾客付款之后后厨的食材库存就应该发生变化”REA则要求你把这些事件通过资源流动和事件对偶连接起来每一次资源流出都要有对应的资源流入这样整个系统就像有一套会计等式一样能随时发现缺了一条腿的地方。“rea”这个名字没有太复杂的故事。它就是Resource、Event、Agent三个单词的首字母缩写小写是为了命令行输入方便。当时我在终端里来回敲了很多候选名最后选定了“rea”。除此之外rea在英文里看着接近real我也确实希望这套建模方法能把代码世界里的真实业务动词显式化让模型不再是挂在墙上的图纸而是能跑进CI里做校验的规则。1.3 “rea”项目定位让模型成为CI里的一等公民我个人理想中的工作方式不是把建模文档放到Wiki里供人查阅而是把模型定义变成工程里的一等公民定义好Resource、Event、Agent以及它们之间的关系之后数据库表结构、接口Schema、事件发布结构都由同一套定义生成并且模型的合法性校验在CI里自动执行。这样业务想改动时先改模型定义再跑一次生成和校验上下游的所有代码同步调整不会出现文档和代码各说各话的情况。这个想法直接影响了“rea”项目的技术选型。核心结论是模型定义独立于具体数据库方言业务校验逻辑全部放在模型层不掺入具体ORM。这样底层用PostgreSQL还是MySQLREA语义保持一致将来要换存储也只动持久化层不动领域规则。2. REA模型建模五步法把业务描述翻译成资源-事件-代理图2.1 Resource、Event、Agent的判断标准与典型反例如果要我概括REA模型的三个构造块我会说Resource是名词Event是动词Agent是参与者。但落实到实际建模单靠这个口诀还不够。Resource不是所有的“东西”都是资源。资源应当是经济价值可被识别、需要被企业控制或管理的对象。库存商品、现金、固定资产是典型资源客户信息反而更像Agent的描述属性。判断标准是这个对象是否会出现在经济交换中是否会被一个Event消耗或产生。比如“商品”会因销售事件而流出所以是Resource“销售员”本身不流出他更多是执行事件的主体所以是Agent。Event指的是改变一个或多个Resource状态的、可被明确定时记录的动作。它不是抽象的业务目标而是发生在具体时间点上的事实。比如“客户下单”一般可以拆成“确认订单需求”和“承诺交付”两个不同动作如果严格按REA的语义订单本身属于契约层发货和收款才是真正改变资源的事件。很多人在这一点上栽跟头把订单状态本身当成事件导致后续资源流动看不出方向。Agent是参与经济事件的人或组织又分为内部Agent和外部Agent。内部Agent指员工、部门、仓库等外部Agent指客户、供应商、银行等。Agent之间的区别不是表结构的区别而是与事件关系的角色差异。建模时不要在Agent表里写死角色角色应该挂在Agent与Event的关系上这一点我后面会专门展开讲。2.2 五步建模流程从名词动词表到事件对偶检查我的经验里把一段业务描述翻译成REA图不需要先上专业工具一支笔加一张纸就够了。这里分享我固定使用的五步流程圈出描述中的所有名词先不区分概念把它们列成一张表圈出所有动词也列一张表。根据判断规则把名词归类为Resource或Agent。拿不准的就问一个问题这个名词会因某个动作而产生或消失、减少或增加吗会就是Resource不会但会发起动作就是Agent。把动词归类为Event但要注意动词时态。只保留表示“已经发生”的动作不要保留“应该”“希望”这类目标性描述。比如“销售”这个词可能对应好几个Event接收订单、承诺交付、发货、收款。给每个Event找它的参与Agent和进出的Resource。事件要么流入资源要么流出资源常见的销售场景可以拆成“货物流出”和“资金流入”两个方向。检查事件对偶。一个Event的流出通常需要另一个Event的流入来补偿或对应。比如“发货”的资源流出对应“应收款增加”“收款”的资源流入对应“应收款减少”。如果找不到对偶对象很可能漏了一个Event。2.3 一个可以照抄的零食店建模示范为了更好地说明我用一个零食店的场景来演示。顾客在柜台买一包薯片用现金付款。按五步法拆分可以列出这样一张表业务元素归类理由顾客外部Agent发起购买并接收商品店员内部Agent执行出库与收款薯片Resource销售事件流出现金Resource收款事件流入销售出库Event改变薯片的可用状态现金收款Event改变现金余额这个例子看着简单但它已经包含了REA模型最重要的两条关系资源流和事件对偶。销售出库事件让薯片这个Resource流出现金收款事件让现金这个Resource流入两个Event通过“销售交易”形成对偶关系。后面扩展到多个商品、采购、退货逻辑完全一样。2.4 与ER图、事件溯源的边界REA管语义其他管存储很多人问REA和ER图、事件溯源到底有什么区别。我的理解是它们不在同一层REA管语义ER管存储结构事件溯源管状态演进方式不需要二选一。ER图的核心产物是实体和关系适合描述稳定数据结构但它没有把“业务规则”变成一等公民。比如你能画出“订单表关联付款表”但为什么会有这种关联、方向是怎么样ER图上通常靠注释约定时间一长就失效。事件溯源的思路是“不保存当前状态把每次状态变更事件保存下来状态由事件重放得到”它和REA在哲学上非常像都看重Event。但事件溯源本身不关心业务语义一个存储层的事件可以叫order_created也可以叫something_happened没有统一判定规则REA则强制你回答“这个事件到底改变什么资源由谁参与”所以REA更适合作为事件溯源系统的事件语义标准。实际项目里常见的最佳组合是REA定义业务语义和规则ER图定义持久化表结构事件溯源把Event追加到日志表通过投影生成Resource当前状态和Agent行为记录。我自己的“rea”项目就在这个组合中占住最上面的语义层。3. “rea”项目代码落地从语义模型到事务写入与投影查询3.1 技术选型与目录划分PythonPydantic的取舍技术选型上我没有选择Java、Go或者TypeScript而是用Python加上类型注解。原因很简单REA建模最重要的产出是易读的语义定义和可交换的数据结构Python的Pydantic配合类型注解可以在很短的行数内完成结构定义和校验我需要跟SQLAlchemy或Django ORM配合时也很顺手。如果你的团队技术栈是Java完全可以换成Record类嵌套定义但核心思想不变用强类型结构描述模型而不是用字符串散落在注释里。工程目录我按领域边界拆rea/ model/ resource.py event.py agent.py relation.py parse/ rules.py validate/ balance.py duality.py store/ postgres.py projection.py cli.pymodel里放的是纯领域定义validate里放的是不依赖数据库的校验逻辑store里才是具体数据库读写和投影表。这样划分的好处是以后如果想从PostgreSQL换到MySQL只需要替换store层如果要增加新的校验也只需要写一个纯函数。3.2 核心类型实现ResourceFlow为什么必须独立建模接下来看核心代码。Resource、Agent、Event的基础类型可以这样写from pydantic import BaseModel from enum import Enum from typing import List from datetime import datetime class ResourceKind(str, Enum): CASH cash INVENTORY inventory SERVICE service class AgentKind(str, Enum): INTERNAL internal EXTERNAL external class Resource(BaseModel): id: str kind: ResourceKind name: str status: str active class Agent(BaseModel): id: str kind: AgentKind name: str class ResourceFlow(BaseModel): resource_id: str direction: str # IN / OUT quantity: float unit: str unit_price: float class ReaEvent(BaseModel): id: str kind: str occurred_at: datetime agent_links: List[dict] # 包含 agent_id 和 role flows: List[ResourceFlow]这个设计看起来很简单但有两个细节特别想强调。第一个quantity、unit、unit_price要放在ResourceFlow上而不是放在ReaEvent上。很多人一开始把“总金额”这个字段放在Event上后续遇到多商品订单和多计量单位就很痛苦。第二个agent_links里每一条都包含agent_id和rolerole是描述这个Agent在该事件中承担的角色而不是Agent本身的固定身份。3.3 校验函数与事件对偶让业务规则跑在CI里模型校验逻辑是“rea”项目里最有价值的部分。因为REA模型强约束了资源流动方向所以可以用纯函数检查事件是否满足平衡条件。最基本的两个校验函数def event_has_positive_flow(event: ReaEvent) - bool: return any(f.direction IN for f in event.flows) and any(f.direction OUT for f in event.flows) def event_total_value_matches(event: ReaEvent) - bool: inflow_value sum(f.quantity * f.unit_price for f in event.flows if f.direction IN) outflow_value sum(f.quantity * f.unit_price for f in event.flows if f.direction OUT) return abs(inflow_value - outflow_value) 1e-6注意event_total_value_matches不是所有REA事件都必须比如仅库存盘点调整事件可能只有单方向变动。所以实际项目中要做规则配置把强校验只应用在结算类事件上。这一点我踩过坑一开始对所有事件做金额平衡校验结果很多“单纯转换”类型的事件频繁报警。事件对偶校验也很重要def check_duality(events: List[ReaEvent], duality_map: dict) - List[str]: issues [] for e in events: if e.id not in duality_map and any(f.direction OUT for f in e.flows): issues.append(fevent {e.id} 有资源流出但缺少对偶事件) return issues逻辑很直白一个Event如果让资源流出通常一定要有一个让资源流入的Event在模型里存在否则整个经济交换不闭环。比如销售出库对应收款或应收款增加库存领用对应成本归集固定资产买入对应后续折旧。虽然实际业务可以跨会计期间但建模层面至少要声明对偶关系。3.4 数据落库与投影表避免REA模型被查询性能拖垮数据落库部分我用的不是直接把Event表当成事件日志而是按模型拆成四张物理表rea_resource、rea_agent、rea_event、rea_flow。rea_event与rea_flow分离让一个Event支持多个ResourceFlow查询也可以在需要时聚合。rea_event与rea_agent通过rea_event_agent关联表避免在Agent表内写死角色。为了读模型性能还建了一张rea_event_projection表把常用的“事件资源代理”聚合字段存成一张可查询的宽表。这样业务侧查“最近7天每个销售员的销售额”时不需要join四张表再聚合只需要查投影表。这个思路很重要因为REA干净的核心模型并不适合直接扛高并发查询它更像一个标准化的源数据层。代码层面写事件时先调用校验函数通过后再插入rea_event和rea_flow最后更新投影表。整个过程用事务包裹避免出现“事件已入库但投影未更新”的不一致。4. 项目实战中常见的4个问题与排查办法4.1 一个Event对应多个Resource时数量与单位如何拆分我实际遇到过一个典型的销售订单包含三种商品其中一个按箱销售、一个按件销售还有一个服务工时按小时计费。如果Event表只维护一个amount字段后面做成本核算时根本无法拆分。失败的做法是在Event上直接存总金额比如这样event ReaEvent(idE1, amount520) # 无法知道520对应什么问题很明显报表只能看到总数库存扣减不知道从哪个SKU扣也无法精确到单位和单价。正确做法是把金额和数量拆到ResourceFlow上每个流记录quantity、unit、unit_priceEvent本身只记录发生时间、参与Agent以及事件类型。汇总金额时用SUM(flow.quantity * flow.unit_price)动态计算每个资源维度都有独立语义。这个模式还有一个额外好处可以支持一个事件中部分资源方向流入、部分方向流出。比如“退换货”事件中新商品流出、旧商品流入同一事件内两个方向都有用ResourceFlow表达最自然。4.2 Agent的role字段放错位置导致跨系统身份冲突常见错误是在Agent表上设置role列比如“客户”“供应商”“员工”。表面看没问题但同一个组织可能在不同事件中扮演不同角色它既是客户又是供应商同一个人既是审批人又是经办人。把role固定在Agent上会让跨系统交换身份时冲突。我见过最典型的崩溃场景是某公司对接多套系统A系统把对方单位标记为“客户”B系统把同一单位标记为“供应商”。当需要统一账户时两张表合并出现同ID不同role的冲突。REA的解法很简单Agent是纯粹的身份主体role只存在于Agent与Event的关联关系上。在代码里就是agent_links里的role字段。这样同一Agent可以在“采购”事件里是供应商在“销售”事件里是客户互不干扰。实际落地时还可以增加role_type枚举并验证与事件kind的匹配比如sales事件里至少应该出现buyer和seller之一purchase事件中应出现vendor和buyer之一。这也是不错的校验点。4.3 REA事件向财务凭证映射不能用一对一财务系统评审里我最常被问到的一个问题是“我们把REA事件直接作为会计凭证行行不行”我的答案是通常不行。REA事件描述业务事实会计凭证则依赖科目体系和借贷规则。一个REA事件可能对应多个科目行比如销售出库事件在财务上既要记录收入科目又要记录成本科目反之多个后台明细行可能汇总成一张凭证。因此我建议设计一张event_account_mapping表event_kind会计动作借方科目贷方科目触发条件sales_dispatch确认收入应收账款主营业务收入出库完成sales_dispatch结转成本主营业务成本库存商品出库完成映射表可以让财务团队独立维护业务事件模型不用跟着科目体系频繁改。这也是REA最有用的落地技巧之一不要让领域模型直接绑定财务科目。4.4 模型设计漂亮但报表查询慢的优化思路一个全模型join的查询长什么样大概是用户要看某销售员的业绩从销售事件出发join发货单、join客户、join商品、join收款。数据量级一到百万级单条查询也可能拖慢。REA模型非常正规但并不适合直接把长期明细拼给大屏看板。我的做法是分层核心事件表和资源流表只负责写入与一致性查询需求统一走投影表或物化视图。投影表可以按账套加时间分区按常用查询条件建索引。如果投影表跟不上分析需求再把事件流导出到数据仓库做进一步宽表加工。另外REA中的Agent和Resource表规模通常不大频繁访问时完全可以缓存。事件表增长很快但大多数报表并不需要扫全表只要按时间窗过滤配合索引性能没有问题。5. “rea”的适用场景与扩展路线什么时候能用、怎么演进5.1 一个判断标准什么项目该引入REA什么项目不该那么怎么判断项目值不值得引入REA我这里给一个很朴素的标准如果业务中同一类资源会有多条进出路径而且多件事件在时间上是先有资源流出、后有资金流入且参与者涉及内外多方就非常适合。典型的包括进销存、供应链、订单合同管理、积分体系、订阅计费、租赁与资产处置。如果你的系统只是一个简单的CRUD管理后台比如员工信息维护、公告管理引入REA就是过度设计只会拖延交付。5.2 与DDD、微服务、事件驱动结合的四步落地顺序REA可以和DDD、微服务、事件驱动组合起来。我建议的顺序是这样的用REA梳理核心域内的经济资源与经济事件定义事件名、资源类型、参与者角色。这一步产出的是通用语言。把REA中的Event映射为DDD中的领域事件把Resource映射为聚合根把Agent映射为身份主体。微服务之间通信时统一使用REA事件结构作为消息契约而不是各写各的订单事件。读模型使用投影表避免每个服务都去查源事件表。我帮某团队落地时发现REA最大的好处不是让它代替DDD而是让DDD事件风暴中的无数“事件”有了一个判定准则有些候选事件根本不改变资源那就不是核心事件有些事件改变了资源但没有对偶事件业务闭环就不完整。5.3 新手练习路线图从零食店模型到可运行库存与财务闭环如果你从零开始学习我建议的路线是用一个零食店模型手工画出第一张REA图。建四张表资源、代理、事件、资源流并录入一张销售单。写一个最小的校验函数检查这张销售单是否同时有流入和流出。逐步加入供应商、采购、费用、退款让模型变复杂。再回到工程代码参照第三部分的目录结构把模型代码化。这一步做完你基本就能判断REA适不适合自己的业务了。如果适合就可以在真实项目里把它作为领域语义层配合事件溯源做进一步落地。5.4 我的三句评审口诀资源跟着事件走角色挂在事件旁流出必须找流入最后说一个我自己总结的决策口诀资源跟着事件走角色挂在事件旁流出必须找流入。这三句话可以当作评审代码时的快速检查清单。看到一张业务表就想一下这张表是资源还是代理如果它既是资源又是代理多半需要拆分。看到一个状态位试着把它变成事件。看到一条没有对应流入的流出逻辑先亮红灯。这几条规则虽然简单但在大多数业务评审现场比争论半天UML符号要有用得多。

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

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

免费获取报价 →
↑