material_manage_system/docs/superpowers/plans/2026-10-15-material-sources-db-migration.md
文睿 张 9539b02f1c 同步设计与计划文档到 Task 1 的实现结论
Task 1 实现与评审暴露出文档和计划里的若干错误,据实修正:

- 设计文档 6.2 / 6.4:preferred_row 生成列由 `THEN id` 改为 `THEN 1`。
  原写法既被 MySQL 拒绝(生成列不能引用自增列,ERROR 3109),语义上也
  失效——每行 id 各不相同,唯一键永不冲突,「每物料至多一条首选」形同
  虚设。并补注它与 user_organizations 用 `THEN user_id` 的场合差异:
  那张表没有自增列,用的是业务键。
- 设计文档 6.1 / 6.2、计划 Task 1:两张关系表补
  `FOREIGN KEY(organization_id) REFERENCES organizations(id)`。改造前的
  material_suppliers 旧定义本就有这条单列外键,重写时漏掉了,而项目里
  所有按组织分表的表都有它。
- 计划 Global Constraints:删除「init.sql 必须可重复执行」这一错误约束。
  脚本本就不幂等(普通建表没有 IF NOT EXISTS、schema_migrations 的
  INSERT 也没有幂等,第二次执行在第 8 行即中止),改为只要求新增的分流
  语句自身幂等。
- 计划 Task 1 Step 5:material_manufacturers 建表位置改为
  organization_partners 之后(原位置触发前向外键 ERROR 1824)。
- 计划 Task 1 Step 7:临时表补 DEFAULT CHARSET=utf8mb4(不补会因排序
  规则与持久表不一致报 ERROR 1267)。
- 计划 Task 1 Step 13:改为在 scratch 库重放三条分流语句验证幂等,不再
  要求整脚本重跑。
2026-10-15 16:03:43 +08:00

