# 行为任务上报(必接)

行为任务主要用于部分渠道导流场景,可引导新用户更深入地体验游戏内容。用户在游戏内完成开放平台配置的目标行为后,CP 需要将行为完成结果同步给美团游戏平台;如果是可多次上报行为任务,悬浮球会同步展示当前进度,例如 3/10,并在任务完成后展示已完成状态、触发奖励发放。

当前更推荐使用「平台代上报」方案。CP 只需要在行为完成时发送行为完成消息,美团游戏平台会完成行为数据上报,并实时更新可多次上报行为任务进度。

接入方案 适用对象 CP 需要做什么 数据上报方 是否兼容原有服务端接口
方案一:平台代上报 新接入行为任务 在用户完成行为时,通过 wx.sendCustomMsg 发送 action_code_report 消息 美团游戏平台 推荐迁移到该方案,不需要 CP 再调用原服务端接口
方案二:CP 自行上报 已接入 /api/v3/action/submit 的存量 CP CP 服务端调用原接口上报,同时通过 wx.sendCustomMsg 发送 action_code_report 消息 CP 服务端 兼容原服务端上报方案

# 一、接入前准备

  1. 在美团游戏开放平台配置行为任务,并登记对应的 actionCode。
  2. 确认游戏内触发行为完成的时机,例如完成一局、完成一关、完成一次分享或观看一次广告。
  3. 如果是可多次上报行为任务,确认一次游戏行为对应的进度增量。大多数场景为 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。该方案保留原有服务端接口能力,并通过前端消息通知美团游戏平台更新悬浮球进度。

# 接入流程

  1. 服务端上报:CP 服务端继续调用本文下方的「CP 服务端上报接口」,完成行为数据上报。
  2. 前端通知:CP 前端在同一次行为完成后,通过 wx.sendCustomMsg 发送 action_code_report 消息,用于更新悬浮球任务进度。
  3. 进度保持一致:如果是可多次上报行为任务,前端消息中的 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白名单: img.png 这里为服务器ip白名单,填写域名无效;如果服务器有多个出口ip,可以添加多个ip白名单,也可用子网掩码方式

  • Q:签名错误
  • A:遇到签名问题,请仔细查看接入代码,可能出现的问题:
    a)日期内容url encode时,有些encode库会将空格专码为"+"导致错误,应转换为"%20";
    b)使用SHA加密的长度可能会被截断,注意是32位;
    c)代码类库与代码版本不匹配;
    d)空值的参数名不参与签名;(比如非必填参数 innerSource)
  • 可用开平中的工具进行验证: img_1.png 密文代表bizContent加密后内容,该部分可以解密使用; 密文签名代表sign加密后内容,可以进行对照
上次更新: 8/10/2026, 5:36:37 PM