feat: 抓取进度可见 + 已入库秀预检(避免重复下载)

一、防重复下载
图片 key 是内容 sha1,不下载无法判断是否已存在;又没存 source_url,
于是每次重爬都把几百张已下过的图再下一遍(线上实测 Celine 62 分钟、0 新增)。
新增 GET /admin/internal/shows/covered?brand_uid=,按 (年份,季节,类型)
聚合已入库图片数,供 spider 抓前整场跳过。

二、进度可见
一次抓取可能跑几十分钟,此前只有 running/done,看不出「在干什么」,
也分不清「慢」和「卡死」。
- crawl_tasks 增加 progress_done/total/text/at
- POST /admin/internal/crawl/tasks/:id/progress(spider 每场 + 每批图片上报)
- GET /admin/crawl/live(页面 4 秒轮询,局部刷新状态/进度/已提交)
- progress_at 超过 10 分钟没更新标黄「可能卡住」
- 渲染测试守住进度列与轮询锚点
This commit is contained in:
toom1996
2026-10-25 21:22:56 +08:00
parent db3e3efab9
commit 51e3d13383
8 changed files with 408 additions and 21 deletions

View File

@ -16,15 +16,16 @@
<table>
<thead><tr>
<th>ID</th><th>品牌</th><th>状态</th><th>场次上限</th><th>已提交</th><th>尝试</th><th>Worker</th><th>更新时间</th><th>错误</th><th>操作</th>
<th>ID</th><th>品牌</th><th>状态</th><th>进度</th><th>场次上限</th><th>已提交</th><th>尝试</th><th>Worker</th><th>更新时间</th><th>错误</th><th>操作</th>
</tr></thead>
<tbody>
{{range .Rows}}<tr>
<td><code>{{.ID}}</code></td>
<td><a class="view" href="/admin/brands/{{.BrandUID}}">{{.BrandName}}</a></td>
<td><span class="badge">{{.StatusLabel}}</span></td>
<td><span class="badge" data-live-status="{{.ID}}">{{.StatusLabel}}</span></td>
<td class="progress{{if .Stale}} stale{{end}}" data-live-progress="{{.ID}}">{{if .ProgressLabel}}{{.ProgressLabel}}{{else}}—{{end}}</td>
<td>{{if eq .MaxShows 0}}<strong>全量</strong>{{else}}{{.MaxShows}}{{end}}</td>
<td>{{.Shows}}</td>
<td data-live-shows="{{.ID}}">{{.Shows}}</td>
<td>{{.Attempts}}</td>
<td class="muted">{{if .Worker}}{{.Worker}}{{else}}—{{end}}</td>
<td class="muted">{{.UpdatedAt}}</td>
@ -41,4 +42,42 @@
</table>
{{template "pager" .Pager}}
</div>
<script>
// 抓取进度轮询:每 4 秒只刷新「状态 / 进度 / 已提交」三列。
//
// 为什么不用 WebSocket:进度是每场秀一次的低频事件,轮询足够;
// 而且局部更新避免了整页刷新——整页刷会打断滚动位置和正在填的表单。
// 为什么不用整页 meta refresh:同上,且会把「失败原因」的展开状态一起冲掉。
(function () {
var cells = document.querySelectorAll('[data-live-progress]');
if (!cells.length) return;
function paint(items) {
var byId = {};
for (var i = 0; i < items.length; i++) byId[String(items[i].id)] = items[i];
for (var j = 0; j < cells.length; j++) {
var cell = cells[j];
var id = cell.getAttribute('data-live-progress');
var it = byId[id];
if (!it) continue;
cell.textContent = it.progress || '—';
if (it.stale) cell.classList.add('stale'); else cell.classList.remove('stale');
var st = document.querySelector('[data-live-status="' + id + '"]');
if (st) st.textContent = it.status_label;
var sh = document.querySelector('[data-live-shows="' + id + '"]');
if (sh) sh.textContent = it.shows;
}
}
function tick() {
fetch('/admin/crawl/live', { credentials: 'same-origin' })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) { if (d && d.items) paint(d.items); })
.catch(function () { /* 网络抖动忽略,下一轮再拉 */ });
}
setInterval(tick, 4000);
tick();
})();
</script>
{{end}}