安全(P0) - 发布包签名:新增 scripts/package_keygen.php、scripts/package_verify.php、 ywxapp\service\PackageVerifier;FrameworkService::apply() 解压前强制验签 (ywxapp.framework_package_verify,默认开启) - JWT 收紧:config/jwt.php 移除公开默认密钥,JwtService::assertStrongSecret() 校验非空 / 长度≥32 / 已知弱值;.env.production.example 键名由 JWT_SECRET_KEY 修正为 JWT_SECRET - /api/v1/Article:免登录白名单收窄为只读接口,写操作字段白名单 + assertOwner() 越权校验 - 开发者令牌:查找接口不再回显明文、统一失败文案 + 限流 - TemplateInstaller:文件名白名单 + realpath 归一化 + 目录内断言,修复 Zip Slip;补 use ZipArchive - 后台登录:接入验证码、失败锁定与登录限流(config/ratelimit.php) - 缓存 / Session 驱动可由 .env 覆盖(CACHE_DRIVER / SESSION_TYPE),集群部署改 redis - paycenter 支付日志开关改为 PAY_LOG,不再跟随 APP_DEBUG 工程化 / 交付 - 新增 CHANGELOG.md 与 .env.production.example;composer.lock 入库锁定依赖版本 - 插件离线安装 CLI 增加 --uid,后台上传安装透传 uid,付费插件授权校验可通过 - 授权拒绝文案区分「未携带 uid」与「未找到有效授权」,便于排障 - .gitignore 放行 .env.production.example / .env.addon.example / .gitattributes / public/.htaccess 结构 - appmall 由 addon/appmall 迁移为独立应用 app/appmall(市场 / 开发者中心 / 审核 / 分润)
183 lines
8.0 KiB
JavaScript
183 lines
8.0 KiB
JavaScript
layui.define(['form', 'table', 'http', 'auth', 'laydate'], function (exports) {
|
|
var $ = layui.jquery,
|
|
form = layui.form,
|
|
table = layui.table,
|
|
layer = layui.layer,
|
|
http = layui.http,
|
|
laydate = layui.laydate,
|
|
auth = layui.auth;
|
|
var _savedPerms = (layui.data('backend').permission) || [];
|
|
auth.init(_savedPerms);
|
|
auth.setController('spider');
|
|
|
|
// 日期选择
|
|
laydate.render({ elem: '#dateInput' });
|
|
|
|
// 汇总 + 趋势(纯 CSS 迷你条形图,不依赖 echarts)
|
|
function loadStats() {
|
|
http.get('stats').then(function (res) {
|
|
if (res.code !== 0) { return; }
|
|
var summary = (res.data && res.data.summary) || [];
|
|
var trend = (res.data && res.data.trend) || { dates: [], series: [] };
|
|
var $body = $('#summaryBody').empty();
|
|
|
|
// DNS 熔断提示
|
|
if (res.data && res.data.dns_down) {
|
|
$('#dnsWarn').show().html('DNS 查询连续失败,真伪校验已临时降级(5 分钟后自动恢复)。请检查服务器 DNS 配置或 /etc/resolv.conf。');
|
|
} else {
|
|
$('#dnsWarn').hide();
|
|
}
|
|
|
|
if (summary.length === 0) {
|
|
$body.append('<tr><td colspan="8" style="text-align:center;color:#999;">暂无蜘蛛来访记录</td></tr>');
|
|
return;
|
|
}
|
|
|
|
var seriesMap = {};
|
|
(trend.series || []).forEach(function (s) { seriesMap[s.name] = s.data; });
|
|
|
|
// 蜘蛛下拉(去重,避免重复追加)
|
|
var $sel = $('#spiderSelect');
|
|
if ($sel.find('option').length <= 1) {
|
|
summary.forEach(function (row) {
|
|
$sel.append('<option value="' + row.spider + '">' + row.label + '</option>');
|
|
});
|
|
form.render('select');
|
|
}
|
|
|
|
summary.forEach(function (row) {
|
|
var line = seriesMap[row.label] || [];
|
|
var max = Math.max.apply(null, line.concat([1]));
|
|
var bars = line.map(function (v, i) {
|
|
var h = v > 0 ? Math.max(2, Math.round(v / max * 28)) : 1;
|
|
return '<span title="' + (trend.dates[i] || '') + ': ' + v + '" style="display:inline-block;width:5px;margin-right:1px;height:' + h + 'px;background:' + (v > 0 ? '#1E9FFF' : '#e2e2e2') + ';vertical-align:bottom;"></span>';
|
|
}).join('');
|
|
|
|
// 不支持 rDNS 校验的蜘蛛,真伪列显示占位说明
|
|
var realCell, fakeCell;
|
|
if (row.verifiable === false) {
|
|
realCell = '<span style="color:#999;" title="该蜘蛛官方不提供 rDNS,无法校验真伪">—</span>';
|
|
fakeCell = '<span style="color:#999;">—</span>';
|
|
} else {
|
|
realCell = '<span style="color:#118c26;">' + (row.real || 0) + '</span>';
|
|
fakeCell = (row.fake > 0)
|
|
? '<span style="color:#d81e06;font-weight:600;" title="伪造 UA 的抓取次数">' + row.fake + '</span>'
|
|
: '0';
|
|
}
|
|
|
|
$body.append('<tr>'
|
|
+ '<td>' + row.label + ' <span style="color:#999;">(' + row.spider + ')</span></td>'
|
|
+ '<td>' + row.today + '</td>'
|
|
+ '<td>' + row.week + '</td>'
|
|
+ '<td>' + row.month + '</td>'
|
|
+ '<td>' + row.total + '</td>'
|
|
+ '<td>' + realCell + '</td>'
|
|
+ '<td>' + fakeCell + '</td>'
|
|
+ '<td><div style="height:30px;line-height:30px;">' + bars + '</div></td>'
|
|
+ '</tr>');
|
|
});
|
|
});
|
|
}
|
|
loadStats();
|
|
|
|
// 明细表
|
|
table.render({
|
|
elem: '#dataTable',
|
|
url: 'index',
|
|
toolbar: '#tableBar',
|
|
defaultToolbar: ['filter', 'exports', 'print'],
|
|
parseData: function (res) {
|
|
return {
|
|
code: res.code === 0 ? 0 : 1,
|
|
msg: res.message || '',
|
|
count: res.count || (res.data ? res.data.length : 0),
|
|
data: res.data || []
|
|
};
|
|
},
|
|
cols: [[
|
|
{ field: 'id', width: 90, title: 'ID', sort: true },
|
|
{ field: 'spider_text', width: 110, title: '蜘蛛' },
|
|
{ field: 'verified_text', width: 100, title: '真伪', templet: function (d) {
|
|
if (d.verified === 1) {
|
|
return '<span style="color:#118c26;">真蜘蛛</span>';
|
|
}
|
|
if (d.verified === 2) {
|
|
return '<span style="color:#d81e06;font-weight:600;" title="' + (d.rdns || '无 rDNS 记录') + '">假蜘蛛</span>';
|
|
}
|
|
return '<span style="color:#999;" title="' + (d.verifiable ? '尚未校验或 DNS 暂不可用' : '该蜘蛛不支持 rDNS 校验') + '">待验证</span>';
|
|
}},
|
|
{ field: 'rdns', width: 220, title: 'rDNS', templet: function (d) {
|
|
return d.rdns ? '<span style="font-size:12px;">' + d.rdns + '</span>' : '<span style="color:#ccc;">—</span>';
|
|
}},
|
|
{ field: 'url', title: '抓取URL', minWidth: 220 },
|
|
{ field: 'ip', width: 140, title: '来源IP' },
|
|
{ field: 'app', width: 90, title: '应用' },
|
|
{ field: 'http_code', width: 80, title: '状态码' },
|
|
{ field: 'time_text', width: 170, title: '抓取时间' }
|
|
]],
|
|
limits: [15, 30, 50, 100],
|
|
limit: 15,
|
|
page: true,
|
|
skin: 'line',
|
|
done: function () {
|
|
var view = $('#dataTable').next('.layui-table-view');
|
|
auth.filterDom(view);
|
|
}
|
|
});
|
|
|
|
// 搜索
|
|
form.on('submit(data-search-btn)', function (data) {
|
|
table.reload('dataTable', { page: { curr: 1 }, where: data.field });
|
|
return false;
|
|
});
|
|
|
|
$('#btn-reset').on('click', function () {
|
|
$('input[name="ip"],input[name="url"],input[name="date"]').val('');
|
|
$('#spiderSelect').val('');
|
|
$('#verifiedSelect').val('');
|
|
form.render('select');
|
|
table.reload('dataTable', { page: { curr: 1 }, where: { spider: '', verified: '', ip: '', url: '', date: '' } });
|
|
});
|
|
|
|
// 工具条:重新校验 / 清理旧日志
|
|
table.on('toolbar(dataTable)', function (elem) {
|
|
if (elem.event === 'dataRecheck') {
|
|
layer.confirm('将对历史上「待验证」的蜘蛛记录重新做 rDNS 双向校验(每 IP 一次 DNS 查询,最多 200 个 IP),可能耗时数秒。是否继续?', {
|
|
title: '重新校验真伪',
|
|
btn: ['开始校验', '取消']
|
|
}, function (index) {
|
|
layer.close(index);
|
|
var loadIndex = layer.load(2, { shade: [0.3, '#000'] });
|
|
http.post('recheck', { limit: 200 }).then(function (res) {
|
|
layer.close(loadIndex);
|
|
if (res.code === 0) {
|
|
layer.msg(res.message || '校验完成', { icon: 1, time: 5000 });
|
|
table.reload('dataTable', {}, true);
|
|
loadStats();
|
|
} else {
|
|
layer.msg(res.msg || res.message || '校验失败', { icon: 2 });
|
|
}
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (elem.event === 'dataClear') {
|
|
layer.prompt({ title: '保留最近多少天的明细?(聚合报表不受影响)', value: '30' }, function (value, index) {
|
|
layer.close(index);
|
|
var days = parseInt(value, 10) || 30;
|
|
http.post('clear', { days: days }).then(function (res) {
|
|
if (res.code === 0) {
|
|
layer.msg(res.message || '清理完成');
|
|
table.reload('dataTable', {}, true);
|
|
} else {
|
|
layer.msg(res.msg || res.message || '清理失败');
|
|
}
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
exports('spider', {});
|
|
});
|