398 lines
13 KiB
JavaScript
398 lines
13 KiB
JavaScript
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<Object>} 上传结果
|
||
*/
|
||
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; |