微信小程序-微信自动退款
1、首先分享
微信自动退款接口:
https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4
微信付款 代码案例 (很多共同的代码 都在付款逻辑里面)
https://www.cnblogs.com/yi1036943655/p/7211275.html
2、小程序端代码 栗子
//获取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) { }
2、接口端代码 栗子
@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;
3、Http请求 代码(这块的代码逻辑和付款的是不一样的)
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(); } }
4、退款结果通知 后台代码 栗子
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(); } }
5、注意事项
1、退款 调用的时候需要证书 证书需要下载
2、退款回调 需要解密 解密代码 在上面