## 阻断性缺陷 - list.vue 是 0 字节空文件、slist.vue 与 search/search.vue 从未存在, 而前者是 tabBar 首页、后者是 tabBar「搜索」页 —— 开屏即白屏。 按 .nvue 原型与详情页契约重建三页(CSS 渐变主视觉、分类筛选、搜索历史/热搜/联想)。 - parse-image-url.js 对空封面调 undefined.startsWith 直接抛错,列表页整页崩。 - 云函数目录缺 uni-cms-articles / uni-cms-categories / uni-cms-unlock-record schema 与 schema.ext.js,线上内容渲染与解锁逻辑无配置可用。 ## 越权与数据一致性 - uni-cms-articles:del/update/add 全部无鉴权,未登录即可删任意文章、 改他人文章作者与阅读量。补 login + 作者归属校验,作者与计数改为服务端取值。 - comments.likeComment/relikeComment:直接采信客户端传入的 user_id, 可冒名点赞刷计数。改为以令牌为准,并纳入事务。 - comments.updateComment:对数组取 .author_id,权限判断恒失败; 字段名 updateTime 与 ip_location 类型与 schema 不符。 - comments.deleteComment:`!root_id === 0` 优先级错误导致计数恒不减; 且误更新 uni-cms-articles、按不存在的 type 字段删点赞明细产生孤儿数据。 - cms-articles-like/collect:查重条件混入本次请求时间戳,防重永远失效, 可无限重复刷计数;补唯一索引并事务化。 - cms-vote:读-改-写票数导致并发丢票,记录与统计非原子;改为事务 + 原子自增。 - cms-articles-log:忽略传入 user_id 直接返回全表,泄露全站浏览记录。 - article_info:get() 使用未定义变量必崩;读接口全部无鉴权。 - user-info:公开资料接口可查任意用户 last_login_ip。 ## 资源与数据 - 全项目清空失效的签名外链(expire_at 均为 2025-03,必然 403), 改为本地生成资源:6 套文章模板、8 个编辑器图标、2 张文章配图。 - 新增分类 / 模板 / 礼物 / 热搜词种子数据,并在 db_init.json 登记, 同时补上点赞、收藏、投票、浏览日志的唯一索引。 ## 功能 - 草稿箱:预览页拆出「发布」与「存为草稿」,作品列表按状态筛选并显示徽标。 原实现有 4 个 tab 但只有 1 个有内容,且 article_status 在 UI 上无体现。 - 编辑中断恢复:接上原本空实现的「编辑草稿」回调,区分新建与编辑已有文章。 ## 工具 - tools/audit-project.js:编码 / 页面路由 / 云调用 / 云函数鉴权 / 敏感信息检查 - tools/check-vue.js:SFC 脚本语法(词法扫描处理 import·export 与条件编译) - tools/verify.js:一键验证;两个检查器各带自测,防止"永远通过" - tools/gen-*.py:模板与图标资源生成脚本
234 lines
7.5 KiB
JavaScript
234 lines
7.5 KiB
JavaScript
#!/usr/bin/env node
|
||
'use strict';
|
||
|
||
/**
|
||
* 一键验证:跑完所有检查器并汇总。
|
||
*
|
||
* node tools/verify.js # 全部检查
|
||
* node tools/verify.js --quick # 跳过耗时的编码扫描
|
||
*
|
||
* 退出码 0 表示全部通过,可用于 CI 或提交前自检。
|
||
*/
|
||
|
||
const { spawnSync } = require('child_process');
|
||
const path = require('path');
|
||
|
||
const ROOT = path.resolve(__dirname, '..');
|
||
const QUICK = process.argv.includes('--quick');
|
||
|
||
const CHECKS = [
|
||
{
|
||
name: '静态审计',
|
||
script: 'tools/audit-project.js',
|
||
desc: '编码 / 页面路由 / 云调用 / 云函数鉴权 / 敏感信息',
|
||
},
|
||
{
|
||
name: 'Vue SFC 校验',
|
||
script: 'tools/check-vue.js',
|
||
desc: '脚本语法 / 生命周期位置 / Vue2 残留 API',
|
||
},
|
||
];
|
||
|
||
const SELFTESTS = [
|
||
{ name: '静态审计自测', script: 'tools/audit-project.js', args: ['--selftest'] },
|
||
{ name: 'Vue 校验自测', script: 'tools/check-vue.js', args: ['--selftest'] },
|
||
];
|
||
|
||
function run(script, args = []) {
|
||
const r = spawnSync(process.execPath, [path.join(ROOT, script), ...args], {
|
||
encoding: 'utf8',
|
||
cwd: ROOT,
|
||
});
|
||
return { code: r.status, out: (r.stdout || '') + (r.stderr || '') };
|
||
}
|
||
|
||
function summarize(out) {
|
||
// 从报告末尾抓 "结论: N 错误 / M 警告"
|
||
const m = /结论:\s*(\d+)\s*错误\s*\/\s*(\d+)\s*警告/.exec(out);
|
||
if (m) return { errors: Number(m[1]), warnings: Number(m[2]) };
|
||
// 自测输出
|
||
const f = /✗\s*(\d+)\s*项失败/.exec(out);
|
||
if (f) return { errors: Number(f[1]), warnings: 0 };
|
||
return null;
|
||
}
|
||
|
||
let failed = 0;
|
||
|
||
console.log('');
|
||
console.log('═══════════════════════════════════════════════');
|
||
console.log(' 军歌嘹亮 · 一键验证');
|
||
console.log('═══════════════════════════════════════════════');
|
||
|
||
// 第一阶段:先确认检查器本身可信
|
||
console.log('');
|
||
console.log('【检查器自测】');
|
||
for (const t of SELFTESTS) {
|
||
const { code, out } = run(t.script, t.args);
|
||
const ok = code === 0;
|
||
if (!ok) failed++;
|
||
console.log(` ${ok ? '✓' : '✗'} ${t.name}`);
|
||
if (!ok) {
|
||
console.log(
|
||
out
|
||
.split('\n')
|
||
.filter((l) => l.includes('✗'))
|
||
.map((l) => ' ' + l.trim())
|
||
.join('\n')
|
||
);
|
||
}
|
||
}
|
||
|
||
// 第二阶段:跑实际检查
|
||
console.log('');
|
||
console.log('【项目检查】');
|
||
const results = [];
|
||
for (const c of CHECKS) {
|
||
if (QUICK && c.name.includes('静态')) continue;
|
||
const { code, out } = run(c.script);
|
||
const s = summarize(out);
|
||
const ok = code === 0;
|
||
if (!ok) failed++;
|
||
results.push({ ...c, ok, summary: s });
|
||
const stat = s ? `${s.errors} 错误 / ${s.warnings} 警告` : code === 0 ? '通过' : '失败';
|
||
console.log(` ${ok ? '✓' : '✗'} ${c.name.padEnd(14)} ${stat}`);
|
||
}
|
||
|
||
// 第三阶段:数据与资源完整性
|
||
console.log('');
|
||
console.log('【数据与资源】');
|
||
const fs = require('fs');
|
||
|
||
const dataChecks = [
|
||
['分类种子数据', 'uniCloud-alipay/database/uni-cms-categories.init_data.json'],
|
||
['文章模板数据', 'uniCloud-alipay/database/cms-temp.init_data.json'],
|
||
['礼物种子数据', 'uniCloud-alipay/database/gifts.init_data.json'],
|
||
['热搜词数据', 'uniCloud-alipay/database/opendb-search-hot.init_data.json'],
|
||
['模板图片资源', 'static/template/default.png'],
|
||
['编辑器图标', 'static/editor-icons/text.png'],
|
||
['文章配图', 'static/article/wz1.png'],
|
||
];
|
||
|
||
for (const [label, rel] of dataChecks) {
|
||
const abs = path.join(ROOT, rel);
|
||
const exists = fs.existsSync(abs);
|
||
let detail = '';
|
||
if (exists) {
|
||
try {
|
||
const content = fs.readFileSync(abs, 'utf8');
|
||
if (rel.endsWith('.json')) {
|
||
const arr = JSON.parse(content);
|
||
detail = Array.isArray(arr) ? `${arr.length} 条` : '对象';
|
||
} else {
|
||
detail = `${(fs.statSync(abs).size / 1024).toFixed(1)} KB`;
|
||
}
|
||
} catch (e) {
|
||
detail = `解析失败: ${e.message}`;
|
||
}
|
||
}
|
||
if (!exists) failed++;
|
||
console.log(` ${exists ? '✓' : '✗'} ${label.padEnd(14)} ${detail}`);
|
||
}
|
||
|
||
// schema 部署完整性:主表 schema 缺失会导致线上集合无 schema 约束
|
||
const schemaChecks = [
|
||
['文章表', 'uniCloud-alipay/database/uni-cms-articles.schema.json'],
|
||
['分类表', 'uniCloud-alipay/database/uni-cms-categories.schema.json'],
|
||
['解锁记录表', 'uniCloud-alipay/database/uni-cms-unlock-record.schema.json'],
|
||
['文章内容扩展', 'uniCloud-alipay/database/uni-cms-articles.schema.ext.js'],
|
||
];
|
||
|
||
for (const [label, rel] of schemaChecks) {
|
||
const abs = path.join(ROOT, rel);
|
||
const exists = fs.existsSync(abs);
|
||
if (!exists) failed++;
|
||
console.log(` ${exists ? '✓' : '✗'} ${label.padEnd(14)} ${exists ? '已部署' : '缺失'}`);
|
||
}
|
||
|
||
// schema 与种子数据是 JSONC(uniCloud 允许行注释),需要用宽松方式解析
|
||
function parseJSONC(text) {
|
||
let out = '';
|
||
let i = 0;
|
||
const n = text.length;
|
||
while (i < n) {
|
||
const c = text[i];
|
||
// 字符串原样保留
|
||
if (c === '"') {
|
||
let j = i + 1;
|
||
while (j < n) {
|
||
if (text[j] === '\\') { j += 2; continue; }
|
||
if (text[j] === '"') break;
|
||
j++;
|
||
}
|
||
out += text.slice(i, Math.min(j + 1, n));
|
||
i = j + 1;
|
||
continue;
|
||
}
|
||
// 行注释:换成等量空白以保持结构
|
||
if (c === '/' && text[i + 1] === '/') {
|
||
const end = text.indexOf('\n', i);
|
||
const stop = end === -1 ? n : end;
|
||
out += ' '.repeat(stop - i);
|
||
i = stop;
|
||
continue;
|
||
}
|
||
out += c;
|
||
i++;
|
||
}
|
||
return JSON.parse(out);
|
||
}
|
||
|
||
// 所有 schema 与 db_init 都必须能被解析
|
||
const schemaFiles = fs
|
||
.readdirSync(path.join(ROOT, 'uniCloud-alipay/database'))
|
||
.filter((f) => f.endsWith('.schema.json') || f === 'db_init.json');
|
||
|
||
let badSchema = 0;
|
||
for (const f of schemaFiles) {
|
||
const abs = path.join(ROOT, 'uniCloud-alipay/database', f);
|
||
try {
|
||
const obj = parseJSONC(fs.readFileSync(abs, 'utf8'));
|
||
if (!obj || typeof obj !== 'object') throw new Error('根节点不是对象');
|
||
} catch (e) {
|
||
badSchema++;
|
||
failed++;
|
||
console.log(` ✗ ${f.padEnd(14)} ${e.message}`);
|
||
}
|
||
}
|
||
if (!badSchema) {
|
||
console.log(` ✓ ${'schema 解析'.padEnd(14)} ${schemaFiles.length} 个文件全部有效`);
|
||
}
|
||
|
||
// db_init 必须登记所有种子数据表,否则线上初始化会漏表
|
||
const initPath = path.join(ROOT, 'uniCloud-alipay/database/db_init.json');
|
||
try {
|
||
const init = parseJSONC(fs.readFileSync(initPath, "utf8"));
|
||
const registered = Object.keys(init);
|
||
const expected = [
|
||
'uni-cms-categories',
|
||
'cms-temp',
|
||
'gifts',
|
||
'opendb-search-hot',
|
||
'cms-articles-like',
|
||
'cms-articles-collect',
|
||
'cms-vote',
|
||
'cms-articles-log',
|
||
];
|
||
const missing = expected.filter((t) => !registered.includes(t));
|
||
if (missing.length) failed++;
|
||
console.log(
|
||
` ${missing.length ? '✗' : '✓'} ${'db_init 登记'.padEnd(14)} ${
|
||
missing.length ? '缺少: ' + missing.join(', ') : `${registered.length} 张表`
|
||
}`
|
||
);
|
||
} catch (e) {
|
||
failed++;
|
||
console.log(` ✗ db_init 登记 解析失败: ${e.message}`);
|
||
}
|
||
|
||
console.log('');
|
||
console.log('═══════════════════════════════════════════════');
|
||
console.log(failed ? `✗ ${failed} 项未通过` : '✓ 全部通过');
|
||
console.log('═══════════════════════════════════════════════');
|
||
|
||
process.exit(failed ? 1 : 0);
|