diff --git a/app/Blog/Services/AttachmentImporter.php b/app/Blog/Services/AttachmentImporter.php new file mode 100644 index 0000000..a4522c8 --- /dev/null +++ b/app/Blog/Services/AttachmentImporter.php @@ -0,0 +1,87 @@ + 0]); + } + + $properties = [ + 'downloads' => (int) ($row['downloads'] ?? 0), + 'isimage' => (bool) ($row['isimage'] ?? false), + 'legacy_filepath' => $row['filepath'] ?? null, + 'legacy_thumb' => $row['thumb_filepath'] ?? null, + 'legacy_articleid' => (int) ($row['articleid'] ?? 0), + 'pending_sync' => true, + ]; + + $source = $this->locateSource($row['filepath'] ?? null, $attachmentsDir); + + if ($source && $syncNow) { + $media = $post->addMedia($source) + ->preservingOriginal() + ->withCustomProperties($properties) + ->usingFileName(basename($row['filename']) ?: basename($source)) + ->toMediaCollection('attachments', MediaDisk::name()); + + $media->setCustomProperty('pending_sync', false); + $media->save(); + + return $media; + } + + return $this->createPendingRecord($post->id, $row, $properties); + } + + private function locateSource(?string $filepath, ?string $attachmentsDir): ?string + { + if (! $attachmentsDir || ! $filepath) { + return null; + } + + $candidate = rtrim($attachmentsDir, '/').'/'.ltrim($filepath, '/'); + + return is_file($candidate) ? $candidate : null; + } + + private function createPendingRecord(int $postId, array $row, array $properties): Media + { + $disk = MediaDisk::name(); + + $media = new Media; + $media->model_type = Post::class; + $media->model_id = $postId; + $media->uuid = (string) Str::uuid(); + $media->collection_name = 'attachments'; + $media->name = pathinfo($row['filename'] ?? 'attachment', PATHINFO_FILENAME) ?: 'attachment'; + $media->file_name = $row['filename'] ?: basename($row['filepath'] ?? 'attachment.bin'); + $media->mime_type = $row['filetype'] ?: null; + $media->size = (int) ($row['filesize'] ?? 0); + $media->disk = $disk; + $media->conversions_disk = $disk; + $media->manipulations = []; + $media->custom_properties = $properties; + $media->generated_conversions = []; + $media->responsive_images = []; + $media->save(); + + return $media; + } +} diff --git a/app/Console/Commands/SablogImport.php b/app/Console/Commands/SablogImport.php new file mode 100644 index 0000000..74d2711 --- /dev/null +++ b/app/Console/Commands/SablogImport.php @@ -0,0 +1,340 @@ +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")')]); + } +} diff --git a/app/Models/Category.php b/app/Models/Category.php new file mode 100644 index 0000000..178d887 --- /dev/null +++ b/app/Models/Category.php @@ -0,0 +1,29 @@ +hasMany(Post::class); + } + + public function getUrlAttribute(): string + { + return route('category.show', $this->slug ?? $this->id); + } +} diff --git a/app/Models/Comment.php b/app/Models/Comment.php new file mode 100644 index 0000000..502b14d --- /dev/null +++ b/app/Models/Comment.php @@ -0,0 +1,50 @@ + 'array', + ]; + + public $timestamps = false; + + public function post(): BelongsTo + { + return $this->belongsTo(Post::class); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function isVisible(): bool + { + return $this->status === self::STATUS_PUBLISHED; + } +} diff --git a/app/Models/Link.php b/app/Models/Link.php new file mode 100644 index 0000000..adccf1c --- /dev/null +++ b/app/Models/Link.php @@ -0,0 +1,23 @@ + 'boolean', + ]; +} diff --git a/app/Models/PluginRecord.php b/app/Models/PluginRecord.php new file mode 100644 index 0000000..49423ef --- /dev/null +++ b/app/Models/PluginRecord.php @@ -0,0 +1,24 @@ + 'boolean', + ]; + + public function getKeyAttribute(): string + { + return $this->vendor.'/'.$this->name; + } +} diff --git a/app/Models/Post.php b/app/Models/Post.php new file mode 100644 index 0000000..c0564fa --- /dev/null +++ b/app/Models/Post.php @@ -0,0 +1,111 @@ + 'array', + 'is_sticky' => 'boolean', + 'close_comment' => 'boolean', + 'published_at' => 'datetime', + ]; + + public function category(): BelongsTo + { + return $this->belongsTo(Category::class); + } + + public function author(): BelongsTo + { + return $this->belongsTo(User::class, 'user_id'); + } + + public function comments(): HasMany + { + return $this->hasMany(Comment::class); + } + + public function scopePublished(Builder $query): Builder + { + return $query->where('status', 'published') + ->whereNotNull('published_at') + ->where('published_at', '<=', now()); + } + + public function scopeSearch(Builder $query, ?string $keyword): Builder + { + if (! $keyword) { + return $query; + } + + return $query->where(function (Builder $q) use ($keyword) { + $q->where('title', 'like', "%{$keyword}%") + ->orWhere('content', 'like', "%{$keyword}%") + ->orWhere('excerpt', 'like', "%{$keyword}%") + ->orWhere('keywords', 'like', "%{$keyword}%"); + }); + } + + public function registerMediaCollections(): void + { + $this->addMediaCollection('attachments')->useDisk(\App\Support\MediaDisk::name()); + } + + public function registerMediaConversions(?Media $media = null): void + { + $this->addMediaConversion('thumb') + ->width(500) + ->height(500) + ->performOnCollections('attachments'); + } + + public function getUrlAttribute(): string + { + return route('posts.show', $this->slug ?? $this->id); + } + + public function getExcerptOrFallbackAttribute(): string + { + if ($this->excerpt) { + return $this->excerpt; + } + + $text = strip_tags($this->content); + $text = preg_replace('/\[[^\]]*\]/', '', $text); + + return mb_substr($text, 0, 200); + } +} diff --git a/app/Models/Setting.php b/app/Models/Setting.php new file mode 100644 index 0000000..2108426 --- /dev/null +++ b/app/Models/Setting.php @@ -0,0 +1,60 @@ + $key], ['value' => is_scalar($value) || $value === null ? $value : json_encode($value)]); + self::flushCache(); + } + + public static function forget(string $key): void + { + self::where('key', $key)->delete(); + self::flushCache(); + } + + public static function allSettings(): array + { + return Cache::remember(self::CACHE_KEY, now()->addDay(), function () { + return self::query()->pluck('value', 'key')->all(); + }); + } + + public static function flushCache(): void + { + Cache::forget(self::CACHE_KEY); + } + + public static function seedDefaults(array $defaults): void + { + foreach ($defaults as $key => $value) { + if (! self::query()->where('key', $key)->exists()) { + self::query()->create(['key' => $key, 'value' => $value]); + } + } + self::flushCache(); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 68f3a66..963972e 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,16 +2,18 @@ namespace App\Models; -// use Illuminate\Contracts\Auth\MustVerifyEmail; use Database\Factories\UserFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Illuminate\Support\Facades\Hash; +use Spatie\Permission\Traits\HasRoles; class User extends Authenticatable { /** @use HasFactory */ - use HasFactory, Notifiable; + use HasFactory, Notifiable, HasRoles; /** * The attributes that are mass assignable. @@ -22,6 +24,13 @@ class User extends Authenticatable 'name', 'email', 'password', + 'legacy_md5', + 'url', + 'logincount', + 'loginip', + 'regip', + 'logintime', + 'lastpost_at', ]; /** @@ -32,6 +41,7 @@ class User extends Authenticatable protected $hidden = [ 'password', 'remember_token', + 'legacy_md5', ]; /** @@ -44,6 +54,44 @@ class User extends Authenticatable return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', + 'logintime' => 'datetime', + 'lastpost_at' => 'datetime', ]; } + + public function posts(): HasMany + { + return $this->hasMany(Post::class); + } + + public function comments(): HasMany + { + return $this->hasMany(Comment::class); + } + + /** + * 验证登录:优先新密码哈希,兼容 sablog 老 MD5 密码。 + */ + public function verifyPassword(string $plain): bool + { + if ($this->password && Hash::check($plain, $this->password)) { + return true; + } + + if ($this->legacy_md5 && md5($plain) === strtolower($this->legacy_md5)) { + // 登录成功后自动升级为现代哈希 + $this->password = Hash::make($plain); + $this->legacy_md5 = null; + $this->save(); + + return true; + } + + return false; + } + + public function isAdmin(): bool + { + return $this->hasRole('admin'); + } } diff --git a/app/Support/MediaDisk.php b/app/Support/MediaDisk.php new file mode 100644 index 0000000..cd2ef49 --- /dev/null +++ b/app/Support/MediaDisk.php @@ -0,0 +1,21 @@ +where($column, $slug)->where('id', '!=', $ignoreId ?? 0)->exists()) { + $slug = $base.'-'.$i++; + } + + return $slug; + } +} diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index 05fb5d9..eafb447 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -16,7 +16,7 @@ return new class extends Migration $table->string('name'); $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); - $table->string('password'); + $table->string('password')->nullable(); $table->rememberToken(); $table->timestamps(); }); diff --git a/database/migrations/2026_08_11_100001_create_categories_and_posts_table.php b/database/migrations/2026_08_11_100001_create_categories_and_posts_table.php new file mode 100644 index 0000000..ed4ddd8 --- /dev/null +++ b/database/migrations/2026_08_11_100001_create_categories_and_posts_table.php @@ -0,0 +1,48 @@ +id(); + $table->string('name', 50); + $table->string('slug', 120)->nullable()->unique(); + $table->tinyInteger('display_order')->default(0); + $table->unsignedInteger('post_count')->default(0); + $table->timestamps(); + }); + + Schema::create('posts', function (Blueprint $table) { + $table->id(); + $table->foreignId('category_id')->nullable()->constrained()->nullOnDelete(); + $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); + $table->string('title'); + $table->string('slug', 255)->nullable()->unique(); + $table->text('excerpt')->nullable(); + $table->longText('content'); + $table->string('keywords', 255)->nullable(); + $table->string('status', 20)->default('published')->index(); + $table->boolean('is_sticky')->default(false); + $table->boolean('close_comment')->default(false); + $table->string('read_password', 255)->nullable(); + $table->unsignedBigInteger('views')->default(0); + $table->unsignedInteger('comment_count')->default(0); + $table->json('meta')->nullable(); + $table->timestamp('published_at')->nullable()->index(); + $table->timestamps(); + + $table->index(['status', 'published_at']); + }); + } + + public function down(): void + { + Schema::dropIfExists('posts'); + Schema::dropIfExists('categories'); + } +}; diff --git a/database/migrations/2026_08_11_100002_create_comments_and_trackbacks_table.php b/database/migrations/2026_08_11_100002_create_comments_and_trackbacks_table.php new file mode 100644 index 0000000..2bcd0e2 --- /dev/null +++ b/database/migrations/2026_08_11_100002_create_comments_and_trackbacks_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('post_id')->constrained()->cascadeOnDelete(); + $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); + $table->string('author_name', 50); + $table->string('author_email', 255)->nullable(); + $table->string('author_url', 255)->nullable(); + $table->mediumText('content'); + $table->string('ip', 64)->nullable(); + $table->string('status', 20)->default('pending')->index(); + $table->json('ai_review')->nullable(); + $table->timestamp('created_at')->useCurrent(); + + $table->index(['post_id', 'status']); + }); + } + + public function down(): void + { + Schema::dropIfExists('comments'); + } +}; diff --git a/database/migrations/2026_08_11_100003_create_links_and_settings_table.php b/database/migrations/2026_08_11_100003_create_links_and_settings_table.php new file mode 100644 index 0000000..bafd0ab --- /dev/null +++ b/database/migrations/2026_08_11_100003_create_links_and_settings_table.php @@ -0,0 +1,45 @@ +id(); + $table->string('name', 100); + $table->string('url', 255); + $table->string('note', 255)->nullable(); + $table->tinyInteger('display_order')->default(0); + $table->boolean('visible')->default(true); + $table->timestamps(); + }); + + Schema::create('settings', function (Blueprint $table) { + $table->string('key')->primary(); + $table->text('value'); + $table->timestamps(); + }); + + Schema::create('plugin_records', function (Blueprint $table) { + $table->id(); + $table->string('vendor', 60); + $table->string('name', 60); + $table->string('version', 40)->nullable(); + $table->boolean('enabled')->default(true); + $table->timestamps(); + + $table->unique(['vendor', 'name']); + }); + } + + public function down(): void + { + Schema::dropIfExists('plugin_records'); + Schema::dropIfExists('settings'); + Schema::dropIfExists('links'); + } +}; diff --git a/database/migrations/2026_08_11_100004_add_blog_columns_to_users_table.php b/database/migrations/2026_08_11_100004_add_blog_columns_to_users_table.php new file mode 100644 index 0000000..426b538 --- /dev/null +++ b/database/migrations/2026_08_11_100004_add_blog_columns_to_users_table.php @@ -0,0 +1,30 @@ +string('legacy_md5', 32)->nullable()->index(); + $table->string('url', 255)->nullable(); + $table->unsignedInteger('logincount')->default(0); + $table->string('loginip', 64)->nullable(); + $table->string('regip', 64)->nullable(); + $table->timestamp('logintime')->nullable(); + $table->timestamp('lastpost_at')->nullable(); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn([ + 'legacy_md5', 'url', 'logincount', 'loginip', 'regip', 'logintime', 'lastpost_at', + ]); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 6b901f8..1a0f9bd 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -2,24 +2,44 @@ namespace Database\Seeders; +use App\Models\Setting; use App\Models\User; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\Hash; +use Spatie\Permission\Models\Role; class DatabaseSeeder extends Seeder { - use WithoutModelEvents; - - /** - * Seed the application's database. - */ public function run(): void { - // User::factory(10)->create(); + foreach (['admin', 'editor', 'member'] as $role) { + Role::findOrCreate($role); + } - User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', + $admin = User::query()->firstOrCreate( + ['email' => 'admin@laralog.test'], + [ + 'name' => '管理员', + 'password' => Hash::make('password'), + ] + ); + $admin->assignRole('admin'); + + Setting::seedDefaults([ + 'site_name' => config('blog.name'), + 'site_description' => config('blog.description'), + 'site_icp' => config('blog.icp'), + 'active_theme' => 'sablog', + 'comment_audit' => '0', + 'comment_min_len' => (string) config('blog.comment_min_len'), + 'comment_max_len' => (string) config('blog.comment_max_len'), + 'comment_post_space' => (string) config('blog.comment_post_space'), + 'comment_order' => '1', + 'rss_num' => (string) config('blog.rss_num'), + 'seo_default_keywords' => '', + 'seo_default_description' => '', + 'site_closed' => '0', + 'site_closed_note' => '系统升级中...', ]); } }