1390 lines
51 KiB
TypeScript
1390 lines
51 KiB
TypeScript
import { createError, defineEventHandler, readBody } from 'h3'
|
|
import { db } from '~/drizzle/db'
|
|
import {
|
|
cardCodeRedeemLogs,
|
|
cardCodes,
|
|
emailTemplates,
|
|
gradeClass,
|
|
notificationSettings,
|
|
notifications,
|
|
playTimes,
|
|
schedules,
|
|
semesters,
|
|
songBlacklists,
|
|
songs,
|
|
systemSettings,
|
|
userIdentities,
|
|
users,
|
|
userStatusLogs,
|
|
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') {
|
|
throw createError({
|
|
statusCode: 403,
|
|
message: '只有超级管理员可以恢复备份'
|
|
})
|
|
}
|
|
|
|
const body = await readBody(event)
|
|
const {
|
|
tableName,
|
|
records,
|
|
mappings,
|
|
mode = 'merge',
|
|
overwriteSuperAdmin = false,
|
|
hasSuperAdminInBackup = false
|
|
} = body
|
|
|
|
if (overwriteSuperAdmin && user.role !== 'SUPER_ADMIN') {
|
|
throw createError({
|
|
statusCode: 403,
|
|
message: '仅超级管理员可以覆盖超级管理员账号数据'
|
|
})
|
|
}
|
|
|
|
if (!tableName || !records || !Array.isArray(records)) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: '无效的请求参数'
|
|
})
|
|
}
|
|
|
|
// 转换映射对象为Map
|
|
const userIdMapping = new Map(
|
|
Object.entries(mappings?.users || {}).map(([k, v]) => [Number(k), Number(v)])
|
|
)
|
|
const songIdMapping = new Map(
|
|
Object.entries(mappings?.songs || {}).map(([k, v]) => [Number(k), Number(v)])
|
|
)
|
|
const 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))
|
|
)
|
|
const temporaryPreservedUserId = mappings?.meta?.temporaryPreservedUserId
|
|
? Number(mappings.meta.temporaryPreservedUserId)
|
|
: null
|
|
const shouldOverwriteSuperAdmin = overwriteSuperAdmin && hasSuperAdminInBackup
|
|
|
|
const newMappings = {
|
|
users: {},
|
|
songs: {},
|
|
cardCodes: {}
|
|
}
|
|
|
|
const stats = {
|
|
processed: 0,
|
|
created: 0,
|
|
updated: 0,
|
|
errors: 0,
|
|
warnings: []
|
|
}
|
|
|
|
console.log(`处理批次: ${tableName}, ${records.length} 条记录`)
|
|
|
|
for (const record of records) {
|
|
try {
|
|
await db.transaction(async (tx) => {
|
|
switch (tableName) {
|
|
case 'users': {
|
|
const buildUserData = (includePassword = false) => {
|
|
const userData: any = {}
|
|
const userFields = [
|
|
'username',
|
|
'name',
|
|
'grade',
|
|
'class',
|
|
'role',
|
|
'email',
|
|
'emailVerified',
|
|
'lastLoginIp',
|
|
'meowNickname',
|
|
'forcePasswordChange',
|
|
'status',
|
|
'statusChangedBy',
|
|
'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') {
|
|
const existingUser = await tx.query.users.findFirst({
|
|
where: eq(users.username, record.username)
|
|
})
|
|
|
|
if (existingUser) {
|
|
createdUser = (
|
|
await tx
|
|
.update(users)
|
|
.set(buildUserData(false))
|
|
.where(eq(users.username, record.username))
|
|
.returning()
|
|
)[0]
|
|
stats.updated++
|
|
} else {
|
|
try {
|
|
const existingUserWithId = await tx.query.users.findFirst({
|
|
where: eq(users.id, record.id)
|
|
})
|
|
|
|
if (existingUserWithId) {
|
|
createdUser = (
|
|
await tx.insert(users).values(buildUserData(true)).returning()
|
|
)[0]
|
|
} else {
|
|
createdUser = (
|
|
await tx
|
|
.insert(users)
|
|
.values({
|
|
...buildUserData(true),
|
|
id: record.id
|
|
})
|
|
.returning()
|
|
)[0]
|
|
}
|
|
} catch (error) {
|
|
console.warn(`用户 ${record.username} 使用原始ID创建失败,使用自动生成ID`)
|
|
createdUser = (await tx.insert(users).values(buildUserData(true)).returning())[0]
|
|
}
|
|
stats.created++
|
|
}
|
|
} else {
|
|
if (
|
|
shouldOverwriteSuperAdmin &&
|
|
temporaryPreservedUserId &&
|
|
Number(record.id) === temporaryPreservedUserId
|
|
) {
|
|
createdUser = (await tx.insert(users).values(buildUserData(true)).returning())[0]
|
|
stats.created++
|
|
if (record.id && createdUser?.id) {
|
|
newMappings.users[record.id] = createdUser.id
|
|
}
|
|
break
|
|
}
|
|
|
|
const isBackupSuperAdminRecord =
|
|
record.role === 'SUPER_ADMIN' || preservedSuperAdminIds.has(Number(record.id))
|
|
if (!shouldOverwriteSuperAdmin && isBackupSuperAdminRecord) {
|
|
if (record.id) {
|
|
const existingUserWithId = await tx.query.users.findFirst({
|
|
where: eq(users.id, record.id)
|
|
})
|
|
if (existingUserWithId) {
|
|
newMappings.users[record.id] = existingUserWithId.id
|
|
}
|
|
}
|
|
stats.updated++
|
|
break
|
|
}
|
|
|
|
const existingUserWithId = await tx.query.users.findFirst({
|
|
where: eq(users.id, record.id)
|
|
})
|
|
|
|
if (existingUserWithId) {
|
|
createdUser = (
|
|
await tx
|
|
.update(users)
|
|
.set(buildUserData(true))
|
|
.where(eq(users.id, record.id))
|
|
.returning()
|
|
)[0]
|
|
stats.updated++
|
|
} else {
|
|
createdUser = (
|
|
await tx
|
|
.insert(users)
|
|
.values({
|
|
...buildUserData(true),
|
|
id: record.id
|
|
})
|
|
.returning()
|
|
)[0]
|
|
stats.created++
|
|
}
|
|
}
|
|
|
|
if (record.id && createdUser.id) {
|
|
newMappings.users[record.id] = createdUser.id
|
|
}
|
|
break
|
|
}
|
|
|
|
case 'userIdentities': {
|
|
let validIdentityUserId = record.userId
|
|
if (record.userId) {
|
|
const mappedUserId = userIdMapping.get(record.userId)
|
|
if (mappedUserId) {
|
|
validIdentityUserId = mappedUserId
|
|
} else {
|
|
const userExists = await tx.query.users.findFirst({
|
|
where: eq(users.id, record.userId)
|
|
})
|
|
if (!userExists) {
|
|
console.warn(`身份关联记录的用户ID ${record.userId} 不存在,跳过`)
|
|
return
|
|
}
|
|
}
|
|
} else {
|
|
return
|
|
}
|
|
|
|
const identityData = {
|
|
userId: validIdentityUserId,
|
|
provider: record.provider,
|
|
providerUserId: record.providerUserId,
|
|
providerUsername: record.providerUsername,
|
|
avatar: record.avatar || null,
|
|
createdAt: record.createdAt ? new Date(record.createdAt) : new Date()
|
|
}
|
|
|
|
if (
|
|
!shouldOverwriteSuperAdmin &&
|
|
preservedSuperAdminIds.has(Number(validIdentityUserId))
|
|
) {
|
|
stats.updated++
|
|
break
|
|
}
|
|
|
|
if (mode === 'merge') {
|
|
await tx
|
|
.insert(userIdentities)
|
|
.values(identityData)
|
|
.onConflictDoUpdate({
|
|
target: [userIdentities.provider, userIdentities.providerUserId],
|
|
set: identityData
|
|
})
|
|
stats.updated++
|
|
} else if (record.id) {
|
|
const existingIdentityWithId = await tx.query.userIdentities.findFirst({
|
|
where: eq(userIdentities.id, record.id)
|
|
})
|
|
|
|
if (existingIdentityWithId) {
|
|
await tx
|
|
.update(userIdentities)
|
|
.set(identityData)
|
|
.where(eq(userIdentities.id, record.id))
|
|
stats.updated++
|
|
} else {
|
|
await tx.insert(userIdentities).values({
|
|
...identityData,
|
|
id: record.id
|
|
})
|
|
stats.created++
|
|
}
|
|
} else {
|
|
await tx.insert(userIdentities).values(identityData)
|
|
stats.created++
|
|
}
|
|
break
|
|
}
|
|
|
|
case 'userStatusLogs': {
|
|
let validUserId = record.userId
|
|
if (record.userId) {
|
|
const mappedUserId = userIdMapping.get(record.userId)
|
|
if (mappedUserId) {
|
|
validUserId = mappedUserId
|
|
} else {
|
|
const userExists = await tx.query.users.findFirst({
|
|
where: eq(users.id, record.userId)
|
|
})
|
|
if (!userExists) {
|
|
console.warn(`用户状态日志的用户ID ${record.userId} 不存在,跳过`)
|
|
return
|
|
}
|
|
}
|
|
} else {
|
|
return
|
|
}
|
|
|
|
const userStatusLogData = {
|
|
userId: validUserId,
|
|
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)
|
|
stats.created++
|
|
} else {
|
|
if (record.id) {
|
|
const existingLog = await tx.query.userStatusLogs.findFirst({
|
|
where: eq(userStatusLogs.id, record.id)
|
|
})
|
|
|
|
if (existingLog) {
|
|
await tx
|
|
.update(userStatusLogs)
|
|
.set(userStatusLogData)
|
|
.where(eq(userStatusLogs.id, record.id))
|
|
stats.updated++
|
|
} else {
|
|
await tx.insert(userStatusLogs).values({
|
|
...userStatusLogData,
|
|
id: record.id
|
|
})
|
|
stats.created++
|
|
}
|
|
} else {
|
|
await tx.insert(userStatusLogs).values(userStatusLogData)
|
|
stats.created++
|
|
}
|
|
}
|
|
break
|
|
}
|
|
|
|
case '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
|
|
|
|
if (record.requesterId) {
|
|
const mappedUserId = userIdMapping.get(record.requesterId)
|
|
if (mappedUserId) {
|
|
validRequesterId = mappedUserId
|
|
} else {
|
|
const userExists = await tx.query.users.findFirst({
|
|
where: eq(users.id, record.requesterId)
|
|
})
|
|
if (!userExists) {
|
|
console.warn(`歌曲请求者ID ${record.requesterId} 不存在,跳过`)
|
|
return
|
|
}
|
|
}
|
|
} else {
|
|
return
|
|
}
|
|
|
|
if (record.preferredPlayTimeId) {
|
|
const playTimeExists = await tx.query.playTimes.findFirst({
|
|
where: eq(playTimes.id, record.preferredPlayTimeId)
|
|
})
|
|
if (!playTimeExists) {
|
|
validPreferredPlayTimeId = null
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
const songFields = [
|
|
'title',
|
|
'artist',
|
|
'semester',
|
|
'cover',
|
|
'musicPlatform',
|
|
'musicId',
|
|
'durationSeconds',
|
|
'submissionNote',
|
|
'submissionNotePublic',
|
|
'submissionNotePublicStatus'
|
|
]
|
|
songFields.forEach((field) => {
|
|
if (record.hasOwnProperty(field)) {
|
|
songData[field] = record[field]
|
|
}
|
|
})
|
|
|
|
if (record.playedAt) songData.playedAt = new Date(record.playedAt)
|
|
else songData.playedAt = null
|
|
|
|
if (record.createdAt) songData.createdAt = new Date(record.createdAt)
|
|
else songData.createdAt = new Date()
|
|
|
|
if (record.updatedAt) songData.updatedAt = new Date(record.updatedAt)
|
|
else songData.updatedAt = new Date()
|
|
|
|
let createdSong
|
|
if (mode === 'merge') {
|
|
const existingSong = await tx.query.songs.findFirst({
|
|
where: and(eq(songs.title, record.title), eq(songs.artist, record.artist))
|
|
})
|
|
|
|
if (existingSong) {
|
|
createdSong = (
|
|
await tx
|
|
.update(songs)
|
|
.set(songData)
|
|
.where(eq(songs.id, existingSong.id))
|
|
.returning()
|
|
)[0]
|
|
stats.updated++
|
|
} else {
|
|
try {
|
|
const existingSongWithId = await tx.query.songs.findFirst({
|
|
where: eq(songs.id, record.id)
|
|
})
|
|
|
|
if (existingSongWithId) {
|
|
createdSong = (await tx.insert(songs).values(songData).returning())[0]
|
|
} else {
|
|
createdSong = (
|
|
await tx
|
|
.insert(songs)
|
|
.values({
|
|
...songData,
|
|
id: record.id
|
|
})
|
|
.returning()
|
|
)[0]
|
|
}
|
|
} catch (error) {
|
|
createdSong = (await tx.insert(songs).values(songData).returning())[0]
|
|
}
|
|
stats.created++
|
|
}
|
|
} else {
|
|
const existingSongWithId = await tx.query.songs.findFirst({
|
|
where: eq(songs.id, record.id)
|
|
})
|
|
|
|
if (existingSongWithId) {
|
|
createdSong = (
|
|
await tx.update(songs).set(songData).where(eq(songs.id, record.id)).returning()
|
|
)[0]
|
|
stats.updated++
|
|
} else {
|
|
createdSong = (
|
|
await tx
|
|
.insert(songs)
|
|
.values({
|
|
...songData,
|
|
id: record.id
|
|
})
|
|
.returning()
|
|
)[0]
|
|
stats.created++
|
|
}
|
|
}
|
|
|
|
if (record.id && createdSong.id) {
|
|
newMappings.songs[record.id] = createdSong.id
|
|
}
|
|
break
|
|
}
|
|
|
|
case 'playTimes': {
|
|
const playTimeData: any = {}
|
|
const playTimeFields = ['name', 'startTime', 'endTime', 'description']
|
|
playTimeFields.forEach((field) => {
|
|
if (record.hasOwnProperty(field)) {
|
|
playTimeData[field] = record[field]
|
|
}
|
|
})
|
|
playTimeData.enabled = record.hasOwnProperty('enabled') ? record.enabled : true
|
|
|
|
if (mode === 'merge') {
|
|
const existingPlayTime = await tx.query.playTimes.findFirst({
|
|
where: eq(playTimes.name, record.name)
|
|
})
|
|
|
|
if (existingPlayTime) {
|
|
await tx
|
|
.update(playTimes)
|
|
.set(playTimeData)
|
|
.where(eq(playTimes.id, existingPlayTime.id))
|
|
stats.updated++
|
|
} else {
|
|
await tx.insert(playTimes).values(playTimeData)
|
|
stats.created++
|
|
}
|
|
} else {
|
|
const existingPlayTimeWithId = await tx.query.playTimes.findFirst({
|
|
where: eq(playTimes.id, record.id)
|
|
})
|
|
|
|
if (existingPlayTimeWithId) {
|
|
await tx.update(playTimes).set(playTimeData).where(eq(playTimes.id, record.id))
|
|
stats.updated++
|
|
} else {
|
|
await tx.insert(playTimes).values({
|
|
...playTimeData,
|
|
id: record.id
|
|
})
|
|
stats.created++
|
|
}
|
|
}
|
|
break
|
|
}
|
|
|
|
case 'semesters': {
|
|
const semesterData: any = {}
|
|
const semesterFields = ['name']
|
|
semesterFields.forEach((field) => {
|
|
if (record.hasOwnProperty(field)) {
|
|
semesterData[field] = record[field]
|
|
}
|
|
})
|
|
semesterData.isActive = record.hasOwnProperty('isActive') ? record.isActive : false
|
|
if (record.createdAt) semesterData.createdAt = new Date(record.createdAt)
|
|
|
|
if (mode === 'merge') {
|
|
const existingSemester = await tx.query.semesters.findFirst({
|
|
where: eq(semesters.name, semesterData.name)
|
|
})
|
|
if (existingSemester) {
|
|
await tx
|
|
.update(semesters)
|
|
.set(semesterData)
|
|
.where(eq(semesters.id, existingSemester.id))
|
|
stats.updated++
|
|
} else {
|
|
await tx.insert(semesters).values(semesterData)
|
|
stats.created++
|
|
}
|
|
} else {
|
|
const existing = await tx.query.semesters.findFirst({
|
|
where: eq(semesters.id, record.id)
|
|
})
|
|
if (existing) {
|
|
await tx.update(semesters).set(semesterData).where(eq(semesters.id, record.id))
|
|
stats.updated++
|
|
} else {
|
|
await tx.insert(semesters).values({ ...semesterData, id: record.id })
|
|
stats.created++
|
|
}
|
|
}
|
|
break
|
|
}
|
|
|
|
case '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 fields = [
|
|
'enablePlayTimeSelection',
|
|
'instanceId',
|
|
'defaultTheme',
|
|
'enabledThemes',
|
|
'telemetryEnabled',
|
|
'siteTitle',
|
|
'siteLogoUrl',
|
|
'schoolLogoHomeUrl',
|
|
'schoolLogoPrintUrl',
|
|
'siteDescription',
|
|
'submissionGuidelines',
|
|
'icpNumber',
|
|
'gonganNumber',
|
|
'enableSubmissionLimit',
|
|
'dailySubmissionLimit',
|
|
'weeklySubmissionLimit',
|
|
'monthlySubmissionLimit',
|
|
'scheduleDaysBeforeEnabled',
|
|
'scheduleDaysBefore',
|
|
'scheduleDaysAfterEnabled',
|
|
'scheduleDaysAfter',
|
|
'showBlacklistKeywords',
|
|
'hideStudentInfo',
|
|
'enableReplayRequests',
|
|
'enableCollaborativeSubmission',
|
|
'enableSubmissionRemarks',
|
|
'enableCardCodeRequests',
|
|
'requireCardCodeForRequests',
|
|
'enableCardCodeLimitBypass',
|
|
'enableSubmissionRestriction',
|
|
'submissionRestrictionScope',
|
|
'sameSongRestrictionHours',
|
|
'sameArtistRestrictionHours',
|
|
'enableRequestTimeLimitation',
|
|
'forceBlockAllRequests',
|
|
'forcePasswordChangeOnFirstLogin',
|
|
'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'
|
|
]
|
|
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()
|
|
if (existing) {
|
|
await tx
|
|
.update(systemSettings)
|
|
.set(systemSettingsData)
|
|
.where(eq(systemSettings.id, existing.id))
|
|
stats.updated++
|
|
} else {
|
|
await tx.insert(systemSettings).values(systemSettingsData)
|
|
stats.created++
|
|
}
|
|
} else {
|
|
const existing = await tx.query.systemSettings.findFirst({
|
|
where: eq(systemSettings.id, record.id)
|
|
})
|
|
if (existing) {
|
|
await tx
|
|
.update(systemSettings)
|
|
.set(systemSettingsData)
|
|
.where(eq(systemSettings.id, record.id))
|
|
stats.updated++
|
|
} else {
|
|
await tx.insert(systemSettings).values({ ...systemSettingsData, id: record.id })
|
|
stats.created++
|
|
}
|
|
}
|
|
break
|
|
}
|
|
|
|
case '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
|
|
|
|
let validSongId = record.songId
|
|
const mappedSongId = songIdMapping.get(record.songId)
|
|
if (mappedSongId) {
|
|
validSongId = mappedSongId
|
|
} else {
|
|
const songExists = await tx.query.songs.findFirst({
|
|
where: eq(songs.id, record.songId)
|
|
})
|
|
if (!songExists) return
|
|
}
|
|
|
|
let validPlayTimeId = record.playTimeId
|
|
if (record.playTimeId) {
|
|
const ptExists = await tx.query.playTimes.findFirst({
|
|
where: eq(playTimes.id, record.playTimeId)
|
|
})
|
|
if (!ptExists) validPlayTimeId = null
|
|
}
|
|
|
|
const scheduleData: any = {
|
|
songId: validSongId,
|
|
playDate: new Date(record.playDate),
|
|
playTimeId: validPlayTimeId,
|
|
played: record.hasOwnProperty('played') ? record.played : false,
|
|
sequence: record.hasOwnProperty('sequence') ? record.sequence : 1
|
|
}
|
|
if (record.publishedAt) scheduleData.publishedAt = new Date(record.publishedAt)
|
|
if (record.isDraft !== undefined) scheduleData.isDraft = record.isDraft
|
|
|
|
if (mode === 'merge') {
|
|
const existing = await tx.query.schedules.findFirst({
|
|
where: and(
|
|
eq(schedules.songId, validSongId),
|
|
eq(schedules.playDate, scheduleData.playDate)
|
|
)
|
|
})
|
|
if (existing) {
|
|
await tx.update(schedules).set(scheduleData).where(eq(schedules.id, existing.id))
|
|
stats.updated++
|
|
} else {
|
|
await tx.insert(schedules).values(scheduleData)
|
|
stats.created++
|
|
}
|
|
} else {
|
|
const existing = await tx.query.schedules.findFirst({
|
|
where: eq(schedules.id, record.id)
|
|
})
|
|
if (existing) {
|
|
await tx.update(schedules).set(scheduleData).where(eq(schedules.id, record.id))
|
|
stats.updated++
|
|
} else {
|
|
await tx.insert(schedules).values({ ...scheduleData, id: record.id })
|
|
stats.created++
|
|
}
|
|
}
|
|
break
|
|
}
|
|
|
|
case 'notificationSettings': {
|
|
let validUserId = record.userId
|
|
if (record.userId) {
|
|
const mappedUserId = userIdMapping.get(record.userId)
|
|
if (mappedUserId) validUserId = mappedUserId
|
|
else {
|
|
const userExists = await tx.query.users.findFirst({
|
|
where: eq(users.id, record.userId)
|
|
})
|
|
if (!userExists) return
|
|
}
|
|
}
|
|
|
|
const notificationSettingsData: any = { userId: validUserId }
|
|
const fields = ['refreshInterval', 'songVotedThreshold', 'meowUserId']
|
|
fields.forEach((field) => {
|
|
if (record.hasOwnProperty(field)) notificationSettingsData[field] = record[field]
|
|
})
|
|
notificationSettingsData.enabled = record.hasOwnProperty('enabled')
|
|
? record.enabled
|
|
: true
|
|
notificationSettingsData.songRequestEnabled = record.hasOwnProperty(
|
|
'songRequestEnabled'
|
|
)
|
|
? record.songRequestEnabled
|
|
: true
|
|
notificationSettingsData.songVotedEnabled = record.hasOwnProperty('songVotedEnabled')
|
|
? record.songVotedEnabled
|
|
: true
|
|
notificationSettingsData.songPlayedEnabled = record.hasOwnProperty('songPlayedEnabled')
|
|
? record.songPlayedEnabled
|
|
: true
|
|
|
|
if (!notificationSettingsData.refreshInterval)
|
|
notificationSettingsData.refreshInterval = 60
|
|
if (!notificationSettingsData.songVotedThreshold)
|
|
notificationSettingsData.songVotedThreshold = 1
|
|
|
|
notificationSettingsData.createdAt = record.createdAt
|
|
? new Date(record.createdAt)
|
|
: new Date()
|
|
notificationSettingsData.updatedAt = record.updatedAt
|
|
? new Date(record.updatedAt)
|
|
: new Date()
|
|
|
|
if (mode === 'merge') {
|
|
const existing = await tx.query.notificationSettings.findFirst({
|
|
where: eq(notificationSettings.userId, validUserId)
|
|
})
|
|
if (existing) {
|
|
await tx
|
|
.update(notificationSettings)
|
|
.set(notificationSettingsData)
|
|
.where(eq(notificationSettings.id, existing.id))
|
|
stats.updated++
|
|
} else {
|
|
await tx.insert(notificationSettings).values(notificationSettingsData)
|
|
stats.created++
|
|
}
|
|
} else {
|
|
const existing = await tx.query.notificationSettings.findFirst({
|
|
where: eq(notificationSettings.id, record.id)
|
|
})
|
|
if (existing) {
|
|
await tx
|
|
.update(notificationSettings)
|
|
.set(notificationSettingsData)
|
|
.where(eq(notificationSettings.id, record.id))
|
|
stats.updated++
|
|
} else {
|
|
await tx
|
|
.insert(notificationSettings)
|
|
.values({ ...notificationSettingsData, id: record.id })
|
|
stats.created++
|
|
}
|
|
}
|
|
break
|
|
}
|
|
|
|
case 'notifications': {
|
|
let validUserId = record.userId
|
|
if (record.userId) {
|
|
const mappedUserId = userIdMapping.get(record.userId)
|
|
if (mappedUserId) validUserId = mappedUserId
|
|
else {
|
|
const userExists = await tx.query.users.findFirst({
|
|
where: eq(users.id, record.userId)
|
|
})
|
|
if (!userExists) return
|
|
}
|
|
}
|
|
|
|
const notificationData: any = { userId: validUserId }
|
|
const fields = [
|
|
'batchId',
|
|
'source',
|
|
'senderName',
|
|
'senderUsername',
|
|
'title',
|
|
'message',
|
|
'type',
|
|
'userDeleted'
|
|
]
|
|
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()
|
|
|
|
if (mode === 'merge') {
|
|
const existing = await tx.query.notifications.findFirst({
|
|
where: and(
|
|
eq(notifications.userId, validUserId),
|
|
eq(notifications.type, record.type),
|
|
eq(notifications.message, record.message)
|
|
)
|
|
})
|
|
if (existing) {
|
|
await tx
|
|
.update(notifications)
|
|
.set(notificationData)
|
|
.where(eq(notifications.id, existing.id))
|
|
stats.updated++
|
|
} else {
|
|
await tx.insert(notifications).values(notificationData)
|
|
stats.created++
|
|
}
|
|
} else {
|
|
const existing = await tx.query.notifications.findFirst({
|
|
where: eq(notifications.id, record.id)
|
|
})
|
|
if (existing) {
|
|
await tx
|
|
.update(notifications)
|
|
.set(notificationData)
|
|
.where(eq(notifications.id, record.id))
|
|
stats.updated++
|
|
} else {
|
|
await tx.insert(notifications).values({ ...notificationData, id: record.id })
|
|
stats.created++
|
|
}
|
|
}
|
|
break
|
|
}
|
|
|
|
case 'songBlacklist': {
|
|
let validCreatedBy = record.createdBy
|
|
if (record.createdBy) {
|
|
const mappedUserId = userIdMapping.get(record.createdBy)
|
|
if (mappedUserId) validCreatedBy = mappedUserId
|
|
else {
|
|
const userExists = await tx.query.users.findFirst({
|
|
where: eq(users.id, record.createdBy)
|
|
})
|
|
if (!userExists) validCreatedBy = null
|
|
}
|
|
}
|
|
|
|
const blacklistData: any = {
|
|
type: record.type || 'KEYWORD',
|
|
value: record.value || '',
|
|
reason: record.reason || null,
|
|
isActive: record.hasOwnProperty('isActive') ? record.isActive : true,
|
|
createdBy: validCreatedBy,
|
|
createdAt: record.createdAt ? new Date(record.createdAt) : new Date(),
|
|
updatedAt: record.updatedAt ? new Date(record.updatedAt) : new Date()
|
|
}
|
|
|
|
if (mode === 'merge') {
|
|
const existing = await tx.query.songBlacklists.findFirst({
|
|
where: and(
|
|
eq(songBlacklists.type, blacklistData.type),
|
|
eq(songBlacklists.value, blacklistData.value)
|
|
)
|
|
})
|
|
if (existing) {
|
|
await tx
|
|
.update(songBlacklists)
|
|
.set(blacklistData)
|
|
.where(eq(songBlacklists.id, existing.id))
|
|
stats.updated++
|
|
} else {
|
|
await tx.insert(songBlacklists).values(blacklistData)
|
|
stats.created++
|
|
}
|
|
} else {
|
|
const existing = await tx.query.songBlacklists.findFirst({
|
|
where: eq(songBlacklists.id, record.id)
|
|
})
|
|
if (existing) {
|
|
await tx
|
|
.update(songBlacklists)
|
|
.set(blacklistData)
|
|
.where(eq(songBlacklists.id, record.id))
|
|
stats.updated++
|
|
} else {
|
|
await tx.insert(songBlacklists).values({ ...blacklistData, id: record.id })
|
|
stats.created++
|
|
}
|
|
}
|
|
break
|
|
}
|
|
|
|
case '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) {
|
|
const mappedUserId = userIdMapping.get(record.userId)
|
|
if (mappedUserId) validUserId = mappedUserId
|
|
else {
|
|
const userExists = await tx.query.users.findFirst({
|
|
where: eq(users.id, record.userId)
|
|
})
|
|
if (!userExists) return
|
|
}
|
|
} else return
|
|
|
|
let validSongId = record.songId
|
|
if (record.songId) {
|
|
const mappedSongId = songIdMapping.get(record.songId)
|
|
if (mappedSongId) validSongId = mappedSongId
|
|
else {
|
|
const songExists = await tx.query.songs.findFirst({
|
|
where: eq(songs.id, record.songId)
|
|
})
|
|
if (!songExists) return
|
|
}
|
|
} else return
|
|
|
|
const voteData: any = {
|
|
userId: validUserId,
|
|
songId: validSongId,
|
|
createdAt: record.createdAt ? new Date(record.createdAt) : new Date()
|
|
}
|
|
|
|
// 对于投票,merge和replace模式下都需要检查重复,因为联合主键或唯一约束
|
|
const existing = await tx.query.votes.findFirst({
|
|
where: and(eq(votes.userId, validUserId), eq(votes.songId, validSongId))
|
|
})
|
|
|
|
if (existing) {
|
|
await tx.update(votes).set(voteData).where(eq(votes.id, existing.id))
|
|
stats.updated++
|
|
} else {
|
|
await tx.insert(votes).values(voteData)
|
|
stats.created++
|
|
}
|
|
break
|
|
}
|
|
|
|
case 'scheduleSongPool': {
|
|
await restoreScheduleSongPoolRecord(tx, record, songIdMapping, userIdMapping, stats, () => { stats.created++ })
|
|
break
|
|
}
|
|
}
|
|
})
|
|
stats.processed++
|
|
} catch (error) {
|
|
console.error(`处理记录失败 (${tableName}):`, error)
|
|
stats.errors++
|
|
stats.warnings.push(`记录处理失败: ${error.message}`)
|
|
}
|
|
}
|
|
|
|
// 防锁死:恢复系统设置块后校验 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,
|
|
stats
|
|
}
|
|
})
|