ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

REA模型实战:用事件驱动解决业务与财务数据不一致

REA模型实战:用事件驱动解决业务与财务数据不一致 去年年中我在梳理模拟项目X的订单履约链路时被一个简单的问题卡住了财务说账上还有120万应收账款业务说早该催款的只有80万两边数字对不上。系统里明明都用了同一套订单数据为什么算出来的结果会差这么多追下去才发现症结出在底层数据模型上。财务模块延续的是复式记账那一套——凭证、科目、借、贷、余额。每一笔订单都要经过凭证模板转换成会计分录再由总账系统汇总成余额。业务侧的数据模型则围绕订单、发货单、回款单这些业务单据。两套模型之间的桥梁是几十个字段映射和转换规则。只要中间任何一张单据漏配、错配财务账与业务台账就会悄悄分叉。这不是某一家企业特有的问题而是传统会计信息系统的结构性毛病。复式记账法诞生于商业贸易靠纸质凭证的年代它擅长的是记录金额的增减而不是描述业务发生了什么。借贷分录天然地站在结果的视角站在账户的视角站在一种已经被抽象过、压缩过的状态上。它不关心订单是几点几分被确认、货物由谁拣选、物流签收发生在哪个环节而恰恰是这些过程信息构成了数字化业务系统的核心。正因如此我才开始认真研究rea。rea的全称是Resource-Event-Agent最早是会计信息系统领域的研究者提出的一个企业建模框架后来被大量用在业务流程建模、领域驱动设计甚至事件溯源架构中。它主张用资源、事件、参与者三要素来描述业务并认为会计账簿中的数字应该是这套业务模型的自然派生结果而不是需要另起炉灶单独维护的一套数据。这篇文章会给你完整展示我如何用rea模型梳理一条从销售到收款的全流程包括底层表结构设计、与DDD和事件溯源的映射关系以及落地时踩过的四个坑。如果你也正在被业务数据与财务数据不一致折磨或者想在下一个项目里尝试事件溯源架构这篇文章应该能帮你省下不少摸索时间。1. 数据分叉背后借贷式记账为何拖累了数字化业务1.1 财务模块与业务台账各说各话我在模拟项目X里最先碰到的矛盾其实是很多企业系统的共性困境。订单、发货、回款在业务库中都是标准的结构化数据每一笔都有时间戳、有状态、有操作人可财务模块不看这些它只认凭证。系统里有一个凭证生成器把每一个业务动作翻译成借xx科目、贷xx科目再汇总进总账。问题就出在这个翻译层上——翻译规则一旦不完整业务侧和财务侧就再也对不上。比如一张销售订单在业务侧已经发货但凭证生成器因为某个状态位没更新没有生成对应的收入确认凭证。此时业务台账显示已发货未回款财务账上却完全没有这笔应收记录。等到月底对账时两边就差出一大截。你可以在事后补一个批处理任务去修复但每次修复都是打补丁补丁越打越多系统越来越难维护。这种困境的根源是数据模型的方向性错误。借贷记账法和业务事件流是两套完全不同的抽象层级。前者把业务动作压缩成借贷金额这一种表达方式后者需要保留业务动作的完整语义——谁、何时、做了什么、改变了什么。当你用一套高度压缩的数据结构去承载需要全细节的业务逻辑时摩擦是必然的。1.2 rea是另一种组织业务事实的方式rea给我的第一个启发就是它换了提问方式。传统模型问的是这笔业务该记在哪个科目、该借还是该贷rea问的是发生了什么事件、涉及什么资源、由谁参与。这三个问题看起来朴素建立出来的数据模型却完全不一样。它不预设账户这个抽象概念而是把业务事实本身作为建模的起点。会计意义上的借和贷在rea里不存在取而代之的是资源流入和资源流出而流入流出后得到的状态——比如应收余额、库存数量——全部可以由事件推导出来。两个模式放在一起对比会更直观视角传统借贷模式rea事件模式记录对象账户、凭证、科目资源、事件、参与者核心问题余额是多少发生了什么时间特性期末汇总、过账结转事件发生时记录余额产生方式借贷汇总事件流投影业务追溯凭证到流水多跳映射事件天然关联表格里最后一行是我感触最深的地方。传统架构要做一笔应收追溯通常是从科目余额出发找到凭证从凭证找到业务单据中间还可能经过几张结转表rea模型则可以直接定位到哪一笔销售事件产生了这笔应收哪一笔收款事件核销了它。这种追溯能力对数字化业务太重要了几乎决定了一个系统能否被长期维护。2. 拆开rea资源、事件、参与者分别回答什么问题2.1 资源不是所有资产都能进模型很多人第一次接触rea时最常犯的错误就是把资源等同于会计科目或资产。实际上rea中的资源必须满足两个条件第一它必须处于企业的控制范围内或交换范围内第二它必须能够在经济事件中被消耗、转化、转移或创造。举个例子库存商品是资源因为销售事件会让它减少采购事件会让它增加现金和银行存款是资源因为收款事件会让它增加付款事件会让它减少。但厂房、设备、商标权这些在企业日常经营活动建模中往往不会被当作rea资源——它们不参与频繁的业务事件流转更多是作为一个固定的经营前提存在建模时把注意力放在它们服务的那些业务事件上即可。为了便于理解我把资源类比成水库里的水位。水位本身是静态的存量你不关心它的绝对值而是关心它在什么时间点因为什么事件发生了多少变化。这个视角和传统库存管理不一样传统库存管理一上来就要正确的当前库存量而rea更关心库存量是如何一步步变成现在这个值的。2.2 事件只看直接改变资源状态的动作如果说资源是水库的水位那么事件就是打开阀门或关上阀门的动作它是整个rea模型的枢纽。rea对事件的定义有一套严格但不难把握的标准一个事件必须在一个明确的时间点发生必须能被记录必须与至少一个参与者相关并且必须直接改变一个或多个资源的状态。注意这里我特意强调直接。这个直接标准对建模粒度非常关键。回到销售场景中客户点击了商品详情页就不算事件因为点击动作没有直接改变任何受控资源的状态。而销售订单被确认就算事件因为它直接让库存商品从可售状态变成预占状态或者说资源的状态发生了变化。我把这个判断标准总结成一个辅助公式如果一个活动既不增加、不减少、不转化、也不转移任何资源那它就不应该出现在rea模型中。用这个标准去评审建模草稿通常能很快发现哪些地方塞进了太多伪事件。2.3 参与者内部与外部划分以及职责分离参与者是整个模型中容易被忽略但同样重要的一环因为资源的状态变化从来不是自动发生的背后必须有人的决策或行动。rea把参与者分成两类内部参与者和外部参与者。内部参与者是企业内部的角色例如销售员、仓管员、财务人员外部参与者是企业边界之外的角色例如客户、供应商、物流承运商。这种区分在建模时非常有用因为它对应了企业风险管理中的职责分离原则。一个合格的事件模型应当能回答这个事件由哪个内部参与者触发、对哪个外部参与者生效这天然支持了审批流、控制流的设计。比如销售员录入订单、仓库员审核出库、财务人员确认收款三个步骤在三类参与者之间形成了内部控制链路。我曾经在一个项目里见过反面教材业务单据上只有一个操作人字段客户和员工统统往里面塞。这样做导致审计时完全无法回答是谁代表公司、是谁代表客户每一个关键操作的责任边界都是模糊的。rea的参与者划分能强制你把这个边界建立起来这对系统合规性是非常大的加分。2.4 三类关系把三要素编织成业务网络光有三类要素还不够rea模型靠三类语义关系把它们串起来存量-流量关系Resource-Event描述资源与事件之间的增减关系。一个事件可以使资源流入也可以使资源流出甚至可以同时流入流出例如调拨事件。触发关系Event-Event描述事件之间的因果顺序。如销售订单确认触发出库发货出库发货触发收款登记。控制关系Event-Agent描述参与者与事件之间的责任关系。内部参与者对事件负执行责任外部参与者则作为相对方。这三类关系就是rea的语法。构建模型时我基本就是围绕这三类关系反复自问这个事件改变了什么资源它由谁触发、影响了谁它的前序事件和后续事件分别是什么一旦能顺畅回答这三个问题模型的雏形就出来了。3. 实操演练从销售订单到回款的一条完整事件链3.1 划清业务边界列出事件候选为了让讨论不悬空我设定一个具体的演练对象某零售企业的订单到收款流程。范围从客户提交订单开始到企业收到货款、完成资金核销为止。在动手之前我习惯先列一条候选活动清单。这里不用管粒度对不对凡是流程中肉眼可见的动作都先写下来客户下单、销售确认订单、仓库拣货并出库、物流配送、客户签收、客户付款、财务核销。这七个动作是我准备放进模型的事件候选。划定边界这件事看似简单其实直接影响建模质量。我曾经见过有人把客户发起退货申请也包括进订单到收款的边界里导致事件链变得很复杂。边界清晰之后建模的最大价值反而体现在后续可扩展性上——如果以后要加退货流程只需要在events表里增加对应的事件类型而不用推翻已有的表结构。3.2 用资源状态变化过滤事件候选接下来用上一章那个直接改变资源状态的标准逐个过滤候选动作。候选活动是否直接改变受控资源状态是否进入模型说明客户提交订单否否待确认状态资源未变只是草案销售确认订单是是库存从可售变为预占仓库拣货并出库是是库存实物数量减少物流配送否否配送过程中货物仍归企业状态未变客户签收是是物权转移企业失去商品控制客户付款是是银行存款增加应收债权减少财务核销否否匹配动作资源本身无变化经过筛选真正进入rea模型的事件是四个订单确认、出库发货、客户签收、收款到账。这个筛选过程经常会颠覆直觉。我在实际项目里发现大多数团队建模不一致根源就在于把一些动作和事件混为一谈。比如把物流配送单独建模成事件会让后续的聚合边界变得很别扭。3.3 补齐参与者和资源事件-资源-参与者对照事件确定后资源与参与者就很好找了。我把四个事件的信息补成一个对照表这是建模过程中最常用来评审的产物。事件涉及资源外部参与者内部参与者订单确认库存商品在库可售变预占客户销售员出库发货库存商品在库减少承运人仓库员客户签收在途商品所有权转移客户仓库员收款到账银行存款增加、应收债权减少客户财务人员这张表一出来整个流程的语义就清晰了。每个事件都在资源层面有明确影响、在参与人层面有明确责任体。后续无论是开发接口、设计权限还是做审计追溯都能直接从这张表出发。这里有一个细节需要注意应收债权在这个表里出现在收款到账事件中但它并不是一个独立的资源实体而是订单确认和收款到账两个事件共同形成的派生状态。上表为了表达方便写了减少实际建模时不要把它作为资源落表。3.4 落成四张核心表SQL建表与索引思路在很多文本中rea被描述为纯概念模型但落到工程上完全可行。我在模拟项目X里就是用四张核心表实现了这套模型agents表参与者档案区分内部/外部。resources表资源档案例如库存商品、银行存款。events表核心事件表记录事件类型、发生时间、参与的内部与外部参与者。event_resource_links表事件与资源的关联表记录方向流入/流出和数量金额。一个典型的表结构定义大概是这样以通用SQL风格展示CREATE TABLE agents ( agent_id INT PRIMARY KEY, agent_type VARCHAR(20), -- internal / external agent_name VARCHAR(100) ); CREATE TABLE resources ( resource_id INT PRIMARY KEY, resource_name VARCHAR(100), unit VARCHAR(20) ); CREATE TABLE events ( event_id INT PRIMARY KEY, event_type VARCHAR(40), -- e.g. order_confirmed, shipment_made occurred_at DATETIME, internal_agent_id INT, external_agent_id INT, CONSTRAINT fk_events_internal FOREIGN KEY (internal_agent_id) REFERENCES agents(agent_id), CONSTRAINT fk_events_external FOREIGN KEY (external_agent_id) REFERENCES agents(agent_id) ); CREATE TABLE event_resource_links ( link_id INT PRIMARY KEY, event_id INT, resource_id INT, direction VARCHAR(10), -- inflow / outflow quantity DECIMAL(18,4), amount DECIMAL(18,2), CONSTRAINT fk_links_event FOREIGN KEY (event_id) REFERENCES events(event_id), CONSTRAINT fk_links_resource FOREIGN KEY (resource_id) REFERENCES resources(resource_id) );注意这张表里我没有为销售订单发货单回款单这三类单据各建一套独立结构而是让它们统一并入events表再通过event_type区分。这样设计有一个直接好处任何业务查询都只需要过滤事件类型而不需要跨多张业务单据表做join。索引方面我通常会为event_type和occurred_at建联合索引因为几乎所有报表和追溯查询都会以这两个字段为过滤条件。event_resource_links表则一定要对event_id建索引这直接决定了派生余额的查询性能。3.5 财务报表由事件流派生应收账款的计算示例最令我惊喜的部分是财务报表不再需要过账环节。以应收账款为例它的定义可以直接写成事件投影应收余额 订单确认事件中流出的商品金额之和 - 已核销的收款事件金额之和把这个逻辑落到数据库里就是两条SQLSELECT COALESCE(SUM(amount), 0) AS receivable_generated FROM event_resource_links l JOIN events e ON l.event_id e.event_id WHERE e.event_type order_confirmed AND l.direction outflow;SELECT COALESCE(SUM(amount), 0) AS receivable_settled FROM event_resource_links l JOIN events e ON l.event_id e.event_id WHERE e.event_type payment_received AND l.direction inflow;余额等于两者的差完全不需要一张单独维护的余额表。这听起来简单但背后带来的架构变化是巨大的账务数据从被动的结果存储变成了由事件实时推导的活动图任何一笔余额波动都能追踪到源头事件。4. 从模型到架构rea如何与DDD、事件溯源结合4.1 事件即领域事件rea天然贴近事件溯源我在设计模拟项目X的时候只需要问自己一个问题系统的历史数据是否足够支撑任意时点的回放如果需要那么事件溯源几乎是必然的选择。而rea的事件恰恰就是事件溯源里需要的领域事件。两者结合得如此自然是因为它们都在回答同一类问题历史到底发生了什么而不是现在账上还剩多少。事件溯源把系统当前状态看作对历史事件流的累积投影rea则告诉我们事件流如何与资源和参与者关联如何从事件流中恢复出业务语义。用rea建模再配事件溯源等于直接把业务事实作为系统的唯一真相来源。会计数字、库存数量、客户余额全都只是投影视图可以随时按需重建这就是数据与账本不再分家的实现方式。4.2 聚合边界跟随事件链划分而不是按单据划分引入rea之前我在划分DDD聚合边界时经常犹豫订单聚合要不要包含发运信息发运聚合要不要包含应收记录如果各选各的又回到数据不一致的怪圈。rea给了个比较清晰的判断依据一组事件如果它们之间存在触发关系而且一个内部参与者对另一个事件负责那么这些事件往往应该落在同一个聚合边界内或者至少通过事件同步保持一致。以销售到回款为例订单确认、出库发货、客户签收、收款到账这条事件链共享同一个客户、同一张订单上下文放在一个聚合里会让数据一致性最直观代价是聚合略微变大拆成多个聚合也可以但需要接受最终一致性带来的复杂度。聚合方案一致性复杂度适用场景全部事件放一个聚合强一致低-中订单量不大单体系统订单发货放一个聚合收款独立订单侧强一致收款最终一致中以销售为中心的企业推荐起点每个事件一个聚合最终一致高高并发、复杂分销网络我在实际项目中倾向于选择中间方案把订单确认与出库发货放在一个聚合把收款到账作为另一个聚合两者通过领域事件通知进行协调。这样既保持高频路径的强一致性又不至于让聚合无限膨胀。4.3 库存与余额从存储字段变成派生投影在这个模型下库存数量、应收余额这些传统意义上的主数据全部变成了派生视图。你不再需要在库存表里维护一个实时扣减的数字而是通过库存事件流计算得出当前库存。这种思路恰恰是CQRS架构所提倡的写模型中只追加不可变事件读模型中维护面向查询的投影。库存的投影可以是一个物化表每次出库或入库事件发生后就增量更新也可以是一张按需重算的动态视图。使用派生视图最大的好处是杜绝了分布式事务。两个系统不需要在同一个事务里同步扣减库存和记录销售只需要保证事件流的顺序一致。这个转变让系统的扩展方式从改数据库变成了追加新的事件类型变化很轻影响很可控。4.4 审计与内控控制关系直接复用传统财务系统在审计时最痛苦的就是追溯一张凭证对着一堆流水流水又对应一堆业务单据中间任何一步丢了映射审计就卡壳。rea模型天然把业务单据、业务事件、资源和参与者全部串联在一起因此审计追溯的路径大幅缩短。审计人员可以直接从一条收款记录反向跟踪到对应的销售事件、出库事件、甚至当初的订单确认操作以及操作人。这种透明性不是靠事后补录而是建模时就把控制和授权关系放进了事件结构里。控制关系在rea中还有一个额外的作用它天然支持权限设计。内部参与者和外部参与者被放在不同的字段里系统中查某个客户名下的所有订单或者查某个销售员名下已经确认但未回款的订单都只是一次简单的事件过滤不需要额外维护复杂的权限视图。5. 落地过程中踩过的四个坑5.1 坑一把资源建模成科目翻版我第一次尝试用rea时想着资源不就是钱和货吗于是顺手把应收账款建成了资源。这个选择看起来方便实际上打乱了整个模型的语义。因为应收账款不是一个基础业务实体而是销售事件与收款事件共同投影出的派生状态。把它作为资源就等于在事件之外又维护了一套账户体系模型又会退回到复式记账的老路上。正确做法是把应收留给派生层资源层只保留客户实际获得的商品、服务以及货币资金这类基础实体。这个教训我后来又验证到库存上库存量同样不是资源库存对应的基础资源是具体的商品实体而库存量是累计流入流出事件的投影。5.2 坑二事件粒度是最大的败笔来源事件粒度是最难拿捏的部分。太小的事件会把系统变成流水账例如前面说的点击了详情页一旦混入模型后续聚合边界和报表语义都会被干扰。太大的事件则会掩盖细节例如把客户签收和客户付款合并成一个销售完成事件你会丢失收款时间与签收时间之间的差异这对现金流分析至关重要。我的经验是每当犹豫要不要把某个活动设成事件就检查它是否引起可识别资源的状态变化以及它的发生时间对业务决策是否有价值。这两个条件同时满足才值得成为模型中的事件。我在项目里曾经把仓库内部移库也建模成独立事件后来发现这个动作对财务和外部客户没有任何意义白白增加了事件量。砍掉之后查询效率明显提升聚合边界也清晰很多。5.3 坑三把业务规则塞进事件模型rea擅长表达发生了什么但并不擅长表达应该发生什么。折扣规则、信用额度校验、审批流阈值……这些业务规则可以在应用层实现不需要塞进rea模型。我在模拟项目X的早期就犯了把太多校验逻辑塞进事件定义里的错误结果每个事件类型都要写一堆if-else模型反而被规则淹没。后来把规则全部移到应用服务层rea专心作为事实记录层代码结构清爽了很多。可以这样理解rea是数据库的事实表业务规则是服务层的过滤器。把它们分层之后每个部分都能独立演进而不是耦合在一起。5.4 坑四历史数据迁移与期初余额事件引入rea时最难的是处理历史数据。旧系统里长期积累的科目余额、未核销凭证怎么迁入新模型我的做法是引入一个期初余额事件概念把历史存量转化为几个特定事件。例如迁移日之前的所有应收账款汇总成为一条期初应收确认事件迁移日之前的所有库存数量汇总成为一条期初库存盘点事件。历史数据转化为的期初事件涉及资源旧系统库存数量期初库存盘点事件库存商品旧系统应收余额期初应收确认事件商品/服务待核销旧系统应付余额期初应付确认事件采购服务/商品待结算旧系统银行余额期初资金到位事件银行存款这样设计虽然看起来新增了一个特殊事件但换来的是所有后续逻辑的统一。旧账不再需要特殊的历史余额表而是和其他事件一样参与投影运算这大大简化了迁移代码。这里有一个额外提醒期初事件一定要核对原系统的明细。曾经有一次迁移我们把应收余额只按总额导入结果后续老客户发起退货时找不到对应的销售事件可冲销处理起来特别麻烦。后来我调整策略凡是能追溯到原单据的应收尽量逐笔导入为独立事件只有确实无法追溯的散单才以总额方式入账。结尾在这个方向投入了很多时间之后我最后想分享几个实践层面的体会。rea并不是要取代会计学而是一种重新组织业务事实的方式。它最打动我的地方是逼迫我从记账结果退回到业务事实把每个动作看成一条不可变的事件。这个思考习惯对我设计DDD聚合、选择事件溯源架构、甚至是评审别人的数据模型都有很直接的帮助。最后给你一个小技巧。下次评审任何业务模型时先问三个问题这里最核心的资源是什么哪些动作直接改变了资源状态是谁执行和推动的如果团队讨论始终绕不开这三个问题说明业务建模往正确的方向走了一大步。哪怕不引入全套rea这套提问方式也能让你快速发现模型里的裂缝。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表