一、前言
下面我以微信支付v3为例,通过spirngboot集成到我们的项目中,不依赖其他第三方框架。当然适用简单项目,后续需要更多的改进。项目为简单的一个在线课程项目,通过在线购买视频课程,生成和支付订单,完成在线内容的学习。
二、springboot集成
2.1 配置信息与配置类
- 配置文件
# 微信支付相关参数
# 商户号
wxpay.mch-id=xxxx
# 商户API证书序列号
wxpay.mch-serial-no=xxxx
# 商户私钥文件
wxpay.private-key-path=xxxx
# APIv3密钥
wxpay.api-v3-key=XXXX
# APPID
wxpay.appid=
# 微信服务器地址
wxpay.domain=https://api.mch.weixin.qq.com
# 接收结果通知地址
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
wxpay.notify-domain=XXXX
# APIv2密钥
wxpay.partnerKey: XXX
-
相应的参数值,根据官方文档自己去申请,如果只是学习研究,可以通过下面链接1获取。
-
wxpay.notify-domain:微信回调通知地址,如果有在公网部署用公网地址,没有的话通过内网地址穿透工具,这里推荐开源免费的ngrok工具。
-
配置类
package com.gaogzhen.paymentdemo.config; import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder; import com.wechat.pay.contrib.apache.httpclient.auth.*; import com.wechat.pay.contrib.apache.httpclient.util.PemUtil; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.apache.http.impl.client.CloseableHttpClient; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.nio.charset.StandardCharsets; import java.security.PrivateKey; @Configuration @PropertySource("classpath:wxpay.properties") //读取配置文件 @ConfigurationProperties(prefix="wxpay") //读取wxpay节点 @Data //使用set方法将wxpay节点中的值填充到当前类的属性中 @Slf4j public class WxPayConfig { // 商户号 private String mchId; // 商户API证书序列号 private String mchSerialNo; // 商户私钥文件 private String privateKeyPath; // APIv3密钥 private String apiV3Key; // APPID private String appid; // 微信服务器地址 private String domain; // 接收结果通知地址 private String notifyDomain; // APIv2密钥 private String partnerKey; /** * 获取商户的私钥文件 * @param filename * @return */ private PrivateKey getPrivateKey(String filename){ try { return PemUtil.loadPrivateKey(new FileInputStream(filename)); } catch (FileNotFoundException e) { throw new RuntimeException("私钥文件不存在", e); } } /** * 获取签名验证器 * @return */ @Bean public ScheduledUpdateCertificatesVerifier getVerifier(){ log.info("获取签名验证器"); //获取商户私钥 PrivateKey privateKey = getPrivateKey(privateKeyPath); //私钥签名对象 PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey); //身份认证对象 WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner); // 使用定时更新的签名验证器,不需要传入证书 ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier( wechatPay2Credentials, apiV3Key.getBytes(StandardCharsets.UTF_8)); return verifier; } /** * 获取http请求对象 * @param verifier * @return */ @Bean(name = "wxPayClient") public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier){ log.info("获取httpClient"); //获取商户私钥 PrivateKey privateKey = getPrivateKey(privateKeyPath); WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() .withMerchant(mchId, mchSerialNo, privateKey) .withValidator(new WechatPay2Validator(verifier)); // ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新 CloseableHttpClient httpClient = builder.build(); return httpClient; } /** * 获取HttpClient,无需进行应答签名验证,跳过验签的流程 */ @Bean(name = "wxPayNoSignClient") public CloseableHttpClient getWxPayNoSignClient(){ //获取商户私钥 PrivateKey privateKey = getPrivateKey(privateKeyPath); //用于构造HttpClient WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() //设置商户信息 .withMerchant(mchId, mchSerialNo, privateKey) //无需进行签名验证、通过withValidator((response) -> true)实现 .withValidator((response) -> true); // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新 CloseableHttpClient httpClient = builder.build(); log.info("== getWxPayNoSignClient END =="); return httpClient; } }
- 使用定时更新的签名验证器,不需要传入证书:这里我们需要验证传回来的信息是微信返回的那么使用的是微信平台证书;相应的微信那边需要验证它接受的信息是我们商户这边发送过去的,需要用到商户平台证书。
2.2 微信相关枚举信息
-
微信API接口枚举WxApiType
package com.gaogzhen.paymentdemo.enums.wxpay; import lombok.AllArgsConstructor; import lombok.Getter; @AllArgsConstructor @Getter public enum WxApiType { /** * Native下单 */ NATIVE_PAY("/v3/pay/transactions/native"), /** * Native下单 */ NATIVE_PAY_V2("/pay/unifiedorder"), /** * 查询订单 */ ORDER_QUERY_BY_NO("/v3/pay/transactions/out-trade-no/%s"), /** * 关闭订单 */ CLOSE_ORDER_BY_NO("/v3/pay/transactions/out-trade-no/%s/close"), /** * 申请退款 */ DOMESTIC_REFUNDS("/v3/refund/domestic/refunds"), /** * 查询单笔退款 */ DOMESTIC_REFUNDS_QUERY("/v3/refund/domestic/refunds/%s"), /** * 申请交易账单 */ TRADE_BILLS("/v3/bill/tradebill"), /** * 申请资金账单 */ FUND_FLOW_BILLS("/v3/bill/fundflowbill"); /** * 类型 */ private final String type; }
-
支付状态
package com.gaogzhen.paymentdemo.enums.wxpay; import lombok.AllArgsConstructor; import lombok.Getter; @AllArgsConstructor @Getter public enum WxApiType { /** * Native下单 */ NATIVE_PAY("/v3/pay/transactions/native"), /** * Native下单 */ NATIVE_PAY_V2("/pay/unifiedorder"), /** * 查询订单 */ ORDER_QUERY_BY_NO("/v3/pay/transactions/out-trade-no/%s"), /** * 关闭订单 */ CLOSE_ORDER_BY_NO("/v3/pay/transactions/out-trade-no/%s/close"), /** * 申请退款 */ DOMESTIC_REFUNDS("/v3/refund/domestic/refunds"), /** * 查询单笔退款 */ DOMESTIC_REFUNDS_QUERY("/v3/refund/domestic/refunds/%s"), /** * 申请交易账单 */ TRADE_BILLS("/v3/bill/tradebill"), /** * 申请资金账单 */ FUND_FLOW_BILLS("/v3/bill/fundflowbill"); /** * 类型 */ private final String type; }
-
退款状态
package com.gaogzhen.paymentdemo.enums.wxpay; import lombok.AllArgsConstructor; import lombok.Getter; @AllArgsConstructor @Getter public enum WxRefundStatus { /** * 退款成功 */ SUCCESS("SUCCESS"), /** * 退款关闭 */ CLOSED("CLOSED"), /** * 退款处理中 */ PROCESSING("PROCESSING"), /** * 退款异常 */ ABNORMAL("ABNORMAL"); /** * 类型 */ private final String type; }
2.3 工具类
-
接口协议为http,通过HttpClientUtils工具类执行接口请求,或者可以使用其他的http工具类比如基础封装HttpClient,OkHttp或者hutool封装的http工具类,或者Forest框架等等是,这里我们就简单封装下,代码如下:
package com.gaogzhen.paymentdemo.util; import org.apache.http.Consts; import org.apache.http.HttpEntity; import org.apache.http.NameValuePair; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.*; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.conn.ssl.SSLContextBuilder; import org.apache.http.conn.ssl.TrustStrategy; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils; import javax.net.ssl.SSLContext; import java.io.IOException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.text.ParseException; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; /** * http请求客户端 */ public class HttpClientUtils { 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 HttpClientUtils(String url, Map<String, String> param) { this.url = url; this.param = param; } public HttpClientUtils(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)); } 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() { // 信任所有 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; } }
2.4 业务接口
也就是我们controller中对外暴露的接口,代码如下
package com.gaogzhen.paymentdemo.controller;
import com.gaogzhen.paymentdemo.service.WxPayService;
import com.gaogzhen.paymentdemo.util.HttpUtils;
import com.gaogzhen.paymentdemo.util.WechatPay2ValidatorForRequest;
import com.gaogzhen.paymentdemo.vo.R;
import com.google.gson.Gson;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@CrossOrigin //跨域
@RestController
@RequestMapping("/api/wx-pay")
@Api(tags = "网站微信支付APIv3")
@Slf4j
public class WxPayController {
@Resource
private WxPayService wxPayService;
@Resource
private Verifier verifier;
/**
* Native下单
* @param productId
* @return
* @throws Exception
*/
@ApiOperation("调用统一下单API,生成支付二维码")
@PostMapping("/native/{productId}")
public R nativePay(@PathVariable Long productId) throws Exception {
log.info("发起支付请求 v3");
//返回支付二维码连接和订单号
Map<String, Object> map = wxPayService.nativePay(productId);
return R.ok().setData(map);
}
/**
* 支付通知
* 微信支付通过支付通知接口将用户支付成功消息通知给商户
*/
@ApiOperation("支付通知")
@PostMapping("/native/notify")
public String nativeNotify(HttpServletRequest request, HttpServletResponse response){
Gson gson = new Gson();
Map<String, String> map = new HashMap<>();//应答对象
try {
//处理通知参数
String body = HttpUtils.readData(request);
Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
String requestId = (String)bodyMap.get("id");
log.info("支付通知的id ===> {}", requestId);
//log.info("支付通知的完整数据 ===> {}", body);
//int a = 9 / 0;
//签名的验证
WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
= new WechatPay2ValidatorForRequest(verifier, requestId, body);
if(!wechatPay2ValidatorForRequest.validate(request)){
log.error("通知验签失败");
//失败应答
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "通知验签失败");
return gson.toJson(map);
}
log.info("通知验签成功");
//处理订单
wxPayService.processOrder(bodyMap);
//应答超时
//模拟接收微信端的重复通知
TimeUnit.SECONDS.sleep(5);
//成功应答
response.setStatus(200);
map.put("code", "SUCCESS");
map.put("message", "成功");
return gson.toJson(map);
} catch (Exception e) {
e.printStackTrace();
//失败应答
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "失败");
return gson.toJson(map);
}
}
/**
* 用户取消订单
* @param orderNo
* @return
* @throws Exception
*/
@ApiOperation("用户取消订单")
@PostMapping("/cancel/{orderNo}")
public R cancel(@PathVariable String orderNo) throws Exception {
log.info("取消订单");
wxPayService.cancelOrder(orderNo);
return R.ok().setMessage("订单已取消");
}
/**
* 查询订单
* @param orderNo
* @return
* @throws Exception
*/
@ApiOperation("查询订单:测试订单状态用")
@GetMapping("/query/{orderNo}")
public R queryOrder(@PathVariable String orderNo) throws Exception {
log.info("查询订单");
String result = wxPayService.queryOrder(orderNo);
return R.ok().setMessage("查询成功").data("result", result);
}
@ApiOperation("申请退款")
@PostMapping("/refunds/{orderNo}/{reason}")
public R refunds(@PathVariable String orderNo, @PathVariable String reason) throws Exception {
log.info("申请退款");
wxPayService.refund(orderNo, reason);
return R.ok();
}
/**
* 查询退款
* @param refundNo
* @return
* @throws Exception
*/
@ApiOperation("查询退款:测试用")
@GetMapping("/query-refund/{refundNo}")
public R queryRefund(@PathVariable String refundNo) throws Exception {
log.info("查询退款");
String result = wxPayService.queryRefund(refundNo);
return R.ok().setMessage("查询成功").data("result", result);
}
/**
* 退款结果通知
* 退款状态改变后,微信会把相关退款结果发送给商户。
*/
@ApiOperation("退款结果通知")
@PostMapping("/refunds/notify")
public String refundsNotify(HttpServletRequest request, HttpServletResponse response){
log.info("退款通知执行");
Gson gson = new Gson();
Map<String, String> map = new HashMap<>();//应答对象
try {
//处理通知参数
String body = HttpUtils.readData(request);
Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
String requestId = (String)bodyMap.get("id");
log.info("支付通知的id ===> {}", requestId);
//签名的验证
WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
= new WechatPay2ValidatorForRequest(verifier, requestId, body);
if(!wechatPay2ValidatorForRequest.validate(request)){
log.error("通知验签失败");
//失败应答
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "通知验签失败");
return gson.toJson(map);
}
log.info("通知验签成功");
//处理退款单
wxPayService.processRefund(bodyMap);
//成功应答
response.setStatus(200);
map.put("code", "SUCCESS");
map.put("message", "成功");
return gson.toJson(map);
} catch (Exception e) {
e.printStackTrace();
//失败应答
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "失败");
return gson.toJson(map);
}
}
@ApiOperation("获取账单url:测试用")
@GetMapping("/querybill/{billDate}/{type}")
public R queryTradeBill(
@PathVariable String billDate,
@PathVariable String type) throws Exception {
log.info("获取账单url");
String downloadUrl = wxPayService.queryBill(billDate, type);
return R.ok().setMessage("获取账单url成功").data("downloadUrl", downloadUrl);
}
@ApiOperation("下载账单")
@GetMapping("/downloadbill/{billDate}/{type}")
public R downloadBill(
@PathVariable String billDate,
@PathVariable String type) throws Exception {
log.info("下载账单");
String result = wxPayService.downloadBill(billDate, type);
return R.ok().data("result", result);
}
}
tips::
- 这里我们只做简单集成,如果生成中使用,需要更完善的配置,比如跨域可以单独做更详细的配置,统一的错误处理,分布式并发一致性,性能优化等等。
三、演示-支付与退款
-
确认支付,获取二维码
-
微信扫码,支付回调
-
输入支付密码,完成支付,调用我们自己的后续处理逻辑
退款流程我们不在详述,看下我们的微信支付记录:
微信支付基础使用,演示完毕,完整前后端代码通过链接1 sgg的视频教程获取。后续我们会借助这个项目,通过集成一些开源的支付工具或者框架,比如jeepay,IJPay,roncoo-pay,spring-boot-pay 来集成支付功能。
结语
欢迎小伙伴一起学习交流,需要啥工具或者有啥问题随时联系我。
❓QQ:806797785
⭐️源代码地址:https://github.com/gaogzhen
[1]微信支付&支付宝支付视频[CP/OL]
[2]微信支付官方文档[CP/OL]