diff --git a/server/api/admin/system-settings/index.post.ts b/server/api/admin/system-settings/index.post.ts index b33c4fe..9f2ac77 100644 --- a/server/api/admin/system-settings/index.post.ts +++ b/server/api/admin/system-settings/index.post.ts @@ -198,6 +198,520 @@ export default defineEventHandler(async (event) => { updateData.forceBlockAllRequests = body.forceBlockAllRequests } + if (body.submissionNoteRequiresApproval !== undefined) { + if (typeof body.submissionNoteRequiresApproval !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'submissionNoteRequiresApproval 必须是布尔值' + }) + } + updateData.submissionNoteRequiresApproval = body.submissionNoteRequiresApproval + } + + if (body.telemetryEnabled !== undefined) { + if (typeof body.telemetryEnabled !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'telemetryEnabled 必须是布尔值' + }) + } + updateData.telemetryEnabled = body.telemetryEnabled + } + + if (body.showBeianIcon !== undefined) { + if (typeof body.showBeianIcon !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'showBeianIcon 必须是布尔值' + }) + } + updateData.showBeianIcon = body.showBeianIcon + } + + if (body.scheduleDaysDisplay !== undefined) { + if (body.scheduleDaysDisplay !== null && (!Number.isInteger(body.scheduleDaysDisplay) || body.scheduleDaysDisplay < 0)) { + throw createError({ + statusCode: 400, + message: 'scheduleDaysDisplay 必须是非负整数或null' + }) + } + updateData.scheduleDaysDisplay = body.scheduleDaysDisplay + } + + if (body.scheduleDaysEditable !== undefined) { + if (body.scheduleDaysEditable !== null && (!Number.isInteger(body.scheduleDaysEditable) || body.scheduleDaysEditable < 0)) { + throw createError({ + statusCode: 400, + message: 'scheduleDaysEditable 必须是非负整数或null' + }) + } + updateData.scheduleDaysEditable = body.scheduleDaysEditable + } + + if (body.scheduleDaysRequestable !== undefined) { + if (body.scheduleDaysRequestable !== null && (!Number.isInteger(body.scheduleDaysRequestable) || body.scheduleDaysRequestable < 0)) { + throw createError({ + statusCode: 400, + message: 'scheduleDaysRequestable 必须是非负整数或null' + }) + } + updateData.scheduleDaysRequestable = body.scheduleDaysRequestable + } + + if (body.scheduleDaysBeforeEnabled !== undefined) { + if (typeof body.scheduleDaysBeforeEnabled !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'scheduleDaysBeforeEnabled 必须是布尔值' + }) + } + updateData.scheduleDaysBeforeEnabled = body.scheduleDaysBeforeEnabled + } + + if (body.scheduleDaysBefore !== undefined) { + if (body.scheduleDaysBefore !== null && (!Number.isInteger(body.scheduleDaysBefore) || body.scheduleDaysBefore < 0)) { + throw createError({ + statusCode: 400, + message: 'scheduleDaysBefore 必须是非负整数或null' + }) + } + updateData.scheduleDaysBefore = body.scheduleDaysBefore + } + + if (body.scheduleDaysAfterEnabled !== undefined) { + if (typeof body.scheduleDaysAfterEnabled !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'scheduleDaysAfterEnabled 必须是布尔值' + }) + } + updateData.scheduleDaysAfterEnabled = body.scheduleDaysAfterEnabled + } + + if (body.scheduleDaysAfter !== undefined) { + if (body.scheduleDaysAfter !== null && (!Number.isInteger(body.scheduleDaysAfter) || body.scheduleDaysAfter < 0)) { + throw createError({ + statusCode: 400, + message: 'scheduleDaysAfter 必须是非负整数或null' + }) + } + updateData.scheduleDaysAfter = body.scheduleDaysAfter + } + + if (body.forcePasswordChangeOnFirstLogin !== undefined) { + if (typeof body.forcePasswordChangeOnFirstLogin !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'forcePasswordChangeOnFirstLogin 必须是布尔值' + }) + } + updateData.forcePasswordChangeOnFirstLogin = body.forcePasswordChangeOnFirstLogin + } + + if (body.enableSubmissionRestriction !== undefined) { + if (typeof body.enableSubmissionRestriction !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'enableSubmissionRestriction 必须是布尔值' + }) + } + updateData.enableSubmissionRestriction = body.enableSubmissionRestriction + } + + if (body.submissionRestrictionScope !== undefined) { + updateData.submissionRestrictionScope = body.submissionRestrictionScope + } + + if (body.cardCodeEnabled !== undefined) { + if (typeof body.cardCodeEnabled !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'cardCodeEnabled 必须是布尔值' + }) + } + updateData.cardCodeEnabled = body.cardCodeEnabled + } + + if (body.cardCodeSubmissionRequired !== undefined) { + if (typeof body.cardCodeSubmissionRequired !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'cardCodeSubmissionRequired 必须是布尔值' + }) + } + updateData.cardCodeSubmissionRequired = body.cardCodeSubmissionRequired + } + + if (body.cardCodeVoteEnabled !== undefined) { + if (typeof body.cardCodeVoteEnabled !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'cardCodeVoteEnabled 必须是布尔值' + }) + } + updateData.cardCodeVoteEnabled = body.cardCodeVoteEnabled + } + + if (body.cardCodeReplayEnabled !== undefined) { + if (typeof body.cardCodeReplayEnabled !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'cardCodeReplayEnabled 必须是布尔值' + }) + } + updateData.cardCodeReplayEnabled = body.cardCodeReplayEnabled + } + + 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.submissionRestrictionEnabled !== undefined) { + if (typeof body.submissionRestrictionEnabled !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'submissionRestrictionEnabled 必须是布尔值' + }) + } + updateData.submissionRestrictionEnabled = body.submissionRestrictionEnabled + } + + if (body.submissionRestrictionGradeEnabled !== undefined) { + if (typeof body.submissionRestrictionGradeEnabled !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'submissionRestrictionGradeEnabled 必须是布尔值' + }) + } + updateData.submissionRestrictionGradeEnabled = body.submissionRestrictionGradeEnabled + } + + if (body.submissionRestrictionClassEnabled !== undefined) { + if (typeof body.submissionRestrictionClassEnabled !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'submissionRestrictionClassEnabled 必须是布尔值' + }) + } + updateData.submissionRestrictionClassEnabled = body.submissionRestrictionClassEnabled + } + + if (body.submissionRestrictionGraduateEnabled !== undefined) { + if (typeof body.submissionRestrictionGraduateEnabled !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'submissionRestrictionGraduateEnabled 必须是布尔值' + }) + } + updateData.submissionRestrictionGraduateEnabled = body.submissionRestrictionGraduateEnabled + } + + if (body.submissionRestrictionWithdrawnEnabled !== undefined) { + if (typeof body.submissionRestrictionWithdrawnEnabled !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'submissionRestrictionWithdrawnEnabled 必须是布尔值' + }) + } + updateData.submissionRestrictionWithdrawnEnabled = body.submissionRestrictionWithdrawnEnabled + } + + if (body.captchaProvider !== undefined) { + updateData.captchaProvider = body.captchaProvider + } + + if (body.captchaEnabled !== undefined) { + if (typeof body.captchaEnabled !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'captchaEnabled 必须是布尔值' + }) + } + updateData.captchaEnabled = body.captchaEnabled + } + + if (body.allowRegister !== undefined) { + if (typeof body.allowRegister !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'allowRegister 必须是布尔值' + }) + } + updateData.allowRegister = body.allowRegister + } + + if (body.registerRequireEmail !== undefined) { + if (typeof body.registerRequireEmail !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'registerRequireEmail 必须是布尔值' + }) + } + updateData.registerRequireEmail = body.registerRequireEmail + } + + if (body.registerRequireEmailVerify !== undefined) { + if (typeof body.registerRequireEmailVerify !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'registerRequireEmailVerify 必须是布尔值' + }) + } + updateData.registerRequireEmailVerify = body.registerRequireEmailVerify + } + + if (body.registerDefaultRole !== undefined) { + updateData.registerDefaultRole = body.registerDefaultRole + } + + 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.registerEmailRequired !== undefined) { + if (typeof body.registerEmailRequired !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'registerEmailRequired 必须是布尔值' + }) + } + updateData.registerEmailRequired = body.registerEmailRequired + } + + if (body.registerRequiresGradeClass !== undefined) { + if (typeof body.registerRequiresGradeClass !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'registerRequiresGradeClass 必须是布尔值' + }) + } + updateData.registerRequiresGradeClass = body.registerRequiresGradeClass + } + + if (body.oauthEnabled !== undefined) { + if (typeof body.oauthEnabled !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'oauthEnabled 必须是布尔值' + }) + } + updateData.oauthEnabled = body.oauthEnabled + } + + if (body.oauthAllowRegister !== undefined) { + if (typeof body.oauthAllowRegister !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'oauthAllowRegister 必须是布尔值' + }) + } + updateData.oauthAllowRegister = body.oauthAllowRegister + } + + if (body.oauthAllowLogin !== undefined) { + if (typeof body.oauthAllowLogin !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'oauthAllowLogin 必须是布尔值' + }) + } + updateData.oauthAllowLogin = body.oauthAllowLogin + } + + if (body.oauthAllowBind !== undefined) { + if (typeof body.oauthAllowBind !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'oauthAllowBind 必须是布尔值' + }) + } + updateData.oauthAllowBind = body.oauthAllowBind + } + + if (body.allowOAuthRegistration !== undefined) { + if (typeof body.allowOAuthRegistration !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'allowOAuthRegistration 必须是布尔值' + }) + } + updateData.allowOAuthRegistration = body.allowOAuthRegistration + } + + if (body.oauthProviders !== undefined) { + updateData.oauthProviders = body.oauthProviders + } + + if (body.githubOAuthEnabled !== undefined) { + if (typeof body.githubOAuthEnabled !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'githubOAuthEnabled 必须是布尔值' + }) + } + updateData.githubOAuthEnabled = body.githubOAuthEnabled + } + + if (body.casdoorOAuthEnabled !== undefined) { + if (typeof body.casdoorOAuthEnabled !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'casdoorOAuthEnabled 必须是布尔值' + }) + } + updateData.casdoorOAuthEnabled = body.casdoorOAuthEnabled + } + + if (body.googleOAuthEnabled !== undefined) { + if (typeof body.googleOAuthEnabled !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'googleOAuthEnabled 必须是布尔值' + }) + } + updateData.googleOAuthEnabled = body.googleOAuthEnabled + } + + if (body.aggregateOAuthEnabled !== undefined) { + if (typeof body.aggregateOAuthEnabled !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'aggregateOAuthEnabled 必须是布尔值' + }) + } + updateData.aggregateOAuthEnabled = body.aggregateOAuthEnabled + } + + if (body.aggregateOAuthLoginType !== undefined) { + updateData.aggregateOAuthLoginType = body.aggregateOAuthLoginType + } + + if (body.customOAuthEnabled !== undefined) { + if (typeof body.customOAuthEnabled !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'customOAuthEnabled 必须是布尔值' + }) + } + updateData.customOAuthEnabled = body.customOAuthEnabled + } + + if (body.captchaMaxFailures !== undefined) { + if (body.captchaMaxFailures !== null && (!Number.isInteger(body.captchaMaxFailures) || body.captchaMaxFailures < 0)) { + throw createError({ + statusCode: 400, + message: 'captchaMaxFailures 必须是非负整数或null' + }) + } + updateData.captchaMaxFailures = body.captchaMaxFailures + } + + if (body.autoBackupEnabled !== undefined) { + if (typeof body.autoBackupEnabled !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'autoBackupEnabled 必须是布尔值' + }) + } + updateData.autoBackupEnabled = body.autoBackupEnabled + } + + if (body.autoBackupIntervalHours !== undefined) { + if (body.autoBackupIntervalHours !== null && (!Number.isInteger(body.autoBackupIntervalHours) || body.autoBackupIntervalHours < 0)) { + throw createError({ + statusCode: 400, + message: 'autoBackupIntervalHours 必须是非负整数或null' + }) + } + updateData.autoBackupIntervalHours = body.autoBackupIntervalHours + } + + if (body.autoBackupRetentionDays !== undefined) { + if (body.autoBackupRetentionDays !== null && (!Number.isInteger(body.autoBackupRetentionDays) || body.autoBackupRetentionDays < 0)) { + throw createError({ + statusCode: 400, + message: 'autoBackupRetentionDays 必须是非负整数或null' + }) + } + updateData.autoBackupRetentionDays = body.autoBackupRetentionDays + } + + if (body.statisticsCodeEnabled !== undefined) { + if (typeof body.statisticsCodeEnabled !== 'boolean') { + throw createError({ + statusCode: 400, + message: 'statisticsCodeEnabled 必须是布尔值' + }) + } + updateData.statisticsCodeEnabled = body.statisticsCodeEnabled + } + + if (body.statisticsCodeBody !== undefined) { + updateData.statisticsCodeBody = body.statisticsCodeBody + } + + if (body.statisticsCodeHead !== undefined) { + updateData.statisticsCodeHead = body.statisticsCodeHead + } + + if (body.defaultTheme !== undefined) { + updateData.defaultTheme = + typeof body.defaultTheme === 'string' && body.defaultTheme.trim() + ? body.defaultTheme.trim() + : null + } + + if (body.enabledThemes !== undefined) { + updateData.enabledThemes = + body.enabledThemes === null || body.enabledThemes === undefined + ? null + : typeof body.enabledThemes === 'string' + ? body.enabledThemes + : JSON.stringify(body.enabledThemes) + } + // SMTP配置字段 if (body.smtpEnabled !== undefined) { if (typeof body.smtpEnabled !== 'boolean') { @@ -266,10 +780,69 @@ export default defineEventHandler(async (event) => { }) } + const DEFAULTS = { + submissionNoteRequiresApproval: false, + telemetryEnabled: true, + showBeianIcon: true, + scheduleDaysDisplay: 7, + scheduleDaysEditable: 3, + scheduleDaysRequestable: 14, + scheduleDaysBeforeEnabled: false, + scheduleDaysBefore: 1, + scheduleDaysAfterEnabled: false, + scheduleDaysAfter: 1, + forcePasswordChangeOnFirstLogin: false, + enableSubmissionRestriction: false, + submissionRestrictionScope: 'all', + cardCodeEnabled: false, + cardCodeSubmissionRequired: false, + cardCodeVoteEnabled: false, + cardCodeReplayEnabled: false, + enableCardCodeRequests: false, + requireCardCodeForRequests: false, + enableCardCodeLimitBypass: false, + submissionRestrictionEnabled: false, + submissionRestrictionGradeEnabled: false, + submissionRestrictionClassEnabled: false, + submissionRestrictionGraduateEnabled: false, + submissionRestrictionWithdrawnEnabled: false, + captchaProvider: 'TURNSTILE', + captchaEnabled: false, + allowRegister: true, + registerRequireEmail: false, + registerRequireEmailVerify: false, + registerDefaultRole: 'USER', + registerRequiresApproval: false, + oauthRegisterRequiresApproval: false, + registerEmailRequired: false, + registerRequiresGradeClass: false, + oauthEnabled: false, + oauthAllowRegister: true, + oauthAllowLogin: true, + oauthAllowBind: true, + allowOAuthRegistration: false, + oauthProviders: '[]', + githubOAuthEnabled: false, + casdoorOAuthEnabled: false, + googleOAuthEnabled: false, + aggregateOAuthEnabled: false, + aggregateOAuthLoginType: 'qq', + customOAuthEnabled: false, + captchaMaxFailures: 3, + autoBackupEnabled: false, + autoBackupIntervalHours: 24, + autoBackupRetentionDays: 30, + statisticsCodeEnabled: false, + defaultTheme: 'System', + enabledThemes: '["System","ClassicDark","ClassicLight","ModernLight"]' +} as const + // 获取当前设置,如果不存在则创建 const settingsResult = await db.select().from(systemSettings).limit(1) let settings = settingsResult[0] || null + const fillDefaults = (obj: any) => ({ ...obj, ...Object.fromEntries(Object.entries(DEFAULTS).filter(([k]) => !Object.prototype.hasOwnProperty.call(obj || {}, k) || (obj && obj[k] === undefined || obj[k] === null))) } + if (!settings) { // 如果不存在,创建新设置 const newSettingsResult = await db @@ -324,7 +897,7 @@ export default defineEventHandler(async (event) => { .set(updateData) .where(eq(systemSettings.id, settings.id)) .returning() - settings = updatedSettingsResult[0] + settings = fillDefaults(updatedSettingsResult[0]) } // 清除系统设置缓存 diff --git a/server/api/admin/system-settings/index.ts b/server/api/admin/system-settings/index.ts index cbe9256..8603bf9 100644 --- a/server/api/admin/system-settings/index.ts +++ b/server/api/admin/system-settings/index.ts @@ -1,6 +1,7 @@ import { db } from '~/drizzle/db' import { systemSettings } from '~/drizzle/schema' import { CacheService } from '../../../services/cacheService' +import { SYSTEM_SETTINGS_DEFAULTS } from '~~/server/utils/system-settings-defaults' export default defineEventHandler(async (event) => { // 检查用户认证和权限 @@ -66,13 +67,15 @@ export default defineEventHandler(async (event) => { // 将设置存入缓存 try { + const merged: any = { ...SYSTEM_SETTINGS_DEFAULTS, ...(settings || {}) } + settings = merged await cacheService.setSystemSettings(settings) } catch (cacheError) { console.warn('缓存系统设置失败:', cacheError) } // 隐藏敏感字段 - if (settings.smtpPassword) { + if (settings && settings.smtpPassword) { settings.smtpPassword = '****************' } diff --git a/server/api/site-config.get.ts b/server/api/site-config.get.ts index 2e0c55c..d19a930 100644 --- a/server/api/site-config.get.ts +++ b/server/api/site-config.get.ts @@ -2,6 +2,7 @@ import { db } from '~/drizzle/db' import { systemSettings } from '~/drizzle/schema' import { cacheService } from '../services/cacheService' import { isRedisReady } from '../utils/redis' +import { SYSTEM_SETTINGS_DEFAULTS } from '~~/server/utils/system-settings-defaults' // 过滤敏感字段,只返回公开配置 const publicFields = [ @@ -36,13 +37,11 @@ const publicFields = [ ] const filterPublicSettings = (data: any) => { - if (!data) { - return {} - } + const merged: any = { ...SYSTEM_SETTINGS_DEFAULTS, ...(data || {}) } const result: Record = {} for (const key of publicFields) { - if (Object.prototype.hasOwnProperty.call(data, key)) { - result[key] = data[key] + if (Object.prototype.hasOwnProperty.call(merged, key)) { + result[key] = merged[key] } } return result @@ -55,7 +54,7 @@ export default defineEventHandler(async (event) => { const cachedSettings = await cacheService.getSystemSettings() if (cachedSettings) { console.log('[API] 系统设置缓存命中') - return filterPublicSettings(cachedSettings) + return filterPublicSettings({ ...SYSTEM_SETTINGS_DEFAULTS, ...cachedSettings }) } } @@ -100,7 +99,7 @@ export default defineEventHandler(async (event) => { settings = newSettings[0] } - const publicSettings = filterPublicSettings(settings) + const publicSettings = filterPublicSettings({ ...SYSTEM_SETTINGS_DEFAULTS, ...(settings || {}) }) // 将结果缓存到Redis(如果可用)- 永久缓存 if (settings && isRedisReady()) { diff --git a/server/services/cacheService.ts b/server/services/cacheService.ts index 7c91700..c28cbad 100644 --- a/server/services/cacheService.ts +++ b/server/services/cacheService.ts @@ -16,7 +16,7 @@ const CACHE_TTL = { SONG_COUNT: 180, // 3分钟 USER_VOTES: 120, // 2分钟 EMPTY_RESULT: 60, // 1分钟(空结果缓存时间较短) - SYSTEM_SETTINGS: 3600, // 60分钟 + SYSTEM_SETTINGS: 900, // 15分钟(新功能上线后避免 Redis 缓存旧 DB 值导致主题/排期等配置失效) RANDOM_OFFSET: 60, // 随机偏移量,防止缓存雪崩 LOCK_TIMEOUT: 30, // 分布式锁超时时间 REFRESH_AHEAD: 60 // 提前刷新时间(秒)