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

REA业务建模实战:用事件流重构业务数据底座

REA业务建模实战:用事件流重构业务数据底座 ★ FEATURED ARTICLE
1. 会议室白板上那三个字母REA到底解决什么问题先讲个真实经历。去年年中我帮某公司梳理他们一套进销存系统的数据模型开评审会时财务顾问在白板上写了REA三个字母说要按这个思路重构。我当时第一反应是这又是哪个新框架等他把模型讲完我才发现这不是什么新工具而是一套从1980年代就存在的企业业务建模方法只不过在国内很多团队里属于那种听说过、没系统学过、更没人落地过的知识正好我那段时间用它重建了一套业务数据底座踩了不少坑也攒了不少经验。这篇文章就是把这段落地过程掰开揉碎讲一遍适合正在做业务系统建模、ERP集成、财务数据中台或者想知道除了借贷科目之外业务账还能怎么记的读者。要理解REA得先理解它要解决的问题。传统企业软件里数据通常按单据和科目组织。进销存里有采购单、入库单、销售单、出库单财务系统里有凭证、总账、科目余额。表面上各管各的实际上同一个业务动作在几个系统里被记了好几遍名字还不一样比如仓库叫出库销售部叫交货财务叫销售出库成本结转说的其实是同一件事。这种割裂导致一个很现实的问题销售经理问这个月到底卖了多少财务问收入确认了多少仓库问发出去了多少三个数永远对不齐每次月底都要花大量时间人工核对。REA做的事情就是换一个角度组织数据不再以单据类型为第一分类而是以业务事件为第一分类把每个事件关联到的资源、参与者、发生时间完整记录下来然后再从这些原子事件出发去生成库存数、收入数、往来余额。这相当于把原来分散在各系统里、各自加工过的半成品数据重新拆回原材料需要什么统计口径时再临时加工。所以REA准确说是资源-事件-参与者三要素模型的缩写由一位会计信息系统研究者提出核心思想是会计信息系统应当记录能改变企业经济资源的业务事件本身而不是直接记录会计科目变动。换句话说业务事件是源头会计凭证只是它的一种输出视图。我在实际项目里的体会是这个思路帮助我抵抗住了一类非常常见的设计冲动——每来一个新的报表需求就往A表加一列状态往B表加一个标志位最后系统变成一个数据沼泽。REA那种事件是唯一事实来源其余都是推导结果的约束天然逼着你把系统做干净。但有一点得先说明白REA不是要推翻你现有的ERP或者财务模块它更像是一套组织数据和设计表结构的方法论。你完全可以在一个旧系统旁边用REA思路建一张业务事件明细表把各系统的业务动作统一接入把它作为中台层的事实底座再往上做报表、对账、审计追溯。我在甲方落地时走的就是这条路不折腾老系统只新增一套事件流水层。2. 资源、事件、参与者三类要素与五条关键关系的拆解2.1 资源能被组织控制并且有价值的东西先看三类要素的第一类资源。所谓资源就是企业拥有或控制、可以在交换中获得价值的对象库存商品、现金、银行存款这些是最直观的。REA里对资源有一个很重要的限定它必须能归属到某个明确的会计主体并且有可计量的价值换句话说不是任何东西都能当资源。比如客户关系算不算资源严格说算但在REA落地时一般不会把客户关系或品牌这种无形资产直接放进资源表因为难以量化、难以关联具体事件强行建模只会让表结构变得僵硬。我在重建模型时的做法是先把企业资产负债表和库存账上有名字的对象列出来作为候选资源再问一句有没有一个事件会让它的数量或价值发生变化有才能入表。比如咖啡豆是资源因为采购事件让它流入、销售事件让它流出。客户就不是资源客户在REA里属于参与者因为客户本身不因为业务事件发生数量增减他们是通过控制关系参与到事件里的。这里有个容易混淆的点像应收账款这种在会计上是资产科目的东西在REA模型里通常不作为资源表里的一条静态记录而是作为互换缺口的投影出现。也就是客户拿了货但还没付钱这个事实在REA中表现为销售事件已发生对应的收款事件尚未发生这一状态而不是一张独立的应收账款主数据表。这个设计初看有点绕但正是REA在追溯性上远强于传统科目账的原因每一笔应收都能精确落到某一次销售事件和某一个未完成的收款事件上。2.2 事件业务动线上那些动词第二类要素也是REA的核心事件。资源用名词命名事件则对应动词采购、销售、领用、入库、收款、付款、盘点、报废都是事件。事件的定义非常严格它必须是能改变资源存量或引发经济交换的业务动作一张销售订单如果还没真正改动货物和现金的存量它算不算事件严格说的是待执行承诺不算影响资源的事件但在完整REA建模里是可以把它作为一种承诺类事件先记下来但不能拿来控制库存和收入。很多团队在落地REA时把订单、计划这些也一股脑塞进事件表最后发现库存算不准就是没区分承诺事件和执行事件。我给客户建模时把事件分成两层承诺层订单、计划、合同和实绩层入库、出库、收付款。实绩层才直接驱动资源变动承诺层用于跟踪业务进程两层的关联关系靠单据编号或者一件可追溯字段串起来。这样做的好处是既保留了业务视角的订单追踪能力又不污染真实库存和资金计算。事件还得有一个属性发生时间。REA强调按业务事件的实际发生时间记账而不是按单据录入时间这对月底结账后还能更正前期业务非常重要。我们在事件表里同时保留了业务时间和记账时间两个字段前者用于业务分析后者用于系统对账避免因为补单、回溯录入导致的数据偏差。2.3 参与者内部的人、外部的人第三类要素是参与者。参与者分内部和外部两种内部参与者是本公司员工、部门比如仓库员、销售员、收银员外部参与者是客户、供应商、承运方等。参与者与事件之间是控制关系意思是任何一个改变资源的业务事件必须能回答两个问题谁做的决定谁执行的操作这就是为什么事件和参与者要建立多对多关系一件销售事务里至少有两个参与者销售人员促成它客户接收它仓库员执行它。传统单据表里一般只记一个业务员字段REA要求把参与者拆成一张独立的参与者维度表事件参与者关系表来承载多重身份这在做流程追溯时价值非常大。举一个我实际遇到场景某仓库月底盘点发现账面库存和实物库存差了40箱传统模式下大家查单证看人库记录、看调拨单很难定位责任环节。用REA事件参与者关系表可以直接查在这段时间内所有对这批货物执行过入库、出库、移库事件的人分别是哪些把每个事件的参与者串成一条责任链数据能直接告诉你流程卡在哪个环节、问题最可能在谁那里。这不只是财务建模其实是把业务流程管理、内控审计的诉求统一进了同一套数据结构里。2.4 五类关系一表看全三要素之间靠关系连接我在设计里实际用到的关系有五种它们共同保证了模型的严谨性关系名连接对象语义说明举例存量关系事件-资源事件导致资源流入或流出入库事件增加咖啡豆库存控制关系事件-参与者参与者决定或执行了该事件销售员签订销售事件互换关系事件-事件两个经济事件互相配对形成交换发货事件与收款事件配对责任关系内部参与者-外部参与者企业内部人员对外部伙伴承担责任销售员面对客户触发关系事件-事件一个事件引发下一个事件订单事件触发发货事件我在设计系统时没有为每一种关系都建独立的物理表那样会非常繁琐。实际方案是事件表加一个配对组号字段用来表达互换关系事件参与者关系表存控制关系事件资源关系表存存量关系触发关系通过上游事件号字段表达。把语义关系降维成字段和关系表既保留了REA的逻辑严谨性又不会让建模过度膨胀到没法落地。这是从理论到实践必须做的权衡。3. 给一家小型咖啡豆贸易商完整建模从采购到回款的手工推演3.1 第一步把业务动线改写成事件流理论光说容易飘我拿一个虚构的小业务来完整走一遍建模过程。假设有一家做精品咖啡豆的小贸易公司业务很简单向产地供应商买生豆入库卖给烘焙店客户收款付款偶尔报废损耗。第一步是画出业务动线把它改写成事件流。这家公司的典型流程是采购下单供应商送货入库销售部与客户签销售单仓库按销售单出库发货财务收到客户货款财务向供应商支付货款仓库处理一次受潮报废。这些动作里下单和签销售单属于承诺层事件真正改变资源的是入库、出库、收款、付款、报废所以我把这五个作为核心实绩事件两个承诺事件作为追踪辅助。实际落地时订单事件和入库事件通过一条上游单据号字段关联就能知道这张订单到货没有、齐不齐。用REA视角重新表述这家公司的业务地图就是采购入库事件生豆资源流入与采购付款事件现金资源流出构成一组互换关系销售出库事件生豆资源流出与销售收款事件现金资源流入构成另一组互换关系报废事件是生豆资源的单向流出它没有配对的资源流入属于经营损耗。这么一摆整个公司的资源流向其实只有三条线非常清晰。3.2 第二步为每个事件补上资源和参与者事件流确定后逐个事件补全它的资源维度和参与者维度。我做了一张表来整理这张表后来也成了建表时最直接的参考事件资源流入/流出内部参与者外部参与者采购入库咖啡豆流入采购员、仓管员供应商采购付款现金流出出纳供应商销售出库咖啡豆流出销售员、仓管员客户销售收款现金流入销售员、出纳客户报废咖啡豆流出仓管员、审批人无采购订单承诺无资源变动采购员供应商销售订单承诺无资源变动销售员客户这一步会暴露出很多问题。比如这家公司实际业务里销售员带客户参观样品后直接报价、收款根本没有走系统下单流程——那销售订单这个承诺事件其实不是必选项如果强行建模反而会引入虚构数据。建模时一定要按真实业务动作来而不是按现成系统的功能菜单来。再比如报废事件在制度上要求必须有审批人这个审批人是内部参与者但传统进销存报废单里经常只记一个经办人字段用REA模型就能自然地把经办和审批两个角色拆开存将来查谁审批的、谁经手的一目了然。3.3 第三步用资源守恒检查模型是否漏了环节模型搭完后要做校验。我把这称为资源守恒检查把每个资源看作一个水池所有流入事件往池里加水所有流出事件从池里放水池子在期初和期末的存量必须能校验上。以咖啡豆为例期初库存加采购入库总量减销售出库总量再减报废总量应当正好等于期末库存。如果对不上说明模型里漏了什么事件。我在验证一家客户的数据时就发现仓库在月底做了一次盘点调整这批盘盈盘亏之前从来没人建模导致月底库存总差一点点。后来把盘点调整也作为一种事件类型建模数据就闭合了。这个资源守恒思路还能用来做自动对账不用等财务报表出来业务系统自己就能每天跑一遍平衡检查哪个资源的水池不平哪一个事件数据有问题立刻暴露。我后来在系统里专门写了一个对账任务把所有资源分类轮流跑一遍守恒校验跑完出一张明细表标注哪类资源、哪一天、哪个事件导致的不平这比月末财务人员拿着表去各系统里翻高效得多。4. 把REA模型转成数据库表表结构、案例SQL与查询思路4.1 推荐的主表与关系表设计模型确定后下一步就是落成数据库表。我的核心思想是用一套通用的资源库、事件库、参与者库作为事实底座具体业务字段尽量放在属性字典或者扩展字段里避免每个业务类型都建一套新表。这样设计后续加一个业务事件类型时只需要插数据不需要改表结构。下面是我实际用过的核心建表逻辑-- 资源表存所有可量化、可控制的经济资源 CREATE TABLE resource ( resource_id BIGINT PRIMARY KEY, resource_code VARCHAR(32) NOT NULL, resource_name VARCHAR(128) NOT NULL, unit VARCHAR(20), is_money_flag BOOLEAN DEFAULT FALSE, created_at TIMESTAMP ); -- 参与者表内部员工、外部客户和供应商都放在一起 CREATE TABLE participant ( participant_id BIGINT PRIMARY KEY, participant_type VARCHAR(32), -- INTERNAL / EXTERNAL role_code VARCHAR(32), -- CUSTOMER / SUPPLIER / SALESMAN / CASHIER display_name VARCHAR(128) ); -- 事件主表记录每个业务事件 CREATE TABLE business_event ( event_id BIGINT PRIMARY KEY, event_code VARCHAR(64), event_type VARCHAR(32), -- PURCHASE_IN / SALES_OUT / RECEIPT / PAYMENT / WASTE biz_time TIMESTAMP, entry_time TIMESTAMP, docs_no VARCHAR(32), group_id VARCHAR(48), -- 互换配对组号 upstream_event_id BIGINT, -- 触发关系上游事件ID memo TEXT ); -- 事件资源关系表一个事件涉及多个资源多对多 CREATE TABLE event_resource ( id BIGINT PRIMARY KEY, event_id BIGINT NOT NULL, resource_id BIGINT NOT NULL, flow_type VARCHAR(4), -- IN / OUT quantity DECIMAL(18,4), unit_price DECIMAL(18,4), amount DECIMAL(18,4) ); -- 事件参与者关系表一个事件有多个参与者且参与者角色不同 CREATE TABLE event_participant ( id BIGINT PRIMARY KEY, event_id BIGINT NOT NULL, participant_id BIGINT NOT NULL, role_in_event VARCHAR(32) -- 例如 APPROVER / OPERATOR / COUNTERPARTY );这套结构看着简单但它诚实反应了业务事实。举个例子销售出库事件在event_resource表里对应一行咖啡豆流出的记录同时在event_participant表里对应了销售员OPERATOR、仓管员OPERATOR、客户COUNTERPARTY三条参与者数据。如果你想知道某个客户这一年买了多少货、经手人是谁、发了几次货一张SQL联查就能完成不需要在销售单主表里塞一堆乱七八糟的冗余字段。4.2 一个更贴近项目的父子表方案事件头资源明细上面那套通用表是理论上的纯REA但实际项目里开发同事抱怨得最多的就是你这事件表太抽象了我怎么套业务单据所以后来我退了一步做了一套更贴近实际开发的父子表方案每个业务单据仍然有主表和明细表但主表字段不再存放具体业务的散装字段而是对齐到事件公共属性明细表对齐到事件资源关系。比如销售出库单主表就是一行事件出库单明细表就是这次出库涉及的多种咖啡豆、数量、单价。这样做的效果是开发人员写接口时心智模型还是单据头明细行改动成本小数据存储结构本质上却已经是REA事件模型天然支持跨单据追溯以后想加一个按资源维度统计的报表不需要再抽数据直接对事件资源明细表做合计就行。如果业务特别复杂比如一个销售出库单既出货物又涉及包装物回收包装物也算资源父子表照样处理明细行里同时出现货物流出、包装物流出的两行每行有自己独立的flow_type和quantity。这在传统单据设计里会很别扭但在事件明细模型里非常自然。4.3 三条必会的账面恢复查询与冗余边界结构设计完很多人会问REA模型里没有库存字段、没有应收余额那我怎么查答案是靠聚合查询即从事件流水还原视图。我在项目里最常用的三条SQL可以覆盖绝大多数场景。第一条查某资源在某时点的实时库存SELECT r.resource_name, SUM(CASE WHEN er.flow_type IN THEN er.quantity WHEN er.flow_type OUT THEN -er.quantity ELSE 0 END) AS stock_qty FROM business_event e JOIN event_resource er ON er.event_id e.event_id JOIN resource r ON r.resource_id er.resource_id WHERE r.resource_id :res_id AND e.biz_time :as_of_time AND e.event_type IN (PURCHASE_IN,SALES_OUT,WASTE,STOCK_TAKE_ADJ) GROUP BY r.resource_name;第二条查某客户截至某日的累计购买额SELECT p.display_name, SUM(er.amount) AS total_purchase_amount FROM event_participant ep JOIN participant p ON p.participant_id ep.participant_id JOIN business_event e ON e.event_id ep.event_id JOIN event_resource er ON er.event_id e.event_id WHERE p.participant_type EXTERNAL AND p.role_code CUSTOMER AND ep.role_in_event COUNTERPARTY AND e.event_type SALES_OUT AND e.biz_time :as_of_time GROUP BY p.display_name;第三条查所有已发货未收款的互换缺口即传统意义上的应收账款SELECT e_sales.docs_no, e_sales.biz_time AS sales_time, p.display_name AS customer_name, SUM(er_sales.amount) AS amount_due FROM business_event e_sales JOIN event_resource er_sales ON er_sales.event_id e_sales.event_id JOIN event_participant ep_cty ON ep_cty.event_id e_sales.event_id JOIN participant p ON p.participant_id ep_cty.participant_id WHERE e_sales.event_type SALES_OUT AND NOT EXISTS ( SELECT 1 FROM business_event e_pay JOIN event_resource er_pay ON er_pay.event_id e_pay.event_id WHERE e_pay.event_type RECEIPT AND e_pay.group_id e_sales.group_id ) GROUP BY e_sales.docs_no, e_sales.biz_time, p.display_name;这类查询的特点是把冗余降到了最低不维护余额字段就永远不会因为某一处状态没更新导致账不平。有人会说性能会不会差确实如果业务量一天几十万条事件每条SQL都实时聚合会很吃力。所以我又加了一层账本表每天晚上把当天的资源变动按资源维度累加进一张余额快照表实时查询走快照追溯查询走明细两头兼顾。这就是我强调的保留明细事实、提供预聚合视图。5. 落地三次才总结出的坑多对多、账期、性能与名词5.1 坑把服务、工时也硬塞进资源里我第一次做REA落地时犯过一个现在想起来很蠢的错。客户有维修服务业务工程师上门修设备每小时收费。我一开始按字面意思把服务工时当成一种资源设计了服务工时资源表结果运维团队当场发问工时怎么入库怎么盘点怎么报废我当时答不上来。REA里的资源概念本质是可交换、可存量、可控制的有形体或明确权益而服务类业务动作更像是一连串事件接单、派工、执行、结算的产物不该被抽象成一种资源。正确的做法是把维修服务建模为一系列事件其中维修执行事件关联到被维修设备资源的状态变化由损坏变维修完成由故障变正常工时只是参与者在事件上投入的工作量属性不是资源实体。这个教训让我明白资源必须能回答它的存量是什么意思回答不上来就不是资源换个角度建模。5.2 坑事件时间点和财务账期间对不上第二个坑出现在月底结账。REA按业务事件实际发生时间记录但财务结账是按月、按季度分会计期间的。一个跨月的典型场景客户在3月31日晚上收到货但仓库单据录入是4月1日收款又发生在4月5日。如果单纯按业务时间统计3月销售收入会漏掉这张货已发出、跨期间的订单如果按录入时间统计业务部门又说不对这货明明3月底就发了。解决办法是我在事件表里加了一个账务归属期间字段由业务规则自动派生正常情况下等于业务时间所属期间跨期末处理的单据允许财务人员调整归属期间但原始业务时间不更新。这样业务分析随时可用真实时间聚合财务出表用期间字段归集互不干扰。这块设计看起来是小事实际决定了系统能不能在月底被财务信任。5.3 坑多对多关系表的查询性能第三个坑来自多对多结构。理论模型很完美但上线后第一个月销售部查客户累计购买额的报表慢到超时。排查后发现event_resource和event_participant两张关系表都已经几百万行实时聚合太吃资源。我没有粗暴地去优化SQL而是做了两个改动第一把客户-商品-事件这种高频分析维度做成物化视图每天定时刷新报表直接查视图 第二在业务层面明确一个规则普通经营分析允许几分钟级数据延迟实时看板只查当日增量数据不做全量聚合。这套思路落地后SQL平均查询时间从十几秒降到几百毫秒。REA模型作为事实底座是完全正确的但工程化必须引入聚合层不能指望所有查询都跑原始事件表。5.4 坑跨系统名词对齐比建模本身更耗时第四个坑不在技术模型里而在组织协同里。开发了一套REA事件模型后要接各系统数据才发现每个人对发货出库交货的定义都不一样仓库说发货是把货交给快递销售说出货是财务开发票司机说出货是装上货车。如果这些系统都按自己的口径把数据灌进来事件表瞬间就乱了。我们在项目启动阶段专门组织了一次业务名词定义会把每个接入系统的同义词映射到统一的REA标准事件编码上销售出库、采购入库、收款、付款、盘点、报废事件编码唯一各系统的字段映射到事件明细的属性字典里。定义表建完后数据对接速度才真正起来。这个坑让我认识到技术建模做得再完美也一定要前置一项口径治理工作它是REA落地能否成功的隐形前提。5.5 问题速查表我把上述踩坑归纳成一张决策表后面做同类项目时直接对照症状根因对策服务/培训业务建模困难把不可存量的服务当成资源按事件序列建模工时作为属性月末账和业务账对不上事件时间与账务期间混淆增加账务归属期间派生字段多表聚合查询变慢未做预聚合层物化视图 余额快照表各系统数据语义冲突没有统一业务名词上线前先做事件编码映射定义6. REA与复式记账不是二选一视图层联动方案6.1 为什么最终还是要回到借贷分录有人看完REA会觉得那会计凭证是不是可以不要了这种想法很危险。REA确实记录了业务事件的完整语义但企业对外报税、股东报告、审计都要求有一套高度标准化、具备法律约束力的记账凭证体系。复式记账经过几百年考验凭证-科目-余额这条链路本身就是一个极好的校验机制不该被抛弃。所以落地时我坚持的原则是REA作为业务事实层复式记账作为会计输出层两层之间通过规则做自动映射。这也回答了刚接触REA的人最常见的疑问我用了REA是不是要把公司财务系统换了完全不用。你可以在业务系统里用REA组织业务事件然后生成符合会计准则的总账凭证送入已有财务系统老系统继续做它擅长的事出报表、应对审计。6.2 通过记账规则表把事件翻译成凭证事件如何变成凭证我建议不要写死在代码里而是配一张记账规则表。规则表的逻辑很简单每个事件类型对应一组借贷分录模板模板里关联具体的科目和取值字段。以销售出库事件为例规则可以配成借应收账款贷主营业务收入、贷应交税费金额取事件资源明细里的售销金额字段同时再配一张成本结转规则借主营业务成本贷库存商品金额取库存成本价。有了规则表业务上新增一种事件时只需要在配置表里加一行不需要发版本改代码财务口径调整时灵活性也高。这套机制后来被我们称为规则驱动转凭证它让REA事件层和财务总账层实现了解耦两者各自演进互不阻塞。记账规则表就是两者之间的协议层。6.3 用资源流入流出存量变动做双向对账REA还提供了一个额外红利天然的双向对账能力。总账上的存货科目余额、应收科目余额应该分别等于REA事件层里对应资源的存量视图和互换缺口视图。我做了个对账脚本每月结账前跑一遍比较总账库存商品科目余额与REA库存视图的差异比较总账应收账款明细与REA未收款事件列表的差异比较总账银行存款科目与REA现金资源流入流出净额的差异。差异不为零时脚本自动定位到发生差异的那一天、那一个事件、那张单据。这套对账机制上线后月底财务结账时间从两天压缩到半天财务同事第一次真正信任了业务系统的数据。这也是我在项目中最有成就感的成果不是因为引入了一个新模型而是让业务和财务从此讲同一种语言。6.4 什么项目适合REA什么项目不必硬套最后给个选型建议别什么系统都往REA上靠。以下几种情况很合适业务流程复杂、事件类型会持续增加、需要按人员和环节追溯责任、业务数据要支撑多种分析口径、数据中台建设、以及财务业务一体化改造项目。反过来如果只是一个简单的后台管理功能表单单一、不产生跨系统追溯需求、没有复杂业务流程用普通的CRUD加几个状态字段就够了强行套REA只是增加开发成本收益不明显。我后来判断项目是否采用REA就一句话有没有需要被反复追溯和交叉统计的业务事实有就值得没有就别给自己加戏。说到底REA给我的最大启发不是那三张表而是让事实只存一次让所有结果都可以被推导这一原则。这几年做下来我越发觉得企业软件里大量所谓对不上账的根因都是因为同一个事实被不同系统以半成品状态各存了一份加工口径又不一致。REA把事实重新还原成事件流就像把照片还原成底片怎么冲洗、冲成什么样子都由后续使用场景决定底片本身永远干净可靠。如果看完这篇你也准备动手落地我的建议是不要试图一次性把企业全部业务都建模成REA先选一条现金流主线采购到销售、从收付款的回款跑通一遍把资源守恒校验和事件编码映射规则搭好再逐步扩展其他业务线。开始慢一点后面的路会顺很多。
阅读完成 · 觉得有帮助?
咨询建站