2026-05-07 16:37:22 +08:00

306 lines
9.7 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.

'use server'
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { rewriteMediaUrlForClient } from '@/lib/url-rewrite'
import { createMaterialLog } from '@/lib/material-log'
import { resolveWhiteBackgroundPreviewFileUrl } from '@/lib/white-background-preview'
import {
countWhiteBackgroundAssets,
countWhiteBackgroundFixedViews,
countWhiteBackgroundOtherImages,
getWhiteBackgroundAssetWhereForSku,
groupWhiteBackgroundAssetsBySku,
hasAnyWhiteBackgroundAssetGroup,
hasCompleteWhiteBackgroundAssetGroup,
} from '@/lib/standard-model-white-background'
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,
materialId: true,
},
})
}
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const materialId = params.id
const { searchParams } = new URL(request.url)
const validateSkuName = searchParams.get('validateSkuName')
const excludeSkuId = searchParams.get('excludeSkuId') || ''
const material = await prisma.material.findUnique({
where: { id: materialId },
select: { id: true, materialType: true, userId: true },
})
if (!material) {
return NextResponse.json({ error: '素材不存在' }, { status: 404 })
}
if (validateSkuName !== null) {
const normalizedSkuName = normalizeSkuName(validateSkuName)
if (!normalizedSkuName) {
return NextResponse.json({ valid: false, error: 'SKU 名称不能为空' }, { status: 400 })
}
const duplicateSku = await findDuplicateStandardModelSkuName(
normalizedSkuName,
excludeSkuId || undefined,
)
return NextResponse.json({
valid: !duplicateSku,
normalizedSkuName,
error: duplicateSku ? '所有标准模型下 SKU 名称不能重复' : null,
})
}
const skus = await prisma.materialSku.findMany({
where: {
materialId,
deletedAt: null,
},
orderBy: { sortOrder: 'asc' },
})
const whiteBackgroundAssets = skus.length > 0
? await prisma.materialAsset.findMany({
where: {
materialId,
OR: skus.flatMap((sku) => getWhiteBackgroundAssetWhereForSku(sku.id).OR),
},
select: {
id: true,
assetType: true,
fileName: true,
fileUrl: true,
sortOrder: true,
},
})
: []
const whiteBackgroundMap = groupWhiteBackgroundAssetsBySku(whiteBackgroundAssets)
const uploaderIds = Array.from(
new Set(skus.map((sku) => sku.whiteBackgroundUploadedBy).filter((value): value is string => Boolean(value))),
)
const uploaders = uploaderIds.length > 0
? await prisma.user.findMany({
where: { id: { in: uploaderIds } },
select: { id: true, username: true, realName: true },
})
: []
const uploaderMap = new Map(uploaders.map((user) => [user.id, user]))
return NextResponse.json({
skus: skus.map((sku) => {
const whiteBackgroundGroup = whiteBackgroundMap.get(sku.id)
const whiteBackgroundUploaded = hasAnyWhiteBackgroundAssetGroup(whiteBackgroundGroup)
const whiteBackgroundComplete = hasCompleteWhiteBackgroundAssetGroup(whiteBackgroundGroup)
const whiteBackgroundFixedCount = countWhiteBackgroundFixedViews(whiteBackgroundGroup)
const whiteBackgroundOtherCount = countWhiteBackgroundOtherImages(whiteBackgroundGroup)
const whiteBackgroundCount = countWhiteBackgroundAssets(whiteBackgroundGroup)
const previewAsset =
Object.values(whiteBackgroundGroup?.fixed || {}).find(Boolean) ||
whiteBackgroundGroup?.other?.[0] ||
whiteBackgroundGroup?.zipAssets?.[0]
const previewAssetUrl = resolveWhiteBackgroundPreviewFileUrl(previewAsset?.fileUrl || '')
const uploader = sku.whiteBackgroundUploadedBy
? uploaderMap.get(sku.whiteBackgroundUploadedBy) || null
: null
return {
id: sku.id,
materialId: sku.materialId,
skuName: sku.skuName,
previewUrl: rewriteMediaUrlForClient((sku as any).previewUrl || '', request, 'image') || '',
zipFileName: (sku as any).fileName || '',
zipFilePath: sku.filePath,
zipFileSize: sku.fileSize.toString(),
sortOrder: sku.sortOrder,
createdAt: sku.createdAt,
updatedAt: sku.updatedAt,
whiteBackgroundUploaded,
whiteBackgroundComplete,
whiteBackgroundFixedCount,
whiteBackgroundOtherCount,
whiteBackgroundCount,
whiteBackgroundSourceType: sku.whiteBackgroundSourceType,
whiteBackgroundUploadedBy: sku.whiteBackgroundUploadedBy,
whiteBackgroundUploadedAt: sku.whiteBackgroundUploadedAt,
whiteBackgroundUploader: uploader,
whiteBackgroundFileName: whiteBackgroundUploaded ? `${sku.skuName}_白底图.zip` : '',
whiteBackgroundFileUrl: rewriteMediaUrlForClient(previewAssetUrl || '', request, 'image') || '',
}
}),
total: skus.length,
})
} catch (error) {
console.error('获取 SKU 列表失败:', error)
return NextResponse.json({ error: '获取 SKU 列表失败' }, { status: 500 })
}
}
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
let body: any = null
let normalizedSkuName = ''
let userId = ''
try {
const session = await getServerSession(authOptions)
if (!session?.user || !(session.user as any).id) {
return NextResponse.json({ error: '请先登录' }, { status: 401 })
}
userId = (session.user as any).id
const materialId = params.id
const material = await prisma.material.findUnique({
where: { id: materialId },
select: { id: true, materialType: true },
})
if (!material) {
return NextResponse.json({ error: '素材不存在' }, { status: 404 })
}
if (material.materialType !== '标准模型') {
return NextResponse.json({ error: '只有标准模型类型才能添加 SKU' }, { status: 400 })
}
body = await request.json()
const { skuName, previewUrl, zipFileName, zipFilePath, zipFileSize } = body
normalizedSkuName = normalizeSkuName(skuName)
console.info('[SKU Create] start', JSON.stringify({
materialId,
userId,
rawSkuName: skuName,
normalizedSkuName,
hasPreviewUrl: Boolean(previewUrl),
hasZipFilePath: Boolean(zipFilePath),
zipFileName: zipFileName || null,
zipFileSize: Number(zipFileSize || 0),
}))
if (!normalizedSkuName || !previewUrl) {
return NextResponse.json({ error: '缺少必填字段:skuName 和 previewUrl' }, { status: 400 })
}
const duplicateSku = await findDuplicateStandardModelSkuName(normalizedSkuName)
if (duplicateSku) {
return NextResponse.json({ error: '所有标准模型下 SKU 名称不能重复' }, { status: 409 })
}
const maxSortOrder = await prisma.materialSku.aggregate({
where: { materialId },
_max: { sortOrder: true },
})
const newSortOrder = (maxSortOrder._max.sortOrder ?? -1) + 1
const sku = await prisma.materialSku.create({
data: {
materialId,
skuName: normalizedSkuName,
previewUrl: previewUrl || null,
filePath: zipFilePath || '',
fileName: zipFileName || null,
fileSize: BigInt(zipFileSize || 0),
sortOrder: newSortOrder,
} as any,
})
await createMaterialLog({
materialId,
userId,
action: 'UPDATE',
changes: {
skuOperation: {
oldValue: null,
newValue: 'CREATE',
},
skuId: {
oldValue: null,
newValue: sku.id,
},
skuName: {
oldValue: null,
newValue: sku.skuName,
},
skuFileName: {
oldValue: null,
newValue: (sku as any).fileName || null,
},
skuSortOrder: {
oldValue: null,
newValue: sku.sortOrder,
},
},
})
console.info('[SKU Create] success', JSON.stringify({
materialId,
userId,
skuId: sku.id,
normalizedSkuName: sku.skuName,
sortOrder: sku.sortOrder,
}))
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,
createdAt: sku.createdAt,
updatedAt: sku.updatedAt,
}, { status: 201 })
} catch (error) {
console.error('[SKU Create] failed:', {
materialId: params.id,
userId,
rawSkuName: body?.skuName || null,
normalizedSkuName: normalizedSkuName || null,
hasPreviewUrl: Boolean(body?.previewUrl),
hasZipFilePath: Boolean(body?.zipFilePath),
error,
})
const errorMessage = error instanceof Error ? error.message : String(error)
return NextResponse.json({
error: '添加 SKU 失败',
details: process.env.NODE_ENV === 'development' ? errorMessage : undefined,
}, { status: 500 })
}
}