optimize: sử dụng redis để cache cho các response từ server
This commit is contained in:
+167
-20
@@ -7,9 +7,11 @@ dotenv.config({ path: envPath });
|
||||
|
||||
import 'reflect-metadata';
|
||||
import * as fs from 'fs';
|
||||
import * as zlib from 'zlib';
|
||||
import { promisify } from 'util';
|
||||
import sharp from 'sharp';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { Module, Controller, Get, Post, Body, Param, Patch, Delete, ParseUUIDPipe, NotFoundException, BadRequestException, UnauthorizedException, UseGuards, Req, Query, ForbiddenException, Injectable, UseInterceptors, UploadedFiles } from '@nestjs/common';
|
||||
import { Module, Controller, Get, Post, Body, Param, Patch, Delete, ParseUUIDPipe, NotFoundException, BadRequestException, UnauthorizedException, UseGuards, Req, Query, ForbiddenException, Injectable, UseInterceptors, UploadedFiles, Inject } from '@nestjs/common';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { FilesInterceptor } from '@nestjs/platform-express';
|
||||
import { diskStorage } from 'multer';
|
||||
@@ -24,10 +26,26 @@ import { JwtAuthGuard } from './auth/jwt-auth.guard';
|
||||
import { JwtStrategy } from './auth/jwt.strategy';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { SetMetadata, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { CacheModule, CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { Cache } from 'cache-manager';
|
||||
import { redisStore } from 'cache-manager-redis-yet';
|
||||
import { HttpAdapterHost } from '@nestjs/core';
|
||||
import { CompressCacheInterceptor } from './common/compress-cache.interceptor';
|
||||
|
||||
// Promisify các hàm nén để sử dụng async/await
|
||||
const gzip = promisify(zlib.gzip);
|
||||
const gunzip = promisify(zlib.gunzip);
|
||||
|
||||
// Khai báo vị trí thư mục upload cụ thể
|
||||
const UPLOAD_ROOT = path.join(process.cwd(), 'uploads');
|
||||
|
||||
// Cấu hình TTL (mili giây) cho từng loại dữ liệu
|
||||
const CACHE_TTL = {
|
||||
DEFAULT: 600000, // 10 phút mặc định
|
||||
RESOURCE_TO_TOUR: 3600000, // 1 giờ cho ánh xạ tài nguyên -> tour
|
||||
USER_ROLE: 300000, // 5 phút cho quyền hạn người dùng
|
||||
};
|
||||
|
||||
async function bootstrap() {
|
||||
if (!process.env.DATABASE_URL) {
|
||||
throw new Error('❌ DATABASE_URL không tồn tại trong biến môi trường! Kiểm tra file .env tại: ' + envPath);
|
||||
@@ -66,7 +84,11 @@ export const Roles = (...roles: ParticipantRole[]) => SetMetadata(ROLES_KEY, rol
|
||||
// This guard checks if the user is a participant of the tour and has one of the required roles.
|
||||
@Injectable()
|
||||
export class TourRoleGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector, private prisma: PrismaService) {}
|
||||
constructor(
|
||||
private reflector: Reflector,
|
||||
private prisma: PrismaService,
|
||||
@Inject(CACHE_MANAGER) private cacheManager: Cache
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const requiredRoles = this.reflector.getAllAndOverride<ParticipantRole[]>(ROLES_KEY, [
|
||||
@@ -80,18 +102,80 @@ export class TourRoleGuard implements CanActivate {
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user = request.user; // User object from JwtAuthGuard
|
||||
// Kiểm tra cả 'tourId' và 'id' để tương thích với các route khác nhau
|
||||
const tourId = request.params.tourId || request.params.id;
|
||||
|
||||
let tourId = request.params.tourId;
|
||||
const resourceId = request.params.id || request.params.legId || request.params.locationId;
|
||||
|
||||
if (!user || !tourId) {
|
||||
return false; // User or tourId not available
|
||||
// Nếu không có tourId trực tiếp, tìm tourId thông qua các tài nguyên liên quan
|
||||
if (!tourId && resourceId) {
|
||||
const resCacheKey = `res-to-tour:${resourceId}`;
|
||||
const compressedData = await this.cacheManager.get<Buffer>(resCacheKey);
|
||||
|
||||
if (compressedData) {
|
||||
try {
|
||||
const decompressed = await gunzip(compressedData);
|
||||
tourId = decompressed.toString();
|
||||
} catch (e) {
|
||||
console.error('Lỗi giải nén cache:', e);
|
||||
}
|
||||
} else {
|
||||
// Thử xem resourceId có phải là tourId không
|
||||
const isTour = await this.prisma.tour.findUnique({ where: { id: resourceId }, select: { id: true } });
|
||||
if (isTour) {
|
||||
tourId = resourceId;
|
||||
} else {
|
||||
// Thử xem resourceId có phải là legId không
|
||||
const leg = await this.prisma.leg.findUnique({ where: { id: resourceId }, select: { tourId: true } });
|
||||
if (leg) {
|
||||
tourId = leg.tourId;
|
||||
} else {
|
||||
// Thử xem resourceId có phải là locationId không
|
||||
const loc = await this.prisma.location.findUnique({
|
||||
where: { id: resourceId },
|
||||
include: { leg: { select: { tourId: true } } }
|
||||
});
|
||||
if (loc) {
|
||||
tourId = loc.leg.tourId;
|
||||
} else {
|
||||
// Thử xem resourceId có phải là photoId không
|
||||
const photo = await this.prisma.photo.findUnique({ where: { id: resourceId }, select: { tourId: true } });
|
||||
if (photo) tourId = photo.tourId;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Cache ánh xạ tài nguyên -> tour trong 1 giờ để giảm tải query ngược
|
||||
if (tourId) {
|
||||
try {
|
||||
const compressed = await gzip(Buffer.from(tourId));
|
||||
await this.cacheManager.set(resCacheKey, compressed, CACHE_TTL.RESOURCE_TO_TOUR);
|
||||
} catch (e) {
|
||||
await this.cacheManager.set(resCacheKey, tourId, CACHE_TTL.RESOURCE_TO_TOUR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const participation = await this.prisma.tourParticipant.findUnique({
|
||||
where: { tourId_userId: { tourId, userId: user.id } },
|
||||
});
|
||||
if (!user || !tourId) {
|
||||
// Nếu đây là các route công khai hoặc không liên quan đến Tour, cho phép đi qua
|
||||
// nhưng ở đây chúng ta đang áp dụng guard cho các route cần phân quyền Tour
|
||||
if (!resourceId && !request.params.tourId) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!participation || !rolesToCheck.some(role => participation.role === role)) {
|
||||
// Cache vai trò người dùng trong tour (5 phút)
|
||||
const roleCacheKey = `user-role:${user.id}:${tourId}`;
|
||||
let role = await this.cacheManager.get<ParticipantRole>(roleCacheKey);
|
||||
|
||||
if (!role) {
|
||||
const participation = await this.prisma.tourParticipant.findUnique({
|
||||
where: { tourId_userId: { tourId, userId: user.id } },
|
||||
});
|
||||
if (!participation) return false;
|
||||
role = participation.role;
|
||||
await this.cacheManager.set(roleCacheKey, role, CACHE_TTL.USER_ROLE);
|
||||
}
|
||||
|
||||
if (!rolesToCheck.some(r => role === r)) {
|
||||
throw new ForbiddenException('Bạn không có quyền thực hiện hành động này trong tour này.');
|
||||
}
|
||||
return true;
|
||||
@@ -167,8 +251,10 @@ class AuthController {
|
||||
class PublicTourController {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
@UseInterceptors(CompressCacheInterceptor) // Áp dụng Interceptor nén và cache
|
||||
@Get(':id/public')
|
||||
async getPublicTourDetails(@Param('id', ParseUUIDPipe) id: string) {
|
||||
console.log(`[PublicTour] Đang truy vấn chi tiết Tour ID: ${id}`);
|
||||
const tour = await this.prisma.tour.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
@@ -198,14 +284,20 @@ class PublicTourController {
|
||||
},
|
||||
});
|
||||
|
||||
if (!tour) throw new NotFoundException(`Không tìm thấy Tour`);
|
||||
if (!tour) {
|
||||
console.warn(`[PublicTour] Không tìm thấy Tour với ID: ${id} trong Database`);
|
||||
throw new NotFoundException(`Không tìm thấy Tour`);
|
||||
}
|
||||
return tour;
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('tours')
|
||||
class TourController {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
@Inject(CACHE_MANAGER) private cacheManager: Cache
|
||||
) {}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post()
|
||||
@@ -449,13 +541,43 @@ class TourController {
|
||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||
@Delete(':id')
|
||||
async deleteTour(@Param('id', ParseUUIDPipe) id: string) {
|
||||
// 1. Lấy danh sách ảnh thuộc tour để có đường dẫn file 2K
|
||||
const photos = await this.prisma.photo.findMany({
|
||||
where: { tourId: id }
|
||||
// 1. Lấy thông tin chi tiết Tour cùng các tài nguyên liên quan để dọn dẹp cache và file
|
||||
const tour = await this.prisma.tour.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
participants: true,
|
||||
photos: true,
|
||||
legs: {
|
||||
include: { locations: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!tour) throw new NotFoundException('Không tìm thấy tour');
|
||||
|
||||
// --- BẮT ĐẦU DỌN DẸP CACHE ---
|
||||
// a. Xóa cache vai trò của tất cả thành viên trong tour này
|
||||
for (const participant of tour.participants) {
|
||||
await this.cacheManager.del(`user-role:${participant.userId}:${id}`);
|
||||
}
|
||||
|
||||
// b. Xóa cache mapping tài nguyên (Leg, Location, Photo) về Tour này
|
||||
await this.cacheManager.del(`res-to-tour:${id}`); // Bản thân tour
|
||||
|
||||
for (const leg of tour.legs) {
|
||||
await this.cacheManager.del(`res-to-tour:${leg.id}`);
|
||||
for (const loc of leg.locations) {
|
||||
await this.cacheManager.del(`res-to-tour:${loc.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const photo of tour.photos) {
|
||||
await this.cacheManager.del(`res-to-tour:${photo.id}`);
|
||||
}
|
||||
// --- KẾT THÚC DỌN DẸP CACHE ---
|
||||
|
||||
// 2. Xóa các file vật lý (ảnh 2K) trong thư mục uploads/tours
|
||||
for (const photo of photos) {
|
||||
for (const photo of tour.photos) {
|
||||
if (photo.imageUrl) {
|
||||
const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
|
||||
if (fs.existsSync(displayFilePath)) {
|
||||
@@ -477,6 +599,7 @@ class TourController {
|
||||
}
|
||||
|
||||
// getPublicTours does not need TourRoleGuard as it's for any logged-in user to see their tours
|
||||
@UseInterceptors(CompressCacheInterceptor) // Áp dụng Interceptor nén và cache
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('explore')
|
||||
async getPublicTours(@Req() req: any) {
|
||||
@@ -507,6 +630,7 @@ class TourController {
|
||||
});
|
||||
}
|
||||
|
||||
@UseInterceptors(CompressCacheInterceptor) // Áp dụng Interceptor nén và cache
|
||||
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER, ParticipantRole.MEMBER, ParticipantRole.MEMBER_NO_FINANCE, ParticipantRole.VIEWER_ONLY) // Any participant can view tour details
|
||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||
@Get(':id')
|
||||
@@ -551,6 +675,9 @@ class TourController {
|
||||
const validRoles = ['OWNER', 'MANAGER', 'MEMBER', 'MEMBER_NO_FINANCE', 'VIEWER_ONLY'] as const;
|
||||
const role = validRoles.includes(body.role as any) ? body.role as 'OWNER' | 'MANAGER' | 'MEMBER' | 'MEMBER_NO_FINANCE' | 'VIEWER_ONLY' : 'MEMBER';
|
||||
|
||||
// Xóa cache khi thay đổi quyền hạn hoặc thêm thành viên mới
|
||||
await this.cacheManager.del(`user-role:${body.userId}:${tourId}`);
|
||||
|
||||
const participation = await this.prisma.tourParticipant.findUnique({
|
||||
where: { tourId_userId: { tourId, userId: body.userId } },
|
||||
});
|
||||
@@ -672,6 +799,8 @@ class TourController {
|
||||
throw new BadRequestException('Yêu cầu này đã được xử lý trước đó.');
|
||||
}
|
||||
|
||||
await this.cacheManager.del(`user-role:${joinRequest.userId}:${tourId}`);
|
||||
|
||||
const existing = await this.prisma.tourParticipant.findUnique({
|
||||
where: { tourId_userId: { tourId, userId: joinRequest.userId } },
|
||||
});
|
||||
@@ -743,6 +872,9 @@ class TourController {
|
||||
if (!participation) {
|
||||
throw new NotFoundException('Thành viên này không có trong tour');
|
||||
}
|
||||
|
||||
await this.cacheManager.del(`user-role:${userId}:${tourId}`);
|
||||
|
||||
await this.prisma.tourParticipant.delete({
|
||||
where: { tourId_userId: { tourId, userId } },
|
||||
});
|
||||
@@ -862,7 +994,7 @@ class LocationController {
|
||||
|
||||
@Controller('legs')
|
||||
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Default roles for Leg operations
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||
class LegController {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
@@ -910,6 +1042,7 @@ function calculateDistance(lat1: number, lon1: number, lat2: number, lon2: numbe
|
||||
|
||||
@Controller('routing')
|
||||
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Default roles for Routing operations
|
||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||
class RoutingController {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
@@ -1024,7 +1157,7 @@ class RoutingController {
|
||||
|
||||
@Controller('photos')
|
||||
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER, ParticipantRole.MEMBER, ParticipantRole.MEMBER_NO_FINANCE) // All members can delete their own photos
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||
class PhotoController {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
@@ -1204,7 +1337,8 @@ class CommentController {
|
||||
});
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER, ParticipantRole.MEMBER, ParticipantRole.MEMBER_NO_FINANCE, ParticipantRole.VIEWER_ONLY)
|
||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||
@Post(':locationId/comments')
|
||||
async addComment(
|
||||
@Param('locationId', ParseUUIDPipe) locationId: string,
|
||||
@@ -1239,6 +1373,15 @@ class CommentController {
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
CacheModule.registerAsync({
|
||||
isGlobal: true,
|
||||
useFactory: async () => ({
|
||||
store: await redisStore({
|
||||
url: process.env.REDIS_URL || 'redis://localhost:6379',
|
||||
ttl: CACHE_TTL.DEFAULT, // Cấu hình TTL mặc định cho toàn bộ store
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
JwtModule.register({
|
||||
secret: process.env.JWT_SECRET || 'super-secret',
|
||||
signOptions: { expiresIn: '1d' },
|
||||
@@ -1251,4 +1394,8 @@ class CommentController {
|
||||
class AppModule {}
|
||||
|
||||
|
||||
bootstrap();
|
||||
bootstrap().catch(err => {
|
||||
console.error('💥 Lỗi khởi động Server:');
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user