182 lines
5.4 KiB
TypeScript
182 lines
5.4 KiB
TypeScript
/**
|
||
* 分析缺失 material_assets 记录的素材
|
||
* 只读分析,不做任何修改
|
||
*/
|
||
|
||
import { prisma } from '../lib/prisma';
|
||
import { access } from 'fs/promises';
|
||
import path from 'path';
|
||
|
||
interface AnalysisResult {
|
||
materialId: string;
|
||
materialType: string;
|
||
productModel: string;
|
||
previewUrl: string;
|
||
zipFileName: string;
|
||
zipFileSize: bigint;
|
||
createdAt: Date;
|
||
hasPreviewUrl: boolean;
|
||
hasZipFile: boolean;
|
||
zipFileExists: boolean;
|
||
previewUrlAccessible: boolean;
|
||
}
|
||
|
||
async function checkFileExists(filePath: string): Promise<boolean> {
|
||
try {
|
||
await access(filePath);
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async function analyzeMissingAssets() {
|
||
console.log('========== 分析缺失 material_assets 记录的素材 ==========\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,
|
||
zipFileSize: true,
|
||
createdAt: true,
|
||
assets: {
|
||
select: { id: true },
|
||
},
|
||
},
|
||
orderBy: { createdAt: 'asc' },
|
||
});
|
||
|
||
console.log(`总共找到 ${materials.length} 个任务类型素材\n`);
|
||
|
||
// 筛选出没有 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 byType: { [key: string]: number } = {};
|
||
missingAssets.forEach(m => {
|
||
byType[m.materialType] = (byType[m.materialType] || 0) + 1;
|
||
});
|
||
|
||
console.log('按类型统计:');
|
||
Object.entries(byType).forEach(([type, count]) => {
|
||
console.log(` ${type}: ${count} 个`);
|
||
});
|
||
console.log('');
|
||
|
||
// 详细分析每个素材
|
||
console.log('详细分析:');
|
||
console.log('='.repeat(100));
|
||
|
||
const results: AnalysisResult[] = [];
|
||
|
||
for (const material of missingAssets) {
|
||
const hasPreviewUrl = !!material.previewUrl && material.previewUrl.trim().length > 0;
|
||
const hasZipFile = !!material.zipFileName && material.zipFileName.trim().length > 0;
|
||
|
||
// 检查 zip 文件是否存在
|
||
let zipFileExists = false;
|
||
if (hasZipFile) {
|
||
const zipPath = path.join(process.cwd(), material.zipFileName);
|
||
zipFileExists = await checkFileExists(zipPath);
|
||
}
|
||
|
||
// 检查预览图 URL 是否可访问(简单判断是否为有效 URL)
|
||
const previewUrlAccessible = hasPreviewUrl && (
|
||
material.previewUrl.startsWith('http://') ||
|
||
material.previewUrl.startsWith('https://')
|
||
);
|
||
|
||
results.push({
|
||
materialId: material.id,
|
||
materialType: material.materialType,
|
||
productModel: material.productModel || '',
|
||
previewUrl: material.previewUrl || '',
|
||
zipFileName: material.zipFileName || '',
|
||
zipFileSize: material.zipFileSize,
|
||
createdAt: material.createdAt,
|
||
hasPreviewUrl,
|
||
hasZipFile,
|
||
zipFileExists,
|
||
previewUrlAccessible,
|
||
});
|
||
|
||
const status = zipFileExists && previewUrlAccessible ? '✅' : '⚠️';
|
||
console.log(`${status} ${material.materialType} | ${material.productModel || '(无型号)'}`);
|
||
console.log(` ID: ${material.id}`);
|
||
console.log(` 创建时间: ${material.createdAt.toISOString()}`);
|
||
console.log(` 预览图: ${hasPreviewUrl ? (previewUrlAccessible ? '✅ 有效' : '❌ 无效') : '❌ 缺失'}`);
|
||
console.log(` ZIP文件: ${hasZipFile ? (zipFileExists ? '✅ 存在' : '❌ 不存在') : '❌ 缺失'}`);
|
||
if (hasZipFile) {
|
||
console.log(` ZIP路径: ${material.zipFileName}`);
|
||
}
|
||
console.log('');
|
||
}
|
||
|
||
console.log('='.repeat(100));
|
||
console.log('\n汇总统计:');
|
||
|
||
const validCount = results.filter(r => r.zipFileExists && r.previewUrlAccessible).length;
|
||
const invalidCount = results.length - validCount;
|
||
|
||
console.log(` ✅ 可安全迁移: ${validCount} 个(文件完整)`);
|
||
console.log(` ⚠️ 需要检查: ${invalidCount} 个(文件缺失或无效)`);
|
||
|
||
// 输出可迁移的素材ID列表
|
||
if (validCount > 0) {
|
||
console.log('\n可安全迁移的素材ID列表:');
|
||
results
|
||
.filter(r => r.zipFileExists && r.previewUrlAccessible)
|
||
.forEach(r => {
|
||
console.log(` ${r.materialId} | ${r.materialType} | ${r.productModel}`);
|
||
});
|
||
}
|
||
|
||
// 输出需要检查的素材ID列表
|
||
if (invalidCount > 0) {
|
||
console.log('\n⚠️ 需要人工检查的素材ID列表:');
|
||
results
|
||
.filter(r => !r.zipFileExists || !r.previewUrlAccessible)
|
||
.forEach(r => {
|
||
const issues: string[] = [];
|
||
if (!r.zipFileExists) issues.push('ZIP文件不存在');
|
||
if (!r.previewUrlAccessible) issues.push('预览图无效');
|
||
console.log(` ${r.materialId} | ${r.materialType} | ${r.productModel} | ${issues.join(', ')}`);
|
||
});
|
||
}
|
||
|
||
console.log('\n========== 分析完成 ==========');
|
||
}
|
||
|
||
// 执行分析
|
||
analyzeMissingAssets()
|
||
.then(() => {
|
||
console.log('\n✅ 分析成功完成');
|
||
process.exit(0);
|
||
})
|
||
.catch((error) => {
|
||
console.error('\n❌ 分析失败:', error);
|
||
process.exit(1);
|
||
});
|