初次提交
This commit is contained in:
@@ -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 || '删除备份文件失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -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 || '下载备份文件失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -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 || '创建数据库备份失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -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', '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
@@ -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 || '上传备份文件失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user