springboot3集成微信V3版本Native支付,从申请证书到支付回调通知

1,995 阅读9分钟

最近在做一个支付相关的 Spring Boot 项目,需要集成微信支付功能。最开始我以为只要拿到商户号和API密钥,调用几个接口就能搞定,但真正开始接入时才发现,微信支付涉及到的流程远比想象中复杂。如果是自己从统一下单、签名校验,到回调通知处理,每一步都需要严格按照微信的接口文档来实现,操作起来十分麻烦。而且还涉及证书的配置、请求的加解密等安全性问题。就想着使用官方库,官方库的文档马马虎虎,自己记录一下这个操作流程。

1、申请证书

0.png

这里的商户号是商户信息中微信支付商户号, 商户名称为企业营业执照名称

image.png

生成的请求串粘贴到商户平台,然后进行安全验证

image.png

然后先点击复制生成的证书串,然后点击微信支付商户平台证书工具中的【下一步】,自动粘贴证书串

image.png

然后点击下一步生成证书

image.png

点击管理证书可以查看证书序列号

image.png

APIv3密钥

image.png

image.png

项目配置

这里使用官方提供的jar包简化操作,需要引入依赖,依赖的地址:官方仓库

开发库由 core 和 service 组成:

以 Native 支付下单为例,先导入相关的依赖

<dependency>
  <groupId>com.github.wechatpay-apiv3</groupId>
  <artifactId>wechatpay-java</artifactId>
  <version>0.2.17</version>
</dependency>

然后在yml文件中配置需要的参数,注意一点,我这里是把私钥内容直接写在yml配置文件中。除此之外,可以复制私钥文件到文件夹中,这里填写文件的位置,通过读取文件的形式操作,使用适合自己的方法即可。

wechat:
  pay:
    app-id: 1234567890
    notify-url: https://xxxxxx.cn/api/notify
    merchant-id: 1234567890
    private-key: |-
      -----BEGIN PRIVATE KEY-----
      MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC07F5PCCWrhmm6
      ...
      -----END PRIVATE KEY-----
    merchant-serial-number: adkhjkagkajgakhjfhd
    api-v3-key: adhjahgjhahdfhjdak

具体实现

配置类

初始化商户配置,生成一个配置类,从 v0.2.3 版本开始,微信支付引入了一个名为 RSAAutoCertificateConfig 的配置类,用于自动更新平台证书

import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Description: 支付配置类
 * @Author: xiaodou
 * @Date: 2025/2/13
 * @Version: V1.0
 * @JDK: JDK21
 */
@Configuration
@Data
public class WechatPayConfig {
    /**
     * 小程序公众号的appId
     */
    @Value("${wechat.pay.app-id}")
    private String appId;

    /**
     * 回调地址
     */
    @Value("${wechat.pay.notify-url}")
    private String notifyUrl;

    /**
     * 商户号
     */
    @Value("${wechat.pay.merchant-id}")
    private String merchantId;

    /**
     * 商户API私钥路径
     */
    @Value("${wechat.pay.private-key}")
    private String privateKey;

    /**
     * 商户证书序列号
     */
    @Value("${wechat.pay.merchant-serial-number}")
    private String merchantSerialNumber;

    /**
     * 商户APIV3密钥
     */
    @Value("${wechat.pay.api-v3-key}")
    private String apiV3Key;

    @Bean
    public RSAAutoCertificateConfig rsaAutoCertificateConfig() {
        return new RSAAutoCertificateConfig.Builder().merchantId(merchantId)
            .privateKey(privateKey)
            .merchantSerialNumber(merchantSerialNumber)
            .apiV3Key(apiV3Key)
            .build();
    }
}

生成订单

