You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

208 lines
6.1 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Console\Commands;
use App\Models\Activity;
use App\Models\StudyTour;
use App\Models\Venue;
use Illuminate\Console\Command;
/**
* 将库内已保存的绝对资源 URL如旧测试域名 http批量改为当前正式域名 https。
* 部署后请确保生产 .env 中 APP_URL=https://szkp-map.langye.net避免新上传仍带旧域名。
*/
class RewriteStorageMediaUrlsCommand extends Command
{
protected $signature = 'media:rewrite-domain
{--from=http://szkp-map.ali251.langye.net : 旧地址前缀(含协议)}
{--to=https://szkp-map.langye.net : 新地址前缀(含协议)}
{--dry-run : 只报告将修改的记录数,不写库}';
protected $description = '批量替换 venues / activities / study_tours 等媒体字段中的存储域名';
public function handle(): int
{
$from = (string) $this->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<string, mixed> $data
* @return array<string, mixed>
*/
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;
}
}