前提

在进行JSAPI微信支付之前,需要准备好一下配置

  • 申请小程序的appid:wxaxxxxxxxxxxbxx8a (类似这样的)

  • 申请商户号:1xxxxxxxxx6

  • 小程序开通微信支付,绑定已经申请好的商户号。登录小程序后台(mp.weixin.qq.com)。点击左侧导航栏的微信支付,在页面中进行开通。(

    注意:以上信息的申请都需要使用企业账户,个人账户不行

    ​ 商户号官网地址:pay.weixin.qq.com

    ​ 小程序官网地址: mp.weixin.qq.com

    • 需要在商户端(pay.weixin.qq.com),api安全配置好apiv3的密钥

整体介绍

博主这篇博客,主要是小程序对接微信支付(JSAPI)

后端:spring boot

前端:微信小程序,uinapp

适用人群:已经申请好所有的资料,小程序平台,微信商户平台等等,本文不提供任何资料。并且需要有自己的业务场景,部分代码无法直接运行,需要加入自己的订单结构

我的maven依赖

<dependencies>
        <dependency>
            <groupId>org.jdom</groupId>
            <artifactId>jdom2</artifactId>
            <version>2.0.6.1</version>
        </dependency>
<!--        json处理-->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
        </dependency>
<!--        微信支付SDk-->
        <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-apache-httpclient</artifactId>
            <version>0.4.7</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
<!--        mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.25</version>
        </dependency>
<!--        mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.1</version>
        </dependency>
<!--        lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
<!--        web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
<!--        swagger-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.7.0</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.7.0</version>
        </dependency>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

1、整体流程

支付的流程图:
在这里插入图片描述

1-1、如上图,第2点请求下单,访问我们自己的后端接口所需的参数

字段名变量名类型必填示例值描述
应用IDappidstring[1,32]wxd678efh567hg6787由微信生成的应用ID,全局唯一。请求基础下单接口时请注意APPID的应用属性,例如公众号场景下,需使用应用属性为公众号的服务号APPID
直连商户号mchidstring[1,32]1230000109直连商户的商户号,由微信支付生成并下发。
商品描述descriptionstring[1,127]Image形象店-深圳腾大-QQ公仔商品描述
商户订单号out_trade_nostring[6,32]1217752501201407033233368018商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一
通知地址notify_urlstring[1,256]可以先随便写一个不存的地址都行,不影响正常支付,但是获取不到支付结果信息,无法进行修改订单状态异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 公网域名必须为https,如果是走专线接入,使用专线NAT IP或者私有回调域名可使用http
订单金额amountobjectHashMap<Object, Object> amountMap = new HashMap<>(); amountMap.put(“total”,fee);//金额 amountMap.put(“currency”,“CNY”);//货币类型订单金额信息,他需要一个map,需要进行一层嵌套,可以去参考官网https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml
支付者payerobject//支付者 HashMap<Object, Object> playerMap = new HashMap<>(); playerMap.put(“openid”,openid);支付者信息,用户在直连商户appid下的唯一标识。 下单前需获取到用户的Openid

以上参数需要注意,openid的获取

2、openid 的获取

需要在小程序端调用wx.login获取临时登陆凭证在获取openid

wx.login({
  success (res) {
    if (res.code) {
      //发起网络请求
      wx.request({
        url: 'https://example.com/onLogin',//这个接口是需要自己去编写的。也就是下方的那个controller   @GetMapping("/onLogin") public string onLogin(HttpServletRequest request)方法
        data: {
          code: res.code
        }
      })
    } else {
      console.log('登录失败!' + res.errMsg)
    }
  }
})


通过上面这个方法获取到res.code然后我们自己在编写一个后端接口,去获取openid

下面这个是我自己写的controller

		
		@Resource
    private WxPayConfig wxPayConfig;//这个是一个wx的配置类
	
    @Resource
    private CloseableHttpClient wxPayClient;//配置类中的一个bean

@GetMapping("/onLogin")
    public string onLogin(HttpServletRequest request){
        String js_code = request.getParameter("code");//前端发起请求携带上面获取到的code,后端接收

      //app Secret是小程序密钥(在mp.weixin.qq.com中的开发管理-》开发设置-》AppSecret(小程序密钥)中设置)
        String baseUrl="https://api.weixin.qq.com/sns/jscode2session?appid="+wxPayConfig.getAppid()+"&secret="+wxPayConfig.getAppSecret()
          +"&js_code="+js_code+"&grant_type=authorization_code";
        String res=null;
        try {
          //这里发起请求获取到session-key,和openid
            res = requestByGetMethod(baseUrl).split("/n")[0];
            System.out.println(res);
        } catch (Exception e) {
            e.printStackTrace();
        }
        log.info("res:"+res);
        return res;//返回给前端
    }


//这个方法就是用于发起get请求的
/**
     * 模拟发送url Get 请求
     * @param url
     * @return
     */
    public String requestByGetMethod(String url) {
        log.info("发起get请求");
        CloseableHttpClient httpClient = HttpClients.createDefault();
        StringBuilder entityStringBuilder = null;
        try {
            HttpGet get = new HttpGet(url);
            CloseableHttpResponse httpResponse = null;
            httpResponse = httpClient.execute(get);
            try {
                HttpEntity entity = httpResponse.getEntity();
                entityStringBuilder = new StringBuilder();
                if (null != entity) {
                    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(httpResponse.getEntity().getContent(), "UTF-8"), 8 * 1024);
                    String line = null;
                    while ((line = bufferedReader.readLine()) != null) {
                        entityStringBuilder.append(line + "/n");
                    }
                }
            } finally {
                httpResponse.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (httpClient != null) {
                    httpClient.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return entityStringBuilder.toString();
    }

3、统一下单Controller(预支付订单)

调用下单接口,返回prepay_id等信息提供给前端,供前端调起支付页面,这里也对应官方图的第二点下单请求

注意:在请求中你需要携带一下参数,具体需要的参数可以看1-1的表格

在这里插入图片描述

下面这个controller是在前端

		
		@Resource
    private WxPayConfig wxPayConfig;

    @Resource
    private CloseableHttpClient wxPayClient;

    @Resource
    private Verifier verifier;

		@ResponseBody
    @RequestMapping("returnparam")
    public HashMap<String, String> doOrder(HttpServletRequest request, HttpServletResponse response) throws Exception{
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        //得到openid(微信用户唯一的openid)
        String openid = request.getParameter("openid");
        //得到价钱(自定义)
        int fee = 0;//单位是分
        if (null != request.getParameter("price")) {
            fee = Integer.parseInt(request.getParameter("price").toString());
        }
        //得到商品的ID(自定义)
        String goodsid=request.getParameter("goodsid");
        //订单标题(自定义)
        String title = request.getParameter("title");
        //时间戳,
        String times = System.currentTimeMillis() + "";

        //订单编号(自定义 这里以时间戳+随机数)
        Random random = new Random();
        String did = times+random.nextInt(1000);

        log.info("生成订单");
        //调用统一下单API
        HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi");
        // 请求body参数
        Gson gson = new Gson();
        HashMap<Object, Object> paramsMap = new HashMap<>();
        paramsMap.put("appid",wxPayConfig.getAppid());//appid
        paramsMap.put("mchid",wxPayConfig.getMchId());//商户号
        paramsMap.put("description",title);//商品描述
        paramsMap.put("out_trade_no",did);//商户订单号
        paramsMap.put("notify_url","http://d4a93w.natappfree.cc/wxBuy");//通知地址,可随便写,如果不需要通知的话,不影响支付,但是影响后续修改订单状态

        //订单金额
        HashMap<Object, Object> amountMap = new HashMap<>();
        amountMap.put("total",fee);//金额
        amountMap.put("currency","CNY");//货币类型
        paramsMap.put("amount",amountMap);

        //支付者
        HashMap<Object, Object> playerMap = new HashMap<>();
        playerMap.put("openid",openid);

        paramsMap.put("payer",playerMap);

        //将参数转化未json字符串
        String jsonParamsMap = gson.toJson(paramsMap);
        log.info("请求参数:"+jsonParamsMap);

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

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

        try {
            int statusCode = resp.getStatusLine().getStatusCode();
            String bodyAsString = EntityUtils.toString(resp.getEntity());
            if (statusCode == 200) { //处理成功
                log.info("成功,返回结果 = " + bodyAsString);
            } else if (statusCode == 204) { //处理成功,无返回Body
                log.info("成功");
            } else {
                System.out.println("小程序下单失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString);
                throw new IOException("request failed");
            }
            //相应结果
            HashMap<String,String> resultMap = gson.fromJson(bodyAsString, HashMap.class);


          	//获取prepay—id
            String prepayId = resultMap.get("prepay_id");


            //获取到perpayid之后需要对数据进行二次封装,前端调起支付必须存在的参数
            HashMap<String, String> payMap = new HashMap<>();
            payMap.put("appid",wxPayConfig.getAppid());//appid
            long currentTimestamp = System.currentTimeMillis();//时间戳,别管那么多,他就是需要
            payMap.put("timeStamp",currentTimestamp+"");
            String nonceStr = UUID.randomUUID().toString()
                .replaceAll("-", "")
                .substring(0, 32);;//随机字符串,别管那么多他就是需要,要咱就给
            payMap.put("nonceStr",nonceStr);
						//apiv3只支持这种加密方式
            payMap.put("signType","RSA");
						
            payMap.put("package","prepay_id="+prepayId);

            //通过appid,timeStamp,nonceStr,signType,package以及商户密钥进行key=value形式进行拼接加密
         	 	//加密方法我会放在这个代码段段下面
            String aPackage = buildMessageTwo("传入你的appid", currentTimestamp, nonceStr, payMap.get("package"));
          
            //获取对应的签名
          	//加密方法我会放在这个代码段段下面
            String paySign = sign(wxPayConfig.getPrivateKeyPath(),aPackage.getBytes("utf-8"));

            payMap.put("paySign",paySign);
          
         /**
          *	在这里你可以加入自己的数据库操作,存储一条订单信息,状态为未支付就行了
          *	在这里你可以加入自己的数据库操作,存储一条订单信息,状态为未支付就行了
          *	在这里你可以加入自己的数据库操作,存储一条订单信息,状态为未支付就行了
          */

            log.info("给前端的玩意:"+payMap);//前端会根据这些参数调起支付页面
					//到这里,就已经完成了官网图中的第8步了
            return payMap;

        }finally {
            resp.close();
        }
    }

4、配置类和配置文件

注意:下面这个配置文件需要读取resources中的wxpay.properties配置文件,等下我也会把我的wxpay.properties贴到下方,还有一个证书文件,需要放置在与src同级的目录中《apiclient_key.pem》,这个文件的获取在https://pay.weixin.qq.com/index.php/core/cert/api_cert#/中申请API证书,也就是申请APIV3的那个页面

一定要记得把证书放入到项目目录中!!!
aoiclient_key.pem就是证书,放置在与src同级目录中

在这里插入图片描述

下方这个是一个properties文件,跟yml配置同级

在这里插入图片描述

package com.wanliu.paymentdemo.config;

//import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
//import com.wechat.pay.contrib.apache.httpclient.auth.*;
//import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
//import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.util.Base64;
import java.util.Map;
import java.util.UUID;


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

    // 商户号
    private String mchId;

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

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

    // APIv3密钥
    private String apiV3Key;

    // APPID
    private String appid;

    // 微信服务器地址,这个字段没有在,本文中使用到可以不用管
    private String domain;

    // APIv2密钥
    private String partnerKey;

    //小程序密匙
    private String appSecret;




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

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

    /**
     * 获取签名验证器
     * @return
     */
    @Bean
    public Verifier getVerifier() throws Exception {
        log.info("获取签名验证器");
        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(privateKeyPath);
        // 私钥签名对象
        PrivateKeySigner keySigner = new PrivateKeySigner(mchSerialNo, privateKey);
        // 身份认证对象
        WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, keySigner);

        // 获取证书管理器实例
        CertificatesManager certificatesManager = CertificatesManager.getInstance();
        // 向证书管理器增加需要自动更新平台证书的商户信息
        certificatesManager.putMerchant(mchId, wechatPay2Credentials,
                apiV3Key.getBytes(StandardCharsets.UTF_8));
        // ... 若有多个商户号,可继续调用putMerchant添加商户信息
        Verifier verifier = certificatesManager.getVerifier(mchId);

        return verifier;
    }

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

        log.info("获取httpclient");

        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(privateKeyPath);
        // 从证书管理器中获取verifier
        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant(mchId, mchSerialNo, privateKey)
                .withValidator(new WechatPay2Validator(verifier));
        // ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient

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

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

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

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

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

        log.info("== getWxPayNoSignClient END ==");
        return httpClient;
    }


    public String decryptFromResource(Map<String,Object> bodyMap) throws GeneralSecurityException {
        log.info("秘文解密");

        //通知数据
        Map<String,String > resourceMap =(Map<String, String>) bodyMap.get("resource");
        //数据秘文
        String ciphertext = resourceMap.get("ciphertext");
        //获取随机串
        String nonce = resourceMap.get("nonce");
        String associated_data = resourceMap.get("associated_data");

        log.info("秘文===》{}",ciphertext);
        AesUtil aesUtil = new AesUtil(getApiV3Key().getBytes(StandardCharsets.UTF_8));
        //获取明文(解密后的数据)
        String plainText = aesUtil.decryptToString(associated_data.getBytes(StandardCharsets.UTF_8),
                nonce.getBytes(StandardCharsets.UTF_8),
                ciphertext);

        log.info("明文====》{}",plainText);

        return plainText;
    }


    /**
     * 拼接五个参数
     * @param appId
     * @param timestamp
     * @param nonceStr
     * @param packag
     * @return
     */
    public  String buildMessageTwo(String appId, long timestamp, String nonceStr, String packag) {
        return appId + "\n"
                + timestamp + "\n"
                + nonceStr + "\n"
                + packag + "\n";
    }

    /**
     * 进行二次封装
     * @param wxCertPath
     * @param message
     * @return
     * @throws NoSuchAlgorithmException
     * @throws SignatureException
     * @throws IOException
     * @throws InvalidKeyException
     * @throws java.security.InvalidKeyException
     */
    public  String sign(String wxCertPath,byte[] message) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException, java.security.InvalidKeyException {
        Signature sign = Signature.getInstance("SHA256withRSA"); //SHA256withRSA
        sign.initSign(PemUtil.loadPrivateKey(new FileInputStream(wxCertPath))); // 微信证书私钥
        sign.update(message);
        return Base64.getEncoder().encodeToString(sign.sign());
    }
    /**
     * 获取32位随机字符串
     * @return
     */
    public  String getNonceStr(){
        return UUID.randomUUID().toString()
                .replaceAll("-", "")
                .substring(0, 32);
    }
    /**
     * 获取当前时间戳,单位秒
     * @return
     */
    public long getCurrentTimestamp() {
        return System.currentTimeMillis()/1000;
    }
}

我的wxpay.properties,这里我没有用完自己小程序的配置,这里使用的是尚硅谷的,谷粒学院的公众号的配置,请各位修改成自己小程序的配置,这点很重要!!!

# 微信支付相关参数
# 商户号
wxpay.mch-id=1558950191

# 商户API证书序列号
wxpay.mch-serial-no=34345964330B66427E0D3D28826C4993C77E631F
# 商户私钥文件
wxpay.private-key-path=apiclient_key.pem
# APIv3密钥
wxpay.api-v3-key=UDuLFDcmy5Eb6o0nTNZdu6ek4DDh4K8B # 你申请的APIv3密钥
# APPID
wxpay.appid=wx74862e0dfcf69954
# 微信服务器地址
wxpay.domain=https://api.mch.weixin.qq.com
# 接收结果通知地址
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
wxpay.notify-domain=https://500c-219-143-130-12.ngrok.io

# APIv2密钥
wxpay.partnerKey=T6m9iK73b0kn9g5v426MKfHQH7X8rKwb


#小程序密匙
wxpay.appSecret=你的小程序密钥,在mp.weixin.qq.com中的开发设置里面,appid的下面可以去配置,或重置

5、工具类

httpUtils解析通知信息

这个HttpUtils主要是用于解析notify_url回调地址,传回来的数据,解析请求头中的数据,微信官方会告诉你支付结果,并由你根据这个结果的状态做一些处理,比如修改数据库订单状态或者其他的

httpUtils主要是解析微信给我们的通知信息,有什么(强调,解析出来的东西也有加密的信息,需要再次解密,也就是验签的过程了)

注意:你接收到通知后,需要应答微信官方,否则微信官方会认为通知失败,然后在24h4m内,反复通知你

在这里插入图片描述

package com.wanliu.paymentdemo.util;

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


public class HttpUtils {

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

WechatPay2ValidatorForRequest验签

WechatPay2ValidatorForRequest就是从微信官方发给我的加密信息,进行验签解密。大家主要看WechatPay2ValidatorForRequest中的validate方法即可

package com.wanliu.paymentdemo.util;


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

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

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


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

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

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

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

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

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

            //构造验签名串
            String message = buildMessage(request);
            //从请求头中拿到验签名序列号
            String serial = request.getHeader(WECHAT_PAY_SERIAL);
            //从请求头中拿到携带的签名
            String signature = request.getHeader(WECHAT_PAY_SIGNATURE);

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

        return true;
    }

    protected final void validateParameters(HttpServletRequest request) {

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

        String header = null;
        for (String headerName : headers) {
            header = request.getHeader(headerName);

            if (header == null) {
                throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
            }
        }

        //获取时间戳,判断请求是否过期
        String timestampStr = header;
        try {
            //通过时间戳,创建一个基于时间戳的时间对象
            Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
            // 拒绝过期的请求
            if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
                throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
            }
        } catch (DateTimeException | NumberFormatException e) {
            throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
        }
    }

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

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

}

