From 1cb8c40607647179656c6d6529a8937199f15d4a Mon Sep 17 00:00:00 2001 From: adonis Date: Mon, 24 Aug 2026 12:45:33 +0330 Subject: [PATCH] fix(settings): never overwrite existing values on restore-from-backup Prefer superadmin over stale backup defaults like limangold.ir when filling empty keys. Co-authored-by: Cursor --- .../SettingsRestoreFromBackupCommand.php | 105 ++++++++++++++---- 1 file changed, 85 insertions(+), 20 deletions(-) diff --git a/app/Console/Commands/SettingsRestoreFromBackupCommand.php b/app/Console/Commands/SettingsRestoreFromBackupCommand.php index 9993e88..409ca83 100644 --- a/app/Console/Commands/SettingsRestoreFromBackupCommand.php +++ b/app/Console/Commands/SettingsRestoreFromBackupCommand.php @@ -13,27 +13,37 @@ use Illuminate\Support\Facades\Schema; * بازگرداندن مقادیر تنظیمات can_edit=0 از جدول بکاپ (پیش‌فرض settings_copy1) * به جدول settings — بعد از این‌که مالکیت این کلیدها از سوپرادمین به همین نصب برگشت. * - * کلیدهای گروه «دسترسی مشتری به قابلیت‌ها» (مالک: سوپرادمین) و کلیدهای runtime رد می‌شوند. + * قاعده سخت: اگر ردیف لوکال value غیرخالی دارد، هرگز عوض نمی‌شود + * (حتی با --overwrite؛ برای اجبار از --force-overwrite استفاده کن). * - * فالبک --fill-from-superadmin باید قبل از دیپلوی سوپرادمینِ جدید اجرا شود؛ - * بعد از آن دیپلوی، این کلیدها از سوپرادمین حذف شده‌اند و فالبکی نمی‌ماند. + * پر کردن فقط برای ردیف غایب یا value خالی: + * 1) اگر --fill-from-superadmin و سوپرادمین مقدار دارد → همان + * 2) وگرنه مقدار بکاپ + * + * کلیدهای گروه «دسترسی مشتری به قابلیت‌ها» و runtime رد می‌شوند. */ class SettingsRestoreFromBackupCommand extends Command { protected $signature = 'settings:restore-from-backup {--table=settings_copy1 : جدول بکاپ} {--dry-run : فقط گزارش، بدون نوشتن} - {--overwrite : مقدار لوکال غیرخالی را هم بازنویسی کن} - {--fill-from-superadmin : کلیدهای غایب/خالی در بکاپ را از سوپرادمین پر کن}'; + {--fill-from-superadmin : برای کلید خالی/غایب اول از سوپرادمین پر کن، بعد بکاپ} + {--force-overwrite : خطرناک — value غیرخالی لوکال را هم بازنویسی کن}'; - protected $description = 'بازگرداندن مقادیر تنظیمات پیشرفته از جدول بکاپ به settings'; + protected $description = 'بازگرداندن مقادیر تنظیمات پیشرفته از جدول بکاپ به settings (بدون دست زدن به value موجود)'; + + /** پیش‌فرض‌های deploy که در بکاپ مانده‌اند و نباید بر مقدار واقعی سوپرادمین بچربند */ + private const DEPLOY_DEFAULT_VALUES = [ + 'https://limangold.ir', + 'http://limangold.ir', + ]; public function handle(): int { $table = trim((string) $this->option('table')) ?: 'settings_copy1'; $dryRun = (bool) $this->option('dry-run'); - $overwrite = (bool) $this->option('overwrite'); $fillFromSuperAdmin = (bool) $this->option('fill-from-superadmin'); + $forceOverwrite = (bool) $this->option('force-overwrite'); if (! Schema::hasTable($table)) { $this->error("جدول بکاپ «{$table}» وجود ندارد."); @@ -41,6 +51,10 @@ class SettingsRestoreFromBackupCommand extends Command return self::FAILURE; } + if ($forceOverwrite) { + $this->warn('--force-overwrite روشن است: value غیرخالی لوکال هم ممکن است عوض شود.'); + } + $backupRows = $this->readBackup($table); $superAdminValues = $fillFromSuperAdmin ? $this->superAdminValues() : []; @@ -63,18 +77,16 @@ class SettingsRestoreFromBackupCommand extends Command $hasType = Setting::hasTypeColumn(); $report = []; - $counts = ['inserted' => 0, 'updated' => 0, 'skipped' => 0]; + $counts = ['inserted' => 0, 'updated' => 0, 'skipped' => 0, 'kept' => 0]; foreach ($keys as $key) { $backup = $backupRows[$key] ?? null; $local = $localRows->get($key); - - $value = $backup['value'] ?? null; - $source = $value === null || $value === '' ? '' : 'backup'; - if (($value === null || $value === '') && array_key_exists($key, $superAdminValues)) { - $value = $superAdminValues[$key]; - $source = 'superadmin'; - } + [$value, $source] = $this->resolveFillValue( + $backup['value'] ?? null, + $superAdminValues[$key] ?? null, + $fillFromSuperAdmin + ); if (! $local) { $counts['inserted']++; @@ -96,9 +108,28 @@ class SettingsRestoreFromBackupCommand extends Command continue; } - $localValue = (string) ($local->value ?? ''); + $localValue = trim((string) ($local->value ?? '')); + $localIsDeployDefault = $this->isDeployDefault($localValue); + + // قاعده سخت: value واقعی موجود دست نخوره + // استثنا: لوکال فقط پیش‌فرض deploy (مثل limangold.ir) است و SA مقدار واقعی دارد + $allowReplaceDefault = $fillFromSuperAdmin + && $localIsDeployDefault + && isset($superAdminValues[$key]) + && ! $this->isDeployDefault($superAdminValues[$key]); + + if ($localValue !== '' && ! $forceOverwrite && ! $allowReplaceDefault) { + $counts['kept']++; + + continue; + } + $writeValue = $value !== null && $value !== '' - && ($localValue === '' || ($overwrite && $localValue !== (string) $value)); + && ( + $localValue === '' + || $allowReplaceDefault + || ($forceOverwrite && $localValue !== (string) $value) + ); $metaChanged = false; if ($backup) { @@ -143,14 +174,16 @@ class SettingsRestoreFromBackupCommand extends Command } $this->info(sprintf( - '%sinsert=%d update=%d skip=%d (backup=%s, superadmin=%d)', + '%sinsert=%d update=%d kept=%d skip=%d (backup=%s, superadmin=%d)', $dryRun ? '[dry-run] ' : '', $counts['inserted'], $counts['updated'], + $counts['kept'], $counts['skipped'], $table, count($superAdminValues) )); + $this->comment('kept = ردیف‌هایی که value غیرخالی داشتند و دست نخورده ماندند.'); if ($dryRun) { $this->comment('هیچ چیزی نوشته نشد. برای اجرا همین دستور را بدون --dry-run بزن.'); @@ -159,6 +192,37 @@ class SettingsRestoreFromBackupCommand extends Command return self::SUCCESS; } + /** + * برای پر کردن خالی/غایب: با --fill-from-superadmin اول SA، بعد بکاپ. + * پیش‌فرض‌های deploy در بکاپ (مثل limangold.ir) اگر SA مقدار واقعی دارد، نادیده گرفته می‌شوند. + * + * @return array{0: ?string, 1: string} + */ + private function resolveFillValue(?string $backupValue, ?string $superAdminValue, bool $preferSuperAdmin): array + { + $backupValue = $backupValue === null ? null : trim($backupValue); + $superAdminValue = $superAdminValue === null ? null : trim($superAdminValue); + $backupIsDefault = $backupValue !== null && $backupValue !== '' + && in_array(rtrim(strtolower($backupValue), '/'), array_map( + static fn ($v) => rtrim(strtolower($v), '/'), + self::DEPLOY_DEFAULT_VALUES + ), true); + + if ($preferSuperAdmin && $superAdminValue !== null && $superAdminValue !== '') { + return [$superAdminValue, 'superadmin']; + } + + if ($backupValue !== null && $backupValue !== '' && ! ($backupIsDefault && $preferSuperAdmin)) { + return [$backupValue, 'backup']; + } + + if ($backupValue !== null && $backupValue !== '') { + return [$backupValue, 'backup']; + } + + return [null, '']; + } + /** * ردیف‌های can_edit=0 بکاپ، بدون کلیدهای سوپرادمین و runtime. * @@ -217,10 +281,10 @@ class SettingsRestoreFromBackupCommand extends Command continue; } $value = $item['value'] ?? null; - if ($value === null || $value === '') { + if ($value === null || trim((string) $value) === '') { continue; } - $values[$key] = (string) $value; + $values[$key] = trim((string) $value); } return $values; @@ -244,6 +308,7 @@ class SettingsRestoreFromBackupCommand extends Command if (($backup['description'] ?? '') !== '' && (string) ($local->description ?? '') !== $backup['description']) { return true; } + return $hasType && ($backup['type'] ?? '') !== '' && (string) ($local->type ?? '') !== $backup['type']; }