1、在pom.xml中添加依赖
2、在application.yml添加以下配置
3、创建微信支付config配置类
4.微信支付订单
5、支付通知
6.主动查询订单状态
7、apiclient_key.pem(私钥文件)
1、在pom.xml中添加依赖
<!--微信支付SDK--> <dependency> <groupId>com.github.wechatpay-apiv3</groupId> <artifactId>wechatpay-apache-httpclient</artifactId> <version>0.3.0</version> </dependency> <!-- json处理器:介绍gson依赖 --> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.6</version> </dependency>
2、在application.yml添加以下配置
# 微信支付相关参数 wxpay: # 商户号 mch-id: xxx1228xx # 商户API证书序列号 mch-serial-no: xxx6xxx5414D07xxxxxxxxECBBD047AA6xxx # 商户私钥文件 # 注:本文件放在项目根目目录下 private-key-path: ./apiclient_key.pem # APIv3密钥 api-v3-key: xxxxxxxycUJLDxxxZplQR683Mxxxxxx # APPID appid: xxxxxxc27e0e7cxxx # 微信服务器地址 domain: https://api.mch.weixin.qq.com # 接收结果通知地址 # 注:每次重新启动ngrok,需要根据实际情况修改此配置 notify-domain: https://c7c1-240e-3b5-3015-be0-1bc-9bed-fca4-d09b.ngrok.io
3.创建微信支付config配置类
@Configuration @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; /** * 获取商户的私钥文件 * * @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 = getPrivateKe(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;
}
}
4、微信支付下单
Controller:
/**
* native下单
*
* @param nativeReq
* @return
* @throws Exception
*/
@ApiOperation(value = "微信支付下单", notes = "微信支付下单")
@PostMapping("/native")
@Log(BusinessTypeEnum.OTHER)
public Result<NativeRes> nativePay(@RequestBody NativeReq nativeReq) throws Exception {
log.info("发起支付请求, params:{}", Json.toJsonString(nativeReq));
UxmOrder uxmOrder = orderService.createOrderByProductId(nativeReq, PayType.WXPAY);
if (nativeReq.getPrice().compareTo(uxmOrder.getAmount()) != 0) {
throw new ServiceException(ResultCode.ORDER_AMOUNT_NOT_FIT.getMessage(), ResultCode.ORDER_AMOUNT_NOT_FIT.getCode());
}
//生成订单
orderService.save(uxmOrder);
//调用统一下单api(返回支付二维码连接和订单号)
NativeRes nativeRes = wxPayService.nativePay(uxmOrder);
return Result.success(nativeRes);
}
service:
@Resource
private WxPayConfig wxPayConfig;
@Resource
private CloseableHttpClient wxPayClient;
@Resource
private IUxmOrderService orderService;
/**
* 调用统一下单api
*
* @param uxmOrder
* @return
* @throws Exception
*/
@Transactional(rollbackFor = Exception.class)
@Override
public NativeRes nativePay(UxmOrder uxmOrder) throws Exception {
HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
// 请求body参数
Gson gson = new Gson();
Map paramsMap = new HashMap();
paramsMap.put("appid", wxPayConfig.getAppid());
paramsMap.put("mchid", wxPayConfig.getMchId());
paramsMap.put("description", uxmOrder.getSubscribePlan());
paramsMap.put("out_trade_no", uxmOrder.getOrderNo());
paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxApiType.NATIVE_NOTIFY.getType()));
Map amountMap = new HashMap();
//由单位:元 转换为单位:分,并由Bigdecimal转换为整型
String amount = uxmOrder.getAmount().toString();
BigDecimal total = BigDecimalUtil.mul(amount, "100");
amountMap.put("total", total.intValue());
amountMap.put("currency", "CNY");
paramsMap.put("amount", amountMap);
//将参数转换成json字符串
String jsonParams = gson.toJson(paramsMap);
log.info("请求参数 ===> {}" + jsonParams);
StringEntity entity = new StringEntity(jsonParams, "utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response = wxPayClient.execute(httpPost);
try {
String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
int statusCode = response.getStatusLine().getStatusCode();//响应状态码
if (statusCode == 200) { //处理成功
log.info("成功, 返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else {
log.info("Native下单失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString);
throw new IOException("request failed");
}
//响应结果
Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
//二维码
String codeUrl = resultMap.get("code_url");
//保存二维码
String orderNo = uxmOrder.getOrderNo();
orderService.saveCodeUrl(orderNo, codeUrl);
//返回二维码\
NativeRes nativeRes = new NativeRes();
nativeRes.setCodeUrl(codeUrl);
nativeRes.setOrderNo(uxmOrder.getOrderNo());
return nativeRes;
} finally {
response.close();
}
}
封装微信支付api接口:
@AllArgsConstructor
@Getter
public enum WxApiType {
/**
* Native下单
*/
NATIVE_PAY("/v3/pay/transactions/native"),
/**
* 查询订单
*/
ORDER_QUERY_BY_NO("/v3/pay/transactions/out-trade-no/%s"),
/**
* 关闭订单
*/
CLOSE_ORDER_BY_NO("/v3/pay/transactions/out-trade-no/%s/close"),
/**
* 支付通知
*/
NATIVE_NOTIFY("/api/wx-pay/native/notify");
/**
* 类型
*/
private final String type;
}
5、支付通知
Controller:
/**
* 支付通知
* 微信支付通过支付通知接口将用户支付成功消息通知给商户
*/
@ApiOperation(value = "支付通知", notes = "支付通知")
@PostMapping("/native/notify")
@Log(BusinessTypeEnum.OTHER)
public Result<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");
//签名的验证
WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
= new WechatPay2ValidatorForRequest(verifier, requestId, body);
if (!wechatPay2ValidatorForRequest.validate(request)) {
log.error("通知验签失败");
//失败应答
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "通知验签失败");
return Result.success(gson.toJson(map));
}
log.info("通知验签成功");
//处理订单
wxPayService.processOrder(bodyMap);
//应答超时
//模拟接收微信端的重复通知
TimeUnit.SECONDS.sleep(5);
//成功应答
response.setStatus(200);
map.put("code", "SUCCESS");
map.put("message", "成功");
return Result.success(gson.toJson(map));
} catch (Exception e) {
e.printStackTrace();
//失败应答
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "失败");
return Result.success(gson.toJson(map));
}
}
HttpUtils.java
public class HttpUtils {
/**
* 将通知参数转化为字符串
*
* @param request
* @return
*/
public static String readData(HttpServletRequest request) {
BufferedReader br = null;
try {
StringBuilder result = new StringBuilder();
br = request.getReader();
for (String line; (line = br.readLine()) != null; ) {
if (result.length() > 0) {
result.append("\n");
}
result.append(line);
}
return result.toString();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
签名验证类:
/**
* 签名验证
*/
public class WechatPay2ValidatorForRequest {
protected static final Logger log = LoggerFactory.getLogger(WechatPay2ValidatorForRequest.class);
/**
* 应答超时时间,单位为分钟
*/
protected static final long RESPONSE_EXPIRED_MINUTES = 5;
protected final Verifier verifier;
protected final String requestId;
protected final String body;
public WechatPay2ValidatorForRequest(Verifier verifier, String requestId, String body) {
this.verifier = verifier;
this.requestId = requestId;
this.body = body;
}
protected static IllegalArgumentException parameterError(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("parameter error: " + message);
}
protected static IllegalArgumentException verifyFail(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("signature verify fail: " + message);
}
public final boolean validate(HttpServletRequest request) throws IOException {
try {
//处理请求参数
validateParameters(request);
//构造验签名串
String message = buildMessage(request);
String serial = request.getHeader(WECHAT_PAY_SERIAL);
String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
//验签
if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
serial, message, signature, requestId);
}
} catch (IllegalArgumentException e) {
log.warn(e.getMessage());
return false;
}
return true;
}
protected final void validateParameters(HttpServletRequest request) {
// NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
String header = null;
for (String headerName : headers) {
header = request.getHeader(headerName);
if (header == null) {
throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
}
}
//判断请求是否过期
String timestampStr = header;
try {
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
// 拒绝过期请求
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
}
} catch (DateTimeException | NumberFormatException e) {
throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
}
}
protected final String buildMessage(HttpServletRequest request) throws IOException {
String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
String nonce = request.getHeader(WECHAT_PAY_NONCE);
return timestamp + "\n"
+ nonce + "\n"
+ body + "\n";
}
protected final String getResponseBody(CloseableHttpResponse response) throws IOException {
HttpEntity entity = response.getEntity();
return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
}
}
Service:
private final ReentrantLock lock = new ReentrantLock();
/**
* 处理订单
*
* @param bodyMap
*/
@Override
public void processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException {
log.info("处理订单");
//解密报文
String plainText = decryptFromResource(bodyMap);
//将明文转换成map
Gson gson = new Gson();
HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
String orderNo = (String) plainTextMap.get("out_trade_no");
/*在对业务数据进行状态检查和处理之前,
要采用数据锁进行并发控制,
以避免函数重入造成的数据混乱*/
//尝试获取锁:
// 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放
if (lock.tryLock()) {
try {
//处理重复的通知
//接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。
String orderStatus = orderService.getOrderStatus(orderNo);
if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {
return;
}
//模拟通知并发
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//设置订单状态、升级版本、设置过期时间、添加支付记录
this.setOrderInfo(orderNo, plainText);
} finally {
//要主动释放锁
lock.unlock();
}
}
}
密文解密:
/**
* 对称解密
*
* @param bodyMap
* @return
*/
private String decryptFromResource(Map<String, Object> bodyMap) throws GeneralSecurityException {
log.info("密文解密");
//通知数据
Map<String, String> resourceMap = (Map) bodyMap.get("resource");
//数据密文
String ciphertext = resourceMap.get("ciphertext");
//随机串
String nonce = resourceMap.get("nonce");
//附加数据
String associatedData = resourceMap.get("associated_data");
AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
//数据明文
String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
nonce.getBytes(StandardCharsets.UTF_8),
ciphertext);
return plainText;
}
/**
* 设置订单状态、添加支付记录
* @param orderNo
* @param result
*/
private void setOrderInfo(String orderNo, String result){
UxmOrder order = orderService.getOne(new LambdaQueryWrapper<UxmOrder>()
.eq(UxmOrder::getOrderNo, orderNo), false);
if (OrderStatus.NOTPAY.getType().equals(order.getPayStatus())) {
//更新订单状态
orderService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
//记录支付日志
paymentInfoService.createPaymentInfo(result);
}
}
6、主动查询订单状态
Controller:
/**
* 查询订单状态
*/
@ApiOperation(value = "查询订单状态", notes = "查询订单状态")
@PostMapping("/order/confirm/{orderNo}")
public Result<String> orderConfirm(@PathVariable String orderNo) throws Exception {
//通过订单号查询订单
UxmOrder uxmOrder = orderService.getOrderByOrderNo(orderNo);
if (ObjectUtil.isNull(uxmOrder)) {
throw new Exception("参数错误,该订单不存在!");
}
//判断订单是否已支付,未支付则调用微信查询订单接口
if (OrderStatus.NOTPAY.getType().equals(uxmOrder.getPayStatus())) {
//未支付订单状态:调用微信支付查单接口
return Result.success(wxPayService.checkOrderStatus(orderNo));
}
return Result.success(WxTradeState.SUCCESS.getType());//SUCCESS NOTPAY
}
Service:
/**
* 查询订单状态
*
* @param orderNo
* @return
*/
@Override
public String checkOrderStatus(String orderNo) throws Exception {
log.warn("根据订单号核实订单状态 ===> {}", orderNo);
//调用微信支付查单接口
String result = this.queryOrder(orderNo);
Gson gson = new Gson();
Map<String, String> resultMap = gson.fromJson(result, HashMap.class);
//获取微信支付端的订单状态
String tradeState = resultMap.get("trade_state");
//判断订单状态
if (WxTradeState.SUCCESS.getType().equals(tradeState)) {
log.warn("核实订单已支付 ===> {}", orderNo);
//设置订单状态、升级版本、设置过期时间、添加支付记录
this.setOrderInfo(orderNo, result);
return tradeState;
}
return WxTradeState.NOTPAY.getType();
}
/**
* 调用微信支付查单接口
*
* @param orderNo
* @return
* @throws Exception
*/
private String queryOrder(String orderNo) throws Exception {
log.info("查单接口调用 ===> {}", orderNo);
String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo);
url = wxPayConfig.getDomain().concat(url).concat("?mchid=").concat(wxPayConfig.getMchId());
HttpGet httpGet = new HttpGet(url);
httpGet.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response = wxPayClient.execute(httpGet);
try {
String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
int statusCode = response.getStatusLine().getStatusCode();//响应状态码
if (statusCode == 200) { //处理成功
log.info("成功, 返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else {
log.info("查单接口调用,响应码 = " + statusCode + ",返回结果 = " + bodyAsString);
throw new IOException("request failed");
}
return bodyAsString;
} finally {
response.close();
}
}
7、apiclient_key.pem(私钥文件)
文件内容如下:(注:该文件存放位置根据application.yml中的“商户私钥文件”路径为准)
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC6KmMvuf/NVU8c
h/0OSkGP3ngmA+quLz/BTg/bcVYgBvEnB6UC3xQg6TdyBcu55t6Toa37+NnPGezh
TNixKPZpNRTkCJ0PV0Y5ns/KmBC7TJ+iiYmT3pJ3jZQR1we4M+cre6z805XohQiq
yp4zJObG4KLE6LJkFB7M2tArXQI8DFHp2j8SnDozzUQEwkffTHZJuO5exHmi214V
t/lNVfXBZXipisn9YQpUk6D+pXrASVdOTepgXQNIdzbQt07jO6N2UHtYxwNDZjO9
OJf6tGOqYYnebMQ7X+HT9xpxqViCVR2LrymPWe2Xl6+1mtmFoNsiE5Cm5skoqraA
bgc8E5LlAgMBAAECggEABgzuoFR5PeEx6wl4bgh3zQc7/HBQJk0e01eIKGesluni
2JPlBwzdCJzL0obhsi8QuNeeYfwaiCKdkkz/FfLw30Z8YVTuVdtOSv0gX8NFd/Dr
l0rFD+tB82TElTfZ5mC5eK5SVv1BeAcq2vIu5hai8X/HioLNmXcV8S6DaYViVzU5
GV7n2YC2rWWJ1vKZpNmXUWtxeVelmiSN+l20WHok3R/aziJ34KR1GXSfrjyYXgVL
WM7UVcAmzywKsHp5o3sU522kCKd3TGgHEsweaWgsBsqsvqvt5QDEMPbhYb99aUS9
TPwBy08Un1Yio7YQR/w7oOVe0pn8l1g7MdVc1gzK2QKBgQDh/Fa+4UF+q0qtQhKb
r/ElfsAb0q9RJ/A5jyxvL6oxVyE7cTzcSNvJVJXmFiakuQ5QMP+qa9dopBye+v63
/iT/yzmLfS82EnPyo1qx13zcCxQ6I9B/K00/597srUIZLVg3OumkTJi/vS4Zu/TJ
lcMjzXkC17RRC1vO8CW+ftgcqwKBgQDS5CNxuJ5BpkpuzLqopTuQ2gYtEwn8wc98
Junr7UvvxUhVf8iExJXe7+tTM/CzlJyyP+YHozdqKtynKAeosB6GhYaJLpUpwEqI
38sBHdvGPpYzcXSKLczHsY3r1CnP8mpxSSmp4Uwf3HwY/5LVR4qXtKJHn4Foz5Fs
e8iQcRPurwKBgA5gKACCgdEWAm0dG+PtgFCbTIs4jtCB0uVGd1QnWxNCcKnAXVfC
BsE68UIuvIyT/RYa19i2fYB5mByA6P05XI8tFV8LOpqc9+VCgP15MMcqqUG9j4DX
d1WOYX760o6ZdNgmlkBOYxUnaqxWaY79SOmZI46LvDu/ljqGyk/g78x7AoGASRgv
vMvLdl+nrs2g6LEUezlGKLtPm96lBpgKPe6qgjlzv8ahfnsQowuvGdCH3gZlZVbG
aOFGZLLkdb9nIC2i9ucy4TtXEfiHHPfMSd/Ke+TXdI8fYIFNV+2PjiykLWINSKSe
HzZqhySJkrSKdQft3nUKRh7f4K8I7Xvd4UqAKRsCgYBXieQ+7hHwUF71l1RXafJL
9N2+hMBRoMXdY8wuwlCiI6mfo92G0bwg0HGBRyWvbD25oBiE8yq+jFRhhaOIZHuY
n6TG1ogxgu/LWlrsJ9m4x2DiWvCe/A+aFdFOXukkCqGq4n5tSzrYycl8JDAg1lQB
d48QoGAp06HNYefd3AT54w==
-----END PRIVATE KEY-----
补充:
微信支付接口文档地址: 产品中心 - 微信支付商户平台
到此结束,如若对你有帮助,点个赞再走啦~~~