缩略图懒生成 + 评论登录限流:时间线不再拉原图

- 新增 /uploads/thumb/{key}?w=:首访从存储端读一次原图,标准库盒均值缩放后
  落盘 data/.thumbnail_cache(按内容哈希命名,天然失效);失败写 .failed 冷却
  一小时,非图片/超大一律 302 回原图,前端无感。原图路由改用 ServeContent,
  补上视频拖动进度条所需的 Range 支持
- 前端 thumbURL/thumbifyHtml 接入两套 UI 的时间线配图、长文封面与短文正文,
  并补 loading=lazy;详情页与灯箱仍用原图。/api/site 改为
  {settings, uploads_public_base},让前端识别哪些直链属于自家存储
- 后台登录的滑动窗口限速器抽成 internal/ratelimit 共享包(调用面不变),
  新增:读者登录失败按 IP 20 次/10 分钟、评论写入按读者 5 条/分钟
  (站主豁免,且只计成功写入)
This commit is contained in:
Sakurasan committed 2026-09-28 13:34:19 +08:00
1 parent b6342a622d
commit 6108aca34c
17 files changed
+822 -71

No files matched your search

+34
View File
@@ -6,6 +6,40 @@ export function sanitizeHtml(html) {
return DOMPurify.sanitize(html || '')
}
// ---------- 缩略图 ----------
// 时间线只展示小图:把我们存储上的图片直链改写为 /uploads/thumb/{key}?w=,
// 后端懒生成并永久缓存。识别规则:以 uploads_public_base 开头的直链,
// 或同源的 /uploads/ 相对路径(本地存储兜底)。外链(图床/头像)原样返回。
// base 由 site.js 拉到站点设置后经 setThumbBase 注入(不走 import 避免循环依赖)。
const IMG_EXT = /\.(jpe?g|png|gif)([?#].*)?$/i
let thumbBase = ''
export function setThumbBase(base) {
thumbBase = (base || '').replace(/\/+$/, '')
}
export function thumbURL(u, w = 480) {
if (!u || typeof u !== 'string') return u
let key = ''
if (thumbBase && u.startsWith(thumbBase + '/')) key = u.slice(thumbBase.length + 1)
else if (u.startsWith('/uploads/') && !u.startsWith('/uploads/thumb/')) key = u.slice('/uploads/'.length)
else return u
if (!IMG_EXT.test(key)) return u
return '/uploads/thumb/' + key + '?w=' + w
}
// 时间线正文 HTML(短文全文 / 长文 600 字符截断)里的 <img> 换缩略图 + 补懒加载。
// 只在展示层重写,库里存的正文不动。截断的 HTML 可能带半截标签,
// 正则只匹配完整的 <img ...src="..."> ,坏尾巴交给 sanitize/浏览器容错。
export function thumbifyHtml(html, w = 480) {
if (!html) return html
return html.replace(/<img\b[^>]*?>/gi, (tag) => {
let out = tag.replace(/\bsrc="([^"]*)"/i, (m, src) => 'src="' + thumbURL(src, w) + '"')
if (!/\bloading=/i.test(out)) out = out.replace(/^<img/i, '<img loading="lazy"')
return out
})
}
// 用 Intl.DateTimeFormat — locale 感知,未来要 i18n 只换 locale 即可
const longDateFmt = new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',