import { createError, defineEventHandler, readBody, readMultipartFormData } from 'h3' import { db } from '~/drizzle/db' import { apiKeyPermissions, apiKeys, apiLogs, collaborationLogs, emailTemplates, notificationSettings, notifications, playTimes, requestTimes, schedules, semesters, songBlacklists, songCollaborators, songReplayRequests, songs, systemSettings, userIdentities, users, userStatusLogs, votes } from '~/drizzle/schema' import { promises as fs } from 'fs' import path from 'path' import { CacheService } from '../../../services/cacheService' import { SmtpService } from '../../../services/smtpService' import { and, eq, inArray, isNull, notInArray, or } from 'drizzle-orm' export default defineEventHandler(async (event) => { try { // 验证管理员权限 const user = event.context.user if (!user || !['ADMIN', 'SUPER_ADMIN'].includes(user.role)) { throw createError({ statusCode: 403, statusMessage: '权限不足' }) } let backupData let mode = 'merge' let clearExisting = false let overwriteSuperAdmin = false // 检查是否是文件上传 const contentType = event.node.req.headers['content-type'] if (contentType && contentType.includes('multipart/form-data')) { // 处理文件上传 const formData = await readMultipartFormData(event) if (!formData) { throw createError({ statusCode: 400, statusMessage: '请上传备份文件' }) } let fileData = null for (const field of formData) { if (field.name === 'file' && field.data) { fileData = field.data.toString('utf8') } else if (field.name === 'mode' && field.data) { mode = field.data.toString() } else if (field.name === 'clearExisting' && field.data) { clearExisting = field.data.toString() === 'true' } else if (field.name === 'overwriteSuperAdmin' && field.data) { overwriteSuperAdmin = field.data.toString() === 'true' } } if (!fileData) { throw createError({ statusCode: 400, statusMessage: '请上传备份文件' }) } try { backupData = JSON.parse(fileData) } catch (error) { throw createError({ statusCode: 400, statusMessage: '备份文件格式错误' }) } console.log(`开始恢复上传的数据库备份`) } else { // 处理传统的文件名方式(向后兼容) const body = await readBody(event) const { filename, mode: bodyMode = 'merge', clearExisting: bodyClearExisting = false, overwriteSuperAdmin: bodyOverwriteSuperAdmin = false } = body if (!filename) { throw createError({ statusCode: 400, statusMessage: '请指定备份文件名或上传备份文件' }) } const safeFilename = path.basename(filename) if (safeFilename !== filename) { throw createError({ statusCode: 400, statusMessage: '备份文件名不合法' }) } mode = bodyMode clearExisting = bodyClearExisting overwriteSuperAdmin = bodyOverwriteSuperAdmin console.log(`开始恢复数据库备份: ${safeFilename}`) // 读取备份文件 const backupDir = path.join(process.cwd(), 'backups') const filepath = path.join(backupDir, safeFilename) try { const fileContent = await fs.readFile(filepath, 'utf8') backupData = JSON.parse(fileContent) } catch (error) { throw createError({ statusCode: 404, statusMessage: '备份文件不存在或格式错误' }) } } // 验证备份文件格式 if (!backupData.metadata || !backupData.data) { throw createError({ statusCode: 400, statusMessage: '备份文件格式无效' }) } console.log(`备份文件信息:`) console.log(`- 版本: ${backupData.metadata.version}`) console.log(`- 创建时间: ${backupData.metadata.timestamp}`) console.log(`- 创建者: ${backupData.metadata.creator}`) console.log(`- 总记录数: ${backupData.metadata.totalRecords}`) const backupUsers = Array.isArray(backupData.data?.users) ? backupData.data.users : [] const hasSuperAdminInBackup = backupUsers.some((record) => record?.role === 'SUPER_ADMIN') if (overwriteSuperAdmin && user.role !== 'SUPER_ADMIN') { throw createError({ statusCode: 403, statusMessage: '仅超级管理员可以覆盖超级管理员账号数据' }) } const shouldOverwriteSuperAdmin = mode === 'replace' && clearExisting && overwriteSuperAdmin && hasSuperAdminInBackup const preservedSuperAdminIds = new Set() const restoreResults = { success: true, message: '数据恢复完成', details: { tablesProcessed: 0, recordsRestored: 0, errors: [], warnings: [] } } // 如果需要清空现有数据 if (clearExisting) { console.log('清空现有数据...') try { if (shouldOverwriteSuperAdmin) { await db.delete(apiLogs) await db.delete(apiKeyPermissions) await db.delete(apiKeys) await db.delete(notifications) await db.delete(notificationSettings) await db.delete(collaborationLogs) await db.delete(songCollaborators) await db.delete(songReplayRequests) await db.delete(schedules) await db.delete(votes) await db.delete(songs) await db.delete(songBlacklists) await db.delete(userStatusLogs) await db.delete(emailTemplates) await db.delete(userIdentities) await db.delete(users) await db.delete(playTimes) await db.delete(semesters) await db.delete(requestTimes) await db.delete(systemSettings) } else { const preservedUsers = await db .select({ id: users.id }) .from(users) .where(or(eq(users.role, 'SUPER_ADMIN'), eq(users.id, 1))) preservedUsers.forEach((item) => preservedSuperAdminIds.add(item.id)) const preservedUserIdList = [...preservedSuperAdminIds] const preservedApiKeys = await db .select({ id: apiKeys.id }) .from(apiKeys) .where( preservedUserIdList.length > 0 ? inArray(apiKeys.createdByUserId, preservedUserIdList) : eq(apiKeys.createdByUserId, -1) ) const preservedApiKeyIds = preservedApiKeys.map((item) => item.id) if (preservedApiKeyIds.length > 0) { await db .delete(apiLogs) .where(or(isNull(apiLogs.apiKeyId), notInArray(apiLogs.apiKeyId, preservedApiKeyIds))) await db .delete(apiKeyPermissions) .where(notInArray(apiKeyPermissions.apiKeyId, preservedApiKeyIds)) } else { await db.delete(apiLogs) await db.delete(apiKeyPermissions) } if (preservedUserIdList.length > 0) { await db.delete(apiKeys).where(notInArray(apiKeys.createdByUserId, preservedUserIdList)) await db.delete(notifications).where(notInArray(notifications.userId, preservedUserIdList)) await db .delete(notificationSettings) .where(notInArray(notificationSettings.userId, preservedUserIdList)) await db.delete(userStatusLogs).where(notInArray(userStatusLogs.userId, preservedUserIdList)) await db.delete(userIdentities).where(notInArray(userIdentities.userId, preservedUserIdList)) await db.delete(users).where(notInArray(users.id, preservedUserIdList)) } else { await db.delete(apiKeys) await db.delete(notifications) await db.delete(notificationSettings) await db.delete(userStatusLogs) await db.delete(userIdentities) await db.delete(users) } await db.delete(collaborationLogs) await db.delete(songCollaborators) await db.delete(songReplayRequests) await db.delete(schedules) await db.delete(votes) await db.delete(songs) await db.delete(songBlacklists) await db.delete(emailTemplates) await db.delete(playTimes) await db.delete(semesters) await db.delete(requestTimes) await db.delete(systemSettings) } console.log('✅ 现有数据已清空') } catch (error) { console.error('清空数据失败:', error) restoreResults.details.warnings.push(`清空数据失败: ${error.message}`) } } // 建立ID映射表 const userIdMapping = new Map() // 备份ID -> 当前数据库ID const songIdMapping = new Map() // 备份ID -> 当前数据库ID // 定义恢复顺序(考虑外键依赖) const restoreOrder = [ 'systemSettings', 'playTimes', 'semesters', 'requestTimes', 'users', 'userIdentities', 'emailTemplates', 'userStatusLogs', 'songBlacklist', 'songs', 'songCollaborators', 'collaborationLogs', 'songReplayRequests', 'votes', 'schedules', 'notificationSettings', 'notifications', 'apiKeys', 'apiKeyPermissions', 'apiLogs' ] // 按预定义顺序恢复数据,每个表使用独立事务 for (const tableName of restoreOrder) { if (!backupData.data[tableName] || !Array.isArray(backupData.data[tableName])) { continue } const tableData = backupData.data[tableName] console.log(`恢复表: ${tableName} (${tableData.length} 条记录)`) try { let restoredCount = 0 // 分批处理大量数据,每条记录使用独立事务 let batchSize = 10 // 初始批次大小 let consecutiveErrors = 0 for (let i = 0; i < tableData.length; i += batchSize) { const batch = tableData.slice(i, i + batchSize) // 逐条处理记录,每条记录使用独立事务,带重试机制 for (const record of batch) { let retryCount = 0 const maxRetries = 3 let lastError = null while (retryCount <= maxRetries) { try { await db.$transaction( async (tx) => { // 根据表名选择恢复策略 switch (tableName) { case 'users': // 动态构建用户数据,自动跳过不存在的字段 const buildUserData = (includePassword = false) => { const userData = {} const userFields = [ 'username', 'name', 'grade', 'class', 'role', 'email', 'emailVerified', 'lastLoginIp', 'forcePasswordChange', 'meowNickname', 'status', 'statusChangedBy' ] // 处理日期字段 const dateFields = [ 'createdAt', 'updatedAt', 'lastLogin', 'passwordChangedAt', 'meowBoundAt', 'statusChangedAt' ] // 添加基本字段 userFields.forEach((field) => { if (record.hasOwnProperty(field)) { // 确保角色为有效的硬编码角色 if (field === 'role') { if ( !['USER', 'TEACHER', 'SONG_ADMIN', 'ADMIN', 'SUPER_ADMIN'].includes( record[field] ) ) { userData[field] = 'USER' // 默认为普通用户 } else { userData[field] = record[field] } } else { userData[field] = record[field] } } }) // 处理日期字段 dateFields.forEach((field) => { if (record.hasOwnProperty(field) && record[field]) { userData[field] = new Date(record[field]) } else if (record.hasOwnProperty(field)) { userData[field] = null } }) // 密码字段只在创建时需要 if (includePassword && record.hasOwnProperty('password')) { userData.password = record.password } return userData } let createdUser if (mode === 'merge') { // 在merge模式下,先检查用户名是否存在 const existingUser = await tx .select() .from(users) .where(eq(users.username, record.username)) .limit(1) if (existingUser.length > 0) { // 如果用户名已存在,更新现有用户 const result = await tx .update(users) .set(buildUserData(false)) .where(eq(users.username, record.username)) .returning({ id: users.id }) createdUser = result[0] } else { // 如果用户名不存在,尝试使用原始ID创建 try { // 先检查原始ID是否已被占用 const existingUserWithId = await tx .select() .from(users) .where(eq(users.id, record.id)) .limit(1) if (existingUserWithId.length > 0) { // ID已被占用,让数据库自动生成新ID const result = await tx .insert(users) .values(buildUserData(true)) .returning({ id: users.id }) createdUser = result[0] } else { // ID未被占用,使用原始ID const result = await tx .insert(users) .values({ ...buildUserData(true), id: record.id }) .returning({ id: users.id }) createdUser = result[0] } } catch (error) { // 如果创建失败(可能是ID冲突),让数据库自动生成ID console.warn( `用户 ${record.username} 使用原始ID创建失败,使用自动生成ID: ${error.message}` ) const result = await tx .insert(users) .values(buildUserData(true)) .returning({ id: users.id }) createdUser = result[0] } } } else { const isBackupSuperAdminRecord = record.role === 'SUPER_ADMIN' || preservedSuperAdminIds.has(Number(record.id)) if (!shouldOverwriteSuperAdmin && isBackupSuperAdminRecord) { if (record.id) { const existingProtectedUser = await tx .select({ id: users.id }) .from(users) .where(eq(users.id, record.id)) .limit(1) if (existingProtectedUser.length > 0) { userIdMapping.set(record.id, existingProtectedUser[0].id) } } restoreResults.details.warnings.push( `已保留超级管理员账户 ${record.username || `#${record.id}`}` ) break } // 完全恢复模式,检查ID是否已存在 const existingUserWithId = await tx .select() .from(users) .where(eq(users.id, record.id)) .limit(1) if (existingUserWithId.length > 0) { // ID已存在,使用upsert策略更新现有用户 console.warn( `用户ID ${record.id} (${record.username}) 已存在,将更新现有用户` ) const result = await tx .update(users) .set(buildUserData(true)) .where(eq(users.id, record.id)) .returning({ id: users.id }) createdUser = result[0] } else { // ID不存在,检查用户名是否冲突(针对保留的超级管理员等情况) const existingUserWithUsername = await tx .select() .from(users) .where(eq(users.username, record.username)) .limit(1) if (existingUserWithUsername.length > 0) { const targetUser = existingUserWithUsername[0] const isProtectedTargetUser = targetUser.role === 'SUPER_ADMIN' || preservedSuperAdminIds.has(Number(targetUser.id)) if (!shouldOverwriteSuperAdmin && isProtectedTargetUser) { userIdMapping.set(record.id, targetUser.id) restoreResults.details.warnings.push( `已保留受保护账户 ${targetUser.username || `#${targetUser.id}`}` ) break } // 用户名已存在(如保留的admin),更新该用户,并建立映射 console.warn( `用户 ${record.username} (ID ${record.id}) 的用户名已存在于 ID ${existingUserWithUsername[0].id},将合并数据` ) const result = await tx .update(users) .set(buildUserData(true)) .where(eq(users.id, existingUserWithUsername[0].id)) .returning({ id: users.id }) createdUser = result[0] } else { // ID不存在且用户名不冲突,使用原始ID创建 const result = await tx .insert(users) .values({ ...buildUserData(true), id: record.id }) .returning({ id: users.id }) createdUser = result[0] } } } // 建立ID映射 if (record.id && createdUser.id) { userIdMapping.set(record.id, createdUser.id) } break case 'userIdentities': // 验证必需字段 let validIdentityUserId = record.userId if (record.userId) { const mappedUserId = userIdMapping.get(record.userId) if (mappedUserId) { validIdentityUserId = mappedUserId } else { const userExists = await tx .select() .from(users) .where(eq(users.id, record.userId)) .limit(1) if (userExists.length === 0) { console.warn( `身份关联记录的用户ID ${record.userId} 不存在,跳过此记录` ) break } } } else { console.warn(`身份关联记录缺少userId,跳过此记录`) break } const identityData = { userId: validIdentityUserId, provider: record.provider, providerUserId: record.providerUserId, providerUsername: record.providerUsername, createdAt: record.createdAt ? new Date(record.createdAt) : new Date() } if ( !shouldOverwriteSuperAdmin && preservedSuperAdminIds.has(Number(validIdentityUserId)) ) { break } if (mode === 'merge') { await tx .insert(userIdentities) .values(identityData) .onConflictDoUpdate({ target: [userIdentities.provider, userIdentities.providerUserId], set: identityData }) } else { if (record.id) { const existing = await tx .select() .from(userIdentities) .where(eq(userIdentities.id, record.id)) .limit(1) if (existing.length > 0) { await tx .update(userIdentities) .set(identityData) .where(eq(userIdentities.id, record.id)) } else { await tx.insert(userIdentities).values({ ...identityData, id: record.id }) } } else { await tx.insert(userIdentities).values(identityData) } } break case 'userStatusLogs': // 验证外键约束 - 用户ID let validUserStatusLogUserId = record.userId // 使用ID映射查找实际的用户ID if (record.userId) { const mappedUserId = userIdMapping.get(record.userId) if (mappedUserId) { validUserStatusLogUserId = mappedUserId } else { // 尝试直接查找用户ID const userExists = await tx .select() .from(users) .where(eq(users.id, record.userId)) .limit(1) if (userExists.length === 0) { console.warn( `用户状态日志的用户ID ${record.userId} 不存在,跳过此记录` ) return // 跳过此记录,因为userId是必需的 } } } else { console.warn(`用户状态日志缺少userId,跳过此记录`) return // 跳过此记录,因为userId是必需的 } // 构建用户状态日志数据 const userStatusLogData = { userId: validUserStatusLogUserId, oldStatus: record.oldStatus || record.previousStatus || null, newStatus: record.newStatus, reason: record.reason || null, operatorId: record.operatorId || record.changedBy || null, createdAt: record.createdAt ? new Date(record.createdAt) : new Date() } if (mode === 'merge') { // 对于状态日志,通常不需要检查重复,直接创建新记录 await tx.insert(userStatusLogs).values(userStatusLogData) } else { // 完全恢复模式,检查ID是否已存在 if (record.id) { const existingLogWithId = await tx .select() .from(userStatusLogs) .where(eq(userStatusLogs.id, record.id)) .limit(1) if (existingLogWithId.length > 0) { // ID已存在,更新现有记录 console.warn(`用户状态日志ID ${record.id} 已存在,将更新现有记录`) await tx .update(userStatusLogs) .set(userStatusLogData) .where(eq(userStatusLogs.id, record.id)) } else { // ID不存在,使用原始ID创建 await tx.insert(userStatusLogs).values({ ...userStatusLogData, id: record.id }) } } else { // 没有ID,让数据库自动生成 await tx.insert(userStatusLogs).values(userStatusLogData) } } break case 'songs': // 验证外键约束 let validRequesterId = record.requesterId let validPreferredPlayTimeId = record.preferredPlayTimeId // 使用ID映射查找实际的用户ID if (record.requesterId) { const mappedUserId = userIdMapping.get(record.requesterId) if (mappedUserId) { validRequesterId = mappedUserId } else { // 尝试直接查找用户ID const userExists = await tx .select() .from(users) .where(eq(users.id, record.requesterId)) .limit(1) if (userExists.length === 0) { console.warn( `歌曲 ${record.title} 的请求者ID ${record.requesterId} 不存在,跳过此记录` ) return // 跳过此记录,因为requesterId是必需的 } } } else { console.warn(`歌曲 ${record.title} 缺少requesterId,跳过此记录`) return // 跳过此记录,因为requesterId是必需的 } // 检查preferredPlayTimeId是否存在(可选字段) if (record.preferredPlayTimeId) { const playTimeExists = await tx .select() .from(playTimes) .where(eq(playTimes.id, record.preferredPlayTimeId)) .limit(1) if (playTimeExists.length === 0) { console.warn( `歌曲 ${record.title} 的播放时间ID ${record.preferredPlayTimeId} 不存在,将设为null` ) validPreferredPlayTimeId = null } } // 动态构建歌曲数据,自动跳过不存在的字段 const songData = { requesterId: validRequesterId, // 必需字段 preferredPlayTimeId: validPreferredPlayTimeId // 已验证的字段 } // 基本字段 const songFields = [ 'title', 'artist', 'semester', 'cover', 'musicPlatform', 'musicId', 'submissionNote', 'submissionNotePublic' ] songFields.forEach((field) => { if (record.hasOwnProperty(field)) { songData[field] = record[field] } }) // 布尔字段,提供默认值 songData.played = record.hasOwnProperty('played') ? record.played : false // 日期字段 if (record.hasOwnProperty('playedAt') && record.playedAt) { songData.playedAt = new Date(record.playedAt) } else { songData.playedAt = null } if (record.hasOwnProperty('createdAt') && record.createdAt) { songData.createdAt = new Date(record.createdAt) } else { songData.createdAt = new Date() } if (record.hasOwnProperty('updatedAt') && record.updatedAt) { songData.updatedAt = new Date(record.updatedAt) } else { songData.updatedAt = new Date() } let createdSong if (mode === 'merge') { // 检查是否存在相同的歌曲(按标题和艺术家) const existingSong = await tx .select() .from(songs) .where( and(eq(songs.title, record.title), eq(songs.artist, record.artist)) ) .limit(1) if (existingSong.length > 0) { // 如果存在相同歌曲,更新它 const result = await tx .update(songs) .set(songData) .where(eq(songs.id, existingSong[0].id)) .returning({ id: songs.id }) createdSong = result[0] } else { // 如果不存在,尝试使用原始ID创建 try { // 先检查原始ID是否已被占用 const existingSongWithId = await tx .select() .from(songs) .where(eq(songs.id, record.id)) .limit(1) if (existingSongWithId.length > 0) { // ID已被占用,让数据库自动生成新ID const result = await tx .insert(songs) .values(songData) .returning({ id: songs.id }) createdSong = result[0] } else { // ID未被占用,使用原始ID const result = await tx .insert(songs) .values({ ...songData, id: record.id }) .returning({ id: songs.id }) createdSong = result[0] } } catch (error) { // 如果创建失败(可能是ID冲突),让数据库自动生成ID console.warn( `歌曲 ${record.title} 使用原始ID创建失败,使用自动生成ID: ${error.message}` ) const result = await tx .insert(songs) .values(songData) .returning({ id: songs.id }) createdSong = result[0] } } } else { // 完全恢复模式,检查ID是否已存在 const existingSongWithId = await tx .select() .from(songs) .where(eq(songs.id, record.id)) .limit(1) if (existingSongWithId.length > 0) { // ID已存在,使用upsert策略更新现有歌曲 console.warn( `歌曲ID ${record.id} (${record.title} - ${record.artist}) 已存在,将更新现有歌曲` ) const result = await tx .update(songs) .set(songData) .where(eq(songs.id, record.id)) .returning({ id: songs.id }) createdSong = result[0] } else { // ID不存在,使用原始ID创建 const result = await tx .insert(songs) .values({ ...songData, id: record.id }) .returning({ id: songs.id }) createdSong = result[0] } } // 建立歌曲ID映射 if (record.id && createdSong.id) { songIdMapping.set(record.id, createdSong.id) } break case 'playTimes': // 动态构建播放时段数据,自动跳过不存在的字段 const playTimeData = {} const playTimeFields = ['name', 'startTime', 'endTime', 'description'] playTimeFields.forEach((field) => { if (record.hasOwnProperty(field)) { playTimeData[field] = record[field] } }) // 布尔字段,提供默认值 playTimeData.enabled = record.hasOwnProperty('enabled') ? record.enabled : true if (mode === 'merge') { // 检查是否存在相同名称的播放时段 const existingPlayTime = await tx .select() .from(playTimes) .where(eq(playTimes.name, record.name)) .limit(1) if (existingPlayTime.length > 0) { // 如果存在相同播放时段,更新它 await tx .update(playTimes) .set(playTimeData) .where(eq(playTimes.id, existingPlayTime[0].id)) } else { // 如果不存在,创建新播放时段(不指定ID,让数据库自动生成) await tx.insert(playTimes).values(playTimeData) } } else { // 完全恢复模式,检查ID是否已存在 const existingPlayTimeWithId = await tx .select() .from(playTimes) .where(eq(playTimes.id, record.id)) .limit(1) if (existingPlayTimeWithId.length > 0) { // ID已存在,使用upsert策略更新现有播放时段 console.warn( `播放时段ID ${record.id} (${record.name}) 已存在,将更新现有播放时段` ) await tx .update(playTimes) .set(playTimeData) .where(eq(playTimes.id, record.id)) } else { // ID不存在,使用原始ID创建 await tx.insert(playTimes).values({ ...playTimeData, id: record.id }) } } break case 'semesters': // 动态构建学期数据,自动跳过不存在的字段 const semesterData = {} const semesterFields = ['name'] semesterFields.forEach((field) => { if (record.hasOwnProperty(field)) { semesterData[field] = record[field] } }) // 布尔字段,提供默认值 semesterData.isActive = record.hasOwnProperty('isActive') ? record.isActive : false if (mode === 'merge') { const existingSemester = await tx .select() .from(semesters) .where(eq(semesters.name, semesterData.name)) .limit(1) if (existingSemester.length > 0) { await tx .update(semesters) .set(semesterData) .where(eq(semesters.name, semesterData.name)) } else { await tx.insert(semesters).values(semesterData) } } else { // 完全恢复模式,检查ID是否已存在 const existingSemesterWithId = await tx .select() .from(semesters) .where(eq(semesters.id, record.id)) .limit(1) if (existingSemesterWithId.length > 0) { // ID已存在,使用upsert策略更新现有学期 console.warn( `学期ID ${record.id} (${record.name}) 已存在,将更新现有学期` ) await tx .update(semesters) .set(semesterData) .where(eq(semesters.id, record.id)) } else { // ID不存在,使用原始ID创建 await tx.insert(semesters).values({ ...semesterData, id: record.id }) } } break case 'systemSettings': // 动态构建系统设置数据,自动跳过不存在的字段 const systemSettingsData = {} const systemSettingsFields = [ 'enablePlayTimeSelection', 'siteTitle', 'siteLogoUrl', 'schoolLogoHomeUrl', 'schoolLogoPrintUrl', 'siteDescription', 'submissionGuidelines', 'icpNumber', 'gonganNumber', 'enableSubmissionLimit', 'dailySubmissionLimit', 'weeklySubmissionLimit', 'monthlySubmissionLimit', 'showBlacklistKeywords', 'enableRequestTimeLimitation', 'requestTimeLimitation', 'forceBlockAllRequests', 'enableReplayRequests', 'enableCollaborativeSubmission', 'enableSubmissionRemarks', 'hideStudentInfo', 'smtpEnabled', 'smtpHost', 'smtpPort', 'smtpSecure', 'smtpUsername', 'smtpPassword', 'smtpFromEmail', 'smtpFromName' ] // 只添加备份数据中存在的字段 systemSettingsFields.forEach((field) => { if (record.hasOwnProperty(field)) { systemSettingsData[field] = record[field] } }) if (mode === 'merge') { // 检查是否存在系统设置记录(通常只有一条记录) const existingSystemSettings = await tx .select() .from(systemSettings) .limit(1) if (existingSystemSettings.length > 0) { // 如果存在系统设置,更新它 await tx .update(systemSettings) .set(systemSettingsData) .where(eq(systemSettings.id, existingSystemSettings[0].id)) } else { // 如果不存在,创建新系统设置(不指定ID,让数据库自动生成) await tx.insert(systemSettings).values(systemSettingsData) } } else { // 完全恢复模式,检查ID是否已存在 const existingSystemSettingsWithId = await tx .select() .from(systemSettings) .where(eq(systemSettings.id, record.id)) .limit(1) if (existingSystemSettingsWithId.length > 0) { // ID已存在,使用upsert策略更新现有系统设置 console.warn(`系统设置ID ${record.id} 已存在,将更新现有系统设置`) await tx .update(systemSettings) .set(systemSettingsData) .where(eq(systemSettings.id, record.id)) } else { // ID不存在,使用原始ID创建 await tx.insert(systemSettings).values({ ...systemSettingsData, id: record.id }) } } break case 'schedules': // 验证必需字段 if (!record.songId) { console.warn(`排期记录缺少songId,跳过此记录`) return } if (!record.playDate) { console.warn(`排期记录缺少playDate,跳过此记录`) return } // 使用ID映射查找实际的歌曲ID let validSongId = record.songId const mappedSongId = songIdMapping.get(record.songId) if (mappedSongId) { validSongId = mappedSongId } else { // 尝试直接查找歌曲ID const songExists = await tx .select() .from(songs) .where(eq(songs.id, record.songId)) .limit(1) if (songExists.length === 0) { console.warn(`排期记录的歌曲ID ${record.songId} 不存在,跳过此记录`) return } } // 验证playTimeId是否存在(可选字段) let validPlayTimeId = record.playTimeId if (record.playTimeId) { const playTimeExists = await tx .select() .from(playTimes) .where(eq(playTimes.id, record.playTimeId)) .limit(1) if (playTimeExists.length === 0) { console.warn( `排期记录的播放时间ID ${record.playTimeId} 不存在,将设为null` ) validPlayTimeId = null } } // 动态构建排期数据,自动跳过不存在的字段 const scheduleData = { songId: validSongId, playDate: new Date(record.playDate), playTimeId: validPlayTimeId } // 布尔字段,提供默认值 scheduleData.played = record.hasOwnProperty('played') ? record.played : false // 数字字段,提供默认值 scheduleData.sequence = record.hasOwnProperty('sequence') ? record.sequence : 1 if (mode === 'merge') { // 检查是否存在相同的排期(按歌曲ID和播放日期) const existingSchedule = await tx .select() .from(schedules) .where( and( eq(schedules.songId, validSongId), eq(schedules.playDate, new Date(record.playDate)) ) ) .limit(1) if (existingSchedule.length > 0) { // 如果存在相同排期,更新它 await tx .update(schedules) .set(scheduleData) .where(eq(schedules.id, existingSchedule[0].id)) } else { // 如果不存在,创建新排期(不指定ID,让数据库自动生成) await tx.insert(schedules).values(scheduleData) } } else { // 完全恢复模式,检查ID是否已存在 const existingScheduleWithId = await tx .select() .from(schedules) .where(eq(schedules.id, record.id)) .limit(1) if (existingScheduleWithId.length > 0) { // ID已存在,使用upsert策略更新现有排期 console.warn(`排期ID ${record.id} 已存在,将更新现有排期`) await tx .update(schedules) .set(scheduleData) .where(eq(schedules.id, record.id)) } else { // ID不存在,使用原始ID创建 await tx.insert(schedules).values({ ...scheduleData, id: record.id }) } } break case 'notificationSettings': // 验证userId是否存在 let validUserId = record.userId if (record.userId) { const mappedUserId = userIdMapping.get(record.userId) if (mappedUserId) { validUserId = mappedUserId } else { // 尝试直接查找用户ID const userExists = await tx .select() .from(users) .where(eq(users.id, record.userId)) .limit(1) if (userExists.length === 0) { console.warn(`通知设置的用户ID ${record.userId} 不存在,跳过此记录`) return } } } // 动态构建通知设置数据,自动跳过不存在的字段 const notificationSettingsData = { userId: validUserId } const notificationFields = [ 'refreshInterval', 'songVotedThreshold', 'meowUserId' ] notificationFields.forEach((field) => { if (record.hasOwnProperty(field)) { notificationSettingsData[field] = record[field] } }) // 布尔字段,提供默认值 notificationSettingsData.enabled = record.hasOwnProperty('enabled') ? record.enabled : true notificationSettingsData.songRequestEnabled = record.hasOwnProperty( 'songRequestEnabled' ) ? record.songRequestEnabled : true notificationSettingsData.songVotedEnabled = record.hasOwnProperty( 'songVotedEnabled' ) ? record.songVotedEnabled : true notificationSettingsData.songPlayedEnabled = record.hasOwnProperty( 'songPlayedEnabled' ) ? record.songPlayedEnabled : true // 数字字段,提供默认值 if (!notificationSettingsData.hasOwnProperty('refreshInterval')) { notificationSettingsData.refreshInterval = 60 } if (!notificationSettingsData.hasOwnProperty('songVotedThreshold')) { notificationSettingsData.songVotedThreshold = 1 } // 日期字段 notificationSettingsData.createdAt = record.createdAt ? new Date(record.createdAt) : new Date() notificationSettingsData.updatedAt = record.updatedAt ? new Date(record.updatedAt) : new Date() if (mode === 'merge') { // 使用userId作为唯一标识进行upsert,因为数据库中userId有唯一约束 await tx .insert(notificationSettings) .values(notificationSettingsData) .onConflictDoUpdate({ target: notificationSettings.userId, set: notificationSettingsData }) } else { // 完全恢复模式,检查ID是否已存在 const existingNotificationSettingsWithId = await tx .select() .from(notificationSettings) .where(eq(notificationSettings.id, record.id)) .limit(1) if (existingNotificationSettingsWithId.length > 0) { // ID已存在,使用upsert策略更新现有通知设置 console.warn(`通知设置ID ${record.id} 已存在,将更新现有通知设置`) await tx .update(notificationSettings) .set(notificationSettingsData) .where(eq(notificationSettings.id, record.id)) } else { // ID不存在,使用原始ID创建 await tx.insert(notificationSettings).values({ ...notificationSettingsData, id: record.id }) } } break case 'notifications': // 验证userId是否存在 let validNotificationUserId = record.userId if (record.userId) { const mappedUserId = userIdMapping.get(record.userId) if (mappedUserId) { validNotificationUserId = mappedUserId } else { // 尝试直接查找用户ID const userExists = await tx .select() .from(users) .where(eq(users.id, record.userId)) .limit(1) if (userExists.length === 0) { console.warn(`通知的用户ID ${record.userId} 不存在,跳过此记录`) break } } } // 动态构建通知数据,自动跳过不存在的字段 const notificationData = { userId: validNotificationUserId } const notificationDataFields = ['title', 'message', 'type'] notificationDataFields.forEach((field) => { if (record.hasOwnProperty(field)) { notificationData[field] = record[field] } }) // 布尔字段,提供默认值 notificationData.read = record.hasOwnProperty('read') ? record.read : false // 日期字段 notificationData.createdAt = record.createdAt ? new Date(record.createdAt) : new Date() notificationData.updatedAt = record.updatedAt ? new Date(record.updatedAt) : new Date() if (mode === 'merge') { // 检查是否存在相同的通知(按用户ID、类型和消息) const existingNotification = await tx .select() .from(notifications) .where( and( eq(notifications.userId, validNotificationUserId), eq(notifications.type, record.type), eq(notifications.message, record.message) ) ) .limit(1) if (existingNotification.length > 0) { // 如果存在相同通知,更新它 await tx .update(notifications) .set(notificationData) .where(eq(notifications.id, existingNotification[0].id)) } else { // 如果不存在,创建新通知(不指定ID,让数据库自动生成) await tx.insert(notifications).values(notificationData) } } else { // 完全恢复模式,检查ID是否已存在 const existingNotificationWithId = await tx .select() .from(notifications) .where(eq(notifications.id, record.id)) .limit(1) if (existingNotificationWithId.length > 0) { // ID已存在,使用upsert策略更新现有通知 console.warn(`通知ID ${record.id} 已存在,将更新现有通知`) await tx .update(notifications) .set(notificationData) .where(eq(notifications.id, record.id)) } else { // ID不存在,使用原始ID创建 await tx.insert(notifications).values({ ...notificationData, id: record.id }) } } break case 'songBlacklist': // 验证createdBy字段(可选) let validCreatedBy = record.createdBy if (record.createdBy) { // 使用ID映射查找实际的用户ID const mappedUserId = userIdMapping.get(record.createdBy) if (mappedUserId) { validCreatedBy = mappedUserId } else { // 尝试直接查找用户ID const userExists = await tx .select() .from(users) .where(eq(users.id, record.createdBy)) .limit(1) if (userExists.length === 0) { console.warn( `黑名单记录的创建者ID ${record.createdBy} 不存在,将设为null` ) validCreatedBy = null } } } // 构建黑名单数据 const blacklistData = { type: record.type || 'KEYWORD', // 默认为关键词类型 value: record.value || '', reason: record.reason || null, isActive: record.hasOwnProperty('isActive') ? record.isActive : true, createdBy: validCreatedBy, createdAt: record.createdAt ? new Date(record.createdAt) : new Date(), updatedAt: record.updatedAt ? new Date(record.updatedAt) : new Date() } if (mode === 'merge') { // 检查是否存在相同的黑名单记录(相同类型和值) const existingBlacklist = await tx .select() .from(songBlacklists) .where( and( eq(songBlacklists.type, blacklistData.type), eq(songBlacklists.value, blacklistData.value) ) ) .limit(1) if (existingBlacklist.length > 0) { // 如果存在相同记录,更新它 await tx .update(songBlacklists) .set(blacklistData) .where(eq(songBlacklists.id, existingBlacklist[0].id)) } else { // 如果不存在,创建新记录(不指定ID,让数据库自动生成) await tx.insert(songBlacklists).values(blacklistData) } } else { // 完全恢复模式,检查ID是否已存在 const existingBlacklistWithId = await tx .select() .from(songBlacklists) .where(eq(songBlacklists.id, record.id)) .limit(1) if (existingBlacklistWithId.length > 0) { // ID已存在,使用upsert策略更新现有记录 console.warn(`黑名单ID ${record.id} 已存在,将更新现有记录`) await tx .update(songBlacklists) .set(blacklistData) .where(eq(songBlacklists.id, record.id)) } else { // ID不存在,使用原始ID创建 await tx.insert(songBlacklists).values({ ...blacklistData, id: record.id }) } } break case 'songCollaborators': // 验证外键 let validCollabUserId = record.userId let validCollabSongId = record.songId if (record.userId) { const mappedUserId = userIdMapping.get(record.userId) if (mappedUserId) validCollabUserId = mappedUserId } if (record.songId) { const mappedSongId = songIdMapping.get(record.songId) if (mappedSongId) validCollabSongId = mappedSongId } const collabData = { id: record.id, // UUID songId: validCollabSongId, userId: validCollabUserId, status: record.status || 'PENDING', createdAt: record.createdAt ? new Date(record.createdAt) : new Date(), updatedAt: record.updatedAt ? new Date(record.updatedAt) : new Date() } // UUID 表直接插入,如果冲突则更新 await tx.insert(songCollaborators).values(collabData).onConflictDoUpdate({ target: songCollaborators.id, set: collabData }) break case 'collaborationLogs': // 验证外键 (collaboratorId 是 UUID,通常不变,但如果是重新生成的...) // 这里假设 UUID 在 restore songCollaborators 时保持一致 const logData = { id: record.id, collaboratorId: record.collaboratorId, action: record.action, operatorId: record.operatorId, // 可能需要映射,但 operatorId 是 integer ipAddress: record.ipAddress, createdAt: record.createdAt ? new Date(record.createdAt) : new Date() } // 映射 operatorId if (record.operatorId) { const mappedOpId = userIdMapping.get(record.operatorId) if (mappedOpId) logData.operatorId = mappedOpId } await tx.insert(collaborationLogs).values(logData).onConflictDoUpdate({ target: collaborationLogs.id, set: logData }) break case 'songReplayRequests': // 验证外键 let validReplayUserId = record.userId let validReplaySongId = record.songId if (record.userId) { const mappedUserId = userIdMapping.get(record.userId) if (mappedUserId) validReplayUserId = mappedUserId } if (record.songId) { const mappedSongId = songIdMapping.get(record.songId) if (mappedSongId) validReplaySongId = mappedSongId } const replayData = { songId: validReplaySongId, userId: validReplayUserId, createdAt: record.createdAt ? new Date(record.createdAt) : new Date(), updatedAt: record.updatedAt ? new Date(record.updatedAt) : new Date() } if (mode === 'merge') { await tx .insert(songReplayRequests) .values(replayData) .onConflictDoNothing() } else { // 完全恢复模式 if (record.id) { const existing = await tx .select() .from(songReplayRequests) .where(eq(songReplayRequests.id, record.id)) .limit(1) if (existing.length > 0) { await tx .update(songReplayRequests) .set(replayData) .where(eq(songReplayRequests.id, record.id)) } else { await tx.insert(songReplayRequests).values({ ...replayData, id: record.id }) } } else { await tx.insert(songReplayRequests).values(replayData) } } break case 'votes': // 验证外键约束 let validVoteUserId = record.userId let validVoteSongId = record.songId // 使用ID映射查找实际的用户ID if (record.userId) { const mappedUserId = userIdMapping.get(record.userId) if (mappedUserId) { validVoteUserId = mappedUserId } else { // 尝试直接查找用户ID const userExists = await tx .select() .from(users) .where(eq(users.id, record.userId)) .limit(1) if (userExists.length === 0) { console.warn(`投票记录的用户ID ${record.userId} 不存在,跳过此记录`) return // 跳过此记录,因为userId是必需的 } } } else { console.warn(`投票记录缺少userId,跳过此记录`) return // 跳过此记录,因为userId是必需的 } // 使用ID映射查找实际的歌曲ID if (record.songId) { const mappedSongId = songIdMapping.get(record.songId) if (mappedSongId) { validVoteSongId = mappedSongId } else { // 尝试直接查找歌曲ID const songExists = await tx .select() .from(songs) .where(eq(songs.id, record.songId)) .limit(1) if (songExists.length === 0) { console.warn(`投票记录的歌曲ID ${record.songId} 不存在,跳过此记录`) return // 跳过此记录,因为songId是必需的 } } } else { console.warn(`投票记录缺少songId,跳过此记录`) return // 跳过此记录,因为songId是必需的 } // 构建投票数据 const voteData = { userId: validVoteUserId, songId: validVoteSongId, createdAt: record.createdAt ? new Date(record.createdAt) : new Date() } if (mode === 'merge') { // 检查是否存在相同的投票(同一用户对同一歌曲的投票) const existingVote = await tx .select() .from(votes) .where( and( eq(votes.userId, validVoteUserId), eq(votes.songId, validVoteSongId) ) ) .limit(1) if (existingVote.length > 0) { // 如果存在相同投票,更新它 await tx .update(votes) .set(voteData) .where(eq(votes.id, existingVote[0].id)) } else { // 如果不存在,创建新投票(不指定ID,让数据库自动生成) await tx.insert(votes).values(voteData) } } else { // 完全恢复模式,检查是否存在相同的投票(同一用户对同一歌曲的投票) const existingVote = await tx .select() .from(votes) .where( and( eq(votes.userId, validVoteUserId), eq(votes.songId, validVoteSongId) ) ) .limit(1) if (existingVote.length > 0) { // 如果存在相同投票,更新它 await tx .update(votes) .set(voteData) .where(eq(votes.id, existingVote[0].id)) } else { // 如果不存在,创建新投票(不指定ID,让数据库自动生成) await tx.insert(votes).values(voteData) } } break case 'requestTimes': // requestTimes表没有外键依赖 const requestTimeData = { name: record.name, startTime: record.startTime ? new Date(record.startTime) : null, endTime: record.endTime ? new Date(record.endTime) : null, enabled: record.enabled !== undefined ? record.enabled : true, description: record.description || null, expected: record.expected || 0, accepted: record.accepted || 0, past: record.past !== undefined ? record.past : false, createdAt: record.createdAt ? new Date(record.createdAt) : new Date(), updatedAt: record.updatedAt ? new Date(record.updatedAt) : new Date() } if (mode === 'merge') { // 检查是否存在相同名称的请求时段 const existingRequestTime = await tx .select() .from(requestTimes) .where(eq(requestTimes.name, record.name)) .limit(1) if (existingRequestTime.length > 0) { // 如果存在,更新它 await tx .update(requestTimes) .set(requestTimeData) .where(eq(requestTimes.id, existingRequestTime[0].id)) } else { // 如果不存在,创建新记录 await tx.insert(requestTimes).values(requestTimeData) } } else { // 完全恢复模式,检查ID是否已存在 if (record.id) { const existingRequestTimeWithId = await tx .select() .from(requestTimes) .where(eq(requestTimes.id, record.id)) .limit(1) if (existingRequestTimeWithId.length > 0) { // ID已存在,更新现有记录 await tx .update(requestTimes) .set(requestTimeData) .where(eq(requestTimes.id, record.id)) } else { // ID不存在,使用原始ID创建 await tx.insert(requestTimes).values({ ...requestTimeData, id: record.id }) } } else { // 没有ID,创建新记录 await tx.insert(requestTimes).values(requestTimeData) } } break case 'emailTemplates': // 验证外键约束 - updatedByUserId let validEmailTemplateUpdatedByUserId = record.updatedByUserId if (record.updatedByUserId) { const mappedUserId = userIdMapping.get(record.updatedByUserId) if (mappedUserId) { validEmailTemplateUpdatedByUserId = mappedUserId } else { // 尝试直接查找用户ID const userExists = await tx .select() .from(users) .where(eq(users.id, record.updatedByUserId)) .limit(1) if (userExists.length === 0) { console.warn( `邮件模板的更新者ID ${record.updatedByUserId} 不存在,将设置为null` ) validEmailTemplateUpdatedByUserId = null } } } // 构建邮件模板数据 const emailTemplateData = { key: record.key, name: record.name, subject: record.subject, html: record.html, updatedByUserId: validEmailTemplateUpdatedByUserId, createdAt: record.createdAt ? new Date(record.createdAt) : new Date(), updatedAt: record.updatedAt ? new Date(record.updatedAt) : new Date() } if (mode === 'merge') { // 检查是否存在相同key的邮件模板 const existingEmailTemplate = await tx .select() .from(emailTemplates) .where(eq(emailTemplates.key, record.key)) .limit(1) if (existingEmailTemplate.length > 0) { // 如果存在,更新它 await tx .update(emailTemplates) .set(emailTemplateData) .where(eq(emailTemplates.id, existingEmailTemplate[0].id)) } else { // 如果不存在,创建新记录 await tx.insert(emailTemplates).values(emailTemplateData) } } else { // 完全恢复模式,检查ID是否已存在 if (record.id) { const existingEmailTemplateWithId = await tx .select() .from(emailTemplates) .where(eq(emailTemplates.id, record.id)) .limit(1) if (existingEmailTemplateWithId.length > 0) { // ID已存在,更新现有记录 await tx .update(emailTemplates) .set(emailTemplateData) .where(eq(emailTemplates.id, record.id)) } else { // ID不存在,使用原始ID创建 await tx.insert(emailTemplates).values({ ...emailTemplateData, id: record.id }) } } else { // 没有ID,创建新记录 await tx.insert(emailTemplates).values(emailTemplateData) } } break case 'apiKeys': // 验证外键约束 - createdByUserId let validApiKeyCreatedByUserId = record.createdByUserId if (record.createdByUserId) { const mappedUserId = userIdMapping.get(record.createdByUserId) if (mappedUserId) { validApiKeyCreatedByUserId = mappedUserId } else { // 尝试直接查找用户ID const userExists = await tx .select() .from(users) .where(eq(users.id, record.createdByUserId)) .limit(1) if (userExists.length === 0) { console.warn( `API密钥的创建者ID ${record.createdByUserId} 不存在,跳过此记录` ) return // 跳过此记录,因为createdByUserId是必需的 } } } else { console.warn(`API密钥缺少createdByUserId,跳过此记录`) return // 跳过此记录,因为createdByUserId是必需的 } // 构建API密钥数据 const apiKeyData = { name: record.name, description: record.description || null, keyHash: record.keyHash, keyPrefix: record.keyPrefix, isActive: record.isActive !== undefined ? record.isActive : true, expiresAt: record.expiresAt ? new Date(record.expiresAt) : null, createdAt: record.createdAt ? new Date(record.createdAt) : new Date(), updatedAt: record.updatedAt ? new Date(record.updatedAt) : new Date(), lastUsedAt: record.lastUsedAt ? new Date(record.lastUsedAt) : null, createdByUserId: validApiKeyCreatedByUserId, usageCount: record.usageCount || 0 } if (mode === 'merge') { // 检查是否存在相同keyHash的API密钥 const existingApiKey = await tx .select() .from(apiKeys) .where(eq(apiKeys.keyHash, record.keyHash)) .limit(1) if (existingApiKey.length > 0) { // 如果存在,更新它 await tx .update(apiKeys) .set(apiKeyData) .where(eq(apiKeys.id, existingApiKey[0].id)) } else { // 如果不存在,创建新记录 await tx.insert(apiKeys).values(apiKeyData) } } else { // 完全恢复模式,检查ID是否已存在 if (record.id) { const existingApiKeyWithId = await tx .select() .from(apiKeys) .where(eq(apiKeys.id, record.id)) .limit(1) if (existingApiKeyWithId.length > 0) { // ID已存在,更新现有记录 await tx .update(apiKeys) .set(apiKeyData) .where(eq(apiKeys.id, record.id)) } else { // ID不存在,使用原始ID创建 await tx.insert(apiKeys).values({ ...apiKeyData, id: record.id }) } } else { // 没有ID,创建新记录 await tx.insert(apiKeys).values(apiKeyData) } } break case 'apiKeyPermissions': // 验证外键约束 - apiKeyId const validApiKeyPermissionApiKeyId = record.apiKeyId if (record.apiKeyId) { // 尝试直接查找API密钥ID const apiKeyExists = await tx .select() .from(apiKeys) .where(eq(apiKeys.id, record.apiKeyId)) .limit(1) if (apiKeyExists.length === 0) { console.warn( `API密钥权限的API密钥ID ${record.apiKeyId} 不存在,跳过此记录` ) return // 跳过此记录,因为apiKeyId是必需的 } } else { console.warn(`API密钥权限缺少apiKeyId,跳过此记录`) return // 跳过此记录,因为apiKeyId是必需的 } // 构建API密钥权限数据 const apiKeyPermissionData = { apiKeyId: validApiKeyPermissionApiKeyId, permission: record.permission, createdAt: record.createdAt ? new Date(record.createdAt) : new Date() } if (mode === 'merge') { // 检查是否存在相同的API密钥权限 const existingApiKeyPermission = await tx .select() .from(apiKeyPermissions) .where( and( eq(apiKeyPermissions.apiKeyId, validApiKeyPermissionApiKeyId), eq(apiKeyPermissions.permission, record.permission) ) ) .limit(1) if (existingApiKeyPermission.length > 0) { // 如果存在,更新它 await tx .update(apiKeyPermissions) .set(apiKeyPermissionData) .where(eq(apiKeyPermissions.id, existingApiKeyPermission[0].id)) } else { // 如果不存在,创建新记录 await tx.insert(apiKeyPermissions).values(apiKeyPermissionData) } } else { // 完全恢复模式,检查ID是否已存在 if (record.id) { const existingApiKeyPermissionWithId = await tx .select() .from(apiKeyPermissions) .where(eq(apiKeyPermissions.id, record.id)) .limit(1) if (existingApiKeyPermissionWithId.length > 0) { // ID已存在,更新现有记录 await tx .update(apiKeyPermissions) .set(apiKeyPermissionData) .where(eq(apiKeyPermissions.id, record.id)) } else { // ID不存在,使用原始ID创建 await tx.insert(apiKeyPermissions).values({ ...apiKeyPermissionData, id: record.id }) } } else { // 没有ID,创建新记录 await tx.insert(apiKeyPermissions).values(apiKeyPermissionData) } } break case 'apiLogs': // 验证外键约束 - apiKeyId(可选) let validApiLogApiKeyId = record.apiKeyId if (record.apiKeyId) { // 尝试直接查找API密钥ID const apiKeyExists = await tx .select() .from(apiKeys) .where(eq(apiKeys.id, record.apiKeyId)) .limit(1) if (apiKeyExists.length === 0) { console.warn( `API日志的API密钥ID ${record.apiKeyId} 不存在,将设置为null` ) validApiLogApiKeyId = null } } // 构建API日志数据 const apiLogData = { apiKeyId: validApiLogApiKeyId, endpoint: record.endpoint, method: record.method, ipAddress: record.ipAddress, userAgent: record.userAgent || null, statusCode: record.statusCode, responseTimeMs: record.responseTimeMs, requestBody: record.requestBody || null, responseBody: record.responseBody || null, createdAt: record.createdAt ? new Date(record.createdAt) : new Date(), errorMessage: record.errorMessage || null } // API日志通常不需要检查重复,直接创建新记录 await tx.insert(apiLogs).values(apiLogData) break // 其他表的处理逻辑... default: console.warn(`暂不支持恢复表: ${tableName}`) return // 跳出当前事务,不处理此记录 } }, { timeout: 15000, // 每个记录事务15秒超时 maxWait: 3000 // 最大等待时间3秒 } ) // 成功处理,跳出重试循环 restoredCount++ break } catch (recordError) { lastError = recordError retryCount++ // 检查是否是可重试的错误 // Postgres 错误代码: // 23505: unique_violation (唯一约束冲突) // 23503: foreign_key_violation (外键约束冲突) // 40001: serialization_failure (序列化失败) // 40P01: deadlock_detected (死锁检测) const isRetryableError = recordError.code === '23505' || // 唯一约束冲突 recordError.code === '23503' || // 外键约束冲突 recordError.code === '40001' || // 序列化失败 recordError.code === '40P01' || // 死锁 recordError.message.includes('timeout') || recordError.message.includes('connection') || recordError.message.includes('SQLITE_BUSY') || recordError.message.includes('database is locked') if (retryCount <= maxRetries && isRetryableError) { console.warn( `恢复记录失败,第 ${retryCount}/${maxRetries} 次重试 (${tableName}):`, recordError.message ) // 根据错误类型调整重试策略 if (recordError.code === '23505') { // 唯一约束冲突,等待更长时间 await new Promise((resolve) => setTimeout(resolve, 1000 * retryCount)) } else { // 其他错误,短暂等待 await new Promise((resolve) => setTimeout(resolve, 500 * retryCount)) } } else { // 不可重试的错误或重试次数用完 break } } } // 如果所有重试都失败了,记录错误 if (retryCount > maxRetries && lastError) { console.error(`恢复记录最终失败 (${tableName}):`, lastError) restoreResults.details.errors.push( `${tableName}: ${lastError.message} (重试${maxRetries}次后失败)` ) consecutiveErrors++ // 如果连续错误过多,减少批处理大小 if (consecutiveErrors >= 3 && batchSize > 1) { batchSize = Math.max(1, Math.floor(batchSize / 2)) consecutiveErrors = 0 console.warn(`由于连续错误,将批处理大小调整为: ${batchSize}`) } } else if (retryCount <= maxRetries) { // 成功处理,重置连续错误计数 consecutiveErrors = 0 } } // 每批处理完后输出进度 console.log( `${tableName}: 已处理 ${Math.min(i + batchSize, tableData.length)}/${tableData.length} 条记录` ) } console.log(`✅ ${tableName}: 恢复了 ${restoredCount}/${tableData.length} 条记录`) restoreResults.details.recordsRestored += restoredCount restoreResults.details.tablesProcessed++ } catch (tableError) { console.error(`恢复表 ${tableName} 失败:`, tableError) restoreResults.details.errors.push(`表 ${tableName}: ${tableError.message}`) } } console.log(`✅ 数据恢复完成`) console.log(`📊 处理了 ${restoreResults.details.tablesProcessed} 个表`) console.log(`📊 恢复了 ${restoreResults.details.recordsRestored} 条记录`) // 重置所有自增序列 console.log(`🔄 开始重置自增序列...`) const sequenceResetResults = [] const tablesToReset = [ 'Song', 'User', 'UserIdentity', 'UserStatusLog', 'Vote', 'Schedule', 'Notification', 'NotificationSettings', 'PlayTime', 'Semester', 'SystemSettings', 'SongBlacklist', 'SongReplayRequest', 'RequestTime', 'EmailTemplate' ] for (const tableName of tablesToReset) { try { // 调用 fix-sequence 端点 const response = await $fetch('/api/admin/fix-sequence', { method: 'POST', body: { table: tableName }, headers: { 'Content-Type': 'application/json', Authorization: getRequestHeader(event, 'authorization') || '' } }) if (response.success) { sequenceResetResults.push(`${tableName}: 序列已重置`) console.log(`✅ ${tableName} 序列重置成功`) } else { const errorMsg = `${tableName}: 序列重置失败 - ${response.error}` sequenceResetResults.push(errorMsg) console.warn(`⚠️ ${errorMsg}`) } } catch (sequenceError) { const errorMsg = `${tableName}: 序列重置失败 - ${sequenceError.message}` sequenceResetResults.push(errorMsg) console.warn(`⚠️ ${errorMsg}`) } } // 将序列重置结果添加到详情中 restoreResults.details.sequenceResets = sequenceResetResults console.log(`✅ 序列重置完成`) if (restoreResults.details.errors.length > 0) { console.warn(`⚠️ 发生了 ${restoreResults.details.errors.length} 个错误`) restoreResults.success = false restoreResults.message = '数据恢复完成,但存在错误' } // 清除所有缓存 try { const cacheService = CacheService.getInstance() await cacheService.clearAllCaches() console.log('数据恢复后缓存已清除') } catch (cacheError) { console.warn('清除缓存失败:', cacheError) restoreResults.details.warnings = restoreResults.details.warnings || [] restoreResults.details.warnings.push('清除缓存失败') } try { const smtpService = SmtpService.getInstance() const initialized = await smtpService.initializeSmtpConfig() if (!initialized) { restoreResults.details.warnings = restoreResults.details.warnings || [] restoreResults.details.warnings.push('SMTP未启用或配置不完整,SMTP实例已重置') } console.log('数据恢复后SMTP配置已重载') } catch (smtpError) { console.warn('重载SMTP配置失败:', smtpError) restoreResults.details.warnings = restoreResults.details.warnings || [] restoreResults.details.warnings.push('重载SMTP配置失败') } return restoreResults } catch (error) { console.error('恢复数据库备份失败:', error) throw createError({ statusCode: error.statusCode || 500, statusMessage: error.statusMessage || '恢复数据库备份失败' }) } })