以下文档内容,部分摘自【微信支付】产品文档

一、产品介绍

1、产品概述

Native支付,提供商户在PC端网页浏览器中使用微信支付收款的能力。

2、Native支付模式介绍

  • 商户下单获取订单的二维码链接 code_url,将 code_url转换为二维码图片展示给用户。
  • 用户使用微信“扫一扫”进行扫码(不支持通过相册识别或长按识别二维码的方式完成支付)。
  • 扫码进入到微信的支付确认界面,用户可在该页面确认收款方和金额。
  • 用户确认订单收款方和金额无误后,点击“立即支付”会出现验密界面(验证密码或指纹等),同时在该页面也可选择支付方式(零钱或银行卡等)。
  • 验密付款成功后,微信会展示支付成功页面。
  • 支付成功后,用户在微信支付-我的账单-账单明细中查看账单。

3、基础 URL

https://api.mch.weixin.qq.com

二、基础代码

1、项目架构

java
└── com.lee
    ├── config
    │   ├── FastJson2JsonRedisSerializer
    │   ├── RedisCache
    │   ├── RedisConfig
    │   └── WxPayConfig
    ├── constant
    │   ├── HttpStatus
    │   └── PayConstants
    ├── controller
    │   ├── ApiController
    │   └── WxPayController
    ├── domain
    │   ├── TBGoods
    │   └── TBOrder
    ├── entity
    │   ├── AjaxResult
    │   ├── DecryptNotifyResult
    │   └── WxPay
    ├── enums
    │   ├── CurrencyType
    │   ├── InterfaceURL
    │   └── ReturnStatus
    ├── exception
    │   ├── UrlException
    │   └── WxPayException
    ├── listener
    │   └── RedisKeyExpiredListener
    ├── mapper
    │   ├── TBGoodsMapper
    │   └── TBOrderMapper
    ├── service
    │   ├── WxPayService
    │   └── WxPayServiceImpl
    ├── utils
    │   ├── ServletUtils
    │   ├── SpringUtils
    │   └── WxPayUtils
    └── CacheApplication
── resources
    ├── application.yml
    └── wxpay
         └── apiclient_key.pem

2、基础代码

(1)pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- Spring Boot父工程,统一管理依赖版本 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.6</version> <!-- Spring Boot版本 -->
        <!--        <relativePath/>-->
    </parent>

    <groupId>com.lee</groupId>
    <artifactId>WxPay</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <fastjson.version>2.0.25</fastjson.version>
    </properties>

    <dependencies>
        <!-- SpringBoot Web容器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- redis 缓存操作 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
        </dependency>

        <!-- 微信支付 -->
        <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-apache-httpclient</artifactId>
            <version>0.4.8</version>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.18</version>
        </dependency>

        <!-- 阿里JSON解析器 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>${fastjson.version}</version>
        </dependency>

        <!-- servlet包 -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
        </dependency>
    </dependencies>
</project>

(2)yaml

# 开发环境配置
server:
  # 服务器的HTTP端口,默认为8080
  port: 8080
  servlet:
    # 应用的访问路径
    context-path: /
  tomcat:
    # tomcat的URI编码
    uri-encoding: UTF-8
    # 连接数满后的排队数,默认为100
    accept-count: 1000
    threads:
      # tomcat最大线程数,默认为200
      max: 800
      # Tomcat启动初始化的线程数,默认值10
      min-spare: 100
    # tomcat 超时连接时间
    connection-timeout: 1800000

# 微信支付相关参数
wxpay:
  # 商户号
  mchId: 164****034
  # APIv3密钥
  apiV3Key: 112233445566778899qqwweerrttyyuu
  # APPID
  appid: wx******9e266582f8
  # 回调地址
  # 注意:修改后重新启动(需要项目根据实际情况修改配置)
  notifyDomain: https://www.****.com/wx/notify
  # 商户私钥文件
  # 注意:该文件放在项目根目录下
  privateKeyPath: wxpay/apiclient_key.pem
  # 商户API证书序列号
  mchSerialNo: FFFFFF708746F19818F5D63C3C91EDDDB52E53DF
  # 微信支付基础 url
  domain: https://api.mch.weixin.qq.com/v3

lee:
  expiration: 10 # 支付到期时间(Redis)
  
# Spring配置
spring:
  # redis 配置
  redis:
    # 地址
    host: localhost
    # 端口,默认为6379
    port: 6379
    # 数据库索引
    database: 11
    # 密码
    password: 12345678
    # 连接超时时间
    timeout: 10s
    lettuce:
      pool:
        # 连接池中的最小空闲连接
        min-idle: 0
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池的最大数据库连接数
        max-active: 8
        # #连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms

(3)WxPayApplication.java

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author lee
 * @date 2025.08.20
 * 应用程序入口类
 * 作为Spring Boot应用的启动点,负责初始化和启动整个应用程序上下文
 */
@SpringBootApplication
public class WxPayApplication {
    /**
     * 程序主入口方法
     *
     * @param args 命令行参数
     */
    public static void main(String[] args) {
        // 启动Spring Boot应用,创建并刷新应用上下文
        SpringApplication.run(WxPayApplication.class, args);
    }
}

(4)ServletUtils.java

import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * @author lee
 * @date 2023-05-27
 * 客户端工具类
 */
public class ServletUtils {
    /**
     * 获取String参数
     */
    public static String getParameter(String name) {
        return getRequest().getParameter(name);
    }

    /**
     * 获取request
     */
    public static HttpServletRequest getRequest() {
        return getRequestAttributes().getRequest();
    }

    public static ServletRequestAttributes getRequestAttributes() {
        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        return (ServletRequestAttributes) attributes;
    }
}

(5)SpringUtils.java

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Component;

/**
 * @author lee
 * @date 2023-05-27
 * spring工具类 方便在非spring管理环境中获取bean
 *
 */
@Component
public final class SpringUtils implements BeanFactoryPostProcessor {
    /**
     * Spring应用上下文环境
     */
    private static ConfigurableListableBeanFactory beanFactory;

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        SpringUtils.beanFactory = beanFactory;
    }

    /**
     * 获取对象
     *
     * @param name
     * @return Object 一个以所给名字注册的bean的实例
     * @throws BeansException
     */
    @SuppressWarnings("unchecked")
    public static <T> T getBean(String name) throws BeansException {
        return (T) beanFactory.getBean(name);
    }

    /**
     * 获取类型为requiredType的对象
     *
     * @param clz
     * @return
     * @throws BeansException
     */
    public static <T> T getBean(Class<T> clz) throws BeansException {
        T result = (T) beanFactory.getBean(clz);
        return result;
    }
}

(6)FastJson2JsonRedisSerializer.java

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

