系列文章:
1、Java对接微信JS支付-V3版本接口
2、Java对接微信H5支付-V3版本接口
3、Java对接微信Native(扫码)支付-V3版本接口
前言: 对接完H5支付后因为无法满足所有场景的支付需求,所以还需要对接微信JS支付,特来记录一下整个对接过程和遇到的一些坑
适用场景: 微信客户端内部适用(博主的使用场景:公众号内嵌H5页面)
对接流程图:
-----------------------------------------------------对接开始-----------------------------------------------------
1. 查阅微信对接文档
1、JSAPI支付产品介绍
2、JSAPI下单
3、github代码参考
2. 数据准备
微信官网有详细的文档介绍,你需要准备那些东西
1、JSAPI支付接入前准备
3. Java代码
1. 引入微信官网依赖
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.2.12</version>
</dependency>
2. 编写WeChatConfig,将微信公共配置进行配置化
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.notification.NotificationConfig;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.service.payments.h5.H5Service;
import com.wechat.pay.java.service.payments.jsapi.JsapiService;
import com.wechat.pay.java.service.payments.nativepay.NativePayService;
import lombok.Getter;
/**
* @desc: 微信config
* @author: shy
* @date: 2024/4/9 10:06
*/
@Configuration
@Getter
public class WeChatConfig {
/**
* 商户号
*/
@Value("${wechat.pay.merchantId}")
public String merchantId;
/**
* 商户API私钥路径
*/
@Value("${wechat.pay.privateKeyPath}")
public String privateKeyPath;
/**
* 商户证书序列号
*/
@Value("${wechat.pay.merchantSerialNumber}")
public String merchantSerialNumber;
/**
* 商户APIV3密钥
*/
@Value("${wechat.pay.apiV3Key}")
public String apiV3Key;
/**
* AppId
*/
@Value("${wechat.pay.appId}")
public String appId;
private Config config;
@PostConstruct
public void initConfig() {
// 使用自动更新平台证书的RSA配置
// 一个商户号只能初始化一个配置,否则会因为重复的下载任务报错
config = new RSAAutoCertificateConfig.Builder()
.merchantId(merchantId)
.privateKeyFromPath(privateKeyPath)
.merchantSerialNumber(merchantSerialNumber)
.apiV3Key(apiV3Key)
.build();
}
@Primary
@Bean()
public H5Service h5Service() {
return new H5Service.Builder()
.config(config)
.build();
}
@Primary
@Bean()
public JsapiService jsapiService() {
return new JsapiService.Builder()
.config(config)
.build();
}
@Primary
@Bean()
public NativePayService nativePayService() {
return new NativePayService.Builder()
.config(config)
.build();
}
@Primary
@Bean
public NotificationParser notificationParser() {
return new NotificationParser((NotificationConfig) config);
}
}
3. 编写预下单接口
controller
@Autowired
WechatService wechatService;
@RequestMapping("/wxpay/v3/jsPay")
public Result<JsPayResultData> jsPay(Integer memberFeeType, HttpServletRequest request) {
MemberFeeTypeEnum memberFeeTypeEnum = MemberFeeTypeEnum.getMemberFeeTypeEnumByType(memberFeeType);
if (Objects.isNull(memberFeeTypeEnum)) {
return Result.fail(ResultEnum.PARAM_ERROR);
}
JsPayResultData jsPayResultData = wechatService.jsPay(memberFeeType, request, memberFeeTypeEnum);
if (Objects.isNull(jsPayResultData)) {
return Result.fail("支付失败");
}
return Result.success(jsPayResultData);
}
service
@Resource
private WechatPayOrderMapper wechatPayOrderMapper;
@Resource
private JsapiService jsapiService;
public String h5Pay(Integer memberFeeType, HttpServletRequest request, MemberFeeTypeEnum memberFeeTypeEnum) {
com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest prepayRequest = new com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest();
prepayRequest.setAppid(appId);
prepayRequest.setMchid(merchantId);
prepayRequest.setOutTradeNo(WeChatUtil.generateTradeNumber());
prepayRequest.setDescription(memberFeeTypeEnum.getDesc());
prepayRequest.setNotifyUrl(notifyUrl);
com.wechat.pay.java.service.payments.jsapi.model.Amount amount = new com.wechat.pay.java.service.payments.jsapi.model.Amount();
amount.setTotal(memberFeeTypeEnum.getPrice());
prepayRequest.setAmount(amount);
Payer payer = new Payer();
payer.setOpenid("openid");
prepayRequest.setPayer(payer);
// 调用下单方法,得到应答
PrepayResponse response;
try {
com.wechat.pay.java.service.payments.jsapi.model.PrepayResponse prepay = jsapiService.prepay(prepayRequest);
//组合jsapi下单参数
JsPayResultData jsPayResultData = new JsPayResultData();
jsPayResultData.setAppId(appId);
jsPayResultData.setTimeStamp(String.valueOf(WeChatUtil.getCurrentTimestamp()));
jsPayResultData.setNonceStr(WeChatUtil.generateNonceStr());
jsPayResultData.setPrepayId("prepay_id=" + prepay.getPrepayId());
jsPayResultData.setPaySign(WeChatUtil.getSign(
Stream.of(jsPayResultData.getAppId(), jsPayResultData.getTimeStamp(), jsPayResultData.getNonceStr(), jsPayResultData.getPrepayId()).collect(Collectors.joining("\n", "", "\n")),
privateKeyPath,
merchantSerialNumber)
);
//预支付成功,创建预支付订单
int add = addWechatPayOrder(request, prepayRequest.getOutTradeNo(), memberFeeType, memberFeeTypeEnum.getPrice(),WeChatPayTradeTypeEnum.公众号支付);
if (add <= 0) {
return null;
}
return jsPayResultData;
} catch (HttpException e) { // 发送HTTP请求失败
log.error("发送HTTP请求失败: {}", e.getHttpRequest());
} catch (ServiceException e) { // 服务返回状态小于200或大于等于300,例如500
log.error("服务返回状态异常: {}", e.getResponseBody());
} catch (MalformedMessageException e) { // 服务返回成功,返回体类型不合法,或者解析返回体失败
log.error("返回体类型不合法: {}", e.getMessage());
} catch (Exception e) {
log.error("预下单异常: {}", e.getMessage());
}
return null;
}
4. 编写回调接口
controller
@RequestMapping("/v3/payNotify")
public WeChatPayResult payNotify(HttpServletRequest request) {
try {
return wechatService.payNotify(request);
} catch (Exception e) {
log.error("微信回调异常: ", e);
return new WeChatPayResult("FAIL", "FAIL");
}
}
service
@Resource
private NotificationParser notificationParser;
@Resource
private WechatPayOrderMapper wechatPayOrderMapper;
@Transactional
public WeChatPayResult payNotify(HttpServletRequest request) throws Exception{
log.info("微信回调开始-------------------------");
Transaction transaction;
try {
transaction = notificationParser.parse(WeChatUtil.handleNodifyRequestParam(request), Transaction.class);
if (transaction.getTradeState() == Transaction.TradeStateEnum.SUCCESS) {
WechatPayOrder wechatPayOrder = wechatPayOrderMapper.getOrderByTradeNo(transaction.getOutTradeNo());
if (Objects.isNull(wechatPayOrder)) {
log.error("订单不存在, 订单号: {}", transaction.getOutTradeNo());
return new WeChatPayResult("SUCCESS", "ORDER_DOES_NOT_EXIST");
}
//校验订单状态:若订单已支付则直接返回成功
if (Objects.equals(wechatPayOrder.getTradeStatus(), WeChatPayTradeStatusEnum.已支付.getType())) {
log.error("订单已支付, 订单号: {}", transaction.getOutTradeNo());
return new WeChatPayResult("SUCCESS", "ORDER_PAID");
}
//支付成功-修改订单状态
wechatPayOrderMapper.updateTradeStatus(transaction.getOutTradeNo(), transaction.getTransactionId(), wechatPayOrder.getVersion());
//支付成功-添加会员
MemberFeeTypeEnum memberFeeTypeEnum = MemberFeeTypeEnum.getMemberFeeTypeEnumByType(wechatPayOrder.getMemberFeeType());
userAwardService.addAward(wechatPayOrder.getUserId(), memberFeeTypeEnum.getMemberDays());
}
} catch (ValidationException e) {
// 签名验证失败,返回 401 UNAUTHORIZED 状态码
log.error("签名验证失败: ", e);
return new WeChatPayResult("FAIL", "UNAUTHORIZED");
}
log.info("微信回调结束, 微信回调报文: {}", transaction);
return new WeChatPayResult("SUCCESS", "SUCCESS");
}
5. 编写微信工具类
微信的依赖中有很多好用的工具类,但是在他们的文档中都没有体现,需要耐心地找一下
WeChatUtil
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.util.Random;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import com.wechat.pay.java.core.cipher.RSASigner;
import com.wechat.pay.java.core.cipher.SignatureResult;
import com.wechat.pay.java.core.notification.RequestParam;
import com.wechat.pay.java.core.util.NonceUtil;
import com.wechat.pay.java.core.util.PemUtil;
/**
* @desc: 微信工具类
* @author: shy
* @date: 2024/4/8 16:10
*/
public class WeChatUtil {
private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final Random RANDOM = new SecureRandom();
/**
* 生成订单号
*
* @param
* @return String
* @author shy
* @date 2024/4/8 16:15
*/
public static String generateTradeNumber() {
// 定义订单号前缀
String prefix = "shy";
// 当前年月日
String currentTimeStr = DateUtil.getCurrent("yyyyMMddHHmmss");
// 获取当前时间戳
long timestamp = System.currentTimeMillis();
// 构造订单号
return prefix + currentTimeStr + timestamp;
}
/**
* 获取随机字符串 Nonce Str
*
* @param
* @return String
* @author shy
* @date 2024/4/16 17:07
*/
public static String generateNonceStr() {
return NonceUtil.createNonce(32);
}
/**
* 获取当前时间戳,单位秒
* @param
* @return long
* @author shy
* @date 2024/4/16 17:10
*/
public static long getCurrentTimestamp() {
return System.currentTimeMillis() / 1000;
}
public static String getSign(String signatureStr, String privateKeyPath, String merchantSerialNumber) {
PrivateKey privateKey = PemUtil.loadPrivateKeyFromPath(privateKeyPath);
RSASigner rsaSigner = new RSASigner(merchantSerialNumber, privateKey);
SignatureResult signatureResult = rsaSigner.sign(signatureStr);
return signatureResult.getSign();
}
/**
* 构造 RequestParam
*
* @param request
* @return RequestParam
* @author shy
* @date 2024/4/9 11:16
*/
public static RequestParam handleNodifyRequestParam(HttpServletRequest request) throws IOException {
// 请求头Wechatpay-Signature
String signature = request.getHeader("Wechatpay-Signature");
// 请求头Wechatpay-nonce
String nonce = request.getHeader("Wechatpay-Nonce");
// 请求头Wechatpay-Timestamp
String timestamp = request.getHeader("Wechatpay-Timestamp");
// 微信支付证书序列号
String serial = request.getHeader("Wechatpay-Serial");
// 签名方式
String signType = request.getHeader("Wechatpay-Signature-Type");
// 构造 RequestParam
return new RequestParam.Builder().serialNumber(serial).nonce(nonce).signature(signature).timestamp(timestamp).signType(signType).body(getRequestBody(request)).build();
}
public static String getRequestBody(HttpServletRequest request) throws IOException {
ServletInputStream stream;
BufferedReader reader = null;
StringBuilder sb = new StringBuilder();
try {
stream = request.getInputStream();
// 获取响应
reader = new BufferedReader(new InputStreamReader(stream));
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
throw new IOException("读取返回支付接口数据流出现异常!");
} finally {
if (reader != null) {
reader.close();
}
}
return sb.toString();
}
}
6. 测试接口
测试需要使用互联网可以连通的域名,所以我是在生产环境新创建了一个二级域名做的测试,并且回调地址也必须是你申请审核通过的支付域名才可以。
7. 总结
可能是我阅读文档的方式不太对,微信支付的官方开发文档写的真是一言难尽,看了好长时间才大致看明白。所以写下这篇文章,愿后面开发的兄弟们对接起来都可以开开心心,顺顺利利!一次成功!