微信支付项目实战、创建订单到支付退款代码详解

1、微信支付产品介绍

微信支付开发文档:

微信支付-开发者文档 (qq.com)

源码地址:前端后端都有

链接: https://pan.baidu.com/s/1Nx-jLJ1gaZD0rmoGOw9MhA

提取码: qwer 

1.1、付款码支付

用户展示微信钱包内的 付款码 给商家,商家扫描后直接完成支付,适用于线下面对面收银的场景。

1.2JSAPI支付

线下场所:商户展示一个支付二维码,用户使用微信扫描二维码后,输入需要支付的金额,完成支
付。
公众号场景:用户在微信内进入商家公众号,打开某个页面,选择某个产品,完成支付。
PC 网站场景:在网站中展示二维码,用户使用微信扫描二维码,输入需要支付的金额,完成支
付。
特点:用户在客户端输入支付金额

1.3、小程序支付

在微信小程序平台内实现支付的功能。

1.4Native支付

Native 支付是指商户展示支付二维码,用户再用微信 扫一扫 完成支付的模式。这种方式适用于 PC
站。
特点:商家预先指定支付金额

1.5APP支付

商户通过在移动端独立的 APP 应用程序中集成微信支付模块,完成支付。

1.6、刷脸支付

用户在刷脸设备前通过摄像头刷脸、识别身份后进行的一种支付方式。

2、接入指引

2.1、获取商户号

微信商户平台: https://pay.weixin.qq.com/
场景: Native 支付
步骤:提交资料 => 签署协议 => 获取商户号

2.2、获取APPID

APIV2、APIV3一个模式

步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API 安全 => 设置 API 密钥
随机密码生成工具: https://suijimimashengcheng.bmcx.com/

2.5、申请商户API证书

APIv3 版本的所有接口都需要; APIv2 版本的高级接口需要(如:退款、企业红包、企业付款等)
步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API 安全 => 申请 API 证书

2.6、获取微信平台证书

可以预先下载,也可以通过编程的方式获取。后面的课程中,我们会通过编程的方式来获取。
注意:以上所有 API 秘钥和证书需妥善保管防止泄露

3.微信APIv3证书

商户证书
商户 API 证书是指由商户申请的,包含商户的商户号、公司名称、公钥信息的证书。
商户证书在商户后台申请: https://pay.weixin.qq.com/index.php/core/cert/api_cert#/

平台证书(微信支付平台):
微信支付平台证书是指由 微信支付 负责申请的,包含微信支付平台标识、公钥信息的证书。商户可以使
用平台证书中的公钥进行验签。
平台证书的获取: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_0.shtml
使用微信支付商户平台证书工具进行生成
都是对称加密需要使用的加密和解密密钥,一定要保管好,不能泄露。
API 密钥对应 V2 版本的 API
APIv3 密钥对应 V3 版本的 API

 

 

项目实现

依赖

<?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>
    <packaging>jar</packaging>

    <groupId>org.example</groupId>
    <artifactId>weixinZF</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>


    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.9.RELEASE</version>
        <relativePath/>
    </parent>

    <dependencies>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.7.0</version>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.7.0</version>
        </dependency>

        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.14</version>
        </dependency>

<!--        微信支付依赖-->
        <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-apache-httpclient</artifactId>
            <version>0.3.0</version>
        </dependency>
<!--        支付宝支付依赖-->
        <dependency>
            <groupId>com.alipay.sdk</groupId>
            <artifactId>alipay-sdk-java</artifactId>
            <version>4.31.65.ALL</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.1.9.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <!--lombok 依赖,子工程中假如需要lombok,不需要再引入-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope><!--provided 表示此依赖仅在编译阶段有效-->
        </dependency>
        <!--单元测试依赖,子工程中需要单元测试时,不需要再次引入此依赖了-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <!--            <scope>test</scope>&lt;!&ndash;test表示只能在test目录下使用此依赖&ndash;&gt;-->
            <exclusions>
                <exclusion><!--排除一些不需要的依赖-->
                    <groupId>org.junit.jupiter</groupId>
                    <artifactId>junit-jupiter-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.60</version>
            <scope>compile</scope>
        </dependency>

        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>
        <!--其它依赖...-->


    </dependencies>

    <build>
        <plugins>
            <!--通过maven-compiler-plugin插件设置项目
            的统一的jdk编译和运行版本-->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.1.9.RELEASE</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>

                <configuration>
                    <includeSystemScope>true</includeSystemScope>
                    <mainClass>com.wx.WXapplication</mainClass>
                </configuration>
            </plugin>

        </plugins>
    </build>


</project>

配置yml

server:
  port: 8090

spring:
  application:
    name: weixinZF
  datasource:
    #高版本驱动使用
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/payment_demo?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
    #设定用户名和密码
    username: root
    password: root

  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

#SpringBoot整合Mybatis
mybatis-plus:
  #指定别名包
  type-aliases-package: com.jt.pojo
  #扫描指定路径下的映射文件
  mapper-locations: classpath:/mapper/*.xml
  #开启驼峰映射
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  #sql日志
    map-underscore-to-camel-case: true
  # 一二级缓存默认开始 所以可以简化
#打印mysql日志
logging:
  level:
    com.jt.mapper: debug

数据库创建

订单表

CREATE TABLE `t_order_info` (
  `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `title` varchar(256) DEFAULT NULL COMMENT '订单标题',
  `order_no` varchar(50) DEFAULT NULL COMMENT '商户订单编号',
  `user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
  `product_id` bigint(20) DEFAULT NULL COMMENT '支付产品id',
  `total_fee` int(11) DEFAULT NULL COMMENT '订单金额(分)',
  `code_url` varchar(50) DEFAULT NULL COMMENT '订单二维码连接',
  `order_status` varchar(10) DEFAULT NULL COMMENT '订单状态',
  `create_time` datetime DEFAULT current_timestamp() COMMENT '创建时间',
  `update_time` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '更新时间',
`payment_type` varchar(255) DEFAULT NULL COMMENT '支付类型(支付宝~微信)',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=39 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

记录表

CREATE TABLE `t_payment_info` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '支付记录id',
  `order_no` varchar(50) DEFAULT NULL COMMENT '商户订单编号',
  `transaction_id` varchar(50) DEFAULT NULL COMMENT '支付系统交易编号',
  `payment_type` varchar(20) DEFAULT NULL COMMENT '支付类型',
  `trade_type` varchar(20) DEFAULT NULL COMMENT '交易类型',
  `trade_state` varchar(50) DEFAULT NULL COMMENT '交易状态',
  `payer_total` int(11) DEFAULT NULL COMMENT '支付金额(分)',
  `content` text DEFAULT NULL COMMENT '通知参数',
  `create_time` datetime DEFAULT current_timestamp() COMMENT '创建时间',
  `update_time` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

商品表

CREATE TABLE `t_product` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品id',
  `title` varchar(20) DEFAULT NULL COMMENT '商品名称',
  `price` int(11) DEFAULT NULL COMMENT '价格(分)',
  `create_time` datetime DEFAULT current_timestamp() COMMENT '创建时间',
  `update_time` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

退款表

CREATE TABLE `t_refund_info` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '退款单id',
  `order_no` varchar(50) DEFAULT NULL COMMENT '商户订单编号',
  `refund_no` varchar(50) DEFAULT NULL COMMENT '商户退款单编号',
  `refund_id` varchar(50) DEFAULT NULL COMMENT '支付系统退款单号',
  `total_fee` int(11) DEFAULT NULL COMMENT '原订单金额(分)',
  `refund` int(11) DEFAULT NULL COMMENT '退款金额(分)',
  `reason` varchar(50) DEFAULT NULL COMMENT '退款原因',
  `refund_status` varchar(10) DEFAULT NULL COMMENT '退款状态',
  `content_return` text DEFAULT NULL COMMENT '申请退款返回参数',
  `content_notify` text DEFAULT NULL COMMENT '退款结果通知参数',
  `create_time` datetime DEFAULT current_timestamp() COMMENT '创建时间',
  `update_time` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

配置微信相关支付参数

wxpay.properties

# 微信支付相关参数
# 商户号
wxpay.mch-id=11111111
# 商户API证书序列号
wxpay.mch-serial-no=1111111111111111
# 商户私钥文件
wxpay.private-key-path=apiclient_key.pem
# APIv3密钥
wxpay.api-v3-key=111111111111
# APPID
wxpay.appid=111111111111111
# 微信服务器地址
wxpay.domain=https://api.mch.weixin.qq.com
# 接收结果通知地址 使用内网穿透工具获取
wxpay.notify-domain=http://pw46ia.natappfree.cc

将私钥放置项目下

配置Util工具类

http请求客户端工具类

package com.wx.util;

import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.*;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * http请求客户端
 */
public class HttpClientUtils {
	private String url;
	private Map<String, String> param;
	private int statusCode;
	private String content;
	private String xmlParam;
	private boolean isHttps;

	public boolean isHttps() {
		return isHttps;
	}

	public void setHttps(boolean isHttps) {
		this.isHttps = isHttps;
	}

	public String getXmlParam() {
		return xmlParam;
	}

	public void setXmlParam(String xmlParam) {
		this.xmlParam = xmlParam;
	}

	public HttpClientUtils(String url, Map<String, String> param) {
		this.url = url;
		this.param = param;
	}

	public HttpClientUtils(String url) {
		this.url = url;
	}

	public void setParameter(Map<String, String> map) {
		param = map;
	}

	public void addParameter(String key, String value) {
		if (param == null)
			param = new HashMap<String, String>();
		param.put(key, value);
	}

	public void post() throws ClientProtocolException, IOException {
		HttpPost http = new HttpPost(url);
		setEntity(http);
		execute(http);
	}

	public void put() throws ClientProtocolException, IOException {
		HttpPut http = new HttpPut(url);
		setEntity(http);
		execute(http);
	}

	public void get() throws ClientProtocolException, IOException {
		if (param != null) {
			StringBuilder url = new StringBuilder(this.url);
			boolean isFirst = true;
			for (String key : param.keySet()) {
				if (isFirst) {
					url.append("?");
					isFirst = false;
				}else {
					url.append("&");
				}
				url.append(key).append("=").append(param.get(key));
			}
			this.url = url.toString();
		}
		HttpGet http = new HttpGet(url);
		execute(http);
	}

	/**
	 * set http post,put param
	 */
	private void setEntity(HttpEntityEnclosingRequestBase http) {
		if (param != null) {
			List<NameValuePair> nvps = new LinkedList<NameValuePair>();
			for (String key : param.keySet())
				nvps.add(new BasicNameValuePair(key, param.get(key))); // 参数
			http.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); // 设置参数
		}
		if (xmlParam != null) {
			http.setEntity(new StringEntity(xmlParam, Consts.UTF_8));
		}
	}

	private void execute(HttpUriRequest http) throws ClientProtocolException,
			IOException {
		CloseableHttpClient httpClient = null;
		try {
			if (isHttps) {
				SSLContext sslContext = new SSLContextBuilder()
						.loadTrustMaterial(null, new TrustStrategy() {
							// 信任所有
							public boolean isTrusted(X509Certificate[] chain,
									String authType)
									throws CertificateException {
								return true;
							}
						}).build();
				SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
						sslContext);
				httpClient = HttpClients.custom().setSSLSocketFactory(sslsf)
						.build();
			} else {
				httpClient = HttpClients.createDefault();
			}
			CloseableHttpResponse response = httpClient.execute(http);
			try {
				if (response != null) {
					if (response.getStatusLine() != null)
						statusCode = response.getStatusLine().getStatusCode();
					HttpEntity entity = response.getEntity();
					// 响应内容
					content = EntityUtils.toString(entity, Consts.UTF_8);
				}
			} finally {
				response.close();
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			httpClient.close();
		}
	}

	public int getStatusCode() {
		return statusCode;
	}

	public String getContent() throws ParseException, IOException {
		return content;
	}

}

参数转换字符串工具类

package com.wx.util;

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


public class HttpUtils {

