# 商品发货消息协议

# 注意事项(重要!!!)

  • 同样的发货请求(outTradeNo),可能因为网络原因,会请求多次。我们在有限时间内尽量保证触达一次,直到明确返回发货成功为止
  • 针对重复的请求,开发者需要自行保证只发货一次,并且回包需要和第一次一样返回发货成功
  • 通知周期:15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h
  • 必须要开启商品发货推送才能收到回调请求

# 基础配置

  • Client ID:小游戏 Client ID
  • 沙箱支付 key:沙箱环境下的服务端调用凭证
  • 线上支付 key:线上环境下的服务端调用凭证

# 发货推送配置

# 第一步:接入推送能力

小游戏目前提供的消息推送能力,在开发者后台「商店」-「游戏包体管理」-「小游戏管理」-「开放能力」-「小游戏内购」-「基础配置」下:

  • 发货推送配置:用于接收小游戏商品/商城相关发货事件通知

# 第二步:填写服务器配置

点击"推送配置"填写以下信息:

配置项 说明
URL 服务器地址。开发者用来接收发货消息事件的接口 URL。必须以 http://https:// 开头,分别支持 80 端口和 443 端口。注意:如果 URL 请求时间超过 3s,会发生超时错误
Token 令牌。由开发者填写,32 位字符。用作生成签名(该 Token 会和接口 URL 中包含的 Token 进行比对,从而验证安全性)
EncodingAESKey 消息加密密钥。由开发者填写,由 43 位字符组成,字符规范为 A-Z,a-z,0-9。将用作消息体加解密密钥

消息加解密方式

  • 明文模式:消息以明文形式传输
  • 安全模式(推荐):消息经过加密传输,开发者需要对收到的消息进行解密,但回复 TapTap 服务器的消息不需要加密

注意:消息数据格式仅支持 JSON 格式。

# 第三步:验证消息来自 TapTap 服务器

点击"模拟推送",TapTap 服务器会发送 GET 请求到填写的服务器地址 URL 上,GET 请求携带参数如下:

请求示例

URL?signature=c18a3c8a069e3483fda9e5f044a99e312d089151&timestamp=1765879307&nonce=pOAyNu8T&echostr=QOItc9KKmiSJ1snu
参数 描述
signature 签名,signature 结合了开发者填写的 Token 参数和请求中的 timestamp 参数、nonce 参数
timestamp 秒级时间戳(Unix Timestamp)
nonce 随机数
echostr 随机字符串,需原样返回的参数内容

签名校验流程

  1. 将 token、timestamp、nonce 三个参数进行字典序排序
  2. 将三个参数字符串拼接成一个字符串进行 sha1 加密
  3. 开发者获得加密后的字符串可与 signature 对比,确认该请求来源于 TapTap 服务器

示例代码

private String sign(String token, String timestamp, String nonce) {
     List&lt;String&gt; strings = new ArrayList<>();
     strings.add(token);
     strings.add(timestamp);
     strings.add(nonce);

     Collections.sort(strings);

     String toSign = String.join(StringUtils.EMPTY, strings);

     return DigestUtils.sha1Hex(toSign);
}

若确认此次 GET 请求来自 TapTap 服务器,请原样返回 echostr 参数内容,则接入生效,页面的"测试状态"会显示"已通过",否则接入失败。

# 消息推送协议

# 请求参数

字段 类型 说明
ToUserName String 小游戏 Client ID
FromUserName String 固定值:TapTap
CreateTime Long 消息发送时间
MsgType String 消息类型,固定值:event
Event String 事件类型,固定值:minigame_game_pay_goods_deliver_notify
MiniGame Object 商品发货参数,见下文

# MiniGame

