SpringBoot 接入微信支付(包含微信小程序)

2,093 阅读4分钟

1. 准备开发所需的账号以及配置信息

1.1 注册微信公众号或者小程序

无论是接入微信支付 还是微信小程序接入支付都要先获取APPID

微信公众平台网址:mp.weixin.qq.com

1.2 开通微信支付

微信支付目前仅支持商家开通,个人如果要使用的话 可以找第三方平台。

微信支付官网:pay.weixin.qq.com/index.php/a…

  • 申请完成后将mch_id和appid关联起来

  • mch_id:
  • api-v3-key:

  • merchant-serial-numberh和 证书

根据平台提示正常下载即可

下载完成示例

到此准备工作就都完成了

2. 开发接入

开发前请详细阅读微信支付开发文档:pay.weixin.qq.com/docs/mercha…

本章以微信小程序接入为案例,在wechatpay-java给的案例中 Native 支付下单使用NativePayService类。JSAPI 支付和 APP 支付推荐使用服务拓展类 JsapiServiceExtension 和 AppServiceExtension

2.1. 代码准备

2.1.1. 配置文件

# 支付配置;如果你申请了支付渠道,则可以配置 enable = true,否则就配置 false 走流程测试
wxpay:
  config:
    # 状态;true = 开启、false 关闭
    enabled: true
    # 申请支付主体的 appid
    appid: 
    # 商户号
    mchid: 
    # 回调地址
    notify-url: // 必须是https的接口回调
    # 商户API私钥路径
    private-key-path: cert/apiclient_key.pem
    # 商户证书序列号:openssl x509 -in apiclient_cert.pem -noout -serial
    merchant-serial-number: 
    # 商户APIV3密钥
    api-v3-key: 

2.1.2. 配置文件类

@Data
@ConfigurationProperties(prefix = "wxpay.config",ignoreInvalidFields = true)
public class WeChatPayConfigProperties {
    /** 状态;open = 开启、close 关闭 */
    private boolean enable;
    /** 申请支付主体的 appid */
    private String appid;
    /** 商户号 */
    private String mchid;
    /** 回调地址 */
    private String notifyUrl;
    /** 商户API私钥路径 */
    private String privateKeyPath;
    /** 商户证书序列号:openssl x509 -in apiclient_cert.pem -noout -serial */
    private String merchantSerialNumber;
    /** 商户APIV3密钥 */
    private String apiV3Key;
}

2.1.3. spring注入的方式注入JsapiServiceExtension

@Slf4j
@Configuration
@EnableConfigurationProperties(WeChatPayConfigProperties.class)
public class WeChatPayConfig {

    //    /**
    //     * 一个商户号只能初始化一个配置,否则会因为重复的下载任务报错
    //     *
    //     * @param properties 支付配置
    //     * @return NativePay
    //     */
    //    @Bean
    //    @ConditionalOnProperty(value = "wxpay.config.enabled", havingValue = "true", matchIfMissing = false)
    //    public NativePayService buildNativePayService(WeChatPayConfigProperties properties) {
    //        Config config = new RSAAutoCertificateConfig.Builder()
    //                .merchantId(properties.getMchid())
    //                .privateKeyFromPath(getFilePath(properties.getPrivateKeyPath()))
    //                .merchantSerialNumber(properties.getMerchantSerialNumber())
    //                .apiV3Key(properties.getApiV3Key())
    //                .build();
    //        return new NativePayService.Builder().config(config).build();
    //    }

