微信支付全流程实战指南

简介: 本文从底层逻辑到实战代码,完整覆盖了微信支付Native/JSAPI支付、异步回调、退款、对账等核心能力。在实际项目中,需结合业务场景补充异常监控、资金告警、日志审计等能力,进一步保障支付系统的稳定性和资金安全。

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

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

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

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

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

2.1 核心交互逻辑流程图

image.png

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中引入核心依赖,所有版本均采用最新稳定版:

<dependencies>
   <!-- Spring Boot核心依赖 -->
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-web</artifactId>
       <version>3.2.5</version>
   </dependency>
   
   <!-- 微信支付SDK -->
   <dependency>
       <groupId>com.github.wechatpay-apiv3</groupId>
       <artifactId>weixin-pay-java</artifactId>
       <version>4.5.0</version>
   </dependency>
   
   <!-- Lombok(日志、getter/setter) -->
   <dependency>
       <groupId>org.projectlombok</groupId>
       <artifactId>lombok</artifactId>
       <version>1.18.30</version>
       <scope>provided</scope>
   </dependency>
   
   <!-- FastJSON2 -->
   <dependency>
       <groupId>com.alibaba.fastjson2</groupId>
       <artifactId>fastjson2</artifactId>
       <version>2.0.50</version>
   </dependency>
   
   <!-- MyBatis-Plus -->
   <dependency>
       <groupId>com.baomidou</groupId>
       <artifactId>mybatis-plus-boot-starter</artifactId>
       <version>3.5.5</version>
   </dependency>
   
   <!-- MySQL驱动 -->
   <dependency>
       <groupId>com.mysql</groupId>
       <artifactId>mysql-connector-j</artifactId>
       <version>8.3.0</version>
       <scope>runtime</scope>
   </dependency>
   
   <!-- SpringDoc OpenAPI(Swagger3) -->
   <dependency>
       <groupId>org.springdoc</groupId>
       <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
       <version>2.5.0</version>
   </dependency>
   
   <!-- Spring工具类 -->
   <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-context-support</artifactId>
       <version>6.1.6</version>
   </dependency>
   
   <!-- Google集合工具类 -->
   <dependency>
       <groupId>com.google.guava</groupId>
       <artifactId>guava</artifactId>
       <version>33.2.1-jre</version>
   </dependency>
</dependencies>

3.3 核心配置信息

3.3.1 配置文件(application.yml)

spring:
 # 数据库配置
 datasource:
   url: jdbc:mysql://localhost:3306/wechat_pay_demo?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
   username: root
   password: root123456
   driver-class-name: com.mysql.cj.jdbc.Driver
 # 微信支付配置
 wechat:
   pay:
     mch-id: 1234567890  # 你的商户号
     app-id: wx1234567890abcdef  # 绑定的公众号/小程序APPID
     api-key: 1234567890abcdef1234567890abcdef  # 商户平台设置的32位API密钥
     notify-url: https://your-domain.com/api/wechat/pay/notify  # 支付异步通知地址(公网可访问)
     refund-notify-url: https://your-domain.com/api/wechat/refund/notify  # 退款异步通知地址
     cert-path: classpath:cert/apiclient_cert.p12  # 退款等敏感接口所需的证书路径
     cert-password: 1234567890  # 证书密码(默认与商户号一致)

# MyBatis-Plus配置
mybatis-plus:
 mapper-locations: classpath:mapper/*.xml
 type-aliases-package: com.jam.demo.entity
 configuration:
   map-underscore-to-camel-case: true  # 下划线转驼峰
   log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 打印SQL日志

# Swagger3配置
springdoc:
 api-docs:
   path: /api-docs
 swagger-ui:
   path: /swagger-ui.html
   operationsSorter: method
 packages-to-scan: com.jam.demo.controller

3.3.2 微信支付配置类

package com.jam.demo.config;

import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.service.payments.nativepay.NativePayService;
import com.wechat.pay.java.service.payments.jsapi.JsapiPayService;
import com.wechat.pay.java.service.refund.RefundService;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.io.InputStream;

/**
* 微信支付配置类
* 加载配置信息并初始化支付相关服务
* @author ken
*/

@Configuration
@ConfigurationProperties(prefix = "spring.wechat.pay")
@Data
public class WechatPayConfig {

   /** 商户号 */
   private String mchId;
   /** 公众号/小程序APPID */
   private String appId;
   /** API密钥 */
   private String apiKey;
   /** 支付异步通知地址 */
   private String notifyUrl;
   /** 退款异步通知地址 */
   private String refundNotifyUrl;
   /** 证书路径 */
   private String certPath;
   /** 证书密码 */
   private String certPassword;

   /**
    * 初始化微信支付配置(自动加载证书)
    * @return 微信支付配置对象
    * @throws IOException 证书加载异常
    */

   @Bean
   public Config wechatPayConfig() throws IOException {
       // 校验配置参数
       StringUtils.hasText(mchId, "商户号不能为空");
       StringUtils.hasText(appId, "APPID不能为空");
       StringUtils.hasText(apiKey, "API密钥不能为空");
       StringUtils.hasText(certPath, "证书路径不能为空");
       StringUtils.hasText(certPassword, "证书密码不能为空");

       // 加载证书流
       InputStream certInputStream = getClass().getResourceAsStream(certPath.substring("classpath:".length()));
       StringUtils.hasText(certInputStream, "证书文件不存在");

       // 构建自动加载证书的配置
       return new RSAAutoCertificateConfig.Builder()
               .merchantId(mchId)
               .privateKeyFromPath("classpath:cert/apiclient_key.pem") // 商户私钥路径
               .merchantSerialNumber("1234567890ABCDEF") // 商户证书序列号(从商户平台获取)
               .apiV3Key(apiKey)
               .build();
   }

   /**
    * 初始化Native支付服务
    * @param config 微信支付配置
    * @return NativePayService
    */

   @Bean
   public NativePayService nativePayService(Config config) {
       return new NativePayService.Builder().config(config).build();
   }

   /**
    * 初始化JSAPI支付服务
    * @param config 微信支付配置
    * @return JsapiPayService
    */

   @Bean
   public JsapiPayService jsapiPayService(Config config) {
       return new JsapiPayService.Builder().config(config).build();
   }

   /**
    * 初始化退款服务
    * @param config 微信支付配置
    * @return RefundService
    */

   @Bean
   public RefundService refundService(Config config) {
       return new RefundService.Builder().config(config).build();
   }
}

3.4 数据库表设计(MySQL8.0)

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

