首先我们要明确目标,我们点击微信支付官网,我们主要聚焦于这三种支付方式,其中JSPAI与APP主要与uniapp开发微信小程序与APP对接,而NATIVE主要与网页端扫码支付对接
1.三种支付统一准备工作
建议导入这个jar,里面一些提供map和xml互转以及生成签名的函数,使用非常方便。
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
ps:下面代码涉及到的WXPayUtil下面的函数都是这个工具包里来的,所以不要再问这个函数代码在哪
商户号与支付结果通知回调地址
回调url配置途径:
微信商户平台(pay.weixin.qq.com)-->产品中心-->开发配置-->支付配置
既然涉及到维信小程序和app,那么它们的app_id是必须的
统一下单、查单、退款的API地址,去官方文档找即可,我这里直接写好了
/**
* 统一下单接口
*/
public static final String UNIFIED_ORDER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
/**
* 查询订单接口
*/
public static final String ORDER_QUERY_URL = "https://api.mch.weixin.qq.com/pay/orderquery";
/**
* 订单退款接口
*/
public static final String ORDER_REFUND_URL = "https://api.mch.weixin.qq.com/secapi/pay/refund";
API密钥
用于签名算法
获取途径
微信商户平台(pay.weixin.qq.com)-->账户中心-->账户设置-->API安全-->设置API密钥
2.JSAPI统一下单
这种支付的使用场景,我们这里是对接uniapp开发的微信小程序
我们仔细阅读文档并提取信息,其实访问接口的很多参数都不是必填的,我们尽量去关注必填参数
归纳如下:
appid | 微信小程序app_id |
mch_id | 商户号 |
nonce_str | 随机字符串,可以调用WXPayUtil下generateNonceStr方法生成 |
sign | 可以调用WXPayUtil下generateSignature方法生成,这个方法需要用到准备工作中的商户API密钥(APP_KEY)来加密。 |
body | 商品描述,建议软件名字+产品操作,例如天天爱消除-游戏充值 |
out_trade_no | 我们自己生成的订单号,保证同一商号下唯一即可 |
total_fee | 金额,注意单位是分 |
spbill_create_ip | 用户客户端ip |
notify_url | 支付结果通知回调地址 |
trade_type | 交易类型,我们这里填JSAPI |
openid | 因为是JSAPI方式,所以必传,openid其实很好获取,参考地址如何获取openid |
但是下完单之后,怎么样才能让前端调用微信支付呢?这里需要查看uniapp官方文档微信小程序支付
我们发现其实主要是拿到预支付id prepay_id以及签名。
代码实现:
public Map<String, String> unifiedOrderByJsp(Order order, String clientIp) throws Exception {
long begin = System.currentTimeMillis();
//使用map封装微信支付需要的固定参数
Map<String, String> m = new HashMap<>();
//设置支付参数
m.put("appid", OrderUtils.WX_APP_ID);
m.put("mch_id", OrderUtils.MCH_ID);
m.put("nonce_str", WXPayUtil.generateNonceStr());
//商品描述 例如:天天爱消除-游戏充值
m.put("body", "换芯易-" + order.getGoodsModel() + "购买");
//订单号
m.put("out_trade_no", order.getOrderSn());
m.put("total_fee", order.getActualPrice().multiply(new BigDecimal("100")).longValue() + "");
m.put("spbill_create_ip", clientIp);
m.put("notify_url", OrderUtils.NOTIFY_URL);
m.put("trade_type", "JSAPI");
//trade_type=JSAPI时(即JSAPI支付),必须要openid
UserThreeDao userThreeDao = new UserThreeDaoImpl();
UserThree userThree = userThreeDao.queryOpenid(order.getUserId());
if (userThree == null) {
return null;
}
m.put("openid", userThree.getThreeOpenid());
//生成签名
m.put("sign", WXPayUtil.generateSignature(m, OrderUtils.APP_KEY));
//发送请求,请求路径:UNIFIED_ORDER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder"
HttpClient client = new HttpClient(OrderUtils.UNIFIED_ORDER_URL);
//设置xml格式的参数,要把map转为xml
client.setXmlParam(WXPayUtil.mapToXml(m));
//设置支持https
client.setHttps(true);
//执行请求发送
client.post();
//xml转为map接受结果
Map<String, String> response = WXPayUtil.xmlToMap(client.getContent());
long end = System.currentTimeMillis();
System.out.println("请求https://api.mch.weixin.qq.com/pay/unifiedorder耗时:" + (end - begin) + "ms");
System.out.println("请求结果:" + JSON.toJSONString(response));
if ("SUCCESS".equals(response.get("return_code")) && "SUCCESS".equals(response.get("result_code"))) {
Map<String, String> param = new HashMap<>();
//返回结果格式参照https://uniapp.dcloud.io/api/plugins/payment?id=requestpayment
//随机字符串
//时间戳,但是单位为s,不是毫秒
param.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000));
param.put("nonceStr", WXPayUtil.generateNonceStr());
param.put("package", "prepay_id=" + response.get("prepay_id"));
param.put("signType", "MD5");
param.put("paySign", WXPayUtil.generateSignature(param, OrderUtils.APP_KEY));
return param;
}
//为空代表下单失败
return null;
}
3.NATIVE统一下单
主要应用场景是PC端扫码支付。
必填参数归纳如下:
appid | 微信小程序app_id |
mch_id | 商户号 |
nonce_str | 随机字符串,可以调用WXPayUtil下generateNonceStr方法生成 |
sign | 可以调用WXPayUtil下generateSignature方法生成,这个方法需要用到准备工作中的商户API密钥(APP_KEY)来加密。 |
body | 商品描述,建议软件名字+产品操作,例如天天爱消除-游戏充值 |
out_trade_no | 我们自己生成的订单号,保证同一商号下唯一即可 |
total_fee | 金额,注意单位是分 |
spbill_create_ip | 用户客户端ip |
notify_url | 支付结果通知回调地址 |
trade_type | 交易类型,我们这里填NATIVE |
product_id | trade_type=NATIVE时,此参数必传。此参数为二维码中包含的商品ID,我们可以自行定义。 |
代码如下:
public Map<String, String> unifiedOrderByNative(Order order, String clientIp) throws Exception {
long begin = System.currentTimeMillis();
//使用map封装微信支付需要的固定参数
Map<String, String> m = new HashMap<>();
//1、设置支付参数
m.put("appid", OrderUtils.WX_APP_ID);
m.put("mch_id", OrderUtils.MCH_ID);
m.put("nonce_str", WXPayUtil.generateNonceStr());
//商品描述 例如:天天爱消除-游戏充值
m.put("body", "换芯易-" + order.getGoodsModel() + "购买");
//订单号
m.put("out_trade_no", order.getOrderSn());
m.put("total_fee", order.getActualPrice().multiply(new BigDecimal("100")).longValue() + "");
m.put("spbill_create_ip", clientIp);
m.put("notify_url", OrderUtils.NOTIFY_URL);
m.put("trade_type", "NATIVE");
//trade_type=NATIVE时,此参数必传
m.put("product_id", order.getId());
//生成签名
m.put("sign", WXPayUtil.generateSignature(m, OrderUtils.APP_KEY));
//发送请求,请求路径:UNIFIED_ORDER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder"
HttpClient client = new HttpClient(OrderUtils.UNIFIED_ORDER_URL);
//设置xml格式的参数,要把map转为xml
client.setXmlParam(WXPayUtil.mapToXml(m));
//设置支持https
client.setHttps(true);
//执行请求发送
client.post();
//xml转为map接受结果
Map<String, String> response = WXPayUtil.xmlToMap(client.getContent());
long end = System.currentTimeMillis();
System.out.println("请求https://api.mch.weixin.qq.com/pay/unifiedorder耗时:" + (end - begin) + "ms");
System.out.println("请求结果:" + JSON.toJSONString(response));
if ("SUCCESS".equals(response.get("return_code")) && "SUCCESS".equals(response.get("result_code"))) {
Map<String, String> param = new HashMap<>();
//二维码地址
param.put("code_url", response.get("code_url"));
param.put("order_sn", order.getOrderSn());
param.put("order_id", order.getId());
param.put("total_fee", order.getActualPrice().multiply(new BigDecimal("100")).longValue() + "");
return param;
}
//为空代表下单失败
return null;
}
上面代码中最重要的返回结果就是code_url。即生成的支付二维码地址,然后用微信扫码并付款即可。
4.APP统一下单
应用场景是APP端调用微信支付,对接uniapp开发的app
归纳如下:
appid | APP的app_id |
mch_id | 商户号 |
nonce_str | 随机字符串,可以调用WXPayUtil下generateNonceStr方法生成 |
sign | 可以调用WXPayUtil下generateSignature方法生成,这个方法需要用到准备工作中的商户API密钥(APP_KEY)来加密。 |
body | 商品描述,建议软件名字+产品操作,例如天天爱消除-游戏充值 |
out_trade_no | 我们自己生成的订单号,保证同一商号下唯一即可 |
total_fee | 金额,注意单位是分 |
spbill_create_ip | 用户客户端ip |
notify_url | 支付结果通知回调地址 |
trade_type | 交易类型,我们这里填APP |
但是下完单之后,怎么样才能让前端调用微信支付呢?这里需要查看uniapp官方文档微信小程序支付:
我们可以看到常规的比如appid、商户号需要,充要的是预支付id prepayid和签名。
代码实现如下:
public Map<String, String> unifiedOrderByApp(Order order, String clientIp) throws Exception {
long begin = System.currentTimeMillis();
//使用map封装微信支付需要的固定参数
Map<String, String> m = new HashMap<>();
//1、设置支付参数
m.put("appid", OrderUtils.APP_APP_ID);
m.put("mch_id", OrderUtils.MCH_ID);
m.put("nonce_str", WXPayUtil.generateNonceStr());
//商品描述 例如:天天爱消除-游戏充值
m.put("body", "换芯易-" + order.getGoodsModel() + "购买");
//订单号
m.put("out_trade_no", order.getOrderSn());
m.put("total_fee", order.getActualPrice().multiply(new BigDecimal("100")).longValue() + "");
m.put("spbill_create_ip", clientIp);
m.put("notify_url", OrderUtils.NOTIFY_URL);
m.put("trade_type", "APP");
//生成签名
m.put("sign", WXPayUtil.generateSignature(m, OrderUtils.APP_KEY));
//发送请求,请求路径:UNIFIED_ORDER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder"
HttpClient client = new HttpClient(OrderUtils.UNIFIED_ORDER_URL);
//设置xml格式的参数,要把map转为xml
client.setXmlParam(WXPayUtil.mapToXml(m));
//设置支持https
client.setHttps(true);
//执行请求发送
client.post();
//xml转为map接受结果
Map<String, String> response = WXPayUtil.xmlToMap(client.getContent());
long end = System.currentTimeMillis();
System.out.println("请求https://api.mch.weixin.qq.com/pay/unifiedorder耗时:" + (end - begin) + "ms");
System.out.println("请求结果:" + JSON.toJSONString(response));
if ("SUCCESS".equals(response.get("return_code")) && "SUCCESS".equals(response.get("result_code"))) {
Map<String, String> param = new HashMap<>();
param.put("appid", OrderUtils.APP_APP_ID);
//随机字符串
param.put("noncestr", WXPayUtil.generateNonceStr());
//固定值
param.put("package", "Sign=WXPay");
param.put("partnerid", OrderUtils.MCH_ID);
param.put("prepayid", response.get("prepay_id"));
//时间戳,但是单位为s,不是毫秒
param.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
//param.put("package", response.get("sign"));
param.put("sign", WXPayUtil.generateSignature(param, OrderUtils.APP_KEY));
return param;
}
//为空代表下单失败
return null;
}
按照uniapp官方要求的格式封装返回结果给前端。
5.三种支付方式返回结果
对于上面三种支付方式,他们的返回结果我都没有细细分析,因为官方文档写得很详细,我们非常需要关注的也就预支付id这个返回结果。所以贴一下官方文档,有兴趣可以仔细看看(重要的我会标注)
共有
其中APP和JSAPI得着重关注预支付id
native扫码支付主要关注code_url
6.查单
我们实际业务中,过于依赖微信支付的回调结果来判断订单状态显然是不太合适的,所以自己去查单这种操作十分常见。查单需要的必填参数归纳如下:
appid | app_id |
mch_id | 商户号 |
nonce_str | 随机字符串,可以调用WXPayUtil下generateNonceStr方法生成 |
sign | 可以调用WXPayUtil下generateSignature方法生成,这个方法需要用到准备工作中的商户API密钥(APP_KEY)来加密。 |
out_trade_no | 我们自己生成的订单号,保证同一商号下唯一即可,这个订单号需要和我们当时调用统一下单传过去的单号一致 |
ps:这里的out_trade_no可以用transaction_id代替,但是一般我们查单的时候可能还没有transaction_id,所以只能用我们自己程序业务订单号out_trade_no去查,因为transaction_id需要调查单接口才能得到,也就是说如果我们以后"二次查单"可以用这个参数。
代码实现入下:
public Map<String, String> queryOrderStatus(String orderSn) throws Exception {
long beginTime = System.currentTimeMillis();
//1、封装参数
Map<String, String> m = new HashMap<>();
m.put("appid", OrderUtils.APP_APP_ID);
m.put("mch_id", OrderUtils.MCH_ID);
m.put("out_trade_no", orderSn);
m.put("nonce_str", WXPayUtil.generateNonceStr());
//生成签名
m.put("sign", WXPayUtil.generateSignature(m, OrderUtils.APP_KEY));
//发送请求,请求路径:UNIFIED_ORDER_URL = "https://api.mch.weixin.qq.com/pay/orderquery"
HttpClient client = new HttpClient(OrderUtils.ORDER_QUERY_URL);
client.setXmlParam(WXPayUtil.mapToXml(m));
client.setHttps(true);
client.post();
//3、返回第三方的数据
Map<String, String> result = WXPayUtil.xmlToMap(client.getContent());
long endTime = System.currentTimeMillis();
System.out.println("请求https://api.mch.weixin.qq.com/pay/orderquery耗时:" + (endTime - beginTime) + "ms");
System.out.println("请求结果:" + JSON.toJSONString(result));
return result;
}
返回结果参数部分如下,重要的标注起来:
一般情况下主要是利用trade_state来判断用户是否支付成功从而更新订单或记录交易成功物流等业务操作。
Map<String, String> map = orderDao.queryOrderStatus(orderSn);
if (map == null) {
return ApiResponse.createApiResponse(ApiResponse.HTTP_STATE_400_ERROR_10001, "微信支付出错");
}
//微信
if ("SUCCESS".equals(map.get("trade_state"))) {
//更新订单或记录交易成功物流等业务操作
}
7.退款
言简意赅,就是方便(rnm,退钱)
重要参数
appid | app_id |
mch_id | 商户号 |
nonce_str | 随机字符串,可以调用WXPayUtil下generateNonceStr方法生成 |
sign | 可以调用WXPayUtil下generateSignature方法生成,这个方法需要用到准备工作中的商户API密钥(APP_KEY)来加密。 |
out_trade_no | 我们自己生成的订单号,保证同一商号下唯一即可,这个订单号需要和我们当时调用统一下单传过去的单号一致 |
transaction_id | 和上面的out_trade_no二选一,官方推荐transaction_id |
out_refund_no | 其实和统一下单的时候的订单号原理差不多,我们自己随机生成一个,保证同一商户系统下唯一即可 |
total_fee | 要退款的订单的金额 |
refund_fee | 实际要退款的金额 |
代码实现如下:
public Map<String, String> refundOrder(RefundTrace refundTrace, Order order) throws Exception {
long begin = System.currentTimeMillis();
//使用map封装微信支付需要的固定参数
Map<String, String> m = new HashMap<>();
//1、设置支付参数
m.put("appid", OrderUtils.WX_APP_ID);
m.put("mch_id", OrderUtils.MCH_ID);
m.put("nonce_str", WXPayUtil.generateNonceStr());
//微信支付订单号
m.put("transaction_id", order.getPayId());
//商户退款单号
m.put("out_refund_no", refundTrace.getRefundSn());
//订单金额
m.put("total_fee", order.getActualPrice().multiply(new BigDecimal("100")).longValue() + "");
//退款金额 即实际退款金额
m.put("refund_fee", refundTrace.getRefundAmount().multiply(new BigDecimal("100")).longValue() + "");
//退款原因
m.put("refund_desc", refundTrace.getRefundReason());
m.put("notify_url", OrderUtils.NOTIFY_URL);
//生成签名
m.put("sign", WXPayUtil.generateSignature(m, OrderUtils.APP_KEY));
发送请求,请求路径:ORDER_REFUND_URL = "https://api.mch.weixin.qq.com/secapi/pay/refund"
String content = HttpRequestUtils.refundRequest(WXPayUtil.mapToXml(m));
//xml转为map接受结果
Map<String, String> response = WXPayUtil.xmlToMap(content);
long end = System.currentTimeMillis();
System.out.println("请求https://api.mch.weixin.qq.com/secapi/pay/refund耗时:" + (end - begin) + "ms");
System.out.println("请求结果:" + JSON.toJSONString(response));
if ("SUCCESS".equals(response.get("return_code")) && "SUCCESS".equals(response.get("result_code"))) {
Map<String, String> param = new HashMap<>();
param.put("refund_id", response.get("refund_id"));
param.put("refund_fee", response.get("refund_fee"));
return param;
}
//为空代表退款失败
return null;
}
这里注意一下!!!!因为操作涉及到把商户方的钱转回到买方的操作,所以安全系数比较高,调用退款api官方要求证书
证书申请途径如下:
微信商户平台(pay.weixin.qq.com)-->账户中心-->账户设置-->API安全-->申请API证书
申请到证书,需要放到项目下:
那么我们这里的http请求就不能用之前的了,需要配置该证书
上面代码中使用的refundRequest函数细节如下:
public static String refundRequest(String order) throws Exception {
try {
KeyStore clientStore = KeyStore.getInstance("PKCS12");
// 读取项目存放的PKCS12证书文件
FileInputStream instream = new FileInputStream("apiclient_cert.p12");
try {
// 指定PKCS12的密码(商户ID)
clientStore.load(instream, OrderUtils.MCH_ID.toCharArray());
} finally {
instream.close();
}
SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(clientStore, OrderUtils.MCH_ID.toCharArray()).build();
// 指定TLS版本
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext, new String[]{"TLSv1"}, null,
SSLConnectionSocketFactory.getDefaultHostnameVerifier());
// 设置httpclient的SSLSocketFactory
CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
try {
HttpPost httpost = new HttpPost(OrderUtils.ORDER_REFUND_URL); // 设置响应头信息
// httpost.addHeader("Connection", "keep-alive");
// httpost.addHeader("Accept", "*/*");
// httpost.addHeader("Content-Type", CONTENT_TYPE_FORM.toString());
// httpost.addHeader("X-Requested-With", "XMLHttpRequest");
// httpost.addHeader("Cache-Control", "max-age=0");
// httpost.addHeader("User-Agent", DEFAULT_USER_AGENT);
httpost.setEntity(new StringEntity(order, "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();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
8.补充
我所使用的HttpClient工具类源码,我这里使用主要用于发送带xml参数的post请求
public class HttpClient {
private String url;
private Map<String, String> param;
private int statusCode;
private String content;
private String xmlParam;
private boolean isHttps;
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, String> param) {
this.url = url;
this.param = param;
}
public HttpClient(String url) {
this.url = url;
}
public void setParameter(Map<String, String> map) {
param = map;
}
public void addParameter(String key, String value) {
if (param == null) {
param = new HashMap<String, String>();
}
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("?");
isFirst = false;
} else {
url.append("&");
}
url.append(key).append("=").append(param.get(key));
}
System.out.println("请求的url:" + url.toString());
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))); // 参数
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;
}
}