Files
rainblogweb/routes/blog.js
T

423 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const express = require('express');
const jwt = require('jsonwebtoken');
const rateLimit = require('express-rate-limit');
const db = require('../db');
const { authMiddleware, adminOnly, SECRET } = require('../middleware/auth');
const locks = require('../lib/locks');
const { attachAuthor } = require('../lib/author');
const router = express.Router();
// 评论限流:10 分钟窗口内最多 15 次
const commentLimiter = rateLimit({
windowMs: 10 * 60 * 1000,
max: 15,
standardHeaders: true,
legacyHeaders: false,
message: { error: '评论过于频繁,请稍后再试' },
});
// 可选鉴权:有效 token 时解析出 req.user,匿名请求直接放行(用于草稿的"作者可见"判断)
function optionalAuth(req, res, next) {
const header = req.headers.authorization;
if (header && header.startsWith('Bearer ')) {
try {
req.user = jwt.verify(header.slice(7), SECRET);
} catch { /* 无效 token 按匿名处理 */ }
}
next();
}
// LIKE 通配符转义(% _ \),配合 ESCAPE '\' 使用
function escapeLike(s) {
return String(s).replace(/[\\%_]/g, (m) => '\\' + m);
}
// 标签规范化:逗号分隔、去空白、去空项、逗号无空格连接(保证 LIKE 边界匹配可靠)
function normalizeTags(tags) {
return String(tags || '').split(',').map(t => t.trim()).filter(Boolean).join(',');
}
// 评论邮件通知:comment_notify='1' 且评论者不是文章作者时通知作者(失败不影响评论创建)。
// 作者未设置邮箱时回退通知任意 admin(标题/正文标明「管理员代收」);
// SMTP 未配置(getTransporter 返回 null)时打 console.warn 而非完全静默。
async function notifyCommentToAuthor(post, userId, comment) {
try {
if (db.getSetting('comment_notify') !== '1') return;
if (post.author_id === userId) return; // 作者本人评论不通知
const { getTransporter, emailTemplate } = require('./email');
const transporter = getTransporter();
if (!transporter) {
console.warn('评论通知未发送:SMTP 未配置(getTransporter 返回 null),请先配置 SMTP');
return;
}
const author = db.get('SELECT id, email FROM users WHERE id = ?', [post.author_id]);
let recipient = (author && author.email) || '';
let adminProxy = false;
if (!recipient) {
// 回退:作者未设邮箱 → 通知任意 admin(管理员代收)
const admin = db.get("SELECT email FROM users WHERE role = 'admin' AND email <> '' AND email IS NOT NULL LIMIT 1");
if (!admin || !admin.email) return; // 无收件人可发
recipient = admin.email;
adminProxy = true;
}
const siteName = db.getSetting('site_name') || 'RainWeb';
const siteUrl = db.getSetting('site_url') || '';
const base = siteUrl.replace(/\/$/, '');
const link = base + '/blog/' + post.id;
const user = db.get('SELECT username FROM users WHERE id = ?', [userId]);
const commenterName = (user && user.username) || '匿名';
await transporter.sendMail({
from: `"${db.getSetting('smtp_from_name')}" <${db.getSetting('smtp_from_email')}>`,
to: recipient,
subject: `您有新评论 - ${post.title}` + (adminProxy ? '(管理员代收)' : ''),
html: emailTemplate('新评论通知',
(adminProxy ? '<p style="color:#b3261e"><strong>(管理员代收:文章作者未设置邮箱)</strong></p>' : '') +
`<p>您的文章《<strong>${post.title}</strong>》收到一条新评论:</p>
<p style="padding:12px;background:#f5f5f5;border-radius:8px;margin:12px 0">${String(comment.content).replace(/</g, '&lt;')}</p>
<p style="color:#79747e">评论者:${commenterName}</p>
<a class="btn" href="${link}">查看评论</a>`),
});
} catch (e) {
console.error('评论通知邮件发送失败:', e.message);
}
}
// [lock:] 真锁:按 viewer 视角剥离单篇文章,返回带 locks/unlocked/viewer 字段的 post。
// 列表与详情共用——未解锁块内容(含 [image:]/[file:] 附件标签)一律不进 API 响应。
function applyLocks(post, req) {
if (!post) return post;
const viewer = {
userId: req.user ? req.user.id : null,
isAdmin: !!(req.user && req.user.role === 'admin'),
isAuthor: !!(req.user && req.user.id === post.author_id),
hasCommented: false,
};
if (req.user) {
viewer.hasCommented = !!db.get(
'SELECT 1 x FROM blog_comments WHERE post_id = ? AND author_id = ?', [post.id, req.user.id]);
}
const locksMeta = (() => { try { return JSON.parse(post.locks || '[]'); } catch { return []; } })();
const blocks = locks.parseLocks(post.content).blocks;
const { stripped, unlocked } = locks.stripLocks(post.content, { locksMeta, viewer });
post.content = stripped;
const lockMetaList = blocks.map(b => ({ index: b.index, type: b.type })); // 不含 hash
// 附件真锁:对已解锁索引签发附件 token,前端用它加载块内 [image:]/[file:] 附件
//password 块被 admin/作者解锁时同样签发——他们本来就可见该块)
lockMetaList.forEach(m => {
if (unlocked.includes(m.index)) m.token = locks.makeLockToken('blog', post.id, m.index);
});
post.locks = lockMetaList;
post.unlocked = unlocked;
post.viewer = { loggedIn: !!req.user, isAdmin: viewer.isAdmin, isAuthor: viewer.isAuthor, hasCommented: viewer.hasCommented };
return post;
}
// 公开列表:仅返回已发布文章
router.get('/posts', optionalAuth, (req, res, next) => {
if (req.query.all !== '1') {
const rows = db.all("SELECT bp.*, u.username as author_name, u.avatar, u.qq, u.nickname AS author_nickname, u.title AS author_title, u.title_color AS author_title_color, u.role AS author_role, CASE WHEN u.qq IS NOT NULL AND trim(u.qq) <> '' THEN '' WHEN u.email GLOB '[0-9]*@qq.com' THEN substr(u.email, 1, instr(u.email, '@') - 1) ELSE '' END AS qq_from_email FROM blog_posts bp LEFT JOIN users u ON bp.author_id = u.id WHERE bp.published = 1 ORDER BY bp.created_at DESC");
rows.forEach(p => { applyLocks(p, req); attachAuthor(p); });
return res.json(rows);
}
// ?all=1:仅管理员可见,交给下方鉴权路由处理
next();
});
// ?all=1:返回全部文章(含草稿),仅管理员可见
router.get('/posts', authMiddleware, adminOnly, (req, res) => {
const rows = db.all("SELECT bp.*, u.username as author_name, u.avatar, u.qq, u.nickname AS author_nickname, u.title AS author_title, u.title_color AS author_title_color, u.role AS author_role, CASE WHEN u.qq IS NOT NULL AND trim(u.qq) <> '' THEN '' WHEN u.email GLOB '[0-9]*@qq.com' THEN substr(u.email, 1, instr(u.email, '@') - 1) ELSE '' END AS qq_from_email FROM blog_posts bp LEFT JOIN users u ON bp.author_id = u.id ORDER BY bp.created_at DESC");
rows.forEach(p => { applyLocks(p, req); attachAuthor(p); });
res.json(rows);
});
// 搜索:标题/正文/摘要模糊匹配(参数化 + 通配符转义),仅已发布,最多 20 条
router.get('/search', (req, res) => {
const q = (req.query.q || '').trim();
if (!q) return res.json([]);
const pattern = '%' + escapeLike(q) + '%';
const posts = db.all(
`SELECT bp.id, bp.title, bp.excerpt, bp.tags, bp.created_at, u.username as author_name, u.avatar, u.qq, u.nickname AS author_nickname, u.title AS author_title, u.title_color AS author_title_color, u.role AS author_role,
CASE WHEN u.qq IS NOT NULL AND trim(u.qq) <> '' THEN ''
WHEN u.email GLOB '[0-9]*@qq.com' THEN substr(u.email, 1, instr(u.email, '@') - 1)
ELSE '' END AS qq_from_email
FROM blog_posts bp LEFT JOIN users u ON bp.author_id = u.id
WHERE bp.published = 1 AND (bp.title LIKE ? ESCAPE '\\' OR bp.content LIKE ? ESCAPE '\\' OR bp.excerpt LIKE ? ESCAPE '\\')
ORDER BY bp.created_at DESC LIMIT 20`,
[pattern, pattern, pattern]);
posts.forEach(p => { attachAuthor(p); });
res.json(posts);
});
// 标签聚合:返回 [{name, count}],按 count 降序,空标签跳过
router.get('/tags', (req, res) => {
const posts = db.all("SELECT tags FROM blog_posts WHERE published = 1 AND tags != ''");
const map = new Map();
posts.forEach(p => {
String(p.tags || '').split(',').forEach(t => {
const name = t.trim();
if (name) map.set(name, (map.get(name) || 0) + 1);
});
});
const result = Array.from(map.entries()).map(([name, count]) => ({ name, count }))
.sort((a, b) => b.count - a.count);
res.json(result);
});
// 按标签精确匹配(边界匹配,避免子串误中,如 'java' 不匹配 'javascript'
router.get('/tag/:name', (req, res) => {
const name = String(req.params.name || '').trim();
if (!name) return res.json([]);
const posts = db.all(
`SELECT bp.id, bp.title, bp.excerpt, bp.tags, bp.created_at, u.username as author_name, u.avatar, u.qq, u.nickname AS author_nickname, u.title AS author_title, u.title_color AS author_title_color, u.role AS author_role,
CASE WHEN u.qq IS NOT NULL AND trim(u.qq) <> '' THEN ''
WHEN u.email GLOB '[0-9]*@qq.com' THEN substr(u.email, 1, instr(u.email, '@') - 1)
ELSE '' END AS qq_from_email
FROM blog_posts bp LEFT JOIN users u ON bp.author_id = u.id
WHERE bp.published = 1 AND (bp.tags = ? OR bp.tags LIKE ? OR bp.tags LIKE ? OR bp.tags LIKE ?)
ORDER BY bp.created_at DESC`,
[name, name + ',%', '%,' + name + ',%', '%,' + name]);
posts.forEach(p => { attachAuthor(p); });
res.json(posts);
});
// 归档:按月统计已发布文章数
router.get('/archive', (req, res) => {
res.json(db.all("SELECT strftime('%Y-%m', created_at) as month, COUNT(*) as count FROM blog_posts WHERE published = 1 GROUP BY month ORDER BY month DESC"));
});
router.get('/posts/:id', optionalAuth, (req, res) => {
const post = db.get(
"SELECT bp.*, u.username as author_name, u.avatar, u.qq, u.nickname AS author_nickname, u.title AS author_title, u.title_color AS author_title_color, u.role AS author_role, CASE WHEN u.qq IS NOT NULL AND trim(u.qq) <> '' THEN '' WHEN u.email GLOB '[0-9]*@qq.com' THEN substr(u.email, 1, instr(u.email, '@') - 1) ELSE '' END AS qq_from_email FROM blog_posts bp LEFT JOIN users u ON bp.author_id = u.id WHERE bp.id = ?",
[req.params.id]);
if (!post) return res.status(404).json({ error: '文章不存在' });
// 未发布文章(草稿):仅管理员或作者本人可见,其余按 404 处理避免泄露存在性
if (post.published !== 1) {
if (!req.user || !(req.user.role === 'admin' || req.user.id === post.author_id))
return res.status(404).json({ error: '文章不存在' });
} else {
// 已发布文章:阅读量 +1(草稿预览不计数)
db.run('UPDATE blog_posts SET views = views + 1 WHERE id = ?', [post.id]);
post.views = (post.views || 0) + 1;
}
// [lock:] 真锁:按 viewer 视角剥离,未解锁块内容不进响应
applyLocks(post, req);
// 作者头像(自传 > QQ > RainID
attachAuthor(post);
res.json(post);
});
// 上一篇 / 下一篇(按 created_at/id 相邻,仅已发布)
router.get('/posts/:id/prevnext', (req, res) => {
const post = db.get('SELECT id, created_at FROM blog_posts WHERE id = ?', [req.params.id]);
if (!post) return res.status(404).json({ error: '文章不存在' });
const prev = db.get(
`SELECT id, title FROM blog_posts
WHERE published = 1 AND (created_at < ? OR (created_at = ? AND id < ?))
ORDER BY created_at DESC, id DESC LIMIT 1`,
[post.created_at, post.created_at, post.id]);
const next = db.get(
`SELECT id, title FROM blog_posts
WHERE published = 1 AND (created_at > ? OR (created_at = ? AND id > ?))
ORDER BY created_at ASC, id ASC LIMIT 1`,
[post.created_at, post.created_at, post.id]);
res.json({ prev: prev || null, next: next || null });
});
// 点赞状态(匿名 liked=false
router.get('/posts/:id/like', optionalAuth, (req, res) => {
const post = db.get('SELECT id FROM blog_posts WHERE id = ?', [req.params.id]);
if (!post) return res.status(404).json({ error: '文章不存在' });
const count = db.get('SELECT COUNT(*) as c FROM post_likes WHERE post_id = ?', [post.id]).c;
let liked = false;
if (req.user) {
liked = !!db.get('SELECT 1 as x FROM post_likes WHERE post_id = ? AND user_id = ?', [post.id, req.user.id]);
}
res.json({ liked, count });
});
// 点赞
router.post('/posts/:id/like', authMiddleware, (req, res) => {
const post = db.get('SELECT id FROM blog_posts WHERE id = ?', [req.params.id]);
if (!post) return res.status(404).json({ error: '文章不存在' });
db.run('INSERT OR IGNORE INTO post_likes (post_id, user_id) VALUES (?, ?)', [post.id, req.user.id]);
const count = db.get('SELECT COUNT(*) as c FROM post_likes WHERE post_id = ?', [post.id]).c;
res.json({ liked: true, count });
});
// 取消点赞
router.delete('/posts/:id/like', authMiddleware, (req, res) => {
const post = db.get('SELECT id FROM blog_posts WHERE id = ?', [req.params.id]);
if (!post) return res.status(404).json({ error: '文章不存在' });
db.run('DELETE FROM post_likes WHERE post_id = ? AND user_id = ?', [post.id, req.user.id]);
const count = db.get('SELECT COUNT(*) as c FROM post_likes WHERE post_id = ?', [post.id]).c;
res.json({ liked: false, count });
});
// [lock:] 解锁:返回块内原文(含 [image:]/[file:] 标签,前端自行渲染)。
// login 块需登录;reply 块需已评论(含待审核);password 块 bcrypt 校验,
// 失败统一 401(不区分密码错/块不存在);admin/作者恒可解锁。
router.post('/posts/:id/locks/:index/unlock', authMiddleware, locks.unlockLimiter, (req, res) => {
const post = db.get('SELECT id, author_id, published, content, locks FROM blog_posts WHERE id = ?', [req.params.id]);
if (!post) return res.status(404).json({ error: '文章不存在' });
if (post.published !== 1 && !(req.user.role === 'admin' || req.user.id === post.author_id))
return res.status(404).json({ error: '文章不存在' });
const index = parseInt(req.params.index);
if (!Number.isInteger(index) || index < 0) return res.status(401).json({ error: '解锁失败' });
const locksMeta = (() => { try { return JSON.parse(post.locks || '[]'); } catch { return []; } })();
const hasCommented = !!db.get(
'SELECT 1 x FROM blog_comments WHERE post_id = ? AND author_id = ?', [post.id, req.user.id]);
const result = locks.verifyUnlock({
blocks: locks.parseLocks(post.content).blocks,
locksMeta,
index,
password: req.body.password,
viewer: { userId: req.user.id, isAdmin: req.user.role === 'admin', isAuthor: req.user.id === post.author_id },
replyQualified: hasCommented,
});
if (!result.ok) {
return res.status(result.status).json({ error: result.status === 403 ? '评论后解锁' : '解锁失败' });
}
// 附件真锁:随解锁内容一并签发附件 token(前端据此加载块内附件)
res.json({ ok: true, content: result.inner, lockToken: locks.makeLockToken('blog', post.id, index) });
});
router.post('/posts', authMiddleware, adminOnly, (req, res) => {
const { title, content, excerpt, published, use_markdown, tags } = req.body;
if (!title || !content) return res.status(400).json({ error: '标题和内容不能为空' });
const id = db.run(
'INSERT INTO blog_posts (title, content, excerpt, author_id, published, use_markdown, tags, locks) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[title, content, excerpt || '', req.user.id, published !== undefined ? (published ? 1 : 0) : 1, use_markdown !== undefined ? (use_markdown ? 1 : 0) : 1, normalizeTags(tags), locks.saveLocks(content)]);
res.json(db.get('SELECT * FROM blog_posts WHERE id = ?', [id]));
});
router.put('/posts/:id', authMiddleware, adminOnly, (req, res) => {
const { title, content, excerpt, published, use_markdown, tags } = req.body;
if (!db.get('SELECT id FROM blog_posts WHERE id = ?', [req.params.id]))
return res.status(404).json({ error: '文章不存在' });
db.run(
"UPDATE blog_posts SET title=?, content=?, excerpt=?, published=?, use_markdown=?, tags=?, locks=?, updated_at=datetime('now') WHERE id=?",
[title || '', content || '', excerpt || '', published !== undefined ? (published ? 1 : 0) : 1, use_markdown !== undefined ? (use_markdown ? 1 : 0) : 1, normalizeTags(tags), locks.saveLocks(content || ''), req.params.id]);
res.json(db.get('SELECT * FROM blog_posts WHERE id = ?', [req.params.id]));
});
router.delete('/posts/:id', authMiddleware, adminOnly, (req, res) => {
if (!db.get('SELECT id FROM blog_posts WHERE id = ?', [req.params.id]))
return res.status(404).json({ error: '文章不存在' });
db.run('DELETE FROM blog_comments WHERE post_id = ?', [req.params.id]);
db.run('DELETE FROM post_likes WHERE post_id = ?', [req.params.id]);
db.run('DELETE FROM blog_posts WHERE id = ?', [req.params.id]);
res.json({ message: '删除成功' });
});
// Comments
// 待审核评论列表(管理接口,须在 /comments/:postId 之前注册,避免 "pending" 被当作 postId
router.get('/comments/pending', authMiddleware, adminOnly, (req, res) => {
const comments = db.all(
`SELECT bc.*, u.username as author_name, u.avatar, u.qq, u.nickname AS author_nickname, u.title AS author_title, u.title_color AS author_title_color, u.role AS author_role,
CASE WHEN u.qq IS NOT NULL AND trim(u.qq) <> '' THEN ''
WHEN u.email GLOB '[0-9]*@qq.com' THEN substr(u.email, 1, instr(u.email, '@') - 1)
ELSE '' END AS qq_from_email,
bp.title as post_title
FROM blog_comments bc
LEFT JOIN users u ON bc.author_id = u.id
LEFT JOIN blog_posts bp ON bc.post_id = bp.id
WHERE bc.status = 'pending' ORDER BY bc.created_at ASC`);
comments.forEach(c => { attachAuthor(c); });
res.json(comments);
});
// 全量评论列表(管理接口,adminOnly):?status=all|pending|approved|rejected&page=&pageSize=
router.get('/comments', authMiddleware, adminOnly, (req, res) => {
const status = String(req.query.status || 'all');
if (!['all', 'pending', 'approved', 'rejected'].includes(status))
return res.status(400).json({ error: '无效的 status' });
let page = parseInt(req.query.page);
let pageSize = parseInt(req.query.pageSize);
if (!Number.isInteger(page) || page < 1) page = 1;
if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 50) pageSize = 20;
const where = status === 'all' ? '' : 'WHERE bc.status = ?';
const params = status === 'all' ? [] : [status];
const total = (db.get(`SELECT COUNT(*) c FROM blog_comments bc ${where}`, params) || {}).c || 0;
const list = db.all(
`SELECT bc.id, bc.post_id, bp.title as post_title, bc.content, bc.author_id,
u.username as author_name, u.avatar, u.qq,
u.nickname AS author_nickname, u.title AS author_title, u.title_color AS author_title_color, u.role AS author_role,
CASE WHEN u.qq IS NOT NULL AND trim(u.qq) <> '' THEN ''
WHEN u.email GLOB '[0-9]*@qq.com' THEN substr(u.email, 1, instr(u.email, '@') - 1)
ELSE '' END AS qq_from_email,
bc.status, bc.created_at
FROM blog_comments bc
LEFT JOIN users u ON bc.author_id = u.id
LEFT JOIN blog_posts bp ON bc.post_id = bp.id
${where} ORDER BY bc.created_at DESC, bc.id DESC LIMIT ? OFFSET ?`,
[...params, pageSize, (page - 1) * pageSize]);
list.forEach(c => { attachAuthor(c); });
res.json({ list, total, page, pageSize, totalPages: Math.ceil(total / pageSize) });
});
// 评论列表:仅返回已通过审核(approved)的评论(带作者头像)
router.get('/comments/:postId', (req, res) => {
if (!db.get('SELECT id FROM blog_posts WHERE id = ?', [req.params.postId]))
return res.status(404).json({ error: '文章不存在' });
const comments = db.all(
"SELECT bc.*, u.username as author_name, u.avatar, u.qq, u.nickname AS author_nickname, u.title AS author_title, u.title_color AS author_title_color, u.role AS author_role, CASE WHEN u.qq IS NOT NULL AND trim(u.qq) <> '' THEN '' WHEN u.email GLOB '[0-9]*@qq.com' THEN substr(u.email, 1, instr(u.email, '@') - 1) ELSE '' END AS qq_from_email FROM blog_comments bc LEFT JOIN users u ON bc.author_id = u.id WHERE bc.post_id = ? AND bc.status = 'approved' ORDER BY bc.created_at ASC",
[req.params.postId]);
comments.forEach(c => { attachAuthor(c); });
res.json(comments);
});
// 创建评论:支持嵌套回复 parent_id;审核模式(comment_moderate='1')下新评论进待审
router.post('/comments/:postId', authMiddleware, commentLimiter, async (req, res) => {
const { content } = req.body;
const parent_id = parseInt(req.body.parent_id) || 0;
if (!content) return res.status(400).json({ error: '评论内容不能为空' });
const post = db.get('SELECT * FROM blog_posts WHERE id = ?', [req.params.postId]);
if (!post) return res.status(404).json({ error: '文章不存在' });
if (parent_id) {
const parent = db.get('SELECT * FROM blog_comments WHERE id = ?', [parent_id]);
if (!parent) return res.status(400).json({ error: '回复的评论不存在' });
if (parent.post_id !== post.id) return res.status(400).json({ error: '回复的评论不属于该文章' });
}
const status = db.getSetting('comment_moderate') === '1' ? 'pending' : 'approved';
const id = db.run('INSERT INTO blog_comments (post_id, content, author_id, parent_id, status) VALUES (?, ?, ?, ?, ?)',
[post.id, content, req.user.id, parent_id, status]);
const comment = db.get(
`SELECT bc.*, u.username as author_name, u.avatar, u.qq, u.nickname AS author_nickname, u.title AS author_title, u.title_color AS author_title_color, u.role AS author_role,
CASE WHEN u.qq IS NOT NULL AND trim(u.qq) <> '' THEN ''
WHEN u.email GLOB '[0-9]*@qq.com' THEN substr(u.email, 1, instr(u.email, '@') - 1)
ELSE '' END AS qq_from_email
FROM blog_comments bc LEFT JOIN users u ON bc.author_id = u.id WHERE bc.id = ?`, [id]);
attachAuthor(comment);
// 邮件通知(失败不影响评论创建)
notifyCommentToAuthor(post, req.user.id, comment);
res.json(comment);
});
// 审核:通过
router.post('/comments/:id/approve', authMiddleware, adminOnly, (req, res) => {
const comment = db.get('SELECT * FROM blog_comments WHERE id = ?', [req.params.id]);
if (!comment) return res.status(404).json({ error: '评论不存在' });
db.run("UPDATE blog_comments SET status = 'approved' WHERE id = ?", [comment.id]);
res.json({ message: '已通过', comment: db.get('SELECT * FROM blog_comments WHERE id = ?', [comment.id]) });
});
// 审核:拒绝
router.post('/comments/:id/reject', authMiddleware, adminOnly, (req, res) => {
const comment = db.get('SELECT * FROM blog_comments WHERE id = ?', [req.params.id]);
if (!comment) return res.status(404).json({ error: '评论不存在' });
db.run("UPDATE blog_comments SET status = 'rejected' WHERE id = ?", [comment.id]);
res.json({ message: '已拒绝', comment: db.get('SELECT * FROM blog_comments WHERE id = ?', [comment.id]) });
});
router.delete('/comments/:id', authMiddleware, (req, res) => {
const comment = db.get('SELECT * FROM blog_comments WHERE id = ?', [req.params.id]);
if (!comment) return res.status(404).json({ error: '评论不存在' });
if (comment.author_id !== req.user.id && req.user.role !== 'admin')
return res.status(403).json({ error: '无权限' });
db.run('DELETE FROM blog_comments WHERE id = ?', [req.params.id]);
res.json({ message: '删除成功' });
});
module.exports = router;