/// <summary>
        /// code -> oauth.access_token + openid
        /// </summary>
        /// <param name="code">用于换取网页授权access_token。此code只能使用一次,5分钟未被使用自动过期。</param>
        /// <param name="redirectUri"></param>
        /// <returns></returns>
        protected virtual async Task <WeixinOAuthTokenResponse> CustomExchangeCodeAsync(string code, string redirectUri)
        {
            var query = new QueryBuilder()
            {
                { "appid", Options.AppId },
                { "secret", Options.AppSecret },
                { "code", code },
                { "grant_type", "authorization_code" },
                { "redirect_uri", redirectUri }
            };
            var url = Options.TokenEndpoint + query;

            Logger.LogInformation($"Exchanging code via {url}...");
            var response = await Backchannel.GetAsync(url, Context.RequestAborted);

            if (!response.IsSuccessStatusCode)
            {
                var error = "An error occured while exchanging the code.";
                Logger.LogError($"{error} The remote server returned a {response.StatusCode} response with the following payload: {response.Headers.ToString()} {await response.Content.ReadAsStringAsync()}");
                //throw new HttpRequestException($"{error}");
                return(WeixinOAuthTokenResponse.Failed(new Exception(error)));
            }
            var payload = JObject.Parse(await response.Content.ReadAsStringAsync());
            var result  = WeixinOAuthTokenResponse.Success(payload);

            //错误时微信会返回错误JSON数据包,示例如下: { "errcode":40029,"errmsg":"invalid code"}
            if (string.IsNullOrWhiteSpace(result.AccessToken))
            {
                int errorCode    = WeixinOAuthHandlerHelper.GetErrorCode(payload);
                var errorMessage = WeixinOAuthHandlerHelper.GetErrorMessage(payload);
                return(WeixinOAuthTokenResponse.Failed(new Exception($"The remote server returned an error while exchanging the code. {errorCode} {errorMessage}")));
            }
            return(result);
        }
        /// <summary>
        /// Call the OAuthServer and get a user's information.
        /// The context object will have the Identity, AccessToken, and UserInformationEndpoint available.
        /// Using this information, we can query the auth server for claims to attach to the identity.
        /// A particular OAuthServer's endpoint returns a json object with a roles member and a name member.
        /// We call this endpoint with HttpClient, parse the result, and attach the claims to the Identity.
        /// </summary>
        /// <param name="identity"></param>
        /// <param name="properties"></param>
        /// <param name="tokens"></param>
        /// <returns></returns>
        protected virtual async Task <AuthenticationTicket> CustomCreateTicketAsync(
            ClaimsIdentity identity, AuthenticationProperties properties, WeixinOAuthTokenResponse tokens)
        {
            if (identity == null)
            {
                throw new ArgumentNullException(nameof(identity));
            }
            if (properties == null)
            {
                throw new ArgumentNullException(nameof(properties));
            }
            if (tokens == null)
            {
                throw new ArgumentNullException(nameof(tokens));
            }

            var openId = tokens.OpenId;
            var scope  = tokens.Scope;

            // std:NameIdentifier
            identity.AddOptionalClaim(ClaimTypes.NameIdentifier, openId, Options.ClaimsIssuer);
            identity.AddOptionalClaim(WeixinOAuthClaimTypes.OpenId, openId, Options.ClaimsIssuer);
            // scope
            identity.AddOptionalClaim(WeixinOAuthClaimTypes.Scope, scope, Options.ClaimsIssuer);

            if (SplitScope(scope).Contains(WeixinOAuthScopes.snsapi_userinfo))
            {
                identity = await RetrieveUserInfoAsync(tokens.AccessToken, openId, identity);
            }

            var principal = new ClaimsPrincipal(identity);
            var ticket    = new AuthenticationTicket(principal, properties, Scheme.Name);
            var context   = new WeixinOAuthCreatingTicketContext(Context, Scheme, Options, Backchannel, ticket, tokens);
            await Options.Events.CreatingTicket(context);

            return(context.Ticket);
        }