#!/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', }, { name: '云对象方法校验', script: 'tools/check-cloud-methods.js', desc: 'importObject 调用点的方法名是否真实存在', }, { name: '安卓端契约校验', script: 'tools/check-android-contract.js', desc: 'Kotlin 侧调用的云对象方法是否存在', }, ]; const SELFTESTS = [ { name: '静态审计自测', script: 'tools/audit-project.js', args: ['--selftest'] }, { name: 'Vue 校验自测', script: 'tools/check-vue.js', args: ['--selftest'] }, { name: '文章内容适配自测', script: 'tools/check-article-flow.js' }, { name: '云对象方法校验自测', script: 'tools/check-cloud-methods.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);