2026-05-28 17:37:27 +08:00

251 lines
7.6 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'
import {
isValidUploadTemplateStatus,
validateAndFormatUploadTemplateFields,
} from '@/lib/upload-template-save-validation'
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 (!isValidUploadTemplateStatus(status)) {
console.warn('[TemplateCreate] Validation failed: invalid status:', status)
return NextResponse.json({
error: '模板状态必须是 draft、active 或 archived',
errorCode: 'VALIDATION_ERROR',
field: 'status',
}, { status: 400 })
}
const formattedFieldsResult = validateAndFormatUploadTemplateFields(fields)
if (!formattedFieldsResult.valid) {
console.warn('[TemplateCreate] Validation failed:', formattedFieldsResult)
return NextResponse.json({
error: formattedFieldsResult.error,
errorCode: 'VALIDATION_ERROR',
field: formattedFieldsResult.field,
details: formattedFieldsResult.details,
}, { status: 400 })
}
const formattedFields = formattedFieldsResult.fields.map(({ id: _id, ...field }) => field)
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 })
}
}