news 2026/10/20 3:35:20

REA模式:用资源-事件-代理搞定企业采购领域建模

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
REA模式:用资源-事件-代理搞定企业采购领域建模

如果一个项目标题只有三个字母,最容易想到的是“React 拼到一半”或者某个文件名缩写。但对我这种常年做企业应用和账务建模的人来说,“rea”这三个字母更自然指向一种经典的业务领域建模方式——资源-事件-代理,也就是 REA 模式。它不绑定任何开发框架,只是一套描述业务语义的思维模型,专门用来回答企业里“钱、货、单据、责任”四条线到底是怎么流动的。

我最近拿这个模式做了一个模拟项目:给某制造企业的采购域建立一套从采购申请、供应商发货、仓库验收到财务付款的领域模型。过程里最大的感受是,传统建表思路很容易一上来就讨论字段,而 REA 逼着你先想清楚:什么是有价值的资源?哪些动作是真正发生的事件?参与的人和组织分别是谁?想清楚这三个问题,后面建表、写代码、出报表都顺了。

这篇文章会从 REA 的核心元素开始,再用一个采购流程完整演示建模过程,最后给出代码结构、数据库表设计和一组踩坑清单。适合这几类读者:想用 DDD 但找不到业务语义抓手的开发者,正在做采购、库存、结算这类系统重构的中级工程师,以及需要为多套业务系统制定统一数据标准的架构师。

你先别急着搜代码。任何含糊的项目名背后都有一件事要想清楚:这个项目的核心业务价值到底是什么?REA 模式就是帮你想清楚这件事的。

1. “rea”到底指什么:从三个字母到一套业务框架

如果按字面拆,“rea”可以被还原成“Resource-Event-Agent”的首字母拼接。Resource 是资源,Event 是事件,Agent 是代理。这套模型最早出现在会计信息系统领域,1982 年由一位会计信息系统学者提出,目的是替代那种只按“科目”“凭证”“余额”机械建模的老路子,让系统里的事实能够反映企业真实经济过程。

打个比方你就明白了。传统账本记录的是“账户余额”,比如库存 100 件、现金余额 5000 元。但余额是怎么变成 100 件和 5000 元的?中间经历了哪些交易、谁经手的、对应哪张单据?传统模型不一定能回答。REA 则更像流水账,它要求把每一笔“事件”完整记录:某年某月某日入库 50 件原料,某年某月某日支付 5000 元货款。余额不是直接保存的字段,而是把事件流水相加得出的结果。这样一来,审计、追溯、对账全都变得可靠。

我选择用采购域做演示,是因为采购流程刚好包含了 REA 最典型的要素:原料入库是一个资源流入事件,付款是一个资源流出事件,采购员和供应商是代理,采购订单和收货单则是事件发生时的记录凭证。但要注意,单据本身不是资源,单据所代表的物料和资金才是资源。这个区分很多人一开始会搞混。

那么 REA 能解决什么问题?最核心的是跨部门数据一致性。制造业里,仓库、采购、财务各自的系统往往口径不统一,仓库说“到货了”,财务说“还没付款”,业务说“订单都下出去了”。大家都有数据,但串不到一起。REA 把大家关注的事实压成同一套原语,之后无论出库存报表、应付报表还是利润分析,都从同一套事件出发,口径自然一致。

我这里说句实在话:REA 不是银弹,小型 CRUD 系统并不需要这么重的建模。但凡是涉及对账、审计、跨系统协调的业务域,它几乎是最值得先试的建模工具。

2. REA 建模的三块基础积木:资源、事件、代理

2.1 资源:有经济价值的“存量”

资源就像一个容器里的东西,会被业务过程增加或减少。常见资源包括原材料、产成品、现金、应收账款、固定资产、无形资产。判断一个实体是不是资源的简单标准有两个:第一,它能用数量或金额计量;第二,业务过程中存在让它的数量发生增减的事件。

采购域里,原料库存是资源,现金账户余额是资源,供应商开给我们的发票对应的应付义务也是资源。采购订单号、合同编号这些字段不是资源,它们只是业务单据的唯一标识。订单上的“承诺”如果要对未来库存计划产生影响,可以建模成承诺型资源,但这点通常只在大型供应链系统里才需要深入设计。

