parent
9ab7d4786d
commit
87b48441cb
|
|
@ -6,4 +6,5 @@ public interface IDailyStatRepository : IRepository<DailyStat>
|
||||||
{
|
{
|
||||||
Task<IReadOnlyList<DailyStat>> GetByDateRangeAsync(long serviceId, DateOnly startDate, DateOnly endDate);
|
Task<IReadOnlyList<DailyStat>> GetByDateRangeAsync(long serviceId, DateOnly startDate, DateOnly endDate);
|
||||||
Task<DailyStat?> GetByDateAsync(long serviceId, DateOnly date);
|
Task<DailyStat?> GetByDateAsync(long serviceId, DateOnly date);
|
||||||
|
Task UpsertAsync(long serviceId, DateOnly statDate, int sentCnt, int successCnt, int failCnt, int openCnt);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ public static class DependencyInjection
|
||||||
// Workers
|
// Workers
|
||||||
services.AddHostedService<PushWorker>();
|
services.AddHostedService<PushWorker>();
|
||||||
services.AddHostedService<ScheduleWorker>();
|
services.AddHostedService<ScheduleWorker>();
|
||||||
|
services.AddHostedService<DailyStatWorker>();
|
||||||
|
|
||||||
// Token Store & Email Service
|
// Token Store & Email Service
|
||||||
services.AddMemoryCache();
|
services.AddMemoryCache();
|
||||||
|
|
|
||||||
|
|
@ -21,4 +21,32 @@ public class DailyStatRepository : Repository<DailyStat>, IDailyStatRepository
|
||||||
return await _dbSet
|
return await _dbSet
|
||||||
.FirstOrDefaultAsync(s => s.ServiceId == serviceId && s.StatDate == date);
|
.FirstOrDefaultAsync(s => s.ServiceId == serviceId && s.StatDate == date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task UpsertAsync(long serviceId, DateOnly statDate, int sentCnt, int successCnt, int failCnt, int openCnt)
|
||||||
|
{
|
||||||
|
var existing = await _dbSet
|
||||||
|
.FirstOrDefaultAsync(s => s.ServiceId == serviceId && s.StatDate == statDate);
|
||||||
|
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
existing.SentCnt = sentCnt;
|
||||||
|
existing.SuccessCnt = successCnt;
|
||||||
|
existing.FailCnt = failCnt;
|
||||||
|
existing.OpenCnt = openCnt;
|
||||||
|
_context.Entry(existing).State = EntityState.Modified;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _dbSet.AddAsync(new DailyStat
|
||||||
|
{
|
||||||
|
ServiceId = serviceId,
|
||||||
|
StatDate = statDate,
|
||||||
|
SentCnt = sentCnt,
|
||||||
|
SuccessCnt = successCnt,
|
||||||
|
FailCnt = failCnt,
|
||||||
|
OpenCnt = openCnt,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
139
SPMS.Infrastructure/Workers/DailyStatWorker.cs
Normal file
139
SPMS.Infrastructure/Workers/DailyStatWorker.cs
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
2
TASKS.md
2
TASKS.md
|
|
@ -1655,7 +1655,7 @@ Milestone: Phase 3: 메시지 & Push Core
|
||||||
| 9 | [Feature] 실패원인 순위 API | Feature | Medium | ANA-01 | ✅ |
|
| 9 | [Feature] 실패원인 순위 API | Feature | Medium | ANA-01 | ✅ |
|
||||||
| 10 | [Feature] 웹훅 설정 API | Feature | High | WHK-01 | ✅ |
|
| 10 | [Feature] 웹훅 설정 API | Feature | High | WHK-01 | ✅ |
|
||||||
| 11 | [Feature] **웹훅 발송 서비스** | Feature | High | WHK-02 | ✅ |
|
| 11 | [Feature] **웹훅 발송 서비스** | Feature | High | WHK-02 | ✅ |
|
||||||
| 12 | [Feature] **DailyStatWorker 구현** | Feature | Medium | AAG-01 | ⬜ |
|
| 12 | [Feature] **DailyStatWorker 구현** | Feature | Medium | AAG-01 | ✅ |
|
||||||
| 13 | [Feature] **DeadTokenCleanupWorker 구현** | Feature | Medium | DTK-01 | ⬜ |
|
| 13 | [Feature] **DeadTokenCleanupWorker 구현** | Feature | Medium | DTK-01 | ⬜ |
|
||||||
| 14 | [Feature] **데이터 보관 주기 관리 배치** | Feature | Medium | RET-01 | ⬜ |
|
| 14 | [Feature] **데이터 보관 주기 관리 배치** | Feature | Medium | RET-01 | ⬜ |
|
||||||
| 15 | [Feature] **Redis 토큰 캐시 관리** | Feature | Medium | - | ⬜ |
|
| 15 | [Feature] **Redis 토큰 캐시 관리** | Feature | Medium | - | ⬜ |
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user