Thiết Kế Kỹ Thuật

Module Tuyển Dụng
RECR
Phiên bản1.0
Hệ thốngHệ thống Quản lý Nhân sự
ModuleRECR - Tuyển Dụng
Công nghệNestJS, TypeScript, PostgreSQL, RabbitMQ, Drizzle ORM
Ngày2026-08-13
Tác giảNam Nguyen
Trạng tháiNháp
SRS liên quanSRS Tuyển Dụng

Mục Lục

1. Tổng quan

1.1 Mục đích

Tài liệu này mô tả thiết kế kỹ thuật cho module Tuyển Dụng, được triển khai dưới dạng microservice NestJS monorepo.

1.2 Tham chiếu SRS

Tài liệuPhiên bảnMô tả
SRS1.0SRS Tuyển Dụng
API Spec1.0API Spec Tuyển Dụng
Thiết kế Cơ sở dữ liệu1.0Thiết kế Cơ sở dữ liệu Tuyển Dụng

1.3 Các yêu cầu được đề cập

Danh mụcMã yêu cầuPhần thiết kế
Ứng viênFR-001 - FR-007, FR-037, FR-038Phần 4, 5, 6
HRFR-008 - FR-014, FR-028, FR-029, FR-032Phần 4, 5, 6
Nhà tuyển dụngFR-015 - FR-020, FR-033Phần 4, 5, 6
Gói dịch vụFR-021 - FR-026Phần 4, 5, 6
Đánh giáFR-030, FR-031Phần 4, 5, 6
Phân tíchFR-034 - FR-036Phần 8
Phi chức năngNFR-001 - NFR-007Phần 10
Lời mờiFR-039 - FR-046Phần 4, 5, 6, 7 (Xử lý bởi Module AUTH)

2. Kiến trúc

2.1 Cấu trúc NestJS Monorepo

Module RECR tuân theo kiến trúc NestJS monorepo với sự phân tách rõ ràng giữa các thành phần:

apps/
├── recr-api/                    # HTTP API Service (Controller layer)
│   ├── src/
│   │   ├── main.ts
│   │   ├── app.module.ts
│   │   └── controllers/         # Route handlers
│   └── package.json
│
├── recr-worker/                 # Background Worker (Consumer layer)
│   ├── src/
│   │   ├── main.ts
│   │   ├── worker.module.ts
│   │   └── consumers/           # Message queue consumers
│   └── package.json
│
libs/
├── recr-core/                   # Shared Business Logic (Service layer)
│   ├── src/
│   │   ├── services/            # Domain services
│   │   ├── entities/            # Drizzle ORM schemas
│   │   ├── dto/                 # Data transfer objects
│   │   └── events/              # Event definitions
│   └── package.json
│
└── shared/                      # Cross-module utilities
    ├── src/
    │   ├── guards/              # Auth, subscription guards
    │   ├── interceptors/        # Logging, transform
    │   └── validators/          # Custom validators
    └── package.json

2.2 Các quyết định thiết kế chính

#Quyết địnhLý doGiải quyết
1Tách biệt API và Worker services API xử lý các yêu cầu HTTP đồng bộ. Worker xử lý các công việc nền (email, thông báo, phân tích) bất đồng bộ thông qua RabbitMQ. NFR-001, NFR-002
2Thư viện core chia sẻ (recr-core) Services, entities, và DTOs được chia sẻ giữa API và Worker. Là nguồn duy nhất cho logic nghiệp vụ. FR-001 - FR-031
3Kiến trúc sự kiện驱动 với RabbitMQ Tách rời các module. Sự kiện được phát ra bởi services, được tiêu thụ bởi workers để xử lý bất đồng bộ. NFR-003
4Drizzle ORM cho truy cập cơ sở dữ liệu Truy vấn an toàn kiểu, nhẹ nhàng, hỗ trợ TypeScript xuất sắc. NFR-005
5Gói dịch vụ là tùy chọn cho tất cả vai trò Tất cả người dùng có thể mua gói dịch vụ cho các tính năng cao cấp, nhưng chức năng cơ bản vẫn khả dụng mà không cần. FR-023

3. Cấu trúc Dự án

3.1 Cấu trúc Module

Mỗi module domain tuân theo mô hình NestJS module:

