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 <cursoragent@cursor.com>
This commit is contained in:
2026-08-24 12:45:33 +03:30
co-authored by Cursor
parent d2fd315e8a
commit 1cb8c40607
@@ -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'];
}