前言
微信支付的开发文档每次看都觉得无从下手,看了很多的博客,终于跑通了,现在记录一下,以便下次使用。
准备工作
准备工作可以自己去网上百度,这里主要写的是后端请求支付。
springboot方面
maven引入wechatpay-java
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.2.6</version>
</dependency>
<!--json处理器-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
下面的两个类是重点,很多网上的博客都是因为没有这两个类才跑不通的,我也是自己下载了官方的实例,从里面找的
CloseOrderRequest类
// Copyright 2021 Tencent Inc. All rights reserved.
//
// Native支付
//
// Native支付API
//
// API version: 1.2.3
// Code generated by WechatPay APIv3 Generator based on [OpenAPI
// Generator](https://openapi-generator.tech); DO NOT EDIT.
package cn.lasons.eps.wxpay.model;
import static com.wechat.pay.java.core.util.StringUtil.toIndentedString;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/** CloseOrderRequest */
public class CloseOrderRequest {
/** outTradeNo */
@SerializedName("out_trade_no")
@Expose(serialize = false)
private String outTradeNo;
/** mchid 说明:直连商户号 */
@SerializedName("mchid")
private String mchid;
public String getOutTradeNo() {
return outTradeNo;
}
public void setOutTradeNo(String outTradeNo) {
this.outTradeNo = outTradeNo;
}
public String getMchid() {
return mchid;
}
public void setMchid(String mchid) {
this.mchid = mchid;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("class CloseOrderRequest {\n");
sb.append(" outTradeNo: ").append(toIndentedString(outTradeNo)).append("\n");
sb.append(" mchid: ").append(toIndentedString(mchid)).append("\n");
sb.append("}");
return sb.toString();
}
}
PrepayRequest类
// Copyright 2021 Tencent Inc. All rights reserved.
//
// Native支付
//
// Native支付API
//
// API version: 1.2.3
// Code generated by WechatPay APIv3 Generator based on [OpenAPI
// Generator](https://openapi-generator.tech); DO NOT EDIT.
package cn.lasons.eps.wxpay.model;
import static com.wechat.pay.java.core.util.StringUtil.toIndentedString;
import com.google.gson.annotations.SerializedName;
import com.wechat.pay.java.service.lovefeast.model.Amount;
import com.wechat.pay.java.service.partnerpayments.app.model.Detail;
import com.wechat.pay.java.service.partnerpayments.app.model.SceneInfo;
import com.wechat.pay.java.service.partnerpayments.app.model.SettleInfo;
import java.util.List;
/** PrepayRequest */
public class PrepayRequest {
/** 公众号ID 说明:公众号ID */
@SerializedName("appid")
private String appid;
/** 直连商户号 说明:直连商户号 */
@SerializedName("mchid")
private String mchid;
/** 商品描述 说明:商品描述 */
@SerializedName("description")
private String description;
/** 商户订单号 说明:商户订单号 */
@SerializedName("out_trade_no")
private String outTradeNo;
/** 交易结束时间 说明:订单失效时间,格式为rfc3339格式 */
@SerializedName("time_expire")
private String timeExpire;
/** 附加数据 说明:附加数据 */
@SerializedName("attach")
private String attach;
/** 通知地址 说明:有效性:1. HTTPS;2. 不允许携带查询串。 */
@SerializedName("notify_url")
private String notifyUrl;
/** 订单优惠标记 说明:商品标记,代金券或立减优惠功能的参数。 */
@SerializedName("goods_tag")
private String goodsTag;
/** limitPay */
public enum LimitPayEnum {
@SerializedName("no_balance")
NO_BALANCE,
@SerializedName("no_credit")
NO_CREDIT,
@SerializedName("no_debit")
NO_DEBIT,
@SerializedName("balance_only")
BALANCE_ONLY
}
@SerializedName("limit_pay")
private List<LimitPayEnum> limitPay;
/** 电子发票入口开放标识 说明:传入true时,支付成功消息和支付详情页将出现开票入口。需要在微信支付商户平台或微信公众平台开通电子发票功能,传此字段才可生效。 */
@SerializedName("support_fapiao")
private Boolean supportFapiao;
/** amount */
@SerializedName("amount")
private Amount amount;
/** detail */
@SerializedName("detail")
private Detail detail;
/** settleInfo */
@SerializedName("settle_info")
private SettleInfo settleInfo;
/** sceneInfo */
@SerializedName("scene_info")
private SceneInfo sceneInfo;
public String getAppid() {
return appid;
}
public void setAppid(String appid) {
this.appid = appid;
}
public String getMchid() {
return mchid;
}
public void setMchid(String mchid) {
this.mchid = mchid;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getOutTradeNo() {
return outTradeNo;
}
public void setOutTradeNo(String outTradeNo) {
this.outTradeNo = outTradeNo;
}
public String getTimeExpire() {
return timeExpire;
}
public void setTimeExpire(String timeExpire) {
this.timeExpire = timeExpire;
}
public String getAttach() {
return attach;
}
public void setAttach(String attach) {
this.attach = attach;
}
public String getNotifyUrl() {
return notifyUrl;
}
public void setNotifyUrl(String notifyUrl) {
this.notifyUrl = notifyUrl;
}
public String getGoodsTag() {
return goodsTag;
}
public void setGoodsTag(String goodsTag) {
this.goodsTag = goodsTag;
}
public List<LimitPayEnum> getLimitPay() {
return limitPay;
}
public void setLimitPay(List<LimitPayEnum> limitPay) {
this.limitPay = limitPay;
}
public Boolean getSupportFapiao() {
return supportFapiao;
}
public void setSupportFapiao(Boolean supportFapiao) {
this.supportFapiao = supportFapiao;
}
public Amount getAmount() {
return amount;
}
public void setAmount(Amount amount) {
this.amount = amount;
}
public Detail getDetail() {
return detail;
}
public void setDetail(Detail detail) {
this.detail = detail;
}
public SettleInfo getSettleInfo() {
return settleInfo;
}
public void setSettleInfo(SettleInfo settleInfo) {
this.settleInfo = settleInfo;
}
public SceneInfo getSceneInfo() {
return sceneInfo;
}
public void setSceneInfo(SceneInfo sceneInfo) {
this.sceneInfo = sceneInfo;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("class PrepayRequest {\n");
sb.append(" appid: ").append(toIndentedString(appid)).append("\n");
sb.append(" mchid: ").append(toIndentedString(mchid)).append("\n");
sb.append(" description: ").append(toIndentedString(description)).append("\n");
sb.append(" outTradeNo: ").append(toIndentedString(outTradeNo)).append("\n");
sb.append(" timeExpire: ").append(toIndentedString(timeExpire)).append("\n");
sb.append(" attach: ").append(toIndentedString(attach)).append("\n");
sb.append(" notifyUrl: ").append(toIndentedString(notifyUrl)).append("\n");
sb.append(" goodsTag: ").append(toIndentedString(goodsTag)).append("\n");
sb.append(" limitPay: ").append(toIndentedString(limitPay)).append("\n");
sb.append(" supportFapiao: ").append(toIndentedString(supportFapiao)).append("\n");
sb.append(" amount: ").append(toIndentedString(amount)).append("\n");
sb.append(" detail: ").append(toIndentedString(detail)).append("\n");
sb.append(" settleInfo: ").append(toIndentedString(settleInfo)).append("\n");
sb.append(" sceneInfo: ").append(toIndentedString(sceneInfo)).append("\n");
sb.append("}");
return sb.toString();
}
}
接下来就是参数的填写了
package cn.lasons.eps.wxpay;
import cn.lasons.eps.wxpay.model.PrepayRequest;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.service.lovefeast.model.Amount;
import com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class WXPayConstants {
/*appid*/
public static final String APP_ID = "";
/*商户号*/
public static final String MCH_ID = "";
/* 商户证书序列号 */
public static final String MCH_SERIAL_NO = "";
/*APIv3密钥*/
public static final String API_V3KEY = "";
/*支付成功回调地址*/
public static final String NOTIFY_URL = "xxxx/wxPayNotify";
/*序列号地址*/
public static final String PRIVATE_KEY_PATH = "apiclient_key.pem";
private static NativePayService service;
/**
* 微信预支付
*/
public static Map<String, String> nativePayV3(Integer money, String outTradeNo) {
if (service == null) {
Config config = new RSAAutoCertificateConfig.Builder()
.merchantId(MCH_ID)
.privateKeyFromPath(PRIVATE_KEY_PATH)
.merchantSerialNumber(MCH_SERIAL_NO)
.apiV3Key(API_V3KEY)
.build();
service = new NativePayService.Builder().config(config).build();
}
PrepayRequest request = new PrepayRequest();
Amount amount = new Amount();
amount.setTotal(money);
request.setAmount(amount);
request.setAppid(APP_ID);
request.setMchid(MCH_ID);
request.setDescription("积分充值");
request.setNotifyUrl(NOTIFY_URL);
request.setOutTradeNo(outTradeNo);
PrepayResponse response = service.prepay(request);
Map<String, String> payParameters = new HashMap<>();
payParameters.put("code_url", response.getCodeUrl());
return payParameters;
}
/**验签*/
public static boolean signVerify(String serial, String message, String signature) {
// 从证书管理器中获取verifier
Verifier verifier = null;
try {
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new FileInputStream(ResourceUtils.getFile(WXPayConstants.PRIVATE_KEY_PATH)));
// 获取证书管理器实例
CertificatesManager certificatesManager = CertificatesManager.getInstance();
// 向证书管理器增加需要自动更新平台证书的商户信息
certificatesManager.putMerchant(WXPayConstants.MCH_ID, new WechatPay2Credentials(WXPayConstants.MCH_ID,
new PrivateKeySigner(WXPayConstants.MCH_SERIAL_NO, merchantPrivateKey)), WXPayConstants.API_V3KEY.getBytes(StandardCharsets.UTF_8));
// 从证书管理器中获取verifier
verifier = certificatesManager.getVerifier(WXPayConstants.MCH_ID);
return verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature);
} catch (IOException e) {
log.error("验签发送异常", e);
} catch (GeneralSecurityException e) {
log.error("验签发送异常", e);
} catch (HttpCodeException e) {
log.error("验签发送异常", e);
} catch (NotFoundException e) {
log.error("验签发送异常", e);
}
return false;
}
}
万事俱备,可以直接创建post请求
@PostMapping("/wx/pay_native_v3_demo")
public Map<String, String> nativeV3Demo(@RequestBody PayBody data) {
return WXPayConstants.nativePayV3(1, "20231020103211111");
}
@PostMapping(value = "/wxPayNotify")
public Map payCallBack(HttpServletRequest request,
@RequestBody WeChatPayResultIn weChatPayResultIn) {
Map result = new HashMap();
result.put("code", wxPayService.wechatPayCallBack(weChatPayResultIn, request));
return result;
}
WeChatPayResultIn
package cn.lasons.eps.wxpay;
import lombok.Data;
@Data
public class WeChatPayResultIn {
private String id;
private String create_time;
private String resource_type;
private String event_type;
private String summary;
private WechatPayResourceIn resource;
}
WechatPayResourceIn 类
package cn.lasons.eps.wxpay;
import lombok.Data;
@Data
public class WechatPayResourceIn {
private String original_type;
private String algorithm;
private String ciphertext;
private String associated_data;
private String nonce;
}
wechatPayCallBack方法如下
@Transactional
public Map wechatPayCallBackDemo(WeChatPayResultIn weChatPayResultIn, HttpServletRequest request) {
log.error("微信结果通知字符串:{}" + JSON.toJSONString(weChatPayResultIn));
String nonce = weChatPayResultIn.getResource().getNonce();
String ciphertext = weChatPayResultIn.getResource().getCiphertext();
String associated_data = weChatPayResultIn.getResource().getAssociated_data();
AesUtil aesUtil = new AesUtil(WXPayConstants.API_V3KEY.getBytes());
Map result = new HashMap();
result.put("code", "FAIL");
BufferedReader reader = null;
try {
//验签
StringBuilder signStr = new StringBuilder();
// 请求时间戳\n
signStr.append(request.getHeader("Wechatpay-Timestamp")).append("\n");
// 请求随机串\n
signStr.append(request.getHeader("Wechatpay-Nonce")).append("\n");
reader = request.getReader();
log.error("reader:=========>" + reader.toString());
String str = null;
StringBuilder builder = new StringBuilder();
while ((str = reader.readLine()) != null) {
builder.append(str);
}
log.error("请求报文主体: {}", builder);
signStr.append(builder.toString()).append("\n");
// 1.验签
if (!WXPayConstants.signVerify(request.getHeader("Wechatpay-Serial"), signStr.toString(), request.getHeader("Wechatpay-Signature"))) {
result.put("message", "sign error");
log.error("验签失败-sign error");
return result;
}
// 解密请求体
String s = aesUtil.decryptToString(associated_data.getBytes(), nonce.getBytes(), ciphertext);
log.error("解密结果");
log.error(s);
JSONObject jsonObject = JSONObject.parseObject(s);
log.error("json格式化的结果");
log.error(jsonObject.toJSONString());
//订单号
String orderId = jsonObject.getString("out_trade_no");
//微信支付订单号
String transactionId = jsonObject.getString("transaction_id");
String tradeState = jsonObject.getString("trade_state");
//幂等性
if (StringUtils.equals(tradeState, "USERPAYING")) {
//还在支付中,不做处理
return null;
}
if (StringUtils.equals(tradeState, "SUCCESS")) {
// TODO: 2023/10/20 自己的业务逻辑
result.put("code", "OK");
return result;
}
} catch (GeneralSecurityException e) {
log.error("微信支付回调数据处理及自己项目逻辑异常", e);
result.put("message", e.getMessage());
} catch (IOException e) {
log.error("微信支付回调获取body流异常", e);
result.put("message", e.getMessage());
}
return result;
}
成功的实例如下
{
"code_url": "weixin://wxpay/bizpayurl?pr=pZ053QLxxx"
}
看到这个就完成大半了,直接在网上找一个链接转二维码的网站,将后面的weixin:xxx复制,然后就能生成二维码,再用手机的微信扫码,就可以完成支付了。
vue方面
vue主要就是发起一个axios请求,然后将获取到的链接转换为二维码即可,操作如下:
安装链接转二维码
npm install qrcodejs2 --save
写一个div元素去显示二维码
<div id="qrcode" ref="qrcode" />
发起请求
handPay(item) {
pay().then(res => {
console.log('查看返回的数据', res)
this.init(res.data.code_url)
}).catch(console.error)
},
init(code) {
document.getElementById('qrcode').innerHTML = ''
this.qrcode = new QRCode('qrcode', {
width: 200, // 二维码宽度
height: 200, // 二维码高度
text: code
})
},
axios请求
import request from '@/utils/request'
export function pay(data) {
return request({
url: '/wx/pay_native_v3',
method: 'post',
data: data
})
}
这里的request是自己封装axios的,代码如下。里面的getToken可以直接换成自己的token,这里不再赘述。
import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
// create an axios instance
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_URL, // 自己项目的地址
withCredentials: true, // send cookies when cross-domain requests
timeout: 10000 // request timeout
})
// request interceptor
service.interceptors.request.use(
config => {
// do something before request is sent
if (store.getters.token) {
// let each request carry token
// ['X-Token'] is a custom headers key
// please modify it according to the actual situation
config.headers['token'] = getToken()
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
// response interceptor
service.interceptors.response.use(
response => {
const { data } = response
// if the custom code is not 20000, it is judged as an error.
if (data.code != 20000) {
Message({
message: data.data || data.msg || 'Error',
type: 'error',
duration: 5 * 1000
})
},
error => {
console.log('err' + error) // for debug
Message({
message: error.msg || error.data,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export default service
结语
至此,一次简单的vue+springboot对接微信支付的native方式就成功了。回头看,很多的博客都是忽略一些东西,看的云里雾里的。跑通了就自己记录下来,下次少绕点圈子。