-- 订单表
CREATE TABLE `t_order` (
 `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
 `order_no` varchar(64) NOT NULL COMMENT '商户订单号',
 `app_id` varchar(32) NOT NULL COMMENT 'APPID',
 `mch_id` varchar(32) NOT NULL COMMENT '商户号',
 `user_id` bigint NOT NULL COMMENT '用户ID',
 `total_amount` int NOT NULL COMMENT '订单总金额(分)',
 `subject` varchar(128) NOT NULL COMMENT '订单标题',
 `pay_type` varchar(16) NOT NULL COMMENT '支付方式(NATIVE/JSAPI/H5)',
 `order_status` varchar(16) NOT NULL COMMENT '订单状态(PENDING:待支付;SUCCESS:支付成功;CLOSED:已关闭;REFUNDED:已退款)',
 `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
 PRIMARY KEY (`id`),
 UNIQUE KEY `uk_order_no` (`order_no`),
 KEY `idx_user_id` (`user_id`),
 KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

-- 支付记录表
CREATE TABLE `t_pay_record` (
 `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
 `order_id` bigint NOT NULL COMMENT '订单ID',
 `order_no` varchar(64) NOT NULL COMMENT '商户订单号',
 `transaction_id` varchar(64) DEFAULT NULL COMMENT '微信支付订单号',
 `pay_amount` int NOT NULL COMMENT '实际支付金额(分)',
 `pay_time` datetime DEFAULT NULL COMMENT '支付时间',
 `notify_time` datetime DEFAULT NULL COMMENT '通知接收时间',
 `notify_data` text COMMENT '异步通知原始数据',
 `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
 PRIMARY KEY (`id`),
 UNIQUE KEY `uk_transaction_id` (`transaction_id`),
 KEY `idx_order_no` (`order_no`),
 KEY `idx_pay_time` (`pay_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付记录表';

-- 退款记录表
CREATE TABLE `t_refund_record` (
 `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
 `order_id` bigint NOT NULL COMMENT '订单ID',
 `order_no` varchar(64) NOT NULL COMMENT '商户订单号',
 `refund_no` varchar(64) NOT NULL COMMENT '商户退款单号',
 `transaction_id` varchar(64) DEFAULT NULL COMMENT '微信支付订单号',
 `refund_id` varchar(64) DEFAULT NULL COMMENT '微信退款单号',
 `refund_amount` int NOT NULL COMMENT '退款金额(分)',
 `total_amount` int NOT NULL COMMENT '订单总金额(分)',
 `refund_reason` varchar(256) DEFAULT NULL COMMENT '退款原因',
 `refund_status` varchar(16) NOT NULL COMMENT '退款状态(REFUND_PROCESSING:退款中;REFUND_SUCCESS:退款成功;REFUND_FAILED:退款失败)',
 `refund_time` datetime DEFAULT NULL COMMENT '退款成功时间',
 `notify_time` datetime DEFAULT NULL COMMENT '退款通知接收时间',
 `notify_data` text COMMENT '退款通知原始数据',
 `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
 PRIMARY KEY (`id`),
 UNIQUE KEY `uk_refund_no` (`refund_no`),
 UNIQUE KEY `uk_refund_id` (`refund_id`),
 KEY `idx_order_no` (`order_no`),
 KEY `idx_refund_time` (`refund_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='退款记录表';

3.5 基础实体类与通用响应类

3.5.1 订单实体类

package com.jam.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;

/**
* 订单实体类
* @author ken
*/

@Data
@TableName("t_order")
public class Order {

   /** 主键ID */
   @TableId(type = IdType.AUTO)
   private Long id;

   /** 商户订单号 */
   private String orderNo;

   /** APPID */
   private String appId;

   /** 商户号 */
   private String mchId;

   /** 用户ID */
   private Long userId;

   /** 订单总金额(分) */
   private Integer totalAmount;

   /** 订单标题 */
   private String subject;

   /** 支付方式(NATIVE/JSAPI/H5) */
   private String payType;

   /** 订单状态(PENDING:待支付;SUCCESS:支付成功;CLOSED:已关闭;REFUNDED:已退款) */
   private String orderStatus;

   /** 创建时间 */
   private LocalDateTime createTime;

   /** 更新时间 */
   private LocalDateTime updateTime;
}

3.5.2 通用响应类

package com.jam.demo.common;

import com.alibaba.fastjson2.JSONObject;
import lombok.Data;
import org.springframework.http.HttpStatus;

/**
* 通用响应类
* @author ken
*/

@Data
public class R<T> {

   /** 响应码 */
   private int code;

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

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

   /**
    * 成功响应(无数据)
    * @return R<Void>
    */

   public static <T> R<T> success() {
       return new R<>(HttpStatus.OK.value(), "操作成功", null);
   }

   /**
    * 成功响应(带数据)
    * @param data 响应数据
    * @return R<T>
    */

   public static <T> R<T> success(T data) {
       return new R<>(HttpStatus.OK.value(), "操作成功", data);
   }

   /**
    * 失败响应
    * @param msg 失败信息
    * @return R<Void>
    */

   public static <T> R<T> fail(String msg) {
       return new R<>(HttpStatus.INTERNAL_SERVER_ERROR.value(), msg, null);
   }

   /**
    * 失败响应(指定响应码)
    * @param code 响应码
    * @param msg 失败信息
    * @return R<Void>
    */

   public static <T> R<T> fail(int code, String msg) {
       return new R<>(code, msg, null);
   }

   @Override
   public String toString() {
       return JSONObject.toJSONString(this);
   }
}

四、核心支付能力实现:从预支付到支付完成

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

4.1 通用工具类封装

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

package com.jam.demo.util;

import com.alibaba.fastjson2.JSONObject;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.Random;

/**
* 微信支付通用工具类
* @author ken
*/

@Slf4j
public class WechatPayUtil {

   /**
    * 生成商户订单号(规则:时间戳+6位随机数)
    * @return 商户订单号
    */

   public static String generateOrderNo() {
       DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
       String timeStr = LocalDateTime.now().format(formatter);
       String randomStr = String.format("%06d", new Random().nextInt(1000000));
       return timeStr + randomStr;
   }

   /**
    * 生成商户退款单号(规则:REFUND+时间戳+6位随机数)
    * @return 商户退款单号
    */

   public static String generateRefundNo() {
       return "REFUND" + generateOrderNo();
   }

   /**
    * 验证微信支付回调签名
    * 注:实际开发中可直接使用微信支付SDK提供的签名验证工具
    * @param notifyData 回调原始数据(JSON格式)
    * @param apiKey API密钥
    * @param sign 回调中的签名
    * @return 签名是否有效
    */

   public static boolean verifyNotifySign(String notifyData, String apiKey, String sign) {
       try {
           // 1. 解析回调数据,获取所有非空参数
           JSONObject jsonObject = JSONObject.parseObject(notifyData);
           Map<String, String> params = Maps.newTreeMap(); // 按ASCII码升序排序
           jsonObject.forEach((key, value) -> {
               if (!"sign".equals(key) && value != null && StringUtils.hasText(value.toString())) {
                   params.put(key, value.toString());
               }
           });

           // 2. 拼接参数字符串
           StringBuilder sb = new StringBuilder();
           for (Map.Entry<String, String> entry : params.entrySet()) {
               sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
           }
           sb.append("key=").append(apiKey);

           // 3. 计算HMAC-SHA256签名
           String calculatedSign = HmacSha256Util.sign(sb.toString(), apiKey);
           log.info("微信支付回调签名验证:计算签名={},回调签名={}", calculatedSign, sign);

           // 4. 对比签名
           return StringUtils.hasText(calculatedSign) && calculatedSign.equalsIgnoreCase(sign);
       } catch (Exception e) {
           log.error("微信支付回调签名验证失败", e);
           return false;
       }
   }
}

package com.jam.demo.util;

import lombok.extern.slf4j.Slf4j;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

/**
* HMAC-SHA256加密工具类
* @author ken
*/

@Slf4j
public class HmacSha256Util {

   /**
    * HMAC-SHA256加密
    * @param data 待加密数据
    * @param key 密钥
    * @return 加密后的数据(大写)
    */

   public static String sign(String data, String key) {
       try {
           Mac mac = Mac.getInstance("HmacSHA256");
           SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
           mac.init(secretKeySpec);
           byte[] bytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
           // 转Base64并转为大写
           return Base64.getEncoder().encodeToString(bytes).toUpperCase();
       } catch (Exception e) {
           log.error("HMAC-SHA256加密失败", e);
           return null;
       }
   }
}

4.2 Native支付实现(扫码支付)

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

4.2.1 Native支付流程

image.png

4.2.2 Mapper接口与XML

package com.jam.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

/**
* 订单Mapper
* @author ken
*/

@Mapper
public interface OrderMapper extends BaseMapper<Order> {

   /**
    * 根据商户订单号查询订单
    * @param orderNo 商户订单号
    * @return 订单信息
    */

   Order selectByOrderNo(@Param("orderNo") String orderNo);

   /**
    * 更新订单状态
    * @param orderNo 商户订单号
    * @param oldStatus 原状态
    * @param newStatus 新状态
    * @return 影响行数
    */

   int updateOrderStatus(@Param("orderNo") String orderNo, @Param("oldStatus") String oldStatus, @Param("newStatus") String newStatus);
}

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jam.demo.mapper.OrderMapper">

   <select id="selectByOrderNo" resultType="com.jam.demo.entity.Order">
       SELECT id, order_no, app_id, mch_id, user_id, total_amount, subject, pay_type, order_status, create_time, update_time
       FROM t_order
       WHERE order_no = #{orderNo}
   </select>

   <update id="updateOrderStatus">
       UPDATE t_order
       SET order_status = #{newStatus}, update_time = NOW()
       WHERE order_no = #{orderNo} AND order_status = #{oldStatus}
   </update>
</mapper>

4.2.3 Service层实现

package com.jam.demo.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.jam.demo.entity.Order;
import com.jam.demo.entity.PayRecord;
import com.jam.demo.vo.WechatNativePayVO;

/**
* 订单服务接口
* @author ken
*/

public interface OrderService extends IService<Order> {

   /**
    * 创建订单并发起Native支付
    * @param userId 用户ID
    * @param totalAmount 订单金额(分)
    * @param subject 订单标题
    * @return 微信Native支付信息(包含二维码链接)
    */

   WechatNativePayVO createOrderAndNativePay(Long userId, Integer totalAmount, String subject);

   /**
    * 根据商户订单号查询订单
    * @param orderNo 商户订单号
    * @return 订单信息
    */

   Order getOrderByOrderNo(String orderNo);

   /**
    * 更新订单状态
    * @param orderNo 商户订单号
    * @param oldStatus 原状态
    * @param newStatus 新状态
    * @return 是否更新成功
    */

   boolean updateOrderStatus(String orderNo, String oldStatus, String newStatus);

   /**
    * 记录支付结果
    * @param order 订单信息
    * @param transactionId 微信支付订单号
    * @param payAmount 支付金额(分)
    * @param notifyData 回调原始数据
    */

   void recordPayResult(Order order, String transactionId, Integer payAmount, String notifyData);
}

package com.jam.demo.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.demo.entity.Order;
import com.jam.demo.entity.PayRecord;
import com.jam.demo.mapper.OrderMapper;
import com.jam.demo.mapper.PayRecordMapper;
import com.jam.demo.service.OrderService;
import com.jam.demo.util.WechatPayUtil;
import com.jam.demo.vo.WechatNativePayVO;
import com.wechat.pay.java.core.exception.ServiceException;
import com.wechat.pay.java.service.payments.nativepay.NativePayService;
import com.wechat.pay.java.service.payments.nativepay.model.Amount;
import com.wechat.pay.java.service.payments.nativepay.model.PrepayRequest;
import com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.time.LocalDateTime;

/**
* 订单服务实现类
* @author ken
*/

@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {

   @Resource
   private OrderMapper orderMapper;

   @Resource
   private PayRecordMapper payRecordMapper;

   @Resource
   private NativePayService nativePayService;

   @Resource
   private WechatPayConfig wechatPayConfig;

   /**
    * 创建订单并发起Native支付
    * @param userId 用户ID
    * @param totalAmount 订单金额(分)
    * @param subject 订单标题
    * @return 微信Native支付信息(包含二维码链接)
    */

   @Override
   @Transactional(rollbackFor = Exception.class)
   public WechatNativePayVO createOrderAndNativePay(Long userId, Integer totalAmount, String subject)
{
       // 1. 校验参数
       StringUtils.hasText(userId, "用户ID不能为空");
       StringUtils.hasText(totalAmount, "订单金额不能为空");
       if (totalAmount <= 0) {
           throw new IllegalArgumentException("订单金额必须大于0");
       }
       StringUtils.hasText(subject, "订单标题不能为空");

       // 2. 生成商户订单号
       String orderNo = WechatPayUtil.generateOrderNo();

       // 3. 创建订单
       Order order = new Order();
       order.setOrderNo(orderNo);
       order.setAppId(wechatPayConfig.getAppId());
       order.setMchId(wechatPayConfig.getMchId());
       order.setUserId(userId);
       order.setTotalAmount(totalAmount);
       order.setSubject(subject);
       order.setPayType("NATIVE");
       order.setOrderStatus("PENDING"); // 待支付
       order.setCreateTime(LocalDateTime.now());
       order.setUpdateTime(LocalDateTime.now());
       int insertCount = orderMapper.insert(order);
       if (insertCount <= 0) {
           log.error("创建订单失败,orderNo={}", orderNo);
           throw new RuntimeException("创建订单失败");
       }

       // 4. 调用微信支付Native下单接口,获取预支付信息
       PrepayRequest prepayRequest = new PrepayRequest();
       prepayRequest.setAppid(wechatPayConfig.getAppId());
       prepayRequest.setMchid(wechatPayConfig.getMchId());
       prepayRequest.setDescription(subject);
       prepayRequest.setOutTradeNo(orderNo);
       prepayRequest.setNotifyUrl(wechatPayConfig.getNotifyUrl());

       // 设置金额信息(单位:分)
       Amount amount = new Amount();
       amount.setTotal(totalAmount);
       prepayRequest.setAmount(amount);

       try {
           // 调用微信支付SDK接口
           PrepayResponse prepayResponse = nativePayService.prepay(prepayRequest);
           log.info("Native支付预下单成功,orderNo={},prepayId={},codeUrl={}",
                   orderNo, prepayResponse.getPrepayId(), prepayResponse.getCodeUrl());

           // 5. 封装返回结果(前端根据codeUrl生成二维码)
           WechatNativePayVO payVO = new WechatNativePayVO();
           payVO.setOrderNo(orderNo);
           payVO.setPrepayId(prepayResponse.getPrepayId());
           payVO.setCodeUrl(prepayResponse.getCodeUrl());
           payVO.setTotalAmount(totalAmount);
           return payVO;
       } catch (ServiceException e) {
           log.error("Native支付预下单失败,orderNo={},错误信息={}", orderNo, e.getMessage(), e);
           throw new RuntimeException("发起支付失败:" + e.getMessage());
       }
   }

   /**
    * 根据商户订单号查询订单
    * @param orderNo 商户订单号
    * @return 订单信息
    */

   @Override
   public Order getOrderByOrderNo(String orderNo) {
       StringUtils.hasText(orderNo, "商户订单号不能为空");
       return orderMapper.selectByOrderNo(orderNo);
   }

   /**
    * 更新订单状态
    * @param orderNo 商户订单号
    * @param oldStatus 原状态
    * @param newStatus 新状态
    * @return 是否更新成功
    */

   @Override
   @Transactional(rollbackFor = Exception.class)
   public boolean updateOrderStatus(String orderNo, String oldStatus, String newStatus)
{
       StringUtils.hasText(orderNo, "商户订单号不能为空");
       StringUtils.hasText(oldStatus, "原状态不能为空");
       StringUtils.hasText(newStatus, "新状态不能为空");

       int updateCount = orderMapper.updateOrderStatus(orderNo, oldStatus, newStatus);
       return updateCount > 0;
   }

   /**
    * 记录支付结果
    * @param order 订单信息
    * @param transactionId 微信支付订单号
    * @param payAmount 支付金额(分)
    * @param notifyData 回调原始数据
    */

   @Override
   @Transactional(rollbackFor = Exception.class)
   public void recordPayResult(Order order, String transactionId, Integer payAmount, String notifyData)
{
       StringUtils.hasText(order, "订单信息不能为空");
       StringUtils.hasText(transactionId, "微信支付订单号不能为空");
       StringUtils.hasText(payAmount, "支付金额不能为空");

       // 1. 构建支付记录
       PayRecord payRecord = new PayRecord();
       payRecord.setOrderId(order.getId());
       payRecord.setOrderNo(order.getOrderNo());
       payRecord.setTransactionId(transactionId);
       payRecord.setPayAmount(payAmount);
       payRecord.setPayTime(LocalDateTime.now());
       payRecord.setNotifyTime(LocalDateTime.now());
       payRecord.setNotifyData(notifyData);
       payRecord.setCreateTime(LocalDateTime.now());
       payRecord.setUpdateTime(LocalDateTime.now());

       // 2. 插入支付记录
       int insertCount = payRecordMapper.insert(payRecord);
       if (insertCount <= 0) {
           log.error("记录支付结果失败,orderNo={},transactionId={}", order.getOrderNo(), transactionId);
           throw new RuntimeException("记录支付结果失败");
       }

       // 3. 更新订单状态为支付成功
       boolean updateSuccess = updateOrderStatus(order.getOrderNo(), "PENDING", "SUCCESS");
       if (!updateSuccess) {
           log.error("更新订单状态失败,orderNo={},当前状态不为待支付", order.getOrderNo());
           throw new RuntimeException("更新订单状态失败");
       }
       log.info("记录支付结果成功,orderNo={},transactionId={}", order.getOrderNo(), transactionId);
   }
}

4.2.4 Controller层实现

package com.jam.demo.controller;

import com.jam.demo.common.R;
import com.jam.demo.service.OrderService;
import com.jam.demo.vo.WechatNativePayVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
* 微信支付控制器
* @author ken
*/

@Slf4j
@RestController
@RequestMapping("/api/wechat/pay")
@Tag(name = "微信支付相关接口", description = "包含Native支付、JSAPI支付等核心接口")
public class WechatPayController {

   @Resource
   private OrderService orderService;

   /**
    * 创建订单并发起Native支付
    * @param userId 用户ID
    * @param totalAmount 订单金额(分)
    * @param subject 订单标题
    * @return 支付信息(包含二维码链接)
    */

   @PostMapping("/native/create")
   @Operation(summary = "Native支付下单", description = "创建订单并返回微信支付二维码链接,用户扫码完成支付")
   public R<WechatNativePayVO> createNativePay(
           @Parameter(description = "用户ID", required = true)
@RequestParam Long userId,
           @Parameter(description = "订单金额(分)", required = true) @RequestParam Integer totalAmount,
           @Parameter(description = "订单标题", required = true) @RequestParam String subject) {
       try {
           WechatNativePayVO payVO = orderService.createOrderAndNativePay(userId, totalAmount, subject);
           return R.success(payVO);
       } catch (IllegalArgumentException e) {
           log.error("Native支付下单参数错误", e);
           return R.fail(e.getMessage());
       } catch (Exception e) {
           log.error("Native支付下单失败", e);
           return R.fail("Native支付下单失败");
       }
   }
}

4.2.5 支付状态查询接口

// 在WechatPayController中添加
@GetMapping("/query")
@Operation(summary = "查询支付状态", description = "根据商户订单号查询支付结果")
public R<PayStatusVO> queryPayStatus(
       @Parameter(description = "商户订单号", required = true)
@RequestParam String orderNo) {
   try {
       // 1. 查询订单
       Order order = orderService.getOrderByOrderNo(orderNo);
       if (ObjectUtils.isEmpty(order)) {
           return R.fail("订单不存在");
       }

       // 2. 如果订单已支付,直接返回结果
       if ("SUCCESS".equals(order.getOrderStatus())) {
           PayRecord payRecord = payRecordService.getByOrderNo(orderNo);
           PayStatusVO statusVO = new PayStatusVO();
           statusVO.setOrderNo(orderNo);
           statusVO.setOrderStatus(order.getOrderStatus());
           statusVO.setTransactionId(payRecord.getTransactionId());
           statusVO.setPayTime(payRecord.getPayTime());
           return R.success(statusVO);
       }

       // 3. 调用微信支付查询接口,获取最新支付状态
       QueryOrderRequest queryRequest = new QueryOrderRequest();
       queryRequest.setOutTradeNo(orderNo);
       QueryOrderResponse queryResponse = nativePayService.queryOrder(queryRequest);

       // 4. 封装返回结果
       PayStatusVO statusVO = new PayStatusVO();
       statusVO.setOrderNo(orderNo);
       statusVO.setOrderStatus("SUCCESS".equals(queryResponse.getTradeState()) ? "SUCCESS" : "PENDING");
       statusVO.setTransactionId(queryResponse.getTransactionId());
       statusVO.setPayTime("SUCCESS".equals(queryResponse.getTradeState()) ?
               LocalDateTime.parse(queryResponse.getSuccessTime(), DateTimeFormatter.ISO_LOCAL_DATE_TIME) : null);

       // 5. 如果查询到支付成功,同步更新订单状态和支付记录
       if ("SUCCESS".equals(queryResponse.getTradeState()) && "PENDING".equals(order.getOrderStatus())) {
           orderService.recordPayResult(order, queryResponse.getTransactionId(),
                   queryResponse.getAmount().getTotal(), JSONObject.toJSONString(queryResponse));
       }

       return R.success(statusVO);
   } catch (ServiceException e) {
       log.error("查询支付状态失败,orderNo={}", orderNo, e);
       return R.fail("查询支付状态失败:" + e.getMessage());
   } catch (Exception e) {
       log.error("查询支付状态失败,orderNo={}", orderNo, e);
       return R.fail("查询支付状态失败");
   }
}

4.3 JSAPI支付实现(公众号/小程序支付)

JSAPI支付是指用户在公众号/小程序内发起的支付,需先获取用户的openid(用户在公众号/小程序内的唯一标识),再发起支付请求。

4.3.1 JSAPI支付流程

image.png

4.3.2 获取用户OpenID(公众号场景)

package com.jam.demo.controller;

import com.jam.demo.common.R;
import com.jam.demo.config.WechatPayConfig;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;

/**
* 微信公众号相关接口
* @author ken
*/

@Slf4j
@RestController
@RequestMapping("/api/wechat/mp")
@Tag(name = "微信公众号相关接口", description = "包含获取用户openid等接口")
public class WechatMpController {

   @Resource
   private WechatPayConfig wechatPayConfig;

   @Resource
   private RestTemplate restTemplate;

   /**
    * 微信公众号授权回调,获取用户openid
    * 注:需先在公众号后台配置授权回调域名
    * @param code 授权临时票据
    * @return openid
    */

   @GetMapping("/oauth/callback")
   @Operation(summary = "公众号授权回调", description = "通过授权临时票据获取用户openid")
   public R<Map<String, String>> oauthCallback(
           @Parameter(description = "授权临时票据", required = true) @RequestParam String code) {
       try {
           // 微信公众号获取openid的接口地址
           String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid={appid}&secret={secret}&code={code}&grant_type=authorization_code";

           // 构建请求参数
           Map<String, String> params = new HashMap<>();
           params.put("appid", wechatPayConfig.getAppId());
           params.put("secret", "你的公众号开发者密码"); // 从公众号后台获取
           params.put("code", code);

           // 发送请求
           Map<String, String> result = restTemplate.getForObject(url, Map.class, params);
           log.info("公众号授权回调结果:{}", result);

           // 提取openid
           String openid = result.get("openid");
           if (org.springframework.util.StringUtils.isEmpty(openid)) {
               log.error("获取openid失败,错误信息:{}", result.get("errmsg"));
               return R.fail("获取用户信息失败");
           }

           Map<String, String> data = new HashMap<>();
           data.put("openid", openid);
           return R.success(data);
       } catch (Exception e) {
           log.error("公众号授权回调失败", e);
           return R.fail("授权失败");
       }
   }
}

4.3.3 JSAPI支付核心代码

// OrderServiceImpl实现方法
/**
* 创建订单并发起JSAPI支付
* @param userId 用户ID
* @param openid 用户openid
* @param totalAmount 订单金额(分)
* @param subject 订单标题
* @return 前端支付参数(含签名)
*/

@Override
@Transactional(rollbackFor = Exception.class)
public WechatJsapiPayVO createOrderAndJsapiPay(Long userId, String openid, Integer totalAmount, String subject)
{
   // 1. 校验参数
   StringUtils.hasText(userId.toString(), "用户ID不能为空");
   StringUtils.hasText(openid, "用户openid不能为空");
   StringUtils.hasText(totalAmount.toString(), "订单金额不能为空");
   if (totalAmount <= 0) {
       throw new IllegalArgumentException("订单金额必须大于0");
   }
   StringUtils.hasText(subject, "订单标题不能为空");

   // 2. 生成商户订单号
   String orderNo = WechatPayUtil.generateOrderNo();

   // 3. 创建订单
   Order order = new Order();
   order.setOrderNo(orderNo);
   order.setAppId(wechatPayConfig.getAppId());
   order.setMchId(wechatPayConfig.getMchId());
   order.setUserId(userId);
   order.setTotalAmount(totalAmount);
   order.setSubject(subject);
   order.setPayType("JSAPI");
   order.setOrderStatus("PENDING"); // 待支付
   order.setCreateTime(LocalDateTime.now());
   order.setUpdateTime(LocalDateTime.now());
   int insertCount = orderMapper.insert(order);
   if (insertCount <= 0) {
       log.error("创建JSAPI支付订单失败,orderNo={}", orderNo);
       throw new RuntimeException("创建订单失败");
   }

   // 4. 调用微信支付JSAPI下单接口,获取prepay_id
   PrepayRequest jsapiPrepayRequest = new PrepayRequest();
   jsapiPrepayRequest.setAppid(wechatPayConfig.getAppId());
   jsapiPrepayRequest.setMchid(wechatPayConfig.getMchId());
   jsapiPrepayRequest.setDescription(subject);
   jsapiPrepayRequest.setOutTradeNo(orderNo);
   jsapiPrepayRequest.setNotifyUrl(wechatPayConfig.getNotifyUrl());
   
   // 设置金额信息
   Amount amount = new Amount();
   amount.setTotal(totalAmount);
   jsapiPrepayRequest.setAmount(amount);
   
   // 设置支付者信息(核心:JSAPI必须传openid)
   Payer payer = new Payer();
   payer.setOpenid(openid);
   jsapiPrepayRequest.setPayer(payer);

   try {
       // 调用微信支付SDK接口
       com.wechat.pay.java.service.payments.jsapi.model.PrepayResponse prepayResponse =
               jsapiPayService.prepay(jsapiPrepayRequest);
       log.info("JSAPI支付预下单成功,orderNo={},prepayId={}", orderNo, prepayResponse.getPrepayId());

       // 5. 生成前端调起支付的参数(需签名,防止参数篡改)
       WechatJsapiPayVO jsapiPayVO = new WechatJsapiPayVO();
       jsapiPayVO.setAppId(wechatPayConfig.getAppId());
       jsapiPayVO.setTimeStamp(String.valueOf(System.currentTimeMillis() / 1000));
       jsapiPayVO.setNonceStr(UUID.randomUUID().toString().replace("-", ""));
       jsapiPayVO.setPackageStr("prepay_id=" + prepayResponse.getPrepayId());
       jsapiPayVO.setSignType("RSA"); // 新版微信支付推荐RSA签名
       
       // 6. 生成前端支付签名(按微信规范拼接参数并签名)
       String signStr = String.format(
           "%s\n%s\n%s\n%s\n",
           jsapiPayVO.getAppId(),
           jsapiPayVO.getTimeStamp(),
           jsapiPayVO.getNonceStr(),
           jsapiPayVO.getPackageStr()
       );
       // 加载商户私钥进行签名(实际项目中私钥需安全存储,避免硬编码)
       PrivateKey privateKey = getMerchantPrivateKey();
       Signature signature = Signature.getInstance("SHA256withRSA");
       signature.initSign(privateKey);
       signature.update(signStr.getBytes(StandardCharsets.UTF_8));
       String paySign = Base64.getEncoder().encodeToString(signature.sign());
       jsapiPayVO.setPaySign(paySign);
       jsapiPayVO.setOrderNo(orderNo);

       return jsapiPayVO;
   } catch (ServiceException e) {
       log.error("JSAPI支付预下单失败,orderNo={},错误码={},错误信息={}",
               orderNo, e.getErrorCode(), e.getMessage(), e);
       throw new RuntimeException("发起JSAPI支付失败:" + e.getMessage());
   } catch (Exception e) {
       log.error("JSAPI支付参数签名失败,orderNo={}", orderNo, e);
       throw new RuntimeException("生成支付参数失败");
   }
}

/**
* 获取商户私钥(实际项目中建议从配置中心/密钥管理系统获取,避免硬编码)
* @return 商户私钥
* @throws Exception 私钥加载异常
*/

private PrivateKey getMerchantPrivateKey() throws Exception {
   // 从classpath加载私钥文件(apiclient_key.pem)
   InputStream privateKeyStream = getClass().getResourceAsStream("/cert/apiclient_key.pem");
   if (ObjectUtils.isEmpty(privateKeyStream)) {
       throw new FileNotFoundException("商户私钥文件不存在");
   }
   String privateKeyStr = new String(privateKeyStream.readAllBytes(), StandardCharsets.UTF_8)
           .replace("-----BEGIN PRIVATE KEY-----", "")
           .replace("-----END PRIVATE KEY-----", "")
           .replaceAll("\\s+", "");
   KeyFactory keyFactory = KeyFactory.getInstance("RSA");
   return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyStr)));
}

