#06 – Mô hình miền & dữ liệu
Giai đoạn: Thiết kế hệ thống · Phiên bản 0.1 – 29/10/2026
Mô hình logic, độc lập công nghệ. Tên thực thể dùng tiếng Anh để dùng thẳng trong mã nguồn; mô tả tiếng Việt.
#1. Các miền nghiệp vụ (Bounded contexts)
flowchart LR
subgraph ID["Định danh & Quyền"]
U["User / Role"]
ENT["Entitlement"]
end
subgraph CAT["Nội dung (Catalog)"]
BOOK["Book / ContentNode"]
RES["LearningResource"]
CODE["ContentCode (ID)"]
VID["Video"]
TAX["Taxonomy (ma trận)"]
end
subgraph ASM["Đánh giá (Assessment)"]
Q["Question / Rubric"]
A["Assessment / Section / Item"]
AT["Attempt / Answer"]
REC["Recommendation"]
end
subgraph ACT["Kích hoạt"]
BATCH["ActivationBatch / Code"]
end
subgraph CLS["Lớp học"]
C["Class / Assignment"]
end
subgraph COM["Thương mại"]
P["Package / Order / Payment"]
end
subgraph ANA["Phân tích"]
EV["Event"]
end
ACT -- "cấp" --> ENT
COM -- "cấp" --> ENT
CODE --> RES
RES --> A
A --> Q
Q --> TAX
AT --> REC
REC --> TAX
C --> A
ID -.-> ANA
#2. Sơ đồ quan hệ thực thể (ERD)
erDiagram
USER ||--o{ ENTITLEMENT : "có"
USER ||--o{ ATTEMPT : "thực hiện"
USER ||--o{ ORDER : "đặt"
USER ||--o{ CLASS_MEMBER : "thuộc"
BOOK ||--o{ CONTENT_NODE : "gồm"
BOOK ||--o{ ACTIVATION_BATCH : "có"
BOOK ||--o{ CONTENT_CODE : "in"
ACTIVATION_BATCH ||--o{ ACTIVATION_CODE : "sinh"
ACTIVATION_CODE |o--o| ENTITLEMENT : "tạo"
CONTENT_NODE ||--o{ CONTENT_NODE : "con"
CONTENT_NODE ||--o{ LEARNING_RESOURCE : "chứa"
CONTENT_CODE }o--|| LEARNING_RESOURCE : "trỏ tới"
LEARNING_RESOURCE |o--o| VIDEO : "dùng"
LEARNING_RESOURCE |o--o| ASSESSMENT : "là"
ASSESSMENT ||--o{ ASSESSMENT_SECTION : "chia"
ASSESSMENT_SECTION ||--o{ ASSESSMENT_ITEM : "gồm"
ASSESSMENT_ITEM }o--|| QUESTION : "dùng"
QUESTION }o--o| PASSAGE : "thuộc ngữ liệu"
QUESTION |o--o| RUBRIC : "chấm theo"
QUESTION |o--o| VIDEO : "lời giải video"
QUESTION ||--o{ QUESTION_TAG : "gắn"
TAXONOMY_TAG ||--o{ QUESTION_TAG : "được gắn"
TAXONOMY_TAG ||--o{ TAXONOMY_TAG : "con"
TAXONOMY_TAG |o--o{ CONTENT_NODE : "ứng với chuyên đề"
ATTEMPT }o--|| ASSESSMENT : "của"
ATTEMPT }o--o| ASSIGNMENT : "theo bài giao"
ATTEMPT ||--o{ ATTEMPT_ANSWER : "gồm"
ATTEMPT_ANSWER }o--|| QUESTION : "trả lời"
ATTEMPT ||--o{ RECOMMENDATION : "sinh"
RECOMMENDATION }o--|| TAXONOMY_TAG : "về"
CLASS ||--o{ CLASS_MEMBER : "có"
CLASS ||--o{ ASSIGNMENT : "được giao"
ASSIGNMENT }o--|| ASSESSMENT : "giao"
PACKAGE ||--o{ PACKAGE_ITEM : "gồm"
PACKAGE_ITEM }o--|| ASSESSMENT : "đề"
ORDER }o--|| PACKAGE : "mua"
ORDER ||--o{ PAYMENT : "thanh toán"
ORDER |o--o| ENTITLEMENT : "tạo"
USER ||--o{ EVENT : "phát sinh"
#3. Cơ chế ID nội dung và QR
#3.1 Yêu cầu đối với ID
- Ngắn, gõ được trên điện thoại trong < 5 giây; không có ký tự dễ nhầm.
- Học sinh và nhân viên hỗ trợ đọc được ý nghĩa (môn, loại).
- Không trùng trong phạm vi một phiên bản sách; không bao giờ thay đổi sau khi in.
- Không phụ thuộc vào cấu trúc kỹ thuật (đổi tài nguyên đích mà không đổi ID).
#3.2 Định dạng đề xuất (ADR-002)
T V 0 1 2
│ │ └┴┴── Số thứ tự 3 chữ số trong (môn, loại): 001–999
│ └─────── Loại tài nguyên (1 chữ)
└────────── Môn (1 chữ)
| Vị trí | Giá trị | Ý nghĩa |
|---|---|---|
| Môn | T |
Toán |
V |
Ngữ văn | |
A |
Tiếng Anh | |
| Loại | L |
Lý thuyết / mind map / chuyên đề (THEORY) |
V |
Ví dụ minh hoạ (EXAMPLE) | |
B |
Bài tập thực hành / vận dụng (EXERCISE_SET) | |
D |
Đề thi thử / đề luyện tập (MOCK_EXAM, PRACTICE_EXAM) | |
K |
Kiểm tra đầu vào – placement test (PLACEMENT_TEST) |
Ví dụ: TL021 – lý thuyết Toán số 21; TV045 – ví dụ Toán số 45; TD003 – đề thi thử Toán số 3; AK002 – placement test Tiếng Anh số 2; VB007 – bài vận dụng Ngữ văn số 7.
Phạm vi phiên bản: khoá duy nhất của ID là (book_edition_id, code). Học sinh chỉ gõ TV045; hệ thống tra trong các sách học sinh đã kích hoạt (BR-06). QR chứa phiên bản để không mơ hồ: https://<tên miền>/27/TV045 (27 = phiên bản 2027).
Phương án thay thế (nếu HEID muốn ID không mang nghĩa): mã ngẫu nhiên 5 ký tự từ bảng 31 ký tự (≈ 28,6 triệu tổ hợp) – an toàn trước việc đoán ID nhưng khó hỗ trợ qua điện thoại. Vì nội dung đã được bảo vệ bằng quyền sử dụng (BR-03), việc đoán được ID không làm lộ nội dung → khuyến nghị dùng ID có nghĩa.
#3.3 Vòng đời ID
stateDiagram-v2
[*] --> RESERVED: BTV sinh ID
RESERVED --> ASSIGNED: Gán tài nguyên
ASSIGNED --> RESERVED: Bỏ gán (chưa khoá)
ASSIGNED --> LOCKED: Trưởng ban khoá khi chốt in
LOCKED --> LOCKED: Đổi tài nguyên đích (lưu vết)
LOCKED --> RETIRED: Ngừng dùng (chuyển hướng tới tài nguyên thay thế)
RESERVED --> [*]: Xoá (chỉ khi chưa khoá)
#3.4 Mã kích hoạt
- 10 ký tự từ bảng
23456789ABCDEFGHJKMNPQRSTUVWXYZ(31 ký tự), in dạngXXXX-XXXX-XXvà kèm QRhttps://<tên miền>/kich-hoat/<mã>. - Lưu băm của mã để tra cứu (HMAC-SHA256 với khoá bí mật) và bản mã hoá để xuất lại cho nhà in khi cần; không lưu bản rõ trong log.
- Trạng thái:
NEW→EXPORTED→REDEEMED/REVOKED.
#4. Từ điển dữ liệu (thực thể chính)
#4.1 Định danh & quyền
USER
| Trường | Kiểu | Mô tả |
|---|---|---|
| id | UUID | Khoá chính |
| sso_subject | string, nullable | Định danh từ Hoclieu SSO |
| full_name | string | Họ tên |
| phone / email | string, nullable | Ít nhất một trường có giá trị nếu không dùng SSO |
| password_hash | string, nullable | Argon2/bcrypt |
| roles | enum[] | STUDENT, TEACHER, EDITOR, CHIEF_EDITOR, ADMIN |
| grade, school, province | string, nullable | Hồ sơ (FR-01.07) |
| consent_version, consent_at | string, timestamp | Đồng ý điều khoản |
| status | enum | ACTIVE, LOCKED, DELETED |
ENTITLEMENT – quyền sử dụng
| Trường | Kiểu | Mô tả |
|---|---|---|
| id | UUID | |
| user_id | FK USER | |
| scope_type | enum | BOOK, PACKAGE |
| scope_id | UUID | book_edition_id hoặc package_id |
| source | enum | ACTIVATION, PURCHASE, TEACHER, ADMIN_GRANT |
| source_ref | UUID | activation_code_id / order_id |
| valid_from, valid_to | timestamp | Thời hạn |
| status | enum | ACTIVE, REVOKED, EXPIRED |
#4.2 Nội dung
BOOK (một phiên bản sách)
| Trường | Kiểu | Mô tả |
|---|---|---|
| id | UUID | |
| subject | enum | MATH, LITERATURE, ENGLISH |
| title | string | "Ôn thi TNTHPT Toán" |
| edition | string | "2027" – mã phiên bản dùng trong QR (27) |
| isbn | string | |
| cover_url | string | |
| entitlement_days | int | Thời hạn quyền sử dụng sau kích hoạt (hoặc valid_until cố định) |
| status | enum | DRAFT, PUBLISHED, ARCHIVED |
CONTENT_NODE – cây nội dung sách
| Trường | Kiểu | Mô tả |
|---|---|---|
| id, book_id, parent_id | UUID | |
| kind | enum | PART, TOPIC, SKILL, SECTION |
| title | string | "Phần 1 – Ôn luyện kiến thức", "Nguyên hàm – Tích phân" |
| order_no | int | Thứ tự |
| printed_page_from/to | int | Trang trên sách giấy |
| taxonomy_tag_id | FK, nullable | Chuyên đề tương ứng trong ma trận (để đề xuất dẫn về đúng nút) |
LEARNING_RESOURCE
| Trường | Kiểu | Mô tả |
|---|---|---|
| id, node_id | UUID | |
| kind | enum | THEORY, EXAMPLE, EXERCISE_SET, PLACEMENT_TEST, MOCK_EXAM, PRACTICE_EXAM, EXTENDED_EXAM |
| title | string | |
| body | rich text (JSON/HTML có LaTeX) | Nội dung text (lý thuyết, phân tích ví dụ) |
| video_id | FK, nullable | |
| assessment_id | FK, nullable | Với các loại có câu hỏi |
| is_preview | bool | Cho xem không cần kích hoạt |
| version, status | int, enum | DRAFT, IN_REVIEW, PUBLISHED (BR-20) |
CONTENT_CODE – ID in trên sách
| Trường | Kiểu | Mô tả |
|---|---|---|
| book_id + code | khoá duy nhất | TV045 |
| resource_id | FK | Tài nguyên đích hiện tại |
| printed_page | int | Trang in |
| status | enum | RESERVED, ASSIGNED, LOCKED, RETIRED |
| redirect_resource_id | FK, nullable | Khi RETIRED |
| locked_by, locked_at |
VIDEO
| Trường | Kiểu | Mô tả |
|---|---|---|
| id | UUID | |
| provider, provider_asset_id | string | Dịch vụ video và mã tài sản |
| duration_sec | int | |
| thumbnail_url | string | |
| status | enum | UPLOADING, PROCESSING, READY, FAILED |
TAXONOMY_TAG – ma trận kiến thức
| Trường | Kiểu | Mô tả |
|---|---|---|
| id, parent_id | UUID | Cây phân cấp |
| subject | enum | |
| dimension | enum | TOPIC (chuyên đề), SKILL (kỹ năng), QUESTION_TYPE (dạng bài), COGNITIVE_LEVEL (mức độ) |
| code, name | string | T.GT.NH – "Nguyên hàm" |
| weight | decimal | Trọng số trong ma trận đề (tuỳ chọn) |
#4.3 Đánh giá
QUESTION
| Trường | Kiểu | Mô tả |
|---|---|---|
| id | UUID | |
| subject | enum | |
| type | enum | MCQ, TRUE_FALSE, SHORT_ANSWER, ESSAY |
| passage_id | FK, nullable | Ngữ liệu dùng chung (bài đọc) |
| stem | rich text | Đề bài |
| options | JSON | MCQ: [{key:"A", content}]; TRUE_FALSE: [{key:"a", content}] |
| answer | JSON | MCQ: "B"; TRUE_FALSE: {"a":true,"b":false,...}; SHORT_ANSWER: ["-1,5","-1.5"]; ESSAY: null |
| explanation | rich text | Lời giải |
| solution_video_id | FK, nullable | |
| rubric_id | FK, nullable | Với ESSAY |
| difficulty | enum | Mức độ nhận thức (song song với tag) |
| version, status |
RUBRIC: id, criteria JSON [{code, name, max_points, levels:[{label, points, descriptor}]}], total_points.
ASSESSMENT
| Trường | Kiểu | Mô tả |
|---|---|---|
| id | UUID | |
| kind | enum | Như LEARNING_RESOURCE.kind (các loại có câu hỏi) |
| duration_min | int, nullable | null = không giới hạn |
| scoring_scheme | enum | MATH_2025, ENGLISH_2025, RAW_COUNT, SELF_RUBRIC (09) |
| allowed_modes | enum[] | EXAM_VIEW, ANSWER_SHEET, ONE_PER_SCREEN, KEY_LOOKUP |
| allow_back_navigation | bool | false với placement test |
| max_score | decimal | 10 |
ASSESSMENT_SECTION: id, assessment_id, title ("Phần I"), order_no, question_type, points_rule JSON.
ASSESSMENT_ITEM: section_id, question_id, question_version, order_no, points.
ATTEMPT
| Trường | Kiểu | Mô tả |
|---|---|---|
| id | UUID | |
| user_id, assessment_id | FK | |
| assignment_id | FK, nullable | Bài giao của GV |
| mode | enum | EXAM_VIEW, ANSWER_SHEET, ONE_PER_SCREEN |
| timed | bool | |
| started_at, deadline_at, submitted_at | timestamp | deadline tính phía máy chủ |
| submit_type | enum | MANUAL, AUTO_TIMEOUT |
| status | enum | IN_PROGRESS, SUBMITTED, ABANDONED |
| score, max_score | decimal | Điểm tự động |
| self_score | decimal, nullable | Điểm tự đánh giá (Văn) |
| section_scores | JSON | Điểm theo phần |
| duration_sec | int |
ATTEMPT_ANSWER
| Trường | Kiểu | Mô tả |
|---|---|---|
| attempt_id, question_id | khoá | |
| response | JSON | Câu trả lời |
| image_urls | string[] | Ảnh bài viết tay |
| is_correct | bool, nullable | null với tự luận |
| correct_parts | int, nullable | Số ý đúng (TRUE_FALSE) |
| points | decimal | |
| self_grade | JSON, nullable | Mức chọn theo tiêu chí rubric |
| time_spent_ms | int | Đặc biệt quan trọng với ONE_PER_SCREEN |
| flagged | bool | Đánh dấu xem lại |
| updated_at | timestamp | Phục vụ đồng bộ ngoại tuyến (last-write-wins theo client timestamp hợp lệ) |
RECOMMENDATION: attempt_id, tag_id, items_total, items_wrong, items_slow, error_rate, rank, resource_ids (lý thuyết/ví dụ/bài tập gợi ý).
#4.4 Kích hoạt, lớp học, thương mại, phân tích
| Thực thể | Trường chính |
|---|---|
| ACTIVATION_BATCH | id, book_id, quantity, created_by, exported_at, note |
| ACTIVATION_CODE | id, batch_id, code_hmac, code_encrypted, status, redeemed_by, redeemed_at, revoked_reason |
| CLASS | id, teacher_id, name, school_year, lms_class_id, join_code |
| CLASS_MEMBER | class_id, user_id, joined_at |
| ASSIGNMENT | id, class_id, assessment_id, open_at, due_at, allowed_modes, created_by |
| PACKAGE | id, subject, title, price_vnd, duration_days, status |
| PACKAGE_ITEM | package_id, assessment_id, order_no |
| ORDER | id, user_id, package_id, amount_vnd, status (PENDING, PAID, FAILED, CANCELLED, REFUNDED), created_at, paid_at |
| PAYMENT | id, order_id, provider, provider_txn_id (duy nhất), raw_payload, status, received_at |
| EVENT | id, user_id (nullable), anonymous_id, session_id, name, properties JSON, occurred_at, received_at, device, app_version |
| AUDIT_LOG | id, actor_id, action, entity, entity_id, before, after, at |
#5. Ràng buộc & chỉ mục quan trọng
| Ràng buộc | Mục đích |
|---|---|
UNIQUE (book_id, code) trên CONTENT_CODE |
ID duy nhất theo phiên bản sách |
UNIQUE (code_hmac) trên ACTIVATION_CODE |
Tra mã kích hoạt nhanh, không trùng |
UNIQUE (provider, provider_txn_id) trên PAYMENT |
Xử lý IPN trùng lặp (idempotent) |
Partial unique (user_id, assessment_id) WHERE status = 'IN_PROGRESS' trên ATTEMPT |
Tối đa một lượt đang làm (BR-15) |
Trigger/ứng dụng: không cho sửa code khi status ∈ {LOCKED, RETIRED} |
BR-04 |
Index (user_id, submitted_at) trên ATTEMPT |
Lịch sử, biểu đồ |
Index (name, occurred_at) trên EVENT, phân vùng theo tháng |
Báo cáo |
#6. Phân loại dữ liệu & lưu trữ
| Nhóm dữ liệu | Ví dụ | Mức nhạy cảm | Biện pháp |
|---|---|---|---|
| Dữ liệu cá nhân cơ bản | Họ tên, SĐT, email, trường | Cá nhân | Mã hoá khi truyền, giới hạn quyền xem, che một phần trong trang quản trị |
| Dữ liệu học tập | Bài làm, điểm | Cá nhân | Chỉ học sinh, giáo viên của lớp (với bài giao) và Admin xem |
| Mã kích hoạt | Bí mật kinh doanh | Băm + mã hoá, xuất file có mật khẩu | |
| Đáp án, lời giải | Bí mật nội dung | Không gửi xuống trình duyệt trước khi nộp | |
| Sự kiện hành vi | Cá nhân (bút danh) | Ẩn danh hoá khi xoá tài khoản | |
| Giao dịch | Tài chính | Không lưu dữ liệu thẻ; lưu chứng từ theo quy định kế toán |