ensurePlatformAdmin($request); $filters = [ 'status' => trim((string) $request->query('status', '')), 'billing_cycle' => trim((string) $request->query('billing_cycle', '')), 'keyword' => trim((string) $request->query('keyword', '')), 'published' => trim((string) $request->query('published', '')), ]; $query = $this->applyFilters(Plan::query(), $filters) ->orderBy('sort') ->orderByDesc('id'); $filename = 'plans_' . now()->format('Ymd_His') . '.csv'; return response()->streamDownload(function () use ($query) { $out = fopen('php://output', 'w'); // UTF-8 BOM,避免 Excel 打开中文乱码 fwrite($out, "\xEF\xBB\xBF"); fputcsv($out, [ 'ID', '套餐名称', '编码', '计费周期', '售价', '划线价', '状态', '排序', '发布时间', '描述', ]); $query->chunkById(500, function ($plans) use ($out) { foreach ($plans as $plan) { fputcsv($out, [ $plan->id, $plan->name, $plan->code, $plan->billing_cycle, (float) $plan->price, (float) $plan->list_price, $plan->status, (int) $plan->sort, optional($plan->published_at)->format('Y-m-d H:i:s') ?: '', $plan->description ?: '', ]); } }); fclose($out); }, $filename, [ 'Content-Type' => 'text/csv; charset=UTF-8', ]); } public function index(Request $request): View { $this->ensurePlatformAdmin($request); $filters = [ 'status' => trim((string) $request->query('status', '')), 'billing_cycle' => trim((string) $request->query('billing_cycle', '')), 'keyword' => trim((string) $request->query('keyword', '')), // 发布状态筛选(按 published_at 是否为空) // - published:已发布(published_at not null) // - unpublished:未发布(published_at is null) 'published' => trim((string) $request->query('published', '')), ]; $plansQuery = $this->applyFilters(Plan::query()->withCount(['subscriptions', 'platformOrders']), $filters); $plans = (clone $plansQuery) ->orderBy('sort') ->orderByDesc('id') ->paginate(10) ->withQueryString(); return view('admin.plans.index', [ 'plans' => $plans, 'filters' => $filters, 'filterOptions' => [ 'statuses' => $this->statusLabels(), 'billingCycles' => $this->billingCycleLabels(), ], 'summaryStats' => [ 'total_plans' => (clone $plansQuery)->count(), 'active_plans' => (clone $plansQuery)->where('status', 'active')->count(), 'monthly_plans' => (clone $plansQuery)->where('billing_cycle', 'monthly')->count(), 'yearly_plans' => (clone $plansQuery)->where('billing_cycle', 'yearly')->count(), 'published_plans' => (clone $plansQuery)->whereNotNull('published_at')->count(), 'unpublished_plans' => (clone $plansQuery)->whereNull('published_at')->count(), // 治理联动:当前筛选范围内关联订阅/订单总量 'subscriptions_count' => (clone $plansQuery)->sum('subscriptions_count'), 'platform_orders_count' => (clone $plansQuery)->sum('platform_orders_count'), ], 'statusLabels' => $this->statusLabels(), 'billingCycleLabels' => $this->billingCycleLabels(), ]); } public function create(Request $request): View { $this->ensurePlatformAdmin($request); return view('admin.plans.form', [ 'plan' => new Plan(), 'statusLabels' => $this->statusLabels(), 'billingCycleLabels' => $this->billingCycleLabels(), 'formAction' => '/admin/plans', 'method' => 'post', ]); } public function store(Request $request): RedirectResponse { $this->ensurePlatformAdmin($request); $data = $this->validatePlan($request); $plan = Plan::query()->create($data); return redirect('/admin/plans')->with('success', '套餐已创建:' . $plan->name); } public function edit(Request $request, Plan $plan): View { $this->ensurePlatformAdmin($request); return view('admin.plans.form', [ 'plan' => $plan, 'statusLabels' => $this->statusLabels(), 'billingCycleLabels' => $this->billingCycleLabels(), 'formAction' => '/admin/plans/' . $plan->id, 'method' => 'post', ]); } public function setStatus(Request $request, Plan $plan): RedirectResponse { $this->ensurePlatformAdmin($request); $data = $request->validate([ 'status' => ['required', Rule::in(array_keys($this->statusLabels()))], ]); $plan->status = (string) $data['status']; // 最小治理:当启用且未设置发布时间时,自动补一个发布时间(便于运营口径) if ($plan->status === 'active' && $plan->published_at === null) { $plan->published_at = now(); } $plan->save(); return redirect()->back()->with('success', '套餐状态已更新:' . ($this->statusLabels()[$plan->status] ?? $plan->status)); } public function update(Request $request, Plan $plan): RedirectResponse { $this->ensurePlatformAdmin($request); $data = $this->validatePlan($request, $plan->id); $plan->update($data); return redirect('/admin/plans')->with('success', '套餐已更新:' . $plan->name); } protected function validatePlan(Request $request, ?int $planId = null): array { $data = $request->validate([ 'code' => ['required', 'string', 'max:50', 'regex:/^[A-Za-z0-9-_]+$/', Rule::unique('plans', 'code')->ignore($planId)], 'name' => ['required', 'string', 'max:100'], 'billing_cycle' => ['required', Rule::in(array_keys($this->billingCycleLabels()))], 'price' => ['required', 'numeric', 'min:0'], 'list_price' => ['nullable', 'numeric', 'min:0'], 'status' => ['required', Rule::in(array_keys($this->statusLabels()))], 'sort' => ['nullable', 'integer', 'min:0'], 'description' => ['nullable', 'string'], 'published_at' => ['nullable', 'date'], ], [ 'code.regex' => '套餐编码仅支持字母、数字、短横线与下划线。', ]); $data['sort'] = $data['sort'] ?? 0; return $data; } protected function applyFilters(Builder $query, array $filters): Builder { return $query ->when($filters['status'] !== '', fn (Builder $builder) => $builder->where('status', $filters['status'])) ->when($filters['billing_cycle'] !== '', fn (Builder $builder) => $builder->where('billing_cycle', $filters['billing_cycle'])) ->when(($filters['published'] ?? '') !== '', function (Builder $builder) use ($filters) { $published = (string) ($filters['published'] ?? ''); if ($published === 'published') { $builder->whereNotNull('published_at'); } elseif ($published === 'unpublished') { $builder->whereNull('published_at'); } }) ->when($filters['keyword'] !== '', function (Builder $builder) use ($filters) { $keyword = $filters['keyword']; $builder->where(function (Builder $subQuery) use ($keyword) { $subQuery->where('name', 'like', '%' . $keyword . '%') ->orWhere('code', 'like', '%' . $keyword . '%') ->orWhere('description', 'like', '%' . $keyword . '%'); }); }); } 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 [ 'active' => '启用中', 'draft' => '草稿中', 'inactive' => '未启用', ]; } protected function billingCycleLabels(): array { return [ 'monthly' => '月付', 'quarterly' => '季付', 'yearly' => '年付', 'one_time' => '一次性', ]; } }