这里最容易犯的错,是把“属性”当成“资源”。比如一台设备的状态是“运行中”,状态本身不是资源,设备才是资源。设备价值会因为“采购”“折旧”“处置”这些事件而增减,运行状态只是设备资源上的一段属性,不应被单独建成资源表。

2.2 事件:资源在时间轴上发生的“动作”

事件是 REA 最有价值的部分。它代表一次具体的资源流入或流出,必须带有发生时间、数量或金额,并且不能再向下拆分。比如“采购到货”是一个经济事件,“财务付款”是另一个经济事件。事件记录的是增量变化,不是某个时刻的存量结果。

事件分为两类。第一类是经济事件,也就是实际发生的资源增减,比如入库、出库、收款、付款、退货。第二类是承诺事件,代表未来可能会发生的资源增减,比如下达采购订单、接受销售订单。承诺事件不一定最终履约,但会影响之后的计划和分析。

我见过不少团队把事件表做成“操作日志”,往里面塞“用户点了一下保存”这种无业务语义的动作。这是典型的过度记录。REA 的事件必须能回答“资源发生了怎样的变化”,而不是“用户在界面上做了什么”。界面动作可以记审计日志,但那是另一条技术线,不能混进领域事件流。

2.3 代理:谁发起了事件,谁来负责

代理是事件的参与方,分为内部代理和外部代理。内部代理通常是企业内的某个角色,比如采购员、仓管员、财务审核人、部门;外部代理是交易对手,比如供应商、客户、物流承运公司。代理建模的核心不是简单的“操作人”字段,而是独立的代理实体,包含名称、编号、所属组织、角色类型。

经济事件至少要关联两类代理:内部代理回答“企业里谁负责这件事”,外部代理回答“和哪个外部组织发生关系”。例如“仓库收货”事件,内部代理是仓管员,外部代理是供应商。如果把外部代理漏掉了,日后对账就只能靠单据上的对手方名称硬匹配,字段一改就对不上。

代理表可以做得相对简单,但不要把它并进 event 表。独立成表意味着代理本身有主数据,可以被多个事件引用,也方便后面做权限控制和组织维度汇总。

2.4 关系:流入流出、二元性和责任链

三块积木之间靠关系串起来:

  • 资源与事件之间是“流入/流出”关系,一个事件会让一个或多个资源增加或减少。
  • 事件与代理之间是“执行/参与”关系,记录谁参与了这次变化。
  • 两个经济事件之间应该成对出现,一方是资源流出,另一方是资源流入,构成“二元性”。典型例子是“销售出库”对应“收款”,“采购入库”对应“付款”。

二元性这条最容易被忽略,但它恰恰是 REA 能对账的根本原因。假设采购入库事件让原材料增加 100 件,价值 8500 元;付款事件让现金减少 8500 元。两个事件如果不配对,报表只能分别看到“入库 100 件”和“付款 8500 元”,无法证明这批货和这笔钱是同一笔交易。加了二元性关联之后,就能完整描述“一手交货、一手付钱”的价值闭环。

元素通俗叫法典型例子判断方向
资源变化的货或钱原材料、库存现金、应收/应付有唯一身份,能量化价值和数量
事件发生的动作到货、出库、付款、收款有发生时间,有数量金额,不可再拆
代理做事的人或组织采购员、仓管、供应商、客户参与事件并承担对应责任
关系连接逻辑入库事件与付款事件配对能回答谁在何时改变了哪项资源

很多传统模型就直接存“当前状态字段”,例如在订单表里存一个“status = 已付款”。REA 不这么干,它会把“付款事件”单独落下来,状态只是付款事件发生后派生出的查询结果。这个思路很像事件溯源,只是一般事件溯源更多是从软件架构角度讲,REA 则是从业务语义角度讲,两者天然互补。

3. 实操篇:把制造企业的采购-付款流程改造成 REA 模型

3.1 先框定业务范围