/**
 * @author lee
 * @date 2023-05-27
 * Redis使用FastJson序列化
 */
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {
    public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

    private Class<T> clazz;

    public FastJson2JsonRedisSerializer(Class<T> clazz) {
        super();
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException {
        if (t == null) {
            return new byte[0];
        }
        return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException {
        if (bytes == null || bytes.length <= 0) {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);

        return JSON.parseObject(str, clazz, JSONReader.Feature.SupportAutoType);
    }
}

(7)RedisCache.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * @author lee
 * @date 2023-05-27
 * spring redis 工具类
 **/
@SuppressWarnings(value = {"unchecked", "rawtypes"})
@Component
public class RedisCache {
    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key      缓存的键值
     * @param value    缓存的值
     * @param timeout  时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key) {
        return redisTemplate.delete(key);
    }
}

(8)RedisConfig.java

import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @author lee
 * @date 2023-05-27
 * redis配置
 * todo redis也需要开启事件
 */
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

    /**
     * 注册监听事件
     *
     * @param connectionFactory
     * @return
     */
    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        return container;
    }

    @Bean
    @SuppressWarnings(value = {"unchecked", "rawtypes"})
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }
}

(9)WxPayConfig.java

import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ResourceLoader;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author lee
 * @date 2023-05-27
 * 读取项目相关配置
 */
@Slf4j
@Configuration
@ConfigurationProperties(prefix = "wxpay")
public class WxPayConfig {

    public static ResourceLoader resourceLoader;

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

    /**
     * APIv3密钥
     */
    private static String apiV3Key;

    /**
     * APPID
     */
    private static String appid;

    /**
     * 商户私钥文件
     */
    private static String privateKeyPath;

    /**
     * 商户API证书序列号
     */
    private static String mchSerialNo;

    /**
     * 微信支付基础 url
     */
    public static String domain;

    /**
     * 回调地址
     */
    private static String notifyDomain;

    private static final ConcurrentHashMap<String, Verifier> verifierMap = new ConcurrentHashMap<>();

    /**
     * 获取签名验证器
     *
     * @return
     */
    @Bean
    public Verifier defaultVerifier() {
        log.info("获取签名验证器");
        if (verifierMap.isEmpty() || !verifierMap.containsKey(mchSerialNo)) {
            verifierMap.clear();
            // 获取证书管理器实例
            CertificatesManager certificatesManager = CertificatesManager.getInstance();
            //获取商户私钥
            PrivateKey privateKey = getPrivateKey();
            //私钥签名对象
            PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
            //身份认证对象
            WechatPay2Credentials credentials = new WechatPay2Credentials(mchId, privateKeySigner);
            try {
                certificatesManager.putMerchant(mchId, credentials, apiV3Key.getBytes(StandardCharsets.UTF_8));
                Verifier verifier = certificatesManager.getVerifier(mchId);
                verifierMap.put(mchSerialNo, verifier);
                return verifier;
            } catch (Exception e) {
                log.error("获取签名验证器失败 {}", e.fillInStackTrace());
            }
        }
        return null;
    }

    /**
     * 获取 http 请求对象
     *
     * @return
     */
    @Bean(name = "wxPayClient")
    public CloseableHttpClient getWxPayClient(Verifier defaultVerifier) {
        log.info("获取 需要验证的 CloseableHttpClient");
        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                // 设置商户信息
                .withMerchant(mchId, mchSerialNo, getPrivateKey())
                // 需要进行签名验证实现
                .withValidator(new WechatPay2Validator(defaultVerifier));
        // ... 接下来, 你仍然可以通过builder设置各种参数, 来配置你的HttpClient

        // 通过 WechatPayHttpClientBuilder 构造的 HttpClient, 会自动的处理签名和验签, 并进行证书自动更新
        CloseableHttpClient httpClient = builder.build();
        log.info("== getWxPayClient END ==");
        return httpClient;
    }

    /**
     * 获取 HttpClient, 无需进行应答签名验证, 跳过验签的流程
     */
    @Bean(name = "wxPayNoSignClient")
    public CloseableHttpClient getWxPayNoSignClient() {
        log.info("获取 无需验证的 CloseableHttpClient");
        // 用于构造 HttpClient
        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                // 设置商户信息
                .withMerchant(mchId, mchSerialNo, getPrivateKey())
                // 无需进行签名验证、通过 withValidator((response) -> true) 实现
                .withValidator((response) -> true);
        // 通过 WechatPayHttpClientBuilder 构造的 HttpClient, 会自动的处理签名和验签, 并进行证书自动更新
        CloseableHttpClient httpClient = builder.build();
        log.info("== getWxPayNoSignClient END ==");
        return httpClient;
    }

    /**
     * 获取商户的私钥文件
     *
     * @return
     */
    private PrivateKey getPrivateKey() {
        log.info("获取商户私钥");
        try {
            return PemUtil.loadPrivateKey(WxPayConfig.resourceLoader.getResource(ResourceLoader.CLASSPATH_URL_PREFIX + privateKeyPath).getInputStream());
        } catch (IOException e) {
            log.error("私钥格式不正确: {}", e.fillInStackTrace());
        }
        return null;
    }

    public static ResourceLoader getResourceLoader() {
        return resourceLoader;
    }

    @Autowired
    public void setResourceLoader(ResourceLoader resourceLoader) {
        WxPayConfig.resourceLoader = resourceLoader;
    }

    public static String getMchId() {
        return mchId;
    }

    public void setMchId(String mchId) {
        WxPayConfig.mchId = mchId;
    }

    public static String getApiV3Key() {
        return apiV3Key;
    }

    public void setApiV3Key(String apiV3Key) {
        WxPayConfig.apiV3Key = apiV3Key;
    }

    public static String getAppid() {
        return appid;
    }

    public void setAppid(String appid) {
        WxPayConfig.appid = appid;
    }

    public static String getPrivateKeyPath() {
        return privateKeyPath;
    }

    public void setPrivateKeyPath(String privateKeyPath) {
        WxPayConfig.privateKeyPath = privateKeyPath;
    }

    public static String getMchSerialNo() {
        return mchSerialNo;
    }

    public void setMchSerialNo(String mchSerialNo) {
        WxPayConfig.mchSerialNo = mchSerialNo;
    }

    public static String getDomain() {
        return domain;
    }

    public void setDomain(String domain) {
        WxPayConfig.domain = domain;
    }

    public static String getNotifyDomain() {
        return notifyDomain;
    }

    public void setNotifyDomain(String notifyDomain) {
        WxPayConfig.notifyDomain = notifyDomain;
    }
}

(10)HttpStatus.java

/**
 * @author lee
 * @date 2023-05-27
 * 返回状态码
 */
public class HttpStatus {
    /**
     * 操作成功
     */
    public static final int SUCCESS = 200;

    /**
     * 对象创建成功
     */
    public static final int CREATED = 201;

