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*、构建/缓存产物。
This commit is contained in:
195
scripts/diagnose_upload_download.cjs
Normal file
195
scripts/diagnose_upload_download.cjs
Normal file
@@ -0,0 +1,195 @@
|
||||
'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('==================================================');
|
||||
})();
|
||||
Reference in New Issue
Block a user