java 第三方支付

微信、支付宝各种支付退款

Posted by leone on 2018-06-29

java 版微信、支付宝各种支付退款

前言

最近整理了一下自己做过的各种支付退款的业务,并整理如下,只是大致思路代码不保证百分百没有问题但是都是经过我以前实际验证过并投入生产环境的,省略了一些和支付无关的业务流程。

java 微信App支付

  • 参考时序图了解大致流程。

微信App支付文档

  • 大致步骤:

    • 步骤1:用户在商户APP中选择商品,提交订单,选择微信支付。

    • 步骤2:商户后台收到用户支付单,调用微信支付统一下单接口。参见统一下单API

    • 步骤3:统一下单接口返回正常的prepay_id,再按签名规范重新生成签名后,将数据传输给APP。参与签名的字段名为appid,partnerid,prepayid,noncestr,timestamp,package。注意:package的值格式为Sign=WXPay

    • 步骤4:商户APP调起微信支付。api参见本章节app端开发步骤说明

    • 步骤5:商户后台接收支付通知。api参见支付结果通知API

    • 步骤6:商户后台查询支付结果。,api参见查询订单API

  • java 服务端预下单代码如下

    • 预下单业务逻辑
    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
    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
    52
    53
    54
    55
    56
    /**
    * 微信App支付
    *
    * @param request
    * @param orderId
    */
    public Map<String, String> appPay(HttpServletRequest request, Long orderId) {
    Order order = orderService.findOne(orderId);
    if (order.getStatus() != OrderStatusEnum.CREATE.getStatus()) {
    log.error("order status error orderId:{}", orderId);
    return null;
    }
    String spbill_create_ip = AppUtil.getIpAddress(request);
    if (!AppUtil.isIp(spbill_create_ip)) {
    spbill_create_ip = "127.0.0.1";
    }
    String nonce_str = 1 + RandomUtil.getStr(12);
    // 微信app支付十个必须要传入的参数
    Map<String, String> params = new HashMap<>();
    // appId
    params.put("appid", appProperties.getWx().getApp_id());
    // 微信支付商户号
    params.put("mch_id", appProperties.getWx().getMch_id());
    // 随机字符串
    params.put("nonce_str", nonce_str);
    // 商品描述
    params.put("body", "App weChat pay!");
    // 商户订单号
    params.put("out_trade_no", order.getOutTradeNo());
    // 总金额(分)
    params.put("total_fee", order.getTotalFee().toString());
    // 订单生成的机器IP,指用户浏览器端IP
    params.put("spbill_create_ip", spbill_create_ip);
    // 回调url
    params.put("notify_url", appProperties.getWx().getNotify_url());
    // 交易类型:JS_API=公众号支付、NATIVE=扫码支付、APP=app支付
    params.put("trade_type", "APP");
    // 签名
    String sign = AppUtil.createSign(params, appProperties.getWx().getApi_key());
    params.put("sign", "sign");
    String xmlData = AppUtil.mapToXml(params);
    String wxRetXmlData = HttpUtil.sendPostXml(appProperties.getWx().getCreate_order(), xmlData, null);
    Map retData = AppUtil.xmlToMap(wxRetXmlData);
    log.info("微信返回信息:{}", retData);

    // 封装参数返回App端
    Map<String, String> result = new HashMap<>();
    result.put("appid", appProperties.getWx().getApp_id());
    result.put("partnerid", appProperties.getWx().getMch_id());
    result.put("prepayid", retData.get("prepay_id").toString());
    result.put("noncestr", nonce_str);
    result.put("timestamp", RandomUtil.getDateStr(13));
    result.put("package", "Sign=WXPay");
    result.put("sign", AppUtil.createSign(result, appProperties.getWx().getApi_key()));
    return result;
    }
