基于java实现微信Native支付
微信支付Native支付是一种PC端网页收款解决方案。商户通过生成二维码链接(code_url)并转换为二维码展示,用户需使用微信"扫一扫"完成支付。支付流程包括订单确认、密码验证和支付方式选择,成功后可在微信账单中查看明细。基础URL为https://api.mch.weixin.qq.com。后端采用Spring Boot架构,包含配置、缓存、控制器、实体等模块,依赖包括S
·
以下文档内容,部分摘自【微信支付】产品文档
一、产品介绍
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 下 |
(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) | 必填 | 是由微信支付系统生成并分配给每个商户的唯一标识符 |
(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 "";
}
}
四、完结
撒花、结束;自行测试
更多推荐
所有评论(0)