收银台(小程序)接入文档

产品介绍

本文档非小游戏内购文档,小游戏内购详见小游戏内购说明

开发者入驻字节跳动小程序平台,字节跳动提供拉起头条/抖音等App集成的支付宝的能力。资金流和信息流,都是开发者和支付宝直接进行交互,资金流和信息流,字节跳动仅提供拉起支付宝的能力,钱是收款到支付宝的商户后台,具体交互流程参见图1-1。

图1-1

对账、服务端异步回调、结果查询、退款等都是开发者和支付宝直接进行交互,请参考支付宝标准接口。

申请开通支付

申请开通支付功能时,需要在小程序开发者的后台提交申请, 如下图所示,并且提供以下资料:

  1. 商户名称(公司名称)
  2. 法人姓名
  3. 渠道支付的业务场景(暂时只支持支付宝App支付,未来会支持微信支付等更多支付方式)
  4. 支付类型(开发者勾选):虚拟支付 实物支付
  5. 渠道密钥类型(开发者勾选):RSA2 RSA
  6. 支付场景描述(描述会使用支付的场景,注意iOS上虚拟物品不支持使用支付宝/微信支付,有虚拟物品支付的开发者,只能在安卓端上使用支付功能)
图2-1

审核通过以后就能够在小程序开发者后台查看分配的支付app_id、支付秘钥secret和商户号(merchant_id),如图2-2所示。

图2-2

接口文档

给开发者使用的服务端下单接口

该接口用于开发者后台封装订单信息传输给字节跳动,字节跳动返回内部订单号。

  1. 接口名称

    tp.trade.create

  2. 请求说明

    请求地址正式环境 https://tp-pay.snssdk.com/gateway
    Content-Type application/x-www-form-urlencoded
    HTTP方法 POST方式
  3. 请求参数

    公共请求参数

    参数 类型 是否必填 是否签名 最大长度 描述 示例值
    app_id String 32 支付分配给业务方的ID,用于获取签名/验签的密钥信息,不是小程序appid 1001
    method String 128 接口名称 tp.trade.create
    format String 40 仅支持JSON JSON
    charset String 10 请求使用的编码格式,如utf-8、gbk、gb2312等,目前只支持utf-8 utf-8
    sign_type String 10 商户生成签名字符串所使用的签名算法类型,目前支持MD5 MD5
    sign String 344 商户请求参数的签名串,详见签名方法 详见示例
    timestamp String 19 发送请求的时间,发送请求的时间,长整型的时间戳,单位是秒 1534152421
    version String 3 调用的接口版本,固定为:1.0 1.0
    biz_content String 请求参数的集合,最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递,json格式

    业务请求参数

    参数 类型 是否必填 最大长度 描述 示例值
    out_order_no String 必选 32 订单号 20160727001
    uid String 必选 32 唯一标识用户的id,小程序开发者请传open_id。open_id获取方法
    uid_type String 可选 32 小程序开发者可不传,请忽略
    merchant_id String 必选 32 支付分配给业务方的商户号
    total_amount Long 必选 32 金额,分为单位,应传整型
    currency String 必选 9 币种 CNY
    subject String 必选 200 商户订单名称
    body String 必选 200 商户订单详情
    pay_discount String 可选 1000 折扣 格式(3段):订单号^金额^方式|订单号^金额^方式。 方式目前仅支持红包: coupon如:423423^1^coupon。 可选,目前暂不支持 combine
    trade_time String 必选 14 下单时间戳,unix时间戳
    valid_time String 必选 14 订单有效时间(单位 秒) 15
    notify_url String 必选 500 服务器异步通知http地址 请填支付宝下单接口对应的异步通知url
    service_fee String 可选 20 平台手续费
    risk_info String 必选 2048 风控信息,标准的json字符串格式,目前需要传入用户的真实ip: "{"ip":"123.123.123.1"}" {"ip":"123.123.123.1"}
    ext_param String 可选 1024 扩展参数,json格式, 用来上送商户自定义信息
  4. 响应参数

    参数 类型 是否必填 最大长度 描述 示例值
    response String - 返回数据,具体返回数据见response参数
    sign String - 对返回数据(response中的数据)进行签名,详见签名说明 Business Failed

    response 参数

    参数 类型 是否必填 最大长度 描述 示例值
    code String - 网关返回码,仅当code=10000的时候表示成功,其他都是失败,见下面 40004
    msg String - 网关返回码描述,见下面 Business Failed
    sub_code String - 开发者只需关注code,不需要关注subcode,具体业务返回码,参见具体的API接口文档 ACQ.TRADE_HAS_SUCCESS
    sub_msg String - 具体业务返回码描述,参见具体的API接口文档 交易已被支付
    trade_no String 必填 64 支付单号 180312100000000023
  5. 请求示例

    POST请求http://pay-test.byted.org/gateway?app_id=188888888888&biz_content={"out_order_no":"test-withdraw-8","uid":"1966234913","uid_type":1003,"account_type":102,"amount":1,"currency":"CNY"}&charset=utf-8&format=json&method=tp.trade.create&sign=8572aa0122b999650eb34c78a67c5599&sign_type=MD5&timestamp=1544356459&version=1.0 
    
  6. 响应示例

    {
        "response":{
            "code":"10000",
            "msg":"Success",
            "trade_no":"1004130000127421"
        },
        "sign":"ERITJKEIJKJHKKKKKKKHJEREEEEEEEEEEE"
    }
    
  7. 异常示例

    {
        "response":{
            "code":"20000",
            "msg":"Service Currently Unavailable",
            "sub_code":"TP.SYSTEM_ERROR",
            "sub_msg":"接口返回错误"
        },
        "sign":"ERITJKEIJKJHKKKKKKKHJEREEEEEEEEEEE"
    }
    
  8. 业务错误代码 状态码/错误码 - 字典表

