首页 / 资讯中心 / 文章详情

REA模型实战:从资源事件到订单系统建模与落地

REA模型实战:从资源事件到订单系统建模与落地 ★ FEATURED ARTICLE
如果你恰好也在做企业级系统的数据建模应该会对 REA 这三个字母有印象。REA 不是某个框架的缩写而是 Resource-Event-Agent 的缩写一套源自会计信息系统领域的业务建模思路。我第一次接触 REA是在做一个财务业务一体化项目的时候当时被订单、库存、应收应付一大堆单据搞得焦头烂额。后来用 REA 重新梳理才发现很多所谓复杂业务本质上就是资源和事件之间的流转。这篇文章不是理论复述而是把我实际做过的订单履约系统作为示例讲清楚 REA 的三个核心构件怎么落地到表结构和代码里还会把建模过程中几个容易栽跟头的地方单独拎出来。无论你是后端开发、业务架构师还是正在做领域建模的产品经理都可以拿这套思路来梳理自己手头的业务。看完你至少能回答三个问题REA 能用在哪些场景、怎么识别核心构件、落地时有哪些坑。1. REA 到底在解决什么问题1.1 传统表驱动建模的失真感很多业务系统一开始是围绕“单据”建的。客户要下单就建一张订单表仓库要发货就建一张发货单表财务要开票就建一张发票表。每张表各自管各自的字段再通过外键连起来。这种模式开发效率高但有一个隐患单表里存了大量“结果”而不是“过程”。比如订单表里有一个订单金额字段这个金额可能来自多个商品行、优惠券、税费。如果只存一个汇总值后续财务对账时根本不知道这个数怎么来的。更麻烦的是库存扣减通常直接改库存表的“可用数量”字段没有任何一条记录说明“这次扣减是哪个事件导致的”。一旦数据对不上排查链路就断了。我最早接触订单系统时就是这么干的订单表、订单明细表、库存表、账户流水表各玩各的。表面上功能都实现了但一遇到售后、退货、跨期对账就要写很多临时的 SQL 去还原当时的业务逻辑。不是表结构有问题而是我们一开始没有把业务里真正发生的“事件”作为核心一直围绕“单据”这个静态结果在打转。1.2 REA 的三个核心构件资源、事件、参与者REA 模型把业务世界拆成三样东西资源Resource企业拥有或控制的有价值的东西比如现金、库存商品、服务能力。资源是静态的它只在事件发生时被改变。事件Event改变资源状态的经济活动比如销售出库、采购入库、收款、付款。事件是动态的是模型里真正的“动词”。参与者Agent参与事件的人和机构比如客户、供应商、销售员、仓管员、财务。参与者分为外部参与者和内部参与者。用一个简单例子说明客户购买商品。客户是参与者商品是资源卖家发货并把货交给客户这个动作就是事件。在 REA 视角下“订单”本身不是核心核心是“发货”这个事件改变了资源的归属“收款”这个事件改变了现金资源的数量。我第一次把订单金额去掉、换成“发货事件 收款事件”后整个模型立刻变得清晰了。库存减少不是靠 UPDATE 库存表实现的而是由“出库事件”记录触发的应收账款增加也不是拍脑袋填的而是由“发货事件”和“客户”参与者共同决定的。1.3 为什么 REA 比单纯的表结构更适合复杂业务建模单纯的表结构设计是“为了存储而建模”REA 是“为了业务真相而建模”。它有几个明显优势第一天然满足审计追溯。每一个经济事件都关联资源、参与者、时间、数量、单价等于给每一次业务动作都留下了完整证据链。审计人员问“这批货发给谁了谁经手的库存少了多少钱回来没有”都能在模型中找到对应路径。第二财务逻辑和业务逻辑能统一。传统方式里业务模块和财务模块经常是两套数据。业务关注发货财务关注记账两边数字对不上。REA 模型强调事件同时影响资源流和权利义务这样业务事件直接可以推导出会计分录候选不需要再手工转换。第三对微服务和事件溯源架构也有启发。REA 中的“事件”和事件溯源里的“领域事件”概念非常接近。把每个资源变化建模为一组不可变的事件聚合之间通过事件通信这一点和现代后端架构天然契合。2. REA 建模的核心细节与实操要点2.1 从业务流程中识别三要素我见过不少人一上来就画 ER 图结果画出来的还是订单、订单明细那套东西。正确做法是先写流程文字再从文字里剥概念。举个例子一套标准的 B2B 销售流程可以写成客户在系统上下单。系统冻结库存。仓库拣货并发货出库。财务根据发货记录开票。客户打款财务核销。把这段话里的关键词拎出来名词里客户、仓管员、财务是参与者库存商品、现金、应收账款是资源。动词里下单、冻结库存、发货出库、开票、收款都是潜在事件。注意“下单”和“冻结库存”其实是两个不同的事件前者是客户承诺购买后者是企业内部把可用库存锁住。如果把它们合并成一个“订单状态变更”会丢失中间的因果链条。我在建模时倾向把“库存锁定”单独作为一个事件因为它是资源可用数量的变化对后续可承诺库存计算非常关键。2.2 双主体原则每个事件至少涉及两个参与者REA 模型有个容易被忽略的约束每个经济事件至少要关联两个参与者一个外部参与者一个内部参与者。比如销售出库外部参与者是客户内部参与者是仓管员。这样设计不是人为增加复杂度而是为了回答“谁动了资源”和“对谁负责”两个问题。我最初做模型时把发货事件只关联了“订单”和“商品”没有关联参与者的概念。结果后来要统计每个销售员的业绩、每个仓库的吞吐量完全没法查。补上内部参与者以后所有维度分析都能顺着事件表向外扩展。参与者的粒度也要提前定清楚。客户可以是“公司”也可能是“公司下的某个部门”销售员可以是“人”也可能是“小组”。一旦粒度不一致后续统计就会失真。我的建议是先粗后细初期按业务管理单元定义不要一上来就精确到人否则数据维护成本会很高。2.3 事件属性与增量存量设计事件表的核心字段不只是“时间、类型、关联ID”还应当包括“流入数量/流出数量”或“增加额/减少额”。这背后是会计里最基本的平衡公式期末存量 期初存量 本期流入 - 本期流出。用库存举例。不要直接在库存表里放“当前库存”这一个字段让它被反复 UPDATE而是把每次出入库记入事件表库存表只保留初始余额。需要当前库存时用“期初数 所有入库事件合计 - 所有出库事件合计”计算。这种方式虽然查询时多了聚合但任何一笔库存变化都能追溯到对应事件。这种设计在财务余额上作用更大。应收账款余额不需要每天 UPDATE只要把每次“发货事件”对应的应收增加和每次“收款事件”对应的应收减少记录清楚余额自然能算出来。等于把“状态”变成了“事实的推导结果”这是 REA 模型特别值钱的地方。2.4 不仅仅是数据库设计更是业务语义统一做 REA 建模时团队里必须有业务人员参与。因为识别“资源”和“事件”依赖业务语义。比如“采购入库”事件中资源是库存商品但同时会产生“应付账款”这种资源。如果团队里没有财务视角很容易漏掉这些隐性资源。我经历过的项目里开发经常抱怨财务术语看不懂财务又觉得系统没有记账依据。通过 REA 模型两边有了共同的词汇表库存、现金、应收、应付这些资源发货、收款、采购、付款这些事件。业务上说“发货”系统里就是一条可回溯的事件记录。这种语义统一比表结构对齐更重要。3. 用 REA 模型设计一套订单系统的完整过程3.1 需求场景设定假设某公司内部要做一套 B2B 订单系统核心诉求有三个客户下单后仓库能按订单发货。财务能看到每一笔应收和收款记录。管理层能追踪每笔销售对应的库存变化和资金回笼情况。没有 REA 之前这种系统通常会做成订单主表 订单明细表 发货单表 收款单表。现在按 REA 重新建模。3.2 识别概念并归类先说识别结果资源库存商品、应收款项、现金或银行存款。事件销售出库、应收确认、收款、库存锁定。参与者客户、销售员、仓管员、财务。这里有一个需要说明的地方“客户下单”我把它放在事件列表里但没有作为独立的经济事件建模。因为订单本身不改变资源数量它是客户承诺的载体。真正改变资源的是“出库”和“收款”。所以订单表退化为一个契约上下文它把多个事件串起来方便查询。这个取舍我后面还会详细说。3.3 建立概念模型表为了落地我用表格把每个事件和资源、参与者的关系整理清楚事件流入资源流出资源外部参与者内部参与者关键属性销售出库应收款项库存商品客户销售员、仓管员出库数量、单价、时间收款现金/存款应收款项客户财务金额、收款时间、凭证号采购入库库存商品应付账款供应商采购员、仓管员入库数量、成本价、时间付款应付账款现金/存款供应商财务金额、付款时间、凭证号这张表就是整个系统建模的核心。它直接决定了后面数据表和代码服务的划分方式。3.4 数据表结构落地按上面的概念模型我给出一个简化但可运行的表结构。假设用 PostgreSQL资源表先设计余额表-- 资源余额表 CREATE TABLE resource_balance ( resource_id BIGINT NOT NULL, resource_type VARCHAR(40) NOT NULL, -- INVENTORY, RECEIVABLE, CASH balance NUMERIC(18,2) NOT NULL DEFAULT 0, updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (resource_id, resource_type) );然后是事件主表和事件资源流水表-- 经济事件主表 CREATE TABLE economic_event ( event_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, event_type VARCHAR(40) NOT NULL, -- SHIPMENT, PAYMENT, PURCHASE occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(), external_agent_id BIGINT NOT NULL, internal_agent_id BIGINT NOT NULL, document_no VARCHAR(64), UNIQUE (document_no) ); -- 事件资源流水表 CREATE TABLE event_resource_flow ( flow_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, event_id BIGINT NOT NULL REFERENCES economic_event(event_id), resource_id BIGINT NOT NULL, resource_type VARCHAR(40) NOT NULL, direction SMALLINT NOT NULL, -- 1 流入, -1 流出 quantity NUMERIC(18,2) NOT NULL, unit_price NUMERIC(18,2), amount NUMERIC(18,2) NOT NULL );这套结构的核心是economic_event表只保存事件本身所有资源数量和金额变化都放在event_resource_flow里。每一条流水都通过event_id指向一个经济事件由此保证“任何余额变化都有案可查”。3.5 关键代码实现思路实现销售出库时不能只 INSERT 一条事件记录还要在同一事务里更新相关资源余额。下面是一个 PostgreSQL 事务示例BEGIN; INSERT INTO economic_event (event_type, occurred_at, external_agent_id, internal_agent_id, document_no) VALUES (SHIPMENT, now(), :customer_id, :warehouse_staff_id, :shipment_no); -- 获取新事件 ID这里用 lastval() 示意 INSERT INTO event_resource_flow (event_id, resource_id, resource_type, direction, quantity, unit_price, amount) VALUES (lastval(), :inventory_item_id, INVENTORY, -1, :ship_qty, :cost_price, :cost_amount); INSERT INTO event_resource_flow (event_id, resource_id, resource_type, direction, quantity, unit_price, amount) VALUES (lastval(), :customer_id, RECEIVABLE, 1, 1, :sale_price, :receivable_amount); -- 更新库存余额 UPDATE resource_balance SET balance balance - :ship_qty, updated_at now() WHERE resource_id :inventory_item_id AND resource_type INVENTORY; -- 更新应收余额 UPDATE resource_balance SET balance balance :receivable_amount, updated_at now() WHERE resource_id :customer_id AND resource_type RECEIVABLE; COMMIT;这段代码的核心在于先写事件流水再更新余额。如果余额更新失败整个事务回滚不会出现事件有记录但余额没变化的情况。在实际项目中我还会在economic_event表上做唯一约束保证同一张业务单据不会重复入账。余额的实时计算也可以改成异步聚合前提是事件流水本身要保证完整。如果业务量不大直接用上面的同步事务最稳定也最容易向审计解释。4. 常见问题与排查技巧实录4.1 资源粒度具体批次还是 SKUREA 模型里的“库存商品”到底是按 SKU 建模还是按批次建模这是第一个容易纠结的地方。如果业务要求先进先出、批次追溯那么资源粒度必须细化到“批次 SKU”。否则只需要按 SKU 管理即可。我的建议是资源粒度必须和业务追溯粒度保持一致。做医药、食品行业批次是硬要求做标准件电商SKU 就够。只考虑当前需求不考虑追溯粒度后期补批次非常痛苦。4.2 “订单”到底算不算 REA 模型的一部分这是最容易把新手绕进去的问题。严格按 REA 的本意订单不是资源也不是经济事件它只是一组业务承诺的集合。但在实操系统里我们仍然需要订单表因为客户要看订单状态、销售要知道承诺内容。正确做法是订单作为聚合根存在但订单金额、库存变化不应该直接写在订单表里。订单只是把多个“销售出库”“收款计划”串起来的上下文。我见过一些项目把订单金额和库存扣减直接从订单表里改结果 REA 模型只是披了一层皮并没有真正落地。订单可以作为入口但不能作为余额变化的直接来源。4.3 事件是不可变的吗用 REA 建模后事件表里的数据应当是不可变的。如果发生退货不应该把原来的销售出库事件 UPDATE 成“数量 - 1”而应该新增一个“退货入库”事件用正向流入冲抵。这和事件溯源的思想一模一样。我在实际项目里发现开发人员习惯直接改原记录因为看起来省事。但一旦开始改原事件所有基于事件累加的余额全部会错乱。后续对账的时候根本分不清哪些是被改过的。所以事件表一定要做成只能 insert不能 update。这不仅是建模问题也是系统操作纪律问题。4.4 我实际踩过的坑第一次用 REA 建模时我犯了一个典型错误把“库存锁定”事件和“销售出库”事件混在一起。结果下单后直接减了可用库存仓库还没发货报表上库存已经少了。等客户取消订单我又得把库存加回来来回改数数据口径非常混乱。后来把事件拆成“库存承诺”“库存释放”“销售出库”三步每一个动作都有独立事件问题才消失。这也是 REA 模型给我最大的启发事件必须对应真实发生的业务动作不能为了省表就合并。另一个坑是参与者表直接用公司名称做主键。后来发现同一个客户可能在不同地区有不同的销售团队客户维度和销售维度根本不是一回事。改成独立参与者表并用 ID 关联后分析维度才正常。5. REA 模型还能往哪个方向扩展5.1 和事件溯源架构的结合如果你正在设计微服务系统REA 模型的“事件”和事件溯源架构里的“领域事件”可以自然统一。每个服务把资源变化以事件形式发出其他服务订阅事件更新自己的读模型。订单服务发出“OrderShipped”库存服务更新库存余额财务服务更新应收余额。这和 REA 模型天然契合。我在一个库存服务里就是这么干的库存余额表只是投影真正的数据源是连续的库存事件流。任何余额不一致都可以通过回放事件流来修正。这套机制让故障恢复变得非常简单。5.2 与多维数据分析的配合REA 模型建成后事件表天然可以做成星型模型的事实表。资源、参与者、时间都是维度。管理层想看“东北区域客户贡献了多少应收余额”“哪个销售员的发货金额最高”只要在事件表上聚合即可。相比传统订单表分析口径会更准确因为事件表记录的是真实的流动而不是单据状态。5.3 工作流审批与 REA 的边界REA 不关心审批流。审批是过程控制事件才是业务事实。一个采购申请可以流经多层审批但只有最终“采购入库”事件才让库存和应付账款发生变化。设计系统时把审批状态和业务事件分开存储很重要。否则状态字段又变成了一个没有事实依据的“大杂烩”。我在实际项目里会单独建一套 workflow_instance 表来跟踪审批状态经济事件只在审批通过后触发。这样审批流调整了业务事件流依然稳定两边不会互相污染。最后说一点个人体会REA 不是银弹但它提供了一种特别扎实的思考框架。以前我拿到需求就想着建哪些表、存哪些字段现在我会先问自己这笔业务里哪些资源在流动哪些事件在改变这些资源哪些参与者牵涉其中。想清楚这三个问题表结构、接口设计、事务边界都会自然浮出水面。如果你正在做业务系统重构或者被对账、审计、多模块数据不一致折磨过我建议花半天时间把核心流程用 REA 梳理一遍。不用急着推翻现有代码先把概念模型画清楚。你可能会发现不少曾经以为必须靠复杂逻辑解决的问题其实只是事件没建模完整而已。
阅读完成 · 觉得有帮助?
咨询建站