feat(analytics): 添加访问统计功能
实现页面访问统计功能,包括: 1. 前端组件挂载时上报访问数据 2. 后端接口记录访问信息并存储 3. 管理后台新增访问统计视图 4. 相关API文档和设计说明更新
This commit is contained in:
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);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user