251 lines
7.6 KiB
TypeScript
251 lines
7.6 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'
|
||
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 })
|
||
}
|
||
}
|
||
|
||
|