实现统计功能是一个很好的想法,可以帮助你了解博客的受欢迎程度和用户的兴趣。我们将实现以下功能:

  1. 文章访问量统计
  2. 用户兴趣度分析
  3. 生成统计图表

让我们逐步实现这些功能:

  1. 更新数据库模式

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),
});
  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;
});
  1. 创建统计页面组件

创建一个新文件 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>
  );
};
  1. 更新 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>
  `);
});
  1. 在主页添加统计页面链接

更新 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>
  );
};
  1. 添加一些 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;
}
  1. 运行数据库迁移:
bun run generate
bun run push

这个实现添加了以下功能:

  1. 文章访问量统计:记录每篇文章的访问次数。
  2. 用户兴趣度分析:根据用户浏览的文章标签,计算用户对不同主题的兴趣度。
  3. 统计图表:使用 Chart. Js 生成柱状图,展示最受欢迎的文章和用户兴趣分布。

这个实现还可以进一步改进,例如:

  • 添加时间范围筛选,允许查看特定时间段内的统计数据。
  • 实现更复杂的兴趣度计算算法,考虑阅读时间、点赞等因素。
  • 添加更多类型的图表,如饼图、折线图等,以不同方式展示数据。
  • 实现实时统计更新,使用 WebSocket 或轮询来动态更新图表。
  • 添加导出功能,允许将统计数据导出为 CSV 或 Excel 格式。
  • 实现更细粒度的访问量统计,如按小时或天统计。
  • 添加地理位置分析,展示访客来源地分布。
  • 实现 A/B 测试功能,比较不同版本文章的受欢迎程度。

记住,在收集和处理用户数据时,要注意遵守相关的隐私法规,如 GDPR。考虑添加隐私政策,并允许用户选择是否参与数据收集。