fix(check): merge anonymous resignup viewer into Steam-linked viewer
GameStart already detects the Steam-vs-UDID mismatch produced by wipe-and-resignup; it now also reclaims the orphan. New ViewerRepository.MergeAnonymousViewerInto transfers the fresh UDID from V_new onto V_old in one save (freeing the unique-index slot), then deletes V_new in a second save. Partial-failure mode is a benign null-UDID viewer; two rows never contend for the same UDID. Side benefit: future GetViewerByUdid lookups now short-circuit to V_old without going through the Steam handler. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -13,4 +13,11 @@ public interface IViewerRepository
|
||||
ulong socialAccountIdentifier, ulong? shortUdid = null);
|
||||
Task<Models.Viewer> RegisterAnonymousViewer(Guid udid);
|
||||
Task LinkSteamToViewer(long viewerId, ulong steamId);
|
||||
|
||||
/// <summary>
|
||||
/// Merges an anonymous viewer (just created by <c>/tool/signup</c> on a fresh UDID)
|
||||
/// into a target viewer that the Steam ticket resolved to. Transfers the anonymous
|
||||
/// viewer's UDID to the target, then deletes the anonymous viewer.
|
||||
/// </summary>
|
||||
Task MergeAnonymousViewerInto(long anonymousViewerId, long targetViewerId);
|
||||
}
|
||||
|
||||
@@ -182,6 +182,33 @@ public class ViewerRepository : IViewerRepository
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task MergeAnonymousViewerInto(long anonymousViewerId, long targetViewerId)
|
||||
{
|
||||
if (anonymousViewerId == targetViewerId) return;
|
||||
|
||||
var anon = await _dbContext.Set<Models.Viewer>()
|
||||
.FirstOrDefaultAsync(v => v.Id == anonymousViewerId);
|
||||
if (anon is null) return;
|
||||
|
||||
var target = await _dbContext.Set<Models.Viewer>()
|
||||
.FirstOrDefaultAsync(v => v.Id == targetViewerId)
|
||||
?? throw new InvalidOperationException(
|
||||
$"Cannot merge anonymous viewer {anonymousViewerId}: target viewer {targetViewerId} not found.");
|
||||
|
||||
// Two saves: free the UDID slot on the anonymous viewer first (drops the unique-index
|
||||
// conflict), then reassign to the target and delete the anonymous row in the second
|
||||
// save. The partial-failure mode (first save succeeds, second fails) leaves a benign
|
||||
// null-UDID viewer that no client can resolve to — never two rows contending for the
|
||||
// same UDID, which is the failure we actually need to prevent.
|
||||
Guid? freedUdid = anon.Udid;
|
||||
anon.Udid = null;
|
||||
await _dbContext.SaveChangesAsync();
|
||||
|
||||
target.Udid = freedUdid;
|
||||
_dbContext.Set<Models.Viewer>().Remove(anon);
|
||||
await _dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task LinkSteamToViewer(long viewerId, ulong steamId)
|
||||
{
|
||||
var viewer = await _dbContext.Set<Models.Viewer>()
|
||||
|
||||
Reference in New Issue
Block a user