.NET Core 使用 WebApiClient 调用微信商户 API
1. 安装 WebApiClient
这里使用的是 1.0.6 版,使用最新的版本应该也没有问题。
powershell
Install-Package WebApiClient.JIT -Version 1.0.6
2. 定义接口
这里以 企业付款到零钱 API 为例。接口文档见 这里。
因为接口参数和返回值都是 XML 文档,所有需要使用 XmlContent
和 XmlReturn
来指定序列化及反序列化的格式为 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;
}
}
}
参考文档
- 企业付款到零钱
- 安全规范
- 微信支付接口签名校验工具
- .net core 调用数字证书 使用 X509Certificate2
- API 证书及密钥
- Error Deserializing Xml to Object - xmlns='' was not expected
注意
- spbill_create_ip 不支持 IPv6 格式(例:::ffff:172.20.2.1),需使用 IPv4 格式。否则会报错:参数错误:spbill_create_ip 字段必填,并且为合法的 IP 字符串.