微信小程序支付——Java 实现V3

最近接入了微信小程序的微信支付, 之前用的是 V2 版本,这两天用的V3 版本。也搞了不少时间这里就记录一下。
首先是调用下单接口,拿到对应的预支付id,然后通过预支付id ,获取到对应的支付参数,最后将参数返回到前端。
在这里插入图片描述
导入对应的sdk

        <!-- 微信支付SDK -->
        <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-apache-httpclient</artifactId>
            <version>0.4.7</version> <!-- 使用最新稳定版 -->
        </dependency>

初始化配置微信支付


package com.trailer.common.config.wechat;

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.util.PemUtil;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;

import java.io.InputStream;
import java.security.PrivateKey;

/**
 * 微信支付配置属性类,用于绑定yml配置文件
 */
@Configuration

public class WechatPayConfig {
    @Value("${wx.pay.merchant-id}")
    private String merchantId;

    @Value("${wx.pay.private-key-path:classpath:apiclient_key.pem}")
    private String privateKeyPath;

    @Value("${wx.pay.merchant-serial-number}")
    private String merchantSerialNumber;

    @Value("${wx.pay.api-v3-key}")
    private String apiV3Key;

    @Value("${wx.pay.app-id}")
    private String appId;

    @Value("${wx.pay.mch-key}")
    private String mchKey;

    @Value("${wx.pay.cert-path:classpath:/apiclient_cert.p12}")
    private String certPath;

    @Value("${wx.pay.notify-url}")
    private String notifyUrl;

    @Value("${wx.pay.v3-base-url:https://api.mch.weixin.qq.com/v3}")
    private String v3BaseUrl;

    // Getter和Setter方法
    public String getMerchantId() {
        return merchantId;
    }

    public void setMerchantId(String merchantId) {
        this.merchantId = merchantId;
    }

    public String getPrivateKeyPath() {
        return privateKeyPath;
    }

    public void setPrivateKeyPath(String privateKeyPath) {
        this.privateKeyPath = privateKeyPath;
    }

    public String getMerchantSerialNumber() {
        return merchantSerialNumber;
    }

    public void setMerchantSerialNumber(String merchantSerialNumber) {
        this.merchantSerialNumber = merchantSerialNumber;
    }

    public String getApiV3Key() {
        return apiV3Key;
    }

    public void setApiV3Key(String apiV3Key) {
        this.apiV3Key = apiV3Key;
    }

    public String getAppId() {
        return appId;
    }

    public void setAppId(String appId) {
        this.appId = appId;
    }

    public String getMchKey() {
        return mchKey;
    }

    public void setMchKey(String mchKey) {
        this.mchKey = mchKey;
    }

    public String getCertPath() {
        return certPath;
    }

    public void setCertPath(String certPath) {
        this.certPath = certPath;
    }

    public String getNotifyUrl() {
        return notifyUrl;
    }

    public void setNotifyUrl(String notifyUrl) {
        this.notifyUrl = notifyUrl;
    }

    public String getV3BaseUrl() {
        return v3BaseUrl;
    }

    public void setV3BaseUrl(String v3BaseUrl) {
        this.v3BaseUrl = v3BaseUrl;
    }

    /**
     * 初始化商户私钥
     */
    @Bean
    public PrivateKey privateKey() throws Exception {
        Resource resource = new ClassPathResource(privateKeyPath.replace("classpath:", ""));
        try (InputStream inputStream = resource.getInputStream()) {
            return PemUtil.loadPrivateKey(inputStream);
        }
    }

    /**
     * 初始化HTTP客户端
     */
    @Bean
    public CloseableHttpClient httpClient(PrivateKey privateKey) throws Exception {
        // 加载平台证书
        CertificatesManager certificatesManager = CertificatesManager.getInstance();
        certificatesManager.putMerchant(merchantId,
                new WechatPay2Credentials(merchantId, new PrivateKeySigner(merchantSerialNumber, privateKey)),
                apiV3Key.getBytes("UTF-8"));

        // 获取证书验证器
        Verifier verifier = certificatesManager.getVerifier(merchantId);

        // 构建HTTP客户端
        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant(merchantId, merchantSerialNumber, privateKey)
                .withValidator(new WechatPay2Validator(verifier));

        return builder.build();
    }
}


