標籤:
一.前言
之前已經將銀聯支付功能進行了整合(伺服器端戳這裡,用戶端戳這裡),暫時將退款功能擱下了,今天抽了一小段光陰把這個洞給補上了。
其實有了上一次整合支付功能的經驗,對退貨退款的整合就很容易實現了。本文只講伺服器端的處理,用戶端根據需求寫好就行。
銀聯官方提供了一個退貨退款流程圖:
所以過程主要是:伺服器端組織好請求報文->銀聯絡統進行處理->將受理結果和處理結果返回給伺服器。
二.實現
我在代碼中做了一些注釋,所以看完代碼和注釋基本就沒問題了。前提條件依舊是,完成好各項配置工作,可以參考伺服器端的博文。
只是請注意一點:銀聯支付成功後會返回一個流水號,該流水號是後續操作的輸入(退貨、退款、查詢支付狀態等操作)而不是訂單號(原因很簡單啊,訂單號是我們按一定規則產生的,銀聯絡統肯定不認),所以必須將該流水號和我們需要操作的訂單進行綁定,當然最好的方式就是在訂單表裡增加一個流水號欄位。
1.第一步
組織請求報文,向銀聯後台發起退貨退款請求。
/** * 退款流程 * @param orderId //需要退貨退款的訂單ID * @param request * @param response * @throws UnsupportedEncodingException */ @RequestMapping(value = "/pay/refund/{orderId}") @ResponseBody public JSONObject refund(@PathVariable("orderId") String orderId,HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException { //防止亂碼,根據業務需求,這兩句可有可無 request.setCharacterEncoding(DemoBase.encoding_UTF8); response.setContentType("text/html; charset="+ DemoBase.encoding_UTF8); //json用於將資料返回給用戶端 JSONObject json = new JSONObject(); System.out.println("退款開始"); //獲得該訂單的資訊 Order order = orderDAO.getOrder(orderId); if(order==null) { json.put("result", "0"); return json; } //獲得該訂單的流水號 String orderQueryId = order.getOrderQueryId(); //獲得該訂單的總價 String orderOilTotalPrice = order.getOrderOilTotalPrice(); Map<String, String> data = new HashMap<String, String>(); /***銀聯全渠道系統,產品參數,除了encoding自行選擇外其他不需修改***/ data.put("version", DemoBase.version); //版本號碼 data.put("encoding", DemoBase.encoding_UTF8); //字元集編碼 可以使用UTF-8,GBK兩種方式 data.put("signMethod", "01"); //簽名方法 目前只支援01-RSA方式認證加密 data.put("txnType", "04"); //交易類型 04-退貨 data.put("txnSubType", "00"); //交易子類型 預設00 data.put("bizType", "000201"); //業務類型 data.put("channelType", "08"); //渠道類型,07-PC,08-手機 /***商戶接入參數***/ data.put("merId", DemoBase.merId); //商戶號碼,請改成自己申請的商戶號或者open上註冊得來的777商戶號測試 data.put("accessType", "0"); //接入類型,商戶接入固定填0,不需修改 //一定要注意,該orderId並不是我們自己的訂單id,而是退款申請這條業務的id,銀聯提供的DemoBase.getOrderId()是根據系統時間產生的。 data.put("orderId", DemoBase.getOrderId()); //商戶訂單號,8-40位元字字母,不能含“-”或“_”,可以自行定製規則,重新產生,不同於原消費 data.put("txnTime", DemoBase.getCurrentTime()); //訂單發送時間,格式為YYYYMMDDhhmmss,必須取目前時間,否則會報txnTime無效 data.put("currencyCode", "156"); //交易幣種(境內商戶一般是156 人民幣) //一定注意,退款金額必須是整數,而且單位是分。 data.put("txnAmt", Double.valueOf(orderOilTotalPrice).intValue()*100+""); //****退貨金額,單位分,不要帶小數點。退貨金額小於等於原消費金額,當小於的時候可以多次退貨至退貨累計金額等於原消費金額 //data.put("txnAmt",orderOilTotalPrice); //這個透傳欄位很有用,因為前面的orderId已經不是我們自己的訂單id了,而我們在後台通知裡肯定還需要這個訂單id,因為我們需要修改該訂單的狀態,可是data中根本不能put我們自己的訂單id,那麼這個欄位就是用來存放一些我們想傳遞給後台通知的資料,因為data中的資料都會完整的全部返回給後台通知,我這裡只是在透傳欄位裡放了一個orderId,如果有更多的資料需要傳遞,只需要用map或者json儲存資料,然後轉成String就可以了 data.put("reqReserved", orderId); //請求方保留網域,透傳欄位(可以實現商戶自訂參數的追蹤)本交易的後台通知,對本交易的交易狀態查詢交易、對賬檔案中均會原樣返回,商戶可以按需上傳,長度為1-1024個位元組 //後台通知地址必須是真實ip,因為銀聯後台要將通知post到這個後台地址。 data.put("backUrl", DemoBase.backUrl); //後台通知地址,後台通知參數詳見open.unionpay.com協助中心 下載 產品介面規範 網關支付產品介面規範 退貨交易 商戶通知,其他說明同消費交易的後台通知 /***要調通交易以下欄位必須修改***/ //流水號,這才是銀聯後台認識的標識符,該流水號是在支付成功後獲得的。 data.put("origQryId", orderQueryId); //****原消費交易返回的的queryId,可以從消費交易後台通知介面中或者交易狀態查詢介面中擷取 /**請求參數設定完畢,以下對請求參數進行簽名並發送http post請求,接收同步應答報文------------->**/ Map<String, String> reqData = AcpService.sign(data,DemoBase.encoding_UTF8); //報文中certId,signature的值是在signData方法中擷取並自動賦值的,只要認證配置正確即可。 String url = SDKConfig.getConfig().getBackRequestUrl(); //交易請求url從設定檔讀取對應屬性檔案acp_sdk.properties中的 acpsdk.backTransUrl Map<String, String> rspData = AcpService.post(reqData, url,DemoBase.encoding_UTF8);//這裡調用signData之後,調用submitUrl之前不能對submitFromData中的索引值對做任何修改,如果修改會導致驗簽不通過 /**對應答碼的處理,請根據您的商務邏輯來編寫程式,以下應答碼處理邏輯僅供參考------------->**/ //應答碼規範參考open.unionpay.com協助中心 下載 產品介面規範 《平台接入介面規範-第5部分-附錄》 if(!rspData.isEmpty()){ if(AcpService.validate(rspData, DemoBase.encoding_UTF8)){ LogUtil.writeLog("驗證簽名成功"); String respCode = rspData.get("respCode") ; if(("00").equals(respCode)){ //交易已受理(不代表交易已成功),等待接收後台通知更新訂單狀態,也可以主動發起 查詢交易確定交易狀態。 //TODO json.put("result", "1"); return json; }else if(("03").equals(respCode)|| ("04").equals(respCode)|| ("05").equals(respCode)){ //後續需發起交易狀態查詢交易確定交易狀態 //TODO }else{ //其他應答碼為失敗請排查原因 //TODO } }else{ LogUtil.writeErrorLog("驗證簽名失敗"); //TODO 檢查驗證簽名失敗的原因 } }else{ //未返回正確的http狀態 LogUtil.writeErrorLog("未擷取到返回報文或返回http狀態代碼非200"); } json.put("result", "0"); return json; }
2.第二步
後台通知地址中接收銀聯背景處理結果通知,在上一篇中已經寫了後台通知處理,這次加上退款功能後就稍微修改了一下。
/** * 後台通知處理 * @param request * @param response */ @RequestMapping(value = "/pay/backRcvResponse") @ResponseBody public void backRcvResponse(HttpServletRequest request, HttpServletResponse response) { System.out.println("後台通知驗簽開始"); //return AcpService.validateAppResponse(sign, DemoBase.encoding_UTF8); //System.out.println("驗簽開始"); String encoding = request.getParameter(SDKConstants.param_encoding); // 擷取銀聯通知伺服器發送的後台通知參數 Map<String, String> reqParam = Tool.getAllRequestParam(request); LogUtil.printRequestLog(reqParam); Map<String, String> valideData = null; try { if (null != reqParam && !reqParam.isEmpty()) { Iterator<Entry<String, String>> it = reqParam.entrySet().iterator(); valideData = new HashMap<String, String>(reqParam.size()); while (it.hasNext()) { Entry<String, String> e = it.next(); String key = (String) e.getKey(); String value = (String) e.getValue(); value = new String(value.getBytes(encoding), encoding); valideData.put(key, value); } } //重要!驗證簽名前不要修改reqParam中的索引值對的內容,否則會驗簽不過 if (!AcpService.validate(valideData, encoding)) { LogUtil.writeLog("驗證簽名結果[失敗]."); //驗簽失敗,需解決驗簽問題 } else { LogUtil.writeLog("驗證簽名結果[成功]."); //【註:為了安全驗簽成功才應該寫商戶的成功處理邏輯】交易成功,更新商戶訂單狀態 //String orderId =valideData.get("orderId"); //擷取後台通知的資料 //Order order = orderDAO.getOrder(orderId); //擷取交易類型,可參考官方文檔 String txnType = valideData.get("txnType"); System.out.println(Integer.valueOf(txnType)); Order order; switch(Integer.valueOf(txnType))//交易類型 { case 01://消費 String orderId =valideData.get("orderId"); //擷取後台通知的資料,其他欄位也可用類似方式擷取 String payTime = valideData.get("txnTime"); String orderQueryId = valideData.get("queryId"); //String respCode =valideData.get("respCode"); //擷取應答碼,收到後台通知了respCode的值一般是00,可以不需要根據這個應答碼判斷。 //if(orderId!=null&&!"".equals(orderId)) //{ order = orderDAO.getOrder(orderId); if(order!=null) { System.out.println("更新支付狀態:"+orderId); System.out.println("payTime"+payTime); order.setOrderPayStatus(1); order.setOrderPayTime(payTime+".0"); order.setOrderQueryId(orderQueryId); //order.setOrderPayTime(new SimpleDateFormat(" yyyy-MM-dd HH:mm:ss ").parse(payTime)); boolean sucess = orderDAO.update(order); System.out.println("sucess"+sucess); } //} break; case 04://退貨&退款 //獲得透傳欄位的資料,我這裡就是訂單id order = orderDAO.getOrder(valideData.get("reqReserved")); if(order!=null) { //更新訂單狀態 order.setOrderPayStatus(2); order.setOrderStatus(2); orderDAO.update(order); } break; default: break; } } LogUtil.writeLog("BackRcvResponse接收後台通知結束"); //返回給銀聯伺服器http 200 狀態代碼 response.getWriter().print("ok"); } catch(Exception e){} }
ok,還是看一下效果吧。respCode = 00表示成功。
三.問題
不會遇到問題是不可能的,當然按照我上面代碼的實現的也只是能把基本流程跑通,從安全性上來講,還差太多…
1.退款金額必須是整數
比如下面圖中的情況:
2.data中的orderId不是自己的orderId,而是本次退款交易的id。
3.後台通知地址一定是真是ip,localhost不行,迴路地址也不行。
Android開發:app工程整合銀聯-退貨退款功能