- AppUtil.java

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
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
import lombok.extern.slf4j.Slf4j;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.adapters.HexBinaryAdapter;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.*;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
- 项目常用工具
*
- @author Leone
- @since 2018-05-10
**/
@Slf4j
public class AppUtil {

public static String urlEncoder(String value) {
try {
return URLEncoder.encode(value, StandardCharsets.UTF_8.displayName());
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}

public static String urlDecoder(String value) {
try {
return URLDecoder.decode(value, StandardCharsets.UTF_8.displayName());
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}

/**
- 校验手机号
*
- @param phone
- @return
*/
public static boolean isMobile(String phone) {
Pattern pattern = Pattern.compile("^[1][3,4,5,7,8,9][0-9]{9}$");
Matcher matcher = pattern.matcher(phone);
return matcher.matches();
}

/**
- 匹配ip是否合法
*
- @param ip
- @return
*/
public static Boolean isIp(String ip) {
String re = "([1-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])(\\.(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])){3}";
Pattern pattern = Pattern.compile(re);
Matcher matcher = pattern.matcher(ip);
return matcher.matches();
}


/**
- 支付参数生成签名
*
- @param params
- @param apiKey
- @return
*/
public static String createSign(Map<String, String> params, String apiKey) {
StringBuilder sb = new StringBuilder();
Set<Map.Entry<String, String>> set = params.entrySet();
for (Map.Entry<String, String> entry : set) {
String k = entry.getKey();
Object v = entry.getValue();
if (null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) {
sb.append(k).append("=").append(v).append("&");
}
}
sb.append("key=").append(apiKey);
return MD5(sb.toString()).toUpperCase();
}


/**
- 支付参数生成签名
*
- @param params
- @return
*/
public static String createSign(Map<String, String> params) {
StringBuilder sb = new StringBuilder();
Set<Map.Entry<String, String>> set = params.entrySet();
for (Map.Entry<String, String> entry : set) {
String k = entry.getKey();
Object v = entry.getValue();
}
return MD5(sb.toString()).toUpperCase();
}

/**
- 生成md5摘要
*
- @param content
- @return
*/
public static String MD5(String content) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
messageDigest.update(content.getBytes(StandardCharsets.UTF_8));
byte[] hashCode = messageDigest.digest();
return new HexBinaryAdapter().marshal(hashCode).toLowerCase();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

/**
- 生成 HMAC_SHA256
*
- @param content
- @param api_key
- @return
- @throws Exception
*/
public static String HMAC_SHA256(String content, String api_key) {
try {
KeyGenerator generator = KeyGenerator.getInstance("HmacSHA256");
SecretKey secretKey = generator.generateKey();
byte[] key = secretKey.getEncoded();
SecretKey secretKeySpec = new SecretKeySpec(api_key.getBytes(), "HmacSHA256");
Mac mac = Mac.getInstance(secretKeySpec.getAlgorithm());
mac.init(secretKeySpec);
byte[] digest = mac.doFinal(content.getBytes());
return new HexBinaryAdapter().marshal(digest).toLowerCase();
} catch (Exception e) {
e.printStackTrace();
}
return null;

}


/**
- XML格式字符串转换为Map
*
- @param xmlStr
- @return
*/
public static Map<String, String> xmlToMap(String xmlStr) {
try (InputStream inputStream = new ByteArrayInputStream(xmlStr.getBytes(StandardCharsets.UTF_8))) {
Map<String, String> data = new HashMap<>();
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
Document doc = documentBuilder.parse(inputStream);
doc.getDocumentElement().normalize();
NodeList nodeList = doc.getDocumentElement().getChildNodes();
for (int idx = 0; idx < nodeList.getLength(); ++idx) {
Node node = nodeList.item(idx);
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element element = (Element) node;
data.put(element.getNodeName(), element.getTextContent());
}
}
return data;
} catch (Exception ex) {
log.warn("xml convert to map failed message: {}", ex.getMessage());
return null;
}
}

/**
- map转换为xml格式
*
- @param params
- @return
*/
public static String mapToXml(Map<String, String> params) {
StringBuilder sb = new StringBuilder();
Set<Map.Entry<String, String>> es = params.entrySet();
Iterator<Map.Entry<String, String>> it = es.iterator();
sb.append("<xml>");
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
String k = entry.getKey();
Object v = entry.getValue();
sb.append("<").append(k).append(">").append(v).append("</").append(k).append(">");
}
sb.append("</xml>");
return sb.toString();
}


public static void main(String[] args) throws Exception {
System.out.println(MD5("hello"));
String s = null;
System.out.println(Objects.nonNull(s));

}

/**
- 获得request的ip
*
- @param request
- @return
*/
public static String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (Objects.nonNull(ip) && !"unKnown".equalsIgnoreCase(ip)) {
//多次反向代理后会有多个ip值,第一个ip才是真实ip
int index = ip.indexOf(",");
if (index != -1) {
return ip.substring(0, index);
} else {
return ip;
}
}
ip = request.getHeader("X-Real-IP");
if (Objects.nonNull(ip) && !"unKnown".equalsIgnoreCase(ip)) {
return ip;
}
return request.getRemoteAddr();
}

