NewHome/admin/_visits_lib.php

275 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* admin/_visits_lib.php —— 风控管理页数据查询与片段渲染visits.php / visits_data.php 共用)
* 依赖includes/db.php数据层、includes/layout.phphe 转义)
*/
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();
}