Kiến trúc hệ thống
Trang này dành cho người lập trình. Viết theo mẫu arc42 rút gọn.
Ràng buộc chi phối thiết kế
| Ràng buộc | Ảnh hưởng tới kiến trúc |
|---|---|
| Người dùng không chuyên | Giao diện đơn giản, ít bước, thông báo lỗi bằng tiếng Việt dễ hiểu |
| Chạy trên máy chủ riêng của trường | Không dùng dịch vụ tính phí theo số người dùng |
| Quy mô một văn phòng, dưới 50 người dùng | Không cần kiến trúc phân tán, một máy chủ là đủ |
| Phải mở rộng được ra nhiều phòng ban | Phạm vi dữ liệu tính theo cây phòng ban ngay từ đầu |
| Toàn bộ tiếng Việt | Tách chuỗi hiển thị ra tệp riêng từ đầu |
Tổng quan
Trình duyệt (máy cán bộ)
│ HTTPS
▼
nginx ── phục vụ tệp tĩnh + chuyển tiếp /api
│
├──→ Giao diện — React tĩnh (đã dựng sẵn)
│
└──→ Máy chủ ứng dụng — NestJS + TypeScript
│
├──→ PostgreSQL 16 (dữ liệu)
├──→ Thư mục tệp đính kèm (đĩa máy chủ)
└──→ Tác vụ định kỳ (quét hạn, tạo thông báo)
Lựa chọn công nghệ
| Thành phần | Chọn | Lý do |
|---|---|---|
| Giao diện | React + TypeScript + Vite, nền shadcn-admin | Có sẵn màn hình danh sách công việc, bộ lọc, quản lý người dùng. Giấy phép MIT. Dựng ra tệp tĩnh, không cần chạy thường trú |
| Máy chủ ứng dụng | NestJS + TypeScript, chạy trên nền Fastify | Cấu trúc phân lớp sẵn có, hợp với đội nhiều người. Cùng ngôn ngữ với giao diện nên dùng chung kiểu dữ liệu |
| Cơ sở dữ liệu | PostgreSQL 16 | Xem lý do chi tiết |
| Truy cập dữ liệu | Prisma | Sinh kiểu dữ liệu tự động, quản lý chuyển đổi cấu trúc bảng rõ ràng |
| Xác thực | Tự làm, dùng JWT | Bản gốc shadcn-admin dùng dịch vụ Clerk tính phí theo người dùng, phải gỡ |
| Xuất Excel | ExcelJS | Xuất được tệp có định dạng, không chỉ CSV |
| Tài liệu API | @nestjs/swagger | Sinh tự động từ mã nguồn, không lệch với thực tế |
Vì sao dùng chung một ngôn ngữ cho cả hai đầu
Dùng TypeScript cả giao diện lẫn máy chủ cho phép định nghĩa kiểu dữ liệu một lần rồi dùng chung. Đổi tên một trường thì trình biên dịch báo lỗi ở mọi chỗ liên quan, thay vì để lỗi lộ ra lúc chạy.
Vì sao NestJS
NestJS áp đặt sẵn cách chia mã nguồn thành mô-đun, tầng điều khiển, tầng dịch vụ. Với dự án nhiều người cùng làm, việc này quan trọng hơn là được tự do sắp xếp: người mới vào nhìn thư mục là biết mã nguồn nằm ở đâu.
Ba thứ NestJS có sẵn khớp thẳng với nhu cầu của ADang:
| Cơ chế sẵn có | Dùng cho |
|---|---|
| Guard | Tầng phân quyền — chặn trước khi vào tầng nghiệp vụ |
| ValidationPipe | Kiểm tra dữ liệu đầu vào, trả lỗi tiếng Việt theo mẫu thống nhất |
| @nestjs/schedule | Tác vụ định kỳ quét hạn, không cần công cụ ngoài |
Chạy NestJS trên nền Fastify thay vì Express mặc định, vì Fastify xử lý nhanh hơn và đã có kinh nghiệm vận hành trên chính máy chủ này.
Vì sao không dùng Next.js
Giao diện là ứng dụng nội bộ sau màn đăng nhập, không cần công cụ tìm kiếm đọc được, cũng không cần dựng trang phía máy chủ. Dựng ra tệp tĩnh thì nginx phục vụ trực tiếp, không cần tiến trình Node chạy thường trú cho phần giao diện. Đơn giản hơn khi vận hành.
Phân lớp phía máy chủ
Controller — nhận yêu cầu, kiểm tra dữ liệu đầu vào (ValidationPipe)
↓
Guard — kiểm tra đăng nhập, vai trò và phạm vi dữ liệu
↓
Service — quy tắc nghiệp vụ, luồng trạng thái
↓
Repository — Prisma, truy vấn PostgreSQL
Mỗi nhóm chức năng là một mô-đun NestJS riêng, khớp với 5 nhóm trong danh sách chức năng:
src/
├── auth/ đăng nhập, phiên làm việc
├── users/ tài khoản, phòng ban, vai trò
├── tasks/ công việc, bình luận, tệp đính kèm, lịch sử
├── dashboard/ màn hình tổng quan, bộ lọc
├── notifications/ thông báo, tác vụ định kỳ
├── reports/ xuất Excel
└── common/ guard, pipe, bộ lọc lỗi dùng chung
Tầng phân quyền tách riêng và bắt buộc đi qua. Đây là chỗ hay bị bỏ sót nhất. Ẩn nút trên giao diện không phải là phân quyền — người dùng sửa địa chỉ trên trình duyệt là vào được. Mọi yêu cầu đọc hay ghi dữ liệu công việc đều phải qua tầng này.
Cài đặt bằng hai Guard nối tiếp, khai báo ở cấp mô-đun để không thể quên:
| Guard | Việc |
|---|---|
JwtAuthGuard | Xác định người dùng từ phiên đăng nhập |
ScopeGuard | Tính tập phòng ban được phép, gắn vào yêu cầu |
Quy tắc: hàm truy vấn công việc trong tầng Repository luôn nhận tham số phạm vi, không có phiên bản nào lấy tất cả. Nếu một hàm không nhận tham số này thì đó là lỗi thiết kế, không phải tiện lợi.
Cách tính phạm vi dữ liệu
Mỗi yêu cầu được xử lý theo ba bước:
- Lấy vai trò và phòng ban của người dùng từ
user_departments - Tính tập phòng ban họ được xem, bằng cách duyệt cây từ phòng của họ xuống
- Ghép điều kiện đó vào mọi truy vấn công việc
| Vai trò | Tập phòng ban |
|---|---|
| Ban Giám hiệu | Toàn bộ cây từ gốc |
| Trưởng phòng | Phòng mình và các phòng con |
| Nhân viên | Không tính theo phòng — lọc theo task_assignees.user_id |
Tác vụ định kỳ
Dùng @nestjs/schedule trong mô-đun notifications, chạy cứ 15 phút một lần:
| Việc | Nội dung |
|---|---|
| Nhắc trước hạn | Tìm việc còn đúng một ngày tới hạn, tạo thông báo |
| Cảnh báo quá hạn | Tìm việc đã qua hạn mà chưa xong, báo cho người phụ trách và người giao |
| Gửi thông báo | Đẩy các thông báo chưa gửi |
Tách phần tạo thông báo khỏi phần gửi thông báo. Nhờ vậy khi thêm kênh Zalo ở giai đoạn sau, chỉ viết thêm phần gửi, phần tạo giữ nguyên.
Bảo mật
| Vấn đề | Cách xử lý |
|---|---|
| Mật khẩu | Mã hoá bằng bcrypt, không lưu bản gốc |
| Phiên đăng nhập | JWT lưu trong cookie HttpOnly, hết hạn sau 8 giờ |
| Phân quyền | Kiểm tra ở máy chủ cho mọi yêu cầu |
| Tệp đính kèm | Lưu ngoài thư mục web, tải qua đường dẫn có kiểm tra quyền |
| Ghi vết | Mọi thao tác ghi dữ liệu vào activity_log |
| Đường truyền | Bắt buộc HTTPS |
Triển khai
| Môi trường | Nơi chạy |
|---|---|
| Máy cá nhân | Vite chạy sẵn, PostgreSQL trong Docker |
| Thử nghiệm | stg.sotaylop.com |
| Chính thức | sotaylop.com — cùng máy chủ với bản thử, nhưng cổng và cơ sở dữ liệu riêng |
Chi tiết xem Phân chia môi trường.
Những gì cố tình chưa làm
Ghi lại để người sau không tưởng là thiếu sót.
| Không làm | Vì sao | Khi nào cần xem lại |
|---|---|---|
| Bộ nhớ đệm Redis | Dưới 50 người dùng, PostgreSQL thừa sức | Khi vượt 500 người dùng |
| Tách nhiều dịch vụ nhỏ | Đội nhỏ, một khối dễ vận hành hơn | Khi có nhiều đội cùng phát triển |
| Cập nhật thời gian thực | Người dùng tải lại trang là đủ | Khi cần trao đổi tức thời trong việc |
| Ứng dụng cài trên điện thoại | Giao diện web hiển thị được trên điện thoại | Khi cán bộ chủ yếu dùng điện thoại |
| Lưu tệp trên dịch vụ đám mây | Đĩa máy chủ đủ cho quy mô hiện tại | Khi tệp đính kèm vượt 100 GB |