libs/recr-core/src/
├── job/
│   ├── job.module.ts
│   ├── job.controller.ts        # API endpoints (used by recr-api)
│   ├── job.service.ts           # Business logic
│   ├── job.repository.ts        # Database queries (Drizzle)
│   ├── job.consumer.ts          # Event consumers (used by recr-worker)
│   ├── dto/
│   │   ├── create-job.dto.ts
│   │   └── update-job.dto.ts
│   └── entities/
│       └── job.schema.ts        # Drizzle table definition
│
├── application/
│   ├── application.module.ts
│   ├── application.controller.ts
│   ├── application.service.ts
│   ├── application.repository.ts
│   ├── application.consumer.ts
│   └── entities/
│       └── application.schema.ts
│
├── interview/
│   ├── interview.module.ts
│   ├── interview.controller.ts
│   ├── interview.service.ts
│   ├── interview.repository.ts
│   ├── interview.consumer.ts
│   └── entities/
│       └── interview.schema.ts
│
├── subscription/
│   ├── subscription.module.ts
│   ├── subscription.controller.ts
│   ├── subscription.service.ts
│   ├── subscription.repository.ts
│   ├── subscription.guard.ts    # SubscriptionGuard
│   └── entities/
│       └── subscription.schema.ts
│
├── review/
│   ├── review.module.ts
│   ├── review.controller.ts
│   ├── review.service.ts
│   ├── review.repository.ts
│   └── entities/
│       ├── job-review.schema.ts
│       └── candidate-review.schema.ts
│
├── analytics/
│   ├── analytics.module.ts
│   ├── analytics.controller.ts
│   ├── analytics.service.ts
│   ├── opensearch.client.ts     # OpenSearch client
│   └── dto/
│       └── dashboard-query.dto.ts
│
└── events/
    ├── job-events.ts            # Job-related events
    ├── application-events.ts    # Application events
    ├── subscription-events.ts   # Subscription events
    ├── review-events.ts         # Review events
    └── analytics-events.ts      # Analytics tracking events

3.2 Tổ chức File

Thư mụcMục đíchSử dụng bởi
apps/recr-api/Cổng HTTP API, xử lý định tuyếnAPI consumers (frontend, mobile)
apps/recr-worker/Xử lý công việc nềnRabbitMQ consumers
libs/recr-core/Logic nghiệp vụ chia sẻ, entities, DTOsCả API và Worker
libs/shared/Tiện ích cross-moduleTất cả modules

4. Thiết kế Controller

Controllers xử lý các yêu cầu HTTP và ủy quyền cho services. Nằm trong apps/recr-api/src/controllers/.

4.1 Danh sách Controller

ControllerĐường dẫn cơ sởTrách nhiệmYêu cầu
AuthController/recr/authĐăng ký, đăng nhập (Chức năng xác thực thực tế được xử lý bởi Module AUTH)FR-001, FR-002, FR-015
JobController/recr/jobs, /recr/employer/jobs, /recr/hr/jobsCRUD việc làm, tìm kiếm, lọcFR-003, FR-016, FR-017, FR-028, FR-029
ApplicationController/recr/applications, /recr/candidate/applications/:id/resumeNộp hồ sơ, theo dõi trạng thái, tải CV (presigned URL từ module FILE)FR-004, FR-005, FR-008, FR-009, FR-038
InterviewController/recr/hr/interviewsLên lịch phỏng vấnFR-010
EvaluationController/recr/hr/evaluationsĐánh giá ứng viênFR-011
ProfileController/recr/candidate/profile, /recr/candidate/resume, /recr/candidate/resumes, /recr/hr/profile, /recr/employer/profileCập nhật hồ sơ (tất cả vai trò), quản lý CV (tích hợp module FILE)FR-006, FR-007, FR-032, FR-033, FR-037
SubscriptionController/recr/subscriptionLiệt kê gói, mua, gia hạnFR-021 - FR-026
ReviewController/recr/candidate/reviews, /recr/hr/reviewsĐánh giá việc làm, đánh giá ứng viênFR-030, FR-031
InviteController/recr/invitationsGửi lời mời, xác minh, chấp nhận, thu hồi, gửi lại (Chức năng lời mời được xử lý bởi Module AUTH)FR-039, FR-042, FR-043, FR-044, FR-045, FR-046

4.2 Ví dụ Controller

// apps/recr-api/src/controllers/job.controller.ts
@Controller('recr')
export class JobController {
  constructor(private readonly jobService: JobService) {}

  @Post('employer/jobs')
  @UseGuards(AuthGuard, RolesGuard)
  @Roles('employer')
  async createJob(@Body() dto: CreateJobDto, @Req() req) {
    return this.jobService.create(dto, req.user.id);
  }

