首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >微信支付全流程实战指南

微信支付全流程实战指南

作者头像
果酱带你啃java
发布2026-04-14 14:12:39
发布2026-04-14 14:12:39
570
举报

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

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

但对于Java开发者而言,微信支付接入并非易事:官方文档侧重规范说明,缺乏完整的工程化实践方案;签名验证、异步回调、退款对账等环节容易出现细节错误;不同支付场景(Native、JSAPI、H5)的适配逻辑存在差异,稍不注意就会导致支付失败。

二、微信支付底层逻辑与核心概念拆解

在动手编码前,必须先理清微信支付的底层运行逻辑,明确核心概念的含义,避免因概念混淆导致的开发错误。

2.1 核心交互逻辑流程图

2.2 核心概念解析

  1. 商户号(mch_id):微信支付分配给商户的唯一标识,用于资金结算、接口调用的身份认证,需在微信支付商户平台申请。
  2. APPID:公众号/小程序/APP的唯一标识,需与商户号完成绑定,用于关联用户身份。
  3. API密钥(key):商户平台设置的32位密钥,用于接口签名验证,是保障接口安全的核心,需严格保密。
  4. 预支付交易会话标识(prepay_id):微信支付服务端生成的预支付凭证,有效期2小时,是发起实际支付的核心参数。
  5. 签名算法:微信支付采用HMAC-SHA256(推荐)或MD5进行签名,所有接口调用均需通过签名验证,防止参数被篡改。
  6. 异步通知(notify_url):商户系统提供的公网可访问接口,微信支付在支付完成后会异步回调该接口通知支付结果,是确认支付状态的可靠方式。

2.3 签名验证底层原理

