前阵子接手了一个老旧的进销存系统重构,业务方提了一个让我印象很深的要求:"不要只给我做一套记账工具,我要能讲清楚每一笔业务是怎么发生的。"当时我翻遍了企业内部所有资料,最终在一个旧文档角落里看到项目代号:rea。一开始我以为是某个人的名字简写,后来才意识到,这是 Resource-Event-Agent 的缩写,一套比"借贷记账"更贴近业务本质的建模思路。如果你也正在做 ERP、财务中台、数据仓库相关的工作,或者只是想让系统不再"只记结果、不记过程",那么这篇文章应该能给你一条新的思路。
rea 不是一个具体的软件或框架,而是一种业务建模思想。它把任何经济业务看成"资源在事件中流转,代理参与其中"的结构化记录,天然适合用来做进销存、订单、财务、审计等系统的领域建模。我花了大半个项目周期去理解和实践这套思路,踩了不少坑,也收获了很多;下面就把这个过程完整拆开讲。
1. 为什么我会盯上"rea"这个代号
1.1 一次库存对账项目带来的线索
事情是这样开始的:某家商贸公司要统一订单、库存和财务三套系统的数据口径。过去三个系统各算各的:订单系统管销售订单,库存系统管出入库流水,财务系统管应收应付和总账。老板问"这个月的销售毛利到底是多少",三个部门能给出三个数字。整个项目组都在找一种能同时覆盖这些业务场景的底层模型。
翻资料时,一份很老的《系统设计说明》里提到:"建议业务模块的基线模型参考 rea 思想。"当时的系统负责人早已离职,文档里也没有展开解释。我就顺着这个词去查,发现 REA 的全称是 Resources-Events-Agents,也就是资源、事件、代理三要素。这个概念最早出现在会计信息系统研究领域,核心观点很直接:与其把业务数据转换成借贷分录后存进账本,不如直接保存"谁在什么时间、用什么资源、做了什么事"这样的原始记录。
随着资料越查越深,我发现很多后来的财务中台、业务中台设计,本质上都在向 REA 回归:把业务过程变成可追溯的事件流,而不是只存储结果。这个方向跟我当时遇到的对账问题高度吻合,于是决定在项目里小范围试点。
1.2 REA 到底是谁提出的:Resource-Event-Agent
先把三个词说清楚:
- Resource(资源):企业可以控制、有经济价值的东西,比如商品、现金、服务、人员工时、知识产权。它不一定是实物,也包括"应收款"这类权益。
- Event(事件):资源流入或流出企业的经济行为,比如采购入库、销售出库、支付货款、收到退货。事件是 REA 模型里的最小记录单元,必须记录发生的时间、关联资源和参与代理。
- Agent(代理):参与事件的人或组织,可以是企业内部的员工、部门,也可以是企业外的客户、供应商、物流商。代理是事件的执行者或受益者。
这三者之间的关系就是:一个代理发起或参与一个事件,一个事件带动一个或多个资源的流转。销售出库这个事件,由销售员和客户两个代理参与,结果是把商品资源流出企业,同时形成了应收账款资源。这样一条记录,比"借:应收账款 贷:库存商品"要直观得多。
1.3 这个模型解决的最原始问题
REA 最早要解决的问题,其实是会计上的一个老毛病:复式记账记录的是一笔业务的"会计解释",而不是业务本身。采购一批货物,传统分录是"借:存货 贷:应付账款",但"存货"和"应付账款"这两个账户状态是怎么产生的?是从哪张采购单、哪个供应商、哪个验收人那里来的?传统账本里没有完整的答案,需要再依赖凭证附件去反查。
REA 的思路是倒过来:先把业务事件完整记录下来,账务只是从这个事件记录里推导出来的一个视图。业务事件是源头,借贷分录是下游产物。这种设计天然支持追溯:每一笔财务数据都能顺着事件链找到最初的业务动作,审计的时候不用再"翻纸箱子"了。
这也是我认为 REA 值得花时间研究的原因:它不是在教你多记一笔账,而是在教你重新设计业务系统的信息架构。
2. REA 模型的三个核心构件:资源、事件、代理
2.1 资源:不是账户,而是业务对象
在 REA 模型里,"资源"是一个很容易理解错的概念。它不是你财务科目表里的"存货"科目,而是某一类可以被唯一标识的业务对象。
同样叫"库存商品",财务科目是一个聚合概念,把成千上万种商品汇总成一个数字;但 REA 里的资源必须能定位到具体对象,比如 SKU 编码为 XXX0001、批次号为 20250115 的那批 A 型号物料。如果资源没有唯一标识,事件就无法精确陈述"是什么资源流入了、什么资源流出了"。
因此落地时,资源往往不是一张表,而是一组表:商品表、批次表、在途订单表、应收权益表等。资源需要有价值属性,还要有状态属性(可用、待检、已锁定、已转移)。我见过很多项目失败的原因,是把"资源"和"账户"混为一谈,结果做出了一个既不是库存管理、也不是实时账本的四不像。
2.2 事件:业务过程的最小记录单元
事件是整个 REA 模型的核心。在建模阶段,你需要回答的问题是:一个业务从开始到结束,中间经历了哪些不可再拆的原子动作?
以采购为例,可以拆成:创建采购申请、下达采购订单、供应商发货、仓库收货、质检完成、财务付款、供应商开票。每一步都是一个事件,每个事件都有明确的时间、参与者、资源数量和单价。事件之间还有先后关系,比如"收货"必须发生在"发货"之后、"付款"通常发生在"收货"之后。
这里的难点是"原子"的粒度怎么定。拆得太粗,比如把"收货"混进"订单完成"里,后续想统计到货及时率就要拆数据;拆得太细,比如把"质检员拿起测试笔"也算事件,系统会变成噪音仓库。我的经验是:以"是否改变了资源的可用状态或所有权"为准。只要资源状态变了,或者权属变了,就应该单独建一个事件。
2.3 代理:谁参与了这个过程
代理就是业务里的"人"和"组织",但要比系统账号更语义化。一个员工账号可能对应不同的代理角色:他可以是采购员、仓库管理员、销售负责人。REA 建议把"代理"独立建立,让账号、角色、组织架构都作为代理的属性或关联关系,而不是直接写死在事件表里。
实际中这样做的好处很明显:审计时可以回答"这批退货是谁入库的、谁审批的",而不是只能看到"操作工号 00321"。代理可以分层,比如外部代理(供应商)和内部代理(采购员)都参与同一个采购事件。这种多代理的结构,为设置权限和审批流提供了清晰的数据基础。
2.4 三者之间的关联规则
REA 模型并不是简单地把三类数据建三张表,它还有几个关键的关联语义:
- 一个事件至少涉及一个资源,且资源的变化方向要被标明(增加还是减少)。
- 一个事件必须有至少两个代理参与,通常是内部代理和外部代理,或者两个内部代理。
- 出库事件通常对应一个入库事件(如销售出库对应客户收货),形成"交换"闭环。
- 事件可以形成链,比如"订单事件 -> 出库事件 -> 收款事件"之间是因果关系,不是并列关系。
这四条规则在落库时体现为外键关系和流向字段。很多业务系统只做了"资源变动表",没有把"代理"和"事件链"建模出来,所以只能回答"库存少了多少",不能回答"为什么少了、是谁操作的"。
3. REA 和传统复式记账的底层差异
3.1 复式记账本质是"账户状态快照"
传统财务系统的中心是一张总账和多张明细账。每一笔分录都在更新账户余额:借方加、贷方减。账户余额就像水缸里的水位,分录只是不断往水缸注水或放水。你看到的水位只代表当前状态,无法知道这缸水曾经被哪些管道流过、何时流过、为什么流进流出。
业务系统如果完全按照复式记账的思想来设计,往往会面临一个问题:订单、库存、财务各自为政,各有各的"水位"。对账的本质是比对两个水缸的水位,或者把一批水位转换公式套到另一批水位上。一旦业务复杂,转换公式里全是特例,系统就变成了补丁叠补丁。
3.2 REA 记录的是"可验证的经济轨迹"
REA 不维护账户余额,而是维护一条经济轨迹:每一个事件是一个点,每个点都记录了什么资源变动了、谁参与了、发生在什么时候。库存余额、应收账款余额、总账科目余额,都可以通过把事件流按时间轴聚合后推算出来。
这有好有坏。好的一面是,任何一笔余额都可以下钻到业务事件,不再是"凭空出现"的数字;坏的一面是,查询余额需要聚合计算,不能直接读一张余额表。性能上必须用冗余、缓存、快照或物化视图来补,但这也比"数据口径不一致"好解决得多。
3.3 一张对比表:借贷分录 vs REA 事件
为了直观,我列一下同一个业务动作在两种模型下的记录差异。
| 对比维度 | 传统借贷分录 | REA 事件 |
|---|---|---|
| 核心记录 | 借/贷科目金额 | 资源增减、代理参与、事件时间 |
| 记录目标 | 账面余额 | 业务事实 |
| 可追溯性 | 需凭证附件辅助 | 事件链天然可追溯 |
| 是否冗余存储过程 | 过程隐藏 | 过程即数据 |
| 生成财务报表 | 直接可用 | 需聚合视图 |
| 生成审计证据 | 弱 | 强 |
这张表并不是说 REA 要取代复式记账,而是说在业务系统阶段,REA 比借贷分录更适合做底层语义;借贷分录可以做成上层聚合结果,而不是底层存储格式。
3.4 从 REA 推导出复式记账:佐证模型完备性
我一开始也很担心:REA 这么抽象,最后能不能生成合规的会计凭证?后来动手建模才发现,REA 事件本身包含了足够的信息来推导借贷。
比如销售事件:产品资源(库存商品)减少,对应贷方"库存商品";同时应收权益资源增加,对应借方"应收账款"。REA 里每一次资源流出或流入,都能对应到一组会计科目的增减。关键在于建模时就要把"资源类型 -> 默认会计科目"的映射规则配置好。
这个推导过程虽然要写额外代码,但规则统一、便于解释。更重要的是,当业务规则变化时,只需要调整映射,不需要返工底层几十亿条流水。这也是我最终敢在项目里试点 REA 的原因。
4. 用一个贴近实际的案例:从采购到付款的 REA 建模
4.1 场景设定:某公司采购办公耗材
我以一个非常常见的场景来演示:公司采购部向某供应商采购 100 箱 A4 纸,每箱含税价 120 元,合同约定货到验收后 30 天付款。货到后仓库验收入库,财务在第 30 天付款。
传统系统会记录:采购入库单、应付凭证、付款凭证。REA 系统要记录的是:这个业务发生了一串什么事件、资源如何流转、谁参与了。
4.2 第一步:识别资源、事件、代理
先把业务对象归类:
- 资源:100 箱 A4 纸(商品资源)、现金/银行存款(货币资源)、应付权益(负债资源)。
- 事件:采购订单确认、供应商发货、仓库收货、财务付款。
- 代理:采购员(内部代理)、供应商(外部代理)、仓库管理员(内部代理)、出纳(内部代理)。
这里需要注意的是,"应付权益"其实是一个抽象的权益资源,它在收货事件后从零增加为 12000 元,在付款事件后归零。把"权益"也建模成资源,是 REA 走向财务系统的关键一步。
4.3 第二步:画出事件流(文字描述,不用图)
事件流可以用列表描述:
- 下达采购订单:采购员创建订单,确认资源需求和供应商。
- 供应商发货:外部代理发起发货事件,指向商品资源。
- 仓库收货:仓库管理员确认入库,商品资源流入企业。
- 财务付款:出纳支付 12000 元,货币资源流出,同时应付权益资源减少。
每一步都要记录事件编号、事件类型、发生时间、参与代理、关联资源编号、资源数量、资源方向。这样即使将来有争议,也能沿着"付款 -> 收货 -> 订单"这条链把所有凭证重演。
4.4 第三步:落地为表结构
实践中我不建议照抄任何一张"万能 REA 表",而是根据业务场景设计分表。核心可以有以下几张:
-- 事件表 CREATE TABLE rea_event ( event_id UUID PRIMARY KEY, event_type VARCHAR(64) NOT NULL, -- 订单确认/发货/收货/付款 occurred_at TIMESTAMP NOT NULL, location_code VARCHAR(32), UNIQUE (event_type, occurred_at, related_ref) ); -- 资源变动表(每事件可能有多条资源变动) CREATE TABLE rea_resource_flow ( flow_id UUID PRIMARY KEY, event_id UUID REFERENCES rea_event(event_id), resource_type VARCHAR(64) NOT NULL, resource_id VARCHAR(64) NOT NULL, direction VARCHAR(8) NOT NULL, -- IN/OUT quantity DECIMAL(18,4) NOT NULL, unit_price DECIMAL(18,4) NOT NULL, currency_code VARCHAR(16) NOT NULL ); -- 代理参与表 CREATE TABLE rea_agent_partipation ( event_id UUID REFERENCES rea_event(event_id), agent_id VARCHAR(64) NOT NULL, agent_role VARCHAR(64) NOT NULL, PRIMARY KEY (event_id, agent_id, agent_role) );这只是一个最简单的落地形态。真实项目里,还要为每个事件类型建业务扩展字段,比如订单确认事件要关联合同编号,收货事件要关联质检单号。一张通用表确实灵活,但查询和约束都会变得很难维护。
4.5 第四步:如何从事件数据生成会计凭证
有了上面的表,生成凭证就不难了。写一个聚合服务,按资源类型的映射规则推导借贷:
def derive_accounting_entries(event_id): flows = get_resource_flows(event_id) entries = [] for flow in flows: if flow.direction == "IN": # 商品入库 -> 借库存商品,贷应付权益 entries.append(("DEBIT", map_asset_account(flow.resource_type), flow.quantity * flow.unit_price)) entries.append(("CREDIT", map_liability_account(flow.resource_type), flow.quantity * flow.unit_price)) elif flow.direction == "OUT": # 付款 -> 借应付权益,贷银行存款 entries.append(("DEBIT", map_liability_account(flow.resource_type), flow.quantity * flow.unit_price)) entries.append(("CREDIT", map_bank_account(flow.currency_code), flow.quantity * flow.unit_price)) return entries当然这只是示意,实际还要考虑税金、折扣、运费分摊。但重点在于:业务数据在先,会计数据在后,且会计数据可以随时根据规则重新生成,这让历史口径调整变得异常轻松。
5. 在业务系统里落地 REA 时容易踩的坑
5.1 不要一开始就设计一张超级大表
我刚试点时曾想设计一张通吃所有业务的"宇宙事件表",把订单、库存、财务全塞进去。结果连约束都写不完整,外键关系越来越乱,最后还是拆成了领域事件表,再通过统一事件编号关联。
一个更稳妥的做法是:按业务域拆事件表,同时保证每个事件都有一个全局唯一 ID 和统一的事件头表。事件头只存通用字段(发生时间、类型、代理),详情放到领域子表。这样既能保持 REA 的语义统一,又不会让某个子域的变化影响全局。
5.2 资源标识符和物料编码的关系
资源标识符不是简单放一个物料编码就完了。同一 SKU 可能有不同批次、不同仓位、不同价格。如果资源标识符只到 SKU 级别,库存流水会算不清成本。建议资源标识符至少包含 SKU + 批次 + 仓库/仓位(如果业务需要)。在事件流里,这些字段必须写死,不能等后续报表再关联,否则事件链会断。
5.3 事件的时序与状态机的处理
事件除了有发生时间,还有状态:草稿、已确认、已取消、已冲销。REA 强调的是记录"实际发生了什么",但系统推进过程中会有人为改单、返工、作废。这时候不要直接改事件,而是新增一个"冲销事件"把原事件标记为失效,同时记录一个更正事件。
这个模式很像会计上的红冲:保留涂改痕迹,用新事件纠正旧事件。一开始这样做很别扭,总觉得多存了一条无用数据;但后来做对账和审计时,发现正是这些"冗余"帮我们还原了真实业务动作。
5.4 权限模型与代理之间的关系
业务系统里的用户账号不等于代理。一个用户可能既做采购申请又做收货,但在 REA 里这是两个代理角色。如果权限直接绑在账号上,后续系统要支持一个人分管多个公司主体时,就会乱套。
建议把"代理"单独建表,账号通过人员与代理关联,代理再绑定资源和事件的操作权限。这样任何一笔事件都能回答"是哪个主体下的哪个代理做的",权限审计也会简单很多。这个设计在项目上线后帮我们省掉了很多合规问题。
5.5 如何处理冲销、退回、折扣等异常事件
异常事件是落地 REA 时最大的难点。销售退货、采购折让、价保补差,这些并不是资源的简单反向流动,它们往往还涉及费用分摊、税额调整、应收/应付重估。
我总结的经验是:不要试图让一个事件承载所有业务语义,而是把它拆成多个动作。比如退货,拆成"退回商品入库"和"退款确认"两个事件,再把这两个事件关联到原始销售事件上。这样余额能对上,事件链也不会断裂。虽然事件数量变多了,但每个事件保持原子性,后续所有统计口径都会清晰。
6. 扩展方向:超越财务会计,走向业务本体
6.1 REA 是事件溯源的一种"语义层"
如果你了解过事件溯源(Event Sourcing),你会发现 REA 和它有不少共鸣:都是把业务变化记录成不可变事件,而不是直接保存状态。区别在于,事件溯源通常以"系统操作"为记录主体,而 REA 以"经济语义"为记录主体。
例如同样是"订单状态变更",事件溯源记录"订单状态从待付款变为已付款",REA 会记录"客户支付了 12000 元,资金资源流入企业,同时获得订单商品的权益"。后者更贴近业务本质,对中台和数据分析更友好。所以如果要做财务中台,REA 相当于给事件溯源加了一层会计级的语义规范。
6.2 在微服务架构里的应用
微服务拆分最容易出问题的就是跨服务的数据一致性。订单服务、库存服务、财务服务各自维护数据库,靠接口同步数据,经常出现哪条消息丢了、哪张表数据不匹配。
REA 可以作为一种服务间事件的公共协议。每个服务不直接更新对方的表,而是发布符合 REA 语义的事件,比如"商品出库""应收权益增加";其他服务订阅事件后,在自己域内聚合成所需视图。这样一来,服务间通过事件语义协作,而不是通过私有的 RPC 暴露字段。
我实际落地后的感受是:服务之间确实多了一层"翻译",但换来的是业务语义的统一,后续新增对接方的成本大幅下降。不过要注意,REA 事件的消息体需要做版本管理,否则前后端语义一变,消费方全得跟着改。
6.3 与数据中台、行为日志的配合
很多数据中台会把业务日志和埋点日志混在一起,结果分析时不知道该信哪个数。如果用 REA 事件作为中期标准数据,埋点日志和外部行为数据可以作为补充维度关联。比如 APP 上用户点击了某个商品页(行为日志),随后下了一个订单(REA 事件),点击轨迹提供了"为什么",REA 事件提供了"发生了什么"。这样的分层,能让分析与核算既有因果解释又有事实依据。
6.4 未来:自动生成财务报表、智能审计
REA 最终让我看到最有想象力的一点是:如果底层全是一串完整、不可变、按时间轴展开的业务事件,那么财务报表只是这些事件的一个聚合视图。月初结账时,不需要再人为调账,只需要重新执行一套映射规则,就能生成利润表、资产负债表和现金流表。
更进一步的智能审计也是同理:审计规则变成针对事件链的查询脚本,异常检测可以直接从源头发现"哪个代理在非工作时间转移了高价值资源",而不是在财务凭证里大海捞针。虽然这套体系在传统企业里推进起来阻力不小,但一旦推行成功,财务团队的月末加班文化会明显改变。
我自己的体会是:rea 这三个字母,表面上像一个缩写,实际上更像一种提醒——业务建模的初心是先搞清楚"发生了什么",而不是先去算账户余额。如果你也在做系统重构,可以从一个最小场景开始,把订单、库存、财务这三条线都还原成事件流。试过以后你会发现,很多过去解释不通的对账差异,在事件链上一下子就能定位到源头。这个方向,值得多投入一点时间。