Hệ thống VinJobs
Tài liệu thiết kế hướng đối tượng — Recruitment & Career Portal
VinJobs là hệ thống tuyển dụng trực tuyến kết nối nhà tuyển dụng với ứng viên. Hệ thống cho phép đăng tin tuyển dụng, ứng tuyển việc làm, quản lý gói dịch vụ thanh toán, và cung cấp nội dung hướng nghiệp qua module blog.
Thông tin dự án
| Hạng mục | Chi tiết |
|---|---|
| Tên dự án | VinJobs — Recruitment & Career Portal |
| Môn học | Lập trình hướng đối tượng (OOP) |
| Ngôn ngữ minh họa | Java |
| Database | MySQL |
| Số module | 3 (Recruitment, Payment, Blog) |
| Số vai trò | 4 (Admin, Employer, Candidate, ContentManager) |
| Design Patterns | Singleton Facade Bridge |
| Tổng class | 25+ |
| Tổng bảng DB | 13 |
Kiến trúc module
Hệ thống chia thành 3 module nghiệp vụ và các core service dùng chung:
| Module | Chức năng | Class chính |
|---|---|---|
| Recruitment | Quản lý tuyển dụng, ứng tuyển, công ty, CV | Job, Application, Company, CV, SavedJob |
| Payment | Gói dịch vụ, thanh toán, hóa đơn | Subscription, Payment, Invoice, EmployerSubscription |
| Blog | Bài viết hướng nghiệp, danh mục | Post, Category |
| Core | Xác thực, thông báo, kết nối DB | AuthService, NotificationService, DatabaseConnection |
Đóng gói (Encapsulation)
Đóng gói là nguyên lý ẩn dữ liệu bên trong class, chỉ cho phép truy cập thông qua các phương thức public
(getter/setter). Trong VinJobs, tất cả thuộc tính đều khai báo private hoặc
protected.
Ví dụ: Class User
public abstract class User { // Thuộc tính protected — chỉ class con truy cập được protected int id; protected String name; protected String email; private String password; // private — ẩn hoàn toàn protected Role role; protected UserStatus status; // Getter public — cho phép đọc từ bên ngoài public String getName() { return this.name; } public String getEmail() { return this.email; } public Role getRole() { return this.role; } // Setter có validation — kiểm soát dữ liệu đầu vào public void setEmail(String email) { if (email == null || !email.contains("@")) throw new IllegalArgumentException("Email không hợp lệ"); this.email = email; } // password không có getter public → bảo mật public boolean verifyPassword(String input) { return BCrypt.checkpw(input, this.password); } }
| Access Modifier | Class | Package | Subclass | Bên ngoài | Dùng trong VinJobs |
|---|---|---|---|---|---|
private |
Co | Khong | Khong | Khong | password, connection (Singleton) |
protected |
Co | Co | Co | Khong | id, name, email, role trong User |
public |
Co | Co | Co | Co | getter, setter, login(), postJob() |
default |
Co | Co | Khong | Khong | Helper methods nội bộ |
Kế thừa (Inheritance)
Kế thừa cho phép class con thừa hưởng thuộc tính và phương thức từ class cha. VinJobs sử dụng kế thừa ở 2
chỗ chính: phân cấp User và phân cấp Notification.
// Class cha — abstract, không thể tạo instance trực tiếp public abstract class User { protected int id; protected String name; protected String email; public boolean login() { /* logic chung */ } public void updateProfile() { /* logic chung */ } } // Class con — kế thừa User, thêm thuộc tính/phương thức riêng public class Employer extends User { private Company company; // thuộc tính riêng private Subscription activeSub; public Job postJob(Job job) { ... } // phương thức riêng public int getRemainingJobSlots() { ... } // Employer vẫn dùng được login(), updateProfile() từ User } public class Candidate extends User { private int experienceYears; private List<CV> cvList; public Application applyJob(int jobId, int cvId) { ... } public List<Job> searchJobs(SearchCriteria criteria) { ... } }
Bảng kế thừa trong VinJobs
| Class cha | Class con | Thuộc tính kế thừa | Phương thức riêng |
|---|---|---|---|
| User (abstract) | Admin | id, name, email, role, status | manageUsers(), blockUser(), getRevenueStats() |
| Employer | id, name, email, role, status | postJob(), purchaseSubscription(), getRemainingJobSlots() | |
| Candidate | id, name, email, role, status | uploadCV(), applyJob(), searchJobs(), saveJob() | |
| ContentManager | id, name, email, role, status | createPost(), editPost(), managePosts() | |
| Notification (abstract) | JobApprovedNotification | id, title, message, sender | send() — nội dung job approved |
| PaymentSuccessNotification | id, title, message, sender | send() — nội dung thanh toán | |
| ApplicationNotification | id, title, message, sender | send() — nội dung ứng tuyển | |
| ApplicationStatusNotification | id, title, message, sender | send() — nội dung đổi trạng thái | |
| PostApprovedNotification | id, title, message, sender | send() — nội dung bài viết |
Đa hình (Polymorphism)
Đa hình cho phép cùng một phương thức có hành vi khác nhau tùy theo class con. VinJobs thể hiện đa hình ở 2 dạng:
a) Override — Ghi đè phương thức
Mỗi loại Notification ghi đè phương thức send() với nội dung khác nhau:
public abstract class Notification { protected MessageSender sender; public abstract void send(); // abstract — bắt buộc class con phải override } public class JobApprovedNotification extends Notification { private Job job; @Override public void send() { sender.sendMessage(employer.getEmail(), "Tin tuyển dụng đã được duyệt", "Tin '" + job.getTitle() + "' đã active"); } } public class PaymentSuccessNotification extends Notification { private Payment payment; @Override public void send() { sender.sendMessage(employer.getEmail(), "Thanh toán thành công", "Số tiền: " + payment.getAmount() + " VNĐ"); } } // Polymorphism — biến kiểu cha, object kiểu con Notification n1 = new JobApprovedNotification(sender, job); Notification n2 = new PaymentSuccessNotification(sender, payment); n1.send(); // gọi JobApprovedNotification.send() n2.send(); // gọi PaymentSuccessNotification.send()
b) Interface — Đa hình qua interface
Interface MessageSender có nhiều implementation với hành vi khác nhau:
public interface MessageSender { boolean sendMessage(String to, String title, String body); } public class EmailSender implements MessageSender { @Override public boolean sendMessage(String to, String title, String body) { // Gửi qua SMTP server return true; } } public class SmsSender implements MessageSender { @Override public boolean sendMessage(String to, String title, String body) { // Gửi qua SMS API (Twilio, ...) return true; } } // Cùng kiểu MessageSender, hành vi khác nhau MessageSender s1 = new EmailSender(); MessageSender s2 = new SmsSender(); s1.sendMessage("a@b.com", "Hi", "..."); // gửi email s2.sendMessage("0901...", "Hi", "..."); // gửi SMS
Trừu tượng (Abstraction)
Trừu tượng là ẩn đi chi tiết thực thi, chỉ cung cấp giao diện sử dụng. VinJobs thể hiện qua abstract class và interface:
| Loại | Class/Interface | Phần trừu tượng | Phần cụ thể (class con) |
|---|---|---|---|
| Abstract Class | User |
Định nghĩa login(), updateProfile() | Admin, Employer, Candidate, ContentManager |
| Abstract Class | Notification |
Định nghĩa send() — abstract | JobApproved.., PaymentSuccess.., Application.. |
| Interface | MessageSender |
Định nghĩa sendMessage() | EmailSender, SmsSender, PushSender |
// Abstract class — không thể new User() trực tiếp User u1 = new User(); // LỖI COMPILE User u2 = new Employer(); // OK — tạo qua class con User u3 = new Candidate(); // OK // Interface — không thể new MessageSender() trực tiếp MessageSender s = new EmailSender(); // OK — tạo qua implementation
Candidate
Người tìm việc — đăng ký, tạo hồ sơ, ứng tuyển.
| STT | Chức năng | Mô tả | Phương thức |
|---|---|---|---|
| 1 | Đăng ký, đăng nhập | Tạo tài khoản, xác thực | login(), register() |
| 2 | Tạo hồ sơ | Thông tin cá nhân, kinh nghiệm | createProfile() |
| 3 | Upload CV | Tải lên file PDF/DOCX | uploadCV(file) |
| 4 | Tìm kiếm việc làm | Theo từ khóa, vị trí, lương | searchJobs(criteria) |
| 5 | Ứng tuyển | Gửi hồ sơ ứng tuyển | applyJob(jobId, cvId) |
| 6 | Theo dõi trạng thái | Xem trạng thái đơn | trackApplications() |
| 7 | Lưu việc làm | Bookmark job yêu thích | saveJob(jobId) |
Employer
Nhà tuyển dụng — tạo công ty, mua gói, đăng tin, quản lý ứng viên.
| STT | Chức năng | Mô tả | Phương thức |
|---|---|---|---|
| 1 | Tạo công ty | Đăng ký thông tin công ty | createCompany(data) |
| 2 | Mua gói đăng tuyển | Chọn và thanh toán gói | purchaseSubscription(id) |
| 3 | Đăng tin tuyển dụng | Tạo bài đăng (yêu cầu gói) | postJob(job) |
| 4 | Quản lý tin | Sửa, ẩn, xóa tin | manageJobs() |
| 5 | Xem ứng viên | Danh sách ứng viên theo job | viewApplicants(jobId) |
| 6 | Duyệt / Từ chối | Đổi trạng thái ứng viên | approveApplication(id) |
Gói dịch vụ
| Gói | Giá | Số bài | Thời hạn |
|---|---|---|---|
| Basic | 200.000 đ | 5 | 30 ngày |
| Premium | 500.000 đ | 20 | 60 ngày |
| Enterprise | 2.000.000 đ | Unlimited | 365 ngày |
Content Manager
Quản lý nội dung blog — viết bài hướng nghiệp, tin tức ngành.
| STT | Chức năng | Mô tả | Phương thức |
|---|---|---|---|
| 1 | Viết bài blog | Soạn bài hướng nghiệp | createPost(post) |
| 2 | Đăng tin tức | Xu hướng ngành, tips | createPost(post) |
| 3 | Quản lý bài viết | Sửa, xóa, ẩn bài | editPost(), deletePost() |
| 4 | Quản lý danh mục | Tạo và sửa category | manageCategories() |
Admin
Quản trị toàn bộ hệ thống.
| STT | Chức năng | Mô tả | Phương thức |
|---|---|---|---|
| 1 | Quản lý user | CRUD tất cả người dùng | manageUsers() |
| 2 | Quản lý công ty | Duyệt, khóa công ty | manageCompanies() |
| 3 | Quản lý gói | Tạo/sửa subscription | manageSubscriptions() |
| 4 | Quản lý thanh toán | Lịch sử, hoàn tiền | managePayments() |
| 5 | Duyệt bài viết | Approve/reject blog | approvePost(id) |
| 6 | Thống kê | Dashboard doanh thu | getRevenueStats() |
| 7 | Khóa tài khoản | Ban user vi phạm | blockUser(id) |
Use Case — Candidate
Use Case — Employer
Use Case — Admin & Content Manager
Class Diagram — Phân cấp User
Chi tiết thuộc tính — User (Abstract)
| Modifier | Thuộc tính | Kiểu | Mô tả | Ràng buộc |
|---|---|---|---|---|
| # | id | int | Khóa chính | Auto increment |
| # | name | String | Họ tên | NOT NULL, max 100 |
| # | String | Email đăng nhập | UNIQUE, NOT NULL | |
| - | password | String | Mật khẩu (hashed) | NOT NULL, min 6 ký tự |
| # | phone | String | Số điện thoại | Optional |
| # | avatar | String | URL ảnh đại diện | Optional |
| # | role | Role | Vai trò | Enum, NOT NULL |
| # | status | UserStatus | Trạng thái tài khoản | Default: PENDING |
| # | createdAt | DateTime | Ngày tạo | Auto |
Chi tiết thuộc tính riêng — Employer
| Modifier | Thuộc tính | Kiểu | Mô tả |
|---|---|---|---|
| - | position | String | Chức vụ tại công ty |
| - | company | Company | Công ty đã tạo (0..1) |
| - | activeSubscription | Subscription | Gói đang sử dụng |
Chi tiết thuộc tính riêng — Candidate
| Modifier | Thuộc tính | Kiểu | Mô tả |
|---|---|---|---|
| - | title | String | Chức danh mong muốn |
| - | summary | String | Tóm tắt bản thân |
| - | experienceYears | int | Số năm kinh nghiệm |
| - | cvList | List<CV> | Danh sách CV đã upload |
| - | savedJobs | List<SavedJob> | Việc làm đã lưu |
Class Diagram — Recruitment Module
Class Diagram — Payment Module
Class Diagram — Blog Module
Class Diagram — Notification (Bridge)
Class Diagram — Tổng thể hệ thống
Phân tích quan hệ OOP
Hệ thống sử dụng các loại quan hệ sau:
| Loại quan hệ | Ký hiệu UML | Ý nghĩa | Ví dụ trong VinJobs |
|---|---|---|---|
| Inheritance (Kế thừa) | ——▷ | Class con IS-A class cha | Employer IS-A User |
| Realization (Hiện thực) | - - ▷ | Class implement interface | EmailSender implements MessageSender |
| Association (Liên kết) | ——> | Class A biết đến Class B | Job --> Company |
| Aggregation (Gom nhóm) | ◇——> | Whole-Part, part tồn tại độc lập | Notification ◇--> MessageSender |
| Composition (Hợp thành) | ◆——> | Whole-Part, part phụ thuộc whole | Payment ◆--> Invoice |
| Dependency (Phụ thuộc) | - - -> | Class A dùng Class B tạm thời | Repository ..> DatabaseConnection |
Bảng quan hệ chi tiết
| Class A | Quan hệ | Bội số | Class B | Loại | Giải thích |
|---|---|---|---|---|---|
| User | Inheritance | — | Admin, Employer, Candidate, CM | IS-A | Class con kế thừa User |
| Notification | Inheritance | — | 5 loại Notification con | IS-A | Override phương thức send() |
| MessageSender | Realization | — | Email, Sms, Push Sender | Implements | Implement interface |
| Employer | Association | 1 — 0..1 | Company | HAS-A | Mỗi employer tạo tối đa 1 công ty |
| Company | Association | 1 — * | Job | HAS-MANY | Mỗi công ty có nhiều tin |
| Candidate | Association | 1 — * | Application | HAS-MANY | Mỗi ứng viên gửi nhiều đơn |
| Job | Association | 1 — * | Application | HAS-MANY | Mỗi job nhận nhiều đơn |
| Candidate | Association | 1 — * | CV | HAS-MANY | Upload nhiều CV |
| Application | Association | * — 1 | CV | USES | Mỗi đơn đính kèm 1 CV |
| Employer | Association | 1 — * | Payment | HAS-MANY | Nhiều giao dịch |
| Payment | Composition | 1 — 1 | Invoice | OWNS | Invoice phụ thuộc Payment |
| Payment | Association | * — 1 | Subscription | FOR | Mua 1 gói |
| Employer | Association | 1 — 0..1 | EmployerSubscription | HAS | Gói active hiện tại |
| ContentManager | Association | 1 — * | Post | HAS-MANY | Viết nhiều bài |
| Post | Association | * — 1 | Category | BELONGS-TO | Thuộc 1 danh mục |
| Category | Self-ref | 1 — * | Category | PARENT | Danh mục cha-con |
| Notification | Aggregation | * — 1 | MessageSender | BRIDGE | Kênh gửi tách biệt |
| Repository | Dependency | — | DatabaseConnection | USES | Dùng Singleton |
ERD — Entity Relationship Diagram
Danh sách 13 bảng
| STT | Bảng | Mô tả | Số cột | FK |
|---|---|---|---|---|
| 1 | users |
Tất cả người dùng | 9 | — |
| 2 | companies |
Thông tin công ty | 11 | employer_id |
| 3 | jobs |
Tin tuyển dụng | 14 | company_id, employer_id |
| 4 | applications |
Đơn ứng tuyển | 8 | candidate_id, job_id, cv_id |
| 5 | cvs |
File CV | 7 | candidate_id |
| 6 | saved_jobs |
Việc làm đã lưu | 4 | candidate_id, job_id |
| 7 | subscriptions |
Gói dịch vụ | 7 | — |
| 8 | payments |
Giao dịch | 8 | employer_id, subscription_id |
| 9 | invoices |
Hóa đơn | 6 | payment_id |
| 10 | employer_subscriptions |
Gói active | 8 | employer_id, subscription_id |
| 11 | posts |
Bài viết blog | 11 | author_id, category_id |
| 12 | categories |
Danh mục | 6 | parent_id (self) |
| 13 | notifications |
Thông báo | 7 | user_id |
Enums
| Enum | Giá trị | Mô tả |
|---|---|---|
| Role | ADMIN | Quản trị viên |
| EMPLOYER | Nhà tuyển dụng | |
| CANDIDATE | Ứng viên | |
| CONTENT_MANAGER | Người quản lý nội dung | |
| JobStatus | DRAFT | Bản nháp, chưa gửi duyệt |
| PENDING | Chờ Admin duyệt | |
| APPROVED | Đã duyệt, hiển thị công khai | |
| REJECTED | Bị từ chối | |
| EXPIRED | Quá deadline | |
| CLOSED | Employer đóng | |
| ApplicationStatus | PENDING | Chờ xem xét |
| REVIEWING | Đang xem xét | |
| SHORTLISTED | Vào danh sách ngắn | |
| APPROVED | Đã duyệt | |
| REJECTED | Bị từ chối | |
| WITHDRAWN | Ứng viên rút đơn | |
| PaymentMethod | BANK_TRANSFER | Chuyển khoản ngân hàng |
| MOMO | Ví MoMo | |
| VNPAY | VNPay | |
| CREDIT_CARD | Thẻ tín dụng | |
| ZALOPAY | ZaloPay | |
| PostStatus | DRAFT | Bản nháp |
| PENDING_REVIEW | Chờ Admin duyệt | |
| PUBLISHED | Đã xuất bản | |
| REJECTED | Bị từ chối | |
| ARCHIVED | Đã lưu trữ |
SQL DDL — Hoàn chỉnh 13 bảng
CREATE DATABASE IF NOT EXISTS vinjobs CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE vinjobs; -- 1. USERS CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, email VARCHAR(150) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL, phone VARCHAR(20), avatar VARCHAR(500), role ENUM('ADMIN','EMPLOYER','CANDIDATE','CONTENT_MANAGER') NOT NULL, status ENUM('ACTIVE','INACTIVE','BLOCKED','PENDING') DEFAULT 'PENDING', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 2. COMPANIES CREATE TABLE companies ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(200) NOT NULL, description TEXT, logo VARCHAR(500), website VARCHAR(300), address VARCHAR(500), industry VARCHAR(100), size INT DEFAULT 0, status ENUM('PENDING','VERIFIED','SUSPENDED') DEFAULT 'PENDING', employer_id INT UNIQUE NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (employer_id) REFERENCES users(id) ON DELETE CASCADE ); -- 3. SUBSCRIPTIONS CREATE TABLE subscriptions ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, description TEXT, price DECIMAL(12,2) NOT NULL, job_limit INT NOT NULL DEFAULT 0, duration INT NOT NULL, is_active BOOLEAN DEFAULT TRUE ); INSERT INTO subscriptions (name, description, price, job_limit, duration) VALUES ('Basic', 'Gói cơ bản - 5 bài/30 ngày', 200000.00, 5, 30), ('Premium', 'Gói cao cấp - 20 bài/60 ngày', 500000.00, 20, 60), ('Enterprise', 'Gói DN - Không giới hạn', 2000000.00, 0, 365); -- 4. PAYMENTS CREATE TABLE payments ( id INT PRIMARY KEY AUTO_INCREMENT, employer_id INT NOT NULL, subscription_id INT NOT NULL, amount DECIMAL(12,2) NOT NULL, payment_method ENUM('BANK_TRANSFER','MOMO','VNPAY','CREDIT_CARD','ZALOPAY') NOT NULL, status ENUM('PENDING','COMPLETED','FAILED','REFUNDED') DEFAULT 'PENDING', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, paid_at DATETIME, FOREIGN KEY (employer_id) REFERENCES users(id), FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ); -- 5. INVOICES CREATE TABLE invoices ( id INT PRIMARY KEY AUTO_INCREMENT, payment_id INT UNIQUE NOT NULL, invoice_number VARCHAR(50) UNIQUE NOT NULL, total_amount DECIMAL(12,2) NOT NULL, tax DECIMAL(12,2) DEFAULT 0.00, issued_date DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (payment_id) REFERENCES payments(id) ); -- 6. EMPLOYER_SUBSCRIPTIONS CREATE TABLE employer_subscriptions ( id INT PRIMARY KEY AUTO_INCREMENT, employer_id INT NOT NULL, subscription_id INT NOT NULL, jobs_used INT DEFAULT 0, jobs_remaining INT NOT NULL, start_date DATETIME NOT NULL, end_date DATETIME NOT NULL, status ENUM('ACTIVE','EXPIRED','CANCELLED') DEFAULT 'ACTIVE', FOREIGN KEY (employer_id) REFERENCES users(id), FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ); -- 7. JOBS CREATE TABLE jobs ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, description TEXT NOT NULL, requirements TEXT, location VARCHAR(200), salary_min DECIMAL(12,2), salary_max DECIMAL(12,2), type ENUM('FULL_TIME','PART_TIME','CONTRACT','INTERNSHIP','REMOTE') DEFAULT 'FULL_TIME', level ENUM('INTERN','JUNIOR','MIDDLE','SENIOR','LEAD','MANAGER') DEFAULT 'JUNIOR', status ENUM('DRAFT','PENDING','APPROVED','REJECTED','EXPIRED','CLOSED') DEFAULT 'DRAFT', company_id INT NOT NULL, employer_id INT NOT NULL, deadline DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (company_id) REFERENCES companies(id), FOREIGN KEY (employer_id) REFERENCES users(id) ); -- 8. CVS CREATE TABLE cvs ( id INT PRIMARY KEY AUTO_INCREMENT, candidate_id INT NOT NULL, title VARCHAR(200) NOT NULL, file_path VARCHAR(500) NOT NULL, file_type VARCHAR(20) DEFAULT 'PDF', is_primary BOOLEAN DEFAULT FALSE, uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (candidate_id) REFERENCES users(id) ); -- 9. APPLICATIONS CREATE TABLE applications ( id INT PRIMARY KEY AUTO_INCREMENT, candidate_id INT NOT NULL, job_id INT NOT NULL, cv_id INT NOT NULL, cover_letter TEXT, status ENUM('PENDING','REVIEWING','SHORTLISTED','APPROVED','REJECTED','WITHDRAWN') DEFAULT 'PENDING', applied_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY (candidate_id, job_id), FOREIGN KEY (candidate_id) REFERENCES users(id), FOREIGN KEY (job_id) REFERENCES jobs(id), FOREIGN KEY (cv_id) REFERENCES cvs(id) ); -- 10. SAVED_JOBS CREATE TABLE saved_jobs ( id INT PRIMARY KEY AUTO_INCREMENT, candidate_id INT NOT NULL, job_id INT NOT NULL, saved_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY (candidate_id, job_id), FOREIGN KEY (candidate_id) REFERENCES users(id), FOREIGN KEY (job_id) REFERENCES jobs(id) ); -- 11. CATEGORIES CREATE TABLE categories ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, slug VARCHAR(100) UNIQUE NOT NULL, description TEXT, parent_id INT, is_active BOOLEAN DEFAULT TRUE, FOREIGN KEY (parent_id) REFERENCES categories(id) ); -- 12. POSTS CREATE TABLE posts ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(300) NOT NULL, content TEXT NOT NULL, thumbnail VARCHAR(500), slug VARCHAR(300) UNIQUE NOT NULL, status ENUM('DRAFT','PENDING_REVIEW','PUBLISHED','REJECTED','ARCHIVED') DEFAULT 'DRAFT', author_id INT NOT NULL, category_id INT, view_count INT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, published_at DATETIME, FOREIGN KEY (author_id) REFERENCES users(id), FOREIGN KEY (category_id) REFERENCES categories(id) ); -- 13. NOTIFICATIONS CREATE TABLE notifications ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, title VARCHAR(200) NOT NULL, message TEXT NOT NULL, type VARCHAR(50) NOT NULL, is_read BOOLEAN DEFAULT FALSE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ); -- INDEXES CREATE INDEX idx_jobs_company ON jobs(company_id); CREATE INDEX idx_jobs_status ON jobs(status); CREATE INDEX idx_jobs_employer ON jobs(employer_id); CREATE INDEX idx_applications_candidate ON applications(candidate_id); CREATE INDEX idx_applications_job ON applications(job_id); CREATE INDEX idx_payments_employer ON payments(employer_id); CREATE INDEX idx_posts_author ON posts(author_id); CREATE INDEX idx_posts_status ON posts(status); CREATE INDEX idx_notifications_user ON notifications(user_id); CREATE INDEX idx_notifications_read ON notifications(is_read);
Singleton — DatabaseConnection
Vấn đề: Hệ thống có nhiều module (User, Job, Payment, Blog, Notification), mỗi module cần kết nối database. Nếu mỗi module tạo connection riêng sẽ lãng phí tài nguyên.
Giải pháp: Dùng Singleton để đảm bảo toàn bộ hệ thống chỉ tạo đúng 1 instance kết nối database.
Code Java
public class DatabaseConnection { // Biến static — lưu instance duy nhất private static DatabaseConnection instance; private Connection connection; private String host = "localhost"; private String database = "vinjobs"; // Constructor PRIVATE — không thể gọi từ bên ngoài private DatabaseConnection() { try { String url = "jdbc:mysql://" + host + ":3306/" + database; this.connection = DriverManager.getConnection(url, "root", "password"); System.out.println("Kết nối database thành công"); } catch (SQLException e) { throw new RuntimeException("Không thể kết nối database", e); } } // Phương thức static — điểm truy cập duy nhất // synchronized — đảm bảo thread-safe trong môi trường đa luồng public static synchronized DatabaseConnection getInstance() { if (instance == null) { instance = new DatabaseConnection(); } return instance; } public Connection getConnection() { return this.connection; } public boolean isConnected() { try { return connection != null && !connection.isClosed(); } catch (SQLException e) { return false; } } public void closeConnection() { try { if (connection != null) connection.close(); } catch (SQLException e) { e.printStackTrace(); } } } // === Sử dụng === DatabaseConnection db1 = DatabaseConnection.getInstance(); DatabaseConnection db2 = DatabaseConnection.getInstance(); System.out.println(db1 == db2); // true — cùng 1 object
Facade — RecruitmentFacade
Vấn đề: Khi Employer mua gói dịch vụ, client phải gọi lần lượt AuthService, SubscriptionService, PaymentService, NotificationService — phức tạp và dễ sai thứ tự.
Giải pháp: Tạo RecruitmentFacade gom tất cả service, cung cấp 1 phương thức đơn giản cho mỗi tác vụ phức tạp.
Code Java — purchasePlan()
public class RecruitmentFacade { private AuthService authService; private SubscriptionService subscriptionService; private PaymentService paymentService; private NotificationService notificationService; // ... các service khác public Invoice purchasePlan(int employerId, int planId, PaymentMethod method) { // Bước 1: Kiểm tra employer hợp lệ User employer = authService.verifyUser(employerId); if (employer.getRole() != Role.EMPLOYER) throw new UnauthorizedException("Chỉ employer mới được mua gói"); // Bước 2: Lấy thông tin gói Subscription plan = subscriptionService.getPlanById(planId); // Bước 3: Tạo payment Payment payment = paymentService.createPayment( new PaymentData(employerId, planId, plan.getPrice(), method)); // Bước 4: Xử lý thanh toán boolean success = paymentService.processPayment(payment.getId()); if (!success) throw new PaymentFailedException("Thanh toán thất bại"); // Bước 5: Kích hoạt subscription subscriptionService.activateSubscription(employerId, planId); // Bước 6: Tạo hóa đơn Invoice invoice = paymentService.generateInvoice(payment.getId()); // Bước 7: Gửi thông báo notificationService.sendNotification(employerId, "Mua gói thành công", "Đã kích hoạt gói " + plan.getName(), "PAYMENT_SUCCESS"); return invoice; } public Application applyForJob(int candidateId, int jobId, int cvId) { User candidate = authService.verifyUser(candidateId); Job job = jobService.getJobById(jobId); if (job.isExpired()) throw new JobExpiredException("Job đã hết hạn"); if (applicationService.checkDuplicate(candidateId, jobId)) throw new DuplicateException("Đã ứng tuyển rồi"); Application app = applicationService.apply(candidateId, jobId, cvId); notificationService.sendNotification(job.getEmployerId(), "Có ứng viên mới", candidate.getName() + " đã ứng tuyển", "NEW_APPLICATION"); return app; } } // === Client chỉ cần 1 dòng === RecruitmentFacade facade = new RecruitmentFacade(...); Invoice inv = facade.purchasePlan(employerId, 2, PaymentMethod.MOMO); Application app = facade.applyForJob(candidateId, jobId, cvId);
So sánh
| Tiêu chí | Không dùng Facade | Dùng Facade |
|---|---|---|
| Số lệnh gọi | Client gọi 7 service riêng | Client gọi 1 method |
| Thứ tự | Client phải biết thứ tự đúng | Facade xử lý điều phối |
| Coupling | Cao — client phụ thuộc 7 service | Thấp — client chỉ biết Facade |
| Bảo trì | Thay đổi 1 service ảnh hưởng client | Chỉ sửa trong Facade |
| Lỗi | Dễ quên bước, sai thứ tự | Logic tập trung, ít lỗi |
Bridge — Notification System
Vấn đề: Hệ thống có 5 loại thông báo (JobApproved, PaymentSuccess, Application, ApplicationStatus, PostApproved) và 3 kênh gửi (Email, SMS, Push). Nếu tạo class cho mỗi tổ hợp sẽ cần 5 x 3 = 15 class.
Giải pháp: Dùng Bridge tách "loại thông báo" (Abstraction) khỏi "kênh gửi" (Implementation). Chỉ cần 5 + 3 = 8 class.
Ma trận kết hợp
| Loại thông báo \ Kênh | EmailSender | SmsSender | PushSender |
|---|---|---|---|
| JobApprovedNotification | x | x | x |
| PaymentSuccessNotification | x | x | x |
| ApplicationNotification | x | x | x |
| ApplicationStatusNotification | x | x | x |
| PostApprovedNotification | x | x | x |
15 tổ hợp nhưng chỉ cần 8 class (5 Notification + 3 Sender).
Code Java
// === INTERFACE: MessageSender (Implementation) === public interface MessageSender { boolean sendMessage(String to, String title, String body); String getSenderType(); } public class EmailSender implements MessageSender { private String smtpHost = "smtp.gmail.com"; private int smtpPort = 587; @Override public boolean sendMessage(String to, String title, String body) { System.out.println("[EMAIL] -> " + to + ": " + title); // Gửi email qua SMTP return true; } public String getSenderType() { return "EMAIL"; } } public class SmsSender implements MessageSender { private String apiKey; @Override public boolean sendMessage(String to, String title, String body) { System.out.println("[SMS] -> " + to + ": " + title); return true; } public String getSenderType() { return "SMS"; } } public class PushSender implements MessageSender { private String fcmToken; @Override public boolean sendMessage(String to, String title, String body) { System.out.println("[PUSH] -> " + to + ": " + title); return true; } public String getSenderType() { return "PUSH"; } } // === ABSTRACT CLASS: Notification (Abstraction) === public abstract class Notification { protected int id; protected String title; protected String message; protected int userId; protected MessageSender sender; // BRIDGE — tham chiếu đến implementation public Notification(MessageSender sender) { this.sender = sender; } public abstract void send(); } // === CONCRETE: PaymentSuccessNotification === public class PaymentSuccessNotification extends Notification { private Payment payment; private Subscription plan; public PaymentSuccessNotification(MessageSender sender, Payment payment, Subscription plan) { super(sender); this.payment = payment; this.plan = plan; } @Override public void send() { String contact = getUserContact(payment.getEmployerId()); this.title = "Thanh toán thành công"; this.message = "Bạn đã mua gói " + plan.getName() + ". Số tiền: " + payment.getAmount() + " VNĐ"; sender.sendMessage(contact, this.title, this.message); } } // === Sử dụng === // Gửi qua email Notification n1 = new PaymentSuccessNotification( new EmailSender(), payment, plan); n1.send(); // [EMAIL] -> employer@gmail.com: Thanh toán thành công // Gửi qua SMS — chỉ cần đổi sender Notification n2 = new PaymentSuccessNotification( new SmsSender(), payment, plan); n2.send(); // [SMS] -> 0901234567: Thanh toán thành công // Gửi push notification Notification n3 = new PaymentSuccessNotification( new PushSender(), payment, plan); n3.send(); // [PUSH] -> fcm_token: Thanh toán thành công
So sánh
| Tiêu chí | Không dùng Bridge | Dùng Bridge |
|---|---|---|
| Số class | 5 x 3 = 15 class | 5 + 3 = 8 class |
| Thêm kênh mới | Thêm 5 class | Thêm 1 class (implement MessageSender) |
| Thêm loại mới | Thêm 3 class | Thêm 1 class (extend Notification) |
| Mở rộng | Class explosion | Linh hoạt, dễ mở rộng |
Sequence: Mua gói dịch vụ
Sequence: Ứng tuyển
Sequence: Đăng tin tuyển dụng
Sequence: Đăng ký tài khoản
Sequence: Singleton
Sequence: Bridge — Gửi đa kênh
State Diagram — Application
Vòng đời trạng thái của đơn ứng tuyển:
State Diagram — Job
Vòng đời tin tuyển dụng:
State Diagram — Post
Vòng đời bài viết blog:
Activity Diagram — Flow ứng tuyển
Activity Diagram — Flow mua gói
Tổng kết
| Hạng mục | Số lượng | Chi tiết |
|---|---|---|
| Tong class | 25+ | User hierarchy, Recruitment, Payment, Blog, Notification, Service, Repository |
| Abstract Class | 2 | User, Notification |
| Interface | 1 | MessageSender |
| Enum | 10 | Role, UserStatus, JobStatus, JobType, JobLevel, ApplicationStatus, PaymentMethod, PaymentStatus, PostStatus, CompanyStatus |
| Database Table | 13 | users, companies, jobs, applications, cvs, saved_jobs, subscriptions, payments, invoices, employer_subscriptions, posts, categories, notifications |
| Design Pattern | 3 | Singleton (DatabaseConnection), Facade (RecruitmentFacade), Bridge (Notification + MessageSender) |
| Module | 3 + Core | Recruitment, Payment, Blog, Core Services |
| Actor | 4 | Admin, Employer, Candidate, ContentManager |
| Diagram | 20+ | Class (6), ERD (1), Enum (1), Use Case (3), Sequence (6), State (3), Activity (2) |
Checklist OOP
| Tiêu chí | Nội dung |
|---|---|
| Encapsulation | Thuộc tính private/protected, getter/setter có validation |
| Inheritance | User -> Admin, Employer, Candidate, ContentManager; Notification -> 5 loại |
| Polymorphism | Override send() trong mỗi Notification; Interface MessageSender |
| Abstraction | Abstract class User, Notification; Interface MessageSender |
| Singleton | DatabaseConnection — 1 instance duy nhất |
| Facade | RecruitmentFacade — gom 8 service |
| Bridge | Notification + MessageSender — tách abstraction và implementation |