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;
 | ||
|         }
 | ||
|     }
 | ||
| }
 | ||
|     
 |