Skip to content

.NET Core 使用 WebApiClient 调用微信商户 API

🏷️ .NET Core WebApiClient

1. 安装 WebApiClient

这里使用的是 1.0.6 版,使用最新的版本应该也没有问题。

powershell
Install-Package WebApiClient.JIT -Version 1.0.6

2. 定义接口

这里以 企业付款到零钱 API 为例。接口文档见 这里
因为接口参数和返回值都是 XML 文档,所有需要使用 XmlContentXmlReturn 来指定序列化及反序列化的格式为 XML。

IMchCertApi.cs

csharp
/// <summary>
/// 商户 API(带证书)
/// </summary>
public interface IMchCertApi : IHttpApi
{
    /// <summary>
    /// 企业付款到零钱
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    [HttpPost("https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers")]
    [XmlReturn]
    ITask<PayMktResponse> PayMktAsync([XmlContent] PayMktRequest request);
}

3. 定义参数和返回结果模型

因为商户 API 请求参数的 XML 文档的根元素为 xml,所以需要通过 XmlRoot 特性手动指定,否则会默认使用类名(这里就是 PayMktRequest)作为根元素名。
另外在属性上通过 XmlElement 特性指定每个属性对应的 XML 元素名。

PayMktRequest.cs 请求参数

csharp
/// <summary>
/// 企业付款到零钱请求参数
/// </summary>
[XmlRoot("xml")]
public class PayMktRequest
{
    /// <summary>
    /// 商户账号 appid
    /// (必填)
    /// String(128)
    /// 申请商户号的 appid 或商户号绑定的 appid
    /// </summary>
    [XmlElement(ElementName = "mch_appid")]
    public string MchAppId { get; set; }

    /// <summary>
    /// 商户号
    /// (必填)
    /// String(32)
    /// 微信支付分配的商户号
    /// </summary>
    [XmlElement(ElementName = "mchid")]
    public string MchId { get; set; }

    /// <summary>
    /// 设备号
    /// (非必填)
    /// String(32)
    /// 微信支付分配的终端设备号
    /// </summary>
    [XmlElement(ElementName = "device_info")]
    public string DeviceInfo { get; set; }

    /// <summary>
    /// 随机字符串
    /// (必填)
    /// String(32)
    /// 随机字符串,不长于 32 位
    /// </summary>
    [XmlElement(ElementName = "nonce_str")]
    public string NonceStr { get; set; }

    /// <summary>
    /// 签名
    /// (必填)
    /// String(32)
    /// </summary>
    [XmlElement(ElementName = "sign")]
    public string Sign { get; set; }

    /// <summary>
    /// 商户订单号
    /// (必填)
    /// String(32)
    /// 商户订单号,需保持唯一性
    /// (只能是字母或者数字,不能包含有其他字符)
    /// </summary>
    [XmlElement(ElementName = "partner_trade_no")]
    public string PartnerTradeNo { get; set; }

    /// <summary>
    /// 用户 openid
    /// (必填)
    /// String(64)
    /// 商户 appid 下,某用户的 openid
    /// </summary>
    [XmlElement(ElementName = "openid")]
    public string OpenId { get; set; }

    /// <summary>
    /// 校验用户姓名选项
    /// (必填)
    /// String(16)
    /// NO_CHECK:不校验真实姓名
    /// FORCE_CHECK:强校验真实姓名
    /// </summary>
    [XmlElement(ElementName = "check_name")]
    public string CheckName { get; set; }

    /// <summary>
    /// 收款用户姓名
    /// (非必填)
    /// String(64)
    /// 收款用户真实姓名。 
    /// 如果 check_name 设置为 FORCE_CHECK,则必填用户真实姓名
    /// </summary>
    [XmlElement(ElementName = "re_user_name")]
    public string ReUserName { get; set; }

    /// <summary>
    /// 金额
    /// (必填)
    /// int
    /// 企业付款金额,单位为分
    /// </summary>
    [XmlElement(ElementName = "amount")]
    public int Amount { get; set; }

