feat(cache): implement Redis caching layer for hot-read endpoints
Add cache-aside pattern for listing detail, search results, market analytics (4 endpoints), and user profile queries. Cache invalidation on all write mutations. Prometheus cache_hit_total/cache_miss_total metrics with resource labels. - CacheService: getOrSet, invalidate, invalidateByPrefix (SCAN-based) - TTLs: listing 5m, search 1m, market 30m, profile 10m - All 230 tests passing (13 new cache tests + 6 updated handler tests) Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
|
||||
import { Inject, NotFoundException } from '@nestjs/common';
|
||||
import { CacheService, CachePrefix } from '@modules/shared/infrastructure/cache.service';
|
||||
import { VerifyKycCommand } from './verify-kyc.command';
|
||||
import { USER_REPOSITORY, type IUserRepository } from '../../../domain/repositories/user.repository';
|
||||
|
||||
@@ -7,6 +8,7 @@ import { USER_REPOSITORY, type IUserRepository } from '../../../domain/repositor
|
||||
export class VerifyKycHandler implements ICommandHandler<VerifyKycCommand> {
|
||||
constructor(
|
||||
@Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository,
|
||||
private readonly cache: CacheService,
|
||||
) {}
|
||||
|
||||
async execute(command: VerifyKycCommand): Promise<void> {
|
||||
@@ -17,5 +19,7 @@ export class VerifyKycHandler implements ICommandHandler<VerifyKycCommand> {
|
||||
|
||||
user.updateKycStatus(command.kycStatus, command.kycData);
|
||||
await this.userRepo.update(user);
|
||||
|
||||
await this.cache.invalidate(CacheService.buildKey(CachePrefix.USER_PROFILE, command.userId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { Inject, NotFoundException } from '@nestjs/common';
|
||||
import { CacheService, CachePrefix, CacheTTL } from '@modules/shared/infrastructure/cache.service';
|
||||
import { GetProfileQuery } from './get-profile.query';
|
||||
import { USER_REPOSITORY, type IUserRepository } from '../../../domain/repositories/user.repository';
|
||||
|
||||
@@ -19,24 +20,34 @@ export interface UserProfileDto {
|
||||
export class GetProfileHandler implements IQueryHandler<GetProfileQuery> {
|
||||
constructor(
|
||||
@Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository,
|
||||
private readonly cache: CacheService,
|
||||
) {}
|
||||
|
||||
async execute(query: GetProfileQuery): Promise<UserProfileDto> {
|
||||
const user = await this.userRepo.findById(query.userId);
|
||||
if (!user) {
|
||||
throw new NotFoundException('Người dùng không tồn tại');
|
||||
}
|
||||
const cacheKey = CacheService.buildKey(CachePrefix.USER_PROFILE, query.userId);
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email?.value ?? null,
|
||||
phone: user.phone.value,
|
||||
fullName: user.fullName,
|
||||
avatarUrl: user.avatarUrl,
|
||||
role: user.role,
|
||||
kycStatus: user.kycStatus,
|
||||
isActive: user.isActive,
|
||||
createdAt: user.createdAt,
|
||||
};
|
||||
return this.cache.getOrSet(
|
||||
cacheKey,
|
||||
async () => {
|
||||
const user = await this.userRepo.findById(query.userId);
|
||||
if (!user) {
|
||||
throw new NotFoundException('Người dùng không tồn tại');
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email?.value ?? null,
|
||||
phone: user.phone.value,
|
||||
fullName: user.fullName,
|
||||
avatarUrl: user.avatarUrl,
|
||||
role: user.role,
|
||||
kycStatus: user.kycStatus,
|
||||
isActive: user.isActive,
|
||||
createdAt: user.createdAt,
|
||||
};
|
||||
},
|
||||
CacheTTL.USER_PROFILE,
|
||||
'user_profile',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user