- 新增 docs/superpowers/plans/2026-10-15-material-sources-db-migration.md: 拆成两个任务的实施计划(表结构改造 + 基础货源派生;32 条多供应商 补入),每个任务自带隔离库验证步骤,各自结束时数据库都自洽可用 - 修正设计文档 7.1:原改动清单按「逐行删值」列出,与 D10 的临时表 中转方案矛盾。走临时表后 199 行 VALUES 一个字都不用改,改动项由 12 条收敛为 9 条,并说明这正是选中转而非逐个删值的原因
25 KiB
物料多供应商与多制造商关系设计(数据库层)
日期: 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 |
不存在 | — |
三个必须先说明的事实:
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一处消费。material_suppliers设计不完整:它只有查询索引idx_material_suppliers_material,没有唯一键;organization_partner_id是单列外键,没有公司隔离;没有created_at/updated_at;effective_from DATE NOT NULL无默认值。由于该表是空的且无代码引用,改造代价为零。- 文档已明确
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 中物料数据的写入语句后,有两个直接决定改造方式的事实:
manufacturer_model是死列。 该列在全文件中只出现在第 19 行的CREATE TABLE:199 条物料的 INSERT 列清单里没有它,也没有任何UPDATE给它赋值,前端表单同样不含该字段(frontend/src/views/materialForm.js:3-16)。即现有数据的这一列全部为 NULL。因此 D4 拆表时没有型号数据需要迁移。- 物料插入不是裸
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"。供应商和制造商不是物料的两个平行属性,而是同一条货源记录上的两个维度。因此真正的实体是 (物料, 供应商, 制造商),价格、币种、最小订购量、供货周期都是这个三元组上的属性。
三条佐证:
- 预置的
material_suppliers把price/currency/minimum_order_quantity/lead_time_days/is_preferred全部带上了,原始设计意图就是"货源"而非"清单"。 materials.manufacturer_model是物料上的单值文本,多制造商时必然冲突(M1 是ESF32016H24、M2 是别的型号),说明型号也应随制造商走。- 现有数据中供应商与制造商为同一伙伴的那 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(新建)
物料可用的制造商及其型号。
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(改造现有空表)
每条记录表示一条货源:某供应商供某物料、供应的是哪个制造商条目、以及该货源的采购条件。
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、:739) |
目标表由 materials 改为临时表;列清单与 199 行 VALUES 原样保留 |
| 7 | 两处 ON DUPLICATE KEY UPDATE(:731-735、:1495) |
整段删除——临时表是空的,不需要幂等 |
| 8 | 新增分流语句 | 临时表 → materials / material_manufacturers / material_suppliers,见 7.2 |
| 9 | 删除临时表 | 分流完成后清掉 |
注意第 6、7 项:临时表中转的最大好处是 199 行 VALUES 一个字都不用改。 原本要逐行删掉 398 个值(199 行 × 2 列),既繁琐又容易漏;改走临时表后,那 199 行原样搬进临时表,删列的影响只体现在分流语句的列清单上。这也正是 D10 选择中转而非"逐个删值"的原因。
7.2 161 条货源数据的生成方式:临时表中转(D10)
改造后无法再用 INSERT ... SELECT 从 materials 派生货源——init.sql 是一遍建成的初始化脚本,新的 CREATE TABLE materials 已不含那两列,物料 INSERT 也不再写入它们,因此没有来源可供 SELECT。采用临时表中转,按下列顺序执行:
- 建临时表,列与原物料 INSERT 完全一致(含
supplier_id/manufacturer_id)。 - 199 行 VALUES 原样插入临时表——这部分字面量完全不动。
- 物料进
materials:从临时表按新列清单(已删列)写入,保留幂等处理。 - 制造商进
material_manufacturers:从临时表 JOINmaterials(关联键是organization_id+material_code)取到material_id,manufacturer_id非空者各生成一行。 - 货源进
material_suppliers:从临时表 JOINmaterials取material_id,再 LEFT JOINmaterial_manufacturers取material_manufacturer_id;supplier_id或manufacturer_id非空者各生成一行,is_preferred = TRUE、status = 'active'。供应商与制造商皆有的 2 条生成一行(同时带两个维度),而非两行。 - 删除临时表。
两个必须守住的点:
- 顺序不能变。 第 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 |
同上 | 东莞市纳百智造技术有限公司 |
两个关键事实:
- 伙伴主数据没有丢。 被丢弃的第二家已经建成了伙伴记录,例如
WH-P008欧时电子元件(上海)有限公司(scripts/init.sql:569)与WH-P010武汉市普创模型技术有限公司(:571),is_supplier均为 TRUE。所以恢复多供应商关系只需补material_suppliers的行,不需要新建伙伴。 - 但对应关系丢了。 这两个伙伴变量
@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 |
三点结论:
- 32 条多供应商全部在武汉,与
migration-report.md第 3.1 节一致(该节含完整中文清单)。 - 制造商没有同类问题。 两个源库用全部七种分隔符检查后,多值记录均为 0,因此
manufacturer_id的截断不构成额外缺口。 - 涉及的供应商伙伴全部已建好。 迁移脚本对拆出的每一个名字都建了伙伴(
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. 后续(应用层,本文不覆盖)
以下事项由本次结构变更推导而来,需要另行设计,此处仅作记录:
- 写入模式必须是差异对比,不能是整表替换。 软停用(D6)意味着"提交列表有、库中没有"→ INSERT,"库中有、提交列表没有"→ UPDATE 为
obsolete,两侧都有 → UPDATE 采购条件。原本可照抄user_roles的 delete-then-insert 模式(internal/repository/user.go:132-170)会直接物理删除停用记录,与 D6 冲突。 - 判重规则失效。
FindExactDuplicates目前把supplier_id/manufacturer_id作为精确比对维度(internal/repository/material.go:315-321),而这两列将被删除。需要重新定义判重维度。 - Excel 导入表达不了三元组。 模板的供应商/制造商两列是文本(
internal/service/material_import_excel.go:32),且导入坚持"只匹配已有伙伴、绝不新建"(internal/service/material_import.go:225-261)。需要决定导入是否只填默认货源。 - 列表与详情展示多值。 当前列表列宽
min-width="140"渲染单值文本(frontend/src/views/Materials.vue:68-77),详情页用el-descriptions单行(:172-173)。 - 接口层必须承接的完整性校验(对应 D7)。 每条货源写入前依次校验:伙伴存在且属于当前公司;伙伴具备对应身份(
is_supplier/is_manufacturer,可复用internal/repository/material.go:59-67的IsPartnerForRole);货源指向的制造商条目存在且属于同一个物料、同一公司;material_manufacturers的物料属于当前公司。 - 物料日志快照。
material_logs的 JSON 需纳入货源集合,否则变更追溯会丢失。 - 顺手改进 SQL 别名可读性。
internal/repository/material.go:255-258目前用sop/sb/mop/mb这套别名,从名字看不出分别对应哪张表。物料列表查询在本次改造中必然要重写(materials的旧列被删,供应商名改为从货源表取),届时一并换成supplier_op/supplier_bp/manufacturer_op/manufacturer_bp这类表意别名。