跳到主要内容
版本:v4

预约手机号授权

预约手机号授权用于在用户授权或取消授权预约手机号时,将事件异步推送到游戏服务端。游戏服务端完成验签后,可以保存授权关系,并在授权事件中解密得到用户手机号。

请先阅读接口公共说明了解接口签名与验签规则。

配置回调地址

TapTap 开发者中心 > 游戏服务 > 运营工具 > 预约手机号授权 中配置回调地址并开启服务。

回调接口

该接口用于接收用户预约手机号授权状态变更事件。TapTap 在用户授权或取消授权预约手机号时,向配置的回调地址发起 POST 请求。

注意
  • URLhttps://xxx/reserve/callback (仅供参考,开发者在后台配置的回调地址)
  • MethodPOST
  • Content-Typeapplication/json; charset=utf-8
  • 需要验签:是

请求参数

注意

公共参数见 接口公共说明 文档

Body

字段类型说明
event_idstring事件唯一 ID。建议作为幂等键,避免重复处理同一事件。
event_typestring事件类型。authorize 表示用户授权,cancel 表示用户取消授权,test 表示测试推送。
client_idstringTapTap 开放平台 Client ID。
openidstring用户在当前 Client ID 下的 OpenID。
unionidstring用户在当前开发者账号下的 UnionID。
reserve_typestring预约类型。可选值:androidpc
encrypted_phonestring加密后的手机号,仅在 authorize 事件中返回。
timenumber事件发生时间,秒级时间戳。

请求参数示例

用户授权

用户授权后,回调请求体中会包含 encrypted_phone。游戏服务端验签通过后,可以使用开放平台 Server Secret 解密手机号。

{
"event_id": "018fd2aa-7b8c-7b21-9c83-2f36f53fb350",
"event_type": "authorize",
"client_id": "tap-client-id",
"openid": "openid-for-this-client",
"unionid": "unionid-for-this-client",
"reserve_type": "android",
"encrypted_phone": "base64url(nonce || ciphertext || tag)",
"time": 1770000000
}

用户取消授权

用户取消授权后,回调请求体不包含 encrypted_phone。游戏服务端可以根据 openidunionidreserve_type 更新授权状态。

{
"event_id": "018fd2aa-7b8c-7b21-9c83-2f36f53fb351",
"event_type": "cancel",
"client_id": "tap-client-id",
"openid": "openid-for-this-client",
"unionid": "unionid-for-this-client",
"reserve_type": "android",
"time": 1770000100
}

成功响应

注意
  • 状态码:200 状态码
  • 响应示例:无固定响应体要求

错误响应

接口返回非 200 状态码时,TapTap 会视为本次推送未成功,然后重试。重试间隔依次为 60 秒、5 分钟、30 分钟、2 小时、6 小时,之后每 24 小时重试一次,最多重试 8 次。

处理建议

  • 验签通过后再解析和处理请求体。
  • 使用 event_id 做幂等处理。
  • event_typeauthorize 时保存授权关系和手机号,cancel 时清理或标记取消授权。
  • 服务端处理成功后返回 200 状态码。
  • event_typetest 时,请只用于联调验证,不要写入正式业务。

手机号解密算法

算法说明

encrypted_phone 使用不带 padding 的 Base64url 编码(Base64url without padding)。

编码前的原始字节由 nonceciphertexttag 依次拼接:

encrypted_phone = base64url_without_padding(nonce || ciphertext || tag)

服务端解密时,先对 encrypted_phone 做 Base64url without padding 解码,再按以下规则拆分原始字节:

  • nonce:前 12 字节
  • ciphertext:中间部分
  • tag:最后 16 字节

使用 AES-256-GCM 解密,密钥为 TapTap 开放平台发放的 Server Secret 原始字符串的 UTF-8 字节。请勿对 Server Secret 做额外转换。

phone = AES-256-GCM-Decrypt(
key = UTF8(Server Secret),
nonce = nonce,
ciphertext = ciphertext,
tag = tag,
aad = empty
)

示例代码

Node.js 解密示例
const crypto = require("crypto");

function decodeBase64UrlNoPadding(value) {
if (!/^[A-Za-z0-9_-]+$/.test(value) || value.length % 4 === 1) {
throw new Error("invalid encrypted_phone");
}
return Buffer.from(value, "base64url");
}

function decryptReservePhone(encryptedPhone, serverSecret) {
const data = decodeBase64UrlNoPadding(encryptedPhone);
if (data.length <= 28) {
throw new Error("invalid encrypted_phone");
}

const nonce = data.subarray(0, 12);
const ciphertext = data.subarray(12, data.length - 16);
const tag = data.subarray(data.length - 16);
const key = Buffer.from(serverSecret, "utf8");

if (key.length !== 32) {
throw new Error("Server Secret must be 32 bytes");
}

const decipher = crypto.createDecipheriv("aes-256-gcm", key, nonce);
decipher.setAuthTag(tag);
return Buffer.concat([
decipher.update(ciphertext),
decipher.final(),
]).toString("utf8");
}

Go 解密示例
package callback

import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"errors"
)

func DecryptReservePhone(encryptedPhone, serverSecret string) (string, error) {
data, err := base64.RawURLEncoding.DecodeString(encryptedPhone)
if err != nil {
return "", err
}
if len(data) <= 12+16 {
return "", errors.New("invalid encrypted_phone")
}

nonce := data[:12]
ciphertext := data[12 : len(data)-16]
tag := data[len(data)-16:]

key := []byte(serverSecret)
if len(key) != 32 {
return "", errors.New("Server Secret must be 32 bytes")
}

block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCMWithNonceSize(block, 12)
if err != nil {
return "", err
}
ciphertextAndTag := make([]byte, 0, len(ciphertext)+len(tag))
ciphertextAndTag = append(ciphertextAndTag, ciphertext...)
ciphertextAndTag = append(ciphertextAndTag, tag...)
plaintext, err := gcm.Open(nil, nonce, ciphertextAndTag, nil)
if err != nil {
return "", err
}
return string(plaintext), nil
}