fix(settings): drop program-settings last-good cache and prefer montzar

Stale Redis overlay and SMS-origin-first fetch kept feature flags like customer_special_pricing false after SuperAdmin turned them on. Always fetch live and try adm.montzar.ir first.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-29 22:39:08 +03:30
co-authored by Cursor
parent cecb84bad9
commit 63195718a2
4 changed files with 66 additions and 73 deletions
@@ -8,9 +8,9 @@ use Illuminate\Console\Command;
class SuperAdminProgramSettingsRefreshCommand extends Command
{
protected $signature = 'superadmin:refresh-program-settings
{--clear-only : فقط کش last-good را پاک کن}';
{--clear-only : فقط memo درخواست را ریست کن (دیگر کش Redis نیست)}';
protected $description = 'کش overlay تنظیمات سوپرادمین را پاک/تازه می‌کند';
protected $description = 'تنظیمات دسترسی را دوباره از سوپرادمین می‌خواند (بدون کش)';
public function handle(): int
{
@@ -18,20 +18,24 @@ class SuperAdminProgramSettingsRefreshCommand extends Command
if ($this->option('clear-only')) {
$overlay->flushCache();
$this->line('cleared SuperAdmin program-settings cache');
$this->line('cleared SuperAdmin program-settings request memo');
return self::SUCCESS;
}
$result = $overlay->refreshFromSuperAdmin();
if (! ($result['ok'] ?? false)) {
$this->error('fetch from SuperAdmin failed — cache cleared; next HTTP request will retry');
$this->error('fetch from SuperAdmin failed; next HTTP request will retry');
return self::FAILURE;
}
$items = is_array($result['items'] ?? null) ? $result['items'] : [];
$meta = is_array($result['meta'] ?? null) ? $result['meta'] : [];
$this->info('fetched '.(int) ($result['count'] ?? 0).' setting(s) from SuperAdmin');
if ($meta !== []) {
$this->line('meta: '.json_encode($meta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
}
foreach ($items as $key => $value) {
$this->line(" {$key}={$value}");
}
@@ -94,10 +94,11 @@ class SettingsController extends Controller
if (! ($result['ok'] ?? false)) {
return response()->json([
'message' => 'ارتباط با سوپرادمین برقرار نشد؛ کش پاک شد و در درخواست بعدی دوباره تلاش می‌شود',
'message' => 'ارتباط با سوپرادمین برقرار نشد؛ در درخواست بعدی دوباره تلاش می‌شود',
'ok' => false,
'count' => 0,
'items' => [],
'meta' => $result['meta'] ?? [],
], 502);
}
@@ -109,6 +110,7 @@ class SettingsController extends Controller
'count' => $count,
'items' => $result['items'] ?? [],
'source' => $result['source'] ?? 'superadmin',
'meta' => $result['meta'] ?? [],
]);
}
@@ -22,8 +22,21 @@ class SuperAdminProgramSettingsClient
public const RETRY_ROUNDS = 3;
/** @var array{channel?: string, origin?: string, domain?: string}|null */
private ?array $lastFetchMeta = null;
/**
* @return array{channel?: string, origin?: string, domain?: string}
*/
public function lastFetchMeta(): array
{
return $this->lastFetchMeta ?? [];
}
public function fetchItems(): ?array
{
$this->lastFetchMeta = null;
$public = $this->fetchPublic();
if ($public !== null) {
return $public;
@@ -68,7 +81,12 @@ class SuperAdminProgramSettingsClient
}
$items = $this->extractItems($response->json());
if ($items !== null) {
if ($this->hasSettingRows($items)) {
$this->lastFetchMeta = [
'channel' => 'remote',
'origin' => $origin,
];
return $items;
}
} catch (\Throwable $e) {
@@ -96,16 +114,29 @@ class SuperAdminProgramSettingsClient
foreach ($this->attemptOrigins() as $origin) {
try {
$response = Http::acceptJson()
->withHeaders(['Cache-Control' => 'no-store'])
->withHeaders([
'Cache-Control' => 'no-store',
'Pragma' => 'no-cache',
])
->withOptions(['http_errors' => false])
->timeout(6)
->get($origin.self::PUBLIC_PATH, ['domain' => $domain]);
->get($origin.self::PUBLIC_PATH, [
'domain' => $domain,
'_ts' => (string) time(),
]);
if (! $response->successful() || ! $this->isJsonResponse($response)) {
continue;
}
$items = $this->extractItems($response->json());
if ($items !== null) {
if ($this->hasSettingRows($items)) {
$this->lastFetchMeta = [
'channel' => 'public',
'origin' => $origin,
'domain' => $domain,
];
return $items;
}
} catch (\Throwable $e) {
@@ -137,6 +168,9 @@ class SuperAdminProgramSettingsClient
}
/**
* اول montzar (منبع اصلی)، بعد origin/SMS صریح، بعد failover.
* قبلاً SMS اول بود و اگر رپلیکای کهنه جواب ۲۰۰ می‌داد، montzar هیچ‌وقت خوانده نمی‌شد.
*
* @return list<string>
*/
private function origins(): array
@@ -162,7 +196,16 @@ class SuperAdminProgramSettingsClient
}
}
return array_values(array_unique(array_merge($extra, $defaults)));
// montzar اول؛ extra فقط اگر هنوز در لیست نیست به انتها اضافه می‌شود
return array_values(array_unique(array_merge($defaults, $extra)));
}
/**
* @param array<int, array<string, mixed>>|null $items
*/
private function hasSettingRows(?array $items): bool
{
return is_array($items) && $items !== [];
}
private function isJsonResponse($response): bool
@@ -2,22 +2,12 @@
namespace App\Services;
use Illuminate\Support\Facades\Cache;
/**
* تنظیمات پیشرفته سوپرادمین برای ConfigService و get_settings.
* در هر درخواست یک بار fetch می‌شود؛ آخرین پاسخ سالم برای قطعی لحظه‌ای کش می‌ماند.
* هر درخواست HTTP یک بار از سوپرادمین می‌خواند (memo فقط داخل همان درخواست) — بدون کش Redis.
*/
class SuperAdminProgramSettingsOverlay
{
/**
* آخرین پاسخ سالم سوپرادمین — فقط کلیدهای دسترسی (SUPERADMIN_OWNED_KEYS).
* پاسخ remote ممکن است کلیدهای دیگری هم داشته باشد؛ آن‌ها ذخیره و اعمال نمی‌شوند.
*/
private const LAST_GOOD_CACHE_KEY = 'superadmin:program_settings:last_good';
private const LAST_GOOD_CACHE_MINUTES = 1440;
/**
* فقط این ۱۴ دسترسی از سوپرادمین خوانده می‌شود.
* هر کلید دیگری (قیمت، ساعت کاری، برندینگ، …) مال پنل خانگی است.
@@ -73,26 +63,22 @@ class SuperAdminProgramSettingsOverlay
private static bool $loaded = false;
/** پاک کردن کش last-good و memo درخواست — برای دکمهٔ همگام‌سازی تنظیمات پیشرفته */
/** پاک کردن memo درخواست — برای دکمهٔ همگام‌سازی / بروزرسانی */
public function flushCache(): void
{
self::$memo = null;
self::$loaded = false;
try {
Cache::forget(self::LAST_GOOD_CACHE_KEY);
} catch (\Throwable $e) {
// کش در دسترس نیست
}
}
/**
* کش را پاک می‌کند و دوباره از سوپرادمین می‌خواند.
* دوباره از سوپرادمین می‌خواند (بدون کش پایدار).
*
* @return array{ok: bool, count: int, items: array<string, string>, source: string}
* @return array{ok: bool, count: int, items: array<string, string>, source: string, meta?: array<string, mixed>}
*/
public function refreshFromSuperAdmin(): array
{
$this->flushCache();
$client = app(SuperAdminProgramSettingsClient::class);
$map = $this->catalogMap();
if ($map === null) {
return [
@@ -100,6 +86,7 @@ class SuperAdminProgramSettingsOverlay
'count' => 0,
'items' => [],
'source' => 'unavailable',
'meta' => $client->lastFetchMeta(),
];
}
@@ -120,6 +107,7 @@ class SuperAdminProgramSettingsOverlay
'count' => count($items),
'items' => $items,
'source' => 'superadmin',
'meta' => $client->lastFetchMeta(),
];
}
@@ -170,17 +158,6 @@ class SuperAdminProgramSettingsOverlay
$items = app(SuperAdminProgramSettingsClient::class)->fetchItems();
if ($items !== null) {
$this->rememberLastGood($items);
} else {
$items = $this->lastGoodItems();
if ($items !== null) {
\Illuminate\Support\Facades\Log::warning('SuperAdmin program-settings using last-good cache', [
'count' => count($items),
]);
}
}
if ($items === null) {
self::$memo = null;
@@ -216,39 +193,6 @@ class SuperAdminProgramSettingsOverlay
return $map;
}
/** @param array<int, array<string, mixed>> $items */
private function rememberLastGood(array $items): void
{
$owned = array_values(self::ownedItemsMap($items));
if ($owned === []) {
return;
}
try {
Cache::put(
self::LAST_GOOD_CACHE_KEY,
$owned,
now()->addMinutes(self::LAST_GOOD_CACHE_MINUTES)
);
} catch (\Throwable $e) {
// کش در دسترس نیست — همان رفتار قبلی
}
}
/**
* @return array<int, array<string, mixed>>|null
*/
private function lastGoodItems(): ?array
{
try {
$cached = Cache::get(self::LAST_GOOD_CACHE_KEY);
} catch (\Throwable $e) {
return null;
}
return is_array($cached) && $cached !== [] ? $cached : null;
}
/**
* @return array<int, array<string, mixed>>
*/