/** * 为缺失 material_assets 记录的素材补充资产记录 * * 安全措施: * 1. 只处理有 previewUrl 和 zipFilePath 的素材 * 2. 先验证文件存在性 * 3. 使用事务确保数据一致性 * 4. 详细日志记录每一步操作 */ import { prisma } from '../lib/prisma'; import { access } from 'fs/promises'; import path from 'path'; const ASSET_TYPE = { PREVIEW: 'preview', ZIP: 'zip', } as const; async function checkFileExists(filePath: string): Promise { try { await access(filePath); return true; } catch { return false; } } async function migrateMissingAssets(dryRun: boolean = true) { console.log('========== 迁移缺失的 material_assets 记录 =========='); console.log(`模式: ${dryRun ? '🔍 DRY RUN(只分析,不修改)' : '✍️ WRITE(实际修改数据库)'}\n`); // 查询所有任务类型的素材 const taskMaterialTypes = [ '任务-3D原图', '任务-AI原图', '任务-实拍原图', '任务-视频截图', '任务-详情', ]; const materials = await prisma.material.findMany({ where: { materialType: { in: taskMaterialTypes }, deletedAt: null, }, select: { id: true, materialType: true, productModel: true, previewUrl: true, zipFileName: true, zipFilePath: true, zipFileSize: true, createdAt: true, assets: { select: { id: true }, }, }, orderBy: { createdAt: 'asc' }, }); console.log(`总共找到 ${materials.length} 个任务类型素材`); // 筛选出没有 assets 记录的素材 const missingAssets = materials.filter(m => m.assets.length === 0); console.log(`其中 ${missingAssets.length} 个素材缺失 material_assets 记录\n`); if (missingAssets.length === 0) { console.log('✅ 所有素材都有完整的 assets 记录!'); return; } // 验证每个素材的文件完整性 const validMaterials: typeof missingAssets = []; const invalidMaterials: typeof missingAssets = []; console.log('验证文件完整性...\n'); for (const material of missingAssets) { const hasPreviewUrl = !!material.previewUrl && material.previewUrl.trim().length > 0; const hasZipFilePath = !!material.zipFilePath && material.zipFilePath.trim().length > 0; // 检查预览图 URL 是否有效 const previewUrlValid = hasPreviewUrl && ( material.previewUrl.startsWith('http://') || material.previewUrl.startsWith('https://') ); // 检查 zip 文件是否存在 let zipFileExists = false; if (hasZipFilePath) { const zipPath = path.join(process.cwd(), material.zipFilePath); zipFileExists = await checkFileExists(zipPath); } const isValid = previewUrlValid && zipFileExists; if (isValid) { validMaterials.push(material); console.log(`✅ ${material.materialType} | ${material.productModel || '(无型号)'}`); console.log(` ID: ${material.id}`); console.log(` 预览图: ${material.previewUrl}`); console.log(` ZIP: ${material.zipFilePath} (${(Number(material.zipFileSize) / 1024 / 1024).toFixed(2)} MB)`); } else { invalidMaterials.push(material); console.log(`❌ ${material.materialType} | ${material.productModel || '(无型号)'}`); console.log(` ID: ${material.id}`); if (!previewUrlValid) console.log(` ⚠️ 预览图无效或缺失`); if (!zipFileExists) console.log(` ⚠️ ZIP文件不存在: ${material.zipFilePath}`); } console.log(''); } console.log('='.repeat(80)); console.log(`\n验证结果:`); console.log(` ✅ 可安全迁移: ${validMaterials.length} 个`); console.log(` ❌ 无法迁移: ${invalidMaterials.length} 个\n`); if (validMaterials.length === 0) { console.log('没有可迁移的素材。'); return; } if (dryRun) { console.log('🔍 DRY RUN 模式 - 不会修改数据库'); console.log('如需实际迁移,请运行: npx tsx scripts/migrate-missing-assets.ts --write\n'); return; } // 实际迁移 console.log('开始迁移...\n'); let successCount = 0; let failCount = 0; for (const material of validMaterials) { try { console.log(`迁移素材: ${material.id} (${material.materialType} | ${material.productModel})`); // 使用事务创建资产记录 await prisma.$transaction(async (tx) => { // 创建预览图资产 await tx.materialAsset.create({ data: { materialId: material.id, assetType: ASSET_TYPE.PREVIEW, fileUrl: material.previewUrl, fileName: 'preview-0.webp', sortOrder: 0, }, }); // 创建 ZIP 资产 await tx.materialAsset.create({ data: { materialId: material.id, assetType: ASSET_TYPE.ZIP, fileUrl: material.zipFilePath, fileName: material.zipFileName, fileSize: material.zipFileSize, sortOrder: 0, }, }); }); console.log(` ✅ 成功创建 2 条资产记录(1 预览图 + 1 ZIP)\n`); successCount++; } catch (error) { console.error(` ❌ 迁移失败:`, error); console.log(''); failCount++; } } console.log('='.repeat(80)); console.log('\n迁移完成!'); console.log(` ✅ 成功: ${successCount} 个`); console.log(` ❌ 失败: ${failCount} 个`); if (invalidMaterials.length > 0) { console.log(` ⚠️ 跳过: ${invalidMaterials.length} 个(文件缺失或无效)`); } console.log('\n========== 迁移结束 =========='); } // 解析命令行参数 const args = process.argv.slice(2); const dryRun = !args.includes('--write'); // 执行迁移 migrateMissingAssets(dryRun) .then(() => { console.log('\n✅ 脚本执行完成'); process.exit(0); }) .catch((error) => { console.error('\n❌ 脚本执行失败:', error); process.exit(1); });