소스 검색

fix(进件): Task 8 菜单 SQL 订正三处 —— 授权补租户角色、uuid 假断言、batch 先例定性

`551cea9` 的两处内容是照先例/文档写的,本轮对**活库**实测后被证伪或补全。逐条:

1) **授权只给了 role_id = 1 —— 错。** 活库实测:
   role 1 = 管理员角色(ADMIN),全库只被 admin 一人持有;
   role 2 = 租户角色(tenant),被 17 个用户持有(tenant_id 2, 9–18, 21–26)。
   而「收付款」目录(id=268) 连同它下面**全部三个** lsy 兄弟页(269/270/271)
   **都授给了 role 1 和 role 2 两个角色**(8 行 sys_role_menus)。
   只授 role 1 ⇒ 17 个租户用户一个都看不到进件菜单,而进件本就是租户级业务;
   且 admin 是超管、PermissionCheck 对超管直接放行 ⇒ 本机**测不出**这个缺口。
   ⇒ 改成授 role 1 与 2,与这棵子树现有每一行一致。
   ★ `lsy-menu.sql` 自己写的是 role_id = 1,但活库里那几行都带着 role 2 ——
     说明那份脚本的授权行不足以复现现状,不能拿它的字面当权威。

2) **`uuid ... NOT NULL` 这句是假的。** 该断言转抄自 `lsy-menu.sql:22-23`,
   而它照的是 `all_schema.sql`。活库实测:`uuid character varying(64)`,
   **is_nullable = YES**,**无 DEFAULT**;唯一索引 `ix_sys_menu_uuid` 确实在,
   但 PostgreSQL 的唯一索引**允许多个 NULL** ⇒ 不给值不报错、只静默留 NULL。
   ★ 旁证:全库 237 行 / 232 个 distinct uuid,差额 5 —— 正是 id 249–253 那 5 行
   NULL,也就是 batch-menu.sql 插的。**batch-menu.sql 执行过**(那 5 行在库里),
   它不是「会执行失败」,是「不报错地留下了 NULL」。
   ⇒ 本脚本仍用 gen_random_uuid()(不该再往那 5 个里加行),但**理由已订正**,
     且不再传播那句假 schema 断言。

3) **type=3 的形态从「引文档先例」升级为「引活库实测」**:
   全库 186 行 type=3,**全部(186/186)挂在 type=2 父行下,0 例例外**。
   本脚本要写的形态正是库里唯一存在的形态。

只读核对语句的预期读数同步更新(授权行 3 → 6 行)。
`"order"` 实测:268 下现有 1/2/3 ⇒ 本脚本算出 4;`permission` 是 varchar(100),最长串 43。

验证方式(**未执行任何写操作**):全脚本提取后仅 1 条语句,在只读会话
(`transaction_read_only = on`) 里 `PREPARE` —— 只 parse+plan、不执行 —— 通过;
两条正对照(表名写错、类型写错)均如期报错,证明该量具对错误敏感。
读数与脚本在 java/target/t7-probe/(gitignored)。
alphaH 3 주 전
부모
커밋
68f64822c8
1개의 변경된 파일과 57개의 추가작업 그리고 21개의 파일을 삭제
  1. 57 21
      java/src/main/resources/db/lsy-onboarding-menu.sql

+ 57 - 21
java/src/main/resources/db/lsy-onboarding-menu.sql

@@ -6,7 +6,9 @@
 -- 在**已存在**的「收付款」目录下,新增:
 --   · 1 行页面(type=2)「商户进件」 —— 进件页路由
 --   · 2 行按钮(type=3)「建号」「提交入网」 —— 承载两个写权限点
