Data Dictionary · Khác (phần 3/11)
⚙️ Trang này sinh tự động từ source code bằng
scripts/gen-reference.mjs— đừng sửa tay, sửa code rồi chạynpm run gen:reference.
Thuộc Data Dictionary. 34 bảng, curriculum_lesson_interventions tới email_suppressions.
curriculum_lesson_interventions
migration: 0086_course_specs_cp_english.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
lesson_id | TEXT | NOT NULL REFERENCES curriculum_lessons(id) |
seq | INTEGER | NOT NULL |
evidence_vi | TEXT | NOT NULL |
action_vi | TEXT | NOT NULL |
| — | table constraint | PRIMARY KEY (lesson_id, seq) |
Khóa ngoại: lesson_id → curriculum_lessons
curriculum_lesson_materials
Học liệu đầu bài. kind quyết định cách mở: youtube/pdf/link → url; lab → ref_id là id lab trong thư viện Lab đã có (SRC-166). Ràng buộc ép đúng điều đó ở TẦNG DỮ LIỆU: một lab không có url, một video không có ref_id — sai chỗ này thì UI không biết mở bằng cách nào, và nó sẽ hỏng lặng lẽ. reflection_prompt_vi: chỉ đạo "sau khi xem xong thì học sinh cần làm reflection, hoặc report, để thể hiện là đã xem rồi, và rút ra cái gì đó". Để NULL là học liệu đọc thêm, không đòi nộp gì.
migration: 0085_curriculum_outcomes.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
lesson_id | TEXT | NOT NULL REFERENCES curriculum_lessons(id) |
seq | INTEGER | NOT NULL |
kind | TEXT | NOT NULL CHECK (kind IN ('youtube','pdf','lab','link')) |
title_vi | TEXT | NOT NULL |
url | TEXT | — |
ref_id | TEXT | — |
minutes | INTEGER | — |
reflection_prompt_vi | TEXT | — |
| — | table constraint | PRIMARY KEY (lesson_id, seq) |
| — | table constraint | CHECK ((kind = 'lab' AND ref_id IS NOT NULL AND url IS NULL) |
Khóa ngoại: lesson_id → curriculum_lessons
curriculum_lesson_misconceptions
migration: 0086_course_specs_cp_english.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
lesson_id | TEXT | NOT NULL REFERENCES curriculum_lessons(id) |
seq | INTEGER | NOT NULL |
text_vi | TEXT | NOT NULL |
unit_code | TEXT | — |
| — | table constraint | PRIMARY KEY (lesson_id, seq) |
text_en | TEXT | — thêm ở 0194_lesson_content_bilingual.sql |
Khóa ngoại: lesson_id → curriculum_lessons
curriculum_lesson_outcomes
── Lesson ────────────────────────────────────────────────────────────────────────────────────── Cùng dạng "Tôi có thể…" nhưng ở mức nhận biết/hiểu, không phải mức làm ra sản phẩm.
migration: 0085_curriculum_outcomes.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
lesson_id | TEXT | NOT NULL REFERENCES curriculum_lessons(id) |
seq | INTEGER | NOT NULL |
text_vi | TEXT | NOT NULL |
| — | table constraint | PRIMARY KEY (lesson_id, seq) |
text_en | TEXT | — thêm ở 0191_bilingual_outcome_gaps.sql |
Khóa ngoại: lesson_id → curriculum_lessons
curriculum_lesson_questions
migration: 0181_curriculum_bilingual.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
lesson_id | TEXT | NOT NULL REFERENCES curriculum_lessons(id) |
seq | INTEGER | NOT NULL |
text_vi | TEXT | — |
text_en | TEXT | — |
| — | table constraint | PRIMARY KEY (lesson_id, seq) |
Khóa ngoại: lesson_id → curriculum_lessons
curriculum_lesson_quiz
Migration 0204 — Bộ 20 câu trắc nghiệm cuối mỗi lesson của khoá bố mẹ (SRC-680 đợt 3). VÌ SAO CẦN BẢNG MỚI. Thân bài lesson của bố mẹ có bốn phần: SCQA, ví dụ, chỗ dễ hiểu lầm, và một bộ trắc nghiệm. Ba phần đầu đã có chỗ trong tầng Course: SCQA → curriculum_lesson_stories (situation/complication/question/resolution) ví dụ → curriculum_lesson_examples kind='example' chỗ hiểu lầm → curriculum_lesson_examples kind='near_miss' (looksLike → text, whyWrong → why) Phần thứ tư thì không: curriculum_lesson_checks giữ ĐỀ BÀI kiểm tra chứ không giữ phương án và đáp án, còn items là ngân hàng câu hỏi cho học sinh, gắn với knowledge node chứ không gắn với lesson. Nhét trắc nghiệm của bố mẹ vào items là trộn hai thứ có vòng đời khác hẳn nhau: câu trong items được máy sinh, máy chấm, người duyệt; câu ở đây do chủ dự án viết và đọc cùng bài.
migration: 0204_lesson_quiz.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
lesson_id | TEXT | NOT NULL REFERENCES curriculum_lessons(id) |
seq | INTEGER | NOT NULL |
stem_vi | TEXT | NOT NULL |
stem_en | TEXT | — |
answer | INTEGER | NOT NULL CHECK (answer BETWEEN 0 AND 3) |
why_vi | TEXT | — |
why_en | TEXT | — |
| — | table constraint | PRIMARY KEY (lesson_id, seq) |
Khóa ngoại: lesson_id → curriculum_lessons
Index: idx_lesson_quiz_lesson(lesson_id, seq)
curriculum_lesson_quiz_choices
Phương án tách bảng chứ không phải bốn cặp cột choice1_vi/choice1_en…: bốn cặp cột thì sửa một phương án phải biết nó là cột số mấy, và không câu SELECT nào đếm được "câu nào thiếu phương án". Một bảng thì cả hai việc đó là một dòng SQL.
migration: 0204_lesson_quiz.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
lesson_id | TEXT | NOT NULL |
quiz_seq | INTEGER | NOT NULL |
seq | INTEGER | NOT NULL CHECK (seq BETWEEN 0 AND 3) |
text_vi | TEXT | NOT NULL |
text_en | TEXT | — |
| — | table constraint | PRIMARY KEY (lesson_id, quiz_seq, seq) |
Khóa ngoại: lesson_id, quiz_seq → curriculum_lesson_quiz.lesson_id, seq
curriculum_lesson_reflections
Bài Ngẫm của learner. Hai câu hỏi là CỐ ĐỊNH cho mọi bài ("con nhận ra điều gì đáng giữ lại" và "con sẽ làm khác đi thế nào, khi nào"), nên không có bảng đề bài: đề nằm trong giao diện, còn đây chỉ lưu câu trả lời. Một learner một bài đúng MỘT bản ghi, viết lại thì đè lên: đây là chỗ ngẫm chứ không phải nhật ký, và learner quay lại sửa cho đúng hơn là chuyện nên khuyến khích.
migration: 0162_curriculum_lesson_story_reflection.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
lesson_id | TEXT | NOT NULL REFERENCES curriculum_lessons(id) |
learner_id | TEXT | NOT NULL |
findings_vi | TEXT | NOT NULL |
future_vi | TEXT | NOT NULL |
updated_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
| — | table constraint | PRIMARY KEY (lesson_id, learner_id) |
Khóa ngoại: lesson_id → curriculum_lessons
Index: idx_lesson_reflections_learner(learner_id, updated_at DESC)
curriculum_lesson_stories
── VÁ THỨ TỰ (2026-08-30, commit hộ phiên khác — không đụng nội dung của họ) ────────────── Bảng curriculum_lesson_stories được tạo trong 0176_curriculum_lesson_story_reflection.sql. File đó vốn mang số 0162 nên chạy TRƯỚC file này; khi nó được đổi số thành 0176 (SRC-648) thì thứ tự đảo lại, và trên một DB dựng mới file 0163 này chèn vào một bảng chưa tồn tại — "no such table: curriculum_lesson_stories". CI đỏ từ đó, chặn deploy của mọi phiên. Vá bằng cách khai lại bảng ở đây thay vì đổi số một trong hai file: · Đổi số 0163 là nguy hiểm — nó có thể đã áp lên production, mà tên file chính là khoá trong d1_migrations; đổi tên là wrangler chạy lại, và các INSERT dưới đây KHÔNG idempotent. · Đổi số 0176 xuống dưới 0163 thì không còn số trống nào để chen. IF NOT EXISTS nên chạy ở đâu, theo thứ tự nào cũng vô hại; 0176 giữ nguyên bản khai của nó.
migration: 0163_content_lesson_stories.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
lesson_id | TEXT | PRIMARY KEY REFERENCES curriculum_lessons(id) |
situation_vi | TEXT | NOT NULL |
complication_vi | TEXT | NOT NULL |
question_vi | TEXT | NOT NULL |
resolution_vi | TEXT | NOT NULL |
weak | INTEGER | NOT NULL DEFAULT 0 — thêm ở 0184_story_weak_flag.sql |
situation_en | TEXT | — thêm ở 0194_lesson_content_bilingual.sql |
complication_en | TEXT | — thêm ở 0194_lesson_content_bilingual.sql |
question_en | TEXT | — thêm ở 0194_lesson_content_bilingual.sql |
resolution_en | TEXT | — thêm ở 0194_lesson_content_bilingual.sql |
Khóa ngoại: lesson_id → curriculum_lessons
curriculum_lesson_takeaways
Migration 0185 — Key Takeaways ở cuối mỗi Lesson và mỗi Unit (SRC-660). Chỉ đạo chủ dự án 2026-08-31: "Bổ sung Key Takeaways vào cuối các Lesson, và Units." HAI BẢNG CHỨ KHÔNG MỘT BẢNG CÓ CỘT scope: takeaway của lesson và của unit khác nhau về HẠNG, không chỉ khác chỗ treo. Takeaway của một bài là điều đọng lại sau một buổi; của một unit là điều còn lại sau khi quên hết các bài. Một bảng chung với cột phân loại thì hai thứ ấy trông như một, và câu truy vấn nào cũng phải nhớ lọc — quên một lần là trộn hai hạng vào nhau. Song ngữ ngay từ đầu (cùng quy ước _vi = _en nghĩa là chưa dịch, xem migration 0181): mạch 12 khoá viết bằng tiếng Anh, thêm cột sau thì lại phải đi vá một vòng nữa.
migration: 0185_key_takeaways.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
lesson_id | TEXT | NOT NULL REFERENCES curriculum_lessons(id) |
seq | INTEGER | NOT NULL |
text_vi | TEXT | NOT NULL |
text_en | TEXT | — |
| — | table constraint | PRIMARY KEY (lesson_id, seq) |
Khóa ngoại: lesson_id → curriculum_lessons
curriculum_lessons
migration: 0084_curriculum_courses.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
unit_id | TEXT | NOT NULL REFERENCES curriculum_units(id) |
seq | INTEGER | NOT NULL |
name_vi | TEXT | NOT NULL |
guiding_question_vi | TEXT | — |
concepts_vi | TEXT | — |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
name_en | TEXT | — thêm ở 0181_curriculum_bilingual.sql |
guiding_question_en | TEXT | — thêm ở 0181_curriculum_bilingual.sql |
Khóa ngoại: unit_id → curriculum_units
Index: idx_curriculum_lessons_slot(unit_id, seq) UNIQUE
curriculum_part_completions
Learner xác nhận đã xong từng CỤM bên trong một chặng (SRC-668). Chỉ đạo chủ dự án 2026-09-02: "Kéo tới đâu thì ở cuối cần có một button be bé vui vui để xác nhận" — và sau đó: "Mỗi cụm một nút luôn đi." VÌ SAO BẢNG RIÊNG chứ không thêm cột part vào curriculum_stage_completions: 1. Khoá chính ở bảng kia là (lesson_id, learner_id, stage), và SQLite không sửa được khoá chính — phải dựng bảng mới, chép dữ liệu, DROP rồi RENAME. Làm việc đó trên một bảng đang phục vụ learner, trong đúng khoảng thời gian mã mới đã deploy còn migration thì chưa chạy, là đánh cược không cần thiết. 2. Hai thứ này KHÁC NGHĨA nhau. "Xong chặng" là mốc tiến độ mà trang phụ huynh và báo cáo đọc. "Xong cụm" là nhịp đọc trong lúc học — nó nói con đã đi tới đâu trong một trang dài, không nói con đã học xong cái gì. Trộn hai thứ vào một bảng thì mọi câu đếm tiến độ phải nhớ lọc part = '', và sẽ có ngày một câu quên lọc. KHÔNG có cột undo: bấm lại lần nữa thì XOÁ dòng, cùng luật với bảng chặng — một chỉ báo chỉ biết tăng thì không còn là chỉ báo.
migration: 0193_lesson_part_completions.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
lesson_id | TEXT | NOT NULL REFERENCES curriculum_lessons(id) ON DELETE CASCADE |
learner_id | TEXT | NOT NULL |
part | TEXT | NOT NULL |
completed_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
| — | table constraint | PRIMARY KEY (lesson_id, learner_id, part) |
Khóa ngoại: lesson_id → curriculum_lessons
Index: idx_part_completions_learner(learner_id, completed_at DESC)
curriculum_stage_completions
Migration 0180 — Learner xác nhận đã xong từng chặng của một bài (SRC-654). Chỉ đạo chủ dự án 2026-08-31: "Cuối mỗi phần đều có button để xác nhận là 'Tôi đã hoàn thành', click vào xong, thì hệ thống ghi nhận là Learner đã hoàn thành." VÌ SAO GHI THEO CHẶNG CHỨ KHÔNG PHẢI THEO BÀI: một bài có bốn chặng và learner thường bỏ dở giữa chừng. Chỉ ghi "xong bài" thì mọi thứ dở dang trông y hệt nhau — không biết con dừng ở chỗ đọc hay chỗ ngẫm, mà hai chỗ ấy cần hai cách giúp khác nhau. KHÔNG có cột undo: bấm lại lần nữa thì XOÁ dòng (xem service). Một bảng chỉ-thêm thì con số "đã xong" chỉ có thể phồng lên, và một chỉ báo chỉ biết tăng thì không còn là chỉ báo.
migration: 0180_lesson_stage_completions.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
lesson_id | TEXT | NOT NULL REFERENCES curriculum_lessons(id) |
learner_id | TEXT | NOT NULL |
stage | TEXT | NOT NULL |
completed_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
| — | table constraint | PRIMARY KEY (lesson_id, learner_id, stage) |
Khóa ngoại: lesson_id → curriculum_lessons
Index: idx_stage_completions_learner(learner_id, completed_at DESC)
curriculum_task_criteria
Tiêu chí chấm, mỗi dòng một tiêu chí. outcome_seq nối tiêu chí về đúng Unit Outcome mà nó đo — đây là chỗ vòng "outcome → task → chấm" khép lại; để trống nghĩa là tiêu chí chung (trình bày, thời hạn), vẫn hợp lệ.
migration: 0085_curriculum_outcomes.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
unit_id | TEXT | NOT NULL REFERENCES curriculum_unit_tasks(unit_id) |
seq | INTEGER | NOT NULL |
text_vi | TEXT | NOT NULL |
outcome_seq | INTEGER | — |
| — | table constraint | PRIMARY KEY (unit_id, seq) |
text_en | TEXT | — thêm ở 0191_bilingual_outcome_gaps.sql |
Khóa ngoại: unit_id → curriculum_unit_tasks
curriculum_unit_concepts
Migration 0183 — Key Concepts + Skills cho Unit, ví dụ và ví dụ dễ nhầm cho Lesson (SRC-659). Chỉ đạo chủ dự án 2026-08-31: "Mỗi Unit, cần bổ sung Key Concepts, Skills" và "soạn SCQA cho Lesson, 3 ví dụ, 3 ví dụ dễ gây hiểu lầm". VÌ SAO BẢNG MỚI CHỨ KHÔNG DÙNG curriculum_unit_elements ĐANG CÓ: bảng ấy khoá cứng kind bằng CHECK (key_concept, material, framework, model, formula, principle) và KHÔNG có cột tiếng Anh. SQLite không sửa được CHECK, nên thêm 'skill' vào đó là phải dựng lại bảng và chép 800 dòng của 25 course cũ sang. Bảng mới rẻ hơn, và nó mang sẵn song ngữ mà mạch 12 khoá bắt buộc phải có.
migration: 0183_unit_concepts_lesson_examples.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
unit_id | TEXT | NOT NULL REFERENCES curriculum_units(id) |
kind | TEXT | NOT NULL CHECK (kind IN ('key_concept', 'skill')) |
seq | INTEGER | NOT NULL |
label_vi | TEXT | NOT NULL |
label_en | TEXT | — |
detail_vi | TEXT | — |
detail_en | TEXT | — |
| — | table constraint | PRIMARY KEY (unit_id, kind, seq) |
example_vi | TEXT | — thêm ở 0203_curriculum_audience.sql |
example_en | TEXT | — thêm ở 0203_curriculum_audience.sql |
Khóa ngoại: unit_id → curriculum_units
curriculum_unit_elements
key_concept (3-5 mỗi unit) · material (2 ngữ liệu) · framework/model/formula/principle (2-3 khung tư duy).
migration: 0084_curriculum_courses.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
unit_id | TEXT | NOT NULL REFERENCES curriculum_units(id) |
kind | TEXT | NOT NULL CHECK (kind IN ('key_concept','material','framework','model','formula','principle')) |
seq | INTEGER | NOT NULL |
label_vi | TEXT | NOT NULL |
detail_vi | TEXT | — |
| — | table constraint | PRIMARY KEY (unit_id, kind, seq) |
Khóa ngoại: unit_id → curriculum_units
curriculum_unit_misconceptions
migration: 0086_course_specs_cp_english.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
unit_id | TEXT | NOT NULL REFERENCES curriculum_units(id) |
code | TEXT | NOT NULL |
text_vi | TEXT | NOT NULL |
| — | table constraint | PRIMARY KEY (unit_id, code) |
Khóa ngoại: unit_id → curriculum_units
curriculum_unit_outcomes
── Unit ──────────────────────────────────────────────────────────────────────────────────────── Outcome viết ở NGÔI THỨ NHẤT ("Tôi có thể…") đúng như chỉ đạo: chủ ngữ là learner, không phải chương trình. Câu "Học sinh nắm được…" mô tả việc dạy; "Tôi có thể…" mô tả việc học.
migration: 0085_curriculum_outcomes.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
unit_id | TEXT | NOT NULL REFERENCES curriculum_units(id) |
seq | INTEGER | NOT NULL |
text_vi | TEXT | NOT NULL |
| — | table constraint | PRIMARY KEY (unit_id, seq) |
text_en | TEXT | — thêm ở 0181_curriculum_bilingual.sql |
Khóa ngoại: unit_id → curriculum_units
curriculum_unit_prereq_notes
migration: 0086_course_specs_cp_english.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
unit_id | TEXT | NOT NULL REFERENCES curriculum_units(id) |
seq | INTEGER | NOT NULL |
text_vi | TEXT | NOT NULL |
| — | table constraint | PRIMARY KEY (unit_id, seq) |
Khóa ngoại: unit_id → curriculum_units
curriculum_unit_questions
Một Unit có thể có NHIỀU Essential Question, một Lesson có nhiều Guiding Question (spec của chủ dự án 2026-08-31 cho 2 EQ mỗi unit và 3-4 GQ mỗi lesson). Cột đơn essential_question_vi giữ nguyên và luôn chứa câu THỨ NHẤT — mọi thứ đang đọc nó vẫn chạy — còn bảng này giữ đủ bộ. Không đặt khoá ngoại tới curriculum_units(id) trên bảng câu hỏi của lesson và ngược lại: hai bảng tách nhau vì hai tầng có vòng đời khác nhau (unit sống lâu hơn lesson, xem stageOrphans).
migration: 0181_curriculum_bilingual.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
unit_id | TEXT | NOT NULL REFERENCES curriculum_units(id) |
seq | INTEGER | NOT NULL |
text_vi | TEXT | — |
text_en | TEXT | — |
| — | table constraint | PRIMARY KEY (unit_id, seq) |
Khóa ngoại: unit_id → curriculum_units
curriculum_unit_specs
migration: 0086_course_specs_cp_english.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
unit_id | TEXT | PRIMARY KEY REFERENCES curriculum_units(id) |
purpose_vi | TEXT | — |
progression_vi | TEXT | — |
mastery_criteria_vi | TEXT | — |
Khóa ngoại: unit_id → curriculum_units
curriculum_unit_takeaways
migration: 0185_key_takeaways.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
unit_id | TEXT | NOT NULL REFERENCES curriculum_units(id) |
seq | INTEGER | NOT NULL |
text_vi | TEXT | NOT NULL |
text_en | TEXT | — |
| — | table constraint | PRIMARY KEY (unit_id, seq) |
Khóa ngoại: unit_id → curriculum_units
curriculum_unit_tasks
Đúng MỘT Performance Task cho mỗi Unit (unit_id là khoá chính): nó là cửa ra của Unit, có hai cửa thì "hoàn thành Unit" mất nghĩa.
migration: 0085_curriculum_outcomes.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
unit_id | TEXT | PRIMARY KEY REFERENCES curriculum_units(id) |
title_vi | TEXT | NOT NULL |
prompt_vi | TEXT | NOT NULL |
product_vi | TEXT | NOT NULL |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
title_en | TEXT | — thêm ở 0181_curriculum_bilingual.sql |
prompt_en | TEXT | — thêm ở 0181_curriculum_bilingual.sql |
product_en | TEXT | — thêm ở 0181_curriculum_bilingual.sql |
Khóa ngoại: unit_id → curriculum_units
curriculum_units
migration: 0084_curriculum_courses.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
course_id | TEXT | NOT NULL REFERENCES curriculum_courses(id) |
seq | INTEGER | NOT NULL |
name_vi | TEXT | NOT NULL |
big_idea_vi | TEXT | — |
essential_question_vi | TEXT | — |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
name_en | TEXT | — thêm ở 0181_curriculum_bilingual.sql |
big_idea_en | TEXT | — thêm ở 0181_curriculum_bilingual.sql |
essential_question_en | TEXT | — thêm ở 0181_curriculum_bilingual.sql |
compelling_question_vi | TEXT | — thêm ở 0332_curriculum_units_compelling_question.sql |
compelling_question_en | TEXT | — thêm ở 0332_curriculum_units_compelling_question.sql |
Khóa ngoại: course_id → curriculum_courses
Index: idx_curriculum_units_slot(course_id, seq) UNIQUE
dictation_attempts
Mỗi lần learner bấm "Kiểm tra" ghi lại một dòng. Không phải để chấm điểm học bạ mà để biết câu nào cả lớp cùng sai — đó là dữ liệu nói lên ngữ liệu chỗ nào quá khó, thứ không đoán được.
migration: 0190_dictation.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
set_id | TEXT | NOT NULL REFERENCES dictation_sets(id) ON DELETE CASCADE |
learner_id | TEXT | NOT NULL |
scores_json | TEXT | NOT NULL, -- [{seq, percent}] — KHÔNG lưu chữ learner gõ |
overall | INTEGER | NOT NULL |
created_at | TEXT | NOT NULL DEFAULT (datetime('now')) |
Khóa ngoại: set_id → dictation_sets
Index: idx_dictation_attempts_set(set_id, created_at) · idx_dictation_attempts_learner(learner_id, created_at)
dictation_lines
migration: 0190_dictation.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
set_id | TEXT | NOT NULL REFERENCES dictation_sets(id) ON DELETE CASCADE |
seq | INTEGER | NOT NULL |
text | TEXT | NOT NULL |
start_ms | INTEGER | NOT NULL DEFAULT 0 |
end_ms | INTEGER | NOT NULL DEFAULT 0 |
| — | table constraint | PRIMARY KEY (set_id, seq) |
Khóa ngoại: set_id → dictation_sets
dictation_practice
── Một bài luyện của một learner ─────────────────────────────────────────────────────────────── status có 'pending'/'running' vì sinh chữ rồi sinh tiếng mất 15-40 giây: learner phải thấy được "đang làm" chứ không phải một trang trống rồi đột nhiên có bài. best_percent là điểm CAO NHẤT từng đạt, không phải điểm lần cuối. Hạn mức mở khoá dựa vào nó (chỉ đạo: nghe xong 100% một audio thì được tạo tiếp), mà một lần bấm nhầm "Kiểm tra" lúc ô còn trống không được phép lấy mất cái đã đạt.
migration: 0195_dictation_practice.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
learner_id | TEXT | NOT NULL |
seed_id | TEXT | REFERENCES dictation_seeds(id) |
seed_label_vi | TEXT | NOT NULL |
seed_label_en | TEXT | NOT NULL |
level | INTEGER | NOT NULL CHECK (level BETWEEN 1 AND 5) |
sentence_count | INTEGER | NOT NULL CHECK (sentence_count BETWEEN 3 AND 10) |
voice | TEXT | NOT NULL |
title_en | TEXT | NOT NULL DEFAULT '' |
status | TEXT | NOT NULL DEFAULT 'pending' |
| — | table constraint | CHECK (status IN ('pending','running','ready','failed')) |
error | TEXT | NOT NULL DEFAULT '' |
audio_mime | TEXT | NOT NULL DEFAULT 'audio/wav' |
duration_ms | INTEGER | NOT NULL DEFAULT 0 |
model | TEXT | NOT NULL DEFAULT '' |
best_percent | INTEGER | NOT NULL DEFAULT 0 |
attempts | INTEGER | NOT NULL DEFAULT 0 |
listen_ms | INTEGER | NOT NULL DEFAULT 0 |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
ready_at | TEXT | — |
Khóa ngoại: seed_id → dictation_seeds
Index: idx_dictation_practice_learner(learner_id, created_at DESC)
dictation_practice_lines
migration: 0195_dictation_practice.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
practice_id | TEXT | NOT NULL REFERENCES dictation_practice(id) ON DELETE CASCADE |
seq | INTEGER | NOT NULL |
text | TEXT | NOT NULL |
start_ms | INTEGER | NOT NULL DEFAULT 0 |
end_ms | INTEGER | NOT NULL DEFAULT 0 |
| — | table constraint | PRIMARY KEY (practice_id, seq) |
Khóa ngoại: practice_id → dictation_practice
dictation_seeds
── Kho hạt giống chủ đề ──────────────────────────────────────────────────────────────────────── group_slug là chuỗi TỰ DO, không phải enum: chủ dự án 2026-09-02 nói rõ "có thể có nhiều nhóm khác nữa, tôi liệt kê nhanh 4 nhóm đó thôi". Đóng cứng danh sách nhóm ở tầng schema thì thêm một nhóm là một lần deploy — mà thêm nhóm chính là việc người vận hành sẽ làm thường xuyên nhất.
migration: 0195_dictation_practice.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
group_slug | TEXT | NOT NULL |
group_vi | TEXT | NOT NULL |
group_en | TEXT | NOT NULL |
label_vi | TEXT | NOT NULL |
label_en | TEXT | NOT NULL |
brief_en | TEXT | NOT NULL |
enabled | INTEGER | NOT NULL DEFAULT 1 |
sort | INTEGER | NOT NULL DEFAULT 0 |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
updated_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
Index: idx_dictation_seeds_group(group_slug, sort)
dictation_sets
Bài tập chép chính tả cho chặng Investigate của course IELTS Core (SRC-668). Learner chọn một trong ba đoạn nghe (mỗi đoạn <= 2 phút), nghe rồi gõ lại TỪNG CÂU vào một ô riêng, bấm "Kiểm tra" và nhận phần trăm trùng khớp so với câu gốc. Bấm vào đầu mỗi ô là nghe lại ĐÚNG câu đó — nên mỗi câu phải có mốc thời gian trong file audio. Vì sao câu gốc nằm ở bảng RIÊNG chứ không phải một ô JSON trong dictation_sets: mốc thời gian (start_ms, end_ms) là thứ API trả cho trình duyệt, còn text là thứ TUYỆT ĐỐI KHÔNG được trả trước lúc chấm — hai loại dữ liệu có quyền đọc khác nhau thì không ở chung một ô. status mặc định 'draft': nội dung do máy sinh ra, chủ dự án duyệt rồi mới chuyển 'published'. API học chỉ đọc 'published' (xem modules/dictation/service.ts), nên một đoạn chưa duyệt không có đường nào lọt tới learner.
migration: 0190_dictation.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
lesson_id | TEXT | NOT NULL REFERENCES curriculum_lessons(id) ON DELETE CASCADE |
seq | INTEGER | NOT NULL |
topic | TEXT | NOT NULL, -- slug chủ đề, ví dụ 'film-up' | 'culture-japan' |
topic_label_vi | TEXT | NOT NULL DEFAULT '' |
title_vi | TEXT | NOT NULL |
title_en | TEXT | NOT NULL |
audio_mime | TEXT | NOT NULL DEFAULT 'audio/mpeg' |
duration_ms | INTEGER | NOT NULL DEFAULT 0 |
voice | TEXT | NOT NULL DEFAULT '', -- tên giọng của model TTS, để sinh lại đúng giọng cũ |
source_note | TEXT | NOT NULL DEFAULT '', -- ngữ liệu lấy ý từ đâu, cho người duyệt truy lại |
status | TEXT | NOT NULL DEFAULT 'draft' CHECK (status IN ('draft','published','retired')) |
created_at | TEXT | NOT NULL DEFAULT (datetime('now')) |
updated_at | TEXT | NOT NULL DEFAULT (datetime('now')) |
| — | table constraint | UNIQUE (lesson_id, seq) |
topic_label_en | TEXT | NOT NULL DEFAULT '' — thêm ở 0192_dictation_topic_label_en.sql |
Khóa ngoại: lesson_id → curriculum_lessons
Index: idx_dictation_sets_lesson(lesson_id, seq)
email_dispatches
Sổ CHỐNG GỬI TRÙNG cho thư gửi phụ huynh (SRC-683). Vì sao không dùng lại email_sends của 0205: hai bảng, hai vai. email_sends là sổ QUAN SÁT — ghi cả thư hỏng, ghi cả thư gửi lại, và một lỗi ghi sổ ở đó không được phép chặn một lá thư. Bảng này là KHOÁ: giành được hàng mới được gửi. Trộn hai vai vào một bảng thì hoặc mất thư, hoặc gửi hai lần — cả hai đều không hoàn tác được. dedupe_key là câu trả lời cho "đã gửi cái này chưa" của TỪNG loại thư, do mã sinh ra: welcome-parent → user_id (một đời một lần) diagnostic-ready → assessment session id milestone → learner_id:node_id (mỗi kỹ năng vững đúng một lần) exam-countdown → exam_id:30 | :14 | :3 (ba mốc, mỗi mốc một thư) inactivity-nudge → learner_id:ngày VN (kèm trần 7 ngày kiểm bằng email_sends) weekly-report → learner_id📧tuần trial-reminder → session_id:learner_id setup-nudge → user_id event-confirmation → event_id:user_id
migration: 0206_email_dispatch.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
template_id | TEXT | NOT NULL |
dedupe_key | TEXT | NOT NULL |
to_email | TEXT | NOT NULL |
learner_id | TEXT | REFERENCES learners(id) |
send_id | TEXT | — |
status | TEXT | NOT NULL DEFAULT 'claimed' CHECK (status IN ('claimed','sent','skipped','failed')) |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
| — | table constraint | PRIMARY KEY (template_id, dedupe_key) |
Khóa ngoại: learner_id → learners
Index: idx_email_dispatches_created(created_at DESC)
email_opens
Mỗi lần ảnh theo dõi được tải là một dòng. Không khoá duy nhất theo send: mở lại là dữ liệu thật, không phải trùng lặp.
migration: 0205_email_tracking.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
send_id | TEXT | NOT NULL REFERENCES email_sends(id) |
opened_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
user_agent | TEXT | — |
fetch_kind | TEXT | NOT NULL DEFAULT 'direct' CHECK (fetch_kind IN ('direct','proxy')) |
Khóa ngoại: send_id → email_sends
Index: idx_email_opens_send(send_id, opened_at DESC)
email_sends
Sổ thư đã gửi và sổ lượt mở (chỉ đạo chủ dự án 2026-09-07, SRC-682). Chỉ đạo: "Hệ thống cần tracking được ai mở email vào những thời điểm nào." HAI bảng chứ không một: một thư gửi đi có thể được mở NHIỀU lần, và câu hỏi của chủ dự án là "vào những thời điểm nào" — số nhiều. Nhét opened_at thành một cột trong bảng gửi thì lần mở thứ hai ghi đè lần thứ nhất và câu hỏi đó vĩnh viễn không trả lời được.
migration: 0205_email_tracking.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
template_id | TEXT | NOT NULL |
to_email | TEXT | NOT NULL |
learner_id | TEXT | REFERENCES learners(id) |
subject | TEXT | NOT NULL |
track_token | TEXT | NOT NULL UNIQUE |
status | TEXT | NOT NULL DEFAULT 'sent' CHECK (status IN ('sent','skipped','failed')) |
provider_id | TEXT | — |
error | TEXT | — |
sent_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
Khóa ngoại: learner_id → learners
Index: idx_email_sends_to(to_email, sent_at DESC) · idx_email_sends_template(template_id, sent_at DESC) · idx_email_sends_learner(learner_id, sent_at DESC)
email_suppressions
Danh sách KHÔNG gửi thư (SRC-684). Chỉ đạo chủ dự án 2026-09-07: loại hai hồ sơ ra khỏi danh sách gửi. Cách rẻ nhất là sửa thẳng learners.status thành 'archived' — và đó là cách SAI: trạng thái learner điều khiển cả việc học, báo cáo, quyền truy cập. Tắt thư bằng cách xoá sổ một đứa trẻ khỏi hệ thống là dùng dao mổ trâu, và ba tháng nữa không ai còn nhớ vì sao hồ sơ đó bị archive. Bảng này nói đúng một điều: "đừng gửi thư cho đối tượng này". Nó cũng chính là chỗ mà nút "ngừng nhận thư" của phụ huynh sẽ ghi vào khi được xây — một hệ gửi thư hàng loạt mà không có đường tắt là hệ sẽ bị đánh dấu spam.
migration: 0207_email_suppressions.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
scope | TEXT | NOT NULL CHECK (scope IN ('learner','email')) |
key | TEXT | NOT NULL |
reason | TEXT | NOT NULL |
created_by | TEXT | — |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
| — | table constraint | PRIMARY KEY (scope, key) |