2026-08-23 22:34:18 +03:30
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
|
|
|
|
|
|
use App\Models\Setting;
|
|
|
|
|
use App\Services\SuperAdminProgramSettingsClient;
|
|
|
|
|
use App\Services\SuperAdminProgramSettingsOverlay;
|
|
|
|
|
use Illuminate\Console\Command;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* بازگرداندن مقادیر تنظیمات can_edit=0 از جدول بکاپ (پیشفرض settings_copy1)
|
|
|
|
|
* به جدول settings — بعد از اینکه مالکیت این کلیدها از سوپرادمین به همین نصب برگشت.
|
|
|
|
|
*
|
2026-08-24 12:45:33 +03:30
|
|
|
* قاعده سخت: اگر ردیف لوکال value غیرخالی دارد، هرگز عوض نمیشود
|
|
|
|
|
* (حتی با --overwrite؛ برای اجبار از --force-overwrite استفاده کن).
|
2026-08-23 22:34:18 +03:30
|
|
|
*
|
2026-08-24 12:45:33 +03:30
|
|
|
* پر کردن فقط برای ردیف غایب یا value خالی:
|
|
|
|
|
* 1) اگر --fill-from-superadmin و سوپرادمین مقدار دارد → همان
|
|
|
|
|
* 2) وگرنه مقدار بکاپ
|
|
|
|
|
*
|
|
|
|
|
* کلیدهای گروه «دسترسی مشتری به قابلیتها» و runtime رد میشوند.
|
2026-08-23 22:34:18 +03:30
|
|
|
*/
|
|
|
|
|
class SettingsRestoreFromBackupCommand extends Command
|
|
|
|
|
{
|
|
|
|
|
protected $signature = 'settings:restore-from-backup
|
|
|
|
|
{--table=settings_copy1 : جدول بکاپ}
|
|
|
|
|
{--dry-run : فقط گزارش، بدون نوشتن}
|
2026-08-24 12:45:33 +03:30
|
|
|
{--fill-from-superadmin : برای کلید خالی/غایب اول از سوپرادمین پر کن، بعد بکاپ}
|
|
|
|
|
{--force-overwrite : خطرناک — value غیرخالی لوکال را هم بازنویسی کن}';
|
2026-08-23 22:34:18 +03:30
|
|
|
|
2026-08-24 12:45:33 +03:30
|
|
|
protected $description = 'بازگرداندن مقادیر تنظیمات پیشرفته از جدول بکاپ به settings (بدون دست زدن به value موجود)';
|
|
|
|
|
|
|
|
|
|
/** پیشفرضهای deploy که در بکاپ ماندهاند و نباید بر مقدار واقعی سوپرادمین بچربند */
|
|
|
|
|
private const DEPLOY_DEFAULT_VALUES = [
|
|
|
|
|
'https://limangold.ir',
|
|
|
|
|
'http://limangold.ir',
|
|
|
|
|
];
|
2026-08-23 22:34:18 +03:30
|
|
|
|
|
|
|
|
public function handle(): int
|
|
|
|
|
{
|
|
|
|
|
$table = trim((string) $this->option('table')) ?: 'settings_copy1';
|
|
|
|
|
$dryRun = (bool) $this->option('dry-run');
|
|
|
|
|
$fillFromSuperAdmin = (bool) $this->option('fill-from-superadmin');
|
2026-08-24 12:45:33 +03:30
|
|
|
$forceOverwrite = (bool) $this->option('force-overwrite');
|
2026-08-23 22:34:18 +03:30
|
|
|
|
|
|
|
|
if (! Schema::hasTable($table)) {
|
|
|
|
|
$this->error("جدول بکاپ «{$table}» وجود ندارد.");
|
|
|
|
|
|
|
|
|
|
return self::FAILURE;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-24 12:45:33 +03:30
|
|
|
if ($forceOverwrite) {
|
|
|
|
|
$this->warn('--force-overwrite روشن است: value غیرخالی لوکال هم ممکن است عوض شود.');
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-23 22:34:18 +03:30
|
|
|
$backupRows = $this->readBackup($table);
|
|
|
|
|
$superAdminValues = $fillFromSuperAdmin ? $this->superAdminValues() : [];
|
|
|
|
|
|
|
|
|
|
if ($backupRows === [] && $superAdminValues === []) {
|
|
|
|
|
$this->warn('هیچ کلید قابل بازگردانی پیدا نشد.');
|
|
|
|
|
|
|
|
|
|
return self::SUCCESS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$keys = array_values(array_unique(array_merge(
|
|
|
|
|
array_keys($backupRows),
|
|
|
|
|
array_keys($superAdminValues)
|
|
|
|
|
)));
|
|
|
|
|
sort($keys);
|
|
|
|
|
|
|
|
|
|
$localRows = Setting::query()
|
|
|
|
|
->whereIn('key', $keys)
|
|
|
|
|
->get()
|
|
|
|
|
->keyBy('key');
|
|
|
|
|
|
|
|
|
|
$hasType = Setting::hasTypeColumn();
|
|
|
|
|
$report = [];
|
2026-08-24 12:45:33 +03:30
|
|
|
$counts = ['inserted' => 0, 'updated' => 0, 'skipped' => 0, 'kept' => 0];
|
2026-08-23 22:34:18 +03:30
|
|
|
|
|
|
|
|
foreach ($keys as $key) {
|
|
|
|
|
$backup = $backupRows[$key] ?? null;
|
|
|
|
|
$local = $localRows->get($key);
|
2026-08-24 12:45:33 +03:30
|
|
|
[$value, $source] = $this->resolveFillValue(
|
|
|
|
|
$backup['value'] ?? null,
|
|
|
|
|
$superAdminValues[$key] ?? null,
|
|
|
|
|
$fillFromSuperAdmin
|
|
|
|
|
);
|
2026-08-23 22:34:18 +03:30
|
|
|
|
|
|
|
|
if (! $local) {
|
|
|
|
|
$counts['inserted']++;
|
|
|
|
|
$report[] = [$key, '—', $this->mask($key, $value), 'INSERT '.($source ?: 'empty')];
|
|
|
|
|
if (! $dryRun) {
|
|
|
|
|
$row = new Setting();
|
|
|
|
|
$row->key = $key;
|
|
|
|
|
$row->value = $value ?? '';
|
|
|
|
|
$row->title_fa = $backup['title_fa'] ?? $key;
|
|
|
|
|
$row->description = $backup['description'] ?? '';
|
|
|
|
|
$row->is_active = $backup['is_active'] ?? 1;
|
|
|
|
|
$row->can_edit = 0;
|
|
|
|
|
if ($hasType) {
|
|
|
|
|
$row->type = $backup['type'] ?? '';
|
|
|
|
|
}
|
|
|
|
|
$row->save();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-24 12:45:33 +03:30
|
|
|
$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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-23 22:34:18 +03:30
|
|
|
$writeValue = $value !== null && $value !== ''
|
2026-08-24 12:45:33 +03:30
|
|
|
&& (
|
|
|
|
|
$localValue === ''
|
|
|
|
|
|| $allowReplaceDefault
|
|
|
|
|
|| ($forceOverwrite && $localValue !== (string) $value)
|
|
|
|
|
);
|
2026-08-23 22:34:18 +03:30
|
|
|
|
|
|
|
|
$metaChanged = false;
|
|
|
|
|
if ($backup) {
|
|
|
|
|
$metaChanged = $this->metaDiffers($local, $backup, $hasType);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (! $writeValue && ! $metaChanged) {
|
|
|
|
|
$counts['skipped']++;
|
|
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$counts['updated']++;
|
|
|
|
|
$report[] = [
|
|
|
|
|
$key,
|
|
|
|
|
$this->mask($key, $localValue),
|
|
|
|
|
$writeValue ? $this->mask($key, $value) : '(بدون تغییر)',
|
|
|
|
|
$writeValue ? 'UPDATE '.$source : 'META',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if ($dryRun) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($writeValue) {
|
|
|
|
|
$local->value = $value;
|
|
|
|
|
}
|
|
|
|
|
if ($backup) {
|
|
|
|
|
$local->title_fa = $backup['title_fa'] ?: ($local->title_fa ?: $key);
|
|
|
|
|
if (($backup['description'] ?? '') !== '') {
|
|
|
|
|
$local->description = $backup['description'];
|
|
|
|
|
}
|
|
|
|
|
if ($hasType && ($backup['type'] ?? '') !== '') {
|
|
|
|
|
$local->type = $backup['type'];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$local->save();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($report !== []) {
|
|
|
|
|
$this->table(['key', 'مقدار لوکال', 'مقدار جدید', 'اقدام'], $report);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->info(sprintf(
|
2026-08-24 12:45:33 +03:30
|
|
|
'%sinsert=%d update=%d kept=%d skip=%d (backup=%s, superadmin=%d)',
|
2026-08-23 22:34:18 +03:30
|
|
|
$dryRun ? '[dry-run] ' : '',
|
|
|
|
|
$counts['inserted'],
|
|
|
|
|
$counts['updated'],
|
2026-08-24 12:45:33 +03:30
|
|
|
$counts['kept'],
|
2026-08-23 22:34:18 +03:30
|
|
|
$counts['skipped'],
|
|
|
|
|
$table,
|
|
|
|
|
count($superAdminValues)
|
|
|
|
|
));
|
2026-08-24 12:45:33 +03:30
|
|
|
$this->comment('kept = ردیفهایی که value غیرخالی داشتند و دست نخورده ماندند.');
|
2026-08-23 22:34:18 +03:30
|
|
|
|
|
|
|
|
if ($dryRun) {
|
|
|
|
|
$this->comment('هیچ چیزی نوشته نشد. برای اجرا همین دستور را بدون --dry-run بزن.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return self::SUCCESS;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-24 12:45:33 +03:30
|
|
|
/**
|
|
|
|
|
* برای پر کردن خالی/غایب: با --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, ''];
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-23 22:34:18 +03:30
|
|
|
/**
|
|
|
|
|
* ردیفهای can_edit=0 بکاپ، بدون کلیدهای سوپرادمین و runtime.
|
|
|
|
|
*
|
|
|
|
|
* @return array<string, array<string, mixed>>
|
|
|
|
|
*/
|
|
|
|
|
private function readBackup(string $table): array
|
|
|
|
|
{
|
|
|
|
|
$hasType = Schema::hasColumn($table, 'type');
|
|
|
|
|
$columns = ['key', 'value', 'title_fa', 'description', 'is_active', 'can_edit'];
|
|
|
|
|
if ($hasType) {
|
|
|
|
|
$columns[] = 'type';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$rows = [];
|
|
|
|
|
foreach (DB::table($table)->select($columns)->orderBy('key')->get() as $row) {
|
|
|
|
|
$key = trim((string) ($row->key ?? ''));
|
|
|
|
|
if ($key === '' || ! $this->isRestorable($key)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if ((int) ($row->can_edit ?? 0) !== 0) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$rows[$key] = [
|
|
|
|
|
'value' => $row->value === null ? '' : (string) $row->value,
|
|
|
|
|
'title_fa' => (string) ($row->title_fa ?? ''),
|
|
|
|
|
'description' => (string) ($row->description ?? ''),
|
|
|
|
|
'is_active' => (int) ($row->is_active ?? 1),
|
|
|
|
|
'type' => $hasType ? (string) ($row->type ?? '') : '',
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $rows;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* مقدار غیرخالی کلیدهای غیرسوپرادمینی که هنوز در سوپرادمین ماندهاند.
|
|
|
|
|
*
|
|
|
|
|
* @return array<string, string>
|
|
|
|
|
*/
|
|
|
|
|
private function superAdminValues(): array
|
|
|
|
|
{
|
|
|
|
|
$items = app(SuperAdminProgramSettingsClient::class)->fetchItems();
|
|
|
|
|
if (! is_array($items)) {
|
|
|
|
|
$this->warn('سوپرادمین در دسترس نبود — فالبک نادیده گرفته شد.');
|
|
|
|
|
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$values = [];
|
|
|
|
|
foreach ($items as $item) {
|
|
|
|
|
if (! is_array($item)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$key = trim((string) ($item['key'] ?? ''));
|
|
|
|
|
if ($key === '' || ! $this->isRestorable($key)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$value = $item['value'] ?? null;
|
2026-08-24 12:45:33 +03:30
|
|
|
if ($value === null || trim((string) $value) === '') {
|
2026-08-23 22:34:18 +03:30
|
|
|
continue;
|
|
|
|
|
}
|
2026-08-24 12:45:33 +03:30
|
|
|
$values[$key] = trim((string) $value);
|
2026-08-23 22:34:18 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $values;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function isRestorable(string $key): bool
|
|
|
|
|
{
|
|
|
|
|
if (SuperAdminProgramSettingsOverlay::isSuperAdminOwned($key)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ! SuperAdminProgramSettingsOverlay::isRuntime($key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @param array<string, mixed> $backup */
|
|
|
|
|
private function metaDiffers(Setting $local, array $backup, bool $hasType): bool
|
|
|
|
|
{
|
|
|
|
|
if (($backup['title_fa'] ?? '') !== '' && (string) ($local->title_fa ?? '') !== $backup['title_fa']) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (($backup['description'] ?? '') !== '' && (string) ($local->description ?? '') !== $backup['description']) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-08-24 12:45:33 +03:30
|
|
|
|
2026-08-23 22:34:18 +03:30
|
|
|
return $hasType && ($backup['type'] ?? '') !== '' && (string) ($local->type ?? '') !== $backup['type'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function mask(string $key, ?string $value): string
|
|
|
|
|
{
|
|
|
|
|
$value = (string) $value;
|
|
|
|
|
if ($value === '') {
|
|
|
|
|
return '(خالی)';
|
|
|
|
|
}
|
|
|
|
|
if (Setting::isSecretOrServerOnlyKey($key)) {
|
|
|
|
|
return '***('.mb_strlen($value).' نویسه)';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return mb_strlen($value) > 40 ? mb_substr($value, 0, 37).'…' : $value;
|
|
|
|
|
}
|
|
|
|
|
}
|