fix(api): 移除评论导出接口的站点筛选功能
移除评论导出接口的站点筛选功能,现在导出所有站点的评论数据。同时更新相关文档,移除站点隔离相关的API说明,并修复文档组件的内存泄漏问题。导入接口现在支持自动识别和转换第三方评论系统数据格式,并修复站点ID字段的导入问题。 - 移除 `/admin/comments/export` 接口的 `siteId` 查询参数 - 更新数据导入文档,说明第三方评论系统自动转换功能 - 修复文档组件页面切换时的配置更新和内存泄漏 - 修复评论和统计数据的导入逻辑,确保站点ID字段正确保存 - 更新数据库表结构,为统计相关表添加站点ID字段和联合唯一约束
This commit is contained in:
@@ -263,13 +263,8 @@ export function blockEmail(email: string): Promise<{ message: string }> {
|
|||||||
return post<{ message: string }>('/admin/comments/block-email', { email });
|
return post<{ message: string }>('/admin/comments/block-email', { email });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function exportComments(siteId?: string): Promise<any[]> {
|
export function exportComments(): Promise<any[]> {
|
||||||
const searchParams = new URLSearchParams();
|
return get<any[]>('/admin/comments/export');
|
||||||
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 importComments(data: any[]): Promise<{ message: string }> {
|
export function importComments(data: any[]): Promise<{ message: string }> {
|
||||||
|
|||||||
@@ -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 handleExportConfig = () => executeExport(exportConfig, 'cwd-config');
|
||||||
const handleExportStats = () => executeExport(() => exportStats(currentSiteId.value), 'cwd-stats');
|
const handleExportStats = () => executeExport(() => exportStats(currentSiteId.value), 'cwd-stats');
|
||||||
const handleExportBackup = () => executeExport(exportBackup, 'cwd-full-backup');
|
const handleExportBackup = () => executeExport(exportBackup, 'cwd-full-backup');
|
||||||
|
|||||||
@@ -3,18 +3,8 @@ import { Bindings } from '../../bindings';
|
|||||||
|
|
||||||
export const exportComments = async (c: Context<{ Bindings: Bindings }>) => {
|
export const exportComments = async (c: Context<{ Bindings: Bindings }>) => {
|
||||||
try {
|
try {
|
||||||
const siteId = c.req.query('siteId');
|
const query = 'SELECT * FROM Comment ORDER BY priority DESC, created DESC';
|
||||||
let query = 'SELECT * FROM Comment';
|
const { results } = await c.env.CWD_DB.prepare(query).all();
|
||||||
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();
|
|
||||||
|
|
||||||
return c.json(results);
|
return c.json(results);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@@ -25,13 +25,14 @@ const saveComments = async (env: Bindings, comments: any[]) => {
|
|||||||
content_html,
|
content_html,
|
||||||
parent_id,
|
parent_id,
|
||||||
status,
|
status,
|
||||||
likes
|
likes,
|
||||||
|
site_id
|
||||||
} = comment;
|
} = comment;
|
||||||
|
|
||||||
const fields = [
|
const fields = [
|
||||||
'created', 'post_slug', 'name', 'email', 'url',
|
'created', 'post_slug', 'name', 'email', 'url',
|
||||||
'ip_address', 'device', 'os', 'browser', 'ua',
|
'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 = [
|
const values = [
|
||||||
created || Date.now(),
|
created || Date.now(),
|
||||||
@@ -48,7 +49,8 @@ const saveComments = async (env: Bindings, comments: any[]) => {
|
|||||||
content_html || "",
|
content_html || "",
|
||||||
parent_id || null,
|
parent_id || null,
|
||||||
status || "approved",
|
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) {
|
if (id !== undefined && id !== null) {
|
||||||
|
|||||||
@@ -120,13 +120,14 @@ export const importComments = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
content_html,
|
content_html,
|
||||||
parent_id,
|
parent_id,
|
||||||
status,
|
status,
|
||||||
likes
|
likes,
|
||||||
|
site_id
|
||||||
} = comment;
|
} = comment;
|
||||||
|
|
||||||
const fields = [
|
const fields = [
|
||||||
'created', 'post_slug', 'name', 'email', 'url',
|
'created', 'post_slug', 'name', 'email', 'url',
|
||||||
'ip_address', 'device', 'os', 'browser', 'ua',
|
'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 = [
|
const values = [
|
||||||
created || Date.now(),
|
created || Date.now(),
|
||||||
@@ -143,7 +144,8 @@ export const importComments = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
content_html || "",
|
content_html || "",
|
||||||
parent_id || null,
|
parent_id || null,
|
||||||
status || "approved",
|
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) {
|
if (id !== undefined && id !== null) {
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import { Bindings } from '../../bindings';
|
|||||||
|
|
||||||
async function ensureStatsTables(env: Bindings) {
|
async function ensureStatsTables(env: Bindings) {
|
||||||
await env.CWD_DB.prepare(
|
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();
|
).run();
|
||||||
|
|
||||||
await env.CWD_DB.prepare(
|
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();
|
).run();
|
||||||
|
|
||||||
await env.CWD_DB.prepare(
|
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();
|
).run();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +29,8 @@ export const saveStatsData = async (env: Bindings, data: any) => {
|
|||||||
'pv',
|
'pv',
|
||||||
'last_visit_at',
|
'last_visit_at',
|
||||||
'created_at',
|
'created_at',
|
||||||
'updated_at'
|
'updated_at',
|
||||||
|
'site_id'
|
||||||
];
|
];
|
||||||
const values = [
|
const values = [
|
||||||
item.post_slug,
|
item.post_slug,
|
||||||
@@ -38,7 +39,8 @@ export const saveStatsData = async (env: Bindings, data: any) => {
|
|||||||
item.pv,
|
item.pv,
|
||||||
item.last_visit_at,
|
item.last_visit_at,
|
||||||
item.created_at,
|
item.created_at,
|
||||||
item.updated_at
|
item.updated_at,
|
||||||
|
item.site_id || ""
|
||||||
];
|
];
|
||||||
if (item.id) {
|
if (item.id) {
|
||||||
fields.unshift('id');
|
fields.unshift('id');
|
||||||
@@ -55,8 +57,8 @@ export const saveStatsData = async (env: Bindings, data: any) => {
|
|||||||
|
|
||||||
if (Array.isArray(data.page_visit_daily)) {
|
if (Array.isArray(data.page_visit_daily)) {
|
||||||
for (const item of data.page_visit_daily) {
|
for (const item of data.page_visit_daily) {
|
||||||
const fields = ['date', 'domain', 'count', 'created_at', '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];
|
const values = [item.date, item.domain, item.count, item.created_at, item.updated_at, item.site_id || ""];
|
||||||
if (item.id) {
|
if (item.id) {
|
||||||
fields.unshift('id');
|
fields.unshift('id');
|
||||||
values.unshift(item.id);
|
values.unshift(item.id);
|
||||||
@@ -72,8 +74,8 @@ export const saveStatsData = async (env: Bindings, data: any) => {
|
|||||||
|
|
||||||
if (Array.isArray(data.likes)) {
|
if (Array.isArray(data.likes)) {
|
||||||
for (const item of data.likes) {
|
for (const item of data.likes) {
|
||||||
const fields = ['page_slug', 'user_id', 'created_at'];
|
const fields = ['page_slug', 'user_id', 'created_at', 'site_id'];
|
||||||
const values = [item.page_slug, item.user_id, item.created_at];
|
const values = [item.page_slug, item.user_id, item.created_at, item.site_id || ""];
|
||||||
if (item.id) {
|
if (item.id) {
|
||||||
fields.unshift('id');
|
fields.unshift('id');
|
||||||
values.unshift(item.id);
|
values.unshift(item.id);
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref, watch } from "vue";
|
import { onMounted, onBeforeUnmount, ref, watch } from "vue";
|
||||||
import { useData } from "vitepress";
|
import { useData } from "vitepress";
|
||||||
|
|
||||||
const commentsRoot = ref(null);
|
const commentsRoot = ref(null);
|
||||||
const commentsInstance = ref(null);
|
const commentsInstance = ref(null);
|
||||||
const { isDark } = useData();
|
const { isDark, page } = useData();
|
||||||
|
|
||||||
const getTheme = () => (isDark.value ? "dark" : "light");
|
const getTheme = () => (isDark.value ? "dark" : "light");
|
||||||
|
|
||||||
@@ -53,5 +53,21 @@ onMounted(async () => {
|
|||||||
},
|
},
|
||||||
{ immediate: false }
|
{ 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>
|
</script>
|
||||||
|
|||||||
@@ -109,7 +109,12 @@ POST /admin/comments/import
|
|||||||
说明:
|
说明:
|
||||||
|
|
||||||
- 若从 CWD 自身导出的评论数据进行恢复,可直接将 `/admin/comments/export` 接口导出的 JSON 原样提交到本接口;
|
- 若从 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` 字段且存在则会覆盖原有数据
|
||||||
|
|
||||||
**成功响应**
|
**成功响应**
|
||||||
|
|
||||||
|
|||||||
@@ -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`:
|
如果你有多个站点,可以为每个站点配置不同的 `siteId`:
|
||||||
|
|||||||
Reference in New Issue
Block a user