using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using SPMS.Domain.Entities; using SPMS.Domain.Enums; using SPMS.Domain.Interfaces; using SPMS.Infrastructure.Persistence; namespace SPMS.Infrastructure.Workers; public class DailyStatWorker : BackgroundService { private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger _logger; private static readonly TimeZoneInfo KstZone = TimeZoneInfo.FindSystemTimeZoneById("Asia/Seoul"); public DailyStatWorker( IServiceScopeFactory scopeFactory, ILogger logger) { _scopeFactory = scopeFactory; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("DailyStatWorker 시작"); while (!stoppingToken.IsCancellationRequested) { var delay = CalculateDelayUntilNextRun(); _logger.LogInformation("DailyStatWorker 다음 실행까지 {Delay} 대기", delay); try { await Task.Delay(delay, stoppingToken); } catch (TaskCanceledException) { break; } try { await AggregateAsync(stoppingToken); } catch (Exception ex) { _logger.LogError(ex, "DailyStatWorker 집계 중 오류 발생"); } } } private TimeSpan CalculateDelayUntilNextRun() { var nowKst = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, KstZone); var nextRun = nowKst.Date.AddHours(0).AddMinutes(5); if (nowKst >= nextRun) nextRun = nextRun.AddDays(1); var delay = nextRun - nowKst; return delay; } private async Task AggregateAsync(CancellationToken stoppingToken) { using var scope = _scopeFactory.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); var dailyStatRepo = scope.ServiceProvider.GetRequiredService(); var unitOfWork = scope.ServiceProvider.GetRequiredService(); var nowKst = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, KstZone); var yesterday = DateOnly.FromDateTime(nowKst.AddDays(-1)); var startUtc = TimeZoneInfo.ConvertTimeToUtc(yesterday.ToDateTime(TimeOnly.MinValue), KstZone); var endUtc = startUtc.AddDays(1); _logger.LogInformation("DailyStatWorker 집계 시작: {Date}", yesterday); // 서비스별 발송 통계 집계 var sendStats = await context.Set() .Where(l => l.SentAt >= startUtc && l.SentAt < endUtc) .GroupBy(l => l.ServiceId) .Select(g => new { ServiceId = g.Key, SentCnt = g.Count(), SuccessCnt = g.Count(l => l.Status == PushResult.Success), FailCnt = g.Count(l => l.Status == PushResult.Failed) }) .ToListAsync(stoppingToken); // 서비스별 열람 통계 집계 var openStats = await context.Set() .Where(l => l.OpenedAt >= startUtc && l.OpenedAt < endUtc) .GroupBy(l => l.ServiceId) .Select(g => new { ServiceId = g.Key, OpenCnt = g.Count() }) .ToListAsync(stoppingToken); var openDict = openStats.ToDictionary(o => o.ServiceId, o => o.OpenCnt); // 모든 서비스 ID 수집 (발송 + 열람) var allServiceIds = sendStats.Select(s => s.ServiceId) .Union(openStats.Select(o => o.ServiceId)) .Distinct(); foreach (var serviceId in allServiceIds) { var send = sendStats.FirstOrDefault(s => s.ServiceId == serviceId); var openCnt = openDict.GetValueOrDefault(serviceId, 0); await dailyStatRepo.UpsertAsync( serviceId, yesterday, send?.SentCnt ?? 0, send?.SuccessCnt ?? 0, send?.FailCnt ?? 0, openCnt); } await unitOfWork.SaveChangesAsync(); // SystemLog에 집계 완료 기록 context.Set().Add(new SystemLog { Action = "DailyStatAggregation", Details = $"{yesterday:yyyy-MM-dd} 통계 집계 완료 (서비스 {allServiceIds.Count()}개)", CreatedAt = DateTime.UtcNow }); await context.SaveChangesAsync(stoppingToken); _logger.LogInformation("DailyStatWorker 집계 완료: {Date}, 서비스 {Count}개", yesterday, allServiceIds.Count()); } }