Files
KCGL/db_migrations/deploy_borrow_transfer_all.sql
yueli b26b124f8b chore(db): 部署脚本改用通用 SQL 核对段,兼容 DataGrip
psql 的 \echo 是元命令,DataGrip / DBeaver 不认,整份执行会直接报语法错误。
核对段改用 SELECT '常量' AS "核对项" 输出标签行,任何客户端都能跑
(psql 下效果相同)。同步修正头部说明中「需用 psql」的过期表述。

已在临时库重跑验证:零报错、核对结果与改前一致、重跑幂等。
2026-09-17 10:51:31 +08:00

330 lines
18 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.

-- =============================================================================
-- 借库转交Borrow Transfer完整部署脚本
--
-- 本文档把本轮全部库变更按依赖顺序合并为一个脚本,可直接在生产执行。
-- 对应开发环境已执行的 6 个脚本phase4 / 4b / 4c / 4d / 4e / 4f。
--
-- 执行
-- docker exec -i <db容器> psql -U <用户> -d <库名> -v ON_ERROR_STOP=1 < 本文件
-- 脚本不含 psql 专有元命令psql / DataGrip / DBeaver 均可直接整份执行。
--
-- 幂等
-- 全部 DDL 带 IF NOT EXISTS可重复执行。
-- ★ 但三个「回填」段设计为**在新代码上线前执行一次**,见各自注释中的警示。
--
-- 影响面
-- 纯增量:只加列、加表、加索引,不改任何既有列的类型或含义,
-- 不改动任何库存数量字段。全部包在一个事务里,失败自动回滚。
-- =============================================================================
BEGIN;
-- =============================================================================
-- 第 1 段trans_borrow 补 4 列(身份锚点 + 发货操作人)
--
-- 借用人身份从「姓名」升级为「ID 锚点」:姓名无法唯一锚定一个人(重名即
-- 责任链断裂。current_holder_* 表示「东西现在在谁手上」,转交会推进它。
--
-- dispatch_operator 补齐「谁经手发货」—— 此前 execute_dispatch 收到该参数
-- 却从未落库。
-- =============================================================================
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),
ADD COLUMN IF NOT EXISTS dispatch_operator varchar(100);
COMMENT ON COLUMN trans_borrow.borrower_id IS
'初始借用人IDsys_user.id。历史数据回填无法唯一映射时为 NULL';
COMMENT ON COLUMN trans_borrow.current_holder_id IS
'当前实际持有人ID。转交会推进此列已全部归还时为 NULL';
COMMENT ON COLUMN trans_borrow.current_holder_name IS
'当前持有人姓名快照。供列表展示,避免逐行回查 sys_userN+1';
COMMENT ON COLUMN trans_borrow.dispatch_operator IS
'执行借出(扫码发货)的库管操作人姓名。仅展示/追溯用,不参与权限判定;'
'该列上线前的历史记录为 NULL';
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 段:转交流水表(最终形态一次建全)
--
-- 转交粒度 = **明细行**borrow_id物理现场经常只转交部分工具
-- (借 2 件只把 1 件给别人),一张单下不同明细归属不同持有人是正常业务形态。
--
-- 状态机PENDING ──accept──> ACCEPTED主表 current_holder 正式转移)
-- └──reject──> REJECTED主表不动责任仍在原持有人
--
-- 不建外键:与 trans_return / trans_defective_goods 一致 —— 库存行会被
-- 入库模块物理删除,台账表必须能独立存活。
-- =============================================================================
CREATE TABLE IF NOT EXISTS trans_borrow_transfer (
id serial PRIMARY KEY,
borrow_id integer NOT NULL, -- 转交目标明细行 trans_borrow.id
from_user_id integer, -- 发起人(= 发起时的当前持有人)
from_user_name varchar(100),
to_user_id integer, -- 接收人
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), -- 发起人(完整 username存档备查
remark text, -- 转交备注(发起时填写)
borrow_no varchar(100) -- 单据归属,仅供分组展示
);
-- 补列:兼容本表已存在的场景
ALTER TABLE trans_borrow_transfer
ADD COLUMN IF NOT EXISTS borrow_no varchar(100),
ADD COLUMN IF NOT EXISTS status varchar(20) NOT NULL DEFAULT 'PENDING',
ADD COLUMN IF NOT EXISTS reject_seen_at timestamp without time zone,
ADD COLUMN IF NOT EXISTS reject_reason text;
COMMENT ON COLUMN trans_borrow_transfer.borrow_id IS
'转交目标明细行IDtrans_borrow.id。转交粒度 = 明细行,一行最多一条待接收流水';
COMMENT ON COLUMN trans_borrow_transfer.borrow_no IS
'借用单号。仅用于单据归属与列表分组展示;覆盖范围是 borrow_id 指向的单个明细行';
COMMENT ON COLUMN trans_borrow_transfer.status IS
'PENDING 待接收 / ACCEPTED 已接收(主表已转移)/ REJECTED 已拒绝(主表未动)';
COMMENT ON COLUMN trans_borrow_transfer.reject_seen_at IS
'发起方看到「被拒绝」提醒并确认的时间。NULL 且 status=REJECTED 表示尚未告知';
COMMENT ON COLUMN trans_borrow_transfer.reject_reason IS
'拒收原因,独立成列(此前拼在 remark 里,前端无法区分转交备注与拒收原因)';
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);
CREATE INDEX IF NOT EXISTS ix_trans_borrow_transfer_no ON trans_borrow_transfer (borrow_no);
CREATE INDEX IF NOT EXISTS ix_trans_borrow_transfer_status ON trans_borrow_transfer (status);
CREATE INDEX IF NOT EXISTS ix_trans_borrow_transfer_to_user ON trans_borrow_transfer (to_user_id, status);
CREATE INDEX IF NOT EXISTS ix_trans_borrow_transfer_borrow_status ON trans_borrow_transfer (borrow_id, status);
CREATE INDEX IF NOT EXISTS ix_trans_borrow_transfer_from_status ON trans_borrow_transfer (from_user_id, status);
-- =============================================================================
-- 第 3 段:归还流水表
--
-- 原先部分归还时,主表的 return_time / return_operator / return_signature
-- 被**逐次覆盖**,「谁在什么时候还了多少」永久丢失。本表按次记录根治。
--
-- 主表字段保持维护returned_quantity 是累计快照(列表「未还/已还」判定
-- 依赖它return_time 等降级为「最近一次归还」展示快照;逐次权威以本表为准。
-- =============================================================================
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 段:回填 trans_borrow 的身份锚点(存量数据)
--
-- ⚠ 幂等:仅回填仍为 NULL 的行,重跑无副作用。
-- 只回填「姓名在 sys_user 中唯一命中」的行,杜绝重名错绑;命中不了就留 NULL
-- 不猜测、不阻断 —— 前端对 NULL 有「历史数据」的降级处理。
-- =============================================================================
-- 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 OR u.username = tb.borrower_name)
AND (SELECT count(*) FROM sys_user u2
WHERE split_part(u2.username, '/', 1) = tb.borrower_name
OR u2.username = tb.borrower_name) = 1;
-- 4.2 current_holder仅未归还的行已归还 = 物品已回库,无人持有,保持 NULL
-- 这样「current_holder_id IS NOT NULL」本身就是「仍在某人手上」的有效信号
-- 归还校验不会对已结清单据误触发。
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 OR u.username = tb.borrower_name)
AND (SELECT count(*) FROM sys_user u2
WHERE split_part(u2.username, '/', 1) = tb.borrower_name
OR u2.username = tb.borrower_name) = 1;
-- =============================================================================
-- 第 5 段:回填转交流水的 borrow_no 与状态(仅针对旧版遗留流水)
--
-- ⚠⚠ 本段设计为**在新代码上线前执行一次**。
-- 旧版实现是「发起即生效」(写流水的同时就改了主表 current_holder
-- 故这些遗留流水应标为 ACCEPTED若标 PENDING接收人会收到一条
-- 早已生效的待办。
--
-- 安全护栏:只处理 borrow_no IS NULL 的行 —— 那是「本列刚加、由旧代码写入」
-- 的唯一特征。新代码写入的流水必然带 borrow_no因此**重跑不会误伤**
-- 上线后产生的真实待接收记录。
--
-- ★ status 不能一律填 ACCEPTED旧结构表里 status 是刚加的列、全是默认值
-- PENDING若把遗留行统统标成 ACCEPTED后面按 status='REJECTED' 做的
-- 回填就一条都匹配不到(实测踩到)。真正的信号在备注里 ——
-- 旧版拒收会把原因拼成 "[拒绝原因] xxx",据此还原真实状态。
-- =============================================================================
UPDATE trans_borrow_transfer t
SET borrow_no = tb.borrow_no,
status = CASE WHEN t.remark LIKE '%[拒绝原因] %'
THEN 'REJECTED' ELSE 'ACCEPTED' END
FROM trans_borrow tb
WHERE t.borrow_id = tb.id
AND t.borrow_no IS NULL;
-- ★ 兜底:上面按 borrow_id 关联,若某条流水的来源借用行**已被删除**
-- JOIN 匹配不上,它会永久停在默认的 PENDING —— 在接收人那里变成一条
-- 谁也处理不掉的幽灵待办(点开发现物品早已不存在)。
-- 这里把「没有单号」的遗留行一律结掉;新代码写入的流水必然带 borrow_no
-- 不会被误伤。
UPDATE trans_borrow_transfer
SET status = CASE WHEN remark LIKE '%[拒绝原因] %'
THEN 'REJECTED' ELSE 'ACCEPTED' END
WHERE borrow_no IS NULL
AND status = 'PENDING';
-- =============================================================================
-- 第 6 段:回填 reject_seen_at存量已拒绝的流水标记为「已告知」
--
-- ⚠ 本段同样设计为**上线前执行一次**。
-- 存量拒绝产生于本功能上线之前,追溯提醒只会打扰;新产生的拒绝才会触发提醒。
--
-- ⚠ 若在功能已投产后重跑:会把当时「真实的、尚未告知发起方」的拒绝记录
-- 一并标记为已告知,导致发起方收不到提醒(提醒接口再无第二次机会)。
-- 故请务必在维护窗口、新代码上线前执行。
-- =============================================================================
UPDATE trans_borrow_transfer
SET reject_seen_at = CURRENT_TIMESTAMP
WHERE status = 'REJECTED'
AND reject_seen_at IS NULL;
-- =============================================================================
-- 第 7 段:回填 reject_reason从 remark 里拆出被拼接的拒收原因)
--
-- 旧实现把原因拼进备注:"3333\n[拒绝原因] 5555",前端无法区分两句话。
-- 按 '[拒绝原因] ' 标记切分:原因挪到新列,备注还原。
--
-- ★ btrim 默认只去**空格**、不去换行 —— 拼接留下的是 '3333\n'
-- 只写 btrim(x) 会残留一个换行,必须显式给出字符集。
-- ⚠ 与第 6 段同理:仅在上线前执行一次。
-- =============================================================================
UPDATE trans_borrow_transfer
SET reject_reason = nullif(btrim(split_part(remark, '[拒绝原因] ', 2), E' \t\r\n'), ''),
remark = nullif(
btrim(replace(remark,
'[拒绝原因] ' || split_part(remark, '[拒绝原因] ', 2),
''),
E' \t\r\n'),
'')
WHERE status = 'REJECTED'
AND remark LIKE '%[拒绝原因] %'
AND reject_reason IS NULL;
COMMIT;
-- =============================================================================
-- 第 8 段可选默认不执行borrow_transfer 权限码
--
-- ⚠ 本轮最终**取消了**该权限码的使用 —— 转交的责任链规则是
-- 「只有该物品的当前持有人本人可以发起」,而持有人是普通员工,
-- 通常不持有库管权限;再加一道库管权限,能发起的人变成「持有人 ∩ 库管」,
-- 绝大多数持有人反而发不了,功能形同虚设。真正的边界是 service 层的
-- caller_user_id 强校验。
--
-- 因此默认不创建,避免在权限管理界面留下一个无代码引用的死权限。
-- 若将来要加「管理员代办」入口,取消下面注释即可(幂等,可重复执行)。
-- -----------------------------------------------------------------------------
-- BEGIN;
-- 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;
-- =============================================================================
-- 执行后核对
--
-- ★ 这里刻意**不用 psql 的 \echo**DataGrip / DBeaver 等客户端不认元命令,
-- 会直接报语法错误)。改用 SELECT 输出一行带标签的常量,任何客户端都能跑。
-- =============================================================================
SELECT '=== 1) trans_borrow 四个新列(应 4 行)===' AS "核对项";
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','dispatch_operator')
ORDER BY column_name;
SELECT '=== 2) 两张新表(应 2 行)===' AS "核对项";
SELECT table_name FROM information_schema.tables
WHERE table_name IN ('trans_borrow_transfer','trans_borrow_return') ORDER BY table_name;
SELECT '=== 3) 索引总数transfer 应 8 个含主键 / return 应 4 个含主键)===' AS "核对项";
SELECT tablename, count(*) AS FROM pg_indexes
WHERE tablename IN ('trans_borrow_transfer','trans_borrow_return')
GROUP BY tablename ORDER BY tablename;
SELECT '=== 4) 身份锚点回填覆盖率 ===' AS "核对项";
SELECT count(*) AS ,
count(borrower_id) AS ,
count(*) FILTER (WHERE is_returned = FALSE) AS ,
count(current_holder_id) AS
FROM trans_borrow;
SELECT '=== 5) 同单多持有人单数(部分转交的正常形态,无需处理)===' AS "核对项";
SELECT count(*) AS FROM (
SELECT borrow_no FROM trans_borrow WHERE is_returned = FALSE
GROUP BY borrow_no HAVING count(DISTINCT current_holder_id) > 1
) t;
SELECT '=== 6) 遗留拼接痕迹(应为 0===' AS "核对项";
SELECT count(*) AS FROM trans_borrow_transfer
WHERE remark LIKE '%[拒绝原因] %';
SELECT '=== 7) 待告知发起方的拒绝(存量已标记,应为 0===' AS "核对项";
SELECT count(*) AS FROM trans_borrow_transfer
WHERE status = 'REJECTED' AND reject_seen_at IS NULL;
-- =============================================================================
-- 回滚段(整段取消注释执行;⚠ 会丢弃转交/归还流水,请先备份)
-- =============================================================================
-- BEGIN;
-- 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,
-- DROP COLUMN IF EXISTS dispatch_operator;
-- COMMIT;