Skip to content

.NET Core 使用 WebApiClient 获取小程序码(wxacode.getUnlimited)

🏷️ .NET Core WebApiClient 微信小程序

1. 安装 WebApiClient.JITMicrosoft.AspNetCore.Http

bash
Install-Package WebApiClient.JIT -Version 1.0.8
Install-Package Microsoft.AspNetCore.Http -Version 2.1.1

2. IAuthApi.cs

csharp
/// <summary>
/// 权限 API
/// </summary>
public interface IAuthApi : IHttpApi
{
    /// <summary>
    /// 获取小程序全局唯一后台接口调用凭据(access_token)
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    [HttpGet("https://api.weixin.qq.com/cgi-bin/token")]
    ITask<GetAccessTokenResponse> GetAccessToken(GetAccessTokenRequest request);
}

/// <summary>
/// 获取小程序全局唯一后台接口调用凭据 参数
/// </summary>
public class GetAccessTokenRequest
{
    /// <summary>
    /// 填写 client_credential
    /// </summary>
    [AliasAs("grant_type")]
    public string GrantType { get; set; } = "client_credential";

    /// <summary>
    /// 小程序唯一凭证,即 AppID
    /// </summary>
    [AliasAs("appid")]
    public string AppId { get; set; }

    /// <summary>
    /// 小程序唯一凭证密钥,即 AppSecret
    /// </summary>
    [AliasAs("secret")]
    public string Secret { get; set; }
}

/// <summary>
/// 获取小程序全局唯一后台接口调用凭据 返回值
/// </summary>
public class GetAccessTokenResponse : WXBaseResponse
{
    /// <summary>
    /// 接口调用凭证
    /// </summary>
    [AliasAs("access_token")]
    public string AccessToken { get; set; }

    /// <summary>
    /// 凭证有效时间,单位:秒。目前是 7200 秒之内的值。
    /// </summary>
    [AliasAs("expires_in")]
    public int ExpiresIn { get; set; }
}

/// <summary>
/// 微信基础返回信息
/// </summary>
public class WXBaseResponse
{
    /// <summary>
    /// 错误码:
    ///   -1 : 系统繁忙,此时请开发者稍候再试;
    ///   0 : 请求成功;
    ///   ... ...
    /// </summary>
    [AliasAs("errcode")]
    public int ErrCode { get; set; }

    /// <summary>
    /// 错误信息
    /// </summary>
    [AliasAs("errmsg")]
    public string ErrMsg { get; set; }
}

3. IWxacodeApi.cs

因为 wxacode.getUnlimited 返回的类型可能是 Json 也可能是图片,所以这里返回值类型为 HttpResponseMessage

csharp
/// <summary>
/// 小程序码 API
/// </summary>
public interface IWxacodeApi : IHttpApi
{
    /// <summary>
    /// 获取小程序码,适用于需要的码数量极多的业务场景。通过该接口生成的小程序码,永久有效,数量暂无限制。
    /// </summary>
    /// <param name="request"></param>
    /// <param name="access_token"></param>
    /// <returns></returns>
    [HttpPost("https://api.weixin.qq.com/wxa/getwxacodeunlimit")]
    ITask<HttpResponseMessage> GetUnlimited([JsonContent] GetUnlimitedRequest request, [PathQuery] string access_token);
}

/// <summary>
/// 获取小程序码 参数
/// </summary>
public class GetUnlimitedRequest
{
    /// <summary>
    /// 参数 : 最大 32 个可见字符,只支持数字,大小写英文以及部分特殊字符:!#$&'()*+,/:;=?@-._~,其它字符请自行编码为合法字符(因不支持%,中文无法使用 urlencode 处理,请使用其他编码方式)
    /// </summary>
    [AliasAs("scene")]
    public string Scene { get; set; }

    /// <summary>
    /// 主页 : 必须是已经发布的小程序存在的页面(否则报错),例如 pages/index/index, 根路径前不要填加 /,不能携带参数(参数请放在 scene 字段里),如果不填写这个字段,默认跳主页面
    /// </summary>
    [AliasAs("page")]
    public string Page { get; set; }

    /// <summary>
    /// 二维码的宽度,单位 px,最小 280px,最大 1280px
    /// </summary>
    [AliasAs("width")]
    public int width { get; set; } = 430;

    /// <summary>
    /// 接口调用凭证
    /// </summary>
    [AliasAs("auto_color")]
    public bool AutoColor { get; set; } = false;

    /// <summary>
    /// auto_color 为 false 时生效,使用 rgb 设置颜色 例如 {"r":"xxx","g":"xxx","b":"xxx"} 十进制表示
    /// </summary>
    [AliasAs("line_color")]
    public RGBColor LineColor { get; set; }

    /// <summary>
    /// 是否需要透明底色,为 true 时,生成透明底色的小程序
    /// </summary>
    [AliasAs("is_hyaline")]
    public bool IsHyaline { get; set; } = false;
}

/// <summary>
/// RGB
/// </summary>
public class RGBColor
{
    /// <summary>
    /// R
    /// </summary>
    [AliasAs("r")]
    public int R { get; set; }

    /// <summary>
    /// G
    /// </summary>
    [AliasAs("g")]
    public int G { get; set; }

    /// <summary>
    /// B
    /// </summary>
    [AliasAs("b")]
    public int B { get; set; }
}

4. WXApiFactory.cs

csharp
/// <summary>
/// 微信 API 工厂类
/// </summary>
public class WXApiFactory
{
    /// <summary>
    /// 注册 API
    /// </summary>
    public static void Register()
    {
        // 权限 API
        HttpApi.Register<IAuthApi>().ConfigureHttpApiConfig(c => { });

        // 小程序码 API
        HttpApi.Register<IWxacodeApi>().ConfigureHttpApiConfig(c => { });
    }
}