--- 并把这三行授权给 role_id = 1(照 005 / lsy-menu.sql 的先例)。
+-- 并把这三行授权给 role_id 1(管理员角色)**与** 2(租户角色)——
+-- ★ 这与计划/lsy-menu.sql 字面的「role_id = 1」不同,依据是活库现状,见下「★ 授权给谁」。
+--   只想给 admin 就把授权语句里的 `(2)` 删掉;代价写在那一节。
 --
 -- ── ★ 与 lsy-menu.sql 的关键差异(照抄它的写法会出错的地方)──────────────
 -- lsy-menu.sql 的 `dir` CTE 是**插入**收付款目录;本脚本的 `dir` 是**查**它。
@@ -28,12 +30,16 @@
 -- ★ **漏掉按钮行会怎样**:`module_payment:leshouying:onboarding:create` 永远不进入任何角色的
 --   授权集合 ⇒ **普通租户用户点「建号」必 403**;而 **超管直通**(`PermissionCheck` 对
 --   `ROLE_SUPERUSER` 直接 `return true`)⇒ **超管测试一个都测不出来**。
--- ★ 先例:`batch-menu.sql` 用 type=3 行承载 `module_payment:batch:authorize` / `:create` /
---   `:detail`(它那 4 行全是 type=3)。本表自己的父子校验也认这个形态:
---   `MenuService.validateParentChildType` —— `pt == 2` 时 `childType != 3` 即抛
---   「菜单下仅允许新增按钮」(MenuService.java:201-204)。
---   ★ 但 **batch-menu.sql 是坏先例、不可照抄**:它通篇 **没有 `uuid`**(0 处命中),
---     按字面执行会违反 `uuid NOT NULL`(lsy-menu.sql:22-23 已为此留档)。
+-- ★ 先例不只有文档里的 batch-menu.sql —— **活库实测(2026-09-23)**:
+--   全库 **186 行 type=3**,**全部(186/186,0 例例外)挂在 type=2 的父行下**,
+--   没有一例挂在目录(type=1)或按钮(type=3)下。也就是说本脚本要写的形态
+--   **正是库里唯一存在的形态**,不是从文档推出来的。
+--   本表自己的父子校验也认这个形态:`MenuService.validateParentChildType` ——
+--   `pt == 2` 时 `childType != 3` 即抛「菜单下仅允许新增按钮」(MenuService.java:201-204)。
+--   ★ 但 **batch-menu.sql 是不可照抄的先例**(理由已订正,别再用旧说法):
+--     它**执行过**(活库实测:id 249「批量付款」+ 250–253 四个按钮,全在),
+--     可它通篇 **没有 `uuid` 列**(0 处命中)—— 而 uuid 在活库**可空**,
+--     于是它**不报错地插了 5 行 NULL uuid**,正是全表仅有的那 5 个 NULL。
 --     本脚本的列清单照 `lsy-menu.sql` 的 **20 列**写法,`uuid` 用 `gen_random_uuid()`。
 --
 -- ── 列名:本表的真实结构(逐字照 lsy-menu.sql 的警告)──────────────────
@@ -46,10 +52,19 @@
 -- 在本表上一个都不存在;'M' / 'C' 那种字符枚举也不存在(本表 type 是 integer)。
 -- type 的语义(MenuCreateDTO.java:22 逐字):**1=目录 2=菜单 3=按钮 4=外链**。
 --
--- ── ★ uuid:NOT NULL 且无任何兜底 ──────────────────────────────────────
--- `uuid character varying(64) NOT NULL`,**无 DEFAULT**(唯一索引 ix_sys_menu_uuid),
+-- ── ★ uuid:可空、无默认 —— 不给值会**静默**插进 NULL,不报错 ─────────────
+-- **活库实测(2026-09-23,查 information_schema.columns + pg_indexes)**:
+--   `uuid character varying(64)`,**is_nullable = YES**,**column_default = 无**。
+--   ★ 这与 `lsy-menu.sql:22-23` 那句「`uuid character varying(64) NOT NULL`」**不一致** ——
+--     那句照的是 `all_schema.sql`,**活库不是这样**。本文件按活库写,别回去照抄那句。
+-- 唯一索引 `ix_sys_menu_uuid` 确实在(`CREATE UNIQUE INDEX`),但 PostgreSQL 的唯一索引
+--   **允许多个 NULL** ⇒ 不给值既不会报错、也不会撞唯一性,只会静默留下 NULL。
+-- ★ 库里已经有 **5 行是 NULL uuid**(实测:id 249–253,就是 batch-menu.sql 插的那五行;
+--   全表 237 行 / 232 个 distinct uuid,差额正是这 5 个 NULL)。
+--   这 5 行目前**不是活的缺陷**(前端菜单树不拿 uuid 当键,`MenuVO.uuid` 只是透传字段,
+--   且前端类型声明是 `uuid?: string` 可空)—— 但本脚本**不该再往这个集合里加行**。
 -- 应用层也不填(AutoFillMetaObjectHandler 只填 createdTime/updatedTime/tenantId)⇒
