如何快速編寫一個微信Api?

概述

Magicodes.Wx.Sdk致力於打造最簡潔最易於使用的微信Sdk,逐步包括公眾號Sdk、小程式Sdk、企業微信Sdk等,以及Abp VNext集成。

本篇將側重於講述如何向Magicodes.Wx.Sdk進行貢獻。

WebApiClientCore

Magicodes.Wx.Sdk之簡潔很大層面依託於NCC的開源庫WebApiClientCore。Magicodes.Wx.Sdk依託WebApiClientCore完成了微信介面的包裝和校驗。

開源庫地址://github.com/dotnetcore/WebApiClient

快速開始

這裡我們以【客服消息】【添加客服帳號】為例進行講解,官方介面文檔地址為://developers.weixin.qq.com/doc/offiaccount/Customer_Service/Customer_Service_Management.html#2。

比如添加客服帳號介面官方介面文檔說明如下所示:

添加客服账号

主體步驟如下:

1)添加介面IKfAccountApi

參考程式碼如下所示:

/// <summary>
/// 客服管理
/// </summary>
[HttpHost("//api.weixin.qq.com/customservice/kfaccount/")]
public interface IKfAccountApi : IWxApiWithAccessTokenFilter
{
    /// <summary>
    /// 添加客服帳號
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    [HttpPost("add")]
    Task<ApiResultBase> AddAsync(AddOrUpdateKfAccountInput input);

    /// <summary>
    /// 設置客服資訊
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    [HttpPost("update")]
    Task<ApiResultBase> UpdateAsync(AddOrUpdateKfAccountInput input);

    /// <summary>
    /// 刪除客服帳號
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    [HttpPost("del")]
    Task<ApiResultBase> DelAsync(DelKfAccountInput input);
}

如上述程式碼所示,有幾個注意事項:

  1. 需要定義介面

  2. 繼承自IWxApiWithAccessTokenFilter介面將自動在介面請求啟用AccessTokenApiFilter篩選器,即會自動會在介面請求時帶上access_token。

  3. HttpHost用於定義介面跟地址

  4. HttpPost用於設置介面請求方法,常用特性有:

    特性名稱功能描述備註
    HttpHostAttribute 請求服務http絕對完整主機域名 優先順序比Options配置低
    HttpGetAttribute 聲明Get請求方法與路徑 支援null、絕對或相對路徑
    HttpPostAttribute 聲明Post請求方法與路徑 支援null、絕對或相對路徑
    HttpPutAttribute 聲明Put請求方法與路徑 支援null、絕對或相對路徑
    HttpDeleteAttribute 聲明Delete請求方法與路徑 支援null、絕對或相對路徑
    HeaderAttribute 聲明請求頭 常量值
    TimeoutAttribute 聲明超時時間 常量值
    FormFieldAttribute 聲明Form表單欄位與值 常量鍵和值
    FormDataTextAttribute 聲明FormData表單欄位與值 常量鍵和值

2)添加Dto

這一步是非必要的,需要看參數的具體內容和要求。添加客服介面的輸入參數程式碼參考如下:

 public class AddOrUpdateKfAccountInput
    {
        /// <summary>
        /// 完整客服帳號,格式為:帳號前綴@公眾號微訊號,帳號前綴最多10個字元,必須是英文、數字字元或者下劃線,後綴為公眾號微訊號,長度不超過30個字元
        /// </summary>
        [JsonProperty("kf_account")]
        [StringLength(30, MinimumLength = 3)]
        [Required]
        public string Account { get; set; }

        /// <summary>
        /// 客服昵稱,最長16個字
        /// </summary>
        [JsonProperty("nickname")]
        [StringLength(16, MinimumLength = 1)]
        public string Nickname { get; set; }
    }

Dto實體的添加這裡給大家分享一個小技巧。當實體欄位以及層級比較多時,大家可以使用VS的【編輯】==》【選擇性粘貼】==》【將Json粘貼為類】:

选择性粘贴

3)添加ApiResultBase

框架中封裝了默認的返回結果基類,如果沒有其他額外的返回內容,僅需返回ApiResultBase即可。如果有額外的範圍內容,則需要定義子類繼承自ApiResultBase,在可能的情況下,有可能需要重寫部分方法(比如IsSuccess)。如下述程式碼:

public class TokenApiResult : ApiResultBase
{
    /// <summary>
    ///     獲取到的憑證
    /// </summary>
    [JsonProperty("access_token")]
    public string AccessToken { get; set; }

