.NET 小程序微信用户支付
微信支付有两种模式:微信用户主动发起的支付、签约委托支付协议后自动支付。
自动支付又分为两种:首次支付时签约、纯签约。
首次支付时签约和纯签约在后续周期若需要发起自动扣款时,需要在应用服务中发起申请扣款,申请扣款不会立即到账,有处理延时,可通过通知url接收到账提醒;
首次支付时签约和纯签约均需要指定在商户平台中配置的模版id(plan_id),但普通商户是没有那个‘委托代扣模版’的菜单,若需要就要找串串开通,鄙视👎
本次实现的是微信用户主动发起的支付行为扣费。
1、小程序端通过接口wx.requestPayment({})实现支付发起,拉取支付框。本请求接口有两个很关键的参数package、paySign 。请看以下代码:
wx.requestPayment({ 'timeStamp': result.timeStamp,//时间戳 'nonceStr': result.nonceStr,//随机数 'package': result.package,//prepay_id=xxx 'signType': result.signType,//签名类型,一般为MD5 'paySign': result.paySign,//已签名的参数串 'success': function (res) { app.sysShowToast('充值成功!正在进行后台续费操作,请稍后...', 'success',2000); //支付成功后,后台续费保存 that.buyCombo(comboId, outTradeNo); }, 'fail': function (res) { app.sysShowToast('充值失败:' + JSON.stringify(res), 'fail', 2000); } })
这里支付请求的参数我全部都是从后台服务中处理好后返回到小程序前端的。我的后端请求代码如下(仅参考):
wx.request({ url: app.globalData.serverUrl + '/Weixin/WXPay', method: 'POST', data: { comboId: comboId, /*套餐id*/ openid: app.globalData.openId, }, header: { 'content-type': 'application/json' }, success: function (res) { if (res.statusCode === 200) { if (res.data.IsSuccess)//成功 { var result = JSON.parse(res.data.Result); var outTradeNo = result.outTradeNo; //拉取支付框 wx.requestPayment({ 'timeStamp': result.timeStamp,//时间戳 'nonceStr': result.nonceStr,//随机数 'package': result.package,//prepay_id=xxx 'signType': result.signType,//签名类型,一般为MD5 'paySign': result.paySign,//已签名的参数串 'success': function (res) { app.sysShowToast('充值成功!正在进行后台续费操作,请稍后...', 'success',2000); //支付成功后,后台续费保存 that.buyCombo(comboId, outTradeNo); }, 'fail': function (res) { app.sysShowToast('充值失败:' + JSON.stringify(res), 'fail', 2000); } }) } } else{ app.sysShowModal("充值失败", "链接地址无效!") } }, fail: function (err) { app.sysShowModal("连接错误", "无法访问到服务端或请求超时!") } })
补充一句:代码中多次出现的全局方法app.sysShowToast()、app.sysShowModal()
是我封装好的,放置在app.js中。以下代码中包含了对模态框、提示框的封装,感兴趣的朋友可以直接拿去用

//模态框 sysShowModal: function (title, content, showCancel = false, pages = "", isNavigation = false, isTab = false) { var that=this; wx.showModal({ title: title==null?'':title, content: content==null?'':content, showCancel: showCancel, success: function (res) { if (res.confirm) { if (isNavigation && isTab && !that.isEmpty(pages)) { //跳转到tab页 wx.switchTab({ url: pages }); } else if (isNavigation && !isTab && !that.isEmpty(pages)){ //跳转到指定页 wx.navigateTo({ url: pages }) } } else{ app.sysShowModal('您已取消操作'); } } }); }, //模态框(确定后自动返回上一页) sysShowModalBackLastPage: function (title, content,showCancel = false) { var that=this; wx.showModal({ title: title == null ? '' : title, content: content == null ? '' : content, showCancel: showCancel, success: function (res) { if (res.confirm) { that.backLastPage(); } else { wx.showToast({ title: '您已取消操作', icon: 'none', //icon: 'warn', duration: 1000 }); } } }); }, //提示框 sysShowToast: function (title, icon="none", duration = 1000, pages = "", isNavigation = false, isTab = false) { var that = this; wx.showToast({ title: title, icon: icon, duration: duration, //弹出提示框时长 mask: true, success(data) { setTimeout(function () { if (isNavigation && isTab && !that.isEmpty(pages)) { //跳转到tab页 wx.switchTab({ url: pages }); } else if (isNavigation && !isTab && !that.isEmpty(pages)) { //跳转到指定页 wx.navigateTo({ url: pages }) } }, duration) //延迟时间 } }); }, //提示框(自动返回上一页) sysShowToastBackLastPage: function (title, icon = "none", duration = 1000){ var that = this; wx.showToast({ title: title, icon: icon, duration: duration, //弹出提示框时长 mask: true, success(data) { setTimeout(function () { //自动回到上一页 that.backLastPage(); }, duration) //延迟时间 } }); }, //判断字符串是否为空的方法 isEmpty:function (obj) { if(typeof obj == "undefined" || obj == null || obj == "" || obj == 'null') { return true; } else { return false; } }, IsEmpty:function (obj) { if (typeof obj == "undefined" || obj == null || obj == "" || obj == 'null') { return true; } else { return false; } }, //返回上一页(并刷新返回的页面) backLastPage: function () { var pages = getCurrentPages();//当前页面栈 if (pages.length > 1) { var beforePage = pages[pages.length - 2];//获取上一个页面实例对象 beforePage.onLoad();//触发父页面中的方法 } wx.navigateBack({ delta: 1 }); },
2、小程序的前端就是上面的代码,关键在后端逻辑。
后端处理思路及步骤:加工拼凑签名串A->把签名串A带入微信统一下单接口获得预付款prepay_id->加工拼凑签名串B得到两个关键参数package、paySign,和其他参数一起返回给小程序前端。为实现预支付我建立了两个类:一个基础配置类、一个预支付实现类(继承基础配置类)。完整代码如下:
基础配置类:请自己配置相关变量为自身小程序的数据

using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using System.Web; namespace Easyman.BLL.WXService { public class BaseService { /// <summary> /// AppID /// </summary> protected static string _appId = "wxxxxxxxxxxxxxxx"; /// <summary> /// AppSecret /// </summary> protected static string _appSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx"; /// <summary> /// 微信服务器提供的获得登录后的用户信息的接口 /// </summary> protected static string _wxLoginUrl = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code"; /// <summary> /// 微信商户号 /// </summary> protected static string _mch_id = "00000000"; /// <summary> /// 微信商户号api密匙 /// </summary> protected static string _mchApiKey = "xxxxxxxxxxxxxxxxxxxxxxx"; /// <summary> /// 通知地址 /// </summary> protected static string _notify_url = "http://www.weixin.qq.com/wxpay/pay.php"; /// <summary> /// 支付类型 /// </summary> protected static string _trade_type = "JSAPI"; /// <summary> /// 微信下单统一接口 /// </summary> protected static string _payUrl = "https://api.mch.weixin.qq.com/pay/unifiedorder"; /// <summary> /// 支付中签约接口 /// </summary> protected static string _signUrl = "https://api.mch.weixin.qq.com/pay/contractorder"; protected static string _planId = "";//协议模板id,在‘商户平台->高级业务->委托代扣签约管理’中去申请 /// <summary> /// 生成订单号 /// </summary> /// <returns></returns> protected static string GetRandomTime() { Random rd = new Random();//用于生成随机数 string DateStr = DateTime.Now.ToString("yyyyMMddHHmmssMM");//日期 string str = DateStr + rd.Next(10000).ToString().PadLeft(4, '0');//带日期的随机数 return str; } /// <summary> /// 生成随机串 /// </summary> /// <param name="length">字符串长度</param> /// <returns></returns> protected static string GetRandomString(int length) { const string key = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; if (length < 1) return string.Empty; Random rnd = new Random(); byte[] buffer = new byte[8]; ulong bit = 31; ulong result = 0; int index = 0; StringBuilder sb = new StringBuilder((length / 5 + 1) * 5); while (sb.Length < length) { rnd.NextBytes(buffer); buffer[5] = buffer[6] = buffer[7] = 0x00; result = BitConverter.ToUInt64(buffer, 0); while (result > 0 && sb.Length < length) { index = (int)(bit & result); sb.Append(key[index]); result = result >> 5; } } return sb.ToString(); } /// <summary> /// 获取时间戳 GetTimeStamp /// </summary> /// <returns></returns> protected static long GetTimeStamp() { TimeSpan cha = (DateTime.Now - TimeZone.CurrentTimeZone.ToLocalTime(new System.DateTime(1970, 1, 1))); long t = (long)cha.TotalSeconds; return t; } /// <summary> /// MD5签名方法 /// </summary> /// <param name="inputText">加密参数</param> /// <returns></returns> protected static string MD5(string inputText) { System.Security.Cryptography.MD5 md5 = new MD5CryptoServiceProvider(); byte[] fromData = System.Text.Encoding.UTF8.GetBytes(inputText); byte[] targetData = md5.ComputeHash(fromData); string byte2String = null; for (int i = 0; i < targetData.Length; i++) { byte2String += targetData[i].ToString("x2"); } return byte2String; } /// <summary> /// HMAC-SHA256签名方式 /// </summary> /// <param name="message"></param> /// <param name="secret"></param> /// <returns></returns> protected static string HmacSHA256(string message, string secret) { secret = secret ?? ""; var encoding = new System.Text.UTF8Encoding(); byte[] keyByte = encoding.GetBytes(secret); byte[] messageBytes = encoding.GetBytes(message); using (var hmacsha256 = new HMACSHA256(keyByte)) { byte[] hashmessage = hmacsha256.ComputeHash(messageBytes); return Convert.ToBase64String(hashmessage); } } /// <summary> /// 将Model对象转化为url参数形式 /// </summary> /// <param name="obj"></param> /// <param name="url"></param> /// <returns></returns> protected static string ModelToUriParam(object obj) { PropertyInfo[] propertis = obj.GetType().GetProperties(); StringBuilder sb = new StringBuilder(); foreach (var p in propertis) { var v = p.GetValue(obj, null); if (v == null) continue; sb.Append(p.Name); sb.Append("="); sb.Append(v.ToString()); //sb.Append(HttpUtility.UrlEncode(v.ToString())); sb.Append("&"); } sb.Remove(sb.Length - 1, 1); return sb.ToString(); } } }
预支付实现类:

using Easyman.Common.ApiRequest; using Easyman.Common.FW; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using System.Xml; namespace Easyman.BLL.WXService { /// <summary> /// 微信支付 /// </summary> public class WxPayService : BaseService { #region 一、微信用户主动支付 /// <summary> /// 微信用户主动发起的支付(返回必要的参数:package、paySign) /// </summary> /// <param name="openid"></param> /// <param name="payFee"></param> /// <param name="clientId"></param> /// <param name="isSign"></param> /// <returns></returns> public static string GetPayParams(string openid, decimal merFee,string merBody,string merAttach ,string clientId = "139.196.212.68") { PaySign sign = GetPaySign(openid, merFee, merBody, merAttach, clientId); string signs = ModelToUriParam(sign) + "&key=" + _mchApiKey; string md5Signs = MD5(signs).ToUpper();//MD5签名 string body = GenerateBodyXml(sign, md5Signs); string res = Request.PostHttp(_payUrl, body); #region 得到prepay_id //获取xml数据 XmlDocument doc = new XmlDocument(); doc.LoadXml(res); //xml格式转json string json = Newtonsoft.Json.JsonConvert.SerializeXmlNode(doc); JObject jo = (JObject)JsonConvert.DeserializeObject(json); string prepay_id = jo["xml"]["prepay_id"]["#cdata-section"].ToString(); #endregion var resSign= new ResSign { appId = _appId, nonceStr = sign.nonce_str, package = "prepay_id=" + prepay_id, signType = "MD5", timeStamp = GetTimeStamp().ToString(), key = _mchApiKey }; var pars= ModelToUriParam(resSign); return JSON.DecodeToStr(new { timeStamp = resSign.timeStamp, nonceStr = resSign.nonceStr, package = resSign.package, paySign = MD5(pars).ToUpper(), signType = resSign.signType, outTradeNo=sign.out_trade_no//商户订单号 }); } /// <summary> /// 组装签名对象 /// </summary> /// <param name="openid"></param> /// <param name="clientId"></param> /// <param name="payFee"></param> /// <returns></returns> private static PaySign GetPaySign(string openid,decimal merFee, string merBody, string merAttach, string clientId) { PaySign paySign = new PaySign { appid = _appId, attach = merAttach, body = merBody, mch_id = _mch_id, nonce_str = GetRandomString(30), notify_url = _notify_url, openid = openid, out_trade_no = GetRandomTime(), spbill_create_ip = clientId, total_fee = (Math.Round(merFee * 100, 0)).ToString(),//转化为单位:分,且只能为整型 trade_type = _trade_type }; return paySign; } /// <summary> /// 生成交易的xml /// </summary> /// <param name="obj"></param> /// <returns></returns> private static string GenerateBodyXml(object obj,string md5Signs) { PropertyInfo[] propertis = obj.GetType().GetProperties(); StringBuilder sb = new StringBuilder(); sb.Append("<xml>"); foreach (var p in propertis) { var v = p.GetValue(obj, null); if (v == null) continue; sb.AppendFormat("<{0}>{1}</{0}>", p.Name, v.ToString()); } sb.AppendFormat("<sign>{0}</sign>", md5Signs); sb.Append("</xml>"); return sb.ToString(); } #region Model类 /// <summary> /// 签名类A(请注意,此类的属性字段顺序不可调整) /// 微信预支付前面规则,是按参数ASCII码依次排列的,以下属性已人为排列 /// </summary> public class PaySign { public string appid { get; set; } /// <summary> /// 附加数据(描述) /// </summary> public string attach { get; set; } /// <summary> /// 商品描述 /// </summary> public string body { get; set; } /// <summary> /// 商户号 /// </summary> public string mch_id { get; set; } /// <summary> /// 小于32位的随机数 /// </summary> public string nonce_str { get; set; } /// <summary> /// 通知地址 /// </summary> public string notify_url { get; set; } /// <summary> /// 微信用户openid /// </summary> public string openid { get; set; } /// <summary> /// 商户订单号 /// </summary> public string out_trade_no { get; set; } /// <summary> /// 客户端ip /// </summary> public string spbill_create_ip { get; set; } /// <summary> /// 订单金额 /// </summary> public object total_fee { get; set; } /// <summary> /// 支付类型 /// </summary> public string trade_type { get; set; } } /// <summary> /// 返回前端的签名类B(请注意,此类的属性字段顺序不可调整) /// </summary> public class ResSign { public string appId { get; set; } /// <summary> /// 小于32位的随机数 /// </summary> public string nonceStr { get; set; } /// <summary> /// package /// </summary> public string package { get; set; } /// <summary> /// signType /// </summary> public string signType { get; set; } /// <summary> /// timeStamp /// </summary> public string timeStamp { get; set; } /// <summary> /// key /// </summary> public string key { get; set; } } #endregion #endregion } }
预支付类中请求微信统一下单接口的post代码如下:

/// <summary> /// post请求 /// </summary> /// <param name="url">请求url(不含参数)</param> /// <param name="body">请求body. 如果是soap"text/xml; charset=utf-8"则为xml字符串;post的cotentType为"application/x-www-form-urlencoded"则格式为"roleId=1&uid=2"</param> /// <param name="timeout">等待时长(毫秒)</param> /// <param name="contentType">Content-type http标头的值. post默认为"text/xml;charset=UTF-8"</param> /// <returns></returns> public static string PostHttp(string url, string body,string contentType= "text/xml;charset=utf-8") { HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url); httpWebRequest.ContentType = contentType; httpWebRequest.Method = "POST"; //httpWebRequest.Timeout = timeout;//设置超时 if (contentType.Contains("text/xml")) { httpWebRequest.Headers.Add("SOAPAction", "http://tempuri.org/mediate"); } byte[] btBodys = Encoding.UTF8.GetBytes(body); httpWebRequest.ContentLength = btBodys.Length; httpWebRequest.GetRequestStream().Write(btBodys, 0, btBodys.Length); HttpWebResponse httpWebResponse; try { httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse(); } catch (WebException ex) { httpWebResponse = (HttpWebResponse)ex.Response; } //HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse(); StreamReader streamReader = new StreamReader(httpWebResponse.GetResponseStream(), Encoding.UTF8); string responseContent = streamReader.ReadToEnd(); httpWebResponse.Close(); streamReader.Close(); httpWebRequest.Abort(); httpWebResponse.Close(); return responseContent; }
以上就是全部代码,在支付实现过程中碰到很多坑,在这里做特别提示:
1)签名串必须按照ASCII由小到大排序。
2)小程序前端的支付请求一定注意参数变量不要写错了(我在开发时把两个变量值写反了,报鉴权失败 code=-1.好半天才发现写错了),下面是血的教训截图:
以上,希望能帮到大家。