Skip to content

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

  1. Vào Đào tạo > Mẫu chứng chỉ, nhấp "Tạo mẫu".
  2. Thiết kế nội dung trên canvas: kéo thả text/ảnh, đặt 4 placeholder text (, , , ).
  3. Cấu hình các trường bên phải canvas.
  4. 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ườngBắt buộcMô tả
Tên chứng chỉTên hiển thị
Trạng tháiNhá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ạtDanh 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ôngSố 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ànhKhôngToggle bật/tắt auto-issue (mặc định bật) - áp dụng cho mọi anchor
Quy tắc mã (codeRule)4 cấu hình con: enablePrefix, codePrefix, enableIncrement, incrementType, enableIncrementStart, incrementStart

Quy tắc mã chứng chỉ (codeRule)

Tuỳ chọnMô 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ầnCó/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 đầuCó/không đặt giá trị bắt đầu cho số tăng
Điểm khởi đầuSố 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 codePrefix hay incrementType nữa - chỉ cho tăng incrementStart. 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ịBadgeMô tả
courseKhoá họcprimaryCấp khi học viên hoàn thành khoá học (Enrollment.completedAt)
examBài thiwarningCấp khi học viên vượt qua bài thi (đạt điểm pass)
training_programChương trìnhinfoCấ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:

  1. Tab chọn loại entity (Khoá học / Bài thi / Chương trình).
  2. Mỗi tab có ô tìm kiếm + checkbox "Áp dụng cho tất cả" (set wildcard).
  3. Chọn nhiều entity cùng loại → mỗi entity tạo 1 anchor mới đẩy vào anchors[].
  4. 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ắnChip 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 bannerNế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 chipGỡ 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ào template.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:

  1. StakeholderEventDispatcher fire event COMPLETE (cho course / training_program) hoặc event đạt điểm pass (cho exam).

  2. Listener certificate-issue-listener map event → issueReason qua bảng ENTITY_TYPE_TO_REASON:

    Entity typeissueReason
    courseCOURSE_COMPLETION
    examEXAM_PASS
    training_programTRAINING_PROGRAM
  3. Listener tìm tất cả template.anchors[] matching:

    • anchor.type === entityType VÀ (anchor.id === entityId HOẶC anchor.id === 'all')
    • Mẫu có status: 'ACTIVATED'autoIssue: true
    • Chưa có chứng chỉ tồn tại (unique key {templateId, forUser.userId, anchor.type, anchor.id-đã-resolve}, trừ REVOKED)
  4. 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} - id là id thật của entity, KHÔNG BAO GIỜ là 'all').
    • Set eweb, timezone, enrollmentId/examAttemptId/trainingEnrollmentId, triggerEventId, issueReason.
  5. Khi học viên mở /learn/certificates (app employee/classroom), client tự render PNG bằng fabric, upload lên DigitalOcean Spaces, mark ISSUED.

  6. Học viên nhận thông báo qua insertClientNotification.

Lưu ý quan trọng: Trong record CertificateList, anchor.id luô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ộtMô tả
STTThứ tự
Mã chứng chỉ đã reserve (ví dụ HL-2024/000123)
Người nhậnforUser.fullName
Khoá họcforCourse.courseTitle
Trạng tháiBadge PENDING_RENDER / RENDERING / RENDER_FAILED + số lần thử
LỗiTruncated stack/message từ lastRenderError (nếu có)
Hành độngNút Render / Reset (cho RENDER_FAILED)

Hành động

NútMô 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
ResetReset 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).

  1. Chọn mẫu chứng chỉ (templateId).
  2. Chọn người nhận qua NUserSelector / EmployeePicker (không nhập raw ID).
  3. 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ó trong template.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.adminIssue KHÔNG còn auto-resolve courseId nữa - admin bắt buộc gửi đầy đủ {type, id}.
  4. (Tuỳ chọn) Đặt expiresAt. issueReason mặc định = ADMIN_MANUAL.
  5. 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ườngHà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
statusDEACTIVATE (Nháp) - không cấp tự động cho đến khi review và kích hoạt lại
titleThêm hậu tố (bản sao) - ví dụ Chứng chỉ ISO Foundation (bản sao)
expiryMonths, autoIssueDeep-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ườngMô 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ạnCó 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áiMô tả
PENDING_RENDERĐã reserve mã, chờ render PNG
RENDERINGĐang render (client đang xử lý)
ISSUEDĐã render xong, file đã upload Spaces
RENDER_FAILEDRender 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

  1. Mở chi tiết chứng chỉ đã ISSUED.
  2. Nhấp "Thu hồi".
  3. Nhập Lý do thu hồi.
  4. 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 id field, 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ưu anchor.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ường certificateUrl/pdfUrl cũ đã bỏ - dùng certificate.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 (forCourseanchors[], 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:

  1. Drop toàn bộ collection CertificateTemplatesCertificateList ở DB cũ (nếu có).
  2. Drop các unique index cũ tham chiếu forCourse.courseId.
  3. Tạo lại unique index mới: {eweb, templateId, forUser.userId, anchor.type, anchor.id} (partial, status != REVOKED).
  4. 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

Hướng dẫn sử dụng nền tảng HR/LMS Noova. Vận hành bởi VN-ELEARNING.