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 });
|
||||
}
|
||||
|
||||
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 }> {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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` 字段且存在则会覆盖原有数据
|
||||
|
||||
**成功响应**
|
||||
|
||||
|
||||
@@ -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`:
|
||||
|
||||
Reference in New Issue
Block a user