From 09c8aeca2ab167bcf3cbe4c446330dd94a13ee0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=90=9D=E5=8D=9C?= Date: Sun, 15 Mar 2026 02:56:31 +0000 Subject: [PATCH] SubscriptionActivationService: guard against merchant mismatch --- app/Support/SubscriptionActivationService.php | 5 ++ ...vationServiceMerchantMismatchGuardTest.php | 78 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 tests/Feature/SubscriptionActivationServiceMerchantMismatchGuardTest.php diff --git a/app/Support/SubscriptionActivationService.php b/app/Support/SubscriptionActivationService.php index ccce05f..911a60c 100644 --- a/app/Support/SubscriptionActivationService.php +++ b/app/Support/SubscriptionActivationService.php @@ -58,6 +58,11 @@ class SubscriptionActivationService /** @var SiteSubscription $subscription */ $subscription = SiteSubscription::query()->findOrFail($order->site_subscription_id); + // 治理安全阀:订单绑定的订阅必须属于同一站点(merchant),否则拒绝同步,避免误续费/串单。 + if ((int) $subscription->merchant_id !== (int) $order->merchant_id) { + throw new \InvalidArgumentException('订阅与订单站点不一致:请核对订阅ID与订单站点后再同步'); + } + // 以 ends_at 为基准续期: // - 若 ends_at 为空或已过期 => 从 now 起算 // - 若仍有效 => 从 ends_at 起算 diff --git a/tests/Feature/SubscriptionActivationServiceMerchantMismatchGuardTest.php b/tests/Feature/SubscriptionActivationServiceMerchantMismatchGuardTest.php new file mode 100644 index 0000000..66d7dd4 --- /dev/null +++ b/tests/Feature/SubscriptionActivationServiceMerchantMismatchGuardTest.php @@ -0,0 +1,78 @@ +seed(); + + $merchantA = Merchant::query()->firstOrFail(); + $merchantB = Merchant::query()->create([ + 'name' => '另一个演示站点', + 'slug' => 'demo-merchant-b', + 'status' => 'active', + ]); + + $plan = Plan::query()->create([ + 'code' => 'mismatch_guard_test_plan', + 'name' => '站点不一致护栏测试套餐', + 'billing_cycle' => 'monthly', + 'price' => 10, + 'list_price' => 10, + 'status' => 'active', + 'sort' => 10, + 'published_at' => now(), + ]); + + $sub = SiteSubscription::query()->create([ + 'merchant_id' => $merchantA->id, + 'plan_id' => $plan->id, + 'status' => 'activated', + 'source' => 'manual', + 'subscription_no' => 'SUB_MERCHANT_MISMATCH_0001', + 'plan_name' => $plan->name, + 'billing_cycle' => $plan->billing_cycle, + 'period_months' => 1, + 'amount' => 10, + 'starts_at' => now()->subDay(), + 'ends_at' => now()->addDays(20), + 'activated_at' => now()->subDay(), + ]); + + $order = PlatformOrder::query()->create([ + 'merchant_id' => $merchantB->id, + 'plan_id' => $plan->id, + 'site_subscription_id' => $sub->id, + 'order_no' => 'PO_MERCHANT_MISMATCH_0001', + 'order_type' => 'renewal', + 'status' => 'activated', + 'payment_status' => 'paid', + 'plan_name' => $plan->name, + 'billing_cycle' => $plan->billing_cycle, + 'period_months' => 1, + 'quantity' => 1, + 'payable_amount' => 10, + 'paid_amount' => 10, + 'placed_at' => now()->subMinutes(10), + 'paid_at' => now()->subMinutes(5), + 'activated_at' => now()->subMinutes(1), + ]); + + $this->expectException(\InvalidArgumentException::class); + + $service = new SubscriptionActivationService(); + $service->activateOrder($order->id, 1); + } +}