Align dashboard BMPA quick link with bmpa_processable_only filter

This commit is contained in:
萝卜
2026-03-18 02:19:25 +08:00
parent 6e59ae3eb6
commit 3ff16b3d28
7 changed files with 50 additions and 14 deletions

View File

@@ -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");
}
});
}

View File

@@ -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',

View File

@@ -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),
// 可同步(工作台口径):只看可同步 + 未同步(排除同步失败等异常单),与工作台统计口径一致。

View File

@@ -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 @@
<div class="inline-links">
<a href="{!! $allUrl !!}" class="muted">全部</a>
<a href="{!! $buildQuickFilterUrl(['payment_status' => 'unpaid']) !!}" class="muted">待支付</a>
<a href="{!! $buildQuickFilterUrl(['status' => 'pending', 'payment_status' => 'unpaid']) !!}" class="muted">可BMPA处理</a>
<a href="{!! $buildQuickFilterUrl(['bmpa_processable_only' => '1']) !!}" class="muted">可BMPA处理</a>
<a href="{!! $buildQuickFilterUrl(['payment_status' => 'paid', 'status' => 'pending', 'sync_status' => 'unsynced']) !!}" class="muted">待生效</a>
<a href="{!! $buildQuickFilterUrl(['syncable_only' => '1', 'sync_status' => 'unsynced']) !!}" class="muted">可同步订阅</a>
<a href="{!! $buildQuickFilterUrl(['sync_status' => 'failed']) !!}" class="muted">同步失败</a>
@@ -818,7 +818,7 @@
<a class="link" title="{{ $reason }}" href="{!! $safeFullUrlWithQuery(['bmpa_error_keyword' => $reason, 'page' => null]) !!}">{{ $reasonText }}</a>
<span class="muted">{{ $count }}</span>
<span class="muted"></span>
<a class="link" href="{!! $safeFullUrlWithQuery(['bmpa_error_keyword' => $reason, 'status' => 'pending', 'payment_status' => 'unpaid', 'page' => null]) !!}">切到可处理集合重试</a>
<a class="link" href="{!! $safeFullUrlWithQuery(['bmpa_error_keyword' => $reason, 'bmpa_processable_only' => '1', 'page' => null]) !!}">切到可处理集合重试</a>
<span class="muted"></span>
<a class="link" href="{!! $safeFullUrlWithQuery(['bmpa_failed_only' => '1', 'bmpa_error_keyword' => $reason, 'page' => null]) !!}">进入失败集合</a>
@endif
@@ -921,10 +921,10 @@
<div class="card governance-block mb-10">
<div class="muted text-danger governance-block-title"><strong>BMPA 失败治理提示</strong></div>
<div class="muted governance-block-body">
当前筛选包含「批量标记支付并生效失败/失败原因」范围。建议先补齐回执/核对退款/修正状态后,再切到 pending+unpaid 集合重试批量标记支付。
当前筛选包含「批量标记支付并生效失败/失败原因」范围。建议先补齐回执/核对退款/修正状态后,再切到「可BMPA处理」集合重试批量标记支付。
<div class="mt-6 actions gap-10">
<a class="btn btn-secondary btn-sm" href="{!! $safeFullUrlWithQuery(['bmpa_failed_only' => '1', 'page' => null]) !!}">进入批量标记支付失败集合</a>
<a class="btn btn-secondary btn-sm" href="{!! $safeFullUrlWithQuery(['status' => 'pending', 'payment_status' => 'unpaid', 'page' => null]) !!}">切到 pending+unpaid(用于重试)</a>
<a class="btn btn-secondary btn-sm" href="{!! $safeFullUrlWithQuery(['bmpa_processable_only' => '1', 'page' => null]) !!}">切到可BMPA处理(用于重试)</a>
</div>
</div>
</div>
@@ -1031,10 +1031,9 @@
<button class="btn btn-sm" type="submit" @disabled($batchBmpaBlocked) title="{{ $batchBmpaBlockedReason }}">批量标记支付并生效(含订阅同步)(当前筛选范围)</button>
@if($batchBmpaBlocked)
@php
// 提效被阻断时给一键跳转到「可BMPA处理集合」口径pending + unpaid)。
// 提效被阻断时给一键跳转到「可BMPA处理集合」与仪表盘口径对齐)。
$goBmpaProcessableUrl = $buildQuickFilterUrl([
'status' => 'pending',
'payment_status' => 'unpaid',
'bmpa_processable_only' => '1',
]);
@endphp
@include('admin.components.tool_blocked_hint', [

View File

@@ -29,7 +29,7 @@ class AdminDashboardBillingWorkbenchQuickLinksShouldUseBackUrlTest extends TestC
$res->assertOk();
// 期望:链接里带 back=%2Fadmin返回仪表盘且 & 不被 escape 成 &amp;
$res->assertSee('href="/admin/platform-orders?payment_status=unpaid&status=pending&back=%2Fadmin"', false);
$res->assertSee('href="/admin/platform-orders?bmpa_processable_only=1&back=%2Fadmin"', false);
$res->assertSee('href="/admin/platform-orders?payment_status=paid&status=pending&sync_status=unsynced&back=%2Fadmin"', false);
$res->assertSee('href="/admin/platform-orders?syncable_only=1&sync_status=unsynced&back=%2Fadmin"', false);
$res->assertSee('href="/admin/platform-orders?sync_status=failed&back=%2Fadmin"', false);

View File

@@ -28,7 +28,7 @@ class AdminDashboardBillingWorkbenchQuickLinksTest extends TestCase
$res->assertSee('快捷筛选');
$res->assertSee('href="/admin/platform-orders?payment_status=unpaid&status=pending&back=%2Fadmin"', false);
$res->assertSee('href="/admin/platform-orders?bmpa_processable_only=1&back=%2Fadmin"', false);
$res->assertSee('待支付');
$res->assertSee('href="/admin/platform-orders?payment_status=paid&status=pending&sync_status=unsynced&back=%2Fadmin"', false);

View File

@@ -32,7 +32,7 @@ class AdminDashboardMiniBarRowsShouldLinkToGovernanceScopesTest extends TestCase
$this->assertStringContainsString('adm-mini-bar-row-link', $html);
// 漏斗:待支付 / 待生效 / 可同步
$this->assertStringContainsString('href="/admin/platform-orders?payment_status=unpaid&status=pending&back=%2Fadmin"', $html);
$this->assertStringContainsString('href="/admin/platform-orders?bmpa_processable_only=1&back=%2Fadmin"', $html);
$this->assertStringContainsString('href="/admin/platform-orders?payment_status=paid&status=pending&sync_status=unsynced&back=%2Fadmin"', $html);
$this->assertStringContainsString('href="/admin/platform-orders?syncable_only=1&sync_status=unsynced&back=%2Fadmin"', $html);