first commit
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
// 只读证据收集:后台写接口 → 前端按钮文案(含出处),供人工核对后写进 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}`);
|
||||
}
|
||||
Reference in New Issue
Block a user