Wechat payment project actual combat, detailed explanation of order creation to payment refund code

1. Product Introduction of WeChat Pay

 

WeChat payment development documents:

WeChat Pay - Developer Documentation (qq.com)

Source code address: both front-end and back-end

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

Extraction code: qwer 

1.1 , payment code payment

The user shows the " payment code " in the WeChat wallet to the merchant, and the merchant scans it and completes the payment directly, which is suitable for offline face-to-face cashier scenarios.

1.2 , JSAPI payment

Offline place: The merchant displays a payment QR code, and the user scans the QR code with WeChat, enters the amount to be paid, and completes the payment.
attached.
Official account scenario: The user enters the merchant’s official account in WeChat, opens a certain page, selects a product, and completes the payment.
PC website scenario: display the QR code on the website, the user scans the QR code with WeChat, enters the amount to be paid, and completes the payment.
attached.
Features: The user enters the payment amount on the client side

1.3 , mini program payment

The function of payment is realized in the WeChat applet platform.

1.4 , Native payment

Native payment refers to the mode in which the merchant displays the payment QR code, and the user uses WeChat to " scan " to complete the payment. This method is suitable for PC network
stand.
Features: The merchant pre-specifies the payment amount

1.5 , APP payment

The merchant completes the payment by integrating the WeChat payment module in the independent APP application on the mobile terminal.

1.6 . Face payment

It is a payment method in which the user scans his face through the camera and identifies his identity before swiping his face.

2. Access guide

2.1 , get the merchant number

WeChat merchant platform: https://pay.weixin.qq.com/
Scenario: Native payment
Steps: submit information => sign the agreement => obtain the merchant number

2.2 . Get APPID

One mode of APIV2 and APIV3

Steps: Log in to the Merchant Platform => Select Account Center => Security Center => API Security => Set API Key
Random password generation tool: https://suijimimashengcheng.bmcx.com/

2.5 、Apply for merchant API certificate

All interfaces of the APIv3 version are required; APIv2 version of the advanced interface is required (such as: refund, enterprise red envelope, enterprise payment, etc.)
Steps: Log in to the Merchant Platform => Select Account Center => Security Center => API Security => Apply for API Certificate

2.6 . Obtain WeChat platform certificate

It can be pre-downloaded or acquired programmatically. In the following courses, we will obtain it programmatically.
Note: All the above API keys and certificates need to be kept properly to prevent leakage

3. WeChat APIv3 certificate

Merchant certificate :
The merchant API certificate refers to the certificate applied by the merchant, including the merchant's merchant number, company name, and public key information.
Merchant certificate application in merchant background: https://pay.weixin.qq.com/index.php/core/cert/api_cert#/

Platform certificate (WeChat payment platform):
The WeChat Pay platform certificate refers to the certificate that WeChat Pay is responsible for applying for, including the WeChat Pay platform logo and public key information. Merchants can use
Use the public key in the platform certificate to verify the signature.
Obtaining platform certificate: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_0.shtml
Use the WeChat payment merchant platform certificate tool to generate
They are all encryption and decryption keys that need to be used for symmetric encryption, and must be kept well and cannot be disclosed.
The API key corresponds to the V2 version of the API
The APIv3 key corresponds to the V3 version of the API

 

 

project realization

rely

<?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>

configuration 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

database creation

order form

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;

Record Form

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;

Product list

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;

refund form

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;

Configure WeChat related payment parameters

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

Put the private key under the project

Configure the Util tool class

http request client tool class

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;
	}

}

Parameter conversion string tool class

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();
                }
            }
        }
    }
}

Order number tool class: we need to generate a number for our order

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;
    }

}

WeChat signature verification and response tools

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) : "";
    }

}

Create a unified result R class

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;
    }

}

Define entity class

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;//退款结果通知参数
}

define enumeration

For the convenience of development, we pre-define some enumerations in the project. The content defined in the enumeration includes interface address, payment status and other information

API interface address, which encapsulates all interfaces of WeChat payment

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;
}

Encapsulates the notification interface address

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;
}

refund 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;
}

Payment Types

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;
}

Payment status

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;
}

payment type

package com.wx.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

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


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

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

Define the configuration file of MyBatis-Plus

Create a configuration file MybatisPlusConfig in the config package
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 {
}

Define the Mapper layer

