diff --git a/app/Http/Controllers/Admin/PlanController.php b/app/Http/Controllers/Admin/PlanController.php
index d65e4af..a8d0880 100644
--- a/app/Http/Controllers/Admin/PlanController.php
+++ b/app/Http/Controllers/Admin/PlanController.php
@@ -162,6 +162,64 @@ class PlanController extends Controller
]);
}
+ public function show(Request $request, Plan $plan): View
+ {
+ $this->ensurePlatformAdmin($request);
+
+ $plan->loadCount(['subscriptions', 'platformOrders']);
+
+ $summaryStats = [
+ 'subscriptions_count' => (int) SiteSubscription::query()->where('plan_id', $plan->id)->count(),
+ 'activated_subscriptions_count' => (int) SiteSubscription::query()->where('plan_id', $plan->id)->where('status', 'activated')->count(),
+ 'expiring_7d_subscriptions_count' => (int) SiteSubscription::query()
+ ->where('plan_id', $plan->id)
+ ->whereNotNull('ends_at')
+ ->whereBetween('ends_at', [now(), now()->copy()->addDays(7)])
+ ->count(),
+ 'platform_orders_count' => (int) PlatformOrder::query()->where('plan_id', $plan->id)->count(),
+ 'paid_orders_count' => (int) PlatformOrder::query()->where('plan_id', $plan->id)->where('payment_status', 'paid')->count(),
+ 'paid_amount_total' => (float) (PlatformOrder::query()->where('plan_id', $plan->id)->where('payment_status', 'paid')->sum('paid_amount') ?? 0),
+ 'paid_no_receipt_orders_count' => (int) PlatformOrder::query()
+ ->where('plan_id', $plan->id)
+ ->where('payment_status', 'paid')
+ ->whereRaw("JSON_EXTRACT(meta, '$.payment_summary.total_amount') IS NULL")
+ ->whereRaw("JSON_EXTRACT(meta, '$.payment_receipts[0].amount') IS NULL")
+ ->count(),
+ 'sync_failed_orders_count' => (int) PlatformOrder::query()
+ ->where('plan_id', $plan->id)
+ ->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') IS NOT NULL")
+ ->count(),
+ 'renewal_missing_subscription_orders_count' => (int) PlatformOrder::query()
+ ->where('plan_id', $plan->id)
+ ->where('order_type', 'renewal')
+ ->whereNull('site_subscription_id')
+ ->count(),
+ ];
+
+ $recentOrders = PlatformOrder::query()
+ ->with(['merchant', 'siteSubscription'])
+ ->where('plan_id', $plan->id)
+ ->orderByDesc('id')
+ ->limit(10)
+ ->get();
+
+ $recentSubscriptions = SiteSubscription::query()
+ ->with('merchant')
+ ->where('plan_id', $plan->id)
+ ->orderByDesc('id')
+ ->limit(10)
+ ->get();
+
+ return view('admin.plans.show', [
+ 'plan' => $plan,
+ 'summaryStats' => $summaryStats,
+ 'recentOrders' => $recentOrders,
+ 'recentSubscriptions' => $recentSubscriptions,
+ 'statusLabels' => $this->statusLabels(),
+ 'billingCycleLabels' => $this->billingCycleLabels(),
+ ]);
+ }
+
public function store(Request $request): RedirectResponse
{
$this->ensurePlatformAdmin($request);
diff --git a/resources/views/admin/plans/index.blade.php b/resources/views/admin/plans/index.blade.php
index 7f64e09..6df2e45 100644
--- a/resources/views/admin/plans/index.blade.php
+++ b/resources/views/admin/plans/index.blade.php
@@ -257,6 +257,7 @@
@php
+ $showPlanUrl = \App\Support\BackUrl::withBack('/admin/plans/' . $plan->id, $selfWithoutBack);
$editPlanUrl = \App\Support\BackUrl::withBack('/admin/plans/' . $plan->id . '/edit', $selfWithoutBack);
$createOrderUrl = \App\Support\BackUrl::withBack('/admin/platform-orders/create?' . \Illuminate\Support\Arr::query([
'plan_id' => $plan->id,
@@ -275,6 +276,7 @@
]);
@endphp
+ 查看详情
编辑
@if((string) ($plan->status ?? '') === 'active')
创建订单
diff --git a/resources/views/admin/plans/show.blade.php b/resources/views/admin/plans/show.blade.php
new file mode 100644
index 0000000..e276fc4
--- /dev/null
+++ b/resources/views/admin/plans/show.blade.php
@@ -0,0 +1,215 @@
+@extends('admin.layouts.app')
+
+@section('title', '套餐详情')
+@section('page_title', '套餐详情')
+
+@section('content')
+@php
+ $planShowSelf = \App\Support\BackUrl::selfWithoutBack();
+
+ $incomingBack = (string) request()->query('back', '');
+ $safeBackForLinks = \App\Support\BackUrl::sanitizeForLinks($incomingBack);
+
+ $makeSubscriptionUrl = function (array $query) use ($planShowSelf) {
+ return \App\Support\BackUrl::withBack('/admin/site-subscriptions?' . \Illuminate\Support\Arr::query($query), $planShowSelf);
+ };
+
+ $makePlatformOrderUrl = function (array $query) use ($planShowSelf) {
+ return \App\Support\BackUrl::withBack('/admin/platform-orders?' . \Illuminate\Support\Arr::query($query), $planShowSelf);
+ };
+
+ $editPlanUrl = \App\Support\BackUrl::withBack('/admin/plans/' . $plan->id . '/edit', $planShowSelf);
+ $createOrderUrl = \App\Support\BackUrl::withBack('/admin/platform-orders/create?' . \Illuminate\Support\Arr::query([
+ 'plan_id' => $plan->id,
+ 'order_type' => 'new_purchase',
+ ]), $planShowSelf);
+@endphp
+
+
+
+
+
+
+
+
+
+ 已支付订单
+
+ 已付总额:¥{{ number_format((float) ($summaryStats['paid_amount_total'] ?? 0), 2) }}
+
+
+
+
+
+
+
+ 套餐信息
+
+
+ | ID | {{ $plan->id }} |
+ | 套餐名称 | {{ $plan->name }} |
+ | 编码 | {{ $plan->code }} |
+ | 状态 | {{ $statusLabels[$plan->status] ?? $plan->status }} ({{ $plan->status }}) |
+ | 计费周期 | {{ $billingCycleLabels[$plan->billing_cycle] ?? $plan->billing_cycle }} |
+ | 售价 / 划线价 | ¥{{ number_format((float) $plan->price, 2) }} / ¥{{ number_format((float) $plan->list_price, 2) }} |
+ | 发布时间 | {{ optional($plan->published_at)->format('Y-m-d H:i:s') ?: '-' }} |
+ | 描述 | {{ $plan->description ?: '暂无说明' }} |
+
+
+
+
+
+
+
+
+
+
+
+
+ | 订单号 |
+ 站点 |
+ 订单类型 |
+ 订单状态 |
+ 支付状态 |
+ 应付 / 已付 |
+ 关联订阅 |
+
+
+
+ @forelse($recentOrders as $order)
+
+ | {{ $order->order_no }} |
+ {{ $order->merchant?->name ?? '未关联站点' }} |
+ {{ $order->orderTypeLabel() }} |
+ {{ $order->status }} |
+ {{ $order->payment_status }} |
+ ¥{{ number_format((float) $order->payable_amount, 2) }} / ¥{{ number_format((float) $order->paid_amount, 2) }} |
+ {{ $order->siteSubscription?->subscription_no ?? '-' }} |
+
+ @empty
+
+ | 暂无平台订单 |
+
+ @endforelse
+
+
+
+
+
+
+
+
+
+
+
+ | 订阅号 |
+ 站点 |
+ 状态 |
+ 金额 |
+ 开始时间 |
+ 到期时间 |
+
+
+
+ @forelse($recentSubscriptions as $subscription)
+
+ | {{ $subscription->subscription_no }} |
+ {{ $subscription->merchant?->name ?? '未关联站点' }} |
+ {{ $subscription->status }} |
+ ¥{{ number_format((float) $subscription->amount, 2) }} |
+ {{ optional($subscription->starts_at)->format('Y-m-d H:i:s') ?: '-' }} |
+ {{ optional($subscription->ends_at)->format('Y-m-d H:i:s') ?: '-' }} |
+
+ @empty
+
+ | 暂无订阅记录 |
+
+ @endforelse
+
+
+
+
+@endsection
diff --git a/resources/views/admin/site_subscriptions/show.blade.php b/resources/views/admin/site_subscriptions/show.blade.php
index 1ce9366..e09c4d3 100644
--- a/resources/views/admin/site_subscriptions/show.blade.php
+++ b/resources/views/admin/site_subscriptions/show.blade.php
@@ -260,6 +260,10 @@
/ ¥{{ number_format((float) ($summaryStats['total_receipt_amount'] ?? 0), 2) }}
点击订单数可跳转:该订阅下「有回执」订单
+
已付无回执订单:
{{ $summaryStats['no_receipt_orders'] ?? 0 }}
diff --git a/routes/web.php b/routes/web.php
index f5548e1..c5e2ab8 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -149,6 +149,7 @@ Route::prefix('admin')->group(function () {
Route::post('/plans', [PlanController::class, 'store']);
// 注意:必须放在 /plans/{plan} 之前,避免被参数路由吞掉导致 404
Route::post('/plans/seed-defaults', [PlanController::class, 'seedDefaults']);
+ Route::get('/plans/{plan}', [PlanController::class, 'show']);
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/AdminPlanShowTest.php b/tests/Feature/AdminPlanShowTest.php
new file mode 100644
index 0000000..7f950a6
--- /dev/null
+++ b/tests/Feature/AdminPlanShowTest.php
@@ -0,0 +1,182 @@
+seed();
+
+ $this->post('/admin/login', [
+ 'email' => 'platform.admin@demo.local',
+ 'password' => 'Platform@123456',
+ ])->assertRedirect('/admin');
+ }
+
+ public function test_platform_admin_can_open_plan_show_page(): void
+ {
+ $this->loginAsPlatformAdmin();
+
+ $merchant = Merchant::query()->firstOrFail();
+
+ $plan = Plan::query()->create([
+ 'code' => 'plan_show_test',
+ 'name' => '套餐详情测试套餐',
+ 'billing_cycle' => 'monthly',
+ 'price' => 99,
+ 'list_price' => 199,
+ 'status' => 'active',
+ 'sort' => 10,
+ 'description' => '用于验证套餐详情页',
+ 'published_at' => now(),
+ ]);
+
+ $subscription = SiteSubscription::query()->create([
+ 'merchant_id' => $merchant->id,
+ 'plan_id' => $plan->id,
+ 'status' => 'activated',
+ 'source' => 'manual',
+ 'subscription_no' => 'SUB_PLAN_SHOW_0001',
+ 'plan_name' => $plan->name,
+ 'billing_cycle' => $plan->billing_cycle,
+ 'period_months' => 1,
+ 'amount' => 99,
+ 'starts_at' => now()->subDay(),
+ 'ends_at' => now()->addDays(5),
+ 'activated_at' => now()->subDay(),
+ ]);
+
+ PlatformOrder::query()->create([
+ 'merchant_id' => $merchant->id,
+ 'plan_id' => $plan->id,
+ 'site_subscription_id' => $subscription->id,
+ 'order_no' => 'PO_PLAN_SHOW_0001',
+ 'order_type' => 'renewal',
+ 'status' => 'activated',
+ 'payment_status' => 'paid',
+ 'plan_name' => $plan->name,
+ 'billing_cycle' => $plan->billing_cycle,
+ 'period_months' => 1,
+ 'quantity' => 1,
+ 'list_amount' => 99,
+ 'discount_amount' => 0,
+ 'payable_amount' => 99,
+ 'paid_amount' => 99,
+ 'placed_at' => now()->subHour(),
+ 'paid_at' => now()->subMinutes(50),
+ 'activated_at' => now()->subMinutes(40),
+ 'meta' => [],
+ ]);
+
+ $res = $this->get('/admin/plans/' . $plan->id);
+ $res->assertOk()
+ ->assertSee('套餐详情')
+ ->assertSee('套餐详情测试套餐')
+ ->assertSee('查看已付无回执订单')
+ ->assertSee('查看续费缺订阅订单');
+ }
+
+ public function test_guest_cannot_open_plan_show_page(): void
+ {
+ $plan = Plan::query()->create([
+ 'code' => 'plan_show_guest_test',
+ 'name' => '游客不可见套餐',
+ 'billing_cycle' => 'monthly',
+ 'price' => 10,
+ 'list_price' => 10,
+ 'status' => 'active',
+ 'sort' => 10,
+ ]);
+
+ $this->get('/admin/plans/' . $plan->id)->assertRedirect('/admin/login');
+ }
+
+ public function test_plan_show_links_to_subscriptions_and_orders_should_contain_back_to_plan_show(): void
+ {
+ $this->loginAsPlatformAdmin();
+
+ $plan = Plan::query()->create([
+ 'code' => 'plan_show_back_test',
+ 'name' => '套餐详情 back 测试套餐',
+ 'billing_cycle' => 'monthly',
+ 'price' => 88,
+ 'list_price' => 108,
+ 'status' => 'active',
+ 'sort' => 10,
+ 'published_at' => now(),
+ ]);
+
+ $res = $this->get('/admin/plans/' . $plan->id);
+ $res->assertOk();
+
+ $back = '/admin/plans/' . $plan->id;
+
+ $expectedSubscriptionUrl = '/admin/site-subscriptions?' . Arr::query([
+ 'plan_id' => $plan->id,
+ 'back' => $back,
+ ]);
+
+ $expectedOrderUrl = '/admin/platform-orders?' . Arr::query([
+ 'plan_id' => $plan->id,
+ 'back' => $back,
+ ]);
+
+ $expectedPaidNoReceiptUrl = '/admin/platform-orders?' . Arr::query([
+ 'plan_id' => $plan->id,
+ 'payment_status' => 'paid',
+ 'receipt_status' => 'none',
+ 'back' => $back,
+ ]);
+
+ $expectedRenewalMissingUrl = '/admin/platform-orders?' . Arr::query([
+ 'plan_id' => $plan->id,
+ 'renewal_missing_subscription' => '1',
+ 'back' => $back,
+ ]);
+
+ $res->assertSee($expectedSubscriptionUrl, false);
+ $res->assertSee($expectedOrderUrl, false);
+ $res->assertSee($expectedPaidNoReceiptUrl, false);
+ $res->assertSee($expectedRenewalMissingUrl, false);
+ }
+
+ public function test_plan_index_show_link_should_carry_back_to_index_self_without_back(): void
+ {
+ $this->loginAsPlatformAdmin();
+
+ $plan = Plan::query()->create([
+ 'code' => 'plan_index_show_link_test',
+ 'name' => '套餐列表详情入口测试套餐',
+ 'billing_cycle' => 'monthly',
+ 'price' => 18,
+ 'list_price' => 18,
+ 'status' => 'active',
+ 'sort' => 10,
+ ]);
+
+ $res = $this->get('/admin/plans?status=active&back=' . urlencode('/admin/platform-orders'));
+ $res->assertOk();
+
+ $expectedBack = '/admin/plans?' . Arr::query([
+ 'status' => 'active',
+ ]);
+
+ $expectedShowUrl = '/admin/plans/' . $plan->id . '?' . Arr::query([
+ 'back' => $expectedBack,
+ ]);
+
+ $res->assertSee($expectedShowUrl, false);
+ $res->assertSee('查看详情');
+ }
+}
diff --git a/tests/Feature/AdminSiteSubscriptionShowBroadNoReceiptOrdersLinkShouldUseSubscriptionShowSelfBackTest.php b/tests/Feature/AdminSiteSubscriptionShowBroadNoReceiptOrdersLinkShouldUseSubscriptionShowSelfBackTest.php
new file mode 100644
index 0000000..3c7464f
--- /dev/null
+++ b/tests/Feature/AdminSiteSubscriptionShowBroadNoReceiptOrdersLinkShouldUseSubscriptionShowSelfBackTest.php
@@ -0,0 +1,74 @@
+seed();
+
+ $this->post('/admin/login', [
+ 'email' => 'platform.admin@demo.local',
+ 'password' => 'Platform@123456',
+ ])->assertRedirect('/admin');
+ }
+
+ public function test_broad_no_receipt_orders_link_should_use_subscription_show_self_as_back(): void
+ {
+ $this->loginAsPlatformAdmin();
+
+ $merchant = Merchant::query()->firstOrFail();
+ $plan = Plan::query()->create([
+ 'code' => 'sub_show_broad_no_receipt_self_back_plan',
+ 'name' => '订阅详情广义无回执回跳自环测试套餐',
+ 'billing_cycle' => 'monthly',
+ 'price' => 66,
+ 'list_price' => 66,
+ 'status' => 'active',
+ 'sort' => 10,
+ 'published_at' => now(),
+ ]);
+
+ $subscription = SiteSubscription::query()->create([
+ 'merchant_id' => $merchant->id,
+ 'plan_id' => $plan->id,
+ 'status' => 'active',
+ 'source' => 'manual',
+ 'subscription_no' => 'SS_SHOW_BNR_SELF_BACK_0001',
+ 'plan_name' => $plan->name,
+ 'billing_cycle' => $plan->billing_cycle,
+ 'period_months' => 1,
+ 'amount' => 66,
+ 'starts_at' => now()->subDay(),
+ 'ends_at' => now()->addMonth(),
+ ]);
+
+ $res = $this->get('/admin/site-subscriptions/' . $subscription->id . '?back=' . urlencode('/admin/platform-orders?payment_status=paid'));
+ $res->assertOk();
+
+ $html = (string) $res->getContent();
+ $matched = preg_match('/无回执订单(广义):\s* ]+href="([^"]+)"/u', $html, $m);
+ $this->assertSame(1, $matched, '未找到订阅详情页“无回执订单(广义)”链接');
+
+ $href = html_entity_decode($m[1] ?? '');
+ $expectedUrl = '/admin/platform-orders?' . Arr::query([
+ 'site_subscription_id' => $subscription->id,
+ 'receipt_status' => 'none',
+ 'back' => '/admin/site-subscriptions/' . $subscription->id,
+ ]);
+
+ $this->assertSame($expectedUrl, $href);
+ $this->assertStringNotContainsString('payment_status=paid', $href);
+ $this->assertStringNotContainsString(urlencode('/admin/platform-orders?payment_status=paid'), $href);
+ }
+}
|