improvement: 서비스 스코프 정책 고정 (#199)
All checks were successful
SPMS_API/pipeline/head This commit looks good

Reviewed-on: https://git.ipstein.myds.me/SPMS/SPMS_API/pulls/200
This commit is contained in:
김선규 2026-02-24 08:17:42 +00:00
commit 68fe6b91a5
16 changed files with 170 additions and 84 deletions

View File

@ -78,7 +78,7 @@ public class DeviceController : ControllerBase
[SwaggerOperation(Summary = "디바이스 목록", Description = "대시보드에서 디바이스 목록을 조회합니다. JWT 인증 필요.")]
public async Task<IActionResult> GetListAsync([FromBody] DeviceListRequestDto request)
{
var serviceId = GetServiceId();
var serviceId = GetOptionalServiceId();
var result = await _deviceService.GetListAsync(serviceId, request);
return Ok(ApiResponse<DeviceListResponseDto>.Success(result));
}
@ -90,4 +90,11 @@ public class DeviceController : ControllerBase
throw new Domain.Exceptions.SpmsException(ErrorCodes.BadRequest, "서비스 식별 정보가 없습니다.", 400);
}
private long? GetOptionalServiceId()
{
if (HttpContext.Items.TryGetValue("ServiceId", out var obj) && obj is long id)
return id;
return null;
}
}

View File

@ -22,7 +22,7 @@ public class StatsController : ControllerBase
[SwaggerOperation(Summary = "일별 통계 조회", Description = "기간별 일별 발송/성공/실패/열람 통계를 조회합니다.")]
public async Task<IActionResult> GetDailyAsync([FromBody] DailyStatRequestDto request)
{
var serviceId = GetServiceId();
var serviceId = GetOptionalServiceId();
var result = await _statsService.GetDailyAsync(serviceId, request);
return Ok(ApiResponse<DailyStatResponseDto>.Success(result, "조회 성공"));
}
@ -31,7 +31,7 @@ public class StatsController : ControllerBase
[SwaggerOperation(Summary = "요약 통계 조회", Description = "대시보드 요약 통계를 조회합니다.")]
public async Task<IActionResult> GetSummaryAsync()
{
var serviceId = GetServiceId();
var serviceId = GetOptionalServiceId();
var result = await _statsService.GetSummaryAsync(serviceId);
return Ok(ApiResponse<SummaryStatResponseDto>.Success(result, "조회 성공"));
}
@ -40,7 +40,7 @@ public class StatsController : ControllerBase
[SwaggerOperation(Summary = "메시지별 통계 조회", Description = "특정 메시지의 발송 통계를 조회합니다.")]
public async Task<IActionResult> GetMessageStatAsync([FromBody] MessageStatRequestDto request)
{
var serviceId = GetServiceId();
var serviceId = GetOptionalServiceId();
var result = await _statsService.GetMessageStatAsync(serviceId, request);
return Ok(ApiResponse<MessageStatResponseDto>.Success(result, "조회 성공"));
}
@ -49,7 +49,7 @@ public class StatsController : ControllerBase
[SwaggerOperation(Summary = "시간대별 통계 조회", Description = "시간대별 발송 추이를 조회합니다.")]
public async Task<IActionResult> GetHourlyAsync([FromBody] HourlyStatRequestDto request)
{
var serviceId = GetServiceId();
var serviceId = GetOptionalServiceId();
var result = await _statsService.GetHourlyAsync(serviceId, request);
return Ok(ApiResponse<HourlyStatResponseDto>.Success(result, "조회 성공"));
}
@ -58,7 +58,7 @@ public class StatsController : ControllerBase
[SwaggerOperation(Summary = "디바이스 통계 조회", Description = "플랫폼/모델별 디바이스 분포를 조회합니다.")]
public async Task<IActionResult> GetDeviceStatAsync()
{
var serviceId = GetServiceId();
var serviceId = GetOptionalServiceId();
var result = await _statsService.GetDeviceStatAsync(serviceId);
return Ok(ApiResponse<DeviceStatResponseDto>.Success(result, "조회 성공"));
}
@ -67,7 +67,7 @@ public class StatsController : ControllerBase
[SwaggerOperation(Summary = "통계 리포트 다운로드", Description = "일별/시간대별/플랫폼별 통계를 엑셀(.xlsx) 파일로 다운로드합니다.")]
public async Task<IActionResult> ExportReportAsync([FromBody] StatsExportRequestDto request)
{
var serviceId = GetServiceId();
var serviceId = GetOptionalServiceId();
var fileBytes = await _statsService.ExportReportAsync(serviceId, request);
var fileName = $"stats_report_{request.StartDate}_{request.EndDate}.xlsx";
return File(fileBytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName);
@ -77,7 +77,7 @@ public class StatsController : ControllerBase
[SwaggerOperation(Summary = "실패원인 통계 조회", Description = "실패 원인별 집계를 상위 N개로 조회합니다.")]
public async Task<IActionResult> GetFailureStatAsync([FromBody] FailureStatRequestDto request)
{
var serviceId = GetServiceId();
var serviceId = GetOptionalServiceId();
var result = await _statsService.GetFailureStatAsync(serviceId, request);
return Ok(ApiResponse<FailureStatResponseDto>.Success(result, "조회 성공"));
}
@ -86,16 +86,15 @@ public class StatsController : ControllerBase
[SwaggerOperation(Summary = "발송 상세 로그 조회", Description = "특정 메시지의 개별 디바이스별 발송 상세 로그를 조회합니다.")]
public async Task<IActionResult> GetSendLogDetailAsync([FromBody] SendLogDetailRequestDto request)
{
var serviceId = GetServiceId();
var serviceId = GetOptionalServiceId();
var result = await _statsService.GetSendLogDetailAsync(serviceId, request);
return Ok(ApiResponse<SendLogDetailResponseDto>.Success(result, "조회 성공"));
}
private long GetServiceId()
private long? GetOptionalServiceId()
{
if (HttpContext.Items.TryGetValue("ServiceId", out var serviceIdObj) && serviceIdObj is long serviceId)
return serviceId;
throw new Domain.Exceptions.SpmsException(ErrorCodes.BadRequest, "서비스 식별 정보가 없습니다.", 400);
if (HttpContext.Items.TryGetValue("ServiceId", out var obj) && obj is long id)
return id;
return null;
}
}

