platform orders: add clear bmpa errors tool and audit
This commit is contained in:
@@ -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', ''),
|
||||
// 与列表页筛选保持一致(可治理):用于在批量操作后仍能回到同一口径
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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']);
|
||||
|
||||
85
tests/Feature/AdminPlatformOrderClearBmpaErrorsTest.php
Normal file
85
tests/Feature/AdminPlatformOrderClearBmpaErrorsTest.php
Normal 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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user