4.3.4 JSAPI支付VO类定义

package com.jam.demo.vo;

import lombok.Data;

/**
* JSAPI支付前端调起参数VO
* @author ken
*/

@Data
public class WechatJsapiPayVO {
   /** 公众号APPID */
   private String appId;
   /** 时间戳(秒) */
   private String timeStamp;
   /** 随机字符串 */
   private String nonceStr;
   /** 预支付会话标识(固定格式prepay_id=xxx) */
   private String packageStr;
   /** 签名类型(RSA/SHA256-RSA) */
   private String signType;
   /** 支付签名 */
   private String paySign;
   /** 商户订单号 */
   private String orderNo;
}

4.3.5 JSAPI支付Controller接口

// 在WechatPayController中添加
/**
* 创建订单并发起JSAPI支付
* @param userId 用户ID
* @param openid 用户openid
* @param totalAmount 订单金额(分)
* @param subject 订单标题
* @return 前端支付参数
*/

@PostMapping("/jsapi/create")
@Operation(summary = "JSAPI支付下单", description = "创建订单并返回前端调起微信支付的参数,适用于公众号/小程序支付")
public R<WechatJsapiPayVO> createJsapiPay(
       @Parameter(description = "用户ID", required = true)
@RequestParam Long userId,
       @Parameter(description = "用户openid", required = true) @RequestParam String openid,
       @Parameter(description = "订单金额(分)", required = true) @RequestParam Integer totalAmount,
       @Parameter(description = "订单标题", required = true) @RequestParam String subject) {
   try {
       WechatJsapiPayVO jsapiPayVO = orderService.createOrderAndJsapiPay(userId, openid, totalAmount, subject);
       return R.success(jsapiPayVO);
   } catch (IllegalArgumentException e) {
       log.error("JSAPI支付下单参数错误", e);
       return R.fail(e.getMessage());
   } catch (Exception e) {
       log.error("JSAPI支付下单失败", e);
       return R.fail("JSAPI支付下单失败");
   }
}

