资讯详情

从REA模型到进销存实战:用资源、事件、参与者重构业务数据

📅 2026/10/11 9:18:17 | 华诺云谱 👁 阅读
从REA模型到进销存实战:用资源、事件、参与者重构业务数据
做业务系统做到第三年的时候我开始受不了“流水账”式的数据建模了。不管进销存、订单系统、还是内部ERP团队讨论最多的问题永远是“这个数是对的吗”而不是“这个数是怎么来的”。后来接手的一套系统的架构文档里核心模型写着三个字母REA。查完资料我才明白这是 Resources、Events、Agents 的缩写也就是资源、事件、参与者。这套模型最早来自会计信息系统领域但放到今天做交易型系统、库存核算、业务中台数据模型依然非常能打。这篇文章我会用一套最常见的“采购-销售-收款”业务把 REA 从概念一步步拆到表结构、查询逻辑和排错经验。适合正在设计业务数据库、被“对不上账”折磨的开发者也适合需要梳理业务流程、却不知道从哪里下手的同学。读完你至少能回答三个问题资源、事件、参与者分别是什么为什么事件表只追加不更新怎么用事件来推算库存和利润。1. 先认识 REA资源、事件、参与者1.1 资源业务中流转的“东西”资源从字面上理解就是业务活动里被生产、被消耗、被转移、被交换的对象。它分两类一类看得见摸得着商品库存、现金、固定资产另一类是虚拟的服务工时、优惠券额度、积分。判断一个对象算不算 REA 里的资源有一个很重要的标准它必须能被某个事件影响。如果永远不会被任何动作增加或减少那它就不算资源只能算静态主数据。举个例子商品列表是静态主数据商品库存才是资源。因为“库存”会被采购入库增加、被销售出库减少它是一个动态对象。这就像存钱罐存钱罐本身是主数据里面的硬币总数才是资源。建模的时候一定要把“是什么”和“有多少”拆成两个概念REA 资源指的是后者。很多团队错就错在把商品信息表加了一个“库存数量”字段结果每次卖货都要 update 商品表业务一复杂就开始出错。1.2 事件业务真正发生的“动作”事件是 REA 模型的核心它表示业务中确实发生的一件事。“确实发生”这四个字很关键REA 只记录已经完成的事实不记录计划或意图。比如“客户下单”本身不是事件因为下单只代表意愿“客户付款”才是事件因为钱真的到了“仓库发货”才是事件因为货真的出库了。这个区别在订单系统里特别容易绕进去。现实做法是订单建一张表状态字段从待支付到已完成改来改去但 REA 的思路完全不同订单只是一个聚合框架真正驱动资源变动的不是订单本身而是订单背后的付款、发货、签收这些实际动作。每个事件至少影响一个资源并且涉及至少两个参与者。如果一个动作既不影响资源、也没有参与双方那它压根不该出现在业务数据模型里。用这条规则去筛能砍掉大量无效表。1.3 参与者谁参与了这件事参与者就是人或者说可以被当成人来对待的组织机构。REA 把参与者分成内部和外部内部参与者是本公司的员工比如销售员、仓管外部参与者是客户、供应商、物流方。为什么要刻意区分内外因为后面所有对账、审计、权限逻辑都要靠这个标签。如果不区分你根本解释不了为什么采购入库是增加库存、销售出库是减少库存反正都是“出入库”时间一长报表就乱了。这里还要注意参与者不等于用户表。同一个客户如果既做销售又做售后在参与者维度最好保留成一条记录在不同事件里承担不同角色。角色信息放在关联表里而不是改参与者本身。三元结构到这里就成型了资源是“被动的对象”事件是“主动的变化”参与者是“动作的发起者和接受者”。整个业务活动可以压缩成一句话参与者通过发起事件改变了资源的数量和归属。为了好懂拿诊所打比方。药品库存是资源护士发药是事件护士是内部参与者病人和医药公司是外部参与者。药柜里药少了不是一个 update 把数字改小而是新增了一条“发药事件”。两者结果一样但可追溯性天差地别。2. 为什么我放弃“流水账”式建模转向 REA2.1 流水账模型的两个“死穴”大多数团队一开始做进销存都是建一张流水表字段包括商品名、数量、方向、日期、备注。看着简单跑上三个月问题全出来了。第一个死穴口径依赖业务解释。同一张流水表财务要看含税金额运营要看实收金额仓管要看净变化数量。每行流水到底表达什么动作数据库里根本不体现全靠代码里的 if/else 和业务文档来猜。时间一长三个部门导出来的报表数字对不上锅全甩给技术。第二个死穴改数不留痕。订单状态从已支付改成已退款最简单的方式是直接更新订单状态字段后台再顺手把金额改掉。操作一时爽审计火葬场。半年以后有人问“当时为什么显示已支付”你拿不出任何可证明的痕迹。流水账模型把“记录事实”和“更新状态”混在一起事实被覆盖后系统和失忆没有区别。2.2 REA 为什么更适合交易型系统现代业务系统本质上都在处理交易交易的特点是环环相扣、有来有回。REA 恰好就是为描述交易而生的。模拟项目X是个小型电商进销存系统开始用订单加库存字段的方式设计库存表只存“当前数量”卖出减一、采购加一。上线三个月后报表期末库存和仓库实际盘点差了数十件怎么都查不出原因。问题就在于没有记录“哪些事件导致库存减少”只知道结果对不上却不知道错在哪一步。后来改成 REA把出库事件、入库事件单独记录每笔事件关联商品和参与者当前库存是事件累计算出来的每一件商品都能追溯到哪笔销售、哪笔退货对不上账的问题才算根治。这个案例的核心不是换了一套表而是换了一种提问方式。原来问“现在库存是多少”拿到的是快照会过期现在问“哪些事件发生过累计效果是多少”拿到的是可重放的事实永远能算清楚。REA 在交易型系统里最有价值的点也就在这里它不存结果它存过程。2.3 先用场景判断该不该上 REAREA 不是银弹它主要解决“业务事实如何可靠记录”的问题。如果业务核心是内容发布、社交关系REA 完全用不上如果核心是审批流、状态机流转流程引擎比 REA 更合适如果团队连基本的数据建模经验都没有一上来就搞六张表、三张关联表也可能因为复杂度失控。我的判断标准很简单系统里如果频繁出现“库存”“余额”“数量”“金额”这些随时间变化的值并且大家经常为了这些值对不上而吵架那 REA 大概率能帮上忙。进销存、ERP、财务核算、订单履约、积分系统都在这个范围内。拿 REA 和传统流水账做个直观对比对比维度流水账模型REA 模型数据组织以“一条变动记录”为中心以“事件类型关联关系”为中心余额处理直接保存当前余额只用事件累计计算余额历史追溯状态可能被覆盖事件只追加不更新口径一致性依赖代码解释由关联关系天然固定审计友好度弱强3. 实操用 REA 建模一套“采购-销售-收款”业务3.1 场景定义与建模前准备某公司要上一套内部进销存系统业务不复杂从供应商采购商品验收入库把商品销售给客户发货出库客户打款公司付款给供应商偶尔发生退货退款。如果按老思路要建采购单表、销售单表、付款单表、收款单表、库存表、客户表、供应商表八张表起步每张表都存一遍金额、数量、时间、对象表之间再加各种状态字段。系统能用但报表难写对账更难。用 REA 建模流程压缩成四步识别资源、识别事件、识别参与者、建立关系。3.2 步骤一识别资源、事件、参与者先把这张业务图景里的资源拎出来。核心资源有两类商品和现金。商品被采购入库时库存增加被销售出库时库存减少现金在采购付款时减少在销售收款时增加。有人会问应收账款算不算资源在 REA 体系里不算。应收账款是“销售事件已发生但收款事件还没发生”的状态差通过两个事件的关联关系就能查出来不应该单独建一张表去存。这个判断能帮你砍掉一堆应收应付表财务逻辑反而更清晰。接下来是事件。这个场景里只有三个核心事件采购入库、销售出库、收付款。采购入库影响商品资源增加也影响现金资源减少因为要付款给供应商销售出库影响商品资源减少也影响现金资源增加因为要收客户的钱。看得出来了事件和资源是多对多关系一个事件可能影响多个资源这也是为什么不能把数量金额直接塞进事件表里。最后是参与者。内部参与者是公司员工比如采购员、销售员、仓管外部参与者是供应商和客户。表格设计上不需要太复杂重点是给每个人打上“内部/外部”的标签。3.3 步骤二设计六张核心表直接给出一套可以参考的表结构生产环境可以在这个基础上扩展字段。资源表维护业务里的所有资源对象CREATE TABLE resource ( resource_id BIGINT PRIMARY KEY, resource_name VARCHAR(64) NOT NULL, resource_type VARCHAR(16) NOT NULL, -- 商品/现金/服务 unit VARCHAR(16) -- 单位件/元/小时 );参与者表内部员工和外部客户供应商都放这里CREATE TABLE agent ( agent_id BIGINT PRIMARY KEY, agent_name VARCHAR(64) NOT NULL, agent_type VARCHAR(16) NOT NULL -- 内部/外部 );事件表这是核心中的核心。它本身不直接存数量金额只记录“发生了什么、什么时候发生的”CREATE TABLE event ( event_id BIGINT PRIMARY KEY, event_no VARCHAR(32) NOT NULL, -- 业务单据号 event_type VARCHAR(16) NOT NULL, -- 采购入库/销售出库/收款/付款/退货 event_time TIMESTAMP NOT NULL );资源-事件关系表每个事件对不同资源的影响数量和方向都放这里一个事件可以关联多条资源记录CREATE TABLE resource_event ( rel_id BIGINT PRIMARY KEY, event_id BIGINT NOT NULL, resource_id BIGINT NOT NULL, quantity DECIMAL(18,2) NOT NULL, -- 入库为正出库为负 unit_value DECIMAL(18,2) -- 当时单价 );事件-事件关系表用于描述事件之间的因果比如销售出库事件关联到对应的收款事件CREATE TABLE event_event ( rel_id BIGINT PRIMARY KEY, from_event_id BIGINT NOT NULL, to_event_id BIGINT NOT NULL, relation_type VARCHAR(16) NOT NULL -- 销售-收款/采购-付款 );事件-参与者关系表记录每个事件由谁执行、针对谁CREATE TABLE event_agent ( rel_id BIGINT PRIMARY KEY, event_id BIGINT NOT NULL, agent_id BIGINT NOT NULL, agent_role VARCHAR(32) NOT NULL -- 采购员/供应商/销售员/客户 );设计完成后你会发现一个规律所有业务数字都落在 resource_event 表里所有事件因果都落在 event_event 表里所有责任人都落在 event_agent 表里。业务语义非常干净每个表只回答一个问题。有人肯定要问为什么就不能在产品表里加个库存字段答案很简单库存字段是结果不是事实。结果会过期事实不会。REA 始终把事实放在第一位结果必须从事实推导出来而不是提前存一个随时可能被改掉的数字。3.4 步骤三从事件还原库存与利润表结构定了怎么算库存一句话把资源-事件关联表按方向累加。当前库存等于所有入库数量之和减去所有出库数量之和。再加上事件时间的过滤条件就能拿到任意历史时点的库存。这个逻辑用 SQL 写出来很直观SELECT r.resource_name, SUM(CASE WHEN re.quantity 0 THEN re.quantity ELSE 0 END) AS total_in, SUM(CASE WHEN re.quantity 0 THEN -re.quantity ELSE 0 END) AS total_out, SUM(re.quantity) AS current_stock FROM resource r JOIN resource_event re ON r.resource_id re.resource_id WHERE r.resource_type 商品 GROUP BY r.resource_name;利润的计算同理。销售收入是销售出库事件里商品资源的金额之和销售成本要通过入库事件里的 unit_value 按移动加权平均或先进先出推算。这里有个经验不要在事件表里直接存“成本”字段成本是入库那一刻的数量单价决定的和出库事件本身无关。把单位成本和数量分开存后面用哪种成本核算方法都不会被卡住。4. 从 REA 模型到真实报表三类核心查询思路4.1 历史时点库存给查询加上事件时间条件除了实时库存业务上更常问的是“上个月月底还有多少货”。这个查询一点也不复杂核心就是别只 group by 商品把事件时间过滤条件加进去SELECT r.resource_name, SUM(re.quantity) AS stock_at_time FROM resource r JOIN resource_event re ON r.resource_id re.resource_id JOIN event e ON re.event_id e.event_id WHERE r.resource_type 商品 AND e.event_time 2024-10-31 23:59:59 GROUP BY r.resource_name;这段 SQL 的价值在于它不需要一张“历史库存快照表”只要人事后补录一张早前的入库单历史库存会因为事件重放而自动修正。快照做不到这一点快照只能记录当时已经发生的事情之后补录的数据永远只存在于当前表里。很多对账问题本质上是“补录时点”和“统计时点”错位造成的用事件表就不会有这个偏差。4.2 收入与毛利区分“入账”和“出库”毛利报表是另一个高频需求。很多系统的做法是直接拿订单金额减采购金额看起来很合理但实际上忽略了时间差。销售出库事件记录的是商品离库收款事件记录的才是现金到账两者往往不是同一时刻。用 REA 建模之后收入应该从销售出库事件的资源-事件关联表里取成本也按同一批商品的入库成本来算SELECT e.event_no, SUM(CASE WHEN re.resource_id 2 THEN re.quantity * re.unit_value ELSE 0 END) AS revenue, SUM(CASE WHEN re.resource_id 1 THEN -re.quantity * re.unit_value ELSE 0 END) AS cost FROM event e JOIN resource_event re ON e.event_id re.event_id WHERE e.event_type 销售出库 GROUP BY e.event_no;这里假设资源 ID 为 2 的是现金、1 的是商品。实际上这只是一个示例生产环境我会把成本计算逻辑封装成独立函数避免在报表 SQL 里写死资源 ID。成本算法可以试先进先出也可以试移动加权平均但无论用哪种数据来源都是入库事件的单价与数量不会因为销售单上填了一个成本价就产生两张“真相”。4.3 一致性校验三道防线保证数据可信数据模型设计的再好也要有校验机制兜底。我习惯上线前写三个固定校验脚本跑在每天晚上对账任务里。第一道事件完整性校验。每个销售出库事件要么关联一个收款事件要么在业务规则上明确允许挂账。简单校验方式对比销售出库事件数量和对应收款事件数量差异就是未收款的单据。这个差异本身不可怕可怕的是完全没有核对机制坏账到期才发现。第二道资源守恒校验。期初库存加上本期总入库减去本期总出库应该等于期末实际盘点数量。如果不等要么盘点录入有误要么某个事件的方向或数量写错了。REA 的优势在于任何一个事件错了你都能顺着时间轴定位到具体哪一条记录而不是背着几十件库存差来回翻 Excel。第三道金额守恒校验。所有现金资源相关事件的净额应该与银行或第三方平台账户的对账单净流入一致。这个校验对电商和线下零售都适用对不上就说明有收付款事件漏记了。三道校验跑下来数据可信度能提升一大截运维心里也踏实。5. 常见建模错误与排错实录5.1 把“单据”当成“事件”这是新手最容易踩的坑。订单不是事件订单是一个容器发票也不是事件发票是凭证。正确的做法是拆成下单、支付、发货、签收、退货等多个独立事件用 event_event 表把它们关联起来。我在模拟项目X里见过最多的返工都是因为开发把 orders 表当成了事件表结果每个订单既要做库存扣减又要做财务入账逻辑全挤在一张表里改一个字段牵连一堆代码。出问题后怎么排查看事件表的粒度。如果一条事件记录包含“数量”“金额”“状态”三个字段而且状态会被 update 修改那大概率还在用单据思维写模型。真正的 REA 事件应该只有事实没有状态。状态是通过关联事件的数量差算出来的。5.2 资源增减方向出错入库为正、出库为负这个方向看似简单实际非常容易写反。尤其是在做“退货”的时候很多人会直接把退货数量的正负号按业务习惯处理导致库存越退越多。我建议所有资源-事件关联表统一约定凡是导致资源数量增加的方向都是正数凡是导致资源减少的方向都是负数。退货对库存来说是增加所以退货入库是正数对现金来说可能是减少所以退款是负数。如果已经发生方向错误检查的方式很简单列出一段时间内所有事件和它们对某资源数量的影响按时间顺序人工核对一遍。方向写反的事件会像“倒刺”一样让累计值朝错误方向越偏越大。校验脚本里加一条规则就行同一资源在同一个事件里所有资源变动的方向必须一致不允许既有正数又有负数。5.3 金额精度问题数量、金额字段一律用 DECIMAL不要用 FLOAT。这条老生常谈但每次都会有人踩。浮点数在累计求和时会产生无法解释的误差库存数量差零点几还好金额差一毛两毛之后财务那边天天报错。DECIMAL(18,2) 已经覆盖绝大多数业务场景如果有更强精度要求可以用 DECIMAL(20,4)。出库单价、税额这些字段也要定义好位数避免报表里出现 0.30000000000000004 这种数。排错技巧把数据库连接层的实数映射方式统一成 BigDecimal 或等效类型不要为了省事直接用原生浮点对象。经验之谈这种坑修一次要耗两三天早早在规范层堵住后面省心很多。5.4 事后退货与修正退货永远不要更新原销售事件而是新增一个退货事件方向与销售相反再通过 event_event 关联回原销售事件。这样才能保证原始销售数据不被篡改。如果业务上必须“修正”某笔数据比如填错了时间我的建议也是新增修正事件而不是 update 原事件。事件表一旦允许随意修改REA 就退化成了流水账失去全部追溯能力。实际操作中遇到一种情况系统刚上线时事件表还不成熟会有维护人员手动改数改完也没留痕。后面发现数对不上也只能从备份里找。后来我在事件表上加了一个 prevent_update_trigger物理禁止任何对事件表、资源-事件表的 UPDATE 操作要改就 insert 一条新事件。这个动作很狠但长期看非常值。5.5 参与者标签缺失导致对账瘫痪agent_type 字段必须强制填写不允许默认值坚决不要出现“未知”这种标签。内外部参与者牵涉到完全不同的会计口径内部参与者和外部参与者混在一起对账时就要把业务规则猜个遍。参与者表设计没什么高深技巧唯一要求是字段非空、校验严格。另外参与者的唯一性要保证。同一个客户从不同渠道进来如果录成两条 agent 记录后续所有关联查询都会出现分散的客户数据。建表时就加上身份证号、统一社会信用代码或手机号的唯一索引能避免很多统计口径问题。最后分享一点实操心得我从 REA 模型上拿到的最大的教训不是表怎么设计而是动手之前必须把“事实”和“结果”分开想清楚。每张表建的时候都问一下这到底是事实表还是结果表结果能不能从事实推导出来如果推导不出来就说明事实记录不完整。每次模型评审我也只用三个问题验收事件是否只追加不更新任意时点能否重放所有事件资源增减方向是否绝对清晰这三个问题过了REA 建模基本不会走偏。这套方法不只适用于进销存资金管理、订单履约、积分系统、会员权益这些交易型场景都能用同一个思路落地。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