--- 每条 INSERT 都必须显式给值。本脚本用 gen_random_uuid()(36 字符,≤64;volatile ⇒ 逐行取值)。
+-- 每条 INSERT 都要显式给值。本脚本用 gen_random_uuid()(36 字符,≤64;volatile ⇒ 逐行取值)。
 --
 -- ── ★ id:不要写死 ────────────────────────────────────────────────────
 -- `id` 有默认值 `nextval('public.sys_menu_id_seq'::regclass)` ⇒ 本脚本**省略 id 列**,
@@ -73,23 +88,40 @@
 --   frontend/src/views/module_payment/collection/onboarding/index.vue
 -- ★ Task 9 若改了页面路径,这里必须同步改,否则**页面存在但点进去是 404**。
 --
--- ── 需要人工确认的三处(取值来自先例,不是实测数据)──────────────────
+-- ── 需要人工确认的两处(取值来自先例,不是实测数据)──────────────────
 -- 1) route_name:本脚本用 PascalCase `CollectionOnboarding`,照**紧邻**的三个 lsy 页面
 --    (CollectionOrder / CollectionRefund / CollectionMerchant,见 lsy-menu.sql:88-93)。
 --    但 batch-menu.sql 用的是 kebab('payment-batch')—— 两套先例不一致。
 --    route_name 会成为 vue-router 的 name(菜单高亮与 keep-alive 的 include 都按它匹配),必须唯一。
 -- 2) route_path:本脚本用 `/payment/collection/onboarding`(模块前缀式),照同目录三个 lsy 页面。
