From 8e18a77f1964444833a868e203120f13c5e19637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=90=9D=E5=8D=9C?= Date: Sun, 15 Mar 2026 16:58:04 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E7=BB=AD=E8=B4=B9=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E6=94=AF=E6=8C=81=E6=89=8B=E5=B7=A5=E7=BB=91=E5=AE=9A?= =?UTF-8?q?=E8=AE=A2=E9=98=85=EF=BC=88attach-subscription=EF=BC=89+=20?= =?UTF-8?q?=E6=97=B6=E5=8C=BA=E6=94=B9=E4=B8=BA=20Asia/Shanghai?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Admin/PlatformOrderController.php | 61 +++++++++++++++++++ config/app.php | 2 +- routes/web.php | 2 + ...OrderAttachSubscriptionShouldGuardTest.php | 58 ++++++++++++++++++ 4 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 tests/Feature/AdminPlatformOrderAttachSubscriptionShouldGuardTest.php diff --git a/app/Http/Controllers/Admin/PlatformOrderController.php b/app/Http/Controllers/Admin/PlatformOrderController.php index 22f7cb9..a53b0c7 100644 --- a/app/Http/Controllers/Admin/PlatformOrderController.php +++ b/app/Http/Controllers/Admin/PlatformOrderController.php @@ -645,6 +645,67 @@ class PlatformOrderController extends Controller ]); } + /** + * 治理动作:为订单手工绑定订阅(用于“续费缺订阅”脏数据修复)。 + * + * 口径: + * - 仅平台管理员可操作(ensurePlatformAdmin) + * - 仅允许续费单绑定订阅 + * - 订阅必须与订单 merchant_id / plan_id 一致,避免串单 + * - 写入 meta.audit 留痕 + */ + public function attachSubscription(Request $request, PlatformOrder $order): RedirectResponse + { + $admin = $this->ensurePlatformAdmin($request); + + $data = $request->validate([ + 'site_subscription_id' => ['required', 'integer', 'exists:site_subscriptions,id'], + ]); + + if ((string) ($order->order_type ?? '') !== 'renewal') { + return redirect()->back()->with('warning', '仅「续费」类型订单允许绑定订阅。'); + } + + if ((int) ($order->site_subscription_id ?? 0) > 0) { + return redirect()->back()->with('warning', '该订单已绑定订阅,无需重复操作。'); + } + + $subId = (int) $data['site_subscription_id']; + $sub = SiteSubscription::query()->with(['merchant', 'plan'])->findOrFail($subId); + + // 强约束:订阅上下文必须与订单一致 + if ((int) ($sub->merchant_id ?? 0) !== (int) ($order->merchant_id ?? 0)) { + return redirect()->back()->withErrors([ + 'site_subscription_id' => '订阅所属站点与订单站点不一致,禁止绑定(避免串单)。', + ]); + } + if ((int) ($sub->plan_id ?? 0) !== (int) ($order->plan_id ?? 0)) { + return redirect()->back()->withErrors([ + 'site_subscription_id' => '订阅套餐与订单套餐不一致,禁止绑定(避免跨套餐续费)。', + ]); + } + + $order->site_subscription_id = $sub->id; + + $meta = (array) ($order->meta ?? []); + $audit = (array) (data_get($meta, 'audit', []) ?? []); + $audit[] = [ + 'action' => 'attach_subscription', + 'scope' => 'single', + 'at' => now()->toDateTimeString(), + 'admin_id' => $admin->id, + 'subscription_id' => $sub->id, + 'subscription_no' => (string) ($sub->subscription_no ?? ''), + 'note' => '续费缺订阅治理:手工绑定订阅', + ]; + data_set($meta, 'audit', $audit); + $order->meta = $meta; + + $order->save(); + + return redirect()->back()->with('success', '已绑定订阅:' . (string) ($sub->subscription_no ?? $sub->id)); + } + public function activateSubscription(Request $request, PlatformOrder $order, SubscriptionActivationService $service): RedirectResponse { $admin = $this->ensurePlatformAdmin($request); diff --git a/config/app.php b/config/app.php index 423eed5..67f10fe 100644 --- a/config/app.php +++ b/config/app.php @@ -65,7 +65,7 @@ return [ | */ - 'timezone' => 'UTC', + 'timezone' => env('APP_TIMEZONE', 'Asia/Shanghai'), /* |-------------------------------------------------------------------------- diff --git a/routes/web.php b/routes/web.php index 75fba36..9143165 100644 --- a/routes/web.php +++ b/routes/web.php @@ -129,6 +129,8 @@ Route::prefix('admin')->group(function () { Route::post('/platform-orders/{order}/mark-partially-refunded', [PlatformOrderController::class, 'markPartiallyRefunded']); Route::post('/platform-orders/{order}/mark-paid-status', [PlatformOrderController::class, 'markPaidStatus']); Route::post('/platform-orders/{order}/mark-activated', [PlatformOrderController::class, 'markActivated']); + // 治理动作:续费缺订阅时,手工绑定订阅(补齐闭环) + Route::post('/platform-orders/{order}/attach-subscription', [PlatformOrderController::class, 'attachSubscription']); Route::post('/platform-orders/{order}/clear-sync-error', [PlatformOrderController::class, 'clearSyncError']); Route::post('/platform-orders/{order}/clear-bmpa-error', [PlatformOrderController::class, 'clearBmpaError']); diff --git a/tests/Feature/AdminPlatformOrderAttachSubscriptionShouldGuardTest.php b/tests/Feature/AdminPlatformOrderAttachSubscriptionShouldGuardTest.php new file mode 100644 index 0000000..914d16f --- /dev/null +++ b/tests/Feature/AdminPlatformOrderAttachSubscriptionShouldGuardTest.php @@ -0,0 +1,58 @@ +seed(); + + $this->post('/admin/login', [ + 'email' => 'platform.admin@demo.local', + 'password' => 'Platform@123456', + ])->assertRedirect('/admin'); + } + + public function test_attach_subscription_should_guard_non_renewal_orders(): void + { + $this->loginAsPlatformAdmin(); + + $order = PlatformOrder::query()->create([ + 'merchant_id' => 1, + 'plan_id' => 1, + 'order_no' => 'PO_TEST_ATTACH_1', + 'order_type' => 'new_purchase', + 'status' => 'placed', + 'payment_status' => 'unpaid', + 'payment_channel' => '', + 'plan_name' => 'Test', + 'billing_cycle' => 'monthly', + 'period_months' => 1, + 'quantity' => 1, + 'list_amount' => 0, + 'discount_amount' => 0, + 'payable_amount' => 0, + 'paid_amount' => 0, + 'meta' => [], + ]); + + $sub = SiteSubscription::query()->firstOrFail(); + + $res = $this->post("/admin/platform-orders/{$order->id}/attach-subscription", [ + 'site_subscription_id' => $sub->id, + ]); + + $res->assertRedirect(); + + $order->refresh(); + $this->assertTrue((int) ($order->site_subscription_id ?? 0) === 0); + } +}