# 商品发货消息协议
# 注意事项(重要!!!)
- 同样的发货请求(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×tamp=1765879307&nonce=pOAyNu8T&echostr=QOItc9KKmiSJ1snu
| 参数 | 描述 |
|---|---|
| signature | 签名,signature 结合了开发者填写的 Token 参数和请求中的 timestamp 参数、nonce 参数 |
| timestamp | 秒级时间戳(Unix Timestamp) |
| nonce | 随机数 |
| echostr | 随机字符串,需原样返回的参数内容 |
签名校验流程:
- 将 token、timestamp、nonce 三个参数进行字典序排序
- 将三个参数字符串拼接成一个字符串进行 sha1 加密
- 开发者获得加密后的字符串可与 signature 对比,确认该请求来源于 TapTap 服务器
示例代码:
private String sign(String token, String timestamp, String nonce) {
List<String> 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_signature、nonce、timestamp 用于安全模式下对消息内容的解密。
| 参数 | 是否必填 | 描述 |
|---|---|---|
| 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 | 前文描述密文消息体 |
# 消息体验证和解密
开发者先验证消息体签名的正确性,验证通过后,再对消息体进行解密。
验证方式:
- 开发者计算签名:
dev_msg_signature = sha1(sort(Token、timestamp、nonce, encrypt)) - 比较
dev_msg_signature和 URL 上带的msg_signature是否相等,相等则表示验证通过。
解密方式:
TmpMsg = Base64_Decode(encrypt)FullStr = AES_Decrypt(TmpMsg, AESKey);FullStr 如前所述由random(16B) + body_len(4B) + msg + to_user_name组成- 验证尾部的
to_user_name是否正确(可选) - 去掉 FullStr 头部 16 字节的
random(16B)、4 字节的body_len(4B)、和尾部的to_user_name,即得到明文内容
