接入微信支付,实现Native下单,附有源码仓库

接入微信支付

接入指引

获取商户号

微信商户平台:https://pay.weixin.qq.com/

场景:Native支付

步骤:提交资料 => 签署协议 => 获取商户

获取APPID

微信公众平台:https://mp.weixin.qq.com/

步骤:注册服务号 => 服务号认证 => 获取APPID => 绑定商户号

获取APIv3密钥

APIv3版本的接口需要此秘钥

步骤:登录商户平台 => 选择账户中心 => 安全中心 => API安全 => 设置APIv3密钥

随机密码生成工具:https://suijimimashengcheng.bmcx.com/

申请商户API证书

APIv3版本的所有接口都需要;APIv2版本的高级接口需要(如:退款、企业红包、企业付款等)

步骤:登录商户平台 => 选择账户中心 => 安全中心 => API安全 => 申请API证书 => 妥善管理

接入微信支付,实现Native下单,附有源码仓库_第1张图片

获取微信平台证书

通过编程动态获取,后续有教程

demo仓库地址

https://gitee.com/xzxwbb/wxpayment-demo

参考微信支付

【尚硅谷】微信支付&支付宝支付,一套搞定Java在线支付开发教程

demo中的私钥文件,商户的参数由上面的视频资料提供

加载商户私钥

将私钥文件复制到项目根目录下:

接入微信支付,实现Native下单,附有源码仓库_第2张图片

引入SDK

官方提供的SDK仓库,帮助我们完成开发。实现了请求签名的生成和应答签名的验证。


<dependency>
    <groupId>com.github.wechatpay-apiv3groupId>
    <artifactId>wechatpay-apache-httpclientartifactId>
    <version>0.4.8version>
dependency>

image-20220811160420542

接入微信支付,实现Native下单,附有源码仓库_第3张图片

# 示例:私钥存储在文件
        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
        new FileInputStream("/path/to/apiclient_key.pem"));

        # 示例:私钥为String字符串
        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
        new ByteArrayInputStream(privateKey.getBytes("utf-8")));

签名与验签流程

接入微信支付,实现Native下单,附有源码仓库_第4张图片

通过第三方的工具来实现商户私钥签名和微信公钥验签的复杂工作

image-20220811163459278

1、获取签名验证器

 	/**
     * 获取签名验证器
     *
     * @return
     */
    @Bean
    public Verifier getVerifier() throws Exception {
        // 获取证书管理器实例
        CertificatesManager certificatesManager = CertificatesManager.getInstance();
        // 向证书管理器增加需要自动更新平台证书的商户信息

        certificatesManager.putMerchant(
                mchId,
                new WechatPay2Credentials(mchId, new PrivateKeySigner(mchSerialNo, getPrivateKey(privateKeyPath))),
                //对称加密的密钥
                apiV3Key.getBytes(StandardCharsets.UTF_8));

        // ... 若有多个商户号,可继续调用putMerchant添加商户信息
        // 从证书管理器中获取verifier
        Verifier verifier = certificatesManager.getVerifier(mchId);
        return verifier;
    }

2、获取HttpClient对象

	/**
     * 获取 HttpClient 对象
     *
     * @param verifier 签名验证器
     * @return
     */
    @Bean(name = "wxPayClient")
    public CloseableHttpClient getWxPayClient(Verifier verifier) {
        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant(mchId, mchSerialNo, getPrivateKey(privateKeyPath))
                .withValidator(new WechatPay2Validator(verifier));
        // ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient

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

调用Native下单

接入微信支付,实现Native下单,附有源码仓库_第5张图片

生成二维码链接

  • 生成订单信息
/**
* 生成订单
* @param productId
* @return
*/
private OrderInfo generateOrderInfo(Long productId) {
    OrderInfo orderInfo = new OrderInfo();
    orderInfo.setTitle("test")
        .setOrderNo(OrderNoUtils.getOrderNo())
        .setProductId(productId)
        .setTotalFee(1)
        .setOrderStatus(OrderStatus.NOTPAY.getType());
    return orderInfo;
}
  • 生成json请求
/**
* 生成请求json
* @param orderInfo
* @return
*/
private String generateJsonParams(OrderInfo orderInfo) {
    Gson gson = new Gson();
    HashMap paramsMap = new HashMap();
    paramsMap.put("appid", wxPayConfig.getAppid());
    paramsMap.put("mchid", wxPayConfig.getMchId());
    paramsMap.put("description", orderInfo.getTitle());
    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);

    //将参数转换成json字符串
    return gson.toJson(paramsMap);
}
  • 生成HttpPost
/**
* 生成HttpPost
* @param jsonParams
* @return
*/
private HttpPost generateHttpPost(String jsonParams, String url) {
    //拼接请求URL
    HttpPost httpPost = new HttpPost(url);
    //设置请求头,编码集,内容格式
    StringEntity entity = new StringEntity(jsonParams, "utf-8");
    entity.setContentType("application/json");
    httpPost.setEntity(entity);
    httpPost.setHeader("Accept", "application/json");
    return httpPost;
}
  • 执行请求处理响应结果
String url = wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType());//Mative下单的url
HttpPost httpPost = generateHttpPost(jsonParams, url);
//完成签名并执行请求
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
        log.info("成功");
    } else {
        log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString);
        throw new IOException("request failed");
    }
    Gson gson = new Gson();
    Map<String, String> resultMap= gson.fromJson(bodyAsString, HashMap.class);
    String codeUrl = resultMap.get("code_url");

    Map<String, Object> map = new HashMap<>();
    map.put("codeUrl", codeUrl);
    map.put("orderNo", orderInfo.getOrderNo());
    return map;
} finally {
    response.close();
}
  • 前端接收二维码链接生成二维码
//package.json
"vue-qriously": "^1.1.1",
// main.js
import VueQriously from 'vue-qriously'
Vue.use(VueQriously)
//index.vue

避免重复创建订单,查询已存在的未过期的该用户未支付的同款商品的订单

	@Override
    public OrderInfo createOrderByProductId(Long productId) {
        OrderInfo orderInfo = getNoPayOrderByProductId(productId);
        if (orderInfo != null) {
            return orderInfo;
        }
        orderInfo = new OrderInfo();
        orderInfo.setTitle("test")
                .setOrderNo(OrderNoUtils.getOrderNo())
                .setProductId(productId)
                .setTotalFee(1)
                .setOrderStatus(OrderStatus.NOTPAY.getType());
        save(orderInfo);
        return orderInfo;
    }

    private OrderInfo getNoPayOrderByProductId(Long productId) {
        return lambdaQuery().eq(OrderInfo::getProductId, productId)
                .eq(OrderInfo::getOrderStatus, OrderStatus.NOTPAY.getType()).one();
    }
	log.info("生成订单");
    OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId);
    String codeUrl = orderInfo.getCodeUrl();
    if (!StringUtils.isBlank(codeUrl)) {
        log.info("订单已存在,二维码已保存");
        Map<String, Object> map = new HashMap<>();
        map.put("codeUrl", codeUrl);
        map.put("orderNo", orderInfo.getOrderNo());
        return map;
    }
	...
	//保存二维码
	orderInfoService.saveCodeUrl(orderInfo.getOrderNo(), codeUrl);
	Map<String, Object> map = new HashMap<>();

签名生成的原理

HTTP请求方法\n
URL\n
请求时间戳\n
请求随机串\n
请求报文主体\n

计算签名值

绝大多数编程语言提供的签名函数支持对签名数据进行签名。强烈建议商户调用该类函数,使用商户私钥对待签名串进行SHA256 with RSA签名,并对签名结果进行Base64编码得到签名值

$ echo -n -e \
"GET\n/v3/certificates\n1554208460\n593BEC0C930BF1AFEB40B4A08C8FB242\n\n" \
| openssl dgst -sha256 -sign apiclient_key.pem \
| openssl base64 -A

uOVRnA4qG/MNnYzdQxJanN+zU+lTgIcnU9BxGw5dKjK+VdEUz2FeIoC+D5sB/LN+nGzX3hfZg6r5wT1pl2ZobmIc6p0ldN7J6yDgUzbX8Uk3sD4a4eZVPTBvqNDoUqcYMlZ9uuDdCvNv4TM3c1WzsXUrExwVkI1XO5jCNbgDJ25nkT/c1gIFvqoogl7MdSFGc4W4xZsqCItnqbypR3RuGIlR9h9vlRsy7zJR9PBI83X8alLDIfR1ukt1P7tMnmogZ0cuDY8cZsd8ZlCgLadmvej58SLsIkVxFJ8XyUgx9FmutKSYTmYtWBZ0+tNvfGmbXU7cob8H/4nLBiCwIUFluw==

设置HTTP头

微信支付商户API v3要求请求通过HTTP Authorization头来传递签名。 Authorization认证类型签名信息两个部分组成。

下面我们使用命令行演示如何生成签名。

Authorization: 认证类型 签名信息

具体组成为:

1.认证类型,目前为WECHATPAY2-SHA256-RSA2048

2.签名信息

  • 发起请求的商户(包括直连商户、服务商或渠道商)的商户号 mchid
  • 商户API证书序列号serial_no,用于声明所使用的证书
  • 请求随机串nonce_str
  • 时间戳timestamp
  • 签名值signature
  • 注:以上五项签名信息,无顺序要求。

Authorization 头的示例如下:(注意,示例因为排版可能存在换行,实际数据应在一行)

Authorization: WECHATPAY2-SHA256-RSA2048 mchid="1900009191",nonce_str="593BEC0C930BF1AFEB40B4A08C8FB242",signature="uOVRnA4qG/MNnYzdQxJanN+zU+lTgIcnU9BxGw5dKjK+VdEUz2FeIoC+D5sB/LN+nGzX3hfZg6r5wT1pl2ZobmIc6p0ldN7J6yDgUzbX8Uk3sD4a4eZVPTBvqNDoUqcYMlZ9uuDdCvNv4TM3c1WzsXUrExwVkI1XO5jCNbgDJ25nkT/c1gIFvqoogl7MdSFGc4W4xZsqCItnqbypR3RuGIlR9h9vlRsy7zJR9PBI83X8alLDIfR1ukt1P7tMnmogZ0cuDY8cZsd8ZlCgLadmvej58SLsIkVxFJ8XyUgx9FmutKSYTmYtWBZ0+tNvfGmbXU7cob8H/4nLBiCwIUFluw==",timestamp="1554208460",serial_no="1DDE55AD98ED71D6EDD4A4A16996DE7B47773A8C"

最终我们可以组一个包含了签名的HTTP请求了。

$ curl https://api.mch.weixin.qq.com/v3/certificates -H 'Authorization: WECHATPAY2-SHA256-RSA2048 mchid="1900009191",nonce_str="593BEC0C930BF1AFEB40B4A08C8FB242",signature="uOVRnA4qG/MNnYzdQxJanN+zU+lTgIcnU9BxGw5dKjK+VdEUz2FeIoC+D5sB/LN+nGzX3hfZg6r5wT1pl2ZobmIc6p0ldN7J6yDgUzbX8Uk3sD4a4eZVPTBvqNDoUqcYMlZ9uuDdCvNv4TM3c1WzsXUrExwVkI1XO5jCNbgDJ25nkT/c1gIFvqoogl7MdSFGc4W4xZsqCItnqbypR3RuGIlR9h9vlRsy7zJR9PBI83X8alLDIfR1ukt1P7tMnmogZ0cuDY8cZsd8ZlCgLadmvej58SLsIkVxFJ8XyUgx9FmutKSYTmYtWBZ0+tNvfGmbXU7cob8H/4nLBiCwIUFluw==",timestamp="1554208460",serial_no="1DDE55AD98ED71D6EDD4A4A16996DE7B47773A8C"'

演示代码

计算签名的示例代码如下。

import okhttp3.HttpUrl;
import java.security.Signature;
import java.util.Base64;

// Authorization:  
// GET - getToken("GET", httpurl, "")
// POST - getToken("POST", httpurl, json)
String schema = "WECHATPAY2-SHA256-RSA2048";
HttpUrl httpurl = HttpUrl.parse(url);

String getToken(String method, HttpUrl url, String body) {
    String nonceStr = "your nonce string";
    long timestamp = System.currentTimeMillis() / 1000;
    String message = buildMessage(method, url, timestamp, nonceStr, body);
    String signature = sign(message.getBytes("utf-8"));

    return "mchid=\"" + yourMerchantId + "\","
    + "nonce_str=\"" + nonceStr + "\","
    + "timestamp=\"" + timestamp + "\","
    + "serial_no=\"" + yourCertificateSerialNo + "\","
    + "signature=\"" + signature + "\"";
}

String sign(byte[] message) {
    Signature sign = Signature.getInstance("SHA256withRSA");
    sign.initSign(yourPrivateKey);
    sign.update(message);

    return Base64.getEncoder().encodeToString(sign.sign());
}

String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) {
    String canonicalUrl = url.encodedPath();
    if (url.encodedQuery() != null) {
      canonicalUrl += "?" + url.encodedQuery();
    }

    return method + "\n"
        + canonicalUrl + "\n"
        + timestamp + "\n"
        + nonceStr + "\n"
        + body + "\n";
}              

如果您的请求返回了签名错误401 Unauthorized,请参考 常见问题之签名相关

签名验证的原理

如果验证商户的请求签名正确,微信支付会在应答的HTTP头部中包括应答签名。我们建议商户验证应答签名。

同样的,微信支付会在回调的HTTP头部中包括回调报文的签名。商户必须 验证回调的签名,以确保回调是由微信支付发送。

获取平台证书

微信支付API v3使用微信支付 的平台私钥(不是商户私钥 )进行应答签名。相应的,商户的技术人员应使用微信支付平台证书中的公钥验签。目前平台证书只提供API进行下载,请参考 获取平台证书列表。

检查平台证书序列号

微信支付的平台证书序列号位于HTTP头Wechatpay-Serial。验证签名前,请商户先检查序列号是否跟商户当前所持有的 微信支付平台证书的序列号一致。如果不一致,请重新获取证书。否则,签名的私钥和证书不匹配,将无法成功验证签名。

构造验签名串

首先,商户先从应答中获取以下信息。

  • HTTP头Wechatpay-Timestamp 中的应答时间戳。
  • HTTP头Wechatpay-Nonce 中的应答随机串。
  • 应答主体(response Body),需要按照接口返回的顺序进行验签,错误的顺序将导致验签失败。

