WuYongJie ba98bf22cc feat: 添加许可证系统、Agent部署模块和Harbor镜像管理
- 新增许可证服务器 (license-server/)
- 新增 Agent 部署模块 (flnxi-agent/)
- 添加许可证验证和激活页面
- 添加 Harbor 镜像同步脚本
- 更新素材系统支持许可证验证
2025-12-15 21:19:46 +08:00

295 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { Prisma } from '@prisma/client'
import { syncFieldPoolFromTemplates } from '@/lib/field-pool'
import { canUseCustomTemplates } from '@/lib/license'
const baseSelect = {
id: true,
name: true,
description: true,
status: true,
materialType: true,
materialKind: true,
version: true,
fieldGroups: true,
displayConfig: true,
createdAt: true,
updatedAt: true,
fields: {
orderBy: { orderIndex: 'asc' },
select: {
id: true,
key: true,
label: true,
type: true,
required: true,
orderIndex: true,
layoutWidth: true,
validations: true,
optionsSource: true,
optionsStatic: true,
allowedExtensions: true,
visibleOnHomeFilter: true,
isSearchable: true,
isReadOnly: true,
addToCategory: true,
conditions: true,
createdAt: true,
updatedAt: true,
},
},
} as const
export async function GET() {
try {
const session = await getServerSession(authOptions)
if (!session || (session.user as any)?.role !== 'ADMIN') {
console.warn('[TemplateList] Unauthorized access attempt')
return NextResponse.json({
error: '无权限',
errorCode: 'UNAUTHORIZED',
}, { status: 403 })
}
console.log('[TemplateList] Fetching templates...')
const templates = await prisma.uploadTemplate.findMany({
orderBy: { updatedAt: 'desc' },
select: {
...baseSelect,
_count: {
select: { materials: true }
}
},
})
// 转换数据,将_count.materials映射为materialCount
const templatesWithCount = templates.map(template => ({
...template,
materialCount: template._count.materials,
_count: undefined, // 移除_count字段
}))
console.log(`[TemplateList] Found ${templates.length} templates`)
return NextResponse.json(templatesWithCount)
} catch (error: any) {
console.error('[TemplateList] List error:', {
error: error?.message,
code: error?.code,
stack: error?.stack,
})
const errorMessage = error?.message || '获取上传模板失败'
return NextResponse.json({
error: errorMessage,
errorCode: 'LIST_FAILED',
}, { status: 500 })
}
}
export async function POST(req: NextRequest) {
let body: any = null
try {
const session = await getServerSession(authOptions)
if (!session || (session.user as any)?.role !== 'ADMIN') {
return NextResponse.json({ error: '无权限' }, { status: 403 })
}
// 检查自定义模板功能是否可用
if (!canUseCustomTemplates()) {
return NextResponse.json({
error: '自定义模板功能需要标准版或专业版许可证,请升级后使用',
errorCode: 'LICENSE_REQUIRED',
}, { status: 403 })
}
body = await req.json()
const {
name,
description,
status = 'draft',
materialType,
materialKind,
fieldGroups,
displayConfig,
fields = [],
} = body || {}
// 验证必填字段
if (!name || typeof name !== 'string' || !name.trim()) {
console.warn('[TemplateCreate] Validation failed: name is required')
return NextResponse.json({
error: '模板名称不能为空',
errorCode: 'VALIDATION_ERROR',
field: 'name',
}, { status: 400 })
}
// 验证materialType
if (!materialType || typeof materialType !== 'string' || !materialType.trim()) {
console.warn('[TemplateCreate] Validation failed: materialType is required')
return NextResponse.json({
error: '素材类型不能为空',
errorCode: 'VALIDATION_ERROR',
field: 'materialType',
}, { status: 400 })
}
// 验证materialKind
const allowedKinds = ['IMAGE', 'VIDEO', 'OTHER'] as const
const rawKind = typeof materialKind === 'string' && materialKind.trim() ? materialKind.trim().toUpperCase() : ''
if (!rawKind || !(allowedKinds as readonly string[]).includes(rawKind)) {
console.warn('[TemplateCreate] Validation failed: invalid materialKind:', materialKind)
return NextResponse.json({
error: `素材大类必须是以下之一:${allowedKinds.join(', ')}`,
errorCode: 'VALIDATION_ERROR',
field: 'materialKind',
}, { status: 400 })
}
const finalMaterialType = materialType.trim()
const finalMaterialKind = rawKind
// 验证字段数组
if (!Array.isArray(fields)) {
console.warn('[TemplateCreate] Validation failed: fields must be an array')
return NextResponse.json({
error: '字段列表必须是数组',
errorCode: 'VALIDATION_ERROR',
field: 'fields',
}, { status: 400 })
}
// 检查字段key的唯一性
const fieldKeys = new Set<string>()
const duplicateKeys: string[] = []
for (const field of fields) {
const key = String(field.key || '').trim()
if (!key) {
console.warn('[TemplateCreate] Validation failed: field key is required')
return NextResponse.json({
error: '字段key不能为空',
errorCode: 'VALIDATION_ERROR',
field: 'fields',
}, { status: 400 })
}
if (fieldKeys.has(key)) {
duplicateKeys.push(key)
} else {
fieldKeys.add(key)
}
}
if (duplicateKeys.length > 0) {
console.warn('[TemplateCreate] Validation failed: duplicate field keys:', duplicateKeys)
return NextResponse.json({
error: `字段key重复:${duplicateKeys.join(', ')}`,
errorCode: 'VALIDATION_ERROR',
field: 'fields',
details: { duplicateKeys },
}, { status: 400 })
}
const formattedFields = fields.map((field: any, index: number) => {
const key = String(field.key || '').trim()
const label = String(field.label || '').trim() || `字段${index + 1}`
const type = String(field.type || 'text')
// 自动转换optionsStatic数组为JSON字符串
let optionsStatic = field.optionsStatic ?? null
if (optionsStatic !== null && optionsStatic !== undefined) {
if (Array.isArray(optionsStatic)) {
optionsStatic = JSON.stringify(optionsStatic)
console.log(`[TemplateCreate] Converted optionsStatic array to JSON for field: ${key}`)
} else {
optionsStatic = String(optionsStatic)
}
}
return {
key,
label,
type,
required: !!field.required,
orderIndex: field.orderIndex ?? index,
layoutWidth: field.layoutWidth || 'full',
validations: field.validations ?? null,
optionsSource: field.optionsSource ?? null,
optionsStatic,
allowedExtensions: field.allowedExtensions ?? null,
visibleOnHomeFilter: !!field.visibleOnHomeFilter,
isSearchable: !!field.isSearchable,
isReadOnly: !!field.isReadOnly,
addToCategory: !!field.addToCategory,
conditions: field.conditions ?? null,
}
})
const createData = {
name,
description: description ?? null,
status,
materialType: finalMaterialType,
materialKind: finalMaterialKind,
fieldGroups: fieldGroups ?? null,
displayConfig: displayConfig ?? null,
fields: formattedFields.length
? { createMany: { data: formattedFields } }
: undefined,
} as Prisma.UploadTemplateCreateInput
const template = await prisma.uploadTemplate.create({
data: createData,
select: baseSelect,
})
await syncFieldPoolFromTemplates()
return NextResponse.json(template, { status: 201 })
} catch (error: any) {
console.error('[TemplateCreate] Create error:', {
error: error?.message,
code: error?.code,
meta: error?.meta,
stack: error?.stack,
body: body,
})
// 处理Prisma特定错误
if (error?.code) {
if (error.code === 'P2002') {
const target = error?.meta?.target
const field = Array.isArray(target) ? target[0] : 'name'
return NextResponse.json({
error: `${field === 'name' ? '模板名称' : '字段'}已存在,请使用其他${field === 'name' ? '名称' : '值'}`,
errorCode: 'DUPLICATE_ENTRY',
field,
}, { status: 400 })
}
if (error.code === 'P2003') {
return NextResponse.json({
error: '关联数据不存在',
errorCode: 'FOREIGN_KEY_CONSTRAINT',
}, { status: 400 })
}
if (error.code === 'P2000') {
return NextResponse.json({
error: '字段值过长,请缩短后重试',
errorCode: 'VALUE_TOO_LONG',
}, { status: 400 })
}
}
const errorMessage = error?.message || '创建上传模板失败'
return NextResponse.json({
error: errorMessage,
errorCode: 'CREATE_FAILED',
}, { status: 500 })
}
}