1. 微信小程序支付-开发者文档https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml
2. 导入依赖
<!--小程序支付 v3-->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.4.9</version>
</dependency>
3. 微信支付工具类
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.PrivateKey;
/**
* 微信支付工具类
*/
@Component
public class WxPayUtils {
// 商户号
public static final String mchId = "xxxxxxx";
// AppID(小程序ID)
public static final String appId = "xxxxxxx";
// AppSecret(小程序密钥)
public static final String appSecret = "xxxxxxx";
// 授权
public final static String grantType = "authorization_code";
// APIv3密钥
public final static String apiV3Key = "xxxxxxx";
// 证书序列号 (从p12文件解析)
public final static String serialnumber = "xxxxxxx";
// 证书私钥
public static final String privateKey = "xxxxxxx";
/**
* 获取私钥。
*/
public static PrivateKey getPrivateKey() throws IOException {
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
new ByteArrayInputStream(privateKey.getBytes("utf-8")));
return merchantPrivateKey;
}
}
4. 微信支付URL工具类
public interface ConstantUtils {
// JSAPI下单
public final static String JSAPI_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";
// 支付通知
public final static String NOTIFY_URL = "https://你的线上地址.com";
// 关闭订单
public final static String CLOSE_PAY_ORDER_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{out_trade_no}/close";
// 查询订单 (根据商户订单号查询)
public final static String QUERY_PAY_RESULT_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/";
}
5. 微信支付API v3 HttpClient (自动处理签名和验签)
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.exception.HttpCodeException;
import com.wechat.pay.contrib.apache.httpclient.exception.NotFoundException;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
@Slf4j
@Component
@Order(3)
public class WechatPayaHttpclientUtils implements ApplicationRunner {
// 商户API私钥
public static PrivateKey merchantPrivateKey;
// verifier
public static Verifier verifier;
// httpClient
public static CloseableHttpClient httpClient;
@Override
public void run(ApplicationArguments args) {
log.info("----------->构造微信支付API v3 HttpClient");
createHttpClient();
}
/***
* 微信支付API v3 HttpClient
*
* 自动处理签名和验签
*
* @return
*/
public void createHttpClient() {
try {
merchantPrivateKey = WxPayUtils.getPrivateKey();
} catch (Exception e) {
e.printStackTrace();
}
// 获取证书管理器实例
CertificatesManager certificatesManager = CertificatesManager.getInstance();
try {
// 向证书管理器增加需要自动更新平台证书的商户信息
certificatesManager.putMerchant(WxPayUtils.mchId, new WechatPay2Credentials(WxPayUtils.mchId,
new PrivateKeySigner(WxPayUtils.serialnumber, merchantPrivateKey)), WxPayUtils.apiV3Key.getBytes(StandardCharsets.UTF_8));
// ... 若有多个商户号,可继续调用putMerchant添加商户信息
} catch (IOException e) {
e.printStackTrace();
} catch (GeneralSecurityException e) {
e.printStackTrace();
} catch (HttpCodeException e) {
e.printStackTrace();
}
try {
// 从证书管理器中获取verifier
verifier = certificatesManager.getVerifier(WxPayUtils.mchId);
} catch (NotFoundException e) {
e.printStackTrace();
}
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(WxPayUtils.mchId, WxPayUtils.serialnumber, merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier));
// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签
httpClient = builder.build();
}
}
6. controller
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.ruoyils.sy.order.domain.LsOrder;
import com.ruoyi.ruoyils.wx.pay.service.IWxPayService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
@Api(value = "微信-支付", tags = {"微信-支付"})
@RestController
@RequestMapping("/ls/wx/pay")
public class WxPayController extends BaseController {
@Autowired
private IWxPayService wxPayService;
/**
* jsapi下单
*
* @param lsOrder 订单
* @return
*/
@ApiOperation("统一下单")
@PostMapping(value = "/payment")
public AjaxResult payment(@RequestBody LsOrder lsOrder) {
return wxPayService.addOrder(lsOrder);
}
/**
* 支付通知
*
* @param request
* @param response
* @return
*/
@GetMapping("/notifyUrl")
public AjaxResult notifyUrl(HttpServletRequest request, HttpServletResponse response) {
return AjaxResult.success(wxPayService.notifyUrl(request, response));
}
/**
* 关闭订单
*
* 当用户10分钟内未支付,系统自动关闭微信支付订单
*
* @param orderNo 订单号
* @return
*/
@GetMapping("/closePayOrder")
public AjaxResult closePayOrder(String orderNo) {
return AjaxResult.success(wxPayService.closePayOrder(orderNo));
}
}
7. service
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.ContentType;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.sign.Base64;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.ruoyils.exception.MyException;
import com.ruoyi.ruoyils.sy.append.domain.LsAppend;
import com.ruoyi.ruoyils.sy.append.mapper.LsAppendMapper;
import com.ruoyi.ruoyils.sy.order.domain.LsOrder;
import com.ruoyi.ruoyils.sy.order.mapper.LsOrderMapper;
import com.ruoyi.ruoyils.sy.order.service.impl.LsOrderServiceImpl;
import com.ruoyi.ruoyils.sy.product.domain.LsProduct;
import com.ruoyi.ruoyils.sy.product.mapper.LsProductMapper;
import com.ruoyi.ruoyils.utils.*;
import com.ruoyi.ruoyils.wx.pay.service.IWxPayService;
import com.ruoyi.ruoyils.wx.user.domain.LsUser;
import com.ruoyi.ruoyils.wx.user.mapper.LsUserMapper;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.notification.Notification;
import com.wechat.pay.contrib.apache.httpclient.notification.NotificationHandler;
import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.security.Signature;
import java.util.*;
@Order(5)
@Slf4j
@Service
public class WxPayServiceImpl implements IWxPayService {
@Autowired
private LsOrderMapper orderMapper;
@Autowired
private LsUserMapper lsUserMapper;
@Autowired
private LsProductMapper lsProductMapper;
@Autowired
private LsAppendMapper lsAppendMapper;
/**
* 立即下单
*
* @param lsOrder 订单
* @return 结果
*/
@Transactional(rollbackFor = Exception.class)
@Override
public AjaxResult addOrder(LsOrder lsOrder) {
// TODO 计算你的业务订单
// 生成订单
int i = orderMapper.insertLsOrder(lsOrder);
if (i > 0) {
// 调起支付
AjaxResult payment = this.payment(lsOrder);
return payment;
}
} catch (Exception e) {
e.printStackTrace();
throw e;
}
return AjaxResult.error("下单失败 !");
}
/**
* 调起支付
*
* @param lsOrder
* @return
*/
@Override
public AjaxResult payment(LsOrder lsOrder) {
try {
JSONObject order = new JSONObject();
// 应用ID
order.put("appid",WxPayUtils.appId);
// 商户号
order.put("mchid",WxPayUtils.mchId);
// 商品描述
order.put("description",lsOrder.getProductName());
// 订单号
order.put("out_trade_no",lsOrder.getOrderNo());
// 通知地址
order.put("notify_url",ConstantUtils.NOTIFY_URL+"/ls/wx/pay/notifyUrl");
/**订单金额*/
JSONObject amount = new JSONObject();
// 总金额 (默认单位分)
// amount.put("total",lsOrder.getTotalPrice().intValue()*100);
amount.put("total",1);
// 货币类型
amount.put("currency","CNY");
order.put("amount",amount);
/**支付者*/
JSONObject payer = new JSONObject();
LsUser user = SpringUtils.getBean(LsUserMapper.class).selectLsUserById(lsOrder.getUserId());
// 用户标识
payer.put("openid",user.getOpenid());
order.put("payer",payer);
// 微信httpClient
CloseableHttpClient httpClient = WechatPayaHttpclientUtils.httpClient;
if (httpClient == null) {
log.info("预下单请求失败");
return AjaxResult.error("预下单失败,请重试,无法连接微信支付服务器!");
}
HttpPost httpPost = new HttpPost(ConstantUtils.JSAPI_URL);
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-type","application/json; charset=utf-8");
httpPost.setEntity(new StringEntity(order.toJSONString(), "UTF-8"));
// 后面跟使用Apache HttpClient一样
CloseableHttpResponse response = httpClient.execute(httpPost);
String bodyAsString = EntityUtils.toString(response.getEntity());
JSONObject bodyAsJSON = JSONObject.parseObject(bodyAsString);
String message = bodyAsJSON.getString("message");
// 返回code, 说明请求失败
if (bodyAsJSON.containsKey("code")) {
log.info("预下单请求失败{}", message);
return AjaxResult.error("预下单失败,请重试!" + message);
}
// 返回预支付id
final String prepay_id = bodyAsJSON.getString("prepay_id");
if (StringUtils.isEmpty(prepay_id)) {
log.info("预下单请求失败{}", message);
return AjaxResult.error("预下单失败,请重试!" + message);
}
LsOrder preOrder = new LsOrder();
preOrder.setId(lsOrder.getId());
preOrder.setPrepayId(prepay_id);
orderMapper.updateLsOrder(preOrder);
// JSAPI调起支付API: 此API无后台接口交互,需要将列表中的数据签名
// 随机字符串
final String nonceStr = RandomNumberUtils.getRandomString(32,false);
// 时间戳
String timeStamp = String.valueOf(System.currentTimeMillis());
/*签名*/
StringBuilder sb = new StringBuilder();
sb.append(WxPayUtils.appId + "\n"); //小程序appId
sb.append(timeStamp + "\n"); //时间戳
sb.append(nonceStr + "\n"); //随机字符串
sb.append("prepay_id=" + prepay_id + "\n"); //订单详情扩展字符串
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(WechatPayaHttpclientUtils.merchantPrivateKey);
signature.update(sb.toString().getBytes("UTF-8"));
byte[] signBytes = signature.sign();
String paySign = Base64.encode(signBytes);
JSONObject params = new JSONObject();
params.put("appId", WxPayUtils.appId);
params.put("timeStamp", timeStamp);
params.put("nonceStr", nonceStr);
params.put("package", "prepay_id="+prepay_id);
params.put("signType", "RSA");
params.put("paySign", paySign);
return AjaxResult.success(params);
} catch (Exception e) {
e.printStackTrace();
}
return AjaxResult.error("预下单失败,请重试!");
}
/**
* 支付通知
*
* @param servletRequest
* @param response
* @return
*/
@Override
public AjaxResult notifyUrl(HttpServletRequest servletRequest, HttpServletResponse response) {
log.info("----------->微信支付回调开始");
Map<String, String> map = new HashMap<>(12);
String timeStamp = servletRequest.getHeader("Wechatpay-Timestamp");
String nonce = servletRequest.getHeader("Wechatpay-Nonce");
String signature = servletRequest.getHeader("Wechatpay-Signature");
String certSn = servletRequest.getHeader("Wechatpay-Serial");
try (BufferedReader reader = new BufferedReader(new InputStreamReader(servletRequest.getInputStream()))) {
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
stringBuilder.append(line);
}
String obj = stringBuilder.toString();
log.info("支付回调请求参数:{},{},{},{},{}", obj, timeStamp, nonce, signature, certSn);
// 从证书管理器中获取verifier
Verifier verifier = WechatPayaHttpclientUtils.verifier;
String sn = verifier.getValidCertificate().getSerialNumber().toString(16).toUpperCase(Locale.ROOT);
NotificationRequest request = new NotificationRequest.Builder().withSerialNumber(sn)
.withNonce(nonce)
.withTimestamp(timeStamp)
.withSignature(signature)
.withBody(obj)
.build();
NotificationHandler handler = new NotificationHandler(verifier, WxPayUtils.apiV3Key.getBytes(StandardCharsets.UTF_8));
// 验签和解析请求体
Notification notification = handler.parse(request);
JSONObject bodyAsJSON = JSON.parseObject(notification.getDecryptData());
log.info("支付回调响应参数:{}", bodyAsJSON.toJSONString());
//做一些操作
if (bodyAsJSON != null) {
//如果支付成功
String tradeState = bodyAsJSON.getString("trade_state");
if("SUCCESS".equals(tradeState)){
//拿到商户订单号
String outTradeNo = bodyAsJSON.getString("out_trade_no");
JSONObject amountJson = bodyAsJSON.getJSONObject("amount");
Integer payerTotal = amountJson.getInteger("payer_total");
LsOrder order = orderMapper.selectLsOrderByOrderNo(outTradeNo);
if(order != null){
if(order.getStatus() == 1){
//如果支付状态为1 说明订单已经支付成功了,直接响应微信服务器返回成功
response.setStatus(200);
map.put("code", "SUCCESS");
map.put("message", "SUCCESS");
response.setHeader("Content-type", ContentType.JSON.toString());
response.getOutputStream().write(JSONUtil.toJsonStr(map).getBytes(StandardCharsets.UTF_8));
response.flushBuffer();
}
//验证用户支付的金额和订单金额是否一致
if(payerTotal.equals(order.getTotalPrice())){
//修改订单状态
String successTime = bodyAsJSON.getString("success_time");
order.setStatus(1);
order.setPaymentTime(DateUtils.rfcToDate(successTime));
orderMapper.updateLsOrder(order);
//响应微信服务器
response.setStatus(200);
map.put("code", "SUCCESS");
map.put("message", "SUCCESS");
response.setHeader("Content-type", ContentType.JSON.toString());
response.getOutputStream().write(JSONUtil.toJsonStr(map).getBytes(StandardCharsets.UTF_8));
response.flushBuffer();
}
}
}
}
response.setStatus(500);
map.put("code", "FAIL");
map.put("message", "签名错误");
response.setHeader("Content-type", ContentType.JSON.toString());
response.getOutputStream().write(JSONUtil.toJsonStr(map).getBytes(StandardCharsets.UTF_8));
response.flushBuffer();
} catch (Exception e) {
log.error("微信支付回调失败", e);
}
return AjaxResult.error("微信支付回调失败");
}
/**
* 关闭订单
*
* @param orderNo
* @return
*/
@Override
public String closePayOrder(String orderNo) {
JSONObject obj = new JSONObject();
// 直连商户号
obj.put("mchid", WxPayUtils.mchId);
// 请求地址
String closeOrderUrl = ConstantUtils.CLOSE_PAY_ORDER_URL.replace("{out_trade_no}", orderNo);
log.info("关闭微信订单--请求参数{}" , obj.toJSONString());
HttpPost httpPost = new HttpPost(closeOrderUrl);
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-type", "application/json; charset=utf-8");
httpPost.setEntity(new StringEntity(obj.toJSONString(), "UTF-8"));
String bodyAsString ;
// 微信httpClient
CloseableHttpClient httpClient = WechatPayaHttpclientUtils.httpClient;
try {
if(httpClient == null){
log.info("关闭订单失败,请重试,无法连接微信支付服务器!");
}
//执行请求
CloseableHttpResponse response = httpClient.execute(httpPost);
//状态码
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 204) {
//关闭订单成功!
log.info("关闭微信订单--关闭订单成功orderCode:{}!", orderNo);
LsOrder order = new LsOrder();
order.setOrderNo(orderNo);
// 取消订单
order.setStatus(0);
orderMapper.updateLsOrderByOrderAndStatus(order);
}else if(statusCode == 202){
//用户支付中,需要输入密码
log.info("关闭微信订单--用户支付中,需要输入密码,暂时不做处理!");
}else{
log.info("关闭微信订单--关闭支付订单失败,出现未知原因{}", EntityUtils.toString(response.getEntity()));
}
} catch (IOException e) {
log.info("关闭微信订单--关闭订单失败{}", e.getMessage());
}
return null;
}
/**
* 订单查询 (定时查询订单, 修改订单状态)
*
* @return
*/
@Scheduled(cron="0/10 * * * * ?")
public void queryOrder() {
LsOrder od = new LsOrder();
od.setStatus(3); //未支付
List<LsOrder> list = SpringUtils.getBean(LsOrderMapper.class).selectLsOrderList(od);
if (CollectionUtils.isEmpty(list)) {
return;
}
try {
for (LsOrder lsOrder : list) {
// 根据商户订单号查询
URIBuilder uriBuilder = new URIBuilder(ConstantUtils.QUERY_PAY_RESULT_URL+lsOrder.getOrderNo());
uriBuilder.setParameter("mchid", WxPayUtils.mchId);
HttpGet httpGet = new HttpGet(uriBuilder.build());
httpGet.addHeader("Accept", "application/json");
CloseableHttpClient httpClient = WechatPayaHttpclientUtils.httpClient;
if (httpClient == null) {
return;
}
CloseableHttpResponse response = httpClient.execute(httpGet);
String bodyAsString = EntityUtils.toString(response.getEntity());
JSONObject data = JSON.parseObject(bodyAsString);
log.info("微信订单查询:{}", data);
// 商户订单号
String outTradeNo = data.getString("out_trade_no");
// 交易状态 SUCCESS:支付成功, REFUND:转入退款, NOTPAY:未支付, CLOSED:已关闭
String tradeState = data.getString("trade_state");
// 支付完成时间
String successTime = data.getString("success_time");
Date date = DateUtils.rfcToDate(successTime);
if (StringUtils.isNotEmpty(outTradeNo) && StringUtils.isNotEmpty(tradeState)) {
switch (tradeState) {
case "SUCCESS":
log.info("支付成功商户订单号: {}",outTradeNo);
lsOrder.setStatus(1);
lsOrder.setPaymentTime(date);
orderMapper.updateLsOrder(lsOrder);
break;
case "REFUND":
break;
case "NOTPAY":
break;
case "CLOSED":
log.info("已关闭商户订单号: {}",outTradeNo);
lsOrder.setStatus(0);
orderMapper.updateLsOrder(lsOrder);
break;
}
} else {
log.info(data.getString("message"));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
8. 返回给前端调起支付的必要参数
{
"msg": "操作成功",
"code": 200,
"data": {
"timeStamp": "1692334223808",
"package": "prepay_id=wx18125024326178678d4e07673e277c0000",
"paySign": "ocI64smXYkantoHEIkv7fibP0Y83pUmoVN1pQAjrJnFRI75sXCmQt09emPMhZJ+ujuemaGinJdJjmvGZv1JFoWvGSaMv8imDxOQV2EBr9QI+gybtUyC57+H2PhjXIR4gF0M8n7yv6Q9TA+7EIfpXOTaMJjDzM4AkFhAwz/quUAAEJVPLuaMsyF1xqi1qSY9AnE309YhqVG6ETDbZeP9/fuGCs9gD1HdD14HF4BndU696wR4TQdoiTzIyOokrE21oZLdK6Tp6sBPj2mGiIFX8viEHxq8GWOEMOIQXlr4NId4hrYA1Nn6xLk2Ka75t2t8L5V//3rWmbGSOaE5nrkeJcg==",
"appId": "xxxxxxxxxxxxxx",
"signType": "RSA",
"nonceStr": "KFW6FBHHDMALH1A39FM07HKXM7I0T1GR"
}
}