302 lines
9.2 KiB
TypeScript
302 lines
9.2 KiB
TypeScript
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.$transaction(async (tx) => {
|
||
const created = await tx.uploadTemplate.create({
|
||
data: createData,
|
||
select: baseSelect,
|
||
})
|
||
|
||
await syncFieldPoolFromTemplates(tx)
|
||
return created
|
||
})
|
||
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 })
|
||
}
|
||
}
|
||
|
||
|