资讯动态

REA模型重构进销存系统:从表单驱动到事件驱动建模实战

发布时间:2026/10/13 23:55:14 来源:尧图企业网站定制
接手某配件公司的进销存系统重构时最让我头疼的不是并发扣库存也不是报表出数慢而是“业务规则一变数据库结构就要跟着动”。三个月内业务先后要求把采购溢短装按合理误差处理、把销售包装费和运费拆开结算、让成本凭证自动按单核算每一次变更都是表结构、存储过程、报表联动的三次开发。后来我把这套系统的核心数据模型用REAResource-Event-Agent资源-事件-代理模型重新梳理了一遍把建模的出发点从“表单长什么样”改成了“业务到底发生了什么”。这篇文章就是这次重构的完整复盘REA三要素怎么理解、怎么从一个真实的销售业务落地成表、和传统建模思路相比到底差在哪、以及真实项目里最容易踩的坑。如果你正在设计进销存、ERP、财务核算相关的数据模型或者觉得现在的表结构改来改去、报表口径一团乱麻这篇文章应该能给你一个全新的建模视角。1. 传统建模思路为什么总在“业务变化”面前翻车1.1 表单视角掩盖了业务事实大多数项目冷启动时数据模型都是照着业务系统的上一个版本或者纸质表单去设计的。采购单有几栏就建采购主表和采购明细表销售发票有表头有明细就建发票主表和发票行项目表。这种“表单视角”最大的优点是直观业务人员打开界面说“这就是我们的销售单”开发照着原样建表很快就能跑通流程。但问题在于表单是操作的入口不是业务的本质。表单记录的是“录入完的结果”而不是“这个业务动作意味着什么”。还是拿销售场景说话传统的订单表里通常会有一个总金额字段里面混着商品原价、折扣、包装费、运费、税费。当业务方问“这个月折扣促销一共让了多少利”时系统只能把总金额近似还原或者靠额外的打折记录表去推导根本无法确保口径一致。因为原始模型里根本没有任何一个对象在描述“折扣”这件事实描述的是订单汇总结果。我见过不少系统后期的补丁都是这样堆出来的为了回答“折扣让利多少”先建一张折扣表为了回答“退款原因”再建一张退款备注表为了回答“部门业绩”又要给订单明细加部门字段。每一个新问题都在旧模型上补一个新对象。本质上是因为建模起点没有放在业务事实上。1.2 反直觉的结论科目表不是核心模型的一部分REA最先挑战的是财会人员的直觉会计科目表、科目余额、凭证分录这些在REA看来都是派生数据而不是核心模型。真正的主数据是“资源”“事件”“代理”三类对象再加上它们之间的流量关系。这个观点的实际价值在于业务口径是极易变化的。同样是“销售收入”管理层要看含税口径财务要看不含税口径渠道部门要看净收入口径同样是“库存”仓库看的是实物数量财务要的是标准成本金额采购要看在途量。如果建模时把这些口径当成主数据来建任何一个口径调整都会引发全链路修改。但如果模型里存储的是“某天某仓库销售出库了3个配件单价100元”这一业务事实那么无论是含税口径还是不含税口径都只是对这个事实做不同的统计投影而已不需要改动核心数据模型。这也是我后来坚持用REA审视建模工作的原因它不是让你多建几张表那么简单而是要求你先想清楚——系统里哪些数据是“发生的客观事实”哪些数据只是“对事实的一种看法”。2. REA三要素拆解先分清资源、事件、代理再动手画表2.1 三要素各是什么REA模型把企业的经济行为抽象成三类对象资源Resource一切有价值、可以被企业获取或消耗的客体。典型的如现金、商品、应收账款、库存额度、积分余额。事件Event导致资源增减变化的业务动作是整个模型的“动点”。典型的如销售、收款、采购到货、退货、报废、调拨。代理Agent参与事件、在经济行为中承担角色的人或组织。内部代理指员工、部门外部代理指客户、供应商、承运商、监管机构等。用销售打个比方商品从公司流向客户公司收获现金或应收账款。这里商品和现金/应收账款是资源销售与收款是事件公司内部人员与客户是代理。模型里真正会“动”的是被事件连接起来的资源代理负责说明“是谁发起了这样的事件”。2.2 为什么每个事件通常成对出现“双鞋印”REA里有个容易被忽略的细节一个典型的经济事件往往同时涉及一个资源流入和一个资源流出。比如销售出库事件对外是商品资源的流出对客户而言则是“客户负债”或“应收账款”这类资源的流入。这个问题在传统表结构里被简单化为“库存减少、应收增加”两条更新语句但在模型层面其实代表两个资源通过同一事件被联结了。因此REA建议在事件与资源之间用带有方向增量/减量的联结来建模而不是简单地在某一张表上扣库存、累加应收。这样设计最大的好处是任何一笔交易你都能从事件出发顺着这些联结看到它到底影响了哪些资源、各自增减了多少。审计追踪和口径核对会变得非常直接。举一个我实际踩过的例子以前应收账款余额是靠“应收单”和“收款单”两张表手工对平的一旦某一笔收款被拆成两次或者客户用预付款冲抵对账就开始费劲。换成REA之后应收款变化被建模成现金资源与应收资源之间的流量联结每一笔销售出库和每笔收款都在stockflow里成对出现余额自动闭合。2.3 资源与代理模糊时用什么标准判断新建模型时团队经常为一个对象该算资源还是代理争论不休。比如客户、供应商、门店到底是资源还是代理我常用的判断标准只有一个它会不会因为经济业务而被企业获取或消耗如果会那它就是资源如果它只是参与事件、自身并不会因为交易进入或离开企业那它就是代理。按这个标准客户显然是代理——成交一百笔交易客户数量不会因此进入公司资产负债表的任何科目。门店要分情况看如果门店是“销售渠道”门店自身的排名、等级、合同关系属于代理属性但门店的货品库存、押金、应收属于资源。分清之后很多“一张表搞不定”的问题就自然消失了——你不再试图把代理信息和资源信息塞进同一个实体里。2.4 事件链多个事件如何组成一笔完整经济业务的闭环在REA里很少有孤立的事件。一笔完整的销售通常是“销售出库事件”和“收款事件”两个事件先后发生中间通过资源的变化应收账款从增加到清零串联起来。这种事件与事件之间通过资源流量形成的链路我习惯叫它“事件链”。事件链对建模很有帮助。你在设计事件表时不需要预知未来所有可能发生的事件类型只需要让每个事件都遵守同一个规则它可以引用之前的某个事件作为来源source_event_id并且通过资源流量把前后事件咬合起来。这样一来哪怕以后业务增加了“分期收款”这种新玩法也只需要在原有销售出库事件后面追加多期收款事件不需要改动销售事件的表结构。3. 完整建模过程一个小型配件销售业务的REA落地3.1 第一步从业务用例清单反推出事件清单模拟项目X是一家做工业配件分销的公司业务不算复杂但牵扯到进销存、应收应付和成本核算。我第一步不是画ER图而是把所有业务动作列出来询价、下采购订单、供应商发货、质检、入库、下销售订单、拣货、出库、开票、确认收入、客户退货、质检、退款、坏账计提、月末成本结转。然后过滤出一个标准只有导致资源增减变化的动作才叫事件。按这个标准“下采购订单”不是事件因为它不改变任何资源——它只是未来事件的意向表达“质检”也不是事件质检通过与否会影响“合格品资源”和“不合格品资源”的拆分但质检动作本身并不直接产生增加或减少真正的事件是“到货入库”存货资源增加“销售出库”存货资源减少、应收资源增加“收款”现金增加、应收减少“退货入库”存货增加、应收减少。这个看似文字游戏的步骤是REA建模最重要的一环。事件清单一旦出现遗漏或混淆后面所有联结都会错。我通常会邀请业务方一起过清单每提一个动作就问一句“这件事发生后公司手里的什么东西变了”答不上来说明它多半不是事件。3.2 第二步核心表结构怎么落REA落地不需要花哨的框架四张主表加两张联结表就能起步。模拟项目X里我实际建的表结构是这样的CREATE TABLE resource ( resource_id INT PRIMARY KEY, resource_type VARCHAR(32) NOT NULL, -- CASH / INVENTORY / RECEIVABLE / POINT sku VARCHAR(64), name VARCHAR(128), unit VARCHAR(16), current_balance DECIMAL(18,4) DEFAULT 0 -- 冗余余额由程序维护 ); CREATE TABLE event ( event_id INT PRIMARY KEY, event_type VARCHAR(32) NOT NULL, -- SALE_OUT / COLLECTION / PURCHASE_IN / RETURN_IN occurred_at TIMESTAMP NOT NULL, -- 事件发生时间不可变 source_event_id INT, -- 来源事件用于追溯 note VARCHAR(256) ); CREATE TABLE agent ( agent_id INT PRIMARY KEY, agent_type VARCHAR(32) NOT NULL, -- EMPLOYEE / CUSTOMER / SUPPLIER name VARCHAR(128), contact VARCHAR(128) ); CREATE TABLE stockflow ( flow_id INT PRIMARY KEY, event_id INT NOT NULL, resource_id INT NOT NULL, direction CHAR(3) NOT NULL, -- IN / OUT quantity DECIMAL(18,4) NOT NULL, unit_amount DECIMAL(18,4), amount DECIMAL(18,4) ); CREATE TABLE participation ( participation_id INT PRIMARY KEY, event_id INT NOT NULL, agent_id INT NOT NULL, role VARCHAR(32) NOT NULL, -- 业务员 / 仓管 / 客户 / 供应商 valid_from DATE, valid_to DATE );这里有个关键取舍我没有为每一种事件类型各建一张事件表而是统一放进event表用event_type区分同时用各自的扩展表存储类型专属属性。这样新增一种事件类型就只是insert一个扩展表和若干stockflow不用动核心表结构。3.3 第三步拿一笔真实业务走查验证模型设计完模型后我会挑几笔全流程业务在纸面上走一遍确保每一环节的stockflow和participation都能闭合。以“客户下单购买3个A配件单价100元约定9折、先发货后收款”为例销售出库事件E1在stockflow里插入两条记录。一条directionout、resource_idA配件、quantity3、unit_amount90另一条directionin、resource_id应收账款、amount270。participation记录客户、业务员、仓管三个代理。收款事件E2客户全额打款270元。stockflow里插入directionout、resource_id应收账款、amount270同时directionin、resource_id现金、amount270。participation记录客户与财务。月末成本结转不写死在事件表里而是由派生层用移动加权平均成本算出销售出库的存货成本生成结转凭证并在事件表里记录“该凭证来自E1事件”。走查过程中最容易发现的模型错误就是事件或联结的语义不一致。比如把“开票”当作单独事件还是当作销售确认的延伸动作在这个例子里开票不改变资源——它只是把“应收确认”这个事实在形式上登记为发票所以我没有为开票单建事件。把账单纯当表单看的团队往往这里分不清。3.4 第四步派生视图与报表口径怎么接模型层建好后报表员习惯问的第一句话永远是“我要的余额从哪张表查”。REA的标准答案是“余额不是一张表是一句聚合查询。”比如当前库存余额可以在视图里按resource_id过滤把stockflow表里所有directionin的quantity之和减掉directionout的quantity之和。这部分我建议用两个手段落地一是公共聚合视图把最常用的余额、发生额口径固化成一到两张视图二是物化宽表对性能敏感的报表例如月度销售明细、客户余额表单独建物化表靠调度任务刷新。这里要交代清楚派生层的刷新策略。我在模拟项目X里是这样定的报表对象刷新方式刷新频率数据来源日销售统计物化宽表 定时任务每小时event stockflow库存余额视图实时计算准实时stockflow 聚合月末成本结转存储过程重建每日stockflow resource 成本字段另外报表层在查询时千万不要直接对stockflow做全表SUM数据量上来后会拖垮主库。先把派生表设计好再放报表查询。4. 和传统模型硬碰硬六个维度看清差距4.1 同一业务两种模型的直接对比我特意用模拟项目X里同一个销售业务对比传统表单驱动建模和REA建模。这张表可以贴在你评审PPT的第三页对比维度传统表单驱动建模REA 模型建模起点表单/报表字段业务事实业务变化应对表结构、存储过程、报表联动修改核心模型稳定扩展表和视图变化报表可得性直接查业务表直观需聚合视图初期成本高审计完整性依赖单据外键串联容易断链沿事件即可还原全链路团队心智成本较低较高需业务、财务、开发共同理解性能优化主数据即汇总容易成为瓶颈派生层独立设计可控从这张表能看出REA不是“免费的午餐”。它用报表初期开发复杂度换来了更高的业务适应性和审计可信度。关键看项目的业务演进空间如果需求大概率还会变REA就是你给未来买的保险。4.2 “满二赠一”促销需求的推演假设几个月后业务要求加一个“满2件9折、满3件赠1件”的组合促销另外还想知道每个SKU在促销中实际让利多少。传统做法给订单明细表加promo_type、promo_value字段再写两个存储过程去分摊折扣。由于每个SKU的原始单位价格记录缺失或不够干净分摊逻辑会牺牲准确性报表还要再补一张促销汇总表。体感上至少三天开发加两天联调。REA的做法模型里已经存在“销售出库事件”和“商品资源流量”折扣金额完全可以作为那次销售事件的一部分被精确记录。具体来说在扩展表里增加促销规则字段再在stockflow里增加一条directionout的“让利流量”或者用一个专门的特价资源行来表示让利。每个SKU实际让利多少沿着对应事件的stockflow按SKU聚合即可得到。核心模型表没有任何改动变化的只是扩展表和一个统计视图。我后来在复盘里专门标了一笔账传统模式改这个需求从评审到上线至少要一周REA模型下两个开发小时就完成了。这不是吹因为模型层已经把“销售事实”和“统计口径”分离了改的永远只有口径那一层。4.3 什么时候真的不适合用REA诚实地讲REA不是银弹。如果你的业务就是一张采购单加一张销售单没有复杂的促销、折扣、多仓、多币种、成本核算需求表单驱动建模更快且更易维护。REA的前期建模要求高而且团队里得有人能持续解释“为什么事件表里没有状态字段”“科目余额从哪来”不然落地过程会很痛苦。另外如果你手上的系统是一个已经上线多年、数据结构固化的存量系统没有重构计划那么直接引入REA反而会干扰现有团队。REA最好的切入时机是新建系统或者大版本重构时。我通常给团队的建议是小项目用传统建模中大型项目且业务演进空间大才值得为REA付出成本。5. 落地REA时容易踩的三个坑我的实战教训5.1 坑一把所有流程状态硬塞进事件表第一次按REA设计时我在event表里放了status字段待出库/已出库/已红冲/已作废直接引发了两个问题一是状态机和业务事实被混在一起销售出库事件一旦被手工改状态审计线就断了二是状态变化频繁导致事件表被反复UPDATE而REA的核心原则是事件一旦发生就不可变。之后我把流程状态移到了独立的process_instance表事件表只保留occurred_at、event_type、source_event_id这些不可变字段整个模型干净了很多。5.2 坑二过度洁癖不敢建任何派生表REA强调模型层不要放派生数据但如果什么都不建报表性能会非常难看。我的处理策略是分层模型层保留事件和资源流量派生层设计一批物化视图或宽表专门服务报表和接口。关键是给这些派生表定义清晰的刷新规则比如销售日报每小时刷新一次、月末成本表每日定时重算并留出手工触发全量重算的后门。派生表也不是想建就能建必须能追溯到源事件的stockflow否则一经差错数据就无法自证。具体到财务场景我还会在结转凭证表上记录一个来源事件ID这会避免月末对账时扯不清“这凭证到底对应哪笔销售”。5.3 坑三代理身份不做区分统计时当场爆炸有次上线小版本后业务反馈“销售业绩按人统计”时数字不对。沿着participation表查下去发现同一个客户在某笔订单里既被记录为“客户”又被部分业务员记录为“内部代理”。根源是participation表里没有为参与角色设role字段有人把外部代理填进了内部字段。后来我在participation表里明确区分participation_roleinternal_agent/external_agent并为同一代理的多角色设置了有效周期。代理是多身份的不把身份建模好统计类需求后期会不断出问题。这三个坑踩完后我对REA的态度是它不是一套非要全盘照搬的模板而是一种极其有效的审视方式——当你被业务方提出“改个需求”时用它问自己一句“这是业务事实变了还是对事实的口径变了”如果是前者说明模型设计有漏洞如果是后者说明派生层需要调整核心模型不用动。绝大多数情况下答案都是后者。所以我的最终落地方案是以REA为建模核心框架辅以必要的派生表和状态机既保证模型面向业务变化有韧性又不让团队陷入过度抽象的困境。现在业务再提“以旧换新”这类结构调整对我来说也只是新增一个回收事件关联到现有销售事件的stockflow而已而不是异常厚重的评审流程。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