import { createError, defineEventHandler, readBody } from 'h3' import { db } from '~/drizzle/db' import { apiKeys, apiKeyPermissions, apiLogs, collaborationLogs, emailTemplates, notifications, notificationSettings, playTimes, requestTimes, schedules, semesters, songBlacklists, songCollaborators, songReplayRequests, songs, systemSettings, users, userIdentities, userStatusLogs, votes } from '~/drizzle/schema' import { promises as fs } from 'fs' import path from 'path' export default defineEventHandler(async (event) => { try { // 验证管理员权限 const user = event.context.user if (!user || !['ADMIN', 'SUPER_ADMIN'].includes(user.role)) { throw createError({ statusCode: 403, statusMessage: '权限不足' }) } const body = await readBody(event) const { tables = 'all', includeSystemData = true } = body console.log('开始创建数据库备份...') // 创建备份数据对象 const backupData = { metadata: { version: '1.0', timestamp: new Date().toISOString(), creator: user.username, description: '', backupType: 'custom', tables: [], totalRecords: 0 }, data: {} } // 定义要备份的表和对应的查询 const tablesToBackup = { users: { query: async () => { const usersData = await db.select().from(users) const settingsData = await db.select().from(notificationSettings) // 手动关联通知设置 return usersData.map((user) => ({ ...user, notificationSettings: settingsData.filter((setting) => setting.userId === user.id) })) }, description: '用户数据' }, songs: { query: async () => { const songsData = await db.select().from(songs) const usersData = await db .select({ id: users.id, username: users.username, name: users.name }) .from(users) const votesData = await db.select().from(votes) const schedulesData = await db.select().from(schedules) const playTimesData = await db.select().from(playTimes) // 手动关联数据 return songsData.map((song) => ({ ...song, requester: usersData.find((user) => user.id === song.requesterId), votes: votesData .filter((vote) => vote.songId === song.id) .map((vote) => ({ ...vote, user: usersData.find((user) => user.id === vote.userId) })), schedules: schedulesData.filter((schedule) => schedule.songId === song.id), preferredPlayTime: playTimesData.find((pt) => pt.id === song.preferredPlayTimeId) })) }, description: '歌曲数据' }, votes: { query: async () => { const votesData = await db.select().from(votes) const usersData = await db .select({ id: users.id, username: users.username, name: users.name }) .from(users) const songsData = await db .select({ id: songs.id, title: songs.title, artist: songs.artist }) .from(songs) return votesData.map((vote) => ({ ...vote, user: usersData.find((user) => user.id === vote.userId), song: songsData.find((song) => song.id === vote.songId) })) }, description: '投票数据' }, schedules: { query: async () => { const schedulesData = await db.select().from(schedules) const songsData = await db .select({ id: songs.id, title: songs.title, artist: songs.artist }) .from(songs) const playTimesData = await db.select().from(playTimes) return schedulesData.map((schedule) => ({ ...schedule, song: songsData.find((song) => song.id === schedule.songId), playTime: playTimesData.find((pt) => pt.id === schedule.playTimeId) })) }, description: '排期数据' }, notifications: { query: async () => { const notificationsData = await db.select().from(notifications) const usersData = await db .select({ id: users.id, username: users.username, name: users.name }) .from(users) const songsData = await db .select({ id: songs.id, title: songs.title, artist: songs.artist }) .from(songs) return notificationsData.map((notification) => ({ ...notification, user: usersData.find((user) => user.id === notification.userId), song: notification.songId ? songsData.find((song) => song.id === notification.songId) : null })) }, description: '通知数据' }, notificationSettings: { query: async () => { const settingsData = await db.select().from(notificationSettings) const usersData = await db .select({ id: users.id, username: users.username, name: users.name }) .from(users) return settingsData.map((setting) => ({ ...setting, user: usersData.find((user) => user.id === setting.userId) })) }, description: '通知设置' }, playTimes: { query: () => db.select().from(playTimes), description: '播出时段' }, semesters: { query: () => db.select().from(semesters), description: '学期数据' }, songBlacklist: { query: () => db.select().from(songBlacklists), description: '歌曲黑名单' }, userStatusLogs: { query: async () => { const statusLogsData = await db.select().from(userStatusLogs) const usersData = await db .select({ id: users.id, username: users.username, name: users.name }) .from(users) return statusLogsData.map((log) => ({ ...log, user: usersData.find((user) => user.id === log.userId) })) }, description: '用户状态变更日志' }, userIdentities: { query: async () => { const identitiesData = await db.select().from(userIdentities) const usersData = await db .select({ id: users.id, username: users.username, name: users.name }) .from(users) return identitiesData.map((identity) => ({ ...identity, user: usersData.find((user) => user.id === identity.userId) })) }, description: '第三方身份关联' }, songCollaborators: { query: async () => { const collaboratorsData = await db.select().from(songCollaborators) return collaboratorsData }, description: '联合投稿人' }, collaborationLogs: { query: async () => { const logsData = await db.select().from(collaborationLogs) return logsData }, description: '联合投稿审计日志' }, songReplayRequests: { query: async () => { const requestsData = await db.select().from(songReplayRequests) const usersData = await db .select({ id: users.id, username: users.username, name: users.name }) .from(users) const songsData = await db .select({ id: songs.id, title: songs.title, artist: songs.artist }) .from(songs) return requestsData.map((req) => ({ ...req, user: usersData.find((user) => user.id === req.userId), song: songsData.find((song) => song.id === req.songId) })) }, description: '歌曲重播申请' }, apiKeys: { query: async () => { const apiKeysData = await db.select().from(apiKeys) const usersData = await db .select({ id: users.id, username: users.username, name: users.name }) .from(users) const permissionsData = await db.select().from(apiKeyPermissions) return apiKeysData.map((key) => ({ ...key, createdByUser: usersData.find((user) => user.id === key.createdByUserId), permissions: permissionsData.filter((perm) => perm.apiKeyId === key.id) })) }, description: 'API密钥' }, apiKeyPermissions: { query: () => db.select().from(apiKeyPermissions), description: 'API密钥权限' }, apiLogs: { query: async () => { const logsData = await db.select().from(apiLogs) const apiKeysData = await db .select({ id: apiKeys.id, name: apiKeys.name }) .from(apiKeys) return logsData.map((log) => ({ ...log, apiKey: log.apiKeyId ? apiKeysData.find((key) => key.id === log.apiKeyId) : null })) }, description: 'API访问日志' }, emailTemplates: { query: async () => { const templatesData = await db.select().from(emailTemplates) const usersData = await db .select({ id: users.id, username: users.username, name: users.name }) .from(users) return templatesData.map((template) => ({ ...template, updatedByUser: template.updatedByUserId ? usersData.find((user) => user.id === template.updatedByUserId) : null })) }, description: '邮件模板' }, requestTimes: { query: () => db.select().from(requestTimes), description: '请求时段' } } // 如果包含系统数据,添加系统设置表 if (includeSystemData) { tablesToBackup.systemSettings = { query: () => db.select().from(systemSettings), description: '系统设置' } } // 根据请求的表进行备份 let tablesToProcess if (tables === 'all') { tablesToProcess = Object.keys(tablesToBackup) } else if (tables === 'users') { // 仅备份用户相关数据 tablesToProcess = ['users', 'notificationSettings', 'userStatusLogs', 'userIdentities'] // 如果包含系统数据,也添加到处理列表中 if (includeSystemData) { tablesToProcess.push('systemSettings') } } else if (Array.isArray(tables)) { tablesToProcess = tables } else { tablesToProcess = [tables] } if ( includeSystemData && tablesToBackup.systemSettings && !tablesToProcess.includes('systemSettings') ) { tablesToProcess.push('systemSettings') } if (tablesToProcess.length === 1 && tablesToProcess[0] === 'systemSettings') { backupData.metadata.backupType = 'system' backupData.metadata.description = `系统配置备份 - ${new Date().toLocaleString('zh-CN')}` } else if (tables === 'users') { backupData.metadata.backupType = 'users' backupData.metadata.description = `用户数据备份 - ${new Date().toLocaleString('zh-CN')}` } else if (tables === 'all') { backupData.metadata.backupType = 'full' backupData.metadata.description = `完整数据库备份 - ${new Date().toLocaleString('zh-CN')}` } else if (tables === 'songs') { backupData.metadata.backupType = 'songs' backupData.metadata.description = `歌曲数据备份 - ${new Date().toLocaleString('zh-CN')}` } else { backupData.metadata.description = `自定义数据库备份 - ${new Date().toLocaleString('zh-CN')}` } let totalRecords = 0 for (const tableName of tablesToProcess) { if (!tablesToBackup[tableName]) { console.warn(`未知的表名: ${tableName}`) continue } try { console.log(`备份表: ${tableName}`) const tableData = await tablesToBackup[tableName].query() backupData.data[tableName] = tableData backupData.metadata.tables.push({ name: tableName, description: tablesToBackup[tableName].description, recordCount: tableData.length }) totalRecords += tableData.length console.log(`✅ ${tableName}: ${tableData.length} 条记录`) } catch (error) { // 检查是否为表不存在错误 // Postgres 错误代码 42P01: undefined_table (表不存在) const isTableMissing = error.code === '42P01' || (error.message && error.message.includes('does not exist')) || (error.statusMessage && error.statusMessage.includes('does not exist')) if (isTableMissing) { console.warn(`⚠️ 表 ${tableName} 不存在,跳过备份`) // 记录一个空的条目,以便知道跳过了 backupData.metadata.tables.push({ name: tableName, description: `${tablesToBackup[tableName].description} (表不存在,已跳过)`, recordCount: 0, skipped: true }) continue } console.error(`备份表 ${tableName} 失败:`, error) throw createError({ statusCode: 500, statusMessage: `备份表 ${tableName} 失败: ${error.message}` }) } } backupData.metadata.totalRecords = totalRecords // 生成备份文件名(用于下载) const timestamp = new Date().toISOString().replace(/[:.]/g, '-') let filePrefix = 'database-backup' if (tables === 'users') { filePrefix = includeSystemData ? 'users-system-backup' : 'users-backup' } else if (tablesToProcess.length === 1 && tablesToProcess[0] === 'systemSettings') { filePrefix = 'system-settings-backup' } else if (tables === 'songs') { filePrefix = includeSystemData ? 'songs-system-backup' : 'songs-backup' } const filename = `${filePrefix}-${timestamp}.json` console.log(`✅ 备份完成: ${filename}`) console.log(`📊 总计备份 ${totalRecords} 条记录`) // 检测运行环境 const isVercel = process.env.VERCEL || process.env.VERCEL_ENV const isNetlify = process.env.NETLIFY const isServerless = isVercel || isNetlify if (isServerless) { // 在无服务器环境中,直接返回备份数据供前端下载 console.log('🌐 检测到无服务器环境,直接返回备份数据') // 计算数据大小(估算) const dataSize = JSON.stringify(backupData).length return { success: true, message: '数据库备份创建成功', backup: { filename, data: backupData, size: dataSize, metadata: backupData.metadata, downloadMode: 'direct' // 标识为直接下载模式 } } } else { // 在传统服务器环境中,保存到文件系统 try { const backupDir = path.join(process.cwd(), 'backups') try { await fs.access(backupDir) } catch { await fs.mkdir(backupDir, { recursive: true }) } const filepath = path.join(backupDir, filename) await fs.writeFile(filepath, JSON.stringify(backupData, null, 2), 'utf8') return { success: true, message: '数据库备份创建成功', backup: { filename, filepath, size: (await fs.stat(filepath)).size, metadata: backupData.metadata, downloadMode: 'file' // 标识为文件下载模式 } } } catch (fsError) { console.warn('文件系统操作失败,回退到直接返回模式:', fsError.message) // 如果文件系统操作失败,回退到直接返回模式 const dataSize = JSON.stringify(backupData).length return { success: true, message: '数据库备份创建成功(直接下载模式)', backup: { filename, data: backupData, size: dataSize, metadata: backupData.metadata, downloadMode: 'direct' } } } } } catch (error) { console.error('创建数据库备份失败:', error) throw createError({ statusCode: error.statusCode || 500, statusMessage: error.statusMessage || '创建数据库备份失败' }) } })