.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

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