diff --git a/src/WxAgent.Core/AccountBindingMatcher.cs b/src/WxAgent.Core/AccountBindingMatcher.cs new file mode 100644 index 0000000..07ba4b6 --- /dev/null +++ b/src/WxAgent.Core/AccountBindingMatcher.cs @@ -0,0 +1,33 @@ +namespace WxAgent.Core; + +public sealed record UiAccountIdentity(string? WechatId, string? Nickname); +public sealed record DatabaseAccountIdentity(string AccountId, string? WechatId, string? Nickname); +public sealed record AccountMatch(string? AccountId, string Strategy, bool IsUnique) +{ + public bool IsMatched => AccountId is not null && IsUnique; +} + +public static class AccountBindingMatcher +{ + public static AccountMatch Match(UiAccountIdentity ui, IReadOnlyList accounts) + { + ArgumentNullException.ThrowIfNull(ui); + ArgumentNullException.ThrowIfNull(accounts); + var wechatId = Normalize(ui.WechatId); + if (wechatId is not null) + { + var matches = accounts.Where(a => string.Equals(Normalize(a.WechatId), wechatId, StringComparison.OrdinalIgnoreCase)).ToArray(); + if (matches.Length == 1) return new AccountMatch(matches[0].AccountId, "wechatId", true); + if (matches.Length > 1) return new AccountMatch(null, "wechatId", false); + } + + var nickname = Normalize(ui.Nickname); + if (nickname is null) return new AccountMatch(null, "none", false); + var nicknameMatches = accounts.Where(a => string.Equals(Normalize(a.Nickname), nickname, StringComparison.Ordinal)).ToArray(); + return nicknameMatches.Length == 1 + ? new AccountMatch(nicknameMatches[0].AccountId, "nickname", true) + : new AccountMatch(null, nicknameMatches.Length == 0 ? "none" : "nickname", false); + } + + private static string? Normalize(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim(); +} diff --git a/tests/WxAgent.Core.Tests/AccountBindingMatcherTests.cs b/tests/WxAgent.Core.Tests/AccountBindingMatcherTests.cs new file mode 100644 index 0000000..aa6bc07 --- /dev/null +++ b/tests/WxAgent.Core.Tests/AccountBindingMatcherTests.cs @@ -0,0 +1,33 @@ +using WxAgent.Core; +using Xunit; + +namespace WxAgent.Core.Tests; + +public sealed class AccountBindingMatcherTests +{ + [Fact] + public void PrefersExactWechatIdOverNickname() + { + var result = AccountBindingMatcher.Match(new UiAccountIdentity("wxid-a", "Same"), + [new("db-1", "wxid-a", "Other"), new("db-2", "wxid-b", "Same")]); + Assert.True(result.IsMatched); Assert.Equal("db-1", result.AccountId); Assert.Equal("wechatId", result.Strategy); + } + + [Fact] + public void UsesUniqueNicknameOnlyWhenWechatIdIsUnavailable() + { + var result = AccountBindingMatcher.Match(new UiAccountIdentity(null, " Alice "), + [new("db-1", null, "Alice"), new("db-2", null, "Bob")]); + Assert.True(result.IsMatched); Assert.Equal("db-1", result.AccountId); Assert.Equal("nickname", result.Strategy); + } + + [Fact] + public void RejectsDuplicateNicknameAndConflictingDuplicateWechatId() + { + var duplicateName = AccountBindingMatcher.Match(new UiAccountIdentity(null, "Alice"), + [new("db-1", null, "Alice"), new("db-2", null, "Alice")]); + var duplicateId = AccountBindingMatcher.Match(new UiAccountIdentity("wxid-a", "Alice"), + [new("db-1", "wxid-a", "One"), new("db-2", "wxid-a", "Two")]); + Assert.False(duplicateName.IsMatched); Assert.False(duplicateId.IsMatched); + } +}