439 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 物料多供应商与多制造商:数据库改造实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 把 `materials` 上的单值供应商/制造商改造为「物料 × 供应商 × 制造商」的三元货源模型,并在 `init.sql` 中完成 161 条货源数据的分流回填。
**Architecture:** 只改 `scripts/init.sql` 一个文件。新增 `material_manufacturers` 表,改造现有空表 `material_suppliers`,`materials` 删掉三个字段。为避免逐行修改 199 行数据字面量,用一张临时表中转:199 行原样进临时表,再分流到三张目标表。
**Tech Stack:** MySQL 8.0、单文件初始化脚本 `scripts/init.sql`(由 MySQL 官方镜像的 `/docker-entrypoint-initdb.d/` 机制执行,整个文件在同一连接内,临时表可用)。
**Spec:** `docs/superpowers/specs/2026-10-15-material-supplier-manufacturer-design.md`
## Global Constraints
- MySQL 8.0,`CHECK` 约束依赖 8.0.16+。
- 字符集统一 `utf8mb4` / `utf8mb4_unicode_ci`。
- **所有外键均为单列**,不引入任何复合外键(spec 的 D7)。公司隔离与物料一致性由应用层校验。
- `status` 取值固定为 `active` / `obsolete`(D8),不用数字。
- 货源表表名沿用 `material_suppliers`,不改名(D5)。
- `init.sql` **本来就是一次性脚本,不保证可重复执行**:普通建表没有 `IF NOT EXISTS`,`schema_migrations` 的 INSERT 也没有幂等处理,第二次执行会在第 8 行以 `Table 'users' already exists` 中止。本任务只要求**新增的分流语句自身幂等**,不要求整脚本幂等,**也不要顺手去改既有的建表语句**——那是另一个话题。
- **验证一律使用隔离库**,绝不碰真实库。本机 MySQL 在 3306,账号 `root` / 密码 `1234`。
- **`init.sql` 不能靠「指定库名」来隔离**:它第 5-6 行硬编码了 `CREATE DATABASE IF NOT EXISTS wmms;` 与 `USE wmms;`,脚本会自己切回 `wmms`,把表建到真实库上去。必须先用 `sed` 把这两行的库名替换掉,再喂给 mysql:
```bash
mysql -u root -p1234 -e "DROP DATABASE IF EXISTS wms_verify;"
sed -e "s/CREATE DATABASE IF NOT EXISTS wmms/CREATE DATABASE IF NOT EXISTS wms_verify/" \
-e "s/^USE wmms;/USE wms_verify;/" scripts/init.sql | mysql -u root -p1234
```
本计划下面每个验证步骤都已按这个写法给出。另外注意:mysql 在批处理模式下遇到 SQL 错误会**中止**,所以脚本跑挂时不用担心半途污染——但也意味着报错后必须从头重跑。
- 仓库里 `scripts/upgrades/` 目录不存在,没有增量升级路径——改完只能删库用 `init.sql` 重建。
## 本计划的文件改动总览
| 文件 | 职责 |
| --- | --- |
| `scripts/init.sql` | 唯一被改的文件。表结构定义 + 数据分流 + 货源回填全在这里 |
以下行号均以**改造前**的 `scripts/init.sql` 为准。
---
## Task 1: 表结构改造 + 基础货源派生(129 条)
**交付物:** `init.sql` 在空库上完整跑通,`materials` 199 条、`material_manufacturers` 10 条、`material_suppliers` 129 条。
**Files:**
- Modify: `scripts/init.sql`(`CREATE TABLE materials` 约 `:19`;`ALTER TABLE materials` `:30-33`;`CREATE TABLE organization_partners` 约 `:27`;`CREATE TABLE material_suppliers` `:34`;两处物料 `INSERT` 约 `:661` 与 `:738`)
**Interfaces:**
- Produces: 表 `material_manufacturers(organization_id, material_id, manufacturer_id, model, status, ...)`;表 `material_suppliers(organization_id, material_id, supplier_id, material_manufacturer_id, price, currency, minimum_order_quantity, lead_time_days, is_preferred, status, ...)`。Task 2 依赖这两张表的列名与唯一键。
- [ ] **Step 1: 建立隔离验证库**
改造前先跑通一次基线,确认验证流程可用:
```bash
mysql -u root -p1234 -e "DROP DATABASE IF EXISTS wms_verify;"
sed -e "s/CREATE DATABASE IF NOT EXISTS wmms/CREATE DATABASE IF NOT EXISTS wms_verify/" -e "s/^USE wmms;/USE wms_verify;/" scripts/init.sql | mysql -u root -p1234
mysql -u root -p1234 wms_verify -e "SELECT COUNT(*) AS materials FROM materials;"
```
预期:输出 `199`。若这一步就失败,先解决环境问题,不要往下走。
- [ ] **Step 2: 改 `CREATE TABLE materials`,删掉三个字段**
在 `scripts/init.sql:19` 那一行的建表语句中,删除三段列定义:
```
manufacturer_model VARCHAR(200),
supplier_id BIGINT NULL COMMENT 'Default supplier organization_partner ID',
manufacturer_id BIGINT NULL COMMENT 'Manufacturer organization_partner ID',
```
删完该行应为(其余列保持原样,此处只显示关键区段):
```sql
CREATE TABLE materials (id BIGINT AUTO_INCREMENT PRIMARY KEY, organization_id BIGINT NOT NULL, material_code VARCHAR(50) NOT NULL, name VARCHAR(200) NOT NULL, material_image VARCHAR(500), spec VARCHAR(200), source VARCHAR(20) NULL COMMENT '物料来源:外购或外协', remark VARCHAR(500) NULL COMMENT '物料备注,最多500字', material_type_id BIGINT, category_id BIGINT NOT NULL, base_unit_id BIGINT, unit VARCHAR(20), status VARCHAR(20) NOT NULL DEFAULT 'active', version INT NOT NULL DEFAULT 1, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_materials_org_code(organization_id,material_code), KEY idx_materials_category(category_id), FOREIGN KEY(organization_id) REFERENCES organizations(id) ON DELETE RESTRICT, FOREIGN KEY(material_type_id) REFERENCES material_types(id) ON DELETE RESTRICT, FOREIGN KEY(category_id) REFERENCES material_categories(id) ON DELETE RESTRICT, FOREIGN KEY(base_unit_id) REFERENCES units(id) ON DELETE RESTRICT) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
- [ ] **Step 3: 删除 `ALTER TABLE materials` 整段**
删除 `scripts/init.sql:29-33` 整段(4 行 `ALTER TABLE materials ADD ...`:两个索引、两个复合外键)。这段是为旧的两列加的,列已删,整段作废。
- [ ] **Step 4: 删除 `uk_organization_partners_org_id`**
在 `CREATE TABLE organization_partners`(约 `:27`)中删掉:
```
UNIQUE KEY uk_organization_partners_org_id(organization_id,id),
```
注意只删这一个唯一键,另外两个(`uk_organization_partners_org_partner`、`uk_organization_partners_org_code`)保留。
- [ ] **Step 5: 新建 `material_manufacturers`**
插在 `CREATE TABLE organization_partners` **之后**、`CREATE TABLE material_suppliers` **之前**(**首行注释不可省**,它标明这是一张关系表):
> **不能紧接 `CREATE TABLE materials`**:本表含 `FOREIGN KEY(manufacturer_id) REFERENCES organization_partners(id)`,而 `organization_partners` 创建在更后面;MySQL 不允许前向外键,实测 `ERROR 1824: Failed to open the referenced table 'organization_partners'`。该脚本里也没有 `SET FOREIGN_KEY_CHECKS=0`,所以只能靠调整建表顺序解决。
```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(organization_id) REFERENCES organizations(id), FOREIGN KEY(material_id) REFERENCES materials(id), FOREIGN KEY(manufacturer_id) REFERENCES organization_partners(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
- [ ] **Step 6: 用改造后的定义替换 `CREATE TABLE material_suppliers`**
把 `scripts/init.sql:34` 整行替换为(**首行注释不可省**,它标明这是一张关系表):
```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 1 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(organization_id) REFERENCES organizations(id), 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;
```
- [ ] **Step 7: 在物料 INSERT 之前建临时表**
在 `-- ---------- 物料 ----------` 注释块(约 `:654`)之后、第一条物料 `INSERT`(约 `:661`)之前插入:
```sql
-- ---------- 物料:经临时表中转,分流到 materials 与货源表 ----------
-- 源字面量含 supplier_id / manufacturer_id 两列,而 materials 已删掉它们,
-- 因此原样进临时表,再分别流向 materials 与货源表。见设计文档 7.2。
DROP TEMPORARY TABLE IF EXISTS tmp_materials;
CREATE TEMPORARY TABLE tmp_materials (
organization_id BIGINT, material_code VARCHAR(50), name VARCHAR(200),
spec VARCHAR(200), source VARCHAR(20), remark VARCHAR(500),
supplier_id BIGINT, manufacturer_id BIGINT, category_id BIGINT,
base_unit_id BIGINT, unit VARCHAR(20), status VARCHAR(20),
version INT, created_at DATETIME, updated_at DATETIME
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
- [ ] **Step 8: 把两处物料 INSERT 的目标表改成临时表**
北京段(约 `:661`):把 `INSERT INTO materials` 改为 `INSERT INTO tmp_materials`,**列清单与 16 行 VALUES 一字不动**。
武汉段(约 `:738`):同样把 `INSERT INTO materials` 改为 `INSERT INTO tmp_materials`,183 行 VALUES 一字不动。
- [ ] **Step 9: 删掉两处 `AS new` / `ON DUPLICATE KEY UPDATE`**
两处 INSERT 的结尾(约 `:730-735` 与 `:1494-1499`)目前形如:
```sql
'active', 1, '2026-10-10 07:54:57')
AS new
ON DUPLICATE KEY UPDATE
name = new.name, spec = new.spec, source = new.source, remark = new.remark,
supplier_id = new.supplier_id, manufacturer_id = new.manufacturer_id,
category_id = new.category_id, base_unit_id = new.base_unit_id, unit = new.unit,
status = new.status, updated_at = new.updated_at;
```
改为让 VALUES 直接以分号结束:
```sql
'active', 1, '2026-10-10 07:54:57');
```
临时表是空的,不需要幂等处理;幂等由下一步的分流语句承担。
- [ ] **Step 10: 加分流语句(三张表)并删除临时表**
紧接武汉段 INSERT 结束之后插入:
```sql
-- 分流 1:物料本体
INSERT INTO materials
(organization_id, material_code, name, spec, source, remark,
category_id, base_unit_id, unit, status, version, created_at, updated_at)
SELECT t.organization_id, t.material_code, t.name, t.spec, t.source, t.remark,
t.category_id, t.base_unit_id, t.unit, t.status, t.version, t.created_at, t.updated_at
FROM tmp_materials t
ON DUPLICATE KEY UPDATE
name = t.name, spec = t.spec, source = t.source, remark = t.remark,
category_id = t.category_id, base_unit_id = t.base_unit_id, unit = t.unit,
status = t.status, updated_at = t.updated_at;
-- 分流 2:制造商条目(有制造商的物料)
INSERT INTO material_manufacturers (organization_id, material_id, manufacturer_id, status)
SELECT t.organization_id, m.id, t.manufacturer_id, 'active'
FROM tmp_materials t
JOIN materials m ON m.organization_id = t.organization_id
AND m.material_code = t.material_code
WHERE t.manufacturer_id IS NOT NULL
ON DUPLICATE KEY UPDATE status = material_manufacturers.status;
-- 分流 3:货源(有供应商或有制造商的物料)
INSERT INTO material_suppliers
(organization_id, material_id, supplier_id, material_manufacturer_id, is_preferred, status)
SELECT t.organization_id, m.id, t.supplier_id, mm.id, TRUE, 'active'
FROM tmp_materials t
JOIN materials m ON m.organization_id = t.organization_id
AND m.material_code = t.material_code
LEFT JOIN material_manufacturers mm ON mm.organization_id = t.organization_id
AND mm.material_id = m.id
AND mm.manufacturer_id = t.manufacturer_id
WHERE t.supplier_id IS NOT NULL OR t.manufacturer_id IS NOT NULL
ON DUPLICATE KEY UPDATE is_preferred = material_suppliers.is_preferred;
DROP TEMPORARY TABLE tmp_materials;
```
三条顺序不可调换:分流 2 依赖分流 1 已写入 `materials`(要 JOIN 出 `material_id`),分流 3 依赖分流 2 已写入 `material_manufacturers`(要 JOIN 出 `material_manufacturer_id`)。
- [ ] **Step 11: 隔离库重建并核对数量**
```bash
mysql -u root -p1234 -e "DROP DATABASE IF EXISTS wms_verify;"
sed -e "s/CREATE DATABASE IF NOT EXISTS wmms/CREATE DATABASE IF NOT EXISTS wms_verify/" -e "s/^USE wmms;/USE wms_verify;/" scripts/init.sql | mysql -u root -p1234
mysql -u root -p1234 wms_verify -e "
SELECT (SELECT COUNT(*) FROM materials) AS materials,
(SELECT COUNT(*) FROM material_manufacturers) AS manufacturers,
(SELECT COUNT(*) FROM material_suppliers) AS sources;"
```
预期输出:
```
materials manufacturers sources
199 10 129
```
- [ ] **Step 12: 抽查货源的配对是否正确**
```bash
mysql -u root -p1234 wms_verify -e "
SELECT m.material_code, m.name,
sup.partner_code AS supplier, manu.partner_code AS manufacturer, ms.is_preferred
FROM material_suppliers ms
JOIN materials m ON m.id = ms.material_id
LEFT JOIN organization_partners sup ON sup.id = ms.supplier_id
LEFT JOIN material_manufacturers mm ON mm.id = ms.material_manufacturer_id
LEFT JOIN organization_partners manu ON manu.id = mm.manufacturer_id
WHERE m.material_code IN ('B010201','A020201','B010202');"
```
预期:`B010201` 与 `A020201` 各一行,供应商与制造商均为 `BJ-P001` / `BJ-P003`(原厂直销,两者相同);`B010202` 一行,供应商为空、制造商为 `BJ-P002`。
- [ ] **Step 13: 确认分流语句自身幂等**
**不要**在同一个库上重跑整个脚本——`init.sql` 整体不幂等(见 Global Constraints),那样只会得到 `ERROR 1050 at line 8: Table 'users' already exists` 然后中止,证明不了任何事。
分流语句的幂等性由「唯一键 + `ON DUPLICATE KEY UPDATE`」的写法保证。若要实证,单独建一个 scratch 库、把 `tmp_materials` 建表 + 几行样例数据 + 三条分流语句抽成一个独立脚本,连跑两轮:
```bash
mysql -u root -p1234 -e "DROP DATABASE IF EXISTS wms_idem; CREATE DATABASE wms_idem CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mysql -u root -p1234 wms_idem < /tmp/idem_probe.sql # 第一轮
mysql -u root -p1234 wms_idem < /tmp/idem_probe.sql # 第二轮
mysql -u root -p1234 wms_idem -e "SELECT (SELECT COUNT(*) FROM materials) AS m, (SELECT COUNT(*) FROM material_manufacturers) AS mm, (SELECT COUNT(*) FROM material_suppliers) AS ms;"
```
预期:两轮之后计数完全相同(不翻倍)。验证完 `DROP DATABASE wms_idem;`。
- [ ] **Step 14: 提交**
```bash
git add scripts/init.sql
git commit -F /tmp/msg.txt # 中文提交信息写入文件,避免 shell 编码问题
```
---
## Task 2: 补入 32 条被截断的多供应商(161 条)
**交付物:** 上述 32 条物料各多出一行货源,`material_suppliers` 从 129 条增至 **161 条**。
**Files:**
- Modify: `scripts/init.sql`(在 Task 1 的分流 3 之后追加一段)
- Create: `scripts/verify-multi-supplier.sql`(可选,用于核对的查询)
**Interfaces:**
- Consumes: Task 1 建立的三张表与分流语句;源库 `original/material-apply-wuhan/material.db`。
- Produces: 32 条额外的 `material_suppliers` 行。
**背景:** 源库用 7 种分隔符(`\r\n`、`\n`、`\r`、`/`、`、`、`;`、`;`)在 `supplier` 字段写多家供应商,迁移时只保留了第一家。实测 32 条,每条恰好 2 家,全在武汉。被丢弃的第二家其伙伴记录**已经建好**(`migrate_to_mysql.py:323-326` 对每个名字都建了伙伴),所以只需补货源行,不需要新建伙伴。
- [ ] **Step 1: 确认源库的多供应商条数**
源库在仓库外:`C:\1111\12.project\PyCharm\sqlite-to-mysql-migration\original\material-apply-wuhan\material.db`。用**只读**方式打开,按七种分隔符计数(用 `char()` 码点写中文分隔符,避免 shell 传中文被写坏):
```bash
DB="file:C:/1111/12.project/PyCharm/sqlite-to-mysql-migration/original/material-apply-wuhan/material.db?mode=ro"
sqlite3 "$DB" "SELECT COUNT(*) FROM material WHERE supplier LIKE '%/%' OR supplier LIKE '%'||char(10)||'%' OR supplier LIKE '%'||char(13)||'%' OR supplier LIKE '%'||char(12289)||'%' OR supplier LIKE '%'||char(65307)||'%' OR supplier LIKE '%;%';"
```
预期:`32`。若不等,停下来核对源库是不是换了版本,不要继续。
- [ ] **Step 2: 用脚本直接生成全部 32 条补入 SQL**
拆分规则与 `migrate_to_mysql.py:98` 的 `PARTNER_SEPARATORS` 完全一致,需 strip 与去重(见该文件 `split_partners`,`:134-149`)。**脚本写在临时目录,不要放进仓库。**
32 条物料总共只涉及 8 家供应商,其伙伴编码已从 `init.sql:562-595` 的 `tmp_partner_wh` 查出,写死在脚本里:
```python
"""从源库生成 32 条多供应商的补入 SQL(第 2 家起)。只读源库。"""
import sqlite3
import sys
PARTNER_SEPARATORS = ("\r\n", "\n", "\r", "/", "、", ";", ";")
# 供应商名 → 组织伙伴编码(取自 init.sql:562-595 的 tmp_partner_wh)
NAME_TO_CODE = {
"东莞市纳百智造技术有限公司": "WH-P009",
"佛山戊实五金有限公司": "WH-P017",
"北京恒瑞博远科技有限公司": "WH-P016",
"北京致远合创电子科技有限公司": "WH-P007",
"昆山聚格电子材料有限公司": "WH-P029",
"欧时电子元件(上海)有限公司": "WH-P008",
"武汉市普创模型技术有限公司": "WH-P010",
"深圳市美星新材料科技有限公司": "WH-P028",
}
def split_partners(text):
if not text:
return []
parts = [text]
for sep in PARTNER_SEPARATORS:
nxt = []
for part in parts:
nxt.extend(part.split(sep))
parts = nxt
out = []
for part in parts:
name = part.strip()
if name and name not in out:
out.append(name)
return out
DB = "file:C:/1111/12.project/PyCharm/sqlite-to-mysql-migration/original/material-apply-wuhan/material.db?mode=ro"
con = sqlite3.connect(DB, uri=True)
STMT = """INSERT INTO material_suppliers
(organization_id, material_id, supplier_id, material_manufacturer_id, is_preferred, status)
SELECT @org_wh, m.id, {var}, NULL, FALSE, 'active'
FROM materials m WHERE m.organization_id = @org_wh AND m.material_code = '{code}'
ON DUPLICATE KEY UPDATE is_preferred = material_suppliers.is_preferred;"""
count = 0
for code, supplier in con.execute(
"SELECT material_code, supplier FROM material ORDER BY material_code"
):
names = split_partners(supplier)
if len(names) <= 1:
continue
for name in names[1:]: # 第 1 家已由 Task 1 写入
tag = NAME_TO_CODE.get(name)
if tag is None:
raise SystemExit(f"未映射的供应商名:{name}")
var = "@p_" + tag.lower().replace("-p", "_") # WH-P008 → @p_wh_008
print(STMT.format(var=var, code=code))
count += 1
print(f"-- 共生成 {count} 条", file=sys.stderr)
```
运行(输出重定向到文件,避免 Windows 控制台的中文乱码):
```bash
PYTHONIOENCODING=utf-8 python -I gen_multi_supplier.py > multi_supplier.sql
```
预期 stderr 打印 `-- 共生成 32 条`。
- [ ] **Step 3: 把生成的 SQL 追加进 `init.sql`**
把 `multi_supplier.sql` 里的 32 条语句,连同一行说明注释,追加到 Task 1 的「分流 3」之后:
```sql
-- ---------- 多供应商补入:源里用分隔符写了多家,迁移时只留了第一家 ----------
-- 见设计文档 7.4。32 条,每条 2 家,故每条补 1 行,全在武汉。
```
`is_preferred = FALSE`(第一家已在 Task 1 标为 `TRUE`),`material_manufacturer_id` 为 `NULL`(这 32 条的制造商字段本来就是空的)。
**不需要新增伙伴变量。** `@p_wh_001` ~ `@p_wh_034` 已在 `init.sql:619-652` 全部定义,本次用到的 `@p_wh_007`、`@p_wh_008`、`@p_wh_010`、`@p_wh_017`、`@p_wh_029` 都在其中。
- [ ] **Step 4: 隔离库重建并核对总数**
```bash
mysql -u root -p1234 -e "DROP DATABASE IF EXISTS wms_verify;"
sed -e "s/CREATE DATABASE IF NOT EXISTS wmms/CREATE DATABASE IF NOT EXISTS wms_verify/" -e "s/^USE wmms;/USE wms_verify;/" scripts/init.sql | mysql -u root -p1234
mysql -u root -p1234 wms_verify -e "SELECT COUNT(*) AS sources FROM material_suppliers;"
```
预期:`161`。
- [ ] **Step 5: 抽查多供应商物料**
```bash
mysql -u root -p1234 wms_verify -e "
SELECT m.material_code, op.partner_code, ms.is_preferred
FROM material_suppliers ms
JOIN materials m ON m.id = ms.material_id
JOIN organization_partners op ON op.id = ms.supplier_id
WHERE m.material_code = 'B010019'
ORDER BY ms.is_preferred DESC;"
```
预期:两行,`WH-P0xx`(原保留的第一家)`is_preferred=1`,`WH-P008`(欧时电子)`is_preferred=0`。
- [ ] **Step 6: 全量核对 32 条都有 2 行**
```bash
mysql -u root -p1234 wms_verify -e "
SELECT COUNT(*) AS materials_with_two FROM (
SELECT material_id FROM material_suppliers GROUP BY material_id HAVING COUNT(*) = 2
) x;"
```
预期:`32`(这 32 条各 2 行;其余物料若有供应商则各 1 行)。
- [ ] **Step 7: 确认脚本仍可重复执行**
```bash
sed -e "s/CREATE DATABASE IF NOT EXISTS wmms/CREATE DATABASE IF NOT EXISTS wms_verify/" -e "s/^USE wmms;/USE wms_verify;/" scripts/init.sql | mysql -u root -p1234
mysql -u root -p1234 wms_verify -e "SELECT COUNT(*) FROM material_suppliers;"
```
预期:不报错,仍为 `161`。
- [ ] **Step 8: 提交**
```bash
git add scripts/init.sql
git commit -F /tmp/msg.txt
```
---
## 完成后的状态
- `materials`:199 条,已无 `supplier_id` / `manufacturer_id` / `manufacturer_model`。
- `material_manufacturers`:10 条。
- `material_suppliers`:161 条(129 基础 + 32 补入)。
- `init.sql` 仍是单文件、可重复执行、空库一次跑通。
- 后端与前端**尚未适配**,此时启动应用会因查询旧列而失败——这是预期状态,应用层改造另行计划。