1252 lines
32 KiB
Markdown
1252 lines
32 KiB
Markdown
# 📚 Hướng Dẫn Tạo và Quản Lý Subject, Chapter, Lesson
|
|
|
|
## Tổng Quan
|
|
|
|
Hệ thống nội dung được tổ chức theo mô hình phân cấp 3 tầng:
|
|
|
|
```
|
|
Subject (Môn học)
|
|
├── Chapter 1 (Chương 1)
|
|
│ ├── Lesson 1 (Bài học 1)
|
|
│ ├── Lesson 2 (Bài học 2)
|
|
│ └── Lesson 3 (Bài học 3)
|
|
├── Chapter 2 (Chương 2)
|
|
│ ├── Lesson 4 (Bài học 4)
|
|
│ └── Lesson 5 (Bài học 5)
|
|
└── Chapter 3 (Chương 3)
|
|
└── ...
|
|
```
|
|
|
|
### Mối quan hệ
|
|
- **Subject** (1) → **Chapter** (nhiều): Một môn học có nhiều chương
|
|
- **Chapter** (1) → **Lesson** (nhiều): Một chương có nhiều bài học
|
|
- **Lesson** → **Game** (optional): Bài học có thể tham chiếu đến Game engine
|
|
|
|
---
|
|
|
|
## 📊 Cấu Trúc Dữ Liệu
|
|
|
|
### 1. Subject (Môn học)
|
|
|
|
**Bảng**: `subjects`
|
|
**Mục đích**: Quản lý các môn học/giáo trình
|
|
|
|
| Trường | Kiểu | Mô tả |
|
|
|--------|------|-------|
|
|
| `id` | UUID | ID duy nhất |
|
|
| `subject_code` | VARCHAR(50) | Mã môn học (unique) |
|
|
| `subject_name` | VARCHAR(255) | Tên môn học (tiếng Việt) |
|
|
| `subject_name_en` | VARCHAR(255) | Tên tiếng Anh |
|
|
| `description` | TEXT | Mô tả chi tiết |
|
|
| `is_active` | BOOLEAN | Đang hoạt động |
|
|
| `is_premium` | BOOLEAN | Nội dung premium |
|
|
| `is_training` | BOOLEAN | Đào tạo nhân sự |
|
|
| `is_public` | BOOLEAN | Tự học công khai |
|
|
| `required_role` | VARCHAR(50) | Role yêu cầu |
|
|
| `min_subscription_tier` | VARCHAR(50) | Gói tối thiểu |
|
|
|
|
**Ví dụ**:
|
|
```json
|
|
{
|
|
"subject_code": "MATH_G1",
|
|
"subject_name": "Toán lớp 1",
|
|
"subject_name_en": "Math Grade 1",
|
|
"description": "Chương trình Toán học lớp 1 theo SGK mới",
|
|
"is_active": true,
|
|
"is_premium": false,
|
|
"is_training": false,
|
|
"is_public": true,
|
|
"required_role": "student",
|
|
"min_subscription_tier": "basic"
|
|
}
|
|
```
|
|
|
|
### 2. Chapter (Chương học)
|
|
|
|
**Bảng**: `chapters`
|
|
**Mục đích**: Chia môn học thành các chương
|
|
|
|
| Trường | Kiểu | Mô tả |
|
|
|--------|------|-------|
|
|
| `id` | UUID | ID duy nhất |
|
|
| `subject_id` | UUID | FK → subjects.id |
|
|
| `chapter_number` | INTEGER | Số thứ tự chương |
|
|
| `chapter_title` | VARCHAR(255) | Tiêu đề chương |
|
|
| `chapter_description` | TEXT | Mô tả nội dung |
|
|
| `duration_minutes` | INTEGER | Thời lượng ước tính |
|
|
| `is_published` | BOOLEAN | Đã xuất bản |
|
|
| `display_order` | INTEGER | Thứ tự hiển thị |
|
|
|
|
**Ví dụ**:
|
|
```json
|
|
{
|
|
"subject_id": "abc-123-xyz",
|
|
"chapter_number": 1,
|
|
"chapter_title": "Số và chữ số",
|
|
"chapter_description": "Làm quen với các số từ 1 đến 10",
|
|
"duration_minutes": 180,
|
|
"is_published": true,
|
|
"display_order": 1
|
|
}
|
|
```
|
|
|
|
### 3. Lesson (Bài học)
|
|
|
|
**Bảng**: `lessons`
|
|
**Mục đích**: Nội dung giảng dạy cụ thể
|
|
|
|
| Trường | Kiểu | Mô tả |
|
|
|--------|------|-------|
|
|
| `id` | UUID | ID duy nhất |
|
|
| `chapter_id` | UUID | FK → chapters.id |
|
|
| `lesson_number` | INTEGER | Số thứ tự bài học |
|
|
| `lesson_title` | VARCHAR(255) | Tiêu đề bài học |
|
|
| `lesson_type` | ENUM | `json_content` hoặc `url_content` |
|
|
| `lesson_description` | TEXT | Mô tả bài học |
|
|
| `content_json` | JSON | Nội dung tương tác |
|
|
| `content_url` | VARCHAR(500) | Link video/PDF |
|
|
| `content_type` | VARCHAR(50) | youtube, pdf, audio |
|
|
| `duration_minutes` | INTEGER | Thời lượng |
|
|
| `is_published` | BOOLEAN | Đã xuất bản |
|
|
| `is_free` | BOOLEAN | Học thử miễn phí |
|
|
| `display_order` | INTEGER | Thứ tự hiển thị |
|
|
| `thumbnail_url` | VARCHAR(500) | Ảnh thumbnail |
|
|
|
|
**Ví dụ Lesson JSON Content**:
|
|
```json
|
|
{
|
|
"chapter_id": "def-456-uvw",
|
|
"lesson_number": 1,
|
|
"lesson_title": "Đếm từ 1 đến 5",
|
|
"lesson_type": "json_content",
|
|
"lesson_description": "Học đếm các số từ 1 đến 5 qua trò chơi",
|
|
"content_json": {
|
|
"type": "counting_quiz",
|
|
"questions": [
|
|
{
|
|
"id": 1,
|
|
"question": "Có bao nhiêu quả táo?",
|
|
"image": "https://cdn.senaai.tech/images/apples-3.png",
|
|
"correct_answer": 3,
|
|
"options": [2, 3, 4, 5]
|
|
}
|
|
],
|
|
"pass_score": 70
|
|
},
|
|
"duration_minutes": 15,
|
|
"is_published": true,
|
|
"is_free": true,
|
|
"display_order": 1
|
|
}
|
|
```
|
|
|
|
**Ví dụ Lesson URL Content**:
|
|
```json
|
|
{
|
|
"chapter_id": "def-456-uvw",
|
|
"lesson_number": 2,
|
|
"lesson_title": "Video: Hướng dẫn đếm số",
|
|
"lesson_type": "url_content",
|
|
"content_url": "https://www.youtube.com/watch?v=abc123",
|
|
"content_type": "youtube",
|
|
"duration_minutes": 10,
|
|
"is_published": true,
|
|
"is_free": false,
|
|
"display_order": 2
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🚀 Hướng Dẫn Tạo Nội Dung
|
|
|
|
### Quy trình 3 bước
|
|
|
|
```
|
|
Bước 1: Tạo Subject (Môn học)
|
|
↓
|
|
Bước 2: Tạo Chapter (Chương) - liên kết với Subject
|
|
↓
|
|
Bước 3: Tạo Lesson (Bài học) - liên kết với Chapter
|
|
```
|
|
|
|
---
|
|
|
|
## 📝 Bước 1: Tạo Subject (Môn học)
|
|
|
|
### API Endpoint
|
|
```
|
|
POST /api/subjects
|
|
Content-Type: application/json
|
|
Authorization: Bearer <token>
|
|
```
|
|
|
|
### Request Body
|
|
```json
|
|
{
|
|
"subject_code": "MATH_G1",
|
|
"subject_name": "Toán lớp 1",
|
|
"subject_name_en": "Math Grade 1",
|
|
"description": "Chương trình Toán học lớp 1 theo SGK mới, bao gồm số học cơ bản, phép cộng trừ, hình học đơn giản",
|
|
"is_active": true,
|
|
"is_premium": false,
|
|
"is_training": false,
|
|
"is_public": true,
|
|
"required_role": "student",
|
|
"min_subscription_tier": "basic"
|
|
}
|
|
```
|
|
|
|
### Response (201 Created)
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "Subject created successfully",
|
|
"data": {
|
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"subject_code": "MATH_G1",
|
|
"subject_name": "Toán lớp 1",
|
|
"subject_name_en": "Math Grade 1",
|
|
"description": "Chương trình Toán học lớp 1...",
|
|
"is_active": true,
|
|
"is_premium": false,
|
|
"is_training": false,
|
|
"is_public": true,
|
|
"required_role": "student",
|
|
"min_subscription_tier": "basic",
|
|
"created_at": "2026-01-20T10:00:00.000Z",
|
|
"updated_at": "2026-01-20T10:00:00.000Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Ví dụ với cURL
|
|
```bash
|
|
curl -X POST http://localhost:3000/api/subjects \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
|
-d '{
|
|
"subject_code": "MATH_G1",
|
|
"subject_name": "Toán lớp 1",
|
|
"subject_name_en": "Math Grade 1",
|
|
"description": "Chương trình Toán học lớp 1 theo SGK mới",
|
|
"is_active": true,
|
|
"is_premium": false,
|
|
"is_public": true
|
|
}'
|
|
```
|
|
|
|
### Ví dụ với Node.js (Sequelize)
|
|
```javascript
|
|
const { Subject } = require('./models');
|
|
|
|
const mathSubject = await Subject.create({
|
|
subject_code: 'MATH_G1',
|
|
subject_name: 'Toán lớp 1',
|
|
subject_name_en: 'Math Grade 1',
|
|
description: 'Chương trình Toán học lớp 1 theo SGK mới',
|
|
is_active: true,
|
|
is_premium: false,
|
|
is_training: false,
|
|
is_public: true,
|
|
required_role: 'student',
|
|
min_subscription_tier: 'basic'
|
|
});
|
|
|
|
console.log('Subject ID:', mathSubject.id);
|
|
// Lưu ID này để tạo Chapter
|
|
```
|
|
|
|
### Các Subject mẫu khác
|
|
|
|
**Tiếng Việt lớp 1**:
|
|
```json
|
|
{
|
|
"subject_code": "VIET_G1",
|
|
"subject_name": "Tiếng Việt lớp 1",
|
|
"subject_name_en": "Vietnamese Grade 1",
|
|
"description": "Học chữ cái, đọc, viết và hiểu văn bản đơn giản",
|
|
"is_active": true,
|
|
"is_premium": false,
|
|
"is_public": true
|
|
}
|
|
```
|
|
|
|
**Tiếng Anh lớp 1**:
|
|
```json
|
|
{
|
|
"subject_code": "ENG_G1",
|
|
"subject_name": "Tiếng Anh lớp 1",
|
|
"subject_name_en": "English Grade 1",
|
|
"description": "Làm quen với bảng chữ cái, từ vựng và câu giao tiếp cơ bản",
|
|
"is_active": true,
|
|
"is_premium": true,
|
|
"is_public": false,
|
|
"min_subscription_tier": "premium"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📖 Bước 2: Tạo Chapter (Chương)
|
|
|
|
### API Endpoint
|
|
```
|
|
POST /api/chapters
|
|
Content-Type: application/json
|
|
Authorization: Bearer <token>
|
|
```
|
|
|
|
### Request Body
|
|
```json
|
|
{
|
|
"subject_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"chapter_number": 1,
|
|
"chapter_title": "Số và chữ số",
|
|
"chapter_description": "Làm quen với các số từ 1 đến 10, học đếm và nhận biết chữ số",
|
|
"duration_minutes": 180,
|
|
"is_published": true,
|
|
"display_order": 1
|
|
}
|
|
```
|
|
|
|
### Response (201 Created)
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "Chapter created successfully",
|
|
"data": {
|
|
"id": "660e8400-e29b-41d4-a716-446655440001",
|
|
"subject_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"chapter_number": 1,
|
|
"chapter_title": "Số và chữ số",
|
|
"chapter_description": "Làm quen với các số từ 1 đến 10...",
|
|
"duration_minutes": 180,
|
|
"is_published": true,
|
|
"display_order": 1,
|
|
"created_at": "2026-01-20T10:05:00.000Z",
|
|
"updated_at": "2026-01-20T10:05:00.000Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Ví dụ với Node.js
|
|
```javascript
|
|
const { Chapter } = require('./models');
|
|
|
|
// Sử dụng subject_id từ bước 1
|
|
const chapter1 = await Chapter.create({
|
|
subject_id: mathSubject.id, // ID từ bước 1
|
|
chapter_number: 1,
|
|
chapter_title: 'Số và chữ số',
|
|
chapter_description: 'Làm quen với các số từ 1 đến 10, học đếm và nhận biết chữ số',
|
|
duration_minutes: 180,
|
|
is_published: true,
|
|
display_order: 1
|
|
});
|
|
|
|
console.log('Chapter ID:', chapter1.id);
|
|
|
|
// Tạo Chapter 2
|
|
const chapter2 = await Chapter.create({
|
|
subject_id: mathSubject.id,
|
|
chapter_number: 2,
|
|
chapter_title: 'Phép cộng trong phạm vi 10',
|
|
chapter_description: 'Học cách cộng các số từ 1 đến 10',
|
|
duration_minutes: 240,
|
|
is_published: true,
|
|
display_order: 2
|
|
});
|
|
|
|
// Tạo Chapter 3
|
|
const chapter3 = await Chapter.create({
|
|
subject_id: mathSubject.id,
|
|
chapter_number: 3,
|
|
chapter_title: 'Phép trừ trong phạm vi 10',
|
|
chapter_description: 'Học cách trừ các số từ 1 đến 10',
|
|
duration_minutes: 240,
|
|
is_published: false, // Chưa xuất bản
|
|
display_order: 3
|
|
});
|
|
```
|
|
|
|
### Tạo nhiều Chapter cùng lúc
|
|
```javascript
|
|
const chapters = await Chapter.bulkCreate([
|
|
{
|
|
subject_id: mathSubject.id,
|
|
chapter_number: 1,
|
|
chapter_title: 'Số và chữ số',
|
|
chapter_description: 'Làm quen với các số từ 1 đến 10',
|
|
duration_minutes: 180,
|
|
is_published: true,
|
|
display_order: 1
|
|
},
|
|
{
|
|
subject_id: mathSubject.id,
|
|
chapter_number: 2,
|
|
chapter_title: 'Phép cộng trong phạm vi 10',
|
|
chapter_description: 'Học cộng các số từ 1 đến 10',
|
|
duration_minutes: 240,
|
|
is_published: true,
|
|
display_order: 2
|
|
},
|
|
{
|
|
subject_id: mathSubject.id,
|
|
chapter_number: 3,
|
|
chapter_title: 'Phép trừ trong phạm vi 10',
|
|
chapter_description: 'Học trừ các số từ 1 đến 10',
|
|
duration_minutes: 240,
|
|
is_published: true,
|
|
display_order: 3
|
|
}
|
|
]);
|
|
|
|
console.log(`Created ${chapters.length} chapters`);
|
|
```
|
|
|
|
---
|
|
|
|
## 📚 Bước 3: Tạo Lesson (Bài học)
|
|
|
|
### 3.1. Lesson với JSON Content (Tương tác)
|
|
|
|
#### API Endpoint
|
|
```
|
|
POST /api/lessons
|
|
Content-Type: application/json
|
|
Authorization: Bearer <token>
|
|
```
|
|
|
|
#### Request Body
|
|
```json
|
|
{
|
|
"chapter_id": "660e8400-e29b-41d4-a716-446655440001",
|
|
"lesson_number": 1,
|
|
"lesson_title": "Đếm từ 1 đến 5",
|
|
"lesson_type": "json_content",
|
|
"lesson_description": "Học đếm các số từ 1 đến 5 qua trò chơi tương tác",
|
|
"content_json": {
|
|
"type": "counting_quiz",
|
|
"theme": "fruits",
|
|
"questions": [
|
|
{
|
|
"id": 1,
|
|
"question": "Có bao nhiêu quả táo?",
|
|
"image": "https://cdn.senaai.tech/images/apples-3.png",
|
|
"correct_answer": 3,
|
|
"options": [2, 3, 4, 5]
|
|
},
|
|
{
|
|
"id": 2,
|
|
"question": "Có bao nhiêu quả cam?",
|
|
"image": "https://cdn.senaai.tech/images/oranges-5.png",
|
|
"correct_answer": 5,
|
|
"options": [3, 4, 5, 6]
|
|
}
|
|
],
|
|
"instructions": "Nhìn vào hình và đếm số lượng vật, sau đó chọn đáp án đúng",
|
|
"pass_score": 70,
|
|
"max_attempts": 3,
|
|
"show_hints": true
|
|
},
|
|
"duration_minutes": 15,
|
|
"is_published": true,
|
|
"is_free": true,
|
|
"display_order": 1,
|
|
"thumbnail_url": "https://cdn.senaai.tech/thumbnails/lesson-counting.jpg"
|
|
}
|
|
```
|
|
|
|
#### Response (201 Created)
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "Lesson created successfully",
|
|
"data": {
|
|
"id": "770e8400-e29b-41d4-a716-446655440002",
|
|
"chapter_id": "660e8400-e29b-41d4-a716-446655440001",
|
|
"lesson_number": 1,
|
|
"lesson_title": "Đếm từ 1 đến 5",
|
|
"lesson_type": "json_content",
|
|
"lesson_description": "Học đếm các số từ 1 đến 5...",
|
|
"content_json": { /* ... */ },
|
|
"content_url": null,
|
|
"content_type": null,
|
|
"duration_minutes": 15,
|
|
"is_published": true,
|
|
"is_free": true,
|
|
"display_order": 1,
|
|
"thumbnail_url": "https://cdn.senaai.tech/thumbnails/lesson-counting.jpg",
|
|
"created_at": "2026-01-20T10:10:00.000Z",
|
|
"updated_at": "2026-01-20T10:10:00.000Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Ví dụ với Node.js
|
|
```javascript
|
|
const { Lesson } = require('./models');
|
|
|
|
const lesson1 = await Lesson.create({
|
|
chapter_id: chapter1.id, // ID từ bước 2
|
|
lesson_number: 1,
|
|
lesson_title: 'Đếm từ 1 đến 5',
|
|
lesson_type: 'json_content',
|
|
lesson_description: 'Học đếm các số từ 1 đến 5 qua trò chơi tương tác',
|
|
content_json: {
|
|
type: 'counting_quiz', // Khớp với Game.type
|
|
theme: 'fruits',
|
|
questions: [
|
|
{
|
|
id: 1,
|
|
question: 'Có bao nhiêu quả táo?',
|
|
image: 'https://cdn.senaai.tech/images/apples-3.png',
|
|
correct_answer: 3,
|
|
options: [2, 3, 4, 5]
|
|
},
|
|
{
|
|
id: 2,
|
|
question: 'Có bao nhiêu quả cam?',
|
|
image: 'https://cdn.senaai.tech/images/oranges-5.png',
|
|
correct_answer: 5,
|
|
options: [3, 4, 5, 6]
|
|
}
|
|
],
|
|
instructions: 'Nhìn vào hình và đếm số lượng vật, sau đó chọn đáp án đúng',
|
|
pass_score: 70,
|
|
max_attempts: 3,
|
|
show_hints: true
|
|
},
|
|
duration_minutes: 15,
|
|
is_published: true,
|
|
is_free: true,
|
|
display_order: 1,
|
|
thumbnail_url: 'https://cdn.senaai.tech/thumbnails/lesson-counting.jpg'
|
|
});
|
|
|
|
console.log('Lesson ID:', lesson1.id);
|
|
```
|
|
|
|
### 3.2. Lesson với URL Content (Video/PDF)
|
|
|
|
#### Video YouTube
|
|
```javascript
|
|
const lesson2 = await Lesson.create({
|
|
chapter_id: chapter1.id,
|
|
lesson_number: 2,
|
|
lesson_title: 'Video: Hướng dẫn đếm số',
|
|
lesson_type: 'url_content',
|
|
lesson_description: 'Video hướng dẫn cách đếm số từ 1 đến 10',
|
|
content_url: 'https://www.youtube.com/watch?v=abc123xyz',
|
|
content_type: 'youtube',
|
|
duration_minutes: 10,
|
|
is_published: true,
|
|
is_free: false,
|
|
display_order: 2,
|
|
thumbnail_url: 'https://img.youtube.com/vi/abc123xyz/maxresdefault.jpg'
|
|
});
|
|
```
|
|
|
|
#### PDF Document
|
|
```javascript
|
|
const lesson3 = await Lesson.create({
|
|
chapter_id: chapter1.id,
|
|
lesson_number: 3,
|
|
lesson_title: 'Tài liệu: Bảng số từ 1-10',
|
|
lesson_type: 'url_content',
|
|
lesson_description: 'Tài liệu PDF hướng dẫn nhận biết và viết các số',
|
|
content_url: 'https://cdn.senaai.tech/docs/number-chart-1-10.pdf',
|
|
content_type: 'pdf',
|
|
duration_minutes: 5,
|
|
is_published: true,
|
|
is_free: true,
|
|
display_order: 3
|
|
});
|
|
```
|
|
|
|
#### Audio File
|
|
```javascript
|
|
const lesson4 = await Lesson.create({
|
|
chapter_id: chapter1.id,
|
|
lesson_number: 4,
|
|
lesson_title: 'Nghe: Phát âm các số',
|
|
lesson_type: 'url_content',
|
|
lesson_description: 'Học cách phát âm đúng các số từ 1 đến 10',
|
|
content_url: 'https://cdn.senaai.tech/audio/numbers-pronunciation.mp3',
|
|
content_type: 'audio',
|
|
duration_minutes: 8,
|
|
is_published: true,
|
|
is_free: true,
|
|
display_order: 4
|
|
});
|
|
```
|
|
|
|
### 3.3. Tạo nhiều Lesson cùng lúc
|
|
```javascript
|
|
const lessons = await Lesson.bulkCreate([
|
|
{
|
|
chapter_id: chapter1.id,
|
|
lesson_number: 1,
|
|
lesson_title: 'Đếm từ 1 đến 5',
|
|
lesson_type: 'json_content',
|
|
content_json: {
|
|
type: 'counting_quiz',
|
|
questions: [/* ... */]
|
|
},
|
|
duration_minutes: 15,
|
|
is_published: true,
|
|
is_free: true,
|
|
display_order: 1
|
|
},
|
|
{
|
|
chapter_id: chapter1.id,
|
|
lesson_number: 2,
|
|
lesson_title: 'Đếm từ 6 đến 10',
|
|
lesson_type: 'json_content',
|
|
content_json: {
|
|
type: 'counting_quiz',
|
|
questions: [/* ... */]
|
|
},
|
|
duration_minutes: 15,
|
|
is_published: true,
|
|
is_free: true,
|
|
display_order: 2
|
|
},
|
|
{
|
|
chapter_id: chapter1.id,
|
|
lesson_number: 3,
|
|
lesson_title: 'Video: Hướng dẫn đếm số',
|
|
lesson_type: 'url_content',
|
|
content_url: 'https://www.youtube.com/watch?v=abc123',
|
|
content_type: 'youtube',
|
|
duration_minutes: 10,
|
|
is_published: true,
|
|
is_free: false,
|
|
display_order: 3
|
|
}
|
|
]);
|
|
|
|
console.log(`Created ${lessons.length} lessons`);
|
|
```
|
|
|
|
---
|
|
|
|
## 🔍 Truy Vấn và Quản Lý
|
|
|
|
### Lấy danh sách Subject
|
|
```
|
|
GET /api/subjects
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": [
|
|
{
|
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"subject_code": "MATH_G1",
|
|
"subject_name": "Toán lớp 1",
|
|
"is_active": true,
|
|
"chapter_count": 3
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Lấy chi tiết Subject
|
|
```
|
|
GET /api/subjects/:id
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"subject_code": "MATH_G1",
|
|
"subject_name": "Toán lớp 1",
|
|
"description": "Chương trình Toán học lớp 1...",
|
|
"chapters": [
|
|
{
|
|
"id": "660e8400-e29b-41d4-a716-446655440001",
|
|
"chapter_number": 1,
|
|
"chapter_title": "Số và chữ số",
|
|
"lesson_count": 5
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Lấy danh sách Chapter của Subject
|
|
```
|
|
GET /api/subjects/:subject_id/chapters
|
|
```
|
|
|
|
### Lấy danh sách Lesson của Chapter
|
|
```
|
|
GET /api/chapters/:chapter_id/lessons
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": [
|
|
{
|
|
"id": "770e8400-e29b-41d4-a716-446655440002",
|
|
"lesson_number": 1,
|
|
"lesson_title": "Đếm từ 1 đến 5",
|
|
"lesson_type": "json_content",
|
|
"duration_minutes": 15,
|
|
"is_free": true,
|
|
"thumbnail_url": "https://cdn.senaai.tech/thumbnails/lesson-counting.jpg"
|
|
},
|
|
{
|
|
"id": "770e8400-e29b-41d4-a716-446655440003",
|
|
"lesson_number": 2,
|
|
"lesson_title": "Video: Hướng dẫn đếm số",
|
|
"lesson_type": "url_content",
|
|
"duration_minutes": 10,
|
|
"is_free": false
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Lấy chi tiết Lesson
|
|
```
|
|
GET /api/lessons/:id
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"id": "770e8400-e29b-41d4-a716-446655440002",
|
|
"chapter_id": "660e8400-e29b-41d4-a716-446655440001",
|
|
"lesson_number": 1,
|
|
"lesson_title": "Đếm từ 1 đến 5",
|
|
"lesson_type": "json_content",
|
|
"lesson_description": "Học đếm các số từ 1 đến 5...",
|
|
"content_json": {
|
|
"type": "counting_quiz",
|
|
"questions": [/* ... */],
|
|
"pass_score": 70
|
|
},
|
|
"duration_minutes": 15,
|
|
"is_published": true,
|
|
"is_free": true,
|
|
"chapter": {
|
|
"id": "660e8400-e29b-41d4-a716-446655440001",
|
|
"chapter_title": "Số và chữ số",
|
|
"subject": {
|
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"subject_name": "Toán lớp 1"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## ✏️ Cập Nhật Nội Dung
|
|
|
|
### Cập nhật Subject
|
|
```
|
|
PUT /api/subjects/:id
|
|
Content-Type: application/json
|
|
```
|
|
|
|
**Request**:
|
|
```json
|
|
{
|
|
"subject_name": "Toán lớp 1 - Chương trình nâng cao",
|
|
"description": "Chương trình Toán học lớp 1 nâng cao với nhiều bài tập thực hành",
|
|
"is_premium": true,
|
|
"min_subscription_tier": "premium"
|
|
}
|
|
```
|
|
|
|
### Cập nhật Chapter
|
|
```
|
|
PUT /api/chapters/:id
|
|
Content-Type: application/json
|
|
```
|
|
|
|
**Request**:
|
|
```json
|
|
{
|
|
"chapter_title": "Số và chữ số (cập nhật)",
|
|
"chapter_description": "Làm quen với các số từ 1 đến 20",
|
|
"duration_minutes": 240,
|
|
"is_published": true
|
|
}
|
|
```
|
|
|
|
### Cập nhật Lesson
|
|
```
|
|
PUT /api/lessons/:id
|
|
Content-Type: application/json
|
|
```
|
|
|
|
**Request**:
|
|
```json
|
|
{
|
|
"lesson_title": "Đếm từ 1 đến 10 (mở rộng)",
|
|
"content_json": {
|
|
"type": "counting_quiz",
|
|
"questions": [
|
|
/* Thêm câu hỏi mới */
|
|
],
|
|
"pass_score": 80
|
|
},
|
|
"duration_minutes": 20,
|
|
"is_free": false
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🗑️ Xóa Nội Dung
|
|
|
|
### Xóa Subject
|
|
```
|
|
DELETE /api/subjects/:id
|
|
```
|
|
|
|
**Lưu ý**: Xóa Subject sẽ xóa tất cả Chapter và Lesson liên quan (nếu có cascade)
|
|
|
|
### Xóa Chapter
|
|
```
|
|
DELETE /api/chapters/:id
|
|
```
|
|
|
|
**Lưu ý**: Xóa Chapter sẽ xóa tất cả Lesson liên quan
|
|
|
|
### Xóa Lesson
|
|
```
|
|
DELETE /api/lessons/:id
|
|
```
|
|
|
|
---
|
|
|
|
## 📊 Script Tạo Dữ Liệu Mẫu Đầy Đủ
|
|
|
|
```javascript
|
|
// scripts/seed-sample-content.js
|
|
const { Subject, Chapter, Lesson } = require('../models');
|
|
|
|
async function seedSampleContent() {
|
|
try {
|
|
console.log('🌱 Seeding sample content...');
|
|
|
|
// 1. TẠO SUBJECT: Toán lớp 1
|
|
const mathSubject = await Subject.create({
|
|
subject_code: 'MATH_G1',
|
|
subject_name: 'Toán lớp 1',
|
|
subject_name_en: 'Math Grade 1',
|
|
description: 'Chương trình Toán học lớp 1 theo SGK mới',
|
|
is_active: true,
|
|
is_premium: false,
|
|
is_training: false,
|
|
is_public: true,
|
|
required_role: 'student',
|
|
min_subscription_tier: 'basic'
|
|
});
|
|
console.log('✅ Created Subject: Toán lớp 1');
|
|
|
|
// 2. TẠO CHAPTER 1: Số và chữ số
|
|
const chapter1 = await Chapter.create({
|
|
subject_id: mathSubject.id,
|
|
chapter_number: 1,
|
|
chapter_title: 'Số và chữ số',
|
|
chapter_description: 'Làm quen với các số từ 1 đến 10',
|
|
duration_minutes: 180,
|
|
is_published: true,
|
|
display_order: 1
|
|
});
|
|
console.log('✅ Created Chapter 1: Số và chữ số');
|
|
|
|
// 3. TẠO LESSON cho Chapter 1
|
|
await Lesson.bulkCreate([
|
|
{
|
|
chapter_id: chapter1.id,
|
|
lesson_number: 1,
|
|
lesson_title: 'Đếm từ 1 đến 5',
|
|
lesson_type: 'json_content',
|
|
lesson_description: 'Học đếm các số từ 1 đến 5 qua trò chơi',
|
|
content_json: {
|
|
type: 'counting_quiz',
|
|
theme: 'fruits',
|
|
questions: [
|
|
{
|
|
id: 1,
|
|
question: 'Có bao nhiêu quả táo?',
|
|
image: 'https://cdn.senaai.tech/images/apples-3.png',
|
|
correct_answer: 3,
|
|
options: [2, 3, 4, 5]
|
|
},
|
|
{
|
|
id: 2,
|
|
question: 'Có bao nhiêu quả cam?',
|
|
image: 'https://cdn.senaai.tech/images/oranges-5.png',
|
|
correct_answer: 5,
|
|
options: [3, 4, 5, 6]
|
|
}
|
|
],
|
|
pass_score: 70
|
|
},
|
|
duration_minutes: 15,
|
|
is_published: true,
|
|
is_free: true,
|
|
display_order: 1
|
|
},
|
|
{
|
|
chapter_id: chapter1.id,
|
|
lesson_number: 2,
|
|
lesson_title: 'Video: Hướng dẫn đếm số',
|
|
lesson_type: 'url_content',
|
|
lesson_description: 'Video hướng dẫn cách đếm số từ 1 đến 10',
|
|
content_url: 'https://www.youtube.com/watch?v=sample123',
|
|
content_type: 'youtube',
|
|
duration_minutes: 10,
|
|
is_published: true,
|
|
is_free: false,
|
|
display_order: 2
|
|
},
|
|
{
|
|
chapter_id: chapter1.id,
|
|
lesson_number: 3,
|
|
lesson_title: 'Đếm từ 6 đến 10',
|
|
lesson_type: 'json_content',
|
|
lesson_description: 'Tiếp tục học đếm các số từ 6 đến 10',
|
|
content_json: {
|
|
type: 'counting_quiz',
|
|
theme: 'animals',
|
|
questions: [
|
|
{
|
|
id: 1,
|
|
question: 'Có bao nhiêu con mèo?',
|
|
image: 'https://cdn.senaai.tech/images/cats-7.png',
|
|
correct_answer: 7,
|
|
options: [6, 7, 8, 9]
|
|
},
|
|
{
|
|
id: 2,
|
|
question: 'Có bao nhiêu con chó?',
|
|
image: 'https://cdn.senaai.tech/images/dogs-10.png',
|
|
correct_answer: 10,
|
|
options: [8, 9, 10, 11]
|
|
}
|
|
],
|
|
pass_score: 70
|
|
},
|
|
duration_minutes: 15,
|
|
is_published: true,
|
|
is_free: true,
|
|
display_order: 3
|
|
}
|
|
]);
|
|
console.log('✅ Created 3 Lessons for Chapter 1');
|
|
|
|
// 4. TẠO CHAPTER 2: Phép cộng
|
|
const chapter2 = await Chapter.create({
|
|
subject_id: mathSubject.id,
|
|
chapter_number: 2,
|
|
chapter_title: 'Phép cộng trong phạm vi 10',
|
|
chapter_description: 'Học cộng các số từ 1 đến 10',
|
|
duration_minutes: 240,
|
|
is_published: true,
|
|
display_order: 2
|
|
});
|
|
console.log('✅ Created Chapter 2: Phép cộng');
|
|
|
|
// 5. TẠO LESSON cho Chapter 2
|
|
await Lesson.bulkCreate([
|
|
{
|
|
chapter_id: chapter2.id,
|
|
lesson_number: 1,
|
|
lesson_title: 'Phép cộng cơ bản',
|
|
lesson_type: 'json_content',
|
|
lesson_description: 'Học phép cộng 2 số trong phạm vi 5',
|
|
content_json: {
|
|
type: 'math_practice',
|
|
operation: 'addition',
|
|
range: { min: 1, max: 5 },
|
|
question_count: 10,
|
|
show_solution_steps: true
|
|
},
|
|
duration_minutes: 20,
|
|
is_published: true,
|
|
is_free: true,
|
|
display_order: 1
|
|
},
|
|
{
|
|
chapter_id: chapter2.id,
|
|
lesson_number: 2,
|
|
lesson_title: 'Thực hành phép cộng',
|
|
lesson_type: 'json_content',
|
|
lesson_description: 'Luyện tập phép cộng trong phạm vi 10',
|
|
content_json: {
|
|
type: 'math_practice',
|
|
operation: 'addition',
|
|
range: { min: 1, max: 10 },
|
|
question_count: 20,
|
|
show_solution_steps: true,
|
|
allow_calculator: false
|
|
},
|
|
duration_minutes: 25,
|
|
is_published: true,
|
|
is_free: false,
|
|
display_order: 2
|
|
}
|
|
]);
|
|
console.log('✅ Created 2 Lessons for Chapter 2');
|
|
|
|
// 6. TẠO SUBJECT: Tiếng Việt lớp 1
|
|
const vietnameseSubject = await Subject.create({
|
|
subject_code: 'VIET_G1',
|
|
subject_name: 'Tiếng Việt lớp 1',
|
|
subject_name_en: 'Vietnamese Grade 1',
|
|
description: 'Học chữ cái, đọc, viết và hiểu văn bản đơn giản',
|
|
is_active: true,
|
|
is_premium: false,
|
|
is_public: true
|
|
});
|
|
console.log('✅ Created Subject: Tiếng Việt lớp 1');
|
|
|
|
// 7. TẠO CHAPTER cho Tiếng Việt
|
|
const vnChapter1 = await Chapter.create({
|
|
subject_id: vietnameseSubject.id,
|
|
chapter_number: 1,
|
|
chapter_title: 'Bảng chữ cái tiếng Việt',
|
|
chapter_description: 'Học 29 chữ cái tiếng Việt',
|
|
duration_minutes: 300,
|
|
is_published: true,
|
|
display_order: 1
|
|
});
|
|
console.log('✅ Created Chapter: Bảng chữ cái tiếng Việt');
|
|
|
|
// 8. TẠO LESSON cho Tiếng Việt
|
|
await Lesson.create({
|
|
chapter_id: vnChapter1.id,
|
|
lesson_number: 1,
|
|
lesson_title: 'Học chữ A, B, C',
|
|
lesson_type: 'json_content',
|
|
lesson_description: 'Làm quen với 3 chữ cái đầu tiên',
|
|
content_json: {
|
|
type: 'alphabet_learning',
|
|
letters: ['A', 'B', 'C'],
|
|
activities: ['recognize', 'write', 'pronounce']
|
|
},
|
|
duration_minutes: 30,
|
|
is_published: true,
|
|
is_free: true,
|
|
display_order: 1
|
|
});
|
|
console.log('✅ Created Lesson: Học chữ A, B, C');
|
|
|
|
console.log('\n🎉 Sample content seeded successfully!');
|
|
console.log('\nSummary:');
|
|
console.log('- Subjects: 2 (Toán lớp 1, Tiếng Việt lớp 1)');
|
|
console.log('- Chapters: 3 (Số và chữ số, Phép cộng, Bảng chữ cái)');
|
|
console.log('- Lessons: 6');
|
|
|
|
} catch (error) {
|
|
console.error('❌ Error seeding data:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Chạy script
|
|
seedSampleContent()
|
|
.then(() => {
|
|
console.log('\n✅ Done!');
|
|
process.exit(0);
|
|
})
|
|
.catch(err => {
|
|
console.error('\n❌ Failed:', err);
|
|
process.exit(1);
|
|
});
|
|
```
|
|
|
|
### Chạy script
|
|
```bash
|
|
node scripts/seed-sample-content.js
|
|
```
|
|
|
|
---
|
|
|
|
## 🎯 Workflow Thực Tế
|
|
|
|
### Scenario 1: Thêm môn học mới hoàn chỉnh
|
|
|
|
```javascript
|
|
// 1. Tạo Subject
|
|
const englishSubject = await Subject.create({
|
|
subject_code: 'ENG_G1',
|
|
subject_name: 'Tiếng Anh lớp 1',
|
|
is_active: true,
|
|
is_premium: true,
|
|
is_public: false
|
|
});
|
|
|
|
// 2. Tạo nhiều Chapter
|
|
const chapters = await Chapter.bulkCreate([
|
|
{
|
|
subject_id: englishSubject.id,
|
|
chapter_number: 1,
|
|
chapter_title: 'Alphabet',
|
|
is_published: true,
|
|
display_order: 1
|
|
},
|
|
{
|
|
subject_id: englishSubject.id,
|
|
chapter_number: 2,
|
|
chapter_title: 'Greetings',
|
|
is_published: true,
|
|
display_order: 2
|
|
}
|
|
]);
|
|
|
|
// 3. Tạo Lesson cho mỗi Chapter
|
|
for (const chapter of chapters) {
|
|
await Lesson.create({
|
|
chapter_id: chapter.id,
|
|
lesson_number: 1,
|
|
lesson_title: `${chapter.chapter_title} - Lesson 1`,
|
|
lesson_type: 'json_content',
|
|
content_json: { /* ... */ },
|
|
is_published: true,
|
|
display_order: 1
|
|
});
|
|
}
|
|
```
|
|
|
|
### Scenario 2: Sao chép cấu trúc từ môn khác
|
|
|
|
```javascript
|
|
// Sao chép cấu trúc từ Toán lớp 1 sang Toán lớp 2
|
|
const mathG1 = await Subject.findOne({
|
|
where: { subject_code: 'MATH_G1' },
|
|
include: [{
|
|
model: Chapter,
|
|
include: [Lesson]
|
|
}]
|
|
});
|
|
|
|
const mathG2 = await Subject.create({
|
|
subject_code: 'MATH_G2',
|
|
subject_name: 'Toán lớp 2',
|
|
// ... các trường khác
|
|
});
|
|
|
|
// Copy chapters và lessons
|
|
for (const chapter of mathG1.Chapters) {
|
|
const newChapter = await Chapter.create({
|
|
subject_id: mathG2.id,
|
|
chapter_number: chapter.chapter_number,
|
|
chapter_title: chapter.chapter_title,
|
|
// ... các trường khác
|
|
});
|
|
|
|
for (const lesson of chapter.Lessons) {
|
|
await Lesson.create({
|
|
chapter_id: newChapter.id,
|
|
lesson_number: lesson.lesson_number,
|
|
lesson_title: lesson.lesson_title,
|
|
// ... các trường khác
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## ✅ Checklist Tạo Nội Dung
|
|
|
|
### Khi tạo Subject:
|
|
- [ ] `subject_code` là duy nhất (unique)
|
|
- [ ] `subject_name` rõ ràng, dễ hiểu
|
|
- [ ] Chọn đúng `is_premium`, `is_public`, `is_training`
|
|
- [ ] Thiết lập `required_role` và `min_subscription_tier` phù hợp
|
|
|
|
### Khi tạo Chapter:
|
|
- [ ] Liên kết đúng `subject_id`
|
|
- [ ] `chapter_number` theo thứ tự 1, 2, 3...
|
|
- [ ] `display_order` phản ánh thứ tự hiển thị
|
|
- [ ] `duration_minutes` hợp lý với số lượng lesson
|
|
- [ ] Đặt `is_published = true` khi sẵn sàng
|
|
|
|
### Khi tạo Lesson:
|
|
- [ ] Liên kết đúng `chapter_id`
|
|
- [ ] Chọn đúng `lesson_type`: `json_content` hoặc `url_content`
|
|
- [ ] Nếu `json_content`: `content_json.type` phải khớp với `Game.type`
|
|
- [ ] Nếu `url_content`: Kiểm tra URL hợp lệ
|
|
- [ ] `lesson_number` và `display_order` đúng thứ tự
|
|
- [ ] Thiết lập `is_free` cho bài học thử miễn phí
|
|
- [ ] Thêm `thumbnail_url` để hiển thị đẹp
|
|
|
|
---
|
|
|
|
## 🚨 Lỗi Thường Gặp
|
|
|
|
### 1. Lỗi Foreign Key
|
|
```
|
|
Error: Cannot create chapter: subject_id does not exist
|
|
```
|
|
**Giải pháp**: Kiểm tra `subject_id` có tồn tại trong bảng `subjects`
|
|
|
|
### 2. Lỗi Unique Constraint
|
|
```
|
|
Error: subject_code must be unique
|
|
```
|
|
**Giải pháp**: Chọn `subject_code` khác hoặc kiểm tra Subject đã tồn tại
|
|
|
|
### 3. Lỗi JSON Content Type
|
|
```
|
|
Warning: No game found for type: xxx
|
|
```
|
|
**Giải pháp**: Tạo Game với `type` khớp với `content_json.type`
|
|
|
|
### 4. Lỗi Validation
|
|
```
|
|
Error: lesson_type must be either 'json_content' or 'url_content'
|
|
```
|
|
**Giải pháp**: Kiểm tra giá trị `lesson_type` hợp lệ
|
|
|
|
---
|
|
|
|
## 📚 Tài Liệu Liên Quan
|
|
|
|
- [CONTENT_MANAGEMENT_GUIDE.md](./CONTENT_MANAGEMENT_GUIDE.md) - Hướng dẫn tổng thể chi tiết
|
|
- [API Documentation](http://localhost:3000/api-docs) - Swagger UI
|
|
- `models/Subject.js` - Model Subject
|
|
- `models/Chapter.js` - Model Chapter
|
|
- `models/Lesson.js` - Model Lesson
|
|
- `controllers/lessonController.js` - Lesson Controller
|
|
- `scripts/seed-sample-content.js` - Script tạo dữ liệu mẫu
|
|
|
|
---
|
|
|
|
## 💡 Tips & Best Practices
|
|
|
|
1. **Luôn tạo theo thứ tự**: Subject → Chapter → Lesson
|
|
2. **Sử dụng `display_order`**: Kiểm soát thứ tự hiển thị độc lập với số thứ tự
|
|
3. **Draft trước, publish sau**: Tạo với `is_published: false`, kiểm tra kỹ rồi mới publish
|
|
4. **Consistent naming**: Đặt tên theo quy ước rõ ràng (`MATH_G1`, `VIET_G2`)
|
|
5. **Free content**: Luôn có một số bài học miễn phí để học viên trải nghiệm
|
|
6. **Thumbnail**: Thêm thumbnail cho lesson để tăng trải nghiệm UI
|
|
7. **Metadata**: Sử dụng `description` để mô tả đầy đủ nội dung
|
|
8. **Testing**: Test kỹ từng lesson trước khi publish
|
|
|
|
---
|
|
|
|
**Chúc bạn tạo nội dung thành công! 🎉**
|