SpringBoot项目实战:用wechatpay-java 0.2.12搞定小程序支付与退款(附完整回调处理)
SpringBoot实战:微信支付全流程工程化解决方案
微信支付作为国内移动支付的主流方案,其在小程序生态中的集成一直是开发者关注的焦点。本文将基于SpringBoot框架,使用wechatpay-java 0.2.12 SDK,从工程化角度完整实现支付下单、状态查询、退款处理以及安全回调等全流程功能。不同于简单的API调用演示,我们将重点探讨生产环境中可能遇到的各种边界情况处理、配置管理最佳实践以及性能优化技巧。
1. 环境准备与SDK配置
在开始编码前,我们需要确保开发环境满足基本要求。推荐使用JDK 11+和SpringBoot 2.7.x版本,这两个版本在长期支持(LTS)和稳定性方面都有良好表现。对于IDE选择,IntelliJ IDEA或Eclipse都能很好地支持项目开发。
首先在pom.xml中添加必要的依赖:
<dependency> <groupId>com.github.wechatpay-apiv3</groupId> <artifactId>wechatpay-java</artifactId> <version>0.2.12</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>微信支付的配置参数较多,合理的组织方式能显著提升代码可维护性。我们采用YAML文件进行配置,并创建专门的配置类进行管理:
wechat: pay: app-id: wx1234567890abcdef mch-id: 1230000109 mch-serial-no: 1D2F3A4B5C6D7E8F api-v3-key: your-api-v3-key-32bytes private-key-path: classpath:certs/apiclient_key.pem notify-url: https://yourdomain.com/api/notify对应的配置类设计如下:
@Getter @Setter @Configuration @ConfigurationProperties(prefix = "wechat.pay") public class WechatPayConfig { private String appId; private String mchId; private String mchSerialNo; private String apiV3Key; private String privateKeyPath; private String notifyUrl; @Bean public RSAAutoCertificateConfig rsaConfig() throws IOException { return new RSAAutoCertificateConfig.Builder() .merchantId(mchId) .merchantSerialNumber(mchSerialNo) .apiV3Key(apiV3Key) .privateKeyFromPath(privateKeyPath) .build(); } }注意:私钥文件应存放在resources/certs目录下,并确保.gitignore中排除了敏感配置文件
2. 支付核心流程实现
2.1 预支付订单创建
JSAPI支付是小程序支付的主要方式,其核心是生成预支付订单并返回客户端调起支付所需的参数。我们封装一个支付服务类来处理这些逻辑:
@Service @RequiredArgsConstructor public class PaymentService { private final WechatPayConfig config; private final JsapiService jsapiService; public PrepayResponse createPayment(PaymentRequest request) { try { PrepayRequest prepayRequest = new PrepayRequest(); prepayRequest.setAppid(config.getAppId()); prepayRequest.setMchid(config.getMchId()); prepayRequest.setDescription(request.getDescription()); prepayRequest.setOutTradeNo(generateOrderNo()); prepayRequest.setNotifyUrl(config.getNotifyUrl()); Amount amount = new Amount(); amount.setTotal(request.getAmount()); amount.setCurrency("CNY"); prepayRequest.setAmount(amount); Payer payer = new Payer(); payer.setOpenid(request.getOpenId()); prepayRequest.setPayer(payer); return jsapiService.prepay(prepayRequest); } catch (HttpException | ValidationException e) { throw new PaymentException("支付创建失败", e); } } private String generateOrderNo() { return "WX" + System.currentTimeMillis() + ThreadLocalRandom.current().nextInt(1000, 9999); } }关键点说明:
- 订单号生成采用时间戳+随机数策略,避免重复
- 金额单位是分,需要前端做好转换
- 异常处理要区分网络异常和业务异常
2.2 支付状态查询与订单关闭
支付状态查询是交易流程中的重要环节,特别是在处理异步通知时需要进行二次验证:
public PaymentStatus queryPaymentStatus(String orderNo) { try { QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest(); request.setMchid(config.getMchId()); request.setOutTradeNo(orderNo); Transaction transaction = jsapiService.queryOrderByOutTradeNo(request); return mapToPaymentStatus(transaction); } catch (HttpException e) { log.error("查询支付状态异常", e); return PaymentStatus.UNKNOWN; } } private PaymentStatus mapToPaymentStatus(Transaction transaction) { switch (transaction.getTradeState()) { case SUCCESS: return PaymentStatus.SUCCESS; case REFUND: return PaymentStatus.REFUNDED; case CLOSED: return PaymentStatus.CLOSED; case NOTPAY: return PaymentStatus.WAITING; default: return PaymentStatus.UNKNOWN; } }订单关闭接口用于取消未支付的订单,避免资源占用:
public void closeOrder(String orderNo) { CloseOrderRequest request = new CloseOrderRequest(); request.setMchid(config.getMchId()); request.setOutTradeNo(orderNo); try { jsapiService.closeOrder(request); } catch (HttpException e) { if (e.getStatusCode() != 404) { throw new PaymentException("关闭订单失败", e); } // 404表示订单不存在,可以认为是关闭成功 } }3. 退款处理与状态管理
3.1 退款申请实现
退款是支付系统的重要组成部分,良好的退款体验能显著提升用户满意度。退款接口需要特别注意金额校验和幂等性处理:
@Service @RequiredArgsConstructor public class RefundService { private final WechatPayConfig config; private final RefundService refundService; public Refund applyRefund(RefundRequest request) { validateRefundAmount(request); CreateRequest createRequest = new CreateRequest(); createRequest.setOutTradeNo(request.getOrderNo()); createRequest.setOutRefundNo(generateRefundNo()); createRequest.setReason(request.getReason()); createRequest.setNotifyUrl(config.getNotifyUrl()); AmountReq amount = new AmountReq(); amount.setTotal(request.getTotalAmount()); amount.setRefund(request.getRefundAmount()); amount.setCurrency("CNY"); createRequest.setAmount(amount); try { return refundService.create(createRequest); } catch (HttpException e) { throw new RefundException("退款申请失败", e); } } private void validateRefundAmount(RefundRequest request) { if (request.getRefundAmount() > request.getTotalAmount()) { throw new IllegalArgumentException("退款金额不能超过订单总额"); } } private String generateRefundNo() { return "RF" + System.currentTimeMillis() + ThreadLocalRandom.current().nextInt(1000, 9999); } }3.2 退款状态查询与异常处理
退款状态查询需要考虑微信支付的异步处理特性,实现轮询机制:
public RefundStatus queryRefundStatus(String refundNo) { try { QueryByOutRefundNoRequest request = new QueryByOutRefundNoRequest(); request.setOutRefundNo(refundNo); Refund refund = refundService.queryByOutRefundNo(request); return mapToRefundStatus(refund.getStatus()); } catch (HttpException e) { log.error("查询退款状态异常", e); return RefundStatus.UNKNOWN; } } private RefundStatus mapToRefundStatus(String status) { switch (status) { case "SUCCESS": return RefundStatus.SUCCESS; case "CLOSED": return RefundStatus.CLOSED; case "PROCESSING": return RefundStatus.PROCESSING; case "ABNORMAL": return RefundStatus.FAILED; default: return RefundStatus.UNKNOWN; } }对于异常状态的处理建议:
- PROCESSING:设置定时任务定期查询
- ABNORMAL:记录日志并通知运营人员
- UNKNOWN:建议人工介入处理
4. 安全回调处理
4.1 支付结果通知
微信支付的结果通知采用HTTPS POST请求,数据经过AES-GCM加密。我们需要实现解密验证逻辑:
@RestController @RequestMapping("/api/notify") @RequiredArgsConstructor public class PaymentNotifyController { private final WechatPayConfig config; private final PaymentService paymentService; @PostMapping("/payment") public ResponseEntity<String> handlePaymentNotify( @RequestBody NotificationRequest request, HttpServletRequest httpRequest) { try { // 1. 验证签名 verifySignature(httpRequest); // 2. 解密数据 NotificationResource resource = request.getResource(); String plainText = decryptResource(resource); // 3. 处理业务逻辑 PaymentNotification notification = parseNotification(plainText); paymentService.processPaymentResult(notification); return ResponseEntity.ok("success"); } catch (Exception e) { log.error("处理支付通知异常", e); return ResponseEntity.badRequest().body("fail"); } } private String decryptResource(NotificationResource resource) { AesUtil aesUtil = new AesUtil(config.getApiV3Key().getBytes()); return aesUtil.decryptToString( resource.getAssociatedData().getBytes(), resource.getNonce().getBytes(), resource.getCiphertext() ); } }4.2 退款结果通知
退款通知的处理与支付通知类似,但需要注意区分通知类型:
@PostMapping("/refund") public ResponseEntity<String> handleRefundNotify( @RequestBody NotificationRequest request, HttpServletRequest httpRequest) { try { verifySignature(httpRequest); NotificationResource resource = request.getResource(); String plainText = decryptResource(resource); RefundNotification notification = parseRefundNotification(plainText); refundService.processRefundResult(notification); return ResponseEntity.ok("success"); } catch (Exception e) { log.error("处理退款通知异常", e); return ResponseEntity.badRequest().body("fail"); } }安全建议:
- 验证商户号与订单是否匹配
- 检查通知中的金额与实际业务是否一致
- 实现幂等处理,避免重复通知导致重复业务操作
- 记录完整的通知日志,便于后续对账
5. 生产环境优化实践
5.1 性能优化策略
在高并发场景下,支付系统需要特别关注性能表现。以下是一些经过验证的优化方案:
HTTP连接池配置:
@Bean public CloseableHttpClient wechatPayHttpClient() { return HttpClients.custom() .setMaxConnTotal(100) .setMaxConnPerRoute(50) .setConnectionTimeToLive(30, TimeUnit.SECONDS) .build(); }异步通知处理:
@Async("paymentTaskExecutor") public void asyncProcessPayment(PaymentNotification notification) { // 处理逻辑 }本地缓存应用:
@Cacheable(value = "paymentStatus", key = "#orderNo") public PaymentStatus getCachedPaymentStatus(String orderNo) { return queryPaymentStatus(orderNo); }
5.2 监控与告警
完善的监控体系能帮助快速发现和解决问题。建议监控以下指标:
| 指标名称 | 监控方式 | 告警阈值 |
|---|---|---|
| 支付成功率 | 每分钟统计 | <95%持续5分钟 |
| 平均响应时间 | 95分位值 | >500ms |
| 退款处理时长 | 平均值 | >30秒 |
| 通知失败率 | 失败/总数 | >1% |
实现示例:
@Aspect @Component @RequiredArgsConstructor public class PaymentMonitorAspect { private final MeterRegistry meterRegistry; @Around("execution(* com.example.payment..*(..))") public Object monitor(ProceedingJoinPoint pjp) throws Throwable { String methodName = pjp.getSignature().getName(); Timer.Sample sample = Timer.start(meterRegistry); try { return pjp.proceed(); } catch (Exception e) { meterRegistry.counter("payment.error", "method", methodName).increment(); throw e; } finally { sample.stop(meterRegistry.timer("payment.latency", "method", methodName)); } } }5.3 对账与异常处理
每日对账是保证资金安全的重要手段。建议实现以下流程:
- 定时下载微信对账单
- 与本地订单系统比对
- 标记差异订单
- 生成对账报告
- 异常订单人工处理
核心代码片段:
@Scheduled(cron = "0 0 3 * * ?") public void dailyReconciliation() { try { // 下载对账单 String billDate = LocalDate.now().minusDays(1).toString(); InputStream billStream = downloadBill(billDate); // 解析并比对 List<BillRecord> wechatRecords = parseBill(billStream); List<LocalOrder> localOrders = orderService.getDailyOrders(billDate); ReconciliationResult result = comparator.compare(wechatRecords, localOrders); // 处理差异 handleDiscrepancies(result.getDiscrepancies()); // 发送报告 reportService.sendReconciliationReport(result); } catch (Exception e) { log.error("对账任务执行失败", e); alertService.sendAlert("对账任务异常", e.getMessage()); } }在实际项目中,我们发现最常出现的问题是对账单下载超时和金额不一致的情况。针对这些问题,我们实现了自动重试机制和金额容差处理(±1分以内视为一致),显著降低了人工干预的需求。
