diff --git a/SVSim.Database/Repositories/BattlePass/ViewerBattlePassRepository.cs b/SVSim.Database/Repositories/BattlePass/ViewerBattlePassRepository.cs index ebdd6ea..ffb2c7e 100644 --- a/SVSim.Database/Repositories/BattlePass/ViewerBattlePassRepository.cs +++ b/SVSim.Database/Repositories/BattlePass/ViewerBattlePassRepository.cs @@ -26,7 +26,18 @@ public sealed class ViewerBattlePassRepository : IViewerBattlePassRepository WeeklyPeriodStart = null, }; _db.ViewerBattlePassProgress.Add(entry); - return entry; + try + { + await _db.SaveChangesAsync(ct); + return entry; + } + catch (DbUpdateException) + { + // Concurrent /info call won the race; re-read the row the other thread persisted. + _db.Entry(entry).State = EntityState.Detached; + return await _db.ViewerBattlePassProgress + .FirstAsync(p => p.ViewerId == viewerId && p.SeasonId == seasonId, ct); + } } public Task> GetClaimsAsync(long viewerId, int seasonId, CancellationToken ct) => diff --git a/SVSim.EmulatedEntrypoint/Services/BattlePassService.cs b/SVSim.EmulatedEntrypoint/Services/BattlePassService.cs index bab8424..cf99248 100644 --- a/SVSim.EmulatedEntrypoint/Services/BattlePassService.cs +++ b/SVSim.EmulatedEntrypoint/Services/BattlePassService.cs @@ -13,6 +13,9 @@ public sealed class BattlePassService : IBattlePassService // Default cap mirrors the captured /battle_pass/info.gauge_info.weekly_limit_point. public const int WeeklyLimitPointDefault = 3000; + /// JST = UTC+9. Capture format ("2026-04-01 02:00:00") is implicit JST. + private static readonly TimeSpan JstOffset = TimeSpan.FromHours(9); + private readonly IBattlePassRepository _bp; private readonly IViewerBattlePassRepository _viewerBp; private readonly TimeProvider _time; @@ -46,8 +49,6 @@ public sealed class BattlePassService : IBattlePassService if (season is null) return null; var progress = await _viewerBp.GetOrCreateProgressAsync(viewerId, season.Id, ct); - // Persist the lazy-created row so concurrent /info calls don't try to create twice. - await _db.SaveChangesAsync(ct); var rewards = await _bp.GetSeasonRewardsAsync(season.Id, ct); var claims = await _viewerBp.GetClaimsAsync(viewerId, season.Id, ct); @@ -123,7 +124,7 @@ public sealed class BattlePassService : IBattlePassService private static string FormatWireDate(DateTimeOffset dt) => // Capture format is "2026-04-01 02:00:00" (JST, space-separated). Emit in same shape // in JST so the client gets back what it gave. - dt.ToOffset(TimeSpan.FromHours(9)) + dt.ToOffset(JstOffset) .ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); private static string Inv(long v) => v.ToString(CultureInfo.InvariantCulture);