platform orders: add clear bmpa errors tool and audit

This commit is contained in:
萝卜
2026-03-13 13:13:04 +00:00
parent fb32db6fb6
commit f760d920f3
4 changed files with 213 additions and 4 deletions

View File

@@ -991,10 +991,12 @@ class PlatformOrderController extends Controller
'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', ''),
'bmpa_failed_only' => (string) $request->input('bmpa_failed_only', ''),
'synced_only' => (string) $request->input('synced_only', ''),
'sync_status' => trim((string) $request->input('sync_status', '')),
'keyword' => trim((string) $request->input('keyword', '')),
'sync_error_keyword' => trim((string) $request->input('sync_error_keyword', '')),
'bmpa_error_keyword' => trim((string) $request->input('bmpa_error_keyword', '')),
'syncable_only' => (string) $request->input('syncable_only', ''),
'batch_synced_24h' => (string) $request->input('batch_synced_24h', ''),
// 与列表页筛选保持一致(可治理):用于在批量操作后仍能回到同一口径
@@ -1151,10 +1153,12 @@ class PlatformOrderController extends Controller
'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', ''),
'bmpa_failed_only' => (string) $request->input('bmpa_failed_only', ''),
'synced_only' => (string) $request->input('synced_only', ''),
'sync_status' => trim((string) $request->input('sync_status', '')),
'keyword' => trim((string) $request->input('keyword', '')),
'sync_error_keyword' => trim((string) $request->input('sync_error_keyword', '')),
'bmpa_error_keyword' => trim((string) $request->input('bmpa_error_keyword', '')),
'syncable_only' => (string) $request->input('syncable_only', ''),
'batch_synced_24h' => (string) $request->input('batch_synced_24h', ''),
'batch_mark_activated_24h' => (string) $request->input('batch_mark_activated_24h', ''),
@@ -1348,10 +1352,12 @@ class PlatformOrderController extends Controller
'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', ''),
'bmpa_failed_only' => (string) $request->input('bmpa_failed_only', ''),
'synced_only' => (string) $request->input('synced_only', ''),
'sync_status' => trim((string) $request->input('sync_status', '')),
'keyword' => trim((string) $request->input('keyword', '')),
'sync_error_keyword' => trim((string) $request->input('sync_error_keyword', '')),
'bmpa_error_keyword' => trim((string) $request->input('bmpa_error_keyword', '')),
'syncable_only' => (string) $request->input('syncable_only', ''),
'batch_synced_24h' => (string) $request->input('batch_synced_24h', ''),
// 与列表页筛选保持一致(可治理):用于在批量操作后仍能回到同一口径
@@ -1450,7 +1456,7 @@ class PlatformOrderController extends Controller
}
public function clearSyncErrors(Request $request): RedirectResponse
{
{
$this->ensurePlatformAdmin($request);
// 支持两种模式:
@@ -1470,10 +1476,12 @@ class PlatformOrderController extends Controller
'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', ''),
'bmpa_failed_only' => (string) $request->input('bmpa_failed_only', ''),
'synced_only' => (string) $request->input('synced_only', ''),
'sync_status' => trim((string) $request->input('sync_status', '')),
'keyword' => trim((string) $request->input('keyword', '')),
'sync_error_keyword' => trim((string) $request->input('sync_error_keyword', '')),
'bmpa_error_keyword' => trim((string) $request->input('bmpa_error_keyword', '')),
'syncable_only' => (string) $request->input('syncable_only', ''),
'batch_synced_24h' => (string) $request->input('batch_synced_24h', ''),
// 与列表页筛选保持一致(可治理):用于在批量操作后仍能回到同一口径
@@ -1525,6 +1533,83 @@ class PlatformOrderController extends Controller
return redirect()->back()->with('success', $msg . $cleared . ' 条(命中 ' . $matched . ' 条)');
}
public function clearBmpaErrors(Request $request): RedirectResponse
{
$this->ensurePlatformAdmin($request);
// 支持两种模式:
// - scope=all默认清理所有订单的 BMPA 失败标记(需要 confirm=YES
// - scope=filtered仅清理当前筛选结果命中的订单更安全
$scope = (string) $request->input('scope', 'all');
// 防误操作scope=all 需要二次确认
if ($scope === 'all' && (string) $request->input('confirm', '') !== 'YES') {
return redirect()->back()->with('warning', '为避免误操作,清除全部 BMPA 失败标记前请在确认框输入 YES。');
}
$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', ''),
'bmpa_failed_only' => (string) $request->input('bmpa_failed_only', ''),
'synced_only' => (string) $request->input('synced_only', ''),
'sync_status' => trim((string) $request->input('sync_status', '')),
'keyword' => trim((string) $request->input('keyword', '')),
'sync_error_keyword' => trim((string) $request->input('sync_error_keyword', '')),
'bmpa_error_keyword' => trim((string) $request->input('bmpa_error_keyword', '')),
'syncable_only' => (string) $request->input('syncable_only', ''),
'batch_synced_24h' => (string) $request->input('batch_synced_24h', ''),
'batch_mark_activated_24h' => (string) $request->input('batch_mark_activated_24h', ''),
'reconcile_mismatch' => (string) $request->input('reconcile_mismatch', ''),
'receipt_status' => trim((string) $request->input('receipt_status', '')),
'refund_status' => trim((string) $request->input('refund_status', '')),
'refund_inconsistent' => (string) $request->input('refund_inconsistent', ''),
];
$query = PlatformOrder::query()
->whereRaw("JSON_EXTRACT(meta, '$.batch_mark_paid_and_activate_error.message') IS NOT NULL");
if ($scope === 'filtered') {
$query = $this->applyFilters($query, $filters);
}
$orders = $query->get(['id', 'meta']);
$matched = $orders->count();
$cleared = 0;
foreach ($orders as $order) {
$meta = (array) ($order->meta ?? []);
if (! data_get($meta, 'batch_mark_paid_and_activate_error')) {
continue;
}
data_forget($meta, 'batch_mark_paid_and_activate_error');
// 轻量审计:记录清理动作(不做独立表,先落 meta便于排查
$audit = (array) (data_get($meta, 'audit', []) ?? []);
$audit[] = [
'action' => 'clear_bmpa_error',
'scope' => $scope,
'at' => now()->toDateTimeString(),
'admin_id' => $this->platformAdminId($request),
];
data_set($meta, 'audit', $audit);
$order->meta = $meta;
$order->save();
$cleared++;
}
$msg = $scope === 'filtered'
? '已清除当前筛选范围内的 BMPA 失败标记:'
: '已清除全部订单的 BMPA 失败标记:';
return redirect()->back()->with('success', $msg . $cleared . ' 条(命中 ' . $matched . ' 条)');
}
protected function applyFilters(Builder $query, array $filters): Builder
{
return $query

View File

@@ -603,10 +603,10 @@
<input type="hidden" name="bmpa_error_keyword" value="{{ $filters['bmpa_error_keyword'] ?? '' }}">
<input type="hidden" name="reconcile_mismatch" value="{{ $filters['reconcile_mismatch'] ?? '' }}">
<input type="hidden" name="refund_inconsistent" value="{{ $filters['refund_inconsistent'] ?? '' }}">
<button type="submit">清除当前筛选范围的失败标记</button>
<button type="submit">清除同步失败标记(当前筛选范围</button>
</form>
<form method="post" action="/admin/platform-orders/clear-sync-errors" onsubmit="return confirm('确认清除全部订单的“同步失败”标记?该操作不可逆(仅清理 meta 标记),请谨慎。');">
<form method="post" action="/admin/platform-orders/clear-sync-errors" onsubmit="return confirm('确认清除全部订单的“同步失败”标记?该操作不可逆(仅清理 meta 标记),请谨慎。');" class="mb-10">
@csrf
<input type="hidden" name="scope" value="all">
@@ -616,7 +616,45 @@
<span>(必须输入 YES 才会执行)</span>
</label>
<button type="submit">清除全部失败标记</button>
<button type="submit">清除同步失败标记(全部订单)</button>
</form>
<form method="post" action="/admin/platform-orders/clear-bmpa-errors" onsubmit="return confirm('确认清除当前筛选范围内命中的订单的“批量标记支付失败”标记?');" class="mb-10">
@csrf
<input type="hidden" name="scope" value="filtered">
<input type="hidden" name="status" value="{{ $filters['status'] ?? '' }}">
<input type="hidden" name="payment_status" value="{{ $filters['payment_status'] ?? '' }}">
<input type="hidden" name="merchant_id" value="{{ $filters['merchant_id'] ?? '' }}">
<input type="hidden" name="plan_id" value="{{ $filters['plan_id'] ?? '' }}">
<input type="hidden" name="site_subscription_id" value="{{ $filters['site_subscription_id'] ?? '' }}">
<input type="hidden" name="fail_only" value="{{ $filters['fail_only'] ?? '' }}">
<input type="hidden" name="bmpa_failed_only" value="{{ $filters['bmpa_failed_only'] ?? '' }}">
<input type="hidden" name="synced_only" value="{{ $filters['synced_only'] ?? '' }}">
<input type="hidden" name="sync_status" value="{{ $filters['sync_status'] ?? '' }}">
<input type="hidden" name="receipt_status" value="{{ $filters['receipt_status'] ?? '' }}">
<input type="hidden" name="refund_status" value="{{ $filters['refund_status'] ?? '' }}">
<input type="hidden" name="syncable_only" value="{{ $filters['syncable_only'] ?? '' }}">
<input type="hidden" name="batch_synced_24h" value="{{ $filters['batch_synced_24h'] ?? '' }}">
<input type="hidden" name="batch_mark_activated_24h" value="{{ $filters['batch_mark_activated_24h'] ?? '' }}">
<input type="hidden" name="keyword" value="{{ $filters['keyword'] ?? '' }}">
<input type="hidden" name="sync_error_keyword" value="{{ $filters['sync_error_keyword'] ?? '' }}">
<input type="hidden" name="bmpa_error_keyword" value="{{ $filters['bmpa_error_keyword'] ?? '' }}">
<input type="hidden" name="reconcile_mismatch" value="{{ $filters['reconcile_mismatch'] ?? '' }}">
<input type="hidden" name="refund_inconsistent" value="{{ $filters['refund_inconsistent'] ?? '' }}">
<button type="submit">清除批量标记支付失败标记(当前筛选范围)</button>
</form>
<form method="post" action="/admin/platform-orders/clear-bmpa-errors" onsubmit="return confirm('确认清除全部订单的“批量标记支付失败”标记?该操作不可逆(仅清理 meta 标记),请谨慎。');">
@csrf
<input type="hidden" name="scope" value="all">
<label class="muted form-inline-row mb-8">
<span>确认输入</span>
<input type="text" name="confirm" placeholder="YES" class="w-140">
<span>(必须输入 YES 才会执行)</span>
</label>
<button type="submit">清除批量标记支付失败标记(全部订单)</button>
</form>
</div>

View File

@@ -107,6 +107,7 @@ Route::prefix('admin')->group(function () {
Route::post('/platform-orders/batch-mark-paid-and-activate', [PlatformOrderController::class, 'batchMarkPaidAndActivate']);
Route::post('/platform-orders/batch-mark-activated', [PlatformOrderController::class, 'batchMarkActivated']);
Route::post('/platform-orders/clear-sync-errors', [PlatformOrderController::class, 'clearSyncErrors']);
Route::post('/platform-orders/clear-bmpa-errors', [PlatformOrderController::class, 'clearBmpaErrors']);
Route::get('/platform-orders/{order}', [PlatformOrderController::class, 'show']);
Route::post('/platform-orders/{order}/activate-subscription', [PlatformOrderController::class, 'activateSubscription']);
Route::post('/platform-orders/{order}/mark-paid-and-activate', [PlatformOrderController::class, 'markPaidAndActivate']);

View File

@@ -0,0 +1,85 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformOrderClearBmpaErrorsTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_admin_can_clear_bmpa_errors(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'clear_bmpa_err_test',
'name' => '清理 BMPA 失败标记测试',
'billing_cycle' => 'monthly',
'price' => 1,
'list_price' => 1,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$order = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_CLEAR_BMPA_ERR_0001',
'order_type' => 'new_purchase',
'status' => 'pending',
'payment_status' => 'unpaid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 1,
'paid_amount' => 0,
'placed_at' => now(),
'meta' => [
'batch_mark_paid_and_activate_error' => [
'message' => '模拟 BMPA 失败',
'at' => now()->toDateTimeString(),
'admin_id' => 1,
],
],
]);
// scope=all 需要二次确认
$this->post('/admin/platform-orders/clear-bmpa-errors', ['scope' => 'all'])
->assertRedirect();
$order->refresh();
$this->assertNotEmpty(data_get($order->meta, 'batch_mark_paid_and_activate_error.message'));
$this->post('/admin/platform-orders/clear-bmpa-errors', ['scope' => 'all', 'confirm' => 'YES'])
->assertRedirect();
$order->refresh();
$this->assertEmpty(data_get($order->meta, 'batch_mark_paid_and_activate_error'));
}
public function test_guest_cannot_clear_bmpa_errors(): void
{
$this->seed();
$this->post('/admin/platform-orders/clear-bmpa-errors', ['scope' => 'all'])
->assertRedirect('/admin/login');
}
}