6、前端接收到必要的参数,进行调起支付页面

在这里调起支付页面之后,用户进行输入密码,支付等等,如果支付那么微信商户平台会收到你的汇款,然后微信官方会发起请求通知你支付状态,也就是刚刚你在统一下单的时候设置的notify_url参数的接口地址

这里的必要参数也就是第三步统一下单那个步骤中的,@RequestMapping(“returnparam”)接口返回的参数

uni.request({//这个是uniapp版本,下方我会贴微信小程序版本,微信小程序的往下看
    url: that.$api.baseURL+"/returnparam",//你的后端接口
    method:"GET",
    data:{//要传入的参数
     //openid
     openid: openId,
     //订单总金额
     price: order.amount,
     //订单号
     goodsid: order.number,
     //title
     title: ''
    },//上面这里是去调用下单
    success: (res2) => {//这里是访问returnparam接口成功后就会调起支付
     console.log(res2);
     uni.requestPayment({//这里是官方图中的第9步,这里是uniapp官方的调起支付方法
       //后端返回的参数,这里下面就是returnparam接口返回的参数
      timeStamp: res2.data.data.timeStamp,
      nonceStr: res2.data.data.nonceStr,
      package: res2.data.data.package,
      signType: res2.data.data.signType,
      paySign: res2.data.data.paySign,
      appId: res2.data.data.appid,
      success (res3) { 
       console.log(res3);
      },
      fail (res3) { 
       console.log(res3);
      }
     })
    }
   })

