From adffbf2005ac34177b0eec0ae49a70f809427843 Mon Sep 17 00:00:00 2001 From: Blank Date: Tue, 15 Sep 2026 23:45:57 +0800 Subject: [PATCH] =?UTF-8?q?fix(online):=20=E6=8B=86=E8=A7=A3=E7=BE=A4?= =?UTF-8?q?=E7=BB=84=E6=9C=8D=E5=8A=A1=E9=82=80=E8=AF=B7=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E9=99=8D=E4=BD=8E=E8=AE=A4=E7=9F=A5=E5=A4=8D?= =?UTF-8?q?=E6=9D=82=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InviteAsync/RevokeInviteAsync/AnswerInviteAsync 三个方法的 CAS 变更 lambda 判据拆为私有方法 TryAppendInvite/TryRevokeInvite/ TryAnswerInvite,超期就地终结分支拆为 ExpireStaleInviteAsync; 判据顺序、错误码与幂等收敛语义逐字保持,消除 Sonar S3776 三条 CRITICAL 超标(17/16/31 → 13/11/14)。 Linear: GFX-800 --- .../Social/OnlineGroupService.cs | 223 +++++++++++------- 1 file changed, 135 insertions(+), 88 deletions(-) diff --git a/GameFrameX.Online/Social/OnlineGroupService.cs b/GameFrameX.Online/Social/OnlineGroupService.cs index 64f253bc..d61e4e18 100644 --- a/GameFrameX.Online/Social/OnlineGroupService.cs +++ b/GameFrameX.Online/Social/OnlineGroupService.cs @@ -201,23 +201,7 @@ public async Task> InviteAsync(OnlineScope scope RespondedAtTime = 0, }; - var saved = await ReplaceWithReplayAsync(group, (copy) => - { - if (copy.State != OnlineGroupState.Active) - { - // 重放快照已被解散:不再追加邀请(否则邀请会挂在一个已结束的群上)。 - return false; - } - - // 重放快照可能已有并发写入的同向邀请:已在则不再追加第二条(同一被邀请人至多一条待答复)。 - if (FindPendingInvite(copy, inviteeId) != null) - { - return false; - } - - copy.Invites.Add(invite); - return true; - }, cancellationToken).ConfigureAwait(false); + var saved = await ReplaceWithReplayAsync(group, (copy) => TryAppendInvite(copy, invite, inviteeId), cancellationToken).ConfigureAwait(false); if (saved == null) { @@ -332,24 +316,7 @@ public async Task> RevokeInviteAsync(OnlineScope scope } var now = Now(); - var saved = await ReplaceWithReplayAsync(group, (copy) => - { - if (copy.State != OnlineGroupState.Active) - { - return false; - } - - var pending = FindInvite(copy, inviteId); - if (pending == null || pending.State != OnlineGroupInviteState.Pending) - { - // 重放快照已被并发答复:不改写(调用方按当前事实收敛)。 - return false; - } - - pending.State = OnlineGroupInviteState.Revoked; - pending.RespondedAtTime = now; - return true; - }, cancellationToken).ConfigureAwait(false); + var saved = await ReplaceWithReplayAsync(group, (copy) => TryRevokeInvite(copy, inviteId, now), cancellationToken).ConfigureAwait(false); if (saved == null) { @@ -866,25 +833,7 @@ private async Task> AnswerInviteAsync(OnlineScope scop { // 超期但尚未被扫描到的邀请就地终结:接受会新增成员,绝不能放过期邀请入群 // (拒绝不校验有效期——拒绝不改变成员集合,被拒与过期同属「未入群」终态)。 - var stale = await ReplaceWithReplayAsync(group, (copy) => - { - var expired = FindInvite(copy, inviteId); - if (expired == null || expired.State != OnlineGroupInviteState.Pending) - { - // 已被并发答复/撤销:无需就地终结。 - return false; - } - - expired.State = OnlineGroupInviteState.Expired; - return true; - }, cancellationToken).ConfigureAwait(false); - - if (stale != null) - { - await PublishGroupChangedAsync(stale, OnlineSocialEvents.GroupInviteChanged, correlationId, cancellationToken).ConfigureAwait(false); - } - - return OnlineResult.Fail(OnlineErrorCode.StateEnded, "邀请已过期"); + return await ExpireStaleInviteAsync(group, inviteId, correlationId, cancellationToken).ConfigureAwait(false); } if (accept && group.Members.Count >= group.MaxMembers) @@ -892,52 +841,52 @@ private async Task> AnswerInviteAsync(OnlineScope scop return OnlineResult.Fail(OnlineErrorCode.StateOperationForbidden, "群组成员数已达上限"); } - var saved = await ReplaceWithReplayAsync(group, (copy) => + var saved = await ReplaceWithReplayAsync(group, (copy) => TryAnswerInvite(copy, inviteId, scope.PlayerId, accept, target, now), cancellationToken).ConfigureAwait(false); + + if (saved == null) { - if (copy.State != OnlineGroupState.Active) - { - // 重放快照已被解散:绝不把成员加回一个已结束的群(前置判据是针对旧快照做的,此处必须再判)。 - return false; - } + // 两轮 CAS 均失败:重读一次按当前事实收敛(多半已被并发答复抢先)。 + return await ConvergeOnCurrentAsync(scope, groupId, cancellationToken).ConfigureAwait(false); + } - var pending = FindInvite(copy, inviteId); - if (pending == null || pending.State != OnlineGroupInviteState.Pending) - { - // 重放快照已被并发答复/撤销:不改写(调用方按当前事实收敛)。 - return false; - } + var action = accept ? OnlineSocialEvents.GroupMemberJoined : OnlineSocialEvents.GroupInviteChanged; + await PublishGroupChangedAsync(saved, action, correlationId, cancellationToken).ConfigureAwait(false); + return OnlineResult.Ok(saved); + } - if (accept && !copy.Contains(scope.PlayerId)) + /// + /// 就地终结一条已超期且尚未被扫描到的待答复邀请( 接受路径专用)。 + /// + /// 维护约束:接受会新增成员,绝不能放过期邀请入群;拒绝不校验有效期——拒绝不改变成员集合, + /// 被拒与过期同属「未入群」终态,故本方法只在接受路径调用。 + /// + /// + /// 答复依据的群记录快照。 + /// 邀请标识。 + /// 关联标识(可空)。 + /// 取消令牌。 + /// 恒为「邀请已过期」错误结果(终结落库成功才外发邀请变更事件)。 + private async Task> ExpireStaleInviteAsync(OnlineGroup group, string inviteId, string correlationId, CancellationToken cancellationToken) + { + var stale = await ReplaceWithReplayAsync(group, (copy) => + { + var expired = FindInvite(copy, inviteId); + if (expired == null || expired.State != OnlineGroupInviteState.Pending) { - if (copy.Members.Count >= copy.MaxMembers) - { - // 重放快照可能已被并发入群顶到上限:接受不得越过上限(同上,判据必须落在新快照上)。 - // 已在成员集合中的被邀请人不受此限——那一步不新增成员。 - return false; - } - - copy.Members.Add(new OnlineGroupMember - { - PlayerId = scope.PlayerId, - Role = OnlineGroupRole.Member, - JoinedAtTime = now, - }); + // 已被并发答复/撤销:无需就地终结。 + return false; } - pending.State = target; - pending.RespondedAtTime = now; + expired.State = OnlineGroupInviteState.Expired; return true; }, cancellationToken).ConfigureAwait(false); - if (saved == null) + if (stale != null) { - // 两轮 CAS 均失败:重读一次按当前事实收敛(多半已被并发答复抢先)。 - return await ConvergeOnCurrentAsync(scope, groupId, cancellationToken).ConfigureAwait(false); + await PublishGroupChangedAsync(stale, OnlineSocialEvents.GroupInviteChanged, correlationId, cancellationToken).ConfigureAwait(false); } - var action = accept ? OnlineSocialEvents.GroupMemberJoined : OnlineSocialEvents.GroupInviteChanged; - await PublishGroupChangedAsync(saved, action, correlationId, cancellationToken).ConfigureAwait(false); - return OnlineResult.Ok(saved); + return OnlineResult.Fail(OnlineErrorCode.StateEnded, "邀请已过期"); } /// @@ -998,6 +947,104 @@ private async Task CommitAsync(OnlineGroup snapshot, Func + /// 在重放快照副本上追加一条入群邀请( 的 CAS 变更判据)。 + /// + /// 重放得到的群记录副本。 + /// 待追加的邀请条目。 + /// 被邀请玩家标识。 + /// 已追加返回 true;重放快照已解散或已有同被邀请人待答复邀请返回 false(无事可做即不提交)。 + private static bool TryAppendInvite(OnlineGroup copy, OnlineGroupInvite invite, long inviteeId) + { + if (copy.State != OnlineGroupState.Active) + { + // 重放快照已被解散:不再追加邀请(否则邀请会挂在一个已结束的群上)。 + return false; + } + + // 重放快照可能已有并发写入的同向邀请:已在则不再追加第二条(同一被邀请人至多一条待答复)。 + if (FindPendingInvite(copy, inviteeId) != null) + { + return false; + } + + copy.Invites.Add(invite); + return true; + } + + /// + /// 在重放快照副本上撤销一条待答复邀请( 的 CAS 变更判据)。 + /// + /// 重放得到的群记录副本。 + /// 邀请标识。 + /// 当前 UTC 毫秒时刻(回填答复时刻用)。 + /// 已撤销返回 true;重放快照已解散或邀请已被并发答复/撤销返回 false(无事可做即不提交)。 + private static bool TryRevokeInvite(OnlineGroup copy, string inviteId, long nowUnixMilliseconds) + { + if (copy.State != OnlineGroupState.Active) + { + return false; + } + + var pending = FindInvite(copy, inviteId); + if (pending == null || pending.State != OnlineGroupInviteState.Pending) + { + // 重放快照已被并发答复:不改写(调用方按当前事实收敛)。 + return false; + } + + pending.State = OnlineGroupInviteState.Revoked; + pending.RespondedAtTime = nowUnixMilliseconds; + return true; + } + + /// + /// 在重放快照副本上答复一条待答复邀请( 的 CAS 变更判据;接受时按需追加成员)。 + /// + /// 重放得到的群记录副本。 + /// 邀请标识。 + /// 被邀请玩家(答复人)标识。 + /// 接受传 true,拒绝传 false。 + /// 答复目标终态(Accepted / Rejected)。 + /// 当前 UTC 毫秒时刻(入群时刻与答复时刻用)。 + /// 已答复返回 true;重放快照已解散、邀请已被并发答复/撤销或接受越过成员上限返回 false(无事可做即不提交)。 + private static bool TryAnswerInvite(OnlineGroup copy, string inviteId, long playerId, bool accept, OnlineGroupInviteState target, long nowUnixMilliseconds) + { + if (copy.State != OnlineGroupState.Active) + { + // 重放快照已被解散:绝不把成员加回一个已结束的群(前置判据是针对旧快照做的,此处必须再判)。 + return false; + } + + var pending = FindInvite(copy, inviteId); + if (pending == null || pending.State != OnlineGroupInviteState.Pending) + { + // 重放快照已被并发答复/撤销:不改写(调用方按当前事实收敛)。 + return false; + } + + if (accept && !copy.Contains(playerId)) + { + if (copy.Members.Count >= copy.MaxMembers) + { + // 重放快照可能已被并发入群顶到上限:接受不得越过上限(同上,判据必须落在新快照上)。 + // 已在成员集合中的被邀请人不受此限——那一步不新增成员。 + return false; + } + + copy.Members.Add(new OnlineGroupMember + { + PlayerId = playerId, + Role = OnlineGroupRole.Member, + JoinedAtTime = nowUnixMilliseconds, + }); + } + + pending.State = target; + pending.RespondedAtTime = nowUnixMilliseconds; + return true; + } + /// /// CAS 失败后的收敛出口:重读群记录并按当前事实返回成功结果。 ///