签名验证是微信支付接口安全的核心,其底层逻辑可概括为“参数标准化→拼接→加密→验证”四步:

  1. 筛选所有非空的请求参数,排除sign字段;
  2. 按参数名ASCII码升序排序,用“&”拼接成“key=value”格式的字符串;
  3. 在字符串末尾拼接“&key=商户API密钥”,用指定加密算法(HMAC-SHA256/MD5)加密,得到签名值;
  4. 微信支付服务端/商户系统接收参数后,重复上述步骤生成签名,与传入的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中引入核心依赖,所有版本均采用最新稳定版:

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

    /** 响应码 */
    privateint code;

    /** 响应信息 */
    private String msg;

    /** 响应数据 */
    private T data;

    /**
     * 成功响应(无数据)
     * @return R<Void>
     */
    publicstatic <T> R<T> success() {
        returnnew R<>(HttpStatus.OK.value(), "操作成功", null);
    }

    /**
     * 成功响应(带数据)
     * @param data 响应数据
     * @return R<T>
     */
    publicstatic <T> R<T> success(T data) {
        returnnew R<>(HttpStatus.OK.value(), "操作成功", data);
    }

    /**
     * 失败响应
     * @param msg 失败信息
     * @return R<Void>
     */
    publicstatic <T> R<T> fail(String msg) {
        returnnew R<>(HttpStatus.INTERNAL_SERVER_ERROR.value(), msg, null);
    }

    /**
     * 失败响应(指定响应码)
     * @param code 响应码
     * @param msg 失败信息
     * @return R<Void>
     */
    publicstatic <T> R<T> 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<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);
            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<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);
}
代码语言:javascript
复制
<?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层实现
代码语言: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<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);
}
代码语言: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<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) {
            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<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 支付状态查询接口
代码语言:javascript
复制
// 在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(公众号场景)
代码语言: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<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支付核心代码

代码语言: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<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 异步回调核心代码实现

代码语言: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 回调处理关键注意事项

  1. 幂等性保障:必须通过“商户订单号+订单状态”判断是否已处理,避免重复更新订单/重复入账;
  2. 响应格式:成功响应必须是{"code":"SUCCESS","message":"成功"},失败是{"code":"FAIL","message":"失败原因"},格式错误会导致微信重复回调;
  3. 超时处理:回调处理逻辑需在5秒内完成,复杂业务需异步解耦(如通过MQ处理);
  4. 日志记录:必须完整记录回调的所有参数、处理结果,便于问题排查;
  5. 重试机制:微信支付回调失败会按固定策略重试,需保证接口幂等性,同时监控重试次数,避免漏处理。

六、退款功能实现:从接口调用到回调处理

退款是支付业务的必备能力,微信支付退款接口需使用商户证书鉴权,且需处理退款异步回调,保障退款状态的准确性。

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<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
代码语言:javascript
复制
<?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接口
代码语言: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<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实现类
代码语言: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<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) {
            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<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类定义

代码语言: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 签名验证失败

原因分析
  1. 参数拼接顺序错误(未按ASCII码升序);
  2. API密钥错误(与商户平台设置不一致);
  3. 字符编码问题(未使用UTF-8);
  4. 参数值包含特殊字符未转义;
  5. 新版接口使用MD5签名(应使用HMAC-SHA256/RSA)。
解决方案
  1. 严格按微信规范拼接参数,使用TreeMap自动排序;
  2. 核对商户平台API密钥,确保配置文件与平台一致;
  3. 所有参数编码统一为UTF-8;
  4. 使用微信支付SDK自带的签名工具,避免手动拼接;
  5. 新版接口优先使用RSA签名,废弃MD5。

8.2 支付回调重复接收

原因分析
  1. 回调响应格式错误(未返回{"code":"SUCCESS","message":"成功"});
  2. 回调处理超时(超过5秒);
  3. 接口返回非200状态码;
  4. 幂等性校验缺失,重复处理导致业务异常。
解决方案
  1. 严格按微信规范返回响应格式;
  2. 复杂业务逻辑通过MQ异步解耦,确保5秒内返回响应;
  3. 监控回调接口响应状态码,确保返回200;
  4. 基于“商户订单号+订单状态”实现幂等性校验。

8.3 退款接口调用失败

原因分析
  1. 商户证书未正确加载;
  2. 证书密码错误(默认与商户号一致);
  3. 退款金额超过订单金额;
  4. 订单未支付/已退款;
  5. 退款接口权限未开通。
解决方案
  1. 检查证书路径和格式(p12/pem),确保InputStream加载成功;
  2. 核对证书密码,默认与商户号一致;
  3. 退款前校验退款金额≤订单金额;
  4. 退款前校验订单状态为“支付成功”且未发起过退款;
  5. 登录商户平台确认退款接口权限已开通。

8.4 订单状态不一致

原因分析
  1. 仅依赖同步返回结果更新订单状态;
  2. 回调处理失败未重试;
  3. 网络异常导致订单状态更新丢失。
解决方案
  1. 以异步回调结果作为订单状态更新的唯一依据;
  2. 对回调处理失败的订单,定时查询微信支付订单状态进行补偿;
  3. 订单状态更新操作添加事务,确保数据一致性。

九、总结

关键点回顾

  1. 核心流程:微信支付接入的核心是“预下单→支付→异步回调→状态更新”,异步回调是确认支付状态的唯一可靠方式,必须保证签名验证和幂等性;
  2. 安全保障:签名验证、证书鉴权、金额校验是支付安全的三大核心,需严格遵循微信支付规范,避免手动拼接参数和签名;
  3. 工程化实践:通过封装通用工具类、实现定时对账、完善异常处理和告警机制,可大幅提升支付系统的稳定性和可维护性;
  4. 避坑重点:签名错误、回调重复、退款权限、订单状态不一致是高频问题,需针对性做好参数校验、幂等设计和补偿机制。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-12-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 果酱带你啃java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、引言:微信支付的核心价值与接入痛点
    • 二、微信支付底层逻辑与核心概念拆解
      • 2.1 核心交互逻辑流程图
      • 2.2 核心概念解析
      • 2.3 签名验证底层原理
    • 三、接入前准备:环境搭建与配置
      • 3.1 开发环境说明
      • 3.2 Maven依赖配置
      • 3.3 核心配置信息
      • 3.4 数据库表设计(MySQL8.0)
      • 3.5 基础实体类与通用响应类
    • 四、核心支付能力实现:从预支付到支付完成
      • 4.1 通用工具类封装
      • 4.2 Native支付实现(扫码支付)
      • 4.3 JSAPI支付实现(公众号/小程序支付)
      • 4.3.3 JSAPI支付核心代码
    • 五、支付异步回调处理:核心保障与幂等设计
      • 5.1 异步回调处理流程
      • 5.2 异步回调核心代码实现
      • 5.3 回调处理关键注意事项
    • 六、退款功能实现:从接口调用到回调处理
      • 6.1 退款业务流程
      • 6.2 退款记录表Mapper与实体类
      • 6.3 退款Service层实现
      • 6.4 退款Controller接口
      • 6.5 退款VO类定义
    • 七、对账与账单下载:资金安全的最后一道防线
      • 7.1 对账流程
      • 7.2 账单下载核心代码
      • 7.3 对账定时任务配置
    • 八、微信支付常见问题与避坑指南
      • 8.1 签名验证失败
      • 8.2 支付回调重复接收
      • 8.3 退款接口调用失败
      • 8.4 订单状态不一致
    • 九、总结
      • 关键点回顾
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档