    /**
     * 请求已经被接受
     */
    public static final int ACCEPTED = 202;

    /**
     * 操作已经执行成功,但是没有返回数据
     */
    public static final int NO_CONTENT = 204;

    /**
     * 资源已被移除
     */
    public static final int MOVED_PERM = 301;

    /**
     * 重定向
     */
    public static final int SEE_OTHER = 303;

    /**
     * 资源没有被修改
     */
    public static final int NOT_MODIFIED = 304;

    /**
     * 参数列表错误(缺少,格式不匹配)
     */
    public static final int BAD_REQUEST = 400;

    /**
     * 未授权
     */
    public static final int UNAUTHORIZED = 401;

    /**
     * 访问受限,授权过期
     */
    public static final int FORBIDDEN = 403;

    /**
     * 资源,服务未找到
     */
    public static final int NOT_FOUND = 404;

    /**
     * 不允许的http方法
     */
    public static final int BAD_METHOD = 405;

    /**
     * 资源冲突,或者资源被锁
     */
    public static final int CONFLICT = 409;

    /**
     * 不支持的数据,媒体类型
     */
    public static final int UNSUPPORTED_TYPE = 415;

    /**
     * 系统内部错误
     */
    public static final int ERROR = 500;

    /**
     * 接口未实现
     */
    public static final int NOT_IMPLEMENTED = 501;

    /**
     * 系统警告消息
     */
    public static final int WARN = 601;
}

(11)PayConstants.java

/**
 * @author lee
 * @date 2023-05-27
 * 支付相关常量
 */
public class PayConstants {
    /**
     * 订单信息 redis key
     */
    public static final String TB_ORDER_KEY = "order:expiration:{}";


    /**
     * 支付方式 - 微信
     */
    public static final String PAYMENT_METHOD_WX = "WXPAY";


    /**
     * 下单状态 - 下单失败
     */
    public static final String CHANNEL_STATE_ERR = "0";
    public static final String CHANNEL_STATE_SUCC = "1";


    /**
     * 支付状态 - 未支付
     */
    public static final String PAYMENT_STATUS_UN = "0";
    /**
     * 支付状态 - 已支付
     */
    public static final String PAYMENT_STATUS_SUCC = "1";
    /**
     * 支付状态 - 已过期
     */
    public static final String PAYMENT_STATUS_EXP = "2";
    /**
     * 支付状态 - 支付失败
     */
    public static final String PAYMENT_STATUS_ERR = "9";

    /**
     * 订单状态 - 删除
     */
    public static final String DELETED = "2";

    /**
     * 币种-人名币
     */
    public static final String CNY = "CNY";
}

(12)TBGoods.java

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author lee
 * @date 2023-05-27
 * 商品
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TBGoods {
    /**
     * 商品 id
     */
    private Long id;

    /**
     * 商品名
     */
    private String name;

    /**
     * 价格:分
     */
    private String amount;
}

(13)TBOrder.java

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

import java.util.Date;

/**
 * @author lee
 * @date 2023-05-27
 * 订单
 */
@Data
@Builder
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
public class TBOrder {
    /**
     * 订单编号 - 商户订单号
     * 20 位
     */
    private String outTradeNo;

    /**
     * 账户 - 账户 id
     * 20 位
     */
    private String accountId;

    /**
     * 总金额(小写): 分
     */
    private String amount;

    /**
     * 总金额(大写)
     */
    private String amountCapital;

    /**
     * 商品名称 - 商品描述
     */
    private String description;

    /**
     * 支付方式: WXPAY
     */
    private String paymentMethod;

    /**
     * 支付二维码连接
     **/
    private String codeUrl;

    /**
     * 订单状态(0未支付 1已支付 2已过期)
     */
    private String paymentStatus;

    /**
     * 联系方式
     */
    private String phoneNo;

    /**
     * 创建时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;

// ------------------------------------------------------------------------------

    /**
     * 微信支付订单号 - 渠道订单号
     */
    private String transactionId;

    /**
     * 支付时间 - 支付完成时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date payTime;

    /**
     * 支付用户
     */
    private String payer;

    /**
     * DecryptNotifyResult 回调返回内容 json
     */
    private String resource;

// ------------------------------------------------------------------------------

    /**
     * 过期时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date expirationTime;

    /**
     * 更新者
     */
    private String updateBy;

    /**
     * 更新时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date updateTime;

    /**
     * 用户标志(0代表存在 1代表删除)
     */
    private String userDelFlag;
}

(14)AjaxResult.java

import cn.hutool.core.util.ObjectUtil;
import com.lee.constant.HttpStatus;

import java.util.HashMap;

/**
 * @author lee
 * @date 2023-05-27
 * 操作消息提醒
 */
public class AjaxResult extends HashMap<String, Object> {
    private static final long serialVersionUID = 1L;

    /**
     * 状态码
     */
    public static final String CODE_TAG = "code";

    /**
     * 返回内容
     */
    public static final String MSG_TAG = "msg";

    /**
     * 数据对象
     */
    public static final String DATA_TAG = "data";

    /**
     * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
     */
    public AjaxResult() {
    }

    /**
     * 初始化一个新创建的 AjaxResult 对象
     *
     * @param code 状态码
     * @param msg  返回内容
     */
    public AjaxResult(int code, String msg) {
        super.put(CODE_TAG, code);
        super.put(MSG_TAG, msg);
    }

    /**
     * 初始化一个新创建的 AjaxResult 对象
     *
     * @param code 状态码
     * @param msg  返回内容
     * @param data 数据对象
     */
    public AjaxResult(int code, String msg, Object data) {
        super.put(CODE_TAG, code);
        super.put(MSG_TAG, msg);
        if (ObjectUtil.isNotNull(data)) {
            super.put(DATA_TAG, data);
        }
    }

    /**
     * 返回成功消息
     *
     * @return 成功消息
     */
    public static AjaxResult success() {
        return AjaxResult.success("操作成功");
    }

    /**
     * 返回成功数据
     *
     * @return 成功消息
     */
    public static AjaxResult success(Object data) {
        return AjaxResult.success("操作成功", data);
    }

    /**
     * 返回成功消息
     *
     * @param msg 返回内容
     * @return 成功消息
     */
    public static AjaxResult success(String msg) {
        return AjaxResult.success(msg, null);
    }

    /**
     * 返回成功消息
     *
     * @param msg  返回内容
     * @param data 数据对象
     * @return 成功消息
     */
    public static AjaxResult success(String msg, Object data) {
        return new AjaxResult(HttpStatus.SUCCESS, msg, data);
    }

    /**
     * 返回警告消息
     *
     * @param msg 返回内容
     * @return 警告消息
     */
    public static AjaxResult warn(String msg) {
        return AjaxResult.warn(msg, null);
    }

