Files
competition-website/app.js
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

89 lines
3.6 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';
const fs = require('fs');
const path = require('path');
/**
* Egg.js 应用启动钩子 - AppBootHook
* 表结构通过 migration/seeder 管理,不在启动时自动同步
*/
class AppBootHook {
constructor(app) {
this.app = app;
}
async willReady() {
// 数据库表结构由 migration 管理,不在启动时自动 sync
this.app.logger.info('[AppBoot] 服务启动完成');
}
/**
* 进程就绪后,确保 app/public 下所有业务目录存在。
*
* 关键背景(生产踩坑):
* app/public/{works,answers,materials,tmp,...} 这类上传目录通常被 .gitignore 忽略,
* 每次「整目录重新部署」(rsync --delete / 重新 clone) 都会被清空。
* 若运行中的代码某条写入路径恰好没有即时 mkdir,就会 ENOENT → 上传失败 / 下载 404,
* 且旧版 auth 中间件会把这类磁盘异常伪装成「401 登录过期」误导排查。
* 此处统一在启动时把全部已知写入目录递归建好,作为永久兜底,
* 与各处写入点的 fs.mkdirSync(recursive:true) 形成双保险。
*/
async didReady() {
const publicDir = path.join(this.app.baseDir, 'app/public');
const groups = ['zhongzhi', 'gaozhi', 'benke', 'jiaoshi'];
// 需要按「目录/组别」创建的(组别子目录)
const grouped = ['works', 'answers', 'materials', 'templates'];
// 需要直接创建(无组别)的
const flat = ['tmp', 'tmp/excel', 'tmp/zip', 'carousel', 'news'];
const targets = [];
for (const d of grouped) {
for (const g of groups) targets.push(path.join(publicDir, d, g));
}
for (const d of flat) targets.push(path.join(publicDir, d));
let created = 0;
let existed = 0;
for (const t of targets) {
try {
if (!fs.existsSync(t)) {
fs.mkdirSync(t, { recursive: true });
created++;
} else {
existed++;
}
} catch (e) {
this.app.logger.error('[AppBoot] 创建上传目录失败', { target: t, message: e.message });
}
}
this.app.logger.info(
`[AppBoot] 上传目录就绪:已创建 ${created} 个,已存在 ${existed} 个(根: ${publicDir})`
);
// 🔴 连接池容量自举断言(2026-08-13 推进 #16):egg-cluster 按 os.cpus() 起多 worker,
// 总连接数 ≈ worker 数 × pool.max。若超过 MySQL max_connections(默认 151),
// 高峰期会偶发 "connection acquire timeout" / "too many connections"。
// 启动期只做 WARN(不阻断启动),提醒运维调大 DB 或调小 DB_POOL_MAX。
try {
const os = require('os');
const poolMax = parseInt(process.env.DB_POOL_MAX, 10) || 20;
const rows = await this.app.model.query('SELECT @@max_connections AS m', { type: this.app.model.QueryTypes.SELECT });
const dbMax = parseInt(rows && rows[0] && rows[0].m, 10) || 151;
const workers = os.cpus().length;
const estimated = workers * poolMax;
if (estimated > dbMax) {
this.app.logger.warn(
`[AppBoot][pool] 预估总连接数 ${estimated} (${workers} workers × ${poolMax}) 超过 MySQL max_connections=${dbMax},高峰期可能连接获取超时;建议调大 DB max_connections 或调小 DB_POOL_MAX`
);
} else {
this.app.logger.info(`[AppBoot][pool] 连接池容量 OK:${estimated} (${workers} × ${poolMax}) ≤ MySQL max_connections=${dbMax}`);
}
} catch (e) {
this.app.logger.warn('[AppBoot][pool] 连接数自举断言跳过(无法查询 MySQL):' + (e && e.message));
}
}
}
module.exports = AppBootHook;