Skip to content

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

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 字符串.

Page Layout Max Width

Adjust the exact value of the page width of VitePress layout to adapt to different reading needs and screens.

Adjust the maximum width of the page layout
A ranged slider for user to choose and customize their desired width of the maximum width of the page layout can go.

Content Layout Max Width

Adjust the exact value of the document content width of VitePress layout to adapt to different reading needs and screens.

Adjust the maximum width of the content layout
A ranged slider for user to choose and customize their desired width of the maximum width of the content layout can go.