    /**
     * 返回警告消息
     *
     * @param msg  返回内容
     * @param data 数据对象
     * @return 警告消息
     */
    public static AjaxResult warn(String msg, Object data) {
        return new AjaxResult(HttpStatus.WARN, msg, data);
    }

    /**
     * 返回错误消息
     *
     * @return 错误消息
     */
    public static AjaxResult error() {
        return AjaxResult.error("操作失败");
    }

    /**
     * 返回错误消息
     *
     * @param msg 返回内容
     * @return 错误消息
     */
    public static AjaxResult error(String msg) {
        return AjaxResult.error(msg, null);
    }

    /**
     * 返回错误消息
     *
     * @param msg  返回内容
     * @param data 数据对象
     * @return 错误消息
     */
    public static AjaxResult error(String msg, Object data) {
        return new AjaxResult(HttpStatus.ERROR, msg, data);
    }

    /**
     * 返回错误消息
     *
     * @param code 状态码
     * @param msg  返回内容
     * @return 错误消息
     */
    public static AjaxResult error(int code, String msg) {
        return new AjaxResult(code, msg, null);
    }

    /**
     * 方便链式调用
     *
     * @param key   键
     * @param value 值
     * @return 数据对象
     */
    @Override
    public AjaxResult put(String key, Object value) {
        super.put(key, value);
        return this;
    }
}

(15)InterfaceURL.java

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * @author lee
 * @date 2023-05-27
 * 微信接口枚举
 */
@AllArgsConstructor
public enum InterfaceURL {
    /**
     * Native下单
     */
    NATIVE_PAY("/pay/transactions/native"),

    /**
     * 关闭订单
     */
    CLOSE_ORDER_BY_NO("/pay/transactions/out-trade-no/{}/close"),

    /**
     * 查询订单
     */
    ORDER_QUERY_BY_NO("/pay/transactions/id/{}"),

    ;
    @Getter
    private String url;
}

(16)ReturnStatus.java

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * @author lee
 * @date 2023-05-27
 * 微信状态码
 */
@AllArgsConstructor
public enum ReturnStatus {
    S400(400),
    S401(401),
    S403(403),
    S404(404),
    S429(429),
    S500(500),
    S200(200),
    S204(204),

    ;
    @Getter
    private Integer status;
}

(17)UtilException.java

/**
 * @author lee
 * @date 2023-05-27
 * 工具类异常
 */
public class UtilException extends RuntimeException {
    private static final long serialVersionUID = 8247610319171014183L;

    public UtilException(Throwable e) {
        super(e.getMessage(), e);
    }

    public UtilException(String message) {
        super(message);
    }

    public UtilException(String message, Throwable throwable) {
        super(message, throwable);
    }
}

(18)WxPayException.java

/**
 * @author lee
 * @date 2023-05-27
 * 微信支付异常
 */
public class WxPayException extends RuntimeException {

    private static final long serialVersionUID = 1L;

    /**
     * 错误码
     */
    private Integer code;

    /**
     * 错误提示
     */
    private String message;

    /**
     * 空构造方法,避免反序列化问题
     */
    public WxPayException() {
    }

    public WxPayException(String message) {
        this.message = message;
    }

    public WxPayException(Integer code, String message) {
        this.message = message;
        this.code = code;
    }

    @Override
    public String getMessage() {
        return message;
    }

    public Integer getCode() {
        return code;
    }

    public WxPayException setMessage(String message) {
        this.message = message;
        return this;
    }
}

(19)TBGoodsMapper.java

import com.lee.domain.TBGoods;

/**
 * @author lee
 * @date 2023/6/15
 * TB_Goods商品信息 数据层
 */
public interface TBGoodsMapper {

    /**
     * 根据商品id查询商品
     *
     * @param goodsId 商品id
     * @return 结果
     */
    TBGoods getById(Long goodsId);
}

(20)TBOrderMapper.java

import com.lee.domain.TBOrder;

/**
 * @author lee
 * @date 2023/6/15
 * TB_User用户信息 数据层
 */
public interface TBOrderMapper {

    /**
     * 根据条件分页查询用户列表
     *
     * @param tbOrder 用户ID
     * @return 结果
     */
    int createOrder(TBOrder tbOrder);

    /**
     * 根据订单编号查询订单
     *
     * @param outTradeNo 订单编号
     * @return 结果
     */
    TBOrder getOrderByOutTradeNo(String outTradeNo);

    /**
     * 更新订单
     *
     * @param tbOrder 订单
     * @return 结果
     */
    int update(TBOrder tbOrder);

    /**
     * 根据订单编号 获取 支付成功 的订单信息
     *
     * @param outTradeNo
     * @return
     */
    TBOrder getByOutTradeNo(String outTradeNo);
}

三、产品对接

以下步骤为已开通微信支付功能的前提下进行

**注:**自行申请微信支付权限:开通权限

1、商户下单

(1)调用流程

(2)调用参数

url:/v3/pay/transactions/native

参数名 参数含义 参数类型 是否必输 备注
Header Authorization 签名认证 string 必填 代码示例时间:2023/6/15
Accept 请求类型 string 必填 application/json
**Content-Type ** 响应类型 string 必填 application/json
body appid 【公众账号ID】 string(32) 必填 APPID是微信开放平台(移动应用)或微信公众平台(小程序、公众号)
mchid 【商户号】 string(32) 必填 是由微信支付系统生成并分配给每个商户的唯一标识符
description 【商品描述】 string(127) 必填 商品信息描述
out_trade_no 【商户订单号】 string(32) 必填 自己系统内部订单号
time_expire 【支付结束时间】 string(64) 选填 标准格式:yyyy-MM-DDTHH:mm:ss+TIMEZONE
attach 【商户数据包】 string(128) 选填 商户在创建订单时可传入自定义数据包
notify_url 【商户回调地址】 string(255) 必填 商户接收支付成功回调通知的地址
amount 【订单金额】 object 必填 {
“total” : 100,
“currency” : “CNY”
}
total 【总金额】 integer 必填 amount 下
**currency ** 【货币类型】 string(16) 选填 amount 下
  • 关于 Authorization 的解释:根据 签名认证 的说明;只有 JSAPI、APP、小程序调起支付时,才需要支付签名
  • 其他选填非必传参数自行查看 Native下单
  • 加粗参数为我使用到的

(3)应答数据

状态码:statusLine

状态码 描述
200 成功

响应体:entity

参数名 参数含义 参数类型 是否必输 备注
code_url**** 【二维码链接】 string(64) 必填 此URL用于生成支付二维码,然后提供给用户扫码支付

注:其他错误码,自行查看 Native下单

(4)代码

