feat: new chain
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Device;
|
||||
use App\Models\PageVisit;
|
||||
use App\Models\User;
|
||||
use App\Support\AgentScope;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DashboardStatsService
|
||||
{
|
||||
/**
|
||||
* @param array{
|
||||
* range?: string,
|
||||
* channel_id?: string,
|
||||
* channel_exact?: bool,
|
||||
* agent_user_id?: int,
|
||||
* agent?: ?User
|
||||
* } $filters
|
||||
* @return array{
|
||||
* total: int,
|
||||
* new_count: int,
|
||||
* active_count: int,
|
||||
* pv: int,
|
||||
* uv: int,
|
||||
* effective_pv: int,
|
||||
* effective_uv: int,
|
||||
* by_os: list<array{label: string, pv: int, uv: int, pct: float}>,
|
||||
* by_ios_version: list<array{label: string, pv: int, uv: int, pct: float}>,
|
||||
* range: string,
|
||||
* from: string,
|
||||
* to: string
|
||||
* }
|
||||
*/
|
||||
public function collect(array $filters = []): array
|
||||
{
|
||||
$range = (string) ($filters['range'] ?? '30d');
|
||||
[$from, $to] = $this->rangeBounds($range);
|
||||
|
||||
$channelId = trim((string) ($filters['channel_id'] ?? ''));
|
||||
$channelExact = (bool) ($filters['channel_exact'] ?? false);
|
||||
$agent = $filters['agent'] ?? null;
|
||||
$agentUserId = (int) ($filters['agent_user_id'] ?? 0);
|
||||
|
||||
$base = Device::query();
|
||||
AgentScope::applyDeviceChannelScope($base, $agent);
|
||||
if ($agent === null && $agentUserId > 0) {
|
||||
AgentScope::applyAgentUserFilter($base, $agentUserId);
|
||||
}
|
||||
if ($channelId !== '') {
|
||||
if ($channelExact) {
|
||||
$base->where('channel_id', $channelId);
|
||||
} else {
|
||||
$base->where('channel_id', 'like', '%'.$channelId.'%');
|
||||
}
|
||||
}
|
||||
|
||||
$total = (clone $base)->count();
|
||||
$newCount = (clone $base)->whereBetween('created_at', [$from, $to])->count();
|
||||
$activeCount = (clone $base)->whereBetween('updated_at', [$from, $to])->count();
|
||||
|
||||
$visits = PageVisit::query();
|
||||
AgentScope::applyChannelIdScope($visits, $agent);
|
||||
if ($agent === null && $agentUserId > 0) {
|
||||
AgentScope::applyChannelIdAgentUserFilter($visits, $agentUserId);
|
||||
}
|
||||
if ($channelId !== '') {
|
||||
if ($channelExact) {
|
||||
$visits->where('channel_id', $channelId);
|
||||
} else {
|
||||
$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');
|
||||
$effectiveVisits = (clone $visits)->tap(fn (Builder $q) => $this->applyEffectiveFilter($q));
|
||||
$effectivePv = (clone $effectiveVisits)->count();
|
||||
$effectiveUv = (int) (clone $effectiveVisits)->selectRaw('COUNT(DISTINCT client_uid) as aggregate')->value('aggregate');
|
||||
|
||||
$iosVisits = (clone $visits)->where('os', 'iOS');
|
||||
$iosPv = (clone $iosVisits)->count();
|
||||
$iosUv = (int) (clone $iosVisits)->selectRaw('COUNT(DISTINCT client_uid) as aggregate')->value('aggregate');
|
||||
|
||||
$otherVisits = (clone $visits)->where(function (Builder $q) {
|
||||
$q->whereNull('os')->orWhere('os', '!=', 'iOS');
|
||||
});
|
||||
$otherPv = (clone $otherVisits)->count();
|
||||
$otherUv = (int) (clone $otherVisits)->selectRaw('COUNT(DISTINCT client_uid) as aggregate')->value('aggregate');
|
||||
|
||||
$byOs = [
|
||||
$this->row('iOS', $iosPv, $iosUv, $pv),
|
||||
$this->row('其他', $otherPv, $otherUv, $pv),
|
||||
];
|
||||
|
||||
$byIosVersion = (clone $iosVisits)
|
||||
->select('os_version')
|
||||
->selectRaw('COUNT(*) as pv')
|
||||
->selectRaw('COUNT(DISTINCT client_uid) as uv')
|
||||
->groupBy('os_version')
|
||||
->orderByDesc('pv')
|
||||
->limit(20)
|
||||
->get()
|
||||
->map(fn ($r) => $this->row(
|
||||
((string) ($r->os_version ?? '')) !== '' ? (string) $r->os_version : 'Unknown',
|
||||
(int) $r->pv,
|
||||
(int) $r->uv,
|
||||
$iosPv > 0 ? $iosPv : $pv,
|
||||
))
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'new_count' => $newCount,
|
||||
'active_count' => $activeCount,
|
||||
'pv' => $pv,
|
||||
'uv' => $uv,
|
||||
'effective_pv' => $effectivePv,
|
||||
'effective_uv' => $effectiveUv,
|
||||
'by_os' => $byOs,
|
||||
'by_ios_version' => $byIosVersion,
|
||||
'range' => $range,
|
||||
'from' => $from->toDateTimeString(),
|
||||
'to' => $to->toDateTimeString(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Map telegram /data day token to dashboard range key.
|
||||
* Empty / omitted → today.
|
||||
*/
|
||||
public function rangeFromDays(?string $days): string
|
||||
{
|
||||
$days = $days === null ? '' : trim($days);
|
||||
if ($days === '' || $days === '1') {
|
||||
return 'today';
|
||||
}
|
||||
|
||||
return match ($days) {
|
||||
'7' => '7d',
|
||||
'30' => '30d',
|
||||
default => throw new \InvalidArgumentException('Days must be 1, 7, or 30'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: Carbon, 1: Carbon}
|
||||
*/
|
||||
public function rangeBounds(string $range): array
|
||||
{
|
||||
$now = Carbon::now();
|
||||
|
||||
return match ($range) {
|
||||
'today', '1', '1d' => [$now->copy()->startOfDay(), $now->copy()->endOfDay()],
|
||||
'yesterday' => [
|
||||
$now->copy()->subDay()->startOfDay(),
|
||||
$now->copy()->subDay()->endOfDay(),
|
||||
],
|
||||
'7d', '7' => [$now->copy()->subDays(6)->startOfDay(), $now->copy()->endOfDay()],
|
||||
default => [$now->copy()->subDays(29)->startOfDay(), $now->copy()->endOfDay()],
|
||||
};
|
||||
}
|
||||
|
||||
private function applyEffectiveFilter(Builder $query): void
|
||||
{
|
||||
$query->where('os', 'iOS')
|
||||
->where('browser', 'Safari')
|
||||
->whereNotNull('os_version')
|
||||
->where('os_version', '!=', '');
|
||||
|
||||
$driver = DB::connection()->getDriverName();
|
||||
if ($driver === 'sqlite') {
|
||||
$query->whereRaw('CAST(os_version AS INTEGER) < 17');
|
||||
} else {
|
||||
$query->whereRaw("CAST(SUBSTRING_INDEX(os_version, '.', 1) AS UNSIGNED) < 17");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{label: string, pv: int, uv: int, pct: float}
|
||||
*/
|
||||
private function row(string $label, int $pv, int $uv, int $totalPv): array
|
||||
{
|
||||
$pct = $totalPv > 0 ? round(($pv / $totalPv) * 100, 1) : 0.0;
|
||||
|
||||
return [
|
||||
'label' => $label,
|
||||
'pv' => $pv,
|
||||
'uv' => $uv,
|
||||
'pct' => $pct,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user