|
|
@@ -0,0 +1,221 @@
|
|
|
+-- 统一支付平台 —— 菜单与权限(Task 17,2026-10-20)
|
|
|
+--
|
|
|
+-- 【本脚本尚未执行】执行由用户做。执行前先跑文末「执行前检查」两条只读查询。
|
|
|
+--
|
|
|
+-- ── 本脚本干什么 ──────────────────────────────────────────────────────
|
|
|
+-- 新建一个**顶层目录**(type=1)「统一支付平台」,其下:
|
|
|
+-- · 3 行页面(type=2):商户配置 / 订单管理 / 收银台
|
|
|
+-- · 4 行按钮(type=3):订单详情 / 同步上游 / 发起退款 / 发起收款
|
|
|
+-- 并把**目录 + 三页 + 四按钮**(8 行)授权给 role 1(管理员)与 role 2(租户角色)。
|
|
|
+-- ★ 两页中的「商户配置」只有列表权限(租户看自己的行);
|
|
|
+-- 商户新增/删除是**超管身份**门禁(`UnifiedMerchantController#save/delete`
|
|
|
+-- 的 `@perm.hasRole('SUPERUSER')`),**不建权限行**,也不在本脚本授权范围内。
|
|
|
+--
|
|
|
+-- ── permission 必须与 @PreAuthorize 逐字相同 ──────────────────────────
|
|
|
+-- 差一个字符就是 403,而且**超管测不出来**(`PermissionCheck` 对 `ROLE_SUPERUSER` 直接放行)。
|
|
|
+-- 本脚本 7 个 permission 逐字取自 2026-10-20 的源码(BASE 28f4899b,未抄任何文档):
|
|
|
+-- UnifiedMerchantController @RequestMapping("/payment/unified/merchant")
|
|
|
+-- list : @perm.hasAny('module_payment:unified:merchant:list')
|
|
|
+-- save : @perm.hasRole('SUPERUSER') ← 不建权限行
|
|
|
+-- delete : @perm.hasRole('SUPERUSER') ← 不建权限行
|
|
|
+-- UnifiedOrderController @RequestMapping("/payment/unified/order")
|
|
|
+-- list : @perm.hasAny('module_payment:unified:order:list')
|
|
|
+-- detail : @perm.hasAny('module_payment:unified:order:detail')
|
|
|
+-- refunds : @perm.hasAny('module_payment:unified:order:detail') ← 复用 detail
|
|
|
+-- sync : @perm.hasAny('module_payment:unified:order:sync')
|
|
|
+-- refund : @perm.hasAny('module_payment:unified:order:refund')
|
|
|
+-- UnifiedCashierController @RequestMapping("/payment/unified/cashier")
|
|
|
+-- create : @perm.hasAny('module_payment:unified:cashier:create')
|
|
|
+-- snapshot : @perm.hasAny('module_payment:unified:cashier:list')
|
|
|
+-- ★ 取值口径是「现取」,不是「抄一次就完」—— 控制器改了这里必须重取。
|
|
|
+-- ★ 钉子:`UnifiedAdminWiringTest` 双向钉住「注解集合 == SQL 集合 == 这 7 个串」。
|
|
|
+--
|
|
|
+-- ── 表结构约定(照抄踩过的坑)────────────────────────────────────────
|
|
|
+-- · `component_path` 是**相对 `src/views/` 的路径**:不带 `views/` 前缀、不带 `.vue` 后缀。
|
|
|
+-- 本脚本的值与预期的页面文件对齐:
|
|
|
+-- frontend/src/views/module_payment/unified/{merchant,order,cashier}/index.vue
|
|
|
+-- 写错会拼成查不到的模块名 ⇒ **静默落到 error/404.vue**(不是报错)。
|
|
|
+-- ★ 页面文件由 Tasks 19-21 创建;本脚本执行时**不**检查文件是否存在。
|
|
|
+-- · 目录行本身也必须在 `sys_role_menus` 里(`UserService.buildMenuTree` 只会把 parent
|
|
|
+-- 同在集合内的节点挂上树;父不在集合里的子节点被**直接丢弃**,不报错)。
|
|
|
+-- · 用 `MAX("order") + 1` 现算,不写死数字:目录取顶层 MAX+1;页面在目录下 1/2/3;
|
|
|
+-- 按钮在各自页面下 1/2/3 与 1。`sys_menu` 对 (parent_id, "order") 没有唯一约束
|
|
|
+-- ⇒ 撞序不报错,只是两行同序。
|
|
|
+--
|
|
|
+-- ── ★★ 幂等形状(R49):不依赖上一段的 RETURNING ───────────────────────
|
|
|
+-- `har-cashier-menu.sql` 用 `WITH page AS (INSERT … RETURNING id)`,其缺点是
|
|
|
+-- 「页面已存在(未插入)⇒ `page` 为空 ⇒ 按钮与授权一起不插」,无法补齐半套数据。
|
|
|
+-- 本脚本改为**每段独立**:插入后用**稳定锚**从 `sys_menu` 重新选出这些行:
|
|
|
+-- · 目录锚 `route_name='payment-unified'`(身份),不锚 `name`(标签可被人手工改)
|
|
|
+-- · 页面锚 `route_path`
|
|
|
+-- · 按钮锚 `(parent_id, permission)`
|
|
|
+-- · 授权锚 `(role_id, menu_id)`
|
|
|
+-- ⇒ 「目录在、页面缺」「页面在、按钮/授权缺」等半套状态重跑都能补齐,且不插重复行、不报错。
|
|
|
+--
|
|
|
+-- ── uuid / id ────────────────────────────────────────────────────────
|
|
|
+-- 每段 `INSERT INTO sys_menu` 都显式给 `uuid` 赋 `gen_random_uuid()`(SELECT 每输出一行
|
|
|
+-- 求值一次);`id` 省略,由列默认值生成。
|
|
|
+-- `created_time/updated_time = CURRENT_TIMESTAMP`,`status='0'`。
|
|
|
+--
|
|
|
+-- ── ★ 已知未覆盖(如实)──────────────────────────────────────────────
|
|
|
+-- · `route_name` 与前端 SFC `defineOptions.name` 的一致性、`component_path` 指向的文件
|
|
|
+-- 是否存在:Task 17 不检查(页面尚未创建),由 Task 22 的 `verify-unified-menu.mjs` 覆盖。
|
|
|
+-- · 本脚本只做静态断言(`UnifiedAdminWiringTest`),未在数据库执行过。
|
|
|
+
|
|
|
+-- ══════════════════════════════════════════════════════════════════════
|
|
|
+-- 1) 顶层目录:统一支付平台
|
|
|
+-- ══════════════════════════════════════════════════════════════════════
|
|
|
+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,
|
|
|
+ (SELECT COALESCE(MAX("order"), 0) + 1 FROM sys_menu WHERE parent_id IS NULL),
|
|
|
+ NULL, 'money',
|
|
|
+ 'payment-unified', '/payment/unified', NULL,
|
|
|
+ NULL, false, false, false, false, '统一支付平台', NULL,
|
|
|
+ gen_random_uuid(), NULL, '0', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
|
|
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu
|
|
|
+ WHERE route_name = 'payment-unified' AND type = 1
|
|
|
+ AND status = '0' AND parent_id IS NULL);
|
|
|
+
|
|
|
+-- ══════════════════════════════════════════════════════════════════════
|
|
|
+-- 2) 三个页面(type=2):商户配置 / 订单管理 / 收银台
|
|
|
+-- 父目录按 route_name 从 sys_menu 重新选出;页面按 route_path 判重。
|
|
|
+-- ══════════════════════════════════════════════════════════════════════
|
|
|
+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 v.name, 2, v.ord, v.perm, NULL, v.route_name, v.route_path, v.component_path,
|
|
|
+ NULL, false, true, false, false, v.name, NULL,
|
|
|
+ gen_random_uuid(), dir.id, '0', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
|
|
+ FROM (SELECT id FROM sys_menu
|
|
|
+ WHERE route_name = 'payment-unified' AND type = 1
|
|
|
+ AND status = '0' AND parent_id IS NULL) AS dir
|
|
|
+ CROSS JOIN (VALUES
|
|
|
+ ('商户配置', 1, 'module_payment:unified:merchant:list',
|
|
|
+ 'payment-unified-merchant', '/payment/unified/merchant',
|
|
|
+ 'module_payment/unified/merchant/index'),
|
|
|
+ ('订单管理', 2, 'module_payment:unified:order:list',
|
|
|
+ 'payment-unified-order', '/payment/unified/order',
|
|
|
+ 'module_payment/unified/order/index'),
|
|
|
+ ('收银台', 3, 'module_payment:unified:cashier:list',
|
|
|
+ 'payment-unified-cashier', '/payment/unified/cashier',
|
|
|
+ 'module_payment/unified/cashier/index')
|
|
|
+ ) AS v(name, ord, perm, route_name, route_path, component_path)
|
|
|
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu m
|
|
|
+ WHERE m.route_path = v.route_path AND m.type = 2 AND m.status = '0');
|
|
|
+
|
|
|
+-- ══════════════════════════════════════════════════════════════════════
|
|
|
+-- 3) 四个按钮(type=3):订单详情 / 同步上游 / 发起退款 / 发起收款
|
|
|
+-- 父页面按 route_path 从 sys_menu 重新选出;按钮按 (parent_id, permission) 判重。
|
|
|
+-- ★ 漏了某行 ⇒ 非超管点对应按钮必 403,而超管测不出来。
|
|
|
+-- ══════════════════════════════════════════════════════════════════════
|
|
|
+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 v.name, 3, v.ord, v.perm, NULL, NULL, NULL, NULL,
|
|
|
+ NULL, false, false, false, false, v.name, NULL,
|
|
|
+ gen_random_uuid(), p.id, '0', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
|
|
+ FROM (VALUES
|
|
|
+ ('订单详情', 1, 'module_payment:unified:order:detail', '/payment/unified/order'),
|
|
|
+ ('同步上游', 2, 'module_payment:unified:order:sync', '/payment/unified/order'),
|
|
|
+ ('发起退款', 3, 'module_payment:unified:order:refund', '/payment/unified/order'),
|
|
|
+ ('发起收款', 1, 'module_payment:unified:cashier:create', '/payment/unified/cashier')
|
|
|
+ ) AS v(name, ord, perm, page_path)
|
|
|
+ JOIN sys_menu p ON p.route_path = v.page_path AND p.type = 2 AND p.status = '0'
|
|
|
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu b
|
|
|
+ WHERE b.parent_id = p.id AND b.permission = v.perm
|
|
|
+ AND b.type = 3 AND b.status = '0');
|
|
|
+
|
|
|
+-- ══════════════════════════════════════════════════════════════════════
|
|
|
+-- 4) 授权:目录 + 三页 + 四按钮(8 行)× role 1 与 role 2
|
|
|
+-- 三类锚**一次**选出 8 行,再 CROSS JOIN 两个角色;按 (role_id, menu_id) 判重。
|
|
|
+-- ★ 目录行必须授:`buildMenuTree` 会静默丢弃父不在集合里的子节点。
|
|
|
+-- ══════════════════════════════════════════════════════════════════════
|
|
|
+INSERT INTO sys_role_menus (role_id, menu_id)
|
|
|
+SELECT r.role_id, m.id
|
|
|
+ FROM (SELECT id FROM sys_menu
|
|
|
+ WHERE status = '0'
|
|
|
+ AND ((route_name = 'payment-unified' AND type = 1)
|
|
|
+ OR (route_path IN ('/payment/unified/merchant',
|
|
|
+ '/payment/unified/order',
|
|
|
+ '/payment/unified/cashier') AND type = 2)
|
|
|
+ OR (permission IN ('module_payment:unified:order:detail',
|
|
|
+ 'module_payment:unified:order:sync',
|
|
|
+ 'module_payment:unified:order:refund',
|
|
|
+ 'module_payment:unified:cashier:create') AND type = 3))
|
|
|
+ ) AS m
|
|
|
+ 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 = m.id);
|
|
|
+
|
|
|
+
|
|
|
+-- ── 执行前检查(**先跑这两条**)─────────────────────────────────────────
|
|
|
+--
|
|
|
+-- 1) 目录是否已存在(已存在时第 1 段不插,其余段照常补齐缺失的页面/按钮/授权)
|
|
|
+-- SELECT id, name, title, type, route_name, route_path FROM sys_menu
|
|
|
+-- WHERE route_name = 'payment-unified' AND type = 1 AND status = '0' AND parent_id IS NULL;
|
|
|
+-- 期望(首次执行前):**0 行**。若 1 行,记下 id —— 执行后核对同一行。
|
|
|
+--
|
|
|
+-- 2) role 1 / role 2 是否存在(授权段引用的两个角色;缺哪个,那个角色的授权行就插不出来)
|
|
|
+-- SELECT id, name, code FROM sys_role WHERE id IN (1, 2) ORDER BY id;
|
|
|
+-- 期望:**2 行**(id=1 管理员 / id=2 租户角色)。
|
|
|
+--
|
|
|
+-- ── 只读核对(**执行后**手工跑一遍)─────────────────────────────────────
|
|
|
+--
|
|
|
+-- 3) 目录:期望 **1 行**
|
|
|
+-- SELECT id, name, title, type, "order", route_name, route_path, uuid
|
|
|
+-- FROM sys_menu
|
|
|
+-- WHERE route_name = 'payment-unified' AND type = 1 AND status = '0' AND parent_id IS NULL;
|
|
|
+-- 期望:type=1,route_path='/payment/unified',uuid 非空(36 字符)。
|
|
|
+--
|
|
|
+-- 4) 页面:期望 **3 行**
|
|
|
+-- SELECT id, name, type, "order", permission, route_name, route_path, component_path,
|
|
|
+-- keep_alive, uuid
|
|
|
+-- FROM sys_menu
|
|
|
+-- WHERE route_path IN ('/payment/unified/merchant',
|
|
|
+-- '/payment/unified/order',
|
|
|
+-- '/payment/unified/cashier')
|
|
|
+-- AND type = 2 AND status = '0'
|
|
|
+-- ORDER BY "order";
|
|
|
+-- 期望 3 行:
|
|
|
+-- 商户配置 order=1 route_name='payment-unified-merchant' permission='module_payment:unified:merchant:list'
|
|
|
+-- 订单管理 order=2 route_name='payment-unified-order' permission='module_payment:unified:order:list'
|
|
|
+-- 收银台 order=3 route_name='payment-unified-cashier' permission='module_payment:unified:cashier:list'
|
|
|
+-- 三行 keep_alive=true、uuid 非空、component_path 分别为
|
|
|
+-- module_payment/unified/merchant/index 等(相对 views/,无前缀无后缀)。
|
|
|
+--
|
|
|
+-- 5) 按钮:期望 **4 行**
|
|
|
+-- SELECT m.id, m.name, m.type, m."order", m.permission, p.route_path
|
|
|
+-- FROM sys_menu m JOIN sys_menu p ON m.parent_id = p.id
|
|
|
+-- WHERE m.type = 3 AND m.status = '0'
|
|
|
+-- AND p.route_path IN ('/payment/unified/order', '/payment/unified/cashier')
|
|
|
+-- ORDER BY p.route_path, m."order";
|
|
|
+-- 期望 4 行:
|
|
|
+-- /payment/unified/order order=1 订单详情 module_payment:unified:order:detail
|
|
|
+-- /payment/unified/order order=2 同步上游 module_payment:unified:order:sync
|
|
|
+-- /payment/unified/order order=3 发起退款 module_payment:unified:order:refund
|
|
|
+-- /payment/unified/cashier order=1 发起收款 module_payment:unified:cashier:create
|
|
|
+--
|
|
|
+-- 6) 授权:期望 **16 行**(2 个角色 × 8 行:目录 1 + 页面 3 + 按钮 4)
|
|
|
+-- 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.status = '0'
|
|
|
+-- AND ((m.route_name = 'payment-unified' AND m.type = 1)
|
|
|
+-- OR (m.route_path IN ('/payment/unified/merchant',
|
|
|
+-- '/payment/unified/order',
|
|
|
+-- '/payment/unified/cashier') AND m.type = 2)
|
|
|
+-- OR (m.permission IN ('module_payment:unified:order:detail',
|
|
|
+-- 'module_payment:unified:order:sync',
|
|
|
+-- 'module_payment:unified:order:refund',
|
|
|
+-- 'module_payment:unified:cashier:create') AND m.type = 3))
|
|
|
+-- ORDER BY rm.role_id, m.type, m."order";
|
|
|
+-- 期望:role 1 **8 行**、role 2 **8 行**,合计 16 行。
|
|
|
+-- ★ 判据不是「总行数对不对」,而是**两个角色各 8 行都在**:
|
|
|
+-- 少目录行 ⇒ 该角色整棵子树被 `buildMenuTree` 静默丢弃;
|
|
|
+-- 少按钮行 ⇒ 非超管点对应按钮必 403,而超管测不出来。
|
|
|
+--
|
|
|
+-- 7) 端到端(**唯一能证明权限串没写错的一条**):用一个**非超管**账号登录,
|
|
|
+-- 确认 7 个 `module_payment:unified:*` 都出现在
|
|
|
+-- `MenuMapper.selectPermissionsByUserId` 的返回里。
|
|
|
+-- ★ 超管账号**证明不了任何事** —— 它对 ROLE_SUPERUSER 直接放行。
|