feat: add certificates, groups, support system, and moderation

Backend changes:
- Add Certificate generation service with beautiful HTML templates
- Add CourseGroup, GroupMember, GroupMessage models for group collaboration
- Add Homework and HomeworkSubmission models with AI + teacher grading
- Add SupportTicket and TicketMessage models for help desk
- Add Moderation API for admin/moderator course approval workflow
- All new modules: CertificatesModule, GroupsModule, SupportModule, ModerationModule

Frontend changes:
- Add certificate download button when course completed
- Update course page to load enrollment progress from backend
- Integrate lesson completion with backend API

Database schema now supports:
- Course groups with chat functionality
- Homework assignments with dual AI/human grading
- Support ticket system with admin responses
- Full moderation workflow (PENDING_REVIEW -> PUBLISHED/REJECTED)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
root
2026-02-06 10:50:04 +00:00
parent 2ed65f5678
commit 5ddb3db1ac
16 changed files with 605 additions and 1 deletions

View File

@ -42,6 +42,11 @@ model User {
purchases Purchase[]
reviews Review[]
generations CourseGeneration[]
groupMembers GroupMember[]
groupMessages GroupMessage[]
homeworkSubmissions HomeworkSubmission[]
supportTickets SupportTicket[]
ticketMessages TicketMessage[]
@@map("users")
}
@ -183,6 +188,7 @@ model Course {
purchases Purchase[]
reviews Review[]
generation CourseGeneration?
groups CourseGroup[]
// Vector embedding for semantic search
embedding Unsupported("vector(1536)")?
@ -234,6 +240,7 @@ model Lesson {
// Relations
chapter Chapter @relation(fields: [chapterId], references: [id], onDelete: Cascade)
homework Homework[]
// Vector embedding for semantic search
embedding Unsupported("vector(1536)")?
@ -431,3 +438,137 @@ model LessonProgress {
@@index([enrollmentId])
@@map("lesson_progress")
}
// ============================================
// Course Groups & Collaboration
// ============================================
model CourseGroup {
id String @id @default(uuid())
courseId String @map("course_id")
name String
description String? @db.Text
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
members GroupMember[]
messages GroupMessage[]
@@index([courseId])
@@map("course_groups")
}
model GroupMember {
id String @id @default(uuid())
groupId String @map("group_id")
userId String @map("user_id")
role String @default("student") // "teacher", "student"
joinedAt DateTime @default(now()) @map("joined_at")
group CourseGroup @relation(fields: [groupId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([groupId, userId])
@@map("group_members")
}
model GroupMessage {
id String @id @default(uuid())
groupId String @map("group_id")
userId String @map("user_id")
content String @db.Text
createdAt DateTime @default(now()) @map("created_at")
group CourseGroup @relation(fields: [groupId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([groupId])
@@map("group_messages")
}
// ============================================
// Homework & Assignments
// ============================================
model Homework {
id String @id @default(uuid())
lessonId String @map("lesson_id")
title String
description String @db.Text
dueDate DateTime? @map("due_date")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
submissions HomeworkSubmission[]
@@index([lessonId])
@@map("homework")
}
model HomeworkSubmission {
id String @id @default(uuid())
homeworkId String @map("homework_id")
userId String @map("user_id")
content String @db.Text
// AI grading
aiScore Int? @map("ai_score") // 0-100
aiFeedback String? @db.Text @map("ai_feedback")
// Teacher grading
teacherScore Int? @map("teacher_score") // 0-100
teacherFeedback String? @db.Text @map("teacher_feedback")
submittedAt DateTime @default(now()) @map("submitted_at")
gradedAt DateTime? @map("graded_at")
homework Homework @relation(fields: [homeworkId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([homeworkId, userId])
@@map("homework_submissions")
}
// ============================================
// Support Tickets
// ============================================
model SupportTicket {
id String @id @default(uuid())
userId String @map("user_id")
title String
status String @default("open") // "open", "in_progress", "resolved", "closed"
priority String @default("normal") // "low", "normal", "high"
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
messages TicketMessage[]
@@index([userId])
@@index([status])
@@map("support_tickets")
}
model TicketMessage {
id String @id @default(uuid())
ticketId String @map("ticket_id")
userId String @map("user_id")
content String @db.Text
isStaff Boolean @default(false) @map("is_staff")
createdAt DateTime @default(now()) @map("created_at")
ticket SupportTicket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([ticketId])
@@map("ticket_messages")
}