五、支付异步回调处理:核心保障与幂等设计

微信支付的异步回调(notify_url)是确认支付状态的唯一可靠方式,同步返回的支付结果仅作参考。回调处理需满足:签名验证、参数解析、幂等性保障、结果返回规范四大核心要求。

5.1 异步回调处理流程

image.png

5.2 异步回调核心代码实现

package com.jam.demo.controller;

import com.alibaba.fastjson2.JSONObject;
import com.jam.demo.common.R;
import com.jam.demo.entity.Order;
import com.jam.demo.service.OrderService;
import com.jam.demo.util.WechatPayUtil;
import com.wechat.pay.java.core.notification.NotificationConfig;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.core.notification.RequestParam;
import com.wechat.pay.java.service.payments.model.Transaction;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;

/**
* 微信支付回调控制器
* @author ken
*/

@Slf4j
@RestController
@RequestMapping("/api/wechat/pay/notify")
public class WechatPayNotifyController {

   @Resource
   private OrderService orderService;
   @Resource
   private WechatPayConfig wechatPayConfig;

   /**
    * 支付异步回调处理
    * 注:接口需为公网可访问,且不能有登录拦截、参数校验等额外限制
    * @param requestBody 回调原始数据
    * @param wechatPaySerial 微信支付证书序列号
    * @param wechatPaySignature 微信支付签名
    * @param wechatPayTimestamp 时间戳
    * @param wechatPayNonce 随机串
    * @return 回调响应(SUCCESS/FAIL)
    */