这里就是微信小程序的一个请求过程

   let result = await request({
            url: '/returnparam',
            method: 'get',
            data: {
            //openid
            openid:wx.getStorageSync("openid"),
            //订单总金额
            price:this.data.price,
            //订单号
            goodsid:wx.getStorageSync("orderCode"),
            //title
            title: '订单描述',
            },
          });
          console.log(result);
          if(result.appid!=null){
        wx.requestPayment({
            timeStamp:result.timeStamp,
            nonceStr:result.nonceStr,
            package: result.package,
            signType: result.signType,
            paySign: result.paySign,
            appId: request.appid,
            success:function(res){ 
                wx.showToast({
                    title: '支付成功!',
                }),
                setTimeout(()=>
                {
                wx.redirectTo({
                    url: '/pages/index/index'
                })
                }, 3000)
            },
            fail :function(res) {
                console.log("调用失败");
                console.log(res);
             },
             complete:function(res){
                 console.log("成功与否");
             }
          })

7、微信支付通知,notify_url的回调Controller

这个一定要是外网可以访问的地址,建议使用内网穿透

给大家推荐一篇内网穿透的博客:https://blog.csdn.net/Lfl202116888/article/details/124932062?spm=1001.2014.3001.5502

		@Resource
    private WxPayConfig wxPayConfig;

    @Resource
    private CloseableHttpClient wxPayClient;

    @Resource
    private Verifier verifier;


		/**
     * 微信通知回调地址
     * @param request
     * @param response
     * @return
     */
    @PostMapping("/wxBuy")
    public String wxBuy(HttpServletRequest request,HttpServletResponse response){

        Gson gson = new Gson();
        //创建一个应答对象

        HashMap<String, String> map = new HashMap<>();

        try {
            //处理通知参数
            String body = HttpUtils.readData(request);
            HashMap<String,Object> bodyMap = gson.fromJson(body, HashMap.class);

            String requestId = (String) bodyMap.get("id");

            log.info("支付通知的id=====》》》{}",bodyMap.get("id"));
//            log.info("支付通知的完整数据=====》》》{}",body);

            //TODO : 签名的验证
            WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest = new WechatPay2ValidatorForRequest(verifier, requestId,body);
            if (!wechatPay2ValidatorForRequest.validate(request)) {
                log.error("通知验签失败");
                //通知失败应答
                response.setStatus(500);
                map.put("code","ERROR");
                map.put("message","通知验签失败");
                return gson.toJson(map);
            }
            log.info("通知验签成功");
          //TODO : 处理订单
            //这里可以调用你要处理业务逻辑的service,我这里就不写了,
            //然后解密数据在业务service中调用就行,我在这里就直接调用了
            
            //解密密文,获取明文
            String plaintText = wxPayConfig.decryptFromResource(bodyMap);
            //将明文转为map
            HashMap plaintTextMap = gson.fromJson(plaintText, HashMap.class);
          
          
            //获取支付下单的时候,传入的商户订单号,可以根据这个订单号去获取我们的一个订单记录,从而更新订单状态
            String orderNo = (String) plaintTextMap.get("out_trade_no");
            //业务编号
            String transactionId = (String) plaintTextMap.get("transaction_id");
            //trade_type,支付类型,如果有需要的话, 你可以存储在数据库中,这里我们的数据,基本上都是JSapi支付类型
            String tradeType = (String) plaintTextMap.get("trade_type");
            //交易状态
            String tradeState = (String) plaintTextMap.get("trade_state");
            //还有很多,为这里就不一一去写了

          
          
           /**
             * 在更新你订单状态之前,可以先根据orderNo,查询数据库中是否有这个订单
             * 然后查询这个订单是否已经被处理过,也就是状态是否是已经支付的状态
             * 如果这个订单已经被处理了,那么我们可以直接return,没有被处理过我们在处理
             * 这样可以避免数据库被反复的操作
             *
             * 微信官方的解释是:
             *  同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。
             *  推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,
             *  并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,
             *  则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,
             *  以避免函数重入造成的数据混乱。
             */
          
          

            //更新订单状态
            /*
            * 你的数据库操作
            * 一定要存储解密出来的transaction_id字段在数据库中
            * 如果需要跟微信官方对账的话,是需要提供这个字段进行一个查账的
            * */

            
            //成功应答
            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);
        }
    }

