From 44f6defa846d38383ae544df34364ac8fae6168c Mon Sep 17 00:00:00 2001 From: SEAN Date: Tue, 3 Mar 2026 12:55:53 +0900 Subject: [PATCH] =?UTF-8?q?improvement:=20Device=20External=20ID=20(UUID)?= =?UTF-8?q?=20=EB=8F=84=EC=9E=85=20(#275)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DTOs/Device/DeviceAgreeRequestDto.cs | 2 +- .../DTOs/Device/DeviceDeleteRequestDto.cs | 2 +- .../DTOs/Device/DeviceInfoRequestDto.cs | 2 +- .../DTOs/Device/DeviceInfoResponseDto.cs | 2 +- .../DTOs/Device/DeviceListResponseDto.cs | 2 +- .../DTOs/Device/DeviceRegisterRequestDto.cs | 4 + .../DTOs/Device/DeviceRegisterResponseDto.cs | 2 +- .../DTOs/Device/DeviceTagsRequestDto.cs | 2 +- .../DTOs/Device/DeviceUpdateRequestDto.cs | 2 +- .../DTOs/Push/PushLogResponseDto.cs | 2 +- .../DTOs/Push/PushScheduleRequestDto.cs | 2 +- .../DTOs/Push/PushSendRequestDto.cs | 2 +- SPMS.Application/Interfaces/IDeviceService.cs | 2 +- .../Interfaces/ITokenCacheService.cs | 6 +- SPMS.Application/Services/DeviceService.cs | 35 +- SPMS.Application/Services/PushService.cs | 15 +- SPMS.Domain/Entities/Device.cs | 1 + SPMS.Domain/Interfaces/IDeviceRepository.cs | 1 + .../Caching/TokenCacheService.cs | 8 +- ...0303023033_AddExternalDeviceId.Designer.cs | 1269 +++++++++++++++++ .../20260303023033_AddExternalDeviceId.cs | 41 + .../Migrations/AppDbContextModelSnapshot.cs | 8 + .../Configurations/DeviceConfiguration.cs | 2 + .../Repositories/DeviceRepository.cs | 5 + .../Repositories/PushSendLogRepository.cs | 1 + .../Workers/DeadTokenCleanupWorker.cs | 4 +- SPMS.Infrastructure/Workers/PushWorker.cs | 22 +- 27 files changed, 1391 insertions(+), 55 deletions(-) create mode 100644 SPMS.Infrastructure/Migrations/20260303023033_AddExternalDeviceId.Designer.cs create mode 100644 SPMS.Infrastructure/Migrations/20260303023033_AddExternalDeviceId.cs diff --git a/SPMS.Application/DTOs/Device/DeviceAgreeRequestDto.cs b/SPMS.Application/DTOs/Device/DeviceAgreeRequestDto.cs index 6d783cd..96449b4 100644 --- a/SPMS.Application/DTOs/Device/DeviceAgreeRequestDto.cs +++ b/SPMS.Application/DTOs/Device/DeviceAgreeRequestDto.cs @@ -7,7 +7,7 @@ public class DeviceAgreeRequestDto { [Required] [JsonPropertyName("device_id")] - public long DeviceId { get; set; } + public string DeviceId { get; set; } = string.Empty; [Required] [JsonPropertyName("push_agreed")] diff --git a/SPMS.Application/DTOs/Device/DeviceDeleteRequestDto.cs b/SPMS.Application/DTOs/Device/DeviceDeleteRequestDto.cs index 7a61a9b..055bf6f 100644 --- a/SPMS.Application/DTOs/Device/DeviceDeleteRequestDto.cs +++ b/SPMS.Application/DTOs/Device/DeviceDeleteRequestDto.cs @@ -7,5 +7,5 @@ public class DeviceDeleteRequestDto { [Required] [JsonPropertyName("device_id")] - public long DeviceId { get; set; } + public string DeviceId { get; set; } = string.Empty; } diff --git a/SPMS.Application/DTOs/Device/DeviceInfoRequestDto.cs b/SPMS.Application/DTOs/Device/DeviceInfoRequestDto.cs index 93ea812..f60750e 100644 --- a/SPMS.Application/DTOs/Device/DeviceInfoRequestDto.cs +++ b/SPMS.Application/DTOs/Device/DeviceInfoRequestDto.cs @@ -7,5 +7,5 @@ public class DeviceInfoRequestDto { [Required] [JsonPropertyName("device_id")] - public long DeviceId { get; set; } + public string DeviceId { get; set; } = string.Empty; } diff --git a/SPMS.Application/DTOs/Device/DeviceInfoResponseDto.cs b/SPMS.Application/DTOs/Device/DeviceInfoResponseDto.cs index bcaa22c..d355380 100644 --- a/SPMS.Application/DTOs/Device/DeviceInfoResponseDto.cs +++ b/SPMS.Application/DTOs/Device/DeviceInfoResponseDto.cs @@ -5,7 +5,7 @@ namespace SPMS.Application.DTOs.Device; public class DeviceInfoResponseDto { [JsonPropertyName("device_id")] - public long DeviceId { get; set; } + public string DeviceId { get; set; } = string.Empty; [JsonPropertyName("device_token")] public string DeviceToken { get; set; } = string.Empty; diff --git a/SPMS.Application/DTOs/Device/DeviceListResponseDto.cs b/SPMS.Application/DTOs/Device/DeviceListResponseDto.cs index 2ca171e..35ef3d4 100644 --- a/SPMS.Application/DTOs/Device/DeviceListResponseDto.cs +++ b/SPMS.Application/DTOs/Device/DeviceListResponseDto.cs @@ -15,7 +15,7 @@ public class DeviceListResponseDto public class DeviceSummaryDto { [JsonPropertyName("device_id")] - public long DeviceId { get; set; } + public string DeviceId { get; set; } = string.Empty; [JsonPropertyName("platform")] public string Platform { get; set; } = string.Empty; diff --git a/SPMS.Application/DTOs/Device/DeviceRegisterRequestDto.cs b/SPMS.Application/DTOs/Device/DeviceRegisterRequestDto.cs index 7849220..a41c080 100644 --- a/SPMS.Application/DTOs/Device/DeviceRegisterRequestDto.cs +++ b/SPMS.Application/DTOs/Device/DeviceRegisterRequestDto.cs @@ -5,6 +5,10 @@ namespace SPMS.Application.DTOs.Device; public class DeviceRegisterRequestDto { + [Required] + [JsonPropertyName("device_id")] + public string DeviceId { get; set; } = string.Empty; + [Required] [JsonPropertyName("device_token")] public string DeviceToken { get; set; } = string.Empty; diff --git a/SPMS.Application/DTOs/Device/DeviceRegisterResponseDto.cs b/SPMS.Application/DTOs/Device/DeviceRegisterResponseDto.cs index b686208..d92e287 100644 --- a/SPMS.Application/DTOs/Device/DeviceRegisterResponseDto.cs +++ b/SPMS.Application/DTOs/Device/DeviceRegisterResponseDto.cs @@ -5,7 +5,7 @@ namespace SPMS.Application.DTOs.Device; public class DeviceRegisterResponseDto { [JsonPropertyName("device_id")] - public long DeviceId { get; set; } + public string DeviceId { get; set; } = string.Empty; [JsonPropertyName("is_new")] public bool IsNew { get; set; } diff --git a/SPMS.Application/DTOs/Device/DeviceTagsRequestDto.cs b/SPMS.Application/DTOs/Device/DeviceTagsRequestDto.cs index ade810e..5893277 100644 --- a/SPMS.Application/DTOs/Device/DeviceTagsRequestDto.cs +++ b/SPMS.Application/DTOs/Device/DeviceTagsRequestDto.cs @@ -7,7 +7,7 @@ public class DeviceTagsRequestDto { [Required] [JsonPropertyName("device_id")] - public long DeviceId { get; set; } + public string DeviceId { get; set; } = string.Empty; [Required] [JsonPropertyName("tags")] diff --git a/SPMS.Application/DTOs/Device/DeviceUpdateRequestDto.cs b/SPMS.Application/DTOs/Device/DeviceUpdateRequestDto.cs index d0251db..079f048 100644 --- a/SPMS.Application/DTOs/Device/DeviceUpdateRequestDto.cs +++ b/SPMS.Application/DTOs/Device/DeviceUpdateRequestDto.cs @@ -7,7 +7,7 @@ public class DeviceUpdateRequestDto { [Required] [JsonPropertyName("device_id")] - public long DeviceId { get; set; } + public string DeviceId { get; set; } = string.Empty; [JsonPropertyName("device_token")] public string? DeviceToken { get; set; } diff --git a/SPMS.Application/DTOs/Push/PushLogResponseDto.cs b/SPMS.Application/DTOs/Push/PushLogResponseDto.cs index f271ee2..856170e 100644 --- a/SPMS.Application/DTOs/Push/PushLogResponseDto.cs +++ b/SPMS.Application/DTOs/Push/PushLogResponseDto.cs @@ -21,7 +21,7 @@ public class PushLogItemDto public string MessageCode { get; set; } = string.Empty; [JsonPropertyName("device_id")] - public long DeviceId { get; set; } + public string DeviceId { get; set; } = string.Empty; [JsonPropertyName("status")] public string Status { get; set; } = string.Empty; diff --git a/SPMS.Application/DTOs/Push/PushScheduleRequestDto.cs b/SPMS.Application/DTOs/Push/PushScheduleRequestDto.cs index 2d10873..8cd36b0 100644 --- a/SPMS.Application/DTOs/Push/PushScheduleRequestDto.cs +++ b/SPMS.Application/DTOs/Push/PushScheduleRequestDto.cs @@ -10,7 +10,7 @@ public class PushScheduleRequestDto [Required] public string SendType { get; set; } = string.Empty; - public long? DeviceId { get; set; } + public string? DeviceId { get; set; } public List? Tags { get; set; } diff --git a/SPMS.Application/DTOs/Push/PushSendRequestDto.cs b/SPMS.Application/DTOs/Push/PushSendRequestDto.cs index fd0bb36..16c7da1 100644 --- a/SPMS.Application/DTOs/Push/PushSendRequestDto.cs +++ b/SPMS.Application/DTOs/Push/PushSendRequestDto.cs @@ -8,7 +8,7 @@ public class PushSendRequestDto public string MessageCode { get; set; } = string.Empty; [Required] - public long DeviceId { get; set; } + public string DeviceId { get; set; } = string.Empty; public Dictionary? Variables { get; set; } } diff --git a/SPMS.Application/Interfaces/IDeviceService.cs b/SPMS.Application/Interfaces/IDeviceService.cs index a0973a7..fffdfaf 100644 --- a/SPMS.Application/Interfaces/IDeviceService.cs +++ b/SPMS.Application/Interfaces/IDeviceService.cs @@ -8,7 +8,7 @@ public interface IDeviceService Task GetInfoAsync(long serviceId, DeviceInfoRequestDto request); Task UpdateAsync(long serviceId, DeviceUpdateRequestDto request); Task DeleteAsync(long serviceId, DeviceDeleteRequestDto request); - Task AdminDeleteAsync(long deviceId); + Task AdminDeleteAsync(string externalDeviceId); Task GetListAsync(long? serviceId, DeviceListRequestDto request); Task ExportAsync(long? serviceId, DeviceExportRequestDto request); Task SetTagsAsync(long serviceId, DeviceTagsRequestDto request); diff --git a/SPMS.Application/Interfaces/ITokenCacheService.cs b/SPMS.Application/Interfaces/ITokenCacheService.cs index e803dd9..9d6d4a8 100644 --- a/SPMS.Application/Interfaces/ITokenCacheService.cs +++ b/SPMS.Application/Interfaces/ITokenCacheService.cs @@ -2,9 +2,9 @@ namespace SPMS.Application.Interfaces; public interface ITokenCacheService { - Task GetDeviceInfoAsync(long serviceId, long deviceId); - Task SetDeviceInfoAsync(long serviceId, long deviceId, CachedDeviceInfo info); - Task InvalidateAsync(long serviceId, long deviceId); + Task GetDeviceInfoAsync(long serviceId, string deviceId); + Task SetDeviceInfoAsync(long serviceId, string deviceId, CachedDeviceInfo info); + Task InvalidateAsync(long serviceId, string deviceId); Task InvalidateByServiceAsync(long serviceId); } diff --git a/SPMS.Application/Services/DeviceService.cs b/SPMS.Application/Services/DeviceService.cs index e4d6031..2a00a15 100644 --- a/SPMS.Application/Services/DeviceService.cs +++ b/SPMS.Application/Services/DeviceService.cs @@ -35,6 +35,7 @@ public class DeviceService : IDeviceService if (existing != null) { + existing.ExternalDeviceId = request.DeviceId; existing.Platform = platform; existing.OsVersion = request.OsVersion; existing.AppVersion = request.AppVersion; @@ -43,14 +44,15 @@ public class DeviceService : IDeviceService existing.UpdatedAt = DateTime.UtcNow; _deviceRepository.Update(existing); await _unitOfWork.SaveChangesAsync(); - await _tokenCache.InvalidateAsync(serviceId, existing.Id); + await _tokenCache.InvalidateAsync(serviceId, existing.ExternalDeviceId); - return new DeviceRegisterResponseDto { DeviceId = existing.Id, IsNew = false }; + return new DeviceRegisterResponseDto { DeviceId = existing.ExternalDeviceId, IsNew = false }; } var device = new Device { ServiceId = serviceId, + ExternalDeviceId = request.DeviceId, DeviceToken = request.DeviceToken, Platform = platform, OsVersion = request.OsVersion, @@ -64,18 +66,18 @@ public class DeviceService : IDeviceService await _deviceRepository.AddAsync(device); await _unitOfWork.SaveChangesAsync(); - return new DeviceRegisterResponseDto { DeviceId = device.Id, IsNew = true }; + return new DeviceRegisterResponseDto { DeviceId = device.ExternalDeviceId, IsNew = true }; } public async Task GetInfoAsync(long serviceId, DeviceInfoRequestDto request) { - var device = await _deviceRepository.GetByIdAndServiceAsync(request.DeviceId, serviceId); + var device = await _deviceRepository.GetByExternalIdAndServiceAsync(request.DeviceId, serviceId); if (device == null) throw new SpmsException(ErrorCodes.DeviceNotFound, "존재하지 않는 디바이스입니다.", 404); return new DeviceInfoResponseDto { - DeviceId = device.Id, + DeviceId = device.ExternalDeviceId, DeviceToken = device.DeviceToken, Platform = device.Platform.ToString().ToLowerInvariant(), OsVersion = device.OsVersion, @@ -92,7 +94,7 @@ public class DeviceService : IDeviceService public async Task UpdateAsync(long serviceId, DeviceUpdateRequestDto request) { - var device = await _deviceRepository.GetByIdAndServiceAsync(request.DeviceId, serviceId); + var device = await _deviceRepository.GetByExternalIdAndServiceAsync(request.DeviceId, serviceId); if (device == null) throw new SpmsException(ErrorCodes.DeviceNotFound, "존재하지 않는 디바이스입니다.", 404); @@ -106,12 +108,12 @@ public class DeviceService : IDeviceService device.UpdatedAt = DateTime.UtcNow; _deviceRepository.Update(device); await _unitOfWork.SaveChangesAsync(); - await _tokenCache.InvalidateAsync(serviceId, device.Id); + await _tokenCache.InvalidateAsync(serviceId, device.ExternalDeviceId); } public async Task DeleteAsync(long serviceId, DeviceDeleteRequestDto request) { - var device = await _deviceRepository.GetByIdAndServiceAsync(request.DeviceId, serviceId); + var device = await _deviceRepository.GetByExternalIdAndServiceAsync(request.DeviceId, serviceId); if (device == null) throw new SpmsException(ErrorCodes.DeviceNotFound, "존재하지 않는 디바이스입니다.", 404); @@ -119,12 +121,13 @@ public class DeviceService : IDeviceService device.UpdatedAt = DateTime.UtcNow; _deviceRepository.Update(device); await _unitOfWork.SaveChangesAsync(); - await _tokenCache.InvalidateAsync(serviceId, device.Id); + await _tokenCache.InvalidateAsync(serviceId, device.ExternalDeviceId); } - public async Task AdminDeleteAsync(long deviceId) + public async Task AdminDeleteAsync(string externalDeviceId) { - var device = await _deviceRepository.GetByIdAsync(deviceId); + var results = await _deviceRepository.FindAsync(d => d.ExternalDeviceId == externalDeviceId); + var device = results.FirstOrDefault(); if (device == null) throw new SpmsException(ErrorCodes.DeviceNotFound, "존재하지 않는 디바이스입니다.", 404); @@ -132,7 +135,7 @@ public class DeviceService : IDeviceService device.UpdatedAt = DateTime.UtcNow; _deviceRepository.Update(device); await _unitOfWork.SaveChangesAsync(); - await _tokenCache.InvalidateAsync(device.ServiceId, device.Id); + await _tokenCache.InvalidateAsync(device.ServiceId, device.ExternalDeviceId); } public async Task GetListAsync(long? serviceId, DeviceListRequestDto request) @@ -169,7 +172,7 @@ public class DeviceService : IDeviceService { Items = items.Select(d => new DeviceSummaryDto { - DeviceId = d.Id, + DeviceId = d.ExternalDeviceId, Platform = d.Platform.ToString().ToLowerInvariant(), Model = d.DeviceModel, PushAgreed = d.PushAgreed, @@ -259,7 +262,7 @@ public class DeviceService : IDeviceService public async Task SetTagsAsync(long serviceId, DeviceTagsRequestDto request) { - var device = await _deviceRepository.GetByIdAndServiceAsync(request.DeviceId, serviceId); + var device = await _deviceRepository.GetByExternalIdAndServiceAsync(request.DeviceId, serviceId); if (device == null) throw new SpmsException(ErrorCodes.DeviceNotFound, "존재하지 않는 디바이스입니다.", 404); @@ -275,7 +278,7 @@ public class DeviceService : IDeviceService public async Task SetAgreeAsync(long serviceId, DeviceAgreeRequestDto request) { - var device = await _deviceRepository.GetByIdAndServiceAsync(request.DeviceId, serviceId); + var device = await _deviceRepository.GetByExternalIdAndServiceAsync(request.DeviceId, serviceId); if (device == null) throw new SpmsException(ErrorCodes.DeviceNotFound, "존재하지 않는 디바이스입니다.", 404); @@ -286,7 +289,7 @@ public class DeviceService : IDeviceService device.MktAgreeUpdatedAt = DateTime.UtcNow; _deviceRepository.Update(device); await _unitOfWork.SaveChangesAsync(); - await _tokenCache.InvalidateAsync(serviceId, device.Id); + await _tokenCache.InvalidateAsync(serviceId, device.ExternalDeviceId); } private static Platform ParsePlatform(string platform) diff --git a/SPMS.Application/Services/PushService.cs b/SPMS.Application/Services/PushService.cs index 20e69be..80c5594 100644 --- a/SPMS.Application/Services/PushService.cs +++ b/SPMS.Application/Services/PushService.cs @@ -58,7 +58,7 @@ public class PushService : IPushService Target = new PushTargetDto { Type = "device_ids", - Value = JsonSerializer.SerializeToElement(new[] { request.DeviceId }) + Value = JsonSerializer.SerializeToElement(new[] { request.DeviceId }) // UUID string }, CreatedBy = message.CreatedBy, CreatedAt = DateTime.UtcNow.ToString("o") @@ -126,7 +126,7 @@ public class PushService : IPushService if (sendType != "single" && sendType != "tag") throw new SpmsException(ErrorCodes.BadRequest, "send_type은 single 또는 tag만 허용됩니다.", 400); - if (sendType == "single" && request.DeviceId == null) + if (sendType == "single" && string.IsNullOrWhiteSpace(request.DeviceId)) throw new SpmsException(ErrorCodes.BadRequest, "send_type=single 시 device_id는 필수입니다.", 400); if (sendType == "tag" && (request.Tags == null || request.Tags.Count == 0)) @@ -144,7 +144,7 @@ public class PushService : IPushService target = new PushTargetDto { Type = "device_ids", - Value = JsonSerializer.SerializeToElement(new[] { request.DeviceId!.Value }) + Value = JsonSerializer.SerializeToElement(new[] { request.DeviceId! }) // UUID string }; } else @@ -252,7 +252,7 @@ public class PushService : IPushService { SendId = l.Id, MessageCode = l.Message?.MessageCode ?? string.Empty, - DeviceId = l.DeviceId, + DeviceId = l.Device?.ExternalDeviceId ?? l.DeviceId.ToString(), Status = l.Status.ToString().ToLowerInvariant(), FailReason = l.FailReason, SentAt = l.SentAt @@ -301,7 +301,7 @@ public class PushService : IPushService Target = new PushTargetDto { Type = "device_ids", - Value = JsonSerializer.SerializeToElement(new[] { row.DeviceId }) + Value = JsonSerializer.SerializeToElement(new[] { row.DeviceId }) // UUID string }, CreatedBy = message.CreatedBy, CreatedAt = DateTime.UtcNow.ToString("o"), @@ -450,7 +450,8 @@ public class PushService : IPushService if (values.Length <= deviceIdIndex) continue; - if (!long.TryParse(values[deviceIdIndex], out var deviceId)) + var deviceId = values[deviceIdIndex]; + if (string.IsNullOrWhiteSpace(deviceId)) continue; var variables = new Dictionary(); @@ -468,7 +469,7 @@ public class PushService : IPushService private class CsvRow { - public long DeviceId { get; init; } + public string DeviceId { get; init; } = string.Empty; public Dictionary Variables { get; init; } = new(); } diff --git a/SPMS.Domain/Entities/Device.cs b/SPMS.Domain/Entities/Device.cs index eb8aa3c..eb3453d 100644 --- a/SPMS.Domain/Entities/Device.cs +++ b/SPMS.Domain/Entities/Device.cs @@ -5,6 +5,7 @@ namespace SPMS.Domain.Entities; public class Device : BaseEntity { public long ServiceId { get; set; } + public string ExternalDeviceId { get; set; } = string.Empty; public string DeviceToken { get; set; } = string.Empty; public Platform Platform { get; set; } public string? AppVersion { get; set; } diff --git a/SPMS.Domain/Interfaces/IDeviceRepository.cs b/SPMS.Domain/Interfaces/IDeviceRepository.cs index 6887fd4..5937129 100644 --- a/SPMS.Domain/Interfaces/IDeviceRepository.cs +++ b/SPMS.Domain/Interfaces/IDeviceRepository.cs @@ -7,6 +7,7 @@ public interface IDeviceRepository : IRepository { Task GetByServiceAndTokenAsync(long serviceId, string deviceToken); Task GetByIdAndServiceAsync(long id, long serviceId); + Task GetByExternalIdAndServiceAsync(string externalId, long serviceId); Task GetActiveCountByServiceAsync(long serviceId); Task> GetByPlatformAsync(long serviceId, Platform platform); Task<(IReadOnlyList Items, int TotalCount)> GetPagedAsync( diff --git a/SPMS.Infrastructure/Caching/TokenCacheService.cs b/SPMS.Infrastructure/Caching/TokenCacheService.cs index af2ce97..bc28564 100644 --- a/SPMS.Infrastructure/Caching/TokenCacheService.cs +++ b/SPMS.Infrastructure/Caching/TokenCacheService.cs @@ -25,7 +25,7 @@ public class TokenCacheService : ITokenCacheService _logger = logger; } - public async Task GetDeviceInfoAsync(long serviceId, long deviceId) + public async Task GetDeviceInfoAsync(long serviceId, string deviceId) { try { @@ -45,7 +45,7 @@ public class TokenCacheService : ITokenCacheService } } - public async Task SetDeviceInfoAsync(long serviceId, long deviceId, CachedDeviceInfo info) + public async Task SetDeviceInfoAsync(long serviceId, string deviceId, CachedDeviceInfo info) { try { @@ -60,7 +60,7 @@ public class TokenCacheService : ITokenCacheService } } - public async Task InvalidateAsync(long serviceId, long deviceId) + public async Task InvalidateAsync(long serviceId, string deviceId) { try { @@ -97,6 +97,6 @@ public class TokenCacheService : ITokenCacheService } } - private string BuildKey(long serviceId, long deviceId) => + private string BuildKey(long serviceId, string deviceId) => $"{_settings.InstanceName}device:token:{serviceId}:{deviceId}"; } diff --git a/SPMS.Infrastructure/Migrations/20260303023033_AddExternalDeviceId.Designer.cs b/SPMS.Infrastructure/Migrations/20260303023033_AddExternalDeviceId.Designer.cs new file mode 100644 index 0000000..fe32688 --- /dev/null +++ b/SPMS.Infrastructure/Migrations/20260303023033_AddExternalDeviceId.Designer.cs @@ -0,0 +1,1269 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SPMS.Infrastructure; + +#nullable disable + +namespace SPMS.Infrastructure.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260303023033_AddExternalDeviceId")] + partial class AddExternalDeviceId + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SPMS.Domain.Entities.Admin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdminCode") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("varchar(12)"); + + b.Property("AgreePrivacy") + .HasColumnType("tinyint(1)"); + + b.Property("AgreeTerms") + .HasColumnType("tinyint(1)"); + + b.Property("AgreedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DeletedAt") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("EmailVerified") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("EmailVerifiedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("LastLoginAt") + .HasColumnType("datetime(6)"); + + b.Property("MustChangePassword") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Organization") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("RefreshToken") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("RefreshTokenExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("Role") + .HasColumnType("tinyint"); + + b.Property("TempPasswordIssuedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AdminCode") + .IsUnique(); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Admin", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.AppConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ConfigKey") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ConfigValue") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ServiceId", "ConfigKey") + .IsUnique(); + + b.ToTable("AppConfig", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Banner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("LinkType") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("LinkUrl") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Position") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ServiceId"); + + b.ToTable("Banner", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.DailyStat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FailCnt") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("OpenCnt") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("SentCnt") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.Property("StatDate") + .HasColumnType("date"); + + b.Property("SuccessCnt") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("ServiceId", "StatDate") + .IsUnique(); + + b.ToTable("DailyStat", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AgreeUpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("AppVersion") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DeviceModel") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("DeviceToken") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("ExternalDeviceId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("varchar(36)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("MarketingAgreed") + .HasColumnType("tinyint(1)"); + + b.Property("MktAgreeUpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("OsVersion") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Platform") + .HasColumnType("tinyint"); + + b.Property("PushAgreed") + .HasColumnType("tinyint(1)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.Property("Tags") + .HasColumnType("json"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ServiceId", "DeviceToken"); + + b.HasIndex("ServiceId", "ExternalDeviceId") + .IsUnique(); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Faq", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Answer") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("Question") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ServiceId"); + + b.ToTable("Faq", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("DeletedAt") + .HasColumnType("datetime(6)"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("MimeType") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ServiceId"); + + b.ToTable("File", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Body") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("CustomData") + .HasColumnType("json"); + + b.Property("DeletedAt") + .HasColumnType("datetime(6)"); + + b.Property("ImageUrl") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("LinkType") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("LinkUrl") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("MessageCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("MessageCode") + .IsUnique(); + + b.HasIndex("ServiceId"); + + b.ToTable("Message", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Notice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("IsPinned") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ServiceId"); + + b.ToTable("Notice", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("tinyint unsigned"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsRead") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("LinkUrl") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReadAt") + .HasColumnType("datetime(6)"); + + b.Property("TargetAdminId") + .HasColumnType("bigint"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdminId") + .HasColumnType("bigint"); + + b.Property("Amount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("PaidAt") + .HasColumnType("datetime(6)"); + + b.Property("PaymentKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PaymentMethod") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("tinyint"); + + b.Property("TierAfter") + .HasColumnType("tinyint"); + + b.Property("TierBefore") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("AdminId"); + + b.HasIndex("ServiceId"); + + b.ToTable("Payment", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.PushOpenLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("DeviceId") + .HasColumnType("bigint"); + + b.Property("MessageId") + .HasColumnType("bigint"); + + b.Property("OpenedAt") + .HasColumnType("datetime(6)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("MessageId"); + + b.HasIndex("ServiceId", "OpenedAt"); + + b.ToTable("PushOpenLog", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.PushSendLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("DeviceId") + .HasColumnType("bigint"); + + b.Property("FailReason") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("MessageId") + .HasColumnType("bigint"); + + b.Property("SentAt") + .HasColumnType("datetime(6)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("MessageId"); + + b.HasIndex("ServiceId", "SentAt"); + + b.ToTable("PushSendLog", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("ApiKeyCreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ApnsAuthType") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("ApnsBundleId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ApnsCertExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("ApnsCertPassword") + .HasColumnType("text"); + + b.Property("ApnsCertificate") + .HasColumnType("text"); + + b.Property("ApnsKeyId") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("ApnsPrivateKey") + .HasColumnType("text"); + + b.Property("ApnsTeamId") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("DeletedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("FcmCredentials") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("ServiceCode") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("varchar(8)"); + + b.Property("ServiceName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Status") + .HasColumnType("tinyint"); + + b.Property("SubStartedAt") + .HasColumnType("datetime(6)"); + + b.Property("SubTier") + .HasColumnType("tinyint"); + + b.Property("Tags") + .HasColumnType("json"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WebhookEvents") + .HasColumnType("longtext"); + + b.Property("WebhookUrl") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ServiceCode") + .IsUnique(); + + b.ToTable("Service", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.ServiceIp", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("IpAddress") + .IsRequired() + .HasMaxLength(45) + .HasColumnType("varchar(45)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ServiceId"); + + b.ToTable("ServiceIp", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.SystemLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("AdminId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .HasColumnType("json"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("varchar(45)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.Property("TargetId") + .HasColumnType("bigint"); + + b.Property("TargetType") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AdminId"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("ServiceId"); + + b.ToTable("SystemLog", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.Property("TagCode") + .IsRequired() + .HasMaxLength(4) + .HasColumnType("varchar(4)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ServiceId", "Name") + .IsUnique(); + + b.HasIndex("ServiceId", "TagCode") + .IsUnique(); + + b.ToTable("Tag", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.WebhookLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("EventType") + .HasColumnType("tinyint"); + + b.Property("Payload") + .IsRequired() + .HasColumnType("json"); + + b.Property("ResponseBody") + .HasColumnType("text"); + + b.Property("ResponseCode") + .HasColumnType("int"); + + b.Property("SentAt") + .HasColumnType("datetime(6)"); + + b.Property("ServiceId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("tinyint"); + + b.Property("WebhookUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("ServiceId", "SentAt"); + + b.ToTable("WebhookLog", (string)null); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.AppConfig", b => + { + b.HasOne("SPMS.Domain.Entities.Service", "Service") + .WithMany() + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Banner", b => + { + b.HasOne("SPMS.Domain.Entities.Service", "Service") + .WithMany() + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.DailyStat", b => + { + b.HasOne("SPMS.Domain.Entities.Service", "Service") + .WithMany() + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Device", b => + { + b.HasOne("SPMS.Domain.Entities.Service", "Service") + .WithMany("Devices") + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Faq", b => + { + b.HasOne("SPMS.Domain.Entities.Service", "Service") + .WithMany() + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.FileEntity", b => + { + b.HasOne("SPMS.Domain.Entities.Admin", "CreatedByAdmin") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SPMS.Domain.Entities.Service", "Service") + .WithMany() + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByAdmin"); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Message", b => + { + b.HasOne("SPMS.Domain.Entities.Admin", "CreatedByAdmin") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SPMS.Domain.Entities.Service", "Service") + .WithMany("Messages") + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByAdmin"); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Notice", b => + { + b.HasOne("SPMS.Domain.Entities.Admin", "CreatedByAdmin") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SPMS.Domain.Entities.Service", "Service") + .WithMany() + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByAdmin"); + + 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") + .WithMany() + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SPMS.Domain.Entities.Service", "Service") + .WithMany() + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Admin"); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.PushOpenLog", b => + { + b.HasOne("SPMS.Domain.Entities.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SPMS.Domain.Entities.Message", "Message") + .WithMany() + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SPMS.Domain.Entities.Service", "Service") + .WithMany() + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Device"); + + b.Navigation("Message"); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.PushSendLog", b => + { + b.HasOne("SPMS.Domain.Entities.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SPMS.Domain.Entities.Message", "Message") + .WithMany() + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SPMS.Domain.Entities.Service", "Service") + .WithMany() + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Device"); + + b.Navigation("Message"); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Service", b => + { + b.HasOne("SPMS.Domain.Entities.Admin", "CreatedByAdmin") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByAdmin"); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.ServiceIp", b => + { + b.HasOne("SPMS.Domain.Entities.Service", "Service") + .WithMany("ServiceIps") + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.SystemLog", b => + { + b.HasOne("SPMS.Domain.Entities.Admin", "Admin") + .WithMany() + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SPMS.Domain.Entities.Service", "Service") + .WithMany() + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Admin"); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Tag", b => + { + b.HasOne("SPMS.Domain.Entities.Admin", "CreatedByAdmin") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SPMS.Domain.Entities.Service", "Service") + .WithMany("TagList") + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByAdmin"); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.WebhookLog", b => + { + b.HasOne("SPMS.Domain.Entities.Service", "Service") + .WithMany() + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("SPMS.Domain.Entities.Service", b => + { + b.Navigation("Devices"); + + b.Navigation("Messages"); + + b.Navigation("ServiceIps"); + + b.Navigation("TagList"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SPMS.Infrastructure/Migrations/20260303023033_AddExternalDeviceId.cs b/SPMS.Infrastructure/Migrations/20260303023033_AddExternalDeviceId.cs new file mode 100644 index 0000000..a1824d9 --- /dev/null +++ b/SPMS.Infrastructure/Migrations/20260303023033_AddExternalDeviceId.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SPMS.Infrastructure.Migrations +{ + /// + public partial class AddExternalDeviceId : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ExternalDeviceId", + table: "Device", + type: "varchar(36)", + maxLength: 36, + nullable: false, + defaultValue: "") + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_Device_ServiceId_ExternalDeviceId", + table: "Device", + columns: new[] { "ServiceId", "ExternalDeviceId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Device_ServiceId_ExternalDeviceId", + table: "Device"); + + migrationBuilder.DropColumn( + name: "ExternalDeviceId", + table: "Device"); + } + } +} diff --git a/SPMS.Infrastructure/Migrations/AppDbContextModelSnapshot.cs b/SPMS.Infrastructure/Migrations/AppDbContextModelSnapshot.cs index 9c62823..4dd5189 100644 --- a/SPMS.Infrastructure/Migrations/AppDbContextModelSnapshot.cs +++ b/SPMS.Infrastructure/Migrations/AppDbContextModelSnapshot.cs @@ -282,6 +282,11 @@ namespace SPMS.Infrastructure.Migrations .HasMaxLength(255) .HasColumnType("varchar(255)"); + b.Property("ExternalDeviceId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("varchar(36)"); + b.Property("IsActive") .ValueGeneratedOnAdd() .HasColumnType("tinyint(1)") @@ -316,6 +321,9 @@ namespace SPMS.Infrastructure.Migrations b.HasIndex("ServiceId", "DeviceToken"); + b.HasIndex("ServiceId", "ExternalDeviceId") + .IsUnique(); + b.ToTable("Device", (string)null); }); diff --git a/SPMS.Infrastructure/Persistence/Configurations/DeviceConfiguration.cs b/SPMS.Infrastructure/Persistence/Configurations/DeviceConfiguration.cs index 81b0d56..e4b1177 100644 --- a/SPMS.Infrastructure/Persistence/Configurations/DeviceConfiguration.cs +++ b/SPMS.Infrastructure/Persistence/Configurations/DeviceConfiguration.cs @@ -14,6 +14,7 @@ public class DeviceConfiguration : IEntityTypeConfiguration builder.Property(e => e.Id).ValueGeneratedOnAdd(); builder.Property(e => e.ServiceId).IsRequired(); + builder.Property(e => e.ExternalDeviceId).HasMaxLength(36).IsRequired(); builder.Property(e => e.DeviceToken).HasMaxLength(255).IsRequired(); builder.Property(e => e.Platform).HasColumnType("tinyint").IsRequired(); builder.Property(e => e.AppVersion).HasMaxLength(20); @@ -33,6 +34,7 @@ public class DeviceConfiguration : IEntityTypeConfiguration .HasForeignKey(e => e.ServiceId) .OnDelete(DeleteBehavior.Restrict); + builder.HasIndex(e => new { e.ServiceId, e.ExternalDeviceId }).IsUnique(); builder.HasIndex(e => new { e.ServiceId, e.DeviceToken }); } } diff --git a/SPMS.Infrastructure/Persistence/Repositories/DeviceRepository.cs b/SPMS.Infrastructure/Persistence/Repositories/DeviceRepository.cs index 6838a15..56fd450 100644 --- a/SPMS.Infrastructure/Persistence/Repositories/DeviceRepository.cs +++ b/SPMS.Infrastructure/Persistence/Repositories/DeviceRepository.cs @@ -19,6 +19,11 @@ public class DeviceRepository : Repository, IDeviceRepository return await _dbSet.FirstOrDefaultAsync(d => d.Id == id && d.ServiceId == serviceId); } + public async Task GetByExternalIdAndServiceAsync(string externalId, long serviceId) + { + return await _dbSet.FirstOrDefaultAsync(d => d.ExternalDeviceId == externalId && d.ServiceId == serviceId); + } + public async Task GetActiveCountByServiceAsync(long serviceId) { return await _dbSet.CountAsync(d => d.ServiceId == serviceId && d.IsActive); diff --git a/SPMS.Infrastructure/Persistence/Repositories/PushSendLogRepository.cs b/SPMS.Infrastructure/Persistence/Repositories/PushSendLogRepository.cs index d0a7632..7c4377f 100644 --- a/SPMS.Infrastructure/Persistence/Repositories/PushSendLogRepository.cs +++ b/SPMS.Infrastructure/Persistence/Repositories/PushSendLogRepository.cs @@ -17,6 +17,7 @@ public class PushSendLogRepository : Repository, IPushSendLogReposi { var query = _dbSet .Include(l => l.Message) + .Include(l => l.Device) .Where(l => l.ServiceId == serviceId); if (messageId.HasValue) diff --git a/SPMS.Infrastructure/Workers/DeadTokenCleanupWorker.cs b/SPMS.Infrastructure/Workers/DeadTokenCleanupWorker.cs index 3bc6f05..e541027 100644 --- a/SPMS.Infrastructure/Workers/DeadTokenCleanupWorker.cs +++ b/SPMS.Infrastructure/Workers/DeadTokenCleanupWorker.cs @@ -110,7 +110,7 @@ public class DeadTokenCleanupWorker : BackgroundService { var batch = await context.Set() .Where(d => !d.IsActive && d.UpdatedAt < cutoffUtc) - .Select(d => new { d.Id, d.ServiceId }) + .Select(d => new { d.Id, d.ServiceId, d.ExternalDeviceId }) .Take(BatchSize) .ToListAsync(stoppingToken); @@ -130,7 +130,7 @@ public class DeadTokenCleanupWorker : BackgroundService // 삭제된 디바이스의 Redis 캐시 무효화 foreach (var item in batch) - await _tokenCache.InvalidateAsync(item.ServiceId, item.Id); + await _tokenCache.InvalidateAsync(item.ServiceId, item.ExternalDeviceId); _logger.LogInformation("DeadTokenCleanupWorker 배치 삭제: {BatchCount}건 (누적: {TotalDeleted}건), 캐시 무효화 완료", deletedInBatch, totalDeleted); diff --git a/SPMS.Infrastructure/Workers/PushWorker.cs b/SPMS.Infrastructure/Workers/PushWorker.cs index 1083f67..c1090c9 100644 --- a/SPMS.Infrastructure/Workers/PushWorker.cs +++ b/SPMS.Infrastructure/Workers/PushWorker.cs @@ -310,21 +310,21 @@ public class PushWorker : BackgroundService { case "single": { - var deviceIds = ParseDeviceIds(message.Target); - if (deviceIds.Count == 0) return []; + var externalIds = ParseExternalDeviceIds(message.Target); + if (externalIds.Count == 0) return []; var devices = new List(); - foreach (var id in deviceIds) + foreach (var externalId in externalIds) { - // Redis 캐시 우선 조회 - var cached = await _tokenCache.GetDeviceInfoAsync(message.ServiceId, id); + // Redis 캐시 우선 조회 (ExternalDeviceId 기준) + var cached = await _tokenCache.GetDeviceInfoAsync(message.ServiceId, externalId); if (cached != null) { if (cached.IsActive && cached.PushAgreed) { devices.Add(new Device { - Id = id, + ExternalDeviceId = externalId, ServiceId = message.ServiceId, DeviceToken = cached.Token, Platform = (Platform)cached.Platform, @@ -336,10 +336,10 @@ public class PushWorker : BackgroundService } // 캐시 미스 → DB 조회 후 캐시 저장 - var device = await deviceRepo.GetByIdAndServiceAsync(id, message.ServiceId); + var device = await deviceRepo.GetByExternalIdAndServiceAsync(externalId, message.ServiceId); if (device != null) { - await _tokenCache.SetDeviceInfoAsync(message.ServiceId, id, + await _tokenCache.SetDeviceInfoAsync(message.ServiceId, externalId, new CachedDeviceInfo(device.DeviceToken, (int)device.Platform, device.IsActive, device.PushAgreed)); @@ -383,7 +383,7 @@ public class PushWorker : BackgroundService } } - private static List ParseDeviceIds(PushTargetDto target) + private static List ParseExternalDeviceIds(PushTargetDto target) { if (target.Value == null) return []; @@ -392,8 +392,8 @@ public class PushWorker : BackgroundService if (target.Value.Value.ValueKind == JsonValueKind.Array) { return target.Value.Value.EnumerateArray() - .Where(e => e.ValueKind == JsonValueKind.Number) - .Select(e => e.GetInt64()) + .Where(e => e.ValueKind == JsonValueKind.String) + .Select(e => e.GetString()!) .ToList(); } } -- 2.45.1