错误码 说明
MP0000 处理成功
MP0001 参数错误
MP0002 系统错误
MP0003 系统错误
MP0006 参数缺失
MP3007 会员不存在
MP3008 会员创建失败
MP5004 商户号错误
PS0000 下单成功
PS0200 参数错误
PS0401 重复订单
PS0402 订单不存在
PS0403 非法的订单状态
PS0501 发号器错误
PS0502 回调上游业务方失败
PS0503 系统错误
PS0504 系统错误
PS0507 系统错误
PS1304 订单支付中
PS1310 订单已过期
PS1311 订单与流水不符
PS1330 订单异常
PS1331 操作场景不匹配
PS1332 订单刚修改过请稍候重试
PS1339 修单操作成功
PS1341 渠道商户号配置有误
PS1400 内部错误
PS1401 未知的错误码,请向开发者反应
PS1500 渠道侧明确失败
PS1510 回调状态不一致,本地显示该订单已经成功收款
PS1600 本次支付存在风险,请更换其他支付方式或稍后重试
PS1601 单日支付金额超限
PS1602 单日支付笔数超限
PS1603 单笔支付金额超限
GW40001 请求参数错误
GW40004 内部服务错误
GW40007 网关内部逻辑错误
GW4009 业务高峰期,系统繁忙,请您稍后再试
10000 成功
40001 请求参数错误
40004 内部服务错误
40007 网关内部逻辑错误
GW.PARAMS_ERROR 参数错误
GW.SIGN_ERROR 签名错误
GW.INTER_ERROR 服务内部错误(请联系字节跳动工程师,联系之前请确认涉及金额的字段单位是分,参数类型是整数)

签名说明

在小程序开发者的后台提交开通头条支付申请,审核通过以后,在小程序开发者后台就能看到支付app_id和支付app_secret。app_id需要随请求参数一起传递。app_secret作为签名的salt使用。

  1. 生成签名字符串

Key 筛选

去掉请求参数中的字节类型字段(如文件、字节流)、sign字段、值为空的字段(某些接口请求参数中特殊标明不参与签名的字段也需要去掉)。

K-V 排序和拼接

把筛选后的 k-v 集合按照 key 的 ASCII 码升序排序,例如: a, a1, abc, abcd, abce, abd, b1, ba

k-v 按照“key=value” 的格式链接,并且按照 key 排序使用 “&” 连接成一个字符串。

完整示例

例如下面的请求示例:

