feat(friend): IFriendService + IPlayedTogetherWriter + DTO records
Task 3: service contract (interface + DTOs) and FriendService skeleton. All methods throw NotImplementedException; Tasks 4-6 fill in the logic. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
84
SVSim.Database/Services/Friend/FriendDtos.cs
Normal file
84
SVSim.Database/Services/Friend/FriendDtos.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
namespace SVSim.Database.Services.Friend;
|
||||
|
||||
/// <summary>
|
||||
/// One friend in the requested viewer's friend list. Wire shape carries 15 fields;
|
||||
/// most are cosmetic ints emitted as strings (matches prod). Numeric fields
|
||||
/// (viewer_id, rank, emblem_id, degree_id) ship as native ints.
|
||||
/// </summary>
|
||||
public sealed record FriendEntry(
|
||||
int ViewerId,
|
||||
string Name,
|
||||
string CountryCode,
|
||||
int Rank,
|
||||
long EmblemId,
|
||||
int DegreeId,
|
||||
string LastPlayTime, // "yyyy-MM-dd HH:mm:ss"
|
||||
string DeviceType,
|
||||
string MaxFriend,
|
||||
string IsReceivedTwoPickMission,
|
||||
string Birth,
|
||||
string MissionChangeTime,
|
||||
string MissionReceiveType,
|
||||
string IsOfficial,
|
||||
string IsOfficialMarkDisplayed);
|
||||
|
||||
/// <summary>
|
||||
/// One friend apply (sent or received). Wire field <c>id</c> is the apply's PK.
|
||||
/// </summary>
|
||||
public sealed record FriendApplyEntry(
|
||||
int Id,
|
||||
int ViewerId, // OTHER viewer's id
|
||||
string Name,
|
||||
string CountryCode,
|
||||
int Rank,
|
||||
long EmblemId,
|
||||
int DegreeId,
|
||||
string LastPlayTime,
|
||||
string CreateTime,
|
||||
int MissionType); // 0 when omitted on the wire
|
||||
|
||||
/// <summary>
|
||||
/// One recent-opponent row. <see cref="FriendStatus"/> is computed at read time:
|
||||
/// 0 = NO_ACTION, 1 = IS_FRIEND, 2 = IS_SEND (caller has outgoing apply),
|
||||
/// 3 = IS_RECEIVED (caller has incoming apply from opponent).
|
||||
/// <see cref="FriendApplyId"/> is the relevant apply's PK when status is 2 or 3, else 0.
|
||||
/// </summary>
|
||||
public sealed record PlayedTogetherEntry(
|
||||
int ViewerId,
|
||||
string Name,
|
||||
string CountryCode,
|
||||
int Rank,
|
||||
long EmblemId,
|
||||
int DegreeId,
|
||||
string LastPlayTime,
|
||||
string PlayedTime,
|
||||
int FriendStatus,
|
||||
int FriendApplyId,
|
||||
int PlayedMode,
|
||||
int BattleType,
|
||||
int DeckFormat,
|
||||
int TwoPickType);
|
||||
|
||||
public sealed record FriendInfoResult(
|
||||
IReadOnlyList<FriendEntry> Friends,
|
||||
int Count,
|
||||
int MaxCount);
|
||||
|
||||
public sealed record ReceiveApplyInfoResult(
|
||||
IReadOnlyList<FriendApplyEntry> ReceiveApplies,
|
||||
int ApproveApplyCount);
|
||||
|
||||
public sealed record SendApplyInfoResult(
|
||||
IReadOnlyList<FriendApplyEntry> SendApplies,
|
||||
int RemainingApplyCount,
|
||||
int SendApplyMaxCount);
|
||||
|
||||
public sealed record PlayedTogetherResult(
|
||||
IReadOnlyList<PlayedTogetherEntry> Histories);
|
||||
|
||||
/// <summary>Context recorded by <see cref="IPlayedTogetherWriter.RecordAsync"/>.</summary>
|
||||
public sealed record BattleParticipationContext(
|
||||
int PlayedMode,
|
||||
int BattleType,
|
||||
int DeckFormat,
|
||||
int TwoPickType);
|
||||
69
SVSim.Database/Services/Friend/FriendService.cs
Normal file
69
SVSim.Database/Services/Friend/FriendService.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace SVSim.Database.Services.Friend;
|
||||
|
||||
public sealed class FriendService : IFriendService, IPlayedTogetherWriter
|
||||
{
|
||||
internal const int FriendMaxCount = 110;
|
||||
internal const int SendApplyMaxCount = 110;
|
||||
internal const int PlayedTogetherRetention = 50;
|
||||
|
||||
// Cosmetic field defaults matching the prod capture's "no campaign, normal player" state.
|
||||
internal const string DefaultDeviceType = "2";
|
||||
internal const string DefaultMaxFriend = "110";
|
||||
internal const string DefaultIsReceivedTwoPickMission = "1";
|
||||
internal const string DefaultBirth = "0";
|
||||
internal const string DefaultMissionChangeTime = "2017-09-15 02:36:09";
|
||||
internal const string DefaultMissionReceiveType = "0";
|
||||
internal const string DefaultIsOfficial = "0";
|
||||
internal const string DefaultIsOfficialMarkDisplayed = "0";
|
||||
|
||||
private readonly SVSimDbContext _db;
|
||||
private readonly ILogger<FriendService> _log;
|
||||
|
||||
public FriendService(SVSimDbContext db, ILogger<FriendService> log)
|
||||
{
|
||||
_db = db;
|
||||
_log = log;
|
||||
}
|
||||
|
||||
public Task<FriendInfoResult> GetFriendsAsync(long viewerId, CancellationToken ct) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task<ReceiveApplyInfoResult> GetReceiveAppliesAsync(long viewerId, CancellationToken ct) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task<SendApplyInfoResult> GetSendAppliesAsync(long viewerId, CancellationToken ct) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task<PlayedTogetherResult> GetPlayedTogetherAsync(long viewerId, CancellationToken ct) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task<FriendEntry?> SearchAsync(long viewerId, int targetViewerId, CancellationToken ct) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task SendApplyAsync(long viewerId, int targetViewerId, CancellationToken ct) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task ApproveApplyAsync(long viewerId, int applyId, CancellationToken ct) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task RejectApplyAsync(long viewerId, int applyId, CancellationToken ct) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task CancelApplyAsync(long viewerId, int applyId, CancellationToken ct) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task RejectAllAppliesAsync(long viewerId, CancellationToken ct) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task CancelAllAppliesAsync(long viewerId, CancellationToken ct) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task RejectFriendAsync(long viewerId, int targetViewerId, CancellationToken ct) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task RecordAsync(long ownerViewerId, long opponentViewerId, BattleParticipationContext ctx, CancellationToken ct) =>
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
30
SVSim.Database/Services/Friend/IFriendService.cs
Normal file
30
SVSim.Database/Services/Friend/IFriendService.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
namespace SVSim.Database.Services.Friend;
|
||||
|
||||
public interface IFriendService
|
||||
{
|
||||
Task<FriendInfoResult> GetFriendsAsync(long viewerId, CancellationToken ct);
|
||||
Task<ReceiveApplyInfoResult> GetReceiveAppliesAsync(long viewerId, CancellationToken ct);
|
||||
Task<SendApplyInfoResult> GetSendAppliesAsync(long viewerId, CancellationToken ct);
|
||||
Task<PlayedTogetherResult> GetPlayedTogetherAsync(long viewerId, CancellationToken ct);
|
||||
|
||||
/// <summary>Returns null when not found, self-search, or any error.</summary>
|
||||
Task<FriendEntry?> SearchAsync(long viewerId, int targetViewerId, CancellationToken ct);
|
||||
|
||||
/// <summary>No-op if target missing, self, already friends, already-pending apply, or at outgoing-apply cap.</summary>
|
||||
Task SendApplyAsync(long viewerId, int targetViewerId, CancellationToken ct);
|
||||
|
||||
/// <summary>No-op if apply not addressed to caller, would push either side past friend cap. Cleans reverse-direction apply if present.</summary>
|
||||
Task ApproveApplyAsync(long viewerId, int applyId, CancellationToken ct);
|
||||
|
||||
/// <summary>No-op if apply not addressed to caller.</summary>
|
||||
Task RejectApplyAsync(long viewerId, int applyId, CancellationToken ct);
|
||||
|
||||
/// <summary>No-op if apply not sent by caller.</summary>
|
||||
Task CancelApplyAsync(long viewerId, int applyId, CancellationToken ct);
|
||||
|
||||
Task RejectAllAppliesAsync(long viewerId, CancellationToken ct);
|
||||
Task CancelAllAppliesAsync(long viewerId, CancellationToken ct);
|
||||
|
||||
/// <summary>Deletes both directions of the friendship (A→B and B→A).</summary>
|
||||
Task RejectFriendAsync(long viewerId, int targetViewerId, CancellationToken ct);
|
||||
}
|
||||
11
SVSim.Database/Services/Friend/IPlayedTogetherWriter.cs
Normal file
11
SVSim.Database/Services/Friend/IPlayedTogetherWriter.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace SVSim.Database.Services.Friend;
|
||||
|
||||
/// <summary>
|
||||
/// Records a recent-opponent entry on the owner viewer. Upserts the (owner, opponent)
|
||||
/// row to PlayedAt = now, enforces a 50-row per-viewer retention cap by deleting the
|
||||
/// owner's oldest row when at cap. No-op if owner equals opponent.
|
||||
/// </summary>
|
||||
public interface IPlayedTogetherWriter
|
||||
{
|
||||
Task RecordAsync(long ownerViewerId, long opponentViewerId, BattleParticipationContext ctx, CancellationToken ct);
|
||||
}
|
||||
Reference in New Issue
Block a user