optimize: sử dụng redis để cache cho các response từ server

This commit is contained in:
2026-06-16 20:17:17 +07:00
parent 2fabdb79df
commit 37b5d14d7e
10 changed files with 857 additions and 39 deletions
+139 -17
View File
@@ -55,6 +55,8 @@ const envPath = path.resolve(process.cwd(), '..', '.env');
dotenv.config({ path: envPath });
require("reflect-metadata");
const fs = __importStar(require("fs"));
const zlib = __importStar(require("zlib"));
const util_1 = require("util");
const sharp_1 = __importDefault(require("sharp"));
const core_1 = require("@nestjs/core");
const common_1 = require("@nestjs/common");
@@ -70,7 +72,17 @@ const jwt_auth_guard_1 = require("./auth/jwt-auth.guard");
const jwt_strategy_1 = require("./auth/jwt.strategy");
const core_2 = require("@nestjs/core");
const common_2 = require("@nestjs/common");
const cache_manager_1 = require("@nestjs/cache-manager");
const cache_manager_redis_yet_1 = require("cache-manager-redis-yet");
const compress_cache_interceptor_1 = require("./common/compress-cache.interceptor");
const gzip = (0, util_1.promisify)(zlib.gzip);
const gunzip = (0, util_1.promisify)(zlib.gunzip);
const UPLOAD_ROOT = path.join(process.cwd(), 'uploads');
const CACHE_TTL = {
DEFAULT: 600000,
RESOURCE_TO_TOUR: 3600000,
USER_ROLE: 300000,
};
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);
@@ -94,9 +106,10 @@ exports.ROLES_KEY = 'roles';
const Roles = (...roles) => (0, common_2.SetMetadata)(exports.ROLES_KEY, roles);
exports.Roles = Roles;
let TourRoleGuard = class TourRoleGuard {
constructor(reflector, prisma) {
constructor(reflector, prisma, cacheManager) {
this.reflector = reflector;
this.prisma = prisma;
this.cacheManager = cacheManager;
}
async canActivate(context) {
const requiredRoles = this.reflector.getAllAndOverride(exports.ROLES_KEY, [
@@ -107,14 +120,73 @@ let TourRoleGuard = class TourRoleGuard {
const rolesToCheck = requiredRoles && requiredRoles.length > 0 ? requiredRoles : defaultRoles;
const request = context.switchToHttp().getRequest();
const user = request.user;
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 (!tourId && resourceId) {
const resCacheKey = `res-to-tour:${resourceId}`;
const compressedData = await this.cacheManager.get(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 {
const isTour = await this.prisma.tour.findUnique({ where: { id: resourceId }, select: { id: true } });
if (isTour) {
tourId = resourceId;
}
else {
const leg = await this.prisma.leg.findUnique({ where: { id: resourceId }, select: { tourId: true } });
if (leg) {
tourId = leg.tourId;
}
else {
const loc = await this.prisma.location.findUnique({
where: { id: resourceId },
include: { leg: { select: { tourId: true } } }
});
if (loc) {
tourId = loc.leg.tourId;
}
else {
const photo = await this.prisma.photo.findUnique({ where: { id: resourceId }, select: { tourId: true } });
if (photo)
tourId = photo.tourId;
}
}
}
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);
}
}
}
}
if (!user || !tourId) {
if (!resourceId && !request.params.tourId)
return true;
return false;
}
const participation = await this.prisma.tourParticipant.findUnique({
where: { tourId_userId: { tourId, userId: user.id } },
});
if (!participation || !rolesToCheck.some(role => participation.role === role)) {
const roleCacheKey = `user-role:${user.id}:${tourId}`;
let role = await this.cacheManager.get(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 common_1.ForbiddenException('Bạn không có quyền thực hiện hành động này trong tour này.');
}
return true;
@@ -123,7 +195,9 @@ let TourRoleGuard = class TourRoleGuard {
exports.TourRoleGuard = TourRoleGuard;
exports.TourRoleGuard = TourRoleGuard = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [core_2.Reflector, prisma_service_1.PrismaService])
__param(2, (0, common_1.Inject)(cache_manager_1.CACHE_MANAGER)),
__metadata("design:paramtypes", [core_2.Reflector,
prisma_service_1.PrismaService, Object])
], TourRoleGuard);
let AppController = class AppController {
getHello() {
@@ -212,6 +286,7 @@ let PublicTourController = class PublicTourController {
this.prisma = prisma;
}
async getPublicTourDetails(id) {
console.log(`[PublicTour] Đang truy vấn chi tiết Tour ID: ${id}`);
const tour = await this.prisma.tour.findUnique({
where: { id },
include: {
@@ -240,12 +315,15 @@ let PublicTourController = class PublicTourController {
},
},
});
if (!tour)
if (!tour) {
console.warn(`[PublicTour] Không tìm thấy Tour với ID: ${id} trong Database`);
throw new common_1.NotFoundException(`Không tìm thấy Tour`);
}
return tour;
}
};
__decorate([
(0, common_1.UseInterceptors)(compress_cache_interceptor_1.CompressCacheInterceptor),
(0, common_1.Get)(':id/public'),
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
__metadata("design:type", Function),
@@ -257,8 +335,9 @@ PublicTourController = __decorate([
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
], PublicTourController);
let TourController = class TourController {
constructor(prisma) {
constructor(prisma, cacheManager) {
this.prisma = prisma;
this.cacheManager = cacheManager;
}
async createTour(body, req) {
const { title, description, startDate, endDate, adultCount, childCount, childDiscount, tags } = body;
@@ -445,10 +524,32 @@ let TourController = class TourController {
});
}
async deleteTour(id) {
const photos = await this.prisma.photo.findMany({
where: { tourId: id }
const tour = await this.prisma.tour.findUnique({
where: { id },
include: {
participants: true,
photos: true,
legs: {
include: { locations: true }
}
}
});
for (const photo of photos) {
if (!tour)
throw new common_1.NotFoundException('Không tìm thấy tour');
for (const participant of tour.participants) {
await this.cacheManager.del(`user-role:${participant.userId}:${id}`);
}
await this.cacheManager.del(`res-to-tour:${id}`);
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}`);
}
for (const photo of tour.photos) {
if (photo.imageUrl) {
const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
if (fs.existsSync(displayFilePath)) {
@@ -527,6 +628,7 @@ let TourController = class TourController {
async addMember(tourId, body, req) {
const validRoles = ['OWNER', 'MANAGER', 'MEMBER', 'MEMBER_NO_FINANCE', 'VIEWER_ONLY'];
const role = validRoles.includes(body.role) ? body.role : 'MEMBER';
await this.cacheManager.del(`user-role:${body.userId}:${tourId}`);
const participation = await this.prisma.tourParticipant.findUnique({
where: { tourId_userId: { tourId, userId: body.userId } },
});
@@ -627,6 +729,7 @@ let TourController = class TourController {
if (joinRequest.status !== 'PENDING') {
throw new common_1.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 } },
});
@@ -685,6 +788,7 @@ let TourController = class TourController {
if (!participation) {
throw new common_1.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 } },
});
@@ -809,6 +913,7 @@ __decorate([
__metadata("design:returntype", Promise)
], TourController.prototype, "deleteTour", null);
__decorate([
(0, common_1.UseInterceptors)(compress_cache_interceptor_1.CompressCacheInterceptor),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard),
(0, common_1.Get)('explore'),
__param(0, (0, common_1.Req)()),
@@ -817,6 +922,7 @@ __decorate([
__metadata("design:returntype", Promise)
], TourController.prototype, "getPublicTours", null);
__decorate([
(0, common_1.UseInterceptors)(compress_cache_interceptor_1.CompressCacheInterceptor),
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER, client_1.ParticipantRole.MEMBER, client_1.ParticipantRole.MEMBER_NO_FINANCE, client_1.ParticipantRole.VIEWER_ONLY),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
(0, common_1.Get)(':id'),
@@ -903,7 +1009,8 @@ __decorate([
], TourController.prototype, "uploadPhotos", null);
TourController = __decorate([
(0, common_1.Controller)('tours'),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
__param(1, (0, common_1.Inject)(cache_manager_1.CACHE_MANAGER)),
__metadata("design:paramtypes", [prisma_service_1.PrismaService, Object])
], TourController);
let LocationController = class LocationController {
constructor(prisma) {
@@ -1020,7 +1127,7 @@ __decorate([
LegController = __decorate([
(0, common_1.Controller)('legs'),
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
], LegController);
function calculateDistance(lat1, lon1, lat2, lon2) {
@@ -1123,6 +1230,7 @@ __decorate([
RoutingController = __decorate([
(0, common_1.Controller)('routing'),
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
], RoutingController);
let PhotoController = class PhotoController {
@@ -1166,7 +1274,7 @@ __decorate([
PhotoController = __decorate([
(0, common_1.Controller)('photos'),
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER, client_1.ParticipantRole.MEMBER, client_1.ParticipantRole.MEMBER_NO_FINANCE),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
], PhotoController);
let UserController = class UserController {
@@ -1364,7 +1472,8 @@ __decorate([
__metadata("design:returntype", Promise)
], CommentController.prototype, "getComments", null);
__decorate([
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard),
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER, client_1.ParticipantRole.MEMBER, client_1.ParticipantRole.MEMBER_NO_FINANCE, client_1.ParticipantRole.VIEWER_ONLY),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
(0, common_1.Post)(':locationId/comments'),
__param(0, (0, common_1.Param)('locationId', common_1.ParseUUIDPipe)),
__param(1, (0, common_1.Body)()),
@@ -1383,6 +1492,15 @@ let AppModule = class AppModule {
AppModule = __decorate([
(0, common_1.Module)({
imports: [
cache_manager_1.CacheModule.registerAsync({
isGlobal: true,
useFactory: async () => ({
store: await (0, cache_manager_redis_yet_1.redisStore)({
url: process.env.REDIS_URL || 'redis://localhost:6379',
ttl: CACHE_TTL.DEFAULT,
}),
}),
}),
jwt_1.JwtModule.register({
secret: process.env.JWT_SECRET || 'super-secret',
signOptions: { expiresIn: '1d' },
@@ -1393,5 +1511,9 @@ AppModule = __decorate([
exports: [prisma_service_1.PrismaService]
})
], AppModule);
bootstrap();
bootstrap().catch(err => {
console.error('💥 Lỗi khởi động Server:');
console.error(err);
process.exit(1);
});
//# sourceMappingURL=main.js.map