发行 SDK 开发指南

作为发行的 H5-SDK 一般都是类似于 iframe 嵌套外部网页地址的模式:

1
2
3
4
<!-- 假设访问的游戏主页面 HTML, 也就是发行方的访问地址 -->
<body>
<iframe src="{内部的 iframe 地址就是游戏页面地址}"></iframe>
</body>

而游戏研发方就需要引入发行方的一段 JS 脚本, 让研发方的来接入对应功能:

1
2
<!-- 在研发方的 HTML5 游戏页面当中引入 -->
<script src="https://{发行商提供的域名}/static/sdk/PinoSDK.js"></script>

一般不推荐将这个文件下载到本地资源再应用, 否则可能无法及时更新对应 API 接口, 内部 SDK.js 需要实现以下功能:

  • init(单向, 只需要监听): 初始化, 监听 iframe 上层返回的数据信号

  • login(单向, 只需要监听): 授权登录, 由上级 iframe 登陆授权完成之后传递回来数据信号

  • logout(单向, 只需要监听): 注销授权, 有上级 iframe 发起退出信号之后传递回来数据信号

  • pay(双向, 需要手动发起并拦截监听): 发起支付请求, 将研发订单信息传递给 iframe 并等待返回(返回仅代表订单被成功接受)

  • report(单向, 主动上报行为事件): 发起数据上报请求, 提交用户的行为到发行方, 用于记录玩家的行为日志

  • exit(单向, 需要确认退出行为是否成功回调): 退出功能, 这是个可选的接口, 因为部分功能只需要关闭网页就算 ‘退出’

对于客户端来说, 内部的 JS 实际上就相当于代理请求, 将内部的参数提交到上层 iframe 然后由上级请求到服务 api 接口.

具体的 iframe 通讯技术是 postMessage

另外还有种 微端 的平台端, 其实就是原生混合的 WebKit 端, 也就是通过 WebKit 包装套壳应用.

Android WebKit: androidx-webkit

这部分的端其实也是和 H5 端类似, 只是套了层壳方便动态调整内部的做界面调整, 好处是不需要投入太多精力在不同客户端做 UI 处理.

不过需要注意, 这类马甲包现在也是处于海外商城上架的严打范围之中, 因为内部可以动态变化的关系, 很多非法行业都喜欢这样提交上包

其他的包括以下端请求也是同样的, 只是下发时候的数据有所不而已(其他也没有哪里很大的不同):

  • Android: 目前主流移动端之一, 需要实现反编译动态注入初始化分包, 不过处理过一次之后后续只需要维护即可

  • iOS: 目前主流移动端之一, 这部分实际上我接触也不多, 而且 apple store 是出了名上架难

  • PC: PC 的主流其实就是上架 Steam/Amazon/Microsoft Store 之类, 这部分需要对接平台 SDK 包装处理, 那是另外的话题

其他大大小小平台也差不多流程, 所以这里也不做过多说明.

通用参数签名