我用一个模拟的制造企业采购流程来演示。流程大致是:生产部提出原材料需求,采购员向供应商下采购订单,供应商发货后仓库验收入库,财务根据发票安排付款。这是典型的“产、供、销”里供应一环,涉及库存资源、现金资源和供应商代理。

动手前先用一页纸把业务主干画出来,不画关系图,只画时间线:

  1. 采购申请被批准;
  2. 采购员创建采购订单;
  3. 供应商发货,仓库收货并入库;
  4. 财务核对供应商发票;
  5. 财务完成付款。

这条时间线里,真正值得作为 REA 事件的是 2、3、4、5。采购申请只是内部审批动作,不直接改变资源价值,一般可以当作前置流程处理,不进入核心 REA 事件流。如果需要严格跟踪“申请占用预算”,可以把“采购申请批准”当成承诺事件模型化,但大部分项目中它会成为系统工作流的一部分,而非领域事件。

3.2 第一步:拆出事件清单

不要一上来就建表,先写事件。把上面提到的时间线逐条转成事件描述,并标出它改变的是什么资源、资源是流入还是流出。我这里整理了一张表:

事件名称事件类型涉及资源资源方向关键代理
下达采购订单承诺事件对原材料 B 的采购承诺承诺增加采购员、供应商
仓库收货经济事件原材料 B 库存流入仓管员、供应商
发票核对确认经济事件应付义务流入财务审核人、供应商
支付货款经济事件现金流出出纳、供应商

这张表列完之后,你就有了第一版领域事件清单。事件清单的质量直接决定后续所有建模,所以要反复问一句:“把它拆成这样,业务人员能看懂吗?”如果业务人员说“我们还有质量检验环节”,你就要决定是否把“质检合格入库”作为一个独立经济事件。通常可以拆成“到货待检”和“检验合格入库”两个事件,但多数简化模型会把质检放进入库事件的属性里。

3.3 第二步:确认资源和代理

基于事件清单,资源表可以初步定为:

  • 原材料 B:单位、标准成本、状态;
  • 现金:按银行账户或资金池拆分;
  • 应付义务:用来表达“已收货未付款”的负债。

代理表可以定为:

  • 内部代理:采购员、仓管员、财务审核人、出纳;
  • 外部代理:供应商、物流公司。

这里要提醒一点:现金不建议只建一个“现金”资源。制造企业通常有多家银行、多币种、多个资金账户,如果现金资源不区分账户,月末资金对账时你会被数据搞疯。至少按账户拆分,报销和付款事件都指向具体账户。

3.4 第三步:建立流入流出连接与二元性

事件和资源之间使用“流入/流出”连接。这种连接我通常叫 flow,里面有事件 ID、资源 ID、方向、数量、金额。比如“仓库收货”事件产生一条 flow,方向是流入,资源是原材料 B,数量是 100 件;“支付货款”事件产生另一条 flow,方向是流出,资源是现金账户,金额是 8500 元。

接着再把两个经济事件用二元性关联起来,形成“货进钱出”的闭环。仓库收货产生的“原材料流入”对应付款产生的“现金流出”,二元性记录两端事件的 ID。这样从任何一端出发,都能找到另一端对应的业务凭证。

这一步做完后,可以做一个自检:把每个资源的流入数量减去流出数量,是否等于你业务上认同的期末存量;把每个非现金资源的流入金额和流出金额配对,是否和资金流水吻合。如果对不上,说明事件清单仍有遗漏,不要急着写表结构。

3.5 对比传统订单表与 REA 模型

最后放一个传统建模和 REA 建模的对照,能帮你把思路彻底扭过来。

关注点传统订单表设计REA 模型设计
订单状态状态字段直接变更用承诺事件与经济事件推导
库存数量库存表存当前数量由入库事件减去出库事件得出
应付余额应付表存当前余额由收货事件金额减去付款事件金额得出
“谁做了操作”订单表操作人字段独立的代理实体关联事件
审计口径覆盖历史字段事件流水永久保留

看到差异了吧。传统模型方便的是“快速取当前值”,代价是历史不可回溯、跨系统口径难统一。REA 模型最初写报表麻烦一点,但每张报表的出处都有据可查,这才是企业级系统真正需要的可靠。