   @PostMapping("/pay")
   public String payNotify(
           @RequestBody String requestBody,
           @RequestHeader("Wechatpay-Serial")
String wechatPaySerial,
           @RequestHeader("Wechatpay-Signature") String wechatPaySignature,
           @RequestHeader("Wechatpay-Timestamp") String wechatPayTimestamp,
           @RequestHeader("Wechatpay-Nonce") String wechatPayNonce) {
       log.info("收到微信支付异步回调,请求体={},serial={},signature={}",
               requestBody, wechatPaySerial, wechatPaySignature);
       
       // 1. 构建回调请求参数
       RequestParam requestParam = new RequestParam.Builder()
               .serialNumber(wechatPaySerial)
               .nonce(wechatPayNonce)
               .signature(wechatPaySignature)
               .timestamp(wechatPayTimestamp)
               .body(requestBody)
               .build();

       try {
           // 2. 初始化通知解析器(自动验证签名)
           NotificationConfig config = new NotificationConfig.Builder()
                   .merchantId(wechatPayConfig.getMchId())
                   .apiV3Key(wechatPayConfig.getApiKey())
                   .build();
           NotificationParser parser = new NotificationParser(config);
           
           // 3. 解析回调并验证签名,失败会抛出异常
           Transaction transaction = parser.parse(requestParam, Transaction.class);
           String outTradeNo = transaction.getOutTradeNo(); // 商户订单号
           String transactionId = transaction.getTransactionId(); // 微信支付订单号
           Integer totalAmount = transaction.getAmount().getTotal(); // 支付金额(分)
           String tradeState = transaction.getTradeState(); // 支付状态(SUCCESS为成功)

           // 4. 校验支付状态
           if (!"SUCCESS".equals(tradeState)) {
               log.warn("支付未成功,orderNo={},tradeState={}", outTradeNo, tradeState);
               return buildFailResponse("支付未成功");
           }

           // 5. 幂等性校验:查询订单是否已处理过
           Order order = orderService.getOrderByOrderNo(outTradeNo);
           if (ObjectUtils.isEmpty(order)) {
               log.error("回调订单不存在,orderNo={}", outTradeNo);
               return buildFailResponse("订单不存在");
           }
           if ("SUCCESS".equals(order.getOrderStatus())) {
               log.info("订单已处理过,无需重复处理,orderNo={}", outTradeNo);
               return buildSuccessResponse();
           }

           // 6. 校验金额一致性(防止金额篡改)
           if (!totalAmount.equals(order.getTotalAmount())) {
               log.error("回调金额与订单金额不一致,orderNo={},回调金额={},订单金额={}",
                       outTradeNo, totalAmount, order.getTotalAmount());
               return buildFailResponse("金额不一致");
           }

           // 7. 处理业务逻辑:更新订单状态+记录支付记录
           orderService.recordPayResult(order, transactionId, totalAmount, requestBody);
           
           // 8. 返回成功响应(必须按此格式,否则微信会重复回调)
           return buildSuccessResponse();
       } catch (Exception e) {
           log.error("处理微信支付回调失败", e);
           // 9. 返回失败响应,微信会重试(默认重试策略:15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h)
           return buildFailResponse("处理失败:" + e.getMessage());
       }
   }