    /**
     * 将通知参数转化为字符串
     * @param request
     * @return
     */
    public static String readData(HttpServletRequest request) {
        BufferedReader br = null;
        try {
            StringBuilder result = new StringBuilder();
            br = request.getReader();
            for (String line; (line = br.readLine()) != null; ) {
                if (result.length() > 0) {
                    result.append("\n");
                }
                result.append(line);
            }
            return result.toString();
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

订单号工具类:我们需要为我们的订单生成一个编号

package com.wx.util;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;

/**
 * 订单号工具类
 *
 * @author qy
 * @since 1.0
 */
public class OrderNoUtils {

    /**
     * 获取订单编号
     * @return
     */
    public static String getOrderNo() {
        return "ORDER_" + getNo();
    }

    /**
     * 获取退款单编号
     * @return
     */
    public static String getRefundNo() {
        return "REFUND_" + getNo();
    }

    /**
     * 获取编号
     * @return
     */
    public static String getNo() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
        String newDate = sdf.format(new Date());
        String result = "";
        Random random = new Random();
        for (int i = 0; i < 3; i++) {
            result += random.nextInt(10);
        }
        return newDate + result;
    }

}

微信验签应答工具类

package com.wx.util;


import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;

import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;

/**
 * @author xy-peng
 */
public class WechatPay2ValidatorForRequest {

    protected static final Logger log = LoggerFactory.getLogger(WechatPay2ValidatorForRequest.class);
    /**
     * 应答超时时间,单位为分钟
     */
    protected static final long RESPONSE_EXPIRED_MINUTES = 5;
    protected final Verifier verifier;
    protected final String requestId;
    protected final String body;


    public WechatPay2ValidatorForRequest(Verifier verifier, String requestId, String body) {
        this.verifier = verifier;
        this.requestId = requestId;
        this.body = body;
    }

    protected static IllegalArgumentException parameterError(String message, Object... args) {
        message = String.format(message, args);
        return new IllegalArgumentException("parameter error: " + message);
    }

    protected static IllegalArgumentException verifyFail(String message, Object... args) {
        message = String.format(message, args);
        return new IllegalArgumentException("signature verify fail: " + message);
    }

    public final boolean validate(HttpServletRequest request) throws IOException {
        try {
            //处理请求参数
            validateParameters(request);

            //构造验签名串
            String message = buildMessage(request);

            String serial = request.getHeader(WECHAT_PAY_SERIAL);
            String signature = request.getHeader(WECHAT_PAY_SIGNATURE);

            //验签
            if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
                throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
                        serial, message, signature, requestId);
            }
        } catch (IllegalArgumentException e) {
            log.warn(e.getMessage());
            return false;
        }

        return true;
    }

    protected final void validateParameters(HttpServletRequest request) {

        // NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
        String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};

        String header = null;
        for (String headerName : headers) {
            header = request.getHeader(headerName);
            if (header == null) {
                throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
            }
        }

        //判断请求是否过期
        String timestampStr = header;
        try {
            Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
            // 拒绝过期请求
            if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
                throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
            }
        } catch (DateTimeException | NumberFormatException e) {
            throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
        }
    }

    protected final String buildMessage(HttpServletRequest request) throws IOException {
        String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
        String nonce = request.getHeader(WECHAT_PAY_NONCE);
        return timestamp + "\n"
                + nonce + "\n"
                + body + "\n";
    }

    protected final String getResponseBody(CloseableHttpResponse response) throws IOException {
        HttpEntity entity = response.getEntity();
        return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
    }

}

创建统一结果R类

package com.wx.vo;

import lombok.Data;
import lombok.experimental.Accessors;

import java.util.HashMap;
import java.util.Map;

@Data
@Accessors(chain = true)
public class R {

    private Integer code; //响应码
    private String message; //响应消息
    private Map<String, Object> data = new HashMap<>();

    public static R ok(){
        R r = new R();
        r.setCode(200);
        r.setMessage("成功");
        return r;
    }

    public static R error(){
        R r = new R();
        r.setCode(201);
        r.setMessage("失败");
        return r;
    }

    public R data(String key, Object value){
        this.data.put(key, value);
        return this;
    }

}

定义实体类

package com.wx.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;

import java.util.Date;

@Data
public class BaseEntity {

    //定义主键策略:跟随数据库的主键自增
    @TableId(value = "id", type = IdType.AUTO)
    private String id; //主键

    private Date createTime;//创建时间

    private Date updateTime;//更新时间
}
package com.wx.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
@TableName("t_order_info")
public class OrderInfo  extends BaseEntity{

    private String title;//订单标题

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

    private Long userId;//用户id

    private Long productId;//支付产品id

    private Integer totalFee;//订单金额(分)

    private String codeUrl;//订单二维码连接

    private String orderStatus;//订单状态
}
package com.wx.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
@TableName("t_payment_info")
public class PaymentInfo extends BaseEntity{

    private String orderNo;//商品订单编号

    private String transactionId;//支付系统交易编号

    private String paymentType;//支付类型

    private String tradeType;//交易类型

    private String tradeState;//交易状态

    private Integer payerTotal;//支付金额(分)

    private String content;//通知参数
}
package com.wx.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
@TableName("t_product")
public class Product extends BaseEntity{

    private String title; //商品名称

    private Integer price; //价格(分)
}
package com.wx.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
@TableName("t_refund_info")
public class RefundInfo extends BaseEntity{

    private String orderNo;//商品订单编号

    private String refundNo;//退款单编号

    private String refundId;//支付系统退款单号

    private Integer totalFee;//原订单金额(分)

    private Integer refund;//退款金额(分)

    private String reason;//退款原因

    private String refundStatus;//退款单状态

    private String contentReturn;//申请退款返回参数

    private String contentNotify;//退款结果通知参数
}

定义枚举

为了开发方便,我们预先在项目中定义一些枚举。枚举中定义的内容包括接口地址,支付状态等信息

API接口地址,封装了微信支付的所有接口

package com.wx.enums.wxpay;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * api接口地址
 */
@AllArgsConstructor
@Getter
public enum WxApiType {

	/**
	 * Native下单
	 */
	NATIVE_PAY("/v3/pay/transactions/native"),

	/**
	 * Native下单
	 */
	NATIVE_PAY_V2("/pay/unifiedorder"),

	/**
	 * 查询订单
	 */
	ORDER_QUERY_BY_NO("/v3/pay/transactions/out-trade-no/%s"),

	/**
	 * 关闭订单
	 */
	CLOSE_ORDER_BY_NO("/v3/pay/transactions/out-trade-no/%s/close"),

	/**
	 * 申请退款
	 */
	DOMESTIC_REFUNDS("/v3/refund/domestic/refunds"),

	/**
	 * 查询单笔退款
	 */
	DOMESTIC_REFUNDS_QUERY("/v3/refund/domestic/refunds/%s"),

	/**
	 * 申请交易账单
	 */
	TRADE_BILLS("/v3/bill/tradebill"),

	/**
	 * 申请资金账单
	 */
	FUND_FLOW_BILLS("/v3/bill/fundflowbill");


	/**
	 * 类型
	 */
	private final String type;
}

封装了通知接口地址

package com.wx.enums.wxpay;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 封装了通知接口地址
 */
@AllArgsConstructor
@Getter
public enum WxNotifyType {

	/**
	 * 支付通知
	 */
	NATIVE_NOTIFY("/api/wx-pay/native/notify"),

	/**
	 * 支付通知
	 */
	NATIVE_NOTIFY_V2("/api/wx-pay-v2/native/notify"),


	/**
	 * 退款结果通知
	 */
	REFUND_NOTIFY("/api/wx-pay/refunds/notify");

	/**
	 * 类型
	 */
	private final String type;
}

退款类型

package com.wx.enums.wxpay;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 退款
 */
@AllArgsConstructor
@Getter
public enum WxRefundStatus {

    /**
     * 退款成功
     */
    SUCCESS("SUCCESS"),

    /**
     * 退款关闭
     */
    CLOSED("CLOSED"),

    /**
     * 退款处理中
     */
    PROCESSING("PROCESSING"),

    /**
     * 退款异常
     */
    ABNORMAL("ABNORMAL");

    /**
     * 类型
     */
    private final String type;
}

支付类型

package com.wx.enums.wxpay;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 支付订单状态
 */
@AllArgsConstructor
@Getter
public enum WxTradeState {

    /**
     * 支付成功
     */
    SUCCESS("SUCCESS"),

    /**
     * 未支付
     */
    NOTPAY("NOTPAY"),

    /**
     * 已关闭
     */
    CLOSED("CLOSED"),

    /**
     * 转入退款
     */
    REFUND("REFUND");

    /**
     * 类型
     */
    private final String type;
}

支付状态

package com.wx.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum OrderStatus {
    /**
     * 未支付
     */
    NOTPAY("未支付"),


    /**
     * 支付成功
     */
    SUCCESS("支付成功"),

    /**
     * 已关闭
     */
    CLOSED("超时已关闭"),

    /**
     * 已取消
     */
    CANCEL("用户已取消"),

    /**
     * 退款中
     */
    REFUND_PROCESSING("退款中"),

    /**
     * 已退款
     */
    REFUND_SUCCESS("已退款"),

    /**
     * 退款异常
     */
    REFUND_ABNORMAL("退款异常");

    /**
     * 类型
     */
    private final String type;
}

付款类型

package com.wx.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum PayType {
    /**
     * 微信
     */
    WXPAY("微信"),


    /**
     * 支付宝
     */
    ALIPAY("支付宝");

    /**
     * 类型
     */
    private final String type;
}

定义MyBatis-Plus的配置文件

confifig 包中创建配置文件 MybatisPlusConfifig
package com.wx.config;

import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.annotation.MapperScans;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@MapperScan("com.wx.mapper")
@EnableTransactionManagement
public class MybatisPlusConfig {
}

定义Mapper层

继承BaseMapper<>

定义业务层

定义业务层接口继承 IService<>
定义业务层接口的实现类,并继承 ServiceImpl<,>

以上配置完成才可进行业务实现!!!!!!!!!!!!!!!!!!!

业务代码实现

获取商品列表接口

public class ProductController 中添加一个方法
package com.wx.controller;

import com.wx.entity.Product;
import com.wx.service.ProductService;
import com.wx.vo.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.List;

@CrossOrigin
@RestController
@RequestMapping("/api/product")
@Api(tags = "商品管理")
public class ProductController {


    @Resource
    private ProductService productService;
    

    @GetMapping("/list")
    public R list(){
        List<Product> list = productService.list();
        return  R.ok().data("productList",list);
    }
}

结果:

 定义读取支付参数配置WxPayConfig

package com.wx.config;

import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.*;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;


@Configuration
@PropertySource("classpath:wxpay.properties") //读取配置文件
@ConfigurationProperties(prefix="wxpay") //读取wxpay节点
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
@Slf4j
public class WxPayConfig {

    // 商户号
    private String mchId;

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

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

    // APIv3密钥
    private String apiV3Key;

    // APPID
    private String appid;

    // 微信服务器地址
    private String domain;

    // 接收结果通知地址
    private String notifyDomain;

    // APIv2密钥
    private String partnerKey;

    /**
     * 获取商户的私钥文件
     * @param filename
     * @return
     */
    private PrivateKey getPrivateKey(String filename){

        try {
            return PemUtil.loadPrivateKey(new FileInputStream(filename));
        } catch (FileNotFoundException e) {
            throw new RuntimeException("私钥文件不存在", e);
        }
    }

    /**
     * 获取签名验证器.定时更新签名证书
     * @return
     */
    @Bean
    public ScheduledUpdateCertificatesVerifier getVerifier(){

        log.info("获取签名验证器");

        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(privateKeyPath);

        //私钥签名对象
        PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);

        //身份认证对象
        WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);

        // 使用定时更新的签名验证器,不需要传入证书
        ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
                wechatPay2Credentials,
                apiV3Key.getBytes(StandardCharsets.UTF_8));//商户对称加密的秘钥

        return verifier;
    }


    /**
     * 获取http请求对象
     * @param verifier
     * @return
     */
    @Bean(name = "wxPayClient")
    public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier){

        log.info("获取httpClient");

        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(privateKeyPath);

        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant(mchId, mchSerialNo, privateKey)
                .withValidator(new WechatPay2Validator(verifier));
        // ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient

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

        return httpClient;
    }

    /**
     * 获取HttpClient,无需进行应答签名验证,跳过验签的流程
     */
    @Bean(name = "wxPayNoSignClient")
    public CloseableHttpClient getWxPayNoSignClient(){

        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(privateKeyPath);

        //用于构造HttpClient
        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                //设置商户信息
                .withMerchant(mchId, mchSerialNo, privateKey)
                //无需进行签名验证、通过withValidator((response) -> true)实现
                .withValidator((response) -> true);

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

        log.info("== getWxPayNoSignClient END ==");

        return httpClient;
    }

}

测试获取支付参数:

import com.wx.config.WxPayConfig;
import com.wx.vo.R;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.security.PrivateKey;

@Api(tags = "测试控制器")
@RestController
@RequestMapping("/api/test")
public class TestController {

    @Resource
    private WxPayConfig wxPayConfig;

    @GetMapping
    public R getWxPayConfig(){

        String mchId = wxPayConfig.getMchId();
        String privateKeyPath = wxPayConfig.getPrivateKeyPath();
//        PrivateKey privateKey = wxPayConfig.getPrivateKey("apiclient_key.pem");
//        System.out.println("privateKey = " + privateKey);

        return R.ok().data("mchId",mchId).data("privateKeyPath",privateKeyPath);
    }
}

swagger测试获取

1.引入SDK(以下均已在前面配置完成。此处讲解)

前面的依赖是完整的,已经添加

https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay6_0.shtml
我们可以使用官方提供的 SDK ,帮助我们完成开发。实现了请求签名的生成和应答签名的验证。
<dependency>
 <groupId>com.github.wechatpay-apiv3</groupId> 
<artifactId>wechatpay-apache-httpclient</artifactId>
 <version>0.3.0</version> 
</dependency>

 2.获取商户私钥

https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (如何加载商户私钥)
上面测试中注释的部门就是获取商户私钥

3、获取签名验证器和HttpClient

证书密钥使用说明:上面的配置中已经建立,此处再说明一下

