
有段时间我在做一套进销存系统的重构业务方给的脑图里写满了“下单、出库、收款、退货”但每次需求评审我们都会在同一个问题上吵起来这笔业务到底该怎么记录是先记流水还是先改库存销售凭证和收款记录谁才是“真相”后来我试着用 REA 模型把整个业务流程重新梳理了一遍发现那些争论天然就消失了。REA 是 Resources-Events-Agents资源-事件-参与者的缩写最早出现在会计信息系统设计领域用来解决一个很朴素的问题如何用一个统一的方式描述所有商业活动。早年它被视为一种学术化的会计模型但这些年随着企业架构、数据中台、微服务事件建模、甚至区块链智能合约设计的发展REA 的频率明显高了起来。原因很简单它提供了一套足够抽象、又足够落地的业务建模语言。这篇文章我会把 REA 从原理到实操完整讲清楚包括它为什么好用、怎么从零建模一家小店的进销存系统以及我踩过的一些坑。1. 先搞清楚 REA 到底是什么三个字母背后的建模哲学1.1 从奶茶店的一天说起想象一家奶茶店。早上店里进了 10 袋珍珠、5 箱牛奶花了 800 元下午卖出 120 杯奶茶收了 1200 元晚上盘点发现珍珠少了半袋损耗记了一笔。这些事听起来再普通不过但如果让你设计一套系统把这些事记录下来你会发现传统的“库存表 流水账”很难让人满意。传统做法是维护一张当前库存表每次进货就在库存数字上加每次售出就减同时再记一张流水。问题在于流水只是操作记录库存只是数字快照二者之间没有强约束月底对账时只能依赖盘点强行修正。业务上一旦出现退货、赠送、调拨、报损这张库存表就开始变得混乱——到底哪些操作被记进了流水流水和库存是否一致业务发生的时间顺序如何追溯REA 的思路完全不同。它不关心“怎么记流水”它先问三个问题这件事涉及什么资源发生了什么事件是谁参与的奶茶店的一天在 REA 视角下就是一组事件采购事件把现金转换为珍珠和牛奶销售事件把珍珠制品转换为现金盘点事件把损耗记录下来。每一笔经济事实都被放进一个统一的结构里既不会丢也不会对不上。1.2 资源、事件、参与者的准确定义REA 的三个核心元素如果只是背定义很容易糊弄过去但真正用的时候必须吃透资源Resource是能带来价值的、可计量的对象。现金、库存商品、原材料、会员积分、应收账款、设备使用权都算资源。资源必须有两个属性数量和计量单位否则无法建模。“良好的服务态度”不算资源因为它不可计量“本月剩余工时”则算。资源强调的是存量状态而不是流转动作本身。事件Event是发生在某个时间点上的经济事实。采购、销售、收款、付款、退货、损耗、调拨都属于事件。事件是 REA 模型的核心枢纽因为只有事件才能把资源和参与者连接起来。事件一定会改变至少一个资源的数量或者状态。参与者Agent是参与事件的个人或组织。供应商、顾客、员工、店铺、仓库都是参与者。REA 特别强调一个事件通常关联两类参与者一个内部参与者比如店员和一个外部参与者比如顾客。这层对称关系是很多人在建模时忽略的。1.3 为什么事件优先于单据相册快照和监控录像的区别理解 REA 最关键的一步是接受一个观点单据不是业务事实本身只是业务事实的某种视图。传统系统喜欢把“采购入库单”“销售小票”当成底层数据因为单据离业务人员近好录入好理解。但单据本质上是人对经济事件的一层翻译——它可能是手写的、可能是打印的、可能是补录的甚至可能漏录。REA 把事件当作底层事实把单据当作上层展示。你可以说“采购事件”产生了“一张采购单”而不是“录了一张采购单”就等于“发生了一笔采购”。这个转化的意义在于事件是有唯一次序的单据可能会被修改、删除、作废事件是不可回溯篡改的业务事实修改只能靠另一个补偿事件来冲销。我经常用的一个类比是传统记账像是在给业务拍照片月底对账时把一堆照片翻出来比对REA 像是在给业务录视频任何时刻想得到某件商品的库存把视频倒回到此刻算一遍就行。照片可以修图视频很难伪造过程。2. 核心设计思路拆解REA 模型怎么帮你把业务理干净2.1 资源先画存量地图再谈交易我在实际建模时第一个动作不是画事件而是先画“资源存量地图”。把业务里的人财物盘一遍问自己到底哪些东西有价值、可计量、值得被追踪对奶茶店来说至少包括库存的珍珠、牛奶、杯子、现金收入、应收账款比如月结的大客户、积分资产。如果做连锁店可能还要把“门店经营权”或者“固定资产设备”纳入资源。资源地图的作用是帮你确定边界。很多项目做不下去不是因为事件没列全而是因为资源没厘清——把“订单金额”当资源、把“会员等级”当资源这都属于概念错位。资源一定是有“存量”含义的东西订单金额只是事件产生的数值不是可以累积消耗的资源。另外要注意区分“资源”和“资源类型”。数据库里应该有一个资源类型表如珍珠、牛奶再有一个资源实例表如某批次珍珠。小业务可以不做实例化但库存和供应链相关场景强烈建议做否则批次追溯、保质期管理都无从谈起。2.2 事件经济事实的最小记录单元REA 里的事件不是随便什么动作它必须改变资源存量。顾客进店逛了一圈没有产生任何价值流动这不是经济事件顾客付款买走奶茶现金增加、奶茶减少这才是经济事实。更细节地说事件可以分成“承诺”和“履行”两个阶段。订单是承诺发货和收款是履行合同是承诺验收和付款是履行。很多系统把订单直接当成核心事件来设计结果满库都是“待处理订单状态”一旦状态机复杂起来就失控。REA 的建议是把承诺和履行分开建模承诺负责业务过程追踪履行产生经济事实两个层面的数据结构清晰分工。在设计事件表时我倾向于用一个统一的“业务事件”基表再按需扩展子类型。每个事件必须带时间戳最好带上不可变的业务唯一键比如事件号或者追溯 ID这样可以防止重复入账也为后续审计留了退路。2.3 参与者每一个事件都有“双边关系”很多人在建模时把参与者当成普通的外键字段“这笔订单是张三的”然后就结束了。REA 提醒我们经济事件天然有双边性有人给出资源就有人接收资源。销售事件里顾客是外部参与者店员是内部参与者采购事件里供应商是外部参与者仓库管理员是内部参与者。为什么要强强调双边因为只有把内部和外部参与者都建模才能支撑“谁对谁负责”的责任分析。比如月底要看某个店员经手的销售额、某个供应商的供货合格率、某类客户的贡献度这些分析如果一开始就把参与者塞进字符串备注后面做数据汇总就要靠文本匹配极其痛苦。参与者的对称关系在模型层面很便宜不过就是在事件表上放两个外键但在业务分析层面的回报非常大。建模初期多花十分钟补全参与者等于给未来所有围绕“人”和“组织”的报表留好了地基。2.4 互惠规则贷方不消失库存不蒸发REA 模型有三条经典的完整性规则实操中应该有意识地去校验。第一每个经济事件必须至少涉及一个资源的流入和一个资源的流出。销售奶茶现金流入、库存流出采购珍珠库存流入、现金流出。如果一个事件只改了一边比如“发货”只减库存、不记录应收或者收款那业务事实就是不完整的。第二每个事件必须关联一个内部参与者和一个外部参与者。这条规则保证了所有资源流动都有人负责避免出现“无主库存”或者“幽灵交易”。第三资源的存量可以被推导。任意时刻的资源数量等于该资源所有流入事件的总量减去所有流出事件的总量。这是一个很好的调试工具如果某天某个商品的推导库存和业务方记忆中的库存对不上那一定是事件被漏录了或者录重复了而不是系统算错了。这三条规则本质上就是把“复式记账”的思想搬到了更一般化的建模层面。每次我重构业务系统时都会先拿这三条规则去套立刻就能查出数据模型里潜藏的洞。3. 手把手实操把一家奶茶店的进销存系统改造成 REA 模型3.1 第一步圈定业务边界列出全部用例为了讲得可落地我拿一家虚构的奶茶连锁店“某奶茶品牌”作为例子。它目前的业务很简单门店进货、门店销售、退货、盘点、积分兑换外加每个月和 A 公司做一次批发月结。第一步是画用例清单先不要碰表结构。门店向供应商采购原料珍珠、牛奶、杯子顾客到店购买饮品可以现金或扫码支付顾客拿小票退回有质量问题的饮品门店每日盘点处理损耗和盘盈顾客用积分兑换一杯饮品门店从中转仓调拨原料到另一家店用例不用一次列完美但要保证边界清楚。如果连业务方自己都说不清有哪些用例后面建模一定反复返工。3.2 第二步先列资源清单再画事件流用例列好后把资源清单单独拎出来。这份清单我会当成模型的“宪法”之后所有事件都围绕这些资源展开资源名称计量单位说明库存原料珍珠/牛奶/杯子袋/箱/个可继续细分为原料类型门店现金池元每个门店一个现金账户概念应收账款元面向月结客户顾客积分分另一种有价值的资产然后逐个用例画事件流。采购事件现金池减少 X 元、库存原料增加 Y 袋销售事件库存原料减少、现金池增加退货事件库存原料增加、现金池减少。积分兑换本质上也是一个事件对涉及“积分”资源和“饮品”资源的互换。事件流画完后你会发现一套业务的骨架其实非常对称有出去必有进来有增加必有减少。这就是 REA 模型的“经济互惠”关系的直观体现。3.3 第三步补全参与者处理特殊场景退货、调拨、盘点基础事件流确定后把参与者填上。内部参与者包括店员、仓库管理员、门店店长外部参与者包括供应商、顾客、月结客户。每类事件必须能对应到具体的内部人角色和外部对象。特殊场景往往在建模后期才暴露问题。退货不是把销售事件删掉而是生成一个反向的新事件库存回到店里、现金退给顾客调拨事件跨门店发生时是两个事件配对一个门店库存减少另一个门店库存增加盘点损耗是一个特殊事件它没有明显的经济互惠动作但会减少库存资源同时触发“损耗”这种非正常流出。这些场景都需要在设计时预留。3.4 第四步落成表结构和关键 SQL把 REA 模型转成关系型表结构可以保持下面这样清晰的形态。CREATE TABLE agent ( id BIGINT PRIMARY KEY, name VARCHAR(64) NOT NULL, agent_type VARCHAR(16) NOT NULL, -- INTERNAL / EXTERNAL related_org VARCHAR(64) ); CREATE TABLE resource ( id BIGINT PRIMARY KEY, name VARCHAR(64) NOT NULL, unit VARCHAR(16) NOT NULL, category VARCHAR(32), is_active TINYINT DEFAULT 1 ); CREATE TABLE business_event ( id BIGINT PRIMARY KEY, event_code VARCHAR(32) NOT NULL, -- PURCHASE / SALE / RETURN / STOCK_ADJUST occurred_at DATETIME NOT NULL, resource_out_id BIGINT NOT NULL, resource_out_qty DECIMAL(14, 4) NOT NULL, resource_in_id BIGINT NOT NULL, resource_in_qty DECIMAL(14, 4) NOT NULL, internal_agent_id BIGINT NOT NULL, external_agent_id BIGINT NOT NULL, source_ref VARCHAR(64), -- 来源单据号 UNIQUE KEY uk_event (event_code, occurred_at, source_ref) );关键设计点是每个事件都有出和入两个资源侧而不是用正负号去模拟。正负号看着简单但无法同时表达两个不同资源的互换比如“用积分换奶茶”就必然失败。查询当前库存时就非常简单了SELECT resource_id, SUM(in_qty) - SUM(out_qty) AS stock_balance FROM ( SELECT resource_in_id AS resource_id, resource_in_qty AS in_qty, 0 AS out_qty FROM business_event UNION ALL SELECT resource_out_id, 0, resource_in_qty FROM business_event ) t GROUP BY resource_id;所有事件表里没有 UPDATE 库存数字的操作只有 INSERT。这一点对并发和数据一致性非常友好因为库存永远是一个可推导的派生值而不是一个可以互相覆盖的易变字段。3.5 第五步用三条校验规则给模型体检模型落地前我会把之前提到的三条规则变成检查项逐条过一遍。规则一检查有没有事件只涉及一个资源侧若有要么是建模不完整要么需要业务确认是否真的存在“单向”事实。规则二检查每个事件的外部和内部参与者是否都非空空着的多半是漏了。规则三检查抽三到五个资源把期初数量加上事件推导结果和实际盘点或者业务台账对一遍。这套体检成本不高但能拦住绝大多数低级设计缺陷。我见过太多系统上线后才开始补事件流靠手工 SQL 来回调账非常折磨人。建模时多花这半小时后面能省好几天。4. 常见问题与排查技巧建模时最容易翻车的 5 个点4.1 把“订单”当事件其实它是承诺最常见的一个认知偏差是把订单当成经济事件。严格来说订单只是买卖双方的约定钱和货都还没有实质性流动。如果以订单为核心表去建模就会出现“已下单但一直未履约”的脏数据而且订单的取消和修改会让事件流变得极不稳定。我的处理方式是订单单独建表状态只管业务过程的推进订单完成后生成对应的事件记录发货事件、收款事件。订单和事件通过业务编号关联但两者在数据模型上是分开的。这样既不影响业务方“看订单”的习惯也不污染经济事实层。4.2 只建库存表不建事件表月底对账全靠运气很多进销存系统设计者图省事直接在产品表上挂一个 quantity 字段每次出入库就 UPDATE 这个数字。这在单机小工具里能跑但一旦出现退货、调拨、并发下单这个字段会变成不可信任的黑盒。REA 给的建议其实非常朴素库存永远不要直接存从事件推导。如果你觉得查询性能有问题可以保留一份冗余汇总表但事件表必须是唯一的真相源。汇总表可以重建事件表不可以丢失。4.3 事件只记单边流动互惠关系被折断“发完货就完事了等财务后续再补收款”我在不少项目里都听过这种话。事件从这个口子就断了发货事件只记录了库存减少没有记录应该收到多少钱。一个月后财务要平账发现少了应收记录又得补。如果坚持互惠关系发货事件应该同时生成一笔“销售出库”资源流出和一笔“应收账款”资源流入或者预先生成两个事件、通过关联组合成完整交易。业务推进可以分阶段但经济事实必须在事件层一次性闭环最多用状态标记“应收已收”。4.4 参与者塞进一个字符串字段分析时欲哭无泪有人建模时会在事件表上放一个 supplier_name VARCHAR(64)觉得这样简单直观。等到了做供应商对账、店员业绩、顾客消费行为分析的时候同一个供应商可能有“xx奶茶原料公司”“xx原料公司供货”“XX公司”三种写法一切分析都被文本匹配噩梦拖延。从一开始就应该在 agent 表里管理参与者的规范名称和属性事件表只存 agent_id。分析时通过外键去取统一信息就不会出现数据质量分崩离析的状况。4.5 单位与精度模糊模型上线后返工资源单位不统一是另一个高频坑。珍珠进货按“袋”制作按“克”销售按“杯”月底一算小数位对不上到底中间换算错了几步都查不出来。模型初期应该把每种资源定死一个最小计量单位比如“克”所有事件都按最小单位入账展示层再做单位换算。现金资源则要注意货币精度分和元的选择要一致。如果事件量很大精度问题会在报表层被放大到不可收拾。宁可一开始多建一层单位换算表也不要上线后靠脚本清洗。4.6 十分钟自查清单检查点典型问题建议处置事件完整性存在只有单边资源变化的事件补全对应的资源流入/流出参与者完整性内部参与者或外部参与者为空重新梳理业务责任人资源可计量性把订单号、备注当资源移出资源清单单独建实体承诺与履行订单事件与经济事件混表拆分承诺表和履行事件表库存推导当前库存可被直接 UPDATE改为从事件流推导或单向累加流水单位一致性同种资源出现多个单位统一最小计量单位5. 从进销存到更大的舞台REA 的延伸玩法5.1 数据中台把 REA 当作事实层的设计基底数据中台建设最头疼的问题之一是不同业务线对“订单”“成交”“用户”的定义不一致。REA 提了一个思路与其纠结名词不如把精力放在底层可验证的经济事实上。销售就是“某种资源从我们流向客户某种资源从客户流向我们”这个定义放到电商、门店、渠道批发里都成立。所以我在搭数据中台的分层时会建议把事件表作为 DWD明细数据层的公共事实模板各业务线用统一的资源映射表和参与者映射表再做一层自己的扩展字段。这样跨业务的经营分析比如“全渠道的现金流入”“所有库存资源的消耗趋势”就有了一个可拼装的共同底座。5.2 微服务与事件溯源REA 就是天然的事件契约微服务架构里流行事件驱动设计但很多团队在设计事件时很随意有人发一条“user_created”有人发一条“order_paid”字段和语义五花八门。如果用 REA 约束事件设计每条落到消息总线上的业务事件都必须回答三个问题动了哪个资源产生了什么方向哪些人在场这恰好就是一份高质量事件契约。更进一步说REA 和事件溯源Event Sourcing几乎是天生一对。事件溯源要求所有状态变化都被持久化事件表达REA 给出了这些事件的骨架结构。想自己实现一套轻量事件溯源引擎的同学完全可以拿 REA 作为领域模型的基础。5.3 钱包、积分、供应链金融所有资产流转都可以建模表面上 REA 总是出现在进销存和会计领域但把视角放大一点凡是涉及资产流转的场景它都能用。个人记账应用里每月工资入账是事件消费支出是事件信用卡还款是事件余额和负债都是推导出来的资源。区块链场景下的智能合约也一样代币转移就是资源流入流出的两个事件钱包地址就是参与者。我最近在做一个小项目时甚至用 REA 给家里的家庭预算做了套脚本收入的资源、支出的资源、家庭成员的外部参与者视角写起来非常顺手。这套模型的表达能力很强关键在于你是否愿意在早期多花一点时间把“资源和事件”想清楚。我个人在实际操作中最深的体会是REA 给我带来的最大收益不是数据库表怎么建而是和业务方沟通时终于有了一套统一语言。业务方说“我要加一个退货流程”我可以立刻问“退的是哪个资源的流入、哪个资源的流出、内外部参与者是谁”。有了这套语言需求评审从互相猜谜变成了对着模型补细节效率完全不在一个量级。如果你最近也在重构一套业务系统建议先别急着写代码拿一张纸画一个事件表草稿把进出资源、参与者列出来试试。这张纸画顺了后面所有表结构和接口设计都会顺很多。