接下来就是说明下对应的所有接口在服务端的请求响应细节, 后面通用的 sign 处理方式都是可以按照以下方式处理(以 PHP 为例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$appid = "v3243wc";// 应用AppID标识
$appKey = "345f83cea7fe4de056a6045a26645b2b"; // 后台配置的 AppKey

// 初始化数据结构如下
$params = [
'appid' => $appid,
'time' => round(microtime(true) * 1000), // 获取发起的毫秒级UTC时间戳
];

// 必须要对数组的 key 做正序排列
ksort($params);

// 对参数做 KEY=VALUE&KEY=VALUE 排列
$data = [];
foreach ($params as $k=>$v){
$data []= "$k=$v";
}
$paramsString = implode('&',$data);
// 输出结果参数字符串如下:
// appid=v3243wc&time=1766127245519


// 最后附加上 App Key 合并md5处理
$sign = md5("$paramsString$appKey");
// 合并之后的MD5字符串如下:
// 87fe960f93721e8e2e3e87f278e91724

这里就是目前比较广泛通用的参数哈希签名验证签名方式, 后续内部请求的 sign 字段都是用该方式请求出来

初始化请求 - 客户端

在游戏页面完成初始化后需要对 SDK 再初始化, 发起的请求参数如下(实际上是 iframe 代理发起请求):

参数名 参数类型 是否为必须 参数说明
appid string 请求的应用ID凭据, 有的会将应用ID设置为纯数值,有的采用随机字符串标识
time number 发起请求的时间, 采用 UTC 毫秒时间戳, 服务端检测请求延迟必须要在一分钟之内
platform string 标识客户端的来源平台, 比如 GOOGLE/FACEBOOK/IOS/WECHAT 之类
channel string 标识客户端的子包渠道, 也就是后台分配绑定的推广人(地推/主播/买量渠道/广告主)标识, 空字符串代表没有子包渠道
device string 标识客户端设备信息, 比如 H5/Android/iOS/PC 之类
sign string 混合 appKey 和请求参数签名的哈希值, 用于对参数做签名验证

这些参数都是基本要提交上来验证和记录, 之后服务端就需要下发如下数据:

参数名 参数类型 参数说明
appid string 游戏在应用后台生成AppId
time number 发行方响应的毫秒级UTC时间戳, 仅仅作为数据噪音功能
platform string 标识客户端的来源平台, 比如 GOOGLE/FACEBOOK/IOS/WECHAT 之类
channel string 标识客户端的子包渠道, 也就是后台分配绑定的推广人(地推/主播/买量渠道/广告主)标识, 用来结算买量收益
device string 标识客户端设备信息, 比如 H5/Android/iOS/PC 之类
trace string 下发的链路渠道标识, 后续所有行为都要提交该标识, 用于做用户的数据归因
name string 游戏应用名
version string 游戏版本信息, 用于提交确认是否需要升级
icon string 游戏ICON地址
language string 游戏默认的语言模板, 用于调度本身界面的默认语言(i18n)
properties object 后台填写传递 KEY-VALUE 对象组, 内部为 {“xxx”:“yyy”} 对象组, 用于给客户端获取常量参数
extra object 自定义扩展信息, 内部为 {“xxx”:“yyy”} 对象组, 用于提供给 SDK 内部些特定功能, 比如不允许注册/不允许开启支付等需要屏蔽的功能等

这里下发之后的数据比较重要的就是 platform/channel/device, 这是后续需要处理 数据归因 的核心.

数据归因: 统计客户在不同因素(渠道、行为、变量)对最终目标(转化、成交、留存等)的贡献度, 保证打通客户端从点击到游玩之中的链路系统

客户端归因流程如下, 一定要确保这部分流程打通, 具体流程如下(自上而下的流程):

流程环节 核心动作 技术细节
1. 用户触达(推广投放) 生成含channel/platform的唯一推广链接 示例:https://game.com?channel=主播A&platform=FACEBOOK
2. 点击推广链接 解析链接参数,存入本地缓存(localStorage/APP本地文件),有效期7天;
原生APP是采用反编译注入分包写入这些参数
缓存键:trace_channel/trace_platform,值:对应渠道/平台标识
3. 下载/打开游戏页面 读取缓存→URL参数→兜底(channel=空字符串);
原生应用直接读取反编译注入的配置文件参数
优先级:缓存 > URL参数 > 空字符串(自然量)
4. 游戏页面初始化 封装channel/platform/device为全局变量,获取设备ID(OAID/IDFA等) device解析:H5用navigator.userAgent,APP用原生API
5. SDK初始化 组装请求参数(appid/time/platform/channel/device/sign),iframe代理发起请求 sign生成规则:MD5(appid+time+platform+channel+device+appKey)
6. 服务端验证并返回数据 1. 验证sign+time(延迟≤1分钟);
2. 生成trace;
3. 写入Redis缓存;
4. 返回trace+下发数据
生成trace后,立即以trace:{appid}:{trace}为key写入Redis Hash;
返回示例:{trace: "TR_8e7f9b4567890abc_17356896", ...}
7. 进入游戏 客户端缓存trace(localStorage/APP本地文件),持久化存储(直到用户卸载) 缓存键:game_sdk_trace,不可被清空,保障后续行为可溯源
8. 核心行为(注册/付费/留存) 上报行为时,仅携带trace+行为参数 埋点示例:{trace: "TR_8e7f9b4567890abc_17356896", action: "pay", amount: 100, time: 1735689600000}

上面的 trace_id 字段就是下发的 trace 字段, 本质上就是为了服务端能够缓存这部分链路关联数据, 在必要的时候提取出来查询.

自然量是买量当中的说法, 代表非推广买量而来的自然注册登陆的用户, 所以不需要结算给渠道任何付费转化的收益

因为如果你不采用 trace 记录链路, 那么每次发起请求就需要将原来的 channel/platform/device 数据提交上来才能识别出对应平台/渠道/设备.

后面如果扩展出其他字段的话(比如追加 imei 字段), SDK 所有涉及的地方请求结构体都要加字段, 所以不如在服务端用 redis 缓存记录.

总之, 初始化就是作为 SDK 设计当中核心中核心, 后续数据关联都是依赖初始化生成的参数来提交绑定.

请求授权 - 客户端

初始化完成之后之后就可以渲染发行方的 SDK 授权登陆界面, 其中可能有以下登陆方式:

  • 帐号密码登陆: 最简单干脆的登陆方式, 直接帐号密码即可登陆, 这部分好处就是简单高效没有其他外部依赖

  • 邮箱地址登陆: 对发行方提交邮件验证, 官方邮箱会给指定邮箱地址发送注册界面, 点击跳转页面输入绑定密码创建账号再回来登陆

  • OAuth2 授权: 海外最常用的授权方式, 也是主流的第三方授权机制, 需要在 Google/Microsoft 注册 OAuth 来做授权绑定

  • Token 延时: 保存到本地的 Token 做延时登陆操作, 其实也就首次授权之后客户端保存 Token 后续复用(默认一个月Token自动过期)

  • 内置 SDK 授权: 有的渠道需要内置 SDK, 授权的时候必须要唤起内部 SDK 的界面授权加载玩家信息

  • 长链接访问授权: 直接采用原生通讯模式要求访问会话是长链接, 授权完全之后才会断开链接返回对应的授权信息, 比较少见的机制

这部分穿插比较多, 因为会涉及到大量第三方奇葩的授权方式, 建议默认只需要处理 OAuth 和常规帐号密码登陆即可, 请求参数如下:

参数名 参数类型 是否为必须 参数说明
appid string 请求的应用ID凭据, 有的会将应用ID设置为纯数值,有的采用随机字符串标识
time number 发起请求的时间, 采用 UTC 毫秒时间戳, 服务端检测请求延迟必须要在一分钟之内
trace string 下发的链路渠道标识, 后续所有行为都要提交该标识, 用于做用户的数据归因
type string 登陆方式标识, 按照这个下发到指定授权方法
params object 采用 {“xxx”:“yyy”} 数据对象, 授权所需要的参数都是内部以 KEY=VALUE 形式提交上来
sign string 混合 appKey 和请求参数签名的哈希值, 用于对参数做签名验证

这里关键的就是 trace/type/params 这些参数, 主要就是服务端判断 type 类型转发到对应第三方参数返回, 响应参数如下:

参数名 参数类型 参数说明
appid string 游戏在应用后台生成应用标识
time number 发行方响应的毫秒级UTC时间戳, 仅仅作为数据噪音功能
trace string 下发的链路渠道标识, 后续所有行为都要提交该标识, 用于做用户的数据归因
type string 登陆方式标识, 按照这个下发到指定授权方法
extra object 额外的扩展参数, 需要的参数结构都放置在内部

这里就需要说明怎么去调度和处理 type/extra 字段:

  • 假设 type=1(帐号密码登陆), 那么 extra 内部返回的其实也就是 uid/username/token 等授权完成的字段(token 是核心)

  • 假设 type=2(OAuth), 那么 extra 就是 redirectUrl 重定向第三方授权地址, 需要二次处理(网页跳转, 原生唤起 WebKit 窗口)

  • 假设 type=3(邮箱登陆), 那么 extra 返回是提示让玩家关注邮箱, 账号专属注册 URL 已经发送等待玩家手动注册再回来登陆

  • 假设 type=4(Token刷新), 那么 extra 返回的其实也是和帐号密码登陆授权机制一致

某些第三方应用商店需要传递我们作为发行方的在其中注册的 APP 信息, 就需要 extra 返回这些信息透传给第三方 SDK

总之无论任何方式, 最后返回的必然可以获取到以下字段:

参数名 参数类型 参数说明
uid string 用户授权之后在发行系统当中的唯一标识
token string 发行系统之中生成的本次会话授权码
created number 是否为注册新用户, 0 - 老用户, 1 - 新用户

uid 采用 number 类型可能会导致 bigint 引发的异常, 建议采用 string 做处理

这代表用户在发行系统当中唯一标识和会话凭证, 也就是用户后续所有操作的基础.

授权验证 - 服务端

当客户端已经获取到授权 uid/token, 游戏研发方这时候就需要验证授权是否合法, 合法才会在游戏研发方生成账号信息.

游戏研发方的服务端接收到请求之后, 需要从服务端发起以下参数结构体请求:

参数名 参数类型 是否为必须 参数说明
appid string 请求的应用ID凭据, 有的会将应用ID设置为纯数值,有的采用随机字符串标识
time number 发起请求的时间, 采用 UTC 毫秒时间戳, 服务端检测请求延迟必须要在一分钟之内
uid string 玩家在发行后台生成的唯一标识
token string 玩家在发行后台生成的会话凭证
sign string 需要对以上参数名按照KEY正序排列, sign = md5(正序排列参数+后台生成appkey)

这里就是为了验证会话本身合法而非伪造, 如果验证通过直接返回对应简略的数据体即可(甚至可以返回 HTTP 状态码)

验证完成之后游戏研发方就在本地数据库创建关联账号即可, 后续授权登陆都是绑定对应的账号信息

发起支付 - 客户端

支付也是另外比较麻烦的点, 同样也是因为第三方支付都是五花八门不好做处理, 不过支付请求结构体基本上是统一的:

参数名 参数类型 是否为必须 参数说明
appid string 请求的应用ID凭据, 有的会将应用ID设置为纯数值,有的采用随机字符串标识
time number 发起请求的时间, 采用 UTC 毫秒时间戳, 服务端检测请求延迟必须要在一分钟之内
trace string 下发的链路渠道标识, 后续所有行为都要提交该标识, 用于做用户的数据归因
uid string 玩家在发行后台生成的唯一标识
token string 玩家在发行后台生成的会话凭证
passage string 发起指定的第三方支付通道, 由发行方定义的支付标识
cp_order_id string CP方在本地数据库生成的订单号
notify_url string 支付完成回调的地址, 如果为空字符串, 则使用后台已经配置的回调地址
redirect_url string 界面完成支付的跳转地址, 支付界面完成之后跳转的页面地址, 默认可以留空不用跳转
extension string CP方提交过来的扩展字符(最大长度64字符), 会原样返回CP
sid string 玩家所在的服务器ID, 不存在分服填写空字符串
sname string 玩家所在的服务器名称, 不存在分服填写空字符串
role_id string 玩家在指定服务器当中的角色ID
role_name string 玩家在指定服务器当中的角色名
role_level string 玩家在指定服务器当中的角色等级, 不存在等级填写空字符串
role_vip string 玩家在指定服务器当中的VIP等级, 不存在VIP等级填写空字符串
item_id string CP支付的商品名, 有的支付渠道用来展示商品名称
item_desc string CP支付的商品详情, 有的支付渠道用来展示商品详情
item_price number 支付的金额, 注意这里采用分/美分为单位, 对于元/美元需要手动去转回
item_count number 购买的物品数量, 注意道具最后价格等于 price * count
currency string 部分海外地区支付需要用的货币单位, 中国大陆地区传递 CNY
region string 部分海外地区支付需要用的支付地区, 中国大陆地区传递 CN
sign string 混合 appKey 和请求参数签名的哈希值, 用于对参数做签名验证

游戏研发方必须要构建这部分字段参数, 最好在数据库当中创建好预下单的订单ID(cp_order_id), 后续支付完成会自动回调通知.

这里面字段除了必要支付参数, 更重要的是 passage 字段, 代表了客户端发起的支付通道需要对应第三方SDK, 响应内容如下:

参数名 参数类型 参数说明
appid string 游戏在应用后台生成的标识
time number 发行方响应的毫秒级UTC时间戳, 仅仅作为数据噪音功能
passage string 发起指定的第三方支付通道, 由发行方定义的支付标识
order_id string 发行方在数据库生成的订单号, CP方需要保存数据库记录
cp_order_id string CP方生成的订单标识
extra object 采用 {“xxx”:“yyy”} 数据对象, 唤起支付通道所需的参数对象组, 也就是第三方SDK支付所需的内容都放置在其中

按照支付通道不同, extra 内部的结构也有所不同, 基本上没什么太大问题.

支付回调 - 服务端

当第三方支付通道完成订单结算的时候, 发行方就会发通知给研发方订单已经支付成功, 一般回调地址在发行后台配置, 响应结构如下:

参数名 参数类型 参数说明
appid string 游戏在应用后台生成的标识
time number 发行方响应的毫秒级UTC时间戳, 仅仅作为数据噪音功能
trace string 下发的链路渠道标识, 后续所有行为都要提交该标识, 用于做用户的数据归因
order_id string 发行方的订单号
cp_order_id string CP方生成的订单号
uid string 发行方的用户唯一ID
item_id string CP方的商品道具名
item_price number CP方的商品道具单价, 注意这里采用分/美分为单位
item_count number CP方的商品道具购买数量, 道具最终价格等于 price * count
extension string CP方生成订单时候的扩展字符(最大长度64字符)
currency string 货币结算单位: CNY/USD
country string 国家地区标识: CN/US
sign string 以上参数名按照KEY正序排列, sign = md5(正序排列参数+后台生成 app secure)

注意: 支付参数签名验证的 sign 使用的不是 appKey, 而是额外的 appSecure 验证, 涉及支付相关采用 appSecure 而非 appKey

研发方获取到该支付回调之后最好做订单查询验证, 避免被嗅探到回调入口导致伪造回调数据从而绕过验证机制.

订单验证 - 服务端

SDK 发起支付完成之后默认返回生成的 order_id 等扩展参数, 后续防止游戏研发方回调地址和密钥泄露导致被恶意回调,
游戏研发方在收到回调的时候请求查询订单接口从而确认该订单在发行平台是否完成支付, 发起请求的参数如下:

参数名 参数类型 是否为必须 参数说明
appid string 游戏在应用后台生成的标识
time number 发起请求的时间, 采用 UTC 毫秒时间戳, 服务端检测请求延迟必须要在一分钟之内
order_id string 玩家在发行后台生成订单ID
sign string 需要对以上参数名按照KEY正序排列, sign = md5(正序排列参数+后台生成 app secure)

这部分订单查询只需要返回状态和附加的其他信息, 方便后续发行方和研发方对账:

参数名 参数类型 参数说明
appid string 游戏在应用后台生成的标识
time number 发行方响应的毫秒级UTC时间戳, 仅仅作为数据噪音功能
trace string 下发的链路渠道标识, 后续所有行为都要提交该标识, 用于做用户的数据归因
order_id string 发行方的订单号
cp_order_id string CP方生成的订单号
uid string 发行方的用户唯一ID
item_id string CP方的商品道具名
item_price number CP方的商品道具单价, 注意这里采用分/美分为单位
item_count number CP方的商品道具购买数量, 道具最终价格等于 price * count
extension string CP方生成订单时候的扩展字符(最大长度64字符)
currency string 货币结算单位: CNY/USD
country string 国家地区标识: CN/US
status number 订单目前的状态, 确认是否等待回调完成, 如果已经是回调完成的状态就跳过后续业务处理

不过也有大部分游戏发行方没有用该接口查询, 只判断自己数据库内部数据库存在状态写入变化状态, 该接口仅作为建议接入并不是必要的

数据上报 - 客户端

当用户指定行为被触发就需要调用 SDK 来上报对应事件, 上报的结构行为标识需要自定义处理:

事件ID 事件名称 是否必须
100 选择服务器
101 创建角色
102 进入游戏
103 等级提升
其他事件ID… 其他事件名称…

而其中上报的结构体就如下所示:

参数名 参数类型 是否必须 参数说明
appid string 请求的应用ID凭据, 有的会将应用ID设置为纯数值,有的采用随机字符串标识
time number 发起请求的时间, 采用 UTC 毫秒时间戳, 服务端检测请求延迟必须要在一分钟之内
trace string 下发的链路渠道标识, 后续所有行为都要提交该标识, 用于做用户的数据归因
uid string 玩家在发行后台生成的唯一标识
token string 玩家在发行后台生成的会话凭证
action number 事件上报的ID, 去上面的事件表ID传入
sid string 角色所在区服id(最大长度64字符)
sname string 角色所在区服名称(最大长度64字符)
role_id string 玩家在指定服务器当中的角色ID(最大长度64字符),选择服务器时候不存在角色传入空字符串
role_name string 玩家在指定服务器当中的角色名(最大长度64字符),选择服务器时候不存在角色传入空字符串
role_balance string 玩家游戏币余额,选择服务器时候不存在角色传入空字符串
role_level string 玩家在指定服务器当中的角色等级,选择服务器时候不存在角色传入空字符串
role_power string 玩家在指定服务器当中的角色战力,选择服务器时候不存在角色传入空字符串
role_gender number 按照指定值来选择: 0 = 无性别要素,1 = 男,2 = 女, 3 = 非二元性别,选择服务器时候不存在角色传入空字符串
role_vip string 玩家在指定服务器当中的VIP等级,选择服务器时候不存在角色传入空字符串
role_create_time number 角色创建时间戳, 以毫秒级UTC时间戳为单位,选择服务器时候不存在角色传入0
role_level_up_time number 角色等级变化时间, 以毫秒级UTC时间戳为单位,选择服务器时候不存在角色传入0
profession_id string 玩家在游戏之中的职位ID(最大长度64字符), 没有该要素可以不传入或者留空
profession_name string 玩家在游戏之中的职位名称(最大长度64字符), 没有该要素可以不传入或者留空
guild_id string 玩家工会ID(最大长度64字符), 没有该要素可以不传入或者留空
guild_name string 玩家工会名(最大长度64字符), 没有该要素可以不传入或者留空
guild_master_id string 玩家工会会长的角色ID(最大长度64字符), 没有该要素可以不传入或者留空
guild_master_name string 玩家工会会长的角色名(最大长度64字符), 没有该要素可以不传入或者留空
sign string 需要对以上参数名按照KEY正序排列, sign = md5(正序排列参数+后台生成appkey)

服务端处理这个接口一定要避免数据库写入插入, 而是应该先写入 Redis 之类的缓存层然后用脚本异步保存到数据!

血泪教训, 如果不做异步处理直接写入数据库, 可能用户量爆发的时候会导致直接卡死影响客户端其他业务, 并发压力最大的接口之一

上报之后不需要响应太多数据, 直接返回是否完成的状态即可:

参数名 参数类型 参数说明
appid string 游戏在应用后台生成的标识
time number 发行方响应的毫秒级UTC时间戳, 仅仅作为数据噪音功能
trace string 下发的链路渠道标识, 后续所有行为都要提交该标识, 用于做用户的数据归因
status number 默认返回 0 - 成功, 某些情况下可能上报失败, 需要返回客户端记录日志

上报数据是最需要优化的点, 因为都是实打实的数据入库, 并且随着用户量上升数据量也是指数上升, 但是运营做分析统计又不得不需要采用.

这部分最好采用分表机制, 甚至数据量到达一定程序就要抛弃传统数据库改由 Kafka 之类做日志清洗统计

授权注销 - 客户端

当玩家在SDK的悬浮窗或用户中心去退出游戏账号, 渠道发行方去注销目前登录的账号 token 会话, 提交参数如下:

参数名 参数类型 是否必须 参数说明
appid string 请求的应用ID凭据, 有的会将应用ID设置为纯数值,有的采用随机字符串标识
time number 发起请求的时间, 采用 UTC 毫秒时间戳, 服务端检测请求延迟必须要在一分钟之内
trace string 下发的链路渠道标识, 后续所有行为都要提交该标识, 用于做用户的数据归因
uid string 玩家在发行后台生成的唯一标识
token string 玩家在发行后台生成的会话凭证
sign string 需要对以上参数名按照KEY正序排列, sign = md5(正序排列参数+后台生成appkey)

注销不需要太多花哨的返回, 直接确认是否注销成功即可:

参数名 参数类型 参数说明
appid string 游戏在应用后台生成的标识
time number 发行方响应的毫秒级UTC时间戳, 仅仅作为数据噪音功能
trace string 下发的链路渠道标识, 后续所有行为都要提交该标识, 用于做用户的数据归因
status number 默认返回 0 - 成功, 某些情况下可能注销失败, 需要返回客户端记录日志