- POST /v1/in/stats/history/export 엔드포인트 추가 - history/list와 동일 필터(keyword/status/date) 기준 엑셀 내보내기 - PushSendLogRepository에서 GroupBy 쿼리를 private helper로 리팩토링 - ClosedXML로 엑셀 생성 (메시지코드/제목/서비스명/발송일시/대상수/성공/실패/오픈율/상태) Closes #191
283 lines
10 KiB
C#
283 lines
10 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using SPMS.Domain.Entities;
|
|
using SPMS.Domain.Enums;
|
|
using SPMS.Domain.Interfaces;
|
|
|
|
namespace SPMS.Infrastructure.Persistence.Repositories;
|
|
|
|
public class PushSendLogRepository : Repository<PushSendLog>, IPushSendLogRepository
|
|
{
|
|
public PushSendLogRepository(AppDbContext context) : base(context) { }
|
|
|
|
public async Task<(IReadOnlyList<PushSendLog> Items, int TotalCount)> GetPagedWithMessageAsync(
|
|
long serviceId, int page, int size,
|
|
long? messageId = null, long? deviceId = null,
|
|
PushResult? status = null,
|
|
DateTime? startDate = null, DateTime? endDate = null)
|
|
{
|
|
var query = _dbSet
|
|
.Include(l => l.Message)
|
|
.Where(l => l.ServiceId == serviceId);
|
|
|
|
if (messageId.HasValue)
|
|
query = query.Where(l => l.MessageId == messageId.Value);
|
|
|
|
if (deviceId.HasValue)
|
|
query = query.Where(l => l.DeviceId == deviceId.Value);
|
|
|
|
if (status.HasValue)
|
|
query = query.Where(l => l.Status == status.Value);
|
|
|
|
if (startDate.HasValue)
|
|
query = query.Where(l => l.SentAt >= startDate.Value);
|
|
|
|
if (endDate.HasValue)
|
|
query = query.Where(l => l.SentAt < endDate.Value.AddDays(1));
|
|
|
|
var totalCount = await query.CountAsync();
|
|
|
|
var items = await query
|
|
.OrderByDescending(l => l.SentAt)
|
|
.Skip((page - 1) * size)
|
|
.Take(size)
|
|
.ToListAsync();
|
|
|
|
return (items, totalCount);
|
|
}
|
|
|
|
public async Task<List<HourlyStatRaw>> GetHourlyStatsAsync(long? serviceId, DateTime startDate, DateTime endDate)
|
|
{
|
|
var query = _dbSet.Where(l => l.SentAt >= startDate && l.SentAt < endDate);
|
|
if (serviceId.HasValue)
|
|
query = query.Where(l => l.ServiceId == serviceId.Value);
|
|
|
|
return await query
|
|
.GroupBy(l => l.SentAt.Hour)
|
|
.Select(g => new HourlyStatRaw
|
|
{
|
|
Hour = g.Key,
|
|
SendCount = g.Count(),
|
|
SuccessCount = g.Count(l => l.Status == PushResult.Success),
|
|
FailCount = g.Count(l => l.Status == PushResult.Failed)
|
|
})
|
|
.OrderBy(h => h.Hour)
|
|
.ToListAsync();
|
|
}
|
|
|
|
public async Task<MessageStatRaw?> GetMessageStatsAsync(long serviceId, long messageId, DateTime? startDate, DateTime? endDate)
|
|
{
|
|
var query = _dbSet.Where(l => l.ServiceId == serviceId && l.MessageId == messageId);
|
|
|
|
if (startDate.HasValue)
|
|
query = query.Where(l => l.SentAt >= startDate.Value);
|
|
if (endDate.HasValue)
|
|
query = query.Where(l => l.SentAt < endDate.Value.AddDays(1));
|
|
|
|
var hasData = await query.AnyAsync();
|
|
if (!hasData) return null;
|
|
|
|
return await query
|
|
.GroupBy(_ => 1)
|
|
.Select(g => new MessageStatRaw
|
|
{
|
|
TotalSend = g.Count(),
|
|
TotalSuccess = g.Count(l => l.Status == PushResult.Success),
|
|
TotalFail = g.Count(l => l.Status == PushResult.Failed),
|
|
FirstSentAt = g.Min(l => l.SentAt),
|
|
LastSentAt = g.Max(l => l.SentAt)
|
|
})
|
|
.FirstOrDefaultAsync();
|
|
}
|
|
|
|
public async Task<(IReadOnlyList<PushSendLog> Items, int TotalCount)> GetDetailLogPagedAsync(
|
|
long serviceId, long messageId, int page, int size,
|
|
PushResult? status = null, Platform? platform = null)
|
|
{
|
|
var query = _dbSet
|
|
.Include(l => l.Device)
|
|
.Where(l => l.ServiceId == serviceId && l.MessageId == messageId);
|
|
|
|
if (status.HasValue)
|
|
query = query.Where(l => l.Status == status.Value);
|
|
|
|
if (platform.HasValue)
|
|
query = query.Where(l => l.Device.Platform == platform.Value);
|
|
|
|
var totalCount = await query.CountAsync();
|
|
|
|
var items = await query
|
|
.OrderByDescending(l => l.SentAt)
|
|
.Skip((page - 1) * size)
|
|
.Take(size)
|
|
.ToListAsync();
|
|
|
|
return (items, totalCount);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<PushSendLog>> GetExportLogsAsync(
|
|
long serviceId, DateTime startDate, DateTime endDate,
|
|
long? messageId = null, long? deviceId = null,
|
|
PushResult? status = null, int maxCount = 100000)
|
|
{
|
|
var query = _dbSet
|
|
.Include(l => l.Message)
|
|
.Include(l => l.Device)
|
|
.Where(l => l.ServiceId == serviceId && l.SentAt >= startDate && l.SentAt < endDate);
|
|
|
|
if (messageId.HasValue)
|
|
query = query.Where(l => l.MessageId == messageId.Value);
|
|
|
|
if (deviceId.HasValue)
|
|
query = query.Where(l => l.DeviceId == deviceId.Value);
|
|
|
|
if (status.HasValue)
|
|
query = query.Where(l => l.Status == status.Value);
|
|
|
|
return await query
|
|
.OrderByDescending(l => l.SentAt)
|
|
.Take(maxCount)
|
|
.ToListAsync();
|
|
}
|
|
|
|
public async Task<List<MessageDailyStatRaw>> GetMessageDailyStatsAsync(long serviceId, long messageId, DateTime? startDate, DateTime? endDate)
|
|
{
|
|
var query = _dbSet.Where(l => l.ServiceId == serviceId && l.MessageId == messageId);
|
|
|
|
if (startDate.HasValue)
|
|
query = query.Where(l => l.SentAt >= startDate.Value);
|
|
if (endDate.HasValue)
|
|
query = query.Where(l => l.SentAt < endDate.Value.AddDays(1));
|
|
|
|
return await query
|
|
.GroupBy(l => DateOnly.FromDateTime(l.SentAt))
|
|
.Select(g => new MessageDailyStatRaw
|
|
{
|
|
StatDate = g.Key,
|
|
SendCount = g.Count(),
|
|
SuccessCount = g.Count(l => l.Status == PushResult.Success)
|
|
})
|
|
.OrderByDescending(d => d.StatDate)
|
|
.ToListAsync();
|
|
}
|
|
|
|
public async Task<(IReadOnlyList<MessageHistorySummary> Items, int TotalCount)> GetMessageHistoryPagedAsync(
|
|
long? serviceId, int page, int size,
|
|
string? keyword = null, string? status = null,
|
|
DateTime? startDate = null, DateTime? endDate = null)
|
|
{
|
|
var grouped = BuildMessageHistoryQuery(serviceId, keyword, status, startDate, endDate);
|
|
|
|
var totalCount = await grouped.CountAsync();
|
|
|
|
var items = await grouped
|
|
.OrderByDescending(g => g.FirstSentAt)
|
|
.Skip((page - 1) * size)
|
|
.Take(size)
|
|
.ToListAsync();
|
|
|
|
return (items, totalCount);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<MessageHistorySummary>> GetMessageHistoryAllAsync(
|
|
long? serviceId,
|
|
string? keyword = null, string? status = null,
|
|
DateTime? startDate = null, DateTime? endDate = null,
|
|
int maxCount = 100000)
|
|
{
|
|
var grouped = BuildMessageHistoryQuery(serviceId, keyword, status, startDate, endDate);
|
|
|
|
return await grouped
|
|
.OrderByDescending(g => g.FirstSentAt)
|
|
.Take(maxCount)
|
|
.ToListAsync();
|
|
}
|
|
|
|
private IQueryable<MessageHistorySummary> BuildMessageHistoryQuery(
|
|
long? serviceId, string? keyword, string? status,
|
|
DateTime? startDate, DateTime? endDate)
|
|
{
|
|
var query = _dbSet
|
|
.Include(l => l.Message)
|
|
.Include(l => l.Message.Service)
|
|
.Where(l => !l.Message.IsDeleted);
|
|
|
|
if (serviceId.HasValue)
|
|
query = query.Where(l => l.ServiceId == serviceId.Value);
|
|
|
|
if (!string.IsNullOrWhiteSpace(keyword))
|
|
query = query.Where(l => l.Message.MessageCode.Contains(keyword) || l.Message.Title.Contains(keyword));
|
|
|
|
if (startDate.HasValue)
|
|
query = query.Where(l => l.SentAt >= startDate.Value);
|
|
|
|
if (endDate.HasValue)
|
|
query = query.Where(l => l.SentAt < endDate.Value.AddDays(1));
|
|
|
|
// 메시지별 GroupBy 집계
|
|
var grouped = query
|
|
.GroupBy(l => new { l.MessageId, l.Message.MessageCode, l.Message.Title, ServiceName = l.Message.Service.ServiceName })
|
|
.Select(g => new MessageHistorySummary
|
|
{
|
|
MessageId = g.Key.MessageId,
|
|
MessageCode = g.Key.MessageCode,
|
|
Title = g.Key.Title,
|
|
ServiceName = g.Key.ServiceName,
|
|
FirstSentAt = g.Min(l => l.SentAt),
|
|
TotalSendCount = g.Count(),
|
|
SuccessCount = g.Count(l => l.Status == PushResult.Success),
|
|
FailCount = g.Count(l => l.Status == PushResult.Failed)
|
|
});
|
|
|
|
// status 필터 (DB 레벨)
|
|
if (!string.IsNullOrWhiteSpace(status))
|
|
{
|
|
grouped = status.ToLowerInvariant() switch
|
|
{
|
|
"complete" => grouped.Where(g => g.SuccessCount > 0),
|
|
"failed" => grouped.Where(g => g.TotalSendCount > 0 && g.SuccessCount == 0),
|
|
"pending" => grouped.Where(g => g.TotalSendCount == 0),
|
|
_ => grouped
|
|
};
|
|
}
|
|
|
|
return grouped;
|
|
}
|
|
|
|
public async Task<List<FailureStatRaw>> GetFailureStatsByMessageAsync(long serviceId, long messageId, int limit)
|
|
{
|
|
return await _dbSet
|
|
.Where(l => l.ServiceId == serviceId && l.MessageId == messageId
|
|
&& l.Status == PushResult.Failed && l.FailReason != null)
|
|
.GroupBy(l => l.FailReason!)
|
|
.Select(g => new FailureStatRaw
|
|
{
|
|
FailReason = g.Key,
|
|
Count = g.Count()
|
|
})
|
|
.OrderByDescending(f => f.Count)
|
|
.Take(limit)
|
|
.ToListAsync();
|
|
}
|
|
|
|
public async Task<List<FailureStatRaw>> GetFailureStatsAsync(long? serviceId, DateTime startDate, DateTime endDate, int limit)
|
|
{
|
|
var query = _dbSet.Where(l => l.Status == PushResult.Failed
|
|
&& l.SentAt >= startDate
|
|
&& l.SentAt < endDate
|
|
&& l.FailReason != null);
|
|
if (serviceId.HasValue)
|
|
query = query.Where(l => l.ServiceId == serviceId.Value);
|
|
|
|
return await query
|
|
.GroupBy(l => l.FailReason!)
|
|
.Select(g => new FailureStatRaw
|
|
{
|
|
FailReason = g.Key,
|
|
Count = g.Count()
|
|
})
|
|
.OrderByDescending(f => f.Count)
|
|
.Take(limit)
|
|
.ToListAsync();
|
|
}
|
|
}
|