1128 lines
41 KiB
TypeScript
1128 lines
41 KiB
TypeScript
import { db } from '~/drizzle/db'
|
|
import { systemSettings } from '~/drizzle/schema'
|
|
import { eq } from 'drizzle-orm'
|
|
import { SMTP_PASSWORD_MASK, SECRET_FIELD_MASK, maskSystemSettingsSecrets } from './secretMask'
|
|
import { SYSTEM_SETTINGS_DEFAULTS } from '~~/server/utils/system-settings-defaults'
|
|
import {
|
|
getAggregateOAuthLoginTypesOrDefault,
|
|
isSafeAggregateOAuthUrl,
|
|
normalizeAggregateOAuthLoginTypes
|
|
} from '~~/server/utils/oauth-providers'
|
|
import { createApiError } from '~~/server/utils/apiError'
|
|
import { SERVER_ERROR_CODES, MUSIC_SOURCE_PLATFORMS, DEFAULT_THEMES } from '~~/server/config/constants'
|
|
import { parseThemeArray, validateThemeConfig } from '~~/server/utils/theme-config'
|
|
import { fetchGradeClassOptions } from '~~/server/utils/grade-class-options'
|
|
|
|
/**
|
|
* 解析数据库中存储的平台数组(历史脏数据/异常写入时回退默认值)
|
|
*/
|
|
const parsePlatformStored = (value: unknown): string[] => {
|
|
try {
|
|
const parsed = typeof value === 'string' ? JSON.parse(value) : value
|
|
return Array.isArray(parsed) && parsed.length > 0 ? (parsed as string[]) : [...MUSIC_SOURCE_PLATFORMS]
|
|
} catch {
|
|
return [...MUSIC_SOURCE_PLATFORMS]
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 校验平台数组 JSON 字符串
|
|
* @param fieldName 字段名(用于错误消息)
|
|
* @param value 原始值(应为 JSON 字符串)
|
|
* @param requireUnique 是否要求无重复(仅 platformOrder 需要)
|
|
* @returns 校验后的数组
|
|
*/
|
|
const validatePlatformArray = (fieldName: string, value: unknown, requireUnique = false): string[] => {
|
|
if (typeof value !== 'string') {
|
|
throw createApiError(
|
|
400,
|
|
SERVER_ERROR_CODES.COMMON_INVALID_PARAMS,
|
|
`${fieldName} 必须是 JSON 字符串`
|
|
)
|
|
}
|
|
|
|
let parsed: unknown
|
|
try {
|
|
parsed = JSON.parse(value)
|
|
} catch {
|
|
throw createApiError(
|
|
400,
|
|
SERVER_ERROR_CODES.COMMON_INVALID_PARAMS,
|
|
`${fieldName} 格式无效,应为合法 JSON 数组`
|
|
)
|
|
}
|
|
|
|
if (!Array.isArray(parsed)) {
|
|
throw createApiError(
|
|
400,
|
|
SERVER_ERROR_CODES.COMMON_INVALID_PARAMS,
|
|
`${fieldName} 必须是数组`
|
|
)
|
|
}
|
|
|
|
const invalid = parsed.filter((p: unknown) => !(MUSIC_SOURCE_PLATFORMS as readonly string[]).includes(p as string))
|
|
if (invalid.length > 0) {
|
|
throw createApiError(
|
|
400,
|
|
SERVER_ERROR_CODES.COMMON_INVALID_PARAMS,
|
|
`${fieldName} 包含无效的平台: ${(invalid as string[]).join(', ')}`
|
|
)
|
|
}
|
|
|
|
if (parsed.length === 0) {
|
|
throw createApiError(
|
|
400,
|
|
SERVER_ERROR_CODES.COMMON_INVALID_PARAMS,
|
|
`${fieldName} 至少保留一个平台`
|
|
)
|
|
}
|
|
|
|
if (requireUnique && parsed.length !== new Set(parsed as string[]).size) {
|
|
throw createApiError(
|
|
400,
|
|
SERVER_ERROR_CODES.COMMON_INVALID_PARAMS,
|
|
`${fieldName} 中不能包含重复的平台`
|
|
)
|
|
}
|
|
|
|
return parsed as string[]
|
|
}
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
// 检查用户认证和权限
|
|
const user = event.context.user
|
|
|
|
if (!user) {
|
|
throw createError({
|
|
statusCode: 401,
|
|
message: '未授权访问'
|
|
})
|
|
}
|
|
|
|
if (!['ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
|
|
throw createError({
|
|
statusCode: 403,
|
|
message: '只有管理员才能更新系统设置'
|
|
})
|
|
}
|
|
|
|
try {
|
|
const body = await readBody(event)
|
|
|
|
// 验证请求体
|
|
const updateData: any = {}
|
|
|
|
// 获取当前设置,用于验证依赖配置的完整性
|
|
const settingsResult = await db.select().from(systemSettings).limit(1)
|
|
let settings = settingsResult[0]
|
|
|
|
if (body.defaultTheme !== undefined || body.enabledThemes !== undefined) {
|
|
if (user.role !== 'SUPER_ADMIN') {
|
|
// 主题设置仅超级管理员可修改:普通管理员请求剥离主题字段,其余配置照常处理
|
|
delete body.defaultTheme
|
|
delete body.enabledThemes
|
|
} else {
|
|
const enabledThemes = body.enabledThemes !== undefined
|
|
? validateThemeConfig(body.defaultTheme ?? settings?.defaultTheme ?? 'System', body.enabledThemes)
|
|
: parseThemeArray(settings?.enabledThemes, DEFAULT_THEMES)
|
|
const defaultTheme = body.defaultTheme !== undefined ? body.defaultTheme : settings?.defaultTheme || 'System'
|
|
validateThemeConfig(defaultTheme, JSON.stringify(enabledThemes))
|
|
if (body.defaultTheme !== undefined) updateData.defaultTheme = defaultTheme
|
|
updateData.enabledThemes = JSON.stringify(enabledThemes)
|
|
}
|
|
}
|
|
|
|
if (body.telemetryEnabled !== undefined) {
|
|
if (typeof body.telemetryEnabled !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'telemetryEnabled 必须是布尔值'
|
|
})
|
|
}
|
|
updateData.telemetryEnabled = body.telemetryEnabled
|
|
}
|
|
|
|
if (body.hideStudentInfo !== undefined) {
|
|
if (typeof body.hideStudentInfo !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'hideStudentInfo 必须是布尔值'
|
|
})
|
|
}
|
|
updateData.hideStudentInfo = body.hideStudentInfo
|
|
}
|
|
|
|
if (body.enablePlayTimeSelection !== undefined) {
|
|
if (typeof body.enablePlayTimeSelection !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'enablePlayTimeSelection 必须是布尔值'
|
|
})
|
|
}
|
|
updateData.enablePlayTimeSelection = body.enablePlayTimeSelection
|
|
}
|
|
|
|
if (body.siteTitle !== undefined) {
|
|
updateData.siteTitle = body.siteTitle
|
|
}
|
|
|
|
if (body.siteLogoUrl !== undefined) {
|
|
updateData.siteLogoUrl = body.siteLogoUrl
|
|
}
|
|
|
|
if (body.schoolLogoHomeUrl !== undefined) {
|
|
updateData.schoolLogoHomeUrl = body.schoolLogoHomeUrl
|
|
}
|
|
|
|
if (body.schoolLogoPrintUrl !== undefined) {
|
|
updateData.schoolLogoPrintUrl = body.schoolLogoPrintUrl
|
|
}
|
|
|
|
if (body.siteDescription !== undefined) {
|
|
updateData.siteDescription = body.siteDescription
|
|
}
|
|
|
|
if (body.submissionGuidelines !== undefined) {
|
|
updateData.submissionGuidelines = body.submissionGuidelines
|
|
}
|
|
|
|
if (body.icpNumber !== undefined) {
|
|
updateData.icpNumber = body.icpNumber
|
|
}
|
|
|
|
if (body.gonganNumber !== undefined) {
|
|
updateData.gonganNumber = body.gonganNumber
|
|
}
|
|
|
|
if (body.statisticsCode !== undefined) {
|
|
if (body.statisticsCode !== null && typeof body.statisticsCode !== 'string') {
|
|
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, 'statisticsCode 必须是字符串或 null')
|
|
}
|
|
updateData.statisticsCode = body.statisticsCode
|
|
}
|
|
|
|
if (body.statisticsCodeEnabled !== undefined) {
|
|
if (typeof body.statisticsCodeEnabled !== 'boolean') {
|
|
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, 'statisticsCodeEnabled 必须是布尔值')
|
|
}
|
|
updateData.statisticsCodeEnabled = body.statisticsCodeEnabled
|
|
}
|
|
|
|
if (body.showBeianIcon !== undefined) {
|
|
if (typeof body.showBeianIcon !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'showBeianIcon 必须是布尔值'
|
|
})
|
|
}
|
|
updateData.showBeianIcon = body.showBeianIcon
|
|
}
|
|
|
|
if (body.enableSubmissionLimit !== undefined) {
|
|
if (typeof body.enableSubmissionLimit !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'enableSubmissionLimit 必须是布尔值'
|
|
})
|
|
}
|
|
updateData.enableSubmissionLimit = body.enableSubmissionLimit
|
|
}
|
|
|
|
// 点歌券点歌相关设置
|
|
if (body.enableCardCodeRequests !== undefined) {
|
|
if (typeof body.enableCardCodeRequests !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'enableCardCodeRequests 必须是布尔值'
|
|
})
|
|
}
|
|
updateData.enableCardCodeRequests = body.enableCardCodeRequests
|
|
}
|
|
|
|
if (body.requireCardCodeForRequests !== undefined) {
|
|
if (typeof body.requireCardCodeForRequests !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'requireCardCodeForRequests 必须是布尔值'
|
|
})
|
|
}
|
|
updateData.requireCardCodeForRequests = body.requireCardCodeForRequests
|
|
}
|
|
|
|
if (body.enableCardCodeLimitBypass !== undefined) {
|
|
if (typeof body.enableCardCodeLimitBypass !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'enableCardCodeLimitBypass 必须是布尔值'
|
|
})
|
|
}
|
|
updateData.enableCardCodeLimitBypass = body.enableCardCodeLimitBypass
|
|
}
|
|
|
|
// 重复投稿限制
|
|
if (body.enableSubmissionRestriction !== undefined) {
|
|
if (typeof body.enableSubmissionRestriction !== 'boolean') {
|
|
throw createApiError(
|
|
400,
|
|
SERVER_ERROR_CODES.COMMON_INVALID_PARAMS,
|
|
'enableSubmissionRestriction 必须是布尔值'
|
|
)
|
|
}
|
|
updateData.enableSubmissionRestriction = body.enableSubmissionRestriction
|
|
}
|
|
|
|
if (body.submissionRestrictionScope !== undefined) {
|
|
if (body.submissionRestrictionScope !== 'self' && body.submissionRestrictionScope !== 'all') {
|
|
throw createApiError(
|
|
400,
|
|
SERVER_ERROR_CODES.COMMON_INVALID_PARAMS,
|
|
'submissionRestrictionScope 必须是 self 或 all'
|
|
)
|
|
}
|
|
updateData.submissionRestrictionScope = body.submissionRestrictionScope
|
|
}
|
|
|
|
if (body.sameSongRestrictionHours !== undefined) {
|
|
if (
|
|
body.sameSongRestrictionHours !== null &&
|
|
(!Number.isInteger(body.sameSongRestrictionHours) || body.sameSongRestrictionHours < 1 || body.sameSongRestrictionHours > 720)
|
|
) {
|
|
throw createApiError(
|
|
400,
|
|
SERVER_ERROR_CODES.COMMON_INVALID_PARAMS,
|
|
'sameSongRestrictionHours 必须是 1-720 的正整数或 null'
|
|
)
|
|
}
|
|
updateData.sameSongRestrictionHours = body.sameSongRestrictionHours
|
|
}
|
|
|
|
if (body.sameArtistRestrictionHours !== undefined) {
|
|
if (
|
|
body.sameArtistRestrictionHours !== null &&
|
|
(!Number.isInteger(body.sameArtistRestrictionHours) || body.sameArtistRestrictionHours < 1 || body.sameArtistRestrictionHours > 720)
|
|
) {
|
|
throw createApiError(
|
|
400,
|
|
SERVER_ERROR_CODES.COMMON_INVALID_PARAMS,
|
|
'sameArtistRestrictionHours 必须是 1-720 的正整数或 null'
|
|
)
|
|
}
|
|
updateData.sameArtistRestrictionHours = body.sameArtistRestrictionHours
|
|
}
|
|
|
|
if (body.dailySubmissionLimit !== undefined) {
|
|
if (
|
|
body.dailySubmissionLimit !== null &&
|
|
(!Number.isInteger(body.dailySubmissionLimit) || body.dailySubmissionLimit < 0)
|
|
) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'dailySubmissionLimit 必须是非负整数或null'
|
|
})
|
|
}
|
|
updateData.dailySubmissionLimit = body.dailySubmissionLimit
|
|
}
|
|
|
|
if (body.weeklySubmissionLimit !== undefined) {
|
|
if (
|
|
body.weeklySubmissionLimit !== null &&
|
|
(!Number.isInteger(body.weeklySubmissionLimit) || body.weeklySubmissionLimit < 0)
|
|
) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'weeklySubmissionLimit 必须是非负整数或null'
|
|
})
|
|
}
|
|
updateData.weeklySubmissionLimit = body.weeklySubmissionLimit
|
|
}
|
|
|
|
if (body.monthlySubmissionLimit !== undefined) {
|
|
if (
|
|
body.monthlySubmissionLimit !== null &&
|
|
(!Number.isInteger(body.monthlySubmissionLimit) || body.monthlySubmissionLimit < 0)
|
|
) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'monthlySubmissionLimit 必须是非负整数或null'
|
|
})
|
|
}
|
|
updateData.monthlySubmissionLimit = body.monthlySubmissionLimit
|
|
}
|
|
|
|
if (body.scheduleDaysBeforeEnabled !== undefined) {
|
|
if (typeof body.scheduleDaysBeforeEnabled !== 'boolean') {
|
|
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, 'scheduleDaysBeforeEnabled 必须是布尔值')
|
|
}
|
|
updateData.scheduleDaysBeforeEnabled = body.scheduleDaysBeforeEnabled
|
|
}
|
|
|
|
if (body.scheduleDaysAfterEnabled !== undefined) {
|
|
if (typeof body.scheduleDaysAfterEnabled !== 'boolean') {
|
|
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, 'scheduleDaysAfterEnabled 必须是布尔值')
|
|
}
|
|
updateData.scheduleDaysAfterEnabled = body.scheduleDaysAfterEnabled
|
|
}
|
|
|
|
const nextScheduleDaysBeforeEnabled = body.scheduleDaysBeforeEnabled !== undefined
|
|
? body.scheduleDaysBeforeEnabled
|
|
: (settings?.scheduleDaysBeforeEnabled ?? false)
|
|
const nextScheduleDaysAfterEnabled = body.scheduleDaysAfterEnabled !== undefined
|
|
? body.scheduleDaysAfterEnabled
|
|
: (settings?.scheduleDaysAfterEnabled ?? false)
|
|
|
|
if (body.scheduleDaysBefore !== undefined) {
|
|
if (!Number.isInteger(body.scheduleDaysBefore) || body.scheduleDaysBefore < 1 || body.scheduleDaysBefore > 730) {
|
|
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, 'scheduleDaysBefore 必须是 1-730 的正整数')
|
|
}
|
|
updateData.scheduleDaysBefore = body.scheduleDaysBefore
|
|
}
|
|
|
|
if (body.scheduleDaysAfter !== undefined) {
|
|
if (!Number.isInteger(body.scheduleDaysAfter) || body.scheduleDaysAfter < 1 || body.scheduleDaysAfter > 730) {
|
|
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, 'scheduleDaysAfter 必须是 1-730 的正整数')
|
|
}
|
|
updateData.scheduleDaysAfter = body.scheduleDaysAfter
|
|
}
|
|
|
|
if (nextScheduleDaysBeforeEnabled && body.scheduleDaysBefore === undefined && !(settings?.scheduleDaysBefore >= 1)) {
|
|
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, '启用前方排期限制时,必须填写正整数天数')
|
|
}
|
|
if (nextScheduleDaysAfterEnabled && body.scheduleDaysAfter === undefined && !(settings?.scheduleDaysAfter >= 1)) {
|
|
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, '启用后方排期限制时,必须填写正整数天数')
|
|
}
|
|
|
|
if (body.showBlacklistKeywords !== undefined) {
|
|
if (typeof body.showBlacklistKeywords !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'showBlacklistKeywords 必须是布尔值'
|
|
})
|
|
}
|
|
updateData.showBlacklistKeywords = body.showBlacklistKeywords
|
|
}
|
|
|
|
if (body.enableReplayRequests !== undefined) {
|
|
if (typeof body.enableReplayRequests !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'enableReplayRequests 必须是布尔值'
|
|
})
|
|
}
|
|
updateData.enableReplayRequests = body.enableReplayRequests
|
|
}
|
|
|
|
if (body.enableCollaborativeSubmission !== undefined) {
|
|
if (typeof body.enableCollaborativeSubmission !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'enableCollaborativeSubmission 必须是布尔值'
|
|
})
|
|
}
|
|
updateData.enableCollaborativeSubmission = body.enableCollaborativeSubmission
|
|
}
|
|
|
|
if (body.enableSubmissionRemarks !== undefined) {
|
|
if (typeof body.enableSubmissionRemarks !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'enableSubmissionRemarks 必须是布尔值'
|
|
})
|
|
}
|
|
updateData.enableSubmissionRemarks = body.enableSubmissionRemarks
|
|
}
|
|
|
|
if (body.captchaEnabled !== undefined) {
|
|
if (typeof body.captchaEnabled !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'captchaEnabled 必须是布尔值'
|
|
})
|
|
}
|
|
updateData.captchaEnabled = body.captchaEnabled
|
|
}
|
|
|
|
if (body.captchaMaxFailures !== undefined) {
|
|
if (!Number.isInteger(body.captchaMaxFailures) || body.captchaMaxFailures < 1) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'captchaMaxFailures 必须是正整数'
|
|
})
|
|
}
|
|
updateData.captchaMaxFailures = body.captchaMaxFailures
|
|
}
|
|
|
|
if (body.captchaProvider !== undefined) {
|
|
if (body.captchaProvider !== 'graphic' && body.captchaProvider !== 'turnstile') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'captchaProvider 必须是 graphic 或 turnstile'
|
|
})
|
|
}
|
|
|
|
const nextTurnstileSiteKey =
|
|
body.turnstileSiteKey !== undefined ? body.turnstileSiteKey : settings?.turnstileSiteKey
|
|
const nextTurnstileSecretKey =
|
|
body.turnstileSecretKey !== undefined && body.turnstileSecretKey !== SECRET_FIELD_MASK
|
|
? body.turnstileSecretKey
|
|
: settings?.turnstileSecretKey
|
|
|
|
if (
|
|
body.captchaProvider === 'turnstile' &&
|
|
(!nextTurnstileSiteKey || !nextTurnstileSecretKey)
|
|
) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: '启用 Turnstile 验证前,请先配置 Site Key 和 Secret Key'
|
|
})
|
|
}
|
|
|
|
updateData.captchaProvider = body.captchaProvider
|
|
}
|
|
|
|
if (body.turnstileSiteKey !== undefined) {
|
|
updateData.turnstileSiteKey = body.turnstileSiteKey
|
|
}
|
|
|
|
if (body.turnstileSecretKey !== undefined && body.turnstileSecretKey !== SECRET_FIELD_MASK) {
|
|
updateData.turnstileSecretKey = body.turnstileSecretKey
|
|
}
|
|
|
|
if (body.enableRequestTimeLimitation !== undefined) {
|
|
if (typeof body.enableRequestTimeLimitation !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'enableRequestTimeLimitation 必须是布尔值'
|
|
})
|
|
}
|
|
updateData.enableRequestTimeLimitation = body.enableRequestTimeLimitation
|
|
}
|
|
|
|
if (body.forceBlockAllRequests !== undefined) {
|
|
if (typeof body.forceBlockAllRequests !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'forceBlockAllRequests 必须是布尔值'
|
|
})
|
|
}
|
|
updateData.forceBlockAllRequests = body.forceBlockAllRequests
|
|
}
|
|
|
|
if (body.forcePasswordChangeOnFirstLogin !== undefined) {
|
|
if (typeof body.forcePasswordChangeOnFirstLogin !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'forcePasswordChangeOnFirstLogin 必须是布尔值'
|
|
})
|
|
}
|
|
updateData.forcePasswordChangeOnFirstLogin = body.forcePasswordChangeOnFirstLogin
|
|
}
|
|
|
|
// 平台管理配置
|
|
if (body.enabledPlatforms !== undefined || body.platformOrder !== undefined) {
|
|
const enabled = body.enabledPlatforms !== undefined
|
|
? validatePlatformArray('enabledPlatforms', body.enabledPlatforms, true)
|
|
: parsePlatformStored(settings?.enabledPlatforms)
|
|
const order = body.platformOrder !== undefined
|
|
? validatePlatformArray('platformOrder', body.platformOrder, true)
|
|
: parsePlatformStored(settings?.platformOrder)
|
|
|
|
// 交叉一致性:排序中必须至少包含一个已启用的平台,避免“无可用平台”死锁
|
|
if (!order.some((p) => enabled.includes(p))) {
|
|
throw createApiError(
|
|
400,
|
|
SERVER_ERROR_CODES.COMMON_INVALID_PARAMS,
|
|
'platformOrder 必须包含至少一个已启用的平台'
|
|
)
|
|
}
|
|
|
|
if (body.enabledPlatforms !== undefined) {
|
|
updateData.enabledPlatforms = body.enabledPlatforms
|
|
}
|
|
if (body.platformOrder !== undefined) {
|
|
updateData.platformOrder = body.platformOrder
|
|
}
|
|
}
|
|
|
|
// SMTP配置字段
|
|
if (body.smtpEnabled !== undefined) {
|
|
if (typeof body.smtpEnabled !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'smtpEnabled 必须是布尔值'
|
|
})
|
|
}
|
|
updateData.smtpEnabled = body.smtpEnabled
|
|
}
|
|
|
|
if (body.smtpHost !== undefined) {
|
|
updateData.smtpHost = body.smtpHost
|
|
}
|
|
|
|
if (body.smtpPort !== undefined) {
|
|
if (
|
|
body.smtpPort !== null &&
|
|
(!Number.isInteger(body.smtpPort) || body.smtpPort < 1 || body.smtpPort > 65535)
|
|
) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'smtpPort 必须是1-65535之间的整数'
|
|
})
|
|
}
|
|
updateData.smtpPort = body.smtpPort
|
|
}
|
|
|
|
if (body.smtpSecure !== undefined) {
|
|
if (typeof body.smtpSecure !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'smtpSecure 必须是布尔值'
|
|
})
|
|
}
|
|
updateData.smtpSecure = body.smtpSecure
|
|
}
|
|
|
|
if (body.smtpUsername !== undefined) {
|
|
updateData.smtpUsername = body.smtpUsername
|
|
}
|
|
|
|
if (body.smtpPassword !== undefined && body.smtpPassword !== SMTP_PASSWORD_MASK) {
|
|
updateData.smtpPassword = body.smtpPassword
|
|
}
|
|
|
|
if (body.smtpFromEmail !== undefined) {
|
|
updateData.smtpFromEmail = body.smtpFromEmail
|
|
}
|
|
|
|
if (body.smtpFromName !== undefined) {
|
|
updateData.smtpFromName = body.smtpFromName
|
|
}
|
|
|
|
// OAuth 配置字段
|
|
if (body.allowOAuthRegistration !== undefined) {
|
|
if (typeof body.allowOAuthRegistration !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'allowOAuthRegistration 必须是布尔值'
|
|
})
|
|
}
|
|
updateData.allowOAuthRegistration = body.allowOAuthRegistration
|
|
}
|
|
|
|
// 注册配置字段
|
|
if (body.allowRegister !== undefined) {
|
|
if (typeof body.allowRegister !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'allowRegister 必须是布尔值'
|
|
})
|
|
}
|
|
updateData.allowRegister = body.allowRegister
|
|
}
|
|
|
|
if (body.registerRequiresApproval !== undefined) {
|
|
if (typeof body.registerRequiresApproval !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'registerRequiresApproval 必须是布尔值'
|
|
})
|
|
}
|
|
updateData.registerRequiresApproval = body.registerRequiresApproval
|
|
}
|
|
|
|
if (body.oauthRegisterRequiresApproval !== undefined) {
|
|
if (typeof body.oauthRegisterRequiresApproval !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'oauthRegisterRequiresApproval 必须是布尔值'
|
|
})
|
|
}
|
|
updateData.oauthRegisterRequiresApproval = body.oauthRegisterRequiresApproval
|
|
}
|
|
|
|
if (body.submissionNoteRequiresApproval !== undefined) {
|
|
if (typeof body.submissionNoteRequiresApproval !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'submissionNoteRequiresApproval 必须是布尔值'
|
|
})
|
|
}
|
|
updateData.submissionNoteRequiresApproval = body.submissionNoteRequiresApproval
|
|
}
|
|
|
|
// 注册子开关(邮箱必填 / 年级班级必填)以注册入口开启为前提:
|
|
// 两个入口均关闭时子开关无实际作用,直接落 false,避免整单保存被拒且子开关已隐藏无法取消
|
|
const registerEntryOpen = Boolean(
|
|
body.allowRegister ?? settings?.allowRegister
|
|
) || Boolean(body.allowOAuthRegistration ?? settings?.allowOAuthRegistration)
|
|
|
|
if (body.registerEmailRequired !== undefined) {
|
|
if (typeof body.registerEmailRequired !== 'boolean') {
|
|
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, 'registerEmailRequired 必须是布尔值')
|
|
}
|
|
// 开启时 SMTP 必须已配置并启用(合并提交值校验)
|
|
const nextEmailRequired = registerEntryOpen && body.registerEmailRequired
|
|
if (nextEmailRequired) {
|
|
const smtpEnabled = body.smtpEnabled ?? settings?.smtpEnabled
|
|
const smtpHost = body.smtpHost ?? settings?.smtpHost
|
|
if (!smtpEnabled || !smtpHost) {
|
|
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, '请先配置并开启 SMTP 邮件服务,再启用注册邮箱必填')
|
|
}
|
|
}
|
|
updateData.registerEmailRequired = nextEmailRequired
|
|
}
|
|
|
|
if (body.registerRequiresGradeClass !== undefined) {
|
|
if (typeof body.registerRequiresGradeClass !== 'boolean') {
|
|
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, 'registerRequiresGradeClass 必须是布尔值')
|
|
}
|
|
// 开启时系统内必须已有可选年级班级组合,否则注册表单无项可选、注册请求全部被拒
|
|
const nextGradeClassRequired = registerEntryOpen && body.registerRequiresGradeClass
|
|
if (nextGradeClassRequired && (await fetchGradeClassOptions()).length === 0) {
|
|
throw createApiError(
|
|
400,
|
|
SERVER_ERROR_CODES.SETTINGS_GRADE_CLASS_OPTIONS_MISSING,
|
|
'请先在年级班级管理中配置年级班级,再启用注册时必须选择年级班级'
|
|
)
|
|
}
|
|
updateData.registerRequiresGradeClass = nextGradeClassRequired
|
|
}
|
|
|
|
if (body.oauthRedirectUri !== undefined) {
|
|
const normalizedOauthRedirectUri =
|
|
typeof body.oauthRedirectUri === 'string'
|
|
? body.oauthRedirectUri.trim()
|
|
: body.oauthRedirectUri
|
|
|
|
if (normalizedOauthRedirectUri !== null && normalizedOauthRedirectUri !== '') {
|
|
try {
|
|
const uri = new URL(normalizedOauthRedirectUri)
|
|
// 支持 broker 回调 (包含 /callback 或 /api/auth/[provider]/callback 结构)
|
|
const validPathPattern = /(?:\/api)?\/auth\/[^/]+\/callback\/?$|\/callback\/?$/
|
|
if (!validPathPattern.test(uri.pathname)) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message:
|
|
'oauthRedirectUri 必须是回调地址,例如 https://yourdomain.com/api/auth/[provider]/callback'
|
|
})
|
|
}
|
|
} catch (error: any) {
|
|
if (error?.statusCode === 400) throw error
|
|
throw createError({
|
|
statusCode: 400,
|
|
message:
|
|
'oauthRedirectUri 不是合法URL,示例:https://yourdomain.com/api/auth/[provider]/callback'
|
|
})
|
|
}
|
|
}
|
|
updateData.oauthRedirectUri =
|
|
normalizedOauthRedirectUri === '' ? null : normalizedOauthRedirectUri
|
|
}
|
|
|
|
if (body.oauthStateSecret !== undefined && body.oauthStateSecret !== SECRET_FIELD_MASK) {
|
|
updateData.oauthStateSecret = body.oauthStateSecret
|
|
}
|
|
|
|
const nextOauthRedirectUri =
|
|
body.oauthRedirectUri !== undefined ? updateData.oauthRedirectUri : settings?.oauthRedirectUri
|
|
const nextOauthStateSecret =
|
|
body.oauthStateSecret !== undefined && body.oauthStateSecret !== SECRET_FIELD_MASK
|
|
? body.oauthStateSecret
|
|
: settings?.oauthStateSecret
|
|
const nextGithubOAuthEnabled =
|
|
body.githubOAuthEnabled !== undefined
|
|
? body.githubOAuthEnabled
|
|
: (settings?.githubOAuthEnabled ?? false)
|
|
const nextCasdoorOAuthEnabled =
|
|
body.casdoorOAuthEnabled !== undefined
|
|
? body.casdoorOAuthEnabled
|
|
: (settings?.casdoorOAuthEnabled ?? false)
|
|
const nextGoogleOAuthEnabled =
|
|
body.googleOAuthEnabled !== undefined
|
|
? body.googleOAuthEnabled
|
|
: (settings?.googleOAuthEnabled ?? false)
|
|
const nextAggregateOAuthEnabled =
|
|
body.aggregateOAuthEnabled !== undefined
|
|
? body.aggregateOAuthEnabled
|
|
: (settings?.aggregateOAuthEnabled ?? false)
|
|
const nextCustomOAuthEnabled =
|
|
body.customOAuthEnabled !== undefined
|
|
? body.customOAuthEnabled
|
|
: (settings?.customOAuthEnabled ?? false)
|
|
|
|
if (
|
|
nextGithubOAuthEnabled ||
|
|
nextCasdoorOAuthEnabled ||
|
|
nextGoogleOAuthEnabled ||
|
|
nextAggregateOAuthEnabled ||
|
|
nextCustomOAuthEnabled
|
|
) {
|
|
if (!nextOauthRedirectUri || !nextOauthStateSecret) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message:
|
|
'启用 OAuth 登录方式前,请先在管理员后台配置 OAuth 重定向 URI 和 OAuth State 密钥'
|
|
})
|
|
}
|
|
}
|
|
|
|
// GitHub OAuth
|
|
if (body.githubOAuthEnabled !== undefined) {
|
|
if (typeof body.githubOAuthEnabled !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'githubOAuthEnabled 必须是布尔值'
|
|
})
|
|
}
|
|
if (body.githubOAuthEnabled && !body.githubClientId && !settings?.githubClientId) {
|
|
throw createError({ statusCode: 400, message: '启用 GitHub 登录时必须提供 Client ID' })
|
|
}
|
|
if (body.githubOAuthEnabled && !body.githubClientSecret && !settings?.githubClientSecret) {
|
|
throw createError({ statusCode: 400, message: '启用 GitHub 登录时必须提供 Client Secret' })
|
|
}
|
|
updateData.githubOAuthEnabled = body.githubOAuthEnabled
|
|
}
|
|
|
|
if (body.githubClientId !== undefined) {
|
|
updateData.githubClientId = body.githubClientId
|
|
}
|
|
|
|
if (body.githubClientSecret !== undefined && body.githubClientSecret !== SECRET_FIELD_MASK) {
|
|
updateData.githubClientSecret = body.githubClientSecret
|
|
}
|
|
|
|
// Casdoor OAuth
|
|
if (body.casdoorOAuthEnabled !== undefined) {
|
|
if (typeof body.casdoorOAuthEnabled !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'casdoorOAuthEnabled 必须是布尔值'
|
|
})
|
|
}
|
|
if (body.casdoorOAuthEnabled && !body.casdoorServerUrl && !settings?.casdoorServerUrl) {
|
|
throw createError({ statusCode: 400, message: '启用 Casdoor 登录时必须提供服务器 URL' })
|
|
}
|
|
if (body.casdoorOAuthEnabled && !body.casdoorClientId && !settings?.casdoorClientId) {
|
|
throw createError({ statusCode: 400, message: '启用 Casdoor 登录时必须提供 Client ID' })
|
|
}
|
|
if (body.casdoorOAuthEnabled && !body.casdoorClientSecret && !settings?.casdoorClientSecret) {
|
|
throw createError({ statusCode: 400, message: '启用 Casdoor 登录时必须提供 Client Secret' })
|
|
}
|
|
if (
|
|
body.casdoorOAuthEnabled &&
|
|
!body.casdoorOrganizationName &&
|
|
!settings?.casdoorOrganizationName
|
|
) {
|
|
throw createError({ statusCode: 400, message: '启用 Casdoor 登录时必须提供组织名称' })
|
|
}
|
|
updateData.casdoorOAuthEnabled = body.casdoorOAuthEnabled
|
|
}
|
|
|
|
if (body.casdoorServerUrl !== undefined) {
|
|
updateData.casdoorServerUrl = body.casdoorServerUrl
|
|
}
|
|
|
|
if (body.casdoorClientId !== undefined) {
|
|
updateData.casdoorClientId = body.casdoorClientId
|
|
}
|
|
|
|
if (body.casdoorClientSecret !== undefined && body.casdoorClientSecret !== SECRET_FIELD_MASK) {
|
|
updateData.casdoorClientSecret = body.casdoorClientSecret
|
|
}
|
|
|
|
if (body.casdoorOrganizationName !== undefined) {
|
|
updateData.casdoorOrganizationName = body.casdoorOrganizationName
|
|
}
|
|
|
|
// Google OAuth
|
|
if (body.googleOAuthEnabled !== undefined) {
|
|
if (typeof body.googleOAuthEnabled !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'googleOAuthEnabled 必须是布尔值'
|
|
})
|
|
}
|
|
if (body.googleOAuthEnabled && !body.googleClientId && !settings?.googleClientId) {
|
|
throw createError({ statusCode: 400, message: '启用 Google 登录时必须提供 Client ID' })
|
|
}
|
|
if (body.googleOAuthEnabled && !body.googleClientSecret && !settings?.googleClientSecret) {
|
|
throw createError({ statusCode: 400, message: '启用 Google 登录时必须提供 Client Secret' })
|
|
}
|
|
updateData.googleOAuthEnabled = body.googleOAuthEnabled
|
|
}
|
|
|
|
if (body.googleClientId !== undefined) {
|
|
updateData.googleClientId = body.googleClientId
|
|
}
|
|
|
|
if (body.googleClientSecret !== undefined && body.googleClientSecret !== SECRET_FIELD_MASK) {
|
|
updateData.googleClientSecret = body.googleClientSecret
|
|
}
|
|
|
|
// 聚合登陆
|
|
const normalizeOptionalText = (value: any) => (typeof value === 'string' ? value.trim() : value)
|
|
const storedAggregateLoginTypes = getAggregateOAuthLoginTypesOrDefault(
|
|
settings?.aggregateOAuthLoginType
|
|
)
|
|
const nextAggregateLoginTypes =
|
|
body.aggregateOAuthLoginType !== undefined
|
|
? normalizeAggregateOAuthLoginTypes(body.aggregateOAuthLoginType)
|
|
: storedAggregateLoginTypes
|
|
const nextAggregateAppId =
|
|
body.aggregateOAuthAppId !== undefined
|
|
? normalizeOptionalText(body.aggregateOAuthAppId)
|
|
: settings?.aggregateOAuthAppId
|
|
const nextAggregateAppKey =
|
|
body.aggregateOAuthAppKey !== undefined && body.aggregateOAuthAppKey !== SECRET_FIELD_MASK
|
|
? normalizeOptionalText(body.aggregateOAuthAppKey)
|
|
: settings?.aggregateOAuthAppKey
|
|
const nextAggregateEndpoint =
|
|
body.aggregateOAuthEndpoint !== undefined
|
|
? normalizeOptionalText(body.aggregateOAuthEndpoint)
|
|
: settings?.aggregateOAuthEndpoint || ''
|
|
|
|
if (body.aggregateOAuthEnabled !== undefined) {
|
|
if (typeof body.aggregateOAuthEnabled !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'aggregateOAuthEnabled 必须是布尔值'
|
|
})
|
|
}
|
|
updateData.aggregateOAuthEnabled = body.aggregateOAuthEnabled
|
|
}
|
|
|
|
if (nextAggregateOAuthEnabled && !nextAggregateAppId) {
|
|
throw createError({ statusCode: 400, message: '启用聚合登陆时必须提供 AppID' })
|
|
}
|
|
if (nextAggregateOAuthEnabled && !nextAggregateAppKey) {
|
|
throw createError({ statusCode: 400, message: '启用聚合登陆时必须提供 AppKey' })
|
|
}
|
|
if (nextAggregateOAuthEnabled && !nextAggregateEndpoint) {
|
|
throw createError({ statusCode: 400, message: '启用聚合登陆时必须提供接口地址' })
|
|
}
|
|
if (nextAggregateOAuthEnabled && nextAggregateLoginTypes.length === 0) {
|
|
throw createError({ statusCode: 400, message: '启用聚合登陆时至少选择一种登录方式' })
|
|
}
|
|
|
|
if (body.aggregateOAuthAppId !== undefined) {
|
|
updateData.aggregateOAuthAppId = nextAggregateAppId || null
|
|
}
|
|
|
|
if (
|
|
body.aggregateOAuthAppKey !== undefined &&
|
|
body.aggregateOAuthAppKey !== SECRET_FIELD_MASK
|
|
) {
|
|
updateData.aggregateOAuthAppKey = nextAggregateAppKey || null
|
|
}
|
|
|
|
if (body.aggregateOAuthLoginType !== undefined) {
|
|
updateData.aggregateOAuthLoginType = JSON.stringify(nextAggregateLoginTypes)
|
|
}
|
|
|
|
if (body.aggregateOAuthEndpoint !== undefined) {
|
|
if (nextAggregateEndpoint) {
|
|
if (!isSafeAggregateOAuthUrl(nextAggregateEndpoint)) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'aggregateOAuthEndpoint 公网地址必须使用 HTTPS,内网地址可使用 HTTP'
|
|
})
|
|
}
|
|
}
|
|
updateData.aggregateOAuthEndpoint = nextAggregateEndpoint || null
|
|
}
|
|
|
|
// Custom OAuth2
|
|
if (body.customOAuthEnabled !== undefined) {
|
|
if (typeof body.customOAuthEnabled !== 'boolean') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'customOAuthEnabled 必须是布尔值'
|
|
})
|
|
}
|
|
if (body.customOAuthEnabled) {
|
|
const requiredCustomFields = [
|
|
{ key: 'customOAuthAuthorizeUrl', label: '授权端点 URL' },
|
|
{ key: 'customOAuthTokenUrl', label: 'Token 端点 URL' },
|
|
{ key: 'customOAuthUserInfoUrl', label: '用户信息端点 URL' },
|
|
{ key: 'customOAuthClientId', label: 'Client ID' },
|
|
{ key: 'customOAuthClientSecret', label: 'Client Secret' },
|
|
{ key: 'customOAuthUserIdField', label: '用户 ID 字段名' }
|
|
]
|
|
for (const field of requiredCustomFields) {
|
|
if (!body[field.key] && !settings?.[field.key]) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: `启用自定义 OAuth2 登录时必须提供 ${field.label}`
|
|
})
|
|
}
|
|
}
|
|
}
|
|
updateData.customOAuthEnabled = body.customOAuthEnabled
|
|
}
|
|
|
|
if (body.customOAuthDisplayName !== undefined) {
|
|
updateData.customOAuthDisplayName = body.customOAuthDisplayName
|
|
}
|
|
|
|
if (body.customOAuthAuthorizeUrl !== undefined) {
|
|
if (body.customOAuthAuthorizeUrl) {
|
|
try {
|
|
new URL(body.customOAuthAuthorizeUrl)
|
|
} catch {
|
|
throw createError({ statusCode: 400, message: 'customOAuthAuthorizeUrl 不是合法URL' })
|
|
}
|
|
}
|
|
updateData.customOAuthAuthorizeUrl = body.customOAuthAuthorizeUrl
|
|
}
|
|
|
|
if (body.customOAuthTokenUrl !== undefined) {
|
|
if (body.customOAuthTokenUrl) {
|
|
try {
|
|
new URL(body.customOAuthTokenUrl)
|
|
} catch {
|
|
throw createError({ statusCode: 400, message: 'customOAuthTokenUrl 不是合法URL' })
|
|
}
|
|
}
|
|
updateData.customOAuthTokenUrl = body.customOAuthTokenUrl
|
|
}
|
|
|
|
if (body.customOAuthUserInfoUrl !== undefined) {
|
|
if (body.customOAuthUserInfoUrl) {
|
|
try {
|
|
new URL(body.customOAuthUserInfoUrl)
|
|
} catch {
|
|
throw createError({ statusCode: 400, message: 'customOAuthUserInfoUrl 不是合法URL' })
|
|
}
|
|
}
|
|
updateData.customOAuthUserInfoUrl = body.customOAuthUserInfoUrl
|
|
}
|
|
|
|
if (body.customOAuthScope !== undefined) {
|
|
updateData.customOAuthScope = body.customOAuthScope
|
|
}
|
|
|
|
if (body.customOAuthClientId !== undefined) {
|
|
updateData.customOAuthClientId = body.customOAuthClientId
|
|
}
|
|
|
|
if (
|
|
body.customOAuthClientSecret !== undefined &&
|
|
body.customOAuthClientSecret !== SECRET_FIELD_MASK
|
|
) {
|
|
updateData.customOAuthClientSecret = body.customOAuthClientSecret
|
|
}
|
|
|
|
if (body.customOAuthUserIdField !== undefined) {
|
|
updateData.customOAuthUserIdField = body.customOAuthUserIdField
|
|
}
|
|
|
|
if (body.customOAuthUsernameField !== undefined) {
|
|
updateData.customOAuthUsernameField = body.customOAuthUsernameField
|
|
}
|
|
|
|
if (body.customOAuthNameField !== undefined) {
|
|
updateData.customOAuthNameField = body.customOAuthNameField
|
|
}
|
|
|
|
if (body.customOAuthEmailField !== undefined) {
|
|
updateData.customOAuthEmailField = body.customOAuthEmailField
|
|
}
|
|
|
|
if (body.customOAuthAvatarField !== undefined) {
|
|
updateData.customOAuthAvatarField = body.customOAuthAvatarField
|
|
}
|
|
|
|
// 验证每日、每周和每月限额三选一逻辑
|
|
const limitSettings = [
|
|
body.dailySubmissionLimit,
|
|
body.weeklySubmissionLimit,
|
|
body.monthlySubmissionLimit
|
|
].filter((limit) => limit !== undefined && limit !== null)
|
|
|
|
if (body.enableSubmissionLimit && limitSettings.length > 1) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: '每日限额、每周限额和每月限额只能选择其中一种,其他必须设置为空'
|
|
})
|
|
}
|
|
|
|
// 重复投稿限制交叉校验:开关关闭即完全不做限制;开启且未设置时长时沿用本学期同一首歌不可重复投稿的旧规则。
|
|
const nextSameSongHours = body.sameSongRestrictionHours !== undefined
|
|
? body.sameSongRestrictionHours
|
|
: settings?.sameSongRestrictionHours ?? null
|
|
const nextSameArtistHours = body.sameArtistRestrictionHours !== undefined
|
|
? body.sameArtistRestrictionHours
|
|
: settings?.sameArtistRestrictionHours ?? null
|
|
|
|
const anyRestrictionHoursPositive =
|
|
(typeof nextSameSongHours === 'number' && nextSameSongHours >= 1) ||
|
|
(typeof nextSameArtistHours === 'number' && nextSameArtistHours >= 1)
|
|
|
|
// 显式关闭但仍有有效时长:拒绝,提示用户先清除时长
|
|
if (body.enableSubmissionRestriction === false && anyRestrictionHoursPositive) {
|
|
throw createApiError(
|
|
400,
|
|
SERVER_ERROR_CODES.COMMON_INVALID_PARAMS,
|
|
'关闭重复投稿限制时,同一首歌和同一歌手的限制时间必须同时清空'
|
|
)
|
|
}
|
|
// 时长>0 且 enable 未显式提交 ⇒ 自动开启,维持"时长与开关自洽"不变量
|
|
if (anyRestrictionHoursPositive && body.enableSubmissionRestriction === undefined) {
|
|
updateData.enableSubmissionRestriction = true
|
|
}
|
|
|
|
if (!settings) {
|
|
const newSettingsResult = await db
|
|
.insert(systemSettings)
|
|
.values({
|
|
...SYSTEM_SETTINGS_DEFAULTS,
|
|
scheduleDaysBeforeEnabled: SYSTEM_SETTINGS_DEFAULTS.scheduleDaysBeforeEnabled,
|
|
scheduleDaysBefore: SYSTEM_SETTINGS_DEFAULTS.scheduleDaysBefore,
|
|
scheduleDaysAfterEnabled: SYSTEM_SETTINGS_DEFAULTS.scheduleDaysAfterEnabled,
|
|
scheduleDaysAfter: SYSTEM_SETTINGS_DEFAULTS.scheduleDaysAfter,
|
|
...updateData
|
|
})
|
|
.returning()
|
|
settings = newSettingsResult[0]
|
|
} else {
|
|
// 如果存在,更新设置
|
|
const updatedSettingsResult = await db
|
|
.update(systemSettings)
|
|
.set(updateData)
|
|
.where(eq(systemSettings.id, settings.id))
|
|
.returning()
|
|
settings = updatedSettingsResult[0]
|
|
}
|
|
|
|
if (updateData.telemetryEnabled !== undefined) {
|
|
try {
|
|
const { setTelemetryEnabledCache } = await import('~~/server/utils/telemetry')
|
|
setTelemetryEnabledCache(updateData.telemetryEnabled)
|
|
} catch (telemetryError) {
|
|
console.warn('[Telemetry] 遥测开关缓存更新失败:', telemetryError)
|
|
}
|
|
}
|
|
|
|
try {
|
|
const { SmtpService } = await import('~~/server/services/smtpService')
|
|
// 强制刷新:早退检查会导致修改授权码后单例仍持有旧凭据
|
|
await SmtpService.getInstance().initializeSmtpConfig(true)
|
|
console.log('[SMTP] SMTP配置已重新加载(更新系统设置)')
|
|
} catch (smtpError) {
|
|
console.warn('[SMTP] SMTP配置重载失败:', smtpError)
|
|
}
|
|
|
|
return maskSystemSettingsSecrets(settings)
|
|
} catch (error) {
|
|
console.error('更新系统设置失败:', error)
|
|
|
|
if (error.statusCode) {
|
|
throw error
|
|
}
|
|
|
|
throw createError({
|
|
statusCode: 500,
|
|
message: '更新系统设置失败'
|
|
})
|
|
}
|
|
})
|