Procházet zdrojové kódy

fix(乐收赢): 收银台菜单脚本改锚 route_name,并补目录一行给租户角色

本脚本此前一直只在工作区(`lsy-cashier-menu.sql`),本次连同下面两处订正一起入库。

## 一、目录锚点从 `name` 改成 `route_name`

`dir` CTE 原写 `WHERE name = '收付款'`。**那个名字今天不存在了** —— 活库只读实测
(2026-09-28):目录 268 已叫「银联收单」。那次改名是**人在界面上手工做的**:
「银联收单」四个字在全仓(sql / java / md / ts / vue)**零命中**,不是任何脚本干的。
⇒ 锚 `name` 的写法今天会让 `dir` 为空 ⇒ **全脚本静默空跑**(文件头的「幂等」情形 2
本来就是「不报错、什么都不做」,所以这个失效没有任何外部症状)。
锚 `route_name = 'Collection'`(身份,不随标签变),与 `lsy-merchant-menu-rename.sql` 同一模式。

## 二、目录行本身也要授给租户角色

根因是源码实测、不是推测:`UserService.buildMenuTree` 建树时,**父节点不在被授集合里
的子节点被直接丢掉**(`if (p != null) {…}` 没有 else 分支,节点既不进 tree 也不报错)。
而 `GET /system/role/detail/2` 实测:角色 2 持有 54 个菜单,`268/269/270/271/272`
**一个都没有**(同法量角色 1:120 个、五个 id 全有 ⇒ 量具能分辨)。
⇒ 只授页面等于没授。本次把目录 268 一并授给 role 2。

只补目录一行、**不**补兄弟页:`lsy-onboarding-menu.sql:66-69` 记录了
「把 272 商户进件开给角色 2」会重新打开的那条攻击链,本条不碰它。
代价如实写:角色 2 因此多出一个顶层目录,里面可能只有一页。

## 三、订正:目录那一支原先挂的 `EXISTS (SELECT 1 FROM page)` 是个自相矛盾的闸

上一版把「页面行已存在」与「目录已授出去」绑在一起,而这两个条件**恰好会分开**:
页面行是**旧版**脚本(那时还没有目录授权)插的 ⇒ 页面在、目录没授 ⇒ 本脚本一跑就跳过目录,
而角色 2 看不到的根因**正是目录没授**。⇒ 那条闸让脚本在**最需要它的时候**什么都不做,
与它自己下面写的目标相反。现在目录那一行无条件参与,是否真插入仍由 `NOT EXISTS` 逐行判:
幂等性不变(重跑不插重复行、不报错),只是不再依赖「`page` 为空」这个无关条件。

与 `lsy-tenant-page-grants.sql` 的关系:那份也授目录 268 给角色 2,且不受任何 CTE
非空性影响。两者都在时先跑哪一份结果都一样 —— 但本脚本现在**不依赖**它。

## 验证读数(如实标注量具)

- ★ **本脚本没有执行过**(无库访问;用户令「先不碰库」)。上面的活库读数来自交付前的
  只读查询,日期已写在文件里。执行由用户做。
- 结构量:去掉注释后括号配平 **0**、语句数 **1** —— 与 HEAD 版**同值**(正对照)。
- 试过用 JSqlParser 5.0 做语法解析,**量具不适用**:它对 HEAD 版(未改动的)与本次改动版
  **给出同一条** `Encountered unexpected token: "INSERT" ... Was expecting "WITH"` ——
  它不支持 PostgreSQL 的**数据修改型 CTE**(`WITH … AS (INSERT … RETURNING …)`)。
  两个读数一样 ⇒ 该工具对本文件不携带任何信息,不能据此说语法有问题,也不能说没问题。
alphaH před 2 týdny
rodič
revize
e2ca5e24a8
1 změnil soubory, kde provedl 87 přidání a 19 odebrání
  1. 87 19
      java/src/main/resources/db/lsy-cashier-menu.sql

+ 87 - 19
java/src/main/resources/db/lsy-cashier-menu.sql

