微信小程序-微信自动退款

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("退款 微信回调返回是否退款成功:否");
}
}
       response.setContentType("text/xml");
       //给微信服务器返回 成功标示 否则会一直询问 咱们服务器 是否回调成功
PrintWriter writer = response.getWriter();
//封装 返回值
StringBuffer buffer = new StringBuffer();
buffer.append("<xml>");
buffer.append("<return_code>SUCCESS</return_code>");
       buffer.append("<return_msg>OK</return_msg>");
       buffer.append("</xml>");
       //返回
       writer.print(buffer.toString()); }     catch (IOException e) {
      e.printStackTrace();
    } catch (Exception e) {
      e.printStackTrace();
    }
   }

5、注意事项

  1、退款 调用的时候需要证书 证书需要下载

  2、退款回调 需要解密 解密代码 在上面

微信小程序-微信自动退款(Java后台)的更多相关文章

  1. 微信小程序图片上传java后台(前后端代码)

    小程序代码 upload:function(e){ var that = this; wx.showActionSheet({ itemList: ['从相册选择','拍照'], itemColor: ...

  2. 微信小程序—微信自动退款

    微信小程序—微信自动退款 一.业务背景 微信自动退款串接基于酷客多小程序商城系统,为方便财务人员进行订单退款而开发,将酷客多小程序系统财务退款流程和微信退款系统打通.实现一个系统管理运营. 二.业务流 ...

  3. 微信小程序腾讯云php后台解决方案

    微信小程序腾讯云php后台解决方案 微信小程序前段需要添加必要的文件以配合后端 (1)wafer2-client-sdk sdk提供了几种接口包括登陆,获取用户openid,图片上传等 (2)conf ...

  4. 微信小程序+微信管理后台+微信用户前台

    代码地址如下:http://www.demodashi.com/demo/15043.html #### 微信小程序+微信管理后台+微信用户前台 #### 产品介绍 基础功能开发:景区微信地图导游.天 ...

  5. 在微信小程序里自动获得当前手机所在的经纬度并转换成地址

    效果:我在手机上打开微信小程序,自动显示出我当前所在的地理位置: 具体步骤: 1. 使用微信jssdk提供的getLocation API拿到经纬度: 2. 调用高德地图的api使用经纬度去换取地址的 ...

  6. 微信小程序支付及退款流程详解

    微信小程序的支付和退款流程 近期在做微信小程序时,涉及到了小程序的支付和退款流程,所以也大概的将这方面的东西看了一个遍,就在这篇博客里总结一下. 首先说明一下,微信小程序支付的主要逻辑集中在后端,前端 ...

  7. 微信小程序开发(request请求后台获取不到data)

    1微信的request的post请求后台获取不到data(当初这个问题纠结了好久好久),原因是post传递的data是json格式而不是key,value的格式,所以获取不到相应的data就是post ...

  8. 微信小程序开发(后端Java)

    微信使用的开发语言和文件很「特殊」. 小程序所使用的程序文件类型大致分为以下几种: ①WXML(WeiXin Mark Language,微信标记语言) ②WXSS(WeiXin Style Shee ...

  9. 微信小程序支付接口之Django后台

    本文链接:https://blog.csdn.net/qq_41860162/article/details/89098694Python3-django-微信小程序支付接口调用工具类生成一系列微信官 ...

随机推荐

  1. Java中的return语句使用总结

    Java中的return语句总是和方法有密切关系,return语句总是用在方法中,有两个作用,一个是返回方法指定类型的值(这个值总是确定的),一个是结束方法的执行(仅仅一个return语句).   在 ...

  2. ORA-02291:parent key not found

    Hibernate operation: Could not execute JDBC batch update; SQL [insert into dchnpricecarchancesource ...

  3. 不要用Serverzoo 提供的CloudLinux 的五大原因 Linode 強大VPS 資源為你解密

    不要用Serverzoo 提供的CloudLinux 的五大原因 Linode 強大VPS 資源為你解密 https://www.williamformosa.com/cloud-linux/

  4. 【bzoj4551】TJOI2016&HEOI2016树

    这题嘛…… 子树询问什么的,直接dfs序线段树无脑写,是吧…… 然后几分钟之内zcy就写出了这样的东西: #include<bits/stdc++.h> #define N 100005 ...

  5. FineReport——JS二次开发(CSS改变控件样式)

    FR提供一些选择器,可以改变部分控件的样式,那么对于如何书写自己的css: 可以自己写css文件导入,也可以在页面加载结束事件中添加JS方法来改变css样式. 以文本框为例: 书写css文件,保存至% ...

  6. leetcode 之Rotate List(18)

    这题我的第一想法是用头插法,但实际上并不好做,因为每次都需要遍历最后一个.更简单的做法是将其连成环,找到相应的位置重新设头结点和尾结点.这过 有很多细节需要注意,比如K有可能是大于链表长度的,如何重新 ...

  7. php设计模式四 ---- 原型模式

    1.简介 用于创建重复的对象,同时又能保证性能.这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式 意图:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象. 主要解决:在运 ...

  8. javascript 实现图片放大镜功能

    淘宝上经常用到的一个功能是利用图片的放大镜功能来查看商品的细节 下面我们来实现这样一个功能吧,原理很简单: 实现一个可以随鼠标移动的虚框 在另外一个块中对应显示虚框中的内容 实现思路: 虚框用css中 ...

  9. BestCoder Round #86 二,三题题解(尺取法)

    第一题太水,跳过了. NanoApe Loves Sequence题目描述:退役狗 NanoApe 滚回去学文化课啦! 在数学课上,NanoApe 心痒痒又玩起了数列.他在纸上随便写了一个长度为 nn ...

  10. Largest Number——STL的深层理解

    Given a list of non negative integers, arrange them such that they form the largest number. For exam ...