投票奖励

投票奖励使用 server.voteproject.vote Webhook 事件:处理器接收事件,通过 API 获取投票数据,并且只发放一次奖励。

请先配置 项目 Webhooksignature 验证。投票数据通过 GET /votes/:vote_id 请求。

流程如何工作

  1. 接收 Webhook 事件并验证 signature。如果 is_testtrue,返回 204,不要请求投票数据,也不要发放奖励。
  2. 确认 event_type 等于 server.voteproject.vote
  3. event_id 作为投票 ID 使用。
  4. 通过 GET /votes/:vote_id 获取投票数据,并在你的系统中找到玩家。
  5. 在同一个事务中按 event_type + event_id 应用防重复处理,并且只为新事件发放奖励。
  6. 如果无法安全发放奖励,请返回错误响应。修复原因后,从界面重新发送投递

奖励示例

假设玩家 PlayerName 为 ID 为 1 的服务器投票,而你的系统需要给他增加 100 枚金币。

  1. GAMEMONITORING 发送 Webhook,包含 event_type: server.voteevent_id: 9824cabb-2203-437e-9b6c-aba43dde3e4b
  2. 处理器验证 signature。如果签名无效,返回 401 并停止。
  3. 处理器请求 GET /votes/9824cabb-2203-437e-9b6c-aba43dde3e4b,获得昵称、服务器和用户数据,然后找到本地账号。
  4. 在事务中,处理器保存 event_type + event_id 用于防重复处理
  5. 对于新事件,处理器在同一个事务中增加 100 枚金币。
  6. 重复投递时,处理器找到已经保存的事件,不再次发放奖励,并返回 204

同一流程也适用于物品、角色、VIP 时长、促销码或内部队列中的任务。

投票事件

服务器投票会发送 server.vote,项目投票会发送 project.vote。事件请求体只包含投递数据:event_typeevent_idis_testsignature。完整投票数据必须单独请求。

事件示例
{
  "event_id": "9824cabb-2203-437e-9b6c-aba43dde3e4b",
  "event_type": "server.vote",
  "is_test": false,
  "signature": "ae83b8aba88a3a9ab3b97b1f6d65664da5628a9cb64d56d5132807bca5472e4f"
}

在此事件中,event_id 是投票 ID。不要把 Webhook 请求体作为昵称、服务器或用户数据来源:这些值来自 API。

获取投票数据

event_id 当作 vote_id 使用,并通过 GET /votes/:vote_id 请求投票数据:

投票数据请求
curl -sS "https://api.gamemonitoring.cn/votes/9824cabb-2203-437e-9b6c-aba43dde3e4b"

发放奖励通常需要 response.nicknameresponse.server 和公开的 response.user 数据。如果奖励依赖具体服务器,请始终检查 response.server.id

字段使用方式:response.nickname 帮助在你的数据库中查找玩家账号,response.server.id 选择服务器的奖励规则,response.user.id 可以作为投票的 GAMEMONITORING 用户 ID 保存到奖励日志。

如果 API 暂时不可用或返回意外响应,请不要在未验证的情况下发放奖励。返回错误码,修复原因后从界面重新发送投递

第 3 步:投票奖励处理器

示例延续基础处理器:它会验证签名,获取投票数据,防止事件重复处理,并在同一个事务中发放奖励。请把用户表名、余额字段和玩家查找规则替换为你系统中的结构。

运行示例前,请先配置 项目 Webhook,检查 GET /votes/:vote_id,并把 SQL 用户更新语句替换为你的账号模型。

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

// Add the GAMEMONITORING API URL and reward settings for vote events.
$apiUrl = 'https://api.gamemonitoring.cn';
$rewardAmount = '1.00';

// 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;
}

// This reward handler processes server and project vote events.
if (!in_array($eventType, ['server.vote', 'project.vote'], true)) {
    http_response_code(204);
    exit;
}

// At this point the webhook is trusted. Load vote data before opening a database transaction.
$pdo = null;

try {
    // Load full vote data by event_id. Nickname, entity, and user data are not
    // in the webhook body. Return 500 if the API cannot confirm the vote.
    $voteUrl = $apiUrl . '/votes/' . rawurlencode($eventId);
    $voteContext = stream_context_create(['http' => ['timeout' => 5]]);
    $voteBody = @file_get_contents($voteUrl, false, $voteContext);

    if ($voteBody === false) {
        throw new RuntimeException('Vote API request failed');
    }

    $voteResponse = json_decode($voteBody, true) ?: [];
    $vote = $voteResponse['response'] ?? null;

    // Do not issue a reward when the vote response is missing a concrete nickname.
    if (!is_array($vote) || !isset($vote['nickname']) || !is_string($vote['nickname'])) {
        throw new RuntimeException('Vote API response does not include nickname');
    }

    // Verify that the API entity matches the event before changing the account.
    $expectedEntityType = $eventType === 'project.vote' ? 'project' : 'server';
    if (($vote['entity_type'] ?? '') !== $expectedEntityType) {
        throw new RuntimeException('Vote entity type does not match event type');
    }

    // Use vote nickname to update the local account. The entity id is available in
    // vote.entity_id and in either vote.server.id or vote.project.id.
    $nickname = trim($vote['nickname']);

    if ($nickname === '') {
        throw new RuntimeException('Vote nickname is empty');
    }

    // 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.
    $balance = $pdo->prepare('UPDATE users SET balance = balance + ? WHERE nickname = ?');
    $balance->execute([$rewardAmount, $nickname]);

    // 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);
}