1)ApiController.java
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import com.lee.domain.TBGoods;
import com.lee.domain.TBOrder;
import com.lee.entity.AjaxResult;
import com.lee.mapper.TBGoodsMapper;
import com.lee.mapper.TBOrderMapper;
import com.lee.service.WxPayService;
import com.lee.utils.ServletUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

/**
 * @author lee
 * @date 2025.08.20
 * API 接口
 */
@RestController
@RequestMapping("/api")
public class ApiController {
    @Value("${lee.expiration:10}")
    private Integer expiration;
    @Autowired
    private WxPayService wxPayService;
    @Autowired
    private TBOrderMapper tbOrderMapper;
    @Autowired
    private TBGoodsMapper tbGoodsMapper;

    /**
     * 充值 - 下单
     */
    @PostMapping("/native")
    public Map nativePay(@RequestBody Long goodsId) {

        // 查询商品;
        TBGoods tbGoods = tbGoodsMapper.getById(goodsId);
        //  根据请求获取登陆人;
        String accountId = ServletUtils.getParameter("accountId");

        // 微信支付
        TBOrder tbOrder = new TBOrder();
        tbOrder.setAccountId(accountId);

        boolean b = wxPayService.nativePay(tbGoods, tbOrder);
        if (!b) {
            // todo 下单失败:更新订单

            return AjaxResult.error("下单失败!");
        }

        // 返回参数
        String amountRes = tbOrder.toString();
        TBOrder res = TBOrder.builder()
                .outTradeNo(tbOrder.getOutTradeNo())
                .codeUrl(tbOrder.getCodeUrl())
                .amount(amountRes)
                .build();
        AjaxResult ajax = AjaxResult.success(res);
        // 过期时间
        ajax.put("expirationTime", DateUtil.format(DateUtil.offsetMinute(tbOrder.getCreateTime(), expiration), DatePattern.NORM_DATETIME_PATTERN));
        return ajax;
    }

    /**
     * 支付二维码生成后,前台持续调用接口,获取订单状态
     */
    @GetMapping("/order/{outTradeNo}")
    public AjaxResult remove(@PathVariable(value = "outTradeNo") String outTradeNo) {
        TBOrder order = tbOrderMapper.getByOutTradeNo(outTradeNo);
        return AjaxResult.success(ObjectUtil.isNotNull(order));
    }
}
2)WxPayService.java
import com.lee.domain.TBGoods;
import com.lee.domain.TBOrder;

/**
 * @author lee
 * @date 2023-05-27
 * 微信充值 业务层
 */
public interface WxPayService {

    /**
     * 下单
     *
     * @param tbGoods
     * @param tbOrder
     * @return
     */
    boolean nativePay(TBGoods tbGoods, TBOrder tbOrder);
}
3)WxPayServiceImpl.java
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.lee.config.RedisCache;
import com.lee.config.WxPayConfig;
import com.lee.constant.PayConstants;
import com.lee.domain.TBGoods;
import com.lee.domain.TBOrder;
import com.lee.entity.WxPay;
import com.lee.exception.WxPayException;
import com.lee.mapper.TBOrderMapper;
import com.lee.utils.WxPayUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.concurrent.TimeUnit;

/**
 * @author lee
 * @date 2023-05-27
 * 微信充值 服务层处理
 */
@Slf4j
@Service
public class WxPayServiceImpl implements WxPayService {

    @Value("${lee.expiration:10}")
    private Integer expiration;

    @Autowired
    private RedisCache redisCache;

    @Autowired
    private TBOrderMapper tbOrderMapper;

    /**
     * 下单
     *
     * @param tbGoods
     * @param tbOrder
     * @return
     */
    @Transactional
    @Override
    public boolean nativePay(TBGoods tbGoods, TBOrder tbOrder) {
        // 充值金额: 分
        BigDecimal amount = new BigDecimal(tbGoods.getAmount());
        // 商品描述
        String description = StrUtil.format("商品:{},价格:{}", tbGoods.getName(), amount.toString());
        // 商户订单号
        String outTradeNo = String.valueOf(IdUtil.getSnowflake(1, 20).nextId());
        // 回调参数
        String attach = StrUtil.format("{\"attach\":\"{}\"}", "attach");
        log.info("回调携带参数 ===> {}", attach);

        // 请求body参数
        WxPay wxPay = new WxPay();
        // 应用ID、直连商户号、商品描述、商品订单号、回调地址、金额
        wxPay.setAppid(WxPayConfig.getAppid())
                .setMchid(WxPayConfig.getMchId())
                .setDescription(description)
                .setOutTradeNo(outTradeNo)
                .setNotifyUrl(WxPayConfig.getNotifyDomain())
                .setAttach(attach)
                .setAmount(WxPay.Amount.builder().total(amount.longValue()).build());
        //将参数转换成json字符串
        String jsonParams = JSON.toJSONString(wxPay);
        log.info("调用微信支付下单功能的参数 ===> {}" + jsonParams);
        try {
            // Native下单API
            String codeUrl = WxPayUtils.nativePay(jsonParams);
            // 支付二维码连接
            tbOrder.setCodeUrl(codeUrl);
        } catch (WxPayException e) {
            log.error("微信支付下单的失败: {}", e.fillInStackTrace());
            return false;
        }

        // 生成订单: 订单编号
        tbOrder.setOutTradeNo(outTradeNo)
                // 总金额(小写)、总金额(大写)
                .setAmount(amount.toString())
                .setAmountCapital(Convert.digitToChinese(amount.movePointLeft(2)))
                // 商品名称、支付方式、订单状态
                .setDescription(description)
                .setPaymentMethod(PayConstants.PAYMENT_METHOD_WX)
                .setPaymentStatus(PayConstants.PAYMENT_STATUS_UN)
                .setPhoneNo("")
                // 创建时间、更新事时间
                .setCreateTime(DateUtil.date())
                .setUpdateTime(DateUtil.date());
        int insert = tbOrderMapper.createOrder(tbOrder);
        if (insert > 0) {
            // 过期时间缓存 redis
            String redisKey = StrUtil.format(PayConstants.TB_ORDER_KEY, tbOrder.getOutTradeNo());
            redisCache.setCacheObject(redisKey, expiration, expiration, TimeUnit.MINUTES);
        }
        return true;
    }
}
4)WxPayUtils.java
import com.alibaba.fastjson2.JSON;
import com.lee.config.WxPayConfig;
import com.lee.enums.InterfaceURL;
import com.lee.enums.ReturnStatus;
import com.lee.exception.WxPayException;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.http.MediaType;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

/**
 * @author lee
 * @date 2023-05-27
 * 微信支付
 */
@Slf4j
public class WxPayUtils {
    /**
     * {@link WxPayConfig#getWxPayClient 名字与@Bean注册的名字保持一致}
     */
    public static final String CLIENT = "wxPayClient";

    private static CloseableHttpClient wxPayClient = SpringUtils.getBean(CLIENT);

