docs: Relocate ERD documentation to a new ER/ subdirectory and update its README.

This commit is contained in:
JakkrapartXD 2025-12-24 17:32:11 +07:00
parent 7ebe33d920
commit 9f385ed997
3 changed files with 0 additions and 0 deletions

View 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

View 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

312
docs/api-docs/ER/README.md Normal file
View file

@ -0,0 +1,312 @@
# Database Schema (ERD) Documentation
## 📊 Entity-Relationship Diagram
### Online Visualization
View the interactive ERD diagram:
- **dbdiagram.io**: [dbdiagram.io](https://dbdiagram.io/d/e-lerning-6943c64be4bb1dd3a9896479)
## 📋 Database Overview
### Total Tables: **17**
| Category | Tables | Count |
|----------|--------|-------|
| **Core** | users, categories, courses, chapters, lessons | 5 |
| **Content** | quizzes, questions, choices, lesson_attachments | 4 |
| **Progress** | enrollments, lesson_progress, quiz_attempts | 3 |
| **Communication** | announcements, announcement_attachments | 2 |
| **Commerce** (อนาคต) | orders, payments, withdrawals | 3 |
---
## 🎯 Key Features
### 1. **Multi-language Support**
- Uses `jsonb` fields for Thai/English content
- Fields: `title`, `content`, `description`
### 2. **Video Progress Tracking**
- Track playback position per student
- Auto-complete at 90% watched
- Resume from last position
### 3. **Lesson Prerequisites**
- Sequential learning control
- Quiz pass requirements
- Flexible ordering
### 4. **File Attachments**
- Multiple files per lesson
- Support for PDF, DOCX, ZIP, images
- Sortable with descriptions
### 5. **Quiz System**
- Multiple attempts with cooldown
- Score policies (HIGHEST/LATEST/FIRST/AVERAGE)
- Detailed attempt history
---
## 📊 Main Entities
### Users
- Roles: STUDENT, INSTRUCTOR, ADMIN
- Profile information
- Authentication data
### Courses
- Multi-language titles and descriptions
- Categories and pricing
- Instructor ownership
- Approval workflow
### Chapters & Lessons
- Hierarchical structure
- Sort ordering
- Multiple lesson types (video, pdf, text, quiz)
- Prerequisites support
### Progress Tracking
- Enrollment status
- Lesson completion
- Video playback position
- Quiz attempts and scores
---
## 🔗 Relationships
```
users (1) ──── (many) courses [instructor]
users (1) ──── (many) enrollments
users (1) ──── (many) lesson_progress
users (1) ──── (many) quiz_attempts
courses (1) ──── (many) chapters
courses (1) ──── (many) enrollments
courses (1) ──── (many) announcements
chapters (1) ──── (many) lessons
lessons (1) ──── (many) lesson_attachments
lessons (1) ──── (many) lesson_progress
lessons (1) ──── (many) quizzes
quizzes (1) ──── (many) questions
quizzes (1) ──── (many) quiz_attempts
questions (1) ──── (many) choices
```
---
## 🎨 How to Visualize
### Using dbdiagram.io
1. **Open dbdiagram.io**
- Go to [https://dbdiagram.io/d](https://dbdiagram.io/d)
2. **Import Schema**
- Click "Import" button (top right)
- Select "From DBML"
- Copy contents from [`ERD_v2_improved.txt`](./ERD_v2_improved.txt)
- Paste into the editor
- Click "Import"
3. **View Diagram**
- Zoom in/out with mouse wheel
- Drag to pan
- Click tables to highlight relationships
- Export as PNG/PDF/SQL
### Alternative Tools
- **DBeaver**: Import SQL and generate ER diagram
- **MySQL Workbench**: Reverse engineer from database
- **pgAdmin**: PostgreSQL schema visualization
- **draw.io**: Manual diagram creation
---
## 📝 Table Descriptions
### Core Tables
#### `users`
- User accounts and authentication
- Roles: STUDENT, INSTRUCTOR, ADMIN
- Profile information
#### `categories`
- Course categorization
- Multi-language names
#### `courses`
- Course information and metadata
- Pricing and approval status
- Instructor ownership
#### `chapters`
- Course structure organization
- Sortable within courses
#### `lessons`
- Learning content units
- Types: video, pdf, text, quiz
- Prerequisites and sequencing
### Content Tables
#### `quizzes`
- Assessment configuration
- Attempt limits and cooldown
- Passing score requirements
#### `questions`
- Quiz questions
- Types: multiple_choice, true_false
- Point values
#### `choices`
- Answer options for questions
- Correct answer marking
#### `lesson_attachments`
- Supplementary files
- PDFs, documents, code samples
- Multi-language descriptions
### Progress Tables
#### `enrollments`
- Student course registrations
- Progress tracking
- Certificate issuance
#### `lesson_progress`
- Lesson completion status
- Video playback position
- Watch history
#### `quiz_attempts`
- Quiz submission records
- Scores and answers
- Attempt history
### Communication Tables
#### `announcements`
- Course announcements
- Pinning and scheduling
- Multi-language content
#### `announcement_attachments`
- Announcement file attachments
---
## 🔧 Database Constraints
### Primary Keys
- All tables use auto-incrementing integer PKs
### Foreign Keys
- Maintain referential integrity
- Cascade deletes where appropriate
### Unique Constraints
- `(user_id, course_id)` in enrollments
- `(user_id, lesson_id)` in lesson_progress
- Email in users
### Check Constraints (via notes)
- Progress percentages: 0-100
- Scores: 0-100
- Sort orders: >= 0
- Prices: >= 0
### Indexes
- Single column indexes on FKs
- Composite indexes for common queries
- JSONB GIN indexes for multi-language fields
---
## 🚀 Migration Notes
### PostgreSQL Extensions Required
```sql
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- for text search
```
### JSONB Usage
```sql
-- Multi-language fields
title JSONB -- {"th": "...", "en": "..."}
-- Query examples
WHERE title->>'th' LIKE '%Python%'
WHERE title @> '{"en": "Introduction"}'
```
### Soft Delete Pattern
```sql
-- Recommended for important tables
ALTER TABLE courses ADD COLUMN deleted_at TIMESTAMP;
CREATE INDEX idx_courses_deleted ON courses(deleted_at) WHERE deleted_at IS NULL;
```
---
## 📈 Performance Considerations
### Indexes Created
- All foreign keys
- Composite indexes for (course_id, sort_order)
- User email for login
- Course status for filtering
- Last watched timestamp for recent videos
### Query Optimization
- Use `SELECT` specific columns
- Implement pagination
- Cache course lists in Redis
- Use database views for complex queries
---
## 🔗 Related Documentation
- [API Endpoints](./api_endpoints.md) - REST API reference
- [Edge Cases](../edge_cases_quick_reference.md) - Special scenarios
- [Development Setup](../development_setup.md) - Environment configuration
---
## 📊 Statistics
- **Total Tables**: 17
- **Total Relationships**: 20+
- **Multi-language Fields**: 15+
- **Indexed Columns**: 40+
- **JSONB Fields**: 15+
---
## 🎯 Next Steps
1. ✅ Review ERD schema
2. ⏳ Create PostgreSQL database
3. ⏳ Run migrations
4. ⏳ Seed initial data
5. ⏳ Implement API endpoints
6. ⏳ Test with sample data
---
**Last Updated**: 2024-12-24
**Version**: 2.0 (Improved)