/** * 视频归档定时任务 * 功能:定期查询需要归档的视频并执行归档操作 * 触发条件:创建时间超过20分钟且状态为 temporary */ import { db } from "@/lib/db"; import { message } from "@/lib/db/schema"; import { and, lte, isNull, eq } from "drizzle-orm"; import { videoArchiver } from "@/lib/storage/video-archiver"; import type { VideoMetadata } from "@/types/ai"; /** * 查询需要归档的视频消息 * 条件: * 1. 视频状态为 'temporary'(临时) * 2. 创建时间超过20分钟 * 3. 还没有永久URL */ export async function getVideosToArchive() { const twentyMinutesAgo = new Date(Date.now() - 20 * 60 * 1000); return await db.query.message.findMany({ where: and( eq(message.videoStorageStatus, "temporary"), lte(message.createdAt, twentyMinutesAgo), isNull(message.permanentVideoUrl) ), limit: 20, // 每次处理20个,20分钟周期可以处理更多 }); } /** * 从消息中提取视频数据 */ function extractVideoDataFromMessage(msg: any) { try { if (msg.temporaryVideoUrl) { const metadata = msg.videoMetadata as VideoMetadata; return { temporaryUrl: msg.temporaryVideoUrl, taskId: metadata?.taskId || msg.id, metadata: { duration: metadata?.duration, resolution: metadata?.resolution, ratio: metadata?.ratio, }, }; } return null; } catch (error) { console.error("[VideoArchiveJob] 提取视频数据失败:", error); return null; } } /** * 执行视频归档任务 */ export async function runVideoArchiveJob() { console.log("[VideoArchiveJob] ⏰ 开始执行视频归档任务..."); try { // 1. 查询待归档的视频 const videos = await getVideosToArchive(); if (videos.length === 0) { console.log("[VideoArchiveJob] 📭 没有需要归档的视频"); return { success: true, message: "No videos to archive", total: 0, succeeded: 0, failed: 0, }; } console.log(`[VideoArchiveJob] 📹 发现 ${videos.length} 个待归档视频`); // 2. 并发归档(控制并发数为3) const concurrency = 3; const results = []; for (let i = 0; i < videos.length; i += concurrency) { const batch = videos.slice(i, i + concurrency); const batchPromises = batch.map(async (video) => { try { // 从消息内容中提取视频URL和元数据 const videoData = extractVideoDataFromMessage(video); if (!videoData) { console.warn( `[VideoArchiveJob] ⚠️ 消息 ${video.id} 没有视频数据` ); return { success: false, messageId: video.id }; } // 执行归档 const permanentUrl = await videoArchiver.archiveVideo({ messageId: video.id, temporaryUrl: videoData.temporaryUrl, taskId: videoData.taskId, metadata: videoData.metadata, }); return { success: true, messageId: video.id, permanentUrl }; } catch (error) { console.error( `[VideoArchiveJob] ❌ 归档失败 (${video.id}):`, error ); return { success: false, messageId: video.id, error }; } }); const batchResults = await Promise.allSettled(batchPromises); results.push(...batchResults); } // 3. 统计结果 const succeeded = results.filter( (r) => r.status === "fulfilled" && r.value.success ).length; const failed = results.length - succeeded; console.log( `[VideoArchiveJob] ✅ 完成:成功 ${succeeded},失败 ${failed}` ); return { success: true, message: "Video archive job completed", total: results.length, succeeded, failed, }; } catch (error) { console.error("[VideoArchiveJob] ❌ 任务执行出错:", error); return { success: false, message: `Job failed: ${error}`, total: 0, succeeded: 0, failed: 0, }; } }