    /**
     * Native 微信支付下单接口
     *
     * @param param Native 下单参数
     * @return 支付二维码
     */
    public static String nativePay(String param) {
        StringEntity entity = new StringEntity(param, StandardCharsets.UTF_8);
        entity.setContentType(MediaType.APPLICATION_JSON_VALUE);

        HttpPost httpPost = new HttpPost(WxPayConfig.getDomain().concat(InterfaceURL.NATIVE_PAY.getUrl()));
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", MediaType.APPLICATION_JSON_VALUE);
        httpPost.setHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE);

        try (
                // 完成签名并执行请求 - 无需手动关流
                CloseableHttpResponse response = wxPayClient.execute(httpPost)
        ) {
            // 响应状态码
            int statusCode = response.getStatusLine().getStatusCode();
            // 响应体
            String bodyAsString = EntityUtils.toString(response.getEntity());

            // 响应码校验
            if (!ReturnStatus.S200.getStatus().equals(statusCode)) {
                log.info("Native 下单失败, 响应码 = " + statusCode + ", 返回结果 = " + bodyAsString);
                throw new WxPayException(statusCode, bodyAsString);
            }
            // 响应结果
            Map<String, String> resultMap = JSON.parseObject(bodyAsString, HashMap.class);
            // 二维码连接
            return resultMap.get("code_url");
        } catch (IOException e) {
            log.error("Native 下单失败: {}", e.fillInStackTrace());
        }
        return "";
    }
}

2、支付回调

(1)调用流程

(2)回调参数

本步骤为插件封装

<!-- 微信支付 -->
<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-apache-httpclient</artifactId>
    <version>0.4.8</version>
</dependency>
1)回调参数
参数名 参数含义 参数类型 是否必传 备注
Header Wechatpay-Serial 序列号 String 必填 验签的微信支付平台证书序列号/微信支付公钥ID
Wechatpay-Signature 签名值 String 必填 验签的签名值
Wechatpay-Timestamp 验签时间戳 String 必填 验签的时间戳
Wechatpay-Nonce 验签随机字符串 String 必填 验签的随机字符串
body id 【通知ID】 string(36) 必填 回调通知的唯一编号
create_time 【通知创建时间】 string(32) 必填 本次回调通知创建的时间
event_type 【通知的类型】 string(32) 必填 微信支付回调通知的类型
resource_type 【通知数据类型****】 string(32) 必填 固定为encrypt-resource
summary 【回调摘要】 string(64) 必填 微信支付对回调内容的摘要备注
resource 【通知数据】 object 必填 通知资源数据
resource algorithm 【加密算法类型】 string(32) 必填 回调数据密文的加密算法类型,目前为AEAD_AES_256_GCM
ciphertext 【数据密文】 string(1,1048576) 必填 Base64编码后的回调数据密文,商户需Base64解码并使用APIV3密钥解密,具体参考如何解密证书和回调报文
associated_data 【附加数据】 string(16) 选填 参与解密的附加数据,该字段可能为空
original_type 【原始回调类型】 string(16) 必填 加密前的对象类型,为transaction
nonce 【随机串】 string(16) 必填 参与解密的随机串

注:查看代码,数据解码无需自己参与

2)解码后的数据格式
参数名 参数含义 参数类型 是否必输 备注
appid 【公众账号ID】 string(32) 必填 商户下单时传入的公众账号ID
mchid 【商户号】 string(32) 必填 返回信息,回调接收失败原因
out_trade_no 【商户订单号】 string(32) 必填 商户下单时传入的商户号
transaction_id 【微信支付订单号】 string(32) 必填 微信支付侧订单的唯一标识
trade_type 【交易类型】 string(16) 必填 NATIVE:Native支付
trade_state 【交易状态】 string(32) 必填 + SUCCESS:支付成功
+ REFUND:转入退款
+ NOTPAY:未支付
+ CLOSED:已关闭
+ REVOKED:已撤销(仅付款码支付会返回)
+ USERPAYING:用户支付中(仅付款码支付会返回)
+ PAYERROR:支付失败(仅付款码支付会返回)
trade_state_desc 【交易状态描述】 string(256) 必填 对交易状态的详细说明
bank_type 【银行类型】 string(32) 必填 用户支付方式说明,订单支付成功后返回
attach 【商户数据包】 string(128) 选填 商户下单时传入的自定义数据包
success_time 【支付完成时间】 string(64) 必填 用户完成订单支付的时间
payer 【支付者信息】 object 必填 订单的支付者信息
amount 【订单金额】 object 必填 订单金额信息
scene_info 【场景信息】 object 选填 若下单传入该参数,则原样返回
promotion_detail 【优惠功能】 array 选填 代金券信息

字段具体情况,自行查看支付成功回调通知

(3)回调应答数据

参数名 参数含义 参数类型 是否必输 备注
code 【返回状态码】 string(32) 选填 200
message 【返回信息】 string(256) 选填 返回信息,回调接收失败原因

响应规则

  • 若商户应答回调接收成功,微信支付将不再重复发送该回调通知。若因网络或其他原因,商户收到了重复的回调通知,请按正常业务流程进行处理并应答。
  • 若商户应答回调接收失败,或超时(5s)未应答时,微信支付会按照(15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h)的频次重复发送回调通知,直至微信支付接收到商户应答成功,或达到最大发送次数(15次)

(4)代码

1)WxPayController.java
import cn.hutool.core.lang.Pair;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.lee.entity.DecryptNotifyResult;
import com.lee.exception.UtilException;
import com.lee.service.WxPayService;
import com.lee.utils.ServletUtils;
import com.lee.utils.WxPayUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;

/**
 * @author lee
 * @date 2023/6/15
 * Native 微信支付回调接口
 */
@Slf4j
@RestController
@RequestMapping("/wx")
public class WxPayController {

    @Autowired
    private WxPayService wxPayService;

    /**
     * 支付回调
     */
    @PostMapping("/notify")
    public String nativeNotify() {
        HttpServletRequest req = ServletUtils.getRequest();
        log.info("调用方: {}", ip(req));

        JSONObject resJSON = new JSONObject();
        resJSON.put("code", "FAIL");

        // 请求头 Wechatpay-Nonce、Wechatpay-Timestamp、Wechatpay-Signature
        final String nonce = req.getHeader("Wechatpay-Nonce");
        final String timestamp = req.getHeader("Wechatpay-Timestamp");
        final String signature = req.getHeader("Wechatpay-Signature");
        final String serialNumber = req.getHeader("Wechatpay-Serial");

        final String body = getReqParamFromBody(req);
        log.error("请求体: {}", body);
        if (ObjectUtil.isNull(body)) {
            return resJSON.toString();
        }

        // 解密
        DecryptNotifyResult result = WxPayUtils.decryptData(serialNumber, nonce, timestamp, signature, body);
        if (ObjectUtil.isNull(result)) {
            log.error("密文解析失败: {}", result);
            return resJSON.toString();
        }
        log.info("解析完成: result={} ", JSON.toJSONString(result));
        Pair<Boolean, String> pair = wxPayService.nativeNotify(result);

        if (pair.getKey()) {
            resJSON.put("code", "SUCCESS");
        }
        resJSON.put("message", pair.getValue());

        return resJSON.toString();
    }

