- 6.4 要点 1 补充 CHECK 约束的作用域说明,并加四种情形的对照表:
该约束作用于货源行而非物料,既无供应商也无制造商的物料(70 条老数据)
因为没有货源行而不受影响,被挡的只是「建了货源行却说不出是谁的货」
- 7.4 标题去掉「(待解决)」,改为「回源后拆分成多行」——这不是设计缺口,
而是已定方案里的一个执行步骤,并写明拆分到多行的具体做法
- 核准 32 条多供应商物料的供应商家数分布为 {2: 32}(每条恰好 2 家),
据此把回填货源总数由 129 修正为 161(涉及 3.2、D10、6.2、7.1、7.2、7.4)
334 lines
25 KiB
Markdown
334 lines
25 KiB
Markdown
# 物料多供应商与多制造商关系设计(数据库层)
|
||
|
||
**日期:** 2026-10-15
|
||
|
||
**状态:** 初稿,待评审
|
||
|
||
**适用范围:** warehouse-mms 数据库结构
|
||
|
||
## 1. 目标
|
||
|
||
让一个物料可以关联多个供应商和多个制造商,并且能表达出三者之间的对应关系——即"哪家供应商供的是哪家制造的货",同时保留每条货源的采购条件(价格、币种、最小订购量、供货周期、首选标记)。
|
||
|
||
本次只确定数据库结构,前后端改造另行设计。
|
||
|
||
## 2. 范围
|
||
|
||
### 2.1 本次包含
|
||
|
||
- 物料、供应商、制造商三者关系的表结构。
|
||
- 相关表的字段、唯一键、外键、索引设计。
|
||
- 现有 `materials` 表上单值供应商/制造商字段的处置。
|
||
- 现有 `material_suppliers` 空表的改造。
|
||
- 新旧数据的数据回填方案。
|
||
|
||
### 2.2 本次不包含
|
||
|
||
- 后端 model / repository / service / handler 改造。
|
||
- 物料判重规则的调整。
|
||
- Excel 批量导入的模板与解析调整。
|
||
- 前端表单、列表、详情的展示与交互。
|
||
- 权限模型调整。
|
||
- 物料日志内容调整。
|
||
|
||
## 3. 现状
|
||
|
||
### 3.1 数据库现状
|
||
|
||
| 对象 | 现状 | 位置 |
|
||
| --- | --- | --- |
|
||
| `materials.supplier_id` | 单值可空外键,语义为"默认供应商" | `scripts/init.sql:19` |
|
||
| `materials.manufacturer_id` | 单值可空外键,语义为"制造商" | `scripts/init.sql:19` |
|
||
| `materials.manufacturer_model` | 厂家型号,物料上的单值文本;**现有数据全为 NULL**(见 3.3) | `scripts/init.sql:19` |
|
||
| `material_suppliers` | 物料-供应商关系表,带采购条件;**表已建、零数据、零代码引用** | `scripts/init.sql:34` |
|
||
| `material_manufacturers` | 不存在 | — |
|
||
|
||
三个必须先说明的事实:
|
||
|
||
1. `materials` 当前通过复合外键 `FOREIGN KEY(organization_id, supplier_id) REFERENCES organization_partners(organization_id, id)` 保证伙伴属于同一公司(`scripts/init.sql:32-33`),依赖 `organization_partners` 上的唯一键 `uk_organization_partners_org_id(organization_id, id)`(`scripts/init.sql:27`)。该唯一键目前全库只有 `materials` 一处消费。
|
||
2. `material_suppliers` 设计不完整:它只有查询索引 `idx_material_suppliers_material`,**没有唯一键**;`organization_partner_id` 是**单列**外键,没有公司隔离;**没有 `created_at` / `updated_at`**;`effective_from DATE NOT NULL` 无默认值。由于该表是空的且无代码引用,改造代价为零。
|
||
3. 文档已明确 `material_suppliers` 的角色:`docs/数据库表结构与关系说明.md:255` 写明"现有 `material_suppliers` 继续保存多个供应商的价格与供货条件;物料的 `supplier_id` 只表示默认选择";`:196` 注明"当前后端未见完整维护路径"。
|
||
|
||
### 3.2 实测数据
|
||
|
||
对 `scripts/init.sql` 中 `materials` 的 199 行 INSERT 逐行统计:
|
||
|
||
| `(supplier_id, manufacturer_id)` | 行数 |
|
||
| --- | --- |
|
||
| 两者皆空 | 70 |
|
||
| 只有供应商 | 119 |
|
||
| 只有制造商 | 8 |
|
||
| 两者都有 | 2 |
|
||
|
||
- 有供应商的物料:121 行;有制造商的物料:10 行。
|
||
- 两者都有的 2 行中,供应商与制造商是**同一个伙伴**(原厂直销)。
|
||
- 按「每物料一条货源」计,去重后为 129 条(121 + 10 − 2);再加上 7.4 所述 32 条被截断的多供应商(每家一条),回填后共 **161 条**货源记录。
|
||
|
||
两个直接推论:多制造商目前几乎没有数据基础(10/199),改造的迁移压力很小;"只登记供应商、不填制造商"是主流场景(119 行),因此**货源的制造商维度必须可空**。
|
||
|
||
### 3.3 迁移脚本层面的两个发现
|
||
|
||
检查 `scripts/init.sql` 中物料数据的写入语句后,有两个直接决定改造方式的事实:
|
||
|
||
1. **`manufacturer_model` 是死列。** 该列在全文件中只出现在第 19 行的 `CREATE TABLE`:199 条物料的 INSERT 列清单里没有它,也没有任何 `UPDATE` 给它赋值,前端表单同样不含该字段(`frontend/src/views/materialForm.js:3-16`)。即现有数据的这一列**全部为 NULL**。因此 D4 拆表时没有型号数据需要迁移。
|
||
2. **物料插入不是裸 `INSERT`,而是幂等写入。** 北京 16 条(`scripts/init.sql:731`)与武汉 183 条(`:1495`)各带一段 `ON DUPLICATE KEY UPDATE` 子句,其中包含 `supplier_id = new.supplier_id, manufacturer_id = new.manufacturer_id`。删列时必须同步删除这些赋值,否则整个初始化脚本会因 `Unknown column` 直接失败。
|
||
|
||
## 4. 关系模型论证
|
||
|
||
### 4.1 为什么是三元关系
|
||
|
||
「多个供应商」和「多个制造商」如果各自独立存储,会丢失一条关键业务信息:**哪家供应商供的是哪家的货**。
|
||
|
||
现实中一条采购记录的本质是"我从 A 买,买到的是 M 造的货,价格 P、交期 D"。供应商和制造商不是物料的两个平行属性,而是同一条货源记录上的两个维度。因此真正的实体是 `(物料, 供应商, 制造商)`,价格、币种、最小订购量、供货周期都是这个三元组上的属性。
|
||
|
||
三条佐证:
|
||
|
||
1. 预置的 `material_suppliers` 把 `price` / `currency` / `minimum_order_quantity` / `lead_time_days` / `is_preferred` 全部带上了,原始设计意图就是"货源"而非"清单"。
|
||
2. `materials.manufacturer_model` 是物料上的单值文本,多制造商时必然冲突(M1 是 `ESF32016H24`、M2 是别的型号),说明型号也应随制造商走。
|
||
3. 现有数据中供应商与制造商为同一伙伴的那 2 条,在"两个独立列表"模型下会显示成两个相同的名字,语义是坏的。
|
||
|
||
反过来不成立:三元模型能退化成两个清单(每行只填供应商、制造商留空即可),两个清单却推不出配对。
|
||
|
||
### 4.2 被否决的候选模型
|
||
|
||
| 模型 | 说明 | 否决理由 |
|
||
| --- | --- | --- |
|
||
| 两张平行列表 | `material_suppliers` + 新建 `material_manufacturers`,互不关联 | 无法表达配对,将来做比价或替代货源需推倒重构 |
|
||
| 物料变体拆分 | 不同制造商拆成不同物料码,用替代料关系关联 | 物料码膨胀(10 家制造商 = 10 个物料码),采购与库存口径分裂 |
|
||
| 型号留在物料上 | `manufacturer_model` 不下移 | 多制造商时一个字段装不下,语义残缺 |
|
||
|
||
最终采用**三元关系表 + 独立的制造商表**:货源表承载"物料 × 供应商 × 指向的制造商条目 × 采购条件",制造商表单独承载"物料 × 制造商 × 型号"。
|
||
|
||
## 5. 已确定的决策
|
||
|
||
| 序号 | 决策 | 结论 |
|
||
| --- | --- | --- |
|
||
| D1 | 关系语义 | 三元配对,且带采购条件 |
|
||
| D2 | 价格历史 | 只存当前价,唯一键不含生效日期;历史靠 `material_logs` |
|
||
| D3 | `materials` 旧字段 | 删除 `supplier_id`、`manufacturer_id`、`manufacturer_model` 及相关外键索引 |
|
||
| D4 | 厂家型号归属 | 拆出 `material_manufacturers` 第三张表 |
|
||
| D5 | 货源表表名 | 沿用 `material_suppliers`,不改名 |
|
||
| D6 | 停用方式 | 软停用,与 `materials` 保持一致 |
|
||
| D7 | 公司与物料一致性 | **全部放到应用层校验**,数据库只保留单列外键,不引入新的复合外键 |
|
||
| D8 | `status` 取值 | `active` / `obsolete`,与物料及 BOM 版本一致 |
|
||
| D9 | `uk_organization_partners_org_id` | 删除。它是为本次废弃的复合外键而建,此后无任何消费者 |
|
||
| D10 | `init.sql` 数据改造方式 | **临时表中转**:199 行物料原样插入临时表后分流,避免手工搬运 161 条货源 |
|
||
|
||
关于 D6 的依据:物料本身即软停用(`UPDATE materials SET status = 'obsolete'`,`internal/repository/material.go:510`;恢复见 `:545`),其唯一键 `uk_materials_org_code` **不含 `status`**,即作废的物料仍占着物料码、只能恢复不能重建;查询侧也早已习惯带 status 过滤(`internal/repository/material.go:261-264`)。货源沿用同一模型是自洽的。
|
||
|
||
关于 D7 的后果:不引入复合外键意味着数据库**不再保证**伙伴与物料属于同一公司,也**不再保证**货源指向的制造商条目属于同一物料。这些检查全部由应用层承接,清单见第 8 节第 5 条。
|
||
|
||
## 6. 表设计
|
||
|
||
### 6.1 `material_manufacturers`(新建)
|
||
|
||
物料可用的制造商及其型号。
|
||
|
||
```sql
|
||
CREATE TABLE material_manufacturers (
|
||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||
organization_id BIGINT NOT NULL,
|
||
material_id BIGINT NOT NULL,
|
||
manufacturer_id BIGINT NOT NULL COMMENT 'organization_partners ID',
|
||
model VARCHAR(200) NULL COMMENT '厂家型号',
|
||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
UNIQUE KEY uk_material_manufacturers_dim(organization_id, material_id, manufacturer_id),
|
||
FOREIGN KEY(material_id) REFERENCES materials(id),
|
||
FOREIGN KEY(manufacturer_id) REFERENCES organization_partners(id)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||
```
|
||
|
||
- `uk_material_manufacturers_dim` 保证同一物料下同一制造商只有一条。
|
||
- 由于 `manufacturer_id` 非空,本表不需要 `COALESCE` 生成列。
|
||
- `status` 同样采用软停用,且同样**不进入唯一键**,行为与 6.4 要点 3 描述的货源一致:停用过的制造商条目仍占着 `(物料, 制造商)` 键位,重新启用只能恢复,不能重建。
|
||
|
||
### 6.2 `material_suppliers`(改造现有空表)
|
||
|
||
每条记录表示一条货源:某供应商供某物料、供应的是哪个制造商条目、以及该货源的采购条件。
|
||
|
||
```sql
|
||
CREATE TABLE material_suppliers (
|
||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||
organization_id BIGINT NOT NULL,
|
||
material_id BIGINT NOT NULL,
|
||
supplier_id BIGINT NULL COMMENT 'organization_partners ID',
|
||
material_manufacturer_id BIGINT NULL COMMENT 'material_manufacturers ID',
|
||
price DECIMAL(20,6) NULL,
|
||
currency CHAR(3) NULL,
|
||
minimum_order_quantity DECIMAL(20,6) NOT NULL DEFAULT 0,
|
||
lead_time_days INT NOT NULL DEFAULT 0,
|
||
is_preferred BOOLEAN NOT NULL DEFAULT FALSE,
|
||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
supplier_key BIGINT GENERATED ALWAYS AS (COALESCE(supplier_id, 0)) VIRTUAL,
|
||
manufacturer_entry_key BIGINT GENERATED ALWAYS AS (COALESCE(material_manufacturer_id, 0)) VIRTUAL,
|
||
preferred_row BIGINT GENERATED ALWAYS AS (CASE WHEN is_preferred AND status = 'active' THEN id ELSE NULL END) VIRTUAL,
|
||
UNIQUE KEY uk_material_suppliers_dim(organization_id, material_id, supplier_key, manufacturer_entry_key),
|
||
UNIQUE KEY uk_material_suppliers_preferred(organization_id, material_id, preferred_row),
|
||
CONSTRAINT chk_material_suppliers_dimension CHECK (supplier_id IS NOT NULL OR material_manufacturer_id IS NOT NULL),
|
||
KEY idx_material_suppliers_supplier(organization_id, supplier_key),
|
||
KEY idx_material_suppliers_manufacturer(organization_id, manufacturer_entry_key),
|
||
FOREIGN KEY(material_id) REFERENCES materials(id),
|
||
FOREIGN KEY(supplier_id) REFERENCES organization_partners(id),
|
||
FOREIGN KEY(material_manufacturer_id) REFERENCES material_manufacturers(id)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||
```
|
||
|
||
相对现有结构的字段变化:
|
||
|
||
| 列 | 变化 |
|
||
| --- | --- |
|
||
| `organization_partner_id` | 改名为 `supplier_id`,保持可空 |
|
||
| `material_manufacturer_id` | 新增,可空 |
|
||
| `price` / `currency` | 由 `NOT NULL` 改为**可空** |
|
||
| `minimum_order_quantity` / `lead_time_days` | 保留 `NOT NULL DEFAULT 0` |
|
||
| `is_preferred` / `status` | 保留,`status` 默认 `active` |
|
||
| `effective_from` / `effective_to` | **删除**(对应 D2) |
|
||
| `created_at` / `updated_at` | 新增,与其他表一致 |
|
||
|
||
`price` / `currency` 必须改可空的原因很直接:回填的 161 条老数据没有任何价格信息,硬性要求非空则迁不动。
|
||
|
||
### 6.3 `materials`(删列)
|
||
|
||
删除以下对象:
|
||
|
||
- 列:`supplier_id`、`manufacturer_id`、`manufacturer_model`
|
||
- 外键:`fk_materials_supplier`、`fk_materials_manufacturer`(`scripts/init.sql:32-33`)
|
||
- 索引:`idx_materials_org_supplier`、`idx_materials_org_manufacturer`(`scripts/init.sql:30-31`)
|
||
|
||
连带删除:`organization_partners` 上的 `uk_organization_partners_org_id(organization_id, id)`(`scripts/init.sql:27`)。它是为上面两条复合外键而建的,本次改造后不再被任何表消费;且 `id` 已是主键,该组合唯一键本身冗余,删除只省一个索引,无功能影响(对应 D9)。
|
||
|
||
### 6.4 约束设计的三个要点
|
||
|
||
**要点 1 — 用生成列解决 NULL 让唯一键失效。** 唯一键 `(organization_id, material_id, supplier_id, material_manufacturer_id)` 挡不住重复,因为 SQL 中 `NULL <> NULL`,同一物料的 `(A, NULL)` 可以插入多行。而"只登记供应商不填制造商"是主流场景(119/199)。解法是把参与唯一键的列包成 `COALESCE(..., 0)` 生成列,同时原列保持 NULL(单列外键对 NULL 自动放行,正好表达"未指定")。同一手法在本项目已有先例:`user_organizations`(`scripts/init.sql:14`)。
|
||
|
||
但同时为空的**货源行**没有意义——既没有供应商也没有制造商,它不描述任何关系。用 `CHECK (supplier_id IS NOT NULL OR material_manufacturer_id IS NOT NULL)` 挡在数据库层。MySQL 8.0.16 起 `CHECK` 才真正执行约束,本项目用 `mysql:8.0` 镜像,满足要求。
|
||
|
||
**这条约束作用于货源行,不作用于物料,两者不要混淆:**
|
||
|
||
| 情况 | `material_suppliers` 里有没有行 | 是否合法 |
|
||
| --- | --- | --- |
|
||
| 物料既无供应商也无制造商(老数据 70 条,见 3.2) | **没有行** | 合法。约束管不到「没有行」 |
|
||
| 物料有一条货源行,只填了供应商 | 有一行,`material_manufacturer_id` 为 NULL | 合法(119/199 的主流形态) |
|
||
| 物料有一条货源行,只填了制造商 | 有一行,`supplier_id` 为 NULL | 合法 |
|
||
| 物料有一条货源行,两个伙伴都为空 | 有一行,两列均 NULL | **被挡住**——这行不承载任何信息 |
|
||
|
||
也就是说「物料可以既无供应商又无制造商」这个合法状态完全不受影响,被挡的只是「建了货源行却说不清是谁的货」。
|
||
|
||
注意 `price` / `currency` 可空与 `minimum_order_quantity` / `lead_time_days` 非空并不矛盾:前者的 NULL 表示"尚未报价",是未知;后者的 `0` 表示"无最小订购量""无固定交期",是明确的默认值,且原表设计如此。
|
||
|
||
**要点 2 — 公司隔离与物料一致性由应用层承接(对应 D7)。** 因为不再使用复合外键,数据库不会阻止以下三种脏引用:货源或制造商条目的 `organization_id` 与物料不同、货源指向的制造商条目属于另一个物料、伙伴不属于本公司。接口层必须在写入前逐条校验,具体清单见第 8 节第 5 条。
|
||
|
||
这个取舍与项目其他表一致:分类的 `parent_id`、`base_unit_id`、`warehouse_id`、`material_id` 等也都是单列外键,公司一致性统一由服务层保证(`docs/数据库表结构与关系说明.md:237`)。本次改造后,原先只此一处的复合外键模式将完全退出。
|
||
|
||
**要点 3 — 两个唯一键对 `status` 的态度不同,这是有意的不对称。**
|
||
|
||
- 三元组唯一键 `uk_material_suppliers_dim` **不含 status**:停用的货源仍占着它的三元组键位,重新启用只能走"恢复"(UPDATE status),不能 INSERT。这与物料的行为完全一致(作废物料仍占物料码)。
|
||
- 首选唯一键 `uk_material_suppliers_preferred` **必须含 status 条件**(写在 `preferred_row` 生成列里):否则一条首选货源停用后仍占着首选位,新的首选永远插不进来。
|
||
|
||
区别在于:物料编码是业务唯一标识,停用后不该被复用;而"首选"只是当前生效的指向,停用后理应让位。
|
||
|
||
## 7. init.sql 的连带改动与数据回填
|
||
|
||
`scripts/init.sql` 同时是结构定义与数据载体(空库初始化、可重复执行)。删改 `materials` 的列会波及文件内多处,必须一并处理,否则脚本无法执行。
|
||
|
||
### 7.1 必须同步修改的位置
|
||
|
||
| # | 位置 | 改动 |
|
||
| --- | --- | --- |
|
||
| 1 | `CREATE TABLE materials`(`:19`) | 删 `supplier_id`、`manufacturer_id`、`manufacturer_model` 三列 |
|
||
| 2 | `ALTER TABLE materials`(`:30-33`) | 整段删除(2 个索引 + 2 个复合外键,对应 D3) |
|
||
| 3 | `CREATE TABLE organization_partners`(`:27`) | 删 `uk_organization_partners_org_id`(对应 D9) |
|
||
| 4 | 新建 `material_manufacturers` | 见 6.1 |
|
||
| 5 | 新建 `material_suppliers` | 见 6.2,替换原 `:34` 的建表语句 |
|
||
| 6 | 北京物料 INSERT 列清单(`:663`) | 删 `supplier_id, manufacturer_id` 两个列名 |
|
||
| 7 | 北京 16 行 VALUES | 每行删对应的 2 个值 |
|
||
| 8 | 北京 `ON DUPLICATE KEY UPDATE`(`:731-735`) | 删 `supplier_id = new.supplier_id, manufacturer_id = new.manufacturer_id` |
|
||
| 9 | 武汉物料 INSERT 列清单(`:739-741`) | 同 6 |
|
||
| 10 | 武汉 183 行 VALUES | 同 7 |
|
||
| 11 | 武汉 `ON DUPLICATE KEY UPDATE`(`:1495`) | 同 8 |
|
||
| 12 | 新增货源数据 | 161 行 `material_suppliers` + 10 行 `material_manufacturers` |
|
||
|
||
### 7.2 161 条货源数据的生成方式:临时表中转(D10)
|
||
|
||
改造后无法再用 `INSERT ... SELECT` 从 `materials` 派生货源——`init.sql` 是一遍建成的初始化脚本,新的 `CREATE TABLE materials` 已不含那两列,物料 INSERT 也不再写入它们,因此没有来源可供 SELECT。采用临时表中转,按下列顺序执行:
|
||
|
||
1. **建临时表**,列与原物料 INSERT 完全一致(含 `supplier_id` / `manufacturer_id`)。
|
||
2. **199 行 VALUES 原样插入临时表**——这部分字面量完全不动。
|
||
3. **物料进 `materials`**:从临时表按新列清单(已删列)写入,保留幂等处理。
|
||
4. **制造商进 `material_manufacturers`**:从临时表 JOIN `materials`(关联键是 `organization_id` + `material_code`)取到 `material_id`,`manufacturer_id` 非空者各生成一行。
|
||
5. **货源进 `material_suppliers`**:从临时表 JOIN `materials` 取 `material_id`,再 LEFT JOIN `material_manufacturers` 取 `material_manufacturer_id`;`supplier_id` 或 `manufacturer_id` 非空者各生成一行,`is_preferred = TRUE`、`status = 'active'`。供应商与制造商皆有的 2 条生成**一行**(同时带两个维度),而非两行。
|
||
6. **删除临时表**。
|
||
|
||
两个必须守住的点:
|
||
|
||
- **顺序不能变。** 第 5 步依赖第 3、4 步已经落库(要 JOIN 出 `material_id` 和 `material_manufacturer_id`),所以 `materials` 与 `material_manufacturers` 必须先完成写入。
|
||
- **每一步都要保持幂等。** 该脚本的既有约定是可重复执行,`materials` 用 `ON DUPLICATE KEY UPDATE`(子句中已无那两个字段),`material_manufacturers` 与 `material_suppliers` 也要用 `ON DUPLICATE KEY UPDATE` 或 `NOT EXISTS`,否则重跑会撞唯一键失败。
|
||
|
||
收益是字面量只写一次且保持原样,129 条基础货源由 SQL 自动派生,避免手工搬运的错漏;多出的 32 条拆分来源见 7.4。代价是脚本里多一段临时表逻辑。
|
||
|
||
伙伴变量(`@p_bj_00x` / `@p_wh_0xx`,定义于 `:616-652`)继续可用,因为伙伴数据本身不变。
|
||
|
||
### 7.3 型号无历史数据
|
||
|
||
`manufacturer_model` 现有数据全为 NULL(见 3.3),D4 拆出的 `material_manufacturers.model` 只有建表工作,没有迁移工作。
|
||
|
||
### 7.4 被单值字段截断的多供应商数据:回源后拆分成多行
|
||
|
||
源库里有物料用 `/` 或换行写了多家供应商,而 `materials.supplier_id` 是单值外键,迁移时只保留了第一家,其余被丢弃,共涉及 32 条物料:
|
||
|
||
| 组织 | 物料编码 | 源里的供应商 | 落库取值 |
|
||
| --- | --- | --- | --- |
|
||
| 武汉 | `B010019` | 北京致远合创电子科技有限公司、欧时电子元件(上海)有限公司 | 北京致远合创电子科技有限公司 |
|
||
| 武汉 | `A030001` | 东莞市纳百智造技术有限公司、武汉市普创模型技术有限公司 | 东莞市纳百智造技术有限公司 |
|
||
| 武汉 | `A030002` | 同上 | 东莞市纳百智造技术有限公司 |
|
||
| 武汉 | `A020001` | 同上 | 东莞市纳百智造技术有限公司 |
|
||
|
||
两个关键事实:
|
||
|
||
1. **伙伴主数据没有丢。** 被丢弃的第二家已经建成了伙伴记录,例如 `WH-P008` 欧时电子元件(上海)有限公司(`scripts/init.sql:569`)与 `WH-P010` 武汉市普创模型技术有限公司(`:571`),`is_supplier` 均为 TRUE。所以恢复多供应商关系只需补 `material_suppliers` 的行,不需要新建伙伴。
|
||
2. **但对应关系丢了。** 这两个伙伴变量 `@p_wh_008` / `@p_wh_010`(`:626`、`:628`)在全文件中只出现一次,即定义处本身,**没有任何物料引用它们**。"这两家还供了哪些物料"在 `init.sql` 中完全不存在,只能回源 SQLite 库重取。
|
||
|
||
这打破了 D10 的一个隐含前提——它假定现有 199 行字面量是完整的,但这 32 条的字面量本身已残缺(只剩第一家)。**临时表的数据源不能原样照搬,这 32 条需要从源库补全后再写入。**
|
||
|
||
待办:
|
||
|
||
- 源库位置未在仓库中找到(`sqlite-to-mysql-migration/` 目录不存在)。
|
||
- 需一并核查制造商的源字段是否也存在同样的多值截断。
|
||
|
||
#### 核实结果(2026-10-15 已查源库)
|
||
|
||
源库位置:`C:\1111\12.project\PyCharm\sqlite-to-mysql-migration`(在仓库外)。武汉原始库为 `original/material-apply-wuhan/material.db`,北京为 `backup/recovered_material_beijing.db`。
|
||
|
||
用与 `migrate_to_mysql.py` 相同的拆分规则(`PARTNER_SEPARATORS`,共 `\r\n`、`\n`、`\r`、`/`、`、`、`;`、`;` 七种)复现统计:
|
||
|
||
| 源库 | 物料 | 多供应商 | 多制造商 |
|
||
| --- | --: | --: | --: |
|
||
| 武汉 | 183 | **32** | **0** |
|
||
| 北京 | 16 | 0 | 0 |
|
||
|
||
三点结论:
|
||
|
||
1. 32 条多供应商**全部在武汉**,与 `migration-report.md` 第 3.1 节一致(该节含完整中文清单)。
|
||
2. **制造商没有同类问题。** 两个源库用全部七种分隔符检查后,多值记录均为 0,因此 `manufacturer_id` 的截断不构成额外缺口。
|
||
3. **涉及的供应商伙伴全部已建好。** 迁移脚本对拆出的**每一个**名字都建了伙伴(`migrate_to_mysql.py:323-326`:`for name in split_partners(row["supplier"]): touch(name, "is_supplier")`),不只是第一家。所以恢复多供应商关系**只需补 `material_suppliers` 的行**,不需要新增伙伴主数据。
|
||
|
||
因此回填时,这 32 条需从源库取全部供应商、按上述规则拆分后写入**多行**——实测每条恰好 2 家,故每条多出 1 行,32 条共多出 32 行;其余 167 条(199 − 32)沿用现有字面量的单值。合并后即 3.2 与 7.1 所述的 161 条。
|
||
|
||
本地库没有增量升级路径,改完删库用 `init.sql` 重建即可。
|
||
|
||
## 8. 后续(应用层,本文不覆盖)
|
||
|
||
以下事项由本次结构变更推导而来,需要另行设计,此处仅作记录:
|
||
|
||
1. **写入模式必须是差异对比,不能是整表替换。** 软停用(D6)意味着"提交列表有、库中没有"→ INSERT,"库中有、提交列表没有"→ UPDATE 为 `obsolete`,两侧都有 → UPDATE 采购条件。原本可照抄 `user_roles` 的 delete-then-insert 模式(`internal/repository/user.go:132-170`)会直接物理删除停用记录,与 D6 冲突。
|
||
2. **判重规则失效。** `FindExactDuplicates` 目前把 `supplier_id` / `manufacturer_id` 作为精确比对维度(`internal/repository/material.go:315-321`),而这两列将被删除。需要重新定义判重维度。
|
||
3. **Excel 导入表达不了三元组。** 模板的供应商/制造商两列是文本(`internal/service/material_import_excel.go:32`),且导入坚持"只匹配已有伙伴、绝不新建"(`internal/service/material_import.go:225-261`)。需要决定导入是否只填默认货源。
|
||
4. **列表与详情展示多值。** 当前列表列宽 `min-width="140"` 渲染单值文本(`frontend/src/views/Materials.vue:68-77`),详情页用 `el-descriptions` 单行(`:172-173`)。
|
||
5. **接口层必须承接的完整性校验(对应 D7)。** 每条货源写入前依次校验:伙伴存在且属于当前公司;伙伴具备对应身份(`is_supplier` / `is_manufacturer`,可复用 `internal/repository/material.go:59-67` 的 `IsPartnerForRole`);货源指向的制造商条目存在且属于**同一个物料**、同一公司;`material_manufacturers` 的物料属于当前公司。
|
||
6. **物料日志快照。** `material_logs` 的 JSON 需纳入货源集合,否则变更追溯会丢失。
|
||
7. **顺手改进 SQL 别名可读性。** `internal/repository/material.go:255-258` 目前用 `sop` / `sb` / `mop` / `mb` 这套别名,从名字看不出分别对应哪张表。物料列表查询在本次改造中必然要重写(`materials` 的旧列被删,供应商名改为从货源表取),届时一并换成 `supplier_op` / `supplier_bp` / `manufacturer_op` / `manufacturer_bp` 这类表意别名。
|