399 lines
14 KiB
TypeScript

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 {
await ensureSystemSettingsOptionalColumns()
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<Array<{
enabledBuiltinTemplates?: string | null
homeDeleteButtonMode?: string | null
premiumRatio?: number | null
enablePublicShare?: boolean | null
}>>`
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 = await prisma.systemSettings.create({
data: {
headerBgColor: '#1f2937',
headerLogoUrl: defaultLogos.header,
loginLogoUrl: defaultLogos.login,
siteName: 'FLNXI素材库',
premiumRatio: 10,
enableUserActivation: false,
enableRegistration: true,
enableAiImageEdit: featureFlags.aiEditEnabled,
enableTunnelService: featureFlags.tunnelEnabled,
},
}) as SystemSettingsRow
settings.enablePublicShare = true
} else {
// 已存在的旧数据,如果 LOGO 为空则自动补上默认值,并保存一次;不要在 GET 中补写对象存储密钥
let needUpdate = false
const updateData: {
headerLogoUrl?: string
loginLogoUrl?: string
} = {}
const normalizedHeaderLogoUrl = normalizeLogoUrl(settings.headerLogoUrl)
const normalizedLoginLogoUrl = normalizeLogoUrl(settings.loginLogoUrl)
if (normalizedHeaderLogoUrl !== (settings.headerLogoUrl || '')) {
updateData.headerLogoUrl = normalizedHeaderLogoUrl
settings.headerLogoUrl = normalizedHeaderLogoUrl
needUpdate = true
}
if (normalizedLoginLogoUrl !== (settings.loginLogoUrl || '')) {
updateData.loginLogoUrl = normalizedLoginLogoUrl
settings.loginLogoUrl = normalizedLoginLogoUrl
needUpdate = true
}
if (!settings.headerLogoUrl) {
updateData.headerLogoUrl = defaultLogos.header
settings.headerLogoUrl = defaultLogos.header
needUpdate = true
}
if (!settings.loginLogoUrl) {
updateData.loginLogoUrl = defaultLogos.login
settings.loginLogoUrl = defaultLogos.login
needUpdate = true
}
if (needUpdate) {
await prisma.systemSettings.update({
where: { id: settings.id },
data: updateData,
})
}
}
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 }
)
}
}