341 lines
12 KiB
PHP
341 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Blog\Services\AttachmentImporter;
|
|
use App\Models\Category;
|
|
use App\Models\Comment;
|
|
use App\Models\Link;
|
|
use App\Models\Post;
|
|
use App\Models\Setting;
|
|
use App\Models\User;
|
|
use App\Support\SlugGenerator;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
use PDO;
|
|
use Spatie\Tags\Tag;
|
|
|
|
class SablogImport extends Command
|
|
{
|
|
protected $signature = 'sablog:import
|
|
{--host=127.0.0.1 : 老库主机}
|
|
{--port=3306 : 老库端口}
|
|
{--database= : 老库名(必填)}
|
|
{--username=root : 老库用户名}
|
|
{--password= : 老库密码}
|
|
{--prefix=sablog_ : 老表前缀}
|
|
{--attachments-dir= : 老 attachments 目录(用于同步文件,可选)}
|
|
{--sync-attachments : 同步附件文件到新存储}
|
|
{--fresh : 清空目标表后重新导入}';
|
|
|
|
protected $description = '从 SaBlog-X 老库脚本迁移数据到 laralog';
|
|
|
|
private PDO $db;
|
|
|
|
private string $prefix;
|
|
|
|
private array $report = [];
|
|
|
|
public function handle(): int
|
|
{
|
|
$database = $this->option('database');
|
|
if (! $database) {
|
|
$this->error('必须指定 --database 老库名');
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$this->prefix = rtrim($this->option('prefix'), '_').'_';
|
|
|
|
$this->info("连接老库 {$database} ...");
|
|
$this->connect($database);
|
|
|
|
if ($this->option('fresh')) {
|
|
$this->freshTarget();
|
|
}
|
|
|
|
$this->importSettings();
|
|
$this->importCategories();
|
|
$this->importUsers();
|
|
$this->importPosts();
|
|
$this->importTags();
|
|
$this->importComments();
|
|
$this->importLinks();
|
|
$this->importAttachments();
|
|
|
|
$this->syncCounters();
|
|
|
|
$this->newLine();
|
|
$this->info('迁移完成,报告:');
|
|
foreach ($this->report as $table => $count) {
|
|
$this->line(sprintf(' %-16s %d 条', $table, $count));
|
|
}
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
private function connect(string $database): void
|
|
{
|
|
$dsn = sprintf(
|
|
'mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4',
|
|
$this->option('host'),
|
|
$this->option('port'),
|
|
$database
|
|
);
|
|
|
|
$this->db = new PDO($dsn, $this->option('username'), $this->option('password'), [
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
]);
|
|
}
|
|
|
|
private function rows(string $table): array
|
|
{
|
|
return $this->db->query('SELECT * FROM `'.$this->prefix.$table.'`')->fetchAll();
|
|
}
|
|
|
|
private function freshTarget(): void
|
|
{
|
|
$this->warn('--fresh:清空目标表...');
|
|
|
|
DB::table('media')->delete();
|
|
DB::table('taggables')->delete();
|
|
Tag::where('type', 'post')->delete();
|
|
DB::table('posts')->delete();
|
|
DB::table('categories')->delete();
|
|
DB::table('comments')->delete();
|
|
DB::table('links')->delete();
|
|
Setting::where('key', 'like', 'sablog.%')->delete();
|
|
}
|
|
|
|
/**
|
|
* 不迁移的 sablog 表(过时/垃圾化设计,由现代方案取代):
|
|
* trackbacks/trackbacklog —— 引用通告,垃圾来源
|
|
* searchindex —— 搜索缓存,现代 SQL 全文检索足够
|
|
* sessions —— Laravel 原生会话
|
|
* statistics —— 实时统计
|
|
* stylevars —— 老式广告位变量
|
|
* seccode / 验证码 —— 由 AI 审核 + 蜜罐 + 频控取代
|
|
*/
|
|
private function importSettings(): void
|
|
{
|
|
$map = [
|
|
'name' => 'site_name',
|
|
'description' => 'site_description',
|
|
'icp' => 'site_icp',
|
|
'templatename' => 'active_theme',
|
|
'audit_comment' => 'comment_audit',
|
|
'comment_min_len' => 'comment_min_len',
|
|
'comment_max_len' => 'comment_max_len',
|
|
'comment_post_space' => 'comment_post_space',
|
|
'comment_order' => 'comment_order',
|
|
'rss_num' => 'rss_num',
|
|
'title_keywords' => 'seo_default_keywords',
|
|
'meta_keywords' => 'seo_default_keywords',
|
|
'meta_description' => 'seo_default_description',
|
|
'close' => 'site_closed',
|
|
'close_note' => 'site_closed_note',
|
|
'banip_enable' => 'comment_ban_ip_enabled',
|
|
'ban_ip' => 'comment_ban_ip',
|
|
];
|
|
|
|
$count = 0;
|
|
foreach ($this->rows('settings') as $row) {
|
|
$key = $map[$row['title']] ?? 'sablog.'.$row['title'];
|
|
if ($key === 'active_theme' && $row['value'] === 'default') {
|
|
$row['value'] = 'sablog';
|
|
}
|
|
Setting::updateOrCreate(['key' => $key], ['value' => $row['value']]);
|
|
$count++;
|
|
}
|
|
|
|
$this->report['settings'] = $count;
|
|
}
|
|
|
|
private function importCategories(): void
|
|
{
|
|
$count = 0;
|
|
foreach ($this->rows('categories') as $row) {
|
|
$slug = SlugGenerator::make($row['name'], 'categories', 'slug', ignoreId: (int) $row['cid'], fallback: 'category-'.$row['cid']);
|
|
Category::query()->updateOrCreate(['id' => $row['cid']], [
|
|
'name' => $row['name'],
|
|
'slug' => $slug,
|
|
'display_order' => (int) $row['displayorder'],
|
|
'post_count' => (int) $row['articles'],
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
$count++;
|
|
}
|
|
|
|
$this->report['categories'] = $count;
|
|
}
|
|
|
|
private function importUsers(): void
|
|
{
|
|
$groupMap = [
|
|
1 => 'admin',
|
|
2 => 'editor',
|
|
3 => 'member',
|
|
];
|
|
|
|
$count = 0;
|
|
foreach ($this->rows('users') as $row) {
|
|
$email = $this->legacyEmail($row['username'], $count);
|
|
$user = User::query()->updateOrCreate(['id' => $row['userid']], [
|
|
'name' => $row['username'],
|
|
'email' => $email,
|
|
'password' => null,
|
|
'legacy_md5' => $row['password'],
|
|
'url' => $row['url'] ?: null,
|
|
'logincount' => (int) $row['logincount'],
|
|
'loginip' => $row['loginip'] ?: null,
|
|
'regip' => $row['regip'] ?: null,
|
|
'logintime' => $row['logintime'] ? date('Y-m-d H:i:s', (int) $row['logintime']) : null,
|
|
'lastpost_at' => $row['lastpost'] ? date('Y-m-d H:i:s', (int) $row['lastpost']) : null,
|
|
'created_at' => date('Y-m-d H:i:s', (int) $row['regdateline']),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
$role = $groupMap[(int) $row['groupid']] ?? 'member';
|
|
if (! $user->hasRole($role)) {
|
|
$user->assignRole($role);
|
|
}
|
|
$count++;
|
|
}
|
|
|
|
$this->report['users'] = $count;
|
|
}
|
|
|
|
private function legacyEmail(string $username, int $index): string
|
|
{
|
|
$base = strtolower(preg_replace('/[^a-z0-9._-]/i', '', $username)) ?: 'user';
|
|
|
|
return $index === 0 ? $base.'@legacy.local' : $base.'-'.$index.'@legacy.local';
|
|
}
|
|
|
|
private function importPosts(): void
|
|
{
|
|
$count = 0;
|
|
foreach ($this->rows('articles') as $row) {
|
|
$slug = SlugGenerator::make($row['title'], 'posts', 'slug', ignoreId: (int) $row['articleid'], fallback: 'post-'.$row['articleid']);
|
|
$status = (int) $row['visible'] === 1 ? 'published' : 'draft';
|
|
$dateline = date('Y-m-d H:i:s', (int) $row['dateline']);
|
|
|
|
Post::query()->updateOrCreate(['id' => $row['articleid']], [
|
|
'category_id' => (int) $row['cid'] ?: null,
|
|
'user_id' => (int) $row['uid'] ?: null,
|
|
'title' => $row['title'],
|
|
'slug' => $slug,
|
|
'excerpt' => $row['description'] ?: null,
|
|
'content' => $row['content'],
|
|
'keywords' => $row['keywords'] ?: null,
|
|
'status' => $status,
|
|
'is_sticky' => (bool) $row['stick'],
|
|
'close_comment' => (bool) $row['closecomment'],
|
|
'read_password' => $row['readpassword'] ?: null,
|
|
'views' => (int) $row['views'],
|
|
'comment_count' => (int) $row['comments'],
|
|
'published_at' => $status === 'published' ? $dateline : null,
|
|
'created_at' => $dateline,
|
|
'updated_at' => $dateline,
|
|
]);
|
|
$count++;
|
|
}
|
|
|
|
$this->report['posts'] = $count;
|
|
}
|
|
|
|
private function importTags(): void
|
|
{
|
|
$count = 0;
|
|
foreach ($this->rows('tags') as $row) {
|
|
$tagName = trim($row['tag']);
|
|
if (! $tagName) {
|
|
continue;
|
|
}
|
|
|
|
$tag = Tag::query()->where('name', $tagName)->where('type', 'post')->first();
|
|
if (! $tag) {
|
|
$tag = new Tag(['name' => $tagName, 'type' => 'post']);
|
|
$tag->slug = SlugGenerator::make($tagName, 'tags', 'slug', fallback: 'tag-'.crc32($tagName));
|
|
$tag->save();
|
|
}
|
|
|
|
foreach (explode(',', (string) $row['aids']) as $aid) {
|
|
$post = Post::find((int) trim($aid));
|
|
if ($post && ! $post->tags()->where('tags.id', $tag->id)->exists()) {
|
|
$post->tags()->attach($tag->id);
|
|
}
|
|
}
|
|
$count++;
|
|
}
|
|
|
|
$this->report['tags'] = $count;
|
|
}
|
|
|
|
private function importComments(): void
|
|
{
|
|
$count = 0;
|
|
foreach ($this->rows('comments') as $row) {
|
|
Comment::query()->updateOrCreate(['id' => $row['commentid']], [
|
|
'post_id' => (int) $row['articleid'],
|
|
'author_name' => $row['author'] ?: '匿名',
|
|
'author_url' => $row['url'] ?: null,
|
|
'content' => $row['content'],
|
|
'ip' => $row['ipaddress'] ?: null,
|
|
'status' => (int) $row['visible'] === 1 ? 'published' : 'pending',
|
|
'created_at' => date('Y-m-d H:i:s', (int) $row['dateline']),
|
|
]);
|
|
$count++;
|
|
}
|
|
|
|
$this->report['comments'] = $count;
|
|
}
|
|
|
|
private function importLinks(): void
|
|
{
|
|
$count = 0;
|
|
foreach ($this->rows('links') as $row) {
|
|
Link::query()->updateOrCreate(['id' => $row['linkid']], [
|
|
'name' => $row['name'],
|
|
'url' => $row['url'],
|
|
'note' => $row['note'] ?: null,
|
|
'display_order' => (int) $row['displayorder'],
|
|
'visible' => (bool) $row['visible'],
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
$count++;
|
|
}
|
|
|
|
$this->report['links'] = $count;
|
|
}
|
|
|
|
private function importAttachments(): void
|
|
{
|
|
if (! $this->db->query('SHOW TABLES LIKE "'.$this->prefix.'attachments"')->fetchColumn()) {
|
|
return;
|
|
}
|
|
|
|
$importer = app(AttachmentImporter::class);
|
|
$dir = $this->option('attachments-dir');
|
|
$sync = $this->option('sync-attachments');
|
|
|
|
$count = 0;
|
|
foreach ($this->rows('attachments') as $row) {
|
|
$importer->create((int) $row['articleid'], $row, $dir ?: null, $sync);
|
|
$count++;
|
|
}
|
|
|
|
$this->report['attachments'] = $count;
|
|
}
|
|
|
|
private function syncCounters(): void
|
|
{
|
|
// 以导入的真实数据为准重算统计计数
|
|
DB::table('posts')->update(['comment_count' => DB::raw('(SELECT COUNT(*) FROM comments WHERE comments.post_id = posts.id AND comments.status = "published")')]);
|
|
DB::table('categories')->update(['post_count' => DB::raw('(SELECT COUNT(*) FROM posts WHERE posts.category_id = categories.id AND posts.status = "published")')]);
|
|
}
|
|
}
|