最近准备重构和整理发行平台项目, 结合项目中的问题边思考和边总结开发当中的流程和问题, 便于后续查漏补缺.
首先对于发行平台核心总共分为以下部分:
- 后台管理
- SDK对接
- 静态渲染
后台管理
对于发行后台一般提供和设置以下参数:
- appid: 生成的游戏应用ID
- appKey: 游戏接入的登录时候需要用到
- appSecret: CP回调的时候验证回调参数完整性的参与签名
- clientId: 客户端ID(H5应用不需要)
- clientKey: 客户端Key(H5应用不需要)
- notifyUrl: 支付完成需要通知CP的回调地址
SDK对接
SDK对接目前仅有 H5 相关内容, 服务端需要处理以下接口来对接:
- 登录认证(需要CP文档对接,必要)
- 支付唤起(需要CP文档对接,按照游戏性质区分, 有的免费游戏不需要)
- 订单状态(需要CP文档对接,用于CP方确认订单状态)
- SDK初始化(事件上报,必要)
- 选择服务器(事件上报,非必要)
- 创建角色(事件上报,必要)
- 进入游戏(事件上报,必要)
- 等级提升(事件上报,必要)
- 退出游戏(事件上报,按照游戏性质区分,H5没有退出相关功能可以忽略)
选择服务器的时候没有角色信息, 所以上报的涉及的玩家和角色相关信息可以留空或者默认值
实际上可以区分为两部分: 研发联调 和 事件上报
事件上报是很关键的接口, 用于追踪玩家行为的转化率和在线留存等信息
研发联调
研发(CP)联调基本上只涉及授权和支付而已, 结构上报为统一的 JSON|FORM 格式, 内部字段如下:
// 为了简洁, 对于默认不存在账号会自动在系统帮助创建注册, 省下了手动切换注册页面输入的流程
// 以下就是涉及到需要上报的注册|登录信息
{
// 其他必要的登录参数
// 有的第三方可能参数有所不同
// ---------------------------
// 后台创建的游戏应用ID
appid: 1001,
// 对应的推广渠道ID, 该渠道ID就是后台推广员绑定的账号ID
// 用于将玩家绑定专属的渠道, 也就是相同游戏会被分包让其玩家归属到指定后台运营管理员
channel: "100002",
// 登录平台, 有时候采用跨平台(Android|iOS|H5)登录, 两者平台都是同个账号
platform: 0,
// 设备唯一标识码, H5无法获取到会默认留空
imei: "",
// 设备信息上报, 比如 AndroidOS | AppleOS 等
device: "",
// 设备UA信息上报, 比如 Mozilla/5.0 (Linux; Android 6.0; Nexus 5)...
ua: "",
// 额外的提交扩展参数
extra: "",
// 参数签名方法, 该字段不需要参与签名
sign: "",
}
// ------------------------------------------------------------------
// 登录之后响应数据架构
{
// 发行方(我们)平台生成的账号UID
uid: 10001,
// 发行方(我们)平台注册的账号名称
username: "meteorcat",
// 发行方对当前账号会话颁布的授权
token: "",
// 相应的服务器时间戳, 以秒级作为单位
time: 1752460652,
// 作为第三方返回的渠道ID, 比如如果采用微信第三方登录, 那么这里可能就是微信的账号唯一标识
// 注意: 这个所谓的唯一标识可能并不是唯一, 因为其他多个第三方唯一标识可能出现碰撞问题
sdk_id: "",
// 作为第三方返回的账号名, 不过有的第三方返回的名称可能为空
sdk_username: "",
// 是否为新创建的账号: 0 - 否, 1 - 是
// 默认不存在的账号是直接帮助其注册, 所以需要按照这个字段判断是否为新注册玩家
new_account: 1,
// 扩展参数, 有的渠道会有二次客户端认证, 所以需要返回一些信息用于客户端再次校验
extra: "",
}
注意: 登录接口比较复杂, 常规来说除了普通账号密码登录, 还有手机注册|OAuth注册(同时还有H5和客户端)的差别
登录参数授权需要做签名验证, 需要用到后台生成的 appKey 来参与哈希签名, 这里按照 php 来做示例:
<?php
$appid = 10; // 后台生成的应用ID
$key = "ThisIsAppKey";// 后台生成的登录KEY
// 假设提交的订单参数列表
$data = [
"appid" => $appid,
"username" => "meteorcat",
"password" => "meteorcat_password"
// 其他参数略
];
// 需要数组按照KEY列表正序
// 最后的KEY顺序: {"appid":10,"password":"meteorcat_password","username":"meteorcat"}
ksort($data);
// 生成 XXX=YYY&AAA=BBB 格式字符串
$hash = http_build_query($data);
$hash = urldecode($hash);// PHP默认会将其做URL转义, 需要还原回来
// 支付KEY一起参与签名
// 生成签名: ea59bdf866dbdf1df62a4aaf759e12bd
$sign = md5("{$hash}{$key}");
// 把签名随着给发行方(我们)启动支付参数提交
$data['sign'] = $sign;
// 最后就能得到启动支付的参数列表
// {"appid":10,"password":"meteorcat_password","username":"meteorcat","sign":"ea59bdf866dbdf1df62a4aaf759e12bd"}
支付联调其实也是差不多, 只是支付的接口 CP 只需要唤起我们的支付并且拿到我们对应的支付通道唤起支付:
// 启动第三方支付请求
{
// 作为发行方(我们), 内部的游戏应用ID
// 注: 用于关联玩家角色和账号相关, 必须验证好存在其账号UID
appid: 10,
// 作为发行方(我们), 内部的账号ID
// 注: 用于关联玩家角色和账号相关, 必须验证好存在其账号UID
uid: 10001,
// 玩家所在的服务器ID, 不存在分服可以留空
sid: "",
// 玩家所在的服务器名称, 不存在分服可以留空
sname: "",
// 玩家在指定服务器当中的角色ID
// 注意: 采用字符串是因为有的CP方采用 UUID 做角色区分, 所以要做好预留机制
role_id: "100001",
// 玩家在指定服务器当中的角色名
role_name: "MeteorCat",
// 玩家在指定服务器当中的角色等级
// 如果游戏不存在等级就直接采用空字符串
role_level: "",
// 玩家VIP等级, 有的游戏带有充值VIP等级差别
// 如果没有这方面需求可以将VIP等级传递空字符串
role_vip: "",
// CP支付的商品ID, 必须采用字符串, 因为有时候第三方的道具物品是个很复杂的结构
item_id: "1000_20",
// CP支付的商品名, 有的支付渠道用来展示商品名称
item_name: "20金币",
// CP支付的商品详情, 有的支付渠道用来展示商品详情
item_desc: "",
// 支付的金额, 注意这里采用分|美分为单位, 对于元|美元需要手动去转回
price: 2000,
// 支付的货币单位, 有的支付渠道需要传递支付单位, 海外的时候需要对其做记录
// USD - 美元, CNY - 人民币
currency: "USD",
// 支付的地区, 用的支付渠道必须带有地区, 海外的时候需要对其做记录
// US - 美国, CN - 中国
country: "US",
// 支付回调地址, 用于客户端自定义让发行到账后的回调地址
// 注意: 后台也有配置回调地址, 默认 notify_url 参数不传递的时候会在第三方回调之后去通知后台地址
// 注意: 如果配置 notify_url 参数则是默认优先让其通知该地址而非后台配置地址
notify_url: "",
// CP数据库当中生成的订单号(最大长度64字符), 用于回调给CP去关联对应订单号
cp_order_id: "",
// CP自己提交过来的扩展字符(最大长度64字符), 会原样返回CP让其自己去识别处理
extra: "",
// 参数签名, 这个参数不参与加密, 用于验证提交参数完整性
sign: "",
}
// ----------------------------------------------------------------------------------
// 而响应格式数据也是比较简单
{
// 应用ID
appid: 10,
// 玩家用户ID
uid: 10001,
// 发行方数据库内部的订单ID, 注意需要和 cp_order_id 区分
// cp_order_id = CP方自己数据库的订单ID, order_id = 我们作为发行方的唯一订单ID
order_id: "",
// 唤起第三方SDK的参数
extension: {
wx: {
// 微信支付的对应支付跳转链接等信息参数
},
google: {
// 谷歌对应的唤起的GooglePlay钱包参数
}
}
}
需要注意: 支付的订单参数需要做签名处理, 一般默认签名方法直接采用 对象KEY正序 + appSecret 哈希即可.
这里采用 php 简单演示签名过程:
<?php
$appid = 10; // 后台生成的应用ID
$secret = "ThisIsAppSecret";// 后台生成的支付KEY
// 假设提交的订单参数列表
$data = [
"appid" => $appid,
"uid" => 10001,
"cp_order_id" => "20250714141300"
// 其他参数略
];
// 需要数组按照KEY列表正序
// 最后的KEY顺序: {"appid":10,"cp_order_id":"20250714141300","uid":10001}
ksort($data);
// 生成 XXX=YYY&AAA=BBB 格式字符串
$hash = http_build_query($data);
$hash = urldecode($hash);// PHP默认会将其做URL转义, 需要还原回来
// 支付KEY一起参与签名
// 生成签名: 10f2a0ef17b5a0b318df4d1d89a9e969
$sign = md5("{$hash}{$secret}");
// 把签名随着给发行方(我们)启动支付参数提交
$data['sign'] = $sign;
// 最后就能得到启动支付的参数列表
// {"appid":10,"cp_order_id":"20250714141300","uid":10001,"sign":"10f2a0ef17b5a0b318df4d1d89a9e969"}
后续这种方法做哈希签名效率还行, 而且方法也比较通用, 基本上无需依赖太多的组件.
事件上报
初始化事件上报统一的 JSON|FORM 格式, 内部提交的字段就比较简单:
// 初始化的时候字段一般不多, 所以相对比较简洁
// 在后台查看到游戏应用ID和应用KEY,
{
appid: 10001,
appkey: "xxxxxxxxxxxx",
debug: 0, // 是否直接采用大规模日志测试记录, 用来提供给后台做接入测试的时候的参数追踪
}
// ----------------------------------------------------------------------------------
// 初始化上报事件需要返回以下的数据提供给CP方获取接收和渲染对应功能, 也就是上报之后返回的格式数据
// 1. 客服列表信息
// 2. 客户端IP地址
// 3. 可用的支付通道: wx,ali,google,paypal
// 4. SDK公告列表
// 5. 游戏名称
// 6. 游戏icon
// 7. 游戏详情
// 8. 游戏地址(客户端为下载地址,H5为游戏登录地址)
{
// 客服列表
service: [
{
"wx": "MeteorCat_WX1",
"qq": "11111111111",
},
{
"wx": "MeteorCat_WX2",
"qq": "22222222222",
}
],
// 客户端地址
ip_addres: "127.0.0.1",
// 可用的支付通道
payments: [
"wx",
"ali"
],
// SDK公告列表信息
notice: [
{
id: 1001,
title: "免责协议",
content: "hello.world"
}
],
// 游戏应用名
name: "测试游戏",
// 游戏ICON图片地址
icon: "",
// 游戏介绍或者详情
detail: "",
// 游戏下载|游玩地址
url: "",
}
核心事件上报为统一的 JSON|FORM 格式, 只是按照内部字段 active 做行为区分:
{
// 事件上报行为:
// 1 = 选择服务器
// 2 = 创建角色
// 3 = 进入游戏
// 4 = 等级提升
// 5 = 退出游戏
active: 1,
// 作为发行方(我们), 内部的游戏应用ID
// 注: 用于关联玩家角色和账号相关, 上报必须验证好存在其应用ID
appid: 10,
// 作为发行方(我们), 内部的账号ID
// 注: 用于关联玩家角色和账号相关, 上报必须验证好存在其账号UID
uid: 10001,
// 玩家所在的服务器ID, 不存在分服默认为空字符串
sid: "",
// 玩家所在的服务器名称, 不存在分服默认为空字符串
sname: "",
// 玩家在指定服务器当中的角色ID
// 注意: 采用字符串是因为有的CP方采用 UUID 做角色区分, 所以要做好预留机制
role_id: "100001",
// 玩家在指定服务器当中的角色名
role_name: "MeteorCat",
// 玩家的角色性别
// 注意: 为了针对海外发行情况比较复杂, 除了男女还有非二元性的问题, 所以可能有以下差别
// 0 = 游戏没有性别要素
// 1 = 男
// 2 = 女
// 3 = 非二元性别
role_gender: 0,
// 玩家在指定服务器当中的角色等级
// 如果游戏不存在等级就直接采用空字符串
role_level: "1",
// 角色创建时间戳, 以秒为区分
// 必须传入真实的创建角色时间戳, 有的第三方要求这部分数据做审核
role_create_time: 1752460652,
// 角色等级变化时间, 以秒为区分, 没有升级的游戏默认传递为0
// 用于跟踪玩家升级的统计, 来提供给CP作为玩法上面的优化
role_level_up_time: 0,
// 玩家VIP等级, 有的游戏带有充值VIP等级差别
// 如果没有这方面需求可以将VIP等级传递空字符串
role_vip: "",
// 玩家角色身上拥有的付费游戏币数量
// 比如充值1元=1钻石, 那么这种金额等值货币就直接需要提供过来做付费金额留存统计
// 之所以要采用字符串是因为有些CP会采用 1.0 浮点数, 这里直接采用字符串方便容错
money: "1000",
// 玩家在游戏之中的职位ID要素, 默认可以不传入或者留空
profession_id: "",
// 玩家在游戏之中的职位名称要素, 默认可以不传入或者留空
profession_name: "",
// 玩家战力点数, 没有该值可以不传或者留空字符串
power: "",
// 玩家工会ID, 没有该值可以不传或者留空字符串
guild_id: "",
// 玩家工会名, 没有该值可以不传或者留空字符串
guild_name: "",
// 玩家工会长的角色ID, 没有该值可以不传或者留空字符串
guild_master_id: "",
// 玩家工会长的角色名, 没有该值可以不传或者默认留空字符串
guild_matser_name: "",
// 最后提交的签名哈希, 不作为参数去参与加密
// 用来防止无意义的爬虫嗅探和接口破解
sign: ""
}
上面就是上报行为的相关字段, 在实际之中可以采用 JSON 或者 FORM 提交上来, 主要字段需要做签名处理:
// 签名流程伪代码
// 1. 字段格式化
$fileds = ['appid=50','uid=10001',...]; // 所有字段
// 2. 以 & 格式链接所有参数, 必须要以 key 正序排列
$params = 'appid=50&uid=10001...';
// 3. 以 MD5 方式哈希处理
$sign = md5($params);
// 4. 最后把 sign 作为参数一并提交
$fileds = ['appid=50','uid=10001',...,'sign=xxxxxxxxx'];
有的发行为了安全性考虑, 会把CP对接的加密方式和数据上报方式采用不一样方法, 具体按照自己需求