1.概念普及
一、理解什麼是UnionPay、ChinaPay
這兩個概念如果搞不清楚,絕對夠你瞎折騰一段時間的。
UnionPay:中國銀聯,最大的機構;他本身也提供系統介面但都是B2B的,對於單個商戶他們不提供客服,也不提供技術解決,更不會提供商戶後臺(可查消費記錄等);但他的技術介面文件比較齊全,而且也可以使用,警惕不要使用這些介面。
ChinaPay:銀聯電子支付公司,第三方的支付公司,UnionPay的所有介面和服務都託管給類似的第三方公司,ChinaPay再向商戶服務,ChinaPay有自己的介面標準,但是非常不完善,目前只有java版本的案例;並且從UnionPay官網開通商戶後,預設根據地區會自動轉到諸如“ChinaPay”這樣的第三方支付公司,後面的事情全由ChinaPay代管。
2.ChinaPay商戶後臺
開通商戶之後,一般銷售會傳送郵件給你,一般郵件內容包含開戶名稱、技術支援聯絡方式等,另附帶一個壓縮包,主要包括如下內容:
txt:登入賬號資訊
cp.cer:加密公鑰
NetPayClient:.Net的元件和properity配置檔案
doc:介面文件等
logo:按鈕的標準圖片
3.登入ChinaPay商戶後臺獲取交易證照
http://merchant.chinapay.com ,建議用IE或者FireFox登入,需要先下載和安裝證照,安裝ActiveX控制元件,登入證照只提供兩份,多餘兩份的請聯絡銷售;
登入成功之後可以看到訂單、退款單等賬單;
要實現支付介面,必須先獲取一個“交易證照”
點選交易申請證照,獲取到證照之後,需要將證照的上傳回商戶後臺,同時本地需要匯出證照私鑰(帶密碼),以備用。
4.RSA公鑰私鑰加密
本地商城等和ChinaPay介面通訊的時候都需要對資料進行加密和驗證有效性。這裡就牽扯加密的知識了。
RSA公鑰私鑰知識只需要記住下面這三點即可
1.一個端有公鑰和私鑰兩個檔案(或者兩個字串),通訊時候可以用公鑰或者私鑰加密
2.公鑰加密資料傳送,私鑰解密:保證資訊的完整性和保密性,保證加密後的資訊第三方劫持後無法檢視內容,例如郵件等。
3.私鑰加密資料傳送,公鑰解密:保證資料來源可靠,對資訊進行簽名,chinapay即使利用此方式,常用於公告、群發等操作。
5.準備工作
將NetPayClient檔案的dll引用到你的專案,在程式裡建立一個chinapay的資料夾,放入ChinaPay的公鑰(cp.cer)、私鑰(交易證照匯出的pfx檔案,有密碼),以及security.properties檔案
security.properties,開啟修改具體的cer、pfx檔案的路徑以及私鑰密碼
6.付款流程
跟其他產品一樣,根據引數構造一個form,然後提交到目標url中,提交同時加入簽名。
log.Debug("開始支付..."); string payUrl = "https://payment.chinapay.com/CTITS/service/rest/page/nref/000000000017/0/0/0/0/0"; Hashtable myMap = new Hashtable(); myMap.Add("MerId", ChinaPayHelper.merchantCode); string billNO = "CO" + DateTime.Now.ToString("yyyyMMddHHmmssfff"); myMap.Add("MerOrderNo", billNO); myMap.Add("TranDate", DateTime.Now.ToString("yyyyMMdd"));//交易日期 myMap.Add("TranTime", DateTime.Now.ToString("HHmmss"));//交易時間 myMap.Add("OrderAmt", "20"); myMap.Add("TranType", "0001"); myMap.Add("BusiType", "0001"); myMap.Add("MerPageUrl", Request.Url.Scheme + "://" + Request.Url.Authority + "/Home/PayResult"); myMap.Add("MerBgUrl", Request.Url.Scheme + "://" + Request.Url.Authority + "/Home/PayBackRcv"); myMap.Add("CurryNo", "CNY"); myMap.Add("PayTimeOut", "145"); myMap.Add("Version", "20140728"); myMap.Add("CommodityMsg", "ChinaPay測試-商品資訊"); myMap.Add("MerResv", "ChinaPay測試-商戶保留域"); //myMap.Add("TranReserved", "{\"Referred\":\"www.chinapay.com\",\"BusiId\":\"0001\",\"TimeStamp\":\"1438915150976\",\"Remoteputr\":\"172.16.9.44\"}"); /***********************/ //坑1:這裡注意,如果採用chinpay自帶的簽名,必須在伺服器的瀏覽器上獲取交易私鑰,否則由開發環境轉到生產環境會出錯!!!此坑注意 //chinapaysecure.SecssUtil su = new chinapaysecure.SecssUtil(); //string path = Server.MapPath("/cer/security.properties"); //log.Debug(path); //bool flag = su.init(path); //su.sign(myMap); //if ("00" != su.getErrCode()) //{ // log.Error(su.getErrCode() + "=" + su.getErrMsg()); // ViewBag.ChinaPay = "簽名過程發生錯誤,錯誤資訊為-->" + su.getErrMsg(); // return View(); //} //myMap.Add("Signature", su.getSign()); /*******************************************/ //以下為採用自己擴充套件的簽名方法,無需關心測試還是生產環境的問題!!! string signValue = ChinaPayHelper.Sign(myMap);
myMap.Add("Signature", signValue); string sHtmlText = ChinaPayHelper.BuildRequest(myMap, payUrl); Response.Write(sHtmlText);
支付成功,前後臺通知頁面
//同步通知方法 public ActionResult PayResult() { NameValueCollection coll = Request.Form; string[] requestItem = coll.AllKeys; Hashtable myMap = new Hashtable(); for (int i = 0; i < requestItem.Length; i++) { myMap.Add(requestItem[i], Request.Form[requestItem[i]]);//前臺方法接收引數無需UrlDecode,但非同步方法必須轉換 } SecssUtil su = new SecssUtil(); su.init(Server.MapPath("/ChinaPay/CERS/security.properties")); su.verify(myMap); string billNo = myMap["MerOrderNo"].ToString(); string billId = myMap["MerResv"].ToString(); if ("00" != su.getErrCode()) { log.Error("ChinaPayReturn銀聯支付返回資料驗證失敗:" + billNo + ";" + billId + ";" + su.getErrCode() + su.getErrMsg()); ViewBag.ChinaPayResult = "ChinaPayReturn銀聯支付返回資料驗證失敗"; return View(); } StringBuilder sbHtml = new StringBuilder(); sbHtml.Append("<table>"); foreach (DictionaryEntry de in myMap) { sbHtml.Append("<tr><td>" + de.Key.ToString() + "</td><td>" + Server.UrlDecode(de.Value.ToString()) + "</td></tr>"); } sbHtml.Append("</table>"); ViewBag.ChinaPayResult=sbHtml.ToString(); return View(); } //非同步後臺方法,用於接收支付成功和退款成功的通知 [HttpPost] public void PayBackRcv() { NameValueCollection coll = Request.Form; string[] requestItem = coll.AllKeys; Hashtable myMap = new Hashtable(); for (int i = 0; i < requestItem.Length; i++) { log.Debug(requestItem[i] + "=" + HttpUtility.UrlDecode(Request.Form[requestItem[i]])); myMap.Add(requestItem[i], HttpUtility.UrlDecode(Request.Form[requestItem[i]]));//需要UrlDecode,否則驗籤失敗!!! } SecssUtil su = new SecssUtil(); su.init(Server.MapPath("/ChinaPay/CERS/security.properties")); su.verify(myMap); string billNo = myMap["MerOrderNo"].ToString(); string billId = myMap["MerResv"].ToString(); if ("00" != su.getErrCode()) { log.Error("ChinaPayNotify銀聯支付返回資料驗證失敗:" + billNo + ";" + billId + ";" + su.getErrCode() + su.getErrMsg()); Response.Write("fail"); return; } //處理您的業務邏輯 //END }
非同步方法無需返回特定的code給chinapay,chinapay是根據httpstatus判斷的,如果是200,都認為收到訊息了。
坑2:如果出現簽名失敗,需要新增登錄檔許可權,一般本機測試用管理員開啟vs不存在這個問題,部署到伺服器之後需要加許可權
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\eventlog
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\eventlog\Security
對這兩條加入Network service的讀寫許可權。
之後可以在Window日誌中除錯支付中碰到的問題了。
7. 退款
string cpRefundUrl = "https://payment.chinapay.com/CTITS/service/rest/forward/syn/000000000065/0/0/0/0/0"; Hashtable myMap = new Hashtable(); myMap.Add("MerId", ChinaPayHelper.merchantCode); myMap.Add("MerOrderNo", "RO"+DateTime.Now.ToString("yyyyMMddHHmmssfff")); myMap.Add("TranDate", DateTime.Now.ToString("yyyyMMdd"));//交易日期 myMap.Add("TranTime", DateTime.Now.ToString("HHmmss"));//交易時間 myMap.Add("OriOrderNo", "20151010152643514");//改成單號 myMap.Add("OriTranDate", "20151010"); myMap.Add("RefundAmt", "10"); //myMap.Add("OrderAmt", "10"); myMap.Add("TranType", "0401"); myMap.Add("BusiType", "0001"); myMap.Add("MerResv", Guid.NewGuid().ToString()); myMap.Add("MerBgUrl", Request.Url.Scheme + "://" + Request.Url.Authority + "/Home/PayBackRcv"); myMap.Add("CurryNo", "CNY"); myMap.Add("Version", "20140728"); string param = ChinaPayHelper.sort(myMap, null); String chkValue = ChinaPayHelper.Sign(myMap); myMap.Add("Signature", chkValue); param += "&Signature=" + HttpUtility.UrlEncode(chkValue); log.Debug("銀聯退款:" + param); string rst = ChinaPayHelper.Post(cpRefundUrl + "?" + param, ""); log.Debug(rst); Hashtable rstMap = ChinaPayHelper.Str2HashMap(rst); if ("0000,1022,1003".Contains(rstMap["respCode"].ToString())) { return View(("[" + rstMap["respCode"].ToString() + "-" + rstMap["respMsg"].ToString() + "] 銀聯退款已提交,退款成功後將自動返回錢款到客戶銀聯卡!")); } else { return View(("[" + rstMap["respCode"].ToString() + "-" + rstMap["respMsg"].ToString() + "] 銀聯退款失敗!")); }
注意修改原始單號和原始交易日期以及退款金額:
myMap.Add("OriOrderNo", "20151010152643514");//改成單號 myMap.Add("OriTranDate", "20151010"); myMap.Add("RefundAmt", "10");
一般退款會在24小時內完成
8.退款沒有非同步通知???
真碰到過,聯絡客服之後,說是介面問題,他們修復好了之後便正常了,所以如果遇到什麼問題,實在解決不了,趕緊電話客服,chinapay不是那麼靠譜的。。。。!!!
9.自己封裝的工具類,除了sign是override官方的,其他都是官方原始檔裡複製除了的方法,部分方法值得優化哦
using chinapaysecure; using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Web;
/******
jackchain
jackchain@chinacloudtech.com
2015-10-08
*****/
namespace ZPS.Tools { public class ChinaPayHelper { #region 基礎配置 public static string merchantCode = "481601509175565"; //商戶號 private static string privateKeyPath = "/ChinaPay/CERS/donoratico.pfx"; //私鑰檔案地址 private static string privateKeyPwd = "byby1231818"; //私鑰密碼 #endregion /// <summary> /// 簽名,注意官方給出的簽名方法,要求私鑰必須是安裝在伺服器的,假若安裝在本地測試機器上,等釋出到生產環境中後會出現證照無法使用的錯誤 /// </summary> /// <param name="param">需要加密的字串</param> /// <returns></returns> public static string Sign(Hashtable myMap) { string param = sort(myMap, null); X509Certificate2 certificate = new X509Certificate2(System.Web.HttpContext.Current.Server.MapPath(privateKeyPath), privateKeyPwd, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet); RSAParameters key = ((RSACryptoServiceProvider)certificate.PrivateKey).ExportParameters(true); return Convert.ToBase64String(HashAndSignBytes(Encoding.UTF8.GetBytes(param), key)); } /// <summary> /// 構造提交表單 /// </summary> /// <param name="myMap"></param> /// <param name="actionUrl"></param> /// <returns></returns> public static string BuildRequest(Hashtable myMap, string actionUrl) { StringBuilder sbHtml = new StringBuilder(); sbHtml.Append("<form id='cpsubmit' name='cpsubmit' action='" + actionUrl + "' method='POST'>"); foreach (DictionaryEntry de in myMap) { sbHtml.Append("<input type='hidden' name='" + de.Key.ToString() + "' value='" + de.Value.ToString() + "'/>"); } //submit按鈕控制元件請不要含有name屬性 sbHtml.Append("<input type='submit' value='CHINAPAY' style='display:none;'></form>"); sbHtml.Append("<script>document.forms['cpsubmit'].submit();</script>"); return sbHtml.ToString(); } /// <summary> /// 呼叫遠端Restful服務 /// </summary> /// <param name="url">url地址</param> /// <param name="param">引數</param> /// <param name="time">超時時間</param> /// <returns></returns> public static string Post(string url, string param, int time = 60000) { Uri address = new Uri(url); HttpWebRequest request = WebRequest.Create(address) as HttpWebRequest; request.Method = "POST"; request.ContentType = "application/json;charset=utf-8"; //"application/x-www-form-urlencoded"; request.Timeout = time; byte[] byteData = UTF8Encoding.UTF8.GetBytes(param == null ? "" : param); request.ContentLength = byteData.Length; using (Stream postStream = request.GetRequestStream()) { postStream.Write(byteData, 0, byteData.Length); } string result = ""; using (HttpWebResponse response = request.GetResponse() as HttpWebResponse) { StreamReader reader = new StreamReader(response.GetResponseStream()); result = reader.ReadToEnd(); } return (result); } /// <summary> /// 銀聯應答引數轉換為雜湊表 /// </summary> /// <param name="param"></param> /// <returns></returns> public static Hashtable Str2HashMap(string param) { string[] kv = param.Split('&'); Hashtable map = new Hashtable(); foreach (string str in kv) { string[] temp = str.Split('='); if (temp != null && temp.Length >= 2) { map.Add(temp[0], temp[1]); } } return map; } #region 以下方法ChinaPay private static byte[] HashAndSignBytes(byte[] DataToSign, RSAParameters Key) { try { RSACryptoServiceProvider provider = new RSACryptoServiceProvider(); provider.ImportParameters(Key); return provider.SignData(DataToSign, new SHA512CryptoServiceProvider()); } catch (CryptographicException exception) { Console.WriteLine(exception.Message); return null; } } public static string sort(Hashtable paramHashTable, string[] invalidList) { IDictionaryEnumerator enumerator = paramHashTable.GetEnumerator(); ArrayList list = new ArrayList(); while (enumerator.MoveNext()) { string str = enumerator.Key.ToString(); enumerator.Value.ToString(); if (((invalidList == null) || (invalidList.Length <= 0)) || !invalidList.Contains<string>(str)) { list.Add(str); } } IComparer comparer = new myReverserClass(); list.Sort(comparer); string str2 = string.Empty; for (int i = 0; i < list.Count; i++) { string str3 = list[i].ToString(); string str4 = paramHashTable[str3].ToString(); if (i == (list.Count - 1)) { str2 = str2 + str3 + "=" + str4; } else { string str5 = str2; str2 = str5 + str3 + "=" + str4 + "&"; } } StringBuilder builder = new StringBuilder(str2); return builder.ToString(); } #endregion } }
11.總結
1.理解unionpay、chinapay可以少走很多彎路
2.理解RSA方法、警惕交易證照申請和安裝,否則很可能導致測試環境能用、正式環境無法sign
3.伺服器登錄檔許可權,EventLog需要新增IIS許可權
4.多問客服
一個不小的支付公司,卻沒有全語言的demo,真是悲哀,只好貢獻一個.Net的了(MVC)下載demo http://download.csdn.net/detail/ovenj/9186317