Files
saasshop/resources/views/admin/dashboard.blade.php

755 lines
46 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.
@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),
];
$platformOrdersQuickLinks = [
'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),
];
@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' => \App\Support\BackUrl::withBack('/admin/plans', $selfWithoutBack),
'site_subscriptions' => \App\Support\BackUrl::withBack('/admin/site-subscriptions', $selfWithoutBack),
'platform_orders' => \App\Support\BackUrl::withBack('/admin/platform-orders', $selfWithoutBack),
];
@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 = \App\Support\BackUrl::withBack(
'/admin/platform-orders?' . \Illuminate\Support\Arr::query([
'created_from' => $d,
'created_to' => $d,
]),
$selfWithoutBack
);
@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 = \App\Support\BackUrl::withBack(
'/admin/platform-orders?' . \Illuminate\Support\Arr::query([
'merchant_id' => $mid,
'payment_status' => 'paid',
'created_from' => now()->subDays(6)->format('Y-m-d'),
'created_to' => now()->format('Y-m-d'),
]),
$selfWithoutBack
);
@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">
<a class="btn btn-secondary btn-sm" href="{!! $platformOrdersQuickLinks['unpaid_pending'] !!}">待支付({{ (int) ($stats['platform_orders_unpaid_pending'] ?? 0) }}</a>
<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['syncable_only'] !!}">可同步({{ (int) ($stats['platform_orders_syncable'] ?? 0) }}</a>
<a class="btn btn-secondary btn-sm" href="{!! $platformOrdersQuickLinks['sync_failed'] !!}">同步失败({{ (int) ($stats['platform_orders_sync_failed'] ?? 0) }}</a>
<a class="btn btn-secondary btn-sm" href="{!! \App\Support\BackUrl::withBack('/admin/platform-orders?renewal_missing_subscription=1', $selfWithoutBack) !!}">续费缺订阅({{ (int) ($stats['platform_orders_renewal_missing_subscription'] ?? 0) }}</a>
<a class="btn btn-secondary btn-sm" href="{!! \App\Support\BackUrl::withBack('/admin/platform-orders?bmpa_failed_only=1', $selfWithoutBack) !!}">BMPA失败{{ (int) ($stats['platform_orders_bmpa_failed'] ?? 0) }}</a>
<a class="btn btn-secondary btn-sm" href="{!! \App\Support\BackUrl::withBack('/admin/platform-orders?payment_status=paid&receipt_status=none', $selfWithoutBack) !!}">无回执({{ (int) ($stats['platform_orders_paid_no_receipt'] ?? 0) }}</a>
<a class="btn btn-secondary btn-sm" href="{!! \App\Support\BackUrl::withBack('/admin/platform-orders?reconcile_mismatch=1', $selfWithoutBack) !!}">对账不一致({{ (int) ($stats['platform_orders_reconcile_mismatch'] ?? 0) }}</a>
<a class="btn btn-secondary btn-sm" href="{!! \App\Support\BackUrl::withBack('/admin/platform-orders?refund_inconsistent=1', $selfWithoutBack) !!}">退款不一致({{ (int) ($stats['platform_orders_refund_inconsistent'] ?? 0) }}</a>
</div>
@php
$poTotal = (int) ($stats['platform_orders'] ?? 0);
$poSyncFailed = (int) ($stats['platform_orders_sync_failed'] ?? 0);
$poNoReceipt = (int) ($stats['platform_orders_paid_no_receipt'] ?? 0);
$poRenewalMissing = (int) ($stats['platform_orders_renewal_missing_subscription'] ?? 0);
$poSyncFailedPct = $poTotal > 0 ? min(100, max(0, round(($poSyncFailed / $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;
@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>
<a class="adm-mini-bar-row adm-mini-bar-row-link mt-6" data-role="dashboard-po-unpaid-pending-row" href="{!! $platformOrdersQuickLinks['unpaid_pending'] !!}" aria-label="进入待支付订单集合">
<div class="adm-mini-bar-label">待支付</div>
<div class="adm-mini-bar" data-role="dashboard-po-unpaid-pending-bar" title="{{ $poUnpaidPending }} / {{ $poTotal }}{{ $poUnpaidPendingPct }}%">
<span class="adm-mini-bar-fill" style="width: {{ $poUnpaidPendingPct }}%"></span>
</div>
<div class="adm-mini-bar-value">{{ $poUnpaidPendingPct }}%{{ $poUnpaidPending }}</div>
</a>
<a class="adm-mini-bar-row adm-mini-bar-row-link mt-6" data-role="dashboard-po-paid-pending-row" href="{!! $platformOrdersQuickLinks['paid_pending'] !!}" aria-label="进入待生效订单集合">
<div class="adm-mini-bar-label">待生效</div>
<div class="adm-mini-bar" data-role="dashboard-po-paid-pending-bar" title="{{ $poPaidPending }} / {{ $poTotal }}{{ $poPaidPendingPct }}%">
<span class="adm-mini-bar-fill" style="width: {{ $poPaidPendingPct }}%"></span>
</div>
<div class="adm-mini-bar-value">{{ $poPaidPendingPct }}%{{ $poPaidPending }}</div>
</a>
<a class="adm-mini-bar-row adm-mini-bar-row-link mt-6" data-role="dashboard-po-syncable-row" href="{!! $platformOrdersQuickLinks['syncable_only'] !!}" aria-label="进入可同步订单集合">
<div class="adm-mini-bar-label">可同步</div>
<div class="adm-mini-bar" data-role="dashboard-po-syncable-bar" title="{{ $poSyncable }} / {{ $poTotal }}{{ $poSyncablePct }}%">
<span class="adm-mini-bar-fill" style="width: {{ $poSyncablePct }}%"></span>
</div>
<div class="adm-mini-bar-value">{{ $poSyncablePct }}%{{ $poSyncable }}</div>
</a>
</div>
<div class="mt-10" data-role="dashboard-po-governance-bars">
<div class="muted muted-xs">治理风险占比(相对平台订单总量 {{ $poTotal }}</div>
<a class="adm-mini-bar-row adm-mini-bar-row-link mt-6" data-role="dashboard-po-sync-failed-row" href="{!! $platformOrdersQuickLinks['sync_failed'] !!}" aria-label="进入同步失败订单集合">
<div class="adm-mini-bar-label">同步失败</div>
<div class="adm-mini-bar" data-role="dashboard-po-sync-failed-bar" title="{{ $poSyncFailed }} / {{ $poTotal }}{{ $poSyncFailedPct }}%">
<span class="adm-mini-bar-fill" style="width: {{ $poSyncFailedPct }}%"></span>
</div>
<div class="adm-mini-bar-value">{{ $poSyncFailedPct }}%{{ $poSyncFailed }}</div>
</a>
<a class="adm-mini-bar-row adm-mini-bar-row-link mt-6" data-role="dashboard-po-no-receipt-row" href="{!! \App\Support\BackUrl::withBack('/admin/platform-orders?payment_status=paid&receipt_status=none', $selfWithoutBack) !!}" aria-label="进入无回执订单集合">
<div class="adm-mini-bar-label">无回执</div>
<div class="adm-mini-bar" data-role="dashboard-po-no-receipt-bar" title="{{ $poNoReceipt }} / {{ $poTotal }}{{ $poNoReceiptPct }}%">
<span class="adm-mini-bar-fill" style="width: {{ $poNoReceiptPct }}%"></span>
</div>
<div class="adm-mini-bar-value">{{ $poNoReceiptPct }}%{{ $poNoReceipt }}</div>
</a>
<a class="adm-mini-bar-row adm-mini-bar-row-link mt-6" data-role="dashboard-po-renewal-missing-row" href="{!! \App\Support\BackUrl::withBack('/admin/platform-orders?renewal_missing_subscription=1', $selfWithoutBack) !!}" aria-label="进入续费缺订阅订单集合">
<div class="adm-mini-bar-label">续费缺订阅</div>
<div class="adm-mini-bar" data-role="dashboard-po-renewal-missing-bar" title="{{ $poRenewalMissing }} / {{ $poTotal }}{{ $poRenewalMissingPct }}%">
<span class="adm-mini-bar-fill" style="width: {{ $poRenewalMissingPct }}%"></span>
</div>
<div class="adm-mini-bar-value">{{ $poRenewalMissingPct }}%{{ $poRenewalMissing }}</div>
</a>
</div>
</div>
<div class="mt-12">
<div class="muted">订阅到期治理:</div>
<div class="actions mt-8">
<a class="btn btn-secondary btn-sm" href="{!! \App\Support\BackUrl::withBack('/admin/site-subscriptions?expiry=expiring_7d', $selfWithoutBack) !!}">7天内到期({{ (int) ($stats['site_subscriptions_expiring_7d'] ?? 0) }}</a>
<a class="btn btn-secondary btn-sm" href="{!! \App\Support\BackUrl::withBack('/admin/site-subscriptions?expiry=expired', $selfWithoutBack) !!}">已过期({{ (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>
<a class="adm-mini-bar-row adm-mini-bar-row-link mt-6" data-role="dashboard-sub-expiring-7d-row" href="{!! \App\Support\BackUrl::withBack('/admin/site-subscriptions?expiry=expiring_7d', $selfWithoutBack) !!}" aria-label="进入7天内到期订阅集合">
<div class="adm-mini-bar-label">7天内到期</div>
<div class="adm-mini-bar" data-role="dashboard-sub-expiring-7d-bar" title="{{ $subExpiring7d }} / {{ $subTotal }}{{ $pctExpiring7d }}%">
<span class="adm-mini-bar-fill" style="width: {{ $pctExpiring7d }}%"></span>
</div>
<div class="adm-mini-bar-value">{{ $pctExpiring7d }}%{{ $subExpiring7d }}</div>
</a>
<a class="adm-mini-bar-row adm-mini-bar-row-link mt-6" data-role="dashboard-sub-expired-row" href="{!! \App\Support\BackUrl::withBack('/admin/site-subscriptions?expiry=expired', $selfWithoutBack) !!}" aria-label="进入已过期订阅集合">
<div class="adm-mini-bar-label">已过期</div>
<div class="adm-mini-bar" data-role="dashboard-sub-expired-bar" title="{{ $subExpired }} / {{ $subTotal }}{{ $pctExpired }}%">
<span class="adm-mini-bar-fill" style="width: {{ $pctExpired }}%"></span>
</div>
<div class="adm-mini-bar-value">{{ $pctExpired }}%{{ $subExpired }}</div>
</a>
</div>
</div>
<div class="muted muted-xs mt-10">说明:这里先把收费主链的高频治理入口收敛到仪表盘;后续再补趋势/排行的真实聚合。</div>
</div>
<div class="card">
<h3 class="mt-0">平台定位</h3>
<table>
<tr><th>后台角色</th><td>{{ $platformOverview['system_role'] }}</td></tr>
<tr><th>当前视角</th><td>{{ $platformOverview['current_scope'] }}</td></tr>
<tr><th>商家模式</th><td>{{ $platformOverview['merchant_mode'] }}</td></tr>
<tr><th>渠道数</th><td>{{ $platformOverview['channel_count'] }}</td></tr>
<tr><th>活跃商家</th><td>{{ $platformOverview['active_merchants'] }}</td></tr>
<tr><th>待处理订单</th><td>{{ $platformOverview['pending_orders'] }}</td></tr>
</table>
</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>
@php
$platformOrdersIndexUrl = \App\Support\BackUrl::withBack('/admin/platform-orders', $selfWithoutBack);
@endphp
<a class="muted" href="{!! $platformOrdersIndexUrl !!}">查看全部</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 = \App\Support\BackUrl::withBack('/admin/platform-orders?payment_status=paid&receipt_status=none', $selfWithoutBack);
$syncErrMsg = (string) (data_get($po->meta, 'subscription_activation_error.message') ?? '');
$bmpaErrMsg = (string) (data_get($po->meta, 'batch_mark_paid_and_activate_error.message') ?? '');
$syncFailedListUrl = \App\Support\BackUrl::withBack('/admin/platform-orders?sync_status=failed', $selfWithoutBack);
$bmpaFailedListUrl = \App\Support\BackUrl::withBack('/admin/platform-orders?bmpa_failed_only=1', $selfWithoutBack);
// 扫描行:直达治理锚点(与下方提示块的链接口径保持一致)
$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())
@php
$reconcileMismatchUrl = \App\Support\BackUrl::withBack('/admin/platform-orders?reconcile_mismatch=1', $selfWithoutBack);
@endphp
<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="{!! $reconcileMismatchUrl !!}">进入集合</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())
@php
$refundInconsistentUrl = \App\Support\BackUrl::withBack('/admin/platform-orders?refund_inconsistent=1', $selfWithoutBack);
@endphp
<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="{!! $refundInconsistentUrl !!}">进入集合</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))
@php
$renewalMissingSubscriptionUrl = \App\Support\BackUrl::withBack('/admin/platform-orders?renewal_missing_subscription=1', $selfWithoutBack);
@endphp
<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="{!! $renewalMissingSubscriptionUrl !!}">进入集合</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>
@php
$plansIndexUrl = \App\Support\BackUrl::withBack('/admin/plans', $selfWithoutBack);
@endphp
<a class="muted" href="{!! $plansIndexUrl !!}">查看套餐</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 = \App\Support\BackUrl::withBack(
'/admin/platform-orders?' . \Illuminate\Support\Arr::query([
'plan_id' => $planId,
'created_from' => now()->subDays(6)->format('Y-m-d'),
'created_to' => now()->format('Y-m-d'),
]),
$selfWithoutBack
);
@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