first commit

This commit is contained in:
2026-09-21 19:21:35 +08:00
commit 6c944c4ffd
2733 changed files with 296956 additions and 0 deletions
+113
View File
@@ -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}`);
}
+32
View File
@@ -0,0 +1,32 @@
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
;(async () => {
const source = fs.readFileSync(path.join(__dirname, '../../ruoyi-ui/src/utils/operationAudit.js'), 'utf8')
const { parseOperationAudit: parse } = await import('data:text/javascript;base64,' + Buffer.from(source).toString('base64'))
const payload = { auditVersion: 1, action: '修改', target: { id: 18, name: '测试用户' }, state: 'changed', changes: [
{ label: '账户余额', before: '100', after: '200' }, { label: '账户积分', before: '50', after: '80' }
], truncated: false }
const result = parse(JSON.stringify(payload))
assert.equal(result.changes.length, 2)
assert.equal(result.changes[0].before, '100')
assert.match(result.target, /18/)
for (const state of ['failed', 'unavailable', 'unchanged']) {
const p = parse(JSON.stringify({ ...payload, state }))
assert.equal(p.changes.length, 0)
assert.ok(p.notice)
}
assert.match(parse(JSON.stringify({ ...payload, truncated: true })).notice, /上限/)
assert.equal(parse('点击【修改】|余额\t100\t200').changes.length, 1)
assert.equal(parse('点击【修改】').changes.length, 0)
assert.equal(parse('{"password":"secret"}').action, '历史记录')
assert.equal(parse('{malformed').action, '历史记录')
const dangerous = '汉字|\t\n<img onerror=alert(1)>;→'
assert.equal(parse(JSON.stringify({ ...payload, changes: [{ label: '昵称', before: '', after: dangerous }] })).changes[0].after, dangerous)
const vue = fs.readFileSync(path.join(__dirname, '../../ruoyi-ui/src/views/monitor/operlog/index.vue'), 'utf8')
assert.ok(!vue.includes('v-html'))
const compiler = require('../../ruoyi-ui/node_modules/vue-template-compiler')
const template = compiler.parseComponent(vue).template.content
assert.deepEqual(compiler.compile(template).errors, [])
console.log('operation-audit parser and Vue template PASSED')
})().catch(e => { console.error(e); process.exit(1) })
+17
View File
@@ -0,0 +1,17 @@
$ErrorActionPreference = 'Stop'
$root = Resolve-Path (Join-Path $PSScriptRoot '../..')
Push-Location $root
try {
& mvn -o -pl ruoyi-admin -am test-compile -DskipTests -q
if ($LASTEXITCODE -ne 0) { throw 'test-compile failed' }
$cpFile = Join-Path $root 'ruoyi-admin/target/audit-classpath.txt'
& mvn -o -q -pl ruoyi-admin dependency:build-classpath "-Dmdep.outputFile=$cpFile"
if ($LASTEXITCODE -ne 0) { throw 'classpath generation failed' }
$classes = @('ruoyi-admin/target/test-classes','ruoyi-admin/target/classes','ruoyi-common/target/classes','ruoyi-framework/target/classes','ruoyi-system/target/classes','skins-model/target/classes','skins-service/service-admin/target/classes')
$cp = (($classes -join ';') + ';' + (Get-Content $cpFile -Raw).Trim()).Replace('\','/')
$argFile = Join-Path $root 'ruoyi-admin/target/audit-java.args'
$arguments = @('-Dfile.encoding=UTF-8', '-cp', ('"' + $cp + '"'), 'com.ruoyi.audit.UserOperationAuditTest')
[System.IO.File]::WriteAllLines($argFile, $arguments, (New-Object System.Text.UTF8Encoding($false)))
& java "@$argFile"
if ($LASTEXITCODE -ne 0) { throw 'audit regression failed' }
} finally { Pop-Location }
+405
View File
@@ -0,0 +1,405 @@
// 按「方法名 → 注解」显式白名单写入 @Log(不做 URL 猜配,避免错位)
//
// node bin/checks/set-known-audit-logs.js 干跑
// node bin/checks/set-known-audit-logs.js --apply 落盘
//
// 按钮文案全部取自页面源码原文(每条的来源写在本表注释里,格式:views/xxx.vue:行号):
// 工具条按钮(生成招商账号/一键上架/发放CDK)优先于弹窗里的通用【确 定】——
// 运营的心智是"我点了那颗有名字的按钮",【确 定】记进日志等于没信息。
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '..', '..');
const apply = process.argv.includes('--apply');
const A = 'skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/';
const W = 'skins-service/game-wheel-service/src/main/java/com/ruoyi/game/wheel/controller/';
const O = 'skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/controller/';
const TARGETS = [
// ---- 用户列表(ttuser/index.vue:工具条 113/121/129,行内 86/96,下拉 212/218/224)----
[A + 'TtUserController.java', 'generateAccount', { menu: '用户列表', action: '生成招商账号', type: 'INSERT' }],
[A + 'TtUserController.java', 'generateRobot', { menu: '用户列表', action: '生成机器人', type: 'INSERT' }],
[A + 'TtUserController.java', 'generateBlankAccount', { menu: '用户列表', action: '生成白号', type: 'INSERT' }],
[A + 'TtUserController.java', 'generateCardMerchant', { menu: '用户列表', action: '生成卡商账号', type: 'INSERT' }],
[A + 'TtUserController.java', 'edit', { menu: '用户列表', action: '修改', type: 'UPDATE', entity: 'TtUser', mapper: 'TtUserMapper' }],
[A + 'TtUserController.java', 'editInvitationCode', { menu: '用户列表', action: '修改邀请码', type: 'UPDATE', entity: 'TtUser', mapper: 'TtUserMapper' }],
[A + 'TtUserController.java', 'remove', { menu: '用户列表', action: '删除', type: 'DELETE' }],
[A + 'TtUserController.java', 'removeUserPackSackData', { menu: '背包列表', action: '删除', type: 'DELETE' }],
[A + 'TtUserController.java', 'resetUserBalance', { menu: '用户列表', action: '重置余额积分', type: 'CLEAN', entity: 'TtUser', mapper: 'TtUserMapper', idField: 'userId' }],
[A + 'TtUserController.java', 'forceDelete', { menu: '用户列表', action: '真删除', type: 'FORCE' }],
// ---- 亏损返水(ttredPacket/lossRebate.vue:31 保存上限 / 112 发放CDK)----
[A + 'LossRebateController.java', 'grant', { menu: '亏损返水', action: '发放CDK', type: 'GRANT', entity: 'TtLossRebate', mapper: 'TtLossRebateMapper', idField: 'id' }],
[A + 'LossRebateController.java', 'saveConfig', { menu: '亏损返水', action: '保存上限', type: 'UPDATE' }],
// ---- 卡商充值(ttCardMerchantRecharge/index.vue:126 确认充值 / 160 确认修改)----
[A + 'TtCardMerchantController.java', 'cardMerchantRecharge', { menu: '卡商充值', action: '确认充值', type: 'GRANT', entity: 'TtUser', mapper: 'TtUserMapper', idField: 'userId' }],
[A + 'TtCardMerchantController.java', 'cardMerchantUpdateBalance', { menu: '卡商充值', action: '确认修改', type: 'UPDATE', entity: 'TtUser', mapper: 'TtUserMapper', idField: 'userId' }],
// ---- 生成空白卡密(blankCardKey/index.vue:5 生成空白卡密 开弹窗)----
[A + 'AdminBlankCardKeyController.java', 'generate', { menu: '生成空白卡密', action: '生成空白卡密', type: 'INSERT' }],
// ---- 疯狂转盘 游戏配置(game/wheel/config/index.vue:8 重置奖池 / 44 调整奖池 / 241 保存)----
[W + 'AdminWheelController.java', 'resetPool', { menu: '游戏配置', action: '重置奖池', type: 'CLEAN' }],
[W + 'AdminWheelController.java', 'adjustPool', { menu: '游戏配置', action: '调整奖池', type: 'GRANT' }],
[W + 'AdminWheelController.java', 'updateGameConfig', { menu: '游戏配置', action: '保存', type: 'UPDATE' }],
// ---- 疯狂转盘 奖励矩阵(game/wheel/prize/index.vue:79 保存 / 89 全部保存 → saveMatrix)----
[W + 'AdminWheelController.java', 'matrixSave', { menu: '奖励矩阵', action: '保存', type: 'UPDATE' }],
// ---- 商城列表(ttShopping/index.vue:75 一键下架 / 82 一键上架)----
[A + 'ShoppingController.java', 'putAwayAll', { menu: '商城列表', action: '一键上架', type: 'UPDATE' }],
[A + 'ShoppingController.java', 'soldOutAll', { menu: '商城列表', action: '一键下架', type: 'UPDATE' }],
// ---- Roll房列表(ttRoll/index.vue:222 新 增 / 223 修 改 / 120 删除)----
[A + 'TtRollController.java', 'add', { menu: 'Roll房列表', action: '新增', type: 'INSERT', entity: 'TtRoll', mapper: 'TtRollMapper' }],
[A + 'TtRollController.java', 'edit', { menu: 'Roll房列表', action: '修改', type: 'UPDATE', entity: 'TtRoll', mapper: 'TtRollMapper' }],
[A + 'TtRollController.java', 'remove', { menu: 'Roll房列表', action: '删除', type: 'DELETE' }],
// ---- 抽奖活动(turnoverLottery/list.vue:43 新增抽奖活动 / 86 修改 / 92 删除)----
[A + 'TurnoverLotteryAdminController.java', 'create', { menu: '抽奖活动', action: '新增抽奖活动', type: 'INSERT' }],
[A + 'TurnoverLotteryAdminController.java', 'update', { menu: '抽奖活动', action: '修改', type: 'UPDATE' }],
[A + 'TurnoverLotteryAdminController.java', 'delete', { menu: '抽奖活动', action: '删除', type: 'DELETE' }],
// ---- 奥林匹斯 奖池水位(game/olympus/rewardPool/index.vue:101/106 批量注水|抽水、111 重置调整、172/175 行内图标→弹窗 213 确定注水|抽水)----
[O + 'OlympusAdminController.java', 'rewardPoolAdjust', { menu: '奖池水位', action: '注水/抽水', type: 'GRANT' }],
[O + 'OlympusAdminController.java', 'rewardPoolAdjustBatch', { menu: '奖池水位', action: '批量注水/抽水', type: 'GRANT' }],
[O + 'OlympusAdminController.java', 'rewardPoolAdjustReset', { menu: '奖池水位', action: '重置调整', type: 'CLEAN' }],
// ---- 宝箱列表(ttBox/index.vue:48 新增 / 59 修改 / 70 删除 / 282 重置宝箱)----
[A + 'TtBoxController.java', 'add', { menu: '宝箱列表', action: '新增', type: 'INSERT', entity: 'TtBox', mapper: 'TtBoxMapper' }],
[A + 'TtBoxController.java', 'edit', { menu: '宝箱列表', action: '修改', type: 'UPDATE' }],
[A + 'TtBoxController.java', 'remove', { menu: '宝箱列表', action: '删除', type: 'DELETE' }],
// 注意:resetBox 是 @GetMapping,但它改数据(GET 不在常规扫描范围内,这里显式补上)
[A + 'TtBoxController.java', 'resetBox', { menu: '宝箱列表', action: '重置宝箱', type: 'CLEAN', entity: 'TtBox', mapper: 'TtBoxMapper', idField: 'boxId' }],
// ---- Roll 奖品/房间(awarddetali.vue:22 指定获奖者 / 23 取消奖品;rollUser.vue:10 加入机器人 / 18 加入用户)----
[A + 'TtRollController.java', 'namedWinner', { menu: '装备列表', action: '指定获奖者', type: 'INSERT' }],
[A + 'TtRollController.java', 'cancelNamedWinner', { menu: '装备列表', action: '取消奖品', type: 'DELETE' }],
[A + 'TtRollController.java', 'addRobot', { menu: '房间用户', action: '加入机器人', type: 'INSERT' }],
[A + 'TtRollController.java', 'addUser', { menu: '房间用户', action: '加入用户', type: 'INSERT' }],
// ---- 奥林匹斯 箱子配置(game/olympus/box/index.vue:53 编辑 / 180 保存 / 15+141 一键重算分布 / 179 保存为模板 / 214 应用选中模板 / 207 删除模板)----
[O + 'OlympusAdminController.java', 'boxSave', { menu: '箱子配置', action: '编辑', type: 'UPDATE' }],
[O + 'OlympusAdminController.java', 'boxOrnamentsSave', { menu: '箱子配置', action: '保存', type: 'UPDATE' }],
[O + 'OlympusAdminController.java', 'recalcBatch', { menu: '箱子配置', action: '一键重算分布', type: 'CLEAN' }],
[O + 'OlympusAdminController.java', 'templateSave', { menu: '箱子配置', action: '保存为模板', type: 'INSERT' }],
[O + 'OlympusAdminController.java', 'templateApply', { menu: '箱子配置', action: '应用选中模板', type: 'UPDATE' }],
[O + 'OlympusAdminController.java', 'templateDelete', { menu: '箱子配置', action: '删除', type: 'DELETE' }],
// ---- 奥林匹斯 奖品管理(game/olympus/prize/index.vue:44 编辑 → 87 确定 → prizeSave)----
[O + 'OlympusAdminController.java', 'prizeSave', { menu: '奖品管理', action: '编辑', type: 'UPDATE' }],
// ---- 消费排行榜奖励(rechargeRankingReward.vue:5 生成消费奖励 / 23 修改 / 8 重置→handleReset:165)----
[A + 'TtRechargeRankingRewardController.java', 'generatePromotionLevel', { menu: '消费排行榜奖励', action: '生成消费奖励', type: 'UPDATE' }],
[A + 'TtRechargeRankingRewardController.java', 'edit', { menu: '消费排行榜奖励', action: '修改', type: 'UPDATE' }],
[A + 'TtRechargeRankingRewardController.java', 'truncateRankingReward', { menu: '消费排行榜奖励', action: '重置', type: 'CLEAN' }],
// ---- 对战排行榜奖励(fightRankingReward.vue:5 生成排名奖励 / 23 修改 / 8 重置→handleReset:167)----
[A + 'TtFightRankingRewardController.java', 'generatePromotionLevel', { menu: '对战排行榜奖励', action: '生成排名奖励', type: 'UPDATE' }],
[A + 'TtFightRankingRewardController.java', 'edit', { menu: '对战排行榜奖励', action: '修改', type: 'UPDATE' }],
[A + 'TtFightRankingRewardController.java', 'truncateRankingReward', { menu: '对战排行榜奖励', action: '重置', type: 'CLEAN' }],
// ---- 饰品列表(ttOrnaments/index.vue:58 新增饰品 / 123 编辑 / 125=77 删除 / 117 发放饰品)----
[A + 'TtOrnamentsController.java', 'add', { menu: '饰品列表', action: '新增饰品', type: 'INSERT', entity: 'TtOrnament', mapper: 'TtOrnamentMapper' }],
[A + 'TtOrnamentsController.java', 'edit', { menu: '饰品列表', action: '编辑', type: 'UPDATE', entity: 'TtOrnament', mapper: 'TtOrnamentMapper' }],
[A + 'TtOrnamentsController.java', 'delete', { menu: '饰品列表', action: '删除', type: 'DELETE' }],
[A + 'TtOrnamentsController.java', 'grantOrnaments', { menu: '饰品列表', action: '发放饰品', type: 'GRANT' }],
// ================= 以下为"存量旧 JSON 格式"转人话(按钮文案已逐页核实:都是标准 新增/修改/删除)=================
// ---- 福利列表 / 福利开箱记录(ttWelfare/index.vue、welfareBoxRecords.vue)----
[A + 'TtWelfareController.java', 'add', { menu: '福利列表', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtWelfareController.java', 'edit', { menu: '福利列表', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtWelfareController.java', 'remove', { menu: '福利列表', action: '删除', type: 'DELETE' }],
[A + 'TtWelfareController.java', 'removeBoxRecords', { menu: '福利开箱记录', action: '删除', type: 'DELETE' }],
[A + 'TtWelfareController.java', 'clearBoxRecords', { menu: '福利开箱记录', action: '清空全部记录', type: 'CLEAN' }],
// ---- 每月充值福利(monthlyRecharges.vue)----
[A + 'TtWelfareMonthlyRechargesController.java', 'add', { menu: '每月充值福利', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtWelfareMonthlyRechargesController.java', 'edit', { menu: '每月充值福利', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtWelfareMonthlyRechargesController.java', 'remove', { menu: '每月充值福利', action: '删除', type: 'DELETE' }],
// ---- 首充赠送(ttBonus/firstRecharge.vue)----
[A + 'TtFirstRechargeController.java', 'add', { menu: '首充赠送', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtFirstRechargeController.java', 'edit', { menu: '首充赠送', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtFirstRechargeController.java', 'remove', { menu: '首充赠送', action: '删除', type: 'DELETE' }],
// ---- CDK管理(ttRedPack/index.vue)----
[A + 'TtRedPackController.java', 'add', { menu: 'CDK管理', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtRedPackController.java', 'edit', { menu: 'CDK管理', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtRedPackController.java', 'remove', { menu: 'CDK管理', action: '删除', type: 'DELETE' }],
// ---- 汰换记录(ttReplacementRecord/index.vue)----
[A + 'TtReplacementRecordController.java', 'add', { menu: '汰换记录', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtReplacementRecordController.java', 'edit', { menu: '汰换记录', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtReplacementRecordController.java', 'remove', { menu: '汰换记录', action: '删除', type: 'DELETE' }],
// ---- 机器人对战分组(ttFight/robotFightGroup.vue)----
[A + 'TtRobotFightGroupController.java', 'add', { menu: '机器人对战分组', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtRobotFightGroupController.java', 'edit', { menu: '机器人对战分组', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtRobotFightGroupController.java', 'remove', { menu: '机器人对战分组', action: '删除', type: 'DELETE' }],
// ---- 机器人对战分组宝箱(ttFight/robotFightGroupBox.vue)----
[A + 'TtRobotFightGroupBoxController.java', 'add', { menu: '机器人对战分组宝箱', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtRobotFightGroupBoxController.java', 'edit', { menu: '机器人对战分组宝箱', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtRobotFightGroupBoxController.java', 'remove', { menu: '机器人对战分组宝箱', action: '删除', type: 'DELETE' }],
// ---- 机器人对战配置(ttFight/robotFightConfig.vue,两个 controller 共用一页)----
[A + 'TtRobotFightJoinConfigController.java', 'add', { menu: '机器人对战配置', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtRobotFightJoinConfigController.java', 'edit', { menu: '机器人对战配置', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtRobotFightJoinConfigController.java', 'remove', { menu: '机器人对战配置', action: '删除', type: 'DELETE' }],
[A + 'TtRobotFightPriceConfigController.java', 'add', { menu: '机器人对战配置', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtRobotFightPriceConfigController.java', 'edit', { menu: '机器人对战配置', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtRobotFightPriceConfigController.java', 'remove', { menu: '机器人对战配置', action: '删除', type: 'DELETE' }],
// ---- 转赠申请(delivery/transferApply.vue:同意 / 驳回 / 确定同意)----
[A + 'TtTransferApplyController.java', 'approve', { menu: '转赠申请', action: '同意', type: 'GRANT' }],
[A + 'TtTransferApplyController.java', 'reject', { menu: '转赠申请', action: '驳回', type: 'GRANT' }],
[A + 'TtTransferApplyController.java', 'approveBatch', { menu: '转赠申请', action: '确定同意', type: 'GRANT' }],
[A + 'TtTransferApplyController.java', 'rejectBatch', { menu: '转赠申请', action: '驳回', type: 'GRANT' }],
// ---- 任务列表(ttTaskCenter/index.vue)----
[A + 'TtTaskCenterController.java', 'add', { menu: '任务列表', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtTaskCenterController.java', 'edit', { menu: '任务列表', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtTaskCenterController.java', 'remove', { menu: '任务列表', action: '删除', type: 'DELETE' }],
// ---- 开箱机会(ttOrnaments/boxOpenChance.vue;库里无同名菜单,用页面功能名)----
[A + 'TtBoxOpenChanceController.java', 'add', { menu: '开箱机会', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtBoxOpenChanceController.java', 'edit', { menu: '开箱机会', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtBoxOpenChanceController.java', 'remove', { menu: '开箱机会', action: '删除', type: 'DELETE' }],
// ================= 其余有按钮证据的页面(标签来自 evidence.js:直接调用方的按钮文案,CRUD 页按工具条 新增/修改/删除)=================
// ---- 公告(ttSetting/announcement.vue)----
[A + 'TtAnnouncementController.java', 'addAnnouncement', { menu: '公告', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtAnnouncementController.java', 'editAnnouncement', { menu: '公告', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtAnnouncementController.java', 'removeAnnouncementByAnnouncementId', { menu: '公告', action: '删除', type: 'DELETE' }],
// ---- Banner设置 / 彩金 / 文章 / 文章类型 / 宝箱分类(ttSetting/banner.vue, ttBonus/index.vue, contentList, contentType, ttBox/boxType)----
[A + 'TtBannerController.java', 'add', { menu: 'Banner设置', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtBannerController.java', 'edit', { menu: 'Banner设置', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtBannerController.java', 'remove', { menu: 'Banner设置', action: '删除', type: 'DELETE' }],
[A + 'TtBonusController.java', 'add', { menu: '彩金', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtBonusController.java', 'edit', { menu: '彩金', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtBonusController.java', 'remove', { menu: '彩金', action: '删除', type: 'DELETE' }],
[A + 'TtContentController.java', 'add', { menu: '文章', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtContentController.java', 'edit', { menu: '文章', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtContentController.java', 'remove', { menu: '文章', action: '删除', type: 'DELETE' }],
[A + 'TtContentTypeController.java', 'add', { menu: '文章类型', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtContentTypeController.java', 'edit', { menu: '文章类型', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtContentTypeController.java', 'remove', { menu: '文章类型', action: '删除', type: 'DELETE' }],
[A + 'TtBoxTypeController.java', 'add', { menu: '宝箱分类', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtBoxTypeController.java', 'edit', { menu: '宝箱分类', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtBoxTypeController.java', 'remove', { menu: '宝箱分类', action: '删除', type: 'DELETE' }],
// ---- 宝箱饰品(挂在宝箱列表下;批量填货按钮见 ttBox/index.vue:283)----
[A + 'TtBoxOrnamentsController.java', 'add', { menu: '宝箱列表', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtBoxOrnamentsController.java', 'edit', { menu: '宝箱列表', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtBoxOrnamentsController.java', 'remove', { menu: '宝箱列表', action: '删除', type: 'DELETE' }],
[A + 'TtBoxOrnamentsController.java', 'batchAdd', { menu: '宝箱列表', action: '批量填货', type: 'INSERT' }],
// ---- 等级类(ttOrnamentsLevel/index.vue, ttSetting/promotionLevel.vue, ttSetting/vips.vue)----
[A + 'TtOrnamentsLevelController.java', 'edit', { menu: '饰品等级', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtOrnamentsLevelController.java', 'generateOrnamentsLevel', { menu: '饰品等级', action: '新增', type: 'INSERT' }],
[A + 'TtPromotionLevelController.java', 'edit', { menu: '推广等级设置', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtPromotionLevelController.java', 'generatePromotionLevel', { menu: '推广等级设置', action: '生成推广列表', type: 'INSERT' }],
[A + 'TtPromotionLevelController.java', 'truncateVipLevel', { menu: '推广等级设置', action: '重置', type: 'CLEAN' }],
[A + 'TtVipLevelController.java', 'edit', { menu: 'VIP等级设置', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtVipLevelController.java', 'generateVipLevel', { menu: 'VIP等级设置', action: '生成VIP等级', type: 'INSERT' }],
[A + 'TtVipLevelController.java', 'truncateVipLevel', { menu: 'VIP等级设置', action: '重置', type: 'CLEAN' }],
// ---- 充值/卡密(ttRechargeList/index.vue, ttRechargeRecord? , ttGenerateCard/index.vue:135)----
[A + 'TtRechargeListController.java', 'add', { menu: '货币列表', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtRechargeListController.java', 'edit', { menu: '货币列表', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtRechargeListController.java', 'remove', { menu: '货币列表', action: '删除', type: 'DELETE' }],
[A + 'TtRechargeConfigController.java', 'add', { menu: '网站货币管理', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtRechargeConfigController.java', 'edit', { menu: '网站货币管理', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtRechargeConfigController.java', 'remove', { menu: '网站货币管理', action: '删除', type: 'DELETE' }],
[A + 'TtRechargeCardController.java', 'generateCard', { menu: '卡密', action: '生成卡密', type: 'INSERT' }],
[A + 'TtRechargeCardController.java', 'generateBlankCard', { menu: '卡密', action: '生成空白卡密', type: 'INSERT' }],
// ---- CDK口令 / Roll奖池 / 限时Roll / 官方周期Roll配置 / 奖项配置 ----
[A + 'TtRedPacketController.java', 'add', { menu: 'CDK口令', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtRedPacketController.java', 'edit', { menu: 'CDK口令', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtRedPacketController.java', 'remove', { menu: 'CDK口令', action: '删除', type: 'DELETE' }],
[A + 'TtRollJackpotController.java', 'add', { menu: '奖池列表', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtRollJackpotController.java', 'edit', { menu: '奖池列表', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtRollJackpotController.java', 'remove', { menu: '奖池列表', action: '删除', type: 'DELETE' }],
[A + 'TtRollJackpotOrnamentsController.java', 'add', { menu: '奖池列表', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtRollJackpotOrnamentsController.java', 'edit', { menu: '奖池列表', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtRollJackpotOrnamentsController.java', 'remove', { menu: '奖池列表', action: '删除', type: 'DELETE' }],
[A + 'TtTimeRollController.java', 'add', { menu: '时间roll房', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtTimeRollController.java', 'edit', { menu: '时间roll房', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtTimeRollController.java', 'remove', { menu: '时间roll房', action: '删除', type: 'DELETE' }],
[A + 'TtOfficialRollConfigController.java', 'add', { menu: '官方周期Roll配置', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtOfficialRollConfigController.java', 'edit', { menu: '官方周期Roll配置', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtOfficialRollConfigController.java', 'remove', { menu: '官方周期Roll配置', action: '删除', type: 'DELETE' }],
[A + 'TurnoverLotteryPrizeController.java', 'add', { menu: '奖项配置', action: '新增', type: 'INSERT', auto: true }],
[A + 'TurnoverLotteryPrizeController.java', 'edit', { menu: '奖项配置', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TurnoverLotteryPrizeController.java', 'remove', { menu: '奖项配置', action: '删除', type: 'DELETE' }],
// ---- 升级饰品 / 失败奖励(ttUpgrade/index.vue, upgradeFall.vue)----
[A + 'TtUpgradeOrnamentsController.java', 'batchAdd', { menu: '饰品列表', action: '新增', type: 'INSERT' }],
[A + 'TtUpgradeOrnamentsController.java', 'edit', { menu: '饰品列表', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtUpgradeOrnamentsController.java', 'remove', { menu: '饰品列表', action: '删除', type: 'DELETE' }],
[A + 'TtUpgradeFailOrnamentsController.java', 'batchAdd', { menu: '失败奖励', action: '新增', type: 'INSERT' }],
[A + 'TtUpgradeFailOrnamentsController.java', 'edit', { menu: '失败奖励', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtUpgradeFailOrnamentsController.java', 'remove', { menu: '失败奖励', action: '删除', type: 'DELETE' }],
// ---- 头像设置 / 网站道具 / 发货记录 / 商城上下架 / 站点参数 ----
[A + 'TtUserAvatarController.java', 'add', { menu: '头像设置', action: '新增', type: 'INSERT', auto: true }],
[A + 'TtUserAvatarController.java', 'edit', { menu: '头像设置', action: '修改', type: 'UPDATE', auto: true }],
[A + 'TtUserAvatarController.java', 'remove', { menu: '头像设置', action: '删除', type: 'DELETE' }],
[A + 'WebsitePropertyController.java', 'add', { menu: '网站道具', action: '新增', type: 'INSERT', auto: true }],
[A + 'WebsitePropertyController.java', 'edit', { menu: '网站道具', action: '修改', type: 'UPDATE', auto: true }],
[A + 'WebsitePropertyController.java', 'remove', { menu: '网站道具', action: '删除', type: 'DELETE' }],
[A + 'TtDeliveryRecordController.java', 'deliveryApprove', { menu: '发货记录', action: '确认发货', type: 'GRANT' }],
[A + 'ShoppingController.java', 'batchPutAwayOrSoldOut', { menu: '商城列表', action: '上架/下架', type: 'UPDATE' }],
[A + 'WebsiteSetupController.java', 'updateParameterSetting', { menu: '网站设置', action: '保存', type: 'UPDATE' }],
// ---- 疯狂转盘 奖励矩阵删除 ----
[W + 'AdminWheelController.java', 'matrixDelete', { menu: '奖励矩阵', action: '删除', type: 'DELETE' }],
];
const IMPORTS = {
TtUser: 'com.ruoyi.domain.entity.sys.TtUser',
TtUserMapper: 'com.ruoyi.admin.mapper.TtUserMapper',
TtLossRebate: 'com.ruoyi.domain.entity.TtLossRebate',
TtLossRebateMapper: 'com.ruoyi.admin.mapper.TtLossRebateMapper',
TtRoll: 'com.ruoyi.domain.entity.roll.TtRoll',
TtRollMapper: 'com.ruoyi.admin.mapper.TtRollMapper',
TtBox: 'com.ruoyi.domain.other.TtBox',
TtBoxMapper: 'com.ruoyi.admin.mapper.TtBoxMapper',
TtOrnament: 'com.ruoyi.domain.entity.TtOrnament',
TtOrnamentMapper: 'com.ruoyi.admin.mapper.TtOrnamentMapper',
};
const isBlockMember = (l) =>
/^\s*(\/\/|\*|\/\*|@)/.test(l) || (/^\s*[\w.]+\s*=/.test(l) && /[,)]\s*$/.test(l));
// ---- 自动解析实体/Mapper:attrs.auto = true 时,按方法入参里的实体类型去认 ----
function collectFiles(dir, out, suffix) {
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()) collectFiles(p, out, suffix);
else if (e.isFile() && e.name.endsWith(suffix)) out.push(p);
}
return out;
}
const ENTITY = new Map(); // simpleName -> fqn(skins-model 全部类;真正用的时候还要求有 Mapper)
for (const f of collectFiles(path.join(ROOT, 'skins-model'), [], '.java')) {
const src = fs.readFileSync(f, 'utf8');
const cls = (src.match(/public\s+class\s+(\w+)/) || [, null])[1];
const pkg = (src.match(/^package\s+([\w.]+);/m) || [, ''])[1];
if (cls && !ENTITY.has(cls)) ENTITY.set(cls, `${pkg}.${cls}`);
}
const MAPPER = new Map(); // entitySimple -> {simple, fqn}(BaseMapper 与普通 MyBatis 接口都要认)
for (const f of collectFiles(path.join(ROOT, 'skins-service'), [], 'Mapper.java')) {
const simple = path.basename(f, '.java');
const entityName = simple.endsWith('Mapper') ? simple.slice(0, -6) : null;
if (!entityName || !ENTITY.has(entityName) || MAPPER.has(entityName)) continue;
const pkg = (fs.readFileSync(f, 'utf8').match(/^package\s+([\w.]+);/m) || [, ''])[1];
MAPPER.set(entityName, { simple, fqn: `${pkg}.${simple}` });
}
/** 取方法参数里的类型名(优先 @RequestBody 那个) */
function methodTypes(lines, sig) {
let text = '';
for (let i = sig; i < Math.min(sig + 10, lines.length); i++) {
text += lines[i] + ' ';
if (/\)\s*\{/.test(lines[i]) || /\)\s*$/.test(lines[i])) break;
}
const inner = (text.match(/\(([\s\S]*?)\)/) || [, ''])[1];
const types = [];
const rb = inner.match(/@RequestBody\s+(?:@\w+(?:\([^)]*\))?\s+)*(\w+)/);
if (rb) types.push(rb[1]);
for (const m of inner.matchAll(/(?:^|,)\s*(?:final\s+)?([A-Z]\w+)\s+\w+/g)) if (!types.includes(m[1])) types.push(m[1]);
return types;
}
function render(a, indent) {
const parts = [`title = "${a.menu}"`, `action = "${a.action}"`, `businessType = BusinessType.${a.type}`];
if (a.entity) parts.push(`entityClass = ${a.entity}.class`);
if (a.mapper) parts.push(`mapperClass = ${a.mapper}.class`);
if (a.idField) parts.push(`idField = "${a.idField}"`);
const one = `${indent}@Log(${parts.join(', ')})`;
if (one.length <= 130) return [one];
return [`${indent}@Log(${parts.slice(0, 3).join(', ')},`, `${indent} ${parts.slice(3).join(', ')})`];
}
function insertImports(lines, entries) {
for (const e of entries) {
// 集合里既有全限定名(com.ruoyi...),也有短名(TtUser),分开处理
const fqn = e.includes('.') ? e : IMPORTS[e];
if (!fqn) continue;
const simple = fqn.split('.').pop();
if (lines.some((l) => new RegExp(`^import\\s+[\\w.]*\\b${simple};`).test(l.trim()))) continue;
let at = lines.findIndex((l) => /^import com\.ruoyi\./.test(l));
if (at < 0) at = lines.findIndex((l) => /^import /.test(l));
if (at >= 0) lines.splice(at, 0, `import ${fqn};`);
}
}
const byFile = new Map();
for (const [file, method, attrs] of TARGETS) {
if (!byFile.has(file)) byFile.set(file, []);
byFile.get(file).push({ method, attrs });
}
if (process.argv.includes('--debug')) {
console.log(`实体索引 ${ENTITY.size},Mapper 索引 ${MAPPER.size}`);
for (const t of ['TtWelfare', 'TtRedPack', 'TtTaskCenter', 'TtBox']) {
console.log(` ${t}: entity=${ENTITY.get(t) || '无'} mapper=${MAPPER.has(t) ? MAPPER.get(t).fqn : '无'}`);
}
}
let done = 0, skipped = 0;
for (const [rel, list] of byFile) {
const abs = path.join(ROOT, rel);
const lines = fs.readFileSync(abs, 'utf8').split(/\r?\n/);
const edits = [];
const imports = new Set(['com.ruoyi.common.annotation.Log', 'com.ruoyi.common.enums.BusinessType']);
for (const { method, attrs } of list) {
// 找方法签名
let sig = -1;
for (let i = 0; i < lines.length; i++) {
if (new RegExp(`^\\s*(?:public|protected|private)\\s+[\\w<>\\[\\],\\.\\? ]+\\s+${method}\\s*\\(`).test(lines[i])) { sig = i; break; }
}
if (sig < 0) { console.log(` [跳过] 找不到方法 ${path.basename(rel)}.${method}`); skipped++; continue; }
// 注解块顶部
let top = sig;
for (let j = sig - 1; j >= 0; j--) {
if (lines[j].trim() === '' || !isBlockMember(lines[j])) break;
top = j;
}
// 已有 @Log 就整块删掉(支持多行)
let start = -1, end = -1;
for (let j = top; j < sig; j++) {
if (/^\s*@Log\(/.test(lines[j])) {
start = j;
let depth = 0;
outer:
for (let k = j; k < sig; k++) {
for (const ch of lines[k]) {
if (ch === '(') depth++;
else if (ch === ')') { depth--; if (depth === 0) { end = k; break outer; } }
}
}
break;
}
}
const indent = (lines[top].match(/^\s*/) || [' '])[0];
// 自动认实体:入参里第一个"有 @TableName 且有对应 Mapper"的类型
if (attrs.auto && !attrs.entity)
{
for (const t of methodTypes(lines, sig))
{
if (ENTITY.has(t) && MAPPER.has(t)) { attrs.entity = t; attrs.mapper = MAPPER.get(t).simple; break; }
}
}
edits.push({ start, end, top, newLines: render(attrs, indent), method, attrs });
if (attrs.entity && ENTITY.has(attrs.entity)) imports.add(ENTITY.get(attrs.entity));
if (attrs.entity && MAPPER.has(attrs.entity)) imports.add(MAPPER.get(attrs.entity).fqn);
if (attrs.mapper && IMPORTS[attrs.mapper]) imports.add(IMPORTS[attrs.mapper]);
}
for (const ed of edits.sort((a, b) => b.top - a.top)) {
if (ed.start >= 0) lines.splice(ed.start, ed.end - ed.start + 1, ...ed.newLines);
else lines.splice(ed.top, 0, ...ed.newLines);
console.log(` ${path.basename(rel)}.${ed.method} → ${ed.attrs.menu} / ${ed.attrs.action}` +
`${ed.attrs.entity ? ' diff=' + ed.attrs.entity : ''}${ed.attrs.idField ? ' idField=' + ed.attrs.idField : ''}`);
done++;
}
insertImports(lines, imports);
if (apply) fs.writeFileSync(abs, lines.join('\r\n'), 'utf8');
}
console.log(`\n${apply ? 'APPLIED' : 'DRY-RUN'}: 写入 ${done} 个方法,跳过 ${skipped}`);
+12
View File
@@ -0,0 +1,12 @@
@echo off
echo.
echo [信息] 清理工程target生成路径。
echo.
%~d0
cd %~dp0
cd ..
call mvn clean
pause
+43
View File
@@ -0,0 +1,43 @@
// 只读查询/受控清库小工具(连 docker 里的 mysql_db)
// 用法:
// node bin/logtool.js q "select count(*) from sys_oper_log"
// node bin/logtool.js clear-operlog 清理操作日志(先备份到 bin/backup/)
// node bin/logtool.js clear-operlog --yes 跳过确认
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
const MYSQL = ['exec', 'mysql_db', 'mysql', '-uchangzhou', '-piTSJSPPZM3LSGAPC', '--default-character-set=utf8mb4', '-D', 'changzhou'];
function sql(statement) {
return execFileSync('docker', [...MYSQL, '-N', '-B', '-e', statement],
{ encoding: 'buffer', maxBuffer: 256 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] }).toString('utf8');
}
const [cmd, ...rest] = process.argv.slice(2);
if (cmd === 'q') {
console.log(sql(rest.join(' ')));
} else if (cmd === 'clear-operlog') {
const before = sql('select count(*) from sys_oper_log').trim();
console.log(`清空前 sys_oper_log: ${before} 行`);
if (before === '0') process.exit(0);
// 备份成 INSERT 语句,清错了还能捞回来
const dir = path.resolve(__dirname, 'backup');
fs.mkdirSync(dir, { recursive: true });
const dump = execFileSync('docker', [...MYSQL, '-N', '-B', '-e',
'select concat("insert into sys_oper_log (title,business_type,method,request_method,operator_type,oper_name,dept_name,oper_url,oper_ip,oper_location,oper_param,json_result,status,error_msg,cost_time,oper_time) values (",' +
'quote(title),",",business_type,",",quote(method),",",quote(request_method),",",operator_type,",",quote(oper_name),",",quote(dept_name),",",quote(oper_url),",",quote(oper_ip),",",quote(oper_location),",",quote(oper_param),",",quote(json_result),",",status,",",quote(error_msg),",",cost_time,",",quote(oper_time),");") from sys_oper_log'],
{ encoding: 'buffer', maxBuffer: 256 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] }).toString('utf8');
const file = path.join(dir, `sys_oper_log_${new Date().toISOString().replace(/[:.]/g, '-')}.sql`);
fs.writeFileSync(file, dump, 'utf8');
console.log(`已备份 → ${file}`);
if (!rest.includes('--yes')) {
console.error('要真清空请加 --yes');
process.exit(2);
}
sql('truncate table sys_oper_log');
console.log(`清空后 sys_oper_log: ${sql('select count(*) from sys_oper_log').trim()} 行`);
} else {
console.log('用法: node bin/logtool.js q "<sql>" | node bin/logtool.js clear-operlog [--yes]');
}
+12
View File
@@ -0,0 +1,12 @@
@echo off
echo.
echo [信息] 打包Web工程,生成war/jar包文件。
echo.
%~d0
cd %~dp0
cd ..
call mvn clean package -Dmaven.test.skip=true
pause
+14
View File
@@ -0,0 +1,14 @@
@echo off
echo.
echo [信息] 使用Jar命令运行Web工程。
echo.
cd %~dp0
cd ../ruoyi-admin/target
set JAVA_OPTS=-Xms256m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m
java -jar %JAVA_OPTS% ruoyi-admin.jar --server.port=8081
cd bin
pause