chore: init saasshop repo + sql migrations runner + gitee go

This commit is contained in:
萝卜
2026-03-10 11:31:02 +00:00
commit 50f15cdea8
210 changed files with 29534 additions and 0 deletions

View File

@@ -0,0 +1,249 @@
<?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' => '一次性',
];
}
}