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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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." });
|
||||
|
||||
@@ -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?: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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('未配置管理员通知邮箱');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user