   /**
    * 构建成功响应(微信要求固定JSON格式)
    * @return 成功响应字符串
    */

   private String buildSuccessResponse() {
       JSONObject result = new JSONObject();
       result.put("code", "SUCCESS");
       result.put("message", "成功");
       return result.toString();
   }

   /**
    * 构建失败响应
    * @param message 失败原因
    * @return 失败响应字符串
    */

   private String buildFailResponse(String message) {
       JSONObject result = new JSONObject();
       result.put("code", "FAIL");
       result.put("message", message);
       return result.toString();
   }
}

5.3 回调处理关键注意事项

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

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

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

6.1 退款业务流程

image.png

6.2 退款记录表Mapper与实体类

6.2.1 退款记录实体类

package com.jam.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;

/**
* 退款记录表实体类
* @author ken
*/

@Data
@TableName("t_refund_record")
public class RefundRecord {
   /** 主键ID */
   @TableId(type = IdType.AUTO)
   private Long id;
   /** 订单ID */
   private Long orderId;
   /** 商户订单号 */
   private String orderNo;
   /** 商户退款单号 */
   private String refundNo;
   /** 微信支付订单号 */
   private String transactionId;
   /** 微信退款单号 */
   private String refundId;
   /** 退款金额(分) */
   private Integer refundAmount;
   /** 订单总金额(分) */
   private Integer totalAmount;
   /** 退款原因 */
   private String refundReason;
   /** 退款状态(REFUND_PROCESSING:退款中;REFUND_SUCCESS:退款成功;REFUND_FAILED:退款失败) */
   private String refundStatus;
   /** 退款成功时间 */
   private LocalDateTime refundTime;
   /** 退款通知接收时间 */
   private LocalDateTime notifyTime;
   /** 退款通知原始数据 */
   private String notifyData;
   /** 创建时间 */
   private LocalDateTime createTime;
   /** 更新时间 */
   private LocalDateTime updateTime;
}

6.2.2 退款记录Mapper接口

package com.jam.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.RefundRecord;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

/**
* 退款记录Mapper
* @author ken
*/

@Mapper
public interface RefundRecordMapper extends BaseMapper<RefundRecord> {
   /**
    * 根据商户退款单号查询退款记录
    * @param refundNo 商户退款单号
    * @return 退款记录
    */

   RefundRecord selectByRefundNo(@Param("refundNo") String refundNo);

   /**
    * 更新退款状态
    * @param refundNo 商户退款单号
    * @param oldStatus 原状态
    * @param newStatus 新状态
    * @param refundId 微信退款单号
    * @param refundTime 退款成功时间
    * @return 影响行数
    */

   int updateRefundStatus(
           @Param("refundNo")
String refundNo,
           @Param("oldStatus") String oldStatus,
           @Param("newStatus") String newStatus,
           @Param("refundId") String refundId,
           @Param("refundTime") LocalDateTime refundTime);
}

6.2.3 退款记录Mapper XML

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jam.demo.mapper.RefundRecordMapper">
   <select id="selectByRefundNo" resultType="com.jam.demo.entity.RefundRecord">
       SELECT id, order_id, order_no, refund_no, transaction_id, refund_id, refund_amount,
              total_amount, refund_reason, refund_status, refund_time, notify_time, notify_data,
              create_time, update_time
       FROM t_refund_record
       WHERE refund_no = #{refundNo}
   </select>

   <update id="updateRefundStatus">
       UPDATE t_refund_record
       SET refund_status = #{newStatus},
           refund_id = #{refundId},
           refund_time = #{refundTime},
           update_time = NOW()
       WHERE refund_no = #{refundNo} AND refund_status = #{oldStatus}
   </update>
</mapper>

6.3 退款Service层实现

6.3.1 退款Service接口

package com.jam.demo.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.jam.demo.entity.RefundRecord;
import com.jam.demo.vo.RefundVO;

/**
* 退款服务接口
* @author ken
*/

public interface RefundService extends IService<RefundRecord> {
   /**
    * 发起退款
    * @param orderNo 商户订单号
    * @param refundAmount 退款金额(分)
    * @param refundReason 退款原因
    * @return 退款结果
    */

   RefundVO createRefund(String orderNo, Integer refundAmount, String refundReason);

   /**
    * 处理退款异步回调
    * @param notifyData 回调原始数据
    * @return 是否处理成功
    */

   boolean handleRefundNotify(String notifyData);
}

6.3.2 退款Service实现类

package com.jam.demo.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.demo.entity.Order;
import com.jam.demo.entity.RefundRecord;
import com.jam.demo.mapper.OrderMapper;
import com.jam.demo.mapper.RefundRecordMapper;
import com.jam.demo.service.OrderService;
import com.jam.demo.service.RefundService;
import com.jam.demo.util.WechatPayUtil;
import com.jam.demo.vo.RefundVO;
import com.wechat.pay.java.core.exception.ServiceException;
import com.wechat.pay.java.service.refund.RefundService;
import com.wechat.pay.java.service.refund.model.Amount;
import com.wechat.pay.java.service.refund.model.CreateRefundRequest;
import com.wechat.pay.java.service.refund.model.CreateRefundResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.time.LocalDateTime;

/**
* 退款服务实现类
* @author ken
*/

@Slf4j
@Service
public class RefundServiceImpl extends ServiceImpl<RefundRecordMapper, RefundRecord> implements RefundService {

   @Resource
   private RefundRecordMapper refundRecordMapper;
   @Resource
   private OrderService orderService;
   @Resource
   private OrderMapper orderMapper;
   @Resource
   private WechatPayConfig wechatPayConfig;
   @Resource
   private com.wechat.pay.java.service.refund.RefundService wxRefundService;

   /**
    * 发起退款
    * @param orderNo 商户订单号
    * @param refundAmount 退款金额(分)
    * @param refundReason 退款原因
    * @return 退款结果
    */

   @Override
   @Transactional(rollbackFor = Exception.class)
   public RefundVO createRefund(String orderNo, Integer refundAmount, String refundReason)
{
       // 1. 参数校验
       StringUtils.hasText(orderNo, "商户订单号不能为空");
       StringUtils.hasText(refundAmount.toString(), "退款金额不能为空");
       if (refundAmount <= 0) {
           throw new IllegalArgumentException("退款金额必须大于0");
       }
       StringUtils.hasText(refundReason, "退款原因不能为空");

       // 2. 查询订单,校验退款条件
       Order order = orderService.getOrderByOrderNo(orderNo);
       if (ObjectUtils.isEmpty(order)) {
           throw new RuntimeException("订单不存在");
       }
       if (!"SUCCESS".equals(order.getOrderStatus())) {
           throw new RuntimeException("仅支付成功的订单可发起退款");
       }
       if (refundAmount > order.getTotalAmount()) {
           throw new RuntimeException("退款金额不能超过订单总金额");
       }

       // 3. 校验是否已发起过退款(避免重复退款)
       RefundRecord existRefund = lambdaQuery()
               .eq(RefundRecord::getOrderNo, orderNo)
               .eq(RefundRecord::getRefundStatus, "REFUND_PROCESSING")
               .or()
               .eq(RefundRecord::getRefundStatus, "REFUND_SUCCESS")
               .one();
       if (!ObjectUtils.isEmpty(existRefund)) {
           throw new RuntimeException("该订单已发起过退款,请勿重复操作");
       }

       // 4. 生成商户退款单号
       String refundNo = WechatPayUtil.generateRefundNo();

       // 5. 记录退款申请(状态:退款中)
       RefundRecord refundRecord = new RefundRecord();
       refundRecord.setOrderId(order.getId());
       refundRecord.setOrderNo(orderNo);
       refundRecord.setRefundNo(refundNo);
       refundRecord.setTransactionId(null); // 微信支付订单号后续补充
       refundRecord.setRefundId(null); // 微信退款单号后续补充
       refundRecord.setRefundAmount(refundAmount);
       refundRecord.setTotalAmount(order.getTotalAmount());
       refundRecord.setRefundReason(refundReason);
       refundRecord.setRefundStatus("REFUND_PROCESSING");
       refundRecord.setCreateTime(LocalDateTime.now());
       refundRecord.setUpdateTime(LocalDateTime.now());
       int insertCount = refundRecordMapper.insert(refundRecord);
       if (insertCount <= 0) {
           log.error("记录退款申请失败,refundNo={}", refundNo);
           throw new RuntimeException("发起退款失败");
       }

       // 6. 调用微信支付退款接口
       CreateRefundRequest refundRequest = new CreateRefundRequest();
       refundRequest.setOutTradeNo(orderNo);
       refundRequest.setOutRefundNo(refundNo);
       refundRequest.setReason(refundReason);
       refundRequest.setNotifyUrl(wechatPayConfig.getRefundNotifyUrl());

       // 设置金额信息
       Amount amount = new Amount();
       amount.setRefund(refundAmount);
       amount.setTotal(order.getTotalAmount());
       amount.setCurrency("CNY");
       refundRequest.setAmount(amount);

       try {
           CreateRefundResponse refundResponse = wxRefundService.createRefund(refundRequest);
           log.info("调用微信退款接口成功,orderNo={},refundNo={},refundId={}",
                   orderNo, refundNo, refundResponse.getRefundId());

           // 7. 更新退款记录的微信退款单号
           refundRecord.setTransactionId(refundResponse.getTransactionId());
           refundRecord.setRefundId(refundResponse.getRefundId());
           refundRecordMapper.updateById(refundRecord);

           // 8. 封装返回结果
           RefundVO refundVO = new RefundVO();
           refundVO.setOrderNo(orderNo);
           refundVO.setRefundNo(refundNo);
           refundVO.setRefundAmount(refundAmount);
           refundVO.setRefundStatus("REFUND_PROCESSING");
           refundVO.setRefundId(refundResponse.getRefundId());
           return refundVO;
       } catch (ServiceException e) {
           log.error("调用微信退款接口失败,orderNo={},refundNo={},错误码={},错误信息={}",
                   orderNo, refundNo, e.getErrorCode(), e.getMessage(), e);
           // 回滚退款记录(或更新为失败状态)
           refundRecord.setRefundStatus("REFUND_FAILED");
           refundRecordMapper.updateById(refundRecord);
           throw new RuntimeException("发起退款失败:" + e.getMessage());
       }
   }