https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay3_0.shtml

 获取签名验证器

https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (定时更新平台证书功能)
平台证书:平台证书封装了微信的公钥,商户可以使用平台证书中的公钥进行验签。
签名验证器:帮助我们进行验签工作,我们单独将它定义出来,方便后面的开发。
获取 HttpClient 对象
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (定时更新平台证书功能)
HttpClient 对象:是建立远程连接的基础,我们通过 SDK 创建这个对象。

4API字典和相关工具

https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_3.shtml
我们的项目中要实现以下所有 API 的功能。

 接口规则

https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay2_0.shtml
微信支付 APIv3 使用 JSON 作为消息体的数据交换格式。
    <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
        </dependency>

业务功能接口代码实现

Controller

package com.wx.controller;


import com.google.gson.JsonSyntaxException;
import com.wx.service.WxPayService;
import com.wx.util.HttpUtils;
import com.wx.util.WechatPay2ValidatorForRequest;
import com.wx.vo.R;
import com.google.gson.Gson;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;


@CrossOrigin
@RestController
@RequestMapping("/api/wx-pay")
@Api(tags = "网站微信支付API")
@Slf4j
public class WxPayController {

    @Resource
    private WxPayService wxPayService;

    @Resource
    private Verifier verifier;

    @ApiOperation("调用统一下单API,生成支付二维码")
    @PostMapping("native/{productId}")
    public R nativePay(@PathVariable Long productId) throws Exception {//传递商品id
        log.info("发起支付请求");
        //返回支付二维码链接和订单号
        Map<String,Object> map = wxPayService.nativePay(productId);


        return R.ok().setData(map);
    }

    /**
     * 接收微信的通知,支付成功处理,失败处理
     * @param request
     * @param response
     * @return
     */
    @PostMapping("/native/notify")
    public String nativeNotify(HttpServletRequest request, HttpServletResponse response){

        Gson gson = new Gson();
        Map<String,String> map = new HashMap<>();//

        try {
            //处理通知参数
            String body = HttpUtils.readData(request);
            Map<String,Object> bodyMap = gson.fromJson(body, HashMap.class);
            log.info("支付通知的id =====》 {}",bodyMap.get("id"));
            log.info("支付通知的完整数据 =====》 {}",body);
            String requestId = bodyMap.get("id").toString();

            // 签名的验证  针对请求的 因为与微信交互,传递信息需要进行验证
            WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
                    = new WechatPay2ValidatorForRequest(verifier, requestId,body);
            if (!wechatPay2ValidatorForRequest.validate(request)){//判断验签是否成功
                log.error("通知验签失败");
                //失败应答
                response.setStatus(500);
                map.put("code","ERROR");
                map.put("message","通知验签失败");
                return gson.toJson(map);
            }
            log.info("通知验签成功");
            //处理订单  将具有密文数据的bodyMap进行解密获取参数。并存入数据库,存入日志
            wxPayService.processOrder(bodyMap);

            //成功应答
            response.setStatus(200);
            map.put("code","SUCCESS");
            map.put("message","成功");
            return gson.toJson(map);

        } catch (JsonSyntaxException | IOException | GeneralSecurityException e) {
            e.printStackTrace();
            //失败应答
            response.setStatus(500);
            map.put("code","ERROR");
            map.put("message","失败");
            return gson.toJson(map);
        }
    }
    @ApiOperation("用户取消订单")
    @PostMapping("/cancel/{orderNo}")
    public R cancel(@PathVariable String orderNo) throws Exception {

        log.info("取消订单");

        wxPayService.canceOrder(orderNo);
        return R.ok().setMessage("订单已经取消");
    }

    @ApiOperation("微信支付查询订单")
    @GetMapping("/query/{orderNo}")
    public R queryOrder(@PathVariable String orderNo) throws IOException {

        log.info("查询订单");
        String result =  wxPayService.queryOrder(orderNo);
        return R.ok().setMessage("查询成功").data("result",result);
    }


    @ApiOperation("申请退款")
    @PostMapping("/refunds/{orderNo}/{reason}")
    public R refunds(@PathVariable String orderNo, @PathVariable String reason) throws Exception {

        log.info("申请退款");
        wxPayService.refund(orderNo, reason);
        return R.ok();
    }


    /**
     * 查询退款
     * @param refundNo
     * @return
     * @throws Exception
     */
    @ApiOperation("查询退款")
    @GetMapping("/query-refund/{refundNo}")
    public R queryRefund(@PathVariable String refundNo) throws Exception {

        log.info("查询退款");

        String result = wxPayService.queryRefund(refundNo);
        return R.ok().setMessage("查询成功").data("result", result);
    }

    /**
     * 退款结果通知
     * 退款状态改变后,微信会把相关退款结果发送给商户。
     */
    @ApiOperation("退款结果通知")
    @PostMapping("/refunds/notify")
    public String refundsNotify(HttpServletRequest request, HttpServletResponse response){

        log.info("退款通知执行");
        Gson gson = new Gson();
        Map<String, String> map = new HashMap<>();//应答对象

        try {
            //处理通知参数
            String body = HttpUtils.readData(request);
            Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
            String requestId = (String)bodyMap.get("id");
            log.info("支付通知的id ===> {}", requestId);

            //签名的验证
            WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
                    = new WechatPay2ValidatorForRequest(verifier, requestId, body);
            if(!wechatPay2ValidatorForRequest.validate(request)){

                log.error("通知验签失败");
                //失败应答
                response.setStatus(500);
                map.put("code", "ERROR");
                map.put("message", "通知验签失败");
                return gson.toJson(map);
            }
            log.info("通知验签成功");

            //处理退款单
            wxPayService.processRefund(bodyMap);

            //成功应答
            response.setStatus(200);
            map.put("code", "SUCCESS");
            map.put("message", "成功");
            return gson.toJson(map);

        } catch (Exception e) {
            e.printStackTrace();
            //失败应答
            response.setStatus(500);
            map.put("code", "ERROR");
            map.put("message", "失败");
            return gson.toJson(map);
        }
    }
    @ApiOperation("获取账单url")
    @GetMapping("/querybill/{billDate}/{type}")
    public R queryTradeBill(
            @PathVariable String billDate,
            @PathVariable String type) throws Exception {

        log.info("获取账单url");

        String downloadUrl = wxPayService.queryBill(billDate, type);
        return R.ok().setMessage("获取账单url成功").data("downloadUrl", downloadUrl);
    }

    @ApiOperation("下载账单")
    @GetMapping("/downloadbill/{billDate}/{type}")
    public R downloadBill(
            @PathVariable String billDate,
            @PathVariable String type) throws Exception {

        log.info("下载账单");
        String result = wxPayService.downloadBill(billDate, type);

        return R.ok().data("result", result);
    }

}
package com.wx.controller;

import com.wx.entity.OrderInfo;
import com.wx.enums.OrderStatus;
import com.wx.service.OrderInfoService;
import com.wx.vo.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.List;

@CrossOrigin
@Api(tags = "商品订单管理")
@RestController
@RequestMapping("/api/order-info")
public class OrderInfoController {

    @Resource
    private OrderInfoService orderInfoService;


    @GetMapping("/list")
    public R list(){
        List<OrderInfo> list = orderInfoService.listOrderByCreateTimeDesc();

        return R.ok().data("list",list);
    }

    /**
     * 查询订单状态
     * @param orderNo
     * @return
     */
    @ApiOperation("查询订单状态")
    @GetMapping("/query-order-status/{orderNo}")
    public R queryOrderStatus(@PathVariable String orderNo){

        String orderStatus = orderInfoService.getOrderStatus(orderNo);
        if (OrderStatus.SUCCESS.getType().equals(orderStatus)){
            return R.ok().setCode(0).setMessage("支付成功");//支付成功
        }
        return R.ok().setCode(101).setMessage("支付中...");
    }

}
package com.wx.controller;

import com.wx.entity.Product;
import com.wx.service.ProductService;
import com.wx.vo.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.List;

@CrossOrigin
@RestController
@RequestMapping("/api/product")
@Api(tags = "商品管理")
public class ProductController {


    @Resource
    private ProductService productService;


    @GetMapping("/list")
    public R list(){
        List<Product> list = productService.list();
        return  R.ok().data("productList",list);
    }
}

Service

package com.wx.service;


import com.baomidou.mybatisplus.extension.service.IService;
import com.wx.entity.OrderInfo;
import com.wx.enums.OrderStatus;

import java.util.List;

public interface OrderInfoService extends IService<OrderInfo> {

    OrderInfo createOrderByProductId(Long productId,String type);//获取订单信息并存入数据库中

                        //订单号           二维码地址
    void saveCodeUrl(String orderNo,String codeUrl);//因为扫码有俩个小时的时间,所以进行数据库的更新

    List<OrderInfo> listOrderByCreateTimeDesc(); //查询订单列表,并倒序

    void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus);//更改订单状态

    String getOrderStatus(String orderNo);//处理重复的通知

    List<OrderInfo> getNopayOrderByDuration(int minutes,String type);//定时任务

    OrderInfo getOrderByOrderNo(String orderNo);// //根据订单号获取订单信息
}
package com.wx.service;

public interface PaymentInfoService {

    void createPaymentInfo(String plainText);//记录支付日志
}
package com.wx.service;


import com.baomidou.mybatisplus.extension.service.IService;
import com.wx.entity.Product;

public interface ProductService extends IService<Product> {

}
package com.wx.service;


import com.baomidou.mybatisplus.extension.service.IService;
import com.wx.entity.RefundInfo;
import com.wx.enums.PayType;

import java.util.List;

public interface RefundInfoService extends IService<RefundInfo> {

    RefundInfo createRefundByOrderNo(String orderNo, String reason);//根据订单编号创建退款

    void updateRefund(String content);//更新退款单

    List<RefundInfo> getNoRefundOrderByDuration(int minutes, String type);//找出申请退款超过5分钟并且未成功的退款单

    RefundInfo createRefundByOrderNoForAliPay(String orderNo, String reason);

    void updateRefundForAliPay(String refundNo, String content, String refundStatus);
}
package com.wx.service;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Map;

public interface WxPayService {

    Map<String, Object> nativePay(Long productId) throws Exception;

    //解密
    void processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException;

    void canceOrder(String orderNo) throws Exception;//取消订单

    String queryOrder(String orderNo) throws IOException;//微信支付查询订单

    void checkOrderStatus(String orderNo) throws Exception;//查询核实订单状态

    void refund(String orderNo, String reason) throws Exception;//申请退款

    String queryRefund(String refundNo) throws Exception;//查询退款

    void processRefund(Map<String, Object> bodyMap) throws Exception;//退款结果通知

    void checkRefundStatus(String refundNo) throws Exception;//核实订单状态:调用微信支付查询退款接口

    String queryBill(String billDate, String type) throws Exception;//获取账单url

    String downloadBill(String billDate, String type) throws Exception;//下载账单
}

ServiceImpl

package com.wx.service.impl;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wx.entity.OrderInfo;
import com.wx.entity.Product;
import com.wx.enums.OrderStatus;
import com.wx.mapper.OrderInfoMapper;
import com.wx.mapper.ProductMapper;
import com.wx.service.OrderInfoService;
import com.wx.util.OrderNoUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.time.Duration;
import java.time.Instant;
import java.util.List;

