
如果你跟我一样第一次看到“rea”这个缩写时第一反应是某个前端框架或者文件后缀名那这篇内容值得你花点时间读完。REA 不是新造的神秘黑话而是 Resource-Event-Agent 的简称一套在会计信息化、业务建模领域被反复讨论的底层思想。它主张把一笔业务描述成某个代理Agent在某个时刻执行了一个事件Event让某种资源Resource增加或减少。这句话听起来简单但真正落地时会发现它能把业务流程、账务追溯和数据模型从“表结构打架”里解放出来。这套思路适合谁参考后端工程师在画订单模型之前可以看看产品经理想梳理业务状态流转时可以看看做财务系统、库存系统、对账模块的开发者更应该看看。哪怕是刚入行想做领域建模的新人也可以用 REA 来给自己多一把尺子。这篇不会堆术语我会用一个零售场景从头拆到尾把核心概念、表结构设计、和复式记账的关系、以及我踩过的坑都讲清楚。1. 为什么你需要认识REA一套从业务本质出发的建模思路1.1 传统业务建模的痛点订单表为什么越画越乱不少业务系统一开始都从订单表开始。客户表、商品表、订单表、订单明细表、支付流水表、库存表、物流表……表建得越多状态字段就越复杂。你不敢删订单也不能轻易加“取消中”“退款中”这种中间态因为一旦状态机考虑不周全后面每加一个业务动作就要同步改一版表结构。我见过一个典型场景库存表里直接存放“当前数量”用户下单时程序先判断库存够不够够就减库存不够就报错。表面看着没什么问题但第二天财务对账时发现订单记录、支付流水、库存流水各算各的仓库明明少了一件系统里库存却是正的。原因就是所有逻辑都围绕“当前状态”设计没有人记录“什么时候、谁、因什么事件导致了这个状态变化”。这种设计的本质问题是状态是业务运行的结果不是业务运行的凭证。没有凭证出了问题就没有回溯的锚点。REA 的思路正好反过来。它不把“订单状态”当作第一公民而是把所有业务动作看作事件把事件产生的影响记录在资源上。状态只是事件发生后的计算结果随时可以从事件重新推出来。这样一来表结构更稳定业务规则也不会散落在各个 if 分支里。1.2 REA的三角关系资源、事件、代理到底在说什么REA 的名字已经把这个三角关系说清楚了。Resource资源不是指数据库里的“资源表”而是业务上有价值、可以被定量计量的东西。库存商品是资源现金是资源应收账款、服务工时、平台积分也算资源。资源的核心特征就是“能因为事件而变多或变少”。Event事件是时间上的一次性动作表示业务过程中发生了什么。客户下单、支付货款、发货出库、采购入库、发放工资这些都是事件。事件必须能说明它让哪种资源增加还是减少并且带有一个明确的记账时间点。事件不是流程里的“待办”而是已经发生的事实。Agent代理是指参与事件的业务主体。客户、供应商、员工、仓库管理员、财务审批人都属于代理。有的流程还会涉及多个代理比如一次销售的发货动作客户是发起方仓管员是执行方。把代理独立出来不是为了多建一张表而是为了搞清楚“谁有权做这个动作”以及“出了问题应该找谁”。一个典型业务可以用 REA 句式这样描述客户代理在上周二下午时间发生了购买行为事件使现金资源增加、商品库存资源减少。一次业务动作至少要动到两个资源同时至少有两个代理参与。这个结构天然就带了一层约束而不是靠程序员在设计状态机时临场发挥。1.3 落地REA时会反复用到的四个核心规则REA 不是随便把三个对象放一起就完事它有几条核心规则我建议建模前先抄在便签上每个经济事件至少要影响一个或多个资源产生资源增加或减少的“存量流”效果。至少要有两个“配对”的经济事件组合成一个完整的经济交换也就是一个事件让资源流入另一个事件让资源流出。比如销售收款和发货出库就是一对配对事件。每个经济事件必须由代理参与同时代理之间会通过授权关系连接。采购员不能随便替出纳签字就是这类授权的体现。所有资源、事件、代理都必须能够在时间轴上被追溯事件一旦发生不能被直接覆盖修改只能通过相反事件来调整。为什么强调“不能直接改”而不是“改字段”因为业务和审计要求的是可追溯性。你如果直接把“库存数量”改小没人知道是谁在什么时候改的但你如果新增一条“退货事件”所有历史报表都能推算一致。这个规则可以说是 REA 与普通 ER 建模最不一样的地方。2. 核心概念拆解Resource、Event、Agent怎么落地2.1 Resource不是所有业务对象都能当成资源我第一次想把“商品分类”当作 Resource后来发现这条路走不通。商品分类没有可增减的数量也没有在业务事件中被直接影响它更适合作为商品的属性而不是资源本身。资源必须具备三个特点第一能参与业务事件第二能用数量或金额度量第三能被特定代理控制。在一个零售系统里典型的资源包括库存商品、现金等价物、客户预存款、应收款项。你可以为资源设计一张通用表但如果不同资源的属性差异很大也允许拆成子表。重点是所有资源都应有一个不可变的标识和时间戳便于后续推导。这里有一个实操心得资源表尽量存“主数据属性”不要直接存“当前余量”。比如商品的主数据是名称、规格、条形码当前库存余量应该通过计算得出。如果你在资源表上放了一个“当前库存数量”字段那么每次库存变化都得同步更新一旦更新失败或并发冲突就前功尽弃。不要怕计算慢数据库增量聚合的代价远低于数据错乱的代价。2.2 Event业务行为的原子单位也是审计的最小证据Event 是 REA 模型里最像“记录”的东西。它需要包含至少几类信息事件唯一标识、事件发生时间、事件类型入库、出库、收款、付款、方向增量或减量、对应的资源、对应的代理、数量和金额。事件有一个容易混淆的点它到底代表“计划”还是“执行”有经验的团队会把它分成两个层次采购订单是“承诺”收货入库才是“事件”。承诺可以被取消、被修改事件一旦发生就成了事实。推荐把承诺单独建模避免把尚未发生的业务动作误记为资源变动。举个简单例子客户下单后商品还没出库这时候你不应该立刻把库存扣掉而应该先记录一个“销售承诺”等到实际发货事件发生后再影响库存。很多库存超卖问题其实是把“承诺”和“事件”混为一谈了。事件本身最好是“只增不改”的。一旦写进去就不允许通过 UPDATE 去改它的数量或金额。如果发生退货就再新增一条反向事件。这样做的好处是任何一张对账报表都可以从原始事件逐条推导而不是去猜哪条记录被后人改过。我维护过的几个报表口径问题几乎都是因为早期有人直接改了流水金额。2.3 Agent把人、部门和系统角色统一成一套身份Agent 不单指自然用户还包括客户、供应商、内部部门、机器人账号甚至自动化脚本。只要它在业务事件中承担“能发起动作”或“能接收责任”的角色就应该建模为代理。代理之间通常存在上下级或授权关系。比如财务主管授权出纳付款这种授权不是事件本身而是事件发生的条件。REA 鼓励把授权关系单独建表避免把权限判断逻辑埋进代码里。从审计角度看这也更清楚事件发生那一刻谁拥有合法授权。很多系统会把“客户名称”直接写在订单表上这在 REA 看来是不够的。客户应该是一张独立的代理表订单事件只需要引用代理 ID。这样做的好处是同一个客户以后变更联系方式或名称时你不必回溯修改历史订单。历史事实保持原样主数据可以演进两者职责分开。2.4 三条连接关系存量流、配对互换、代理授权光有资源、事件、代理三张核心表还不够它们之间必须有关联规则。第一个规则是存量流关系。事件通过存量流影响资源增量事件让资源增加减量事件让资源减少。比如“采购入库”是商品的增量事件“销售出库”是商品的减量事件“收到客户付款”是现金的增量事件“支付供应商货款”是现金的减量事件。从资源这一侧看任何资源当前余量都可以通过“所有增量事件合计减去所有减量事件合计”得到。第二个规则是配对互换关系。一个完整的经济交换一定包含一个增量事件和一个减量事件它们描述同一个业务过程的两个侧面。销售发货商品减量对应客户付款现金增量采购入库商品增量对应支付货款现金减量。这种配对关系尤其适合做对账你只要找出没有配对的减量事件就能定位到未核销的出库单或未回款的发票。第三个规则是代理授权关系。代理可以授权其他代理参与特定事件。这个规则在严格审计场景下非常有用可以回答“付款事件为何会存在”的问题。不一定要在第一个版本就实现完整授权矩阵但至少要保证代理表能支撑将来的扩展。3. 实操示例用REA给一个小型零售系统建模3.1 场景定义某小型便利店的进销存与资金流我们以一个完全虚构的某小型便利店作为例子它只做四类业务从供应商进货入库、向供应商支付货款、向客户销售出库、收取客户货款。这是最小闭环但已经足够看清 REA 怎么落地。第一步不是画表而是先用 REA 句式把业务流程说一遍采购入库供应商代理 A把商品资源提供给便利店代理 B便利店收到商品库存资源增加。支付货款便利店代理 B把现金资源支付给供应商代理 A现金资源减少。销售出库便利店代理 B把商品卖给客户代理 C商品库存减少。收取货款客户代理 C支付现金给便利店代理 B现金资源增加。注意同一批商品入库会同时产生“采购入库”事件和“支付货款”事件同一笔销售也会同时产生“销售出库”事件和“收取货款”事件。这就是前面说的配对互换关系分别在商品资源和现金资源上形成一增一减。3.2 物理表结构资源、事件、代理如何落到数据库我建议用五张核心表resource、agent、event_record、resource_event、agent_event。前两张是主数据后三张是关联关系。很多教程喜欢只写一张“events”表把资源 ID、代理 ID 都放枚举列里但真实场景中一个事件可能影响多个资源、多个代理所以五张表的拆分更灵活。下面给出最精简的字段示意表名核心字段说明resourceid, name, resource_type资源主数据resource_type 可区分库存、现金、应收agentid, name, agent_type代理主数据agent_type 区分客户、供应商、内部角色event_recordid, event_time, event_type, direction, quantity, amount事件记录direction 是 increment 还是 decrementresource_eventevent_id, resource_id事件与资源的关联事件影响了哪个资源agent_eventevent_id, agent_id, agent_role事件与代理的关联agent_role 表示是发起方还是接收方这种结构会让查询变得多几次 JOIN但换来的是极强的灵活性。比如“查询某段时间每个客户贡献了多少毛利”不再需要改表结构只用关联事件与销售出库即可。对小型系统来说这五张表完全可以支撑。需要强调一个细节event_record 表中的 quantity 和 amount 最好分开存。quantity 只记录业务数量amount 记录该时点的折算金额。因为历史业务里商品单价可能变化当时实际开单金额才是审计证据不能事后拿最新价格去算。3.3 从事件推导库存和资金状态不是被更新的而是被算出来的有了事件记录库存计算就变得很清晰某商品当前库存 该商品所有增量事件的数量合计 - 该商品所有减量事件的数量合计。现金余额同理所有现金增量事件金额合计 - 所有现金减量事件金额合计。你可能会担心性能难道每次前台下单都去跑一次全量聚合当然不是。实际落地时可以建立“投影表”或“快照表”定期从事件表聚合也可以维护一张“当前余额表”。但关键区别是余额表只是缓存不是事实来源。事实来源永远是事件表。哪怕投影表被搞乱了也可以从事件表重新算出来。我用过一个很土但有效的做法每天凌晨跑一次全量重算任务把库存和资金余额投影到独立的聚合表。一旦白天发现余额异常直接拿事件表现算一次和投影表对比几秒钟就能发现问题。这种方式比在每次写入时去 UPDATE 一张余额表靠谱得多。3.4 处理退款与退货用反向事件而不是覆盖修改现实业务里一定会有退款和退货。传统设计会在订单表上加一个状态“已退货”再把库存数量加回来。REA 的推荐做法是新增一个事件类型为“销售退货入库”方向为 increment关联商品资源同时新增“退款记录”方向为 decrement关联现金资源。这样设计以后原销售事件永远不变销售流水、退货流水各归各。财务对账时可以清楚看到“原销售 10 件退 2 件净销售 8 件”。如果你在原销售记录上直接改数量最后审计时谁也说不清当初到底卖出去几件。还有一个容易忽略的点退货事件通常需要关联原销售事件形成“事件链”。建议在 event_record 里增加一个 parent_event_id 字段把退货、退款关联回最初的事件。查询关联明细时会方便很多也能防止同一笔订单被重复退款。4. 对比REA与复式记账、传统ER建模的关系4.1 REA与复式记账拨开“借贷”这层外壳很多人听到 REA 来自会计领域就会疑惑它是不是要替代复式记账实际不是。复式记账是一种会计表达方式REA 是业务事实模型。你可以把 REA 看作复式记账背后的“为什么”复式记账则是一种把 REA 事实投影成账本的成熟规则。复式记账要求每一笔分录至少有一个借方和一个贷方。对应到 REA 里增加资金资源的收款事件记借方减少商品资源的出库事件记贷方或者反过来取决于会计科目方向。重点是借贷规则不是凭空产生的它本质上是在定义“至少两个配对事件”的双边影响。如果你想从 REA 事件表自动生成会计凭证只需要配置事件类型到会计科目的映射关系再按方向设好借贷就能把业务事件变成分录。这种设计最直接的价值是业务流水和财务凭证来自同一条事实源。传统系统往往是业务库记一遍财务系统记一遍两边数据靠手工对账。REA 落地之后财务凭证只是事件表的视图不再需要单独维护一套账。4.2 与传统ER模型/状态机的区别稳定性与灵活性的取舍传统 ER 建模以“实体”为中心把业务对象和它们的关系直接映射成表。状态机建模则把流程约束在预先定义的若干个状态之间。两者在流程稳定、需求明确的场景下非常高效但遇到业务扩张时新增一个“预售”“分批发货”之类的逻辑往往要改表结构或改状态定义。REA 以“事件”为中心资源状态不是建模主体而是事件产生的结果。它对流程的扩展更友好今天只卖实体商品明天加一个“虚拟卡券”只需要新增一种资源类型和对应事件类型表结构几乎不用动。后天增加“代金券抵扣”也只是多一个减量事件和已有现金事件配对即可。做一个简单对比维度传统ER 状态机REA建模中心实体与状态事件与影响业务规则承载写在流程代码和状态转移表里体现在事件配对与资源关联规则中审计追溯依赖日志、状态变更历史事件表本身就是完整追溯链扩展新业务经常需要迁移表结构大多只需新事件/新资源类型查询复杂度相对简单直接需要多次 JOIN 或投影表当然这不是说传统方式一无是处。对于固定、短流程、低变化率的后台管理系统状态机方案反而更直观。REA 的“点”在于业务复杂度高、变化快、需要审计与对账的领域。4.3 什么时候让你别用REA避免过度建模我也见过一上来就想把规章制度、签到打卡都套进 REA 的团队最后反而被复杂关系绕晕。REA 适合经济事件明显的场景比如进销存、财务、供应链、订单履约。如果你的业务只是一个内容发布系统、一个轻量后台核心目标就是记录增删改查那么传统 CRUD 表足够没必要引入事件模型。另一个不适合的场景是低延迟高并发且必须实时读取状态的系统。REA 天然偏审计与可追溯如果你要求每一个读取都实时聚合所有事件性能会很难看。这种情况下可以保留状态缓存但必须注意数据一致性。我的建议是判断标准看业务是否需要回答“谁在什么时间做了什么导致了什么变化”。如果这个问题高频出现REA 是值得的如果业务根本不关心历史变化传统表结构反而更省心。5. 常见误区与落地排查5.1 误区一把资源表和事件表混为一谈最常见的错误是在 resource 表上直接维护“当前库存”或“当前余额”又把事件表当作流水日志。表面看是双写实际上两个数据源互相矛盾时没人能判断谁对。正确的做法是选择一个事实源如果一个字段可以从事件推导那它就不应该再是同一表里的“可更新字段”。如果实在需要保存当前值用于性能也必须把它标记为投影字段并写明不可手动修改。5.2 误区二忘记配对关系导致对账失败第二个坑是没有维护事件之间的配对关系。比如销售发货事件和收款事件相互独立没有关联字段。月底对账时收款多了发货少了无法确认哪些款项对应哪些商品。对策是在 event_record 上增加 exchange_id 或 parent_event_id把一次交易里的增量事件和减量事件绑定到同一个交换标识。这样即使业务数据量很大也能按交换标识核销。5.3 误区三把代理身份写成事件文本字段还有团队喜欢把“客户姓名”“供应商名称”直接字符串存进事件流水表。看起来查询方便实际上代理一旦改名历史事件就全乱了。而且一个事件可能涉及多个代理角色字符串字段无法表达“经手人”和“审核人”的区分。正确做法是始终引用 agent 表的主键通过关联表记录角色。虽然多一次 JOIN但能换来主数据统一管理以及历史事实的真实性。5.4 落地路线的四条建议第一不要指望一口气把全套 REA 建完。从一个最需要审计的业务流开始比如“采购入库 支付货款”把事件、资源、代理全打通以后再逐步扩展到销售。第二先定义事件命名规范统一用“业务动作影响方向”的格式比如“采购入库”“销售出库”“收款”“付款”避免同一个操作在不同模块里叫不同名字。第三把“事件不可修改”作为硬约定哪怕开发初期多写一条注释也别轻易破例。第四定期做投影数据与事件数据的对账校验把对账逻辑做成一个定时任务而不是出了问题再用脚本临时查。如果团队已经有一套传统表结构的系统不建议立即推翻重来。比较温和的做法是保留现有表用于业务查询同时增加事件表把关键业务操作一字不差地记录下来逐步把计算逻辑迁移到事件上。这个过程很像给老房子重新铺设管道一开始会有点不适应但跑通一次“从事件推导库存”的验证后你会开始理解为什么有人会说“REA 提供了业务建模的底层坐标”。最后说一点个人体会。REA 不是一个需要背熟所有规则的框架更像是一种思考习惯每当你准备往数据库里写一个“当前状态”字段时先问自己一句——这个状态是从哪个事件、由谁、在什么时间产生出来的想清楚这个问题很多建模上的纠结会迎刃而解。如果你也正在设计订单、库存、资金相关的系统不妨挑一个小流程按 REA 方式画一遍哪怕只是纸上作业也会有一些意外收获。