2026-05-25 16:48:19 +08:00

299 lines
9.1 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'
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<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 })
}
}