View File

@ -15,12 +15,26 @@ public class SpmsHeaderOperationFilter : IOperationFilter
operation.Parameters ??= new List<OpenApiParameter>();
// v1/in/* 중 X-Service-Code가 필요한 경로만 (device, message, push, stats, file)
if (relativePath.StartsWith("v1/in/device") ||
relativePath.StartsWith("v1/in/message") ||
// v1/in/* 중 X-Service-Code 대상 경로 판별
var isStatsOrDeviceList = relativePath.StartsWith("v1/in/stats") ||
relativePath == "v1/in/device/list";
var isRequired = relativePath.StartsWith("v1/in/message") ||
relativePath.StartsWith("v1/in/push") ||
relativePath.StartsWith("v1/in/stats") ||
relativePath.StartsWith("v1/in/file"))
relativePath.StartsWith("v1/in/file");
var isDeviceNonList = relativePath.StartsWith("v1/in/device") && !isStatsOrDeviceList;
if (isStatsOrDeviceList)
{
operation.Parameters.Add(new OpenApiParameter
{
Name = "X-Service-Code",
In = ParameterLocation.Header,
Required = false,
Description = "서비스 식별 코드 (관리자: 미지정 시 전체 서비스 조회)",
Schema = new OpenApiSchema { Type = "string" }
});
}
else if (isRequired || isDeviceNonList)
{
operation.Parameters.Add(new OpenApiParameter
{

View File

@ -12,20 +12,52 @@ public class ServiceCodeMiddleware
public async Task InvokeAsync(HttpContext context, IServiceRepository serviceRepository)
{
if (context.Request.Path.StartsWithSegments("/v1/out") ||
context.Request.Path.StartsWithSegments("/v1/in/auth") ||
context.Request.Path.StartsWithSegments("/v1/in/account") ||
context.Request.Path.StartsWithSegments("/v1/in/public") ||
context.Request.Path.StartsWithSegments("/v1/in/service") ||
(context.Request.Path.StartsWithSegments("/v1/in/device") &&
!context.Request.Path.StartsWithSegments("/v1/in/device/list")) ||
context.Request.Path.StartsWithSegments("/swagger") ||
context.Request.Path.StartsWithSegments("/health"))
var path = context.Request.Path;
// === SKIP: X-Service-Code 불필요 ===
if (path.StartsWithSegments("/v1/out") ||
path.StartsWithSegments("/v1/in/auth") ||
path.StartsWithSegments("/v1/in/account") ||
path.StartsWithSegments("/v1/in/public") ||
path.StartsWithSegments("/v1/in/service") ||
(path.StartsWithSegments("/v1/in/device") &&
!path.StartsWithSegments("/v1/in/device/list")) ||
path.StartsWithSegments("/swagger") ||
path.StartsWithSegments("/health"))
{
await _next(context);
return;
}
// === OPTIONAL_FOR_ADMIN: 관리자는 X-Service-Code 선택 ===
if (path.StartsWithSegments("/v1/in/stats") ||
path.StartsWithSegments("/v1/in/device/list"))
{
if (context.Request.Headers.TryGetValue("X-Service-Code", out var optionalCode) &&
!string.IsNullOrWhiteSpace(optionalCode))
{
// 헤더가 있으면 기존 검증 수행
await ValidateAndSetService(context, serviceRepository, optionalCode!);
return;
}
// 헤더 없음 — 인증된 사용자만 전체 서비스 모드 허용
if (context.User.Identity?.IsAuthenticated == true)
{
// ServiceId 미설정 = 전체 서비스 모드
await _next(context);
return;
}
// 비인증 요청 → 에러
context.Response.StatusCode = 400;
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(
ApiResponse.Fail(ErrorCodes.ServiceScopeRequired, "X-Service-Code 헤더가 필요합니다."));
return;
}
// === REQUIRED: X-Service-Code 필수 ===
if (!context.Request.Headers.TryGetValue("X-Service-Code", out var serviceCode) ||
string.IsNullOrWhiteSpace(serviceCode))
{
@ -36,7 +68,12 @@ public class ServiceCodeMiddleware
return;
}
var service = await serviceRepository.GetByServiceCodeAsync(serviceCode!);
await ValidateAndSetService(context, serviceRepository, serviceCode!);
}
private async Task ValidateAndSetService(HttpContext context, IServiceRepository serviceRepository, string serviceCode)
{
var service = await serviceRepository.GetByServiceCodeAsync(serviceCode);
if (service == null)
{
context.Response.StatusCode = 404;

View File

@ -8,7 +8,7 @@ public interface IDeviceService
Task<DeviceInfoResponseDto> GetInfoAsync(long serviceId, DeviceInfoRequestDto request);
Task UpdateAsync(long serviceId, DeviceUpdateRequestDto request);
Task DeleteAsync(long serviceId, DeviceDeleteRequestDto request);
Task<DeviceListResponseDto> GetListAsync(long serviceId, DeviceListRequestDto request);
Task<DeviceListResponseDto> GetListAsync(long? serviceId, DeviceListRequestDto request);
Task SetTagsAsync(long serviceId, DeviceTagsRequestDto request);
Task SetAgreeAsync(long serviceId, DeviceAgreeRequestDto request);
}

View File

@ -4,12 +4,12 @@ namespace SPMS.Application.Interfaces;
public interface IStatsService
{
Task<DailyStatResponseDto> GetDailyAsync(long serviceId, DailyStatRequestDto request);
Task<SummaryStatResponseDto> GetSummaryAsync(long serviceId);
Task<MessageStatResponseDto> GetMessageStatAsync(long serviceId, MessageStatRequestDto request);
Task<HourlyStatResponseDto> GetHourlyAsync(long serviceId, HourlyStatRequestDto request);
Task<DeviceStatResponseDto> GetDeviceStatAsync(long serviceId);
Task<SendLogDetailResponseDto> GetSendLogDetailAsync(long serviceId, SendLogDetailRequestDto request);
Task<byte[]> ExportReportAsync(long serviceId, StatsExportRequestDto request);
Task<FailureStatResponseDto> GetFailureStatAsync(long serviceId, FailureStatRequestDto request);
Task<DailyStatResponseDto> GetDailyAsync(long? serviceId, DailyStatRequestDto request);
Task<SummaryStatResponseDto> GetSummaryAsync(long? serviceId);
Task<MessageStatResponseDto> GetMessageStatAsync(long? serviceId, MessageStatRequestDto request);
Task<HourlyStatResponseDto> GetHourlyAsync(long? serviceId, HourlyStatRequestDto request);
Task<DeviceStatResponseDto> GetDeviceStatAsync(long? serviceId);
Task<SendLogDetailResponseDto> GetSendLogDetailAsync(long? serviceId, SendLogDetailRequestDto request);
Task<byte[]> ExportReportAsync(long? serviceId, StatsExportRequestDto request);
Task<FailureStatResponseDto> GetFailureStatAsync(long? serviceId, FailureStatRequestDto request);
}

View File

@ -119,7 +119,7 @@ public class DeviceService : IDeviceService
await _tokenCache.InvalidateAsync(serviceId, device.Id);
}
public async Task<DeviceListResponseDto> GetListAsync(long serviceId, DeviceListRequestDto request)
public async Task<DeviceListResponseDto> GetListAsync(long? serviceId, DeviceListRequestDto request)
{
Platform? platform = null;
if (!string.IsNullOrWhiteSpace(request.Platform))

View File

@ -29,7 +29,7 @@ public class StatsService : IStatsService
_messageRepository = messageRepository;
}
public async Task<DailyStatResponseDto> GetDailyAsync(long serviceId, DailyStatRequestDto request)
public async Task<DailyStatResponseDto> GetDailyAsync(long? serviceId, DailyStatRequestDto request)
{
var (startDate, endDate) = ParseDateRange(request.StartDate, request.EndDate);
@ -64,13 +64,21 @@ public class StatsService : IStatsService
};
}
public async Task<SummaryStatResponseDto> GetSummaryAsync(long serviceId)
public async Task<SummaryStatResponseDto> GetSummaryAsync(long? serviceId)
{
var totalDevices = await _deviceRepository.CountAsync(d => d.ServiceId == serviceId);
var activeDevices = await _deviceRepository.CountAsync(d => d.ServiceId == serviceId && d.IsActive);
var totalMessages = await _messageRepository.CountAsync(m => m.ServiceId == serviceId && !m.IsDeleted);
var totalDevices = serviceId.HasValue
? await _deviceRepository.CountAsync(d => d.ServiceId == serviceId.Value)
: await _deviceRepository.CountAsync(d => true);
var activeDevices = serviceId.HasValue
? await _deviceRepository.CountAsync(d => d.ServiceId == serviceId.Value && d.IsActive)
: await _deviceRepository.CountAsync(d => d.IsActive);
var totalMessages = serviceId.HasValue
? await _messageRepository.CountAsync(m => m.ServiceId == serviceId.Value && !m.IsDeleted)
: await _messageRepository.CountAsync(m => !m.IsDeleted);
var allStats = await _dailyStatRepository.FindAsync(s => s.ServiceId == serviceId);
var allStats = serviceId.HasValue
? await _dailyStatRepository.FindAsync(s => s.ServiceId == serviceId.Value)
: await _dailyStatRepository.FindAsync(s => true);
var totalSend = allStats.Sum(s => (long)s.SentCnt);
var totalSuccess = allStats.Sum(s => (long)s.SuccessCnt);
var totalOpen = allStats.Sum(s => (long)s.OpenCnt);
@ -107,20 +115,22 @@ public class StatsService : IStatsService
};
}
public async Task<MessageStatResponseDto> GetMessageStatAsync(long serviceId, MessageStatRequestDto request)
public async Task<MessageStatResponseDto> GetMessageStatAsync(long? serviceId, MessageStatRequestDto request)
{
if (string.IsNullOrWhiteSpace(request.MessageCode))
throw new SpmsException(ErrorCodes.BadRequest, "message_code는 필수입니다.", 400);
var message = await _messageRepository.GetByMessageCodeAndServiceAsync(request.MessageCode, serviceId);
var message = serviceId.HasValue
? await _messageRepository.GetByMessageCodeAndServiceAsync(request.MessageCode, serviceId.Value)
: await _messageRepository.GetByMessageCodeAsync(request.MessageCode);
if (message == null)
throw new SpmsException(ErrorCodes.MessageNotFound, "존재하지 않는 메시지 코드입니다.", 404);
DateTime? startDate = ParseOptionalDate(request.StartDate);
DateTime? endDate = ParseOptionalDate(request.EndDate);
var stats = await _pushSendLogRepository.GetMessageStatsAsync(serviceId, message.Id, startDate, endDate);
var dailyStats = await _pushSendLogRepository.GetMessageDailyStatsAsync(serviceId, message.Id, startDate, endDate);
var stats = await _pushSendLogRepository.GetMessageStatsAsync(message.ServiceId, message.Id, startDate, endDate);
var dailyStats = await _pushSendLogRepository.GetMessageDailyStatsAsync(message.ServiceId, message.Id, startDate, endDate);
return new MessageStatResponseDto
{
@ -143,7 +153,7 @@ public class StatsService : IStatsService
};
}
public async Task<HourlyStatResponseDto> GetHourlyAsync(long serviceId, HourlyStatRequestDto request)
public async Task<HourlyStatResponseDto> GetHourlyAsync(long? serviceId, HourlyStatRequestDto request)
{
var (startDate, endDate) = ParseDateRange(request.StartDate, request.EndDate);
@ -178,9 +188,11 @@ public class StatsService : IStatsService
};
}
public async Task<DeviceStatResponseDto> GetDeviceStatAsync(long serviceId)
public async Task<DeviceStatResponseDto> GetDeviceStatAsync(long? serviceId)
{
var devices = await _deviceRepository.FindAsync(d => d.ServiceId == serviceId && d.IsActive);
var devices = serviceId.HasValue
? await _deviceRepository.FindAsync(d => d.ServiceId == serviceId.Value && d.IsActive)
: await _deviceRepository.FindAsync(d => d.IsActive);
var total = devices.Count;
var byPlatform = devices
@ -236,9 +248,11 @@ public class StatsService : IStatsService
};
}
public async Task<SendLogDetailResponseDto> GetSendLogDetailAsync(long serviceId, SendLogDetailRequestDto request)
public async Task<SendLogDetailResponseDto> GetSendLogDetailAsync(long? serviceId, SendLogDetailRequestDto request)
{
var message = await _messageRepository.GetByMessageCodeAndServiceAsync(request.MessageCode, serviceId);
var message = serviceId.HasValue
? await _messageRepository.GetByMessageCodeAndServiceAsync(request.MessageCode, serviceId.Value)
: await _messageRepository.GetByMessageCodeAsync(request.MessageCode);
if (message == null)
throw new SpmsException(ErrorCodes.MessageNotFound, "존재하지 않는 메시지 코드입니다.", 404);
@ -266,7 +280,7 @@ public class StatsService : IStatsService
}
var (items, totalCount) = await _pushSendLogRepository.GetDetailLogPagedAsync(
serviceId, message.Id, request.Page, request.Size, status, platform);
message.ServiceId, message.Id, request.Page, request.Size, status, platform);
var totalPages = (int)Math.Ceiling((double)totalCount / request.Size);
@ -291,7 +305,7 @@ public class StatsService : IStatsService
};
}
public async Task<byte[]> ExportReportAsync(long serviceId, StatsExportRequestDto request)
public async Task<byte[]> ExportReportAsync(long? serviceId, StatsExportRequestDto request)
{
var (startDate, endDate) = ParseDateRange(request.StartDate, request.EndDate);
@ -344,7 +358,9 @@ public class StatsService : IStatsService
ws2.Columns().AdjustToContents();
// Sheet 3: 플랫폼별 통계
var devices = await _deviceRepository.FindAsync(d => d.ServiceId == serviceId && d.IsActive);
var devices = serviceId.HasValue
? await _deviceRepository.FindAsync(d => d.ServiceId == serviceId.Value && d.IsActive)
: await _deviceRepository.FindAsync(d => d.IsActive);
var total = devices.Count;
var ws3 = workbook.Worksheets.Add("플랫폼별 통계");
@ -373,7 +389,7 @@ public class StatsService : IStatsService
return stream.ToArray();
}
public async Task<FailureStatResponseDto> GetFailureStatAsync(long serviceId, FailureStatRequestDto request)
public async Task<FailureStatResponseDto> GetFailureStatAsync(long? serviceId, FailureStatRequestDto request)
{
var (startDate, endDate) = ParseDateRange(request.StartDate, request.EndDate);

View File

@ -31,6 +31,7 @@ public static class ErrorCodes
// === Service (3) ===
public const string DecryptionFailed = "131";
public const string InvalidCredentials = "132";
public const string ServiceScopeRequired = "133";
// === Device (4) ===
public const string DeviceNotFound = "141";

View File

@ -28,6 +28,9 @@ public class SpmsException : Exception
public static SpmsException Conflict(string message)
=> new(ErrorCodes.Conflict, message, 409);
public static SpmsException Forbidden(string message)
=> new(ErrorCodes.Forbidden, message, 403);
public static SpmsException LimitExceeded(string message)
=> new(ErrorCodes.LimitExceeded, message, 429);
}