    /**
     * JSON 格式通过请求主体(BODY)传输  获取参数
     **/
    private String getReqParamFromBody(HttpServletRequest req) {
        StringBuffer param = new StringBuffer();
        if (isConvertJSON()) {
            try {
                String str;
                BufferedReader br = req.getReader();
                while ((str = br.readLine()) != null) {
                    param.append(str);
                }
                return param.toString();
            } catch (Exception e) {
                log.error("请求参数转换异常! params=[{}]", param.toString());
                throw new UtilException("转换异常", e);
            }
        } else {
            return param.toString();
        }
    }

    private String ip(HttpServletRequest request) {
        String ip = null;
        //X-Forwarded-For:Squid 服务代理
        String ipAddresses = request.getHeader("X-Forwarded-For");
        if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {
            //Proxy-Client-IP:apache 服务代理
            ipAddresses = request.getHeader("Proxy-Client-IP");
        }
        if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {
            //WL-Proxy-Client-IP:weblogic 服务代理
            ipAddresses = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {
            //HTTP_CLIENT_IP:有些代理服务器
            ipAddresses = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {
            //X-Real-IP:nginx服务代理
            ipAddresses = request.getHeader("X-Real-IP");
        }
        //有些网络通过多层代理,那么获取到的ip就会有多个,一般都是通过逗号(,)分割开来,并且第一个ip为客户端的真实IP
        if (ipAddresses != null && ipAddresses.length() != 0) {
            ip = ipAddresses.split(",")[0];
        }
        //还是不能获取到,最后再通过request.getRemoteAddr();获取
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }

    /**
     * 判断请求参数是否转换为json格式
     */
    private boolean isConvertJSON() {
        String contentType = ServletUtils.getRequest().getContentType();
        // 有 contentType && json格式, get请求不转换
        if (ObjectUtil.isNotNull(contentType)
                && contentType.toLowerCase().indexOf(MediaType.APPLICATION_JSON_VALUE) >= 0
                && !ServletUtils.getRequest().getMethod().equalsIgnoreCase("GET")
        ) { // application/json 需要转换为 json 格式;
            return true;
        }
        return false;
    }
}
2)WxPayUtils.java
import com.alibaba.fastjson2.JSON;
import com.lee.config.WxPayConfig;
import com.lee.entity.DecryptNotifyResult;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.exception.ParseException;
import com.wechat.pay.contrib.apache.httpclient.exception.ValidationException;
import com.wechat.pay.contrib.apache.httpclient.notification.Notification;
import com.wechat.pay.contrib.apache.httpclient.notification.NotificationHandler;
import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest;
import lombok.extern.slf4j.Slf4j;

import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;

/**
 * @author lee
 * @date 2023-05-27
 * 微信支付
 */
@Slf4j
public class WxPayUtils {
    /**
     * @param serialNumber
     * @param nonce
     * @param timestamp
     * @param signature
     * @param body
     * @return
     */
    public static DecryptNotifyResult decryptData(String serialNumber, String nonce, String timestamp, String signature, String body) {
        // 转码
        try {
            body = new String(body.getBytes("GBK"), StandardCharsets.UTF_8);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        log.info("serialNumber: {}; nonce: {}; timestamp: {}; signature: {}", serialNumber, nonce, timestamp, signature);

        // 构建request, 传入必要参数
        NotificationRequest request = new NotificationRequest.Builder().withSerialNumber(serialNumber)
                .withNonce(nonce)
                .withTimestamp(timestamp)
                .withSignature(signature)
                .withBody(body)
                .build();

        Verifier verifier = SpringUtils.getBean(Verifier.class);
//        log.info("verifier: {}", JSON.toJSONString(verifier));
        NotificationHandler handler = new NotificationHandler(verifier, WxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        String decryptData = null;
        // 验签和解析请求体
        try {
            Notification notification = handler.parse(request);
            log.info("--------------> notification: {}", JSON.toJSONString(notification));
            // 从 notification 中获取解密报文
            decryptData = notification.getDecryptData();
            log.info("解析后的数据 notification.getDecryptData(): {}", decryptData);
        } catch (ValidationException e) {
            log.info("Native 回调解密失败: {}", e.getMessage());
        } catch (ParseException e) {
            log.info("Native 回调解密失败: {}", e.getMessage());
        }
        // 转换为对象
        return JSON.parseObject(decryptData, DecryptNotifyResult.class);
    }
}
3)WxPayService.java
import cn.hutool.core.lang.Pair;
import com.lee.entity.DecryptNotifyResult;

/**
 * @author lee
 * @date 2023-05-27
 * 微信充值 业务层
 */
public interface WxPayService {

    /**
     * 支付回调
     *
     * @param result
     * @return
     */
    Pair<Boolean, String> nativeNotify(DecryptNotifyResult result);
}
4)WxPayServiceImpl.java
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Pair;
import cn.hutool.core.lang.mutable.MutablePair;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.lee.config.RedisCache;
import com.lee.constant.PayConstants;
import com.lee.domain.TBOrder;
import com.lee.entity.DecryptNotifyResult;
import com.lee.mapper.TBOrderMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

/**
 * @author lee
 * @date 2023-05-27
 * 微信充值 服务层处理
 */
@Slf4j
@Service
public class WxPayServiceImpl implements WxPayService {

    @Value("${lee.expiration:10}")
    private Integer expiration;

    @Autowired
    private RedisCache redisCache;

    @Autowired
    private TBOrderMapper tbOrderMapper;

