隐藏

.NET ASP.NET 微信Native支付(扫码支付)模式二,及回调,微信退款

发布:2022/4/19 17:13:37作者:管理员 来源:本站 浏览次数:1694

1. 前言


   经过一个多周的焦傲、摧残,终于完成了微信支付及退款,做一下总结,主要是参数、签名、数据接收问题有几个小点要注意,本文基于c#进行开发。


2. 项目背景


   项目为商城,这就需要支付功能,主要做的就是支付模块,所以就要实现现在主流的支付方式,如微信支付、支付宝支付…

   项目业务:系统暂时要求点击支付时网站弹出生成的微信支付二维码,由用户进行扫描购买,数据回调记录数据库。

   微信支付平台:转到

   给出的支付方式如下图,本文主要讲Native支付模式二

   在这里插入图片描述


3.Native支付


   Native支付场景介绍→转到

   扫码支付模式二文档→转到

   准备工作

   (1)申请 微信商户号 微信公众号 (这里具体步骤不描述,因为我做的商城,这里有商家去微信平台申请);

   (2)开通 Native支付(点击开通,很简单,但是是商家的事,不解释);

   (3)微信认证证书(商家提供,仅退款时跟撤销订单时需要);

   ①证书路径,注意应该填写绝对路径;

   ②证书文件不能放在web服务器虚拟目录,应放在有访问权限控制的目录中,防止被他人下载;

   ③建议将证书文件名改为复杂且不容易猜测的文件;

   ④商户服务器要做好病毒和木马防护工作,不被非法侵入者窃取证书文件。

   下载微信官方提供的工具类Demo,地址:转到

   在这里插入图片描述

   解压并进入WxPayAPI文件夹,将 business example lib 三个文件夹复制到咱们项目的工程中(本项目使用VS[Visual Studio 2015] ,如果不会添加 ,VS将复制过来的文件或文件夹显示到解决方案管理)

   在这里插入图片描述在这里插入图片描述

   下面来说一下这三个文件夹

   (1)business文件夹中主要是 NativePay.cs 类,推荐其他文件也不要删除;

   (2)example文件夹中ResultNotifyPage.aspx页面,将此文件放在工程根目录下,MakeQRCode.aspx页面保留,其他文件建议删除;

   (3)lib文件夹全部保留,因为这里面是所依赖的工具类。

   在这里插入图片描述在这里插入图片描述

   后端进行页面配置

   (1)对于lib文件夹,仅需修改DemoConfig.cs[作用:配置微信商户号和公众号、回调地址等相关信息];(我是商城网站,商家总后台设置参数,这里从数据库调出,根据个人需求)

   在这里插入图片描述Appsert公众帐号secert(仅JSAPI支付的时候需要配置)给出了设置了也没关系,


在这里插入图片描述支付只需要这私四个参数:

在这里插入图片描述在这里插入图片描述

(2)对于business文件夹,仅需修改NativePay.cs文件[作用:用来调用微信接口根据我们提供的数据生成二维码链接]

点击支付ajax提交到后台的一个静态方法:(仅供借鉴payment为支付类型(微信、支付宝),indentid订单ID)


$.ajax({

   contentType: "application/json",

   url: "pay.aspx/Pay_btn",

   type: "post",

   data: JSON.stringify({"payment":payment,"indentid":indentid}),

   dataType: "json",

   success: function (date) {

       if (payment == "1") {

           alert(date.d);

       } else {

           window.location.href="Wx_Payment-" + indentid + "-" + date.d + ".html";

       }

   }

});


后台静态方法里,Encrypt()加密方法


NativePay nativePay = new NativePay();//定义这个类

string url2 = nativePay.GetPayUrl(DESEncrypt.Decrypt(indentid));//indentid订单ID(返回二维码的url)

string imgurl = "WX/example/MakeQRCode.aspx?data=" + HttpUtility.UrlEncode(url2);//(将url加入二维码生成页面)

return DESEncrypt.Encrypt(imgurl);//(加密返回给前台)


nativepay类(直接看模式二,根据个人逻辑写入参数):

例:


