|
@@ -43,12 +43,28 @@ const PAGES = [
|
|
|
},
|
|
},
|
|
|
];
|
|
];
|
|
|
|
|
|
|
|
-// 四个按钮的 (permission -> 父页面 route_path) 必须恰好是这四对。
|
|
|
|
|
|
|
+// 四个按钮的 (name -> permission -> 父页面 route_path) 三元组必须恰好是这四组。
|
|
|
const BUTTONS = [
|
|
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"];
|
|
const FORBIDDEN_PERMISSIONS = ["merchant:save", "merchant:delete"];
|
|
@@ -224,9 +240,6 @@ function checkComponentFiles(ctx) {
|
|
|
|
|
|
|
|
function checkPagePermissions(ctx) {
|
|
function checkPagePermissions(ctx) {
|
|
|
const problems = [...ctx.sharedProblems];
|
|
const problems = [...ctx.sharedProblems];
|
|
|
- if (ctx.pageRows.length === 0) {
|
|
|
|
|
- problems.push("no page rows in SQL");
|
|
|
|
|
- }
|
|
|
|
|
for (const row of ctx.pageRows) {
|
|
for (const row of ctx.pageRows) {
|
|
|
if (!row.permission) {
|
|
if (!row.permission) {
|
|
|
problems.push(`route ${row.routePath}: page row has no 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;
|
|
return problems;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -264,9 +291,15 @@ function checkRoles(ctx) {
|
|
|
problems.push("no INSERT INTO sys_role_menus authorization segment");
|
|
problems.push("no INSERT INTO sys_role_menus authorization segment");
|
|
|
return problems;
|
|
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(
|
|
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;
|
|
return problems;
|
|
@@ -286,21 +319,38 @@ function checkDirectoryRow(ctx) {
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
if (!/gen_random_uuid\s*\(\s*\)\s*,\s*NULL\s*,\s*'0'/.test(segment)) {
|
|
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;
|
|
return problems;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function checkIdempotentGuards(ctx) {
|
|
function checkIdempotentGuards(ctx) {
|
|
|
const problems = [...ctx.sharedProblems];
|
|
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;
|
|
return problems;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -310,8 +360,10 @@ function checkUuidDefaults(ctx) {
|
|
|
if (uuidCount < 3) {
|
|
if (uuidCount < 3) {
|
|
|
problems.push(`gen_random_uuid() appears ${uuidCount} times, expected >= 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) => {
|
|
ctx.sysMenuSegments.forEach((segment, index) => {
|
|
|
if (!/gen_random_uuid\s*\(\s*\)/i.test(segment)) {
|
|
if (!/gen_random_uuid\s*\(\s*\)/i.test(segment)) {
|
|
@@ -326,20 +378,31 @@ function checkButtonParents(ctx) {
|
|
|
if (ctx.buttonRows.length !== BUTTONS.length) {
|
|
if (ctx.buttonRows.length !== BUTTONS.length) {
|
|
|
problems.push(`expected ${BUTTONS.length} button rows, found ${ctx.buttonRows.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) {
|
|
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) {
|
|
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;
|
|
return problems;
|
|
@@ -348,13 +411,13 @@ function checkButtonParents(ctx) {
|
|
|
const CHECKS = [
|
|
const CHECKS = [
|
|
|
["route_name 与 SFC defineOptions.name 逐字一致", checkRouteNames],
|
|
["route_name 与 SFC defineOptions.name 逐字一致", checkRouteNames],
|
|
|
["component_path 指向的 SFC 文件存在", checkComponentFiles],
|
|
["component_path 指向的 SFC 文件存在", checkComponentFiles],
|
|
|
- ["页面行 permission 存在且前缀正确", checkPagePermissions],
|
|
|
|
|
|
|
+ ["页面行 permission 逐页等于契约值且前缀正确", checkPagePermissions],
|
|
|
["7 个权限串齐全且无 merchant:save/merchant:delete", checkPermissionStrings],
|
|
["7 个权限串齐全且无 merchant:save/merchant:delete", checkPermissionStrings],
|
|
|
["授权段包含 role 1 与 role 2", checkRoles],
|
|
["授权段包含 role 1 与 role 2", checkRoles],
|
|
|
["目录行 route_name='payment-unified' / icon='money' / parent_id NULL", checkDirectoryRow],
|
|
["目录行 route_name='payment-unified' / icon='money' / parent_id NULL", checkDirectoryRow],
|
|
|
["每个 INSERT 都带 WHERE NOT EXISTS", checkIdempotentGuards],
|
|
["每个 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;
|
|
let ok = 0;
|