配置微信支付的加密和解密、签名

package com.trailer.client.service;


import com.alibaba.fastjson2.JSONObject;
import com.trailer.client.dto.request.TransferSceneReportInfo;
import com.trailer.common.config.wechat.WechatPayConfig;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.Signature;
import java.util.Base64;
import java.util.List;
import java.util.Random;
import java.util.UUID;

/**
 * 微信支付服务类,封装微信支付相关操作
 */
@Service
public class WechatPayService {

    @Autowired
    private WechatPayConfig wechatPayConfig;


    @Autowired
    private CloseableHttpClient httpClient;

    /**
     * 生成随机字符串
     */
    public String generateNonceStr() {
        return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
    }

    /**
     * 生成商户订单号
     */
    public String generateOutTradeNo() {
        return "ORD" + System.currentTimeMillis() + new Random().nextInt(1000);
    }


    /**
     * 小程序 v3 接口
     *
     */
   public  String signV3MiniProgramSign(String appid, long timestamp, String nonceStr, String body) throws Exception{

        //从下往上依次生成
       String signatureStr = String.format("%s\n%d\n%s\n%s\n",
               appid, timestamp, nonceStr, body);
        //签名
       Signature signature = Signature.getInstance("SHA256withRSA");
       signature.initSign(wechatPayConfig.privateKey());
       signature.update(signatureStr.getBytes(StandardCharsets.UTF_8));

       return Base64.getEncoder().encodeToString(signature.sign());
    }

