214 lines
7.5 KiB
PHP
214 lines
7.5 KiB
PHP
<?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;
|
||
}
|
||
}
|
||
}
|
||
|