--- 3) role_id = 1:照 005 与 lsy-menu.sql:65,**请按你库里的实际角色 id 调整**。
---    本脚本只授权给这一个角色;若进件页要给别的角色用,需另外补 sys_role_menus。
+--
+-- ── ★ 授权给谁:role_id 1 **与** 2(此处偏离计划字面,依据是活库现状)────────
+-- **活库实测(2026-09-23)**:
+--   · `sys_role` 一共两行:**id=1「管理员角色」(code=ADMIN),只被 1 个用户持有(`admin`, tenant_id=1)**;
+--     **id=2「租户角色」(code=tenant),被 17 个用户持有**(tenant_id = 2, 9–18, 21–26)。
+--   · 「收付款」目录(id=268)与它下面**全部三个** lsy 兄弟页(269 支付订单 / 270 退款记录 /
+--     271 商户映射)**都授给了 role 1 和 role 2 两个角色**(实测 8 行 sys_role_menus,两角色各 4)。
+--   ⇒ 若只授 role 1,**17 个租户用户一个都看不到这个页面**(菜单树按 sys_role_menus 过滤),
+--     而进件本来就是**租户级**业务、租户用户正是它的使用者;
+--     更糟的是 `admin` 是超管,`PermissionCheck` 对超管直接放行 ⇒ **本机测不出这个缺口**。
+--   ⇒ 故本脚本授两个角色,与这棵子树**现有的每一行**保持一致。
+--   ★ 如果你的意图是「进件页只给 admin」,把下面 INSERT 里的 `(2)` 一行删掉即可 ——
+--     那是一个可以做的选择,但请知道它的后果:**17 个租户用户看不到进件菜单**。
+--     (`lsy-menu.sql` 自己写的是 role_id = 1,可活库里那几行**都**带着 role 2 ——
+--      说明那份脚本的授权行不足以复现现状,别把它的字面当权威。)
 --
 -- ── 其他 ─────────────────────────────────────────────────────────────
 -- · icon 一律留空:图标集是 src/assets/icons/*.svg,经 `i-svg:<name>` 渲染;
 --   缺省时前端回退到 i-svg:menu。要图标请在「菜单管理」界面里选。
 -- · 幂等(两种**相反**的情形都要知道;本脚本不是通用 upsert):
---   1) 页面行已存在 ⇒ `page` 为空 ⇒ **两个按钮也不会插**(buttons 的 FROM 是 page),且**不报错**。
---      若出现「菜单在、按钮不在」,请手工补两条 type=3 的 INSERT。
+--   1) 页面行已存在 ⇒ `page` 为空 ⇒ **两个按钮也不会插**(buttons 的 FROM 是 page),
+--      **授权行也不会插**(它们的 FROM 是 page / buttons),且**全程不报错**。
+--      若出现「菜单在、按钮/授权不在」,请手工补两条 type=3 的 INSERT + 对应的 sys_role_menus。
 --   2) 收付款目录不存在 ⇒ `dir` 为空 ⇒ 三行**都不插**,且**不报错**。
 --      执行前请先跑下面第 1 条只读核对确认目录在。
+-- · `"order"` 实测:收付款目录(id=268)下现有三个子行 order = 1/2/3 ⇒
+--   本脚本的 `MAX("order") + 1` 会算出 **4**,排在「商户映射」之后(实测于 2026-09-23)。
+-- · `permission` 列是 `varchar(100)`;本脚本三个串最长 43 字符,有余量。
 
 WITH dir AS (
     SELECT id FROM sys_menu
@@ -126,10 +158,12 @@ buttons AS (
     RETURNING id
 )
 -- 页面行与两个按钮行都要授权:菜单树按 sys_role_menus 过滤,只授页面会让按钮权限缺席 ⇒ 403。
+-- 两个角色都要(见上「★ 授权给谁」):`page` / `buttons` 各只被引用一次,
+-- 免得依赖「数据修改型 CTE 能否被引用两次」这种我没实测过的语义。
 INSERT INTO sys_role_menus (role_id, menu_id)
-SELECT 1, id FROM page
-UNION ALL
-SELECT 1, id FROM buttons;
+SELECT v.role_id, x.id
+  FROM (SELECT id FROM page UNION ALL SELECT id FROM buttons) AS x
+ CROSS JOIN (VALUES (1), (2)) AS v(role_id);
 
 
 -- ── 只读核对(执行后手工跑一遍;前三条**执行前**也建议先跑)────────────────
@@ -154,11 +188,13 @@ SELECT 1, id FROM buttons;
 --   期望:**2 行**,permission 逐字为
 --         'module_payment:leshouying:onboarding:create' 与 'module_payment:leshouying:onboarding:commit'。
 --
--- 4) 授权行
+-- 4) 授权行(**两个角色各 3 行**,见上「★ 授权给谁」)
 --   SELECT rm.role_id, m.name, 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';
---   期望:**3 行**,role_id 都是 1。
+--    WHERE m.name IN ('商户进件', '建号', '提交入网') AND m.status = '0'
+--    ORDER BY rm.role_id, m.name;
+--   期望:**6 行** —— role_id 1 与 2 **各 3 行**。
+--   若只有 3 行且 role_id 全是 1 ⇒ 17 个租户用户看不到进件菜单。
 --
 -- 5) 端到端(**唯一能证明权限串没写错的一条**):用一个**非超管**的租户账号登录,
 --    确认三个`module_payment:leshouying:onboarding:*`都出现在