fix(友链): 修复申请「通过不了/删不掉」;feat(评论): 通知按文章作者分派

## 友链自助申请(通过不了 / 删除不掉)
根因:admin.js 全局事件委托把 data-id 一律 parseInt,
而友链申请 id 是字符串(lamus5uptzcdam)→ NaN → JSON 成 {"id":null}
→ 后端 String(null||'') 为空 → 400 id and action required,查看/通过/拒绝全失效。
引入于 a74b3c71(10-04 归档 rss-robot 时;后台只有友链申请用字符串 id)。

- admin.js:纯数字才转数字,其余原样(评论/用户/站点按钮请求完全不变)
- admin.js:待审卡片加「删除」按钮
- links.ts:新增 action:'delete'(不受「已处理过」限制,处理过的也能删)
- links.ts:approve 改为先写友链再改状态 + URL 去重(重试幂等)
- links.ts:已是目标状态时返回 {ok:true,already:true},不再 409
  —— 否则网络抖动后重试会让用户以为「怎么点都通不过」

## 评论通知按文章作者分派
编辑角色(女朋友)写的文章,评论通知改发给她,不再打扰博主。
不需要「文章→作者」同步表:Hugo 模板把 author_id 注入 artalkConfig,
artalk.js 已有的 fetch 拦截器把它塞进评论提交体,Worker 直接读。

- artalk.html:artalkConfig 增 pageAuthorId(老文章 0)
- artalk.js:提交评论时带上 author_id(读 window.artalkConfig,防 PJAX 过期)
- comments.ts:createComment 解析 author_id 并传给通知
- mail.ts:收件人改为「这篇文章的主人」——作者非管理员且开了邮件则发给他,
  否则回退博主;已被回复通知过 / 作者本人评论自己的文章都不重复发
- mail.ts:增一行收件人决策日志,便于线上排查

验证:Worker tsc 通过;Hugo 构建通过;artalk.js 语法通过;
友链本地跑通(通过 200 → 再通过 already:true → 友链仅 1 条 → 删除 200 → 再删 404)。
This commit is contained in:
zqlit committed 2026-10-05 16:44:15 +08:00
1 parent 68416366eb
commit 1b2e3d46f8
6 files changed
+124 -25

No files matched your search

+17 -1
View File
@@ -1728,7 +1728,23 @@ artalk.on('list-loaded', function() {
var method = String((init && init.method) || (input && input.method) || 'GET').toUpperCase();
if (method !== 'POST' || !POST_RE.test(url)) return origFetch.apply(this, arguments);
var p = origFetch.apply(this, arguments);
// ★ 把「当前页文章的作者 id」塞进评论提交体 —— 服务端凭它决定这封
// 「有新评论」的通知邮件发给文章作者本人,还是发博主。
// 读 window.artalkConfig 而不是闭包里的 config:PJAX 切页时 artalkConfig
// 会被重新赋值,闭包里那份会过期(把上一页的作者带过去就发错人了)。
var args = arguments;
var pageAuthorId = Number((window.artalkConfig || {}).pageAuthorId || 0);
if (pageAuthorId && init && typeof init.body === 'string') {
try {
var payload = JSON.parse(init.body);
if (payload && typeof payload === 'object' && !payload.author_id) {
payload.author_id = pageAuthorId;
args = [input, Object.assign({}, init, { body: JSON.stringify(payload) })];
}
} catch (e) { /* body 不是 JSON(理论上不会)→ 原样放行,不影响提交 */ }
}
var p = origFetch.apply(this, args);
return p.then(function (res) {
if (res && res.ok) {
// 成功:读一下 body 看是不是落进了审核队列(服务端返回的就是