diff --git a/app/Http/Controllers/Admin/PlatformOrderController.php b/app/Http/Controllers/Admin/PlatformOrderController.php index 25f1c2a..fc2df7e 100644 --- a/app/Http/Controllers/Admin/PlatformOrderController.php +++ b/app/Http/Controllers/Admin/PlatformOrderController.php @@ -419,6 +419,8 @@ class PlatformOrderController extends Controller ->where('payment_status', 'paid') ->where('status', 'activated') ->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation.subscription_id') IS NULL") + // 口径一致:syncable_only=1 隐含 sync_status=unsynced(排除同步失败等异常单) + ->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') IS NULL") // 口径一致:排除(续费但未绑定订阅)的脏数据 ->where(function (\Illuminate\Database\Eloquent\Builder $q) { $q->where('order_type', '!=', 'renewal') @@ -2245,11 +2247,14 @@ class PlatformOrderController extends Controller } }) ->when(($filters['syncable_only'] ?? '') === '1', function (Builder $builder) { - // 只看可同步:已支付 + 已生效 + 尚未写入 subscription_activation.subscription_id + // 只看可同步:已支付 + 已生效 + 未同步 + 非失败 + // - 未同步:subscription_activation.subscription_id 为空 + // - 非失败:subscription_activation_error.message 为空 // 额外治理口径:排除(续费但未绑定订阅)的脏数据,避免误入可同步集合导致串单风险 $builder->where('payment_status', 'paid') ->where('status', 'activated') ->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation.subscription_id') IS NULL") + ->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') IS NULL") ->where(function (Builder $q) { $q->where('order_type', '!=', 'renewal') ->orWhereNotNull('site_subscription_id'); diff --git a/tests/Feature/AdminPlatformOrderSyncableFilterShouldExcludeSyncFailedOrdersTest.php b/tests/Feature/AdminPlatformOrderSyncableFilterShouldExcludeSyncFailedOrdersTest.php new file mode 100644 index 0000000..2d6e8db --- /dev/null +++ b/tests/Feature/AdminPlatformOrderSyncableFilterShouldExcludeSyncFailedOrdersTest.php @@ -0,0 +1,88 @@ +seed(); + + $this->post('/admin/login', [ + 'email' => 'platform.admin@demo.local', + 'password' => 'Platform@123456', + ])->assertRedirect('/admin'); + } + + public function test_syncable_only_filter_should_exclude_sync_failed_orders(): void + { + $this->loginAsPlatformAdmin(); + + $merchant = Merchant::query()->firstOrFail(); + $plan = Plan::query()->create([ + 'code' => 'po_syncable_filter_exclude_sync_failed_plan', + 'name' => 'syncable_only 排除同步失败测试套餐', + 'billing_cycle' => 'monthly', + 'price' => 10, + 'list_price' => 10, + 'status' => 'active', + 'sort' => 10, + 'published_at' => now(), + ]); + + $good = PlatformOrder::query()->create([ + 'merchant_id' => $merchant->id, + 'plan_id' => $plan->id, + 'order_no' => 'PO_SYNCABLE_OK_NO_ERROR_0001', + 'order_type' => 'new_purchase', + '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(9), + 'activated_at' => now()->subMinutes(8), + 'meta' => [], + ]); + + $bad = PlatformOrder::query()->create([ + 'merchant_id' => $merchant->id, + 'plan_id' => $plan->id, + 'order_no' => 'PO_SYNCABLE_BAD_HAS_ERROR_0002', + 'order_type' => 'new_purchase', + '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(7), + 'paid_at' => now()->subMinutes(6), + 'activated_at' => now()->subMinutes(5), + 'meta' => [ + 'subscription_activation_error' => [ + 'message' => '模拟同步失败:站点已过期', + ], + ], + ]); + + $this->get('/admin/platform-orders?syncable_only=1') + ->assertOk() + ->assertSee($good->order_no) + ->assertDontSee($bad->order_no); + } +}