礼包系统开发指南
接入礼包系统之前,需完成应用配置。在 游戏服务 - 应用配置 中可获取 Client ID 和 Server Secret。请妥善保管这些信息,勿传给他人。
开始
礼包系统接入适用于有请求接口能力的游戏,目前尚未提供完全离线的兑换码服务。
根据您的使用场景,您可以灵活对接以下不同兑换接口之一完成兑换流程。
| 方式 | 接口名 | 需游戏方校验后台设置的兑换条件 | 需游戏方提供发送道具接口 |
|---|---|---|---|
| 游戏方校验 | /api/v1.0/cdk/game/submit-check | ✓ | ✓ |
| 游戏方发送 | /api/v1.0/cdk/game/submit-send | ✗ | ✓ |
| 无服务器兑换 | /api/v1.0/cdk/game/submit-simple | ✗ | ✗ |
| 游戏方校验 Web | /api/v1.0/cdk/page/submit-check | ✓ | ✓ |
| 游戏方发送 Web | /api/v1.0/cdk/page/submit-send | ✗ | ✓ |
接入顺序
- 完成应用配置获取 Client ID 和 Server Secret 信息。
- 进入 运营工具-礼包系统-服务器配置 选择性填写游戏方服务器信息并在其中配置发送通知接收地址用来发送相应道具。
- 创建礼包活动导出兑换码,礼包配置完成准备测试。
- 根据实际业务场景使用对应兑换接口完成兑换。
兑换流程
兑换流程大致是:
- 根据接口文档提交 兑换码 与 配置信息等参数 给兑换接口。
- 兑换接口校验数据合法性。
- 兑换系统根据业务场景选择是否调用 游戏方提供的二次校验接口。(可选,当后台配置二次校验接口时会调用)
- 校验全部通过后根据不同接口判断是否调用 游戏方提供的发送道具接口 完成兑换。(可选,当后台配置发送道具接口时会调用)
有三种不同的兑换接口可以进行兑换,整体对接流程只要对接其一接口即可完成:
| 名称 | 描述 |
|---|---|
| 游戏方校验 | 如果您将使用服务器对接兑换系统,可以安全地保存密钥信息,并希望每次兑换时能通过您的应用程序来校验是否允许兑换,调用您的接口来发送道具,请使用此流程。 |
| 游戏方发送 | 如果您将使用服务器对接兑换系统,可以安全地保存密钥信息,并希望调用您的应用程序接口来发送每次兑换的道具,请使用此流程。 |
| 无服务器兑换 | 如果您希望不开发接口完成兑换流程,调用兑换接口后根据返回值来判断是否兑换失败,请使用此流程。 |
三种兑换场景
游戏方校验
当您请求兑换码 submit-check 接口进行兑换时,兑换系统会核对库存以及提交数据是否正确,兑换系统核对通过后会调用您在后台配置的二次校验接口并将后台设置的条件作为参数传给您来校验。
当您的接口校验通过后会继续将后台配置的奖品内容作为参数调用您的发送道具接口,当发送道具成功完成后本次兑换完成。

游戏方发送
当您请求兑换码 submit-send 接口进行兑换时,兑换系统会核对库存以及提交数据是否正确,核对完成后会继续将后台配置的奖品内容作为参数调用您的发送道具接口,当发送道具成功完成后本次兑换完成。

无服务器兑换
当您请求兑换码 submit-simple 接口进行兑换时,兑换系统会核对库存以及提交数据是否正确,兑换信息验证通过后本次兑换完成并将后台配置的道具信息返回给您。

对接兑换接口
游戏方二次校验规范
礼包活动在后台配置了自定义兑换条件后,使用带二次校验能力的接口进行兑换时,兑换系统会将此礼包活动的自定义兑换条件发送给游戏方进行二次校验,下图配置会对您在后台配置的接口发起如 下请求:

curl --location -g --request POST <后台配置的游戏方二次校验接口地址> \
--header 'Content-Type: application/json' \
--data-raw '{"activity_id":<后台活动ID>,"character_id":<用户ID>,"content":"[{\"condition\":\"level\",\"operate":\"gt\",\"value\":\"10\"}]","content_obj": [{"condition":"level","operate":"gt","value":"10"}],"ext":<兑换提交的ext参数>,"gift_code":<礼包码>,"nonce_str":"abcdc","server_code": <后台配置的code>,"sign":"42d2b58a8cd58b5e63d90524aaf63ef97e04f223","timestamp":1658825322,"custom":{<自定义维度>:[<自定义维度字段1>,<自定义维度字段2>]}}'
请在校验完成后返回带有特定字段的 JSON 返回值告诉兑换系统校验结果。
{
"status": true,
"errorCode": "字符串类型的错误code"
}
如果您希望返回字段命名风格保持一致也可以使用下划线命名风格返回信息。
{
"status": true,
"error_code": "字符串类型的错误code"
}
如果您并不要求兑换系统知晓兑换的错误信息可以直接返回 非 200 HTTP code 表示兑换失败,或者返回 HTTP code 200 表示兑换成功。 这样就无需按兑换系统的格式来返回信息。
| 请求参数 | 类型 | 含义 |
|---|---|---|
| activity_id | 字符串 | 礼包活动 ID |
| character_id | 字符串(长度不得超过 50 字符) | 兑换的用户 ID |
| content | 字符串(嵌套 JSON 字符串) | 后台配置的自定义条件 |
| content.condition | 字符串 | 自定义条件-key |
| content.operate | 字符串 | 关系-详情见其他 |
| content.value | 字符串 | 自定义条件-value |
| content_obj | 对象数组(和 content 含义相同) | 后台配置的自定义条件 |
| content_obj.condition | 字符串 | 自定义条件-key |
| content_obj.operate | 字符串 | 关系-详情见其他 |
| content_obj.value | 字符串 | 自定义条件-value |
| ext | 字符串 | 请求接口时传递的数值 |
| gift_code | 字符串 | 用户此次兑换的礼包码 |
| nonce_str | 字符串 | 此次请求的随机字符串 |
| server_code | 字符串 | 服务器 code |
| sign | 字符串 | 签名 |
| timestamp | 数字 | 秒级时间戳 |
| custom | 对象 | 维度信息 |
| custom.xxx | 字符数组 | 自定义维度 |
| 返回参数 | 类型 | 含义 |
|---|---|---|
| errorCode | 字符串 | 错误类型,error_code 含义相同,优先取 errorCode |
| ext | 字符串(可选) | 需要透传给发送道具接口的字符串 |
| status | 布尔值 | 是否校验成功,true 为成功,false 为失败 |
游戏方发送道具规范
后台设置的物品信息,在兑换系统完成兑换校验后会带上配置的参数请求 游戏方的发送道具接口。下图配置会对您在后台配置的接口发起如下请求:

