定时任务性能优化

This commit is contained in:
mtvpls
2026-02-22 23:55:34 +08:00
parent 9a33b90078
commit 4dfea503d5
3 changed files with 79 additions and 38 deletions
+1
View File
@@ -339,6 +339,7 @@ dockge/komodo 等 docker compose UI 也有自动更新功能
| USERNAME | 站长账号 | 任意字符串 | 无默认,必填字段 | | USERNAME | 站长账号 | 任意字符串 | 无默认,必填字段 |
| PASSWORD | 站长密码 | 任意字符串 | 无默认,必填字段 | | PASSWORD | 站长密码 | 任意字符串 | 无默认,必填字段 |
| CRON_PASSWORD | 定时任务 API 访问密码(用于保护 /api/cron 端点) | 任意字符串 | mtvpls | | CRON_PASSWORD | 定时任务 API 访问密码(用于保护 /api/cron 端点) | 任意字符串 | mtvpls |
| CRON_USER_BATCH_SIZE | 定时任务用户批处理大小(控制并发处理的用户数量,影响播放记录和收藏更新任务的并发性能) | 正整数 | 3 |
| SITE_BASE | 站点 url | 形如 https://example.com | 空 | | SITE_BASE | 站点 url | 形如 https://example.com | 空 |
| NEXT_PUBLIC_SITE_NAME | 站点名称 | 任意字符串 | MoonTV | | NEXT_PUBLIC_SITE_NAME | 站点名称 | 任意字符串 | MoonTV |
| ANNOUNCEMENT | 站点公告 | 任意字符串 | 本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。 | | ANNOUNCEMENT | 站点公告 | 任意字符串 | 本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。 |
+52 -35
View File
@@ -54,11 +54,16 @@ export async function GET(
} }
async function cronJob() { async function cronJob() {
// 先刷新配置,确保其他任务使用最新配置
await refreshConfig(); await refreshConfig();
await refreshAllLiveChannels();
await refreshOpenList(); // 其余任务并行执行
await refreshRecordAndFavorites(); await Promise.all([
await checkAnimeSubscriptions(); refreshAllLiveChannels(),
refreshOpenList(),
refreshRecordAndFavorites(),
checkAnimeSubscriptions(),
]);
} }
async function refreshAllLiveChannels() { async function refreshAllLiveChannels() {
@@ -164,26 +169,28 @@ async function refreshRecordAndFavorites() {
const key = `${source}+${id}`; const key = `${source}+${id}`;
let promise = detailCache.get(key); let promise = detailCache.get(key);
if (!promise) { if (!promise) {
// 立即缓存Promise,避免并发时的竞态条件
promise = fetchVideoDetail({ promise = fetchVideoDetail({
source, source,
id, id,
fallbackTitle: fallbackTitle.trim(), fallbackTitle: fallbackTitle.trim(),
}) })
.then((detail) => { .then((detail) => {
// 成功时才缓存结果
const successPromise = Promise.resolve(detail);
detailCache.set(key, successPromise);
return detail; return detail;
}) })
.catch((err) => { .catch((err) => {
console.error(`获取视频详情失败 (${source}+${id}):`, err); console.error(`获取视频详情失败 (${source}+${id}):`, err);
// 失败时从缓存中移除,下次可以重试
detailCache.delete(key);
return null; return null;
}); });
detailCache.set(key, promise);
} }
return promise; return promise;
}; };
for (const user of users) { // 处理单个用户的函数
const processUser = async (user: string) => {
console.log(`开始处理用户: ${user}`); console.log(`开始处理用户: ${user}`);
const storage = getStorage(); const storage = getStorage();
@@ -342,44 +349,54 @@ async function refreshRecordAndFavorites() {
console.log(`收藏处理完成: ${processedFavorites}/${totalFavorites}`); console.log(`收藏处理完成: ${processedFavorites}/${totalFavorites}`);
// 如果有更新,发送汇总邮件 // 如果有更新,异步发送汇总邮件(不阻塞主流程)
if (userUpdates.length > 0) { if (userUpdates.length > 0) {
try { (async () => {
const userEmail = storage.getUserEmail ? await storage.getUserEmail(user) : null; try {
const emailNotifications = storage.getEmailNotificationPreference const userEmail = storage.getUserEmail ? await storage.getUserEmail(user) : null;
? await storage.getEmailNotificationPreference(user) const emailNotifications = storage.getEmailNotificationPreference
: false; ? await storage.getEmailNotificationPreference(user)
: false;
if (userEmail && emailNotifications) { if (userEmail && emailNotifications) {
const config = await getConfig(); const config = await getConfig();
const emailConfig = config?.EmailConfig; const emailConfig = config?.EmailConfig;
if (emailConfig?.enabled) { if (emailConfig?.enabled) {
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'; const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
const siteName = config?.SiteConfig?.SiteName || 'MoonTVPlus'; const siteName = config?.SiteConfig?.SiteName || 'MoonTVPlus';
await EmailService.send(emailConfig, { await EmailService.send(emailConfig, {
to: userEmail, to: userEmail,
subject: `📺 收藏更新汇总 - ${userUpdates.length} 部影片有更新`, subject: `📺 收藏更新汇总 - ${userUpdates.length} 部影片有更新`,
html: getBatchFavoriteUpdateEmailTemplate( html: getBatchFavoriteUpdateEmailTemplate(
user, user,
userUpdates, userUpdates,
siteUrl, siteUrl,
siteName siteName
), ),
}); });
console.log(`邮件汇总已发送至: ${userEmail} (${userUpdates.length} 个更新)`); console.log(`邮件汇总已发送至: ${userEmail} (${userUpdates.length} 个更新)`);
}
} }
} catch (emailError) {
console.error(`发送邮件汇总失败 (${user}):`, emailError);
} }
} catch (emailError) { })().catch(err => console.error(`邮件发送异步任务失败 (${user}):`, err));
console.error(`发送邮件汇总失败 (${user}):`, emailError);
// 邮件发送失败不影响主流程
}
} }
} catch (err) { } catch (err) {
console.error(`获取用户收藏失败 (${user}):`, err); console.error(`获取用户收藏失败 (${user}):`, err);
} }
};
// 分批并行处理用户,避免并发过高
// 可通过环境变量 CRON_USER_BATCH_SIZE 配置批处理大小,默认为 3
const BATCH_SIZE = parseInt(process.env.CRON_USER_BATCH_SIZE || '3', 10);
for (let i = 0; i < users.length; i += BATCH_SIZE) {
const batch = users.slice(i, i + BATCH_SIZE);
console.log(`处理用户批次 ${Math.floor(i / BATCH_SIZE) + 1}/${Math.ceil(users.length / BATCH_SIZE)}: ${batch.join(', ')}`);
await Promise.all(batch.map(user => processUser(user)));
} }
console.log('刷新播放记录/收藏任务完成'); console.log('刷新播放记录/收藏任务完成');
+26 -3
View File
@@ -361,31 +361,51 @@ export async function checkSubscription(subscription: AnimeSubscription) {
* 检查所有订阅(定时任务调用) * 检查所有订阅(定时任务调用)
*/ */
export async function checkAnimeSubscriptions() { export async function checkAnimeSubscriptions() {
console.log('[AnimeSubscription] 开始检查动漫订阅');
const config = await getConfig(); const config = await getConfig();
const animeConfig = config.AnimeSubscriptionConfig; const animeConfig = config.AnimeSubscriptionConfig;
if (!animeConfig?.Enabled) { if (!animeConfig?.Enabled) {
console.log('[AnimeSubscription] 动漫订阅功能未启用,跳过检查');
return; return;
} }
const subscriptions = animeConfig.Subscriptions || []; const subscriptions = animeConfig.Subscriptions || [];
console.log(`[AnimeSubscription] 共有 ${subscriptions.length} 个订阅`);
const now = Date.now(); const now = Date.now();
const MIN_CHECK_INTERVAL = 30 * 60 * 1000; // 30分钟 const MIN_CHECK_INTERVAL = 30 * 60 * 1000; // 30分钟
let configChanged = false; let configChanged = false;
let checkedCount = 0;
let skippedCount = 0;
let errorCount = 0;
for (const sub of subscriptions) { for (const sub of subscriptions) {
if (!sub.enabled) continue; if (!sub.enabled) {
console.log(`[AnimeSubscription] 跳过已禁用的订阅: ${sub.title}`);
skippedCount++;
continue;
}
// 检查是否距离上次检查超过30分钟 // 检查是否距离上次检查超过30分钟
if (now - sub.lastCheckTime < MIN_CHECK_INTERVAL) { const timeSinceLastCheck = now - sub.lastCheckTime;
if (timeSinceLastCheck < MIN_CHECK_INTERVAL) {
const remainingMinutes = Math.ceil((MIN_CHECK_INTERVAL - timeSinceLastCheck) / 60000);
console.log(`[AnimeSubscription] 跳过 ${sub.title}: 距离上次检查仅 ${Math.floor(timeSinceLastCheck / 60000)} 分钟,还需等待 ${remainingMinutes} 分钟`);
skippedCount++;
continue; continue;
} }
try { try {
await checkSubscription(sub); console.log(`[AnimeSubscription] 检查订阅: ${sub.title} (源: ${sub.source}, 上次集数: ${sub.lastEpisode})`);
const result = await checkSubscription(sub);
console.log(`[AnimeSubscription] ${sub.title}: 找到 ${result.found} 个新集数,成功下载 ${result.downloaded}`);
configChanged = true; configChanged = true;
checkedCount++;
} catch (error) { } catch (error) {
console.error(`[AnimeSubscription] ${sub.title}: 检查失败`, error); console.error(`[AnimeSubscription] ${sub.title}: 检查失败`, error);
errorCount++;
} }
} }
@@ -393,5 +413,8 @@ export async function checkAnimeSubscriptions() {
if (configChanged) { if (configChanged) {
await db.saveAdminConfig(config); await db.saveAdminConfig(config);
await setCachedConfig(config); await setCachedConfig(config);
console.log('[AnimeSubscription] 配置已更新并保存');
} }
console.log(`[AnimeSubscription] 检查完成 - 总计: ${subscriptions.length}, 已检查: ${checkedCount}, 跳过: ${skippedCount}, 失败: ${errorCount}`);
} }