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:
WorkBuddy
2026-08-17 12:25:42 +08:00
commit a303cf2d53
109 changed files with 25904 additions and 0 deletions

View 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('==================================================');
})();