gongxiangp2/lib/material-cleanup.ts
2026-05-07 16:37:22 +08:00

233 lines
7.0 KiB
TypeScript

/**
* 统一的素材文件清理逻辑
* 根据不同的素材类型,收集所有需要删除的文件和关联记录。
*/
import path from 'path'
import {
buildPublicDisplayUrl,
buildWhiteBackgroundPublicDisplayPathFromAssetPath,
} from './public-display'
import { prisma } from './prisma'
import {
LEGACY_STANDARD_MODEL_WHITE_BG_ASSET_TYPE,
LEGACY_STANDARD_MODEL_WHITE_BG_ASSET_TYPE_PREFIX,
STANDARD_MODEL_WHITE_BG_OTHER_ASSET_TYPE_PREFIX,
STANDARD_MODEL_WHITE_BG_ASSET_TYPE_PREFIX,
STANDARD_MODEL_WHITE_BG_VIEW_ASSET_TYPE_PREFIX,
} from './standard-model-white-background'
export interface MaterialFileInfo {
fileUrls: string[]
directories: string[]
relatedRecords?: {
assets?: number
skus?: number
}
}
export function normalizeDisplayImages(displayImages: unknown): string[] {
if (!displayImages) {
return []
}
if (Array.isArray(displayImages)) {
return displayImages.filter(
(img): img is string => typeof img === 'string' && img.trim().length > 0,
)
}
if (typeof displayImages === 'string') {
try {
const parsed = JSON.parse(displayImages)
if (Array.isArray(parsed)) {
return parsed.filter(
(img): img is string => typeof img === 'string' && img.trim().length > 0,
)
}
} catch {
// 旧数据可能直接存成单个 URL 字符串,这里降级兼容。
}
return displayImages.trim() ? [displayImages] : []
}
return []
}
/**
* 收集素材的所有文件路径
* @param materialId 素材 ID
* @param material 素材对象,可选;不传时会自动查询
*/
export async function collectMaterialFiles(
materialId: string,
material?: any,
): Promise<MaterialFileInfo> {
const fileUrls: string[] = []
const directories: string[] = []
const relatedRecords: { assets?: number; skus?: number } = {}
if (!material) {
material = await prisma.material.findUnique({
where: { id: materialId },
})
}
if (!material) {
return { fileUrls: [], directories: [], relatedRecords }
}
const materialType = String((material as any).materialType || '')
if ((material as any).zipFilePath) {
fileUrls.push((material as any).zipFilePath)
}
if (
(material as any).videoSourceUrl &&
(material as any).videoSourceUrl !== (material as any).zipFilePath
) {
fileUrls.push((material as any).videoSourceUrl)
}
if ((material as any).previewUrl) {
fileUrls.push((material as any).previewUrl)
}
if ((material as any).videoPreview540Url) {
fileUrls.push((material as any).videoPreview540Url)
}
if ((material as any).videoPreviewClipUrl) {
fileUrls.push((material as any).videoPreviewClipUrl)
}
if ((material as any).videoCoverUrl) {
fileUrls.push((material as any).videoCoverUrl)
}
fileUrls.push(...normalizeDisplayImages((material as any).displayImages))
// 任务类素材都可能存在 MaterialAsset 记录。
const taskMaterialTypes = ['任务-详情', '任务-页面', '任务-其他', '任务-3D原图', '任务-AI原图', '任务-实拍原图', '任务-视频截图', '证书-检测报告'];
if (taskMaterialTypes.includes(materialType)) {
const assets = await prisma.materialAsset.findMany({
where: { materialId },
select: { fileUrl: true },
})
relatedRecords.assets = assets.length
for (const asset of assets) {
if (asset.fileUrl) {
fileUrls.push(asset.fileUrl)
}
}
if (materialType === '任务-详情' || materialType === '任务-页面') {
directories.push(path.join(process.cwd(), 'uploads', 'product-package', materialId))
}
}
if (materialType === '标准模型') {
const skus = await prisma.materialSku.findMany({
where: {
materialId,
},
select: {
filePath: true,
previewUrl: true,
},
})
relatedRecords.skus = skus.length
for (const sku of skus) {
if (sku.filePath) {
fileUrls.push(sku.filePath)
}
if (sku.previewUrl) {
fileUrls.push(sku.previewUrl)
}
}
// 兼容旧版整素材白底图 ZIP 键,以及新版 SKU 级白底图 ZIP 键。
const whiteBgAssets = await prisma.materialAsset.findMany({
where: {
materialId,
OR: [
{ assetType: LEGACY_STANDARD_MODEL_WHITE_BG_ASSET_TYPE },
{ assetType: { startsWith: LEGACY_STANDARD_MODEL_WHITE_BG_ASSET_TYPE_PREFIX } },
{ assetType: { startsWith: STANDARD_MODEL_WHITE_BG_ASSET_TYPE_PREFIX } },
{ assetType: { startsWith: STANDARD_MODEL_WHITE_BG_VIEW_ASSET_TYPE_PREFIX } },
{ assetType: { startsWith: STANDARD_MODEL_WHITE_BG_OTHER_ASSET_TYPE_PREFIX } },
],
},
select: { fileUrl: true },
})
relatedRecords.assets = whiteBgAssets.length;
for (const asset of whiteBgAssets) {
if (asset.fileUrl) {
fileUrls.push(asset.fileUrl)
const publicDisplayPath = buildWhiteBackgroundPublicDisplayPathFromAssetPath(asset.fileUrl)
const publicDisplayUrl = publicDisplayPath ? buildPublicDisplayUrl(publicDisplayPath) : null
if (publicDisplayUrl) {
fileUrls.push(publicDisplayUrl)
}
}
}
directories.push(path.join(process.cwd(), 'uploads', 'skus', materialId, 'white-background'))
}
return {
fileUrls: Array.from(new Set(fileUrls.filter((fileUrl) => fileUrl && fileUrl.trim()))),
directories: Array.from(new Set(directories.filter((dir) => dir && dir.trim()))),
relatedRecords,
}
}
/**
* 删除素材关联记录
* @param materialId 素材 ID
* @param materialType 素材类型
*/
export async function deleteMaterialRelatedRecords(
materialId: string,
materialType: string,
): Promise<{ deletedAssets?: number; deletedSkus?: number }> {
const result: { deletedAssets?: number; deletedSkus?: number } = {}
const taskMaterialTypes = ['任务-详情', '任务-页面', '任务-其他', '任务-3D原图', '任务-AI原图', '任务-实拍原图', '任务-视频截图', '证书-检测报告'];
if (taskMaterialTypes.includes(materialType)) {
const deleteResult = await prisma.materialAsset.deleteMany({
where: { materialId },
})
result.deletedAssets = deleteResult.count
}
if (materialType === '标准模型') {
const deleteSkuResult = await prisma.materialSku.deleteMany({
where: { materialId },
})
result.deletedSkus = deleteSkuResult.count
const deleteWhiteBgResult = await prisma.materialAsset.deleteMany({
where: {
materialId,
OR: [
{ assetType: LEGACY_STANDARD_MODEL_WHITE_BG_ASSET_TYPE },
{ assetType: { startsWith: LEGACY_STANDARD_MODEL_WHITE_BG_ASSET_TYPE_PREFIX } },
{ assetType: { startsWith: STANDARD_MODEL_WHITE_BG_ASSET_TYPE_PREFIX } },
{ assetType: { startsWith: STANDARD_MODEL_WHITE_BG_VIEW_ASSET_TYPE_PREFIX } },
{ assetType: { startsWith: STANDARD_MODEL_WHITE_BG_OTHER_ASSET_TYPE_PREFIX } },
],
},
})
result.deletedAssets = (result.deletedAssets || 0) + deleteWhiteBgResult.count
}
return result
}