feat(guild): change_role atomic transfer + friend_list
- ChangeRoleAsync: Leader-only role management with SubLeader cap check. Atomic leader transfer loads both member rows + Guild.LeaderViewerId in one _db.SaveChangesAsync call (no split saves). Emits ChangeLeader (6) or ChangeSubLeader (7) chat events. Same-role is no-op / returns Ok. - /guild/change_role controller: calls ChangeRoleAsync, returns full updated member list (GuildChangeRoleResponse.Members[]). - /guild/friend_list controller: calls IFriendService.GetFriendsAsync, then annotates each friend with is_join_guild from Viewer.GuildId column. - IGuildRepository.UpdateLeaderViewerIdAsync: added for completeness even though the atomic transfer path bypasses it via direct EF mutation. - Tests: 8 GuildServiceChangeRoleTests (role changes, cap, atomic transfer, no-op, permission); 3 GuildChangeRoleFriendListFlowTests (HTTP roundtrip, is_join_guild); 2 GuildWireShape tests (literal JSON shape); 1508 pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ using SVSim.Database.Entities.Guild;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.Database.Models.Config;
|
||||
using SVSim.Database.Services;
|
||||
using SVSim.Database.Services.Friend;
|
||||
using SVSim.Database.Services.Guild;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Common;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Common.Guild;
|
||||
@@ -20,15 +21,17 @@ public sealed class GuildController : SVSimController
|
||||
private readonly IGuildService _guild;
|
||||
private readonly IGameConfigService _configs;
|
||||
private readonly SVSimDbContext _db;
|
||||
private readonly IFriendService _friends;
|
||||
|
||||
// Wire error code returned when guild operations fail (non-1 result_code).
|
||||
private const int GuildErrorResultCode = 2;
|
||||
|
||||
public GuildController(IGuildService guild, IGameConfigService configs, SVSimDbContext db)
|
||||
public GuildController(IGuildService guild, IGameConfigService configs, SVSimDbContext db, IFriendService friends)
|
||||
{
|
||||
_guild = guild;
|
||||
_configs = configs;
|
||||
_db = db;
|
||||
_friends = friends;
|
||||
}
|
||||
|
||||
[HttpPost("info")]
|
||||
@@ -171,8 +174,40 @@ public sealed class GuildController : SVSimController
|
||||
}
|
||||
|
||||
[HttpPost("friend_list")]
|
||||
public Task<ActionResult<GuildFriendListResponse>> FriendList([FromBody] BaseRequest _, CancellationToken ct)
|
||||
=> Task.FromResult<ActionResult<GuildFriendListResponse>>(new GuildFriendListResponse { Friends = new() });
|
||||
public async Task<ActionResult<GuildFriendListResponse>> FriendList([FromBody] BaseRequest _, CancellationToken ct)
|
||||
{
|
||||
if (!TryGetViewerId(out var viewerId)) return Unauthorized();
|
||||
|
||||
var friendInfo = await _friends.GetFriendsAsync(viewerId, ct);
|
||||
if (friendInfo.Friends.Count == 0)
|
||||
return new GuildFriendListResponse { Friends = new() };
|
||||
|
||||
// For each friend, determine if they are already in a guild (is_join_guild).
|
||||
var friendViewerIds = friendInfo.Friends.Select(f => (long)f.ViewerId).ToList();
|
||||
var viewerRows = await _db.Viewers
|
||||
.AsNoTracking()
|
||||
.Where(v => friendViewerIds.Contains(v.Id))
|
||||
.Select(v => new { v.Id, v.GuildId })
|
||||
.ToDictionaryAsync(v => v.Id, v => v.GuildId, ct);
|
||||
|
||||
var candidates = new List<GuildInviteCandidateDto>(friendInfo.Friends.Count);
|
||||
foreach (var f in friendInfo.Friends)
|
||||
{
|
||||
viewerRows.TryGetValue(f.ViewerId, out var guildId);
|
||||
candidates.Add(new GuildInviteCandidateDto
|
||||
{
|
||||
ViewerId = f.ViewerId,
|
||||
Name = f.Name,
|
||||
EmblemId = f.EmblemId,
|
||||
CountryCode = f.CountryCode,
|
||||
Rank = f.Rank,
|
||||
DegreeId = f.DegreeId,
|
||||
IsJoinGuild = guildId is not null,
|
||||
});
|
||||
}
|
||||
|
||||
return new GuildFriendListResponse { Friends = candidates };
|
||||
}
|
||||
|
||||
[HttpPost("invite_user_list")]
|
||||
public async Task<ActionResult<GuildInviteUserListResponse>> InviteUserList([FromBody] BaseRequest _, CancellationToken ct)
|
||||
@@ -315,8 +350,23 @@ public sealed class GuildController : SVSimController
|
||||
}
|
||||
|
||||
[HttpPost("change_role")]
|
||||
public Task<ActionResult<GuildChangeRoleResponse>> ChangeRole([FromBody] GuildChangeRoleRequest req, CancellationToken ct)
|
||||
=> Task.FromResult<ActionResult<GuildChangeRoleResponse>>(new GuildChangeRoleResponse { Members = new() });
|
||||
public async Task<ActionResult<GuildChangeRoleResponse>> ChangeRole([FromBody] GuildChangeRoleRequest req, CancellationToken ct)
|
||||
{
|
||||
if (!TryGetViewerId(out var viewerId)) return Unauthorized();
|
||||
var r = await _guild.ChangeRoleAsync(viewerId, req.TargetViewerId, req.RoleId, ct);
|
||||
if (!r.IsOk) return WireError();
|
||||
|
||||
// Load caller's membership to get the guild id, then return the full updated member list.
|
||||
var membership = await _db.GuildMembers.FirstOrDefaultAsync(m => m.ViewerId == viewerId, ct);
|
||||
if (membership is null) return WireError();
|
||||
|
||||
var members = await _db.GuildMembers
|
||||
.Where(m => m.GuildId == membership.GuildId)
|
||||
.ToListAsync(ct);
|
||||
|
||||
var memberDtos = await ToMemberDtoListAsync(members, viewerId, ct);
|
||||
return new GuildChangeRoleResponse { Members = memberDtos };
|
||||
}
|
||||
|
||||
// ===== Private helpers =====
|
||||
|
||||
|
||||
Reference in New Issue
Block a user