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

1. 前言

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

2. 项目背景

  1. 项目为商城,这就需要支付功能,主要做的就是支付模块,所以就要实现现在主流的支付方式,如微信支付、支付宝支付…
  2. 项目业务:系统暂时要求点击支付时网站弹出生成的微信支付二维码,由用户进行扫描购买,数据回调记录数据库。
  3. 微信支付平台:转到
  4. 给出的支付方式如下图,本文主要讲Native支付模式二
    在这里插入图片描述

3.Native支付

  1. Native支付场景介绍→转到
  2. 扫码支付模式二文档→转到
  3. 准备工作
    (1)申请 微信商户号 微信公众号 (这里具体步骤不描述,因为我做的商城,这里有商家去微信平台申请);
    (2)开通 Native支付(点击开通,很简单,但是是商家的事,不解释);
    (3)微信认证证书(商家提供,仅退款时跟撤销订单时需要);
    ①证书路径,注意应该填写绝对路径;
    ②证书文件不能放在web服务器虚拟目录,应放在有访问权限控制的目录中,防止被他人下载;
    ③建议将证书文件名改为复杂且不容易猜测的文件;
    ④商户服务器要做好病毒和木马防护工作,不被非法侵入者窃取证书文件。
  4. 下载微信官方提供的工具类Demo,地址:转到
    在这里插入图片描述
  5. 解压并进入WxPayAPI文件夹,将 business example lib 三个文件夹复制到咱们项目的工程中(本项目使用VS[Visual Studio 2015] ,如果不会添加 ,VS将复制过来的文件或文件夹显示到解决方案管理)
    在这里插入图片描述在这里插入图片描述
  6. 下面来说一下这三个文件夹
    (1)business文件夹中主要是 NativePay.cs 类,推荐其他文件也不要删除
    (2)example文件夹中ResultNotifyPage.aspx页面,将此文件放在工程根目录下,MakeQRCode.aspx页面保留,其他文件建议删除
    (3)lib文件夹全部保留,因为这里面是所依赖的工具类
    在这里插入图片描述在这里插入图片描述
  7. 后端进行页面配置
    (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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章