diff --git a/app/Http/Controllers/Admin/PlatformOrderController.php b/app/Http/Controllers/Admin/PlatformOrderController.php index 929631e..7dc1adc 100644 --- a/app/Http/Controllers/Admin/PlatformOrderController.php +++ b/app/Http/Controllers/Admin/PlatformOrderController.php @@ -583,6 +583,105 @@ class PlatformOrderController extends Controller return redirect()->back()->with('success', $msg); } + public function batchMarkActivated(Request $request): RedirectResponse + { + $admin = $this->ensurePlatformAdmin($request); + + // 支持两种 scope: + // - scope=filtered:只处理当前筛选范围内的订单(更安全,默认) + // - scope=all:处理全部订单(谨慎) + $scope = (string) $request->input('scope', 'filtered'); + + $filters = [ + 'status' => trim((string) $request->input('status', '')), + 'payment_status' => trim((string) $request->input('payment_status', '')), + 'merchant_id' => trim((string) $request->input('merchant_id', '')), + 'plan_id' => trim((string) $request->input('plan_id', '')), + 'site_subscription_id' => trim((string) $request->input('site_subscription_id', '')), + 'fail_only' => (string) $request->input('fail_only', ''), + 'synced_only' => (string) $request->input('synced_only', ''), + 'sync_status' => trim((string) $request->input('sync_status', '')), + 'keyword' => trim((string) $request->input('keyword', '')), + 'syncable_only' => (string) $request->input('syncable_only', ''), + 'batch_synced_24h' => (string) $request->input('batch_synced_24h', ''), + ]; + + // 防误操作:批量“仅标记为已生效”默认要求当前筛选口径为「已支付 + 待处理(pending)」 + if ($scope === 'filtered') { + if (($filters['payment_status'] ?? '') !== 'paid' || ($filters['status'] ?? '') !== 'pending') { + return redirect()->back()->with('warning', '为避免误操作,请先筛选「支付状态=已支付」且「订单状态=待处理」,再执行批量仅标记为已生效。'); + } + } + + // 防误操作:scope=all 需要二次确认 + if ($scope === 'all' && (string) $request->input('confirm', '') !== 'YES') { + return redirect()->back()->with('warning', '为避免误操作,执行全量批量生效前请在确认框输入 YES。'); + } + + $query = PlatformOrder::query(); + + if ($scope === 'filtered') { + $query = $this->applyFilters($query, $filters); + } + + // 只处理“已支付 + 待处理”的订单(双保险) + $query = $query + ->where('payment_status', 'paid') + ->where('status', 'pending'); + + $limit = (int) $request->input('limit', 50); + $limit = max(1, min(500, $limit)); + + $matchedTotal = (clone $query)->count(); + + $orders = $query->orderByDesc('id')->limit($limit)->get(['id']); + $processed = $orders->count(); + + $success = 0; + $nowStr = now()->toDateTimeString(); + + foreach ($orders as $row) { + $order = PlatformOrder::query()->find($row->id); + if (! $order) { + continue; + } + + // 再次防御:仅推进 pending + if ($order->payment_status !== 'paid' || $order->status !== 'pending') { + continue; + } + + $order->status = 'activated'; + $order->activated_at = $order->activated_at ?: now(); + + $meta = (array) ($order->meta ?? []); + $audit = (array) (data_get($meta, 'audit', []) ?? []); + $audit[] = [ + 'action' => 'batch_mark_activated', + 'scope' => $scope, + 'at' => $nowStr, + 'admin_id' => $admin->id, + ]; + data_set($meta, 'audit', $audit); + + // 便于筛选/统计:记录最近一次批量生效信息(扁平字段) + data_set($meta, 'batch_mark_activated', [ + 'at' => $nowStr, + 'admin_id' => $admin->id, + 'scope' => $scope, + ]); + + $order->meta = $meta; + $order->save(); + + $success++; + } + + $msg = '批量仅标记为已生效完成:成功 ' . $success . ' 条(命中 ' . $matchedTotal . ' 条,本次处理 ' . $processed . ' 条,limit=' . $limit . ')'; + + return redirect()->back()->with('success', $msg); + } + public function clearSyncErrors(Request $request): RedirectResponse { $this->ensurePlatformAdmin($request); diff --git a/resources/views/admin/platform_orders/index.blade.php b/resources/views/admin/platform_orders/index.blade.php index ecd4681..5c38712 100644 --- a/resources/views/admin/platform_orders/index.blade.php +++ b/resources/views/admin/platform_orders/index.blade.php @@ -200,6 +200,45 @@ +
+ @csrf + + + + + + + + + + + + + +
提示:建议先用快捷筛选「待生效」(已支付+待处理)锁定范围,再执行批量生效。
+ +
+ +
+ @csrf + + + + +
+
@csrf diff --git a/routes/web.php b/routes/web.php index 578a5de..3312d5f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -104,6 +104,7 @@ Route::prefix('admin')->group(function () { Route::get('/platform-orders/create', [PlatformOrderController::class, 'create']); Route::post('/platform-orders', [PlatformOrderController::class, 'store']); Route::post('/platform-orders/batch-activate-subscriptions', [PlatformOrderController::class, 'batchActivateSubscriptions']); + Route::post('/platform-orders/batch-mark-activated', [PlatformOrderController::class, 'batchMarkActivated']); Route::post('/platform-orders/clear-sync-errors', [PlatformOrderController::class, 'clearSyncErrors']); Route::get('/platform-orders/{order}', [PlatformOrderController::class, 'show']); Route::post('/platform-orders/{order}/activate-subscription', [PlatformOrderController::class, 'activateSubscription']); diff --git a/tests/Feature/AdminPlatformOrderBatchMarkActivatedTest.php b/tests/Feature/AdminPlatformOrderBatchMarkActivatedTest.php new file mode 100644 index 0000000..35893ce --- /dev/null +++ b/tests/Feature/AdminPlatformOrderBatchMarkActivatedTest.php @@ -0,0 +1,139 @@ +seed(); + + $this->post('/admin/login', [ + 'email' => 'platform.admin@demo.local', + 'password' => 'Platform@123456', + ])->assertRedirect('/admin'); + } + + public function test_platform_admin_can_batch_mark_activated_for_paid_pending_orders_in_filtered_scope(): void + { + $this->loginAsPlatformAdmin(); + + $merchant = Merchant::query()->firstOrFail(); + $plan = Plan::query()->create([ + 'code' => 'batch_mark_activated_plan', + 'name' => '批量生效测试套餐', + 'billing_cycle' => 'monthly', + 'price' => 10, + 'list_price' => 10, + 'status' => 'active', + 'sort' => 10, + 'published_at' => now(), + ]); + + $o1 = PlatformOrder::query()->create([ + 'merchant_id' => $merchant->id, + 'plan_id' => $plan->id, + 'order_no' => 'PO_BATCH_MARK_0001', + 'order_type' => 'renewal', + 'status' => 'pending', + '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), + 'meta' => [], + ]); + + $o2 = PlatformOrder::query()->create([ + 'merchant_id' => $merchant->id, + 'plan_id' => $plan->id, + 'order_no' => 'PO_BATCH_MARK_0002', + 'order_type' => 'renewal', + 'status' => 'pending', + '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(9), + 'paid_at' => now()->subMinutes(4), + 'meta' => [], + ]); + + // 不应被处理:未支付 + $o3 = PlatformOrder::query()->create([ + 'merchant_id' => $merchant->id, + 'plan_id' => $plan->id, + 'order_no' => 'PO_BATCH_MARK_0003', + 'order_type' => 'renewal', + 'status' => 'pending', + 'payment_status' => 'unpaid', + 'plan_name' => $plan->name, + 'billing_cycle' => $plan->billing_cycle, + 'period_months' => 1, + 'quantity' => 1, + 'payable_amount' => 10, + 'paid_amount' => 0, + 'placed_at' => now()->subMinutes(8), + 'meta' => [], + ]); + + // scope=filtered 且要求 payment_status=paid + status=pending + $this->post('/admin/platform-orders/batch-mark-activated', [ + 'scope' => 'filtered', + 'payment_status' => 'paid', + 'status' => 'pending', + 'limit' => 50, + ])->assertRedirect(); + + $o1->refresh(); + $o2->refresh(); + $o3->refresh(); + + $this->assertSame('activated', $o1->status); + $this->assertSame('activated', $o2->status); + $this->assertSame('pending', $o3->status); + + $audit1 = (array) (data_get($o1->meta, 'audit', []) ?? []); + $this->assertNotEmpty($audit1); + $this->assertSame('batch_mark_activated', (string) data_get(end($audit1), 'action')); + } + + public function test_batch_mark_activated_requires_paid_pending_filters_in_filtered_scope(): void + { + $this->loginAsPlatformAdmin(); + + $this->post('/admin/platform-orders/batch-mark-activated', [ + 'scope' => 'filtered', + 'payment_status' => 'paid', + 'status' => 'activated', + 'limit' => 50, + ])->assertRedirect(); + } + + public function test_guest_cannot_batch_mark_activated(): void + { + $this->seed(); + + $this->post('/admin/platform-orders/batch-mark-activated', [ + 'scope' => 'filtered', + 'payment_status' => 'paid', + 'status' => 'pending', + 'limit' => 50, + ])->assertRedirect('/admin/login'); + } +}