关于对接JSAPI微信支付

Posted by ni on December 27, 2024
本文共6629 字 | 大约需要22.1 分钟阅读

前言

在对接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 包里。

(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进行获取解决