Files

373 lines
14 KiB
PHP
Raw Permalink Normal View History

2026-08-23 22:34:18 +03:30
<?php
namespace App\Console\Commands;
use App\Models\Setting;
use App\Services\SuperAdminProgramSettingsOverlay;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/**
* بازگرداندن مقادیر تنظیمات can_edit=0 از جدول بکاپ (پیش‌فرض settings_copy1)
* به جدول settings — بعد از این‌که مالکیت این کلیدها از سوپرادمین به همین نصب برگشت.
*
* قواعد سخت:
* 1) value غیرخالی لوکال هرگز عوض نمی‌شود (مگر --force-overwrite).
* 2) پیش‌فرض‌های seed/deploy (مثل limangold.ir) هرگز به‌عنوان مقدار واقعی نوشته نمی‌شوند.
* 3) --fill-from-superadmin دیگر کلیدهای غیر دسترسی را از سوپرادمین نمی‌خواند
* (سوپرادمین فقط ۱۴ دسترسی را مالک است).
*
* کلیدهای گروه «دسترسی مشتری به قابلیت‌ها» و runtime رد می‌شوند.
2026-08-23 22:34:18 +03:30
*/
class SettingsRestoreFromBackupCommand extends Command
{
protected $signature = 'settings:restore-from-backup
{--table=settings_copy1 : جدول بکاپ}
{--dry-run : فقط گزارش، بدون نوشتن}
{--fill-from-superadmin : برای کلید خالی/غایب اول از سوپرادمین پر کن، بعد بکاپ}
{--force-overwrite : خطرناک — value غیرخالی لوکال را هم بازنویسی کن}';
2026-08-23 22:34:18 +03:30
protected $description = 'بازگرداندن مقادیر تنظیمات پیشرفته از جدول بکاپ به settings (بدون دست زدن به value موجود)';
/**
* پیش‌فرض‌های 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',
'http://limangold.ir',
'https://payping.limangold.ir',
'http://payping.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');
$forceOverwrite = (bool) $this->option('force-overwrite');
2026-08-23 22:34:18 +03:30
if (! Schema::hasTable($table)) {
$this->error("جدول بکاپ «{$table}» وجود ندارد.");
return self::FAILURE;
}
if ($forceOverwrite) {
$this->warn('--force-overwrite روشن است: value غیرخالی لوکال هم ممکن است عوض شود.');
}
2026-08-23 22:34:18 +03:30
$backupRows = $this->readBackup($table);
$superAdminValues = [];
if ($fillFromSuperAdmin) {
$this->warn('--fill-from-superadmin نادیده گرفته شد: سوپرادمین فقط دسترسی‌ها را دارد، نه بقیهٔ تنظیمات.');
}
2026-08-23 22:34:18 +03:30
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 = [];
$counts = [
'inserted' => 0,
'updated' => 0,
'skipped' => 0,
'kept' => 0,
'seed_ignored' => 0,
];
2026-08-23 22:34:18 +03:30
foreach ($keys as $key) {
$backup = $backupRows[$key] ?? null;
$local = $localRows->get($key);
[$value, $source, $ignoredSeed] = $this->resolveFillValue(
$key,
$backup['value'] ?? null,
$superAdminValues[$key] ?? null,
$fillFromSuperAdmin
);
if ($ignoredSeed) {
$counts['seed_ignored']++;
}
2026-08-23 22:34:18 +03:30
if (! $local) {
// ردیف غایب: فقط اگر مقدار واقعی داریم value می‌گذاریم؛ وگرنه خالی
2026-08-23 22:34:18 +03:30
$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;
}
$localValue = trim((string) ($local->value ?? ''));
$localIsSeed = $localValue !== '' && $this->isSeedDefault($key, $localValue);
$saReal = isset($superAdminValues[$key]) && ! $this->isSeedDefault($key, $superAdminValues[$key])
? $superAdminValues[$key]
: null;
// value واقعی موجود: دست نخوره
// استثنا: لوکال فقط seed است و SA مقدار واقعی دارد → اصلاح اشتباه قبلی
$allowReplaceSeed = $fillFromSuperAdmin && $localIsSeed && $saReal !== null;
if ($localValue !== '' && ! $forceOverwrite && ! $allowReplaceSeed) {
$counts['kept']++;
continue;
}
if ($allowReplaceSeed) {
$value = $saReal;
$source = 'superadmin';
}
2026-08-23 22:34:18 +03:30
$writeValue = $value !== null && $value !== ''
&& ! $this->isSeedDefault($key, $value)
&& (
$localValue === ''
|| $allowReplaceSeed
|| ($forceOverwrite && $localValue !== (string) $value)
);
2026-08-23 22:34:18 +03:30
// فقط meta — و فقط وقتی value را عوض نمی‌کنیم و لوکال seed/خالی نیست
// برای ردیف با value واقعی، meta هم دست نمی‌زنیم (جلوگیری از side-effect)
2026-08-23 22:34:18 +03:30
$metaChanged = false;
if ($backup && ($localValue === '' || $allowReplaceSeed || $forceOverwrite)) {
2026-08-23 22:34:18 +03:30
$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 && $metaChanged) {
2026-08-23 22:34:18 +03:30
$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(
'%sinsert=%d update=%d kept=%d skip=%d seed_ignored=%d (backup=%s, superadmin=%d)',
2026-08-23 22:34:18 +03:30
$dryRun ? '[dry-run] ' : '',
$counts['inserted'],
$counts['updated'],
$counts['kept'],
2026-08-23 22:34:18 +03:30
$counts['skipped'],
$counts['seed_ignored'],
2026-08-23 22:34:18 +03:30
$table,
count($superAdminValues)
));
$this->comment('kept = value واقعی لوکال دست نخورده | seed_ignored = پیش‌فرض limangold/... از منبع پر کردن حذف شد');
2026-08-23 22:34:18 +03:30
if ($dryRun) {
$this->comment('هیچ چیزی نوشته نشد. برای اجرا همین دستور را بدون --dry-run بزن.');
}
return self::SUCCESS;
}
/**
* @return array{0: ?string, 1: string, 2: bool} [value, source, ignoredSeed]
*/
private function resolveFillValue(
string $key,
?string $backupRaw,
?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 !== '') {
return [$superAdminValue, 'superadmin', $ignoredSeed];
}
if ($backupValue !== null && $backupValue !== '') {
return [$backupValue, 'backup', $ignoredSeed];
}
return [null, '', $ignoredSeed];
}
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)), '/');
if ($normalized === '') {
return false;
}
foreach (self::KEY_SEED_DEFAULTS[$key] ?? [] as $default) {
if ($normalized === rtrim(strtolower($default), '/')) {
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;
}
2026-08-23 22:34:18 +03:30
/**
* @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;
}
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-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;
}
}