gongxiangp2/scripts/cleanup-shadow-test-materials.ts

375 lines
11 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.

/**
* 清理影子系统中的测试素材及其文件
* 找出影子系统中有但正式系统中没有的素材,删除这些素材及其所有相关文件
*/
import { PrismaClient } from '@prisma/client'
import { S3Client, DeleteObjectCommand } from '@aws-sdk/client-s3'
import { existsSync, unlinkSync } from 'fs'
import path from 'path'
const PROD_DB_URL = process.env.PROD_DATABASE_URL
const SHADOW_DB_URL = process.env.SHADOW_DATABASE_URL
if (!PROD_DB_URL || !SHADOW_DB_URL) {
throw new Error('必须设置 PROD_DATABASE_URL 和 SHADOW_DATABASE_URL')
}
const prodPrisma = new PrismaClient({
datasources: {
db: {
url: PROD_DB_URL,
},
},
})
const shadowPrisma = new PrismaClient({
datasources: {
db: {
url: SHADOW_DB_URL,
},
},
})
// MinIO客户端
const s3Client = new S3Client({
endpoint: process.env.S3_ENDPOINT || 'http://192.168.58.20:9000',
region: process.env.S3_REGION || 'us-east-1',
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
},
forcePathStyle: true,
})
const S3_BUCKET = process.env.S3_BUCKET || 'gongxiangpan'
const UPLOAD_DIR = process.env.UPLOAD_DIR || path.join(process.cwd(), 'uploads-shadow')
/**
* 从URL提取MinIO Key
*/
function extractS3Key(url: string): string | null {
try {
const urlObj = new URL(url)
let key = urlObj.pathname.replace(/^\//, '')
// 如果路径包含bucket名称,移除它
if (key.startsWith(`${S3_BUCKET}/`)) {
key = key.replace(`${S3_BUCKET}/`, '')
}
// 解码URL编码的路径
try {
key = decodeURIComponent(key)
} catch {
// 如果解码失败,使用原始路径
}
return key
} catch {
return null
}
}
/**
* 删除S3文件
*/
async function deleteS3File(url: string): Promise<boolean> {
try {
const key = extractS3Key(url)
if (!key) {
console.warn(`[Cleanup] 无法提取S3 Key: ${url}`)
return false
}
const command = new DeleteObjectCommand({
Bucket: S3_BUCKET,
Key: key,
})
await s3Client.send(command)
return true
} catch (error: any) {
if (error.name === 'NotFound' || error.$metadata?.httpStatusCode === 404) {
// 文件不存在,视为成功
return true
}
console.error(`[Cleanup] 删除S3文件失败: ${url}`, error.message)
return false
}
}
/**
* 删除本地文件
*/
function deleteLocalFile(filePath: string): boolean {
try {
const absolutePath = path.isAbsolute(filePath)
? filePath
: path.join(process.cwd(), filePath)
if (existsSync(absolutePath)) {
unlinkSync(absolutePath)
return true
}
return true // 文件不存在,视为成功
} catch (error: any) {
console.error(`[Cleanup] 删除本地文件失败: ${filePath}`, error.message)
return false
}
}
/**
* 收集素材的所有文件URL(使用统一的收集函数)
*/
async function collectMaterialFiles(materialId: string, material: any): Promise<string[]> {
const files: string[] = []
// 预览图
if (material.previewUrl && typeof material.previewUrl === 'string') {
files.push(material.previewUrl)
}
// 视频相关文件
if (material.videoSourceUrl && typeof material.videoSourceUrl === 'string') {
files.push(material.videoSourceUrl)
}
if (material.videoPreview540Url && typeof material.videoPreview540Url === 'string') {
files.push(material.videoPreview540Url)
}
if (material.videoPreviewClipUrl && typeof material.videoPreviewClipUrl === 'string') {
files.push(material.videoPreviewClipUrl)
}
if (material.videoCoverUrl && typeof material.videoCoverUrl === 'string') {
files.push(material.videoCoverUrl)
}
// 压缩包文件
if (material.zipFileName && typeof material.zipFileName === 'string') {
// 如果是URL,直接添加
if (material.zipFileName.startsWith('http://') || material.zipFileName.startsWith('https://')) {
files.push(material.zipFileName)
} else {
// 本地文件路径(相对于uploads目录)
const localPath = path.join(UPLOAD_DIR, material.zipFileName)
files.push(localPath)
}
}
if (material.zipFilePath && typeof material.zipFilePath === 'string') {
if (!material.zipFilePath.startsWith('http://') && !material.zipFilePath.startsWith('https://')) {
// 本地文件路径
files.push(material.zipFilePath)
}
}
// 展示图
if (Array.isArray(material.displayImages)) {
material.displayImages.forEach((img: any) => {
if (typeof img === 'string') {
files.push(img)
}
})
}
// 任务-详情:查询资产文件
if (material.materialType === '任务-详情') {
const assets = await shadowPrisma.materialAsset.findMany({
where: { materialId },
select: { fileUrl: true },
})
assets.forEach((asset: any) => {
if (asset.fileUrl) {
files.push(asset.fileUrl)
}
})
}
// 标准模型:查询SKU文件
if (material.materialType === '标准模型') {
const skus = await shadowPrisma.materialSku.findMany({
where: { materialId },
select: { filePath: true, previewUrl: true },
})
skus.forEach((sku: any) => {
if (sku.filePath) {
files.push(sku.filePath)
}
if (sku.previewUrl) {
files.push(sku.previewUrl)
}
})
}
return files.filter(Boolean)
}
async function main() {
console.log('开始清理影子系统中的测试素材...')
console.log('='.repeat(60))
console.log()
try {
// 1. 获取正式系统中的所有素材ID
console.log('📋 步骤 1: 获取正式系统中的素材ID列表...')
const prodMaterials = await prodPrisma.material.findMany({
where: {
deletedAt: null,
},
select: {
id: true,
},
})
const prodMaterialIds = new Set(prodMaterials.map(m => m.id))
console.log(` 正式系统素材数量: ${prodMaterialIds.size}`)
console.log()
// 2. 获取影子系统中的所有素材
console.log('📋 步骤 2: 获取影子系统中的素材...')
const shadowMaterials = await shadowPrisma.material.findMany({
where: {
deletedAt: null,
},
select: {
id: true,
productModel: true,
materialType: true,
previewUrl: true,
videoSourceUrl: true,
videoPreview540Url: true,
videoPreviewClipUrl: true,
videoCoverUrl: true,
zipFileName: true,
zipFilePath: true,
displayImages: true,
},
})
console.log(` 影子系统素材数量: ${shadowMaterials.length}`)
console.log()
// 3. 找出测试素材(影子系统中有但正式系统中没有的)
const testMaterials = shadowMaterials.filter(m => !prodMaterialIds.has(m.id))
console.log(`🔍 步骤 3: 识别测试素材...`)
console.log(` 找到 ${testMaterials.length} 个测试素材`)
console.log()
if (testMaterials.length === 0) {
console.log('✅ 没有发现测试素材,无需清理')
return
}
// 4. 显示测试素材列表
console.log('测试素材列表:')
testMaterials.forEach((m, index) => {
console.log(` ${index + 1}. ${m.productModel} (${m.materialType}) - ID: ${m.id}`)
})
console.log()
// 5. 收集所有需要删除的文件
console.log('📦 步骤 4: 收集测试素材的文件...')
const allFiles: string[] = []
for (const material of testMaterials) {
const files = await collectMaterialFiles(material.id, material)
allFiles.push(...files)
}
const uniqueFiles = Array.from(new Set(allFiles))
console.log(` 找到 ${uniqueFiles.length} 个文件需要删除`)
console.log()
// 6. 删除文件
console.log('🗑️ 步骤 5: 删除测试素材的文件...')
let deletedCount = 0
let failedCount = 0
for (const fileUrl of uniqueFiles) {
if (!fileUrl || typeof fileUrl !== 'string') continue
let success = false
if (fileUrl.startsWith('http://') || fileUrl.startsWith('https://')) {
// S3文件
success = await deleteS3File(fileUrl)
} else {
// 本地文件(可能是相对路径或绝对路径)
// 如果是相对路径,需要转换为绝对路径
let filePath = fileUrl
if (!path.isAbsolute(filePath)) {
// 如果路径以 uploads 开头,使用 UPLOAD_DIR
if (filePath.startsWith('uploads/') || filePath.startsWith('uploads\\')) {
filePath = path.join(process.cwd(), filePath.replace(/^uploads[\/\\]/, ''))
} else {
// 其他相对路径,尝试在 UPLOAD_DIR 中查找
filePath = path.join(process.cwd(), UPLOAD_DIR, filePath)
}
}
success = deleteLocalFile(filePath)
}
if (success) {
deletedCount++
} else {
failedCount++
}
}
console.log(` 删除结果: ${deletedCount} 成功, ${failedCount} 失败`)
console.log()
// 7. 删除关联记录(MaterialAsset, MaterialSku等)
console.log('🗑️ 步骤 6: 删除测试素材的关联记录...')
const testMaterialIds = testMaterials.map(m => m.id)
// 删除 MaterialAsset
const deletedAssets = await shadowPrisma.materialAsset.deleteMany({
where: {
materialId: { in: testMaterialIds },
},
})
console.log(` 删除 MaterialAsset: ${deletedAssets.count} 条`)
// 删除 MaterialSku
const deletedSkus = await shadowPrisma.materialSku.deleteMany({
where: {
materialId: { in: testMaterialIds },
},
})
console.log(` 删除 MaterialSku: ${deletedSkus.count} 条`)
// 删除 MaterialLog
const deletedLogs = await shadowPrisma.materialLog.deleteMany({
where: {
materialId: { in: testMaterialIds },
},
})
console.log(` 删除 MaterialLog: ${deletedLogs.count} 条`)
console.log()
// 8. 删除素材记录
console.log('🗑️ 步骤 7: 删除测试素材记录...')
const deletedMaterials = await shadowPrisma.material.deleteMany({
where: {
id: { in: testMaterialIds },
},
})
console.log(` 删除 Material: ${deletedMaterials.count} 条`)
console.log()
console.log('='.repeat(60))
console.log('✅ 清理完成!')
console.log('='.repeat(60))
console.log()
console.log(`删除统计:`)
console.log(` - 测试素材: ${testMaterials.length} 个`)
console.log(` - 文件: ${deletedCount} 个成功, ${failedCount} 个失败`)
console.log(` - 关联记录: ${deletedAssets.count} 个Asset, ${deletedSkus.count} 个SKU, ${deletedLogs.count} 个Log`)
console.log()
} catch (error) {
console.error('清理失败:', error)
process.exit(1)
} finally {
await prodPrisma.$disconnect()
await shadowPrisma.$disconnect()
}
}
main()