feat: 迁移自定义功能至 1.5.9.9 版本
包含音乐源限制、CI 防 OOM 优化、数据库连接池优化、管理端权限及 UI 细节定制等功能
This commit is contained in:
@@ -1,10 +1,6 @@
|
||||
import { createError, defineEventHandler, readBody } from 'h3'
|
||||
import { db } from '~/drizzle/db'
|
||||
import {
|
||||
cardCodeRedeemLogs,
|
||||
cardCodes,
|
||||
emailTemplates,
|
||||
gradeClass,
|
||||
notificationSettings,
|
||||
notifications,
|
||||
playTimes,
|
||||
@@ -19,21 +15,14 @@ import {
|
||||
votes
|
||||
} from '~/drizzle/schema'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { restoreScheduleSongPoolRecord } from '~~/server/utils/restoreScheduleSongPool'
|
||||
import { omitMaskedSystemSettingsSecrets } from '~~/server/api/admin/system-settings/secretMask'
|
||||
import { createApiError } from '~~/server/utils/apiError'
|
||||
import { validateThemeConfig } from '~~/server/utils/theme-config'
|
||||
import { SERVER_ERROR_CODES } from '~~/server/config/constants'
|
||||
import { normalizeScheduleVisibilitySettings } from '~~/server/utils/system-settings-defaults'
|
||||
import { syncAllSequences } from '~~/server/utils/sequence-sync'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
// 验证管理员权限
|
||||
const user = event.context.user
|
||||
if (!user || user.role !== 'SUPER_ADMIN') {
|
||||
if (!user || !['ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: '只有超级管理员可以恢复备份'
|
||||
statusMessage: '权限不足'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -50,14 +39,14 @@ export default defineEventHandler(async (event) => {
|
||||
if (overwriteSuperAdmin && user.role !== 'SUPER_ADMIN') {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: '仅超级管理员可以覆盖超级管理员账号数据'
|
||||
statusMessage: '仅超级管理员可以覆盖超级管理员账号数据'
|
||||
})
|
||||
}
|
||||
|
||||
if (!tableName || !records || !Array.isArray(records)) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '无效的请求参数'
|
||||
statusMessage: '无效的请求参数'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -68,9 +57,6 @@ export default defineEventHandler(async (event) => {
|
||||
const songIdMapping = new Map(
|
||||
Object.entries(mappings?.songs || {}).map(([k, v]) => [Number(k), Number(v)])
|
||||
)
|
||||
const cardCodeIdMapping = new Map(
|
||||
Object.entries(mappings?.cardCodes || {}).map(([k, v]) => [Number(k), Number(v)])
|
||||
)
|
||||
const preservedSuperAdminIds = new Set(
|
||||
(mappings?.meta?.preservedSuperAdminIds || []).map((id) => Number(id))
|
||||
)
|
||||
@@ -81,8 +67,7 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
const newMappings = {
|
||||
users: {},
|
||||
songs: {},
|
||||
cardCodes: {}
|
||||
songs: {}
|
||||
}
|
||||
|
||||
const stats = {
|
||||
@@ -114,10 +99,7 @@ export default defineEventHandler(async (event) => {
|
||||
'meowNickname',
|
||||
'forcePasswordChange',
|
||||
'status',
|
||||
'statusChangedBy',
|
||||
'remark',
|
||||
'avatarProvider',
|
||||
'avatarProviderUserId'
|
||||
'statusChangedBy'
|
||||
]
|
||||
const dateFields = [
|
||||
'createdAt',
|
||||
@@ -131,7 +113,7 @@ export default defineEventHandler(async (event) => {
|
||||
userFields.forEach((field) => {
|
||||
if (record.hasOwnProperty(field)) {
|
||||
if (field === 'role') {
|
||||
if (!['USER', 'SONG_ADMIN', 'ADMIN', 'SUPER_ADMIN'].includes(record[field])) {
|
||||
if (!['USER', 'TEACHER', 'SONG_ADMIN', 'ADMIN', 'SUPER_ADMIN'].includes(record[field])) {
|
||||
userData[field] = 'USER'
|
||||
} else {
|
||||
userData[field] = record[field]
|
||||
@@ -285,14 +267,10 @@ export default defineEventHandler(async (event) => {
|
||||
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))
|
||||
) {
|
||||
if (!shouldOverwriteSuperAdmin && preservedSuperAdminIds.has(Number(validIdentityUserId))) {
|
||||
stats.updated++
|
||||
break
|
||||
}
|
||||
@@ -352,12 +330,10 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
const userStatusLogData = {
|
||||
userId: validUserId,
|
||||
username: record.username ?? null,
|
||||
name: record.name ?? null,
|
||||
oldStatus: record.oldStatus || record.previousStatus || null,
|
||||
previousStatus: record.previousStatus || null,
|
||||
newStatus: record.newStatus,
|
||||
reason: record.reason || null,
|
||||
operatorId: record.operatorId || record.changedBy || null,
|
||||
changedBy: record.changedBy || null,
|
||||
createdAt: record.createdAt ? new Date(record.createdAt) : new Date()
|
||||
}
|
||||
|
||||
@@ -391,83 +367,6 @@ export default defineEventHandler(async (event) => {
|
||||
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) 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.query.users.findFirst({
|
||||
where: eq(users.id, record[field])
|
||||
})
|
||||
return userExists ? 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 existing = await tx.query.cardCodes.findFirst({
|
||||
where: eq(cardCodes.code, cardCodeData.code)
|
||||
})
|
||||
if (existing) {
|
||||
restoredCardCode = (
|
||||
await tx
|
||||
.update(cardCodes)
|
||||
.set(cardCodeData)
|
||||
.where(eq(cardCodes.id, existing.id))
|
||||
.returning()
|
||||
)[0]
|
||||
stats.updated++
|
||||
} else {
|
||||
restoredCardCode = (await tx.insert(cardCodes).values(cardCodeData).returning())[0]
|
||||
stats.created++
|
||||
}
|
||||
} else {
|
||||
const existing = await tx.query.cardCodes.findFirst({
|
||||
where: eq(cardCodes.id, record.id)
|
||||
})
|
||||
if (existing) {
|
||||
restoredCardCode = (
|
||||
await tx
|
||||
.update(cardCodes)
|
||||
.set(cardCodeData)
|
||||
.where(eq(cardCodes.id, record.id))
|
||||
.returning()
|
||||
)[0]
|
||||
stats.updated++
|
||||
} else {
|
||||
restoredCardCode = (
|
||||
await tx
|
||||
.insert(cardCodes)
|
||||
.values({ ...cardCodeData, id: record.id })
|
||||
.returning()
|
||||
)[0]
|
||||
stats.created++
|
||||
}
|
||||
}
|
||||
|
||||
if (record.id && restoredCardCode?.id) {
|
||||
newMappings.cardCodes[record.id] = restoredCardCode.id
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'songs': {
|
||||
let validRequesterId = record.requesterId
|
||||
let validPreferredPlayTimeId = record.preferredPlayTimeId
|
||||
@@ -498,25 +397,9 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
}
|
||||
|
||||
let validCardCodeId = record.cardCodeId || null
|
||||
if (record.cardCodeId) {
|
||||
const mappedCardCodeId = cardCodeIdMapping.get(record.cardCodeId)
|
||||
if (mappedCardCodeId) {
|
||||
validCardCodeId = mappedCardCodeId
|
||||
} else {
|
||||
const cardCodeExists = await tx.query.cardCodes.findFirst({
|
||||
where: eq(cardCodes.id, record.cardCodeId)
|
||||
})
|
||||
if (!cardCodeExists) {
|
||||
validCardCodeId = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const songData: any = {
|
||||
requesterId: validRequesterId,
|
||||
preferredPlayTimeId: validPreferredPlayTimeId,
|
||||
cardCodeId: validCardCodeId,
|
||||
played: record.hasOwnProperty('played') ? record.played : false
|
||||
}
|
||||
|
||||
@@ -527,10 +410,8 @@ export default defineEventHandler(async (event) => {
|
||||
'cover',
|
||||
'musicPlatform',
|
||||
'musicId',
|
||||
'durationSeconds',
|
||||
'submissionNote',
|
||||
'submissionNotePublic',
|
||||
'submissionNotePublicStatus'
|
||||
'submissionNotePublic'
|
||||
]
|
||||
songFields.forEach((field) => {
|
||||
if (record.hasOwnProperty(field)) {
|
||||
@@ -700,52 +581,10 @@ export default defineEventHandler(async (event) => {
|
||||
break
|
||||
}
|
||||
|
||||
case 'gradeClass': {
|
||||
const gradeClassData: any = {}
|
||||
const gradeClassFields = ['grade', 'class']
|
||||
gradeClassFields.forEach((field) => {
|
||||
if (record.hasOwnProperty(field)) {
|
||||
gradeClassData[field] =
|
||||
typeof record[field] === 'string' ? record[field].trim() : record[field]
|
||||
}
|
||||
})
|
||||
// 空行直接跳过,避免空值配置行锁死配置优先校验
|
||||
if (!gradeClassData.grade || !gradeClassData.class) {
|
||||
stats.warnings.push(`gradeClass 记录 ${record.id ?? ''} 年级或班级为空,已跳过`)
|
||||
break
|
||||
}
|
||||
|
||||
if (mode === 'merge') {
|
||||
const existingGradeClass = await tx.query.gradeClass.findFirst({
|
||||
where: and(
|
||||
eq(gradeClass.grade, gradeClassData.grade),
|
||||
eq(gradeClass.class, gradeClassData.class)
|
||||
)
|
||||
})
|
||||
if (!existingGradeClass) {
|
||||
await tx.insert(gradeClass).values(gradeClassData)
|
||||
stats.created++
|
||||
}
|
||||
} else {
|
||||
const existing = await tx.query.gradeClass.findFirst({
|
||||
where: eq(gradeClass.id, record.id)
|
||||
})
|
||||
if (!existing) {
|
||||
await tx.insert(gradeClass).values({ ...gradeClassData, id: record.id })
|
||||
stats.created++
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'systemSettings': {
|
||||
let systemSettingsData: any = {}
|
||||
const systemSettingsData: any = {}
|
||||
const fields = [
|
||||
'enablePlayTimeSelection',
|
||||
'instanceId',
|
||||
'defaultTheme',
|
||||
'enabledThemes',
|
||||
'telemetryEnabled',
|
||||
'siteTitle',
|
||||
'siteLogoUrl',
|
||||
'schoolLogoHomeUrl',
|
||||
@@ -758,25 +597,14 @@ export default defineEventHandler(async (event) => {
|
||||
'dailySubmissionLimit',
|
||||
'weeklySubmissionLimit',
|
||||
'monthlySubmissionLimit',
|
||||
'scheduleDaysBeforeEnabled',
|
||||
'scheduleDaysBefore',
|
||||
'scheduleDaysAfterEnabled',
|
||||
'scheduleDaysAfter',
|
||||
'showBlacklistKeywords',
|
||||
'hideStudentInfo',
|
||||
'enableReplayRequests',
|
||||
'enableCollaborativeSubmission',
|
||||
'enableSubmissionRemarks',
|
||||
'enableCardCodeRequests',
|
||||
'requireCardCodeForRequests',
|
||||
'enableCardCodeLimitBypass',
|
||||
'enableSubmissionRestriction',
|
||||
'submissionRestrictionScope',
|
||||
'sameSongRestrictionHours',
|
||||
'sameArtistRestrictionHours',
|
||||
'enableRequestTimeLimitation',
|
||||
'requestTimeLimitation',
|
||||
'forceBlockAllRequests',
|
||||
'forcePasswordChangeOnFirstLogin',
|
||||
'smtpEnabled',
|
||||
'smtpHost',
|
||||
'smtpPort',
|
||||
@@ -784,79 +612,11 @@ export default defineEventHandler(async (event) => {
|
||||
'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'
|
||||
'smtpFromName'
|
||||
]
|
||||
fields.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 existing = await tx.query.systemSettings.findFirst()
|
||||
@@ -888,49 +648,6 @@ export default defineEventHandler(async (event) => {
|
||||
break
|
||||
}
|
||||
|
||||
case 'emailTemplates': {
|
||||
// 邮件模板(按 key 去重:merge 模式同名 key 覆盖)
|
||||
if (!record.key || !record.name || !record.subject) return
|
||||
const templateData: any = {
|
||||
key: record.key,
|
||||
name: record.name,
|
||||
subject: record.subject,
|
||||
html: record.html || ''
|
||||
}
|
||||
if (record.contentType) templateData.contentType = record.contentType
|
||||
if (record.headerSubtitle) templateData.headerSubtitle = record.headerSubtitle
|
||||
if (record.actionText) templateData.actionText = record.actionText
|
||||
if (record.actionUrl) templateData.actionUrl = record.actionUrl
|
||||
if (record.updatedByUserId) templateData.updatedByUserId = record.updatedByUserId
|
||||
if (record.createdAt) templateData.createdAt = new Date(record.createdAt)
|
||||
if (record.updatedAt) templateData.updatedAt = new Date(record.updatedAt)
|
||||
|
||||
if (mode === 'merge') {
|
||||
const existing = await tx.query.emailTemplates.findFirst({
|
||||
where: eq(emailTemplates.key, record.key)
|
||||
})
|
||||
if (existing) {
|
||||
await tx.update(emailTemplates).set(templateData).where(eq(emailTemplates.id, existing.id))
|
||||
stats.updated++
|
||||
} else {
|
||||
await tx.insert(emailTemplates).values(templateData)
|
||||
stats.created++
|
||||
}
|
||||
} else {
|
||||
const existing = await tx.query.emailTemplates.findFirst({
|
||||
where: eq(emailTemplates.id, record.id)
|
||||
})
|
||||
if (existing) {
|
||||
await tx.update(emailTemplates).set(templateData).where(eq(emailTemplates.id, record.id))
|
||||
stats.updated++
|
||||
} else {
|
||||
await tx.insert(emailTemplates).values({ ...templateData, id: record.id })
|
||||
stats.created++
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'schedules': {
|
||||
if (!record.songId || !record.playDate) return
|
||||
|
||||
@@ -1085,43 +802,11 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
|
||||
const notificationData: any = { userId: validUserId }
|
||||
const fields = [
|
||||
'batchId',
|
||||
'source',
|
||||
'senderName',
|
||||
'senderUsername',
|
||||
'title',
|
||||
'message',
|
||||
'type',
|
||||
'userDeleted'
|
||||
]
|
||||
const fields = ['title', 'message', 'type']
|
||||
fields.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.query.users.findFirst({
|
||||
where: eq(users.id, record.senderId)
|
||||
})
|
||||
notificationData.senderId =
|
||||
senderExists && senderExists.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()
|
||||
|
||||
@@ -1219,79 +904,6 @@ export default defineEventHandler(async (event) => {
|
||||
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.query.cardCodes.findFirst({
|
||||
where: eq(cardCodes.id, record.cardCodeId)
|
||||
})
|
||||
if (!cardCodeExists) validCardCodeId = null
|
||||
}
|
||||
}
|
||||
|
||||
let validRedeemedBy = record.redeemedBy
|
||||
if (record.redeemedBy) {
|
||||
const mappedUserId = userIdMapping.get(record.redeemedBy)
|
||||
if (mappedUserId) {
|
||||
validRedeemedBy = mappedUserId
|
||||
} else {
|
||||
const userExists = await tx.query.users.findFirst({
|
||||
where: eq(users.id, record.redeemedBy)
|
||||
})
|
||||
if (!userExists) return
|
||||
}
|
||||
} else return
|
||||
|
||||
let validSongId = record.songId || null
|
||||
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) validSongId = null
|
||||
}
|
||||
}
|
||||
|
||||
const logData: any = {
|
||||
cardCodeId: validCardCodeId,
|
||||
codeSnapshot: record.codeSnapshot,
|
||||
redeemedBy: validRedeemedBy,
|
||||
redeemedAt: record.redeemedAt ? new Date(record.redeemedAt) : new Date(),
|
||||
source: record.source || 'UNKNOWN',
|
||||
songId: validSongId,
|
||||
createdAt: record.createdAt ? new Date(record.createdAt) : new Date()
|
||||
}
|
||||
|
||||
if (!logData.codeSnapshot) return
|
||||
|
||||
if (mode === 'merge') {
|
||||
await tx.insert(cardCodeRedeemLogs).values(logData)
|
||||
stats.created++
|
||||
} else {
|
||||
const existing = await tx.query.cardCodeRedeemLogs.findFirst({
|
||||
where: eq(cardCodeRedeemLogs.id, record.id)
|
||||
})
|
||||
if (existing) {
|
||||
await tx
|
||||
.update(cardCodeRedeemLogs)
|
||||
.set(logData)
|
||||
.where(eq(cardCodeRedeemLogs.id, record.id))
|
||||
stats.updated++
|
||||
} else {
|
||||
await tx.insert(cardCodeRedeemLogs).values({ ...logData, id: record.id })
|
||||
stats.created++
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'votes': {
|
||||
let validUserId = record.userId
|
||||
if (record.userId) {
|
||||
@@ -1337,11 +949,6 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'scheduleSongPool': {
|
||||
await restoreScheduleSongPoolRecord(tx, record, songIdMapping, userIdMapping, stats, () => { stats.created++ })
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
stats.processed++
|
||||
@@ -1352,35 +959,6 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 防锁死:恢复系统设置块后校验 SMTP 已配置,否则剥离 registerEmailRequired
|
||||
// (避免恢复后注册必填邮箱却无法发码)
|
||||
if (tableName === 'systemSettings') {
|
||||
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))
|
||||
stats.warnings.push('SMTP 未配置,已剥离 registerEmailRequired,避免注册邮箱流程不可用')
|
||||
}
|
||||
} catch (settingsError) {
|
||||
console.error('恢复后设置一致性校验失败:', settingsError)
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复可能携带显式 id,完成后同步所有自增序列,避免后续插入主键冲突
|
||||
try {
|
||||
await syncAllSequences()
|
||||
} catch (seqError) {
|
||||
const seqErrorMsg = seqError instanceof Error ? seqError.message : '未知错误'
|
||||
stats.warnings.push(`自增序列同步失败: ${seqErrorMsg}`)
|
||||
console.error('自增序列同步失败:', seqError)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
newMappings,
|
||||
|
||||
Reference in New Issue
Block a user