1127 lines
65 KiB
PHP
1127 lines
65 KiB
PHP
@extends('admin.layouts.app')
|
||
|
||
@section('title', '总台仪表盘')
|
||
@section('page_title', '总台仪表盘')
|
||
|
||
@section('content')
|
||
@php
|
||
// 用于构建“从仪表盘跳到其它治理页后可返回仪表盘”的 back
|
||
$selfWithoutBack = \App\Support\BackUrl::selfWithoutBack();
|
||
|
||
// 重要:从仪表盘跳转到治理页时,应默认返回“仪表盘”本身(而不是沿用进入仪表盘时的 back)。
|
||
$billingEntryLinks = [
|
||
'platform_orders' => \App\Support\BackUrl::withBack('/admin/platform-orders', $selfWithoutBack),
|
||
'site_subscriptions' => \App\Support\BackUrl::withBack('/admin/site-subscriptions', $selfWithoutBack),
|
||
'plans' => \App\Support\BackUrl::withBack('/admin/plans', $selfWithoutBack),
|
||
];
|
||
|
||
// 仪表盘内所有“近7天”口径统一从 Controller 注入(避免 Blade 内 now() 计算导致跨天漂移)。
|
||
$rangeFrom7d = (string) ($dashboardRangeFrom7d ?? now()->subDays(6)->format('Y-m-d'));
|
||
$rangeTo7d = (string) ($dashboardRangeTo7d ?? now()->format('Y-m-d'));
|
||
|
||
$platformOrdersQuickLinks = [
|
||
// 复用工作台入口(避免 /admin/platform-orders 入口口径分叉)
|
||
'platform_orders' => $billingEntryLinks['platform_orders'],
|
||
|
||
// 时间范围集合:用于趋势/排行/占比跳转(避免各处散落拼接 query 导致口径漂移)
|
||
'platform_orders_range' => function (string $from, string $to) use ($selfWithoutBack): string {
|
||
return \App\Support\BackUrl::withBack(
|
||
'/admin/platform-orders?' . \Illuminate\Support\Arr::query([
|
||
'created_from' => $from,
|
||
'created_to' => $to,
|
||
]),
|
||
$selfWithoutBack
|
||
);
|
||
},
|
||
|
||
// 站点收入排行:已付订单集合(merchant_id + payment_status=paid + 日期范围)
|
||
'merchant_paid_orders_range' => function (int $merchantId, string $from, string $to) use ($selfWithoutBack): string {
|
||
return \App\Support\BackUrl::withBack(
|
||
'/admin/platform-orders?' . \Illuminate\Support\Arr::query([
|
||
'merchant_id' => $merchantId,
|
||
'payment_status' => 'paid',
|
||
'created_from' => $from,
|
||
'created_to' => $to,
|
||
]),
|
||
$selfWithoutBack
|
||
);
|
||
},
|
||
|
||
// 套餐订单占比:按套餐筛选 + 日期范围(用于 Top5 表格链接,同时被 JS 复用为 mini chart 点击入口)
|
||
'plan_orders_range' => function (int $planId, string $from, string $to) use ($selfWithoutBack): string {
|
||
return \App\Support\BackUrl::withBack(
|
||
'/admin/platform-orders?' . \Illuminate\Support\Arr::query([
|
||
'plan_id' => $planId,
|
||
'created_from' => $from,
|
||
'created_to' => $to,
|
||
]),
|
||
$selfWithoutBack
|
||
);
|
||
},
|
||
|
||
// 平台订单(收费闭环)工作台入口:尽量保持与列表页筛选语义一致。
|
||
'unpaid_pending' => \App\Support\BackUrl::withBack('/admin/platform-orders?payment_status=unpaid&status=pending', $selfWithoutBack),
|
||
// 待生效:paid + pending,并显式锁定 sync_status=unsynced(排除同步失败等异常单)
|
||
'paid_pending' => \App\Support\BackUrl::withBack('/admin/platform-orders?payment_status=paid&status=pending&sync_status=unsynced', $selfWithoutBack),
|
||
// 可同步(工作台口径):只看可同步 + 未同步(排除同步失败等异常单),与工作台统计口径一致。
|
||
'syncable_only' => \App\Support\BackUrl::withBack('/admin/platform-orders?syncable_only=1&sync_status=unsynced', $selfWithoutBack),
|
||
'sync_failed' => \App\Support\BackUrl::withBack('/admin/platform-orders?sync_status=failed', $selfWithoutBack),
|
||
'renewal_missing_subscription' => \App\Support\BackUrl::withBack('/admin/platform-orders?renewal_missing_subscription=1', $selfWithoutBack),
|
||
'bmpa_failed' => \App\Support\BackUrl::withBack('/admin/platform-orders?bmpa_failed_only=1', $selfWithoutBack),
|
||
'paid_no_receipt' => \App\Support\BackUrl::withBack('/admin/platform-orders?payment_status=paid&receipt_status=none', $selfWithoutBack),
|
||
'reconcile_mismatch' => \App\Support\BackUrl::withBack('/admin/platform-orders?reconcile_mismatch=1', $selfWithoutBack),
|
||
'refund_inconsistent' => \App\Support\BackUrl::withBack('/admin/platform-orders?refund_inconsistent=1', $selfWithoutBack),
|
||
];
|
||
|
||
$subscriptionQuickLinks = [
|
||
// 复用工作台入口(避免 /admin/site-subscriptions 入口口径分叉)
|
||
'site_subscriptions' => $billingEntryLinks['site_subscriptions'],
|
||
'expiring_7d' => \App\Support\BackUrl::withBack('/admin/site-subscriptions?expiry=expiring_7d', $selfWithoutBack),
|
||
'expired' => \App\Support\BackUrl::withBack('/admin/site-subscriptions?expiry=expired', $selfWithoutBack),
|
||
];
|
||
@endphp
|
||
|
||
<div data-page="admin.dashboard">
|
||
<div class="page-header mb-20">
|
||
<div class="page-header-main">
|
||
<div>
|
||
<div class="page-header-title">总台仪表盘</div>
|
||
<div class="page-header-subtitle">欢迎回来,{{ $adminName }}。这里是总台管理(平台运营方)的全局总览入口(聚焦:收费闭环与治理动作可达)。</div>
|
||
</div>
|
||
<div class="page-header-actions">
|
||
<a class="btn btn-secondary btn-sm" href="#billing-workbench">跳到收费工作台</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="page-header-meta">
|
||
<div>统计缓存:{{ $cacheMeta['store'] }} / TTL {{ $cacheMeta['ttl'] }}</div>
|
||
<div>当前视角:{{ $platformOverview['current_scope'] }}</div>
|
||
<div>活跃站点:{{ $platformOverview['active_merchants'] }}</div>
|
||
<div>待处理订单:{{ $platformOverview['pending_orders'] }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="kpi-grid mb-20" data-role="kpi-grid">
|
||
@php
|
||
$kpiLinks = [
|
||
'merchants' => \App\Support\BackUrl::withBack('/admin/merchants', $selfWithoutBack),
|
||
'plans' => $billingEntryLinks['plans'],
|
||
'site_subscriptions' => $billingEntryLinks['site_subscriptions'],
|
||
'platform_orders' => $billingEntryLinks['platform_orders'],
|
||
];
|
||
@endphp
|
||
|
||
<div class="card">
|
||
<div class="stat-card-title">站点</div>
|
||
<div class="stat-card-value"><a class="link" href="{!! $kpiLinks['merchants'] !!}">{{ $stats['merchants'] }}</a></div>
|
||
<div class="stat-card-footnote">全站点数量(总台视角)</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="stat-card-title">套餐</div>
|
||
<div class="stat-card-value"><a class="link" href="{!! $kpiLinks['plans'] !!}">{{ (int) ($stats['plans'] ?? 0) }}</a></div>
|
||
<div class="stat-card-footnote">可售套餐目录</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="stat-card-title">订阅</div>
|
||
<div class="stat-card-value"><a class="link" href="{!! $kpiLinks['site_subscriptions'] !!}">{{ (int) ($stats['site_subscriptions'] ?? 0) }}</a></div>
|
||
<div class="stat-card-footnote">站点订阅总量(收费主链)</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="stat-card-title">平台订单</div>
|
||
<div class="stat-card-value"><a class="link" href="{!! $kpiLinks['platform_orders'] !!}">{{ (int) ($stats['platform_orders'] ?? 0) }}</a></div>
|
||
<div class="stat-card-footnote">平台收费订单总量</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="two-col mb-20" data-role="analysis-skeleton" data-eq-col="recent-platform-orders">
|
||
<div class="card" data-role="dashboard-card-trend">
|
||
<h3 class="mt-0">趋势</h3>
|
||
<div class="muted">近7天|平台订单(按天)</div>
|
||
|
||
@php
|
||
$trendRows = (array) ($platformOrderTrend7d ?? []);
|
||
// 用于前端渐进增强渲染迷你图表(JS 读取 data-points)
|
||
$trendPoints = [];
|
||
foreach ($trendRows as $r) {
|
||
$trendPoints[] = [
|
||
'date' => (string) ($r['date'] ?? ''),
|
||
'count' => (int) ($r['count'] ?? 0),
|
||
'paid_sum' => (float) ($r['paid_sum'] ?? 0),
|
||
];
|
||
}
|
||
@endphp
|
||
|
||
<div class="adm-mini-chart" data-role="platform-order-trend-7d-chart" data-points='@json($trendPoints)'></div>
|
||
|
||
@php
|
||
$trendPaidTotal = 0.0;
|
||
$trendOrdersTotal = 0;
|
||
$trendPaidMax = 0.0;
|
||
foreach ($trendRows as $r) {
|
||
$trendPaidTotal += (float) ($r['paid_sum'] ?? 0);
|
||
$trendOrdersTotal += (int) ($r['count'] ?? 0);
|
||
$trendPaidMax = max($trendPaidMax, (float) ($r['paid_sum'] ?? 0));
|
||
}
|
||
@endphp
|
||
<div class="adm-mini-meta" data-role="platform-order-trend-7d-meta">
|
||
<span class="adm-mini-meta-item">7天合计已付:<strong>¥{{ number_format($trendPaidTotal, 2) }}</strong></span>
|
||
<span class="adm-mini-meta-sep">|</span>
|
||
<span class="adm-mini-meta-item">7天订单数:<strong>{{ (int) $trendOrdersTotal }}</strong></span>
|
||
<span class="adm-mini-meta-sep">|</span>
|
||
<span class="adm-mini-meta-item">峰值:<strong>¥{{ number_format($trendPaidMax, 2) }}</strong></span>
|
||
</div>
|
||
|
||
<table class="mt-10" data-role="platform-order-trend-7d">
|
||
<thead>
|
||
<tr>
|
||
<th>日期</th>
|
||
<th>订单数</th>
|
||
<th>已付金额</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@forelse($trendRows as $row)
|
||
<tr>
|
||
@php
|
||
$d = (string) ($row['date'] ?? '');
|
||
$dayOrdersUrl = ($platformOrdersQuickLinks['platform_orders_range'])($d, $d);
|
||
@endphp
|
||
<td><a class="link" href="{!! $dayOrdersUrl !!}">{{ $d }}</a></td>
|
||
<td>{{ (int) ($row['count'] ?? 0) }}</td>
|
||
<td>¥{{ number_format((float) ($row['paid_sum'] ?? 0), 2) }}</td>
|
||
</tr>
|
||
@empty
|
||
<tr>
|
||
<td colspan="3" class="muted">暂无数据</td>
|
||
</tr>
|
||
@endforelse
|
||
</tbody>
|
||
</table>
|
||
|
||
<div class="muted muted-xs mt-10">说明:先接入最小可用趋势数据;后续再补时间范围切换、维度切换与可视化图表。</div>
|
||
</div>
|
||
<div class="card">
|
||
<h3 class="mt-0">排行(近7天站点收入 Top5)</h3>
|
||
@php
|
||
$rankRows = (array) ($merchantRevenueRank7d ?? []);
|
||
$rankTotal = 0.0;
|
||
foreach ($rankRows as $r) {
|
||
$rankTotal += (float) ($r['paid_sum'] ?? 0);
|
||
}
|
||
@endphp
|
||
|
||
<div class="muted">近7天|按已付金额统计</div>
|
||
|
||
@php
|
||
// 用于前端渐进增强渲染迷你排行(JS 读取 data-points)
|
||
$rankPoints = [];
|
||
foreach ($rankRows as $r) {
|
||
$mid = (int) ($r['merchant_id'] ?? 0);
|
||
$rankPoints[] = [
|
||
'merchant_id' => $mid,
|
||
'name' => (string) (($merchantIdToName[$mid] ?? '') ?: ('#' . $mid)),
|
||
'count' => (int) ($r['count'] ?? 0),
|
||
'paid_sum' => (float) ($r['paid_sum'] ?? 0),
|
||
];
|
||
}
|
||
@endphp
|
||
|
||
<div class="adm-mini-rank" data-role="merchant-revenue-rank-7d-chart" data-points='@json($rankPoints)'></div>
|
||
|
||
@php
|
||
$rankPaidMax = 0.0;
|
||
$rankOrdersTotal = 0;
|
||
foreach ($rankRows as $r) {
|
||
$rankPaidMax = max($rankPaidMax, (float) ($r['paid_sum'] ?? 0));
|
||
$rankOrdersTotal += (int) ($r['count'] ?? 0);
|
||
}
|
||
|
||
$rankTotalPaidAll = (float) ($merchantRevenueTotalPaid7d ?? 0);
|
||
if ($rankTotalPaidAll <= 0) {
|
||
$rankTotalPaidAll = (float) $rankTotal;
|
||
}
|
||
|
||
$rankCoveragePct = $rankTotalPaidAll > 0 ? round(((float) $rankTotal / $rankTotalPaidAll) * 100, 1) : 0;
|
||
$rankOtherPaid = max(0.0, $rankTotalPaidAll - (float) $rankTotal);
|
||
$rankOtherPct = max(0.0, round(100 - $rankCoveragePct, 1));
|
||
@endphp
|
||
<div class="adm-mini-meta" data-role="merchant-revenue-rank-7d-meta">
|
||
<span class="adm-mini-meta-item">Top5合计已付:<strong>¥{{ number_format($rankTotal, 2) }}</strong></span>
|
||
<span class="adm-mini-meta-sep">|</span>
|
||
<span class="adm-mini-meta-item">Top5订单数:<strong>{{ (int) $rankOrdersTotal }}</strong></span>
|
||
<span class="adm-mini-meta-sep">|</span>
|
||
<span class="adm-mini-meta-item">覆盖率:<strong>{{ $rankCoveragePct }}%</strong></span>
|
||
<span class="adm-mini-meta-sep">|</span>
|
||
<span class="adm-mini-meta-item">其它:<strong>{{ $rankOtherPct }}%</strong>(¥{{ number_format($rankOtherPaid, 2) }})</span>
|
||
<span class="adm-mini-meta-sep">|</span>
|
||
<span class="adm-mini-meta-item">Top1金额:<strong>¥{{ number_format($rankPaidMax, 2) }}</strong></span>
|
||
</div>
|
||
|
||
<table class="mt-10" data-role="merchant-revenue-rank-7d">
|
||
<thead>
|
||
<tr>
|
||
<th>站点</th>
|
||
<th>订单数</th>
|
||
<th>已付金额</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@forelse($rankRows as $row)
|
||
@php
|
||
$mid = (int) ($row['merchant_id'] ?? 0);
|
||
$mname = (string) (($merchantIdToName[$mid] ?? '') ?: ('#' . $mid));
|
||
$merchantOrdersUrl = ($platformOrdersQuickLinks['merchant_paid_orders_range'])($mid, $rangeFrom7d, $rangeTo7d);
|
||
@endphp
|
||
<tr>
|
||
<td><a class="link" href="{!! $merchantOrdersUrl !!}">{{ $mname }}</a></td>
|
||
<td>{{ (int) ($row['count'] ?? 0) }}</td>
|
||
<td>¥{{ number_format((float) ($row['paid_sum'] ?? 0), 2) }}</td>
|
||
</tr>
|
||
@empty
|
||
<tr>
|
||
<td colspan="3" class="muted">暂无数据</td>
|
||
</tr>
|
||
@endforelse
|
||
</tbody>
|
||
</table>
|
||
|
||
<div class="muted muted-xs mt-10">说明:先落最小可用 Top5 排行;后续补时间范围切换、维度切换与异常排行。</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="two-col mb-20" data-eq-col="recent-platform-orders">
|
||
<div class="card" id="billing-workbench" data-role="dashboard-card-billing-workbench">
|
||
<h3 class="mt-0">收费工作台(快捷治理)</h3>
|
||
<div class="muted">聚焦收费闭环的日常治理入口:订单 → 订阅 → 套餐。</div>
|
||
|
||
<div class="actions mt-10">
|
||
<a class="btn btn-sm" href="{!! $billingEntryLinks['platform_orders'] !!}">平台订单</a>
|
||
<a class="btn btn-secondary btn-sm" href="{!! $billingEntryLinks['site_subscriptions'] !!}">订阅</a>
|
||
<a class="btn btn-secondary btn-sm" href="{!! $billingEntryLinks['plans'] !!}">套餐</a>
|
||
</div>
|
||
|
||
<div class="mt-12">
|
||
<div class="muted">平台订单快捷筛选(只保留“点完能做事”的治理入口):</div>
|
||
<div class="actions mt-8" data-role="dashboard-po-quick-links">
|
||
<a class="btn btn-secondary btn-sm" data-role="dashboard-po-quicklink-bmpa-processable" href="{!! $platformOrdersQuickLinks['unpaid_pending'] !!}">可BMPA处理({{ (int) ($stats['platform_orders_unpaid_pending'] ?? 0) }})</a>
|
||
<a class="btn btn-secondary btn-sm" data-role="dashboard-po-quicklink-syncable" href="{!! $platformOrdersQuickLinks['syncable_only'] !!}">可同步({{ (int) ($stats['platform_orders_syncable'] ?? 0) }})</a>
|
||
<a class="btn btn-secondary btn-sm" data-role="dashboard-po-quicklink-sync-failed" href="{!! $platformOrdersQuickLinks['sync_failed'] !!}">同步失败({{ (int) ($stats['platform_orders_sync_failed'] ?? 0) }})</a>
|
||
<a class="btn btn-secondary btn-sm" data-role="dashboard-po-quicklink-no-receipt" href="{!! $platformOrdersQuickLinks['paid_no_receipt'] !!}">无回执({{ (int) ($stats['platform_orders_paid_no_receipt'] ?? 0) }})</a>
|
||
<a class="btn btn-secondary btn-sm" data-role="dashboard-po-quicklink-reconcile-mismatch" href="{!! $platformOrdersQuickLinks['reconcile_mismatch'] !!}">对账不一致({{ (int) ($stats['platform_orders_reconcile_mismatch'] ?? 0) }})</a>
|
||
<a class="btn btn-secondary btn-sm" data-role="dashboard-po-quicklink-refund-inconsistent" href="{!! $platformOrdersQuickLinks['refund_inconsistent'] !!}">退款不一致({{ (int) ($stats['platform_orders_refund_inconsistent'] ?? 0) }})</a>
|
||
</div>
|
||
|
||
<details class="mt-10" data-role="dashboard-po-advanced-links" data-storage-key="admin.dashboard.po_advanced_links">
|
||
<summary class="muted">高级筛选(少用,默认收起)</summary>
|
||
<div class="actions mt-8">
|
||
<a class="btn btn-secondary btn-sm" href="{!! $platformOrdersQuickLinks['paid_pending'] !!}">待生效({{ (int) ($stats['platform_orders_paid_pending'] ?? 0) }})</a>
|
||
<a class="btn btn-secondary btn-sm" href="{!! $platformOrdersQuickLinks['renewal_missing_subscription'] !!}">续费缺订阅({{ (int) ($stats['platform_orders_renewal_missing_subscription'] ?? 0) }})</a>
|
||
<a class="btn btn-secondary btn-sm" href="{!! $platformOrdersQuickLinks['bmpa_failed'] !!}">BMPA失败({{ (int) ($stats['platform_orders_bmpa_failed'] ?? 0) }})</a>
|
||
</div>
|
||
</details>
|
||
|
||
@php
|
||
$poTotal = (int) ($stats['platform_orders'] ?? 0);
|
||
$poSyncFailed = (int) ($stats['platform_orders_sync_failed'] ?? 0);
|
||
$poBmpaFailed = (int) ($stats['platform_orders_bmpa_failed'] ?? 0);
|
||
$poNoReceipt = (int) ($stats['platform_orders_paid_no_receipt'] ?? 0);
|
||
$poRenewalMissing = (int) ($stats['platform_orders_renewal_missing_subscription'] ?? 0);
|
||
$poReconcileMismatch = (int) ($stats['platform_orders_reconcile_mismatch'] ?? 0);
|
||
$poRefundInconsistent = (int) ($stats['platform_orders_refund_inconsistent'] ?? 0);
|
||
|
||
$poSyncFailedPct = $poTotal > 0 ? min(100, max(0, round(($poSyncFailed / $poTotal) * 100, 1))) : 0;
|
||
$poBmpaFailedPct = $poTotal > 0 ? min(100, max(0, round(($poBmpaFailed / $poTotal) * 100, 1))) : 0;
|
||
$poNoReceiptPct = $poTotal > 0 ? min(100, max(0, round(($poNoReceipt / $poTotal) * 100, 1))) : 0;
|
||
$poRenewalMissingPct = $poTotal > 0 ? min(100, max(0, round(($poRenewalMissing / $poTotal) * 100, 1))) : 0;
|
||
$poReconcileMismatchPct = $poTotal > 0 ? min(100, max(0, round(($poReconcileMismatch / $poTotal) * 100, 1))) : 0;
|
||
$poRefundInconsistentPct = $poTotal > 0 ? min(100, max(0, round(($poRefundInconsistent / $poTotal) * 100, 1))) : 0;
|
||
@endphp
|
||
|
||
@php
|
||
$poUnpaidPending = (int) ($stats['platform_orders_unpaid_pending'] ?? 0);
|
||
$poPaidPending = (int) ($stats['platform_orders_paid_pending'] ?? 0);
|
||
$poSyncable = (int) ($stats['platform_orders_syncable'] ?? 0);
|
||
|
||
$poUnpaidPendingPct = $poTotal > 0 ? min(100, max(0, round(($poUnpaidPending / $poTotal) * 100, 1))) : 0;
|
||
$poPaidPendingPct = $poTotal > 0 ? min(100, max(0, round(($poPaidPending / $poTotal) * 100, 1))) : 0;
|
||
$poSyncablePct = $poTotal > 0 ? min(100, max(0, round(($poSyncable / $poTotal) * 100, 1))) : 0;
|
||
@endphp
|
||
|
||
<div class="mt-10" data-role="dashboard-po-funnel-bars">
|
||
<div class="muted muted-xs">收费主链漏斗(相对平台订单总量 {{ $poTotal }})</div>
|
||
|
||
@include('admin.components.mini_bar_row', [
|
||
'class' => 'mt-6',
|
||
'rowRole' => 'dashboard-po-unpaid-pending-row',
|
||
'barRole' => 'dashboard-po-unpaid-pending-bar',
|
||
'href' => $platformOrdersQuickLinks['unpaid_pending'],
|
||
'ariaLabel' => '进入待支付订单集合',
|
||
'label' => '待支付',
|
||
'pct' => $poUnpaidPendingPct,
|
||
'title' => $poUnpaidPending . ' / ' . $poTotal . '(' . $poUnpaidPendingPct . '%)',
|
||
'value' => $poUnpaidPendingPct . '%(' . $poUnpaidPending . ')',
|
||
])
|
||
|
||
@include('admin.components.mini_bar_row', [
|
||
'class' => 'mt-6',
|
||
'rowRole' => 'dashboard-po-paid-pending-row',
|
||
'barRole' => 'dashboard-po-paid-pending-bar',
|
||
'href' => $platformOrdersQuickLinks['paid_pending'],
|
||
'ariaLabel' => '进入待生效订单集合',
|
||
'label' => '待生效',
|
||
'pct' => $poPaidPendingPct,
|
||
'title' => $poPaidPending . ' / ' . $poTotal . '(' . $poPaidPendingPct . '%)',
|
||
'value' => $poPaidPendingPct . '%(' . $poPaidPending . ')',
|
||
])
|
||
|
||
@include('admin.components.mini_bar_row', [
|
||
'class' => 'mt-6',
|
||
'rowRole' => 'dashboard-po-syncable-row',
|
||
'barRole' => 'dashboard-po-syncable-bar',
|
||
'href' => $platformOrdersQuickLinks['syncable_only'],
|
||
'ariaLabel' => '进入可同步订单集合',
|
||
'label' => '可同步',
|
||
'pct' => $poSyncablePct,
|
||
'title' => $poSyncable . ' / ' . $poTotal . '(' . $poSyncablePct . '%)',
|
||
'value' => $poSyncablePct . '%(' . $poSyncable . ')',
|
||
])
|
||
</div>
|
||
|
||
<div class="mt-10" data-role="dashboard-po-governance-bars">
|
||
<div class="muted muted-xs">治理风险占比(相对平台订单总量 {{ $poTotal }})</div>
|
||
|
||
@include('admin.components.mini_bar_row', [
|
||
'class' => 'mt-6',
|
||
'rowRole' => 'dashboard-po-sync-failed-row',
|
||
'barRole' => 'dashboard-po-sync-failed-bar',
|
||
'href' => $platformOrdersQuickLinks['sync_failed'],
|
||
'ariaLabel' => '进入同步失败订单集合',
|
||
'label' => '同步失败',
|
||
'pct' => $poSyncFailedPct,
|
||
'title' => $poSyncFailed . ' / ' . $poTotal . '(' . $poSyncFailedPct . '%)',
|
||
'value' => $poSyncFailedPct . '%(' . $poSyncFailed . ')',
|
||
])
|
||
|
||
@include('admin.components.mini_bar_row', [
|
||
'class' => 'mt-6',
|
||
'rowRole' => 'dashboard-po-bmpa-failed-row',
|
||
'barRole' => 'dashboard-po-bmpa-failed-bar',
|
||
'href' => $platformOrdersQuickLinks['bmpa_failed'],
|
||
'ariaLabel' => '进入BMPA失败订单集合',
|
||
'label' => 'BMPA失败',
|
||
'pct' => $poBmpaFailedPct,
|
||
'title' => $poBmpaFailed . ' / ' . $poTotal . '(' . $poBmpaFailedPct . '%)',
|
||
'value' => $poBmpaFailedPct . '%(' . $poBmpaFailed . ')',
|
||
])
|
||
|
||
@include('admin.components.mini_bar_row', [
|
||
'class' => 'mt-6',
|
||
'rowRole' => 'dashboard-po-no-receipt-row',
|
||
'barRole' => 'dashboard-po-no-receipt-bar',
|
||
'href' => $platformOrdersQuickLinks['paid_no_receipt'],
|
||
'ariaLabel' => '进入无回执订单集合',
|
||
'label' => '无回执',
|
||
'pct' => $poNoReceiptPct,
|
||
'title' => $poNoReceipt . ' / ' . $poTotal . '(' . $poNoReceiptPct . '%)',
|
||
'value' => $poNoReceiptPct . '%(' . $poNoReceipt . ')',
|
||
])
|
||
|
||
@include('admin.components.mini_bar_row', [
|
||
'class' => 'mt-6',
|
||
'rowRole' => 'dashboard-po-renewal-missing-row',
|
||
'barRole' => 'dashboard-po-renewal-missing-bar',
|
||
'href' => $platformOrdersQuickLinks['renewal_missing_subscription'],
|
||
'ariaLabel' => '进入续费缺订阅订单集合',
|
||
'label' => '续费缺订阅',
|
||
'pct' => $poRenewalMissingPct,
|
||
'title' => $poRenewalMissing . ' / ' . $poTotal . '(' . $poRenewalMissingPct . '%)',
|
||
'value' => $poRenewalMissingPct . '%(' . $poRenewalMissing . ')',
|
||
])
|
||
|
||
@include('admin.components.mini_bar_row', [
|
||
'class' => 'mt-6',
|
||
'rowRole' => 'dashboard-po-reconcile-mismatch-row',
|
||
'barRole' => 'dashboard-po-reconcile-mismatch-bar',
|
||
'href' => $platformOrdersQuickLinks['reconcile_mismatch'],
|
||
'ariaLabel' => '进入对账不一致订单集合',
|
||
'label' => '对账不一致',
|
||
'pct' => $poReconcileMismatchPct,
|
||
'title' => $poReconcileMismatch . ' / ' . $poTotal . '(' . $poReconcileMismatchPct . '%)',
|
||
'value' => $poReconcileMismatchPct . '%(' . $poReconcileMismatch . ')',
|
||
])
|
||
|
||
@include('admin.components.mini_bar_row', [
|
||
'class' => 'mt-6',
|
||
'rowRole' => 'dashboard-po-refund-inconsistent-row',
|
||
'barRole' => 'dashboard-po-refund-inconsistent-bar',
|
||
'href' => $platformOrdersQuickLinks['refund_inconsistent'],
|
||
'ariaLabel' => '进入退款不一致订单集合',
|
||
'label' => '退款不一致',
|
||
'pct' => $poRefundInconsistentPct,
|
||
'title' => $poRefundInconsistent . ' / ' . $poTotal . '(' . $poRefundInconsistentPct . '%)',
|
||
'value' => $poRefundInconsistentPct . '%(' . $poRefundInconsistent . ')',
|
||
])
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-12">
|
||
<div class="muted">订阅到期治理:</div>
|
||
<div class="actions mt-8">
|
||
<a class="btn btn-secondary btn-sm" href="{!! $subscriptionQuickLinks['expiring_7d'] !!}">7天内到期({{ (int) ($stats['site_subscriptions_expiring_7d'] ?? 0) }})</a>
|
||
<a class="btn btn-secondary btn-sm" href="{!! $subscriptionQuickLinks['expired'] !!}">已过期({{ (int) ($stats['site_subscriptions_expired'] ?? 0) }})</a>
|
||
</div>
|
||
|
||
@php
|
||
$subTotal = (int) ($stats['site_subscriptions'] ?? 0);
|
||
$subExpiring7d = (int) ($stats['site_subscriptions_expiring_7d'] ?? 0);
|
||
$subExpired = (int) ($stats['site_subscriptions_expired'] ?? 0);
|
||
|
||
$pctExpiring7d = $subTotal > 0 ? min(100, max(0, round(($subExpiring7d / $subTotal) * 100, 1))) : 0;
|
||
$pctExpired = $subTotal > 0 ? min(100, max(0, round(($subExpired / $subTotal) * 100, 1))) : 0;
|
||
@endphp
|
||
|
||
<div class="mt-10" data-role="dashboard-sub-expiry-bars">
|
||
<div class="muted muted-xs">占比(相对订阅总量 {{ $subTotal }})</div>
|
||
|
||
@include('admin.components.mini_bar_row', [
|
||
'class' => 'mt-6',
|
||
'rowRole' => 'dashboard-sub-expiring-7d-row',
|
||
'barRole' => 'dashboard-sub-expiring-7d-bar',
|
||
'href' => $subscriptionQuickLinks['expiring_7d'],
|
||
'ariaLabel' => '进入7天内到期订阅集合',
|
||
'label' => '7天内到期',
|
||
'pct' => $pctExpiring7d,
|
||
'title' => $subExpiring7d . ' / ' . $subTotal . '(' . $pctExpiring7d . '%)',
|
||
'value' => $pctExpiring7d . '%(' . $subExpiring7d . ')',
|
||
])
|
||
|
||
@include('admin.components.mini_bar_row', [
|
||
'class' => 'mt-6',
|
||
'rowRole' => 'dashboard-sub-expired-row',
|
||
'barRole' => 'dashboard-sub-expired-bar',
|
||
'href' => $subscriptionQuickLinks['expired'],
|
||
'ariaLabel' => '进入已过期订阅集合',
|
||
'label' => '已过期',
|
||
'pct' => $pctExpired,
|
||
'title' => $subExpired . ' / ' . $subTotal . '(' . $pctExpired . '%)',
|
||
'value' => $pctExpired . '%(' . $subExpired . ')',
|
||
])
|
||
</div>
|
||
</div>
|
||
|
||
<div class="muted muted-xs mt-10">说明:这里先把收费主链的高频治理入口收敛到仪表盘;后续再补趋势/排行的真实聚合。</div>
|
||
</div>
|
||
|
||
<div class="card" data-role="dashboard-platform-ops-overview">
|
||
<h3 class="mt-0">平台定位(运营版)</h3>
|
||
<div class="muted">只保留“看完知道下一步做什么”的北极星指标与治理积压。</div>
|
||
|
||
@php
|
||
$ops = $platformOpsOverview ?? [];
|
||
$opsLinks = (array) ($ops['links'] ?? []);
|
||
$paidRevenue30d = (float) ($ops['paid_revenue_30d'] ?? 0);
|
||
$activePaidMerchants = (int) ($ops['active_paid_merchants'] ?? 0);
|
||
$renewalRate30d = (float) ($ops['renewal_success_rate_30d'] ?? 0);
|
||
$renewalSuccess30d = (int) ($ops['renewal_success_30d'] ?? 0);
|
||
$renewalCreated30d = (int) ($ops['renewal_created_30d'] ?? 0);
|
||
|
||
$ordersTotal7d = (int) ($ops['orders_total_7d'] ?? 0);
|
||
|
||
$funnelUnpaidPending7d = (int) ($ops['funnel_unpaid_pending_7d'] ?? 0);
|
||
$funnelPaid7d = (int) ($ops['funnel_paid_7d'] ?? 0);
|
||
$funnelPaidActivated7d = (int) ($ops['funnel_paid_activated_7d'] ?? 0);
|
||
|
||
$goBmpa = (int) ($ops['govern_bmpa_processable'] ?? 0);
|
||
$goSyncable = (int) ($ops['govern_syncable'] ?? 0);
|
||
$goSyncFailed = (int) ($ops['govern_sync_failed'] ?? 0);
|
||
@endphp
|
||
|
||
@php
|
||
// 北极星指标图形化:给运营“规模感/健康感”,但不引入额外分类维度。
|
||
$merchantsTotal = (int) ($stats['merchants'] ?? 0);
|
||
$pctActivePaidMerchants = $merchantsTotal > 0 ? min(100, max(0, round(($activePaidMerchants / $merchantsTotal) * 100, 1))) : 0;
|
||
@endphp
|
||
|
||
<div class="mt-10" data-role="platform-ops-northstar-bars">
|
||
<div class="muted">
|
||
<strong>北极星指标</strong>
|
||
<span class="muted muted-xs">(点击展开口径说明)</span>
|
||
</div>
|
||
|
||
@include('admin.components.mini_bar_row', [
|
||
'class' => 'mt-6',
|
||
'rowRole' => 'ops-northstar-active-paid-merchants-row',
|
||
'barRole' => 'ops-northstar-active-paid-merchants-bar',
|
||
'href' => (string) ($opsLinks['active_paid_merchants_subscriptions'] ?? $billingEntryLinks['site_subscriptions']),
|
||
'ariaLabel' => '进入活跃付费站点订阅集合',
|
||
'label' => '付费渗透',
|
||
'pct' => $pctActivePaidMerchants,
|
||
'title' => $activePaidMerchants . ' / ' . $merchantsTotal . '(' . $pctActivePaidMerchants . '%)',
|
||
'value' => $pctActivePaidMerchants . '%(' . $activePaidMerchants . ')',
|
||
])
|
||
|
||
@include('admin.components.mini_bar_row', [
|
||
'class' => 'mt-6',
|
||
'rowRole' => 'ops-northstar-renewal-rate-row',
|
||
'barRole' => 'ops-northstar-renewal-rate-bar',
|
||
'href' => (string) ($opsLinks['renewal_orders_30d'] ?? $billingEntryLinks['platform_orders']),
|
||
'ariaLabel' => '进入近30天续费订单集合',
|
||
'label' => '续费成功率',
|
||
'pct' => $renewalRate30d,
|
||
'title' => $renewalRate30d . '%(' . $renewalSuccess30d . ' / ' . $renewalCreated30d . ')',
|
||
'value' => $renewalRate30d . '%',
|
||
])
|
||
|
||
<details class="mt-10" data-role="platform-ops-northstar-details" data-storage-key="admin.dashboard.platform_ops_northstar_details">
|
||
<summary class="muted">北极星指标口径说明</summary>
|
||
|
||
<table class="mt-10">
|
||
<tr>
|
||
<th>近30天已收款</th>
|
||
<td>
|
||
<a class="link" href="{!! (string) ($opsLinks['revenue_30d_paid_orders'] ?? $billingEntryLinks['platform_orders']) !!}">
|
||
¥{{ number_format($paidRevenue30d, 2) }}
|
||
</a>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>活跃付费站点</th>
|
||
<td>
|
||
<a class="link" href="{!! (string) ($opsLinks['active_paid_merchants_subscriptions'] ?? $billingEntryLinks['site_subscriptions']) !!}">
|
||
{{ $activePaidMerchants }}
|
||
</a>
|
||
<span class="muted muted-xs">(以“已生效且未到期订阅”估算)</span>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>续费成功率(30天)</th>
|
||
<td>
|
||
<a class="link" href="{!! (string) ($opsLinks['renewal_orders_30d'] ?? $billingEntryLinks['platform_orders']) !!}">
|
||
{{ $renewalRate30d }}%
|
||
</a>
|
||
<span class="muted muted-xs">({{ $renewalSuccess30d }} / {{ $renewalCreated30d }})</span>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<div class="muted muted-xs mt-6">说明:付费渗透 = 活跃付费站点 / 站点总数;续费成功率按近30天续费单计算。</div>
|
||
</details>
|
||
</div>
|
||
|
||
@php
|
||
$den = max(1, $ordersTotal7d);
|
||
$pctUnpaidPending = $den > 0 ? min(100, max(0, round(($funnelUnpaidPending7d / $den) * 100, 1))) : 0;
|
||
$pctPaid = $den > 0 ? min(100, max(0, round(($funnelPaid7d / $den) * 100, 1))) : 0;
|
||
$pctPaidActivated = $den > 0 ? min(100, max(0, round(($funnelPaidActivated7d / $den) * 100, 1))) : 0;
|
||
|
||
// 待处理治理:以平台订单总量作为分母,给一个“规模感”(不要求精确经营含义)。
|
||
$poTotalForOps = (int) ($stats['platform_orders'] ?? 0);
|
||
$denOps = max(1, $poTotalForOps);
|
||
$pctGoBmpa = $poTotalForOps > 0 ? min(100, max(0, round(($goBmpa / $denOps) * 100, 1))) : 0;
|
||
$pctGoSyncable = $poTotalForOps > 0 ? min(100, max(0, round(($goSyncable / $denOps) * 100, 1))) : 0;
|
||
$pctGoSyncFailed = $poTotalForOps > 0 ? min(100, max(0, round(($goSyncFailed / $denOps) * 100, 1))) : 0;
|
||
@endphp
|
||
|
||
<div class="mt-10" data-role="platform-ops-funnel-bars">
|
||
<div class="muted"><strong>收款漏斗(近7天)</strong></div>
|
||
<div class="muted muted-xs mt-6">用于快速判断卡点:催付 / 治理生效 / 同步订阅。</div>
|
||
|
||
@include('admin.components.mini_bar_row', [
|
||
'class' => 'mt-6',
|
||
'rowRole' => 'ops-funnel-unpaid-pending-row',
|
||
'barRole' => 'ops-funnel-unpaid-pending-bar',
|
||
'href' => (string) ($opsLinks['funnel_unpaid_pending_7d'] ?? $platformOrdersQuickLinks['unpaid_pending']),
|
||
'ariaLabel' => '进入近7天待支付订单集合',
|
||
'label' => '待支付',
|
||
'pct' => $pctUnpaidPending,
|
||
'title' => $funnelUnpaidPending7d . ' / ' . $ordersTotal7d . '(' . $pctUnpaidPending . '%)',
|
||
'value' => $pctUnpaidPending . '%(' . $funnelUnpaidPending7d . ')',
|
||
])
|
||
|
||
@include('admin.components.mini_bar_row', [
|
||
'class' => 'mt-6',
|
||
'rowRole' => 'ops-funnel-paid-row',
|
||
'barRole' => 'ops-funnel-paid-bar',
|
||
'href' => (string) ($opsLinks['funnel_paid_7d'] ?? $platformOrdersQuickLinks['platform_orders']),
|
||
'ariaLabel' => '进入近7天已支付订单集合',
|
||
'label' => '已支付',
|
||
'pct' => $pctPaid,
|
||
'title' => $funnelPaid7d . ' / ' . $ordersTotal7d . '(' . $pctPaid . '%)',
|
||
'value' => $pctPaid . '%(' . $funnelPaid7d . ')',
|
||
])
|
||
|
||
@include('admin.components.mini_bar_row', [
|
||
'class' => 'mt-6',
|
||
'rowRole' => 'ops-funnel-paid-activated-row',
|
||
'barRole' => 'ops-funnel-paid-activated-bar',
|
||
'href' => (string) ($opsLinks['funnel_paid_activated_7d'] ?? $platformOrdersQuickLinks['platform_orders']),
|
||
'ariaLabel' => '进入近7天已生效订单集合',
|
||
'label' => '已生效',
|
||
'pct' => $pctPaidActivated,
|
||
'title' => $funnelPaidActivated7d . ' / ' . $ordersTotal7d . '(' . $pctPaidActivated . '%)',
|
||
'value' => $pctPaidActivated . '%(' . $funnelPaidActivated7d . ')',
|
||
])
|
||
|
||
<div class="muted muted-xs mt-6">分母:近7天平台订单总数 {{ $ordersTotal7d }}(含未支付/已支付)。</div>
|
||
</div>
|
||
|
||
<div class="mt-10" data-role="platform-ops-governance-bars">
|
||
<div class="muted"><strong>待处理治理(Top3)</strong></div>
|
||
<div class="muted muted-xs mt-6">用于快速判断治理积压:优先清空“能直接处理”的集合。</div>
|
||
|
||
@include('admin.components.mini_bar_row', [
|
||
'class' => 'mt-6',
|
||
'rowRole' => 'ops-govern-bmpa-row',
|
||
'barRole' => 'ops-govern-bmpa-bar',
|
||
'href' => $platformOrdersQuickLinks['unpaid_pending'],
|
||
'ariaLabel' => '进入可BMPA处理订单集合',
|
||
'label' => '可BMPA',
|
||
'pct' => $pctGoBmpa,
|
||
'title' => $goBmpa . ' / ' . $poTotalForOps . '(' . $pctGoBmpa . '%)',
|
||
'value' => $pctGoBmpa . '%(' . $goBmpa . ')',
|
||
])
|
||
|
||
@include('admin.components.mini_bar_row', [
|
||
'class' => 'mt-6',
|
||
'rowRole' => 'ops-govern-syncable-row',
|
||
'barRole' => 'ops-govern-syncable-bar',
|
||
'href' => $platformOrdersQuickLinks['syncable_only'],
|
||
'ariaLabel' => '进入可同步订单集合',
|
||
'label' => '可同步',
|
||
'pct' => $pctGoSyncable,
|
||
'title' => $goSyncable . ' / ' . $poTotalForOps . '(' . $pctGoSyncable . '%)',
|
||
'value' => $pctGoSyncable . '%(' . $goSyncable . ')',
|
||
])
|
||
|
||
@include('admin.components.mini_bar_row', [
|
||
'class' => 'mt-6',
|
||
'rowRole' => 'ops-govern-sync-failed-row',
|
||
'barRole' => 'ops-govern-sync-failed-bar',
|
||
'href' => $platformOrdersQuickLinks['sync_failed'],
|
||
'ariaLabel' => '进入同步失败订单集合',
|
||
'label' => '同步失败',
|
||
'pct' => $pctGoSyncFailed,
|
||
'title' => $goSyncFailed . ' / ' . $poTotalForOps . '(' . $pctGoSyncFailed . '%)',
|
||
'value' => $pctGoSyncFailed . '%(' . $goSyncFailed . ')',
|
||
])
|
||
|
||
<div class="muted muted-xs mt-6">分母:平台订单总量 {{ $poTotalForOps }}(用于规模感,不作为经营口径)。</div>
|
||
</div>
|
||
|
||
@php
|
||
// 平台健康预警:原因型风险(点进去有明确动作:补回执/对账/核对退款)。
|
||
$riskNoReceipt = (int) ($stats['platform_orders_paid_no_receipt'] ?? 0);
|
||
$riskReconcileMismatch = (int) ($stats['platform_orders_reconcile_mismatch'] ?? 0);
|
||
$riskRefundInconsistent = (int) ($stats['platform_orders_refund_inconsistent'] ?? 0);
|
||
|
||
$pctRiskNoReceipt = $poTotalForOps > 0 ? min(100, max(0, round(($riskNoReceipt / $denOps) * 100, 1))) : 0;
|
||
$pctRiskReconcileMismatch = $poTotalForOps > 0 ? min(100, max(0, round(($riskReconcileMismatch / $denOps) * 100, 1))) : 0;
|
||
$pctRiskRefundInconsistent = $poTotalForOps > 0 ? min(100, max(0, round(($riskRefundInconsistent / $denOps) * 100, 1))) : 0;
|
||
|
||
// 异常积压:点进去要做“治理修复/补关联/重试”。
|
||
$exBmpaFailed = (int) ($stats['platform_orders_bmpa_failed'] ?? 0);
|
||
$exRenewalMissing = (int) ($stats['platform_orders_renewal_missing_subscription'] ?? 0);
|
||
$pctExBmpaFailed = $poTotalForOps > 0 ? min(100, max(0, round(($exBmpaFailed / $denOps) * 100, 1))) : 0;
|
||
$pctExRenewalMissing = $poTotalForOps > 0 ? min(100, max(0, round(($exRenewalMissing / $denOps) * 100, 1))) : 0;
|
||
@endphp
|
||
|
||
<div class="mt-10" data-role="platform-ops-risk-bars">
|
||
<div class="muted"><strong>平台健康预警(Top3)</strong></div>
|
||
<div class="muted muted-xs mt-6">原因型风险:补回执 / 对账 / 核对退款。</div>
|
||
|
||
@include('admin.components.mini_bar_row', [
|
||
'class' => 'mt-6',
|
||
'rowRole' => 'ops-risk-no-receipt-row',
|
||
'barRole' => 'ops-risk-no-receipt-bar',
|
||
'href' => $platformOrdersQuickLinks['paid_no_receipt'],
|
||
'ariaLabel' => '进入无回执订单集合',
|
||
'label' => '无回执',
|
||
'pct' => $pctRiskNoReceipt,
|
||
'title' => $riskNoReceipt . ' / ' . $poTotalForOps . '(' . $pctRiskNoReceipt . '%)',
|
||
'value' => $pctRiskNoReceipt . '%(' . $riskNoReceipt . ')',
|
||
])
|
||
|
||
@include('admin.components.mini_bar_row', [
|
||
'class' => 'mt-6',
|
||
'rowRole' => 'ops-risk-reconcile-mismatch-row',
|
||
'barRole' => 'ops-risk-reconcile-mismatch-bar',
|
||
'href' => $platformOrdersQuickLinks['reconcile_mismatch'],
|
||
'ariaLabel' => '进入对账不一致订单集合',
|
||
'label' => '对账不一致',
|
||
'pct' => $pctRiskReconcileMismatch,
|
||
'title' => $riskReconcileMismatch . ' / ' . $poTotalForOps . '(' . $pctRiskReconcileMismatch . '%)',
|
||
'value' => $pctRiskReconcileMismatch . '%(' . $riskReconcileMismatch . ')',
|
||
])
|
||
|
||
@include('admin.components.mini_bar_row', [
|
||
'class' => 'mt-6',
|
||
'rowRole' => 'ops-risk-refund-inconsistent-row',
|
||
'barRole' => 'ops-risk-refund-inconsistent-bar',
|
||
'href' => $platformOrdersQuickLinks['refund_inconsistent'],
|
||
'ariaLabel' => '进入退款不一致订单集合',
|
||
'label' => '退款不一致',
|
||
'pct' => $pctRiskRefundInconsistent,
|
||
'title' => $riskRefundInconsistent . ' / ' . $poTotalForOps . '(' . $pctRiskRefundInconsistent . '%)',
|
||
'value' => $pctRiskRefundInconsistent . '%(' . $riskRefundInconsistent . ')',
|
||
])
|
||
</div>
|
||
|
||
<details class="mt-10" data-role="platform-ops-exception-bars" data-storage-key="admin.dashboard.platform_ops_exception_bars">
|
||
<summary class="muted"><strong>更多异常积压(少用)</strong></summary>
|
||
<div class="muted muted-xs mt-6">异常型治理:批量失败 / 续费缺订阅等。</div>
|
||
|
||
@include('admin.components.mini_bar_row', [
|
||
'class' => 'mt-6',
|
||
'rowRole' => 'ops-exception-bmpa-failed-row',
|
||
'barRole' => 'ops-exception-bmpa-failed-bar',
|
||
'href' => $platformOrdersQuickLinks['bmpa_failed'],
|
||
'ariaLabel' => '进入BMPA失败订单集合',
|
||
'label' => 'BMPA失败',
|
||
'pct' => $pctExBmpaFailed,
|
||
'title' => $exBmpaFailed . ' / ' . $poTotalForOps . '(' . $pctExBmpaFailed . '%)',
|
||
'value' => $pctExBmpaFailed . '%(' . $exBmpaFailed . ')',
|
||
])
|
||
|
||
@include('admin.components.mini_bar_row', [
|
||
'class' => 'mt-6',
|
||
'rowRole' => 'ops-exception-renewal-missing-row',
|
||
'barRole' => 'ops-exception-renewal-missing-bar',
|
||
'href' => $platformOrdersQuickLinks['renewal_missing_subscription'],
|
||
'ariaLabel' => '进入续费缺订阅订单集合',
|
||
'label' => '续费缺订阅',
|
||
'pct' => $pctExRenewalMissing,
|
||
'title' => $exRenewalMissing . ' / ' . $poTotalForOps . '(' . $pctExRenewalMissing . '%)',
|
||
'value' => $pctExRenewalMissing . '%(' . $exRenewalMissing . ')',
|
||
])
|
||
</details>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="two-col mb-20" data-role="analysis-skeleton-row2" data-eq-col="recent-platform-orders">
|
||
<div class="card" data-role="dashboard-card-recent-platform-orders">
|
||
<div class="flex-between">
|
||
<h3 class="mt-0">最近平台订单</h3>
|
||
<a class="muted" href="{!! $platformOrdersQuickLinks['platform_orders'] !!}">查看全部</a>
|
||
</div>
|
||
|
||
<table data-role="recent-platform-orders-table">
|
||
<thead>
|
||
<tr>
|
||
<th>订单号</th>
|
||
<th>类型</th>
|
||
<th>金额</th>
|
||
<th>支付</th>
|
||
<th>状态</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@forelse(($recentPlatformOrders ?? []) as $po)
|
||
@php
|
||
$poShowUrl = \App\Support\BackUrl::withBack('/admin/platform-orders/' . $po->id, $selfWithoutBack);
|
||
|
||
$hasReceiptEvidence = (data_get($po->meta, 'payment_summary.total_amount') !== null)
|
||
|| (data_get($po->meta, 'payment_receipts.0.amount') !== null);
|
||
$fixReceiptUrl = \App\Support\BackUrl::withBackAndFragment('/admin/platform-orders/' . $po->id, $selfWithoutBack, 'add-payment-receipt');
|
||
$fixRefundReceiptUrl = \App\Support\BackUrl::withBackAndFragment('/admin/platform-orders/' . $po->id, $selfWithoutBack, 'add-refund-receipt');
|
||
$noReceiptListUrl = $platformOrdersQuickLinks['paid_no_receipt'];
|
||
|
||
$syncErrMsg = (string) (data_get($po->meta, 'subscription_activation_error.message') ?? '');
|
||
$bmpaErrMsg = (string) (data_get($po->meta, 'batch_mark_paid_and_activate_error.message') ?? '');
|
||
|
||
$syncFailedListUrl = $platformOrdersQuickLinks['sync_failed'];
|
||
$bmpaFailedListUrl = $platformOrdersQuickLinks['bmpa_failed'];
|
||
|
||
// 扫描行:直达治理锚点(与下方提示块的链接口径保持一致)
|
||
$scanGoReconcileUrl = \App\Support\BackUrl::withBackAndFragment('/admin/platform-orders/' . $po->id, $selfWithoutBack, 'payment-receipts');
|
||
$scanGoRefundUrl = \App\Support\BackUrl::withBackAndFragment('/admin/platform-orders/' . $po->id, $selfWithoutBack, 'refund-receipts');
|
||
$scanGoSyncFailedUrl = \App\Support\BackUrl::withBackAndFragment('/admin/platform-orders/' . $po->id, $selfWithoutBack, 'sync-failed');
|
||
$scanGoBmpaFailedUrl = \App\Support\BackUrl::withBackAndFragment('/admin/platform-orders/' . $po->id, $selfWithoutBack, 'bmpa-failed');
|
||
$scanGoRelationUrl = \App\Support\BackUrl::withBackAndFragment('/admin/platform-orders/' . $po->id, $selfWithoutBack, 'relation-subscription');
|
||
|
||
// 运营扫描用的“治理状态摘要”(不替代下方的治理提示入口,只用于快速判断)
|
||
// 注意:为避免对“未支付订单”造成误导,回执/对账/退款在非 paid/refunded 时显示 "-"。
|
||
$isPaid = ((string) $po->payment_status === 'paid');
|
||
$isRefunded = ((string) $po->payment_status === 'refunded');
|
||
|
||
$receiptStatusText = $isPaid ? ($hasReceiptEvidence ? '有' : '无') : '-';
|
||
$reconcileStatusText = ($isPaid && $hasReceiptEvidence)
|
||
? ($po->isReconcileMismatch() ? '不一致' : '一致')
|
||
: '-';
|
||
$refundStatusText = ($isPaid || $isRefunded) ? ($po->isRefundInconsistent() ? '异常' : '正常') : '-';
|
||
|
||
$syncStatusText = $syncErrMsg !== '' ? '失败' : '正常';
|
||
$bmpaStatusText = $bmpaErrMsg !== '' ? '失败' : '正常';
|
||
$subscriptionStatusText = ((string) $po->order_type === 'renewal')
|
||
? (empty($po->site_subscription_id) ? '缺' : '有')
|
||
: '-';
|
||
|
||
// 运营提效:失败原因较短时,提供“一键进入同原因集合”链接(避免复制粘贴)。
|
||
// 与平台订单列表页的阈值保持一致(避免仪表盘能点、列表页却不支持/反之)。
|
||
$FAILED_REASON_KEYWORD_MAX_LEN = (int) config('saasshop.platform_orders.sync_error_keyword_link_max_len', 200);
|
||
$syncReasonUrl = '';
|
||
if ($syncErrMsg !== '' && mb_strlen($syncErrMsg) <= $FAILED_REASON_KEYWORD_MAX_LEN) {
|
||
$syncReasonUrl = \App\Support\BackUrl::withBack('/admin/platform-orders?' . \Illuminate\Support\Arr::query([
|
||
'sync_status' => 'failed',
|
||
'sync_error_keyword' => $syncErrMsg,
|
||
]), $selfWithoutBack);
|
||
}
|
||
$bmpaReasonUrl = '';
|
||
if ($bmpaErrMsg !== '' && mb_strlen($bmpaErrMsg) <= $FAILED_REASON_KEYWORD_MAX_LEN) {
|
||
$bmpaReasonUrl = \App\Support\BackUrl::withBack('/admin/platform-orders?' . \Illuminate\Support\Arr::query([
|
||
'bmpa_failed_only' => '1',
|
||
'bmpa_error_keyword' => $bmpaErrMsg,
|
||
]), $selfWithoutBack);
|
||
}
|
||
@endphp
|
||
<tr data-role="recent-platform-order-row" data-order-no="{{ $po->order_no }}">
|
||
<td><a class="link" href="{!! $poShowUrl !!}">{{ $po->order_no }}</a></td>
|
||
<td>
|
||
{{ $po->orderTypeLabel() }}
|
||
<div class="muted muted-xs">
|
||
{{ (string) (optional($po->merchant)->name ?: ('站点#' . (int) ($po->merchant_id ?? 0))) }}
|
||
<span class="muted">|</span>
|
||
{{ (string) (optional($po->plan)->name ?: ((int) ($po->plan_id ?? 0) > 0 ? ('套餐#' . (int) $po->plan_id) : '-')) }}
|
||
</div>
|
||
<div class="muted muted-xs adm-order-scanline" data-role="recent-platform-order-scanline">
|
||
<span class="adm-order-scanitem" title="支付回执证据(payment_summary/payment_receipts)">
|
||
回执:
|
||
@if($receiptStatusText === '无')
|
||
<a class="link" href="{!! $fixReceiptUrl !!}"><strong>{{ $receiptStatusText }}</strong></a>
|
||
@else
|
||
<strong>{{ $receiptStatusText }}</strong>
|
||
@endif
|
||
</span>
|
||
<span class="adm-order-scansep">|</span>
|
||
<span class="adm-order-scanitem" title="回执金额 vs 已付金额(仅在有回执证据时判断)">
|
||
对账:
|
||
@if($reconcileStatusText !== '-' && $reconcileStatusText !== '一致')
|
||
<a class="link" href="{!! $scanGoReconcileUrl !!}"><strong>{{ $reconcileStatusText }}</strong></a>
|
||
@else
|
||
<strong>{{ $reconcileStatusText }}</strong>
|
||
@endif
|
||
</span>
|
||
<span class="adm-order-scansep">|</span>
|
||
<span class="adm-order-scanitem" title="退款金额与支付状态一致性">
|
||
退款:
|
||
@if($refundStatusText !== '-' && $refundStatusText !== '正常')
|
||
<a class="link" href="{!! $scanGoRefundUrl !!}"><strong>{{ $refundStatusText }}</strong></a>
|
||
@else
|
||
<strong>{{ $refundStatusText }}</strong>
|
||
@endif
|
||
</span>
|
||
<span class="adm-order-scansep">|</span>
|
||
<span class="adm-order-scanitem" title="订阅激活同步(subscription_activation_error)">
|
||
同步:
|
||
@if($syncStatusText === '失败')
|
||
<a class="link" href="{!! $scanGoSyncFailedUrl !!}"><strong>{{ $syncStatusText }}</strong></a>
|
||
@else
|
||
<strong>{{ $syncStatusText }}</strong>
|
||
@endif
|
||
</span>
|
||
<span class="adm-order-scansep">|</span>
|
||
<span class="adm-order-scanitem" title="批量标记已付并激活(batch_mark_paid_and_activate_error)">
|
||
BMPA:
|
||
@if($bmpaStatusText === '失败')
|
||
<a class="link" href="{!! $scanGoBmpaFailedUrl !!}"><strong>{{ $bmpaStatusText }}</strong></a>
|
||
@else
|
||
<strong>{{ $bmpaStatusText }}</strong>
|
||
@endif
|
||
</span>
|
||
<span class="adm-order-scansep">|</span>
|
||
<span class="adm-order-scanitem" title="续费单订阅关联(site_subscription_id)">
|
||
订阅:
|
||
@if($subscriptionStatusText === '缺')
|
||
<a class="link" href="{!! $scanGoRelationUrl !!}"><strong>{{ $subscriptionStatusText }}</strong></a>
|
||
@else
|
||
<strong>{{ $subscriptionStatusText }}</strong>
|
||
@endif
|
||
</span>
|
||
</div>
|
||
</td>
|
||
<td>¥{{ number_format((float) $po->payable_amount, 2) }}</td>
|
||
<td>
|
||
{{ $po->payment_status }}
|
||
@if((string) $po->payment_status === 'paid' && ! $hasReceiptEvidence)
|
||
<div class="muted text-danger muted-xs row-warn" data-role="recent-order-no-receipt-hint" title="已付 ¥{{ number_format((float) $po->paid_amount, 2) }}|无回执证据">
|
||
<span class="row-warn-prefix">无回执</span>
|
||
<span class="muted">|</span>
|
||
<a class="link" href="{!! $fixReceiptUrl !!}">去补回执</a>
|
||
<span class="muted">|</span>
|
||
<a class="link" href="{!! $noReceiptListUrl !!}">进入集合</a>
|
||
</div>
|
||
@endif
|
||
</td>
|
||
<td>
|
||
{{ $po->status }}
|
||
@if($syncErrMsg !== '')
|
||
<div class="muted text-danger muted-xs row-warn" data-role="recent-order-sync-failed-hint" title="{{ e($syncErrMsg) }}">
|
||
<span class="row-warn-prefix">同步失败</span>
|
||
<span class="muted">|</span>
|
||
<a class="link" href="{!! $syncFailedListUrl !!}">进入集合</a>
|
||
@if($syncReasonUrl !== '')
|
||
<span class="muted">|</span>
|
||
<a class="link" href="{!! $syncReasonUrl !!}">同原因集合</a>
|
||
@elseif($syncErrMsg !== '')
|
||
<span class="muted">|</span>
|
||
<span class="muted muted-xs">原因过长</span>
|
||
@endif
|
||
<span class="muted">|</span>
|
||
<a class="link" href="{!! $scanGoSyncFailedUrl !!}">查看失败详情</a>
|
||
</div>
|
||
@endif
|
||
@if($bmpaErrMsg !== '')
|
||
<div class="muted text-danger muted-xs row-warn" data-role="recent-order-bmpa-failed-hint" title="{{ e($bmpaErrMsg) }}">
|
||
<span class="row-warn-prefix">BMPA失败</span>
|
||
<span class="muted">|</span>
|
||
<a class="link" href="{!! $bmpaFailedListUrl !!}">进入集合</a>
|
||
@if($bmpaReasonUrl !== '')
|
||
<span class="muted">|</span>
|
||
<a class="link" href="{!! $bmpaReasonUrl !!}">同原因集合</a>
|
||
@elseif($bmpaErrMsg !== '')
|
||
<span class="muted">|</span>
|
||
<span class="muted muted-xs">原因过长</span>
|
||
@endif
|
||
<span class="muted">|</span>
|
||
<a class="link" href="{!! $scanGoBmpaFailedUrl !!}">查看失败详情</a>
|
||
</div>
|
||
@endif
|
||
@if((string) $po->status === 'pending' && (string) $po->payment_status === 'paid' && $po->isReconcileMismatch())
|
||
<div class="muted text-danger muted-xs row-warn" data-role="recent-order-reconcile-mismatch-hint" title="已付 ¥{{ number_format((float) $po->paid_amount, 2) }}|回执 ¥{{ number_format((float) $po->receiptTotal(), 2) }}">
|
||
<span class="row-warn-prefix">对账不一致</span>
|
||
<span class="muted">|</span>
|
||
<a class="link" href="{!! $platformOrdersQuickLinks['reconcile_mismatch'] !!}">进入集合</a>
|
||
<span class="muted">|</span>
|
||
<a class="link" href="{!! $scanGoReconcileUrl !!}">去对账</a>
|
||
<span class="muted">|</span>
|
||
<a class="link" href="{!! $fixReceiptUrl !!}">去补回执</a>
|
||
</div>
|
||
@endif
|
||
@if($po->isRefundInconsistent())
|
||
<div class="muted text-danger muted-xs row-warn" data-role="recent-order-refund-inconsistent-hint" title="已付 ¥{{ number_format((float) $po->paid_amount, 2) }}|退款 ¥{{ number_format((float) $po->refundTotal(), 2) }}">
|
||
<span class="row-warn-prefix">退款不一致</span>
|
||
<span class="muted">|</span>
|
||
<a class="link" href="{!! $platformOrdersQuickLinks['refund_inconsistent'] !!}">进入集合</a>
|
||
<span class="muted">|</span>
|
||
<a class="link" href="{!! $scanGoRefundUrl !!}">去核对退款</a>
|
||
<span class="muted">|</span>
|
||
<a class="link" href="{!! $fixRefundReceiptUrl !!}">去补退款记录</a>
|
||
</div>
|
||
@endif
|
||
@if((string) $po->order_type === 'renewal' && empty($po->site_subscription_id))
|
||
<div class="muted text-danger muted-xs row-warn" data-role="recent-order-renewal-missing-subscription-hint" title="站点 #{{ (int) ($po->merchant_id ?? 0) }}|套餐 #{{ (int) ($po->plan_id ?? 0) }}|site_subscription_id 为空">
|
||
<span class="row-warn-prefix">续费缺订阅</span>
|
||
<span class="muted">|</span>
|
||
<a class="link" href="{!! $platformOrdersQuickLinks['renewal_missing_subscription'] !!}">进入集合</a>
|
||
<span class="muted">|</span>
|
||
<a class="link" href="{!! $scanGoRelationUrl !!}">去关联订阅</a>
|
||
</div>
|
||
@endif
|
||
</td>
|
||
</tr>
|
||
@empty
|
||
<tr>
|
||
<td colspan="5" class="muted">暂无数据</td>
|
||
</tr>
|
||
@endforelse
|
||
</tbody>
|
||
</table>
|
||
|
||
<div class="muted muted-xs mt-10">说明:当前先接入最近订单列表;后续补“同步状态/站点/套餐/治理入口”。</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="flex-between">
|
||
<h3 class="mt-0">套餐订单占比(Top5)</h3>
|
||
<a class="muted" href="{!! $billingEntryLinks['plans'] !!}">查看套餐</a>
|
||
</div>
|
||
|
||
<div class="muted">近7天|按订单数统计</div>
|
||
|
||
@php
|
||
$shareRows = (array) ($planOrderShare ?? []);
|
||
$top5Orders = 0;
|
||
foreach ($shareRows as $r) {
|
||
$top5Orders += (int) ($r['count'] ?? 0);
|
||
}
|
||
$totalOrders = (int) ($planOrderShareTotal ?? 0);
|
||
if ($totalOrders <= 0) {
|
||
// 兜底:兼容旧数据(未传 total 时,至少不影响渲染)
|
||
$totalOrders = (int) $top5Orders;
|
||
}
|
||
@endphp
|
||
|
||
@php
|
||
// 用于前端渐进增强渲染占比条形图(JS 读取 data-points)
|
||
$sharePoints = [];
|
||
foreach ($shareRows as $r) {
|
||
$pid = (int) ($r['plan_id'] ?? 0);
|
||
$sharePoints[] = [
|
||
'plan_id' => $pid,
|
||
'name' => (string) (($planIdToName[$pid] ?? '') ?: ('#' . $pid)),
|
||
'count' => (int) ($r['count'] ?? 0),
|
||
];
|
||
}
|
||
@endphp
|
||
|
||
<div class="adm-mini-share" data-role="plan-order-share-top5-chart" data-total="{{ (int) $totalOrders }}" data-points='@json($sharePoints)'></div>
|
||
|
||
@php
|
||
$shareTop1Count = 0;
|
||
foreach ($shareRows as $r) {
|
||
$shareTop1Count = max($shareTop1Count, (int) ($r['count'] ?? 0));
|
||
}
|
||
$shareTop1Pct = $totalOrders > 0 ? round(($shareTop1Count / $totalOrders) * 100, 1) : 0;
|
||
$shareCoveragePct = $totalOrders > 0 ? round(($top5Orders / $totalOrders) * 100, 1) : 0;
|
||
$shareOtherPct = max(0, round(100 - $shareCoveragePct, 1));
|
||
$shareOtherCount = max(0, (int) $totalOrders - (int) $top5Orders);
|
||
@endphp
|
||
<div class="adm-mini-meta" data-role="plan-order-share-top5-meta">
|
||
<span class="adm-mini-meta-item">全量订单:<strong>{{ (int) $totalOrders }}</strong></span>
|
||
<span class="adm-mini-meta-sep">|</span>
|
||
<span class="adm-mini-meta-item">Top5合计:<strong>{{ (int) $top5Orders }}</strong></span>
|
||
<span class="adm-mini-meta-sep">|</span>
|
||
<span class="adm-mini-meta-item">覆盖率:<strong>{{ $shareCoveragePct }}%</strong></span>
|
||
<span class="adm-mini-meta-sep">|</span>
|
||
<span class="adm-mini-meta-item">其它:<strong>{{ $shareOtherPct }}%</strong>({{ (int) $shareOtherCount }}单)</span>
|
||
<span class="adm-mini-meta-sep">|</span>
|
||
<span class="adm-mini-meta-item">Top1占比:<strong>{{ $shareTop1Pct }}%</strong></span>
|
||
</div>
|
||
|
||
<table data-role="plan-order-share-top5">
|
||
<thead>
|
||
<tr>
|
||
<th>套餐</th>
|
||
<th>订单数</th>
|
||
<th>占比</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@forelse($shareRows as $row)
|
||
@php
|
||
$planId = (int) ($row['plan_id'] ?? 0);
|
||
$count = (int) ($row['count'] ?? 0);
|
||
$pct = $totalOrders > 0 ? round(($count / $totalOrders) * 100, 1) : 0;
|
||
$planName = (string) (($planIdToName[$planId] ?? '') ?: ('#' . $planId));
|
||
@endphp
|
||
@php
|
||
$planOrdersUrl = ($platformOrdersQuickLinks['plan_orders_range'])($planId, $rangeFrom7d, $rangeTo7d);
|
||
@endphp
|
||
<tr>
|
||
<td><a class="link" href="{!! $planOrdersUrl !!}">{{ $planName }}</a></td>
|
||
<td>{{ $count }}</td>
|
||
<td>{{ $pct }}%</td>
|
||
</tr>
|
||
@empty
|
||
<tr>
|
||
<td colspan="3" class="muted">暂无数据</td>
|
||
</tr>
|
||
@endforelse
|
||
</tbody>
|
||
</table>
|
||
|
||
<div class="muted muted-xs mt-10">说明:当前口径为“平台订单按 plan_id 的数量占比(Top5)”;后续扩展到金额占比、渠道占比与时间范围切换。</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
@endsection
|