Files
pk10/App/Core/BotApiBaseController.php
li 4a94fe36cf feat: 重构代码架构 + 新增报表/跟单/输赢统计功能
- 拆分 HomeController → TransactionController, ReportWebController, FollowPlanController
- 新增 Service 层: TransactionService, ReportService, FollowPlanService
- pk10.php JS 抽离为 4 个独立文件 (sound/race/bet/poll)
- 前台新增报表查询页面 (/report) + 跟单计划页面 (/follow-plan)
- 后台新增跟单计划管理 + 用户输赢明细统计
- 封盘状态显示倒计时 (x:xx)
- 音效仅在开奖弹窗打开时播放
- 路由按模块分组整理
- autoload 支持 App\Services 命名空间
2026-03-27 18:41:06 +08:00

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) {
// 审计失败不阻断主流程
}
}
}