    /**
     * 调用微信支付V3接口
     */
    public String callV3Api(String path, JSONObject params) throws Exception {
        String url = wechatPayConfig.getV3BaseUrl() + path;
        HttpPost httpPost = new HttpPost(url);

        // 设置请求头
        long timestamp = System.currentTimeMillis() / 1000;
        String nonceStr = generateNonceStr();
        String body = params.toJSONString();

        String signature = signV3("POST", path, timestamp, nonceStr, body);
        System.out.println("签名结果:" + signature);
        httpPost.setHeader("Content-Type", "application/json");
        httpPost.setHeader("Accept", "application/json");
        httpPost.setHeader("Wechatpay-Timestamp", String.valueOf(timestamp));
        httpPost.setHeader("Wechatpay-Nonce", nonceStr);
        httpPost.setHeader("Wechatpay-Signature", signature);
        httpPost.setHeader("Wechatpay-Serial", wechatPayConfig.getMerchantSerialNumber());

        // 设置请求体
        httpPost.setEntity(new StringEntity(body, "UTF-8"));

        // 发送请求
        try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
            String responseBody = EntityUtils.toString(response.getEntity());

            if (response.getStatusLine().getStatusCode() != 200) {
                throw new RuntimeException("调用微信支付接口失败: " + responseBody);
            }

            return responseBody;
        }
    }

    /**
     * 解密微信支付通知数据
     */
    public String decryptNotifyData(String associatedData, String nonce, String ciphertext) throws Exception {
        SecretKeySpec key = new SecretKeySpec(
                wechatPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8),
                "AES"
        );
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");

        byte[] nonceBytes = nonce.getBytes(StandardCharsets.UTF_8);
        byte[] associatedDataBytes = associatedData.getBytes(StandardCharsets.UTF_8);
        byte[] ciphertextBytes = Base64.getDecoder().decode(ciphertext);

        // GCM参数
        GCMParameterSpec parameterSpec = new GCMParameterSpec(128, nonceBytes);

        cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec);
        cipher.updateAAD(associatedDataBytes);

        return new String(cipher.doFinal(ciphertextBytes), StandardCharsets.UTF_8);
    }

    /**
     * 小程序支付下单
     */
    public JSONObject createMiniProgramOrder(String description, String outTradeNo,
                                             int totalAmount, String openid) throws Exception {
        JSONObject params = new JSONObject();
        params.put("appid", wechatPayConfig.getAppId());
        params.put("mchid", wechatPayConfig.getMerchantId());
        params.put("description", description);
        params.put("out_trade_no", outTradeNo);
        params.put("notify_url", wechatPayConfig.getNotifyUrl());

        JSONObject amount = new JSONObject();
        amount.put("total", totalAmount); // 单位:分
        amount.put("currency", "CNY");
        params.put("amount", amount);

        JSONObject payer = new JSONObject();
        payer.put("openid", openid);
        params.put("payer", payer);

        String response = callV3Api("/pay/transactions/jsapi", params);
        return JSONObject.parseObject(response);
    }

    /**
     * 申请退款
     */
    public JSONObject refund(String outTradeNo, String outRefundNo,
                             int totalAmount, int refundAmount) throws Exception {
        JSONObject params = new JSONObject();
        params.put("out_trade_no", outTradeNo);
        params.put("out_refund_no", outRefundNo);

        JSONObject amount = new JSONObject();
        amount.put("total", totalAmount);
        amount.put("refund", refundAmount);
        amount.put("currency", "CNY");
        params.put("amount", amount);

        params.put("notify_url", wechatPayConfig.getNotifyUrl());

        String response = callV3Api("/refund/domestic/refunds", params);
        return JSONObject.parseObject(response);
    }

    /**
     * 商家转账到零钱
     */
    public JSONObject transferToBalance(String outBatchNo, String outDetailNo,
                                        String openid ,String userName , int amount, String description) throws Exception {
        JSONObject params = new JSONObject();
        params.put("appid", wechatPayConfig.getAppId());
        params.put("out_batch_no", outBatchNo);
        params.put("batch_name", "商家转账");
        params.put("batch_remark", description);
        params.put("total_amount", amount);
        params.put("total_num", 1);
        params.put("notify_url", wechatPayConfig.getNotifyUrl());

        JSONObject transferDetail = new JSONObject();
        transferDetail.put("out_detail_no", outDetailNo);
        transferDetail.put("transfer_amount", amount);
        transferDetail.put("transfer_remark", description);
        transferDetail.put("openid", openid);
        if (shouldIncludeUserName(amount, userName)) {
            // 对用户姓名进行加密处理
            String encryptedName = encryptUserName(userName);
            transferDetail.put("user_name", encryptedName);
        }

        params.put("transfer_detail_list", new Object[]{transferDetail});

        String response = callV3Api("/fund-app/mch-transfer/transfer-bills", params);
        return JSONObject.parseObject(response);
    }


    /**
     * 商家转账到零钱
     */
    public JSONObject transferToBalance(String outBillNo, String transferSceneId,
                                        String openid, String userName, int amount,
                                        String description, String userRecvPerception,
                                        List<TransferSceneReportInfo> reportInfos) throws Exception {
        JSONObject params = new JSONObject();
        params.put("appid", wechatPayConfig.getAppId());
        params.put("out_bill_no", outBillNo);
        params.put("transfer_scene_id", transferSceneId);
        params.put("openid", openid);
        params.put("transfer_amount", amount);
        params.put("transfer_remark", description);
        params.put("notify_url", wechatPayConfig.getNotifyUrl());

        if (userRecvPerception != null) {
            params.put("user_recv_perception", userRecvPerception);
        }

        if (shouldIncludeUserName(amount, userName)) {
            String encryptedName = encryptUserName(userName);
            params.put("user_name", encryptedName);
        }

        params.put("transfer_scene_report_infos", reportInfos);

        String response = callV3Api("/fund-app/mch-transfer/transfer-bills", params);
        return JSONObject.parseObject(response);
    }

    /**
     * 判断是否应该包含用户姓名
     *
     * @param amount 转账金额(分)
     * @param userName 用户姓名
     * @return 是否应该包含用户姓名
     */
    private boolean shouldIncludeUserName(int amount, String userName) {
        // 如果没有用户提供姓名,则不包含
        if (userName == null || userName.trim().isEmpty()) {
            return false;
        }

        // 金额小于30分(0.3元)时,不允许填写收款用户姓名
        if (amount < 30) {
            return false;
        }

        // 金额大于等于200000分(2000元)时,必须填写收款用户姓名
        if (amount >= 200000) {
            return true;
        }

        // 0.3元 <= 金额 < 2000元时,可选填写收款用户姓名
        // 这里我们选择填写,以提高安全性
        return true;
    }


    /**
     * 对用户姓名进行加密
     *
     * @param userName 用户姓名
     * @return 加密后的用户姓名
     * @throws Exception 加密异常
     */
    private String encryptUserName(String userName) throws Exception {
        // 微信支付要求使用平台证书对敏感信息进行加密
        // 这里需要实现具体的加密逻辑
        // 暂时返回原值,实际项目中需要根据微信支付文档实现加密
        return encryptWithWechatPublicKey(userName);
    }

    /**
     * 使用微信支付公钥加密用户姓名
     *
     * @param data 待加密数据
     * @return 加密后的数据
     * @throws Exception 加密异常
     */
    private String encryptWithWechatPublicKey(String data) throws Exception {

        // 实际实现应该:
        // 1. 获取微信支付平台证书
        // 2. 使用证书中的公钥对数据进行RSA加密
        // 3. 返回Base64编码的加密结果
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initSign(wechatPayConfig.privateKey());
        signature.update(data.getBytes(StandardCharsets.UTF_8));

        // 临时返回原值,避免空值导致的错误
        if (data == null || data.isEmpty()) {
            throw new IllegalArgumentException("用户姓名不能为空");
        }

        return Base64.getEncoder().encodeToString(signature.sign());// 实际项目中需要替换为加密后的值
    }

}

