fix(api): 移除评论导出接口的站点筛选功能

移除评论导出接口的站点筛选功能,现在导出所有站点的评论数据。同时更新相关文档,移除站点隔离相关的API说明,并修复文档组件的内存泄漏问题。导入接口现在支持自动识别和转换第三方评论系统数据格式,并修复站点ID字段的导入问题。

- 移除 `/admin/comments/export` 接口的 `siteId` 查询参数
- 更新数据导入文档,说明第三方评论系统自动转换功能
- 修复文档组件页面切换时的配置更新和内存泄漏
- 修复评论和统计数据的导入逻辑,确保站点ID字段正确保存
- 更新数据库表结构,为统计相关表添加站点ID字段和联合唯一约束
This commit is contained in:
anghunk
2026-02-09 16:52:48 +08:00
parent a698734df5
commit ac87af705f
9 changed files with 72 additions and 116 deletions

View File

@@ -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<any[]> {
const searchParams = new URLSearchParams();
if (siteId && siteId !== 'default') {
searchParams.set('siteId', siteId);
}
const query = searchParams.toString();
return get<any[]>(query ? `/admin/comments/export?${query}` : '/admin/comments/export');
export function exportComments(): Promise<any[]> {
return get<any[]>('/admin/comments/export');
}
export function importComments(data: any[]): Promise<{ message: string }> {

View File

@@ -180,7 +180,7 @@ async function executeExport(apiFunc: () => Promise<any>, 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');

View File

@@ -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) {

View File

@@ -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');

View File

@@ -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) {

View File

@@ -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);

View File

@@ -3,12 +3,12 @@
</template>
<script setup>
import { onMounted, ref, watch } from "vue";
import { onMounted, onBeforeUnmount, ref, watch } from "vue";
import { useData } from "vitepress";
const commentsRoot = ref(null);
const commentsInstance = ref(null);
const { isDark } = useData();
const { isDark, page } = useData();
const getTheme = () => (isDark.value ? "dark" : "light");
@@ -53,5 +53,21 @@ onMounted(async () => {
},
{ immediate: false }
);
watch(
() => page.value.relativePath,
() => {
if (!commentsInstance.value) return;
commentsInstance.value.updateConfig({});
},
{ immediate: false }
);
});
onBeforeUnmount(() => {
if (commentsInstance.value) {
commentsInstance.value.unmount();
commentsInstance.value = null;
}
});
</script>

View File

@@ -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` 字段且存在则会覆盖原有数据
**成功响应**

View File

@@ -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`