然后,请按照以下规则构造应答的验签名串。签名串共有三行,行尾以\n 结束,包括最后一行。\n为换行符(ASCII编码值为0x0A)。若应答报文主体为空(如HTTP状态码为204 No Content),最后一行仅为一个\n换行符。

应答时间戳\n
应答随机串\n
应答报文主体\n      

如某个应答的HTTP报文为(省略了ciphertext的具体内容):

HTTP/1.1 200 OK
Server: nginx
Date: Tue, 02 Apr 2019 12:59:40 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2204
Connection: keep-alive
Keep-Alive: timeout=8
Content-Language: zh-CN
Request-ID: e2762b10-b6b9-5108-a42c-16fe2422fc8a
Wechatpay-Nonce: c5ac7061fccab6bf3e254dcf98995b8c
Wechatpay-Signature: CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA==
Wechatpay-Timestamp: 1554209980
Wechatpay-Serial: 5157F09EFDC096DE15EBE81A47057A7232F1B8E1
Cache-Control: no-cache, must-revalidate

{"data":[{"serial_no":"5157F09EFDC096DE15EBE81A47057A7232F1B8E1","effective_time":"2018-03-26T11:39:50+08:00","expire_time":"2023-03-25T11:39:50+08:00","encrypt_certificate":{"algorithm":"AEAD_AES_256_GCM","nonce":"4de73afd28b6","associated_data":"certificate","ciphertext":"..."}}]}
              

则验签名串为

1554209980
c5ac7061fccab6bf3e254dcf98995b8c
{"data":[{"serial_no":"5157F09EFDC096DE15EBE81A47057A7232F1B8E1","effective_time":"2018-03-26T11:39:50+08:00","expire_time":"2023-03-25T11:39:50+08:00","encrypt_certificate":{"algorithm":"AEAD_AES_256_GCM","nonce":"4de73afd28b6","associated_data":"certificate","ciphertext":"..."}}]}

获取应答签名

微信支付的应答签名通过HTTP头Wechatpay-Signature传递。(注意,示例因为排版可能存在换行,实际数据应在一行)

Wechatpay-Signature: CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA==

Wechatpay-Signature的字段值使用Base64进行解码,得到应答签名。

某些代理服务器或CDN服务提供商,转发时会“过滤”微信支付扩展的HTTP头,导致应用层无法取到微信支付的签名信息。商户遇到这种情况时,我们建议尝试调整代理服务器配置,或者通过直连的方式访问微信支付的服务器和接收通知。

验证签名

很多编程语言的签名验证函数支持对验签名串和签名 进行签名验证。强烈建议商户调用该类函数,使用微信支付平台公钥对验签名串和签名进行SHA256 with RSA签名验证。

下面展示使用命令行演示如何进行验签。假设我们已经获取了平台证书并保存为1900009191_wxp_cert.pem

首先,从微信支付平台证书导出微信支付平台公钥

$ openssl x509 -in 1900009191_wxp_cert.pem -pubkey -noout > 1900009191_wxp_pub.pem
$ cat 1900009191_wxp_pub.pem
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4zej1cqugGQtVSY2Ah8R
MCKcr2UpZ8Npo+5Ja9xpFPYkWHaF1Gjrn3d5kcwAFuHHcfdc3yxDYx6+9grvJnCA
2zQzWjzVRa3BJ5LTMj6yqvhEmtvjO9D1xbFTA2m3kyjxlaIar/RYHZSslT4VmjIa
tW9KJCDKkwpM6x/RIWL8wwfFwgz2q3Zcrff1y72nB8p8P12ndH7GSLoY6d2Tv0OB
2+We2Kyy2+QzfGXOmLp7UK/pFQjJjzhSf9jxaWJXYKIBxpGlddbRZj9PqvFPTiep
8rvfKGNZF9Q6QaMYTpTp/uKQ3YvpDlyeQlYe4rRFauH3mOE6j56QlYQWivknDX9V
rwIDAQAB
-----END PUBLIC KEY-----

Java支持使用证书初始化签名对象,详见 initVerify(Certificate),并不需要先导出公钥。

然后,把签名base64解码后保存为文件signature.txt

$ openssl base64 -d -A <<< \ 'CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA==' > signature.txt

最后,验证签名 验签名串进行摘要算法签名解码后用微信公钥解密的进行比对

签名生成:摘要->公钥加密->Base64编码

$ openssl dgst -sha256 -verify 1900009191_wxp_pub.pem -signature signature.txt << EOF
1554209980
c5ac7061fccab6bf3e254dcf98995b8c
{"data":[{"serial_no":"5157F09EFDC096DE15EBE81A47057A7232F1B8E1","effective_time":"2018-03-26T11:39:50+08:00","expire_time":"2023-03-25T11:39:50+08:00","encrypt_certificate":{"algorithm":"AEAD_AES_256_GCM","nonce":"d215b0511e9c","associated_data":"certificate","ciphertext":"..."}}]}
EOF
Verified OK

配置内网穿透

设置authToken

ngrok config add-authtoken 2DEjRSBgc9J5A1oVKs2SU9kZ1h5_4B9NJUkDFACvK218GrESM

启动服务

ngrok http 8090

测试外网访问

你获得的外网地址/api/test

Linux系统命令前要加上./

支付通知API

接入微信支付,实现Native下单,附有源码仓库_第6张图片

1、接口

	@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);
            log.info("支付通知的完整数据 ===> {}", body);
            //验签和解析请求体并解密报文
            String decryptData = notificationHandlerUtils.getDecryptData(request, body);
            log.info("支付通知解密完的数据 ===> {}", decryptData);

            //对订单进行处理
            wxPayService.processOrder(decryptData);

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

2、* 对请求体进行验签和解析,解密获得报文

@Component
public class NotificationHandlerUtils {

    @Resource
    private Verifier verifier;
    @Resource
    private WxPayConfig wxPayConfig;

    /**
     * 验签request并解析加密数据获取订单的详情
     * @param request
     * @param body
     * @return
     */
    public String getDecryptData(HttpServletRequest request, String body) {
        String serial = request.getHeader(WECHAT_PAY_SERIAL);
        String nonce = request.getHeader(WECHAT_PAY_NONCE);
        String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
        String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
        // 构建request,传入必要参数
        NotificationRequest req = new NotificationRequest.Builder()
                .withSerialNumber(serial)
                .withNonce(nonce)
                .withTimestamp(timestamp)
                .withSignature(signature)
                .withBody(body)
                .build();
        NotificationHandler handler = new NotificationHandler(verifier, wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        // 验签和解析请求体
        Notification notification = null;
        try {
            notification = handler.parse(req);
        } catch (ValidationException e) {
            throw new RuntimeException("验签失败");
        } catch (ParseException e) {
            throw new RuntimeException("解析失败");
        }
        // 从notification中获取解密报文
        return notification.getDecryptData();
    }
}

3、处理订单

	/**
     * 处理订单
     *
     * @param decryptData 解密后的报文
     */
    @Override
    public void processOrder(String decryptData) {
        Gson gson = new Gson();
        Map<String, Object> decryptDataMap = gson.fromJson(decryptData, HashMap.class);
        String orderNo = (String) decryptDataMap.get("out_trade_no");
        /*在对业务数据进行状态检查和处理之前,
        要采用数据锁进行并发控制,
        以避免函数重入造成的数据混乱*/
        //尝试获取锁:
        // 成功获取则立即返回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(decryptData);
            } finally {
                lock.unlock();
            }
        }

    }

4、更新订单状态

    @Override
    public void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus) {
        log.info("更新订单状态 ===> {}", orderStatus.getType());
        LambdaQueryWrapper<OrderInfo> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(OrderInfo::getOrderNo, orderNo);
        OrderInfo orderInfo = new OrderInfo().setOrderStatus(orderStatus.getType());
        update(orderInfo, wrapper);
    }

5、记录支付日志

	@Override
    public void createPaymentInfo(String decryptData) {
        log.info("记录支付日志");
        Gson gson = new Gson();
        Map<String, Object> decryptDataMap = gson.fromJson(decryptData, HashMap.class);
        String orderNo = (String)decryptDataMap.get("out_trade_no"); 
        String transactionId = (String)decryptDataMap.get("transaction_id"); 
        String tradeType = (String)decryptDataMap.get("trade_type"); 
        String tradeState = (String)decryptDataMap.get("trade_state"); 
        Map<String, Object> amount = (Map)decryptDataMap.get("amount"); 
        Integer payerTotal = ((Double) amount.get("payer_total")).intValue();

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

关闭订单

接口说明

适用对象: 直连商户

请求URL: https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{out_trade_no}/close

请求方式: POST

path指该参数为路径参数

query指该参数需在请求URL传参

body指该参数需在请求JSON传参

请求参数

参数名 变量 类型[长度限制] 必填 描述
直连商户号 mchid string[1,32] body 直连商户的商户号,由微信支付生成并下发。 示例值:1230000109
商户订单号 out_trade_no string[6,32] path 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一 示例值:1217752501201407033233368018

请求示例

{
	"mchid": "1230000109"
}

接口实现

@ApiOperation("用户取消订单")
@PostMapping("/cancel/{orderNo}")
public R cancel(@PathVariable String orderNo) throws IOException {
    log.info("取消订单");
    wxPayService.cancelOrder(orderNo);
    return R.ok().setMessage("订单已取消");
}
	@Override
    public void cancelOrder(String orderNo) throws IOException {
        //调用微信支付的关单接口 
        closeOrder(orderNo);
        // 更新商户端的订单状态
        orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL);
    }

    private void closeOrder(String orderNo) throws IOException {
        log.info("关单接口的调用,订单号 ===> {}", orderNo);
        //创建远程请求对象
        String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo);
        url = wxPayConfig.getDomain().concat(url);//取消订单url
        //组装json请求体
        Gson gson = new Gson();
        Map<String, String> paramsMap = new HashMap();
        paramsMap.put("mchid", wxPayConfig.getMchId());
        //将参数转换成json字符串
        String jsonParams = gson.toJson(paramsMap);
        HttpPost httpPost = generateHttpPost(jsonParams, url);
        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpPost);
        int statusCode = response.getStatusLine().getStatusCode();
        try {
            if (statusCode == 200) { //处理成功
                log.info("成功200");
            } else if (statusCode == 204) { //处理成功,无返回Body
                log.info("成功204");
            } else {
                log.info("取消订单失败,响应码 = " + statusCode);
                throw new IOException("request failed");
            }
        } finally {
            response.close();
        }
    }

微信支付查单API

1、微信支付订单号查询

接口说明

适用对象: 直连商户

请求URL: https://api.mch.weixin.qq.com/v3/pay/transactions/id/{transaction_id}

**请求方式:**GET

path指该参数为路径参数

query指该参数需在请求URL传参

body指该参数需在请求JSON传参

请求参数

参数名 变量 类型[长度限制] 必填 描述
直连商户号 mchid string[1,32] query 直连商户的商户号,由微信支付生成并下发。 示例值:1230000109
微信支付订单号 transaction_id string[1,32] path 微信支付系统生成的订单号 示例值:1217752501201407033233368018

查询订单

2、商户订单号查询

接口说明

适用对象: 直连商户

请求URL: https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{out_trade_no}

**请求方式:**GET

path指该参数为路径参数

query指该参数需在请求URL传参

body指该参数需在请求JSON传参

请求参数

参数名 变量 类型[长度限制] 必填 描述
直连商户号 mchid string[1,32] query 直连商户的商户号,由微信支付生成并下发。 示例值:1230000109
商户订单号 out_trade_no string[6,32] path 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一。 特殊规则:最小字符长度为6 示例值:1217752501201407033233368018

请求示例

  • [URL](javascript:
https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/1217752501201407033233368018?mchid=1230000109

返回参数

参数名 变量 类型[长度限制] 必填 描述
应用ID appid string[1,32] 直连商户申请的公众号或移动应用appid。 示例值:wxd678efh567hg6787
直连商户号 mchid string[1,32] 直连商户的商户号,由微信支付生成并下发。 示例值:1230000109
商户订单号 out_trade_no string[6,32] 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一,详见【商户订单号】。 示例值:1217752501201407033233368018
微信支付订单号 transaction_id string[1,32] 微信支付系统生成的订单号。 示例值:1217752501201407033233368018
交易类型 trade_type string[1,16] 交易类型,枚举值: JSAPI:公众号支付 NATIVE:扫码支付 APP:APP支付 MICROPAY:付款码支付 MWEB:H5支付 FACEPAY:刷脸支付 示例值:MICROPAY
交易状态 trade_state string[1,32] 交易状态,枚举值: SUCCESS:支付成功 REFUND:转入退款 NOTPAY:未支付 CLOSED:已关闭 REVOKED:已撤销(仅付款码支付会返回) USERPAYING:用户支付中(仅付款码支付会返回) PAYERROR:支付失败(仅付款码支付会返回) 示例值:SUCCESS
交易状态描述 trade_state_desc string[1,256] 交易状态描述 示例值:支付成功
付款银行 bank_type string[1,32] 银行类型,采用字符串类型的银行标识。银行标识请参考《银行类型对照表》 示例值:CMC
附加数据 attach string[1,128] 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用,实际情况下只有支付完成状态才会返回该字段。 示例值:自定义数据
支付完成时间 success_time string[1,64] 支付完成时间,遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE,yyyy-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示,北京时间2015年5月20日 13点29分35秒。 示例值:2018-06-08T10:34:56+08:00
+支付者 payer object 支付者信息
+订单金额 amount object 订单金额信息,当支付成功时返回该字段。
+场景信息 scene_info object 支付场景描述
+优惠功能 promotion_detail array 优惠功能,享受优惠时返回该字段。

返回示例

  • [正常示例](javascript:
{
	"amount": {
		"currency": "CNY",
		"payer_currency": "CNY",
		"payer_total": 1,
		"total": 1
	},
	"appid": "wxdace645e0bc2cXXX",
	"attach": "",
	"bank_type": "OTHERS",
	"mchid": "1900006XXX",
	"out_trade_no": "44_2126281063_5504",
	"payer": {
		"openid": "o4GgauJP_mgWEWictzA15WT15XXX"
	},
	"promotion_detail": [],
	"success_time": "2021-03-22T10:29:05+08:00",
	"trade_state": "SUCCESS",
	"trade_state_desc": "支付成功",
	"trade_type": "JSAPI",
	"transaction_id": "4200000891202103228088184743"
}

接口实现

	/**
     * 根据订单号查询微信支付查单接口
     * 响应体的明文
     * @param orderNo
     * @return
     * @throws IOException
     */
    @Override
    public String queryOrder(String orderNo) throws IOException {
        log.info("查单接口调用 ===> {}", orderNo);
        //组装请求URL
        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
                log.info("成功");
            } else {
                log.info("查询订单失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString);
                throw new IOException("request failed");
            }
            return bodyAsString;
        } finally {
            response.close();
        }
    }

定时任务核实超时未支付的订单

1、定时任务创建

@Component
@Slf4j
public class WxPayTask {

    @Resource
    private OrderInfoService orderInfoService;
    @Resource
    private WxPayService wxPayService;

    /**
     * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单
     */
    @Scheduled(cron = "0/30 * * * * ?")
    public void orderConfirm() throws IOException {
        log.info("确认超时订单的状态...");
        //获取超时未支付的订单
        List<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDurations(5);

        for (OrderInfo orderInfo : orderInfoList) {
            String orderNo = orderInfo.getOrderNo();
            log.warn("超时订单 ===> {}", orderNo);
            //核实订单状态:调用微信支付主动查询订单
            wxPayService.checkOrderStatus(orderNo);
        }
    }
}

2、查找超过5分钟未支付的订单

	/**
     * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单
     *
     * @param minutes
     * @return
     */
    @Override
    public List<OrderInfo> getNoPayOrderByDurations(int minutes) {
        //minutes分钟之前的时间
        Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));
        List<OrderInfo> orderInfoList = lambdaQuery().eq(OrderInfo::getOrderStatus, OrderStatus.NOTPAY.getType())
                .le(OrderInfo::getCreateTime, instant)
                .list();
        return orderInfoList;
    }

