Files
CLOAK/log_analytics.php
T
2026-06-15 22:42:59 +08:00

272 lines
12 KiB
PHP
Executable File

<?php
require_once 'cong.php';
session_start();
if (empty($_SESSION['password']) || $_SESSION['password'] != LOGIN_PASSWORD) {
unset($_SESSION['password']);
unset($_SESSION['check_result']);
echo("<script language=javascript>alert('密码错误!');window.location='ip_check_edit.php'; </script>");
exit;
}
$defaultFrom = date('Y-m-d', strtotime('-6 days'));
$defaultTo = date('Y-m-d');
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>日志数据分析</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="//cdn.staticfile.org/layui/2.9.2/css/layui.css" rel="stylesheet">
<script src="//cdn.staticfile.org/Chart.js/4.4.1/chart.umd.min.js"></script>
<style>
:root {
--log-primary: #16baaa;
--log-primary-dark: #0e8d81;
--log-bg: #f0f2f5;
--log-card: #ffffff;
--log-border: #e8ecf1;
--log-text: #2c3e50;
--log-muted: #8c98a4;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--log-bg); color: var(--log-text); font-family: 'Segoe UI', system-ui, sans-serif; }
.log-page { max-width: 1680px; margin: 0 auto; padding: 24px 28px 40px; }
.log-page-header { display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-bottom: 20px; }
.log-page-header h1 { margin: 0; font-size: 1.35rem; font-weight: 600; }
.log-page-header p { margin: 4px 0 0; font-size: .85rem; color: var(--log-muted); }
.log-nav { display: flex; gap: 8px; flex-wrap: wrap; }
.log-nav a {
display: inline-block; padding: 6px 14px; border-radius: 6px; font-size: .85rem;
text-decoration: none; border: 1px solid var(--log-border); background: #fff; color: var(--log-text);
}
.log-nav a.active { background: var(--log-primary); color: #fff; border-color: var(--log-primary); }
.log-card {
background: var(--log-card); border: 1px solid var(--log-border); border-radius: 10px;
box-shadow: 0 2px 12px rgba(15, 23, 42, .04); margin-bottom: 16px; overflow: hidden;
}
.log-card-title { padding: 14px 20px; font-weight: 600; border-bottom: 1px solid var(--log-border); background: #fafbfc; }
.log-card-body { padding: 18px 20px; }
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; }
.stat-item {
background: linear-gradient(135deg, #f8fafc 0%, #eef3f9 100%);
border: 1px solid var(--log-border); border-radius: 8px; padding: 14px 16px; text-align: center;
}
.stat-item .val { font-size: 1.5rem; font-weight: 700; color: var(--log-primary-dark); }
.stat-item .lbl { font-size: .78rem; color: var(--log-muted); margin-top: 4px; }
.chart-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
.chart-box { position: relative; height: 280px; }
.chart-box-wide { grid-column: 1 / -1; height: 300px; }
.conclusion-list { margin: 0; padding: 0; list-style: none; }
.conclusion-list li {
padding: 10px 14px; margin-bottom: 8px; border-radius: 8px; font-size: .9rem; line-height: 1.5;
background: #f0faf9; border-left: 3px solid var(--log-primary);
}
.conclusion-list li.alert { background: #fff8e6; border-left-color: #e6a23c; }
.notice-bar {
padding: 10px 16px; margin-bottom: 16px; border-radius: 8px; font-size: .85rem;
background: #e8f4fd; color: #0c5460; border: 1px solid #bee5eb;
}
.loading-mask { text-align: center; padding: 60px; color: var(--log-muted); }
@media (max-width: 900px) { .chart-grid { grid-template-columns: 1fr; } .chart-box-wide { grid-column: auto; } }
</style>
</head>
<body>
<div class="log-page">
<div class="log-page-header">
<div>
<h1>日志数据分析</h1>
<p>判定结果、理由、国家与时序分布 · 自动分析结论</p>
</div>
<div class="log-nav">
<a href="table.php">访问日志</a>
<a href="log_analytics.php" class="active">数据分析</a>
<a href="dashboard.php">配置中心</a>
</div>
</div>
<div class="notice-bar">数据来源于 visitor_logs 表;若已开启异步写入,日志可能有短暂延迟(取决于队列 Worker 落库进度)。</div>
<div class="log-card">
<div class="log-card-body">
<form class="layui-form layui-row layui-col-space16" id="filter-form">
<div class="layui-col-md3">
<input type="text" name="date_from" id="date_from" placeholder="开始日期" class="layui-input" value="<?php echo htmlspecialchars($defaultFrom); ?>">
</div>
<div class="layui-col-md3">
<input type="text" name="date_to" id="date_to" placeholder="结束日期" class="layui-input" value="<?php echo htmlspecialchars($defaultTo); ?>">
</div>
<div class="layui-col-md3">
<input type="text" name="domain" placeholder="域名(可选)" class="layui-input">
</div>
<div class="layui-col-md3">
<input type="text" name="campagin_id" placeholder="策略 ID(可选)" class="layui-input">
</div>
<div class="layui-col-md12" style="margin-top:8px;">
<button type="button" class="layui-btn" id="btn-query" style="background:var(--log-primary);">查询分析</button>
</div>
</form>
</div>
</div>
<div id="content-area">
<div class="loading-mask">加载中…</div>
</div>
</div>
<script src="//cdn.staticfile.org/layui/2.9.2/layui.js"></script>
<script>
layui.use(['laydate'], function(){
var laydate = layui.laydate;
laydate.render({ elem: '#date_from', type: 'date' });
laydate.render({ elem: '#date_to', type: 'date' });
var charts = [];
function destroyCharts() {
charts.forEach(function(c){ try { c.destroy(); } catch(e){} });
charts = [];
}
function renderStats(summary) {
return '<div class="log-card"><div class="log-card-title">概览</div><div class="log-card-body"><div class="stat-grid">' +
statBox(summary.total, '总访问') +
statBox(summary.pass, '通过') +
statBox(summary.block, '拦截') +
statBox(summary.wait, '检测中') +
statBox(summary.pass_rate + '%', '通过率') +
statBox(summary.block_rate + '%', '拦截率') +
'</div></div></div>';
}
function statBox(val, lbl) {
return '<div class="stat-item"><div class="val">' + val + '</div><div class="lbl">' + lbl + '</div></div>';
}
function renderConclusions(list) {
var html = '<div class="log-card"><div class="log-card-title">分析结论</div><div class="log-card-body"><ul class="conclusion-list">';
(list || []).forEach(function(line) {
var cls = (line.indexOf('告警') !== -1 || line.indexOf('关注') !== -1) ? 'alert' : '';
html += '<li class="' + cls + '">' + escapeHtml(line) + '</li>';
});
html += '</ul></div></div>';
return html;
}
function escapeHtml(s) {
var d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
function renderChartArea(data) {
var hasTiming = data.timing && data.timing.stages && data.timing.stages.length > 0;
return '<div class="log-card"><div class="log-card-title">可视化图表</div><div class="log-card-body chart-grid">' +
'<div class="chart-box"><canvas id="chart-result"></canvas></div>' +
'<div class="chart-box"><canvas id="chart-country"></canvas></div>' +
'<div class="chart-box chart-box-wide"><canvas id="chart-hour"></canvas></div>' +
'<div class="chart-box chart-box-wide"><canvas id="chart-reason"></canvas></div>' +
(hasTiming ? '<div class="chart-box chart-box-wide"><canvas id="chart-timing"></canvas></div>' : '') +
'</div></div>';
}
function makeChart(id, config) {
var el = document.getElementById(id);
if (!el) return;
charts.push(new Chart(el, config));
}
function buildCharts(data) {
var colors = ['#16baaa','#e74c3c','#f39c12','#3498db','#9b59b6','#1abc9c','#95a5a6'];
if (data.by_result && data.by_result.length) {
makeChart('chart-result', {
type: 'doughnut',
data: {
labels: data.by_result.map(function(r){ return r.label; }),
datasets: [{ data: data.by_result.map(function(r){ return r.count; }), backgroundColor: colors }]
},
options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: '判定结果分布' } } }
});
}
if (data.by_country && data.by_country.length) {
makeChart('chart-country', {
type: 'bar',
data: {
labels: data.by_country.map(function(r){ return r.country; }),
datasets: [{ label: '访问量', data: data.by_country.map(function(r){ return r.count; }), backgroundColor: '#16baaa' }]
},
options: { responsive: true, maintainAspectRatio: false, indexAxis: 'y', plugins: { title: { display: true, text: '国家 Top10' } } }
});
}
if (data.by_hour && data.by_hour.length) {
makeChart('chart-hour', {
type: 'line',
data: {
labels: data.by_hour.map(function(r){ return r.hour; }),
datasets: [{ label: '访问量', data: data.by_hour.map(function(r){ return r.count; }), borderColor: '#3498db', backgroundColor: 'rgba(52,152,219,.1)', fill: true, tension: 0.3 }]
},
options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: '24 小时访问分布' } } }
});
}
if (data.by_reason && data.by_reason.length) {
makeChart('chart-reason', {
type: 'bar',
data: {
labels: data.by_reason.map(function(r){ return r.reason.length > 24 ? r.reason.slice(0,24)+'…' : r.reason; }),
datasets: [{ label: '次数', data: data.by_reason.map(function(r){ return r.count; }), backgroundColor: '#e74c3c' }]
},
options: {
responsive: true, maintainAspectRatio: false, indexAxis: 'y',
plugins: { title: { display: true, text: '拦截/待判定理由 Top15' }, tooltip: { callbacks: { title: function(items){ return data.by_reason[items[0].dataIndex].reason; } } } }
}
});
}
if (data.timing && data.timing.stages && data.timing.stages.length) {
var topStages = data.timing.stages.slice(0, 10);
makeChart('chart-timing', {
type: 'bar',
data: {
labels: topStages.map(function(s){ return s.stage; }),
datasets: [{ label: '平均 ms', data: topStages.map(function(s){ return s.avg_ms; }), backgroundColor: '#9b59b6' }]
},
options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: '判定阶段平均耗时(抽样 ' + data.timing.sample_size + ' 条)' } } }
});
}
}
function loadData() {
var form = document.getElementById('filter-form');
var params = new URLSearchParams(new FormData(form));
destroyCharts();
document.getElementById('content-area').innerHTML = '<div class="loading-mask">分析中…</div>';
fetch('datatable/log_analytics.php?' + params.toString())
.then(function(r){ return r.json(); })
.then(function(res) {
if (res.code !== 0) {
document.getElementById('content-area').innerHTML = '<div class="log-card"><div class="log-card-body">' + escapeHtml(res.msg || '加载失败') + '</div></div>';
return;
}
var data = res.data;
document.getElementById('content-area').innerHTML =
renderStats(data.summary) + renderConclusions(data.conclusions) + renderChartArea(data);
buildCharts(data);
})
.catch(function(err) {
document.getElementById('content-area').innerHTML = '<div class="log-card"><div class="log-card-body">请求失败:' + escapeHtml(String(err)) + '</div></div>';
});
}
document.getElementById('btn-query').addEventListener('click', loadData);
loadData();
});
</script>
</body>
</html>