有了上面的代码下单和支付的流程就很简单了。

JSAPI/小程序下单

创建订单获取预支付 ID

 private OrderSubmitResponse handlePayment(UserOrders userOrders, String openid) {
        try {
            // 1. 转换金额单位(元 -> 分)

            int totalAmount = userOrders.getTotalAmount().intValue();

            // 2. 调用工具类创建小程序支付订单
            JSONObject payResult = wechatPayUtil.createMiniProgramOrder(
                    "拖车服务-" + userOrders.getStartAddress() + "至" + userOrders.getEndAddress(),
                    userOrders.getOrderSn(),
                    totalAmount,
                    openid
            );

            // 3. 获取预支付ID
            String prepayId = payResult.getString("prepay_id");
            if (prepayId == null || prepayId.isEmpty()) {
                throw new GlobalException("获取预支付ID失败");
            }

            // 4. 生成小程序调起支付的签名参数
            Map<String, String> payParams = generateMiniProgramPayParams(prepayId);

            // 5. 构建返回结果
            return OrderSubmitResponse.builder()
                    .orderNo(userOrders.getOrderSn())
                    .prepayment(userOrders.getPrepayment())
                    .payParams(payParams)
                    .build();
        }catch (Exception e){
                throw new GlobalException("支付失败");
        }
    }

JSAPI调起支付

 /**
     * 生成小程序调起支付的参数
     */
    private Map<String, String> generateMiniProgramPayParams(String prepayId) throws Exception {
        String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
        String nonceStr = generateRandomString(32);
        String packageValue = "prepay_id=" + prepayId;
        String signType = "RSA";

        // 调用工具类生成签名
        String paySign = wechatPayUtil.signV3MiniProgramSign(
                wechatPayConfig.getAppId(),
                Long.parseLong(timeStamp),
                nonceStr,
                packageValue
        );

        Map<String, String> params = new HashMap<>();
        params.put("appId", wechatPayConfig.getAppId());
        params.put("timeStamp", timeStamp);
        params.put("nonceStr", nonceStr);
        params.put("package", packageValue);
        params.put("signType", signType);
        params.put("paySign", paySign);

        return params;
    }

前端只要拿到对应的支付参数调用支付即可了。

在这里插入图片描述
调用后在页面上会出现对应的说明成功了。
在这里插入图片描述

踩坑:
签名出错:V2 和 V3 的签名方式不一样。=,所以要注意看是调用的是V 几的接口。
签名证书是否配置正确。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值