From ac87af705fa2615d603d158dfac69fc06b426102 Mon Sep 17 00:00:00 2001 From: anghunk Date: Mon, 9 Feb 2026 16:52:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(api):=20=E7=A7=BB=E9=99=A4=E8=AF=84?= =?UTF-8?q?=E8=AE=BA=E5=AF=BC=E5=87=BA=E6=8E=A5=E5=8F=A3=E7=9A=84=E7=AB=99?= =?UTF-8?q?=E7=82=B9=E7=AD=9B=E9=80=89=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除评论导出接口的站点筛选功能,现在导出所有站点的评论数据。同时更新相关文档,移除站点隔离相关的API说明,并修复文档组件的内存泄漏问题。导入接口现在支持自动识别和转换第三方评论系统数据格式,并修复站点ID字段的导入问题。 - 移除 `/admin/comments/export` 接口的 `siteId` 查询参数 - 更新数据导入文档,说明第三方评论系统自动转换功能 - 修复文档组件页面切换时的配置更新和内存泄漏 - 修复评论和统计数据的导入逻辑,确保站点ID字段正确保存 - 更新数据库表结构,为统计相关表添加站点ID字段和联合唯一约束 --- cwd-admin/src/api/admin.ts | 9 +--- cwd-admin/src/views/DataView/index.vue | 2 +- cwd-api/src/api/admin/exportComments.ts | 14 +----- cwd-api/src/api/admin/importBackup.ts | 52 +++++++++++----------- cwd-api/src/api/admin/importComments.ts | 8 ++-- cwd-api/src/api/admin/importStats.ts | 20 +++++---- docs/.vitepress/components/footerDoc.vue | 20 ++++++++- docs/api/admin/data-migration.md | 7 ++- docs/config/site-isolation.md | 56 ------------------------ 9 files changed, 72 insertions(+), 116 deletions(-) diff --git a/cwd-admin/src/api/admin.ts b/cwd-admin/src/api/admin.ts index 468ecc5..e1e6cbe 100644 --- a/cwd-admin/src/api/admin.ts +++ b/cwd-admin/src/api/admin.ts @@ -263,13 +263,8 @@ export function blockEmail(email: string): Promise<{ message: string }> { return post<{ message: string }>('/admin/comments/block-email', { email }); } -export function exportComments(siteId?: string): Promise { - const searchParams = new URLSearchParams(); - if (siteId && siteId !== 'default') { - searchParams.set('siteId', siteId); - } - const query = searchParams.toString(); - return get(query ? `/admin/comments/export?${query}` : '/admin/comments/export'); +export function exportComments(): Promise { + return get('/admin/comments/export'); } export function importComments(data: any[]): Promise<{ message: string }> { diff --git a/cwd-admin/src/views/DataView/index.vue b/cwd-admin/src/views/DataView/index.vue index 54835be..279d06e 100644 --- a/cwd-admin/src/views/DataView/index.vue +++ b/cwd-admin/src/views/DataView/index.vue @@ -180,7 +180,7 @@ async function executeExport(apiFunc: () => Promise, fileNamePrefix: string } // 导出处理 -const handleExportComments = () => executeExport(() => exportComments(currentSiteId.value), 'comments-export'); +const handleExportComments = () => executeExport(exportComments, 'comments-export'); const handleExportConfig = () => executeExport(exportConfig, 'cwd-config'); const handleExportStats = () => executeExport(() => exportStats(currentSiteId.value), 'cwd-stats'); const handleExportBackup = () => executeExport(exportBackup, 'cwd-full-backup'); diff --git a/cwd-api/src/api/admin/exportComments.ts b/cwd-api/src/api/admin/exportComments.ts index 0b1fcec..189332e 100644 --- a/cwd-api/src/api/admin/exportComments.ts +++ b/cwd-api/src/api/admin/exportComments.ts @@ -3,18 +3,8 @@ import { Bindings } from '../../bindings'; export const exportComments = async (c: Context<{ Bindings: Bindings }>) => { try { - const siteId = c.req.query('siteId'); - let query = 'SELECT * FROM Comment'; - const params: any[] = []; - - if (siteId) { - query += ' WHERE site_id = ?'; - params.push(siteId); - } - - query += ' ORDER BY priority DESC, created DESC'; - - const { results } = await c.env.CWD_DB.prepare(query).bind(...params).all(); + const query = 'SELECT * FROM Comment ORDER BY priority DESC, created DESC'; + const { results } = await c.env.CWD_DB.prepare(query).all(); return c.json(results); } catch (e: any) { diff --git a/cwd-api/src/api/admin/importBackup.ts b/cwd-api/src/api/admin/importBackup.ts index a167120..17d321c 100644 --- a/cwd-api/src/api/admin/importBackup.ts +++ b/cwd-api/src/api/admin/importBackup.ts @@ -24,32 +24,34 @@ const saveComments = async (env: Bindings, comments: any[]) => { content_text, content_html, parent_id, - status, - likes - } = comment; + status, + likes, + site_id + } = comment; - const fields = [ - 'created', 'post_slug', 'name', 'email', 'url', - 'ip_address', 'device', 'os', 'browser', 'ua', - 'content_text', 'content_html', 'parent_id', 'status', 'likes' - ]; - const values = [ - 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", - typeof likes === 'number' && Number.isFinite(likes) && likes >= 0 ? likes : 0 - ]; + const fields = [ + 'created', 'post_slug', 'name', 'email', 'url', + 'ip_address', 'device', 'os', 'browser', 'ua', + 'content_text', 'content_html', 'parent_id', 'status', 'likes', 'site_id' + ]; + const values = [ + 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", + typeof likes === 'number' && Number.isFinite(likes) && likes >= 0 ? likes : 0, + site_id || "" + ]; if (id !== undefined && id !== null) { fields.unshift('id'); diff --git a/cwd-api/src/api/admin/importComments.ts b/cwd-api/src/api/admin/importComments.ts index bb62418..07df044 100644 --- a/cwd-api/src/api/admin/importComments.ts +++ b/cwd-api/src/api/admin/importComments.ts @@ -120,13 +120,14 @@ export const importComments = async (c: Context<{ Bindings: Bindings }>) => { content_html, parent_id, status, - likes + likes, + site_id } = comment; const fields = [ 'created', 'post_slug', 'name', 'email', 'url', 'ip_address', 'device', 'os', 'browser', 'ua', - 'content_text', 'content_html', 'parent_id', 'status', 'likes' + 'content_text', 'content_html', 'parent_id', 'status', 'likes', 'site_id' ]; const values = [ created || Date.now(), @@ -143,7 +144,8 @@ export const importComments = async (c: Context<{ Bindings: Bindings }>) => { content_html || "", parent_id || null, status || "approved", - typeof likes === 'number' && Number.isFinite(likes) && likes >= 0 ? likes : 0 + typeof likes === 'number' && Number.isFinite(likes) && likes >= 0 ? likes : 0, + site_id || "" ]; if (id !== undefined && id !== null) { diff --git a/cwd-api/src/api/admin/importStats.ts b/cwd-api/src/api/admin/importStats.ts index e390664..284af50 100644 --- a/cwd-api/src/api/admin/importStats.ts +++ b/cwd-api/src/api/admin/importStats.ts @@ -3,15 +3,15 @@ import { Bindings } from '../../bindings'; async function ensureStatsTables(env: Bindings) { await env.CWD_DB.prepare( - 'CREATE TABLE IF NOT EXISTS page_stats (id INTEGER PRIMARY KEY AUTOINCREMENT, post_slug TEXT UNIQUE NOT NULL, post_title TEXT, post_url TEXT, pv INTEGER NOT NULL DEFAULT 0, last_visit_at INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)' + 'CREATE TABLE IF NOT EXISTS page_stats (id INTEGER PRIMARY KEY AUTOINCREMENT, site_id TEXT NOT NULL DEFAULT "", post_slug TEXT NOT NULL, post_title TEXT, post_url TEXT, pv INTEGER NOT NULL DEFAULT 0, last_visit_at INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, UNIQUE(site_id, post_slug))' ).run(); await env.CWD_DB.prepare( - 'CREATE TABLE IF NOT EXISTS page_visit_daily (id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT NOT NULL, domain TEXT, count INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)' + 'CREATE TABLE IF NOT EXISTS page_visit_daily (id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT NOT NULL, domain TEXT, count INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, site_id TEXT NOT NULL DEFAULT "")' ).run(); await env.CWD_DB.prepare( - 'CREATE TABLE IF NOT EXISTS Likes (id INTEGER PRIMARY KEY AUTOINCREMENT, page_slug TEXT NOT NULL, user_id TEXT NOT NULL, created_at INTEGER NOT NULL, UNIQUE(page_slug, user_id))' + 'CREATE TABLE IF NOT EXISTS Likes (id INTEGER PRIMARY KEY AUTOINCREMENT, site_id TEXT NOT NULL DEFAULT "", page_slug TEXT NOT NULL, user_id TEXT NOT NULL, created_at INTEGER NOT NULL, UNIQUE(site_id, page_slug, user_id))' ).run(); } @@ -29,7 +29,8 @@ export const saveStatsData = async (env: Bindings, data: any) => { 'pv', 'last_visit_at', 'created_at', - 'updated_at' + 'updated_at', + 'site_id' ]; const values = [ item.post_slug, @@ -38,7 +39,8 @@ export const saveStatsData = async (env: Bindings, data: any) => { item.pv, item.last_visit_at, item.created_at, - item.updated_at + item.updated_at, + item.site_id || "" ]; if (item.id) { fields.unshift('id'); @@ -55,8 +57,8 @@ export const saveStatsData = async (env: Bindings, data: any) => { if (Array.isArray(data.page_visit_daily)) { for (const item of data.page_visit_daily) { - const fields = ['date', 'domain', 'count', 'created_at', 'updated_at']; - const values = [item.date, item.domain, item.count, item.created_at, item.updated_at]; + const fields = ['date', 'domain', 'count', 'created_at', 'updated_at', 'site_id']; + const values = [item.date, item.domain, item.count, item.created_at, item.updated_at, item.site_id || ""]; if (item.id) { fields.unshift('id'); values.unshift(item.id); @@ -72,8 +74,8 @@ export const saveStatsData = async (env: Bindings, data: any) => { if (Array.isArray(data.likes)) { for (const item of data.likes) { - const fields = ['page_slug', 'user_id', 'created_at']; - const values = [item.page_slug, item.user_id, item.created_at]; + const fields = ['page_slug', 'user_id', 'created_at', 'site_id']; + const values = [item.page_slug, item.user_id, item.created_at, item.site_id || ""]; if (item.id) { fields.unshift('id'); values.unshift(item.id); diff --git a/docs/.vitepress/components/footerDoc.vue b/docs/.vitepress/components/footerDoc.vue index 96bd41f..84e8bdd 100644 --- a/docs/.vitepress/components/footerDoc.vue +++ b/docs/.vitepress/components/footerDoc.vue @@ -3,12 +3,12 @@ diff --git a/docs/api/admin/data-migration.md b/docs/api/admin/data-migration.md index daa46d7..149f31b 100644 --- a/docs/api/admin/data-migration.md +++ b/docs/api/admin/data-migration.md @@ -109,7 +109,12 @@ POST /admin/comments/import 说明: - 若从 CWD 自身导出的评论数据进行恢复,可直接将 `/admin/comments/export` 接口导出的 JSON 原样提交到本接口; -- 若从 Twikoo / Artalk 等其他评论系统迁移数据,可通过管理后台「评论数据导入」功能上传对应的 JSON 文件,前端会自动转换为上述结构后调用本接口。 +- 若从 **Twikoo** / **Artalk** 等其他评论系统迁移数据,本接口会自动识别并转换数据格式: + - **Twikoo**: 自动映射 `nick` → `name`, `mail` → `email`, `href` → `post_slug`, `comment` → `content_text` 等字段 + - **Artalk**: 自动映射 `nick` → `name`, `page_key` → `post_slug`, `content` → `content_text` 等字段 + - 时间字段自动转换为时间戳格式 + - 非数字 ID 会自动丢弃,由数据库重新生成自增 ID +- 数据导入采用 `INSERT OR REPLACE` 策略,若提供 `id` 字段且存在则会覆盖原有数据 **成功响应** diff --git a/docs/config/site-isolation.md b/docs/config/site-isolation.md index ce63b24..0edb546 100644 --- a/docs/config/site-isolation.md +++ b/docs/config/site-isolation.md @@ -37,62 +37,6 @@ CWD 评论系统支持通过 `siteId` 参数实现多站点数据隔离。当你 - 避免使用特殊字符和空格 - 建议使用有意义的名称,便于识别不同站点 -## API 调用 - -### 公开 API - -当使用站点隔离功能时,所有公开 API 请求都需要在查询参数中携带 `siteId`: - -```javascript -// 获取评论列表 -GET /api/comments?post_slug=hello-world&siteId=blog - -// 提交评论 -POST /api/comments -{ - "post_slug": "hello-world", - "name": "张三", - "email": "zhangsan@example.com", - "content": "很棒的文章!" -} -// 请求头:X-Site-Id: blog - -// 获取配置 -GET /api/config/comments?siteId=blog -``` - -### 管理后台 API - -管理后台同样支持通过 `siteId` 参数进行数据筛选: - -```javascript -// 获取评论列表 -GET /admin/comments/list?siteId=blog - -// 导出评论数据 -GET /admin/comments/export?siteId=blog - -// 获取评论统计数据 -GET /admin/stats/comments?siteId=blog - -// 获取访问统计概览 -GET /admin/analytics/overview?siteId=blog - -// 获取页面访问统计 -GET /admin/analytics/pages?siteId=blog - -// 获取点赞统计数据 -GET /admin/likes/stats?siteId=blog - -// 导出统计数据 -GET /admin/export/stats?siteId=blog -``` - -说明: - -- 当提供 `siteId` 参数且不为 `default` 时,接口仅返回该站点下的数据 -- 不提供 `siteId` 参数时,返回所有站点的数据 - ## 多站点示例 如果你有多个站点,可以为每个站点配置不同的 `siteId`: