Webhook

Webhook 会在平台发生操作后,把 GAMEMONITORING 事件发送到你的系统。它是普通的 POST 请求,请求体为 JSON,网站、面板或游戏服务可以据此自动响应。

如果你要配置投票奖励,请先按照本指南连接 Webhook,然后使用单独流程:投票奖励

连接

此设置会把 GAMEMONITORING 项目连接到你的处理器,并提供用于验证传入请求的签名 token。

  1. 打开 我的项目,创建项目或选择已有项目,然后进入 Webhook 设置。
  2. 创建公开 HTTPS endpoint,接受 Content-Type: application/jsonPOST 请求,并且不要重定向请求。
  3. 在 Webhook 设置中填写完整处理器 URL,例如 https://panel.example.com/gamemonitoring-webhook,并保存。
  4. 从同一块设置中复制签名 token,并把它写入处理器脚本。
  5. 在处理器中验证 signature,处理需要的 event_type,并且只在事件处理完成后返回成功的 2xx 响应。

本地测试时,可以在自己的电脑上运行处理器,并通过公开 HTTPS URL 暴露它。可以使用 ngrok 或其他隧道服务,然后在 Webhook 设置中填写生成的公开 URL。

设置完成后,从界面发送测试 Webhook 并检查投递状态。对于示例 URL,服务器必须在 /gamemonitoring-webhook 路径接受 POST

如果测试失败,请先查看处理器响应:401 表示签名错误,403 或 HTML 验证页面通常表示 WAF 或 bot protection,超时表示该 URL 无法从互联网访问或响应太慢。

处理器要求

  • URL 必须可以从互联网访问。本地地址、私有网络以及带登录名或密码的 URL 不适用。
  • 生产环境建议使用 HTTPS。HTTP 也支持,但传输过程中的数据保护较弱。
  • 处理器必须接受 POST 方法和 JSON 请求体,且不发生重定向。
  • 只有在系统处理完事件后才返回 2xx。通常 204 No Content 就足够。
  • 如果事件无法安全处理,请返回错误码。3xx4xx5xx、超时和连接错误都会被视为投递失败。
  • 如果使用 firewall、bot protection 或 allowlist,请把 GAMEMONITORING IP 地址 加入例外。
  • 不要在响应体中返回 token、stack trace、SQL 错误或其他内部细节。处理器响应会显示在界面中,因此错误文本必须安全且易懂。

事件数据

每个 Webhook 都会带有 JSON 请求体和基础字段:

  • event_type — 需要处理的事件。
  • event_id — 唯一事件 ID。请与 event_type 一起用于幂等处理和防重复投递。
  • is_test — 表示来自界面的测试投递。
  • signature — 事件请求体的签名。
Webhook 事件示例
{
  "event_id": "9824cabb-2203-437e-9b6c-aba43dde3e4b",
  "event_type": "example.event",
  "is_test": false,
  "signature": "0ac4c97a5d934599dbd78985c4bcbb6926e77b4809d2be56333b1b25f638f064"
}

示例解读:event_type 表示需要处理哪个事件;event_id 用于在修改状态前保证幂等;is_test: true 表示技术投递检查;signature 不是业务数据,只用于验证请求真实性。

处理逻辑取决于 event_type。对于 server.voteproject.vote,请使用单独流程:投票奖励

如果 is_testtrue,请验证签名并返回 2xx,但不要修改余额、发放物品或启动生产操作。

处理器响应示例

每个传入事件都应以一个明确结果结束:

  • 204 No Content — 签名正确,事件已处理或已安全跳过。测试投递和已经处理过的事件也返回相同响应。
  • 400 Bad Request — 缺少必填字段。这表示处理器错误或请求体不符合预期,不应执行业务逻辑。
  • 401 Unauthorized — 签名无效。不要发起 API 请求,不要修改数据库,也不要发放奖励。
  • 500 Internal Server Error — 数据库、队列或内部系统暂时不可用。投递会保持失败状态,修复原因后可以重试。

例如,处理器收到事件、验证签名、保存 event_type + event_id 并处理事件后,可以返回 204。如果数据库不可用,事件无法保存,最好返回 500,避免投递过早被标记为成功。

