1056 lines
39 KiB
PHP
Executable File
1056 lines
39 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>");
|
|
//die("输入的密码不正确,请重新输入。");
|
|
}
|
|
|
|
?>
|
|
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>访问判断日志</title>
|
|
<meta name="renderer" content="webkit">
|
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<link href="//cdn.staticfile.org/layui/2.9.2/css/layui.css" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--log-primary: #16baaa;
|
|
--log-primary-dark: #0e8d81;
|
|
--log-bg: #f0f2f5;
|
|
--log-card: #ffffff;
|
|
--log-border: #e8ecf1;
|
|
--log-header-bg: #f4f7fb;
|
|
--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, -apple-system, sans-serif;
|
|
}
|
|
.log-page {
|
|
max-width: 1680px;
|
|
margin: 0 auto;
|
|
padding: 24px 28px 40px;
|
|
}
|
|
.log-page-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 20px;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
}
|
|
.log-page-header h1 {
|
|
margin: 0;
|
|
font-size: 1.35rem;
|
|
font-weight: 600;
|
|
color: var(--log-text);
|
|
}
|
|
.log-page-header p {
|
|
margin: 4px 0 0;
|
|
font-size: .85rem;
|
|
color: var(--log-muted);
|
|
}
|
|
.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-body { padding: 18px 20px; }
|
|
.log-filter-grid .layui-col-md3 { margin-bottom: 12px; }
|
|
.log-filter-grid .layui-input,
|
|
.log-filter-grid .layui-select-title input {
|
|
border-radius: 6px;
|
|
border-color: var(--log-border);
|
|
}
|
|
.log-actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 14px 20px;
|
|
border-top: 1px solid var(--log-border);
|
|
background: #fafbfc;
|
|
}
|
|
.log-actions .layui-btn { border-radius: 6px; }
|
|
.log-actions .layui-btn-primary { background: #fff; }
|
|
.log-count {
|
|
margin-left: auto;
|
|
font-size: .88rem;
|
|
color: var(--log-muted);
|
|
}
|
|
.log-count strong {
|
|
color: var(--log-primary-dark);
|
|
font-size: 1rem;
|
|
margin: 0 4px;
|
|
}
|
|
#btn-clear { background: #e74c3c !important; border-color: #e74c3c !important; }
|
|
.log-table-wrap { padding: 0; }
|
|
.log-table-wrap .layui-table-view {
|
|
border: none;
|
|
border-radius: 0 0 10px 10px;
|
|
margin: 0;
|
|
}
|
|
/* Sticky header */
|
|
.log-table-wrap .layui-table-header {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 20;
|
|
border-radius: 0;
|
|
}
|
|
.log-table-wrap .layui-table-header .layui-table {
|
|
background: linear-gradient(180deg, #eef3f9 0%, #e4ebf5 100%);
|
|
}
|
|
.log-table-wrap .layui-table-header th {
|
|
color: #3d5167 !important;
|
|
font-weight: 600 !important;
|
|
font-size: 12.5px;
|
|
border-color: #d8e0ea !important;
|
|
padding: 11px 10px !important;
|
|
}
|
|
/* Zebra + hover */
|
|
.log-table-wrap .layui-table-body tr:nth-child(even) td {
|
|
background-color: #fafbfc;
|
|
}
|
|
.log-table-wrap .layui-table-body tr:hover td {
|
|
background-color: #eef6ff !important;
|
|
}
|
|
.log-table-wrap .layui-table-body td {
|
|
border-color: #eef1f5 !important;
|
|
padding: 9px 10px !important;
|
|
font-size: 12.5px;
|
|
line-height: 1.45;
|
|
transition: background-color .15s ease;
|
|
}
|
|
.log-table-wrap .layui-table-page {
|
|
border-top: 1px solid var(--log-border);
|
|
padding: 12px 16px;
|
|
background: #fafbfc;
|
|
}
|
|
/* Badges */
|
|
.cloak-badge {
|
|
display: inline-block;
|
|
padding: 2px 9px;
|
|
border-radius: 999px;
|
|
font-size: 11.5px;
|
|
font-weight: 600;
|
|
line-height: 1.5;
|
|
white-space: nowrap;
|
|
letter-spacing: .02em;
|
|
}
|
|
.badge-success { background: #d4edda; color: #155724; }
|
|
.badge-error { background: #f8d7da; color: #721c24; }
|
|
.badge-warning { background: #fff3cd; color: #856404; }
|
|
.badge-info { background: #d1ecf1; color: #0c5460; }
|
|
.badge-muted { background: #e9ecef; color: #495057; }
|
|
.reason-badge {
|
|
max-width: 100%;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
vertical-align: middle;
|
|
}
|
|
/* Truncation + mono timestamp */
|
|
.cell-ellipsis {
|
|
display: block;
|
|
max-width: 100%;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
cursor: default;
|
|
}
|
|
.cell-mono {
|
|
font-family: 'SF Mono', 'Consolas', 'Monaco', monospace;
|
|
font-size: 12px;
|
|
color: #4a5568;
|
|
letter-spacing: -.02em;
|
|
}
|
|
.cell-copy-link { cursor: copy; }
|
|
.cell-copy-link:hover { color: var(--log-primary-dark); }
|
|
.cell-ip { font-family: 'SF Mono', 'Consolas', monospace; font-size: 12px; }
|
|
.text-muted { color: #adb5bd; }
|
|
.fp-hdata-cell {
|
|
display: flex;
|
|
flex-direction: row;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 4px 8px;
|
|
max-width: 100%;
|
|
min-height: 24px;
|
|
}
|
|
.fp-hdata-summary {
|
|
flex: 1 1 140px;
|
|
min-width: 0;
|
|
font-size: 11.5px;
|
|
color: #475569;
|
|
line-height: 1.35;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.fp-hdata-actions {
|
|
display: inline-flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 4px;
|
|
flex: 0 0 auto;
|
|
}
|
|
.fp-hdata-btn {
|
|
display: inline-block;
|
|
padding: 1px 8px;
|
|
font-size: 11px;
|
|
border-radius: 4px;
|
|
background: #e8f4fd;
|
|
color: #0c5460;
|
|
cursor: pointer;
|
|
border: 1px solid #bee5eb;
|
|
width: fit-content;
|
|
}
|
|
.fp-hdata-btn:hover { background: #d1ecf1; }
|
|
/* 指纹 / API 审计列:解除 layui 固定行高,避免「判定详情」等按钮被裁切 */
|
|
.log-table-wrap .layui-table-body td[data-field="fp_hdata_text"],
|
|
.log-table-wrap .layui-table-body td[data-field="api_by_request_text"],
|
|
.log-table-wrap .layui-table-body td[data-field="api_by_response_text"],
|
|
.log-table-wrap .layui-table-body td[data-field="api_risk_request_text"],
|
|
.log-table-wrap .layui-table-body td[data-field="api_risk_response_text"] {
|
|
height: auto !important;
|
|
vertical-align: top;
|
|
}
|
|
.log-table-wrap .layui-table-body td[data-field="fp_hdata_text"] > .layui-table-cell,
|
|
.log-table-wrap .layui-table-body td[data-field="api_by_request_text"] > .layui-table-cell,
|
|
.log-table-wrap .layui-table-body td[data-field="api_by_response_text"] > .layui-table-cell,
|
|
.log-table-wrap .layui-table-body td[data-field="api_risk_request_text"] > .layui-table-cell,
|
|
.log-table-wrap .layui-table-body td[data-field="api_risk_response_text"] > .layui-table-cell {
|
|
height: auto !important;
|
|
max-height: none !important;
|
|
white-space: normal !important;
|
|
overflow: visible !important;
|
|
line-height: 1.35;
|
|
padding-top: 7px !important;
|
|
padding-bottom: 7px !important;
|
|
}
|
|
.fp-risk-explain { font-size: 13px; line-height: 1.55; color: #2c3e50; }
|
|
.fp-risk-explain-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
|
|
.fp-risk-tag { font-size: 12px; padding: 2px 10px; border-radius: 999px; background: #eef2ff; color: #3730a3; font-weight: 600; }
|
|
.fp-risk-kv { width: 100%; border-collapse: collapse; margin-bottom: 14px; font-size: 12.5px; }
|
|
.fp-risk-kv th { text-align: left; width: 88px; padding: 6px 8px; background: #f8fafc; border: 1px solid #e2e8f0; vertical-align: top; color: #64748b; }
|
|
.fp-risk-kv td { padding: 6px 8px; border: 1px solid #e2e8f0; word-break: break-word; }
|
|
.fp-risk-reason-raw { font-family: 'SF Mono', Consolas, monospace; font-size: 12px; }
|
|
.fp-risk-section-title { font-weight: 600; margin: 12px 0 8px; font-size: 13px; color: #334155; }
|
|
.fp-risk-factor-list { margin: 0; padding-left: 1.2rem; }
|
|
.fp-risk-factor-list > li { margin-bottom: 10px; }
|
|
.fp-factor-text { font-weight: 500; margin-bottom: 4px; }
|
|
.fp-factor-fields { margin: 0; padding-left: 1rem; list-style: disc; }
|
|
.fp-factor-fields mark.fp-field-key { background: #fef3c7; padding: 0 4px; border-radius: 3px; font-weight: 600; }
|
|
.fp-factor-fields code { font-size: 11.5px; word-break: break-all; }
|
|
.fp-factor-miss { font-size: 12px; color: #94a3b8; }
|
|
.fp-hdata-json {
|
|
max-height: 320px;
|
|
overflow: auto;
|
|
background: #0f172a;
|
|
color: #e2e8f0;
|
|
padding: 12px;
|
|
border-radius: 8px;
|
|
font-size: 11.5px;
|
|
line-height: 1.45;
|
|
margin: 0;
|
|
white-space: pre-wrap;
|
|
word-break: break-all;
|
|
}
|
|
.fp-hdata-json .fp-hl-line { background: rgba(250, 204, 21, .18); display: inline-block; width: 100%; }
|
|
.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); }
|
|
.fp-risk-conclusions { margin: 0 0 14px; padding: 0; list-style: none; }
|
|
.fp-risk-conclusions li {
|
|
padding: 9px 12px; margin-bottom: 8px; border-radius: 8px; font-size: 12.5px; line-height: 1.55;
|
|
border-left: 3px solid #16baaa; background: #f0faf9;
|
|
}
|
|
.fp-risk-conclusions li.warn { background: #fff8e6; border-left-color: #e6a23c; }
|
|
.fp-risk-conclusions li.alert { background: #fef2f2; border-left-color: #e74c3c; }
|
|
.fp-risk-conclusions li.ok { background: #f0fdf4; border-left-color: #22c55e; }
|
|
.fp-risk-conclusions li .fp-conc-action {
|
|
display: block; margin-top: 4px; font-size: 12px; color: #64748b;
|
|
}
|
|
.fp-risk-conclusions li a { color: #0e8d81; text-decoration: none; }
|
|
.fp-risk-conclusions li a:hover { text-decoration: underline; }
|
|
.col-settings-list {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
max-height: 420px;
|
|
overflow-y: auto;
|
|
}
|
|
.col-settings-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 8px 10px;
|
|
margin-bottom: 6px;
|
|
background: #f8fafc;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 6px;
|
|
cursor: grab;
|
|
user-select: none;
|
|
}
|
|
.col-settings-item.dragging { opacity: .55; border-style: dashed; }
|
|
.col-settings-item.drag-over { border-color: var(--log-primary); background: #eefaf8; }
|
|
.col-settings-handle {
|
|
color: #94a3b8;
|
|
font-size: 14px;
|
|
line-height: 1;
|
|
cursor: grab;
|
|
}
|
|
.col-settings-title { flex: 1; font-size: 13px; color: #334155; }
|
|
.col-settings-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-top: 14px;
|
|
padding-top: 12px;
|
|
border-top: 1px solid #e8ecf1;
|
|
}
|
|
.col-settings-tip {
|
|
font-size: 12px;
|
|
color: #64748b;
|
|
margin: 0 0 10px;
|
|
line-height: 1.5;
|
|
}
|
|
@media (max-width: 768px) {
|
|
.log-page { padding: 16px 12px 32px; }
|
|
.log-count { margin-left: 0; width: 100%; }
|
|
}
|
|
</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" class="active">访问日志</a>
|
|
<a href="simulate_test.php">模拟测试</a>
|
|
<a href="log_analytics.php">数据分析</a>
|
|
<a href="dashboard.php">配置中心</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="log-card">
|
|
<div class="log-card-body">
|
|
<form class="layui-form layui-row layui-col-space16 log-filter-grid">
|
|
<div class="layui-col-md3">
|
|
<div class="layui-input-wrap">
|
|
<div class="layui-input-prefix">
|
|
<i class="layui-icon layui-icon-username"></i>
|
|
</div>
|
|
<input type="text" name="domain" value="" placeholder="域名" class="layui-input" lay-affix="clear">
|
|
</div>
|
|
</div>
|
|
<div class="layui-col-md3">
|
|
<div class="layui-input-wrap">
|
|
<input type="text" name="spage" value="" placeholder="访问页面,可输入配置名称搜索" class="layui-input" lay-affix="clear">
|
|
</div>
|
|
</div>
|
|
<div class="layui-col-md3">
|
|
<div class="layui-input-wrap">
|
|
<select name="result">
|
|
<option value="">全部</option>
|
|
<option value="true">TRUE(通过访问)</option>
|
|
<option value="false">FALSE(禁止访问)</option>
|
|
<option value="wait">检测中</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="layui-col-md3">
|
|
<div class="layui-input-wrap">
|
|
<input type="text" name="country" placeholder="国家" lay-affix="clear" class="layui-input">
|
|
</div>
|
|
</div>
|
|
<div class="layui-col-md3">
|
|
<div class="layui-input-wrap">
|
|
<input type="text" name="IP" placeholder="IP" lay-affix="clear" class="layui-input">
|
|
</div>
|
|
</div>
|
|
<div class="layui-col-md3">
|
|
<div class="layui-input-wrap">
|
|
<div class="layui-input-prefix">
|
|
<i class="layui-icon layui-icon-date"></i>
|
|
</div>
|
|
<input type="text" name="visit_date" readonly placeholder="访问日期" class="layui-input demo-table-search-date">
|
|
</div>
|
|
</div>
|
|
<div class="layui-btn-container layui-col-xs12">
|
|
<a class="layui-btn" lay-submit href="javascript:void(0);" id="export" target="_blank">导出</a>
|
|
<button class="layui-btn" lay-submit lay-filter="demo-table-search">搜索</button>
|
|
<button type="reset" class="layui-btn layui-btn-primary">清除条件</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="log-actions">
|
|
<button class="layui-btn layui-btn-sm" id="btn-refresh" onclick="resetTable()" lay-event="footerDemoBtn1">刷新</button>
|
|
<a href="simulate_test.php" class="layui-btn layui-btn-sm layui-btn-normal">模拟测试</a>
|
|
<button class="layui-btn layui-btn-sm layui-btn-normal" id="btn-col-settings" type="button">列设置</button>
|
|
<button class="layui-btn layui-btn-sm btn-danger" id="btn-clear" lay-on="confirm" lay-event="footerDemoBtn2">清空数据库</button>
|
|
<span class="log-count">当前查询数量:<strong id="total_nums">—</strong></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="log-card log-table-wrap">
|
|
<div id="demo-laypage-all"></div>
|
|
<table class="layui-hide" id="ID-table-demo-search" lay-filter="test"></table>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/dist/layui.js"></script>
|
|
<script src="/dist/jquery.min.js"></script>
|
|
<script>
|
|
|
|
layui.use(function(){
|
|
var table = layui.table;
|
|
var form = layui.form;
|
|
var laydate = layui.laydate;
|
|
var layer = layui.layer;
|
|
var LINK_CELL_MAX = 150;
|
|
var COLUMN_STORAGE_KEY = 'cloak_log_table_cols_v1';
|
|
var TABLE_ID = 'ID-table-demo-search';
|
|
var colConfig = [];
|
|
var dragField = null;
|
|
|
|
function escapeHtml(s) {
|
|
if (s == null || s === '') return '';
|
|
return String(s)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
function escapeAttr(s) {
|
|
return String(s)
|
|
.replace(/&/g, '&')
|
|
.replace(/"/g, '"')
|
|
.replace(/</g, '<');
|
|
}
|
|
|
|
function cellEllipsis(value, maxLen) {
|
|
var text = value == null ? '' : String(value);
|
|
if (!text) return '<span class="text-muted">—</span>';
|
|
var esc = escapeHtml(text);
|
|
var display = text.length > maxLen ? escapeHtml(text.slice(0, maxLen)) + '…' : esc;
|
|
return '<span class="cell-ellipsis" title="' + esc + '">' + display + '</span>';
|
|
}
|
|
|
|
function cellLinkEllipsis(value, maxLen) {
|
|
maxLen = maxLen || LINK_CELL_MAX;
|
|
var text = value == null ? '' : String(value);
|
|
if (!text) return '<span class="text-muted">—</span>';
|
|
var esc = escapeHtml(text);
|
|
var display = text.length > maxLen ? escapeHtml(text.slice(0, maxLen)) + '…' : esc;
|
|
return '<span class="cell-ellipsis cell-copy-link" title="双击复制完整内容" data-full="' + escapeAttr(text) + '">' + display + '</span>';
|
|
}
|
|
|
|
function badgeResult(val) {
|
|
var raw = val == null ? '' : String(val);
|
|
var v = raw.toLowerCase();
|
|
if (v === 'true' || v === '1') {
|
|
return '<span class="cloak-badge badge-success">TRUE</span>';
|
|
}
|
|
if (v === 'false' || v === '0') {
|
|
return '<span class="cloak-badge badge-error">FALSE</span>';
|
|
}
|
|
if (v === 'wait' || raw.indexOf('检测') !== -1) {
|
|
return '<span class="cloak-badge badge-info">检测中</span>';
|
|
}
|
|
return '<span class="cloak-badge badge-muted">' + escapeHtml(raw) + '</span>';
|
|
}
|
|
|
|
function badgeReason(reason) {
|
|
if (reason == null || reason === '') {
|
|
return '<span class="text-muted">—</span>';
|
|
}
|
|
var text = String(reason);
|
|
var cls = 'badge-info';
|
|
if (text.indexOf('未确定流量来源') !== -1) {
|
|
cls = 'badge-warning';
|
|
} else if (/过滤|黑名单|无效|代理|VPN|禁止|欺骗|无来源/i.test(text)) {
|
|
cls = 'badge-error';
|
|
} else if (/白名单|允许|通过/i.test(text)) {
|
|
cls = 'badge-success';
|
|
} else if (/^\d+$/.test(text.trim()) || /风险|WebDriver|Canvas|分辨率|自动化|指纹/i.test(text)) {
|
|
cls = 'badge-warning';
|
|
}
|
|
var esc = escapeHtml(text);
|
|
var short = text.length > 26 ? escapeHtml(text.slice(0, 26)) + '…' : esc;
|
|
return '<span class="cloak-badge ' + cls + ' reason-badge" title="' + esc + '">' + short + '</span>';
|
|
}
|
|
|
|
function formatJsonPretty(str) {
|
|
try {
|
|
return JSON.stringify(JSON.parse(str), null, 2);
|
|
} catch (e) {
|
|
return str;
|
|
}
|
|
}
|
|
|
|
function cellApiAudit(val, title, d, field) {
|
|
if (val === '__LAZY__' && d && d.id && field) {
|
|
return '<div class="fp-hdata-cell">'
|
|
+ '<span class="fp-hdata-summary text-muted">已记录</span>'
|
|
+ '<span class="fp-hdata-actions"><span class="fp-hdata-btn fp-hdata-btn-json" data-log-id="' + escapeAttr(String(d.id)) + '" data-field="' + escapeAttr(field) + '" data-title="' + escapeAttr(title || 'API 数据') + '">查看 JSON</span></span>'
|
|
+ '</div>';
|
|
}
|
|
if (val == null || val === '' || val === '-') {
|
|
return '<span class="text-muted">—</span>';
|
|
}
|
|
var text = String(val);
|
|
var short = text.length > 40 ? text.slice(0, 40) + '…' : text;
|
|
return '<div class="fp-hdata-cell">'
|
|
+ '<span class="fp-hdata-summary" title="' + escapeAttr(text) + '">' + escapeHtml(short) + '</span>'
|
|
+ '<span class="fp-hdata-actions"><span class="fp-hdata-btn fp-hdata-btn-json" data-full="' + escapeAttr(text) + '" data-title="' + escapeAttr(title || 'API 数据') + '">查看 JSON</span></span>'
|
|
+ '</div>';
|
|
}
|
|
|
|
function cellFpHdata(d) {
|
|
if (!d.fp_hdata_text) {
|
|
return '<span class="text-muted">—</span>';
|
|
}
|
|
var summary = d.fp_hdata_text || '';
|
|
var html = '<div class="fp-hdata-cell">';
|
|
if (summary) {
|
|
html += '<span class="fp-hdata-summary" title="' + escapeAttr(summary) + '">' + escapeHtml(summary.length > 48 ? summary.slice(0, 48) + '…' : summary) + '</span>';
|
|
}
|
|
html += '<span class="fp-hdata-actions">';
|
|
html += '<span class="fp-hdata-btn" data-explain-id="' + escapeAttr(String(d.id)) + '">判定详情</span>';
|
|
html += '<span class="fp-hdata-btn fp-hdata-btn-raw" data-log-id="' + escapeAttr(String(d.id)) + '" data-field="fp_hdata">原始 hdata</span>';
|
|
html += '</span></div>';
|
|
return html;
|
|
}
|
|
|
|
var COLUMN_DEFS = [
|
|
{ field: 'id', title: 'ID', width: 56, fixed: 'left', defaultVisible: true },
|
|
{ field: 'domain', title: '域名', width: 120, defaultVisible: true, templet: function(d){ return cellEllipsis(d.domain, 18); } },
|
|
{ field: 'visit_date', title: '访问日期', width: 130, defaultVisible: true, templet: function(d){
|
|
return '<span class="cell-mono" title="' + escapeHtml(d.visit_date) + '">' + escapeHtml(d.visit_date) + '</span>';
|
|
}},
|
|
{ field: 'country', title: '国家', width: 100, defaultVisible: true, templet: function(d){ return cellEllipsis(d.country, 12); } },
|
|
{ field: 'result', title: '是否正常', width: 88, defaultVisible: true, templet: function(d){ return badgeResult(d.result); } },
|
|
{ field: 'reason', title: '理由', width: 140, defaultVisible: true, templet: function(d){ return badgeReason(d.reason); } },
|
|
{ field: 'judge_timing_text', title: '判断用时', width: 240, defaultVisible: true, templet: function(d){ return cellEllipsis(d.judge_timing_text, 36); } },
|
|
{ field: 'vtimes', title: '第几次访问', width: 96, defaultVisible: true },
|
|
{ field: 'fp_url', title: '跳转至', width: 280, defaultVisible: true, templet: function(d){ return cellLinkEllipsis(d.fp_url, LINK_CELL_MAX); } },
|
|
{ field: 'device', title: '访问设备', width: 200, defaultVisible: true, templet: function(d){ return cellEllipsis(d.device, 32); } },
|
|
{ field: 'referer', title: '来源', width: 280, defaultVisible: true, templet: function(d){ return cellLinkEllipsis(d.referer, LINK_CELL_MAX); } },
|
|
{ field: 'IP', title: 'IP(支持IPV6)', width: 130, defaultVisible: true, templet: function(d){
|
|
return '<span class="cell-ip" title="' + escapeHtml(d.IP) + '">' + escapeHtml(d.IP) + '</span>';
|
|
}},
|
|
{ field: 'page', title: '访问页面', width: 280, defaultVisible: true, templet: function(d){ return cellLinkEllipsis(d.page, LINK_CELL_MAX); } },
|
|
{ field: 'client', title: '客户端', width: 80, defaultVisible: true, templet: function(d){ return cellEllipsis(d.client, 10); } },
|
|
{ field: 'language', title: '语言', width: 80, defaultVisible: true, templet: function(d){ return cellEllipsis(d.language, 10); } },
|
|
{ field: 'browser', title: '浏览器', width: 88, defaultVisible: true, templet: function(d){ return cellEllipsis(d.browser, 10); } },
|
|
{ field: 'campagin_id', title: '广告策略', width: 88, defaultVisible: true, templet: function(d){ return cellEllipsis(d.campagin_id, 10); } },
|
|
{ field: 'user_agent', title: 'User-Agent', width: 320, defaultVisible: false, templet: function(d){ return cellEllipsis(d.user_agent, 48); } },
|
|
{ field: 'http_referer', title: 'HTTP-Referer', width: 260, defaultVisible: false, templet: function(d){ return cellLinkEllipsis(d.http_referer, LINK_CELL_MAX); } },
|
|
{ field: 'accept_language_raw', title: 'Accept-Language', width: 220, defaultVisible: false, templet: function(d){ return cellEllipsis(d.accept_language_raw, 32); } },
|
|
{ field: 'fp_hdata_text', title: '指纹 hdata', width: 360, defaultVisible: false, templet: function(d){ return cellFpHdata(d); } },
|
|
{ field: 'api_by_request_text', title: 'byApi 请求参数', width: 300, defaultVisible: false, templet: function(d){ return cellApiAudit(d.api_by_request_text, 'byApi 请求参数 #' + d.id, d, 'api_by_request'); } },
|
|
{ field: 'api_by_response_text', title: 'byApi 返回值', width: 300, defaultVisible: false, templet: function(d){ return cellApiAudit(d.api_by_response_text, 'byApi 返回值 #' + d.id, d, 'api_by_response'); } },
|
|
{ field: 'api_risk_request_text', title: 'byApiRisk 请求参数', width: 300, defaultVisible: false, templet: function(d){ return cellApiAudit(d.api_risk_request_text, 'byApiRisk 请求参数 #' + d.id, d, 'api_risk_request'); } },
|
|
{ field: 'api_risk_response_text', title: 'byApiRisk 返回值', width: 300, defaultVisible: false, templet: function(d){ return cellApiAudit(d.api_risk_response_text, 'byApiRisk 返回值 #' + d.id, d, 'api_risk_response'); } },
|
|
{ field: 'sim_tag', title: '模拟', width: 88, defaultVisible: true, templet: function(d){
|
|
if (String(d.is_simulation) === '1') {
|
|
var src = d.sim_source_log_id ? ' #' + d.sim_source_log_id : '';
|
|
return '<span class="cloak-badge badge-info">是' + escapeHtml(String(src)) + '</span>';
|
|
}
|
|
return '<span class="text-muted">—</span>';
|
|
}},
|
|
{ field: 'sim_action', title: '操作', width: 108, defaultVisible: true, templet: function(d){
|
|
return '<a class="layui-btn layui-btn-xs layui-btn-normal" href="simulate_test.php?from_log=' + encodeURIComponent(String(d.id)) + '">模拟测试</a>';
|
|
}},
|
|
];
|
|
|
|
var COLUMN_DEF_MAP = {};
|
|
COLUMN_DEFS.forEach(function(def) {
|
|
COLUMN_DEF_MAP[def.field] = def;
|
|
});
|
|
|
|
function getDefaultColConfig() {
|
|
return COLUMN_DEFS.map(function(def) {
|
|
return { field: def.field, visible: def.defaultVisible !== false };
|
|
});
|
|
}
|
|
|
|
function normalizeColConfig(raw) {
|
|
var defaults = getDefaultColConfig();
|
|
if (!Array.isArray(raw) || !raw.length) {
|
|
return defaults;
|
|
}
|
|
var seen = {};
|
|
var merged = [];
|
|
raw.forEach(function(item) {
|
|
if (!item || !item.field || !COLUMN_DEF_MAP[item.field] || seen[item.field]) {
|
|
return;
|
|
}
|
|
seen[item.field] = true;
|
|
merged.push({
|
|
field: item.field,
|
|
visible: item.visible !== false
|
|
});
|
|
});
|
|
defaults.forEach(function(item) {
|
|
if (!seen[item.field]) {
|
|
merged.push(item);
|
|
}
|
|
});
|
|
return merged;
|
|
}
|
|
|
|
function loadColConfig() {
|
|
try {
|
|
var raw = localStorage.getItem(COLUMN_STORAGE_KEY);
|
|
if (!raw) {
|
|
return getDefaultColConfig();
|
|
}
|
|
return normalizeColConfig(JSON.parse(raw));
|
|
} catch (e) {
|
|
return getDefaultColConfig();
|
|
}
|
|
}
|
|
|
|
function saveColConfig(config) {
|
|
colConfig = normalizeColConfig(config);
|
|
localStorage.setItem(COLUMN_STORAGE_KEY, JSON.stringify(colConfig));
|
|
}
|
|
|
|
function buildColItem(def, visible) {
|
|
var col = {
|
|
field: def.field,
|
|
title: def.title,
|
|
width: def.width,
|
|
hide: !visible
|
|
};
|
|
if (def.templet) {
|
|
col.templet = def.templet;
|
|
}
|
|
return col;
|
|
}
|
|
|
|
function buildCols(config) {
|
|
config = normalizeColConfig(config || colConfig);
|
|
var cols = [];
|
|
var firstVisibleField = null;
|
|
config.forEach(function(item) {
|
|
if (item.visible && !firstVisibleField) {
|
|
firstVisibleField = item.field;
|
|
}
|
|
});
|
|
config.forEach(function(item) {
|
|
var def = COLUMN_DEF_MAP[item.field];
|
|
if (!def) {
|
|
return;
|
|
}
|
|
var col = buildColItem(def, item.visible);
|
|
if (def.fixed && item.field === firstVisibleField) {
|
|
col.fixed = def.fixed;
|
|
} else {
|
|
delete col.fixed;
|
|
}
|
|
cols.push(col);
|
|
});
|
|
if (!cols.length) {
|
|
cols.push(buildColItem(COLUMN_DEF_MAP.id, true));
|
|
}
|
|
return [cols];
|
|
}
|
|
|
|
function fixRichTableCellHeights() {
|
|
var fields = ['fp_hdata_text', 'api_by_request_text', 'api_by_response_text', 'api_risk_request_text', 'api_risk_response_text'];
|
|
fields.forEach(function(field) {
|
|
$('.log-table-wrap .layui-table-body td[data-field="' + field + '"]').each(function() {
|
|
var $cell = $(this).children('.layui-table-cell');
|
|
$cell.css({ height: 'auto', whiteSpace: 'normal', overflow: 'visible' });
|
|
$(this).css('height', 'auto');
|
|
});
|
|
});
|
|
}
|
|
|
|
function applyTableRowStyles(res) {
|
|
if (!res || !Array.isArray(res.data)) {
|
|
return;
|
|
}
|
|
var $rows = $('.log-table-wrap .layui-table-body table tbody tr');
|
|
res.data.forEach(function(row, idx) {
|
|
var $tr = $rows.eq(idx);
|
|
if (!$tr.length) {
|
|
return;
|
|
}
|
|
if (row.reason && String(row.reason).indexOf('未确定流量来源') !== -1) {
|
|
$tr.css({ color: '#856404', backgroundColor: '#fff8e6' });
|
|
}
|
|
});
|
|
}
|
|
|
|
function onTableDataRendered(res, count) {
|
|
if (typeof count === 'number') {
|
|
$("#total_nums").text(count);
|
|
}
|
|
applyTableRowStyles(res);
|
|
fixRichTableCellHeights();
|
|
}
|
|
|
|
function reloadTableCols() {
|
|
table.reload(TABLE_ID, {
|
|
cols: buildCols(colConfig),
|
|
done: function(res, curr, count) {
|
|
onTableDataRendered(res, count);
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderColSettingsList() {
|
|
var html = '<p class="col-settings-tip">勾选显示列,拖拽左侧手柄调整顺序。设置保存在本浏览器。</p>';
|
|
html += '<ul class="col-settings-list" id="col-settings-list">';
|
|
colConfig.forEach(function(item) {
|
|
var def = COLUMN_DEF_MAP[item.field];
|
|
if (!def) {
|
|
return;
|
|
}
|
|
html += '<li class="col-settings-item" draggable="true" data-field="' + escapeAttr(item.field) + '">';
|
|
html += '<span class="col-settings-handle" title="拖拽排序">☰</span>';
|
|
html += '<input type="checkbox" lay-skin="primary" class="col-settings-check" data-field="' + escapeAttr(item.field) + '"' + (item.visible ? ' checked' : '') + '>';
|
|
html += '<span class="col-settings-title">' + escapeHtml(def.title) + '</span>';
|
|
html += '</li>';
|
|
});
|
|
html += '</ul>';
|
|
html += '<div class="col-settings-actions">';
|
|
html += '<button type="button" class="layui-btn layui-btn-sm" id="col-settings-apply">应用</button>';
|
|
html += '<button type="button" class="layui-btn layui-btn-sm layui-btn-primary" id="col-settings-reset">恢复默认</button>';
|
|
html += '</div>';
|
|
return html;
|
|
}
|
|
|
|
function refreshColSettingsPanel($panel) {
|
|
var checks = {};
|
|
$panel.find('.col-settings-item').each(function() {
|
|
var field = this.getAttribute('data-field');
|
|
checks[field] = $(this).find('.col-settings-check').prop('checked');
|
|
});
|
|
colConfig.forEach(function(item) {
|
|
if (Object.prototype.hasOwnProperty.call(checks, item.field)) {
|
|
item.visible = !!checks[item.field];
|
|
}
|
|
});
|
|
$panel.find('.col-settings-body').html(renderColSettingsList());
|
|
form.render('checkbox');
|
|
}
|
|
|
|
function bindColSettingsEvents(layerIndex, $panel) {
|
|
$panel.off('.colSettings');
|
|
$panel.on('dragstart.colSettings', '.col-settings-item', function(e) {
|
|
dragField = this.getAttribute('data-field');
|
|
this.classList.add('dragging');
|
|
if (e.originalEvent && e.originalEvent.dataTransfer) {
|
|
e.originalEvent.dataTransfer.effectAllowed = 'move';
|
|
}
|
|
});
|
|
$panel.on('dragend.colSettings', '.col-settings-item', function() {
|
|
this.classList.remove('dragging');
|
|
$panel.find('.col-settings-item').removeClass('drag-over');
|
|
dragField = null;
|
|
});
|
|
$panel.on('dragover.colSettings', '.col-settings-item', function(e) {
|
|
e.preventDefault();
|
|
$panel.find('.col-settings-item').removeClass('drag-over');
|
|
this.classList.add('drag-over');
|
|
});
|
|
$panel.on('drop.colSettings', '.col-settings-item', function(e) {
|
|
e.preventDefault();
|
|
var targetField = this.getAttribute('data-field');
|
|
if (!dragField || !targetField || dragField === targetField) {
|
|
return;
|
|
}
|
|
var fromIdx = -1;
|
|
var toIdx = -1;
|
|
colConfig.forEach(function(item, idx) {
|
|
if (item.field === dragField) fromIdx = idx;
|
|
if (item.field === targetField) toIdx = idx;
|
|
});
|
|
if (fromIdx < 0 || toIdx < 0) {
|
|
return;
|
|
}
|
|
var moved = colConfig.splice(fromIdx, 1)[0];
|
|
colConfig.splice(toIdx, 0, moved);
|
|
refreshColSettingsPanel($panel);
|
|
});
|
|
|
|
$panel.on('click.colSettings', '#col-settings-apply', function() {
|
|
var next = [];
|
|
$panel.find('.col-settings-item').each(function() {
|
|
var field = this.getAttribute('data-field');
|
|
var visible = $(this).find('.col-settings-check').prop('checked');
|
|
next.push({ field: field, visible: !!visible });
|
|
});
|
|
if (!next.some(function(item) { return item.visible; })) {
|
|
layer.msg('至少保留一列显示');
|
|
return;
|
|
}
|
|
saveColConfig(next);
|
|
reloadTableCols();
|
|
layer.close(layerIndex);
|
|
layer.msg('列设置已应用');
|
|
});
|
|
|
|
$panel.on('click.colSettings', '#col-settings-reset', function() {
|
|
colConfig = getDefaultColConfig();
|
|
refreshColSettingsPanel($panel);
|
|
});
|
|
}
|
|
|
|
function openColSettings() {
|
|
colConfig = loadColConfig();
|
|
var shell = '<div class="col-settings-body">' + renderColSettingsList() + '</div>';
|
|
var layerIndex = layer.open({
|
|
type: 1,
|
|
title: '表格列设置',
|
|
area: ['360px', '560px'],
|
|
shadeClose: true,
|
|
content: '<div class="col-settings-panel" style="padding:14px 16px 16px;">' + shell + '</div>',
|
|
success: function(layero) {
|
|
var $panel = layero.find('.col-settings-panel');
|
|
form.render('checkbox');
|
|
bindColSettingsEvents(layerIndex, $panel);
|
|
}
|
|
});
|
|
}
|
|
|
|
colConfig = loadColConfig();
|
|
|
|
table.render({
|
|
elem: '#ID-table-demo-search',
|
|
id: TABLE_ID,
|
|
url: 'datatable/datalist.php',
|
|
cols: buildCols(colConfig),
|
|
page: true,
|
|
limit: 50,
|
|
height: 800,
|
|
size: "sm",
|
|
skin: "line",
|
|
even: true,
|
|
done: function(res, curr, count, origin){
|
|
onTableDataRendered(res, count);
|
|
},
|
|
});
|
|
|
|
$('#btn-col-settings').on('click', openColSettings);
|
|
|
|
$(document).on('click', '.fp-hdata-btn', function(e) {
|
|
e.stopPropagation();
|
|
var id = this.getAttribute('data-explain-id');
|
|
var logId = this.getAttribute('data-log-id');
|
|
var field = this.getAttribute('data-field');
|
|
var full = this.getAttribute('data-full') || '';
|
|
|
|
if (id) {
|
|
if (window.__fpExplainCache && window.__fpExplainCache[id]) {
|
|
layer.open({
|
|
type: 1,
|
|
title: '二次风控判定解析 #' + id,
|
|
area: ['720px', '85vh'],
|
|
shadeClose: true,
|
|
content: '<div style="padding:16px 18px;max-height:calc(85vh - 60px);overflow:auto;">' + window.__fpExplainCache[id] + '</div>'
|
|
});
|
|
return;
|
|
}
|
|
var loadIdx = layer.load(1);
|
|
$.getJSON('datatable/fp_explain.php', { id: id }, function(res) {
|
|
layer.close(loadIdx);
|
|
if (!res || res.code !== 0 || !res.html) {
|
|
layer.msg((res && res.msg) ? res.msg : '加载判定详情失败');
|
|
return;
|
|
}
|
|
window.__fpExplainCache = window.__fpExplainCache || {};
|
|
window.__fpExplainCache[id] = res.html;
|
|
layer.open({
|
|
type: 1,
|
|
title: '二次风控判定解析 #' + id,
|
|
area: ['720px', '85vh'],
|
|
shadeClose: true,
|
|
content: '<div style="padding:16px 18px;max-height:calc(85vh - 60px);overflow:auto;">' + res.html + '</div>'
|
|
});
|
|
}).fail(function() {
|
|
layer.close(loadIdx);
|
|
layer.msg('加载判定详情失败');
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (logId && field) {
|
|
var popupTitle = this.getAttribute('data-title') || (field === 'fp_hdata' ? '指纹 hdata' : field);
|
|
var isJson = this.classList.contains('fp-hdata-btn-json') || field.indexOf('api_') === 0 || field === 'fp_hdata';
|
|
var loadIdx2 = layer.load(1);
|
|
$.getJSON('datatable/log_blob.php', { id: logId, field: field }, function(res) {
|
|
layer.close(loadIdx2);
|
|
if (!res || res.code !== 0 || !res.data) {
|
|
layer.msg((res && res.msg) ? res.msg : '加载失败');
|
|
return;
|
|
}
|
|
var body = isJson ? escapeHtml(formatJsonPretty(res.data)) : escapeHtml(res.data);
|
|
layer.open({
|
|
type: 1,
|
|
title: popupTitle + ' #' + logId,
|
|
area: ['680px', '70vh'],
|
|
shadeClose: true,
|
|
content: '<pre style="padding:14px;margin:0;max-height:calc(70vh - 50px);overflow:auto;font-size:12px;white-space:pre-wrap;word-break:break-all;">' + body + '</pre>'
|
|
});
|
|
}).fail(function() {
|
|
layer.close(loadIdx2);
|
|
layer.msg('加载失败');
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (full) {
|
|
var popupTitleStatic = this.getAttribute('data-title') || '指纹 hdata';
|
|
var bodyStatic = this.classList.contains('fp-hdata-btn-json')
|
|
? escapeHtml(formatJsonPretty(full))
|
|
: escapeHtml(full);
|
|
layer.open({
|
|
type: 1,
|
|
title: popupTitleStatic,
|
|
area: ['680px', '70vh'],
|
|
shadeClose: true,
|
|
content: '<pre style="padding:14px;margin:0;max-height:calc(70vh - 50px);overflow:auto;font-size:12px;white-space:pre-wrap;word-break:break-all;">' + bodyStatic + '</pre>'
|
|
});
|
|
}
|
|
});
|
|
$(document).on('dblclick', '.cell-copy-link', function() {
|
|
var full = this.getAttribute('data-full') || '';
|
|
if (!full) return;
|
|
var notify = function() { layer.msg('已复制完整内容'); };
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
navigator.clipboard.writeText(full).then(notify).catch(function() {
|
|
layer.msg('复制失败');
|
|
});
|
|
return;
|
|
}
|
|
var ta = document.createElement('textarea');
|
|
ta.value = full;
|
|
document.body.appendChild(ta);
|
|
ta.select();
|
|
try {
|
|
document.execCommand('copy');
|
|
notify();
|
|
} catch (e) {
|
|
layer.msg('复制失败');
|
|
}
|
|
document.body.removeChild(ta);
|
|
});
|
|
|
|
// 日期
|
|
laydate.render({
|
|
elem: '.demo-table-search-date'
|
|
});
|
|
// 搜索提交
|
|
form.on('submit(demo-table-search)', function(data){
|
|
var field = data.field; // 获得表单字段
|
|
// 执行搜索重载
|
|
table.reload('ID-table-demo-search', {
|
|
page: {
|
|
curr: 1 // 重新从第 1 页开始
|
|
},
|
|
where: field // 搜索的字段
|
|
});
|
|
// layer.msg('搜索成功<br>此处为静态模拟数据,实际使用时换成真实接口即可');
|
|
return false; // 阻止默认 form 跳转
|
|
});
|
|
// table.reload('ID-table-demo-search', {
|
|
// page: {
|
|
// curr: 1 // 重新从第 1 页开始
|
|
// },
|
|
// });
|
|
var util = layui.util;
|
|
util.on('lay-on', {
|
|
confirm: function(){
|
|
layer.confirm('确定清除所有日志数据?', {
|
|
btn: ['确定', '关闭'] //按钮
|
|
}, function(){
|
|
$.get("datatable/datalist.php", {clear: "true"}, function(result){
|
|
layer.msg('清除成功', {icon: 1});
|
|
window.location.reload();
|
|
});
|
|
}, function(){
|
|
|
|
});
|
|
},
|
|
});
|
|
});
|
|
|
|
// 刷新按钮
|
|
function resetTable() {
|
|
var table = layui.table;
|
|
table.reload('ID-table-demo-search', {
|
|
page: {
|
|
curr: 1 // 重新从第 1 页开始
|
|
},
|
|
});
|
|
}
|
|
|
|
$("#export").click(function() {
|
|
var visit_date = $("input[name=visit_date]").val();
|
|
var domain = $("input[name=domain]").val();
|
|
var spage = $("input[name=spage]").val();
|
|
var country = $("input[name=country]").val();
|
|
var IP = $("input[name=IP]").val();
|
|
var result = $("select[name=result]").val();
|
|
|
|
var url = 'datatable/export.php?';
|
|
var args = [];
|
|
|
|
if(visit_date) {
|
|
args.push( "visit_date=" + visit_date );
|
|
}
|
|
|
|
if(domain) {
|
|
args.push( "domain=" + domain );
|
|
}
|
|
|
|
if(spage) {
|
|
args.push( "spage=" + spage );
|
|
}
|
|
|
|
if(country) {
|
|
args.push( "country=" + country );
|
|
}
|
|
|
|
if(IP) {
|
|
args.push( "IP=" + IP );
|
|
}
|
|
|
|
if(result) {
|
|
args.push( "result=" + encodeURIComponent(result) );
|
|
}
|
|
|
|
window.location.href=url + args.join("&");
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|