app_id=2014072300007148
method=alipay.mobile.public.menu.add
charset=GBK
timestamp=2014-07-24 03:07:50
biz_content={"key": "value", "key1": "value1"}
sign=abcded
sign_type=RSA2

按照key的ASCII码升序排序:

app_id, biz_content, charset, method, sign_type, timestamp, version
则待签名字符串signStr为(使用 “&” 符号连接):
app_id=2014072300007148&biz_content={"key": "value", "key1": "value1"
&charset=GBK&method=alipay.mobile.public.menu.add&sign_type=RSA2&timestamp=2014-07-24 03:07:50&version=1.0
  1. 签名(request) 如果请求选择MD5签名,签名逻辑为H(string+app_secret),其中H表示MD5算法,string + app_secret是字符串直接拼接,string在前,中间没有连接符。

  2. 验证签名(response) 头条的返回都使用MD5withRSA加签,需要使用下面的公钥验证我们response body。ToC收银台验签公钥为:

-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOZZ7iAkS3oN970+yDONe5TPhPrLHoNOZOjJjackEtgbptdy4PYGBGdeAUAz75TO7YUGESCM+JbyOz1YzkMfKl2HwYdoePEe8qzfk5CPq6VAhYJjDFA/M+BAZ6gppWTjKnwMcHVK4l2qiepKmsw6bwf/kkLTV9l13r6Iq5U+vrmwIDAQAB
-----END PUBLIC KEY-----

验签的内容是response里面的所有内容,对里面的key 进行排序,k-v 按照“key=value”的格式链接,并且按照 key 排序使用“&”连接成一个字符串形成待签名串(后续验签过程,即对待签名串做md5,再rsa验证)

给开发者使用的调起小程序支付的前端接口

开发者前端按照规定格式封装支付宝的数据传给小程序平台,小程序平台调起支付后台完成签名,然后通过SDK调起支付宝APP完成支付。暂时只支付宝APP支付。 接口请参考: http://developer.toutiao.com/docs/open/requestPayment.html

为保证系统的安全性,字节跳动的支付SDK内设有字节跳动专用的签名验签逻辑,需要开发者透传给小程序的参数带有字节跳动的签名才能正常调起支付SDK。

特别注意: 为了保证用户体验,支付回调的前端页面地址,必须为字节跳动小程序的地址,不允许跳转到外部地址。 例如,XX火车票的小程序,用户在支付完成后,需要跳回XX火车票在字节跳动内的小程序页面,不能跳转到其他第三方的页面或字节跳动外的页面。

产品交互DEMO

字节跳动小程序现有支付流程:业务场景订单页确认支付后打开支付宝App,输入支付密码并支付成功后,自动跳回小程序。

常见问题

签名不通过

签名不通过的响应例子

{
	"response":{
        "code":"40001",
        "msg":"Params Error",
        "sub_code":"GW.SIGN_ERROR",
        "sub_msg":"Sign Error"
    },
  "sign":"ao0UNbqEak66L8oT/WZigfpWGLs2XkNoCk6NlAaJxpSsWIrhWJUKEXJ8FNyIcGQ7cNbfojMP0weFRIJkCUvy5ilQhSxjwoIzLC2Ph37RSLFxx8paGczQNYOuNOIeazlIvqkHPqE3rVOslxQyZqWrzHzNLCbZ1KkD9pbOZV13eeg="
}

解决方案

  1. 核对开发文档,签名字段是否少字段或多字段
  2. 生成签名串是否按字母顺序排序
  3. 根据给出示例进行核对
  4. 注意签名过程中的空字符处理逻辑
  5. md5 值是小写,不能是大写

签名逻辑(md5)

func BuildMd5WithSalt(signMap map[string]string{}, salt string) string {
    signArr := make([]string, 0, len(signMap))
    for key, val := range signMap {
        signArr = append(signArr, fmt.Sprintf("%v=%v", key, val))
    }
    sort.Strings(signArr) // 升序排序
    signStr := strings.Join(signArr, "&")
    finStr := signStr + salt // salt是分配的密钥 
    sign := fmt.Sprintf("%x", md5.Sum([]byte(finStr)))
    return sign
}

signMap是需要签名的key-value格式数据

服务端签名示例(NodeJS)

var crypto = require('crypto'); 
var secret="xxxxxxxxxxx"
data = {
    app_id: '800000040005',
    method: 'tp.trade.confirm',
    sign: '',
    sign_type: 'MD5',
    timestamp: '1534342228111',
    trade_no: '20180821162045321823181631558207',
    merchant_id: '1300000004',
    uid: '6287430262',
    total_amount: 12,
    pay_channel: 'ALIPAY_NO_SIGN',
    pay_type: 'ALIPAY_APP',
    params: JSON.stringify({
        // 如果是新版支付宝,url 示例:
        url: 'app_id=2018041302549907&biz_content=%7B%22body%22%3A%22novel%22%2C%22subject%22%3A%22%E6%B5%8B%E8%AF%95%E7%9A%84%E5%95%86%E5%93%81%22%2C%22out_trade_no%22%3A%22201808211756233909095950%22%2C%22timeout_express%22%3A%2230m%22%2C%22total_amount%22%3A%220.01%22%2C%22seller_id%22%3A%22jrtoutiaoyxgs%40bytedance.com%22%2C%22product_code%22%3A%22QUICK_MSECURITY_PAY%22%7D&charset=utf-8&format=JSON&method=alipay.trade.app.pay&notify_url=https%3A%2F%2Ftp-pay-test.snssdk.com%2Fcallback%2Fali_pay&sign=ZfVkvu%2FSzBqFuqQMgr6MvsXomlr6BCuz7GYDnpsxd3SLVfCssV0q2cnxZyfjh%2FY%2Bk7PO1IeEl4rppQg%2FXgRuIqMXyKdhmigj4oPdQVJEkbSQEcCW4m8mwpXLNjlLH%2FHae3u3hjrMDVPuVXeIxjoq1NLPXy09GY5u1MX8E2lkn8xtmOxA2cXXRIrAa8gTplUoXWkSSkZMgvSTzQ9RjRmlKtK4nERdDWh5RBXLNDU%2FD2FfqIeZuLNZh%2BW8j4dYGtPDm9nWYRz0tLizJDm6E76aTM3qvLi0havCCrHgxZ5d8tVN7GNztA6olbGOiXubEGUq4yBqCojiALEEVpKqfQdZGQ%3D%3D&sign_type=RSA2&timestamp=2018-08-21+17%3A56%3A24&version=1.0'
        // 如果是老版支付宝,url 示例:
        // url: '_input_charset=\"utf-8\"&body=\"novel\"&it_b_pay=\"30m\"&notify_url=\"https://tp-pay-test.snssdk.com/callback/ali_pay\"&out_trade_no=\"201808211755020406852103\"&partner=\"2088801374045154\"&payment_type=\"1\"&seller_id=\"adsense@bytedance.com\"&service=\"mobile.securitypay.pay\"&subject=\"测试的商品\"&total_fee=\"0.01\"&sign=\"RGdwAoCy5DsjdFBdtrN9WzdYtyZGlUHn8dbAQVQsIPidLTR9s%2BCVtAj%2BtYzL8oAHP0IXJZw8U6EGlyA2MG2ZxhJRI1N1RhDMZOz56eAXO%2FITZYiGSB01hkhx9yhqmWAUJQfUMRHJZswS1DEpwam1JfaoahZ%2Bf%2FEE%2FkvG6ma67t4%3D\"&sign_type=\"RSA\"'
    }),
};

sign_array=new Array( "app_id="+data.app_id, 
                      "sign_type="+data.sign_type,
                      "timestamp="+data.timestamp,
                      "trade_no="+data.trade_no,
                      "merchant_id="+data.merchant_id,
                      "uid="+data.uid,
                      "total_amount="+data.total_amount,
                      "params="+ data.params,
                    )
sign_array.sort(); // 升序
sign_str = sign_array.join("&")+secret
data.sign = crypto.createHash('md5').update(sign_str).digest("hex")

签名字例子

app_id=800000040005&merchant_id=1300000004&params={"url":"app_id=2018041302549907&biz_content=%7B%22body%22%3A%22novel%22%2C%22subject%22%3A%22%E6%B5%8B%E8%AF%95%E7%9A%84%E5%95%86%E5%93%81%22%2C%22out_trade_no%22%3A%22201808211756233909095950%22%2C%22timeout_express%22%3A%2230m%22%2C%22total_amount%22%3A%220.01%22%2C%22seller_id%22%3A%22jrtoutiaoyxgs%40bytedance.com%22%2C%22product_code%22%3A%22QUICK_MSECURITY_PAY%22%7D&charset=utf-8&format=JSON&method=alipay.trade.app.pay&notify_url=https%3A%2F%2Ftp-pay-test.snssdk.com%2Fcallback%2Fali_pay&sign=ZfVkvu%2FSzBqFuqQMgr6MvsXomlr6BCuz7GYDnpsxd3SLVfCssV0q2cnxZyfjh%2FY%2Bk7PO1IeEl4rppQg%2FXgRuIqMXyKdhmigj4oPdQVJEkbSQEcCW4m8mwpXLNjlLH%2FHae3u3hjrMDVPuVXeIxjoq1NLPXy09GY5u1MX8E2lkn8xtmOxA2cXXRIrAa8gTplUoXWkSSkZMgvSTzQ9RjRmlKtK4nERdDWh5RBXLNDU%2FD2FfqIeZuLNZh%2BW8j4dYGtPDm9nWYRz0tLizJDm6E76aTM3qvLi0havCCrHgxZ5d8tVN7GNztA6olbGOiXubEGUq4yBqCojiALEEVpKqfQdZGQ%3D%3D&sign_type=RSA2&timestamp=2018-08-21+17%3A56%3A24&version=1.0"}&sign_type=MD5&timestamp=1534342228111&total_amount=12&trade_no=20180821162045321823181631558207&uid=6287430262xxxxxxxxxxx

biz_content问题

biz_content编码问题目前大多数情况是后端遇到的问题,所以这里给出一个完整的python例子,用于核对编码问题。

data = {
    "out_order_no": out_order_no,
    "uid": uid,
    "merchant_id": merchant_id,
    "total_amount": "1",
    "currency": "CNY",
    "subject": "签名测试商户",
    "body": "签名测试的一个订单",
    "valid_time": "600",
    "trade_time": "%s" % int(time.time()),
    "notify_url": "http://xxx.xxx.xxx/",
    "risk_info": json.dumps({"ip": "127.0.0.1"}),
}

payload = {
    "app_id": app_id,
    "format": "JSON",
    "charset": "utf-8",
    "sign_type": "MD5",
    "timestamp": int(time.time()),
    "version": "1.0",
    "merchant_id": merchant_id,
    "uid": uid,
    "biz_content": json.dumps(data),
    "method": "tp.trade.create"
}
sign_list = []
for key, value in payload.items():
    sign_list.append("%s=%s" % (key, value))
sign_list.sort()  # 升序
payload["sign"] = md5(string.join(sign_list, "&") + secret)
response = requests.request("POST", url, data=urlencode(payload), headers={'Content-Type': "application/x-www-form-urlencoded"})
print response.text

params中的url转义问题

url转义问题参考biz_content问题

app_id和merchant_id传错

错误请求响应

{
    "response":{
        "code":"40007",
        "msg":"Service Internal Error",
        "sub_code":"GW.THIRD_PARTY_ERROR",
        "sub_msg":"Gateway call third party err: merchantV2 appId"
    },
    "sign":"y3bRQYPreOC9hxZdkZpCxsKSWaxBbaIxf6kR8hHBHVn4EXcBU12Ym1mMsV17RX3DCBJWAZe8R8LVKWkRByUI4DiE9srmoDWz1ZSBWOeFN3cmqWzlxKf2p9BGfjhgdSbF1Vi2YkqpkBMVvSaE85XCOjaEZxGMELyIGnxm4etLOqM="
}

到小程序开发平台查看确认参数是否正确,如下图

拉起支付宝失败

确保支付接口传递的data.params.url参数能正常拉起支付宝。

利用支付宝提供的测试demo检测url能否正常拉起支付宝app。