当前位置: 首页 > news >正文

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 性能优化策略

在高并发场景下,支付系统需要特别关注性能表现。以下是一些经过验证的优化方案:

  1. HTTP连接池配置

    @Bean public CloseableHttpClient wechatPayHttpClient() { return HttpClients.custom() .setMaxConnTotal(100) .setMaxConnPerRoute(50) .setConnectionTimeToLive(30, TimeUnit.SECONDS) .build(); }
  2. 异步通知处理

    @Async("paymentTaskExecutor") public void asyncProcessPayment(PaymentNotification notification) { // 处理逻辑 }
  3. 本地缓存应用

    @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 对账与异常处理

每日对账是保证资金安全的重要手段。建议实现以下流程:

  1. 定时下载微信对账单
  2. 与本地订单系统比对
  3. 标记差异订单
  4. 生成对账报告
  5. 异常订单人工处理

核心代码片段:

@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分以内视为一致),显著降低了人工干预的需求。

http://www.cnnetsun.cn/news/2702155.html

相关文章:

  • 告别Web界面!用InfluxDB CLI命令行5分钟搞定用户、Token和Bucket配置
  • 别再折腾Stable Diffusion了!用Krita+ComfyUI打造实时AI绘画工作流(保姆级配置指南)
  • 告别电机乱抖!深入解析STC无刷电调PCB设计:为什么我的四层板比两层板稳定这么多?
  • 别再手动解析了!用Python和OpenSSL搞定ECC公钥PEM到X,Y坐标的转换(附完整代码)
  • 新手也能搞定的CTF文件上传靶场通关:从Upload到蚁剑连接的全流程避坑
  • 从零构建ChatGPT插件连接器:意图识别与API调用实战
  • 特斯拉Optimus人形机器人:技术解析与应用前景
  • STM32硬件IIC避坑指南:从EV5到EV8_2,手把手教你调试F407的I2C1(库函数版)
  • 大模型可信度评估:从八大维度到实战指南
  • 零知识证明在核裁军核查中的应用:物理化实现与安全挑战
  • TranslucentTB框架依赖终极解决方案:快速修复Microsoft.UI.Xaml缺失问题
  • 软件安全评审实战指南:从流程设计到团队赋能
  • SAP ABAP Web Service实战:从SE80到SOAMANAGER,手把手教你打通内外系统接口
  • 实验室数智化转型的真正起点:AI 报告审核如何成为第一道“质量闸门”,IACheck重构审核逻辑
  • 从ROS1到ROS2:YDLidar雷达驱动迁移实战与踩坑记录(附Ubuntu 20.04/22.04配置)
  • 从数据到决策:构建个性化气候情景洞察系统的技术架构与实践
  • 号称“每吸一口赚比特币”的大麻vape,真有这么神奇?
  • 高精度时间同步:从NTP到PTP的分布式系统时间基础设施实战
  • CUDA并行编程实战:用“线程-像素”映射思想,一步步实现卷积和池化层
  • .NET Gadgeteer:模块化硬件与.NET Micro Framework的快速原型开发实践
  • Keil C51 BL51链接器长命令行问题解决方案
  • 在PC上重燃Switch游戏热情:Ryujinx模拟器的技术魔法与体验革新
  • 恶意软件自动化检测系统架构:从静态分析到动态沙箱的实战设计
  • 纯C写的MFCC特征提取工具,零外部依赖,支持PCM语音输入和13维输出
  • 终极IDM激活脚本:3种简单方法永久解锁下载管理器完整教程
  • 20kVA无局放充气式变压器的现场适配
  • Promptions:动态提示词精炼框架,让AI更懂你的意图
  • QwQ-32B-w8a8与主流框架兼容性:HuggingFace、PyTorch、TensorRT集成
  • 终极指南:如何快速上手世界最强将棋AI引擎YaneuraOu
  • 千问 LeetCode 2920. 收集所有金币可获得的最大积分 Java实现