feat: Document video progress tracking APIs and update the main API endpoint list.
This commit is contained in:
parent
4b8f524eac
commit
abac1c2d8c
5 changed files with 230 additions and 3 deletions
204
docs/api-docs/ERD v.1.txt
Normal file
204
docs/api-docs/ERD v.1.txt
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
Table users {
|
||||
id int [pk
|
||||
]
|
||||
name varchar
|
||||
email varchar [unique
|
||||
]
|
||||
password varchar
|
||||
role varchar // admin | instructor | learner
|
||||
created_at datetime
|
||||
updated_at datetime
|
||||
}
|
||||
|
||||
Table courses {
|
||||
id int [pk
|
||||
]
|
||||
categories_id int
|
||||
title jsonb // { "th": "...", "en": "..." }
|
||||
description jsonb // { "th": "...", "en": "..." }
|
||||
price decimal
|
||||
is_free boolean
|
||||
status varchar // draft | pending | approved | rejected
|
||||
instructor_id int
|
||||
approved_by int // admin user id
|
||||
created_at datetime
|
||||
updated_at datetime
|
||||
}
|
||||
|
||||
Table categories {
|
||||
id int [pk
|
||||
]
|
||||
name jsonb // multi-language category name
|
||||
description jsonb // optional
|
||||
created_at datetime
|
||||
updated_at datetime
|
||||
}
|
||||
|
||||
Table chapters {
|
||||
id int [pk
|
||||
]
|
||||
course_id int
|
||||
title jsonb // multi-language
|
||||
sort_order int
|
||||
created_at datetime
|
||||
updated_at datetime
|
||||
}
|
||||
|
||||
Table lessons {
|
||||
id int [pk
|
||||
]
|
||||
chapter_id int
|
||||
title jsonb // multi-language
|
||||
content jsonb // multi-language lesson content
|
||||
type varchar // video | pdf | text | quiz
|
||||
sort_order int
|
||||
created_at datetime
|
||||
updated_at datetime
|
||||
}
|
||||
|
||||
Table quizzes {
|
||||
id int [pk
|
||||
]
|
||||
lesson_id int
|
||||
title jsonb // multi-language
|
||||
passing_score int
|
||||
time_limit int
|
||||
created_at datetime
|
||||
updated_at datetime
|
||||
}
|
||||
|
||||
Table questions {
|
||||
id int [pk
|
||||
]
|
||||
quiz_id int
|
||||
question jsonb // multi-language
|
||||
score int
|
||||
created_at datetime
|
||||
updated_at datetime
|
||||
}
|
||||
|
||||
Table choices {
|
||||
id int [pk
|
||||
]
|
||||
question_id int
|
||||
text jsonb // multi-language
|
||||
is_correct boolean
|
||||
created_at datetime
|
||||
updated_at datetime
|
||||
}
|
||||
|
||||
Table enrollments {
|
||||
id int [pk
|
||||
]
|
||||
user_id int
|
||||
course_id int
|
||||
status varchar // enrolled | completed
|
||||
enrolled_at datetime
|
||||
created_at datetime
|
||||
updated_at datetime
|
||||
}
|
||||
|
||||
Table announcements {
|
||||
id int [pk
|
||||
]
|
||||
course_id int
|
||||
instructor_id int
|
||||
title jsonb // multi-language
|
||||
content jsonb // multi-language
|
||||
is_pinned boolean // pin important announcements to top
|
||||
published_at datetime // scheduled publish date
|
||||
created_at datetime
|
||||
updated_at datetime
|
||||
}
|
||||
|
||||
Table announcement_attachments {
|
||||
id int [pk
|
||||
]
|
||||
announcement_id int
|
||||
file_name varchar
|
||||
file_path varchar
|
||||
file_size int
|
||||
mime_type varchar
|
||||
created_at datetime
|
||||
}
|
||||
|
||||
Table orders {
|
||||
id int [pk
|
||||
]
|
||||
user_id int
|
||||
total_amount decimal
|
||||
status varchar // pending | paid | cancelled
|
||||
created_at datetime
|
||||
updated_at datetime
|
||||
}
|
||||
|
||||
Table order_items {
|
||||
id int [pk
|
||||
]
|
||||
order_id int
|
||||
course_id int
|
||||
price decimal
|
||||
created_at datetime
|
||||
updated_at datetime
|
||||
}
|
||||
|
||||
Table payments {
|
||||
id int [pk
|
||||
]
|
||||
order_id int
|
||||
provider varchar
|
||||
transaction_id varchar
|
||||
amount decimal
|
||||
status varchar // success | failed
|
||||
paid_at datetime
|
||||
created_at datetime
|
||||
updated_at datetime
|
||||
}
|
||||
|
||||
Table instructor_balances {
|
||||
id int [pk
|
||||
]
|
||||
instructor_id int
|
||||
available_amount decimal
|
||||
withdrawn_amount decimal
|
||||
created_at datetime
|
||||
updated_at datetime
|
||||
}
|
||||
|
||||
Table withdrawal_requests {
|
||||
id int [pk
|
||||
]
|
||||
instructor_id int
|
||||
amount decimal
|
||||
status varchar // pending | approved | rejected | paid
|
||||
approved_by int
|
||||
created_at datetime
|
||||
updated_at datetime
|
||||
}
|
||||
|
||||
Ref: courses.instructor_id > users.id
|
||||
Ref: courses.approved_by > users.id
|
||||
|
||||
Ref: courses.categories_id > categories.id
|
||||
|
||||
Ref: chapters.course_id > courses.id
|
||||
Ref: lessons.chapter_id > chapters.id
|
||||
Ref: quizzes.lesson_id > lessons.id
|
||||
Ref: questions.quiz_id > quizzes.id
|
||||
Ref: choices.question_id > questions.id
|
||||
|
||||
Ref: enrollments.user_id > users.id
|
||||
Ref: enrollments.course_id > courses.id
|
||||
|
||||
Ref: announcements.course_id > courses.id
|
||||
Ref: announcements.instructor_id > users.id
|
||||
Ref: announcement_attachments.announcement_id > announcements.id
|
||||
|
||||
Ref: orders.user_id > users.id
|
||||
Ref: order_items.order_id > orders.id
|
||||
Ref: order_items.course_id > courses.id
|
||||
Ref: payments.order_id > orders.id
|
||||
|
||||
Ref: instructor_balances.instructor_id > users.id
|
||||
Ref: withdrawal_requests.instructor_id > users.id
|
||||
Ref: withdrawal_requests.approved_by > users.id
|
||||
328
docs/api-docs/ERD_v2_improved.txt
Normal file
328
docs/api-docs/ERD_v2_improved.txt
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
// E-Learning Platform - Enhanced ERD with Constraints
|
||||
// Version 2.0 - Improved with NOT NULL, CHECK, DEFAULT, and UNIQUE constraints
|
||||
|
||||
Table users {
|
||||
id int [pk, increment]
|
||||
name varchar [not null]
|
||||
email varchar [unique, not null]
|
||||
password varchar [not null]
|
||||
role varchar [not null, default: 'student', note: 'admin | instructor | student']
|
||||
created_at datetime [not null, default: `now()`]
|
||||
updated_at datetime [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
email [unique]
|
||||
role
|
||||
}
|
||||
}
|
||||
|
||||
Table courses {
|
||||
id int [pk, increment]
|
||||
categories_id int [ref: > categories.id]
|
||||
title jsonb [not null, note: '{ "th": "...", "en": "..." }']
|
||||
description jsonb [note: '{ "th": "...", "en": "..." }']
|
||||
price decimal [not null, default: 0, note: 'must be >= 0']
|
||||
is_free boolean [not null, default: false]
|
||||
status varchar [not null, default: 'draft', note: 'draft | pending | approved | rejected']
|
||||
instructor_id int [not null, ref: > users.id]
|
||||
approved_by int [ref: > users.id, note: 'admin user id']
|
||||
created_at datetime [not null, default: `now()`]
|
||||
updated_at datetime [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
instructor_id
|
||||
categories_id
|
||||
status
|
||||
(instructor_id, status)
|
||||
}
|
||||
}
|
||||
|
||||
Table categories {
|
||||
id int [pk, increment]
|
||||
name jsonb [not null, note: 'multi-language category name']
|
||||
description jsonb [note: 'optional']
|
||||
created_at datetime [not null, default: `now()`]
|
||||
updated_at datetime [not null, default: `now()`]
|
||||
}
|
||||
|
||||
Table chapters {
|
||||
id int [pk, increment]
|
||||
course_id int [not null, ref: > courses.id]
|
||||
title jsonb [not null, note: 'multi-language']
|
||||
sort_order int [not null, default: 0, note: 'must be >= 0']
|
||||
created_at datetime [not null, default: `now()`]
|
||||
updated_at datetime [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
course_id
|
||||
(course_id, sort_order)
|
||||
}
|
||||
}
|
||||
|
||||
Table lessons {
|
||||
id int [pk, increment]
|
||||
chapter_id int [not null, ref: > chapters.id]
|
||||
title jsonb [not null, note: 'multi-language']
|
||||
content jsonb [note: 'multi-language lesson content']
|
||||
type varchar [not null, note: 'video | pdf | text | quiz']
|
||||
sort_order int [not null, default: 0, note: 'must be >= 0']
|
||||
is_sequential boolean [not null, default: true, note: 'require previous lessons to be completed']
|
||||
prerequisite_lesson_ids jsonb [note: 'array of lesson IDs that must be completed first, e.g. [1, 2, 3]']
|
||||
require_pass_quiz boolean [default: false, note: 'require passing quiz to unlock next lessons']
|
||||
created_at datetime [not null, default: `now()`]
|
||||
updated_at datetime [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
chapter_id
|
||||
(chapter_id, sort_order)
|
||||
type
|
||||
}
|
||||
}
|
||||
|
||||
Table quizzes {
|
||||
id int [pk, increment]
|
||||
lesson_id int [not null, ref: > lessons.id]
|
||||
title jsonb [not null, note: 'multi-language']
|
||||
passing_score int [not null, default: 60, note: 'must be 0-100']
|
||||
time_limit int [note: 'in minutes, must be > 0 if set']
|
||||
max_attempts int [note: 'null = unlimited']
|
||||
cooldown_minutes int [note: 'waiting time between attempts']
|
||||
score_policy varchar [default: 'highest', note: 'highest | latest | first | average']
|
||||
created_at datetime [not null, default: `now()`]
|
||||
updated_at datetime [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
lesson_id
|
||||
}
|
||||
}
|
||||
|
||||
Table questions {
|
||||
id int [pk, increment]
|
||||
quiz_id int [not null, ref: > quizzes.id]
|
||||
question jsonb [not null, note: 'multi-language']
|
||||
question_type varchar [not null, default: 'multiple_choice', note: 'multiple_choice | true_false']
|
||||
score int [not null, default: 1, note: 'must be > 0']
|
||||
sort_order int [not null, default: 0]
|
||||
created_at datetime [not null, default: `now()`]
|
||||
updated_at datetime [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
quiz_id
|
||||
(quiz_id, sort_order)
|
||||
}
|
||||
}
|
||||
|
||||
Table choices {
|
||||
id int [pk, increment]
|
||||
question_id int [not null, ref: > questions.id]
|
||||
text jsonb [not null, note: 'multi-language']
|
||||
is_correct boolean [not null, default: false]
|
||||
sort_order int [not null, default: 0]
|
||||
created_at datetime [not null, default: `now()`]
|
||||
updated_at datetime [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
question_id
|
||||
}
|
||||
}
|
||||
|
||||
Table enrollments {
|
||||
id int [pk, increment]
|
||||
user_id int [not null, ref: > users.id]
|
||||
course_id int [not null, ref: > courses.id]
|
||||
status varchar [not null, default: 'enrolled', note: 'enrolled | completed']
|
||||
progress_percentage int [not null, default: 0, note: '0-100']
|
||||
certificate_issued boolean [not null, default: false]
|
||||
certificate_id varchar [unique, note: 'unique certificate identifier']
|
||||
enrolled_at datetime [not null, default: `now()`]
|
||||
completed_at datetime [note: 'when status changed to completed']
|
||||
created_at datetime [not null, default: `now()`]
|
||||
updated_at datetime [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
(user_id, course_id) [unique, name: 'unique_enrollment']
|
||||
user_id
|
||||
course_id
|
||||
status
|
||||
}
|
||||
}
|
||||
|
||||
Table lesson_progress {
|
||||
id int [pk, increment]
|
||||
user_id int [not null, ref: > users.id]
|
||||
lesson_id int [not null, ref: > lessons.id]
|
||||
is_completed boolean [not null, default: false]
|
||||
completed_at datetime
|
||||
video_progress_seconds int [default: 0, note: 'current playback position in seconds']
|
||||
video_duration_seconds int [note: 'total video duration in seconds']
|
||||
video_progress_percentage decimal(5,2) [note: 'calculated: (progress/duration)*100']
|
||||
last_watched_at datetime [note: 'last time user watched this video']
|
||||
created_at datetime [not null, default: `now()`]
|
||||
updated_at datetime [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
(user_id, lesson_id) [unique]
|
||||
user_id
|
||||
lesson_id
|
||||
last_watched_at
|
||||
}
|
||||
}
|
||||
|
||||
Table quiz_attempts {
|
||||
id int [pk, increment]
|
||||
user_id int [not null, ref: > users.id]
|
||||
quiz_id int [not null, ref: > quizzes.id]
|
||||
score int [not null, default: 0, note: '0-100']
|
||||
total_questions int [not null]
|
||||
correct_answers int [not null, default: 0]
|
||||
is_passed boolean [not null, default: false]
|
||||
attempt_number int [not null, default: 1]
|
||||
started_at datetime [not null, default: `now()`]
|
||||
completed_at datetime
|
||||
created_at datetime [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
user_id
|
||||
quiz_id
|
||||
(user_id, quiz_id)
|
||||
(user_id, quiz_id, attempt_number)
|
||||
}
|
||||
}
|
||||
|
||||
Table announcements {
|
||||
id int [pk, increment]
|
||||
course_id int [not null, ref: > courses.id]
|
||||
instructor_id int [not null, ref: > users.id]
|
||||
title jsonb [not null, note: 'multi-language']
|
||||
content jsonb [not null, note: 'multi-language']
|
||||
is_pinned boolean [not null, default: false, note: 'pin important announcements to top']
|
||||
published_at datetime [note: 'scheduled publish date, null = publish immediately']
|
||||
created_at datetime [not null, default: `now()`]
|
||||
updated_at datetime [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
course_id
|
||||
instructor_id
|
||||
(course_id, is_pinned, published_at)
|
||||
(course_id, published_at)
|
||||
}
|
||||
}
|
||||
|
||||
Table announcement_attachments {
|
||||
id int [pk, increment]
|
||||
announcement_id int [not null, ref: > announcements.id]
|
||||
file_name varchar [not null]
|
||||
file_path varchar [not null, note: 'S3 key or file path']
|
||||
file_size int [not null, note: 'in bytes']
|
||||
mime_type varchar [not null]
|
||||
created_at datetime [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
announcement_id
|
||||
}
|
||||
}
|
||||
|
||||
Table lesson_attachments {
|
||||
id int [pk, increment]
|
||||
lesson_id int [not null, ref: > lessons.id]
|
||||
file_name varchar [not null]
|
||||
file_path varchar [not null, note: 'S3 key or file path']
|
||||
file_size int [not null, note: 'in bytes']
|
||||
mime_type varchar [not null]
|
||||
description jsonb [note: 'multi-language description']
|
||||
sort_order int [not null, default: 0]
|
||||
created_at datetime [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
lesson_id
|
||||
(lesson_id, sort_order)
|
||||
}
|
||||
}
|
||||
|
||||
Table orders {
|
||||
id int [pk, increment]
|
||||
user_id int [not null, ref: > users.id]
|
||||
total_amount decimal [not null, default: 0, note: 'must be >= 0']
|
||||
status varchar [not null, default: 'pending', note: 'pending | paid | cancelled']
|
||||
created_at datetime [not null, default: `now()`]
|
||||
updated_at datetime [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
user_id
|
||||
status
|
||||
(user_id, status)
|
||||
}
|
||||
}
|
||||
|
||||
Table order_items {
|
||||
id int [pk, increment]
|
||||
order_id int [not null, ref: > orders.id]
|
||||
course_id int [not null, ref: > courses.id]
|
||||
price decimal [not null, note: 'must be >= 0']
|
||||
created_at datetime [not null, default: `now()`]
|
||||
updated_at datetime [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
order_id
|
||||
course_id
|
||||
}
|
||||
}
|
||||
|
||||
Table payments {
|
||||
id int [pk, increment]
|
||||
order_id int [not null, ref: > orders.id]
|
||||
provider varchar [not null, note: 'stripe | paypal | promptpay etc.']
|
||||
transaction_id varchar [unique, note: 'unique transaction ID from payment provider']
|
||||
amount decimal [not null, note: 'must be > 0']
|
||||
status varchar [not null, default: 'pending', note: 'pending | success | failed']
|
||||
paid_at datetime
|
||||
created_at datetime [not null, default: `now()`]
|
||||
updated_at datetime [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
order_id
|
||||
transaction_id [unique]
|
||||
status
|
||||
}
|
||||
}
|
||||
|
||||
Table instructor_balances {
|
||||
id int [pk, increment]
|
||||
instructor_id int [not null, unique, ref: > users.id, note: 'one balance record per instructor']
|
||||
available_amount decimal [not null, default: 0, note: 'must be >= 0']
|
||||
withdrawn_amount decimal [not null, default: 0, note: 'must be >= 0']
|
||||
created_at datetime [not null, default: `now()`]
|
||||
updated_at datetime [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
instructor_id [unique]
|
||||
}
|
||||
}
|
||||
|
||||
Table withdrawal_requests {
|
||||
id int [pk, increment]
|
||||
instructor_id int [not null, ref: > users.id]
|
||||
amount decimal [not null, note: 'must be > 0']
|
||||
status varchar [not null, default: 'pending', note: 'pending | approved | rejected | paid']
|
||||
approved_by int [ref: > users.id, note: 'admin user id']
|
||||
rejected_reason varchar [note: 'reason if rejected']
|
||||
created_at datetime [not null, default: `now()`]
|
||||
updated_at datetime [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
instructor_id
|
||||
status
|
||||
(instructor_id, status)
|
||||
}
|
||||
}
|
||||
|
||||
// Additional helpful notes:
|
||||
// 1. All foreign keys should have ON DELETE actions defined during implementation
|
||||
// 2. Suggested ON DELETE actions:
|
||||
// - courses.instructor_id: RESTRICT (prevent deleting instructor with courses)
|
||||
// - enrollments: CASCADE (delete enrollments when user/course deleted)
|
||||
// - announcements: CASCADE (delete announcements when course deleted)
|
||||
// - quiz_attempts: CASCADE (delete attempts when user deleted)
|
||||
// 3. All jsonb fields support multi-language: { "th": "...", "en": "..." }
|
||||
// 4. Indexes are suggested for common query patterns
|
||||
// 5. Consider adding soft delete (deleted_at) for important tables
|
||||
|
|
@ -70,6 +70,8 @@ Authorization: Bearer <token>
|
|||
| GET | `/students/courses/:courseId/learn` | 👨🎓 Student | Get course learning page (with lock status) |
|
||||
| GET | `/students/courses/:courseId/lessons/:lessonId` | 👨🎓 Student | Get lesson content (checks prerequisites) |
|
||||
| GET | `/students/courses/:courseId/lessons/:lessonId/access-check` | 👨🎓 Student | Check lesson access without loading content |
|
||||
| POST | `/students/lessons/:lessonId/progress` | 👨🎓 Student | Save video progress |
|
||||
| GET | `/students/lessons/:lessonId/progress` | 👨🎓 Student | Get video progress |
|
||||
| POST | `/students/courses/:courseId/lessons/:lessonId/complete` | 👨🎓 Student | Mark lesson as complete |
|
||||
|
||||
### Instructor Endpoints
|
||||
|
|
@ -303,13 +305,13 @@ Authorization: Bearer <token>
|
|||
|
||||
---
|
||||
|
||||
## Total Endpoints: **93+**
|
||||
## Total Endpoints: **95+**
|
||||
|
||||
- Authentication: 6
|
||||
- User Management: 4
|
||||
- Categories: 5
|
||||
- Courses: 12
|
||||
- Chapters & Lessons: 16 (-1)
|
||||
- Courses: 14 (+2)
|
||||
- Chapters & Lessons: 16
|
||||
- Quizzes: 10
|
||||
- Announcements: 6
|
||||
- Reports: 11
|
||||
|
|
|
|||
191
docs/api-docs/api_video_progress.md
Normal file
191
docs/api-docs/api_video_progress.md
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
# Video Progress Tracking - API Usage Examples
|
||||
|
||||
## Save Video Progress
|
||||
|
||||
### POST `/students/lessons/:lessonId/progress`
|
||||
|
||||
**Purpose**: บันทึกตำแหน่งการดูวีดีโอ (เรียกทุก 5 วินาที)
|
||||
|
||||
#### Request:
|
||||
```http
|
||||
POST /api/students/lessons/5/progress
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"progress_seconds": 450,
|
||||
"duration_seconds": 900
|
||||
}
|
||||
```
|
||||
|
||||
#### Response 200:
|
||||
```json
|
||||
{
|
||||
"lesson_id": 5,
|
||||
"video_progress_seconds": 450,
|
||||
"video_duration_seconds": 900,
|
||||
"video_progress_percentage": 50.00,
|
||||
"is_completed": false,
|
||||
"last_watched_at": "2024-12-24T14:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Auto-Complete (90%+):
|
||||
```json
|
||||
{
|
||||
"progress_seconds": 810,
|
||||
"duration_seconds": 900
|
||||
}
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"lesson_id": 5,
|
||||
"video_progress_seconds": 810,
|
||||
"video_duration_seconds": 900,
|
||||
"video_progress_percentage": 90.00,
|
||||
"is_completed": true,
|
||||
"completed_at": "2024-12-24T14:05:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Get Video Progress
|
||||
|
||||
### GET `/students/lessons/:lessonId/progress`
|
||||
|
||||
**Purpose**: ดึงตำแหน่งการดูวีดีโอเพื่อเล่นต่อ
|
||||
|
||||
#### Request:
|
||||
```http
|
||||
GET /api/students/lessons/5/progress
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
#### Response 200 (Has Progress):
|
||||
```json
|
||||
{
|
||||
"lesson_id": 5,
|
||||
"video_progress_seconds": 450,
|
||||
"video_duration_seconds": 900,
|
||||
"video_progress_percentage": 50.00,
|
||||
"is_completed": false,
|
||||
"last_watched_at": "2024-12-24T13:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Response 200 (No Progress):
|
||||
```json
|
||||
{
|
||||
"lesson_id": 5,
|
||||
"video_progress_seconds": 0,
|
||||
"video_duration_seconds": null,
|
||||
"video_progress_percentage": 0,
|
||||
"is_completed": false,
|
||||
"last_watched_at": null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Frontend Integration
|
||||
|
||||
```javascript
|
||||
const VideoPlayer = ({ lessonId, videoUrl }) => {
|
||||
const videoRef = useRef(null);
|
||||
|
||||
// Load saved progress
|
||||
useEffect(() => {
|
||||
async function loadProgress() {
|
||||
const res = await fetch(`/api/students/lessons/${lessonId}/progress`);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.video_progress_seconds > 0) {
|
||||
videoRef.current.currentTime = data.video_progress_seconds;
|
||||
}
|
||||
}
|
||||
loadProgress();
|
||||
}, [lessonId]);
|
||||
|
||||
// Save progress every 5 seconds
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (videoRef.current && !videoRef.current.paused) {
|
||||
saveProgress(
|
||||
Math.floor(videoRef.current.currentTime),
|
||||
Math.floor(videoRef.current.duration)
|
||||
);
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
async function saveProgress(currentTime, duration) {
|
||||
await fetch(`/api/students/lessons/${lessonId}/progress`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
progress_seconds: currentTime,
|
||||
duration_seconds: duration
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return <video ref={videoRef} src={videoUrl} controls />;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Modified Endpoints
|
||||
|
||||
### GET `/students/courses/:courseId/learn`
|
||||
|
||||
**Added**: `video_progress_percentage` และ `resume_from` ในแต่ละ lesson
|
||||
|
||||
```json
|
||||
{
|
||||
"lessons": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Lesson 1",
|
||||
"type": "video",
|
||||
"is_completed": true,
|
||||
"video_progress_percentage": 100
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Lesson 2",
|
||||
"type": "video",
|
||||
"is_completed": false,
|
||||
"video_progress_percentage": 50,
|
||||
"resume_from": 450
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### GET `/students/courses/:courseId/progress`
|
||||
|
||||
**Added**: `recently_watched` section
|
||||
|
||||
```json
|
||||
{
|
||||
"course_id": 1,
|
||||
"progress_percentage": 30,
|
||||
"recently_watched": [
|
||||
{
|
||||
"lesson_id": 2,
|
||||
"title": "Lesson 2",
|
||||
"video_progress_percentage": 50,
|
||||
"last_watched_at": "2024-12-24T14:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
296
docs/api-docs/edge_cases_quick_reference.md
Normal file
296
docs/api-docs/edge_cases_quick_reference.md
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
# Edge Cases Quick Reference
|
||||
|
||||
> สรุปกรณีพิเศษที่ต้องระวังในระบบ E-Learning - สำหรับอ้างอิงด่วน
|
||||
|
||||
## 📋 สารบัญ Edge Cases
|
||||
|
||||
| # | Edge Case | ความเสี่ยง | แนวทางแก้ไข |
|
||||
|---|-----------|-----------|-------------|
|
||||
| 1 | [การเปลี่ยนบทบาทโดย Admin](#1-การเปลี่ยนบทบาทผู้ใช้) | 🟡 ปานกลาง | Invalidate sessions + audit log |
|
||||
| 2 | [หลักสูตรถูกซ่อน/ลบ](#2-หลักสูตรถูกซ่อนลบ) | 🟡 ปานกลาง | Read-only mode สำหรับผู้ลงทะเบียนแล้ว |
|
||||
| 3 | [ลงทะเบียนซ้ำ](#3-ลงทะเบียนซ้ำ) | 🟢 ต่ำ | ตรวจสอบและ redirect |
|
||||
| 4 | [ข้ามลำดับบทเรียน](#4-ข้ามลำดับบทเรียน) | 🟡 ปานกลาง | ตรวจสอบ prerequisite |
|
||||
| 5 | [ทำแบบทดสอบหลายครั้ง](#5-ทำแบบทดสอบหลายครั้ง) | 🟡 ปานกลาง | จำกัดครั้ง + cooldown |
|
||||
| 6 | [แก้ไขขณะมีคนใช้งาน](#6-แก้ไขขณะมีคนใช้งาน) | 🔴 สูง | Soft delete + แจ้งเตือน |
|
||||
| 7 | [ลบ Instructor ที่มีหลักสูตร](#7-ลบ-instructor-ที่มีหลักสูตร) | 🔴 สูง | บังคับโอนหลักสูตรก่อน |
|
||||
| 8 | [เข้าถึงข้อมูลข้ามหลักสูตร](#8-เข้าถึงข้อมูลข้ามหลักสูตร) | 🔴 สูง | Middleware ตรวจสอบ ownership |
|
||||
| 9 | [เพิ่มเนื้อหาหลังจบหลักสูตร](#9-เพิ่มเนื้อหาหลังจบหลักสูตร) | 🟡 ปานกลาง | เนื้อหาใหม่ = เสริม (ไม่บังคับ) |
|
||||
| 10 | [Maintenance Mode](#10-maintenance-mode) | 🟡 ปานกลาง | Auto-save + แจ้งเตือนล่วงหน้า |
|
||||
| 11 | [Video Progress Tracking](#11-video-progress-tracking) | 🟡 ปานกลาง | Save ทุก 5 วินาที + last_watched_at |
|
||||
|
||||
---
|
||||
|
||||
## 1. การเปลี่ยนบทบาทผู้ใช้
|
||||
|
||||
> **หมายเหตุ**: ไม่มีการเปลี่ยนบทบาทระหว่าง Student-Instructor ในการใช้งานปกติ
|
||||
> เกิดขึ้นเฉพาะกรณีพิเศษที่ Admin จัดการ (เช่น แก้ไขข้อมูลผิดพลาด)
|
||||
|
||||
**ปัญหา**: Token/Session เก่ายังมีสิทธิ์เดิม
|
||||
|
||||
**วิธีแก้**:
|
||||
```javascript
|
||||
// Admin เปลี่ยนบทบาท (กรณีพิเศษ)
|
||||
// 1. บันทึก audit log (ระบุเหตุผล)
|
||||
// 2. อัปเดตบทบาท
|
||||
// 3. Invalidate sessions ทั้งหมด
|
||||
// 4. แจ้งเตือนผู้ใช้ทางอีเมล
|
||||
// 5. บังคับ login ใหม่
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- ✅ เฉพาะ Admin เท่านั้น
|
||||
- ✅ ต้องระบุเหตุผลและบันทึก audit log
|
||||
- ✅ Invalidate sessions ทันที
|
||||
- ✅ แจ้งเตือนผู้ใช้ทางอีเมล
|
||||
|
||||
---
|
||||
|
||||
## 2. หลักสูตรถูกซ่อน/ลบ
|
||||
|
||||
**ปัญหา**: Student ที่ลงทะเบียนแล้วควรเข้าถึงได้หรือไม่?
|
||||
|
||||
**วิธีแก้**:
|
||||
- **ซ่อน**: Student ที่ลงทะเบียนแล้ว → เข้าถึงได้ปกติ
|
||||
- **ลบ**: Student ที่ลงทะเบียนแล้ว → Read-only mode
|
||||
|
||||
**Key Points**:
|
||||
- ✅ ใช้ Soft Delete
|
||||
- ✅ ใบประกาศนียบัตรที่ออกแล้วยังใช้ได้
|
||||
- ❌ ห้ามทำแบบทดสอบใหม่ในหลักสูตรที่ถูกลบ
|
||||
|
||||
---
|
||||
|
||||
## 3. ลงทะเบียนซ้ำ
|
||||
|
||||
**ปัญหา**: ข้อมูลความคืบหน้าจะเป็นอย่างไร?
|
||||
|
||||
**วิธีแก้**:
|
||||
```javascript
|
||||
if (existingEnrollment) {
|
||||
return redirect(`/courses/${courseId}`);
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- ✅ ตรวจสอบก่อนสร้าง enrollment
|
||||
- ✅ Redirect ไปที่หลักสูตร
|
||||
- ❌ ห้ามรีเซ็ตความคืบหน้า
|
||||
|
||||
---
|
||||
|
||||
## 4. ข้ามลำดับบทเรียน
|
||||
|
||||
**ปัญหา**: Student ใช้ Direct URL ข้ามลำดับ
|
||||
|
||||
**วิธีแก้**:
|
||||
```javascript
|
||||
// ตรวจสอบว่าเรียน Lesson ก่อนหน้าครบหรือไม่
|
||||
if (!allPreviousCompleted) {
|
||||
return { access: false, nextAvailableLesson };
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- ✅ ตรวจสอบที่ Backend (ห้ามพึ่ง Frontend)
|
||||
- ✅ แสดง Lesson ถัดไปที่เข้าถึงได้
|
||||
- ✅ รองรับหลักสูตรที่ไม่บังคับลำดับ
|
||||
|
||||
---
|
||||
|
||||
## 5. ทำแบบทดสอบหลายครั้ง
|
||||
|
||||
**ปัญหา**: จำกัดครั้ง? คะแนนไหนที่นับ? รอกี่นาที?
|
||||
|
||||
**วิธีแก้**:
|
||||
```javascript
|
||||
// ตรวจสอบ:
|
||||
// 1. จำนวนครั้งที่ทำได้ (maxAttempts)
|
||||
// 2. ระยะเวลารอ (cooldownMinutes)
|
||||
// 3. นโยบายคะแนน (HIGHEST/LATEST/FIRST/AVERAGE)
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- ✅ ใช้คะแนนสูงสุด (HIGHEST) เป็นค่าเริ่มต้น
|
||||
- ✅ กำหนด cooldown ได้ต่อแบบทดสอบ
|
||||
- ✅ เก็บประวัติการทำทุกครั้ง
|
||||
|
||||
---
|
||||
|
||||
## 6. แก้ไขขณะมีคนใช้งาน
|
||||
|
||||
**ปัญหา**: Student กำลังดู Lesson อยู่ แต่ Instructor ลบ
|
||||
|
||||
**วิธีแก้**:
|
||||
```javascript
|
||||
// ใช้ Soft Delete
|
||||
if (activeViewers.length > 0) {
|
||||
markAsDeleted(); // ลบถาวรใน 24 ชม.
|
||||
notifyInstructor();
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- ✅ ใช้ Soft Delete แทน Hard Delete
|
||||
- ✅ แจ้งเตือน Instructor ว่ามีคนดูอยู่
|
||||
- ✅ Student ที่เข้าถึงก่อนหน้า → ดูต่อได้
|
||||
|
||||
---
|
||||
|
||||
## 7. ลบ Instructor ที่มีหลักสูตร
|
||||
|
||||
**ปัญหา**: หลักสูตรจะกลายเป็น "ไม่มีเจ้าของ"
|
||||
|
||||
**วิธีแก้**:
|
||||
```javascript
|
||||
if (courses.length > 0) {
|
||||
return error('กรุณาโอนหลักสูตรก่อน');
|
||||
}
|
||||
// ใช้ Soft Delete (deactivate)
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- ✅ บังคับโอนหลักสูตรก่อนลบ
|
||||
- ✅ ใช้ Soft Delete (deactivate)
|
||||
- ✅ เก็บ Audit Log
|
||||
|
||||
---
|
||||
|
||||
## 8. เข้าถึงข้อมูลข้ามหลักสูตร
|
||||
|
||||
**ปัญหา**: Instructor A ดูข้อมูลหลักสูตรของ Instructor B
|
||||
|
||||
**วิธีแก้**:
|
||||
```javascript
|
||||
// Middleware ตรวจสอบ ownership
|
||||
if (role === 'INSTRUCTOR' && course.instructorId !== userId) {
|
||||
return 403;
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- ✅ ตรวจสอบ ownership ที่ Backend
|
||||
- ✅ Admin → read-only access
|
||||
- ✅ Instructor → เฉพาะหลักสูตรของตนเอง
|
||||
|
||||
---
|
||||
|
||||
## 9. เพิ่มเนื้อหาหลังจบหลักสูตร
|
||||
|
||||
**ปัญหา**: ความคืบหน้ากลับไม่ครบ 100%
|
||||
|
||||
**วิธีแก้**:
|
||||
```javascript
|
||||
// คำนวณจากเนื้อหาตอนลงทะเบียน
|
||||
if (originalProgress === 100 && certificateIssued) {
|
||||
return { progress: 100, newContentAvailable: true };
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- ✅ ใบประกาศนียบัตรยังใช้ได้
|
||||
- ✅ เนื้อหาใหม่ = เสริม (ไม่บังคับ)
|
||||
- ✅ แสดงป้าย "มีเนื้อหาใหม่"
|
||||
|
||||
---
|
||||
|
||||
## 10. Maintenance Mode
|
||||
|
||||
**ปัญหา**: Student กำลังทำแบบทดสอบ
|
||||
|
||||
**วิธีแก้**:
|
||||
```javascript
|
||||
// 1. แจ้งเตือนล่วงหน้า 24 ชม.
|
||||
// 2. Auto-save ทุก 30 วินาที
|
||||
// 3. เก็บ draft 24 ชม.
|
||||
// 4. Admin เข้าถึงได้
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- ✅ Auto-save แบบทดสอบทุก 30 วินาที
|
||||
- ✅ แจ้งเตือนล่วงหน้า
|
||||
- ✅ กู้คืนข้อมูลได้ภายใน 24 ชม.
|
||||
|
||||
---
|
||||
|
||||
## 11. Video Progress Tracking
|
||||
|
||||
**ปัญหา**: Student ดูวีดีโอค้างไว้หลายแท็บ หรือปิดเบราว์เซอร์กลางคัน
|
||||
|
||||
**วิธีแก้**:
|
||||
```javascript
|
||||
// 1. Save progress ทุก 5 วินาที
|
||||
// 2. ใช้ last_watched_at เป็นตัวตัดสิน
|
||||
// 3. Auto-complete เมื่อดู 90%+
|
||||
// 4. Resume จากตำแหน่งล่าสุด
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- ✅ Save progress ทุก 5 วินาที (ไม่บ่อยเกินไป)
|
||||
- ✅ ใช้ `last_watched_at` ตัดสินว่า session ไหนล่าสุด
|
||||
- ✅ Auto-complete เมื่อดู ≥ 90%
|
||||
- ✅ Resume จากตำแหน่งที่บันทึกล่าสุด
|
||||
- ❌ ไม่ save เมื่อ video pause หรือ buffering
|
||||
|
||||
**Concurrent Updates**:
|
||||
```javascript
|
||||
// แท็บ A: บันทึกที่ 5:00 (14:00:00)
|
||||
// แท็บ B: บันทึกที่ 3:00 (14:00:05)
|
||||
// → ใช้แท็บ B (last_watched_at ใหม่กว่า)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔒 หลักการสำคัญ (Best Practices)
|
||||
|
||||
### 1. Soft Delete > Hard Delete
|
||||
- ใช้ `isDeleted` flag แทนการลบจริง
|
||||
- เก็บข้อมูลไว้สำหรับ audit trail
|
||||
- ป้องกันการสูญหายของข้อมูล
|
||||
|
||||
### 2. Invalidate Sessions เมื่อเปลี่ยนสิทธิ์
|
||||
- เปลี่ยนบทบาท → logout ทันที
|
||||
- ลบบัญชี → invalidate sessions
|
||||
- เปลี่ยนรหัสผ่าน → logout devices อื่น
|
||||
|
||||
### 3. ตรวจสอบสิทธิ์ที่ Backend เสมอ
|
||||
- Frontend = UX (ซ่อน/แสดง UI)
|
||||
- Backend = Security (ป้องกันการเข้าถึง)
|
||||
- ห้ามพึ่งพา Frontend เพียงอย่างเดียว
|
||||
|
||||
### 4. Auto-save สำหรับข้อมูลสำคัญ
|
||||
- แบบทดสอบ → auto-save ทุก 30 วินาที
|
||||
- เนื้อหาที่กำลังเขียน → auto-save
|
||||
- เก็บ draft ไว้อย่างน้อย 24 ชม.
|
||||
|
||||
### 5. แจ้งเตือนผู้ใช้
|
||||
- Maintenance → แจ้งล่วงหน้า 24 ชม.
|
||||
- ลบเนื้อหา → แจ้งว่ามีคนดูอยู่
|
||||
- เปลี่ยนแปลงสำคัญ → ส่งอีเมลแจ้ง
|
||||
|
||||
---
|
||||
|
||||
## 📊 สรุปความเสี่ยงและลำดับความสำคัญ
|
||||
|
||||
### 🔴 ความเสี่ยงสูง (ต้องจัดการก่อน)
|
||||
1. แก้ไขขณะมีคนใช้งาน
|
||||
2. ลบ Instructor ที่มีหลักสูตร
|
||||
3. เข้าถึงข้อมูลข้ามหลักสูตร
|
||||
|
||||
### 🟡 ความเสี่ยงปานกลาง
|
||||
4. การเปลี่ยนบทบาทโดย Admin (กรณีพิเศษ)
|
||||
5. หลักสูตรถูกซ่อน/ลบ
|
||||
6. ข้ามลำดับบทเรียน
|
||||
7. ทำแบบทดสอบหลายครั้ง
|
||||
8. เพิ่มเนื้อหาหลังจบหลักสูตร
|
||||
9. Maintenance Mode
|
||||
|
||||
### 🟢 ความเสี่ยงต่ำ
|
||||
10. ลงทะเบียนซ้ำ
|
||||
|
||||
---
|
||||
|
||||
## 🔗 เอกสารที่เกี่ยวข้อง
|
||||
|
||||
- [ตารางสิทธิ์แบบเต็ม](./permissions_matrix.md)
|
||||
- [User Roles and Capabilities](./user_roles_and_capabilities.md)
|
||||
- [System Architecture](./elearning_architecture.md)
|
||||
Loading…
Add table
Add a link
Reference in a new issue