const fs = require('fs'); const path = require('path'); const minioService = require('./services/minioService'); const { getDB, initDB } = require('./database'); /** * 文件迁移到 MinIO 的脚本 * 将本地 uploads 目录下的文件迁移到 MinIO 存储 */ class FilesMigration { constructor() { this.minioService = minioService; this.uploadsDir = path.join(__dirname, 'uploads'); this.migratedFiles = []; this.failedFiles = []; } /** * 递归获取目录下所有文件 * @param {string} dir - 目录路径 * @param {string} baseDir - 基础目录路径 * @returns {Array} 文件列表 */ getAllFiles(dir, baseDir = dir) { const files = []; const items = fs.readdirSync(dir); for (const item of items) { const fullPath = path.join(dir, item); const stat = fs.statSync(fullPath); if (stat.isDirectory()) { files.push(...this.getAllFiles(fullPath, baseDir)); } else { const relativePath = path.relative(baseDir, fullPath); files.push({ fullPath, relativePath: relativePath.replace(/\\/g, '/'), // 统一使用正斜杠 fileName: item, size: stat.size, mtime: stat.mtime }); } } return files; } /** * 生成新的 MinIO 路径(去掉 uploads 前缀) * @param {Object} fileInfo - 文件信息 * @returns {string} 去掉 uploads 前缀的文件路径 */ generateMinioPath(fileInfo) { // 去掉 uploads/ 前缀,因为数据库中存储的是 /uploads/xxx,而 MinIO 中应该是 xxx let minioPath = fileInfo.relativePath; if (minioPath.startsWith('uploads/')) { minioPath = minioPath.substring('uploads/'.length); } return minioPath; } /** * 上传单个文件到 MinIO * @param {Object} fileInfo - 文件信息 * @returns {Promise} 上传结果 */ async uploadFileToMinio(fileInfo) { try { const minioPath = this.generateMinioPath(fileInfo); // 使用固定的存储桶名称 const bucketName = 'jurongquan'; console.log(`正在上传: ${fileInfo.relativePath} -> ${minioPath}`); // 读取文件内容 const fileBuffer = fs.readFileSync(fileInfo.fullPath); // 上传到 MinIO const uploadResult = await this.minioService.uploadFileForMigration( bucketName, minioPath, fileBuffer, this.getContentType(fileInfo.fileName) ); const result = { url: uploadResult.data.url, path: uploadResult.data.path }; return { success: true, originalPath: fileInfo.relativePath, minioPath, minioUrl: result.url, fileInfo }; } catch (error) { console.error(`上传失败 ${fileInfo.relativePath}:`, error.message); return { success: false, originalPath: fileInfo.relativePath, error: error.message, fileInfo }; } } /** * 根据文件扩展名获取 Content-Type * @param {string} fileName - 文件名 * @returns {string} Content-Type */ getContentType(fileName) { const ext = path.extname(fileName).toLowerCase(); const contentTypes = { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.gif': 'image/gif', '.webp': 'image/webp', '.pdf': 'application/pdf', '.doc': 'application/msword', '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', '.txt': 'text/plain' }; return contentTypes[ext] || 'application/octet-stream'; } /** * 更新数据库中的文件路径引用 * @param {Array} migratedFiles - 已迁移的文件列表 */ async updateDatabaseReferences(migratedFiles) { const db = getDB(); let updatedCount = 0; console.log('\n开始更新数据库中的文件路径引用...'); for (const file of migratedFiles) { console.log(file,'file'); try { // 原路径:/uploads/avatars/xxx.jpg const oldPath = `/${file.originalPath}`; // 新路径:/avatars/xxx.jpg (去掉 uploads 前缀) const newPath = `/${file.minioPath}`; // 更新用户头像 const [avatarResult] = await db.execute( 'UPDATE users SET avatar = ? WHERE avatar = ?', [newPath, oldPath] ); if (avatarResult.affectedRows > 0) { console.log(`更新用户头像: ${oldPath} -> ${newPath}`); updatedCount += avatarResult.affectedRows; } // 更新用户微信收款码 const [wechatQrResult] = await db.execute( 'UPDATE users SET wechat_qr = ? WHERE wechat_qr = ?', [newPath, oldPath] ); if (wechatQrResult.affectedRows > 0) { console.log(`更新用户微信收款码: ${oldPath} -> ${newPath}`); updatedCount += wechatQrResult.affectedRows; } // 更新用户支付宝收款码 const [alipayQrResult] = await db.execute( 'UPDATE users SET alipay_qr = ? WHERE alipay_qr = ?', [newPath, oldPath] ); if (alipayQrResult.affectedRows > 0) { console.log(`更新用户支付宝收款码: ${oldPath} -> ${newPath}`); updatedCount += alipayQrResult.affectedRows; } // 更新用户云闪付收款码 const [unionpayQrResult] = await db.execute( 'UPDATE users SET unionpay_qr = ? WHERE unionpay_qr = ?', [newPath, oldPath] ); if (unionpayQrResult.affectedRows > 0) { console.log(`更新用户云闪付收款码: ${oldPath} -> ${newPath}`); updatedCount += unionpayQrResult.affectedRows; } // 更新用户营业执照 const [businessLicenseResult] = await db.execute( 'UPDATE users SET business_license = ? WHERE business_license = ?', [newPath, oldPath] ); if (businessLicenseResult.affectedRows > 0) { console.log(`更新用户营业执照: ${oldPath} -> ${newPath}`); updatedCount += businessLicenseResult.affectedRows; } // 更新用户身份证正面 const [idCardFrontResult] = await db.execute( 'UPDATE users SET id_card_front = ? WHERE id_card_front = ?', [newPath, oldPath] ); if (idCardFrontResult.affectedRows > 0) { console.log(`更新用户身份证正面: ${oldPath} -> ${newPath}`); updatedCount += idCardFrontResult.affectedRows; } // 更新用户身份证反面 const [idCardBackResult] = await db.execute( 'UPDATE users SET id_card_back = ? WHERE id_card_back = ?', [newPath, oldPath] ); if (idCardBackResult.affectedRows > 0) { console.log(`更新用户身份证反面: ${oldPath} -> ${newPath}`); updatedCount += idCardBackResult.affectedRows; } // 更新产品图片 const [productResult] = await db.execute( 'UPDATE products SET image_url = ? WHERE image_url = ?', [newPath, oldPath] ); if (productResult.affectedRows > 0) { console.log(`更新产品图片: ${oldPath} -> ${newPath}`); updatedCount += productResult.affectedRows; } // 更新产品店铺头像 const [shopAvatarResult] = await db.execute( 'UPDATE products SET shop_avatar = ? WHERE shop_avatar = ?', [newPath, oldPath] ); if (shopAvatarResult.affectedRows > 0) { console.log(`更新产品店铺头像: ${oldPath} -> ${newPath}`); updatedCount += shopAvatarResult.affectedRows; } // 更新产品图片(JSON 字段中的图片) const [products] = await db.execute( 'SELECT id, images FROM products WHERE images LIKE ?', [`%${oldPath}%`] ); for (const product of products) { if (product.images) { const updatedImages = product.images.replace(new RegExp(oldPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newPath); await db.execute( 'UPDATE products SET images = ? WHERE id = ?', [updatedImages, product.id] ); console.log(`更新产品图片集合: 产品ID ${product.id}`); updatedCount++; } } // 更新转账凭证 const [transferResult] = await db.execute( 'UPDATE transfers SET voucher_url = ? WHERE voucher_url = ?', [newPath, oldPath] ); if (transferResult.affectedRows > 0) { console.log(`更新转账凭证: ${oldPath} -> ${newPath}`); updatedCount += transferResult.affectedRows; } // 更新区域代理收款码 const [agentQrResult] = await db.execute( 'UPDATE regional_agents SET qr_code_url = ? WHERE qr_code_url = ?', [newPath, oldPath] ); if (agentQrResult.affectedRows > 0) { console.log(`更新区域代理收款码: ${oldPath} -> ${newPath}`); updatedCount += agentQrResult.affectedRows; } // 更新规格图片 const [specResult] = await db.execute( 'UPDATE spec_values SET image_url = ? WHERE image_url = ?', [newPath, oldPath] ); if (specResult.affectedRows > 0) { console.log(`更新规格图片: ${oldPath} -> ${newPath}`); updatedCount += specResult.affectedRows; } } catch (error) { console.error(`更新数据库引用失败 ${file.originalPath}:`, error.message); } } console.log(`\n数据库更新完成,共更新 ${updatedCount} 条记录`); } /** * 执行迁移 */ async migrate() { try { console.log('开始文件迁移到 MinIO...'); console.log(`源目录: ${this.uploadsDir}`); // 初始化数据库连接 console.log('初始化数据库连接...'); await initDB(); console.log('数据库连接初始化完成'); // 检查源目录是否存在 if (!fs.existsSync(this.uploadsDir)) { console.log('uploads 目录不存在,无需迁移'); return; } // 获取所有文件 const allFiles = this.getAllFiles(this.uploadsDir); console.log(`找到 ${allFiles.length} 个文件需要迁移`); if (allFiles.length === 0) { console.log('没有文件需要迁移'); return; } // 逐个上传文件 for (let i = 0; i < allFiles.length; i++) { const file = allFiles[i]; console.log(`\n进度: ${i + 1}/${allFiles.length}`); const result = await this.uploadFileToMinio(file); if (result.success) { this.migratedFiles.push(result); } else { this.failedFiles.push(result); } // 添加小延迟避免过快请求 await new Promise(resolve => setTimeout(resolve, 100)); } // 输出迁移结果 console.log('\n=== 迁移结果 ==='); console.log(`成功迁移: ${this.migratedFiles.length} 个文件`); console.log(`迁移失败: ${this.failedFiles.length} 个文件`); if (this.failedFiles.length > 0) { console.log('\n失败的文件:'); this.failedFiles.forEach(file => { console.log(`- ${file.originalPath}: ${file.error}`); }); } // 更新数据库引用,将 /uploads/xxx 路径更新为 /xxx if (this.migratedFiles.length > 0) { await this.updateDatabaseReferences(this.migratedFiles); } // 生成迁移报告 await this.generateMigrationReport(); console.log('\n迁移完成!'); console.log('\n注意事项:'); console.log('1. 请验证文件访问是否正常'); console.log('2. 确认数据库中的文件路径已正确更新'); console.log('3. 测试完成后可以删除本地 uploads 目录'); console.log('4. 查看 migration-report.json 了解详细迁移信息'); } catch (error) { console.error('迁移过程中发生错误:', error); } } /** * 生成迁移报告 */ async generateMigrationReport() { const report = { migrationDate: new Date().toISOString(), totalFiles: this.migratedFiles.length + this.failedFiles.length, successCount: this.migratedFiles.length, failedCount: this.failedFiles.length, migratedFiles: this.migratedFiles, failedFiles: this.failedFiles }; const reportPath = path.join(__dirname, 'migration-report.json'); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2)); console.log(`\n迁移报告已生成: ${reportPath}`); } } // 如果直接运行此脚本 if (require.main === module) { const migration = new FilesMigration(); migration.migrate().catch(console.error); } module.exports = FilesMigration;