- 拆分 HomeController → TransactionController, ReportWebController, FollowPlanController - 新增 Service 层: TransactionService, ReportService, FollowPlanService - pk10.php JS 抽离为 4 个独立文件 (sound/race/bet/poll) - 前台新增报表查询页面 (/report) + 跟单计划页面 (/follow-plan) - 后台新增跟单计划管理 + 用户输赢明细统计 - 封盘状态显示倒计时 (x:xx) - 音效仅在开奖弹窗打开时播放 - 路由按模块分组整理 - autoload 支持 App\Services 命名空间
182 lines
5.9 KiB
PHP
182 lines
5.9 KiB
PHP
<?php
|
|
namespace App\Core;
|
|
|
|
use Db\Database;
|
|
|
|
class BotApiBaseController extends BaseController
|
|
{
|
|
protected ?Database $db = null;
|
|
protected ?array $botInstance = null;
|
|
protected ?array $groupConfig = null;
|
|
protected array $requestJson = [];
|
|
protected string $rawRequestBody = '';
|
|
|
|
public function __construct()
|
|
{
|
|
$this->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) {
|
|
// 审计失败不阻断主流程
|
|
}
|
|
}
|
|
}
|