☰
从订单系统重构到REA建模:事件驱动的业务设计实战
2026/10/21 6:32:30 网站建设 项目流程

1. 从一次糟糕的订单系统说起

去年我接手过一个电商系统的重构项目,第一周看代码时差点没把屏幕拍碎。订单流程散落在六七个服务里,每个服务对“订单”的理解都不一样:有的把支付和订单混在一起存,有的把优惠券抵扣直接写成字段塞进订单表,最离谱的是库存扣减居然在订单创建时同步执行,用户下单没付款就把库存锁死了。整个团队天天在“订单该不该有状态”“支付回调到底该归谁管”这些问题上扯皮。

后来我们请了个做过财务系统的老前辈来帮忙梳理,他看了我们那张画得密密麻麻的ER图,只问了一句话:“你们先告诉我,业务里发生了什么事,什么东西变了,谁参与了这个事?”这一问直接把所有人问懵了。他接着说自己做业务建模从来不看ER图,而是用一套叫REA的思路:先承认业务本质上是一串事件,而不是一张状态表。

那次重构给我留下了很深的印象。REA这个词,全称是Resource-Event-Agent,翻译过来就是“资源-事件-代理”。它最早是会计信息系统领域提出的业务建模框架,后来被不少做领域驱动设计的人拿来当作事件溯源、业务建模的基础思想。你可以把它理解成一套比“数据库表设计”更靠近业务本质的视角:

  • Resource(资源):业务里被消耗、被生产、被转移的东西,比如钱、商品、积分、库存。
  • Event(事件):业务中真实发生的事,比如“下单”“支付”“发货”,它是会留下记录的动作。
  • Agent(代理):参与事件的人或组织,比如“顾客”“仓库管理员”“供应商”。

这套模型最反常识的地方在于:它把“订单”这种我们习以为常的东西给拆掉了。它不认为“订单”是业务的基本单位,认为业务的基本单位是“事件”——订单只是多个事件(提交、支付、发货、签收)在某个维度上的聚合视图。想通了这一点,很多系统设计的复杂度会瞬间降下来。

这篇我就结合那次重构和后来在不同项目里反复使用REA建模的经历,把核心思路、实操步骤、容易踩的坑一次说清。如果你是做业务系统、数据建模或者DDD落地的人,这篇应该能帮你在下次设计时少走不少弯路。

2. 为什么业务建模先想“事件”,而不是先想“表”

2.1 传统ER建模的隐性隐患

新手做设计,最常见的路径是拉着业务方问“你们主要有哪些单据”,然后对着单据画表格。订单表、订单明细表、用户表、库存表,外键一挂,ER图出来了,评审会上大家都觉得没问题。可上了生产之后,问题比想象中来得快。

就拿我那次接手的系统来说,订单表里直接存了“优惠金额”“支付状态”“物流单号”“售后类型”这些字段,表面上一次查询全拿到了,实际上每有一个新的业务变化就要去动这张表。比如后来要接入分期支付,产品说“就加一个分期状态字段嘛”,结果发现订单状态机从“待支付/已支付”变成四五个状态的复杂流转,所有依赖订单状态的代码全部要跟着改。到后期,没有一个人能说得清订单表里几十个字段到底在哪些场景下会被更新。

ER建模的核心问题在于“把现状当结构”。它适合描述一个已经稳定下来的业务,但当你要设计一个每天都在演化的业务系统时,你画出的每一张表都在提前承诺一种不变性——“订单就是这样的”“结算就是这样的”。而现实业务,尤其是互联网业务,恰恰是每个月都在变的。

REA的思路正好相反。它不关心“订单表长什么样”,它先问“业务里到底发生了什么”。这是从“状态”到“流”的转变:订单不是一种状态集合,而是一连串事件的投影。

2.2 REA模型是一种“业务语言”,不只是“存储结构”

我第一次用REA重新梳理那个电商系统时,业务方拿过来的需求是“顾客下单后要能查看物流、申请退款、用优惠券”。按照以前的做法,我们会去设计订单表、物流表、优惠券表、售后表。用REA框架后,我们做的是这样一层拆解:

  • 资源:商品、钱(应收款/已收款)、优惠券、库存。
  • 事件:顾客提交订单(产生应付款)、顾客完成支付(实收款发生变化)、仓库发货(库存减少、物流资源产生)、顾客申请退款(退款金额产生)。

每一条业务规则都能清晰地挂到“某类事件改变了某类资源”这个关系上,而不是“某个状态位由谁更新”。这带来的直接效果是:你和业务方聊需求时,他们说的每一句话几乎都能被翻译成REA里的一种关系,字段上不再有那么多分歧。

