|
|
<?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;
|
|
|
}
|
|
|
}
|