Files
KCGL/db_migrations/phase15_warehouse_location_company.sql
yueli 4890b996c4 feat(warehouse): 库位按公司隔离,并重做批量生成与库位管理
隔离(db_migrations/phase15_warehouse_location_company.sql)
- sys_warehouse_location 新增 company_name,NULL = 全局共享
- 存量 3371 行按 full_path 顶段首字母回填:Y*→IRIS(500),L*/C*→LICA(2871)
- 读范围 = 本公司 + 全局;写范围 = 仅本公司
  删除/改名/批量生成都加归属校验 —— 只隔离读接口等于没隔离,
  否则 LICA 一条 DELETE 就能删掉 IRIS 整棵库位树
- /tree 与 /children 补上缺失的 @jwt_required()
- 公司名走白名单校验,非法值 400(拼错会造出谁都看不见的库位,且无从排查)
- 超管可改归属,级联整棵子树(归属撕裂会让父子可见性错乱)

改名补偿
- 级联重写子孙 full_path(原实现只改自己一行,子孙仍挂旧前缀,
  而库存快照存的正是 full_path,改一次名整棵子树就和台账对不上)
- 同事务改写在库台账 stock_buy/semi/product/adjustment 的快照,
  以及未结束盘点会话的 scope_config.locations
- 刻意不含 trans_outbound / trans_borrow 等历史单据(改历史 = 篡改操作记录),
  也不含 trans_repair.customer_location(那是客户地址不是库位)

批量生成
- 起点支持三种:勾选的一批库位 / 某子树下所有末级 / 单个父级
- 已存在同名跳过 —— full_path 上有唯一约束 uniq_full_path,
  不跳过会整单 500 并整体回滚,连本该新建的也一并丢失
- 数量上限改按实际创建数(各层之和 × 起点数),不再是各层相乘
  (旧口径低估,3000 的上限因此能被悄悄突破)
- 前端加生成结构预览:逐层数量 + 完整路径示例,边填边看

前端清理
- 删除 utils/warehouseCompany.ts 前缀白名单,WarehouseSelector 与三个
  入库页改为纯渲染后端数据(隔离已下沉到后端)
- 盘点页 loadLocationTree 必须带 company_name:跨域角色在后端返回 None,
  不带就会拉全量、盘点范围脱离所选公司
- 盘点页切公司时清库位树缓存(原来会继续沿用上一个公司的树)
- 修复批量删除只取叶子导致中间层节点残留成孤儿
2026-09-29 12:03:53 +08:00

149 lines
7.2 KiB
PL/PgSQL
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.

