202 lines
5.9 KiB
TypeScript
202 lines
5.9 KiB
TypeScript
/**
|
||
* 为缺失 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);
|
||
});
|