我这里是Native支付下单,主要流程步骤如下:

  1. 业务端创建订单
    用户确认下单后,先在业务系统(服务端)中生成业务订单,保存到数据库。此时订单状态为"待支付",生成一个唯一的业务订单号(如 out_trade_no)。

  2. 调用微信预支付接口
    业务端将订单信息(金额、商品描述等)和业务订单号(out_trade_no)发送给微信支付接口,获取微信的支付二维码信息

  3. 返回支付参数给前端
    业务端将微信返回的支付参数(如 订单ID、时间戳、支付二维码等)返回给前端,前端使用支付二维码信息生生成二维码,等待用户支付。生成操作可以参考《那个深夜,她说二维码扫不出来,我说我们还有办法》

  4. 用户支付成功后,微信异步通知(回调)
    微信支付成功后,微信端会通过回调通知服务端,此时需要根据微信返回的订单号(out_trade_no)更新业务订单状态为"已支付"。

sequenceDiagram
    participant 用户
    participant 前端
    participant 业务端(服务端)
    participant 微信支付接口
    participant 数据库

    用户->>前端: 确认下单
    前端->>业务端(服务端): 提交订单请求
    业务端(服务端)->>数据库: 创建业务订单(状态=待支付,生成out_trade_no)
    数据库-->>业务端(服务端): 返回订单数据
    业务端(服务端)->>微信支付接口: 调用统一下单API(带out_trade_no,金额,描述等)
    微信支付接口-->>业务端(服务端): 返回预支付参数(含支付二维码URL)
    业务端(服务端)-->>前端: 返回支付参数(含二维码URL)
    前端->>用户: 展示支付二维码
    用户->>微信支付接口: 扫码并确认支付
    微信支付接口->>业务端(服务端): 异步回调通知(含out_trade_no,支付结果)
    业务端(服务端)->>数据库: 更新订单状态=已支付
    业务端(服务端)-->>微信支付接口: 回复SUCCESS(防止微信重复回调)
    前端->>业务端(服务端): 轮询或WebSocket查询订单状态
    业务端(服务端)-->>前端: 返回支付成功状态
    前端->>用户: 显示支付成功

请求类:

import lombok.Data;

/**
 * @Description: 预下单请求类
 * @Author: xiaodou
 * @Date: 2025/2/13
 * @Version: V1.0
 * @JDK: JDK21
 */
@Data
public class UserPrePayReq {
    // 生成订单的金额,单位是分
    private Integer rechargeAmount;
}

响应类:

import lombok.Data;
import lombok.experimental.Accessors;

/**
 * @Description: 预支付响应类
 * @Author: xiaodou
 * @Date: 2025/2/13
 * @Version: V1.0
 * @JDK: JDK21
 */
@Data
@Accessors(chain = true)
public class UserPrePayRes {
    // 订单ID,业务端自己生成的订单ID
    private String orderId;
    // 微信端返回的二维码内容,传递到前端 生成二维码即可
    private String codeUrl;
}

订单号生成类

使用与时间相关的信息生成订单号(out_trade_no),这个订单号并没有用作数据库中保存订单数据的ID

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;

/**
 * @Description: 订单号生成工具类
 * @Author: xiaodou
 * @Date: 2025/2/13
 * @Version: V1.0
 * @JDK: JDK21
 */
public class OutTradeNoGenerator {

    private static final String PREFIX = "RECHARGE"; // 可按业务调整前缀
    private static final Random RANDOM = new Random();

    /**
     * 生成唯一 out_trade_no
     * 示例:RECHARGE_20250213153045123_3948
     */
    public static String generate() {
        String timestamp = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());
        String randomPart = String.format("%04d", RANDOM.nextInt(10000));
        return PREFIX + "_" + timestamp + "_" + randomPart;
    }

    /**
     * 带上用户ID尾号
     */
    public static String generateWithUserId(long userId) {
        String timestamp = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());
        String userSuffix = String.format("%04d", userId % 10000);
        return PREFIX + "_" + timestamp + "_" + userSuffix;
    }
}

使用之前构建 config,再构建 service 即可调用 prepay() 发送请求。Result为自定义响应类,可忽略,使用代码修改为自己的响应类

预下单代码:

import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.exception.ValidationException;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.service.payments.model.Transaction;
import com.wechat.pay.java.service.payments.nativepay.NativePayService;
import com.wechat.pay.java.service.payments.nativepay.model.Amount;
import com.wechat.pay.java.service.payments.nativepay.model.PrepayRequest;
import com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/pay")
public class WechatNativePayController {
    private static final Logger logger = LoggerFactory.getLogger(WorkflowController.class);

