一、首先分享php
微信自動退款接口: html
https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4java
微信付款 代碼案例 (不少共同的代碼 都在付款邏輯裏面)git
http://www.javashuo.com/article/p-azwpdbqh-e.htmlweb
二、小程序端代碼 栗子算法
//獲取openId wx.request({ url: 'http://192.168.1.183:8081/order/refund', data: { 'amount':1, 'incrementId': outTradeNo, 'orderId': orderId, 'productId': productId, 'amount': amount, 'sku': sku, 'name': name }, method: 'POST', header: { 'content-type': 'application/x-www-form-urlencoded' }, success: function (result) { }
二、接口端代碼 栗子sql
@Transactional(rollbackFor=MyException.class) @Override public JSONObject refundOrder(HttpServletRequest request) { //設置最終返回對象 JSONObject resultJson = new JSONObject(); //接受參數(金額) String amount = request.getParameter("amount"); //接受參數(訂單Id) String orderId = request.getParameter("orderId"); //接受參數(商品ID) String productId = request.getParameter("productId"); //接受參數(商品sku) String sku = request.getParameter("sku"); //接受參數(商品name) String name = request.getParameter("name"); //接受參數(商品訂單號) String incrementId = request.getParameter("incrementId"); //建立hashmap(用戶得到簽名) SortedMap<String, String> paraMap = new TreeMap<String, String>(); //設置隨機字符串 String nonceStr = Utils.getUUIDString().replaceAll("-", ""); //設置商戶退款單號 Integer randomNumber = new Random().nextInt(900)+ 100; String orderIncrementId = DateUtil.formatDate(new Date(), DateUtil.DATE_FMT_FOR_ORDER_NUMBER)+randomNumber; //設置請求參數(小程序ID) paraMap.put("appid", Configuration.APPLYID); //設置請求參數(商戶號) paraMap.put("mch_id", Configuration.MCHID); //設置請求參數(隨機字符串) paraMap.put("nonce_str", nonceStr); //設置請求參數(商戶訂單號) paraMap.put("out_trade_no", incrementId); //設置請求參數(商戶退款單號) paraMap.put("out_refund_no", orderIncrementId); //設置請求參數(訂單金額) paraMap.put("total_fee", amount); //設置請求參數(退款金額) paraMap.put("refund_fee", amount); //TODO (這個回調地址 沒有具體進行測試 須要寫好邏輯 打版在測試)設置請求參數(通知地址) paraMap.put("notify_url", "http://abcdefg.nat123.cc:443/order/refundCallback"); //調用邏輯傳入參數按照字段名的 ASCII 碼從小到大排序(字典序) String stringA = formatUrlMap(paraMap, false, false); //第二步,在stringA最後拼接上key獲得stringSignTemp字符串,並對stringSignTemp進行MD5運算,再將獲得的字符串全部字符轉換爲大寫,獲得sign值signValue。(簽名) String sign = MD5Util.MD5(stringA+"&key="+Configuration.KEY).toUpperCase(); //將參數 編寫XML格式 StringBuffer paramBuffer = new StringBuffer(); paramBuffer.append("<xml>"); paramBuffer.append("<appid>"+Configuration.APPLYID+"</appid>"); paramBuffer.append("<mch_id>"+Configuration.MCHID+"</mch_id>"); paramBuffer.append("<nonce_str>"+paraMap.get("nonce_str")+"</nonce_str>"); paramBuffer.append("<sign>"+sign+"</sign>"); paramBuffer.append("<out_refund_no>"+paraMap.get("out_refund_no")+"</out_refund_no>"); paramBuffer.append("<out_trade_no>"+paraMap.get("out_trade_no")+"</out_trade_no>"); paramBuffer.append("<refund_fee>"+paraMap.get("refund_fee")+"</refund_fee>"); paramBuffer.append("<total_fee>"+paraMap.get("total_fee")+"</total_fee>"); paramBuffer.append("<notify_url>"+paraMap.get("notify_url")+"</notify_url>"); paramBuffer.append("</xml>"); try { //發送請求(POST)(得到數據包ID)(這有個注意的地方 若是不轉碼成ISO8859-1則會告訴你body不是UTF8編碼 就算你改爲UTF8編碼也同樣很差使 因此修改爲ISO8859-1) Map<String,String> map = doXMLParse(doRefund(request,Configuration.REFUND_URL, new String(paramBuffer.toString().getBytes(), "ISO8859-1"))); //應該建立 退款表數據 if(map!=null && (StringUtils.isNotBlank(map.get("return_code")) && "SUCCESS".equals(map.get("return_code")))){ if(StringUtils.isBlank(map.get("err_code_des"))) { //接口調用成功 執行操做邏輯 返回成功狀態碼給前臺 }else { resultJson.put("returnCode", "error"); resultJson.put("err_code_des", map.get("err_code_des")); } }else { resultJson.put("returnCode", map.get("return_code")); resultJson.put("err_code_des", map.get("err_code_des")); } } catch (UnsupportedEncodingException e) { log.info("微信 退款 異常:"+e.getMessage()); e.printStackTrace(); } catch (Exception e) { log.info("微信 退款 異常:"+e.getMessage()); e.printStackTrace(); } log.info("微信 退款 失敗"); return resultJson;
三、Http請求 代碼(這塊的代碼邏輯和付款的是不同的)json
private String doRefund(HttpServletRequest request,String url,String data) throws Exception{ /** * 注意PKCS12證書 是從微信商戶平臺-》帳戶設置-》 API安全 中下載的 */ KeyStore keyStore = KeyStore.getInstance("PKCS12"); String substring = request.getSession().getServletContext().getRealPath("/").substring(0, request.getSession().getServletContext().getRealPath("/").lastIndexOf("webapp\\")); FileInputStream instream = new FileInputStream(substring+"resources/refund_certificate/apiclient_cert.p12");//P12文件目錄 證書路徑 try { /** * 此處要改 * */ keyStore.load(instream, Configuration.MCHID.toCharArray());//這裏寫密碼..默認是你的MCHID } finally { instream.close(); } // Trust own CA and all self-signed certs /** * 此處要改 * */ SSLContext sslcontext = SSLContexts.custom() .loadKeyMaterial(keyStore, Configuration.MCHID.toCharArray())//這裏也是寫密碼的 .build(); // Allow TLSv1 protocol only SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory( sslcontext, new String[] { "TLSv1" }, null, SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER); CloseableHttpClient httpclient = HttpClients.custom() .setSSLSocketFactory(sslsf) .build(); try { HttpPost httpost = new HttpPost(url); // 設置響應頭信息 httpost.addHeader("Connection", "keep-alive"); httpost.addHeader("Accept", "*/*"); httpost.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); httpost.addHeader("Host", "api.mch.weixin.qq.com"); httpost.addHeader("X-Requested-With", "XMLHttpRequest"); httpost.addHeader("Cache-Control", "max-age=0"); httpost.addHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0) "); httpost.setEntity(new StringEntity(data, "UTF-8")); CloseableHttpResponse response = httpclient.execute(httpost); try { HttpEntity entity = response.getEntity(); String jsonStr = EntityUtils.toString(response.getEntity(), "UTF-8"); EntityUtils.consume(entity); return jsonStr; } finally { response.close(); } } finally { httpclient.close(); } }
四、退款結果通知 後臺代碼 栗子小程序
AESUtil微信小程序
package com.bodi.repository; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; public class AESUtil { /** * 密鑰算法 */ private static final String ALGORITHM = "AES"; /** * 加解密算法/工做模式/填充方式 */ private static final String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS5Padding"; /** * 生成key */ private static SecretKeySpec key = new SecretKeySpec(MD5Util.MD5Encode(Configuration.KEY, "UTF-8").toLowerCase().getBytes(), ALGORITHM); /** * AES加密 * * @param data * @return * @throws Exception */ public static String encryptData(String data) throws Exception { // 建立密碼器 Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING); // 初始化 cipher.init(Cipher.ENCRYPT_MODE, key); return Base64Util.encode(cipher.doFinal(data.getBytes())); } /** * AES解密 * * @param base64Data * @return * @throws Exception */ public static String decryptData(String base64Data) throws Exception { Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING); cipher.init(Cipher.DECRYPT_MODE, key); return new String(cipher.doFinal(Base64Util.decode(base64Data))); }
Base64Util
package com.bodi.repository; import java.io.ByteArrayOutputStream; public class Base64Util { private static final char[] base64EncodeChars = new char[] { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' }; private static byte[] base64DecodeChars = new byte[] { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1 }; private Base64Util() { } /** * 將字節數組編碼爲字符串 * * @param data */ public static String encode(byte[] data) { StringBuffer sb = new StringBuffer(); int len = data.length; int i = 0; int b1, b2, b3; while (i < len) { b1 = data[i++] & 0xff; if (i == len) { sb.append(base64EncodeChars[b1 >>> 2]); sb.append(base64EncodeChars[(b1 & 0x3) << 4]); sb.append("=="); break; } b2 = data[i++] & 0xff; if (i == len) { sb.append(base64EncodeChars[b1 >>> 2]); sb.append(base64EncodeChars[((b1 & 0x03) << 4) | ((b2 & 0xf0) >>> 4)]); sb.append(base64EncodeChars[(b2 & 0x0f) << 2]); sb.append("="); break; } b3 = data[i++] & 0xff; sb.append(base64EncodeChars[b1 >>> 2]); sb.append(base64EncodeChars[((b1 & 0x03) << 4) | ((b2 & 0xf0) >>> 4)]); sb.append(base64EncodeChars[((b2 & 0x0f) << 2) | ((b3 & 0xc0) >>> 6)]); sb.append(base64EncodeChars[b3 & 0x3f]); } return sb.toString(); } public static byte[] decode(String str) throws Exception { byte[] data = str.getBytes("GBK"); int len = data.length; ByteArrayOutputStream buf = new ByteArrayOutputStream(len); int i = 0; int b1, b2, b3, b4; while (i < len) { /* b1 */ do { b1 = base64DecodeChars[data[i++]]; } while (i < len && b1 == -1); if (b1 == -1) { break; } /* b2 */ do { b2 = base64DecodeChars[data[i++]]; } while (i < len && b2 == -1); if (b2 == -1) { break; } buf.write((b1 << 2) | ((b2 & 0x30) >>> 4)); /* b3 */ do { b3 = data[i++]; if (b3 == 61) { return buf.toByteArray(); } b3 = base64DecodeChars[b3]; } while (i < len && b3 == -1); if (b3 == -1) { break; } buf.write(((b2 & 0x0f) << 4) | ((b3 & 0x3c) >>> 2)); /* b4 */ do { b4 = data[i++]; if (b4 == 61) { return buf.toByteArray(); } b4 = base64DecodeChars[b4]; } while (i < len && b4 == -1); if (b4 == -1) { break; } buf.write(((b3 & 0x03) << 6) | b4); } return buf.toByteArray(); } }
MD5
package com.bodi.repository; import java.security.MessageDigest; public class MD5Util { /** * 十六進制下數字到字符的映射數組 */ private final static String[] hexDigits = {"0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"}; /** * @Title: encodeByMD5 * @Description: 對字符串進行MD5編碼 * @author yihj * @param @param originString * @param @return 參數 * @return String 返回類型 * @throws */ public static String MD5(String originString){ if (originString!=null) { try { //建立具備指定算法名稱的信息摘要 MessageDigest md5 = MessageDigest.getInstance("MD5"); //使用指定的字節數組對摘要進行最後更新,而後完成摘要計算 byte[] results = md5.digest(originString.getBytes()); //將獲得的字節數組變成字符串返回 String result = byteArrayToHexString(results); return result; } catch (Exception e) { e.printStackTrace(); } } return null; } public static String MD5Encode(String origin, String charsetname) { String resultString = null; try { resultString = new String(origin); MessageDigest md = MessageDigest.getInstance("MD5"); if (charsetname == null || "".equals(charsetname)) resultString = byteArrayToHexString(md.digest(resultString .getBytes())); else resultString = byteArrayToHexString(md.digest(resultString .getBytes(charsetname))); } catch (Exception exception) { } return resultString; } /** * @Title: byteArrayToHexString * @Description: 輪換字節數組爲十六進制字符串 * @author yihj * @param @param b * @param @return 參數 * @return String 返回類型 * @throws */ private static String byteArrayToHexString(byte[] b){ StringBuffer resultSb = new StringBuffer(); for(int i=0;i<b.length;i++){ resultSb.append(byteToHexString(b[i])); } return resultSb.toString(); } /** * @Title: byteToHexString * @Description: 將一個字節轉化成十六進制形式的字符串 * @author yihj * @param @param b * @param @return 參數 * @return String 返回類型 * @throws */ private static String byteToHexString(byte b){ int n = b; if(n<0) n=256+n; int d1 = n/16; int d2 = n%16; return hexDigits[d1] + hexDigits[d2]; } /** * MD5加密 byte 數據 * * @param source * 要加密字符串的byte數據 * @return */ public static String getMD5(byte[] source) { String s = null; char hexDigits[] = { // 用來將字節轉換成 16 進製表示的字符 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; try { java.security.MessageDigest md = java.security.MessageDigest .getInstance("MD5"); md.update(source); byte tmp[] = md.digest(); // MD5 的計算結果是一個 128 位的長整數, // 用字節表示就是 16 個字節 char str[] = new char[16 * 2]; // 每一個字節用 16 進製表示的話,使用兩個字符, // 因此表示成 16 進制須要 32 個字符 int k = 0; // 表示轉換結果中對應的字符位置 for (int i = 0; i < 16; i++) { // 從第一個字節開始,對 MD5 的每個字節 // 轉換成 16 進制字符的轉換 byte byte0 = tmp[i]; // 取第 i 個字節 str[k++] = hexDigits[byte0 >>> 4 & 0xf]; // 取字節中高 4 位的數字轉換, // >>> // 爲邏輯右移,將符號位一塊兒右移 str[k++] = hexDigits[byte0 & 0xf]; // 取字節中低 4 位的數字轉換 } s = new String(str); // 換後的結果轉換爲字符串 } catch (Exception e) { e.printStackTrace(); } return s; } }
實際退款代碼 邏輯
@Override public void refundCallback(HttpServletRequest request, HttpServletResponse response) { log.info("退款 微信回調接口方法 start"); String inputLine = ""; String notityXml = ""; try { while((inputLine = request.getReader().readLine()) != null){ notityXml += inputLine; } //關閉流 request.getReader().close(); log.info("退款 微信回調內容信息:"+notityXml); //解析成Map Map<String,String> map = doXMLParse(notityXml); //判斷 退款是否成功 if("SUCCESS".equals(map.get("return_code"))){ log.info("退款 微信回調返回是否退款成功:是"); //得到 返回的商戶訂單號 String passMap = AESUtil.decryptData(map.get("req_info")); //拿到解密信息 map = doXMLParse(passMap); //拿到解密後的訂單號 String outTradeNo = map.get("out_trade_no"); log.info("退款 微信回調返回商戶訂單號:"+map.get("out_trade_no")); //支付成功 修改訂單狀態 通知微信成功回調 int sqlRow = orderJpaDao.updateOrderStatus("refunded",new Timestamp(System.currentTimeMillis()), outTradeNo); if(sqlRow == 1) { log.info("退款 微信回調 更改訂單狀態成功"); } }else { //得到 返回的商戶訂單號 String passMap = AESUtil.decryptData(map.get("req_info")); //拿到解密信息 map = doXMLParse(passMap); //拿到解密後的訂單號 String outTradeNo = map.get("out_trade_no"); //更改 狀態爲取消 int sqlRow = orderJpaDao.updateOrderStatus("canceled",new Timestamp(System.currentTimeMillis()), outTradeNo); if(sqlRow == 1) { log.info("退款 微信回調返回是否退款成功:否"); } } //給微信服務器返回 成功標示 不然會一直詢問 我們服務器 是否回調成功 PrintWriter writer = response.getWriter(); //封裝 返回值 StringBuffer buffer = new StringBuffer(); buffer.append("<xml>"); buffer.append("<return_code><![CDATA[SUCCESS]]></return_code>"); buffer.append("<return_msg><![CDATA[OK]]></return_msg>"); buffer.append("</xml>"); //返回 writer.print(buffer.toString()); } catch (IOException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } }
五、注意事項
一、退款 調用的時候須要證書 證書須要下載
二、退款回調 須要解密 解密代碼 在上面