【微信支付】java-微信小程序支付-V3接口
一、对接前准备
最开始需要在微信支付的官网注册一个商户;
在管理页面中申请关联小程序,通过小程序的 appid 进行关联;商户号和appid之间是多对多的关系
进入微信公众平台,功能-微信支付中确认关联
具体流程请浏览官方文档:接入前准备-小程序支付 | 微信支付商户平台文档中心
流程走完之后,需要获取以下参数:
1,商户 id: mchId,
2,小程序id:appId
3,商户证书序列号: 这个商户证书序列号在申请完证书之后就可以看到
4, 商户APIV3密钥, 我对接的是v3 接口 所以用APIV3密钥
当你按照文档下载商户证书zip,解压得到4个文件,一定要保存好。不能泄露
二、开始写代码
pom引入微信库
com.github.wechatpay-apiv3
wechatpay-apache-httpclient
0.4.7
com.alibaba
fastjson
1.2.80
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
商户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;@Component@Data@Slf4j@ConfigurationProperties(prefix = "wxpay")public class WechatPayConfig { private String appId; private String mchId; private String apiV3Key; private String notifyUrl; private String refundNotifyUrl; private String keyPemPath; private String serialNo; public PrivateKey getPrivateKey(String keyPemPath) { InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(keyPemPath); if (inputStream == null) { throw new RuntimeException("私钥文件不存在"); } return PemUtil.loadPrivateKey(inputStream); } @Bean public 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); } @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;@Component@Slf4jpublic class WechatPayRequest { @Resource private CloseableHttpClient wxPayClient; 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 ? "" : EntityUtils.toString(entity); //响应状态码 int statusCode = response.getStatusLine().getStatusCode(); //处理成功,204是,关闭订单时微信返回的正常状态码 if (statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_NO_CONTENT) { log.info("成功, 返回结果 = " + body); } else { String msg = "微信支付请求失败,响应码 = " + statusCode + ",返回结果 = " + body; log.info("支付模块-生成订单 = " + msg); throw new RuntimeException(msg); } return body; } catch (Exception e) { throw new RuntimeException(e.getMessage()); } } public String wechatHttpPost(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 ? "" : EntityUtils.toString(entity); //响应状态码 int statusCode = response.getStatusLine().getStatusCode(); //处理成功,204是,关闭订单时微信返回的正常状态码 if (statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_NO_CONTENT) { log.info("成功, 返回结果 = " + body); // 请求成功或已处理成功,返回成功的响应 return "退款处理中"; } else if (statusCode == HttpStatus.SC_BAD_REQUEST || statusCode == HttpStatus.SC_INTERNAL_SERVER_ERROR) { // 请求参数错误或系统错误,返回失败的响应 JSONObject json = JSONObject.parseObject(body); return json.getString("message"); } else if (statusCode == HttpStatus.SC_FORBIDDEN) { // 权限问题,没有退款权限 return "没有退款权限"; } else if (statusCode == HttpStatus.SC_NOT_FOUND) { // 订单号不存在 return "订单号不存在"; } else if (statusCode == 429) { // 频率限制 return "退款请求频率过高,请稍后重试"; } else if (statusCode == HttpStatus.SC_PAYMENT_REQUIRED) { // 余额不足 return "商户余额不足,请充值后重试"; } else { // 其他状态码,返回通用的失败响应 return "退款失败,请稍后重试"; } } catch (Exception e) { log.info("支付模块-退款失败 = " + e.getMessage()); JSONObject json = JSONObject.parseObject(e.getMessage()); return json.getString("message"); } }}
支付回调解密处理
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.*;@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 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); } } 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) : ""; } 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
编写下单 退款 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;@Slf4j@RestController@RequestMapping("/pay")public class PayController { @Resource private WechatPayConfig wechatPayConfig; @Resource private 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.html payerMap.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>() { }); Object prepayId = resMap.get("prepay_id"); // 获取签名 String paySign; try { StringBuilder sb = new StringBuilder(); // 应用id sb.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<>(); // 小程序id map.put("appId", wechatPayConfig.getAppId()); // 时间戳 map.put("timeStamp", System.currentTimeMillis() / 1000); // 随机字符串 map.put("nonceStr", "56523268632356"); // 预支付交易会话ID map.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;@RestController@Slf4j@RequestMapping("/callback")public class CallbackController { @Resource private WechatPayConfig wechatPayConfig; @Resource private Verifier verifier; private final ReentrantLock lock = new ReentrantLock(); @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); } @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); // 转换为Map Map bodyMap = JSONObject.parseObject(body, new TypeReference>() { }); // 微信的通知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; }}
整体项目结构
请求预支付订单 得到预支付订单参数 小程序拿着这些参数拉起支付页面进行支付
支付完成后 微信会回调我们预留的回调接口
这里使用的是花生壳内网穿透进行回调测试的
退款业务就不一一截图了 图片太多显得文章拉胯
总结
来源地址:https://blog.csdn.net/T_2021_7_10/article/details/132276992
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341