fix(settings): never plant deploy seed defaults on restore-from-backup

Ignore limangold/payping seed URLs from backup or superadmin when filling;
keep any real local value untouched unless force-overwrite.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-24 12:48:10 +03:30
co-authored by Cursor
parent 3eac5bcb3a
commit 5c542e2733
@@ -13,12 +13,10 @@ use Illuminate\Support\Facades\Schema;
* بازگرداندن مقادیر تنظیمات can_edit=0 از جدول بکاپ (پیش‌فرض settings_copy1) * بازگرداندن مقادیر تنظیمات can_edit=0 از جدول بکاپ (پیش‌فرض settings_copy1)
* به جدول settings — بعد از این‌که مالکیت این کلیدها از سوپرادمین به همین نصب برگشت. * به جدول settings — بعد از این‌که مالکیت این کلیدها از سوپرادمین به همین نصب برگشت.
* *
* قاعده سخت: اگر ردیف لوکال value غیرخالی دارد، هرگز عوض نمی‌شود * قواعد سخت:
* (حتی با --overwrite؛ برای اجبار از --force-overwrite استفاده کن). * 1) value غیرخالی لوکال هرگز عوض نمی‌شود (مگر --force-overwrite).
* * 2) پیش‌فرض‌های seed/deploy (مثل limangold.ir) هرگز به‌عنوان مقدار واقعی نوشته نمی‌شوند.
* پر کردن فقط برای ردیف غایب یا value خالی: * 3) با --fill-from-superadmin برای خالی/غایب: اول SA واقعی، بعد بکاپ واقعی.
* 1) اگر --fill-from-superadmin و سوپرادمین مقدار دارد → همان
* 2) وگرنه مقدار بکاپ
* *
* کلیدهای گروه «دسترسی مشتری به قابلیت‌ها» و runtime رد می‌شوند. * کلیدهای گروه «دسترسی مشتری به قابلیت‌ها» و runtime رد می‌شوند.
*/ */
@@ -32,10 +30,34 @@ class SettingsRestoreFromBackupCommand extends Command
protected $description = 'بازگرداندن مقادیر تنظیمات پیشرفته از جدول بکاپ به settings (بدون دست زدن به value موجود)'; protected $description = 'بازگرداندن مقادیر تنظیمات پیشرفته از جدول بکاپ به settings (بدون دست زدن به value موجود)';
/** پیش‌فرض‌های deploy که در بکاپ مانده‌اند و نباید بر مقدار واقعی سوپرادمین بچربند */ /**
private const DEPLOY_DEFAULT_VALUES = [ * پیش‌فرض‌های INSERT در deploy_settings_const — اگر فقط همین‌ها در بکاپ/SA باشند،
* نوشته نمی‌شوند (خالی می‌ماند تا مقدار واقعی پروژه از بین نرود).
*
* @var array<string, list<string>>
*/
private const KEY_SEED_DEFAULTS = [
'montzar_base_url' => [
'https://limangold.ir',
'http://limangold.ir',
],
'payping_fin_url' => [
'https://limangold.ir/kernel/admin/fin',
],
'payping_start_url' => [
'https://payping.limangold.ir/payment/start',
],
'payping_verify_url' => [
'https://payping.limangold.ir/payment/verify',
],
];
/** دامنه/مقدارهایی که در هر کلیدی seed محسوب می‌شوند */
private const GLOBAL_SEED_MARKERS = [
'https://limangold.ir', 'https://limangold.ir',
'http://limangold.ir', 'http://limangold.ir',
'https://payping.limangold.ir',
'http://payping.limangold.ir',
]; ];
public function handle(): int public function handle(): int
@@ -77,18 +99,29 @@ class SettingsRestoreFromBackupCommand extends Command
$hasType = Setting::hasTypeColumn(); $hasType = Setting::hasTypeColumn();
$report = []; $report = [];
$counts = ['inserted' => 0, 'updated' => 0, 'skipped' => 0, 'kept' => 0]; $counts = [
'inserted' => 0,
'updated' => 0,
'skipped' => 0,
'kept' => 0,
'seed_ignored' => 0,
];
foreach ($keys as $key) { foreach ($keys as $key) {
$backup = $backupRows[$key] ?? null; $backup = $backupRows[$key] ?? null;
$local = $localRows->get($key); $local = $localRows->get($key);
[$value, $source] = $this->resolveFillValue( [$value, $source, $ignoredSeed] = $this->resolveFillValue(
$key,
$backup['value'] ?? null, $backup['value'] ?? null,
$superAdminValues[$key] ?? null, $superAdminValues[$key] ?? null,
$fillFromSuperAdmin $fillFromSuperAdmin
); );
if ($ignoredSeed) {
$counts['seed_ignored']++;
}
if (! $local) { if (! $local) {
// ردیف غایب: فقط اگر مقدار واقعی داریم value می‌گذاریم؛ وگرنه خالی
$counts['inserted']++; $counts['inserted']++;
$report[] = [$key, '—', $this->mask($key, $value), 'INSERT '.($source ?: 'empty')]; $report[] = [$key, '—', $this->mask($key, $value), 'INSERT '.($source ?: 'empty')];
if (! $dryRun) { if (! $dryRun) {
@@ -109,30 +142,38 @@ class SettingsRestoreFromBackupCommand extends Command
} }
$localValue = trim((string) ($local->value ?? '')); $localValue = trim((string) ($local->value ?? ''));
$localIsDeployDefault = $this->isDeployDefault($localValue); $localIsSeed = $localValue !== '' && $this->isSeedDefault($key, $localValue);
$saReal = isset($superAdminValues[$key]) && ! $this->isSeedDefault($key, $superAdminValues[$key])
? $superAdminValues[$key]
: null;
// قاعده سخت: value واقعی موجود دست نخوره // value واقعی موجود: دست نخوره
// استثنا: لوکال فقط پیش‌فرض deploy (مثل limangold.ir) است و SA مقدار واقعی دارد // استثنا: لوکال فقط seed است و SA مقدار واقعی دارد → اصلاح اشتباه قبلی
$allowReplaceDefault = $fillFromSuperAdmin $allowReplaceSeed = $fillFromSuperAdmin && $localIsSeed && $saReal !== null;
&& $localIsDeployDefault
&& isset($superAdminValues[$key])
&& ! $this->isDeployDefault($superAdminValues[$key]);
if ($localValue !== '' && ! $forceOverwrite && ! $allowReplaceDefault) { if ($localValue !== '' && ! $forceOverwrite && ! $allowReplaceSeed) {
$counts['kept']++; $counts['kept']++;
continue; continue;
} }
if ($allowReplaceSeed) {
$value = $saReal;
$source = 'superadmin';
}
$writeValue = $value !== null && $value !== '' $writeValue = $value !== null && $value !== ''
&& ! $this->isSeedDefault($key, $value)
&& ( && (
$localValue === '' $localValue === ''
|| $allowReplaceDefault || $allowReplaceSeed
|| ($forceOverwrite && $localValue !== (string) $value) || ($forceOverwrite && $localValue !== (string) $value)
); );
// فقط meta — و فقط وقتی value را عوض نمی‌کنیم و لوکال seed/خالی نیست
// برای ردیف با value واقعی، meta هم دست نمی‌زنیم (جلوگیری از side-effect)
$metaChanged = false; $metaChanged = false;
if ($backup) { if ($backup && ($localValue === '' || $allowReplaceSeed || $forceOverwrite)) {
$metaChanged = $this->metaDiffers($local, $backup, $hasType); $metaChanged = $this->metaDiffers($local, $backup, $hasType);
} }
@@ -157,7 +198,7 @@ class SettingsRestoreFromBackupCommand extends Command
if ($writeValue) { if ($writeValue) {
$local->value = $value; $local->value = $value;
} }
if ($backup) { if ($backup && $metaChanged) {
$local->title_fa = $backup['title_fa'] ?: ($local->title_fa ?: $key); $local->title_fa = $backup['title_fa'] ?: ($local->title_fa ?: $key);
if (($backup['description'] ?? '') !== '') { if (($backup['description'] ?? '') !== '') {
$local->description = $backup['description']; $local->description = $backup['description'];
@@ -174,16 +215,17 @@ class SettingsRestoreFromBackupCommand extends Command
} }
$this->info(sprintf( $this->info(sprintf(
'%sinsert=%d update=%d kept=%d skip=%d (backup=%s, superadmin=%d)', '%sinsert=%d update=%d kept=%d skip=%d seed_ignored=%d (backup=%s, superadmin=%d)',
$dryRun ? '[dry-run] ' : '', $dryRun ? '[dry-run] ' : '',
$counts['inserted'], $counts['inserted'],
$counts['updated'], $counts['updated'],
$counts['kept'], $counts['kept'],
$counts['skipped'], $counts['skipped'],
$counts['seed_ignored'],
$table, $table,
count($superAdminValues) count($superAdminValues)
)); ));
$this->comment('kept = ردیف‌هایی که value غیرخالی داشتند و دست نخورده ماندند.'); $this->comment('kept = value واقعی لوکال دست نخورده | seed_ignored = پیش‌فرض limangold/... از منبع پر کردن حذف شد');
if ($dryRun) { if ($dryRun) {
$this->comment('هیچ چیزی نوشته نشد. برای اجرا همین دستور را بدون --dry-run بزن.'); $this->comment('هیچ چیزی نوشته نشد. برای اجرا همین دستور را بدون --dry-run بزن.');
@@ -193,45 +235,72 @@ class SettingsRestoreFromBackupCommand extends Command
} }
/** /**
* برای پر کردن خالی/غایب: با --fill-from-superadmin اول SA، بعد بکاپ. * @return array{0: ?string, 1: string, 2: bool} [value, source, ignoredSeed]
*
* @return array{0: ?string, 1: string}
*/ */
private function resolveFillValue(?string $backupValue, ?string $superAdminValue, bool $preferSuperAdmin): array private function resolveFillValue(
{ string $key,
$backupValue = $backupValue === null ? null : trim($backupValue); ?string $backupRaw,
$superAdminValue = $superAdminValue === null ? null : trim($superAdminValue); ?string $superAdminRaw,
bool $preferSuperAdmin
): array {
$backupWasSeed = $backupRaw !== null && trim($backupRaw) !== '' && $this->isSeedDefault($key, trim($backupRaw));
$saWasSeed = $superAdminRaw !== null && trim($superAdminRaw) !== '' && $this->isSeedDefault($key, trim($superAdminRaw));
$ignoredSeed = $backupWasSeed || $saWasSeed;
$backupValue = $this->nullIfSeed($key, $backupRaw);
$superAdminValue = $this->nullIfSeed($key, $superAdminRaw);
if ($preferSuperAdmin && $superAdminValue !== null && $superAdminValue !== '') { if ($preferSuperAdmin && $superAdminValue !== null && $superAdminValue !== '') {
return [$superAdminValue, 'superadmin']; return [$superAdminValue, 'superadmin', $ignoredSeed];
} }
if ($backupValue !== null && $backupValue !== '') { if ($backupValue !== null && $backupValue !== '') {
return [$backupValue, 'backup']; return [$backupValue, 'backup', $ignoredSeed];
} }
return [null, '']; return [null, '', $ignoredSeed];
} }
private function isDeployDefault(string $value): bool private function nullIfSeed(string $key, ?string $value): ?string
{
if ($value === null) {
return null;
}
$value = trim($value);
if ($value === '') {
return null;
}
if ($this->isSeedDefault($key, $value)) {
return null;
}
return $value;
}
private function isSeedDefault(string $key, string $value): bool
{ {
$normalized = rtrim(strtolower(trim($value)), '/'); $normalized = rtrim(strtolower(trim($value)), '/');
if ($normalized === '') { if ($normalized === '') {
return false; return false;
} }
foreach (self::DEPLOY_DEFAULT_VALUES as $default) { foreach (self::KEY_SEED_DEFAULTS[$key] ?? [] as $default) {
if ($normalized === rtrim(strtolower($default), '/')) { if ($normalized === rtrim(strtolower($default), '/')) {
return true; return true;
} }
} }
foreach (self::GLOBAL_SEED_MARKERS as $marker) {
$m = rtrim(strtolower($marker), '/');
if ($normalized === $m || str_starts_with($normalized, $m.'/')) {
return true;
}
}
return false; return false;
} }
/** /**
* ردیف‌های can_edit=0 بکاپ، بدون کلیدهای سوپرادمین و runtime.
*
* @return array<string, array<string, mixed>> * @return array<string, array<string, mixed>>
*/ */
private function readBackup(string $table): array private function readBackup(string $table): array
@@ -264,8 +333,6 @@ class SettingsRestoreFromBackupCommand extends Command
} }
/** /**
* مقدار غیرخالی کلیدهای غیرسوپرادمینی که هنوز در سوپرادمین مانده‌اند.
*
* @return array<string, string> * @return array<string, string>
*/ */
private function superAdminValues(): array private function superAdminValues(): array