微信

H5支付

官网文档链接:https://pay.weixin.qq.com/wiki/doc/api/H5.php?chapter=15_4

流程图: http://blogimgbucketxun.oss-cn-beijing.aliyuncs.com/xiaoYiXun/common/20191026_09134038.png

1、用户在商户侧完成下单,使用微信支付进行支付
2、由商户后台向微信支付发起下单请求(调用统一下单接口)注:交易类型trade_type=MWEB
3、统一下单接口返回支付相关参数给商户后台,如支付跳转url(参数名“mweb_url”),商户通过mweb_url调起微信支付中间页
4、中间页进行H5权限的校验,安全性检查(此处常见错误请见文档)
5、如支付成功,商户后台会接收到微信侧的异步通知
6、用户在微信支付收银台完成支付或取消支付,返回商户页面(默认为返回支付发起页面)
7、商户在展示页面,引导用户主动发起支付结果的查询
8,9、商户后台判断是否接到收微信侧的支付结果通知,如没有,后台调用我们的订单查询接口确认订单状态
10、展示最终的订单支付结果给用户

统一下单接口

URL地址:https://api.mch.weixin.qq.com/pay/unifiedorder

必要参数

参数含义可以去官方文档查看,本文就不做解释了

<xml>
<appid>wx2421b1c4370ec43b</appid>
<attach>支付测试</attach>
<body>H5支付测试</body>
<mch_id>10000100</mch_id>
<nonce_str>1add1a30ac87aa2db72f57a2375d8fec</nonce_str>
<notify_url>http://wxpay.wxutil.com/pub_v2/pay/notify.v2.php</notify_url>
<openid>oUpF8uMuAJO_M2pxb1Q9zNjWeS6o</openid>
<out_trade_no>1415659990</out_trade_no>
<spbill_create_ip>14.23.150.211</spbill_create_ip>
<total_fee>1</total_fee>
<trade_type>MWEB</trade_type>
<scene_info>{"h5_info": {"type":"IOS","app_name": "王者荣耀","package_name": "com.tencent.tmgp.sgame"}}</scene_info>
<sign>0CB01533B8C1EF103065174F50BCA001</sign>
</xml>

签名

这些参数里面个人感觉也就签名稍微麻烦那么一点,需要把你将发送或者接收到的数据全部按照参数名ASCII码从小到大排序(字典序),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA,最后再拼上key得到即将加密的字符串stringSignTemp

key设置路径:微信商户平台(https://pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置

使用工具类生成微信方需要的签名

package com.abroad.utils;

import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;

/**
 * @author 萧一旬
 * @date Create in 10:35 2019/10/25
 */
public class MD5 {

    private final static String[] hexDigits = {"0", "1", "2", "3", "4", "5", "6", "7",
            "8", "9", "a", "b", "c", "d", "e", "f"};

    /**
     * 转换字节数组为16进制字串
     *
     * @param b 字节数组
     * @return 16进制字串
     */
    public static String byteArrayToHexString(byte[] b) {
        StringBuilder resultSb = new StringBuilder();
        for (byte aB : b) {
            resultSb.append(byteToHexString(aB));
        }
        return resultSb.toString();
    }

    /**
     * 转换byte到16进制
     *
     * @param b 要转换的byte
     * @return 16进制格式
     */
    private static String byteToHexString(byte b) {
        int n = b;
        if (n < 0) {
            n = 256 + n;
        }
        int d1 = n / 16;
        int d2 = n % 16;
        return hexDigits[d1] + hexDigits[d2];
    }

    /**
     * MD5编码
     *
     * @param origin 原始字符串
     * @return 经过MD5加密之后的结果
     */
    public static String MD5Encode(String origin) {
        String resultString = null;
        try {
            resultString = origin;
            MessageDigest md = MessageDigest.getInstance("MD5");
            resultString = byteArrayToHexString(md.digest(resultString.getBytes("UTF-8")));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return resultString;
    }

    /**
     * @param payKey 商户平台设置的密钥key
     * @param map    所有发送或者接收到的数据集合
     * @return signValue 签名
     */
    public static String getSign(String payKey, Map<String, Object> map) {
        ArrayList<String> list = new ArrayList<String>();
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            if (entry.getValue() != "") {
                list.add(entry.getKey() + "=" + entry.getValue() + "&");
            }
        }
        int size = list.size();
        String[] arrayToSort = list.toArray(new String[size]);

        //根据字符ASCII码进行递增排序
        Arrays.sort(arrayToSort, String.CASE_INSENSITIVE_ORDER);
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < size; i++) sb.append(arrayToSort[i]);
        String result = sb.toString();
        result += ("key=" + (payKey.trim()));
        return MD5.MD5Encode(result.toString()).toUpperCase();
    }

    /**
     * @param payKey 商户平台设置的密钥key
     * @param map    所有发送或者接收到的数据集合
     * @return signValue 签名
     */
    public static String getSignToDouYin(String payKey, Map<String, Object> map) {
        ArrayList<String> list = new ArrayList<String>();
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            if (entry.getValue() != "") {
                list.add(entry.getKey() + "=" + entry.getValue() + "&");
            }
        }
        int size = list.size();
        String[] arrayToSort = list.toArray(new String[size]);

        //根据字符ASCII码进行递增排序
        Arrays.sort(arrayToSort, String.CASE_INSENSITIVE_ORDER);
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < size; i++) sb.append(arrayToSort[i]);
        String result = sb.toString();
        result += payKey.trim();
        return MD5.MD5Encode(result.toString()).toUpperCase();
    }
}

