yunzer/app/service/VisitStatsService.php
2025-06-07 09:15:14 +08:00

214 lines
7.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\service;
use think\facade\Cache;
use think\facade\Request;
use think\facade\Db;
use think\facade\Log;
class VisitStatsService
{
// Redis实例
protected $redis;
// 键名前缀
protected $prefix = 'stats:';
public function __construct()
{
try {
// 获取Redis处理器
$this->redis = Cache::store('redis')->handler();
} catch (\Exception $e) {
// Redis连接失败使用文件缓存
$this->redis = Cache::store('file')->handler();
Log::error('Redis连接失败已切换到文件缓存' . $e->getMessage());
}
}
/**
* 记录访问并更新统计数据
*/
public function recordVisit(string $page = 'home', string $userId = null): array
{
try {
$date = date('Y-m-d');
$hour = date('H');
$userId = $userId ?? Request::ip();
// 使用管道提高性能
$pipe = $this->redis->multi();
// 总访问量(PV)
$pipe->incr($this->prefix.'total_visits');
// 每日访问量
$pipe->incr($this->prefix.'daily:'.$date);
// 页面统计
$pipe->zIncrBy($this->prefix.'page_views', 1, $page);
// UV统计(使用HyperLogLog节省内存)
$pipe->pfAdd($this->prefix.'uv:'.$date, [$userId]);
// 时段统计
$pipe->hIncrBy($this->prefix.'hourly:'.$date, $hour, 1);
// 执行所有命令
$result = $pipe->exec();
// 使用Redis锁控制更新频率每5分钟更新一次数据库
$lockKey = $this->prefix.'update_lock:'.$date;
if (!$this->redis->exists($lockKey)) {
$this->redis->setex($lockKey, 300, 1); // 5分钟锁
$this->updateDailyStats($date, [
'total_visits' => $result[0],
'daily_visits' => $result[1],
'unique_visitors' => $this->getUniqueVisitors($date)
]);
}
return [
'total' => $result[0],
'daily' => $result[1],
'page' => $result[2],
'uv' => $result[3],
'hourly'=> $result[4]
];
} catch (\Exception $e) {
Log::error('访问统计失败:' . $e->getMessage());
return [
'total' => 0,
'daily' => 0,
'page' => 0,
'uv' => 0,
'hourly'=> 0
];
}
}
/**
* 更新每日统计数据
*/
protected function updateDailyStats(string $date, array $stats)
{
try {
// 获取其他统计数据
$otherStats = [
'total_users' => Db::name('users')->count(),
'new_users' => Db::name('users')->whereDay('create_time', $date)->count(),
'total_articles' => Db::name('articles')->where('delete_time', null)->count(),
'daily_articles' => Db::name('articles')->whereDay('create_time', $date)->count(),
'article_views' => Db::name('articles')->whereDay('update_time', $date)->sum('views'),
'total_resources' => Db::name('resources')->where('delete_time', null)->count(),
'daily_resources' => Db::name('resources')->whereDay('create_time', $date)->count(),
'resource_downloads' => Db::name('resources')->whereDay('update_time', $date)->sum('downloads')
];
// 只在调试模式下记录日志
if (config('app.debug')) {
Log::info('统计数据:' . json_encode($otherStats, JSON_UNESCAPED_UNICODE));
}
// 合并统计数据
$stats = array_merge($stats, $otherStats);
// 检查记录是否存在
$exists = Db::name('daily_stats')->where('date', $date)->find();
if ($exists) {
// 更新已存在的记录
Db::name('daily_stats')->where('date', $date)->update([
'total_users' => $stats['total_users'],
'new_users' => $stats['new_users'],
'total_visits' => $stats['total_visits'],
'daily_visits' => $stats['daily_visits'],
'unique_visitors' => $stats['unique_visitors'],
'total_articles' => $stats['total_articles'],
'daily_articles' => $stats['daily_articles'],
'article_views' => $stats['article_views'],
'total_resources' => $stats['total_resources'],
'daily_resources' => $stats['daily_resources'],
'resource_downloads' => $stats['resource_downloads']
]);
} else {
// 插入新记录
Db::name('daily_stats')->insert([
'date' => $date,
'total_users' => $stats['total_users'],
'new_users' => $stats['new_users'],
'total_visits' => $stats['total_visits'],
'daily_visits' => $stats['daily_visits'],
'unique_visitors' => $stats['unique_visitors'],
'total_articles' => $stats['total_articles'],
'daily_articles' => $stats['daily_articles'],
'article_views' => $stats['article_views'],
'total_resources' => $stats['total_resources'],
'daily_resources' => $stats['daily_resources'],
'resource_downloads' => $stats['resource_downloads']
]);
}
} catch (\Exception $e) {
Log::error('更新统计数据失败:' . $e->getMessage());
}
}
/**
* 获取总访问量
*/
public function getTotalVisits(): int
{
try {
return (int)$this->redis->get($this->prefix.'total_visits');
} catch (\Exception $e) {
Log::error('获取总访问量失败:' . $e->getMessage());
return 0;
}
}
/**
* 获取当日访问量
*/
public function getDailyVisits(string $date = null): int
{
try {
$date = $date ?? date('Y-m-d');
return (int)$this->redis->get($this->prefix.'daily:'.$date);
} catch (\Exception $e) {
Log::error('获取当日访问量失败:' . $e->getMessage());
return 0;
}
}
/**
* 获取独立访客数(UV)
*/
public function getUniqueVisitors(string $date = null): int
{
try {
$date = $date ?? date('Y-m-d');
return $this->redis->pfCount($this->prefix.'uv:'.$date);
} catch (\Exception $e) {
Log::error('获取独立访客数失败:' . $e->getMessage());
return 0;
}
}
}