在上面这个controller中,处理订单那个部分,其实还有很多参数可以去获取,根据业务需要求,我这里只是举例,获取了几个比较重要的,具体详细可以去官网中查看:微信支付-开发者文档 (qq.com)

还有具体的业务,我也没有去写了,大家根据你们自己的业务去完成即可

在这里插入图片描述

8、前端小程序端,定时器调用查询订单状态

支付成功跳转支付成功的页面,然后清除定时器停止循环查单,如果用户一直没有支付的话,定时器反复查单,接口只会返回一个101的状态码,和一个支付中的状态信息。如果用户退出支付页面,也要记得关闭定时器.。这里的查单是查询我们自己数据库中的订单状态不是,微信官方的查单

9、后端提供给小程序查询订单状态的接口

因为我这里没有具体的业务操作,所以我吧大体思路给大家写了一下,大家可以参考一下

/**
     * 提供给前端查询支付订单状态的接口
     * @param orderNo
     * @return
     */
    @GetMapping("/query-order-status/{orderNo}")
    public R queryOderStatus(@PathVariable("orderNo") String orderNo){
        //调用你的service去根据orderNo*(订单id)去获取当前订单
        //if判断,你根据订单id查询到到订单对象中到订单状态是否为成功状态(已经支付)
        //成功返回,状态码200,已经消息,支付成功
        //如果状态不是已经支付,则返回状态码101(这个状态码自己自己设定,只是提供给前端做判断的),消息,支付中

    }

