网页支付 — PHP SDK
| 适用于 | 使用 PHP 后端的联合运营(标准)或独家/直接运营(Expub)游戏 |
| 包 | Packagist 上的 alo-game/paymentsdk |
| 最新版本 | 2.2.0(更新日志) |
| 要求 | PHP ≥ 8.1 |
| 实现 | 完整实现网页支付 — 标准(目前仅有英文版)契约——外加网页支付 — Expub(目前仅有英文版)(包含 Expub 共用的移动端内购(目前仅有英文版)) |
本页包含在 PHP 中集成 WebPay 所需的全部信息。你不需要先阅读网页支付 — 标准或网页支付 — Expub(两页均目前仅有英文版)——那两页描述的是团队手写自己接口时所依据的原始 HTTP 契约。这个 SDK 已经替你实现了两套契约:每次调用都保证字段名正确、签名方案正确、响应格式正确。你只需要编写少量 PHP 函数,永远不必直接接触 HTTP、签名或 JSON。
这个包提供两个相互独立的模块——按你游戏的运营模式二选一,而不是默认两个都装:
- WebPay(见下文)——面向联合运营(co-pub)游戏,三个钩子,HMAC-SHA256。
- Expub(见下文)——面向独家/直接运营(expub)游戏,四个钩子,MD5 或 HMAC-SHA256(取决于你的游戏在 Console 中的配置),同时也覆盖该游戏的移动端内购。
无论你适用哪个模块,底层用的都是对应运营模式下所有游戏使用的同一套契约——同样的调用,同样的报文格式。这个 SDK 只是让你不必再手写那套报文格式。
SDK 对比手写契约
| 手写这套报文契约 | 本 SDK | |
|---|---|---|
| 签名、时间戳校验 | 自己实现、自己测试 | WebpayHandler/ExpubHandler 已经实现好,已经测试过 |
字段名(plat_order_num、order_id、productid/productId、……) | 照着文档手动敲——这里一旦悄悄写错,往往要等到真实玩家的订单失败了才会暴露 | 有类型的 DTO 属性($request->orderId、……)——写错字段名会是 PHP 的致命错误,而不是上线后才发现的静默不一致 |
响应格式(errcode 报文体 vs. HTTP 状态码) | 按每套契约自己拼出完全一致的结构 | CheckUidResult/CreateOrderResult/PaymentReceivedResult 替你构建 |
| 你需要写的业务逻辑 | 全部:解析、签名、响应格式,加上你自己的玩家/订单查询 | 只有你自己的玩家/订单查询——各个 on... 钩子 |
| 把调用路由到处理方法 | 两种方式都一样——PHP 没有运行时路由器供包挂载 | 两种方式都一样 |
| 幂等性(可安全重试) | 两种方式都是你的责任——SDK 无法知道"已经发货"在你的数据库里意味着什么 | 两种方式都是你的责任 |
这个 SDK 消除的是纯转录风险的部分——签名方案、字段名、响应格式——而对那些原本就不属于机械劳动的部分不做改变:你自己的数据库查询,以及哪条路由分发到哪个处理方法。它让你需要做对的这部分表面更小、更有类型检查,而不是让你需要思考的代码量变少。
我该用哪个模块?
规则与契约选择表(目前仅有英文版)一致:取决于你游戏的运营模式,而不是接入时间的早晚。
- 联合运营(co-pub)游戏 → WebPay 模块。玩家手动输入自己的游戏内 UID,不涉及任何 Alogame 关联账号。
- 独家/直接运营(expub)游戏 → Expub 模块,同时覆盖它的网页充值页面和移动端内购——玩家此时已经登录了自己的 Alogame 账号,这正是 Alogame 能直接拉取角色列表、而不需要玩家手动输入 UID 的原因。
一个游戏只会是其中一种,不会两者都是——如果不确定该用哪个,写代码之前先问你的 Alogame 运营对接人。
WebPay 模块(网页支付 — 标准)
流程
以下每一次调用都由 Alogame 发起。玩家的充值动作从 Alogame Portal 开始——你的服务器从不主动调用 Alogame,只负责响应。
请求与响应参考
以下内容是实际在线路上传输的数据——SDK 会替你完成全部构建与解析,但了解其结构有助于你排查日志或与 Alogame 运营对接排查不一致的地方。每次调用都是 POST,已签名(x-timestamp + x-signature,参见身份认证,目前仅有英文版),且必须在 8 秒内响应——响应过慢会被视为失败,对 onPaymentReceived 而言意味着会触发重试(见下文)。
onCheckUid — 玩家在充值页面输入 UID
报文接口: check_uid
CheckUidRequest 属性 | 报文字段 | 类型 | 说明 |
|---|---|---|---|
$uid | uid | string | 玩家输入的游戏内角色 UID |
$serverId | server_id | string|null | 仅当你实现了 ServerListHookInterface 时才会出现 |
$gameId | game_id | string|null | 仅当 Alogame 将该合作方拆分为多个商店条目时才会出现(较少见——见下方 ServerInfo/平台说明) |
{ "uid": "100002078" }
返回 CheckUidResult::found($nickname, $server) 或 CheckUidResult::notFound()。SDK 会替你把它转换成对应的报文响应:
// 找到
{ "errcode": 0, "msg": "success", "data": { "nickname": "DragonSlayer", "server": "Server 1" } }
// 未找到
{ "errcode": 1, "msg": "user not found" }
Alogame 判断角色是否缺失,靠的是对 msg 做不区分大小写匹配,只要包含子串 "not found" 或 "not exist" 即可——其他任何内容都会作为通用系统错误展示给玩家,而不是"请检查你的 UID"。CheckUidResult::notFound() 已经返回了符合规则的措辞,你永远不需要自己写这个字符串。
onGetCharacterList — UID 校验通过,玩家选择角色(可选)
报文接口: get_character_list
需要 SDK ≥ 2.1.0——如果你还在 2.0.x,请先执行 composer update alo-game/paymentsdk,否则下面这个接口并不存在。
只有当你的钩子对象实现了 CharacterListHookInterface 时才会被调用——如果在你的游戏里一个 UID 只对应一个角色,可以完全跳过这一节。与 ServerListHookInterface 相互独立:可以只实现其中一个、两个都实现,或者都不实现。
GetCharacterListRequest 属性 | 报文字段 | 类型 | 说明 |
|---|---|---|---|
$uid | uid | string | onCheckUid 已经校验通过的 UID——角色列表按它来划分 |
$serverId | server_id | string|null | 仅当你同时实现了 ServerListHookInterface 时才会出现 |
$gameId | game_id | string|null | 较少见——见上方 onCheckUid 的说明 |
{ "uid": "100002078", "server_id": "1" }
返回一个 CharacterInfo 数组,SDK 会替你转换成对应的报文响应:
{
"errcode": 0,
"data": [
{ "characterId": "c-1001", "name": "DragonSlayer" },
{ "characterId": "c-1002", "name": "IronMage" }
]
}
这里没有需要上报的失败情形,原因和 onGetServerList 一样:不存在的 UID 已经在 onCheckUid 阶段被拒绝了,而一个有效 UID 返回空数组是完全合法的答案(玩家只是看不到角色选择而已)。玩家选中的 characterId 会随下一次 onCreateOrder 调用,以 $request->characterId 的形式传给你。
onCreateOrder — 玩家确认了礼包,尚未进入支付界面
报文接口: create_order
CreateOrderRequest 属性 | 报文字段 | 类型 | 说明 |
|---|---|---|---|
$orderId | plat_order_num | string | Alogame 自己的订单编号——请存下来,onPaymentReceived 会用它作为关联键找回这笔订单 |
$uid | uid | string | 与 onCheckUid 已经验证过的同一个 UID |
$productId | productid | string | 你自己的商品 ID,与提交给 Alogame 时完全一致 |
$amount | amount | integer | 订单总额,单位 VND,不含小数 |
$sandbox | sandbox | integer(0/1) | 开发/预发布环境为 1,生产环境为 0——由 Alogame 根据环境自动设置,不是你需要配置的项 |
$serverId | server_id | string|null | 仅当按服务器划分时才会出现 |
$characterId | character_id | string|null | SDK ≥ 2.1.0。 仅当你实现了 CharacterListHookInterface 时才会出现——玩家选中的角色。请发放给这个角色,而不是仅凭 $uid 推断出来的角色 |
$gameId | game_id | string|null | 较少见——见 onCheckUid 下方的说明 |
{
"uid": "100002078",
"plat_order_num": "260509161539010042",
"productid": "pack_500_gold",
"amount": 49000,
"sandbox": 0
}
返回 CreateOrderResult::created($yourOwnOrderCode)。$yourOwnOrderCode 是必填的——SDK 的报文响应里始终会带上它:
{ "errcode": 0, "msg": "success", "data": { "order_num": "PARTNER-ORD-20260315-001" } }
$request->orderId 去重如果 Alogame 重试了 create_order(网络抖动、超时),同一个 plat_order_num 会到达两次。此时请返回与第一次相同的 order_num——不要为同一个 plat_order_num 重复创建订单。
onPaymentReceived — 支付渠道已确认收款,立即发放道具
报文接口: payment_notify
PaymentReceivedRequest 属性 | 报文字段 | 类型 | 说明 |
|---|---|---|---|
$orderId | plat_order_num | string | Alogame 的订单编号,与 onCreateOrder 中的值相同 |
$orderCode | order_num | string | 你自己的订单编号,即 onCreateOrder 返回的那个 |
$amount | amount | integer | 订单总额,单位 VND |
$gameId | game_id | string|null | 较少见——见 onCheckUid 下方的说明 |
报文体里还带有一个 status 字段(恒为 1,表示"支付成功")——SDK 没有把它暴露在 PaymentReceivedRequest 上,因为目前它没有其他取值。
{
"plat_order_num": "260509161539010042",
"order_num": "PARTNER-ORD-20260315-001",
"amount": 49000,
"status": 1
}
发货完成后返回 PaymentReceivedResult::ok(),失败则返回 PaymentReceivedResult::failed($reason):
// ok
{ "errcode": 0 }
只要不是 PaymentReceivedResult::ok()——包括你的接口超时——都会触发重试。Alogame 最多会重试 4 次总共:首次调用,之后分别在 60 秒、120 秒、120 秒后重试。
请以 $request->orderCode(或 $request->orderId)为依据匹配,对已经发货过的订单返回 ok()——永远不要重复发放道具,也永远不要对一个已完成的订单返回 failed(),否则重试循环会一直针对一个实际已经完成的订单继续跑下去。
前置条件
写代码之前,先向你的 Alogame 运营对接人获取:
- 一个共享的 HMAC
secret_key——这是本模块实际使用的唯一凭证(直接传入WebpayHandler的构造函数)。game_code本身从不出现在任何请求或响应体中,SDK 也没有对应的参数——把你的secret_key/base URL 与某个游戏关联起来的,是 Alogame 自己的 Console 配置,而不是你发送的任何内容。 - 哪个环境(开发/预发布 还是 生产)会最先调用你
1. 安装
公开包,无需任何凭证:
composer require alo-game/paymentsdk
源码:github.com/alo-game/alogame-paymentsdk-php(MIT 许可,更新日志)。
2. 实现三个钩子
这是你唯一需要实现的接口。是 Alogame 调用这些方法——你永远不会调用 Alogame:
use Alogame\PaymentSdk\WebPay\Contracts\WebpayHookInterface;
use Alogame\PaymentSdk\WebPay\Dto\{
CheckUidRequest, CheckUidResult,
CreateOrderRequest, CreateOrderResult,
PaymentReceivedRequest, PaymentReceivedResult,
};
final class MyGameHooks implements WebpayHookInterface
{
public function onCheckUid(CheckUidRequest $request): CheckUidResult
{
$player = MyPlayerRepository::findByUid($request->uid);
return $player
? CheckUidResult::found($player->nickname, $player->serverName)
: CheckUidResult::notFound();
}
public function onCreateOrder(CreateOrderRequest $request): CreateOrderResult
{
// $request->orderId 是 Alogame 自己的引用编号(报文里的 plat_order_num)
// ——存下来,onPaymentReceived 需要靠它找回这笔订单。
$order = MyOrderRepository::create(
alogameOrderId: $request->orderId,
uid: $request->uid,
productId: $request->productId,
amount: $request->amount,
);
return CreateOrderResult::created($order->id);
}
public function onPaymentReceived(PaymentReceivedRequest $request): PaymentReceivedResult
{
$order = MyOrderRepository::findByOwnId($request->orderCode);
if ($order === null) {
return PaymentReceivedResult::failed('unknown order');
}
MyInventory::deliver($order->uid, $order->productId);
return PaymentReceivedResult::ok();
}
}
onPaymentReceived 在未返回 ok 时(包括超时)最多会被重试 4 次——具体时间表和匹配依据见上方请求与响应参考。onCreateOrder 也有同样的要求:带有相同 $request->orderId 的重复调用必须返回与第一次相同的 orderCode,而不是创建第二笔订单。
3. 接好路由
Alogame 调用的是三个不同的 URL,不是一个——handleCheckUid、handleCreateOrder、handlePaymentReceived 各自响应不同的调用,所以必须有代码来判断每个进来的请求该调用哪一个。这个 SDK 刻意做成了框架无关(它并不知道 Laravel 的路由器,只会做一次原始的 $_SERVER['REQUEST_URI'] 判断),所以这一步路由判断的管线工作留给了你——下面每个分支内部的处理,调用的方法早已帮你完整实现好了。这里展示的是纯 PHP 写法;在 Laravel/Symfony 里 match 会消失,变成三条路由定义,各自调用同一个处理方法:
use Alogame\PaymentSdk\WebPay\WebpayHandler;
$handler = new WebpayHandler(
secret: getenv('ALOGAME_WEBPAY_SECRET'),
hooks: new MyGameHooks(),
onError: fn (\Throwable $e) => error_log($e), // 绝不让 bug 泄漏到线路上
);
// 各自接到自己的路由——对原始 body 签名,永远不要对重新解析后的 body 签名
// (与其他所有契约同样的规则,见身份认证页面)。
$response = match (true) {
$_SERVER['REQUEST_URI'] === '/webpay/check-uid' => $handler->handleCheckUid(getallheaders(), file_get_contents('php://input')),
$_SERVER['REQUEST_URI'] === '/webpay/create-order' => $handler->handleCreateOrder(getallheaders(), file_get_contents('php://input')),
$_SERVER['REQUEST_URI'] === '/webpay/payment-received' => $handler->handlePaymentReceived(getallheaders(), file_get_contents('php://input')),
};
http_response_code($response->status);
header('Content-Type: application/json');
echo json_encode($response->body);
4. 可选:多服务器
只有当你的游戏运行多个服务器、玩家充值前必须先选择一个服务器时才需要。实现第二个接口即可——Alogame 会自动识别,不需要另外配置:
use Alogame\PaymentSdk\WebPay\Contracts\ServerListHookInterface;
use Alogame\PaymentSdk\WebPay\Dto\{GetServerListRequest, ServerInfo};
final class MyGameHooks implements WebpayHookInterface, ServerListHookInterface
{
// ...onCheckUid / onCreateOrder / onPaymentReceived 同上...
public function onGetServerList(GetServerListRequest $request): array
{
return array_map(
static fn ($server) => new ServerInfo($server->id, $server->name),
MyServerRepository::all(),
);
}
}
再接一条第四路由到 $handler->handleGetServerList(...)。如果你的游戏只有一个共享服务器,可以完全跳过这一节。
5. 可选:一个 UID 对应多个角色
需要 SDK ≥ 2.1.0。 只有当你的一个 UID 可能拥有多个角色、玩家必须选择由哪个角色接收充值时才需要。与第 4 步相互独立——可以只实现其中一个、两个都实现,或者都不实现。识别机制相同:Alogame 看的是接口,而不是某个配置开关。
use Alogame\PaymentSdk\WebPay\Contracts\CharacterListHookInterface;
use Alogame\PaymentSdk\WebPay\Dto\{CharacterInfo, GetCharacterListRequest};
final class MyGameHooks implements WebpayHookInterface, CharacterListHookInterface
{
// ...onCheckUid / onCreateOrder / onPaymentReceived 同上...
public function onGetCharacterList(GetCharacterListRequest $request): array
{
return array_map(
static fn ($character) => new CharacterInfo($character->id, $character->name),
MyCharacterRepository::forUid($request->uid, $request->serverId),
);
}
}
再接一条路由到 $handler->handleGetCharacterList(...)。玩家选中的值会随同一次 onCreateOrder 调用,以 $request->characterId 传给你——请发放给那个角色。如果在你的游戏里一个 UID 只对应一个角色,可以完全跳过这一节。
6. 接好健康检查
再接一条路由——排在第 4、5 步中你实际需要的那些之后:
$response = $handler->handleHealthCheck(getallheaders(), file_get_contents('php://input'));
Alogame 可以随时调用它(已签名,与其他调用一样)来确认你的接口可访问,并且存档的密钥仍然匹配。请把这个路径连同其他路径一起交给 Alogame 运营。
7. 把接口地址给 Alogame
按环境把以下信息发给 Alogame 运营:
- Base URL + 四个必需路径(每个可选的选择器各再加一个:服务器列表、角色列表)
- 确认前置条件中的
secret_key已经接入
Alogame 运营会在他们那边配置好 Console——你不需要自己动 Console。
Alogame 这一侧的契约,正是上面请求与响应参考所描述的全部内容:把正确的数据、以正确的格式,发送到你给出的路径,然后读回你的响应说了什么——仅此而已。onCheckUid/onCreateOrder/onPaymentReceived 之间如何处理这些数据(用哪个数据库、哪个玩家、哪笔订单)完全是你后端内部的事,Alogame 无法查看也无法验证。
上线前,Alogame 的 Console 测试工具会用你提供的一个 UID 调用你的 check-uid 路径,并把原始响应展示给你看。这只能证明该路径可达、且这一次调用的响应格式正确——它不能、也无法证明 onCheckUid 对任意 UID 都能正确查到你真实的玩家数据。这部分正确性完全需要你自己在第 8 步之前验证好,就像验证你后端其他任何部分一样。
8. 正式上线
Alogame 会把你的配置切换为生效状态。从这一刻起,每次调用都是真实的玩家流量。
Expub 模块(网页支付 — Expub + 移动端内购)
同时覆盖独家/直接运营游戏的网页充值页面和移动端内购——两个渠道完全共用 createOrder_url/exchange_url,所以同样两个钩子会响应两边的调用。
流程
玩家只有在登录了自己的 Alogame 账号之后才会进入这个流程——与 WebPay 的联合运营流程不同,这里不涉及手动输入 UID。
和 WebPay 的 co-pub 流程不同,Alogame 自己的结账流程从不会对 expub 游戏调用 check-uid——玩家已经从 onGetUserList 的响应里选好了角色,uid 是否存在已经在这一步被证明过了。有些合作方的后端出于其他原因已经有独立的 check-uid 接口(比如这个 SDK 出现之前就有,或者他们自己的工具在用)——见下面的 onCheckUid,它是可选的,原因正是这个。
请求与响应参考
每次调用都是 POST,并使用你的游戏在 Console 中配置的签名策略。ExpubHandler 会根据每个请求的请求头自动识别是哪一种——代码里无需任何配置:
| 策略 | Alogame 发送的内容 | 结果的返回方式 |
|---|---|---|
| MD5(已有的 expub 游戏) | Signature 请求头,时间戳以秒为单位放在报文体里——参见身份认证(目前仅有英文版) | 每种结果对应一个 HTTP 状态码,报文体见下文 |
| HMAC-SHA256(新接入推荐,SDK ≥ 2.2.0) | x-timestamp(毫秒)+ x-signature 请求头,报文体里没有 timestamp——参见身份认证 | 始终返回 200,报文为 {"errcode", "msg", "data"} |
两种策略下的钩子和 DTO 完全相同——只有报文响应不同,而响应由 SDK 生成。HMAC 下:
| 钩子返回 | 报文响应 |
|---|---|
onGetUserList → 角色列表 | {"errcode":0,"msg":"success","data":[{"uid","characterName","server"}]} |
CheckUidResult::found(...) / notFound() | {"errcode":0,...,"data":{"characterName","server"}} / {"errcode":1,"msg":"uid not found"} |
CreateOrderResult::created($code) 或 duplicate($code) | {"errcode":0,"msg":"success","data":{"order_num":"<code>"}} |
CreateOrderResult::productNotFound() / uidNotFound() | {"errcode":1,"msg":"product not found"} / {"errcode":1,"msg":"uid not found"} |
PaymentReceivedResult::ok() 或 alreadyProcessed() | {"errcode":0,"msg":"success"} |
PaymentReceivedResult::orderCodeNotFound() | {"errcode":1,"msg":"order_code not found"} |
重复订单和已发放的支付返回 errcode 0 是有意为之:它们是对已经成功的操作的重试。HMAC 下网页充值和移动端内购对部分字段的命名也不同(order_id 与 plat_order_num、productId 与 productid、order_code 与 order_num、price 与 amount)——SDK 两种都接受,所以你的钩子看到的始终是相同的 DTO 属性。
2.2.0 之前 ExpubHandler 只校验 MD5。使用旧版本的 HMAC 游戏每次调用都会收到 401 SIGNATURE_INVALID("timestamp" 缺失或过期),因为 HMAC 的时间戳在请求头里,而不在报文体里。执行 composer update alo-game/paymentsdk 即可——无需修改代码。
onGetUserList — 玩家打开充值页面
报文接口: getUserList_url
GetUserListRequest 属性 | 报文字段 | 类型 | 说明 |
|---|---|---|---|
$userId | userId | string | Alogame 账号的数字 ID——与 Alogame 客户端 SDK 在登录时返回给你游戏的 userId 相同,不是 uuid。该账号关联的所有角色都应该被返回 |
$extInfo | ext_info | string|null | 由 SDK 转发的上下文信息 |
{ "userId": "1234", "ext_info": "{}" }
返回一个 UserCharacter 数组。空数组也是合法的——一个尚未创建角色的已关联账号也应返回 200。以下为 MD5 响应(HMAC 见上表):
{
"userId": "1234",
"uids": [
{ "uid": "char_001", "server": "Server 1", "characterName": "DragonSlayer" }
]
}
onCheckUid — 可选,仅当你的后端已经有这个接口时才实现
报文接口: checkUser_url
CheckUidHookInterface不属于 ExpubHookInterface。Alogame 自己的结账流程从不会对 expub 游戏调用它(见上面流程下的说明)——除非你的 hooks 类同时 implements CheckUidHookInterface,否则 ExpubHandler::handleCheckUid() 会返回 404 NOT_CONFIGURED。只有当你的后端出于其他原因已经有独立的 check-uid 接口时才需要实现它——比如这个 SDK 出现之前就有,或者你自己的工具在调用它——例如 oe-007 的 ?ac=check_uid。
CheckUidRequest 属性 | 报文字段 | 类型 | 说明 |
|---|---|---|---|
$uid | uid | string | 来自 onGetUserList 响应中的角色 UID |
$extInfo | ext_info | string|null | 由 SDK 转发的上下文信息 |
{ "uid": "char_001", "ext_info": "{}" }
返回 CheckUidResult::found($characterName, $server) 或 CheckUidResult::notFound():
// 找到 — 200 OK
{ "characterName": "DragonSlayer", "server": "Server 1" }
errcodeCheckUidResult::notFound() 会产生真正的 404,而不是带错误体的 200——与上面 WebPay 的响应格式正好相反。
onCreateOrder — 同一个钩子同时服务网页充值和移动端内购
报文接口: createOrder_url —— 与移动端内购(目前仅有英文版)共用
CreateOrderRequest 属性 | 报文字段 | 类型 | 说明 |
|---|---|---|---|
$orderId | order_id | string | Alogame 的订单编号——请存下来,onPaymentReceived 会用它匹配回这笔订单 |
$uid | uid | string | 玩家在 onGetUserList 响应里选中的角色 UID |
$productId | productId | string | 你自己的商品 ID |
$amount | price | int|null | 订单总额——请依据 $osId 区分渠道,而不是依据这个字段是否存在 |
$serverId | serverId | string|null | 仅当游戏按服务器划分时才会出现 |
$osId | os_id | string|null | 移动端内购购买时为 "ios"/"android",网页充值时为 null——这是唯一能区分两个渠道的字段 |
$extInfo | ext_info | string|null | 由 SDK 转发的上下文信息 |
{ "order_id": "alo_ord_1a2b3c4d", "uid": "char_001", "productId": "pack_500_gold", "ext_info": "{}" }
返回 CreateOrderResult::created($yourOwnOrderCode):
// 201 Created
{ "status": "success", "order_code": "PARTNER-ORD-20260315-001" }
$request->orderId 去重如果 Alogame 重试,请返回 CreateOrderResult::duplicate($existingOrderCode)——SDK 会以 409 状态码返回原来的 order_code,绝不会创建新订单。
onPaymentReceived — 立即发放道具
报文接口: exchange_url —— 与移动端内购(目前仅有英文版)共用
PaymentReceivedRequest 属性 | 报文字段 | 类型 | 说明 |
|---|---|---|---|
$orderCode | order_code | string | 你自己的订单编号,来自 onCreateOrder |
$extInfo | ext_info | string|null | 由 SDK 转发的上下文信息 |
{ "order_code": "PARTNER-ORD-20260315-001", "ext_info": "{}" }
发货完成后返回 PaymentReceivedResult::ok():
// 200 OK
{ "processingStatus": "completed" }
对一笔已经发货过的订单,请返回 PaymentReceivedResult::alreadyProcessed()(409)——永远不要重复发放,也永远不要把一次重复调用当作 orderCodeNotFound()。
前置条件
- 一个共享的
secret(MD5 或 HMAC 均使用它),来自你的 Alogame 运营对接人——这是ExpubHandler唯一使用的凭证。 - 确认这个游戏是 expub 而不是 co-pub——如果不确定,见上文我该用哪个模块?。
1. 安装
与上面 WebPay 使用同一个包,无需单独安装:
composer require alo-game/paymentsdk
2. 实现三个必需的钩子(如果你已经有 check-uid 接口,再加上它)
use Alogame\PaymentSdk\Expub\Contracts\ExpubHookInterface;
use Alogame\PaymentSdk\Expub\Dto\{
GetUserListRequest, UserCharacter,
CreateOrderRequest, CreateOrderResult,
PaymentReceivedRequest, PaymentReceivedResult,
};
final class MyExpubGameHooks implements ExpubHookInterface
{
public function onGetUserList(GetUserListRequest $request): array
{
// 该 Alogame 账号关联的所有角色——玩家会在 Alogame 那一侧从中选择一个
// 再充值。返回空数组也合法(账号已关联,但还没有角色)。
return MyCharacterRepository::findAllByAlogameUserId($request->userId);
}
public function onCreateOrder(CreateOrderRequest $request): CreateOrderResult
{
// $request->osId 在移动端内购购买时为 "ios"/"android",网页充值时为
// null——同一个方法,如果发货逻辑不同就据此分支。
$existing = MyOrderRepository::findByAlogameOrderId($request->orderId);
if ($existing !== null) {
// Alogame 重试了——返回同一个 order_code,不要创建新订单。
return CreateOrderResult::duplicate($existing->orderCode);
}
$order = MyOrderRepository::create(
alogameOrderId: $request->orderId,
uid: $request->uid,
productId: $request->productId,
);
return CreateOrderResult::created($order->id);
}
public function onPaymentReceived(PaymentReceivedRequest $request): PaymentReceivedResult
{
$order = MyOrderRepository::findByOwnId($request->orderCode);
if ($order === null) {
return PaymentReceivedResult::orderCodeNotFound();
}
MyInventory::deliver($order->uid, $order->productId);
return PaymentReceivedResult::ok();
}
}
与上面 WebPay 同样的要求:onCreateOrder 必须对重复的 $request->orderId 返回同一个 orderCode,onPaymentReceived 必须对已经发货过的 $request->orderCode 返回 ok(),绝不重复发货。
3. 接好路由
四个不同的 URL,这套契约没有健康检查路由(与 WebPay 不同)——Alogame 目前不会为 expub 游戏调用健康检查:
use Alogame\PaymentSdk\Expub\ExpubHandler;
$handler = new ExpubHandler(
secret: getenv('ALOGAME_EXPUB_SECRET'),
hooks: new MyExpubGameHooks(),
onError: fn (\Throwable $e) => error_log($e),
);
$response = match (true) {
$_SERVER['REQUEST_URI'] === '/expub/get-user-list' => $handler->handleGetUserList(getallheaders(), file_get_contents('php://input')),
$_SERVER['REQUEST_URI'] === '/expub/check-uid' => $handler->handleCheckUid(getallheaders(), file_get_contents('php://input')),
$_SERVER['REQUEST_URI'] === '/expub/create-order' => $handler->handleCreateOrder(getallheaders(), file_get_contents('php://input')),
$_SERVER['REQUEST_URI'] === '/expub/payment-received' => $handler->handlePaymentReceived(getallheaders(), file_get_contents('php://input')),
};
http_response_code($response->status);
header('Content-Type: application/json');
echo json_encode($response->body);
不管你有没有实现 CheckUidHookInterface,check-uid 这条路由都照样接好——handleCheckUid() 自己会在你的 hooks 类没实现时返回 404 NOT_CONFIGURED,这两种情况下都是正确的响应。
4. 可选:check-uid
只有当你的后端出于其他原因已经有独立的 check-uid 接口时才需要——Alogame 自己的结账流程从不会对 expub 游戏调用它(见上面流程)。实现第二个接口——ExpubHandler 会自动识别,不需要额外配置:
use Alogame\PaymentSdk\Expub\Contracts\CheckUidHookInterface;
use Alogame\PaymentSdk\Expub\Dto\{CheckUidRequest, CheckUidResult};
final class MyExpubGameHooks implements ExpubHookInterface, CheckUidHookInterface
{
// ...onGetUserList / onCreateOrder / onPaymentReceived 同上...
public function onCheckUid(CheckUidRequest $request): CheckUidResult
{
$character = MyCharacterRepository::findByUid($request->uid);
return $character
? CheckUidResult::found($character->name, $character->server)
: CheckUidResult::notFound();
}
}
如果你的后端没有理由暴露这个接口,整节跳过即可。
5. 把接口地址给 Alogame
按环境把 base URL + 全部四个路径发给 Alogame 运营,并确认前置条件中的 secret 已经接入。Alogame 运营会在他们那边配置好 Console。
6. 正式上线
Alogame 会把你的配置切换为生效状态。从这一刻起,无论是网页充值还是移动端内购,每次调用都是真实的玩家流量。
升级
运行 composer update 之前先看一下更新日志——这个包运行在你的生产后端里,和其他任何依赖一样需要谨慎对待。两个模块在同一个包里各自独立地升版本,更新日志会注明某次发布具体改动了哪一个。
当前版本:2.2.0。 相对 2.1.x 是纯新增——没有任何需要迁移的地方,所有已有钩子的签名保持不变。Expub 模块现在也支持使用 HMAC-SHA256 签名的游戏(见 Expub 模块的“请求与响应参考”);MD5 游戏不受任何影响。
2.1.0 为 WebPay 模块新增了可选的 onGetCharacterList 钩子(以及 onCreateOrder 上的 $characterId),面向一个 UID 拥有多个角色的游戏。
handleHealthCheck() 会返回 WebPay 模块自身的契约版本(包版本 2.1.0 对应 1.1.0,此前为 1.0.0)——Alogame 支持团队正是通过它来判断你的部署是否已经支持角色列表。
实施清单
WebPay 模块:
-
composer require alo-game/paymentsdk成功 - 已经针对你自己的数据实现了
onCheckUid、onCreateOrder、onPaymentReceived -
onCreateOrder和onPaymentReceived能安全地接受同一笔订单被重复调用 - 三条路由已接好(每个可选的选择器——服务器列表、角色列表——各再加一条),且都传递原始请求体
- 健康检查路由已接好,路径已发给 Alogame 运营
- Base URL + 路径 + 密钥确认信息已发给 Alogame 运营
- 已通过 Alogame 上线前的测试调用
Expub 模块:
-
composer require alo-game/paymentsdk成功 - 已经针对你自己的数据实现了
onGetUserList、onCreateOrder、onPaymentReceived - 实现了
onCheckUid并加上了CheckUidHookInterface——仅当你的后端已经有独立的 check-uid 接口时才需要 -
onCreateOrder对重复的order_id返回duplicate(),而不是创建新订单 -
onPaymentReceived对重复的order_code返回alreadyProcessed(),而不是二次发货 - 四条路由已接好,且都传递原始请求体
- Base URL + 全部四个路径 + 密钥确认信息已发给 Alogame 运营
- 如果这个游戏有移动端 App,移动端内购已接入同一套
createOrder_url/exchange_url