@Slf4j
@Service
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {

    @Resource
    private ProductMapper productMapper;

    @Resource
    private OrderInfoMapper orderInfoMapper;

    /**
     * 生成订单到数据库中
     * @param productId
     * @return
     */
    @Override
    public OrderInfo createOrderByProductId(Long productId,String type) {


        //查找已存在但未支付的订单
        OrderInfo orderInfo = this.getNoPayOrderByProductId(productId,type);
        if (orderInfo != null){
            return orderInfo;
        }

        //获取商品信息
        Product product = productMapper.selectById(productId);

        //生成订单
        orderInfo = new OrderInfo();
        orderInfo.setTitle(product.getTitle());
        orderInfo.setOrderNo(OrderNoUtils.getOrderNo());//设置订单号
        orderInfo.setProductId(productId);
        orderInfo.setTotalFee(product.getPrice()); //设置订单金额类型 分
        orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType()); //订单状态
//        orderInfo.setUserId();  真实项目中需要存入用户的id,谁下的订单
//        orderInfo.setCodeUrl();  //二维码链接
        orderInfo.setPaymentType(type);
        baseMapper.insert(orderInfo);

        return orderInfo;

    }


    //查找已存在但未支付的订单.如果订单存在且没有支付则返回没支付的订单,防止重复创建订单对象
    private OrderInfo getNoPayOrderByProductId(Long productId,String type) {

        QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("product_id",productId);
        queryWrapper.eq("order_status",OrderStatus.NOTPAY.getType());
        queryWrapper.eq("payment_type",type);
//        queryWrapper.eq("user_id",userId); //再根据用户的id获取用户的订单
        OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);

        return orderInfo;
    }

    /**
     * 存储订单二维码,在数据库中直接更改
     * @param orderNo
     * @param codeUrl
     */
    @Override               //订单号           二维码
    public void saveCodeUrl(String orderNo, String codeUrl) {

        QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("order_no",orderNo);

        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setCodeUrl(codeUrl);
        baseMapper.update(orderInfo,queryWrapper);
    }

    /**
     * 查询订单列表,并倒序查询
     * @return
     */
    @Override
    public List<OrderInfo> listOrderByCreateTimeDesc() { //订单管理

        QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.orderByDesc("create_time");

        return baseMapper.selectList(queryWrapper);
    }

    /**
     * 根据订单号更新订单状态
     * @param orderNo
     * @param orderStatus
     */
    @Override
    public void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus) {

        log.info("更新订单状态 ===》"+orderStatus.getType());
        QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("order_no", orderNo);

        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setOrderStatus(orderStatus.getType());

        baseMapper.update(orderInfo,queryWrapper);
    }

    /**
     * 处理未支付的订单
     * @param orderNo
     * @return
     */
    @Override
    public String getOrderStatus(String orderNo) {
        QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("order_no", orderNo);
        OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
        if (orderInfo == null){
            return null;
        }

        return orderInfo.getOrderStatus();
    }

    /**
     * 查询创建超过minutes分钟并且未支付的订单
     * @param minutes
     * @return
     */
    @Override
    public List<OrderInfo> getNopayOrderByDuration(int minutes,String type) {

        Instant instant = Instant.now().minus(Duration.ofMillis(minutes));//时间实例,用当前时间减去输入的时间

        QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("order_status",OrderStatus.NOTPAY.getType());
        queryWrapper.le("create_time",instant);//小于
        queryWrapper.eq("payment_type",type);//根据支付的类型查询 微信or支付宝

        List<OrderInfo> orderInfos = baseMapper.selectList(queryWrapper);

        return orderInfos;
    }


    /**
     * 根据订单号获取订单
     * @param orderNo
     * @return
     */
    @Override
    public OrderInfo getOrderByOrderNo(String orderNo) {

        QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("order_no", orderNo);
        OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);

        return orderInfo;
    }
}
package com.wx.service.impl;


import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.gson.Gson;
import com.wx.entity.PaymentInfo;
import com.wx.enums.PayType;
import com.wx.mapper.PaymentInfoMapper;
import com.wx.service.PaymentInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;

@Service
@Slf4j
public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, PaymentInfo> implements PaymentInfoService {

    @Override
    public void createPaymentInfo(String plainText) {

        log.info("记录微信支付日志");
        Gson gson = new Gson();
        HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);

        //获取商户的订单号
        String orderNo = plainTextMap.get("out_trade_no").toString();
        //微信支付单号 如果支付有问题可以通过支付单号进行处理
        String transactionId = plainTextMap.get("transaction_id").toString();
        //支付类型,因为可能是通过网页,或者app及其他进行支付
        String tradeType = plainTextMap.get("trade_type").toString();
        //支付状态
        String tradeState = plainTextMap.get("trade_state").toString();
        //用户支付的金额
        Map<String,Object> amount = (Map) plainTextMap.get("amount");
        Integer payerTotal = ((Double) amount.get("payer_total")).intValue();

        PaymentInfo paymentInfo = new PaymentInfo();
        paymentInfo.setOrderNo(orderNo);
        paymentInfo.setPaymentType(PayType.WXPAY.getType());
        paymentInfo.setTransactionId(transactionId);
        paymentInfo.setTradeType(tradeType);
        paymentInfo.setTradeState(tradeState);
        paymentInfo.setPayerTotal(payerTotal);
        paymentInfo.setContent(plainText);

        baseMapper.insert(paymentInfo);//插入数据库中

    }

    /**
     * 记录支付宝日志
     * @param params
     */
    @Override
    public void createPaymentInfoForAlipay(Map<String, String> params) {

        String orderNo = params.get("out_trade_no");//获取订单号
        String tradeNo = params.get("trade_no"); //业务编号
        String tradeStatus = params.get("trade_status");//交易状态
        String totalAmount = params.get("total_amount");
        int totalAmoutInt = new BigDecimal(totalAmount).multiply(new BigDecimal("100")).intValue();//交易金额

        PaymentInfo paymentInfo = new PaymentInfo();
        paymentInfo.setOrderNo(orderNo);
        paymentInfo.setPaymentType(PayType.ALIPAY.getType());
        paymentInfo.setTransactionId(tradeNo);
        paymentInfo.setTradeType("电脑网站支付");
        paymentInfo.setTradeState(tradeStatus);
        paymentInfo.setPayerTotal(totalAmoutInt);

        Gson gson = new Gson();
        String json = gson.toJson(params, HashMap.class);
        paymentInfo.setContent(json);

        baseMapper.insert(paymentInfo);//存储到日志表格中

    }
}
package com.wx.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wx.entity.Product;
import com.wx.mapper.ProductMapper;
import com.wx.service.ProductService;
import org.springframework.stereotype.Service;

@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {

}
package com.wx.service.impl;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.gson.Gson;
import com.wx.entity.OrderInfo;
import com.wx.entity.RefundInfo;
import com.wx.enums.PayType;
import com.wx.enums.wxpay.WxRefundStatus;
import com.wx.mapper.RefundInfoMapper;
import com.wx.service.OrderInfoService;
import com.wx.service.RefundInfoService;
import com.wx.util.OrderNoUtils;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class RefundInfoServiceImpl extends ServiceImpl<RefundInfoMapper, RefundInfo> implements RefundInfoService {

    @Resource
    private OrderInfoService orderInfoService;

    /**
     * 根据订单号创建退款订单
     * @param orderNo
     * @return
     */
    @Override
    public RefundInfo createRefundByOrderNo(String orderNo, String reason) {

        //根据订单号获取订单信息
        OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);

        //根据订单号生成退款订单
        RefundInfo refundInfo = new RefundInfo();
        refundInfo.setOrderNo(orderNo);//订单编号
        refundInfo.setRefundNo(OrderNoUtils.getRefundNo());//退款单编号
        refundInfo.setTotalFee(orderInfo.getTotalFee());//原订单金额(分)
        refundInfo.setRefund(orderInfo.getTotalFee());//退款金额(分)
        refundInfo.setReason(reason);//退款原因

        //保存退款订单
        baseMapper.insert(refundInfo);

        return refundInfo;
    }


    /**
     * 记录退款记录
     * @param content
     */
    @Override
    public void updateRefund(String content) {

        //将json字符串转换成Map
        Gson gson = new Gson();
        Map<String, String> resultMap = gson.fromJson(content, HashMap.class);

        //根据退款单编号修改退款单
        QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("refund_no", resultMap.get("out_refund_no"));

        //设置要修改的字段
        RefundInfo refundInfo = new RefundInfo();

        refundInfo.setRefundId(resultMap.get("refund_id"));//微信支付退款单号

        //查询退款和申请退款中的返回参数
        if(resultMap.get("status") != null){
            refundInfo.setRefundStatus(resultMap.get("status"));//退款状态
            refundInfo.setContentReturn(content);//将全部响应结果存入数据库的content字段
        }
        //退款回调中的回调参数
        if(resultMap.get("refund_status") != null){
            refundInfo.setRefundStatus(resultMap.get("refund_status"));//退款状态
            refundInfo.setContentNotify(content);//将全部响应结果存入数据库的content字段
        }

        //更新退款单
        baseMapper.update(refundInfo, queryWrapper);
    }

    /**
     * 找出申请退款超过minutes分钟并且未成功的退款单
     * @param minutes
     * @return
     */
    @Override
    public List<RefundInfo> getNoRefundOrderByDuration(int minutes,String tpye) {

        //minutes分钟之前的时间
        Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));

        QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("refund_status", WxRefundStatus.PROCESSING.getType());
        queryWrapper.eq("payment_type", PayType.WXPAY.getType());
        queryWrapper.le("create_time", instant);
        List<RefundInfo> refundInfoList = baseMapper.selectList(queryWrapper);
        return refundInfoList;
    }

    /**
     * 根据订单号创建退款订单
     * @param orderNo
     * @return
     */
    @Override
    public RefundInfo createRefundByOrderNoForAliPay(String orderNo, String reason) {

        //根据订单号获取订单信息
        OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);

        //根据订单号生成退款订单
        RefundInfo refundInfo = new RefundInfo();
        refundInfo.setOrderNo(orderNo);//订单编号
        refundInfo.setRefundNo(OrderNoUtils.getRefundNo());//退款单编号

        refundInfo.setTotalFee(orderInfo.getTotalFee());//原订单金额(分)
        refundInfo.setRefund(orderInfo.getTotalFee());//退款金额(分)
        refundInfo.setReason(reason);//退款原因

        //保存退款订单
        baseMapper.insert(refundInfo);

        return refundInfo;
    }

    /**
     * 更新退款记录
     * @param refundNo
     * @param content
     * @param refundStatus
     */
    @Override
    public void updateRefundForAliPay(String refundNo, String content, String refundStatus) {

        //根据退款单编号修改退款单
        QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("refund_no", refundNo);

        //设置要修改的字段
        RefundInfo refundInfo = new RefundInfo();
        refundInfo.setRefundStatus(refundStatus);//退款状态
        refundInfo.setContentReturn(content);//将全部响应结果存入数据库的content字段

        //更新退款单
        baseMapper.update(refundInfo, queryWrapper);

    }

}
package com.wx.service.impl;

