这是一个很棒的想法!我们将实现基于用户搜索和阅读历史的文章推荐功能,并添加通过邮件通知用户的功能。这将大大提升用户体验和参与度。让我们开始吧:

第二十八部分:文章推荐和用户通知系统

  1. 首先,我们需要跟踪用户的搜索和阅读历史。更新 src/db/schema.ts
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
 
// ... 保留现有的表定义
 
export const userActivities = sqliteTable("user_activities", {
  id: integer("id").primaryKey(),
  userId: integer("user_id").notNull().references(() => users.id),
  postId: integer("post_id").references(() => posts.id),
  activityType: text("activity_type").notNull(), // 'search' or 'read'
  content: text("content"), // search query or null for read
  createdAt: integer("created_at", { mode: "timestamp" }).notNull().default(sql`CURRENT_TIMESTAMP`),
});
 
export const userPreferences = sqliteTable("user_preferences", {
  id: integer("id").primaryKey(),
  userId: integer("user_id").notNull().references(() => users.id),
  emailNotifications: integer("email_notifications").notNull().default(1),
  email: text("email").notNull(),
});
  1. 创建新的迁移文件 src/db/migrations/0012_add_user_activities_and_preferences.ts
export async function up(db) {
  await db.run(sql`
    CREATE TABLE user_activities (
      id INTEGER PRIMARY KEY,
      user_id INTEGER NOT NULL,
      post_id INTEGER,
      activity_type TEXT NOT NULL,
      content TEXT,
      created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY (user_id) REFERENCES users(id),
      FOREIGN KEY (post_id) REFERENCES posts(id)
    );
  `);
 
  await db.run(sql`
    CREATE TABLE user_preferences (
      id INTEGER PRIMARY KEY,
      user_id INTEGER NOT NULL,
      email_notifications INTEGER NOT NULL DEFAULT 1,
      email TEXT NOT NULL,
      FOREIGN KEY (user_id) REFERENCES users(id)
    );
  `);
 
  // 为现有用户添加默认偏好设置
  await db.run(sql`
    INSERT INTO user_preferences (user_id, email)
    SELECT id, email FROM users;
  `);
}
 
export async function down(db) {
  await db.run(sql`DROP TABLE IF EXISTS user_activities;`);
  await db.run(sql`DROP TABLE IF EXISTS user_preferences;`);
}
  1. 运行迁移:
bun run migrate
  1. 更新 src/routes/posts.ts 文件,记录用户活动:
import { db } from "../db";
import { userActivities } from "../db/schema";
 
export const postRoutes = new Elysia()
  // ... 保留现有的路由
  .get("/posts/:id", async ({ params, session, render }) => {
    const post = await db.select().from(posts).where(eq(posts.id, parseInt(params.id))).get();
    if (!post) {
      return "Post not found";
    }
 
    if (session.user) {
      await db.insert(userActivities).values({
        userId: session.user.id,
        postId: post.id,
        activityType: "read",
      });
    }
 
    return render("posts/show", { post });
  })
  .get("/search", async ({ query, session, render }) => {
    const searchQuery = query.q as string;
    if (!searchQuery) {
      return render("search", { results: [], query: "" });
    }
 
    if (session.user) {
      await db.insert(userActivities).values({
        userId: session.user.id,
        activityType: "search",
        content: searchQuery,
      });
    }
 
    const results = await searchPosts(searchQuery);
    return render("search", { results, query: searchQuery });
  });
  1. 创建一个新文件 src/services/recommendations.ts 来处理推荐逻辑:
import { db } from "../db";
import { posts, userActivities } from "../db/schema";
import { sql, eq, desc } from "drizzle-orm";
 
export async function getRecommendedPosts(userId: number, limit = 5) {
  const userKeywords = await db
    .select({ content: userActivities.content })
    .from(userActivities)
    .where(eq(userActivities.userId, userId))
    .orderBy(desc(userActivities.createdAt))
    .limit(20)
    .all();
 
  const keywords = userKeywords
    .map(activity => activity.content)
    .filter(Boolean)
    .join(" ");
 
  if (!keywords) {
    return [];
  }
 
  return db
    .select({
      id: posts.id,
      title: posts.title,
      content: posts.content,
      rank: sql`bm25(posts_search)`.as("rank"),
    })
    .from(posts)
    .innerJoin(postsSearch, eq(posts.id, postsSearch.id))
    .where(sql`posts_search MATCH ${keywords}`)
    .orderBy(sql`rank`)
    .limit(limit)
    .all();
}
  1. 创建一个新文件 src/services/notifications.ts 来处理邮件通知:
