REA建模:用资源-事件-代理重构订单与结算系统
标题 rea 拆分重构之后我把它定向理解为“资源-事件-代理”这一套经典的业务建模思想。这套模型在国外企业建模、财务系统设计、DDD领域建模里用得很多但在国内技术社区的讨论热度一直不算高。实际操作中用REA思维去重新梳理业务系统尤其是订单、库存、结算这类涉及多方、跨流程的系统会明显感觉到逻辑变得干净、可追踪、好扩展。这篇文章就按REA的核心理念、建模实操、常见误区和踩坑经验来展开附带一套可以直接用来设计业务模块的建模方法和检查清单。1. 为什么我突然开始重看REA模型过去几年我一直在做各类企业级业务系统的领域建模包括订单履约、库存管理、财务对账、会员积分这类典型模块。用得最多的是事件风暴和DDD聚合建模后来在一个结算系统重构项目中反复踩坑才真正把REA翻出来认真研究了一遍。先回答一个基础问题REA是什么。它由三个核心概念组成——Resource资源、Event事件、Agent代理分别对应业务世界里的“东西”“发生的事情”和“参与的人或部门”。这套框架最早用于会计信息系统设计是会计学教授William McCarthy在上世纪80年代提出的核心目的是用一套贴近业务本质的模型来描述企业的资源流转和价值变化取代传统复式记账那种以“借贷科目”为骨架的建模思维。放在技术语境下理解REA的价值不是让你不用数据库表、不用领域对象而是给你一套筛选领域逻辑的“筛子”哪些实体值得建表哪些状态变化值得记录谁参与了变化变化造成了什么资源的增减。这套筛选逻辑恰好就是事件驱动架构和DDD战术建模中特别容易潦草处理的环节。那这次选题“rea”我就打算沿着从概念到实操的路径把REA在业务系统建模中的应用彻底讲透。适合谁来读正在做订单、财务、库存类系统设计的业务架构师、后端开发也包括刚接触DDD但总是纠结聚合边界怎么划的读者。读完你会得到一套可操作的建模清单不需要有会计背景我也会把会计和业务之间那层术语差异拆开看。2. 拆开看REA的每一个核心原子2.1 资源不只是数据表里的字段Resource在REA里指的是“经济资源”是系统里有价值、可被交换或被消耗的事物。注意这里有别于技术领域常说的“实体”——资源不只是一条记录它代表的是某一时刻有价值意义的对象库里的一件商品、账户里的一笔余额、卡包里的一张优惠券都属于资源维度。在建模的时候容易犯的第一个错误是“见什么表都当资源”。比如订单表、物流单表这类记录型数据本质上不是资源而是“事件”的载体。它们记录的是已经发生的交易动作不是被交易的东西本身。如果你把订单表也建模成一个资源那后续状态流转、对账追溯都会被各种流程字段拖垮。实操上识别资源的判断标准很简单这个对象会不会被“消耗”“减少”或“增加”会不会出现在系统内外部的价值交换链路上如果答案是肯定的它才具备资源资格。反过来一个纯粹记录状态的标签比如“订单状态”“处理节点”就不算资源只是事件的结果快照。2.2 事件整个模型的发动机Event是REA模型中最核心的部分——它描述“发生了什么”。事件必须有明确的时点、明确的动作类型以及明确的参与者。下单、支付、扣库存、发货、签收、退款这些都是事件。要用一句话记住这个定位资源是被事件驱动的事件是被代理执行的而整个业务流就是一连串有序事件的串联。这个视角和事件溯源Event Sourcing理念十分契合也和传统ERP系统里只保存当前状态的做法不同——REA会把“过程”作为一等公民保留下来。建模事件时要区分增值事件Increment Event和减值事件Decrement Event。增值事件指资源的流入方向例如入库、收款、优惠券发放减值事件对应资源流出例如出库、退款、积分消耗。几乎所有业务动作都能归入这两类中间的判断是所有链路是否闭环的检查开关。2.3 代理认识“谁”做了这件事Agent是参与事件的实体可以是个人、部门或外部系统。代理与事件之间关系有两种——执行关系和参与关系。在订单履约中“用户”是发起支付事件的执行代理“仓库系统”是执行发货事件的执行代理。把代理剥离成独立模块最大的好处是权限模型、审计日志、异常复核都可以复用同一套身份体系。很多系统在设计时会把操作人直接塞进业务表里当字段短期看是省事但一旦涉及多个系统、多类角色就会出现权限绑死在流程上、无法扩展的问题。REA把代理抽象出来等于让模型天然具备“谁在何时做了什么”的审计能力。2.4 三者的连接关系才是它最值钱的部分单独看三个概念并不算惊艳。REA真正的价值在于定义了三者之间的经济联系例如资源-事件、事件-代理、以及事件-事件之间的双向或链式控制关系。比如一笔“销售出库”事件它必然连接一个“库存资源”的减少和一个“销售订单”事件的触发。这个连接不是外键只是逻辑关联而是在建模层面强制你回答这个事件到底动了哪些资源由谁执行的它是哪个事件触发的后置动作一旦要考虑这些问题很多通常被忽略的边角逻辑就暴露出来了。3. 从零开始用REA重造一个小型订单系统3.1 建模前的边界圈定与角色定义这次实操我把场景限定在一个极简的电商本地生活订单履约模块参与角色只包括用户、商家、平台结算三个代理。业务流包含用户下单、商家接单、用户支付、平台抽成、商家结算、用户取消订单、退款。第一步不碰表和类先做一张事件清单把该业务环节里所有“发生了什么”的候选事件全列出来用户创建订单用户完成支付商家确认接单商家完成履约平台完成分账用户发起退款系统执行退款用户取消未支付订单这八个事件就是系统的“事件原语”。后续所有状态、表结构、接口设计都服务于“记录好这些事件”这个目标而不是反过来让事件去适配表结构。3.2 第二步识别每类资源的进出路径事件清单确定后第二步是把资源路径画清楚。用户创建订单 - 生成一个“待支付订单快照资源”实际上它是订单资源的一个状态版本支付事件 - 增加“平台收款账户余额资源”同时产生“用户支付流水资源”商家接单 - 没有直接资源变更但触达了履约事件履约完成 - 减少“平台应付商家账资源”与“用户已购服务资源”之间需要建立结算关联退款事件 - 减少“平台收款账户余额”增加“用户账户余额”。这里注意一个细节不要把“订单状态”直接建模为资源变动。订单状态是事件产生的派生结果你完全可以随时通过回放事件计算出当前状态。为追求写读性能而单独维护状态字段没问题但业务语义上必须清楚它不是源头。3.3 第三步定义代理关系和权限范围三类代理的建模相对固定用户、商家、平台。各自的事件执行边界需要严格区分。用户能触发创建订单、支付、取消订单这类上游事件商家能触发接单、履约这种执行类事件平台只触发分账、结算类动作。在设计API接口时我们同样沿用这条边界每个代理对应一组可执行操作集合操作与事件一一对应。这样权限配置会变得异常简洁审计也舒服。3.4 关键落库设计事件表长什么样落到数据库实现时我最终采用了一套“事件主表 资源流水表 代理关联表”的三角结构。事件主表结构如下event_id全局唯一事件编号event_type事件类型枚举occurrence_time业务发生时间agent_id执行代理IDresource_id关联资源ID可能为空比如用户取消未支付订单时没有资源变动parent_event_id触发当前事件的父事件ID可为空payload_json业务上下文快照这套结构把事件、资源、代理全部串在一个逻辑链路里。每产生一个新事件生成一条事件记录同时根据事件类型追加资源流水。后续要做对账、统计、风控都只需要围绕这套事件流来做不需要在十几个业务表里来回join。3.5 从模型反推接口协议接口方面我强烈建议所有变更类操作都设计成“提交事件”的语义而不是“更新状态”的语义。例如POST /api/v1/events/payment而不是POST /api/v1/order/status/paid。前者表达的是“发生了一个支付事件”后者表达的是“修改数据状态”。两者前端调用可能完全一样但接口语义会直接影响后端设计者的建模自觉。项目重构之后我们团队内部定了一条规矩任何修改操作先想清楚它在业务上到底是哪个事件再去写接口逻辑。这条规矩本质上就是REA思维在工程层面的落地。4. REA与常见建模方法的核心差异4.1 传统数据库表驱动的建模方式传统方式下设计订单模块基本是建订单表、订单明细表、支付流水表、库存表用外键把它们关联起来。开发效率高但随着业务分支增多售后、改价、多渠道、多次支付合并问题开始显露表结构需要不断加字段状态机和业务规则纠缠在一起排查一个人为操作引发的数据不一致问题时只能在十几张表手工核对时间线。REA对此的解决思路是“保留原始事件数据不强求单表状态一致性”。传统表驱动方式把业务状态视为核心数据REA则把业务状态视为事件演算的产物。只要事件流完整任何时点的快照都可以被重建。4.2 DDD与REA结合是实践中最顺滑的组合很多人问DDD的聚合和REA建模到底是什么关系。我的理解是DDD提供的是“如何划分边界并管理复杂性的方法论”REA提供的是“边界内部如何识别核心要素的视角”。两者搭配时聚合根往往就是某个主事件或一组事件链实体对应资源和代理的形态。比如订单聚合并不适合把“订单状态”当聚合根可以改成“订单创建事件”为聚合起点把支付履约退款等事件组织在同一段事件链中订单信息变成事件链上的投影。这能极大减少聚合内状态不一致的问题。4.3 事件溯源、CQRS和REA的天然契合REA要求完整保存事件流和事件溯源架构天然同频。事件溯源把业务状态存储为事件序列REA则告诉我们应该存储哪类事件。两者结合系统在复盘用户纠纷时可以把当时发生的事件帧逐帧还原。CQRS模型下读模型完全可以用事件流构建出针对不同角色定制的数据视图。但这里必须提醒一点不是所有业务都适合完整事件溯源。比如库存操作频繁、吞吐量大的场景每笔事件都落到事件库存储成本和序列化成本会直线上升。稳妥做法是把REA事件模型用于交易结算类业务低频但高价值而对于库存流水这种高频数据可以只保留汇总结果和最近一段时间的明细事件。5. 实操中最容易踩的五个坑附排查方法5.1 把快照当事件导致事件流断裂我见过太多次系统里记录了“当前欠款金额”但没有记录“这笔欠款最初是因为什么事件产生的”。一旦审计追问来源就只能靠人工翻日志。排查方法很简单对任意一条资源流水问一句“这条流水是由哪个事件写进来的”如果回答不了事件链就是断裂的。5.2 缺少父事件关联无法追根溯源如果一个子事件没有父事件ID就只能靠时间戳去猜测链路。尤其在退款、改签这类多级流程场景没有父事件关联会让问题定位难上数倍。设计事件表时必须预留parent_event_id字段并在写入逻辑中强制要求非根事件填写。5.3 滥用“资源”概念最常见的错误是把所有业务对象都塞进Resource导致资源、事件、代理边界混乱。判断标准回到前面的定义如果一个对象不会被增加、减少或消耗并代表某种可以被交易或流通的价值那它就不是该业务域的资源。5.4 忽略时间字段的口径一致性REA模型中时间属性是核心字段但不同系统对“业务时间”“系统时间”“支付渠道时间”的定义差异极大。做对账时如果混用会直接导致核对失败。建议强制一条规则事件表始终记录三个时间维度——业务发生时间、系统入库时间、外部来源时间分别建字段别偷懒复用。5.5 代理身份复用导致审计粗颗粒如果把“平台”当成一个代理来记录分不清到底是平台客服操作还是平台自动任务触发后期定责就无从谈起。代理建模的粒度应至少到“操作入口”比如平台客服A和平台自动任务属于同机构但不同代理子身份在建模时必须区分开。6. 排查问题时的实操清单这套模型搭好之后排查问题本身也能形成一个固定的操作流。出现资金对不上、库存不平、积分多发这类异常时我的排查顺序是这样先锁定时间窗口查找窗口内全部事件记录。按资源维度筛选涉及该资源的所有事件。按代理维度检查是否存在可疑角色操作。按父事件链路从根事件逐级往下展开。最后比对资源流水总量是否等于各事件明细的代数和。这套方法论从根源上把定位成本降低了。之前用传统表结构排查对账问题要跨模块拉一堆Excel手工比对现在直接对着事件表写几条SQL几分钟就能定位。基于我个人的实操体会REA建模在交易、财务、供应链这类需要强审计和高追溯性的业务领域价值尤其明显。如果你的系统还在天天处理各类数据不一致纠纷不妨抽半天时间把自己模块里的事件完整列一遍把资源流动画清楚。这个动作本身就已经能帮你找到不少隐患。最后再分享一个小技巧项目初期建模时不要先想着ER图先在白板上只写“事件动词”和“资源名词”用“谁执行什么动作使什么资源发生了变化”这个句式覆盖所有业务规则。等所有规则都能用这个句式说清楚再去设计表结构一切就变得非常自然。这条路我走通了也希望给正在做业务建模的你提供一个新的思考支点。