4. 从模型到实现:代码结构和数据库落表

4.1 用类型安全代码固定领域规则

建模完成后可以先用代码把领域类型表达出来。这里我用 Java 的 record 类型做示例,只是为了表达语义,不代表只能这么写。关键点是事件里不保存余额,只保存本次变化量。

public record Resource( String id, String code, String name, String unit, BigDecimal unitPrice, boolean active ) {} public enum AgentType { INSIDER, OUTSIDER } public record Agent( String id, String code, String name, AgentType agentType, String orgScope ) {} public enum EventType { COMMITMENT, ECONOMIC } public record Event( String id, EventType type, Instant occurredAt, BigDecimal quantity, BigDecimal amount ) {} public enum FlowDirection { IN, OUT } public record Flow( String id, String eventId, String resourceId, FlowDirection direction, BigDecimal quantity, BigDecimal amount ) {} public record Duality( String id, String decrementEventId, String incrementEventId ) {}

这段代码最核心的约束是:resource 和 event 通过 flow 产生关系,event 自身不维护“变化后的余额”。你可能会想在 Event 里加一个 resourceId 字段,这也可以,但一旦一个事件涉及多个资源,这个字段就会被迫冗余。我更推荐把“事件和资源的关系”单独拆成 flow 表,这样一对多、多对多都自然支持。

4.2 数据库表设计示例

数据库落表时,先建资源、代理、事件、流程、二元性五类核心表。依然用 PostgreSQL 风格示例。

CREATE TABLE resource ( id uuid PRIMARY KEY, code varchar(32) UNIQUE NOT NULL, name varchar(128) NOT NULL, unit varchar(16) NOT NULL, unit_price numeric(18,2) NOT NULL DEFAULT 0, active boolean NOT NULL DEFAULT true ); CREATE TABLE agent ( id uuid PRIMARY KEY, code varchar(32) UNIQUE NOT NULL, name varchar(128) NOT NULL, agent_type varchar(8) NOT NULL CHECK (agent_type IN ('INSIDER','OUTSIDER')), org_scope varchar(64) ); CREATE TABLE event ( id uuid PRIMARY KEY, event_type varchar(16) NOT NULL CHECK (event_type IN ('COMMITMENT','ECONOMIC')), event_time timestamptz NOT NULL, quantity numeric(18,4) NOT NULL DEFAULT 1, amount numeric(18,2) NOT NULL DEFAULT 0 ); CREATE TABLE flow ( id uuid PRIMARY KEY, event_id uuid NOT NULL REFERENCES event(id), resource_id uuid NOT NULL REFERENCES resource(id), direction varchar(8) NOT NULL CHECK (direction IN ('IN','OUT')), quantity numeric(18,4) NOT NULL DEFAULT 1, amount numeric(18,2) NOT NULL DEFAULT 0 ); CREATE TABLE duality ( id uuid PRIMARY KEY, decrement_event_id uuid NOT NULL REFERENCES event(id), increment_event_id uuid NOT NULL REFERENCES event(id) );

事件和代理的关系可以用中间表表达:

CREATE TABLE event_agent ( event_id uuid NOT NULL REFERENCES event(id), agent_id uuid NOT NULL REFERENCES agent(id), role varchar(32) NOT NULL, PRIMARY KEY (event_id, agent_id, role) );

写到这里特别强调一个投产经验:初期不要密集建外键约束。历史数据导入时,事件顺序不一定整齐,早期的大堆外键会让你不得不反复禁约束再重建。我的习惯是先建主键和非空约束跑通流程,等数据迁移稳定后再在核心关联列上补外键。用“先松后紧”的方式上线,比一开始追求完美约束能少踩很多坑。

4.3 从 REA 模型反推库存、应付和利润

REA 模型里的所有查询都围绕聚合事件展开。比如要查某个资源的当前库存,就把所有经济事件中该资源的流入量汇总,再减去流出量。