5. ESSearchApiExtension.cs

csharp
/// <summary>
/// 微信 Api 扩展类
/// </summary>
public static class ESSearchApiExtension
{
    /// <summary>
    /// 使用微信 Api
    /// </summary>
    /// <param name="app"></param>
    public static void UseWXApi(this IApplicationBuilder app)
    {
        WXApiFactory.Register();
    }
}

6. Startup.cs

csharp
/// <summary>
/// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
/// </summary>
/// <param name="app"></param>
/// <param name="env"></param>
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // ...

    app.UseMvc();

    // ...

    // 使用微信 API
    app.UseWXApi();
}

7. WXAccessTokenUtils.cs

csharp
/// <summary>
/// 微信接口调用凭证通用方法
/// </summary>
public class WXAccessTokenUtils
{
    /// <summary>
    /// 刷新接口调用凭证
    /// </summary>
    /// <param name="appId"></param>
    public static async Task<GetAccessTokenResponse> RefreshAccessTokenAsync(string appId)
    {
        var response = await HttpApi.Resolve<IAuthApi>().GetAccessToken(new GetAccessTokenRequest()
        {
            AppId = appId,
            Secret = ZConfig.GetConfigString(ApolloConfigKey.AppSecret),
        });

        if (response.ErrCode == 0)
        {
            // 将接口调用凭据保存到缓存
            RedisUtil.Set($"{CacheKey.WXAccessTokenPrefix}_{appId}", new AccessTokenCacheModel()
            {
                AccessToken = response.AccessToken,
                ExpiresTime = DateTime.Now.AddSeconds(response.ExpiresIn),
            }, response.ExpiresIn / 60);
        }

        return response;
    }

    /// <summary>
    /// 获取接口调用凭证
    /// </summary>
    /// <param name="appId"></param>
    /// <returns></returns>
    public static async Task<string> GetAccessTokenAsync(string appId)
    {
        var cacheKey = $"{CacheKey.WXAccessTokenPrefix}_{appId}";
        // 将接口调用凭据保存到缓存
        var cachedAccessToken = RedisUtil.Get<AccessTokenCacheModel>(cacheKey);

        if (cachedAccessToken == null || cachedAccessToken.ExpiresTime < DateTime.Now)
        {
            // 没有缓存或凭证已过期时,刷新凭证
            var response = await RefreshAccessTokenAsync(appId);
            if (response != null && response.ErrCode == 0)
            {
                return response.AccessToken;
            }
            else
            {
                return string.Empty;
            }
        }
        else if(cachedAccessToken.ExpiresTime.AddMinutes(-5) < DateTime.Now)
        {
            // 即将过期时重新获取凭证
            var response = await RefreshAccessTokenAsync(appId);
            if (response != null && response.ErrCode != 0)
            {
                return response.AccessToken;
            }
            else
            {
                return cachedAccessToken.AccessToken;
            }
        }

        return cachedAccessToken.AccessToken;
    }
}

8. QRCodeController.cs

失败时返回的是 Json 字符串,成功时返回的是图片的 buffer。
这里通过返回消息的 ContentType 来判断是哪种类型。

csharp
/// <summary>
/// 获取小程序码
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[HttpPost("wxacode-unlimited")]
public async Task<Result> GetWXACodeUnlimited(GetUnlimitedRequest request)
{
    var appId = ZConfig.GetConfigString(ApolloConfigKey.AppID);
    var accessToken = await WXAccessTokenUtils.GetAccessTokenAsync(appId);

    var responseMessage = await HttpApi.Resolve<IWxacodeApi>().GetUnlimited(request, accessToken);
    if (responseMessage == null || responseMessage.StatusCode != System.Net.HttpStatusCode.OK)
    {
        return new Result()
        {
            IsSuccess = false,
            Msg = "访问微信 API 异常",
        };
    }
    if (responseMessage.Content.Headers.ContentType.MediaType == "image/jpeg")
    {
        return new Result()
        {
            IsSuccess = true,
            Msg = Convert.ToBase64String(await responseMessage.Content.ReadAsByteArrayAsync()),
        };
    }
    else if (responseMessage.Content.Headers.ContentType.MediaType == "application/json")
    {
        var response = JsonHelper.DeserializeObject<WXBaseResponse>(await responseMessage.Content.ReadAsStringAsync());
        return new Result()
        {
            IsSuccess = false,
            Msg = response.ErrMsg,
        };
    }

    return new Result()
    {
        IsSuccess = false,
        Msg = "未处理的二维码类型,请联系客服。",
    };
}

/// <summary>
/// 获取小程序码图片
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[HttpPost("wxacode-unlimited-img")]
public async Task GetWXACodeUnlimitedImage(GetUnlimitedRequest request)
{
    var appId = ZConfig.GetConfigString(ApolloConfigKey.ZYXY_AppID);
    var accessToken = await WXAccessTokenUtils.GetAccessTokenAsync(appId);
    var responseMessage = await HttpApi.Resolve<IWxacodeApi>().GetUnlimited(request, accessToken);

    if (responseMessage != null
        && responseMessage.StatusCode == HttpStatusCode.OK
        && responseMessage.Content.Headers.ContentType.MediaType == "image/jpeg")
    {
        Response.Clear();
        Response.ContentType = responseMessage.Content.Headers.ContentType.MediaType;
        await Response.Body.WriteAsync(await responseMessage.Content.ReadAsByteArrayAsync());
        return;
    }

    Response.StatusCode = (int)HttpStatusCode.InternalServerError;
}

9. QRCode.wxml

html
<image wx:if="{{result.isSuccess}}" src='data:image/jpeg;base64,{{result.msg}}' />

参考

  1. wxacode.getUnlimited
  2. auth.getAccessToken