SpringBoot微信小程序支付/回调
一、引入依赖
<dependency>
<groupId>org.jdom</groupId>
<artifactId>jdom</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
<version>4.3.9.B</version>
</dependency>
二、配置文件
2.1、WxDataConfigure
@Component
@Slf4j
public class WxDataConfigure {
/**小程序appId*/
public static String appId = "";
/**小程序的密码,此处不能写名称*/
public static String 密码 = "";
/**商户号*/
public static String mch_id = "";
/**商户支付秘钥V2*/
public static String key = "";
/**商户支付秘钥V3*/
public static String keyV3 = "";
/**退款用到*/
public static String certUrl = "";
/**商家转账到零钱*/
public static String pemUrl = "";
public static String privateKeyPath = "";
public static String privateCertPath = "";
public static String sn = "";
/**商户证书序列号*/
public static String serial_no = "";
/**回调通知地址(需内网穿透测试)*/
public static String notify_url = "";
/**交易类型*/
public static String trade_type = "JSAPI";
/**统一下单API接口链接*/
public static String url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
/**查询订单API接口链接*/
public static String query_url = "https://api.mch.weixin.qq.com/pay/orderquery";
/**退款接口*/
public static String refund_url = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";
/**退款回调接口*/
public static String refund_notify_url = "";
/**商家转账到零钱*/
public static String batches_url = "https://api.mch.weixin.qq.com/v3/transfer/batches";
/**
* 预支付
* @return
*/
public static WxPayService unifiedOrderWxPayService() {
log.info("======================初始化微信支付接口服务开始======================");
WxPayConfig payConfig = new WxPayConfig();
payConfig.setAppId(appId);
payConfig.setMchId(mch_id);
payConfig.setMchKey(key);
payConfig.setKeyPath(certUrl);
payConfig.setTradeType(trade_type);
WxPayService wxPayService = new WxPayServiceImpl();
wxPayService.setConfig(payConfig);
log.info("======================初始化微信支付接口服务完成======================");
return wxPayService;
}
/**
* 退款
* @return
*/
public static WxPayService wxPayService() {
//logger.info("======================初始化微信支付接口服务开始======================");
WxPayConfig payConfig = new WxPayConfig();
payConfig.setAppId(appId);
payConfig.setMchId(mch_id);
payConfig.setPrivateKeyPath(privateKeyPath);
payConfig.setNotifyUrl(refund_notify_url);
payConfig.setApiV3Key(keyV3);
payConfig.setPrivateCertPath(privateCertPath);
payConfig.setCertSerialNo(serial_no);
payConfig.setMchKey(key);
payConfig.setKeyPath(certUrl);
payConfig.setTradeType(trade_type);
WxPayService wxPayService = new WxPayServiceImpl();
wxPayService.setConfig(payConfig);
//logger.info("======================初始化微信支付接口服务完成======================");
return wxPayService;
}
}
三、utils工具类
3.1、RandomStringGenerator生成随机数
public class RandomStringGenerator {
/**
* 获取一定长度的随机字符串
*
* @param length 指定字符串长度
* @return 一定长度的字符串
*/
public static String getRandomStringByLength(int length) {
String base = "abcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < length; i++) {
int number = random.nextInt(base.length());
sb.append(base.charAt(number));
}
return sb.toString();
}
}
3.2、Signature 微信签名
public class Signature {
/**
* 签名算法
*
* @param o 要参与签名的数据对象
* @return 签名
* @throws IllegalAccessException
*/
public static String getSign(Object o) throws IllegalAccessException {
ArrayList<String> list = new ArrayList<String>();
Class cls = o.getClass();
Field[] fields = cls.getDeclaredFields();
for (Field f : fields) {
f.setAccessible(true);
if (f.get(o) != null && f.get(o) != "") {
String name = f.getName();
XStreamAlias anno = f.getAnnotation(XStreamAlias.class);
if (anno != null) {
name = anno.value();
}
list.add(name + "=" + f.get(o) + "&");
}
}
int size = list.size();
String[] arrayToSort = list.toArray(new String[size]);
Arrays.sort(arrayToSort, String.CASE_INSENSITIVE_ORDER);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < size; i++) {
sb.append(arrayToSort[i]);
}
String result = sb.toString();
result += "key=" + Configure.key;
System.out.println("签名数据:" + result);
result = MD5.MD5Encode(result, "utf-8").toUpperCase();
return result;
}
public static String getSign(Map<String, Object> map) {
ArrayList<String> list = new ArrayList<String>();
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (entry.getValue() != "") {
list.add(entry.getKey() + "=" + entry.getValue() + "&");
}
}
int size = list.size();
String[] arrayToSort = list.toArray(new String[size]);
Arrays.sort(arrayToSort, String.CASE_INSENSITIVE_ORDER);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < size; i++) {
sb.append(arrayToSort[i]);
}
String result = sb.toString();
result += "key=" + Configure.key;
//Util.log("Sign Before MD5:" + result);
result = MD5.MD5Encode(result, "utf-8").toUpperCase();
//Util.log("Sign Result:" + result);
return result;
}
}
3.3、HttpRequest请求
public class HttpRequest {
//连接超时时间,默认10秒
private static final int socketTimeout = 10000;
//传输超时时间,默认30秒
private static final int connectTimeout = 30000;
/**
* 向指定URL发送GET方法的请求
*
* @param url 发送请求的URL
* @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
* @return URL 所代表远程资源的响应结果
*/
public static String sendGet(String url, String param) {
String result = "";
BufferedReader in = null;
try {
String urlNameString = url + "?" + param;
URL realUrl = new URL(urlNameString);
// 打开和URL之间的连接
URLConnection connection = realUrl.openConnection();
// 设置通用的请求属性
connection.setRequestProperty("accept", "*/*");
connection.setRequestProperty("connection", "Keep-Alive");
connection.setRequestProperty("user-agent",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
// 建立实际的连接
connection.connect();
// 获取所有响应头字段
Map<String, List<String>> map = connection.getHeaderFields();
// 遍历所有的响应头字段
for (String key : map.keySet()) {
System.out.println(key + "--->" + map.get(key));
}
// 定义 BufferedReader输入流来读取URL的响应
in = new BufferedReader(new InputStreamReader(
connection.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
} catch (Exception e) {
System.out.println("发送GET请求出现异常!" + e);
e.printStackTrace();
}
// 使用finally块来关闭输入流
finally {
try {
if (in != null) {
in.close();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
return result;
}
/**
* post请求
*
* @throws IOException
* @throws ClientProtocolException
* @throws NoSuchAlgorithmException
* @throws KeyStoreException
* @throws KeyManagementException
* @throws UnrecoverableKeyException
*/
public static String sendPost(String url, Object xmlObj) throws ClientProtocolException, IOException, UnrecoverableKeyException, KeyManagementException, KeyStoreException, NoSuchAlgorithmException {
HttpPost httpPost = new HttpPost(url);
//解决XStream对出现双下划线的bug
XStream xStreamForRequestPostData = new XStream(new DomDriver("UTF-8", new XmlFriendlyNameCoder("-_", "_")));
xStreamForRequestPostData.alias("xml", xmlObj.getClass());
//将要提交给API的数据对象转换成XML格式数据Post给API
String postDataXML = xStreamForRequestPostData.toXML(xmlObj);
System.out.println(postDataXML);
//得指明使用UTF-8编码,否则到API服务器XML的中文不能被成功识别
StringEntity postEntity = new StringEntity(postDataXML, "UTF-8");
httpPost.addHeader("Content-Type", "text/xml");
httpPost.setEntity(postEntity);
//设置请求器的配置
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout).setConnectTimeout(connectTimeout).build();
httpPost.setConfig(requestConfig);
HttpClient httpClient = HttpClients.createDefault();
HttpResponse response = httpClient.execute(httpPost);
HttpEntity entity = response.getEntity();
String result = EntityUtils.toString(entity, "UTF-8");
return result;
}
/**
* http POST 请求
*
* @param url:请求地址
* @param body: body实体字符串
* @param certPath: 证书路径
* @param password: 证书密码
* @return
*/
public static String httpPostReflect(String url, String body, InputStream certPath, String password) {
String xmlRes = "{}";
HttpClient client = createSSLClientCert(certPath, password);
HttpPost httpost = new HttpPost(url);
try {
//所有请求的body都需采用UTF-8编码
// StringEntity entity = new StringEntity(body,"UTF-8");
// httpost.setEntity(entity);
//支付平台所有的API仅支持JSON格式的请求调用,HTTP请求头Content-Type设为application/json
httpost.addHeader("Connection", "keep-alive");
httpost.addHeader("Accept", "*/*");
httpost.addHeader("Content-Type", "application/json; 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.addHeader("Authorization",Configure.sn + " " + data);
httpost.setEntity(new StringEntity(body, "UTF-8"));
HttpResponse response = client.execute(httpost);
//所有响应也采用UTF-8编码
String result = EntityUtils.toString(response.getEntity(), "UTF-8");
xmlRes = result;
} catch (ClientProtocolException e) {
System.out.println(e);
} catch (UnknownHostException e) {
System.out.println(e);
} catch (IOException e) {
System.out.println(e);
}
return xmlRes;
}
/**
* 创建带证书的实例
*
* @param certPath
* @return
*/
public static CloseableHttpClient createSSLClientCert(InputStream certPath, String password) {
try {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(certPath, password.toCharArray());
certPath.close();
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
//信任所有
public boolean isTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
return true;
}
}).loadKeyMaterial(keyStore, password.toCharArray()).build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, new String[]{"TLSv1", "TLSv1.2"}, null, SSLConnectionSocketFactory.getDefaultHostnameVerifier());
return HttpClients.custom().setSSLSocketFactory(sslsf).build();
} catch (KeyManagementException e) {
System.out.println(e);
} catch (NoSuchAlgorithmException e) {
System.out.println(e);
} catch (KeyStoreException e) {
System.out.println(e);
} catch (FileNotFoundException e) {
System.out.println(e);
} catch (Exception e) {
System.out.println(e);
}
return HttpClients.createDefault();
}
/**
* 自定义证书管理器,信任所有证书
*
* @author pc
*/
public static class MyX509TrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(
java.security.cert.X509Certificate[] arg0, String arg1)
throws CertificateException {
}
@Override
public void checkServerTrusted(
java.security.cert.X509Certificate[] arg0, String arg1)
throws CertificateException {
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
}
}
三、实体类
3.1、OrderReturnInfo 订单返回实体类
@Data
public class OrderReturnInfo {
private String return_code;
private String return_msg;
private String result_code;
private String appid;
private String mch_id;
private String nonce_str;
private String sign;
private String prepay_id;
private String trade_type;
}
四、后台方法
4.1、统一下单
/***
* 测试微信支付,回调等需要内网穿透测试
* @param orderForm
* @return
*/
@ApiOperation(value = "统一下单", tags = "统一下单")
@PostMapping(value = "/order")
public Map<String,String> order(@RequestBody OrderForm orderForm){
log.info("进入统一下单 START...");
String id = UUIDGenerator.generate();
try {
OrderInfo order = new OrderInfo();
order.setAppid(WxDataConfigure.appId);
order.setMch_id(WxDataConfigure.mch_id);
order.setNonce_str(RandomStringGenerator.getRandomStringByLength(32));
//避坑 body如果传中文,签名编码改为utf-8, 不然本地测通,线上提示签名错误
order.setBody("");
order.setOut_trade_no(id);
//单位分
order.setTotal_fee(orderForm.getSumPrice().multiply(new BigDecimal(100)).intValue());
order.setSpbill_create_ip("127.0.0.1");
order.setNotify_url(WxDataConfigure.notify_url);
order.setTrade_type(WxDataConfigure.trade_type);
order.setSign_type("MD5");
//这里直接使用当前用户的openid
order.setOpenid(orderForm.getOpenId());
//生成签名
String sign = Signature.getSign(order);
order.setSign(sign);
String result = HttpRequest.sendPost(WxDataConfigure.url, order);
System.out.println(result);
XStream xStream = new XStream();
xStream.alias("xml", OrderReturnInfo.class);
OrderReturnInfo returnInfo = (OrderReturnInfo) xStream.fromXML(result);
// 二次签名
if ("SUCCESS".equals(returnInfo.getReturn_code()) && returnInfo.getReturn_code().equals(returnInfo.getResult_code())) {
SignInfo signInfo = new SignInfo();
signInfo.setAppId(WxDataConfigure.appId);
long time = System.currentTimeMillis() / 1000;
signInfo.setTimeStamp(String.valueOf(time));
signInfo.setNonceStr(RandomStringGenerator.getRandomStringByLength(32));
signInfo.setRepay_id("prepay_id=" + returnInfo.getPrepay_id());
signInfo.setSignType("MD5");
//生成签名
String sign1 = Signature.getSign(signInfo);
Map<String, String> payInfo = new HashMap<>();
payInfo.put("timeStamp", signInfo.getTimeStamp());
payInfo.put("nonceStr", signInfo.getNonceStr());
payInfo.put("package", signInfo.getRepay_id());
payInfo.put("signType", signInfo.getSignType());
payInfo.put("paySign", sign1);
payInfo.put("id",id);
//订单信息放入redis, 回调再新增
//todo
log.info("统一下单成功 END");
return payInfo;
}
log.info("统一下单失败 END");
HashMap<String, String> map = new HashMap<>();
map.put("code","201");
map.put("msg","统一下单失败!");
return map;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
4.2、回调方法(需内网穿透测试)
配置在WxDataConfigure中notify_url(回调通知地址)里
@PostMapping(value = "/notify")
public void wxNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
log.info("进入微信支付回调.....");
BufferedReader br = new BufferedReader(new InputStreamReader((ServletInputStream) request.getInputStream()));
String line = null;
StringBuilder sb = new StringBuilder();
while ((line = br.readLine()) != null) {
sb.append(line);
}
br.close();
//sb为微信返回的xml
String notityXml = sb.toString();
String resXml = "";
System.out.println("接收到的报文:" + notityXml);
Map map = PayUtil.doXMLParse(notityXml);
String returnCode = (String) map.get("return_code");
if ("SUCCESS".equals(returnCode)) {
//验证签名是否正确
//回调验签时需要去除sign和空值参数
Map<String, String> validParams = PayUtil.paraFilter(map);
//把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串
String validStr = PayUtil.createLinkString(validParams);
//拼装生成服务器端验证的签名
String sign = PayUtil.sign(validStr, WxDataConfigure.key, "utf-8").toUpperCase();
// 因为微信回调会有八次之多,所以当第一次回调成功了,那么我们就不再执行逻辑了
//根据微信官网的介绍,此处不仅对回调的参数进行验签,还需要对返回的金额与系统订单的金额进行比对等
if (sign.equals(map.get("sign"))) {
/** 业务相关逻辑 **/
String id = map.get("out_trade_no").toString();
// TODO 处理业务逻辑
//通知微信服务器已经支付成功
resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
+ "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
} else {
System.out.println("微信支付回调失败!签名不一致");
}
} else {
resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[报文为空]]></return_msg>" + "</xml> ";
}
System.out.println(resXml);
System.out.println("微信支付回调数据结束");
BufferedOutputStream out = new BufferedOutputStream(
response.getOutputStream());
out.write(resXml.getBytes());
out.flush();
out.close();
}
一个在学习的开发者,勿喷,欢迎交流