import com.google.gson.Gson;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import com.wx.config.WxPayConfig;
import com.wx.entity.OrderInfo;
import com.wx.entity.RefundInfo;
import com.wx.enums.OrderStatus;
import com.wx.enums.PayType;
import com.wx.enums.wxpay.WxApiType;
import com.wx.enums.wxpay.WxNotifyType;
import com.wx.enums.wxpay.WxRefundStatus;
import com.wx.enums.wxpay.WxTradeState;
import com.wx.service.OrderInfoService;
import com.wx.service.PaymentInfoService;
import com.wx.service.RefundInfoService;
import com.wx.service.WxPayService;
import com.wx.util.OrderNoUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {


    @Resource //注入配置中的对象 保函了验签的过程
    private CloseableHttpClient wxPayClient;

    @Resource
    private WxPayConfig wxPayConfig;

    @Resource
    private OrderInfoService orderInfoService;  //将订单存入数据库中

    @Resource
    private PaymentInfoService paymentInfoService;

    @Resource
    private RefundInfoService refundsInfoService;

    @Resource
    private CloseableHttpClient wxPayNoSignClient; //无需应答签名


    private final ReentrantLock lock = new ReentrantLock(); //可重入锁

    /**
     * 创建订单,调用Native支付接口
     * @param productId
     * @return code_url(二维码地址) 和 订单号
     * @throws Exception
     */
    @Override
    public Map<String, Object> nativePay(Long productId) throws Exception {
        /**
         *  如果调用不成功需要去商户平台登录该商户号,在产品中心-我的产品-开通“公众号支付”,这样就可以用于小程序支付了 。
         */

        log.info("生成订单");

        //生成订单  TODO : 存入数据库
        OrderInfo orderInfo= orderInfoService.createOrderByProductId(productId, PayType.WXPAY.getType());
        String codeUrl = orderInfo.getCodeUrl();
        if (orderInfo != null && codeUrl!=null){
            log.info("订单已保存,二维码已经存在");
            //如果第一次创建订单则不会进入,因为数据库没有相应的二维码数据
            //如果第二次调用则有数据,就直接进行返回数据库存储的数据

            //返回二维码
            Map<String, Object> map = new HashMap<>();
            map.put("codeUrl",codeUrl);
            map.put("orderNo",orderInfo.getOrderNo());

            return map;

        }
        log.info("调用统一下单API");


        //调用统一下单API
        HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));//放入远程链接地址

        // 请求body参数
        Gson gson = new Gson();
        Map paramsMap = new HashMap<>();
        paramsMap.put("appid",wxPayConfig.getAppid());//应用ID
        paramsMap.put("mchid",wxPayConfig.getMchId());//商户号
        paramsMap.put("description",orderInfo.getTitle());//商品描述 用了上面的title
        paramsMap.put("out_trade_no",orderInfo.getOrderNo());//订单号
        paramsMap.put("notify_url",wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));//通知地址

        Map amountMap = new HashMap<>();
        amountMap.put("total",orderInfo.getTotalFee());//金额
        amountMap.put("currency","CNY");    //货币类型

        paramsMap.put("amount",amountMap);
        String jsonParams = gson.toJson(paramsMap);//转换成json的格式
        log.info("请求参数:"+jsonParams);


        StringEntity entity = new StringEntity(jsonParams,"utf-8");
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpPost);

        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());//响应头
            int statusCode = response.getStatusLine().getStatusCode();//响应状态
            if (statusCode == 200) { //处理成功
                log.info("成功 = " + bodyAsString);
            } else if (statusCode == 204) { //处理成功,无返回Body
                System.out.println("成功");
            } else {
                System.out.println("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString);
                throw new IOException("request failed");
            }
            //响应结果
            HashMap<String,String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
            //二维码
            codeUrl = resultMap.get("code_url");
            System.out.println("resultMap = " + resultMap);

            //保存新二维码
            String orderNo = orderInfo.getOrderNo();//订单号
            orderInfoService.saveCodeUrl(orderNo,codeUrl);

            Map<String, Object> map = new HashMap<>();
            map.put("codeUrl",codeUrl);
            map.put("orderNo",orderInfo.getOrderNo());

            return map;

        } finally {
            response.close();
        }

    }

    @Override
    public void processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException {

        log.info("处理订单");
        //解密报文
        String plainText = decryptFromResource(bodyMap);

        //将明文转换成map
        Gson gson = new Gson();
        HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
        String orderNo = plainTextMap.get("out_trade_no").toString();//获取商户订单号

        /**
         *在对业务数据进行状态检查和处理之前
         * 要采用数据锁进行并发控制
         * 以避免函数重入造成的数据混乱
         */
        //尝试获取锁,成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放
        if (lock.tryLock()){
            try {
                //处理重复的通知 因为微信通知可能会出现重复的原因,所以进行处理一下
                String orderStatus = orderInfoService.getOrderStatus(orderNo);
                if (!OrderStatus.NOTPAY.getType().equals(orderStatus)){//如果支付状态不等于未支付的
                    return;
                }

                //更新订单状态,支付成功更改状态
                orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.SUCCESS);

                //记录支付日志
                paymentInfoService.createPaymentInfo(plainText);
            } finally {
                //需要主动释放锁
                lock.unlock();
            }
        }
    }

    /**
     * 取消订单
     * @param orderNo
     */
    @Override
    public void canceOrder(String orderNo) throws Exception {

        //调用微信支付的关单接口
        this.closeOrder(orderNo);

        //更新商户端的订单状态
        orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CANCEL);

    }

    /**
     * 微信支付查询订单
     * @param orderNo
     * @return
     */
    @Override
    public String queryOrder(String orderNo) throws IOException {

        log.info("查询订单接口调用 ===》");
        //因为路径中有占位符,所以进行替换
        String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(),orderNo);
        url = wxPayConfig.getDomain().concat(url).concat("?mchid=").concat(wxPayConfig.getMchId());

        HttpGet httpGet = new HttpGet(url);
        httpGet.setHeader("Accept", "application/json");
        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpGet);
        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());//响应头
            int statusCode = response.getStatusLine().getStatusCode();//响应状态
            if (statusCode == 200) { //处理成功
                log.info("成功 = " + bodyAsString);
            } else if (statusCode == 204) { //处理成功,无返回Body
                System.out.println("成功");
            } else {
                System.out.println("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString);
                throw new IOException("request failed");
            }


            return bodyAsString;

        } finally {
            response.close();
        }
    }

    /**
     * 根据订单号查询微信支付查单接口,核实订单状态
     * 如果订单已经支付,则更新商户端订单状态
     * 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
     * @param orderNo
     */
    @Override
    public void checkOrderStatus(String orderNo) throws Exception {

        log.warn("根据订单号核实订单状态 ===》"+orderNo);

        //调用微信支付查单接口
        String result = this.queryOrder(orderNo);
        Gson gson = new Gson();
        HashMap resultMap = gson.fromJson(result, HashMap.class);

        //获取微信支付端的订单状态
        Object tradeState = resultMap.get("trade_state");

        //判断订单状态
        if (WxTradeState.SUCCESS.getType().equals(tradeState)){

            log.warn("核实订单已支付 === 》"+orderNo);

            //如果订单已经支付则更新订单状态
            orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.SUCCESS);
            //记录支付日志
            paymentInfoService.createPaymentInfo(result);
        }
        if (WxTradeState.NOTPAY.getType().equals(tradeState)){
            log.warn("核实订单未支付 === 》"+orderNo);

            //如果订单未支付,则调用关闭订单接口
            this.closeOrder(orderNo);

            //更新本地订单状态 不用记录日志
            orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CLOSED);
        }
    }


    /**
     * 退款
     * @param orderNo
     * @param reason
     * @throws IOException
     */
    @Override
    public void refund(String orderNo, String reason) throws Exception {

        log.info("创建退款单记录");
        //根据订单编号创建退款单
        RefundInfo refundsInfo = refundsInfoService.createRefundByOrderNo(orderNo, reason);

        log.info("调用退款API");

        //调用统一下单API
        String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
        HttpPost httpPost = new HttpPost(url);

        // 请求body参数
        Gson gson = new Gson();
        Map paramsMap = new HashMap();
        paramsMap.put("out_trade_no", orderNo);//订单编号
        paramsMap.put("out_refund_no", refundsInfo.getRefundNo());//退款单编号
        paramsMap.put("reason",reason);//退款原因
        paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));//退款通知地址

        Map amountMap = new HashMap();
        amountMap.put("refund", refundsInfo.getRefund());//退款金额
        amountMap.put("total", refundsInfo.getTotalFee());//原订单金额
        amountMap.put("currency", "CNY");//退款币种
        paramsMap.put("amount", amountMap);

        //将参数转换成json字符串
        String jsonParams = gson.toJson(paramsMap);
        log.info("请求参数 ===> {}" + jsonParams);

        StringEntity entity = new StringEntity(jsonParams,"utf-8");
        entity.setContentType("application/json");//设置请求报文格式
        httpPost.setEntity(entity);//将请求报文放入请求对象
        httpPost.setHeader("Accept", "application/json");//设置响应报文格式

        //完成签名并执行请求,并完成验签
        CloseableHttpResponse response = wxPayClient.execute(httpPost);

        try {

            //解析响应结果
            String bodyAsString = EntityUtils.toString(response.getEntity());
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                log.info("成功, 退款返回结果 = " + bodyAsString);
            } else if (statusCode == 204) {
                log.info("成功");
            } else {
                throw new RuntimeException("退款异常, 响应码 = " + statusCode+ ", 退款返回结果 = " + bodyAsString);
            }

            //更新订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING);

            //更新退款单
            refundsInfoService.updateRefund(bodyAsString);

        } finally {
            response.close();
        }
    }



    /**
     * 查询退款接口调用
     * @param refundNo
     * @return
     */
    @Override
    public String queryRefund(String refundNo) throws Exception {

        log.info("查询退款接口调用 ===> {}", refundNo);

        String url =  String.format(WxApiType.DOMESTIC_REFUNDS_QUERY.getType(), refundNo);
        url = wxPayConfig.getDomain().concat(url);

        //创建远程Get 请求对象
        HttpGet httpGet = new HttpGet(url);
        httpGet.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpGet);

        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                log.info("成功, 查询退款返回结果 = " + bodyAsString);
            } else if (statusCode == 204) {
                log.info("成功");
            } else {
                throw new RuntimeException("查询退款异常, 响应码 = " + statusCode+ ", 查询退款返回结果 = " + bodyAsString);
            }

            return bodyAsString;

        } finally {
            response.close();
        }
    }

    /**
     * 根据退款单号核实退款单状态
     * @param refundNo
     * @return
     */
    @Override
    public void checkRefundStatus(String refundNo) throws Exception {

        log.warn("根据退款单号核实退款单状态 ===> {}", refundNo);

        //调用查询退款单接口
        String result = this.queryRefund(refundNo);

        //组装json请求体字符串
        Gson gson = new Gson();
        Map<String, String> resultMap = gson.fromJson(result, HashMap.class);

        //获取微信支付端退款状态
        String status = resultMap.get("status");

        String orderNo = resultMap.get("out_trade_no");

        if (WxRefundStatus.SUCCESS.getType().equals(status)) {

            log.warn("核实订单已退款成功 ===> {}", refundNo);

            //如果确认退款成功,则更新订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);

            //更新退款单
            refundsInfoService.updateRefund(result);
        }

        if (WxRefundStatus.ABNORMAL.getType().equals(status)) {

            log.warn("核实订单退款异常  ===> {}", refundNo);

            //如果确认退款成功,则更新订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL);

            //更新退款单
            refundsInfoService.updateRefund(result);
        }
    }


    /**
     * 申请账单
     * @param billDate
     * @param type
     * @return
     * @throws Exception
     */
    @Override
    public String queryBill(String billDate, String type) throws Exception {
        log.warn("申请账单接口调用 {}", billDate);

        String url = "";
        if("tradebill".equals(type)){
            url =  WxApiType.TRADE_BILLS.getType();
        }else if("fundflowbill".equals(type)){
            url =  WxApiType.FUND_FLOW_BILLS.getType();
        }else{
            throw new RuntimeException("不支持的账单类型");
        }

        url = wxPayConfig.getDomain().concat(url).concat("?bill_date=").concat(billDate);

        //创建远程Get 请求对象
        HttpGet httpGet = new HttpGet(url);
        httpGet.addHeader("Accept", "application/json");

        //使用wxPayClient发送请求得到响应
        CloseableHttpResponse response = wxPayClient.execute(httpGet);

        try {

            String bodyAsString = EntityUtils.toString(response.getEntity());

            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                log.info("成功, 申请账单返回结果 = " + bodyAsString);
            } else if (statusCode == 204) {
                log.info("成功");
            } else {
                throw new RuntimeException("申请账单异常, 响应码 = " + statusCode+ ", 申请账单返回结果 = " + bodyAsString);
            }

            //获取账单下载地址
            Gson gson = new Gson();
            Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
            return resultMap.get("download_url");

        } finally {
            response.close();
        }
    }

    /**
     * 下载账单
     * @param billDate
     * @param type
     * @return
     * @throws Exception
     */
    @Override
    public String downloadBill(String billDate, String type) throws Exception {
        log.warn("下载账单接口调用 {}, {}", billDate, type);

        //获取账单url地址
        String downloadUrl = this.queryBill(billDate, type);
        //创建远程Get 请求对象
        HttpGet httpGet = new HttpGet(downloadUrl);
        httpGet.addHeader("Accept", "application/json");

        //使用wxPayClient发送请求得到响应
        CloseableHttpResponse response = wxPayNoSignClient.execute(httpGet);

        try {

            String bodyAsString = EntityUtils.toString(response.getEntity());

            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                log.info("成功, 下载账单返回结果 = " + bodyAsString);
            } else if (statusCode == 204) {
                log.info("成功");
            } else {
                throw new RuntimeException("下载账单异常, 响应码 = " + statusCode+ ", 下载账单返回结果 = " + bodyAsString);
            }

            return bodyAsString;

        } finally {
            response.close();
        }
    }

    /**
     * 处理退款单 退款通知
     */
    @Override
    public void processRefund(Map<String, Object> bodyMap) throws Exception {

        log.info("退款单");

        //解密报文
        String plainText = decryptFromResource(bodyMap);

        //将明文转换成map
        Gson gson = new Gson();
        HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
        String orderNo = (String)plainTextMap.get("out_trade_no");//获取订单信息

        if(lock.tryLock()){
            try {

                String orderStatus = orderInfoService.getOrderStatus(orderNo);
                //判断订单是否接收订单回调
                if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)) {
                    return;
                }
                //如果当前订单是正在退款的状态下
                //更新订单状态
                orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);

                //更新退款单
                refundsInfoService.updateRefund(plainText);

            } finally {
                //要主动释放锁
                lock.unlock();
            }
        }
    }

    /**
     * 关单接口的调用
     * @param orderNo
     */
    private void closeOrder(String orderNo) throws Exception {

        log.info("关单接口的调用,订单号 ===》{}",orderNo);
        //创建远程关闭订单地址
        String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(),orderNo);//地址中含有占位符 所以进行替换掉
        url = wxPayConfig.getDomain().concat(url);
        HttpPost httpPost = new HttpPost(url);

        //组装json请求体
        Gson gson = new Gson();
        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("mchid",wxPayConfig.getMchId());
        String jsonParams = gson.toJson(paramMap);
        log.info("请求参数 === 》 {}",jsonParams);

        //将请求参数设置到请求对象中
        StringEntity entity = new StringEntity(jsonParams,"utf-8");
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");

        CloseableHttpResponse response = wxPayClient.execute(httpPost);
        try {

            int statusCode = response.getStatusLine().getStatusCode();//响应状态
            if (statusCode == 200) { //处理成功
                log.info("成功200" );
            } else if (statusCode == 204) { //处理成功,无返回Body
                System.out.println("成功204");
            } else {
                System.out.println("Native下单失败,响应码 = " + statusCode);
                throw new IOException("request failed");
            }


        } finally {
            response.close();
        }
    }

    /**
     * 对称解密
     * @param bodyMap
     * @return
     */
    private String decryptFromResource(Map<String, Object> bodyMap) throws GeneralSecurityException {
        log.info("密文解密");
        //通知数据
        Map<String,String> resourceMap = (Map)bodyMap.get("resource");
        //获取数据中的密文
        String ciphertext = resourceMap.get("ciphertext");
        //随机串
        String nonce = resourceMap.get("nonce");
        //附加数据
        String associatedData = resourceMap.get("associated_data");

        //解密工具
        AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8)
                , nonce.getBytes(StandardCharsets.UTF_8)
                , ciphertext);
        log.info("明文===》{}",plainText);

        return plainText;
    }
}

微信APP支付获取签名时间戳