10、用户取消订单

用户如果取消订单的话,那么要干两件事情

  • 调用微信支付官方的,关单接口
  • 更改为我们自己数据库中,这条订单的订单状态为,取消订单
    @PostMapping("/cancel/{orderNo}")
        public R cancel(@PathVariable String orderNo) throws Exception {
            log.info("取消订单");
    
            //调用service业务,我这里为了给大家方便掩饰,就直接写controller中了
    
            //调用微信支付的关单接口
            closeOrder(orderNo);
    
    
            //修改我们自己数据库中的订单状态为订单已取消
            //这里我没有具体业务,我就不写了,大家根据自己的情况去写
    
            return R.ok().setMessage("订单已取消");
        }
    
        public void closeOrder(String orderNo) throws Exception {
            log.info("关单接口的调用,订单号===》{}",orderNo);
    
            //微信官方提供的关单url,我们进行一个拼串
            String url="https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/"+orderNo+"/close";
            HttpPost httpPost = new HttpPost(url);
            //在请求体中还需要携带商户号
            Gson gson = new Gson();
            HashMap<String, String> paramsMap = new HashMap<>();
            paramsMap.put("mchid",wxPayConfig.getMchId());
            //参数都组装完成之后,需要转为json
            String jsonParamsMap = gson.toJson(paramsMap);
            //将请求参数设置到请求对象中
            StringEntity entity = new StringEntity(jsonParamsMap,"utf-8");
            entity.setContentType("application/json");
            httpPost.setEntity(entity);
            httpPost.setHeader("Accept", "application/json");
    
            //完成签名并执行请求
            CloseableHttpResponse resp = wxPayClient.execute(httpPost);
    
            try {
                int statusCode = resp.getStatusLine().getStatusCode();
                if (statusCode == 200) { //处理成功
                    log.info("成功200," );
                } else if (statusCode == 204) { //处理成功,无返回Body
                    log.info("成功204");
                } else {
                    System.out.println("小程序下单失败,响应码 = " + statusCode );
                    throw new IOException("request failed");
                }
    
    
    
            }finally {
                resp.close();
            }
        }