    /**
     * 支付回调
     *
     * @param result
     * @return
     */
    @Override
    public Pair<Boolean, String> nativeNotify(DecryptNotifyResult result) {
        // 查询现有订单
        TBOrder tbOrder = tbOrderMapper.getOrderByOutTradeNo(result.getOutTradeNo());
        log.info("订单. tbOrder={} ", JSON.toJSONString(tbOrder));
        if (ObjectUtil.isNull(tbOrder)) {
            return MutablePair.of(false, StrUtil.format("订单不存在: {}", result.getOutTradeNo()));
        }
        // 核对金额
        Integer totalFee = result.getAmount().getTotal();
        long wxPayAmt = new BigDecimal(totalFee).longValue();
        long payAmt = Long.valueOf(tbOrder.getAmount());
        if (payAmt != wxPayAmt) {
            log.info("金额不一致!支付金额: {}分, 下单金额: {}分", wxPayAmt, payAmt);
            return MutablePair.of(false, StrUtil.format("金额不一致!支付金额: {}分, 下单金额: {}分", wxPayAmt, payAmt));
        }

        String channelState = result.getTradeState();
        if ("SUCCESS".equals(channelState)) {
            tbOrder.setPaymentStatus(PayConstants.PAYMENT_STATUS_SUCC);
        } else
            // CLOSED - 已关闭, REVOKED - 已撤销, PAYERROR - 支付失败
            if ("CLOSED".equals(channelState)
                    || "REVOKED".equals(channelState)
                    || "PAYERROR".equals(channelState)) {
                // 支付失败
                tbOrder.setPaymentStatus(PayConstants.PAYMENT_STATUS_ERR);
            }

        // 渠道订单号、支付时间、返回内容
        tbOrder.setTransactionId(result.getTransactionId())
                .setPayTime(DateUtil.parseDateTime(result.getSuccessTime()))
                .setResource(JSON.toJSONString(result));

        DecryptNotifyResult.Payer payer = result.getPayer();
        if (ObjectUtil.isNotNull(payer)) {
            // 支付用户ID
            tbOrder.setPayer(payer.getOpenid());
        }

        // 更新订单
        int update = tbOrderMapper.update(tbOrder);
        if (update > 0) {
            // 删除 redis 缓存
            String redisKey = StrUtil.format(PayConstants.TB_ORDER_KEY, tbOrder.getOutTradeNo());
            redisCache.deleteObject(redisKey);
        }
        log.info("订单支付成功: {}", result.getOutTradeNo());
        return MutablePair.of(true, StrUtil.format("订单支付成功: {}", result.getOutTradeNo()));
    }
}

3、关闭订单

(1)调用流程

(2)调用参数

url:/v3/pay/transactions/out-trade-no/{out_trade_no}/close

参数名 参数含义 参数类型 是否必输 备注
Header Authorization 签名认证 string 必填 代码示例时间:2023/6/15
Accept 请求类型 string 必填 application/json
**Content-Type ** 响应类型 string 必填 application/json
path out_trade_no 【商户订单号】 string(32) 必填 商户下单时传入的商户系统内部订单号
body mchid 【商户号】 string(32) 必填 是由微信支付系统生成并分配给每个商户的唯一标识符
  • 关于 Authorization 的解释:根据 签名认证 的说明;只有 JSAPI、APP、小程序调起支付时,才需要支付签名
  • 其他选填非必传参数自行查看 关闭订单
  • 加粗参数为我使用到的

(3)应答数据

(4)代码

1)RedisKeyExpiredListener.java
import cn.hutool.core.date.DateUtil;
import com.lee.constant.PayConstants;
import com.lee.domain.TBOrder;
import com.lee.mapper.TBOrderMapper;
import com.lee.utils.WxPayUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;

@Slf4j
@Component
public class RedisKeyExpiredListener extends KeyExpirationEventMessageListener {
    @Value("${lee.expiration:10}")
    private Integer expiration;

    @Autowired
    TBOrderMapper tbOrderMapper;

    public RedisKeyExpiredListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    @Override
    public void onMessage(Message message, byte[] pattern) {
        String key = new String(message.getBody(), StandardCharsets.UTF_8);
        log.info("redis expired key: {}", key);
        // 获取 获取数据库 0; 订单id 1
        String[] info = key.split(":");
        TBOrder tbOrder = tbOrderMapper.getOrderByOutTradeNo(info[2]);
        if (PayConstants.DELETED.equals(tbOrder.getUserDelFlag())) {
            log.info("订单:{} 已被删除.", tbOrder.getOutTradeNo());
        }

        if (PayConstants.PAYMENT_STATUS_SUCC.equals(tbOrder.getPaymentStatus())) {
            log.info("订单:{} 已支付.", tbOrder.getOutTradeNo());
        }

        // 修改状态 和 订单过期时间
        tbOrder.setPaymentStatus(PayConstants.PAYMENT_STATUS_EXP);
        tbOrder.setExpirationTime(DateUtil.offsetMinute(tbOrder.getCreateTime(), expiration));
        if (tbOrderMapper.update(tbOrder) <= 0) {
            log.info("订单状态修改失败: {}", tbOrder.getPaymentStatus());
        }

        // 关单
        WxPayUtils.closePay(tbOrder.getOutTradeNo());

        log.info("订单 {} 过期未支付!.", tbOrder.getOutTradeNo());
    }
}
2)WxPayUtils.java
import cn.hutool.core.util.StrUtil;
import com.lee.config.WxPayConfig;
import com.lee.enums.InterfaceURL;
import com.lee.enums.ReturnStatus;
import com.lee.exception.WxPayException;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.http.MediaType;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;

/**
 * @author lee
 * @date 2023-05-27
 * 微信支付
 */
@Slf4j
public class WxPayUtils {
    /**
     * {@link WxPayConfig#getWxPayClient 名字与@Bean注册的名字保持一致}
     */
    public static final String CLIENT = "wxPayClient";

    private static CloseableHttpClient wxPayClient = SpringUtils.getBean(CLIENT);

    /**
     * Native 微信支付关单接口
     *
     * @param outTradeNo Native 关单参数
     * @return 无应答包体
     */
    public static String closePay(String outTradeNo) {
        StringEntity entity = new StringEntity("{\"mchid\":\"" + WxPayConfig.getMchId() + "\"}", StandardCharsets.UTF_8);
        entity.setContentType(MediaType.APPLICATION_JSON_VALUE);

        HttpPost httpPost = new HttpPost(WxPayConfig.getDomain().concat(StrUtil.format(InterfaceURL.CLOSE_ORDER_BY_NO.getUrl(), outTradeNo)));
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", MediaType.APPLICATION_JSON_VALUE);
        httpPost.setHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE);

        try (
                // 完成签名并执行请求 - 无需手动关流
                CloseableHttpResponse response = wxPayClient.execute(httpPost)
        ) {
            // 响应状态码
            int statusCode = response.getStatusLine().getStatusCode();

            // 响应体
            if (Objects.isNull(response.getEntity())) {
                return "";
            }
            String bodyAsString = EntityUtils.toString(response.getEntity());

            // 响应码校验
            if (!ReturnStatus.S204.getStatus().equals(statusCode)) {
                log.info("Native 关单失败, 响应码 = " + statusCode + ", 返回结果 = " + bodyAsString);
                throw new WxPayException(statusCode, bodyAsString);
            }
            return bodyAsString;
        } catch (IOException e) {
            log.error("Native 关单失败: {}", e.fillInStackTrace());
        }
        return "";
    }
}

四、完结

撒花、结束;自行测试

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