/**
     * 调用统一下单api  服务订单
     * @param wnOrderDecoration
     * @return
     * @throws Exception
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> nativePay2(WnOrderDecoration wnOrderDecoration) throws Exception {

        wnOrderDecorationService.updateById(wnOrderDecoration);
        log.info("调用统一下单API");

        Double money = null;
        String orderNumber = null;
        if (wnOrderDecoration.getPayState().equals(OrderStatus.NOTPAY.getType())){
            money = wnOrderDecoration.getVisitCost();//上门金额 先支付上面费用
            orderNumber = wnOrderDecoration.getDoorOrderNumber();
        }

        if (wnOrderDecoration.getPayState().equals(OrderStatus.SUCCESS.getType())){
            money = wnOrderDecoration.getCost();//设计师报价
            orderNumber = wnOrderDecoration.getOrderNumber();
        }


        //调用统一下单API
        HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));//放入远程链接地址

        // 请求body参数
        Gson gson = new Gson();
        Map paramsMap = new HashMap<>();
        paramsMap.put("appid",wxPayConfig.getAppid());//应用ID
        paramsMap.put("mchid",wxPayConfig.getMchId());//商户号
        paramsMap.put("description",wnOrderDecoration.getOrderName());//商品描述 用了上面的title
        paramsMap.put("out_trade_no",orderNumber);//订单号
        paramsMap.put("notify_url",wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));//通知地址

        Map amountMap = new HashMap<>();
        amountMap.put("total",money);//金额
        amountMap.put("currency","CNY");    //货币类型

        paramsMap.put("amount",amountMap);
        String jsonParams = gson.toJson(paramsMap);//转换成json的格式
        log.info("请求参数:"+jsonParams);

        StringEntity entity = new StringEntity(jsonParams,"utf-8");
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpPost);

        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());//响应头

            //响应结果
            HashMap<String,String> resultMap = gson.fromJson(bodyAsString, HashMap.class);

            //预支付id
            String codeUrl = resultMap.get("prepay_id");
            System.out.println("resultMap = " + resultMap);


            
            //时间戳
            Long timeStamp = genTimeStamp();
            //随机字符串
            String nonce = RandomUtil.randomString(32);
            //获取签名sign------------------
            StringBuilder builder1 = new StringBuilder();
            builder1.append("wxde22cf376c634bea").append("\n");// 应用id
            //时间戳
            builder1.append(timeStamp).append("\n"); //时间戳
            //随机字符串
            builder1.append(nonce).append("\n"); //字符串
            //预支付id
            builder1.append(codeUrl).append("\n"); // 预支付id
            //签名
            String sign = sign(builder1.toString().getBytes(StandardCharsets.UTF_8));

            Map<String, Object> map = new HashMap<>();
            map.put("prepay_id",codeUrl); //预支付id
            map.put("orderNo",wnOrderDecoration.getOrderNumber());//订单号
            map.put("appid",wxPayConfig.getAppid());//应用id
            map.put("partnerid",wxPayConfig.getMchId()); //商户号
            map.put("package","Sign=WXPay");//订单详情扩展字符串 固定的
            map.put("noncestr",nonce);//随机字符串
            map.put("timestamp",timeStamp);//时间戳
            map.put("sign",sign);//签名
            return map;

        } finally {
            response.close();
        }

    }

    //获取签名
    String sign(byte[] message) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException {
        Signature sign = Signature.getInstance("SHA256withRSA");
//        PrivateKey privateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream(wxPayConfig.getPrivateKeyPath().getBytes("utf-8")));
//        sign.initSign(privateKey);
        sign.initSign(getPrivateKey(wxPayConfig.getPrivateKeyPath()));
        sign.update(message);
        return Base64.getEncoder().encodeToString(sign.sign());
    }

    /**
     * 获取私钥。
     *
     * @param filename 私钥文件路径  (required)
     * @return 私钥对象
     */
    public static PrivateKey getPrivateKey(String filename) throws IOException {

        String content = new String(Files.readAllBytes(Paths.get(filename)), "utf-8");
        try {
            String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
                    .replace("-----END PRIVATE KEY-----", "")
                    .replaceAll("\\s+", "");

            KeyFactory kf = KeyFactory.getInstance("RSA");
            return kf.generatePrivate(
                    new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持RSA", e);
        } catch (InvalidKeySpecException e) {
            throw new RuntimeException("无效的密钥格式");
        }
    }

    //获取时间戳
    private static long genTimeStamp() {
        return System.currentTimeMillis() / 1000;
    }

微信公众号支付(JS-SDK权限签名算法)

获取token

package com.ruoyi.web.jxxt.util.wxGz;

import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.utils.http.HttpUtils;
import lombok.Data;

@Data
public class AccessToken {
    private String token;
    private long expiresTime;//过期时间

    public AccessToken(String token, String expiresIn) {
        super();
        this.token = token;
        //当前时间+有效期 = 过期时间
        this.expiresTime = System.currentTimeMillis()+Integer.parseInt(expiresIn);
    }

    /**
     * 判断token是否过期
     * @return
     */
    public boolean isExpire() {
        return System.currentTimeMillis() > expiresTime;
    }
    //get and set ...
    private static AccessToken at;//token获取的次数有限,有效期也有限,所以需要保存起来
    private static String GET_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";

    //登录测试号管理界面-测试号信息下面可以得到你的APPID和APPSECRET
    private static String APPID = "wxde22xxxx";
    private static String APPSECRET = "e8186658cxxxxx";
    /**
     * 发送get请求获取AccessToken
     */
    private static String getToken() {
        String url = GET_TOKEN_URL.replace("APPID", APPID).replace("APPSECRET", APPSECRET);
        String tokenStr = HttpUtils.sendGet(url);//调用工具类发get请求
        System.out.println(tokenStr);
        JSONObject jsonObject = JSONObject.parseObject(tokenStr);
        String token = jsonObject.getString("access_token");
        String expiresIn = jsonObject.getString("expires_in");
        at = new AccessToken(token, expiresIn);
        return at.token;
    }

    /**
     * todo 获取AccessToken  向外提供   调用的ip必须在公众号设置白名单。不然获取不到token!!!!!!!!!!
     */
    public static String getAccessToken() {
        //过期了或者没有值再去发送请求获取
        if(at == null || at.isExpire()) {
            getToken();
        }
        return at.getToken();
    }

    public static void main(String[] args) {
        String accessToken = getAccessToken();
        System.out.println("accessToken = " + accessToken);
    }


}
    public String getWXaccessToken() {
        String accessToken = AccessToken.getAccessToken();
        return accessToken;
    }

    public String getWXJsapiTicket(String token) {
        String ticket = null;
        if (StringUtils.isBlank(ticket)) {
            String url ="https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=" + token +"&type=jsapi";
            RestTemplate restTemplate = new RestTemplate();
            String resp = restTemplate.getForObject(url, String.class);
            JSONObject resJson = JSONObject.parseObject(resp);
            return resJson.getString("ticket");
        }
        return ticket;
    }



    public static String sha1(String decript) {
        try {
            MessageDigest digest = java.security.MessageDigest.getInstance("SHA-1");
            digest.update(decript.getBytes());
            byte[] messageDigest = digest.digest();
            // Create Hex String
            StringBuilder hexString = new StringBuilder();
            // 字节数组转换为 十六进制 数
            for (byte b : messageDigest) {
                String shaHex = Integer.toHexString(b & 0xFF);
                if (shaHex.length() < 2) {
                    hexString.append(0);
                }
                hexString.append(shaHex);
            }
            return hexString.toString();
        } catch (NoSuchAlgorithmException e) {
            log.error("微信签名时失败,请检查!", e);
        }
        return "";
    }


    /**
     *   入参为url
     * @param url
     * @return
     */
    @PostMapping("/getWXSign")
    public String getWXSign(@RequestBody String url) {

        log.info("urlsssss 参数           为:"+url);
        JSONObject jsonObject = JSONObject.parseObject(url);
        url = jsonObject.getString("url");
        log.info("url 参数           为:"+url);

        long timestamp = System.currentTimeMillis() / 1000;
//        //随机字符串
//        int noncestr = Math.abs(new Random().nextInt());
        //随机字符串
        String noncestr = RandomUtil.randomString(32);
        String[] urls = url.split("#");
        String newUrl = urls[0];
        JSONObject respJson =new JSONObject();
        String[] signArr =new String[]{"url=" + newUrl,"jsapi_ticket=" + getWXJsapiTicket(getWXaccessToken()),"noncestr=" + noncestr,"timestamp=" + timestamp};
        Arrays.sort(signArr);
        String signStr = StringUtils.join(signArr,"&");

        log.info("signStr 参数为  :"+ signStr);
//        String resSign = DigestUtils.sha1Hex(signStr);
        String resSign = sha1(signStr);
        respJson.put("appId", "wxde22cf376c634bea");
        respJson.put("timestamp", timestamp);
        respJson.put("nonceStr", noncestr);
        respJson.put("signature", resSign);

        return respJson.toJSONString();
    }

创建定时任务进行定时查询订单状态

package com.wx.task;

import com.wx.entity.OrderInfo;
import com.wx.entity.RefundInfo;
import com.wx.enums.PayType;
import com.wx.service.OrderInfoService;
import com.wx.service.RefundInfoService;
import com.wx.service.WxPayService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.List;

@Slf4j
@Component
public class WxPayTask {

    @Resource
    private OrderInfoService orderInfoService;

    @Resource
    private WxPayService wxPayService;

    @Resource
    private RefundInfoService refundInfoService;

    /**
     * 从第0秒开始,每隔30秒执行一次,查询创建超过五分钟,并且未支付的订单
     */
    //@Scheduled(cron = "0/30 * * * * ?")
    public void orderConfirm() throws Exception {
        log.info("定时任务启动====");

        List<OrderInfo> orderInfoList = orderInfoService.getNopayOrderByDuration(5, PayType.WXPAY.getType());

        for (OrderInfo orderInfo : orderInfoList){
            String orderNo = orderInfo.getOrderNo();
            log.warn("超时订单 === > {}", orderNo);

            //核实订单状态:调用微信支付查单接口
            wxPayService.checkOrderStatus(orderNo);
        }
    }
    /**
     * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未成功的退款单
     * */
    //@Scheduled(cron = "0/30 * * * * ?")
    public void refundConfirm() throws Exception {
        log.info("refundConfirm 被执行......");
        //找出申请退款超过5分钟并且未成功的退款单
        List<RefundInfo> refundInfoList = refundInfoService.getNoRefundOrderByDuration(5, PayType.WXPAY.getType());
        for (RefundInfo refundInfo : refundInfoList) {
            String refundNo = refundInfo.getRefundNo();
            log.warn("超时未退款的退款单号 ===> {}", refundNo);

            //核实订单状态:调用微信支付查询退款接口
            wxPayService.checkRefundStatus(refundNo);
        }
    }
}

测试

 

支付宝支付

AlipayClientConfig

引入参数文件,获取参数并组装到client中。后面方便调用

package com.wx.config;


import com.alipay.api.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;

import javax.annotation.Resource;

/**
 * 加载支付宝配置参数文件
 */
@Configuration
//加载配置文件
@PropertySource("classpath:alipay-sandbox.properties")
public class AlipayClientConfig {

    @Resource
    private Environment config;//注入此对象,方便读取配置文件中数据

    @Bean
    public AlipayClient alipayClient() throws AlipayApiException {

        AlipayConfig alipayConfig = new AlipayConfig();

        //设置网关地址
        alipayConfig.setServerUrl(config.getProperty("alipay.gateway-url"));
        //设置应用Id
        alipayConfig.setAppId(config.getProperty("alipay.app-id"));
        //设置应用私钥
        alipayConfig.setPrivateKey(config.getProperty("alipay.merchant-private-key"));
        //设置请求格式,固定值json
        alipayConfig.setFormat(AlipayConstants.FORMAT_JSON);
        //设置字符集
        alipayConfig.setCharset(AlipayConstants.CHARSET_UTF8);
        //设置支付宝公钥
        alipayConfig.setAlipayPublicKey(config.getProperty("alipay.alipay-public-key"));
        //设置签名类型
        alipayConfig.setSignType(AlipayConstants.SIGN_TYPE_RSA2);
        //构造client
        AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig);

        return alipayClient;
    }
}

alipay-sandbox.properties

支付宝参数:此处用的是沙箱模式

# 支付宝支付相关参数

# 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
alipay.app-id=2021000121632501

# 商户PID,卖家支付宝账号ID
alipay.seller-id=2088621987731295

# 支付宝网关
alipay.gateway-url=https://openapi.alipaydev.com/gateway.do

