Files
BYW-VoiceHub/VoiceHub-main/server/api/admin/backup/export.post.ts
T
Henry_Du 4ce4649aaf
Build VoiceHub FPK / build-fpk (push) Failing after 56m5s
Build and Push Docker Image / build-and-push (push) Canceled after 1h9m35s
初次提交
2026-09-06 08:19:02 +08:00

540 lines
17 KiB
TypeScript

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 || '创建数据库备份失败'
})
}
})