/** * Next.js Instrumentation * * 这个文件在应用启动时执行,用于: * 1. 启动临时文件定时清理任务 * 2. 初始化其他需要在启动时运行的服务 */ export async function register() { // 只在 Node.js 服务端运行(不在 Edge 运行时) if (process.env.NEXT_RUNTIME === 'nodejs') { console.log('[Instrumentation] Next.js 服务端启动...') const { ensureDetailGeneratorWorkerStarted } = await import('./lib/detail-generator/runtime') ensureDetailGeneratorWorkerStarted() const { ensureMaterialPurgeWorkerStarted } = await import('./lib/material-purge') ensureMaterialPurgeWorkerStarted() // 启动临时文件清理任务 try { const { startCleanupScheduler } = await import('./lib/temp-cleanup') startCleanupScheduler() console.log('[Instrumentation] 临时文件清理任务已启动') } catch (error) { console.error('[Instrumentation] 启动清理任务失败:', error) } const recoveryGlobal = globalThis as typeof globalThis & { __taskDetailUploadRecoveryStarted?: boolean } if (!recoveryGlobal.__taskDetailUploadRecoveryStarted) { recoveryGlobal.__taskDetailUploadRecoveryStarted = true // 先注册周期任务,再做首次恢复。数据库启动瞬时不可用时,后续周期仍会重试, // 不会因为一次失败让 processing/queued 任务永久无人处理。 setInterval(() => { void import('./lib/task-detail-upload-jobs') .then(({ recoverTaskDetailUploadJobs }) => recoverTaskDetailUploadJobs()) .catch((error) => { console.error('[Instrumentation] 任务-详情后台队列周期恢复失败:', error) }) }, 60_000) } try { const { recoverTaskDetailUploadJobs } = await import('./lib/task-detail-upload-jobs') await recoverTaskDetailUploadJobs() console.log('[Instrumentation] 任务-详情后台队列恢复检查完成') } catch (error) { // 迁移未执行或数据库暂不可用时只记录;周期恢复已提前注册,会继续重试。 console.error('[Instrumentation] 任务-详情后台队列恢复失败:', error) } const videoRecoveryGlobal = globalThis as typeof globalThis & { __videoTranscodeRecoveryScheduler?: { initial: Promise } } if (!videoRecoveryGlobal.__videoTranscodeRecoveryScheduler) { const { createVideoTranscodeRecoveryScheduler } = await import('./lib/video-transcode-recovery') const scheduler = createVideoTranscodeRecoveryScheduler() videoRecoveryGlobal.__videoTranscodeRecoveryScheduler = scheduler try { const recovered = await scheduler.initial console.log(`[Instrumentation] 视频转码启动恢复完成,重新入队 ${recovered} 个任务`) } catch (error) { // 周期调度器已先注册;数据库瞬时不可用时,60 秒后会继续重试。 console.error('[Instrumentation] 视频转码启动恢复失败,已保留周期重试:', error) } } try { const { ensureWorkReviewNotificationWorkerStarted } = await import('./lib/work-review-notifications') ensureWorkReviewNotificationWorkerStarted() console.log('[Instrumentation] 作品过稿通知与每小时提醒任务已启动') } catch (error) { console.error('[Instrumentation] 启动作作品过稿通知任务失败:', error) } try { const { ensureTaskDetailChangeNotificationWorkerStarted } = await import('./lib/task-detail-change-notifications') const { expireDueTaskDetailChanges, recoverMissingTaskDetailChangeRequestNotifications, recoverStaleApplyingTaskDetailChanges, } = await import('./lib/task-detail-change-approval') ensureTaskDetailChangeNotificationWorkerStarted() const approvalGlobal = globalThis as typeof globalThis & { __taskDetailChangeExpiryStarted?: boolean } if (!approvalGlobal.__taskDetailChangeExpiryStarted) { approvalGlobal.__taskDetailChangeExpiryStarted = true setInterval(() => { void expireDueTaskDetailChanges().catch((error) => { console.error('[Instrumentation] 图片变更审批超时扫描失败:', error) }) void recoverMissingTaskDetailChangeRequestNotifications().catch((error) => { console.error('[Instrumentation] 图片变更审批缺失通知补偿失败:', error) }) void recoverStaleApplyingTaskDetailChanges().catch((error) => { console.error('[Instrumentation] 图片变更审批应用中断恢复失败:', error) }) }, 60_000).unref?.() } void expireDueTaskDetailChanges().catch((error) => { console.error('[Instrumentation] 图片变更审批首次超时扫描失败:', error) }) void recoverMissingTaskDetailChangeRequestNotifications().catch((error) => { console.error('[Instrumentation] 图片变更审批首次缺失通知补偿失败:', error) }) void recoverStaleApplyingTaskDetailChanges().catch((error) => { console.error('[Instrumentation] 图片变更审批首次应用中断恢复失败:', error) }) console.log('[Instrumentation] 图片变更审批通知与超时否决任务已启动') } catch (error) { console.error('[Instrumentation] 启动图片变更审批任务失败:', error) } // 组织架构/在职状态/离职联动每日同步:应用启动即调度(每日 02:00 Asia/Shanghai), // 不再依赖用户访问考核调研懒启动,容器重启后自动恢复。 try { const { ensureDailyOrganizationSyncStarted } = await import('./lib/organization-sync') ensureDailyOrganizationSyncStarted() console.log('[Instrumentation] 组织架构每日同步任务已调度(每日 02:00 Asia/Shanghai)') } catch (error) { console.error('[Instrumentation] 调度组织架构每日同步任务失败:', error) } // 钉钉审批流程快照:每小时增量同步,并每 24 小时全量校准流程与商品 ID。 // 看板只读本地 PostgreSQL 快照,避免用户请求直接扫描上游无索引大表。 try { const { ensureHourlyDingWorkflowSyncStarted } = await import('./lib/ding-workflow-sync') ensureHourlyDingWorkflowSyncStarted() console.log('[Instrumentation] 钉钉流程增量同步与每日全量校准任务已调度') } catch (error) { console.error('[Instrumentation] 调度钉钉流程每小时同步任务失败:', error) } // 业绩转化经营数据:首次按已完结美术流程商品建立快照,之后每日只重刷 // 最近两天,覆盖报表库对前一天数据的迟到写入或更正。 try { const { ensureDailyPerformanceConversionSyncStarted } = await import('./lib/performance-conversion') ensureDailyPerformanceConversionSyncStarted() console.log('[Instrumentation] 业绩转化经营数据每日增量同步任务已调度') } catch (error) { console.error('[Instrumentation] 调度业绩转化经营数据每日同步任务失败:', error) } // 作品过稿后台是首轮通过指标的唯一权威源;应用启动后增量检查项目、 // V1 版本和评审记录,避免看板退化为只在有人打开时才知道数据是否更新。 try { const { ensureHourlyWorkReviewDashboardSyncStarted } = await import('./lib/work-review-dashboard-sync') ensureHourlyWorkReviewDashboardSyncStarted() console.log('[Instrumentation] 作品过稿后台每小时增量检查任务已调度') } catch (error) { console.error('[Instrumentation] 调度作品过稿每小时增量检查任务失败:', error) } } }