首页 / 资讯中心 / 文章详情

REA模型实战:用资源-事件-代理重构业务对账与结算系统

REA模型实战:用资源-事件-代理重构业务对账与结算系统 ★ FEATURED ARTICLE
“rea”这个项目代号一开始只是我在某个结算系统重构里画的一个草图文件名。当时想把讲师课酬、销售提成、机构分成这几条来回扯皮的业务线理清楚试了几种建模方式都不够痛快直到用上了 REAResource-Event-Agent这套资源-事件-代理人的思路整个模型才真正对上了业务。后来这个草图长成了一个能跑通全流程的原型它解决的问题很具体让每一笔业务活动可以被完整追溯谁参与、交换了什么东西、价值怎么流动全部用一套规则串起来。这套模型特别适合业务架构梳理、领域建模以及想给财务或供应链系统做底层重构的人参考。ROI 很高因为它不限定技术栈拿到现有项目里也能一步步迁过去。1. 项目概述与建模思路REA 是什么以及为什么选它1.1 从一场扯皮会议说起某天我被拉进一个会议起因是业务方和财务对不上账。业务说这个单子明明成交了课也上完了为什么财务那边说收入没确认财务说你那是“收款”不是“教学完成”收入确认的时点跟你合同签没签字两码事。两边各自拉 Excel谁也说不清“钱”和“课”之间的因果链。这种场景做系统的人应该都不陌生。表面是口径冲突本质是业务模型里缺少一种对“价值交换”的统一描述方式。我当时刚好在翻一些企业建模的旧资料想起 REA 模型。它最早是会计信息系统里的东西用来替代“借贷记账法”在信息系统里的地位后来被用在供应链、电子商务、企业架构这些地方。核心思想特别朴素把业务抽象成资源、事件、代理人三类东西再加几条链接规则就能描述绝大多数价值交换活动。这个想法让我眼前一亮。因为它不是我平时用的那种“订单表加状态字段”的思路而是把业务过程当成一连串可验证的事件链事件改变资源的状态代理人参与事件资源在事件之间流转。谁在什么时候对什么资源做了什么全都能查。回到刚才的例子只要把“签约销售”“教学服务”“收款”“结算”各自建模为事件再明确每个事件增减哪些资源、涉及哪些人账就能对上了。1.2 REA 的三类核心实体资源、事件、代理人REA 展开其实是三个单词Resource资源、Event事件、Agent代理人。我习惯把 Resource 理解成“有价值且可以控制的东西”比如现金、课程服务、商品库存、应收账款Event 是“改变资源状态的行为”比如销售、采购、收款、支付Agent 是“参与事件的人或组织”包括内部员工、外部客户、合作方。这三类的鉴别特征是我在这个项目里用得最多的判断清单。看一个业务要素到底该归到哪一类不要看它叫什么名字要看它做什么。比如“订单”表面像一个资源或者一个文档但它本质上是一个记录了“销售事件”的数据载体如果把它建模成资源后面算库存和收入时会非常别扭。再比如“合同”它描述的是双方约定的责任义务本身不直接参与交换它是链接条件的载体不是资源也不是事件。下面这张表是我做概念模型时经常贴在旁边的判断规则候选要素可能的归类判断特征错误建模后果现金、库存、课时、积分资源可以被消耗、提供、交换账实不符销售、支付、授课、入库事件表示一个行为发生改变资源状态只能看到状态看不到过程员工、客户、供应商代理人参与事件承担责任拥有权利责任链条断裂订单、合同、发票链接/契约载体描述事件参与条件和资源交换承诺大量状态冗余查询退化成报表当时我做了个决定项目代号就叫“rea”目标不是做一个“数据库设计模板”而是做一套建模和实现的方法能把接入的每个业务都翻译成这套统一结构。1.3 与传统复式记账/表结构建模的差异与选型理由传统的业务表建模通常是状态驱动一个订单表里放一堆状态字段从“待支付”到“已发货”到“已完成”怎么流转靠程序里写死的状态机。问题在于状态越多查询越复杂业务逻辑越难改。你以为加一个状态字段只是加一个枚举值实际上上下游的判断逻辑全要跟着动。复式记账法比状态好一些它有借必有贷借贷必相等能保证账目平衡但它更关注“钱和资源在一个账户里的增减”而不是业务活动本身。如果你在做采购、销售这类流程系统直接照搬“借库存、贷应付账款”这种结构业务人员是看不懂的因为他们脑子里是“跟某客户签了单、发了一批货、收回来一笔款”不是“借方和贷方”。REA 的结构是事件驱动每个事件左右都要链接资源这就是它自带平衡校验的原因。在课程销售这个案例里销售事件会同时增加应收资源、减少课程库存资源收款事件会同时减少应收资源、增加现金资源。任意时刻把资源净额和事件发生明细对一遍如果对不上就是模型链路有问题。这种“事件两侧都能追溯”的结构比状态机灵活比复式记账贴近业务。我选 REA 还有一个原因是它特别适合跨部门系统重构。它不依赖现有数据库里的任何表而是一张可以反推所有业务规则的“底图”。当不同部门还在争论字段含义的时候你可以拿 REA 图对所有人说先把资源和事件画出来我们再来谈数据怎么落。1.4 REA 建模最终交付物概念模型、链接规则、平衡路径说明做了这个项目后我发现 REA 的交付物跟常规需求文档不太一样。常规文档最后交付的是原型图或者接口定义但 REA 交付的是一套“业务拓扑结构”包括三类实体的定义、实体之间的链接规则以及以资源为节点的多条闭环校验路径。概念模型解决“有什么”和“是什么”的问题。比如课程服务是一种资源销售课程是一种事件讲师和学员是代理人。这一步的输出通常是一张图图上只画三类图标矩形代表资源圆形代表事件三角形代表代理人。图的价值不是好看而是让业务方和开发方对着图说话。我发现只要图里某个事件旁边挂了两个代理人往往就会暴露隐藏的审批或协作流程。链接规则解决“之间什么关系”。REA 中最核心的是资源-事件链接每个事件至少影响一个资源这个影响可以表达为资源数量的增减、权利的产生、义务的消除。事件-代理人链接说明谁对谁承担责任。两条以上路径到达同一个资源时就有了平衡校验的基础。比如我把“收入确认”定义为“教学完成事件发生后产生应收资源和现金资源之间的状态转移”那它的完整路径就是教学完成事件既减少了“已购课时”资源又增加了“可结算收入”资源。这样业务方所说的“课已经上了收入可以确认”在模型里就有了明确锚点。2. 核心细节解析与实操要点三类实体怎么定义才不踩坑2.1 资源Resource的边界是“物”还是“账本账户”这里我先声明以下关于资源分类和事件识别的经验来自我在“rea”原型中用到的通用做法面向的是大多数业务系统里常见的情况如果你的领域更特殊可以在这个基础上做变体。资源一定是“有价值且有可辨识边界”的东西。所谓边界就是你能说清楚“这个资源现在是谁的”“它的可用量是多少”。现金有边界因为你能数出来库存有边界因为你能盘点课时有边界因为学员账户里能看到剩余数量应收账款有边界因为每个人欠多少是明确的。但有些东西很像资源却不是资源例如“订单状态”。状态是资源的一种属性而不是资源本身。在 REA 里资源应该是那个被事件改变了的实体对象状态只是事件发生后的投影。假设你把“订单列表”当资源“待发货”和“已发货”就是两个状态值那么你真正应该建模的资源是“库存商品”订单只是记录这次销售事件修改了哪些库存。另一个容易踩坑的是把账户和资源混为一谈。比如“预收款余额”是一个账户它可以由多条收款事件组成余额本身有时候像资源但它更接近“资源集合的快照”。我建议在资源表里保留“账户金额”作为资源的一类但用事件表把每一次预收、消费都串上去这样余额才能被重新计算而不是只靠字段维护。比如说每次读余额时都从 Event 表里聚合而不是读资源表的冗余字段。代价是查询复杂一点好处是永远不会出现“余额表改了但流水没记”的情况。2.2 事件Event的识别give 与 take、双侧事件事件是 REA 模型的心脏也是最难识别准确的部分。判断一个行为算不算事件标准是它是否直接改变了某种资源的数量、状态、使用权或所有权。比如“学员报名”不是一个事件因为它只是表达了意图真正的事件是“缴费”它改变了资金资源“完成授课”是事件它把教师的教学服务转化为学员已获取课时“发放结业证书”也是事件它产生了一个新凭证资源。REA 的事件大多成对出现叫做 give 事件和 take 事件。一个资源的流出对应另一个资源的流入。例如销售事件本身是一个 give交付商品加 take获取现金权利的综合表达。我刚建模时总是把事件写成一个销售事件只是“减少库存”没有把“增加应收账款”作为另一侧导致模型少了一条腿。后来我强制自己在每个事件后面写下它的 give 资源和 take 资源没有双侧就说明这个事件被拆解得还不够细。识别事件时还要注意“时点”。有的业务行为持续很久比如一个季度合同是不是一个事件如果合同期内每月提供服务那应该按月份拆成多次“服务提供事件”而不是一次性事件。这是因为 REA 的目的是让资源增减与事件发生时刻对齐如果一次事件里塞了一段持续服务你就无法知道哪个月的收入对应哪次交付。我处理这种情况的原则是宁可拆细事后聚合不要合并事后拆分。2.3 代理人Agent与内外部参与角色代理人就是“有资格做决策、承担责任的个体或组织”。在课酬结算案例里讲师、学员、销售顾问、财务审批人都是代理人外部合作机构也可以是代理人。判断一个角色是否需要建模成独立代理人看一个特征你是否需要对这个角色追溯“他的参与导致了哪些事件”需要则建。比如“推荐人”虽然不直接参与交易但推荐关系会影响佣金结算如果不把他建模成代理人推荐佣金事件就没有参与者后面算账会失去依据。设计代理人时最容易出问题的点是“一个人同时担任多个角色”。同一个学员可能又是内部员工同一个客户可能既是销售对象又是供应商。不要为了图方便把角色硬编码在代理人属性里而是在事件与代理人链接关系中区分“以什么角色参与”。我的做法是让代理人有独立标识表再用“参与类型”字段去表达角色。这样如果需要给一个角色加属性不至于动代理人的根表。内部代理人和外部代理人还有一个区别内部代理人往往要关联组织、部门和岗位外部代理人更关注联系人和账户信息。REA 不强制你用一个表容纳所有属性它允许你在逻辑上统一物理上按视角拆分。我做课酬系统时把讲师、学员、销售都放进同一张代理人表但各自关联一张扩展表效果很好。2.4 链接规则与多路径平衡校验经济契约、责任义务三类实体都定义清楚后真正让模型运转起来的是链接规则。链接规则看起来只是概念图上的连线但落库时就是外键和约束。我归纳过 REA 里最通用的四类链接资源-事件链接表示事件对资源的增减事件-代理人链接表示谁参与事件-事件链接表示事件之间的因-果关系资源-资源链接表示转化或替代关系。多路径平衡校验是整套模型最有含金量的地方。举例同一个资源“应收课时”可能经由两条路径发生变化一条是缴费事件按价格兑换课时增加课时另一条是完成授课事件按课时扣除消耗减少课时。两条路径如果各算各的就会出现“课程上了但课时账户不对”的问题。解决办法是把资源表做成一个“实时核算目标”而把事件表作为唯一事实来源所有资源数量都从事件明细推导。我实现时写过一个简单的校验存储过程对每一个时期内每个资源统计所有相关 give 事件和 take 事件的数量增减再和资源当前库存对比。如果不一致说明有事件没有被链接。这个校验跑一轮下来能快速抓出开发时漏掉的关联。3. 实操过程与核心环节实现从需求到模型再到表3.1 案例背景某培训机构讲师课酬结算系统这个案例基于我在“rea”项目里实际打磨过的一个模拟场景我把它称为“某讲师课酬结算系统”。背景是这样某培训机构线下开课运营模式是销售顾问卖课学员支付学费讲师按实际上课课时拿课酬机构从学费中留成。业务痛点集中在月底结算销售说我们卖了很多课财务说要等课上完才能算成本讲师说我课上了几节你们课酬算错了。传统做法是做几张 Excel 加状态字段月底对半天。我带着 REA 思路重新建模目标是把整个业务流程拆成一眼能看懂的模型然后落到 MySQL 表里让任何人跑一条 SQL 都能校验课程销量、已上课时、待付课酬和机构留存是否正确。3.2 第一步按“销售课程服务→授课→收款→支付课酬”梳理事件链建模不要急着画表先画事件链。我拿白板把业务从头到尾走了一遍理出来的事件顺序是报名缴费、课程开班、完成授课、退费申请、结算单确认、课酬打款。这六个动作就是六个候选事件。逐一过事件判据后最终确定其中五个为核心事件报名缴费和课程开班合并或保留视情况而定。如果你希望“缴费”和“开班”是两种不同资源变化就应该拆成两个事件在课酬结算系统里缴费产生现金资源和预收账款或应收冲抵开班产生课时资源和班级库存占用所以拆开更有价值。事件识别的过程中我记录下了每个事件涉及的 give 资源、take 资源、内部代理人和外部代理人这套记录直接成为后续建表的主干。每组事件都按“谁、在什么时间、改变了什么资源、产生了什么义务”来描述每一句描述都能落到表里的几条记录上。这一步多花一小时后面编码少踩很多坑。3.3 第二步绘制 REA 图并定义链接关系画图时不要想着漂亮要想着“能不能让业务方点头”。我在白板上画了一个简化版 REA 图列出的核心事件和链接关系如下表事件参与代理人影响资源give/take触发或后续事件报名缴费学员、销售顾问现金增加课时额度增加销量资源增加触发课程开班课程开班教务、班级学员班级名额占用课时库存减少触发生成班级课程事件完成授课讲师、学员课时资源减少应付讲师薪酬增加触发结算单确认结算单确认财务审批人、讲师应付薪酬确认应扣税费资源减少触发课酬打款课酬打款财务操作人、讲师现金减少应付薪酬减少无这张表是整个模型的“宪法”。业务人员看到“完成授课事件会减少课时资源、增加应付讲师薪酬”时基本不会再争论收入确认的时点问题因为它把话说透了。3.4 第三步定义校验约束、平衡规则和审计追溯要求在这个模拟结算系统里我定了几条硬约束每条都对应一个校验 SQL 或存储过程逻辑学员已缴金额必须等于报名缴费事件中现金增加的总和不等于则需要查原始事件。讲师应付课酬必须等于完成授课事件中课时消耗乘以单价任何手工改数都会打破这条链接。现金资源余额必须等于所有收款事件增加现金总和减所有支付事件减少现金总和由系统自动汇总。每个事件必须至少有一个内部代理人和一个外部代理人防止数据缺失导致责任无法追溯。为了审计方便我给每个事件表加了一个全局事件号所有资源增减都带事件号。这样如果某天财务问“这笔课酬对应的是哪几节课”一条按事件号关联的查询就能把所有链路拉出来。最终表结构没有过度设计用的都是最简单的关系表。以下是核心资源表和事件表的简化 DDL字段做了精简但能反映我在“rea”原型中的落库思路CREATE TABLE resource_balance ( id BIGINT PRIMARY KEY AUTO_INCREMENT, resource_type VARCHAR(32) NOT NULL, -- CASH, COURSE_CREDIT, PAYABLE, RECEIVABLE, etc. agent_id BIGINT NOT NULL, -- 资源所属代理人 resource_key VARCHAR(128) NOT NULL, -- 资源粒度的业务键如 学员ID课程ID balance_qty DECIMAL(18,4) NOT NULL DEFAULT 0, balance_amount DECIMAL(18,4) NOT NULL DEFAULT 0, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_res (resource_type, agent_id, resource_key) ); CREATE TABLE event_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, event_type VARCHAR(32) NOT NULL, -- SIGNUP, CLASS_START, LESSON_DONE, SETTLEMENT, PAYOUT event_no VARCHAR(64) NOT NULL, occurred_at DATETIME NOT NULL, internal_agent_id BIGINT NOT NULL, external_agent_id BIGINT NOT NULL, event_data JSON, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE event_link ( id BIGINT PRIMARY KEY AUTO_INCREMENT, event_id BIGINT NOT NULL, resource_type VARCHAR(32) NOT NULL, resource_agent_id BIGINT NOT NULL, resource_key VARCHAR(128) NOT NULL, flow_direction ENUM(IN, OUT) NOT NULL, quantity DECIMAL(18,4) NOT NULL, amount DECIMAL(18,4) NOT NULL, FOREIGN KEY (event_id) REFERENCES event_log(id) );每次发生真实业务操作时业务事务里同时写 event_log 和 event_link再刷新 resource_balance。阅读时永远以 event_log 和 event_link 为准resource_balance 只是物化视图级别的缓存。这里有个细节千万不要把 resource_balance 当成主数据它只是查询加速用的结果所有对账、审计、补账逻辑都基于 event_link。3.5 第四步可运行的校验逻辑与查询实现有了表和事件链校验逻辑就能写得很干净。我用一段伪代码表示“资源余额重算”的核心实际项目里可以写成存储过程或者定时任务function recalculate(resourceType, agentId, resourceKey): totalIn SELECT SUM(quantity) FROM event_link WHERE resource_type resourceType AND resource_agent_id agentId AND resource_key resourceKey AND flow_direction IN totalOut SELECT SUM(quantity) FROM event_link WHERE resource_type resourceType AND resource_agent_id agentId AND resource_key resourceKey AND flow_direction OUT currentBalance totalIn - totalOut UPDATE resource_balance SET balance_qty currentBalance WHERE resource_type resourceType AND agent_id agentId AND resource_key resourceKey校验的金额逻辑同样用这个方式唯一区别是单位转为金额。这整套实现有个很好的性质不管业务怎么扩只要新事件遵循 give/take 双资源链接旧的对账逻辑就不用改。后来我在课程包升级、退费、转班这些需求里都用同一套方法非常省心。4. 常见问题与排查技巧实录REA 落地的那些坑4.1 路径校验失败事件方向标反或资源缺一边最常出现的问题是在 event_link 里把方向搞反。写的时候想着“我付了课酬现金减少了”但在表里录入方向时写成了 IN导致对账后现金余额虚增。排查思路并不复杂但很依赖日志级数据选一个真实的现金流水人肉走一遍原业务把每个事件链接到 resource_balance 的累计变更直到发现余额偏差发生在哪一笔事件。另一个关联问题是事件漏掉了资源侧。比如做“退费”时只记了现金减少忘了同时减少课时资源或应收账款。这会导致学员课时余额偏多但现金总额是对的。我用一个小技巧避免这种错误每个事件在原型阶段就强制填 give 和 take 两个资源如果事件只有单侧变化我会质疑这个事件是不是描述不完整而不是直接把数据塞进表里。4.2 代理人归属不清晰责任认定模糊设置内部代理人和外部代理人时如果没有在链接表中区分“角色”过段时间就会忘记某个代理人在事件里到底干了什么。比如“报名缴费”里外部人员是学员销售顾问是内部人员但销售顾问可能也是代理关系的推荐代理人。如果只是把 agent_id 填成销售顾问的 ID后续统计提成时就要额外关联很多张临时表。我的经验是把“参与事件的角色”做成一个独立字段不隐藏在原表中。这样每次查事件明细时都能清楚地看到“这个内部代理人是作为销售顾问参与还是作为财务审批参与”。不要怕字段多怕的是字段含义不清晰。醒来后你会发现分析师问的问题有一半是“这个负责人到底是审批人还是创建人”。4.3 多币种、期初存量与历史数据迁移这个项目如果在真实企业里做一定会遇到多币种和期初数的问题。REA 模型天然适合多币种因为事件记录的是原始币种金额资源余额可以按币种分组统计。但要注意同一个资源在链接表里的 amount 字段应该保留原币种不要统一折算成记账本位币。一旦折算汇率变动就会污染事件事实。我建议单独建汇率表和折算视图不动业务事实表。期初存量怎么处理最简单的方式是构造一个“期初导入事件”责任人设为系统管理员。这个特殊事件在 event_log 里按照普通事件记录但 event_type 标记为 INIT_IMPORT。这样做的好处是老数据也能纳入同一套对账链路避免资源余额凭空出现。后续任何人对期初数有异议都可以直接打开这个初始化事件查看历史快照和导入凭证。4.4 从现有系统迁移到 REA别急着删老表多数情况不是新建系统而是要改造一个已经有大量数据的旧系统。我在“rea”原型里试过一种稳妥的迁移策略先在旧库旁边建一套新的 REA 事件表通过离线解析老订单表和流水表把历史业务“重放”成事件链然后再开始并行运行。期间旧系统照常写自己的业务表新系统只消费新增业务等两边对账一致后再把读流量切到新模型。这个策略能避免那种“重构一年上线爆炸”的情况。因为 REA 的表结构完全可以从旧数据里生成你不需要业务方先提供完美数据而是用事件链去重放数据重放不成功的地方往往就是老系统里数据冲突的地方。这些冲突点列表就是最宝贵的重构需求清单。4.5 全流程问题速查表我踩过的坑和检查项症状可能原因快速排查动作资源余额越算越少event_link 方向错或漏记事件按时间重放找第一个余额异常事件收入和现金对不上把收款事件拆成资源时少了一侧检查收款事件 give/take 两侧资源讲师课酬多算结算单确认事件重复写入给事件加唯一约束如 (event_type, source_biz_id)代理人角色混乱参与角色未建模在事件链接表增加 role 字段不叠加代理人了事老数据对账失败历史数据缺字段生成期初导入事件补数据不修旧表跨币种不平amount 被折算成统一币种改为保存原币种用视图折算排查还有一个我始终用的原则先看事件链后看余额表。因为余额表只是投影事件链才是能真正说明业务因果关系的证据。你盯着余额表找问题只能发现问题现状你盯着事件链找问题能找到问题是怎么发生的、该由哪个事件来纠正。“rea”做到最后我的体会是这套模型的收益不在刚开始建模时体现而是在后续需求变更时体现。业务方提一个“新增退课”的需求只要退课是“原缴费事件的逆事件”整个事件链就是现成的开发只需要加一个新的 event_type资源增减逻辑直接复用。反过来如果一开始就用状态字段堆业务这种需求往往要动五六个表。这套方法不挑行业只要你面对的业务里存在价值交换和责任追溯就值得试着用 REA 重新画一次业务底图。
阅读完成 · 觉得有帮助?
咨询建站