做业务系统这些年跟“账对不上”和“业务规则散落各处”这两件事真的缠斗了很久。手里的项目标题是“rea”我先说明白这不是某个新框架的名字缩写而是我在复盘某订单中台时反复用到的 REA 模型——资源、事件、代理Resource-Event-Agent。REA 模型的底层逻辑特别简单但想真正落地先弄懂它能帮你少踩一半的坑。不管你是管库存、管订单、管会员积分还是搞财务共享中心这套模型都能帮你想清楚数据从哪来、往哪去、谁在中间负责行动。适合正在做业务架构、企业信息化、账务与业务一体化的开发者和管理者参考。有人一听到“模型”两个字就紧张其实它就是一套看业务的视角。今天我不讲教科书定义直接把项目现场怎么拆、怎么建、怎么对账讲清楚。原本这个标题只是“rea”三个字母很多朋友会猜是实时分析还是资源估算我都理解但在业务建模这个语境里它最实用的解释就是资源、事件、代理三者组成的建模方法一句话说就是把业务拆成谁拿了什么、发生了什么、谁参与。1. 揭开REA的面纱它到底是什么1.1 一句话版本资源、事件、代理REA 最早从会计信息系统里长出来后来被越来越多的业务系统借鉴。三种元素各司其职资源Resource企业能用来创造价值的东西比如现金、商品、库存、版权、信用额度。资源不一定是实体也可以是服务能力。事件Event业务发生的最小单位比如销售开单、出库、收款、退货、入库。事件必须能在时间和空间上可追溯。代理Agent参与业务的人或组织。对内可能是销售员、仓管员对外是客户、供应商、物流公司。这三者的关系老外喜欢说“经济交换”。落到现实就是你拿资源、我拿资源我们之间发生一个事件事件再关联到代理。非常像两个朋友交换礼物付出一个东西换回另一个东西双方都要在事件里留下记痕。如果只让我记住一句话我会说“流程即事件往来即代理存货与资金即资源”。这句话治好我多年的建模纠结以前看到流程就画流程图最后画出一堆谁也看不懂的箭头换成REA之后每个流程节点都要回答自己到底是资源、事件还是代理模型自然就收敛了。1.2 为什么是REA而不是借贷记账我用买咖啡的场景给你看差异。传统会计表会记录借库存商品——咖啡豆 100元贷银行存款 100元。这没问题账是平的但这个记录没有回答“谁去买的”“和哪个供应商买的”“为什么买这批”。所有上下文在分录里消失后续想按供应商分析只能挂辅助核算要不就在凭证摘要里写一段任性文本。REA 的视角完全不同。库存在“资源”里银行存款也是“资源”“采购入库”和“付款”是两个独立事件代理是“采购员A”和“供应商某咖啡贸易公司”。当你想问“上个月每个供应商的供货准时率”不用去翻凭证摘要从事件表里就能拉出来。REA 的价值是让业务过程和会计结果走同一条数据链路而不是等到财务月底冲账时才来对业务。这个差异在月末结转时感受最深以前业务系统导给财务的数据经常差几分钱用REA重新建模后业务事件本身就有金额财务只是换了视角来看同一批事件差异来源几乎消失了。这里也顺带回应一下热搜词“底层逻辑”。很多朋友觉得它虚但 REA 就是把底层逻辑实体化的过程不追求表面科目而追求业务发生的真实事件。用这个方法设计系统报表口径自然统一跨部门扯皮会少很多。2. 建模实操用REA梳理一个销售业务2.1 先找资源钱、货、信用别把资源当科目我习惯从一个看得见的核心流程开始。电商订单中台最经典的就是销售与收款。第一步列资源商品库存、应收账款、银行存款、积分/优惠券。很多人会把“销售费用”也列进去这就是误区。费用是结果值不是交换的资源。资源一定要能“被消耗”或“被获取”并且能计量。判断标准很简单如果这个对象不能从一边流到另一边它就不是资源。举优惠券的例子。在传统系统里它只是个营销字段在REA里它是标准资源用户用积分换优惠券优惠券冲抵订单金额资源流向清清楚楚。如果你刚开始建模建议资源数量控制在10个以内太多反而难维护。做模型设计时留一点松弛感不需要一次把所有可能业务都塞进去。我记得第一次建模就把“营销活动”当资源结果活动根本不会在事件之间流动纯粹浪费了一张表。2.2 再定事件和代理谁在什么时间点了什么事件是REA里最花功夫的部分。好的事件有两个要求业务原子性、时间可追溯性。业务原子性意思是事件拆到不能再拆。比如“销售开单”不能拆成“记录商品、计算金额、分配库存”三个事件因为业务上它是一个动作但“支付”要单独成一个事件因为它在时间上可能与开单错开。可以这样判断如果一个动作被系统拒绝后用户会立刻重试整个流程那它就是一个原子事件如果用户需要重新排队或者换页面那多半是另一个事件。代理需要在事件里明确主次。销售事件里的代理是客户和销售员采购事件里的代理是供应商和采购员。如果代理还有组织关系比如销售员还要分大区、小组就另建组织维表不要在REA主链路里反复叠。代理字段一定不要用可枚举的字符串硬塞宁可多建一个维表后面灵活得多。下表是梳理某跨平台系统的样例核心流程环节资源事件代理客户下订单商品库存订单创建客户、销售员支付银行存款收付款客户、收款员发货商品库存出库仓管员、物流公司开发票应收账款开票财务人员、客户2.3 串成完整视图REA业务图谱把上面环节接起来就能画出业务链路订单创建事件消耗“商品库存”同时产出“应收账款”收款事件消耗“应收账款”同时产出“银行存款”。事件与事件之间用“来源事件”和“目标事件”连接形成链条。我一般分三层视图看业务流程层订单、支付、履约、售后资源流动层商品库存 → 应收账款 → 银行存款代理参与层客户、销售、仓库、财务三层不是三套表而是一套数据模型的三个视角。画图时注意箭头方向资源一定是从一个事件流向另一个事件。箭头画反了后面报表会数据错乱。我项目里就发生过一次订单资源流箭头画反导致月报里销售额变成负增长排查了半天才发现是方向错了。所以建议每张图谱都在末尾做一个校验每个资源的“总流入”减去“总流出”必须等于当前库存或余额不要等到报表错乱才回头查。3. 从REA模型到表结构完整落地过程3.1 数据库五张核心表设计这是大家最关心的落地部分。我通常建五张表resource_table资源表、event_table事件表、agent_table代理表、resource_event_link资源与事件关联表、event_agent_link事件与代理关联表。直接给一个可参考的简化DDL-- 资源表 CREATE TABLE resource_table ( resource_id VARCHAR(64) PRIMARY KEY, resource_type VARCHAR(32) NOT NULL, resource_name VARCHAR(128) NOT NULL, unit VARCHAR(32), created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 事件表 CREATE TABLE event_table ( event_id VARCHAR(64) PRIMARY KEY, event_type VARCHAR(32) NOT NULL, occurred_time TIMESTAMP NOT NULL, status TINYINT NOT NULL DEFAULT 0, biz_no VARCHAR(64) NOT NULL, ref_event_id VARCHAR(64), UNIQUE KEY uk_biz_no (biz_no) ); -- 代理表 CREATE TABLE agent_table ( agent_id VARCHAR(64) PRIMARY KEY, agent_type VARCHAR(32) NOT NULL, agent_name VARCHAR(128) NOT NULL ); -- 资源与事件关联表 CREATE TABLE resource_event_link ( link_id BIGINT AUTO_INCREMENT PRIMARY KEY, resource_id VARCHAR(64) NOT NULL, event_id VARCHAR(64) NOT NULL, direction TINYINT NOT NULL COMMENT 1:流入 0:流出, quantity DECIMAL(20,4) NOT NULL, amount DECIMAL(20,4) NOT NULL DEFAULT 0, biz_no VARCHAR(64) NOT NULL, INDEX idx_event (event_id), UNIQUE KEY uk_rel (event_id, resource_id, direction, biz_no) ); -- 事件与代理关联表 CREATE TABLE event_agent_link ( link_id BIGINT AUTO_INCREMENT PRIMARY KEY, event_id VARCHAR(64) NOT NULL, agent_id VARCHAR(64) NOT NULL, role VARCHAR(32) NOT NULL COMMENT 例如customer、merchant、warehouse, INDEX idx_event (event_id) );为什么拆五张表而不是直接堆字段原因有二一是REA要求资源与事件是多对多比如销售订单可以同时关联商品库存和优惠券两个资源不拆关联表字段会严重冗余二是方便统计分析按资源维度、代理维度任意组合切分。实测下来五张表比一张二十列大宽表灵活得多尤其在订单、库存、财务三方对账时特别爽。表数量看着有点多但每张表职责清晰维护心智负担反而更小。3.2 用API和事件驱动架构实现REA模型落到接口我习惯把每个REA事件映射成一个API或一个领域消息。比如订单创建就是POST /orders支付成功就是OrderPaid事件。关键点一个业务操作如果是复合的比如“下单并支付”必须拆成两个事件分别入表不能在事件表里合并。不拆后续想分析“用户下了单但迟迟不支付”就不可能甚至会出现“订单还在支付事件已经消失”这种荒唐事。事件驱动架构和REA天然搭因为REA讲资源守恒资源的流入流出必须平衡天然适合用消息对账。我做消息管道时会在两个事件间加协调动作确保“订单创建”事件和“库存扣减”事件要么同时可见要么都能回滚。这里要小心协调动作别做成强事务否则高并发下单时数据库锁非常痛苦。用事件表加状态机来做最终一致性实测在峰值下单量翻倍时依然稳定。接口幂等要利用事件表里的biz_no唯一键。同一张订单重复推过来第二次insert会被唯一键挡住。这个设计在对接外部渠道时太重要了曾经有渠道回调重复推了三遍不是幂等挡在数据库层库存直接变负数。3.3 与主流系统对接时的映射技巧理想很丰满现实常是别人只给你一堆接口。老系统没有REA概念怎么映射我总结三句口诀“字段对资源接口对事件客商对代理”。旧接口里的订单号、支付单号、出入库单号先映射成事件接口明细映射成资源联系人映射成代理。映射完成后写一层适配器把旧接口数据转换成REA标准事件再落库。这个适配器是临时还是长期保留取决于老系统下线节奏但至少保证数据链不断。有一个情况要特别警惕老系统里的“余额表”不是资源而是多个事件计算后的派生值。千万别把余额表直接当资源录入会导致后续结算单与余额表对不上。正确做法是反解出产生余额的若干事件按REA重建。这一块工作量比想象的大但做完后你会发现很多历史对账问题豁然开朗。我某次重构订单系统历史余额表里埋了一个隐藏的手续费逻辑不反解事件根本查不出来。4. 踩坑实录REA落地常见问题速查4.1 把资源表和流水表混在一起早期某次进销存改造我把“库存变动流水”当资源表来建结果每次库存加减都变成资源的新实例查询越跑越慢逻辑也绕。比如同一件商品进了10次货就建了10条资源记录完全没法表达“当前库存还剩多少”。后来才意识到资源表存的是当前可用的资源实体流水表记录的是资源的每一次流入流出。前者是主数据后者是事件日志。两者要分表但可以通过resource_id做关联。这也是最容易犯的错一不小心理性就会被“反正都是记录数据”带偏。4.2 事件只有正向没有反向导致对账困难下单有事件退款呢库存有出库事件退回入库呢如果只记录正向事件订单取消后资源链就断了。解决思路是事件表预留ref_event_id字段反向事件引用原事件。比如退款事件ref_event_id指向原支付事件查询对账时按引用拖出完整闭环。我用这个方法修复过一个退款链路以前退款金额总和总是对不上加了这个引用后链路一眼看穿。对账报表里加一列“原事件类型”业务方少问十几次。4.3 忽略幂等性重复入账坑到哭这个前面讲过biz_no唯一键但再强调一下不仅主事件要幂等资源事件关联表也要幂等。我早期只给事件表加了唯一键没给关联表加补偿逻辑重放时关联表多出几条重复资源变动。最后在关联表加了联合唯一索引重复投递直接报错被消息重试机制消化掉。上线后跑了一个月未再出现重复库存记录。如果你的系统里事件日志和资源变动是两条链路一定要检查两边是否都做了防重。4.4 业务复杂后模型崩坏的征兆与对策REA模型做到后期最大敌人是过度抽象。一个模型里几百个事件、几十种代理关系最后谁也维护不了。我建议按域收敛交易域里只放交易事件仓储域里只放库存事件跨域用事件同步。不要在一个域里试图描述所有业务。模型如果开始出现“这个表到底归谁管”的争论就说明域边界已经模糊了。下面是问题速查表症状根因解决思路资源数量失控把派生值当资源回归“被消耗/被获取”判断标准事件与事件对不上缺少ref_event_id增加反向引用形成闭环接口重复推送幂等缺失事件表和关联表都加唯一键模型难维护跨域大杂烩分域建模事件同步最后分享一个小经验REA模型上线后别急着删旧明细流水双轨跑一个月。让财报从REA链路出同时保留旧账两边核对无差异再切换。我第一次做迁移时急着关停旧库结果历史数据差了几笔手续费花了一周手工找平。从那以后双轨验证成了标准动作。REA模型不是万能银弹但它能把复杂的业务抽象得足够清晰只要你愿意按它的规矩来资源、事件、代理永远不要混在一起。