refactor(comment): 统一字段命名并改用时间戳存储创建时间

将 author 字段重命名为 name,pub_date 改为 created 并使用时间戳存储
更新相关 API、数据库 schema 和前端组件以适配新字段
同时将 user_agent 简化为 ua 并改进日期处理逻辑
This commit is contained in:
anghunk
2026-01-20 13:50:08 +08:00
parent 520cfcc447
commit eb0a7a32aa
15 changed files with 112 additions and 99 deletions

File diff suppressed because one or more lines are too long

View File

@@ -8,8 +8,8 @@ export type AdminLoginResponse = {
export type CommentItem = {
id: number;
pubDate: string;
author: string;
created: number;
name: string;
email: string;
avatar: string;
postSlug: string;
@@ -18,6 +18,7 @@ export type CommentItem = {
contentText: string;
contentHtml: string;
status: string;
ua?: string | null;
};
export type CommentListResponse = {

View File

@@ -32,12 +32,12 @@
v-if="item.avatar"
:src="item.avatar"
class="cell-avatar"
:alt="item.author"
:alt="item.name"
/>
<div class="cell-author-main">
<div class="cell-author-name">{{ item.author }}</div>
<div class="cell-author-name">{{ item.name }}</div>
<div class="cell-author-email">{{ item.email }}</div>
<span class="cell-time">{{ formatDate(item.pubDate) }}</span>
<span class="cell-time">{{ formatDate(item.created) }}</span>
</div>
</div>
</div>
@@ -138,10 +138,10 @@ const filteredComments = computed(() => {
return comments.value.filter((item) => item.status === statusFilter.value);
});
function formatDate(value: string) {
function formatDate(value: number) {
const d = new Date(value);
if (Number.isNaN(d.getTime())) {
return value;
return String(value);
}
return d.toLocaleString();
}

View File

@@ -1,16 +1,16 @@
-- create comment table
CREATE TABLE IF NOT EXISTS Comment (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pub_date DATETIME DEFAULT CURRENT_TIMESTAMP,
created INTEGER NOT NULL,
post_slug TEXT NOT NULL,
author TEXT NOT NULL,
name TEXT NOT NULL,
email TEXT NOT NULL,
url TEXT,
ip_address TEXT,
device TEXT,
os TEXT,
browser TEXT,
user_agent TEXT,
ua TEXT,
content_text TEXT NOT NULL,
content_html TEXT NOT NULL,
parent_id INTEGER,

View File

@@ -4,7 +4,7 @@ import { Bindings } from '../../bindings';
export const exportComments = async (c: Context<{ Bindings: Bindings }>) => {
try {
const { results } = await c.env.CWD_DB.prepare(
'SELECT * FROM Comment ORDER BY pub_date DESC'
'SELECT * FROM Comment ORDER BY created DESC'
).all();
return c.json(results);

View File

@@ -25,16 +25,16 @@ export const importComments = async (c: Context<{ Bindings: Bindings }>) => {
const stmts = comments.map((comment: any) => {
const {
id,
pub_date,
created,
post_slug,
author,
name,
email,
url,
ip_address,
device,
os,
browser,
user_agent,
ua,
content_text,
content_html,
parent_id,
@@ -48,15 +48,26 @@ export const importComments = async (c: Context<{ Bindings: Bindings }>) => {
// 构建 SQL。
// 能够处理 id 存在或不存在的情况
const fields = [
'pub_date', 'post_slug', 'author', 'email', 'url',
'ip_address', 'device', 'os', 'browser', 'user_agent',
'created', 'post_slug', 'name', 'email', 'url',
'ip_address', 'device', 'os', 'browser', 'ua',
'content_text', 'content_html', 'parent_id', 'status'
];
const values = [
pub_date, post_slug, author, email, url,
ip_address, device, os, browser, user_agent,
content_text, content_html, parent_id, status
].map(v => v === undefined ? null : v);
created || Date.now(),
post_slug || "",
name || "Anonymous",
email || "",
url || null,
ip_address || null,
device || null,
os || null,
browser || null,
ua || null,
content_text || "",
content_html || "",
parent_id || null,
status || "approved"
];
if (id) {
fields.unshift('id');

View File

@@ -14,7 +14,7 @@ export const listComments = async (c: Context<{ Bindings: Bindings }>) => {
).first<{ count: number }>();
const { results } = await c.env.CWD_DB.prepare(
'SELECT * FROM Comment ORDER BY pub_date DESC LIMIT ? OFFSET ?'
'SELECT * FROM Comment ORDER BY created DESC LIMIT ? OFFSET ?'
)
.bind(limit, offset)
.all();
@@ -30,8 +30,8 @@ export const listComments = async (c: Context<{ Bindings: Bindings }>) => {
const data = await Promise.all(
results.map(async (row: any) => ({
id: row.id,
pubDate: row.pub_date,
author: row.author,
created: row.created,
name: row.name,
email: row.email,
postSlug: row.post_slug,
url: row.url,
@@ -39,6 +39,7 @@ export const listComments = async (c: Context<{ Bindings: Bindings }>) => {
contentText: row.content_text,
contentHtml: row.content_html,
status: row.status,
ua: row.ua,
avatar: await getCravatar(row.email, avatarPrefix || undefined)
}))
);

View File

@@ -15,12 +15,12 @@ export const getComments = async (c: Context<{ Bindings: Bindings }>) => {
try {
// 1. 查询审核通过的评论
const query = `
SELECT id, author, email, url, content_text as contentText,
content_html as contentHtml, pub_date as pubDate, parent_id as parentId,
SELECT id, name, email, url, content_text as contentText,
content_html as contentHtml, created, parent_id as parentId,
post_slug as postSlug
FROM Comment
WHERE post_slug = ? AND status = "approved"
ORDER BY pub_date DESC
ORDER BY created DESC
`
const { results } = await c.env.CWD_DB.prepare(query).bind(post_slug).all()
@@ -52,7 +52,7 @@ export const getComments = async (c: Context<{ Bindings: Bindings }>) => {
// 获取直接父评论的作者名
const parentComment = commentMap.get(comment.parentId)
if (parentComment) {
comment.replyToAuthor = parentComment.author
comment.replyToAuthor = parentComment.name
}
// 向上查找根评论
@@ -74,7 +74,7 @@ export const getComments = async (c: Context<{ Bindings: Bindings }>) => {
// 对每个根评论的 replies 按时间正序排列
rootComments.forEach(root => {
root.replies.sort((a: any, b: any) =>
new Date(a.pubDate).getTime() - new Date(b.pubDate).getTime()
a.created - b.created
)
})

View File

@@ -22,7 +22,7 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
if (!data || typeof data !== 'object') {
return c.json({ message: '无效的请求体' }, 400);
}
const { post_slug, content: rawContent, author: rawAuthor, email, url, post_title, post_url } = data;
const { post_slug, content: rawContent, name: rawName, email, url, post_title, post_url } = data;
const parentId = (data as any).parent_id ?? (data as any).parentId ?? null;
if (!post_slug || typeof post_slug !== 'string') {
return c.json({ message: 'post_slug 必填' }, 400);
@@ -30,7 +30,7 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
if (!rawContent || typeof rawContent !== 'string') {
return c.json({ message: '评论内容不能为空' }, 400);
}
if (!rawAuthor || typeof rawAuthor !== 'string') {
if (!rawName || typeof rawName !== 'string') {
return c.json({ message: '昵称不能为空' }, 400);
}
if (!email || typeof email !== 'string') {
@@ -39,7 +39,7 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
if (!isValidEmail(email)) {
return c.json({ message: '邮箱格式不正确' }, 400);
}
const userAgent = c.req.header('user-agent') || "";
const ua = c.req.header('user-agent') || "";
// 1. 获取 IP (Worker 获取 IP 的标准方式)
const ip = c.req.header('cf-connecting-ip') || "127.0.0.1";
@@ -47,11 +47,11 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
// 2. 检查评论频率控制 (对应 canPostComment)
// 这里建议使用 D1 查最近一条评论的时间,或者直接放行(如果使用了 Cloudflare WAF
const lastComment = await c.env.CWD_DB.prepare(
'SELECT pub_date FROM Comment WHERE ip_address = ? ORDER BY pub_date DESC LIMIT 1'
).bind(ip).first<{ pub_date: string }>();
'SELECT created FROM Comment WHERE ip_address = ? ORDER BY created DESC LIMIT 1'
).bind(ip).first<{ created: number }>();
if (lastComment) {
const lastTime = new Date(lastComment.pub_date).getTime();
const lastTime = lastComment.created;
if (Date.now() - lastTime < 10 * 1000) {
return c.json({ message: "评论频繁等10s后再试" }, 429);
}
@@ -71,7 +71,7 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
// 3. 准备数据
const cleanedContent = checkContent(rawContent);
const contentText = cleanedContent;
const author = checkContent(rawAuthor);
const name = checkContent(rawName);
// Markdown 渲染与 XSS 过滤
const html = await marked.parse(cleanedContent, { async: true });
@@ -89,32 +89,32 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
console.log('PostComment:request', {
postSlug: post_slug,
hasParent: parentId !== null && parentId !== undefined,
author,
name,
email,
ip
});
const uaParser = new UAParser(userAgent);
const uaParser = new UAParser(ua);
const uaResult = uaParser.getResult();
// 4. 写入 D1 数据库
try {
const { success } = await c.env.CWD_DB.prepare(`
INSERT INTO Comment (
pub_date, post_slug, author, email, url, ip_address,
os, browser, device, user_agent, content_text, content_html,
created, post_slug, name, email, url, ip_address,
os, browser, device, ua, content_text, content_html,
parent_id, status
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).bind(
new Date().toISOString(),
Date.now(),
post_slug,
author,
name,
email,
url || null,
ip,
`${uaResult.os.name || ""} ${uaResult.os.version || ""}`.trim(),
`${uaResult.browser.name || ""} ${uaResult.browser.version || ""}`.trim(),
uaResult.device.model || uaResult.device.type || "Desktop",
userAgent,
ua,
contentText,
contentHtml,
parentId || null,
@@ -156,8 +156,8 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
const isAdminReply = !!adminEmail && email === adminEmail;
const parentComment = await c.env.CWD_DB.prepare(
"SELECT author, email, content_html FROM Comment WHERE id = ?"
).bind(parentId).first<{ author: string, email: string, content_html: string }>();
"SELECT name, email, content_html FROM Comment WHERE id = ?"
).bind(parentId).first<{ name: string, email: string, content_html: string }>();
if (parentComment && parentComment.email && parentComment.email !== email) {
let canSendUserMail = true;
@@ -173,14 +173,14 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
if (canSendUserMail && isValidEmail(parentComment.email)) {
console.log('PostComment:mailDispatch:userReply:send', {
toEmail: parentComment.email,
toName: parentComment.author
toName: parentComment.name
});
await sendCommentReplyNotification(c.env, {
toEmail: parentComment.email,
toName: parentComment.author,
toName: parentComment.name,
postTitle: data.post_title,
parentComment: parentComment.content_html,
replyAuthor: author,
replyAuthor: name,
replyContent: contentHtml,
postUrl: data.post_url,
}, notifySettings.smtp);
@@ -202,7 +202,7 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
await sendCommentNotification(c.env, {
postTitle: data.post_title,
postUrl: data.post_url,
commentAuthor: author,
commentAuthor: name,
commentContent: contentHtml
}, notifySettings.smtp);
await c.env.CWD_DB.prepare(

View File

@@ -25,7 +25,7 @@ export class CommentForm extends Component {
const { formErrors, submitting } = this.props;
const { localForm } = this.state;
const canSubmit = localForm.author.trim() && localForm.email.trim() && localForm.content.trim();
const canSubmit = localForm.name.trim() && localForm.email.trim() && localForm.content.trim();
const root = this.createElement('form', {
className: 'cwd-comment-form',
@@ -46,7 +46,7 @@ export class CommentForm extends Component {
className: 'cwd-form-row',
children: [
// 昵称
this.createFormField('昵称 *', 'text', 'author', localForm.author, formErrors.author),
this.createFormField('昵称 *', 'text', 'name', localForm.name, formErrors.name),
// 邮箱
this.createFormField('邮箱 *', 'email', 'email', localForm.email, formErrors.email),
// 网址
@@ -103,13 +103,13 @@ export class CommentForm extends Component {
// 只在非提交状态时同步表单数据(避免覆盖用户正在输入的内容)
if (!this.props.submitting && this.props.form !== prevProps.form) {
// 保留当前正在输入的内容
const currentAuthor = this.state.localForm.author || '';
const currentName = this.state.localForm.name || '';
const currentEmail = this.state.localForm.email || '';
const currentUrl = this.state.localForm.url || '';
const currentContent = this.state.localForm.content || '';
this.state.localForm = {
author: this.props.form.author || currentAuthor,
name: this.props.form.name || currentName,
email: this.props.form.email || currentEmail,
url: this.props.form.url || currentUrl,
content: this.props.form.content !== undefined ? this.props.form.content : currentContent,
@@ -134,7 +134,7 @@ export class CommentForm extends Component {
const { formErrors, submitting } = this.props;
const { localForm } = this.state;
const canSubmit = localForm.author.trim() && localForm.email.trim() && localForm.content.trim();
const canSubmit = localForm.name.trim() && localForm.email.trim() && localForm.content.trim();
// 更新提交按钮状态
const submitBtn = this.elements.root.querySelector('button[type="submit"]');
@@ -159,8 +159,8 @@ export class CommentForm extends Component {
updateErrors(formErrors) {
if (!this.elements.root) return;
const authorInput = this.elements.root.querySelector('input[name="author"]');
this.updateFieldError(authorInput, formErrors?.author);
const nameInput = this.elements.root.querySelector('input[name="name"]');
this.updateFieldError(nameInput, formErrors?.name);
const emailInput = this.elements.root.querySelector('input[name="email"]');
this.updateFieldError(emailInput, formErrors?.email);
@@ -227,12 +227,12 @@ export class CommentForm extends Component {
* 设置输入框的值
*/
setInputValues(root, form) {
const authorInput = root.querySelector('input[name="author"]');
const nameInput = root.querySelector('input[name="name"]');
const emailInput = root.querySelector('input[name="email"]');
const urlInput = root.querySelector('input[name="url"]');
const contentTextarea = root.querySelector('textarea');
if (authorInput) authorInput.value = form.author || '';
if (nameInput) nameInput.value = form.name || '';
if (emailInput) emailInput.value = form.email || '';
if (urlInput) urlInput.value = form.url || '';
if (contentTextarea) contentTextarea.value = form.content || '';

View File

@@ -45,7 +45,7 @@ export class CommentItem extends Component {
this.createElement('img', {
attributes: {
src: comment.avatar,
alt: comment.author,
alt: comment.name,
loading: 'lazy'
}
})
@@ -74,11 +74,11 @@ export class CommentItem extends Component {
target: '_blank',
rel: 'noopener noreferrer'
},
text: comment.author
text: comment.name
})
]
})
: this.createTextElement('span', comment.author, 'cwd-author-name'),
: this.createTextElement('span', comment.name, 'cwd-author-name'),
// 博主标识
...(isAdmin ? [
this.createTextElement('span', `${adminBadge}`, 'cwd-admin-badge')
@@ -102,7 +102,7 @@ export class CommentItem extends Component {
},
text: '回复'
}),
this.createTextElement('span', formatRelativeTime(comment.pubDate), 'cwd-comment-time')
this.createTextElement('span', formatRelativeTime(comment.created), 'cwd-comment-time')
]
})
]
@@ -140,7 +140,7 @@ export class CommentItem extends Component {
const replyContainer = root.querySelector('.cwd-reply-editor-container');
if (replyContainer) {
this.replyEditor = new ReplyEditor(replyContainer, {
replyToAuthor: comment.author,
replyToAuthor: comment.name,
content: this.props.replyContent,
error: this.props.replyError,
submitting: this.props.submitting,
@@ -212,7 +212,7 @@ export class CommentItem extends Component {
if (isReplying && replyContainer) {
// 显示回复编辑器
this.replyEditor = new ReplyEditor(replyContainer, {
replyToAuthor: comment.author,
replyToAuthor: comment.name,
content: this.props.replyContent,
error: this.props.replyError,
submitting: this.props.submitting,

View File

@@ -39,7 +39,7 @@ export function createApiClient(config) {
/**
* 提交评论
* @param {Object} data - 评论数据
* @param {string} data.author - 昵称
* @param {string} data.name - 昵称
* @param {string} data.email - 邮箱
* @param {string} data.url - 网址(可选)
* @param {string} data.content - 评论内容
@@ -56,7 +56,7 @@ export function createApiClient(config) {
post_slug: config.postSlug,
post_title: config.postTitle,
post_url: config.postUrl,
author: data.author,
name: data.name,
email: data.email,
url: data.url || undefined,
content: data.content,

View File

@@ -15,7 +15,7 @@ function loadUserInfo() {
if (data) {
const parsed = JSON.parse(data);
return {
author: parsed.author || '',
name: parsed.name || '',
email: parsed.email || '',
url: parsed.url || ''
};
@@ -23,18 +23,18 @@ function loadUserInfo() {
} catch (e) {
console.error('读取用户信息失败:', e);
}
return { author: '', email: '', url: '' };
return { name: '', email: '', url: '' };
}
/**
* 保存用户信息到 localStorage
* @param {string} author - 昵称
* @param {string} name - 昵称
* @param {string} email - 邮箱
* @param {string} url - 网址
*/
function saveUserInfo(author, email, url) {
function saveUserInfo(name, email, url) {
try {
const data = { author, email, url };
const data = { name, email, url };
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
} catch (e) {
console.error('保存用户信息失败:', e);
@@ -115,7 +115,7 @@ export function createCommentStore(config, fetchComments, submitComment) {
// 表单数据
form: {
author: savedInfo.author || '',
name: savedInfo.name || '',
email: savedInfo.email || '',
url: savedInfo.url || '',
content: ''
@@ -131,8 +131,8 @@ export function createCommentStore(config, fetchComments, submitComment) {
// 监听用户信息变化,自动保存到 localStorage
store.subscribe((state) => {
if (state.form.author || state.form.email || state.form.url) {
saveUserInfo(state.form.author, state.form.email, state.form.url);
if (state.form.name || state.form.email || state.form.url) {
saveUserInfo(state.form.name, state.form.email, state.form.url);
}
});
@@ -194,7 +194,7 @@ export function createCommentStore(config, fetchComments, submitComment) {
try {
await submitComment({
author: form.author,
name: form.name,
email: form.email,
url: form.url,
content: form.content
@@ -249,7 +249,7 @@ export function createCommentStore(config, fetchComments, submitComment) {
try {
await submitComment({
author: state.form.author,
name: state.form.name,
email: state.form.email,
url: state.form.url,
content: state.replyContent,

View File

@@ -4,11 +4,11 @@
/**
* 格式化时间3天内显示相对时间超过3天显示完整日期
* @param {string} dateStr - 日期字符串
* @param {string|number} dateValue - 日期字符串或时间戳
* @returns {string}
*/
export function formatRelativeTime(dateStr) {
const date = new Date(dateStr);
export function formatRelativeTime(dateValue) {
const date = new Date(dateValue);
const now = new Date();
const diff = now.getTime() - date.getTime();
@@ -32,16 +32,16 @@ export function formatRelativeTime(dateStr) {
}
// 超过3天显示完整日期
return formatDateTime(dateStr);
return formatDateTime(dateValue);
}
/**
* 格式化日期时间
* @param {string} dateStr - 日期字符串
* @param {string|number} dateValue - 日期字符串或时间戳
* @returns {string}
*/
export function formatDateTime(dateStr) {
const date = new Date(dateStr);
export function formatDateTime(dateValue) {
const date = new Date(dateValue);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
@@ -53,11 +53,11 @@ export function formatDateTime(dateStr) {
/**
* 格式化日期
* @param {string} dateStr - 日期字符串
* @param {string|number} dateValue - 日期字符串或时间戳
* @returns {string}
*/
export function formatDate(dateStr) {
const date = new Date(dateStr);
export function formatDate(dateValue) {
const date = new Date(dateValue);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
@@ -66,11 +66,11 @@ export function formatDate(dateStr) {
/**
* 格式化时间
* @param {string} dateStr - 日期字符串
* @param {string|number} dateValue - 日期字符串或时间戳
* @returns {string}
*/
export function formatTime(dateStr) {
const date = new Date(dateStr);
export function formatTime(dateValue) {
const date = new Date(dateValue);
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');

View File

@@ -45,7 +45,7 @@ export function validateCommentContent(content) {
/**
* 验证评论表单
* @param {Object} data - 表单数据
* @param {string} data.author - 昵称
* @param {string} data.name - 昵称
* @param {string} data.email - 邮箱
* @param {string} data.url - 网址
* @param {string} data.content - 评论内容
@@ -55,10 +55,10 @@ export function validateCommentForm(data) {
const errors = {};
// 验证昵称
if (!data.author || data.author.trim().length === 0) {
errors.author = '请输入昵称';
} else if (data.author.length > 50) {
errors.author = '昵称不能超过 50 字';
if (!data.name || data.name.trim().length === 0) {
errors.name = '请输入昵称';
} else if (data.name.length > 50) {
errors.name = '昵称不能超过 50 字';
}
// 验证邮箱
@@ -88,7 +88,7 @@ export function validateCommentForm(data) {
/**
* 验证回复所需的用户信息
* @param {Object} data - 用户信息
* @param {string} data.author - 昵称
* @param {string} data.name - 昵称
* @param {string} data.email - 邮箱
* @param {string} data.url - 网址
* @returns {{valid: boolean, errors: Object<string, string>}}
@@ -97,10 +97,10 @@ export function validateReplyUserInfo(data) {
const errors = {};
// 验证昵称
if (!data.author || data.author.trim().length === 0) {
errors.author = '请输入昵称';
} else if (data.author.length > 50) {
errors.author = '昵称不能超过 50 字';
if (!data.name || data.name.trim().length === 0) {
errors.name = '请输入昵称';
} else if (data.name.length > 50) {
errors.name = '昵称不能超过 50 字';
}
// 验证邮箱