152 lines
4.9 KiB
Bash
152 lines
4.9 KiB
Bash
#!/bin/bash
|
||
# 通过SSH检查缺失的源文件并生成CSV报告
|
||
|
||
SERVER_IP="${SERVER_IP:-192.168.58.20}"
|
||
SERVER_USER="${SERVER_USER:-root}"
|
||
SERVER_UPLOADS_PATH="${SERVER_UPLOADS_PATH:-/mnt/PoolOne/apps/youyidian-3d/uploads}"
|
||
SSH_KEY_PATH="${SSH_KEY_PATH:-$HOME/.ssh/id_rsa}"
|
||
SSH_PORT="${SSH_PORT:-22}"
|
||
|
||
OUTPUT_FILE="${1:-missing-source-files-$(date +%Y%m%d-%H%M%S).csv}"
|
||
|
||
echo "============================================================"
|
||
echo "检查素材源文件缺失情况"
|
||
echo "============================================================"
|
||
echo "服务器: ${SERVER_USER}@${SERVER_IP}:${SSH_PORT}"
|
||
echo "服务器路径: ${SERVER_UPLOADS_PATH}"
|
||
echo "输出文件: ${OUTPUT_FILE}"
|
||
echo ""
|
||
|
||
# 测试SSH连接
|
||
echo "🔌 测试SSH连接..."
|
||
if ! ssh -i "${SSH_KEY_PATH}" -p ${SSH_PORT} -o ConnectTimeout=10 -o StrictHostKeyChecking=no ${SERVER_USER}@${SERVER_IP} "hostname" > /dev/null 2>&1; then
|
||
echo "❌ SSH连接失败"
|
||
exit 1
|
||
fi
|
||
echo "✅ SSH连接成功"
|
||
echo ""
|
||
|
||
# 在服务器上执行检查(通过Docker容器连接数据库)
|
||
echo "📋 查询数据库并检查文件..."
|
||
ssh -i "${SSH_KEY_PATH}" -p ${SSH_PORT} -o StrictHostKeyChecking=no ${SERVER_USER}@${SERVER_IP} << 'EOF'
|
||
cd /mnt/PoolOne/apps/youyidian-3d
|
||
|
||
# 使用Docker容器执行Node.js脚本
|
||
docker exec youyidian-3d-material node -e "
|
||
const { PrismaClient } = require('@prisma/client');
|
||
const { execSync } = require('child_process');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const prisma = new PrismaClient();
|
||
const uploadsPath = '/mnt/PoolOne/apps/youyidian-3d/uploads';
|
||
|
||
async function checkFiles() {
|
||
const materials = await prisma.material.findMany({
|
||
where: { deletedAt: null },
|
||
select: {
|
||
id: true,
|
||
materialType: true,
|
||
productModel: true,
|
||
authorName: true,
|
||
categoryName: true,
|
||
sceneName: true,
|
||
platformName: true,
|
||
productCategoryName: true,
|
||
renderer: true,
|
||
zipFilePath: true,
|
||
zipFileName: true,
|
||
zipFileSize: true,
|
||
createdAt: true,
|
||
updatedAt: true,
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
});
|
||
|
||
const results = [];
|
||
let checked = 0;
|
||
|
||
for (const m of materials) {
|
||
checked++;
|
||
let fileExists = false;
|
||
|
||
if (m.zipFilePath && (m.zipFilePath.startsWith('uploads/') || m.zipFilePath.startsWith('/app/uploads/'))) {
|
||
const fileName = path.basename(m.zipFilePath);
|
||
const filePath = path.join(uploadsPath, fileName);
|
||
try {
|
||
fs.accessSync(filePath, fs.constants.F_OK);
|
||
fileExists = true;
|
||
} catch {
|
||
fileExists = false;
|
||
}
|
||
} else if (m.zipFilePath && (m.zipFilePath.startsWith('http://') || m.zipFilePath.startsWith('https://'))) {
|
||
fileExists = true; // HTTP URL标记为存在
|
||
}
|
||
|
||
if (!fileExists) {
|
||
results.push({
|
||
id: m.id,
|
||
materialType: m.materialType || '',
|
||
productModel: m.productModel || '',
|
||
authorName: m.authorName || '',
|
||
categoryName: m.categoryName || '',
|
||
sceneName: m.sceneName || '',
|
||
platformName: m.platformName || '',
|
||
productCategoryName: m.productCategoryName || '',
|
||
renderer: m.renderer || '',
|
||
zipFilePath: m.zipFilePath || '',
|
||
zipFileName: m.zipFileName || '',
|
||
zipFileSize: m.zipFileSize ? m.zipFileSize.toString() : '',
|
||
createdAt: m.createdAt.toISOString(),
|
||
updatedAt: m.updatedAt.toISOString(),
|
||
});
|
||
}
|
||
|
||
if (checked % 100 === 0) {
|
||
console.error(\`已检查: \${checked}/\${materials.length}\`);
|
||
}
|
||
}
|
||
|
||
// 生成CSV
|
||
const headers = ['素材ID', '素材类型', '产品型号(SPU)', '作者', '使用店铺', '场景类别', '平台分类', '品类分类', '渲染器', '源文件路径', '源文件名', '源文件大小(字节)', '创建时间', '更新时间'];
|
||
const csvRows = [headers.join(',')];
|
||
|
||
for (const r of results) {
|
||
const escape = (s) => {
|
||
if (s === null || s === undefined) return '';
|
||
const str = String(s);
|
||
if (str.includes(',') || str.includes('\"') || str.includes('\n')) {
|
||
return '\"' + str.replace(/\"/g, '\"\"') + '\"';
|
||
}
|
||
return str;
|
||
};
|
||
|
||
csvRows.push([
|
||
r.id, r.materialType, r.productModel, r.authorName, r.categoryName,
|
||
r.sceneName, r.platformName, r.productCategoryName, r.renderer,
|
||
r.zipFilePath, r.zipFileName, r.zipFileSize,
|
||
r.createdAt, r.updatedAt
|
||
].map(escape).join(','));
|
||
}
|
||
|
||
console.log(csvRows.join('\n'));
|
||
await prisma.\$disconnect();
|
||
}
|
||
|
||
checkFiles().catch(console.error);
|
||
" > /tmp/missing-files.csv
|
||
|
||
EOF
|
||
|
||
# 下载CSV文件到本地
|
||
echo "📥 下载CSV报告..."
|
||
scp -i "${SSH_KEY_PATH}" -P ${SSH_PORT} ${SERVER_USER}@${SERVER_IP}:/tmp/missing-files.csv "${OUTPUT_FILE}"
|
||
|
||
echo ""
|
||
echo "✅ CSV报告已生成: ${OUTPUT_FILE}"
|
||
echo ""
|
||
|
||
# 显示统计信息
|
||
echo "📊 统计信息:"
|
||
ssh -i "${SSH_KEY_PATH}" -p ${SSH_PORT} -o StrictHostKeyChecking=no ${SERVER_USER}@${SERVER_IP} "wc -l /tmp/missing-files.csv"
|