fix(guild): final-review remediation — replay_detail flat, FK on leader, transaction wraps, _db extraction
C1: replay_detail — flatten stored payload to data level. ChatReplayDetailTask.Parse() calls new ReplayDetailInfo(data) which unguardedly accesses data[battleId], data[seed], data[vid1], etc. Wrapping under replay_info key crashes the client. Controller now returns Ok(JsonElement) directly so stored battle fields are at data root. Wire-shape test added. C2: Guild.LeaderViewerId long to long?; add HasOne<Viewer> FK with OnDelete=SetNull; migration AddGuildLeaderViewerIdFk; all consumers null-guarded with ?? 0L. C3: BreakupAsync — wrap 6 destructive ops in IDbContextTransaction with InMemory fallback. C4: CommitJoinAsync — wrap member-add + viewer-guildId-set + invite/request cleanup in IDbContextTransaction with InMemory fallback. Chat event emitted after commit. I1: GuildController — remove SVSimDbContext field; inject IViewerRepository + IGuildMemberRepository. Add IGuildMemberRepository.GetViewerIdsInAGuildAsync (batch guild-membership check). All _db.Viewers / _db.GuildMembers queries replaced with repo calls. I2: GuildService — extract 3 _db.Viewers queries: GetEquippedEmblemIdAsync (CreateAsync), LoadGuildProfileBatchAsync (ListOutgoingInvitesAsync + ListPendingJoinRequestsForMyGuildAsync). Add GuildMemberProfile record with IsOfficialMarkDisplayed. GetEmblemListAsync for EmblemList. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ public class Guild
|
||||
public int GuildId { get; set; } // server-generated 9-digit
|
||||
public string Name { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public long LeaderViewerId { get; set; }
|
||||
public long? LeaderViewerId { get; set; }
|
||||
public long EmblemId { get; set; }
|
||||
public GuildActivity Activity { get; set; }
|
||||
public GuildJoinCondition JoinCondition { get; set; }
|
||||
|
||||
5018
SVSim.Database/Migrations/20260627202442_AddGuildLeaderViewerIdFk.Designer.cs
generated
Normal file
5018
SVSim.Database/Migrations/20260627202442_AddGuildLeaderViewerIdFk.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace SVSim.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddGuildLeaderViewerIdFk : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<long>(
|
||||
name: "LeaderViewerId",
|
||||
table: "Guilds",
|
||||
type: "bigint",
|
||||
nullable: true,
|
||||
oldClrType: typeof(long),
|
||||
oldType: "bigint");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Guilds_LeaderViewerId",
|
||||
table: "Guilds",
|
||||
column: "LeaderViewerId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Guilds_Viewers_LeaderViewerId",
|
||||
table: "Guilds",
|
||||
column: "LeaderViewerId",
|
||||
principalTable: "Viewers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Guilds_Viewers_LeaderViewerId",
|
||||
table: "Guilds");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Guilds_LeaderViewerId",
|
||||
table: "Guilds");
|
||||
|
||||
migrationBuilder.AlterColumn<long>(
|
||||
name: "LeaderViewerId",
|
||||
table: "Guilds",
|
||||
type: "bigint",
|
||||
nullable: false,
|
||||
defaultValue: 0L,
|
||||
oldClrType: typeof(long),
|
||||
oldType: "bigint",
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,7 @@ namespace SVSim.Database.Migrations
|
||||
b.Property<int>("JoinCondition")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long>("LeaderViewerId")
|
||||
b.Property<long?>("LeaderViewerId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Name")
|
||||
@@ -120,6 +120,8 @@ namespace SVSim.Database.Migrations
|
||||
|
||||
b.HasKey("GuildId");
|
||||
|
||||
b.HasIndex("LeaderViewerId");
|
||||
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.ToTable("Guilds");
|
||||
@@ -3653,6 +3655,14 @@ namespace SVSim.Database.Migrations
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Entities.Guild.Guild", b =>
|
||||
{
|
||||
b.HasOne("SVSim.Database.Models.Viewer", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("LeaderViewerId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Entities.Guild.GuildChatMessage", b =>
|
||||
{
|
||||
b.HasOne("SVSim.Database.Entities.Guild.Guild", "Guild")
|
||||
|
||||
@@ -70,4 +70,15 @@ public sealed class GuildMemberRepository : IGuildMemberRepository
|
||||
result[row.GuildId] = row.Count;
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<HashSet<long>> GetViewerIdsInAGuildAsync(
|
||||
IReadOnlyList<long> viewerIds, CancellationToken ct = default)
|
||||
{
|
||||
if (viewerIds.Count == 0) return new HashSet<long>();
|
||||
var ids = await _db.GuildMembers
|
||||
.Where(m => viewerIds.Contains(m.ViewerId))
|
||||
.Select(m => m.ViewerId)
|
||||
.ToListAsync(ct);
|
||||
return new HashSet<long>(ids);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,4 +15,11 @@ public interface IGuildMemberRepository
|
||||
|
||||
/// <summary>Returns a count of members per guild for the given guild ids. Used by Task 9 search.</summary>
|
||||
Task<Dictionary<int, int>> CountBatchByGuildIdsAsync(IReadOnlyList<int> guildIds, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Batch-checks whether each viewer id in <paramref name="viewerIds"/> is currently in any guild.
|
||||
/// Returns a set of viewer ids that ARE in a guild. Used by <c>/guild/friend_list</c> to set
|
||||
/// <c>is_join_guild</c>.
|
||||
/// </summary>
|
||||
Task<HashSet<long>> GetViewerIdsInAGuildAsync(IReadOnlyList<long> viewerIds, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,18 @@ public record ChatUserProfile(
|
||||
int Rank,
|
||||
int DegreeId);
|
||||
|
||||
/// <summary>
|
||||
/// Richer profile shape used by guild management surfaces (invite list, join-request list).
|
||||
/// Extends <see cref="ChatUserProfile"/> with <c>IsOfficialMarkDisplayed</c>.
|
||||
/// </summary>
|
||||
public record GuildMemberProfile(
|
||||
string Name,
|
||||
long EmblemId,
|
||||
string CountryCode,
|
||||
int Rank,
|
||||
int DegreeId,
|
||||
bool IsOfficialMarkDisplayed);
|
||||
|
||||
public interface IViewerRepository
|
||||
{
|
||||
Task<Models.Viewer?> GetViewerBySocialConnection(SocialAccountType accountType, ulong socialId);
|
||||
@@ -57,4 +69,23 @@ public interface IViewerRepository
|
||||
/// in the controller. Ids with no matching row are absent from the result. Read-only (AsNoTracking).
|
||||
/// </summary>
|
||||
Task<IReadOnlyDictionary<long, ChatUserProfile>> LoadChatProfilesAsync(IReadOnlyCollection<long> viewerIds, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Batch-loads the <see cref="GuildMemberProfile"/> fields (emblem, degree,
|
||||
/// <c>IsOfficialMarkDisplayed</c>) for a set of viewer ids. Used by guild management surfaces
|
||||
/// (invite_user_list, join_request_list). Ids with no matching row are absent. Read-only.
|
||||
/// </summary>
|
||||
Task<IReadOnlyDictionary<long, GuildMemberProfile>> LoadGuildProfileBatchAsync(IReadOnlyCollection<long> viewerIds, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Loads a viewer's currently-equipped emblem id, or <c>100_000_000</c> (default) if none.
|
||||
/// Used by <see cref="SVSim.Database.Services.Guild.GuildService"/> to seed the guild's initial emblem.
|
||||
/// Returns <c>100_000_000</c> if the viewer doesn't exist.
|
||||
/// </summary>
|
||||
Task<long> GetEquippedEmblemIdAsync(long viewerId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Batch-loads owned guild-emblem ids for a viewer. Used by <c>/guild/emblem_list</c>.
|
||||
/// </summary>
|
||||
Task<List<long>> GetEmblemListAsync(long viewerId, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -323,6 +323,56 @@ public class ViewerRepository : IViewerRepository
|
||||
DegreeId: r.DegreeId));
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyDictionary<long, GuildMemberProfile>> LoadGuildProfileBatchAsync(IReadOnlyCollection<long> viewerIds, CancellationToken ct = default)
|
||||
{
|
||||
if (viewerIds.Count == 0) return new Dictionary<long, GuildMemberProfile>();
|
||||
var rows = await _dbContext.Set<Models.Viewer>()
|
||||
.AsNoTracking()
|
||||
.Include(v => v.Info.SelectedEmblem)
|
||||
.Include(v => v.Info.SelectedDegree)
|
||||
.Where(v => viewerIds.Contains(v.Id))
|
||||
.Select(v => new
|
||||
{
|
||||
v.Id,
|
||||
v.DisplayName,
|
||||
EmblemId = v.Info.SelectedEmblem != null ? v.Info.SelectedEmblem.Id : 100_000_000L,
|
||||
CountryCode = v.Info.CountryCode ?? "",
|
||||
DegreeId = v.Info.SelectedDegree != null ? (int)v.Info.SelectedDegree.Id : 0,
|
||||
IsOfficialMarkDisplayed = v.Info.IsOfficialMarkDisplayed,
|
||||
})
|
||||
.ToListAsync(ct);
|
||||
return rows.ToDictionary(
|
||||
r => r.Id,
|
||||
r => new GuildMemberProfile(
|
||||
Name: r.DisplayName,
|
||||
EmblemId: r.EmblemId,
|
||||
CountryCode: r.CountryCode,
|
||||
Rank: 1, // TODO: real rank when rank tracking lands
|
||||
DegreeId: r.DegreeId,
|
||||
IsOfficialMarkDisplayed: r.IsOfficialMarkDisplayed));
|
||||
}
|
||||
|
||||
public async Task<long> GetEquippedEmblemIdAsync(long viewerId, CancellationToken ct = default)
|
||||
{
|
||||
var row = await _dbContext.Set<Models.Viewer>()
|
||||
.AsNoTracking()
|
||||
.Include(v => v.Info.SelectedEmblem)
|
||||
.Where(v => v.Id == viewerId)
|
||||
.Select(v => new { EmblemId = v.Info.SelectedEmblem != null ? v.Info.SelectedEmblem.Id : 100_000_000L })
|
||||
.FirstOrDefaultAsync(ct);
|
||||
return row?.EmblemId ?? 100_000_000L;
|
||||
}
|
||||
|
||||
public async Task<List<long>> GetEmblemListAsync(long viewerId, CancellationToken ct = default)
|
||||
{
|
||||
var viewer = await _dbContext.Set<Models.Viewer>()
|
||||
.AsNoTracking()
|
||||
.Include(v => v.Emblems)
|
||||
.Where(v => v.Id == viewerId)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
return viewer?.Emblems.Select(e => (long)e.Id).ToList() ?? new List<long>();
|
||||
}
|
||||
|
||||
private async Task<Models.Viewer> BuildDefaultViewer(string displayName, int initialTutorialState = 1)
|
||||
{
|
||||
Models.Viewer viewer = new Models.Viewer
|
||||
|
||||
@@ -520,6 +520,9 @@ public class SVSimDbContext : DbContext
|
||||
e.Property(g => g.Name).HasMaxLength(64);
|
||||
e.Property(g => g.Description).HasMaxLength(512);
|
||||
e.HasIndex(g => g.Name); // name search
|
||||
e.HasOne<Viewer>().WithMany()
|
||||
.HasForeignKey(g => g.LeaderViewerId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<GuildMember>(e =>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using SVSim.Database.Entities.Guild;
|
||||
using SVSim.Database.Models.Config;
|
||||
using SVSim.Database.Repositories.Guild;
|
||||
@@ -67,9 +68,9 @@ public sealed class GuildService : IGuildService
|
||||
var rows = await _guilds.SearchAsync(name ?? "", activity, joinCondition, memberBucket, cfg.MaxMemberNum, cfg.SearchResultCap, ct);
|
||||
var guildIds = rows.Select(r => r.GuildId).ToList();
|
||||
var memberCounts = await _members.CountBatchByGuildIdsAsync(guildIds, ct);
|
||||
var leaderIds = rows.Select(r => r.LeaderViewerId).Distinct().ToList();
|
||||
var leaderIds = rows.Select(r => r.LeaderViewerId).Where(id => id.HasValue).Select(id => id!.Value).Distinct().ToList();
|
||||
var leaderNames = await _viewers.LoadDisplayNamesAsync(leaderIds, ct);
|
||||
return rows.Select(r => new GuildSearchEntry(r, memberCounts.GetValueOrDefault(r.GuildId, 0), leaderNames.GetValueOrDefault(r.LeaderViewerId, ""))).ToList();
|
||||
return rows.Select(r => new GuildSearchEntry(r, memberCounts.GetValueOrDefault(r.GuildId, 0), leaderNames.GetValueOrDefault(r.LeaderViewerId ?? 0L, ""))).ToList();
|
||||
}
|
||||
|
||||
public async Task<GuildOpResult> CreateAsync(long viewerId, CreateGuildRequest req, CancellationToken ct = default)
|
||||
@@ -86,10 +87,9 @@ public sealed class GuildService : IGuildService
|
||||
if (await _guilds.NameExistsAsync(req.Name, ct))
|
||||
return new(GuildOpResultCode.NameTaken);
|
||||
|
||||
var viewer = await _db.Viewers
|
||||
.Include(v => v.Info.SelectedEmblem)
|
||||
.FirstOrDefaultAsync(v => v.Id == viewerId, ct);
|
||||
if (viewer is null) return new(GuildOpResultCode.PermissionDenied);
|
||||
// Load the viewer's equipped emblem to use as the guild's initial emblem.
|
||||
// If the viewer doesn't exist (shouldn't happen post-auth), returns 100_000_000 default.
|
||||
long viewerEmblemId = await _viewers.GetEquippedEmblemIdAsync(viewerId, ct);
|
||||
|
||||
var id = await _idGen.NextAsync(
|
||||
(cand, c) => _guilds.GetByIdAsync(cand, c).ContinueWith(t => t.Result is not null, c),
|
||||
@@ -101,7 +101,7 @@ public sealed class GuildService : IGuildService
|
||||
Name = req.Name,
|
||||
Description = "",
|
||||
LeaderViewerId = viewerId,
|
||||
EmblemId = DefaultEmblemId(viewer),
|
||||
EmblemId = viewerEmblemId,
|
||||
Activity = (GuildActivity)req.Activity,
|
||||
JoinCondition = (GuildJoinCondition)req.JoinCondition,
|
||||
CreatedAt = now,
|
||||
@@ -116,22 +116,12 @@ public sealed class GuildService : IGuildService
|
||||
await _joinRequests.CancelPendingForViewerAsync(viewerId, now, ct);
|
||||
|
||||
// Update Viewer.GuildId pointer for quick lookups.
|
||||
viewer.GuildId = id;
|
||||
await _db.SaveChangesAsync(ct);
|
||||
await _viewers.SetGuildIdAsync(viewerId, id, ct);
|
||||
|
||||
await _chat.EmitSystemEventAsync(id, viewerId, GuildChatMessageType.CreateGuild, body: null, ct);
|
||||
return new(GuildOpResultCode.Ok, GuildId: id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the viewer's currently-equipped emblem id, or 100000000 (default) if none.
|
||||
/// </summary>
|
||||
private static long DefaultEmblemId(Models.Viewer viewer)
|
||||
{
|
||||
var emblemId = viewer.Info?.SelectedEmblem?.Id;
|
||||
return emblemId is > 0 ? emblemId.Value : 100_000_000L;
|
||||
}
|
||||
|
||||
public async Task<GuildOpResult> UpdateAsync(long viewerId, UpdateGuildRequest req, CancellationToken ct = default)
|
||||
{
|
||||
var m = await _members.GetMembershipAsync(viewerId, ct);
|
||||
@@ -177,17 +167,33 @@ public sealed class GuildService : IGuildService
|
||||
if (membership is null) return new(GuildOpResultCode.NotInGuild);
|
||||
if (membership.Role != GuildRole.Leader) return new(GuildOpResultCode.PermissionDenied);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var members = await _members.ListByGuildAsync(membership.GuildId, ct);
|
||||
foreach (var m in members) await _viewers.ClearGuildIdAsync(m.ViewerId, ct);
|
||||
IDbContextTransaction? tx = null;
|
||||
try
|
||||
{
|
||||
try { tx = await _db.Database.BeginTransactionAsync(ct); } catch (InvalidOperationException) { /* InMemory provider */ }
|
||||
|
||||
await _chatMessages.DeleteAllForGuildAsync(membership.GuildId, ct);
|
||||
await _invites.DeleteAllForGuildAsync(membership.GuildId, ct);
|
||||
await _joinRequests.DeleteAllForGuildAsync(membership.GuildId, ct);
|
||||
await _members.DeleteAllForGuildAsync(membership.GuildId, ct);
|
||||
await _guilds.MarkBrokenUpAsync(membership.GuildId, now, ct);
|
||||
var now = DateTime.UtcNow;
|
||||
var members = await _members.ListByGuildAsync(membership.GuildId, ct);
|
||||
foreach (var m in members) await _viewers.ClearGuildIdAsync(m.ViewerId, ct);
|
||||
|
||||
return GuildOpResult.Ok;
|
||||
await _chatMessages.DeleteAllForGuildAsync(membership.GuildId, ct);
|
||||
await _invites.DeleteAllForGuildAsync(membership.GuildId, ct);
|
||||
await _joinRequests.DeleteAllForGuildAsync(membership.GuildId, ct);
|
||||
await _members.DeleteAllForGuildAsync(membership.GuildId, ct);
|
||||
await _guilds.MarkBrokenUpAsync(membership.GuildId, now, ct);
|
||||
|
||||
if (tx != null) await tx.CommitAsync(ct);
|
||||
return GuildOpResult.Ok;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (tx != null) await tx.RollbackAsync(ct);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
tx?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<GuildOpResult> InviteAsync(long callerViewerId, long targetViewerId, CancellationToken ct = default)
|
||||
@@ -305,12 +311,31 @@ public sealed class GuildService : IGuildService
|
||||
|
||||
private async Task CommitJoinAsync(long viewerId, int guildId, DateTime now, CancellationToken ct)
|
||||
{
|
||||
await _members.AddAsync(
|
||||
new GuildMember { GuildId = guildId, ViewerId = viewerId, Role = GuildRole.Regular, JoinedAt = now },
|
||||
ct);
|
||||
await _viewers.SetGuildIdAsync(viewerId, guildId, ct);
|
||||
await _invites.ConsumePendingForViewerAsync(viewerId, now, ct);
|
||||
await _joinRequests.CancelPendingForViewerAsync(viewerId, now, ct);
|
||||
IDbContextTransaction? tx = null;
|
||||
try
|
||||
{
|
||||
try { tx = await _db.Database.BeginTransactionAsync(ct); } catch (InvalidOperationException) { /* InMemory provider */ }
|
||||
|
||||
await _members.AddAsync(
|
||||
new GuildMember { GuildId = guildId, ViewerId = viewerId, Role = GuildRole.Regular, JoinedAt = now },
|
||||
ct);
|
||||
await _viewers.SetGuildIdAsync(viewerId, guildId, ct);
|
||||
await _invites.ConsumePendingForViewerAsync(viewerId, now, ct);
|
||||
await _joinRequests.CancelPendingForViewerAsync(viewerId, now, ct);
|
||||
|
||||
if (tx != null) await tx.CommitAsync(ct);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (tx != null) await tx.RollbackAsync(ct);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
tx?.Dispose();
|
||||
}
|
||||
|
||||
// Emit chat event outside the transaction — chat failure doesn't roll back the join.
|
||||
await _chat.EmitSystemEventAsync(guildId, viewerId, GuildChatMessageType.Join, body: null, ct);
|
||||
}
|
||||
|
||||
@@ -470,28 +495,20 @@ public sealed class GuildService : IGuildService
|
||||
if (pendingInvites.Count == 0) return Array.Empty<GuildOutgoingInviteEntry>();
|
||||
|
||||
var inviteeIds = pendingInvites.Select(i => i.InviteeViewerId).Distinct().ToList();
|
||||
var names = await _viewers.LoadDisplayNamesAsync(inviteeIds, ct);
|
||||
|
||||
// Load emblem + degree data for invitees.
|
||||
var viewerRows = await _db.Viewers
|
||||
.AsNoTracking()
|
||||
.Include(v => v.Info.SelectedEmblem)
|
||||
.Include(v => v.Info.SelectedDegree)
|
||||
.Where(v => inviteeIds.Contains(v.Id))
|
||||
.ToDictionaryAsync(v => v.Id, ct);
|
||||
var profiles = await _viewers.LoadGuildProfileBatchAsync(inviteeIds, ct);
|
||||
|
||||
var result = new List<GuildOutgoingInviteEntry>(pendingInvites.Count);
|
||||
foreach (var invite in pendingInvites)
|
||||
{
|
||||
viewerRows.TryGetValue(invite.InviteeViewerId, out var v);
|
||||
profiles.TryGetValue(invite.InviteeViewerId, out var p);
|
||||
result.Add(new GuildOutgoingInviteEntry(
|
||||
InviteId: invite.Id,
|
||||
InviteeViewerId: invite.InviteeViewerId,
|
||||
Name: names.GetValueOrDefault(invite.InviteeViewerId, ""),
|
||||
EmblemId: v?.Info?.SelectedEmblem?.Id is > 0 ? v.Info.SelectedEmblem.Id : 100_000_000L,
|
||||
CountryCode: v?.Info?.CountryCode ?? "",
|
||||
Rank: 1,
|
||||
DegreeId: v?.Info?.SelectedDegree?.Id ?? 0,
|
||||
Name: p?.Name ?? "",
|
||||
EmblemId: p?.EmblemId ?? 100_000_000L,
|
||||
CountryCode: p?.CountryCode ?? "",
|
||||
Rank: p?.Rank ?? 1,
|
||||
DegreeId: p?.DegreeId ?? 0,
|
||||
CreatedAt: invite.CreatedAt));
|
||||
}
|
||||
return result;
|
||||
@@ -505,14 +522,14 @@ public sealed class GuildService : IGuildService
|
||||
|
||||
var guildIds = pendingInvites.Select(i => i.GuildId).Distinct().ToList();
|
||||
var memberCounts = await _members.CountBatchByGuildIdsAsync(guildIds, ct);
|
||||
var leaderIds = pendingInvites.Select(i => i.Guild.LeaderViewerId).Distinct().ToList();
|
||||
var leaderIds = pendingInvites.Select(i => i.Guild.LeaderViewerId).Where(id => id.HasValue).Select(id => id!.Value).Distinct().ToList();
|
||||
var leaderNames = await _viewers.LoadDisplayNamesAsync(leaderIds, ct);
|
||||
|
||||
return pendingInvites.Select(invite => new GuildReceivedInviteEntry(
|
||||
InviteId: invite.Id,
|
||||
Guild: invite.Guild,
|
||||
MemberNum: memberCounts.GetValueOrDefault(invite.GuildId, 0),
|
||||
LeaderName: leaderNames.GetValueOrDefault(invite.Guild.LeaderViewerId, ""))).ToList();
|
||||
LeaderName: leaderNames.GetValueOrDefault(invite.Guild.LeaderViewerId ?? 0L, ""))).ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<GuildJoinRequestEntry>> ListPendingJoinRequestsForMyGuildAsync(long viewerId, CancellationToken ct = default)
|
||||
@@ -527,26 +544,19 @@ public sealed class GuildService : IGuildService
|
||||
if (requests.Count == 0) return Array.Empty<GuildJoinRequestEntry>();
|
||||
|
||||
var applicantIds = requests.Select(r => r.ViewerId).Distinct().ToList();
|
||||
var names = await _viewers.LoadDisplayNamesAsync(applicantIds, ct);
|
||||
|
||||
var viewerRows = await _db.Viewers
|
||||
.AsNoTracking()
|
||||
.Include(v => v.Info.SelectedEmblem)
|
||||
.Include(v => v.Info.SelectedDegree)
|
||||
.Where(v => applicantIds.Contains(v.Id))
|
||||
.ToDictionaryAsync(v => v.Id, ct);
|
||||
var profiles = await _viewers.LoadGuildProfileBatchAsync(applicantIds, ct);
|
||||
|
||||
return requests.Select(r =>
|
||||
{
|
||||
viewerRows.TryGetValue(r.ViewerId, out var v);
|
||||
profiles.TryGetValue(r.ViewerId, out var p);
|
||||
return new GuildJoinRequestEntry(
|
||||
ApplicantViewerId: r.ViewerId,
|
||||
Name: names.GetValueOrDefault(r.ViewerId, ""),
|
||||
EmblemId: v?.Info?.SelectedEmblem?.Id is > 0 ? v.Info.SelectedEmblem.Id : 100_000_000L,
|
||||
CountryCode: v?.Info?.CountryCode ?? "",
|
||||
Rank: 1,
|
||||
DegreeId: v?.Info?.SelectedDegree?.Id ?? 0,
|
||||
IsOfficialMarkDisplayed: v?.Info?.IsOfficialMarkDisplayed == true,
|
||||
Name: p?.Name ?? "",
|
||||
EmblemId: p?.EmblemId ?? 100_000_000L,
|
||||
CountryCode: p?.CountryCode ?? "",
|
||||
Rank: p?.Rank ?? 1,
|
||||
DegreeId: p?.DegreeId ?? 0,
|
||||
IsOfficialMarkDisplayed: p?.IsOfficialMarkDisplayed ?? false,
|
||||
RequestedAt: r.CreatedAt);
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user