创建订单(SPI)
收藏
我的收藏接口说明
抖音侧通知第三方创建景区订单。
- 1.ClientKey 维度默认配置是 抖音侧支付成功后 通知第三方创单,不额外通知支付成功。
- 2.抖音请求第三方超时,如支付前创单则默认创单成功,抖音会走后续链路支付订单;如支付后创单,重试仍超时,则会拒单。
基本信息
Scope | life.capacity.trip_order_create | |||
权限要求 | 景区行业解决方案-景区创建订单 | |||
回调场景 | 景区创建订单 |
请求头
- •Content-Type:
application/json
- •X-Bytedance-Logid: 请求 logid, 用于问题排查用
- •x-life-clientkey: 服务商应用 的 client_key
- •X-life-sign: 请求签名,签名规则
请求参数
名称 | 类型 | 是否必填 | 描述 | 示例值 |
order_id | string | 是 | 抖音侧订单 ID(可作幂等键) | 100001 |
account_id | string | 是 | 商家ID | |
poi_id | string | 是 | 抖音POI_ID(即门店id) | |
product_id | string | 是 | 抖音侧商品 ID | 1773200310436864 |
product_out_id | string | 否 | 第三方商品 ID | 1773200310436864 |
sku_id | string | 是 | 抖音侧规格 ID | 200001 |
sku_out_id | string | 否 | 第三方规格 ID | 300001 |
count | int | 是 | 购买份数(等于「发放凭证」接口的 copies) | 3 |
traveler_info | object | 是 | 出行人群(对应商品 crowds) | |
-total_num | int32 | 是 | 出行人数 | 1 |
-diff_target_crowd | bool | 是 | 是否区分人群 | |
-crowd_list | list | 否 | 人群列表 | |
--crowd_type | int32 | 是 | 人群类型 Child=1 儿童 Adult=2 成人 Old=3 老人 Student=4 学生 Special=5 特殊人群 Male=6 男士 Female=7 女士 Group=8 团体 Couple=9 情侣 Military = 10 军人 Teacher = 11 教师 Disabled = 12 残疾 | |
--traveler_num | int32 | 是 | 此人群的适用人数 | |
biz_type | int | 是 | 业务类型,1 预售券,2 日历票 | 2 |
amount | struct | 是 | 金额信息 | |
- pay_amount | int | 是 | 支付金额,分 | 8000 |
- origin_amount | int | 是 | 原始金额,分 | 10000 |
buyer | struct | 是 | 购买人信息,默认只强制留手机号 | |
- name | string | 是 | 姓名,加密,可能为空串,默认不强制购买人留姓名 | 小明 |
- phone | string | 是 | 联系电话,加密,肯定有 | 17812342702 |
create_order_time_unix | int | 是 | 创建订单时间戳,秒 | 1678856518 |
book_start_day | string | 是 | 日历票下单参数,预定开始日期,yyyy-MM-dd | 2023-03-15 |
book_end_day | string | 是 | 日历票下单参数,预定结束日期,yyyy-MM-dd | 2023-03-16 |
tourists | list<struct> | 否 | 日历票下单参数,游玩人信息,根据商品配置留资 | |
- name | string | 否 | 姓名,加密 | 小红 |
- phone | string | 否 | 联系电话,加密 | 18112345678 |
- license_type | int | 否 | 证件类型 1 身份证 2 港澳通行证 3 台湾通行证 4 回乡证 5 台胞证 6 护照 7 外籍护照 8 外国人永久居留证 | 1 |
- license_id | string | 否 | 证件 ID,加密 | 11204416541220243X |
remark | string | 否 | 日历票下单参数,备注要求 | 需要 xxxxx |
refund_rule | struct | 否 | 日历票下单参数,退改规则 | |
-auto_refund_time | int | 否 | 自动退,离园日 24 时往后推的秒数 | |
-can_refund_partly | bool | 否 | 是否支持部分退 | |
-auto_verify_timestamp | int64 | 否 | 自动核销时间 | |
- refund_type | int | 是 | 退改类型,1 未使用随时退,2 不可退,3 有条件退 | 3 |
- refund_details | list<struct> | 否 | 退改详情,支持多个阶梯退 | |
-- refund_time | int | 是 | 当退改类型为有条件退时必填,入园日 24 时往前倒推的秒数 | 21600 |
-- refund_fee_type | int | 是 | 当退改类型为有条件退时必填,退改手续费类型,1 金额,2 比例 | 2 |
-- refund_fee | int | 是 | 当退改类型为有条件退时必填,退改手续费,如果类型为金额则单位为分,如果类型为比例则单位为万分位 | 1234 |
ticket_rule | struct | 是 | 票务规则,包含凭证方式、券码类型、券码服务商 | |
- code_sending_info | list<int32> | 是 | 凭证发放方式,多选 (建议全部写入list,凭证回调时,如果有额外类型的凭证会报错) | 1 身份证 2 券号 3 券码 6 URL 链接 |
- code_type | int32 | 是 | 券码类型,日历票 code_type = 2 | 2 第三方券 |
- url_type | int32 | 否 | 如果code_sending_info=6 凭证类型为url,则必填 | 1 静态二维码 2 其他 |
ticket_specification | struct | 否 | 日历票下单参数,票种规格说明 | |
- ticket_session | strcut | 否 | 场次 | |
-- ticket_session_name | string | 是 | 名称 | 上午场 |
-- ticket_session_time | string | 否 | 时间 | 8:00-9:00 |
- ticket_seat | string | 否 | 坐席 | 普通座 |
- ticket_area | string | 否 | 区域 | 上行 |
解密方法
- 1.根据 ClientKey 找到 ClientSecret,将 ClientSecret 向左右使用字符补齐 32 位/裁剪至 32 位,
- a.补齐:补位字符:#, 先补左侧再补右侧 再补左侧······直到补满 32 位。
- b.裁剪:先裁剪左侧再裁右侧再裁左侧······直到剩余 32 位。
- c.(正常不需要补齐,secret 默认为 32 位,此举是为了以防万一)
- 2.将 ClientSecret 作为 Key, 右侧 16 位为向量 IV
- 3.将密文进行 base64 解码。
- 4.使用 AES-256-CBC 模式解密解码后的密文,对齐使用 PKCS5Padding 方式
Golang SDK
package utils import ( "bytes" "crypto/aes" "crypto/cipher" "encoding/base64" ) // AesDecrypt 解密函数 // encryptedStr:base64后的密文 // secret:appid/client_key对应的client_secret // return: []byte 明文 func AesDecrypt(encryptedStr string, secret string) ([]byte, error) { // 加密字符串进行base64解码 decodeBytes, err := base64.StdEncoding.DecodeString(encryptedStr) if err != nil { return nil, err } key, iv := parseSecret(secret) block, err := aes.NewCipher(key) if err != nil { return nil, err } blockSize := block.BlockSize() blockMode := cipher.NewCBCDecrypter(block, iv[:blockSize]) origData := make([]byte, len(decodeBytes)) blockMode.CryptBlocks(origData, decodeBytes) origData = PKCS5UnPadding(origData) return origData, nil } // parseSecret 将secret解析为key和iv func parseSecret(secret string) ([]byte, []byte) { // secret对齐为32位 secret = cutSecret(secret) secret = fillSecret(secret) key, iv := secret, secret[16:] return []byte(key), []byte(iv) } func fillSecret(secret string) string { if len(secret) >= 32 { return secret } rightCnt := (32 - len(secret)) / 2 leftCnt := 32 - len(secret) - rightCnt var byt bytes.Buffer byt.Write(bytes.Repeat([]byte("#"), leftCnt)) byt.WriteString(secret) byt.Write(bytes.Repeat([]byte("#"), rightCnt)) return byt.String() } func cutSecret(secret string) string { if len(secret) <= 32 { return secret } rightCnt := (len(secret) - 32) / 2 leftCnt := len(secret) - 32 - rightCnt return secret[leftCnt: 32+leftCnt] } func PKCS5UnPadding(origData []byte) []byte { length := len(origData) unpadding := int(origData[length-1]) return origData[:(length - unpadding)] }
Java SDK
package com.douyin.open.goodlife; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; public class SignUtil { /* * appid/client_key对应的client_secret TODO 这里换成服务商的appsecret */ private static final String secret = "12345678901234566543210987654321"; private static final String key; private static final String iv; static { key = parseSecret(secret); iv = key.substring(16); } /** * @Description AES解密 * @param data base64后的密文 * @return 明文 */ public static String decryptAES(String data) throws Exception { try { byte[] encrypted1 = decode(data);//先用base64解密 Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES"); IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes()); cipher.init(Cipher.DECRYPT_MODE, keyspec, ivspec); byte[] original = cipher.doFinal(encrypted1); String originalString = new String(original); return originalString.trim(); } catch (Exception e) { e.printStackTrace(); return null; } } /** * base64编码 */ public static String encode(byte[] byteArray) { return new String(Base64.getEncoder().encode(byteArray)); } /** * base64解码 */ public static byte[] decode(String base64EncodedString) { return Base64.getDecoder().decode(base64EncodedString); } private static String parseSecret(String secret) { secret = fillSecret(secret); secret = cutSecret(secret); return secret; } private static String cutSecret(String secret) { if (secret.length() <= 32) { return secret; } int rightCnt = (secret.length() - 32) / 2; int leftCnt = secret.length() - 32 - rightCnt; return secret.substring(leftCnt, 32 + leftCnt); } private static String fillSecret(String secret) { if (secret.length() >= 32) { return secret; } int rightCnt = (32 - secret.length()) / 2; int leftCnt = 32 - secret.length() - rightCnt; StringBuilder sb = new StringBuilder(""); for (int i = 0; i < leftCnt; i++) { sb.append('#'); } sb.append(secret); for (int i = 0; i < rightCnt; i++) { sb.append('#'); } return sb.toString(); } }
响应参数
名称 | 类型 | 是否必填 | 描述 | 示例值 |
data | struct | 是 | | |
- error_code | int | 是 | 错误码,合法范围为 [0,999999] | 0 |
- description | string | 是 | 错误信息 | |
- order_out_id | string | 否 | 外部订单 ID,当处理成功时必须返回且不为空 | |
- confirm_info | struct | 否 | 确认接单信息,支付后创单模式必须返回 | |
-- confirm_mode | int | 是 | 接单模式,1 同步接单,2 异步接单 | 1 |
-- confirm_result | int | 否 | 接单结果,同步接单时必填,1 接单,2 拒单 | 1 |
响应示例
{ "data": { "error_code": 0, "description": "", "order_out_id": "123123", "confirm_info": { "confirm_mode": 1, "confirm_result": 1 } } }
错误码
- •重试-最多12次
- ◦创建订单失败时,若请求未触达商家(如网络错误)或商家返回明确错误码(error_code=100)抖音会进行重试
- ◦重试策略:采用退避策略第一次间隔 5s,之后每次之间间隔 10s、20s、40、60s、60s(60s 最多重试12次)。
- •幂等
- ◦为防止重试导致的重复下单,服务商可根据抖音订单 Id 做幂等处理
error_code | 错误场景描述 | 商家原因描述(可自定义返回) |
0 | 成功 | |
1 | 库存不足 | |
2 | 商品已下架 | |
3 | 当前出行人已购票 | |
4 | 当前日期不可预订 | |
5 | 年龄不符合,仅限指定年龄的用户购买 | |
6 | 性别不符合,仅限指定性别的用户购买 | |
7 | 身份证限购,用户已达购买上限 | |
8 | 手机号限购,用户已达购买上限 | |
9 | 订单购买数量超过上限 | |
10 | 用户地区不符合,仅限特定地区的用户购买 | |
11 | 其他原因限购(请返回具体原因) | |
12 | 缺少手机号信息 | |
13 | 缺少证件信息 | |
14 | 缺少出行人姓名 | |
15 | 出行人数和份数不匹配 | |
19 | 手机号格式问题 | |
20 | 证件号格式问题 | |
21 | 姓名格式问题 | |
22 | 商家账户余额不足,无法下单(服务商场景使用) | |
23 | 价格不一致 | |
100 | 商家系统内部异常,需要抖音侧重试调用 | |
999999 | 其他原因不可预订,请返回具体原因(抖音不重试调用) | |