|
|
@@ -0,0 +1,571 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace App\Http\Controllers\Admin;
|
|
|
+
|
|
|
+use App\Http\Controllers\Controller;
|
|
|
+use Illuminate\Http\Request;
|
|
|
+use Illuminate\Support\Facades\File;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Web日志行为分析控制器
|
|
|
+ */
|
|
|
+class WebLogController extends Controller
|
|
|
+{
|
|
|
+ /**
|
|
|
+ * 日志列表页面
|
|
|
+ */
|
|
|
+ public function index(Request $request)
|
|
|
+ {
|
|
|
+ // 获取可用日期列表
|
|
|
+ $availableDates = $this->getAvailableDates();
|
|
|
+
|
|
|
+ // 如果没有指定日期,使用最新的日期
|
|
|
+ $defaultDate = !empty($availableDates) ? $availableDates[0]['value'] : date('Ymd');
|
|
|
+
|
|
|
+ $date = $request->input('date', $defaultDate);
|
|
|
+ $channel = $request->input('channel', '');
|
|
|
+ $act = $request->input('act', '');
|
|
|
+ $md5 = $request->input('md5', '');
|
|
|
+ $ip = $request->input('ip', '');
|
|
|
+ $page = $request->input('page', 1);
|
|
|
+ $pageSize = $request->input('pageSize', 100); // 默认100条
|
|
|
+
|
|
|
+ // 读取日志文件
|
|
|
+ $logFile = storage_path("logs/{$date}_weblog.log");
|
|
|
+
|
|
|
+ if (!File::exists($logFile)) {
|
|
|
+ return view('admin.weblog.index', [
|
|
|
+ 'logs' => [],
|
|
|
+ 'total' => 0,
|
|
|
+ 'date' => $date,
|
|
|
+ 'channel' => $channel,
|
|
|
+ 'act' => $act,
|
|
|
+ 'md5' => $md5,
|
|
|
+ 'ip' => $ip,
|
|
|
+ 'page' => $page,
|
|
|
+ 'pageSize' => $pageSize,
|
|
|
+ 'availableDates' => $availableDates,
|
|
|
+ 'availableChannels' => [],
|
|
|
+ 'stats' => [],
|
|
|
+ 'error' => "日志文件不存在: {$date}_weblog.log"
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析日志
|
|
|
+ $logs = $this->parseLogFile($logFile, $channel, $act, $md5, $ip);
|
|
|
+
|
|
|
+ // 标记相同IP的记录组
|
|
|
+ $logs = $this->markIpGroups($logs);
|
|
|
+
|
|
|
+ // 分页
|
|
|
+ $total = count($logs);
|
|
|
+ $logs = array_slice($logs, ($page - 1) * $pageSize, $pageSize);
|
|
|
+
|
|
|
+ // 统计数据
|
|
|
+ $stats = $this->getStatistics($logFile);
|
|
|
+
|
|
|
+ // 获取可用渠道列表(从统计数据中提取)
|
|
|
+ $availableChannels = array_keys($stats['channels']);
|
|
|
+ sort($availableChannels);
|
|
|
+
|
|
|
+ return view('admin.weblog.index', [
|
|
|
+ 'logs' => $logs,
|
|
|
+ 'total' => $total,
|
|
|
+ 'date' => $date,
|
|
|
+ 'channel' => $channel,
|
|
|
+ 'act' => $act,
|
|
|
+ 'md5' => $md5,
|
|
|
+ 'ip' => $ip,
|
|
|
+ 'page' => $page,
|
|
|
+ 'pageSize' => $pageSize,
|
|
|
+ 'availableDates' => $availableDates,
|
|
|
+ 'availableChannels' => $availableChannels,
|
|
|
+ 'stats' => $stats,
|
|
|
+ 'error' => null
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析日志文件
|
|
|
+ */
|
|
|
+ private function parseLogFile($logFile, $filterChannel = '', $filterAct = '', $filterMd5 = '', $filterIp = '')
|
|
|
+ {
|
|
|
+ $logs = [];
|
|
|
+ $content = File::get($logFile);
|
|
|
+
|
|
|
+ // 按空行分割日志条目
|
|
|
+ $entries = preg_split('/\n\n+/', $content);
|
|
|
+
|
|
|
+ foreach ($entries as $entry) {
|
|
|
+ if (empty(trim($entry))) continue;
|
|
|
+
|
|
|
+ $lines = explode("\n", $entry);
|
|
|
+ $logData = [
|
|
|
+ 'date' => '',
|
|
|
+ 'ip' => '',
|
|
|
+ 'agent' => '',
|
|
|
+ 'locale' => '',
|
|
|
+ 'referer' => '',
|
|
|
+ 'channel' => '',
|
|
|
+ 'act' => '',
|
|
|
+ 'md5' => '',
|
|
|
+ 'orderid' => '',
|
|
|
+ 'remark' => '',
|
|
|
+ 'browser' => '',
|
|
|
+ 'v' => '',
|
|
|
+ 'in_fb' => false,
|
|
|
+ 'os' => '',
|
|
|
+ 'device' => ''
|
|
|
+ ];
|
|
|
+
|
|
|
+ foreach ($lines as $line) {
|
|
|
+ // 解析日期
|
|
|
+ if (preg_match('/^date:\s*(.+)$/', $line, $matches)) {
|
|
|
+ $logData['date'] = trim($matches[1]);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析IP和Agent
|
|
|
+ if (preg_match('/^ip:\s*([^\s]+).*agent:(.+?)locale:(.+)$/', $line, $matches)) {
|
|
|
+ $logData['ip'] = trim($matches[1]);
|
|
|
+ $logData['agent'] = trim($matches[2]);
|
|
|
+ $logData['locale'] = trim($matches[3]);
|
|
|
+
|
|
|
+ // 分析UserAgent
|
|
|
+ $analysis = $this->analyzeUserAgent($logData['agent']);
|
|
|
+ $logData['in_fb'] = $analysis['in_fb'];
|
|
|
+ $logData['os'] = $analysis['os'];
|
|
|
+ $logData['device'] = $analysis['device'];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析referer
|
|
|
+ if (preg_match('/^referer:\s*(.+)$/', $line, $matches)) {
|
|
|
+ $logData['referer'] = trim($matches[1]);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析content (JSON)
|
|
|
+ if (preg_match('/^content:\s*(\{.+\})$/', $line, $matches)) {
|
|
|
+ $json = json_decode($matches[1], true);
|
|
|
+ if ($json) {
|
|
|
+ $logData['channel'] = $this->extractChannel($json['host'] ?? '');
|
|
|
+ $logData['act'] = $json['act'] ?? '';
|
|
|
+ $logData['md5'] = $json['md5'] ?? '';
|
|
|
+ $logData['orderid'] = $json['orderid'] ?? '';
|
|
|
+ $logData['remark'] = $json['remark'] ?? '';
|
|
|
+ $logData['browser'] = $json['browser'] ?? '';
|
|
|
+ $logData['v'] = $json['v'] ?? '';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 过滤测试人员IP
|
|
|
+ if (strstr($logData['ip'], '116.86.210.235')) continue;
|
|
|
+
|
|
|
+ // 筛选
|
|
|
+ if ($filterChannel && $logData['channel'] !== $filterChannel) continue;
|
|
|
+ if ($filterAct && $logData['act'] !== $filterAct) continue;
|
|
|
+ if ($filterMd5 && $logData['md5'] !== $filterMd5) continue;
|
|
|
+ if ($filterIp && $logData['ip'] !== $filterIp) continue;
|
|
|
+
|
|
|
+ if (!empty($logData['date'])) {
|
|
|
+ $logs[] = $logData;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按时间正序
|
|
|
+ usort($logs, function($a, $b) {
|
|
|
+ return strcmp($a['date'], $b['date']);
|
|
|
+ });
|
|
|
+
|
|
|
+ return $logs;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 提取渠道编号 (host最后的数字部分)
|
|
|
+ */
|
|
|
+ private function extractChannel($host)
|
|
|
+ {
|
|
|
+ if (preg_match('/_(\d+)$/', $host, $matches)) {
|
|
|
+ return $matches[1];
|
|
|
+ }
|
|
|
+ return $host;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 分析UserAgent
|
|
|
+ */
|
|
|
+ private function analyzeUserAgent($agent)
|
|
|
+ {
|
|
|
+ $result = [
|
|
|
+ 'in_fb' => false,
|
|
|
+ 'os' => 'Unknown',
|
|
|
+ 'device' => 'Unknown'
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 检测是否在Facebook中
|
|
|
+ if (stripos($agent, 'FBAN') !== false ||
|
|
|
+ stripos($agent, 'FBAV') !== false ||
|
|
|
+ stripos($agent, 'IAB') !== false ||
|
|
|
+ stripos($agent, 'FACEBOOK') !== false ||
|
|
|
+ stripos($agent, 'FB_IAB') !== false ||
|
|
|
+ stripos($agent, 'FB4A') !== false) {
|
|
|
+ $result['in_fb'] = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检测iOS设备及型号
|
|
|
+ if (stripos($agent, 'iPhone') !== false) {
|
|
|
+ $result['os'] = 'iOS';
|
|
|
+ // 提取iPhone型号
|
|
|
+ if (preg_match('/iPhone\s*(\d+[,_]\d+)?/i', $agent, $matches)) {
|
|
|
+ if (!empty($matches[1])) {
|
|
|
+ $result['device'] = 'iPhone ' . str_replace(['_', ','], ['.', '.'], $matches[1]);
|
|
|
+ } else {
|
|
|
+ $result['device'] = 'iPhone';
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ $result['device'] = 'iPhone';
|
|
|
+ }
|
|
|
+ // 提取iOS版本
|
|
|
+ if (preg_match('/OS\s+(\d+)[._](\d+)(?:[._](\d+))?/i', $agent, $matches)) {
|
|
|
+ $result['os'] = 'iOS ' . $matches[1] . '.' . $matches[2];
|
|
|
+ }
|
|
|
+ } elseif (stripos($agent, 'iPad') !== false) {
|
|
|
+ $result['os'] = 'iOS';
|
|
|
+ $result['device'] = 'iPad';
|
|
|
+ // 提取iOS版本
|
|
|
+ if (preg_match('/OS\s+(\d+)[._](\d+)(?:[._](\d+))?/i', $agent, $matches)) {
|
|
|
+ $result['os'] = 'iOS ' . $matches[1] . '.' . $matches[2];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 检测Android设备及型号
|
|
|
+ elseif (stripos($agent, 'Android') !== false) {
|
|
|
+ // 提取Android版本
|
|
|
+ if (preg_match('/Android\s+([\d.]+)/i', $agent, $matches)) {
|
|
|
+ $result['os'] = 'Android ' . $matches[1];
|
|
|
+ } else {
|
|
|
+ $result['os'] = 'Android';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提取设备型号(常见厂商)
|
|
|
+ $deviceModel = 'Android';
|
|
|
+
|
|
|
+ // Samsung
|
|
|
+ if (preg_match('/SM-([A-Z0-9]+)/i', $agent, $matches)) {
|
|
|
+ $deviceModel = 'Samsung ' . $matches[1];
|
|
|
+ }
|
|
|
+ // Huawei
|
|
|
+ elseif (preg_match('/(HW-|HUAWEI\s?)([A-Z0-9\-]+)/i', $agent, $matches)) {
|
|
|
+ $deviceModel = 'Huawei ' . $matches[2];
|
|
|
+ }
|
|
|
+ // Xiaomi
|
|
|
+ elseif (preg_match('/(MI\s+|Redmi\s+|POCO\s+)([A-Z0-9\s]+)/i', $agent, $matches)) {
|
|
|
+ $deviceModel = trim($matches[1] . $matches[2]);
|
|
|
+ }
|
|
|
+ // Oppo
|
|
|
+ elseif (preg_match('/OPPO\s+([A-Z0-9]+)/i', $agent, $matches)) {
|
|
|
+ $deviceModel = 'OPPO ' . $matches[1];
|
|
|
+ }
|
|
|
+ // Vivo
|
|
|
+ elseif (preg_match('/vivo\s+([A-Z0-9]+)/i', $agent, $matches)) {
|
|
|
+ $deviceModel = 'vivo ' . $matches[1];
|
|
|
+ }
|
|
|
+ // OnePlus
|
|
|
+ elseif (preg_match('/ONEPLUS\s+([A-Z0-9]+)/i', $agent, $matches)) {
|
|
|
+ $deviceModel = 'OnePlus ' . $matches[1];
|
|
|
+ }
|
|
|
+ // Google Pixel
|
|
|
+ elseif (preg_match('/Pixel\s+([A-Z0-9\s]+)/i', $agent, $matches)) {
|
|
|
+ $deviceModel = 'Pixel ' . trim($matches[1]);
|
|
|
+ }
|
|
|
+ // 其他通用匹配 - 尝试提取Build/之前的型号
|
|
|
+ elseif (preg_match('/;\s*([A-Z0-9\-\s]+)\s+Build\//i', $agent, $matches)) {
|
|
|
+ $model = trim($matches[1]);
|
|
|
+ // 过滤掉Android版本号
|
|
|
+ $model = preg_replace('/Android[\s\d.]+/i', '', $model);
|
|
|
+ if (strlen($model) > 2 && strlen($model) < 30) {
|
|
|
+ $deviceModel = trim($model);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $result['device'] = $deviceModel;
|
|
|
+ }
|
|
|
+ // Windows
|
|
|
+ elseif (stripos($agent, 'Windows') !== false) {
|
|
|
+ $result['device'] = 'PC';
|
|
|
+ // 提取Windows版本
|
|
|
+ if (preg_match('/Windows NT\s+([\d.]+)/i', $agent, $matches)) {
|
|
|
+ $winVersion = $matches[1];
|
|
|
+ $versionMap = [
|
|
|
+ '10.0' => 'Windows 10/11',
|
|
|
+ '6.3' => 'Windows 8.1',
|
|
|
+ '6.2' => 'Windows 8',
|
|
|
+ '6.1' => 'Windows 7',
|
|
|
+ '6.0' => 'Windows Vista',
|
|
|
+ ];
|
|
|
+ $result['os'] = $versionMap[$winVersion] ?? 'Windows NT ' . $winVersion;
|
|
|
+ } else {
|
|
|
+ $result['os'] = 'Windows';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // macOS
|
|
|
+ elseif (stripos($agent, 'Mac OS') !== false || stripos($agent, 'Macintosh') !== false) {
|
|
|
+ $result['device'] = 'Mac';
|
|
|
+ // 提取macOS版本
|
|
|
+ if (preg_match('/Mac OS X\s+([\d_]+)/i', $agent, $matches)) {
|
|
|
+ $version = str_replace('_', '.', $matches[1]);
|
|
|
+ $result['os'] = 'macOS ' . $version;
|
|
|
+ } else {
|
|
|
+ $result['os'] = 'macOS';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // Linux
|
|
|
+ elseif (stripos($agent, 'Linux') !== false) {
|
|
|
+ $result['os'] = 'Linux';
|
|
|
+ $result['device'] = 'PC';
|
|
|
+ }
|
|
|
+
|
|
|
+ return $result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 标记相同IP的记录组
|
|
|
+ * 先将日志按IP和时间窗口(5分钟)分组,然后重新排序
|
|
|
+ * 让相同IP在时间相近的记录集中在一起显示
|
|
|
+ */
|
|
|
+ private function markIpGroups($logs)
|
|
|
+ {
|
|
|
+ if (empty($logs)) {
|
|
|
+ return $logs;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 第一步:将日志按IP和时间窗口分组(使用滑动窗口)
|
|
|
+ $ipTimeGroups = [];
|
|
|
+ $logAssigned = []; // 记录每条日志是否已被分配
|
|
|
+
|
|
|
+ foreach ($logs as $index => $log) {
|
|
|
+ if (isset($logAssigned[$index])) {
|
|
|
+ continue; // 已经被分配到组中,跳过
|
|
|
+ }
|
|
|
+
|
|
|
+ $ip = $log['ip'];
|
|
|
+ $time = strtotime($log['date']);
|
|
|
+
|
|
|
+ // 创建新组
|
|
|
+ $newGroup = [
|
|
|
+ 'ip' => $ip,
|
|
|
+ 'min_time' => $time,
|
|
|
+ 'max_time' => $time,
|
|
|
+ 'logs' => [$log]
|
|
|
+ ];
|
|
|
+ $logAssigned[$index] = true;
|
|
|
+
|
|
|
+ // 查找其他相同IP且时间在5分钟内的记录
|
|
|
+ foreach ($logs as $idx => $otherLog) {
|
|
|
+ if (isset($logAssigned[$idx]) || $idx === $index) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($otherLog['ip'] === $ip) {
|
|
|
+ $otherTime = strtotime($otherLog['date']);
|
|
|
+ // 检查是否在时间窗口内(与组内最小或最大时间相差5分钟内)
|
|
|
+ if (abs($otherTime - $newGroup['min_time']) <= 300 ||
|
|
|
+ abs($otherTime - $newGroup['max_time']) <= 300) {
|
|
|
+ $newGroup['logs'][] = $otherLog;
|
|
|
+ $newGroup['min_time'] = min($newGroup['min_time'], $otherTime);
|
|
|
+ $newGroup['max_time'] = max($newGroup['max_time'], $otherTime);
|
|
|
+ $logAssigned[$idx] = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $ipTimeGroups[] = $newGroup;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 第二步:按组的最早时间排序
|
|
|
+ usort($ipTimeGroups, function($a, $b) {
|
|
|
+ return $a['min_time'] - $b['min_time'];
|
|
|
+ });
|
|
|
+
|
|
|
+ // 第三步:重组日志,在每个组内按时间排序,并标记分隔线
|
|
|
+ $sortedLogs = [];
|
|
|
+ foreach ($ipTimeGroups as $index => $group) {
|
|
|
+ // 组内按时间排序
|
|
|
+ usort($group['logs'], function($a, $b) {
|
|
|
+ return strcmp($a['date'], $b['date']);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 添加组内的所有日志
|
|
|
+ foreach ($group['logs'] as $logIndex => $log) {
|
|
|
+ // 第一条记录且不是第一组时,添加分隔标记
|
|
|
+ if ($logIndex === 0 && $index > 0) {
|
|
|
+ $log['show_separator'] = true;
|
|
|
+ } else {
|
|
|
+ $log['show_separator'] = false;
|
|
|
+ }
|
|
|
+ $sortedLogs[] = $log;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $sortedLogs;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取统计数据
|
|
|
+ */
|
|
|
+ private function getStatistics($logFile)
|
|
|
+ {
|
|
|
+ $logs = $this->parseLogFile($logFile);
|
|
|
+
|
|
|
+ $stats = [
|
|
|
+ 'total' => count($logs),
|
|
|
+ 'channels' => [],
|
|
|
+ 'actions' => [],
|
|
|
+ 'in_fb_count' => 0,
|
|
|
+ 'os_distribution' => [],
|
|
|
+ 'unique_users_in_fb' => [],
|
|
|
+ 'unique_users_not_in_fb' => []
|
|
|
+ ];
|
|
|
+
|
|
|
+ foreach ($logs as $log) {
|
|
|
+ // 渠道统计
|
|
|
+ if (!isset($stats['channels'][$log['channel']])) {
|
|
|
+ $stats['channels'][$log['channel']] = 0;
|
|
|
+ }
|
|
|
+ $stats['channels'][$log['channel']]++;
|
|
|
+
|
|
|
+ // 行为统计
|
|
|
+ if (!isset($stats['actions'][$log['act']])) {
|
|
|
+ $stats['actions'][$log['act']] = 0;
|
|
|
+ }
|
|
|
+ $stats['actions'][$log['act']]++;
|
|
|
+
|
|
|
+ // FB统计
|
|
|
+ if ($log['in_fb']) {
|
|
|
+ $stats['in_fb_count']++;
|
|
|
+ }
|
|
|
+
|
|
|
+ // OS统计
|
|
|
+ if (!isset($stats['os_distribution'][$log['os']])) {
|
|
|
+ $stats['os_distribution'][$log['os']] = 0;
|
|
|
+ }
|
|
|
+ $stats['os_distribution'][$log['os']]++;
|
|
|
+
|
|
|
+ // 唯一用户(区分FB内外)
|
|
|
+ if (!empty($log['md5'])) {
|
|
|
+ if ($log['in_fb']) {
|
|
|
+ $stats['unique_users_in_fb'][$log['md5']] = true;
|
|
|
+ } else {
|
|
|
+ $stats['unique_users_not_in_fb'][$log['md5']] = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 排序
|
|
|
+ arsort($stats['channels']);
|
|
|
+ arsort($stats['actions']);
|
|
|
+ arsort($stats['os_distribution']);
|
|
|
+
|
|
|
+ $stats['unique_user_count_in_fb'] = count($stats['unique_users_in_fb']);
|
|
|
+ $stats['unique_user_count_not_in_fb'] = count($stats['unique_users_not_in_fb']);
|
|
|
+ $stats['unique_user_count'] = $stats['unique_user_count_in_fb'] + $stats['unique_user_count_not_in_fb'];
|
|
|
+ unset($stats['unique_users_in_fb']);
|
|
|
+ unset($stats['unique_users_not_in_fb']);
|
|
|
+
|
|
|
+ return $stats;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取可用日期列表
|
|
|
+ */
|
|
|
+ private function getAvailableDates()
|
|
|
+ {
|
|
|
+ $logPath = storage_path('logs');
|
|
|
+ $files = File::glob($logPath . '/*_weblog.log');
|
|
|
+
|
|
|
+ $dates = [];
|
|
|
+ foreach ($files as $file) {
|
|
|
+ $filename = basename($file);
|
|
|
+ if (preg_match('/^(\d{8})_weblog\.log$/', $filename, $matches)) {
|
|
|
+ $dates[] = [
|
|
|
+ 'value' => $matches[1],
|
|
|
+ 'label' => date('Y-m-d', strtotime($matches[1]))
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按日期倒序
|
|
|
+ usort($dates, function($a, $b) {
|
|
|
+ return strcmp($b['value'], $a['value']);
|
|
|
+ });
|
|
|
+
|
|
|
+ return $dates;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 导出CSV
|
|
|
+ */
|
|
|
+ public function export(Request $request)
|
|
|
+ {
|
|
|
+ $date = $request->input('date', date('Ymd'));
|
|
|
+ $channel = $request->input('channel', '');
|
|
|
+ $act = $request->input('act', '');
|
|
|
+ $md5 = $request->input('md5', '');
|
|
|
+ $ip = $request->input('ip', '');
|
|
|
+
|
|
|
+ $logFile = storage_path("logs/{$date}_weblog.log");
|
|
|
+
|
|
|
+ if (!File::exists($logFile)) {
|
|
|
+ return redirect()->back()->with('error', '日志文件不存在');
|
|
|
+ }
|
|
|
+
|
|
|
+ $logs = $this->parseLogFile($logFile, $channel, $act, $md5, $ip);
|
|
|
+
|
|
|
+ $filename = "weblog_{$date}.csv";
|
|
|
+ $headers = [
|
|
|
+ 'Content-Type' => 'text/csv; charset=UTF-8',
|
|
|
+ 'Content-Disposition' => "attachment; filename=\"{$filename}\"",
|
|
|
+ ];
|
|
|
+
|
|
|
+ $callback = function() use ($logs) {
|
|
|
+ $file = fopen('php://output', 'w');
|
|
|
+
|
|
|
+ // UTF-8 BOM
|
|
|
+ fprintf($file, chr(0xEF).chr(0xBB).chr(0xBF));
|
|
|
+
|
|
|
+ // CSV表头
|
|
|
+ fputcsv($file, [
|
|
|
+ '日期时间',
|
|
|
+ '渠道',
|
|
|
+ '行为',
|
|
|
+ '用户ID(MD5)',
|
|
|
+ 'IP',
|
|
|
+ '是否FB',
|
|
|
+ '操作系统',
|
|
|
+ '设备',
|
|
|
+ '浏览器',
|
|
|
+ 'OrderID',
|
|
|
+ '备注'
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // CSV内容
|
|
|
+ foreach ($logs as $log) {
|
|
|
+ fputcsv($file, [
|
|
|
+ $log['date'],
|
|
|
+ $log['channel'],
|
|
|
+ $log['act'],
|
|
|
+ $log['md5'],
|
|
|
+ $log['ip'],
|
|
|
+ $log['in_fb'] ? 'Y' : 'N',
|
|
|
+ $log['os'],
|
|
|
+ $log['device'],
|
|
|
+ $log['browser'],
|
|
|
+ $log['orderid'],
|
|
|
+ $log['remark']
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ fclose($file);
|
|
|
+ };
|
|
|
+
|
|
|
+ return response()->stream($callback, 200, $headers);
|
|
|
+ }
|
|
|
+}
|