11、商户端迟迟未收到异步通知结果

如果我们商户后台,迟迟没有收到异步通知的结果,那么我们应该主动的去调用微信官方的查单接口,查询这个订单到底有没有支付成功,如果成功了我们就要去修改我们的数据库中这条订单的,订单状态

为什么要这么做呢?

因为有的时候,用户支付成功之后,可能因为网络的一个延迟啊,或者有些其他的原因导致,消息没有及时的更新,微信官方,也没有及时的给我们发送一个通知,没有发送通知的话,我们商户端,是不可能去修改用户的这个订单为,已支付的一个状态。

所以我们如果一直没有收到这个订单的消息,我们需要主动的查询这个订单的一个情况

所以说,这个查单的这个流程,肯定是需要我们在后端写一个定时任务去调用的

这里只是一个查询订单的一个接口,如果有需要的话大家可以看看

 @GetMapping("/query/{orderNo}")
    public R queryOrder(@PathVariable String orderNo) throws Exception {
        String result = findOrder(orderNo);
        return R.ok().setMessage("查询成功").data("result",result);
    }

    public String findOrder(String orderNo) throws Exception {
        log.info("查询订单");
//        同理,这里应该是调用业务方法。为了方便演示,我直接写在controller中
        String url="https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/"+orderNo+"?mchid="+wxPayConfig.getMchId();
        HttpGet httpGet = new HttpGet(url);
        httpGet.setHeader("Accept","application/json");

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


        try {
            int statusCode = resp.getStatusLine().getStatusCode();
            String bodyAsString = EntityUtils.toString(resp.getEntity());
            if (statusCode == 200) { //处理成功
                log.info("成功,返回结果 = " + bodyAsString);
            } else if (statusCode == 204) { //处理成功,无返回Body
                log.info("成功");
            } else {
                System.out.println("小程序下单失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString);
                throw new IOException("request failed");
            }
            log.info("bodyAsString:"+bodyAsString);
            return bodyAsString;
        }finally {
            resp.close();
        }
    }

这个才是定时任务,开始轮询查单

注意:这里开启定时任务的这个注解,一定要先在spring boot的启动类上加一个注解:@EnableScheduling

 /**
     * 从第0秒开始每隔30秒执行一次,查询创建超过5分钟,并且没有支付第订单
     */
    @Scheduled(cron = "0/30 * * * * ?")
    public void orderConfirm() throws Exception {

        //这里还是一样,应该是去调用业务方法,为了演示我直接写出来

        //TODO :查询创建超过5分钟并且没有支付的订单
        Instant instant=Instant.now().minus(Duration.ofMinutes(5));//得到五分钟之前的时间

        //创建查询对象
        QueryWrapper<OrderInfo> queryWrapper=new QueryWrapper<>();
        //拼接查询调教
        queryWrapper.eq("order_status","未支付");//这里的未支付,应该是写枚举,或者0,1,2这些数据的,这里为了一眼能让大家看出来我就直接写了,状态=未支付
        queryWrapper.le("create_time",instant);

        //这里调用查询所以订单的方法,吧queryWrapper传入进去,得到一个list,便是所有超过五分钟,并且未支付的订单了
        //这里我就模拟一个结果出来,当然他没有数据
        List<OrderInfo> orderInfoList=new ArrayList<>();

        //这里查询出所有的超时订单之后,我们应该去核实一下,这些超时的订到单,是不是真的都是未支付的
        //所以我们需要去调用,微信官方的,查单接口
        //因为这里需要去判断,是用户真的没有支付,还是用户支付了,但是微信官方给我们发起的通知,我们没有接收到,导致订单状态没有被更改
        //如果是用户没有支付,那我们需要对这个订单进行一个关单,并修改状态为超时
        //如果用户支付了,但是是因为我们没有收到通知,那么我们则主动查单,修改订单状态为支付成功

        for (OrderInfo orderInfo : orderInfoList) {
            //直接调用写好对查单方法即可
            String result = findOrder(orderInfo.getOrderNo());
            Gson gson = new Gson();
            HashMap hashMap = gson.fromJson(result, HashMap.class);
            //获取微信官方的订单状态,获取到到数据应该是"trade_state": "SUCCESS",这样的
            Object trade_state = hashMap.get("trade_state");
          
            //判断订单状态
            //这里所有的状态还是建议写枚举,然后根据枚举去做判断
            //如果查询出来的这个订单是已经支付的话,那么我们吧我们自己的数据库中的订单状态也改成已经支付
            //如果判断是未支付的话,那么就要调用微信的关单接口,也就是我的博客中的第9点用户取消订单中的closeOrder方法去进行关单
            //并且修改我们自己数据库中的订单状态为超时关闭
        }


    }

12、申请退款

申请退款的流程,跟支付是差不多的

先申请退款,然后微信收到退款申请后,退款

退款成功后,给我们商户端发起一个通知

我们接收通知,修改状态为退款成功,并应答微信官方,说我们已经收到你的通知了

如因为其他原因商户端没有收到异步通知,如博客中第10步,也是一样的,进行一个查单

//《orderNo》是我们自己生成的订单号
//《reason》是退款理由
//这里退款有两种模式,
//第一种是使用我们生成的订单号进行查询并退款
//第二种就是,在第三步的时候,微信会返回给我们一个微信官方的订单号进行退款
//这里我选择的是第一种方式
@PostMapping("/refunds/{orderNo}/{reason}")
    public R refunds(@PathVariable String orderNo,@PathVariable String reason) throws IOException {
        log.info("申请退款");
        //先根据订单号去查询到,当前要退款到这个订单
        //我这里就创建一个空对象来代替
        OrderInfo orderInfo = new OrderInfo();
        //这里还要创建一个退款单对象
        RefundInfo refundInfo = new RefundInfo();
        //设置退款单中有那些信息
        refundInfo.setOrderNo(orderNo);//订单编号
        refundInfo.setRefundNo(wxPayConfig.getNonceStr());//退款单号,这里的退款单号,也是我们自己生成的订单,供保存退款信息的,微信那边也需要我们提供一个这个单号,一般的业务都会需要保存一个退款的信息,所以这里需要这个退款单号
        refundInfo.setTotalFee(orderInfo.getTotalFee());//原订单金额
        refundInfo.setRefund(orderInfo.getTotalFee());//要退款的金额
        refundInfo.setReason(reason);//退款原因
        //大家的退款单对象,可以根据自己的业务需求去设计。退款单也是需要一张表去存储

        //这里去调用数据库操作,去保存refundInfo这样的一条退款单信息
        //保存好退款到记录之后

        //调用退款Api进行退款操作
        log.info("调用退款APi");
        String url="https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";
        HttpPost httpPost = new HttpPost(url);

        //组装请求参数
        Gson gson = new Gson();
        Map paramsMap = new HashMap<>();
        paramsMap.put("out_trade_no",orderNo);//订单编号
        paramsMap.put("out_refund_no",refundInfo.getRefundNo());//退款单号
        paramsMap.put("reason",reason);//退款原因
        paramsMap.put("notify_url","http://d4a93w.natappfree.cc/refunds/notify");//退款成功异步回调地址

        Map amountMap = new HashMap<>();
        //正确对写法应该是这样,但是我上吗创建的是空对象,所里这里我需要吧退款金额之类的写死
//        amountMap.put("refund",refundInfo.getRefund());//退款金额
//        amountMap.put("total",refundInfo.getTotalFee());//原订单金额、
        amountMap.put("refund",1);//退款金额
        amountMap.put("total",1);//原订单金额
        amountMap.put("currency","CNY");//退款币种

        paramsMap.put("amount",amountMap);

        //将参数转为json
        String jsonParams = gson.toJson(paramsMap);
        log.info("请求参数:{}",jsonParams);

        StringEntity entity = new StringEntity(jsonParams,"utf-8");
        entity.setContentType("application/json");//设置请求报文的格式
        httpPost.setEntity(entity);//吧请求报文添加到请求对象中
        httpPost.setHeader("Accept", "application/json");//设置响应报文的格式

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

        try {

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

            //更新订单状态为退款中
            //这里大家自己去写数据库操作更改状态

            //更新退款单,根据退款的bodyAsString的数据,获取你想要保留的参数,保存到退款单中,以便后续的退款
            //比如退款单号(微信官方的),可以供我们后续如果对这笔退款有疑问,可以进行一个退款查询
					return R.ok();
        }finally {
            resp.close();
        }



        
    }

这里给大家看看我的这个退款过程

在这里插入图片描述

这里的退款参数具体的大家可以去看看官网上,有哪些是你们需要存入退款单中的:微信支付-开发者文档 (qq.com)

在这里插入图片描述

13、退款回调通知

这个是在申请退款中notify_url填写的这个参数的地址,会被微信回调的接口。微信回告知我们退款的成功的信息,需要我们进行接收,解密信息,然后应答微信官方我们收到了信息,在接收信息解密的过程中,我们可以判断是否退款成功,我们就可以进行修改退款单、订单的一些状态,把他们修改成退款成功

这里的回调逻辑,与博客中的第6点基本一致,只是进行的操作不同,一个是退款,一个是支付

 		@PostMapping("/refunds/notify")
    public String refundsNotify(HttpServletRequest request,HttpServletResponse response) {
        log.info("退款结果通知");
        Gson gson = new Gson();
        HashMap<String, String> map = new HashMap<>();//应答对象
        try {
            //处理通知参数
            String body = HttpUtils.readData(request);
            HashMap<String,Object> bodyMap = gson.fromJson(body, HashMap.class);
            String requestId = (String) bodyMap.get("id");
            log.info("支付通知的id===》{}",requestId);

            //签名验证
            WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest =
                    new WechatPay2ValidatorForRequest(verifier, requestId, body);
            if (!wechatPay2ValidatorForRequest.validate(request)){
                log.error("通知验签失败");
                //失败应答
                response.setStatus(500);
                map.put("code","ERROR");
                map.put("message","通知验签失败");
                return gson.toJson(map);
            }
            log.info("通知验签成功");

            //处理退款单
            //解密密文,获取明文
            String plaintText = wxPayConfig.decryptFromResource(bodyMap);
            //将明文转为map
            HashMap plaintTextMap = gson.fromJson(plaintText, HashMap.class);
            //获取支付下单的时候,传入的商户订单号,可以根据这个订单号去获取我们的一个订单记录,从而更新订单状态
            String orderNo = (String) plaintTextMap.get("out_trade_no");
            /**
             * 这里获取了单号可以去查询一个,这个订单的状态是不是退款中
             * 如果不是退款中,那直接return返回就行了,不用在执行下面的更新状态
             * 如果是退款中的话,我们需要更新状态为退款成功。这里写你自己的数据库操作更新即可
             * 如果你有退款单的话,同理,更新退款单状态即可
             *
             */

            //成功应答
            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);
        }
    }

以上基本上就是所有的微信小程序JSAPI支付的完整流程,

  • 统一下单
  • 支付通知
  • 查询支付结果
  • 申请退款
  • 退款通知
  • 查询退款结果
  • 关闭订单

~~~

欢迎+Q群讨论:821596752

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