Files
competition-website/scripts/diagnose_upload_download.cjs
WorkBuddy a303cf2d53 chore: 后端首次基线提交
运行必须源码:app/(controller/model/service/middleware/lib/utils/validate/router)、config/、database/(migrations/seeders/sql)、启动脚本、app/public/templates 模板。
已忽略运行非必须:node_modules/、logs/、run/、app/public 运行时上传数据、.env*、构建/缓存产物。
2026-08-17 12:25:42 +08:00

196 lines
7.7 KiB
JavaScript
Raw 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.
'use strict';
/**
* 生产环境 上传/下载 链路诊断脚本
* ---------------------------------------------------------------------------
* 用途:在生产后端目录下运行,回答三个问题:
* 1) app/public/{works,answers,materials,tmp} 目录是否存在、是否可写?
* 2) 代码里「上传存盘路径」与「下载读取路径」是否真的指向同一文件?(路径自洽性)
* 3) 数据库里已有的 submission 记录,其 file_path 是否对应磁盘上真实存在的文件?
*
* 运行:
* cd <生产 backend 目录>
* node scripts/diagnose_upload_download.cjs
* # 如需抽查数据库(可选):
* DB_HOST=127.0.0.1 DB_USER=root DB_PASS=74191221 DB_NAME=competition_db \
* node scripts/diagnose_upload_download.cjs
*
* 不依赖 Egg 启动,纯 Node + fs 即可;DB 部分为可选(拿不到模块/连接失败则跳过)。
*/
const fs = require('fs');
const path = require('path');
// ---------- 1. 定位 backend 根目录(app/public 的上一级) ----------
function resolveBackendRoot() {
// 脚本放在 backend/scripts/ 下时,根目录是 scripts 的上一级
let here = __dirname;
for (let i = 0; i < 4; i++) {
if (fs.existsSync(path.join(here, 'app', 'public'))) return here;
const parent = path.dirname(here);
if (parent === here) break;
here = parent;
}
// 兜底:以当前工作目录为根
return process.cwd();
}
const root = resolveBackendRoot();
const publicDir = path.join(root, 'app', 'public');
console.log('==================================================');
console.log(' 上传/下载 链路诊断');
console.log('==================================================');
console.log('backend 根目录 :', root);
console.log('app/public :', publicDir);
console.log('app/public 存在:', fs.existsSync(publicDir));
console.log('');
// ---------- 2. 检查各业务目录的存在性与可写性 ----------
const subDirs = ['works', 'answers', 'materials', 'tmp'];
const groups = ['zhongzhi', 'gaozhi', 'benke', 'jiaoshi'];
function checkDir(p) {
const exists = fs.existsSync(p);
let type = exists ? (fs.statSync(p).isDirectory() ? '目录' : '非目录!') : '不存在';
let writable = false;
if (exists) {
try {
const probe = path.join(p, '.write_probe_' + Date.now());
fs.writeFileSync(probe, 'x');
fs.unlinkSync(probe);
writable = true;
} catch (e) {
writable = false;
}
}
return { exists, type, writable };
}
console.log('--- 2.1 业务目录存在性 / 可写性 ---');
const dirReport = {};
for (const d of subDirs) {
const full = path.join(publicDir, d);
const r = checkDir(full);
dirReport[d] = r;
console.log(
` app/public/${d.padEnd(9)} : ${r.type.padEnd(8)} 可写=${r.writable}`
);
if (d === 'works' || d === 'materials') {
for (const g of groups) {
const gp = path.join(full, g);
const gr = checkDir(gp);
console.log(
` └─ ${g.padEnd(9)} : ${gr.type.padEnd(8)} 可写=${gr.writable}`
);
}
}
}
console.log('');
// ---------- 3. 路径自洽性模拟(与源码逻辑一致) ----------
console.log('--- 3. 路径自洽性(模拟一次上传→下载应指向同一文件)---');
function simulate(group, safeName) {
// 上传存盘:service.upload.saveWorkFile / admin.uploadMaterial / exam.uploadWord
const savedAbs = path.join(publicDir, 'works', group, safeName); // 例:works/benke/...
// 下载读取:admin.downloadSubmissionFile -> path.join(app.baseDir,'app/public',submission.file_path)
const dbRelative = path.join('works', group, safeName).replace(/\\/g, '/');
const readAbs = path.join(publicDir, dbRelative);
const ok = path.resolve(savedAbs) === path.resolve(readAbs);
console.log(` ${group}/${safeName}`);
console.log(` 上传落盘路径 : ${savedAbs}`);
console.log(` 下载读取路径 : ${readAbs}`);
console.log(` 自洽一致 : ${ok ? '✓ YES' : '✗ NO —— 代码有路径错配!'}`);
return ok;
}
const selfConsistent =
simulate('benke', 'work_1700000000000_ab12cd.docx') &&
simulate('zhongzhi', 'exam_1_1700000000000.docx') &&
simulate('zhongzhi', '报告.pdf');
console.log('');
// ---------- 4. 抽查数据库里 submission 的 file_path 是否真实存在 ----------
console.log('--- 4. 数据库 submission.file_path 真实性抽查(可选)---');
const dbCfg = {
host: process.env.DB_HOST || '127.0.0.1',
user: process.env.DB_USER || 'root',
password: process.env.DB_PASS || '74191221',
database: process.env.DB_NAME || 'competition_db',
};
(async () => {
let mysqlOk = false;
try {
const mysql = require(path.join(root, 'node_modules', 'mysql2', 'promise'));
const conn = await mysql.createConnection({
host: dbCfg.host,
user: dbCfg.user,
password: dbCfg.password,
database: dbCfg.database,
connectTimeout: 4000,
});
mysqlOk = true;
console.log(` 已连接数据库 ${dbCfg.database} @ ${dbCfg.host}`);
const tables = [
{ name: 'benke_submissions', label: '本科', col: 'file_path' },
{ name: 'jiaoshi_submissions', label: '教师', col: 'file_path' },
{ name: 'exam_submissions', label: '中职/高职(word)', col: 'word_file_path' },
];
for (const t of tables) {
try {
const [rows] = await conn.query(
`SELECT id, ${t.col} FROM ${t.name} WHERE ${t.col} IS NOT NULL AND ${t.col} <> '' LIMIT 10`
);
console.log(` [${t.label}] ${t.name}.${t.col} 抽样 ${rows.length} 条:`);
if (rows.length === 0) {
console.log(' (无带路径的记录,或该表无文件)');
}
for (const r of rows) {
const fp = r[t.col];
const abs = path.join(publicDir, fp);
const exists = fs.existsSync(abs);
console.log(
` id=${r.id} file_path="${fp}" -> 磁盘${exists ? '✓ 存在' : '✗ 缺失!'}`
);
if (!exists) console.log(` 解析绝对路径: ${abs}`);
}
} catch (e) {
console.log(` [${t.label}] 查询 ${t.name} 失败: ${e.message}`);
}
}
await conn.end();
} catch (e) {
console.log(` 数据库检查跳过(未连接: ${e.message})`);
console.log(' 如需抽查,请在环境变量中提供 DB_HOST/DB_USER/DB_PASS/DB_NAME 后重跑。');
}
console.log('');
// ---------- 5. 结论 ----------
console.log('==================================================');
console.log(' 诊断结论');
console.log('==================================================');
const dirsMissing = subDirs.filter(d => !dirReport[d].exists);
const dirsNotWritable = subDirs.filter(d => dirReport[d].exists && !dirReport[d].writable);
if (dirsMissing.length) {
console.log(`✗ 关键目录缺失: app/public/${dirsMissing.join(', ')}`);
console.log(' → 上传文件无处落盘,下载必然 404。请在服务器后端目录执行:');
for (const d of dirsMissing) {
console.log(` mkdir -p app/public/${d}/{zhongzhi,gaozhi,benke,jiaoshi}`);
}
} else {
console.log('✓ 业务目录均存在');
}
if (dirsNotWritable.length) {
console.log(`✗ 目录不可写: app/public/${dirsNotWritable.join(', ')}`);
console.log(' → 检查进程用户对该目录的写权限(chown/chmod)。');
} else {
console.log('✓ 业务目录均可写');
}
console.log(selfConsistent ? '✓ 代码路径自洽(上传/下载指向同一文件)' : '✗ 代码路径不自洽,需修代码');
if (mysqlOk) {
console.log(' → 查看上方「磁盘缺失」标记:若有,说明文件没落盘(上传未成功或目录曾缺失)。');
}
console.log('==================================================');
})();