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;// verifierpublic static Verifier verifier;// httpClientpublic static CloseableHttpClient httpClient;@Overridepublic 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 {// 从证书管理器中获取verifierverifier = 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 {@Autowiredprivate 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));}}
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 {@Autowiredprivate LsOrderMapper orderMapper;@Autowiredprivate LsUserMapper lsUserMapper;@Autowiredprivate LsProductMapper lsProductMapper;@Autowiredprivate LsAppendMapper lsAppendMapper;/*** 立即下单** @param lsOrder 订单* @return 结果*/@Transactional(rollbackFor = Exception.class)@Overridepublic 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*/@Overridepublic AjaxResult payment(LsOrder lsOrder) {try {JSONObject order = new JSONObject();// 应用IDorder.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);// 微信httpClientCloseableHttpClient 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);}// 返回预支付idfinal 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"); //小程序appIdsb.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*/@Overridepublic 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);// 从证书管理器中获取verifierVerifier 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*/public boolean closePayOrder(String orderNo) {JSONObject obj = new JSONObject();// 直连商户号obj.put("mchid", WxPayUtils.mchId);// 请求地址String closeOrderUrl = ConstantUtils.CLOSE_PAY_ORDER_URL.replace("{out_trade_no}", orderNo);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"));// 微信httpClientCloseableHttpClient httpClient = WechatPayaHttpclientUtils.httpClient;try {if(httpClient == null){log.info("关闭订单失败,请重试,无法连接微信支付服务器!");}//执行请求CloseableHttpResponse response = httpClient.execute(httpPost);//状态码int statusCode = response.getStatusLine().getStatusCode();if (statusCode == 204) {//关闭订单成功!log.info("微信关闭订单成功: {}", orderNo);}else if(statusCode == 202){//用户支付中,需要输入密码log.info("关闭微信订单--用户支付中,需要输入密码,暂时不做处理!");}else{log.info("关闭微信订单--关闭支付订单失败,出现未知原因: {}", EntityUtils.toString(response.getEntity()));}} catch (IOException e) {log.info("关闭微信订单--关闭订单失败: {}", e.getMessage());}return false;}/*** 订单查询 (定时查询订单, 修改订单状态)** @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":// 查询订单未支付 截止时间是否超时if (DateUtils.compareCurrentDateToEndDate(lsOrder.getNoPaymentCutoffTime())) {lsOrder.setStatus(0);if (orderMapper.updateLsOrder(lsOrder)>0) {// 微信关闭订单closePayOrder(lsOrder.getOrderNo());}}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"}
}