  @Get('employer/jobs')
  @UseGuards(AuthGuard, RolesGuard)
  @Roles('employer')
  async listMyJobs(@Req() req, @Query() query: PaginationDto) {
    return this.jobService.findByOwner(req.user.id, query);
  }

  @Get('jobs')
  async browseJobs(@Query() query: JobFilterDto) {
    return this.jobService.browse(query);
  }
}

5. Thiết kế Service

Services chứa logic nghiệp vụ. Nằm trong libs/recr-core/src/services/.

5.1 Danh sách Service

ServiceTrách nhiệmPhụ thuộc
JobServiceCRUD việc làm, quản lý trạng thái, tìm kiếmJobRepository, EventEmitter
ApplicationServiceNộp hồ sơ, luồng trạng thái, liên kết CV và truy cập CV đã đóng băngApplicationRepository, JobService, EventEmitter, FileService (FILE)
InterviewServiceLên lịch phỏng vấn, phát hiện xung độtInterviewRepository, ApplicationService
EvaluationServiceChấm điểm đánh giá, tổng hợpEvaluationRepository, InterviewRepository
ProfileServiceCập nhật hồ sơ (tất cả vai trò), quản lý CV (tải lên, liệt kê, xóa thông qua module FILE)UserRepository, CandidateRepository, HrProfileRepository, EmployerRepository, FileService (FILE)
SubscriptionServiceQuản lý gói, mua, gia hạn, xác thựcSubscriptionRepository, PaymentService
ReviewServiceĐánh giá việc làm, đánh giá ứng viên, tổng hợp điểm đánh giáReviewRepository, JobRepository
InviteServiceVòng đời lời mời: tạo token, xác thực, quản lý trạng thái, phát hành sự kiện (Triển khai bởi Module AUTH)InviteRepository, EventEmitter

5.2 Ví dụ Service

// libs/recr-core/src/services/job.service.ts
@Injectable()
export class JobService {
  constructor(
    private readonly jobRepository: JobRepository,
    private readonly eventEmitter: EventEmitter2,
  ) {}

  async create(dto: CreateJobDto, userId: string): Promise<Job> {
    // 1. Validate employer owns company
    const job = await this.jobRepository.create({
      ...dto,
      createdBy: userId,
      status: 'draft',
    });

    // 2. Emit event for async processing
    this.eventEmitter.emit('job.created', {
      jobId: job.id,
      createdBy: userId,
      title: dto.title,
    });

    return job;
  }

  async submitForApproval(jobId: string, userId: string): Promise<Job> {
    const job = await this.jobRepository.findById(jobId);

    // 1. Validate ownership
    if (job.createdBy !== userId) {
      throw new ForbiddenException('Not job owner');
    }

    // 2. Update status
    const updated = await this.jobRepository.updateStatus(
      jobId,
      'pending_approval'
    );

    // 3. Emit event for notification worker
    this.eventEmitter.emit('job.submitted_for_approval', {
      jobId: job.id,
      title: job.title,
      createdBy: userId,
    });

    return updated;
  }
}

6. Thiết kế Worker

Workers tiêu thụ tin nhắn từ RabbitMQ để xử lý bất đồng bộ. Nằm trong apps/recr-worker/src/consumers/.

6.1 Danh sách Worker

Worker (Trình xử lý)Hàng đợiMục đíchSự kiện xử lý
JobConsumerrecr.job.eventsCông việc nền liên quan đến việc làmjob.created, job.submitted_for_approval
ApplicationConsumerrecr.application.eventsThông báo hồ sơapplication.submitted, application.status_changed
InterviewConsumerrecr.interview.eventsThông báo phỏng vấninterview.scheduled, interview.completed
SubscriptionConsumerrecr.subscription.eventsKiểm tra hết hạn gói dịch vụsubscription.purchased, subscription.expiring
ReviewConsumerrecr.review.eventsTổng hợp đánh giá, thông báoreview.submitted
NotificationConsumernotification.eventsEmail, thông báo đẩynotification.send
FileConsumerfile.eventsXử lý sự kiện module FILE (đầu vào)file.deleted
InviteConsumerrecr.invitation.eventsThông báo email lời mời (Triển khai bởi Module AUTH)invitation.created, invitation.accepted, invitation.revoked, invitation.resent

6.2 Ví dụ Worker

// apps/recr-worker/src/consumers/job.consumer.ts
@RabbitMQConsumer('recr.job.events')
export class JobConsumer {
  constructor(
    private readonly notificationService: NotificationService,
    private readonly logService: LogService,
  ) {}

