114 lines
6.2 KiB
JavaScript
114 lines
6.2 KiB
JavaScript
// 只读证据收集:后台写接口 → 前端按钮文案(含出处),供人工核对后写进 set-known-audit-logs.js
|
|
//
|
|
// node bin/checks/evidence.js 输出到 stdout(很长,建议重定向)
|
|
// node bin/checks/evidence.js > ev.txt
|
|
//
|
|
// 链路:controller 端点 → api 函数(按 url+method 匹配)→ 调用该函数的 handler → 绑定该 handler 的按钮文案
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const ROOT = path.resolve(__dirname, '..', '..');
|
|
const UI = path.join(ROOT, 'ruoyi-ui', 'src');
|
|
|
|
function walk(dir, out, filter) {
|
|
if (!fs.existsSync(dir)) return out;
|
|
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const p = path.join(dir, e.name);
|
|
if (e.isDirectory()) { if (e.name !== 'node_modules' && e.name !== 'dist') walk(p, out, filter); }
|
|
else if (!filter || filter(e.name)) out.push(p);
|
|
}
|
|
return out;
|
|
}
|
|
const norm = (u) => String(u).replace(/\$\{[^}]*\}/g, '{}').replace(/\{[^}]*\}/g, '{}')
|
|
.replace(/\/+/g, '/').replace(/\/$/, '').replace(/['"`;\s]/, '');
|
|
|
|
// ---------- 1. api 函数 → url/method ----------
|
|
const apiEntries = []; // {fn, url, method, module}
|
|
for (const f of walk(path.join(UI, 'api'), [], (n) => n.endsWith('.js'))) {
|
|
const src = fs.readFileSync(f, 'utf8');
|
|
const mod = '@/api/' + path.relative(path.join(UI, 'api'), f).replace(/\\/g, '/').replace(/\.js$/, '');
|
|
for (const m of src.matchAll(/export function (\w+)\s*\(([\s\S]{0,600}?)\n\}/g)) {
|
|
const body = m[2];
|
|
const url = (body.match(/url:\s*([`'"])([^`'"]+)\1/) || [])[2];
|
|
const method = ((body.match(/method:\s*['"](\w+)['"]/) || [])[1] || 'get').toUpperCase();
|
|
if (url) apiEntries.push({ fn: m[1], url, method, module: mod, file: path.relative(ROOT, f) });
|
|
}
|
|
}
|
|
|
|
// ---------- 2. .vue 里的按钮 + handler → api 调用 ----------
|
|
const vueFiles = walk(path.join(UI, 'views'), [], (n) => n.endsWith('.vue'));
|
|
const fnEvidence = new Map(); // fn -> [{vue,line,label}]
|
|
for (const f of vueFiles) {
|
|
const src = fs.readFileSync(f, 'utf8');
|
|
if (!/from ['"]@\/api\//.test(src)) continue;
|
|
const rel = path.relative(ROOT, f);
|
|
// 按钮:<el-button ... @click="h" ...>文字</el-button> / el-dropdown-item
|
|
for (const m of src.matchAll(/<el-(?:button|dropdown-item)[^>]*?@click(?:\.native)?="([\w.]+)[^"]*"[\s\S]{0,400}?>\s*([^<>{}]{1,20}?)\s*<\/el-(?:button|dropdown-item)>/g)) {
|
|
const handler = m[1].split('.')[0];
|
|
const label = m[2].trim();
|
|
const line = (src.slice(0, m.index).match(/\n/g) || []).length + 1;
|
|
// handler 函数体:从定义处到下一个顶层方法定义(截断保护),比按第一个 } 截更可靠
|
|
const hm = src.match(new RegExp('(?:^|\\n)\\s*' + handler + '\\s*\\([^)]*\\)\\s*\\{'));
|
|
if (!hm) continue;
|
|
const rest = src.slice(hm.index + hm[0].length);
|
|
const nextDef = rest.search(/\n\s{0,4}[A-Za-z_$][\w$]*\s*\([^)]*\)\s*\{/);
|
|
const body = rest.slice(0, nextDef < 0 ? 3000 : Math.min(nextDef, 3000));
|
|
for (const e of apiEntries) {
|
|
if (new RegExp('\\b' + e.fn + '\\s*\\(').test(body)) {
|
|
if (!fnEvidence.has(e.fn)) fnEvidence.set(e.fn, []);
|
|
fnEvidence.get(e.fn).push({ vue: rel, line, label });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------- 3. controller 端点 ----------
|
|
const isBlockMember = (l) => /^\s*(\/\/|\*|\/\*|@)/.test(l) || (/^\s*[\w.]+\s*=/.test(l) && /[,)]\s*$/.test(l));
|
|
const endpoints = [];
|
|
for (const f of walk(path.join(ROOT, 'skins-service'), [], (n) => n.endsWith('Controller.java'))
|
|
.concat(walk(path.join(ROOT, 'skins-promo'), [], (n) => n.endsWith('Controller.java')))) {
|
|
const src = fs.readFileSync(f, 'utf8');
|
|
const base = (src.match(/@RequestMapping\(\s*(?:value\s*=\s*)?"([^"]*)"/) || [, null])[1];
|
|
if (!base || !base.startsWith('/admin')) continue;
|
|
const lines = src.split(/\r?\n/);
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const mm = lines[i].match(/^\s*@(Get|Post|Put|Delete)Mapping\b(.*)$/);
|
|
if (!mm) continue;
|
|
if (mm[1].toUpperCase() === 'GET' && !/reset|clear|delete|remove|update|save|apply|generate|sync|send|publish|toggle|adjust|grant|cancel|recalc/i.test(lines[i])) {
|
|
// GET 默认跳过,除非名字像改数据
|
|
}
|
|
let top = i; for (let j = i - 1; j >= 0; j--) { if (lines[j].trim() === '' || !isBlockMember(lines[j])) break; top = j; }
|
|
const block = lines.slice(top, i).join('\n');
|
|
let name = null;
|
|
for (let j = i + 1; j < Math.min(i + 14, lines.length); j++) {
|
|
const s = lines[j].match(/^\s*(?:public|protected|private)\s+[\w<>\[\],\.\? ]+\s+(\w+)\s*\(/); if (s) { name = s[1]; break; } }
|
|
const sub = (mm[2].match(/"([^"]*)"/) || [, ''])[1];
|
|
endpoints.push({
|
|
controller: path.basename(f, '.java'), method: name, http: mm[1].toUpperCase(),
|
|
url: base + sub, human: /menu\s*=/.test(block) || /action\s*=/.test(block), hasLog: /@Log\(/.test(block),
|
|
});
|
|
}
|
|
}
|
|
|
|
// ---------- 4. 关联 ----------
|
|
const out = [];
|
|
for (const ep of endpoints) {
|
|
if (ep.http === 'GET' && !/reset|clear|delete|remove|update|save|apply|generate|sync|send|publish|toggle|adjust|grant|cancel|recalc/i.test(ep.method || '')) continue;
|
|
if (ep.http === 'GET' && !/reset|clear|delete|remove|update|save|apply|generate|sync|send|publish|toggle|adjust|grant|cancel|recalc/i.test(ep.url)) continue;
|
|
const hit = apiEntries.find((e) => e.method === ep.http && norm(e.url) === norm(ep.url))
|
|
|| apiEntries.find((e) => e.method === ep.http && norm(e.url).startsWith(norm(ep.url) + '/'));
|
|
const ev = hit ? (fnEvidence.get(hit.fn) || []) : [];
|
|
out.push({ ep, hit, ev });
|
|
}
|
|
|
|
const state = (ep) => (!ep.hasLog ? '不记' : ep.human ? '已人话' : '旧JSON');
|
|
const onlyTodo = process.argv.includes('--todo');
|
|
console.log('controller.method | HTTP url | 现状 | 建议文案 | 证据');
|
|
for (const { ep, hit, ev } of out) {
|
|
if (onlyTodo && ep.human) continue;
|
|
const labels = [...new Set(ev.map((x) => x.label))];
|
|
const suggest = labels.length === 1 ? labels[0] : labels.length === 0 ? '—(无按钮证据)' : labels.map((l) => l + '?').join('/');
|
|
const evidence = ev.slice(0, 2).map((x) => `${x.vue}:${x.line}`).join(' ') || (hit ? `api ${hit.fn} 无按钮绑定` : '无 api');
|
|
console.log(`${(ep.controller + '.' + ep.method).padEnd(52)} ${ep.http.padEnd(6)} ${ep.url.padEnd(44)} ${state(ep).padEnd(6)} ${suggest.padEnd(22)} ${evidence}`);
|
|
}
|