psql 的 \echo 是元命令,DataGrip / DBeaver 不认,整份执行会直接报语法错误。 核对段改用 SELECT '常量' AS "核对项" 输出标签行,任何客户端都能跑 (psql 下效果相同)。同步修正头部说明中「需用 psql」的过期表述。 已在临时库重跑验证:零报错、核对结果与改前一致、重跑幂等。
330 lines
18 KiB
PL/PgSQL
330 lines
18 KiB
PL/PgSQL
-- =============================================================================
|
||
-- 借库转交(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
|
||
'初始借用人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)';
|
||
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
|
||
'转交目标明细行ID(trans_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;
|