# 商户私钥,您的PKCS8格式RSA2私钥
alipay.merchant-private-key=MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCq309YMdt/Kt2leisVuMbA6fTSmc2s9iY6wtuCDSbqz3RK187qsZepa2S7l6J16BWKXak0QIus70ZCGZ61U//ToQqDXc3JKlKvp19Pcq8YpvzByv0Z2FdtvGi9tbjX1icB2Xt/6uO9BYuixi9d3e1kzx9/M2RDiVuPmTSPvmIGJLYSmjsmJO1FOCyZQa5X/d0no5Ko4vKtV/DanqoqWNsOGpoU7bCFA/Y+PtS4xSEUgnsSWjymEQlfSublENadXhSLEP144ZrHKRDdFwTrua64KbQVFR5dnXvcVd4ERCD5C2Vtl+b3qx1puYlCxFPXp/dgC6f4iqQNZCj+W4m3NqmVAgMBAAECggEASVD34ofB/paN8+qvgep+nVfFTHfh4EzdqmjhdrPd9vJ8m4BtsBXzVSZXWoZ9lsm2NGBrsZfgVpt0Mfh8OKGKK2v17tfY7G/Uern+E0DKEHHWEfDfGK/TE6q75mqKnVGt+wUuEHzgqsIuX/FZcZU/vvmAMjwC0Vemib7a5rJxrOBvP40siA/e9se4PwmQHqfXH5J6vyJna6dH1r4f+sxhWdCb4O1VxZgI52J7rMStYGqwnEMKv5h7aB2zpq6BQbcblvNw6hBA80sn+F+LJM0Auebqk+HX/wZXHKsJVoRYEtCUNhl4YoNo5V3U3WYci1JXPJ+Op6PMI8n4iJZSTj6YIQKBgQDnDh2DkPR5RCLjJ1F+Kq5EotDNwLA21/xibLHE/gTT9kdxfKdSckZjOVp+nlMQ2Z+L8khD3YfRDD4sUheL8fKA22G9GnY31/4c2/XsjWPogr0BpgxFRt954OyPIoL+FQLkZnH05MOY5bq9N9/gfuuF3txCTJgMUYEWTba3Q52hkwKBgQC9Ud2wwwhVED5x80Tl4z4QVKro3ubbdat+IiCLOAOoW1IyRG+HV3CbG82DMT0F6h3YRBaRtC/UUoeh/YFpsYjhH30SghiM7N9l4Sk4X9z4eMvYklE02P81TOOukTmJzugHxtwb6k2YZC4LOu7+S2sRc0kTmUfX1CgmZ2L07ochNwKBgQCf4m6d6iKh/3o8wapsqdApgpkGp73IVbE50ok5DaX9nsBVUbLfJGB8rOVoFNraIB19U8yZ2aPwDo6/UJcmqefrLuP1XWhMwFQBWFxWsoheDooHqAV5ss9VoUVQzsriU1vK/PECS4LmPKH56b4rtOf5nPvBjQryCzxOWLyFGG7trQKBgElOCakH25IUWBmHOIZLFxz7q7G/nWQci+qrDC7b4Y6uzYTpOsYM9W0Zttm1lwtTO3sh4htIybxMuHfg0Ns8AuQobSVdemQW0+l+5ZcOh2EuZL/W59quqyLYQtC1KrJRi0Z3mYK1lpYLNEjk6OVODocTPJh6IXdQjrtQDOEJ+wjBAoGBAJopA4T5OnunBZkvYZPXya747m6U4nJFIcS0lJfM9ZGP2hw/mCOhXb2khL789v4G78kwUgfnVAcPPyyG1jjFtPiWb9+DrANZijBWb6HxnvTRytjeROlxIgXiLmb7MqTZHLujyb3H80rhTu3rqk8Jp1Yx26sEKKJXz3rc5KkSVOFt

# 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥
alipay.alipay-public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgjkP008ZFSXVqWoSClXeKDTX7leNDVabMQcexv3TxZY4KYNUSd091BeYlE59feUaGTcehHc9r3N48jaqbZJyX8M2ogdqFlA/8iep22WcQu5ybIEUX45L40ClYqiqKLYpj/uuPFrekEKdZrS1DxaawDaazGypFFzpz/Lf6ijjbDeQhVsSqaPDAZEqmGWUo6oF1bahCpYJb9q/orqaihqA1vb7oRm7k3n8e76H6O1xxDVNenIsi4tit0wlZ6XneOVxnzEgsk0NAGa8BEH2gKrkVycVgBAUxjr7yWVyJuL0pYJkHnQbg6WxLDaDhe8iqGC1faSGqlB4PcIJp+pXHwv0DwIDAQAB

# 接口内容加密秘钥,对称秘钥
alipay.content-key=DNxJbSgGPbQwXL3jnKw42A==

# 页面跳转同步通知页面路径
alipay.return-url=http://localhost:8080/#/success

# 服务器异步通知页面路径  需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
alipay.notify-url=http://ddme2g.natappfree.cc/api/ali-pay/trade/notify





AliPayController

package com.wx.controller;

import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayConstants;
import com.alipay.api.internal.util.AlipaySignature;
import com.wx.entity.OrderInfo;
import com.wx.service.AliPayService;
import com.wx.service.OrderInfoService;
import com.wx.vo.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.Map;

@RestController
@CrossOrigin
@RequestMapping("/api/ali-pay")
@Api(tags = "支付宝支付")
@Slf4j
public class AliPayController {

    @Resource
    private AliPayService aliPayService;

    @Resource
    private Environment config;

    @Resource
    private OrderInfoService orderInfoService;

    @ApiOperation("统一收单下单支付页面接口调用")
    @PostMapping("/trade/page/pay/{productId}")
    public R tradePage(@PathVariable Long productId){

        log.info("统一收单下单支付页面接口调用");
        //支付报开放平台接受 request 请求对象后
        //会被开放者生成一个html形式的from表单,包含自动提交的脚本
        String formStr = aliPayService.tradeCteate(productId);

        //我们将from表单字符串返回给前端程序.之后前端将会调用自动提交脚本,进行表单的提交
        //此时,表单会自动提交的action属性所执行的支付宝开放平台中,从而为用户展示一个支付页面
        return R.ok().data("formStr",formStr);
    }

    @ApiOperation("支付通知")
    @PostMapping("/trade/notify")
    public String tradeNotify(@RequestParam Map<String,String> params){

        log.info("支付通知正在进行");
        log.info("通知参数 ===》 :"+params);

        String result = "failure";

        try {
            //异步通知验签
            boolean signVerified = AlipaySignature.rsaCheckV1(
                    params,
                    config.getProperty("alipay.alipay-public-key"),
                    AlipayConstants.CHARSET_UTF8,
                    AlipayConstants.SIGN_TYPE_RSA2);//调用SDK验证签名
            if(!signVerified){

                //验签失败则记录异常日志,并在response中返回failure.
                log.error("异步通知验签失败");
                return result;
            }


            //验签成功后
            log.info("支付成功异步通知验签成功!");

            //按照支付结果异步通知中的描述,对支付结果中的业务内容进行二次校验
            //1.商家需要验证该通知数据中的 out_trade_no 是否为商家系统中创建的订单号。
            String outTradeNo = params.get("out_trade_no");
            OrderInfo oeder = orderInfoService.getOrderByOrderNo(outTradeNo);
            if (oeder == null){
                log.error("订单不存在");
                return result;
            }

            //2.判断 total_amount 是否确实为该订单的实际金额(即商家订单创建时的金额)。
            String totalAmount = params.get("total_amount");
            int totalAmoutInt = new BigDecimal(totalAmount).multiply(new BigDecimal("100")).intValue();
            int totalFeeInt = oeder.getTotalFee().intValue();
            if (totalAmoutInt != totalFeeInt){
                log.error("金额校验失败");
                return result;
            }
            //3.校验通知中的 seller_id(或者 seller_email) 是否为 out_trade_no 这笔单据的对应的操作方(有的时候,一个商家可能有多个 seller_id/seller_email)。
            String sellerId = params.get("seller_id");
            String sellerIdPro = config.getProperty("alipay.seller-id");
            if (!sellerId.equals(sellerIdPro)){
                log.error("商家pid校验失败");
                return result;
            }
            //4.验证 app_id 是否为该商家本身。
            String appId = params.get("app_id");
            String appIdProperty = config.getProperty("alipay.app-id");
            if (!appId.equals(appIdProperty)){
                log.error("appid校验失败");
                return result;
            }

            //只有交易通知状态为 TRADE_SUCCESS 或 TRADE_FINISHED 时,支付宝才会认定为买家付款成功。
            String tradeStatus = params.get("trade_status");
            if (!"TRADE_SUCCESS".equals(tradeStatus)){
                log.error("支付未成功");
                return result;
            }
            //处理业务,修改订单状态,记录支付日志
            aliPayService.processOrder(params);

            //校验成功后在response中返回success并继续商户自身业务处理,校验失败返回failure




            //向支付宝返回成功的标识,否则会一直不间断的发送通知给我们
            result = "success";
        } catch (AlipayApiException e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 用户取消订单
     * @param orderNo
     * @return
     */
    @ApiOperation("用户取消订单")
    @PostMapping("/trade/close/{orderNo}")
    public R cancel(@PathVariable String orderNo){
        log.info("用户取消订单");

        aliPayService.cancelOrder(orderNo);
        return R.ok().setMessage("用户已取消订单");
    }

    /**
     * 查询订单
     * @param orderNo
     * @return
     */
    @ApiOperation("支付宝支付查询订单")
    @GetMapping("/trade/query/{orderNo}")
    public R queryOrder(@PathVariable String orderNo){

        log.info("查询订单");
        String result =  aliPayService.queryOrder(orderNo);
        return R.ok().setMessage("查询成功").data("result",result);
    }

    /**
     * 申请退款
     * @param orderNo
     * @param reason
     * @return
     */
    @ApiOperation("申请退款")
    @PostMapping("/trade/refund/{orderNo}/{reason}")
    public R refunds(@PathVariable String orderNo, @PathVariable String reason){

        log.info("申请退款");
        aliPayService.refund(orderNo, reason);
        return R.ok();
    }

    /**
     * 查询退款
     * @param orderNo
     * @return
     * @throws Exception
     */
    @ApiOperation("查询退款")
    @GetMapping("/trade/fastpay/refund/{orderNo}")
    public R queryRefund(@PathVariable String orderNo) throws Exception {

        log.info("查询退款");

        String result = aliPayService.queryRefund(orderNo);
        return R.ok().setMessage("查询成功").data("result", result);
    }

    /**
     * 根据账单类型和日期获取账单url地址
     *
     * @param billDate
     * @param type
     * @return
     */
    @ApiOperation("获取账单url")
    @GetMapping("/bill/downloadurl/query/{billDate}/{type}")
    public R queryTradeBill(
            @PathVariable String billDate,
            @PathVariable String type)  {
        log.info("获取账单url");
        String downloadUrl = aliPayService.queryBill(billDate, type);
        return R.ok().setMessage("获取账单url成功").data("downloadUrl", downloadUrl);
    }


}

AliPayTradeState

支付类型

package com.wx.enums.wxpay;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum AliPayTradeState {

    /**
     * 支付成功
     */
    SUCCESS("TRADE_SUCCESS"),

    /**
     * 未支付
     */
    NOTPAY("WAIT_BUYER_PAY"),

    /**
     * 已关闭
     */
    CLOSED("TRADE_CLOSED"),

    /**
     * 退款成功
     */
    REFUND_SUCCESS("REFUND_SUCCESS"),

    /**
     * 退款失败
     */
    REFUND_ERROR("REFUND_ERROR");

    /**
     * 类型
     */
    private final String type;
}

AliPayServiceImpl

package com.wx.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.request.*;
import com.alipay.api.response.*;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.google.gson.internal.LinkedTreeMap;
import com.wx.entity.OrderInfo;
import com.wx.entity.RefundInfo;
import com.wx.enums.OrderStatus;
import com.wx.enums.PayType;
import com.wx.enums.wxpay.AliPayTradeState;
import com.wx.service.AliPayService;
import com.wx.service.OrderInfoService;
import com.wx.service.PaymentInfoService;
import com.wx.service.RefundInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

@Service
@Slf4j
public class AliPayServiceImpl implements AliPayService {


    @Resource
    private OrderInfoService orderInfoService;

    @Resource
    private AlipayClient alipayClient;

    @Resource
    private Environment config;

    @Resource
    private PaymentInfoService paymentInfoService;

    @Resource
    private RefundInfoService refundsInfoService;

    private final ReentrantLock lock = new ReentrantLock();
    /**
     * 统一收单下单支付页面接口调用
     * @param productId
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public String tradeCteate(Long productId) {

        try {
            //生成订单
            log.info("生成订单");
            OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId, PayType.ALIPAY.getType());

            //调用支付宝接口
            AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
            //配置需要的公共请求参数
            //支付完成后,支付宝发起异步通知的地址
            request.setNotifyUrl(config.getProperty("alipay.notify-url"));
            //支付完成后,我们想让页面跳转回成功的页面,配置returnUrl
            request.setReturnUrl(config.getProperty("alipay.return-url"));

            //组装当前业务方法的请求参数
            JSONObject bizContent = new JSONObject();
            bizContent.put("out_trade_no", orderInfo.getOrderNo());
            //因为微信是分,这里支付宝是元,所以进行更改
            BigDecimal total = new BigDecimal(orderInfo.getTotalFee().toString()).divide(new BigDecimal("100"));
            bizContent.put("total_amount", total);
            bizContent.put("subject", orderInfo.getTitle());
            bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");

            request.setBizContent(bizContent.toString());

            //执行请求,调用支付宝接口
            AlipayTradePagePayResponse response = alipayClient.pageExecute(request);

            if(response.isSuccess()){
                log.info("调用成功,返回结果 ===> " + response.getBody());
                return response.getBody();
            } else {
                log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
                throw new RuntimeException("创建支付交易失败");
            }
        } catch (AlipayApiException e) {
            e.printStackTrace();
            throw new RuntimeException("创建支付交易失败");
        }
    }

    /**
     * 处理业务,修改订单状态,记录支付日志
     * @param params
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void processOrder(Map<String, String> params) {
        log.info("处理订单");

        //获取订单号
        String orderNo = params.get("out_trade_no");

        /**
         * 在对业务数据进行状态检查和处理之前
         * 要采用数据锁进行控制
         * 以避免函数重入造成数据混乱
         */
        if (lock.tryLock()) {
            try {

                //处理重复的通知
                //接口调用的幂等性:无论接口被调用多少次,以下业务只执行一次
                String orderStatus = orderInfoService.getOrderStatus(orderNo);
                if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {//如果不等于未支付
                    return;
                }

                //更新订单状态
                orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);

                //记录订单日志
                paymentInfoService.createPaymentInfoForAlipay(params);
            }finally {
                //释放锁
                lock.unlock();
            }
        }
    }

