diff --git a/.gitignore b/.gitignore index b0dba07..8bf35c1 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,10 @@ run/ .env* !.env.example +# 本地开发配置(Egg 仅在 serverEnv=local 时加载,生产不加载) +# 含本地 DB 口令,故排除入库;如需团队共享请提交不含口令的 config.local.example.js +config/config.local.js + # Coverage / cache coverage/ .cache/ diff --git a/config/config.default.js b/config/config.default.js index ecc1cb5..2af3a2d 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -10,14 +10,46 @@ // 向上查找,并支持 DEPLOY_ENV_FILE 显式指定),填充 process.env;仅当变量尚未设置时 // 生效(外部注入优先),保证幂等。 // 注意:APP_ENV / NODE_ENV 不从此处加载,避免本地开发误判为生产(真实生产由启动器注入)。 +// +// 🔴 2026-09-23 修复(本地启动崩溃 1045,详见 memory/2026-09-23.md): +// 原先本函数在【模块加载期】立即执行,且无条件把 .env.production 的全部键灌进 +// process.env —— 于是本地 `npm run dev` 也会拿到**生产库账号**(见 deploy/.env.production +// 的 DB_USER), +// 而本机 MySQL 并无该账号 ⇒ SequelizeAccessDeniedError(1045) ⇒ agent_worker 退出 +// ⇒ master 退出 ⇒ 7001 无监听。现改为两处收口: +// ① 【执行时机】推迟到 module.exports(appInfo) 内部 —— 此时 appInfo.env 已确定; +// ② 【作用范围】非生产环境跳过「连接类 / 凭据类 / 网关开关类」键(见下方常量)。 +// 生产(appInfo.env === 'prod' 或 NODE_ENV=production)**不跳过任何键**,行为逐键不变。 +// 生产必然满足判定的三重依据(2026-09-23 取证): +// a) egg-scripts/lib/cmd/start.js:109 无条件 `env.NODE_ENV = 'production'`; +// b) deploy/.env.production:17 `NODE_ENV=production`,且 start-backend.sh 以 +// `set -a; source "$ENV_FILE"` 注入 shell 后再 exec node; +// c) start.js:126 注释:argv -> EGG_SERVER_ENV -> 兜底 egg 取 `prod`。 // --------------------------------------------------------------------------- -(function loadDeployEnv() { +const DEPLOY_ENV_SKIP_KEYS = new Set(['APP_ENV', 'NODE_ENV']); + +// 非生产环境额外跳过的键前缀:这些键一旦注入本机进程会造成**真实副作用**, +// 或把生产凭据带进本地进程。逐类取证依据(2026-09-23): +// DB_ —— 实证危害:注入生产库账号后本地必然 1045 崩溃(2026-09-23 已实测); +// REDIS_ —— 当前全库无 app.redis 使用点(grep 零命中),但一旦启用即连生产 Redis; +// ALIYUN_ —— 短信网关密钥;本地 realSend 恒 false(auth.js:54 `isProduction && realSend`), +// 注入与否不改变行为,跳过仅为避免生产密钥落入本地进程(泄露面); +// SMS_ —— SMS_ENABLED 全库零引用(config.sms.enabled 无使用点),跳过仅为口径一致。 +// 仍保留注入的键:JWT_SECRET / APP_KEYS / EGG_WORKERS / STATIC_BASE_URL 等 —— 本地无副作用。 +const DEPLOY_ENV_LOCAL_SKIP_PREFIXES = ['DB_', 'REDIS_', 'ALIYUN_', 'SMS_']; + +function loadDeployEnv(appInfo) { try { const fs = require('fs'); const path = require('path'); - const SKIP_KEYS = new Set(['APP_ENV', 'NODE_ENV']); const targets = []; + // 生产判定:appInfo.env 为权威(Egg getServerEnv 的结果),NODE_ENV 为兼容兜底。 + // 两者皆不满足即视为非生产(本地/测试),采用**保守**方向跳过连接类键。 + const isProdEnv = + process.env.NODE_ENV === 'production' || + (appInfo && (appInfo.env === 'prod' || appInfo.env === 'production')); + // 1) 显式指定(最高优先级) if (process.env.DEPLOY_ENV_FILE) targets.push(process.env.DEPLOY_ENV_FILE); @@ -39,6 +71,7 @@ } let loadedFrom = null; + let skipped = []; for (const file of targets) { let text; try { @@ -55,9 +88,13 @@ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { val = val.slice(1, -1); } - if (key && !SKIP_KEYS.has(key) && process.env[key] === undefined) { - process.env[key] = val; + if (!key || DEPLOY_ENV_SKIP_KEYS.has(key) || process.env[key] !== undefined) continue; + // 非生产环境:跳过连接类 / 凭据类 / 网关开关类键,避免「本地进程连到生产库」 + if (!isProdEnv && DEPLOY_ENV_LOCAL_SKIP_PREFIXES.some((p) => key.startsWith(p))) { + skipped.push(key); + continue; } + process.env[key] = val; } loadedFrom = file; break; // 第一个存在的文件即为权威来源 @@ -68,14 +105,30 @@ } else { console.warn('[deploy-env] 未找到 .env.production,将完全依赖进程环境变量注入(缺失生产密钥会触发安全守卫)'); } + // 非生产环境把「被跳过的键」显式打印出来。⚠️ 只打印键名,绝不打印值。 + // 注意:本行出现 ≠ 那些键被注入(恰恰相反,是被跳过了),排查时勿误读。 + if (skipped.length > 0) { + console.log( + '[deploy-env] 非生产环境(env=' + + ((appInfo && appInfo.env) || process.env.NODE_ENV || 'unknown') + + '),已跳过 ' + + skipped.length + + ' 个连接类/凭据类键(防止本地进程连到生产库或持有生产密钥):' + + skipped.join(', ') + ); + } } catch (e) { console.warn('[deploy-env] 加载部署环境变量失败:' + (e && e.message)); } -})(); +} module.exports = (appInfo) => { const config = (exports = {}); + // 🔴 必须在下文任何 process.env.* 读取之前执行(原本是模块级 IIFE,见函数头注释)。 + // 放在此处而非模块顶层,是为了拿到已确定的 appInfo.env 以区分生产/本地。 + loadDeployEnv(appInfo); + const isProd = process.env.NODE_ENV === 'production' || appInfo.env === 'prod'; // -----------------------------------------------------------------