diff --git a/app/Http/Controllers/Admin/PlanController.php b/app/Http/Controllers/Admin/PlanController.php
index 6497714..0f59fde 100644
--- a/app/Http/Controllers/Admin/PlanController.php
+++ b/app/Http/Controllers/Admin/PlanController.php
@@ -228,6 +228,49 @@ class PlanController extends Controller
});
}
+ public function seedDefaults(Request $request): RedirectResponse
+ {
+ $this->ensurePlatformAdmin($request);
+
+ // 安全护栏:仅当当前库没有任何套餐时才允许一键初始化
+ $existingCount = Plan::query()->count();
+ if ($existingCount > 0) {
+ return redirect()->back()->with('error', '当前已有套餐(' . $existingCount . ' 条),为避免污染运营数据,已阻止一键初始化。');
+ }
+
+ $now = now();
+ $defaults = [
+ [
+ 'code' => 'starter_monthly',
+ 'name' => '基础版(月付)',
+ 'billing_cycle' => 'monthly',
+ 'price' => 99,
+ 'list_price' => 129,
+ 'status' => 'active',
+ 'sort' => 10,
+ 'description' => '适合刚开站的试运营阶段,可用于 Demo 场景。',
+ 'published_at' => $now,
+ ],
+ [
+ 'code' => 'pro_yearly',
+ 'name' => '专业版(年付)',
+ 'billing_cycle' => 'yearly',
+ 'price' => 1999,
+ 'list_price' => 2599,
+ 'status' => 'active',
+ 'sort' => 20,
+ 'description' => '面向成长型站点,后续搭配授权项配置。',
+ 'published_at' => $now,
+ ],
+ ];
+
+ foreach ($defaults as $row) {
+ Plan::query()->create($row);
+ }
+
+ return redirect('/admin/plans')->with('success', '已初始化默认套餐:' . count($defaults) . ' 条。');
+ }
+
protected function statusLabels(): array
{
return [
diff --git a/resources/views/admin/plans/index.blade.php b/resources/views/admin/plans/index.blade.php
index 7f882cb..365c08b 100644
--- a/resources/views/admin/plans/index.blade.php
+++ b/resources/views/admin/plans/index.blade.php
@@ -11,13 +11,21 @@
diff --git a/routes/web.php b/routes/web.php
index a963b83..3c9e725 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -121,6 +121,8 @@ Route::prefix('admin')->group(function () {
Route::get('/plans/export', [PlanController::class, 'export']);
Route::get('/plans/create', [PlanController::class, 'create']);
Route::post('/plans', [PlanController::class, 'store']);
+ // 注意:必须放在 /plans/{plan} 之前,避免被参数路由吞掉导致 404
+ Route::post('/plans/seed-defaults', [PlanController::class, 'seedDefaults']);
Route::get('/plans/{plan}/edit', [PlanController::class, 'edit']);
Route::post('/plans/{plan}', [PlanController::class, 'update']);
Route::post('/plans/{plan}/set-status', [PlanController::class, 'setStatus']);
diff --git a/tests/Feature/AdminPlanSeedDefaultsTest.php b/tests/Feature/AdminPlanSeedDefaultsTest.php
new file mode 100644
index 0000000..3cc05a6
--- /dev/null
+++ b/tests/Feature/AdminPlanSeedDefaultsTest.php
@@ -0,0 +1,58 @@
+seed();
+
+ $this->post('/admin/login', [
+ 'email' => 'platform.admin@demo.local',
+ 'password' => 'Platform@123456',
+ ])->assertRedirect('/admin');
+ }
+
+ public function test_platform_admin_can_seed_default_plans_when_empty(): void
+ {
+ $this->loginAsPlatformAdmin();
+
+ // 清空 seed 带来的默认套餐(测试口径:模拟空库)
+ Plan::query()->delete();
+ $this->assertSame(0, Plan::query()->count());
+
+ $this->post('/admin/plans/seed-defaults')
+ ->assertRedirect('/admin/plans');
+
+ $this->assertSame(2, Plan::query()->count());
+ $this->assertNotNull(Plan::query()->where('code', 'starter_monthly')->first());
+ $this->assertNotNull(Plan::query()->where('code', 'pro_yearly')->first());
+
+ $this->get('/admin/plans')
+ ->assertOk()
+ ->assertSee('基础版(月付)')
+ ->assertSee('专业版(年付)');
+ }
+
+ public function test_seed_default_plans_is_blocked_when_plans_exist(): void
+ {
+ $this->loginAsPlatformAdmin();
+
+ $this->assertTrue(Plan::query()->count() > 0);
+
+ $this->post('/admin/plans/seed-defaults')
+ ->assertRedirect();
+
+ // 不应新增到 2 条以上;且仍能打开列表
+ $this->get('/admin/plans')
+ ->assertOk()
+ ->assertSee('套餐管理');
+ }
+}