feat: ILoginBonusService — JST day-bucketed claim + grant + DTO assembly
GrantIfDueAsync advances streak (1..15 cycle), grants the day's reward via inventory tx, returns wire-shape DailyLoginBonus. IsDue helper for MyPage. Also fixes GameConfigurationJsonbTests section-count (15→16) which was broken since Task 3 added the LoginBonus [ConfigSection] but the assertion wasn't updated.
This commit is contained in:
@@ -113,6 +113,7 @@ public class Program
|
||||
builder.Services.AddScoped<IArenaTwoPickService, ArenaTwoPickService>();
|
||||
builder.Services.AddScoped<IMatchContextBuilder, MatchContextBuilder>();
|
||||
builder.Services.AddScoped<IStoryService, StoryService>();
|
||||
builder.Services.AddScoped<ILoginBonusService, LoginBonusService>();
|
||||
builder.Services.AddScoped<IDeckListBuilder, DeckListBuilder>();
|
||||
builder.Services.AddSingleton<IRandom, SystemRandom>();
|
||||
builder.Services.AddSingleton<PuzzleMissionEvaluator>();
|
||||
|
||||
22
SVSim.EmulatedEntrypoint/Services/ILoginBonusService.cs
Normal file
22
SVSim.EmulatedEntrypoint/Services/ILoginBonusService.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using SVSim.Database.Services.Inventory;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos;
|
||||
|
||||
namespace SVSim.EmulatedEntrypoint.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Daily login bonus controller-facing API. Single entry point: called from
|
||||
/// /load/index inside the existing inventory transaction. Returns null when the viewer
|
||||
/// has already claimed today's bonus.
|
||||
/// </summary>
|
||||
public interface ILoginBonusService
|
||||
{
|
||||
/// <summary>
|
||||
/// If due, advances <c>tx.Viewer.LoginBonusStreak</c> + <c>LastLoginBonusClaimedAt</c>,
|
||||
/// grants the day's reward via <paramref name="tx"/>, and returns the populated wire
|
||||
/// DTO. If not due, returns null and leaves viewer state untouched.
|
||||
/// </summary>
|
||||
Task<DailyLoginBonus?> GrantIfDueAsync(IInventoryTransaction tx, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Read-only "would GrantIfDueAsync emit a bonus right now?" — for MyPage flag.</summary>
|
||||
bool IsDue(SVSim.Database.Models.Viewer viewer);
|
||||
}
|
||||
68
SVSim.EmulatedEntrypoint/Services/LoginBonusService.cs
Normal file
68
SVSim.EmulatedEntrypoint/Services/LoginBonusService.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System.Globalization;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.Database.Models.Config;
|
||||
using SVSim.Database.Services;
|
||||
using SVSim.Database.Services.Inventory;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos;
|
||||
|
||||
namespace SVSim.EmulatedEntrypoint.Services;
|
||||
|
||||
public class LoginBonusService : ILoginBonusService
|
||||
{
|
||||
private readonly IGameConfigService _config;
|
||||
private readonly TimeProvider _time;
|
||||
|
||||
public LoginBonusService(IGameConfigService config, TimeProvider time)
|
||||
{
|
||||
_config = config;
|
||||
_time = time;
|
||||
}
|
||||
|
||||
public bool IsDue(Viewer viewer)
|
||||
{
|
||||
if (viewer.LastLoginBonusClaimedAt is null) return true;
|
||||
var now = _time.GetUtcNow();
|
||||
var lastClaim = new DateTimeOffset(
|
||||
DateTime.SpecifyKind(viewer.LastLoginBonusClaimedAt.Value, DateTimeKind.Utc));
|
||||
return JstPeriod.DayKey(lastClaim) != JstPeriod.DayKey(now);
|
||||
}
|
||||
|
||||
public async Task<DailyLoginBonus?> GrantIfDueAsync(IInventoryTransaction tx, CancellationToken ct = default)
|
||||
{
|
||||
var viewer = tx.Viewer;
|
||||
if (!IsDue(viewer)) return null;
|
||||
|
||||
var cfg = _config.Get<LoginBonusConfig>();
|
||||
if (cfg.Normal.Count == 0) return null; // catalog misconfigured — nothing to grant
|
||||
|
||||
int newStreak = (viewer.LoginBonusStreak % cfg.Normal.Count) + 1;
|
||||
var entry = cfg.Normal[newStreak - 1];
|
||||
|
||||
await tx.GrantAsync(entry.RewardType, entry.RewardDetailId, entry.RewardNumber, ct);
|
||||
|
||||
viewer.LoginBonusStreak = newStreak;
|
||||
viewer.LastLoginBonusClaimedAt = _time.GetUtcNow().UtcDateTime;
|
||||
|
||||
return new DailyLoginBonus
|
||||
{
|
||||
Normal = new LoginBonusCampaign
|
||||
{
|
||||
Name = cfg.Name,
|
||||
CampaignId = cfg.CampaignId.ToString(CultureInfo.InvariantCulture),
|
||||
Img = cfg.Img,
|
||||
NowCount = newStreak,
|
||||
IsNextReward = true,
|
||||
IsOneDayMultiRewards = false,
|
||||
Rewards = cfg.Normal.Select(n => new LoginBonusReward
|
||||
{
|
||||
EffectId = n.EffectId.ToString(CultureInfo.InvariantCulture),
|
||||
RewardType = ((int)n.RewardType).ToString(CultureInfo.InvariantCulture),
|
||||
RewardDetailId = n.RewardDetailId.ToString(CultureInfo.InvariantCulture),
|
||||
RewardNumber = n.RewardNumber.ToString(CultureInfo.InvariantCulture),
|
||||
}).ToList(),
|
||||
},
|
||||
Total = null,
|
||||
Campaign = new List<LoginBonusCampaign>(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user