diff --git a/Expedience.Api/Controllers/ExpedienceController.cs b/Expedience.Api/Controllers/ExpedienceController.cs index 146de9c..083eb1a 100644 --- a/Expedience.Api/Controllers/ExpedienceController.cs +++ b/Expedience.Api/Controllers/ExpedienceController.cs @@ -2,6 +2,7 @@ using System.Text.Json; using Expedience.Api.Encryption; using Expedience.Infrastructure; +using Expedience.Infrastructure.Models; using MassTransit; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -110,5 +111,52 @@ namespace Expedience.Api.Controllers return StatusCode(500); } - } + + [HttpGet("TopX/{recordType}/{territoryId}/{limit}")] + public async Task GetTopXRecords(int recordType, int territoryId, int limit, CancellationToken cancellationToken) + { + try + { + using var scope = _serviceScopeFactory.CreateScope(); + var repository = scope.ServiceProvider.GetService(); + + var records = await repository.GetTopXRecords(recordType, territoryId, limit, cancellationToken); + var results = new List(); + foreach (var record in records) + { + var dutyCompletionResult = new DutyCompletionResult + { + Id = record.Id, + UserId = record.UserId, + TerritoryId = record.TerritoryId, + IsMinILevel = recordType == 3, + HasNpcMembers = recordType == 4, + StartTime = record.StartTime, + EndTime = record.EndTime, + Hours = record.Duration.Hours, + Minutes = record.Duration.Minutes, + Seconds = record.Duration.Seconds, + Milliseconds = record.Duration.Milliseconds, + GameVersion = record.GameVersion, + PluginVersion = record.PluginVersion, + Territory = record.Territory, + User = record.User, + DataCenter = record.DutyCompletionResult.DataCenter, + UploadedAt = record.UploadedAt, + Lang = record.DutyCompletionResult.Lang, + }; + + results.Add(dutyCompletionResult); + } + return Ok(results); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting Top X Records: {errorMessage}", ex.Message); + } + + return StatusCode(500); + } + } +} } diff --git a/Expedience.Infrastructure/ExpedienceContext.cs b/Expedience.Infrastructure/ExpedienceContext.cs index 28e962b..59efec6 100644 --- a/Expedience.Infrastructure/ExpedienceContext.cs +++ b/Expedience.Infrastructure/ExpedienceContext.cs @@ -19,6 +19,7 @@ namespace Expedience.Infrastructure public DbSet Users { get; set; } public DbSet DutyCompletionRecords { get; set; } public DbSet DeepDungeonRecords { get; set; } + public DbSet TopXRecords { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -76,6 +77,37 @@ namespace Expedience.Infrastructure .HasNoKey() .ToTable(t => t.ExcludeFromMigrations()); + modelBuilder.Entity(cfg => + { + cfg.HasNoKey(); + cfg.ToView("TopXCompletionResults"); // This is just a placeholder name + + // If you want to include navigation properties: + cfg.HasOne(e => e.Territory) + .WithMany() + .HasForeignKey(e => e.TerritoryId) + .OnDelete(DeleteBehavior.NoAction); + + cfg.HasOne(e => e.User) + .WithMany() + .HasForeignKey(e => e.UserId) + .OnDelete(DeleteBehavior.NoAction); + + // Configure the relationship with DutyMembers + cfg.HasMany(e => e.DutyMembers) + .WithOne() + .HasForeignKey("UploadId") // Assuming UploadId in DutyMember corresponds to Id in TopXCompletionResult + .OnDelete(DeleteBehavior.NoAction); + + // Configure the one-to-one relationship with DutyCompletionResult + cfg.HasOne(e => e.DutyCompletionResult) + .WithOne() + .HasForeignKey("Id") + .OnDelete(DeleteBehavior.NoAction); + }); + + base.OnModelCreating(modelBuilder); + base.OnModelCreating(modelBuilder); } } diff --git a/Expedience.Infrastructure/ExpedienceRepository.cs b/Expedience.Infrastructure/ExpedienceRepository.cs index 45bf645..312a78c 100644 --- a/Expedience.Infrastructure/ExpedienceRepository.cs +++ b/Expedience.Infrastructure/ExpedienceRepository.cs @@ -9,6 +9,7 @@ public interface IExpedienceRepository { ValueTask> GetDutyCompletionRecords(string expac, CancellationToken cancellationToken); ValueTask> GetDeepDungeonRecords(CancellationToken cancellationToken); + ValueTask> GetTopXRecords(int recordType, int territoryId, int limit, CancellationToken cancellationToken); } public class ExpedienceRepository : IExpedienceRepository @@ -43,4 +44,46 @@ public class ExpedienceRepository : IExpedienceRepository return records; } + + public async ValueTask> GetTopXRecords(int recordType, int territoryId, int limit, CancellationToken cancellationToken) + { + var cacheKey = $"xpd-topx-{recordType}-{territoryId}-{limit}"; + + var cacheSeconds = 600; + var records = await _memcachedClient.GetValueOrCreateAsync(cacheKey, cacheSeconds, + async () => + { + var topXResults = await _dbContext.TopXRecords.FromSqlInterpolated($"SELECT * FROM public.get_topxcompletionresults({recordType}, {territoryId}, {limit})") + .ToListAsync(cancellationToken); + + // Get the IDs we need for our related entities + var topXIds = topXResults.Select(r => r.Id).ToList(); + var userIds = topXResults.Select(r => r.UserId).Distinct().ToList(); + + // Then, fetch the related DutyMembers and DutyCompletionResults + var dutyMembers = await _dbContext.DutyMembers + .Where(dm => topXIds.Contains(dm.UploadId)) + .ToListAsync(); + + var dutyCompletionResults = await _dbContext.DutyCompletionResults + .Where(dcr => topXIds.Contains(dcr.Id)) + .ToListAsync(); + + var users = await _dbContext.Users + .Where(u => userIds.Contains(u.UserId)) + .ToListAsync(); + + // Now, associate the DutyMembers and DutyCompletionResults with their corresponding TopXCompletionResults + foreach (var result in topXResults) + { + result.DutyMembers = dutyMembers.Where(dm => dm.UploadId == result.Id).ToList(); + result.DutyCompletionResult = dutyCompletionResults.First(dcr => dcr.Id == result.Id); + result.User = users.First(u => u.UserId == result.UserId); + } + + return topXResults; + }); + + return records; + } } diff --git a/Expedience.Infrastructure/Models/TopXCompletionResult.cs b/Expedience.Infrastructure/Models/TopXCompletionResult.cs new file mode 100644 index 0000000..bb81c34 --- /dev/null +++ b/Expedience.Infrastructure/Models/TopXCompletionResult.cs @@ -0,0 +1,31 @@ +using Expedience.Models; +using System; + +namespace Expedience.Infrastructure.Models; + +public class TopXCompletionResult +{ + public int Position { get; set; } + public Guid Id { get; set; } + public int TerritoryId { get; set; } + public string PlaceName { get; set; } + public string ContentName { get; set; } + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + public TimeSpan Duration { get; set; } + public int Level { get; set; } + public string ContentType { get; set; } + public string Expac { get; set; } + public DateTime UploadedAt { get; set; } + public int UserId { get; set; } + public string Username { get; set; } + public int WorldId { get; set; } + public string Datacenter { get; set; } + public string PluginVersion { get; set; } + public string GameVersion { get; set; } + + public Territory Territory { get; set; } = null!; + public User User { get; set; } = null!; + public ICollection DutyMembers { get; set; } = null!; + public DutyCompletionResult DutyCompletionResult { get; set; } = null!; +} \ No newline at end of file