Files
BYW-VoiceHub/app/drizzle/db.ts
T
Henry_Du d818819fff
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Failing after 1m24s
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Skipped
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Failing after 4s
feat: 实现主题、黑名单、排程等14项功能迭代
- style(theme): 修复浅色模式不生效问题,为 ClassicLight 和 ModernLight 增加独立差异化 CSS 变量定义(主色/圆角/阴影/背景对比度)
- feat(blacklist): 后端放开 LANGUAGE/GENRE 类型校验;check API 新增语种(CJK/拉丁/假名/谚文/西里尔/阿拉伯)和曲风关键词匹配逻辑;前端 BlacklistManager UI 增加 语种/曲风 四类型切换、类型筛选、图标与标签颜色映射、输入提示
- fix(DataAnalysisPanel): 活跃用户列表容器加 max-h-[400px] + overflow-y-auto + 底部渐变遮罩;tooltip 加 max-height + truncate 防止超高;头像失败回退首字母
- feat(admin): 新增 user-avatar.ts 工具(OAuth→Gravatar→DiceBear 三级兜底);active-users/realtime/users 列表返回 avatarUrl;DataAnalysisPanel/UserManager 展示圆形头像;权限分级 ROLE_LEVELS,SUPER_ADMIN 全权限,ADMIN 仅可管理 ≤ USER 级用户的密码与角色,前端按钮联动禁用
- fix+style(schedule): ScheduleManager 标签页 sticky min-h 防抖、间距调优、表单分组、移动端 padding 统一
- feat(schedule): 自动排期新增「按投稿人数优先覆盖」策略(算法:requester + collaborators 总数降序),支持 CustomSelect 切换;新增 localStorage voicehub:schedule:auto-config 持久化 7 项配置(策略/时段/年级/排序/标签/搜索/学期)500ms debounce
- fix(schedule): songs/index.get 新增 usePool 参数,从 schedule_song_pool(status=PENDING,targetPlayDate IS NULL) 取歌并去重,排除已排期 songs.played=false;schedule.post 和 draft-bulk 分配后更新池状态为 ASSIGNED 避免重复分配
- feat(player): ScheduleManager 新增「构建播放队列」按钮,按 playDate→时段→sequence 排序后经 useAudioPlayer.playSong 整列载入并 toast 反馈
- fix(db): db.ts 检测 NETLIFY/VERCEL serverless 环境使用最小池 max=1 idle_timeout=1 keep_alive=false,Node Server 保持 max=10
- feat(site-config): site-config.get.ts 与 useSiteConfig.js 导出 statisticsCode 和 statisticsCodeEnabled;index.vue 挂载后解析 HTML 动态重插 script 元素确保第三方统计执行
- fix(schedule-printer): ScheduleItemPrint 封面 aspect-square object-cover;print-fix.css 打印时强制 48px×48px contain、去除圆角阴影
- fix(songRequest): 新增 server/utils/url.ts normalizeCoverUrl(trim/去零宽空格/相对协议补 https/非 http 丢弃/URL.parse 兜底);request.post/add.post/import.post 三处写入 cover/coverUrl 前统一规范化
2026-09-12 08:40:59 +08:00

