306 lines
9.7 KiB
TypeScript
306 lines
9.7 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 { 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 })
|
||
}
|
||
}
|