3. 批次关联数据模型设计

批次关联管理的核心不在于“记录一个批次号”,而在于把供应商、物料、批次、产品、订单这五类对象之间的多对多、层级与流向关系稳定地表达出来。如果数据模型设计不当,后续的追溯查询会退化为大量字符串匹配,既慢又容易断链。本节从实体关系出发,落到关键字段与数据库表结构,给出可直接参考的设计方案。

一、五个核心实体及其关系

先明确各实体的职责边界:供应商是批次的外部来源主体;物料是采购进来的原料或包材,属于“品类级”定义;批次是物料在某一时间、某一来源下的具体实例,是追溯的最小单元;产品是生产出来的成品品类;订单既包括采购订单(供应商→企业),也包括销售订单(企业→客户),是批次流动的业务凭据。

它们之间的关系可以概括为:一个供应商可供应多种物料,一种物料可来自多个供应商;一个物料对应多个批次,每个批次有唯一的来源供应商;批次经生产投料转化为产品批次,形成“父批次—子批次”的层级;订单则把批次与外部主体(供应商或客户)绑定,是正向追踪与反向追溯的入口。下图展示了这一实体关系与批次层级结构。

供应商 supplier 物料 material 批次 batch(追溯最小单元) 产品 product 订单 采购单 / 销售单 供货 实例化 投料转化 批次归属 批次父子层级(关联父批次) 原料批次 RM-2024-0312 成品批次A FG-2024-0315 成品批次B FG-2024-0316 再加工批次 FG-2024-0320 parent_batch_id 多级追溯

二、关键字段设计

批次号(batch_no)是业务主键,建议采用“类型码+日期+流水”的可读编码,例如原料批次 RM-20240312-001、成品批次 FG-20240315-002。批次号必须全局唯一,但不宜作为数据库主键,因为业务编码规则可能调整;应另设自增或雪花 ID 作为物理主键,批次号加唯一索引。

生产日期(production_date)与保质期(shelf_life_days)应分开存储,而不是只存到期日。原因是保质期可能因复检、工艺变更而调整,保留原始天数便于重算。到期日 expiry_date 可由两者推导,但建议冗余存储并建索引,因为追溯查询中“是否过期”是高频过滤条件。计算方式为:expiry_date = production_date + shelf_life_days,按自然日计算,需在字段注释中明确“含生产当日”或“不含”,避免跨系统口径不一致。

关联父批次(parent_batch_id)是层级追溯的关键。它指向同一张批次表的主键,形成自引用结构。一个成品批次通常由多个原料批次投料产生,因此严格来说父子关系是多对多,需要一张批次关联表(batch_relation)来承载,记录“父批次、子批次、投入量、关联类型(投料/分装/返工)”。若只在批次表上放一个 parent_batch_id,会丢失多原料投料信息,这是常见的设计缺陷。

此外,批次表还应包含:物料 ID 或产品 ID(二选一或通过类型字段区分)、供应商 ID(原料批次必填)、订单 ID(可空,用于关联业务凭据)、状态(待检/合格/不合格/冻结)、库位与数量。状态字段对召回演练尤其重要——冻结状态应能阻断出库。

三、数据库表设计原则

原则一:主数据与业务数据分离。供应商、物料、产品属于主数据,变更频率低;批次、订单属于业务数据,增长快。分表后,主数据可加缓存,业务表可独立分区。

原则二:批次表只存“实例属性”,关系放关联表。批次号、日期、状态是实例属性;父子关系、订单归属是关系,应拆到关联表,避免宽表膨胀和更新异常。

原则三:为追溯路径建索引。反向追溯(从成品查原料)依赖 parent_batch_id 或关联表的 child_batch_id 索引;正向追踪(从原料查流向)依赖 parent_batch_id 索引。两个方向都要覆盖。

原则四:保留审计字段。创建时间、创建人、更新时间是召回报告的必要证据,不可省略。

下面给出核心表的建表示例(以 MySQL 语法示意,字段类型可按实际库调整):

CREATE TABLE batch (
  id            BIGINT PRIMARY KEY AUTO_INCREMENT,
  batch_no      VARCHAR(40)  NOT NULL,
  batch_type    TINYINT      NOT NULL COMMENT '1原料 2成品 3半成品',
  material_id   BIGINT       NULL,
  product_id    BIGINT       NULL,
  supplier_id   BIGINT       NULL COMMENT '原料批次必填',
  production_date DATE       NOT NULL,
  shelf_life_days INT       NOT NULL,
  expiry_date   DATE         NOT NULL,
  status        TINYINT      NOT NULL DEFAULT 0 COMMENT '0待检 1合格 2不合格 3冻结',
  created_at    DATETIME     NOT NULL,
  UNIQUE KEY uk_batch_no (batch_no),
  KEY idx_expiry (expiry_date),
  KEY idx_supplier (supplier_id)
);

CREATE TABLE batch_relation (
  id             BIGINT PRIMARY KEY AUTO_INCREMENT,
  parent_batch_id BIGINT NOT NULL COMMENT '上游批次',
  child_batch_id  BIGINT NOT NULL COMMENT '下游批次',
  relation_type   TINYINT NOT NULL COMMENT '1投料 2分装 3返工',
  input_qty       DECIMAL(12,3) NULL,
  created_at      DATETIME NOT NULL,
  KEY idx_parent (parent_batch_id),
  KEY idx_child  (child_batch_id)
);

四、一个可验证的追溯示例

假设原料批次 RM-20240312-001(生产日期 2024-03-12,保质期 180 天,到期日 2024-09-08)于 3 月 15 日投料生产成品批次 FG-20240315-002,投入量 500 kg。在 batch_relation 中写入一条记录:parent 为原料批次 ID,child 为成品批次 ID,relation_type=1,input_qty=500。当客户投诉成品批次时,以 child_batch_id 查关联表即可得到全部上游原料批次;再以原料批次查供应商,即完成反向追溯。反之,若某原料供应商通报质量问题,以 parent_batch_id 查关联表,可列出所有受影响成品批次及其订单,用于定向召回。

设计自检:任取一个成品批次,能否在两次关联查询内得到“全部原料批次 + 供应商 + 对应订单”?若需要三次以上或依赖字符串拼接,说明关联表或索引设计需要调整。

最后提醒两个易错点:一是保质期单位要统一为“天”,避免月、年混用导致到期日计算偏差;二是批次状态变更应留痕,建议增加状态变更日志表,否则召回时无法证明“该批次在何时被冻结”。数据模型一旦承载了这两点,批次关联管理才真正具备可审计、可追溯的工程价值。