using System.Net.Http.Json; using System.Text.Json; using Tiku.Application.Auth; namespace Tiku.Infrastructure.Auth; public sealed class WechatOAuthClient(HttpClient httpClient) : IWechatOAuthClient { private static readonly Uri WebAccessTokenEndpoint = new("https://api.weixin.qq.com/sns/oauth2/access_token"); private static readonly Uri WebUserInfoEndpoint = new("https://api.weixin.qq.com/sns/userinfo"); private static readonly Uri MiniAppCode2SessionEndpoint = new("https://api.weixin.qq.com/sns/jscode2session"); public async Task ExchangeWebCodeAsync( WechatProviderOptions options, string code, CancellationToken cancellationToken = default) { var tokenUri = BuildUri(WebAccessTokenEndpoint, new Dictionary { ["appid"] = options.AppId, ["secret"] = options.AppSecret, ["code"] = code, ["grant_type"] = "authorization_code" }); using var tokenDocument = await GetWechatJsonAsync(tokenUri, cancellationToken); var token = tokenDocument.RootElement; var accessToken = RequiredString(token, "access_token", "wechat_access_token_missing"); var openId = RequiredString(token, "openid", "wechat_openid_missing"); var unionId = OptionalString(token, "unionid"); var userInfoUri = BuildUri(WebUserInfoEndpoint, new Dictionary { ["access_token"] = accessToken, ["openid"] = openId, ["lang"] = "zh_CN" }); using var userDocument = await GetWechatJsonAsync(userInfoUri, cancellationToken); var user = userDocument.RootElement; return new WechatIdentity( openId, OptionalString(user, "unionid") ?? unionId, OptionalString(user, "nickname"), OptionalString(user, "headimgurl"), null, user.GetRawText()); } public async Task ExchangeMiniAppCodeAsync( WechatProviderOptions options, string code, CancellationToken cancellationToken = default) { var uri = BuildUri(MiniAppCode2SessionEndpoint, new Dictionary { ["appid"] = options.AppId, ["secret"] = options.AppSecret, ["js_code"] = code, ["grant_type"] = "authorization_code" }); using var document = await GetWechatJsonAsync(uri, cancellationToken); var root = document.RootElement; return new WechatIdentity( RequiredString(root, "openid", "wechat_openid_missing"), OptionalString(root, "unionid"), null, null, RequiredString(root, "session_key", "wechat_session_key_missing"), root.GetRawText()); } private async Task GetWechatJsonAsync( Uri uri, CancellationToken cancellationToken) { using var response = await httpClient.GetAsync(uri, cancellationToken); response.EnsureSuccessStatusCode(); var document = await response.Content.ReadFromJsonAsync(cancellationToken) ?? throw new InvalidCredentialsException("wechat_empty_response"); if (document.RootElement.TryGetProperty("errcode", out var errcode) && errcode.ValueKind == JsonValueKind.Number && errcode.GetInt32() != 0) { throw new InvalidCredentialsException("wechat_code_exchange_failed"); } return document; } private static Uri BuildUri(Uri endpoint, IReadOnlyDictionary query) { var builder = new UriBuilder(endpoint); builder.Query = string.Join( '&', query.Select(pair => $"{Uri.EscapeDataString(pair.Key)}={Uri.EscapeDataString(pair.Value)}")); return builder.Uri; } private static string RequiredString(JsonElement element, string property, string errorCode) { return OptionalString(element, property) ?? throw new InvalidCredentialsException(errorCode); } private static string? OptionalString(JsonElement element, string property) { return element.TryGetProperty(property, out var value) && value.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(value.GetString()) ? value.GetString()!.Trim() : null; } }