恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
REA模型实战:资源事件代理三要素破解业务对账难题
首页
资讯中心
/
REA模型实战:资源事件代理三要素破解业务对账难题
REA模型实战:资源事件代理三要素破解业务对账难题
发布时间:2026/10/12 9:39:21
做业务系统的时间长了难免会碰到一个绕不过去的坎业务部门说“我们要一个能自动对账的订单系统”开发一听需求第一句话就是“那发货单和回款单到底是什么关系”。大多数项目最后都会做成一张大流水表再加一堆状态字段。表结构倒好写真正跑起来以后对不清的账、改不完的状态、理不顺的责任全冒出来了。我最近又把REA模型翻出来重新梳理了一遍。REA就是Resource、Event、Agent的缩写核心是资源、事件、代理三要素。它不打算教你怎么设计接口而是先逼着你想清楚一项业务里到底哪些东西在流动流动发生在哪个时点是谁参与了流动。这篇文章把REA模型的来龙去脉、完整建模过程和落地时的坑位从头到尾捋了一遍适合被“账实不符”困扰的后端开发、业务架构师也适合那些看够了状态机还想再往前推一步的产品经理。1. REA到底在解决什么问题传统记账与状态流水的两难1.1 复式记账只告诉你“钱去哪了”没告诉你“事做没做对”传统财务上最成熟的工具是借贷复式记账有借必有贷、借贷必相等。这套方法管钱管物非常可靠但它有个先天局限只能记录已经被财务确认的分录表达不了业务发生的完整过程。比如一张销售订单在没开票、没发货、没回款之前账面上没有任何分录。可恰恰是这个“还没形成会计凭证”的订单在驱动后面所有动作。早期的业务系统为了解决这个问题选择了把订单、出库单、回款单各自建成一张表再用状态字段把它们的关联暗示出来。0表示待审核、1表示已出库、2表示已收款状态一多组合就指数膨胀。更麻烦的是状态字段没有内在语义系统里没人说得清“这个订单到底对应哪笔收款那笔收款到底覆盖了哪些订单”。REA模型正好补上这个空缺。它把“经济事件”当作第一公民来设计一次销售出库是一个事件一次客户回款是另一个事件两个事件之间通过“撮合关系”明确连接。事件一旦落库就同时记录了资源流向和参与人账实能不能对上从这个模型里直接看得到。1.2 三要素拆解资源、事件、代理分别是什么REA的基本构成非常简单只有三类实体再加上它们之间的关系。资源是稀缺的、能被组织控制并且有经济价值的对象。钱是资源库存商品是资源客户手里持有的兑换积分也是资源。资源强调“能控制”所以空气不是资源别人土地上的矿藏也不是资源。事件是资源流入、流出或转换的动作。一次采购收货带来资源流入一次销售发货造成资源流出一次生产加工把原材料转换为产成品这些都是事件。事件必须是一个已经发生的业务事实不能是计划、不能是意图。代理是参与事件的个人或部门。销售员、仓管员、客户、供应商、财务人员都属于代理。一个事件至少要有一个内部代理通常还会关联一个外部代理这样责任才算闭环。三类实体之间的关系也很固定事件影响资源事件参与代理一个流入事件和一个流出事件通过“撮合关系”配对。把一个咖啡馆的例子套进去就非常直观咖啡豆库存是资源顾客付钱是现金流入事件咖啡师做咖啡并递给顾客是商品流出事件顾客、咖啡师、收银员是代理。付钱和递咖啡这两个事件互为一对撮合构成一个完整的经营循环。1.3 REA和ER图、用例图、状态机的本质区别很多团队觉得REA没什么新意因为它最后画出来也是几张关系图看起来类似ER图。但REA和普通ER图的关注点完全不同。ER图是静态数据结构的快照描述有什么表、表之间怎么关联。用例图描述角色与系统之间的交互。状态机描述某个对象的状态流转路径。它们都没有回答一个最核心的问题一笔业务里的经济责任是怎么传递的。REA把注意力放在经济事件的闭环上。它要求每个资源流入事件最终要对上资源流出事件比如采购收货必须对上供应商付款销售发货必须对上客户回款。这种“撮合约束”就是责任闭环的体现。用REA搭出来的系统天然具备审计追溯能力而不是靠报表临时拼凑。我在实际项目里最大的感受是REA不排斥ER图、状态机它是站在它们之上的一层语义约束。ER图可以继续设计表状态机可以继续控制流程顺序但业务对账的规则和逻辑应该由REA来定义。2. 完整建模案例给某农贸批发站搭一套销售与库存模型很多文章讲REA都停留在概念动不动就拿“飞机零部件制造”“跨国供应链”举例离普通开发者的日常太远。这次换一个接地气的场景某个农贸批发站主营土豆、鸡蛋和时令蔬菜既做门店散客零售也做周边餐馆的月结供应还从上游种植基地批量采购。整个业务有买、有卖、有账期、有付款非常适合演示REA建模。2.1 第一步圈定边界只建销售子域不碰采购细节建模最怕一开始就把整个公司全画进去。订单、采购、仓储、财务、采购员、销售员、供应商、客户全揉在一起模型还没画完人先晕了。实践上我建议先从最痛的一条链路开始这里选择“销售发货到客户回款”这个循环。边界一旦确定候选实体就好列了。资源这边先列出土豆、鸡蛋、蔬菜的库存再列出银行存款。事件这边列出“销售出库”和“客户回款”两个核心动作。代理这边则是客户、销售员、仓管员、财务收款岗。这组清单已经足够启动建模。这一步的要点是严格区分“流程动作”和“经济事件”。出库单审核、打印发票、发送发货通知都是流程动作不直接造成资源增减不属于经济事件“把货交给客户”是资源流出“客户付钱”是资源流入这两个才是REA要建模的事件。很多初学者在这里卡住误把操作步骤当事件一张图里全是流程反而看不清资源流动。2.2 第二步识别资源后先问“资源在变少还是变多”库存商品在销售出库事件里是减少资源银行存款在回款事件里是增加资源。一个事件可以同时对多个资源产生影响比如“销售出库”同时减少库存但如果客户当场刷卡它还会增加银行存款或者减少客户的预收余额。为了避免事件过于复杂建议在模型边界内先保持单一职责出库事件只管出库回款事件只管收款。这里有一个容易被大家忽略的细节应收账款和应付账款在REA中一般不会被建模成资源。应收应付款更像一系列事件撮合的余额结果而不是可以独立控制的经济资源。如果你把应收款当成资源来建模会凭空多出一套细分的“资源类型”还容易把它和真正的货币资金混淆导致对账多出一层不必要的中间状态。仓库库存这类资源还要考虑计量单位。鸡蛋按箱算、蔬菜按公斤算单位必须落在资源上。销售事件记录数量和单价时系统要能追溯到当时采用的计量单位避免后面统计口径打架。2.3 第三步定义撮合关系让每个事件都有另一半这是REA建模最有价值的一步。销售出库这个资源流出事件必须对应一个资源流入事件来完成经济循环那就是客户回款。于是“销售出库—客户回款”构成一对撮合。实际操作中两者往往不是一一对应。餐馆客户周一和周二各取一次货周三统一结清两笔货款就会出现两个销售出库事件撮合一个回款事件的情况。反过来一笔预付货款分三次提货就是一个收款事件撮合多个出库事件。所以在设计撮合关系时一定要设计成多对多并单独建一张撮合表来记录配对明细。来把这个例子映射成表格资源流入事件流出事件撮合关系关联代理土豆等库存商品采购收货销售出库出库与回款撮合客户、销售员、仓管员银行存款客户回款供应商付款回款与原出库撮合客户、财务收款岗银行存款客户回款经营费用支出付款与费用事件撮合财务、供应商这个表格不是REA的全部但把核心骨架画出来了。每一行都意味着一个责任闭环客户拿走了货那就必须有一笔对应的钱回来钱回来后谁收的、什么时候收的、对应哪批货全部有据可查。2.4 第四步循环校验资源流必须闭合模型画完后一定要沿着资源流走一遍闭环。土豆从种植基地采购入院库存增加卖给餐馆库存减少餐馆回款银行存款增加。到这里一条从商品转换成货币的完整路径就走通了。校验时发现一个问题门店散客零售场景里顾客买完当场付款出库和回款几乎同时发生也应当建模成两个独立事件。很多团队嫌麻烦把它简化成“销售事件”一个动作结果现金短款了就分不清是货发错了还是钱收错了。宁可多建一条事件也别在审计链路上留窟窿。关于月度结算的餐馆客户模型里还要增加一个“账期对账”过程。REA并不直接管账期但可以通过撮合表的待匹配事件推断出来凡是已出库但未匹配到回款事件的记录就是应收余额。这个查询逻辑比额外维护一张应收余额表更干净。3. 从REA模型到数据库和代码落地实操概念模型再漂亮最终也要落到数据库和代码里。REA在落地时最大的误解是“我们要把所有业务都改造成事件表”。我不建议这样做REA更应该作为业务对账的语义层与日常业务表并存。下面是一套我实测下来比较顺的落地方式。3.1 最小表结构资源表、事件表、代理表、撮合表最基础的落地方案只需要四类表。资源表保存商品、资金账户等对象事件表保存每次业务事实代理表保存客户、供应商和内部人员撮合表保存事件之间的配对关系。-- 资源表库存商品、资金账户等 CREATE TABLE resource ( id BIGINT PRIMARY KEY, resource_code VARCHAR(64) NOT NULL, resource_name VARCHAR(128) NOT NULL, unit VARCHAR(32), is_fund BOOLEAN NOT NULL DEFAULT FALSE ); -- 代理表客户、供应商、内部员工 CREATE TABLE agent ( id BIGINT PRIMARY KEY, agent_code VARCHAR(64) NOT NULL, agent_name VARCHAR(128) NOT NULL, agent_type VARCHAR(32) NOT NULL -- CUSTOMER/SUPPLIER/STAFF ); -- 事件表已经发生的业务事实 CREATE TABLE event ( id BIGINT PRIMARY KEY, event_type VARCHAR(32) NOT NULL, -- SALE_OUT/RECEIVE_PAYMENT business_no VARCHAR(64) NOT NULL, occurred_at TIMESTAMP NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- 事件与资源的关联资源增减明细 CREATE TABLE event_resource ( event_id BIGINT NOT NULL, resource_id BIGINT NOT NULL, delta_quantity DECIMAL(18,4) NOT NULL, delta_amount DECIMAL(18,2) NOT NULL, direction VARCHAR(8) NOT NULL, -- IN/OUT FOREIGN KEY (event_id) REFERENCES event(id), FOREIGN KEY (resource_id) REFERENCES resource(id) ); -- 事件与代理的关联谁参与了这个事件 CREATE TABLE event_agent ( event_id BIGINT NOT NULL, agent_id BIGINT NOT NULL, role VARCHAR(32) NOT NULL, -- SELLER/CUSTOMER/CASHIER/WAREHOUSE FOREIGN KEY (event_id) REFERENCES event(event_id), FOREIGN KEY (agent_id) REFERENCES agent(id) ); -- 撮合表事件之间的配对关系例如出库匹配回款 CREATE TABLE event_match ( left_event_id BIGINT NOT NULL, right_event_id BIGINT NOT NULL, match_amount DECIMAL(18,2) NOT NULL, matched_at TIMESTAMP NOT NULL, PRIMARY KEY (left_event_id, right_event_id), FOREIGN KEY (left_event_id) REFERENCES event(id), FOREIGN KEY (right_event_id) REFERENCES event(id) );这套结构不是给业务操作提供支持而是给对账查询提供依据。日常操作仍然可以走订单表、库存表、收付款单表只是在完成业务操作时额外向事件表插入一条事实并更新撮合关系。这会带来一点写路径上的额外开销换来的是对账链路彻底清晰。3.2 用一段伪代码展示事件记录与撮合配对我习惯用一个统一的事件服务来承接所有业务事件登记。核心逻辑很简单先写业务主表再登记事件最后返回事件ID。def record_business_event(event_type, business_no, occurred_at, resource_changes, agent_roles): with transaction(): event_id insert_event(event_type, business_no, occurred_at) for change in resource_changes: insert_event_resource( event_idevent_id, resource_idchange.resource_id, delta_quantitychange.quantity, delta_amountchange.amount, directionchange.direction ) for role, agent_id in agent_roles.items(): insert_event_agent(event_idevent_id, agent_idagent_id, rolerole) return event_id def match_events(sale_event_id, payment_event_id, amount): with transaction(): insert_event_match( left_event_idsale_event_id, right_event_idpayment_event_id, match_amountamount, matched_atnow() ) def receivable_balance(customer_agent_id): # 查询该客户所有未匹配的出库事件汇总剩余金额 sql SELECT SUM(er.delta_amount - COALESCE(m.matched_amount, 0)) FROM event e JOIN event_resource er ON er.event_id e.id JOIN event_agent ea ON ea.event_id e.id AND ea.role CUSTOMER LEFT JOIN ( SELECT left_event_id, SUM(match_amount) AS matched_amount FROM event_match GROUP BY left_event_id ) m ON m.left_event_id e.id WHERE e.event_type SALE_OUT AND ea.agent_id ? return query(sql, customer_agent_id)伪代码里的应收余额查询就是REA模型的直接红利客户欠多少钱不再依赖某个字段被正确更新而是由已出库事件和已回款事件的撮合差推导得出天然防漏、防重。3.3 REA、事件溯源与状态机分工不一样不少团队听到REA就联想到事件溯源以为要把所有数据都改成事件流。其实两者关注的层次完全不同。事件溯源是一种存储与状态重建方式REA是一种业务语义建模方式。状态机则负责控制事件的合法顺序。用一句话区分状态机决定“下一步允许发生什么”REA决定“发生了什么之后必须对应什么”。销售订单可以走状态机待审核、已审核、已出库、已完成。但“已收款”不应该作为一个状态直接更新而是通过登记一个客户回款事件、再与出库事件完成撮合来体现。状态机管流程REA管经济责任事件溯源管状态的备份恢复。三者各司其职混在一起反而会把系统改得不伦不类。这里给一个非常实用的建议在落地REA时不要急着把所有模块都事件化。先把销售发货、客户回款、采购收货、供应商付款这四类核心经济事件引入REA其余模块保持传统表结构。REA事件表作为审计与对账层效果已经足够好改动风险也小很多。4. REA建模过程中最容易踩的坑框架好用归好用实际建模时还是有几个坑基本每个项目都会碰到。我在自己搭demo和帮朋友梳理业务模型时反复踩过类似的雷这里集中写出来。4.1 事件表容易演变成“大杂烩流水表”最典型的误操作是把所有动作都塞进事件表。登录算事件改价格算事件审核通过算事件用户点击也算事件。表面上看这能统一记录所有行为但REA事件和操作日志在使命上有本质区别。操作日志记录“谁在什么时间改了什么字段”目的是追踪操作行为REA事件记录“资源从哪流向哪谁承担什么责任”目的是重建业务状态与对账。对比项REA业务事件操作日志核心目的表达资源流动与经济责任记录系统操作痕迹能否恢复业务状态能能重建账面余额通常不能是否参与对账必须参与不参与典型内容销售出库、客户回款、采购收货审批通过、字段修改、登录退出如果把这些东西混到一张表里事件表会变成一个大泥潭查询撮合关系时性能崩溃不说语义还彻底混乱。正确做法是两张表分开操作日志走日志框架REA事件走专门的事件表二者从设计上就不该合流。4.2 事件粒度控制不好撮合会变成一笔糊涂账粒度过大会让事件失去描摹业务的能力。比如把一周的发货合并成一个大事件客户中途分批回款时根本没法匹配粒度过小又会让人发疯每个包裹、每张拣货单都建一个事件系统开发和运维成本直线上升。我自己的判断标准是一个事件必须能明确表达一次完整的资源责任转移并且可以在对账时与对立事件清晰配对。“完整的责任转移”不等于“最小动作”。在农贸批发站的例子里一次销售出库可以把三车土豆同时运给一个客户挪为一个出库事件没问题但如果这个客户要求分批验收那就应该按车拆成三个出库事件接受部分对账。可一旦涉及拆单、换货、拒收就可能需要更细的拆包事件。这个粒度应该由业务语义决定由业务对账功能倒推出来。4.3 别把REA当银弹这些场景不适合硬套REA擅长表达资源经济循环但也有一些场景并不需要它。纯内容平台、内部工具型应用、没有资源流动也没有多方代理责任的小系统硬套REA只会增加模型复杂度让团队陷入“为抽象而抽象”的境地。具体判断可以参考三条标准第一业务是否反复出现资源流入流出第二是否有多方代理参与并且需要明确责任第三是否存在跨周期对账或余额核对需求。三条里中两条以上才值得引入REA。只中一条或者完全不中的用状态机加普通流水表就够了。我也遇到过团队在微服务架构里纠结要不要用REA统一所有服务的数据语义。这是个更大的话题但我的看法是先在单体业务模块内把REA跑通让对账模型稳定再谈跨服务事件。分布式环境下REA撮合关系会变成跨服务通信的难点不适合第一个版本就去碰。5. 我自己的实操心得先从一条对不齐的链路开始如果让我给还在犹豫的团队一个最小建议不要一上来就重构老系统。选一条当前对账最痛的链路比如“销售发货到客户回款”先用REA把模型画清楚再落地一对撮合表。模型画得好不好有一个很笨却很有效的检验方法找一个人盯着资源问“这批货现在变成了什么钱到哪个环节了”只要这两个问题能被模型中的事件链条回答出来模型基本不会歪。回答不出来或者中间需要靠脑补的状态字段来填坑那就是哪里还没建到位。我在做模拟demo时还有一个小技巧每次插入事件之前先写一句完整的中文描述比如“2024年5月6日销售员张三把20箱鸡蛋卖给客户李四预计6月1日回款”。这句话里能划分出资源、事件、代理就说明事件拆法是对的如果这句话怎么都写不顺多半是事件粒度或者代理角色没理清。REA不是万能的更不是新潮的花架子。它只是把那些散落在状态字段、关联表、对账SQL里的业务真相提前到设计阶段讲清楚。多花一点时间把事件模型建稳后面开发报表和对账功能时省下来的时间远远不止这一点。