    /// <summary>
    /// 企业付款备注
    /// (必填)
    /// String(100)
    /// 企业付款备注,必填。注意:备注中的敏感词会被转成字符*
    /// </summary>
    [XmlElement(ElementName = "desc")]
    public string Desc { get; set; }

    /// <summary>
    /// Ip 地址
    /// (必填)
    /// String(32)
    /// 该 IP 同在商户平台设置的 IP 白名单中的 IP 没有关联,该 IP 可传用户端或者服务端的 IP。
    /// </summary>
    [XmlElement(ElementName = "spbill_create_ip")]
    public string SpbillCreateIp { get; set; }
}

PayMktResponse.cs 返回结果模型

csharp
/// <summary>
/// 企业付款到零钱响应
/// </summary>
[XmlRoot("xml")]
public class PayMktResponse
{
    /// <summary>
    /// 返回状态码
    /// (必填)
    /// String(16)
    /// SUCCESS/FAIL
    /// 此字段是通信标识,非交易标识,交易是否成功需要查看 result_code 来判断
    /// </summary>
    [XmlElement(ElementName = "return_code")]
    public string ReturnCode { get; set; }

    /// <summary>
    /// 返回信息
    /// (非必填)
    /// String(128)
    /// 返回信息,如非空,为错误原因
    /// </summary>
    [XmlElement(ElementName = "return_msg")]
    public string ReturnMsg { get; set; }

    /// <summary>
    /// 商户账号 appid
    /// (必填)
    /// String(128)
    /// 申请商户号的 appid 或商户号绑定的 appid
    /// </summary>
    [XmlElement(ElementName = "mch_appid")]
    public string MchAppId { get; set; }

    /// <summary>
    /// 商户号
    /// (必填)
    /// String(32)
    /// 微信支付分配的商户号
    /// </summary>
    [XmlElement(ElementName = "mchid")]
    public string MchId { get; set; }

    /// <summary>
    /// 设备号
    /// (非必填)
    /// String(32)
    /// 微信支付分配的终端设备号
    /// </summary>
    [XmlElement(ElementName = "device_info")]
    public string DeviceInfo { get; set; }

    /// <summary>
    /// 随机字符串
    /// (必填)
    /// String(32)
    /// 随机字符串,不长于 32 位
    /// </summary>
    [XmlElement(ElementName = "nonce_str")]
    public string NonceStr { get; set; }

    /// <summary>
    /// 业务结果
    /// (必填)
    /// String(16)
    /// SUCCESS/FAIL,注意:当状态为FAIL时,存在业务结果未明确的情况。
    /// 如果状态为 FAIL,请务必关注错误代码(err_code 字段),通过查询接口确认此次付款的结果。
    /// </summary>
    [XmlElement(ElementName = "result_code")]
    public string ResultCode { get; set; }

    /// <summary>
    /// 错误代码
    /// (非必填)
    /// String(32)
    /// 错误码信息,注意:出现未明确的错误码时(SYSTEMERROR 等),请务必用原商户订单号重试,或通过查询接口确认此次付款的结果。
    /// 错误码:
    ///   NO_AUTH:没有该接口权限
    ///   AMOUNT_LIMIT:金额超限
    ///   PARAM_ERROR:参数错误
    ///   OPENID_ERROR:Openid 错误
    ///   SEND_FAILED:付款错误
    ///   NOTENOUGH:余额不足
    ///   SYSTEMERROR:系统繁忙,请稍后再试。
    ///   NAME_MISMATCH:姓名校验出错
    ///   SIGN_ERROR:签名错误
    ///   XML_ERROR:Post 内容出错
    ///   FATAL_ERROR:两次请求参数不一致
    ///   FREQ_LIMIT:超过频率限制,请稍后再试。
    ///   MONEY_LIMIT:已经达到今日付款总额上限/已达到付款给此用户额度上限
    ///   CA_ERROR:商户 API 证书校验出错
    ///   V2_ACCOUNT_SIMPLE_BAN:无法给非实名用户付款
    ///   PARAM_IS_NOT_UTF8:请求参数中包含非 utf8 编码字符
    ///   SENDNUM_LIMIT:该用户今日付款次数超过限制,如有需要请登录微信支付商户平台更改 API 安全配置
    ///   RECV_ACCOUNT_NOT_ALLOWED:收款账户不在收款账户列表
    ///   PAY_CHANNEL_NOT_ALLOWED:本商户号未配置 API 发起能力
    /// </summary>
    [XmlElement(ElementName = "err_code")]
    public string ErrCode { get; set; }

