WuYongJie 542c7bcc49 feat: 优化自定义上传模板系统
- 修复内置模板验证问题(builtin-video-material等)
- 添加模板验证工具 lib/template-validator.ts
- 创建模板系统优化需求文档(30个详细需求)
- 优化上传API的模板验证逻辑
- 改进错误提示信息的清晰度
- 添加字段池和分类管理相关需求
- 完善自定义字段的创建、验证、存储逻辑
- 清理过期的 .specstory 和 .trae 文件
2025-12-13 14:26:43 +08:00

320 lines
12 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'
const CACHE_KEY = 'system-settings'
const CACHE_TTL = 60000 // 1分钟缓存
// GET - 获取系统设置(带缓存)
export async function GET() {
try {
await ensureSystemSettingsOptionalColumns()
// 先尝试从缓存获取
const cachedSettings = cache.get(CACHE_KEY)
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 可能不存在,先不查询
},
}) as any
// 如果字段存在,尝试查询它
try {
const fullSettings = await prisma.$queryRaw<Array<{ enabledBuiltinTemplates?: string | null; homeDeleteButtonMode?: string | null; premiumRatio?: number | null }>>`
SELECT "enabledBuiltinTemplates", "homeDeleteButtonMode", "premiumRatio" 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%
}
} catch {
// 字段不存在,设置为默认值
if (settings) {
settings.enabledBuiltinTemplates = null
settings.homeDeleteButtonMode = 'ADMIN_ONLY'
settings.premiumRatio = settings.premiumRatio ?? 10 // 确保有默认值
}
}
const OLD_HEADER_DEFAULT = '/logo/logo2.png'
const OLD_LOGIN_DEFAULT = '/logo/logo1.png'
if (!settings) {
// 如果不存在,创建默认设置(包含默认 LOGO 与对象存储配置)
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,
// 对象存储相关字段:初次创建时直接带上当前环境值,便于在面板中展示“系统当前值”
s3Endpoint: process.env.S3_ENDPOINT || undefined,
s3AccessKey: process.env.S3_ACCESS_KEY_ID || undefined,
s3SecretKey: process.env.S3_SECRET_ACCESS_KEY || undefined,
s3Bucket: process.env.S3_BUCKET || undefined,
s3Region: process.env.S3_REGION || 'us-east-1',
s3PublicUrl: process.env.S3_PUBLIC_DOMAIN || undefined,
},
}) as any
} else {
// 已存在的旧数据,如果 LOGO 或对象存储字段为空,则自动补上默认值,并保存一次
let needUpdate = false
const updateData: {
headerLogoUrl?: string
loginLogoUrl?: string
s3Endpoint?: string
s3AccessKey?: string
s3SecretKey?: string
s3Bucket?: string
s3Region?: string
s3PublicUrl?: string
} = {}
// LOGO 默认值兜底或纠正
const shouldSwapLegacyDefaults =
settings.headerLogoUrl === OLD_HEADER_DEFAULT &&
settings.loginLogoUrl === OLD_LOGIN_DEFAULT
if (!settings.headerLogoUrl || shouldSwapLegacyDefaults) {
updateData.headerLogoUrl = defaultLogos.header
settings.headerLogoUrl = defaultLogos.header
needUpdate = true
}
if (!settings.loginLogoUrl || shouldSwapLegacyDefaults) {
updateData.loginLogoUrl = defaultLogos.login
settings.loginLogoUrl = defaultLogos.login
needUpdate = true
}
// 对象存储字段:为空时,用当前环境变量补上,方便面板展示“系统当前值”
if (!settings.s3Endpoint && process.env.S3_ENDPOINT) {
updateData.s3Endpoint = process.env.S3_ENDPOINT
settings.s3Endpoint = process.env.S3_ENDPOINT
needUpdate = true
}
if (!settings.s3AccessKey && process.env.S3_ACCESS_KEY_ID) {
updateData.s3AccessKey = process.env.S3_ACCESS_KEY_ID
settings.s3AccessKey = process.env.S3_ACCESS_KEY_ID
needUpdate = true
}
if (!settings.s3SecretKey && process.env.S3_SECRET_ACCESS_KEY) {
updateData.s3SecretKey = process.env.S3_SECRET_ACCESS_KEY
settings.s3SecretKey = process.env.S3_SECRET_ACCESS_KEY
needUpdate = true
}
if (!settings.s3Bucket && process.env.S3_BUCKET) {
updateData.s3Bucket = process.env.S3_BUCKET
settings.s3Bucket = process.env.S3_BUCKET
needUpdate = true
}
if (!settings.s3Region && process.env.S3_REGION) {
updateData.s3Region = process.env.S3_REGION
settings.s3Region = process.env.S3_REGION
needUpdate = true
}
if (!settings.s3PublicUrl && process.env.S3_PUBLIC_DOMAIN) {
updateData.s3PublicUrl = process.env.S3_PUBLIC_DOMAIN
settings.s3PublicUrl = process.env.S3_PUBLIC_DOMAIN
needUpdate = true
}
if (needUpdate) {
await prisma.systemSettings.update({
where: { id: settings.id },
data: updateData,
})
}
}
// 存入缓存
cache.set(CACHE_KEY, settings, CACHE_TTL)
return NextResponse.json(settings)
} catch (error) {
console.error('[SystemSettings] Get error:', error)
return NextResponse.json(
{ error: '获取系统设置失败' },
{ status: 500 }
)
}
}
// PUT - 更新系统设置(管理员权限)
export async function PUT(request: NextRequest) {
try {
await ensureSystemSettingsOptionalColumns()
const session = await getServerSession(authOptions)
const userRole = (session?.user as any)?.role
const isSuperAdmin = Boolean((session?.user as any)?.isSuperAdmin)
if (!session || userRole !== 'ADMIN') {
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,
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))))
}
// 只有最高权限账号可以修改 enableUserActivation
if (
(enableUserActivation !== undefined ||
enableAiImageEdit !== undefined ||
enableTunnelService !== undefined) &&
!isSuperAdmin
) {
return NextResponse.json(
{ error: '只有最高权限账号可以修改受保护的功能开关' },
{ status: 403 }
)
}
// 获取或创建系统设置
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 WHERE "id" = $2`,
homeDeleteButtonMode ?? 'ADMIN_ONLY',
settings.id
)
} catch (e) {
console.error('[SystemSettings] Failed to set homeDeleteButtonMode 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)
}
}
}
// 清除缓存,强制下次重新加载
cache.delete(CACHE_KEY)
return NextResponse.json(settings)
} catch (error) {
console.error('Update system settings error:', error)
return NextResponse.json(
{ error: '更新系统设置失败' },
{ status: 500 }
)
}
}