SELECT r.code AS resource_code, r.name AS resource_name, COALESCE(SUM(CASE WHEN f.direction = 'IN' THEN f.quantity ELSE 0 END), 0) AS total_in, COALESCE(SUM(CASE WHEN f.direction = 'OUT' THEN f.quantity ELSE 0 END), 0) AS total_out, COALESCE(SUM(CASE WHEN f.direction = 'IN' THEN f.quantity ELSE 0 END), 0) - COALESCE(SUM(CASE WHEN f.direction = 'OUT' THEN f.quantity ELSE 0 END), 0) AS stock_qty FROM flow f JOIN resource r ON r.id = f.resource_id JOIN event e ON e.id = f.event_id WHERE e.event_type = 'ECONOMIC' GROUP BY r.code, r.name;

这段 SQL 的思想是:库存不是一个存储字段,而是一个投影结果。同理,想要得到一段时间内的应付余额,就用收货事件的总金额减去付款事件的总金额;想要算毛利,就用销售出库资源金额减去对应采购资源金额。所有分析口径都从事件和 flow 推出来,这样各个报表之间不会出现“库存对不上、利润算错”的经典尴尬。

有一点必须提醒:事件表里的 amount 要明确币种口径。如果涉及多币种,至少要同时保留“原始币种金额”和“本币金额”两列,否则历史汇率一变动,回溯报表就会全部失真。不要偷懒只存一个本币金额,因为你永远无法判断历史上那个汇率是怎么折算的。

5. 真正落地时最容易踩的五个坑

5.1 把“状态”当成“事件”或“资源”来设计

最典型的问题是在资源表里直接存“库存余额”字段,或者用状态字段覆盖历史。这样做的好处是查询简单,坏处是历史被抹掉、并发更新锁容易兜不住。REA 的做法是永远保存事件增量,余额只是聚合计算的结果。

如果系统里已经有大量状态表,可以从分钟级快照开始迁移。先把当前状态作为基准快照,再向前补做一段事件流水,将今天的余额与事件流投影的结果对齐,差异逐笔排查。别试图从五年前开始回放,风险和工作量都失控。

5.2 忘记建立二元性,事件之间是孤岛

只记录“货出库了”“钱收到了”,但两条事件之间没有任何关联,月末对账时就得靠代码来回匹配。最初建表时,duality 表和 flow 表同等重要。如果实在不想加表,也一定要在 event 表里增加一个 linkage_key,把出库事件与收款事件挂在一起,保证链路可追踪。

5.3 没有区分承诺事件与经济事件

很多项目把订单状态简单存成字符串,状态一改就把之前的业务动作覆盖了。更合理的方式是把订单下达视为承诺事件,把到货和付款视为经济事件。承诺事件可以被修改、取消,经济事件一旦发生后就不应被直接修改,只能通过反向事件冲销。这个规则一旦在代码层落地,业务上的“追单”“退货”“冲销”就都有了对应的标准动作。

5.4 代理建模太薄弱,追责只能看日志

事件表加一个“操作人”字段,并不等于代理建模。代理是独立的主数据,拥有组织归属和角色信息。特别是外部代理,如果只是存一个字符串名称,供应商改名或重复时就会对不上账。把内部代理和外部代理都建成独立表,并为每次事件记录参与角色,责任链路才完整。

5.5 复杂度失控:什么都想装进 REA

REA 很适合描述价值链,但没必要把通知记录、页面点击、日志系统都装进来。如果一个域里没有“资源增减”,强行套 REA 只会得到一张张无意义的抽象表。建模时先划定事件边界:真正改变资源数量或金额的动作才进 REA,其他行为仍按其原本的技术或业务逻辑建模。

下面这张速查表,适合写设计文档时直接贴进附录。

现象可能原因对应做法
当月出库单找不到对应收款未建 duality增加事件配对或 linkage_key
库存与业务单据反复对不上资源表冗余了余额字段余额改为由事件聚合查询
审计追溯不到经办人代理模型缺失独立 agent 表并区分内外部
状态机越写越复杂承诺事件被当状态字段分离承诺事件与经济事件
数据模型没人敢改外键约束过早过密先建主键,后补约束
多币种报表金额错乱只存了本币金额保留原始币种与本币双口径

6. REA 的适用圈:什么项目适合,什么项目别硬套

