微信支付全流程实战指南

微信支付全流程实战指南

一、引言:微信支付的核心价值与接入痛点

在移动支付主导的当下,微信支付作为国内主流支付方式,已成为企业服务端开发的必备能力。无论是电商下单、服务缴费还是内容付费,稳定可靠的微信支付接入直接影响用户体验与资金安全。

但对于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.363.2 Maven依赖配置在pom.xml中引入核心依赖,所有版本均采用最新稳定版:

代码语言:javascript复制

org.springframework.boot

spring-boot-starter-web

3.2.5

com.github.wechatpay-apiv3

weixin-pay-java

4.5.0

org.projectlombok

lombok

1.18.30

provided

com.alibaba.fastjson2

fastjson2

2.0.50

com.baomidou

mybatis-plus-boot-starter

3.5.5

com.mysql

mysql-connector-j

8.3.0

runtime

org.springdoc

springdoc-openapi-starter-webmvc-ui

2.5.0

org.springframework

spring-context-support

6.1.6

com.google.guava

guava

33.2.1-jre

3.3 核心配置信息3.3.1 配置文件(application.yml)代码语言:javascript复制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 微信支付配置类代码语言:javascript复制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

publicclass 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, "证书文件不存在");

// 构建自动加载证书的配置

returnnew 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) {

returnnew NativePayService.Builder().config(config).build();

}

/**

* 初始化JSAPI支付服务

* @param config 微信支付配置

* @return JsapiPayService

*/

@Bean

public JsapiPayService jsapiPayService(Config config) {

returnnew JsapiPayService.Builder().config(config).build();

}

/**

* 初始化退款服务

* @param config 微信支付配置

* @return RefundService

*/

@Bean

public RefundService refundService(Config config) {

returnnew RefundService.Builder().config(config).build();

}

}

3.4 数据库表设计(MySQL8.0)微信支付相关业务需设计订单表、支付记录表、退款记录表,用于存储订单信息、支付结果、退款信息,保障数据可追溯。

代码语言:javascript复制-- 订单表

CREATETABLE`t_order` (

`id`bigintNOTNULL AUTO_INCREMENT COMMENT'主键ID',

`order_no`varchar(64) NOTNULLCOMMENT'商户订单号',

`app_id`varchar(32) NOTNULLCOMMENT'APPID',

`mch_id`varchar(32) NOTNULLCOMMENT'商户号',

`user_id`bigintNOTNULLCOMMENT'用户ID',

`total_amount`intNOTNULLCOMMENT'订单总金额(分)',

`subject`varchar(128) NOTNULLCOMMENT'订单标题',

`pay_type`varchar(16) NOTNULLCOMMENT'支付方式(NATIVE/JSAPI/H5)',

`order_status`varchar(16) NOTNULLCOMMENT'订单状态(PENDING:待支付;SUCCESS:支付成功;CLOSED:已关闭;REFUNDED:已退款)',

`create_time` datetime NOTNULLDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',

`update_time` datetime NOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间',

PRIMARY KEY (`id`),

UNIQUEKEY`uk_order_no` (`order_no`),

KEY`idx_user_id` (`user_id`),

KEY`idx_create_time` (`create_time`)

) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='订单表';

-- 支付记录表

CREATETABLE`t_pay_record` (

`id`bigintNOTNULL AUTO_INCREMENT COMMENT'主键ID',

`order_id`bigintNOTNULLCOMMENT'订单ID',

`order_no`varchar(64) NOTNULLCOMMENT'商户订单号',

`transaction_id`varchar(64) DEFAULTNULLCOMMENT'微信支付订单号',

`pay_amount`intNOTNULLCOMMENT'实际支付金额(分)',

`pay_time` datetime DEFAULTNULLCOMMENT'支付时间',

`notify_time` datetime DEFAULTNULLCOMMENT'通知接收时间',

`notify_data`textCOMMENT'异步通知原始数据',

`create_time` datetime NOTNULLDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',

`update_time` datetime NOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间',

PRIMARY KEY (`id`),

UNIQUEKEY`uk_transaction_id` (`transaction_id`),

KEY`idx_order_no` (`order_no`),

KEY`idx_pay_time` (`pay_time`)

) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='支付记录表';

