# 游戏道具发放通知
- 玩家在游戏中心等成功领取礼包后,美团开放平台会回调游戏开发者服务端的回调接口(开发者提供的回调接口的端口号,必须是80或者443),完成道具发放通知。
- 本接口调用超时场景美团开放平台会进行重试,重试间隔采取递增的策略。接口正常返回场景不会重试,发货失败率持续偏高,平台可能对礼包进行停用处理。
- 回调说明
: 接口回调采取对称加密的方式,假设开发者在游戏开放平台注册的道具发放回调接口为http://url.callback;则回调时形式如:
method:POST header:application/json;charset=utf-8 url:http://url.callback postdata: { data:加密后的数据, sign:签名 } - 开发者的服务端在接收到回调时,可以使用sign的算法进行签名验证。如果匹配了,说明来自于美团游戏开放平台的接口回调是正确的,开发者可以进一步解密数据包,基于
orderId做幂等去重后执行道具发放业务逻辑。 - 已接入支付回调的游戏商,签名/解密逻辑完全复用,只需新增道具发货的消息类型判断逻辑即可。
# 道具发放通知接口
请求方式: POST
Content-Type: application/json;charset=utf-8
URL: 由游戏商提供的回调地址
# 外层请求参数
| 参数名 | 类型 | 是否必填 | 说明 |
|---|---|---|---|
| data | String | 是 | AES-256-GCM 加密后的数据,Base64 URL-safe 编码 |
| sign | String | 是 | SHA1 签名,hex 输出 |
# data 解密后字段
| 参数名 | 类型 | 长度 | 是否必填 | 说明 |
|---|---|---|---|---|
| orderId | String | 64 | 是 | 发货订单唯一id,用于幂等去重。同一 orderId 首次成功后,后续重复调用直接返回 code=0,不得重复发货 |
| testFlag | Boolean | - | 是 | true=测试请求,false=正式请求 |
| mgcId | String | 32 | 是 | 美团侧玩家角色号 |
| system | String | - | 是 | 用户终端:android / ios / harmonyOS /unknown |
| giftTypeId | String | - | 是 | 礼包类型(预留字段) |
| giftId | String | 32 | 是 | 礼包id(开放平台配置获取) |
| sendTime | String | - | 是 | 发货时间,格式:yyyy-MM-dd HH:mm:ss |
| goodsList | Array | - | 是 | 道具列表,见下表 |
goodsList 元素字段
| 参数名 | 类型 | 是否必填 | 说明 |
|---|---|---|---|
| id | String | 是 | 道具id,可以在开放平台“礼包创建”模块查看 |
| sendCount | String | 是 | 发货数量 |
# 请求示例(解密后)
{
"orderId": "202605291234567890",
"testFlag": false,
"mgcId": "100000001",
"system": "ios",
"giftTypeId": "1",
"giftId": "10001001",
"sendTime": "2026-05-29 10:00:00",
"goodsList": [
{ "id": "2001", "sendCount": "5" },
{ "id": "2002", "sendCount": "10" }
]
}
# 响应参数
| 参数名 | 类型 | 是否必填 | 说明 |
|---|---|---|---|
| code | int | 是 | 0=成功,非0=失败 |
| msg | String | 否 | 错误描述(调试用) |
| resultCode | String | 当code≠0时必填 | 业务结果码,见下表 |
resultCode 枚举
| 值 | 说明 | 游戏中心重试策略 |
|---|---|---|
| USER_UNREGISTERED | 用户未注册或未创建角色 | 不重试 |
| SEND_CONDITION_NOT_SATISFIED | 发放条件不满足 | 不重试 |
| OTHER | 其他错误 | 不重试 |
若出现大量非USER_UNREGISTERED,SEND_CONDITION_NOT_SATISFIED以外的结果码可能导致平台对礼包停用
# 成功响应示例
{ "code": 0, "msg": "ok" }
# 失败响应示例
{ "code": 1, "msg": "用户未创建角色", "resultCode": "USER_UNREGISTERED" }
# 附录
# 验签与解密流程
- 根据 appId 和 appSecret(开发者后台获取),计算
secretKey = AESUtil.createKey(appId + "&" + appSecret) - 验证签名:
SHAUtil.encryptSHA1Str(data + secretKey)的结果与请求中的sign一致则通过 - 签名验证通过后,调用
decryptWithAESGCM256(secretKey, data)解密得到明文 JSON - 解析 JSON,基于
orderId幂等去重,执行道具发放业务逻辑
# Sign 签名计算
String secretKey = AESUtil.createKey(appId + "&" + appSecret); //开发者可以在开发者后台查询得到开放平台为其分配的appId和appSecret
signature = SHAUtil.encryptSHA1Str(data + secretKey) 美团开发平台回调返回的报文中data字段对应的value
# AESUtil 工具类(Java)
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Base64;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
public static String createKey(String password) {
try {
byte[] key = password.getBytes("UTF-8");
MessageDigest sha = MessageDigest.getInstance("SHA-1");
key = sha.digest(key);
key = Arrays.copyOf(key, 32);
SecretKey secretKey = new SecretKeySpec(key, "AES");
return Base64.getEncoder().encodeToString(secretKey.getEncoded());
} catch (Exception var3) {
throw new RuntimeException(var3);
}
}
# SHAUtil 工具类(Java)
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public static String encryptSHA1Str(String text) {
byte[] bytes = encryptSHA(text, "SHA-1");
return byte2hex(bytes);
}
private static byte[] encryptSHA(String text, String algorithm) {
MessageDigest md;
try {
md = MessageDigest.getInstance(algorithm);
} catch (NoSuchAlgorithmException var4) {
throw new IllegalArgumentException(var4);
}
byte[] infoBytes = text.getBytes(StandardCharsets.UTF_8);
md.update(infoBytes);
return md.digest();
}
private static String byte2hex(byte[] bytes) {
StringBuilder stringBuilder = new StringBuilder();
byte[] var2 = bytes;
int var3 = bytes.length;
for(int var4 = 0; var4 < var3; ++var4) {
byte b = var2[var4];
String hex = Integer.toHexString(b & 255);
if (hex.length() == 1) {
stringBuilder.append("0");
}
stringBuilder.append(hex);
}
return stringBuilder.toString();
}
# AES-256-GCM 解密工具类(Java)
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
private static final String ALGORITHM = "AES";
private static final String ALGORITHM_PADDING = "AES/GCM/NoPadding";
private static final int GCM_NONCE_LENGTH_BIT = 128;
private static final int GCM_TAG_LENGTH_BIT = 128;
/**
* @param key 密钥(AESUtil.createKey(appId + "&" + appSecret) 的返回值)
* @param encryptData 密文数据(请求体中的 data 字段)
* @return 解密后的明文 JSON 字符串
*/
public static String decryptWithAESGCM256(String key, String encryptData) {
try {
byte[] decodeData = Base64.getUrlDecoder().decode(encryptData);
byte[] nonceBytes = Arrays.copyOfRange(decodeData, 0, GCM_NONCE_LENGTH_BIT / 8);
byte[] contentBytes = Arrays.copyOfRange(decodeData, GCM_NONCE_LENGTH_BIT / 8, decodeData.length);
byte[] keyBytes = Base64.getDecoder().decode(key.getBytes(StandardCharsets.UTF_8));
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH_BIT, nonceBytes);
Cipher cipher = Cipher.getInstance(ALGORITHM_PADDING, "SunJCE");
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);
return new String(cipher.doFinal(contentBytes), StandardCharsets.UTF_8);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
# PHP Demo(PHP 7.1+)
/**
* 生成密钥
* $appId:开发者后台获取
* $appSecret:开发者后台获取
*/
function createKey($appId, $appSecret) {
return base64_encode(str_pad(sha1($appId.'&'.$appSecret, true), 32, pack('V', 0)));
}
/**
* SHA1 签名
* $secretKey:createKey 返回值
* $encryptData:请求体中的 data 字段
*/
function encryptSHA1Str($secretKey, $encryptData) {
return sha1($encryptData . $secretKey);
}
/**
* AES-256-GCM 解密
* $secretKey:createKey 返回值
* $encryptData:请求体中的 data 字段
*/
function decryptWithAESGCM256($secretKey, $encryptData) {
$decodeEncrypt = urlsafe_b64decode($encryptData);
$decodeSecret = base64_decode($secretKey);
return openssl_decrypt(
substr($decodeEncrypt, 16, -16),
'aes-256-gcm',
$decodeSecret,
1,
substr($decodeEncrypt, 0, 16),
substr($decodeEncrypt, -16, 16)
);
}
function urlsafe_b64decode($string) {
$data = str_replace(['-', '_'], ['+', '/'], $string);
$mod4 = strlen($data) % 4;
if ($mod4) {
$data .= substr('====', $mod4);
}
return base64_decode($data);
}
# JS Demo(Node.js)
import crypto from 'crypto';
// 使用示例
const secretKey = getSecretKey(`${appId}&${appSecret}`);
const isValid = getEncryptSha1Str(data, secretKey) === sign; // 验签
const plainText = getDecryptRes(secretKey, data); // 解密
/**
* 生成密钥
* @param {string} password appId + "&" + appSecret
*/
export function getSecretKey(password) {
let key = crypto.createHash('sha1').update(password).digest();
key = Buffer.concat([Buffer.from(key, 0, 32), Buffer.alloc(32, 0)]).slice(0, 32);
return Buffer.from(key).toString('base64');
}
/**
* SHA1 签名
* @param {string} encrpyData 请求体中的 data 字段
* @param {string} secretKey getSecretKey 返回值
*/
export function getEncryptSha1Str(encrpyData, secretKey) {
return crypto.createHash('sha1').update(encrpyData + secretKey).digest('hex');
}
/**
* AES-256-GCM 解密
* @param {string} secretKey getSecretKey 返回值
* @param {string} encrpyData 请求体中的 data 字段
*/
export function getDecryptRes(secretKey, encrpyData) {
try {
const decodeSecretKey = Buffer.from(secretKey, 'base64');
const decodeEncrpyData = Buffer.from(encrpyData, 'base64');
const iv = decodeEncrpyData.slice(0, 16);
const contentData = decodeEncrpyData.slice(16, -16);
const authTag = decodeEncrpyData.slice(-16);
const decipher = crypto.createDecipheriv('aes-256-gcm', decodeSecretKey, iv);
decipher.setAuthTag(authTag);
return decipher.update(contentData, undefined, 'utf8') + decipher.final('utf8');
} catch (e) {
console.log('decrypt error', e);
}
}
# 验收 Checklist
接口开发完成后,上线前须通过以下 3 个场景的验收:
| 验收场景 | 操作说明 | 预期结果 |
|---|---|---|
| 正常发放成功 | 使用已创建游戏角色的账号触发发货 | 返回 code=0,角色收到道具 |
| 未注册用户 | 使用未创建游戏角色的账号触发发货 | 返回 code≠0,resultCode=USER_UNREGISTERED |
| 幂等验证 | 对同一账号触发两次发放(相同 orderId) | 两次均返回 code=0,道具不重复发放 |
测试入口:在开放平台礼包管理列表点击「测试」,参见:礼包测试与验收