Files
YwxAppThink/public/static/backend/modules/spider.js
T
yangwx 18101cf11a feat: 上线前安全加固与发布工程化;appmall 由插件迁移为独立应用
安全(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(市场 / 开发者中心 / 审核 / 分润)
2026-10-22 00:53:39 +08:00

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', {});
});