3、调用查单接口更新订单状态

	/**
     * 根据订单号查询微信支付查单接口,核实订单状态
     * 如果订单已支付,则更新商户端订单状态,并记录支付日志
     * 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
     *
     * @param orderNo 创建5分钟且未支付的订单号
     */
    @Override
    public void checkOrderStatus(String orderNo) throws IOException {
        log.warn("根据订单号核实订单状态 ===> {}", orderNo);
        Gson gson = new Gson();
        //查单
        String bodyAsString = queryOrder(orderNo);
        Map<String, Object> bodyMap = gson.fromJson(bodyAsString, HashMap.class);
        String trade_state = (String) bodyMap.get("trade_state");
        if (WxTradeState.SUCCESS.getType().equals(trade_state)) {
            log.warn("核实订单已支付 ===> {}", orderNo);
            //如果确认订单已支付则更新本地订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
            //记录支付日志
            paymentInfoService.createPaymentInfo(bodyAsString);
        } else if (WxTradeState.NOTPAY.getType().equals(trade_state)) {
            log.warn("核实订单未支付 ===> {}", orderNo);
            //关单接口
            closeOrder(orderNo);
            //更新为超时关闭
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);
        }
    }

微信支付退款

申请退款API

状态机

退款状态转变如下:

接入微信支付,实现Native下单,附有源码仓库_第7张图片

接口说明

**适用对象:**直连商户

**请求URL:**https://api.mch.weixin.qq.com/v3/refund/domestic/refunds

**请求方式:**POST

**接口频率:**150qps

path 指该参数为路径参数

query 指该参数为URL参数

body 指该参数需在请求JSON传参

请求参数

参数名 变量 类型[长度限制] 必填 描述
微信支付订单号 transaction_id string[1, 32] 二选一 body原支付交易对应的微信订单号 示例值:1217752501201407033233368018
商户订单号 out_trade_no string[6, 32] body原支付交易对应的商户订单号 示例值:1217752501201407033233368018
商户退款单号 out_refund_no string[1, 64] body商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。 示例值:1217752501201407033233368018
退款原因 reason string[1, 80] body若商户传入,会在下发给用户的退款消息中体现退款原因 示例值:商品已售完
退款结果回调url notify_url string[8, 256] body异步接收微信支付退款结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 如果参数中传了notify_url,则商户平台上配置的回调地址将不会生效,优先回调当前传的这个地址。 示例值:https://weixin.qq.com
退款资金来源 funds_account string[1,32] body若传递此参数则使用对应的资金账户退款,否则默认使用未结算资金退款(仅对老资金流商户适用) 枚举值: AVAILABLE:可用余额账户 示例值:AVAILABLE
+金额信息 amount object body订单金额信息
+退款商品 goods_detail array body指定商品退款需要传此参数,其他场景无需传递
请求示例
  • [JSON](javascript:
{
  "transaction_id": "1217752501201407033233368018",
  "out_refund_no": "1217752501201407033233368018",
  "reason": "商品已售完",
  "notify_url": "https://weixin.qq.com",
  "funds_account": "AVAILABLE",
  "amount": {
    "refund": 888,
    "from": [
      {
        "account": "AVAILABLE",
        "amount": 444
      }
    ],
    "total": 888,
    "currency": "CNY"
  },
  "goods_detail": [
    {
      "merchant_goods_id": "1217752501201407033233368018",
      "wechatpay_goods_id": "1001",
      "goods_name": "iPhone6s 16G",
      "unit_price": 528800,
      "refund_amount": 528800,
      "refund_quantity": 1
    }
  ]
}

返回参数

参数名 变量 类型[长度限制] 必填 描述
微信支付退款单号 refund_id string[1, 32] 微信支付退款单号 示例值:50000000382019052709732678859
商户退款单号 out_refund_no string[1, 64] 商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。 示例值:1217752501201407033233368018
微信支付订单号 transaction_id string[1, 32] 微信支付交易订单号 示例值:1217752501201407033233368018
商户订单号 out_trade_no string[1, 32] 原支付交易对应的商户订单号 示例值:1217752501201407033233368018
退款渠道 channel string[1, 16] 枚举值: ORIGINAL:原路退款 BALANCE:退回到余额 OTHER_BALANCE:原账户异常退到其他余额账户 OTHER_BANKCARD:原银行卡异常退到其他银行卡 示例值:ORIGINAL
退款入账账户 user_received_account string[1, 64] 取当前退款单的退款入账方,有以下几种情况: 1)退回银行卡:{银行名称}{卡类型}{卡尾号} 2)退回支付用户零钱:支付用户零钱 3)退还商户:商户基本账户商户结算银行账户 4)退回支付用户零钱通:支付用户零钱通 示例值:招商银行信用卡0403
退款成功时间 success_time string[1, 64] 退款成功时间,当退款状态为退款成功时有返回。 示例值:2020-12-01T16:18:12+08:00
退款创建时间 create_time string[1, 64] 退款受理时间 示例值:2020-12-01T16:18:12+08:00
退款状态 status string[1, 32] 退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台-交易中心,手动处理此笔退款。 枚举值: SUCCESS:退款成功 CLOSED:退款关闭 PROCESSING:退款处理中 ABNORMAL:退款异常 示例值:SUCCESS
资金账户 funds_account string[1, 32] 退款所使用资金对应的资金账户类型 枚举值: UNSETTLED : 未结算资金 AVAILABLE : 可用余额 UNAVAILABLE : 不可用余额 OPERATION : 运营户 BASIC : 基本账户(含可用余额和不可用余额) 示例值:UNSETTLED
+金额信息 amount object 金额详细信息
+优惠退款信息 promotion_detail array 优惠退款信息
返回示例
  • [正常示例](javascript:
{
  "refund_id": "50000000382019052709732678859",
  "out_refund_no": "1217752501201407033233368018",
  "transaction_id": "1217752501201407033233368018",
  "out_trade_no": "1217752501201407033233368018",
  "channel": "ORIGINAL",
  "user_received_account": "招商银行信用卡0403",
  "success_time": "2020-12-01T16:18:12+08:00",
  "create_time": "2020-12-01T16:18:12+08:00",
  "status": "SUCCESS",
  "funds_account": "UNSETTLED",
  "amount": {
    "total": 100,
    "refund": 100,
    "from": [
      {
        "account": "AVAILABLE",
        "amount": 444
      }
    ],
    "payer_total": 90,
    "payer_refund": 90,
    "settlement_refund": 100,
    "settlement_total": 100,
    "discount_refund": 10,
    "currency": "CNY"
  },
  "promotion_detail": [
    {
      "promotion_id": "109519",
      "scope": "SINGLE",
      "type": "DISCOUNT",
      "amount": 5,
      "refund_amount": 100,
      "goods_detail": [
	    {
			"merchant_goods_id": "1217752501201407033233368018",
			"wechatpay_goods_id": "1001",
			"goods_name": "iPhone6s 16G",
			"unit_price": 528800,
			"refund_amount": 528800,
			"refund_quantity": 1
		}
      ]
    }
  ]
}

* 实现退款

1、创建退款订单

	/**
     * 创建退款订单
     * @param orderNo
     * @param reason
     * @return
     */
    @Override
    public RefundInfo createRefundByOrderNo(String orderNo, String reason) {
        OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);
        RefundInfo refundInfo = new RefundInfo();
        refundInfo.setOrderNo(orderNo)
                .setRefundNo(OrderNoUtils.getRefundNo())
                .setTotalFee(orderInfo.getTotalFee())//原金额
                .setRefund(orderInfo.getTotalFee())//退款金额
                .setReason(reason);

        save(refundInfo);
        return refundInfo;
    }

	@Override
    public OrderInfo getOrderByOrderNo(String orderNo) {
        return lambdaQuery().eq(OrderInfo::getOrderNo, orderNo).one();
    }

2、组装Json请求体

	private String generateJsonParamsOfRefund(RefundInfo refundInfo) {
        Gson gson = new Gson();
        Map<String, Object> paramsMap = new HashMap<>();
        paramsMap.put("out_trade_no", refundInfo.getOrderNo());//商品订单号
        paramsMap.put("out_refund_no", refundInfo.getRefundNo());//退款单号
        paramsMap.put("reason", refundInfo.getReason());
        paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));
        Map<String, Object> amountMap = new HashMap<>();
        amountMap.put("refund", refundInfo.getRefund());//退款金额
        amountMap.put("total", refundInfo.getTotalFee());//原订单金额
        amountMap.put("currency", "CNY");
        paramsMap.put("amount", amountMap);
        //将参数转换成json字符串
        return gson.toJson(paramsMap);
    }

3、调用申请退款接口

	/**
     * 申请退款
     *
     * @param orderNo
     * @param reason
     */
    @Override
    public void refund(String orderNo, String reason) throws IOException {
        log.info("创建退款单记录");
        RefundInfo refundInfo = refundInfoService.createRefundByOrderNo(orderNo, reason);
        log.info("调用退款API");
        String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
        //json请求体
        String jsonParams = generateJsonParamsOfRefund(refundInfo);
        HttpPost httpPost = generateHttpPost(jsonParams, url);
        //完成签名并执行请求
        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
                log.info("成功");
            } else {
                throw new RuntimeException("退款失败,响应码 = " + statusCode + ",退款返回结果 = " + bodyAsString);
            }
            //更新订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING);
            //更新退款单
            refundInfoService.updateRefund(bodyAsString);
        } finally {
            response.close();
        }
    }

4、* 记录响应体或者请求体的明文数据到数据库中

	/**
     * 记录退款记录
     * @param content
     */
    @Override
    public void updateRefund(String content) {
        //将json字符串转换成Map
        Gson gson = new Gson();
        Map<String, String> resultMap = gson.fromJson(content, HashMap.class);
        //根据退款单编号修改退款单
        LambdaQueryWrapper<RefundInfo> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(RefundInfo::getRefundNo, resultMap.get("out_refund_no"));

        RefundInfo refundInfo = new RefundInfo();
        refundInfo.setRefundId(resultMap.get("refund_id"));
        //1、查询退款和申请退款中的返回参数
        if (resultMap.get("status") != null) {
            refundInfo.setRefundStatus(resultMap.get("status"));
            refundInfo.setContentReturn(content);
        }
        //2、退款回调中的回调参数
        if (resultMap.get("refund_status") != null) {
            refundInfo.setRefundStatus(resultMap.get("refund_status"));
            refundInfo.setContentNotify(content);
        }
        update(refundInfo,wrapper);
    }

查询退款API

接口说明

**适用对象:**直连商户

**请求URL:**https://api.mch.weixin.qq.com/v3/refund/domestic/refunds/{out_refund_no}

**请求方式:**GET

path 指该参数为路径参数

query 指该参数为URL参数

body 指该参数需在请求JSON传参

请求参数