-- 退款记录表

CREATETABLE`t_refund_record` (

`id`bigintNOTNULL AUTO_INCREMENT COMMENT'主键ID',

`order_id`bigintNOTNULLCOMMENT'订单ID',

`order_no`varchar(64) NOTNULLCOMMENT'商户订单号',

`refund_no`varchar(64) NOTNULLCOMMENT'商户退款单号',

`transaction_id`varchar(64) DEFAULTNULLCOMMENT'微信支付订单号',

`refund_id`varchar(64) DEFAULTNULLCOMMENT'微信退款单号',

`refund_amount`intNOTNULLCOMMENT'退款金额(分)',

`total_amount`intNOTNULLCOMMENT'订单总金额(分)',

`refund_reason`varchar(256) DEFAULTNULLCOMMENT'退款原因',

`refund_status`varchar(16) NOTNULLCOMMENT'退款状态(REFUND_PROCESSING:退款中;REFUND_SUCCESS:退款成功;REFUND_FAILED:退款失败)',

`refund_time` datetime DEFAULTNULLCOMMENT'退款成功时间',

`notify_time` datetime DEFAULTNULLCOMMENT'退款通知接收时间',

`notify_data`textCOMMENT'退款通知原始数据',

`create_time` datetime NOTNULLDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',

`update_time` datetime NOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间',

PRIMARY KEY (`id`),

UNIQUEKEY`uk_refund_no` (`refund_no`),

UNIQUEKEY`uk_refund_id` (`refund_id`),

KEY`idx_order_no` (`order_no`),

KEY`idx_refund_time` (`refund_time`)

) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='退款记录表';

3.5 基础实体类与通用响应类3.5.1 订单实体类代码语言:javascript复制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")

publicclass 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 通用响应类代码语言:javascript复制package com.jam.demo.common;

import com.alibaba.fastjson2.JSONObject;

import lombok.Data;

import org.springframework.http.HttpStatus;

/**

* 通用响应类

* @author ken

*/

@Data

publicclass R {

/** 响应码 */

privateint code;

/** 响应信息 */

private String msg;

/** 响应数据 */

private T data;

/**

* 成功响应(无数据)

* @return R

*/

publicstatic R success() {

returnnew R<>(HttpStatus.OK.value(), "操作成功", null);

}

/**

* 成功响应(带数据)

* @param data 响应数据

* @return R

*/

publicstatic R success(T data) {

returnnew R<>(HttpStatus.OK.value(), "操作成功", data);

}

/**

* 失败响应

* @param msg 失败信息

* @return R

*/

publicstatic R fail(String msg) {

returnnew R<>(HttpStatus.INTERNAL_SERVER_ERROR.value(), msg, null);

}

/**

* 失败响应(指定响应码)

* @param code 响应码

* @param msg 失败信息

* @return R

*/

publicstatic R fail(int code, String msg) {

returnnew R<>(code, msg, null);

}

@Override

public String toString() {

return JSONObject.toJSONString(this);

}

}

四、核心支付能力实现:从预支付到支付完成微信支付支持多种支付场景,其中Native支付(扫码支付)、JSAPI支付(公众号/小程序支付)是最常用的两种方式。本节将分别实现这两种支付方式,涵盖预支付订单生成、支付凭证获取、支付状态查询等核心功能。

4.1 通用工具类封装先封装微信支付通用工具类,处理签名验证、订单号生成等通用逻辑,避免代码冗余。

代码语言:javascript复制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

publicclass 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 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 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);

returnfalse;

}

}

}

代码语言:javascript复制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

publicclass 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);

returnnull;

}

}

}

4.2 Native支付实现(扫码支付)Native支付是指商户系统生成支付二维码,用户使用微信扫码完成支付的方式,适用于线下门店、自助设备等场景。

4.2.1 Native支付流程4.2.2 Mapper接口与XML代码语言:javascript复制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