View File

@ -4,7 +4,7 @@ namespace SPMS.Domain.Interfaces;
public interface IDailyStatRepository : IRepository<DailyStat>
{
Task<IReadOnlyList<DailyStat>> GetByDateRangeAsync(long serviceId, DateOnly startDate, DateOnly endDate);
Task<DailyStat?> GetByDateAsync(long serviceId, DateOnly date);
Task<IReadOnlyList<DailyStat>> GetByDateRangeAsync(long? serviceId, DateOnly startDate, DateOnly endDate);
Task<DailyStat?> GetByDateAsync(long? serviceId, DateOnly date);
Task UpsertAsync(long serviceId, DateOnly statDate, int sentCnt, int successCnt, int failCnt, int openCnt);
}

View File

@ -10,7 +10,7 @@ public interface IDeviceRepository : IRepository<Device>
Task<int> GetActiveCountByServiceAsync(long serviceId);
Task<IReadOnlyList<Device>> GetByPlatformAsync(long serviceId, Platform platform);
Task<(IReadOnlyList<Device> Items, int TotalCount)> GetPagedAsync(
long serviceId, int page, int size,
long? serviceId, int page, int size,
Platform? platform = null, bool? pushAgreed = null,
bool? isActive = null, List<int>? tags = null);
}

View File