    /// <summary>
    ///     憑證有效時間,單位:秒
    /// </summary>
    [JsonProperty("expires_in")]
    internal int Expires { get; set; }

    /// <summary>
    ///     憑證過期時間
    /// </summary>
    public DateTime ExpiresTime { get; set; }
}

至此,一個介面就編寫完成了。只需要定義interface和Dto就可以了。是不是非常簡單?

4)單元測試編寫

/// <summary>
/// 模板消息單元測試
/// </summary>
public class TemplateApiTest : TestBase, IClassFixture<TestWebApplicationFactory>
{
    private readonly ITemplateApi templateApi;
    public TemplateApiTest(TestWebApplicationFactory webApplicationFactory, ITestOutputHelper output) : base(webApplicationFactory, output)
    {
        //通過父類的GetRequiredService獲取到Api
        templateApi = GetRequiredService<ITemplateApi>();
    }

    /// <summary>
    /// 模板消息發送測試
    /// </summary>
    /// <returns></returns>
    [Fact]
    public async Task SendAsync_Test()
    {
        var result = await templateApi.SendAsync(new SendTemplateMessageInput()
        {
            To = "oXELNwzZgamuLS0JrJhVgdelzKyw",
            TemplateId = "riid7aad8OKRQD9Ey6dclWBBkrqZSFDhlpKh0_spGLA",
            Data = new System.Collections.Generic.Dictionary<string, TemplateDataItem>()
            {
                {"first",new TemplateDataItem("測試") },
                {"keyword1",new TemplateDataItem("雪雁") },
                {"keyword2",new TemplateDataItem("2021.2.5") },
                {"remark",new TemplateDataItem("備註") },
            }
        });
        //判斷Api是否調用成功,未成功將拋出異常WxSdkException
        result.EnsureSuccess();
    }
}

整體參考:

//github.com/xin-lai/Magicodes.Wx.Sdk/pull/1/commits/85263ed9a807581f7315a90fe6e00c51c994d386

其他須知內容

IWxApiWithAccessTokenFilter介面

IWxApiWithAccessTokenFilter介面用於定義需要攜帶公眾號Acces sToken的介面。

如下述參考程式碼所示,其啟用了AccessTokenApiFilter篩選器,以及關閉了AcceptContentType的匹配約束(公眾號的介面官方寫的一塌糊塗,很不規範)。

參考程式碼:

/// <summary>
/// 
/// </summary>
[JsonNetReturn(EnsureMatchAcceptContentType = false)]
[AccessTokenApiFilter]
//[LoggingFilter]
public interface IWxApiWithAccessTokenFilter
{
}

AccessTokenApiFilter篩選器

AccessTokenApiFilter介面篩選器會在啟用的API、Action上自動添加access_token參數值。

參考程式碼如下所示:

public class AccessTokenApiFilter : ApiFilterAttribute
{
    public override async Task OnRequestAsync(ApiRequestContext context)
    {
        ITokenManager tokenManager = context.HttpContext.ServiceProvider.GetRequiredService<ITokenManager>();
        string accessToken = await tokenManager.GetAccessTokenAsync();
        context.HttpContext.RequestMessage.AddUrlQuery("access_token", accessToken);
    }

    public override Task OnResponseAsync(ApiResponseContext context)
    {
        return Task.CompletedTask;
    }
}

關於ApiResultBase

ApiResultBase定義了公眾號API返回基類,用於接收錯誤碼、錯誤消息、是否執行成功的判斷以及獲取友好消息提示。

參考程式碼:

/// <summary>
///     API請求結果
///     {"errcode":40164,"errmsg":"invalid ip 218.76.8.29 ipv6 ::ffff:218.76.8.29, not in whitelist rid: 60122c35-705c3134-51b45a3d"}
/// </summary>
public class ApiResultBase
{
    /// <summary>
    ///     返回碼
    /// </summary>
    [JsonProperty("errcode")]
    public virtual ReturnCodes ReturnCode { get; set; }

    /// <summary>
    ///     錯誤消息
    /// </summary>
    [JsonProperty("errmsg")]
    public virtual string Message { get; set; }

    /// <summary>
    ///     是否為成功返回
    /// </summary>
    /// <returns></returns>
    public virtual bool IsSuccess()
    {
        return ReturnCode == ReturnCodes.請求成功;
    }

    /// <summary>
    ///     獲取友好提示
    /// </summary>
    /// <returns></returns>
    public virtual string GetFriendlyMessage()
    {
        return ReturnCode.ToString();
    }
}