內容摘要:用戶支付完成后,憑商戶訂單號發起退款申請,微信處理后,將支付金額原路退回至用戶支付賬戶。
服務端開發環境:.NET MVC 開發語言C#;
一、開發前准備(此處,前提默認條件為您已開通微信商戶平台,且成功支付一筆訂單)
1、微信支付接口中,涉及資金回滾的接口會使用到API證書,包括退款、撤銷接口等。證書下載及說明,您可參閱https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=4_3,下載並將證書上傳至目標服務器(本人為阿里雲ECS服務器,IIS 10.0);
二、官方接口地址:https://api.mch.weixin.qq.com/secapi/pay/refund;
三、代碼實現
1、主體代碼(調用日志請視業務情況自行添加,本文不再添加日志)
public class Refund { /*** * 申請退款完整業務流程邏輯 * @param transaction_id 微信訂單號(優先使用) * @param out_trade_no 商戶訂單號 * @param refund_orderNo 訂單退單號 * @param total_fee 訂單總金額 * @param refund_fee 退款金額 * @return 退款結果(xml格式) * @payType 2微信支付 3微信小程序支付 */ public void Run(string orderNo, string refund_orderNo, float total_fee, float refund_fee, int payType) { try { WxPayData data = new WxPayData();// 微信服務基礎數據幫助類WxPayData請參閱代碼塊3 data.SetValue("out_trade_no", orderNo); // 支付訂單單號 data.SetValue("total_fee", total_fee * 100); // 訂單總金額,微信金額單位為分 data.SetValue("refund_fee", refund_fee * 100); // 退款金額,微信金額單位為分 data.SetValue("out_refund_no", refund_orderNo); // 隨機生成商戶退款單號 可參閱refund_orderNo = WxPayApi.GenerateOutTradeNo()生成唯一退款碼 data.SetValue("op_user_id", WxPayConfig.GetConfig().GetMchID()); // 資金流向的微信商戶號 WxPayData wxResult = WxPayApi.Refund(data, payType, 6);// 提交退款申請給API,接收返回數據 WxPayApi.Refund()方法請參閱代碼塊2 if (wxResult.GetValue("return_code") + "" == "SUCCESS") { if (wxResult.GetValue("result_code") + "" == "SUCCESS") { // ...退款成功的操作 } else { // 退款失敗的操作... // 微信返回的退款失敗碼 string return_ero_des = wxResult.GetValue("err_code_des") + ""; } } else { // 退款失敗的操作... // 退款失敗原因 string return_msg = wxResult.GetValue("return_msg") + ""; } } catch (Exception ex) { throw ex; } } }
2、WxPayApi.Refund退款方法
public class WxPayApi { /** * 根據當前系統時間加隨機序列來生成訂單號 * @return 訂單號 */ public static string GenerateOutTradeNo() { var ran = new Random(); return string.Format("{0}{1}{2}", WxPayConfig.GetConfig().GetMchID(), DateTime.Now.ToString("yyyyMMddHHmmss"), ran.Next(999)); } public static string GenerateNonceStr() { RandomGenerator randomGenerator = new RandomGenerator(); return randomGenerator.GetRandomUInt().ToString(); } /** * * 測速上報 * @param string interface_url 接口URL * @param int timeCost 接口耗時 * @param WxPayData inputObj參數數組 */ private static void ReportCostTime(string interface_url, int timeCost, WxPayData inputObj) { //如果不需要進行上報 if (WxPayConfig.GetConfig().GetReportLevel() == 0) { return; } //如果僅失敗上報 if (WxPayConfig.GetConfig().GetReportLevel() == 1 && inputObj.IsSet("return_code") && inputObj.GetValue("return_code").ToString() == "SUCCESS" && inputObj.IsSet("result_code") && inputObj.GetValue("result_code").ToString() == "SUCCESS") { return; } //上報邏輯 WxPayData data = new WxPayData(); 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); } catch (WxPayException ex) { //不做任何處理 } } /** * * 測速上報接口實現 * @param WxPayData inputObj 提交給測速上報接口的參數 * @param int timeOut 測速上報接口超時時間 * @throws WxPayException * @return 成功時返回測速上報接口返回的結果,其他拋異常 */ public static WxPayData Report(WxPayData inputObj, 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", WxPayConfig.GetConfig().GetAppID());//公眾賬號ID inputObj.SetValue("mch_id", WxPayConfig.GetConfig().GetMchID());//商戶號 inputObj.SetValue("user_ip", WxPayConfig.GetConfig().GetIp());//終端ip inputObj.SetValue("time", DateTime.Now.ToString("yyyyMMddHHmmss"));//商戶上報時間 inputObj.SetValue("nonce_str", GenerateNonceStr());//隨機字符串 inputObj.SetValue("sign_type", WxPayData.SIGN_TYPE_HMAC_SHA256);//簽名類型 inputObj.SetValue("sign", inputObj.MakeSign());//簽名 string xml = inputObj.ToXml(); string response = HttpService.Post(xml, url, false, timeOut); WxPayData result = new WxPayData(); result.FromXml(response); return result; } /** * * 申請退款 * @param WxPayData inputObj 提交給申請退款API的參數 * @param int timeOut 超時時間 * @throws WxPayException * @return 成功時返回接口調用結果,其他拋異常 */ public static WxPayData Refund(WxPayData inputObj, int payType, 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!"); } if (payType == 2)// 微信PC掃碼支付退款 { inputObj.SetValue("appid", WxPayConfig.GetConfig().GetAppID());// 微信公眾平台AppId } else // 微信小程序支付退款 { inputObj.SetValue("appid", WxAppId);// 微信小程序appID } 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; } }
3、退款基礎數據幫助類
/// <summary> /// 微信支付協議接口數據類,所有的API接口通信都依賴這個數據結構, /// 在調用接口之前先填充各個字段的值,然后進行接口通信, /// 這樣設計的好處是可擴展性強,用戶可隨意對協議進行更改而不用重新設計數據結構, /// 還可以隨意組合出不同的協議數據包,不用為每個協議設計一個數據包結構 /// </summary> public class WxPayData { public const string SIGN_TYPE_MD5 = "MD5"; public const string SIGN_TYPE_HMAC_SHA256 = "HMAC-SHA256"; public WxPayData() { } //采用排序的Dictionary的好處是方便對數據包進行簽名,不用再簽名之前再做一次排序 private SortedDictionary<string, object> m_values = new SortedDictionary<string, object>(); /** * 設置某個字段的值 * @param key 字段名 * @param value 字段值 */ public void SetValue(string key, object value) { m_values[key] = value; } /** * 根據字段名獲取某個字段的值 * @param key 字段名 * @return key對應的字段值 */ public object GetValue(string key) { object o = null; m_values.TryGetValue(key, out o); return o; } /** * 判斷某個字段是否已設置 * @param key 字段名 * @return 若字段key已被設置,則返回true,否則返回false */ public bool IsSet(string key) { object o = null; m_values.TryGetValue(key, out o); if (null != o) return true; else return false; } /** * @將Dictionary轉成xml * @return 經轉換得到的xml串 * @throws WxPayException **/ public string ToXml() { //數據為空時不能轉化為xml格式 if (0 == m_values.Count) { throw new WxPayException("WxPayData數據為空!"); } string xml = "<xml>"; foreach (KeyValuePair<string, object> pair in m_values) { //字段值不能為null,會影響后續流程 if (pair.Value == null) { throw new WxPayException("WxPayData內部含有值為null的字段!"); } if (pair.Value.GetType() == typeof(int) || pair.Value.GetType() == typeof(float)) { xml += "<" + pair.Key + ">" + pair.Value + "</" + pair.Key + ">"; } else if (pair.Value.GetType() == typeof(string)) { xml += "<" + pair.Key + ">" + "<![CDATA[" + pair.Value + "]]></" + pair.Key + ">"; } else//除了string和int類型不能含有其他數據類型 { throw new WxPayException("WxPayData字段數據類型錯誤!"); } } xml += "</xml>"; return xml; } /** * @將xml轉為WxPayData對象並返回對象內部的數據 * @param string 待轉換的xml串 * @return 經轉換得到的Dictionary * @throws WxPayException */ public SortedDictionary<string, object> FromXml(string xml) { if (string.IsNullOrEmpty(xml)) { throw new WxPayException("將空的xml串轉換為WxPayData不合法!"); } SafeXmlDocument xmlDoc = new SafeXmlDocument(); 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 { if (m_values["return_code"] + "" != "SUCCESS") { return m_values; } CheckSign();//驗證簽名,不通過會拋異常 } catch (WxPayException ex) { throw new WxPayException(ex.Message); } return m_values; } /** * @Dictionary格式轉化成url參數格式 * @ return url格式串, 該串不包含sign字段值 */ public string ToUrl() { string buff = ""; foreach (KeyValuePair<string, object> pair in m_values) { if (pair.Value == null) { throw new WxPayException("WxPayData內部含有值為null的字段!"); } if (pair.Key != "sign" && pair.Value.ToString() != "") { buff += pair.Key + "=" + pair.Value + "&"; } } buff = buff.Trim('&'); return buff; } /** * @Dictionary格式化成Json * @return json串數據 */ public string ToJson() { string jsonStr = JsonMapper.ToJson(m_values); return jsonStr; } /** * @values格式化成能在Web頁面上顯示的結果(因為web頁面上不能直接輸出xml格式的字符串) */ public string ToPrintStr() { string str = ""; foreach (KeyValuePair<string, object> pair in m_values) { if (pair.Value == null) { throw new WxPayException("WxPayData內部含有值為null的字段!"); } str += string.Format("{0}={1}\n", pair.Key, pair.Value.ToString()); } str = HttpUtility.HtmlEncode(str); return str; } /** * @生成簽名,詳見簽名生成算法 * @return 簽名, sign字段不參加簽名 */ public string MakeSign(string signType) { //轉url格式 string str = ToUrl(); //在string后加入API KEY str += "&key=" + WxPayConfig.GetConfig().GetKey(); if (signType == SIGN_TYPE_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(); } else if (signType == SIGN_TYPE_HMAC_SHA256) { return CalcHMACSHA256Hash(str, WxPayConfig.GetConfig().GetKey()); } else { throw new WxPayException("sign_type 不合法"); } } /** * @生成簽名,詳見簽名生成算法 * @return 簽名, sign字段不參加簽名 SHA256 */ public string MakeSign() { return MakeSign(SIGN_TYPE_HMAC_SHA256); } /** * * 檢測簽名是否正確 * 正確返回true,錯誤拋異常 */ public bool CheckSign(string signType) { //如果沒有設置簽名,則跳過檢測 if (!IsSet("sign")) { throw new WxPayException("WxPayData簽名存在但不合法!"); } //如果設置了簽名但是簽名為空,則拋異常 else if (GetValue("sign") == null || GetValue("sign").ToString() == "") { throw new WxPayException("WxPayData簽名存在但不合法!"); } //獲取接收到的簽名 string return_sign = GetValue("sign").ToString(); //在本地計算新的簽名 string cal_sign = MakeSign(signType); if (cal_sign == return_sign) { return true; } throw new WxPayException("WxPayData簽名驗證錯誤!"); } /** * * 檢測簽名是否正確 * 正確返回true,錯誤拋異常 */ public bool CheckSign() { return CheckSign(SIGN_TYPE_HMAC_SHA256); } /** * @獲取Dictionary */ public SortedDictionary<string, object> GetValues() { return m_values; } private string CalcHMACSHA256Hash(string plaintext, string salt) { string result = ""; var enc = Encoding.UTF8;// Encoding.Default; byte[] baText2BeHashed = enc.GetBytes(plaintext), baSalt = enc.GetBytes(salt); System.Security.Cryptography.HMACSHA256 hasher = new HMACSHA256(baSalt); byte[] baHashedText = hasher.ComputeHash(baText2BeHashed); result = string.Join("", baHashedText.ToList().Select(b => b.ToString("x2")).ToArray()); return result; } }
4、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) { X509Certificate2 cert = new X509Certificate2(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) { System.Threading.Thread.ResetAbort(); } catch (WebException e) { throw new WxPayException(e.ToString()); } catch (Exception e) { throw new WxPayException(e.ToString()); } finally { //關閉連接和流 if (response != null) { response.Close(); } if (request != null) { request.Abort(); } } return result; }