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;
(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