SPMS_API/SPMS.Infrastructure/Workers/DailyStatWorker.cs
2026-02-11 10:17:06 +09:00

140 lines
4.8 KiB
C#

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<DailyStatWorker> _logger;
private static readonly TimeZoneInfo KstZone = TimeZoneInfo.FindSystemTimeZoneById("Asia/Seoul");
public DailyStatWorker(
IServiceScopeFactory scopeFactory,
ILogger<DailyStatWorker> 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<AppDbContext>();
var dailyStatRepo = scope.ServiceProvider.GetRequiredService<IDailyStatRepository>();
var unitOfWork = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();
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<PushSendLog>()
.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<PushOpenLog>()
.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<SystemLog>().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());
}
}