字段 类型 说明
Payload String 携带的具体内容,格式为 JSON,具体内容见下表 Payload(因为这里需要对消息内容统一签名,所以统一把消息内容设计成 JSON 格式)
PayEventSig String to_hex(hmac_sha256(Key, Event + '&' + Payload)。这里的 Key 是基础配置里的线上支付 key 或者沙箱支付 key
IsMock Bool False: 真实推送

# Payload(JSON)

字段 类型 说明
OpenId String 接收商品的玩家 openid
Env Int 环境配置:
- 0: 正式环境
- 1: 沙箱环境
OutTradeNo String 业务订单号,需保证唯一性
GoodsInfo Object 发货商品,见下文

# GoodsInfo

字段 类型 说明
ProductId String 游戏发货商品 ID 标识
Quantity Int 购买商品数量
OrigPrice Int 物品原始价格(单位:分)
ActualPrice Int 物品实际支付价格(单位:分)
Attach String 透传数据
OrderSource Int 订单来源,固定值1

# 请求示例

{
  "ToUserName": "$to_user_name",
  "FromUserName": "TapTap",
  "CreateTime": 1765728000,
  "MsgType": "event",
  "Event": "minigame_game_pay_goods_deliver_notify",
  "MiniGame": {
    "Payload": "{\"OpenId\":\"FMcQIiefcDfN45pDluNvpw==\",\"Env\":0,\"OutTradeNo\":\"order_123456\",\"GoodsInfo\":{\"ProductId\":\"diamond_100\",\"Quantity\":1,\"OrigPrice\":600,\"ActualPrice\":600,\"Attach\":\"extra_data\",\"OrderSource\":1}}",
    "PayEventSig": "$pay_event_sig",
    "IsMock": false
  }
}

$to_user_name 小游戏 Client ID
$pay_event_sig 支付请求签名

# 返回参数

字段 类型 必填 说明
ErrCode Int 发送状态。0:成功,其他:失败
ErrMsg String 错误原因,用于调试。在 ErrCode 非 0 的情况下可以返回

# 返回示例

注意:对于推送消息,不管是否加密,开发者回复明文消息即可。

成功返回

{
  "ErrCode": 0,
  "ErrMsg": "Success"
}

失败返回

{
  "ErrCode": -1,
  "ErrMsg": "发货失败,库存不足"
}

# PayEventSig

示例代码

String Event = "minigame_game_pay_goods_deliver_notify";
String Payload = "{}";
Mac mac = org.apache.commons.codec.digest.HmacUtils.getInitializedMac(HmacAlgorithms.HMAC_SHA_256, Key.getBytes(StandardCharsets.UTF_8));
byte[] sha256 = mac.doFinal((Event + "&" + Payload).getBytes(StandardCharsets.UTF_8));
String PayEventSig = Hex.encodeHexString(sha256);

# 消息推送

# URL 参数

TapTap 服务器发往开发者服务器的请求会附带如下的 URL 参数,其中 signature 用于验证消息来自 TapTap 服务器,msg_signaturenoncetimestamp 用于安全模式下对消息内容的解密。

参数 是否必填 描述
signature 签名,signature 结合了开发者填写的 Token 参数和请求中的 timestamp 参数、nonce 参数
timestamp 秒级时间戳(Unix Timestamp)
nonce 随机数
msg_signature 安全模式会有此参数,消息体签名串,用于安全模式的加解密
encrypt_type 安全模式会有此参数,固定值: aes

# 消息解密

如果选择了安全模式,推送的消息格式如下:

# 请求示例

{
  "Encrypt": "$encrypt",
  "ToUserName": "$to_user_name"
}

$encrypt 是加密后的密文,开发者将其解密后即可得到原始消息。
$to_user_name 小游戏 Client ID

# 消息加密解密

在接收消息事件的 URL 中,选择安全模式时,会增加 2 个参数(此前已有 2 个参数,为秒级时间戳(Unix Timestamp) timestamp,随机数 nonce),分别是:

  • encrypt_type(加密类型,固定值:aes
  • msg_signature(消息体签名,用于验证消息体的正确性)

# 加密解密技术方案

消息加密解密技术方案基于 AES 加解密算法来实现,具体如下:

# Token(令牌)

令牌。由开发者填写,32 位字符。用作生成签名(该 Token 会和接口 URL 中包含的 Token 进行比对,从而验证安全性)。见:服务器配置,第二步:填写服务器配置

# EncodingAESKey(消息加密密钥)

即消息加解密 Key,长度固定为 43 个字符,从 a-z、A-Z、0-9 共 62 个字符中选取。见:服务器配置,第二步:填写服务器配置

# AESKey

EncodingAESKey 尾部填充一个字符的 "=",用 Base64_Decode 生成 32 个字节的 AESKey。

AESKey = Base64_Decode(EncodingAESKey + "=")

# AES 加密

  • AES 采用 CBC 模式
  • 秘钥长度为 32 个字节(256 位)
  • 数据采用 PKCS#5 或 PKCS#7 填充

# 接收消息

# 消息体加密

现有消息为明文,格式如下

{
   "ToUserName": "$to_user_name",
   "FromUserName": "TapTap",
   "CreateTime": 1765728000,
   "MsgType": "event",
   "Event": "minigame_game_pay_goods_deliver_notify",
   "MiniGame": {
      "Payload": "{\"OpenId\":\"FMcQIiefcDfN45pDluNvpw==\",\"Env\":0,\"OutTradeNo\":\"order_123456\",\"GoodsInfo\":{\"ProductId\":\"diamond_100\",\"Quantity\":1,\"OrigPrice\":600,\"ActualPrice\":600,\"Attach\":\"extra_data\",\"OrderSource\":1}}",
      "PayEventSig": "$pay_event_sig",
      "IsMock": false
   }
}

加密后,消息格式如下

{
  "ToUserName": "$to_user_name",
  "Encrypt": "$encrypt"
}

其中,$encrypt 是由 TapTap 对消息做了如下加密处理后的结果:

AESKey = Base64_Decode(EncodingAESKey + "=");
FullStr = random(16B) + body_len(4B) + body + to_user_name;
encrypt = Base64_Encode(AES_Encrypt(FullStr, AESKey));

FullStr 中:

  • random(16B) 为 16 字节的随机字符串
  • body_len 为 body 长度,占 4 个字节(网络字节序)
  • body 消息体内容
  • to_user_name 为小游戏 Client ID

# 消息体签名

为了验证消息体的合法性,TapTap 新增了消息体签名,开发者可用以验证消息体的真实性,并对验证通过的消息体进行解密。 在推送消息时,将会在消息接收 URL 上增加参数:msg_signature

msg_signature = sha1(sort(Token、timestamp、nonce, encrypt))
参数 描述
Token 令牌
timestamp URL 上原有参数,秒级时间戳(Unix Timestamp)
nonce URL 上原有参数,随机数
encrypt 前文描述密文消息体

# 消息体验证和解密

开发者先验证消息体签名的正确性,验证通过后,再对消息体进行解密。

验证方式

  1. 开发者计算签名:dev_msg_signature = sha1(sort(Token、timestamp、nonce, encrypt))
  2. 比较 dev_msg_signature 和 URL 上带的 msg_signature 是否相等,相等则表示验证通过。

解密方式

  1. TmpMsg = Base64_Decode(encrypt)
  2. FullStr = AES_Decrypt(TmpMsg, AESKey);FullStr 如前所述由 random(16B) + body_len(4B) + msg + to_user_name 组成
  3. 验证尾部的 to_user_name 是否正确(可选)
  4. 去掉 FullStr 头部 16 字节的 random(16B)、4 字节的 body_len(4B)、和尾部的 to_user_name,即得到明文内容