275 lines
10 KiB
PHP
275 lines
10 KiB
PHP
<?php
|
||
/**
|
||
* admin/_visits_lib.php —— 风控管理页数据查询与片段渲染(visits.php / visits_data.php 共用)
|
||
* 依赖:includes/db.php(数据层)、includes/layout.php(he 转义)
|
||
*/
|
||
require_once dirname(__DIR__) . '/includes/db.php';
|
||
require_once dirname(__DIR__) . '/includes/layout.php';
|
||
|
||
/** 访问概况统计(累计访问 / 独立 IP / 今日访问 / 黑名单规则数) */
|
||
function vis_stat_all(): array
|
||
{
|
||
$entries = visitors_entries();
|
||
$ips = [];
|
||
$today = 0;
|
||
$todayStart = date('Y-m-d 00:00:00');
|
||
foreach ($entries as $e) {
|
||
$ips[$e['ip']] = 1;
|
||
if ($e['created_at'] >= $todayStart) {
|
||
$today++;
|
||
}
|
||
}
|
||
return [
|
||
'total' => count($entries),
|
||
'ips' => count($ips),
|
||
'today' => $today,
|
||
'ban' => (int) db()->query('SELECT COUNT(*) FROM ip_blacklist')->fetchColumn(),
|
||
];
|
||
}
|
||
|
||
/** 访问记录查询(完整 / 精简 + IP、关键词筛选 + 分页) */
|
||
function vis_query_access(string $mode, string $ipF, string $q, int $page, int $perPage = 30): array
|
||
{
|
||
if (!in_array($mode, ['full', 'compact'], true)) {
|
||
$mode = 'full';
|
||
}
|
||
$entries = visitors_entries();
|
||
$all = [];
|
||
if ($mode === 'compact') {
|
||
// 精简模式:按 IP 聚合(最近访问时间 / IP / 访问次数),首次出现即该 IP 最新一条
|
||
$grp = [];
|
||
$order = [];
|
||
foreach ($entries as $e) {
|
||
if ($ipF !== '' && stripos($e['ip'], $ipF) === false) {
|
||
continue;
|
||
}
|
||
if ($q !== '' && stripos($e['url'], $q) === false && stripos($e['method'], $q) === false) {
|
||
continue;
|
||
}
|
||
if (!isset($grp[$e['ip']])) {
|
||
$grp[$e['ip']] = ['ip' => $e['ip'], 'last_at' => $e['created_at'], 'cnt' => 0];
|
||
$order[] = $e['ip'];
|
||
}
|
||
$grp[$e['ip']]['cnt']++;
|
||
}
|
||
foreach ($order as $ip) {
|
||
$all[] = $grp[$ip];
|
||
}
|
||
} else {
|
||
// 完整模式:逐条明细
|
||
foreach ($entries as $e) {
|
||
if ($ipF !== '' && stripos($e['ip'], $ipF) === false) {
|
||
continue;
|
||
}
|
||
if ($q !== '' && stripos($e['url'], $q) === false && stripos($e['method'], $q) === false) {
|
||
continue;
|
||
}
|
||
$all[] = $e;
|
||
}
|
||
}
|
||
$page = max(1, $page);
|
||
$total = count($all);
|
||
$totalPages = max(1, (int) ceil($total / $perPage));
|
||
if ($page > $totalPages) {
|
||
$page = $totalPages;
|
||
}
|
||
return [
|
||
'mode' => $mode,
|
||
'page' => $page,
|
||
'perPage' => $perPage,
|
||
'total' => $total,
|
||
'totalPages' => $totalPages,
|
||
'logs' => array_slice($all, ($page - 1) * $perPage, $perPage),
|
||
'query' => ['mode' => $mode, 'ip' => $ipF, 'q' => $q],
|
||
'hasFilter' => ($ipF !== '' || $q !== ''),
|
||
];
|
||
}
|
||
|
||
/** 生成带参数的链接($q 为基础参数,$ov 为覆盖项) */
|
||
function vis_make_url(array $q, array $ov = []): string
|
||
{
|
||
foreach ($ov as $k => $v) {
|
||
if ($v === null || $v === '') {
|
||
unset($q[$k]);
|
||
} else {
|
||
$q[$k] = $v;
|
||
}
|
||
}
|
||
return $q ? ('visits.php?' . http_build_query($q)) : 'visits.php';
|
||
}
|
||
|
||
/** 分页链接的 AJAX 查询串(供 data-vis-page 使用) */
|
||
function vis_page_query(array $q, int $page): string
|
||
{
|
||
$q['page'] = $page;
|
||
return http_build_query($q);
|
||
}
|
||
|
||
/** 渲染访问记录片段(统计行 + 表格 + 分页 / 空态) */
|
||
function vis_render_access(array $d): string
|
||
{
|
||
$logs = $d['logs'];
|
||
$mode = $d['mode'];
|
||
$q = $d['query'];
|
||
$page = (int) $d['page'];
|
||
$totalPages = (int) $d['totalPages'];
|
||
ob_start();
|
||
if ($logs) {
|
||
?>
|
||
<div class="tip" style="margin:10px 0 6px">
|
||
共命中 <?= (int) $d['total'] ?> <?= $mode === 'compact' ? '个 IP' : '条记录' ?>;
|
||
当前第 <?= $page ?> / <?= $totalPages ?> 页,每页 <?= (int) $d['perPage'] ?> 条。
|
||
</div>
|
||
<div class="v-scroll">
|
||
<?php if ($mode === 'compact'): ?>
|
||
<table class="v-tbl">
|
||
<thead>
|
||
<tr>
|
||
<th>最近访问时间</th>
|
||
<th>IP 地址</th>
|
||
<th>访问次数</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($logs as $r): ?>
|
||
<tr>
|
||
<td class="v-nowrap"><?= he((string) $r['last_at']) ?></td>
|
||
<td class="v-nowrap"><?= he((string) $r['ip']) ?></td>
|
||
<td><b><?= (int) $r['cnt'] ?></b> 次</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php else: ?>
|
||
<table class="v-tbl">
|
||
<thead>
|
||
<tr>
|
||
<th>访问时间</th>
|
||
<th>IP 地址</th>
|
||
<th>请求方式</th>
|
||
<th>访问 URL</th>
|
||
<th>响应码</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($logs as $r): ?>
|
||
<tr>
|
||
<td class="v-nowrap"><?= he((string) $r['created_at']) ?></td>
|
||
<td class="v-nowrap"><?= he((string) $r['ip']) ?></td>
|
||
<td><span class="v-tag"><?= he((string) $r['method']) ?></span></td>
|
||
<td><?= he((string) $r['url']) ?></td>
|
||
<td class="v-nowrap"><?php
|
||
$code = (int) $r['status'];
|
||
$cls = ($code >= 200 && $code < 300) ? 'good' : (($code >= 300 && $code < 400) ? 'info' : (($code >= 400 && $code < 500) ? 'warn' : 'bad'));
|
||
?><span class="v-tag <?= $cls ?>"><?= $code ?></span>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<?php if ($totalPages > 1): ?>
|
||
<div class="pager">
|
||
<span class="pg-info">共 <?= (int) $d['total'] ?> 条</span>
|
||
<?php if ($page > 1): ?>
|
||
<a class="btn btn-sm" href="<?= he(vis_make_url($q, ['page' => $page - 1])) ?>" data-vis-section="access"
|
||
data-vis-page="<?= he(vis_page_query($q, $page - 1)) ?>">← 上一页</a>
|
||
<?php endif; ?>
|
||
<?php
|
||
$win = 2;
|
||
$start = max(1, $page - $win);
|
||
$end = min($totalPages, $page + $win);
|
||
if ($start > 1) {
|
||
echo '<span class="tip">…</span>';
|
||
}
|
||
for ($p = $start; $p <= $end; $p++) {
|
||
if ($p === $page) {
|
||
echo '<span class="btn btn-sm btn-primary">' . $p . '</span>';
|
||
} else {
|
||
echo '<a class="btn btn-sm" href="' . he(vis_make_url($q, ['page' => $p])) . '" data-vis-section="access" data-vis-page="' . he(vis_page_query($q, $p)) . '">' . $p . '</a>';
|
||
}
|
||
}
|
||
if ($end < $totalPages) {
|
||
echo '<span class="tip">…</span>';
|
||
}
|
||
?>
|
||
<?php if ($page < $totalPages): ?>
|
||
<a class="btn btn-sm" href="<?= he(vis_make_url($q, ['page' => $page + 1])) ?>" data-vis-section="access"
|
||
data-vis-page="<?= he(vis_page_query($q, $page + 1)) ?>">下一页 →</a>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
<?php
|
||
} else {
|
||
?>
|
||
<div class="err-msg" style="margin-top:12px">
|
||
<?= $d['hasFilter'] ? '该筛选条件下暂无访问记录,可点击「重置」查看全部。' : '还没有访问记录,访问站点前台页面后将在这里实时展示。' ?>
|
||
</div>
|
||
<?php
|
||
}
|
||
return (string) ob_get_clean();
|
||
}
|
||
|
||
/** 设备信息展示:优先客户端采集的 device,回退 UA 解析 */
|
||
function vis_device_text(array $row): string
|
||
{
|
||
$dev = trim((string) ($row['device'] ?? ''));
|
||
if ($dev !== '') {
|
||
return $dev;
|
||
}
|
||
return login_ua_brief((string) ($row['ua'] ?? ''));
|
||
}
|
||
|
||
/** 登录失败日志查询(IP × 日期聚合) */
|
||
function vis_query_loginfail(string $lf, int $limit = 200): array
|
||
{
|
||
return [
|
||
'filter' => $lf,
|
||
'rows' => login_fail_daily_stats($lf, $limit),
|
||
];
|
||
}
|
||
|
||
/** 登录失败日志片段渲染(表格 / 空态) */
|
||
function vis_render_loginfail(array $d): string
|
||
{
|
||
$rows = $d['rows'];
|
||
ob_start();
|
||
if ($rows) {
|
||
?>
|
||
<div class="v-scroll" style="margin-top:8px">
|
||
<table class="v-tbl">
|
||
<thead>
|
||
<tr>
|
||
<th>日期</th>
|
||
<th>IP 地址</th>
|
||
<th>失败次数</th>
|
||
<th>最近失败时间</th>
|
||
<th>设备信息</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($rows as $r): ?>
|
||
<tr>
|
||
<td class="v-nowrap"><?= he((string) $r['day']) ?></td>
|
||
<td class="v-nowrap"><?= he((string) $r['ip']) ?></td>
|
||
<td><b><?= (int) $r['cnt'] ?></b> 次</td>
|
||
<td class="v-nowrap"><?= he((string) $r['last_at']) ?></td>
|
||
<td><?= he(vis_device_text($r)) ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php
|
||
} else {
|
||
?>
|
||
<div class="err-msg" style="margin-top:12px">
|
||
<?= $d['filter'] !== '' ? '该 IP 暂无登录失败记录。' : '暂无登录失败记录。' ?>
|
||
</div>
|
||
<?php
|
||
}
|
||
return (string) ob_get_clean();
|
||
}
|