publicinterface OrderMapper extends BaseMapper {

/**

* 根据商户订单号查询订单

* @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);

}

代码语言:javascript复制

UPDATE t_order

SET order_status = #{newStatus}, update_time = NOW()

WHERE order_no = #{orderNo} AND order_status = #{oldStatus}

4.2.3 Service层实现代码语言:javascript复制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

*/

publicinterface OrderService extends IService {

/**

* 创建订单并发起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);

}

代码语言:javascript复制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

publicclass OrderServiceImpl extends ServiceImpl 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) {

thrownew 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);

thrownew 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);

thrownew 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);

thrownew RuntimeException("记录支付结果失败");

}

// 3. 更新订单状态为支付成功

boolean updateSuccess = updateOrderStatus(order.getOrderNo(), "PENDING", "SUCCESS");

if (!updateSuccess) {

log.error("更新订单状态失败,orderNo={},当前状态不为待支付", order.getOrderNo());

thrownew RuntimeException("更新订单状态失败");

}

log.info("记录支付结果成功,orderNo={},transactionId={}", order.getOrderNo(), transactionId);

}

}

4.2.4 Controller层实现代码语言:javascript复制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支付等核心接口")

publicclass WechatPayController {

@Resource

private OrderService orderService;

/**

* 创建订单并发起Native支付

* @param userId 用户ID

* @param totalAmount 订单金额(分)

* @param subject 订单标题

* @return 支付信息(包含二维码链接)

*/

@PostMapping("/native/create")

@Operation(summary = "Native支付下单", description = "创建订单并返回微信支付二维码链接,用户扫码完成支付")

public R 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 支付状态查询接口代码语言:javascript复制// 在WechatPayController中添加

@GetMapping("/query")

@Operation(summary = "查询支付状态", description = "根据商户订单号查询支付结果")

public R 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(公众号场景)代码语言:javascript复制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等接口")

publicclass WechatMpController {

@Resource

private WechatPayConfig wechatPayConfig;

@Resource

private RestTemplate restTemplate;

/**

* 微信公众号授权回调,获取用户openid

* 注:需先在公众号后台配置授权回调域名

* @param code 授权临时票据

* @return openid

*/

@GetMapping("/oauth/callback")

@Operation(summary = "公众号授权回调", description = "通过授权临时票据获取用户openid")

public R> 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 params = new HashMap<>();

params.put("appid", wechatPayConfig.getAppId());

params.put("secret", "你的公众号开发者密码"); // 从公众号后台获取

params.put("code", code);

// 发送请求

Map 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 data = new HashMap<>();

data.put("openid", openid);

return R.success(data);

} catch (Exception e) {

log.error("公众号授权回调失败", e);

return R.fail("授权失败");

}

}

}

4.3.3 JSAPI支付核心代码代码语言:javascript复制// 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) {

thrownew 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);

thrownew 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);