将所有参数封装到map集合,调用getSign()方法生成所需要的签名

例子

//生成签名
            Map<String, Object> params = new HashMap<String, Object>();
            params.put("appid", order.getAppid());
            params.put("body", body);
            params.put("mch_id", order.getMch_id().trim());
            params.put("nonce_str", order.getNonce_str());
            params.put("notify_url", order.getNotify_url());
            params.put("out_trade_no", order.getOut_trade_no());
            params.put("spbill_create_ip", order.getSpbill_create_ip());
            params.put("total_fee", order.getTotal_fee());
            params.put("trade_type", order.getTrade_type());
            params.put("sign_type", "MD5");

            String sign = MD5.getSign(Configure.getKey(), params);

生成的签名,也可以使用官方的签名验证工具来验证签名的准确性
链接:https://pay.weixin.qq.com/wiki/doc/api/H5.php?chapter=20_1

只要你生成的和用官方测试工具生成的签名一致,那签名这块就没问题了

当确认签名无误之后就可以封装数据了,需要准备的数据,大致就和上述的差不多,具体还需要什么具体对待

StringBuffer data = new StringBuffer();
data.append("<xml>");
data.append(" <appid>" + "<![CDATA[" + order.getAppid() + "]]>" + "</appid>");
data.append(" <body>" + "<![CDATA[" + body + "]]>" + "</body>");
data.append(" <mch_id>" + order.getMch_id().trim() + "</mch_id>");
data.append(" <nonce_str>" + "<![CDATA[" + order.getNonce_str() + "]]>" + "</nonce_str>");
data.append(" <notify_url>" + "<![CDATA[" + order.getNotify_url() + "]]>" + "</notify_url>");
data.append("  <out_trade_no>" + "<![CDATA[" + order.getOut_trade_no() + "]]>" + "</out_trade_no>");
data.append("  <spbill_create_ip>" + "<![CDATA[" + order.getSpbill_create_ip() + "]]>" + "</spbill_create_ip>");
data.append("  <total_fee>" + "<![CDATA[" + order.getTotal_fee() + "]]>" + "</total_fee>");
data.append("  <trade_type>" + "<![CDATA[" + order.getTrade_type() + "]]>" + "</trade_type>");
data.append("  <sign_type>" + "<![CDATA[MD5]]>" + "</sign_type>");
data.append("  <sign>" + "<![CDATA[" + order.getSign() + "]]>" + "</sign>");
data.append("</xml>");

String result = HttpRequest.doPostJson("https://api.mch.weixin.qq.com/pay/unifiedorder", data.toString());

参数封装好后,就需要发请求给微信服务器了,发请求也是一个需要注意的点,最好还是使用那种广为流传的Http工具类,比如RestTemplate,HttpClient等等,因为这个原因,也算是走了不少的冤枉路