@ -11,7 +11,7 @@ public interface IPushSendLogRepository : IRepository<PushSendLog>
PushResult? status = null,
DateTime? startDate = null, DateTime? endDate = null);
Task<List<HourlyStatRaw>> GetHourlyStatsAsync(long serviceId, DateTime startDate, DateTime endDate);
Task<List<HourlyStatRaw>> GetHourlyStatsAsync(long? serviceId, DateTime startDate, DateTime endDate);
Task<MessageStatRaw?> GetMessageStatsAsync(long serviceId, long messageId, DateTime? startDate, DateTime? endDate);
Task<List<MessageDailyStatRaw>> GetMessageDailyStatsAsync(long serviceId, long messageId, DateTime? startDate, DateTime? endDate);
Task<(IReadOnlyList<PushSendLog> Items, int TotalCount)> GetDetailLogPagedAsync(
@ -21,7 +21,7 @@ public interface IPushSendLogRepository : IRepository<PushSendLog>
long serviceId, DateTime startDate, DateTime endDate,
long? messageId = null, long? deviceId = null,
PushResult? status = null, int maxCount = 100000);
Task<List<FailureStatRaw>> GetFailureStatsAsync(long serviceId, DateTime startDate, DateTime endDate, int limit);
Task<List<FailureStatRaw>> GetFailureStatsAsync(long? serviceId, DateTime startDate, DateTime endDate, int limit);
}
public class HourlyStatRaw