    /**
     * 用户取消支付宝订单
     * @param orderNo
     */
    @Override
    public void cancelOrder(String orderNo) {

        //调用支付宝提供的统一收单交易关闭接口
        this.closeOrder(orderNo);


        //更新用户订单状态
        orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CANCEL);
    }

    /**
     * 支付宝查询订单
     * @param orderNo
     * @return 返回订单查询结果 如果返回null则表示订单不存在
     */
    @Override
    public String queryOrder(String orderNo) {

        try {
            log.info("查询接口调用 === 》"+orderNo);

            AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
            JSONObject bizContent = new JSONObject();
            bizContent.put("out_trade_no", orderNo);
            request.setBizContent(bizContent.toString());

            AlipayTradeQueryResponse response = alipayClient.execute(request);
            if(response.isSuccess()){
                log.info("调用成功,返回结果 ===> " + response.getBody());
                return response.getBody();

            } else {
                log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
                //throw new RuntimeException("查询接口调用失败");//因为会出现只点击但未扫码,支付宝没有创建订单的情况,就会报错。所以直接取消。这样直接把这个订单改成取消
                return null;
            }
        } catch (AlipayApiException e) {
            e.printStackTrace();
            throw new RuntimeException("查询接口调用失败");
        }

    }

    /**
     * 根据订单号查询支付宝支付查单接口,核实订单状态
     * 如果订单未创建,则直接更新商户端订单状态
     * 如果订单已经支付,则更新商户端订单状态
     * 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
     * @param orderNo
     */
    @Override
    public void checkOrderStatus(String orderNo) {

        log.warn("根据订单号核实订单状态 ===》"+orderNo);

        String result = this.queryOrder(orderNo);

        //订单未创建
        if (result == null){
            log.warn("核实订单未创建 ===》"+orderNo);

            //更新本地订单状态
            orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CLOSED);
        }

        try {
            //解析查单响应结果
            Gson gson = new Gson();
            HashMap<String, LinkedTreeMap> resultMap = gson.fromJson(result, HashMap.class);
            LinkedTreeMap alipayTradeQueryResponse = resultMap.get("alipay_trade_query_response");
            String tradeStatus = alipayTradeQueryResponse.get("trade_status").toString();//获取订单状态

            //判断订单是否是未支付的订单
            if (AliPayTradeState.NOTPAY.getType().equals(tradeStatus)){
                log.info("未支付订单==== 》"+orderNo);

                //如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
                this.closeOrder(orderNo);

                //并更新商户端订单状态
                orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CLOSED);
                log.info("更改订单状态==== 》"+OrderStatus.CLOSED);

            }
            if(AliPayTradeState.SUCCESS.getType().equals(tradeStatus)) {
                log.warn("核实订单已支付 ===> {}", orderNo);

                //如果订单已支付,则更新商户端订单状态
                orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);

                //并记录支付日志
                paymentInfoService.createPaymentInfoForAlipay(alipayTradeQueryResponse);
            }
        } catch (NullPointerException e) {
            log.info("支付宝未创建订单,改为超时已关闭"+orderNo);
        }

    }

    /**
     * 关单接口的调用
     * @param orderNo
     */
    private void closeOrder(String orderNo) {

        try {
            log.info("关单接口的调用,订单号:"+orderNo);

            AlipayTradeCloseRequest request = new AlipayTradeCloseRequest();
            JSONObject bizContent = new JSONObject();
            bizContent.put("out_trade_no", orderNo);
            request.setBizContent(bizContent.toString());
            //调用关单接口
            AlipayTradeCloseResponse response = alipayClient.execute(request);


            if(response.isSuccess()){
                log.info("调用成功,返回结果 ===> " + response.getBody());

            } else {
                log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
//                throw new RuntimeException("关单接口调用失败");//因为会出现只点击但未扫码,支付宝没有创建订单的情况,就会报错。所以直接取消。这样直接把这个订单改成取消
            }
        } catch (AlipayApiException e) {
            e.printStackTrace();
            throw new RuntimeException("关单接口调用失败");
        }

    }


    /**
     * 退款
     * @param orderNo
     * @param reason
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void refund(String orderNo, String reason) {

        try {
            log.info("调用退款API");

            //创建退款单
            RefundInfo refundInfo = refundsInfoService.createRefundByOrderNoForAliPay(orderNo, reason);

            //调用统一收单交易退款接口
            AlipayTradeRefundRequest request = new AlipayTradeRefundRequest ();

            //组装当前业务方法的请求参数
            JSONObject bizContent = new JSONObject();
            bizContent.put("out_trade_no", orderNo);//订单编号
            BigDecimal refund = new BigDecimal(refundInfo.getRefund().toString()).divide(new BigDecimal("100"));
            //BigDecimal refund = new BigDecimal("2").divide(new BigDecimal("100"));
            bizContent.put("refund_amount", refund);//退款金额:不能大于支付金额
            bizContent.put("refund_reason", reason);//退款原因(可选)

            request.setBizContent(bizContent.toString());

            //执行请求,调用支付宝接口
            AlipayTradeRefundResponse response = alipayClient.execute(request);

            if(response.isSuccess()){
                log.info("调用成功,返回结果 ===> " + response.getBody());

                //更新订单状态
                orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);

                //更新退款单
                refundsInfoService.updateRefundForAliPay(
                        refundInfo.getRefundNo(),
                        response.getBody(),
                        AliPayTradeState.REFUND_SUCCESS.getType()); //退款成功

            } else {
                log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());

                //更新订单状态
                orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL);

                //更新退款单
                refundsInfoService.updateRefundForAliPay(
                        refundInfo.getRefundNo(),
                        response.getBody(),
                        AliPayTradeState.REFUND_ERROR.getType()); //退款失败
            }


        } catch (AlipayApiException e) {
            e.printStackTrace();
            throw new RuntimeException("创建退款申请失败");
        }
    }

    /**
     * 查询退款
     * @param orderNo
     * @return
     */
    @Override
    public String queryRefund(String orderNo) {

        try {
            log.info("查询退款接口调用 ===> {}", orderNo);

            AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest();
            JSONObject bizContent = new JSONObject();
            bizContent.put("out_trade_no", orderNo);
            bizContent.put("out_request_no", orderNo);
            request.setBizContent(bizContent.toString());

            AlipayTradeFastpayRefundQueryResponse response = alipayClient.execute(request);
            if(response.isSuccess()){
                log.info("调用成功,返回结果 ===> " + response.getBody());
                return response.getBody();
            } else {
                log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
                //throw new RuntimeException("查单接口的调用失败");
                return null;//订单不存在
            }

        } catch (AlipayApiException e) {
            e.printStackTrace();
            throw new RuntimeException("查单接口的调用失败");
        }
    }

    /**
     * 申请账单
     * @param billDate
     * @param type
     * @return
     */
    @Override
    public String queryBill(String billDate, String type) {

        try {

            AlipayDataDataserviceBillDownloadurlQueryRequest request = new AlipayDataDataserviceBillDownloadurlQueryRequest();
            JSONObject bizContent = new JSONObject();
            bizContent.put("bill_type", type);
            bizContent.put("bill_date", billDate);
            request.setBizContent(bizContent.toString());
            AlipayDataDataserviceBillDownloadurlQueryResponse response = alipayClient.execute(request);

            if(response.isSuccess()){
                log.info("调用成功,返回结果 ===> " + response.getBody());

                //获取账单下载地址
                Gson gson = new Gson();
                HashMap<String, LinkedTreeMap> resultMap = gson.fromJson(response.getBody(), HashMap.class);
                LinkedTreeMap billDownloadurlResponse = resultMap.get("alipay_data_dataservice_bill_downloadurl_query_response");
                String billDownloadUrl = (String)billDownloadurlResponse.get("bill_download_url");

                return billDownloadUrl;
            } else {
                log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
                throw new RuntimeException("申请账单失败");
            }

        } catch (AlipayApiException e) {
            e.printStackTrace();
            throw new RuntimeException("申请账单失败");
        }
    }

}

AliPayService

package com.wx.service;

import com.alipay.api.AlipayApiException;

import java.util.Map;

public interface AliPayService {
    String tradeCteate(Long productId);//统一收单下单支付页面接口调用

    void processOrder(Map<String, String> params);//处理业务,修改订单状态,记录支付日志

    void cancelOrder(String orderNo);//支付宝用户取消订单

    String queryOrder(String orderNo);//支付宝查询订单

    void checkOrderStatus(String orderNo);//处理超时的订单

    void refund(String orderNo, String reason);//申请退款

    String queryRefund(String orderNo);

    String queryBill(String billDate, String type);//下载账单
}

AliPayTask

package com.wx.task;

import com.google.gson.Gson;
import com.wx.entity.OrderInfo;
import com.wx.enums.PayType;
import com.wx.service.AliPayService;
import com.wx.service.OrderInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;

@Slf4j
@Component
public class AliPayTask {

    @Resource
    private OrderInfoService orderInfoService;

    @Resource
    private AliPayService aliPayService;

    /**
     * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单
     * @throws Exception
     */
    @Scheduled(cron = "0/30 * * * * ?")
    public void orderConfirm() throws Exception {
        log.info("支付宝定时任务启动====");

        List<OrderInfo> orderInfoList = orderInfoService.getNopayOrderByDuration(5, PayType.ALIPAY.getType());

        for (OrderInfo orderInfo : orderInfoList){
            String orderNo = orderInfo.getOrderNo();
            log.warn("超时订单 === > {}", orderNo);

            //核实订单状态:调用支付宝支付查单接口
            aliPayService.checkOrderStatus(orderNo);
        }


    }
}

支付宝信息总结

开放平台账号注册

常规接⼊流程

创建应⽤:选择应⽤类型、填写应⽤基本信息、添加应⽤功能、配置应⽤环境(获取⽀付宝公
钥、应⽤公钥、应⽤私钥、⽀付宝⽹关地址,配置接⼝内容加密⽅式)、查看 APPID
绑定应⽤:将开发者账号中的 APPID 和商家账号 PID 进⾏绑定
配置秘钥:即创建应⽤中的 配置应⽤环境 步骤
上线应⽤:将应⽤提交审核
签约功能:在商家中⼼上传营业执照、已备案⽹站信息等,提交审核进⾏签约

使⽤沙箱

沙箱环境配置: https://opendocs.alipay.com/common/02kkv7
沙箱版⽀付宝的下载和登录: https://open.alipay.com/platform/appDaily.htm?tab=tool

引⼊依赖

参考⽂档:开放平台 => ⽂档 => 开发⼯具 => 服务端 SDK => Java => 通⽤版 => Maven 项⽬依赖
https://search.maven.org/artifact/com.alipay.sdk/alipay-sdk-java

创建客⼾端连接对象

创建带数据签名的客⼾端对象
参考⽂档:开放平台 => ⽂档 => 开发⼯具 => 技术接⼊指南 => 数据签名 
https://opendocs.alipay.com/common/02kf5q
参考⽂档中 公钥方式 完善 AlipayClientConfig 类,添加 alipayClient() ⽅法 初始化 AlipayClient 对象

⽀付调⽤流程

https://opendocs.alipay.com/open/270/105899

接⼝说明

alipay.trade.page.pay(统一收单下单并支付页面接口)

https://opendocs.alipay.com/apis/028r8t?scene=22
公共请求参数:所有接⼝都需要的参数
请求参数:当前接⼝需要的参数
公共响应参数:所有接⼝的响应中都包含的数据
响应参数:当前接⼝的响应中包含的数据

内网穿透工具:

NATAPP -https://natapp.cn/login内网穿透教程:

NATAPP1分钟快速新手图文教程 - NATAPP-内网穿透 基于ngrok的国内高速内网映射工具https://natapp.cn/article/natapp_newbie

猜你喜欢

转载自blog.csdn.net/Java_Mr_Jin/article/details/125907192