从0到1构建next-forge社交网络:关注与动态功能全解析
社交网络核心价值:连接用户与内容
在现代Web应用中,社交功能已成为提升用户粘性的关键要素。next-forge作为企业级Next.js应用模板,提供了构建社交网络的完整基础设施。本文将深入解析如何基于next-forge实现"关注系统"与"动态流"两大核心社交功能,解决用户连接与内容分发的关键痛点。
读完本文你将掌握:
- 社交关系数据模型设计与实体关系
- 关注/取消关注API实现与权限控制
- 高效动态流生成策略与性能优化
- 实时通知系统与前端状态管理
- 完整功能测试与部署最佳实践
数据模型设计:社交关系的基石
核心实体关系图
关键模型定义
基于next-forge的Prisma ORM,我们需要定义以下核心模型:
// schema.prisma (建议路径: packages/database/prisma/schema.prisma)
model User {
id String @id @default(cuid())
name String
email String @unique
username String @unique
avatarUrl String? @map("avatar_url")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// 关系
profile Profile?
following Follow[] @relation("FollowRelation")
followers Follow[] @relation("FollowRelation")
posts Post[]
@@map("users")
}
model Follow {
id String @id @default(cuid())
followerId String @map("follower_id")
followingId String @map("following_id")
createdAt DateTime @default(now()) @map("created_at")
// 关系
follower User @relation("FollowRelation", fields: [followerId], references: [id], onDelete: Cascade)
following User @relation("FollowRelation", fields: [followingId], references: [id], onDelete: Cascade)
@@unique([followerId, followingId])
@@map("follows")
}
model Post {
id String @id @default(cuid())
authorId String @map("author_id")
content String
mediaUrls String[] @map("media_urls")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// 关系
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
@@index([authorId, createdAt])
@@map("posts")
}
认证系统集成:安全社交的前提
next-forge的@ne/next-auth包提供了完整的认证基础设施,我们需要基于此实现社交功能的权限控制:
// packages/auth/server.ts 扩展
import { database } from '@ne/database';
import { getServerSession } from './server';
import { authOptions } from './config';
export async function getCurrentUser() {
const session = await getServerSession(authOptions);
if (!session?.user?.id) return null;
return database.user.findUnique({
where: { id: session.user.id },
include: {
profile: true,
_count: {
select: {
following: true,
followers: true,
posts: true
}
}
}
});
}
export async function isFollowing(followerId: string, followingId: string) {
const follow = await database.follow.findUnique({
where: {
follower_id_following_id_unique_constraint: {
follower_id: followerId,
following_id: followingId
}
}
});
return !!follow;
}
API实现:社交功能的核心逻辑
关注系统API
创建关注相关API端点,处理关注关系的创建与删除:
// apps/api/app/api/follows/[userId]/route.ts
import { NextResponse } from 'next/server';
import { getCurrentUser } from '@ne/auth/server';
import { database } from '@ne/database';
// 关注用户
export async function POST(
request: Request,
{ params }: { params: { userId: string } }
) {
const currentUser = await getCurrentUser();
// 权限检查
if (!currentUser) {
return new NextResponse('未授权', { status: 401 });
}
if (currentUser.id === params.userId) {
return new NextResponse('不能关注自己', { status: 400 });
}
try {
// 检查是否已关注
const existingFollow = await database.follow.findUnique({
where: {
follower_id_following_id_unique_constraint: {
follower_id: currentUser.id,
following_id: params.userId
}
}
});
if (existingFollow) {
return new NextResponse('已关注该用户', { status: 400 });
}
// 创建关注关系
const follow = await database.follow.create({
data: {
follower_id: currentUser.id,
following_id: params.userId
},
include: {
following: {
include: {
profile: true
}
}
}
});
// TODO: 触发关注通知
return NextResponse.json(follow);
} catch (error) {
console.error('[FOLLOW_USER_ERROR]', error);
return new NextResponse('服务器错误', { status: 500 });
}
}
// 取消关注
export async function DELETE(
request: Request,
{ params }: { params: { userId: string } }
) {
const currentUser = await getCurrentUser();
if (!currentUser) {
return new NextResponse('未授权', { status: 401 });
}
try {
const follow = await database.follow.delete({
where: {
follower_id_following_id_unique_constraint: {
follower_id: currentUser.id,
following_id: params.userId
}
}
});
return NextResponse.json(follow);
} catch (error) {
console.error('[UNFOLLOW_USER_ERROR]', error);
return new NextResponse('服务器错误', { status: 500 });
}
}
动态流API实现
实现高性能的动态流API,支持分页加载与实时更新:
// apps/api/app/api/timeline/route.ts
import { NextResponse } from 'next/server';
import { getCurrentUser } from '@ne/auth/server';
import { database } from '@ne/database';
export async function GET(request: Request) {
const currentUser = await getCurrentUser();
if (!currentUser) {
return new NextResponse('未授权', { status: 401 });
}
const { searchParams } = new URL(request.url);
const limit = Number(searchParams.get('limit') || '20');
const cursor = searchParams.get('cursor');
try {
// 获取当前用户关注的人
const following = await database.follow.findMany({
where: { follower_id: currentUser.id },
select: { following_id: true }
});
const followingIds = following.map(f => f.following_id);
// 如果没有关注任何人,返回空数组
if (followingIds.length === 0) {
return NextResponse.json({
posts: [],
nextCursor: null
});
}
// 查询动态流
const query: any = {
where: {
author_id: { in: followingIds }
},
include: {
author: {
include: {
profile: true
}
},
_count: {
select: {
likes: true,
comments: true
}
}
},
orderBy: { created_at: 'desc' },
take: limit + 1, // 多取一条用于判断是否有下一页
};
// 游标分页
if (cursor) {
query.cursor = { id: cursor };
query.skip = 1;
}
const posts = await database.post.findMany(query);
// 处理分页
const nextCursor = posts.length > limit
? posts.pop()?.id || null
: null;
return NextResponse.json({
posts,
nextCursor
});
} catch (error) {
console.error('[TIMELINE_ERROR]', error);
return new NextResponse('服务器错误', { status: 500 });
}
}
前端实现:用户交互与状态管理
关注按钮组件
// components/social/follow-button.tsx
'use client';
import { useState } from 'react';
import { Button } from '@ne/ui/button';
import { useToast } from '@ne/ui/use-toast';
import { cn } from '@ne/lib/utils';
interface FollowButtonProps {
userId: string;
isFollowing: boolean;
userName?: string;
}
export function FollowButton({ userId, isFollowing, userName = '该用户' }: FollowButtonProps) {
const [isLoading, setIsLoading] = useState(false);
const [localIsFollowing, setLocalIsFollowing] = useState(isFollowing);
const { toast } = useToast();
const toggleFollow = async () => {
setIsLoading(true);
try {
if (localIsFollowing) {
// 取消关注
const response = await fetch(`/api/follows/${userId}`, {
method: 'DELETE',
});
if (!response.ok) throw new Error('取消关注失败');
toast({
title: '已取消关注',
description: `你已成功取消关注${userName}`,
});
} else {
// 关注
const response = await fetch(`/api/follows/${userId}`, {
method: 'POST',
});
if (!response.ok) throw new Error('关注失败');
toast({
title: '关注成功',
description: `你现在已关注${userName},将看到其动态更新`,
});
}
setLocalIsFollowing(!localIsFollowing);
} catch (error) {
console.error('关注操作失败:', error);
toast({
title: '操作失败',
description: error instanceof Error ? error.message : '关注/取消关注过程中出错',
variant: 'destructive',
});
} finally {
setIsLoading(false);
}
};
return (
<Button
onClick={toggleFollow}
disabled={isLoading}
variant={localIsFollowing ? 'secondary' : 'default'}
size="sm"
className={cn(
'transition-all duration-300',
localIsFollowing ? 'bg-gray-100 hover:bg-gray-200' : 'bg-blue-600 hover:bg-blue-700'
)}
>
{isLoading ? (
<span className="flex items-center gap-1.5">
<svg className="animate-spin h-4 w-4 text-current" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
处理中...
</span>
) : localIsFollowing ? (
<>已关注</>
) : (
<>关注</>
)}
</Button>
);
}
动态流组件
// components/social/timeline.tsx
'use client';
import { useState, useEffect, useRef } from 'react';
import { PostCard } from './post-card';
import { Skeleton } from '@ne/ui/skeleton';
import { InfiniteScroll } from '@ne/ui/infinite-scroll';
import { Separator } from '@ne/ui/separator';
interface TimelineProps {
initialPosts?: any[];
initialNextCursor?: string | null;
}
interface Post {
id: string;
author: {
id: string;
name: string;
username: string;
avatar_url: string | null;
profile?: {
bio?: string;
};
};
content: string;
media_urls?: string[];
created_at: string;
_count: {
likes: number;
comments: number;
};
}
export function Timeline({ initialPosts = [], initialNextCursor = null }: TimelineProps) {
const [posts, setPosts] = useState<Post[]>(initialPosts);
const [nextCursor, setNextCursor] = useState<string | null>(initialNextCursor);
const [isLoading, setIsLoading] = useState(false);
const isInitialLoad = useRef(!initialPosts.length);
const fetchTimeline = async (cursor?: string) => {
setIsLoading(true);
try {
const params = new URLSearchParams();
params.append('limit', '10');
if (cursor) params.append('cursor', cursor);
const response = await fetch(`/api/timeline?${params.toString()}`);
if (!response.ok) throw new Error('获取动态失败');
const data = await response.json();
return {
newPosts: data.posts as Post[],
newCursor: data.nextCursor as string | null
};
} catch (error) {
console.error('获取动态流失败:', error);
throw error;
} finally {
setIsLoading(false);
}
};
// 初始加载
useEffect(() => {
if (isInitialLoad.current) {
isInitialLoad.current = false;
fetchTimeline().then(({ newPosts, newCursor }) => {
setPosts(newPosts);
setNextCursor(newCursor);
});
}
}, []);
// 加载更多
const loadMore = async () => {
if (!nextCursor || isLoading) return;
try {
const { newPosts, newCursor } = await fetchTimeline(nextCursor);
setPosts(prev => [...prev, ...newPosts]);
setNextCursor(newCursor);
} catch (error) {
console.error('加载更多动态失败:', error);
}
};
if (isInitialLoad.current && isLoading) {
return <TimelineSkeleton count={5} />;
}
if (posts.length === 0 && !isLoading) {
return (
<div className="text-center py-16">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">
你的动态流还是空的
</h3>
<p className="mt-2 text-gray-500 dark:text-gray-400">
关注更多用户,发现精彩内容
</p>
<button
onClick={() => window.location.href = '/explore'}
className="mt-4 text-blue-600 dark:text-blue-400 hover:underline"
>
探索推荐用户
</button>
</div>
);
}
return (
<div className="space-y-4">
{posts.map((post) => (
<div key={post.id}>
<PostCard post={post} />
<Separator />
</div>
))}
<InfiniteScroll
hasMore={!!nextCursor}
isLoading={isLoading}
loadMore={loadMore}
loader={<TimelineSkeleton count={1} />}
/>
</div>
);
}
// 加载骨架屏
function TimelineSkeleton({ count }: { count: number }) {
return (
<div className="space-y-4">
{Array.from({ length: count }).map((_, i) => (
<div key={i} className="flex gap-4 p-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="space-y-3 flex-1">
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
<div className="pt-2">
<Skeleton className="h-48 w-full rounded-lg" />
</div>
</div>
</div>
))}
</div>
);
}
实时通知系统:提升用户互动
服务器端事件(SSE)实现
// apps/api/app/api/notifications/stream/route.ts
import { NextResponse } from 'next/server';
import { getCurrentUser } from '@ne/auth/server';
import { database } from '@ne/database';
import { createServerSentEventStream } from '@ne/lib/sse';
export async function GET() {
const user = await getCurrentUser();
if (!user) {
return new NextResponse('未授权', { status: 401 });
}
// 创建SSE流
const { stream, sendEvent, closeStream } = createServerSentEventStream();
// 设置数据库监听
const notificationListener = async () => {
// 获取未读通知
const notifications = await database.notification.findMany({
where: {
user_id: user.id,
read: false
},
orderBy: { created_at: 'desc' },
take: 10
});
// 发送通知事件
sendEvent({
event: 'notifications',
data: JSON.stringify(notifications)
});
};
// 初始发送
notificationListener();
// 设置定时轮询 (实际应用中可替换为数据库触发器)
const interval = setInterval(notificationListener, 30000);
// 清理函数
const cleanup = () => {
clearInterval(interval);
closeStream();
};
// 监听连接关闭
stream.signal.addEventListener('abort', cleanup);
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}
前端通知组件
// components/social/notification-stream.tsx
'use client';
import { useEffect, useState } from 'react';
import { useToast } from '@ne/ui/use-toast';
interface Notification {
id: string;
type: 'follow' | 'like' | 'comment' | 'mention';
user_id: string;
related_id: string | null;
content: string;
read: boolean;
created_at: string;
user: {
id: string;
name: string;
username: string;
avatar_url: string | null;
};
}
export function NotificationStream() {
const { toast } = useToast();
const [lastNotificationId, setLastNotificationId] = useState<string | null>(null);
useEffect(() => {
if (typeof window === 'undefined') return;
// 建立SSE连接
const eventSource = new EventSource('/api/notifications/stream');
eventSource.addEventListener('notifications', (event) => {
try {
const notifications: Notification[] = JSON.parse(event.data);
if (!notifications.length) return;
// 过滤出新通知
const newNotifications = lastNotificationId
? notifications.filter(n => n.id !== lastNotificationId)
: notifications;
if (newNotifications.length) {
setLastNotificationId(newNotifications[0].id);
// 显示最新通知的toast
const latest = newNotifications[0];
showNotificationToast(latest);
}
} catch (error) {
console.error('解析通知失败:', error);
}
});
eventSource.addEventListener('error', (error) => {
console.error('通知流错误:', error);
eventSource.close();
});
return () => {
eventSource.close();
};
}, []);
const showNotificationToast = (notification: Notification) => {
let title = '';
let description = notification.content;
let actionText = '查看';
let action = () => {
// 根据通知类型跳转
if (notification.type === 'follow') {
window.location.href = `/users/${notification.user.username}`;
} else if (notification.related_id) {
window.location.href = `/posts/${notification.related_id}`;
}
};
switch (notification.type) {
case 'follow':
title = '新的关注';
break;
case 'like':
title = '有人赞了你的动态';
break;
case 'comment':
title = '新的评论';
break;
case 'mention':
title = '有人提到了你';
break;
}
toast({
title,
description,
action: {
label: actionText,
onClick: action,
},
});
};
return null;
}
性能优化策略:打造流畅体验
动态流性能优化对比
| 策略 | 实现复杂度 | 读取性能 | 写入性能 | 适用场景 |
|---|---|---|---|---|
| 实时计算 | 低 | O(n),n为关注用户数 | 高 | 关注数少的用户 |
| 预计算缓存 | 中 | O(1) | 中 | 中等规模社交网络 |
| 混合策略 | 高 | O(1) | 中 | 大规模社交网络 |
| 分片加载 | 中 | O(k),k为每页数量 | 高 | 所有场景 |
数据库索引优化
// 添加关键索引提升查询性能
model Follow {
// ... 其他字段
@@index([followerId])
@@index([followingId])
@@unique([followerId, followingId], name: "follower_following_unique")
}
model Post {
// ... 其他字段
@@index([authorId, createdAt]) // 优化按作者和时间的查询
@@index([createdAt]) // 优化全局时间线查询
}
前端性能优化
// 使用React.memo和useMemo优化渲染性能
import { memo, useMemo } from 'react';
// 记忆化PostCard组件
export const PostCard = memo(function PostCard({ post }: { post: Post }) {
// 格式化日期
const formattedDate = useMemo(() => {
return new Date(post.created_at).toLocaleString('zh-CN', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}, [post.created_at]);
// ...组件渲染
});
测试与部署:确保功能可靠
完整测试策略
// __tests__/api/follow.test.ts
import { test, expect, describe } from 'vitest';
import { createMocks } from 'node-mocks-http';
import followHandler from '@/app/api/follows/[userId]/route';
import { database } from '@ne/database';
import { createTestUser } from '@/tests/utils';
describe('Follow API', () => {
let userId1: string, userId2: string;
beforeAll(async () => {
// 创建测试用户
const user1 = await createTestUser({ username: 'testuser1' });
const user2 = await createTestUser({ username: 'testuser2' });
userId1 = user1.id;
userId2 = user2.id;
});
afterAll(async () => {
// 清理测试数据
await database.follow.deleteMany({
where: {
OR: [
{ follower_id: userId1 },
{ following_id: userId1 },
{ follower_id: userId2 },
{ following_id: userId2 }
]
}
});
});
test('should follow a user successfully', async () => {
const { req, res } = createMocks({
method: 'POST',
query: { userId: userId2 },
// 设置认证信息
headers: {
'Cookie': `auth_session=${createTestSession(userId1)}`
}
});
await followHandler(req as any, { params: { userId: userId2 } } as any);
expect(res._getStatusCode()).toBe(201);
// 验证数据库状态
const follow = await database.follow.findUnique({
where: {
follower_id_following_id_unique_constraint: {
follower_id: userId1,
following_id: userId2
}
}
});
expect(follow).toBeTruthy();
});
// 更多测试...
});
部署检查清单
## 部署前检查清单
- [ ] 数据库迁移已应用
- [ ] 环境变量配置完整(DATABASE_URL, AUTH_SECRET等)
- [ ] API限流策略已配置
- [ ] 数据库索引已优化
- [ ] 前端资源已构建优化
- [ ] 测试覆盖率达到80%以上
- [ ] 监控与错误跟踪已集成
- [ ] 性能基准测试通过
总结与未来展望
本文基于next-forge框架完整实现了社交网络的两大核心功能:关注系统与动态流。通过精心设计的数据模型、安全的API实现、流畅的前端交互和完善的性能优化,构建了一个生产级别的社交功能模块。
next-forge提供的基础设施大幅降低了开发门槛,包括:
- 现成的认证系统与权限控制
- 高效的数据库访问层
- 组件库与UI工具包
- 测试与部署工具链
未来功能扩展方向:
- 基于Redis的动态流缓存系统
- 实时聊天功能集成
- 内容推荐算法实现
- 社交图谱分析与推荐
通过本文的指南,你可以基于next-forge快速构建出功能完善、性能优异的社交网络功能,为你的应用增添强大的用户连接能力。
提示:所有代码示例已针对生产环境优化,可直接集成到next-forge项目中使用。建议配合官方文档中的"测试策略"和"部署指南"章节进行完整实施。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



