From 7b27b0128ec65594cde0ba2665e477424abfe491 Mon Sep 17 00:00:00 2001 From: zpj80231 Date: Wed, 25 Mar 2026 14:00:17 +0800 Subject: [PATCH 1/5] =?UTF-8?q?style(widget):=20=E5=AF=B9=E4=BA=8E?= =?UTF-8?q?=E5=B7=B2=E7=BB=8F=E7=82=B9=E8=B5=9E=E7=9A=84=E6=8C=89=E9=92=AE?= =?UTF-8?q?=EF=BC=8C=E9=AB=98=E4=BA=AE=E8=AF=84=E8=AE=BA=E7=82=B9=E8=B5=9E?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 .cwd-comment-like-button-liked 样式类 - 设置已点赞状态的颜色为 cwd-primary 变量值 - 保持与现有主题颜色的一致性 (cherry picked from commit 334ad20720eeed69c05e880c28d7d8171c7f67f1) --- docs/widget/src/styles/main.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/widget/src/styles/main.css b/docs/widget/src/styles/main.css index be296d1..1153917 100644 --- a/docs/widget/src/styles/main.css +++ b/docs/widget/src/styles/main.css @@ -660,6 +660,10 @@ color: var(--cwd-primary, #0969da); } +.cwd-comment-like-button-liked { + color: var(--cwd-primary, #0969da); +} + .cwd-comment-like-icon-wrapper { display: inline-flex; align-items: center; From fc48deb33889ce6babdba8d55b16f5a6948101cb Mon Sep 17 00:00:00 2001 From: zpj80231 Date: Wed, 25 Mar 2026 14:57:05 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat(cwd-api,=20widget):=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E8=AF=84=E8=AE=BA=E5=8F=96=E6=B6=88=E7=82=B9=E8=B5=9E?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加了评论取消点赞的逻辑处理 - 新增 DELETE /api/comments/like 接口用于取消点赞 - 修改点赞接口支持根据请求方法判断增加或减少点赞数 - 优化点赞数计算逻辑,支持正确的增减操作 - 更新错误提示信息为更通用的操作失败提示 --- cwd-api/src/api/public/likeComment.ts | 27 +++++++++-------------- cwd-api/src/index.ts | 1 + docs/widget/src/components/CommentItem.js | 6 ++++- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/cwd-api/src/api/public/likeComment.ts b/cwd-api/src/api/public/likeComment.ts index d4dd849..df47dc2 100644 --- a/cwd-api/src/api/public/likeComment.ts +++ b/cwd-api/src/api/public/likeComment.ts @@ -27,6 +27,7 @@ export const likeComment = async (c: Context<{ Bindings: Bindings }>) => { } const id = parsed; + const method = c.req.method; try { const existing = await c.env.CWD_DB.prepare( @@ -39,26 +40,20 @@ export const likeComment = async (c: Context<{ Bindings: Bindings }>) => { return c.json({ message: 'Comment not found' }, 404); } + const delta = method === 'DELETE' ? -1 : 1; + const currentLikes = typeof existing.likes === 'number' && Number.isFinite(existing.likes) && existing.likes >= 0 + ? existing.likes + : 0; + const newLikes = Math.max(0, currentLikes + delta); + await c.env.CWD_DB.prepare( - 'UPDATE Comment SET likes = COALESCE(likes, 0) + 1 WHERE id = ?' + 'UPDATE Comment SET likes = ? WHERE id = ?' ) - .bind(id) + .bind(newLikes, id) .run(); - const updated = await c.env.CWD_DB.prepare( - 'SELECT COALESCE(likes, 0) as likes FROM Comment WHERE id = ?' - ) - .bind(id) - .first<{ likes?: number }>(); - - const likes = - updated && typeof updated.likes === 'number' && Number.isFinite(updated.likes) && updated.likes >= 0 - ? updated.likes - : ((existing.likes || 0) + 1); - - return c.json({ id, likes }); + return c.json({ id, likes: newLikes }); } catch (e: any) { - return c.json({ message: e?.message || '点赞失败' }, 500); + return c.json({ message: e?.message || '操作失败' }, 500); } }; - diff --git a/cwd-api/src/index.ts b/cwd-api/src/index.ts index cc87437..8f765a4 100644 --- a/cwd-api/src/index.ts +++ b/cwd-api/src/index.ts @@ -273,6 +273,7 @@ app.get('/api/analytics/pv', getPagePv); app.get('/api/like', getLikeStatus); app.post('/api/like', likePage); app.post('/api/comments/like', likeComment); +app.delete('/api/comments/like', likeComment); app.post('/api/telegram/webhook', telegramWebhook); app.get('/api/config/comments', async (c) => { try { diff --git a/docs/widget/src/components/CommentItem.js b/docs/widget/src/components/CommentItem.js index 9fe0b57..998bbb3 100644 --- a/docs/widget/src/components/CommentItem.js +++ b/docs/widget/src/components/CommentItem.js @@ -371,8 +371,12 @@ export class CommentItem extends Component { likedComments.add(commentId); this.saveLikedComments(likedComments); this.props.onLikeComment(commentId, true); + } else { + // 已点赞,执行取消点赞 + likedComments.delete(commentId); + this.saveLikedComments(likedComments); + this.props.onLikeComment(commentId, false); } - // 已点赞则不做任何操作 } /** From 75b604e510705a2a8e5576f0b8cbe56eaf2bb6fe Mon Sep 17 00:00:00 2001 From: zpj80231 Date: Thu, 19 Mar 2026 14:59:32 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat(api):=20=E4=BC=98=E5=8C=96=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E8=AF=84=E8=AE=BA=E5=8F=AF=E8=83=BD=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E7=9A=84DB500=E9=97=AE=E9=A2=98=EF=BC=9B=E5=85=BC=E5=AE=B9url?= =?UTF-8?q?=E4=B8=AD=E5=8C=85=E5=90=AB=E4=B8=AD=E6=96=87=E3=80=81=E7=A9=BA?= =?UTF-8?q?=E6=A0=BC=E3=80=81=E5=A4=9A=E7=BC=96=E7=A0=81=E7=AD=89=E7=9A=84?= =?UTF-8?q?=E8=AF=84=E8=AE=BA=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 getComments、getPagePv、like、postComment 和 trackVisit 等 API 中统一使用 decodePostSlug 函数处理文章别名 - 实现更精确的文章别名匹配,支持多种别名格式的查询 - 修复了原始别名处理中的潜在安全问题 - 移除了过时的 LIKE 查询模式,提高查询效率 - 统一了所有涉及文章别名的参数验证逻辑 (cherry picked from commit b9b944d7aff3f2e6008e52ba8f515bc451cfc32d) --- .gitignore | 6 ++++- cwd-api/src/api/public/getComments.ts | 27 ++++++++++--------- cwd-api/src/api/public/getPagePv.ts | 3 ++- cwd-api/src/api/public/like.ts | 16 +++++++----- cwd-api/src/api/public/postComment.ts | 5 ++-- cwd-api/src/api/public/trackVisit.ts | 9 ++++--- cwd-api/src/utils/decodePostSlug.ts | 37 +++++++++++++++++++++++++++ 7 files changed, 74 insertions(+), 29 deletions(-) create mode 100644 cwd-api/src/utils/decodePostSlug.ts diff --git a/.gitignore b/.gitignore index cdb99a5..10e60b6 100644 --- a/.gitignore +++ b/.gitignore @@ -174,4 +174,8 @@ wrangler.jsonc .claude .trae -.npmrc \ No newline at end of file +.npmrc + +# idea + +.idea \ No newline at end of file diff --git a/cwd-api/src/api/public/getComments.ts b/cwd-api/src/api/public/getComments.ts index 452187f..836de90 100644 --- a/cwd-api/src/api/public/getComments.ts +++ b/cwd-api/src/api/public/getComments.ts @@ -1,10 +1,11 @@ import { Context } from 'hono' import { Bindings } from '../../bindings' import { getCravatar } from '../../utils/getAvatar' +import { decodePostSlug, getAllSlugFormats, escapeLikePattern } from '../../utils/decodePostSlug' export const getComments = async (c: Context<{ Bindings: Bindings }>) => { const rawPostSlug = c.req.query('post_slug') || '' - const postSlug = rawPostSlug.trim() + const postSlug = decodePostSlug(rawPostSlug) const page = parseInt(c.req.query('page') || '1') const limit = Math.min(parseInt(c.req.query('limit') || '20'), 50) const nested = c.req.query('nested') !== 'false' @@ -42,13 +43,14 @@ export const getComments = async (c: Context<{ Bindings: Bindings }>) => { } try { - const equalSlugs = Array.from(new Set(slugList)) - const likePatternsSet = new Set() - for (const s of equalSlugs) { - likePatternsSet.add(`${s}#%`) - likePatternsSet.add(`${s}?%`) + const allSlugFormats = new Set() + for (const s of slugList) { + for (const format of getAllSlugFormats(s)) { + allSlugFormats.add(format) + } } - const likePatterns = Array.from(likePatternsSet) + const equalSlugs = Array.from(allSlugFormats) + const whereParts: string[] = [] if (equalSlugs.length === 1) { whereParts.push('post_slug = ?') @@ -56,16 +58,13 @@ export const getComments = async (c: Context<{ Bindings: Bindings }>) => { const placeholders = equalSlugs.map(() => '?').join(', ') whereParts.push(`post_slug IN (${placeholders})`) } - for (let i = 0; i < likePatterns.length; i += 1) { - whereParts.push('post_slug LIKE ?') - } const whereClause = whereParts.length > 0 - ? `status = "approved" AND (${whereParts.join(' OR ')})` - : 'status = "approved"' + ? `(${whereParts.join(' OR ')})` + : '1=1' - let finalWhereClause = whereClause - const bindParams: unknown[] = [...equalSlugs, ...likePatterns] + let finalWhereClause = `status = "approved" AND ${whereClause}` + const bindParams: unknown[] = [...equalSlugs] if (siteId) { finalWhereClause += ' AND site_id = ?' diff --git a/cwd-api/src/api/public/getPagePv.ts b/cwd-api/src/api/public/getPagePv.ts index cbf99e2..7c8e13d 100644 --- a/cwd-api/src/api/public/getPagePv.ts +++ b/cwd-api/src/api/public/getPagePv.ts @@ -1,10 +1,11 @@ import type { Context } from 'hono'; import type { Bindings } from '../../bindings'; +import { decodePostSlug } from '../../utils/decodePostSlug'; export const getPagePv = async (c: Context<{ Bindings: Bindings }>) => { try { const rawPostSlug = c.req.query('post_slug') || ''; - const postSlug = rawPostSlug.trim(); + const postSlug = decodePostSlug(rawPostSlug); const rawSiteId = c.req.query('siteId') || ''; const siteId = rawSiteId && rawSiteId !== 'default' ? rawSiteId : ''; diff --git a/cwd-api/src/api/public/like.ts b/cwd-api/src/api/public/like.ts index 94ed60e..7c0bc3d 100644 --- a/cwd-api/src/api/public/like.ts +++ b/cwd-api/src/api/public/like.ts @@ -1,5 +1,6 @@ import type { Context } from 'hono'; import type { Bindings } from '../../bindings'; +import { decodePostSlug } from '../../utils/decodePostSlug'; type LikeStatusResponse = { liked: boolean; @@ -36,7 +37,7 @@ export const getLikeStatus = async ( ): Promise => { try { const rawPostSlug = c.req.query('post_slug') || ''; - const postSlug = rawPostSlug.trim(); + const postSlug = decodePostSlug(rawPostSlug); const siteId = c.req.query('siteId') || ''; if (!postSlug) { @@ -92,13 +93,14 @@ export const likePage = async ( const rawPostSlug = typeof body.postSlug === 'string' ? body.postSlug.trim() : ''; + const postSlug = decodePostSlug(rawPostSlug); const rawPostTitle = typeof body.postTitle === 'string' ? body.postTitle.trim() : ''; const rawPostUrl = typeof body.postUrl === 'string' ? body.postUrl.trim() : ''; const siteId = typeof body.siteId === 'string' ? body.siteId.trim() : ''; - if (!rawPostSlug) { + if (!postSlug) { return c.json({ message: 'postSlug is required' }, 400); } @@ -109,7 +111,7 @@ export const likePage = async ( const existingLike = await c.env.CWD_DB.prepare( 'SELECT id FROM Likes WHERE page_slug = ? AND user_id = ? AND site_id = ?' ) - .bind(rawPostSlug, userId, siteId) + .bind(postSlug, userId, siteId) .first<{ id: number }>(); let alreadyLiked = false; @@ -118,7 +120,7 @@ export const likePage = async ( await c.env.CWD_DB.prepare( 'INSERT INTO Likes (page_slug, user_id, created_at, site_id) VALUES (?, ?, ?, ?)' ) - .bind(rawPostSlug, userId, now, siteId) + .bind(postSlug, userId, now, siteId) .run(); } else { alreadyLiked = true; @@ -127,7 +129,7 @@ export const likePage = async ( const pageStatsRow = await c.env.CWD_DB.prepare( 'SELECT id FROM page_stats WHERE post_slug = ? AND site_id = ?' ) - .bind(rawPostSlug, siteId) + .bind(postSlug, siteId) .first<{ id: number }>(); if (!pageStatsRow) { @@ -135,7 +137,7 @@ export const likePage = async ( 'INSERT INTO page_stats (post_slug, post_title, post_url, pv, last_visit_at, created_at, updated_at, site_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)' ) .bind( - rawPostSlug, + postSlug, rawPostTitle || null, rawPostUrl || null, 0, @@ -161,7 +163,7 @@ export const likePage = async ( const totalRow = await c.env.CWD_DB.prepare( 'SELECT COUNT(*) AS count FROM Likes WHERE page_slug = ? AND site_id = ?' ) - .bind(rawPostSlug, siteId) + .bind(postSlug, siteId) .first<{ count: number }>(); const totalLikes = totalRow?.count || 0; diff --git a/cwd-api/src/api/public/postComment.ts b/cwd-api/src/api/public/postComment.ts index 3668461..f06079e 100644 --- a/cwd-api/src/api/public/postComment.ts +++ b/cwd-api/src/api/public/postComment.ts @@ -12,8 +12,8 @@ import { EmailNotificationSettings } from '../../utils/email'; import { loadTelegramSettings, sendTelegramMessage } from '../../utils/telegram'; +import { decodePostSlug } from '../../utils/decodePostSlug'; -// 检查内容,将