option('from'); $to = (string) $this->option('to'); $dry = (bool) $this->option('dry-run'); if ($from === $to) { $this->error('--from 与 --to 不能相同。'); return self::FAILURE; } $total = 0; $total += $this->rewriteVenues($from, $to, $dry); $total += $this->rewriteActivities($from, $to, $dry); $total += $this->rewriteStudyTours($from, $to, $dry); if ($dry) { $this->info("[dry-run] 共将更新 {$total} 条记录(未写入数据库)。"); } else { $this->info("已更新 {$total} 条记录。"); } return self::SUCCESS; } protected function rewriteVenues(string $from, string $to, bool $dry): int { $n = 0; Venue::query()->orderBy('id')->chunkById(100, function ($venues) use ($from, $to, $dry, &$n) { foreach ($venues as $venue) { if ($this->applyVenueOrActivity($venue, $from, $to, $dry)) { $n++; } } }); $this->line("venues: {$n}"); return $n; } protected function rewriteActivities(string $from, string $to, bool $dry): int { $n = 0; Activity::query()->orderBy('id')->chunkById(100, function ($activities) use ($from, $to, $dry, &$n) { foreach ($activities as $activity) { if ($this->applyVenueOrActivity($activity, $from, $to, $dry)) { $n++; } } }); $this->line("activities: {$n}"); return $n; } protected function rewriteStudyTours(string $from, string $to, bool $dry): int { $n = 0; StudyTour::query()->orderBy('id')->chunkById(100, function ($rows) use ($from, $to, $dry, &$n) { foreach ($rows as $row) { $dirty = false; if ($this->replaceStringField($row, 'cover_image', $from, $to)) { $dirty = true; } if ($this->replaceStringField($row, 'intro_html', $from, $to)) { $dirty = true; } if ($this->replaceGalleryMedia($row, $from, $to)) { $dirty = true; } if ($dirty) { $n++; if (! $dry) { $row->save(); } } } }); $this->line("study_tours: {$n}"); return $n; } /** @param Venue|Activity $model */ protected function applyVenueOrActivity(Venue|Activity $model, string $from, string $to, bool $dry): bool { $dirty = false; if ($this->replaceStringField($model, 'cover_image', $from, $to)) { $dirty = true; } if ($this->replaceStringField($model, 'detail_html', $from, $to)) { $dirty = true; } if ($this->replaceGalleryMedia($model, $from, $to)) { $dirty = true; } if ($this->replaceJsonSnapshotField($model, 'last_approved_snapshot', $from, $to)) { $dirty = true; } if ($dirty && ! $dry) { $model->save(); } return $dirty; } protected function replaceStringField(object $model, string $attr, string $from, string $to): bool { $v = $model->{$attr}; if (! is_string($v) || $v === '') { return false; } $next = str_replace($from, $to, $v); if ($next === $v) { return false; } $model->{$attr} = $next; return true; } protected function replaceGalleryMedia(object $model, string $from, string $to): bool { $gm = $model->gallery_media ?? null; if (! is_array($gm) || $gm === []) { return false; } $changed = false; foreach ($gm as $i => $item) { if (! is_array($item)) { continue; } $url = $item['url'] ?? null; if (is_string($url) && str_contains($url, $from)) { $gm[$i]['url'] = str_replace($from, $to, $url); $changed = true; } } if ($changed) { $model->gallery_media = array_values($gm); } return $changed; } protected function replaceJsonSnapshotField(object $model, string $attr, string $from, string $to): bool { $snap = $model->{$attr}; if ($snap === null || $snap === []) { return false; } if (! is_array($snap)) { return false; } $next = $this->replaceStringsRecursive($snap, $from, $to); if ($next === $snap) { return false; } $model->{$attr} = $next; return true; } /** * @param array $data * @return array */ protected function replaceStringsRecursive(array $data, string $from, string $to): array { foreach ($data as $k => $v) { if (is_string($v)) { $data[$k] = str_replace($from, $to, $v); } elseif (is_array($v)) { $data[$k] = $this->replaceStringsRecursive($v, $from, $to); } } return $data; } }