前言
在对接JSAPI时遇到点问题,在此记录下
流程
1. 创建订单,并进行预下单,将时间戳,预支付id等信息保存到订单信息
2. 前端调起支付时,根据订单Id,返回支付所需参数,由前端拉起支付
3. 支付完后,编写回调接口,修改订单状态
4. 编写退款接口,进行退款
5. 编写退款回调接口,退款后进行更新订单
预支付
添加超时时间一定要将格式修改为 yyyy-MM-dd'T'HH:mm:ssXXX
并注意时区
private JsPayResultData wxPay(Integer totalAmountInCents, OrderInfo orderInfo, String openId) {
com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest prepayRequest = new com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest();
prepayRequest.setAppid(weChatConfig.getAppId());
prepayRequest.setMchid(weChatConfig.getMerchantId());
prepayRequest.setDescription(orderInfo.getOrderName()); // 订单描述
prepayRequest.setNotifyUrl(weChatConfig.getNotifyUrl()); // 回调地址
prepayRequest.setOutTradeNo(orderInfo.getOrderSn()); // 订单号
com.wechat.pay.java.service.payments.jsapi.model.Amount amount = new com.wechat.pay.java.service.payments.jsapi.model.Amount();
amount.setTotal(totalAmountInCents);
amount.setCurrency("CNY");
prepayRequest.setAmount(amount);
Payer payer = new Payer();
// TODO 开发完公众号登录,填充openid
if(openId == null){
throw new ServiceException("登录信息过期,请重新登录", 401);
}
payer.setOpenid(openId);
prepayRequest.setPayer(payer);
// 添加超时时间
// 当前时间加5分钟
prepayRequest.setTimeExpire(LocalDateTime.now()
.plusMinutes(5)
.atZone(ZoneId.systemDefault()) // 或者明确指定 ZoneId.of("Asia/Shanghai")
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX")));
// 调用下单方法,得到应答
try {
com.wechat.pay.java.service.payments.jsapi.model.PrepayResponse prepay = jsapiService.prepay(prepayRequest);
//组合jsapi下单参数
JsPayResultData jsPayResultData = new JsPayResultData();
jsPayResultData.setAppId(weChatConfig.getAppId());
jsPayResultData.setTimeStamp(String.valueOf(WeChatUtil.getCurrentTimestamp()));
jsPayResultData.setNonceStr(WeChatUtil.generateNonceStr());
jsPayResultData.setPrepayId("prepay_id=" + prepay.getPrepayId());
String privateKey = null;
try (InputStream inputStream = getClass().getResourceAsStream(weChatConfig.getPrivateKeyPath())){
privateKey = IOUtil.toString(inputStream);
} catch (IOException e) {
Log.error("IO异常, {}", e);
}
jsPayResultData.setPaySign(
WeChatUtil.getSign(
Stream.of(jsPayResultData.getAppId(), jsPayResultData.getTimeStamp(), jsPayResultData.getNonceStr(), jsPayResultData.getPrepayId()).collect(Collectors.joining("\n", "", "\n")),
privateKey,
weChatConfig.getMerchantSerialNumber()
)
);
//预支付成功,创建预支付订单
return jsPayResultData;
} catch (HttpException e) { // 发送HTTP请求失败
log.error("发送HTTP请求失败: {}", e.getHttpRequest());
} catch (ServiceException e) { // 服务返回状态小于200或大于等于300,例如500
log.error("服务返回状态异常: {}", e.getMessage());
} catch (MalformedMessageException e) { // 服务返回成功,返回体类型不合法,或者解析返回体失败
log.error("返回体类型不合法: {}", e.getMessage());
} catch (Exception e) {
log.error("预下单异常: {}", e.getMessage());
}
return null;
}
调起支付
String privateKey = null;
try (InputStream inputStream = getClass().getResourceAsStream(weChatConfig.getPrivateKeyPath())){
privateKey = IOUtil.toString(inputStream);
} catch (IOException e) {
Log.error("IO异常, {}", e);
}
String sign = WeChatUtil.getSign(
Stream.of(orderInfo.getAppId(), orderInfo.getTimeStamp(), orderInfo.getNonceStr(), orderInfo.getPrepayId()).collect(Collectors.joining("\n", "", "\n")),
privateKey,
weChatConfig.getMerchantSerialNumber()
);
踩坑
在微信的SDK中提供了PemUtil
工具类,里面有两个方法
loadPrivateKeyFromString
:加载秘钥通过String字符串的方式loadPrivateKeyFromPath
:加载通过路径的方式
开始对接时把秘钥放在Resource下通过class.getResource,本地测试可以通过
部署上线后,打成了jar包获取不到。如果把文件上传到服务器中并使用绝对路径就可以使用
所以推荐使用第一种loadPrivateKeyFromString
原因
问题一、
URL resource = Application.class.getResource("/cars.xlsx");
String path= resource.getPath();
// 结果为: E:/../target/classes/cars.xlsx
这个是由IDEA自己生成的一个路径,而在我们打包的时候问什么读取呢? 那么这个时候我们显然需要去看看打包之后的情况:
显然打包到的文件,不在com的同级目录下。
ok,这里我们就明白了一个道理,那就是,之所以找不到,那就是这个打包之后的文件位置放的不一样。那么问题提出来了,那么如何解决这个问题
问题二、
(1) getClass().getClassLoader().getResource()
- 返回值: 它返回一个
URL
对象。 - 特点:
getResource
查找的是资源的 绝对路径(相对于classpath
根目录)。- 如果资源找不到,则返回
null
。 - 返回的 URL 对象可能是
file://
协议或jar://
协议,具体取决于资源的位置。
- 常见问题:
- 资源在 Jar 包 中时,
URL
可能无法直接被转换为File
对象,因为 Jar 文件内的资源不是以文件形式存储,而是压缩在 Jar 包里。
- 资源在 Jar 包 中时,
(2) getClass().getClassLoader().getResourceAsStream()
- 返回值: 它返回一个
InputStream
对象。 - 特点:
- 直接返回资源的流,无论资源是在文件系统还是 Jar 包中。
- 更通用,因为它与资源的实际存储形式(文件还是压缩包)无关。
- 如果资源找不到,则返回
null
。
解决
解决一、
对于Class.getResource:
先获取文件的路径path,不以’/‘开头时,默认是从此类所在的包下取资源;path以’/’开头时,则是从项目的ClassPath根下获取资源。所以在当前打包之后的 话,我们找不到的,因为这个文件还在外面一层。
对于ClassLoader.getResource:
同样先获取文件的路径,path不以’/’开头时,首先通过双亲委派机制,使用的逐级向上委托的形式加载的,最后发现双亲没有加载到文件,最后通过当前类加载classpath根下资源文件。这样一来当前类没找到,但是老爹,老爷,太爷能找到。
在classLoader里面它的一个过程是这样的:
解决二、
getClass().getClassLoader().getResource("wechat/apiclient_key.pem")
返回的是一个 URL,但对于 Jar 内资源,URL 是以 jar://
协议开头的,这种 URL 不能直接用来创建一个 File
对象
getClass().getClassLoader().getResourceAsStream("wechat/apiclient_key.pem")
会直接返回资源的字节流。
无论资源是在文件系统中还是压缩在 Jar 文件中,getResourceAsStream
都能以流的形式访问资源。
因此,InputStream
是更适合处理类路径资源的方式,尤其是在运行环境中资源可能已经被打包成 Jar 文件。
支付回调
public static RequestParam handleNodifyRequestParam(HttpServletRequest request) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8));
String line ;
StringBuilder sb = new StringBuilder();
while ((line = br.readLine()) != null){
sb.append(line);
}
br.close();
String body = sb.toString();
// 请求头 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");
// 构造 RequestParam
return new RequestParam.Builder()
.body(body)
.nonce(nonce)
.serialNumber(serial)
.signature(signature)
.timestamp(timestamp)
.build();
}
}
踩坑
在回调时通过请求头获取到参数,获取参数是加密状态的,需要通过验签进行解密。
根据微信文档和SDK的步骤进行操作,发现验签失败
后续根据请求参数发现有乱码,进行UTF_8进行获取解决