  @RabbitMQHandler('job.submitted_for_approval')
  async handleJobSubmitted(event: JobSubmittedEvent) {
    // 1. Log the event
    await this.logService.log({
      action: 'job.submitted_for_approval',
      jobId: event.jobId,
      userId: event.createdBy,
    });

    // 2. Notify HR team
    await this.notificationService.send({
      type: 'email',
      to: '[email protected]',
      subject: `New job pending approval: ${event.title}`,
      template: 'job-pending-approval',
      data: { jobId: event.jobId, title: event.title },
    });

    // 3. Notify admins if needed
    if (event.requiresAdminApproval) {
      await this.notificationService.send({
        type: 'email',
        to: '[email protected]',
        subject: `Job requires admin approval: ${event.title}`,
        template: 'job-admin-approval',
        data: { jobId: event.jobId },
      });
    }
  }
}

7. Xử lý Sự kiện

7.1 luồng Sự kiện

┌─────────────────────────────────────────────────────────────────┐
│                        EVENT FLOW                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Controller ──► Service ──► EventEmitter ──► RabbitMQ ──► Worker │
│                  │                           │                  │
│                  ▼                           ▼                  │
│            Repository                 Consumer                   │
│            (Drizzle)                  (Background)               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

7.2 Định nghĩa Sự kiện

Sự kiệnNgười phátNgười tiêu thụDữ liệu
job.createdJobServiceJobConsumer{ jobId, createdBy, title }
job.submitted_for_approvalJobServiceJobConsumer{ jobId, title, createdBy }
application.submittedApplicationServiceApplicationConsumer{ applicationId, jobId, candidateId }
application.status_changedApplicationServiceApplicationConsumer{ applicationId, oldStatus, newStatus }
interview.scheduledInterviewServiceInterviewConsumer{ interviewId, candidateId, scheduledAt }
subscription.purchasedSubscriptionServiceSubscriptionConsumer{ userId, planId, amount }
subscription.expiringCronJobSubscriptionConsumer{ userId, subscriptionId, expiresAt }
review.submittedReviewServiceReviewConsumer{ reviewId, type, targetId }

7.3 Ví dụ EventEmitter

// libs/recr-core/src/events/application-events.ts
export interface ApplicationSubmittedEvent {
  applicationId: string;
  jobId: string;
  candidateId: string;
  candidateName: string;
  jobTitle: string;
  timestamp: Date;
}

export interface ApplicationStatusChangedEvent {
  applicationId: string;
  jobId: string;
  candidateId: string;
  oldStatus: string;
  newStatus: string;
  changedBy: string;
  timestamp: Date;
}

// Emitted in ApplicationService
this.eventEmitter.emit('application.submitted', {
  applicationId: app.id,
  jobId: app.jobId,
  candidateId: app.candidateId,
  candidateName: user.fullName,
  jobTitle: job.title,
  timestamp: new Date(),
} satisfies ApplicationSubmittedEvent);

7.4 Cấu hình RabbitMQ

// apps/recr-worker/src/worker.module.ts
@Module({
  imports: [
    RabbitMQModule.forRoot({
      exchanges: [
        {
          name: 'recr.events',
          type: 'topic',
        },
      ],
      uri: process.env.RABBITMQ_URL,
    }),
  ],
  providers: [
    JobConsumer,
    ApplicationConsumer,
    InterviewConsumer,
    SubscriptionConsumer,
    ReviewConsumer,
  ],
})
export class WorkerModule {}

8. Tích hợp Cross-Module

8.1 Tích hợp Module FILE

Module RECR phụ thuộc vào module FILE cho tất cả các thao tác lưu trữ file. RECR không bao giờ tải file trực tiếp lên object storage; thay vào đó, nó ủy quyền cho module FILE thông qua các lệnh gọi API.

Điểm tích hợp

Thao tácThành phần RECRAPI Module FILEMô tả
Tải lên CVProfileServiceURL tải lên presigned của FILEỨng viên tải CV lên thông qua presigned URL; RECR lưu file_id trong recr_candidate_resumes
Liệt kê CVProfileServiceAPI metadata của FILERECR lấy metadata file (tên, loại, kích thước) từ module FILE
Xóa CVProfileServiceAPI xóa của FILERECR gọi FILE để xóa file, sau đó xóa bản ghi trong recr_candidate_resumes
Xác thực CVApplicationServiceAPI metadata của FILERECR xác thực file_id tồn tại và thuộc danh mục RESUME trước khi liên kết với hồ sơ
Tải CVApplicationServiceURL tải presigned của FILERECR tạo presigned URL có thời hạn để xem/tải CV ứng viên