import { createTransport } from "nodemailer";
 
const transporter = createTransport({
  host: "smtp.example.com",
  port: 587,
  secure: false,
  auth: {
    user: "your-email@example.com",
    pass: "your-password",
  },
});
 
export async function sendRecommendationEmail(to: string, recommendations: any[]) {
  const mailOptions = {
    from: '"Your Blog" <noreply@yourblog.com>',
    to,
    subject: "New Article Recommendations",
    html: `
      <h1>Here are some articles you might like:</h1>
      <ul>
        ${recommendations.map(post => `
          <li><a href="http://yourblog.com/posts/${post.id}">${post.title}</a></li>
        `).join('')}
      </ul>
    `,
  };
 
  await transporter.sendMail(mailOptions);
}
  1. 创建一个新文件 src/jobs/sendRecommendations.ts 来定期发送推荐:
import { db } from "../db";
import { users, userPreferences } from "../db/schema";
import { getRecommendedPosts } from "../services/recommendations";
import { sendRecommendationEmail } from "../services/notifications";
import { eq } from "drizzle-orm";
 
export async function sendRecommendations() {
  const usersWithPreferences = await db
    .select({
      id: users.id,
      email: userPreferences.email,
    })
    .from(users)
    .innerJoin(userPreferences, eq(users.id, userPreferences.userId))
    .where(eq(userPreferences.emailNotifications, 1))
    .all();
 
  for (const user of usersWithPreferences) {
    const recommendations = await getRecommendedPosts(user.id);
    if (recommendations.length > 0) {
      await sendRecommendationEmail(user.email, recommendations);
    }
  }
}
  1. 更新 src/index.ts 文件,添加定时任务:
import { Elysia } from "elysia";
import { cron } from "@elysiajs/cron";
import { sendRecommendations } from "./jobs/sendRecommendations";
 
const app = new Elysia()
  // ... 其他中间件和路由
  .use(cron({
    name: "send-recommendations",
    pattern: "0 9 * * *", // 每天早上9点运行
    run: sendRecommendations,
  }))
  .listen(3000);
 
console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`);
  1. 更新 src/routes/users.ts 文件,添加用户偏好设置路由:
import { db } from "../db";
import { userPreferences } from "../db/schema";
 
export const userRoutes = new Elysia()
  // ... 保留现有的路由
  .get("/preferences", async ({ session, render }) => {
    if (!session.user) {
      return "Unauthorized";
    }
 
    const preferences = await db
      .select()
      .from(userPreferences)
      .where(eq(userPreferences.userId, session.user.id))
      .get();
 
    return render("users/preferences", { preferences });
  })
  .post("/preferences", async ({ session, body, set }) => {
    if (!session.user) {
      return "Unauthorized";
    }
 
    const { emailNotifications } = body;
 
    await db.update(userPreferences)
      .set({ emailNotifications: emailNotifications ? 1 : 0 })
      .where(eq(userPreferences.userId, session.user.id));
 
    set.redirect = "/preferences";
  });
  1. 创建一个新的视图文件 src/views/users/preferences.eta
<% layout('../layouts/main.eta', { title: 'User Preferences' }) %>
 
<h1>User Preferences</h1>
 
<form action="/preferences" method="POST">
  <label>
    <input type="checkbox" name="emailNotifications" value="1" <%= it.preferences.emailNotifications ? 'checked' : '' %>>
    Receive email notifications for article recommendations
  </label>
  <button type="submit">Save Preferences</button>
</form>

这些更改实现了以下功能:

  1. 跟踪用户的搜索和阅读历史。
  2. 基于用户活动生成个性化文章推荐。
  3. 通过电子邮件向用户发送推荐文章。
  4. 允许用户设置是否接收推荐邮件。

主要改进包括:

  • 提高了用户参与度,通过推荐相关内容保持用户兴趣。
  • 实现了个性化的用户体验。
  • 添加了电子邮件通知系统,以保持与用户的互动。
  • 给予用户控制权,允许他们选择是否接收推荐。

这个实现为博客系统添加了智能推荐和通知功能,大大提升了用户体验和参与度。用户可以发现更多感兴趣的内容,而博客也能保持与用户的持续互动。

接下来,我们可以考虑以下方向来进一步完善系统:

  1. 实现文章的协作编辑功能。
  2. 添加文章版本控制系统。
  3. 实现社交功能,如用户评论和文章分享。
  4. 添加数据分析和报告功能,以了解用户行为和内容表现。

你希望继续哪个方向,或者有其他想法吗?