REA最核心的价值是它提供了一套“找词”的方法。业务分析中最难的不是写SQL,而是把业务里模糊的表达精确成可建模的术语。业务方说“用户查订单”“客服改订单”“财务对账”,这中间涉及的是完全不同的资源和事件。REA强迫你在第一阶段就把它们区分开,而不是统统塞进一张大表里。


3. REA三要素怎么落进真实业务

3.1 Resource(资源):不只是“数据表主键”

初学者容易把Resource直接理解成数据库里的一张表。这是最大的误解。REA里的资源指的是“有价值、可计量、业务愿意去追踪的东西”。它有三个判断标准:

  • 有价值:没有人会为一个无价值的东西去设计一套追踪系统。积分、优惠券余额、商品库存都是有价值的。
  • 可计量:能够用数量、金额、状态等维度量化。
  • 有生命周期:资源不是静态的,它会被增加、减少、消耗、转化。

举个例子,电商系统里“购物车”算不算资源?购物车本身没价值,但它里面的商品条目有。如果把购物车整个当资源建模,你会发现很难跟其他资源(库存、价格)建立统一的关系。正确做法是把购物车拆解成“顾客这个代理”与“商品这个资源”之间的一个中间过程,而不是又一个资源节点。

我踩过的坑是:把“会员等级”当成资源建模。会员等级本身不可被事件增减,它是用户累计消费达到阈值后计算出来的派生数据,不是资源。正确的是把“累计消费金额”这种可计量的值作为资源,会员等级只是它的一个投影视图。

资源类型示例是否适合建模为Resource
实物资源商品、库存、原材料适合,有数量、有状态、会增减
资金资源现金、应收款、积分余额适合,可计量且会流动
数字资源优惠券、会员资格、云存储空间视情况,若有余额/有效期内变化则适合
派生视图会员等级、订单状态、报表指标不适合,是其他资源与事件计算后的结果

在实际建模时,我建议把资源分成“存量型资源”和“流量型资源”。存量型资源关注当前有多少,比如库存;流量型资源关注累计发生了多少,比如总销售额。REA事件天然负责记录流量,资源表只保存流量累积后的快照。

3.2 Event(事件):业务事实的唯一来源

REA里的Event是整个模型的锚点,也是最难建模的部分。它必须是“真实发生的、不可篡改的业务事实”。什么时候算一次支付?不是“用户点了支付按钮”,而是“支付网关返回了成功回调”。时间点定义错了,后面所有基于事件的统计都会失真。

定义Event时有几个非常关键的问题要提前想清楚:

  1. 事件的粒度:“下单”和“提交订单”是一回事吗?在电商业务里,“提交订单”事件会产生“未支付订单”的资源,“支付成功”事件才会产生“待发货状态”。粒度太粗会把不同资源的变化混在一起,太细又会让建模陷入琐碎。
  2. 事件是“账本记录”而不是“业务操作”:操作是实体的方法,事件是已经发生的事实。用户“发起退款”只是一个意图,退款是否成立要看后续的审核。所以“退款申请”和“退款完成”是两个事件。
  3. 事件与资源的关系:一个事件通常会增加一个资源的流出、同时增加另一个资源的流入,比如销售事件会令“商品库存”减少、令“应收款”增加。这种“双入账”视角来自复式记账法,REA直接继承了这一思想。

我自己的经验是以“真实世界发生了什么”作为判断标准,而不是以“我们的系统做了什么”。有一次设计审批流程,团队纠结于“审批通过”和“提交审批”是不是两个事件。实际分析后,“提交审批”只改变了一个内部工单的状态,并未让任何资源在不同代理之间转移;而“审批通过”则授予了该员工某种权限资源。因此前者可以不建模为REA事件,后者才是。

3.3 Agent(代理):别把人只当作一个ID

Agent定义是“参与事件的人或组织”。初看很简单,但实践中常会被忽略一层:一个Agent在系统里可能扮演多个角色,而不同角色关心的资源是不同的。

例如,“张三”这个用户,在“顾客”角色下下了单,之后成为“退货申请人”,后来又可能变成“恶意退货”被风控关注的主体。同一个代理在不同事件中行使不同的角色权责。REA建模要求我们至少记录“事件-代理-角色”三元关系,而不是给每个事件硬绑一个用户ID。

另一个常见问题是“外部系统算不算代理”。在一次积分系统对接中,第三方积分商城发来“积分扣减成功”的回调,我们不知道这个回调算谁的行为。用REA分析后,第三方积分商城可以被建模为“外部代理”,它参与的事件是“积分扣减”,而扣减的对象是“用户积分余额”资源。这样外部系统和内部系统就有了统一的语言。


