feat(analytics): 添加访问统计功能
实现页面访问统计功能,包括: 1. 前端组件挂载时上报访问数据 2. 后端接口记录访问信息并存储 3. 管理后台新增访问统计视图 4. 相关API文档和设计说明更新
This commit is contained in:
@@ -83,6 +83,23 @@ export type CommentStatsResponse = {
|
||||
}[];
|
||||
};
|
||||
|
||||
export type VisitOverviewResponse = {
|
||||
totalPv: number;
|
||||
totalPages: number;
|
||||
};
|
||||
|
||||
export type VisitPageItem = {
|
||||
postSlug: string;
|
||||
postTitle: string | null;
|
||||
postUrl: string | null;
|
||||
pv: number;
|
||||
lastVisitAt: string | null;
|
||||
};
|
||||
|
||||
export type VisitPagesResponse = {
|
||||
items: VisitPageItem[];
|
||||
};
|
||||
|
||||
export async function loginAdmin(name: string, password: string): Promise<string> {
|
||||
const res = await post<AdminLoginResponse>('/admin/login', { name, password });
|
||||
const key = res.data.key;
|
||||
@@ -212,3 +229,11 @@ export function importComments(data: any[]): Promise<{ message: string }> {
|
||||
export function fetchCommentStats(): Promise<CommentStatsResponse> {
|
||||
return get<CommentStatsResponse>('/admin/stats/comments');
|
||||
}
|
||||
|
||||
export function fetchVisitOverview(): Promise<VisitOverviewResponse> {
|
||||
return get<VisitOverviewResponse>('/admin/analytics/overview');
|
||||
}
|
||||
|
||||
export function fetchVisitPages(): Promise<VisitPagesResponse> {
|
||||
return get<VisitPagesResponse>('/admin/analytics/pages');
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import CommentsView from '../views/CommentsView.vue';
|
||||
import SettingsView from '../views/SettingsView.vue';
|
||||
import DataView from '../views/DataView.vue';
|
||||
import StatsView from '../views/StatsView.vue';
|
||||
import AnalyticsVisitView from '../views/AnalyticsVisitView.vue';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
@@ -30,6 +31,11 @@ const routes: RouteRecordRaw[] = [
|
||||
name: 'stats',
|
||||
component: StatsView
|
||||
},
|
||||
{
|
||||
path: 'analytics',
|
||||
name: 'analytics',
|
||||
component: AnalyticsVisitView
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'settings',
|
||||
|
||||
266
cwd-admin/src/views/AnalyticsVisitView.vue
Normal file
266
cwd-admin/src/views/AnalyticsVisitView.vue
Normal file
@@ -0,0 +1,266 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2 class="page-title">访问统计</h2>
|
||||
<div
|
||||
v-if="toastVisible"
|
||||
class="toast"
|
||||
:class="toastType === 'error' ? 'toast-error' : 'toast-success'"
|
||||
>
|
||||
{{ toastMessage }}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3 class="card-title">整体概览</h3>
|
||||
<div v-if="loading" class="page-hint">加载中...</div>
|
||||
<div v-else-if="error" class="page-error">{{ error }}</div>
|
||||
<div v-else>
|
||||
<div class="stats-grid">
|
||||
<div class="stats-item">
|
||||
<div class="stats-label">全站总访问量</div>
|
||||
<div class="stats-value">{{ overview.totalPv }}</div>
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<div class="stats-label">有访问记录的页面数</div>
|
||||
<div class="stats-value">{{ overview.totalPages }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3 class="card-title">页面访问明细(按 PV 排序)</h3>
|
||||
<div v-if="loading" class="page-hint">加载中...</div>
|
||||
<div v-else-if="error" class="page-error">{{ error }}</div>
|
||||
<div v-else-if="items.length === 0" class="page-hint">暂无访问数据</div>
|
||||
<div v-else class="domain-table">
|
||||
<div class="domain-table-header">
|
||||
<div class="domain-cell domain-cell-domain">页面标题</div>
|
||||
<div class="domain-cell">访问量</div>
|
||||
<div class="domain-cell">最后访问时间</div>
|
||||
<div class="domain-cell">页面地址</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="item in items"
|
||||
:key="item.postSlug"
|
||||
class="domain-table-row"
|
||||
>
|
||||
<div class="domain-cell domain-cell-domain">
|
||||
{{ item.postTitle || item.postSlug }}
|
||||
</div>
|
||||
<div class="domain-cell">
|
||||
{{ item.pv }}
|
||||
</div>
|
||||
<div class="domain-cell">
|
||||
{{ formatTime(item.lastVisitAt) }}
|
||||
</div>
|
||||
<div class="domain-cell">
|
||||
<a
|
||||
v-if="item.postUrl"
|
||||
:href="item.postUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{{ item.postUrl }}
|
||||
</a>
|
||||
<span v-else>-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import {
|
||||
fetchVisitOverview,
|
||||
fetchVisitPages,
|
||||
type VisitOverviewResponse,
|
||||
type VisitPageItem,
|
||||
} from "../api/admin";
|
||||
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
const overview = ref<VisitOverviewResponse>({
|
||||
totalPv: 0,
|
||||
totalPages: 0,
|
||||
});
|
||||
const items = ref<VisitPageItem[]>([]);
|
||||
|
||||
const toastMessage = ref("");
|
||||
const toastType = ref<"success" | "error">("success");
|
||||
const toastVisible = ref(false);
|
||||
|
||||
function showToast(msg: string, type: "success" | "error" = "success") {
|
||||
toastMessage.value = msg;
|
||||
toastType.value = type;
|
||||
toastVisible.value = true;
|
||||
window.setTimeout(() => {
|
||||
toastVisible.value = false;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function formatTime(value: string | null): string {
|
||||
if (!value) {
|
||||
return "-";
|
||||
}
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return value;
|
||||
}
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const d = String(date.getDate()).padStart(2, "0");
|
||||
const h = String(date.getHours()).padStart(2, "0");
|
||||
const mm = String(date.getMinutes()).padStart(2, "0");
|
||||
return `${y}-${m}-${d} ${h}:${mm}`;
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
const [overviewRes, pagesRes] = await Promise.all([
|
||||
fetchVisitOverview(),
|
||||
fetchVisitPages(),
|
||||
]);
|
||||
overview.value = overviewRes;
|
||||
items.value = pagesRes.items || [];
|
||||
} catch (e: any) {
|
||||
const msg = e.message || "加载访问统计数据失败";
|
||||
error.value = msg;
|
||||
showToast(msg, "error");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: #24292f;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #ffffff;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #d0d7de;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.page-hint {
|
||||
font-size: 14px;
|
||||
color: #57606a;
|
||||
}
|
||||
|
||||
.page-error {
|
||||
font-size: 14px;
|
||||
color: #d1242f;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stats-item {
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
background-color: #f6f8fa;
|
||||
border: 1px solid #d0d7de;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
font-size: 14px;
|
||||
color: #57606a;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
color: #24292f;
|
||||
}
|
||||
|
||||
.domain-table {
|
||||
border: 1px solid #d0d7de;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.domain-table-header {
|
||||
display: flex;
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
|
||||
.domain-table-row {
|
||||
display: flex;
|
||||
border-top: 1px solid #eaeae0;
|
||||
}
|
||||
|
||||
.domain-cell {
|
||||
flex: 1;
|
||||
padding: 10px 10px;
|
||||
font-size: 14px;
|
||||
color: #24292f;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.domain-cell a {
|
||||
color: #57606a;
|
||||
}
|
||||
|
||||
.domain-cell-domain {
|
||||
flex: 2;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
min-width: 220px;
|
||||
max-width: 320px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
box-shadow: 0 8px 24px rgba(140, 149, 159, 0.2);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.toast-success {
|
||||
background-color: #1a7f37;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
background-color: #d1242f;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -75,6 +75,13 @@
|
||||
>
|
||||
数据看板
|
||||
</li>
|
||||
<li
|
||||
class="menu-item"
|
||||
:class="{ active: isRouteActive('analytics') }"
|
||||
@click="goAnalytics"
|
||||
>
|
||||
访问统计
|
||||
</li>
|
||||
<li
|
||||
class="menu-item"
|
||||
:class="{ active: isRouteActive('settings') }"
|
||||
@@ -140,6 +147,11 @@ function goStats() {
|
||||
closeSider();
|
||||
}
|
||||
|
||||
function goAnalytics() {
|
||||
router.push({ name: "analytics" });
|
||||
closeSider();
|
||||
}
|
||||
|
||||
function goData() {
|
||||
router.push({ name: "data" });
|
||||
closeSider();
|
||||
|
||||
@@ -169,6 +169,10 @@ export class CWDComments {
|
||||
|
||||
this._render();
|
||||
this.store.loadComments();
|
||||
|
||||
if (this.api && typeof this.api.trackVisit === 'function') {
|
||||
this.api.trackVisit();
|
||||
}
|
||||
})();
|
||||
|
||||
this._mounted = true;
|
||||
|
||||
@@ -97,9 +97,27 @@
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function trackVisit() {
|
||||
try {
|
||||
await fetch(`${baseUrl}/api/analytics/visit`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
postSlug: config.postSlug,
|
||||
postTitle: config.postTitle,
|
||||
postUrl: config.postUrl
|
||||
})
|
||||
});
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
fetchComments,
|
||||
submitComment,
|
||||
verifyAdminKey
|
||||
verifyAdminKey,
|
||||
trackVisit
|
||||
};
|
||||
}
|
||||
|
||||
78
cwd-api/src/api/admin/visitAnalytics.ts
Normal file
78
cwd-api/src/api/admin/visitAnalytics.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { Context } from 'hono';
|
||||
import type { Bindings } from '../../bindings';
|
||||
|
||||
type VisitOverview = {
|
||||
totalPv: number;
|
||||
totalPages: number;
|
||||
};
|
||||
|
||||
type VisitPageItem = {
|
||||
postSlug: string;
|
||||
postTitle: string | null;
|
||||
postUrl: string | null;
|
||||
pv: number;
|
||||
lastVisitAt: string | null;
|
||||
};
|
||||
|
||||
export const getVisitOverview = async (
|
||||
c: Context<{ Bindings: Bindings }>
|
||||
) => {
|
||||
try {
|
||||
await c.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 TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)'
|
||||
).run();
|
||||
|
||||
const row = await c.env.CWD_DB.prepare(
|
||||
'SELECT COUNT(*) as totalPages, COALESCE(SUM(pv), 0) as totalPv FROM page_stats'
|
||||
).first<{
|
||||
totalPages: number | null;
|
||||
totalPv: number | null;
|
||||
}>();
|
||||
|
||||
const data: VisitOverview = {
|
||||
totalPv: row?.totalPv || 0,
|
||||
totalPages: row?.totalPages || 0
|
||||
};
|
||||
|
||||
return c.json(data);
|
||||
} catch (e: any) {
|
||||
return c.json(
|
||||
{ message: e.message || '获取访问统计概览失败' },
|
||||
500
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const getVisitPages = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
try {
|
||||
await c.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 TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)'
|
||||
).run();
|
||||
|
||||
const { results } = await c.env.CWD_DB.prepare(
|
||||
'SELECT post_slug, post_title, post_url, pv, last_visit_at FROM page_stats ORDER BY pv DESC, last_visit_at DESC'
|
||||
).all<{
|
||||
post_slug: string;
|
||||
post_title: string | null;
|
||||
post_url: string | null;
|
||||
pv: number;
|
||||
last_visit_at: string | null;
|
||||
}>();
|
||||
|
||||
const items: VisitPageItem[] = results.map((row) => ({
|
||||
postSlug: row.post_slug,
|
||||
postTitle: row.post_title,
|
||||
postUrl: row.post_url,
|
||||
pv: row.pv || 0,
|
||||
lastVisitAt: row.last_visit_at
|
||||
}));
|
||||
|
||||
return c.json({ items });
|
||||
} catch (e: any) {
|
||||
return c.json(
|
||||
{ message: e.message || '获取页面访问统计失败' },
|
||||
500
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
69
cwd-api/src/api/public/trackVisit.ts
Normal file
69
cwd-api/src/api/public/trackVisit.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { Context } from 'hono';
|
||||
import type { Bindings } from '../../bindings';
|
||||
|
||||
type TrackVisitBody = {
|
||||
postSlug?: string;
|
||||
postTitle?: string;
|
||||
postUrl?: string;
|
||||
};
|
||||
|
||||
export const trackVisit = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
try {
|
||||
const body = (await c.req.json().catch(() => ({}))) as TrackVisitBody;
|
||||
|
||||
const rawPostSlug = typeof body.postSlug === 'string' ? body.postSlug.trim() : '';
|
||||
const rawPostTitle = typeof body.postTitle === 'string' ? body.postTitle.trim() : '';
|
||||
const rawPostUrl = typeof body.postUrl === 'string' ? body.postUrl.trim() : '';
|
||||
|
||||
if (!rawPostSlug) {
|
||||
return c.json({ message: 'postSlug is required' }, 400);
|
||||
}
|
||||
|
||||
await c.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 TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)'
|
||||
).run();
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const existing = await c.env.CWD_DB.prepare(
|
||||
'SELECT id, pv FROM page_stats WHERE post_slug = ?'
|
||||
)
|
||||
.bind(rawPostSlug)
|
||||
.first<{ id: number; pv: number }>();
|
||||
|
||||
if (!existing) {
|
||||
await c.env.CWD_DB.prepare(
|
||||
'INSERT INTO page_stats (post_slug, post_title, post_url, pv, last_visit_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||
)
|
||||
.bind(
|
||||
rawPostSlug,
|
||||
rawPostTitle || null,
|
||||
rawPostUrl || null,
|
||||
1,
|
||||
now,
|
||||
now,
|
||||
now
|
||||
)
|
||||
.run();
|
||||
} else {
|
||||
const newPv = (existing.pv || 0) + 1;
|
||||
await c.env.CWD_DB.prepare(
|
||||
'UPDATE page_stats SET post_title = ?, post_url = ?, pv = ?, last_visit_at = ?, updated_at = ? WHERE id = ?'
|
||||
)
|
||||
.bind(
|
||||
rawPostTitle || null,
|
||||
rawPostUrl || null,
|
||||
newPv,
|
||||
now,
|
||||
now,
|
||||
existing.id
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
return c.json({ success: true });
|
||||
} catch (e: any) {
|
||||
return c.json({ message: e.message || '记录访问数据失败' }, 500);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -23,6 +23,8 @@ import { getAdminEmail } from './api/admin/getAdminEmail';
|
||||
import { setAdminEmail } from './api/admin/setAdminEmail';
|
||||
import { testEmail } from './api/admin/testEmail';
|
||||
import { getStats } from './api/admin/getStats';
|
||||
import { trackVisit } from './api/public/trackVisit';
|
||||
import { getVisitOverview, getVisitPages } from './api/admin/visitAnalytics';
|
||||
|
||||
const app = new Hono<{ Bindings: Bindings }>();
|
||||
const VERSION = `v${packageJson.version}`;
|
||||
@@ -214,6 +216,7 @@ app.get('/', (c) => {
|
||||
app.get('/api/comments', getComments);
|
||||
app.post('/api/comments', postComment);
|
||||
app.post('/api/verify-admin', verifyAdminKey);
|
||||
app.post('/api/analytics/visit', trackVisit);
|
||||
app.get('/api/config/comments', async (c) => {
|
||||
try {
|
||||
const settings = await loadCommentSettings(c.env);
|
||||
@@ -240,6 +243,8 @@ app.post('/admin/comments/import', importComments);
|
||||
app.put('/admin/comments/status', updateStatus);
|
||||
app.put('/admin/comments/update', updateComment);
|
||||
app.get('/admin/stats/comments', getStats);
|
||||
app.get('/admin/analytics/overview', getVisitOverview);
|
||||
app.get('/admin/analytics/pages', getVisitPages);
|
||||
app.get('/admin/settings/email', getAdminEmail);
|
||||
app.put('/admin/settings/email', setAdminEmail);
|
||||
app.get('/admin/settings/email-notify', async (c) => {
|
||||
|
||||
@@ -10,7 +10,9 @@ Authorization: Bearer <token>
|
||||
|
||||
Token 通过登录接口获取,有效期为 24 小时。
|
||||
|
||||
## 管理员登录
|
||||
## 1. 身份认证相关
|
||||
|
||||
### 1.1 管理员登录
|
||||
|
||||
```
|
||||
POST /admin/login
|
||||
@@ -93,7 +95,9 @@ POST /admin/login
|
||||
}
|
||||
```
|
||||
|
||||
## 获取评论列表
|
||||
## 2. 评论管理相关
|
||||
|
||||
### 2.1 获取评论列表
|
||||
|
||||
```
|
||||
GET /admin/comments/list
|
||||
@@ -135,6 +139,7 @@ GET /admin/comments/list
|
||||
"contentText": "很棒的文章!",
|
||||
"contentHtml": "很棒的文章!",
|
||||
"status": "approved",
|
||||
"priority": 2,
|
||||
"ua": "Mozilla/5.0 ...",
|
||||
"avatar": "https://gravatar.com/avatar/..."
|
||||
}
|
||||
@@ -158,7 +163,7 @@ GET /admin/comments/list
|
||||
}
|
||||
```
|
||||
|
||||
## 更新评论状态
|
||||
### 2.2 更新评论状态
|
||||
|
||||
```
|
||||
PUT /admin/comments/status
|
||||
@@ -166,6 +171,8 @@ PUT /admin/comments/status
|
||||
|
||||
更新评论状态(例如通过 / 拒绝)。
|
||||
|
||||
**注意**:此接口仅用于更新评论状态,如需修改评论内容、置顶权重等,请使用 `/admin/comments/update` 接口。
|
||||
|
||||
- 方法:`PUT`
|
||||
- 路径:`/admin/comments/status`
|
||||
- 鉴权:需要(Bearer Token)
|
||||
@@ -212,7 +219,106 @@ PUT /admin/comments/status
|
||||
}
|
||||
```
|
||||
|
||||
## 删除指定评论
|
||||
### 2.3 更新评论内容
|
||||
|
||||
```
|
||||
PUT /admin/comments/update
|
||||
```
|
||||
|
||||
更新评论的详细信息,包括昵称、邮箱、网址、评论地址、内容、状态和置顶权重等。
|
||||
|
||||
- 方法:`PUT`
|
||||
- 路径:`/admin/comments/update`
|
||||
- 鉴权:需要(Bearer Token)
|
||||
|
||||
**请求头**
|
||||
|
||||
| 名称 | 必填 | 示例 |
|
||||
| -------------- | ---- | ------------------ |
|
||||
| `Content-Type` | 是 | `application/json` |
|
||||
|
||||
**请求体**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "张三",
|
||||
"email": "zhangsan@example.com",
|
||||
"url": "https://zhangsan.me",
|
||||
"postSlug": "https://example.com/blog/hello-world",
|
||||
"contentText": "更新后的评论内容",
|
||||
"status": "approved",
|
||||
"priority": 2
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------ | ---- | -------------------------------------- |
|
||||
| `id` | number | 是 | 评论 ID |
|
||||
| `name` | string | 是 | 评论者昵称 |
|
||||
| `email` | string | 是 | 评论者邮箱 |
|
||||
| `url` | string | 否 | 评论者个人主页或站点地址 |
|
||||
| `postSlug` | string | 否 | 评论地址(文章 URL),不传则保持原值 |
|
||||
| `contentText` | string | 是 | 评论内容(纯文本) |
|
||||
| `status` | string | 否 | 评论状态,可选 `approved`、`pending`、`rejected` |
|
||||
| `priority` | number | 否 | 置顶权重,默认为 1,数值越大越靠前 |
|
||||
|
||||
**置顶权重说明**:
|
||||
|
||||
- `1`:不置顶(默认值)
|
||||
- `2` 或更高:数值越大,该评论在列表中排序越靠前
|
||||
- 公开 API 获取评论时,会按照 `priority DESC` 进行排序
|
||||
|
||||
**成功响应**
|
||||
|
||||
- 状态码:`200`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Comment updated, id: 1."
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**
|
||||
|
||||
- ID 无效:
|
||||
- 状态码:`400` 或 `404`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Missing or invalid id"
|
||||
}
|
||||
```
|
||||
|
||||
或
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Comment not found"
|
||||
}
|
||||
```
|
||||
|
||||
- 必填字段为空:
|
||||
- 状态码:`400`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "昵称不能为空"
|
||||
}
|
||||
```
|
||||
|
||||
- 更新失败:
|
||||
- 状态码:`500`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Update failed"
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 删除指定评论
|
||||
|
||||
```
|
||||
DELETE /admin/comments/delete
|
||||
@@ -260,7 +366,142 @@ DELETE /admin/comments/delete
|
||||
}
|
||||
```
|
||||
|
||||
## 导出所有评论数据
|
||||
### 2.5 将指定 IP 加入评论黑名单
|
||||
|
||||
```
|
||||
POST /admin/comments/block-ip
|
||||
```
|
||||
|
||||
通过接口将指定 IP 地址加入评论黑名单,后续该 IP 提交评论将被拒绝。
|
||||
|
||||
- 方法:`POST`
|
||||
- 路径:`/admin/comments/block-ip`
|
||||
- 鉴权:需要(Bearer Token)
|
||||
|
||||
**请求头**
|
||||
|
||||
| 名称 | 必填 | 示例 |
|
||||
| -------------- | ---- | ------------------ |
|
||||
| `Content-Type` | 是 | `application/json` |
|
||||
|
||||
**请求体**
|
||||
|
||||
```json
|
||||
{
|
||||
"ip": "1.1.1.1"
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ------ | ------ | ---- | ------------------------------------------------ |
|
||||
| `ip` | string | 是 | 要加入黑名单的 IP 地址,多个 IP 用逗号或换行分隔 |
|
||||
|
||||
**成功响应**
|
||||
|
||||
- 状态码:`200`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "已加入 IP 黑名单"
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**
|
||||
|
||||
- IP 为空:
|
||||
- 状态码:`400`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "IP 地址不能为空"
|
||||
}
|
||||
```
|
||||
|
||||
- 内部错误:
|
||||
- 状态码:`500`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "操作失败"
|
||||
}
|
||||
```
|
||||
|
||||
### 2.6 将指定邮箱加入评论黑名单
|
||||
|
||||
```
|
||||
POST /admin/comments/block-email
|
||||
```
|
||||
|
||||
通过接口将指定邮箱地址加入评论黑名单,后续该邮箱提交评论将被拒绝。
|
||||
|
||||
- 方法:`POST`
|
||||
- 路径:`/admin/comments/block-email`
|
||||
- 鉴权:需要(Bearer Token)
|
||||
|
||||
**请求头**
|
||||
|
||||
| 名称 | 必填 | 示例 |
|
||||
| -------------- | ---- | ------------------ |
|
||||
| `Content-Type` | 是 | `application/json` |
|
||||
|
||||
**请求体**
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "spam@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ------- | ------ | ---- | ------------------------------------------------ |
|
||||
| `email` | string | 是 | 要加入黑名单的邮箱地址,多个邮箱用逗号或换行分隔 |
|
||||
|
||||
**成功响应**
|
||||
|
||||
- 状态码:`200`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "已加入邮箱黑名单"
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**
|
||||
|
||||
- 邮箱为空:
|
||||
- 状态码:`400`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "邮箱不能为空"
|
||||
}
|
||||
```
|
||||
|
||||
- 邮箱格式不正确:
|
||||
- 状态码:`400`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "邮箱格式不正确"
|
||||
}
|
||||
```
|
||||
|
||||
- 内部错误:
|
||||
- 状态码:`500`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "操作失败"
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 评论数据导入导出
|
||||
|
||||
### 3.1 导出所有评论数据
|
||||
|
||||
```
|
||||
GET /admin/comments/export
|
||||
@@ -309,7 +550,7 @@ GET /admin/comments/export
|
||||
}
|
||||
```
|
||||
|
||||
## 导入评论数据
|
||||
### 3.2 导入评论数据
|
||||
|
||||
```
|
||||
POST /admin/comments/import
|
||||
@@ -377,95 +618,9 @@ POST /admin/comments/import
|
||||
}
|
||||
```
|
||||
|
||||
## 获取当前通知邮箱配置
|
||||
## 4. 评论设置相关
|
||||
|
||||
```
|
||||
GET /admin/settings/email
|
||||
```
|
||||
|
||||
获取当前通知邮箱配置。
|
||||
|
||||
- 方法:`GET`
|
||||
- 路径:`/admin/settings/email`
|
||||
- 鉴权:需要(Bearer Token)
|
||||
|
||||
**成功响应**
|
||||
|
||||
- 状态码:`200`
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "admin@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**
|
||||
|
||||
- 状态码:`500`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "错误信息"
|
||||
}
|
||||
```
|
||||
|
||||
## 设置通知邮箱
|
||||
|
||||
```
|
||||
PUT /admin/settings/email
|
||||
```
|
||||
|
||||
设置通知邮箱,用于接收新评论提醒。
|
||||
|
||||
- 方法:`PUT`
|
||||
- 路径:`/admin/settings/email`
|
||||
- 鉴权:需要(Bearer Token)
|
||||
|
||||
**请求头**
|
||||
|
||||
| 名称 | 必填 | 示例 |
|
||||
| -------------- | ---- | ------------------ |
|
||||
| `Content-Type` | 是 | `application/json` |
|
||||
|
||||
**请求体**
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "admin@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
**成功响应**
|
||||
|
||||
- 状态码:`200`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "保存成功"
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**
|
||||
|
||||
- 邮箱格式不正确:
|
||||
- 状态码:`400`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "邮箱格式不正确"
|
||||
}
|
||||
```
|
||||
|
||||
- 服务器错误:
|
||||
- 状态码:`500`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "错误信息"
|
||||
}
|
||||
```
|
||||
|
||||
## 获取评论配置
|
||||
### 4.1 获取评论配置
|
||||
|
||||
```
|
||||
GET /admin/settings/comments
|
||||
@@ -500,13 +655,13 @@ GET /admin/settings/comments
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
| ---------------- | --------------- | --------------------------------------------------------- |
|
||||
| `adminEmail` | string | 博主邮箱地址,用于显示“博主”标识以及管理员身份验证 |
|
||||
| `adminEmail` | string | 博主邮箱地址,用于显示"博主"标识以及管理员身份验证 |
|
||||
| `adminBadge` | string | 博主标识文字,例如 `"博主"` |
|
||||
| `avatarPrefix` | string | 头像地址前缀,如 Gravatar 或 Cravatar 镜像地址 |
|
||||
| `adminEnabled` | boolean | 是否启用博主标识相关展示 |
|
||||
| `allowedDomains` | Array\<string\> | 允许调用组件的域名列表,留空则不限制 |
|
||||
| `adminKey` | string\|null | 管理员评论密钥(明文),仅通过管理后台接口返回 |
|
||||
| `adminKeySet` | boolean | 是否已经设置过管理员评论密钥 |
|
||||
| `adminKeySet` | boolean | 是否已经设置过管理员管理员评论密钥 |
|
||||
| `requireReview` | boolean | 是否开启新评论先审核再显示(true 表示新评论默认为待审核) |
|
||||
| `blockedIps` | Array\<string\> | IP 黑名单列表,匹配到的 IP 提交评论将被拒绝 |
|
||||
| `blockedEmails` | Array\<string\> | 邮箱黑名单列表,匹配到的邮箱提交评论将被拒绝 |
|
||||
@@ -521,7 +676,7 @@ GET /admin/settings/comments
|
||||
}
|
||||
```
|
||||
|
||||
## 更新评论配置
|
||||
### 4.2 更新评论配置
|
||||
|
||||
```
|
||||
PUT /admin/settings/comments
|
||||
@@ -599,16 +754,51 @@ PUT /admin/settings/comments
|
||||
}
|
||||
```
|
||||
|
||||
## 将指定 IP 加入评论黑名单
|
||||
### 4.3 获取通知邮箱配置(已废弃)
|
||||
|
||||
```
|
||||
POST /admin/comments/block-ip
|
||||
GET /admin/settings/email
|
||||
```
|
||||
|
||||
通过接口将指定 IP 地址加入评论黑名单,后续该 IP 提交评论将被拒绝。
|
||||
获取当前通知邮箱配置。
|
||||
|
||||
- 方法:`POST`
|
||||
- 路径:`/admin/comments/block-ip`
|
||||
> [!NOTE]
|
||||
> 此接口已被 `/admin/settings/email-notify` 替代,建议使用新接口获取完整的邮件通知配置。
|
||||
|
||||
- 方法:`GET`
|
||||
- 路径:`/admin/settings/email`
|
||||
- 鉴权:需要(Bearer Token)
|
||||
|
||||
**成功响应**
|
||||
|
||||
- 状态码:`200`
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "admin@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**
|
||||
|
||||
- 状态码:`500`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "错误信息"
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 设置通知邮箱(已废弃)
|
||||
|
||||
```
|
||||
PUT /admin/settings/email
|
||||
```
|
||||
|
||||
设置通知邮箱,用于接收新评论提醒。
|
||||
|
||||
- 方法:`PUT`
|
||||
- 路径:`/admin/settings/email`
|
||||
- 鉴权:需要(Bearer Token)
|
||||
|
||||
**请求头**
|
||||
@@ -621,99 +811,22 @@ POST /admin/comments/block-ip
|
||||
|
||||
```json
|
||||
{
|
||||
"ip": "1.1.1.1"
|
||||
"email": "admin@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ------ | ------ | ---- | ------------------------------------------------ |
|
||||
| `ip` | string | 是 | 要加入黑名单的 IP 地址,多个 IP 用逗号或换行分隔 |
|
||||
|
||||
**成功响应**
|
||||
|
||||
- 状态码:`200`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "已加入 IP 黑名单"
|
||||
"message": "保存成功"
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**
|
||||
|
||||
- IP 为空:
|
||||
- 状态码:`400`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "IP 地址不能为空"
|
||||
}
|
||||
```
|
||||
|
||||
- 内部错误:
|
||||
- 状态码:`500`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "操作失败"
|
||||
}
|
||||
```
|
||||
|
||||
## 将指定邮箱加入评论黑名单
|
||||
|
||||
```
|
||||
POST /admin/comments/block-email
|
||||
```
|
||||
|
||||
通过接口将指定邮箱地址加入评论黑名单,后续该邮箱提交评论将被拒绝。
|
||||
|
||||
- 方法:`POST`
|
||||
- 路径:`/admin/comments/block-email`
|
||||
- 鉴权:需要(Bearer Token)
|
||||
|
||||
**请求头**
|
||||
|
||||
| 名称 | 必填 | 示例 |
|
||||
| -------------- | ---- | ------------------ |
|
||||
| `Content-Type` | 是 | `application/json` |
|
||||
|
||||
**请求体**
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "spam@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ------- | ------ | ---- | ------------------------------------------------ |
|
||||
| `email` | string | 是 | 要加入黑名单的邮箱地址,多个邮箱用逗号或换行分隔 |
|
||||
|
||||
**成功响应**
|
||||
|
||||
- 状态码:`200`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "已加入邮箱黑名单"
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**
|
||||
|
||||
- 邮箱为空:
|
||||
- 状态码:`400`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "邮箱不能为空"
|
||||
}
|
||||
```
|
||||
|
||||
- 邮箱格式不正确:
|
||||
- 状态码:`400`
|
||||
|
||||
@@ -723,16 +836,250 @@ POST /admin/comments/block-email
|
||||
}
|
||||
```
|
||||
|
||||
- 内部错误:
|
||||
- 服务器错误:
|
||||
- 状态码:`500`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "操作失败"
|
||||
"message": "错误信息"
|
||||
}
|
||||
```
|
||||
|
||||
## 获取评论统计数据(数据看板)
|
||||
## 5. 邮件通知配置相关
|
||||
|
||||
### 5.1 获取邮件通知配置
|
||||
|
||||
```
|
||||
GET /admin/settings/email-notify
|
||||
```
|
||||
|
||||
获取邮件通知配置,包括 SMTP 配置和邮件模板。
|
||||
|
||||
- 方法:`GET`
|
||||
- 路径:`/admin/settings/email-notify`
|
||||
- 鉴权:需要(Bearer Token)
|
||||
|
||||
**成功响应**
|
||||
|
||||
- 状态码:`200`
|
||||
|
||||
```json
|
||||
{
|
||||
"globalEnabled": true,
|
||||
"smtp": {
|
||||
"host": "smtp.qq.com",
|
||||
"port": 465,
|
||||
"user": "noreply@qq.com",
|
||||
"pass": "",
|
||||
"secure": true
|
||||
},
|
||||
"templates": {
|
||||
"reply": "回复评论的邮件模板 HTML",
|
||||
"admin": "新评论通知的邮件模板 HTML"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
| -------------- | ------- | ------------------------------------------------------------ |
|
||||
| `globalEnabled` | boolean | 是否全局启用邮件通知 |
|
||||
| `smtp.host` | string | SMTP 服务器地址,默认 `smtp.qq.com` |
|
||||
| `smtp.port` | number | SMTP 服务器端口,默认 `465` |
|
||||
| `smtp.user` | string | SMTP 用户名(发件邮箱) |
|
||||
| `smtp.pass` | string | SMTP 密码(脱敏显示,不返回完整密码) |
|
||||
| `smtp.secure` | boolean | 是否使用 SSL/TLS,默认 `true` |
|
||||
| `templates.reply` | string | 回复评论时的邮件模板,支持变量占位符 |
|
||||
| `templates.admin` | string | 新评论通知的邮件模板,支持变量占位符 |
|
||||
|
||||
**邮件模板变量**
|
||||
|
||||
回复评论模板(`templates.reply`)支持的变量:
|
||||
|
||||
| 变量名 | 说明 |
|
||||
| ----------------- | ------------ |
|
||||
| `${toEmail}` | 收件人邮箱 |
|
||||
| `${toName}` | 收件人昵称 |
|
||||
| `${postTitle}` | 文章标题 |
|
||||
| `${parentComment}` | 被回复的评论内容 |
|
||||
| `${replyAuthor}` | 回复者昵称 |
|
||||
| `${replyContent}` | 回复内容 |
|
||||
| `${postUrl}` | 文章链接 |
|
||||
|
||||
新评论通知模板(`templates.admin`)支持的变量:
|
||||
|
||||
| 变量名 | 说明 |
|
||||
| ----------------- | ------------ |
|
||||
| `${postTitle}` | 文章标题 |
|
||||
| `${postUrl}` | 文章链接 |
|
||||
| `${commentAuthor}` | 评论者昵称 |
|
||||
| `${commentContent}` | 评论内容 |
|
||||
|
||||
**错误响应**
|
||||
|
||||
- 状态码:`500`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "错误信息"
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 更新邮件通知配置
|
||||
|
||||
```
|
||||
PUT /admin/settings/email-notify
|
||||
```
|
||||
|
||||
更新邮件通知配置,包括 SMTP 配置和邮件模板。
|
||||
|
||||
- 方法:`PUT`
|
||||
- 路径:`/admin/settings/email-notify`
|
||||
- 鉴权:需要(Bearer Token)
|
||||
|
||||
**请求头**
|
||||
|
||||
| 名称 | 必填 | 示例 |
|
||||
| -------------- | ---- | ------------------ |
|
||||
| `Content-Type` | 是 | `application/json` |
|
||||
|
||||
**请求体**
|
||||
|
||||
```json
|
||||
{
|
||||
"globalEnabled": true,
|
||||
"smtp": {
|
||||
"host": "smtp.qq.com",
|
||||
"port": 465,
|
||||
"user": "noreply@qq.com",
|
||||
"pass": "your_password",
|
||||
"secure": true
|
||||
},
|
||||
"templates": {
|
||||
"reply": "<div>...</div>",
|
||||
"admin": "<div>...</div>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| -------------- | ------- | ---- | ------------------------------------------------------------ |
|
||||
| `globalEnabled` | boolean | 否 | 是否全局启用邮件通知 |
|
||||
| `smtp` | object | 否 | SMTP 配置对象 |
|
||||
| `smtp.host` | string | 否 | SMTP 服务器地址 |
|
||||
| `smtp.port` | number | 否 | SMTP 服务器端口 |
|
||||
| `smtp.user` | string | 否 | SMTP 用户名(发件邮箱) |
|
||||
| `smtp.pass` | string | 否 | SMTP 密码 |
|
||||
| `smtp.secure` | boolean | 否 | 是否使用 SSL/TLS |
|
||||
| `templates` | object | 否 | 邮件模板对象 |
|
||||
| `templates.reply` | string | 否 | 回复评论的邮件模板 HTML |
|
||||
| `templates.admin` | string | 否 | 新评论通知的邮件模板 HTML |
|
||||
|
||||
**成功响应**
|
||||
|
||||
- 状态码:`200`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "保存成功"
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**
|
||||
|
||||
- 状态码:`500`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "保存失败"
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 测试邮件发送
|
||||
|
||||
```
|
||||
POST /admin/settings/email-test
|
||||
```
|
||||
|
||||
测试邮件通知配置是否正确,发送一封测试邮件到指定邮箱。
|
||||
|
||||
- 方法:`POST`
|
||||
- 路径:`/admin/settings/email-test`
|
||||
- 鉴权:需要(Bearer Token)
|
||||
|
||||
**请求头**
|
||||
|
||||
| 名称 | 必填 | 示例 |
|
||||
| -------------- | ---- | ------------------ |
|
||||
| `Content-Type` | 是 | `application/json` |
|
||||
|
||||
**请求体**
|
||||
|
||||
```json
|
||||
{
|
||||
"toEmail": "test@example.com",
|
||||
"smtp": {
|
||||
"host": "smtp.qq.com",
|
||||
"port": 465,
|
||||
"user": "noreply@qq.com",
|
||||
"pass": "your_password",
|
||||
"secure": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------ | ---- | ------------------------------------ |
|
||||
| `toEmail` | string | 是 | 接收测试邮件的邮箱地址 |
|
||||
| `smtp` | object | 是 | SMTP 配置对象(与更新配置接口相同) |
|
||||
|
||||
**成功响应**
|
||||
|
||||
- 状态码:`200`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "邮件发送成功"
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**
|
||||
|
||||
- 接收邮箱无效:
|
||||
- 状态码:`400`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "请输入有效的接收邮箱"
|
||||
}
|
||||
```
|
||||
|
||||
- SMTP 配置不完整:
|
||||
- 状态码:`400`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "SMTP 配置不完整"
|
||||
}
|
||||
```
|
||||
|
||||
- 邮件发送失败:
|
||||
- 状态码:`500`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "邮件发送失败: 具体错误信息"
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 统计数据相关
|
||||
|
||||
### 6.1 获取评论统计数据(数据看板)
|
||||
|
||||
```
|
||||
GET /admin/stats/comments
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
# 公开 API
|
||||
|
||||
无需认证即可访问的公开接口,包括评论获取、评论提交以及评论设置获取。
|
||||
无需认证即可访问的公开接口,包括评论获取、评论提交、配置获取和身份验证。
|
||||
|
||||
包含路径、方法、参数、请求体和响应示例。
|
||||
|
||||
## 获取指定文章的评论列表
|
||||
## 1. 评论相关
|
||||
|
||||
### 1.1 获取指定文章的评论列表
|
||||
|
||||
```
|
||||
GET /api/comments
|
||||
```
|
||||
|
||||
获取指定文章的评论列表。
|
||||
获取指定文章的评论列表,支持分页和嵌套结构。
|
||||
|
||||
- 方法:`GET`
|
||||
- 路径:`/api/comments`
|
||||
@@ -42,6 +44,7 @@ GET /api/comments
|
||||
"pubDate": "2026-01-13T10:00:00Z",
|
||||
"postSlug": "/blog/hello-world",
|
||||
"avatar": "https://gravatar.com/avatar/...",
|
||||
"priority": 2,
|
||||
"replies": [
|
||||
{
|
||||
"id": 2,
|
||||
@@ -54,7 +57,8 @@ GET /api/comments
|
||||
"postSlug": "/blog/hello-world",
|
||||
"avatar": "https://gravatar.com/avatar/...",
|
||||
"parentId": 1,
|
||||
"replyToAuthor": "张三"
|
||||
"replyToAuthor": "张三",
|
||||
"priority": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -70,8 +74,9 @@ GET /api/comments
|
||||
|
||||
说明:
|
||||
|
||||
- 当 `nested=true`(默认)时,接口返回的是“根评论列表”,每条根评论包含其 `replies`。
|
||||
- 当 `nested=true`(默认)时,接口返回的是"根评论列表",每条根评论包含其 `replies`。
|
||||
- 当 `nested=false` 时,接口返回扁平列表,所有评论都在 `data` 中,`replies` 为空。
|
||||
- `priority` 字段:评论的置顶权重,数值越大排序越靠前。
|
||||
|
||||
**错误响应**
|
||||
|
||||
@@ -95,7 +100,7 @@ GET /api/comments
|
||||
}
|
||||
```
|
||||
|
||||
## 提交新评论或回复
|
||||
### 1.2 提交新评论或回复
|
||||
|
||||
```
|
||||
POST /api/comments
|
||||
@@ -124,11 +129,12 @@ POST /api/comments
|
||||
"email": "zhangsan@example.com",
|
||||
"url": "https://zhangsan.me",
|
||||
"content": "很棒的文章!",
|
||||
"parent_id": 1
|
||||
"parent_id": 1,
|
||||
"adminToken": "your-admin-key"
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明**
|
||||
字段说明:
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ------------ | ------ | ---- | ------------------------------------------------------------------------------------------------------------- |
|
||||
@@ -140,20 +146,31 @@ POST /api/comments
|
||||
| `url` | string | 否 | 评论者个人主页或站点地址 |
|
||||
| `content` | string | 是 | 评论内容,内部会过滤 `<script>...</script>` 片段 |
|
||||
| `parent_id` | number | 否 | 父评论 ID,用于回复功能;缺省或 `null` 表示根评论 |
|
||||
| `adminToken` | string | 否 | 管理员评论密钥,博主发布评论时需要先通过 `/api/verify-admin` 验证密钥后将密钥传入此字段,评论将直接通过且不受审核设置影响 |
|
||||
|
||||
**成功响应**
|
||||
|
||||
- 状态码:`200`
|
||||
|
||||
评论直接通过时:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Comment submitted. Awaiting moderation."
|
||||
"message": "评论已提交",
|
||||
"status": "approved"
|
||||
}
|
||||
```
|
||||
|
||||
当前实现中评论会直接以 `approved` 状态写入数据库,后续如引入人工审核可在实现中调整为“待审核”状态。
|
||||
评论进入待审核状态时(开启"先审核再显示"且非管理员评论):
|
||||
|
||||
**错误响应**与限流
|
||||
```json
|
||||
{
|
||||
"message": "已提交评论,待管理员审核后显示",
|
||||
"status": "pending"
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**
|
||||
|
||||
- 请求体缺失或字段类型错误:
|
||||
|
||||
@@ -165,7 +182,7 @@ POST /api/comments
|
||||
}
|
||||
```
|
||||
|
||||
- 缺少必填字段示例:
|
||||
- 缺少必填字段:
|
||||
|
||||
- `post_slug` 为空:
|
||||
|
||||
@@ -207,6 +224,56 @@ POST /api/comments
|
||||
}
|
||||
```
|
||||
|
||||
- IP 或邮箱被限制:
|
||||
|
||||
- IP 被限制:
|
||||
- 状态码:`403`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "当前 IP 已被限制评论,请联系站长进行处理"
|
||||
}
|
||||
```
|
||||
|
||||
- 邮箱被限制:
|
||||
- 状态码:`403`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "当前邮箱已被限制评论,请联系站长进行处理"
|
||||
}
|
||||
```
|
||||
|
||||
- 管理员评论验证失败:
|
||||
|
||||
- 未输入密钥:
|
||||
- 状态码:`401`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "请输入管理员密钥",
|
||||
"requireAuth": true
|
||||
}
|
||||
```
|
||||
|
||||
- 密钥错误:
|
||||
- 状态码:`401`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "密钥错误"
|
||||
}
|
||||
```
|
||||
|
||||
- 验证失败次数过多:
|
||||
- 状态码:`403`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "验证失败次数过多,请30分钟后再试"
|
||||
}
|
||||
```
|
||||
|
||||
- 评论频率限制:
|
||||
|
||||
- 状态码:`429`
|
||||
@@ -214,7 +281,7 @@ POST /api/comments
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "评论频繁,等 10s 后再试"
|
||||
"message": "评论频繁,等10s后再试"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -228,7 +295,9 @@ POST /api/comments
|
||||
}
|
||||
```
|
||||
|
||||
## 获取评论相关的公开配置
|
||||
## 2. 配置相关
|
||||
|
||||
### 2.1 获取评论相关的公开配置
|
||||
|
||||
```
|
||||
GET /api/config/comments
|
||||
@@ -248,7 +317,7 @@ GET /api/config/comments
|
||||
{
|
||||
"adminEmail": "admin@example.com",
|
||||
"adminBadge": "博主",
|
||||
"avatarPrefix": "https://gravatar.com/avatar",
|
||||
"avatar": "https://gravatar.com/avatar",
|
||||
"adminEnabled": true,
|
||||
"allowedDomains": [],
|
||||
"requireReview": false
|
||||
@@ -259,7 +328,7 @@ GET /api/config/comments
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
| -------------- | ------- | -------------------------------------------------------------------- |
|
||||
| `adminEmail` | string | 博主邮箱地址,用于在前端展示“博主”标识,并触发管理员身份验证流程 |
|
||||
| `adminEmail` | string | 博主邮箱地址,用于在前端展示"博主"标识,并触发管理员身份验证流程 |
|
||||
| `adminBadge` | string | 博主标识文字,例如 `"博主"` |
|
||||
| `avatarPrefix` | string | 头像地址前缀,如 Gravatar 或 Cravatar 镜像地址 |
|
||||
| `adminEnabled` | boolean | 是否启用博主标识相关展示(关闭时不显示徽标,但仍可作为管理员邮箱) |
|
||||
@@ -275,3 +344,80 @@ GET /api/config/comments
|
||||
"message": "加载评论配置失败"
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 身份验证相关
|
||||
|
||||
### 3.1 验证管理员密钥
|
||||
|
||||
```
|
||||
POST /api/verify-admin
|
||||
```
|
||||
|
||||
验证前台管理员评论所需的密钥,用于博主发布评论时的身份验证。
|
||||
|
||||
- 方法:`POST`
|
||||
- 路径:`/api/verify-admin`
|
||||
- 鉴权:不需要
|
||||
|
||||
**请求头**
|
||||
|
||||
| 名称 | 必填 | 示例 |
|
||||
| -------------- | ---- | ------------------ |
|
||||
| `Content-Type` | 是 | `application/json` |
|
||||
|
||||
**请求体**
|
||||
|
||||
```json
|
||||
{
|
||||
"adminToken": "your-admin-key"
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------ | ---- | -------------- |
|
||||
| `adminToken` | string | 是 | 管理员评论密钥 |
|
||||
|
||||
**风控说明**
|
||||
|
||||
- 同一 IP 连续验证失败 3 次后,该 IP 将被锁定 30 分钟
|
||||
- 失败次数记录有效期为 1 小时
|
||||
|
||||
**成功响应**
|
||||
|
||||
- 状态码:`200`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "验证通过"
|
||||
}
|
||||
```
|
||||
|
||||
或未设置密钥时:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "未设置管理员密钥"
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**
|
||||
|
||||
- 寽钥错误:
|
||||
- 状态码:`401`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "密钥错误"
|
||||
}
|
||||
```
|
||||
|
||||
- 验证失败次数过多:
|
||||
- 状态码:`403`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "验证失败次数过多,请30分钟后再试"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -18,6 +18,70 @@
|
||||
|
||||
开启是否显示博主标签,配置博主邮箱和标签文字,即可在评论中显示博主标签;关闭为不显示。
|
||||
|
||||
### 管理员评论密钥
|
||||
|
||||
设置管理员评论密钥后,博主在前台发布评论时需要输入此密钥进行身份验证,评论将被直接通过,不受"先审核再显示"设置的影响。
|
||||
|
||||
**注意**:
|
||||
- 同一 IP 连续验证失败 3 次后,该 IP 将被锁定 30 分钟
|
||||
- 失败次数记录有效期为 1 小时
|
||||
|
||||
### 评论置顶
|
||||
|
||||
在评论管理页面,点击"编辑"按钮可修改评论的置顶权重:
|
||||
|
||||
- `1`:不置顶(默认值)
|
||||
- `2` 或更高:数值越大,该评论在列表中排序越靠前
|
||||
|
||||
置顶后的评论会在前台评论列表中优先显示。
|
||||
|
||||
### 邮件通知配置
|
||||
|
||||
在设置页面可以配置邮件通知功能,包括:
|
||||
|
||||
#### 全局开关
|
||||
|
||||
控制是否启用邮件通知功能。
|
||||
|
||||
#### SMTP 配置
|
||||
|
||||
| 字段 | 说明 | 示例 |
|
||||
| ------- | -------------------------------- | ----------------- |
|
||||
| 主机 | SMTP 服务器地址 | `smtp.qq.com` |
|
||||
| 端口 | SMTP 服务器端口 | `465` |
|
||||
| 用户名 | 发件邮箱 | `noreply@qq.com` |
|
||||
| 密码 | 邮箱密码或授权码 | `your_password` |
|
||||
| 安全连接 | 是否使用 SSL/TLS | `true` |
|
||||
|
||||
#### 邮件模板
|
||||
|
||||
支持自定义 HTML 邮件模板,包含以下变量:
|
||||
|
||||
**回复评论模板变量**:
|
||||
|
||||
| 变量名 | 说明 |
|
||||
| ----------------- | ------------ |
|
||||
| `${toEmail}` | 收件人邮箱 |
|
||||
| `${toName}` | 收件人昵称 |
|
||||
| `${postTitle}` | 文章标题 |
|
||||
| `${parentComment}` | 被回复的评论内容 |
|
||||
| `${replyAuthor}` | 回复者昵称 |
|
||||
| `${replyContent}` | 回复内容 |
|
||||
| `${postUrl}` | 文章链接 |
|
||||
|
||||
**新评论通知模板变量**:
|
||||
|
||||
| 变量名 | 说明 |
|
||||
| ----------------- | ------------ |
|
||||
| `${postTitle}` | 文章标题 |
|
||||
| `${postUrl}` | 文章链接 |
|
||||
| `${commentAuthor}` | 评论者昵称 |
|
||||
| `${commentContent}` | 评论内容 |
|
||||
|
||||
#### 测试邮件
|
||||
|
||||
配置完成后,可以使用"测试邮件"功能发送一封测试邮件到指定邮箱,验证配置是否正确。
|
||||
|
||||
## 使用官方管理后台
|
||||
|
||||
使用官方提供的管理后台(最新版本)https://cwd.zishu.me
|
||||
|
||||
Reference in New Issue
Block a user