264 lines
13 KiB
PHP
264 lines
13 KiB
PHP
<?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(),
|
||
// 对账不一致(基于 paid_amount vs 回执总额,容差见 config('saasshop.amounts.tolerance'))
|
||
'platform_orders_reconcile_mismatch' => (function () {
|
||
$tol = (float) config('saasshop.amounts.tolerance', 0.01);
|
||
$tolCents = (int) round($tol * 100);
|
||
$tolCents = max(1, $tolCents);
|
||
|
||
$q = PlatformOrder::query();
|
||
$driver = $q->getQuery()->getConnection()->getDriverName();
|
||
|
||
// 重要:与 PlatformOrder::isReconcileMismatch() 口径一致:若无回执证据,不判定 mismatch(而是走“无回执”治理集合)
|
||
$q->where(function ($b) {
|
||
$b->whereRaw("JSON_EXTRACT(meta, '$.payment_summary.total_amount') IS NOT NULL")
|
||
->orWhereRaw("JSON_EXTRACT(meta, '$.payment_receipts[0].amount') IS NOT NULL");
|
||
});
|
||
|
||
if ($driver === 'sqlite') {
|
||
$q->whereRaw("ABS(ROUND((CASE WHEN JSON_EXTRACT(meta, '$.payment_summary.total_amount') IS NOT NULL THEN CAST(JSON_EXTRACT(meta, '$.payment_summary.total_amount') AS REAL) ELSE (SELECT IFNULL(SUM(CAST(JSON_EXTRACT(value, '$.amount') AS REAL)), 0) FROM json_each(COALESCE(JSON_EXTRACT(meta, '$.payment_receipts'), '[]'))) END) * 100) - ROUND(paid_amount * 100)) >= {$tolCents}");
|
||
} else {
|
||
$q->whereRaw("ABS(ROUND((CASE WHEN JSON_EXTRACT(meta, '$.payment_summary.total_amount') IS NOT NULL THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(meta, '$.payment_summary.total_amount')) AS DECIMAL(12,2)) ELSE (SELECT IFNULL(SUM(j.amount), 0) FROM JSON_TABLE(meta, '$.payment_receipts[*]' COLUMNS(amount DECIMAL(12,2) PATH '$.amount')) j) END) * 100) - ROUND(paid_amount * 100)) >= {$tolCents}");
|
||
}
|
||
|
||
return $q->count();
|
||
})(),
|
||
// 退款数据不一致(口径与平台订单列表 refund_inconsistent=1 一致)
|
||
'platform_orders_refund_inconsistent' => (function () {
|
||
$tol = (float) config('saasshop.amounts.tolerance', 0.01);
|
||
$tolCents = (int) round($tol * 100);
|
||
$tolCents = max(1, $tolCents);
|
||
|
||
$q = PlatformOrder::query();
|
||
$driver = $q->getQuery()->getConnection()->getDriverName();
|
||
|
||
if ($driver === 'sqlite') {
|
||
$refundTotalExpr = "(CASE WHEN JSON_EXTRACT(meta, '$.refund_summary.total_amount') IS NOT NULL THEN CAST(JSON_EXTRACT(meta, '$.refund_summary.total_amount') AS REAL) ELSE (SELECT IFNULL(SUM(CAST(JSON_EXTRACT(value, '$.amount') AS REAL)), 0) FROM json_each(COALESCE(JSON_EXTRACT(meta, '$.refund_receipts'), '[]'))) END)";
|
||
|
||
$q->where(function ($b) use ($refundTotalExpr, $tolCents) {
|
||
$b->where(function ($q2) use ($refundTotalExpr, $tolCents) {
|
||
$q2->where('payment_status', 'refunded')
|
||
->whereRaw('paid_amount > 0')
|
||
->whereRaw("(ROUND($refundTotalExpr * 100) + {$tolCents}) < ROUND(paid_amount * 100)");
|
||
})->orWhere(function ($q2) use ($refundTotalExpr, $tolCents) {
|
||
$q2->where('payment_status', '!=', 'refunded')
|
||
->whereRaw('paid_amount > 0')
|
||
->whereRaw("ROUND($refundTotalExpr * 100) >= (ROUND(paid_amount * 100) + {$tolCents})");
|
||
});
|
||
});
|
||
} else {
|
||
$refundTotalExpr = "(CASE WHEN JSON_EXTRACT(meta, '$.refund_summary.total_amount') IS NOT NULL THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(meta, '$.refund_summary.total_amount')) AS DECIMAL(12,2)) ELSE (SELECT IFNULL(SUM(j.amount), 0) FROM JSON_TABLE(meta, '$.refund_receipts[*]' COLUMNS(amount DECIMAL(12,2) PATH '$.amount')) j) END)";
|
||
|
||
$q->where(function ($b) use ($refundTotalExpr, $tolCents) {
|
||
$b->where(function ($q2) use ($refundTotalExpr, $tolCents) {
|
||
$q2->where('payment_status', 'refunded')
|
||
->whereRaw('paid_amount > 0')
|
||
->whereRaw("(ROUND($refundTotalExpr * 100) + {$tolCents}) < ROUND(paid_amount * 100)");
|
||
})->orWhere(function ($q2) use ($refundTotalExpr, $tolCents) {
|
||
$q2->where('payment_status', '!=', 'refunded')
|
||
->whereRaw('paid_amount > 0')
|
||
->whereRaw("ROUND($refundTotalExpr * 100) >= (ROUND(paid_amount * 100) + {$tolCents})");
|
||
});
|
||
});
|
||
}
|
||
|
||
return $q->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'],
|
||
],
|
||
]);
|
||
}
|
||
}
|