-- ============================================================================= -- 借库转交(Borrow Transfer)一期:底层模型改造 + 转交/归还流水表 + 权限码 -- -- 背景 -- 原 trans_borrow 是**单行记录模式**,身份维度只有一个 borrower_name 字符串: -- · 「谁借的」与「谁现在还拿着」是同一个字段,无法表达持有权变更; -- · 归还时 return_time / return_operator / return_signature 被**逐次覆盖**, -- 部分归还下「谁在什么时候还了多少」永久丢失(失忆症)。 -- 本迁移为转交功能打底,并把身份锚点从「姓名」升级为「ID + 姓名快照」。 -- -- 内容分五段: -- 1) trans_borrow 补三列(borrower_id / current_holder_id / current_holder_name) -- 2) 新建 trans_borrow_transfer(转交流水) -- 3) 新建 trans_borrow_return (归还流水,治失忆症) -- 4) 历史数据回填(仅姓名能唯一映射到 sys_user 的行) -- 5) 注册 borrow_transfer 权限码 -- -- --------------------------------------------------------------------------- -- 1) 为什么 _id 列允许 NULL,而新写入强制带 ID -- 存量 85 行只有 borrower_name(纯姓名),其中 17/18 个姓名能唯一映射到 -- sys_user(username 形如「姓名/拼音」)。无法映射的(如「测试01」)保持 -- NULL —— 不回填错误数据,也不阻断迁移。 -- 应用层(execute_dispatch)对**新数据**强制要求 borrower_id, -- 故 NULL 只可能出现在迁移前的历史行上,是「历史未锚定」的显式标记。 -- -- 2) 为什么 current_holder 只回填「未归还」的行 -- 语义约定:current_holder_id 表示**当前实际持有人**。 -- · 未归还的行 → 持有人 = 借用人(转交会在此之上继续推进); -- · 已全部归还的行 → 物品已回库,无人持有,保持 NULL(借用人仍记在 -- borrower_id,作为历史)。 -- 这样「current_holder_id IS NOT NULL」本身就是「仍在某人手上」的有效信号, -- 归还校验(returner_id 必须等于 current_holder_id)不会对已结清单据误触发。 -- -- 3) 为什么借用/归还/转交三张表都不建外键 -- 与 trans_return / trans_defective_goods 的处理一致:库存行会被入库模块 -- 物理删除(实测已有悬空),台账表必须能独立存活,隔离判定不依赖 join 链。 -- 故只建索引,不建 FK 约束。 -- -- 幂等:全部带 IF NOT EXISTS / WHERE NOT EXISTS,可重复执行。 -- 执行:docker exec -i inventory_db psql -U test -d inventory_system < 本文件 -- ============================================================================= BEGIN; -- --------------------------------------------------------------------------- -- 1) trans_borrow 补列 -- --------------------------------------------------------------------------- -- numeric(19,4) 与 quantity / returned_quantity 同型,保证转交与归还的 -- 数量比较(qty = quantity - returned_quantity)不引入浮点误差。 ALTER TABLE trans_borrow ADD COLUMN IF NOT EXISTS borrower_id integer, ADD COLUMN IF NOT EXISTS current_holder_id integer, ADD COLUMN IF NOT EXISTS current_holder_name varchar(100); COMMENT ON COLUMN trans_borrow.borrower_id IS '初始借用人ID(sys_user.id)。历史数据回填,无法唯一映射时为 NULL'; COMMENT ON COLUMN trans_borrow.current_holder_id IS '当前实际持有人ID。转交会推进此列;已全部归还时为 NULL'; COMMENT ON COLUMN trans_borrow.current_holder_name IS '当前持有人姓名快照。供列表展示,避免逐行回查 sys_user(N+1)'; CREATE INDEX IF NOT EXISTS ix_trans_borrow_borrower_id ON trans_borrow (borrower_id); CREATE INDEX IF NOT EXISTS ix_trans_borrow_current_holder ON trans_borrow (current_holder_id); -- --------------------------------------------------------------------------- -- 2) trans_borrow_transfer —— 转交流水 -- -- 一行 = 一次转交动作,**不做覆盖式更新**(与 trans_return 同一范式)。 -- 单行模式的主表只能存「当前持有人」,A→B→C 的链路全靠本表回答。 -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS trans_borrow_transfer ( id serial PRIMARY KEY, borrow_id integer NOT NULL, -- 关联 trans_borrow.id from_user_id integer, -- 转出人(转交前的 current_holder) from_user_name varchar(100), to_user_id integer, -- 接收人(转交后的 current_holder) to_user_name varchar(100), transfer_qty numeric(19,4) NOT NULL DEFAULT 0, -- 一期恒等于整单待还量 transfer_time timestamp without time zone DEFAULT CURRENT_TIMESTAMP, operator_name varchar(100), -- 执行转交操作的库管 remark text ); COMMENT ON TABLE trans_borrow_transfer IS '借库转交流水:一行=一次转交,不做覆盖式更新,承载 A→B→C 的完整持有链'; COMMENT ON COLUMN trans_borrow_transfer.transfer_qty IS '转交数量。一期仅支持整单全量转交,故恒等于 quantity - returned_quantity'; CREATE INDEX IF NOT EXISTS ix_trans_borrow_transfer_borrow ON trans_borrow_transfer (borrow_id); CREATE INDEX IF NOT EXISTS ix_trans_borrow_transfer_time ON trans_borrow_transfer (transfer_time); -- --------------------------------------------------------------------------- -- 3) trans_borrow_return —— 归还流水(治失忆症) -- -- 原先部分归还时主表的 return_time / return_operator / return_signature -- 被逐次覆盖,「谁在什么时候还了多少」只剩最后一次。本表按次记录。 -- -- ★ 主表字段的去留:returned_quantity / is_returned / status 仍在主表累加 -- (它们是**累计快照**,聚合语义正确);return_time / return_operator / -- return_signature 降级为「最近一次归还」展示快照(records.vue 依赖它们 -- 显示归还人/归还时间),权威的逐次明细以本表为准。 -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS trans_borrow_return ( id serial PRIMARY KEY, borrow_id integer NOT NULL, -- 关联 trans_borrow.id returner_id integer, -- 实际来还东西的人(校验过==current_holder) return_qty numeric(19,4) NOT NULL DEFAULT 0, return_time timestamp without time zone DEFAULT CURRENT_TIMESTAMP, operator_name varchar(100) -- 执行还库操作的库管 ); COMMENT ON TABLE trans_borrow_return IS '借库归还流水:一行=一次归还,替代主表的覆盖式写入,保留分批归还历史'; COMMENT ON COLUMN trans_borrow_return.returner_id IS '实际归还人ID。写入前已强校验其等于 trans_borrow.current_holder_id'; COMMENT ON COLUMN trans_borrow_return.operator_name IS '执行还库操作的库管姓名(窗口经手人),与 returner_id 是两个人'; CREATE INDEX IF NOT EXISTS ix_trans_borrow_return_borrow ON trans_borrow_return (borrow_id); CREATE INDEX IF NOT EXISTS ix_trans_borrow_return_time ON trans_borrow_return (return_time); CREATE INDEX IF NOT EXISTS ix_trans_borrow_return_returner ON trans_borrow_return (returner_id); -- --------------------------------------------------------------------------- -- 4) 历史数据回填 -- 仅在「姓名在 sys_user 中唯一命中」时回填,杜绝重名错绑。 -- --------------------------------------------------------------------------- -- 4.1 borrower_id:全部可唯一映射的行(含已归还,作历史留痕) UPDATE trans_borrow tb SET borrower_id = u.id FROM sys_user u WHERE tb.borrower_id IS NULL AND tb.borrower_name IS NOT NULL AND split_part(u.username, '/', 1) = tb.borrower_name AND (SELECT count(*) FROM sys_user u2 WHERE split_part(u2.username, '/', 1) = tb.borrower_name) = 1; -- 4.2 current_holder:仅未归还的行(已归还=物品已回库,无人持有) -- 姓名快照同步写入;不信任 borrower_name 的大小写/空格,统一取 sys_user 口径。 UPDATE trans_borrow tb SET current_holder_id = u.id, current_holder_name = split_part(u.username, '/', 1) FROM sys_user u WHERE tb.current_holder_id IS NULL AND tb.is_returned = FALSE AND tb.borrower_name IS NOT NULL AND split_part(u.username, '/', 1) = tb.borrower_name AND (SELECT count(*) FROM sys_user u2 WHERE split_part(u2.username, '/', 1) = tb.borrower_name) = 1; -- --------------------------------------------------------------------------- -- 5) 注册 borrow_transfer 权限码 -- -- ★ 为什么用**无冒号**形式而不是 op_borrow:transfer: -- 见 add_outbound_return_perm.sql 的论证 —— 形如 : 的码会 -- 命中后端 _expand_operation_perms() 的前缀桥接,使持有 op_borrow:operation -- 的角色被自动放行;无冒号码不触发桥接,与前端 hasPermission 精确匹配一致。 -- -- ★ 为什么授予面与 op_borrow:operation 完全一致(含 OUTBOUND): -- 「能执行借出」的人就是窗口前处理实物交接的人,转交是同一动作的延伸。 -- 若业务上要求收紧为仅库管,从下方 VALUES 中移除 'OUTBOUND' 即可。 -- --------------------------------------------------------------------------- INSERT INTO sys_element (menu_code, name, code, element_type) SELECT 'op_borrow', '借库转交(库管)', 'borrow_transfer', 'operation' WHERE NOT EXISTS ( SELECT 1 FROM sys_element WHERE code = 'borrow_transfer' ); INSERT INTO sys_role_permission (role_code, target_code, type, company_name) SELECT v.role_code, 'borrow_transfer', 'element', NULL FROM (VALUES ('SUPER_ADMIN'), ('SUPERVISOR'), ('WAREHOUSE_MGR'), ('OUTBOUND')) AS v(role_code) WHERE NOT EXISTS ( SELECT 1 FROM sys_role_permission r WHERE r.role_code = v.role_code AND r.target_code = 'borrow_transfer' AND r.type = 'element' ); COMMIT; -- ============================================================================= -- 执行后核对 -- ============================================================================= \echo '--- 1) trans_borrow 三个新列 ---' SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'trans_borrow' AND column_name IN ('borrower_id','current_holder_id','current_holder_name') ORDER BY column_name; \echo '--- 2) 两张新表 ---' SELECT table_name FROM information_schema.tables WHERE table_name IN ('trans_borrow_transfer','trans_borrow_return') ORDER BY table_name; \echo '--- 3) 回填覆盖率(应 total=85,borrower_id 回填 84,holder 回填=未归还行数)---' SELECT count(*) AS total, count(borrower_id) AS borrower_id_filled, count(*) FILTER (WHERE borrower_id IS NULL) AS borrower_id_null, count(*) FILTER (WHERE is_returned = FALSE) AS unreturned, count(current_holder_id) AS holder_filled FROM trans_borrow; \echo '--- 4) 未能锚定的历史行(预期:测试01)---' SELECT id, borrow_no, borrower_name, is_returned FROM trans_borrow WHERE borrower_id IS NULL ORDER BY id; \echo '--- 5) 权限码与授权(应 1 元素 + 4 角色)---' SELECT e.code, e.name, e.menu_code, e.element_type FROM sys_element e WHERE e.code = 'borrow_transfer'; SELECT role_code, COALESCE(company_name, '') AS company_name FROM sys_role_permission WHERE target_code = 'borrow_transfer' ORDER BY role_code; -- ============================================================================= -- 回滚段 -- ============================================================================= -- BEGIN; -- DELETE FROM sys_role_permission WHERE target_code = 'borrow_transfer'; -- DELETE FROM sys_element WHERE code = 'borrow_transfer'; -- DROP TABLE IF EXISTS trans_borrow_return; -- DROP TABLE IF EXISTS trans_borrow_transfer; -- ALTER TABLE trans_borrow -- DROP COLUMN IF EXISTS borrower_id, -- DROP COLUMN IF EXISTS current_holder_id, -- DROP COLUMN IF EXISTS current_holder_name; -- COMMIT;