@@ -3,7 +3,7 @@
 -- 【本脚本尚未执行】执行由用户做。执行前请先读下面「★ 授权给谁」一节。
 --
 -- ── 本脚本干什么 ──────────────────────────────────────────────────────
--- 在**已存在**的「收付款」目录下,新增:
+-- 在**已存在**的「银联收单」目录(id=268,`route_name='Collection'`;2026-09-23 时叫「收付款」)下,新增:
 --   · 1 行页面(type=2)「扫码收款」 —— 收银台路由
 --   · 2 行按钮(type=3)「发起收款」「撤销收款」 —— 承载另两个写权限点
 -- 并把这三行授权给 role_id = 1(管理员角色)**与 role_id = 2(租户角色)**。
@@ -23,10 +23,14 @@
 --     id=270「退款记录」 CollectionRefund
 --     id=271「商户映射」 CollectionMerchant
 --     id=272「商户进件」 CollectionOnboarding
---   ⇒ 活库的命名空间是 **`Collection<名词>`**。
+--   ⇒ 活库的命名空间是 **`Collection<名词>`**(目录自己是 **`Collection`**)。
 --   ★ 上面四个名字是**那一刻**的读数。其中 id=271 随后在本批次改名
 --     「商户映射」→「手工登记商户号」(`lsy-merchant-menu-rename.sql`,同一批次交付);
 --     其余三个名字未动。`route_name` 四行都没变。
+--   ★★ **2026-09-28 复量(六天后)**:`name` 那一列**已经变了** ——
+--     目录 268 今天叫「银联收单」,271 今天仍叫「商户映射」(改名脚本尚未执行)。
+--     **五个 `route_name` 一个都没变** ⇒ 这正是本脚本把 `dir` 的锚点从 `name` 改成 `route_name` 的实测依据。
+--     (那句「人在界面上手工改名」的旁证:「银联收单」四字在全仓 sql/java/md/ts/vue **零命中**。)
 --
 -- ★★ 但 `frontend/src/views/module_payment/collection/` 下**三个页面的源码**里写的是
 --    `LeshouyingOrder` / `LeshouyingRefund` / `LeshouyingMerchant` —— **与活库对不上**。
@@ -58,11 +62,24 @@
 -- 前缀/后缀写错会拼成查不到的模块名 ⇒ **静默落到 error/404.vue**(不是报错)。
 --
 -- ── ★★ 授权给谁:role 1 **和** role 2 ─────────────────────────────────
--- **活库只读实测(2026-09-23)**:收付款子行的现有授权是
+-- **活库只读实测(2026-09-23)**:收付款子行的现有授权曾是
 --   支付订单 → role 1,2 / 退款记录 → role 1,2 / 商户映射(今「手工登记商户号」) → role 1,2
 --   / **商户进件 → 只有 role 1**。
 --   ⇒ 本页按**多数形态**(两个角色都授)。
 --
+-- ★★ **2026-09-28 复量:上面那段读数今天不成立了。**
+--   `GET /system/role/detail/2` 实测:角色 2 持有 **54** 个菜单,`268/269/270/271/272` **一个都没有**
+--   (同法量角色 1:120 个,五个 id 全有 ⇒ 量具能分辨,不是「查不出来」)。
+--   两个结论:
+--     1) 「本页按多数形态」的**前提没了** —— 角色 2 今天连兄弟页都看不到。
+--     2) 只授页面**等于没授**(父目录缺席 ⇒ 见文末授权段的 `buildMenuTree` 根因)
+--        ⇒ 本次把目录行一并授给角色 2。
+--   ★ 成因**未定**,不猜:可能是有人改过角色配置,也可能是
+--     「角色详情不带授权 ⇒ 抽屉全不勾且保存清空授权」那个缺陷(`1bda59a`,**2026-09-28 才修**)
+--     期间被触发过一次。两种解释我用现有读数分不开,留给出题的人。
+--   ★ 仍然成立的部分:role 2 是**唯一**能让收款台真正收到钱的角色 ——
+--     `uk_lsy_merchant_mch_no` 是全局唯一,而 `Y52211090004` 属租户 2。
+--
 -- ★ `lsy-onboarding-menu.sql` 只授 role 1,它挡的是这条链(每一跳它都实测过):
 --   `onboarding:commit` 的租户归属闸证明的是「**本租户内存在一行 mch_no 等于该值**」,
 --   而映射表**调用方自己就能写**(`merchant:list` 一个读权限守着写和删、mch_no 由调用方给)
