看到“rea”这个标题,估计不少人的第一反应跟我一样:这不是 React 少打了个 c,就是某个函数里的 read 写错了。但如果它是一个项目代号,那就有意思了——去年我在给某团队重构中后台系统时,正好把核心模块代号起为 rea,全称是 Resource-Event-Agent,也就是“资源-事件-代理”模型。这套模型原本是会计信息系统领域用来做业务语义建模的经典框架,核心思想一句话就能概括:把业务系统中的所有变化,都看成某个资源因为某个事件发生了权属或数量的变动,并且这个事件一定关联了某个代理。
那段时间我最大的感受是:传统 CRUD 表结构只关心“字段够不够存”,而 REA 关心的是“业务到底有没有被完整描述”。如果你正在设计订单、库存、资金类的数据架构,或者被对账逻辑折磨得够呛,这篇文章值得花十分钟看完。我会讲清楚 REA 模型到底是什么、怎么用它从零搭建一套“购销存”最小闭环,以及我在实操中踩过的坑和排查思路。
1. 为什么是 REA:先把业务建模的底层问题说透
1.1 传统 CRUD 建表,到底把什么弄丢了
传统方式下,我们设计业务表通常是从“界面长什么样”倒推的。订单表要有订单号、客户名、商品名、数量、金额、状态;出入库单表要有单据号、仓库、商品、数量。这类表建起来很快,录入数据也方便,但麻烦会在后面一点一点冒出来。
举个例子。我在模拟项目X里见过一张库存流水表,里面记录了商品的入出库数量,也写了经办人,但月底财务对账时,光查“这批货是谁买的、从哪个供应商那里买的、采购单和入库单是不是同一张单子”就费了半天劲。原因很直接:流水表只记录了“结果”,没记录“原因”。它知道库存从 0 变成了 100,但不知道这 100 是采购入库、销售退货还是盘盈来的,也不知道对应的采购订单在哪、付款到哪一步了。
说白了,传统表结构最容易丢的东西,是“谁→对谁→做了什么→影响了什么资源→发生在什么时间”这条完整的事件链。而没有事件链,后续的统计、对账、审计都要靠 join 一堆表外加人工判断去“补线索”,表越多,口径越乱。
1.2 资源、事件、代理:REA 的三件套
REA 模型把业务世界抽象成三个核心概念,这也是它名称的来源:
- Resource(资源):有价值、且能被一个主体控制的物品或权利。比如库存商品、现金、应收账款、固定资产,甚至服务工时也算。判断标准很简单——它能不能被“转移”或“消耗”。
- Event(事件):让资源发生变化的经济活动。比如采购入库、销售出库、支付货款、收到退款。注意,这里我刻意不用“订单”作为主事件,订单只是承诺,真正影响资源状态的是“履行”动作。
- Agent(代理):参与事件的主体。通常分为内部代理和外部代理两类:内部代理是员工、部门,外部代理是客户、供应商、承运商。
REA 最关键的设计原则是:业务数据库里应该保存的是“事件流”,而不是“修改后的余额”。余额、库存量、应收应付这类数值,都是事件流的派生结果,可以实时计算,也可以定期物化。这个原则带来的直接好处是:只要事件记录没丢、没改,余额随时可以重算,任何一次对账偏差都能追到具体事件。
1.3 三种核心关系:把业务链条串起来
理解了三个实体之后,真正让 REA 跑起来的是它们之间的三种关系,我在建模时会把它们画成关系矩阵来梳理。
第一是资源与事件的关系。一次事件往往会影响多个资源。一张销售出库单可能同时减少多个商品的库存;一次采购入库可能同时增加商品库存和减少银行存款。所以资源与事件之间是多对多关系,需要中间表记录“本次事件对某个资源的影响是什么,数量多少,金额多少,方向是增还是减”。
第二是代理与事件的关系。一个事件可能涉及多个代理。一次销售事件,既有外部客户作为往来单位,也有内部销售员、仓库管理员作为经办人。代理与事件也是多对多关系,并且中间表里最好带上“角色”字段,否则你分不清这张记录里的代理是客户还是仓管。
第三是事件与事件的关系,也就是事件链。比如“采购订单”事件链接着“收货入库”事件,再链接着“付款”事件。事件链表达的是一件事从承诺到履行的完整过程,这也是对账时最重要的线索。我在实际建模中,会单独建一张事件链关系表,记录事件之间的父子关系或前后置关系。
提示:REA 不是让你把表拆得更碎,而是让你在拆表之前先想明白:哪些数据是原始事件,哪些数据是派生结果。这决定了后续所有查询和统计的稳定性。
2. 从需求到模型:用 REA 搭一个购销存最小闭环
2.1 先圈需求,别急着建表
某团队当时的需求很典型:小团队内部业务系统,要管理采购、销售、库存和资金收付,业务规模不大,但要能回答三个问题——现在库存是多少、还欠供应商多少钱、客户还欠我们多少钱。这个规模正好适合用 REA 完整跑一遍。
我先列了一张业务动作清单,把术语统一好:
- 采购相关:创建采购订单、供应商发货、仓库收货入库、发起付款、供应商确认收款。
- 销售相关:创建销售订单、仓库发货出库、客户签收、发起收款、客户确认付款。
- 库存相关:盘点调整、采购退货出库、销售退货入库。盘点调整这里严格说不是经济事件,但也可以用“事件”表达:盘盈盘亏本身是对账面资源的校正动作,所以也放进事件表,只是事件类型要单独标识。
- 资金相关:收款、付款、退款。
这些动作看起来不少,但用 REA 一归类就很清爽:资源就两类,商品和资金;代理就两组,内部员工和外部往来单位;事件就是上面这些动作本身。模型复杂度一下子降下来了。
2.2 实体表设计:资源、事件、代理分别怎么建
在设计表结构时,我遵循了“先拆语义,再定物理表”的顺序。下面是我们最终落地的核心表结构,你可以直接参考。
首先是资源表。商品资源和资金资源差异较大,建议分开建。商品资源表常见字段如下:
CREATE TABLE resource_product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_code VARCHAR(32) NOT NULL UNIQUE COMMENT '商品编码', product_name VARCHAR(128) NOT NULL COMMENT '商品名称', spec VARCHAR(64) NULL COMMENT '规格', unit VARCHAR(16) NOT NULL COMMENT '计量单位', is_active TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) COMMENT '商品资源表';资金资源表很简单,就是结算账户,或者说“钱放哪了”:可以是银行账户、现金账户、虚拟账户。关键是要有账户编码和币种。
CREATE TABLE resource_fund ( id BIGINT PRIMARY KEY AUTO_INCREMENT, fund_code VARCHAR(32) NOT NULL UNIQUE COMMENT '账户编码', account_name VARCHAR(128) NOT NULL COMMENT '账户名称', currency_code CHAR(3) NOT NULL DEFAULT 'CNY', is_active TINYINT NOT NULL DEFAULT 1 ) COMMENT '资金资源表';然后是代理表。内部代理通常是员工,外部代理是客户和供应商,这两类属性差异很大,我强烈建议分开。内部代理表记录员工信息,外部代理表用agent_type区分客户和供应商,也可以后续扩展成承运商、服务商。
CREATE TABLE agent_internal ( id BIGINT PRIMARY KEY AUTO_INCREMENT, employee_no VARCHAR(32) NOT NULL UNIQUE COMMENT '工号', display_name VARCHAR(64) NOT NULL COMMENT '姓名', department VARCHAR(64) NULL, is_active TINYINT NOT NULL DEFAULT 1 ) COMMENT '内部代理表'; CREATE TABLE agent_external ( id BIGINT PRIMARY KEY AUTO_INCREMENT, agent_code VARCHAR(32) NOT NULL UNIQUE COMMENT '往来单位编码', agent_type CHAR(1) NOT NULL COMMENT 'C=客户 V=供应商', agent_name VARCHAR(128) NOT NULL COMMENT '往来单位名称', contact_name VARCHAR(64) NULL, contact_phone VARCHAR(32) NULL, is_active TINYINT NOT NULL DEFAULT 1 ) COMMENT '外部代理表';接下来是核心事件表。这里有个设计取舍:是把采购订单、入库单、销售订单、出库单、收付款都分开建各自的事件表,还是统一建一张大事件表?我在模拟项目X里选择了后者——事件表统一,配合事件类型字段区分,再搭配事件属性扩展表。这样做的原因有三个:一是所有事件的公共属性(时间、状态、幂等号、备注)只需要维护一套;二是后续归档、统计、审计时不用跨多张表合并;三是扩展新事件类型时只需要加类型枚举和扩展属性,不用新建表。
CREATE TABLE business_event ( id BIGINT PRIMARY KEY AUTO_INCREMENT, event_type VARCHAR(32) NOT NULL COMMENT '事件类型:PURCHASE_ORDER/RECEIPT/PAYMENT/SALES_ORDER/SHIPMENT...', event_no VARCHAR(48) NOT NULL UNIQUE COMMENT '业务单据号,幂等唯一', occurred_at DATETIME NOT NULL COMMENT '业务发生时间', status VARCHAR(16) NOT NULL COMMENT 'DRAFT/COMMITTED/CANCELLED...', total_amount DECIMAL(18,2) NULL COMMENT '事件总金额,仅作展示和核对', remark VARCHAR(512) NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) COMMENT '业务事件表';事件与资源的中间表,这是整个模型里最关键的一张表,每次事件对资源的具体影响都记在这里:
CREATE TABLE event_resource ( id BIGINT PRIMARY KEY AUTO_INCREMENT, event_id BIGINT NOT NULL, resource_type VARCHAR(32) NOT NULL COMMENT '动的是商品还是资金', resource_id BIGINT NOT NULL COMMENT '对应 resource_product.id 或 resource_fund.id', quantity DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT '数量,商品类资源必填,资金类资源填0', unit_price DECIMAL(18,4) NULL COMMENT '单价', amount DECIMAL(18,2) NOT NULL COMMENT '该资源行金额', direction TINYINT NOT NULL COMMENT '资源流入=1,流出=-1', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_event_id (event_id), KEY idx_resource (resource_type, resource_id) ) COMMENT '事件-资源影响明细表';事件与代理的中间表,和上一张表配合,记录“谁参与了这件事,以什么角色参与”:
CREATE TABLE event_agent ( id BIGINT PRIMARY KEY AUTO_INCREMENT, event_id BIGINT NOT NULL, agent_type VARCHAR(16) NOT NULL COMMENT 'INTERNAL/EXTERNAL', agent_id BIGINT NOT NULL, role_type VARCHAR(32) NOT NULL COMMENT 'CREATOR/APPROVER/CUSTOMER/SUPPLIER/HANDLER...', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_event_id (event_id), KEY idx_agent (agent_type, agent_id) ) COMMENT '事件-代理参与表';最后是事件链关系表,用于把采购订单、入库、付款这些事件串成链条。注意这里保存的是事件之间的引用关系,比如“收货事件”是由哪个“采购订单事件”触发的:
CREATE TABLE event_link ( id BIGINT PRIMARY KEY AUTO_INCREMENT, prev_event_id BIGINT NOT NULL COMMENT '前置事件ID', next_event_id BIGINT NOT NULL COMMENT '后续事件ID', link_type VARCHAR(16) NOT NULL COMMENT 'CAUSES/COMPENSATES/REVOKES...', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_prev_event (prev_event_id), KEY idx_next_event (next_event_id) ) COMMENT '事件链关系表';2.3 一个容易踩的坑:金额到底记在事件头还是事件行
在建表时,某团队同事提出了一个很典型的疑问:business_event.total_amount和event_resource.amount都存金额,会不会冗余?
答案是:两者都有必要,但用途完全不同。事件头金额是人为录入的“单据金额”,用于展示和人工核对;事件行金额是参与计算的“影响金额”,用于余额聚合和账实核对。如果只记一行金额,一张单子包含多个商品时,你没法知道哪个商品占的金额是多少;而如果只用行金额、不记头金额,单据被多次编辑后,你很难判断原始的“开单金额”到底是哪一版。
所以在模拟项目X里,我们约定:一切统计以event_resource.amount为准,business_event.total_amount只作为“单据面见金额”。月底对账如果行汇总和头金额对不上,说明有人改了明细没改头,或者反了过来,这是需要排查的信号,而不是直接忽略。
这个模型跑通后,我再讲一下具体的实操落地过程。
3. 实操全过程:建表、造数、验证账实一致性
3.1 落地四步走:关系矩阵→物理表→视图→校验任务
第一步,先把需求清单里的每个动作都填进一张关系矩阵表。矩阵的行是资源,列是事件,交叉点写“+数量”“-金额”。比如“采购入库”这一列,在“商品资源”行的交叉点写“+数量+金额”,在“资金资源”行写“-金额(应付款)”。这张矩阵看起来没什么技术含量,却是整个建模中最重要的一步,因为它逼着你把每个业务动作对资源的影响方向敲定,避免后续写代码时出现“同一类事件有的加、有的减”的口径混乱。
第二步,按矩阵建物理表。这里有个实操建议:外键约束不要建得太死。因为事件表、中间表都会频繁插入,而且历史事件不允许修改,外键约束在并发写入时很容易成为瓶颈。我们最终只在event_resource.event_id、event_agent.event_id上建了普通索引,外键关系完全由应用层保证。数据库层的强约束只留下了business_event.event_no唯一索引,这是幂等底线。
第三步,建视图。REA 模型下,库存余额、应收余额这些数值都是派生数据,不建议写业务代码去维护“余额表”,而是用一个统一的可查询视图撑住。我先建了“库存余额视图”,它本质上是把所有event_resource按照商品分组、按方向求和:
CREATE VIEW v_product_balance AS SELECT er.resource_id, rp.product_code, rp.product_name, rp.unit, SUM( CASE WHEN er.direction = 1 THEN er.quantity WHEN er.direction = -1 THEN -er.quantity ELSE 0 END ) AS balance_quantity, SUM( CASE WHEN er.direction = 1 THEN er.amount WHEN er.direction = -1 THEN -er.amount ELSE 0 END ) AS balance_amount FROM event_resource er LEFT JOIN resource_product rp ON er.resource_id = rp.id WHERE er.resource_type = 'PRODUCT' GROUP BY er.resource_id, rp.product_code, rp.product_name, rp.unit;第四步,把日常对账常用但聚合成本高的统计,建成定时刷新任务。比如“应付余额”和“应收余额”这两个指标,每天凌晨跑一次物化任务,把结果存到单独的统计表。业务查询时优先读统计表,需要追溯明细时再回事件表查。这套做法避免了实时 join 大量中间表导致性能问题。
3.2 跑通一条采购入库事件流
现在我用一条最小数据链路演示整个过程:向供应商 S 采购商品 A 共 100 件,单价 5 元,金额 500 元,货已到库。
第一步,插一条采购订单事件,状态为COMMITTED(已确定):
INSERT INTO business_event (event_type, event_no, occurred_at, status, total_amount, remark) VALUES ('PURCHASE_ORDER', 'PO20250315001', '2025-03-15 10:00:00', 'COMMITTED', 500.00, '采购100件商品A');第二步,插入事件-资源明细,表示“商品 A 流入库存,数量 100,金额 500,方向为 1”:
INSERT INTO event_resource (event_id, resource_type, resource_id, quantity, unit_price, amount, direction) SELECT id, 'PRODUCT', 1, 100, 5.00, 500.00, 1 FROM business_event WHERE event_no = 'PO20250315001';注意这里用了SELECT id FROM business_event而不是硬编码事件 ID,是为了保证幂等性和可读性。实际项目里你可以在插入时先查一次event_no是否存在,存在就跳过,这是防重复执行的基本功。
第三步,插入事件-代理,表示“供应商 S 是此事件的往来单位,员工 E 是经办人”:
INSERT INTO event_agent (event_id, agent_type, agent_id, role_type) SELECT id, 'EXTERNAL', 1001, 'SUPPLIER' FROM business_event WHERE event_no = 'PO20230315001'; INSERT INTO event_agent (event_id, agent_type, agent_id, role_type) SELECT id, 'INTERNAL', 501, 'APPROVER' FROM business_event WHERE event_no = 'PO20230315001';第四步,当仓库实际收货后,再插入一条“收货入库”事件,并通过event_link把采购订单事件和收货事件连起来:
INSERT INTO business_event (event_type, event_no, occurred_at, status, total_amount, remark) VALUES ('RECEIPT', 'RECV20250315001', '2025-03-15 14:00:00', 'COMMITTED', 500.00, '实际入库'); INSERT INTO event_link (prev_event_id, next_event_id, link_type) SELECT po.id, rec.id, 'CAUSES' FROM business_event po, business_event rec WHERE po.event_no = 'PO20250315001' AND rec.event_no = 'RECV20250315001';这时候查询库存视图,就能看到商品 A 的库存余额为 100 件、金额 500 元。这就是 REA 的直观优势:库存数值不是某个“库存余额表”里写死的,而是你能从事件流里一步步算出来的。
3.3 账实一致性校验的核心 SQL
模型落地之后,验证它靠不靠得住,就看三组校验 SQL。第一组是资源余额校验,也就是库存数量、库存金额不能为负(除特殊场景外):
SELECT resource_id, SUM( CASE WHEN direction = 1 THEN quantity ELSE -quantity END ) AS balance FROM event_resource WHERE resource_type = 'PRODUCT' GROUP BY resource_id HAVING balance < 0;这组查询如果有结果,说明某笔出库事件对应的入库数量不够扣,或者入库事件被误删了,优先检查对应的business_event.status是否被错误修改。
第二组是事件链完整性校验。比如一张销售出库单如果找不到对应的销售订单事件,那说明事件链断了,最常见的原因是有人直接在执行层补了出库单,而没有走订单流程。校验 SQL 可以这样写:
SELECT r.event_no, e.prev_event_id FROM business_event r LEFT JOIN event_link e ON e.next_event_id = r.id WHERE r.event_type IN ('RECEIPT', 'SHIPMENT') AND e.prev_event_id IS NULL;有结果就说明链断了,需要人工核查。这条规则在项目上线后几乎成了每周必跑的任务,效果很好。
第三组是分派一致性校验。校验的是同一张事件头金额,是否和所有事件行金额合计一致。如果确认了所有统计都以行金额为准,那这个校验就是用来抓“改单不彻底”的:
SELECT be.event_no, be.total_amount, SUM(er.amount) AS line_total FROM business_event be JOIN event_resource er ON er.event_id = be.id GROUP BY be.id, be.event_no, be.total_amount HAVING ABS(be.total_amount - line_total) > 0.01;这三组 SQL 是 REA 模型运行的基本护栏,每个月至少跑一次。跑完之后,财务和研发都能拿着同样的结果开会,不用再争“谁的数据错了”。
4. 常见问题与排查实录
4.1 多对多中间表,如何避免聚合时“算重”
REA 的三张中间表看起来漂亮,一旦开始做报表,第一个遇到的坑就是“重复聚合”。我这里说的不是多表 join 后的笛卡尔积,而是更隐蔽的口径问题。
举个例子。一张销售出库单同时出了两个商品,A 商品 10 件,B 商品 20 件,总金额 100 元。事件行表里有两行记录,各带金额。如果写报表 SQL 时图省事,先 join 事件头,再 join 事件行,最后 sum(事件头.total_amount),你得到的是两个商品各一次 100 元,总计 200 元。报表整整多了一倍。
我们踩过一次这个坑之后,定了一条硬性约定:所有涉及金额的统计,一律用event_resource行表,永不直接 sumbusiness_event.total_amount。头表的金额只允许出现在“单号列表”“详情展示”这样的只读场景。在代码 review 环节,这条规则被写进了检查项;后来某个月对账差异缩小到零,这条约定功不可没。
4.2 业务规则到底放哪一层
REA 模型只给了骨架,它不会告诉你“库存不能为负”这种规则写在哪里。我在实际中的经验是分三层处理:
最底层的数据完整性约束,比如事件号必须唯一、方向字段只能为 1 或 -1、金额不能为负,这些直接写在数据库层。用CHECK约束或者唯一索引挡死,从物理上杜绝脏数据。
中间层的业务规则校验,比如“销售出库前确认库存足够”、“采购收货前确认订单已提交”,这些放在应用服务层。原因很简单:这些规则会随业务变化频繁调整,放在数据库触发器里,改一次规则就要改一次 schema,上线成本太高,还容易出现触发器栈嵌套导致死锁。
顶层的流程编排规则,比如“先收款后发货还是先发货后收款”,这属于业务流程决策,放在服务编排层,根本不需要进数据库。
做这个分层的时候,我们内部也有过争执,后来用一次线上事故分了胜负:有同事把“订单金额不能为空”的规则写进数据库触发器,结果某次临时需要允许零元订单做促销测试时,改触发器的脚本卡在发布流程里多花了四个小时。从那天开始,团队就统一了“数据库只守底线,不写业务判断”的原则。
4.3 和传统 ERP/财务系统如何互相翻译
很多团队不会完全推倒原有的 ERP 系统,REA 新模型和旧系统之间需要做数据映射。这里我整理了一张我们在模拟项目X里用过的映射表,几乎可以直接抄:
| REA 概念 | 传统财务/ERP 概念 | 映射说明 |
|---|---|---|
| Resource(商品/资金资源) | 存货科目、现金科目、银行科目 | 资源表对应到科目余额,商品资源映射到存货科目,资金资源映射到货币资金科目 |
| Economic Event(经济事件) | 业务凭证、记账凭证 | 一次采购入库事件,就是传统库存模块的“入库单”加财务模块的“借存货/贷应付”凭证 |
| 事件-资源明细 | 凭证分录行 | 事件资源行对应到分录的借方或贷方,direction=1 即资源增加,direction=-1 即资源减少 |
| Agent(代理) | 往来单位辅助核算、部门、员工 | 外部代理映射到客户/供应商档案,内部代理映射到操作员和部门 |
| 事件链 | 单据流转关系、凭证引用关系 | 事件链对应到“采购订单→入库单→采购发票→付款单”的关联关系 |
有了这张映射表,REA 模型的数据可以很自然地导出到传统财务报表里。我们实际做的事情是每天把新增事件同步到一个“导出中间表”,然后由 ERP 那边的程序读取,自动生成记账凭证。映射关系,就靠event_resource.resource_type和direction两个字段判断借贷方向。
注意:翻译映射有一个容易忽略的点——传统科目的余额是一个时点值,而 REA 事件流是时期值。导数据时如果直接拿 REA 的累计事件流覆盖科目余额,大概率对不上。我建议只导出增量事件,让老系统自己更新余额,不要用新系统的余额去覆盖老系统的余额。
4.4 性能、归档与报表优化
REA 模型把大量信息拆到中间表,业务跑半年后,event_resource表很容易膨胀到几千万行。几千万行并不算多,但如果没有合理的查询路径,报表就能把库拖垮。
我们的优化方案是组合拳。第一,按事件时间做分区表,按月分区。business_event和event_resource都以occurred_at作为分区键,这样查上个月的报表时实际只扫一个分区,速度提升明显。第二,把日常报表统一改成物化视图,每天凌晨重建。实时查询只保留“单号详情查询”和“异常校验”两类核心场景,其他所有聚合统计全部走物化结果。第三,中间表索引不过度设计,但三个复合索引必须有:(resource_type, resource_id)、(event_id)、(agent_type, agent_id)。如果查询经常按时间过滤,再把occurred_at加到第一个索引前部。
我还见过一个挺典型的反面案例。某团队为了让库存余额查询更快,专门建了一张“库存余额表”,每次出库都在事务里同时更新这张余额表。表面上看查询快了,但事务锁冲突直线上升,而且一旦出现运行时异常导致余额表和事件表不一致,恢复数据变成了灾难。REA 模型鼓励的“余额是派生数据”这个原则,从长远看,能救你很多次。
4.5 事件字段随意扩展导致的“类型沼泽”
再补一个我在两种模型之间切换时踩过的坑。REA 模型的business_event.event_type一开始只定义了十几种类型,结果业务越接越多,有人图省事,直接把“退货”“换货”“赠品”“内部领用”都塞进两个通用类型里,然后靠备注字段区分。一个月后写报表时,前端开发根本不知道该怎么归类,只能到处问。
后来我们花了三天,把所有事件重新梳理了一遍,增加了一个sub_type字段,并且明确每种子类型的资源影响方向。这件事给我最大的教训是:REA 模型是语义模型,不是分类垃圾桶。业务上含义不同的事件,就算字段长得再像,也一定要用不同的类型编码定义,否则模型的“自解释性”会越来越差,最终又退化成大家各自为政的表结构。
结尾
整个 rea 项目做下来,我最大的体会是:REA 模型最值钱的地方不在那几张表,而是逼着你在动手写代码之前,先把业务语义按“资源、事件、代理”三个维度梳理了一遍。它让开发、财务、业务方用同一种语言描述“一笔业务到底发生了什么”,而不是各看各的表、各说各的话。那位架构师听完我讲这套模型之后说了一句话,我记到现在:传统表结构是为“录入”设计的,所以统计一直在打补丁;REA 是为“事件”设计的,统计只是事件流的一次投影。
最后分享一个小技巧:如果你正准备给自己的系统做建模规划,别一上来就画表结构,找个周末,拿一张真实的采购单、一张销售单,用白纸把“资源、事件、代理”三个维度分别列出来,再画清楚事件之间的链条。能把这两张单子的闭环跑通,后面的表设计和对账逻辑都会顺畅很多。