import { NextRequest, NextResponse } from 'next/server' import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/prisma' import { defaultLogos, featureFlags } from '@/lib/config' import { cache } from '@/lib/cache' import { ensureSystemSettingsOptionalColumns } from '@/lib/system-settings-columns' import { canUseSystemSettings } from '@/lib/license' import { isCurrentSuperAdmin } from '@/lib/admin-permissions' const CACHE_KEY = 'system-settings' const CACHE_TTL = 60000 // 1分钟缓存 const LEGACY_LOGO_PATHS = new Set(['/logo/logo1.png', '/logo/logo2.png', '/logo/logo1', '/logo/logo2']) type SystemSettingsRow = { id: string headerBgColor: string | null siteName: string | null premiumRatio?: number | null updatedAt: Date createdAt: Date headerLogoUrl: string | null loginLogoUrl: string | null s3AccessKey?: string | null s3Bucket?: string | null s3Endpoint?: string | null s3PublicUrl?: string | null s3Region?: string | null s3SecretKey?: string | null enableRegistration: boolean enableUserActivation: boolean enableAiImageEdit: boolean enableTunnelService: boolean enablePublicShare?: boolean | null enabledBuiltinTemplates?: string | null homeDeleteButtonMode?: string | null } function getPublicSystemSettings(settings: SystemSettingsRow) { return { headerBgColor: settings.headerBgColor, siteName: settings.siteName, updatedAt: settings.updatedAt, createdAt: settings.createdAt, headerLogoUrl: normalizeLogoUrl(settings.headerLogoUrl), loginLogoUrl: normalizeLogoUrl(settings.loginLogoUrl), } } function getAdminSystemSettings(settings: SystemSettingsRow, includeSensitiveSecrets: boolean) { if (includeSensitiveSecrets) { return settings } const { s3AccessKey: _s3AccessKey, s3SecretKey: _s3SecretKey, ...safeSettings } = settings return safeSettings } function isSystemSettingsAdmin(session: unknown) { return Boolean((session as any)?.user?.role === 'ADMIN') } function normalizeLogoUrl(url: string | null | undefined) { if (!url) return '' const trimmed = url.trim() if (!trimmed) return '' if (LEGACY_LOGO_PATHS.has(trimmed)) return '' return trimmed } function buildPublicFallbackSettings(): SystemSettingsRow { const now = new Date(0) return { id: 'fallback-public', headerBgColor: '#1f2937', siteName: 'FLNXI素材库', premiumRatio: 10, updatedAt: now, createdAt: now, headerLogoUrl: '', loginLogoUrl: '', s3AccessKey: '', s3Bucket: '', s3Endpoint: '', s3PublicUrl: '', s3Region: 'us-east-1', s3SecretKey: '', enableRegistration: true, enableUserActivation: false, enableAiImageEdit: false, enableTunnelService: false, enablePublicShare: true, enabledBuiltinTemplates: null, homeDeleteButtonMode: 'ADMIN_ONLY', } } // GET - 获取系统设置(带缓存) export async function GET() { try { const session = await getServerSession(authOptions) const canReadAdminSettings = isSystemSettingsAdmin(session) const canReadSensitiveSecrets = canReadAdminSettings && await isCurrentSuperAdmin(session) // 先尝试从缓存获取 const cacheKey = canReadSensitiveSecrets ? `${CACHE_KEY}:super-admin` : canReadAdminSettings ? `${CACHE_KEY}:admin` : `${CACHE_KEY}:public` const cachedSettings = cache.get(cacheKey) if (cachedSettings) { return NextResponse.json(cachedSettings) } // 获取或创建系统设置 // 使用 select 来避免查询不存在的字段 let settings = await prisma.systemSettings.findFirst({ select: { id: true, headerBgColor: true, siteName: true, premiumRatio: true, updatedAt: true, createdAt: true, headerLogoUrl: true, loginLogoUrl: true, s3AccessKey: true, s3Bucket: true, s3Endpoint: true, s3PublicUrl: true, s3Region: true, s3SecretKey: true, enableRegistration: true, enableUserActivation: true, enableAiImageEdit: true, enableTunnelService: true, // homeDeleteButtonMode 是可选列,这里不直接在 select 中声明,避免旧版本类型不匹配 // enabledBuiltinTemplates / enablePublicShare 可能不存在,先不查询 }, }) as SystemSettingsRow | null // 如果字段存在,尝试查询它 try { const fullSettings = await prisma.$queryRaw>` SELECT "enabledBuiltinTemplates", "homeDeleteButtonMode", "premiumRatio", "enablePublicShare" FROM "system_settings" LIMIT 1 ` if (fullSettings[0] && settings) { settings.enabledBuiltinTemplates = fullSettings[0].enabledBuiltinTemplates settings.homeDeleteButtonMode = fullSettings[0].homeDeleteButtonMode || 'ADMIN_ONLY' settings.premiumRatio = fullSettings[0].premiumRatio ?? 10 // 默认值10% settings.enablePublicShare = fullSettings[0].enablePublicShare !== false } } catch { // 字段不存在,设置为默认值 if (settings) { settings.enabledBuiltinTemplates = null settings.homeDeleteButtonMode = 'ADMIN_ONLY' settings.premiumRatio = settings.premiumRatio ?? 10 // 确保有默认值 settings.enablePublicShare = true } } if (!settings) { settings = buildPublicFallbackSettings() } else { const normalizedHeaderLogoUrl = normalizeLogoUrl(settings.headerLogoUrl) const normalizedLoginLogoUrl = normalizeLogoUrl(settings.loginLogoUrl) settings.headerLogoUrl = normalizedHeaderLogoUrl || defaultLogos.header settings.loginLogoUrl = normalizedLoginLogoUrl || defaultLogos.login } const responseSettings = canReadAdminSettings ? getAdminSystemSettings(settings, canReadSensitiveSecrets) : getPublicSystemSettings(settings) // 存入缓存 cache.set(cacheKey, responseSettings, CACHE_TTL) return NextResponse.json(responseSettings) } catch (error) { console.error('[SystemSettings] Get error:', error) // 公共页面在数据库不可用时仍返回兜底值,避免登录/注册页直接 500 const fallbackSettings = buildPublicFallbackSettings() const fallbackResponse = NextResponse.json(getPublicSystemSettings(fallbackSettings)) fallbackResponse.headers.set('x-system-settings-fallback', '1') return fallbackResponse } } // PUT - 更新系统设置(最高权限) export async function PUT(request: NextRequest) { try { await ensureSystemSettingsOptionalColumns() const session = await getServerSession(authOptions) if (!session || !(await isCurrentSuperAdmin(session))) { return NextResponse.json( { error: '只有最高权限账号可以修改系统设置' }, { status: 403 } ) } // 检查系统设置功能是否可用(免费版只能查看,不能修改) if (!(await canUseSystemSettings())) { return NextResponse.json( { error: '当前许可证未包含系统设置功能,请联系管理员处理授权' }, { status: 403 } ) } const body = await request.json() const { headerBgColor, siteName, headerLogoUrl, loginLogoUrl, s3Endpoint, s3AccessKey, s3SecretKey, s3Bucket, s3Region, s3PublicUrl, enableUserActivation, enableRegistration, enableAiImageEdit, enableTunnelService, enablePublicShare, enabledBuiltinTemplates, homeDeleteButtonMode, premiumRatio, } = body let sanitizedPremiumRatio: number | undefined if (premiumRatio !== undefined) { const numericRatio = Number(premiumRatio) if (!Number.isFinite(numericRatio)) { return NextResponse.json( { error: '精品作品比例必须为数字' }, { status: 400 } ) } sanitizedPremiumRatio = Math.min(100, Math.max(0, Number(numericRatio.toFixed(2)))) } // 获取或创建系统设置 let settings = await prisma.systemSettings.findFirst() if (!settings) { // 创建(可选列如 homeDeleteButtonMode 使用原生 SQL 单独更新,以兼容旧版本 Prisma 类型) settings = await prisma.systemSettings.create({ data: { headerBgColor: headerBgColor || '#1f2937', headerLogoUrl: headerLogoUrl ?? defaultLogos.header, loginLogoUrl: loginLogoUrl ?? defaultLogos.login, siteName: siteName || 'FLNXI素材库', premiumRatio: sanitizedPremiumRatio ?? 10, enableUserActivation: enableUserActivation !== undefined ? enableUserActivation : false, enableRegistration: enableRegistration !== undefined ? enableRegistration : true, enableAiImageEdit: enableAiImageEdit !== undefined ? enableAiImageEdit : featureFlags.aiEditEnabled, enableTunnelService: enableTunnelService !== undefined ? enableTunnelService : featureFlags.tunnelEnabled, } }) // 通过原生 SQL 写入可选列,避免类型不匹配 try { await prisma.$executeRawUnsafe( `UPDATE "system_settings" SET "homeDeleteButtonMode" = $1, "enablePublicShare" = $2 WHERE "id" = $3`, homeDeleteButtonMode ?? 'ADMIN_ONLY', enablePublicShare !== undefined ? Boolean(enablePublicShare) : true, settings.id ) } catch (e) { console.error('[SystemSettings] Failed to set optional columns on create:', e) } } else { // 更新 const updateData: any = {} if (headerBgColor !== undefined) updateData.headerBgColor = headerBgColor if (siteName !== undefined) updateData.siteName = siteName if (headerLogoUrl !== undefined) updateData.headerLogoUrl = headerLogoUrl if (loginLogoUrl !== undefined) updateData.loginLogoUrl = loginLogoUrl if (s3Endpoint !== undefined) updateData.s3Endpoint = s3Endpoint if (s3AccessKey !== undefined) updateData.s3AccessKey = s3AccessKey if (s3SecretKey !== undefined) updateData.s3SecretKey = s3SecretKey if (s3Bucket !== undefined) updateData.s3Bucket = s3Bucket if (s3Region !== undefined) updateData.s3Region = s3Region if (s3PublicUrl !== undefined) updateData.s3PublicUrl = s3PublicUrl if (enableUserActivation !== undefined) updateData.enableUserActivation = enableUserActivation if (enableRegistration !== undefined) updateData.enableRegistration = enableRegistration if (enableAiImageEdit !== undefined) updateData.enableAiImageEdit = enableAiImageEdit if (enableTunnelService !== undefined) updateData.enableTunnelService = enableTunnelService if (enabledBuiltinTemplates !== undefined) updateData.enabledBuiltinTemplates = enabledBuiltinTemplates if (sanitizedPremiumRatio !== undefined) updateData.premiumRatio = sanitizedPremiumRatio settings = await prisma.systemSettings.update({ where: { id: settings.id }, data: updateData, }) // 单独更新可选列 homeDeleteButtonMode if (homeDeleteButtonMode !== undefined) { try { await prisma.$executeRawUnsafe( `UPDATE "system_settings" SET "homeDeleteButtonMode" = $1 WHERE "id" = $2`, homeDeleteButtonMode, settings.id ) } catch (e) { console.error('[SystemSettings] Failed to update homeDeleteButtonMode:', e) } } if (enablePublicShare !== undefined) { try { await prisma.$executeRawUnsafe( `UPDATE "system_settings" SET "enablePublicShare" = $1 WHERE "id" = $2`, Boolean(enablePublicShare), settings.id ) } catch (e) { console.error('[SystemSettings] Failed to update enablePublicShare:', e) } } } // 清除缓存,强制下次重新加载 cache.delete(CACHE_KEY) cache.delete(`${CACHE_KEY}:public`) cache.delete(`${CACHE_KEY}:admin`) cache.delete(`${CACHE_KEY}:super-admin`) return NextResponse.json(settings) } catch (error) { console.error('Update system settings error:', error) return NextResponse.json( { error: '更新系统设置失败' }, { status: 500 } ) } }