    /// <summary>
    /// 错误代码描述
    /// (非必填)
    /// String(128)
    /// 结果信息描述
    /// </summary>
    [XmlElement(ElementName = "err_code_des")]
    public string ErrCodeDes { get; set; }

    /// <summary>
    /// 商户订单号
    /// (必填)
    /// String(32)
    /// 商户订单号,需保持历史全局唯一性 (只能是字母或者数字,不能包含有其他字符)
    /// (在 return_code 和 result_code 都为 SUCCESS 的时候有返回)
    /// </summary>
    [XmlElement(ElementName = "partner_trade_no")]
    public string PartnerTradeNo { get; set; }

    /// <summary>
    /// 微信付款单号
    /// (必填)
    /// String(64)
    /// 企业付款成功,返回的微信付款单号
    /// (在 return_code 和 result_code 都为 SUCCESS 的时候有返回)
    /// </summary>
    [XmlElement(ElementName = "payment_no")]
    public string PaymentNo { get; set; }

    /// <summary>
    /// 付款成功时间
    /// (必填)
    /// String(32)
    /// 企业付款成功时间
    /// (在 return_code 和 result_code 都为 SUCCESS 的时候有返回)
    /// </summary>
    [XmlElement(ElementName = "payment_time")]
    public string PaymentTime { get; set; }
}

4. 注册接口