Xử lý sự kiện

Sự kiệnHướngTrình xử lýHành động
file.deletedĐầu vào (từ FILE)FileConsumer (recr-worker)Dọn dẹp tham chiếu trong recr_candidate_resumes và recr_applications khi file bị xóa khỏi module FILE

Quy ước đường dẫn File

// FILE module storage path for candidate resumes
{candidate_user_id}/resumes/{uuid}.{extension}

// Example:
101/resumes/550e8400-e29b-41d4-a716-446655440000.pdf

Ví dụ mã

// libs/recr-core/src/services/profile.service.ts
@Injectable()
export class ProfileService {
  constructor(
    private readonly candidateRepository: CandidateRepository,
    private readonly fileService: FileService,  // FILE module client
  ) {}

  async uploadResume(userId: string, fileId: string): Promise<Resume> {
    // 1. Validate file exists in FILE module
    const fileMetadata = await this.fileService.getFile(fileId);
    if (!fileMetadata || fileMetadata.category !== 'RESUME') {
      throw new BadRequestException('Invalid file: must be a RESUME category file');
    }

    // 2. Store reference in recr_candidate_resumes
    const resume = await this.candidateRepository.createResume({
      candidateId: userId,
      fileId: fileId,
    });

    return resume;
  }

  async getResumeDownloadUrl(applicationId: string, userId: string): Promise<DownloadUrl> {
    // 1. Get application and verify ownership
    const application = await this.applicationRepository.findById(applicationId);
    if (application.candidateId !== userId) {
      throw new ForbiddenException('Not authorized');
    }

    // 2. Get presigned download URL from FILE module
    const downloadUrl = await this.fileService.getPresignedDownloadUrl(
      application.resumeFileId,
      { expiresIn: 3600 }  // 1 hour
    );

    return downloadUrl;
  }
}

8.2 FileConsumer (Trình xử lý sự kiện đầu vào)

// apps/recr-worker/src/consumers/file.consumer.ts
@RabbitMQConsumer('file.events')
export class FileConsumer {
  constructor(
    private readonly candidateRepository: CandidateRepository,
    private readonly applicationRepository: ApplicationRepository,
  ) {}

  @RabbitMQHandler('file.deleted')
  async handleFileDeleted(event: FileDeletedEvent) {
    // 1. Clean up recr_candidate_resumes references
    await this.candidateRepository.removeResumeByFileId(event.fileId);

    // 2. Log warning for applications with deleted resume files
    const affectedApplications = await this.applicationRepository
      .findByResumeFileId(event.fileId);
    for (const app of affectedApplications) {
      // Log warning - application has orphaned resume reference
      // The frozen resume_file_id remains for historical records
      console.warn(
        `Application ${app.id} has orphaned resume file: ${event.fileId}`
      );
    }
  }
}

9. Tích hợp OpenSearch

9.1 Tổng quan Kiến trúc

Module RECR sử dụng OpenSearch cho phân tích và OpenSearch Dashboards để trực quan hóa. Các sự kiện được ghi lại thông qua RabbitMQ consumers và được index trong OpenSearch. Người dùng truy cập dashboard trực tiếp thông qua giao diện OpenSearch Dashboards.

┌─────────────────────────────────────────────────────────────────┐
│                    OPENSEARCH ARCHITECTURE                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Services ──► RabbitMQ ──► Analytics Consumer ──► OpenSearch    │
│                                                  (Index)        │
│                                                     │           │
│                                                     ▼           │
│                                          OpenSearch Dashboards  │
│                                          (Built-in UI)          │
│                                                     │           │
│                                              ┌──────┴──────┐   │
│                                              ▼             ▼   │
│                                        User Analytics  Job     │
│                                        Dashboard      Dashboard │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

9.2 Tại sao chọn OpenSearch Dashboards?

Tính năngOpenSearch DashboardsDashboard Tùy chỉnh
Cài đặtSẵn sàng cùng OpenSearchCần phát triển frontend
Trực quan hóaBiểu đồ, đồ thị, bản đồ có sẵnPhải tự xây dựng biểu đồ
Bộ lọcBộ lọc khoảng thời gian, trường có sẵnPhát triển bộ lọc tùy chỉnh
Tổng hợpCông cụ tổng hợp trực quanPhải viết truy vấn tổng hợp
Chia sẻChia sẻ dashboard qua URLPhát triển tính năng chia sẻ
Chi phíMiễn phí với OpenSearchChi phí thời gian phát triển

