diff --git a/app/Http/Controllers/Admin/PlatformOrderController.php b/app/Http/Controllers/Admin/PlatformOrderController.php index cd58832..15c7d34 100644 --- a/app/Http/Controllers/Admin/PlatformOrderController.php +++ b/app/Http/Controllers/Admin/PlatformOrderController.php @@ -282,6 +282,12 @@ class PlatformOrderController extends Controller // 创建时间范围(用于“趋势→集合”跳转与运营筛选) 'created_from' => trim((string) $request->query('created_from', '')), 'created_to' => trim((string) $request->query('created_to', '')), + + // 真正可 BMPA 处理集合(与仪表盘「可BMPA处理」口径对齐): + // - pending + unpaid + // - 排除:续费缺订阅(order_type=renewal 且 site_subscription_id 为空) + // - 排除:存在退款轨迹(refund_total > 0) + 'bmpa_processable_only' => (string) $request->query('bmpa_processable_only', ''), ]; @@ -1342,6 +1348,12 @@ class PlatformOrderController extends Controller // 创建时间范围(用于“趋势→集合”跳转与运营筛选) 'created_from' => trim((string) $request->query('created_from', '')), 'created_to' => trim((string) $request->query('created_to', '')), + + // 真正可 BMPA 处理集合(与仪表盘「可BMPA处理」口径对齐): + // - pending + unpaid + // - 排除:续费缺订阅(order_type=renewal 且 site_subscription_id 为空) + // - 排除:存在退款轨迹(refund_total > 0) + 'bmpa_processable_only' => (string) $request->query('bmpa_processable_only', ''), ]; @@ -2605,6 +2617,28 @@ class PlatformOrderController extends Controller } elseif ($to !== '') { $builder->where('created_at', '<=', $to . ' 23:59:59'); } + }) + ->when(($filters['bmpa_processable_only'] ?? '') === '1', function (Builder $builder) { + // 真正可 BMPA 处理集合(与仪表盘统计口径对齐): + // - pending + unpaid + // - 排除:续费缺订阅(renewal 且 site_subscription_id 为空) + // - 排除:存在退款轨迹(refund_total > 0) + $builder->where('status', 'pending') + ->where('payment_status', 'unpaid') + ->where(function (Builder $q) { + $q->where('order_type', '!=', 'renewal') + ->orWhereNotNull('site_subscription_id'); + }); + + $driver = $builder->getQuery()->getConnection()->getDriverName(); + + if ($driver === 'sqlite') { + $refundTotalExpr = "(CASE WHEN JSON_EXTRACT(meta, '$.refund_summary.total_amount') IS NOT NULL THEN CAST(JSON_EXTRACT(meta, '$.refund_summary.total_amount') AS REAL) ELSE (SELECT IFNULL(SUM(CAST(JSON_EXTRACT(value, '$.amount') AS REAL)), 0) FROM json_each(COALESCE(JSON_EXTRACT(meta, '$.refund_receipts'), '[]'))) END)"; + $builder->whereRaw("ROUND(($refundTotalExpr) * 100) <= 0"); + } else { + $refundTotalExpr = "(CASE WHEN JSON_EXTRACT(meta, '$.refund_summary.total_amount') IS NOT NULL THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(meta, '$.refund_summary.total_amount')) AS DECIMAL(12,2)) ELSE (SELECT IFNULL(SUM(j.amount), 0) FROM JSON_TABLE(meta, '$.refund_receipts[*]' COLUMNS(amount DECIMAL(12,2) PATH '$.amount')) j) END)"; + $builder->whereRaw("ROUND(($refundTotalExpr) * 100) <= 0"); + } }); } diff --git a/resources/views/admin/components/filters_hidden_inputs.blade.php b/resources/views/admin/components/filters_hidden_inputs.blade.php index c5f1665..75c5087 100644 --- a/resources/views/admin/components/filters_hidden_inputs.blade.php +++ b/resources/views/admin/components/filters_hidden_inputs.blade.php @@ -24,6 +24,7 @@ 'refund_status', 'syncable_only', 'renewal_missing_subscription', + 'bmpa_processable_only', 'batch_synced_24h', 'batch_activation_run_id', 'batch_bmpa_run_id', diff --git a/resources/views/admin/dashboard.blade.php b/resources/views/admin/dashboard.blade.php index 4673b59..541de6b 100644 --- a/resources/views/admin/dashboard.blade.php +++ b/resources/views/admin/dashboard.blade.php @@ -59,8 +59,10 @@ ); }, - // 平台订单(收费闭环)工作台入口:尽量保持与列表页筛选语义一致。 - 'unpaid_pending' => \App\Support\BackUrl::withBack('/admin/platform-orders?payment_status=unpaid&status=pending', $selfWithoutBack), + // 平台订单(收费闭环)工作台入口: + // 注意:仪表盘「可BMPA处理」统计口径已升级(排除退款轨迹/续费缺订阅)。 + // 因此这里不再用简单 pending+unpaid,而是用 bmpa_processable_only=1 统一表达“真正可 BMPA 处理集合”。 + 'unpaid_pending' => \App\Support\BackUrl::withBack('/admin/platform-orders?bmpa_processable_only=1', $selfWithoutBack), // 待生效:paid + pending,并显式锁定 sync_status=unsynced(排除同步失败等异常单) 'paid_pending' => \App\Support\BackUrl::withBack('/admin/platform-orders?payment_status=paid&status=pending&sync_status=unsynced', $selfWithoutBack), // 可同步(工作台口径):只看可同步 + 未同步(排除同步失败等异常单),与工作台统计口径一致。 diff --git a/resources/views/admin/platform_orders/index.blade.php b/resources/views/admin/platform_orders/index.blade.php index 1d07796..efe8dc7 100644 --- a/resources/views/admin/platform_orders/index.blade.php +++ b/resources/views/admin/platform_orders/index.blade.php @@ -308,10 +308,10 @@ @php // 快捷筛选:尽量保留当前筛选上下文(站点/套餐/订阅ID/keyword/lead_id/back/时间范围等),仅覆盖目标筛选字段,并清空 page。 - // 注意:不保留 syncable_only/fail_only 等“工具型开关”,避免用户从一个集合切到另一个集合时被残留开关影响(导致误判/空结果)。 + // 注意:不保留 syncable_only/fail_only/bmpa_processable_only 等“工具型开关”,避免用户从一个集合切到另一个集合时被残留开关影响(导致误判/空结果)。 $buildQuickFilterUrl = function (array $overrides) use ($safeBackForLinks) { // 快捷筛选:仅保留上下文字段(站点/套餐/订阅ID/keyword/lead_id/时间范围/安全 back),避免把其它筛选条件叠加导致空结果。 - // 该构造器内部会强制清空 page,并且不会继承 syncable_only/fail_only 等“工具型开关”。 + // 该构造器内部会强制清空 page,并且不会继承 syncable_only/fail_only/bmpa_processable_only 等“工具型开关”。 return \App\Support\BackUrl::currentPathQuickFilter( ['merchant_id', 'plan_id', 'site_subscription_id', 'keyword', 'lead_id', 'created_from', 'created_to'], $overrides, @@ -327,7 +327,7 @@