一、对接前准备

最开始需要在微信支付的官网注册一个商户;
在管理页面中申请关联小程序,通过小程序的 appid 进行关联;商户号和appid之间是多对多的关系
进入微信公众平台,功能-微信支付中确认关联

具体流程请浏览官方文档:接入前准备-小程序支付 | 微信支付商户平台文档中心

流程走完之后,需要获取以下参数:

1,商户 id:mchId,

2,小程序id:appId
3,商户证书序列号:这个商户证书序列号在申请完证书之后就可以看到
4, 商户APIV3密钥, 我对接的是v3 接口 所以用APIV3密钥

当你按照文档下载商户证书zip,解压得到4个文件,一定要保存好。不能泄露

二、开始写代码

1.pom引入微信库

com.github.wechatpay-apiv3
wechatpay-apache-httpclient
0.4.7

com.alibaba
fastjson
1.2.80

2.yml文件写入配置

wxpay:#应用编号appId: xxxxxxxxxxxxx#商户号mchId: xxxxxxxxxxxxx# APIv3密钥apiV3Key: xxxxxxxxxxxxx# 支付通知回调, 本地测试内网穿透地址notifyUrl: http://405c3382p5.goho.co:25325/wenbo-pay/notify/payNotify# 退款通知回调,本地测试内网穿透地址refundNotifyUrl: http://405c3382p5.goho.co:25325/wenbo-pay/notify/refundNotify# 密钥路径,resources根目录下keyPemPath: apiclient_key.pem# 商户证书序列号serialNo: xxxxxxxxxxxxx# 小程序密钥appSecret: xxxxxxxxxxxxx

3.商户API私钥 放入resources根目录下 能获取到就行

编写配置类获取yml配置

