2673 lines
124 KiB
TypeScript
2673 lines
124 KiB
TypeScript
import { createError, defineEventHandler, readBody, readMultipartFormData } from 'h3'
|
||
import { db } from '~/drizzle/db'
|
||
import {
|
||
apiKeyPermissions,
|
||
apiKeys,
|
||
apiLogs,
|
||
cardCodeRedeemLogs,
|
||
cardCodes,
|
||
collaborationLogs,
|
||
emailTemplates,
|
||
gradeClass,
|
||
notificationSettings,
|
||
notifications,
|
||
playTimes,
|
||
requestTimes,
|
||
schedules,
|
||
scheduleSongPool,
|
||
semesters,
|
||
songBlacklists,
|
||
songCollaborators,
|
||
songReplayRequests,
|
||
songs,
|
||
systemSettings,
|
||
userIdentities,
|
||
users,
|
||
userStatusLogs,
|
||
votes
|
||
} from '~/drizzle/schema'
|
||
import { promises as fs } from 'fs'
|
||
import path from 'path'
|
||
import { SmtpService } from '../../../services/smtpService'
|
||
import { and, eq, inArray, isNull, notInArray, or } from 'drizzle-orm'
|
||
import { restoreScheduleSongPoolRecord } from '~~/server/utils/restoreScheduleSongPool'
|
||
import { omitMaskedSystemSettingsSecrets } from '~~/server/api/admin/system-settings/secretMask'
|
||
import { validateThemeConfig } from '~~/server/utils/theme-config'
|
||
import { createApiError } from '~~/server/utils/apiError'
|
||
import { SERVER_ERROR_CODES } from '~~/server/config/constants'
|
||
import { normalizeScheduleVisibilitySettings } from '~~/server/utils/system-settings-defaults'
|
||
|
||
export default defineEventHandler(async (event) => {
|
||
try {
|
||
// 验证管理员权限
|
||
const user = event.context.user
|
||
if (!user || user.role !== 'SUPER_ADMIN') {
|
||
throw createError({
|
||
statusCode: 403,
|
||
message: '只有超级管理员可以恢复备份'
|
||
})
|
||
}
|
||
|
||
let backupData
|
||
let mode = 'merge'
|
||
let clearExisting = false
|
||
let overwriteSuperAdmin = false
|
||
|
||
// 检查是否是文件上传
|
||
const contentType = event.node.req.headers['content-type']
|
||
|
||
if (contentType && contentType.includes('multipart/form-data')) {
|
||
// 处理文件上传
|
||
const formData = await readMultipartFormData(event)
|
||
|
||
if (!formData) {
|
||
throw createError({
|
||
statusCode: 400,
|
||
message: '请上传备份文件'
|
||
})
|
||
}
|
||
|
||
let fileData = null
|
||
for (const field of formData) {
|
||
if (field.name === 'file' && field.data) {
|
||
fileData = field.data.toString('utf8')
|
||
} else if (field.name === 'mode' && field.data) {
|
||
mode = field.data.toString()
|
||
} else if (field.name === 'clearExisting' && field.data) {
|
||
clearExisting = field.data.toString() === 'true'
|
||
} else if (field.name === 'overwriteSuperAdmin' && field.data) {
|
||
overwriteSuperAdmin = field.data.toString() === 'true'
|
||
}
|
||
}
|
||
|
||
if (!fileData) {
|
||
throw createError({
|
||
statusCode: 400,
|
||
message: '请上传备份文件'
|
||
})
|
||
}
|
||
|
||
try {
|
||
backupData = JSON.parse(fileData)
|
||
} catch (error) {
|
||
throw createError({
|
||
statusCode: 400,
|
||
message: '备份文件格式错误'
|
||
})
|
||
}
|
||
|
||
console.log(`开始恢复上传的数据库备份`)
|
||
} else {
|
||
// 处理传统的文件名方式(向后兼容)
|
||
const body = await readBody(event)
|
||
const {
|
||
filename,
|
||
mode: bodyMode = 'merge',
|
||
clearExisting: bodyClearExisting = false,
|
||
overwriteSuperAdmin: bodyOverwriteSuperAdmin = false
|
||
} = body
|
||
|
||
if (!filename) {
|
||
throw createError({
|
||
statusCode: 400,
|
||
message: '请指定备份文件名或上传备份文件'
|
||
})
|
||
}
|
||
|
||
const safeFilename = path.basename(filename)
|
||
if (safeFilename !== filename) {
|
||
throw createError({
|
||
statusCode: 400,
|
||
message: '备份文件名不合法'
|
||
})
|
||
}
|
||
|
||
mode = bodyMode
|
||
clearExisting = bodyClearExisting
|
||
overwriteSuperAdmin = bodyOverwriteSuperAdmin
|
||
|
||
console.log(`开始恢复数据库备份:${safeFilename}`)
|
||
|
||
// 读取备份文件
|
||
const backupDir = path.join(process.cwd(), 'backups')
|
||
const filepath = path.join(backupDir, safeFilename)
|
||
|
||
try {
|
||
const fileContent = await fs.readFile(filepath, 'utf8')
|
||
backupData = JSON.parse(fileContent)
|
||
} catch (error) {
|
||
throw createError({
|
||
statusCode: 404,
|
||
message: '备份文件不存在或格式错误'
|
||
})
|
||
}
|
||
}
|
||
|
||
// 验证备份文件格式
|
||
if (!backupData.metadata || !backupData.data) {
|
||
throw createError({
|
||
statusCode: 400,
|
||
message: '备份文件格式无效'
|
||
})
|
||
}
|
||
|
||
console.log(`备份文件信息:`)
|
||
console.log(`- 版本:${backupData.metadata.version}`)
|
||
console.log(`- 创建时间: ${backupData.metadata.timestamp}`)
|
||
console.log(`- 创建者: ${backupData.metadata.creator}`)
|
||
console.log(`- 总记录数: ${backupData.metadata.totalRecords}`)
|
||
|
||
const backupUsers = Array.isArray(backupData.data?.users) ? backupData.data.users : []
|
||
const hasSuperAdminInBackup = backupUsers.some((record) => record?.role === 'SUPER_ADMIN')
|
||
if (overwriteSuperAdmin && user.role !== 'SUPER_ADMIN') {
|
||
throw createError({
|
||
statusCode: 403,
|
||
message: '仅超级管理员可以覆盖超级管理员账号数据'
|
||
})
|
||
}
|
||
const shouldOverwriteSuperAdmin =
|
||
mode === 'replace' && clearExisting && overwriteSuperAdmin && hasSuperAdminInBackup
|
||
const preservedSuperAdminIds = new Set<number>()
|
||
|
||
const restoreResults = {
|
||
success: true,
|
||
message: '数据恢复完成',
|
||
details: {
|
||
tablesProcessed: 0,
|
||
recordsRestored: 0,
|
||
errors: [],
|
||
warnings: []
|
||
}
|
||
}
|
||
|
||
// 如果需要清空现有数据
|
||
if (clearExisting) {
|
||
console.log('清空现有数据...')
|
||
try {
|
||
if (shouldOverwriteSuperAdmin) {
|
||
await db.delete(apiLogs)
|
||
await db.delete(apiKeyPermissions)
|
||
await db.delete(apiKeys)
|
||
await db.delete(notifications)
|
||
await db.delete(notificationSettings)
|
||
await db.delete(cardCodeRedeemLogs)
|
||
await db.delete(collaborationLogs)
|
||
await db.delete(songCollaborators)
|
||
await db.delete(songReplayRequests)
|
||
await db.delete(scheduleSongPool)
|
||
await db.delete(schedules)
|
||
await db.delete(votes)
|
||
await db.delete(songs)
|
||
await db.delete(cardCodes)
|
||
await db.delete(songBlacklists)
|
||
await db.delete(userStatusLogs)
|
||
await db.delete(emailTemplates)
|
||
await db.delete(userIdentities)
|
||
await db.delete(users)
|
||
await db.delete(playTimes)
|
||
await db.delete(semesters)
|
||
await db.delete(requestTimes)
|
||
await db.delete(systemSettings)
|
||
} else {
|
||
const preservedUsers = await db
|
||
.select({ id: users.id })
|
||
.from(users)
|
||
.where(or(eq(users.role, 'SUPER_ADMIN'), eq(users.id, 1)))
|
||
preservedUsers.forEach((item) => preservedSuperAdminIds.add(item.id))
|
||
const preservedUserIdList = [...preservedSuperAdminIds]
|
||
|
||
const preservedApiKeys = await db
|
||
.select({ id: apiKeys.id })
|
||
.from(apiKeys)
|
||
.where(
|
||
preservedUserIdList.length > 0
|
||
? inArray(apiKeys.createdByUserId, preservedUserIdList)
|
||
: eq(apiKeys.createdByUserId, -1)
|
||
)
|
||
const preservedApiKeyIds = preservedApiKeys.map((item) => item.id)
|
||
|
||
if (preservedApiKeyIds.length > 0) {
|
||
await db
|
||
.delete(apiLogs)
|
||
.where(or(isNull(apiLogs.apiKeyId), notInArray(apiLogs.apiKeyId, preservedApiKeyIds)))
|
||
await db
|
||
.delete(apiKeyPermissions)
|
||
.where(notInArray(apiKeyPermissions.apiKeyId, preservedApiKeyIds))
|
||
} else {
|
||
await db.delete(apiLogs)
|
||
await db.delete(apiKeyPermissions)
|
||
}
|
||
|
||
if (preservedUserIdList.length > 0) {
|
||
await db.delete(cardCodeRedeemLogs)
|
||
await db.delete(apiKeys).where(notInArray(apiKeys.createdByUserId, preservedUserIdList))
|
||
await db
|
||
.delete(notifications)
|
||
.where(notInArray(notifications.userId, preservedUserIdList))
|
||
await db
|
||
.delete(notificationSettings)
|
||
.where(notInArray(notificationSettings.userId, preservedUserIdList))
|
||
await db
|
||
.delete(userStatusLogs)
|
||
.where(notInArray(userStatusLogs.userId, preservedUserIdList))
|
||
await db
|
||
.delete(userIdentities)
|
||
.where(notInArray(userIdentities.userId, preservedUserIdList))
|
||
await db.delete(users).where(notInArray(users.id, preservedUserIdList))
|
||
} else {
|
||
await db.delete(cardCodeRedeemLogs)
|
||
await db.delete(apiKeys)
|
||
await db.delete(notifications)
|
||
await db.delete(notificationSettings)
|
||
await db.delete(userStatusLogs)
|
||
await db.delete(userIdentities)
|
||
await db.delete(users)
|
||
}
|
||
|
||
await db.delete(collaborationLogs)
|
||
await db.delete(songCollaborators)
|
||
await db.delete(songReplayRequests)
|
||
await db.delete(scheduleSongPool)
|
||
await db.delete(schedules)
|
||
await db.delete(votes)
|
||
await db.delete(songs)
|
||
await db.delete(cardCodes)
|
||
await db.delete(songBlacklists)
|
||
await db.delete(emailTemplates)
|
||
await db.delete(playTimes)
|
||
await db.delete(semesters)
|
||
await db.delete(requestTimes)
|
||
await db.delete(systemSettings)
|
||
}
|
||
console.log('✅ 现有数据已清空')
|
||
} catch (error) {
|
||
console.error('清空数据失败:', error)
|
||
restoreResults.details.warnings.push(`清空数据失败: ${error.message}`)
|
||
}
|
||
}
|
||
|
||
// 建立ID映射表
|
||
const userIdMapping = new Map() // 备份ID -> 当前数据库ID
|
||
const songIdMapping = new Map() // 备份ID -> 当前数据库ID
|
||
const cardCodeIdMapping = new Map() // 备份ID -> 当前数据库ID
|
||
|
||
// 定义恢复顺序(考虑外键依赖)
|
||
const restoreOrder = [
|
||
'systemSettings',
|
||
'playTimes',
|
||
'semesters',
|
||
'requestTimes',
|
||
'gradeClass',
|
||
'users',
|
||
'userIdentities',
|
||
'emailTemplates',
|
||
'userStatusLogs',
|
||
'songBlacklist',
|
||
'cardCodes',
|
||
'songs',
|
||
'songCollaborators',
|
||
'collaborationLogs',
|
||
'songReplayRequests',
|
||
'votes',
|
||
'schedules',
|
||
'scheduleSongPool',
|
||
'cardCodeRedeemLogs',
|
||
'notificationSettings',
|
||
'notifications',
|
||
'apiKeys',
|
||
'apiKeyPermissions',
|
||
'apiLogs'
|
||
]
|
||
|
||
// 按预定义顺序恢复数据,每个表使用独立事务
|
||
for (const tableName of restoreOrder) {
|
||
if (!backupData.data[tableName] || !Array.isArray(backupData.data[tableName])) {
|
||
continue
|
||
}
|
||
|
||
const tableData = backupData.data[tableName]
|
||
|
||
console.log(`恢复表: ${tableName} (${tableData.length} 条记录)`)
|
||
|
||
try {
|
||
let restoredCount = 0
|
||
|
||
// 分批处理大量数据,每条记录使用独立事务
|
||
let batchSize = 10 // 初始批次大小
|
||
let consecutiveErrors = 0
|
||
|
||
for (let i = 0; i < tableData.length; i += batchSize) {
|
||
const batch = tableData.slice(i, i + batchSize)
|
||
|
||
// 逐条处理记录,每条记录使用独立事务,带重试机制
|
||
for (const record of batch) {
|
||
let retryCount = 0
|
||
const maxRetries = 3
|
||
let lastError = null
|
||
|
||
while (retryCount <= maxRetries) {
|
||
try {
|
||
await db.$transaction(
|
||
async (tx) => {
|
||
const stats = {
|
||
created: 0,
|
||
warnings: restoreResults.details.warnings
|
||
}
|
||
// 根据表名选择恢复策略
|
||
switch (tableName) {
|
||
case 'users':
|
||
// 动态构建用户数据,自动跳过不存在的字段
|
||
const buildUserData = (includePassword = false) => {
|
||
const userData = {}
|
||
const userFields = [
|
||
'username',
|
||
'name',
|
||
'grade',
|
||
'class',
|
||
'role',
|
||
'email',
|
||
'emailVerified',
|
||
'lastLoginIp',
|
||
'forcePasswordChange',
|
||
'meowNickname',
|
||
'status',
|
||
'statusChangedBy',
|
||
'remark',
|
||
'avatarProvider',
|
||
'avatarProviderUserId'
|
||
]
|
||
|
||
// 处理日期字段
|
||
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') {
|
||
// 在merge模式下,先检查用户名是否存在
|
||
const existingUser = await tx
|
||
.select()
|
||
.from(users)
|
||
.where(eq(users.username, record.username))
|
||
.limit(1)
|
||
|
||
if (existingUser.length > 0) {
|
||
// 如果用户名已存在,更新现有用户
|
||
const result = await tx
|
||
.update(users)
|
||
.set(buildUserData(false))
|
||
.where(eq(users.username, record.username))
|
||
.returning({ id: users.id })
|
||
createdUser = result[0]
|
||
} else {
|
||
// 如果用户名不存在,尝试使用原始ID创建
|
||
try {
|
||
// 先检查原始ID是否已被占用
|
||
const existingUserWithId = await tx
|
||
.select()
|
||
.from(users)
|
||
.where(eq(users.id, record.id))
|
||
.limit(1)
|
||
|
||
if (existingUserWithId.length > 0) {
|
||
// ID已被占用,让数据库自动生成新ID
|
||
const result = await tx
|
||
.insert(users)
|
||
.values(buildUserData(true))
|
||
.returning({ id: users.id })
|
||
createdUser = result[0]
|
||
} else {
|
||
// ID未被占用,使用原始ID
|
||
const result = await tx
|
||
.insert(users)
|
||
.values({
|
||
...buildUserData(true),
|
||
id: record.id
|
||
})
|
||
.returning({ id: users.id })
|
||
createdUser = result[0]
|
||
}
|
||
} catch (error) {
|
||
// 如果创建失败(可能是ID冲突),让数据库自动生成ID
|
||
console.warn(
|
||
`用户 ${record.username} 使用原始ID创建失败,使用自动生成ID: ${error.message}`
|
||
)
|
||
const result = await tx
|
||
.insert(users)
|
||
.values(buildUserData(true))
|
||
.returning({ id: users.id })
|
||
createdUser = result[0]
|
||
}
|
||
}
|
||
} else {
|
||
const isBackupSuperAdminRecord =
|
||
record.role === 'SUPER_ADMIN' ||
|
||
preservedSuperAdminIds.has(Number(record.id))
|
||
if (!shouldOverwriteSuperAdmin && isBackupSuperAdminRecord) {
|
||
if (record.id) {
|
||
const existingProtectedUser = await tx
|
||
.select({ id: users.id })
|
||
.from(users)
|
||
.where(eq(users.id, record.id))
|
||
.limit(1)
|
||
if (existingProtectedUser.length > 0) {
|
||
userIdMapping.set(record.id, existingProtectedUser[0].id)
|
||
restoreResults.details.warnings.push(
|
||
`已保留超级管理员账户 ${record.username || `#${record.id}`}`
|
||
)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
// 完全恢复模式,检查ID是否已存在
|
||
const existingUserWithId = await tx
|
||
.select()
|
||
.from(users)
|
||
.where(eq(users.id, record.id))
|
||
.limit(1)
|
||
|
||
if (existingUserWithId.length > 0) {
|
||
// ID已存在,使用upsert策略更新现有用户
|
||
console.warn(
|
||
`用户ID ${record.id} (${record.username}) 已存在,将更新现有用户`
|
||
)
|
||
const result = await tx
|
||
.update(users)
|
||
.set(buildUserData(true))
|
||
.where(eq(users.id, record.id))
|
||
.returning({ id: users.id })
|
||
createdUser = result[0]
|
||
} else {
|
||
// ID不存在,检查用户名是否冲突(针对保留的超级管理员等情况)
|
||
const existingUserWithUsername = await tx
|
||
.select()
|
||
.from(users)
|
||
.where(eq(users.username, record.username))
|
||
.limit(1)
|
||
|
||
if (existingUserWithUsername.length > 0) {
|
||
const targetUser = existingUserWithUsername[0]
|
||
const isProtectedTargetUser =
|
||
targetUser.role === 'SUPER_ADMIN' ||
|
||
preservedSuperAdminIds.has(Number(targetUser.id))
|
||
if (!shouldOverwriteSuperAdmin && isProtectedTargetUser) {
|
||
userIdMapping.set(record.id, targetUser.id)
|
||
restoreResults.details.warnings.push(
|
||
`已保留受保护账户 ${targetUser.username || `#${targetUser.id}`}`
|
||
)
|
||
break
|
||
}
|
||
|
||
// 用户名已存在(如保留的admin),更新该用户,并建立映射
|
||
console.warn(
|
||
`用户 ${record.username} (ID ${record.id}) 的用户名已存在于 ID ${existingUserWithUsername[0].id},将合并数据`
|
||
)
|
||
const result = await tx
|
||
.update(users)
|
||
.set(buildUserData(true))
|
||
.where(eq(users.id, existingUserWithUsername[0].id))
|
||
.returning({ id: users.id })
|
||
createdUser = result[0]
|
||
} else {
|
||
// ID不存在且用户名不冲突,使用原始ID创建
|
||
const result = await tx
|
||
.insert(users)
|
||
.values({
|
||
...buildUserData(true),
|
||
id: record.id
|
||
})
|
||
.returning({ id: users.id })
|
||
createdUser = result[0]
|
||
}
|
||
}
|
||
}
|
||
// 建立ID映射
|
||
if (record.id && createdUser.id) {
|
||
userIdMapping.set(record.id, createdUser.id)
|
||
}
|
||
break
|
||
|
||
case 'userIdentities':
|
||
// 验证必需字段
|
||
let validIdentityUserId = record.userId
|
||
if (record.userId) {
|
||
const mappedUserId = userIdMapping.get(record.userId)
|
||
if (mappedUserId) {
|
||
validIdentityUserId = mappedUserId
|
||
} else {
|
||
const userExists = await tx
|
||
.select()
|
||
.from(users)
|
||
.where(eq(users.id, record.userId))
|
||
.limit(1)
|
||
if (userExists.length === 0) {
|
||
console.warn(
|
||
`身份关联记录的用户ID ${record.userId} 不存在,跳过此记录`
|
||
)
|
||
break
|
||
}
|
||
}
|
||
} else {
|
||
console.warn(`身份关联记录缺少userId,跳过此记录`)
|
||
break
|
||
}
|
||
|
||
const identityData = {
|
||
userId: validIdentityUserId,
|
||
provider: record.provider,
|
||
providerUserId: record.providerUserId,
|
||
providerUsername: record.providerUsername,
|
||
avatar: record.avatar || null,
|
||
createdAt: record.createdAt ? new Date(record.createdAt) : new Date()
|
||
}
|
||
|
||
if (
|
||
!shouldOverwriteSuperAdmin &&
|
||
preservedSuperAdminIds.has(Number(validIdentityUserId))
|
||
) {
|
||
break
|
||
}
|
||
|
||
if (mode === 'merge') {
|
||
await tx
|
||
.insert(userIdentities)
|
||
.values(identityData)
|
||
.onConflictDoUpdate({
|
||
target: [userIdentities.provider, userIdentities.providerUserId],
|
||
set: identityData
|
||
})
|
||
} else {
|
||
if (record.id) {
|
||
const existing = await tx
|
||
.select()
|
||
.from(userIdentities)
|
||
.where(eq(userIdentities.id, record.id))
|
||
.limit(1)
|
||
if (existing.length > 0) {
|
||
await tx
|
||
.update(userIdentities)
|
||
.set(identityData)
|
||
.where(eq(userIdentities.id, record.id))
|
||
} else {
|
||
await tx.insert(userIdentities).values({
|
||
...identityData,
|
||
id: record.id
|
||
})
|
||
}
|
||
} else {
|
||
await tx.insert(userIdentities).values(identityData)
|
||
}
|
||
}
|
||
break
|
||
|
||
case 'userStatusLogs':
|
||
// 验证外键约束 - 用户ID
|
||
let validUserStatusLogUserId = record.userId
|
||
|
||
// 使用ID映射查找实际的用户ID
|
||
if (record.userId) {
|
||
const mappedUserId = userIdMapping.get(record.userId)
|
||
if (mappedUserId) {
|
||
validUserStatusLogUserId = mappedUserId
|
||
} else {
|
||
// 尝试直接查找用户ID
|
||
const userExists = await tx
|
||
.select()
|
||
.from(users)
|
||
.where(eq(users.id, record.userId))
|
||
.limit(1)
|
||
if (userExists.length === 0) {
|
||
console.warn(
|
||
`用户状态日志的用户ID ${record.userId} 不存在,跳过此记录`
|
||
)
|
||
return // 跳过此记录,因为userId是必需的
|
||
}
|
||
}
|
||
} else {
|
||
console.warn(`用户状态日志缺少userId,跳过此记录`)
|
||
return // 跳过此记录,因为userId是必需的
|
||
}
|
||
|
||
// 构建用户状态日志数据(含快照列,删除用户后审计可追溯)
|
||
const userStatusLogData = {
|
||
userId: validUserStatusLogUserId,
|
||
username: record.username ?? null,
|
||
name: record.name ?? null,
|
||
oldStatus: record.oldStatus || record.previousStatus || null,
|
||
newStatus: record.newStatus,
|
||
reason: record.reason || null,
|
||
operatorId: record.operatorId || record.changedBy || null,
|
||
createdAt: record.createdAt ? new Date(record.createdAt) : new Date()
|
||
}
|
||
|
||
if (mode === 'merge') {
|
||
// 对于状态日志,通常不需要检查重复,直接创建新记录
|
||
await tx.insert(userStatusLogs).values(userStatusLogData)
|
||
} else {
|
||
// 完全恢复模式,检查ID是否已存在
|
||
if (record.id) {
|
||
const existingLogWithId = await tx
|
||
.select()
|
||
.from(userStatusLogs)
|
||
.where(eq(userStatusLogs.id, record.id))
|
||
.limit(1)
|
||
|
||
if (existingLogWithId.length > 0) {
|
||
// ID已存在,更新现有记录
|
||
console.warn(`用户状态日志ID ${record.id} 已存在,将更新现有记录`)
|
||
await tx
|
||
.update(userStatusLogs)
|
||
.set(userStatusLogData)
|
||
.where(eq(userStatusLogs.id, record.id))
|
||
} else {
|
||
// ID不存在,使用原始ID创建
|
||
await tx.insert(userStatusLogs).values({
|
||
...userStatusLogData,
|
||
id: record.id
|
||
})
|
||
}
|
||
} else {
|
||
// 没有ID,让数据库自动生成
|
||
await tx.insert(userStatusLogs).values(userStatusLogData)
|
||
}
|
||
}
|
||
break
|
||
|
||
case 'cardCodes': {
|
||
const cardCodeData: any = {}
|
||
const cardCodeFields = ['code', 'status', 'note']
|
||
cardCodeFields.forEach((field) => {
|
||
if (record.hasOwnProperty(field)) {
|
||
cardCodeData[field] = record[field]
|
||
}
|
||
})
|
||
|
||
if (!cardCodeData.code) {
|
||
console.warn('点歌券缺少 code,跳过此记录')
|
||
return
|
||
}
|
||
|
||
const mapUserId = async (field: 'lockedBy' | 'redeemedBy') => {
|
||
if (!record[field]) return null
|
||
const mappedUserId = userIdMapping.get(record[field])
|
||
if (mappedUserId) return mappedUserId
|
||
if (mode === 'merge') return null
|
||
const userExists = await tx
|
||
.select({ id: users.id })
|
||
.from(users)
|
||
.where(eq(users.id, record[field]))
|
||
.limit(1)
|
||
return userExists.length > 0 ? record[field] : null
|
||
}
|
||
|
||
cardCodeData.lockedBy = await mapUserId('lockedBy')
|
||
cardCodeData.redeemedBy = await mapUserId('redeemedBy')
|
||
cardCodeData.lockedAt = record.lockedAt ? new Date(record.lockedAt) : null
|
||
cardCodeData.redeemedAt = record.redeemedAt
|
||
? new Date(record.redeemedAt)
|
||
: null
|
||
cardCodeData.createdAt = record.createdAt
|
||
? new Date(record.createdAt)
|
||
: new Date()
|
||
cardCodeData.updatedAt = record.updatedAt
|
||
? new Date(record.updatedAt)
|
||
: new Date()
|
||
|
||
let restoredCardCode
|
||
if (mode === 'merge') {
|
||
const existingCardCode = await tx
|
||
.select()
|
||
.from(cardCodes)
|
||
.where(eq(cardCodes.code, cardCodeData.code))
|
||
.limit(1)
|
||
|
||
if (existingCardCode.length > 0) {
|
||
restoredCardCode = (
|
||
await tx
|
||
.update(cardCodes)
|
||
.set(cardCodeData)
|
||
.where(eq(cardCodes.id, existingCardCode[0].id))
|
||
.returning()
|
||
)[0]
|
||
} else {
|
||
restoredCardCode = (
|
||
await tx.insert(cardCodes).values(cardCodeData).returning()
|
||
)[0]
|
||
}
|
||
} else {
|
||
const existingCardCodeWithId = await tx
|
||
.select()
|
||
.from(cardCodes)
|
||
.where(eq(cardCodes.id, record.id))
|
||
.limit(1)
|
||
|
||
if (existingCardCodeWithId.length > 0) {
|
||
restoredCardCode = (
|
||
await tx
|
||
.update(cardCodes)
|
||
.set(cardCodeData)
|
||
.where(eq(cardCodes.id, record.id))
|
||
.returning()
|
||
)[0]
|
||
} else {
|
||
restoredCardCode = (
|
||
await tx
|
||
.insert(cardCodes)
|
||
.values({ ...cardCodeData, id: record.id })
|
||
.returning()
|
||
)[0]
|
||
}
|
||
}
|
||
|
||
if (record.id && restoredCardCode?.id) {
|
||
cardCodeIdMapping.set(record.id, restoredCardCode.id)
|
||
}
|
||
break
|
||
}
|
||
|
||
case 'songs':
|
||
// 验证外键约束
|
||
let validRequesterId = record.requesterId
|
||
let validPreferredPlayTimeId = record.preferredPlayTimeId
|
||
|
||
// 使用ID映射查找实际的用户ID
|
||
if (record.requesterId) {
|
||
const mappedUserId = userIdMapping.get(record.requesterId)
|
||
if (mappedUserId) {
|
||
validRequesterId = mappedUserId
|
||
} else {
|
||
// 尝试直接查找用户ID
|
||
const userExists = await tx
|
||
.select()
|
||
.from(users)
|
||
.where(eq(users.id, record.requesterId))
|
||
.limit(1)
|
||
if (userExists.length === 0) {
|
||
console.warn(
|
||
`歌曲 ${record.title} 的请求者ID ${record.requesterId} 不存在,跳过此记录`
|
||
)
|
||
return // 跳过此记录,因为requesterId是必需的
|
||
}
|
||
}
|
||
} else {
|
||
console.warn(`歌曲 ${record.title} 缺少requesterId,跳过此记录`)
|
||
return // 跳过此记录,因为requesterId是必需的
|
||
}
|
||
|
||
// 检查preferredPlayTimeId是否存在(可选字段)
|
||
if (record.preferredPlayTimeId) {
|
||
const playTimeExists = await tx
|
||
.select()
|
||
.from(playTimes)
|
||
.where(eq(playTimes.id, record.preferredPlayTimeId))
|
||
.limit(1)
|
||
if (playTimeExists.length === 0) {
|
||
console.warn(
|
||
`歌曲 ${record.title} 的播放时间ID ${record.preferredPlayTimeId} 不存在,将设为null`
|
||
)
|
||
validPreferredPlayTimeId = null
|
||
}
|
||
}
|
||
|
||
let validCardCodeId = record.cardCodeId || null
|
||
if (record.cardCodeId) {
|
||
const mappedCardCodeId = cardCodeIdMapping.get(record.cardCodeId)
|
||
if (mappedCardCodeId) {
|
||
validCardCodeId = mappedCardCodeId
|
||
} else {
|
||
const cardCodeExists = await tx
|
||
.select({ id: cardCodes.id })
|
||
.from(cardCodes)
|
||
.where(eq(cardCodes.id, record.cardCodeId))
|
||
.limit(1)
|
||
if (cardCodeExists.length === 0) {
|
||
console.warn(
|
||
`歌曲 ${record.title} 的点歌券ID ${record.cardCodeId} 不存在,将设为null`
|
||
)
|
||
validCardCodeId = null
|
||
}
|
||
}
|
||
}
|
||
|
||
// 动态构建歌曲数据,自动跳过不存在的字段
|
||
const songData = {
|
||
requesterId: validRequesterId, // 必需字段
|
||
preferredPlayTimeId: validPreferredPlayTimeId, // 已验证的字段
|
||
cardCodeId: validCardCodeId
|
||
}
|
||
|
||
// 基本字段
|
||
const songFields = [
|
||
'title',
|
||
'artist',
|
||
'semester',
|
||
'cover',
|
||
'musicPlatform',
|
||
'musicId',
|
||
'durationSeconds',
|
||
'submissionNote',
|
||
'submissionNotePublic',
|
||
'submissionNotePublicStatus'
|
||
]
|
||
songFields.forEach((field) => {
|
||
if (record.hasOwnProperty(field)) {
|
||
songData[field] = record[field]
|
||
}
|
||
})
|
||
|
||
// 布尔字段,提供默认值
|
||
songData.played = record.hasOwnProperty('played') ? record.played : false
|
||
|
||
// 日期字段
|
||
if (record.hasOwnProperty('playedAt') && record.playedAt) {
|
||
songData.playedAt = new Date(record.playedAt)
|
||
} else {
|
||
songData.playedAt = null
|
||
}
|
||
|
||
if (record.hasOwnProperty('createdAt') && record.createdAt) {
|
||
songData.createdAt = new Date(record.createdAt)
|
||
} else {
|
||
songData.createdAt = new Date()
|
||
}
|
||
|
||
if (record.hasOwnProperty('updatedAt') && record.updatedAt) {
|
||
songData.updatedAt = new Date(record.updatedAt)
|
||
} else {
|
||
songData.updatedAt = new Date()
|
||
}
|
||
|
||
let createdSong
|
||
if (mode === 'merge') {
|
||
// 检查是否存在相同的歌曲(按标题和艺术家)
|
||
const existingSong = await tx
|
||
.select()
|
||
.from(songs)
|
||
.where(
|
||
and(eq(songs.title, record.title), eq(songs.artist, record.artist))
|
||
)
|
||
.limit(1)
|
||
|
||
if (existingSong.length > 0) {
|
||
// 如果存在相同歌曲,更新它
|
||
const result = await tx
|
||
.update(songs)
|
||
.set(songData)
|
||
.where(eq(songs.id, existingSong[0].id))
|
||
.returning({ id: songs.id })
|
||
createdSong = result[0]
|
||
} else {
|
||
// 如果不存在,尝试使用原始ID创建
|
||
try {
|
||
// 先检查原始ID是否已被占用
|
||
const existingSongWithId = await tx
|
||
.select()
|
||
.from(songs)
|
||
.where(eq(songs.id, record.id))
|
||
.limit(1)
|
||
|
||
if (existingSongWithId.length > 0) {
|
||
// ID已被占用,让数据库自动生成新ID
|
||
const result = await tx
|
||
.insert(songs)
|
||
.values(songData)
|
||
.returning({ id: songs.id })
|
||
createdSong = result[0]
|
||
} else {
|
||
// ID未被占用,使用原始ID
|
||
const result = await tx
|
||
.insert(songs)
|
||
.values({
|
||
...songData,
|
||
id: record.id
|
||
})
|
||
.returning({ id: songs.id })
|
||
createdSong = result[0]
|
||
}
|
||
} catch (error) {
|
||
// 如果创建失败(可能是ID冲突),让数据库自动生成ID
|
||
console.warn(
|
||
`歌曲 ${record.title} 使用原始ID创建失败,使用自动生成ID: ${error.message}`
|
||
)
|
||
const result = await tx
|
||
.insert(songs)
|
||
.values(songData)
|
||
.returning({ id: songs.id })
|
||
createdSong = result[0]
|
||
}
|
||
}
|
||
} else {
|
||
// 完全恢复模式,检查ID是否已存在
|
||
const existingSongWithId = await tx
|
||
.select()
|
||
.from(songs)
|
||
.where(eq(songs.id, record.id))
|
||
.limit(1)
|
||
|
||
if (existingSongWithId.length > 0) {
|
||
// ID已存在,使用upsert策略更新现有歌曲
|
||
console.warn(
|
||
`歌曲ID ${record.id} (${record.title} - ${record.artist}) 已存在,将更新现有歌曲`
|
||
)
|
||
const result = await tx
|
||
.update(songs)
|
||
.set(songData)
|
||
.where(eq(songs.id, record.id))
|
||
.returning({ id: songs.id })
|
||
createdSong = result[0]
|
||
} else {
|
||
// ID不存在,使用原始ID创建
|
||
const result = await tx
|
||
.insert(songs)
|
||
.values({
|
||
...songData,
|
||
id: record.id
|
||
})
|
||
.returning({ id: songs.id })
|
||
createdSong = result[0]
|
||
}
|
||
}
|
||
// 建立歌曲ID映射
|
||
if (record.id && createdSong.id) {
|
||
songIdMapping.set(record.id, createdSong.id)
|
||
}
|
||
break
|
||
|
||
case 'playTimes':
|
||
// 动态构建播放时段数据,自动跳过不存在的字段
|
||
const playTimeData = {}
|
||
const playTimeFields = ['name', 'startTime', 'endTime', 'description']
|
||
|
||
playTimeFields.forEach((field) => {
|
||
if (record.hasOwnProperty(field)) {
|
||
playTimeData[field] = record[field]
|
||
}
|
||
})
|
||
|
||
// 布尔字段,提供默认值
|
||
playTimeData.enabled = record.hasOwnProperty('enabled')
|
||
? record.enabled
|
||
: true
|
||
|
||
if (mode === 'merge') {
|
||
// 检查是否存在相同名称的播放时段
|
||
const existingPlayTime = await tx
|
||
.select()
|
||
.from(playTimes)
|
||
.where(eq(playTimes.name, record.name))
|
||
.limit(1)
|
||
|
||
if (existingPlayTime.length > 0) {
|
||
// 如果存在相同播放时段,更新它
|
||
await tx
|
||
.update(playTimes)
|
||
.set(playTimeData)
|
||
.where(eq(playTimes.id, existingPlayTime[0].id))
|
||
} else {
|
||
// 如果不存在,创建新播放时段(不指定ID,让数据库自动生成)
|
||
await tx.insert(playTimes).values(playTimeData)
|
||
}
|
||
} else {
|
||
// 完全恢复模式,检查ID是否已存在
|
||
const existingPlayTimeWithId = await tx
|
||
.select()
|
||
.from(playTimes)
|
||
.where(eq(playTimes.id, record.id))
|
||
.limit(1)
|
||
|
||
if (existingPlayTimeWithId.length > 0) {
|
||
// ID已存在,使用upsert策略更新现有播放时段
|
||
console.warn(
|
||
`播放时段ID ${record.id} (${record.name}) 已存在,将更新现有播放时段`
|
||
)
|
||
await tx
|
||
.update(playTimes)
|
||
.set(playTimeData)
|
||
.where(eq(playTimes.id, record.id))
|
||
} else {
|
||
// ID不存在,使用原始ID创建
|
||
await tx.insert(playTimes).values({
|
||
...playTimeData,
|
||
id: record.id
|
||
})
|
||
}
|
||
}
|
||
break
|
||
|
||
case 'semesters':
|
||
// 动态构建学期数据,自动跳过不存在的字段
|
||
const semesterData = {}
|
||
const semesterFields = ['name']
|
||
|
||
semesterFields.forEach((field) => {
|
||
if (record.hasOwnProperty(field)) {
|
||
semesterData[field] = record[field]
|
||
}
|
||
})
|
||
|
||
// 布尔字段,提供默认值
|
||
semesterData.isActive = record.hasOwnProperty('isActive')
|
||
? record.isActive
|
||
: false
|
||
|
||
if (mode === 'merge') {
|
||
const existingSemester = await tx
|
||
.select()
|
||
.from(semesters)
|
||
.where(eq(semesters.name, semesterData.name))
|
||
.limit(1)
|
||
if (existingSemester.length > 0) {
|
||
await tx
|
||
.update(semesters)
|
||
.set(semesterData)
|
||
.where(eq(semesters.name, semesterData.name))
|
||
} else {
|
||
await tx.insert(semesters).values(semesterData)
|
||
}
|
||
} else {
|
||
// 完全恢复模式,检查ID是否已存在
|
||
const existingSemesterWithId = await tx
|
||
.select()
|
||
.from(semesters)
|
||
.where(eq(semesters.id, record.id))
|
||
.limit(1)
|
||
|
||
if (existingSemesterWithId.length > 0) {
|
||
// ID已存在,使用upsert策略更新现有学期
|
||
console.warn(
|
||
`学期ID ${record.id} (${record.name}) 已存在,将更新现有学期`
|
||
)
|
||
await tx
|
||
.update(semesters)
|
||
.set(semesterData)
|
||
.where(eq(semesters.id, record.id))
|
||
} else {
|
||
// ID不存在,使用原始ID创建
|
||
await tx.insert(semesters).values({
|
||
...semesterData,
|
||
id: record.id
|
||
})
|
||
}
|
||
}
|
||
break
|
||
|
||
case 'gradeClass':
|
||
// 年级班级配置(唯一约束 grade+class)
|
||
const gradeClassData = {
|
||
grade: typeof record.grade === 'string' ? record.grade.trim() : '',
|
||
class: typeof record.class === 'string' ? record.class.trim() : ''
|
||
}
|
||
// 空行直接跳过,避免空值配置行锁死配置优先校验
|
||
if (!gradeClassData.grade || !gradeClassData.class) {
|
||
stats.warnings.push(`gradeClass 记录 ${record.id ?? ''} 年级或班级为空,已跳过`)
|
||
break
|
||
}
|
||
|
||
if (mode === 'merge') {
|
||
const existingGradeClass = await tx
|
||
.select()
|
||
.from(gradeClass)
|
||
.where(
|
||
and(
|
||
eq(gradeClass.grade, gradeClassData.grade),
|
||
eq(gradeClass.class, gradeClassData.class)
|
||
)
|
||
)
|
||
.limit(1)
|
||
if (existingGradeClass.length === 0) {
|
||
await tx.insert(gradeClass).values(gradeClassData)
|
||
}
|
||
} else {
|
||
// 完全恢复模式,检查ID是否已存在
|
||
const existingGradeClassWithId = await tx
|
||
.select()
|
||
.from(gradeClass)
|
||
.where(eq(gradeClass.id, record.id))
|
||
.limit(1)
|
||
|
||
if (existingGradeClassWithId.length === 0) {
|
||
await tx.insert(gradeClass).values({
|
||
...gradeClassData,
|
||
id: record.id
|
||
})
|
||
}
|
||
}
|
||
break
|
||
|
||
case 'systemSettings':
|
||
// 动态构建系统设置数据,自动跳过不存在的字段
|
||
let systemSettingsData = {}
|
||
const systemSettingsFields = [
|
||
'enablePlayTimeSelection',
|
||
'instanceId',
|
||
'defaultTheme',
|
||
'enabledThemes',
|
||
'telemetryEnabled',
|
||
'siteTitle',
|
||
'siteLogoUrl',
|
||
'schoolLogoHomeUrl',
|
||
'schoolLogoPrintUrl',
|
||
'siteDescription',
|
||
'submissionGuidelines',
|
||
'icpNumber',
|
||
'gonganNumber',
|
||
'enableSubmissionLimit',
|
||
'dailySubmissionLimit',
|
||
'weeklySubmissionLimit',
|
||
'monthlySubmissionLimit',
|
||
'scheduleDaysBeforeEnabled',
|
||
'scheduleDaysBefore',
|
||
'scheduleDaysAfterEnabled',
|
||
'scheduleDaysAfter',
|
||
'showBlacklistKeywords',
|
||
'enableRequestTimeLimitation',
|
||
'forceBlockAllRequests',
|
||
'forcePasswordChangeOnFirstLogin',
|
||
'enableReplayRequests',
|
||
'enableCollaborativeSubmission',
|
||
'enableSubmissionRemarks',
|
||
'enableCardCodeRequests',
|
||
'requireCardCodeForRequests',
|
||
'enableCardCodeLimitBypass',
|
||
'enableSubmissionRestriction',
|
||
'submissionRestrictionScope',
|
||
'sameSongRestrictionHours',
|
||
'sameArtistRestrictionHours',
|
||
'hideStudentInfo',
|
||
'smtpEnabled',
|
||
'smtpHost',
|
||
'smtpPort',
|
||
'smtpSecure',
|
||
'smtpUsername',
|
||
'smtpPassword',
|
||
'smtpFromEmail',
|
||
'smtpFromName',
|
||
'allowOAuthRegistration',
|
||
'allowRegister',
|
||
'registerRequiresApproval',
|
||
'registerEmailRequired',
|
||
'registerRequiresGradeClass',
|
||
'oauthRegisterRequiresApproval',
|
||
'submissionNoteRequiresApproval',
|
||
'oauthRedirectUri',
|
||
'oauthStateSecret',
|
||
'oauthProviders',
|
||
'githubOAuthEnabled',
|
||
'githubClientId',
|
||
'githubClientSecret',
|
||
'casdoorOAuthEnabled',
|
||
'casdoorServerUrl',
|
||
'casdoorClientId',
|
||
'casdoorClientSecret',
|
||
'casdoorOrganizationName',
|
||
'googleOAuthEnabled',
|
||
'googleClientId',
|
||
'googleClientSecret',
|
||
'aggregateOAuthEnabled',
|
||
'aggregateOAuthAppId',
|
||
'aggregateOAuthAppKey',
|
||
'aggregateOAuthLoginType',
|
||
'aggregateOAuthEndpoint',
|
||
'customOAuthEnabled',
|
||
'customOAuthDisplayName',
|
||
'customOAuthAuthorizeUrl',
|
||
'customOAuthTokenUrl',
|
||
'customOAuthUserInfoUrl',
|
||
'customOAuthScope',
|
||
'customOAuthClientId',
|
||
'customOAuthClientSecret',
|
||
'customOAuthUserIdField',
|
||
'customOAuthUsernameField',
|
||
'customOAuthNameField',
|
||
'customOAuthEmailField',
|
||
'customOAuthAvatarField',
|
||
'captchaEnabled',
|
||
'captchaMaxFailures',
|
||
'captchaProvider',
|
||
'turnstileSiteKey',
|
||
'turnstileSecretKey',
|
||
'autoBackupEnabled',
|
||
'autoBackupConfig',
|
||
'statisticsCodeEnabled',
|
||
'statisticsCode',
|
||
'enabledPlatforms',
|
||
'platformOrder'
|
||
]
|
||
|
||
// 只添加备份数据中存在的字段
|
||
systemSettingsFields.forEach((field) => {
|
||
if (record.hasOwnProperty(field)) {
|
||
systemSettingsData[field] = record[field]
|
||
}
|
||
})
|
||
if (Object.prototype.hasOwnProperty.call(systemSettingsData, 'defaultTheme') || Object.prototype.hasOwnProperty.call(systemSettingsData, 'enabledThemes')) {
|
||
if (!Object.prototype.hasOwnProperty.call(systemSettingsData, 'defaultTheme') || !Object.prototype.hasOwnProperty.call(systemSettingsData, 'enabledThemes')) {
|
||
throw createApiError(400, SERVER_ERROR_CODES.THEME_INVALID_LIST, '主题配置必须同时包含默认主题和启用主题列表')
|
||
}
|
||
systemSettingsData.enabledThemes = JSON.stringify(validateThemeConfig(systemSettingsData.defaultTheme, systemSettingsData.enabledThemes))
|
||
}
|
||
for (const field of ['scheduleDaysBeforeEnabled', 'scheduleDaysAfterEnabled']) {
|
||
if (Object.prototype.hasOwnProperty.call(systemSettingsData, field) && systemSettingsData[field] !== null && typeof systemSettingsData[field] !== 'boolean') {
|
||
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, `${field} 必须是布尔值`)
|
||
}
|
||
}
|
||
for (const field of ['scheduleDaysBefore', 'scheduleDaysAfter']) {
|
||
if (Object.prototype.hasOwnProperty.call(systemSettingsData, field) && (systemSettingsData[field] !== null && (!Number.isInteger(systemSettingsData[field]) || systemSettingsData[field] < 1 || systemSettingsData[field] > 730))) {
|
||
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, `${field} 必须是 1-730 的正整数`)
|
||
}
|
||
}
|
||
systemSettingsData = normalizeScheduleVisibilitySettings(systemSettingsData)
|
||
systemSettingsData = omitMaskedSystemSettingsSecrets(systemSettingsData)
|
||
|
||
if (mode === 'merge') {
|
||
// 检查是否存在系统设置记录(通常只有一条记录)
|
||
const existingSystemSettings = await tx
|
||
.select()
|
||
.from(systemSettings)
|
||
.limit(1)
|
||
|
||
if (existingSystemSettings.length > 0) {
|
||
// 如果存在系统设置,更新它
|
||
await tx
|
||
.update(systemSettings)
|
||
.set(systemSettingsData)
|
||
.where(eq(systemSettings.id, existingSystemSettings[0].id))
|
||
} else {
|
||
// 如果不存在,创建新系统设置(不指定ID,让数据库自动生成)
|
||
await tx.insert(systemSettings).values(systemSettingsData)
|
||
}
|
||
} else {
|
||
// 完全恢复模式,检查ID是否已存在
|
||
const existingSystemSettingsWithId = await tx
|
||
.select()
|
||
.from(systemSettings)
|
||
.where(eq(systemSettings.id, record.id))
|
||
.limit(1)
|
||
|
||
if (existingSystemSettingsWithId.length > 0) {
|
||
// ID已存在,使用upsert策略更新现有系统设置
|
||
console.warn(`系统设置ID ${record.id} 已存在,将更新现有系统设置`)
|
||
await tx
|
||
.update(systemSettings)
|
||
.set(systemSettingsData)
|
||
.where(eq(systemSettings.id, record.id))
|
||
} else {
|
||
// ID不存在,使用原始ID创建
|
||
await tx.insert(systemSettings).values({
|
||
...systemSettingsData,
|
||
id: record.id
|
||
})
|
||
}
|
||
}
|
||
break
|
||
|
||
case 'schedules':
|
||
// 验证必需字段
|
||
if (!record.songId) {
|
||
console.warn(`排期记录缺少songId,跳过此记录`)
|
||
return
|
||
}
|
||
|
||
if (!record.playDate) {
|
||
console.warn(`排期记录缺少playDate,跳过此记录`)
|
||
return
|
||
}
|
||
|
||
// 使用ID映射查找实际的歌曲ID
|
||
let validSongId = record.songId
|
||
const mappedSongId = songIdMapping.get(record.songId)
|
||
if (mappedSongId) {
|
||
validSongId = mappedSongId
|
||
} else {
|
||
// 尝试直接查找歌曲ID
|
||
const songExists = await tx
|
||
.select()
|
||
.from(songs)
|
||
.where(eq(songs.id, record.songId))
|
||
.limit(1)
|
||
if (songExists.length === 0) {
|
||
console.warn(`排期记录的歌曲ID ${record.songId} 不存在,跳过此记录`)
|
||
return
|
||
}
|
||
}
|
||
|
||
// 验证playTimeId是否存在(可选字段)
|
||
let validPlayTimeId = record.playTimeId
|
||
if (record.playTimeId) {
|
||
const playTimeExists = await tx
|
||
.select()
|
||
.from(playTimes)
|
||
.where(eq(playTimes.id, record.playTimeId))
|
||
.limit(1)
|
||
if (playTimeExists.length === 0) {
|
||
console.warn(
|
||
`排期记录的播放时间ID ${record.playTimeId} 不存在,将设为null`
|
||
)
|
||
validPlayTimeId = null
|
||
}
|
||
}
|
||
|
||
// 动态构建排期数据,自动跳过不存在的字段
|
||
const scheduleData = {
|
||
songId: validSongId,
|
||
playDate: new Date(record.playDate),
|
||
playTimeId: validPlayTimeId
|
||
}
|
||
|
||
// 布尔字段,提供默认值
|
||
scheduleData.played = record.hasOwnProperty('played')
|
||
? record.played
|
||
: false
|
||
|
||
// 数字字段,提供默认值
|
||
scheduleData.sequence = record.hasOwnProperty('sequence')
|
||
? record.sequence
|
||
: 1
|
||
|
||
// 重播申请绑定:申请先于排期恢复,校验绑定存在且属于同一首歌,避免悬空绑定
|
||
let validScheduleReplayRequestId = null
|
||
if (record.replayRequestId) {
|
||
const boundReplayRequest = await tx
|
||
.select({
|
||
id: songReplayRequests.id,
|
||
songId: songReplayRequests.songId
|
||
})
|
||
.from(songReplayRequests)
|
||
.where(eq(songReplayRequests.id, record.replayRequestId))
|
||
.limit(1)
|
||
if (
|
||
boundReplayRequest.length > 0 &&
|
||
boundReplayRequest[0].songId === validSongId
|
||
) {
|
||
validScheduleReplayRequestId = record.replayRequestId
|
||
}
|
||
}
|
||
scheduleData.replayRequestId = validScheduleReplayRequestId
|
||
|
||
if (mode === 'merge') {
|
||
// 检查是否存在相同的排期(按歌曲ID和播放日期)
|
||
const existingSchedule = await tx
|
||
.select()
|
||
.from(schedules)
|
||
.where(
|
||
and(
|
||
eq(schedules.songId, validSongId),
|
||
eq(schedules.playDate, new Date(record.playDate))
|
||
)
|
||
)
|
||
.limit(1)
|
||
|
||
if (existingSchedule.length > 0) {
|
||
// 如果存在相同排期,更新它
|
||
await tx
|
||
.update(schedules)
|
||
.set(scheduleData)
|
||
.where(eq(schedules.id, existingSchedule[0].id))
|
||
} else {
|
||
// 如果不存在,创建新排期(不指定ID,让数据库自动生成)
|
||
await tx.insert(schedules).values(scheduleData)
|
||
}
|
||
} else {
|
||
// 完全恢复模式,检查ID是否已存在
|
||
const existingScheduleWithId = await tx
|
||
.select()
|
||
.from(schedules)
|
||
.where(eq(schedules.id, record.id))
|
||
.limit(1)
|
||
|
||
if (existingScheduleWithId.length > 0) {
|
||
// ID已存在,使用upsert策略更新现有排期
|
||
console.warn(`排期ID ${record.id} 已存在,将更新现有排期`)
|
||
await tx
|
||
.update(schedules)
|
||
.set(scheduleData)
|
||
.where(eq(schedules.id, record.id))
|
||
} else {
|
||
// ID不存在,使用原始ID创建
|
||
await tx.insert(schedules).values({
|
||
...scheduleData,
|
||
id: record.id
|
||
})
|
||
}
|
||
}
|
||
break
|
||
|
||
case 'notificationSettings':
|
||
// 验证userId是否存在
|
||
let validUserId = record.userId
|
||
if (record.userId) {
|
||
const mappedUserId = userIdMapping.get(record.userId)
|
||
if (mappedUserId) {
|
||
validUserId = mappedUserId
|
||
} else {
|
||
// 尝试直接查找用户ID
|
||
const userExists = await tx
|
||
.select()
|
||
.from(users)
|
||
.where(eq(users.id, record.userId))
|
||
.limit(1)
|
||
if (userExists.length === 0) {
|
||
console.warn(`通知设置的用户ID ${record.userId} 不存在,跳过此记录`)
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
// 动态构建通知设置数据,自动跳过不存在的字段
|
||
const notificationSettingsData = { userId: validUserId }
|
||
const notificationFields = [
|
||
'refreshInterval',
|
||
'songVotedThreshold',
|
||
'meowUserId'
|
||
]
|
||
|
||
notificationFields.forEach((field) => {
|
||
if (record.hasOwnProperty(field)) {
|
||
notificationSettingsData[field] = record[field]
|
||
}
|
||
})
|
||
|
||
// 布尔字段,提供默认值
|
||
notificationSettingsData.enabled = record.hasOwnProperty('enabled')
|
||
? record.enabled
|
||
: true
|
||
notificationSettingsData.songRequestEnabled = record.hasOwnProperty(
|
||
'songRequestEnabled'
|
||
)
|
||
? record.songRequestEnabled
|
||
: true
|
||
notificationSettingsData.songVotedEnabled = record.hasOwnProperty(
|
||
'songVotedEnabled'
|
||
)
|
||
? record.songVotedEnabled
|
||
: true
|
||
notificationSettingsData.songPlayedEnabled = record.hasOwnProperty(
|
||
'songPlayedEnabled'
|
||
)
|
||
? record.songPlayedEnabled
|
||
: true
|
||
|
||
// 数字字段,提供默认值
|
||
if (!notificationSettingsData.hasOwnProperty('refreshInterval')) {
|
||
notificationSettingsData.refreshInterval = 60
|
||
}
|
||
if (!notificationSettingsData.hasOwnProperty('songVotedThreshold')) {
|
||
notificationSettingsData.songVotedThreshold = 1
|
||
}
|
||
|
||
// 日期字段
|
||
notificationSettingsData.createdAt = record.createdAt
|
||
? new Date(record.createdAt)
|
||
: new Date()
|
||
notificationSettingsData.updatedAt = record.updatedAt
|
||
? new Date(record.updatedAt)
|
||
: new Date()
|
||
|
||
if (mode === 'merge') {
|
||
// 使用userId作为唯一标识进行upsert,因为数据库中userId有唯一约束
|
||
await tx
|
||
.insert(notificationSettings)
|
||
.values(notificationSettingsData)
|
||
.onConflictDoUpdate({
|
||
target: notificationSettings.userId,
|
||
set: notificationSettingsData
|
||
})
|
||
} else {
|
||
// 完全恢复模式,检查ID是否已存在
|
||
const existingNotificationSettingsWithId = await tx
|
||
.select()
|
||
.from(notificationSettings)
|
||
.where(eq(notificationSettings.id, record.id))
|
||
.limit(1)
|
||
|
||
if (existingNotificationSettingsWithId.length > 0) {
|
||
// ID已存在,使用upsert策略更新现有通知设置
|
||
console.warn(`通知设置ID ${record.id} 已存在,将更新现有通知设置`)
|
||
await tx
|
||
.update(notificationSettings)
|
||
.set(notificationSettingsData)
|
||
.where(eq(notificationSettings.id, record.id))
|
||
} else {
|
||
// ID不存在,使用原始ID创建
|
||
await tx.insert(notificationSettings).values({
|
||
...notificationSettingsData,
|
||
id: record.id
|
||
})
|
||
}
|
||
}
|
||
break
|
||
|
||
case 'notifications':
|
||
// 验证userId是否存在
|
||
let validNotificationUserId = record.userId
|
||
if (record.userId) {
|
||
const mappedUserId = userIdMapping.get(record.userId)
|
||
if (mappedUserId) {
|
||
validNotificationUserId = mappedUserId
|
||
} else {
|
||
// 尝试直接查找用户ID
|
||
const userExists = await tx
|
||
.select()
|
||
.from(users)
|
||
.where(eq(users.id, record.userId))
|
||
.limit(1)
|
||
if (userExists.length === 0) {
|
||
console.warn(`通知的用户ID ${record.userId} 不存在,跳过此记录`)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
// 动态构建通知数据,自动跳过不存在的字段
|
||
const notificationData = { userId: validNotificationUserId }
|
||
const notificationDataFields = [
|
||
'batchId',
|
||
'source',
|
||
'senderName',
|
||
'senderUsername',
|
||
'title',
|
||
'message',
|
||
'type',
|
||
'userDeleted'
|
||
]
|
||
|
||
notificationDataFields.forEach((field) => {
|
||
if (record.hasOwnProperty(field)) {
|
||
notificationData[field] = record[field]
|
||
}
|
||
})
|
||
|
||
// senderId 需要重新映射到目标库的用户 ID;映射未命中时回查同 ID 用户并比对用户名快照,
|
||
// 不一致则置空,避免跨库恢复时将发送人归属到错误用户,展示仍靠快照字段
|
||
if (Object.prototype.hasOwnProperty.call(record, 'senderId')) {
|
||
if (record.senderId) {
|
||
const mappedSenderId = userIdMapping.get(record.senderId)
|
||
if (mappedSenderId) {
|
||
notificationData.senderId = mappedSenderId
|
||
} else {
|
||
const senderExists = await tx
|
||
.select({ id: users.id, username: users.username })
|
||
.from(users)
|
||
.where(eq(users.id, record.senderId))
|
||
.limit(1)
|
||
notificationData.senderId =
|
||
senderExists.length > 0 &&
|
||
senderExists[0].username === record.senderUsername
|
||
? record.senderId
|
||
: null
|
||
}
|
||
} else {
|
||
notificationData.senderId = null
|
||
}
|
||
}
|
||
|
||
// 布尔字段,提供默认值
|
||
notificationData.read = record.hasOwnProperty('read') ? record.read : false
|
||
notificationData.important = Object.prototype.hasOwnProperty.call(
|
||
record,
|
||
'important'
|
||
)
|
||
? record.important
|
||
: false
|
||
|
||
// 日期字段
|
||
notificationData.createdAt = record.createdAt
|
||
? new Date(record.createdAt)
|
||
: new Date()
|
||
notificationData.updatedAt = record.updatedAt
|
||
? new Date(record.updatedAt)
|
||
: new Date()
|
||
|
||
if (mode === 'merge') {
|
||
// 检查是否存在相同的通知(按用户ID、类型和消息)
|
||
const existingNotification = await tx
|
||
.select()
|
||
.from(notifications)
|
||
.where(
|
||
and(
|
||
eq(notifications.userId, validNotificationUserId),
|
||
eq(notifications.type, record.type),
|
||
eq(notifications.message, record.message)
|
||
)
|
||
)
|
||
.limit(1)
|
||
|
||
if (existingNotification.length > 0) {
|
||
// 如果存在相同通知,更新它
|
||
await tx
|
||
.update(notifications)
|
||
.set(notificationData)
|
||
.where(eq(notifications.id, existingNotification[0].id))
|
||
} else {
|
||
// 如果不存在,创建新通知(不指定ID,让数据库自动生成)
|
||
await tx.insert(notifications).values(notificationData)
|
||
}
|
||
} else {
|
||
// 完全恢复模式,检查ID是否已存在
|
||
const existingNotificationWithId = await tx
|
||
.select()
|
||
.from(notifications)
|
||
.where(eq(notifications.id, record.id))
|
||
.limit(1)
|
||
|
||
if (existingNotificationWithId.length > 0) {
|
||
// ID已存在,使用upsert策略更新现有通知
|
||
console.warn(`通知ID ${record.id} 已存在,将更新现有通知`)
|
||
await tx
|
||
.update(notifications)
|
||
.set(notificationData)
|
||
.where(eq(notifications.id, record.id))
|
||
} else {
|
||
// ID不存在,使用原始ID创建
|
||
await tx.insert(notifications).values({
|
||
...notificationData,
|
||
id: record.id
|
||
})
|
||
}
|
||
}
|
||
break
|
||
|
||
case 'songBlacklist':
|
||
// 验证createdBy字段(可选)
|
||
let validCreatedBy = record.createdBy
|
||
|
||
if (record.createdBy) {
|
||
// 使用ID映射查找实际的用户ID
|
||
const mappedUserId = userIdMapping.get(record.createdBy)
|
||
if (mappedUserId) {
|
||
validCreatedBy = mappedUserId
|
||
} else {
|
||
// 尝试直接查找用户ID
|
||
const userExists = await tx
|
||
.select()
|
||
.from(users)
|
||
.where(eq(users.id, record.createdBy))
|
||
.limit(1)
|
||
if (userExists.length === 0) {
|
||
console.warn(
|
||
`黑名单记录的创建者ID ${record.createdBy} 不存在,将设为null`
|
||
)
|
||
validCreatedBy = null
|
||
}
|
||
}
|
||
}
|
||
|
||
// 构建黑名单数据
|
||
const blacklistData = {
|
||
type: record.type || 'KEYWORD', // 默认为关键词类型
|
||
value: record.value || '',
|
||
reason: record.reason || null,
|
||
isActive: record.hasOwnProperty('isActive') ? record.isActive : true,
|
||
createdBy: validCreatedBy,
|
||
createdAt: record.createdAt ? new Date(record.createdAt) : new Date(),
|
||
updatedAt: record.updatedAt ? new Date(record.updatedAt) : new Date()
|
||
}
|
||
|
||
if (mode === 'merge') {
|
||
// 检查是否存在相同的黑名单记录(相同类型和值)
|
||
const existingBlacklist = await tx
|
||
.select()
|
||
.from(songBlacklists)
|
||
.where(
|
||
and(
|
||
eq(songBlacklists.type, blacklistData.type),
|
||
eq(songBlacklists.value, blacklistData.value)
|
||
)
|
||
)
|
||
.limit(1)
|
||
|
||
if (existingBlacklist.length > 0) {
|
||
// 如果存在相同记录,更新它
|
||
await tx
|
||
.update(songBlacklists)
|
||
.set(blacklistData)
|
||
.where(eq(songBlacklists.id, existingBlacklist[0].id))
|
||
} else {
|
||
// 如果不存在,创建新记录(不指定ID,让数据库自动生成)
|
||
await tx.insert(songBlacklists).values(blacklistData)
|
||
}
|
||
} else {
|
||
// 完全恢复模式,检查ID是否已存在
|
||
const existingBlacklistWithId = await tx
|
||
.select()
|
||
.from(songBlacklists)
|
||
.where(eq(songBlacklists.id, record.id))
|
||
.limit(1)
|
||
|
||
if (existingBlacklistWithId.length > 0) {
|
||
// ID已存在,使用upsert策略更新现有记录
|
||
console.warn(`黑名单ID ${record.id} 已存在,将更新现有记录`)
|
||
await tx
|
||
.update(songBlacklists)
|
||
.set(blacklistData)
|
||
.where(eq(songBlacklists.id, record.id))
|
||
} else {
|
||
// ID不存在,使用原始ID创建
|
||
await tx.insert(songBlacklists).values({
|
||
...blacklistData,
|
||
id: record.id
|
||
})
|
||
}
|
||
}
|
||
break
|
||
|
||
case 'songCollaborators':
|
||
// 验证外键
|
||
let validCollabUserId = record.userId
|
||
let validCollabSongId = record.songId
|
||
|
||
if (record.userId) {
|
||
const mappedUserId = userIdMapping.get(record.userId)
|
||
if (mappedUserId) validCollabUserId = mappedUserId
|
||
}
|
||
|
||
if (record.songId) {
|
||
const mappedSongId = songIdMapping.get(record.songId)
|
||
if (mappedSongId) validCollabSongId = mappedSongId
|
||
}
|
||
|
||
const collabData = {
|
||
id: record.id, // UUID
|
||
songId: validCollabSongId,
|
||
userId: validCollabUserId,
|
||
status: record.status || 'PENDING',
|
||
createdAt: record.createdAt ? new Date(record.createdAt) : new Date(),
|
||
updatedAt: record.updatedAt ? new Date(record.updatedAt) : new Date()
|
||
}
|
||
|
||
// UUID 表直接插入,如果冲突则更新
|
||
await tx.insert(songCollaborators).values(collabData).onConflictDoUpdate({
|
||
target: songCollaborators.id,
|
||
set: collabData
|
||
})
|
||
break
|
||
|
||
case 'collaborationLogs':
|
||
// 验证外键 (collaboratorId 是 UUID,通常不变,但如果是重新生成的...)
|
||
// 这里假设 UUID 在 restore songCollaborators 时保持一致
|
||
const logData = {
|
||
id: record.id,
|
||
collaboratorId: record.collaboratorId,
|
||
action: record.action,
|
||
operatorId: record.operatorId, // 可能需要映射,但 operatorId 是 integer
|
||
ipAddress: record.ipAddress,
|
||
createdAt: record.createdAt ? new Date(record.createdAt) : new Date()
|
||
}
|
||
|
||
// 映射 operatorId
|
||
if (record.operatorId) {
|
||
const mappedOpId = userIdMapping.get(record.operatorId)
|
||
if (mappedOpId) logData.operatorId = mappedOpId
|
||
}
|
||
|
||
await tx.insert(collaborationLogs).values(logData).onConflictDoUpdate({
|
||
target: collaborationLogs.id,
|
||
set: logData
|
||
})
|
||
break
|
||
|
||
case 'songReplayRequests':
|
||
// 验证外键
|
||
let validReplayUserId = record.userId
|
||
let validReplaySongId = record.songId
|
||
|
||
if (record.userId) {
|
||
const mappedUserId = userIdMapping.get(record.userId)
|
||
if (mappedUserId) validReplayUserId = mappedUserId
|
||
}
|
||
|
||
if (record.songId) {
|
||
const mappedSongId = songIdMapping.get(record.songId)
|
||
if (mappedSongId) validReplaySongId = mappedSongId
|
||
}
|
||
|
||
// 校验期望播出时段是否存在(可选字段)
|
||
let validReplayPlayTimeId = record.preferredPlayTimeId || null
|
||
if (validReplayPlayTimeId) {
|
||
const replayPlayTimeExists = await tx
|
||
.select({ id: playTimes.id })
|
||
.from(playTimes)
|
||
.where(eq(playTimes.id, validReplayPlayTimeId))
|
||
.limit(1)
|
||
if (replayPlayTimeExists.length === 0) {
|
||
validReplayPlayTimeId = null
|
||
}
|
||
}
|
||
|
||
// 保留原始状态与申请元数据,避免历史已履行记录恢复为 PENDING 触发部分唯一索引冲突
|
||
const replayData = {
|
||
songId: validReplaySongId,
|
||
userId: validReplayUserId,
|
||
status: record.status || 'PENDING',
|
||
preferredPlayTimeId: validReplayPlayTimeId,
|
||
submissionNote: record.submissionNote ?? null,
|
||
submissionNotePublic: record.submissionNotePublic === true,
|
||
submissionNotePublicStatus: record.submissionNotePublicStatus ?? null,
|
||
createdAt: record.createdAt ? new Date(record.createdAt) : new Date(),
|
||
updatedAt: record.updatedAt ? new Date(record.updatedAt) : new Date()
|
||
}
|
||
|
||
if (mode === 'merge') {
|
||
await tx
|
||
.insert(songReplayRequests)
|
||
.values(replayData)
|
||
.onConflictDoNothing()
|
||
} else {
|
||
// 完全恢复模式
|
||
if (record.id) {
|
||
const existing = await tx
|
||
.select()
|
||
.from(songReplayRequests)
|
||
.where(eq(songReplayRequests.id, record.id))
|
||
.limit(1)
|
||
if (existing.length > 0) {
|
||
await tx
|
||
.update(songReplayRequests)
|
||
.set(replayData)
|
||
.where(eq(songReplayRequests.id, record.id))
|
||
} else {
|
||
await tx.insert(songReplayRequests).values({
|
||
...replayData,
|
||
id: record.id
|
||
})
|
||
}
|
||
} else {
|
||
await tx.insert(songReplayRequests).values(replayData)
|
||
}
|
||
}
|
||
break
|
||
|
||
case 'cardCodeRedeemLogs': {
|
||
let validCardCodeId = record.cardCodeId || null
|
||
if (record.cardCodeId) {
|
||
const mappedCardCodeId = cardCodeIdMapping.get(record.cardCodeId)
|
||
if (mappedCardCodeId) {
|
||
validCardCodeId = mappedCardCodeId
|
||
} else {
|
||
const cardCodeExists = await tx
|
||
.select({ id: cardCodes.id })
|
||
.from(cardCodes)
|
||
.where(eq(cardCodes.id, record.cardCodeId))
|
||
.limit(1)
|
||
if (cardCodeExists.length === 0) {
|
||
validCardCodeId = null
|
||
}
|
||
}
|
||
}
|
||
|
||
let validRedeemedBy = record.redeemedBy
|
||
if (record.redeemedBy) {
|
||
const mappedUserId = userIdMapping.get(record.redeemedBy)
|
||
if (mappedUserId) {
|
||
validRedeemedBy = mappedUserId
|
||
} else {
|
||
const userExists = await tx
|
||
.select({ id: users.id })
|
||
.from(users)
|
||
.where(eq(users.id, record.redeemedBy))
|
||
.limit(1)
|
||
if (userExists.length === 0) {
|
||
console.warn(
|
||
`点歌券日志的操作用户ID ${record.redeemedBy} 不存在,跳过此记录`
|
||
)
|
||
return
|
||
}
|
||
}
|
||
} else {
|
||
console.warn('点歌券日志缺少redeemedBy,跳过此记录')
|
||
return
|
||
}
|
||
|
||
let validLogSongId = record.songId || null
|
||
if (record.songId) {
|
||
const mappedSongId = songIdMapping.get(record.songId)
|
||
if (mappedSongId) {
|
||
validLogSongId = mappedSongId
|
||
} else {
|
||
const songExists = await tx
|
||
.select({ id: songs.id })
|
||
.from(songs)
|
||
.where(eq(songs.id, record.songId))
|
||
.limit(1)
|
||
if (songExists.length === 0) {
|
||
validLogSongId = null
|
||
}
|
||
}
|
||
}
|
||
|
||
const logData = {
|
||
cardCodeId: validCardCodeId,
|
||
codeSnapshot: record.codeSnapshot,
|
||
redeemedBy: validRedeemedBy,
|
||
redeemedAt: record.redeemedAt ? new Date(record.redeemedAt) : new Date(),
|
||
source: record.source || 'UNKNOWN',
|
||
songId: validLogSongId,
|
||
createdAt: record.createdAt ? new Date(record.createdAt) : new Date()
|
||
}
|
||
|
||
if (!logData.codeSnapshot) {
|
||
console.warn('点歌券日志缺少codeSnapshot,跳过此记录')
|
||
return
|
||
}
|
||
|
||
if (mode === 'merge') {
|
||
await tx.insert(cardCodeRedeemLogs).values(logData)
|
||
} else {
|
||
const existingLogWithId = await tx
|
||
.select()
|
||
.from(cardCodeRedeemLogs)
|
||
.where(eq(cardCodeRedeemLogs.id, record.id))
|
||
.limit(1)
|
||
if (existingLogWithId.length > 0) {
|
||
await tx
|
||
.update(cardCodeRedeemLogs)
|
||
.set(logData)
|
||
.where(eq(cardCodeRedeemLogs.id, record.id))
|
||
} else {
|
||
await tx
|
||
.insert(cardCodeRedeemLogs)
|
||
.values({ ...logData, id: record.id })
|
||
}
|
||
}
|
||
break
|
||
}
|
||
|
||
case 'votes':
|
||
// 验证外键约束
|
||
let validVoteUserId = record.userId
|
||
let validVoteSongId = record.songId
|
||
|
||
// 使用ID映射查找实际的用户ID
|
||
if (record.userId) {
|
||
const mappedUserId = userIdMapping.get(record.userId)
|
||
if (mappedUserId) {
|
||
validVoteUserId = mappedUserId
|
||
} else {
|
||
// 尝试直接查找用户ID
|
||
const userExists = await tx
|
||
.select()
|
||
.from(users)
|
||
.where(eq(users.id, record.userId))
|
||
.limit(1)
|
||
if (userExists.length === 0) {
|
||
console.warn(`投票记录的用户ID ${record.userId} 不存在,跳过此记录`)
|
||
return // 跳过此记录,因为userId是必需的
|
||
}
|
||
}
|
||
} else {
|
||
console.warn(`投票记录缺少userId,跳过此记录`)
|
||
return // 跳过此记录,因为userId是必需的
|
||
}
|
||
|
||
// 使用ID映射查找实际的歌曲ID
|
||
if (record.songId) {
|
||
const mappedSongId = songIdMapping.get(record.songId)
|
||
if (mappedSongId) {
|
||
validVoteSongId = mappedSongId
|
||
} else {
|
||
// 尝试直接查找歌曲ID
|
||
const songExists = await tx
|
||
.select()
|
||
.from(songs)
|
||
.where(eq(songs.id, record.songId))
|
||
.limit(1)
|
||
if (songExists.length === 0) {
|
||
console.warn(`投票记录的歌曲ID ${record.songId} 不存在,跳过此记录`)
|
||
return // 跳过此记录,因为songId是必需的
|
||
}
|
||
}
|
||
} else {
|
||
console.warn(`投票记录缺少songId,跳过此记录`)
|
||
return // 跳过此记录,因为songId是必需的
|
||
}
|
||
|
||
// 构建投票数据
|
||
const voteData = {
|
||
userId: validVoteUserId,
|
||
songId: validVoteSongId,
|
||
createdAt: record.createdAt ? new Date(record.createdAt) : new Date()
|
||
}
|
||
|
||
if (mode === 'merge') {
|
||
// 检查是否存在相同的投票(同一用户对同一歌曲的投票)
|
||
const existingVote = await tx
|
||
.select()
|
||
.from(votes)
|
||
.where(
|
||
and(
|
||
eq(votes.userId, validVoteUserId),
|
||
eq(votes.songId, validVoteSongId)
|
||
)
|
||
)
|
||
.limit(1)
|
||
|
||
if (existingVote.length > 0) {
|
||
// 如果存在相同投票,更新它
|
||
await tx
|
||
.update(votes)
|
||
.set(voteData)
|
||
.where(eq(votes.id, existingVote[0].id))
|
||
} else {
|
||
// 如果不存在,创建新投票(不指定ID,让数据库自动生成)
|
||
await tx.insert(votes).values(voteData)
|
||
}
|
||
} else {
|
||
// 完全恢复模式,检查是否存在相同的投票(同一用户对同一歌曲的投票)
|
||
const existingVote = await tx
|
||
.select()
|
||
.from(votes)
|
||
.where(
|
||
and(
|
||
eq(votes.userId, validVoteUserId),
|
||
eq(votes.songId, validVoteSongId)
|
||
)
|
||
)
|
||
.limit(1)
|
||
|
||
if (existingVote.length > 0) {
|
||
// 如果存在相同投票,更新它
|
||
await tx
|
||
.update(votes)
|
||
.set(voteData)
|
||
.where(eq(votes.id, existingVote[0].id))
|
||
} else {
|
||
// 如果不存在,创建新投票(不指定ID,让数据库自动生成)
|
||
await tx.insert(votes).values(voteData)
|
||
}
|
||
}
|
||
break
|
||
|
||
case 'scheduleSongPool': {
|
||
await restoreScheduleSongPoolRecord(tx, record, songIdMapping, userIdMapping, stats)
|
||
break
|
||
}
|
||
|
||
case 'requestTimes':
|
||
// requestTimes表没有外键依赖
|
||
const requestTimeData = {
|
||
name: record.name,
|
||
startTime: record.startTime ? new Date(record.startTime) : null,
|
||
endTime: record.endTime ? new Date(record.endTime) : null,
|
||
enabled: record.enabled !== undefined ? record.enabled : true,
|
||
description: record.description || null,
|
||
expected: record.expected || 0,
|
||
accepted: record.accepted || 0,
|
||
past: record.past !== undefined ? record.past : false,
|
||
createdAt: record.createdAt ? new Date(record.createdAt) : new Date(),
|
||
updatedAt: record.updatedAt ? new Date(record.updatedAt) : new Date()
|
||
}
|
||
|
||
if (mode === 'merge') {
|
||
// 检查是否存在相同名称的请求时段
|
||
const existingRequestTime = await tx
|
||
.select()
|
||
.from(requestTimes)
|
||
.where(eq(requestTimes.name, record.name))
|
||
.limit(1)
|
||
|
||
if (existingRequestTime.length > 0) {
|
||
// 如果存在,更新它
|
||
await tx
|
||
.update(requestTimes)
|
||
.set(requestTimeData)
|
||
.where(eq(requestTimes.id, existingRequestTime[0].id))
|
||
} else {
|
||
// 如果不存在,创建新记录
|
||
await tx.insert(requestTimes).values(requestTimeData)
|
||
}
|
||
} else {
|
||
// 完全恢复模式,检查ID是否已存在
|
||
if (record.id) {
|
||
const existingRequestTimeWithId = await tx
|
||
.select()
|
||
.from(requestTimes)
|
||
.where(eq(requestTimes.id, record.id))
|
||
.limit(1)
|
||
|
||
if (existingRequestTimeWithId.length > 0) {
|
||
// ID已存在,更新现有记录
|
||
await tx
|
||
.update(requestTimes)
|
||
.set(requestTimeData)
|
||
.where(eq(requestTimes.id, record.id))
|
||
} else {
|
||
// ID不存在,使用原始ID创建
|
||
await tx.insert(requestTimes).values({
|
||
...requestTimeData,
|
||
id: record.id
|
||
})
|
||
}
|
||
} else {
|
||
// 没有ID,创建新记录
|
||
await tx.insert(requestTimes).values(requestTimeData)
|
||
}
|
||
}
|
||
break
|
||
|
||
case 'emailTemplates':
|
||
// 验证外键约束 - updatedByUserId
|
||
let validEmailTemplateUpdatedByUserId = record.updatedByUserId
|
||
|
||
if (record.updatedByUserId) {
|
||
const mappedUserId = userIdMapping.get(record.updatedByUserId)
|
||
if (mappedUserId) {
|
||
validEmailTemplateUpdatedByUserId = mappedUserId
|
||
} else {
|
||
// 尝试直接查找用户ID
|
||
const userExists = await tx
|
||
.select()
|
||
.from(users)
|
||
.where(eq(users.id, record.updatedByUserId))
|
||
.limit(1)
|
||
if (userExists.length === 0) {
|
||
console.warn(
|
||
`邮件模板的更新者ID ${record.updatedByUserId} 不存在,将设置为null`
|
||
)
|
||
validEmailTemplateUpdatedByUserId = null
|
||
}
|
||
}
|
||
}
|
||
|
||
// 构建邮件模板数据
|
||
const emailTemplateData = {
|
||
key: record.key,
|
||
name: record.name,
|
||
subject: record.subject,
|
||
html: record.html,
|
||
updatedByUserId: validEmailTemplateUpdatedByUserId,
|
||
createdAt: record.createdAt ? new Date(record.createdAt) : new Date(),
|
||
updatedAt: record.updatedAt ? new Date(record.updatedAt) : new Date()
|
||
}
|
||
|
||
if (mode === 'merge') {
|
||
// 检查是否存在相同key的邮件模板
|
||
const existingEmailTemplate = await tx
|
||
.select()
|
||
.from(emailTemplates)
|
||
.where(eq(emailTemplates.key, record.key))
|
||
.limit(1)
|
||
|
||
if (existingEmailTemplate.length > 0) {
|
||
// 如果存在,更新它
|
||
await tx
|
||
.update(emailTemplates)
|
||
.set(emailTemplateData)
|
||
.where(eq(emailTemplates.id, existingEmailTemplate[0].id))
|
||
} else {
|
||
// 如果不存在,创建新记录
|
||
await tx.insert(emailTemplates).values(emailTemplateData)
|
||
}
|
||
} else {
|
||
// 完全恢复模式,检查ID是否已存在
|
||
if (record.id) {
|
||
const existingEmailTemplateWithId = await tx
|
||
.select()
|
||
.from(emailTemplates)
|
||
.where(eq(emailTemplates.id, record.id))
|
||
.limit(1)
|
||
|
||
if (existingEmailTemplateWithId.length > 0) {
|
||
// ID已存在,更新现有记录
|
||
await tx
|
||
.update(emailTemplates)
|
||
.set(emailTemplateData)
|
||
.where(eq(emailTemplates.id, record.id))
|
||
} else {
|
||
// ID不存在,使用原始ID创建
|
||
await tx.insert(emailTemplates).values({
|
||
...emailTemplateData,
|
||
id: record.id
|
||
})
|
||
}
|
||
} else {
|
||
// 没有ID,创建新记录
|
||
await tx.insert(emailTemplates).values(emailTemplateData)
|
||
}
|
||
}
|
||
break
|
||
|
||
case 'apiKeys':
|
||
// 验证外键约束 - createdByUserId
|
||
let validApiKeyCreatedByUserId = record.createdByUserId
|
||
|
||
if (record.createdByUserId) {
|
||
const mappedUserId = userIdMapping.get(record.createdByUserId)
|
||
if (mappedUserId) {
|
||
validApiKeyCreatedByUserId = mappedUserId
|
||
} else {
|
||
// 尝试直接查找用户ID
|
||
const userExists = await tx
|
||
.select()
|
||
.from(users)
|
||
.where(eq(users.id, record.createdByUserId))
|
||
.limit(1)
|
||
if (userExists.length === 0) {
|
||
console.warn(
|
||
`API密钥的创建者ID ${record.createdByUserId} 不存在,跳过此记录`
|
||
)
|
||
return // 跳过此记录,因为createdByUserId是必需的
|
||
}
|
||
}
|
||
} else {
|
||
console.warn(`API密钥缺少createdByUserId,跳过此记录`)
|
||
return // 跳过此记录,因为createdByUserId是必需的
|
||
}
|
||
|
||
// 构建API密钥数据
|
||
const apiKeyData = {
|
||
name: record.name,
|
||
description: record.description || null,
|
||
keyHash: record.keyHash,
|
||
keyPrefix: record.keyPrefix,
|
||
isActive: record.isActive !== undefined ? record.isActive : true,
|
||
expiresAt: record.expiresAt ? new Date(record.expiresAt) : null,
|
||
createdAt: record.createdAt ? new Date(record.createdAt) : new Date(),
|
||
updatedAt: record.updatedAt ? new Date(record.updatedAt) : new Date(),
|
||
lastUsedAt: record.lastUsedAt ? new Date(record.lastUsedAt) : null,
|
||
createdByUserId: validApiKeyCreatedByUserId,
|
||
usageCount: record.usageCount || 0
|
||
}
|
||
|
||
if (mode === 'merge') {
|
||
// 检查是否存在相同keyHash的API密钥
|
||
const existingApiKey = await tx
|
||
.select()
|
||
.from(apiKeys)
|
||
.where(eq(apiKeys.keyHash, record.keyHash))
|
||
.limit(1)
|
||
|
||
if (existingApiKey.length > 0) {
|
||
// 如果存在,更新它
|
||
await tx
|
||
.update(apiKeys)
|
||
.set(apiKeyData)
|
||
.where(eq(apiKeys.id, existingApiKey[0].id))
|
||
} else {
|
||
// 如果不存在,创建新记录
|
||
await tx.insert(apiKeys).values(apiKeyData)
|
||
}
|
||
} else {
|
||
// 完全恢复模式,检查ID是否已存在
|
||
if (record.id) {
|
||
const existingApiKeyWithId = await tx
|
||
.select()
|
||
.from(apiKeys)
|
||
.where(eq(apiKeys.id, record.id))
|
||
.limit(1)
|
||
|
||
if (existingApiKeyWithId.length > 0) {
|
||
// ID已存在,更新现有记录
|
||
await tx
|
||
.update(apiKeys)
|
||
.set(apiKeyData)
|
||
.where(eq(apiKeys.id, record.id))
|
||
} else {
|
||
// ID不存在,使用原始ID创建
|
||
await tx.insert(apiKeys).values({
|
||
...apiKeyData,
|
||
id: record.id
|
||
})
|
||
}
|
||
} else {
|
||
// 没有ID,创建新记录
|
||
await tx.insert(apiKeys).values(apiKeyData)
|
||
}
|
||
}
|
||
break
|
||
|
||
case 'apiKeyPermissions':
|
||
// 验证外键约束 - apiKeyId
|
||
const validApiKeyPermissionApiKeyId = record.apiKeyId
|
||
|
||
if (record.apiKeyId) {
|
||
// 尝试直接查找API密钥ID
|
||
const apiKeyExists = await tx
|
||
.select()
|
||
.from(apiKeys)
|
||
.where(eq(apiKeys.id, record.apiKeyId))
|
||
.limit(1)
|
||
if (apiKeyExists.length === 0) {
|
||
console.warn(
|
||
`API密钥权限的API密钥ID ${record.apiKeyId} 不存在,跳过此记录`
|
||
)
|
||
return // 跳过此记录,因为apiKeyId是必需的
|
||
}
|
||
} else {
|
||
console.warn(`API密钥权限缺少apiKeyId,跳过此记录`)
|
||
return // 跳过此记录,因为apiKeyId是必需的
|
||
}
|
||
|
||
// 构建API密钥权限数据
|
||
const apiKeyPermissionData = {
|
||
apiKeyId: validApiKeyPermissionApiKeyId,
|
||
permission: record.permission,
|
||
createdAt: record.createdAt ? new Date(record.createdAt) : new Date()
|
||
}
|
||
|
||
if (mode === 'merge') {
|
||
// 检查是否存在相同的API密钥权限
|
||
const existingApiKeyPermission = await tx
|
||
.select()
|
||
.from(apiKeyPermissions)
|
||
.where(
|
||
and(
|
||
eq(apiKeyPermissions.apiKeyId, validApiKeyPermissionApiKeyId),
|
||
eq(apiKeyPermissions.permission, record.permission)
|
||
)
|
||
)
|
||
.limit(1)
|
||
|
||
if (existingApiKeyPermission.length > 0) {
|
||
// 如果存在,更新它
|
||
await tx
|
||
.update(apiKeyPermissions)
|
||
.set(apiKeyPermissionData)
|
||
.where(eq(apiKeyPermissions.id, existingApiKeyPermission[0].id))
|
||
} else {
|
||
// 如果不存在,创建新记录
|
||
await tx.insert(apiKeyPermissions).values(apiKeyPermissionData)
|
||
}
|
||
} else {
|
||
// 完全恢复模式,检查ID是否已存在
|
||
if (record.id) {
|
||
const existingApiKeyPermissionWithId = await tx
|
||
.select()
|
||
.from(apiKeyPermissions)
|
||
.where(eq(apiKeyPermissions.id, record.id))
|
||
.limit(1)
|
||
|
||
if (existingApiKeyPermissionWithId.length > 0) {
|
||
// ID已存在,更新现有记录
|
||
await tx
|
||
.update(apiKeyPermissions)
|
||
.set(apiKeyPermissionData)
|
||
.where(eq(apiKeyPermissions.id, record.id))
|
||
} else {
|
||
// ID不存在,使用原始ID创建
|
||
await tx.insert(apiKeyPermissions).values({
|
||
...apiKeyPermissionData,
|
||
id: record.id
|
||
})
|
||
}
|
||
} else {
|
||
// 没有ID,创建新记录
|
||
await tx.insert(apiKeyPermissions).values(apiKeyPermissionData)
|
||
}
|
||
}
|
||
break
|
||
|
||
case 'apiLogs':
|
||
// 验证外键约束 - apiKeyId(可选)
|
||
let validApiLogApiKeyId = record.apiKeyId
|
||
|
||
if (record.apiKeyId) {
|
||
// 尝试直接查找API密钥ID
|
||
const apiKeyExists = await tx
|
||
.select()
|
||
.from(apiKeys)
|
||
.where(eq(apiKeys.id, record.apiKeyId))
|
||
.limit(1)
|
||
if (apiKeyExists.length === 0) {
|
||
console.warn(
|
||
`API日志的API密钥ID ${record.apiKeyId} 不存在,将设置为null`
|
||
)
|
||
validApiLogApiKeyId = null
|
||
}
|
||
}
|
||
|
||
// 构建API日志数据
|
||
const apiLogData = {
|
||
apiKeyId: validApiLogApiKeyId,
|
||
endpoint: record.endpoint,
|
||
method: record.method,
|
||
ipAddress: record.ipAddress,
|
||
userAgent: record.userAgent || null,
|
||
statusCode: record.statusCode,
|
||
responseTimeMs: record.responseTimeMs,
|
||
requestBody: record.requestBody || null,
|
||
responseBody: record.responseBody || null,
|
||
createdAt: record.createdAt ? new Date(record.createdAt) : new Date(),
|
||
errorMessage: record.errorMessage || null
|
||
}
|
||
|
||
// API日志通常不需要检查重复,直接创建新记录
|
||
await tx.insert(apiLogs).values(apiLogData)
|
||
break
|
||
|
||
// 其他表的处理逻辑...
|
||
default:
|
||
console.warn(`暂不支持恢复表: ${tableName}`)
|
||
return // 跳出当前事务,不处理此记录
|
||
}
|
||
},
|
||
{
|
||
timeout: 15000, // 每个记录事务15秒超时
|
||
maxWait: 3000 // 最大等待时间3秒
|
||
}
|
||
)
|
||
|
||
// 成功处理,跳出重试循环
|
||
restoredCount++
|
||
break
|
||
} catch (recordError) {
|
||
lastError = recordError
|
||
retryCount++
|
||
|
||
// 检查是否是可重试的错误
|
||
// Postgres 错误代码:
|
||
// 23505: unique_violation (唯一约束冲突)
|
||
// 23503: foreign_key_violation (外键约束冲突)
|
||
// 40001: serialization_failure (序列化失败)
|
||
// 40P01: deadlock_detected (死锁检测)
|
||
const isRetryableError =
|
||
recordError.code === '23505' || // 唯一约束冲突
|
||
recordError.code === '23503' || // 外键约束冲突
|
||
recordError.code === '40001' || // 序列化失败
|
||
recordError.code === '40P01' || // 死锁
|
||
recordError.message.includes('timeout') ||
|
||
recordError.message.includes('connection') ||
|
||
recordError.message.includes('SQLITE_BUSY') ||
|
||
recordError.message.includes('database is locked')
|
||
|
||
if (retryCount <= maxRetries && isRetryableError) {
|
||
console.warn(
|
||
`恢复记录失败,第 ${retryCount}/${maxRetries} 次重试 (${tableName}):`,
|
||
recordError.message
|
||
)
|
||
|
||
// 根据错误类型调整重试策略
|
||
if (recordError.code === '23505') {
|
||
// 唯一约束冲突,等待更长时间
|
||
await new Promise((resolve) => setTimeout(resolve, 1000 * retryCount))
|
||
} else {
|
||
// 其他错误,短暂等待
|
||
await new Promise((resolve) => setTimeout(resolve, 500 * retryCount))
|
||
}
|
||
} else {
|
||
// 不可重试的错误或重试次数用完
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果所有重试都失败了,记录错误
|
||
if (retryCount > maxRetries && lastError) {
|
||
console.error(`恢复记录最终失败 (${tableName}):`, lastError)
|
||
restoreResults.details.errors.push(
|
||
`${tableName}: ${lastError.message} (重试${maxRetries}次后失败)`
|
||
)
|
||
consecutiveErrors++
|
||
|
||
// 如果连续错误过多,减少批处理大小
|
||
if (consecutiveErrors >= 3 && batchSize > 1) {
|
||
batchSize = Math.max(1, Math.floor(batchSize / 2))
|
||
consecutiveErrors = 0
|
||
console.warn(`由于连续错误,将批处理大小调整为: ${batchSize}`)
|
||
}
|
||
} else if (retryCount <= maxRetries) {
|
||
// 成功处理,重置连续错误计数
|
||
consecutiveErrors = 0
|
||
}
|
||
}
|
||
|
||
// 每批处理完后输出进度
|
||
console.log(
|
||
`${tableName}: 已处理 ${Math.min(i + batchSize, tableData.length)}/${tableData.length} 条记录`
|
||
)
|
||
}
|
||
|
||
console.log(`✅ ${tableName}: 恢复了 ${restoredCount}/${tableData.length} 条记录`)
|
||
restoreResults.details.recordsRestored += restoredCount
|
||
restoreResults.details.tablesProcessed++
|
||
} catch (tableError) {
|
||
console.error(`恢复表 ${tableName} 失败:`, tableError)
|
||
restoreResults.details.errors.push(`表 ${tableName}: ${tableError.message}`)
|
||
}
|
||
}
|
||
|
||
console.log(`✅ 数据恢复完成`)
|
||
console.log(`📊 处理了 ${restoreResults.details.tablesProcessed} 个表`)
|
||
console.log(`📊 恢复了 ${restoreResults.details.recordsRestored} 条记录`)
|
||
|
||
// 防锁死:恢复开关前校验 SMTP 已配置,否则剥离 registerEmailRequired(避免恢复后注册必填邮箱却无法发码)
|
||
try {
|
||
const restoredSettings = await db.select().from(systemSettings).limit(1)
|
||
if (
|
||
restoredSettings[0]?.registerEmailRequired &&
|
||
(!restoredSettings[0]?.smtpEnabled || !restoredSettings[0]?.smtpHost)
|
||
) {
|
||
await db
|
||
.update(systemSettings)
|
||
.set({ registerEmailRequired: false })
|
||
.where(eq(systemSettings.id, restoredSettings[0].id))
|
||
console.warn('⚠️ SMTP 未配置,已剥离 registerEmailRequired,避免注册邮箱流程不可用')
|
||
}
|
||
} catch (settingsError) {
|
||
console.error('恢复后设置一致性校验失败:', settingsError)
|
||
}
|
||
|
||
// 重置所有自增序列
|
||
console.log(`🔄 开始重置自增序列...`)
|
||
const sequenceResetResults = []
|
||
const tablesToReset = [
|
||
'Song',
|
||
'User',
|
||
'UserIdentity',
|
||
'UserStatusLog',
|
||
'Vote',
|
||
'Schedule',
|
||
'Notification',
|
||
'NotificationSettings',
|
||
'PlayTime',
|
||
'Semester',
|
||
'SystemSettings',
|
||
'SongBlacklist',
|
||
'SongReplayRequest',
|
||
'RequestTime',
|
||
'EmailTemplate'
|
||
]
|
||
|
||
for (const tableName of tablesToReset) {
|
||
try {
|
||
// 调用 fix-sequence 端点
|
||
const response = await $fetch('/api/admin/fix-sequence', {
|
||
method: 'POST',
|
||
body: { table: tableName },
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
Authorization: getRequestHeader(event, 'authorization') || ''
|
||
}
|
||
})
|
||
|
||
if (response.success) {
|
||
sequenceResetResults.push(`${tableName}: 序列已重置`)
|
||
console.log(`✅ ${tableName} 序列重置成功`)
|
||
} else {
|
||
const errorMsg = `${tableName}: 序列重置失败 - ${response.error}`
|
||
sequenceResetResults.push(errorMsg)
|
||
console.warn(`⚠️ ${errorMsg}`)
|
||
}
|
||
} catch (sequenceError) {
|
||
const errorMsg = `${tableName}: 序列重置失败 - ${sequenceError.message}`
|
||
sequenceResetResults.push(errorMsg)
|
||
console.warn(`⚠️ ${errorMsg}`)
|
||
}
|
||
}
|
||
|
||
// 将序列重置结果添加到详情中
|
||
restoreResults.details.sequenceResets = sequenceResetResults
|
||
console.log(`✅ 序列重置完成`)
|
||
|
||
if (restoreResults.details.errors.length > 0) {
|
||
console.warn(`⚠️ 发生了 ${restoreResults.details.errors.length} 个错误`)
|
||
restoreResults.success = false
|
||
restoreResults.message = '数据恢复完成,但存在错误'
|
||
}
|
||
|
||
try {
|
||
const smtpService = SmtpService.getInstance()
|
||
const initialized = await smtpService.initializeSmtpConfig()
|
||
if (!initialized) {
|
||
restoreResults.details.warnings = restoreResults.details.warnings || []
|
||
restoreResults.details.warnings.push('SMTP未启用或配置不完整,SMTP实例已重置')
|
||
}
|
||
console.log('数据恢复后SMTP配置已重载')
|
||
} catch (smtpError) {
|
||
console.warn('重载SMTP配置失败:', smtpError)
|
||
restoreResults.details.warnings = restoreResults.details.warnings || []
|
||
restoreResults.details.warnings.push('重载SMTP配置失败')
|
||
}
|
||
|
||
return restoreResults
|
||
} catch (error) {
|
||
console.error('恢复数据库备份失败:', error)
|
||
throw createError({
|
||
statusCode: error.statusCode || 500,
|
||
message: error.message || '恢复数据库备份失败'
|
||
})
|
||
}
|
||
})
|