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

+16 -2
View File
@@ -1123,7 +1123,8 @@ async function viewLinks() {
'<span class="c">' +
'<button class="btn tiny" data-act="l-ap-view" data-id="' + esc(a.id) + '">查看</button>' +
'<button class="btn tiny" data-act="l-ap-approve" data-id="' + esc(a.id) + '">通过</button>' +
'<button class="btn tiny danger" data-act="l-ap-reject" data-id="' + esc(a.id) + '">拒绝</button>' +
'<button class="btn tiny" data-act="l-ap-reject" data-id="' + esc(a.id) + '">拒绝</button>' +
'<button class="btn tiny danger" data-act="l-ap-del" data-id="' + esc(a.id) + '">删除</button>' +
'</span>' +
'</div>').join('');
appHtml =
@@ -1906,7 +1907,12 @@ document.addEventListener('click', async (e) => {
const el = e.target.closest('[data-act]');
if (!el) return;
const act = el.dataset.act;
const id = el.dataset.id ? parseInt(el.dataset.id, 10) : 0;
// ★ 不能无脑 parseInt:友链申请的 id 是字符串(la + base36,如 lamus5uptzcdam),
// parseInt 会得到 NaN,序列化成 JSON 变成 null —— 后端只看到空 id,一律 400
// 「id and action required」,表现就是「查看/通过/拒绝点了没反应」。
// 规矩:纯数字才转数字,其余原样保留。
const rawId = el.dataset.id || '';
const id = /^\d+$/.test(rawId) ? parseInt(rawId, 10) : rawId;
// --- 通用
if (act === 'modal-close') { closeModal(); return; }
@@ -2071,6 +2077,14 @@ document.addEventListener('click', async (e) => {
});
return;
}
if (act === 'l-ap-del') {
const a = (state.linkApps || []).find((x) => x.id === id);
confirmBox('删除申请记录', '确定删除「' + (a ? a.name : '') + '」这条申请吗?只清掉申请记录,已有的友链不受影响。', async () => {
await rssApi('/api/link-apply/review', { method: 'POST', body: { id, action: 'delete' } });
toast('✓ 已删除申请记录'); await viewLinks();
});
return;
}
// --- 友链
if (act === 'l-reload') { el.dataset.busy = '1'; try { await viewLinks(); } finally { delete el.dataset.busy; } return; }
if (act === 'l-add') { addLink(); return; }