359 lines
11 KiB
TypeScript

'use server'
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { createMaterialLog } from '@/lib/material-log'
import { deleteFileIfUnreferenced, deleteLocalFile } from '@/lib/file-cleanup'
import {
getWhiteBackgroundAssetWhereForSku,
getWhiteBackgroundRelativeDirectory,
} from '@/lib/standard-model-white-background'
import { canManageSkuMaterial } from '../authorization'
function normalizeSkuName(value: string): string {
return String(value || '').replace(/[^a-zA-Z0-9\-_.]/g, '').toUpperCase().trim()
}
async function findDuplicateStandardModelSkuName(skuName: string, excludeSkuId?: string) {
return prisma.materialSku.findFirst({
where: {
deletedAt: null,
skuName: {
equals: skuName,
mode: 'insensitive',
},
...(excludeSkuId ? { NOT: { id: excludeSkuId } } : {}),
material: {
is: {
materialType: '标准模型',
deletedAt: null,
},
},
},
select: { id: true },
})
}
export async function GET(
request: NextRequest,
{ params }: { params: { id: string; skuId: string } }
) {
try {
const { id: materialId, skuId } = params
const sku = await prisma.materialSku.findFirst({
where: {
id: skuId,
materialId,
deletedAt: null,
},
})
if (!sku) {
return NextResponse.json({ error: 'SKU 不存在' }, { status: 404 })
}
const uploader = sku.whiteBackgroundUploadedBy
? await prisma.user.findUnique({
where: { id: sku.whiteBackgroundUploadedBy },
select: { id: true, username: true, realName: true },
})
: null
return NextResponse.json({
id: sku.id,
materialId: sku.materialId,
skuName: sku.skuName,
previewUrl: (sku as any).previewUrl || '',
zipFileName: (sku as any).fileName || '',
zipFilePath: sku.filePath,
zipFileSize: sku.fileSize.toString(),
sortOrder: sku.sortOrder,
whiteBackgroundSourceType: sku.whiteBackgroundSourceType,
whiteBackgroundUploadedBy: sku.whiteBackgroundUploadedBy,
whiteBackgroundUploadedAt: sku.whiteBackgroundUploadedAt,
whiteBackgroundUploader: uploader,
createdAt: sku.createdAt,
updatedAt: sku.updatedAt,
})
} catch (error) {
console.error('获取 SKU 详情失败:', error)
return NextResponse.json({ error: '获取 SKU 详情失败' }, { status: 500 })
}
}
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string; skuId: string } }
) {
try {
const session = await getServerSession(authOptions)
if (!session?.user || !(session.user as any).id) {
return NextResponse.json({ error: '请先登录' }, { status: 401 })
}
const { id: materialId, skuId } = params
const existingSku = await prisma.materialSku.findFirst({
where: { id: skuId, materialId, deletedAt: null },
include: {
material: {
select: {
userId: true,
},
},
},
})
if (!existingSku) {
return NextResponse.json({ error: 'SKU 不存在' }, { status: 404 })
}
const userId = (session.user as any).id
if (!(await canManageSkuMaterial(session.user as any, existingSku.material))) {
return NextResponse.json({ error: '无权限操作此素材' }, { status: 403 })
}
const body = await request.json()
const { skuName, previewUrl, zipFileName, zipFilePath, zipFileSize, sortOrder } = body
const normalizedSkuName = skuName !== undefined ? normalizeSkuName(skuName) : undefined
if (normalizedSkuName !== undefined && !normalizedSkuName) {
return NextResponse.json({ error: 'SKU 名称不能为空' }, { status: 400 })
}
if (normalizedSkuName !== undefined && normalizedSkuName !== existingSku.skuName) {
const duplicateSku = await findDuplicateStandardModelSkuName(normalizedSkuName, skuId)
if (duplicateSku) {
return NextResponse.json({ error: '所有标准模型下 SKU 名称不能重复' }, { status: 409 })
}
}
const updateData: any = {}
if (normalizedSkuName !== undefined) updateData.skuName = normalizedSkuName
if (previewUrl !== undefined) updateData.previewUrl = previewUrl || null
if (zipFileName !== undefined) updateData.fileName = zipFileName || null
if (zipFilePath !== undefined) updateData.filePath = zipFilePath
if (zipFileSize !== undefined) updateData.fileSize = BigInt(zipFileSize)
if (sortOrder !== undefined) updateData.sortOrder = sortOrder
const updatedSku = await prisma.materialSku.update({
where: { id: skuId },
data: updateData,
})
const changes: Record<string, { oldValue: unknown; newValue: unknown }> = {
skuOperation: {
oldValue: null,
newValue: 'UPDATE',
},
}
if (updateData.skuName !== undefined && existingSku.skuName !== updatedSku.skuName) {
changes.skuName = {
oldValue: existingSku.skuName,
newValue: updatedSku.skuName,
}
}
if (updateData.previewUrl !== undefined && (existingSku as any).previewUrl !== (updatedSku as any).previewUrl) {
changes.skuPreviewUrl = {
oldValue: (existingSku as any).previewUrl || null,
newValue: (updatedSku as any).previewUrl || null,
}
}
if (updateData.fileName !== undefined && (existingSku as any).fileName !== (updatedSku as any).fileName) {
changes.skuFileName = {
oldValue: (existingSku as any).fileName || null,
newValue: (updatedSku as any).fileName || null,
}
}
if (updateData.filePath !== undefined && existingSku.filePath !== updatedSku.filePath) {
changes.skuFilePath = {
oldValue: existingSku.filePath,
newValue: updatedSku.filePath,
}
}
if (updateData.fileSize !== undefined && existingSku.fileSize.toString() !== updatedSku.fileSize.toString()) {
changes.skuFileSize = {
oldValue: existingSku.fileSize.toString(),
newValue: updatedSku.fileSize.toString(),
}
}
if (updateData.sortOrder !== undefined && existingSku.sortOrder !== updatedSku.sortOrder) {
changes.skuSortOrder = {
oldValue: existingSku.sortOrder,
newValue: updatedSku.sortOrder,
}
}
if (Object.keys(changes).length > 1) {
await createMaterialLog({
materialId: updatedSku.materialId,
userId,
action: 'UPDATE',
changes,
})
}
return NextResponse.json({
id: updatedSku.id,
materialId: updatedSku.materialId,
skuName: updatedSku.skuName,
previewUrl: (updatedSku as any).previewUrl || '',
zipFileName: (updatedSku as any).fileName || '',
zipFilePath: updatedSku.filePath,
zipFileSize: updatedSku.fileSize.toString(),
sortOrder: updatedSku.sortOrder,
createdAt: updatedSku.createdAt,
updatedAt: updatedSku.updatedAt,
})
} catch (error) {
console.error('更新 SKU 失败:', error)
return NextResponse.json({ error: '更新 SKU 失败' }, { status: 500 })
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string; skuId: string } }
) {
try {
const session = await getServerSession(authOptions)
if (!session?.user || !(session.user as any).id) {
return NextResponse.json({ error: '请先登录' }, { status: 401 })
}
const { id: materialId, skuId } = params
const existingSku = await prisma.materialSku.findFirst({
where: { id: skuId, materialId, deletedAt: null },
include: {
material: {
select: {
userId: true,
},
},
},
})
if (!existingSku) {
return NextResponse.json({ error: 'SKU 不存在' }, { status: 404 })
}
const userId = (session.user as any).id
if (!(await canManageSkuMaterial(session.user as any, existingSku.material))) {
return NextResponse.json({ error: '无权限操作此素材' }, { status: 403 })
}
const whiteBgAssets = await prisma.materialAsset.findMany({
where: {
materialId: existingSku.materialId,
...getWhiteBackgroundAssetWhereForSku(skuId),
},
select: {
id: true,
fileUrl: true,
},
})
await prisma.$transaction(async (tx) => {
if (whiteBgAssets.length > 0) {
await tx.materialAsset.deleteMany({
where: {
materialId: existingSku.materialId,
...getWhiteBackgroundAssetWhereForSku(skuId),
},
})
}
await tx.materialSku.delete({
where: { id: skuId },
})
})
const cleanupTargets = [
existingSku.filePath,
(existingSku as any).previewUrl || '',
...whiteBgAssets.map((asset) => asset.fileUrl || ''),
].filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
const cleanupResults = []
for (const fileUrl of cleanupTargets) {
cleanupResults.push(
await deleteFileIfUnreferenced(fileUrl, {
excludeSkuIds: [skuId],
excludeAssetIds: whiteBgAssets.map((asset) => asset.id),
})
)
}
const whiteBackgroundDirectory = getWhiteBackgroundRelativeDirectory(existingSku.materialId, skuId)
const whiteBackgroundDirectoryResult = await deleteLocalFile(
whiteBackgroundDirectory,
{ allowDirectoryDelete: true },
)
const deletedFileCount = cleanupResults.filter((item) => item.success && !item.skipped).length
const skippedFileCount = cleanupResults.filter((item) => item.skipped).length
const failedFileCount =
cleanupResults.filter((item) => !item.success).length +
(whiteBackgroundDirectoryResult.success ? 0 : 1)
await createMaterialLog({
materialId: existingSku.materialId,
userId,
action: 'UPDATE',
changes: {
skuOperation: {
oldValue: null,
newValue: 'DELETE',
},
skuId: {
oldValue: existingSku.id,
newValue: existingSku.id,
},
skuName: {
oldValue: existingSku.skuName,
newValue: existingSku.skuName,
},
deleteMode: {
oldValue: 'active',
newValue: 'permanent',
},
fileCleanup: {
oldValue: null,
newValue: {
deleted: deletedFileCount,
skipped: skippedFileCount,
failed: failedFileCount,
},
},
},
})
return NextResponse.json({
success: true,
message: 'SKU 已删除',
details: {
deletedFiles: deletedFileCount,
skippedFiles: skippedFileCount,
failedFiles: failedFileCount,
deletedWhiteBackgroundAssets: whiteBgAssets.length,
deletedWhiteBackgroundDirectory: whiteBackgroundDirectoryResult.success,
},
})
} catch (error) {
console.error('删除 SKU 失败:', error)
return NextResponse.json({ error: '删除 SKU 失败' }, { status: 500 })
}
}