Files
zszq-trs/YLErpWeb/fe-tests/guard_arch.js
hjhan b3c2fdfe38 test(fe): 补充守卫注释与架构闸门,防新增内联金额计算
- 生产接线处补注释,追溯到具体历史 bug (dcf649f2/20ea93d8/3c5f25a5/f873239a)

- swapCalc.js 注明哪些函数已接入生产、哪些是纯交叉校验规格

- 新增 guard_arch.js 架构闸门:扫描提交新增行,拦截 Vue 组件内联金额计算

- pre-commit hook 串行运行 单元测试 + 架构闸门
2026-07-07 13:26:32 +08:00

123 lines
4.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* guard_arch.js — 前端架构闸门(零依赖,纯 Node)
* ============================================================================
* 目的:防止在 Vue 组件方法里【新增】手写「金额 / 精度」计算(toFixed / _.round /
* Math.round / × multiplier 缩放等),避免又长出一份「不可测的内联公式」。
*
* 规则:仅扫描【本次提交改动中新增/修改的行】(git diff 的 + 行),且位于
* wwwroot/Scripts/app/ 并定义 new Vue(...) 的文件。对其中每一行「金额算术」,
* 若该行没有路由到某个 *Calc 模块(含 SwapCalc.),则判为违规 → 退出码 1。
*
* 设计要点:
* - 只查「新增/修改的行」,不查历史存量。这样不会阻断对存量文件(如仍含内联计算的
* unwindSwapTrade.js)的正常改动,只拦「新写的内联金额公式」。
* - 已外置计算逻辑的组件(incomeSwapTrade.js / swapTradeEdit.js)通过调用 SwapCalc.*
* 保持合规;新代码必须把金额计算放进 *Calc 模块(参考 swapCalc.js)。
* - 注释行会被剥离后再判断,避免注释里的样例文字误报。
* - 无需 jest / npm install,和 _shim_run.js 同属零依赖守卫。
*
* 用法:node guard_arch.js (一般在 pre-commit / CI 中自动调用)
* ============================================================================
*/
'use strict';
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
// fe-tests -> YLErpWeb -> zszq-trs(仓库根)
const ROOT = path.resolve(__dirname, '..', '..');
// 金额 / 精度算术模式(命中即怀疑)
const MONEY_PATTERNS = [
/\.toFixed\s*\(/, // 显示/入库精度
/_\.round\s*\(/, // lodash 四舍五入
/Math\.round\s*\(/, // 原生四舍五入
/\*\s*this\.multiplier/, // 价格 × 乘数缩放(dcf649f2 / 20ea93d8 类)
/\*\s*thisObj\.multiplier/,
];
// 合规标记:行内引用了某个 *Calc 模块(如 SwapCalc.),视为已外置
const COMPLIANT = /Calc\./;
function tryCmd(cmd) {
try {
return execSync(cmd, { cwd: ROOT }).toString().trim();
} catch (e) {
return '';
}
}
function changedAppVueFiles() {
const listCmd = 'git diff --cached --name-only --diff-filter=ACM -- "*.js"';
let out = tryCmd(listCmd);
if (!out) out = tryCmd('git diff --name-only --diff-filter=ACM -- "*.js"');
if (!out) return [];
return out.split('\n').filter((f) => {
if (!/wwwroot[\\/]Scripts[\\/]app[\\/]/.test(f)) return false;
const full = path.join(ROOT, f);
if (!fs.existsSync(full)) return false;
try {
return /new\s+Vue\s*\(/.test(fs.readFileSync(full, 'utf8'));
} catch (e) {
return false;
}
});
}
// 取文件在本次提交中【新增/修改】的行(git diff 的 + 行,排除 +++ 文件头)
function addedLines(file) {
const quoted = JSON.stringify(file);
let out = tryCmd(`git diff --cached -U0 -- ${quoted}`);
if (!out) out = tryCmd(`git diff -U0 -- ${quoted}`);
if (!out) return [];
return out.split('\n')
.filter((l) => l.startsWith('+') && !l.startsWith('+++'))
.map((l) => l.slice(1));
}
function stripComment(line) {
// 去掉 /* */ 块注释与 // 行内注释,避免注释里的样例文字误报
return line.replace(/\/\*.*?\*\//g, '').replace(/\/\/.*$/, '').trim();
}
function main() {
const files = changedAppVueFiles();
if (files.length) {
console.log(`[guard_arch] 扫描 ${files.length} 个改动的 Vue 组件文件的新增/修改行`);
}
let violations = 0;
for (const file of files) {
const added = addedLines(file);
const found = [];
added.forEach((rawLine) => {
const code = stripComment(rawLine);
if (!code) return;
const hasMoney = MONEY_PATTERNS.some((p) => p.test(code));
if (hasMoney && !COMPLIANT.test(code)) {
found.push(` + ${rawLine.trim()}`);
}
});
if (found.length) {
violations += found.length;
console.log(`[guard_arch] 违规 ${file} 发现【新增】内联金额计算(应路由到 *Calc 模块):`);
found.forEach((f) => console.log(f));
}
}
if (violations > 0) {
console.log(
`\n[guard_arch] 发现 ${violations} 处【新增】内联金额计算, 提交被阻断。` +
`请把金额/精度计算抽到 *Calc 模块(参考 swapCalc.js)。`
);
process.exit(1);
}
console.log('[guard_arch] OK: 改动的 Vue 组件未新增内联金额计算。');
process.exit(0);
}
main();