bucket($scope . '_requests', $subject, 1), $this->bucket($scope . '_endpoint', $subject . ':' . hash('sha256', $endpoint), 1), ]; if ($listCost > 0) { $buckets[] = $this->bucket($scope . '_list_rows', $subject, $listCost); } try { [$blocked, $retry, $notice] = (new RedisQuota())->consume($buckets); } catch (\Throwable $error) { // Do not log connection strings, request data or exception messages. if (time() - self::$lastFailureLog >= 60) { self::$lastFailureLog = time(); Log::channel('mini_protection')->error('mini_api.protection_unavailable', [ 'request_id' => $requestId, 'exception_type' => get_class($error), ]); } throw new ApiException(50303, '访问保护服务暂时不可用,请稍后重试', 503, ['retry_after' => 5]); } if (!$blocked) { return; } if ($notice) { Log::channel('mini_protection')->warning('mini_api.quota_exceeded', [ 'request_id' => $requestId, 'scope' => $scope, 'subject_hash' => $subject, 'endpoint' => $endpoint, 'rule' => $buckets[$blocked - 1]['rule'], 'retry_after' => $retry, ]); } throw new ApiException(42902, '访问过于频繁,请在' . $retry . '秒后重试', 429, ['retry_after' => $retry]); } private function bucket(string $rule, string $subject, int $cost): array { [$limit, $seconds] = config('mini_protection.limits.' . $rule); return [ 'key' => config('mini_protection.prefix') . $rule . ':' . $subject, 'limit' => (int) $limit, 'seconds' => (int) $seconds, 'cost' => $cost, 'rule' => $rule, ]; } }