thrownew RuntimeException("发起JSAPI支付失败:" + e.getMessage());

} catch (Exception e) {

log.error("JSAPI支付参数签名失败,orderNo={}", orderNo, e);

thrownew 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)) {

thrownew 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类定义代码语言:javascript复制package com.jam.demo.vo;

import lombok.Data;

/**

* JSAPI支付前端调起参数VO

* @author ken

*/

@Data

publicclass 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接口代码语言:javascript复制// 在WechatPayController中添加

/**

* 创建订单并发起JSAPI支付

* @param userId 用户ID

* @param openid 用户openid

* @param totalAmount 订单金额(分)

* @param subject 订单标题

* @return 前端支付参数

*/

@PostMapping("/jsapi/create")

@Operation(summary = "JSAPI支付下单", description = "创建订单并返回前端调起微信支付的参数,适用于公众号/小程序支付")

public R 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 异步回调核心代码实现代码语言:javascript复制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")

publicclass 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 退款记录实体类代码语言:javascript复制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")

publicclass 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接口代码语言:javascript复制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

publicinterface RefundRecordMapper extends BaseMapper {

/**

* 根据商户退款单号查询退款记录

* @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代码语言:javascript复制

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}

6.3 退款Service层实现6.3.1 退款Service接口代码语言:javascript复制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 {

/**

* 发起退款

* @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实现类代码语言:javascript复制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

publicclass RefundServiceImpl extends ServiceImpl 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) {

thrownew IllegalArgumentException("退款金额必须大于0");

}

StringUtils.hasText(refundReason, "退款原因不能为空");

// 2. 查询订单,校验退款条件

Order order = orderService.getOrderByOrderNo(orderNo);

if (ObjectUtils.isEmpty(order)) {

thrownew RuntimeException("订单不存在");

}

if (!"SUCCESS".equals(order.getOrderStatus())) {

thrownew RuntimeException("仅支付成功的订单可发起退款");

}

if (refundAmount > order.getTotalAmount()) {

thrownew 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)) {

thrownew 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);

thrownew 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);

thrownew 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);

returnfalse;

}

// 3. 幂等性校验

if (!"REFUND_PROCESSING".equals(refundRecord.getRefundStatus())) {

log.info("退款记录已处理,无需重复操作,outRefundNo={}", outRefundNo);

returntrue;

}

// 4. 更新退款状态

String newStatus = "";

LocalDateTime refundTime = null;

if ("SUCCESS".equals(refundStatus)) {

newStatus = "REFUND_SUCCESS";

refundTime = LocalDateTime.now();

// 同步更新订单状态为已退款

orderService.updateOrderStatus(refundRecord.getOrderNo(), "SUCCESS", "REFUNDED");

} elseif ("CLOSED".equals(refundStatus)) {

newStatus = "REFUND_FAILED";

} else {

log.warn("退款状态未知,outRefundNo={},refundStatus={}", outRefundNo, refundStatus);

returnfalse;

}

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);

returnfalse;

}

}

}

6.4 退款Controller接口代码语言:javascript复制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 = "包含发起退款、退款回调处理等接口")

publicclass RefundController {

@Resource

private RefundService refundService;

/**

* 发起退款

* @param orderNo 商户订单号

* @param refundAmount 退款金额(分)

* @param refundReason 退款原因

* @return 退款结果

*/

@PostMapping("/create")

@Operation(summary = "发起退款", description = "对已支付成功的订单发起退款,需校验退款金额和订单状态")

public R 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类定义代码语言:javascript复制package com.jam.demo.vo;

import lombok.Data;

/**

* 退款结果VO

* @author ken

*/

@Data

publicclass RefundVO {

/** 商户订单号 */

private String orderNo;

/** 商户退款单号 */

private String refundNo;

/** 退款金额(分) */

private Integer refundAmount;

/** 退款状态 */

private String refundStatus;

/** 微信退款单号 */

private String refundId;

}

七、对账与账单下载:资金安全的最后一道防线对账是保障资金安全的核心环节,微信支付提供日账单、交易账单、退款账单等多种账单类型,商户需定期下载并核对,确保交易数据与资金流水一致。

7.1 对账流程7.2 账单下载核心代码代码语言:javascript复制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

publicclass 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) {

thrownew 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);

thrownew RuntimeException("下载账单失败");

}

// 4. 解压GZIP压缩流

GZIPInputStream gzipInputStream = new GZIPInputStream(inputStream);

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

byte[] buffer = newbyte[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);

thrownew RuntimeException("下载账单失败:" + e.getMessage());

} catch (Exception e) {

log.error("解压账单失败,billDate={}", billDate, e);

thrownew 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 对账定时任务配置代码语言:javascript复制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

publicclass 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 订单状态不一致原因分析仅依赖同步返回结果更新订单状态;回调处理失败未重试;网络异常导致订单状态更新丢失。解决方案以异步回调结果作为订单状态更新的唯一依据;对回调处理失败的订单,定时查询微信支付订单状态进行补偿;订单状态更新操作添加事务,确保数据一致性。九、总结关键点回顾核心流程:微信支付接入的核心是“预下单→支付→异步回调→状态更新”,异步回调是确认支付状态的唯一可靠方式,必须保证签名验证和幂等性;安全保障:签名验证、证书鉴权、金额校验是支付安全的三大核心,需严格遵循微信支付规范,避免手动拼接参数和签名;工程化实践:通过封装通用工具类、实现定时对账、完善异常处理和告警机制,可大幅提升支付系统的稳定性和可维护性;避坑重点:签名错误、回调重复、退款权限、订单状态不一致是高频问题,需针对性做好参数校验、幂等设计和补偿机制。

相关探索