refactor(admin): 重构评论管理界面样式和功能

feat(login): 添加API地址配置功能并支持本地存储

fix(api): 移除CWD_CONFIG_KV相关配置改用数据库存储
fix(cors): 修改跨域配置允许所有来源

perf(email): 添加邮件发送日志记录和调试信息
perf(comments): 优化评论提交和邮件通知逻辑

docs(backend): 更新后端配置文档移除CWD_CONFIG_KV相关说明

chore: 更新端口号和示例配置
This commit is contained in:
anghunk
2026-01-19 16:08:14 +08:00
parent 3d0e3a317a
commit e6e21dff61
17 changed files with 636 additions and 355 deletions

View File

@@ -3,14 +3,14 @@ import { Bindings } from '../../bindings';
export const getAdminEmail = async (c: Context<{ Bindings: Bindings }>) => {
try {
let email: string | null = null;
if (c.env.CWD_CONFIG_KV) {
email = await c.env.CWD_CONFIG_KV.get('settings:admin_notify_email');
}
if (!email) {
email = c.env.EMAIL_ADDRESS || null;
}
return c.json({ email });
await c.env.CWD_DB.prepare(
'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)'
).run();
const row = await c.env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?')
.bind('admin_notify_email')
.first<{ value: string }>();
const email = row?.value || c.env.EMAIL_ADDRESS || null;
return c.json({ email: email });
} catch (e: any) {
return c.json({ message: e.message }, 500);
}

View File

@@ -11,10 +11,12 @@ export const setAdminEmail = async (c: Context<{ Bindings: Bindings }>) => {
if (!email || !isValidEmail(email)) {
return c.json({ message: '邮箱格式不正确' }, 400);
}
if (!c.env.CWD_CONFIG_KV) {
return c.json({ message: '未配置 CWD_CONFIG_KV无法保存设置' }, 500);
}
await c.env.CWD_CONFIG_KV.put('settings:admin_notify_email', email);
await c.env.CWD_DB.prepare(
'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)'
).run();
await c.env.CWD_DB.prepare('REPLACE INTO Settings (key, value) VALUES (?, ?)')
.bind('admin_notify_email', email)
.run();
return c.json({ message: '保存成功' });
} catch (e: any) {
return c.json({ message: e.message }, 500);

View File

@@ -58,6 +58,17 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
// 3. 准备数据
const content = checkContent(rawContent);
const author = checkContent(rawAuthor);
console.log('PostComment:request', {
postSlug: post_slug,
hasParent: !!parent_id,
author,
email,
ip,
hasSendEmailBinding: !!c.env.SEND_EMAIL,
fromEmail: c.env.CF_FROM_EMAIL,
emailAddressEnv: c.env.EMAIL_ADDRESS
});
const uaParser = new UAParser(userAgent);
const uaResult = uaParser.getResult();
@@ -88,12 +99,20 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
if (!success) throw new Error("Database insert failed");
console.log('PostComment:inserted', {
postSlug: post_slug,
hasParent: !!parent_id,
ip
});
// 5. 发送邮件通知 (后台异步执行,不阻塞用户响应)
if (c.env.SEND_EMAIL && c.env.CF_FROM_EMAIL) {
console.log('PostComment:mailDispatch:start', {
hasParent: !!data.parent_id
});
c.executionCtx.waitUntil((async () => {
try {
if (data.parent_id) {
// 回复逻辑:查询父评论信息
const parentComment = await c.env.CWD_DB.prepare(
"SELECT author, email, content_text FROM Comment WHERE id = ?"
).bind(data.parent_id).first<{ author: string, email: string, content_text: string }>();
@@ -104,6 +123,10 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
).bind(parentComment.email).first<{ created_at: string }>();
const canSendUserMail = !recentUserMail || (Date.now() - new Date(recentUserMail.created_at).getTime() > 60 * 1000);
if (canSendUserMail) {
console.log('PostComment:mailDispatch:userReply:send', {
toEmail: parentComment.email,
toName: parentComment.author
});
await sendCommentReplyNotification(c.env, {
toEmail: parentComment.email,
toName: parentComment.author,
@@ -116,15 +139,23 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
await c.env.CWD_DB.prepare(
"INSERT INTO EmailLog (recipient, type, ip_address, created_at) VALUES (?, ?, ?, ?)"
).bind(parentComment.email, 'user-reply', ip, new Date().toISOString()).run();
console.log('PostComment:mailDispatch:userReply:logInserted', {
toEmail: parentComment.email
});
}
if (!canSendUserMail) {
console.log('PostComment:mailDispatch:userReply:skippedByRateLimit', {
toEmail: parentComment.email
});
}
}
} else {
// 新评论通知站长
const adminEmailRow = await c.env.CWD_DB.prepare(
"SELECT created_at FROM EmailLog WHERE type = 'admin-notify' ORDER BY created_at DESC LIMIT 1"
).first<{ created_at: string }>();
const canSendAdminMail = !adminEmailRow || (Date.now() - new Date(adminEmailRow.created_at).getTime() > 15 * 1000);
if (canSendAdminMail) {
console.log('PostComment:mailDispatch:admin:send');
await sendCommentNotification(c.env, {
postTitle: data.post_title,
postUrl: data.post_url,
@@ -134,12 +165,21 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
await c.env.CWD_DB.prepare(
"INSERT INTO EmailLog (recipient, type, ip_address, created_at) VALUES (?, ?, ?, ?)"
).bind('admin', 'admin-notify', ip, new Date().toISOString()).run();
console.log('PostComment:mailDispatch:admin:logInserted');
}
if (!canSendAdminMail) {
console.log('PostComment:mailDispatch:admin:skippedByRateLimit');
}
}
} catch (mailError) {
console.error("Mail Notification Failed:", mailError);
}
})());
} else {
console.log('PostComment:mailDispatch:skipNoBinding', {
hasSendEmailBinding: !!c.env.SEND_EMAIL,
fromEmail: c.env.CF_FROM_EMAIL
});
}
return c.json({ message: "Comment submitted. Awaiting moderation." });

View File

@@ -1,7 +1,6 @@
export type Bindings = {
CWD_DB: D1Database
CWD_AUTH_KV: KVNamespace;
CWD_CONFIG_KV?: KVNamespace;
ALLOW_ORIGIN: string
CF_FROM_EMAIL?: string
SEND_EMAIL?: {

View File

@@ -14,8 +14,24 @@ import { getAdminEmail } from './api/admin/getAdminEmail';
import { setAdminEmail } from './api/admin/setAdminEmail';
const app = new Hono<{ Bindings: Bindings }>();
const VERSION = 'v0.0.1';
app.use('*', async (c, next) => {
console.log('Request:start', {
method: c.req.method,
path: c.req.path,
url: c.req.url,
hasDb: !!c.env.CWD_DB,
hasAuthKv: !!c.env.CWD_AUTH_KV
});
const res = await next();
console.log('Request:end', {
method: c.req.method,
path: c.req.path
});
return res;
});
// 跨域
app.use('/api/*', async (c, next) => {
const corsMiddleware = customCors(c.env.ALLOW_ORIGIN);
return corsMiddleware(c, next);
@@ -25,7 +41,12 @@ app.use('/admin/*', async (c, next) => {
return corsMiddleware(c, next);
});
// API
app.get('/', (c) => {
return c.html(
`CWD 评论部署成功,当前版本 ${VERSION}<a href="https://github.com/anghunk/cwd-comments" target="_blank" rel="noreferrer">查看文档</a>`
);
});
app.get('/api/comments', getComments);
app.post('/api/comments', postComment);

View File

@@ -1,23 +1,12 @@
import { cors } from 'hono/cors'
export const customCors = (allowOriginStr: string | undefined) => {
// 1. 将环境变量字符串解析为数组
// 如果环境变量不存在,则默认为空数组
const allowedOrigins = allowOriginStr
? allowOriginStr.split(',').map(origin => origin.trim())
: []
export const customCors = (_allowOriginStr: string | undefined) => {
return cors({
origin: (origin) => {
// 如果请求的 origin 在白名单中,或者是本地文件(null)
if (!origin || allowedOrigins.includes(origin)) {
return origin
}
},
origin: '*',
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization'],
exposeHeaders: ['Content-Length'],
maxAge: 600,
credentials: true,
credentials: false,
})
}
}

View File

@@ -17,6 +17,14 @@ export async function sendCommentReplyNotification(
) {
const { toEmail, toName, postTitle, parentComment, replyAuthor, replyContent, postUrl } = params;
console.log('EmailReplyNotification:start', {
toEmail,
toName,
postTitle,
fromEmail: env.CF_FROM_EMAIL,
hasSendBinding: !!env.SEND_EMAIL
});
const html = `
<div style="font-family: sans-serif; line-height: 1.6; color: #333;">
<p>Hi <b>${toName}</b></p>
@@ -39,6 +47,10 @@ export async function sendCommentReplyNotification(
`;
if (!env.SEND_EMAIL || !env.CF_FROM_EMAIL) {
console.error('EmailReplyNotification:missingBinding', {
hasSendBinding: !!env.SEND_EMAIL,
fromEmail: env.CF_FROM_EMAIL
});
throw new Error('未配置邮件发送绑定或发件人地址');
}
@@ -48,6 +60,10 @@ export async function sendCommentReplyNotification(
subject: `你在 example.com 上的评论有了新回复`,
html
});
console.log('EmailReplyNotification:sent', {
toEmail
});
}
/**
@@ -65,6 +81,13 @@ export async function sendCommentNotification(
const { postTitle, postUrl, commentAuthor, commentContent } = params;
const toEmail = await getAdminNotifyEmail(env);
console.log('EmailAdminNotification:start', {
toEmail,
postTitle,
fromEmail: env.CF_FROM_EMAIL,
hasSendBinding: !!env.SEND_EMAIL
});
const html = `
<div style="font-family: sans-serif;">
<p><b>${commentAuthor}</b> 在文章《${postTitle}》下发表了评论:</p>
@@ -76,6 +99,10 @@ export async function sendCommentNotification(
`;
if (!env.SEND_EMAIL || !env.CF_FROM_EMAIL) {
console.error('EmailAdminNotification:missingBinding', {
hasSendBinding: !!env.SEND_EMAIL,
fromEmail: env.CF_FROM_EMAIL
});
throw new Error('未配置邮件发送绑定或发件人地址');
}
@@ -85,14 +112,31 @@ export async function sendCommentNotification(
subject: `新评论通知:${postTitle}`,
html
});
console.log('EmailAdminNotification:sent', {
toEmail
});
}
// 读取管理员通知邮箱:优先 KV 设置,其次环境变量
async function getAdminNotifyEmail(env: Bindings): Promise<string> {
if (env.CWD_CONFIG_KV) {
const val = await env.CWD_CONFIG_KV.get('settings:admin_notify_email');
if (val) return val;
await env.CWD_DB.prepare(
'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)'
).run();
const row = await env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?')
.bind('admin_notify_email')
.first<{ value: string }>();
if (row?.value) {
console.log('EmailAdminNotification:useDbEmail', {
email: row.value
});
return row.value;
}
if (env.EMAIL_ADDRESS) return env.EMAIL_ADDRESS;
if (env.EMAIL_ADDRESS) {
console.log('EmailAdminNotification:useEnvEmail', {
email: env.EMAIL_ADDRESS
});
return env.EMAIL_ADDRESS;
}
console.error('EmailAdminNotification:noAdminEmail');
throw new Error('未配置管理员通知邮箱');
}