4. 一次完整建模实战:从需求到模型

这一节我用一个真实的模拟项目来走一遍完整流程。我们当时需要设计一个“企业内部设备领用系统”,需求看似简单:员工可以申请领用笔记本电脑、显示器、耳机等资产,管理员审批后进行发放,离职时需要归还。

最初团队直接画了三张表:员工表、设备表、领用记录表。看着够用,可加了“设备维修”“资产报废”“部门调拨”需求后,这套结构根本撑不住。于是我们改用REA重新建模。

4.1 第一步:找出所有资源、事件、代理

先把需求里的名词和动词全部列出来,再一个一个归类:

  • 代理:员工、资产管理员、财务(涉及折旧核算时)、供应商(采购设备时)。
  • 资源:设备资产(有库存记录与归属)、固定资产账面价值、维修保养额度。
  • 事件:
    • 领用申请:员工提出领用请求,申请时锁定设备资源。
    • 发放完成:管理员发放设备,设备归属权从“仓库”转到“员工”。
    • 归还登记:员工归还设备,设备归属权转回“仓库”。
    • 维修记录:设备送修,维修是改变设备“可用状态”的事件,但不转移归属权。

你会发现“设备维修”既不是领用也不是归还,但它确实改变了“设备可用数量”这种资源。如果当初用ER设计,必然会把它当作一个“附属流水表”,而用REA后,它天然和领用、发放形成一套事件链,状态统计变得非常顺。

4.2 第二步:确定事件时序与资源联动

每一条业务规则,都可以用“某事件发生后,某资源因某代理而变化”来描述。

  • “员工申请领用”事件产生一条“待处理申请”记录,同时将设备资源的状态置为“已锁定”。
  • “管理员审核通过并发放”事件让“可用设备”资源减少、“员工名下设备”资源增加。
  • “员工归还设备”事件让“员工名下设备”减少、“库存可用设备”增加。
  • “送修”事件将“可用设备”减少,同时增加“维修中设备”,等维修完成后反向操作。

这套描述最舒服的地方在于,每个事件对应的资源变化是确定的,不会出现“更新一下设备状态那么复杂吗”这种纠缠不清的问题。后续需求再加“设备丢失”也容易扩展:新增“报失”事件,让“员工名下设备”减少、“丢失设备”资源增加,完全不影响已有事件。

4.3 第三步:落表时的事件表与快照表分离

模型确定后,存储结构其实非常自然地就出来了。事件表是业务事实的追加式日志,快照表是为了查询效率刻意冗余出来的投影。

我们实际建的表结构大概是:

事件表:event_log - event_id - event_type (apply/approve/issue/return/repair/lost) - resource_ref(指向对应的资源类型与ID) - agent_ref(指向员工或管理员) - occurred_at - payload(JSON扩展字段,保存事件当时的上下文快照,如备注、当时的部门等) 资源表:device_asset - asset_id - category - status(空闲/锁定/在用/维修中/报废) - current_holder_id(冗余自事件表,便于查询)

这里有个容易忽略的点:事件表要保存“事件发生时的完整上下文”。比如“领用申请”事件发生时,员工所属部门是A,三个月后调去了B,但审计时应该看到的是申请当时的部门A。所以我们在payload里冗余了一份业务上下文,而非仅仅外键关联。

4.4 第四步:用事务与幂等保证数据一致性

把事件作为唯一事实来源后,最怕出现“事件记录了,资源快照没更新”或“重复推送事件导致资源被扣两次”。我们前后的做法是这样:

  • 同一事件的幂等性:给每个事件一个全局唯一的event_id,业务上可用“申请单号+事件类型+操作序号”生成,入库时做唯一约束。
  • 事件与快照更新放在同一个本地事务里,资源表的变更依据事件表里的原始数据进行重算,而不是依赖于“前端传来的当前状态”。举例:发放设备时,应该用“设备当前状态”去校验是否可发放,一旦校验通过,扣减操作必须与该事件在同一条数据库事务内完成。
  • 异步重试不重新扣资源:下游服务消费事件时,用一个消费进度表记录已处理的事件ID,重复消息直接忽略。

5. 三个最容易翻车的建模细节

5.1 把“状态”当成“事件”,模型会变成大泥球

最常见的错误,是把“订单状态=已支付”“内容状态=已审核”直接当作一个事件。状态是一系列事件累积后的结果投影,它不是事实本身。正确的做法是记录“支付成功”这个事件,如果要“已支付”状态,用事件流算出来,或者维护一个可异步刷新的快照字段。

