feat: add admin env badge, quick post button, and harden plugin enable
Build and Deploy / verify (push) Failing after 15m17s
Build and Deploy / deploy (push) Skipped

Show local/prod in the Filament topbar, jump to create post in one click, tighten sidebar spacing, and stop Hello World from 500ing when its tables are missing.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
gouki
2026-09-18 08:24:16 +00:00
co-authored by Cursor
parent b39423831c
commit 469160dc3f
19 changed files with 825 additions and 43 deletions
+6
View File
@@ -1,5 +1,11 @@
# Changelog # Changelog
## 2026-09-17
- Admin top-right now shows the current environment (`local` / `prod`) and a **写文章** shortcut to create a post.
- Tightened Filament sidebar vertical spacing (correct `fi-sidebar-item-btn` selectors + smaller group gaps).
- Hello World plugin: enable runs plugin migrations; missing `hello_world_logs` no longer 500s the demo page; plugin boot/hooks are isolated.
## 2026-09-16 ## 2026-09-16
- Switched new attachments/covers to `xpier/filament-media-library`. - Switched new attachments/covers to `xpier/filament-media-library`.
+12
View File
@@ -12,6 +12,18 @@ Modern blog migrated from SaBlog-X: Filament admin, themes, plugins, sablog impo
See `README.md` and `docs/` for install, import, plugins, and themes. See `README.md` and `docs/` for install, import, plugins, and themes.
## Admin chrome
- Top-right badge shows the current app environment (`local` / `prod` / others).
- Top-right **写文章** button jumps to `/admin/posts/create`.
- Sidebar nav is compacted (Filament default `gap-y-7` / `py-8` is too tall for this site).
## Hello World plugin
- Enabling a plugin now runs that plugin's `database/migrations` automatically.
- Plugin boot / hook failures are isolated so one plugin cannot take down the whole admin.
- `/hello-world` stays available even if `hello_world_logs` is missing.
## Media library (xpier) ## Media library (xpier)
- Package: `xpier/filament-media-library` (admin Media Library + MediaPicker). - Package: `xpier/filament-media-library` (admin Media Library + MediaPicker).
+2 -1
View File
@@ -4,7 +4,8 @@
- **脚本迁移**:`php artisan sablog:import` 一键从老库迁入全部数据(文章/分类/评论/标签/友链/用户/附件),保留原始 ID,老 URL 全部 301 兼容 - **脚本迁移**:`php artisan sablog:import` 一键从老库迁入全部数据(文章/分类/评论/标签/友链/用户/附件),保留原始 ID,老 URL 全部 301 兼容
- **多主题**:`themes/` 目录即皮肤,后台一键切换/上传 ZIP;内置 sablog 经典风 + modern 简约风两套 - **多主题**:`themes/` 目录即皮肤,后台一键切换/上传 ZIP;内置 sablog 经典风 + modern 简约风两套
- **插件系统**:`plugins/` 目录 + 钩子系统(action/filter),ZIP 安装 + 可选远程市场 - **插件系统**:`plugins/` 目录 + 钩子系统(action/filter),ZIP 安装 + 可选远程市场;启用时自动跑插件迁移
- **后台顶栏**:右上角显示当前环境(local/prod)+ 一键写文章
- **内置插件**:SEO & GEO(JSON-LD/OG/llms.txt/Markdown 导出)、AI 评论审核 + 内容润色(Workerman 异步)、支付(支付宝/微信/沙箱)、会员(套餐订阅 + 付费文章) - **内置插件**:SEO & GEO(JSON-LD/OG/llms.txt/Markdown 导出)、AI 评论审核 + 内容润色(Workerman 异步)、支付(支付宝/微信/沙箱)、会员(套餐订阅 + 付费文章)
- **附件全走 S3**:R2 / COS / OSS 兼容,新上传零落盘,`attachments:sync-s3` 一键同步老附件(大文件 Multipart) - **附件全走 S3**:R2 / COS / OSS 兼容,新上传零落盘,`attachments:sync-s3` 一键同步老附件(大文件 Multipart)
@@ -45,7 +45,11 @@ class PluginManagerServiceProvider extends ServiceProvider
$provider = $manager->resolveProvider($key); $provider = $manager->resolveProvider($key);
if ($provider && class_exists($provider)) { if ($provider && class_exists($provider)) {
$provider::bootPlugin($manager); try {
$provider::bootPlugin($manager);
} catch (\Throwable $e) {
report($e);
}
} }
} }
} }
@@ -49,6 +49,7 @@ class PostContentRenderer
// 插件钩子:付费内容过滤、AI 处理等 // 插件钩子:付费内容过滤、AI 处理等
$html = app(\App\Blog\Support\PluginManager::class)->applyFilters('post.rendered', $html, $post); $html = app(\App\Blog\Support\PluginManager::class)->applyFilters('post.rendered', $html, $post);
$html = is_string($html) ? $html : '';
// 兜底:任何情况下不把裸 [paid] 标签输出到页面(如会员插件被停用时) // 兜底:任何情况下不把裸 [paid] 标签输出到页面(如会员插件被停用时)
return preg_replace('/\[(\/?paid)\]/i', '', $html); return preg_replace('/\[(\/?paid)\]/i', '', $html);
+36 -2
View File
@@ -6,6 +6,7 @@ declare(strict_types=1);
namespace App\Blog\Support; namespace App\Blog\Support;
use App\Models\PluginRecord; use App\Models\PluginRecord;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use RuntimeException; use RuntimeException;
@@ -155,6 +156,8 @@ class PluginManager
throw new RuntimeException('无法启用 '.$plugin.':'.implode(';', $problems)); throw new RuntimeException('无法启用 '.$plugin.':'.implode(';', $problems));
} }
$this->migratePlugin($plugin);
[$vendor, $name] = array_pad(explode('.', $plugin), 2, $plugin); [$vendor, $name] = array_pad(explode('.', $plugin), 2, $plugin);
PluginRecord::query()->updateOrCreate( PluginRecord::query()->updateOrCreate(
['vendor' => $vendor, 'name' => $name], ['vendor' => $vendor, 'name' => $name],
@@ -162,6 +165,29 @@ class PluginManager
); );
} }
/**
* Run the plugin's own migrations so enabling a demo/third-party plugin
* does not 500 on missing tables.
*/
public function migratePlugin(string $plugin): void
{
$relativePath = 'plugins/'.$plugin.'/database/migrations';
if (! is_dir(base_path($relativePath))) {
return;
}
$exitCode = Artisan::call('migrate', [
'--path' => $relativePath,
'--force' => true,
'--no-interaction' => true,
]);
if ($exitCode !== 0) {
throw new RuntimeException('插件迁移失败:'.$plugin.'。'.trim(Artisan::output()));
}
}
public function disable(string $plugin): void public function disable(string $plugin): void
{ {
$dependents = $this->dependents($plugin); $dependents = $this->dependents($plugin);
@@ -243,7 +269,11 @@ class PluginManager
{ {
foreach ($this->actions[$hook] ?? [] as $callbacks) { foreach ($this->actions[$hook] ?? [] as $callbacks) {
foreach ($callbacks as $callback) { foreach ($callbacks as $callback) {
$callback(...$args); try {
$callback(...$args);
} catch (\Throwable $e) {
report($e);
}
} }
} }
} }
@@ -252,7 +282,11 @@ class PluginManager
{ {
foreach ($this->filters[$hook] ?? [] as $callbacks) { foreach ($this->filters[$hook] ?? [] as $callbacks) {
foreach ($callbacks as $callback) { foreach ($callbacks as $callback) {
$value = $callback($value, ...$args); try {
$value = $callback($value, ...$args);
} catch (\Throwable $e) {
report($e);
}
} }
} }
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Filament\Support;
/**
* Admin top-right environment badge: local stays local, production shows as prod.
*/
class AdminEnvironment
{
public static function name(): string
{
$environment = app()->environment();
return match ($environment) {
'production', 'prod' => 'prod',
default => $environment,
};
}
public static function color(): string
{
return match (app()->environment()) {
'production', 'prod' => 'danger',
'local' => 'gray',
'staging' => 'warning',
default => 'info',
};
}
}
+15 -7
View File
@@ -62,13 +62,21 @@ class AppServiceProvider extends ServiceProvider
PanelsRenderHook::STYLES_AFTER, PanelsRenderHook::STYLES_AFTER,
fn () => Blade::render(<<<'HTML' fn () => Blade::render(<<<'HTML'
<style> <style>
.fi-sidebar { padding-inline: 0.75rem; } .ll-topbar-tools { display: flex; align-items: center; gap: 0.45rem; margin-inline-end: 0.15rem; }
.fi-sidebar-nav { padding-inline: 0; } .ll-env-badge { text-transform: uppercase; letter-spacing: 0.06em; font-weight: 700; }
.fi-sidebar-nav-groups { margin-inline: 0; }
.fi-sidebar-nav .fi-sidebar-item-button { padding-inline: 0.6rem; border-radius: 0.5rem; } .fi-sidebar { padding-inline: 0.65rem; }
.fi-sidebar-nav .fi-sidebar-item-label { font-size: 0.82rem; } .fi-sidebar-nav { padding-inline: 0; padding-block: 0.4rem !important; gap: 0.35rem !important; }
.fi-sidebar-nav .fi-sidebar-group-label { font-size: 0.68rem; letter-spacing: 0.04em; } .fi-sidebar-nav-groups { margin-inline: 0; gap: 0.35rem !important; }
.fi-sidebar-brand { padding-block: 0.9rem; } .fi-sidebar-group { gap: 0.08rem !important; }
.fi-sidebar-group-items { gap: 0.04rem !important; }
.fi-sidebar-nav .fi-sidebar-item-btn,
.fi-sidebar-nav .fi-sidebar-item-button { padding-inline: 0.55rem; padding-block: 0.26rem !important; min-height: 0 !important; border-radius: 0.45rem; }
.fi-sidebar-nav .fi-sidebar-group-btn { padding-block: 0.12rem !important; padding-inline: 0.55rem; }
.fi-sidebar-nav .fi-sidebar-item-label { font-size: 0.82rem; line-height: 1.2; }
.fi-sidebar-nav .fi-sidebar-group-label { font-size: 0.66rem; letter-spacing: 0.04em; line-height: 1.2; }
.fi-sidebar-brand { padding-block: 0.5rem; }
.fi-sidebar-header { height: auto !important; min-height: 2.6rem; }
/* 表格行紧凑:数据行高度由 .fi-ta-text / .fi-ta-icon 的 padding-block 驱动;操作列按钮禁止换行堆叠 */ /* 表格行紧凑:数据行高度由 .fi-ta-text / .fi-ta-icon 的 padding-block 驱动;操作列按钮禁止换行堆叠 */
.fi-ta-text, .fi-ta-icon { padding-block: calc(var(--spacing) * 1.75) !important; } .fi-ta-text, .fi-ta-icon { padding-block: calc(var(--spacing) * 1.75) !important; }
@@ -21,6 +21,7 @@ use Filament\Pages\Dashboard;
use Filament\Panel; use Filament\Panel;
use Filament\PanelProvider; use Filament\PanelProvider;
use Filament\Support\Colors\Color; use Filament\Support\Colors\Color;
use Filament\View\PanelsRenderHook;
use Filament\Widgets\AccountWidget; use Filament\Widgets\AccountWidget;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies; use Illuminate\Cookie\Middleware\EncryptCookies;
@@ -75,6 +76,10 @@ class AdminPanelProvider extends PanelProvider
->icon('heroicon-o-squares-plus') ->icon('heroicon-o-squares-plus')
->url(fn (): string => \App\Filament\Pages\ThemeInject::getUrl()), ->url(fn (): string => \App\Filament\Pages\ThemeInject::getUrl()),
]) ])
->renderHook(
PanelsRenderHook::USER_MENU_BEFORE,
fn (): \Illuminate\Contracts\View\View => view('filament.hooks.topbar-tools')
)
->navigationGroups([ ->navigationGroups([
NavigationGroup::make('内容'), NavigationGroup::make('内容'),
NavigationGroup::make('管理'), NavigationGroup::make('管理'),
@@ -0,0 +1,582 @@
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
DROP TABLE IF EXISTS `cache`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `cache` (
`key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`value` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
`expiration` int NOT NULL,
PRIMARY KEY (`key`),
KEY `cache_expiration_index` (`expiration`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `cache_locks`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `cache_locks` (
`key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`owner` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`expiration` int NOT NULL,
PRIMARY KEY (`key`),
KEY `cache_locks_expiration_index` (`expiration`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `categories`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `categories` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`slug` varchar(120) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`display_order` tinyint NOT NULL DEFAULT '0',
`description` text COLLATE utf8mb4_unicode_ci,
`cover_media_id` bigint unsigned DEFAULT NULL,
`post_count` int unsigned NOT NULL DEFAULT '0',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `categories_slug_unique` (`slug`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `comments`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `comments` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`post_id` bigint unsigned NOT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`author_name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`author_email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`author_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`content` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
`ip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
`ai_review` json DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `comments_user_id_foreign` (`user_id`),
KEY `comments_post_id_status_index` (`post_id`,`status`),
KEY `comments_status_index` (`status`),
CONSTRAINT `comments_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE,
CONSTRAINT `comments_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `failed_jobs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `failed_jobs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`uuid` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`connection` text COLLATE utf8mb4_unicode_ci NOT NULL,
`queue` text COLLATE utf8mb4_unicode_ci NOT NULL,
`payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`exception` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`failed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `failed_jobs_uuid_unique` (`uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `hello_world_logs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `hello_world_logs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`comment_id` bigint unsigned DEFAULT NULL,
`note` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `hello_world_logs_comment_id_foreign` (`comment_id`),
CONSTRAINT `hello_world_logs_comment_id_foreign` FOREIGN KEY (`comment_id`) REFERENCES `comments` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `job_batches`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `job_batches` (
`id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`total_jobs` int NOT NULL,
`pending_jobs` int NOT NULL,
`failed_jobs` int NOT NULL,
`failed_job_ids` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`options` mediumtext COLLATE utf8mb4_unicode_ci,
`cancelled_at` int DEFAULT NULL,
`created_at` int NOT NULL,
`finished_at` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `jobs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `jobs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`queue` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`attempts` tinyint unsigned NOT NULL,
`reserved_at` int unsigned DEFAULT NULL,
`available_at` int unsigned NOT NULL,
`created_at` int unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `jobs_queue_index` (`queue`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `links`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `links` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`url` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`note` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`display_order` tinyint NOT NULL DEFAULT '0',
`visible` tinyint(1) NOT NULL DEFAULT '1',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `market_packages`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `market_packages` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`item_key` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
`item_type` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
`title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`price` bigint unsigned NOT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
`payment_id` bigint unsigned DEFAULT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `market_packages_item_key_item_type_unique` (`item_key`,`item_type`),
KEY `market_packages_payment_id_foreign` (`payment_id`),
KEY `market_packages_user_id_foreign` (`user_id`),
CONSTRAINT `market_packages_payment_id_foreign` FOREIGN KEY (`payment_id`) REFERENCES `payments` (`id`) ON DELETE SET NULL,
CONSTRAINT `market_packages_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `media`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `media` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`model_id` bigint unsigned NOT NULL,
`uuid` char(36) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`collection_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`file_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`mime_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`disk` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`conversions_disk` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`size` bigint unsigned NOT NULL,
`manipulations` json NOT NULL,
`custom_properties` json NOT NULL,
`generated_conversions` json NOT NULL,
`responsive_images` json NOT NULL,
`order_column` int unsigned DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `media_uuid_unique` (`uuid`),
KEY `media_model_type_model_id_index` (`model_type`,`model_id`),
KEY `media_order_column_index` (`order_column`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `media_folders`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `media_folders` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`parent_id` bigint unsigned DEFAULT NULL,
`code` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`sort` int NOT NULL DEFAULT '0',
`is_active` tinyint(1) NOT NULL DEFAULT '1',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `media_folders_code_unique` (`code`),
KEY `media_folders_parent_id_foreign` (`parent_id`),
CONSTRAINT `media_folders_parent_id_foreign` FOREIGN KEY (`parent_id`) REFERENCES `media_folders` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `media_library`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `media_library` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint unsigned DEFAULT NULL,
`disk` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'public',
`path` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`original_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`extension` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`mime_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`size` bigint unsigned NOT NULL DEFAULT '0',
`type` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'image',
`source` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'admin',
`folder` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`alt_text` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`custom_properties` json DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`deleted_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `media_library_path_unique` (`path`),
KEY `media_library_user_id_foreign` (`user_id`),
KEY `media_library_type_index` (`type`),
KEY `media_library_folder_index` (`folder`),
KEY `media_library_source_index` (`source`),
CONSTRAINT `media_library_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `membership_plans`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `membership_plans` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`slug` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`description` text COLLATE utf8mb4_unicode_ci,
`price` bigint unsigned NOT NULL,
`duration_days` int unsigned NOT NULL DEFAULT '30',
`permissions` json DEFAULT NULL,
`active` tinyint(1) NOT NULL DEFAULT '1',
`sort` int unsigned NOT NULL DEFAULT '0',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `membership_plans_slug_unique` (`slug`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `migrations`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `migrations` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`migration` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`batch` int NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `model_has_permissions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `model_has_permissions` (
`permission_id` bigint unsigned NOT NULL,
`model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`model_id` bigint unsigned NOT NULL,
PRIMARY KEY (`permission_id`,`model_id`,`model_type`),
KEY `model_has_permissions_model_id_model_type_index` (`model_id`,`model_type`),
CONSTRAINT `model_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `model_has_roles`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `model_has_roles` (
`role_id` bigint unsigned NOT NULL,
`model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`model_id` bigint unsigned NOT NULL,
PRIMARY KEY (`role_id`,`model_id`,`model_type`),
KEY `model_has_roles_model_id_model_type_index` (`model_id`,`model_type`),
CONSTRAINT `model_has_roles_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `password_reset_tokens`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `password_reset_tokens` (
`email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`token` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `payments`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `payments` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`order_no` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`subject` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`description` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`amount` bigint unsigned NOT NULL,
`channel` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'alipay',
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
`gateway_trade_no` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`payload` json DEFAULT NULL,
`paid_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`payable_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`payable_id` bigint unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `payments_order_no_unique` (`order_no`),
KEY `payments_user_id_status_index` (`user_id`,`status`),
KEY `payments_payable_type_payable_id_index` (`payable_type`,`payable_id`),
CONSTRAINT `payments_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `permissions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `permissions` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `permissions_name_guard_name_unique` (`name`,`guard_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `personal_access_tokens`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `personal_access_tokens` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`tokenable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`tokenable_id` bigint unsigned NOT NULL,
`name` text COLLATE utf8mb4_unicode_ci NOT NULL,
`token` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
`abilities` text COLLATE utf8mb4_unicode_ci,
`last_used_at` timestamp NULL DEFAULT NULL,
`expires_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `personal_access_tokens_token_unique` (`token`),
KEY `personal_access_tokens_tokenable_type_tokenable_id_index` (`tokenable_type`,`tokenable_id`),
KEY `personal_access_tokens_expires_at_index` (`expires_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `plugin_records`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `plugin_records` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`vendor` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`version` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`enabled` tinyint(1) NOT NULL DEFAULT '1',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `plugin_records_vendor_name_unique` (`vendor`,`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `posts`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `posts` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`category_id` bigint unsigned DEFAULT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`slug` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`excerpt` text COLLATE utf8mb4_unicode_ci,
`content` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`content_format` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'markdown',
`keywords` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`cover_media_id` bigint unsigned DEFAULT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'published',
`is_sticky` tinyint(1) NOT NULL DEFAULT '0',
`close_comment` tinyint(1) NOT NULL DEFAULT '0',
`read_password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`views` bigint unsigned NOT NULL DEFAULT '0',
`comment_count` int unsigned NOT NULL DEFAULT '0',
`meta` json DEFAULT NULL,
`published_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `posts_slug_unique` (`slug`),
KEY `posts_category_id_foreign` (`category_id`),
KEY `posts_user_id_foreign` (`user_id`),
KEY `posts_status_published_at_index` (`status`,`published_at`),
KEY `posts_status_index` (`status`),
KEY `posts_published_at_index` (`published_at`),
CONSTRAINT `posts_category_id_foreign` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL,
CONSTRAINT `posts_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `redirects`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `redirects` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`hash` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL,
`target_url` text COLLATE utf8mb4_unicode_ci NOT NULL,
`clicks` bigint unsigned NOT NULL DEFAULT '0',
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',
`last_clicked_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `redirects_hash_unique` (`hash`),
KEY `redirects_target_url_index` (`target_url`(191))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `role_has_permissions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `role_has_permissions` (
`permission_id` bigint unsigned NOT NULL,
`role_id` bigint unsigned NOT NULL,
PRIMARY KEY (`permission_id`,`role_id`),
KEY `role_has_permissions_role_id_foreign` (`role_id`),
CONSTRAINT `role_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE,
CONSTRAINT `role_has_permissions_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `roles`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `roles` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `roles_name_guard_name_unique` (`name`,`guard_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `sessions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `sessions` (
`id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`ip_address` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`user_agent` text COLLATE utf8mb4_unicode_ci,
`payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`last_activity` int NOT NULL,
PRIMARY KEY (`id`),
KEY `sessions_user_id_index` (`user_id`),
KEY `sessions_last_activity_index` (`last_activity`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `settings`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `settings` (
`key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`value` text COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `subscriptions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `subscriptions` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint unsigned NOT NULL,
`membership_plan_id` bigint unsigned DEFAULT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',
`starts_at` timestamp NULL DEFAULT NULL,
`ends_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `subscriptions_membership_plan_id_foreign` (`membership_plan_id`),
KEY `subscriptions_user_id_status_index` (`user_id`,`status`),
CONSTRAINT `subscriptions_membership_plan_id_foreign` FOREIGN KEY (`membership_plan_id`) REFERENCES `membership_plans` (`id`) ON DELETE SET NULL,
CONSTRAINT `subscriptions_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `taggables`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `taggables` (
`tag_id` bigint unsigned NOT NULL,
`taggable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`taggable_id` bigint unsigned NOT NULL,
UNIQUE KEY `taggables_tag_id_taggable_id_taggable_type_unique` (`tag_id`,`taggable_id`,`taggable_type`),
KEY `taggables_taggable_type_taggable_id_index` (`taggable_type`,`taggable_id`),
CONSTRAINT `taggables_tag_id_foreign` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `tags`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `tags` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` json NOT NULL,
`slug` json NOT NULL,
`type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`order_column` int DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `users` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`email_verified_at` timestamp NULL DEFAULT NULL,
`password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`legacy_md5` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`logincount` int unsigned NOT NULL DEFAULT '0',
`loginip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`regip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`logintime` timestamp NULL DEFAULT NULL,
`lastpost_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `users_email_unique` (`email`),
KEY `users_legacy_md5_index` (`legacy_md5`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (1,'0001_01_01_000000_create_users_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (2,'0001_01_01_000001_create_cache_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (3,'0001_01_01_000002_create_jobs_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (4,'2026_08_11_092446_create_permission_tables',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (5,'2026_08_11_092448_create_media_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (6,'2026_08_11_092449_create_tag_tables',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (7,'2026_08_11_100000_create_membership_tables',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (8,'2026_08_11_100000_create_payments_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (9,'2026_08_11_100001_create_categories_and_posts_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (10,'2026_08_11_100002_create_comments_and_trackbacks_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (11,'2026_08_11_100003_create_links_and_settings_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (12,'2026_08_11_100004_add_blog_columns_to_users_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (13,'2026_08_11_100005_add_content_format_to_posts_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (14,'2026_08_11_113537_create_personal_access_tokens_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (15,'2026_08_12_000000_create_hello_world_logs_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (16,'2026_08_12_100000_add_payable_to_payments_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (17,'2026_08_12_200000_create_market_packages_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (18,'2026_08_12_300000_add_description_to_categories_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (19,'2026_08_12_400000_create_redirects_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (20,'2026_08_15_000001_create_media_library_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (21,'2026_08_15_000002_create_media_folders_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (22,'2026_08_15_000003_add_soft_deletes_to_media_library_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (23,'2026_08_15_000004_add_custom_properties_to_media_library_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (24,'2026_09_16_000001_add_cover_media_id_to_posts_and_categories',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (25,'2026_09_16_000002_seed_default_media_folders',2);
+7
View File
@@ -0,0 +1,7 @@
# Journal
## 2026-09-17 Hello World plugin 500
Enabling `demo.hello-world` could 500 if `hello_world_logs` was never migrated, or if a plugin hook/boot exception escaped.
Fix: run the plugin's migrations on enable; keep `/hello-world` and `comment.created` working without the table; isolate plugin boot and hooks so one plugin cannot take down the admin.
+23 -18
View File
@@ -27,15 +27,17 @@ plugins/{vendor}.{name}/
"provider": "Plugins\\Vendor\\Name\\ServiceProvider", "provider": "Plugins\\Vendor\\Name\\ServiceProvider",
"requires": ["neatstudio.payment"], "requires": ["neatstudio.payment"],
"filament_pages": ["Plugins\\Vendor\\Name\\Filament\\Pages\\SettingsPage"], "filament_pages": ["Plugins\\Vendor\\Name\\Filament\\Pages\\SettingsPage"],
"filament_resources": ["Plugins\\Vendor\\Name\\Filament\\Resources\\OrderResource"] "filament_resources": [
"Plugins\\Vendor\\Name\\Filament\\Resources\\OrderResource"
]
} }
``` ```
| 字段 | 说明 | | 字段 | 说明 |
|------|------| | --------------------------------------- | ------------------------------------------------------------------------ |
| `provider` | 入口类 FQCN;缺省时自动推断 `src/ServiceProvider.php` | | `provider` | 入口类 FQCN;缺省时自动推断 `src/ServiceProvider.php` |
| `requires` | 依赖的其他插件(如会员依赖支付),按 `vendor.name` 引用 | | `requires` | 依赖的其他插件(如会员依赖支付),按 `vendor.name` 引用 |
| `usage` | 使用说明(换行文本),后台插件卡片可折叠查看 | | `usage` | 使用说明(换行文本),后台插件卡片可折叠查看 |
| `filament_pages` / `filament_resources` | 注册到后台的页面/资源(由 `PluginPages` 汇总,统一归入「插件」导航分组) | | `filament_pages` / `filament_resources` | 注册到后台的页面/资源(由 `PluginPages` 汇总,统一归入「插件」导航分组) |
## 依赖管理(requires 强制校验) ## 依赖管理(requires 强制校验)
@@ -82,26 +84,27 @@ class ServiceProvider extends PluginServiceProvider
### addAction(hook, callback, priority) — 无返回值 ### addAction(hook, callback, priority) — 无返回值
| Hook | 参数 | 用途 | | Hook | 参数 | 用途 |
|------|------|------| | --------------------- | --------- | ------------------------------------------------------ |
| `comment.created` | `Comment` | 新评论创建(AI 审核在此监听) | | `comment.created` | `Comment` | 新评论创建(AI 审核在此监听) |
| `payment.paid` | `Payment` | 支付成功(按 `$payment->payable` 分发:订阅激活/解锁) | | `payment.paid` | `Payment` | 支付成功(按 `$payment->payable` 分发:订阅激活/解锁) |
| `filament.post_form` | `Schema` | 文章编辑表单追加字段(会员付费设置在此注入) | | `filament.post_form` | `Schema` | 文章编辑表单追加字段(会员付费设置在此注入) |
| `filament.post_table` | `Table` | 文章列表追加列/操作/过滤 | | `filament.post_table` | `Table` | 文章列表追加列/操作/过滤 |
### addFilter(hook, callback, priority) — 返回值传给下一个过滤器 ### addFilter(hook, callback, priority) — 返回值传给下一个过滤器
| Hook | 签名 | 用途 | | Hook | 签名 | 用途 |
|------|------|------| | --------------------- | ------------------------------------ | --------------------------------------- |
| `post.rendered` | `(string $html, Post $post): string` | 文章渲染后处理(付费内容过滤、paywall) | | `post.rendered` | `(string $html, Post $post): string` | 文章渲染后处理(付费内容过滤、paywall) |
| `seo.structured_data` | `(array $data): array` | 扩展 JSON-LD 结构化数据 | | `seo.structured_data` | `(array $data): array` | 扩展 JSON-LD 结构化数据 |
| `payment.gateway` | `(array $gateways): array` | 注册支付渠道 | | `payment.gateway` | `(array $gateways): array` | 注册支付渠道 |
## 后台注入(Filament 表单 / 表格) ## 后台注入(Filament 表单 / 表格)
核心资源的表单/表格会触发 `filament.post_form` / `filament.post_table` 两个钩子,插件可追加自己的字段与列,停用插件后字段自动消失,核心零侵入。 核心资源的表单/表格会触发 `filament.post_form` / `filament.post_table` 两个钩子,插件可追加自己的字段与列,停用插件后字段自动消失,核心零侵入。
> 注意:`Schema::components()` 与 `Table::columns()` 都是**整体替换**语义,追加要这样写: > 注意:`Schema::components()` 与 `Table::columns()` 都是**整体替换**语义,追加要这样写:
>
> - 表单:`$schema->components([...$schema->getComponents(), Section::make(...)])` > - 表单:`$schema->components([...$schema->getComponents(), Section::make(...)])`
> - 表格:`$table->pushColumns([...])` > - 表格:`$table->pushColumns([...])`
@@ -152,7 +155,9 @@ public function payableUrl(): ?string { return route('filament.admin.resources.
## 迁移 ## 迁移
插件迁移放在 `database/migrations/`,`app/Providers/AppServiceProvider` 启动时自动注册,`php artisan migrate` 会一并执行(无需手动 --path)。 插件迁移放在 `database/migrations/`,`app/Providers/AppServiceProvider` 启动时自动注册,`php artisan migrate` 会一并执行(无需手动 --path)。后台**启用插件**时也会单独跑该插件目录下的迁移,避免启用后立刻访问插件页面因缺表 500。
插件 `boot` 与钩子(action/filter)失败会被隔离并写入日志,单个插件异常不应拖垮整站。
## 打包与安装 ## 打包与安装
+13 -11
View File
@@ -28,8 +28,10 @@ plugins/{vendor}.{name}/ # 目录名必须 vendor.name 格式(如 dem
"author": "你的名字", "author": "你的名字",
"type": "demo", "type": "demo",
"provider": "Plugins\\Demo\\HelloWorld\\ServiceProvider", "provider": "Plugins\\Demo\\HelloWorld\\ServiceProvider",
"requires": ["neatstudio.payment"], // 可选:依赖的插件 "requires": ["neatstudio.payment"], // 可选:依赖的插件
"filament_pages": ["Plugins\\Demo\\HelloWorld\\Filament\\Pages\\HelloWorldSettings"], "filament_pages": [
"Plugins\\Demo\\HelloWorld\\Filament\\Pages\\HelloWorldSettings"
],
"filament_resources": ["...\\Resources\\XxxResource"] "filament_resources": ["...\\Resources\\XxxResource"]
} }
``` ```
@@ -74,14 +76,14 @@ class ServiceProvider extends PluginServiceProvider
### 常用钩子 ### 常用钩子
| Hook | 类型 | 参数 | 用途 | | Hook | 类型 | 参数 | 用途 |
|------|------|------|------| | --------------------- | ------ | ------------------------------- | --------------------------------------- |
| `post.rendered` | filter | `(string $html, Post) : string` | 文章渲染后处理(短代码/付费过滤) | | `post.rendered` | filter | `(string $html, Post) : string` | 文章渲染后处理(短代码/付费过滤) |
| `comment.created` | action | `Comment` | 新评论创建 | | `comment.created` | action | `Comment` | 新评论创建 |
| `payment.paid` | action | `Payment` | 支付成功(按 `$payment->payable` 分发) | | `payment.paid` | action | `Payment` | 支付成功(按 `$payment->payable` 分发) |
| `filament.post_form` | action | `Schema` | 文章编辑表单追加字段 | | `filament.post_form` | action | `Schema` | 文章编辑表单追加字段 |
| `filament.post_table` | action | `Table` | 文章列表追加列 | | `filament.post_table` | action | `Table` | 文章列表追加列 |
| `seo.structured_data` | filter | `(array $data) : array` | 扩展 JSON-LD | | `seo.structured_data` | filter | `(array $data) : array` | 扩展 JSON-LD |
完整列表见 `docs/plugins.md`。 完整列表见 `docs/plugins.md`。
@@ -119,7 +121,7 @@ return view('demo-hello::hello', compact('logs'));
## 6. 迁移 + 模型 ## 6. 迁移 + 模型
迁移放在 `database/migrations/`,**无需手动 --path**,`php artisan migrate` 自动执行(AppServiceProvider 已注册 `plugins/*/database/migrations`)。 迁移放在 `database/migrations/`,**无需手动 --path**,`php artisan migrate` 自动执行(AppServiceProvider 已注册 `plugins/*/database/migrations`)。后台启用插件时也会再跑一次该插件自己的迁移。
模型照常写,命名空间 `Plugins\Demo\HelloWorld\Models`。 模型照常写,命名空间 `Plugins\Demo\HelloWorld\Models`。
@@ -24,6 +24,8 @@ class HelloWorldSettings extends Page
protected static ?string $title = 'Hello World 设置'; protected static ?string $title = 'Hello World 设置';
protected static ?string $slug = 'hello-world-settings';
protected string $view = 'filament.pages.plugin-settings'; protected string $view = 'filament.pages.plugin-settings';
public array $data = []; public array $data = [];
@@ -15,7 +15,9 @@ class HelloWorldController
*/ */
public function index() public function index()
{ {
$logs = HelloWorldLog::query()->with('comment')->latest()->limit(10)->get(); $logs = \Illuminate\Support\Facades\Schema::hasTable('hello_world_logs')
? HelloWorldLog::query()->with('comment')->latest()->limit(10)->get()
: collect();
$view = view('demo-hello::hello', ['logs' => $logs]); $view = view('demo-hello::hello', ['logs' => $logs]);
app(\App\Blog\View\Composers\SidebarComposer::class)->compose($view); app(\App\Blog\View\Composers\SidebarComposer::class)->compose($view);
@@ -29,15 +29,21 @@ class ServiceProvider extends PluginServiceProvider
$manager->addFilter('post.rendered', function (string $html, Post $post) { $manager->addFilter('post.rendered', function (string $html, Post $post) {
$greeting = (string) Setting::get('hello_world_greeting', '你好'); $greeting = (string) Setting::get('hello_world_greeting', '你好');
return preg_replace_callback( $replaced = preg_replace_callback(
'/\[hello\s+([^\]]+)\]/', '/\[hello\s+([^\]]+)\]/u',
fn (array $m) => '<span class="hello-world">'.$greeting.','.e($m[1]).'!</span>', fn (array $m) => '<span class="hello-world">'.$greeting.','.e($m[1]).'!</span>',
$html $html
); );
return is_string($replaced) ? $replaced : $html;
}, 20); }, 20);
// 4. 动作示例:新评论创建时写日志(演示数据关联 + 钩子监听) // 4. 动作示例:新评论创建时写日志(演示数据关联 + 钩子监听)
$manager->addAction('comment.created', function (Comment $comment) { $manager->addAction('comment.created', function (Comment $comment) {
if (! \Illuminate\Support\Facades\Schema::hasTable('hello_world_logs')) {
return;
}
HelloWorldLog::create([ HelloWorldLog::create([
'comment_id' => $comment->id, 'comment_id' => $comment->id,
'note' => '捕获新评论:'.mb_substr($comment->content, 0, 30), 'note' => '捕获新评论:'.mb_substr($comment->content, 0, 30),
@@ -0,0 +1,22 @@
@php
$environmentName = \App\Filament\Support\AdminEnvironment::name();
$environmentColor = \App\Filament\Support\AdminEnvironment::color();
$createPostUrl = \App\Filament\Resources\Posts\PostResource::getUrl('create');
@endphp
<div class="ll-topbar-tools">
<x-filament::badge :color="$environmentColor" size="sm" class="ll-env-badge">
{{ $environmentName }}
</x-filament::badge>
<x-filament::button
tag="a"
:href="$createPostUrl"
size="sm"
color="primary"
icon="heroicon-o-pencil-square"
wire:navigate
>
写文章
</x-filament::button>
</div>
+32
View File
@@ -71,4 +71,36 @@ class AdminPagesTest extends TestCase
{ {
$this->get('/admin')->assertRedirect('/admin/login'); $this->get('/admin')->assertRedirect('/admin/login');
} }
public function test_admin_environment_maps_production_to_prod(): void
{
$original = app()->environment();
app()['env'] = 'production';
$this->assertSame('prod', \App\Filament\Support\AdminEnvironment::name());
$this->assertSame('danger', \App\Filament\Support\AdminEnvironment::color());
app()['env'] = $original;
}
public function test_admin_topbar_shows_environment_and_create_post_button(): void
{
$this->actingAs($this->admin)
->get('/admin')
->assertOk()
->assertSee(\App\Filament\Support\AdminEnvironment::name(), false)
->assertSee('写文章', false)
->assertSee('/admin/posts/create', false);
}
public function test_hello_world_plugin_pages_load_when_enabled(): void
{
$manager = app(\App\Blog\Support\PluginManager::class);
$manager->enable('demo.hello-world');
\Plugins\Demo\HelloWorld\ServiceProvider::bootPlugin($manager);
$this->actingAs($this->admin)->get('/admin')->assertOk();
$this->actingAs($this->admin)->get('/admin/posts/create')->assertOk();
$this->get('/hello-world')->assertOk();
}
} }
+20
View File
@@ -67,4 +67,24 @@ class DemoPluginTest extends TestCase
->assertOk() ->assertOk()
->assertSee('Hello World 演示页'); ->assertSee('Hello World 演示页');
} }
public function test_hello_world_page_survives_missing_log_table(): void
{
\Illuminate\Support\Facades\Schema::dropIfExists('hello_world_logs');
$this->get('/hello-world')->assertOk()->assertSee('Hello World 演示页');
}
public function test_enable_hello_world_runs_plugin_migrations(): void
{
\Illuminate\Support\Facades\Schema::dropIfExists('hello_world_logs');
\Illuminate\Support\Facades\DB::table('migrations')
->where('migration', 'like', '%hello_world_logs%')
->delete();
app(PluginManager::class)->enable('demo.hello-world');
$this->assertTrue(\Illuminate\Support\Facades\Schema::hasTable('hello_world_logs'));
$this->assertTrue(app(PluginManager::class)->isEnabled('demo.hello-world'));
}
} }