开发
API
直播能力

附录

支付设置校验

小程序通过 GET 请求,携带以下 query 参数以如下逻辑校验请求是否来源于字节跳动小程序服务端,并返回 echostr 字段

  1. token 开发者自己保存,也可以通过开发者平台查询到
  2. timestamp 是开发者服务端请求开发者的回调地址的 query 参数
  3. nonce 是开发者服务端请求开发者的回调地址的 query 参数
  4. msg 是开发者平台服务端请求开发者的回调地址的 query 参数
  5. echostr 是开发者平台服务端请求开发者的回调地址的 query 参数

开发者用 token,timestamp,nonce,msg 计算签名 signature,和请求开发者的回调地址 body 里面的 signature 对比,如果一致,说明请求来自于开发者服务端,返回 echostr。 golang 示例 sortedString := make([]string, 4)

sortedString = append(sortedString, token)
 sortedString = append(sortedString, timestamp)
 sortedString = append(sortedString, nonce)
 sortedString = append(sortedString, msg)
 sort.Strings(sortedString)
 h := sha1.New()
 h.Write([]byte(strings.Join(sortedString, "")))
 bs := h.Sum(nil)
 _signature := fmt.Sprintf("%x", bs)
 // signature 一致表示请求来源于 字节跳动小程序服务端
 if _signature == Signature {
   return echostr
 }
 return nil

nodejs 示例(Koa)

// http handler GET
 // function handler(ctx) {
   const { signature, timestamp, msg: '', nonce, echostr } = ctx.query;
   const strArr = [token, timestamp, nonce, msg].sort();
   const str = strArr.join('');
   const _signature = require('crypto').createHash('sha1').update(str).digest('hex');
   // signature 一致表示请求来源于 字节跳动小程序服务端
   if (_signature === signature) {
     ctx.body = echostr;
     return;
   }
   ctx.body = '';
 // }

地址可访问性校验通过之后,线上支付订单信息会通过填写的回调地址进行回调

回调签名算法

小程序平台所有发往开发者服务端请求。都会使用如下的算法进行签名,用来验证请求的来源:

  1. 将所有字段(验证时注意不包含 sign 签名本身,不包含空字段与 type 常量字段)内容与平台上配置的 token 一起,按照字典序排序
  2. 所有字段内容连接成一个字符串
  3. 使用 sha-1 算法计算字符串摘要作为签名 golang 示例
 sortedString := make([]string, 4)
 sortedString = append(sortedString, token)
 sortedString = append(sortedString, timestamp)
 sortedString = append(sortedString, nonce)
 sortedString = append(sortedString, msg)
 sort.Strings(sortedString)
 h := sha1.New()
 h.Write([]byte(strings.Join(sortedString, "")))
 bs := h.Sum(nil)
 _signature := fmt.Sprintf("%x", bs)

nodejs 示例(Koa)

// http handler GET
 // function handler(ctx) {
   const { signature, timestamp, msg: '', nonce, echostr } = ctx.query;
   const strArr = [token, timestamp, nonce, msg].sort();
   const str = strArr.join('');
   const _signature = require('crypto').createHash('sha1').update(str).digest('hex');

回调响应

在开发者服务端收到回调且处理成功后,需要按以下 json 返回表示处理成功,否则会认为通知失败进行重试。

{
  "err_no": 0,
  "err_tips": "success"
}

请求签名算法

发往小程序服务端的请求,在没有特殊说明时,均需要使用担保支付秘钥进行签名,由于保证请求的来源:

  1. sign, app_id , thirdparty_id 字段用于标识身份字段,不参与签名。将其他字段内容(不包含 key)与支付 SALT 一起进行字典序排序后,使用&符号链接
  2. 使用 md5 算法对该字符串计算摘要,作为结果
  3. 参与加签的字段均以 POST 请求中的 body 内容为准, 不考虑参数默认值等规则. 对于对象类型与数组类型的参数, 使用 POST 中的字符串原串进行左右去除空格后进行加签
  4. 如有其他安全性需要, 可以在请求中添加 nonce 字段, 该字段无任何业务影响, 仅影响加签内容, 使同一请求的多次签名不同.

php 示例加签

<?php
function sign($map) {
    $rList = array();
    foreach($map as $k =>$v) {
        if ($k == "other_settle_params" || $k == "app_id" || $k == "sign" || $k == "thirdparty_id")
            continue;
        $value = trim(strval($v));
        $len = strlen($value);
        if ($len > 1 && substr($value, 0,1)=="\"" && substr($value,$len, $len-1)=="\"")
            $value = substr($value,1, $len-1);
        $value = trim($value);
        if ($value == "" || $value == "null")
            continue;
        array_push($rList, $value);
    }
    array_push($rList, "your_payment_salt");
    sort($rList, 2);
    return md5(implode('&', $rList));
}
?>

golang 示例加签

import (
   "crypto/md5"
   "fmt"
   "sort"
   "strings"
)

// 支付密钥值
const salt = "your_payment_salt"

/*
    paramsMap: POST 请求中的字符串转换为 map
    eg:
        "{\"a\":\"string\",\"b\":1,\"c\":true}"
                ==>
        map[string]interface {}{"a":"string", "b":1, "c":true}
*/
func getSign(paramsMap map[string]interface{}) string {
   var paramsArr []string
   for k, v := range paramsMap {
      if k == "other_settle_params" {
          continue
      }
      value := strings.TrimSpace(fmt.Sprintf("%v", v))
      if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") && len(value) > 1 {
         value = value[1 : len(value)-1]
      }
      value = strings.TrimSpace(value)
      if value == "" || value == "null" {
         continue
      }
      switch k {
      // app_id, thirdparty_id, sign 字段用于标识身份,不参与签名
      case "app_id", "thirdparty_id", "sign":
      default:
         paramsArr = append(paramsArr, value)
      }
   }

   paramsArr = append(paramsArr, salt)
   sort.Strings(paramsArr)
   return fmt.Sprintf("%x", md5.Sum([]byte(strings.Join(paramsArr, "&"))))
}

