db = new Database(); $this->rawRequestBody = file_get_contents('php://input') ?: ''; $this->requestJson = $this->getJsonInput(); } protected function getJsonInput(): array { $raw = $this->rawRequestBody; if (!$raw) { return []; } $data = json_decode($raw, true); return is_array($data) ? $data : []; } protected function jsonResponse(bool $success, string $message = '', array $data = [], int $statusCode = 200): void { http_response_code($statusCode); header('Content-Type: application/json; charset=utf-8'); echo json_encode([ 'success' => $success, 'message' => $message, 'data' => $data, ], JSON_UNESCAPED_UNICODE); exit; } protected function auditAndRespond(bool $success, string $message = '', array $data = [], int $statusCode = 200, ?int $groupId = null, ?string $idempotencyKey = null): void { $payload = [ 'success' => $success, 'message' => $message, 'data' => $data, ]; $responseBody = json_encode($payload, JSON_UNESCAPED_UNICODE); $this->logApiRequest( (int)($this->botInstance['id'] ?? 0) ?: null, $groupId, $idempotencyKey, $statusCode, $this->botInstance !== null, $this->rawRequestBody, $responseBody ); http_response_code($statusCode); header('Content-Type: application/json; charset=utf-8'); echo $responseBody; exit; } protected function getHeader(string $name): string { $key = 'HTTP_' . strtoupper(str_replace('-', '_', $name)); return trim($_SERVER[$key] ?? ''); } protected function authenticateBotRequest(): void { $botKey = $this->getHeader('X-Bot-Key'); $timestamp = $this->getHeader('X-Timestamp'); $signature = $this->getHeader('X-Signature'); if ($botKey === '' || $timestamp === '' || $signature === '') { $this->jsonResponse(false, 'Missing bot auth headers', [], 401); } if (!ctype_digit($timestamp)) { $this->jsonResponse(false, 'Invalid timestamp', [], 401); } $now = time(); if (abs($now - (int)$timestamp) > 300) { $this->jsonResponse(false, 'Timestamp expired', [], 401); } $bot = $this->db->get('bot_instances', '*', [ 'bot_key' => $botKey, 'status' => 1, ]); if (!$bot) { $this->jsonResponse(false, 'Bot auth failed', [], 401); } $rawBody = $this->rawRequestBody; $method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET'); $path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/'; $signPayload = $timestamp . "\n" . $method . "\n" . $path . "\n" . $rawBody; $expected = hash_hmac('sha256', $signPayload, $bot['bot_secret']); if (!hash_equals($expected, $signature)) { $this->logApiRequest((int)$bot['id'], null, null, 0, false, $rawBody, ''); $this->jsonResponse(false, 'Signature verify failed', [], 401); } $this->botInstance = $bot; } protected function requireGroupByTelegramId(string $tgGroupId): array { $group = $this->db->get('bot_groups', '*', [ 'tg_group_id' => $tgGroupId, 'status' => 1, ]); if (!$group) { $this->jsonResponse(false, 'Group config not found', [], 404); } if ((int)$group['bot_id'] !== (int)($this->botInstance['id'] ?? 0)) { $this->jsonResponse(false, 'Group does not belong to this bot', [], 403); } $this->groupConfig = $group; return $group; } protected function getGroupWallet(int $groupId): array { $wallet = $this->db->get('bot_group_wallets', '*', [ 'group_id' => $groupId, 'status' => 1, ]); if (!$wallet) { $this->jsonResponse(false, 'Group wallet not configured', [], 422); } return $wallet; } protected function buildIdempotencyKey(string $fallbackPrefix = 'bot'): string { $key = trim((string)($this->requestJson['idempotency_key'] ?? '')); if ($key !== '') { return $key; } $groupId = (string)($this->groupConfig['tg_group_id'] ?? 'unknown'); $messageId = (string)($this->requestJson['bet_context']['message_id'] ?? $this->requestJson['message_id'] ?? uniqid()); return $fallbackPrefix . ':' . $groupId . ':' . $messageId; } protected function logApiRequest(?int $botId, ?int $groupId, ?string $idempotencyKey, int $responseCode, bool $signatureOk, string $requestBody, string $responseBody): void { try { $this->db->insert('bot_api_request_logs', [ 'bot_id' => $botId, 'group_id' => $groupId, 'request_uri' => parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/', 'http_method' => strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET'), 'idempotency_key' => $idempotencyKey, 'request_body' => $requestBody, 'response_body' => $responseBody, 'response_code' => $responseCode, 'client_ip' => $_SERVER['REMOTE_ADDR'] ?? '', 'signature_ok' => $signatureOk ? 1 : 0, 'created_at' => date('Y-m-d H:i:s'), ]); } catch (\Throwable $e) { // 审计失败不阻断主流程 } } }