最近在做一个支付相关的 Spring Boot 项目,需要集成微信支付功能。最开始我以为只要拿到商户号和API密钥,调用几个接口就能搞定,但真正开始接入时才发现,微信支付涉及到的流程远比想象中复杂。如果是自己从统一下单、签名校验,到回调通知处理,每一步都需要严格按照微信的接口文档来实现,操作起来十分麻烦。而且还涉及证书的配置、请求的加解密等安全性问题。就想着使用官方库,官方库的文档马马虎虎,自己记录一下这个操作流程。
1、申请证书
这里的商户号是商户信息中微信支付商户号, 商户名称为企业营业执照名称
生成的请求串粘贴到商户平台,然后进行安全验证
然后先点击复制生成的证书串,然后点击微信支付商户平台证书工具中的【下一步】,自动粘贴证书串
然后点击下一步生成证书
点击管理证书可以查看证书序列号
APIv3密钥
项目配置
这里使用官方提供的jar包简化操作,需要引入依赖,依赖的地址:官方仓库
开发库由 core 和 service 组成:
- core 为基础库,包含自动签名和验签的 HTTP 客户端、回调处理、加解密库。
- service 为业务服务,包含业务接口和使用示例:https://github.com/wechatpay-apiv3/wechatpay-java/tree/main/service/src/example/java/com/wechat/pay/java/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支付下单,主要流程步骤如下:
-
业务端创建订单
用户确认下单后,先在业务系统(服务端)中生成业务订单,保存到数据库。此时订单状态为"待支付",生成一个唯一的业务订单号(如out_trade_no)。 -
调用微信预支付接口
业务端将订单信息(金额、商品描述等)和业务订单号(out_trade_no)发送给微信支付接口,获取微信的支付二维码信息。 -
返回支付参数给前端
业务端将微信返回的支付参数(如订单ID、时间戳、支付二维码等)返回给前端,前端使用支付二维码信息生生成二维码,等待用户支付。生成操作可以参考《那个深夜,她说二维码扫不出来,我说我们还有办法》 -
用户支付成功后,微信异步通知(回调)
微信支付成功后,微信端会通过回调通知服务端,此时需要根据微信返回的订单号(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();
}