View File

@ -8,18 +8,20 @@ public class DailyStatRepository : Repository<DailyStat>, IDailyStatRepository
{
public DailyStatRepository(AppDbContext context) : base(context) { }
public async Task<IReadOnlyList<DailyStat>> GetByDateRangeAsync(long serviceId, DateOnly startDate, DateOnly endDate)
public async Task<IReadOnlyList<DailyStat>> GetByDateRangeAsync(long? serviceId, DateOnly startDate, DateOnly endDate)
{
return await _dbSet
.Where(s => s.ServiceId == serviceId && s.StatDate >= startDate && s.StatDate <= endDate)
.OrderByDescending(s => s.StatDate)
.ToListAsync();
var query = _dbSet.Where(s => s.StatDate >= startDate && s.StatDate <= endDate);
if (serviceId.HasValue)
query = query.Where(s => s.ServiceId == serviceId.Value);
return await query.OrderByDescending(s => s.StatDate).ToListAsync();
}
public async Task<DailyStat?> GetByDateAsync(long serviceId, DateOnly date)
public async Task<DailyStat?> GetByDateAsync(long? serviceId, DateOnly date)
{
return await _dbSet
.FirstOrDefaultAsync(s => s.ServiceId == serviceId && s.StatDate == date);
var query = _dbSet.Where(s => s.StatDate == date);
if (serviceId.HasValue)
query = query.Where(s => s.ServiceId == serviceId.Value);
return await query.FirstOrDefaultAsync();
}
public async Task UpsertAsync(long serviceId, DateOnly statDate, int sentCnt, int successCnt, int failCnt, int openCnt)

View File

@ -32,11 +32,13 @@ public class DeviceRepository : Repository<Device>, IDeviceRepository
}
public async Task<(IReadOnlyList<Device> Items, int TotalCount)> GetPagedAsync(
long serviceId, int page, int size,
long? serviceId, int page, int size,
Platform? platform = null, bool? pushAgreed = null,
bool? isActive = null, List<int>? tags = null)
{
var query = _dbSet.Where(d => d.ServiceId == serviceId);
IQueryable<Device> query = _dbSet;
if (serviceId.HasValue)
query = query.Where(d => d.ServiceId == serviceId.Value);
if (platform.HasValue)
query = query.Where(d => d.Platform == platform.Value);

View File

@ -45,10 +45,13 @@ public class PushSendLogRepository : Repository<PushSendLog>, IPushSendLogReposi
return (items, totalCount);
}
public async Task<List<HourlyStatRaw>> GetHourlyStatsAsync(long serviceId, DateTime startDate, DateTime endDate)
public async Task<List<HourlyStatRaw>> GetHourlyStatsAsync(long? serviceId, DateTime startDate, DateTime endDate)
{
return await _dbSet
.Where(l => l.ServiceId == serviceId && l.SentAt >= startDate && l.SentAt < 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
{
@ -157,14 +160,16 @@ public class PushSendLogRepository : Repository<PushSendLog>, IPushSendLogReposi
.ToListAsync();
}
public async Task<List<FailureStatRaw>> GetFailureStatsAsync(long serviceId, DateTime startDate, DateTime endDate, int limit)
public async Task<List<FailureStatRaw>> GetFailureStatsAsync(long? serviceId, DateTime startDate, DateTime endDate, int limit)
{
return await _dbSet
.Where(l => l.ServiceId == serviceId
&& l.Status == PushResult.Failed
var query = _dbSet.Where(l => l.Status == PushResult.Failed
&& l.SentAt >= startDate
&& l.SentAt < endDate
&& l.FailReason != null)
&& 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
{