由于这个 API 需要使用证书,可以通过指定其 HttpClientHandler 来加载证书。(证书文件下载请参照 安全规范
另外这里还通过 ConfigureHttpApiConfig 方法配置了过滤器来记录日志。
注意: 需要在项目启动时调用一次 WXApiFactory.Register() 方法。

WXApiFactory.cs

csharp
/// <summary>
/// 微信 API 工厂类
/// </summary>
public class WXApiFactory
{
    /// <summary>
    /// 注册 API
    /// </summary>
    public static void Register()
    {
        // 商户(带证书)API
        HttpApi.Register<IMchCertApi>()
            .ConfigureHttpMessageHandler(() => {
                var handler = new HttpClientHandler()
                {
                    ClientCertificateOptions = ClientCertificateOption.Manual,
                    SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls,
                };

                try
                {
                    handler.ClientCertificates.Add(new X509Certificate2(
                        ZConfig.GetConfigString(ApolloConfigKey.MchApiclientCertPath), // 证书位置
                        ZConfig.GetConfigString(ApolloConfigKey.MchID), // 证书密码(默认是商户号)
                        X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.MachineKeySet));
                }
                catch (Exception ex)
                {
                    ILogger.Error(ex);
                }

                handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true;

                return handler;
            })
            .ConfigureHttpApiConfig(c => {
                c.GlobalFilters.Add(new WXApiLogFilter());
            });
    }
}

WXApiLogFilter.cs

csharp
/// <summary>
/// 微信 API 日志过滤器
/// </summary>
class WXApiLogFilter : IApiActionFilter
{
    /// <summary>
    /// 开始请求时
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public async Task OnBeginRequestAsync(ApiActionContext context)
    {
        ILogger.Info($"{{ \"OnBeginRequestAsync\": {{ \"RequestMessage\":{context.RequestMessage.ToJson()} }}, \"RequestString\": {(await context.RequestMessage.GetRequestStringAsync()).ToJson()} }}");
    }

    /// <summary>
    /// 结束请求时
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public Task OnEndRequestAsync(ApiActionContext context)
    {
        ILogger.Info($"{{ \"OnEndRequestAsync\": {{ \"ResponseMessage\":{context.ResponseMessage.ToJson()} }}, \"Result\": {context.Result.ToJson()} }}");
        return Task.CompletedTask;
    }
}

5. 调用接口

csharp
var request = new PayMktRequest()
{
    MchAppId = ZConfig.GetConfigString(ApolloConfigKey.AppID),
    MchId = ZConfig.GetConfigString(ApolloConfigKey.MchID),
    DeviceInfo = null,
    NonceStr = ZString.GenerateRandomString(32, RandomStringType.LetterNum),
    PartnerTradeNo = commissionLog.Guid.ToString("N"),
    OpenId = commissionLog.OpenId,
    CheckName = "NO_CHECK",
    ReUserName = null,
    Amount = ZConvert.ToInt32(Math.Floor(commissionLog.Amount * 100)),
    Desc = commissionLog.Title,
    SpbillCreateIp = GetClientIP(),
};

// 参数签名
request.Sign();

var response = await HttpApi.Resolve<IMchCertApi>().PayMktAsync(request);

SignExtension.cs 签名的扩展方法

csharp
/// <summary>
/// 商户 API 请求参数签名扩展方法
/// </summary>
public static class SignExtension
{
    /// <summary>
    /// 签名
    /// </summary>
    /// <param name="request"></param>
    public static PayMktRequest Sign(this PayMktRequest request)
    {
        request.Sign = GenerateSign(request);
        return request;
    }

    /// <summary>
    /// 生成签名
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    private static string GenerateSign(object request)
    {
        // 获取所有的属性和值的 KeyValue 对
        var pairs = new List<KeyValuePair<string, string>>();
        foreach (var property in request.GetType().GetProperties())
        {
            var attribute = (XmlElementAttribute)property.GetCustomAttributes(typeof(XmlElementAttribute), false).FirstOrDefault();
            var key = attribute?.ElementName ?? property.Name;
            if (key == "sign") continue; // sign 参数不参与签名

            var value = property.GetValue(request)?.ToString();
            if (string.IsNullOrEmpty(value)) continue; // 参数的值为空不参与签名

            pairs.Add(new KeyValuePair<string, string>(key, value));
        }

        // 参数名 ASCII 码从小到大排序(字典序)
        pairs.Sort((p1, p2) => p1.Key.CompareTo(p2.Key));

        // 使用 URL 键值对的格式(即 key1=value1&key2=value2…)拼接成字符串 stringA
        var stringA = string.Join("&", pairs.Select(p => $"{p.Key}={p.Value}").ToArray());

        // 在 stringA 最后拼接上 key 得到 stringSignTemp 字符串,并对 stringSignTemp 进行 MD5 运算,
        // 再将得到的字符串所有字符转换为大写,得到 sign 值 signValue
        var stringSignTemp = stringA + $"&key={ZConfig.GetConfigString(ApolloConfigKey.MchKey)}";
        var sign = MD5(stringSignTemp).ToUpper();

        return sign;
    }

    /// <summary>
    /// MD5 加密
    /// </summary>
    /// <param name="value"></param>
    /// <returns></returns>
    private static string MD5(string value)
    {
        using (var md5 = System.Security.Cryptography.MD5.Create())
        {
            var result = md5.ComputeHash(Encoding.UTF8.GetBytes(value));
            var pwd = BitConverter.ToString(result).Replace("-", "");
            return pwd;
        }
    }
}

参考文档

  1. 企业付款到零钱
  2. 安全规范
  3. 微信支付接口签名校验工具
  4. .net core 调用数字证书 使用 X509Certificate2
  5. API 证书及密钥
  6. Error Deserializing Xml to Object - xmlns='' was not expected

注意

  1. spbill_create_ip 不支持 IPv6 格式(例:::ffff:172.20.2.1),需使用 IPv4 格式。否则会报错:参数错误:spbill_create_ip 字段必填,并且为合法的 IP 字符串.