    private final WechatPayConfig wechatPayConfig;
    private final RSAAutoCertificateConfig rsaConfig;

    /**
     * Native支付预下单
     * Result为自定义响应类,可忽略
     */
    @PostMapping("prepay")
    public Result<UserPrePayRes> prepay(@RequestHeader("Authorization") String token,
        @RequestBody UserPrePayReq userPrePayReq) {
        logger.info("start to prepay,info: {}", userPrePayReq);
        // 判断今日事否为空
        if (userPrePayReq == null || userPrePayReq.getRechargeAmount() == null) {
            logger.info("userPrePayReq is null");
            return Result.fail();
        }
        // 订单描述内容
        String description = "充值积分: ";
        try {
            // 订单的金额,单位是分
            Integer rechargeAmount = userPrePayReq.getRechargeAmount();
            // 用户ID,业务端使用自己的方法获取用户ID,JwtUtil为我自己定义的工具类
            String userId = JwtUtil.getUserId(token);
            // 生成订单ID,OutTradeNoGenerator是我自己定义的工具类
            String outTradeNo = OutTradeNoGenerator.generate();
            // 业务端生成自己的订单,此处省略代码
            logger.info("save order finish");

            // 构建Native 支付的service
            NativePayService service = new NativePayService.Builder().config(rsaConfig)
                .build();
            // 预支付请求类
            PrepayRequest request = new PrepayRequest();
            // 订单的总金额,单位是分
            Amount amount = new Amount();
            amount.setTotal(rechargeAmount);
            request.setAmount(amount);
            // 小程序或者公众号的appId
            request.setAppid(wechatPayConfig.getAppId());
            // 商户号
            request.setMchid(wechatPayConfig.getMerchantId());
            // 订单描述信息,展示给用户看
            request.setDescription(description);
            // 回调地址,微信端通知支付结果
            request.setNotifyUrl(wechatPayConfig.getNotifyUrl());
            // 业务端的订单号
            request.setOutTradeNo(outTradeNo);
            // 调用下单方法,得到应答
            PrepayResponse response = service.prepay(request);
            //转化成业务端自己的响应类返回给前端
            UserPrePayRes userPrePayRes = new UserPrePayRes()
                .setCodeUrl(response.getCodeUrl()) // 微信端返回的二维码内容
                .setOrderId(rechargeOrder.getId()); // 业务端自己的订单ID
            return Result.success(userPrePayRes);
        } catch (Exception e) {
            logger.error("prepay error", e);
            return Result.fail(e.getMessage());
        }
    }
}

前端收到响应之后,只要把response.getCodeUrl()的内容转化成二维码,在页面中展示出来即可。

支付回调处理

接着是处理支付回调,首先需要在服务器上创建一个公开的 HTTP 端点,接受来自微信支付的回调通知。这个接口不能有权限校验,当接收到回调通知,使用 notification 中的 NotificationParser 解析回调通知。

具体步骤如下:

1、使用回调通知请求的数据,构建 RequestParam

  • HTTP 请求体 body。切记使用原始报文,不要用 JSON 对象序列化后的字符串,避免验签的 body 和原文不一致。
  • HTTP 头 Wechatpay-Signature。应答的微信支付签名。
  • HTTP 头 Wechatpay-Serial。微信支付平台证书的序列号,验签必须使用序列号对应的微信支付平台证书。
  • HTTP 头 Wechatpay-Nonce。签名中的随机数。
  • HTTP 头 Wechatpay-Timestamp。签名中的时间戳。
  • HTTP 头 Wechatpay-Signature-Type。签名类型。

2、初始化 NotificationConfig,根据回调签名选择使用微信支付公钥或者平台证书选择不同的Config

3、初始化 NotificationParser

4、调用 NotificationParser.parse() 验签、解密并将 JSON 转换成具体的通知回调对象。如果验签失败,SDK 会抛出 ValidationException

