包含完整的项目代码、最佳实践和常见问题的解决方案
清晰的项目目录结构,遵循 NestJS 最佳实践
im-system/
├── src/
│ ├── auth/
│ │ ├── auth.controller.ts
│ │ ├── auth.service.ts
│ │ ├── auth.module.ts
│ │ ├── dto/
│ │ │ ├── login.dto.ts
│ │ │ └── register.dto.ts
│ │ ├── guards/
│ │ │ └── jwt-auth.guard.ts
│ │ └── strategies/
│ │ └── jwt.strategy.ts
│ ├── users/
│ │ ├── users.controller.ts
│ │ ├── users.service.ts
│ │ ├── users.module.ts
│ │ └── dto/
│ │ ├── create-user.dto.ts
│ │ └── update-user.dto.ts
│ ├── chat/
│ │ ├── chat.gateway.ts
│ │ ├── chat.service.ts
│ │ ├── chat.module.ts
│ │ └── dto/
│ │ └── create-message.dto.ts
│ ├── prisma/
│ │ ├── prisma.service.ts
│ │ └── prisma.module.ts
│ ├── common/
│ │ ├── filters/
│ │ │ └── http-exception.filter.ts
│ │ ├── interceptors/
│ │ │ └── transform.interceptor.ts
│ │ └── decorators/
│ │ └── current-user.decorator.ts
│ ├── app.module.ts
│ └── main.ts
├── prisma/
│ └── schema.prisma
├── test/
├── docker-compose.yml
├── Dockerfile
├── package.json
├── tsconfig.json
└── README.md
按功能模块组织代码
每个功能模块包含控制器、服务、模块和相关的 DTO、守卫等,保持代码的高内聚和低耦合。
Prisma ORM 集成
使用 Prisma 作为数据库工具包,提供类型安全的数据库访问和自动迁移功能。
JWT + Passport 策略
基于 JWT 的无状态认证系统,结合 Passport 策略实现灵活的身份验证。
关键功能的完整代码实现和最佳实践
完整的用户认证系统,包括注册、登录、JWT 令牌生成和验证功能。
// auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from '../prisma/prisma.service';
import * as bcrypt from 'bcrypt';
import { LoginDto, RegisterDto } from './dto';
@Injectable()
export class AuthService {
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
) {}
async validateUser(email: string, password: string): Promise {
const user = await this.prisma.user.findUnique({
where: { email },
});
if (user && await bcrypt.compare(password, user.password)) {
const { password, ...result } = user;
return result;
}
return null;
}
async login(loginDto: LoginDto) {
const user = await this.validateUser(loginDto.email, loginDto.password);
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
const payload = { email: user.email, sub: user.id };
return {
access_token: this.jwtService.sign(payload),
user: {
id: user.id,
email: user.email,
username: user.username,
},
};
}
async register(registerDto: RegisterDto) {
const hashedPassword = await bcrypt.hash(registerDto.password, 10);
const user = await this.prisma.user.create({
data: {
email: registerDto.email,
username: registerDto.username,
password: hashedPassword,
},
});
const { password, ...result } = user;
return result;
}
}
实时消息处理网关,支持房间管理、消息广播和用户状态同步。
// chat/chat.gateway.ts
import {
WebSocketGateway,
WebSocketServer,
SubscribeMessage,
OnGatewayInit,
OnGatewayConnection,
OnGatewayDisconnect,
MessageBody,
ConnectedSocket,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { ChatService } from './chat.service';
import { CreateMessageDto } from './dto/create-message.dto';
@WebSocketGateway({
cors: {
origin: '*',
credentials: true,
},
})
export class ChatGateway implements
OnGatewayInit,
OnGatewayConnection,
OnGatewayDisconnect {
@WebSocketServer() server: Server;
constructor(private readonly chatService: ChatService) {}
afterInit(server: Server) {
console.log('WebSocket server initialized');
}
handleConnection(client: Socket) {
console.log(`Client connected: ${client.id}`);
// 可以在这里进行用户认证和房间加入
}
handleDisconnect(client: Socket) {
console.log(`Client disconnected: ${client.id}`);
// 清理用户状态和房间信息
}
@SubscribeMessage('joinRoom')
async handleJoinRoom(
@MessageBody() payload: { room: string; userId: string },
@ConnectedSocket() client: Socket,
) {
client.join(payload.room);
// 获取房间历史消息
const messages = await this.chatService.getMessages(payload.room, 50);
client.emit('joined', {
room: payload.room,
messages: messages.reverse(),
});
// 通知其他用户有新成员加入
client.to(payload.room).emit('userJoined', {
userId: payload.userId,
message: `${payload.userId} joined the room`,
});
}
@SubscribeMessage('leaveRoom')
handleLeaveRoom(
@MessageBody() payload: { room: string; userId: string },
@ConnectedSocket() client: Socket,
) {
client.leave(payload.room);
client.to(payload.room).emit('userLeft', {
userId: payload.userId,
message: `${payload.userId} left the room`,
});
}
@SubscribeMessage('message')
async handleMessage(
@MessageBody() payload: CreateMessageDto,
@ConnectedSocket() client: Socket,
) {
// 保存消息到数据库
const message = await this.chatService.createMessage(payload);
// 广播消息给房间内的所有用户
this.server.to(payload.room).emit('message', {
...message,
status: 'delivered',
});
}
@SubscribeMessage('typing')
handleTyping(
@MessageBody() payload: { room: string; userId: string; isTyping: boolean },
@ConnectedSocket() client: Socket,
) {
client.to(payload.room).emit('typing', {
userId: payload.userId,
isTyping: payload.isTyping,
});
}
@SubscribeMessage('markRead')
handleMarkRead(
@MessageBody() payload: { messageIds: string[]; room: string },
@ConnectedSocket() client: Socket,
) {
// 更新消息已读状态
this.chatService.markMessagesAsRead(payload.messageIds);
// 通知发送者消息已读
this.server.to(payload.room).emit('messagesRead', {
messageIds: payload.messageIds,
readerId: client.id,
});
}
}
消息存储和查询服务,支持消息历史、状态管理和文件处理。
// chat/chat.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateMessageDto } from './dto/create-message.dto';
@Injectable()
export class ChatService {
constructor(private prisma: PrismaService) {}
async createMessage(data: CreateMessageDto) {
return this.prisma.message.create({
data: {
content: data.content,
type: data.type || 'TEXT',
room: data.room,
senderId: data.senderId,
receiverId: data.receiverId,
fileUrl: data.fileUrl,
},
include: {
sender: {
select: {
id: true,
username: true,
avatar: true,
status: true,
},
},
receiver: {
select: {
id: true,
username: true,
avatar: true,
},
},
},
});
}
async getMessages(room: string, limit: number = 50, offset: number = 0) {
return this.prisma.message.findMany({
where: { room },
orderBy: { createdAt: 'desc' },
skip: offset,
take: limit,
include: {
sender: {
select: {
id: true,
username: true,
avatar: true,
status: true,
},
},
receiver: {
select: {
id: true,
username: true,
avatar: true,
},
},
},
});
}
async getPrivateMessages(userId1: string, userId2: string, limit: number = 50) {
return this.prisma.message.findMany({
where: {
OR: [
{ senderId: userId1, receiverId: userId2 },
{ senderId: userId2, receiverId: userId1 },
],
},
orderBy: { createdAt: 'desc' },
take: limit,
include: {
sender: {
select: {
id: true,
username: true,
avatar: true,
},
},
receiver: {
select: {
id: true,
username: true,
avatar: true,
},
},
},
});
}
async markMessagesAsRead(messageIds: string[]) {
return this.prisma.message.updateMany({
where: {
id: { in: messageIds },
},
data: {
status: 'read',
},
});
}
async deleteMessage(messageId: string, userId: string) {
const message = await this.prisma.message.findUnique({
where: { id: messageId },
});
if (!message || message.senderId !== userId) {
throw new Error('Message not found or unauthorized');
}
return this.prisma.message.delete({
where: { id: messageId },
});
}
async getUnreadCount(userId: string, room?: string) {
const where: any = {
receiverId: userId,
status: 'sent',
};
if (room) {
where.room = room;
}
return this.prisma.message.count({
where,
});
}
async searchMessages(query: string, room: string, limit: number = 20) {
return this.prisma.message.findMany({
where: {
room,
content: {
contains: query,
mode: 'insensitive',
},
},
orderBy: { createdAt: 'desc' },
take: limit,
include: {
sender: {
select: {
id: true,
username: true,
avatar: true,
},
},
},
});
}
}
完整的数据库模型定义,包含用户、消息、群组等核心实体。
// prisma/schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
username String @unique
password String
avatar String?
status UserStatus @default(ONLINE)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lastSeen DateTime @default(now())
// Relations
sentMessages Message[] @relation("SentMessages")
receivedMessages Message[] @relation("ReceivedMessages")
groupMembers GroupMember[]
ownedGroups Group[] @relation("GroupOwner")
fileUploads File[] @relation("FileUploader")
@@map("users")
}
model Message {
id String @id @default(cuid())
content String
type MessageType @default(TEXT)
room String
senderId String
receiverId String?
fileUrl String?
status MessageStatus @default(SENT)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
sender User @relation("SentMessages", fields: [senderId], references: [id])
receiver User? @relation("ReceivedMessages", fields: [receiverId], references: [id])
file File? @relation(fields: [fileUrl], references: [url])
@@map("messages")
}
model Group {
id String @id @default(cuid())
name String
description String?
type GroupType @default(PUBLIC)
avatar String?
creatorId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
creator User @relation("GroupOwner", fields: [creatorId], references: [id])
members GroupMember[]
@@map("groups")
}
model GroupMember {
id String @id @default(cuid())
userId String
groupId String
role GroupRole @default(MEMBER)
joinedAt DateTime @default(now())
// Relations
user User @relation(fields: [userId], references: [id])
group Group @relation(fields: [groupId], references: [id])
@@unique([userId, groupId])
@@map("group_members")
}
model File {
id String @id @default(cuid())
originalName String
fileName String @unique
mimeType String
size Int
url String @unique
type FileType @default(GENERAL)
uploaderId String
createdAt DateTime @default(now())
// Relations
uploader User @relation("FileUploader", fields: [uploaderId], references: [id])
messages Message[]
@@map("files")
}
// Enums
enum UserStatus {
ONLINE
OFFLINE
AWAY
BUSY
}
enum MessageType {
TEXT
IMAGE
VIDEO
FILE
AUDIO
}
enum MessageStatus {
SENT
DELIVERED
READ
}
enum GroupType {
PUBLIC
PRIVATE
}
enum GroupRole {
ADMIN
MODERATOR
MEMBER
}
enum FileType {
AVATAR
MESSAGE
GROUP_AVATAR
GENERAL
}
体验实时聊天功能,测试 WebSocket 连接和消息同步
开发过程中的经验和建议