-- =============================================================================
-- 库位归属公司化:sys_warehouse_location 增加 company_name
--
-- 背景
-- 库位字典表全表共享,IRIS 与 LICA 都能看到对方的库位。此前的「隔离」是
-- 前端硬编码前缀白名单(inventory-web/src/utils/warehouseCompany.ts:
-- IRIS→['Y'],LICA→['C','L']),改前端即可绕过,且新增公司必须改代码。
-- 更有甚者,/tree 与 /children 两个读接口连 @jwt_required() 都没有。
--
-- 本次把「归属」从命名约定升级为数据字段,隔离下沉到后端。
--
-- 口径(依据 2026-09-29 全表摸底,实测 3371 行 / 33 个顶级前缀)
-- 顶级前缀首字母唯一决定归属,无第三种取值、无跨司同名前缀:
-- Y1~Y8 → IRIS 500 行
-- L1~L14 / C10,C11 / CC1~CC9 → LICA 2871 行
-- 故按「full_path 顶段首字母」回填,而非裸 LIKE 'Y%':
-- split_part 取顶段是精确匹配,不受 LIKE 通配符(_ / %)影响 ——
-- 库位名由用户自由输入,含下划线时 LIKE 会误命中。
--
-- ★ L14(仅 2 个节点,同族其余均为 217)与 CC1~CC9(各 4 个节点)结构异常,
-- 疑似历史遗留/半截生成。按前缀归入 LICA,但**建议业务侧复核后**再定,
-- 若需改归属,单行 UPDATE 即可(见文件末「手工修正」段)。
--
-- 安全性
-- 纯追加式 DDL,无 NOT NULL、无 DEFAULT,既有行取值 NULL。
-- NULL 是**有意义**的取值 —— 表示全局共享(两司均可见),不是「未归类」。
-- 本迁移把全部存量行都收敛到 IRIS/LICA,迁移后应为 0 行 NULL。
--
-- 幂等
-- ADD COLUMN IF NOT EXISTS + CREATE TABLE IF NOT EXISTS +
-- UPDATE 带 company_name IS NULL 条件,可重复执行;
-- 重复执行不会覆盖已归类(含人工修正过)的值。
--
-- 执行(dev 库;生产为 *_prod 容器与 pgdata_prod,须由部署流程另行执行)
-- docker exec -i inventory_db psql -U test -d inventory_system \
-- -v ON_ERROR_STOP=1 < 本文件
--
-- ★ 上线顺序:必须先跑本文件,再发布带 company_name 列的代码。
-- 反过来会让 /api/v1/warehouse/* 全部 500(模型引用了不存在的列)。
-- =============================================================================
BEGIN;
-- ---------------------------------------------------------------------------
-- 0) 备份原表(3371 行,代价可忽略)。回滚时从这里捞。
-- IF NOT EXISTS 保证重复执行时**不会**用迁移后的数据覆盖掉原始备份。
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS sys_warehouse_location_bak_20260929 AS
SELECT * FROM sys_warehouse_location;
-- ---------------------------------------------------------------------------
-- 1) 归属列
-- ---------------------------------------------------------------------------
ALTER TABLE sys_warehouse_location
ADD COLUMN IF NOT EXISTS company_name varchar(50);
COMMENT ON COLUMN sys_warehouse_location.company_name IS
'公司归属:IRIS/LICA;NULL = 全局共享(两司均可见)。由 warehouse.py 依据 JWT claim company_name 过滤';
CREATE INDEX IF NOT EXISTS ix_wh_loc_company
ON sys_warehouse_location (company_name);
-- ---------------------------------------------------------------------------
-- 2) 存量回填:按 full_path 顶段首字母
-- WHERE company_name IS NULL 保证幂等,且不覆盖已归类/人工修正过的值。
-- full_path 为 NULL 的行 split_part 返回 NULL,不命中任何分支 → 保持 NULL
-- (即全局共享)。摸底确认当前无此类行。
-- ---------------------------------------------------------------------------
UPDATE sys_warehouse_location
SET company_name = 'IRIS'
WHERE company_name IS NULL
AND upper(left(split_part(full_path, '/', 1), 1)) = 'Y';
UPDATE sys_warehouse_location
SET company_name = 'LICA'
WHERE company_name IS NULL
AND upper(left(split_part(full_path, '/', 1), 1)) IN ('L', 'C');
COMMIT;
-- =============================================================================
-- 执行后核对
-- =============================================================================
SELECT '=== 1) 归属分布(期望 IRIS=500 / LICA=2871 / NULL=0)===' AS "核对项";
SELECT COALESCE(company_name, '(NULL=全局)') AS 归属,
count(*) AS 节点数
FROM sys_warehouse_location
GROUP BY company_name
ORDER BY 节点数 DESC;
SELECT '=== 2) ★ 仍未归类的行(应为 0;非 0 需人工判定)===' AS "核对项";
SELECT id, parent_id, name, full_path, level
FROM sys_warehouse_location
WHERE company_name IS NULL
ORDER BY id;
SELECT '=== 3) 顶级节点归属明细(应 33 行,Y*→IRIS,其余→LICA)===' AS "核对项";
SELECT COALESCE(company_name, '(NULL)') AS 归属,
split_part(full_path, '/', 1) AS 顶级库位,
count(*) AS 含子孙节点数
FROM sys_warehouse_location
WHERE parent_id IS NULL
GROUP BY company_name, split_part(full_path, '/', 1)
ORDER BY 归属, 顶级库位;
SELECT '=== 4) ★ 跨公司父子节点(应为 0;非 0 说明回填把某棵树切开了)===' AS "核对项";
SELECT c.id AS 子节点id, c.full_path AS 子路径, c.company_name AS 子公司,
p.id AS 父节点id, p.full_path AS 父路径, p.company_name AS 父公司
FROM sys_warehouse_location c
JOIN sys_warehouse_location p ON c.parent_id = p.id
WHERE c.company_name IS DISTINCT FROM p.company_name
ORDER BY c.id;
SELECT '=== 5) 疑似重复的 (company_name, parent_id, name)(>=2 表示可考虑加唯一约束)===' AS "核对项";
SELECT company_name, parent_id, name, count(*) AS 重复数
FROM sys_warehouse_location
GROUP BY company_name, parent_id, name
HAVING count(*) >= 2
ORDER BY 重复数 DESC, name;
-- =============================================================================
-- 手工修正(按需单独执行,本迁移不自动跑)
-- =============================================================================
-- 例:L14 结构异常,若业务确认它其实不属于 LICA:
-- UPDATE sys_warehouse_location SET company_name = 'IRIS'
-- WHERE full_path = 'L14' OR full_path LIKE 'L14/%';
--
-- 例:某个库位要改成全局共享:
-- UPDATE sys_warehouse_location SET company_name = NULL WHERE id = ?;
-- =============================================================================
-- 回滚段(仅撤销本迁移)
-- =============================================================================
-- BEGIN;
-- -- 方式一:从备份表整体还原(会丢弃迁移后新增的库位)
-- -- TRUNCATE sys_warehouse_location;
-- -- INSERT INTO sys_warehouse_location SELECT * FROM sys_warehouse_location_bak_20260929;
-- --
-- -- 方式二:只去列(应用层须同步回滚,否则 /warehouse/* 报列不存在)
-- DROP INDEX IF EXISTS ix_wh_loc_company;
-- ALTER TABLE sys_warehouse_location DROP COLUMN IF EXISTS company_name;
-- COMMIT;
--
-- 备份表确认无误后可另行清理:
-- DROP TABLE sys_warehouse_location_bak_20260929;