9.3 Chỉ mục OpenSearch

Chỉ mụcMục đíchCác trường
recr-user-eventsSự kiện đăng ký và đăng nhậpuserId, email, role, action, timestamp, ipAddress, userAgent
recr-job-eventsSự kiện liên quan đến việc làmjobId, createdBy, action, title, status, timestamp
recr-application-eventsSự kiện hồ sơapplicationId, candidateId, jobId, action, status, timestamp
recr-subscription-eventsSự kiện gói dịch vụuserId, planId, action, amount, timestamp

9.4 Schema Sự kiện

// libs/recr-core/src/analytics/opensearch.schema.ts
export interface UserEvent {
  eventId: string;
  userId: number;
  email: string;
  role: 'candidate' | 'hr' | 'employer';
  action: 'registered' | 'logged_in' | 'logged_out' | 'login_failed';
  timestamp: Date;
  ipAddress?: string;
  userAgent?: string;
  metadata?: Record<string, any>;
}

export interface JobEvent {
  eventId: string;
  jobId: number;
  createdBy: number;
  action: 'created' | 'updated' | 'published' | 'closed';
  title: string;
  status: string;
  timestamp: Date;
}

export interface ApplicationEvent {
  eventId: string;
  applicationId: number;
  candidateId: number;
  jobId: number;
  action: 'submitted' | 'status_changed' | 'withdrawn';
  status: string;
  timestamp: Date;
}

9.5 Client OpenSearch

// libs/recr-core/src/analytics/opensearch.client.ts
import { Client } from '@opensearch-project/opensearch';

@Injectable()
export class OpenSearchClient {
  private client: Client;

  constructor(private readonly config: ConfigService) {
    this.client = new Client({
      node: this.config.get('OPENSEARCH_URL'),
      auth: {
        username: this.config.get('OPENSEARCH_USERNAME'),
        password: this.config.get('OPENSEARCH_PASSWORD'),
      },
    });
  }

  async indexEvent(index: string, event: any): Promise<void> {
    await this.client.index({
      index,
      body: {
        ...event,
        timestamp: new Date().toISOString(),
      },
    });
  }
}

9.6 Dashboard có sẵn

OpenSearch Dashboards cung cấp các trực quan hóa có sẵn sau:

Dashboard Phân tích Người dùng

Trực quan hóaLoạiMô tả
Tổng số Người dùng theo Vai tròBiểu đồ TrònPhân bổ người dùng theo vai trò (Ứng viên, HR, Nhà tuyển dụng)
Xu hướng Đăng kýBiểu đồ ĐườngSố lượng đăng ký theo ngày/tuần/tháng
Người dùng Hoạt độngChỉ sốNgười dùng đã đăng nhập trong 7 ngày qua
Tăng trưởng Người dùngBiểu đồ CộtTỷ lệ tăng trưởng so với tháng trước

Dashboard Phân tích Đăng nhập

Trực quan hóaLoạiMô tả
Tần suất Đăng nhậpBiểu đồ ĐườngSố lần đăng nhập theo giờ/ngày
Giờ cao điểm Đăng nhậpBản đồ nhiệtCác giờ có hoạt động đăng nhập cao nhất
Đăng nhập Thất bạiChỉ sốSố lần thử đăng nhập thất bại
Đăng nhập theo Vai tròBiểu đồ CộtPhân bổ đăng nhập theo vai trò người dùng

9.7 Truy cập Dashboard

# OpenSearch Dashboards URL
http://opensearch-dashboards:5601

# Default credentials
Username: admin
Password: admin (or as configured)

# Direct link to RECR dashboards
http://opensearch-dashboards:5601/app/dashboards#/view/recr-user-analytics
http://opensearch-dashboards:5601/app/dashboards#/view/recr-login-analytics

10. Kiến trúc Dữ liệu

10.1 Cấu trúc Schema Drizzle

// libs/recr-core/src/entities/candidate-resume.schema.ts
import { pgTable, bigserial, varchar, boolean, timestamp, uuid } from 'drizzle-orm/pg-core';