签名验证

签名位于 signature 字段。请在任何业务逻辑、API 请求或数据库修改之前验证它。

验证时,取事件请求体中除 signature 以外的所有字段,按 key 字母顺序排序,并组成用 & 连接的 key=value 字符串。布尔值写作 truefalse

签名字符串
event_id=9824cabb-2203-437e-9b6c-aba43dde3e4b&event_type=example.event&is_test=false

对于上面的示例,签名字符串只由 event_idevent_typeis_test 组成。然后使用 Webhook 设置中的签名 token 计算 HMAC-SHA256,并与请求中的 signature 比较。

示例中的预计算签名使用演示 token paste-webhook-token-here。在你的处理器中,请使用 Webhook 设置中的 token。

在处理器中:

  • 用排序后的 key 构建签名字符串;
  • 使用签名 token 计算 HMAC-SHA256
  • 用恒定时间比较函数将结果与 signature 比较:PHP 使用 hash_equals,Node.js 使用 timingSafeEqual,Python 使用 compare_digest
  • 如果签名无效,返回 401

第 1 步:基础处理器

先实现一个能接受任意 Webhook 的处理器:读取 JSON,验证 signature,处理测试投递,检查基础字段,并返回 204。在这一步,处理器只确认投递可以被正确接收。请在这个基础路径可用后再添加具体事件的逻辑。

php
<?php
// Replace this token with the signing token from your GAMEMONITORING webhook settings.
$secret = 'paste-webhook-token-here';

// Read and decode the JSON body sent by GAMEMONITORING.
$event = json_decode(file_get_contents('php://input'), true) ?: [];

// Test deliveries are signed too. Normalize the boolean value to the lowercase
// string used by GAMEMONITORING when the signature is calculated.
$isTest = ($event['is_test'] ?? false) === true;
$signingData = array_replace($event, ['is_test' => $isTest ? 'true' : 'false']);

// Build the exact signing string: all body fields except signature,
// sorted by key and joined as key=value pairs with &.
$fields = array_values(array_filter(array_keys($event), fn($field) => $field !== 'signature'));
sort($fields, SORT_STRING);

// Calculate HMAC-SHA256 with the webhook token from your settings.
$signing = implode('&', array_map(fn($field) => $field . '=' . (string) ($signingData[$field] ?? ''), $fields));
$expected = hash_hmac('sha256', $signing, $secret);
$actual = (string) ($event['signature'] ?? '');

// Reject the request before doing any work when the signature is invalid.
if (!hash_equals($expected, $actual)) {
    http_response_code(401);
    exit;
}

// Test deliveries must not change balance, inventory, roles, or production data.
if ($isTest) {
    http_response_code(204);
    exit;
}

// Real deliveries must include an event type and a stable event id.
$eventType = (string) ($event['event_type'] ?? '');
$eventId = (string) ($event['event_id'] ?? '');

if ($eventType === '' || $eventId === '') {
    http_response_code(400);
    exit;
}

// At this point the webhook is trusted. Add event-specific logic here.
syslog(LOG_INFO, 'Accepted webhook event ' . $eventType . ' #' . $eventId);

// 204 tells GAMEMONITORING that the delivery was accepted successfully.
http_response_code(204);

第 2 步:添加去重

Webhook 使用 at-least-once 投递模型:同一个事件可能到达多次。在处理器修改系统状态之前,请按 event_type + event_id 让该操作具备幂等性。

首先创建一张表,用唯一键保存 event_typeevent_id 这一对值。如果记录已经存在,说明事件已经处理过。

已处理 Webhook 事件表
CREATE TABLE gamemonitoring_webhooks (
  event_type varchar(64) NOT NULL,
  event_id varchar(100) NOT NULL,
  created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (event_type, event_id)
);

然后扩展基础处理器:在签名和基础字段验证通过后,保存 event_type + event_id,并在同一个事务中执行状态变更。

php
<?php
// Replace this token with the signing token from your GAMEMONITORING webhook settings.
$secret = 'paste-webhook-token-here';

