- 新增许可证服务器 (license-server/) - 新增 Agent 部署模块 (flnxi-agent/) - 添加许可证验证和激活页面 - 添加 Harbor 镜像同步脚本 - 更新素材系统支持许可证验证
329 lines
12 KiB
TypeScript
329 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'
|
|
import { canUseSystemSettings } from '@/lib/license'
|
|
|
|
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 }
|
|
)
|
|
}
|
|
|
|
// 检查系统设置功能是否可用(免费版只能查看,不能修改)
|
|
if (!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,
|
|
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 }
|
|
)
|
|
}
|
|
}
|
|
|