export const recrCandidateResumes = pgTable('recr_candidate_resumes', {
  id: uuid('id').defaultRandom().primaryKey(),
  candidateId: bigint('candidate_id', { mode: 'number' }).notNull()
    .references(() => recrCandidates.id, { onDelete: 'cascade' }),
  fileId: uuid('file_id').notNull(),  // References sys_files.id in FILE module
  isPrimary: boolean('is_primary').notNull().default(false),
  createdAt: timestamp('created_at').notNull().defaultNow(),
  updatedAt: timestamp('updated_at').notNull().defaultNow(),
});
// libs/recr-core/src/entities/application.schema.ts
import { pgTable, bigserial, varchar, text, timestamp, uuid, bigint } from 'drizzle-orm/pg-core';

export const recrApplications = pgTable('recr_applications', {
  id: bigserial('id', { mode: 'number' }),
  jobId: bigint('job_id', { mode: 'number' }).notNull()
    .references(() => recrJobs.id),
  candidateId: bigint('candidate_id', { mode: 'number' }).notNull()
    .references(() => recrCandidates.id),
  resumeFileId: uuid('resume_file_id')  // Frozen resume at submission (FILE module)
    .references(() => sysFiles.id),
  coverLetter: text('cover_letter'),
  status: varchar('status', { length: 20 }).notNull().default('submitted'),
  appliedAt: timestamp('applied_at').notNull().defaultNow(),
  updatedAt: timestamp('updated_at').notNull().defaultNow(),
});
// libs/recr-core/src/entities/job.schema.ts
import { pgTable, bigserial, varchar, decimal, timestamp, jsonb, bigint } from 'drizzle-orm/pg-core';

export const recrJobs = pgTable('recr_jobs', {
  id: bigserial('id', { mode: 'number' }),
  employerId: bigint('employer_id', { mode: 'number' }),
  createdBy: bigint('created_by', { mode: 'number' }).notNull(),
  title: varchar('title', { length: 200 }).notNull(),
  description: varchar('description', { length: 5000 }),
  department: varchar('department', { length: 100 }),
  location: varchar('location', { length: 200 }),
  salaryMin: decimal('salary_min', { precision: 12, scale: 2 }),
  salaryMax: decimal('salary_max', { precision: 12, scale: 2 }),
  employmentType: varchar('employment_type', { length: 20 }).notNull(),
  status: varchar('status', { length: 20 }).notNull().default('draft'),
  createdAt: timestamp('created_at').notNull().defaultNow(),
  updatedAt: timestamp('updated_at').notNull().defaultNow(),
});

10.2 Mẫu Repository

// libs/recr-core/src/repositories/job.repository.ts
@Injectable()
export class JobRepository {
  constructor(private readonly db: DrizzleService) {}

  async create(data: CreateJobInput): Promise<Job> {
    const [job] = await this.db.db
      .insert(recrJobs)
      .values(data)
      .returning();
    return job;
  }

  async findById(id: number): Promise<Job | undefined> {
    return this.db.db.query.recrJobs.findFirst({
      where: eq(recrJobs.id, id),
    });
  }

  async findByOwner(userId: number, pagination: PaginationDto) {
    return this.db.db.query.recrJobs.findMany({
      where: eq(recrJobs.createdBy, userId),
      limit: pagination.limit,
      offset: pagination.offset,
    });
  }
}

11. Thiết kế Bảo mật

Yêu cầu SRSGiải pháp Thiết kếTriển khai
NFR-005Kiểm soát truy cập dựa trên vai tròJWT role claim + NestJS Guards (@Roles decorator)
FR-024Xác thực gói dịch vụSubscriptionGuard tùy chọn cho tính năng cao cấp

11.1 Triển khai Guard

// libs/shared/src/guards/roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.get<string[]>(
      'roles',
      context.getHandler()
    );
    if (!requiredRoles) return true;

    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some((role) => user.role === role);
  }
}

// Usage in controller
@Post('employer/jobs')
@UseGuards(AuthGuard, RolesGuard)
@Roles('employer')
async createJob() { ... }

11.2 Guard Gói dịch vụ (Tùy chọn)

// libs/recr-core/src/subscription/subscription.guard.ts
@Injectable()
export class SubscriptionGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const userId = request.user.id;

    // Check if user has active subscription
    const subscription = await this.subscriptionService.getActive(userId);

    if (!subscription) {
      throw new ForbiddenException({
        code: 'SUBSCRIPTION_REQUIRED',
        message: 'Active subscription required for this feature',
      });
    }

    return true;
  }
}

12. Tóm tắt Truy xuất nguồn gốc

