这是一个很棒的想法!我们将实现基于用户搜索和阅读历史的文章推荐功能,并添加通过邮件通知用户的功能。这将大大提升用户体验和参与度。让我们开始吧:
第二十八部分:文章推荐和用户通知系统
- 首先,我们需要跟踪用户的搜索和阅读历史。更新
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(),
});- 创建新的迁移文件
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;`);
}- 运行迁移:
bun run migrate- 更新
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 });
});- 创建一个新文件
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();
}- 创建一个新文件
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);
}- 创建一个新文件
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);
}
}
}- 更新
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}`);- 更新
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";
});- 创建一个新的视图文件
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>这些更改实现了以下功能:
- 跟踪用户的搜索和阅读历史。
- 基于用户活动生成个性化文章推荐。
- 通过电子邮件向用户发送推荐文章。
- 允许用户设置是否接收推荐邮件。
主要改进包括:
- 提高了用户参与度,通过推荐相关内容保持用户兴趣。
- 实现了个性化的用户体验。
- 添加了电子邮件通知系统,以保持与用户的互动。
- 给予用户控制权,允许他们选择是否接收推荐。
这个实现为博客系统添加了智能推荐和通知功能,大大提升了用户体验和参与度。用户可以发现更多感兴趣的内容,而博客也能保持与用户的持续互动。
接下来,我们可以考虑以下方向来进一步完善系统:
- 实现文章的协作编辑功能。
- 添加文章版本控制系统。
- 实现社交功能,如用户评论和文章分享。
- 添加数据分析和报告功能,以了解用户行为和内容表现。
你希望继续哪个方向,或者有其他想法吗?