
第一次在项目文档里看到“REA”三个字母我没太当回事。当时以为它不过是某个框架的缩写直到自己动手做一套订单加库存的数据模型翻了旧账本、对了几个月的流水才发现 REA 的价值被绝大多数人低估了。REA 全称 Resource、Event、Agent也就是资源、事件、代理人它是一套描述企业业务流程的数据建模理念。它不像传统财务系统那样从“会计科目”出发而是从“业务实际发生了什么”出发把每一笔交易拆成三件事动了什么资源、发生了哪些事件、谁参与其中。这套模型特别适合做订单、库存、收付款这类偏业务系统的底层架构也适合给数据分析师搭一套可回溯、可推演的事实表。这篇博客我结合一个图书销售收款的业务场景把 REA 的建模步骤、表结构和踩过的坑一次性说清楚。想摆脱“对不上账”和“改个需求就要调表”的同学可以先从这里入手。1. 为什么说 REA 是对“业务发生了什么”的结构化还原1.1 传统台账里的“余额”让人越记越糊涂很多小团队一开始都用 Excel 管理业务。举个例子一家社区书店每天的销售流程是这样客户来订书店员记一张销售单仓库按销售单发货财务收到货款后在收款记录里填一行金额。表面上一切正常但时间一长问题就暴露了。销售单上写着“客户A买了3本书”库存表里手动把库存数量减去3收款表里记了一笔300元。这三张表互相独立没人记录“这3本书具体是哪天发的”、“哪次发货对应哪张销售单”、“收到的300元到底对应哪笔发货”。等月底想对账时只能靠人工逐条比对经常发现库存数量对不上收款金额和销售金额差出一截。问题出在哪儿出在把“业务行为”压缩进了零散的字段里。记录的时候只存结果不存过程丢失了时间、动作、参与人这些关键信息。刚开始数据量小还看不出问题一旦业务跑起来数据就成了孤岛谁也说不清某个数字是怎么来的。1.2 事件驱动先把事实记下来再说REA 想解决的就是“数据可理解、可追溯”的问题。它的核心思路不是直接记账而是记录“事件”。一笔客户订单是一个事件一次仓库发货是一个事件一笔收款也是一个事件。资源之间的流转全部由事件触发每个事件都会挂上对应的代理人。于是你可以从事件出发问出很多传统表回答不了的问题某天某个客户订了哪些书、谁来负责发货、约定什么时候收款、实际哪一笔款对应哪一张发货单。不用再去几张表之间来回猜把事件串起来就是完整的业务流程。这套思路在会计领域被称为“事件驱动会计”。传统的复式记账只在凭证上写借贷REA 则先把业务事实记录为事件等需要出报表时再从事件推导出会计分录。数据的可信度从源头上就比“人工维护余额”高得多。1.3 谁适合读这份建模思路如果你是做业务系统建模的产品或研发REA 能帮你把订单、库存、支付这类模块的数据结构设计得更有弹性。如果你是做数据仓库或数据分析的同学REA 里事件明细天然接近事实表后续做指标统计会顺手很多。即便是财务从业者想理解经济业务的本质、不想被凭证格式束缚也可以把 REA 当作一种看问题的视角。还有正在自学系统设计的在校同学REA 是一个非常经典又容易上手的建模案例拿它练手比照着教科书画 ER 图更贴近真实业务。接下来我从三要素开始一点一点拆开讲。2. 拆开核心三件套资源、事件、代理人2.1 资源能创造价值也能被控制资源是 REA 模型里第一个关键词。说到资源千万别直接理解成“会计科目”里的存货或者现金。REA 里资源的判断标准很简单企业能不能控制它、它能不能参与交换、它能不能带来经济价值。图书、库存商品、现金、固定资产、服务甚至一项知识产权只要符合这三个条件都可以算资源。为什么要把资源单独建模而不是像传统台账那样维护一个“当前库存”字段因为资源是有生命周期的。一本书从采购入库到上架再到销售出库中间有在途、在库、已售等多种状态。如果你只在表里存一个“现有数量”那“数量从10变成7”的过程就完全丢了。到底是卖了3本还是报损了1本、退货了1本这些过程都必须用事件记录。实际操作中我的建议是每类资源建一张主表存它的标识和相对稳定的属性。比如图书表存 ISBN、书名、分类但不直接存“库存数量”。库存数量的来源永远是对入库事件和出库事件的汇总。这样做初期会觉得查询麻烦一点但换来了数据完全可回溯。2.2 事件业务里真正发生的动作事件是 REA 模型的基石。很多人在建模时常犯一个错误就是把“单据”当“事件”。比如建一张销售订单表里面塞了订单号、客户、金额、商品明细然后就以为订单事件记录完了。实际上单据只是事件的载体真正的事件是“客户提交了订购请求”、“仓库完成了发货”、“财务收到了付款”这些具体的动作。REA 里还要区分承诺事件和实际事件。承诺事件是未来要履行的义务典型的有销售订单、采购合同、服务预约实际事件是已经发生的交易比如发货、收货、付款、收款。这两者必须分开建模原因是它们发生的时间不同。举例来说客户6月签了一份全年采购合同8月才陆续提货。如果只看到订单就把金额都算进收入报表和现金流就会严重失真。把承诺事件和实际事件分开就能清楚知道“合同金额多少”和“实际履约多少”是两回事。事件还应保留足够细的粒度。一张销售订单里如果包含3种书就不要只在订单表里记一个总金额而应该把每种书拆到订单明细行。后续发货、退货、对账都要靠这些明细行去匹配。粒度越细模型的可扩展性越强。2.3 代理人谁参与了这件事代理人是事件的参与者可以是企业内部的人比如销售员、仓库管理员、出纳也可以是外部主体比如客户、供应商、物流公司。把代理人单独建模最直接的好处就是责任可追溯。我做过的一个系统里同一个客户既是采购方又是供应商还承担过一部分配送业务。如果我在每一张业务表里都写死一个“客户名称”字段这种多重身份就会让数据乱成一锅粥。正确的做法是建一张主体表存放人和组织的基本信息再通过角色表或外键字段表达“他这次是以什么身份参与的”。代理人和事件之间是多对多的关系。一个销售事件里有客户、销售员、审批人多个角色一个代理人也可能参与数十个事件。建模时在事件表里存放参与者外键或者建一张参与关系表看业务复杂度来定。基本要求是任何一笔事件都能回答“谁在什么时间做了什么”。3. 把三要素串起来三类关系与一条增值链3.1 存量-流量关系资源如何被事件改变资源与事件之间的关系在 REA 里叫存量-流量关系。通俗地说事件会让资源增加或者减少。收货事件让库存增加发货事件让库存减少收款事件让银行存款增加付款事件让银行存款减少。这组关系可以用一个很朴素的等式来约束期末存量 期初存量 流入 - 流出。这个等式看起来像最普通的库存公式但它提醒了我一个重要原则不要直接修改存量字段要让所有存量变化都来自事件。你可以在数据库里建一个“库存汇总表”但它只能作为查询缓存不能成为录入入口。我做库存模块时就把每一次入库、出库、报损都记录为独立事件。盘点时如果发现账实不符不用去猜“库存数量字段是不是被谁改过”只需要把事件流水拉出来和实物记录逐条比对很快就能定位到是哪一笔操作出了问题。这种“事件流水账 汇总视图”的方式是 REA 建模最扎实的底盘。3.2 二元性关系两个事件才构成完整交换在 REA 里一个完整的经济交换至少包含两个对应的事件。销售和收款是一对采购和付款是一对发货和收货是一对。这种成对出现的关系与复式记账“有借必有贷”的内在逻辑是一脉相承的但表现形式不同。传统复式记账在一张凭证里写借贷分录REA 则在两个或多个事件之间建立关联。比如一张发货单金额3万元客户分三笔付款分别是1万、1万、1万。传统账套里你看到的是应收账款余额被逐个减值但对账时还得靠人工判断哪笔钱对应哪批货。REA 里每笔收款事件都可以关联到发货事件核销关系一目了然。建模时可以在收款事件表里加一个“关联发货单号”字段也可以单独建一张核销关联表。我更推荐后一种方式因为业务常常出现部分核销、合并核销的情况单独建表能更灵活地表达多对多关系。二元性关系是 REA 建模中最容易被忽略、却最能提升对账效率的部分。3.3 参与关系与流程链条参与关系解决的是“这个事件由谁参与”的问题。资源、事件、代理人之间的完整连接构成一个业务循环。举个例子销售收款循环里有这样一组连接客户与销售订单事件建立参与关系销售订单事件通过订单明细与图书资源建立存量-流量关系图书资源又通过发货明细与发货事件建立存量-流量关系发货事件与收款事件建立二元性关系收款事件又通过参与关系关联到出纳和客户。多个业务循环再首尾相接就形成了企业的增值链。销售收款循环结束后现金资源增加下一步可能是采购付款循环用现金去购买新的图书资源。REA 模型到了这一层就不只是数据表设计了它还能帮你梳理企业整体运转逻辑对流程优化也很有帮助。4. 拿“图书销售收款”走一遍完整建模4.1 建模总步骤我归纳为六步顺序很重要先业务后技术明确业务范围圈定要建模的流程边界。识别资源列出这个流程里涉及哪些有价值、可交换的对象。识别事件列出所有已经发生的业务动作并区分承诺事件和实际事件。识别代理人列出参与这些事件的人和组织。确定关系补充存量-流量关系、二元性关系和参与关系。设计表结构把实体和关系转成关系数据库中的表和外键。第二步到第五步一定要先写清楚不要急着开建表语句。模型设计得不对SQL 写再多也是白搭。4.2 从业务场景中识别 REA 元素假设我们在做一家社区书店的销售收款系统。业务过程是客户下订单销售员接单仓库按订单发货出纳收到货款。按六步走识别结果如下表。类型名称说明资源图书书店持有的可销售商品资源现金收款后企业持有的资金事件客户订单承诺事件约定未来交付图书事件发货实际事件图书流出企业事件收款实际事件现金流入企业代理人客户外部代理人参与下单和收款代理人销售员内部代理人负责订单代理人出纳内部代理人负责收款关系也一并梳理出来客户订单通过订单明细关联图书发货通过发货明细关联图书收款关联现金发货和收款构成二元性关系客户参与订单和收款事件销售员参与订单事件出纳参与收款事件。这里注意我特意没有把“应收账款”列为资源。应收账款的本质是发货和收款之间尚未完成的时间差是一个抽象的中间状态应该靠事件关系去计算而不是靠新建一张资源表。这一点我在后面的坑里会详细展开。4.3 表结构与建表 SQL按上面的识别结果我设计了几张核心表。先建资源表和代理人表再建事件表最后建明细表。用最基础的关系数据库语法就能表达。create table book ( id bigint primary key, isbn varchar(20), title varchar(100), category varchar(50), unit_price numeric(10,2) ); create table cash_account ( id bigint primary key, account_no varchar(30), account_name varchar(50), currency varchar(10) ); create table customer ( id bigint primary key, customer_name varchar(50), contact_info varchar(200) ); create table employee ( id bigint primary key, employee_name varchar(50), role varchar(30) );事件表按承诺事件和实际事件分开。销售订单和订单明细放一起发货和发货明细放一起收款单独一张表。create table event_sales_order ( id bigint primary key, order_no varchar(30), order_date date, customer_id bigint references customer(id), salesperson_id bigint references employee(id) ); create table event_sales_order_item ( id bigint primary key, order_id bigint references event_sales_order(id), book_id bigint references book(id), quantity integer ); create table event_delivery ( id bigint primary key, delivery_no varchar(30), delivery_date date, order_id bigint references event_sales_order(id) ); create table event_delivery_item ( id bigint primary key, delivery_id bigint references event_delivery(id), book_id bigint references book(id), quantity integer ); create table event_payment ( id bigint primary key, payment_no varchar(30), paid_date date, delivery_id bigint references event_delivery(id), cash_account_id bigint references cash_account(id), customer_id bigint references customer(id), cashier_id bigint references employee(id), amount numeric(10,2) );这套表结构最大的特点是“余额”不单独存放。订单金额、发货金额、未收金额都能通过关联查询算出来。事件表里存的全是已经发生的动作时间、参与人、关联单据都完整保留。4.4 用查询反向验证模型建完表之后我会用几个SQL来验证模型是否成立。第一个查询是统计每种图书的发货销量确认库存流出有据可查。select b.title, sum(di.quantity) as sold_qty from event_delivery d join event_delivery_item di on di.delivery_id d.id join book b on b.id di.book_id group by b.title;第二个查询用来核对“发了多少货”和“收了多少钱”是否一致也就是验证二元性关系。这是对账最重要的场景。select d.id as delivery_id, sum(di.quantity * b.unit_price) as delivery_amount, coalesce(sum(p.amount), 0) as paid_amount, sum(di.quantity * b.unit_price) - coalesce(sum(p.amount), 0) as unpaid_amount from event_delivery d join event_delivery_item di on di.delivery_id d.id join book b on b.id di.book_id left join event_payment p on p.delivery_id d.id group by d.id;如果这个查询跑出来的未收金额长时间居高不下说明发货和收款之间出现了脱节。通过 REA 模型你可以直接看到是哪张发货单、哪个客户、哪笔收款没配对。这比传统方式翻几张表逐个核对要高效得多。5. REA 与传统复式记账的对比值不值得迁移5.1 从科目到事实两种视角的差异传统复式记账以会计科目为中心。每一笔业务被拆成“借什么、贷什么”最终汇总到总账和报表。这种做法的优点是标准、合规、财务人员熟悉缺点是当业务复杂时科目离真实业务太远很多明细信息被压缩掉了。REA 以业务事实为中心。先记录事件再在需要时把事件推导成凭证。我做过一个对比用一张表能说清楚两边的差异。对比维度传统借贷记账REA 建模核心视角会计分录、科目余额、报表优先业务事实、事件流、流程优先数据粒度凭证分录通常按单据汇总事件和行项目保留完整明细可追溯性余额可看但来源链条长一切汇总从事件出发来源清晰业务扩展新业务可能要加新科目新增事件和资源即可结构稳定理解成本需要会计背景业务人员也能较快理解我并不是说传统记账一无是处。法定报表、税务申报仍然强烈依赖会计科目这是现实约束。REA 更合适的定位是“业务系统的底层事件层”而不是替代整个财务核算体系。5.2 从 REA 推传统记账凭证既然 REA 记录的是事件那就意味着任何时候需要出财务报表都可以通过一套映射规则把事件转成会计分录。举个例子一次发货事件发生后对应到传统记账就是这样发货确认成本借“主营业务成本”贷“库存商品”。收款确认收入借“银行存款”贷“主营业务收入”同时处理税费相关科目。问题来了这些分录从哪来来源就是 REA 里的事件类型和资源增减关系。发货事件意味着图书资源流出收款事件意味着现金资源流入。只要维护一张“事件类型到会计科目”的映射表系统就能自动生成凭证。这也说明REA 和复式记账不是对立关系而是分层关系。REA 在底层记录事实传统记账在上层输出报表。两者结合既保证了业务的可追溯性又满足了财务合规要求。5.3 什么时候适合用 REA结合我自己的项目经验这几类场景用 REA 收益最大第一新系统从零开始建模。没有历史包袱可以直接按事件驱动设计避免一开始就掉进“科目表跟着业务改”的循环里。第二业务模式变化快。电商、订阅服务、多业态经营这类系统经常要加新玩法。REA 的扩展方式是新增事件或新增资源老表结构不用大动。第三审计和溯源要求高。需要对数据血缘做完整追溯的场合REA 的事件明细能清楚还原每一笔数据的来源。但如果你维护的是一个已经稳定运行多年的传统财务系统且近期没有大的业务重构那就不必为了用 REA 而 REA。比较务实的做法是在现有系统的数据仓库层按 REA 思路搭一套事件明细模型把传统凭证和业务事件映射起来。这样既不影响老系统也能享受事件模型的灵活性。6. 实践中的坑与常见问题速查6.1 坑一把整张单据当成事件我第一次建模时就把销售订单表直接当成了事件表里面既放订单头又塞订单明细。结果后来要对接发货和退货发现订单表里没法干净表达“订了3本书只发了2本还退了1本”。后来我把订单头拆成事件表把商品明细拆成独立的明细表发货和退货同样用明细表记录再通过外键关联。这个结构彻底解决了部分发货、部分退货的问题。只要涉及“一对多”的业务事件表和明细表必须分开建。6.2 坑二把应收账款这类科目硬塞进资源应收账款在传统会计里是一个科目但放到 REA 模型里它不适合作为“资源”出现。原因是应收账款不是企业当前控制的资源而是一种未来获取资源的经济权利它的价值和时间高度不确定。如果硬把它建模成资源很容易在模型里引入大量虚拟事件比如“确认应收”事件、“冲销应收”事件最后模型退化成了带新马甲的会计科目表。正确做法是通过发货事件和收款事件的二元性关系计算某张发货单关联的收款金额未收部分就是“应收账款”。查未收余额时直接用发货金额减去已关联收款金额账龄也可以按发货日期算不需要另建虚拟资源。6.3 坑三代理人不区分身份与角色有一个客户在我负责的系统里既购买图书又向我们供应文具。起初我在订单表里写了“客户名称”在采购表里也写了“供应商名称”两个字段各自维护结果同一家公司出现了两套名称数据完全对不齐。解决方法是把“主体”和“角色”分开。建一张客户主体表存公司或个人的基本信息在业务事件里通过外键表达它当前参与的角色。比如销售订单事件关联“客户角色”采购订单事件关联“供应商角色”。如果一个人在公司内部还兼任多个岗位同样用角色字段或角色关联表来处理。6.4 坑四为了性能放弃事件明细REA 模型要求保持事件明细但很多人在实际开发中会发现查询余额和汇总经常要 join 多张表性能不够理想。为了性能有人会选择在资源表里直接加“当前余额”字段每次事件发生后更新这个字段。这样做表面上是优化实际是在破坏数据可追溯性。一旦某次更新写错余额就再也无法被完整还原。我的做法是保持事件流水不变在查询层做优化。比如给常用汇总查询建物化视图或者每天生成一张库存快照表。底层事件永远保留上层读模型随意裁剪这样才兼顾准确和效率。6.5 常见问题速查问题我的回答REA 是不是只能用在会计系统不是只要是记录“谁在何时对什么资源做了什么动作”的业务都可以用。用普通数据库可以实现 REA 吗完全可以外键关系加事件表就够了REA 本质是一套建模约定不需要特殊工具。销售订单是事件还是文档销售订单是承诺事件但要注意把单据头和明细行拆开它代表的是一种未来交付义务。退货怎么建模增加一个退货事件资源和销售发货形成反向的存量-流量关系再和原发货事件建立关联。REA 能直接替代企业资源计划系统吗不能REA 解决的是核心业务数据结构问题但企业资源计划系统还包含大量流程引擎、权限策略和外围系统两者不是一回事。最后分享一个小习惯我拿到业务流程后会先用便签把事件列出来先不管表和字段。事件顺序理清楚了资源和代理人自然会浮现出来。REA 模型最大的收益不在于一张表设计得漂亮而在于它逼你把“业务到底如何发生”这件事问到底。把这个基本功做到位以后新需求追加往往只是新增一个事件而不是推翻整张表结构。