一、引言:微信支付的核心价值与接入痛点
在移动支付主导的当下,微信支付作为国内主流支付方式,已成为企业服务端开发的必备能力。无论是电商下单、服务缴费还是内容付费,稳定可靠的微信支付接入直接影响用户体验与资金安全。
但对于Java开发者而言,微信支付接入并非易事:官方文档侧重规范说明,缺乏完整的工程化实践方案;签名验证、异步回调、退款对账等环节容易出现细节错误;不同支付场景(Native、JSAPI、H5)的适配逻辑存在差异,稍不注意就会导致支付失败。
二、微信支付底层逻辑与核心概念拆解
在动手编码前,必须先理清微信支付的底层运行逻辑,明确核心概念的含义,避免因概念混淆导致的开发错误。
2.1 核心交互逻辑流程图
2.2 核心概念解析
- 商户号(mch_id):微信支付分配给商户的唯一标识,用于资金结算、接口调用的身份认证,需在微信支付商户平台申请。
- APPID:公众号/小程序/APP的唯一标识,需与商户号完成绑定,用于关联用户身份。
- API密钥(key):商户平台设置的32位密钥,用于接口签名验证,是保障接口安全的核心,需严格保密。
- 预支付交易会话标识(prepay_id):微信支付服务端生成的预支付凭证,有效期2小时,是发起实际支付的核心参数。
- 签名算法:微信支付采用HMAC-SHA256(推荐)或MD5进行签名,所有接口调用均需通过签名验证,防止参数被篡改。
- 异步通知(notify_url):商户系统提供的公网可访问接口,微信支付在支付完成后会异步回调该接口通知支付结果,是确认支付状态的可靠方式。
2.3 签名验证底层原理
签名验证是微信支付接口安全的核心,其底层逻辑可概括为“参数标准化→拼接→加密→验证”四步:
- 筛选所有非空的请求参数,排除sign字段;
- 按参数名ASCII码升序排序,用“&”拼接成“key=value”格式的字符串;
- 在字符串末尾拼接“&key=商户API密钥”,用指定加密算法(HMAC-SHA256/MD5)加密,得到签名值;
- 微信支付服务端/商户系统接收参数后,重复上述步骤生成签名,与传入的sign字段对比,一致则说明参数未被篡改。
注意:微信支付新版接口均推荐使用HMAC-SHA256算法,MD5仅用于兼容旧版本,本文所有示例均采用HMAC-SHA256。
三、接入前准备:环境搭建与配置
3.1 开发环境说明
- JDK:17
- 构建工具:Maven 3.9.6
- 框架:Spring Boot 3.2.5
- 持久层:MyBatis-Plus 3.5.5
- 接口文档:Swagger3(SpringDoc OpenAPI 2.5.0)
- JSON工具:FastJSON2 2.0.50
- 微信支付SDK:weixin-pay-java 4.5.0(官方推荐的Java SDK,简化接口调用)
- 数据库:MySQL 8.0.36
3.2 Maven依赖配置
在pom.xml中引入核心依赖,所有版本均采用最新稳定版:
<dependencies>
<!-- Spring Boot核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.5</version>
</dependency>
<!-- 微信支付SDK -->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>weixin-pay-java</artifactId>
<version>4.5.0</version>
</dependency>
<!-- Lombok(日志、getter/setter) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!-- FastJSON2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.50</version>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.3.0</version>
<scope>runtime</scope>
</dependency>
<!-- SpringDoc OpenAPI(Swagger3) -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>
<!-- Spring工具类 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>6.1.6</version>
</dependency>
<!-- Google集合工具类 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.2.1-jre</version>
</dependency>
</dependencies>
3.3 核心配置信息
3.3.1 配置文件(application.yml)
spring:
# 数据库配置
datasource:
url: jdbc:mysql://localhost:3306/wechat_pay_demo?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root123456
driver-class-name: com.mysql.cj.jdbc.Driver
# 微信支付配置
wechat:
pay:
mch-id: 1234567890 # 你的商户号
app-id: wx1234567890abcdef # 绑定的公众号/小程序APPID
api-key: 1234567890abcdef1234567890abcdef # 商户平台设置的32位API密钥
notify-url: https://your-domain.com/api/wechat/pay/notify # 支付异步通知地址(公网可访问)
refund-notify-url: https://your-domain.com/api/wechat/refund/notify # 退款异步通知地址
cert-path: classpath:cert/apiclient_cert.p12 # 退款等敏感接口所需的证书路径
cert-password: 1234567890 # 证书密码(默认与商户号一致)
# MyBatis-Plus配置
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.jam.demo.entity
configuration:
map-underscore-to-camel-case: true # 下划线转驼峰
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL日志
# Swagger3配置
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
operationsSorter: method
packages-to-scan: com.jam.demo.controller
3.3.2 微信支付配置类
package com.jam.demo.config;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.service.payments.nativepay.NativePayService;
import com.wechat.pay.java.service.payments.jsapi.JsapiPayService;
import com.wechat.pay.java.service.refund.RefundService;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.io.InputStream;
/**
* 微信支付配置类
* 加载配置信息并初始化支付相关服务
* @author ken
*/
@Configuration
@ConfigurationProperties(prefix = "spring.wechat.pay")
@Data
public class WechatPayConfig {
/** 商户号 */
private String mchId;
/** 公众号/小程序APPID */
private String appId;
/** API密钥 */
private String apiKey;
/** 支付异步通知地址 */
private String notifyUrl;
/** 退款异步通知地址 */
private String refundNotifyUrl;
/** 证书路径 */
private String certPath;
/** 证书密码 */
private String certPassword;
/**
* 初始化微信支付配置(自动加载证书)
* @return 微信支付配置对象
* @throws IOException 证书加载异常
*/
@Bean
public Config wechatPayConfig() throws IOException {
// 校验配置参数
StringUtils.hasText(mchId, "商户号不能为空");
StringUtils.hasText(appId, "APPID不能为空");
StringUtils.hasText(apiKey, "API密钥不能为空");
StringUtils.hasText(certPath, "证书路径不能为空");
StringUtils.hasText(certPassword, "证书密码不能为空");
// 加载证书流
InputStream certInputStream = getClass().getResourceAsStream(certPath.substring("classpath:".length()));
StringUtils.hasText(certInputStream, "证书文件不存在");
// 构建自动加载证书的配置
return new RSAAutoCertificateConfig.Builder()
.merchantId(mchId)
.privateKeyFromPath("classpath:cert/apiclient_key.pem") // 商户私钥路径
.merchantSerialNumber("1234567890ABCDEF") // 商户证书序列号(从商户平台获取)
.apiV3Key(apiKey)
.build();
}
/**
* 初始化Native支付服务
* @param config 微信支付配置
* @return NativePayService
*/
@Bean
public NativePayService nativePayService(Config config) {
return new NativePayService.Builder().config(config).build();
}
/**
* 初始化JSAPI支付服务
* @param config 微信支付配置
* @return JsapiPayService
*/
@Bean
public JsapiPayService jsapiPayService(Config config) {
return new JsapiPayService.Builder().config(config).build();
}
/**
* 初始化退款服务
* @param config 微信支付配置
* @return RefundService
*/
@Bean
public RefundService refundService(Config config) {
return new RefundService.Builder().config(config).build();
}
}
3.4 数据库表设计(MySQL8.0)
微信支付相关业务需设计订单表、支付记录表、退款记录表,用于存储订单信息、支付结果、退款信息,保障数据可追溯。
-- 订单表
CREATE TABLE `t_order` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`order_no` varchar(64) NOT NULL COMMENT '商户订单号',
`app_id` varchar(32) NOT NULL COMMENT 'APPID',
`mch_id` varchar(32) NOT NULL COMMENT '商户号',
`user_id` bigint NOT NULL COMMENT '用户ID',
`total_amount` int NOT NULL COMMENT '订单总金额(分)',
`subject` varchar(128) NOT NULL COMMENT '订单标题',
`pay_type` varchar(16) NOT NULL COMMENT '支付方式(NATIVE/JSAPI/H5)',
`order_status` varchar(16) NOT NULL COMMENT '订单状态(PENDING:待支付;SUCCESS:支付成功;CLOSED:已关闭;REFUNDED:已退款)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_user_id` (`user_id`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
-- 支付记录表
CREATE TABLE `t_pay_record` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`order_id` bigint NOT NULL COMMENT '订单ID',
`order_no` varchar(64) NOT NULL COMMENT '商户订单号',
`transaction_id` varchar(64) DEFAULT NULL COMMENT '微信支付订单号',
`pay_amount` int NOT NULL COMMENT '实际支付金额(分)',
`pay_time` datetime DEFAULT NULL COMMENT '支付时间',
`notify_time` datetime DEFAULT NULL COMMENT '通知接收时间',
`notify_data` text COMMENT '异步通知原始数据',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_transaction_id` (`transaction_id`),
KEY `idx_order_no` (`order_no`),
KEY `idx_pay_time` (`pay_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付记录表';
-- 退款记录表
CREATE TABLE `t_refund_record` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`order_id` bigint NOT NULL COMMENT '订单ID',
`order_no` varchar(64) NOT NULL COMMENT '商户订单号',
`refund_no` varchar(64) NOT NULL COMMENT '商户退款单号',
`transaction_id` varchar(64) DEFAULT NULL COMMENT '微信支付订单号',
`refund_id` varchar(64) DEFAULT NULL COMMENT '微信退款单号',
`refund_amount` int NOT NULL COMMENT '退款金额(分)',
`total_amount` int NOT NULL COMMENT '订单总金额(分)',
`refund_reason` varchar(256) DEFAULT NULL COMMENT '退款原因',
`refund_status` varchar(16) NOT NULL COMMENT '退款状态(REFUND_PROCESSING:退款中;REFUND_SUCCESS:退款成功;REFUND_FAILED:退款失败)',
`refund_time` datetime DEFAULT NULL COMMENT '退款成功时间',
`notify_time` datetime DEFAULT NULL COMMENT '退款通知接收时间',
`notify_data` text COMMENT '退款通知原始数据',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_refund_no` (`refund_no`),
UNIQUE KEY `uk_refund_id` (`refund_id`),
KEY `idx_order_no` (`order_no`),
KEY `idx_refund_time` (`refund_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='退款记录表';
3.5 基础实体类与通用响应类
3.5.1 订单实体类
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 订单实体类
* @author ken
*/
@Data
@TableName("t_order")
public class Order {
/** 主键ID */
@TableId(type = IdType.AUTO)
private Long id;
/** 商户订单号 */
private String orderNo;
/** APPID */
private String appId;
/** 商户号 */
private String mchId;
/** 用户ID */
private Long userId;
/** 订单总金额(分) */
private Integer totalAmount;
/** 订单标题 */
private String subject;
/** 支付方式(NATIVE/JSAPI/H5) */
private String payType;
/** 订单状态(PENDING:待支付;SUCCESS:支付成功;CLOSED:已关闭;REFUNDED:已退款) */
private String orderStatus;
/** 创建时间 */
private LocalDateTime createTime;
/** 更新时间 */
private LocalDateTime updateTime;
}
3.5.2 通用响应类
package com.jam.demo.common;
import com.alibaba.fastjson2.JSONObject;
import lombok.Data;
import org.springframework.http.HttpStatus;
/**
* 通用响应类
* @author ken
*/
@Data
public class R<T> {
/** 响应码 */
private int code;
/** 响应信息 */
private String msg;
/** 响应数据 */
private T data;
/**
* 成功响应(无数据)
* @return R<Void>
*/
public static <T> R<T> success() {
return new R<>(HttpStatus.OK.value(), "操作成功", null);
}
/**
* 成功响应(带数据)
* @param data 响应数据
* @return R<T>
*/
public static <T> R<T> success(T data) {
return new R<>(HttpStatus.OK.value(), "操作成功", data);
}
/**
* 失败响应
* @param msg 失败信息
* @return R<Void>
*/
public static <T> R<T> fail(String msg) {
return new R<>(HttpStatus.INTERNAL_SERVER_ERROR.value(), msg, null);
}
/**
* 失败响应(指定响应码)
* @param code 响应码
* @param msg 失败信息
* @return R<Void>
*/
public static <T> R<T> fail(int code, String msg) {
return new R<>(code, msg, null);
}
@Override
public String toString() {
return JSONObject.toJSONString(this);
}
}
四、核心支付能力实现:从预支付到支付完成
微信支付支持多种支付场景,其中Native支付(扫码支付)、JSAPI支付(公众号/小程序支付)是最常用的两种方式。本节将分别实现这两种支付方式,涵盖预支付订单生成、支付凭证获取、支付状态查询等核心功能。
4.1 通用工具类封装
先封装微信支付通用工具类,处理签名验证、订单号生成等通用逻辑,避免代码冗余。
package com.jam.demo.util;
import com.alibaba.fastjson2.JSONObject;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.Random;
/**
* 微信支付通用工具类
* @author ken
*/
@Slf4j
public class WechatPayUtil {
/**
* 生成商户订单号(规则:时间戳+6位随机数)
* @return 商户订单号
*/
public static String generateOrderNo() {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
String timeStr = LocalDateTime.now().format(formatter);
String randomStr = String.format("%06d", new Random().nextInt(1000000));
return timeStr + randomStr;
}
/**
* 生成商户退款单号(规则:REFUND+时间戳+6位随机数)
* @return 商户退款单号
*/
public static String generateRefundNo() {
return "REFUND" + generateOrderNo();
}
/**
* 验证微信支付回调签名
* 注:实际开发中可直接使用微信支付SDK提供的签名验证工具
* @param notifyData 回调原始数据(JSON格式)
* @param apiKey API密钥
* @param sign 回调中的签名
* @return 签名是否有效
*/
public static boolean verifyNotifySign(String notifyData, String apiKey, String sign) {
try {
// 1. 解析回调数据,获取所有非空参数
JSONObject jsonObject = JSONObject.parseObject(notifyData);
Map<String, String> params = Maps.newTreeMap(); // 按ASCII码升序排序
jsonObject.forEach((key, value) -> {
if (!"sign".equals(key) && value != null && StringUtils.hasText(value.toString())) {
params.put(key, value.toString());
}
});
// 2. 拼接参数字符串
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : params.entrySet()) {
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
sb.append("key=").append(apiKey);
// 3. 计算HMAC-SHA256签名
String calculatedSign = HmacSha256Util.sign(sb.toString(), apiKey);
log.info("微信支付回调签名验证:计算签名={},回调签名={}", calculatedSign, sign);
// 4. 对比签名
return StringUtils.hasText(calculatedSign) && calculatedSign.equalsIgnoreCase(sign);
} catch (Exception e) {
log.error("微信支付回调签名验证失败", e);
return false;
}
}
}
package com.jam.demo.util;
import lombok.extern.slf4j.Slf4j;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* HMAC-SHA256加密工具类
* @author ken
*/
@Slf4j
public class HmacSha256Util {
/**
* HMAC-SHA256加密
* @param data 待加密数据
* @param key 密钥
* @return 加密后的数据(大写)
*/
public static String sign(String data, String key) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(secretKeySpec);
byte[] bytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
// 转Base64并转为大写
return Base64.getEncoder().encodeToString(bytes).toUpperCase();
} catch (Exception e) {
log.error("HMAC-SHA256加密失败", e);
return null;
}
}
}
4.2 Native支付实现(扫码支付)
Native支付是指商户系统生成支付二维码,用户使用微信扫码完成支付的方式,适用于线下门店、自助设备等场景。
4.2.1 Native支付流程
4.2.2 Mapper接口与XML
package com.jam.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 订单Mapper
* @author ken
*/
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
/**
* 根据商户订单号查询订单
* @param orderNo 商户订单号
* @return 订单信息
*/
Order selectByOrderNo(@Param("orderNo") String orderNo);
/**
* 更新订单状态
* @param orderNo 商户订单号
* @param oldStatus 原状态
* @param newStatus 新状态
* @return 影响行数
*/
int updateOrderStatus(@Param("orderNo") String orderNo, @Param("oldStatus") String oldStatus, @Param("newStatus") String newStatus);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jam.demo.mapper.OrderMapper">
<select id="selectByOrderNo" resultType="com.jam.demo.entity.Order">
SELECT id, order_no, app_id, mch_id, user_id, total_amount, subject, pay_type, order_status, create_time, update_time
FROM t_order
WHERE order_no = #{orderNo}
</select>
<update id="updateOrderStatus">
UPDATE t_order
SET order_status = #{newStatus}, update_time = NOW()
WHERE order_no = #{orderNo} AND order_status = #{oldStatus}
</update>
</mapper>
4.2.3 Service层实现
package com.jam.demo.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.jam.demo.entity.Order;
import com.jam.demo.entity.PayRecord;
import com.jam.demo.vo.WechatNativePayVO;
/**
* 订单服务接口
* @author ken
*/
public interface OrderService extends IService<Order> {
/**
* 创建订单并发起Native支付
* @param userId 用户ID
* @param totalAmount 订单金额(分)
* @param subject 订单标题
* @return 微信Native支付信息(包含二维码链接)
*/
WechatNativePayVO createOrderAndNativePay(Long userId, Integer totalAmount, String subject);
/**
* 根据商户订单号查询订单
* @param orderNo 商户订单号
* @return 订单信息
*/
Order getOrderByOrderNo(String orderNo);
/**
* 更新订单状态
* @param orderNo 商户订单号
* @param oldStatus 原状态
* @param newStatus 新状态
* @return 是否更新成功
*/
boolean updateOrderStatus(String orderNo, String oldStatus, String newStatus);
/**
* 记录支付结果
* @param order 订单信息
* @param transactionId 微信支付订单号
* @param payAmount 支付金额(分)
* @param notifyData 回调原始数据
*/
void recordPayResult(Order order, String transactionId, Integer payAmount, String notifyData);
}
package com.jam.demo.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.demo.entity.Order;
import com.jam.demo.entity.PayRecord;
import com.jam.demo.mapper.OrderMapper;
import com.jam.demo.mapper.PayRecordMapper;
import com.jam.demo.service.OrderService;
import com.jam.demo.util.WechatPayUtil;
import com.jam.demo.vo.WechatNativePayVO;
import com.wechat.pay.java.core.exception.ServiceException;
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;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.time.LocalDateTime;
/**
* 订单服务实现类
* @author ken
*/
@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
@Resource
private OrderMapper orderMapper;
@Resource
private PayRecordMapper payRecordMapper;
@Resource
private NativePayService nativePayService;
@Resource
private WechatPayConfig wechatPayConfig;
/**
* 创建订单并发起Native支付
* @param userId 用户ID
* @param totalAmount 订单金额(分)
* @param subject 订单标题
* @return 微信Native支付信息(包含二维码链接)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public WechatNativePayVO createOrderAndNativePay(Long userId, Integer totalAmount, String subject) {
// 1. 校验参数
StringUtils.hasText(userId, "用户ID不能为空");
StringUtils.hasText(totalAmount, "订单金额不能为空");
if (totalAmount <= 0) {
throw new IllegalArgumentException("订单金额必须大于0");
}
StringUtils.hasText(subject, "订单标题不能为空");
// 2. 生成商户订单号
String orderNo = WechatPayUtil.generateOrderNo();
// 3. 创建订单
Order order = new Order();
order.setOrderNo(orderNo);
order.setAppId(wechatPayConfig.getAppId());
order.setMchId(wechatPayConfig.getMchId());
order.setUserId(userId);
order.setTotalAmount(totalAmount);
order.setSubject(subject);
order.setPayType("NATIVE");
order.setOrderStatus("PENDING"); // 待支付
order.setCreateTime(LocalDateTime.now());
order.setUpdateTime(LocalDateTime.now());
int insertCount = orderMapper.insert(order);
if (insertCount <= 0) {
log.error("创建订单失败,orderNo={}", orderNo);
throw new RuntimeException("创建订单失败");
}
// 4. 调用微信支付Native下单接口,获取预支付信息
PrepayRequest prepayRequest = new PrepayRequest();
prepayRequest.setAppid(wechatPayConfig.getAppId());
prepayRequest.setMchid(wechatPayConfig.getMchId());
prepayRequest.setDescription(subject);
prepayRequest.setOutTradeNo(orderNo);
prepayRequest.setNotifyUrl(wechatPayConfig.getNotifyUrl());
// 设置金额信息(单位:分)
Amount amount = new Amount();
amount.setTotal(totalAmount);
prepayRequest.setAmount(amount);
try {
// 调用微信支付SDK接口
PrepayResponse prepayResponse = nativePayService.prepay(prepayRequest);
log.info("Native支付预下单成功,orderNo={},prepayId={},codeUrl={}",
orderNo, prepayResponse.getPrepayId(), prepayResponse.getCodeUrl());
// 5. 封装返回结果(前端根据codeUrl生成二维码)
WechatNativePayVO payVO = new WechatNativePayVO();
payVO.setOrderNo(orderNo);
payVO.setPrepayId(prepayResponse.getPrepayId());
payVO.setCodeUrl(prepayResponse.getCodeUrl());
payVO.setTotalAmount(totalAmount);
return payVO;
} catch (ServiceException e) {
log.error("Native支付预下单失败,orderNo={},错误信息={}", orderNo, e.getMessage(), e);
throw new RuntimeException("发起支付失败:" + e.getMessage());
}
}
/**
* 根据商户订单号查询订单
* @param orderNo 商户订单号
* @return 订单信息
*/
@Override
public Order getOrderByOrderNo(String orderNo) {
StringUtils.hasText(orderNo, "商户订单号不能为空");
return orderMapper.selectByOrderNo(orderNo);
}
/**
* 更新订单状态
* @param orderNo 商户订单号
* @param oldStatus 原状态
* @param newStatus 新状态
* @return 是否更新成功
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateOrderStatus(String orderNo, String oldStatus, String newStatus) {
StringUtils.hasText(orderNo, "商户订单号不能为空");
StringUtils.hasText(oldStatus, "原状态不能为空");
StringUtils.hasText(newStatus, "新状态不能为空");
int updateCount = orderMapper.updateOrderStatus(orderNo, oldStatus, newStatus);
return updateCount > 0;
}
/**
* 记录支付结果
* @param order 订单信息
* @param transactionId 微信支付订单号
* @param payAmount 支付金额(分)
* @param notifyData 回调原始数据
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void recordPayResult(Order order, String transactionId, Integer payAmount, String notifyData) {
StringUtils.hasText(order, "订单信息不能为空");
StringUtils.hasText(transactionId, "微信支付订单号不能为空");
StringUtils.hasText(payAmount, "支付金额不能为空");
// 1. 构建支付记录
PayRecord payRecord = new PayRecord();
payRecord.setOrderId(order.getId());
payRecord.setOrderNo(order.getOrderNo());
payRecord.setTransactionId(transactionId);
payRecord.setPayAmount(payAmount);
payRecord.setPayTime(LocalDateTime.now());
payRecord.setNotifyTime(LocalDateTime.now());
payRecord.setNotifyData(notifyData);
payRecord.setCreateTime(LocalDateTime.now());
payRecord.setUpdateTime(LocalDateTime.now());
// 2. 插入支付记录
int insertCount = payRecordMapper.insert(payRecord);
if (insertCount <= 0) {
log.error("记录支付结果失败,orderNo={},transactionId={}", order.getOrderNo(), transactionId);
throw new RuntimeException("记录支付结果失败");
}
// 3. 更新订单状态为支付成功
boolean updateSuccess = updateOrderStatus(order.getOrderNo(), "PENDING", "SUCCESS");
if (!updateSuccess) {
log.error("更新订单状态失败,orderNo={},当前状态不为待支付", order.getOrderNo());
throw new RuntimeException("更新订单状态失败");
}
log.info("记录支付结果成功,orderNo={},transactionId={}", order.getOrderNo(), transactionId);
}
}
4.2.4 Controller层实现
package com.jam.demo.controller;
import com.jam.demo.common.R;
import com.jam.demo.service.OrderService;
import com.jam.demo.vo.WechatNativePayVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 微信支付控制器
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/api/wechat/pay")
@Tag(name = "微信支付相关接口", description = "包含Native支付、JSAPI支付等核心接口")
public class WechatPayController {
@Resource
private OrderService orderService;
/**
* 创建订单并发起Native支付
* @param userId 用户ID
* @param totalAmount 订单金额(分)
* @param subject 订单标题
* @return 支付信息(包含二维码链接)
*/
@PostMapping("/native/create")
@Operation(summary = "Native支付下单", description = "创建订单并返回微信支付二维码链接,用户扫码完成支付")
public R<WechatNativePayVO> createNativePay(
@Parameter(description = "用户ID", required = true) @RequestParam Long userId,
@Parameter(description = "订单金额(分)", required = true) @RequestParam Integer totalAmount,
@Parameter(description = "订单标题", required = true) @RequestParam String subject) {
try {
WechatNativePayVO payVO = orderService.createOrderAndNativePay(userId, totalAmount, subject);
return R.success(payVO);
} catch (IllegalArgumentException e) {
log.error("Native支付下单参数错误", e);
return R.fail(e.getMessage());
} catch (Exception e) {
log.error("Native支付下单失败", e);
return R.fail("Native支付下单失败");
}
}
}
4.2.5 支付状态查询接口
// 在WechatPayController中添加
@GetMapping("/query")
@Operation(summary = "查询支付状态", description = "根据商户订单号查询支付结果")
public R<PayStatusVO> queryPayStatus(
@Parameter(description = "商户订单号", required = true) @RequestParam String orderNo) {
try {
// 1. 查询订单
Order order = orderService.getOrderByOrderNo(orderNo);
if (ObjectUtils.isEmpty(order)) {
return R.fail("订单不存在");
}
// 2. 如果订单已支付,直接返回结果
if ("SUCCESS".equals(order.getOrderStatus())) {
PayRecord payRecord = payRecordService.getByOrderNo(orderNo);
PayStatusVO statusVO = new PayStatusVO();
statusVO.setOrderNo(orderNo);
statusVO.setOrderStatus(order.getOrderStatus());
statusVO.setTransactionId(payRecord.getTransactionId());
statusVO.setPayTime(payRecord.getPayTime());
return R.success(statusVO);
}
// 3. 调用微信支付查询接口,获取最新支付状态
QueryOrderRequest queryRequest = new QueryOrderRequest();
queryRequest.setOutTradeNo(orderNo);
QueryOrderResponse queryResponse = nativePayService.queryOrder(queryRequest);
// 4. 封装返回结果
PayStatusVO statusVO = new PayStatusVO();
statusVO.setOrderNo(orderNo);
statusVO.setOrderStatus("SUCCESS".equals(queryResponse.getTradeState()) ? "SUCCESS" : "PENDING");
statusVO.setTransactionId(queryResponse.getTransactionId());
statusVO.setPayTime("SUCCESS".equals(queryResponse.getTradeState()) ?
LocalDateTime.parse(queryResponse.getSuccessTime(), DateTimeFormatter.ISO_LOCAL_DATE_TIME) : null);
// 5. 如果查询到支付成功,同步更新订单状态和支付记录
if ("SUCCESS".equals(queryResponse.getTradeState()) && "PENDING".equals(order.getOrderStatus())) {
orderService.recordPayResult(order, queryResponse.getTransactionId(),
queryResponse.getAmount().getTotal(), JSONObject.toJSONString(queryResponse));
}
return R.success(statusVO);
} catch (ServiceException e) {
log.error("查询支付状态失败,orderNo={}", orderNo, e);
return R.fail("查询支付状态失败:" + e.getMessage());
} catch (Exception e) {
log.error("查询支付状态失败,orderNo={}", orderNo, e);
return R.fail("查询支付状态失败");
}
}
4.3 JSAPI支付实现(公众号/小程序支付)
JSAPI支付是指用户在公众号/小程序内发起的支付,需先获取用户的openid(用户在公众号/小程序内的唯一标识),再发起支付请求。
4.3.1 JSAPI支付流程
4.3.2 获取用户OpenID(公众号场景)
package com.jam.demo.controller;
import com.jam.demo.common.R;
import com.jam.demo.config.WechatPayConfig;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
/**
* 微信公众号相关接口
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/api/wechat/mp")
@Tag(name = "微信公众号相关接口", description = "包含获取用户openid等接口")
public class WechatMpController {
@Resource
private WechatPayConfig wechatPayConfig;
@Resource
private RestTemplate restTemplate;
/**
* 微信公众号授权回调,获取用户openid
* 注:需先在公众号后台配置授权回调域名
* @param code 授权临时票据
* @return openid
*/
@GetMapping("/oauth/callback")
@Operation(summary = "公众号授权回调", description = "通过授权临时票据获取用户openid")
public R<Map<String, String>> oauthCallback(
@Parameter(description = "授权临时票据", required = true) @RequestParam String code) {
try {
// 微信公众号获取openid的接口地址
String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid={appid}&secret={secret}&code={code}&grant_type=authorization_code";
// 构建请求参数
Map<String, String> params = new HashMap<>();
params.put("appid", wechatPayConfig.getAppId());
params.put("secret", "你的公众号开发者密码"); // 从公众号后台获取
params.put("code", code);
// 发送请求
Map<String, String> result = restTemplate.getForObject(url, Map.class, params);
log.info("公众号授权回调结果:{}", result);
// 提取openid
String openid = result.get("openid");
if (org.springframework.util.StringUtils.isEmpty(openid)) {
log.error("获取openid失败,错误信息:{}", result.get("errmsg"));
return R.fail("获取用户信息失败");
}
Map<String, String> data = new HashMap<>();
data.put("openid", openid);
return R.success(data);
} catch (Exception e) {
log.error("公众号授权回调失败", e);
return R.fail("授权失败");
}
}
}
4.3.3 JSAPI支付核心代码
// OrderServiceImpl实现方法
/**
* 创建订单并发起JSAPI支付
* @param userId 用户ID
* @param openid 用户openid
* @param totalAmount 订单金额(分)
* @param subject 订单标题
* @return 前端支付参数(含签名)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public WechatJsapiPayVO createOrderAndJsapiPay(Long userId, String openid, Integer totalAmount, String subject) {
// 1. 校验参数
StringUtils.hasText(userId.toString(), "用户ID不能为空");
StringUtils.hasText(openid, "用户openid不能为空");
StringUtils.hasText(totalAmount.toString(), "订单金额不能为空");
if (totalAmount <= 0) {
throw new IllegalArgumentException("订单金额必须大于0");
}
StringUtils.hasText(subject, "订单标题不能为空");
// 2. 生成商户订单号
String orderNo = WechatPayUtil.generateOrderNo();
// 3. 创建订单
Order order = new Order();
order.setOrderNo(orderNo);
order.setAppId(wechatPayConfig.getAppId());
order.setMchId(wechatPayConfig.getMchId());
order.setUserId(userId);
order.setTotalAmount(totalAmount);
order.setSubject(subject);
order.setPayType("JSAPI");
order.setOrderStatus("PENDING"); // 待支付
order.setCreateTime(LocalDateTime.now());
order.setUpdateTime(LocalDateTime.now());
int insertCount = orderMapper.insert(order);
if (insertCount <= 0) {
log.error("创建JSAPI支付订单失败,orderNo={}", orderNo);
throw new RuntimeException("创建订单失败");
}
// 4. 调用微信支付JSAPI下单接口,获取prepay_id
PrepayRequest jsapiPrepayRequest = new PrepayRequest();
jsapiPrepayRequest.setAppid(wechatPayConfig.getAppId());
jsapiPrepayRequest.setMchid(wechatPayConfig.getMchId());
jsapiPrepayRequest.setDescription(subject);
jsapiPrepayRequest.setOutTradeNo(orderNo);
jsapiPrepayRequest.setNotifyUrl(wechatPayConfig.getNotifyUrl());
// 设置金额信息
Amount amount = new Amount();
amount.setTotal(totalAmount);
jsapiPrepayRequest.setAmount(amount);
// 设置支付者信息(核心:JSAPI必须传openid)
Payer payer = new Payer();
payer.setOpenid(openid);
jsapiPrepayRequest.setPayer(payer);
try {
// 调用微信支付SDK接口
com.wechat.pay.java.service.payments.jsapi.model.PrepayResponse prepayResponse =
jsapiPayService.prepay(jsapiPrepayRequest);
log.info("JSAPI支付预下单成功,orderNo={},prepayId={}", orderNo, prepayResponse.getPrepayId());
// 5. 生成前端调起支付的参数(需签名,防止参数篡改)
WechatJsapiPayVO jsapiPayVO = new WechatJsapiPayVO();
jsapiPayVO.setAppId(wechatPayConfig.getAppId());
jsapiPayVO.setTimeStamp(String.valueOf(System.currentTimeMillis() / 1000));
jsapiPayVO.setNonceStr(UUID.randomUUID().toString().replace("-", ""));
jsapiPayVO.setPackageStr("prepay_id=" + prepayResponse.getPrepayId());
jsapiPayVO.setSignType("RSA"); // 新版微信支付推荐RSA签名
// 6. 生成前端支付签名(按微信规范拼接参数并签名)
String signStr = String.format(
"%s\n%s\n%s\n%s\n",
jsapiPayVO.getAppId(),
jsapiPayVO.getTimeStamp(),
jsapiPayVO.getNonceStr(),
jsapiPayVO.getPackageStr()
);
// 加载商户私钥进行签名(实际项目中私钥需安全存储,避免硬编码)
PrivateKey privateKey = getMerchantPrivateKey();
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(signStr.getBytes(StandardCharsets.UTF_8));
String paySign = Base64.getEncoder().encodeToString(signature.sign());
jsapiPayVO.setPaySign(paySign);
jsapiPayVO.setOrderNo(orderNo);
return jsapiPayVO;
} catch (ServiceException e) {
log.error("JSAPI支付预下单失败,orderNo={},错误码={},错误信息={}",
orderNo, e.getErrorCode(), e.getMessage(), e);
throw new RuntimeException("发起JSAPI支付失败:" + e.getMessage());
} catch (Exception e) {
log.error("JSAPI支付参数签名失败,orderNo={}", orderNo, e);
throw new RuntimeException("生成支付参数失败");
}
}
/**
* 获取商户私钥(实际项目中建议从配置中心/密钥管理系统获取,避免硬编码)
* @return 商户私钥
* @throws Exception 私钥加载异常
*/
private PrivateKey getMerchantPrivateKey() throws Exception {
// 从classpath加载私钥文件(apiclient_key.pem)
InputStream privateKeyStream = getClass().getResourceAsStream("/cert/apiclient_key.pem");
if (ObjectUtils.isEmpty(privateKeyStream)) {
throw new FileNotFoundException("商户私钥文件不存在");
}
String privateKeyStr = new String(privateKeyStream.readAllBytes(), StandardCharsets.UTF_8)
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyStr)));
}
4.3.4 JSAPI支付VO类定义
package com.jam.demo.vo;
import lombok.Data;
/**
* JSAPI支付前端调起参数VO
* @author ken
*/
@Data
public class WechatJsapiPayVO {
/** 公众号APPID */
private String appId;
/** 时间戳(秒) */
private String timeStamp;
/** 随机字符串 */
private String nonceStr;
/** 预支付会话标识(固定格式prepay_id=xxx) */
private String packageStr;
/** 签名类型(RSA/SHA256-RSA) */
private String signType;
/** 支付签名 */
private String paySign;
/** 商户订单号 */
private String orderNo;
}
4.3.5 JSAPI支付Controller接口
// 在WechatPayController中添加
/**
* 创建订单并发起JSAPI支付
* @param userId 用户ID
* @param openid 用户openid
* @param totalAmount 订单金额(分)
* @param subject 订单标题
* @return 前端支付参数
*/
@PostMapping("/jsapi/create")
@Operation(summary = "JSAPI支付下单", description = "创建订单并返回前端调起微信支付的参数,适用于公众号/小程序支付")
public R<WechatJsapiPayVO> createJsapiPay(
@Parameter(description = "用户ID", required = true) @RequestParam Long userId,
@Parameter(description = "用户openid", required = true) @RequestParam String openid,
@Parameter(description = "订单金额(分)", required = true) @RequestParam Integer totalAmount,
@Parameter(description = "订单标题", required = true) @RequestParam String subject) {
try {
WechatJsapiPayVO jsapiPayVO = orderService.createOrderAndJsapiPay(userId, openid, totalAmount, subject);
return R.success(jsapiPayVO);
} catch (IllegalArgumentException e) {
log.error("JSAPI支付下单参数错误", e);
return R.fail(e.getMessage());
} catch (Exception e) {
log.error("JSAPI支付下单失败", e);
return R.fail("JSAPI支付下单失败");
}
}
五、支付异步回调处理:核心保障与幂等设计
微信支付的异步回调(notify_url)是确认支付状态的唯一可靠方式,同步返回的支付结果仅作参考。回调处理需满足:签名验证、参数解析、幂等性保障、结果返回规范四大核心要求。
5.1 异步回调处理流程
5.2 异步回调核心代码实现
package com.jam.demo.controller;
import com.alibaba.fastjson2.JSONObject;
import com.jam.demo.common.R;
import com.jam.demo.entity.Order;
import com.jam.demo.service.OrderService;
import com.jam.demo.util.WechatPayUtil;
import com.wechat.pay.java.core.notification.NotificationConfig;
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.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
/**
* 微信支付回调控制器
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/api/wechat/pay/notify")
public class WechatPayNotifyController {
@Resource
private OrderService orderService;
@Resource
private WechatPayConfig wechatPayConfig;
/**
* 支付异步回调处理
* 注:接口需为公网可访问,且不能有登录拦截、参数校验等额外限制
* @param requestBody 回调原始数据
* @param wechatPaySerial 微信支付证书序列号
* @param wechatPaySignature 微信支付签名
* @param wechatPayTimestamp 时间戳
* @param wechatPayNonce 随机串
* @return 回调响应(SUCCESS/FAIL)
*/
@PostMapping("/pay")
public String payNotify(
@RequestBody String requestBody,
@RequestHeader("Wechatpay-Serial") String wechatPaySerial,
@RequestHeader("Wechatpay-Signature") String wechatPaySignature,
@RequestHeader("Wechatpay-Timestamp") String wechatPayTimestamp,
@RequestHeader("Wechatpay-Nonce") String wechatPayNonce) {
log.info("收到微信支付异步回调,请求体={},serial={},signature={}",
requestBody, wechatPaySerial, wechatPaySignature);
// 1. 构建回调请求参数
RequestParam requestParam = new RequestParam.Builder()
.serialNumber(wechatPaySerial)
.nonce(wechatPayNonce)
.signature(wechatPaySignature)
.timestamp(wechatPayTimestamp)
.body(requestBody)
.build();
try {
// 2. 初始化通知解析器(自动验证签名)
NotificationConfig config = new NotificationConfig.Builder()
.merchantId(wechatPayConfig.getMchId())
.apiV3Key(wechatPayConfig.getApiKey())
.build();
NotificationParser parser = new NotificationParser(config);
// 3. 解析回调并验证签名,失败会抛出异常
Transaction transaction = parser.parse(requestParam, Transaction.class);
String outTradeNo = transaction.getOutTradeNo(); // 商户订单号
String transactionId = transaction.getTransactionId(); // 微信支付订单号
Integer totalAmount = transaction.getAmount().getTotal(); // 支付金额(分)
String tradeState = transaction.getTradeState(); // 支付状态(SUCCESS为成功)
// 4. 校验支付状态
if (!"SUCCESS".equals(tradeState)) {
log.warn("支付未成功,orderNo={},tradeState={}", outTradeNo, tradeState);
return buildFailResponse("支付未成功");
}
// 5. 幂等性校验:查询订单是否已处理过
Order order = orderService.getOrderByOrderNo(outTradeNo);
if (ObjectUtils.isEmpty(order)) {
log.error("回调订单不存在,orderNo={}", outTradeNo);
return buildFailResponse("订单不存在");
}
if ("SUCCESS".equals(order.getOrderStatus())) {
log.info("订单已处理过,无需重复处理,orderNo={}", outTradeNo);
return buildSuccessResponse();
}
// 6. 校验金额一致性(防止金额篡改)
if (!totalAmount.equals(order.getTotalAmount())) {
log.error("回调金额与订单金额不一致,orderNo={},回调金额={},订单金额={}",
outTradeNo, totalAmount, order.getTotalAmount());
return buildFailResponse("金额不一致");
}
// 7. 处理业务逻辑:更新订单状态+记录支付记录
orderService.recordPayResult(order, transactionId, totalAmount, requestBody);
// 8. 返回成功响应(必须按此格式,否则微信会重复回调)
return buildSuccessResponse();
} catch (Exception e) {
log.error("处理微信支付回调失败", e);
// 9. 返回失败响应,微信会重试(默认重试策略:15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h)
return buildFailResponse("处理失败:" + e.getMessage());
}
}
/**
* 构建成功响应(微信要求固定JSON格式)
* @return 成功响应字符串
*/
private String buildSuccessResponse() {
JSONObject result = new JSONObject();
result.put("code", "SUCCESS");
result.put("message", "成功");
return result.toString();
}
/**
* 构建失败响应
* @param message 失败原因
* @return 失败响应字符串
*/
private String buildFailResponse(String message) {
JSONObject result = new JSONObject();
result.put("code", "FAIL");
result.put("message", message);
return result.toString();
}
}
5.3 回调处理关键注意事项
- 幂等性保障:必须通过“商户订单号+订单状态”判断是否已处理,避免重复更新订单/重复入账;
- 响应格式:成功响应必须是
{"code":"SUCCESS","message":"成功"},失败是{"code":"FAIL","message":"失败原因"},格式错误会导致微信重复回调; - 超时处理:回调处理逻辑需在5秒内完成,复杂业务需异步解耦(如通过MQ处理);
- 日志记录:必须完整记录回调的所有参数、处理结果,便于问题排查;
- 重试机制:微信支付回调失败会按固定策略重试,需保证接口幂等性,同时监控重试次数,避免漏处理。
六、退款功能实现:从接口调用到回调处理
退款是支付业务的必备能力,微信支付退款接口需使用商户证书鉴权,且需处理退款异步回调,保障退款状态的准确性。
6.1 退款业务流程
6.2 退款记录表Mapper与实体类
6.2.1 退款记录实体类
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 退款记录表实体类
* @author ken
*/
@Data
@TableName("t_refund_record")
public class RefundRecord {
/** 主键ID */
@TableId(type = IdType.AUTO)
private Long id;
/** 订单ID */
private Long orderId;
/** 商户订单号 */
private String orderNo;
/** 商户退款单号 */
private String refundNo;
/** 微信支付订单号 */
private String transactionId;
/** 微信退款单号 */
private String refundId;
/** 退款金额(分) */
private Integer refundAmount;
/** 订单总金额(分) */
private Integer totalAmount;
/** 退款原因 */
private String refundReason;
/** 退款状态(REFUND_PROCESSING:退款中;REFUND_SUCCESS:退款成功;REFUND_FAILED:退款失败) */
private String refundStatus;
/** 退款成功时间 */
private LocalDateTime refundTime;
/** 退款通知接收时间 */
private LocalDateTime notifyTime;
/** 退款通知原始数据 */
private String notifyData;
/** 创建时间 */
private LocalDateTime createTime;
/** 更新时间 */
private LocalDateTime updateTime;
}
6.2.2 退款记录Mapper接口
package com.jam.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.RefundRecord;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 退款记录Mapper
* @author ken
*/
@Mapper
public interface RefundRecordMapper extends BaseMapper<RefundRecord> {
/**
* 根据商户退款单号查询退款记录
* @param refundNo 商户退款单号
* @return 退款记录
*/
RefundRecord selectByRefundNo(@Param("refundNo") String refundNo);
/**
* 更新退款状态
* @param refundNo 商户退款单号
* @param oldStatus 原状态
* @param newStatus 新状态
* @param refundId 微信退款单号
* @param refundTime 退款成功时间
* @return 影响行数
*/
int updateRefundStatus(
@Param("refundNo") String refundNo,
@Param("oldStatus") String oldStatus,
@Param("newStatus") String newStatus,
@Param("refundId") String refundId,
@Param("refundTime") LocalDateTime refundTime);
}
6.2.3 退款记录Mapper XML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jam.demo.mapper.RefundRecordMapper">
<select id="selectByRefundNo" resultType="com.jam.demo.entity.RefundRecord">
SELECT id, order_id, order_no, refund_no, transaction_id, refund_id, refund_amount,
total_amount, refund_reason, refund_status, refund_time, notify_time, notify_data,
create_time, update_time
FROM t_refund_record
WHERE refund_no = #{refundNo}
</select>
<update id="updateRefundStatus">
UPDATE t_refund_record
SET refund_status = #{newStatus},
refund_id = #{refundId},
refund_time = #{refundTime},
update_time = NOW()
WHERE refund_no = #{refundNo} AND refund_status = #{oldStatus}
</update>
</mapper>
6.3 退款Service层实现
6.3.1 退款Service接口
package com.jam.demo.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.jam.demo.entity.RefundRecord;
import com.jam.demo.vo.RefundVO;
/**
* 退款服务接口
* @author ken
*/
public interface RefundService extends IService<RefundRecord> {
/**
* 发起退款
* @param orderNo 商户订单号
* @param refundAmount 退款金额(分)
* @param refundReason 退款原因
* @return 退款结果
*/
RefundVO createRefund(String orderNo, Integer refundAmount, String refundReason);
/**
* 处理退款异步回调
* @param notifyData 回调原始数据
* @return 是否处理成功
*/
boolean handleRefundNotify(String notifyData);
}
6.3.2 退款Service实现类
package com.jam.demo.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.demo.entity.Order;
import com.jam.demo.entity.RefundRecord;
import com.jam.demo.mapper.OrderMapper;
import com.jam.demo.mapper.RefundRecordMapper;
import com.jam.demo.service.OrderService;
import com.jam.demo.service.RefundService;
import com.jam.demo.util.WechatPayUtil;
import com.jam.demo.vo.RefundVO;
import com.wechat.pay.java.core.exception.ServiceException;
import com.wechat.pay.java.service.refund.RefundService;
import com.wechat.pay.java.service.refund.model.Amount;
import com.wechat.pay.java.service.refund.model.CreateRefundRequest;
import com.wechat.pay.java.service.refund.model.CreateRefundResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.time.LocalDateTime;
/**
* 退款服务实现类
* @author ken
*/
@Slf4j
@Service
public class RefundServiceImpl extends ServiceImpl<RefundRecordMapper, RefundRecord> implements RefundService {
@Resource
private RefundRecordMapper refundRecordMapper;
@Resource
private OrderService orderService;
@Resource
private OrderMapper orderMapper;
@Resource
private WechatPayConfig wechatPayConfig;
@Resource
private com.wechat.pay.java.service.refund.RefundService wxRefundService;
/**
* 发起退款
* @param orderNo 商户订单号
* @param refundAmount 退款金额(分)
* @param refundReason 退款原因
* @return 退款结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public RefundVO createRefund(String orderNo, Integer refundAmount, String refundReason) {
// 1. 参数校验
StringUtils.hasText(orderNo, "商户订单号不能为空");
StringUtils.hasText(refundAmount.toString(), "退款金额不能为空");
if (refundAmount <= 0) {
throw new IllegalArgumentException("退款金额必须大于0");
}
StringUtils.hasText(refundReason, "退款原因不能为空");
// 2. 查询订单,校验退款条件
Order order = orderService.getOrderByOrderNo(orderNo);
if (ObjectUtils.isEmpty(order)) {
throw new RuntimeException("订单不存在");
}
if (!"SUCCESS".equals(order.getOrderStatus())) {
throw new RuntimeException("仅支付成功的订单可发起退款");
}
if (refundAmount > order.getTotalAmount()) {
throw new RuntimeException("退款金额不能超过订单总金额");
}
// 3. 校验是否已发起过退款(避免重复退款)
RefundRecord existRefund = lambdaQuery()
.eq(RefundRecord::getOrderNo, orderNo)
.eq(RefundRecord::getRefundStatus, "REFUND_PROCESSING")
.or()
.eq(RefundRecord::getRefundStatus, "REFUND_SUCCESS")
.one();
if (!ObjectUtils.isEmpty(existRefund)) {
throw new RuntimeException("该订单已发起过退款,请勿重复操作");
}
// 4. 生成商户退款单号
String refundNo = WechatPayUtil.generateRefundNo();
// 5. 记录退款申请(状态:退款中)
RefundRecord refundRecord = new RefundRecord();
refundRecord.setOrderId(order.getId());
refundRecord.setOrderNo(orderNo);
refundRecord.setRefundNo(refundNo);
refundRecord.setTransactionId(null); // 微信支付订单号后续补充
refundRecord.setRefundId(null); // 微信退款单号后续补充
refundRecord.setRefundAmount(refundAmount);
refundRecord.setTotalAmount(order.getTotalAmount());
refundRecord.setRefundReason(refundReason);
refundRecord.setRefundStatus("REFUND_PROCESSING");
refundRecord.setCreateTime(LocalDateTime.now());
refundRecord.setUpdateTime(LocalDateTime.now());
int insertCount = refundRecordMapper.insert(refundRecord);
if (insertCount <= 0) {
log.error("记录退款申请失败,refundNo={}", refundNo);
throw new RuntimeException("发起退款失败");
}
// 6. 调用微信支付退款接口
CreateRefundRequest refundRequest = new CreateRefundRequest();
refundRequest.setOutTradeNo(orderNo);
refundRequest.setOutRefundNo(refundNo);
refundRequest.setReason(refundReason);
refundRequest.setNotifyUrl(wechatPayConfig.getRefundNotifyUrl());
// 设置金额信息
Amount amount = new Amount();
amount.setRefund(refundAmount);
amount.setTotal(order.getTotalAmount());
amount.setCurrency("CNY");
refundRequest.setAmount(amount);
try {
CreateRefundResponse refundResponse = wxRefundService.createRefund(refundRequest);
log.info("调用微信退款接口成功,orderNo={},refundNo={},refundId={}",
orderNo, refundNo, refundResponse.getRefundId());
// 7. 更新退款记录的微信退款单号
refundRecord.setTransactionId(refundResponse.getTransactionId());
refundRecord.setRefundId(refundResponse.getRefundId());
refundRecordMapper.updateById(refundRecord);
// 8. 封装返回结果
RefundVO refundVO = new RefundVO();
refundVO.setOrderNo(orderNo);
refundVO.setRefundNo(refundNo);
refundVO.setRefundAmount(refundAmount);
refundVO.setRefundStatus("REFUND_PROCESSING");
refundVO.setRefundId(refundResponse.getRefundId());
return refundVO;
} catch (ServiceException e) {
log.error("调用微信退款接口失败,orderNo={},refundNo={},错误码={},错误信息={}",
orderNo, refundNo, e.getErrorCode(), e.getMessage(), e);
// 回滚退款记录(或更新为失败状态)
refundRecord.setRefundStatus("REFUND_FAILED");
refundRecordMapper.updateById(refundRecord);
throw new RuntimeException("发起退款失败:" + e.getMessage());
}
}
/**
* 处理退款异步回调
* @param notifyData 回调原始数据
* @return 是否处理成功
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean handleRefundNotify(String notifyData) {
try {
// 1. 解析退款回调数据
JSONObject notifyJson = JSONObject.parseObject(notifyData);
String outRefundNo = notifyJson.getString("out_refund_no"); // 商户退款单号
String refundId = notifyJson.getString("refund_id"); // 微信退款单号
String refundStatus = notifyJson.getString("refund_status"); // 退款状态
// 2. 查询退款记录
RefundRecord refundRecord = refundRecordMapper.selectByRefundNo(outRefundNo);
if (ObjectUtils.isEmpty(refundRecord)) {
log.error("退款回调记录不存在,outRefundNo={}", outRefundNo);
return false;
}
// 3. 幂等性校验
if (!"REFUND_PROCESSING".equals(refundRecord.getRefundStatus())) {
log.info("退款记录已处理,无需重复操作,outRefundNo={}", outRefundNo);
return true;
}
// 4. 更新退款状态
String newStatus = "";
LocalDateTime refundTime = null;
if ("SUCCESS".equals(refundStatus)) {
newStatus = "REFUND_SUCCESS";
refundTime = LocalDateTime.now();
// 同步更新订单状态为已退款
orderService.updateOrderStatus(refundRecord.getOrderNo(), "SUCCESS", "REFUNDED");
} else if ("CLOSED".equals(refundStatus)) {
newStatus = "REFUND_FAILED";
} else {
log.warn("退款状态未知,outRefundNo={},refundStatus={}", outRefundNo, refundStatus);
return false;
}
int updateCount = refundRecordMapper.updateRefundStatus(
outRefundNo,
"REFUND_PROCESSING",
newStatus,
refundId,
refundTime
);
// 5. 记录回调数据
refundRecord.setNotifyTime(LocalDateTime.now());
refundRecord.setNotifyData(notifyData);
refundRecordMapper.updateById(refundRecord);
log.info("处理退款回调成功,outRefundNo={},refundStatus={}", outRefundNo, newStatus);
return updateCount > 0;
} catch (Exception e) {
log.error("处理退款回调失败", e);
return false;
}
}
}
6.4 退款Controller接口
package com.jam.demo.controller;
import com.jam.demo.common.R;
import com.jam.demo.service.RefundService;
import com.jam.demo.vo.RefundVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 退款控制器
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/api/wechat/refund")
@Tag(name = "退款相关接口", description = "包含发起退款、退款回调处理等接口")
public class RefundController {
@Resource
private RefundService refundService;
/**
* 发起退款
* @param orderNo 商户订单号
* @param refundAmount 退款金额(分)
* @param refundReason 退款原因
* @return 退款结果
*/
@PostMapping("/create")
@Operation(summary = "发起退款", description = "对已支付成功的订单发起退款,需校验退款金额和订单状态")
public R<RefundVO> createRefund(
@Parameter(description = "商户订单号", required = true) @RequestParam String orderNo,
@Parameter(description = "退款金额(分)", required = true) @RequestParam Integer refundAmount,
@Parameter(description = "退款原因", required = true) @RequestParam String refundReason) {
try {
RefundVO refundVO = refundService.createRefund(orderNo, refundAmount, refundReason);
return R.success(refundVO);
} catch (IllegalArgumentException e) {
log.error("发起退款参数错误", e);
return R.fail(e.getMessage());
} catch (Exception e) {
log.error("发起退款失败", e);
return R.fail("发起退款失败:" + e.getMessage());
}
}
/**
* 退款异步回调处理
* @param notifyData 回调原始数据
* @return 回调响应
*/
@PostMapping("/notify")
@Operation(summary = "退款异步回调", description = "接收微信支付退款结果回调,更新退款状态")
public String refundNotify(@Parameter(description = "回调原始数据", required = true) @RequestParam String notifyData) {
try {
boolean handleSuccess = refundService.handleRefundNotify(notifyData);
if (handleSuccess) {
return "{\"code\":\"SUCCESS\",\"message\":\"成功\"}";
} else {
return "{\"code\":\"FAIL\",\"message\":\"处理失败\"}";
}
} catch (Exception e) {
log.error("处理退款回调失败", e);
return "{\"code\":\"FAIL\",\"message\":\"处理异常\"}";
}
}
}
6.5 退款VO类定义
package com.jam.demo.vo;
import lombok.Data;
/**
* 退款结果VO
* @author ken
*/
@Data
public class RefundVO {
/** 商户订单号 */
private String orderNo;
/** 商户退款单号 */
private String refundNo;
/** 退款金额(分) */
private Integer refundAmount;
/** 退款状态 */
private String refundStatus;
/** 微信退款单号 */
private String refundId;
}
七、对账与账单下载:资金安全的最后一道防线
对账是保障资金安全的核心环节,微信支付提供日账单、交易账单、退款账单等多种账单类型,商户需定期下载并核对,确保交易数据与资金流水一致。
7.1 对账流程
7.2 账单下载核心代码
package com.jam.demo.service;
import com.wechat.pay.java.service.bill.BillService;
import com.wechat.pay.java.service.bill.model.DownloadBillRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.zip.GZIPInputStream;
/**
* 对账服务实现类
* @author ken
*/
@Slf4j
@Service
public class BillServiceImpl {
@Resource
private BillService billService;
@Resource
private WechatPayConfig wechatPayConfig;
/**
* 下载微信支付交易账单
* @param billDate 账单日期(格式:yyyyMMdd)
* @return 账单内容
*/
public String downloadTradeBill(String billDate) {
// 1. 参数校验
StringUtils.hasText(billDate, "账单日期不能为空");
// 校验日期格式
try {
LocalDate.parse(billDate, DateTimeFormatter.ofPattern("yyyyMMdd"));
} catch (Exception e) {
throw new IllegalArgumentException("账单日期格式错误,需为yyyyMMdd");
}
// 2. 构建账单下载请求
DownloadBillRequest request = new DownloadBillRequest();
request.setBillDate(billDate);
request.setBillType("TRADE"); // 交易账单:TRADE,退款账单:REFUND
request.setTarType("GZIP"); // 压缩类型:GZIP
request.setMchid(wechatPayConfig.getMchId());
try {
// 3. 调用接口下载账单(返回InputStream)
InputStream inputStream = billService.downloadBill(request);
if (ObjectUtils.isEmpty(inputStream)) {
log.error("下载账单失败,billDate={}", billDate);
throw new RuntimeException("下载账单失败");
}
// 4. 解压GZIP压缩流
GZIPInputStream gzipInputStream = new GZIPInputStream(inputStream);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = gzipInputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
}
String billContent = outputStream.toString(StandardCharsets.UTF_8.name());
// 5. 关闭流
gzipInputStream.close();
outputStream.close();
inputStream.close();
log.info("下载账单成功,billDate={},账单长度={}", billDate, billContent.length());
return billContent;
} catch (ServiceException e) {
log.error("下载账单接口调用失败,billDate={},错误码={},错误信息={}",
billDate, e.getErrorCode(), e.getMessage(), e);
throw new RuntimeException("下载账单失败:" + e.getMessage());
} catch (Exception e) {
log.error("解压账单失败,billDate={}", billDate, e);
throw new RuntimeException("解压账单失败");
}
}
/**
* 核对账单数据
* @param billContent 账单内容
* @return 对账结果
*/
public String checkBill(String billContent) {
// 1. 按行解析账单(首行为表头,最后一行为统计行)
String[] lines = billContent.split("\n");
if (lines.length <= 2) {
return "账单数据为空";
}
// 2. 统计本地订单数、金额 vs 账单订单数、金额
int localOrderCount = 0; // 本地支付成功订单数
int billOrderCount = 0; // 账单支付成功订单数
long localTotalAmount = 0; // 本地支付总金额(分)
long billTotalAmount = 0; // 账单支付总金额(分)
// 3. 遍历账单行(跳过表头和统计行)
for (int i = 1; i < lines.length - 1; i++) {
String line = lines[i];
if (StringUtils.isEmpty(line)) {
continue;
}
String[] fields = line.split(",");
// 账单字段:交易时间,公众账号ID,商户号,子商户号,设备号,微信订单号,商户订单号,用户标识,交易类型,交易状态,...
String tradeStatus = fields[9]; // 交易状态
if ("SUCCESS".equals(tradeStatus)) {
billOrderCount++;
String totalAmountStr = fields[15]; // 总金额(元)
billTotalAmount += Math.round(Double.parseDouble(totalAmountStr) * 100); // 转为分
}
}
// 4. 查询本地支付成功订单数据(示例逻辑,需根据实际业务调整)
// localOrderCount = orderService.count(lambdaQuery().eq(Order::getOrderStatus, "SUCCESS").eq(Order::getCreateTime, billDate));
// localTotalAmount = orderService.sum(lambdaQuery().eq(Order::getOrderStatus, "SUCCESS").eq(Order::getCreateTime, billDate));
// 5. 对比数据
if (localOrderCount == billOrderCount && localTotalAmount == billTotalAmount) {
return String.format("对账成功:本地订单数=%d,账单订单数=%d;本地总金额=%d分,账单总金额=%d分",
localOrderCount, billOrderCount, localTotalAmount, billTotalAmount);
} else {
String errorMsg = String.format("对账异常:本地订单数=%d,账单订单数=%d;本地总金额=%d分,账单总金额=%d分",
localOrderCount, billOrderCount, localTotalAmount, billTotalAmount);
log.error(errorMsg);
// 触发告警(如钉钉/企业微信通知)
sendAlarm(errorMsg);
return errorMsg;
}
}
/**
* 发送对账异常告警
* @param msg 告警信息
*/
private void sendAlarm(String msg) {
// 实际项目中对接告警渠道,此处为示例
log.warn("对账异常告警:{}", msg);
}
}
7.3 对账定时任务配置
package com.jam.demo.config;
import com.jam.demo.service.BillServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import javax.annotation.Resource;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
/**
* 对账定时任务配置
* 每日凌晨1点下载前一日账单并对账(微信支付账单生成时间为次日0点后)
* @author ken
*/
@Slf4j
@Configuration
@EnableScheduling
public class BillScheduleConfig {
@Resource
private BillServiceImpl billService;
/**
* 每日凌晨1点执行对账任务
*/
@Scheduled(cron = "0 0 1 * * ?")
public void checkBillTask() {
try {
// 获取前一日日期(格式:yyyyMMdd)
String billDate = LocalDate.now().minusDays(1).format(DateTimeFormatter.ofPattern("yyyyMMdd"));
log.info("开始执行对账任务,账单日期={}", billDate);
// 下载账单
String billContent = billService.downloadTradeBill(billDate);
// 核对账单
String checkResult = billService.checkBill(billContent);
log.info("对账任务执行完成,结果={}", checkResult);
} catch (Exception e) {
log.error("执行对账任务失败", e);
}
}
}
八、微信支付常见问题与避坑指南
8.1 签名验证失败
原因分析
- 参数拼接顺序错误(未按ASCII码升序);
- API密钥错误(与商户平台设置不一致);
- 字符编码问题(未使用UTF-8);
- 参数值包含特殊字符未转义;
- 新版接口使用MD5签名(应使用HMAC-SHA256/RSA)。
解决方案
- 严格按微信规范拼接参数,使用TreeMap自动排序;
- 核对商户平台API密钥,确保配置文件与平台一致;
- 所有参数编码统一为UTF-8;
- 使用微信支付SDK自带的签名工具,避免手动拼接;
- 新版接口优先使用RSA签名,废弃MD5。
8.2 支付回调重复接收
原因分析
- 回调响应格式错误(未返回
{"code":"SUCCESS","message":"成功"}); - 回调处理超时(超过5秒);
- 接口返回非200状态码;
- 幂等性校验缺失,重复处理导致业务异常。
解决方案
- 严格按微信规范返回响应格式;
- 复杂业务逻辑通过MQ异步解耦,确保5秒内返回响应;
- 监控回调接口响应状态码,确保返回200;
- 基于“商户订单号+订单状态”实现幂等性校验。
8.3 退款接口调用失败
原因分析
- 商户证书未正确加载;
- 证书密码错误(默认与商户号一致);
- 退款金额超过订单金额;
- 订单未支付/已退款;
- 退款接口权限未开通。
解决方案
- 检查证书路径和格式(p12/pem),确保InputStream加载成功;
- 核对证书密码,默认与商户号一致;
- 退款前校验退款金额≤订单金额;
- 退款前校验订单状态为“支付成功”且未发起过退款;
- 登录商户平台确认退款接口权限已开通。
8.4 订单状态不一致
原因分析
- 仅依赖同步返回结果更新订单状态;
- 回调处理失败未重试;
- 网络异常导致订单状态更新丢失。
解决方案
- 以异步回调结果作为订单状态更新的唯一依据;
- 对回调处理失败的订单,定时查询微信支付订单状态进行补偿;
- 订单状态更新操作添加事务,确保数据一致性。
九、总结
关键点回顾
- 核心流程:微信支付接入的核心是“预下单→支付→异步回调→状态更新”,异步回调是确认支付状态的唯一可靠方式,必须保证签名验证和幂等性;
- 安全保障:签名验证、证书鉴权、金额校验是支付安全的三大核心,需严格遵循微信支付规范,避免手动拼接参数和签名;
- 工程化实践:通过封装通用工具类、实现定时对账、完善异常处理和告警机制,可大幅提升支付系统的稳定性和可维护性;
- 避坑重点:签名错误、回调重复、退款权限、订单状态不一致是高频问题,需针对性做好参数校验、幂等设计和补偿机制。
本文从底层逻辑到实战代码,完整覆盖了微信支付Native/JSAPI支付、异步回调、退款、对账等核心能力。在实际项目中,需结合业务场景补充异常监控、资金告警、日志审计等能力,进一步保障支付系统的稳定性和资金安全。