public string GetPayUrl(string indentid)

       {

           // Log.Info(this.GetType().ToString(), "Native pay mode 2 url is producing...");

           IndentBLL indentbll = new IndentBLL();//订单类

           IndenterBLL indenterbll = new IndenterBLL();//订单明细类

           Indent showindent = indentbll.GetModel(int.Parse(indentid));//通过订单ID获取当前订单

           BasicBLL basicbll = new BasicBLL();//网站信息设置类

           DataTable showbasic = new DataTable();

           showbasic = basicbll.GetAllList().Tables[0];//获取网站信息设置


           WxPayData data = new WxPayData();

           data.SetValue("body", showbasic.Rows[0]["Webname"].ToString());//商品描述(我这里给的是网站标题中文,默认是只能英文字符串,如何改下边说到)

           data.SetValue("attach", showindent.OrderNo);//附加数据,如商家数据包,自身系统生成的订单号

           data.SetValue("out_trade_no", showindent.OrderNo);//随机字符串(系统生成的订单号)

           data.SetValue("total_fee",Convert.ToInt32((showindent.Total*100)).ToString());//总金额

           data.SetValue("time_start", DateTime.Now.ToString("yyyyMMddHHmmss"));//交易起始时间

           data.SetValue("time_expire", DateTime.Now.AddMinutes(10).ToString("yyyyMMddHHmmss"));//交易结束时间

           data.SetValue("goods_tag", showindent.UserID);//商品标记 会员用户名

           data.SetValue("trade_type", "NATIVE");//交易类型

           data.SetValue("product_id", indentid);//商品ID(订单ID)


           WxPayData result = WxPayApi.UnifiedOrder(data);//调用统一下单接口

           string url = result.GetValue("code_url").ToString();//获得统一下单接口返回的二维码链接


           //Log.Info(this.GetType().ToString(), "Get native pay mode 2 url : " + url);

           return url;

       }


注:1. body,传入值必须为英文,负责签名错误;2.total_fee商品金额 单位是分!!!单位是分!!!单位是分!!!3.trade_type交易类型 一定为NATIVE。

(3)返回的url做了一个页面Wx_Payment.aspx接收传入加密的订单ID(indnetid)和加密的imgurl;

局部代码获取两个值,Decrypt()为解密方法:


string indentid = Request.QueryString["key1"];

imgurl =DESEncrypt.Decrypt(Request.QueryString["key2"]);

showindent = indentbll.GetModel(int.Parse(DESEncrypt.Decrypt(indentid)));//查询订单

showindenter = indenterbll.GetList("IndentID="+ int.Parse(DESEncrypt.Decrypt(indentid))).Tables[0];//查询订单明细


在这里插入图片描述效果:

在这里插入图片描述(4)回调,用户扫码支付成功后微信平台异步回调ResultNotifyPage.aspx


后台调用了ResultNotify 类:


protected void Page_Load(object sender, EventArgs e)

{

    //File.WriteAllText(Server.MapPath("/log.txt"), Request.Url.ToString() + "==" + DateTime.Now.ToString());

    ResultNotify resultNotify = new ResultNotify(this);

    resultNotify.ProcessNotify();

}


ResultNotify 类里:


public override void ProcessNotify()

{

   WxPayData notifyData = GetNotifyData();

   HelpClass helpclass = new HelpClass();//自己写的帮助类


   //File.WriteAllText(HttpContext.Current.Server.MapPath("/log2.txt"), notifyData.ToXml() + "==" + DateTime.Now.ToString());

   //File.WriteAllText(HttpContext.Current.Server.MapPath("/log3.txt"), notifyData.IsSet("transaction_id").ToString() + "==" + DateTime.Now.ToString());

   //检查支付结果中transaction_id是否存在

   if (!notifyData.IsSet("transaction_id"))

   {

       //若transaction_id不存在,则立即返回结果给微信支付后台

       WxPayData res = new WxPayData();

       res.SetValue("return_code", "FAIL");

       res.SetValue("return_msg", "支付结果中微信订单号不存在");

       //File.WriteAllText(HttpContext.Current.Server.MapPath("/log4.txt"), res.ToXml() + "==" + DateTime.Now.ToString());

       page.Response.Write(res.ToXml());

       page.Response.End();

   }


   string transaction_id = notifyData.GetValue("transaction_id").ToString();//获取微信平台返回的交易号

   string orderno= notifyData.GetValue("out_trade_no").ToString();//返回的有传入的系统自动生成的订单号


   //File.WriteAllText(HttpContext.Current.Server.MapPath("/log5.txt"), transaction_id + "="+ QueryOrder(transaction_id).ToString() + "=" + DateTime.Now.ToString());



   //查询订单,判断订单真实性

   if (!QueryOrder(transaction_id))

   {

       //若订单查询失败,则立即返回结果给微信支付后台

       WxPayData res = new WxPayData();

       res.SetValue("return_code", "FAIL");

       res.SetValue("return_msg", "订单查询失败");

       page.Response.Write(res.ToXml());

       page.Response.End();

   }

   //查询订单成功

   else

   {

       //File.WriteAllText(HttpContext.Current.Server.MapPath("/log6.txt"), "=sql=" + DateTime.Now.ToString());


       WxPayData res = new WxPayData();

       res.SetValue("return_code", "SUCCESS");

       res.SetValue("return_msg", "OK");

      helpclass.Update_table("Indent", "PaymentType=2,PaymentNumber='" + transaction_id + "',PaymentTime='" + DateTime.Now.ToString() + "',PaymentStatus=1,OrdStatus=2", "OrderNo='" + orderno + "'");//通过订单编号修改支付状态,支付类型,处理结果,微信交易号,订单状态,支付时间等(修改数据库信息)

       page.Response.Write(res.ToXml());

       page.Response.End();

   }

}