nodejs 示例(Koa)

// params 代表请求内容
var skip_arr = ["thirdparty_id", "app_id", "sign"];
var paramArray = new Array();
for (var k in params) {
  if (skip_arr.indexOf(k) != -1) {
    continue;
  }
  if (params[k] == "") {
    continue;
  }
  paramArray.push(params[k]);
}
paramArray.push(SALT);
paramArray.sort();
var signStr = paramArray.join("&");
return md5(signStr);

Java(jdk 1.8) 示例加签

// 支付密钥值
private static final String SALT = "your_payment_salt";

// paramsMap 参数含义解释同 golang
public static String getSign(Map<String, Object> paramsMap) {
    List<String> paramsArr = new ArrayList<>();
    for (Map.Entry<String, Object> entry : paramsMap.entrySet()) {
        String key = entry.getKey();
        if (key.equals("other_settle_params")) {
            continue;
        }
        String value = entry.getValue().toString();

        value = value.trim();
        if (value.startsWith("\"") && value.endsWith("\"") && value.length() > 1) {
            value = value.substring(1, value.length() - 1);
        }
        value = value.trim();
        if (value.equals("") || value.equals("null")) {
            continue;
        }
        switch (key) {
            case "app_id":
            case "thirdparty_id":
            case "sign":
                break;
            default:
                paramsArr.add(value);
                break;
        }
    }
    paramsArr.add(SALT);
    Collections.sort(paramsArr);
    StringBuilder signStr = new StringBuilder();
    String sep = "";
    for (String s : paramsArr) {
        signStr.append(sep).append(s);
        sep = "&";
    }
    return md5FromStr(signStr.toString());
}

public static String md5FromStr(String inStr) {
    MessageDigest md5;
    try {
        md5 = MessageDigest.getInstance("MD5");
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
        return "";
    }

    byte[] byteArray = inStr.getBytes(StandardCharsets.UTF_8);
    byte[] md5Bytes = md5.digest(byteArray);
    StringBuilder hexValue = new StringBuilder();
    for (byte md5Byte : md5Bytes) {
        int val = ((int) md5Byte) & 0xff;
        if (val < 16) {
            hexValue.append("0");
        }
        hexValue.append(Integer.toHexString(val));
    }
    return hexValue.toString();
}

python 示例加签(python2 和 python3 注意 get_sign 返回值的处理!!!)

# -*- coding: UTF-8 -*-
import hashlib

# 支付密钥值
SALT = "your_payment_salt"


# params_map 参数含义解释同 golang
def get_sign(params_map):  # type: (dict[str, any]) -> str
    params_list = []
    for k, v in params_map.items():
        if k == "other_settle_params":
            continue
        value = str(v).strip()
        if value.startswith("\"") and value.endswith("\"") and len(value) > 1:
            value = value[1: len(value) - 1]
        value = value.strip()
        if value == "" or value == "null":
            continue
        if k not in ("app_id", "thirdparty_id", "sign"):
            params_list.append(value)
    params_list.append(SALT)
    params_list.sort()
    original_str = str("&").join(params_list)
    # python2
    return hashlib.md5(original_str).hexdigest()
    # python3
    # return hashlib.md5(original_str.encode("utf-8")).hexdigest()

.net C# 示例加签

// 支付密钥值
private const string Salt = "your_payment_salt";

private static string GetSign(Dictionary<string, Object> paramsDict)
{
    var paramsList = new ArrayList();
    foreach (var (k, v) in paramsDict)
    {
        if ("other_settle_params".Equals(k))
        {
            continue;
        }

        var value = v.ToString().Trim();
        if (value.EndsWith("\"") && value.EndsWith("\"") && value.Length > 1)
        {
            value = value.Substring(1, value.Length - 1);
        }

        value = value.Trim();
        if ("".Equals(value) || "null".Equals(value))
        {
            continue;
        }

        switch (k)
        {
            case "app_id":
            case "thirdparty_id":
            case "sign":
                break;
            default:
                paramsList.Add(value);
                break;
        }
    }

    paramsList.Add(Salt);
    // 按照 ASCII 编码进行排序
    var paramsArray = (string[]) paramsList.ToArray(typeof(string));
    Array.Sort(paramsArray, string.CompareOrdinal);
    var signStr = string.Join("&", paramsArray);

    return GetMd5(signStr);
}

private static string GetMd5(string sDataIn)
{
    var md5 = new MD5CryptoServiceProvider();
    byte[] bytValue, bytHash;
    bytValue = Encoding.UTF8.GetBytes(sDataIn);
    bytHash = md5.ComputeHash(bytValue);
    md5.Clear();
    var sTemp = bytHash.Aggregate("", (current, t) => current + t.ToString("X").PadLeft(2, '0'));

    return sTemp.ToLower();
}

手续费计算规则

手续费=floor((订单总金额-已退款金额)*0.006)

手续费计算需要根据当前用户可操作金额(订单总金额-已退款或结算金额)计算,对乘费率千分之六后的金额向下取整即为该笔交易的手续费。 手续费会在调用结算时扣减,若该笔交易在结算后产生退款等行为,手续费不会回退。

点击纠错
评价此篇文档