178 lines
6.2 KiB
TypeScript
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.
import {drizzle} from 'drizzle-orm/postgres-js';
import {and, asc, count, desc, eq, exists, gt, gte, lt, lte, or, sql} from 'drizzle-orm';
import postgres from 'postgres';
import * as schema from './schema.ts';
import {config} from 'dotenv';
import path from 'path';
import {fileURLToPath} from 'url';
// 加载环境变量(优先使用工作目录的 .env,确保构建后运行时能正确加载)
config({ path: path.resolve(process.cwd(), '.env') });
// 检查环境变量
if (!process.env.DATABASE_URL) {
throw new Error('DATABASE_URL environment variable is not set');
}
// 创建PostgreSQL连接
const connectionString = process.env.DATABASE_URL;
// 检测数据库类型
const isNeonDatabase = connectionString.includes('neon.tech') || connectionString.includes('neon.database.com');
// 检测 Serverless 部署环境
const isServerlessEnv = process.env.NETLIFY === 'true' || !!process.env.VERCEL;
// 根据数据库类型和部署环境选择配置
const getDatabaseConfig = () => {
if (isNeonDatabase || isServerlessEnv) {
// Neon Database / Serverless 环境 (Netlify/Vercel) 优化配置
return {
max: 1, // Serverless 环境下每个实例保持最小连接数,利用平台自身的连接池
idle_timeout: 1, // 尽快释放空闲连接,适应 Serverless 的快速冻结特性
connect_timeout: 10, // 减少连接超时时间
max_lifetime: 3600, // 连接最大生命周期(1小时)
ssl: isNeonDatabase ? 'require' : (connectionString.includes('sslmode=require') || connectionString.includes('ssl=true') ? 'require' : false),
prepare: false, // 禁用预处理语句以提高兼容性
keep_alive: false, // 禁用 TCP keep-alive,避免 Serverless 环境下连接残留
transform: {
undefined: null, // 将undefined转换为null
},
connection: {
application_name: 'voicehub-app'
},
onnotice: process.env.NODE_ENV === 'development' ? console.log : undefined,
debug: process.env.NODE_ENV === 'development' && process.env.DEBUG_SQL === 'true'
};
} else {
// 标准 PostgreSQL 数据库配置(本地开发 / 常规 Node Server)
return {
max: 10, // 普通PostgreSQL可以支持更多连接
idle_timeout: 20, // 空闲超时时间
connect_timeout: 30, // 连接超时时间以适应网络延迟
max_lifetime: 3600, // 连接最大生命周期(1小时)
ssl: connectionString.includes('sslmode=require') || connectionString.includes('ssl=true') ? 'require' : false,
prepare: false, // 禁用预处理语句以提高兼容性
transform: {
undefined: null, // 将undefined转换为null
},
connection: {
application_name: 'voicehub-app'
},
onnotice: process.env.NODE_ENV === 'development' ? console.log : undefined,
debug: process.env.NODE_ENV === 'development' && process.env.DEBUG_SQL === 'true'
};
}
};
const client = postgres(connectionString, getDatabaseConfig());
// 创建Drizzle数据库实例
export const db = drizzle(client, { schema });
// 导出连接客户端(用于手动查询或关闭连接)
export { client };
// 导出schema以便在其他地方使用
export * from './schema.ts';
// 导出drizzle-orm函数
export {eq, and, gt, gte, lt, lte, count, exists, desc, asc, or, sql};
// 数据库连接测试函数
export async function testConnection() {
try {
await client`SELECT 1`;
console.log('✅ Database connection successful');
return true;
} catch (error) {
console.error('❌ Database connection failed:', error);
return false;
}
}
// 获取数据库连接状态
export function getConnectionStatus() {
return {
isConnected: !client.ended,
maxConnections: client.options.max,
idleTimeout: client.options.idle_timeout,
connectTimeout: client.options.connect_timeout
};
}
// 连接管理 - 根据数据库类型自适应
let idleTimer: NodeJS.Timeout | null = null;
// Neon 数据库使用更短的空闲时间以支持自动启停,普通 PostgreSQL 使用更长的空闲时间
const IDLE_TIMEOUT = isNeonDatabase ? 5 * 60 * 1000 : 10 * 60 * 1000; // Neon: 5分钟,PostgreSQL: 10分钟
// 重置空闲计时器
function resetIdleTimer() {
if (idleTimer) {
clearTimeout(idleTimer);
}
// 只在生产环境启用自动断开
if (process.env.NODE_ENV === 'production') {
idleTimer = setTimeout(async () => {
try {
if (!client.ended) {
const dbType = isNeonDatabase ? 'Neon' : 'PostgreSQL';
console.log(`🔄 Auto-closing idle ${dbType} database connections${isNeonDatabase ? ' for Serverless optimization' : ''}`);
await client.end({ timeout: isNeonDatabase ? 5 : 10 });
}
} catch (error) {
console.error('❌ Error during auto-close:', error);
}
}, IDLE_TIMEOUT);
}
}
// 包装数据库操作以支持自动启停
export function withAutoReconnect<T extends any[], R>(
operation: (...args: T) => Promise<R>
) {
return async (...args: T): Promise<R> => {
resetIdleTimer();
try {
return await operation(...args);
} catch (error: any) {
// 如果连接已关闭,记录信息
if (error?.code === 'CONNECTION_ENDED' || client.ended) {
const dbType = isNeonDatabase ? 'Neon' : 'PostgreSQL';
console.log(`🔄 ${dbType} database connection ended${isNeonDatabase ? ', Neon will auto-reconnect on next query' : ', will reconnect on next query'}`);
}
throw error;
}
};
}
// 优雅关闭数据库连接
export async function closeConnection() {
try {
if (idleTimer) {
clearTimeout(idleTimer);
idleTimer = null;
}
if (!client.ended) {
await client.end({ timeout: 10 });
console.log('✅ Database connection closed gracefully');
}
} catch (error) {
console.error('❌ Error closing database connection:', error);
}
}
// 设置优雅关闭处理
if (typeof process !== 'undefined') {
const gracefulShutdown = async () => {
console.log('🔄 Shutting down database connections...');
await closeConnection();
};
process.on('SIGINT', gracefulShutdown);
process.on('SIGTERM', gracefulShutdown);
process.on('beforeExit', gracefulShutdown);
}