# 游戏道具发放通知

  • 玩家在游戏中心等成功领取礼包后,美团开放平台会回调游戏开发者服务端的回调接口(开发者提供的回调接口的端口号,必须是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" }

# 附录

# 验签与解密流程

  1. 根据 appId 和 appSecret(开发者后台获取),计算 secretKey = AESUtil.createKey(appId + "&" + appSecret)
  2. 验证签名:SHAUtil.encryptSHA1Str(data + secretKey) 的结果与请求中的 sign 一致则通过
  3. 签名验证通过后,调用 decryptWithAESGCM256(secretKey, data) 解密得到明文 JSON
  4. 解析 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,道具不重复发放

测试入口:在开放平台礼包管理列表点击「测试」,参见:礼包测试与验收

上次更新: 8/10/2026, 5:36:37 PM