Thiết Kế Kỹ Thuật
Module Tuyển Dụng
RECR
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
1.3 Các yêu cầu được đề cập
| Danh mục | Mã yêu cầu | Phần thiết kế |
| Ứng viên | FR-001 - FR-007, FR-037, FR-038 | Phần 4, 5, 6 |
| HR | FR-008 - FR-014, FR-028, FR-029, FR-032 | Phần 4, 5, 6 |
| Nhà tuyển dụng | FR-015 - FR-020, FR-033 | Phần 4, 5, 6 |
| Gói dịch vụ | FR-021 - FR-026 | Phần 4, 5, 6 |
| Đánh giá | FR-030, FR-031 | Phần 4, 5, 6 |
| Phân tích | FR-034 - FR-036 | Phần 8 |
| Phi chức năng | NFR-001 - NFR-007 | Phần 10 |
| Lời mời | FR-039 - FR-046 | Phầ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 định | Lý do | Giải quyết |
| 1 | Tá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 |
| 2 | Thư 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 |
| 3 | Kiế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 |
| 4 | Drizzle 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 |
| 5 | Gó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ục | Mục đích | Sử dụng bởi |
apps/recr-api/ | Cổng HTTP API, xử lý định tuyến | API consumers (frontend, mobile) |
apps/recr-worker/ | Xử lý công việc nền | RabbitMQ consumers |
libs/recr-core/ | Logic nghiệp vụ chia sẻ, entities, DTOs | Cả API và Worker |
libs/shared/ | Tiện ích cross-module | Tấ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ệm | Yê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/jobs | CRUD việc làm, tìm kiếm, lọc | FR-003, FR-016, FR-017, FR-028, FR-029 |
| ApplicationController | /recr/applications, /recr/candidate/applications/:id/resume | Nộ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/interviews | Lên lịch phỏng vấn | FR-010 |
| EvaluationController | /recr/hr/evaluations | Đánh giá ứng viên | FR-011 |
| ProfileController | /recr/candidate/profile, /recr/candidate/resume, /recr/candidate/resumes, /recr/hr/profile, /recr/employer/profile | Cậ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/subscription | Liệt kê gói, mua, gia hạn | FR-021 - FR-026 |
| ReviewController | /recr/candidate/reviews, /recr/hr/reviews | Đánh giá việc làm, đánh giá ứng viên | FR-030, FR-031 |
| InviteController | /recr/invitations | Gử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
| Service | Trách nhiệm | Phụ thuộc |
| JobService | CRUD việc làm, quản lý trạng thái, tìm kiếm | JobRepository, EventEmitter |
| ApplicationService | Nộp hồ sơ, luồng trạng thái, liên kết CV và truy cập CV đã đóng băng | ApplicationRepository, JobService, EventEmitter, FileService (FILE) |
| InterviewService | Lên lịch phỏng vấn, phát hiện xung đột | InterviewRepository, ApplicationService |
| EvaluationService | Chấm điểm đánh giá, tổng hợp | EvaluationRepository, InterviewRepository |
| ProfileService | Cậ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) |
| SubscriptionService | Quản lý gói, mua, gia hạn, xác thực | SubscriptionRepository, PaymentService |
| ReviewService | Đánh giá việc làm, đánh giá ứng viên, tổng hợp điểm đánh giá | ReviewRepository, JobRepository |
| InviteService | Vò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 đợi | Mục đích | Sự kiện xử lý |
| JobConsumer | recr.job.events | Công việc nền liên quan đến việc làm | job.created, job.submitted_for_approval |
| ApplicationConsumer | recr.application.events | Thông báo hồ sơ | application.submitted, application.status_changed |
| InterviewConsumer | recr.interview.events | Thông báo phỏng vấn | interview.scheduled, interview.completed |
| SubscriptionConsumer | recr.subscription.events | Kiểm tra hết hạn gói dịch vụ | subscription.purchased, subscription.expiring |
| ReviewConsumer | recr.review.events | Tổng hợp đánh giá, thông báo | review.submitted |
| NotificationConsumer | notification.events | Email, thông báo đẩy | notification.send |
| FileConsumer | file.events | Xử lý sự kiện module FILE (đầu vào) | file.deleted |
| InviteConsumer | recr.invitation.events | Thô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ện | Người phát | Người tiêu thụ | Dữ liệu |
job.created | JobService | JobConsumer | { jobId, createdBy, title } |
job.submitted_for_approval | JobService | JobConsumer | { jobId, title, createdBy } |
application.submitted | ApplicationService | ApplicationConsumer | { applicationId, jobId, candidateId } |
application.status_changed | ApplicationService | ApplicationConsumer | { applicationId, oldStatus, newStatus } |
interview.scheduled | InterviewService | InterviewConsumer | { interviewId, candidateId, scheduledAt } |
subscription.purchased | SubscriptionService | SubscriptionConsumer | { userId, planId, amount } |
subscription.expiring | CronJob | SubscriptionConsumer | { userId, subscriptionId, expiresAt } |
review.submitted | ReviewService | ReviewConsumer | { 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ác | Thành phần RECR | API Module FILE | Mô tả |
| Tải lên CV | ProfileService | URL 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ê CV | ProfileService | API metadata của FILE | RECR lấy metadata file (tên, loại, kích thước) từ module FILE |
| Xóa CV | ProfileService | API xóa của FILE | RECR gọi FILE để xóa file, sau đó xóa bản ghi trong recr_candidate_resumes |
| Xác thực CV | ApplicationService | API metadata của FILE | RECR 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 CV | ApplicationService | URL tải presigned của FILE | RECR tạo presigned URL có thời hạn để xem/tải CV ứng viên |
Xử lý sự kiện
| Sự kiện | Hướng | Trì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ăng | OpenSearch Dashboards | Dashboard Tùy chỉnh |
| Cài đặt | Sẵn sàng cùng OpenSearch | Cần phát triển frontend |
| Trực quan hóa | Biểu đồ, đồ thị, bản đồ có sẵn | Phải tự xây dựng biểu đồ |
| Bộ lọc | Bộ lọc khoảng thời gian, trường có sẵn | Phát triển bộ lọc tùy chỉnh |
| Tổng hợp | Công cụ tổng hợp trực quan | Phải viết truy vấn tổng hợp |
| Chia sẻ | Chia sẻ dashboard qua URL | Phát triển tính năng chia sẻ |
| Chi phí | Miễn phí với OpenSearch | Chi phí thời gian phát triển |
9.3 Chỉ mục OpenSearch
| Chỉ mục | Mục đích | Các trường |
recr-user-events | Sự kiện đăng ký và đăng nhập | userId, email, role, action, timestamp, ipAddress, userAgent |
recr-job-events | Sự kiện liên quan đến việc làm | jobId, createdBy, action, title, status, timestamp |
recr-application-events | Sự kiện hồ sơ | applicationId, candidateId, jobId, action, status, timestamp |
recr-subscription-events | Sự 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óa | Loại | Mô tả |
| Tổng số Người dùng theo Vai trò | Biểu đồ Tròn | Phâ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 đồ Đường | Số lượng đăng ký theo ngày/tuần/tháng |
| Người dùng Hoạt động | Chỉ số | Người dùng đã đăng nhập trong 7 ngày qua |
| Tăng trưởng Người dùng | Biểu đồ Cột | Tỷ lệ tăng trưởng so với tháng trước |
Dashboard Phân tích Đăng nhập
| Trực quan hóa | Loại | Mô tả |
| Tần suất Đăng nhập | Biểu đồ Đường | Số lần đăng nhập theo giờ/ngày |
| Giờ cao điểm Đăng nhập | Bản đồ nhiệt | Các giờ có hoạt động đăng nhập cao nhất |
| Đăng nhập Thất bại | Chỉ số | Số lần thử đăng nhập thất bại |
| Đăng nhập theo Vai trò | Biểu đồ Cột | Phâ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 SRS | Giải pháp Thiết kế | Triển khai |
| NFR-005 | Kiểm soát truy cập dựa trên vai trò | JWT role claim + NestJS Guards (@Roles decorator) |
| FR-024 | Xá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 SRS | Yêu cầu | Controller | Service | Worker |
| FR-001 | Đăng ký Người dùng | AuthController (AUTH module) | AuthService (AUTH) | - |
| FR-002 | Đăng nhập | AuthController (AUTH module) | AuthService (AUTH) | - |
| FR-003 | Duyệt Việc làm | JobController | JobService | - |
| FR-004 | Nộp Hồ sơ | ApplicationController | ApplicationService, FileService (FILE) | ApplicationConsumer |
| FR-005 | Xem Trạng thái Hồ sơ | ApplicationController | ApplicationService | - |
| FR-006 | Cập nhật Hồ sơ | ProfileController | ProfileService | - |
| FR-007 | Tải lên CV | ProfileController | ProfileService, FileService (FILE) | - |
| FR-008 | Xem Hồ sơ | ApplicationController | ApplicationService, FileService (FILE) | - |
| FR-009 | Sơ tuyển Ứng viên | ApplicationController | ApplicationService | - |
| FR-010 | Lên lịch Phỏng vấn | InterviewController | InterviewService | InterviewConsumer |
| FR-011 | Đánh giá Ứng viên | EvaluationController | EvaluationService | - |
| FR-012 | Từ chối Ứng viên | ApplicationController | ApplicationService | - |
| FR-013 | Theo dõi Quy trình | ApplicationController | ApplicationService | - |
| FR-014 | Tạo Báo cáo | ApplicationController | ApplicationService | - |
| FR-015 | Người dùng tạo tổ chức | AuthController (AUTH module) | AuthService (AUTH) | - |
| FR-016 | Đăng Tin tuyển dụng (Nhà tuyển dụng) | JobController | JobService | JobConsumer |
| FR-017 | Quản lý Tin tuyển dụng | JobController | JobService | - |
| FR-018 | Xem Người nộp đơn | JobController | JobService | - |
| FR-019 | Duyệt Tin tuyển dụng | JobController | JobService | JobConsumer |
| FR-020 | Xem Phân tích | JobController | JobService | - |
| FR-021 | Gói Dịch vụ | SubscriptionController | SubscriptionService | - |
| FR-022 | Mua Gói dịch vụ | SubscriptionController | SubscriptionService | SubscriptionConsumer |
| FR-023 | Kiểm soát Truy cập Gói dịch vụ | - | SubscriptionGuard | - |
| FR-024 | Xác thực Gói dịch vụ | - | SubscriptionGuard | - |
| FR-025 | Gia hạn Gói dịch vụ | SubscriptionController | SubscriptionService | - |
| FR-026 | Lịch sử Gói dịch vụ | SubscriptionController | SubscriptionService | - |
| FR-028 | HR Đăng Tin | JobController | JobService | JobConsumer |
| FR-029 | HR Quản lý Tin | JobController | JobService | - |
| FR-030 | Đánh giá Việc làm | ReviewController | ReviewService | ReviewConsumer |
| FR-031 | Đánh giá Ứng viên | ReviewController | ReviewService | ReviewConsumer |
| FR-032 | Cập nhật Hồ sơ HR | ProfileController | ProfileService | - |
| FR-033 | Cập nhật Hồ sơ Nhà tuyển dụng | ProfileController | ProfileService | - |
| FR-034 | Phân tích Đăng ký Người dùng | AnalyticsController | AnalyticsService | AnalyticsConsumer |
| FR-035 | Phân tích Đăng nhập Người dùng | AnalyticsController | AnalyticsService | AnalyticsConsumer |
| FR-036 | Xem Dashboard | AnalyticsController | AnalyticsService | - |
| FR-037 | Quản lý CV Ứng viên | ProfileController | ProfileService, FileService (FILE) | - |
| FR-038 | Liên kết CV Hồ sơ | ApplicationController | ApplicationService, FileService (FILE) | - |
| NFR-001 | Thời gian Phản hồi API | - | - | - |
| NFR-002 | Xử lý Hồ sơ | - | - | - |
| NFR-003 | Người dùng Đồng thời | - | - | - |
| NFR-004 | Thời gian Hoạt động Hệ thống | - | - | - |
| NFR-005 | RBAC | Tất cả Controllers | - | - |
| NFR-006 | Lưu trữ CV | - | FileService (module FILE) | - |
| NFR-007 | Tìm kiếm Việc làm | JobController | JobService | - |
| FR-039 | Gửi Lời mời | InviteController (AUTH module) | InviteService (AUTH) | InviteConsumer (AUTH) |
| FR-040 | Tạo Token | - | InviteService (AUTH) | - |
| FR-041 | Gửi sự kiện Email | - | InviteService (AUTH) | InviteConsumer (AUTH) |
| FR-042 | Xác minh Token | InviteController (AUTH module) | InviteService (AUTH) | - |
| FR-043 | Chấp nhận Lời mời | InviteController (AUTH module) | InviteService (AUTH) | InviteConsumer (AUTH) |
| FR-044 | Thu hồi Lời mời | InviteController (AUTH module) | InviteService (AUTH) | - |
| FR-045 | Liệt kê Lời mời | InviteController (AUTH module) | InviteService (AUTH) | - |
| FR-046 | Gửi lại Lời mời | InviteController (AUTH module) | InviteService (AUTH) | InviteConsumer (AUTH) |