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

## 友链自助申请(通过不了 / 删除不掉)
根因:admin.js 全局事件委托把 data-id 一律 parseInt,
而友链申请 id 是字符串(lamus5uptzcdam)→ NaN → JSON 成 {"id":null}
→ 后端 String(null||'') 为空 → 400 id and action required,查看/通过/拒绝全失效。
引入于 299ab57e(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 24f68040c2
commit 57bd4ea737
6 files changed
+124 -25

No files matched your search

+47 -15
View File
@@ -210,38 +210,70 @@ export async function linkApplyReview(request: Request, env: Env): Promise<Respo
return respond({ error: 'invalid json' }, 400);
}
const id = String(body.id || '');
const action = body.action === 'approve' ? 'approve' : body.action === 'reject' ? 'reject' : '';
const action =
body.action === 'approve' ? 'approve'
: body.action === 'reject' ? 'reject'
: body.action === 'delete' ? 'delete'
: '';
if (!id || !action) return respond({ error: 'id and action required' }, 400);
const st = await kvGetJson<{ apps: LinkApplication[] }>(env, 'link_applications', { apps: [] });
const app = st.apps.find((a) => a.id === id);
if (!app) return respond({ error: '申请不存在' }, 404);
if (app.status !== 'pending') return respond({ error: '该申请已处理过' }, 409);
app.status = action === 'approve' ? 'approved' : 'rejected';
// 删除:清理记录用。**不受「已处理过」限制** —— 否则处理过的申请就永远删不掉了。
if (action === 'delete') {
st.apps = st.apps.filter((a) => a.id !== id);
await kvPutJson(env, 'link_applications', st);
return respond({ ok: true, deleted: id });
}
if (app.status !== 'pending') {
// 幂等:状态已经是目标状态就直接算成功。
// 跨境链路会偶发握手失败(后台前端此时会弹错),用户自然会再点一次 ——
// 若这里回 409「该申请已处理过」,他会以为「怎么点都通不过」,
// 而实际上第一次早就通过了。重复的「通过」不会再发一遍邮件。
if (
(action === 'approve' && app.status === 'approved') ||
(action === 'reject' && app.status === 'rejected')
) {
return respond({ ok: true, already: true });
}
return respond({ error: '该申请已处理过' }, 409);
}
app.feedback = String(body.feedback || '').slice(0, 300);
app.reviewedAt = new Date().toISOString();
await kvPutJson(env, 'link_applications', st);
let mailOk = false;
if (action === 'approve') {
// 写入友链
// ★ 顺序要紧:**先写友链、再改申请状态**。
// 反过来的话(旧实现就是),一旦写 friend_links 失败,申请已经变成 approved,
// 用户再点只会得到「该申请已处理过」—— 友链永远加不上、也重试不了。
// 顺带做去重:该 URL 已在友链里就别再 push 一条重复的(重试因此是幂等的)。
const links = await kvGetJson<{ links: Link[] }>(env, 'friend_links', { links: [] });
links.links.push({
name: app.name,
url: app.url,
// 申请人没填图标 → 用站内 favicon 服务自动抓(页面/友圈直接用这个地址)
image: app.image || `/api/favicon?url=${encodeURIComponent(app.url)}`,
description: app.description || '',
rss: app.feed,
addedAt: new Date().toISOString(),
});
await kvPutJson(env, 'friend_links', links);
const dup = links.links.some((l) => normUrl(l.url) === normUrl(app.url));
if (!dup) {
links.links.push({
name: app.name,
url: app.url,
// 申请人没填图标 → 用站内 favicon 服务自动抓(页面/友圈直接用这个地址)
image: app.image || `/api/favicon?url=${encodeURIComponent(app.url)}`,
description: app.description || '',
rss: app.feed,
addedAt: new Date().toISOString(),
});
await kvPutJson(env, 'friend_links', links);
}
app.status = 'approved';
await kvPutJson(env, 'link_applications', st);
mailOk = await sendMailSafe(env, app.email,
`✅ 你的友链申请已通过:${app.name}`,
`<p>你的友链申请(<a href="${app.url}">${app.url}</a>)已审核通过,现已加进友链列表,感谢支持!</p>` +
(app.feedback ? `<p>站长留言:${app.feedback}</p>` : ''));
} else {
app.status = 'rejected';
await kvPutJson(env, 'link_applications', st);
mailOk = await sendMailSafe(env, app.email,
`关于你的友链申请:${app.name}`,
`<p>很抱歉,你的友链申请(<a href="${app.url}">${app.url}</a>)本次未能通过。</p>` +