初次提交
Build VoiceHub FPK / build-fpk (push) Failing after 56m5s
Build and Push Docker Image / build-and-push (push) Canceled after 1h9m35s

This commit is contained in:
2026-09-06 08:19:02 +08:00
commit 4ce4649aaf
1061 changed files with 337258 additions and 0 deletions
+175
View File
@@ -0,0 +1,175 @@
import { createError, defineEventHandler, readBody } from 'h3'
import { db } from '~/drizzle/db'
import {
apiKeyPermissions,
apiKeys,
apiLogs,
collaborationLogs,
emailTemplates,
notifications,
notificationSettings,
playTimes,
requestTimes,
schedules,
semesters,
songBlacklists,
songCollaborators,
songReplayRequests,
songs,
systemSettings,
userIdentities,
users,
userStatusLogs,
votes
} from '~/drizzle/schema'
import { eq, inArray, isNull, notInArray, or } from 'drizzle-orm'
export default defineEventHandler(async (event) => {
// 验证管理员权限
const user = event.context.user
if (!user || !['ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
throw createError({
statusCode: 403,
statusMessage: '权限不足'
})
}
// 只有超级管理员可以清空数据库
if (user.role !== 'SUPER_ADMIN') {
throw createError({
statusCode: 403,
statusMessage: '只有超级管理员可以清空数据库'
})
}
try {
const body = await readBody(event)
const finalizeTempUser = body?.finalizeTempUser === true
const overwriteSuperAdmin = body?.overwriteSuperAdmin === true
const hasSuperAdminInBackup = body?.hasSuperAdminInBackup === true
const shouldOverwriteSuperAdmin = overwriteSuperAdmin && hasSuperAdminInBackup
if (finalizeTempUser) {
const currentUserId = Number(user.id)
const currentUserApiKeys = await db
.select({ id: apiKeys.id })
.from(apiKeys)
.where(eq(apiKeys.createdByUserId, currentUserId))
const currentUserApiKeyIds = currentUserApiKeys.map((item) => item.id)
if (currentUserApiKeyIds.length > 0) {
await db.delete(apiLogs).where(inArray(apiLogs.apiKeyId, currentUserApiKeyIds))
await db
.delete(apiKeyPermissions)
.where(inArray(apiKeyPermissions.apiKeyId, currentUserApiKeyIds))
}
await db.delete(apiKeys).where(eq(apiKeys.createdByUserId, currentUserId))
await db.delete(notifications).where(eq(notifications.userId, currentUserId))
await db.delete(notificationSettings).where(eq(notificationSettings.userId, currentUserId))
await db.delete(userStatusLogs).where(eq(userStatusLogs.userId, currentUserId))
await db.delete(userIdentities).where(eq(userIdentities.userId, currentUserId))
await db.delete(users).where(eq(users.id, currentUserId))
return {
success: true,
message: '临时管理员账户已清理',
finalized: true
}
}
let preservedSuperAdminIds: number[] = []
let temporaryPreservedUserId: number | null = null
if (!shouldOverwriteSuperAdmin) {
const preservedUsers = await db
.select({ id: users.id })
.from(users)
.where(or(eq(users.role, 'SUPER_ADMIN'), eq(users.id, 1)))
preservedSuperAdminIds = preservedUsers.map((item) => item.id)
} else {
temporaryPreservedUserId = Number(user.id)
}
console.log('清空现有数据...')
if (shouldOverwriteSuperAdmin || preservedSuperAdminIds.length === 0) {
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(userStatusLogs)
await db.delete(emailTemplates)
await db.delete(songBlacklists)
await db.delete(userIdentities)
await db.delete(songs)
await db.delete(playTimes)
await db.delete(semesters)
await db.delete(requestTimes)
await db.delete(systemSettings)
if (temporaryPreservedUserId) {
await db.delete(users).where(notInArray(users.id, [temporaryPreservedUserId]))
} else {
await db.delete(users)
}
} else {
const preservedApiKeys = await db
.select({ id: apiKeys.id })
.from(apiKeys)
.where(inArray(apiKeys.createdByUserId, preservedSuperAdminIds))
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)
}
await db.delete(apiKeys).where(notInArray(apiKeys.createdByUserId, preservedSuperAdminIds))
await db.delete(notifications).where(notInArray(notifications.userId, preservedSuperAdminIds))
await db
.delete(notificationSettings)
.where(notInArray(notificationSettings.userId, preservedSuperAdminIds))
await db.delete(collaborationLogs)
await db.delete(songCollaborators)
await db.delete(songReplayRequests)
await db.delete(schedules)
await db.delete(votes)
await db.delete(userStatusLogs).where(notInArray(userStatusLogs.userId, preservedSuperAdminIds))
await db.delete(emailTemplates)
await db.delete(songBlacklists)
await db.delete(userIdentities).where(notInArray(userIdentities.userId, preservedSuperAdminIds))
await db.delete(songs)
await db.delete(playTimes)
await db.delete(semesters)
await db.delete(requestTimes)
await db.delete(systemSettings)
await db.delete(users).where(notInArray(users.id, preservedSuperAdminIds))
}
console.log('✅ 现有数据已清空')
return {
success: true,
message: '数据已清空',
shouldOverwriteSuperAdmin,
preservedSuperAdminIds,
temporaryPreservedUserId
}
} catch (error) {
console.error('清空数据失败:', error)
throw createError({
statusCode: 500,
statusMessage: '清空数据失败: ' + error.message
})
}
})
@@ -0,0 +1,71 @@
import { createError, defineEventHandler, getRouterParam } from 'h3'
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 filename = getRouterParam(event, 'filename')
if (!filename) {
throw createError({
statusCode: 400,
statusMessage: '请指定文件名'
})
}
// 验证文件名安全性
if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
throw createError({
statusCode: 400,
statusMessage: '无效的文件名'
})
}
// 验证文件扩展名
if (!filename.endsWith('.json')) {
throw createError({
statusCode: 400,
statusMessage: '只能删除JSON格式的备份文件'
})
}
const backupDir = path.join(process.cwd(), 'backups')
const filepath = path.join(backupDir, filename)
// 检查文件是否存在
try {
await fs.access(filepath)
} catch {
throw createError({
statusCode: 404,
statusMessage: '备份文件不存在'
})
}
// 删除文件
await fs.unlink(filepath)
console.log(`管理员 ${user.username} 删除了备份文件: ${filename}`)
return {
success: true,
message: '备份文件删除成功',
filename
}
} catch (error) {
console.error('删除备份文件失败:', error)
throw createError({
statusCode: error.statusCode || 500,
statusMessage: error.statusMessage || '删除备份文件失败'
})
}
})
+84
View File
@@ -0,0 +1,84 @@
import { createError, defineEventHandler, getQuery, setHeader } from 'h3'
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 query = getQuery(event)
const { filename } = query
if (!filename || typeof filename !== 'string') {
throw createError({
statusCode: 400,
statusMessage: '缺少文件名参数'
})
}
// 安全检查:确保文件名不包含路径遍历字符
if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
throw createError({
statusCode: 400,
statusMessage: '无效的文件名'
})
}
// 检查文件是否为备份文件
if (
!filename.endsWith('.json') ||
(!filename.startsWith('database-backup-') &&
!filename.startsWith('users-backup-') &&
!filename.startsWith('uploaded-'))
) {
throw createError({
statusCode: 400,
statusMessage: '无效的备份文件'
})
}
const backupDir = path.join(process.cwd(), 'backups')
const filepath = path.join(backupDir, filename)
try {
// 检查文件是否存在
await fs.access(filepath)
// 读取文件内容
const fileContent = await fs.readFile(filepath, 'utf8')
const fileStats = await fs.stat(filepath)
// 设置响应头
setHeader(event, 'Content-Type', 'application/json')
setHeader(event, 'Content-Disposition', `attachment; filename="${filename}"`)
setHeader(event, 'Content-Length', fileStats.size.toString())
setHeader(event, 'Cache-Control', 'no-cache')
return fileContent
} catch (error) {
if (error.code === 'ENOENT') {
throw createError({
statusCode: 404,
statusMessage: '备份文件不存在'
})
}
throw createError({
statusCode: 500,
statusMessage: '读取备份文件失败'
})
}
} catch (error) {
console.error('下载备份文件失败:', error)
throw createError({
statusCode: error.statusCode || 500,
statusMessage: error.statusMessage || '下载备份文件失败'
})
}
})
@@ -0,0 +1,76 @@
import { createError, defineEventHandler, getRouterParam, setHeader } from 'h3'
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 filename = getRouterParam(event, 'filename')
if (!filename) {
throw createError({
statusCode: 400,
statusMessage: '请指定文件名'
})
}
// 验证文件名安全性
if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
throw createError({
statusCode: 400,
statusMessage: '无效的文件名'
})
}
// 验证文件扩展名
if (!filename.endsWith('.json')) {
throw createError({
statusCode: 400,
statusMessage: '只能下载JSON格式的备份文件'
})
}
const backupDir = path.join(process.cwd(), 'backups')
const filepath = path.join(backupDir, filename)
// 检查文件是否存在
try {
await fs.access(filepath)
} catch {
throw createError({
statusCode: 404,
statusMessage: '备份文件不存在'
})
}
// 获取文件信息
const stats = await fs.stat(filepath)
// 设置响应头
setHeader(event, 'Content-Type', 'application/json')
setHeader(event, 'Content-Disposition', `attachment; filename="${filename}"`)
setHeader(event, 'Content-Length', stats.size.toString())
setHeader(event, 'Cache-Control', 'no-cache')
// 读取并返回文件内容
const fileContent = await fs.readFile(filepath)
console.log(`管理员 ${user.username} 下载了备份文件: ${filename}`)
return fileContent
} catch (error) {
console.error('下载备份文件失败:', error)
throw createError({
statusCode: error.statusCode || 500,
statusMessage: error.statusMessage || '下载备份文件失败'
})
}
})
+539
View File
@@ -0,0 +1,539 @@
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 || '创建数据库备份失败'
})
}
})
+155
View File
@@ -0,0 +1,155 @@
import { createError, defineEventHandler } from 'h3'
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 isVercel = process.env.VERCEL || process.env.VERCEL_ENV
const isNetlify = process.env.NETLIFY
const isServerless = isVercel || isNetlify
if (isServerless) {
// 在无服务器环境中,无法持久化存储文件,返回空列表
console.log('🌐 检测到无服务器环境,备份文件无法持久化存储')
return {
success: true,
backups: [],
total: 0,
message: '无服务器环境中备份文件无法持久化存储,请使用直接下载功能'
}
}
const backupDir = path.join(process.cwd(), 'backups')
// 检查备份目录是否存在
try {
await fs.access(backupDir)
} catch {
// 如果目录不存在,尝试创建它
try {
await fs.mkdir(backupDir, { recursive: true })
} catch (mkdirError) {
console.warn('无法创建备份目录,可能在只读文件系统中:', mkdirError.message)
return {
success: true,
backups: [],
total: 0,
message: '文件系统只读,备份文件无法持久化存储'
}
}
return {
success: true,
backups: [],
total: 0
}
}
// 读取备份目录中的文件
const files = await fs.readdir(backupDir)
const backupFiles = files.filter(
(file) =>
file.endsWith('.json') &&
(file.startsWith('database-backup-') ||
file.startsWith('users-backup-') ||
file.startsWith('users-system-backup-') ||
file.startsWith('songs-backup-') ||
file.startsWith('songs-system-backup-') ||
file.startsWith('system-settings-backup-') ||
file.startsWith('uploaded-'))
)
const backups = []
for (const filename of backupFiles) {
try {
const filepath = path.join(backupDir, filename)
const stats = await fs.stat(filepath)
// 尝试读取备份文件的元数据
let metadata = null
try {
const fileContent = await fs.readFile(filepath, 'utf8')
const backupData = JSON.parse(fileContent)
if (backupData.metadata) {
metadata = backupData.metadata
} else if (backupData.users) {
// 兼容旧格式的用户备份文件
metadata = {
version: '0.1',
timestamp: backupData.timestamp || stats.mtime.toISOString(),
creator: 'system',
description: '用户数据备份(旧格式)',
tables: [
{ name: 'users', recordCount: backupData.totalUsers || backupData.users.length }
],
totalRecords: backupData.totalUsers || backupData.users.length
}
}
} catch (parseError) {
console.warn(`无法解析备份文件 ${filename}:`, parseError.message)
}
// 判断备份类型
let type = 'users' // 默认为用户备份
if (filename.startsWith('database-backup-')) {
type = 'full'
} else if (
filename.startsWith('users-backup-') ||
filename.startsWith('users-system-backup-')
) {
type = 'users'
} else if (filename.startsWith('songs-backup-') || filename.startsWith('songs-system-backup-')) {
type = 'songs'
} else if (filename.startsWith('system-settings-backup-')) {
type = 'system'
} else if (filename.startsWith('uploaded-')) {
// 对于上传的文件,根据元数据判断类型
if (metadata) {
if (metadata.backupType) {
type = metadata.backupType
} else if (metadata.tables && metadata.tables.length > 1) {
type = 'full'
}
}
}
backups.push({
filename,
size: stats.size,
createdAt: stats.mtime,
metadata,
type,
isValid: metadata !== null
})
} catch (error) {
console.error(`处理备份文件 ${filename} 失败:`, error)
}
}
// 按创建时间倒序排列
backups.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
return {
success: true,
backups,
total: backups.length
}
} catch (error) {
console.error('获取备份文件列表失败:', error)
throw createError({
statusCode: 500,
statusMessage: '获取备份文件列表失败'
})
}
})
@@ -0,0 +1,967 @@
import { createError, defineEventHandler, readBody } from 'h3'
import { db } from '~/drizzle/db'
import {
notificationSettings,
notifications,
playTimes,
schedules,
semesters,
songBlacklists,
songs,
systemSettings,
userIdentities,
users,
userStatusLogs,
votes
} from '~/drizzle/schema'
import { and, eq } from 'drizzle-orm'
export default defineEventHandler(async (event) => {
// 验证管理员权限
const user = event.context.user
if (!user || !['ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
throw createError({
statusCode: 403,
statusMessage: '权限不足'
})
}
const body = await readBody(event)
const {
tableName,
records,
mappings,
mode = 'merge',
overwriteSuperAdmin = false,
hasSuperAdminInBackup = false
} = body
if (overwriteSuperAdmin && user.role !== 'SUPER_ADMIN') {
throw createError({
statusCode: 403,
statusMessage: '仅超级管理员可以覆盖超级管理员账号数据'
})
}
if (!tableName || !records || !Array.isArray(records)) {
throw createError({
statusCode: 400,
statusMessage: '无效的请求参数'
})
}
// 转换映射对象为Map
const userIdMapping = new Map(
Object.entries(mappings?.users || {}).map(([k, v]) => [Number(k), Number(v)])
)
const songIdMapping = new Map(
Object.entries(mappings?.songs || {}).map(([k, v]) => [Number(k), Number(v)])
)
const preservedSuperAdminIds = new Set(
(mappings?.meta?.preservedSuperAdminIds || []).map((id) => Number(id))
)
const temporaryPreservedUserId = mappings?.meta?.temporaryPreservedUserId
? Number(mappings.meta.temporaryPreservedUserId)
: null
const shouldOverwriteSuperAdmin = overwriteSuperAdmin && hasSuperAdminInBackup
const newMappings = {
users: {},
songs: {}
}
const stats = {
processed: 0,
created: 0,
updated: 0,
errors: 0,
warnings: []
}
console.log(`处理批次: ${tableName}, ${records.length} 条记录`)
for (const record of records) {
try {
await db.transaction(async (tx) => {
switch (tableName) {
case 'users': {
const buildUserData = (includePassword = false) => {
const userData: any = {}
const userFields = [
'username',
'name',
'grade',
'class',
'role',
'email',
'emailVerified',
'lastLoginIp',
'meowNickname',
'forcePasswordChange',
'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') {
const existingUser = await tx.query.users.findFirst({
where: eq(users.username, record.username)
})
if (existingUser) {
createdUser = (
await tx
.update(users)
.set(buildUserData(false))
.where(eq(users.username, record.username))
.returning()
)[0]
stats.updated++
} else {
try {
const existingUserWithId = await tx.query.users.findFirst({
where: eq(users.id, record.id)
})
if (existingUserWithId) {
createdUser = (
await tx.insert(users).values(buildUserData(true)).returning()
)[0]
} else {
createdUser = (
await tx
.insert(users)
.values({
...buildUserData(true),
id: record.id
})
.returning()
)[0]
}
} catch (error) {
console.warn(`用户 ${record.username} 使用原始ID创建失败,使用自动生成ID`)
createdUser = (await tx.insert(users).values(buildUserData(true)).returning())[0]
}
stats.created++
}
} else {
if (
shouldOverwriteSuperAdmin &&
temporaryPreservedUserId &&
Number(record.id) === temporaryPreservedUserId
) {
createdUser = (await tx.insert(users).values(buildUserData(true)).returning())[0]
stats.created++
if (record.id && createdUser?.id) {
newMappings.users[record.id] = createdUser.id
}
break
}
const isBackupSuperAdminRecord =
record.role === 'SUPER_ADMIN' || preservedSuperAdminIds.has(Number(record.id))
if (!shouldOverwriteSuperAdmin && isBackupSuperAdminRecord) {
if (record.id) {
const existingUserWithId = await tx.query.users.findFirst({
where: eq(users.id, record.id)
})
if (existingUserWithId) {
newMappings.users[record.id] = existingUserWithId.id
}
}
stats.updated++
break
}
const existingUserWithId = await tx.query.users.findFirst({
where: eq(users.id, record.id)
})
if (existingUserWithId) {
createdUser = (
await tx
.update(users)
.set(buildUserData(true))
.where(eq(users.id, record.id))
.returning()
)[0]
stats.updated++
} else {
createdUser = (
await tx
.insert(users)
.values({
...buildUserData(true),
id: record.id
})
.returning()
)[0]
stats.created++
}
}
if (record.id && createdUser.id) {
newMappings.users[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.query.users.findFirst({
where: eq(users.id, record.userId)
})
if (!userExists) {
console.warn(`身份关联记录的用户ID ${record.userId} 不存在,跳过`)
return
}
}
} else {
return
}
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))) {
stats.updated++
break
}
if (mode === 'merge') {
await tx
.insert(userIdentities)
.values(identityData)
.onConflictDoUpdate({
target: [userIdentities.provider, userIdentities.providerUserId],
set: identityData
})
stats.updated++
} else if (record.id) {
const existingIdentityWithId = await tx.query.userIdentities.findFirst({
where: eq(userIdentities.id, record.id)
})
if (existingIdentityWithId) {
await tx
.update(userIdentities)
.set(identityData)
.where(eq(userIdentities.id, record.id))
stats.updated++
} else {
await tx.insert(userIdentities).values({
...identityData,
id: record.id
})
stats.created++
}
} else {
await tx.insert(userIdentities).values(identityData)
stats.created++
}
break
}
case 'userStatusLogs': {
let validUserId = record.userId
if (record.userId) {
const mappedUserId = userIdMapping.get(record.userId)
if (mappedUserId) {
validUserId = mappedUserId
} else {
const userExists = await tx.query.users.findFirst({
where: eq(users.id, record.userId)
})
if (!userExists) {
console.warn(`用户状态日志的用户ID ${record.userId} 不存在,跳过`)
return
}
}
} else {
return
}
const userStatusLogData = {
userId: validUserId,
previousStatus: record.previousStatus || null,
newStatus: record.newStatus,
reason: record.reason || null,
changedBy: record.changedBy || null,
createdAt: record.createdAt ? new Date(record.createdAt) : new Date()
}
if (mode === 'merge') {
await tx.insert(userStatusLogs).values(userStatusLogData)
stats.created++
} else {
if (record.id) {
const existingLog = await tx.query.userStatusLogs.findFirst({
where: eq(userStatusLogs.id, record.id)
})
if (existingLog) {
await tx
.update(userStatusLogs)
.set(userStatusLogData)
.where(eq(userStatusLogs.id, record.id))
stats.updated++
} else {
await tx.insert(userStatusLogs).values({
...userStatusLogData,
id: record.id
})
stats.created++
}
} else {
await tx.insert(userStatusLogs).values(userStatusLogData)
stats.created++
}
}
break
}
case 'songs': {
let validRequesterId = record.requesterId
let validPreferredPlayTimeId = record.preferredPlayTimeId
if (record.requesterId) {
const mappedUserId = userIdMapping.get(record.requesterId)
if (mappedUserId) {
validRequesterId = mappedUserId
} else {
const userExists = await tx.query.users.findFirst({
where: eq(users.id, record.requesterId)
})
if (!userExists) {
console.warn(`歌曲请求者ID ${record.requesterId} 不存在,跳过`)
return
}
}
} else {
return
}
if (record.preferredPlayTimeId) {
const playTimeExists = await tx.query.playTimes.findFirst({
where: eq(playTimes.id, record.preferredPlayTimeId)
})
if (!playTimeExists) {
validPreferredPlayTimeId = null
}
}
const songData: any = {
requesterId: validRequesterId,
preferredPlayTimeId: validPreferredPlayTimeId,
played: record.hasOwnProperty('played') ? record.played : false
}
const songFields = [
'title',
'artist',
'semester',
'cover',
'musicPlatform',
'musicId',
'submissionNote',
'submissionNotePublic'
]
songFields.forEach((field) => {
if (record.hasOwnProperty(field)) {
songData[field] = record[field]
}
})
if (record.playedAt) songData.playedAt = new Date(record.playedAt)
else songData.playedAt = null
if (record.createdAt) songData.createdAt = new Date(record.createdAt)
else songData.createdAt = new Date()
if (record.updatedAt) songData.updatedAt = new Date(record.updatedAt)
else songData.updatedAt = new Date()
let createdSong
if (mode === 'merge') {
const existingSong = await tx.query.songs.findFirst({
where: and(eq(songs.title, record.title), eq(songs.artist, record.artist))
})
if (existingSong) {
createdSong = (
await tx
.update(songs)
.set(songData)
.where(eq(songs.id, existingSong.id))
.returning()
)[0]
stats.updated++
} else {
try {
const existingSongWithId = await tx.query.songs.findFirst({
where: eq(songs.id, record.id)
})
if (existingSongWithId) {
createdSong = (await tx.insert(songs).values(songData).returning())[0]
} else {
createdSong = (
await tx
.insert(songs)
.values({
...songData,
id: record.id
})
.returning()
)[0]
}
} catch (error) {
createdSong = (await tx.insert(songs).values(songData).returning())[0]
}
stats.created++
}
} else {
const existingSongWithId = await tx.query.songs.findFirst({
where: eq(songs.id, record.id)
})
if (existingSongWithId) {
createdSong = (
await tx.update(songs).set(songData).where(eq(songs.id, record.id)).returning()
)[0]
stats.updated++
} else {
createdSong = (
await tx
.insert(songs)
.values({
...songData,
id: record.id
})
.returning()
)[0]
stats.created++
}
}
if (record.id && createdSong.id) {
newMappings.songs[record.id] = createdSong.id
}
break
}
case 'playTimes': {
const playTimeData: any = {}
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.query.playTimes.findFirst({
where: eq(playTimes.name, record.name)
})
if (existingPlayTime) {
await tx
.update(playTimes)
.set(playTimeData)
.where(eq(playTimes.id, existingPlayTime.id))
stats.updated++
} else {
await tx.insert(playTimes).values(playTimeData)
stats.created++
}
} else {
const existingPlayTimeWithId = await tx.query.playTimes.findFirst({
where: eq(playTimes.id, record.id)
})
if (existingPlayTimeWithId) {
await tx.update(playTimes).set(playTimeData).where(eq(playTimes.id, record.id))
stats.updated++
} else {
await tx.insert(playTimes).values({
...playTimeData,
id: record.id
})
stats.created++
}
}
break
}
case 'semesters': {
const semesterData: any = {}
const semesterFields = ['name']
semesterFields.forEach((field) => {
if (record.hasOwnProperty(field)) {
semesterData[field] = record[field]
}
})
semesterData.isActive = record.hasOwnProperty('isActive') ? record.isActive : false
if (record.createdAt) semesterData.createdAt = new Date(record.createdAt)
if (mode === 'merge') {
const existingSemester = await tx.query.semesters.findFirst({
where: eq(semesters.name, semesterData.name)
})
if (existingSemester) {
await tx
.update(semesters)
.set(semesterData)
.where(eq(semesters.id, existingSemester.id))
stats.updated++
} else {
await tx.insert(semesters).values(semesterData)
stats.created++
}
} else {
const existing = await tx.query.semesters.findFirst({
where: eq(semesters.id, record.id)
})
if (existing) {
await tx.update(semesters).set(semesterData).where(eq(semesters.id, record.id))
stats.updated++
} else {
await tx.insert(semesters).values({ ...semesterData, id: record.id })
stats.created++
}
}
break
}
case 'systemSettings': {
const systemSettingsData: any = {}
const fields = [
'enablePlayTimeSelection',
'siteTitle',
'siteLogoUrl',
'schoolLogoHomeUrl',
'schoolLogoPrintUrl',
'siteDescription',
'submissionGuidelines',
'icpNumber',
'gonganNumber',
'enableSubmissionLimit',
'dailySubmissionLimit',
'weeklySubmissionLimit',
'monthlySubmissionLimit',
'showBlacklistKeywords',
'hideStudentInfo',
'enableReplayRequests',
'enableCollaborativeSubmission',
'enableSubmissionRemarks',
'enableRequestTimeLimitation',
'requestTimeLimitation',
'forceBlockAllRequests',
'smtpEnabled',
'smtpHost',
'smtpPort',
'smtpSecure',
'smtpUsername',
'smtpPassword',
'smtpFromEmail',
'smtpFromName'
]
fields.forEach((field) => {
if (record.hasOwnProperty(field)) systemSettingsData[field] = record[field]
})
if (mode === 'merge') {
const existing = await tx.query.systemSettings.findFirst()
if (existing) {
await tx
.update(systemSettings)
.set(systemSettingsData)
.where(eq(systemSettings.id, existing.id))
stats.updated++
} else {
await tx.insert(systemSettings).values(systemSettingsData)
stats.created++
}
} else {
const existing = await tx.query.systemSettings.findFirst({
where: eq(systemSettings.id, record.id)
})
if (existing) {
await tx
.update(systemSettings)
.set(systemSettingsData)
.where(eq(systemSettings.id, record.id))
stats.updated++
} else {
await tx.insert(systemSettings).values({ ...systemSettingsData, id: record.id })
stats.created++
}
}
break
}
case 'schedules': {
if (!record.songId || !record.playDate) return
let validSongId = record.songId
const mappedSongId = songIdMapping.get(record.songId)
if (mappedSongId) {
validSongId = mappedSongId
} else {
const songExists = await tx.query.songs.findFirst({
where: eq(songs.id, record.songId)
})
if (!songExists) return
}
let validPlayTimeId = record.playTimeId
if (record.playTimeId) {
const ptExists = await tx.query.playTimes.findFirst({
where: eq(playTimes.id, record.playTimeId)
})
if (!ptExists) validPlayTimeId = null
}
const scheduleData: any = {
songId: validSongId,
playDate: new Date(record.playDate),
playTimeId: validPlayTimeId,
played: record.hasOwnProperty('played') ? record.played : false,
sequence: record.hasOwnProperty('sequence') ? record.sequence : 1
}
if (record.publishedAt) scheduleData.publishedAt = new Date(record.publishedAt)
if (record.isDraft !== undefined) scheduleData.isDraft = record.isDraft
if (mode === 'merge') {
const existing = await tx.query.schedules.findFirst({
where: and(
eq(schedules.songId, validSongId),
eq(schedules.playDate, scheduleData.playDate)
)
})
if (existing) {
await tx.update(schedules).set(scheduleData).where(eq(schedules.id, existing.id))
stats.updated++
} else {
await tx.insert(schedules).values(scheduleData)
stats.created++
}
} else {
const existing = await tx.query.schedules.findFirst({
where: eq(schedules.id, record.id)
})
if (existing) {
await tx.update(schedules).set(scheduleData).where(eq(schedules.id, record.id))
stats.updated++
} else {
await tx.insert(schedules).values({ ...scheduleData, id: record.id })
stats.created++
}
}
break
}
case 'notificationSettings': {
let validUserId = record.userId
if (record.userId) {
const mappedUserId = userIdMapping.get(record.userId)
if (mappedUserId) validUserId = mappedUserId
else {
const userExists = await tx.query.users.findFirst({
where: eq(users.id, record.userId)
})
if (!userExists) return
}
}
const notificationSettingsData: any = { userId: validUserId }
const fields = ['refreshInterval', 'songVotedThreshold', 'meowUserId']
fields.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.refreshInterval)
notificationSettingsData.refreshInterval = 60
if (!notificationSettingsData.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') {
const existing = await tx.query.notificationSettings.findFirst({
where: eq(notificationSettings.userId, validUserId)
})
if (existing) {
await tx
.update(notificationSettings)
.set(notificationSettingsData)
.where(eq(notificationSettings.id, existing.id))
stats.updated++
} else {
await tx.insert(notificationSettings).values(notificationSettingsData)
stats.created++
}
} else {
const existing = await tx.query.notificationSettings.findFirst({
where: eq(notificationSettings.id, record.id)
})
if (existing) {
await tx
.update(notificationSettings)
.set(notificationSettingsData)
.where(eq(notificationSettings.id, record.id))
stats.updated++
} else {
await tx
.insert(notificationSettings)
.values({ ...notificationSettingsData, id: record.id })
stats.created++
}
}
break
}
case 'notifications': {
let validUserId = record.userId
if (record.userId) {
const mappedUserId = userIdMapping.get(record.userId)
if (mappedUserId) validUserId = mappedUserId
else {
const userExists = await tx.query.users.findFirst({
where: eq(users.id, record.userId)
})
if (!userExists) return
}
}
const notificationData: any = { userId: validUserId }
const fields = ['title', 'message', 'type']
fields.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') {
const existing = await tx.query.notifications.findFirst({
where: and(
eq(notifications.userId, validUserId),
eq(notifications.type, record.type),
eq(notifications.message, record.message)
)
})
if (existing) {
await tx
.update(notifications)
.set(notificationData)
.where(eq(notifications.id, existing.id))
stats.updated++
} else {
await tx.insert(notifications).values(notificationData)
stats.created++
}
} else {
const existing = await tx.query.notifications.findFirst({
where: eq(notifications.id, record.id)
})
if (existing) {
await tx
.update(notifications)
.set(notificationData)
.where(eq(notifications.id, record.id))
stats.updated++
} else {
await tx.insert(notifications).values({ ...notificationData, id: record.id })
stats.created++
}
}
break
}
case 'songBlacklist': {
let validCreatedBy = record.createdBy
if (record.createdBy) {
const mappedUserId = userIdMapping.get(record.createdBy)
if (mappedUserId) validCreatedBy = mappedUserId
else {
const userExists = await tx.query.users.findFirst({
where: eq(users.id, record.createdBy)
})
if (!userExists) validCreatedBy = null
}
}
const blacklistData: any = {
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 existing = await tx.query.songBlacklists.findFirst({
where: and(
eq(songBlacklists.type, blacklistData.type),
eq(songBlacklists.value, blacklistData.value)
)
})
if (existing) {
await tx
.update(songBlacklists)
.set(blacklistData)
.where(eq(songBlacklists.id, existing.id))
stats.updated++
} else {
await tx.insert(songBlacklists).values(blacklistData)
stats.created++
}
} else {
const existing = await tx.query.songBlacklists.findFirst({
where: eq(songBlacklists.id, record.id)
})
if (existing) {
await tx
.update(songBlacklists)
.set(blacklistData)
.where(eq(songBlacklists.id, record.id))
stats.updated++
} else {
await tx.insert(songBlacklists).values({ ...blacklistData, id: record.id })
stats.created++
}
}
break
}
case 'votes': {
let validUserId = record.userId
if (record.userId) {
const mappedUserId = userIdMapping.get(record.userId)
if (mappedUserId) validUserId = mappedUserId
else {
const userExists = await tx.query.users.findFirst({
where: eq(users.id, record.userId)
})
if (!userExists) return
}
} else return
let validSongId = record.songId
if (record.songId) {
const mappedSongId = songIdMapping.get(record.songId)
if (mappedSongId) validSongId = mappedSongId
else {
const songExists = await tx.query.songs.findFirst({
where: eq(songs.id, record.songId)
})
if (!songExists) return
}
} else return
const voteData: any = {
userId: validUserId,
songId: validSongId,
createdAt: record.createdAt ? new Date(record.createdAt) : new Date()
}
// 对于投票,merge和replace模式下都需要检查重复,因为联合主键或唯一约束
const existing = await tx.query.votes.findFirst({
where: and(eq(votes.userId, validUserId), eq(votes.songId, validSongId))
})
if (existing) {
await tx.update(votes).set(voteData).where(eq(votes.id, existing.id))
stats.updated++
} else {
await tx.insert(votes).values(voteData)
stats.created++
}
break
}
}
})
stats.processed++
} catch (error) {
console.error(`处理记录失败 (${tableName}):`, error)
stats.errors++
stats.warnings.push(`记录处理失败: ${error.message}`)
}
}
return {
success: true,
newMappings,
stats
}
})
File diff suppressed because it is too large Load Diff
+133
View File
@@ -0,0 +1,133 @@
import { createError, defineEventHandler } from 'h3'
import { promises as fs } from 'fs'
import path from 'path'
import formidable from 'formidable'
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 backupDir = path.join(process.cwd(), 'backups')
try {
await fs.access(backupDir)
} catch {
await fs.mkdir(backupDir, { recursive: true })
}
// 解析上传的文件
const form = formidable({
uploadDir: backupDir,
keepExtensions: true,
maxFileSize: 100 * 1024 * 1024, // 100MB
filter: ({ mimetype, originalFilename }) => {
// 只允许JSON文件
return (
mimetype === 'application/json' ||
(originalFilename && originalFilename.endsWith('.json'))
)
}
})
const [fields, files] = await form.parse(event.node.req)
if (!files.file || !files.file[0]) {
throw createError({
statusCode: 400,
statusMessage: '请选择要上传的备份文件'
})
}
const uploadedFile = files.file[0]
// 验证文件内容
let backupData
try {
const fileContent = await fs.readFile(uploadedFile.filepath, 'utf8')
backupData = JSON.parse(fileContent)
} catch (error) {
// 删除无效文件
await fs.unlink(uploadedFile.filepath)
throw createError({
statusCode: 400,
statusMessage: '备份文件格式无效'
})
}
// 验证备份文件结构
if (!backupData.metadata && !backupData.users) {
await fs.unlink(uploadedFile.filepath)
throw createError({
statusCode: 400,
statusMessage: '不是有效的备份文件'
})
}
// 生成新的文件名
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
const originalName = uploadedFile.originalFilename || 'backup.json'
const fileExtension = path.extname(originalName)
const baseName = path.basename(originalName, fileExtension)
const newFilename = `uploaded-${baseName}-${timestamp}${fileExtension}`
const newFilepath = path.join(backupDir, newFilename)
// 移动文件到新位置
await fs.rename(uploadedFile.filepath, newFilepath)
// 更新备份文件的元数据
if (backupData.metadata) {
// 新格式备份文件
backupData.metadata.uploadedBy = user.username
backupData.metadata.uploadedAt = new Date().toISOString()
backupData.metadata.originalFilename = originalName
backupData.metadata.creator = user.username // 确保创建者信息正确
} else if (backupData.users) {
// 旧格式备份文件,创建元数据
backupData.metadata = {
version: '0.1',
timestamp: backupData.timestamp || new Date().toISOString(),
creator: user.username,
uploadedBy: user.username,
uploadedAt: new Date().toISOString(),
originalFilename: originalName,
description: '用户数据备份(上传)',
tables: [{ name: 'users', recordCount: backupData.totalUsers || backupData.users.length }],
totalRecords: backupData.totalUsers || backupData.users.length
}
}
// 重新保存文件
await fs.writeFile(newFilepath, JSON.stringify(backupData, null, 2), 'utf8')
console.log(`管理员 ${user.username} 上传了备份文件: ${newFilename}`)
// 获取文件信息
const stats = await fs.stat(newFilepath)
return {
success: true,
message: '备份文件上传成功',
backup: {
filename: newFilename,
originalFilename: originalName,
size: stats.size,
uploadedBy: user.username,
uploadedAt: new Date().toISOString(),
metadata: backupData.metadata || null
}
}
} catch (error) {
console.error('上传备份文件失败:', error)
throw createError({
statusCode: error.statusCode || 500,
statusMessage: error.statusMessage || '上传备份文件失败'
})
}
})