修复 R124 企业微信移动入口与部署迁移门禁

这个提交包含在:
2026-05-08 14:09:04 +08:00
父节点 377c76b5aa
当前提交 b78e82edab
共修改 14 个文件,包含 1375 行新增和 32 行删除
+178
查看文件
@@ -0,0 +1,178 @@
#!/usr/bin/env node
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
import path from "node:path";
import process from "node:process";
const rootDir = process.cwd();
const routerPath = path.join(rootDir, "client/src/router/index.tsx");
const pagesDir = path.join(rootDir, "client/src/pages");
const findings = [];
function toPosix(value) {
return value.split(path.sep).join("/");
}
function addFinding({ rule, file, line = 1, message, evidence }) {
findings.push({
rule,
file: toPosix(path.relative(rootDir, file)),
line,
message,
evidence,
});
}
function lineNumber(source, index) {
return source.slice(0, index).split("\n").length;
}
function collectFiles(dir) {
if (!existsSync(dir)) return [];
const files = [];
for (const entry of readdirSync(dir)) {
const fullPath = path.join(dir, entry);
const stat = statSync(fullPath);
if (stat.isDirectory()) {
files.push(...collectFiles(fullPath));
continue;
}
if (/Mobile.*\.tsx$/.test(entry)) {
files.push(fullPath);
}
}
return files;
}
function extractRouteBlock(source, routePath) {
const marker = `path="${routePath}"`;
const start = source.indexOf(marker);
if (start < 0) return null;
const nextRoute = source.indexOf("\n <Route", start + marker.length);
const end = nextRoute >= 0 ? nextRoute : source.indexOf("\n </Route>", start);
return {
start,
block: source.slice(start, end >= 0 ? end : start + 1200),
};
}
function scanRouter() {
if (!existsSync(routerPath)) {
addFinding({
rule: "mobile.router.exists",
file: routerPath,
message: "找不到 client/src/router/index.tsx",
});
return;
}
const source = readFileSync(routerPath, "utf8");
const mobileRoutePaths = Array.from(source.matchAll(/path="(mobile[^"]*)"/g)).map((match) => match[1]);
const routeComponents = [];
for (const routePath of mobileRoutePaths) {
const route = extractRouteBlock(source, routePath);
if (!route) continue;
const components = Array.from(route.block.matchAll(/<([A-Z][A-Za-z0-9]+Page)\s*\/>/g)).map((match) => match[1]);
routeComponents.push({ routePath, components });
for (const component of components) {
if (!component.startsWith("Mobile")) {
addFinding({
rule: "mobile.router.no_pc_page",
file: routerPath,
line: lineNumber(source, route.start),
message: `/mobile 路由不能直挂 PC 页面组件:${routePath} -> ${component}`,
evidence: component,
});
}
}
}
const requiredRoutes = new Map([
["mobile/profile", "MobileProfilePage"],
["mobile/notifications", "MobileNotificationsPage"],
["mobile/capture", "MobileCapturePage"],
["mobile/reimbursements", "MobileReimbursementsPage"],
["mobile/me", "MobileMePage"],
["mobile/approvals", "MobileApprovalsPage"],
]);
for (const [routePath, expectedComponent] of requiredRoutes) {
const route = routeComponents.find((item) => item.routePath === routePath);
if (!route) {
addFinding({
rule: "mobile.router.required_route",
file: routerPath,
message: `缺少企业微信移动入口路由:${routePath}`,
});
continue;
}
if (!route.components.includes(expectedComponent)) {
addFinding({
rule: "mobile.router.required_component",
file: routerPath,
line: lineNumber(source, source.indexOf(`path="${routePath}"`)),
message: `${routePath} 应挂载 ${expectedComponent}`,
evidence: route.components.join(", ") || "(none)",
});
}
}
console.log("[mobile-route-contract] routes");
for (const item of routeComponents) {
console.log(` ${item.routePath} -> ${item.components.join(", ") || "(no page component found)"}`);
}
}
function scanMobilePages() {
for (const file of collectFiles(pagesDir)) {
const source = readFileSync(file, "utf8");
const patterns = [
{
regex: /\bfrom ["']\.\.\/components\/ui\/table["']/g,
rule: "mobile.page.no_table_component",
message: "移动页面不应导入桌面表格组件。",
},
{
regex: /<\s*(?:Table|table)\b/g,
rule: "mobile.page.no_visible_table_markup",
message: "移动页面不应渲染桌面表格。",
},
{
regex: /page-shell-grid/g,
rule: "mobile.page.no_pc_shell_grid",
message: "移动页面不应使用 PC 页面双栏 shell 布局。",
},
];
for (const pattern of patterns) {
let match = pattern.regex.exec(source);
while (match) {
addFinding({
rule: pattern.rule,
file,
line: lineNumber(source, match.index),
message: pattern.message,
evidence: match[0],
});
match = pattern.regex.exec(source);
}
}
}
}
scanRouter();
scanMobilePages();
if (findings.length) {
console.log(`[mobile-route-contract] ${findings.length} finding(s)`);
for (const finding of findings) {
console.log(`ERROR ${finding.rule} ${finding.file}:${finding.line} ${finding.message}`);
if (finding.evidence) {
console.log(` evidence: ${finding.evidence}`);
}
}
process.exitCode = 1;
} else {
console.log("[mobile-route-contract] no findings");
}
+28
查看文件
@@ -16,6 +16,9 @@ const scanRoots = [
const sourceExtensions = new Set([".js", ".jsx", ".ts", ".tsx"]);
const forbiddenCopy = ["先处理", "清爽"];
const userFacingBusinessTermFiles = new Set([
"client/src/pages/MobileCapturePage.tsx",
"client/src/pages/MobileNotificationsPage.tsx",
"client/src/pages/MobileProfilePage.tsx",
"client/src/pages/MobileReimbursementDetailPage.tsx",
"client/src/pages/ReimbursementDetailPage.tsx",
"client/src/components/travel/TravelPaymentTrackCard.tsx",
@@ -127,6 +130,30 @@ function scanNativeFileInputs(file, source) {
}
}
function scanMobileCapturePdfInputs(file, source) {
const normalized = toPosix(path.relative(rootDir, file));
if (normalized !== "client/src/pages/MobileCapturePage.tsx") {
return;
}
const fileInputPattern = /<input\b[^>]*\btype=["']file["'][^>]*>/g;
let match = fileInputPattern.exec(source);
while (match) {
const inputSource = match[0];
if (/application\/pdf/i.test(inputSource) && /\bcapture\b/i.test(inputSource)) {
addFinding({
rule: "form.no_pdf_capture_input",
severity: "error",
file,
line: lineNumber(source, match.index),
message: "PDF 上传入口不能带 capture,否则企业微信容易直接打开摄像头。",
evidence: inputSource.replace(/\s+/g, " ").slice(0, 160),
});
}
match = fileInputPattern.exec(source);
}
}
function scanUserFacingBusinessTerms(file, source) {
const normalized = toPosix(path.relative(rootDir, file));
if (!userFacingBusinessTermFiles.has(normalized)) {
@@ -226,6 +253,7 @@ function scanFile(file) {
scanUserFacingBusinessTerms(file, source);
scanMobileDarkControls(file, source);
scanNativeFileInputs(file, source);
scanMobileCapturePdfInputs(file, source);
scanRawBusinessIdentifiers(file, source);
scanNavigationTitleCoverage(file, source);
}