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' import { isCurrentSuperAdmin } from '@/lib/admin-permissions' 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 (!(await isCurrentSuperAdmin(session))) { return NextResponse.json({ error: '仅最高权限账号可创建上传模板' }, { status: 403 }) } // 检查自定义模板功能是否可用 if (!(await 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', 'AUDIO', '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() 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 }) } }