Java接入微信native、jsapi支付
一、说明
本文示例使用的微信支付版本为V2版本,是旧版本,旧版本与新版本的接口不一,并不通用。
微信官方接口文档地址:https://pay.weixin.qq.com/wiki/doc/api/index.html
二、微信支付所需要的参数
appid、mchid、apikey
这些参数,需要公司去微信官方平台进行申请才行,通常来说,作为程序员不用操心。我没有申请过,具体的流程我就不做说明了。
申请地址:https://pay.weixin.qq.com/index.php/apply/applyment_home/guide_normal#none
以上参数我放到了nacos中,放入配置文件中也行,使用时直接注入就可以了。
三、引入微信依赖
<dependency>
<groupId>com.github.liyiorg</groupId>
<artifactId>weixin-popular</artifactId>
<version>2.8.16</version>
</dependency>
四、开发注意事项
- 微信旧版本的请求格式为xml格式,请求前需要进行转换。
- 回调接口的的数据在请求流中,需要将流中的数据读取出转换成map。
- 旧版本的小程序的支付与网页端的支付,几乎一样,只有两个参数不同:
- openid:网页支付(native)时不需要这个请求参数,小程序支付(JSAPI)时,需要将微信用户的openid加入到请求参数中去。
- appid:网页支付时就是所申请的appid,小程序支付时,为申请的小程序appid
- 做小程序支付时,需要给前端返回签名信息,生成签名时的map中的"appId"的"I"为大写,请求微信接口的时候为小写,此处一定要注意,否则,在微信小程序支付时会验签失败。
- 微信支付的回调地址必须是可以通过外网访问的地址,需要在线下测试时,可以用内网穿透做到自己本机的代码可以被外网访问,具体怎么操作后续会写一篇博客来说明。
五、微信支付流程说明
我只做过native支付和小程序支付(JSAPI),所以此处就只写这两种支付流程。
微信官方说明的比较详细,我写的流程说明我感觉比较通俗易懂一些。大家可以结合着理解。
1.native支付
- 前提,已经生成订单。
- 前端根据订单号、金额等参数(其他参数看具体的业务需要)请求后端的支付接口。
- 后端支付接口封装数据进行请求微信官方的接口。
- 微信接口验证成功,返回相关数据。
- 把微信接口返回数据中的“code_url”返回给前端。
- 前端拿着二维码链接进行展示。
- 用户扫码付款。
- 付款成功后,微信会根据回调地址通知后端系统。
- 后端系统判断付款是否成功,然后做相应的逻辑处理就行。
附一张微信官方流程图:
2.小程序支付(JSAPI)
- 前提:订单生成。
- 前端根据订单号、金额、openid等参数(其他参数看具体的业务需要)请求后端的支付接口。
- 后端封装请求需要的参数,请求微信的接口。
- 根据微信接口返回数据中的prepay_id进行生成签名,并将前端需要的参数进行封装返回。
- 前端拿着后端返回的数据,请求微信的接口,调起微信支付。
- 用户支付。
- 支付成功,微信根据回调地址通知后端系统。
- 判断是否支付成功,并做出相应的逻辑处理。
附一张微信官方流程图:
六、代码:
1.Service层代码
以下代码为业务层实现类代码,直接在控制层调用,然后返回就行,我在控制层没有做逻辑处理。
/**
* 公众账号id
*/
@Value("${wechat.cert.appid}")
private String appid;
/**
* 小程序id
*/
@Value("${wechat.cert.appletid}")
private String appletid;
/**
* 小程序秘钥
*/
@Value("${wechat.cert.appletSecret}")
private String appletSecret;
/**
* 商户号
*/
@Value("${wechat.cert.mchid}")
private String mchid;
/**
* 商户key
*/
@Value("${wechat.cert.apikey}")
private String apikey;
/**
* 回调地址
*/
@Value("${wechat.pay.notifyUrl}")
private String notifyUrl;
/**
* 微信支付的请求地址
*/
private static final String unifiedOrderUrl = "https://api.mch.weixin.qq.com/pay/unifiedorder";
/**
* 微信支付订单的查询接口地址
*/
private static final String orderQueryUrl = "https://api.mch.weixin.qq.com/pay/orderquery";
/**
* 关闭微信支付订单的接口地址
*/
private static final String closeOrder = "https://api.mch.weixin.qq.com/pay/closeorder";
/**
* 成功标识
*/
private static final String SUCCESS = "SUCCESS";
/**
* 远程调用shop模块接口
*/
@Resource
private RemoteShopService remoteShopService;
/**
* 支付共用业务
*/
@Resource
private PaymentCommon paymentCommon;
/**
* redis封装工具类
*/
@Resource
private RedisService redisService;
/**
* 微信(native、jsapi)付款,这里加入分布式锁来进行幂等性校验,防止用户多次点击支付按钮
* @param payInfo 支付信息
* @return String 支付二维码url,小程序支付时是prepay_id,有效期两个小时。
*/
@Override
public Object payment(PayInfo payInfo) {
// 参数校验,校验当前系统支付时所需要的参数信息
paymentCommon.checkParam(payInfo);
// 获取订单号
String outTradeNo = payInfo.getOutTradeNo();
log.info("用户:" + "请求微信支付!" + "订单号:" + outTradeNo);
try {
// 获取锁
if (redisService.getCacheObject(CacheConstants.PAYMENT_LOCK + outTradeNo) == null) {
// 加锁
redisService.setCacheObject(CacheConstants.PAYMENT_LOCK + outTradeNo, 1, 5L, TimeUnit.SECONDS);
// 校验订单状态
paymentCommon.chechStatus(payInfo);
// 封装请求微信接口的公共参数
Map<String, String> map = this.packPublicParam(outTradeNo);
/*
封装付款接口独有参数
*/
map.put("body", subject);
// 微信支付的金额单位为分
double totalFee = Double.parseDouble(payInfo.getTotalAmount()) * 100;
map.put("total_fee", (int)totalFee + "");
map.put("spbill_create_ip", IpUtils.getHostIp());
map.put("notify_url", notifyUrl);
map.put("trade_type", paymentCommon.getTradeType(payInfo.getMethod()));
// 设置起止时间
Date timeStart = new Date();
Date timeExpire = org.apache.commons.lang3.time.DateUtils.addMinutes(timeStart, 5);
map.put("time_start", DateUtils.parseDateToStr(DateUtils.YYYYMMDDHHMMSS, timeStart));
map.put("time_expire", DateUtils.parseDateToStr(DateUtils.YYYYMMDDHHMMSS, timeExpire));
// 初始化返回值,微信小程序支付与网页支付前端需要的数据不同,所以定义一个Object类型。
Object result;
if (!StringUtils.isEmpty(payInfo.getUserid())) {
// 用户id不为空,为jsapi(小程序支付),返回前端调用微信接口时的参数信息
map.put("openid", payInfo.getUserid());
map.put("appid", appletid);
// 请求微信接口,获取返回值
Map<String, String> wxAddress = this.getWxAddress(unifiedOrderUrl, map);
result = this.sign(wxAddress.get("prepay_id"));
} else {
// 用户id为空,则为native支付,返回codeUrl,
result = this.getWxAddress(unifiedOrderUrl, map).get("code_url");
}
// 添加支付流水信息
paymentCommon.packPayInfo(payInfo);
log.info("用户:" + payInfo.getUserid() + "请求微信支付成功!");
return result;
}
} finally {
// 释放锁
redisService.deleteObject(CacheConstants.PAYMENT_LOCK + outTradeNo);
}
return null;
}
/**
* 查询微信订单
* @param ordersn 订单编号
* @return Map<String, String>
*/
@Override
public Map<String, String> orderQuery(String ordersn) {
//封装参数发起请求
return this.getWxAddress(orderQueryUrl, this.packPublicParam(ordersn));
}
/**
* 关闭微信支付订单
*
* @param ordersn 订单编号
* @return Map<String, String>
*/
@Override
public Map<String, String> closeOrder(String ordersn) {
return this.getWxAddress(closeOrder, this.packPublicParam(ordersn));
}
/**
* 微信支付后的回调方法
*
* @param request 请求流
* @return String 回调后的信息,返回给微信官方
*/
@Override
public String callback(HttpServletRequest request) throws Exception {
// 将微信请求的流信息转换成map集合
Map<String, String> map = this.getMap(request);
// 初始化返回码与返回信息
String code = "SUCCESS", msg = "OK";
String orderId = map.get("out_trade_no");
log.info("用户" + map.get("openid") + "微信支付后开始修改订单状态,订单编号是:" + orderId);
try {
if (redisService.getCacheObject(CacheConstants.PAY_CALLBACK_LOCK + orderId) == null) {
// 异步加锁,5秒过期
redisService.setCacheObject(CacheConstants.PAY_CALLBACK_LOCK + orderId, 1, 5L, TimeUnit.SECONDS);
// 做请求幂等性处理,校验订单的状态
String status = remoteShopService.getOrderStatusById(orderId);
if (!"待发货".equals(status)) {
// 判断用户是否支付成功
if (map.get("return_code").equals(SUCCESS) && map.get("result_code").equals(SUCCESS)) {
// 用户支付成功,修改订单状态s
if (remoteShopService.updateOrderStatus(orderId, 2) != 1) {
if (remoteShopService.updateOrderStatus(orderId, 2) != 1) {
// 两次调用失败,记录日志,抛出异常
log.error("用户" + map.get("openid") + "微信支付后修改订单状态失败,订单编号是:" + orderId);
}
}
// 查询支付流水信息,进行信息匹配
PayInfo payInfo = remoteShopService.getPayInfoByOutTradeNo(orderId);
assert payInfo != null;
if (!payInfo.getTotalAmount().equals(map.get("total_fee"))) {
// 支付信息不匹配
code = "FAIL";
msg = "支付信息不匹配";
} else {
// 支付信息匹配,修改支付流水信息
payInfo.setTradeNo(map.get("transaction_id"));
if (remoteShopService.updatePayInfo(JSONObject.toJSONString(payInfo))!=1) {
if (remoteShopService.updatePayInfo(JSONObject.toJSONString(payInfo))!=1) {
code = "FAIL";
msg = "修改用户支付信息失败";
log.error("用户" + map.get("openid") + "微信支付后修改支付信息失败,订单编号是:" + orderId);
}
}
}
} else {
// 用户付款失败,将失败信息记入日志
log.error("用户" + map.get("openid") + "微信支付失败----" +
map.get("err_code") + ":" + map.get("err_code_des"));
code = "FAIL";
msg = "用户付款失败";
}
}
}
} finally {
// 释放锁
redisService.deleteObject(CacheConstants.PAY_CALLBACK_LOCK + orderId);
}
// 封装返回参数,并返回给微信官方
HashMap<String, String> hashMap = new HashMap<>();
hashMap.put("return_code", code);
hashMap.put("return_msg", msg);
return WXPayUtil.mapToXml(hashMap);
}
/*===================================================私有方法===================================================*/
/**
* 封装公共的请求微信接口参数
* @param ordersn 订单编号
* @return Map<String, String> 封装请求微信接口的公共参数后的map集合
*/
private Map<String, String> packPublicParam(String ordersn){
// 初始化封装请求参数
HashMap<String, String> map = new HashMap<>();
// 封装请求微信接口的参数
map.put("appid", appid);
map.put("mch_id", mchid);
map.put("nonce_str", WXPayUtil.generateNonceStr());
map.put("out_trade_no", ordersn);
return map;
}
/**
* 请求微信接口
* @param url 请求的地址
* @param param 请求参数
* @return Map<String, String> 请求微信接口后,微信返回的数据
*/
private Map<String, String> getWxAddress(String url, Map<String, String> param){
try {
// 将参数转换为xml
String s = WXPayUtil.generateSignedXml(param, apikey);
// 请求微信地址
HttpClient httpClient = new HttpClient(url);
httpClient.setXmlParam(s);
httpClient.post();
// 获取请求结果:结果为xml格式,需转换为map格式
String content = httpClient.getContent();
Map<String, String> map = WXPayUtil.xmlToMap(content);
if (map.get("return_code").equals(SUCCESS) && map.get("result_code").equals(SUCCESS)) {
// 请求成功,将请求结果返回
return map;
} else {
String s1 = "";
switch (url) {
case unifiedOrderUrl:
s1 = "微信支付";
break;
case orderQueryUrl:
s1 = "微信支付的订单查询";
break;
case closeOrder:
s1 = "关闭微信支付订单";
break;
}
//请求失败,将失败信息记入日志
log.error("请求微信接口失败(" + s1 + "----)" +
map.get("err_code") +
":" +
map.get("err_code_des"));
throw new BaseException("请求微信支付失败,请刷新重试");
}
} catch (Exception e) {
e.printStackTrace();
throw new BaseException("请求微信支付失败,请刷新重试");
}
}
/**
* 将微信请求的流信息转换成map集合
* @param request 请求流
* @return Map<String, String>
*/
private Map<String, String> getMap(HttpServletRequest request) throws Exception {
// 获取输入流,微信的回调信息是通过信息流的方式传输的
ServletInputStream inputStream = request.getInputStream();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// 定义缓冲区
byte[] bytes = new byte[1024];
int len = 0;
while ((len = inputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, len);
}
// 从输出流获取数据,转换为map
String string = outputStream.toString();
return WXPayUtil.xmlToMap(string);
}
/**
* 计算签名值,在微信小程序支付时用到,计算后,返回前端
* @param prepayId 预支付交易会话标识
* @return 签名值
*/
private Map<String, String> sign(String prepayId) {
TreeMap<String, String> map = new TreeMap<>(String::compareTo);
// 注意,此处的appId的“I”为大写,在请求微信接口的时候是小写,注意区分,否则会验签失败!
map.put("appId", appletid);
map.put("timeStamp", System.currentTimeMillis() / 1000 +"");
map.put("nonceStr", WXPayUtil.generateNonceStr());
map.put("package", "prepay_id=" + prepayId);
map.put("signType", "MD5");
try {
// 调用微信的工具类方法,生成签名
map.put("paySign", generateSignature(map, apikey));
// 为了信息的安全,将appId删除后返回前端
map.remove("appId");
} catch (Exception e) {
e.printStackTrace();
}
return map;
}
}
2.请求工具类代码
以下代码直接拿着用就行。
public class HttpClient {
/**
* 地址
*/
private String url;
/**
* 参数
*/
private Map<String, Object> param;
/**
* 状态码
*/
private int statusCode;
/**
* 响应内容
*/
private String content;
/**
* xml类型参数
*/
private String xmlParam;
/**
* 是不是HTTPS请求(默认是true)
*/
private boolean isHttps = true;
public boolean isHttps() {
return isHttps;
}
public void setHttps(boolean isHttps) {
this.isHttps = isHttps;
}
public String getXmlParam() {
return xmlParam;
}
public void setXmlParam(String xmlParam) {
this.xmlParam = xmlParam;
}
public HttpClient(String url, Map<String, Object> param) {
this.url = url;
this.param = param;
}
public HttpClient(String url) {
this.url = url;
}
public void setParameter(Map<String, Object> map) {
param = map;
}
public void addParameter(String key, String value) {
if (param == null)
param = new HashMap<String, Object>();
param.put(key, value);
}
public void post() throws ClientProtocolException, IOException {
HttpPost http = new HttpPost(url);
setEntity(http);
execute(http);
}
public void put() throws ClientProtocolException, IOException {
HttpPut http = new HttpPut(url);
setEntity(http);
execute(http);
}
public void get() throws ClientProtocolException, IOException {
if (param != null) {
StringBuilder url = new StringBuilder(this.url);
boolean isFirst = true;
for (String key : param.keySet()) {
if (isFirst) {
url.append("?");
}else {
url.append("&");
}
url.append(key).append("=").append(param.get(key));
}
this.url = url.toString();
}
HttpGet http = new HttpGet(url);
execute(http);
}
/**
* set http post,put param
*/
private void setEntity(HttpEntityEnclosingRequestBase http) {
if (param != null) {
List<NameValuePair> nvps = new LinkedList<NameValuePair>();
for (String key : param.keySet()) {
nvps.add(new BasicNameValuePair(key, param.get(key).toString())); // 参数
}
http.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); // 设置参数
}
if (xmlParam != null) {
http.setEntity(new StringEntity(xmlParam, Consts.UTF_8));
}
}
private void execute(HttpUriRequest http) throws ClientProtocolException,
IOException {
CloseableHttpClient httpClient = null;
try {
if (isHttps) {
SSLContext sslContext = new SSLContextBuilder()
.loadTrustMaterial(null, new TrustStrategy() {
// 信任所有
@Override
public boolean isTrusted(X509Certificate[] chain,
String authType)
throws CertificateException {
return true;
}
}).build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslContext);
httpClient = HttpClients.custom().setSSLSocketFactory(sslsf)
.build();
} else {
httpClient = HttpClients.createDefault();
}
CloseableHttpResponse response = httpClient.execute(http);
try {
if (response != null) {
if (response.getStatusLine() != null) {
statusCode = response.getStatusLine().getStatusCode();
}
HttpEntity entity = response.getEntity();
// 响应内容
content = EntityUtils.toString(entity, Consts.UTF_8);
}
} finally {
response.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
httpClient.close();
}
}
public int getStatusCode() {
return statusCode;
}
public String getContent() throws ParseException, IOException {
return content;
}
}
七、Native测试说明
1.请求微信支付后返回数据说明
具体的参数说明,详见微信官方文档:
说明:在V2版本中,微信native支付与JSAPI支付接口说明,都是这篇文档,接口也是一致的。
URL:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_1
2.付款测试
新建“WxPay.html”文件,然后将"qrious.js"文件(根据链接生成二维码用的)放在同一目录下,这个js文件在网上下载就行。
2.1 WxPay.html代码:
<html>
<head>
<title>二维码</title>
</head>
<body>
<img id="abc">
<script src="qrious.js"></script>
<script>
var qr = new QRious({
element:document.getElementById('abc'),
size:400,
level:'L',
value:"weixin://wxpay/bizpayurl?pr=qxB37vLzz"
});
</script>
</body>
</html>
2.2 说明
代码中的value值就是请求微信接口后数据中的“code_url”,将此值复制替换上边代码中的值就行。
替换后,保存修改,用浏览器打后开就会有一个二维码,用自己的微信扫描二维码会出现付款的界面,付款成功就是OK了。
3.回调测试
线下的回调测试需要用到内网穿透,具体的方法,后续会出一篇博客来说明。
以上是我个人做支付时的代码,可能还有一些不完善,不成熟的地方,欢迎各位讨论!