我见过最夸张的案例是,团队把状态机里的每个状态转移都建模成事件。系统里Event表一天几十万条状态变更流水,但真正的业务价值信息被淹没在一堆无意义的内部切换里。判断一个状态变化是不是应该建模为事件,还是看他是否改变了某个资源的账本,或者说是否在代理间转移了资源的所有权。

5.2 忽略资源“存量”与“流量”的区别,统计必翻车

在设备系统中,“资产总量”是流量值——历史上所有采购流入的设备总量;“当前可用设备”是存量值——减去所有流出的结果。如果只建模流量的某几个事件而漏掉了“报废”事件,那么存量统计会永远虚高。

经验是:每一类资源都至少要有一个“增加型事件”和一个“减少型事件”构成完整循环。找不到对应的减少型事件时,要警惕模型缺了一块。

比如积分资源,增加型事件是“签到/消费奖励”,减少型事件是“积分消耗”或“积分过期”。有人建模时只写了“积分增加”和“积分扣减”,却漏了“积分过期”,结果每年年底清积分时对不上账。这类问题在REA建模阶段如果能画出“资源生命周期循环图”,当场就能发现。

5.3 事件与代理之间缺少角色,权限与审计双双失灵

在审计场景里,光记录“张三操作了”不够,还得知道他是以什么身份、什么权限操作的。我们有一个系统因为Agent节点上没挂角色,后来安全部门要求提供“哪些客服以高权限修改过订单金额”的审计记录时,临时从操作日志里抓关键字拼接,痛苦到怀疑人生。

所以设计事件表时,最好显式预留两个字段:agent_role和agent_org_unit。即使当期不需要,审计系统上线时你会感谢当时的自己。


6. 常见问题速查表:建模时反复出现的坎

症状根因处理方式
业务方说“改个状态就行”,你总觉得会出事把状态演化看成单点更新跳出状态位,追问“是什么事件导致了这个变化”
资源和事件之间关系混乱,说不清谁先谁后没有做“资源增减闭环”检查画出每个资源的流入/流出事件链
报表统计数据总是差一点某个资源增减事件漏建模了重新盘点资源生命周期,找缺失的减少型/增加型事件
测试环境里事件被反复重放后数据错乱了事件缺少幂等约束给事件表加唯一业务ID,消费侧用进度表去重
复盘上月数据时,想不起当时某管理员为什么这么批事件表没保存“当时上下文”payload字段中冗余快照,按需补充描述信息
接口对接方回调事件顺序乱了跨系统事件顺序天然不可靠不依赖顺序,每个事件通过resource_ref+幂等ID自行校验

这条速查表里最值钱的就是“事件表里的上下文快照”那一行。很多系统事件表只记了操作与时间,后来做审计、做数据回放、做“为什么当时是这么个结果”的复盘时,全凭猜。往payload里塞一份“事件发生时的业务现场信息”,后期价值巨大,而且一点也不难实现。


7. 关于工具与协作流程的个人体会

REA建模这件事,最大的阻力往往不是技术,而是团队已有的思维惯性。做过传统CRUD开发的同事会非常难以接受“怎么连订单表都不要了”这种思路。我的做法是分三步推进:

  • 第一步:先不和团队讲REA理论,只带着大家一起把真实业务中的“事件清单”列出来。这个过程几乎不需要解释,业务方会觉得你在认真听他们讲话。
  • 第二步:把事件清单里涉及的“资源增减”和“代理参与人”画出来。常见工具是白板或者在线绘图,重点是让所有人看到“事件”作为核心节点的威力。
  • 第三步:然后才开始聊落表结构。这时候你会发现,之前吵不清楚的边界问题,居然都因为事件粒度统一而自然消解了。

关于事件粒度是否细化,业内有不同的实践倾向。我的建议是:以“是否需要独立审计、是否会引起资源增减、是否会被外部系统回调”为三个判定条件。至少满足其中一个,才单独建模成一个事件。否则就合并到其他事件里作为属性。这样既能保证事件模型的表达力,又不会把模型搞得支离破碎。

我实际试过最成功的一次,是带着五个刚毕业的同事用半天时间把整个财务报销流程用REA推演了一遍。起初他们连“资源”都觉得抽象,后来用“报销单是申请事件、审批是审核事件、打款是转账事件、报销额度是资源”的类比讲了一遍,当场就通了。从那之后他们做新需求时,自己会先问一句“这里发生了什么事件”,而不是直接打开数据库看表结构。

REA不是银弹,遇到没有清晰资源边界的内容型系统、或者纯粹写操作没有业务事件的场景,强行套REA会增加复杂度。但只要你做的是有交易、有库存、有资金流转、有审批闭环的业务系统,这套“先找事件,再定资源与代理”的建模思路,绝对值得放进你的工具箱。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询