Files
saasshop/app/Http/Controllers/Admin/DashboardController.php

200 lines
8.5 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\Admin;
use App\Models\Order;
use App\Models\Product;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use App\Models\SiteSubscription;
use App\Models\User;
use App\Support\CacheKeys;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\View\View;
class DashboardController extends Controller
{
use ResolvesPlatformAdminContext;
public function index(Request $request): View
{
$admin = $this->ensurePlatformAdmin($request);
$stats = Cache::remember(
CacheKeys::platformDashboardStats(),
now()->addMinutes(10),
fn () => [
'merchants' => Merchant::count(),
'admins' => Admin::count(),
'users' => User::count(),
'products' => Product::count(),
'orders' => Order::count(),
// 收费中心(平台侧)
'plans' => Plan::count(),
'site_subscriptions' => SiteSubscription::count(),
'site_subscriptions_expired' => SiteSubscription::query()
->whereNotNull('ends_at')
->where('ends_at', '<', now())
->count(),
'site_subscriptions_expiring_7d' => SiteSubscription::query()
->whereNotNull('ends_at')
->where('ends_at', '>=', now())
->where('ends_at', '<', now()->addDays(7))
->count(),
'platform_orders' => PlatformOrder::count(),
'platform_orders_unpaid_pending' => PlatformOrder::query()
->where('payment_status', 'unpaid')
->where('status', 'pending')
->count(),
'platform_orders_paid_pending' => PlatformOrder::query()
->where('payment_status', 'paid')
->where('status', 'pending')
// 口径对齐“待生效”语义:排除明确的同步失败(失败单应该去同步失败治理)
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') IS NULL")
->count(),
// 可同步沿用平台订单列表口径paid+activated+未同步+无失败),且排除续费缺订阅脏数据
'platform_orders_syncable' => PlatformOrder::query()
->where('payment_status', 'paid')
->where('status', 'activated')
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation.subscription_id') IS NULL")
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') IS NULL")
->where(function ($q) {
$q->where('order_type', '!=', 'renewal')
->orWhereNotNull('site_subscription_id');
})
->count(),
// 同步失败沿用平台订单列表口径meta.subscription_activation_error.message 存在即失败)
'platform_orders_sync_failed' => PlatformOrder::query()
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') IS NOT NULL")
->count(),
'platform_orders_renewal_missing_subscription' => PlatformOrder::query()
->where('order_type', 'renewal')
->whereNull('site_subscription_id')
->count(),
// BMPA 失败:用于运营快速定位“批量标记支付并生效”失败的订单集合
'platform_orders_bmpa_failed' => PlatformOrder::query()
->whereRaw("JSON_EXTRACT(meta, '$.batch_mark_paid_and_activate_error.message') IS NOT NULL")
->count(),
// 无回执(已支付但缺少回执证据):用于治理“已付但无回执”的风险订单
'platform_orders_paid_no_receipt' => PlatformOrder::query()
->where('payment_status', 'paid')
->whereRaw("JSON_EXTRACT(meta, '$.payment_summary.total_amount') IS NULL")
->whereRaw("JSON_EXTRACT(meta, '$.payment_receipts[0].amount') IS NULL")
->count(),
// 站点治理
'active_merchants' => Merchant::query()->where('status', 'active')->count(),
'pending_orders' => Order::query()->where('status', 'pending')->count(),
]
);
// 趋势卡(最小可用):近 7 天平台订单按天统计(订单数 + 已付金额)
$trendDays = 7;
$trendStart = now()->startOfDay()->subDays($trendDays - 1);
$trendEnd = now()->endOfDay();
$trendRawRows = PlatformOrder::query()
->selectRaw("DATE(created_at) as d")
->selectRaw('COUNT(*) as cnt')
->selectRaw("SUM(CASE WHEN payment_status = 'paid' THEN paid_amount ELSE 0 END) as paid_sum")
->whereBetween('created_at', [$trendStart, $trendEnd])
->groupBy('d')
->orderBy('d')
->get();
$trendByDate = [];
foreach ($trendRawRows as $r) {
$trendByDate[(string) $r->d] = [
'date' => (string) $r->d,
'count' => (int) ($r->cnt ?? 0),
'paid_sum' => (float) ($r->paid_sum ?? 0),
];
}
$platformOrderTrend7d = [];
for ($i = 0; $i < $trendDays; $i++) {
$day = $trendStart->copy()->addDays($i)->format('Y-m-d');
$platformOrderTrend7d[] = $trendByDate[$day] ?? [
'date' => $day,
'count' => 0,
'paid_sum' => 0.0,
];
}
$recentPlatformOrders = PlatformOrder::query()
->orderByDesc('id')
->limit(5)
->get();
// 占比卡(最小可用):按套餐统计平台订单数量 TopN
$planOrderShare = PlatformOrder::query()
->selectRaw('plan_id, COUNT(*) as cnt')
->whereNotNull('plan_id')
->groupBy('plan_id')
->orderByDesc('cnt')
->limit(5)
->get()
->map(function ($row) {
return [
'plan_id' => (int) $row->plan_id,
'count' => (int) $row->cnt,
];
})
->values()
->all();
$planIdToName = Plan::query()->pluck('name', 'id')->all();
// 排行卡(最小可用):近 7 天站点收入排行 Top5按已付金额
$merchantRevenueRank7d = PlatformOrder::query()
->selectRaw('merchant_id, COUNT(*) as cnt')
->selectRaw("SUM(CASE WHEN payment_status = 'paid' THEN paid_amount ELSE 0 END) as paid_sum")
->whereBetween('created_at', [$trendStart, $trendEnd])
->groupBy('merchant_id')
->orderByDesc('paid_sum')
->limit(5)
->get()
->map(function ($row) {
return [
'merchant_id' => (int) ($row->merchant_id ?? 0),
'count' => (int) ($row->cnt ?? 0),
'paid_sum' => (float) ($row->paid_sum ?? 0),
];
})
->values()
->all();
$merchantIdToName = Merchant::query()->pluck('name', 'id')->all();
return view('admin.dashboard', [
'adminName' => $admin->name,
'stats' => $stats,
'platformOrderTrend7d' => $platformOrderTrend7d,
'recentPlatformOrders' => $recentPlatformOrders,
'planOrderShare' => $planOrderShare,
'planIdToName' => $planIdToName,
'merchantRevenueRank7d' => $merchantRevenueRank7d,
'merchantIdToName' => $merchantIdToName,
'platformAdmin' => $admin,
'cacheMeta' => [
'store' => config('cache.default'),
'ttl' => '10m',
],
'platformOverview' => [
'system_role' => '总台管理',
'current_scope' => '总台运营方视角',
'merchant_mode' => '统一管理多个站点',
'channel_count' => 5,
'active_merchants' => $stats['active_merchants'],
'pending_orders' => $stats['pending_orders'],
],
]);
}
}