gongxiangp2/scripts/migrate-missing-assets.ts

202 lines
5.9 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.

/**
* 为缺失 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<boolean> {
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);
});