    /**
     * 一个商户号只能初始化一个配置,否则会因为重复的下载任务报错--微信小程序支付
     *
     * @param properties 支付配置
     * @return NativePay
     */
    @Bean
    @ConditionalOnProperty(value = "wxpay.config.enabled", havingValue = "true", matchIfMissing = false)
    public JsapiServiceExtension jsapiService(WeChatPayConfigProperties properties) {
        Config config = new RSAAutoCertificateConfig.Builder()
        .merchantId(properties.getMchid())
        .privateKeyFromPath(getFilePath(properties.getPrivateKeyPath()))
        .merchantSerialNumber(properties.getMerchantSerialNumber())
        .apiV3Key(properties.getApiV3Key())
        .build();

        JsapiServiceExtension service = new JsapiServiceExtension.Builder().config(config).build();

        return service;
    }
    // 支付回调配置参数注入
    @Bean
    @ConditionalOnProperty(value = "wxpay.config.enabled", havingValue = "true", matchIfMissing = false)
    public NotificationConfig buildNotificationConfig(WeChatPayConfigProperties properties) {
        return new RSAAutoCertificateConfig.Builder()
        .merchantId(properties.getMchid())
        .privateKeyFromPath(getFilePath(properties.getPrivateKeyPath()))
        .merchantSerialNumber(properties.getMerchantSerialNumber())
        .apiV3Key(properties.getApiV3Key())
        .build();
    }


    @Bean
    @ConditionalOnBean(NotificationConfig.class)
    @ConditionalOnProperty(value = "wxpay.config.enabled", havingValue = "true", matchIfMissing = false)
    public NotificationParser notificationParser(NotificationConfig notificationConfig) {
        return new NotificationParser(notificationConfig);
    }