参数名 变量 类型[长度限制] 必填 描述
商户退款单号 out_refund_no string[1, 64] path商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。 示例值:1217752501201407033233368018
请求示例
  • [URL](javascript:
https://api.mch.weixin.qq.com/v3/refund/domestic/refunds/1217752501201407033233368018

返回参数

参数名 变量 类型[长度限制] 必填 描述
微信支付退款单号 refund_id string[1, 32] 微信支付退款单号 示例值:50000000382019052709732678859
商户退款单号 out_refund_no string[1, 64] 商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。 示例值:1217752501201407033233368018
微信支付订单号 transaction_id string[1, 32] 微信支付交易订单号 示例值:1217752501201407033233368018
商户订单号 out_trade_no string[1, 32] 原支付交易对应的商户订单号 示例值:1217752501201407033233368018
退款渠道 channel string[1, 16] 枚举值: ORIGINAL:原路退款 BALANCE:退回到余额 OTHER_BALANCE:原账户异常退到其他余额账户 OTHER_BANKCARD:原银行卡异常退到其他银行卡 示例值:ORIGINAL
退款入账账户 user_received_account string[1, 64] 取当前退款单的退款入账方,有以下几种情况: 1)退回银行卡:{银行名称}{卡类型}{卡尾号} 2)退回支付用户零钱:支付用户零钱 3)退还商户:商户基本账户商户结算银行账户 4)退回支付用户零钱通:支付用户零钱通 示例值:招商银行信用卡0403
退款成功时间 success_time string[1, 64] 退款成功时间,当退款状态为退款成功时有返回。 示例值:2020-12-01T16:18:12+08:00
退款创建时间 create_time string[1, 64] 退款受理时间 示例值:2020-12-01T16:18:12+08:00
退款状态 status string[1, 32] 款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台-交易中心,手动处理此笔退款。 枚举值: SUCCESS:退款成功 CLOSED:退款关闭 PROCESSING:退款处理中 ABNORMAL:退款异常 示例值:SUCCESS
资金账户 funds_account string[1, 32] 退款所使用资金对应的资金账户类型 枚举值: UNSETTLED : 未结算资金 AVAILABLE : 可用余额 UNAVAILABLE : 不可用余额 OPERATION : 运营户 BASIC : 基本账户(含可用余额和不可用余额) 示例值:UNSETTLED
+金额信息 amount object 金额详细信息
+优惠退款信息 promotion_detail array 优惠退款信息
返回示例
  • [正常示例](javascript:
{
  "refund_id": "50000000382019052709732678859",
  "out_refund_no": "1217752501201407033233368018",
  "transaction_id": "1217752501201407033233368018",
  "out_trade_no": "1217752501201407033233368018",
  "channel": "ORIGINAL",
  "user_received_account": "招商银行信用卡0403",
  "success_time": "2020-12-01T16:18:12+08:00",
  "create_time": "2020-12-01T16:18:12+08:00",
  "status": "SUCCESS",
  "funds_account": "UNSETTLED",
  "amount": {
    "total": 100,
    "refund": 100,
    "from": [
      {
        "account": "AVAILABLE",
        "amount": 444
      }
    ],
    "payer_total": 90,
    "payer_refund": 90,
    "settlement_refund": 100,
    "settlement_total": 100,
    "discount_refund": 10,
    "currency": "CNY"
  },
  "promotion_detail": [
    {
      "promotion_id": "109519",
      "scope": "SINGLE",
      "type": "DISCOUNT",
      "amount": 5,
      "refund_amount": 100,
      "goods_detail": [
 		{
			"merchant_goods_id": "1217752501201407033233368018",
			"wechatpay_goods_id": "1001",
			"goods_name": "iPhone6s 16G",
			"unit_price": 528800,
			"refund_amount": 528800,
			"refund_quantity": 1
       }
		]
    }
  ]
}

* 实现查询退款

1、创建定时任务

	/**
     * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未成功的退款单
     */
    @Scheduled(cron = "0/30 * * * * ?")
    public void refundConfirm() throws IOException {
        log.info("确认超时退款订单的状态...");
        //获取超时退款仍在处理中的订单
        List<RefundInfo> RefundInfoList = refundInfoService.getNoPayOrderByDurations(5);

        for (RefundInfo refundInfo : RefundInfoList) {
            String refundNo = refundInfo.getRefundNo();
            log.warn("超时未退款的退款单号 ===> {}", refundNo);

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

	/**
     * 找出申请退款超过minutes分钟并且未成功的退款单
     * @param minutes
     * @return
     */
    @Override
    public List<RefundInfo> getNoPayOrderByDurations(int minutes) {
        //minutes分钟之前的时间
        Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));
        List<RefundInfo> RefundInfoList = lambdaQuery().eq(RefundInfo::getRefundStatus, WxRefundStatus.PROCESSING.getType())
                .le(RefundInfo::getCreateTime, instant)
                .list();
        return RefundInfoList;
    }

2、核实退款仍在处理中的退款单

	/**
     * 1.调用查询退款记录接口
     * 2.更新数据库
     * @param refundNo
     * @throws IOException
     */
    @Override
    public void checkRefundStatus(String refundNo) throws IOException {
        log.warn("根据退款订单号核实退款订单状态 ===> {}", refundNo);
        Gson gson = new Gson();
        //查询退款订单
        String bodyAsString = queryRefund(refundNo);
        Map<String, String> bodyMap = gson.fromJson(bodyAsString, HashMap.class);
        String status = bodyMap.get("status");
        String orderNo = bodyMap.get("out_trade_no");
        if (WxRefundStatus.SUCCESS.getType().equals(status)) {
            log.warn("核实订单退款成功 ===> {}", refundNo);
            //如果确认订单已退款成功则更新本地订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
        } else if (WxRefundStatus.ABNORMAL.getType().equals(status)) {
            log.warn("核实订单退款异常 ===> {}", refundNo);
            //如果确认订单退款异常则更新本地订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL);
        }
        //保存响应体到数据库中
        refundInfoService.updateRefund(bodyAsString);
    }

3、调用API查询退款单

	/**
     * 调用查询退款单接口,核实订单状态
     * @param refundNo
     * @return
     * @throws IOException
     */
    @Override
    public String queryRefund(String refundNo) throws IOException {
        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) { //处理成功,无返回Body
                log.info("成功");
            } else {
                log.info("查询退款失败,响应码 = " + statusCode + ",查询退款返回结果 = " + bodyAsString);
                throw new IOException("request failed");
            }
            return bodyAsString;
        } finally {
            response.close();
        }

    }

退款结果通知

接口说明

**适用对象:**直连商户

**请求方式:**POST

**请求URL:**该链接是通过[申请退款接口]指定的notify_url,必须为https协议。如果链接无法访问,商户将无法接收到微信通知。 通知url必须为直接可访问的url,不能携带参数。示例:“https://pay.weixin.qq.com/wxpay/pay.action”

通知规则

商户退款完成后,微信会把相关退款结果和用户信息发送给清算机构,清算机构需要接收处理后返回应答成功,然后继续给异步通知到下游从业机构。

对后台通知交互时,如果微信收到应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。(通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m)

通知报文

退款结果通知是以POST 方法访问商户设置的通知url,通知的数据以JSON 格式通过请求主体(BODY)传输。通知的数据包括了加密的支付结果详情
(注:由于涉及到回调加密和解密,商户必须先设置好apiv3密钥后才能解密回调通知,apiv3密钥设置文档指引详见APIv3密钥设置指引)

参数解密

下面详细描述对通知数据进行解密的流程:

  1. 1、用商户平台上设置的APIv3密钥 【微信商户平台—>账户设置—>API安全—>设置APIv3密钥】 ,记为key;
  2. 2、针对resource.algorithm中描述的算法(目前为AEAD_AES_256_GCM),取得对应的参数nonce和associated_data;
  3. 3、使用key、nonce和associated_data,对数据密文resource.ciphertext进行解密,得到JSON形式的资源对象。

注: AEAD_AES_256_GCM算法的接口细节,请参考rfc5116。微信支付使用的密钥key长度为32个字节,随机串nonce长度12个字节,associated_data长度小于16个字节并可能为空字符串。

通知参数

参数名 变量 类型[长度限制] 必填 描述
通知ID id string[1,36] 通知的唯一ID 示例值:EV-2018022511223320873
通知创建时间 create_time string[1,32] 通知创建的时间,遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE,yyyy-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示,北京时间2015年5月20日13点29分35秒。 示例值:2018-06-08T10:34:56+08:00
通知类型 event_type string[1,32] 通知的类型: REFUND.SUCCESS:退款成功通知 REFUND.ABNORMAL:退款异常通知 REFUND.CLOSED:退款关闭通知 示例值:REFUND.SUCCESS
通知简要说明 summary string[1,16] 通知简要说明 示例值:退款成功
通知数据类型 resource_type string[1,32] 通知的资源数据类型,支付成功通知为encrypt-resource 示例值:encrypt-resource
+通知数据 resource object 通知资源数据 json格式,见示例

通知签名

加密不能保证通知请求来自微信。微信会对发送给商户的通知进行签名,并将签名值放在通知的HTTP头Wechatpay-Signature。商户应当验证签名,以确认请求来自微信,而不是其他的第三方。签名验证的算法请参考 《微信支付API v3签名验证》。

回调示例

退款通知
{
    "id":"EV-2018022511223320873",
    "create_time":"2018-06-08T10:34:56+08:00",
    "resource_type":"encrypt-resource",
    "event_type":"REFUND.SUCCESS",
    "summary":"退款成功",
    "resource" : {
        "original_type": "refund",
        "algorithm":"AEAD_AES_256_GCM",
        "ciphertext": "...",
        "associated_data": "",
        "nonce": "..."
    }
}
商户对resource对象进行解密后,得到的资源对象示例
{
    "mchid": "1900000100",
    "transaction_id": "1008450740201411110005820873",
    "out_trade_no": "20150806125346",
    "refund_id": "50200207182018070300011301001",
    "out_refund_no": "7752501201407033233368018",
    "refund_status": "SUCCESS",
    "success_time": "2018-06-08T10:34:56+08:00",
    "user_received_account": "招商银行信用卡0403",
    "amount" : {
        "total": 999,
        "refund": 999,
        "payer_total": 999,
        "payer_refund": 999
    }
}

通知参数

参数名 变量 类型[长度限制] 必填 描述
直连商户号 mchid string[1,32] 直连商户的商户号,由微信支付生成并下发。 示例值:1900000100
商户订单号 out_trade_no string[1,32] 返回的商户订单号 示例值: 1217752501201407033233368018
微信支付订单号 transaction_id string[1,32] 微信支付订单号 示例值: 1217752501201407033233368018
商户退款单号 out_refund_no string[1,64] 商户退款单号 示例值: 1217752501201407033233368018
微信支付退款单号 refund_id string[1,32] 微信退款单号 示例值: 1217752501201407033233368018
退款状态 refund_status string[1,16] 退款状态,枚举值: SUCCESS:退款成功 CLOSED:退款关闭 ABNORMAL:退款异常,退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往【商户平台—>交易中心】,手动处理此笔退款 示例值:SUCCESS
退款成功时间 success_time string[1,64] 1、退款成功时间,遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE,yyyy-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示,北京时间2015年5月20日13点29分35秒。 2、当退款状态为退款成功时返回此参数。 示例值:2018-06-08T10:34:56+08:00
退款入账账户 user_received_account string[1,64] 取当前退款单的退款入账方。 1、退回银行卡:{银行名称}{卡类型}{卡尾号} 2、退回支付用户零钱: 支付用户零钱 3、退还商户: 商户基本账户、商户结算银行账户 4、退回支付用户零钱通:支付用户零钱通 示例值:招商银行信用卡0403
+金额信息 amount object 金额信息

通知应答

**接收成功:**HTTP应答状态码需返回200或204,无需返回应答报文。

**接收失败:**HTTP应答状态码需返回5XX或4XX,同时需返回应答报文,格式如下:

**注意:**重试过多会导致微信支付端积压过多通知而堵塞,影响其他正常通知。

参数名 变量 类型[长度限制] 必填 描述
返回状态码 code string[1,32] 错误码,SUCCESS为接收成功,其他错误码为失败 示例值:SUCCESS
返回信息 message string[1,256] 返回信息,如非空,为错误原因 示例值:系统错误
{   
    "code": "SUCCESS"
}

* 实现退款通知

1、验签解析请求体,再对加密数据解密

	/**
     * 退款结果通知
     * 退款状态改变后,微信会把相关退款结果发送给商户
     */
    @PostMapping("/refunds/notify")
    public String refundsNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
        log.info("退款通知执行");
        Gson gson = new Gson();
        Map<String, String> map = new HashMap<>();//应答对象
        //处理通知参数,将请求体转换为字符串
        try {
            String body = HttpUtils.readData(request);
            log.info("退款通知的完整数据 ===> {}", body);
            //验签和解析请求体并解密报文
            String decryptData = notificationHandlerUtils.getDecryptData(request, body);
            log.info("退款通知解密完的数据 ===> {}", decryptData);

            //对订单进行处理
            wxPayService.processRefund(decryptData);

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

2、根据明文处理退款单

 	/**
     * 处理退款通知
     * @param decryptData 解密后的明文
     */
    @Override
    public void processRefund(String decryptData) {
        Gson gson = new Gson();
        Map<String, Object> decryptDataMap = gson.fromJson(decryptData, HashMap.class);
        String orderNo = (String) decryptDataMap.get("out_trade_no");
        /*在对业务数据进行状态检查和处理之前,
        要采用数据锁进行并发控制,
        以避免函数重入造成的数据混乱*/
        //尝试获取锁:
        // 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放
        if (lock.tryLock()) {
            try {
                //处理重复通知
                //保证接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的
                String orderStatus = orderInfoService.getOrderStatus(orderNo);
                //申请退款后的OrderStatus == REFUND_PROCESSING
                if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)) {
                    return;
                }
                //更新订单状态 -- 退款成功
                orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
                //将回调通知的请求体存入数据库
                refundInfoService.updateRefund(decryptData);
            } finally {
                lock.unlock();
            }
        }
    }

下载账单

1、获取账单URL

	/**
     * 申请账单
     *
     * @param billDate
     * @param type
     * @return
     */
    @Override
    public String queryBill(String billDate, String type) throws IOException {
        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);

        HttpGet httpGet = new HttpGet(url);
        httpGet.addHeader("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
                log.info("成功");
            } else {
                log.info("申请账单异常,响应码 = " + statusCode + ",申请账单异常返回结果 = " + bodyAsString);
                throw new IOException("request failed");
            }
            //获取账单下载地址
            Gson gson = new Gson();
            Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
            return resultMap.get("download_url");
        } finally {
            response.close();
        }

2、下载无需对应答验签

	/**
     * 获取 HttpClient 对象 无需进行应答签名验证,跳过验签的流程
     *
     * @return
     */
    @Bean(name = "wxPayNoSignClient")
    public CloseableHttpClient getWxPayNoSignClient() {
        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                //设置商户信息
                .withMerchant(mchId, mchSerialNo, getPrivateKey(privateKeyPath))
                //无需进行签名验证、通过withValidator((response) -> true)实现
                .withValidator((response) -> true);

        // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
        CloseableHttpClient httpClient = builder.build();
        return httpClient;
    }
	/**
 * 下载账单
 * @param billDate
 * @param type
 * @return
 */