我使用的就是下面这个工具类

import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * HttpClientUtil请求工具类
 *
 * @author 萧一旬
 * @createTime 2018/6/27 15:24
 */
public class HttpRequest {

    private HttpRequest(){}

    public static String doGet(String url, Map<String, String> param) {

        // 创建Httpclient对象
        CloseableHttpClient httpclient = HttpClients.createDefault();

        String resultString = "";
        CloseableHttpResponse response = null;
        try {
            // 创建uri
            URIBuilder builder = new URIBuilder(url);
            if (param != null) {
                for (String key : param.keySet()) {
                    builder.addParameter(key, param.get(key));
                }
            }
            URI uri = builder.build();

            // 创建http GET请求
            HttpGet httpGet = new HttpGet(uri);

            // 执行请求
            response = httpclient.execute(httpGet);
            // 判断返回状态是否为200
            if (response.getStatusLine().getStatusCode() == 200) {
                resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (response != null) {
                    response.close();
                }
                httpclient.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return resultString;
    }
    public static String doGet(String url) {
        return doGet(url, null);
    }
    public static String doPost(String url, Map<String, String> param) {
        // 创建Httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        CloseableHttpResponse response = null;
        String resultString = "";
        try {
            // 创建Http Post请求
            HttpPost httpPost = new HttpPost(url);
            // 创建参数列表
            if (param != null) {
                List<NameValuePair> paramList = new ArrayList<>();
                for (String key : param.keySet()) {
                    paramList.add(new BasicNameValuePair(key, param.get(key)));
                }
                // 模拟表单
                UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList);
                httpPost.setEntity(entity);
            }
            // 执行http请求
            response = httpClient.execute(httpPost);
            resultString = EntityUtils.toString(response.getEntity(), "utf-8");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                response.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

        return resultString;
    }

    public static String doPost(String url) {
        return doPost(url, null);
    }

    public static String doPostJson(String url, String json) {
        // 创建Httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        CloseableHttpResponse response = null;
        String resultString = "";
        try {
            // 创建Http Post请求
            HttpPost httpPost = new HttpPost(url);
            // 创建请求内容
            StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
            httpPost.setEntity(entity);
            // 执行http请求
            response = httpClient.execute(httpPost);
            resultString = EntityUtils.toString(response.getEntity(), "utf-8");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                response.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        return resultString;
    }

}

当你给微信服务器发送请求之后,正常来说,他会给你返回一个xml格式的数据:

<xml>
   <return_code><![CDATA[SUCCESS]]></return_code>
   <return_msg><![CDATA[OK]]></return_msg>
   <appid><![CDATA[wx2421b1c4370ec43b]]></appid>
   <mch_id><![CDATA[10000100]]></mch_id>
   <nonce_str><![CDATA[IITRi8Iabbblz1Jc]]></nonce_str>
   <sign><![CDATA[7921E432F65EB8ED0CE9755F0E86D72F]]></sign>
   <result_code><![CDATA[SUCCESS]]></result_code>
   <prepay_id><![CDATA[wx201411101639507cbf6ffd8b0779950874]]></prepay_id>
   <trade_type><![CDATA[MWEB]]></trade_type>
   <mweb_url><![CDATA[https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx2016121516420242444321ca0631331346&package=1405458241]]></mweb_url>
</xml>

关于XML格式数据使用问题

xml格式数据当然不怎么方便我们使用,所以还得转换成所需的参数格式来方便使用

方式一

XStream是一个简单的基于Java库,Java对象序列化到XML,反之亦然(即:可以轻易的将Java对象和xml文档相互转换)

XStream xStream = new XStream();
xStream.alias("xml", OrderReturnInfo.class); 
	
OrderReturnInfo returnInfo = (OrderReturnInfo)xStream.fromXML(result);

转换成功后就可以做一些其他操作,比如生成支付订单信息什么的,这里就不上代码了,直接略过

方式二

其实不推荐直接把xml格式数据直接转成实体类,为什么这么说呢。
微信最近就对于退款返回的参数做了修改,然后转成实体类的时候,因为他新添了字段,但是实体类中却没有,所以转换的时候就会报错,这个时候还得重新修改代码再发布。
当时我的项目在线上就是使用的上述方法,然后微信突然多加了几个返回字段,导致退款是成功了,但是请求退款后,微信的返回值的处理失败,导致线上退款成功逻辑走不通。

所以还是推荐把xml格式数据转成JSONObject对象来使用

<!-- xml 转 json、-->
<dependency>
    <groupId>com.fasterxml</groupId>
    <artifactId>jackson-xml-databind</artifactId>
    <version>0.6.2</version>
</dependency>

<!-- https://mvnrepository.com/artifact/net.sf.json-lib/json-lib -->
<!-- JSONObject -->
<dependency>
    <groupId>net.sf.json-lib</groupId>
    <artifactId>json-lib</artifactId>
    <version>2.4</version>
</dependency>

PS:引入json-lib的时候可能报一下错误

  • Could not find artifact net.sf.json-lib: json-lib: jar:2.4 in central (https://repo.maven.apache.org/maven2)

解决办法:

<!-- https://mvnrepository.com/artifact/net.sf.json-lib/json-lib -->
<!-- JSONObject -->
<dependency>
    <groupId>net.sf.json-lib</groupId>
    <artifactId>json-lib</artifactId>
    <version>2.4</version>
    <classifier>jdk15</classifier>
</dependency>

使用测试:

@Test
void contextLoads() {
	//展示的测试数据就是最新的微信退款所返回的xml格式数据,可以和之前的对比,会发现多了很多数据,所以转成JavaBean的话就需要对JavaBean也做对应的修改
    String s = "<xml><return_code><![CDATA[SUCCESS]]></return_code>" +
            "<return_msg><![CDATA[OK]]></return_msg>" +
            "<appid><![CDATA[wxf518db02e2]]></appid>" +
            "<mch_id><![CDATA[14932]]></mch_id>" +
            "<nonce_str><![CDATA[RLP3V9ZOl]]></nonce_str>" +
            "<sign><![CDATA[2EA5B6559FC1CB79C1EE9D]]></sign>" +
            "<result_code><![CDATA[SUCCESS]]></result_code>" +
            "<transaction_id><![CDATA[4200005768960]]></transaction_id>" +
            "<out_trade_no><![CDATA[WeCh52]]></out_trade_no>" +
            "<out_refund_no><![CDATA[78hkdq8mf9autkyebfsfptxi]]></out_refund_no>" +
            "<refund_id><![CDATA[5000000642200624]]></refund_id>" +
            "<refund_channel><![CDATA[]]></refund_channel>" +
            "<refund_fee>10</refund_fee>" +
            "<coupon_refund_fee>0</coupon_refund_fee>" +
            "<total_fee>10</total_fee>" +
            "<cash_fee>10</cash_fee>" +
            "<coupon_refund_count>0</coupon_refund_count>" +
            "<cash_refund_fee>10</cash_refund_fee>" +
            "</xml>";
    XmlMapper xmlMapper = new XmlMapper();
    JSONObject jsonObject = null;
    try {
        jsonObject = xmlMapper.readValue(s, JSONObject.class);
        System.out.println("jsonObject = " + jsonObject);
        if ("SUCCESS".equals(jsonObject.get("return_code")) && "SUCCESS".equals(jsonObject.get("result_code"))) {
            System.out.println("退款成功");
        }
    } catch (Exception e) {
        e.printStackTrace();
    }

}

封装返回

当你做完所有操作后,就得开始封装返回给字节跳动的数据,所以我们对于字节跳动的文档多多少少也还是要熟悉一点。
当我们返回出去后,前端就会调用字节跳动的tt.pay接口,所以我们返回的参数还需要根据这个接口来规定

官方链接:https://microapp.bytedance.com/dev/cn/mini-app/develop/open-capacity/payment/pay

/**
 * @author 萧一旬
 * @date Create in 9:19 2019/10/31
 */
 @Data
public class HFivePayResultToDouYin {


    /**
     * 头条支付分配给商户 app_id
     */
    private String appid;

    /**
     * 签名
     */
    private String sign;

    /**
     * 交易类型
     */
    private String trade_type;
	
    /**
     * 微信支付跳转链接
     */
    private String mweb_url;

    /**
     * 用户id
     */
    private String userId;

    /**
     * 下单时间戳
     */
    private long trade_time;

    /**
     * 请求时间戳
     */
    private long timestamp;

    /**
     * 订单号
     */
    private String out_order_no;

    /**
     * 商户号
     */
    private String merchant_id;

    /**
     * 商户订单详情
     */
    private String body;

    /**
     * 商户订单名称
     */
    private String subject;

    /**
     * 金额
     */
    private int total_amount;

	/**
	 * 调用支付宝 App 支付所需的支付请求参数
	 */
	private String alipay_url;
}

实体类有了,接下来肯定就是给实体类封装参数了,其他参数没什么好说的,字节跳动需要的订单号out_order_no,是生成订单时的订单唯一标识

然后还值得一提的就是字节跳动需要的签名了,这个签名我用上述的那个签名方法说我签名错误,最后还是看官方给的Demo才成功签名

这个签名我们需要使用下面这个签名类,我不知道上面微信的签名能不能使用这个方法,我也没去尝试

调用BuildMd5WithSalt()即可,map集合依旧是我们的参数集合,salt就是字节跳动给我们的支付secret

package com.abroad.utils;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.codec.binary.Base64;

import java.io.IOException;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.*;

public class SignUtil {
    public static String BuildMd5WithSalt(Map<String, Object> dataMap, String salt) {
        String signStr = GenSignStr(dataMap);
        return DigestUtils.md5Hex(signStr + salt);
    }

    private static String GenSignStr(Map<String, Object> data) {
        StringBuilder sb = new StringBuilder();
        Set<String> entrySet = data.keySet();
        List<String> list = new ArrayList<String>(entrySet);
        Collections.sort(list);
        for (String key: list) {
            if ( key.equals("") || data.get(key).equals("") || data.get(key)==null) {
                continue;
            }
            sb.append(key);
            sb.append("=");
            sb.append(data.get(key));
            sb.append("&");
        }
        if (sb.length() > 0) {
            sb.deleteCharAt(sb.length() - 1);
        }
        return sb.toString();
    }

    public static boolean VerifyMd5WithRsa(Map<String, Object> params, String sign, String publicKey) throws SignatureException, InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException {
        String signStr = GenSignStr(params);
        return RsaVerify(signStr, sign, publicKey);
    }

    private static boolean RsaVerify(String target, String verifySign, String publicKey) throws SignatureException, NoSuchAlgorithmException, InvalidKeyException, InvalidKeySpecException {
        //获取公钥
        byte[] keyBytes = Base64.decodeBase64(publicKey);//解密由base64编码的公钥
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); // 构造X509EncodedKeySpec对象
        //公钥验签
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PublicKey publicK = keyFactory.generatePublic(keySpec);
        Signature signature = Signature.getInstance("MD5withRSA");
        signature.initVerify(publicK);
        signature.update(target.getBytes());
        byte[] signBytes = Base64.decodeBase64(verifySign);
        return signature.verify(signBytes);
    }

}

例子:

Map<String, Object> objectMap = new HashMap<>();
                objectMap.put("app_id", hFivePayResultToDouYin.getAppid());
                objectMap.put("merchant_id", hFivePayResultToDouYin.getMerchant_id());
                objectMap.put("timestamp", hFivePayResultToDouYin.getTimestamp());
                objectMap.put("sign_type", "MD5");
                objectMap.put("out_order_no", hFivePayResultToDouYin.getOut_order_no());
                 objectMap.put("total_amount", hFivePayResultToDouYin.getTotal_amount());
                objectMap.put("product_code", "pay");
                objectMap.put("payment_type", "direct");
                objectMap.put("trade_type", "H5");
                objectMap.put("version", "2.0");
                objectMap.put("currency", "CNY");
                objectMap.put("subject", hFivePayResultToDouYin.getSubject());
                objectMap.put("body", hFivePayResultToDouYin.getBody());
                objectMap.put("uid", hFivePayResultToDouYin.getUserId());
                objectMap.put("trade_time", hFivePayResultToDouYin.getTrade_time());
                objectMap.put("valid_time", "300");
                objectMap.put("notify_url", "https://tp-pay-test.snssdk.com/cashdesk/test/paycallback");
                objectMap.put("wx_type", hFivePayResultToDouYin.getTrade_type());
                objectMap.put("wx_url", hFivePayResultToDouYin.getMweb_url());
                String douyinSign = SignUtil.BuildMd5WithSalt(objectMap, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
//                Map<String,Object> objectMap = new HashMap<>();
//                objectMap.put("app_id", "800000000001");
//                objectMap.put("merchant_id", "1900000001");
//                objectMap.put("timestamp", "1570694312");
//                objectMap.put("sign_type", "MD5");
//                objectMap.put("out_order_no", "201900000000000001");
//                objectMap.put("total_amount", "1");
//                objectMap.put("product_code", "pay");
//                objectMap.put("payment_type", "direct");
//                objectMap.put("trade_type", "H5");
//                objectMap.put("version", "2.0");
//                objectMap.put("currency", "CNY");
//                objectMap.put("subject", "测试订单");
//                objectMap.put("body", "测试订单");
//                objectMap.put("uid", "0000000000000001");
//                objectMap.put("trade_time", "1570585744");
//                objectMap.put("valid_time", "300");
//                objectMap.put("notify_url", "");
//                objectMap.put("wx_type", "MWEB");
//                objectMap.put("wx_url", "https://wx.tenpay.com/xxx");
//                objectMap.put("alipay_url", "app_id=2019000000000006&biz_content=xxxx");
//                String douyinSign = SignUtil.BuildMd5WithSalt(objectMap,"a");
                hFivePayResultToDouYin.setSign(douyinSign);

如果你想验证这个签名方式的正确性的话,可以使用官方给你的测试数据(被我注释的部分)进行签名,如果你生成的签名和官方给的结果一样,那就说明签名是正确的

这里需要注意的是:

  • 微信签名最后拼接key的时候,是&key=xxxxxx,而字节跳动的签名是直接拼接xxxxxx

关于网友的疑惑

有网友评论说没有在官方文档中看到需要拼接字符密钥key,所以在这里附上官网文档的地址:https://microapp.bytedance.com/dev/cn/mini-app/develop/open-capacity/payment/mini-app-pay-plugin-reference/server-sign

当你调用微信统一下单接口成功返回之后,也就字节跳动的签名需要稍微注意一点,其他被需要的参数都很容易获取,封装好后返回给前端即可

这个时候如果你没有配置支付域名的话好像是还不能成功吊起支付的

首先我们需要在微信支付平台->产品中心->我的产品配置H5支付域名
其次,还需要在微信公众平台-网页授权域名,这个没什么好说的,跟着配置就行,我是把这个文件直接丢到了项目里

做完这些,微信统一下单以及客户端跳转支付就完成了。

查询订单

逻辑代码:微信小程序技术摘要-查询订单

退款

逻辑代码:微信小程序技术摘要-退款

下单退款简化

使用第三方简化请求过程,内容在微信小程序文章中:微信小程序技术摘要-使用第三方API实现支付退款

支付宝

根据官方文档可知,字节跳动小程序调用支付宝支付是接入APP支付,像那些创建应用,签约支付以及密钥设置获取的操作,跟着官方文档来就行

app支付接口2.0

HTTPS请求地址:https://openapi.alipay.com/gateway.do

请求支付宝接口并按要求返回

此代码被整合成了一篇新的博客:支付宝接入技术摘要-APP支付

alipayResponse.getBody()封装到上述说的返回JavaBean中返回即可

hFivePayResultToDouYin.setAlipay_url(alipayResponse.getBody());

同样的,在待签名的数据中也要把它加进去

objectMap.put("alipay_url", hFivePayResultToDouYin.getAlipay_url());

这个时候,前端字节跳动收银台也可以进入到支付宝进行支付了,反正我是可以了[捂脸.jpg]

退款

代码访问链接:支付宝接入技术摘要-退款

支付回调

微信和支付宝的回调从某种角度上其实可以说是一致的,所以其实回调API基本上没什么区别

代码详见:支付宝接入技术摘要-支付回调-解析流获取参数

支付宝官方文档:https://opendocs.alipay.com/open/203/105286

微信官方文档:https://pay.weixin.qq.com/wiki/doc/api/H5.php?chapter=9_7&index=8

PS:官方文档都均提到了验签,但是实践表明也可以不验签。以及微信官方文档说,当一切无误需按指定的格式返回给微信,但是我测试的时候也没有按指定格式返回也并未出什么错(百度小程序是需要严格按照规定格式返回)

解析获取用户手机号

这个方法将用户的信息转成了JSONObject对象

//这三个参数前端都会传
public static JSONObject getDyUserPhoneNumber(String code, String iv, String encryptedData) {

    String appid = "";
    String appSecret = "";
    String apiUrl = "https://developer.toutiao.com/api/apps/jscode2session?appid=" + appid + "&secret=" + appSecret + "&code=" + code;
    System.out.println(apiUrl);
    String responseBody = HttpClientUtil.doGet2(apiUrl);
    System.out.println(responseBody);
	//这里是获取session_key,解密用的
    JSONObject jsonObject = JSON.parseObject(responseBody);
    if (!StringUtil.isEmpty(jsonObject.getString("openid")) && !StringUtil.isEmpty(jsonObject.getString("session_key"))) {
        //解密获取用户信息
        JSONObject userInfoJSON = getUserInfo(encryptedData, jsonObject.getString("session_key"), iv);
        if (userInfoJSON != null) {
            userInfoJSON.put("openId", jsonObject.getString("openid"));
            return userInfoJSON;
        }
    }
    return null;
}

public static JSONObject getUserInfo(String encryptedData, String sessionKey, String iv) {
    
    // 被加密的数据
    byte[] dataByte = Base64Utils.decode(encryptedData.getBytes());
    // 加密秘钥
    byte[] keyByte = Base64Utils.decode(sessionKey.getBytes());
    // 偏移量
    byte[] ivByte = Base64Utils.decode(iv.getBytes());
    try {

        // 如果密钥不足16位,那么就补足.  这个if 中的内容很重要
        int base = 16;
        if (keyByte.length % base != 0) {
            int groups = keyByte.length / base + (keyByte.length % base != 0 ? 1 : 0);
            byte[] temp = new byte[groups * base];
            Arrays.fill(temp, (byte) 0);
            System.arraycopy(keyByte, 0, temp, 0, keyByte.length);
            keyByte = temp;
        }
        // 初始化
        Security.addProvider(new BouncyCastleProvider());
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
        SecretKeySpec spec = new SecretKeySpec(keyByte, "AES");
        AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES");
        parameters.init(new IvParameterSpec(ivByte));
        cipher.init(Cipher.DECRYPT_MODE, spec, parameters);// 初始化
        byte[] resultByte = cipher.doFinal(dataByte);
        if (null != resultByte && resultByte.length > 0) {
            String result = new String(resultByte, "UTF-8");
            return JSON.parseObject(result);
        }
    } catch (NoSuchAlgorithmException e) {
        log.error(e.getMessage(), e);
    } catch (NoSuchPaddingException e) {
        log.error(e.getMessage(), e);
    } catch (InvalidParameterSpecException e) {
        log.error(e.getMessage(), e);
    } catch (IllegalBlockSizeException e) {
        log.error(e.getMessage(), e);
    } catch (BadPaddingException e) {
        log.error(e.getMessage(), e);
    } catch (UnsupportedEncodingException e) {
        log.error(e.getMessage(), e);
    } catch (InvalidKeyException e) {
        log.error(e.getMessage(), e);
    } catch (InvalidAlgorithmParameterException e) {
        log.error(e.getMessage(), e);
    } catch (NoSuchProviderException e) {
        log.error(e.getMessage(), e);
    }
    return null;
}

结语

代码写到这的话,基本上也就差不多了,支付和退款。
另外支付宝上也没有类似的微信的订单查询接口[可能有但是我没找到],然后再者字节跳动文档上tt.paygetOrderStatus(res)的注释中也只是说查询微信而没有查支付宝,所以我也就没有写这个接口了

还有值得注意的就是退款方面了,支付宝支付的订单需要在支付宝退款,微信支付的订单需要在微信退款,所以这就需要在支付回调的时候区分开来(比如加个字段值来记录是微信或者支付宝,然后这个字段的值是由支付回调接口里判断给值)