不是所有项目都需要 REA。简单的内容管理、设备台账、站内通知这类模型,用普通表结构反而更高效。因为这类系统没有复杂的资源流动,也没有对账和审计需求,强行用事件流水建全套结构,只会让本来一小时能开发完的功能变成三天。

但以下情况,REA 几乎是首选:

  1. 核心会计与结算系统,需要记账和对账同时完成;
  2. 多套异构系统之间需要统一业务语义,REA 可以作为公共语义层;
  3. 供应链计划涉及订单承诺和实际履约,需要同时管理计划和事实;
  4. 大型组织做数字化转型,管理层要求业务全程可追溯。

在这些场景里,REA 的价值不是“用几个新名词”,而是把散落在各处的业务事实变成统一事件流。你可以先只做一个域,比如“采购到付款”或“订单到回款”,跑通后再逐步扩展到其他域。

从 REA 可以很自然地延伸出两条深水区路线。一条是事件溯源:既然业务事实已经表达为事件流,就让事件流作为系统真相源,数据库里的表和投影都从事件流派生。另一条是 DDD 落地:REA 可以直接映射到领域模型里的聚合根、领域事件和值对象,减少业务概念到代码概念的转译成本。

我在几个模拟项目里反复试过一种做法,先不写代码,拿一张白纸把资源、事件、代理之间的箭头画出来。画到能跟业务人员清晰解释“每一次付款对应哪一次收货”的时候,再往数据库模型转。这个习惯帮我避免了至少两次大规模返工。很多项目死掉不是因为技术不行,而是前期没有从业务事实上对齐口径。

所以回到标题本身,“rea”三个字母很短,但它背后代表的问题很长:你的企业里,哪些资源在流动?哪些事件改变了它们的价值?又有哪些代理该为这些变化负责?把这三个问题回答清楚,比任何代码框架都更能决定项目上限。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 3:34:32

VRRP虚拟路由器冗余协议:原理、配置与排错实战指南

1. 为什么VRRP至今仍是网络高可用的“压舱石”第一次接触VRRP是在一个园区网的改造项目里,核心交换机双机部署,下面挂了三十多台接入设备,用户要求“断一台核心,业务不能断”。当时我第一反应是用堆叠或者链路聚合,但堆…

作者头像 李华
网站建设 2026/10/20 3:34:22

用C#和ASP.NET Core在浏览器中查询Access数据库的轻量方案

简介:这份资源面向具备一定C#基础的开发者与小型项目维护人员,解决如何通过Web页面查询Access数据库的实际问题。内容围绕ASP.NET与ADO.NET技术栈展开,涵盖OleDbConnection连接建立、OleDbCommand执行SQL、OleDbDataReader读取结果&#xff0…

作者头像 李华
网站建设 2026/10/20 3:33:55

Linux数据链路层深度解析:从以太网帧到XDP性能优化实战

1. 为什么数据链路层值得单独拎出来讲很多人学网络,习惯性地从 TCP/IP 四层模型往下背,背到数据链路层就开始含糊其辞——“不就是网卡那一层嘛”“负责 MAC 地址的呗”。我在实际排查网络故障的头几年也是这个心态,直到有一次线上环境出现间…

作者头像 李华
网站建设 2026/10/20 3:33:00

谷歌浏览器解压版:绿色便携运行原理与实战配置

简介:本资源为谷歌浏览器(Chrome)绿色解压版,面向前端开发、测试工程师及对系统环境洁净度有要求的技术人员,解决常规安装版可能带来的注册表污染、权限依赖或多人共用设备时的配置冲突问题。压缩包为ZIP格式&#xff…

作者头像 李华
网站建设 2026/10/20 3:32:44

SAA停摆项目接手实录:ARGI模块从零跑通与调优避坑指南

1. 项目缘起与整体设计思路拆解1.1 这个标题到底在说什么先把标题拆开看。"SAA 停摆"指的是某个流式分析架构项目在交接过程中陷入停滞——原团队撤了,代码仓库还在,但没人能说清楚它到底能不能跑、怎么跑。"前团队的 ARGI"是这套架…

作者头像 李华