ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

REA模型替代凭证三张表:业务系统建模实战

REA模型替代凭证三张表:业务系统建模实战 做业务系统设计我后来很少直接照搬记账凭证的三张表——凭证、科目、分录除非项目真的小到不用扩展。REA模型Resource-Event-Agent资源—事件—参与者是我在多个进销存、电商订单、财务业务一体化项目里反复验证过的一套建模框架专门用来解决“业务说不清、数据库越改越乱、财务和业务两套口径对不上”这一类问题。这篇文章不是教科书复述是实际建模踩坑后的总结适合做会计信息系统、订单履约系统、库存系统设计的朋友参考。刚入行时我也迷信“三张表打天下”觉得业务就是借借贷贷、凭证科目。后来被现实教育了老板要的不只是月末金额还要知道每个客户买了什么、买了多少次、哪些订单发了货没回款、仓库里某件商品的历史进出明细。凭证表里哪存得下这些关系于是我开始研究REA模型越用越顺。下面我会用自己的理解和一段完整的实操案例把REA讲透。1. REA模型是什么为什么它能替代“三张表”思路1.1 先理解传统记账模型怎么“失真”传统记账模型的本质是把业务活动压缩成借贷分录。业务员卖出一批商品财务记账借应收账款、贷主营业务收入再借主营业务成本、贷库存商品。金额有了但“哪个客户买的”“买了哪些SKU”“谁经手的”“什么时候发货的”全部被丢进附注摘要字段。摘要字段最大的问题是非结构化。系统里能按科目汇总金额但没法回答结构化问题某客户复购周期是多久某仓库的库存周转率如何这些信息不是不存在而是“埋”在文本里机器读不了。后来不少团队加了辅助明细表但跟主逻辑是两张皮最后变成贴膏药式开发。注意问题根源不是财务人员不专业而是“借贷”诞生于手写账本时代它为了复式记账和报表而生不是为了多主体、多视角的业务分析而生。今天的信息系统要面向流程、面向用户、面向多维分析凭证模型自然喘不过气。1.2 资源、事件、参与者三个词拆掉整张凭证表REA模型最早是会计信息系统研究领域的一套概念建模框架核心就三个要素要素英文含义例子判断标准资源Resource企业拥有或可掌控的、有经济价值的对象库存商品、现金、设备是否稀缺、有经济价值、可计量事件Event导致资源增减的经济活动销售、采购、收款、付款是否直接改变资源数量参与者Agent参与事件的人或组织顾客、销售员、供应商、财务能否对事件承担责任或参与其中有一个容易踩的误区人能不能当资源严格讲人是参与者不是资源。企业不是在“消耗”员工而是在与参与者交换或分工。如果你把员工建模成资源资源流的语义就乱了后面做成本核算会非常别扭。1.3 三对核心关系决定REA图的“语法”光有要素还不够REA强调事件、资源、参与者之间的三对关系。这三对关系是骨架理解了它们才看得懂REA图。事件与资源Stockflow资源流。事件对资源有“流入”或“流出”的效应。销售事件让库存商品流出采购事件让库存商品流入收款事件让资金流入。事件与事件Duality经济互换。一个流出资源的事件总会对应一个流入资源的事件。销售对应收款采购对应付款。这一对关系替代了传统借贷的平衡逻辑。事件与参与者Controllability可控性。表示事件由谁发起、由谁确认、对谁负责。销售事件必须关联销售员和顾客收款事件必须关联出纳和顾客。后面所有建模工作都可以理解为把业务过程翻译成“谁参与了什么事件动了哪些资源该事件又和哪个事件配套”。2. REA模型背后的设计逻辑为什么这样建模更稳2.1 从“一张照片”到“一段录像”源头事实完整留存打个比方借贷凭证像一张照片它定格了一个金额结果REA像一段录像保留了过程里的所有实体、关系和顺序。信息系统最怕的就是源头数据丢失——源头细节一旦丢后面任何分析都做不出来。我在一个传统批发企业改造项目里深有体会。原来系统只有一张“销售出库单”金额、客户、日期连库存批次都没有。业务想统计“哪个批次先过期”系统根本回答不了因为源头就没保存这个事实。按照REA重新梳理后每一笔销售事件都关联具体SKU和数量库存批次作为资源属性留存后续的各种分析全部从明细推导再也不用为临时需求补录历史数据。2.2 多对多关系不用再弯弯绕传统凭证模型下“一个订单对应多个商品”“一次收款覆盖多个订单”这类多对多关系很难处理。通常要加中间表但中间表挂在哪、由谁维护团队经常吵架。REA从一开始就用实体关系表达业务多对多是非常自然的事。销售事件和商品可以多对多销售事件和收款事件可以多对多只要拆出关联表就能表达。设计角度讲这样的数据结构很稳定新分析需求来的时候不需要动主表只需要在关系上做查询。2.3 报表和存储解耦月末结转不再是噩梦传统模型里为了输出利润表和资产负债表经常在数据库里存放大量汇总数据月末还有结转流程。REA不这么做它通过事件和资源流记录所有“事实”报表只是在这个事实集上执行查询聚合。这个特性在电商场景特别受用。原来的财务系统每月一次大结转期间所有业务数据都锁死不能改。改成REA结构后资金流、存货流实时可查不需要月末一次性结转。报表是算出来的不是提前存好的。2.4 REA与凭证模型的关键差异对比维度凭证模型REA模型中心概念凭证—科目—分录资源—事件—参与者存储内容借贷金额与科目业务事实及关系回答业务流程问题弱强报表方式直接汇总分录基于事实集查询聚合多对多支持难需要补充中间表天然支持适合场景纯财务核算系统业务财务一体化的业务系统这个表不是说要彻底抛弃凭证模型而是提醒你如果你的项目需要做业务过程分析、需要灵活扩展REA做底层更合理。凭证模型可以作为报表输出层的口径但不应作为唯一的存储结构。3. 实操用REA给小型电商平台设计订单履约与收款模型3.1 场景先定下来销售、发货与收款为了讲透实操我设计一个典型场景某小型电商平台经营图书类自营商品涉及顾客下单、仓库发货、顾客付款、财务对账。要求能统计每个顾客的历史订单与消费总额能追踪每笔订单的物流状态能算出“已确认销售但未收款”的金额。这里有一个重要的范围控制第一版只把“销售→发货→收款”作为核心经济过程跑通采购、付款、退货后续再加。很多项目失败是因为一开始就想把全公司业务塞进模型结果连参与者都数不清。REA建模讲究“先窄后宽”核心流程稳定后再扩展外部环节这样才能控制复杂度。3.2 识别实体哪些算资源、事件、参与者围绕上述场景我先列出所有候选实体类型实体为什么必须存在资源商品SKU销售时库存减少资源资金账户收款时余额增加事件销售事件商品流出、收入确认事件收款事件资金流入参与者顾客购买行为的发起人参与者销售客服内部操作人参与者仓管员发货操作人参与者出纳收款确认人这个清单比较简单。实际项目里还会出现“优惠券”“积分”“物流单号”这些对象我的经验是先判断它是否直接导致资源增减如果不是就放一边等主链路建模完成后再补充。比如优惠券不直接导致资金增减它只是影响销售单价可以在销售明细上记录折扣信息不必单独建模成资源。3.3 关系基数该一对多还是多对多逐条盘清实体识别完之后逐个确认关系基数顾客和销售事件一个顾客可以多次购买一次销售只属于一个顾客。这是1对N。销售事件和商品SKU一个订单可以包含多本图书一本图书可以被多次出售。这是N对N必须拆关联表。销售事件和收款事件一次销售可能分次收款一次收款也可以合并覆盖多个订单。严格按REA语义这也是N对N。收款事件和资金账户一次收款进入一个账户一个账户有多次收款记录。这是1对N。顾客和收款事件一个顾客多次付款一次付款由一个顾客发起。这是1对N。这个环节是建模的核心谈判点。尤其是“销售—收款”关系大多数初级设计师会想当然画成1对1因为财务单据里通常写着“一单一款”。但现实是电商平台经常有合并支付、预存款抵扣、分期付款如果你按1对1设计这些场景全部没法表达。3.4 落到数据库建表SQL与两个高频查询REA模型落到关系型数据库时实体表对应资源、事件、参与者关联表对应它们之间的关系。下面给出MySQL风格的结构示例注意销售事件表不存放总金额。CREATE TABLE customers ( customer_id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, credit_limit DECIMAL(12,2) DEFAULT 0 ); CREATE TABLE employees ( employee_id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, role VARCHAR(50) NOT NULL ); CREATE TABLE skus ( sku_id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, price DECIMAL(12,2) NOT NULL, stock_qty INT NOT NULL DEFAULT 0 ); CREATE TABLE sales ( sale_id INT PRIMARY KEY AUTO_INCREMENT, sale_date DATETIME NOT NULL, customer_id INT NOT NULL, employee_id INT NOT NULL, status VARCHAR(20) NOT NULL, FOREIGN KEY (customer_id) REFERENCES customers(customer_id), FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ); CREATE TABLE payments ( payment_id INT PRIMARY KEY AUTO_INCREMENT, payment_date DATETIME NOT NULL, account_id INT NOT NULL, amount DECIMAL(12,2) NOT NULL, cashier_id INT NOT NULL, FOREIGN KEY (cashier_id) REFERENCES employees(employee_id) ); CREATE TABLE sales_sku_items ( sale_id INT NOT NULL, sku_id INT NOT NULL, quantity INT NOT NULL, unit_price DECIMAL(12,2) NOT NULL, PRIMARY KEY (sale_id, sku_id), FOREIGN KEY (sale_id) REFERENCES sales(sale_id), FOREIGN KEY (sku_id) REFERENCES skus(sku_id) ); CREATE TABLE payment_sale_links ( payment_id INT NOT NULL, sale_id INT NOT NULL, allocated_amount DECIMAL(12,2) NOT NULL, PRIMARY KEY (payment_id, sale_id), FOREIGN KEY (payment_id) REFERENCES payments(payment_id), FOREIGN KEY (sale_id) REFERENCES sales(sale_id) );注意这里的payment_sale_links表它表达销售事件与收款事件的Duality关系。allocated_amount记录本次支付分摊到某笔销售上的金额。没有这张表你就只能靠“是否已支付”字段硬撑撑不了多久。两个高频查询示例第一个查某顾客的订单明细SELECT c.name AS customer_name, s.sale_id, s.sale_date, k.title AS book_title, si.quantity, si.unit_price, (si.quantity * si.unit_price) AS line_total FROM sales s JOIN customers c ON s.customer_id c.customer_id JOIN sales_sku_items si ON s.sale_id si.sale_id JOIN skus k ON si.sku_id k.sku_id WHERE c.customer_id 1024 ORDER BY s.sale_date DESC;第二个算“已销售未收款”余额。这个需求在传统表结构里往往要额外维护应收表而REA模型直接通过销售金额减已分配收款的差值推导SELECT c.name AS customer_name, SUM(si.quantity * si.unit_price) AS total_sold_amount, COALESCE(SUM(psl.allocated_amount), 0) AS total_paid_amount, SUM(si.quantity * si.unit_price) - COALESCE(SUM(psl.allocated_amount), 0) AS outstanding_amount FROM sales s JOIN customers c ON s.customer_id c.customer_id JOIN sales_sku_items si ON s.sale_id si.sale_id LEFT JOIN payment_sale_links psl ON s.sale_id psl.sale_id GROUP BY c.customer_id, c.name HAVING outstanding_amount 0;这个查询跑出来的结果其实就是财务口中的“应收账款账龄分析”的数据源。它不需要独立应收表随时可以算而且永远和销售、收款明细对得上。3.5 建模过程的三个取舍实操中会遇到三个典型取舍我直接给出自己的决策逻辑。第一订单实体要不要单独建我建议加一个orders表作为承诺/意图记录包含下单时间和状态与销售事件保持1对1。原因是电商业务在发货前就有购物车、订单状态、取消逻辑这些状态变化不产生经济后果放到销售事件里会污染资源流语义。订单是“业务意图”销售事件是“经济实现”两者分开反而清晰。第二发货算不算独立事件如果库存管理要求高发货就是销售事件的一部分因为发货那一刻库存才真正减少。有些公司把“创建销售单”和“实际出库”分开以便统计已下单未发货量。这没问题但出库动作才是经济事件创建销售单只是信息事件。不要把信息事件当成资源流的主事件。第三库存数量字段维护在SKU表里吗可以维护一个stock_qty做展示用但关键库存报表必须通过“采购流入—销售流出”推导。否则每次人工改库存、订单退款、报损都直接改字段过两个月数据一定对不上。REA的核心思想是“事实可推导展示可冗余”。4. 建模实操中反复踩到的坑与排查技巧4.1 事件还是活动判断一段操作是否值得建模关于“事件”和“活动”的区别业务方经常和我争论。顾客浏览商品算不算事件肯定不算因为没有资源增减。商品上架算不算不算虽然定了价格但不直接改变资源数量。那么仓库内部移库呢资源总量不变严格说也不算经济事件如果业务需要追溯库位应单独记录仓位变动不放在经济事件层。我的判断标准很简单是否改变了资源数量或价值。财务关心的动作算经济事件业务和仓库关心的操作可以用“操作记录”补充避免影响REA核心结构。这个规则拿到评审会上基本能一次达成共识。4.2 订单、销售、收款三角关系最容易翻车很多初学者会把订单表直接当销售表然后在订单表里加“是否已支付”字段。看着省事实则会带来三个问题一次收款覆盖多张订单时支付金额挂在哪订单部分发货怎么表示跨订单退款怎么冲销正确做法是销售事件表和收款事件表独立用payment_sale_links作互换关系记录每次分摊金额。我在项目里为了让产品经理理解会画一个具体例子一个顾客用一笔付款结算了三个订单问他在订单表上怎么记录。画出来之后大家自然认同独立设计。经验业务方坚持“一单一款”时不要急着反驳。先问他现有的支付方式里有没有合并支付、预付抵扣如果有就必须拆表。4.3 应收应付别急着建表先想清楚怎么推导这是REA建模中最容易引起财务同事困惑的地方。传统会计体系里应收账款是一个科目也是一张表。但REA模型里应收账款不是一个源头实体而是“销售事件与收款事件之间的时间差”。也就是说只要销售和收款之间有时间差就自然产生应收余额。我们要做的不是建立一张新的应收流水表而是通过销售金额减去已分配收款金额推导出应收视图。我的一次实操经历很典型某公司财务要求独立应收表说“对账方便”。我坚持先不建先按REA口径把对账报表做了出来。结果月底对账时用推导方式生成的应收余额和银行流水自动匹配不再需要两张表来回核差异。财务同事后来主动取消了一张手工维护的Excel应收台账。核心逻辑是应收是一种结果不是一种事实。把结果当成源头数据维护就一定会出现两边对不上的问题。4.4 五条自检清单评审模型时逐条过我在每次REA模型评审会结束前都会拿出这份清单逐条过一遍。每个经济事件是否至少连接一个内部参与者、一个外部参与者如果没有外部参与者说明这个事件可能不是真正的交换事件。每个经济事件是否至少连接一个资源如果没连接资源它可能是活动不是事件。每个资源流出事件是否找到了资源流入事件配对销售要对应收款采购要对应付款找不到配对模型是残缺的。是否存在把金额汇总放在主表的情况如果有改成从明细事件推导。销售总金额不是字段而是明细累加。订单、物流等非经济事件是否被误当成经济事件它们可以保留为业务记录但不能直接参与资源流。这五条不是理论教条每一条背后都是真金白银的返工教训。尤其是第四条几乎每个从传统财务系统切换过来的团队都会踩因为大家习惯了“订单总金额”这种冗余字段。但在REA结构中冗余字段一旦和明细不一致对账就是灾难。把REA用顺之后我最大的变化是跟业务方开会时不再讨论字段怎么放而是讨论业务过程里哪里真正产生价值。REA模型不是万能的它要求你在开始阶段多花点时间想清楚资源会不会动、事件有没有经济后果、参与者是谁但这份前置思考会在后面省下大量返工。最后再分享一个小技巧每次评审模型手里拿三个问题反复盘问——这个事件动了哪项资源这个资源和资金有没有对应关系谁对这个事件负责三个问题问完90%的建模分歧当场就能消失。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进