Browse Source

fix(统一支付前端): 菜单契约脚本 R78 评审修复

alphaH 1 week ago
parent
commit
87935348cd
1 changed files with 96 additions and 33 deletions
  1. 96 33
      frontend/scripts/verify-unified-menu.mjs

+ 96 - 33
frontend/scripts/verify-unified-menu.mjs

@@ -43,12 +43,28 @@ const PAGES = [
   },
 ];
 
-// 四个按钮的 (permission -> 父页面 route_path) 必须恰好是这四对。
+// 四个按钮的 (name -> permission -> 父页面 route_path) 三元组必须恰好是这四组。
 const BUTTONS = [
-  { permission: "module_payment:unified:order:detail", pagePath: "/payment/unified/order" },
-  { permission: "module_payment:unified:order:sync", pagePath: "/payment/unified/order" },
-  { permission: "module_payment:unified:order:refund", pagePath: "/payment/unified/order" },
-  { permission: "module_payment:unified:cashier:create", pagePath: "/payment/unified/cashier" },
+  {
+    name: "订单详情",
+    permission: "module_payment:unified:order:detail",
+    pagePath: "/payment/unified/order",
+  },
+  {
+    name: "同步上游",
+    permission: "module_payment:unified:order:sync",
+    pagePath: "/payment/unified/order",
+  },
+  {
+    name: "发起退款",
+    permission: "module_payment:unified:order:refund",
+    pagePath: "/payment/unified/order",
+  },
+  {
+    name: "发起收款",
+    permission: "module_payment:unified:cashier:create",
+    pagePath: "/payment/unified/cashier",
+  },
 ];
 
 const FORBIDDEN_PERMISSIONS = ["merchant:save", "merchant:delete"];