订单表一个支付状态字段,在用户扫码页面隔段时间判断一下是否支付,付过了就给支付页面关了,不能用户付完之后一直还停留在扫码二维码页面,然后二维码扫码页面(Wx_Payment.aspx)ajax一秒一提交订单ID(indentid)到后台判断订单支付状态,付过了跳转到订单详情页;


<script>

  window.setInterval(function () {

       var entid = $("#entid").val();

       $.ajax({

           contentType: "application/json",

           url: "Wx_Payment.aspx/Status",

           type: "post",

           data: JSON.stringify({ "indentid": entid }),

           dataType: "json",

           success: function (date) {

               if (date.d == true) {

                   window.location.href = "order_info-" + entid + ".html";

               }

           }

       });

   }, 1000);

</script>


后台:


[WebMethod]

public static bool Status(string indentid)

{

   IndentBLL indentbll = new IndentBLL();

   Indent showindent = indentbll.GetModel(int.Parse(DESEncrypt.Decrypt(indentid)));

   if (showindent.PaymentStatus == 1)//PaymentStatus支付状态

   {

       return true;

   }

   else

   {

       return false;

   }

}


(5)传参时body设置中文签名错误问题,参考:https://blog.csdn.net/YuanMxy/article/details/89331113

找到Data类的CalcHMACSHA256Hash方法将var enc = Encoding.Default;改为var enc = Encoding.UTF8;


微信支付、支付参数还可以参考:https://blog.csdn.net/YuanMxy/article/details/89359573?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522158632862519724845049815%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=158632862519724845049815&biz_id=14&utm_source=distribute.pc_search_result.none-task-blog-soetl_SOETL-18


(7)退款,此时需要证书和证书密码,在DemoConfig.cs加入

用户点击退款


$('.tuibtn').click(function () {

           var id = $(this).attr("tid");//获取要退款订单的ID

           $.ajax({

               contentType: "application/json",

               url: "SelectIndent.aspx/TuiKuan",

               type: "post",

               data: JSON.stringify({ "indentid": id }),//订单ID传入后台的静态方法

               dataType: "json",

               success: function (date) {

                   if (date.d == "ok") {

                       alert("退款成功!");

                       window.location.reload();

                   } else if (date.d == "no") {

                       alert("退款失败!");

                   }

                   else {

                       alert("退款失败!" + date.d);

                   }

               }

           });

       });


后台:


[WebMethod]

public static string TuiKuan(string indentid)

