-- 菜单合并:「代付」目录收纳六个顶级菜单(2026-10-05) -- -- 【怎么跑】Navicat 整段执行(纯 SQL,无 psql 元命令)。三段: -- 0 预检(只读)→ 1 合并(写,单条语句包在 BEGIN/COMMIT 里)→ 2 复核(只读)。 -- ★ 先看第 0 段;再看第 1 段末尾那行计数(期望 1 | 6 | 2);再对第 2 段。 -- -- 【合并对象】六个当前都是**顶级**(parent_id IS NULL)。以下为 2026-10-05 盘点实测值: -- 190 企业管理 type=2 order=1 /payment/enterprise module_payment/enterprise/index -- 193 员工管理 type=2 order=1 /payment/employee module_payment/employee/index -- 200 资金专户转账页 type=2 order=1 /account module_payment/account/index -- 222 操作指南 type=4 order=1 外链 https://doc.qcsj88888.com/quick-guide.html -- 249 批量付款 type=2 order=3 /payment/batch module_payment/batch/index -- 194 员工授信额度 type=2 order=3 /payment/institution module_payment/institution/index -- -- 【为什么必须同时把「代付」授给两个角色】UserService#buildMenuTree 对「父节点不在授权 -- 集合里」的子节点**静默丢弃**(不进树、不报错、日志不打): -- · 角色 2(租户角色)持有这六个的**全部** ⇒ 只改 parent_id 不补授 ⇒ 租户侧六个页面 -- **一起消失**,且页面上没有任何异常可查。 -- · 角色 1(管理员角色)持有一项(249 批量付款)⇒ 不补授 ⇒ 非超管用户看不到批量付款。 -- -- 【六个子菜单的 route_path 一个都不改,也**不需要**改】它们本来就是绝对路径;前端 -- permission.store.ts 的 generator() 直接拿 route_path 当路由 path(**不拼父路径**), -- 侧栏 MenuItem.resolvePath 对绝对路径同样原样取胜 ⇒ 挂到新父目录下路径不变。 -- 同构的现成例子就在线上跑着:银联收款(268, type=1, component_path=NULL) 下面挂着 -- 支付订单(269, /payment/collection/order) —— 「无组件的目录 + 子页」这套形状已验证。 -- -- 【没测量过的两项,本轮不受影响】六个的 route_name / hidden 未在盘点里取到。 -- · route_name:侧栏叶子优先按 name 跳转、无 name 时按 path 跳转,两条分支都成立;本轮不改它。 -- · hidden:第 0 段顺带量出来(六个今天都是可见的顶级项,预期全 false)。 -- -- 【幂等】三处守卫,重跑第二遍全部 no-op、不报错、不产生第二行: -- ① 建目录:库里已有 name/title 为「代付」的行(**不分类型、不分状态**),或 route_name -- ('PaymentPayout')、route_path ('/payment/payout') 已被占用 ⇒ 不建。 -- ② 归位:UPDATE ... FROM dir —— dir 为空则一行都不动;另加 parent_id IS NULL 条件, -- 不覆盖别人已经改过的行。 -- ③ 授权:授的是本语句刚建出来的**新 id** ⇒ 重跑时插不进重复行(表上另有 PK 兜底)。 -- ★ 失败是**可见**的:第 1 段计数读到 0|0|0 就是守卫命中(多半是库里已有「代付」行), -- 第 2 段会把现状打出来 —— 别猜,把 0 段和 2 段的结果发回来。 -- -- 【顺序】「代付」自身 order=1:六个搬走后顶级 order=1 只剩 仪表盘(1) ⇒ 代付落回六个原来 -- 的位置(仪表盘之后、系统管理(order=2)之前)。与仪表盘并列同值,谁在前由数据库返回顺序 -- 决定(顶栏现状本来就有多个并列 order)。要 100% 确定,见第 2 段后面的可选一句。 -- 六个子菜单在代付内的顺序 = 你原话里的列举顺序(1..6),改 moved 里的 VALUES 即可调。 -- ========== 0 预检(只读) ========== -- 0a) 名字 / route_name / route_path 占用检查 —— 期望:**无数据** SELECT '同名代付' AS what, id, type, status, route_name, route_path FROM sys_menu WHERE name = '代付' OR title = '代付' UNION ALL SELECT 'route_name 被占', id, type, status, route_name, route_path FROM sys_menu WHERE route_name = 'PaymentPayout' UNION ALL SELECT 'route_path 被占', id, type, status, route_name, route_path FROM sys_menu WHERE route_path = '/payment/payout'; -- 0b) 六行现状 —— 期望:6 行,parent_id 全空、status 全 '0' SELECT id, parent_id, type, "order", title, route_path, hidden, status FROM sys_menu WHERE id IN (190, 193, 200, 222, 249, 194) ORDER BY id; -- ========== 1 合并(写) ========== BEGIN; WITH dir AS ( INSERT INTO sys_menu (name, type, "order", permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, affix, title, params, uuid, parent_id, status, created_time, updated_time) SELECT '代付', 1, 1, NULL, NULL, 'PaymentPayout', '/payment/payout', NULL, NULL, false, true, false, false, '代付', NULL, gen_random_uuid(), NULL, '0', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE name = '代付' OR title = '代付') AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE route_name = 'PaymentPayout') AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE route_path = '/payment/payout') RETURNING id ), moved AS ( UPDATE sys_menu m SET parent_id = dir.id, "order" = v.ord FROM dir, (VALUES (190, 1), (193, 2), (200, 3), (222, 4), (249, 5), (194, 6)) AS v(mid, ord) WHERE m.id = v.mid AND m.parent_id IS NULL RETURNING m.id ), granted_rows AS ( INSERT INTO sys_role_menus (role_id, menu_id) SELECT r.role_id, dir.id FROM dir, (VALUES (1), (2)) AS r(role_id) RETURNING role_id ) SELECT (SELECT count(*) FROM dir) AS dir_created, (SELECT count(*) FROM moved) AS menus_moved, (SELECT count(*) FROM granted_rows) AS grants_inserted; -- 期望:1 | 6 | 2 COMMIT; -- ========== 2 复核(只读) ========== -- 2a) 目录 + 六个成员 —— 期望:7 行,代付在首(parent_title 空),其余 parent_title = 代付 SELECT m.id, m.parent_id, m.type, m."order", m.title, m.route_path, m.hidden, m.status, p.title AS parent_title FROM sys_menu m LEFT JOIN sys_menu p ON p.id = m.parent_id WHERE m.name = '代付' OR m.parent_id IN (SELECT id FROM sys_menu WHERE name = '代付' AND type = 1) ORDER BY (m.parent_id IS NULL) DESC, m."order", m.id; -- 2b) 授权 —— 期望:代付 granted_roles='1,2';190/193/200/222/194 = '2';249 = '1,2' SELECT m.id, m.title, m.type, (SELECT string_agg(rm.role_id::text, ',' ORDER BY rm.role_id) FROM sys_role_menus rm WHERE rm.menu_id = m.id) AS granted_roles FROM sys_menu m WHERE m.name = '代付' OR m.parent_id IN (SELECT id FROM sys_menu WHERE name = '代付' AND type = 1) ORDER BY m.id; -- 2c) 静默丢弃体检(与 sys-menu-inventory-readonly.sql 第 ③ 段同式)—— 期望:**无数据** SELECT rm.role_id, r.name AS role_name, m.id AS menu_id, m.title, m.parent_id, p.title AS parent_title FROM sys_role_menus rm JOIN sys_menu m ON m.id = rm.menu_id AND m.status = '0' JOIN sys_role r ON r.id = rm.role_id JOIN sys_menu p ON p.id = m.parent_id WHERE NOT EXISTS (SELECT 1 FROM sys_role_menus x WHERE x.role_id = rm.role_id AND x.menu_id = m.parent_id) ORDER BY rm.role_id, m.id; -- ── 可选:让「代付」与「仪表盘」的前后 100% 确定(现状是并列 order=1)────────── -- UPDATE sys_menu SET "order" = 0 WHERE id = 1 AND type = 1; -- 仪表盘提前到唯一 0 -- ── ★ 执行后必须**重新登录**才生效:菜单树与权限集合在登录时取,不重登看不到变化。 -- ========== 回滚(默认整段注释;要回滚时逐句去掉注释执行) ========== -- 前置:确认「代付」是本次新建的(即第 0 段 0a 当时读到「无数据」)——否则最后一句会删掉 -- 你原有的目录行。下面两行的 order 值还原为盘点实测的旧值。 -- BEGIN; -- UPDATE sys_menu SET parent_id = NULL, "order" = 1 WHERE id IN (190, 193, 200, 222); -- UPDATE sys_menu SET parent_id = NULL, "order" = 3 WHERE id IN (249, 194); -- DELETE FROM sys_role_menus -- WHERE role_id IN (1, 2) -- AND menu_id IN (SELECT id FROM sys_menu WHERE name = '代付' AND type = 1); -- DELETE FROM sys_menu -- WHERE name = '代付' AND type = 1 AND parent_id IS NULL; -- COMMIT;