Inherit BaseMapper<>

Define the business layer

Define the business layer interface to inherit IService<>
Define the implementation class of the business layer interface and inherit ServiceImpl<,>

Only after the above configuration is completed can the business be implemented! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Business code implementation

Get product list interface

Add a method in the 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);
    }
}

result:

 Define read payment parameter configuration 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;
    }

}

Test to get payment parameters:

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 test acquisition

1. Introduce the SDK (the following configurations have been completed before. Explain here)

The preceding dependencies are complete and have been added

https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay6_0.shtml
We can use the official SDK to help us complete the development. The generation of the request signature and the verification of the response signature are realized.
<dependency>
 <groupId>com.github.wechatpay-apiv3</groupId> 
<artifactId>wechatpay-apache-httpclient</artifactId>
 <version>0.3.0</version> 
</dependency>

 2. Obtain the merchant's private key

https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (how to load merchant private key)
The department annotated in the above test is to obtain the merchant's private key

3. Get the signature verifier and HttpClient

Instructions for using the certificate key: It has been established in the above configuration, and I will explain it here again

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

 Get a signature verifier

https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (regular update platform certificate function)
Platform certificate: The platform certificate encapsulates the public key of WeChat, and the merchant can use the public key in the platform certificate to verify the signature.
Signature verifier: to help us perform signature verification, we define it separately to facilitate subsequent development.
Get the HttpClient object
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (regular update platform certificate function)
HttpClient object: It is the basis for establishing a remote connection, and we create this object through the SDK .

4. API dictionary and related tools

https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_3.shtml
In our project, we need to implement all the functions of the following APIs .

 interface rules

https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay2_0.shtml
WeChat Pay APIv3 uses JSON as the data exchange format for the message body.
    <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
        </dependency>

Business function interface code implementation

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;
    }
}

WeChat APP payment obtains signature time stamp

/**
     * 调用统一下单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;
    }

WeChat official account payment (JS-SDK permission signature algorithm)

get 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();
    }

Create a scheduled task to query the order status regularly

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);
        }
    }
}

test

 

Pay with Ali-Pay

AlipayClientConfig

Introduce the parameter file, obtain the parameters and assemble them into the client. It is convenient to call later

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

Alipay parameters: The sandbox mode is used here

# 支付宝支付相关参数

# 应用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

Payment Types

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);
        }


    }
}

Alipay information summary

Open platform account registration

Regular access process

Create an application : select the application type, fill in the basic information of the application, add application functions, configure the application environment (obtain Alipay public
key, application public key, application private key, Alipay gateway address, configure interface content encryption method), view APPID
Binding application : Bind the APPID in the developer account and the PID of the merchant account
Configure the secret key : that is, the " configure application environment " step in creating the application
Online application : Submit the application for review
Signing function : Upload business license, registered website information, etc. in the merchant center, submit for review and sign the contract

use sandbox

Sandbox environment configuration: https://opendocs.alipay.com/common/02kkv7
Sandbox Alipay download and login: https://open.alipay.com/platform/appDaily.htm?tab=tool

Introduce dependencies

Reference documentation: Open Platform => Documentation => Development Tools => Server SDK => Java => General Edition => Maven Project Dependencies
https://search.maven.org/artifact/com.alipay.sdk/alipay-sdk-java

Create a client connection object

Create a client object with a data signature
Reference documents: Open Platform => Documentation => Development Tools => Technical Access Guide => Data Signature 
https://opendocs.alipay.com/common/02kf5q
Refer to the public key to improve the AlipayClientConfig class and add the alipayClient() method to initialize the AlipayClient object

Payment call process

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

Interface description

alipay.trade.page.pay (unified receiving order and payment page interface)

https://opendocs.alipay.com/apis/028r8t?scene=22
Public request parameters: parameters required by all interfaces
Request parameters: parameters required by the current interface
Common response parameters: the data contained in the response of all interfaces
Response parameters: the data contained in the response of the current interface

Intranet penetration tool:

NATAPP - https://natapp.cn/login intranet penetration tutorial:

NATAPP 1-minute fast novice graphic tutorial - NATAPP-Intranet penetration A domestic high-speed intranet mapping tool based on ngrok https://natapp.cn/article/natapp_newbie

Guess you like

Origin blog.csdn.net/Java_Mr_Jin/article/details/125907192