@@ -224,9 +240,6 @@ function checkComponentFiles(ctx) {
 
 function checkPagePermissions(ctx) {
   const problems = [...ctx.sharedProblems];
-  if (ctx.pageRows.length === 0) {
-    problems.push("no page rows in SQL");
-  }
   for (const row of ctx.pageRows) {
     if (!row.permission) {
       problems.push(`route ${row.routePath}: page row has no permission`);
@@ -236,6 +249,20 @@ function checkPagePermissions(ctx) {
       );
     }
   }
+  for (const page of PAGES) {
+    const row = ctx.pageRows.find((candidate) => candidate.routePath === page.routePath);
+    if (!row) {
+      problems.push(
+        `route ${page.routePath}: no page row in SQL (expected permission '${page.permission}')`
+      );
+      continue;
+    }
+    if (row.permission !== page.permission) {
+      problems.push(
+        `route ${page.routePath}: permission '${row.permission}' != expected '${page.permission}'`
+      );
+    }
+  }
   return problems;
 }
 
@@ -264,9 +291,15 @@ function checkRoles(ctx) {
     problems.push("no INSERT INTO sys_role_menus authorization segment");
     return problems;
   }
-  if (!/VALUES\s*\(\s*1\s*\)\s*,\s*\(\s*2\s*\)/.test(ctx.authSegment)) {
+  // 只对角色数字取集合(顺序无关);仍假设角色列表是 `(VALUES (n), (n), ...)` 形状。
+  const valuesMatch = /VALUES\s*((?:\(\s*\d+\s*\)\s*,?\s*)+)/i.exec(ctx.authSegment);
+  const roleIds = valuesMatch
+    ? [...valuesMatch[1].matchAll(/\(\s*(\d+)\s*\)/g)].map((match) => Number(match[1]))
+    : [];
+  const roleSet = new Set(roleIds);
+  if (!roleSet.has(1) || !roleSet.has(2)) {
     problems.push(
-      "authorization segment lacks role pair (VALUES (1), (2)) — role 1 and role 2 required"
+      `authorization role VALUES [${roleIds.join(", ")}] must contain role 1 and role 2`
     );
   }
   return problems;
@@ -286,21 +319,38 @@ function checkDirectoryRow(ctx) {
     );
   }
   if (!/gen_random_uuid\s*\(\s*\)\s*,\s*NULL\s*,\s*'0'/.test(segment)) {
-    problems.push("directory row parent_id is not NULL (expected gen_random_uuid(), NULL, '0')");
+    problems.push(
+      "directory row route_path '/payment/unified': parent_id is not NULL (expected gen_random_uuid(), NULL, '0')"
+    );
   }
   return problems;
 }
 
 function checkIdempotentGuards(ctx) {
   const problems = [...ctx.sharedProblems];
-  const menuInserts = countMatches(ctx.sql, /INSERT\s+INTO\s+sys_menu/gi);
-  const roleInserts = countMatches(ctx.sql, /INSERT\s+INTO\s+sys_role_menus/gi);
-  const guards = countMatches(ctx.sql, /WHERE\s+NOT\s+EXISTS/gi);
-  if (menuInserts + roleInserts > guards) {
-    problems.push(
-      `INSERT INTO sys_menu=${menuInserts} + INSERT INTO sys_role_menus=${roleInserts} > WHERE NOT EXISTS=${guards}`
-    );
+  const starts = [...ctx.sql.matchAll(/INSERT\s+INTO\s+(\w+)/gi)].map((match) => ({
+    index: match.index,
+    table: match[1],
+  }));
+  if (starts.length === 0 && ctx.sharedProblems.length === 0) {
+    problems.push(`no INSERT INTO statements found in ${SQL_PATH}`);
+    return problems;
   }
+  // 逐段统计:每段(切到下一个 INSERT INTO 为止)必须自带 WHERE NOT EXISTS;
+  // 这比「全局条数 ≤」更严,且失败时能点名到具体是哪一段。
+  const seenByTable = new Map();
+  starts.forEach((start, index) => {
+    const nextStart = index + 1 < starts.length ? starts[index + 1].index : ctx.sql.length;
+    const segment = ctx.sql.slice(start.index, nextStart);
+    const guards = countMatches(segment, /WHERE\s+NOT\s+EXISTS/gi);
+    const ordinal = (seenByTable.get(start.table) ?? 0) + 1;
+    seenByTable.set(start.table, ordinal);
+    if (guards === 0) {
+      problems.push(
+        `INSERT INTO ${start.table} #${ordinal} (segment ${index + 1}): INSERT=1, WHERE NOT EXISTS=${guards}`
+      );
+    }
+  });
   return problems;
 }
 
@@ -310,8 +360,10 @@ function checkUuidDefaults(ctx) {
   if (uuidCount < 3) {
     problems.push(`gen_random_uuid() appears ${uuidCount} times, expected >= 3`);
   }
-  if (ctx.sysMenuSegments.length !== 3) {
-    problems.push(`expected 3 INSERT INTO sys_menu segments, found ${ctx.sysMenuSegments.length}`);
+  if (ctx.sysMenuSegments.length < 3) {
+    problems.push(
+      `expected >= 3 INSERT INTO sys_menu segments, found ${ctx.sysMenuSegments.length}`
+    );
   }
   ctx.sysMenuSegments.forEach((segment, index) => {
     if (!/gen_random_uuid\s*\(\s*\)/i.test(segment)) {
@@ -326,20 +378,31 @@ function checkButtonParents(ctx) {
   if (ctx.buttonRows.length !== BUTTONS.length) {
     problems.push(`expected ${BUTTONS.length} button rows, found ${ctx.buttonRows.length}`);
   }
-  const expectedByPermission = new Map(
-    BUTTONS.map((button) => [button.permission, button.pagePath])
-  );
+  const expectedByName = new Map(BUTTONS.map((button) => [button.name, button]));
   for (const row of ctx.buttonRows) {
-    const expectedPath = expectedByPermission.get(row.permission);
-    if (expectedPath === undefined) {
-      problems.push(`unexpected button permission '${row.permission}' -> ${row.pagePath}`);
-    } else if (row.pagePath !== expectedPath) {
-      problems.push(`button ${row.permission} -> ${row.pagePath}, expected -> ${expectedPath}`);
+    const expected = expectedByName.get(row.name);
+    if (expected === undefined) {
+      problems.push(
+        `unexpected button name '${row.name}' (permission '${row.permission}' -> ${row.pagePath})`
+      );
+      continue;
+    }
+    if (row.permission !== expected.permission) {
+      problems.push(
+        `button '${row.name}': permission '${row.permission}' != expected '${expected.permission}'`
+      );
+    }
+    if (row.pagePath !== expected.pagePath) {
+      problems.push(
+        `button '${row.name}' (${row.permission}) -> ${row.pagePath}, expected -> ${expected.pagePath}`
+      );
     }
   }
   for (const button of BUTTONS) {
-    if (!ctx.buttonRows.some((row) => row.permission === button.permission)) {
-      problems.push(`button ${button.permission} missing (expected -> ${button.pagePath})`);
+    if (!ctx.buttonRows.some((row) => row.name === button.name)) {
+      problems.push(
+        `button '${button.name}' missing (expected ${button.permission} -> ${button.pagePath})`
+      );
     }
   }
   return problems;
@@ -348,13 +411,13 @@ function checkButtonParents(ctx) {
 const CHECKS = [
   ["route_name 与 SFC defineOptions.name 逐字一致", checkRouteNames],
   ["component_path 指向的 SFC 文件存在", checkComponentFiles],
-  ["页面行 permission 存在且前缀正确", checkPagePermissions],
+  ["页面行 permission 逐页等于契约值且前缀正确", checkPagePermissions],
   ["7 个权限串齐全且无 merchant:save/merchant:delete", checkPermissionStrings],
   ["授权段包含 role 1 与 role 2", checkRoles],
   ["目录行 route_name='payment-unified' / icon='money' / parent_id NULL", checkDirectoryRow],
   ["每个 INSERT 都带 WHERE NOT EXISTS", checkIdempotentGuards],
-  ["gen_random_uuid() 覆盖三段 sys_menu INSERT", checkUuidDefaults],
-  ["按钮权限绑定到正确父页面", checkButtonParents],
+  ["gen_random_uuid() 覆盖每段 sys_menu INSERT(≥3 段)", checkUuidDefaults],
+  ["按钮 name → permission → 父页面 route_path 三元组正确", checkButtonParents],
 ];
 
 let ok = 0;