前言
前几个周整理了微信支付的统一下单、支付结果通知、订单查询接口。趁这几天有时间,整理了一下我的代码,又把APP支付的退款接口、关闭订单整理了,把APP支付的后端接口整理了一下。
github仓库地址:[https://github.com/wolfsheep/WeChatAPIv3ForAppWithNetCore](https://github.com/wolfsheep/WeChatAPIv3ForAppWithNetCore)
gitee仓库地址:[https://gitee.com/dawenyang_admin/WeChatAPIv3ForAppWithNetCore](https://gitee.com/dawenyang_admin/WeChatAPIv3ForAppWithNetCore)
把我的代码重构了一遍,更方便大家的使用。可能还有很多问题,欢迎大家反馈。下面我具体列一下我的实现。
重要的事说在前面
- 本地调试时记得安装微信支付安全证书!!!
- 发布到服务器上也要安装微信支付安全证书!!!
- 私钥从微信支付后台发放的证书中拷贝出来!!!!!
理论实战
统一下单
调用方法
private readonly ILogger<WeChatPayTestController> _logger;
private readonly IConfiguration _configuration;
public WeChatPayTestController(ILogger<WeChatPayTestController> logger, IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
}
/// <summary>
/// 统一下单接口
/// </summary>
/// <returns></returns>
[HttpGet, Route("api/WeChatPayTest/GenerateOrder")]
public async Task<AppPayModel> GenerateOrder()
{
var orderNumber = $"{DateTime.Now:yyyyMMddHHmmssff}{CodeHelper.CreateNumCode(3)}";
var helper = new WxPayHelper(WxPayConst.appid, WxPayConst.mchid, WxPayConst.serialNo, WxPayConst.privateKey);
var notify_url = _configuration["notify_url"]; //这个放在配置文件,从配置文件读取比较灵活,或者写到数据库中
var payodel = await helper.UnionGenerateOrder("好东西啊", 1, orderNumber, notify_url, "附加信息测试啊");
#region 为APP生成下单所需的参数,看个人实际需求,也可以APP自己生成所需的参数
var signModel = WxPayForAppHelper.GetSign(WxPayConst.appid, payodel.prepay_id, WxPayConst.privateKey);
#endregion
return signModel;
}
查询订单
调用方法
/// <summary>
/// 查询订单接口
/// </summary>
[HttpGet, Route("api/WeChatPayTest/QueryOrder")]
public async Task<WxPayStatusRespModel> QueryOrder(string orderNumber)
{
var helper = new WxPayHelper(WxPayConst.appid, WxPayConst.mchid, WxPayConst.serialNo, WxPayConst.privateKey);
var payModel = await helper.QueryOrder(orderNumber);
return payModel;
}
关闭订单
调用方法
/// <summary>
/// 查询订单接口
/// </summary>
[HttpGet, Route("api/WeChatPayTest/CloseOrder")]
public async Task<ClostOrderRespModel> CloseOrder(string orderNumber)
{
var helper = new WxPayHelper(WxPayConst.appid, WxPayConst.mchid, WxPayConst.serialNo, WxPayConst.privateKey);
var payModel = await helper.ClostOrder(orderNumber);
return payModel;
}
支付结果通知
调用方法
/// <summary>
/// 微信支付成功结果回调接口
/// </summary>
/// <returns>退款通知http应答码为200且返回状态码为SUCCESS才会当做商户接收成功,否则会重试。注意:重试过多会导致微信支付端积压过多通知而堵塞,影响其他正常通知。</returns>
[HttpPost, Route("api/WeChatPayTest/WxPayCallback")]
[AllowAnonymous]
public async Task<WxPayCallbackRespModel> WxPayCallback()
{
#region 获取字符串流
/**
* .NET 获取字符串流
* System.IO.Stream s = HttpContext.Current.Request.InputStream;
int count = 0;
byte[] buffer = new byte[1024];
StringBuilder builder = new StringBuilder();
while ((count = s.Read(buffer, 0, 1024)) > 0)
{
builder.Append(Encoding.UTF8.GetString(buffer, 0, count));
}
s.Flush();
s.Close();
s.Dispose();
*/
var buffer = new MemoryStream();
Request.Body.CopyTo(buffer);
#endregion
//我没有使用官方的那种验证数据安全性的方法,我解密出来数据之后,直接拿着订单号再去查询一下订单状态,然后再更新到数据库中。我嫌麻烦……
var str = Encoding.UTF8.GetString(buffer.GetBuffer());
var wxPayNotifyModel = str.ToObject<WxPayNotifyModel>();
var resource = wxPayNotifyModel?.resource ?? new WxPayResourceModel();
var decryptStr = AesGcmHelper.AesGcmDecrypt(resource.associated_data, resource.nonce, resource.ciphertext, WxPayConst.APIV3Key);
var decryptModel = decryptStr.ToObject<WxPayResourceDecryptModel>();
var viewModel = new WxPayCallbackRespModel();
if (string.IsNullOrEmpty(decryptModel.out_trade_no))
{
viewModel.code = "FAIL";
viewModel.message = "数据解密失败";
}
else
{
var resp = await QueryOrder(decryptModel.out_trade_no);
//然后进行数据库更新处理……等等其他操作
}
return viewModel;
}
申请退款
调用方法
/// <summary>
/// 退款接口
/// </summary>
[HttpGet, Route("api/WeChatPayTest/Refunds")]
public async Task<RefundsRespModel> Refunds(string orderNumber)
{
var helper = new WxPayHelper(WxPayConst.appid, WxPayConst.mchid, WxPayConst.serialNo, WxPayConst.privateKey);
var refundNumber = $"{DateTime.Now:yyyyMMddHHmmssff}{CodeHelper.CreateNumCode(3)}";
var payModel = await helper.Refunds(orderNumber, refundNumber, "测试退款行不行", 1, 2, "https://xxxxx.top/api/WeChatPayTest/RefundsCallback");
return payModel;
}
查询退款
调用方法
/// <summary>
/// 查询退款结果接口
/// </summary>
/// <param name="refundNumber">商户系统内部的退款单号,商户系统内部唯一</param>
/// <returns></returns>
[HttpGet, Route("api/WeChatPayTest/QueryRefunds")]
public async Task<QueryRefundsOrderRespModel> QueryRefunds(string refundNumber)
{
var helper = new WxPayHelper(WxPayConst.appid, WxPayConst.mchid, WxPayConst.serialNo, WxPayConst.privateKey);
var payModel = await helper.QueryRefundsOrder(refundNumber);
return payModel;
}
退款结果通知
实现方法方法
/// <summary>
/// 退款通知回调接口
/// </summary>
/// <returns>退款通知http应答码为200且返回状态码为SUCCESS才会当做商户接收成功,否则会重试。注意:重试过多会导致微信支付端积压过多通知而堵塞,影响其他正常通知。</returns>
[HttpPost, Route("api/WeChatPayTest/RefundsCallback")]
[AllowAnonymous]
public async Task<RefundsCallbackRespModel> RefundsCallback()
{
#region 获取字符串流
/**
* .NET 获取字符串流
* System.IO.Stream s = HttpContext.Current.Request.InputStream;
int count = 0;
byte[] buffer = new byte[1024];
StringBuilder builder = new StringBuilder();
while ((count = s.Read(buffer, 0, 1024)) > 0)
{
builder.Append(Encoding.UTF8.GetString(buffer, 0, count));
}
s.Flush();
s.Close();
s.Dispose();
*/
var buffer = new MemoryStream();
Request.Body.CopyTo(buffer);
#endregion
//我没有使用官方的那种验证数据安全性的方法,我解密出来数据之后,直接拿着商户退款订单号再去查询一下订单状态,然后再更新到数据库中。我嫌麻烦……
var str = Encoding.UTF8.GetString(buffer.GetBuffer());
var wxPayNotifyModel = str.ToObject<RefundsCallbackModel>();
var resource = wxPayNotifyModel?.resource ?? new RefundsCallbackResourceModel();
var decryptStr = AesGcmHelper.AesGcmDecrypt(resource.associated_data, resource.nonce, resource.ciphertext, WxPayConst.APIV3Key);
var decryptModel = decryptStr.ToObject<RefundsCallbackDecryptModel>();
var viewModel = new RefundsCallbackRespModel();
if (string.IsNullOrEmpty(decryptModel.out_trade_no))
{
viewModel.code = "FAIL";
viewModel.message = "数据解密失败";
}
else
{
var resp = await QueryRefunds(decryptModel.out_refund_no);
//然后进行数据库更新处理……等等其他操作
}
return viewModel;
}
工具类方法
WxPayHelper.cs
封装好的专门提供给用户使用的方法,可以直接传参,不需要用户构造工具类、实体,只需要把调用接口所需要的参数准备好传递过来就行了。
public class WxPayHelper
{
/// <summary>
/// 直连商户申请的公众号或移动应用appid。
/// </summary>
private string _appid { set; get; }
/// <summary>
/// 直连商户的商户号,由微信支付生成并下发。
/// </summary>
private string _mchid { set; get; }
/// <summary>
/// 证书序列号,查看证书序列号:https://wechatpay-api.gitbook.io/wechatpay-api-v3/chang-jian-wen-ti/zheng-shu-xiang-guan#ru-he-cha-kan-zheng-shu-xu-lie-hao
/// </summary>
private string _serialNo;
/// <summary>
/// 私钥不包括私钥文件起始的-----BEGIN PRIVATE KEY----- 亦不包括结尾的-----END PRIVATE KEY-----
/// </summary>
private string _privateKey;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="appid">直连商户申请的公众号或移动应用appid</param>
/// <param name="mchid">直连商户的商户号,由微信支付生成并下发</param>
/// <param name="serialNo">证书序列号,查看证书序列号:https://wechatpay-api.gitbook.io/wechatpay-api-v3/chang-jian-wen-ti/zheng-shu-xiang-guan#ru-he-cha-kan-zheng-shu-xu-lie-hao</param>
/// <param name="privateKey">私钥不包括私钥文件起始的-----BEGIN PRIVATE KEY----- 亦不包括结尾的-----END PRIVATE KEY-----</param>
public WxPayHelper(string appid, string mchid, string serialNo, string privateKey)
{
_appid = appid;
_mchid = mchid;
_serialNo = serialNo;
_privateKey = privateKey;
}
/// <summary>
/// 统一下单接口,只对接了必填的字段
/// </summary>
/// <param name="description">商品描述</param>
/// <param name="price">订单总金额,单位为分</param>
/// <param name="out_trade_no">商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一</param>
/// <param name="notify_url">通知URL必须为直接可访问的URL,不允许携带查询串。格式:URL</param>
/// <param name="attach">附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用</param>
/// <param name="currency">货币类型,CNY:人民币,境内商户号仅支持人民币</param>
/// <returns></returns>
public async Task<WxPayRespModel> UnionGenerateOrder(string description, int price, string out_trade_no, string notify_url, string attach = "", string currency = "CNY")
{
var url = "https://api.mch.weixin.qq.com/v3/pay/transactions/app";
var req = new GenerateOrderModelForWxPay
{
appid = this._appid,
mchid = this._mchid,
description = description,
amount = new WxPayAmountModel
{
total = price,
currency = currency
},
out_trade_no = out_trade_no,
attach = attach,
notify_url = notify_url
};
var client = new HttpClient(new WxPayRequestHandler(_mchid, _serialNo, _privateKey));
var bodyJson = new StringContent(req.ToJson(), Encoding.UTF8, "application/json");
var resp = await client.PostAsync(url, bodyJson);
// 注意!!! 这个resp只是http的结果,需要把接口具体返回的值读取出来,如果接口报错的话,这地方可以看到具体的错误信息,我就是在这里入坑的。
var respStr = await resp.Content.ReadAsStringAsync();
// 如果下单成功,就解析返回的结果,把prepay_id解析出来
var viewModel = respStr.ToObject<WxPayRespModel>();
return viewModel;
}
/// <summary>
/// 查询订单
/// </summary>
/// <param name="orderNumber">商户自己的订单号,不是微信生成的订单号</param>
public async Task<WxPayStatusRespModel> QueryOrder(string orderNumber)
{
var url = $"https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{orderNumber}?mchid={_mchid}";
var client = new HttpClient(new WxPayRequestHandler(_mchid, _serialNo, _privateKey));
var resp = await client.GetAsync(url);
var respStr = await resp.Content.ReadAsStringAsync();
var payModel = respStr.ToObject<WxPayStatusRespModel>();
return payModel;
}
/// <summary>
/// 关闭订单
/// </summary>
/// <param name="orderNumber">商户自己的订单号,不是微信生成的订单号</param>
public async Task<ClostOrderRespModel> ClostOrder(string orderNumber)
{
var url = $"https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{orderNumber}/close";
var client = new HttpClient(new WxPayRequestHandler(_mchid, _serialNo, _privateKey));
var bodyJson = new StringContent(new { mchid = this._mchid }.ToJson(), Encoding.UTF8, "application/json");
var resp = await client.PostAsync(url, bodyJson);
var respStr = await resp.Content.ReadAsStringAsync();
var closeModel = new ClostOrderRespModel();
if (!string.IsNullOrEmpty(respStr))
closeModel = respStr.ToObject<ClostOrderRespModel>();
closeModel.StatusCode = resp.StatusCode.ToString();
return closeModel;
}
/// <summary>
/// 微信支付申请退款接口
/// </summary>
/// <param name="out_trade_no">商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一;原支付交易对应的商户订单号。</param>
/// <param name="out_refund_no">商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。</param>
/// <param name="reason">若商户传入,会在下发给用户的退款消息中体现退款原因。</param>
/// <param name="refund">退款金额,币种的最小单位,只能为整数,不能超过原订单支付金额。</param>
/// <param name="total">原支付交易的订单总金额,币种的最小单位,只能为整数。</param>
/// <param name="notify_url">异步接收微信支付退款结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 如果参数中传了notify_url,则商户平台上配置的回调地址将不会生效,优先回调当前传的这个地址。示例值:https://weixin.qq.com ,必须是https的网址</param>
/// <param name="currency">符合ISO 4217标准的三位字母代码,目前只支持人民币:CNY。</param>
/// <returns></returns>
public async Task<RefundsRespModel> Refunds(string out_trade_no, string out_refund_no, string reason, int refund, int total, string notify_url = "", string currency = "CNY")
{
var req = new RefundsRequestModel
{
out_refund_no = out_refund_no,
out_trade_no = out_trade_no,
reason = reason,
notify_url = notify_url,
amount = new RefundsAmountModel
{
refund = refund,
total = total,
currency = currency
}
};
var url = $"https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";
var client = new HttpClient(new WxPayRequestHandler(_mchid, _serialNo, _privateKey));
var bodyJson = new StringContent(req.ToJson(), Encoding.UTF8, "application/json");
var resp = await client.PostAsync(url, bodyJson);
var respStr = await resp.Content.ReadAsStringAsync();
var payModel = respStr.ToObject<RefundsRespModel>();
return payModel;
}
/// <summary>
/// 查询退款结果
/// </summary>
/// <param name="out_refund_no">商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。</param>
public async Task<QueryRefundsOrderRespModel> QueryRefundsOrder(string out_refund_no)
{
var url = $"https://api.mch.weixin.qq.com/v3/refund/domestic/refunds/{out_refund_no}";
var client = new HttpClient(new WxPayRequestHandler(_mchid, _serialNo, _privateKey));
var resp = await client.GetAsync(url);
var respStr = await resp.Content.ReadAsStringAsync();
var payModel = respStr.ToObject<QueryRefundsOrderRespModel>();
return payModel;
}
}
Http请求方法
WxPayRequestHandler.cs
封装好的专用于请求微信支付接口的工具类。
internal class WxPayRequestHandler : DelegatingHandler
{
/// <summary>
/// 直连商户的商户号,由微信支付生成并下发。
/// </summary>
private string _merchantId = string.Empty;
/// <summary>
/// 证书序列号,查看证书序列号:https://wechatpay-api.gitbook.io/wechatpay-api-v3/chang-jian-wen-ti/zheng-shu-xiang-guan#ru-he-cha-kan-zheng-shu-xu-lie-hao
/// </summary>
private string _serialNo = string.Empty;
/// <summary>
/// 私钥不包括私钥文件起始的-----BEGIN PRIVATE KEY----- 亦不包括结尾的-----END PRIVATE KEY-----
/// </summary>
private string _privateKey = string.Empty;
/// <summary>
/// 构造方法
/// </summary>
/// <param name="mchid">直连商户的商户号,由微信支付生成并下发。</param>
/// <param name="serialNo">证书序列号,查看证书序列号:https://wechatpay-api.gitbook.io/wechatpay-api-v3/chang-jian-wen-ti/zheng-shu-xiang-guan#ru-he-cha-kan-zheng-shu-xu-lie-hao</param>
/// <param name="privateKey">私钥不包括私钥文件起始的-----BEGIN PRIVATE KEY----- 亦不包括结尾的-----END PRIVATE KEY-----</param>
public WxPayRequestHandler(string mchid, string serialNo, string privateKey)
{
InnerHandler = new HttpClientHandler();
this._merchantId = mchid;
this._serialNo = serialNo;
this._privateKey = privateKey;
}
protected async override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var auth = await BuildAuthAsync(request);
string value = $"WECHATPAY2-SHA256-RSA2048 {auth}";
request.Headers.Add("Authorization", value);
request.Headers.Add("Accept", "application/json");
request.Headers.Add("User-Agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.0.3705;)");
return await base.SendAsync(request, cancellationToken);
}
private async Task<string> BuildAuthAsync(HttpRequestMessage request)
{
var method = request.Method.ToString();
var body = "";
if (method == "POST" || method == "PUT" || method == "PATCH")
{
var c = request.Content;
body = await c.ReadAsStringAsync();//这里读取的数据一定要跟传入的参数一致,debug时看到的数据与传入的参数不一致是不可以的
}
string uri = request.RequestUri.PathAndQuery;
var timestamp = DateTimeOffset.Now.ToUnixTimeSeconds();
string nonce = Path.GetRandomFileName();
var message = $"{method}\n{uri}\n{timestamp}\n{nonce}\n{body}\n";
var signature = Sign(message);
return $"mchid=\"{_merchantId}\",nonce_str=\"{nonce}\",timestamp=\"{timestamp}\",serial_no=\"{_serialNo}\",signature=\"{signature}\"";
}
private string Sign(string message)
{
byte[] keyData = Convert.FromBase64String(_privateKey);
using (CngKey cngKey = CngKey.Import(keyData, CngKeyBlobFormat.Pkcs8PrivateBlob))
using (RSACng rsa = new RSACng(cngKey))
{
byte[] data = System.Text.Encoding.UTF8.GetBytes(message);
return Convert.ToBase64String(rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
}
}
}
说在最后,代码已经上传到码云和github了,大家可以提提意见。
大神,Core版本发布到Centos服务器上,小程序调起支付API,签名Sign这里报错,请问这个怎么解决,windows的签名方式linux不支持