    public static String getFilePath(String classFilePath) {
        String filePath = "";
        try {
            String templateFilePath = "tempfiles/classpathfile/";
            File tempDir = new File(templateFilePath);
            if (!tempDir.exists()) {
                tempDir.mkdirs();
            }
            String[] filePathList = classFilePath.split("/");
            String checkFilePath = "tempfiles/classpathfile";
            for (String item : filePathList) {
                checkFilePath += "/" + item;
            }
            File tempFile = new File(checkFilePath);
            if (tempFile.exists()) {
                filePath = checkFilePath;
            } else {
                //解析
                ClassPathResource classPathResource = new ClassPathResource(classFilePath);
                InputStream inputStream = classPathResource.getInputStream();
                checkFilePath = "tempfiles/classpathfile";
                for (int i = 0; i < filePathList.length; i++) {
                    checkFilePath += "/" + filePathList[i];
                    if (i == filePathList.length - 1) {
                        //文件
                        File file = new File(checkFilePath);
                        if (!file.exists()) {
                            FileUtils.copyInputStreamToFile(inputStream, file);
                        }
                    } else {
                        //目录
                        tempDir = new File(checkFilePath);
                        if (!tempDir.exists()) {
                            tempDir.mkdirs();
                        }
                    }
                }
                inputStream.close();
                filePath = checkFilePath;
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        return filePath;
    }
}

这块有个细节点注意一下,商户API私钥路径会自动生成在项目根路径temprifles中,如果配置错了 重新配置的话记得删除这个目录后在启动项目,避坑!

2.1.4. 调用小程序下单

// 依赖注入
@Autowired(required = false)
private JsapiServiceExtension jsapiService;


public PrepayWithRequestPaymentResponse openMembership(String openId,Long vipId){
        BossVipProduct bossVipProduct = bossVipProductMapper.selectByPrimaryProductId(vipId);

        BigDecimal totalAmount = bossVipProduct.getPrice();


        String orderId = RandomStringUtils.randomNumeric(12);
        // 创建订单--- 根据自己实际项目需求编写
        BossOrder bossOrder = BossOrder.builder()
                .payAmount(totalAmount)
                .productId(bossVipProduct.getProductId())
                .productName(bossVipProduct.getProductName())
                .orderId(orderId)
                .orderTime(new Date())
                // 此处代表订单类型 1 代表用户报名培训 2 代表开通会员
                .orderStatus(2)
                .totalAmount(totalAmount)
                .payType(0)
                .payStatus(0)
                .userId(openId)
                .createTime(new Date())
                .build();

        int i = bossOrderMapper.insertSelective(bossOrder);

        Amount amount=new Amount();
        amount.setTotal(totalAmount.multiply(new BigDecimal(100)).intValue());

        PrepayRequest prepayRequest=new PrepayRequest();
        prepayRequest.setAmount(amount);
        prepayRequest.setAppid(appid);
        prepayRequest.setMchid(mchid);
        prepayRequest.setNotifyUrl(notifyUrl);
        prepayRequest.setOutTradeNo(orderId);
        prepayRequest.setDescription(bossVipProduct.getProductName());

        PayOrderAggregation payOrderAggregation = PayOrderAggregation.builder()
                .orderType(PayOrderType.VIP_APPLY)
                .productId(bossVipProduct.getProductId())
                .build();

        prepayRequest.setAttach(JSON.toJSONString(payOrderAggregation));
        Payer payer = new Payer();
        payer.setOpenid(openId);
        prepayRequest.setPayer(payer);

        PrepayWithRequestPaymentResponse prepayWithRequestPaymentResponse = jsapiService.prepayWithRequestPayment(prepayRequest);
    // PrepayWithRequestPaymentResponse 返回的就是前端调起支付所需的全部参数,直接返回就好
        return prepayWithRequestPaymentResponse;
    

    }

2.1.5. 支付回调

package com.christielu.boss.controller;

import com.alibaba.fastjson2.JSON;
import com.christielu.boss.domain.PayOrderAggregation;
import com.christielu.boss.enums.PayOrderType;
import com.christielu.boss.service.TrainPayService;
import com.christielu.boss.service.UserService;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.core.notification.RequestParam;
import com.wechat.pay.java.service.payments.model.Transaction;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @Author: cxc
 * @CreateTime: 2024-03-27  22:42
 * @Description: TODO
 * @Version: 1.0
 */
@RestController
@CrossOrigin
@RequestMapping("/pay")
@Slf4j
public class PayController {

    @Autowired
    private NotificationParser notificationParser; // 在配置类中已经注入过了

    @PostMapping("payNotify")
    public void payNotify(@RequestBody String requestBody, HttpServletRequest request, HttpServletResponse response) throws IOException {
        try {
            log.info("PayController:" + requestBody);
            // 微信传过来的签名
            String header = request.getHeader("Wechatpay-Signature");
            // 随机串
            String nonceStr = request.getHeader("Wechatpay-Nonce");
            // 微信传递过来的签名
            String signature = request.getHeader("Wechatpay-Signature");
            // 证书序列号(微信平台)
            String serialNo = request.getHeader("Wechatpay-Serial");
            // 时间戳
            String timestamp = request.getHeader("Wechatpay-Timestamp");

            RequestParam requestParam = new RequestParam.Builder()
            .serialNumber(serialNo)
            .nonce(nonceStr)
            .signature(signature)
            .timestamp(timestamp)
            .body(requestBody)
            .build();
            // 以支付通知回调为例,验签、解密并转换成 Transaction

            Transaction transaction = notificationParser.parse(requestParam, Transaction.class);
            log.info("transaction:{}",JSON.toJSONString(transaction));
            Transaction.TradeStateEnum tradeState = transaction.getTradeState();
            if (Transaction.TradeStateEnum.SUCCESS.equals(tradeState)){
                String attach = transaction.getAttach();
                PayOrderAggregation payOrderAggregation = JSON.parseObject(attach, PayOrderAggregation.class);
                // 区分订单类型 // 根据自己代码实际逻辑编写
                if (payOrderAggregation.getOrderType().equals(PayOrderType.TRAIN_APPLY)) {
                    trainPayService.payNotify(transaction,payOrderAggregation);
                }else if (payOrderAggregation.getOrderType().equals(PayOrderType.VIP_APPLY)){
                    userService.openMembershipPayNotify(transaction,payOrderAggregation);
                }

            }else {
                response.getWriter().write("<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>");
            }
        } catch (IOException e) {
            response.getWriter().write("<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>");
        }


    }

}

3. 总结 

一次完整的发起支付,到支付回调流程就完毕了,如果有其他需求,参考文档即可,最重要的就是注入我们所需要的类,和将相关的初始化配置 ,在上面的WeChatPayConfig类中。