/**
- 过滤掉关键参数
*
- @param param
- @return
*/
public static HashMap<String, String> paramFilter(Map<String, String> param) {
HashMap<String, String> result = new HashMap<>();
if (param == null || param.size() <= 0) {
return result;
}
for (String key : param.keySet()) {
String value = param.get(key);
if (value == null || value.equals("") || key.equalsIgnoreCase("sign") || key.equalsIgnoreCase("sign_type")) {
continue;
}
result.put(key, value);
}
return result;
}

/**
- 把Request中的数据解析为xml
*
- @param request
- @return
*/
public static String requestDataToXml(HttpServletRequest request) {
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(request.getInputStream()))) {
String line;
StringBuilder sb = new StringBuilder();
while ((line = bufferedReader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

/**
- xml 转换 bean
*
- @param clazz
- @param xml
- @param <T>
- @return
*/
public static <T> T xmlToBean(Class<T> clazz, String xml) {
try {
JAXBContext context = JAXBContext.newInstance(clazz);
Unmarshaller unmarshaller = context.createUnmarshaller();
return (T) unmarshaller.unmarshal(new StringReader(xml));
} catch (JAXBException e) {
e.printStackTrace();
}
return null;
}


}
- App端的到json格式的数据调用本地微信App,json格式如下
1
2
3
4
5
6
7
8
9
{
"appId":"wxxxx",
"partnerid":"xxxx",
"noncestr":"f7382ae04f15cf4e5fd5fbecf342",
"prepayid":"xxxx",
"timeStamp":"20180906095441"
"package":"Sign=WXPay",
"sign":"AE3E21CCB1DF50B65A0531000E9EF788"
}

App端用户支付成功后微信会发起异步回调,回调url是我们发起支付时设置的url我们在回调业务中做对应的保存流水等业务并向微信响应收到异步通知不然微信会一直调用异步通知方法会使流水信息保存多次等情况。

  • 支付回调
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 微信支付回调
*
* @param request
* @param response
* @throws IOException
*/
public void notifyOrder(HttpServletRequest request, HttpServletResponse response) throws IOException {
String resXml = "<xml><return_code><![CDATA[SUCCESS]]>" +
"</return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
out.write(resXml.getBytes());
out.flush();
out.close();
log.info("wx notify success");
// 保存流水信息
}

待完善。。。

java 微信小程序支付

  • 小程序支付开发步骤参见官方文档

  • 开发前你需要有微信公众平台的账号、已注册好的小程序、微信商户平台等信息

  • 大致步骤
    和App支付差不多,后台向微信发起预下单,需要appid、商户号、api_key、等一些必要的参数调用微信的统一下单接口返回对应的信息,然后我们需要自己从微信那边返回的信息中拿到prepay_id这个字段然后封装一些其他的信息如appid、时间戳、随机字符串、paySign等返回到前端,前端拿到这些参数调用微信App的支付,当用户支付成功后微信会发起异步回调,然后后台收到回调向微信响应回调成功。

  • java服务端业务逻辑

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
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
/**
* 微信小程序支付
*
* @param orderId
* @param request
*/
public Map xcxPay(Long orderId, HttpServletRequest request) {
Order order = orderService.findOne(orderId);
User user = userService.findOne(order.getUserId());
String nonce_str = RandomUtil.getNum(12);
String outTradeNo = 1 + RandomUtil.getNum(11);
String spbill_create_ip = AppUtil.getIpAddress(request);
if (!AppUtil.isIp(spbill_create_ip)) {
spbill_create_ip = "127.0.0.1";
}
// 小程序支付需要参数
SortedMap<String, String> reqMap = new TreeMap<>();
reqMap.put("appid", appProperties.getWx().getApp_id());
reqMap.put("mch_id", appProperties.getWx().getMch_id());
reqMap.put("nonce_str", nonce_str);
reqMap.put("body", "小程序支付");
reqMap.put("out_trade_no", outTradeNo);
reqMap.put("total_fee", order.getTotalFee().toString());
reqMap.put("spbill_create_ip", spbill_create_ip);
reqMap.put("notify_url", appProperties.getWx().getNotify_url());
reqMap.put("trade_type", appProperties.getWx().getTrade_type());
reqMap.put("openid", user.getOpenid());
String sign = AppUtil.createSign(reqMap, appProperties.getWx().getApi_key());
reqMap.put("sign", sign);
String xml = AppUtil.mapToXml(reqMap);
String result = HttpUtil.sendPostXml(appProperties.getWx().getCreate_order(), xml, null);
Map<String, String> resData = AppUtil.xmlToMap(result);
log.info("resData:{}", resData);
if ("SUCCESS".equals(resData.get("return_code"))) {
Map<String, String> resultMap = new LinkedHashMap<>();
//返回的预付单信息
String prepay_id = resData.get("prepay_id");
resultMap.put("appId", appProperties.getWx().getApp_id());
resultMap.put("nonceStr", nonce_str);
resultMap.put("package", "prepay_id=" + prepay_id);
resultMap.put("signType", "MD5");
resultMap.put("timeStamp", RandomUtil.getDateStr(14));
String paySign = AppUtil.createSign(resultMap, appProperties.getWx().getApi_key());
resultMap.put("paySign", paySign);
log.info("return data:{}", resultMap);
return resultMap;
} else {
throw new ValidateException(ExceptionMessage.WEI_XIN_PAY_FAIL);
}

}

小程序端用户支付成功后微信会发起异步回调,回调url是我们发起支付时设置的url我们在回调业务中做对应的保存流水等业务并向微信响应收到异步通知不然微信会一直调用异步通知方法会使流水信息保存多次等情况。

  • 支付回调
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 微信支付回调
*
* @param request
* @param response
* @throws IOException
*/
public void notifyOrder(HttpServletRequest request, HttpServletResponse response) throws IOException {
String resXml = "<xml><return_code><![CDATA[SUCCESS]]>" +
"</return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
out.write(resXml.getBytes());
out.flush();
out.close();
log.info("wx notify success");
// 保存流水信息
}

java 微信扫码支付

这里我们参考模式二,模式二与模式一相比,流程更为简单,不依赖设置的回调支付URL。商户后台系统先调用微信支付的统一下单接口,微信后台系统返回链接参数code_url,商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付。注意:code_url有效期为2小时,过期后扫码不能再发起支付。

  • 大致流程

    • (1)商户后台系统根据用户选购的商品生成订单。

    • (2)用户确认支付后调用微信支付统一下单API生成预支付交易;

    • (3)微信支付系统收到请求后生成预支付交易单,并返回交易会话的二维码链接code_url。

    • (4)商户后台系统根据返回的code_url生成二维码。

    • (5)用户打开微信“扫一扫”扫描二维码,微信客户端将扫码内容发送到微信支付系统。

    • (6)微信支付系统收到客户端请求,验证链接有效性后发起用户支付,要求用户授权。

    • (7)用户在微信客户端输入密码,确认支付后,微信客户端提交授权。

    • (8)微信支付系统根据用户授权完成支付交易。

    • (9)微信支付系统完成支付交易后给微信客户端返回交易结果,并将交易结果通过短信、微信消息提示用户。微信客户端展示支付交易结果页面。

    • (10)微信支付系统通过发送异步消息通知商户后台系统支付结果。商户后台系统需回复接收情况,通知微信后台系统不再发送该单的支付通知。

    • (11)未收到支付通知的情况,商户后台系统调用查询订单API

    • (12)商户确认订单已支付后给用户发货。

  • 后台发起支付业务方法

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 微信扫码支付传入金额为分
*
* @param totalFee
* @param response
* @param request
* @return
* @throws Exception
*/
public boolean qrCodePay(String totalFee, HttpServletResponse response,
HttpServletRequest request) {
String nonce_str = RandomUtil.getStr(12);
String outTradeNo = 1 + RandomUtil.getNum(11);
String spbill_create_ip = AppUtil.getIpAddress(request);
if (!AppUtil.isIp(spbill_create_ip)) {
spbill_create_ip = "127.0.0.1";
}
Map<String, String> params = new TreeMap<>();
params.put("appid", appProperties.getWx().getApp_id());
params.put("mch_id", appProperties.getWx().getMch_id());
params.put("nonce_str", nonce_str);
params.put("body", "微信扫码支付");
params.put("out_trade_no", outTradeNo);
params.put("total_fee", totalFee);
params.put("spbill_create_ip", spbill_create_ip);
params.put("notify_url", appProperties.getWx().getRefund_url());
params.put("trade_type", "NATIVE");
String sign = AppUtil.createSign(params, appProperties.getWx().getApi_key());
params.put("sign", sign);
String requestXml = AppUtil.mapToXml(params);
String responseXml = HttpUtil.sendPostXml(appProperties.getWx().getCreate_order(), requestXml, null);
Map<String, String> respMap = AppUtil.xmlToMap(responseXml);
//return_code为微信返回的状态码,SUCCESS表示成功,return_msg 如非空,为错误原因 签名失败 参数格式校验错误
if ("SUCCESS".equals(respMap.get("return_code")) && "SUCCESS".equals(respMap.get("result_code"))) {
log.info("wx pre pay success response:{}", respMap);
// 二维码中需要包含微信返回的信息
ImageCodeUtil.createQRCode(respMap.get("code_url"), response);
// 保存订单信息
return true;
}
log.error("wx pre pay error response:{}", respMap);
return false;
}
  • 创建二维码方法
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
26
27
/**
* 生成二维码并响应到浏览器
*
* @param content
* @param response
*/
public static void createQRCode(String content, HttpServletResponse response) {
int width = 300, height = 300;
String format = "png";
Map<EncodeHintType, Object> hashMap = new HashMap<>();
hashMap.put(EncodeHintType.CHARACTER_SET, StandardCharsets.UTF_8);
hashMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
hashMap.put(EncodeHintType.MARGIN, 1);
try {
response.setHeader("Cache-control", "no-cache");
response.setHeader("Pragma", "no-cache");
response.setHeader("content-type", "image/png");
response.setCharacterEncoding(StandardCharsets.UTF_8.displayName());
response.setDateHeader("Expires", 0);
BitMatrix bitMatrix = new MultiFormatWriter()
.encode(content, BarcodeFormat.QR_CODE, width, height, hashMap);
BufferedImage img = MatrixToImageWriter.toBufferedImage(bitMatrix);
ImageIO.write(img, format, response.getOutputStream());
} catch (Exception e) {
log.warn("create QRCode error message:{}", e.getMessage());
}
}

生成二维码用户扫码支付成功后微信会发起异步调用我们发起支付时设置的回调url我们在回调业务中做对应的保存流水等业务并向微信响应收到异步通知不然微信会一直调用异步通知方法会使流水信息保存多次等情况

  • 支付回调
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 微信支付回调
*
* @param request
* @param response
* @throws IOException
*/
public void notifyOrder(HttpServletRequest request, HttpServletResponse response) throws IOException {
String resXml = "<xml><return_code><![CDATA[SUCCESS]]>" +
"</return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
out.write(resXml.getBytes());
out.flush();
out.close();
log.info("wx notify success");
// 保存流水信息
}

java 微信退款

  • 场景介绍

当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家,微信支付将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。

  • 注意:

    • 1、交易时间超过一年的订单无法提交退款;

    • 2、微信支付退款支持单笔交易分多次退款,多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。申请退款总金额不能超过订单金额。 一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号。

    • 3、请求频率限制:150qps,即每秒钟正常的申请退款请求次数不超过150次错误或无效请求频率限制:6qps,即每秒钟异常或错误的退款申请请求不超过6次

    • 4、每个支付订单的部分退款次数不能超过50次

    • 5、微信退款需要到微信商户平台下载证书,并配合证书一起使用。

  • java服务端代码
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* 微信退款
*
* @param orderId
* @return
* @throws Exception
*/
public boolean wxRefund(Long orderId) throws Exception {
String nonceStr = RandomUtil.getStr(12);
String out_refund_no = RandomUtil.getStr(12);
Order order = orderService.findOne(orderId);

SortedMap<String, String> params = new TreeMap<>();
// 公众账号ID
params.put("appid", appProperties.getWx().getApp_id());
// 商户号
params.put("mch_id", appProperties.getWx().getMch_id());
// 随机字符串
params.put("nonce_str", nonceStr);
// 商户订单号
params.put("out_trade_no", order.getOutTradeNo());
// 订单金额
params.put("total_fee", order.getTotalFee().toString());
// 商户退款单号
params.put("out_refund_no", out_refund_no);
// 退款原因
params.put("refund_fee", order.getTotalFee().toString());
// 退款结果通知url
params.put("notify_url", appProperties.getWx().getRefund_notify_url());
// 签名
params.put("sign", AppUtil.createSign(params, appProperties.getWx().getApi_key()));
String data = AppUtil.mapToXml(params);

CloseableHttpClient httpClient = HttpUtil.sslHttpsClient(appProperties.getWx().getCertificate_path(), appProperties.getWx().getApi_key());
String xmlResponse = HttpUtil.sendSslXmlPost(appProperties.getWx().getRefund_url(), data, null, httpClient);
Map<String, String> mapData = AppUtil.xmlToMap(xmlResponse);
// return_code为微信返回的状态码,SUCCESS表示申请退款成功,return_msg 如非空,为错误原因 签名失败 参数格式校验错误
if ("SUCCESS".equalsIgnoreCase(mapData.get("return_code"))) {
log.info("wx refund success response:{}", mapData);
// 修改订单状态为退款保存退款订单等操作

return true;
}
log.error("wx refund error response:{}", mapData);
return false;
}
  • 最终封装好的参数如下
1
2
3
4
5
6
7
8
9
10
11
<xml>
<appid>wx2421b1c4370ec43b</appid>
<mch_id>10000100</mch_id>
<nonce_str>6cefdb308e1e2e8aabd48cf79e546a02</nonce_str>
<out_refund_no>1415701182</out_refund_no>
<out_trade_no>1415757673</out_trade_no>
<refund_fee>1</refund_fee>
<total_fee>1</total_fee>
<transaction_id>4006252001201705123297353072</transaction_id>
<sign>FE56DD4AA85C0EECA82C35595A69E153</sign>
</xml>
  • HttpUtil.java
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
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204

import lombok.extern.slf4j.Slf4j;
import org.apache.http.*;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HttpContext;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;

import javax.net.ssl.SSLContext;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* http请求工具类
*
* @author Leone
**/
@Slf4j
public class HttpUtil {

private HttpUtil() {
}

private final static String UTF8 = StandardCharsets.UTF_8.displayName();

private static CloseableHttpClient httpClient;

static {
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(3000).setConnectionRequestTimeout(1000).setSocketTimeout(4000).setExpectContinueEnabled(true).build();
PoolingHttpClientConnectionManager pool = new PoolingHttpClientConnectionManager();
pool.setMaxTotal(300);
pool.setDefaultMaxPerRoute(50);
HttpRequestRetryHandler retryHandler = (IOException exception, int executionCount, HttpContext context) -> {
if (executionCount > 1) {
return false;
}
if (exception instanceof NoHttpResponseException) {
log.info("[NoHttpResponseException has retry request:" + context.toString() + "][executionCount:" + executionCount + "]");
return true;
} else if (exception instanceof SocketException) {
log.info("[SocketException has retry request:" + context.toString() + "][executionCount:" + executionCount + "]");
return true;
}
return false;
};
httpClient = HttpClients.custom().setConnectionManager(pool).setDefaultRequestConfig(requestConfig).setRetryHandler(retryHandler).build();
}

/**
* @param certPath
* @param password
* @return
* @throws Exception
*/
public static CloseableHttpClient sslHttpsClient(String certPath, String password) throws Exception {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
try (InputStream inputStream = new FileInputStream(new File(certPath))) {
keyStore.load(inputStream, password.toCharArray());
}
SSLContext sslContext = SSLContexts.custom().loadKeyMaterial(keyStore, password.toCharArray()).build();
SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, new String[]{"TLSv1"}, null, SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
return HttpClients.custom().setSSLSocketFactory(sslConnectionSocketFactory).build();
}


/**
* 设置请求头信息
*
* @param headers
* @param request
* @return
*/
private static void setHeaders(Map<String, Object> headers, HttpRequest request) {
if (null != headers && headers.size() > 0) {
for (Map.Entry<String, Object> entry : headers.entrySet()) {
request.addHeader(entry.getKey(), entry.getValue().toString());
}
}
}

/**
* 发送post请求请求体为xml
*
* @param url
* @param xml
* @param headers
* @return
*/
public static String sendPostXml(String url, String xml, Map<String, Object> headers) {
String result = null;
try {
HttpPost httpPost = new HttpPost(url);
setHeaders(headers, httpPost);
StringEntity entity = new StringEntity(xml, StandardCharsets.UTF_8);
httpPost.addHeader("Content-Type", "text/xml");
httpPost.setEntity(entity);
HttpResponse response = httpClient.execute(httpPost);
HttpEntity responseData = response.getEntity();
result = EntityUtils.toString(responseData, StandardCharsets.UTF_8);
} catch (IOException e) {
e.printStackTrace();
}
return result;
}

/**
* 发送json请求
*
* @param url
* @param json
* @return
*/
public static String sendPostJson(String url, String json, Map<String, Object> headers) {
String result = null;
try {
HttpPost httpPost = new HttpPost(url);
setHeaders(headers, httpPost);
StringEntity stringEntity = new StringEntity(json, StandardCharsets.UTF_8);
stringEntity.setContentType("application/json");
httpPost.setEntity(stringEntity);
HttpResponse response = httpClient.execute(httpPost);
HttpEntity responseData = response.getEntity();
result = EntityUtils.toString(responseData, StandardCharsets.UTF_8);
} catch (IOException e) {
e.printStackTrace();
}
return result;
}

/**
* 发送get请求
*
* @param url
* @param params
* @param header
* @return
*/
public static String sendGet(String url, Map<String, Object> params, Map<String, Object> header) {
String result = null;
try {
URIBuilder builder = new URIBuilder(url);
if (params != null && params.size() > 0) {
List<NameValuePair> pairs = new ArrayList<>();
for (Map.Entry<String, Object> entry : params.entrySet()) {
pairs.add(new BasicNameValuePair(entry.getKey(), entry.getValue().toString()));
}
builder.setParameters(pairs);
}
HttpGet httpGet = new HttpGet(builder.build());
setHeaders(header, httpGet);
HttpResponse response = httpClient.execute(httpGet);
result = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
} catch (Exception e) {
e.printStackTrace();
}
return result;
}


/**
* 发送get请求
*
* @param url
* @param xml
* @param headers
* @return
*/
public static String sendSslXmlPost(String url, String xml, Map<String, Object> headers, CloseableHttpClient httpClient) {
String result = null;
try {
HttpPost httpPost = new HttpPost(url);
setHeaders(headers, httpPost);
StringEntity entity = new StringEntity(xml, StandardCharsets.UTF_8);
httpPost.addHeader("Content-Type", "text/xml");
httpPost.setEntity(entity);
HttpResponse response = httpClient.execute(httpPost);
HttpEntity responseData = response.getEntity();
result = EntityUtils.toString(responseData, StandardCharsets.UTF_8);
} catch (Exception e) {
e.printStackTrace();
}
return result;
}

}

java 支付宝App支付

// 待完善

github:https://github.com/janlle/java-pay