import_viewer: round-trip ViewerMission + ViewerEventCounter
This commit is contained in:
@@ -10,6 +10,7 @@ using SVSim.EmulatedEntrypoint.Extensions;
|
|||||||
using SVSim.EmulatedEntrypoint.Infrastructure;
|
using SVSim.EmulatedEntrypoint.Infrastructure;
|
||||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Admin;
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Admin;
|
||||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.Admin;
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.Admin;
|
||||||
|
using SVSim.EmulatedEntrypoint.Services;
|
||||||
|
|
||||||
namespace SVSim.EmulatedEntrypoint.Controllers;
|
namespace SVSim.EmulatedEntrypoint.Controllers;
|
||||||
|
|
||||||
@@ -79,6 +80,7 @@ public class AdminController : SVSimController
|
|||||||
.Include(v => v.Info).ThenInclude(i => i.SelectedDegree)
|
.Include(v => v.Info).ThenInclude(i => i.SelectedDegree)
|
||||||
.Include(v => v.Currency)
|
.Include(v => v.Currency)
|
||||||
.Include(v => v.MissionData)
|
.Include(v => v.MissionData)
|
||||||
|
.Include(v => v.Missions)
|
||||||
.Include(v => v.Classes).ThenInclude(c => c.Class)
|
.Include(v => v.Classes).ThenInclude(c => c.Class)
|
||||||
.Include(v => v.Sleeves)
|
.Include(v => v.Sleeves)
|
||||||
.Include(v => v.Emblems)
|
.Include(v => v.Emblems)
|
||||||
@@ -265,6 +267,60 @@ public class AdminController : SVSimController
|
|||||||
viewer.MissionData.MissionChangeTime = DateTimeOffset.FromUnixTimeSeconds(mct).UtcDateTime;
|
viewer.MissionData.MissionChangeTime = DateTimeOffset.FromUnixTimeSeconds(mct).UtcDateTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pass B: Missions + ViewerEventCounter
|
||||||
|
var skippedMissionIds = new HashSet<int>();
|
||||||
|
var skippedMissionCounterIds = new HashSet<int>();
|
||||||
|
if (request.Missions is { } missions)
|
||||||
|
{
|
||||||
|
viewer.Missions.Clear();
|
||||||
|
var missionIds = missions.Select(m => m.MissionId).Distinct().ToList();
|
||||||
|
var catalogs = await _dbContext.MissionCatalog
|
||||||
|
.Where(c => missionIds.Contains(c.Id))
|
||||||
|
.ToDictionaryAsync(c => c.Id);
|
||||||
|
var nowUtc = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
foreach (var m in missions)
|
||||||
|
{
|
||||||
|
if (!catalogs.TryGetValue(m.MissionId, out var cat))
|
||||||
|
{
|
||||||
|
skippedMissionIds.Add(m.MissionId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
viewer.Missions.Add(new ViewerMission
|
||||||
|
{
|
||||||
|
MissionCatalogId = cat.Id,
|
||||||
|
Slot = cat.LotType == 6 ? 0 : 1,
|
||||||
|
MissionStatus = m.MissionStatus,
|
||||||
|
AssignedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||||
|
ClaimedAt = null
|
||||||
|
});
|
||||||
|
|
||||||
|
var resolved = ResolveMissionCounter(cat, nowUtc);
|
||||||
|
if (resolved is null)
|
||||||
|
{
|
||||||
|
skippedMissionCounterIds.Add(m.MissionId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var (eventKey, period) = resolved.Value;
|
||||||
|
var existingCounter = await _dbContext.ViewerEventCounters
|
||||||
|
.FirstOrDefaultAsync(c => c.ViewerId == viewer.Id && c.EventKey == eventKey && c.Period == period);
|
||||||
|
if (existingCounter is null)
|
||||||
|
{
|
||||||
|
_dbContext.ViewerEventCounters.Add(new ViewerEventCounter
|
||||||
|
{
|
||||||
|
ViewerId = viewer.Id,
|
||||||
|
EventKey = eventKey,
|
||||||
|
Period = period,
|
||||||
|
Count = m.TotalCount
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existingCounter.Count = m.TotalCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await _dbContext.SaveChangesAsync();
|
await _dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
if (skippedCardIds.Count > 0)
|
if (skippedCardIds.Count > 0)
|
||||||
@@ -275,6 +331,20 @@ public class AdminController : SVSimController
|
|||||||
request.SteamId, viewer.Id, skippedCardIds.Count,
|
request.SteamId, viewer.Id, skippedCardIds.Count,
|
||||||
string.Join(", ", skippedCardIds.Take(20)));
|
string.Join(", ", skippedCardIds.Take(20)));
|
||||||
}
|
}
|
||||||
|
if (skippedMissionIds.Count > 0)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"ImportViewer (steam_id={SteamId}, viewer_id={ViewerId}): skipped {Count} unknown mission_id(s). Sample: [{Sample}]",
|
||||||
|
request.SteamId, viewer.Id, skippedMissionIds.Count,
|
||||||
|
string.Join(", ", skippedMissionIds.Take(10)));
|
||||||
|
}
|
||||||
|
if (skippedMissionCounterIds.Count > 0)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"ImportViewer (steam_id={SteamId}, viewer_id={ViewerId}): {Count} mission(s) had unresolvable EventKey, counter not written. Sample: [{Sample}]",
|
||||||
|
request.SteamId, viewer.Id, skippedMissionCounterIds.Count,
|
||||||
|
string.Join(", ", skippedMissionCounterIds.Take(10)));
|
||||||
|
}
|
||||||
|
|
||||||
return new ImportViewerResponse
|
return new ImportViewerResponse
|
||||||
{
|
{
|
||||||
@@ -282,6 +352,8 @@ public class AdminController : SVSimController
|
|||||||
ShortUdid = viewer.ShortUdid,
|
ShortUdid = viewer.ShortUdid,
|
||||||
WasCreated = wasCreated,
|
WasCreated = wasCreated,
|
||||||
SkippedCardCount = skippedCardIds.Count,
|
SkippedCardCount = skippedCardIds.Count,
|
||||||
|
SkippedMissionCount = skippedMissionIds.Count,
|
||||||
|
SkippedMissionCounterCount = skippedMissionCounterIds.Count,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,6 +372,20 @@ public class AdminController : SVSimController
|
|||||||
owned.AddRange(rows);
|
owned.AddRange(rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: unify with MissionAssembler.cs — same logic duplicated here.
|
||||||
|
private static (string EventKey, string Period)? ResolveMissionCounter(MissionCatalogEntry catalog, DateTimeOffset nowUtc)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(catalog.EventType)) return null;
|
||||||
|
var period = catalog.LotType switch
|
||||||
|
{
|
||||||
|
6 => JstPeriod.DayKey(nowUtc),
|
||||||
|
2 => JstPeriod.WeekKey(nowUtc),
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
if (period is null) return null;
|
||||||
|
return (catalog.EventType!, period);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fallback sleeve id used when an imported deck has no resolvable <c>sleeve_id</c>.
|
/// Fallback sleeve id used when an imported deck has no resolvable <c>sleeve_id</c>.
|
||||||
/// 3000011 is prod's default deck sleeve.
|
/// 3000011 is prod's default deck sleeve.
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ public class ImportViewerRequest
|
|||||||
[JsonPropertyName("decks")] public List<ImportDeck>? Decks { get; set; }
|
[JsonPropertyName("decks")] public List<ImportDeck>? Decks { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("mission_meta")] public ImportMissionMeta? MissionMeta { get; set; }
|
[JsonPropertyName("mission_meta")] public ImportMissionMeta? MissionMeta { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("missions")] public List<ImportMission>? Missions { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ImportDeck
|
public class ImportDeck
|
||||||
@@ -81,6 +83,18 @@ public class ImportItem
|
|||||||
[JsonPropertyName("count")] public int Count { get; set; }
|
[JsonPropertyName("count")] public int Count { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ImportMission
|
||||||
|
{
|
||||||
|
[JsonPropertyName("mission_id")]
|
||||||
|
public int MissionId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("mission_status")]
|
||||||
|
public int MissionStatus { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("total_count")]
|
||||||
|
public int TotalCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class ImportMissionMeta
|
public class ImportMissionMeta
|
||||||
{
|
{
|
||||||
[JsonPropertyName("has_received_pick_two_mission")]
|
[JsonPropertyName("has_received_pick_two_mission")]
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using SVSim.Database;
|
using SVSim.Database;
|
||||||
using SVSim.Database.Enums;
|
using SVSim.Database.Enums;
|
||||||
|
using SVSim.Database.Models;
|
||||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Admin;
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Admin;
|
||||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.Admin;
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.Admin;
|
||||||
using SVSim.UnitTests.Infrastructure;
|
using SVSim.UnitTests.Infrastructure;
|
||||||
@@ -484,4 +485,104 @@ public class AdminControllerTests
|
|||||||
Assert.That(viewer.MissionData.MissionChangeTime,
|
Assert.That(viewer.MissionData.MissionChangeTime,
|
||||||
Is.EqualTo(DateTimeOffset.FromUnixTimeSeconds(1_700_000_000L).UtcDateTime));
|
Is.EqualTo(DateTimeOffset.FromUnixTimeSeconds(1_700_000_000L).UtcDateTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ImportViewer_Missions_RoundTripsWithCounter()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
using (var seedScope = factory.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var db = seedScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||||
|
db.MissionCatalog.Add(new MissionCatalogEntry
|
||||||
|
{
|
||||||
|
Id = 9001, LotType = 6 /* daily */,
|
||||||
|
EventType = "battle_win_total", EventArg = null
|
||||||
|
});
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
var client = factory.CreateClient();
|
||||||
|
ulong steamId = 70000000000000002UL;
|
||||||
|
|
||||||
|
var resp = await client.PostAsJsonAsync("/admin/import_viewer", new ImportViewerRequest
|
||||||
|
{
|
||||||
|
SteamId = steamId,
|
||||||
|
Missions = new List<ImportMission>
|
||||||
|
{
|
||||||
|
new() { MissionId = 9001, MissionStatus = 1, TotalCount = 7 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
resp.EnsureSuccessStatusCode();
|
||||||
|
var body = await resp.Content.ReadFromJsonAsync<ImportViewerResponse>(JsonOptions);
|
||||||
|
Assert.That(body!.SkippedMissionCount, Is.EqualTo(0));
|
||||||
|
Assert.That(body.SkippedMissionCounterCount, Is.EqualTo(0));
|
||||||
|
|
||||||
|
using var scope = factory.Services.CreateScope();
|
||||||
|
var verifyDb = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||||
|
var viewerId = await verifyDb.Viewers
|
||||||
|
.Where(v => v.SocialAccountConnections
|
||||||
|
.Any(s => s.AccountType == SocialAccountType.Steam && s.AccountId == steamId))
|
||||||
|
.Select(v => v.Id).SingleAsync();
|
||||||
|
Assert.That(verifyDb.ViewerMissions.Count(m => m.ViewerId == viewerId && m.MissionCatalogId == 9001), Is.EqualTo(1));
|
||||||
|
Assert.That(verifyDb.ViewerEventCounters
|
||||||
|
.Count(c => c.ViewerId == viewerId && c.EventKey == "battle_win_total" && c.Count == 7), Is.EqualTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ImportViewer_Missions_UnknownMissionIdSkipped()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
var client = factory.CreateClient();
|
||||||
|
ulong steamId = 70000000000000003UL;
|
||||||
|
|
||||||
|
var resp = await client.PostAsJsonAsync("/admin/import_viewer", new ImportViewerRequest
|
||||||
|
{
|
||||||
|
SteamId = steamId,
|
||||||
|
Missions = new List<ImportMission>
|
||||||
|
{
|
||||||
|
new() { MissionId = 999999, MissionStatus = 1, TotalCount = 3 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
resp.EnsureSuccessStatusCode();
|
||||||
|
var body = await resp.Content.ReadFromJsonAsync<ImportViewerResponse>(JsonOptions);
|
||||||
|
Assert.That(body!.SkippedMissionCount, Is.EqualTo(1));
|
||||||
|
|
||||||
|
using var scope = factory.Services.CreateScope();
|
||||||
|
var verifyDb2 = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||||
|
Assert.That(verifyDb2.ViewerMissions.Any(), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ImportViewer_Missions_CatalogPresentButCounterUnresolvable()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
using (var seedScope = factory.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var seedDb = seedScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||||
|
seedDb.MissionCatalog.Add(new MissionCatalogEntry
|
||||||
|
{
|
||||||
|
Id = 9002, LotType = 6,
|
||||||
|
EventType = null /* unresolvable */, EventArg = null
|
||||||
|
});
|
||||||
|
await seedDb.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
var client = factory.CreateClient();
|
||||||
|
ulong steamId = 70000000000000004UL;
|
||||||
|
|
||||||
|
var resp = await client.PostAsJsonAsync("/admin/import_viewer", new ImportViewerRequest
|
||||||
|
{
|
||||||
|
SteamId = steamId,
|
||||||
|
Missions = new List<ImportMission>
|
||||||
|
{
|
||||||
|
new() { MissionId = 9002, MissionStatus = 1, TotalCount = 5 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var body = await resp.Content.ReadFromJsonAsync<ImportViewerResponse>(JsonOptions);
|
||||||
|
Assert.That(body!.SkippedMissionCount, Is.EqualTo(0));
|
||||||
|
Assert.That(body.SkippedMissionCounterCount, Is.EqualTo(1));
|
||||||
|
|
||||||
|
using var scope = factory.Services.CreateScope();
|
||||||
|
var verifyDb3 = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||||
|
Assert.That(verifyDb3.ViewerMissions.Count(m => m.MissionCatalogId == 9002), Is.EqualTo(1));
|
||||||
|
Assert.That(verifyDb3.ViewerEventCounters.Any(), Is.False);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user