Files
saasshop/app/Http/Controllers/Admin/PlanController.php

250 lines
9.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Concerns\ResolvesPlatformAdminContext;
use App\Http\Controllers\Controller;
use App\Models\Plan;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\StreamedResponse;
class PlanController extends Controller
{
use ResolvesPlatformAdminContext;
public function export(Request $request): StreamedResponse
{
$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' => 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(), $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(),
],
'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 . '%');
});
});
}
protected function statusLabels(): array
{
return [
'active' => '启用中',
'draft' => '草稿中',
'inactive' => '未启用',
];
}
protected function billingCycleLabels(): array
{
return [
'monthly' => '月付',
'quarterly' => '季付',
'yearly' => '年付',
'one_time' => '一次性',
];
}
}