   /**
    * 处理退款异步回调
    * @param notifyData 回调原始数据
    * @return 是否处理成功
    */

   @Override
   @Transactional(rollbackFor = Exception.class)
   public boolean handleRefundNotify(String notifyData)
{
       try {
           // 1. 解析退款回调数据
           JSONObject notifyJson = JSONObject.parseObject(notifyData);
           String outRefundNo = notifyJson.getString("out_refund_no"); // 商户退款单号
           String refundId = notifyJson.getString("refund_id"); // 微信退款单号
           String refundStatus = notifyJson.getString("refund_status"); // 退款状态

           // 2. 查询退款记录
           RefundRecord refundRecord = refundRecordMapper.selectByRefundNo(outRefundNo);
           if (ObjectUtils.isEmpty(refundRecord)) {
               log.error("退款回调记录不存在,outRefundNo={}", outRefundNo);
               return false;
           }

           // 3. 幂等性校验
           if (!"REFUND_PROCESSING".equals(refundRecord.getRefundStatus())) {
               log.info("退款记录已处理,无需重复操作,outRefundNo={}", outRefundNo);
               return true;
           }

           // 4. 更新退款状态
           String newStatus = "";
           LocalDateTime refundTime = null;
           if ("SUCCESS".equals(refundStatus)) {
               newStatus = "REFUND_SUCCESS";
               refundTime = LocalDateTime.now();
               // 同步更新订单状态为已退款
               orderService.updateOrderStatus(refundRecord.getOrderNo(), "SUCCESS", "REFUNDED");
           } else if ("CLOSED".equals(refundStatus)) {
               newStatus = "REFUND_FAILED";
           } else {
               log.warn("退款状态未知,outRefundNo={},refundStatus={}", outRefundNo, refundStatus);
               return false;
           }

           int updateCount = refundRecordMapper.updateRefundStatus(
                   outRefundNo,
                   "REFUND_PROCESSING",
                   newStatus,
                   refundId,
                   refundTime
           );

           // 5. 记录回调数据
           refundRecord.setNotifyTime(LocalDateTime.now());
           refundRecord.setNotifyData(notifyData);
           refundRecordMapper.updateById(refundRecord);

           log.info("处理退款回调成功,outRefundNo={},refundStatus={}", outRefundNo, newStatus);
           return updateCount > 0;
       } catch (Exception e) {
           log.error("处理退款回调失败", e);
           return false;
       }
   }
}

6.4 退款Controller接口

package com.jam.demo.controller;

import com.jam.demo.common.R;
import com.jam.demo.service.RefundService;
import com.jam.demo.vo.RefundVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
* 退款控制器
* @author ken
*/

@Slf4j
@RestController
@RequestMapping("/api/wechat/refund")
@Tag(name = "退款相关接口", description = "包含发起退款、退款回调处理等接口")
public class RefundController {

   @Resource
   private RefundService refundService;

   /**
    * 发起退款
    * @param orderNo 商户订单号
    * @param refundAmount 退款金额(分)
    * @param refundReason 退款原因
    * @return 退款结果
    */

   @PostMapping("/create")
   @Operation(summary = "发起退款", description = "对已支付成功的订单发起退款,需校验退款金额和订单状态")
   public R<RefundVO> createRefund(
           @Parameter(description = "商户订单号", required = true)
@RequestParam String orderNo,
           @Parameter(description = "退款金额(分)", required = true) @RequestParam Integer refundAmount,
           @Parameter(description = "退款原因", required = true) @RequestParam String refundReason) {
       try {
           RefundVO refundVO = refundService.createRefund(orderNo, refundAmount, refundReason);
           return R.success(refundVO);
       } catch (IllegalArgumentException e) {
           log.error("发起退款参数错误", e);
           return R.fail(e.getMessage());
       } catch (Exception e) {
           log.error("发起退款失败", e);
           return R.fail("发起退款失败:" + e.getMessage());
       }
   }

   /**
    * 退款异步回调处理
    * @param notifyData 回调原始数据
    * @return 回调响应
    */

   @PostMapping("/notify")
   @Operation(summary = "退款异步回调", description = "接收微信支付退款结果回调,更新退款状态")
   public String refundNotify(@Parameter(description = "回调原始数据", required = true) @RequestParam String notifyData) {
       try {
           boolean handleSuccess = refundService.handleRefundNotify(notifyData);
           if (handleSuccess) {
               return "{\"code\":\"SUCCESS\",\"message\":\"成功\"}";
           } else {
               return "{\"code\":\"FAIL\",\"message\":\"处理失败\"}";
           }
       } catch (Exception e) {
           log.error("处理退款回调失败", e);
           return "{\"code\":\"FAIL\",\"message\":\"处理异常\"}";
       }
   }
}

6.5 退款VO类定义

package com.jam.demo.vo;

import lombok.Data;

/**
* 退款结果VO
* @author ken
*/

@Data
public class RefundVO {
   /** 商户订单号 */
   private String orderNo;
   /** 商户退款单号 */
   private String refundNo;
   /** 退款金额(分) */
   private Integer refundAmount;
   /** 退款状态 */
   private String refundStatus;
   /** 微信退款单号 */
   private String refundId;
}

七、对账与账单下载:资金安全的最后一道防线

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

7.1 对账流程

image.png

7.2 账单下载核心代码

package com.jam.demo.service;

import com.wechat.pay.java.service.bill.BillService;
import com.wechat.pay.java.service.bill.model.DownloadBillRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.zip.GZIPInputStream;

/**
* 对账服务实现类
* @author ken
*/

@Slf4j
@Service
public class BillServiceImpl {

   @Resource
   private BillService billService;
   @Resource
   private WechatPayConfig wechatPayConfig;

   /**
    * 下载微信支付交易账单
    * @param billDate 账单日期(格式:yyyyMMdd)
    * @return 账单内容
    */

