feat(sms): failover SuperAdmin SMS hosts with sticky success cache
Try montzar/montech/montest when wallet panel calls fail; remember last working base for 7 days. No .env change required.
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Services\SmsBilling;
|
namespace App\Services\SmsBilling;
|
||||||
|
|
||||||
use Illuminate\Http\Client\Response;
|
use Illuminate\Http\Client\Response;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
@@ -13,14 +14,27 @@ use Illuminate\Support\Facades\Log;
|
|||||||
* Env: SUPERADMIN_SMS_BASE_URL + SUPERADMIN_SMS_SHARED_SECRET
|
* Env: SUPERADMIN_SMS_BASE_URL + SUPERADMIN_SMS_SHARED_SECRET
|
||||||
* Optional: SUPERADMIN_SMS_DOMAIN (else host of APP_URL)
|
* Optional: SUPERADMIN_SMS_DOMAIN (else host of APP_URL)
|
||||||
*
|
*
|
||||||
|
* Failover: tries SUPERADMIN_SMS_BASE_URL first (if set), then the three
|
||||||
|
* known SuperAdmin hosts. Last successful base is cached and tried first next time.
|
||||||
|
*
|
||||||
* Fallback: SUPERADMIN_REMOTE_TOKEN (per-tenant Bearer) if shared secret missing.
|
* Fallback: SUPERADMIN_REMOTE_TOKEN (per-tenant Bearer) if shared secret missing.
|
||||||
*/
|
*/
|
||||||
class SuperAdminSmsClient
|
class SuperAdminSmsClient
|
||||||
{
|
{
|
||||||
|
private const SUCCESS_BASE_CACHE_KEY = 'superadmin_sms_success_base';
|
||||||
|
|
||||||
|
private const SUCCESS_BASE_CACHE_TTL_SECONDS = 60 * 60 * 24 * 7;
|
||||||
|
|
||||||
|
/** @var list<string> */
|
||||||
|
private const DEFAULT_BASES = [
|
||||||
|
'https://adm.montzar.ir/api/remote/sms',
|
||||||
|
'https://adm.montech.ir/api/remote/sms',
|
||||||
|
'https://adm.montest.ir/api/remote/sms',
|
||||||
|
];
|
||||||
|
|
||||||
public function enabled(): bool
|
public function enabled(): bool
|
||||||
{
|
{
|
||||||
$base = trim((string) config('services.superadmin.sms_base_url', ''));
|
if ($this->baseUrls() === []) {
|
||||||
if ($base === '') {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,40 +196,158 @@ class SuperAdminSmsClient
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
$bases = $this->orderedBaseUrls();
|
||||||
$pending = Http::withHeaders($this->authHeaders($method, $canonicalPath))
|
$method = strtoupper($method);
|
||||||
->acceptJson()
|
$lastError = null;
|
||||||
->timeout($timeout);
|
|
||||||
|
|
||||||
$url = $this->url($relativeUrl);
|
foreach ($bases as $index => $base) {
|
||||||
$method = strtoupper($method);
|
try {
|
||||||
|
$pending = Http::withHeaders($this->authHeaders($method, $canonicalPath))
|
||||||
|
->acceptJson()
|
||||||
|
->timeout($timeout);
|
||||||
|
|
||||||
/** @var Response $response */
|
$url = $this->urlForBase($base, $relativeUrl);
|
||||||
$response = match ($method) {
|
|
||||||
'GET' => $pending->get($url, $query),
|
|
||||||
'POST' => $pending->post($url, $body ?? []),
|
|
||||||
'PUT' => $pending->put($url, $body ?? []),
|
|
||||||
'PATCH' => $pending->patch($url, $body ?? []),
|
|
||||||
default => throw new \InvalidArgumentException("Unsupported method {$method}"),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!$response->successful()) {
|
/** @var Response $response */
|
||||||
Log::warning("SuperAdmin SMS {$label} failed", [
|
$response = match ($method) {
|
||||||
'status' => $response->status(),
|
'GET' => $pending->get($url, $query),
|
||||||
'body' => $response->body(),
|
'POST' => $pending->post($url, $body ?? []),
|
||||||
|
'PUT' => $pending->put($url, $body ?? []),
|
||||||
|
'PATCH' => $pending->patch($url, $body ?? []),
|
||||||
|
default => throw new \InvalidArgumentException("Unsupported method {$method}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!$response->successful()) {
|
||||||
|
$lastError = 'HTTP '.$response->status();
|
||||||
|
Log::warning("SuperAdmin SMS {$label} failed", [
|
||||||
|
'base' => $base,
|
||||||
|
'candidate' => $index + 1,
|
||||||
|
'total' => count($bases),
|
||||||
|
'status' => $response->status(),
|
||||||
|
'body' => $response->body(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $response->json();
|
||||||
|
if (!is_array($data)) {
|
||||||
|
$lastError = 'invalid JSON';
|
||||||
|
Log::warning("SuperAdmin SMS {$label} invalid JSON", [
|
||||||
|
'base' => $base,
|
||||||
|
]);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->rememberSuccessfulBase($base);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$lastError = $e->getMessage();
|
||||||
|
Log::warning("SuperAdmin SMS {$label} exception", [
|
||||||
|
'base' => $base,
|
||||||
|
'candidate' => $index + 1,
|
||||||
|
'total' => count($bases),
|
||||||
|
'error' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::warning("SuperAdmin SMS {$label} unavailable on all bases", [
|
||||||
|
'bases' => $bases,
|
||||||
|
'last_error' => $lastError,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private function orderedBaseUrls(): array
|
||||||
|
{
|
||||||
|
$bases = $this->baseUrls();
|
||||||
|
if ($bases === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$sticky = $this->readSuccessfulBase();
|
||||||
|
if ($sticky === null || !in_array($sticky, $bases, true)) {
|
||||||
|
return $bases;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique(array_merge([$sticky], $bases)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Env base (if set) first, then the three known SuperAdmin hosts.
|
||||||
|
* No .env change required when SUPERADMIN_SMS_BASE_URL already points at montzar.
|
||||||
|
*
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private function baseUrls(): array
|
||||||
|
{
|
||||||
|
$out = [];
|
||||||
|
|
||||||
|
$fromEnv = trim((string) config('services.superadmin.sms_base_url', ''));
|
||||||
|
if ($fromEnv !== '') {
|
||||||
|
foreach (preg_split('/[\s,;]+/', $fromEnv) ?: [] as $piece) {
|
||||||
|
$normalized = $this->normalizeBaseUrl((string) $piece);
|
||||||
|
if ($normalized !== '') {
|
||||||
|
$out[] = $normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (self::DEFAULT_BASES as $default) {
|
||||||
|
$out[] = $this->normalizeBaseUrl($default);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique(array_filter($out)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeBaseUrl(string $value): string
|
||||||
|
{
|
||||||
|
$base = rtrim(trim($value), '/');
|
||||||
|
if ($base === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept origin-only values like https://adm.montzar.ir
|
||||||
|
if (!str_contains($base, '/api/remote/sms')) {
|
||||||
|
$base .= '/api/remote/sms';
|
||||||
|
}
|
||||||
|
|
||||||
|
return rtrim($base, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function urlForBase(string $base, string $path): string
|
||||||
|
{
|
||||||
|
return rtrim($base, '/').'/'.ltrim($path, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function rememberSuccessfulBase(string $base): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
Cache::put(self::SUCCESS_BASE_CACHE_KEY, $base, self::SUCCESS_BASE_CACHE_TTL_SECONDS);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::debug('SuperAdmin SMS sticky base cache write failed', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function readSuccessfulBase(): ?string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$cached = Cache::get(self::SUCCESS_BASE_CACHE_KEY);
|
||||||
|
if (!is_string($cached) || trim($cached) === '') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = $response->json();
|
return $this->normalizeBaseUrl($cached);
|
||||||
|
|
||||||
return is_array($data) ? $data : null;
|
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
Log::warning("SuperAdmin SMS {$label} exception", [
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -280,11 +412,4 @@ class SuperAdminSmsClient
|
|||||||
|
|
||||||
return $host;
|
return $host;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function url(string $path): string
|
|
||||||
{
|
|
||||||
$base = rtrim((string) config('services.superadmin.sms_base_url', ''), '/');
|
|
||||||
|
|
||||||
return $base.'/'.ltrim($path, '/');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -42,7 +42,9 @@ return [
|
|||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
| SuperAdmin SMS wallet (source of truth when both env vars are set).
|
| SuperAdmin SMS wallet (source of truth when secret/token is set).
|
||||||
|
| SUPERADMIN_SMS_BASE_URL is optional preferred host; client also fails over to
|
||||||
|
| adm.montzar.ir / adm.montech.ir / adm.montest.ir automatically (no .env change needed).
|
||||||
| Example: SUPERADMIN_SMS_BASE_URL=https://adm.montzar.ir/api/remote/sms
|
| Example: SUPERADMIN_SMS_BASE_URL=https://adm.montzar.ir/api/remote/sms
|
||||||
*/
|
*/
|
||||||
'superadmin' => [
|
'superadmin' => [
|
||||||
|
|||||||
Reference in New Issue
Block a user