最近公司小程序积分商城添加了一个新的需求,以前都是积分兑换商品,现在支持积分和金额可以购买商品.然后涉及了微信支付调用接口. 总的来说比以前过的支付宝接口要更为繁琐些. 因为不支持本地测试,要在线上环境或者内网穿透方式才能进行测试.搞了几天虽然踩了几个坑,但是最后还在功能开发成功,
开发环境:.springboot和mybatisplus框架()
1、微信JSAPI支付官方文档:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_3&index=1;
1、必须申请微信公众平台(企业用户开通);
2、必须开通小程序平台,并与微信公众平台进行绑定;
3、必须开通微信商户平台,并妥善保管号商户号和商户密钥;
(值得说明的是:微信商户密钥在拿到手之后,请首先重置三次以上,具体原因不清楚,但是不重置的话,后期开发的时候,微信统一下单接口会频繁报错,而且错误信息莫名其妙,主要报错信息为:“签名错误”,即使你的参数签名在微信的签名校验工具中校验通过,也会提示你“签名错误”,但是此时你无需对接口做任何改动,只需要重置商户密钥3次以上,此问题便可解决)
4、网站升级https协议,因为调用微信支付成功后,微信服务器会对你传值的回调地址(notify_url字段,可以理解为具体的业务逻辑处理方法路径)进行回调,(虽然目前统一下单接口可以回调http协议接口,但是仍然建议网站升级为https协议);
1、统一下单接口,后台通过统一下单接口,向微信请求下单支付,微信后台接到参数后,会生成一个商户订单,并将预下单id(prepay_id 这个返回字段很重要)返回给后台;
2、后台接收微信返回值,进行二次签名,并将签名的参数返回给小程序前台;
3、小程序端接收到签名参数后,调用 wx.requestPayment 方法,传入参数,调起收银台;
4、用户支付后,微信服务器处理本次支付情况,并回调后台业务处理接口。
1、微信支付model类:
public class PayModel { ////// 统一下单API /// public static string orderUrl = ConfigurationManager.AppSettings["WXunifiedorder"].ToString(); ////// 支付结果通知API /// public static string notifyUrl = ConfigurationManager.AppSettings["WxPayNotifyurl"].ToString(); ////// 查询订单API /// public static string queryUrl = ConfigurationManager.AppSettings["WxPayQueryOrder"].ToString(); ////// 小程序唯一标识 /// public static string AppID = ConfigurationManager.AppSettings["WxAppId"].ToString(); ////// 小程序的 app secret /// public static string secret = ConfigurationManager.AppSettings["WxSecret"].ToString(); ////// 商户号(微信支付分配的商户号) /// public static string mchid = ConfigurationManager.AppSettings["WxMerchantNo"].ToString(); //////商户平台设置的密钥key /// public static string WxMerchantKey = ConfigurationManager.AppSettings["WxMerchantKey"].ToString(); ////// 随机字符串不长于 32 位 /// public static string nonceStr = RandomNum.CreateRandomNum(32).ToUpper(); ////// 时间戳 从1970年1月1日00:00:00至今的秒数,即当前的时间 /// public static string timeStamp = Convert.ToInt64((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalSeconds).ToString(); ////// 交易类型 小程序取值如下:JSAPI /// public static string tradeType = "JSAPI"; ////// 签名类型 默认为MD5,支持HMAC-SHA256和MD5。 /// public static string signType = "MD5"; ////// 商品描述 商品简单描述,该字段请按照规范传递 /// public static string body = "挪威躺椅"; ////// 附加数据 在查询API和支付通知中原样返回 /// public static string attach = "商城支付"; ////// 签名,参与签名参数:appid,mch_id,transaction_id,out_trade_no,nonce_str,key /// public string sign = ""; ////// 微信订单号,优先使用 /// public string transactionid = ""; ////// 商户系统内部订单号 /// public string out_trade_no = ""; ////// 订单金额 /// public decimal totalfee=0; } public class OrderReloadModel { public string return_msg { get; set; } public string return_code { get; set; } public string orderNo { get; set; } public string AppID { get; set; } public string package { get; set; } public string timeStamp { get; set; } public string nonecStr { get; set; } public string signType { get; set; } public string paysign { get; set; } }
2、微信支付帮助类
publi class PayHelper { #region 生成签名 ////// 获取签名数据 /// /// /// ///public static string GetSignInfo(Dictionary strParam, string key) { int i = 0; string sign = string.Empty; StringBuilder sb = new StringBuilder(); try { foreach (KeyValuePair temp in strParam) { if (temp.Value == "" || temp.Value == null || temp.Key.ToLower() == "sign") { continue; } i++; sb.Append(temp.Key.Trim() + "=" + temp.Value.Trim() + "&"); } sb.Append("key=" + key.Trim() + ""); sign = CryptoService.Md5EncryptStr(sb.ToString()).ToUpper(); LogHelper.Writer("服务端参数:" + sb.ToString() + "签名:" + sign, "", 5); } catch (Exception ex) { throw ex; } return sign; } #endregion #region XML 处理 /// /// 获取XML值 /// /// XML字符串 /// 字段值 ///public static string GetXmlValue(string strXml, string strData) { string xmlValue = string.Empty; XmlDocument xmlDocument = new XmlDocument(); xmlDocument.LoadXml(strXml); var selectSingleNode = xmlDocument.DocumentElement.SelectSingleNode(strData); if (selectSingleNode != null) { xmlValue = selectSingleNode.InnerText; } return xmlValue; } /// /// 集合转换XML数据 (拼接成XML请求数据) /// /// 参数 ///public static string CreateXmlParam(Dictionary strParam) { StringBuilder sb = new StringBuilder(); try { sb.Append(" "); foreach (KeyValuePair "); } catch (Exception ex) { throw ex; // AddLog("PayHelper", "CreateXmlParam", ex.Message, ex); } var a = sb.ToString(); return sb.ToString(); } ///k in strParam) { if (k.Key == "attach" || k.Key == "body" || k.Key == "sign") { sb.Append("<" + k.Key + ">" + k.Key + ">"); } else { sb.Append("<" + k.Key + ">" + k.Value + "" + k.Key + ">"); } } sb.Append(" /// XML数据转换集合(XML数据拼接成字符串) /// /// ///public static Dictionary GetFromXml(string xmlString) { Dictionary sParams = new Dictionary (); try { XmlDocument doc = new XmlDocument(); doc.LoadXml(xmlString); XmlElement root = doc.DocumentElement; int len = root.ChildNodes.Count; for (int i = 0; i < len; i++) { string name = root.ChildNodes[i].Name; if (!sParams.ContainsKey(name)) { sParams.Add(name.Trim(), root.ChildNodes[i].InnerText.Trim()); } } } catch (Exception ex) { throw ex; } return sParams; } /// /// 返回通知 XML /// /// /// ///public static string GetReturnXml(string returnCode, string returnMsg) { StringBuilder sb = new StringBuilder(); sb.Append(" "); sb.Append(" "); return sb.ToString(); } #endregion ///"); sb.Append(" "); sb.Append(" /// 获得Post过来的数据 /// ///public static string GetPostStr() { Int32 intLen = Convert.ToInt32(HttpContext.Current.Request.InputStream.Length); byte[] b = new byte[intLen]; HttpContext.Current.Request.InputStream.Read(b, 0, intLen); return Encoding.UTF8.GetString(b); } /// /// 模拟POST提交 (不需要微信API证书) /// /// 请求地址 /// xml参数 ///返回结果 public static string PostHttpResponse(string url, string xmlParam) { HttpWebRequest myHttpWebRequest = (HttpWebRequest)HttpWebRequest.Create(url); myHttpWebRequest.Method = "POST"; myHttpWebRequest.ContentType = "application/x-www-form-urlencoded;charset=utf-8"; // Encode the data byte[] encodedBytes = Encoding.UTF8.GetBytes(xmlParam); myHttpWebRequest.ContentLength = encodedBytes.Length; // Write encoded data into request stream Stream requestStream = myHttpWebRequest.GetRequestStream(); requestStream.Write(encodedBytes, 0, encodedBytes.Length); requestStream.Close(); HttpWebResponse result; try { result = (HttpWebResponse)myHttpWebRequest.GetResponse(); } catch { return string.Empty; } if (result.StatusCode == HttpStatusCode.OK) { using (Stream mystream = result.GetResponseStream()) { using (StreamReader reader = new StreamReader(mystream)) { return reader.ReadToEnd(); } } } return null; }////// 获取客户端IP /// ///public static string GetUserIP() { string ipv4 = String.Empty; foreach (IPAddress ip in Dns.GetHostAddresses(GetClientIP())) { if (ip.AddressFamily.ToString() == "InterNetwork") { ipv4 = ip.ToString(); break; } } if (ipv4 != String.Empty) { return ipv4; } // 利用 Dns.GetHostEntry 方法,由获取的 IPv6 位址反查 DNS 纪录, // 再逐一判断何者为 IPv4 协议,即可转为 IPv4 位址。 foreach (IPAddress ip in Dns.GetHostEntry(GetClientIP()).AddressList) //foreach (IPAddress ip in Dns.GetHostAddresses(Dns.GetHostName())) { if (ip.AddressFamily.ToString() == "InterNetwork") { ipv4 = ip.ToString(); break; } } return ipv4; } }
3、微信支付加密类(MD5加密):
public static string Md5EncryptStr(string input) { var result = string.Empty; if (string.IsNullOrEmpty(input)) return result; using (var md5 = MD5.Create()) { result = GetMd5Hash(md5, input); } return result; }
4、统一下单接口:
public JsonResult GetPrayPayId(string openId, string orderNo)
{
Result result = new Result();
List
try
{
#region 统一下单接口API接口调用参数准备
//获取请求数据
Dictionary
//小程序ID
strParam.Add("appid", PayModel.AppID);//商品描述 对商品的简单描述,次字段值会在 订单详情--商品 字段展示出来
strParam.Add("body", PayModel.body);
//商户号 微信商户平台商户号,10位数字
strParam.Add("mch_id", PayModel.mchid);
//随机字符串
strParam.Add("nonce_str", PayModel.nonceStr);
//通知地址 (异步接收微信支付结果通知的回调地址,可以看作一个后台方法的完整路径,通知url必须为外网可访问的url,不能携带参数。)
strParam.Add("notify_url", PayModel.notifyUrl);
//用户标识
strParam.Add("openid", openId);
//商户订单号 必须保持唯一,如果针对同一个订单重复提交,在订单信息(如订单金额)发生改变时,会导致prepay_id为空
strParam.Add("out_trade_no", orderNo);
//终端IP
strParam.Add("spbill_create_ip", PayHelper.GetUserIP());
//标价金额 已分为单位
strParam.Add("total_fee", order_cent.ToString());
//交易类型
strParam.Add("trade_type", PayModel.tradeType);
strParam.Add("sign", PayHelper.GetSignInfo(strParam, PayModel.WxMerchantKey));
#endregion
#region 统一下单接口返回结果
//获取预支付ID
string preInfo = PayHelper.PostHttpResponse(PayModel.orderUrl, PayHelper.CreateXmlParam(strParam));
string return_code = PayHelper.GetXmlValue(preInfo, "return_code");
string return_msg = PayHelper.GetXmlValue(preInfo, "return_msg");
LogHelper.Writer("统一下单请求发送至微信,微信接口统一下单方法返回结果:/WXWecahtPay/GetPrayPayId 参数【" + strParam + "】,微信平台返回结果【" + preInfo + "】", "", 5);
#endregion
#region 将统一下单的返回参数返回给小程序前台
OrderReloadModel info = new OrderReloadModel();
if (return_code == "SUCCESS")
{
//再次签名
string nonecStr = PayModel.nonceStr;
string timeStamp = PayModel.timeStamp;
string package = "prepay_id=" + PayHelper.GetXmlValue(preInfo, "prepay_id");
Dictionary
singInfo.Add("appId", PayModel.AppID);
singInfo.Add("nonceStr", nonecStr);
singInfo.Add("package", package);
singInfo.Add("signType", PayModel.signType);
singInfo.Add("timeStamp", timeStamp);
//将二次签名后的参数返回给小程序
info.return_msg = return_msg;
info.return_code = return_code;
info.orderNo = orderNo;
info.AppID = PayModel.AppID;
info.package = package;
info.timeStamp = timeStamp;
info.nonecStr = nonecStr;
info.signType = PayModel.signType;
info.paysign = PayHelper.GetSignInfo(singInfo, PayModel.WxMerchantKey);
listMsg.Add(info);
if (listMsg.Count > 0)
{
result.ResultCode = Infrastructure.Enum.ReusltCode.OK;
result.ResultObj = listMsg;
}
}
else
{
info = new OrderReloadModel();
info.return_msg = return_msg;
info.return_code = return_code;
info.orderNo = orderNo;
listMsg.Add(info);
result.ResultCode = Infrastructure.Enum.ReusltCode.Fail;
result.ResultObj = listMsg;
}
#endregion
LogHelper.Writer("返回小程序支付参数接口 方法:/WXWecahtPay/GetPrayPayId 参数【string openId=" + openId + ", string orderNo=" + orderNo + "】,返回结果【" + JsonConvert.SerializeObject(result) + "】", "", 5);
}
catch (Exception ex)
{
result.ResultCode = Infrastructure.Enum.ReusltCode.Exception;
result.ResultMsg = ex.Message;
LogHelper.Writer("返回小程序支付参数接口:/WXWecahtPay/GetPrayPayId 接口异常 异常信息【" + ex.Message + "】", "", 5);
}
return new IMG_JsonResult(result);
}
5、支付成功回调方法:
////// 支付结果通知API /// ///[HttpPost] public JsonResult OrderNotify() { string strResult = string.Empty; try { //1.将微信小程序请求支付的参数传入body中,提交到微信服务器,并获取微信通知的参数 string strXML = PayHelper.GetPostStr(); if (string.IsNullOrEmpty(strXML)) { strResult = "未检测到支付业务!"; return new JsonResult(strResult); } LogHelper.Writer("支付请求发送至微信,微信接口回调方法:/WXWecahtPay/OrderNotify 参数【" + strXML + "】", "", 5); //判断是否请求成功 if (PayHelper.GetXmlValue(strXML, "return_code") == "SUCCESS") { //判断是否支付成功 if (PayHelper.GetXmlValue(strXML, "result_code") == "SUCCESS") { //获得本次支付的签名 string getSign = PayHelper.GetXmlValue(strXML, "sign"); //验证签名是否有效 string sign = PayHelper.GetSignInfo(PayHelper.GetFromXml(strXML), PayModel.WxMerchantKey); if (sign == getSign) { //校验订单信息 string wxOrderNum = PayHelper.GetXmlValue(strXML, "transaction_id"); //微信订单号 string orderNum = PayHelper.GetXmlValue(strXML, "out_trade_no"); //商户订单号 string orderTotal = PayHelper.GetXmlValue(strXML, "total_fee"); string openid = PayHelper.GetXmlValue(strXML, "openid"); string payMark = PayHelper.GetXmlValue(strXML, "transaction_id"); //微信支付订单号 //2.更新订单的相关状态 你的业务处理代码...//3.返回一个xml格式的结果给微信服务器,完成支付流程,避免微信重复回调我们的服务器,造成服务器不必要的开支 strResult = PayHelper.GetReturnXml("SUCCESS", "OK"); } else { strResult = PayHelper.GetReturnXml("FAIL", "签名不一致!"); } } else { strResult = PayHelper.GetReturnXml("FAIL", "支付通知失败!"); } } else { strResult = PayHelper.GetReturnXml("FAIL", "支付通知失败!"); } LogHelper.Writer("支付请求发送至微信,微信接口回调方法:/WXWecahtPay/OrderNotify 参数【" + strXML + "】 接口处理结果【" + strResult + "】", "", 5); } catch (Exception ex) { strResult = ex.Message; LogHelper.Writer("支付请求发送至微信,微信接口回调方法:/WXWecahtPay/OrderNotify 接口异常 异常信息【" + ex.Message + "】", "", 5); } return new JsonResult(strResult); }