feat(admin): 订阅列表支持一键绑定到订单(续费缺订阅治理)
This commit is contained in:
@@ -660,14 +660,19 @@ class PlatformOrderController extends Controller
|
|||||||
|
|
||||||
$data = $request->validate([
|
$data = $request->validate([
|
||||||
'site_subscription_id' => ['required', 'integer', 'exists:site_subscriptions,id'],
|
'site_subscription_id' => ['required', 'integer', 'exists:site_subscriptions,id'],
|
||||||
|
'back' => ['nullable', 'string', 'max:2000'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$safeBack = \App\Support\BackUrl::sanitizeForLinks((string) ($data['back'] ?? ''));
|
||||||
|
|
||||||
if ((string) ($order->order_type ?? '') !== 'renewal') {
|
if ((string) ($order->order_type ?? '') !== 'renewal') {
|
||||||
return redirect()->back()->with('warning', '仅「续费」类型订单允许绑定订阅。');
|
return ($safeBack !== '' ? redirect($safeBack) : redirect()->back())
|
||||||
|
->with('warning', '仅「续费」类型订单允许绑定订阅。');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((int) ($order->site_subscription_id ?? 0) > 0) {
|
if ((int) ($order->site_subscription_id ?? 0) > 0) {
|
||||||
return redirect()->back()->with('warning', '该订单已绑定订阅,无需重复操作。');
|
return ($safeBack !== '' ? redirect($safeBack) : redirect()->back())
|
||||||
|
->with('warning', '该订单已绑定订阅,无需重复操作。');
|
||||||
}
|
}
|
||||||
|
|
||||||
$subId = (int) $data['site_subscription_id'];
|
$subId = (int) $data['site_subscription_id'];
|
||||||
@@ -675,14 +680,16 @@ class PlatformOrderController extends Controller
|
|||||||
|
|
||||||
// 强约束:订阅上下文必须与订单一致
|
// 强约束:订阅上下文必须与订单一致
|
||||||
if ((int) ($sub->merchant_id ?? 0) !== (int) ($order->merchant_id ?? 0)) {
|
if ((int) ($sub->merchant_id ?? 0) !== (int) ($order->merchant_id ?? 0)) {
|
||||||
return redirect()->back()->withErrors([
|
return ($safeBack !== '' ? redirect($safeBack) : redirect()->back())
|
||||||
'site_subscription_id' => '订阅所属站点与订单站点不一致,禁止绑定(避免串单)。',
|
->withErrors([
|
||||||
]);
|
'site_subscription_id' => '订阅所属站点与订单站点不一致,禁止绑定(避免串单)。',
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
if ((int) ($sub->plan_id ?? 0) !== (int) ($order->plan_id ?? 0)) {
|
if ((int) ($sub->plan_id ?? 0) !== (int) ($order->plan_id ?? 0)) {
|
||||||
return redirect()->back()->withErrors([
|
return ($safeBack !== '' ? redirect($safeBack) : redirect()->back())
|
||||||
'site_subscription_id' => '订阅套餐与订单套餐不一致,禁止绑定(避免跨套餐续费)。',
|
->withErrors([
|
||||||
]);
|
'site_subscription_id' => '订阅套餐与订单套餐不一致,禁止绑定(避免跨套餐续费)。',
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$order->site_subscription_id = $sub->id;
|
$order->site_subscription_id = $sub->id;
|
||||||
@@ -703,7 +710,8 @@ class PlatformOrderController extends Controller
|
|||||||
|
|
||||||
$order->save();
|
$order->save();
|
||||||
|
|
||||||
return redirect()->back()->with('success', '已绑定订阅:' . (string) ($sub->subscription_no ?? $sub->id));
|
return ($safeBack !== '' ? redirect($safeBack) : redirect()->back())
|
||||||
|
->with('success', '已绑定订阅:' . (string) ($sub->subscription_no ?? $sub->id));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function activateSubscription(Request $request, PlatformOrder $order, SubscriptionActivationService $service): RedirectResponse
|
public function activateSubscription(Request $request, PlatformOrder $order, SubscriptionActivationService $service): RedirectResponse
|
||||||
|
|||||||
@@ -39,6 +39,12 @@
|
|||||||
$incomingBack = (string) request()->query('back', '');
|
$incomingBack = (string) request()->query('back', '');
|
||||||
$safeBackForLinks = \App\Support\BackUrl::sanitizeForLinks($incomingBack);
|
$safeBackForLinks = \App\Support\BackUrl::sanitizeForLinks($incomingBack);
|
||||||
|
|
||||||
|
// “从订单详情页来挑订阅”的治理交互:
|
||||||
|
// - attach_order_id:表示把选中的订阅绑定回某个订单
|
||||||
|
// - attach_back:绑定成功后回跳到哪里(通常是订单详情页)
|
||||||
|
$attachOrderId = (int) request()->query('attach_order_id', 0);
|
||||||
|
$safeAttachBackForLinks = \App\Support\BackUrl::sanitizeForLinks((string) request()->query('attach_back', ''));
|
||||||
|
|
||||||
// 用于摘要卡等入口:保留当前 query 并覆盖字段,同时安全透传 back。
|
// 用于摘要卡等入口:保留当前 query 并覆盖字段,同时安全透传 back。
|
||||||
$safeFullUrlWithQuery = function (array $overrides = []) use ($safeBackForLinks) {
|
$safeFullUrlWithQuery = function (array $overrides = []) use ($safeBackForLinks) {
|
||||||
return \App\Support\BackUrl::currentPathWithQuery($overrides, $safeBackForLinks);
|
return \App\Support\BackUrl::currentPathWithQuery($overrides, $safeBackForLinks);
|
||||||
@@ -300,6 +306,25 @@
|
|||||||
<div class="mt-4 actions gap-10">
|
<div class="mt-4 actions gap-10">
|
||||||
<a class="btn btn-secondary btn-sm" href="{!! $renewOrderUrl !!}">续费下单</a>
|
<a class="btn btn-secondary btn-sm" href="{!! $renewOrderUrl !!}">续费下单</a>
|
||||||
|
|
||||||
|
@if($attachOrderId > 0)
|
||||||
|
@php
|
||||||
|
// 从订单详情进入订阅管理页时:提供“绑定到该订单”的治理按钮
|
||||||
|
// 注意:提交后由 attachSubscription 做强校验(续费单 + merchant/plan 一致)
|
||||||
|
$attachBack = $safeAttachBackForLinks !== '' ? $safeAttachBackForLinks : $safeBackForLinks;
|
||||||
|
if ($attachBack === '') {
|
||||||
|
$attachBack = $back;
|
||||||
|
}
|
||||||
|
@endphp
|
||||||
|
<form method="post" action="/admin/platform-orders/{{ $attachOrderId }}/attach-subscription" class="inline-form" onsubmit="return confirm('确认将该订阅绑定到目标订单?');">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="site_subscription_id" value="{{ $subscription->id }}">
|
||||||
|
@if($attachBack !== '')
|
||||||
|
<input type="hidden" name="back" value="{!! $attachBack !!}">
|
||||||
|
@endif
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm">绑定到订单 #{{ $attachOrderId }}</button>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
|
||||||
<form method="post" action="/admin/site-subscriptions/{{ $subscription->id }}/set-status">
|
<form method="post" action="/admin/site-subscriptions/{{ $subscription->id }}/set-status">
|
||||||
@csrf
|
@csrf
|
||||||
<select name="status" onchange="this.form.submit()" class="w-140">
|
<select name="status" onchange="this.form.submit()" class="w-140">
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\Merchant;
|
||||||
|
use App\Models\Plan;
|
||||||
|
use App\Models\SiteSubscription;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class AdminSiteSubscriptionIndexAttachOrderIdShouldRenderBindButtonTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected function loginAsPlatformAdmin(): void
|
||||||
|
{
|
||||||
|
$this->seed();
|
||||||
|
|
||||||
|
$this->post('/admin/login', [
|
||||||
|
'email' => 'platform.admin@demo.local',
|
||||||
|
'password' => 'Platform@123456',
|
||||||
|
])->assertRedirect('/admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_index_should_render_bind_to_order_button_when_attach_order_id_present(): void
|
||||||
|
{
|
||||||
|
$this->loginAsPlatformAdmin();
|
||||||
|
|
||||||
|
$merchant = Merchant::query()->firstOrFail();
|
||||||
|
|
||||||
|
$plan = Plan::query()->create([
|
||||||
|
'code' => 'sub_index_attach_order_plan',
|
||||||
|
'name' => '订阅列表绑定订单按钮测试套餐',
|
||||||
|
'billing_cycle' => 'monthly',
|
||||||
|
'price' => 10,
|
||||||
|
'list_price' => 10,
|
||||||
|
'status' => 'active',
|
||||||
|
'sort' => 10,
|
||||||
|
'published_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$sub = SiteSubscription::query()->create([
|
||||||
|
'merchant_id' => $merchant->id,
|
||||||
|
'plan_id' => $plan->id,
|
||||||
|
'status' => 'active',
|
||||||
|
'source' => 'manual',
|
||||||
|
'subscription_no' => 'SS_ATTACH_ORDER_BTN_0001',
|
||||||
|
'plan_name' => $plan->name,
|
||||||
|
'billing_cycle' => $plan->billing_cycle,
|
||||||
|
'period_months' => 1,
|
||||||
|
'amount' => 10,
|
||||||
|
'starts_at' => now()->subDays(1),
|
||||||
|
'ends_at' => now()->addDays(10),
|
||||||
|
'snapshot' => [],
|
||||||
|
'meta' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$orderId = 12345;
|
||||||
|
|
||||||
|
$res = $this->get('/admin/site-subscriptions?attach_order_id=' . $orderId . '&attach_back=%2Fadmin%2Fplatform-orders%2F1');
|
||||||
|
$res->assertOk();
|
||||||
|
|
||||||
|
$html = (string) $res->getContent();
|
||||||
|
$this->assertStringContainsString('绑定到订单 #' . $orderId, $html);
|
||||||
|
$this->assertStringContainsString('/admin/platform-orders/' . $orderId . '/attach-subscription', $html);
|
||||||
|
$this->assertStringContainsString('name="site_subscription_id" value="' . $sub->id . '"', $html);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user