- Expand batch upload list area and optimize preview image layout - Improve upload speed tracking and display (server-side & client-side) - Reduce network detection log noise - Optimize Docker build process (caching, env validation) - Add shadow environment deployment support
259 lines
6.4 KiB
TypeScript
259 lines
6.4 KiB
TypeScript
/**
|
||
* R47修复:环境变量启动验证
|
||
*
|
||
* 目的:在应用启动时验证所有必需的环境变量,提供清晰的错误提示
|
||
* 时机:在 lib/prisma.ts 和其他关键模块中调用
|
||
*/
|
||
|
||
/**
|
||
* 环境变量验证错误
|
||
*/
|
||
export class EnvValidationError extends Error {
|
||
public missingVars: string[]
|
||
|
||
constructor(missingVars: string[]) {
|
||
const varList = missingVars.map(v => ` - ${v}`).join('\n')
|
||
super(
|
||
`缺少必需的环境变量:\n${varList}\n\n` +
|
||
`请在 .env 文件或 docker-compose.yml 中配置这些变量。\n` +
|
||
`详情请参考:.env.example 或项目文档。`
|
||
)
|
||
this.name = 'EnvValidationError'
|
||
this.missingVars = missingVars
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 必需的环境变量列表
|
||
*/
|
||
const REQUIRED_ENV_VARS = [
|
||
// 数据库
|
||
'DATABASE_URL',
|
||
|
||
// NextAuth
|
||
'NEXTAUTH_SECRET',
|
||
'NEXTAUTH_URL',
|
||
|
||
// S3/MinIO(如果使用S3存储)
|
||
// 'S3_ENDPOINT',
|
||
// 'S3_BUCKET',
|
||
// 'S3_ACCESS_KEY_ID',
|
||
// 'S3_SECRET_ACCESS_KEY',
|
||
|
||
// Redis(如果使用Redis)
|
||
// 'REDIS_URL',
|
||
// 'USE_REDIS',
|
||
|
||
// 管理员API密钥
|
||
'ADMIN_API_KEY',
|
||
]
|
||
|
||
/**
|
||
* 可选但建议配置的环境变量
|
||
*/
|
||
const OPTIONAL_ENV_VARS = [
|
||
'S3_ENDPOINT',
|
||
'S3_BUCKET',
|
||
'S3_ACCESS_KEY_ID',
|
||
'S3_SECRET_ACCESS_KEY',
|
||
'REDIS_URL',
|
||
'USE_REDIS',
|
||
'DINGTALK_APP_KEY',
|
||
'DINGTALK_APP_SECRET',
|
||
'DOWNLOAD_TOKEN_SECRET',
|
||
'DISABLE_CACHE',
|
||
]
|
||
|
||
/**
|
||
* 验证所有必需的环境变量
|
||
* @throws {EnvValidationError} 如果缺少必需的环境变量
|
||
*/
|
||
export function validateRequiredEnvVars(): void {
|
||
const missing: string[] = []
|
||
|
||
for (const varName of REQUIRED_ENV_VARS) {
|
||
if (!process.env[varName]) {
|
||
missing.push(varName)
|
||
}
|
||
}
|
||
|
||
if (missing.length > 0) {
|
||
throw new EnvValidationError(missing)
|
||
}
|
||
|
||
console.log('[Env] ✅ 所有必需的环境变量已配置')
|
||
}
|
||
|
||
/**
|
||
* 验证环境变量的格式
|
||
* @throws {Error} 如果环境变量格式不正确
|
||
*/
|
||
export function validateEnvFormats(): void {
|
||
const errors: string[] = []
|
||
|
||
// 验证 DATABASE_URL 格式
|
||
const databaseUrl = process.env.DATABASE_URL
|
||
if (databaseUrl) {
|
||
try {
|
||
const url = new URL(databaseUrl)
|
||
if (url.protocol !== 'postgresql:' && url.protocol !== 'postgres:') {
|
||
errors.push(`DATABASE_URL 格式错误:应该使用 postgresql:// 或 postgres:// 协议`)
|
||
}
|
||
} catch (e) {
|
||
errors.push(`DATABASE_URL 格式错误:${e}`)
|
||
}
|
||
}
|
||
|
||
// 验证 NEXTAUTH_URL 格式
|
||
const nextauthUrl = process.env.NEXTAUTH_URL
|
||
if (nextauthUrl) {
|
||
try {
|
||
new URL(nextauthUrl)
|
||
} catch (e) {
|
||
errors.push(`NEXTAUTH_URL 格式错误:${e}`)
|
||
}
|
||
}
|
||
|
||
// 验证 REDIS_URL 格式(如果配置了)
|
||
const redisUrl = process.env.REDIS_URL
|
||
if (redisUrl) {
|
||
try {
|
||
const url = new URL(redisUrl)
|
||
if (url.protocol !== 'redis:') {
|
||
errors.push(`REDIS_URL 格式错误:应该使用 redis:// 协议`)
|
||
}
|
||
} catch (e) {
|
||
errors.push(`REDIS_URL 格式错误:${e}`)
|
||
}
|
||
}
|
||
|
||
// 验证 S3_ENDPOINT 格式(如果配置了)
|
||
const s3Endpoint = process.env.S3_ENDPOINT
|
||
if (s3Endpoint) {
|
||
try {
|
||
const url = new URL(s3Endpoint)
|
||
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
||
errors.push(`S3_ENDPOINT 格式错误:应该使用 http:// 或 https:// 协议`)
|
||
}
|
||
} catch (e) {
|
||
errors.push(`S3_ENDPOINT 格式错误:${e}`)
|
||
}
|
||
}
|
||
|
||
if (errors.length > 0) {
|
||
const errorList = errors.map(e => ` - ${e}`).join('\n')
|
||
throw new Error(
|
||
`环境变量格式错误:\n${errorList}\n\n` +
|
||
`请检查 .env 文件或 docker-compose.yml 中的配置。`
|
||
)
|
||
}
|
||
|
||
console.log('[Env] ✅ 环境变量格式验证通过')
|
||
}
|
||
|
||
/**
|
||
* 检查可选环境变量并给出提示
|
||
*/
|
||
export function checkOptionalEnvVars(): void {
|
||
const configured: string[] = []
|
||
const notConfigured: string[] = []
|
||
|
||
for (const varName of OPTIONAL_ENV_VARS) {
|
||
if (process.env[varName]) {
|
||
configured.push(varName)
|
||
} else {
|
||
notConfigured.push(varName)
|
||
}
|
||
}
|
||
|
||
if (configured.length > 0) {
|
||
console.log(`[Env] ℹ️ 已配置的可选变量:${configured.join(', ')}`)
|
||
}
|
||
|
||
if (notConfigured.length > 0) {
|
||
console.log(`[Env] ⚠️ 未配置的可选变量:${notConfigured.join(', ')}`)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检测是否在 Next.js 构建阶段
|
||
* 构建时不应该验证环境变量,因为构建环境没有运行时环境变量
|
||
*/
|
||
function isBuildPhase(): boolean {
|
||
// 方式1:检测 NEXT_PHASE 环境变量
|
||
if (process.env.NEXT_PHASE === 'phase-production-build') {
|
||
return true
|
||
}
|
||
// 方式2:检测 npm_lifecycle_event(构建脚本中会设置)
|
||
if (process.env.npm_lifecycle_event === 'build') {
|
||
return true
|
||
}
|
||
// 方式3:检测 __NEXT_PRIVATE_PREBUNDLED_REACT(Next.js 内部变量)
|
||
if (process.env.__NEXT_PRIVATE_PREBUNDLED_REACT) {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
/**
|
||
* 完整的环境变量验证(在应用启动时调用)
|
||
* @throws {Error} 如果验证失败且在生产运行时环境
|
||
*/
|
||
export function validateEnvironment(): void {
|
||
// 构建阶段跳过验证
|
||
if (isBuildPhase()) {
|
||
return
|
||
}
|
||
|
||
console.log('[Env] 🔍 开始验证环境变量...')
|
||
|
||
try {
|
||
validateRequiredEnvVars()
|
||
validateEnvFormats()
|
||
checkOptionalEnvVars()
|
||
|
||
console.log('[Env] ✅ 环境变量验证完成')
|
||
} catch (error) {
|
||
console.error('[Env] ❌ 环境变量验证失败:', error)
|
||
// 只在生产运行时抛出错误,开发环境只记录警告
|
||
if (process.env.NODE_ENV === 'production') {
|
||
throw error
|
||
}
|
||
console.warn('[Env] ⚠️ 非 production 环境,跳过环境变量验证错误')
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 安全获取环境变量(带默认值和类型转换)
|
||
*/
|
||
export function getEnvVar(key: string, defaultValue?: string): string | undefined {
|
||
const value = process.env[key]
|
||
if (value === undefined || value === '') {
|
||
return defaultValue
|
||
}
|
||
return value
|
||
}
|
||
|
||
/**
|
||
* 获取布尔型环境变量
|
||
*/
|
||
export function getBoolEnvVar(key: string, defaultValue: boolean = false): boolean {
|
||
const value = process.env[key]
|
||
if (value === undefined || value === '') {
|
||
return defaultValue
|
||
}
|
||
return value === 'true' || value === '1'
|
||
}
|
||
|
||
/**
|
||
* 获取数字型环境变量
|
||
*/
|
||
export function getNumberEnvVar(key: string, defaultValue: number = 0): number {
|
||
const value = process.env[key]
|
||
if (value === undefined || value === '') {
|
||
return defaultValue
|
||
}
|
||
const num = parseInt(value, 10)
|
||
return isNaN(num) ? defaultValue : num
|
||
}
|