import cron from 'node-cron'; import githubService from './githubService.js'; import huggingfaceStorageService from './huggingfaceStorageService.js'; import fs from 'fs/promises'; import path from 'path'; /** * 自动备份服务 * 负责定时将修改的PPT数据同步到GitHub仓库 */ class AutoBackupService { constructor() { this.isInitialized = false; this.cronJob = null; this.backupInterval = '0 */8 * * *'; // 每8小时执行一次 this.lastBackupTime = null; this.backupStats = { totalBackups: 0, successfulBackups: 0, failedBackups: 0, lastBackupResult: null }; // 跟踪修改的PPT数据 this.modifiedPPTs = new Map(); // userId -> Set of pptIds this.lastModificationCheck = new Date(); console.log('🔄 AutoBackupService initialized'); } /** * 初始化自动备份服务 */ async initialize() { try { console.log('🚀 Initializing AutoBackupService...'); // 加载上次备份时间 await this.loadBackupState(); // 启动定时任务 this.startCronJob(); this.isInitialized = true; console.log('✅ AutoBackupService initialized successfully'); console.log(`⏰ Next backup scheduled: ${this.getNextBackupTime()}`); } catch (error) { console.error('❌ Failed to initialize AutoBackupService:', error); throw error; } } /** * 启动定时任务 */ startCronJob() { if (this.cronJob) { this.cronJob.stop(); } console.log(`⏰ Starting auto backup cron job: ${this.backupInterval}`); this.cronJob = cron.schedule(this.backupInterval, async () => { console.log('🔄 Auto backup triggered by cron job'); await this.performAutoBackup(); }, { scheduled: true, timezone: 'UTC' }); console.log('✅ Auto backup cron job started'); } /** * 停止定时任务 */ stopCronJob() { if (this.cronJob) { this.cronJob.stop(); this.cronJob = null; console.log('🛑 Auto backup cron job stopped'); } } /** * 记录PPT修改 */ recordPPTModification(userId, pptId) { if (!this.modifiedPPTs.has(userId)) { this.modifiedPPTs.set(userId, new Set()); } this.modifiedPPTs.get(userId).add(pptId); console.log(`📝 Recorded modification: User ${userId}, PPT ${pptId}`); } /** * 获取修改的PPT列表 */ getModifiedPPTs() { const result = {}; for (const [userId, pptIds] of this.modifiedPPTs.entries()) { result[userId] = Array.from(pptIds); } return result; } /** * 清除修改记录 */ clearModificationRecords() { this.modifiedPPTs.clear(); console.log('🧹 Cleared modification records'); } /** * 执行自动备份 */ async performAutoBackup() { const startTime = new Date(); console.log(`🔄 Starting auto backup at ${startTime.toISOString()}`); const backupResult = { timestamp: startTime, success: false, totalUsers: 0, totalPPTs: 0, successfulBackups: 0, failedBackups: 0, errors: [], duration: 0 }; try { this.backupStats.totalBackups++; // 获取修改的PPT列表 const modifiedPPTs = this.getModifiedPPTs(); const userIds = Object.keys(modifiedPPTs); backupResult.totalUsers = userIds.length; if (userIds.length === 0) { console.log('ℹ️ No modified PPTs found, skipping backup'); backupResult.success = true; return backupResult; } console.log(`📊 Found ${userIds.length} users with modified PPTs`); // 为每个用户备份修改的PPT for (const userId of userIds) { const pptIds = modifiedPPTs[userId]; backupResult.totalPPTs += pptIds.length; console.log(`👤 Backing up ${pptIds.length} PPTs for user ${userId}`); for (const pptId of pptIds) { try { // 获取PPT数据 const pptData = await githubService.getPPT(userId, pptId); if (!pptData) { console.warn(`⚠️ PPT ${pptId} not found for user ${userId}`); backupResult.failedBackups++; continue; } // 执行备份到GitHub await githubService.savePPT(userId, pptId, pptData); console.log(`✅ Successfully backed up PPT ${pptId} for user ${userId}`); backupResult.successfulBackups++; } catch (error) { console.error(`❌ Failed to backup PPT ${pptId} for user ${userId}:`, error); backupResult.failedBackups++; backupResult.errors.push({ userId, pptId, error: error.message }); } } } // 清除修改记录 this.clearModificationRecords(); backupResult.success = backupResult.failedBackups === 0; if (backupResult.success) { this.backupStats.successfulBackups++; console.log(`✅ Auto backup completed successfully: ${backupResult.successfulBackups} PPTs backed up`); } else { this.backupStats.failedBackups++; console.error(`❌ Auto backup completed with errors: ${backupResult.failedBackups} failures`); } } catch (error) { console.error('❌ Auto backup failed:', error); backupResult.success = false; backupResult.errors.push({ type: 'system', error: error.message }); this.backupStats.failedBackups++; } finally { const endTime = new Date(); backupResult.duration = endTime.getTime() - startTime.getTime(); this.lastBackupTime = startTime; this.backupStats.lastBackupResult = backupResult; // 保存备份状态 await this.saveBackupState(); console.log(`🏁 Auto backup finished in ${backupResult.duration}ms`); } return backupResult; } /** * 手动触发备份 */ async triggerManualBackup() { console.log('🔄 Manual backup triggered'); return await this.performAutoBackup(); } /** * 获取下次备份时间 */ getNextBackupTime() { if (!this.cronJob) { return 'Not scheduled'; } // 计算下次执行时间(简化版本) const now = new Date(); const nextHour = Math.ceil(now.getHours() / 8) * 8; const nextBackup = new Date(now); if (nextHour >= 24) { nextBackup.setDate(nextBackup.getDate() + 1); nextBackup.setHours(0, 0, 0, 0); } else { nextBackup.setHours(nextHour, 0, 0, 0); } return nextBackup.toISOString(); } /** * 获取备份统计信息 */ getBackupStats() { return { ...this.backupStats, lastBackupTime: this.lastBackupTime, nextBackupTime: this.getNextBackupTime(), modifiedPPTsCount: Array.from(this.modifiedPPTs.values()) .reduce((total, pptSet) => total + pptSet.size, 0), isRunning: !!this.cronJob, backupInterval: this.backupInterval }; } /** * 加载备份状态 */ async loadBackupState() { try { const stateFile = path.join(process.cwd(), 'data', 'backup-state.json'); try { const stateData = await fs.readFile(stateFile, 'utf8'); const state = JSON.parse(stateData); this.lastBackupTime = state.lastBackupTime ? new Date(state.lastBackupTime) : null; this.backupStats = { ...this.backupStats, ...state.backupStats }; console.log('📂 Loaded backup state from file'); } catch (error) { if (error.code !== 'ENOENT') { console.warn('⚠️ Failed to load backup state:', error.message); } } } catch (error) { console.warn('⚠️ Failed to load backup state:', error.message); } } /** * 保存备份状态 */ async saveBackupState() { try { const stateFile = path.join(process.cwd(), 'data', 'backup-state.json'); const stateDir = path.dirname(stateFile); // 确保目录存在 await fs.mkdir(stateDir, { recursive: true }); const state = { lastBackupTime: this.lastBackupTime, backupStats: this.backupStats, savedAt: new Date().toISOString() }; await fs.writeFile(stateFile, JSON.stringify(state, null, 2)); console.log('💾 Saved backup state to file'); } catch (error) { console.warn('⚠️ Failed to save backup state:', error.message); } } /** * 更新备份间隔 */ updateBackupInterval(cronExpression) { this.backupInterval = cronExpression; console.log(`⏰ Updated backup interval to: ${cronExpression}`); // 重启定时任务 this.startCronJob(); } /** * 清理服务 */ async cleanup() { console.log('🧹 Cleaning up AutoBackupService...'); this.stopCronJob(); await this.saveBackupState(); console.log('✅ AutoBackupService cleanup completed'); } } // 创建单例实例 const autoBackupService = new AutoBackupService(); export default autoBackupService;