@@ -86,11 +103,14 @@
 --     SELECT 2, ...`(即回到只授 role 1),不用重跑本脚本 —— 见「放开/收回」。
 --
 -- ── 幂等(与 lsy-onboarding-menu.sql 同形,两种相反情形都不报错)──────────
---   1) 页面行已存在 ⇒ `page` 为空 ⇒ **两个按钮与全部授权行都不会插**,且不报错。
---   2) 收付款目录不存在 ⇒ `dir` 为空 ⇒ 三行都不插,且不报错。执行前先跑第 1 条只读核对。
+--   1) 页面行已存在 ⇒ `page` 为空 ⇒ 页面与两个按钮都不插(`buttons` 是 `FROM page`,
+--      自然也为空),且不报错。
+--      ★ 唯一**不看** `page` 是否为空的是目录那一行(见末条 INSERT 的注释):
+--        它由 `NOT EXISTS` 单独判。⇒ 重跑时若目录已授过,仍然什么都不做。
+--   2) 目录不存在 ⇒ `dir` 为空 ⇒ 三行都不插,且不报错。执行前先跑第 1 条只读核对。
 --
 -- ── "order" ──────────────────────────────────────────────────────────
--- 用 `MAX("order") + 1` **现算**,不写死数字。实测(2026-09-23)现值为 1/2/3/4 ⇒ 本行会拿到 **5**。
+-- 用 `MAX("order") + 1` **现算**,不写死数字。实测(2026-09-28 复量)目录 268 下现值为 1/2/3/4 ⇒ 本行会拿到 **5**。
 --   ★ 写成字面量 5 会在「有人手工调过序」或「本脚本重跑」时撞车,而 `sys_menu` 对
 --     (parent_id, "order") **没有唯一约束** ⇒ 撞了也不报错,只是两行同序。
 --   ★ 全表行数实测 240(`lsy-onboarding-menu.sql` 写作时是 237,其后 +3 正是它自己插的
@@ -100,13 +120,23 @@
 --   DELETE FROM sys_role_menus
 --    WHERE role_id = 2
 --      AND menu_id IN (SELECT id FROM sys_menu
---                       WHERE name IN ('扫码收款', '发起收款', '撤销收款') AND status = '0');
+--                       WHERE (name IN ('扫码收款', '发起收款', '撤销收款')
+--                              OR route_name = 'Collection')   -- ← 目录那行(2026-09-28 新增的授权)
+--                         AND status = '0');
+--   ★ 收回**目录那一行**(`route_name='Collection'`)才是让页面从角色 2 视野里消失的那一下;
+--     只删页面行、留着目录行**效果一样**(子节点失去父节点即被丢弃),但会留下一个空目录。
 --   ★ **不要**用「重跑本脚本」来收回:页面行已存在 ⇒ 授权行也不会插(见幂等 1),
 --     但同样**也不会删**,跑了等于什么都没做。
+--   ★ 这一条**只动 role 2**:角色 1 对 268 的持有是**本脚本执行前就有**的,不在本次交付范围内。
 
 WITH dir AS (
+    -- ★★ 锚在 `route_name`(**身份**)上,**不锚 `name`**(标签)—— 与 `lsy-merchant-menu-rename.sql:38-39` 同一模式。
+    --   理由是一条实测:本目录 2026-09-23 叫「收付款」,**2026-09-28 实测已叫「银联收单」**。
+    --   那次改名是**人在界面上手工做的** —— 「银联收单」四个字在全仓(sql/java/md/ts/vue)**零命中**,
+    --   不是任何脚本干的。⇒ 锚 `name` 的写法今天会 `dir` 为空 ⇒ **全脚本静默空跑**(见文件头「幂等」情形 2)。
+    --   同目录的 route_name 实测 = `Collection`(活库只读,2026-09-28)。
     SELECT id FROM sys_menu
-     WHERE name = '收付款' AND type = 1 AND status = '0' AND parent_id IS NULL
+     WHERE route_name = 'Collection' AND type = 1 AND status = '0' AND parent_id IS NULL
 ),
 page AS (
     INSERT INTO sys_menu (name, type, "order", permission, icon, route_name, route_path, component_path,
@@ -140,18 +170,50 @@ buttons AS (
 )
 -- 页面行与两个按钮行**都要**授权:菜单树按 sys_role_menus 过滤,只授页面会让按钮权限缺席 ⇒ 403。
 -- 两个角色都授(理由见上「★★ 授权给谁」—— 与进件那份的差异是**故意的**)。
+--
+-- ★★ 2026-09-28 追加第三类:**目录行 `dir` 本身也要授给角色 2**。
+--   根因(源码实测,不是推测):`UserService.buildMenuTree:672-677` 建树时,
+--   父节点不在被授集合里的子节点**直接被丢掉**(`if (p != null) {…}` **没有 else 分支**,
+--   节点既不进 tree 也不报错)。而 `role/detail/2` 实测:角色 2 持有的 54 个菜单里
+--   **268/269/270/271/272 一个都没有** ⇒ 只授页面的话,角色 2 登录后**什么都看不到**。
+--   ⇒ 这是本脚本原先的缺口:它按「兄弟页已授 role 1,2」的前提写了授权范围
+--     (见上「★★ 授权给谁」的 2026-09-23 读数),而那个前提**今天不成立**。
+--   ★ 只补 `dir` 一行、**不**补兄弟页:`lsy-onboarding-menu.sql:66-69` 记录了
+--     「把 272 商户进件开给角色 2」会重新打开的那条攻击链,本条不碰它。
+--   ★ 代价如实写:角色 2 因此多出一个顶层目录「银联收单」,里面只有「扫码收款」一页。
+--     而 `uk_lsy_merchant_mch_no` 是**全局**唯一(不带 tenant_id)⇒ 除租户 2 外,
+--     其余租户进来只会读到「该租户未配置商户号」那句 404 文案(功能上无害,但确实是死角)。
+--
+-- `NOT EXISTS` 是为了 `dir`:角色 1 **本来就持有 268**,裸插会撞 (role_id, menu_id) 约束
+-- ⇒ 整条语句失败、连带回滚页面与按钮的授权。与 `har-order-refund-menu.sql:34` 同形。
+--
+-- ★★ 2026-09-30 订正:`dir` 那一支**故意不挂** `EXISTS (SELECT 1 FROM page)`(原先挂着)。
+--   原先那条闸把「页面行已存在」与「目录已授出去」绑在一起,而这两个条件**恰好会分开**:
+--   页面行是旧版脚本(那时还没有目录授权)插的 ⇒ 页面在、目录没授 ⇒ 本脚本一跑就跳过目录,
+--   而角色 2 看不到「银联收单」的根因**正是目录没授**(`buildMenuTree` 静默丢弃父不在集合里的
+--   子节点)。⇒ 那条闸让脚本在**最需要它的时候**什么都不做,与它自己下面写的目标相反。
+--   现在目录那一行**无条件参与**,是否真的插入仍由 `NOT EXISTS` 逐行判 ——
+--   幂等性不变(重跑不插重复行、不报错),只是不再依赖「`page` 为空」这个无关条件。
+--   ★ 与本仓 `lsy-tenant-page-grants.sql` 的关系:那份脚本也授目录 268 给角色 2,
+--     且不受任何 CTE 非空性影响。两者都在时,先跑哪一份结果都一样(都是 `NOT EXISTS` 逐行判)。
 INSERT INTO sys_role_menus (role_id, menu_id)
 SELECT r.role_id, x.id
-  FROM (SELECT id FROM page UNION ALL SELECT id FROM buttons) AS x
- CROSS JOIN (VALUES (1), (2)) AS r(role_id);
+  FROM (SELECT id FROM page
+        UNION ALL SELECT id FROM buttons
+        UNION ALL SELECT d.id FROM dir d) AS x
+ CROSS JOIN (VALUES (1), (2)) AS r(role_id)
+ WHERE NOT EXISTS (SELECT 1 FROM sys_role_menus rm
+                    WHERE rm.role_id = r.role_id AND rm.menu_id = x.id);
 
 
 -- ── 只读核对(执行后手工跑一遍;第 1 条**执行前**也先跑)────────────────
 --
--- 1) 收付款目录在不在(**为空则本脚本什么都不会插,且不报错**)
---   SELECT id, name, type, route_path FROM sys_menu
---    WHERE name = '收付款' AND type = 1 AND status = '0' AND parent_id IS NULL;
---   期望:**1 行**(2026-09-23 实测 id=268)。0 行 ⇒ 先建目录再回来。
+-- 1) 目录在不在(**为空则本脚本什么都不会插,且不报错**)
+--   SELECT id, name, title, type, route_name, route_path FROM sys_menu
+--    WHERE route_name = 'Collection' AND type = 1 AND status = '0' AND parent_id IS NULL;
+--   期望:**1 行**。2026-09-28 实测 = id 268 / name「银联收单」/ route_path `/payment/collection`。
+--   ★ 查的是 `route_name` 而不是 `name`(同 CTE 的理由)。若这里用旧名「收付款」查,今天会得 0 行
+--     —— 那**不是**目录不存在,只是它被改过名了。
 --
 -- 2) 页面行
 --   SELECT id, name, type, "order", permission, route_name, route_path, component_path,
@@ -170,12 +232,18 @@ SELECT r.role_id, x.id
 --   期望:**2 行**,permission 逐字为
 --         'module_payment:leshouying:cashier:create' 与 'module_payment:leshouying:cashier:close'。
 --
--- 4) 授权行(**6 行**:3 个菜单 × 2 个角色)
---   SELECT rm.role_id, m.name, m.permission
+-- 4) 授权行(**7 行**)
+--   SELECT rm.role_id, m.id, m.name, m.type, m.permission
 --     FROM sys_role_menus rm JOIN sys_menu m ON m.id = rm.menu_id
---    WHERE m.name IN ('扫码收款', '发起收款', '撤销收款') AND m.status = '0'
---    ORDER BY rm.role_id, m.name;
---   期望:**6 行**,role_id 是 1 和 2 各 3 行。若只有 3 行(全是 1)⇒ role 2 被收回了,见文件头。
+--    WHERE m.status = '0'
+--      AND (m.name IN ('扫码收款', '发起收款', '撤销收款') OR m.route_name = 'Collection')
+--    ORDER BY rm.role_id, m.type, m.id;
+--   期望:**7 行** ——
+--     role 1:扫码收款 + 发起收款 + 撤销收款 = **3 行**(目录 268 它**本来就有**,`NOT EXISTS` 跳过 ⇒ 不重复计数)
+--     role 2:同上 3 行 **+ 目录 268 = 4 行**
+--   ★ 判据不是「行数对不对」,而是 **role 2 那 4 行在不在**:
+--     少了目录那行 ⇒ 角色 2 登录后**整页不可见**(父节点缺席,见文件头授权段)。
+--     少了按钮行 ⇒ 非超管点「发起收款 / 撤销收款」**必 403**,而**超管测不出来**。
 --
 -- 5) 端到端(**唯一能证明权限串没写错的一条**):用一个**非超管**的租户账号登录,
 --    确认三个 `module_payment:leshouying:cashier:*` 都出现在