@Override
public String downloadBill(String billDate, String type) throws IOException {
        log.warn("下载账单接口调用 {}, {}", billDate, type);
        String downloadUrl = queryBill(billDate, type);

        HttpGet httpGet = new HttpGet(downloadUrl);
        httpGet.addHeader("Accept", "application/json");
        //完成签名并执行请求,无需验签响应结果
        CloseableHttpResponse response = wxPayNoSignClient.execute(httpGet);
        # 接入微信支付

## 接入指引

### 获取商户号

[微信商户平台](https://pay.weixin.qq.com/):https://pay.weixin.qq.com/

场景:Native支付

步骤:提交资料 => 签署协议 => 获取商户

### 获取APPID

[微信公众平台](https://mp.weixin.qq.com/):https://mp.weixin.qq.com/

步骤:注册服务号 => 服务号认证 => 获取APPID => 绑定商户号

### 获取APIv3密钥

APIv3版本的接口需要此秘钥

步骤:登录商户平台 => 选择账户中心 => 安全中心 => API安全 => 设置APIv3密钥

[随机密码生成工具](https://suijimimashengcheng.bmcx.com/):https://suijimimashengcheng.bmcx.com/

### 申请商户API证书

APIv3版本的所有接口都需要;APIv2版本的高级接口需要(如:退款、企业红包、企业付款等)

步骤:登录商户平台 => 选择账户中心 => 安全中心 => API安全 => 申请API证书 => 妥善管理

![image-20220923132557280](https://gitee.com/xzxwbb/cloud-image/raw/master/img/image-20220923132557280.png)

### 获取微信平台证书

通过编程动态获取,后续有教程

### demo仓库地址

https://gitee.com/xzxwbb/wxpayment-demo

### 参考微信支付

[【尚硅谷】微信支付&支付宝支付,一套搞定Java在线支付开发教程](https://www.bilibili.com/video/BV1US4y1D77m?share_source=copy_web&vd_source=b8915732f82f77d0c66288af3bcb3b1d)

demo中的私钥文件,商户的参数由上面的视频资料提供


## 加载商户私钥

### 将私钥文件复制到项目根目录下:

![image-20220923132744453](https://gitee.com/xzxwbb/cloud-image/raw/master/img/image-20220923132744453.png)



### 引入SDK

[官方提供的SDK仓库,帮助我们完成开发。实现了请求签名的生成和应答签名的验证。](https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient)

```xml
<!--微信支付SDK 最新版本-->
<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-apache-httpclient</artifactId>
    <version>0.4.8</version>
</dependency>

image-20220811160420542

接入微信支付,实现Native下单,附有源码仓库_第8张图片

# 示例:私钥存储在文件
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
        new FileInputStream("/path/to/apiclient_key.pem"));

# 示例:私钥为String字符串
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
        new ByteArrayInputStream(privateKey.getBytes("utf-8")));

签名与验签流程

接入微信支付,实现Native下单,附有源码仓库_第9张图片

通过第三方的工具来实现商户私钥签名和微信公钥验签的复杂工作

image-20220811163459278

1、获取签名验证器

 	/**
     * 获取签名验证器
     *
     * @return
     */
    @Bean
    public Verifier getVerifier() throws Exception {
        // 获取证书管理器实例
        CertificatesManager certificatesManager = CertificatesManager.getInstance();
        // 向证书管理器增加需要自动更新平台证书的商户信息

        certificatesManager.putMerchant(
                mchId,
                new WechatPay2Credentials(mchId, new PrivateKeySigner(mchSerialNo, getPrivateKey(privateKeyPath))),
                //对称加密的密钥
                apiV3Key.getBytes(StandardCharsets.UTF_8));

        // ... 若有多个商户号,可继续调用putMerchant添加商户信息
        // 从证书管理器中获取verifier
        Verifier verifier = certificatesManager.getVerifier(mchId);
        return verifier;
    }

2、获取HttpClient对象

	/**
     * 获取 HttpClient 对象
     *
     * @param verifier 签名验证器
     * @return
     */
    @Bean(name = "wxPayClient")
    public CloseableHttpClient getWxPayClient(Verifier verifier) {
        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant(mchId, mchSerialNo, getPrivateKey(privateKeyPath))
                .withValidator(new WechatPay2Validator(verifier));
        // ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient

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

调用Native下单

接入微信支付,实现Native下单,附有源码仓库_第10张图片

生成二维码链接

  • 生成订单信息
/**
* 生成订单
* @param productId
* @return
*/
private OrderInfo generateOrderInfo(Long productId) {
    OrderInfo orderInfo = new OrderInfo();
    orderInfo.setTitle("test")
        .setOrderNo(OrderNoUtils.getOrderNo())
        .setProductId(productId)
        .setTotalFee(1)
        .setOrderStatus(OrderStatus.NOTPAY.getType());
    return orderInfo;
}
  • 生成json请求
/**
* 生成请求json
* @param orderInfo
* @return
*/
private String generateJsonParams(OrderInfo orderInfo) {
    Gson gson = new Gson();
    HashMap paramsMap = new HashMap();
    paramsMap.put("appid", wxPayConfig.getAppid());
    paramsMap.put("mchid", wxPayConfig.getMchId());
    paramsMap.put("description", orderInfo.getTitle());
    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);

    //将参数转换成json字符串
    return gson.toJson(paramsMap);
}
  • 生成HttpPost
/**
* 生成HttpPost
* @param jsonParams
* @return
*/
private HttpPost generateHttpPost(String jsonParams, String url) {
    //拼接请求URL
    HttpPost httpPost = new HttpPost(url);
    //设置请求头,编码集,内容格式
    StringEntity entity = new StringEntity(jsonParams, "utf-8");
    entity.setContentType("application/json");
    httpPost.setEntity(entity);
    httpPost.setHeader("Accept", "application/json");
    return httpPost;
}
  • 执行请求处理响应结果
String url = wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType());//Mative下单的url
HttpPost httpPost = generateHttpPost(jsonParams, url);
//完成签名并执行请求
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
        log.info("成功");
    } else {
        log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString);
        throw new IOException("request failed");
    }
    Gson gson = new Gson();
    Map<String, String> resultMap= gson.fromJson(bodyAsString, HashMap.class);
    String codeUrl = resultMap.get("code_url");

    Map<String, Object> map = new HashMap<>();
    map.put("codeUrl", codeUrl);
    map.put("orderNo", orderInfo.getOrderNo());
    return map;
} finally {
    response.close();
}
  • 前端接收二维码链接生成二维码
//package.json
"vue-qriously": "^1.1.1",
// main.js
import VueQriously from 'vue-qriously'
Vue.use(VueQriously)
//index.vue

避免重复创建订单,查询已存在的未过期的该用户未支付的同款商品的订单

	@Override
    public OrderInfo createOrderByProductId(Long productId) {
        OrderInfo orderInfo = getNoPayOrderByProductId(productId);
        if (orderInfo != null) {
            return orderInfo;
        }
        orderInfo = new OrderInfo();
        orderInfo.setTitle("test")
                .setOrderNo(OrderNoUtils.getOrderNo())
                .setProductId(productId)
                .setTotalFee(1)
                .setOrderStatus(OrderStatus.NOTPAY.getType());
        save(orderInfo);
        return orderInfo;
    }

    private OrderInfo getNoPayOrderByProductId(Long productId) {
        return lambdaQuery().eq(OrderInfo::getProductId, productId)
                .eq(OrderInfo::getOrderStatus, OrderStatus.NOTPAY.getType()).one();
    }
	log.info("生成订单");
    OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId);
    String codeUrl = orderInfo.getCodeUrl();
    if (!StringUtils.isBlank(codeUrl)) {
        log.info("订单已存在,二维码已保存");
        Map<String, Object> map = new HashMap<>();
        map.put("codeUrl", codeUrl);
        map.put("orderNo", orderInfo.getOrderNo());
        return map;
    }
	...
	//保存二维码
	orderInfoService.saveCodeUrl(orderInfo.getOrderNo(), codeUrl);
	Map<String, Object> map = new HashMap<>();

签名生成的原理

HTTP请求方法\n
URL\n
请求时间戳\n
请求随机串\n
请求报文主体\n

计算签名值

绝大多数编程语言提供的签名函数支持对签名数据进行签名。强烈建议商户调用该类函数,使用商户私钥对待签名串进行SHA256 with RSA签名,并对签名结果进行Base64编码得到签名值

$ echo -n -e \
"GET\n/v3/certificates\n1554208460\n593BEC0C930BF1AFEB40B4A08C8FB242\n\n" \
| openssl dgst -sha256 -sign apiclient_key.pem \
| openssl base64 -A

uOVRnA4qG/MNnYzdQxJanN+zU+lTgIcnU9BxGw5dKjK+VdEUz2FeIoC+D5sB/LN+nGzX3hfZg6r5wT1pl2ZobmIc6p0ldN7J6yDgUzbX8Uk3sD4a4eZVPTBvqNDoUqcYMlZ9uuDdCvNv4TM3c1WzsXUrExwVkI1XO5jCNbgDJ25nkT/c1gIFvqoogl7MdSFGc4W4xZsqCItnqbypR3RuGIlR9h9vlRsy7zJR9PBI83X8alLDIfR1ukt1P7tMnmogZ0cuDY8cZsd8ZlCgLadmvej58SLsIkVxFJ8XyUgx9FmutKSYTmYtWBZ0+tNvfGmbXU7cob8H/4nLBiCwIUFluw==

设置HTTP头

微信支付商户API v3要求请求通过HTTP Authorization头来传递签名。 Authorization认证类型签名信息两个部分组成。

下面我们使用命令行演示如何生成签名。

Authorization: 认证类型 签名信息

具体组成为:

1.认证类型,目前为WECHATPAY2-SHA256-RSA2048

2.签名信息

  • 发起请求的商户(包括直连商户、服务商或渠道商)的商户号 mchid
  • 商户API证书序列号serial_no,用于声明所使用的证书
  • 请求随机串nonce_str
  • 时间戳timestamp
  • 签名值signature
  • 注:以上五项签名信息,无顺序要求。

Authorization 头的示例如下:(注意,示例因为排版可能存在换行,实际数据应在一行)

Authorization: WECHATPAY2-SHA256-RSA2048 mchid="1900009191",nonce_str="593BEC0C930BF1AFEB40B4A08C8FB242",signature="uOVRnA4qG/MNnYzdQxJanN+zU+lTgIcnU9BxGw5dKjK+VdEUz2FeIoC+D5sB/LN+nGzX3hfZg6r5wT1pl2ZobmIc6p0ldN7J6yDgUzbX8Uk3sD4a4eZVPTBvqNDoUqcYMlZ9uuDdCvNv4TM3c1WzsXUrExwVkI1XO5jCNbgDJ25nkT/c1gIFvqoogl7MdSFGc4W4xZsqCItnqbypR3RuGIlR9h9vlRsy7zJR9PBI83X8alLDIfR1ukt1P7tMnmogZ0cuDY8cZsd8ZlCgLadmvej58SLsIkVxFJ8XyUgx9FmutKSYTmYtWBZ0+tNvfGmbXU7cob8H/4nLBiCwIUFluw==",timestamp="1554208460",serial_no="1DDE55AD98ED71D6EDD4A4A16996DE7B47773A8C"

最终我们可以组一个包含了签名的HTTP请求了。

$ curl https://api.mch.weixin.qq.com/v3/certificates -H 'Authorization: WECHATPAY2-SHA256-RSA2048 mchid="1900009191",nonce_str="593BEC0C930BF1AFEB40B4A08C8FB242",signature="uOVRnA4qG/MNnYzdQxJanN+zU+lTgIcnU9BxGw5dKjK+VdEUz2FeIoC+D5sB/LN+nGzX3hfZg6r5wT1pl2ZobmIc6p0ldN7J6yDgUzbX8Uk3sD4a4eZVPTBvqNDoUqcYMlZ9uuDdCvNv4TM3c1WzsXUrExwVkI1XO5jCNbgDJ25nkT/c1gIFvqoogl7MdSFGc4W4xZsqCItnqbypR3RuGIlR9h9vlRsy7zJR9PBI83X8alLDIfR1ukt1P7tMnmogZ0cuDY8cZsd8ZlCgLadmvej58SLsIkVxFJ8XyUgx9FmutKSYTmYtWBZ0+tNvfGmbXU7cob8H/4nLBiCwIUFluw==",timestamp="1554208460",serial_no="1DDE55AD98ED71D6EDD4A4A16996DE7B47773A8C"'

演示代码

计算签名的示例代码如下。

import okhttp3.HttpUrl;
import java.security.Signature;
import java.util.Base64;

// Authorization:  
// GET - getToken("GET", httpurl, "")
// POST - getToken("POST", httpurl, json)
String schema = "WECHATPAY2-SHA256-RSA2048";
HttpUrl httpurl = HttpUrl.parse(url);

String getToken(String method, HttpUrl url, String body) {
    String nonceStr = "your nonce string";
    long timestamp = System.currentTimeMillis() / 1000;
    String message = buildMessage(method, url, timestamp, nonceStr, body);
    String signature = sign(message.getBytes("utf-8"));

    return "mchid=\"" + yourMerchantId + "\","
    + "nonce_str=\"" + nonceStr + "\","
    + "timestamp=\"" + timestamp + "\","
    + "serial_no=\"" + yourCertificateSerialNo + "\","
    + "signature=\"" + signature + "\"";
}

String sign(byte[] message) {
    Signature sign = Signature.getInstance("SHA256withRSA");
    sign.initSign(yourPrivateKey);
    sign.update(message);

    return Base64.getEncoder().encodeToString(sign.sign());
}

String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) {
    String canonicalUrl = url.encodedPath();
    if (url.encodedQuery() != null) {
      canonicalUrl += "?" + url.encodedQuery();
    }

    return method + "\n"
        + canonicalUrl + "\n"
        + timestamp + "\n"
        + nonceStr + "\n"
        + body + "\n";
}              

如果您的请求返回了签名错误401 Unauthorized,请参考 常见问题之签名相关

签名验证的原理

如果验证商户的请求签名正确,微信支付会在应答的HTTP头部中包括应答签名。我们建议商户验证应答签名。

同样的,微信支付会在回调的HTTP头部中包括回调报文的签名。商户必须 验证回调的签名,以确保回调是由微信支付发送。

获取平台证书

微信支付API v3使用微信支付 的平台私钥(不是商户私钥 )进行应答签名。相应的,商户的技术人员应使用微信支付平台证书中的公钥验签。目前平台证书只提供API进行下载,请参考 获取平台证书列表。

检查平台证书序列号

微信支付的平台证书序列号位于HTTP头Wechatpay-Serial。验证签名前,请商户先检查序列号是否跟商户当前所持有的 微信支付平台证书的序列号一致。如果不一致,请重新获取证书。否则,签名的私钥和证书不匹配,将无法成功验证签名。

构造验签名串

首先,商户先从应答中获取以下信息。

  • HTTP头Wechatpay-Timestamp 中的应答时间戳。
  • HTTP头Wechatpay-Nonce 中的应答随机串。
  • 应答主体(response Body),需要按照接口返回的顺序进行验签,错误的顺序将导致验签失败。

然后,请按照以下规则构造应答的验签名串。签名串共有三行,行尾以\n 结束,包括最后一行。\n为换行符(ASCII编码值为0x0A)。若应答报文主体为空(如HTTP状态码为204 No Content),最后一行仅为一个\n换行符。

应答时间戳\n
应答随机串\n
应答报文主体\n      

如某个应答的HTTP报文为(省略了ciphertext的具体内容):

HTTP/1.1 200 OK
Server: nginx
Date: Tue, 02 Apr 2019 12:59:40 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2204
Connection: keep-alive
Keep-Alive: timeout=8
Content-Language: zh-CN
Request-ID: e2762b10-b6b9-5108-a42c-16fe2422fc8a
Wechatpay-Nonce: c5ac7061fccab6bf3e254dcf98995b8c
Wechatpay-Signature: CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA==
Wechatpay-Timestamp: 1554209980
Wechatpay-Serial: 5157F09EFDC096DE15EBE81A47057A7232F1B8E1
Cache-Control: no-cache, must-revalidate

{"data":[{"serial_no":"5157F09EFDC096DE15EBE81A47057A7232F1B8E1","effective_time":"2018-03-26T11:39:50+08:00","expire_time":"2023-03-25T11:39:50+08:00","encrypt_certificate":{"algorithm":"AEAD_AES_256_GCM","nonce":"4de73afd28b6","associated_data":"certificate","ciphertext":"..."}}]}
              

则验签名串为

1554209980
c5ac7061fccab6bf3e254dcf98995b8c
{"data":[{"serial_no":"5157F09EFDC096DE15EBE81A47057A7232F1B8E1","effective_time":"2018-03-26T11:39:50+08:00","expire_time":"2023-03-25T11:39:50+08:00","encrypt_certificate":{"algorithm":"AEAD_AES_256_GCM","nonce":"4de73afd28b6","associated_data":"certificate","ciphertext":"..."}}]}

获取应答签名

微信支付的应答签名通过HTTP头Wechatpay-Signature传递。(注意,示例因为排版可能存在换行,实际数据应在一行)

Wechatpay-Signature: CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA==

Wechatpay-Signature的字段值使用Base64进行解码,得到应答签名。

某些代理服务器或CDN服务提供商,转发时会“过滤”微信支付扩展的HTTP头,导致应用层无法取到微信支付的签名信息。商户遇到这种情况时,我们建议尝试调整代理服务器配置,或者通过直连的方式访问微信支付的服务器和接收通知。

验证签名

很多编程语言的签名验证函数支持对验签名串和签名 进行签名验证。强烈建议商户调用该类函数,使用微信支付平台公钥对验签名串和签名进行SHA256 with RSA签名验证。

下面展示使用命令行演示如何进行验签。假设我们已经获取了平台证书并保存为1900009191_wxp_cert.pem

首先,从微信支付平台证书导出微信支付平台公钥

$ openssl x509 -in 1900009191_wxp_cert.pem -pubkey -noout > 1900009191_wxp_pub.pem
$ cat 1900009191_wxp_pub.pem
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4zej1cqugGQtVSY2Ah8R
MCKcr2UpZ8Npo+5Ja9xpFPYkWHaF1Gjrn3d5kcwAFuHHcfdc3yxDYx6+9grvJnCA
2zQzWjzVRa3BJ5LTMj6yqvhEmtvjO9D1xbFTA2m3kyjxlaIar/RYHZSslT4VmjIa
tW9KJCDKkwpM6x/RIWL8wwfFwgz2q3Zcrff1y72nB8p8P12ndH7GSLoY6d2Tv0OB
2+We2Kyy2+QzfGXOmLp7UK/pFQjJjzhSf9jxaWJXYKIBxpGlddbRZj9PqvFPTiep
8rvfKGNZF9Q6QaMYTpTp/uKQ3YvpDlyeQlYe4rRFauH3mOE6j56QlYQWivknDX9V
rwIDAQAB
-----END PUBLIC KEY-----

Java支持使用证书初始化签名对象,详见 initVerify(Certificate),并不需要先导出公钥。

然后,把签名base64解码后保存为文件signature.txt

$ openssl base64 -d -A <<< \ 'CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA==' > signature.txt

最后,验证签名 验签名串进行摘要算法签名解码后用微信公钥解密的进行比对

签名生成:摘要->公钥加密->Base64编码

$ openssl dgst -sha256 -verify 1900009191_wxp_pub.pem -signature signature.txt << EOF
1554209980
c5ac7061fccab6bf3e254dcf98995b8c
{"data":[{"serial_no":"5157F09EFDC096DE15EBE81A47057A7232F1B8E1","effective_time":"2018-03-26T11:39:50+08:00","expire_time":"2023-03-25T11:39:50+08:00","encrypt_certificate":{"algorithm":"AEAD_AES_256_GCM","nonce":"d215b0511e9c","associated_data":"certificate","ciphertext":"..."}}]}
EOF
Verified OK

配置内网穿透

设置authToken

ngrok config add-authtoken 2DEjRSBgc9J5A1oVKs2SU9kZ1h5_4B9NJUkDFACvK218GrESM

启动服务

ngrok http 8090

测试外网访问

你获得的外网地址/api/test

Linux系统命令前要加上./

支付通知API

接入微信支付,实现Native下单,附有源码仓库_第11张图片

1、接口

	@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);
            log.info("支付通知的完整数据 ===> {}", body);
            //验签和解析请求体并解密报文
            String decryptData = notificationHandlerUtils.getDecryptData(request, body);
            log.info("支付通知解密完的数据 ===> {}", decryptData);

            //对订单进行处理
            wxPayService.processOrder(decryptData);

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

2、* 对请求体进行验签和解析,解密获得报文

@Component
public class NotificationHandlerUtils {

    @Resource
    private Verifier verifier;
    @Resource
    private WxPayConfig wxPayConfig;

    /**
     * 验签request并解析加密数据获取订单的详情
     * @param request
     * @param body
     * @return
     */
    public String getDecryptData(HttpServletRequest request, String body) {
        String serial = request.getHeader(WECHAT_PAY_SERIAL);
        String nonce = request.getHeader(WECHAT_PAY_NONCE);
        String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
        String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
        // 构建request,传入必要参数
        NotificationRequest req = new NotificationRequest.Builder()
                .withSerialNumber(serial)
                .withNonce(nonce)
                .withTimestamp(timestamp)
                .withSignature(signature)
                .withBody(body)
                .build();
        NotificationHandler handler = new NotificationHandler(verifier, wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        // 验签和解析请求体
        Notification notification = null;
        try {
            notification = handler.parse(req);
        } catch (ValidationException e) {
            throw new RuntimeException("验签失败");
        } catch (ParseException e) {
            throw new RuntimeException("解析失败");
        }
        // 从notification中获取解密报文
        return notification.getDecryptData();
    }
}

3、处理订单

	/**
     * 处理订单
     *
     * @param decryptData 解密后的报文
     */
    @Override
    public void processOrder(String decryptData) {
        Gson gson = new Gson();
        Map<String, Object> decryptDataMap = gson.fromJson(decryptData, HashMap.class);
        String orderNo = (String) decryptDataMap.get("out_trade_no");
        /*在对业务数据进行状态检查和处理之前,
        要采用数据锁进行并发控制,
        以避免函数重入造成的数据混乱*/
        //尝试获取锁:
        // 成功获取则立即返回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(decryptData);
            } finally {
                lock.unlock();
            }
        }

    }

4、更新订单状态

    @Override
    public void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus) {
        log.info("更新订单状态 ===> {}", orderStatus.getType());
        LambdaQueryWrapper<OrderInfo> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(OrderInfo::getOrderNo, orderNo);
        OrderInfo orderInfo = new OrderInfo().setOrderStatus(orderStatus.getType());
        update(orderInfo, wrapper);
    }

5、记录支付日志

	@Override
    public void createPaymentInfo(String decryptData) {
        log.info("记录支付日志");
        Gson gson = new Gson();
        Map<String, Object> decryptDataMap = gson.fromJson(decryptData, HashMap.class);
        String orderNo = (String)decryptDataMap.get("out_trade_no"); 
        String transactionId = (String)decryptDataMap.get("transaction_id"); 
        String tradeType = (String)decryptDataMap.get("trade_type"); 
        String tradeState = (String)decryptDataMap.get("trade_state"); 
        Map<String, Object> amount = (Map)decryptDataMap.get("amount"); 
        Integer payerTotal = ((Double) amount.get("payer_total")).intValue();

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

关闭订单

接口说明

适用对象: 直连商户

请求URL: https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{out_trade_no}/close

请求方式: POST

path指该参数为路径参数

query指该参数需在请求URL传参

body指该参数需在请求JSON传参

请求参数

参数名 变量 类型[长度限制] 必填 描述
直连商户号 mchid string[1,32] body 直连商户的商户号,由微信支付生成并下发。 示例值:1230000109
商户订单号 out_trade_no string[6,32] path 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一 示例值:1217752501201407033233368018

请求示例

{
	"mchid": "1230000109"
}

接口实现

@ApiOperation("用户取消订单")
@PostMapping("/cancel/{orderNo}")
public R cancel(@PathVariable String orderNo) throws IOException {
    log.info("取消订单");
    wxPayService.cancelOrder(orderNo);
    return R.ok().setMessage("订单已取消");
}
	@Override
    public void cancelOrder(String orderNo) throws IOException {
        //调用微信支付的关单接口 
        closeOrder(orderNo);
        // 更新商户端的订单状态
        orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL);
    }

    private void closeOrder(String orderNo) throws IOException {
        log.info("关单接口的调用,订单号 ===> {}", orderNo);
        //创建远程请求对象
        String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo);
        url = wxPayConfig.getDomain().concat(url);//取消订单url
        //组装json请求体
        Gson gson = new Gson();
        Map<String, String> paramsMap = new HashMap();
        paramsMap.put("mchid", wxPayConfig.getMchId());
        //将参数转换成json字符串
        String jsonParams = gson.toJson(paramsMap);
        HttpPost httpPost = generateHttpPost(jsonParams, url);
        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpPost);
        int statusCode = response.getStatusLine().getStatusCode();
        try {
            if (statusCode == 200) { //处理成功
                log.info("成功200");
            } else if (statusCode == 204) { //处理成功,无返回Body
                log.info("成功204");
            } else {
                log.info("取消订单失败,响应码 = " + statusCode);
                throw new IOException("request failed");
            }
        } finally {
            response.close();
        }
    }

微信支付查单API

1、微信支付订单号查询

接口说明

适用对象: 直连商户

请求URL: https://api.mch.weixin.qq.com/v3/pay/transactions/id/{transaction_id}

**请求方式:**GET

path指该参数为路径参数

query指该参数需在请求URL传参

body指该参数需在请求JSON传参

请求参数

参数名 变量 类型[长度限制] 必填 描述
直连商户号 mchid string[1,32] query 直连商户的商户号,由微信支付生成并下发。 示例值:1230000109
微信支付订单号 transaction_id string[1,32] path 微信支付系统生成的订单号 示例值:1217752501201407033233368018

查询订单

2、商户订单号查询

接口说明

适用对象: 直连商户

请求URL: https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{out_trade_no}

**请求方式:**GET

path指该参数为路径参数

query指该参数需在请求URL传参

body指该参数需在请求JSON传参

请求参数

参数名 变量 类型[长度限制] 必填 描述
直连商户号 mchid string[1,32] query 直连商户的商户号,由微信支付生成并下发。 示例值:1230000109
商户订单号 out_trade_no string[6,32] path 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一。 特殊规则:最小字符长度为6 示例值:1217752501201407033233368018

请求示例

  • [URL](javascript:
https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/1217752501201407033233368018?mchid=1230000109

返回参数

参数名 变量 类型[长度限制] 必填 描述
应用ID appid string[1,32] 直连商户申请的公众号或移动应用appid。 示例值:wxd678efh567hg6787
直连商户号 mchid string[1,32] 直连商户的商户号,由微信支付生成并下发。 示例值:1230000109
商户订单号 out_trade_no string[6,32] 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一,详见【商户订单号】。 示例值:1217752501201407033233368018
微信支付订单号 transaction_id string[1,32] 微信支付系统生成的订单号。 示例值:1217752501201407033233368018
交易类型 trade_type string[1,16] 交易类型,枚举值: JSAPI:公众号支付 NATIVE:扫码支付 APP:APP支付 MICROPAY:付款码支付 MWEB:H5支付 FACEPAY:刷脸支付 示例值:MICROPAY
交易状态 trade_state string[1,32] 交易状态,枚举值: SUCCESS:支付成功 REFUND:转入退款 NOTPAY:未支付 CLOSED:已关闭 REVOKED:已撤销(仅付款码支付会返回) USERPAYING:用户支付中(仅付款码支付会返回) PAYERROR:支付失败(仅付款码支付会返回) 示例值:SUCCESS
交易状态描述 trade_state_desc string[1,256] 交易状态描述 示例值:支付成功
付款银行 bank_type string[1,32] 银行类型,采用字符串类型的银行标识。银行标识请参考《银行类型对照表》 示例值:CMC
附加数据 attach string[1,128] 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用,实际情况下只有支付完成状态才会返回该字段。 示例值:自定义数据
支付完成时间 success_time string[1,64] 支付完成时间,遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE,yyyy-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示,北京时间2015年5月20日 13点29分35秒。 示例值:2018-06-08T10:34:56+08:00
+支付者 payer object 支付者信息
+订单金额 amount object 订单金额信息,当支付成功时返回该字段。
+场景信息 scene_info object 支付场景描述
+优惠功能 promotion_detail array 优惠功能,享受优惠时返回该字段。

返回示例

  • [正常示例](javascript:
{
	"amount": {
		"currency": "CNY",
		"payer_currency": "CNY",
		"payer_total": 1,
		"total": 1
	},
	"appid": "wxdace645e0bc2cXXX",
	"attach": "",
	"bank_type": "OTHERS",
	"mchid": "1900006XXX",
	"out_trade_no": "44_2126281063_5504",
	"payer": {
		"openid": "o4GgauJP_mgWEWictzA15WT15XXX"
	},
	"promotion_detail": [],
	"success_time": "2021-03-22T10:29:05+08:00",
	"trade_state": "SUCCESS",
	"trade_state_desc": "支付成功",
	"trade_type": "JSAPI",
	"transaction_id": "4200000891202103228088184743"
}

接口实现

	/**
     * 根据订单号查询微信支付查单接口
     * 响应体的明文
     * @param orderNo
     * @return
     * @throws IOException
     */
    @Override
    public String queryOrder(String orderNo) throws IOException {
        log.info("查单接口调用 ===> {}", orderNo);
        //组装请求URL
        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
                log.info("成功");
            } else {
                log.info("查询订单失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString);
                throw new IOException("request failed");
            }
            return bodyAsString;
        } finally {
            response.close();
        }
    }

定时任务核实超时未支付的订单

1、定时任务创建

@Component
@Slf4j
public class WxPayTask {

    @Resource
    private OrderInfoService orderInfoService;
    @Resource
    private WxPayService wxPayService;

    /**
     * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单
     */
    @Scheduled(cron = "0/30 * * * * ?")
    public void orderConfirm() throws IOException {
        log.info("确认超时订单的状态...");
        //获取超时未支付的订单
        List<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDurations(5);

        for (OrderInfo orderInfo : orderInfoList) {
            String orderNo = orderInfo.getOrderNo();
            log.warn("超时订单 ===> {}", orderNo);
            //核实订单状态:调用微信支付主动查询订单
            wxPayService.checkOrderStatus(orderNo);
        }
    }
}

2、查找超过5分钟未支付的订单

	/**
     * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单
     *
     * @param minutes
     * @return
     */
    @Override
    public List<OrderInfo> getNoPayOrderByDurations(int minutes) {
        //minutes分钟之前的时间
        Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));
        List<OrderInfo> orderInfoList = lambdaQuery().eq(OrderInfo::getOrderStatus, OrderStatus.NOTPAY.getType())
                .le(OrderInfo::getCreateTime, instant)
                .list();
        return orderInfoList;
    }

3、调用查单接口更新订单状态

	/**
     * 根据订单号查询微信支付查单接口,核实订单状态
     * 如果订单已支付,则更新商户端订单状态,并记录支付日志
     * 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
     *
     * @param orderNo 创建5分钟且未支付的订单号
     */
    @Override
    public void checkOrderStatus(String orderNo) throws IOException {
        log.warn("根据订单号核实订单状态 ===> {}", orderNo);
        Gson gson = new Gson();
        //查单
        String bodyAsString = queryOrder(orderNo);
        Map<String, Object> bodyMap = gson.fromJson(bodyAsString, HashMap.class);
        String trade_state = (String) bodyMap.get("trade_state");
        if (WxTradeState.SUCCESS.getType().equals(trade_state)) {
            log.warn("核实订单已支付 ===> {}", orderNo);
            //如果确认订单已支付则更新本地订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
            //记录支付日志
            paymentInfoService.createPaymentInfo(bodyAsString);
        } else if (WxTradeState.NOTPAY.getType().equals(trade_state)) {
            log.warn("核实订单未支付 ===> {}", orderNo);
            //关单接口
            closeOrder(orderNo);
            //更新为超时关闭
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);
        }
    }

微信支付退款

申请退款API

状态机

退款状态转变如下:

接入微信支付,实现Native下单,附有源码仓库_第12张图片

接口说明

**适用对象:**直连商户

**请求URL:**https://api.mch.weixin.qq.com/v3/refund/domestic/refunds

**请求方式:**POST

**接口频率:**150qps

path 指该参数为路径参数

query 指该参数为URL参数

body 指该参数需在请求JSON传参

请求参数

参数名 变量 类型[长度限制] 必填 描述
微信支付订单号 transaction_id string[1, 32] 二选一 body原支付交易对应的微信订单号 示例值:1217752501201407033233368018
商户订单号 out_trade_no string[6, 32] body原支付交易对应的商户订单号 示例值:1217752501201407033233368018
商户退款单号 out_refund_no string[1, 64] body商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。 示例值:1217752501201407033233368018
退款原因 reason string[1, 80] body若商户传入,会在下发给用户的退款消息中体现退款原因 示例值:商品已售完
退款结果回调url notify_url string[8, 256] body异步接收微信支付退款结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 如果参数中传了notify_url,则商户平台上配置的回调地址将不会生效,优先回调当前传的这个地址。 示例值:https://weixin.qq.com
退款资金来源 funds_account string[1,32] body若传递此参数则使用对应的资金账户退款,否则默认使用未结算资金退款(仅对老资金流商户适用) 枚举值: AVAILABLE:可用余额账户 示例值:AVAILABLE
+金额信息 amount object body订单金额信息
+退款商品 goods_detail array body指定商品退款需要传此参数,其他场景无需传递
请求示例
  • [JSON](javascript:
{
  "transaction_id": "1217752501201407033233368018",
  "out_refund_no": "1217752501201407033233368018",
  "reason": "商品已售完",
  "notify_url": "https://weixin.qq.com",
  "funds_account": "AVAILABLE",
  "amount": {
    "refund": 888,
    "from": [
      {
        "account": "AVAILABLE",
        "amount": 444
      }
    ],
    "total": 888,
    "currency": "CNY"
  },
  "goods_detail": [
    {
      "merchant_goods_id": "1217752501201407033233368018",
      "wechatpay_goods_id": "1001",
      "goods_name": "iPhone6s 16G",
      "unit_price": 528800,
      "refund_amount": 528800,
      "refund_quantity": 1
    }
  ]
}

返回参数

参数名 变量 类型[长度限制] 必填 描述
微信支付退款单号 refund_id string[1, 32] 微信支付退款单号 示例值:50000000382019052709732678859
商户退款单号 out_refund_no string[1, 64] 商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。 示例值:1217752501201407033233368018
微信支付订单号 transaction_id string[1, 32] 微信支付交易订单号 示例值:1217752501201407033233368018
商户订单号 out_trade_no string[1, 32] 原支付交易对应的商户订单号 示例值:1217752501201407033233368018
退款渠道 channel string[1, 16] 枚举值: ORIGINAL:原路退款 BALANCE:退回到余额 OTHER_BALANCE:原账户异常退到其他余额账户 OTHER_BANKCARD:原银行卡异常退到其他银行卡 示例值:ORIGINAL
退款入账账户 user_received_account string[1, 64] 取当前退款单的退款入账方,有以下几种情况: 1)退回银行卡:{银行名称}{卡类型}{卡尾号} 2)退回支付用户零钱:支付用户零钱 3)退还商户:商户基本账户商户结算银行账户 4)退回支付用户零钱通:支付用户零钱通 示例值:招商银行信用卡0403
退款成功时间 success_time string[1, 64] 退款成功时间,当退款状态为退款成功时有返回。 示例值:2020-12-01T16:18:12+08:00
退款创建时间 create_time string[1, 64] 退款受理时间 示例值:2020-12-01T16:18:12+08:00
退款状态 status string[1, 32] 退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台-交易中心,手动处理此笔退款。 枚举值: SUCCESS:退款成功 CLOSED:退款关闭 PROCESSING:退款处理中 ABNORMAL:退款异常 示例值:SUCCESS
资金账户 funds_account string[1, 32] 退款所使用资金对应的资金账户类型 枚举值: UNSETTLED : 未结算资金 AVAILABLE : 可用余额 UNAVAILABLE : 不可用余额 OPERATION : 运营户 BASIC : 基本账户(含可用余额和不可用余额) 示例值:UNSETTLED
+金额信息 amount object 金额详细信息
+优惠退款信息 promotion_detail array 优惠退款信息
返回示例
  • [正常示例](javascript:
{
  "refund_id": "50000000382019052709732678859",
  "out_refund_no": "1217752501201407033233368018",
  "transaction_id": "1217752501201407033233368018",
  "out_trade_no": "1217752501201407033233368018",
  "channel": "ORIGINAL",
  "user_received_account": "招商银行信用卡0403",
  "success_time": "2020-12-01T16:18:12+08:00",
  "create_time": "2020-12-01T16:18:12+08:00",
  "status": "SUCCESS",
  "funds_account": "UNSETTLED",
  "amount": {
    "total": 100,
    "refund": 100,
    "from": [
      {
        "account": "AVAILABLE",
        "amount": 444
      }
    ],
    "payer_total": 90,
    "payer_refund": 90,
    "settlement_refund": 100,
    "settlement_total": 100,
    "discount_refund": 10,
    "currency": "CNY"
  },
  "promotion_detail": [
    {
      "promotion_id": "109519",
      "scope": "SINGLE",
      "type": "DISCOUNT",
      "amount": 5,
      "refund_amount": 100,
      "goods_detail": [
	    {
			"merchant_goods_id": "1217752501201407033233368018",
			"wechatpay_goods_id": "1001",
			"goods_name": "iPhone6s 16G",
			"unit_price": 528800,
			"refund_amount": 528800,
			"refund_quantity": 1
		}
      ]
    }
  ]
}

* 实现退款

1、创建退款订单

	/**
     * 创建退款订单
     * @param orderNo
     * @param reason
     * @return
     */
    @Override
    public RefundInfo createRefundByOrderNo(String orderNo, String reason) {
        OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);
        RefundInfo refundInfo = new RefundInfo();
        refundInfo.setOrderNo(orderNo)
                .setRefundNo(OrderNoUtils.getRefundNo())
                .setTotalFee(orderInfo.getTotalFee())//原金额
                .setRefund(orderInfo.getTotalFee())//退款金额
                .setReason(reason);

        save(refundInfo);
        return refundInfo;
    }

	@Override
    public OrderInfo getOrderByOrderNo(String orderNo) {
        return lambdaQuery().eq(OrderInfo::getOrderNo, orderNo).one();
    }

2、组装Json请求体

	private String generateJsonParamsOfRefund(RefundInfo refundInfo) {
        Gson gson = new Gson();
        Map<String, Object> paramsMap = new HashMap<>();
        paramsMap.put("out_trade_no", refundInfo.getOrderNo());//商品订单号
        paramsMap.put("out_refund_no", refundInfo.getRefundNo());//退款单号
        paramsMap.put("reason", refundInfo.getReason());
        paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));
        Map<String, Object> amountMap = new HashMap<>();
        amountMap.put("refund", refundInfo.getRefund());//退款金额
        amountMap.put("total", refundInfo.getTotalFee());//原订单金额
        amountMap.put("currency", "CNY");
        paramsMap.put("amount", amountMap);
        //将参数转换成json字符串
        return gson.toJson(paramsMap);
    }

3、调用申请退款接口

	/**
     * 申请退款
     *
     * @param orderNo
     * @param reason
     */
    @Override
    public void refund(String orderNo, String reason) throws IOException {
        log.info("创建退款单记录");
        RefundInfo refundInfo = refundInfoService.createRefundByOrderNo(orderNo, reason);
        log.info("调用退款API");
        String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
        //json请求体
        String jsonParams = generateJsonParamsOfRefund(refundInfo);
        HttpPost httpPost = generateHttpPost(jsonParams, url);
        //完成签名并执行请求
        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
                log.info("成功");
            } else {
                throw new RuntimeException("退款失败,响应码 = " + statusCode + ",退款返回结果 = " + bodyAsString);
            }
            //更新订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING);
            //更新退款单
            refundInfoService.updateRefund(bodyAsString);
        } finally {
            response.close();
        }
    }

4、* 记录响应体或者请求体的明文数据到数据库中

	/**
     * 记录退款记录
     * @param content
     */
    @Override
    public void updateRefund(String content) {
        //将json字符串转换成Map
        Gson gson = new Gson();
        Map<String, String> resultMap = gson.fromJson(content, HashMap.class);
        //根据退款单编号修改退款单
        LambdaQueryWrapper<RefundInfo> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(RefundInfo::getRefundNo, resultMap.get("out_refund_no"));

        RefundInfo refundInfo = new RefundInfo();
        refundInfo.setRefundId(resultMap.get("refund_id"));
        //1、查询退款和申请退款中的返回参数
        if (resultMap.get("status") != null) {
            refundInfo.setRefundStatus(resultMap.get("status"));
            refundInfo.setContentReturn(content);
        }
        //2、退款回调中的回调参数
        if (resultMap.get("refund_status") != null) {
            refundInfo.setRefundStatus(resultMap.get("refund_status"));
            refundInfo.setContentNotify(content);
        }
        update(refundInfo,wrapper);
    }

查询退款API

接口说明

**适用对象:**直连商户

**请求URL:**https://api.mch.weixin.qq.com/v3/refund/domestic/refunds/{out_refund_no}

**请求方式:**GET

path 指该参数为路径参数

query 指该参数为URL参数

body 指该参数需在请求JSON传参

请求参数

参数名 变量 类型[长度限制] 必填 描述
商户退款单号 out_refund_no string[1, 64] path商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。 示例值:1217752501201407033233368018
请求示例
  • [URL](javascript:
https://api.mch.weixin.qq.com/v3/refund/domestic/refunds/1217752501201407033233368018

返回参数

参数名 变量 类型[长度限制] 必填 描述
微信支付退款单号 refund_id string[1, 32] 微信支付退款单号 示例值:50000000382019052709732678859
商户退款单号 out_refund_no string[1, 64] 商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。 示例值:1217752501201407033233368018
微信支付订单号 transaction_id string[1, 32] 微信支付交易订单号 示例值:1217752501201407033233368018
商户订单号 out_trade_no string[1, 32] 原支付交易对应的商户订单号 示例值:1217752501201407033233368018
退款渠道 channel string[1, 16] 枚举值: ORIGINAL:原路退款 BALANCE:退回到余额 OTHER_BALANCE:原账户异常退到其他余额账户 OTHER_BANKCARD:原银行卡异常退到其他银行卡 示例值:ORIGINAL
退款入账账户 user_received_account string[1, 64] 取当前退款单的退款入账方,有以下几种情况: 1)退回银行卡:{银行名称}{卡类型}{卡尾号} 2)退回支付用户零钱:支付用户零钱 3)退还商户:商户基本账户商户结算银行账户 4)退回支付用户零钱通:支付用户零钱通 示例值:招商银行信用卡0403
退款成功时间 success_time string[1, 64] 退款成功时间,当退款状态为退款成功时有返回。 示例值:2020-12-01T16:18:12+08:00
退款创建时间 create_time string[1, 64] 退款受理时间 示例值:2020-12-01T16:18:12+08:00
退款状态 status string[1, 32] 款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台-交易中心,手动处理此笔退款。 枚举值: SUCCESS:退款成功 CLOSED:退款关闭 PROCESSING:退款处理中 ABNORMAL:退款异常 示例值:SUCCESS
资金账户 funds_account string[1, 32] 退款所使用资金对应的资金账户类型 枚举值: UNSETTLED : 未结算资金 AVAILABLE : 可用余额 UNAVAILABLE : 不可用余额 OPERATION : 运营户 BASIC : 基本账户(含可用余额和不可用余额) 示例值:UNSETTLED
+金额信息 amount object 金额详细信息
+优惠退款信息 promotion_detail array 优惠退款信息
返回示例
  • [正常示例](javascript:
{
  "refund_id": "50000000382019052709732678859",
  "out_refund_no": "1217752501201407033233368018",
  "transaction_id": "1217752501201407033233368018",
  "out_trade_no": "1217752501201407033233368018",
  "channel": "ORIGINAL",
  "user_received_account": "招商银行信用卡0403",
  "success_time": "2020-12-01T16:18:12+08:00",
  "create_time": "2020-12-01T16:18:12+08:00",
  "status": "SUCCESS",
  "funds_account": "UNSETTLED",
  "amount": {
    "total": 100,
    "refund": 100,
    "from": [
      {
        "account": "AVAILABLE",
        "amount": 444
      }
    ],
    "payer_total": 90,
    "payer_refund": 90,
    "settlement_refund": 100,
    "settlement_total": 100,
    "discount_refund": 10,
    "currency": "CNY"
  },
  "promotion_detail": [
    {
      "promotion_id": "109519",
      "scope": "SINGLE",
      "type": "DISCOUNT",
      "amount": 5,
      "refund_amount": 100,
      "goods_detail": [
 		{
			"merchant_goods_id": "1217752501201407033233368018",
			"wechatpay_goods_id": "1001",
			"goods_name": "iPhone6s 16G",
			"unit_price": 528800,
			"refund_amount": 528800,
			"refund_quantity": 1
       }
		]
    }
  ]
}

* 实现查询退款

1、创建定时任务

	/**
     * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未成功的退款单
     */
    @Scheduled(cron = "0/30 * * * * ?")
    public void refundConfirm() throws IOException {
        log.info("确认超时退款订单的状态...");
        //获取超时退款仍在处理中的订单
        List<RefundInfo> RefundInfoList = refundInfoService.getNoPayOrderByDurations(5);

        for (RefundInfo refundInfo : RefundInfoList) {
            String refundNo = refundInfo.getRefundNo();
            log.warn("超时未退款的退款单号 ===> {}", refundNo);

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

	/**
     * 找出申请退款超过minutes分钟并且未成功的退款单
     * @param minutes
     * @return
     */
    @Override
    public List<RefundInfo> getNoPayOrderByDurations(int minutes) {
        //minutes分钟之前的时间
        Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));
        List<RefundInfo> RefundInfoList = lambdaQuery().eq(RefundInfo::getRefundStatus, WxRefundStatus.PROCESSING.getType())
                .le(RefundInfo::getCreateTime, instant)
                .list();
        return RefundInfoList;
    }

2、核实退款仍在处理中的退款单

	/**
     * 1.调用查询退款记录接口
     * 2.更新数据库
     * @param refundNo
     * @throws IOException
     */
    @Override
    public void checkRefundStatus(String refundNo) throws IOException {
        log.warn("根据退款订单号核实退款订单状态 ===> {}", refundNo);
        Gson gson = new Gson();
        //查询退款订单
        String bodyAsString = queryRefund(refundNo);
        Map<String, String> bodyMap = gson.fromJson(bodyAsString, HashMap.class);
        String status = bodyMap.get("status");
        String orderNo = bodyMap.get("out_trade_no");
        if (WxRefundStatus.SUCCESS.getType().equals(status)) {
            log.warn("核实订单退款成功 ===> {}", refundNo);
            //如果确认订单已退款成功则更新本地订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
        } else if (WxRefundStatus.ABNORMAL.getType().equals(status)) {
            log.warn("核实订单退款异常 ===> {}", refundNo);
            //如果确认订单退款异常则更新本地订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL);
        }
        //保存响应体到数据库中
        refundInfoService.updateRefund(bodyAsString);
    }

3、调用API查询退款单

	/**
     * 调用查询退款单接口,核实订单状态
     * @param refundNo
     * @return
     * @throws IOException
     */
    @Override
    public String queryRefund(String refundNo) throws IOException {
        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) { //处理成功,无返回Body
                log.info("成功");
            } else {
                log.info("查询退款失败,响应码 = " + statusCode + ",查询退款返回结果 = " + bodyAsString);
                throw new IOException("request failed");
            }
            return bodyAsString;
        } finally {
            response.close();
        }

    }

退款结果通知

接口说明

**适用对象:**直连商户

**请求方式:**POST

**请求URL:**该链接是通过[申请退款接口]指定的notify_url,必须为https协议。如果链接无法访问,商户将无法接收到微信通知。 通知url必须为直接可访问的url,不能携带参数。示例:“https://pay.weixin.qq.com/wxpay/pay.action”

通知规则

商户退款完成后,微信会把相关退款结果和用户信息发送给清算机构,清算机构需要接收处理后返回应答成功,然后继续给异步通知到下游从业机构。

对后台通知交互时,如果微信收到应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。(通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m)

通知报文

退款结果通知是以POST 方法访问商户设置的通知url,通知的数据以JSON 格式通过请求主体(BODY)传输。通知的数据包括了加密的支付结果详情
(注:由于涉及到回调加密和解密,商户必须先设置好apiv3密钥后才能解密回调通知,apiv3密钥设置文档指引详见APIv3密钥设置指引)

参数解密

下面详细描述对通知数据进行解密的流程:

  1. 1、用商户平台上设置的APIv3密钥 【微信商户平台—>账户设置—>API安全—>设置APIv3密钥】 ,记为key;
  2. 2、针对resource.algorithm中描述的算法(目前为AEAD_AES_256_GCM),取得对应的参数nonce和associated_data;
  3. 3、使用key、nonce和associated_data,对数据密文resource.ciphertext进行解密,得到JSON形式的资源对象。

注: AEAD_AES_256_GCM算法的接口细节,请参考rfc5116。微信支付使用的密钥key长度为32个字节,随机串nonce长度12个字节,associated_data长度小于16个字节并可能为空字符串。

通知参数

参数名 变量 类型[长度限制] 必填 描述
通知ID id string[1,36] 通知的唯一ID 示例值:EV-2018022511223320873
通知创建时间 create_time string[1,32] 通知创建的时间,遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE,yyyy-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示,北京时间2015年5月20日13点29分35秒。 示例值:2018-06-08T10:34:56+08:00
通知类型 event_type string[1,32] 通知的类型: REFUND.SUCCESS:退款成功通知 REFUND.ABNORMAL:退款异常通知 REFUND.CLOSED:退款关闭通知 示例值:REFUND.SUCCESS
通知简要说明 summary string[1,16] 通知简要说明 示例值:退款成功
通知数据类型 resource_type string[1,32] 通知的资源数据类型,支付成功通知为encrypt-resource 示例值:encrypt-resource
+通知数据 resource object 通知资源数据 json格式,见示例

通知签名

加密不能保证通知请求来自微信。微信会对发送给商户的通知进行签名,并将签名值放在通知的HTTP头Wechatpay-Signature。商户应当验证签名,以确认请求来自微信,而不是其他的第三方。签名验证的算法请参考 《微信支付API v3签名验证》。

回调示例

退款通知
{
    "id":"EV-2018022511223320873",
    "create_time":"2018-06-08T10:34:56+08:00",
    "resource_type":"encrypt-resource",
    "event_type":"REFUND.SUCCESS",
    "summary":"退款成功",
    "resource" : {
        "original_type": "refund",
        "algorithm":"AEAD_AES_256_GCM",
        "ciphertext": "...",
        "associated_data": "",
        "nonce": "..."
    }
}
商户对resource对象进行解密后,得到的资源对象示例
{
    "mchid": "1900000100",
    "transaction_id": "1008450740201411110005820873",
    "out_trade_no": "20150806125346",
    "refund_id": "50200207182018070300011301001",
    "out_refund_no": "7752501201407033233368018",
    "refund_status": "SUCCESS",
    "success_time": "2018-06-08T10:34:56+08:00",
    "user_received_account": "招商银行信用卡0403",
    "amount" : {
        "total": 999,
        "refund": 999,
        "payer_total": 999,
        "payer_refund": 999
    }
}

通知参数

参数名 变量 类型[长度限制] 必填 描述
直连商户号 mchid string[1,32] 直连商户的商户号,由微信支付生成并下发。 示例值:1900000100
商户订单号 out_trade_no string[1,32] 返回的商户订单号 示例值: 1217752501201407033233368018
微信支付订单号 transaction_id string[1,32] 微信支付订单号 示例值: 1217752501201407033233368018
商户退款单号 out_refund_no string[1,64] 商户退款单号 示例值: 1217752501201407033233368018
微信支付退款单号 refund_id string[1,32] 微信退款单号 示例值: 1217752501201407033233368018
退款状态 refund_status string[1,16] 退款状态,枚举值: SUCCESS:退款成功 CLOSED:退款关闭 ABNORMAL:退款异常,退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往【商户平台—>交易中心】,手动处理此笔退款 示例值:SUCCESS
退款成功时间 success_time string[1,64] 1、退款成功时间,遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE,yyyy-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示,北京时间2015年5月20日13点29分35秒。 2、当退款状态为退款成功时返回此参数。 示例值:2018-06-08T10:34:56+08:00
退款入账账户 user_received_account string[1,64] 取当前退款单的退款入账方。 1、退回银行卡:{银行名称}{卡类型}{卡尾号} 2、退回支付用户零钱: 支付用户零钱 3、退还商户: 商户基本账户、商户结算银行账户 4、退回支付用户零钱通:支付用户零钱通 示例值:招商银行信用卡0403
+金额信息 amount object 金额信息

通知应答

**接收成功:**HTTP应答状态码需返回200或204,无需返回应答报文。

**接收失败:**HTTP应答状态码需返回5XX或4XX,同时需返回应答报文,格式如下:

**注意:**重试过多会导致微信支付端积压过多通知而堵塞,影响其他正常通知。

参数名 变量 类型[长度限制] 必填 描述
返回状态码 code string[1,32] 错误码,SUCCESS为接收成功,其他错误码为失败 示例值:SUCCESS
返回信息 message string[1,256] 返回信息,如非空,为错误原因 示例值:系统错误
{   
    "code": "SUCCESS"
}

* 实现退款通知

1、验签解析请求体,再对加密数据解密

	/**
     * 退款结果通知
     * 退款状态改变后,微信会把相关退款结果发送给商户
     */
    @PostMapping("/refunds/notify")
    public String refundsNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
        log.info("退款通知执行");
        Gson gson = new Gson();
        Map<String, String> map = new HashMap<>();//应答对象
        //处理通知参数,将请求体转换为字符串
        try {
            String body = HttpUtils.readData(request);
            log.info("退款通知的完整数据 ===> {}", body);
            //验签和解析请求体并解密报文
            String decryptData = notificationHandlerUtils.getDecryptData(request, body);
            log.info("退款通知解密完的数据 ===> {}", decryptData);

            //对订单进行处理
            wxPayService.processRefund(decryptData);

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

2、根据明文处理退款单

 	/**
     * 处理退款通知
     * @param decryptData 解密后的明文
     */
    @Override
    public void processRefund(String decryptData) {
        Gson gson = new Gson();
        Map<String, Object> decryptDataMap = gson.fromJson(decryptData, HashMap.class);
        String orderNo = (String) decryptDataMap.get("out_trade_no");
        /*在对业务数据进行状态检查和处理之前,
        要采用数据锁进行并发控制,
        以避免函数重入造成的数据混乱*/
        //尝试获取锁:
        // 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放
        if (lock.tryLock()) {
            try {
                //处理重复通知
                //保证接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的
                String orderStatus = orderInfoService.getOrderStatus(orderNo);
                //申请退款后的OrderStatus == REFUND_PROCESSING
                if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)) {
                    return;
                }
                //更新订单状态 -- 退款成功
                orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
                //将回调通知的请求体存入数据库
                refundInfoService.updateRefund(decryptData);
            } finally {
                lock.unlock();
            }
        }
    }

下载账单

1、获取账单URL

	/**
     * 申请账单
     *
     * @param billDate
     * @param type
     * @return
     */
    @Override
    public String queryBill(String billDate, String type) throws IOException {
        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);

        HttpGet httpGet = new HttpGet(url);
        httpGet.addHeader("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
                log.info("成功");
            } else {
                log.info("申请账单异常,响应码 = " + statusCode + ",申请账单异常返回结果 = " + bodyAsString);
                throw new IOException("request failed");
            }
            //获取账单下载地址
            Gson gson = new Gson();
            Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
            return resultMap.get("download_url");
        } finally {
            response.close();
        }

2、下载无需对应答验签

	/**
     * 获取 HttpClient 对象 无需进行应答签名验证,跳过验签的流程
     *
     * @return
     */
    @Bean(name = "wxPayNoSignClient")
    public CloseableHttpClient getWxPayNoSignClient() {
        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                //设置商户信息
                .withMerchant(mchId, mchSerialNo, getPrivateKey(privateKeyPath))
                //无需进行签名验证、通过withValidator((response) -> true)实现
                .withValidator((response) -> true);

        // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
        CloseableHttpClient httpClient = builder.build();
        return httpClient;
    }
	/**
     * 下载账单
     * @param billDate
     * @param type
     * @return
     */
    @Override
    public String downloadBill(String billDate, String type) throws IOException {
        log.warn("下载账单接口调用 {}, {}", billDate, type);
        String downloadUrl = queryBill(billDate, type);

        HttpGet httpGet = new HttpGet(downloadUrl);
        httpGet.addHeader("Accept", "application/json");
        //完成签名并执行请求,无需验签响应结果
        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) { //处理成功,无返回Body
                log.info("成功");
            } else {
                log.info("下载账单异常,响应码 = " + statusCode + ",下载账单异常返回结果 = " + bodyAsString);
                throw new IOException("request failed");
            }
            return bodyAsString;
        } finally {
            response.close();
        }
    }

你可能感兴趣的:(支付对接,spring,boot,mysql,java)