# 行为任务上报(必接)
行为任务主要用于部分渠道导流场景,可引导新用户更深入地体验游戏内容。用户在游戏内完成开放平台配置的目标行为后,CP 需要将行为完成结果同步给美团游戏平台;如果是可多次上报行为任务,悬浮球会同步展示当前进度,例如 3/10,并在任务完成后展示已完成状态、触发奖励发放。
当前更推荐使用「平台代上报」方案。CP 只需要在行为完成时发送行为完成消息,美团游戏平台会完成行为数据上报,并实时更新可多次上报行为任务进度。
| 接入方案 | 适用对象 | CP 需要做什么 | 数据上报方 | 是否兼容原有服务端接口 |
|---|---|---|---|---|
| 方案一:平台代上报 | 新接入行为任务 | 在用户完成行为时,通过 wx.sendCustomMsg 发送 action_code_report 消息 | 美团游戏平台 | 推荐迁移到该方案,不需要 CP 再调用原服务端接口 |
| 方案二:CP 自行上报 | 已接入 /api/v3/action/submit 的存量 CP | CP 服务端调用原接口上报,同时通过 wx.sendCustomMsg 发送 action_code_report 消息 | CP 服务端 | 兼容原服务端上报方案 |
# 一、接入前准备
- 在美团游戏开放平台配置行为任务,并登记对应的
actionCode。 - 确认游戏内触发行为完成的时机,例如完成一局、完成一关、完成一次分享或观看一次广告。
- 如果是可多次上报行为任务,确认一次游戏行为对应的进度增量。大多数场景为
1,批量增加时可传大于1的正整数。
# 二、方案一:平台代上报(新接入 CP 使用)
适用于新接入行为任务的 CP,也适用于希望在悬浮球上实时展示可多次上报行为任务进度的存量 CP。CP 只需要在前端发送行为完成消息,美团游戏平台会完成行为数据上报,并实时更新用户可见的任务进度。
# 调用时机
每次用户完成一次目标行为后立即调用。
# 调用示例
wx.sendCustomMsg(`${appId}_game_to_plugin`, {
type: "action_code_report",
params: {
actionCode: "YOUR_ACTION_CODE",
reportMode: "platform",
actionCount: 1
}
});
# 参数说明
| 参数名 | 类型 | 是否必填 | 说明 |
|---|---|---|---|
| type | String | 是 | 固定传 "action_code_report",表示行为完成上报 |
| params.actionCode | String | 是 | 在开放平台配置的行为 code,必须与任务配置完全一致,区分大小写 |
| params.reportMode | String | 是 | 固定传 "platform",表示使用平台代上报 |
| params.actionCount | Number | 否 | 本次行为进度增量,默认为 1;必须为正整数 |
# 三、方案二:CP 自行上报(仅适用于2026年6月前已接入过行为任务的游戏)
适用于已经接入 /api/v3/action/submit 的存量 CP。该方案保留原有服务端接口能力,并通过前端消息通知美团游戏平台更新悬浮球进度。
# 接入流程
- 服务端上报:CP 服务端继续调用本文下方的「CP 服务端上报接口」,完成行为数据上报。
- 前端通知:CP 前端在同一次行为完成后,通过
wx.sendCustomMsg发送action_code_report消息,用于更新悬浮球任务进度。 - 进度保持一致:如果是可多次上报行为任务,前端消息中的
actionCount应与服务端上报的actionTimes保持一致。
# 前端消息
wx.sendCustomMsg(`${appId}_game_to_plugin`, {
type: "action_code_report",
params: {
actionCode: "YOUR_ACTION_CODE",
reportMode: "cp",
actionCount: 1
}
});
reportMode 可以不传,也可以传 "cp"。不要传 "platform",否则会按平台代上报方案处理。
# 接入注意事项
actionCode必须与开放平台任务配置一致;不一致时悬浮球不会更新该任务进度。- CP 自行上报方案包含服务端上报和前端通知两个步骤:服务端接口用于提交行为数据,前端消息用于更新用户可见的任务进度。
- 同一游戏可以同时存在多个行为任务,不同行为按各自的
actionCode上报即可。 actionCount与actionTimes都表示本次行为带来的进度增量。前端消息使用actionCount,服务端接口使用actionTimes。- CP 自行上报方案中,前端消息的
reportMode可以不传,也可以传"cp";不要传"platform"。
# CP 服务端上报接口
本接口仅适用于「CP 自行上报」方案。使用「平台代上报」方案时,CP 无需对接本接口。
该接口兼容原有单次行为上报;未传 actionTimes 时,平台按 1 次处理。可多次上报行为任务如需一次增加多个进度,可在 bizContent 中传入 actionTimes。
# 请求信息
- URL: https://mgc.meituan.com/mgc/gateway/api/v3/action/submit
- Method: POST
- Content-Type: application/json
# 请求入参
| 参数名 | 类型 | 默认值 | 是否必填 | 说明 |
|---|---|---|---|---|
| clientId | String | 是 | 应用ID 可从开放平台查看 | |
| ts | long | 是 | 发送请求的时间戳 1970-01-01 00:00:00.000到当前时刻的毫秒数 | |
| sign | String | 是 | 参数签名,见「加密与签名说明」 | |
| nonce | String | 是 | 随机字符串,每次请求需变更 | |
| signType | String | 是 | 签名算法 固定为: SHA1 | |
| encryptType | String | 是 | 加密算法 固定为: AES-256-GCM | |
| bizContent | String | 是 | 业务参数-见下表;需要加密,见「加密与签名说明」 |
# bizContent 内容
| 参数名 | 类型 | 默认值 | 是否必填 | 说明 |
|---|---|---|---|---|
| mgcId | String | 是 | 美团侧玩家角色号 | |
| actionCode | String | 是 | 在游戏开平中成功注册的行为code | |
| actionFinishTime | String | 是 | 行为完成时间,格式:yyyy-MM-dd HH:mm:ss;timezone = "GMT+8" | |
| actionTimes | int | 1 | 否 | 本次行为进度增量,必须为正整数;不传时按 1 次处理,兼容原有单次行为上报 |
| innerSource | String | 否 | 渠道入口参数;暂不区分渠道时可不传或传空字符串 | |
| gameSn | String | 是 | 数据的全局唯一标识。用于幂等 |
# 响应参数
| 参数名 | 类型 | 默认值 | 是否必填 | 说明 |
|---|---|---|---|---|
| code | int | 是 | 异常码 | |
| msg | String | 是 | 异常描述 |
{
"code": 0,
"msg": "ok"
}
code !=0 时为失败,详见异常码
如遇到接入问题,请优先查看文档下方"常见接入问题"
# 异常码
| 异常码 | 异常说明 |
|---|---|
| 710002 | 签名值无效 |
| 710005 | ts格式不正确 |
| 710006 | ts时间不正确 |
| 710008 | mgcId参数错误 |
| 710012 | clientId参数错误 |
| 710013 | nonce随机字符串重复 |
| 710018 | 当前IP无权访问 |
# 加密与签名说明
# 获取密钥 key
AES 加密使用的密钥由 appId 和 appSecret 生成。开发者可以在开发者后台查询开放平台分配的 appId 和 appSecret。
String secretKey = AESUtil.createKey(appId + "&" + appSecret);
# AESUtil 工具类
import java.security.MessageDigest;
import javax.crypto.SecretKey;
public static String createKey(String password) {
try {
byte[] keyBytes = password.getBytes(StandardCharsets.UTF_8);
MessageDigest sha = MessageDigest.getInstance("SHA-1");
keyBytes = sha.digest(keyBytes);
keyBytes = Arrays.copyOf(keyBytes, 32);
SecretKey secretKey = new SecretKeySpec(keyBytes, "AES");
return Base64.getEncoder().encodeToString(secretKey.getEncoded());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
# bizContent 加密
bizContent 内容使用 AES_256_GCM 算法加密。
AES_256_GCM的加密java示例代码,仅供参考
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
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 AES_KEY_LENGTH_BIT = 256;
public static final int GCM_NONCE_LENGTH_BIT = 128;
public static final int GCM_TAG_LENGTH_BIT = 128;
public static String encryptWithAESGCM256(String key, byte[] content) {
try {
byte[] keyBytes = Base64.getDecoder().decode(key.getBytes(StandardCharsets.UTF_8));
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
Cipher cipher = Cipher.getInstance(ALGORITHM_PADDING, "SunJCE");
SecureRandom random = SecureRandom.getInstance("NativePRNGNonBlocking");
final byte[] nonceBytes = new byte[GCM_NONCE_LENGTH_BIT / 8];
random.nextBytes(nonceBytes);
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH_BIT, nonceBytes);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec);
byte[] contentBytes = cipher.doFinal(content);
byte[] finalBytes = new byte[nonceBytes.length + contentBytes.length];
System.arraycopy(nonceBytes, 0, finalBytes, 0, nonceBytes.length);
System.arraycopy(contentBytes, 0, finalBytes, nonceBytes.length, contentBytes.length);
String result = new String(Base64.getUrlEncoder().encode(finalBytes), StandardCharsets.UTF_8);
return result;
} catch (Exception e) {
logger.error("encrypt error", e);
}
return null;
}
# Sign 签名计算
String data = "" //bizContent参数按照参数名字典排序,以&符连接,例如:a=1&b=2&c=3
data += String.format("&uri=%s", URLEncoder.encode("/api/v3/action/submit")); // path随着访问的url改变
data += "&method=POST";
data += String.format("&secret=%s", appSecret); //开发者可以在开发者后台查询得到开放平台为其分配的appId和appSecret
// 以上生成data数据的顺序不可改变
String secretKey = AESUtil.createKey(appId + "&" + appSecret);
String signature = SHAUtil.encryptSHA1Str(data + secretKey);
# SHAUtil 工具类
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import org.apache.commons.codec.digest.DigestUtils;
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 e) {
throw new IllegalArgumentException(e);
}
byte[] infoBytes = text.getBytes(StandardCharsets.UTF_8);
md.update(infoBytes);
return md.digest();
}
private static String byte2hex(byte[] bytes) {
StringBuilder stringBuilder = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(b & 0xFF);
if (hex.length() == 1) {
stringBuilder.append("0");
}
stringBuilder.append(hex);
}
return stringBuilder.toString();
}
# Python 数据加密及签名发送示例
import random
import uuid
from Crypto.Cipher import AES
import base64
import hashlib
import os
import time
import datetime
import pytz
from urllib.parse import quote
import json
class MtServerUtlis(object):
def get_pay_config(self):
pay_config = dict()
pay_config["appId"] = ""
pay_config["appSecret"] = ""
return pay_config
def nonce_str(self):
random_uuid = uuid.uuid4()
random_string = str(random_uuid)
return random_string
def generate_out_biz_no(self, type=1):
timestamp = int(time.time() * 1000) # 获取当前时间戳,精确到毫秒
random_num = random.randint(1000, 9999) # 生成一个四位随机数
if type == 1:
out_biz_no = f"{timestamp}_{random_num}" # 组合时间戳和随机数
else:
out_biz_no = f"{timestamp}{random_num}" # 组合时间戳和随机数
return out_biz_no
def calculate_signature(self, secret):
sha1 = hashlib.sha1()
sha1.update(secret.encode("utf-8"))
key_bytes = sha1.digest()
# 截断或填充结果到 32 字节
key_bytes = key_bytes.ljust(32, b"\0")
# 返回 Base64 编码的密钥字符串
return base64.b64encode(key_bytes).decode("utf-8")
def encrypt_with_aes_gcm_256(self, key, content):
try:
key_bytes = base64.urlsafe_b64decode(key.encode("utf-8"))
nonce_bytes = os.urandom(16) # 128 bits
cipher = AES.new(key_bytes, AES.MODE_GCM, nonce=nonce_bytes)
ciphertext, tag = cipher.encrypt_and_digest(content)
final_bytes = nonce_bytes + ciphertext + tag
result = base64.urlsafe_b64encode(final_bytes).decode("utf-8")
return result
except Exception as e:
print(f"Encrypt error: {e}")
return None
def encrypt_sha1_str(self, text):
sha1_hash = hashlib.sha1(text.encode("utf-8")).hexdigest()
return sha1_hash
Mt = MtServerUtlis()
client_id = ""
timestamp = int(time.time() * 1000)
gameSn = Mt.generate_out_biz_no()
get_config = Mt.get_pay_config()
# 设置时区
timezone = pytz.timezone("Asia/Shanghai")
now = datetime.datetime.now(timezone)
formatted_time = now.strftime("%Y-%m-%d %H:%M:%S")
httpMethod = "POST"
httpReuqestUrl = "/api/v3/action/submit"
httpRequestBody = dict()
httpRequestBody["clientId"] = ""
httpRequestBody["ts"] = timestamp
httpRequestBody["nonce"] = Mt.nonce_str()
httpRequestBody["signType"] = "SHA1"
httpRequestBody["encryptType"] = "AES-256-GCM"
bizContent = dict()
bizContent["mgcId"] = ""
bizContent["actionCode"] = ""
bizContent["actionFinishTime"] = formatted_time
bizContent["actionTimes"] = 1 # 可选,不传时默认为 1
bizContent["gameSn"] = gameSn
data_str = "&".join([f"{k}={quote(bizContent[k])}" for k in sorted(bizContent.keys())])
data_str += f"&uri={quote(httpReuqestUrl, safe='')}"
data_str += f"&method={httpMethod}"
data_str += f"&secret={get_config['appSecret']}"
secret_key = Mt.calculate_signature(f"{client_id}&{get_config['appSecret']}")
httpRequestBody["bizContent"] = Mt.encrypt_with_aes_gcm_256(
secret_key, json.dumps(bizContent).encode("utf-8")
)
str_to_sign = data_str + secret_key
sign = hashlib.sha1(str_to_sign.encode("utf-8")).hexdigest()
httpRequestBody["sign"] = sign
# PHP 数据加密及签名发送示例
<?php
class Demo {
private $appId;
private $appSecret;
private $secretKey;
public function __construct($appId,$appSecret) {
$this->appId = $appId;
$this->appSecret = $appSecret;
$this->secretKey = $this->createKey($appId, $appSecret);
}
private function createKey($appId, $appSecret) {
return base64_encode(str_pad(sha1($appId.'&'.$appSecret,true), 32,pack('V', 0)));
}
public function encryptWithAESGCM256($data) {
try {
$secretKey = base64_decode($this->secretKey);
$iv = openssl_random_pseudo_bytes(16);
$tag = null;
$ciphertext = openssl_encrypt($data, 'aes-256-gcm', $secretKey, OPENSSL_RAW_DATA, $iv, $tag);
if ($ciphertext === false) throw new Exception("Encryption failed");
return $this->urlsafe_b64encode($iv . $ciphertext . $tag);
} catch (Exception $e) {
echo "Encryption error: " . $e->getMessage();
return null;
}
}
public function decryptWithAESGCM256($encryptedData) {
try {
$secretKey = base64_decode($this->secretKey);
$decodedData = $this->urlsafe_b64decode($encryptedData);
$iv = substr($decodedData, 0, 16);
$ciphertext = substr($decodedData, 16, -16);
$tag = substr($decodedData, -16);
$decryptedData = openssl_decrypt($ciphertext, 'aes-256-gcm', $secretKey, OPENSSL_RAW_DATA, $iv, $tag);
if ($decryptedData === false) throw new Exception("Decryption failed");
return $decryptedData;
} catch (Exception $e) {
echo "Decryption error: " . $e->getMessage();
return null;
}
}
private function urlsafe_b64encode($string) {
$data = base64_encode($string);
$data = str_replace(array('+','/'),array('-','_'),$data);
return $data;
}
private function urlsafe_b64decode($string) {
$data = str_replace(array('-','_'),array('+','/'),$string);
$mod4 = strlen($data) % 4;
if ($mod4) {
$data .= substr('====', $mod4);
}
return base64_decode($data);
}
public function createSign($bizContent){
$bizContent = json_decode($bizContent,true);
ksort($bizContent);
$str = http_build_query($bizContent);
$str = str_ireplace("+", "%20", $str);
$str .= "&uri=" . urlencode("/api/v3/action/submit");
$str .= "&method=POST";
$str .= "&secret=" . $this->appSecret;
$secretKey = $this->createKey($this->appId, $this->appSecret);
$sign = $this->encryptSHA1Str($secretKey, $str);
return $sign;
}
private function encryptSHA1Str($secretKey, $encryptData) {
$aaa = $encryptData.$secretKey;
$sign = sha1($aaa);
return $sign;
}
public function incrementgameaction($sign,$encryptedData) {
$url = 'https://mgc.meituan.com/mgc/gateway/api/v3/action/submit';
$data = [
'clientId' => $this->appId,
'ts' => time() * 1000,
'nonce' => $this->getrandstr(8) . time(),
'signType' => 'SHA1',
'encryptType' => 'AES-256-GCM',
'sign' => $sign,
'bizContent' => $encryptedData,
];
return $this->send($url, json_encode($data), $requestType = 1, $timeout = 10);
}
private function send($url, $params, $requestType = 1, $timeout = 10, $header = array()) {
$ch = curl_init();
if(empty($header)){
$header = array(
'Content-Type: application/json; charset=UTF-8',
);
}
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
if($requestType==1){
curl_setopt($ch, CURLOPT_POST, true);
}
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
//curl_setopt($ch, CURLOPT_SSLVERSION, 2);//设置SSL协议版本号 不检验https
$returnTransfer = curl_exec($ch);
curl_close($ch);
return $returnTransfer;
}
private function getrandstr($length){
$str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890';
$randStr = str_shuffle($str);//打乱字符串
$rands= substr($randStr,0,$length);//substr(string,start,length);返回字符串的一部分
return $rands;
}
}
//美团开放平台获取参数appId,appSecret
$appId = '';
$appSecret = '';
$demo = new Demo($appId,$appSecret);
//bizContent数据,参数说明:
//actionCode: 请从美团运营小伙伴处申请,该值仅供测试参考,请替换
//actionTimes:可选,本次行为进度增量;不传时默认为 1
//innerSource:根据自身平台业务决定是否传值,如无用到该参数,可不传
$bizContent = '{"mgcId":"1234567890","actionCode":"","actionFinishTime":"2024-11-01 23:59:59","actionTimes":1,"gameSn":"xxxxxxxxx","innerSource":"xxxxxx"}';
//加密bizContent
$encryptedData = $demo->encryptWithAESGCM256($bizContent);
echo "Encrypted data: " . $encryptedData . "\n\n";
//解密bizContent
$decryptedData = $demo->decryptWithAESGCM256($encryptedData);
echo "Decrypted data: " . $decryptedData . "\n\n";
//数据签名
$sign = $demo->createSign($bizContent);
echo "Request sign: " . $sign . "\n\n";
//游戏增量行为上报
$response = $demo->incrementgameaction($sign,$encryptedData);
echo "Request response: " . $response . "\n\n";
# 四、接入常见问题
- Q:提示IP无权限怎么办?
- A:需要到开平填写ip白名单:
这里为服务器ip白名单,填写域名无效;如果服务器有多个出口ip,可以添加多个ip白名单,也可用子网掩码方式
- Q:签名错误
- A:遇到签名问题,请仔细查看接入代码,可能出现的问题:
a)日期内容url encode时,有些encode库会将空格专码为"+"导致错误,应转换为"%20";
b)使用SHA加密的长度可能会被截断,注意是32位;
c)代码类库与代码版本不匹配;
d)空值的参数名不参与签名;(比如非必填参数 innerSource) - 可用开平中的工具进行验证:
密文代表bizContent加密后内容,该部分可以解密使用;
密文签名代表sign加密后内容,可以进行对照
这里为服务器ip白名单,填写域名无效;如果服务器有多个出口ip,可以添加多个ip白名单,也可用子网掩码方式
密文代表bizContent加密后内容,该部分可以解密使用;
密文签名代表sign加密后内容,可以进行对照