improvement: Notification 도메인 구축 (#247)
- Domain: NotificationCategory enum, Notification entity, INotificationRepository - Infrastructure: NotificationConfiguration, NotificationRepository, AppDbContext/DI 등록 - Migration: AddNotificationTable 생성 및 적용 - Application: DTO 7개, INotificationService, NotificationService, DI 등록 - API: NotificationController (summary, list, read, read-all) Closes #247
This commit is contained in:
parent
f474b916c4
commit
c29a48163d
68
SPMS.API/Controllers/NotificationController.cs
Normal file
68
SPMS.API/Controllers/NotificationController.cs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
using SPMS.Application.DTOs.Notification;
|
||||
using SPMS.Application.Interfaces;
|
||||
using SPMS.Domain.Common;
|
||||
using SPMS.Domain.Exceptions;
|
||||
|
||||
namespace SPMS.API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("v1/in/notification")]
|
||||
[Authorize]
|
||||
[ApiExplorerSettings(GroupName = "notification")]
|
||||
public class NotificationController : ControllerBase
|
||||
{
|
||||
private readonly INotificationService _notificationService;
|
||||
|
||||
public NotificationController(INotificationService notificationService)
|
||||
{
|
||||
_notificationService = notificationService;
|
||||
}
|
||||
|
||||
[HttpPost("summary")]
|
||||
[SwaggerOperation(Summary = "알림 요약 조회", Description = "최근 N건의 알림과 미읽 건수를 반환합니다. 헤더 뱃지용.")]
|
||||
public async Task<IActionResult> GetSummaryAsync([FromBody] NotificationSummaryRequestDto request)
|
||||
{
|
||||
var adminId = GetAdminId();
|
||||
var result = await _notificationService.GetSummaryAsync(adminId, request);
|
||||
return Ok(ApiResponse<NotificationSummaryResponseDto>.Success(result));
|
||||
}
|
||||
|
||||
[HttpPost("list")]
|
||||
[SwaggerOperation(Summary = "알림 목록 조회", Description = "알림 목록을 페이지 단위로 조회합니다. 카테고리/기간/읽음 필터를 지원합니다.")]
|
||||
public async Task<IActionResult> GetListAsync([FromBody] NotificationListRequestDto request)
|
||||
{
|
||||
var adminId = GetAdminId();
|
||||
var result = await _notificationService.GetListAsync(adminId, request);
|
||||
return Ok(ApiResponse<NotificationListResponseDto>.Success(result));
|
||||
}
|
||||
|
||||
[HttpPost("read")]
|
||||
[SwaggerOperation(Summary = "알림 읽음 처리", Description = "단건 알림을 읽음 처리합니다. 이미 읽은 알림은 무시(멱등).")]
|
||||
public async Task<IActionResult> MarkAsReadAsync([FromBody] NotificationReadRequestDto request)
|
||||
{
|
||||
var adminId = GetAdminId();
|
||||
var result = await _notificationService.MarkAsReadAsync(adminId, request);
|
||||
return Ok(ApiResponse<NotificationReadResponseDto>.Success(result));
|
||||
}
|
||||
|
||||
[HttpPost("read-all")]
|
||||
[SwaggerOperation(Summary = "알림 전체 읽음", Description = "해당 관리자의 모든 미읽 알림을 일괄 읽음 처리합니다.")]
|
||||
public async Task<IActionResult> MarkAllAsReadAsync()
|
||||
{
|
||||
var adminId = GetAdminId();
|
||||
var result = await _notificationService.MarkAllAsReadAsync(adminId);
|
||||
return Ok(ApiResponse<NotificationReadResponseDto>.Success(result));
|
||||
}
|
||||
|
||||
private long GetAdminId()
|
||||
{
|
||||
var adminIdClaim = User.FindFirst("adminId")?.Value;
|
||||
if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId))
|
||||
throw new SpmsException(ErrorCodes.Unauthorized, "인증 정보가 올바르지 않습니다.", 401);
|
||||
|
||||
return adminId;
|
||||
}
|
||||
}
|
||||
30
SPMS.Application/DTOs/Notification/NotificationItemDto.cs
Normal file
30
SPMS.Application/DTOs/Notification/NotificationItemDto.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SPMS.Application.DTOs.Notification;
|
||||
|
||||
public class NotificationItemDto
|
||||
{
|
||||
[JsonPropertyName("notification_id")]
|
||||
public long NotificationId { get; set; }
|
||||
|
||||
[JsonPropertyName("category")]
|
||||
public string Category { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("content")]
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("link_url")]
|
||||
public string? LinkUrl { get; set; }
|
||||
|
||||
[JsonPropertyName("is_read")]
|
||||
public bool IsRead { get; set; }
|
||||
|
||||
[JsonPropertyName("read_at")]
|
||||
public DateTime? ReadAt { get; set; }
|
||||
|
||||
[JsonPropertyName("created_at")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SPMS.Application.DTOs.Notification;
|
||||
|
||||
public class NotificationListRequestDto
|
||||
{
|
||||
[JsonPropertyName("page")]
|
||||
public int Page { get; set; } = 1;
|
||||
|
||||
[JsonPropertyName("size")]
|
||||
public int Size { get; set; } = 20;
|
||||
|
||||
[JsonPropertyName("category")]
|
||||
public string? Category { get; set; }
|
||||
|
||||
[JsonPropertyName("from")]
|
||||
public DateTime? From { get; set; }
|
||||
|
||||
[JsonPropertyName("to")]
|
||||
public DateTime? To { get; set; }
|
||||
|
||||
[JsonPropertyName("is_read")]
|
||||
public bool? IsRead { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using SPMS.Application.DTOs.Notice;
|
||||
|
||||
namespace SPMS.Application.DTOs.Notification;
|
||||
|
||||
public class NotificationListResponseDto
|
||||
{
|
||||
[JsonPropertyName("items")]
|
||||
public List<NotificationItemDto> Items { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("pagination")]
|
||||
public PaginationDto Pagination { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("unread_count")]
|
||||
public int UnreadCount { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SPMS.Application.DTOs.Notification;
|
||||
|
||||
public class NotificationReadRequestDto
|
||||
{
|
||||
[Required]
|
||||
[JsonPropertyName("notification_id")]
|
||||
public long NotificationId { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SPMS.Application.DTOs.Notification;
|
||||
|
||||
public class NotificationReadResponseDto
|
||||
{
|
||||
[JsonPropertyName("unread_count")]
|
||||
public int UnreadCount { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SPMS.Application.DTOs.Notification;
|
||||
|
||||
public class NotificationSummaryRequestDto
|
||||
{
|
||||
[JsonPropertyName("limit")]
|
||||
public int Limit { get; set; } = 5;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SPMS.Application.DTOs.Notification;
|
||||
|
||||
public class NotificationSummaryResponseDto
|
||||
{
|
||||
[JsonPropertyName("recent_items")]
|
||||
public List<NotificationItemDto> RecentItems { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("unread_count")]
|
||||
public int UnreadCount { get; set; }
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ public static class DependencyInjection
|
|||
services.AddScoped<IMessageService, MessageService>();
|
||||
services.AddScoped<IStatsService, StatsService>();
|
||||
services.AddScoped<ITagService, TagService>();
|
||||
services.AddScoped<INotificationService, NotificationService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
|
|
|||
11
SPMS.Application/Interfaces/INotificationService.cs
Normal file
11
SPMS.Application/Interfaces/INotificationService.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using SPMS.Application.DTOs.Notification;
|
||||
|
||||
namespace SPMS.Application.Interfaces;
|
||||
|
||||
public interface INotificationService
|
||||
{
|
||||
Task<NotificationSummaryResponseDto> GetSummaryAsync(long adminId, NotificationSummaryRequestDto request);
|
||||
Task<NotificationListResponseDto> GetListAsync(long adminId, NotificationListRequestDto request);
|
||||
Task<NotificationReadResponseDto> MarkAsReadAsync(long adminId, NotificationReadRequestDto request);
|
||||
Task<NotificationReadResponseDto> MarkAllAsReadAsync(long adminId);
|
||||
}
|
||||
104
SPMS.Application/Services/NotificationService.cs
Normal file
104
SPMS.Application/Services/NotificationService.cs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
using SPMS.Application.DTOs.Notice;
|
||||
using SPMS.Application.DTOs.Notification;
|
||||
using SPMS.Application.Interfaces;
|
||||
using SPMS.Domain.Common;
|
||||
using SPMS.Domain.Enums;
|
||||
using SPMS.Domain.Exceptions;
|
||||
using SPMS.Domain.Interfaces;
|
||||
|
||||
namespace SPMS.Application.Services;
|
||||
|
||||
public class NotificationService : INotificationService
|
||||
{
|
||||
private readonly INotificationRepository _notificationRepository;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
public NotificationService(
|
||||
INotificationRepository notificationRepository,
|
||||
IUnitOfWork unitOfWork)
|
||||
{
|
||||
_notificationRepository = notificationRepository;
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
public async Task<NotificationSummaryResponseDto> GetSummaryAsync(long adminId, NotificationSummaryRequestDto request)
|
||||
{
|
||||
var limit = request.Limit > 0 ? request.Limit : 5;
|
||||
var recentItems = await _notificationRepository.GetRecentAsync(adminId, limit);
|
||||
var unreadCount = await _notificationRepository.GetUnreadCountAsync(adminId);
|
||||
|
||||
return new NotificationSummaryResponseDto
|
||||
{
|
||||
RecentItems = recentItems.Select(MapToItemDto).ToList(),
|
||||
UnreadCount = unreadCount
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<NotificationListResponseDto> GetListAsync(long adminId, NotificationListRequestDto request)
|
||||
{
|
||||
// category 문자열 → enum 파싱
|
||||
NotificationCategory? category = null;
|
||||
if (!string.IsNullOrWhiteSpace(request.Category))
|
||||
{
|
||||
if (!Enum.TryParse<NotificationCategory>(request.Category, true, out var parsed))
|
||||
throw new SpmsException(ErrorCodes.BadRequest, $"유효하지 않은 카테고리: {request.Category}");
|
||||
category = parsed;
|
||||
}
|
||||
|
||||
var (items, totalCount) = await _notificationRepository.GetPagedAsync(
|
||||
adminId, category, request.From, request.To, request.IsRead, request.Page, request.Size);
|
||||
|
||||
var unreadCount = await _notificationRepository.GetUnreadCountAsync(adminId);
|
||||
var totalPages = (int)Math.Ceiling((double)totalCount / request.Size);
|
||||
|
||||
return new NotificationListResponseDto
|
||||
{
|
||||
Items = items.Select(MapToItemDto).ToList(),
|
||||
Pagination = new PaginationDto
|
||||
{
|
||||
Page = request.Page,
|
||||
Size = request.Size,
|
||||
TotalCount = totalCount,
|
||||
TotalPages = totalPages
|
||||
},
|
||||
UnreadCount = unreadCount
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<NotificationReadResponseDto> MarkAsReadAsync(long adminId, NotificationReadRequestDto request)
|
||||
{
|
||||
var notification = await _notificationRepository.GetByIdAndAdminAsync(request.NotificationId, adminId);
|
||||
if (notification == null)
|
||||
throw new SpmsException(ErrorCodes.NotFound, "알림을 찾을 수 없습니다.", 404);
|
||||
|
||||
// 멱등성: 이미 읽은 알림은 스킵
|
||||
if (!notification.IsRead)
|
||||
{
|
||||
notification.IsRead = true;
|
||||
notification.ReadAt = DateTime.UtcNow;
|
||||
_notificationRepository.Update(notification);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var unreadCount = await _notificationRepository.GetUnreadCountAsync(adminId);
|
||||
return new NotificationReadResponseDto { UnreadCount = unreadCount };
|
||||
}
|
||||
|
||||
public async Task<NotificationReadResponseDto> MarkAllAsReadAsync(long adminId)
|
||||
{
|
||||
await _notificationRepository.MarkAllAsReadAsync(adminId);
|
||||
return new NotificationReadResponseDto { UnreadCount = 0 };
|
||||
}
|
||||
|
||||
private static NotificationItemDto MapToItemDto(Domain.Entities.Notification n) => new()
|
||||
{
|
||||
NotificationId = n.Id,
|
||||
Category = n.Category.ToString().ToLower(),
|
||||
Title = n.Title,
|
||||
Content = n.Content,
|
||||
LinkUrl = n.LinkUrl,
|
||||
IsRead = n.IsRead,
|
||||
ReadAt = n.ReadAt,
|
||||
CreatedAt = n.CreatedAt
|
||||
};
|
||||
}
|
||||
18
SPMS.Domain/Entities/Notification.cs
Normal file
18
SPMS.Domain/Entities/Notification.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
using SPMS.Domain.Enums;
|
||||
|
||||
namespace SPMS.Domain.Entities;
|
||||
|
||||
public class Notification : BaseEntity
|
||||
{
|
||||
public long TargetAdminId { get; set; }
|
||||
public NotificationCategory Category { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public string? LinkUrl { get; set; }
|
||||
public bool IsRead { get; set; }
|
||||
public DateTime? ReadAt { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
// Navigation
|
||||
public Admin TargetAdmin { get; set; } = null!;
|
||||
}
|
||||
10
SPMS.Domain/Enums/NotificationCategory.cs
Normal file
10
SPMS.Domain/Enums/NotificationCategory.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
namespace SPMS.Domain.Enums;
|
||||
|
||||
public enum NotificationCategory : byte
|
||||
{
|
||||
Send = 0, // 발송
|
||||
Certificate = 1, // 인증서
|
||||
Service = 2, // 서비스
|
||||
Failure = 3, // 실패
|
||||
System = 4 // 시스템
|
||||
}
|
||||
15
SPMS.Domain/Interfaces/INotificationRepository.cs
Normal file
15
SPMS.Domain/Interfaces/INotificationRepository.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
using SPMS.Domain.Entities;
|
||||
using SPMS.Domain.Enums;
|
||||
|
||||
namespace SPMS.Domain.Interfaces;
|
||||
|
||||
public interface INotificationRepository : IRepository<Notification>
|
||||
{
|
||||
Task<(IReadOnlyList<Notification> Items, int TotalCount)> GetPagedAsync(
|
||||
long adminId, NotificationCategory? category, DateTime? from, DateTime? to,
|
||||
bool? isRead, int page, int size);
|
||||
Task<int> GetUnreadCountAsync(long adminId);
|
||||
Task<IReadOnlyList<Notification>> GetRecentAsync(long adminId, int limit);
|
||||
Task<Notification?> GetByIdAndAdminAsync(long id, long adminId);
|
||||
Task<int> MarkAllAsReadAsync(long adminId);
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ public class AppDbContext : DbContext
|
|||
public DbSet<Faq> Faqs => Set<Faq>();
|
||||
public DbSet<AppConfig> AppConfigs => Set<AppConfig>();
|
||||
public DbSet<Tag> Tags => Set<Tag>();
|
||||
public DbSet<Notification> Notifications => Set<Notification>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ public static class DependencyInjection
|
|||
services.AddScoped<IDailyStatRepository, DailyStatRepository>();
|
||||
services.AddScoped<IWebhookLogRepository, WebhookLogRepository>();
|
||||
services.AddScoped<ITagRepository, TagRepository>();
|
||||
services.AddScoped<INotificationRepository, NotificationRepository>();
|
||||
|
||||
// External Services
|
||||
services.AddScoped<IJwtService, JwtService>();
|
||||
|
|
|
|||
1249
SPMS.Infrastructure/Migrations/20260226003916_AddNotificationTable.Designer.cs
generated
Normal file
1249
SPMS.Infrastructure/Migrations/20260226003916_AddNotificationTable.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,63 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace SPMS.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddNotificationTable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Notification",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
TargetAdminId = table.Column<long>(type: "bigint", nullable: false),
|
||||
Category = table.Column<byte>(type: "tinyint unsigned", nullable: false),
|
||||
Title = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Content = table.Column<string>(type: "varchar(1000)", maxLength: 1000, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
LinkUrl = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
IsRead = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: false),
|
||||
ReadAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Notification", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Notification_Admin_TargetAdminId",
|
||||
column: x => x.TargetAdminId,
|
||||
principalTable: "Admin",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Notification_TargetAdminId_CreatedAt",
|
||||
table: "Notification",
|
||||
columns: new[] { "TargetAdminId", "CreatedAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Notification_TargetAdminId_IsRead",
|
||||
table: "Notification",
|
||||
columns: new[] { "TargetAdminId", "IsRead" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Notification");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -536,6 +536,54 @@ namespace SPMS.Infrastructure.Migrations
|
|||
b.ToTable("Notice", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SPMS.Domain.Entities.Notification", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<byte>("Category")
|
||||
.HasColumnType("tinyint unsigned");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("varchar(1000)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<bool>("IsRead")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("tinyint(1)")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("LinkUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("varchar(500)");
|
||||
|
||||
b.Property<DateTime?>("ReadAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<long>("TargetAdminId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TargetAdminId", "CreatedAt");
|
||||
|
||||
b.HasIndex("TargetAdminId", "IsRead");
|
||||
|
||||
b.ToTable("Notification", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SPMS.Domain.Entities.Payment", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
|
|
@ -1029,6 +1077,17 @@ namespace SPMS.Infrastructure.Migrations
|
|||
b.Navigation("Service");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SPMS.Domain.Entities.Notification", b =>
|
||||
{
|
||||
b.HasOne("SPMS.Domain.Entities.Admin", "TargetAdmin")
|
||||
.WithMany()
|
||||
.HasForeignKey("TargetAdminId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("TargetAdmin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SPMS.Domain.Entities.Payment", b =>
|
||||
{
|
||||
b.HasOne("SPMS.Domain.Entities.Admin", "Admin")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using SPMS.Domain.Entities;
|
||||
|
||||
namespace SPMS.Infrastructure.Persistence.Configurations;
|
||||
|
||||
public class NotificationConfiguration : IEntityTypeConfiguration<Notification>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Notification> builder)
|
||||
{
|
||||
builder.ToTable("Notification");
|
||||
|
||||
builder.HasKey(e => e.Id);
|
||||
builder.Property(e => e.Id).ValueGeneratedOnAdd();
|
||||
|
||||
builder.Property(e => e.Title).HasMaxLength(200).IsRequired();
|
||||
builder.Property(e => e.Content).HasMaxLength(1000).IsRequired();
|
||||
builder.Property(e => e.LinkUrl).HasMaxLength(500);
|
||||
builder.Property(e => e.IsRead).IsRequired().HasDefaultValue(false);
|
||||
builder.Property(e => e.CreatedAt).IsRequired();
|
||||
|
||||
builder.HasOne(e => e.TargetAdmin)
|
||||
.WithMany()
|
||||
.HasForeignKey(e => e.TargetAdminId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasIndex(e => new { e.TargetAdminId, e.CreatedAt });
|
||||
builder.HasIndex(e => new { e.TargetAdminId, e.IsRead });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using SPMS.Domain.Entities;
|
||||
using SPMS.Domain.Enums;
|
||||
using SPMS.Domain.Interfaces;
|
||||
|
||||
namespace SPMS.Infrastructure.Persistence.Repositories;
|
||||
|
||||
public class NotificationRepository : Repository<Notification>, INotificationRepository
|
||||
{
|
||||
public NotificationRepository(AppDbContext context) : base(context) { }
|
||||
|
||||
public async Task<(IReadOnlyList<Notification> Items, int TotalCount)> GetPagedAsync(
|
||||
long adminId, NotificationCategory? category, DateTime? from, DateTime? to,
|
||||
bool? isRead, int page, int size)
|
||||
{
|
||||
IQueryable<Notification> query = _dbSet.Where(n => n.TargetAdminId == adminId);
|
||||
|
||||
if (category.HasValue)
|
||||
query = query.Where(n => n.Category == category.Value);
|
||||
|
||||
if (from.HasValue)
|
||||
query = query.Where(n => n.CreatedAt >= from.Value);
|
||||
|
||||
if (to.HasValue)
|
||||
query = query.Where(n => n.CreatedAt <= to.Value);
|
||||
|
||||
if (isRead.HasValue)
|
||||
query = query.Where(n => n.IsRead == isRead.Value);
|
||||
|
||||
var totalCount = await query.CountAsync();
|
||||
|
||||
var items = await query
|
||||
.OrderByDescending(n => n.CreatedAt)
|
||||
.Skip((page - 1) * size)
|
||||
.Take(size)
|
||||
.ToListAsync();
|
||||
|
||||
return (items, totalCount);
|
||||
}
|
||||
|
||||
public async Task<int> GetUnreadCountAsync(long adminId)
|
||||
=> await _dbSet.CountAsync(n => n.TargetAdminId == adminId && !n.IsRead);
|
||||
|
||||
public async Task<IReadOnlyList<Notification>> GetRecentAsync(long adminId, int limit)
|
||||
=> await _dbSet
|
||||
.Where(n => n.TargetAdminId == adminId)
|
||||
.OrderByDescending(n => n.CreatedAt)
|
||||
.Take(limit)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<Notification?> GetByIdAndAdminAsync(long id, long adminId)
|
||||
=> await _dbSet.FirstOrDefaultAsync(n => n.Id == id && n.TargetAdminId == adminId);
|
||||
|
||||
public async Task<int> MarkAllAsReadAsync(long adminId)
|
||||
=> await _dbSet
|
||||
.Where(n => n.TargetAdminId == adminId && !n.IsRead)
|
||||
.ExecuteUpdateAsync(s => s
|
||||
.SetProperty(n => n.IsRead, true)
|
||||
.SetProperty(n => n.ReadAt, DateTime.UtcNow));
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user