// Read and decode the JSON body sent by GAMEMONITORING.
$event = json_decode(file_get_contents('php://input'), true) ?: [];

// Test deliveries are signed too. Normalize the boolean value to the lowercase
// string used by GAMEMONITORING when the signature is calculated.
$isTest = ($event['is_test'] ?? false) === true;
$signingData = array_replace($event, ['is_test' => $isTest ? 'true' : 'false']);

// Build the exact signing string: all body fields except signature,
// sorted by key and joined as key=value pairs with &.
$fields = array_values(array_filter(array_keys($event), fn($field) => $field !== 'signature'));
sort($fields, SORT_STRING);

// Calculate HMAC-SHA256 with the webhook token from your settings.
$signing = implode('&', array_map(fn($field) => $field . '=' . (string) ($signingData[$field] ?? ''), $fields));
$expected = hash_hmac('sha256', $signing, $secret);
$actual = (string) ($event['signature'] ?? '');

// Reject the request before doing any work when the signature is invalid.
if (!hash_equals($expected, $actual)) {
    http_response_code(401);
    exit;
}

// Test deliveries must not change balance, inventory, roles, or production data.
if ($isTest) {
    http_response_code(204);
    exit;
}

// Real deliveries must include an event type and a stable event id.
$eventType = (string) ($event['event_type'] ?? '');
$eventId = (string) ($event['event_id'] ?? '');

if ($eventType === '' || $eventId === '') {
    http_response_code(400);
    exit;
}

// At this point the webhook is trusted. Deduplicate it before event-specific logic.
$pdo = null;

try {
    // Add your local database connection for deduplication and event-specific work.
    $pdo = new PDO('mysql:host=127.0.0.1;dbname=game;charset=utf8mb4', 'game', 'password', [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    ]);

    // Keep deduplication and the real state change in one transaction.
    // If any step fails, return 500 so the delivery can be retried.
    $pdo->beginTransaction();

    // Store the event once. This requires the table to have a unique key on
    // (event_type, event_id). Duplicate deliveries affect zero rows.
    $deduplicate = $pdo->prepare('INSERT IGNORE INTO gamemonitoring_webhooks (event_type, event_id) VALUES (?, ?)');
    $deduplicate->execute([$eventType, $eventId]);

    // The event was already processed earlier. Return success without changing
    // state again, because duplicate delivery is expected.
    if ($deduplicate->rowCount() === 0) {
        $pdo->commit();
        http_response_code(204);
        exit;
    }

    // Add event-specific database changes here. Keep them after the
    // deduplication insert and inside this same transaction.

    // Commit only after deduplication and event-specific work both succeed.
    $pdo->commit();

    // Log only newly processed real events after the transaction succeeds.
    syslog(LOG_INFO, 'Accepted webhook event ' . $eventType . ' #' . $eventId);

    http_response_code(204);
} catch (Throwable $error) {
    // Roll back partial database work so the event can be retried safely.
    if ($pdo instanceof PDO && $pdo->inTransaction()) {
        $pdo->rollBack();
    }

    // 500 keeps the delivery failed instead of marking unfinished work as done.
    http_response_code(500);
}

不要把昵称、用户 ID 或服务器 ID 作为去重 key:同一个用户可能触发不同事件,也可能稍后重复一次允许的操作。key 必须是 event_type + event_id

示例:处理器已经修改了你的系统状态,但在 GAMEMONITORING 收到 204 之前连接中断。稍后投递被重试,同一个事件再次到达。处理器必须找到已经保存的 event_type + event_id,跳过重复状态变更,并返回 204

如果处理器暂时无法处理事件,请返回错误响应。原因修复后,如果该事件支持重试,可以从界面重新发送投递。

测试和重新发送

测试投递(is_test: true)会检查处理器 URL、签名和 HTTP 响应。处理器必须走同一条处理路径:读取 JSON,验证 signature,识别 is_test,并返回成功的 2xx 响应。

测试事件不得修改余额、库存、角色、订阅或其他生产数据。测试只需要技术日志和 204 响应。

如果投递失败,界面会显示状态、HTTP 状态码和处理器响应。修复原因后,如果该事件支持重试,可以重新发送失败的投递。