Files
saasshop/app/Http/Controllers/Admin/PlatformBatchController.php
2026-03-18 09:22:28 +08:00

236 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Concerns\ResolvesPlatformAdminContext;
use App\Http\Controllers\Controller;
use App\Models\PlatformOrder;
use App\Support\BackUrl;
use Illuminate\Http\Request;
use Illuminate\View\View;
class PlatformBatchController extends Controller
{
use ResolvesPlatformAdminContext;
public function show(Request $request): View
{
$this->ensurePlatformAdmin($request);
$type = trim((string) $request->query('type', ''));
$runId = trim((string) $request->query('run_id', ''));
$type = $type === 'bmpa' ? 'bmpa' : ($type === 'bas' ? 'bas' : '');
$incomingBack = (string) $request->query('back', '');
$safeBackForLinks = BackUrl::sanitizeForLinks($incomingBack);
// 抽样复核:换一单(用 id 游标避免随机导致测试不稳定)
$spotAfterId = (int) $request->query('spot_after_id', 0);
if ($type === '' || $runId === '') {
return view('admin.platform_batches.show', [
'type' => $type,
'runId' => $runId,
'safeBackForLinks' => $safeBackForLinks,
'error' => '参数不完整:请提供 typebas/bmpa与 run_id。',
'summary' => null,
'fallbackCounts' => null,
'governanceLinks' => [],
'spotCheck' => ['order_id' => 0, 'url' => '', 'label' => ''],
]);
}
// 基于订单 meta 的扁平字段回捞 last_result冗余写入策略下可行
// 注意:为避免全表扫描,这里先按 run_id 过滤再取最近一条。
$keyPrefix = $type === 'bas' ? '$.batch_activation' : '$.batch_mark_paid_and_activate';
$query = PlatformOrder::query();
$driver = $query->getQuery()->getConnection()->getDriverName();
if ($driver === 'sqlite') {
$query->whereRaw("JSON_EXTRACT(meta, '{$keyPrefix}.run_id') = ?", [$runId]);
} else {
$query->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(meta, '{$keyPrefix}.run_id')) = ?", [$runId]);
}
$order = $query->orderByDesc('id')->first(['id', 'meta']);
$summary = null;
if ($order) {
$summary = data_get($order->meta, $type === 'bas'
? 'batch_activation.last_result'
: 'batch_mark_paid_and_activate.last_result');
}
// 兜底:若 last_result 未写入(例如批量 job 尚未跑完,或历史数据未补齐),
// 则基于同批次订单粗略统计matched/processed/failed失败口径BAS=subscription_activation_errorBMPA=batch_mark_paid_and_activate_error
// 注意:这里尽量不做重计算 success因为“成功”的定义可能随业务变动仅用于 UI 提示。
$fallbackCounts = null;
if ($summary === null) {
$baseQuery = PlatformOrder::query();
$driver2 = $baseQuery->getQuery()->getConnection()->getDriverName();
if ($driver2 === 'sqlite') {
$baseQuery->whereRaw("JSON_EXTRACT(meta, '{$keyPrefix}.run_id') = ?", [$runId]);
} else {
$baseQuery->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(meta, '{$keyPrefix}.run_id')) = ?", [$runId]);
}
$matched = (int) (clone $baseQuery)->count();
$failed = 0;
if ($type === 'bas') {
$failed = (int) (clone $baseQuery)
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') IS NOT NULL")
->count();
}
if ($type === 'bmpa') {
$failed = (int) (clone $baseQuery)
->whereRaw("JSON_EXTRACT(meta, '$.batch_mark_paid_and_activate_error.message') IS NOT NULL")
->count();
}
$fallbackCounts = [
'matched' => $matched,
'failed' => $failed,
];
}
// 治理入口:全部/成功/失败/按Top原因/可重试
$governanceLinks = [];
if ($type === 'bas') {
$governanceLinks['all'] = BackUrl::withBackAndFragment('/admin/platform-orders?' . \Illuminate\Support\Arr::query([
'batch_activation_run_id' => $runId,
]), $safeBackForLinks, 'filters');
$governanceLinks['success'] = BackUrl::withBackAndFragment('/admin/platform-orders?' . \Illuminate\Support\Arr::query([
'batch_activation_run_id' => $runId,
'sync_status' => 'synced',
]), $safeBackForLinks, 'filters');
$governanceLinks['failed'] = BackUrl::withBackAndFragment('/admin/platform-orders?' . \Illuminate\Support\Arr::query([
'batch_activation_run_id' => $runId,
'sync_status' => 'failed',
]), $safeBackForLinks, 'filters');
$governanceLinks['retry_syncable'] = BackUrl::withBackAndFragment('/admin/platform-orders?' . \Illuminate\Support\Arr::query([
'batch_activation_run_id' => $runId,
'sync_status' => 'unsynced',
'syncable_only' => '1',
]), $safeBackForLinks, 'filters');
$topReason = (string) (data_get($summary, 'top_reasons.0.reason') ?? '');
$maxLen = (int) config('saasshop.platform_orders.sync_error_keyword_link_max_len', 200);
$maxLen = max(50, min(1000, $maxLen));
if ($topReason !== '' && mb_strlen($topReason) <= $maxLen) {
$governanceLinks['top_reason'] = BackUrl::withBackAndFragment('/admin/platform-orders?' . \Illuminate\Support\Arr::query([
'batch_activation_run_id' => $runId,
'sync_status' => 'failed',
'sync_error_keyword' => $topReason,
]), $safeBackForLinks, 'filters');
}
}
if ($type === 'bmpa') {
$governanceLinks['all'] = BackUrl::withBackAndFragment('/admin/platform-orders?' . \Illuminate\Support\Arr::query([
'batch_bmpa_run_id' => $runId,
]), $safeBackForLinks, 'filters');
$governanceLinks['success'] = BackUrl::withBackAndFragment('/admin/platform-orders?' . \Illuminate\Support\Arr::query([
'batch_bmpa_run_id' => $runId,
'bmpa_success_only' => '1',
]), $safeBackForLinks, 'filters');
$governanceLinks['failed'] = BackUrl::withBackAndFragment('/admin/platform-orders?' . \Illuminate\Support\Arr::query([
'batch_bmpa_run_id' => $runId,
'bmpa_failed_only' => '1',
]), $safeBackForLinks, 'filters');
// 本批次可同步重试:用于处理“批量 BMPA 后仍未同步订阅”的订单(例如同步暂时失败/后续补救)。
// 口径batch_bmpa_run_id + syncable_only=1 + sync_status=unsynced
$governanceLinks['retry_syncable'] = BackUrl::withBackAndFragment('/admin/platform-orders?' . \Illuminate\Support\Arr::query([
'batch_bmpa_run_id' => $runId,
'syncable_only' => '1',
'sync_status' => 'unsynced',
]), $safeBackForLinks, 'filters');
// 本批次可再次尝试:对齐仪表盘/列表的“真正可BMPA处理集合”口径。
$governanceLinks['retry_processable'] = BackUrl::withBackAndFragment('/admin/platform-orders?' . \Illuminate\Support\Arr::query([
'batch_bmpa_run_id' => $runId,
'bmpa_processable_only' => '1',
]), $safeBackForLinks, 'filters');
$topReason = (string) (data_get($summary, 'top_reasons.0.reason') ?? '');
$maxLen = (int) config('saasshop.platform_orders.sync_error_keyword_link_max_len', 200);
$maxLen = max(50, min(1000, $maxLen));
if ($topReason !== '' && mb_strlen($topReason) <= $maxLen) {
$governanceLinks['top_reason'] = BackUrl::withBackAndFragment('/admin/platform-orders?' . \Illuminate\Support\Arr::query([
'batch_bmpa_run_id' => $runId,
'bmpa_failed_only' => '1',
'bmpa_error_keyword' => $topReason,
]), $safeBackForLinks, 'filters');
}
}
// 抽样复核入口:从“成功集合”里取一单,方便运营 spot-check。
// - BAS优先取已同步且无错误的订单
// - BMPA优先取本批次标记支付成功且无错误的订单
$spotCheck = [
'order_id' => 0,
'url' => '',
'label' => '',
];
$selfWithoutBack = BackUrl::selfWithoutBack();
$sampleQuery = PlatformOrder::query();
$driver3 = $sampleQuery->getQuery()->getConnection()->getDriverName();
if ($driver3 === 'sqlite') {
$sampleQuery->whereRaw("JSON_EXTRACT(meta, '{$keyPrefix}.run_id') = ?", [$runId]);
} else {
$sampleQuery->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(meta, '{$keyPrefix}.run_id')) = ?", [$runId]);
}
if ($spotAfterId > 0) {
$sampleQuery->where('id', '<', $spotAfterId);
}
if ($type === 'bas') {
$sampleQuery->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation.subscription_id') IS NOT NULL")
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') IS NULL");
$sampleOrder = $sampleQuery->orderByDesc('id')->first(['id']);
if ($sampleOrder) {
$spotCheck['order_id'] = (int) $sampleOrder->id;
$spotCheck['label'] = '抽样复核:查看订阅同步';
$spotCheck['url'] = BackUrl::withBackAndFragment('/admin/platform-orders/' . $sampleOrder->id, $selfWithoutBack, 'subscription-sync');
}
}
if ($type === 'bmpa') {
$sampleQuery->whereRaw("JSON_EXTRACT(meta, '$.batch_mark_paid_and_activate.run_id') IS NOT NULL")
->whereRaw("JSON_EXTRACT(meta, '$.batch_mark_paid_and_activate_error.message') IS NULL");
$sampleOrder = $sampleQuery->orderByDesc('id')->first(['id']);
if ($sampleOrder) {
$spotCheck['order_id'] = (int) $sampleOrder->id;
$spotCheck['label'] = '抽样复核:查看支付回执';
$spotCheck['url'] = BackUrl::withBackAndFragment('/admin/platform-orders/' . $sampleOrder->id, $selfWithoutBack, 'payment-receipts');
}
}
return view('admin.platform_batches.show', [
'type' => $type,
'runId' => $runId,
'safeBackForLinks' => $safeBackForLinks,
'error' => '',
'summary' => $summary,
'fallbackCounts' => $fallbackCounts,
'governanceLinks' => $governanceLinks,
'spotCheck' => $spotCheck,
]);
}
}