import { Injectable, Logger, Inject } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; import { CommandBus } from '@nestjs/cqrs'; import { subDays } from 'date-fns'; import { CalculateDauCommand } from '../commands/calculate-dau/calculate-dau.command'; import { PresenceRedisRepository } from '../../infrastructure/redis/presence-redis.repository'; import { OnlineDetectionService } from '../../domain/services/online-detection.service'; import { OnlineSnapshot } from '../../domain/entities/online-snapshot.entity'; import { IOnlineSnapshotRepository, ONLINE_SNAPSHOT_REPOSITORY } from '../../domain/repositories/online-snapshot.repository.interface'; @Injectable() export class AnalyticsScheduler { private readonly logger = new Logger(AnalyticsScheduler.name); constructor( private readonly commandBus: CommandBus, private readonly presenceRedisRepository: PresenceRedisRepository, private readonly onlineDetectionService: OnlineDetectionService, @Inject(ONLINE_SNAPSHOT_REPOSITORY) private readonly snapshotRepository: IOnlineSnapshotRepository, ) {} @Cron(CronExpression.EVERY_MINUTE) async recordOnlineSnapshot(): Promise { try { const now = new Date(); const threshold = this.onlineDetectionService.getOnlineThreshold(now); const count = await this.presenceRedisRepository.countOnlineUsers(threshold); const snapshot = OnlineSnapshot.create({ ts: now, onlineCount: count, windowSeconds: this.onlineDetectionService.getWindowSeconds(), }); await this.snapshotRepository.insert(snapshot); this.logger.debug(`Online snapshot: ${count} users`); } catch (error) { this.logger.error('Failed to record online snapshot', error); } } @Cron(CronExpression.EVERY_HOUR) async cleanupExpiredPresence(): Promise { try { const threshold = Math.floor(Date.now() / 1000) - 86400; await this.presenceRedisRepository.removeExpiredUsers(threshold); } catch (error) { this.logger.error('Failed to cleanup expired presence', error); } } @Cron('0 0 1 * * *', { timeZone: 'Asia/Shanghai' }) async calculateYesterdayDau(): Promise { try { const yesterday = subDays(new Date(), 1); await this.commandBus.execute(new CalculateDauCommand(yesterday)); this.logger.log('Yesterday DAU calculated'); } catch (error) { this.logger.error('Failed to calculate yesterday DAU', error); } } @Cron(CronExpression.EVERY_HOUR) async calculateTodayDauRolling(): Promise { try { await this.commandBus.execute(new CalculateDauCommand(new Date())); } catch (error) { this.logger.error('Failed to calculate today DAU', error); } } }