Unity用C#完成抖音小游戏接入引力引擎(Gravity Engine)完整指南,一篇文章讲清楚!

本文记录了将引力引擎 SDK 接入抖音小游戏(Unity)的完整过程,包含云函数搭建、openId 获取、SDK 初始化、注册事件上报等所有细节,新手按步骤操作即可完成接入。

文章目录

    • [1. 背景与目标](#1. 背景与目标)
    • [2. 准备工作](#2. 准备工作)
      • [2.1 引力引擎后台参数](#2.1 引力引擎后台参数)
      • [2.2 抖音开发者后台参数](#2.2 抖音开发者后台参数)
      • [2.3 Unity 工程准备](#2.3 Unity 工程准备)
    • [3. 关键概念澄清](#3. 关键概念澄清)
    • [4. 架构设计](#4. 架构设计)
    • [5. 第一步:搭建腾讯云函数(为了获取抖音OpenID)](#5. 第一步:搭建腾讯云函数(为了获取抖音OpenID))
      • [5.1 创建云函数](#5.1 创建云函数)
      • [5.2 编写云函数代码](#5.2 编写云函数代码)
      • [5.3 配置环境变量](#5.3 配置环境变量)
      • [5.4 配置公网访问](#5.4 配置公网访问)
      • [5.5 部署并发布版本](#5.5 部署并发布版本)
      • [5.6 验证云函数](#5.6 验证云函数)
    • [6. 第二步:Unity 接入引力引擎 SDK](#6. 第二步:Unity 接入引力引擎 SDK)
      • [6.1 创建 GravityEngineManager.cs](#6.1 创建 GravityEngineManager.cs)
    • [7. 第三步:修改游戏启动逻辑](#7. 第三步:修改游戏启动逻辑)
    • [8. 第四步:配置抖音合法域名](#8. 第四步:配置抖音合法域名)
    • [9. 验证与测试](#9. 验证与测试)
    • 在这里插入图片描述
    • [10. 常见问题排查](#10. 常见问题排查)
      • [❌ `HTTP/1.1 443 Unknown HTTP status`](#❌ HTTP/1.1 443 Unknown HTTP status)
      • [❌ `error: 2, errmsg: "bad secret"`](#❌ error: 2, errmsg: "bad secret")
      • [❌ `error: 1, errmsg: "未注册的 appid"`](#❌ error: 1, errmsg: "未注册的 appid")
      • [❌ `[GravityEngine] 初始化失败: name must be required`](#❌ [GravityEngine] 初始化失败: name must be required)
      • [❌ `[GravityEngine] 初始化失败: 参数错误`](#❌ [GravityEngine] 初始化失败: 参数错误)
    • 总结

1. 背景与目标

引力引擎(Gravity Engine)是字节跳动旗下的广告归因与用户分析平台,用于统计小游戏广告变现效果。

目标:

  • 在抖音小游戏中接入引力引擎 SDK
  • 使用抖音真实 openId 作为用户标识(clientId)
  • 上报用户注册事件 $MPRegister(引力引擎要求的买量归因必要事件)

仅上报注册事件 ,广告展示($AdShow)由引力引擎后台从抖音自动拉取,无需客户端手动上报。


2. 准备工作

2.1 引力引擎后台参数

登录 引力引擎后台 获取以下参数:

参数 说明 示例
Access Token SDK 鉴权 Token LZh7dWzhqb****
App ID(包名) 抖音小游戏 appid tt0c5c7ae1923c3*****

2.2 抖音开发者后台参数

登录 抖音开发者后台 → 开发管理 → 开发设置,获取:

参数 说明
AppID 小游戏唯一标识,如 tt0c5c7ae1923c3*****
AppSecret 用于服务端换取 openId,不能暴露在客户端

2.3 Unity 工程准备

  1. 导入 GravityEngine-x.x.x.unitypackage(从引力引擎后台下载)

  2. 确保已接入头条 StarkSDK / TTSDK 插件

  3. 在 Unity 的 Player Settings → Scripting Define Symbols 中添加:

    复制代码
    GRAVITY_BYTEDANCE_TT_GAME_MODE

3. 关键概念澄清

openId 与 anonymousCode 的区别

这是接入时最容易混淆的地方:

标识符 来源 长度 是否为真正 openId
anonymousCode TT.Login 客户端回调 100+ 字符(开发者工具中尤其长) ,是匿名临时标识
真正的 openId 服务端 code2Session 换取 约 36 字符

引力引擎要求 clientId 必须为抖音 openId 原值 ,因此必须通过 code2Session 获取真实 openId。

为什么需要云函数

调用 code2Session 接口需要传 AppSecret,而 AppSecret 不能放在客户端代码中(违反抖音安全规范,可能导致小游戏下架)。

解决方案: 用腾讯云函数做中转服务,AppSecret 存在云端环境变量中,客户端只传 appidcode


4. 架构设计

复制代码
Unity 客户端
  │ POST { appid, code } + Header: x-api-key
  ▼
腾讯云函数(GetOpenID)
  │ 根据 appid 查找 AppSecret
  │ 调用抖音 code2Session API
  ▼
抖音服务端
  │ 返回真实 openid
  ▼
云函数 → 只返回 openid(过滤敏感的 session_key)
  ▼
Unity 客户端
  │ 缓存 openId 到 PlayerPrefs
  │ 初始化引力引擎 SDK
  ▼
引力引擎后台
  └ 上报 $MPRegister 注册事件

多项目复用: 云函数通过 JSON 映射表支持多个项目,添加新项目只需修改环境变量,无需改代码。


5. 第一步:搭建腾讯云函数(为了获取抖音OpenID)

如果你已经获取过了,可以跳过这一步

为什么一定要云函数或者服务器?因为获取OpenID需要向抖音传递AppSecret参数,AppSecret 放在客户端存在泄露风险,抖音文档明确建议仅在服务端使用。

5.1 创建云函数

  1. 登录 腾讯云函数控制台
  2. 点击 新建,配置如下:
配置项
函数类型 Web 函数(重要!不要选事件函数)
函数名称 GetOpenID
地域 广州(延迟低)
运行环境 Node.js 16.13
提交方法 在线编辑
执行方法 index.main_handler

5.2 编写云函数代码

在在线编辑器中,找到 app.js全部替换为以下代码:

javascript 复制代码
/**
 * 腾讯云函数 Web 函数 - 抖音小游戏 code2Session 中转服务
 * 零依赖,使用 Node.js 内置模块
 * 
 * 环境变量:
 *   APP_SECRETS = {"tt你的appid1":"AppSecret1","tt你的appid2":"AppSecret2"}
 *   API_KEY     = 自定义随机字符串(防止接口被滥用)
 */

const http = require('http');
const https = require('https');

let secretsMap = null;
function getSecretsMap() {
  if (secretsMap) return secretsMap;
  try {
    secretsMap = JSON.parse(process.env.APP_SECRETS || '{}');
  } catch (e) {
    console.error('APP_SECRETS 解析失败:', e.message);
    secretsMap = {};
  }
  return secretsMap;
}

function httpsGet(url) {
  return new Promise((resolve, reject) => {
    const req = https.get(url, { timeout: 5000 }, (res) => {
      let body = '';
      res.on('data', (chunk) => body += chunk);
      res.on('end', () => resolve(body));
    });
    req.on('error', reject);
    req.on('timeout', () => { req.destroy(); reject(new Error('请求超时')); });
  });
}

function readBody(req) {
  return new Promise((resolve) => {
    let raw = '';
    req.on('data', (chunk) => raw += chunk);
    req.on('end', () => resolve(raw));
  });
}

function send(res, statusCode, data) {
  res.writeHead(statusCode, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(data));
}

const server = http.createServer(async (req, res) => {
  if (req.method !== 'POST') {
    return send(res, 405, { error: 1, errmsg: '仅支持 POST 请求' });
  }

  // API Key 鉴权
  const expectedKey = process.env.API_KEY;
  if (expectedKey) {
    const clientKey = req.headers['x-api-key'] || '';
    if (clientKey !== expectedKey) {
      return send(res, 403, { error: 1, errmsg: '无效的 API Key' });
    }
  }

  // 解析请求体
  let body;
  try {
    const raw = await readBody(req);
    body = JSON.parse(raw || '{}');
  } catch (e) {
    return send(res, 400, { error: 1, errmsg: '请求体 JSON 格式错误' });
  }

  const { appid, code, anonymous_code } = body;

  if (!appid) return send(res, 400, { error: 1, errmsg: '缺少 appid' });
  if (!code && !anonymous_code) return send(res, 400, { error: 1, errmsg: '缺少 code 或 anonymous_code' });

  // 查找对应 AppSecret
  const secret = getSecretsMap()[appid];
  if (!secret) {
    console.error(`未注册的 appid: ${appid}`);
    return send(res, 400, { error: 1, errmsg: '未注册的 appid' });
  }

  // 调用抖音 code2Session
  let url = `https://minigame.zijieapi.com/mgplatform/api/apps/jscode2session?appid=${appid}&secret=${secret}`;
  if (code) url += `&code=${encodeURIComponent(code)}`;
  if (anonymous_code) url += `&anonymous_code=${encodeURIComponent(anonymous_code)}`;

  try {
    const raw = await httpsGet(url);
    const data = JSON.parse(raw);
    console.log(`code2Session appid=${appid} error=${data.error} hasOpenid=${!!data.openid}`);

    // 只返回必要字段,不暴露 session_key
    return send(res, 200, {
      error: data.error || 0,
      openid: data.openid || '',
      anonymous_openid: data.anonymous_openid || '',
      errmsg: data.errmsg || data.message || ''
    });
  } catch (e) {
    console.error('code2Session 异常:', e.message);
    return send(res, 500, { error: 1, errmsg: '服务端请求异常' });
  }
});

// Web 函数必须监听 9000 端口
server.listen(9000, () => {
  console.log('GetOpenID 服务已启动,监听端口 9000');
});

5.3 配置环境变量

在创建页面展开 高级配置 → 环境配置,添加两条环境变量:

变量名 说明
APP_SECRETS {"tt你的appid":"你的AppSecret"} 多项目用逗号分隔加在同一个 JSON 里
API_KEY 任意随机字符串,如 MyKey2026abcXYZ 客户端请求时携带,防止接口被滥用

多项目示例:

json 复制代码
{
  "tt0c5c7ae1923c3*****": "AppSecret_游戏1",
  "tt511627abcf779*****": "AppSecret_游戏2"
}

⚠️ 注意:APP_SECRETS 的 key 必须是 tt 开头的抖音 appid,不是其他格式

其他配置:

配置项
执行超时时间 改为 10 秒(默认 3 秒不够)

5.4 配置公网访问

展开 函数 URL 配置 ,勾选 公网访问 ,鉴权类型选 开放(我们自己用 API_KEY 鉴权)。

5.5 部署并发布版本

  1. 点击 完成 创建函数

  2. 进入函数后,点 版本管理 → 发布新版本

  3. 将新版本流量设为 100%

  4. 函数 URL 页面记录公网访问 URL,格式如:

    复制代码
    https://1255346748-xxxxxxxx.ap-guangzhou.tencentscf.com

5.6 验证云函数

在命令行测试(PowerShell):

powershell 复制代码
Invoke-RestMethod `
  -Uri "https://你的云函数URL" `
  -Method POST `
  -ContentType "application/json" `
  -Headers @{"x-api-key"="你的API_KEY"} `
  -Body '{"appid":"tt你的appid","code":"test123"}'

预期返回(test123 是假 code,抖音会报错,但说明云函数本身跑通):

json 复制代码
{"error": 2, "openid": "", "errmsg": "bad secret"}

如果返回 443 exit unexpected,说明云函数没有成功启动,检查代码是否完整粘贴并重新发布版本。


6. 第二步:Unity 接入引力引擎 SDK

导入插件到Unity
点击跳转插件地址
点击跳转官方文档

6.1 创建 GravityEngineManager.cs

Assets/Scripts/Analytics/ 目录下创建 GravityEngineManager.cs

csharp 复制代码
using System.Collections;
using System.Collections.Generic;
using Com.Arcanemiracle;
using GravityEngine;
using UnityEngine;
using UnityEngine.Networking;
#if SOLARENGINE_BYTEDANCE
using TTSDK;
#endif

/// <summary>
/// 引力引擎SDK管理器(抖音小游戏 GRAVITY_BYTEDANCE_TT_GAME_MODE)
/// 接入说明:
///   - 仅上报 $MPRegister 用户注册事件
///   - clientId 必须使用抖音真实 openId(通过 code2Session 服务端接口换取)
///   - 启动流程:有缓存 openId 直接启动;否则 TT.Login → 云函数 → 获取真实 openId
/// </summary>
public class GravityEngineManager : UnitySingleton<GravityEngineManager>
{
    // 引力引擎 Access Token(在引力引擎后台获取)
    private const string ACCESS_TOKEN = "你的引力引擎AccessToken";

    // 云函数配置(AppSecret 安全存储在云端,不在客户端暴露)
    private const string DOUYIN_APPID = "tt你的抖音AppID";
    private const string CODE2SESSION_URL = "https://你的云函数URL";
    private const string CODE2SESSION_API_KEY = "你设置的API_KEY";

    // PlayerPrefs 缓存键
    private const string PREF_REAL_OPENID = "GravityEngine_RealOpenId";
    private const string PREF_NICKNAME = "GravityEngine_NickName";
    private const string PREF_REGISTERED = "GravityEngine_Registered";

    private bool _isSdkStarted = false;

    public override void Awake()
    {
        base.Awake();
    }

    /// <summary>
    /// TT SDK 初始化完成后调用(在 TT.InitSDK 回调中触发)
    /// 有缓存真实 openId 则直接启动;否则走 TT.Login + code2Session 流程
    /// </summary>
    public void InitAfterTTSDKReady()
    {
#if GRAVITY_BYTEDANCE_TT_GAME_MODE
        string cachedOpenId = PlayerPrefs.GetString(PREF_REAL_OPENID, "");
        string cachedNickName = PlayerPrefs.GetString(PREF_NICKNAME, "");

        if (!string.IsNullOrEmpty(cachedOpenId))
        {
            Debug.Log($"[GravityEngine] 使用缓存的真实 openId: {cachedOpenId}");
            StartAndInitSDK(cachedOpenId, cachedNickName);
        }
        else
        {
            FetchRealOpenId();
        }
#endif
    }

    /// <summary>
    /// TT.Login(forceLogin:true) 获取 code,再通过云函数换取真实 openId
    /// 抖音宿主中用户已登录,forceLogin:true 静默完成,不弹窗
    /// 若失败,降级到 anonymous_code → anonymous_openid
    /// </summary>
    private void FetchRealOpenId()
    {
#if GRAVITY_BYTEDANCE_TT_GAME_MODE
        TT.Login(
            successCallback: (code, anonymousCode, isLogin) =>
            {
                Debug.Log($"[GravityEngine] TT.Login 成功 - code长度: {code?.Length ?? 0}, isLogin: {isLogin}");

                if (!string.IsNullOrEmpty(code))
                {
                    StartCoroutine(RequestCode2Session(code));
                }
                else if (!string.IsNullOrEmpty(anonymousCode))
                {
                    Debug.LogWarning("[GravityEngine] code 为空,降级使用 anonymousCode 换取 anonymous_openid");
                    StartCoroutine(RequestCode2SessionAnonymous(anonymousCode));
                }
                else
                {
                    Debug.LogError("[GravityEngine] code 和 anonymousCode 均为空,无法初始化");
                }
            },
            failedCallback: (errMsg) =>
            {
                Debug.LogError($"[GravityEngine] TT.Login(forceLogin:true) 失败: {errMsg},尝试匿名登录");
                TT.Login(
                    successCallback: (code2, anonymousCode2, isLogin2) =>
                    {
                        if (!string.IsNullOrEmpty(anonymousCode2))
                            StartCoroutine(RequestCode2SessionAnonymous(anonymousCode2));
                        else
                            Debug.LogError("[GravityEngine] 匿名登录也未获取到有效标识");
                    },
                    failedCallback: (errMsg2) =>
                    {
                        Debug.LogError($"[GravityEngine] 匿名登录也失败: {errMsg2}");
                    },
                    forceLogin: false
                );
            },
            forceLogin: true
        );
#endif
    }

    private IEnumerator RequestCode2Session(string code)
    {
        string postData = JsonUtility.ToJson(new Code2SessionRequestWithCode
        {
            appid = DOUYIN_APPID,
            code = code
        });
        yield return RequestCode2SessionInternal(postData, "openid");
    }

    private IEnumerator RequestCode2SessionAnonymous(string anonymousCode)
    {
        string postData = JsonUtility.ToJson(new Code2SessionRequestWithAnonymous
        {
            appid = DOUYIN_APPID,
            anonymous_code = anonymousCode
        });
        yield return RequestCode2SessionInternal(postData, "anonymous_openid");
    }

    private IEnumerator RequestCode2SessionInternal(string postData, string openIdField)
    {
        using (var request = new UnityWebRequest(CODE2SESSION_URL, "POST"))
        {
            request.uploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(postData));
            request.downloadHandler = new DownloadHandlerBuffer();
            request.SetRequestHeader("Content-Type", "application/json");
            request.SetRequestHeader("x-api-key", CODE2SESSION_API_KEY);
            request.timeout = 10;
            yield return request.SendWebRequest();

            if (request.result == UnityWebRequest.Result.Success)
            {
                var json = request.downloadHandler.text;
                Debug.Log($"[GravityEngine] code2Session 响应: {json}");

                var resp = JsonUtility.FromJson<Code2SessionResponse>(json);
                string resultOpenId = openIdField == "anonymous_openid"
                    ? resp.anonymous_openid
                    : resp.openid;

                if (resp.error == 0 && !string.IsNullOrEmpty(resultOpenId))
                {
                    Debug.Log($"[GravityEngine] 获取真实 openId 成功: {resultOpenId}");
                    CacheAndInitSDK(resultOpenId, "");
                }
                else
                {
                    Debug.LogError($"[GravityEngine] code2Session 失败: error={resp.error}, errmsg={resp.errmsg}");
                }
            }
            else
            {
                Debug.LogError($"[GravityEngine] code2Session 网络请求失败: {request.error}");
            }
        }
    }

    private void CacheAndInitSDK(string openId, string nickName)
    {
        PlayerPrefs.SetString(PREF_REAL_OPENID, openId);
        if (!string.IsNullOrEmpty(nickName))
            PlayerPrefs.SetString(PREF_NICKNAME, nickName);
        PlayerPrefs.Save();
        StartAndInitSDK(openId, nickName);
    }

    /// <summary>
    /// 用户显式登录后调用,更新昵称(openId 不变)
    /// </summary>
    public void OnNickNameUpdated(string nickName)
    {
#if GRAVITY_BYTEDANCE_TT_GAME_MODE
        if (string.IsNullOrEmpty(nickName)) return;
        PlayerPrefs.SetString(PREF_NICKNAME, nickName);
        PlayerPrefs.Save();
        string cachedOpenId = PlayerPrefs.GetString(PREF_REAL_OPENID, "");
        if (_isSdkStarted && !string.IsNullOrEmpty(cachedOpenId))
            StartAndInitSDK(cachedOpenId, nickName);
#endif
    }

    private void StartAndInitSDK(string openId, string nickName)
    {
#if GRAVITY_BYTEDANCE_TT_GAME_MODE
        if (!_isSdkStarted)
        {
            if (FindObjectOfType<GravityEngineAPI>() == null)
                new GameObject("GravityEngine", typeof(GravityEngineAPI));

            GravityEngineAPI.StartGravityEngine(ACCESS_TOKEN, openId, GravityEngineAPI.SDKRunMode.NORMAL);
            _isSdkStarted = true;
            Debug.Log($"[GravityEngine] SDK 已启动,clientId(openId): {openId}");
        }

        // nickname 为必填项,为空时用 openId 前8位兜底
        string effectiveNickName = string.IsNullOrEmpty(nickName)
            ? openId.Substring(0, System.Math.Min(8, openId.Length))
            : nickName;

        int appVersion = 1;
        if (int.TryParse(Application.version, out int parsedVersion))
            appVersion = parsedVersion;

        GravityEngineAPI.Initialize(
            openId,
            effectiveNickName,
            appVersion,
            openId,
            false,
            new GEInitializeCallback(
                (_) =>
                {
                    Debug.Log("[GravityEngine] 初始化成功");
                    GravityEngineAPI.Flush();
                    TrackRegisterEventOnce();
                },
                (errorMsg) =>
                {
                    Debug.LogError($"[GravityEngine] 初始化失败: {errorMsg}");
                }
            )
        );
#endif
    }

    /// <summary>
    /// 首次运行上报 $MPRegister 注册事件(仅一次,本地标记去重)
    /// </summary>
    private void TrackRegisterEventOnce()
    {
#if GRAVITY_BYTEDANCE_TT_GAME_MODE
        if (PlayerPrefs.GetInt(PREF_REGISTERED, 0) == 0)
        {
            GravityEngineAPI.TrackMPRegister();
            PlayerPrefs.SetInt(PREF_REGISTERED, 1);
            PlayerPrefs.Save();
            Debug.Log("[GravityEngine] 首次上报注册事件 $MPRegister");
        }
        else
        {
            Debug.Log("[GravityEngine] 注册事件已上报过,跳过");
        }
#endif
    }

    // 分开两个请求类,避免 JsonUtility 把 null 字段序列化为空字符串导致云函数误判
    [System.Serializable]
    private class Code2SessionRequestWithCode
    {
        public string appid;
        public string code;
    }

    [System.Serializable]
    private class Code2SessionRequestWithAnonymous
    {
        public string appid;
        public string anonymous_code;
    }

    [System.Serializable]
    private class Code2SessionResponse
    {
        public int error;
        public string openid;
        public string anonymous_openid;
        public string errmsg;
        public string message;
    }
}

/// <summary>
/// 引力引擎初始化回调
/// </summary>
public class GEInitializeCallback : IInitializeCallback
{
    private readonly System.Action<Dictionary<string, object>> _onSuccess;
    private readonly System.Action<string> _onFailed;

    public GEInitializeCallback(
        System.Action<Dictionary<string, object>> onSuccess,
        System.Action<string> onFailed)
    {
        _onSuccess = onSuccess;
        _onFailed = onFailed;
    }

    public void onSuccess(Dictionary<string, object> responseJson) => _onSuccess?.Invoke(responseJson);
    public void onFailed(string errorMsg) => _onFailed?.Invoke(errorMsg);
}

7. 第三步:修改游戏启动逻辑

在游戏的 TT.InitSDK 成功回调 中调用 GravityEngineManager

csharp 复制代码
// GameLaunch.cs 中的 TT.InitSDK 调用处
TT.InitSDK(
    successCallback: () =>
    {
        // TT SDK 就绪后初始化引力引擎
        // 必须在 InitSDK 回调后调用,否则 TT API 尚未就绪
        GravityEngineManager.Instance.InitAfterTTSDKReady();

        // ...其他初始化逻辑
    },
    failCallback: (errMsg) =>
    {
        Debug.LogError($"TT.InitSDK 失败: {errMsg}");
    }
);

如果用户有手动登录流程,在登录成功后调用(更新昵称):

csharp 复制代码
// 用户登录成功后
GravityEngineManager.Instance.OnNickNameUpdated(nickName);

8. 第四步:配置抖音合法域名

登录 抖音小游戏开发者后台 → 开发管理 → 开发设置 → 服务器域名 → request 合法域名,添加:

复制代码
https://你的云函数URL(如 https://1255346748-xxx.ap-guangzhou.tencentscf.com)
https://backend.gravity-engine.com
https://api.gravity-engine.com

9. 验证与测试

最后打包到模拟器或真机扫码运行,看到日志成功之后,过段时间可以看到后台

预期日志

首次启动:

复制代码
[GravityEngine] TT.Login 成功 - code长度: 123, isLogin: True
[GravityEngine] code2Session 响应: {"error":0,"openid":"_000YbIAk...","errmsg":""}
[GravityEngine] 获取真实 openId 成功: _000YbIAkZvmOLxJAH9SfhUl4Bh7gIZ_QdoZ
[GravityEngineSDK] 有接入 ByteDanceTT 小游戏
[GravityEngine] SDK 已启动,clientId(openId): _000YbIAkZvmOLxJAH9SfhUl4Bh7gIZ_QdoZ
[GravityEngine] 初始化成功
[GravityEngine] 首次上报注册事件 $MPRegister

后续启动(使用缓存):

复制代码
[GravityEngine] 使用缓存的真实 openId: _000YbIAkZvmOLxJAH9SfhUl4Bh7gIZ_QdoZ
[GravityEngine] SDK 已启动,clientId(openId): _000YbIAkZvmOLxJAH9SfhUl4Bh7gIZ_QdoZ
[GravityEngine] 初始化成功
[GravityEngine] 注册事件已上报过,跳过

引力引擎后台验证

登录引力引擎后台 → 分析 → 用户组查,应能看到:

  • 用户 openId 与日志一致
  • 注册时间正确
  • 媒体平台显示"自然量"或对应渠道

10. 常见问题排查

HTTP/1.1 443 Unknown HTTP status

原因: 云函数进程启动失败(0 code exit unexpected)。

解决:

  1. 检查 app.js 代码是否完整粘贴
  2. 确认 scf_bootstrap 内容为 node app.js(Web 函数默认)
  3. 重新部署并发布新版本(编辑后必须点"部署"再"发布版本")

error: 2, errmsg: "bad secret"

原因: APP_SECRETS 环境变量中的 AppSecret 不正确。

解决: 仔细核对 AppSecret,注意每一个字符(大小写、0 和 O 等容易混淆的字符)。


error: 1, errmsg: "未注册的 appid"

原因: APP_SECRETS 的 key 与客户端传来的 appid 不匹配。

解决: 确认 APP_SECRETS 的 key 格式为 tt 开头的抖音 appid,如 tt0c5c7ae1923c3d4f07


[GravityEngine] 初始化失败: name must be required

原因: 引力引擎 Initialize 接口的 nickName 参数不能为空。

解决: 代码中已用 openId 前 8 位作为默认昵称兜底,确认 effectiveNickName 逻辑存在。


[GravityEngine] 初始化失败: 参数错误

原因: clientId(openId)格式不正确,常见于使用了超长的 anonymousCode(开发者工具中可超过 100 字符)。

解决: 确保使用通过 code2Session 换取的真实 openId(约 36 字符),而不是直接使用 anonymousCode


总结

步骤 关键点
云函数 Web 函数类型,监听 9000 端口,AppSecret 存环境变量
openId 获取 TT.Logincode → 云函数 code2Session → 真实 openId
SDK 初始化 必须在 TT.InitSDK 回调后执行
事件上报 仅上报 $MPRegister$AdShow 由后台自动拉取
缓存机制 openId 存 PlayerPrefs,后续启动直接使用,无需重复调用云函数
多项目复用 一个云函数通过 APP_SECRETS JSON 支持多个 appid
相关推荐
福赖3 小时前
《C#反射机制》
开发语言·c#
向上的车轮3 小时前
熟悉C#如何转TypeScript?
开发语言·typescript·c#
我是唐青枫3 小时前
C#.NET ReaderWriterLockSlim 深入解析:读写锁原理、升级锁与使用边界
开发语言·c#·.net
WarrenMondeville4 小时前
4.Unity面向对象-接口隔离原则
java·unity·接口隔离原则
The Sheep 20234 小时前
C# 操作XML
xml·前端·c#
JosieBook5 小时前
【C#】C# 中的 enum、struct 和 class 对比总结
开发语言·算法·c#
WarrenMondeville5 小时前
3.Unity面向对象-里氏替换原则
unity·游戏引擎·里氏替换原则
Scout-leaf6 小时前
WPF新手村教程(七)—— 终章(MVVM架构初见杀)
c#·wpf
WarrenMondeville6 小时前
5.Unity面向对象-依赖倒置原则
unity·设计模式·依赖倒置原则