Giao diện
Chứng chỉ đào tạo
Cấu hình mẫu chứng chỉ, cấp chứng chỉ tự động khi học viên hoàn thành khoá, quản lý chứng chỉ chờ render và cấp thủ công cho từng nhân viên.
Quyền truy cập: Cần quyền Quản lý mẫu chứng chỉ (CERTIFICATE_TEMPLATE_CREATE - tạo/sửa/xoá/nhân bản mẫu), Cấp chứng chỉ (CERTIFICATE_ISSUE - cấp thủ công qua certificate.adminIssue, tách khỏi quyền template), Admin Điều hướng:
- Mẫu chứng chỉ:
Đào tạo > Mẫu chứng chỉhoặc cấu hình trong chương trình - Chứng chỉ chờ render:
/manage/cert-pending - Cấp chứng chỉ thủ công:
/manage/cert-issue
Tổng quan
Tính năng Chứng chỉ đào tạo cấp chứng chỉ tự động khi nhân viên hoàn thành khoá học hoặc chương trình đào tạo, và cho phép admin cấp thủ công cho cá nhân.
Từ phiên bản 8.10, hệ thống được tái cấu trúc toàn diện:
- Schema canonical: mỗi chứng chỉ là 1 bản ghi với
forUser.userId+ anchor đã resolve (courseId / examId / trainingProgramId), có status (PENDING_RENDER / RENDERING / ISSUED / RENDER_FAILED / REVOKED / ARCHIVED) - Render client-side: PNG được vẽ trên trình duyệt (admin hoặc học viên) bằng fabric.js, không cần worker server-side
- Lưu private trên DigitalOcean Spaces: ACL=private, mỗi lần xem cần presigned URL ngắn hạn
- Auto-issue qua StakeholderEvent COMPLETE: không phụ thuộc collection-hooks, an toàn xuyên app
- Code generator atomic: mã chứng chỉ duy nhất theo
codePrefix + incrementValue, có 3 mode đếm
Từ phiên bản 8.12 (Multi-anchor refactor):
- Anchors[] thay vì forCourse: 1 mẫu chứng chỉ có thể được kích hoạt bởi NHIỀU NGUỒN (khoá học, bài thi, chương trình đào tạo) trong cùng một template.
- 3 entity types:
course,exam,training_program- mỗi anchor lưu{type, id, title}. - Wildcard per-type:
{type: 'exam', id: 'all'}áp dụng cho mọi bài thi trong tenant; KHÔNG có wildcard global (không thể chọn "tất cả mọi entity"). - Cài đặt 2 chiều: ngoài việc thêm anchor từ template, mỗi entity (course/exam/training_program) có riêng tab "Cài đặt chung > Chứng chỉ" để gắn template - đồng bộ vào
template.anchors[]. - Idempotency unique key đổi: từ
(templateId, userId, courseId)→(templateId, userId, anchor.type, anchor.id).
Mẫu chứng chỉ (Certificate Template)
Tạo mẫu
- Vào
Đào tạo > Mẫu chứng chỉ, nhấp "Tạo mẫu". - Thiết kế nội dung trên canvas: kéo thả text/ảnh, đặt 4 placeholder text (
,,,). - Cấu hình các trường bên phải canvas.
- Nhấp "Cập nhật" - xuất hiện hộp thoại xác nhận.
Trường cấu hình mẫu
| Trường | Bắt buộc | Mô tả |
|---|---|---|
| Tên chứng chỉ | Có | Tên hiển thị |
| Trạng thái | Có | Nháp / Đã kích hoạt (khi chuyển sang ACTIVATED, hệ thống validate anchors[] minCount = 1) |
| Nguồn kích hoạt (anchors[]) | Có khi kích hoạt | Danh sách các nguồn (khoá học / bài thi / chương trình đào tạo) sẽ kích hoạt cấp chứng chỉ - xem mục "Multi-anchor" bên dưới. Thay thế hoàn toàn trường forCourse cũ. |
| Thời hạn hiệu lực (tháng) | Không | Số tháng chứng chỉ có hiệu lực; để trống = vĩnh viễn |
| Tự động cấp chứng chỉ khi hoàn thành | Không | Toggle bật/tắt auto-issue (mặc định bật) - áp dụng cho mọi anchor |
| Quy tắc mã (codeRule) | Có | 4 cấu hình con: enablePrefix, codePrefix, enableIncrement, incrementType, enableIncrementStart, incrementStart |
Quy tắc mã chứng chỉ (codeRule)
| Tuỳ chọn | Mô tả |
|---|---|
| Bật tiền tố | Có/không sử dụng codePrefix (ví dụ HL-2024/) |
| Tiền tố mã | Văn bản hiển thị trước số (ví dụ HL-ACADEMY-2024/) |
| Bật số tăng dần | Có/không thêm số thứ tự |
| Quy luật tăng (incrementType) | 3 chế độ: ALL_TOTAL (tổng tất cả chứng chỉ), PREFIX_TOTAL (tổng theo cùng tiền tố), COURSE_TOTAL (tổng theo anchor - kể từ 8.12 áp cho mọi loại entity, không riêng khoá học) |
| Bật điểm khởi đầu | Có/không đặt giá trị bắt đầu cho số tăng |
| Điểm khởi đầu | Số khởi đầu (ví dụ 100 → mã đầu tiên là HL-2024/101) |
Hệ thống hiển thị preview định dạng: Mã số chỉ sẽ có định dạng: HL-ACADEMY-2024/1 (giá trị placeholder ABCDEF nếu chưa bật prefix/increment).
Lock-after-issue (8.10): Sau khi đã cấp ít nhất 1 chứng chỉ từ mẫu, không cho phép đổi
codePrefixhayincrementTypenữa - chỉ cho tăngincrementStart. Nguyên tắc 1 mã = 1 cert vĩnh viễn để đảm bảo tính minh bạch audit.
Multi-anchor - một mẫu, nhiều nguồn kích hoạt
Khái niệm
Từ 8.12, mỗi mẫu chứng chỉ có thể được kích hoạt bởi nhiều nguồn (anchors) thuộc các loại khác nhau. Mỗi anchor là cấu hình {type, id, title} đại diện một entity cụ thể (hoặc wildcard trong loại đó).
Mục tiêu:
- Tái sử dụng 1 mẫu thiết kế cho nhiều khoá / kỳ thi / chương trình mà không cần clone.
- Vẫn tách rõ chứng chỉ theo từng anchor (1 học viên có thể được cấp nhiều bản từ cùng 1 mẫu, mỗi bản gắn một anchor riêng).
Các loại entity hỗ trợ
Loại (type) | Nhãn hiển thị | Badge | Mô tả |
|---|---|---|---|
course | Khoá học | primary | Cấp khi học viên hoàn thành khoá học (Enrollment.completedAt) |
exam | Bài thi | warning | Cấp khi học viên vượt qua bài thi (đạt điểm pass) |
training_program | Chương trình | info | Cấp khi học viên hoàn thành toàn bộ chương trình đào tạo |
Wildcard per-type: mỗi anchor có thể đặt
id: 'all'để áp dụng cho mọi entity thuộc loại đó (ví dụ{type: 'exam', id: 'all'}= mọi bài thi). KHÔNG có wildcard global - không thể tạo 1 anchor áp cho cả course + exam + training_program cùng lúc.
Hiển thị trên danh sách mẫu
Cột "Nguồn kích hoạt" tóm tắt số lượng theo loại, ví dụ: "2 khoá, 1 thi" hoặc "Mọi bài thi" (khi anchor wildcard).
AnchorMultiPicker trong trình thiết kế mẫu
Trong trang Tạo/Sửa mẫu chứng chỉ, panel bên phải canvas có component AnchorMultiPicker:
- Tab chọn loại entity (Khoá học / Bài thi / Chương trình).
- Mỗi tab có ô tìm kiếm + checkbox "Áp dụng cho tất cả" (set wildcard).
- Chọn nhiều entity cùng loại → mỗi entity tạo 1 anchor mới đẩy vào
anchors[]. - Hiển thị chip danh sách các anchor đã chọn, có thể xoá từng cái.
Khi lưu mẫu (status = ACTIVATED), hệ thống validate anchors.length >= 1. Mẫu DRAFT có thể có anchors = [].
Ví dụ mẫu hỗn hợp
Một mẫu "Chứng chỉ ISO Foundation" có thể cấu hình anchors:
json
[
{ "type": "course", "id": "course-iso-101", "title": "ISO 9001 Foundation" },
{ "type": "exam", "id": "exam-iso-final", "title": "Bài thi ISO 9001" },
{ "type": "training_program", "id": "all", "title": "Mọi chương trình đào tạo" }
]→ 1 học viên có thể nhận tối đa 3 bản chứng chỉ từ mẫu này (một cho mỗi anchor được kích hoạt), mỗi bản có mã riêng và gắn anchor riêng trong record.
Cài đặt chứng chỉ trong entity (Course / Exam / Training Program)
Ngoài việc cấu hình từ mẫu, người dùng có thể gắn mẫu chứng chỉ ngay trong từng entity:
Điều hướng: Mở entity (Khoá học / Bài thi / Chương trình) > tab Cài đặt chung > block Chứng chỉ (component EntityCertificateSection.vue).
Giao diện
| Phần tử | Mô tả |
|---|---|
| Danh sách mẫu đã gắn | Chip hiển thị mẫu đang áp dụng cho entity này (kể cả mẫu wildcard id: 'all') |
| Nút "Thêm mẫu" | Mở modal chọn mẫu chứng chỉ ACTIVATED hợp lệ |
| Info banner | Nếu có mẫu wildcard áp cho entity này, hiển thị banner "Mẫu này áp dụng cho mọi <loại>" và không cho gỡ ở entity (phải gỡ từ AnchorMultiPicker của mẫu) |
| Nút xoá mỗi chip | Gỡ entity này khỏi template.anchors[] (đồng bộ 2 chiều) |
Đồng bộ 2 chiều với template.anchors[]
- Thêm mẫu trong entity → push
{type, id: entity._id, title: entity.title}vàotemplate.anchors[]. - Gỡ mẫu trong entity → pull anchor tương ứng khỏi
template.anchors[]. - Thêm/gỡ trong AnchorMultiPicker của mẫu → tự động cập nhật danh sách entity hiển thị block "Chứng chỉ".
Lưu ý: Wildcard anchor (
id: 'all') hiển thị ở mọi entity cùng loại như "áp dụng tự động", không thể gỡ riêng cho 1 entity. Muốn loại trừ 1 entity, phải bỏ wildcard và liệt kê từng entity cụ thể.
Cấp chứng chỉ tự động
Khi nhân viên hoàn thành 1 trong các entity được gắn anchor (khoá học, bài thi, chương trình đào tạo), hệ thống tự động:
StakeholderEventDispatcher fire event
COMPLETE(cho course / training_program) hoặc event đạt điểm pass (cho exam).Listener
certificate-issue-listenermap event →issueReasonqua bảng ENTITY_TYPE_TO_REASON:Entity type issueReason courseCOURSE_COMPLETIONexamEXAM_PASStraining_programTRAINING_PROGRAMListener tìm tất cả
template.anchors[]matching:anchor.type === entityTypeVÀ (anchor.id === entityIdHOẶCanchor.id === 'all')- Mẫu có
status: 'ACTIVATED'vàautoIssue: true - Chưa có chứng chỉ tồn tại (unique key
{templateId, forUser.userId, anchor.type, anchor.id-đã-resolve}, trừ REVOKED)
Insert bản ghi CertificateList với status
PENDING_RENDER:- Reserve mã chứng chỉ qua
CertCodeService(atomic findOneAndUpdate counter) - Snapshot
forUser(userId, employeeId, fullName, email) +anchorđã resolve ({type, id, title}-idlà id thật của entity, KHÔNG BAO GIỜ là'all'). - Set
eweb,timezone,enrollmentId/examAttemptId/trainingEnrollmentId,triggerEventId,issueReason.
- Reserve mã chứng chỉ qua
Khi học viên mở
/learn/certificates(app employee/classroom), client tự render PNG bằng fabric, upload lên DigitalOcean Spaces, markISSUED.Học viên nhận thông báo qua
insertClientNotification.
Lưu ý quan trọng: Trong record CertificateList,
anchor.idluôn là id thật của entity (course/exam/training_program đã resolve). Wildcard'all'chỉ xuất hiện ởtemplate.anchors[], KHÔNG BAO GIỜ lưu vào bản ghi chứng chỉ - đảm bảo truy vết audit chính xác từng nguồn cấp.
Trang "Chứng chỉ chờ render" (admin)
Điều hướng: /manage/cert-pending
Trang admin để xem và render các chứng chỉ orphan (học viên không kích hoạt render qua việc mở app).
Giao diện
| Cột | Mô tả |
|---|---|
| STT | Thứ tự |
| Mã | Mã chứng chỉ đã reserve (ví dụ HL-2024/000123) |
| Người nhận | forUser.fullName |
| Khoá học | forCourse.courseTitle |
| Trạng thái | Badge PENDING_RENDER / RENDERING / RENDER_FAILED + số lần thử |
| Lỗi | Truncated stack/message từ lastRenderError (nếu có) |
| Hành động | Nút Render / Reset (cho RENDER_FAILED) |
Hành động
| Nút | Mô tả |
|---|---|
| Render tất cả (N) | Render hàng loạt tất cả PENDING_RENDER, hiển thị progress "Đang render X/Y..." |
| Render (mỗi dòng) | Render 1 chứng chỉ ngay trên trình duyệt admin, upload Spaces, mark ISSUED |
| Reset | Reset trạng thái RENDER_FAILED về PENDING_RENDER để thử lại |
Auto-release stale RENDERING: Nếu một chứng chỉ kẹt trạng thái RENDERING quá 60 giây (do client crash), hệ thống tự release về PENDING_RENDER. Sau ≥5 lần thử, tự chuyển sang RENDER_FAILED.
Mobile UA: Trên trình duyệt mobile, banner hiển thị "Mở trên desktop để hoàn tất chứng chỉ" - vì fabric render trên mobile có thể OOM.
Cấp chứng chỉ thủ công
Điều hướng: Trang Cấp chứng chỉ trong sidebar admin Quyền yêu cầu: CERTIFICATE_ISSUE (tách riêng khỏi CERTIFICATE_TEMPLATE_CREATE).
- Chọn mẫu chứng chỉ (
templateId). - Chọn người nhận qua NUserSelector / EmployeePicker (không nhập raw ID).
- Chọn anchor cụ thể: UI hiển thị dropdown 2 bước:
- Bước 1: Chọn loại anchor (
type) trong danh sách các loại có trongtemplate.anchors[](course / exam / training_program). - Bước 2: Chọn entity cụ thể (
id) - picker tương ứng loại đã chọn. Nếu anchor đã được khai báo wildcard (id: 'all') trong template, admin vẫn phải chọn 1 entity cụ thể; không bao giờ ghi'all'vào record. - Method backend
certificate.adminIssueKHÔNG còn auto-resolve courseId nữa - admin bắt buộc gửi đầy đủ{type, id}.
- Bước 1: Chọn loại anchor (
- (Tuỳ chọn) Đặt
expiresAt.issueReasonmặc định =ADMIN_MANUAL. - Nhấp "Cấp chứng chỉ".
Hệ thống render PNG ngay trên trình duyệt admin (synchronous trong dialog), upload Spaces, hiển thị preview liên kết ngay sau khi cấp.
Idempotency: Nếu đã có chứng chỉ tồn tại cho cùng
(templateId, userId, anchor.type, anchor.id)ở status khác REVOKED, hộp thoại sẽ cảnh báo và không tạo bản ghi mới.
Nhân bản mẫu
Trong danh sách mẫu chứng chỉ, mỗi mẫu có hành động "Nhân bản" giúp tái sử dụng thiết kế cho mục đích khác:
| Trường | Hành vi khi nhân bản |
|---|---|
| layout (canvas + placeholder positions) | Deep-copy nguyên trạng |
| codeRule (prefix, increment, start...) | Deep-copy nguyên trạng |
| anchors[] | Reset về [] - buộc người dùng chọn lại nguồn kích hoạt mới để tránh xung đột |
| status | DEACTIVATE (Nháp) - không cấp tự động cho đến khi review và kích hoạt lại |
| title | Thêm hậu tố (bản sao) - ví dụ Chứng chỉ ISO Foundation (bản sao) |
| expiryMonths, autoIssue | Deep-copy nguyên trạng |
Mục đích: Tránh trường hợp 2 mẫu cùng cấp song song cho 1 anchor (gây trùng cert / bối rối học viên). Người dùng bắt buộc chỉnh
anchors[]trước khi kích hoạt bản sao.
Cấu hình chứng chỉ trong Chương trình đào tạo
Sub-section Chứng chỉ trong tab Thông tin của chương trình:
| Trường | Mô tả |
|---|---|
| Bật chứng chỉ | Cấp chứng chỉ khi hoàn thành chương trình (không chỉ khoá) |
| Mẫu chứng chỉ | Chọn mẫu áp cho chương trình |
| Tiền tố mã | Ghi đè codePrefix mặc định cho riêng chương trình này |
| Thời hạn (tháng) | Số tháng chứng chỉ có hiệu lực |
| Yêu cầu gia hạn | Có cần gia hạn sau khi hết hạn |
Khi học viên hoàn thành chương trình (TrainingEnrollment.completedAt được set), StakeholderEvent TRAINING_PROGRAM_COMPLETE fire, listener tạo CertificateList với issueReason: 'TRAINING_PROGRAM'.
Trạng thái chứng chỉ
| Trạng thái | Mô tả |
|---|---|
| PENDING_RENDER | Đã reserve mã, chờ render PNG |
| RENDERING | Đang render (client đang xử lý) |
| ISSUED | Đã render xong, file đã upload Spaces |
| RENDER_FAILED | Render thất bại sau ≥5 lần thử |
| REVOKED | Đã thu hồi, không hợp lệ |
| ARCHIVED | Đã lưu trữ |
Xem chứng chỉ
Admin
Trong trang chi tiết chứng chỉ, click "Xem chứng chỉ" - hệ thống gọi method certificate.getViewUrl để tạo presigned URL ngắn hạn (1 giờ).
Học viên
Trang /learn/certificates trong app employee/classroom liệt kê tất cả chứng chỉ của học viên. Nhấp "Xem" - presigned URL được tạo và mở trong tab mới.
Bảo mật: Tất cả file chứng chỉ lưu private trên DigitalOcean Spaces (ACL=private). Mỗi lần xem cần gen URL mới. Liên kết không thể chia sẻ trực tiếp - chỉ người có quyền xem mới gen được URL.
Thu hồi chứng chỉ
Các bước
- Mở chi tiết chứng chỉ đã ISSUED.
- Nhấp "Thu hồi".
- Nhập Lý do thu hồi.
- Xác nhận thu hồi.
Kết quả: Trạng thái chứng chỉ chuyển sang
REVOKED. Hệ thống ghi nhận thời gian và lý do thu hồi. Mã chứng chỉ KHÔNG được tái sử dụng cho người khác (giữ gap, counter chỉ tăng).
Lưu ý
- Không backward-compat: Schema cũ (single
idfield, lowercase status) đã được canonicalize. Pre-launch DB sẽ reset. - Bundle fabric ~250KB: Chỉ load khi route /learn/certificates hoặc /manage/cert-pending mount (dynamic import).
- Code uniqueness DB-level: Unique partial index trên
{eweb, code}(status != REVOKED) - không thể trùng mã trong cùng tenant. - Idempotency (8.12): Unique partial index đổi từ
{eweb, templateId, userId, courseId}→{eweb, templateId, userId, anchor.type, anchor.id}(status != REVOKED). Record chứng chỉ luôn lưuanchor.idđã resolve, KHÔNG BAO GIỜ là'all'. - CodeRule lock: Sau khi đã cấp ≥1 cert, không cho đổi codePrefix/incrementType nữa.
- Không backward-compat field
pdfUrl: trườngcertificateUrl/pdfUrlcũ đã bỏ - dùngcertificate.getViewUrlđể gen presigned URL.
Migration từ 8.10/8.11 sang 8.12 (multi-anchor)
KHÔNG có migration script. Refactor multi-anchor là pre-launch reset DB - toàn bộ template/cert seed cũ phải tạo lại từ đầu trên môi trường mới.
Lý do:
- Schema thay đổi căn bản (
forCourse→anchors[], unique index mới). - Code path auto-issue rewrite hoàn toàn quanh
ENTITY_TYPE_TO_REASON. - Không có khách hàng production sử dụng phiên bản chứng chỉ cũ - chấp nhận reset.
Checklist khi triển khai 8.12:
- Drop toàn bộ collection
CertificateTemplatesvàCertificateListở DB cũ (nếu có). - Drop các unique index cũ tham chiếu
forCourse.courseId. - Tạo lại unique index mới:
{eweb, templateId, forUser.userId, anchor.type, anchor.id}(partial, status != REVOKED). - Re-seed mẫu chứng chỉ thông qua UI (AnchorMultiPicker) hoặc seed script mới có khai báo
anchors[].
Xem thêm
- Chương trình đào tạo - Cấu hình chứng chỉ trong chương trình
- Ghi danh - Quản lý ghi danh và hoàn thành