   public String downloadTradeBill(String billDate) {
       // 1. 参数校验
       StringUtils.hasText(billDate, "账单日期不能为空");
       // 校验日期格式
       try {
           LocalDate.parse(billDate, DateTimeFormatter.ofPattern("yyyyMMdd"));
       } catch (Exception e) {
           throw new IllegalArgumentException("账单日期格式错误,需为yyyyMMdd");
       }

       // 2. 构建账单下载请求
       DownloadBillRequest request = new DownloadBillRequest();
       request.setBillDate(billDate);
       request.setBillType("TRADE"); // 交易账单:TRADE,退款账单:REFUND
       request.setTarType("GZIP"); // 压缩类型:GZIP
       request.setMchid(wechatPayConfig.getMchId());

       try {
           // 3. 调用接口下载账单(返回InputStream)
           InputStream inputStream = billService.downloadBill(request);
           if (ObjectUtils.isEmpty(inputStream)) {
               log.error("下载账单失败,billDate={}", billDate);
               throw new RuntimeException("下载账单失败");
           }

           // 4. 解压GZIP压缩流
           GZIPInputStream gzipInputStream = new GZIPInputStream(inputStream);
           ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
           byte[] buffer = new byte[1024];
           int len;
           while ((len = gzipInputStream.read(buffer)) != -1) {
               outputStream.write(buffer, 0, len);
           }
           String billContent = outputStream.toString(StandardCharsets.UTF_8.name());

           // 5. 关闭流
           gzipInputStream.close();
           outputStream.close();
           inputStream.close();

           log.info("下载账单成功,billDate={},账单长度={}", billDate, billContent.length());
           return billContent;
       } catch (ServiceException e) {
           log.error("下载账单接口调用失败,billDate={},错误码={},错误信息={}",
                   billDate, e.getErrorCode(), e.getMessage(), e);
           throw new RuntimeException("下载账单失败:" + e.getMessage());
       } catch (Exception e) {
           log.error("解压账单失败,billDate={}", billDate, e);
           throw new RuntimeException("解压账单失败");
       }
   }

   /**
    * 核对账单数据
    * @param billContent 账单内容
    * @return 对账结果
    */

   public String checkBill(String billContent) {
       // 1. 按行解析账单(首行为表头,最后一行为统计行)
       String[] lines = billContent.split("\n");
       if (lines.length <= 2) {
           return "账单数据为空";
       }

       // 2. 统计本地订单数、金额 vs 账单订单数、金额
       int localOrderCount = 0; // 本地支付成功订单数
       int billOrderCount = 0; // 账单支付成功订单数
       long localTotalAmount = 0; // 本地支付总金额(分)
       long billTotalAmount = 0; // 账单支付总金额(分)

       // 3. 遍历账单行(跳过表头和统计行)
       for (int i = 1; i < lines.length - 1; i++) {
           String line = lines[i];
           if (StringUtils.isEmpty(line)) {
               continue;
           }
           String[] fields = line.split(",");
           // 账单字段:交易时间,公众账号ID,商户号,子商户号,设备号,微信订单号,商户订单号,用户标识,交易类型,交易状态,...
           String tradeStatus = fields[9]; // 交易状态
           if ("SUCCESS".equals(tradeStatus)) {
               billOrderCount++;
               String totalAmountStr = fields[15]; // 总金额(元)
               billTotalAmount += Math.round(Double.parseDouble(totalAmountStr) * 100); // 转为分
           }
       }

       // 4. 查询本地支付成功订单数据(示例逻辑,需根据实际业务调整)
       // localOrderCount = orderService.count(lambdaQuery().eq(Order::getOrderStatus, "SUCCESS").eq(Order::getCreateTime, billDate));
       // localTotalAmount = orderService.sum(lambdaQuery().eq(Order::getOrderStatus, "SUCCESS").eq(Order::getCreateTime, billDate));

       // 5. 对比数据
       if (localOrderCount == billOrderCount && localTotalAmount == billTotalAmount) {
           return String.format("对账成功:本地订单数=%d,账单订单数=%d;本地总金额=%d分,账单总金额=%d分",
                   localOrderCount, billOrderCount, localTotalAmount, billTotalAmount);
       } else {
           String errorMsg = String.format("对账异常:本地订单数=%d,账单订单数=%d;本地总金额=%d分,账单总金额=%d分",
                   localOrderCount, billOrderCount, localTotalAmount, billTotalAmount);
           log.error(errorMsg);
           // 触发告警(如钉钉/企业微信通知)
           sendAlarm(errorMsg);
           return errorMsg;
       }
   }

   /**
    * 发送对账异常告警
    * @param msg 告警信息
    */

   private void sendAlarm(String msg) {
       // 实际项目中对接告警渠道,此处为示例
       log.warn("对账异常告警:{}", msg);
   }
}

7.3 对账定时任务配置

package com.jam.demo.config;

import com.jam.demo.service.BillServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;

import javax.annotation.Resource;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

/**
* 对账定时任务配置
* 每日凌晨1点下载前一日账单并对账(微信支付账单生成时间为次日0点后)
* @author ken
*/

@Slf4j
@Configuration
@EnableScheduling
public class BillScheduleConfig {

   @Resource
   private BillServiceImpl billService;

   /**
    * 每日凌晨1点执行对账任务
    */

   @Scheduled(cron = "0 0 1 * * ?")
   public void checkBillTask() {
       try {
           // 获取前一日日期(格式:yyyyMMdd)
           String billDate = LocalDate.now().minusDays(1).format(DateTimeFormatter.ofPattern("yyyyMMdd"));
           log.info("开始执行对账任务,账单日期={}", billDate);
           
           // 下载账单
           String billContent = billService.downloadTradeBill(billDate);
           
           // 核对账单
           String checkResult = billService.checkBill(billContent);
           log.info("对账任务执行完成,结果={}", checkResult);
       } catch (Exception e) {
           log.error("执行对账任务失败", e);
       }
   }
}

八、微信支付常见问题与避坑指南

8.1 签名验证失败

原因分析

  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. 避坑重点:签名错误、回调重复、退款权限、订单状态不一致是高频问题,需针对性做好参数校验、幂等设计和补偿机制。

本文从底层逻辑到实战代码,完整覆盖了微信支付Native/JSAPI支付、异步回调、退款、对账等核心能力。在实际项目中,需结合业务场景补充异常监控、资金告警、日志审计等能力,进一步保障支付系统的稳定性和资金安全。

目录
相关文章
|
2天前
|
云安全 监控 安全
|
7天前
|
机器学习/深度学习 人工智能 自然语言处理
Z-Image:冲击体验上限的下一代图像生成模型
通义实验室推出全新文生图模型Z-Image,以6B参数实现“快、稳、轻、准”突破。Turbo版本仅需8步亚秒级生成,支持16GB显存设备,中英双语理解与文字渲染尤为出色,真实感和美学表现媲美国际顶尖模型,被誉为“最值得关注的开源生图模型之一”。
932 5
|
13天前
|
人工智能 Java API
Java 正式进入 Agentic AI 时代:Spring AI Alibaba 1.1 发布背后的技术演进
Spring AI Alibaba 1.1 正式发布,提供极简方式构建企业级AI智能体。基于ReactAgent核心,支持多智能体协作、上下文工程与生产级管控,助力开发者快速打造可靠、可扩展的智能应用。
1097 41
|
9天前
|
机器学习/深度学习 人工智能 数据可视化
1秒生图!6B参数如何“以小博大”生成超真实图像?
Z-Image是6B参数开源图像生成模型,仅需16GB显存即可生成媲美百亿级模型的超真实图像,支持中英双语文本渲染与智能编辑,登顶Hugging Face趋势榜,首日下载破50万。
664 38
|
13天前
|
人工智能 前端开发 算法
大厂CIO独家分享:AI如何重塑开发者未来十年
在 AI 时代,若你还在紧盯代码量、执着于全栈工程师的招聘,或者仅凭技术贡献率来评判价值,执着于业务提效的比例而忽略产研价值,你很可能已经被所谓的“常识”困住了脚步。
758 67
大厂CIO独家分享:AI如何重塑开发者未来十年
|
9天前
|
存储 自然语言处理 测试技术
一行代码,让 Elasticsearch 集群瞬间雪崩——5000W 数据压测下的性能避坑全攻略
本文深入剖析 Elasticsearch 中模糊查询的三大陷阱及性能优化方案。通过5000 万级数据量下做了高压测试,用真实数据复刻事故现场,助力开发者规避“查询雪崩”,为您的业务保驾护航。
473 30
|
16天前
|
数据采集 人工智能 自然语言处理
Meta SAM3开源:让图像分割,听懂你的话
Meta发布并开源SAM 3,首个支持文本或视觉提示的统一图像视频分割模型,可精准分割“红色条纹伞”等开放词汇概念,覆盖400万独特概念,性能达人类水平75%–80%,推动视觉分割新突破。
937 59
Meta SAM3开源:让图像分割,听懂你的话
|
5天前
|
弹性计算 网络协议 Linux
阿里云ECS云服务器详细新手购买流程步骤(图文详解)
新手怎么购买阿里云服务器ECS?今天出一期阿里云服务器ECS自定义购买流程:图文全解析,阿里云服务器ECS购买流程图解,自定义购买ECS的设置选项是最复杂的,以自定义购买云服务器ECS为例,包括付费类型、地域、网络及可用区、实例、镜像、系统盘、数据盘、公网IP、安全组及登录凭证详细设置教程:
204 114