fix: statisc

This commit is contained in:
hashbro
2026-08-10 03:53:10 +08:00
parent ad30453922
commit aad44bf52e
33 changed files with 1207 additions and 28 deletions
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Concerns\PortalAware;
use App\Http\Controllers\Controller;
use App\Models\Device;
use App\Models\PageVisit;
use App\Models\User;
use App\Support\AgentScope;
use Carbon\Carbon;
@@ -49,6 +50,49 @@ class DashboardController extends Controller
$newCount = (clone $base)->whereBetween('created_at', [$from, $to])->count();
$activeCount = (clone $base)->whereBetween('updated_at', [$from, $to])->count();
$visits = PageVisit::query();
AgentScope::applyChannelIdScope($visits, $this->agent());
if (! $this->isAgentPortal() && $agentUserId > 0) {
AgentScope::applyChannelIdAgentUserFilter($visits, $agentUserId);
}
if ($channelId !== '') {
$visits->where('channel_id', 'like', '%'.$channelId.'%');
}
$visits->whereBetween('created_at', [$from, $to]);
$pv = (clone $visits)->count();
$uv = (int) (clone $visits)->selectRaw('COUNT(DISTINCT client_uid) as aggregate')->value('aggregate');
$byOs = (clone $visits)
->select('os')
->selectRaw('COUNT(*) as pv')
->selectRaw('COUNT(DISTINCT client_uid) as uv')
->groupBy('os')
->orderByDesc('pv')
->limit(10)
->get()
->map(static fn ($r) => [
'label' => ((string) ($r->os ?? '')) !== '' ? (string) $r->os : 'Unknown',
'pv' => (int) $r->pv,
'uv' => (int) $r->uv,
])
->values();
$byBrowser = (clone $visits)
->select('browser')
->selectRaw('COUNT(*) as pv')
->selectRaw('COUNT(DISTINCT client_uid) as uv')
->groupBy('browser')
->orderByDesc('pv')
->limit(10)
->get()
->map(static fn ($r) => [
'label' => ((string) ($r->browser ?? '')) !== '' ? (string) $r->browser : 'Unknown',
'pv' => (int) $r->pv,
'uv' => (int) $r->uv,
])
->values();
return response()->json([
'code' => 0,
'msg' => '',
@@ -56,6 +100,10 @@ class DashboardController extends Controller
'total' => $total,
'new_count' => $newCount,
'active_count' => $activeCount,
'pv' => $pv,
'uv' => $uv,
'by_os' => $byOs,
'by_browser' => $byBrowser,
'range' => $range,
'from' => $from->toDateTimeString(),
'to' => $to->toDateTimeString(),
@@ -0,0 +1,177 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Concerns\PortalAware;
use App\Http\Controllers\Controller;
use App\Models\PageVisit;
use App\Models\User;
use App\Support\AgentScope;
use Carbon\Carbon;
use Illuminate\Http\Request;
class PageVisitController extends Controller
{
use PortalAware;
public function index()
{
$agents = $this->isAgentPortal()
? collect()
: User::query()->orderBy('username')->get(['id', 'username']);
return view('admin.visits.index', [
'portal' => $this->portal(),
'agents' => $agents,
]);
}
public function data(Request $request)
{
$q = $this->scopedQuery($request);
$limit = min(100, max(1, (int) $request->query('limit', 20)));
$page = max(1, (int) $request->query('page', 1));
$total = (clone $q)->count();
$rows = (clone $q)
->orderByDesc('id')
->forPage($page, $limit)
->get([
'id', 'channel_id', 'client_uid', 'os', 'os_version',
'browser', 'browser_version', 'user_agent', 'ip', 'path', 'created_at',
]);
return response()->json([
'code' => 0,
'msg' => '',
'count' => $total,
'data' => $rows->map(static fn (PageVisit $v) => [
'id' => $v->id,
'channel_id' => $v->channel_id,
'client_uid' => $v->client_uid,
'os' => $v->os ?: '',
'os_version' => $v->os_version ?: '',
'browser' => $v->browser ?: '',
'browser_version' => $v->browser_version ?: '',
'user_agent' => $v->user_agent ?: '',
'ip' => $v->ip ?: '',
'path' => $v->path ?: '',
'created_at' => optional($v->created_at)?->toDateTimeString(),
])->values(),
]);
}
public function groups(Request $request)
{
$group = (string) $request->query('group', 'os');
$base = $this->scopedQuery($request);
$builder = match ($group) {
'os_version' => $base
->select('os', 'os_version')
->selectRaw('COUNT(*) as pv')
->selectRaw('COUNT(DISTINCT client_uid) as uv')
->groupBy('os', 'os_version'),
'browser' => $base
->select('browser')
->selectRaw('COUNT(*) as pv')
->selectRaw('COUNT(DISTINCT client_uid) as uv')
->groupBy('browser'),
'browser_version' => $base
->select('browser', 'browser_version')
->selectRaw('COUNT(*) as pv')
->selectRaw('COUNT(DISTINCT client_uid) as uv')
->groupBy('browser', 'browser_version'),
default => $base
->select('os')
->selectRaw('COUNT(*) as pv')
->selectRaw('COUNT(DISTINCT client_uid) as uv')
->groupBy('os'),
};
$rows = $builder
->orderByDesc('pv')
->limit(50)
->get()
->map(static function ($row) use ($group) {
$label = match ($group) {
'os_version' => trim(((string) ($row->os ?? '')).' '.((string) ($row->os_version ?? ''))),
'browser' => (string) ($row->browser ?? ''),
'browser_version' => trim(((string) ($row->browser ?? '')).' '.((string) ($row->browser_version ?? ''))),
default => (string) ($row->os ?? ''),
};
return [
'label' => $label !== '' ? $label : 'Unknown',
'pv' => (int) $row->pv,
'uv' => (int) $row->uv,
];
})
->values();
return response()->json([
'code' => 0,
'msg' => '',
'data' => [
'group' => $group,
'rows' => $rows,
],
]);
}
private function scopedQuery(Request $request)
{
$q = PageVisit::query();
AgentScope::applyChannelIdScope($q, $this->agent());
$agentUserId = $this->isAgentPortal()
? (int) ($this->agent()?->id ?? 0)
: (int) $request->query('agent_user_id', 0);
if (! $this->isAgentPortal() && $agentUserId > 0) {
AgentScope::applyChannelIdAgentUserFilter($q, $agentUserId);
}
$channelId = trim((string) $request->query('channel_id', ''));
if ($channelId !== '') {
$q->where('channel_id', 'like', '%'.$channelId.'%');
}
$os = trim((string) $request->query('os', ''));
if ($os !== '') {
$q->where('os', $os);
}
$osVersion = trim((string) $request->query('os_version', ''));
if ($osVersion !== '') {
$q->where('os_version', $osVersion);
}
$browser = trim((string) $request->query('browser', ''));
if ($browser !== '') {
$q->where('browser', $browser);
}
$browserVersion = trim((string) $request->query('browser_version', ''));
if ($browserVersion !== '') {
$q->where('browser_version', $browserVersion);
}
[$from, $to] = $this->rangeBounds((string) $request->query('range', '30d'));
$q->whereBetween('created_at', [$from, $to]);
return $q;
}
/**
* @return array{0: Carbon, 1: Carbon}
*/
private function rangeBounds(string $range): array
{
$now = Carbon::now();
return match ($range) {
'today' => [$now->copy()->startOfDay(), $now->copy()->endOfDay()],
'yesterday' => [
$now->copy()->subDay()->startOfDay(),
$now->copy()->subDay()->endOfDay(),
],
'7d' => [$now->copy()->subDays(6)->startOfDay(), $now->copy()->endOfDay()],
default => [$now->copy()->subDays(29)->startOfDay(), $now->copy()->endOfDay()],
};
}
}
@@ -22,9 +22,13 @@ class SystemSettingsController extends Controller
'telegram_owner_chat_id' => ['nullable', 'string', 'max:64'],
'channels_max_per_agent' => ['nullable', 'integer', 'min:1', 'max:100'],
'channels_domains' => ['nullable', 'string', 'max:4000'],
'panel_admin_hosts' => ['nullable', 'string', 'max:4000'],
'panel_agent_hosts' => ['nullable', 'string', 'max:4000'],
]);
$domains = SettingsService::parseDomains($data['channels_domains'] ?? null);
$adminHosts = SettingsService::parseDomains($data['panel_admin_hosts'] ?? null);
$agentHosts = SettingsService::parseDomains($data['panel_agent_hosts'] ?? null);
$settings->putMany([
'telegram.bot_token' => $data['telegram_bot_token'] ?? null,
@@ -34,6 +38,9 @@ class SystemSettingsController extends Controller
: null,
// Persist normalized comma-separated list (empty clears override to []).
'channels.domains' => $domains === [] ? '' : implode(',', $domains),
// Empty = no Host restriction for that panel.
'panel.admin_hosts' => $adminHosts === [] ? '' : implode(',', $adminHosts),
'panel.agent_hosts' => $agentHosts === [] ? '' : implode(',', $agentHosts),
]);
return response()->json(['code' => 0, 'msg' => '已保存']);
@@ -0,0 +1,64 @@
<?php
namespace App\Http\Controllers;
use App\Models\PageVisit;
use App\Support\UserAgentParser;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Cache;
class PageHitController extends Controller
{
private const GIF_1X1 = "GIF89a\x01\x00\x01\x00\x80\x00\x00\xff\xff\xff\x00\x00\x00!\xf9\x04\x01\x00\x00\x00\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;";
public function __invoke(Request $request): Response
{
$channelId = strtolower(trim((string) $request->query('c', $request->input('c', ''))));
$clientUid = trim((string) $request->query('u', $request->input('u', '')));
$path = trim((string) $request->query('p', $request->input('p', '')));
if (! preg_match('/^[0-9a-f]{32}$/', $channelId)) {
return $this->pixel();
}
if (! preg_match('/^[0-9a-fA-F-]{8,64}$/', $clientUid) && ! preg_match('/^tmp_[0-9a-f]+$/i', $clientUid)) {
return $this->pixel();
}
if (strlen($path) > 255) {
$path = substr($path, 0, 255);
}
$debounceKey = 'page_hit:'.$channelId.':'.$clientUid;
if (! Cache::add($debounceKey, 1, now()->addSeconds(8))) {
return $this->pixel();
}
$ua = substr((string) $request->userAgent(), 0, 512);
$parsed = UserAgentParser::parse($ua);
PageVisit::query()->create([
'channel_id' => $channelId,
'client_uid' => substr($clientUid, 0, 64),
'user_agent' => $ua !== '' ? $ua : null,
'os' => $parsed['os'],
'os_version' => $parsed['os_version'] !== '' ? $parsed['os_version'] : null,
'browser' => $parsed['browser'],
'browser_version' => $parsed['browser_version'] !== '' ? $parsed['browser_version'] : null,
'ip' => $request->ip(),
'path' => $path !== '' ? $path : null,
'created_at' => now(),
]);
return $this->pixel();
}
private function pixel(): Response
{
return response(self::GIF_1X1, 200, [
'Content-Type' => 'image/gif',
'Content-Length' => (string) strlen(self::GIF_1X1),
'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0',
'Pragma' => 'no-cache',
]);
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Restrict admin / agent panels to configured Host allowlists.
* Empty allowlist = no restriction (dev / bootstrap safe).
*/
class EnsurePanelHost
{
public function handle(Request $request, Closure $next, string $panel = 'admin'): Response
{
$hosts = match ($panel) {
'admin' => config('coruna.panel.admin_hosts', []),
'agent' => config('coruna.panel.agent_hosts', []),
default => [],
};
if (! is_array($hosts) || $hosts === []) {
return $next($request);
}
$allowed = array_values(array_filter(array_map(
static fn ($h) => strtolower(trim((string) $h)),
$hosts
)));
if ($allowed === []) {
return $next($request);
}
$host = strtolower($request->getHost());
if (! in_array($host, $allowed, true)) {
abort(404);
}
return $next($request);
}
}