diff --git a/cwd-admin/src/api/admin.ts b/cwd-admin/src/api/admin.ts
index 9abb92f..2e38dc6 100644
--- a/cwd-admin/src/api/admin.ts
+++ b/cwd-admin/src/api/admin.ts
@@ -13,6 +13,7 @@ export type CommentItem = {
email: string;
avatar: string;
postSlug: string;
+ postUrl: string | null;
url: string | null;
ipAddress: string | null;
contentText: string;
@@ -172,6 +173,7 @@ export function updateComment(data: {
name: string;
email: string;
url?: string | null;
+ postUrl?: string | null;
postSlug?: string;
contentText: string;
status?: string;
@@ -182,6 +184,7 @@ export function updateComment(data: {
name: data.name,
email: data.email,
url: data.url ?? null,
+ postUrl: data.postUrl ?? null,
postSlug: data.postSlug,
content: data.contentText,
status: data.status,
diff --git a/cwd-admin/src/views/CommentsView/components/ModalEdit.vue b/cwd-admin/src/views/CommentsView/components/ModalEdit.vue
index 50019d4..ac6155a 100644
--- a/cwd-admin/src/views/CommentsView/components/ModalEdit.vue
+++ b/cwd-admin/src/views/CommentsView/components/ModalEdit.vue
@@ -16,9 +16,13 @@
-
+
+
+
+
+
@@ -66,6 +70,7 @@ interface EditForm {
email: string;
url: string;
postSlug: string;
+ postUrl: string;
contentText: string;
status: string;
priority: number;
diff --git a/cwd-admin/src/views/CommentsView/index.vue b/cwd-admin/src/views/CommentsView/index.vue
index 8cdb023..19ebb78 100644
--- a/cwd-admin/src/views/CommentsView/index.vue
+++ b/cwd-admin/src/views/CommentsView/index.vue
@@ -263,6 +263,7 @@ const editForm = ref<{
email: string;
url: string;
postSlug: string;
+ postUrl: string;
contentText: string;
status: string;
priority: number;
@@ -438,6 +439,7 @@ function openEdit(item: CommentItem) {
email: item.email,
url: item.url || "",
postSlug: item.postSlug || "",
+ postUrl: item.postUrl || "",
contentText: item.contentText,
status: item.status,
priority:
@@ -466,6 +468,7 @@ async function submitEdit() {
const trimmedContent = current.contentText.trim();
const trimmedUrl = current.url.trim();
const trimmedPostSlug = current.postSlug.trim();
+ const trimmedPostUrl = current.postUrl.trim();
const priorityValue =
typeof current.priority === "number" && Number.isFinite(current.priority)
? current.priority
@@ -485,6 +488,7 @@ async function submitEdit() {
name: trimmedName,
email: trimmedEmail,
url: trimmedUrl || null,
+ postUrl: trimmedPostUrl || null,
postSlug: newPostSlug,
contentText: trimmedContent,
status: current.status,
@@ -497,6 +501,7 @@ async function submitEdit() {
email: trimmedEmail,
url: trimmedUrl || null,
postSlug: newPostSlug,
+ postUrl: trimmedPostUrl || null,
contentText: trimmedContent,
status: current.status,
priority: priorityValue,
diff --git a/cwd-api/src/api/admin/getDomains.ts b/cwd-api/src/api/admin/getDomains.ts
index 58ffa1d..1f5036f 100644
--- a/cwd-api/src/api/admin/getDomains.ts
+++ b/cwd-api/src/api/admin/getDomains.ts
@@ -25,15 +25,15 @@ export const getDomains = async (c: Context<{ Bindings: Bindings }>) => {
const domains = new Set();
const { results: commentRows } = await c.env.CWD_DB.prepare(
- 'SELECT post_slug, url FROM Comment'
+ 'SELECT post_slug, post_url FROM Comment'
).all<{
post_slug: string;
- url: string | null;
+ post_url: string | null;
}>();
for (const row of commentRows) {
const domain =
- extractDomain(row.post_slug) || extractDomain(row.url);
+ extractDomain(row.post_url) || extractDomain(row.post_slug);
if (domain) {
domains.add(domain);
}
diff --git a/cwd-api/src/api/admin/getStats.ts b/cwd-api/src/api/admin/getStats.ts
index e88b00f..9153b3f 100644
--- a/cwd-api/src/api/admin/getStats.ts
+++ b/cwd-api/src/api/admin/getStats.ts
@@ -37,11 +37,11 @@ export const getStats = async (c: Context<{ Bindings: Bindings }>) => {
const domainFilter = rawDomain.trim().toLowerCase();
const { results } = await c.env.CWD_DB.prepare(
- 'SELECT created, post_slug, url, status FROM Comment'
+ 'SELECT created, post_slug, post_url, status FROM Comment'
).all<{
created: number;
post_slug: string;
- url: string | null;
+ post_url: string | null;
status: string;
}>();
@@ -69,7 +69,7 @@ export const getStats = async (c: Context<{ Bindings: Bindings }>) => {
for (const row of results) {
const domain =
- extractDomain(row.post_slug) || extractDomain(row.url) || 'unknown';
+ extractDomain(row.post_url) || extractDomain(row.post_slug) || 'unknown';
let counts = domainMap.get(domain);
if (!counts) {
diff --git a/cwd-api/src/api/admin/listComments.ts b/cwd-api/src/api/admin/listComments.ts
index edeebbf..9c87760 100644
--- a/cwd-api/src/api/admin/listComments.ts
+++ b/cwd-api/src/api/admin/listComments.ts
@@ -17,7 +17,7 @@ export const listComments = async (c: Context<{ Bindings: Bindings }>) => {
const params: (string | number)[] = [];
if (domain) {
const pattern = `%://${domain}/%`;
- whereSql = 'WHERE post_slug LIKE ? OR url LIKE ?';
+ whereSql = 'WHERE post_slug LIKE ? OR post_url LIKE ?';
params.push(pattern, pattern);
}
diff --git a/cwd-api/src/api/admin/updateComment.ts b/cwd-api/src/api/admin/updateComment.ts
index 38e43d7..6ba4305 100644
--- a/cwd-api/src/api/admin/updateComment.ts
+++ b/cwd-api/src/api/admin/updateComment.ts
@@ -25,10 +25,16 @@ export const updateComment = async (c: Context<{ Bindings: Bindings }>) => {
}
const existing = await c.env.CWD_DB.prepare(
- 'SELECT id, status, post_slug, priority FROM Comment WHERE id = ?'
+ 'SELECT id, status, post_slug, post_url, priority FROM Comment WHERE id = ?'
)
.bind(id)
- .first<{ id: number; status: string; post_slug: string; priority: number | null }>();
+ .first<{
+ id: number;
+ status: string;
+ post_slug: string;
+ post_url: string | null;
+ priority: number | null;
+ }>();
if (!existing) {
return c.json({ message: 'Comment not found' }, 404);
@@ -48,6 +54,15 @@ export const updateComment = async (c: Context<{ Bindings: Bindings }>) => {
: typeof body.post_slug === 'string'
? body.post_slug
: '';
+ const hasPostUrlField =
+ Object.prototype.hasOwnProperty.call(body, 'postUrl') ||
+ Object.prototype.hasOwnProperty.call(body, 'post_url');
+ const rawPostUrl =
+ typeof body.postUrl === 'string'
+ ? body.postUrl
+ : typeof body.post_url === 'string'
+ ? body.post_url
+ : '';
const contentSource =
typeof body.content === 'string'
@@ -63,6 +78,9 @@ export const updateComment = async (c: Context<{ Bindings: Bindings }>) => {
const postSlug = hasPostSlugField
? (rawPostSlug.trim() || existing.post_slug)
: existing.post_slug;
+ const postUrl = hasPostUrlField
+ ? (rawPostUrl.trim() || null)
+ : existing.post_url;
let priority: number = typeof existing.priority === 'number' && Number.isFinite(existing.priority)
? existing.priority
@@ -107,9 +125,9 @@ export const updateComment = async (c: Context<{ Bindings: Bindings }>) => {
});
const { success } = await c.env.CWD_DB.prepare(
- 'UPDATE Comment SET name = ?, email = ?, url = ?, content_text = ?, content_html = ?, status = ?, post_slug = ?, priority = ? WHERE id = ?'
+ 'UPDATE Comment SET name = ?, email = ?, url = ?, content_text = ?, content_html = ?, status = ?, post_slug = ?, post_url = ?, priority = ? WHERE id = ?'
)
- .bind(name, email, url, contentText, contentHtml, status, postSlug, priority, id)
+ .bind(name, email, url, contentText, contentHtml, status, postSlug, postUrl, priority, id)
.run();
if (!success) {
diff --git a/cwd-api/src/api/public/getComments.ts b/cwd-api/src/api/public/getComments.ts
index c48a2c0..be25aa9 100644
--- a/cwd-api/src/api/public/getComments.ts
+++ b/cwd-api/src/api/public/getComments.ts
@@ -33,28 +33,39 @@ export const getComments = async (c: Context<{ Bindings: Bindings }>) => {
}
try {
- let query = `
- SELECT id, name, email, url, content_text as contentText,
+ const equalSlugs = Array.from(new Set(slugList))
+ const likePatternsSet = new Set()
+ for (const s of equalSlugs) {
+ likePatternsSet.add(`${s}#%`)
+ likePatternsSet.add(`${s}?%`)
+ }
+ const likePatterns = Array.from(likePatternsSet)
+ const whereParts: string[] = []
+ if (equalSlugs.length === 1) {
+ whereParts.push('post_slug = ?')
+ } else if (equalSlugs.length > 1) {
+ 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"'
+ const query = `
+ SELECT id, name, email, url, content_text as contentText,
content_html as contentHtml, created, parent_id as parentId,
post_slug as postSlug, post_url as postUrl, priority, COALESCE(likes, 0) as likes
- FROM Comment
- WHERE status = "approved" AND post_slug = ?
+ FROM Comment
+ WHERE ${whereClause}
ORDER BY priority DESC, created DESC
`
- if (slugList.length > 1) {
- const placeholders = slugList.map(() => '?').join(', ')
- query = `
- SELECT id, name, email, url, content_text as contentText,
- content_html as contentHtml, created, parent_id as parentId,
- post_slug as postSlug, post_url as postUrl, priority, COALESCE(likes, 0) as likes
- FROM Comment
- WHERE status = "approved" AND post_slug IN (${placeholders})
- ORDER BY priority DESC, created DESC
- `
- }
+ const bindParams: unknown[] = [...equalSlugs, ...likePatterns]
const [commentsResult, adminEmailRows] = await Promise.all([
- c.env.CWD_DB.prepare(query).bind(...slugList).all(),
+ c.env.CWD_DB.prepare(query).bind(...bindParams).all(),
c.env.CWD_DB.prepare('SELECT key, value FROM Settings WHERE key IN (?, ?)')
.bind('comment_admin_email', 'admin_notify_email')
.all<{ key: string; value: string }>()
diff --git a/docs/api/admin/comments.md b/docs/api/admin/comments.md
index 177364e..c1718d5 100644
--- a/docs/api/admin/comments.md
+++ b/docs/api/admin/comments.md
@@ -44,7 +44,8 @@ GET /admin/comments/list
"created": 1736762400000,
"name": "张三",
"email": "zhangsan@example.com",
- "postSlug": "https://your-blog.example.com/blog/hello-world",
+ "postSlug": "hello-world",
+ "postUrl": "https://your-blog.example.com/blog/hello-world",
"url": "https://zhangsan.me",
"ipAddress": "127.0.0.1",
"contentText": "很棒的文章!",
@@ -63,6 +64,25 @@ GET /admin/comments/list
}
```
+返回字段说明:
+
+| 字段名 | 类型 | 说明 |
+| ----------- | ------ | -------------------------- |
+| `id` | number | 评论 ID |
+| `created` | number | 创建时间戳 |
+| `name` | string | 评论者昵称 |
+| `email` | string | 评论者邮箱 |
+| `postSlug` | string | 文章 slug |
+| `postUrl` | string | null) | 文章完整 URL |
+| `url` | string | null) | 评论者个人主页地址 |
+| `ipAddress` | string | 评论者 IP 地址 |
+| `contentText` | string | 评论内容(纯文本) |
+| `contentHtml` | string | 评论内容(渲染后的 HTML) |
+| `status` | string | 评论状态 |
+| `priority` | number | 置顶权重 |
+| `ua` | string | 用户代理 |
+| `avatar` | string | 头像 URL(Gravatar) |
+
**鉴权错误**
- 未携带 Token 或 Token 失效:
@@ -156,7 +176,8 @@ PUT /admin/comments/update
"name": "张三",
"email": "zhangsan@example.com",
"url": "https://zhangsan.me",
- "postSlug": "https://example.com/blog/hello-world",
+ "postSlug": "hello-world",
+ "postUrl": "https://example.com/blog/hello-world",
"contentText": "更新后的评论内容",
"status": "approved",
"priority": 2
@@ -171,10 +192,12 @@ PUT /admin/comments/update
| `name` | string | 是 | 评论者昵称 |
| `email` | string | 是 | 评论者邮箱 |
| `url` | string | 否 | 评论者个人主页或站点地址 |
-| `postSlug` | string | 否 | 评论地址(文章 URL),不传则保持原值 |
-| `contentText` | string | 是 | 评论内容(纯文本) |
-`| `status` | string | 否 | 评论状态,可选 `approved`、`pending`、`rejected` |
-| `priority` | number | 否 | 置顶权重,默认为 1,数值越大越靠前 |
+| `postSlug` | string | 否 | 文章 slug,不传则保持原值,支持 `postSlug` 或 `post_slug` |
+| `postUrl` | string | 否 | 文章完整 URL,不传则保持原值,支持 `postUrl` 或 `post_url` |
+| `content` | string | 是* | 评论内容(Markdown 或纯文本),与 `contentText` 二选一 |
+| `contentText` | string | 是* | 评论内容(纯文本),与 `content` 二选一 |
+| `status` | string | 否 | 评论状态,可选 `approved`、`pending`、`rejected` |
+| `priority` | number | 否 | 置顶权重,默认为 1,数值越大越靠前,必须 >= 1 |
**置顶权重说明**:
diff --git a/docs/api/public/comments.md b/docs/api/public/comments.md
index 77c45fd..bc4aed2 100644
--- a/docs/api/public/comments.md
+++ b/docs/api/public/comments.md
@@ -16,12 +16,12 @@ GET /api/comments
**查询参数**
-| 名称 | 位置 | 类型 | 必填 | 说明 |
-| ----------- | ----- | ------- | ---- | -------------------------------------------------------------------------------------------- |
-| `post_slug` | query | string | 是 | 使用 `window.location.origin + window`location.pathname`,获取带域名的链接,否则后台无法识别 |
-| `page` | query | integer | 否 | 页码,默认 `1` |
-| `limit` | query | integer | 否 | 每页数量,默认 `20`,最大 `50` |
-| `nested` | query | string | 否 | 是否返回嵌套结构,默认 `'true'` |
+| 名称 | 位置 | 类型 | 必填 | 说明 |
+| ----------- | ----- | ------- | ---- | ------------------------------------------ |
+| `post_slug` | query | string | 是 | 文章 slug,与前端 `CWDComments` 的 `postSlug` 参数对应 |
+| `page` | query | integer | 否 | 页码,默认 `1` |
+| `limit` | query | integer | 否 | 每页数量,默认 `20`,最大 `50` |
+| `nested` | query | string | 否 | 是否返回嵌套结构,默认 `'true'` |
**成功响应**
@@ -70,6 +70,25 @@ GET /api/comments
}
```
+返回字段说明:
+
+| 字段名 | 类型 | 说明 |
+| ----------- | ------ | -------------------------- |
+| `id` | number | 评论 ID |
+| `author` | string | 评论者昵称 |
+| `email` | string | 评论者邮箱(hash后) |
+| `url` | string | null) | 评论者个人主页地址 |
+| `contentText` | string | 评论内容(纯文本) |
+| `contentHtml` | string | 评论内容(渲染后的 HTML) |
+| `pubDate` | string | 发布时间(ISO 8601 格式) |
+| `postSlug` | string | 文章 slug |
+| `avatar` | string | 头像 URL(Gravatar) |
+| `priority` | number | 置顶权重,数值越大排序越靠前 |
+| `likes` | number | 点赞数,默认为 0 |
+| `replies` | array | 子评论列表(嵌套结构时) |
+| `parentId` | number | null) | 父评论 ID(回复时) |
+| `replyToAuthor` | string( | null) | 被回复者的昵称(回复时) |
+
说明:
- 当 `nested=true`(默认)时,接口返回的是"根评论列表",每条根评论包含其 `replies`。
@@ -121,7 +140,7 @@ POST /api/comments
```json
{
- "post_slug": "https://example.com/blog/hello-world",
+ "post_slug": "hello-world",
"post_title": "博客标题,可选",
"post_url": "https://example.com/blog/hello-world",
"name": "张三",
@@ -135,16 +154,16 @@ POST /api/comments
字段说明:
-| 字段名 | 类型 | 必填 | 说明 |
-| ------------ | ------ | ---- | ------------------------------------------------------------------------------------------------------------- |
-| `post_slug` | string | 是 | 文章唯一标识符,应与前端组件初始化时的 `postSlug` 值一致,`window.location.origin + window.location.pathname` |
-| `post_title` | string | 否 | 文章标题,用于邮件通知内容 |
-| `post_url` | string | 否 | 文章 URL,用于邮件通知中的跳转链接 |
-| `name` | string | 是 | 评论者昵称 |
-| `email` | string | 是 | 评论者邮箱,需为合法邮箱格式 |
-| `url` | string | 否 | 评论者个人主页或站点地址 |
-| `content` | string | 是 | 评论内容,内部会过滤 `` 片段 |
-| `parent_id` | number | 否 | 父评论 ID,用于回复功能;缺省或 `null` 表示根评论 |
+| 字段名 | 类型 | 必填 | 说明 |
+| ------------ | ------ | ---- | -------------------------------------------------------------------- |
+| `post_slug` | string | 是 | 文章 slug,应与前端组件初始化时的 `postSlug` 值一致,如 `hello-world` |
+| `post_title` | string | 否 | 文章标题,用于邮件通知内容 |
+| `post_url` | string | 否 | 文章完整 URL,用于邮件通知中的跳转链接 |
+| `name` | string | 是 | 评论者昵称 |
+| `email` | string | | 是 | 评论者邮箱,需为合法邮箱格式 |
+| `url` | string | 否 | 评论者个人主页或站点地址 |
+| `content` | string | 是 | 评论内容,内部会过滤 `` 片段 |
+| `parent_id` | number | 否 | 父评论 ID,用于回复功能;缺省或 `null` 表示根评论 |
| `adminToken` | string | 否 | 管理员评论密钥,博主发布评论时需要先通过 `/api/verify-admin` 验证密钥后将密钥传入此字段,评论将直接通过且不受审核设置影响 |
**成功响应**