curl --location -g --request POST <后台配置的游戏方发送道具接口地址> \
--header 'Content-Type: application/json' \
--data-raw '{"activity_id": <后台活动ID>,"character_id":"uid","content":"[{\"name\": \"奖品名称\", \"number\": 2}]","content_obj": [{"name": "奖品名称", "number": 2}],"ext":<兑换提交的ext>,"gift_code":"gift_code","nonce_str":"abcdc","server_code":<后台配置的code>,"sign":"fbe23e6f6f5193355b29e9ea2913b1992af56346","check_ext":<二次兑换接口返回的ext>,"timestamp":1658827492,"custom":{<自定义维度>:[<自定义维度字段1>,<自定义维度字段2>]}}'
同样请在发送完成后返回带有特定字段的 JSON 返回值告诉兑换系统发送道具结果,也可以使用下划线命名返回字段。
{
"status": true,
"errorCode": "字符串类型的错误code"
}
如果您并不要求兑换系统知晓兑换的错误信息可以直接返回 非 200 HTTP code 表示兑换失败,或者返回 HTTP code 200 表示兑换成功。 这样就无需按兑换系统的格式来返回信息。
| 请求参数 | 类型 | 含义 |
|---|---|---|
| activity_id | 字符串 | 礼包活动 ID |
| character_id | 字符串(长度不得超过 50 字符) | 兑换的用户 ID |
| server_code | 字符串 | 服务器 code |
| content | 字符串(嵌套 JSON 字符串) | 后台配置的自定义奖品 |
| content.name | 字符串 | 奖品名称 |
| content.number | 数字 | 奖品数量 |
| content_obj | 对象数组(和 content 含义相同) | 后台配置的自定义奖品 |
| content_obj.name | 字符串 | 奖品名称 |
| content_obj.number | 整型 | 奖品数量 |
| ext | 字符串 | 请求接口时传递的数值 |
| gift_code | 字符串 | 用户此次兑换的礼包码 |
| nonce_str | 字符串 | 此次请求的随机字符串 |
| sign | 字符串 | 签名 |
| check_ext | 字符串 | 二次校验透传过来的字符串参数 |
| timestamp | 数字 | 秒级时间戳 |
| custom | 对象 | 维度信息 |
| custom.xxx | 字符数组 | 自定义维度 |
| 返回参数 | 类型 | 含义 |
|---|---|---|
| errorCode | 字符串 | 错误类型,error_code 含义相同,优先取 errorCode |
| status | 布尔值 | 是否校验成功,true 为成功,false 为失败 |
无服务器兑换
使用无服务器兑换接口进行兑换时无需游戏方开发校验与发送接口。
但要注意在 后台配置礼包时需创建无服务器礼包。
兑换系统在接收到兑换请求后直接将兑换结果返回,游戏根据返回结果判断是否发放道具。
接口参数的详细说明请看文档下面的 三种兑换接口 - 无服务器兑换接口
维度配置
在礼包后台中可以配置自定义维度信息,配置完成后创建活动礼包时可以勾选上相应维度信息。这些勾选上维度的礼包在二次校验,发送道具,以及无服务器返回结果里会带上勾选的维度参数,方便您对礼包做进一步分类业务的处理。
请求域名
| TapTap 版本 | 域名 |
|---|---|
| 国内 taptap.cn | https://poster-api.xd.cn |
| 海外 taptap.io | https://poster-api.xd.com |
签名
游戏方与兑换系统的接口通讯时将使用签名参数作为基础的安全验证,其中 Secret 为后台 游戏服务 - 应用配置 中 Server Secret。
为了保证安全性,请勿将 Server Secret 信息放在客户端中使用以防敏感信息泄漏。
计算签名伪代码如下:
sign == sha1(timestamp + nonce_str + secret)
您也可以通过单元测试验证签名计算过程正确与否,如下是 golang 示例:
func TestMakeSign(t *testing.T) {
timestamp := 1655724586
nonceStr := "abcde"
secret := "abc"
sign := MakeSign(int64(timestamp), nonceStr, secret)
assert.Equal(t, sign, "3cb8c38833fa742e7873378faddcbe5b56088482")
//output: 3cb8c38833fa742e7873378faddcbe5b56088482
}
为了 Secret 不存放在客户端中,无服务器兑换返回的签名通过 Client ID 代替 Secret 进行验签。
sign == sha1(timestamp + nonce_str + client_id)
二次校验兑换接口
POST /api/v1.0/cdk/game/submit-check
Header Content-Type:application/json
请求参数:
| 请求参数 | 类型 | 是否必选 | 含义 |
|---|---|---|---|
| client_id | 字符串 | 必传 | 开发者后台 ClientId 信息 |
| gift_code | 字符串 | 必传 | 这次兑换的兑换码 |
| server_code | 字符串 | 必传 | 后台配 置的服务器 code |
| character_id | 字符串 | 必传(长度不得超过 50 字符) | 游戏用户 ID |
| nonce_str | 字符串 | 必传 | 随机字符串,建议五位随机字符串 |
| sign | 字符串 | 必传 | 签名 |
| timestamp | 数字 | 必传 | 时间戳,单位为秒,有效期为一分钟 |
| ext | 字符串 | 非必传 | 该字段会原封不动出现在 ext 中传给游戏二次校验接口 |
| lang | 字符串 | 非必传 | 提示的语言类型,详情见其它 |
请求示例:
curl --location --request POST '<URL>/api/v1.0/cdk/game/submit-check' \
--header 'Content-Type: application/json' \
--data-raw '{"client_id":<后台ClientId>,"gift_code":<这次使用的兑换码>,"server_code":<服务器code>,"character_id":<游戏用户ID>,"nonce_str":<随机字符串>,"sign":<签名>,"timestamp":<时间戳>,"ext": <透传字段>}'
无二次校验兑换接口
POST /api/v1.0/cdk/game/submit-send
Header Content-Type:application/json
请求参数:
| 请求参数 | 类型 | 是否必选 | 含义 |
|---|---|---|---|
| client_id | 字符串 | 必传 | 开发者后台 ClientId 信息 |
| gift_code | 字符串 | 必传 | 这次兑换的兑换码 |
| server_code | 字符串 | 必传 | 后台配置的服务器 code |
| character_id | 字符串 | 必传(长度不得超过 50 字符) | 游戏用户 ID |
| nonce_str | 字符串 | 必传 | 随机字符串,建议五位随机字符串 |
| sign | 字符串 | 必传 | 签名 |
| timestamp | 数字 | 必传 | 时间戳,单位为秒,有效期为一分钟 |
| lang | 字符串 | 非必传 | 提示的语言类型,详情见其它 |
请求示例:
curl --location --request POST '<URL>/api/v1.0/cdk/game/submit-send' \
--header 'Content-Type: application/json' \
--data-raw '{"client_id":<后台ClientId>,"gift_code":<这次使用的兑换码>,"server_code":<服务器code>,"character_id":<游戏用户ID>,"nonce_str":<随机字符串>,"sign":<签名>,"timestamp":<时间戳>,"ext": <透传字段>}'
无服务器兑换接口
POST /api/v1.0/cdk/game/submit-simple
Header Content-Type:application/json
请求参数:
| 请求参数 | 类型 | 是否必选 | 含义 |
|---|---|---|---|
| client_id | 字符串 | 必传 | 开发者后台 ClientId 信息 |
| gift_code | 字符串 | 必传 | 这次兑换的兑换码 |
| character_id | 字符串 | 必传(长度不得超过 50 字符) | 游戏用户 ID |
| nonce_str | 字符串 | 必传 | 随机字符串,建议五位随机字符串 |
| sign | 字符串 | 必传 | 签名,该签名用 ClientId 代替 Secret |
| timestamp | 整型 | 必传 | 时间戳,单位为秒,有效期为一分钟 |
| lang | 字符串 | 非必传 | 提示的语言类型,详情见其它 |
返回参数:
| 返回参数 | 类型 | 含义 |
|---|---|---|
| activity_id | 字符串 | 礼包活动 ID |
| nonce_str | 字符串 | 随机字符串 |
| timestamp | 整型 | 时间戳 |
| content | 字符串(嵌套 JSON 字符串) | 后台配置的自定义奖品 |
| content.name | 字符串 | 奖品名称 |
| content.number | 数字 | 奖品数量 |
| content_obj | 数组对象(含义与 content 相同) | 后台配置的自定义奖品 |
| content_obj.name | 字符串 | 奖品名称 |
| content_obj.number | 整型 | 奖品数量 |
| sign | 字符串 | 返回签名,验证返回数据没有被修改 |
| c_sign | 字符串 | 内容签名,验证返回数据没有被修改 |
| error | 整形 | 0 为成功,其他为失败码 |
| success | 布尔 | 是否成功 |
| custom | 对象 | 维度信息 |
| custom.xxx | 字符数组 | 自定义维度 |
请求示例:
curl --location --request POST '<URL>/api/v1.0/cdk/game/submit-simple' \
--header 'Content-Type: application/json' \
--data-raw '{"client_id":<后台ClientId>,"gift_code":<这次使用的兑换码>,"character_id":<游戏用户ID>,"nonce_str":<随机字符串>,"sign":<签名>,"timestamp":<时间戳>}'
Shell 请求示例:
#! /usr/bin/env bash
# TapDC 后台应用的 Client ID
client_id="请替换为控制台的 `Client ID`"
# 本次兑换的兑换码 在“礼包活动面板-数据-导出” 然后导出的文件里面可以看到格式如 S0LLC8ICB2MP2 的兑换码
gift_code="请替换为礼包面板中的兑换码"
# 随机字符串,建议五位随机字符串
nonce_str="A2B3Z"
# 游戏用户 ID
character_id="6347de128b****3ee825e029"
# 签名,该签名用 ClientId 代替 Secret
signed=$(echo -n $(date +%s)$nonce_str$client_id |openssl sha1)
RESULT_REQUEST=`curl --location --request POST 'https://poster-api.xd.cn/api/v1.0/cdk/game/submit-simple' \
--header 'Content-Type: application/json' \
--data-raw "{\"client_id\":\"$client_id\",\"gift_code\":\"$gift_code\",\"character_id\":\"$character_id\",\"nonce_str\":\"$nonce_str\",\"sign\":\"$signed\",\"timestamp\":$(date +%s)}"`
echo $RESULT_REQUEST
Android 请求示例:
可以参考 Android Demo 中礼包系统功能
C# 请求示例:
using UnityEngine.Networking;
using System;
using System.Text;
using System.Security.Cryptography;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class APIClient : MonoBehaviour
{
private const string apiUrl = "https://poster-api.xd.cn/api/v1.0/cdk/game/submit-simple";
private const string contentType = "application/json";
private const string clientId = "hskcocvse6x1cgkklm";
private const string giftCode = "NZ4mp2cztRMXH";
private const string characterId = "879a0cdd9nnb917055ee";
private const string nonceStr = "RFG7U";
public GUISkin demoSkin;
void Start()
{
}
private void OnGUI()
{
GUI.skin = demoSkin;
float scale = 1.0f;
float btnWidth= Screen.width / 5 * 2;
float btnWidth2 = btnWidth + 80 * scale;
float btnHeight = Screen.height / 25;
float btnTop = 30 * scale;
float btnGap = 20 * scale;
GUI.skin.button.fontSize = Convert.ToInt32(13 * scale);
var style = new GUIStyle(GUI.skin.button) { fontSize = 20 };
var labelStyle = new GUIStyle(GUI.skin.label) { fontSize = 30 };
if (GUI.Button(new Rect((Screen.width - btnGap) / 2 - btnWidth, btnTop, btnWidth /2, btnHeight), "返回", style))
{
UnityEngine.SceneManagement.SceneManager.LoadSceneAsync(0);
}
btnTop += btnHeight + 20 * scale;
if (GUI.Button(new Rect((Screen.width - btnGap) / 2 - btnWidth, btnTop, btnWidth, btnHeight), "无服务器兑换", style))
{
StartCoroutine(StartRequest());
}
}
private IEnumerator StartRequest()
{
// 获取当前时间戳(秒)
int timestamp = (int)(System.DateTime.UtcNow.Subtract(new System.DateTime(1970, 1, 1))).TotalSeconds;
// 拼接并加密 sign 参数
string sign = GetSign(timestamp, nonceStr, clientId);
// 构建请求参数
string requestBody = "{\"client_id\":\"" + clientId + "\",\"gift_code\":\"" + giftCode + "\",\"character_id\":\"" + characterId + "\",\"nonce_str\":\"" + nonceStr + "\",\"timestamp\":" + timestamp + ",\"sign\":\"" + sign + "\"}";
// 创建 UnityWebRequest 对象
UnityWebRequest request = new UnityWebRequest(apiUrl, "POST");
// 设置请求头
request.SetRequestHeader("Content-Type", contentType);
// 设置请求体
byte[] bodyRaw = Encoding.UTF8.GetBytes(requestBody);
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
request.downloadHandler = new DownloadHandlerBuffer();
// 发送请求
yield return request.SendWebRequest();
// 处理响应
if (request.result == UnityWebRequest.Result.Success)
{
Debug.Log("API request succeeded");
Debug.Log(request.downloadHandler.text);
}
else
{
Debug.Log("API request failed: " + request.error);
Debug.Log(request.downloadHandler.text);
}
}
private string GetSign(int timestamp, string nonceStr, string clientId)
{
// 拼接参数并进行 SHA1 加密
string signString = timestamp.ToString() + nonceStr + clientId;
byte[] signBytes = Encoding.UTF8.GetBytes(signString);
byte[] signHash = new SHA1CryptoServiceProvider().ComputeHash(signBytes);
string sign = BitConverter.ToString(signHash).Replace("-", "").ToLowerInvariant();
return sign;
}
}
返回示例:
{
"activity_id":"TDS20220928151122U9H",
"content":"[{\"name\": \"奖品\", \"number\": 2}]",
"content_obj":[
{
"name":"奖品名称",
"number":2
}
],
"nonce_str":"0DD4B",
"sign":"5895f87d3dfb5e0918c1b195c015d9284609d122",
"timestamp":1664354023,
"success":true,
"error":0,
"c_sign":"12c6fc06c99a462375eeb3f43dfd832b08ca9e17",
"custom":{
"seasons":[
"101",
"102"
]
}
}