{

   HelpClass helpclass = new HelpClass();

   IndentBLL indentbll = new IndentBLL();

   Indent showindent = indentbll.GetModel(int.Parse(indentid));//获取当前订单

   try

   {

       WxPayData result = Refund.Run(showindent.PaymentNumber,showindent.OrderNo,Convert.ToInt32(showindent.Total*100).ToString(),Convert.ToInt32((Convert.ToDecimal(showindent.Refund_fee)*100)).ToString());//调起退款

       string result_code = result.GetValue("result_code").ToString();//业务结果(SUCCESS)

       string out_trade_no= result.GetValue("out_trade_no").ToString();//商户订单号(系统自己生产的订单号)

       string out_refund_no= result.GetValue("out_refund_no").ToString();//商户退款订单号(自己生成的返回回来了)

       string refund_id= result.GetValue("refund_id").ToString();//微信退款单号

       string refund_fee= result.GetValue("refund_fee").ToString();//实际退款金额

       if(result_code== "SUCCESS")//证明退款成功了

       {

           helpclass.Update_table("Indent", "Payrefund_bool='1',Refund_id='" + refund_id + "',Refund_no='" + out_refund_no + "',Yesrun_fee='" + (Convert.ToDecimal(refund_fee)/100).ToString() + "'", "OrderNo='" + out_trade_no + "'");//保存处理结果(记录到数据库,修改当前订单,自己的逻辑代码)

           IndenterBLL indenterbll = new IndenterBLL();

           DataTable showindenter = new DataTable();

           GoodsBLL goodsbll = new GoodsBLL();

           showindenter = indenterbll.GetList("IndentID=" + indentid).Tables[0];//获取当前订单的订单详情

           foreach (DataRow item in showindenter.Rows)

           {

               if (goodsbll.GetList("ID=" + int.Parse(item["GoodID"].ToString())).Tables[0].Rows.Count > 0)

               {

                   Goods goods = goodsbll.GetModel(int.Parse(item["GoodID"].ToString()));

                   helpclass.Update_table("Goods", "Sales=" + (goods.Sales - (int.Parse(item["GoodCount"].ToString()))) + ",Inventory=" + (goods.Inventory + (int.Parse(item["GoodCount"].ToString()))), "ID=" + (int.Parse(item["GoodID"].ToString())));//修改商品的销售量和库存量

               }

           }

           return "ok";

           

       }

       else

       {

           return "no";

       }


   }

   catch (WxPayException ex)

   {

       return ex.ToString();

   }

   catch (Exception ex)

   {

       return ex.ToString();

   }

}


Refund类的 Run方法:


/***

       * 申请退款完整业务流程逻辑

       * @param transaction_id 微信订单号(优先使用)

       * @param out_trade_no 商户订单号

       * @param total_fee 订单总金额

       * @param refund_fee 退款金额

       * @return 退款结果(xml格式)

       */

       public static WxPayData Run(string transaction_id, string out_trade_no, string total_fee, string refund_fee)

       {

           //File.WriteAllText(HttpContext.Current.Server.MapPath("/log.txt"), transaction_id + "==" + out_trade_no+"=="+ total_fee+"=="+ refund_fee+"=="+DateTime.Now.ToString());

           WxPayData data = new WxPayData();

           if (!string.IsNullOrEmpty(transaction_id))//微信订单号存在的条件下,则已微信订单号为准

           {

               data.SetValue("transaction_id", transaction_id);

           }

           else//微信订单号不存在,才根据商户订单号去退款

           {

               data.SetValue("out_trade_no", out_trade_no);

           }

           data.SetValue("total_fee", int.Parse(total_fee));//订单总金额

           data.SetValue("refund_fee", int.Parse(refund_fee));//退款金额

           data.SetValue("out_refund_no", WxPayApi.GenerateOutTradeNo());//随机生成商户退款单号

           data.SetValue("op_user_id", WxPayConfig.GetConfig().GetMchID());//操作员,默认为商户号

           WxPayData result = WxPayApi.Refund(data);//提交退款申请给API,接收返回数据

           //File.WriteAllText(HttpContext.Current.Server.MapPath("/log1.txt"), result.ToXml()+"==" + DateTime.Now.ToString());

           return result;

       }


WxPayApi的Refund():


/**

       *

       * 申请退款

       * @param WxPayData inputObj 提交给申请退款API的参数

       * @param int timeOut 超时时间

       * @throws WxPayException

       * @return 成功时返回接口调用结果,其他抛异常

       */

       public static WxPayData Refund(WxPayData inputObj, int timeOut = 6)

       {

           string url = "https://api.mch.weixin.qq.com/secapi/pay/refund";

           //检测必填参数

           if (!inputObj.IsSet("out_trade_no") && !inputObj.IsSet("transaction_id"))

           {

               throw new WxPayException("退款申请接口中,out_trade_no、transaction_id至少填一个!");

           }

           else if (!inputObj.IsSet("out_refund_no"))

           {

               throw new WxPayException("退款申请接口中,缺少必填参数out_refund_no!");

           }

           else if (!inputObj.IsSet("total_fee"))

           {

               throw new WxPayException("退款申请接口中,缺少必填参数total_fee!");

           }

           else if (!inputObj.IsSet("refund_fee"))

           {

               throw new WxPayException("退款申请接口中,缺少必填参数refund_fee!");

           }

           else if (!inputObj.IsSet("op_user_id"))

           {

               throw new WxPayException("退款申请接口中,缺少必填参数op_user_id!");

           }

           inputObj.SetValue("appid", WxPayConfig.GetConfig().GetAppID());//公众账号ID

           inputObj.SetValue("mch_id", WxPayConfig.GetConfig().GetMchID());//商户号

           inputObj.SetValue("nonce_str", Guid.NewGuid().ToString().Replace("-", ""));//随机字符串

           inputObj.SetValue("sign_type", WxPayData.SIGN_TYPE_HMAC_SHA256);//签名类型

           inputObj.SetValue("sign", inputObj.MakeSign());//签名

           string xml = inputObj.ToXml();

           var start = DateTime.Now;

           string response = HttpService.Post(xml, url, true, timeOut);//调用HTTP通信接口提交数据到API

           var end = DateTime.Now;

           int timeCost = (int)((end - start).TotalMilliseconds);//获得接口耗时

           //将xml格式的结果转换为对象以返回

           WxPayData result = new WxPayData();

           result.FromXml(response);

           ReportCostTime(url, timeCost, result);//测速上报

           return result;

       }


