.NET Core之微信支付之公众号、H5支付篇
前言
本篇主要记录微信支付中公众号及H5支付全过程。
准备篇
公众号或者服务号(并开通微信支付功能)、商户平台中开通JSAPI支付、H5支付。
配置篇
公众号或者服务号中 -------开发-------开发者工具---------web开发者工具-------绑定为开发者
公众号或者服务号中 -------公众号设置--------功能设置 :填写业务域名、JS安全域名、网页授权域名 示例:pay.one.com
商户平台中--------产品中心-------开发配置------JSAPI支付授权目录填写:http://pay.one.com/ http://pay.one.com/WeChatPay/PubPay/-----H5支付填写:pay.one.com
若对配置还有疑问,可参考官方文档:
https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842
https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6
开发篇
JSAPI支付
本Demo是基于Payment 的SDK开发。具体详情可参考:https://github.com/Essensoft/Payment
首先 使用Nuget安装payment:
Install-Package :Essensoft.AspNetCore.Payment.WeChatPay -Version 2.3.2
建一个Model: WeChatPayPubPayViewModel
public class WeChatPayPubPayViewModel { [Required] [Display(Name = "out_trade_no")] public string OutTradeNo { get; set; } [Required] [Display(Name = "body")] public string Body { get; set; } [Required] [Display(Name = "total_fee")] public int TotalFee { get; set; } [Required] [Display(Name = "spbill_create_ip")] public string SpbillCreateIp { get; set; } [Required] [Display(Name = "notify_url")] public string NotifyUrl { get; set; } [Required] [Display(Name = "trade_type")] public string TradeType { get; set; } [Required] [Display(Name = "openid")] public string OpenId { get; set; } }
WeChatPayController:
//微信支付请求客户端(用于处理请求与响应) private readonly IWeChatPayClient _client; private readonly ILogger<WeChatPayController> _logger; private IHttpContextAccessor _accessor; public WeChatPayController(IWeChatPayClient client, IHttpContextAccessor accessor, ILogger<WeChatPayController> logger) { _client = client; _accessor = accessor; _logger = logger; } /// <summary> /// 公众号支付 /// </summary> /// <returns></returns> [HttpGet] public IActionResult PubPay() { WeChatPayPubPayViewModel payModel=new WeChatPayPubPayViewModel() { Body = "微信公众号支付测试", OutTradeNo = DateTime.Now.ToString("yyyyMMddHHmmssfff"), TotalFee = 1,//分 单位 SpbillCreateIp = "127.0.0.1", NotifyUrl = "http://pay.one.com/notify/wechatpay/unifiedorder", TradeType = "JSAPI", OpenId = "" //此处需进行授权 获取OpenId }; return View(payModel); } /// <summary> /// 公众号支付 /// </summary> /// <param name="viewModel"></param> /// <returns></returns> [HttpPost] public async Task<IActionResult> PubPay(WeChatPayPubPayViewModel viewModel) { if(string.IsNullOrEmpty(viewModel.OpenId)) { ViewData["response"] = "请返回上级重新进入此页面以获取最新数据"; return View(); } var request = new WeChatPayUnifiedOrderRequest { Body = viewModel.Body, OutTradeNo = viewModel.OutTradeNo, TotalFee = viewModel.TotalFee, SpbillCreateIp = viewModel.SpbillCreateIp, NotifyUrl = viewModel.NotifyUrl, TradeType = viewModel.TradeType, OpenId = viewModel.OpenId //此处需进行授权 获取OpenId }; var response = await _client.ExecuteAsync(request);if (response.ReturnCode == "SUCCESS" && response.ResultCode == "SUCCESS") { var req = new WeChatPayH5CallPaymentRequest { Package = "prepay_id=" + response.PrepayId }; var parameter = await _client.ExecuteAsync(req); // 将参数(parameter)给 公众号前端 让他在微信内H5调起支付(https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6) ViewData["parameter"] = JsonConvert.SerializeObject(parameter); ViewData["response"] = response.Body; return View(); } ViewData["response"] = response.Body; return View(); }
注意:公众号或者微信内支付,需要授权获取到用户的OpenId。所以,此处我们还需要进行微信授权,而授权方式有两种,一种是静默授权、一种是需要用户同意,区别是 静默授权只能拿到Openid,而经用户同意后可拿到 微信头像、昵称、性别等其他信息。
具体可参阅文档:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842
页面:
@using Newtonsoft.Json @model WeChatPayPubPayViewModel @{ ViewData["Title"] = "公众号支付-统一下单"; } <nav aria-label="breadcrumb"> <ol class="breadcrumb"> <li class="breadcrumb-item"><a asp-controller="WeChatPay" asp-action="Index">微信支付</a></li> <li class="breadcrumb-item active" aria-current="page">@ViewData["Title"]</li> </ol> </nav> <br /> <div class="card"> <div class="card-body"> <form asp-controller="WeChatPay" asp-action="PubPay"> <div asp-validation-summary="All" class="text-danger"></div> <div class="form-group"> <label asp-for="OutTradeNo"></label> <input type="text" class="form-control" asp-for="OutTradeNo" value="@Model?.OutTradeNo" /> </div> <div class="form-group"> <label asp-for="Body"></label> <input type="text" class="form-control" asp-for="Body" value="@Model?.Body" /> </div> <div class="form-group"> <label asp-for="TotalFee"></label> <input type="text" class="form-control" asp-for="TotalFee" value="@Model?.TotalFee" /> </div> <div class="form-group"> <label asp-for="SpbillCreateIp"></label> <input type="text" class="form-control" asp-for="SpbillCreateIp" value="@Model?.SpbillCreateIp" /> </div> <div class="form-group"> <label asp-for="NotifyUrl"></label> <input type="text" class="form-control" asp-for="NotifyUrl" value="@Model?.NotifyUrl" /> </div> <div class="form-group"> <label asp-for="TradeType"></label> <input type="text" class="form-control" asp-for="TradeType" value="@Model?.TradeType" /> </div> <div class="form-group"> <label asp-for="OpenId"></label> <input type="text" class="form-control" asp-for="OpenId" value="@Model?.OpenId" /> </div> <button type="submit" class="btn btn-primary">提交请求</button> <button type="button" class="btn btn-success" id="PayNow">立即支付</button> </form> <hr /> <form class="form-horizontal"> <div class="form-group"> <label>Response:</label> <textarea class="form-control" rows="10">@ViewData["response"]</textarea> </div> <div class="form-group"> <label>Parameter:</label> <textarea class="form-control" rows="3">@ViewData["parameter"]</textarea> </div> </form> </div> </div> @section Scripts { @{await Html.RenderPartialAsync("_ValidationScriptsPartial"); } } <script src="~/lib/jquery/dist/jquery.min.js"></script> <script type="text/javascript"> $(function () { $("#PayNow").on('click', function () { const local = "http://pay.one.com/WeChatPay/PayBack/"; window.location.href ='https://open.weixin.qq.com/connect/oauth2/authorize?appid=@ViewBaig.AppId&redirect_uri=' + encodeURIComponent(local)+'&response_type=code&scope=snsapi_base&state=a#wechat_redirect'; }); }); </script>
此时:PayBack Action如下:
[HttpGet] public async Task<IActionResult> PayBack() { var code = Request.Query["code"]; var state = Request.Query["state"]; OAuthToken tokenModel = new OAuthToken(); //通过code换取token if (!string.IsNullOrEmpty(code)) { _logger.LogWarning("授权成功"); ViewBag.Code = code; tokenModel = OauthApi.GetAuthToken(code, wechatAppId); } var request = new WeChatPayUnifiedOrderRequest { Body = "微信公众号支付测试", OutTradeNo = DateTime.Now.ToString("yyyyMMddHHmmssfff"), TotalFee = 1,//分 单位 SpbillCreateIp = "127.0.0.1", NotifyUrl = "http://pay.one.com/notify/wechatpay/unifiedorder", TradeType = "JSAPI", OpenId = tokenModel.Openid //此处需进行授权 获取OpenId }; var response = await _client.ExecuteAsync(request); _logger.LogWarning($"统一下单接口返回:{response.ReturnCode}"); if (response.ReturnCode == "SUCCESS" && response.ResultCode == "SUCCESS") { var req = new WeChatPayH5CallPaymentRequest { Package = "prepay_id=" + response.PrepayId }; var parameter = await _client.ExecuteAsync(req); // 将参数(parameter)给 公众号前端 让他在微信内H5调起支付(https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6) ViewData["parameter"] = JsonConvert.SerializeObject(parameter); _logger.LogWarning($"统一下单成功,即将调起微信支付:{ViewData["parameter"].ToString()}"); ViewData["response"] = response.Body;
if (ViewData["parameter"] != null)
{
ViewBag.Test = JsonConvert.DeserializeObject(ViewData["parameter"]?.ToString());
}
return View(); } ViewData["response"] = response.Body; return View(); }
其中:OAuthToken是网页授权 返回的实体:
/// 获取网页授权token时,返回的实体 /// </summary> public class OAuthToken : BaseRes { /// <summary> /// 网页授权接口调用凭证。注意:此access_token与基础支持的access_token不同 /// </summary> [JsonProperty("access_token")] public string AccessToken { get; set; } private int _expiresIn; /// <summary> /// access_token接口调用凭证超时时间,单位(秒) /// </summary> [JsonProperty("expires_in")] public int ExpiresIn { get { return _expiresIn; } set { ExpiresTime = DateTime.Now.AddSeconds(value); _expiresIn = value; } } /// <summary> /// 用于刷新access_token /// </summary> [JsonProperty("refresh_token")] public string RefreshToken { get; set; } /// <summary> /// 用户唯一标识。请注意,在未关注公众号时,用户访问公众号的网页,也会产生一个用户和公众号唯一的openid /// </summary> [JsonProperty("openid")] public string Openid { get; set; } /// <summary> /// 用户授权的作用域,使用逗号(,)分隔 /// </summary> [JsonProperty("scope")] public string Scope { get; set; } [JsonProperty("expires_time")] public DateTime ExpiresTime { get; set; } /// <summary> /// 只有在用户将公众号绑定到微信开放平台账号后,才会出现该字段 /// </summary> [JsonProperty("unionid")] public string Unionid { get; set; } }
然后PayBack返回的视图相当于一个支付过渡页中 带有微信的js标识,可以主动调起微信支付,大概视图如下:
@{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <script src="~/lib/jquery/dist/jquery.js"></script> <title>正在支付</title> </head> <body> <div class="row paytop30" style="text-align: center;margin-top: 30px"> <div class="col-xs-12 text-center "> <img src="~/Images/registerNewSite/pay.gif" class="payLoding" style="width: 130px;height:100px" /> <p class="registEidtText" id="backText"></p> </div> </div> <script src="~/lib/layer/layer.js"></script> <script type="text/javascript"> $(function() { @{ var appId = ViewBag.Test?.appId; var timeStamp = ViewBag.Test?.timeStamp; var nonceStr = ViewBag.Test?.nonceStr; var package = ViewBag.Test?.package; var signType = ViewBag.Test?.signType; var paySign = ViewBag.Test?.paySign; } onBridgeReady(); }); function onBridgeReady() { WeixinJSBridge.invoke( 'getBrandWCPayRequest', { "appId": "@appId", "timeStamp": "@timeStamp", "nonceStr": "@nonceStr", "package": "@package", "signType": "@signType", "paySign": "@paySign" }, function(res) { if (res.err_msg == "get_brand_wcpay_request:ok") { // 使用以上方式判断前端返回,微信团队郑重提示: //res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。 $("#backText").html('即将跳转订单详情页'); setTimeout(function() { window.location.href = "支付详情页"; }, 2000); } else { $("#backText").html('即将跳转订单详情页'); setTimeout(function() { window.location.href = "支付详情页";
}, 2000);
}
});
}
if (typeof WeixinJSBridge == "undefined") {
if (document.addEventListener) {
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
} else if (document.attachEvent) {
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
} else {
onBridgeReady();
}
</script>
</body>
</html>
最后 贴一下支付成功后的回调函数:
[Route("notify/wechatpay")] public class WeChatPayNotifyController : Controller { private readonly IWeChatPayNotifyClient _client; private readonly ILogger<WeChatPayNotifyController> _logger; public WeChatPayNotifyController(IWeChatPayNotifyClient client,ILogger<WeChatPayNotifyController> logger) { _client = client; _logger = logger; } /// <summary> /// 统一下单支付结果通知 /// </summary> /// <returns></returns> [Route("unifiedorder")] [HttpPost] public async Task<IActionResult> Unifiedorder() { try { _logger.LogWarning($"进入回调"); var payconfig = OpenApi.GetPayConfig(); var notify = await _client.ExecuteAsync<WeChatPayUnifiedOrderNotify>(Request); _logger.LogWarning($"返回状态码:{notify.ReturnCode}"); if (notify.ReturnCode == "SUCCESS") { _logger.LogWarning($"业务结果码:{notify.ResultCode}"); if (notify.ResultCode == "SUCCESS") { _logger.LogWarning($"支付方式:{notify.TradeType}"); _logger.LogWarning($"商户订单号:{notify.OutTradeNo}"); _logger.LogWarning($"微信支付订单号:{notify.TransactionId}"); _logger.LogWarning($"支付金额:{notify.TotalFee}");
//自己的业务 return WeChatPayNotifyResult.Success; } } return NoContent(); } catch(Exception ex) { _logger.LogWarning($"回调失败:{ex.Message}"); return NoContent(); } } }
然后测试一下支付,查看服务器Log如下:
H5支付
H5支付是指再除开微信浏览器以外的移动端浏览器上进行微信回复操作。
和上面步骤大体一致,有几个地方需要注意
1:客户端IP问题:H5支付的时候,微信支付系统会根据客户端调起的当前Ip 作为支付Ip,若发现 发起支付请求时,ip有问题,则会支付失败,或者提示系统繁忙。这里贴一下我获取IP的代码:
Utils.GetUserIp(_accessor.HttpContext);//页面上调用 /// <summary> /// 穿过代理服务器获取真实IP /// </summary> /// <returns></returns> public static string GetUserIp(this HttpContext context) { var ip = context.Request.Headers["X-Forwarded-For"].FirstOrDefault(); if (string.IsNullOrEmpty(ip)) { ip = context.Connection.RemoteIpAddress.ToString(); } return ip; }
2:TradeType类型应该是:MWEB
3:若调起微信支付成功后,默认回调到支付首页,若需要设置回调页面,则可以再URl中拼接:
/// <summary> /// H5支付 /// </summary> /// <param name="viewModel"></param> /// <returns></returns> [HttpPost] public async Task<IActionResult> H5Pay(WeChatPayH5PayViewModel viewModel) { var request = new WeChatPayUnifiedOrderRequest { Body = viewModel.Body, OutTradeNo = viewModel.OutTradeNo, TotalFee = viewModel.TotalFee, SpbillCreateIp = viewModel.SpbillCreateIp, NotifyUrl = viewModel.NotifyUrl, TradeType = viewModel.TradeType }; var response = await _client.ExecuteAsync(request); // mweb_url为拉起微信支付收银台的中间页面,可通过访问该url来拉起微信客户端,完成支付,mweb_url的有效期为5分钟。 if (response.MwebUrl == null) { ViewData["response"] = response.ReturnMsg; return View(); } return Redirect(response.MwebUrl); }
更多详细可参考文档:https://pay.weixin.qq.com/wiki/doc/api/H5.php?chapter=15_4
4:支付结果通知:
完整Demo
$("#PayNow").on("click", function () { var result = isWeiXin(); var orderId = $(this).attr("data-orderId"); var pay = $(this).attr("data-pay"); var userId = $(this).attr("data-userId"); var account = $(this).attr("data-account"); var matchId = $(this).attr("data-matchId"); if (result === 0) { // layer.msg(pay); const local = "支付网址/WeChatPay/PayBack?pay=" + pay + "&userId=" + parseInt(userId) + "&account=" + account + "&matchId=" + parseInt(matchId) +"&orderId="+orderId; window.location.href = 'https://open.weixin.qq.com/connect/oauth2/authorize?appid=wxdd269ffaec1a1725&redirect_uri=' + encodeURIComponent(local) + '&response_type=code&scope=snsapi_base&state=a#wechat_redirect'; } else if (result === 1) { window.location.href = "/WeChatPay/H5Pay?pay=" + pay + "&userId=" + parseInt(userId) + "&account=" + account + "&matchId=" + parseInt(matchId) + "&orderId=" + orderId; } else { layer.msg('请使用移动设备完成支付'); } });
//判断是否是微信浏览器的函数 function isWeiXin() { //window.navigator.userAgent属性包含了浏览器类型、版本、操作系统类型、浏览器引擎类型等信息,这个属性可以用来判断浏览器类型 if (browser.versions.mobile) {//判断是否是移动设备打开。browser代码在下面 var ua = navigator.userAgent.toLowerCase();//获取判断用的对象 if (ua.match(/MicroMessenger/i) == "micromessenger") { return 0; } else { return 1; } } else { //否则就是PC浏览器打开 return 2; } } var browser = { versions: function () { var u = navigator.userAgent, app = navigator.appVersion; return { //移动终端浏览器版本信息 trident: u.indexOf('Trident') > -1, //IE内核 presto: u.indexOf('Presto') > -1, //opera内核 webKit: u.indexOf('AppleWebKit') > -1, //苹果、谷歌内核 gecko: u.indexOf('Gecko') > -1 && u.indexOf('KHTML') == -1, //火狐内核 mobile: !!u.match(/AppleWebKit.*Mobile.*/), //是否为移动终端 ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/), //ios终端 android: u.indexOf('Android') > -1 || u.indexOf('Linux') > -1, //android终端或uc浏览器 iPhone: u.indexOf('iPhone') > -1, //是否为iPhone或者QQHD浏览器 iPad: u.indexOf('iPad') > -1, //是否iPad webApp: u.indexOf('Safari') == -1 //是否web应该程序,没有头部与底部 }; }(), language: (navigator.browserLanguage || navigator.language).toLowerCase() }
[HttpGet] public async Task<IActionResult> H5Pay() { var pay = Request.Query["pay"]; var userId = Request.Query["userId"]; var account = Request.Query["account"]; var matchId = Request.Query["matchId"]; var orderId = Request.Query["orderId"]; var model = new WeChatPayUnifiedOrderRequest() { Body = "赛事云平台在线报名费", OutTradeNo = DateTime.Now.ToString("yyyyMMddHHmmssfff"), TotalFee = int.Parse(pay),//分 需要*100 得到元 SpbillCreateIp = Utils.GetUserIp(_httpContextAccessor.HttpContext), NotifyUrl = "支付网址/notify/wechatpay/unifiedorder/"+long.Parse(matchId),//带一个数据Id,可以根据自己业务需要替换 TradeType = "MWEB" }; var response = await _client.ExecuteAsync(model); // mweb_url为拉起微信支付收银台的中间页面,可通过访问该url来拉起微信客户端,完成支付,mweb_url的有效期为5分钟。 if (response.MwebUrl == null) { ViewData["response"] = response.ReturnMsg;//异常 _logger.LogError($"H5支付出现异常:{response.ReturnMsg}"); return Redirect(response.MwebUrl); } //将下单数据存入订单支付表 根据自己业务需要自行替换 OrderPayModel orderPay = new OrderPayModel() { OrderId = int.Parse(orderId), OrderNumber = model.OutTradeNo, UserId = int.Parse(userId), LoginAccount = account, IsPay = false, OrderMoney = decimal.Parse(pay), PayType = PayType.MWEB, CreteDateTime = DateTime.Now }; if (!string.IsNullOrEmpty(matchId) && !string.IsNullOrEmpty(orderId)) { var addBack = await _crewManager.AddOrderPay(int.Parse(matchId), orderPay); } return Redirect(response.MwebUrl); }
回调:
/// <summary> /// 统一下单支付结果通知 /// </summary> /// <returns></returns> [Route("unifiedorder/{matchId}")] //可以跟一个参数 [HttpPost] public async Task<IActionResult> Unifiedorder(int matchId) { try { var payconfig = OpenApi.GetPayConfig(); var notify = await _client.ExecuteAsync<WeChatPayUnifiedOrderNotify>(Request); if (notify.ReturnCode == "SUCCESS") { if (notify.ResultCode == "SUCCESS") { //根据订单号修改订单支付状态及金额 OrderPayModel model = new OrderPayModel() { OrderNumber = notify.OutTradeNo, PayMoeny = decimal.Parse(notify.TotalFee), IsPay = true, TransactionId = notify.TransactionId, PaidDate = DateTime.Now };
//根据自己项目业务自行调整 if (matchId!=0) { var result = await _crewManager.BackOrderPay(matchId, model); } return WeChatPayNotifyResult.Success; } } return NoContent(); } catch (Exception ex) { _logger.LogInformation($"回调失败:{ex.Message}"); return NoContent(); } }
注意:
1、同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。
2、后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信会判定本次通知失败,重新发送通知,直到成功为止(在通知一直不成功的情况下,微信总共会发起10次通知,通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m),但微信不保证通知最终一定能成功。
3、在订单状态不明或者没有收到微信支付结果通知的情况下,建议商户主动调用微信支付【查询订单API】确认订单状态。
特别提醒:
1、商户系统对于支付结果通知的内容一定要做签名验证,并校验返回的订单金额是否与商户侧的订单金额一致,防止数据泄漏导致出现“假通知”,造成资金损失。
2、当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
最后可以测试下H5支付,查看返回的Log:
本文只是抛砖引玉,更多具体支付场景和代码,还需各位看官结合自己项目量身定做。
更多示例Demo可入群获取。
- 感谢你的阅读。如果你觉得这篇文章对你有帮助或者有启发,就请推荐一下吧~你的精神支持是博主强大的写作动力。欢迎转载!
- 博主的文章没有高度、深度和广度,只是凑字数。由于博主的水平不高(其实是个菜B),不足和错误之处在所难免,希望大家能够批评指出。
- 欢迎加入.NET 从入门到精通技术讨论群→523490820 期待你的加入
- 不舍得打乱,就永远学不会复原。被人嘲笑的梦想,才更有实现的价值。
- 我的博客:http://www.cnblogs.com/zhangxiaoyong/