779 lines
23 KiB
TypeScript
779 lines
23 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
||
import { getServerSession } from 'next-auth'
|
||
import { authOptions } from '@/lib/auth'
|
||
import { prisma } from '@/lib/prisma'
|
||
import { syncFieldPoolFromTemplates } from '@/lib/field-pool'
|
||
import { canUseCustomTemplates } from '@/lib/license'
|
||
import { isCurrentSuperAdmin } from '@/lib/admin-permissions'
|
||
|
||
const selectTemplate = {
|
||
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
|
||
|
||
async function ensureAdmin() {
|
||
const session = await getServerSession(authOptions)
|
||
if (!session || (session.user as any)?.role !== 'ADMIN') {
|
||
return null
|
||
}
|
||
return session
|
||
}
|
||
|
||
type TemplateStructureChange =
|
||
| {
|
||
type: 'materialTypeChanged'
|
||
before: string
|
||
after: string
|
||
}
|
||
| {
|
||
type: 'materialKindChanged'
|
||
before: string
|
||
after: string
|
||
}
|
||
| {
|
||
type: 'fieldDeleted'
|
||
fieldId: string
|
||
fieldKey: string
|
||
fieldLabel: string
|
||
}
|
||
| {
|
||
type: 'fieldKeyChanged'
|
||
fieldId: string
|
||
before: string
|
||
after: string
|
||
fieldLabel: string
|
||
}
|
||
| {
|
||
type: 'fieldTypeChanged'
|
||
fieldId: string
|
||
fieldKey: string
|
||
fieldLabel: string
|
||
before: string
|
||
after: string
|
||
}
|
||
|
||
function detectDangerousTemplateChanges(args: {
|
||
existingTemplate: {
|
||
materialType: string
|
||
materialKind: string
|
||
fields: Array<{ id: string; key: string; type: string; label: string }>
|
||
}
|
||
nextMaterialType?: string
|
||
nextMaterialKind?: string
|
||
nextFields?: Array<{ id?: string | null; key: string; type: string; label: string }>
|
||
}): TemplateStructureChange[] {
|
||
const { existingTemplate, nextMaterialType, nextMaterialKind, nextFields } = args
|
||
const changes: TemplateStructureChange[] = []
|
||
|
||
if (nextMaterialType && nextMaterialType !== existingTemplate.materialType) {
|
||
changes.push({
|
||
type: 'materialTypeChanged',
|
||
before: existingTemplate.materialType,
|
||
after: nextMaterialType,
|
||
})
|
||
}
|
||
|
||
if (nextMaterialKind && nextMaterialKind !== existingTemplate.materialKind) {
|
||
changes.push({
|
||
type: 'materialKindChanged',
|
||
before: existingTemplate.materialKind,
|
||
after: nextMaterialKind,
|
||
})
|
||
}
|
||
|
||
if (!nextFields) {
|
||
return changes
|
||
}
|
||
|
||
const incomingById = new Map(
|
||
nextFields
|
||
.filter((field) => field.id)
|
||
.map((field) => [field.id as string, field])
|
||
)
|
||
|
||
for (const existingField of existingTemplate.fields) {
|
||
const incomingField = incomingById.get(existingField.id)
|
||
|
||
if (!incomingField) {
|
||
changes.push({
|
||
type: 'fieldDeleted',
|
||
fieldId: existingField.id,
|
||
fieldKey: existingField.key,
|
||
fieldLabel: existingField.label,
|
||
})
|
||
continue
|
||
}
|
||
|
||
if (incomingField.key !== existingField.key) {
|
||
changes.push({
|
||
type: 'fieldKeyChanged',
|
||
fieldId: existingField.id,
|
||
before: existingField.key,
|
||
after: incomingField.key,
|
||
fieldLabel: existingField.label,
|
||
})
|
||
}
|
||
|
||
if (incomingField.type !== existingField.type) {
|
||
changes.push({
|
||
type: 'fieldTypeChanged',
|
||
fieldId: existingField.id,
|
||
fieldKey: existingField.key,
|
||
fieldLabel: existingField.label,
|
||
before: existingField.type,
|
||
after: incomingField.type,
|
||
})
|
||
}
|
||
}
|
||
|
||
return changes
|
||
}
|
||
|
||
export async function GET(_: NextRequest, { params }: { params: { id: string } }) {
|
||
try {
|
||
const session = await ensureAdmin()
|
||
if (!session) {
|
||
console.warn('[TemplateGet] Unauthorized access attempt')
|
||
return NextResponse.json({
|
||
error: '无权限',
|
||
errorCode: 'UNAUTHORIZED',
|
||
}, { status: 403 })
|
||
}
|
||
|
||
const { id } = params
|
||
|
||
console.log('[TemplateGet] Fetching template:', id)
|
||
const template = await prisma.uploadTemplate.findUnique({
|
||
where: { id },
|
||
select: {
|
||
...selectTemplate,
|
||
_count: {
|
||
select: { materials: true }
|
||
}
|
||
},
|
||
})
|
||
|
||
if (!template) {
|
||
console.warn('[TemplateGet] Template not found:', id)
|
||
return NextResponse.json({
|
||
error: '模板不存在',
|
||
errorCode: 'NOT_FOUND',
|
||
}, { status: 404 })
|
||
}
|
||
|
||
// 获取使用该模板的素材列表(最多10个,用于预览)
|
||
const materials = await prisma.material.findMany({
|
||
where: { templateId: id },
|
||
take: 10,
|
||
orderBy: { createdAt: 'desc' },
|
||
select: {
|
||
id: true,
|
||
productModel: true,
|
||
previewUrl: true,
|
||
createdAt: true,
|
||
},
|
||
})
|
||
|
||
// 统计字段填写率(如果有素材使用该模板)
|
||
let fieldStats = null
|
||
if (template._count.materials > 0) {
|
||
fieldStats = await calculateFieldStats(id, template.fields)
|
||
}
|
||
|
||
const result = {
|
||
...template,
|
||
materialCount: template._count.materials,
|
||
materials,
|
||
fieldStats,
|
||
_count: undefined,
|
||
}
|
||
|
||
console.log('[TemplateGet] Template fetched:', {
|
||
templateId: id,
|
||
templateName: template.name,
|
||
materialCount: template._count.materials,
|
||
})
|
||
|
||
return NextResponse.json(result)
|
||
} catch (error: any) {
|
||
console.error('[TemplateGet] Get error:', {
|
||
templateId: params.id,
|
||
error: error?.message,
|
||
stack: error?.stack,
|
||
})
|
||
|
||
const errorMessage = error?.message || '获取模板失败'
|
||
return NextResponse.json({
|
||
error: errorMessage,
|
||
errorCode: 'GET_FAILED',
|
||
}, { status: 500 })
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 计算字段填写率统计
|
||
*/
|
||
async function calculateFieldStats(templateId: string, fields: any[]) {
|
||
try {
|
||
// 获取所有使用该模板的素材
|
||
const materials = await prisma.material.findMany({
|
||
where: { templateId },
|
||
select: {
|
||
id: true,
|
||
metadata: true,
|
||
},
|
||
})
|
||
|
||
if (materials.length === 0) {
|
||
return null
|
||
}
|
||
|
||
const totalMaterials = materials.length
|
||
const fieldStats = fields.map(field => {
|
||
let filledCount = 0
|
||
|
||
// 统计该字段在多少素材中被填写
|
||
for (const material of materials) {
|
||
if (!material.metadata) continue
|
||
|
||
try {
|
||
const metadata = JSON.parse(material.metadata)
|
||
const templateFields = metadata.templateFields || {}
|
||
const value = templateFields[field.key]
|
||
|
||
// 判断字段是否被填写(非空、非null、非undefined)
|
||
if (value !== null && value !== undefined && value !== '') {
|
||
filledCount++
|
||
}
|
||
} catch (error) {
|
||
// 忽略JSON解析错误
|
||
}
|
||
}
|
||
|
||
const fillRate = (filledCount / totalMaterials) * 100
|
||
|
||
return {
|
||
fieldKey: field.key,
|
||
fieldLabel: field.label,
|
||
fieldType: field.type,
|
||
filledCount,
|
||
totalCount: totalMaterials,
|
||
fillRate: Math.round(fillRate * 100) / 100, // 保留2位小数
|
||
isLowFillRate: fillRate < 30, // 填写率低于30%标记为低填写率
|
||
}
|
||
})
|
||
|
||
// 按填写率排序
|
||
fieldStats.sort((a, b) => a.fillRate - b.fillRate)
|
||
|
||
return {
|
||
totalMaterials,
|
||
fields: fieldStats,
|
||
lowFillRateFields: fieldStats.filter(f => f.isLowFillRate),
|
||
}
|
||
} catch (error) {
|
||
console.error('[TemplateGet] Error calculating field stats:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
export async function PUT(req: NextRequest, { params }: { params: { id: string } }) {
|
||
try {
|
||
const session = await ensureAdmin()
|
||
if (!session) 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 })
|
||
}
|
||
|
||
const { id } = params
|
||
const body = await req.json()
|
||
const {
|
||
name,
|
||
description,
|
||
status,
|
||
materialType,
|
||
materialKind,
|
||
fieldGroups,
|
||
displayConfig,
|
||
fields,
|
||
version,
|
||
} = body || {}
|
||
|
||
const existing = await prisma.uploadTemplate.findUnique({
|
||
where: { id },
|
||
select: {
|
||
id: true,
|
||
version: true,
|
||
name: true,
|
||
updatedAt: true,
|
||
materialType: true,
|
||
materialKind: true,
|
||
fields: {
|
||
select: {
|
||
id: true,
|
||
key: true,
|
||
type: true,
|
||
label: true,
|
||
},
|
||
orderBy: { orderIndex: 'asc' },
|
||
},
|
||
_count: {
|
||
select: {
|
||
materials: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
if (!existing) {
|
||
console.warn(`[TemplateUpdate] Template not found: ${id}`)
|
||
return NextResponse.json({ error: '模板不存在' }, { status: 404 })
|
||
}
|
||
|
||
// 版本冲突检测
|
||
if (version !== undefined && version !== existing.version) {
|
||
console.warn('[TemplateUpdate] Version conflict detected:', {
|
||
templateId: id,
|
||
templateName: existing.name,
|
||
clientVersion: version,
|
||
serverVersion: existing.version,
|
||
lastUpdated: existing.updatedAt,
|
||
})
|
||
return NextResponse.json({
|
||
error: '模板已被其他人修改,请刷新后重试',
|
||
errorCode: 'VERSION_CONFLICT',
|
||
details: {
|
||
templateName: existing.name,
|
||
clientVersion: version,
|
||
serverVersion: existing.version,
|
||
lastUpdated: existing.updatedAt,
|
||
}
|
||
}, { status: 409 })
|
||
}
|
||
|
||
console.log('[TemplateUpdate] Version check passed:', {
|
||
templateId: id,
|
||
templateName: existing.name,
|
||
currentVersion: existing.version,
|
||
newVersion: (existing.version || 1) + 1,
|
||
})
|
||
|
||
// 验证必填字段
|
||
if (name !== undefined && (typeof name !== 'string' || !name.trim())) {
|
||
console.warn('[TemplateUpdate] Validation failed: invalid name')
|
||
return NextResponse.json({
|
||
error: '模板名称不能为空',
|
||
errorCode: 'VALIDATION_ERROR',
|
||
field: 'name',
|
||
}, { status: 400 })
|
||
}
|
||
|
||
// 验证materialType
|
||
if (materialType !== undefined && (typeof materialType !== 'string' || !materialType.trim())) {
|
||
console.warn('[TemplateUpdate] Validation failed: invalid materialType')
|
||
return NextResponse.json({
|
||
error: '素材类型不能为空',
|
||
errorCode: 'VALIDATION_ERROR',
|
||
field: 'materialType',
|
||
}, { status: 400 })
|
||
}
|
||
|
||
// 验证materialKind
|
||
if (materialKind !== undefined) {
|
||
const allowedKinds = ['IMAGE', 'VIDEO', 'AUDIO', 'OTHER'] as const
|
||
const rawKind = typeof materialKind === 'string' ? materialKind.trim().toUpperCase() : ''
|
||
if (!rawKind || !(allowedKinds as readonly string[]).includes(rawKind)) {
|
||
console.warn('[TemplateUpdate] Validation failed: invalid materialKind:', materialKind)
|
||
return NextResponse.json({
|
||
error: `素材大类必须是以下之一:${allowedKinds.join(', ')}`,
|
||
errorCode: 'VALIDATION_ERROR',
|
||
field: 'materialKind',
|
||
}, { status: 400 })
|
||
}
|
||
}
|
||
|
||
const updateData: any = {}
|
||
if (name !== undefined) updateData.name = name.trim()
|
||
if (description !== undefined) updateData.description = description
|
||
if (status !== undefined) updateData.status = status
|
||
if (fieldGroups !== undefined) updateData.fieldGroups = fieldGroups
|
||
if (displayConfig !== undefined) updateData.displayConfig = displayConfig
|
||
if (materialType !== undefined) {
|
||
updateData.materialType = materialType.trim()
|
||
}
|
||
if (materialKind !== undefined) {
|
||
const rawKind = materialKind.trim().toUpperCase()
|
||
updateData.materialKind = rawKind
|
||
}
|
||
updateData.version = (existing.version || 1) + 1
|
||
|
||
let formattedFields = null
|
||
|
||
if (Array.isArray(fields)) {
|
||
// 检查字段key的唯一性
|
||
const fieldKeys = new Set<string>()
|
||
const duplicateKeys: string[] = []
|
||
|
||
for (const field of fields) {
|
||
const key = String(field.key || '').trim()
|
||
if (!key) {
|
||
console.warn('[TemplateUpdate] 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('[TemplateUpdate] Validation failed: duplicate field keys:', duplicateKeys)
|
||
return NextResponse.json({
|
||
error: `字段key重复:${duplicateKeys.join(', ')}`,
|
||
errorCode: 'VALIDATION_ERROR',
|
||
field: 'fields',
|
||
details: { duplicateKeys },
|
||
}, { status: 400 })
|
||
}
|
||
|
||
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(`[TemplateUpdate] Converted optionsStatic array to JSON for field: ${key}`)
|
||
} else {
|
||
optionsStatic = String(optionsStatic)
|
||
}
|
||
}
|
||
|
||
return {
|
||
id: field.id ?? null,
|
||
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,
|
||
}
|
||
})
|
||
}
|
||
|
||
if (existing._count.materials > 0) {
|
||
const dangerousChanges = detectDangerousTemplateChanges({
|
||
existingTemplate: {
|
||
materialType: existing.materialType,
|
||
materialKind: existing.materialKind,
|
||
fields: existing.fields,
|
||
},
|
||
nextMaterialType: updateData.materialType,
|
||
nextMaterialKind: updateData.materialKind,
|
||
nextFields: formattedFields || undefined,
|
||
})
|
||
|
||
if (dangerousChanges.length > 0) {
|
||
console.warn('[TemplateUpdate] Dangerous structure change blocked:', {
|
||
templateId: id,
|
||
templateName: existing.name,
|
||
materialCount: existing._count.materials,
|
||
dangerousChanges,
|
||
})
|
||
|
||
return NextResponse.json({
|
||
error: `模板“${existing.name}”已有 ${existing._count.materials} 个素材在使用,不能直接修改素材大类、素材类型,也不能删除字段、修改字段 key 或修改字段类型。请新建模板后迁移素材。`,
|
||
errorCode: 'TEMPLATE_STRUCTURE_LOCKED',
|
||
details: {
|
||
templateName: existing.name,
|
||
materialCount: existing._count.materials,
|
||
dangerousChanges,
|
||
},
|
||
}, { status: 409 })
|
||
}
|
||
}
|
||
|
||
const result = await prisma.$transaction(async (tx) => {
|
||
console.log('[TemplateUpdate] Updating template:', {
|
||
templateId: id,
|
||
templateName: existing.name,
|
||
oldVersion: existing.version,
|
||
newVersion: updateData.version,
|
||
fieldsCount: formattedFields?.length || 0,
|
||
})
|
||
|
||
const updated = await tx.uploadTemplate.update({
|
||
where: { id },
|
||
data: updateData,
|
||
select: { id: true, version: true },
|
||
})
|
||
|
||
console.log('[TemplateUpdate] Template updated successfully:', {
|
||
templateId: updated.id,
|
||
newVersion: updated.version,
|
||
})
|
||
|
||
if (formattedFields) {
|
||
const incomingIds = formattedFields.map((f) => f.id).filter(Boolean) as string[]
|
||
|
||
await tx.uploadField.deleteMany({
|
||
where: {
|
||
templateId: id,
|
||
id: { notIn: incomingIds.length ? incomingIds : [''] },
|
||
},
|
||
})
|
||
|
||
for (const field of formattedFields) {
|
||
if (field.id) {
|
||
await tx.uploadField.update({
|
||
where: { id: field.id },
|
||
data: {
|
||
key: field.key,
|
||
label: field.label,
|
||
type: field.type,
|
||
required: field.required,
|
||
orderIndex: field.orderIndex,
|
||
layoutWidth: field.layoutWidth,
|
||
validations: field.validations,
|
||
optionsSource: field.optionsSource,
|
||
optionsStatic: field.optionsStatic,
|
||
allowedExtensions: field.allowedExtensions,
|
||
visibleOnHomeFilter: field.visibleOnHomeFilter,
|
||
isSearchable: field.isSearchable,
|
||
isReadOnly: field.isReadOnly,
|
||
addToCategory: field.addToCategory,
|
||
conditions: field.conditions,
|
||
},
|
||
})
|
||
} else {
|
||
await tx.uploadField.create({
|
||
data: {
|
||
templateId: id,
|
||
key: field.key,
|
||
label: field.label,
|
||
type: field.type,
|
||
required: field.required,
|
||
orderIndex: field.orderIndex,
|
||
layoutWidth: field.layoutWidth,
|
||
validations: field.validations,
|
||
optionsSource: field.optionsSource,
|
||
optionsStatic: field.optionsStatic,
|
||
allowedExtensions: field.allowedExtensions,
|
||
visibleOnHomeFilter: field.visibleOnHomeFilter,
|
||
isSearchable: field.isSearchable,
|
||
isReadOnly: field.isReadOnly,
|
||
addToCategory: field.addToCategory,
|
||
conditions: field.conditions,
|
||
},
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
return updated
|
||
})
|
||
|
||
const template = await prisma.uploadTemplate.findUnique({
|
||
where: { id: result.id },
|
||
select: selectTemplate,
|
||
})
|
||
|
||
console.log('[TemplateUpdate] Syncing field pool after template update...')
|
||
await syncFieldPoolFromTemplates()
|
||
|
||
console.log('[TemplateUpdate] Update completed:', {
|
||
templateId: template?.id,
|
||
templateName: template?.name,
|
||
version: template?.version,
|
||
})
|
||
|
||
return NextResponse.json(template)
|
||
} catch (error: any) {
|
||
console.error('[TemplateUpdate] Update error:', {
|
||
templateId: params.id,
|
||
error: error?.message,
|
||
code: error?.code,
|
||
meta: error?.meta,
|
||
stack: error?.stack,
|
||
})
|
||
|
||
// 处理Prisma特定错误
|
||
if (error?.code) {
|
||
if (error.code === 'P2002') {
|
||
return NextResponse.json({
|
||
error: '模板名称已存在,请使用其他名称',
|
||
errorCode: 'DUPLICATE_NAME',
|
||
}, { status: 400 })
|
||
}
|
||
if (error.code === 'P2003') {
|
||
return NextResponse.json({
|
||
error: '关联数据不存在',
|
||
errorCode: 'FOREIGN_KEY_CONSTRAINT',
|
||
}, { status: 400 })
|
||
}
|
||
if (error.code === 'P2025') {
|
||
return NextResponse.json({
|
||
error: '模板不存在或已被删除',
|
||
errorCode: 'NOT_FOUND',
|
||
}, { status: 404 })
|
||
}
|
||
}
|
||
|
||
const errorMessage = error?.message || '更新模板失败'
|
||
return NextResponse.json({
|
||
error: errorMessage,
|
||
errorCode: 'UPDATE_FAILED',
|
||
}, { status: 500 })
|
||
}
|
||
}
|
||
|
||
export async function DELETE(_: NextRequest, { params }: { params: { id: string } }) {
|
||
try {
|
||
const session = await ensureAdmin()
|
||
if (!session) 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 })
|
||
}
|
||
|
||
const { id } = params
|
||
|
||
// 检查模板是否存在
|
||
const template = await prisma.uploadTemplate.findUnique({
|
||
where: { id },
|
||
select: {
|
||
id: true,
|
||
name: true,
|
||
_count: {
|
||
select: { materials: true }
|
||
}
|
||
},
|
||
})
|
||
|
||
if (!template) {
|
||
console.warn(`[TemplateDelete] Template not found: ${id}`)
|
||
return NextResponse.json({ error: '模板不存在' }, { status: 404 })
|
||
}
|
||
|
||
// 检查是否有素材使用该模板
|
||
const materialCount = template._count.materials
|
||
if (materialCount > 0) {
|
||
console.warn('[TemplateDelete] Cannot delete template with materials:', {
|
||
templateId: id,
|
||
templateName: template.name,
|
||
materialCount,
|
||
})
|
||
return NextResponse.json({
|
||
error: `无法删除模板"${template.name}",因为有 ${materialCount} 个素材正在使用该模板。请先删除或迁移这些素材。`,
|
||
errorCode: 'TEMPLATE_IN_USE',
|
||
details: {
|
||
templateName: template.name,
|
||
materialCount,
|
||
}
|
||
}, { status: 400 })
|
||
}
|
||
|
||
console.log('[TemplateDelete] Deleting template:', {
|
||
templateId: id,
|
||
templateName: template.name,
|
||
})
|
||
|
||
await prisma.uploadTemplate.delete({ where: { id } })
|
||
|
||
console.log('[TemplateDelete] Syncing field pool after template deletion...')
|
||
await syncFieldPoolFromTemplates()
|
||
|
||
console.log('[TemplateDelete] Template deleted successfully:', {
|
||
templateId: id,
|
||
templateName: template.name,
|
||
})
|
||
|
||
return NextResponse.json({ success: true })
|
||
} catch (error: any) {
|
||
console.error('[TemplateDelete] Delete error:', {
|
||
templateId: params.id,
|
||
error: error?.message,
|
||
code: error?.code,
|
||
stack: error?.stack,
|
||
})
|
||
|
||
// 处理Prisma特定错误
|
||
if (error?.code === 'P2025') {
|
||
return NextResponse.json({
|
||
error: '模板不存在或已被删除',
|
||
errorCode: 'NOT_FOUND',
|
||
}, { status: 404 })
|
||
}
|
||
|
||
const errorMessage = error?.message || '删除模板失败'
|
||
return NextResponse.json({
|
||
error: errorMessage,
|
||
errorCode: 'DELETE_FAILED',
|
||
}, { status: 500 })
|
||
}
|
||
}
|
||
|
||
|