Post():


public static string Post(string xml, string url, bool isUseCert, int timeout)

       {

           System.GC.Collect();//垃圾回收,回收没有正常关闭的http连接


           string result = "";//返回结果


           HttpWebRequest request = null;

           HttpWebResponse response = null;

           Stream reqStream = null;


           try

           {

               //设置最大连接数

               ServicePointManager.DefaultConnectionLimit = 200;

               //设置https验证方式

               if (url.StartsWith("https", StringComparison.OrdinalIgnoreCase))

               {

                   ServicePointManager.ServerCertificateValidationCallback =

                           new RemoteCertificateValidationCallback(CheckValidationResult);

               }


               /***************************************************************

               * 下面设置HttpWebRequest的相关属性

               * ************************************************************/

               request = (HttpWebRequest)WebRequest.Create(url);

               request.UserAgent = USER_AGENT;

               request.Method = "POST";

               request.Timeout = timeout * 1000;


               //设置代理服务器

               //WebProxy proxy = new WebProxy();                          //定义一个网关对象

               //proxy.Address = new Uri(WxPayConfig.PROXY_URL);              //网关服务器端口:端口

               //request.Proxy = proxy;


               //设置POST的数据类型和长度

               request.ContentType = "text/xml";

               byte[] data = System.Text.Encoding.UTF8.GetBytes(xml);

               request.ContentLength = data.Length;


               //是否使用证书

               if (isUseCert)

               {

                   string path = HttpContext.Current.Request.PhysicalApplicationPath;

                   X509Certificate2 cert = new X509Certificate2(path + WxPayConfig.GetConfig().GetSSlCertPath(), WxPayConfig.GetConfig().GetSSlCertPassword());//将证书相对地址拼成了绝对地址

                   request.ClientCertificates.Add(cert);

               }


               //往服务器写入数据

               reqStream = request.GetRequestStream();

               reqStream.Write(data, 0, data.Length);

               reqStream.Close();


               //获取服务端返回

               response = (HttpWebResponse)request.GetResponse();

             

               //获取服务端返回数据

               StreamReader sr = new StreamReader(response.GetResponseStream(), Encoding.UTF8);

               

               result = sr.ReadToEnd().Trim();

               sr.Close();

           }

           catch (System.Threading.ThreadAbortException e)

           {

               Log.Error("HttpService", "Thread - caught ThreadAbortException - resetting.");

               Log.Error("Exception message: {0}", e.Message);

               System.Threading.Thread.ResetAbort();

           }

           catch (WebException e)

           {

               Log.Error("HttpService", e.ToString());

               if (e.Status == WebExceptionStatus.ProtocolError)

               {

                   Log.Error("HttpService", "StatusCode : " + ((HttpWebResponse)e.Response).StatusCode);

                   Log.Error("HttpService", "StatusDescription : " + ((HttpWebResponse)e.Response).StatusDescription);

               }

               throw new WxPayException(e.ToString());

           }

           catch (Exception e)

           {

               Log.Error("HttpService", e.ToString());

               throw new WxPayException(e.ToString());

           }

           finally

           {

               //关闭连接和流

               if (response != null)

               {

                   response.Close();

               }

               if(request != null)

               {

                   request.Abort();

               }

           }

           return result;

       }


注:WxPayData 支付或退款时传进去的参数,成功后微信平台有的参数不一定传回,付款时统一下单传进和返回的参数见:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_1,退款时见:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_4