在商业游戏当中比较有以下基础角色:
游戏研发(Content Provider, 简称CP): 最基础的游戏开发底层游戏发行(Publisher, 商业发行): 游戏推广和商业计划执行者
在 Steam|WeGame|Epic 之类平台的平台也能看到不少相关信息, 当然也有研发和发行一体的商业公司.
游戏发行 涉及以下工作( 以下内容不分单机和手游 ):
- 海外|国内运营的本地文化和本地化处理, 需要按照不同地方规避文化民俗和联系语言本地化翻译(可能要联系其他海外发行合作)
- 海外|国内授权和支付体系接入, 需要暴露外部Web接口联系
游戏研发来接入处理 - 海外|国内广告平台接入, 需要
游戏发行的商务确认平台广告量级从而才能进行广告推送(Facebook|抖音贴片广告位) - 海外|国内运营客服的处理,
游戏研发不会直接和玩家对接, 而是玩家和客服反馈, 通过运营整合活动BUG联系策划出活动或者修复 - 海外|国内需要按照定下的推广指标, 通过
财务再按照商定的分账比例由游戏发行转账给具体的游戏研发
并且 游戏发行 还有部分 第三方 来处理, 也就是 游戏研发 没有余力搭建维护整套发行架构的时候就会去寻求 第三方发行.
比较知名的就是直接接入腾讯体系发行, 只需要接入对应内部体系并且联系他们推广就行, 不过分成比例也是很高
这里主要说下 第三方游戏发行 的开发和设计, 最基础的就是必须实现接口:
登陆请求请求支付支付回调
还有广告追踪转化SDK等, 这些附属的SDK比较高级和复杂所以不展开说明
注意: 如果作为 第三方游戏发行方, 你对接的是 游戏研发 的技术人员而非玩家用户.
# 常规登陆账号体系
[客户端] --> username|password --> [服务器] <---> 数据查询存在并生成授权token
# 第三方发行账号体系
[客户端] --> username|password --> [CP服务器]
|
|--> [appid, username, password] + 发行申请的 secretKey 哈希出 token
|
|--> [第三方发行服务器]
|
| --> 数据查询存在并生成授权token
|
|
如果存在账号返回信息, 不存在就创建账号入库
|
[CP服务器] <------------------------------ |
|
|
(可选步骤,有的CP方需要记录入库来保存, 也有的CP方不想维护独立服务器)
验证发行返回的账号是否在 CP 自己维护的数据库用户表存在, 不存在就创建
|
|
[客户端] <------------- 将发行的验证的授权凭证数据返回客户端
这种架构可以有效把职责独立出来, 发行方面不需要和研发做太多交互, 直接单独维护自己 API 服务就行;
对于 游戏发行, 你只需要提供给 游戏研发 以下数据:
appKey: 登陆授权的参与签名哈希处理的密钥appSecret: 创建订单的时候参与签名哈希处理的密钥
授权验证
这里提供我个人比较常用且基础的第三方授权机制, 基本上可以用于大部分发行情况:
- 请求方法:
POST(仅支持) - 参数方式:
Form(表单形式)
为什么仅支持 POST 方式, 不采用 GET|POST 混合?
主要除了 GET 数据可传输数据比 POST 小之外, 还有个问题就是转发日志安全性;
一般为了防止 DDOS 攻击会采用高防服务器过滤请求之后转发到最终目标服务器,
有些默认第三方服务器都会做本地请求日志把 GET 相关参数写入到本地服务器日志,
如果该服务器被攻破的时候能够看到大量 GET 请求参数带有 username|password 相关参数.
这里是考虑到安全因素, 防止默认 Web 服务记录 GET 参数日志, 如果对这些不太敏感也可以默认支持处理 GET 提交
为什么参数必须要
FormData 提交?
主要是因为 FormData 这种方式更加灵活, 不仅仅支持 String 还支持 Blob 提交,
有时候支持奇葩需要 OpenSSL 密钥证书等文件提交认证的情况, 传统的 application/json 适用面仅仅作为字符串处理.
这里也是为了适用性考虑, 如果能够确认仅有 JSON 字符串提交的话可以采用传统
RESTAPI方式
这里提供默认的提交参数 请求表单:
| 字段名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| appid | Long | 必须 | 在发行创建的应用ID |
| channel | Long | 必须 | 不同第三方渠道标识,比如 快手/抖音/微信/应用宝等 |
| time | Int | 必须 | 时间戳用于参与哈希防止碰撞, 使用秒级即可 |
| extension | JSON | 必须 | 第三方渠道所需的参数,sid,token,sessionId等不同渠道所需参数不同 |
| sign | String | 必须 | 数据签名串,按照除 sign 参数以 key 正序合并追加发行密钥来 md5 处理 |
这里加密方式提供个 PHP 示例可以直接查看 sign 哈希过程:
<?php
// 基础数据
$appid = 1001; // 应用ID
$appKey = "c53d93bafeb52c0be562613daef647de";// 参与哈希Key
$channel = 11; // 渠道标识
$time = 1747335102;// 秒级时间戳
// 渠道所需数据
$extension = json_encode([
"username" => "meteorcat",
"password" => "password"
]);
echo "extension: '{$extension}'".PHP_EOL;
// 合并成参数列表, appKey 不参与哈希
$params = [
'appid' => $appid,
'channel' => $channel,
'time' => $time,
'extension' => $extension
];
// 按照Key正序排序
ksort($params);
echo str_repeat("=",84).PHP_EOL;
var_dump($params);
echo str_repeat("=",84).PHP_EOL;
// 构建 Key1=Value1&Key2=Value2 形式
// 注意 http_build_query 会按照 url 方编码, 需要 urldecode 解码处理下
$encrypt = urldecode(http_build_query($params));
echo "encrypt: '{$encrypt}'".PHP_EOL;
echo str_repeat("=",84).PHP_EOL;
// 添加 appKey
$encryptWithKey = "$encrypt$appKey";
echo "encryptWithKey: '{$encryptWithKey}'".PHP_EOL;
echo str_repeat("=",84).PHP_EOL;
// 最后处理成 sign
$sign = md5($encryptWithKey);
echo "sign: '{$sign}'".PHP_EOL;
echo str_repeat("=",84).PHP_EOL;
// 最后合并到提交 form 表单参数
$params['sign'] = $sign;
var_dump($params);
echo str_repeat("=",84).PHP_EOL;
# 最后展示打印内容数据如下
# extension: '{"username":"meteorcat","password":"password"}'
# ====================================================================================
# array(4) {
# ["appid"]=>
# int(1001)
# ["channel"]=>
# int(11)
# ["extension"]=>
# string(46) "{"username":"meteorcat","password":"password"}"
# ["time"]=>
# int(1747335102)
# }
# ====================================================================================
# encrypt: 'appid=1001&channel=11&extension={"username":"meteorcat","password":"password"}&time=1747335102'
# ====================================================================================
# encryptWithKey: 'appid=1001&channel=11&extension={"username":"meteorcat","password":"password"}&time=1747335102c53d93bafeb52c0be562613daef647de'
# ====================================================================================
# sign: '5ad78915353f59639729cb535dd95fdf'
# ====================================================================================
# array(5) {
# ["appid"]=>
# int(1001)
# ["channel"]=>
# int(11)
# ["extension"]=>
# string(46) "{"username":"meteorcat","password":"password"}"
# ["time"]=>
# int(1747335102)
# ["sign"]=>
# string(32) "5ad78915353f59639729cb535dd95fdf"
# }
# ====================================================================================
以上就是一步步的构建生成签名的流程, 之后就是 响应表单 的参数 JSON:
{
// 其他都是不同对应类型失败, 400 为默认失败
"state": 200,
// 响应结构体, 如果非 200 响应就 null, 200 成功就是对象组
"data": {
// 作为发行生成的落地在数据库唯一id(long)
"uid": 10001,
// 作为发行生成的落地在数据库的用户名,比如 10001.wechat,10001.qq,10001.tiktok,....(格式:uid.渠道简称)
"username": "meteorcat",
// 第三方渠道验证成功返回的用户标识唯一ID(比如微信openid), 有的渠道需要第三方渠道这些信息
"sdkUid": "oaKk343WOktAaT2ygsX138BGblrg",
// 第三方渠道验证可能会返回在该平台的昵称, 用于设置默认的用户帐号名
"sdkUsername": "MeteorCat",
// 当前授权登陆的唯一凭证, 可以md5处理即可
token: "",
// 特殊的扩展字段, 有些平台可能后续会用到的参数可以放置其中用 JSON 返回
extension: "{}",
}
}
注意: 这里返回 token 就是在自己游戏发行平台在本次授权的凭证.
另外还需要提供 token 登陆接口, 用于客户端后续直接以 token 登陆方式保存本地不需要再次请求第三方渠道的情况:
| 字段名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| uid | Long | 必须 | 在发行的用户唯一ID |
| token | String | 必须 | 授权返回token字段 |
| time | Int | 必须 | 时间戳用于参与哈希防止碰撞, 使用秒级即可 |
| sign | String | 必须 | 数据签名串,按照除 sign 参数以 key 正序合并追加发行密钥来 md5 处理 |
这样好处就是不需要每次再去第三方渠道再去确认授权, 有的第三方渠道确认授权过多会将其设为风险行为
订单预下单
订单的创建流程其实也和登陆验证一致, 请求表单 参数如下:
| 字段名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| uid | Long | 必须 | 在发行的用户唯一ID |
| orderId | String | 必须 | 充值订单ID, 由CP方的自己服务器数据生成提供 |
| orderItem | String | 必须 | 充值订单道具标识, 比如 ‘104’ |
| orderName | String | 必须 | 充值订单道具名, 比如 ‘钻石’ |
| orderDesc | String | 必须 | 充值订单详情, 比如 ‘钻石 * 100’ |
| amount | Long | 必须 | 充值订单金额, 以分为单位 |
| roleId | String | 必须 | 玩家角色ID, 注意单个UID是可以对应多个角色Id,也就是同个账号可能在多个区服都账号 |
| roleName | String | 必须 | 玩家角色名称 |
| roleLevel | Long | 必须 | 玩家角色等级, 没有等级机制可以为0 |
| serverId | String | 必须 | 玩家角色所属服务器ID |
| serverName | String | 必须 | 玩家角色所属服务器名称 |
| extension | JSON | 必须 | 自定义字段, 支付回调将其原封不动回调给 CP 的服务器用于通知游戏服务器发放 |
| notifyUrl | String | 必须 | 支付订单到账的时候, 作为我们游戏发行需要回调的地址通知的地址, 可以不配置但需要在发行管理后台设置默认回调(优先采用参数而非后台定义) |
| time | Int | 必须 | 时间戳用于参与哈希防止碰撞, 使用秒级即可 |
| sign | String | 必须 | 数据签名串,按照除 sign 参数以 key 正序合并追加发行密钥来 md5 处理 |
这里
sign生成方式和登陆验证一样所以不做赘述.
游戏发行方订单不需要处理响应, 但是需要处理回调通知给游戏研发方订单已到账,
默认第三方支付回调之后需要标识好并按照 notifyUrl 通知以下 JSON 格式:
// 这里按照 notifyUrl 回调给 CP 方
{
// 200 代表成功, 其他为失败
"status": 200,
// 200 该字段为对象组, 其他为 null
"data": {
// 作为游戏应用的ID
"appid": 1001,
// 作为我们发行方的唯一标识ID
"uid": 10001,
// 第三方渠道标识, 比如 微信 = 10, QQ = 12, ....
"channel": 10,
// 发行游戏订单id, 需要区分 1.发行订单ID 2.第三方订单ID 3.CP订单ID
"sdkOrderId": 2025051603555501,
// 玩家角色所属服务器ID
"serverId": "1",
// 玩家购买的道具标识
"orderItem": "104",
// 玩家支付金额, 单位为分
"amount": 600,
// CP方请求推送过来的扩展参数原样返回, 用于 CP 自己去提取对应参数
extension: "{}",
// 用于CP验证用 appSecret 参数哈希匹配
sign: "1e20f28a33fcbb27ded35c9425e18aff",
}
}
这里 CP 方回调处理方式有几种处理方案:
- 发行支付回调的时候, 由
CP地址打印显示SUCCESS或者FAILED确认CP方已经接收到回调不需要再发回调请求 - 发行支付回调的时候,
CP接收到之后额外推送给发行另外验证接口, 告诉发行方已经接收到数据完成了
这两种方法按照自己需求选择就行了, 上面就是 作为开发 比较基础的 游戏发行 的处理流程,
高级还有广告投放的 归因统计, 后续有机会再单独说明.