最近公司app项目的开发,有幸参与到该项目中,并且由我独立负责服务端的开发。本文主要讲下在微信回调开发遇到的问题以及解决方案!而在前端开发方面都是由另外一位同事主导,我只负责返回请求参数,因此这里不做讨论。说到这里不得不吐槽下微信文档的说明,大部分都是云里雾里的。
首先从官方开放平台下载服务端c#开发sdk文件。然后找到里头一个 “支付结果通知回调处理类” ResultNotify.cs文件。主要处理流程都在下面这个方法里头。从这个方法里头衍生出其他的方法来。
本来只有一个应用的话,也是不必要在来发文章讨论的,直接跟着sdk写好的方法走就成,但是我们项目最终是要有两个app的,也因此申请了两个微信移动应用,充值分别充入对应的商户里头。官方文档表明了 “通知url必须为直接可访问的url,不能携带参数。示例:notify_url:“https://pay.weixin.qq.com/wxpay/pay.action”” 因此这导致回调都是分开的。一开始我想要直接使用一个配置文件【config.cs】,但是验证一直通不过。最后没办法,只得分开了两份代码,都具备各自的配置文件和其他方法。
后来实在想不通,既然他的config.cs可以通过WxPayData进行重新赋值,没道理需要分开写。因此为了测试,我把以前充值的单号找出来,重新写了两个查询订单的方法,反复写测试返回参数比对查看。最终才发现问题的关键。
1、首先不得不说下官方封装的HttpService.Post(xml, url, false, timeOut)//调用HTTP通信接口提交数据
图片1 config.cs
图片2 HttpService.cs
你说你这里晓得传递一个证书验证启用开关,怎么代理服务器的设置,就不晓得也整个开关,让我们自由选择呢。
结果我每次运行这个方法就提示超时。刚开始也没明白怎么整,直接找到config.cs的PROXY_URL端口配置为自己项目的服务器端口【/*默认IP和端口号分别为0.0.0.0和0,此时不开启代理(如有需要才设置)*/官方文档里头的注释,关键你这里写不开启,可是怎么到了方法里头默认的竟然是开启的,而且一点说明都木有!!】还是不行,都要疯了,大量查阅资料,终于找到有网友提出的解决方案,直接把这几行代码注释掉。
2、是在验证签名过程,这还在自己本身,文档没有阅读仔细,
一开始以为签名都只需要【appid、mch_id、nonce_str、transaction_id+key】的,然后我在MakeSign()方法里头直接封装str参数串,在运行,返回的还是签名错误。
没办法只得在重写下几个用到的方法,然后到处打印出字符串,最后才发现在我们从远程获取到正确的数据后,是整个对象里头的参数都又进行了签名的。【appid、bank_type、cash_fee、fee_type......transaction_id+key】,然后又回头去看官网,文档里头提示 “微信接口可能增加字段,验证签名时必须支持增加的扩展字段”。果然文档提示的都要小心要注意!!
最后,我想到的方案是修改MakeSign(string key)增加个key秘钥的传递。因为远程返回的xml里头都有appid和mch_id,因此只要稍微调整几个方法即可。完整代码如下:
//回调方法 public string Wx_Notify_url() { string logid = Guid.NewGuid().ToString(); const string key="......";//(获取商户api秘钥 )//////重点是这里这里这里,两个回调这里的key对应的是商户里头设置的秘钥 这里是常量常量常量 //对返回的支付结果通知的内容做签名验证 WxPayAPI.WxPayData notifyData = GetNotifyData(key); //检查支付结果中transaction_id是否存在 if (!notifyData.IsSet("transaction_id")) { //若transaction_id不存在,则立即返回结果给微信支付后台 WxPayAPI.WxPayData res = new WxPayAPI.WxPayData(); res.SetValue("return_code", "FAIL"); res.SetValue("return_msg", "支付结果中微信订单号不存在"); //Log.Error(this.GetType().ToString(), "The Pay result is error : " + res.ToXml()); return res.ToXml(); } string transaction_id = notifyData.GetValue("transaction_id").ToString(); //先写入记事本对账 if (transaction_id != "") { //写入记事本 DbHelperSQL.ExecuteSql("insert into [log] values('" + logid + "', 'transaction_id: " + transaction_id + ", 订单号:" + notifyData.GetValue("out_trade_no").ToString() + "', getdate(), 'wx')"); } //支付结果中查询订单,判断订单真实性 判断条件!QueryOrder(transaction_id) 不判断transaction_id == "" if (!QueryOrder(transaction_id, notifyData, key)) { //若订单查询失败,则立即返回结果给微信支付后台 WxPayAPI.WxPayData res = new WxPayAPI.WxPayData(); res.SetValue("return_code", "FAIL"); res.SetValue("return_msg", "订单查询失败"); return res.ToXml(); } //查询订单成功 else{ //业务逻辑处理start... ////////////////////////////////////////////////////////////// //更改订单状态 if(.....){ //插入币值记录 } ///////////////////////////////////////////////////////////// //业务逻辑处理end... WxPayAPI.WxPayData res = new WxPayAPI.WxPayData(); res.SetValue("return_code", "SUCCESS"); res.SetValue("return_msg", "OK"); return res.ToXml(); } } /// <summary> /// 接收从微信支付后台发送过来的数据并验证签名 /// </summary> /// <returns>微信支付后台返回的数据</returns> private WxPayAPI.WxPayData GetNotifyData(string KEY) { //接收从微信后台POST过来的数据 System.IO.Stream s = 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(); //Log.Info(this.GetType().ToString(), "Receive data from WeChat : " + builder.ToString()); //转换数据格式并验证签名 WxPayAPI.WxPayData data = new WxPayAPI.WxPayData(); try { data.FromXml(builder.ToString(), KEY); } catch (WxPayAPI.WxPayException ex) { //若签名错误,则立即返回结果给微信支付后台 WxPayAPI.WxPayData res = new WxPayAPI.WxPayData(); res.SetValue("return_code", "FAIL"); res.SetValue("return_msg", ex.Message); //Log.Error(this.GetType().ToString(), "Sign check error : " + res.ToXml()); Response.Write(res.ToXml()); } //Log.Info(this.GetType().ToString(), "Check sign success"); return data; } //查询订单 这里我重写了下,把远程返回的data和加密key直接传入 public bool QueryOrder(string transaction_id, WxPayData inputObj, string key) { WxPayAPI.WxPayData req = new WxPayAPI.WxPayData(); //增加对req进行赋值////////////////////////////////////// req.SetValue("appid", inputObj.GetValue("appid")); req.SetValue("mch_id", inputObj.GetValue("mch_id")); req.SetValue("key", key); //////////////////////////////////////////////////////// req.SetValue("transaction_id", transaction_id); WxPayAPI.WxPayData res = WxPayAPI.WxPayApi.OrderQuery(req); if (res.GetValue("return_code").ToString() == "FAIL") return false; if (res.GetValue("return_code").ToString() == "SUCCESS" && res.GetValue("result_code").ToString() == "SUCCESS") { return true; } else { return false; } }
然后,在QueryOrder()方法里头进行一些列sign验证,只要把这几个方法在重新调整下,即可!修改WxPayApi文件:
using System; using System.Collections.Generic; using System.Web; using System.Net; using System.IO; using System.Text; namespace WxPayAPI { public class WxPayApi { /** * * 查询订单 * @param WxPayData inputObj 提交给查询订单API的参数 * @param int timeOut 超时时间 * @throws WxPayException * @return 成功时返回订单查询结果,其他抛异常 */ public static WxPayData OrderQuery(WxPayData inputObj, int timeOut = 6) { string url = "https://api.mch.weixin.qq.com/pay/orderquery"; //检测必填参数 if (!inputObj.IsSet("out_trade_no") && !inputObj.IsSet("transaction_id")) { throw new WxPayException("订单查询接口中,out_trade_no、transaction_id至少填一个!"); } //1、我这里定义了临时WxPayData对象,因为inputObj把key也装进去了,而这里签名并不需要key,不然会出错,而且这个key是要到处使用的, //2、另外的解决方法就是string key = inputObj.GetValue("KEY").ToString(); 然后移除key值,不过微信 WxPayData对象并没有封装这个方法,Directory字典有内部方法可以使用【m_values.Remove("key");//这里就可以直接根据他们文档来写个方法了】,偷懒了下,我选择方案1 WxPayData tmp = new WxPayData(); tmp.SetValue("transaction_id", inputObj.GetValue("transaction_id")); tmp.SetValue("appid", inputObj.GetValue("appid"));//公众账号ID tmp.SetValue("mch_id", inputObj.GetValue("mch_id"));//商户号 tmp.SetValue("nonce_str", WxPayApi.GenerateNonceStr());//随机字符串 tmp.SetValue("sign", tmp.MakeSign(inputObj.GetValue("key").ToString()));//签名 string xml = tmp.ToXml(); var start = DateTime.Now; Log.Debug("WxPayApi", "OrderQuery request : " + xml); string response = HttpService.Post(xml, url, false, timeOut);//调用HTTP通信接口提交数据 Log.Debug("WxPayApi", "OrderQuery response : " + response); var end = DateTime.Now; int timeCost = (int)((end - start).TotalMilliseconds);//获得接口耗时 //将xml格式的数据转化为对象以返回 WxPayData result = new WxPayData(); result.FromXml(response, inputObj.GetValue("key").ToString());//这里增加传递秘钥key ReportCostTime(url, timeCost, result, inputObj.GetValue("key").ToString());//测速上报 //不知道这个注销有没有影响,这个倒是没测 return result; } /** * * 测速上报 * @param string interface_url 接口URL * @param int timeCost 接口耗时 * @param WxPayData inputObj参数数组 */ private static void ReportCostTime(string interface_url, int timeCost, WxPayData inputObj, string key) { //如果不需要进行上报 if(WxPayConfig3.REPORT_LEVENL == 0) { return; } //如果仅失败上报 if(WxPayConfig3.REPORT_LEVENL == 1 && inputObj.IsSet("return_code") && inputObj.GetValue("return_code").ToString() == "SUCCESS" && inputObj.IsSet("result_code") && inputObj.GetValue("result_code").ToString() == "SUCCESS") { return; } //上报逻辑 WxPayData3 data = new WxPayData3(); data.SetValue("interface_url",interface_url); data.SetValue("execute_time_",timeCost); //返回状态码 if(inputObj.IsSet("return_code")) { data.SetValue("return_code",inputObj.GetValue("return_code")); } //返回信息 if(inputObj.IsSet("return_msg")) { data.SetValue("return_msg",inputObj.GetValue("return_msg")); } //业务结果 if(inputObj.IsSet("result_code")) { data.SetValue("result_code",inputObj.GetValue("result_code")); } //错误代码 if(inputObj.IsSet("err_code")) { data.SetValue("err_code",inputObj.GetValue("err_code")); } //错误代码描述 if(inputObj.IsSet("err_code_des")) { data.SetValue("err_code_des",inputObj.GetValue("err_code_des")); } //商户订单号 if(inputObj.IsSet("out_trade_no")) { data.SetValue("out_trade_no",inputObj.GetValue("out_trade_no")); } //设备号 if(inputObj.IsSet("device_info")) { data.SetValue("device_info",inputObj.GetValue("device_info")); } try { Report(data, key);///增加key值 } catch (WxPayException ex) { //不做任何处理 } } /** * * 测速上报接口实现 * @param WxPayData inputObj 提交给测速上报接口的参数 * @param int timeOut 测速上报接口超时时间 * @throws WxPayException * @return 成功时返回测速上报接口返回的结果,其他抛异常 */ public static WxPayData Report(WxPayData inputObj, string key, int timeOut = 1) { string url = "https://api.mch.weixin.qq.com/payitil/report"; //检测必填参数 if(!inputObj.IsSet("interface_url")) { throw new WxPayException("接口URL,缺少必填参数interface_url!"); } if(!inputObj.IsSet("return_code")) { throw new WxPayException("返回状态码,缺少必填参数return_code!"); } if(!inputObj.IsSet("result_code")) { throw new WxPayException("业务结果,缺少必填参数result_code!"); } if(!inputObj.IsSet("user_ip")) { throw new WxPayException("访问接口IP,缺少必填参数user_ip!"); } if(!inputObj.IsSet("execute_time_")) { throw new WxPayException("接口耗时,缺少必填参数execute_time_!"); } inputObj.SetValue("appid",WxPayConfig3.APPID);//公众账号ID inputObj.SetValue("mch_id",WxPayConfig3.MCHID);//商户号 inputObj.SetValue("user_ip",WxPayConfig3.IP);//终端ip inputObj.SetValue("time",DateTime.Now.ToString("yyyyMMddHHmmss"));//商户上报时间 inputObj.SetValue("nonce_str",GenerateNonceStr());//随机字符串 inputObj.SetValue("sign", inputObj.MakeSign(key));//签名 string xml = inputObj.ToXml(); Log.Info("WxPayApi", "Report request : " + xml); string response = HttpService.Post(xml, url, false, timeOut); Log.Info("WxPayApi", "Report response : " + response); WxPayData result = new WxPayData(); result.FromXml(response, key);///增加key值 return result; } } }
然后,修改Data.cs文件:
/** * @将xml转为WxPayData对象并返回对象内部的数据 * @param string 待转换的xml串 * @return 经转换得到的Dictionary * @throws WxPayException */ public SortedDictionary<string, object> FromXml(string xml, string key/*传递秘钥*/) { if (string.IsNullOrEmpty(xml)) { Log.Error(this.GetType().ToString(), "将空的xml串转换为WxPayData不合法!"); throw new WxPayException("将空的xml串转换为WxPayData不合法!"); } XmlDocument xmlDoc = new XmlDocument(); xmlDoc.LoadXml(xml); XmlNode xmlNode = xmlDoc.FirstChild;//获取到根节点<xml> XmlNodeList nodes = xmlNode.ChildNodes; foreach (XmlNode xn in nodes) { XmlElement xe = (XmlElement)xn; m_values[xe.Name] = xe.InnerText;//获取xml的键值对到WxPayData内部的数据中 } try { //2015-06-29 错误是没有签名 if (m_values["return_code"].ToString() != "SUCCESS") { return m_values; } CheckSign(key);//验证签名,不通过会抛异常 //在把秘钥传入验证签名的方法里头 } catch (WxPayException ex) { throw new WxPayException(ex.Message); } return m_values; } /** * * 检测签名是否正确 * 正确返回true,错误抛异常 */ public bool CheckSign(string key) { //如果没有设置签名,则跳过检测 if (!IsSet("sign")) { Log.Error(this.GetType().ToString(), "WxPayData签名存在但不合法!"); throw new WxPayException("WxPayData签名存在但不合法!"); } //如果设置了签名但是签名为空,则抛异常 else if(GetValue("sign") == null || GetValue("sign").ToString() == "") { Log.Error(this.GetType().ToString(), "WxPayData签名存在但不合法!"); throw new WxPayException("WxPayData签名存在但不合法!"); } //获取接收到的签名 string return_sign = GetValue("sign").ToString(); //在本地计算新的签名 string cal_sign = MakeSign(key);//生成sign直接使用传入的key值,而不是config.cs配置好的默认的key值 if (cal_sign == return_sign) { return true; } Log.Error(this.GetType().ToString(), "WxPayData签名验证错误!"); throw new WxPayException("WxPayData签名验证错误!"); } /** * @生成签名,详见签名生成算法 * @return 签名, sign字段不参加签名 */ public string MakeSign(string key) { //转url格式 string str = ToUrl(); //在string后加入API KEY //str += "&key=" + WxPayConfig.KEY;//不适用写好的方法 str += "&key=" + key; //MD5加密 var md5 = MD5.Create(); var bs = md5.ComputeHash(Encoding.UTF8.GetBytes(str)); var sb = new StringBuilder(); foreach (byte b in bs) { sb.Append(b.ToString("x2")); } //所有字符转为大写 return sb.ToString().ToUpper(); }
最后,补充下上面OrderQuery()说的方案二删除key的方法,已经测试过,有效!!
//这个是WxPayApi.cs的调整 string key = inputObj.GetValue("KEY").ToString(); inputObj.RemoveTkey("KEY"); //Data.cs文件增加方法RemoveTkey /** * 根据字段名删除某个字段的值 * @param key 字段名 * @return key对应的字段值 */ public bool RemoveTkey(string Tkey) { bool e = m_values.Remove(Tkey); return e; }