152 lines
4.0 KiB
TypeScript
152 lines
4.0 KiB
TypeScript
/**
|
||
* 视频归档定时任务
|
||
* 功能:定期查询需要归档的视频并执行归档操作
|
||
* 触发条件:创建时间超过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,
|
||
};
|
||
}
|
||
}
|
||
|