5、接下来可以执行你的业务逻辑了。如果执行成功,你应返回 200 OK 的状态码。如果执行失败,你应返回 4xx 或者 5xx的状态码,例如数据库操作失败建议返回 500 Internal Server Error

/**
 * 支付回调
 *
 * @param request
 * @param response
 * @return
 */
@PostMapping("/notify")
public ResponseEntity<Object> notifyPay(HttpServletRequest request, HttpServletResponse response) {
    // HTTP 头 `Wechatpay-Signature`。应答的微信支付签名
    String signature = request.getHeader("Wechatpay-Signature");
    // HTTP 头 `Wechatpay-Serial`。微信支付平台证书的序列号,验签必须使用序列号对应的微信支付平台证书。
    String serial = request.getHeader("Wechatpay-Serial");
    // HTTP 头 `Wechatpay-Nonce`。签名中的随机数。
    String nonce = request.getHeader("Wechatpay-Nonce");
    // HTTP 头 `Wechatpay-Timestamp`。签名中的时间戳。
    String timestamp = request.getHeader("Wechatpay-Timestamp");

    com.wechat.pay.java.core.notification.RequestParam requestParam =
        new com.wechat.pay.java.core.notification.RequestParam.Builder().serialNumber(serial)
            .nonce(nonce)
            .signature(signature)
            .timestamp(timestamp)
            .body(getRequestBody(request)) // HTTP 请求体 body
            .build();
    // 初始化 `NotificationConfig`,根据回调签名选择使用微信支付公钥或者平台证书选择不同的Config
    // 这里使用WechatPayConfig中生成的 private final RSAAutoCertificateConfig rsaConfig;
    NotificationParser parser = new NotificationParser(rsaConfig);
    try {
        // 以支付通知回调为例,验签、解密并转换成 Transaction
        Transaction transaction = parser.parse(requestParam, Transaction.class);
        logger.info("transaction: {}", transaction);
        // 业务端订单号
        String outTradeNo = transaction.getOutTradeNo();
        // 微信支付侧订单的唯一标识
        String transactionId = transaction.getTransactionId();
        // 用户完成订单支付的时间
        String successTime = transaction.getSuccessTime();
        // 对交易状态的详细说明,比如“支付成功”
        String tradeStateDesc = transaction.getTradeStateDesc();
        // 交易状态
        // SUCCESS:支付成功
        // REFUND:转入退款
        // NOTPAY:未支付
        // CLOSED:已关闭
        // REVOKED:已撤销(仅付款码支付会返回)
        // USERPAYING:用户支付中(仅付款码支付会返回)
        // PAYERROR:支付失败(仅付款码支付会返回)
        Transaction.TradeStateEnum tradeState = transaction.getTradeState();
        // 如果是支付成功状态
        if (tradeState.equals(Transaction.TradeStateEnum.SUCCESS)) {
            // 业务端更新订单状态和支付成功时间,可以保留微信支付端的响应消息
            // 业务端更新用户金额等
            // 如果存在支付超时关闭订单的操作,此时也可以移除相关操作
            // 此处略去代码
            
            // 给微信支付端发送通知,防止微信端重复发送响应
            ResponseEntity<Object> notifyRes = ResponseEntity.status(HttpStatus.OK)
                .build();
            return notifyRes;
        }
        
        // 如果是支付成功以外的情况,业务端根据自己的情况做处理
        logger.info("notify tradeState: {}", tradeState);
    } catch (ValidationException e) {
        // 签名验证失败,返回 401 UNAUTHORIZED 状态码
        logger.error("sign verification failed", e);
        return ResponseEntity
            .status(HttpStatus.UNAUTHORIZED)
            .build();
    }
    // 接收消息并处理成功,返回 200 OK 状态码
    return ResponseEntity.status(HttpStatus.OK).build();
}

private String getRequestBody(HttpServletRequest request) {
    StringBuilder sb = new StringBuilder();
    try (ServletInputStream inputStream = request.getInputStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));) {
        String line;

        while ((line = reader.readLine()) != null) {
            sb.append(line);
        }
    } catch (IOException e) {
    }

    return sb.toString();
}