359 lines
11 KiB
TypeScript
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 })
|
|
}
|
|
}
|