Mã yêu cầu SRSYêu cầuControllerServiceWorker
FR-001Đăng ký Người dùngAuthController (AUTH module)AuthService (AUTH)-
FR-002Đăng nhậpAuthController (AUTH module)AuthService (AUTH)-
FR-003Duyệt Việc làmJobControllerJobService-
FR-004Nộp Hồ sơApplicationControllerApplicationService, FileService (FILE)ApplicationConsumer
FR-005Xem Trạng thái Hồ sơApplicationControllerApplicationService-
FR-006Cập nhật Hồ sơProfileControllerProfileService-
FR-007Tải lên CVProfileControllerProfileService, FileService (FILE)-
FR-008Xem Hồ sơApplicationControllerApplicationService, FileService (FILE)-
FR-009Sơ tuyển Ứng viênApplicationControllerApplicationService-
FR-010Lên lịch Phỏng vấnInterviewControllerInterviewServiceInterviewConsumer
FR-011Đánh giá Ứng viênEvaluationControllerEvaluationService-
FR-012Từ chối Ứng viênApplicationControllerApplicationService-
FR-013Theo dõi Quy trìnhApplicationControllerApplicationService-
FR-014Tạo Báo cáoApplicationControllerApplicationService-
FR-015Người dùng tạo tổ chứcAuthController (AUTH module)AuthService (AUTH)-
FR-016Đăng Tin tuyển dụng (Nhà tuyển dụng)JobControllerJobServiceJobConsumer
FR-017Quản lý Tin tuyển dụngJobControllerJobService-
FR-018Xem Người nộp đơnJobControllerJobService-
FR-019Duyệt Tin tuyển dụngJobControllerJobServiceJobConsumer
FR-020Xem Phân tíchJobControllerJobService-
FR-021Gói Dịch vụSubscriptionControllerSubscriptionService-
FR-022Mua Gói dịch vụSubscriptionControllerSubscriptionServiceSubscriptionConsumer
FR-023Kiểm soát Truy cập Gói dịch vụ-SubscriptionGuard-
FR-024Xác thực Gói dịch vụ-SubscriptionGuard-
FR-025Gia hạn Gói dịch vụSubscriptionControllerSubscriptionService-
FR-026Lịch sử Gói dịch vụSubscriptionControllerSubscriptionService-
FR-028HR Đăng TinJobControllerJobServiceJobConsumer
FR-029HR Quản lý TinJobControllerJobService-
FR-030Đánh giá Việc làmReviewControllerReviewServiceReviewConsumer
FR-031Đánh giá Ứng viênReviewControllerReviewServiceReviewConsumer
FR-032Cập nhật Hồ sơ HRProfileControllerProfileService-
FR-033Cập nhật Hồ sơ Nhà tuyển dụngProfileControllerProfileService-
FR-034Phân tích Đăng ký Người dùngAnalyticsControllerAnalyticsServiceAnalyticsConsumer
FR-035Phân tích Đăng nhập Người dùngAnalyticsControllerAnalyticsServiceAnalyticsConsumer
FR-036Xem DashboardAnalyticsControllerAnalyticsService-
FR-037Quản lý CV Ứng viênProfileControllerProfileService, FileService (FILE)-
FR-038Liên kết CV Hồ sơApplicationControllerApplicationService, FileService (FILE)-
NFR-001Thời gian Phản hồi API---
NFR-002Xử lý Hồ sơ---
NFR-003Người dùng Đồng thời---
NFR-004Thời gian Hoạt động Hệ thống---
NFR-005RBACTất cả Controllers--
NFR-006Lưu trữ CV-FileService (module FILE)-
NFR-007Tìm kiếm Việc làmJobControllerJobService-
FR-039Gửi Lời mờiInviteController (AUTH module)InviteService (AUTH)InviteConsumer (AUTH)
FR-040Tạo Token-InviteService (AUTH)-
FR-041Gửi sự kiện Email-InviteService (AUTH)InviteConsumer (AUTH)
FR-042Xác minh TokenInviteController (AUTH module)InviteService (AUTH)-
FR-043Chấp nhận Lời mờiInviteController (AUTH module)InviteService (AUTH)InviteConsumer (AUTH)
FR-044Thu hồi Lời mờiInviteController (AUTH module)InviteService (AUTH)-
FR-045Liệt kê Lời mờiInviteController (AUTH module)InviteService (AUTH)-
FR-046Gửi lại Lời mờiInviteController (AUTH module)InviteService (AUTH)InviteConsumer (AUTH)