package com.example.pay.config;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 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.stereotype.Component;import java.io.IOException;import java.io.InputStream;import java.nio.charset.StandardCharsets;import java.security.GeneralSecurityException;import java.security.PrivateKey;/** * @ClassName: WechatPayConfig * @author: tang * @createTime 2023-08-10 * 小程序支付配置类 */@Component@Data@Slf4j@ConfigurationProperties(prefix = "wxpay")public class WechatPayConfig {/** * 应用编号 */private String appId;/** * 商户号 */private String mchId;/** * APIv3密钥 */private String apiV3Key;/** * 支付通知回调地址 */private String notifyUrl;/** * 退款回调地址 */private String refundNotifyUrl;/** * API 证书中的 key.pem */private String keyPemPath;/** * 商户序列号 */private String serialNo;/** * 获取商户的私钥文件 * * @param keyPemPath * @return */public PrivateKey getPrivateKey(String keyPemPath) {InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(keyPemPath);if (inputStream == null) {throw new RuntimeException("私钥文件不存在");}return PemUtil.loadPrivateKey(inputStream);}/** * 获取证书管理器实例 * * @return */@Beanpublic Verifier getVerifier() throws GeneralSecurityException, IOException, HttpCodeException, NotFoundException {log.info("获取证书管理器实例");//获取商户私钥PrivateKey privateKey = getPrivateKey(keyPemPath);//私钥签名对象PrivateKeySigner privateKeySigner = new PrivateKeySigner(serialNo, privateKey);//身份认证对象WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);// 使用定时更新的签名验证器,不需要传入证书CertificatesManager certificatesManager = CertificatesManager.getInstance();certificatesManager.putMerchant(mchId, wechatPay2Credentials, apiV3Key.getBytes(StandardCharsets.UTF_8));return certificatesManager.getVerifier(mchId);}/** * 获取支付http请求对象 * * @param verifier * @return */@Bean(name = "wxPayClient")public CloseableHttpClient getWxPayClient(Verifier verifier) {//获取商户私钥PrivateKey privateKey = getPrivateKey(keyPemPath);WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create().withMerchant(mchId, serialNo, privateKey).withValidator(new WechatPay2Validator(verifier));// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新return builder.build();}}

封装 支付-退款 请求API

package com.example.pay.config;import com.alibaba.fastjson.JSONObject;import lombok.extern.slf4j.Slf4j;import org.apache.http.HttpEntity;import org.apache.http.HttpStatus;import org.apache.http.client.methods.CloseableHttpResponse;import org.apache.http.client.methods.HttpPost;import org.apache.http.entity.StringEntity;import org.apache.http.impl.client.CloseableHttpClient;import org.apache.http.util.EntityUtils;import org.springframework.stereotype.Component;import javax.annotation.Resource;/** * @ClassName: WechatPayRequest * @author: tang * @createTime 2023-08-10 * 封装公共的请求API,用于在业务请求时的统一处理。 */@Component@Slf4jpublic class WechatPayRequest {@Resourceprivate CloseableHttpClient wxPayClient;/** * 支付请求 * * @param url * @param paramsStr * @return */public String wechatHttpOrderPost(String url, String paramsStr) {try {HttpPost httpPost = new HttpPost(url);StringEntity stringEntity = new StringEntity(paramsStr, "utf-8");stringEntity.setContentType("application/json");httpPost.setEntity(stringEntity);httpPost.setHeader("Accept", "application/json");CloseableHttpResponse response = wxPayClient.execute(httpPost);//响应体HttpEntity entity = response.getEntity();String body = entity == null " />

支付回调解密处理

package com.example.pay.config;import com.alibaba.fastjson.JSONObject;import com.alibaba.fastjson.TypeReference;import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;import lombok.extern.slf4j.Slf4j;import org.apache.http.HttpEntity;import org.apache.http.client.methods.CloseableHttpResponse;import org.apache.http.util.EntityUtils;import javax.servlet.http.HttpServletRequest;import java.io.BufferedReader;import java.io.IOException;import java.nio.charset.StandardCharsets;import java.time.DateTimeException;import java.time.Duration;import java.time.Instant;import java.util.Map;import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;/** * @ClassName: WechatPayValidator * @author: tang * @createTime 2023-08-10 * 回调校验器 * 用于对微信支付成功后的回调数据进行签名验证,保证数据的安全性与真实性。 */@Slf4jpublic class WechatPayValidator {/** * 应答超时时间,单位为分钟 */private static final long RESPONSE_EXPIRED_MINUTES = 5;private final Verifier verifier;private final String requestId;private final String body;public WechatPayValidator(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) {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;}private void validateParameters(HttpServletRequest request) {// NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at lastString[] 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);}}private String buildMessage(HttpServletRequest request) {String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);String nonce = request.getHeader(WECHAT_PAY_NONCE);return timestamp + "\n"+ nonce + "\n"+ body + "\n";}private String getResponseBody(CloseableHttpResponse response) throws IOException {HttpEntity entity = response.getEntity();return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";}/** * 对称解密,异步通知的加密数据 * * @param resource 加密数据 * @param apiV3Key apiV3密钥 * @param type 1-支付,2-退款 * @return */public static Map decryptFromResource(String resource, String apiV3Key, Integer type) {String msg = type == 1 ? "支付成功" : "退款成功";log.info(msg + ",回调通知,密文解密");try {//通知数据Map resourceMap = JSONObject.parseObject(resource, new TypeReference<Map>() {});//数据密文String ciphertext = resourceMap.get("ciphertext");//随机串String nonce = resourceMap.get("nonce");//附加数据String associatedData = resourceMap.get("associated_data");log.info("密文: {}", ciphertext);AesUtil aesUtil = new AesUtil(apiV3Key.getBytes(StandardCharsets.UTF_8));String resourceStr = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),nonce.getBytes(StandardCharsets.UTF_8),ciphertext);log.info(msg + ",回调通知,解密结果 : {}", resourceStr);return JSONObject.parseObject(resourceStr, new TypeReference<Map>() {});} catch (Exception e) {throw new RuntimeException("回调参数,解密失败!");}}/** * 将通知参数转化为字符串 * * @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();}}}}}

编写下单 退款 controller这里注意 微信用户openid

package com.example.pay.controller;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;import com.alibaba.fastjson.TypeReference;import com.example.pay.config.WechatPayConfig;import com.example.pay.config.WechatPayRequest;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;import java.nio.charset.StandardCharsets;import java.security.PrivateKey;import java.security.Signature;import java.util.Base64;import java.util.HashMap;import java.util.Map;import java.util.Random;/** * @ClassName: pay * @author: tang * @createTime 2023-08-16 */@Slf4j@RestController@RequestMapping("/pay")public class PayController {@Resourceprivate WechatPayConfig wechatPayConfig;@Resourceprivate WechatPayRequest wechatPayRequest;/** * 预支付订单生成入口 */@GetMapping("/transactions")public Map transactions() {// 统一参数封装Map params = new HashMap(10);// 1,appid:公众号或移动应用的唯一标识符。params.put("appid", wechatPayConfig.getAppId());// 2,mch_id:商户号,由微信支付分配。params.put("mchid", wechatPayConfig.getMchId());// 3.description body:商品描述。params.put("description", "奥迪a4l 2023-限量款");// 4.out_trade_no:商户订单号,由商户自定义。params.put("out_trade_no", "we56f45waf4w6a5fwa");// 5.notify_url:接收微信支付异步通知回调地址。params.put("notify_url", wechatPayConfig.getNotifyUrl());// 6.total_fee:订单总金额,单位为分。Map amountMap = new HashMap(4);// 金额单位为分amountMap.put("total", 999999);amountMap.put("currency", "CNY");params.put("amount", amountMap);// 7.openid:用户在商户appid下的唯一标识。Map payerMap = new HashMap(4);// openid需要前端小程序通过用户code 请求微信接口获取用户唯一openid不懂的看官方文档:https://developers.weixin.qq.com/doc/aispeech/miniprogram/quickuse.htmlpayerMap.put("openid", "xxxxxxxxxxxxxxxxxxxx");params.put("payer", payerMap);String paramsStr = JSON.toJSONString(params);log.info("请求参数 ===> {}" + paramsStr);// 微信预支付下单接口路径String payUrl = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";// 获取支付 prepay_id参数String resStr = wechatPayRequest.wechatHttpOrderPost(payUrl, paramsStr);Map resMap = JSONObject.parseObject(resStr, new TypeReference<Map>() {});Object prepayId = resMap.get("prepay_id");// 获取签名String paySign;try {StringBuilder sb = new StringBuilder();// 应用idsb.append(wechatPayConfig.getAppId()).append("\n");// 支付签名时间戳sb.append(System.currentTimeMillis() / 1000).append("\n");// 随机字符串sb.append("5w7er7wa4fwa5e").append("\n");// 预支付交易会话ID这个要注意 key = "prepay_id=xxxxxx"sb.append("prepay_id=").append(prepayId).append("\n");// 签名Signature sign = Signature.getInstance("SHA256withRSA");// 获取商户私钥并进行签名PrivateKey privateKey = wechatPayConfig.getPrivateKey(wechatPayConfig.getKeyPemPath());sign.initSign(privateKey);sign.update(sb.toString().getBytes(StandardCharsets.UTF_8));// 得到签名paySign = Base64.getEncoder().encodeToString(sign.sign());} catch (Exception e) {log.error("支付模块_生成交易签名失败!" + e);return new HashMap();}// 将签名时数据和签名一起返回前端用于前端吊起支付Map map = new HashMap();// 小程序idmap.put("appId", wechatPayConfig.getAppId());// 时间戳map.put("timeStamp", System.currentTimeMillis() / 1000);// 随机字符串map.put("nonceStr", "56523268632356");// 预支付交易会话IDmap.put("package", "prepay_id=" + prepayId);// 签名方式map.put("signType", "RSA");// 签名map.put("paySign", paySign);return map;}/** * 申请退款 */@GetMapping("/refundOrder")public String refundOrder() {log.info("根据订单号申请退款,订单号: {}", "要退款的订单号这里写死");// 退款请求路径String url = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";// 设置参数Map params = new HashMap(2);// 要退款的订单编号订单编号params.put("out_trade_no", "57984wera64");// 商户自定义退款记录单号 用于退款记录的单号 跟退款订单号不是一样的int outRefundNo = new Random().nextInt(999999999);log.info("退款申请号:{}", outRefundNo);params.put("out_refund_no", outRefundNo + "");// 退款原因params.put("reason", "申请退款");// 退款通知回调地址params.put("notify_url", wechatPayConfig.getRefundNotifyUrl());Map amountMap = new HashMap();//退款金额,单位:分amountMap.put("refund", 999999);//原订单金额,单位:分amountMap.put("total", 99999);//退款币种amountMap.put("currency", "CNY");params.put("amount", amountMap);String paramsStr = JSON.toJSONString(params);// todo 插入一条退款记录到数据库log.info("请求参数 ===> {}" + paramsStr);String res = wechatPayRequest.wechatHttpPost(url, paramsStr);log.info("退款结果:{}", res);return res;}}

支付 退款 回调controller

package com.example.pay.controller;import com.alibaba.fastjson.JSONObject;import com.alibaba.fastjson.TypeReference;import com.example.pay.config.WechatPayConfig;import com.example.pay.config.WechatPayValidator;import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;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.locks.ReentrantLock;/** * @ClassName: NotifyController * @author: tang * @createTime 2023-08-10 */@RestController@Slf4j@RequestMapping("/callback")public class CallbackController {@Resourceprivate WechatPayConfig wechatPayConfig;@Resourceprivate Verifier verifier;private final ReentrantLock lock = new ReentrantLock();/** * 支付回调处理 * * @param request * @param response * @return */@PostMapping("/payNotify")public Map payNotify(HttpServletRequest request, HttpServletResponse response) {log.info("支付回调");// 处理通知参数Map bodyMap = getNotifyBody(request);if (bodyMap == null) {return falseMsg(response);}log.warn("=========== 在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱 ===========");if (lock.tryLock()) {try {// 解密resource中的通知数据String resource = bodyMap.get("resource").toString();Map resourceMap = WechatPayValidator.decryptFromResource(resource, wechatPayConfig.getApiV3Key(), 1);String orderNo = resourceMap.get("out_trade_no").toString();// String transactionId = resourceMap.get("transaction_id").toString();// 更改状态 获取订单号修改订单状态为已支付// TODO 根据订单号,做幂等处理,并且在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱log.warn("=========== 根据订单号,做幂等处理 ===========");} finally {//要主动释放锁lock.unlock();}}//成功应答return trueMsg(response);}/** * 退款回调处理 * * @param request * @param response * @return */@PostMapping("/refundNotify")public Map refundNotify(HttpServletRequest request, HttpServletResponse response) {log.info("退款回调");// 处理通知参数Map bodyMap = getNotifyBody(request);if (bodyMap == null) {return falseMsg(response);}log.warn("=========== 在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱 ===========");if (lock.tryLock()) {try {// 解密resource中的通知数据String resource = bodyMap.get("resource").toString();Map resourceMap = WechatPayValidator.decryptFromResource(resource, wechatPayConfig.getApiV3Key(), 2);String orderNo = resourceMap.get("out_trade_no").toString();//String transactionId = resourceMap.get("transaction_id").toString();log.info("退款所有参数" + resourceMap);// TODO 根据订单号,做幂等处理,并且在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱// 更改订单状态为已退款log.warn("=========== 根据订单号,做幂等处理 ===========");} finally {//要主动释放锁lock.unlock();}}//成功应答return trueMsg(response);}private Map getNotifyBody(HttpServletRequest request) {//处理通知参数String body = WechatPayValidator.readData(request);log.info("退款回调参数:{}", body);// 转换为MapMap bodyMap = JSONObject.parseObject(body, new TypeReference<Map>() {});// 微信的通知ID(通知的唯一ID)String notifyId = bodyMap.get("id").toString();// 验证签名信息WechatPayValidator wechatPayValidator= new WechatPayValidator(verifier, notifyId, body);if (!wechatPayValidator.validate(request)) {log.error("通知验签失败");return null;}log.info("通知验签成功");return bodyMap;}private Map falseMsg(HttpServletResponse response) {Map resMap = new HashMap(8);//失败应答response.setStatus(500);resMap.put("code", "ERROR");resMap.put("message", "通知验签失败");return resMap;}private Map trueMsg(HttpServletResponse response) {Map resMap = new HashMap(8);//成功应答response.setStatus(200);resMap.put("code", "SUCCESS");resMap.put("message", "成功");return resMap;}}

整体项目结构

请求预支付订单 得到预支付订单参数 小程序拿着这些参数拉起支付页面进行支付

支付完成后 微信会回调我们预留的回调接口

这里使用的是花生壳内网穿透进行回调测试的

退款业务就不一一截图了 图片太多显得文章拉胯


总结