实现统计功能是一个很好的想法,可以帮助你了解博客的受欢迎程度和用户的兴趣。我们将实现以下功能:
- 文章访问量统计
- 用户兴趣度分析
- 生成统计图表
让我们逐步实现这些功能:
- 更新数据库模式
在 src/db/schema.ts 中添加访问记录表:
export const pageViews = sqliteTable('page_views', {
id: integer('id').primaryKey(),
postId: integer('post_id').notNull().references(() => posts.id),
visitorId: text('visitor_id').notNull(),
timestamp: integer('timestamp', { mode: 'timestamp' }).notNull().default(sql`CURRENT_TIMESTAMP`),
});
export const userInterests = sqliteTable('user_interests', {
id: integer('id').primaryKey(),
visitorId: text('visitor_id').notNull(),
tagId: integer('tag_id').notNull().references(() => tags.id),
score: integer('score').notNull().default(1),
});- 实现访问量统计
在 src/index.ts 中添加以下代码:
import { nanoid } from 'nanoid';
import { pageViews, userInterests } from './db/schema';
// ... 其他导入和代码
// 中间件:为每个访客生成唯一ID
app.derive(({ cookie }) => ({
getVisitorId: () => {
let visitorId = cookie.visitorId;
if (!visitorId) {
visitorId = nanoid();
setCookie('visitorId', visitorId, {
httpOnly: true,
maxAge: 365 * 24 * 60 * 60, // 1 year
});
}
return visitorId;
}
}));
// 更新获取文章的路由
app.get('/api/post/:id', async ({ params, getVisitorId }) => {
const post = await db.select().from(posts).where(eq(posts.id, parseInt(params.id))).get();
if (!post) {
return new Response('Post not found', { status: 404 });
}
const visitorId = getVisitorId();
// 记录页面访问
await db.insert(pageViews).values({ postId: post.id, visitorId }).run();
// 获取文章标签
const postTags = await db.select()
.from(postTags)
.innerJoin(tags, eq(postTags.tagId, tags.id))
.where(eq(postTags.postId, post.id))
.all();
// 更新用户兴趣度
for (const pt of postTags) {
await db.insert(userInterests)
.values({ visitorId, tagId: pt.tags.id })
.onConflict(['visitor_id', 'tag_id'])
.merge({ score: sql`${userInterests.score} + 1` })
.run();
}
return {
...post,
tags: postTags.map(pt => pt.tags.name),
};
});
// 获取文章访问量
app.get('/api/stats/views', async () => {
const viewStats = await db.select({
postId: pageViews.postId,
title: posts.title,
views: sql<number>`count(${pageViews.id})`.as('views'),
})
.from(pageViews)
.innerJoin(posts, eq(pageViews.postId, posts.id))
.groupBy(pageViews.postId)
.orderBy(sql`views DESC`)
.limit(10)
.all();
return viewStats;
});
// 获取用户兴趣度统计
app.get('/api/stats/interests', async () => {
const interestStats = await db.select({
tagId: userInterests.tagId,
name: tags.name,
score: sql<number>`sum(${userInterests.score})`.as('score'),
})
.from(userInterests)
.innerJoin(tags, eq(userInterests.tagId, tags.id))
.groupBy(userInterests.tagId)
.orderBy(sql`score DESC`)
.limit(20)
.all();
return interestStats;
});- 创建统计页面组件
创建一个新文件 src/components/Stats.tsx:
import React, { useState, useEffect } from 'react';
import { Bar } from 'react-chartjs-2';
import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js';
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
interface ViewStat {
postId: number;
title: string;
views: number;
}
interface InterestStat {
tagId: number;
name: string;
score: number;
}
export const Stats: React.FC = () => {
const [viewStats, setViewStats] = useState<ViewStat[]>([]);
const [interestStats, setInterestStats] = useState<InterestStat[]>([]);
useEffect(() => {
fetch('/api/stats/views')
.then(res => res.json())
.then(setViewStats);
fetch('/api/stats/interests')
.then(res => res.json())
.then(setInterestStats);
}, []);
const viewsChartData = {
labels: viewStats.map(stat => stat.title),
datasets: [
{
label: 'Page Views',
data: viewStats.map(stat => stat.views),
backgroundColor: 'rgba(75, 192, 192, 0.6)',
},
],
};
const interestsChartData = {
labels: interestStats.map(stat => stat.name),
datasets: [
{
label: 'Interest Score',
data: interestStats.map(stat => stat.score),
backgroundColor: 'rgba(153, 102, 255, 0.6)',
},
],
};
const chartOptions = {
responsive: true,
plugins: {
legend: {
position: 'top' as const,
},
title: {
display: true,
text: 'Blog Statistics',
},
},
};
return (
<div>
<h2>Top 10 Most Viewed Posts</h2>
<Bar data={viewsChartData} options={chartOptions} />
<h2>User Interests</h2>
<Bar data={interestsChartData} options={chartOptions} />
</div>
);
};- 更新
src/index.ts以添加统计页面路由
import { Stats } from './components/Stats';
// ... 其他代码
app.get('/stats', ({ html }) => {
const statsHtml = renderToString(<Stats />);
return html(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blog Statistics</title>
<link rel="stylesheet" href="/public/styles.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<div id="root">${statsHtml}</div>
</body>
</html>
`);
});- 在主页添加统计页面链接
更新 src/components/Blog.tsx:
// ... 其他导入和代码
export const Blog: React.FC<BlogProps> = ({ posts, user }) => {
// ... 其他代码
return (
<div>
<h1>My Blog</h1>
<nav>
<a href="/">Home</a> |
<a href="/stats">Statistics</a>
</nav>
{/* ... 其他JSX */}
</div>
);
};- 添加一些 CSS 样式
在 src/public/styles.css 中添加以下样式:
canvas {
max-width: 800px;
margin: 20px auto;
}
nav {
margin-bottom: 20px;
}
nav a {
margin-right: 10px;
text-decoration: none;
color: #333;
}
nav a:hover {
text-decoration: underline;
}- 运行数据库迁移:
bun run generate
bun run push这个实现添加了以下功能:
- 文章访问量统计:记录每篇文章的访问次数。
- 用户兴趣度分析:根据用户浏览的文章标签,计算用户对不同主题的兴趣度。
- 统计图表:使用 Chart. Js 生成柱状图,展示最受欢迎的文章和用户兴趣分布。
这个实现还可以进一步改进,例如:
- 添加时间范围筛选,允许查看特定时间段内的统计数据。
- 实现更复杂的兴趣度计算算法,考虑阅读时间、点赞等因素。
- 添加更多类型的图表,如饼图、折线图等,以不同方式展示数据。
- 实现实时统计更新,使用 WebSocket 或轮询来动态更新图表。
- 添加导出功能,允许将统计数据导出为 CSV 或 Excel 格式。
- 实现更细粒度的访问量统计,如按小时或天统计。
- 添加地理位置分析,展示访客来源地分布。
- 实现 A/B 测试功能,比较不同版本文章的受欢迎程度。
记住,在收集和处理用户数据时,要注意遵守相关的隐私法规,如 GDPR。考虑添加隐私政策,并允许用户选择是否参与数据收集。