veza/veza-backend-api/internal/core/education/course.go
2025-12-03 20:29:37 +01:00

452 lines
13 KiB
Go

package education
import (
"context"
"fmt"
"sync"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
)
// Course représente un cours de formation
type Course struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Instructor string `json:"instructor"`
Category string `json:"category"`
Level CourseLevel `json:"level"`
Duration time.Duration `json:"duration"`
Price float64 `json:"price"`
Currency string `json:"currency"`
Language string `json:"language"`
Thumbnail string `json:"thumbnail"`
VideoURL string `json:"video_url"`
Lessons []*Lesson `json:"lessons"`
Exercises []*Exercise `json:"exercises"`
Certificates []*Certificate `json:"certificates"`
Tags []string `json:"tags"`
IsPublished bool `json:"is_published"`
IsFree bool `json:"is_free"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
mu sync.RWMutex
}
// CourseLevel définit le niveau de difficulté d'un cours
type CourseLevel string
const (
CourseLevelBeginner CourseLevel = "beginner"
CourseLevelIntermediate CourseLevel = "intermediate"
CourseLevelAdvanced CourseLevel = "advanced"
CourseLevelExpert CourseLevel = "expert"
)
// Lesson représente une leçon dans un cours
type Lesson struct {
ID string `json:"id"`
CourseID string `json:"course_id"`
Title string `json:"title"`
Description string `json:"description"`
Content string `json:"content"`
VideoURL string `json:"video_url"`
Duration time.Duration `json:"duration"`
Order int `json:"order"`
IsFree bool `json:"is_free"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Exercise représente un exercice pratique
type Exercise struct {
ID string `json:"id"`
CourseID string `json:"course_id"`
LessonID string `json:"lesson_id"`
Title string `json:"title"`
Description string `json:"description"`
Type ExerciseType `json:"type"`
Content string `json:"content"`
Solution string `json:"solution"`
Points int `json:"points"`
TimeLimit time.Duration `json:"time_limit"`
IsRequired bool `json:"is_required"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ExerciseType définit le type d'exercice
type ExerciseType string
const (
ExerciseTypeQuiz ExerciseType = "quiz"
ExerciseTypeProject ExerciseType = "project"
ExerciseTypeAudio ExerciseType = "audio"
ExerciseTypeCode ExerciseType = "code"
ExerciseTypeEssay ExerciseType = "essay"
)
// Certificate représente un certificat de formation
type Certificate struct {
ID string `json:"id"`
CourseID string `json:"course_id"`
UserID uuid.UUID `json:"user_id"`
Title string `json:"title"`
Description string `json:"description"`
Score float64 `json:"score"`
MaxScore float64 `json:"max_score"`
IsPassed bool `json:"is_passed"`
IssuedAt time.Time `json:"issued_at"`
ExpiresAt time.Time `json:"expires_at"`
CreatedAt time.Time `json:"created_at"`
}
// CourseProgress représente la progression d'un utilisateur dans un cours
type CourseProgress struct {
ID string `json:"id"`
UserID uuid.UUID `json:"user_id"`
CourseID string `json:"course_id"`
Progress float64 `json:"progress"` // 0.0 à 1.0
CompletedLessons []string `json:"completed_lessons"`
CurrentLesson string `json:"current_lesson"`
Score float64 `json:"score"`
TimeSpent time.Duration `json:"time_spent"`
LastAccessed time.Time `json:"last_accessed"`
IsCompleted bool `json:"is_completed"`
CompletedAt time.Time `json:"completed_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// CourseManager gère les cours et formations
type CourseManager struct {
courses map[string]*Course
progress map[string]*CourseProgress
logger *zap.Logger
mu sync.RWMutex
}
// NewCourseManager crée un nouveau gestionnaire de cours
func NewCourseManager(logger *zap.Logger) *CourseManager {
return &CourseManager{
courses: make(map[string]*Course),
progress: make(map[string]*CourseProgress),
logger: logger,
}
}
// CreateCourse crée un nouveau cours
func (cm *CourseManager) CreateCourse(ctx context.Context, title, description, instructor, category string, level CourseLevel, duration time.Duration, price float64, language string) (*Course, error) {
cm.mu.Lock()
defer cm.mu.Unlock()
courseID := uuid.New().String()
course := &Course{
ID: courseID,
Title: title,
Description: description,
Instructor: instructor,
Category: category,
Level: level,
Duration: duration,
Price: price,
Currency: "EUR",
Language: language,
Lessons: []*Lesson{},
Exercises: []*Exercise{},
Certificates: []*Certificate{},
Tags: []string{},
IsPublished: false,
IsFree: price == 0,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
cm.courses[courseID] = course
cm.logger.Info("Cours créé",
zap.String("course_id", courseID),
zap.String("title", title),
zap.String("instructor", instructor))
return course, nil
}
// GetCourse récupère un cours par son ID
func (cm *CourseManager) GetCourse(ctx context.Context, courseID string) (*Course, error) {
cm.mu.RLock()
defer cm.mu.RUnlock()
course, exists := cm.courses[courseID]
if !exists {
return nil, fmt.Errorf("cours non trouvé: %s", courseID)
}
return course, nil
}
// ListCourses liste tous les cours disponibles
func (cm *CourseManager) ListCourses(ctx context.Context, filters map[string]interface{}) ([]*Course, error) {
cm.mu.RLock()
defer cm.mu.RUnlock()
var courses []*Course
for _, course := range cm.courses {
// Appliquer les filtres si fournis
if filters != nil {
if category, ok := filters["category"].(string); ok && course.Category != category {
continue
}
if level, ok := filters["level"].(CourseLevel); ok && course.Level != level {
continue
}
if isPublished, ok := filters["is_published"].(bool); ok && course.IsPublished != isPublished {
continue
}
if isFree, ok := filters["is_free"].(bool); ok && course.IsFree != isFree {
continue
}
}
courses = append(courses, course)
}
return courses, nil
}
// UpdateCourse met à jour un cours
func (cm *CourseManager) UpdateCourse(ctx context.Context, courseID string, updates map[string]interface{}) (*Course, error) {
cm.mu.Lock()
defer cm.mu.Unlock()
course, exists := cm.courses[courseID]
if !exists {
return nil, fmt.Errorf("cours non trouvé: %s", courseID)
}
// Appliquer les mises à jour
if title, ok := updates["title"].(string); ok {
course.Title = title
}
if description, ok := updates["description"].(string); ok {
course.Description = description
}
if instructor, ok := updates["instructor"].(string); ok {
course.Instructor = instructor
}
if category, ok := updates["category"].(string); ok {
course.Category = category
}
if level, ok := updates["level"].(CourseLevel); ok {
course.Level = level
}
if duration, ok := updates["duration"].(time.Duration); ok {
course.Duration = duration
}
if price, ok := updates["price"].(float64); ok {
course.Price = price
course.IsFree = price == 0
}
if isPublished, ok := updates["is_published"].(bool); ok {
course.IsPublished = isPublished
}
course.UpdatedAt = time.Now()
cm.logger.Info("Cours mis à jour",
zap.String("course_id", courseID),
zap.String("title", course.Title))
return course, nil
}
// DeleteCourse supprime un cours
func (cm *CourseManager) DeleteCourse(ctx context.Context, courseID string) error {
cm.mu.Lock()
defer cm.mu.Unlock()
if _, exists := cm.courses[courseID]; !exists {
return fmt.Errorf("cours non trouvé: %s", courseID)
}
delete(cm.courses, courseID)
cm.logger.Info("Cours supprimé",
zap.String("course_id", courseID))
return nil
}
// AddLesson ajoute une leçon à un cours
func (cm *CourseManager) AddLesson(ctx context.Context, courseID, title, description, content, videoURL string, duration time.Duration, order int, isFree bool) (*Lesson, error) {
cm.mu.Lock()
defer cm.mu.Unlock()
course, exists := cm.courses[courseID]
if !exists {
return nil, fmt.Errorf("cours non trouvé: %s", courseID)
}
lessonID := uuid.New().String()
lesson := &Lesson{
ID: lessonID,
CourseID: courseID,
Title: title,
Description: description,
Content: content,
VideoURL: videoURL,
Duration: duration,
Order: order,
IsFree: isFree,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
course.Lessons = append(course.Lessons, lesson)
course.UpdatedAt = time.Now()
cm.logger.Info("Leçon ajoutée",
zap.String("course_id", courseID),
zap.String("lesson_id", lessonID),
zap.String("title", title))
return lesson, nil
}
// AddExercise ajoute un exercice à un cours
func (cm *CourseManager) AddExercise(ctx context.Context, courseID, lessonID, title, description, content, solution string, exerciseType ExerciseType, points int, timeLimit time.Duration, isRequired bool) (*Exercise, error) {
cm.mu.Lock()
defer cm.mu.Unlock()
course, exists := cm.courses[courseID]
if !exists {
return nil, fmt.Errorf("cours non trouvé: %s", courseID)
}
exerciseID := uuid.New().String()
exercise := &Exercise{
ID: exerciseID,
CourseID: courseID,
LessonID: lessonID,
Title: title,
Description: description,
Type: exerciseType,
Content: content,
Solution: solution,
Points: points,
TimeLimit: timeLimit,
IsRequired: isRequired,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
course.Exercises = append(course.Exercises, exercise)
course.UpdatedAt = time.Now()
cm.logger.Info("Exercice ajouté",
zap.String("course_id", courseID),
zap.String("exercise_id", exerciseID),
zap.String("title", title))
return exercise, nil
}
// GetUserProgress récupère la progression d'un utilisateur dans un cours
func (cm *CourseManager) GetUserProgress(ctx context.Context, userID uuid.UUID, courseID string) (*CourseProgress, error) {
cm.mu.RLock()
defer cm.mu.RUnlock()
progressKey := fmt.Sprintf("%s_%s", userID.String(), courseID)
progress, exists := cm.progress[progressKey]
if !exists {
return nil, fmt.Errorf("progression non trouvée pour l'utilisateur %s dans le cours %s", userID, courseID)
}
return progress, nil
}
// UpdateUserProgress met à jour la progression d'un utilisateur
func (cm *CourseManager) UpdateUserProgress(ctx context.Context, userID uuid.UUID, courseID string, progress float64, completedLessons []string, currentLesson string, score float64, timeSpent time.Duration) (*CourseProgress, error) {
cm.mu.Lock()
defer cm.mu.Unlock()
progressKey := fmt.Sprintf("%s_%s", userID.String(), courseID)
userProgress, exists := cm.progress[progressKey]
if !exists {
userProgress = &CourseProgress{
ID: uuid.New().String(),
UserID: userID,
CourseID: courseID,
Progress: progress,
CompletedLessons: completedLessons,
CurrentLesson: currentLesson,
Score: score,
TimeSpent: timeSpent,
LastAccessed: time.Now(),
IsCompleted: progress >= 1.0,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
cm.progress[progressKey] = userProgress
} else {
userProgress.Progress = progress
userProgress.CompletedLessons = completedLessons
userProgress.CurrentLesson = currentLesson
userProgress.Score = score
userProgress.TimeSpent = timeSpent
userProgress.LastAccessed = time.Now()
userProgress.IsCompleted = progress >= 1.0
userProgress.UpdatedAt = time.Now()
if userProgress.IsCompleted && userProgress.CompletedAt.IsZero() {
userProgress.CompletedAt = time.Now()
}
}
cm.logger.Info("Progression utilisateur mise à jour",
zap.String("user_id", userID.String()),
zap.String("course_id", courseID),
zap.Float64("progress", progress))
return userProgress, nil
}
// IssueCertificate émet un certificat pour un utilisateur
func (cm *CourseManager) IssueCertificate(ctx context.Context, courseID string, userID uuid.UUID, title, description string, score, maxScore float64) (*Certificate, error) {
cm.mu.Lock()
defer cm.mu.Unlock()
certificateID := uuid.New().String()
isPassed := score >= maxScore*0.7 // 70% pour réussir
certificate := &Certificate{
ID: certificateID,
CourseID: courseID,
UserID: userID,
Title: title,
Description: description,
Score: score,
MaxScore: maxScore,
IsPassed: isPassed,
IssuedAt: time.Now(),
ExpiresAt: time.Now().AddDate(2, 0, 0), // Valide 2 ans
CreatedAt: time.Now(),
}
// Ajouter le certificat au cours
if course, exists := cm.courses[courseID]; exists {
course.Certificates = append(course.Certificates, certificate)
course.UpdatedAt = time.Now()
}
cm.logger.Info("Certificat émis",
zap.String("certificate_id", certificateID),
zap.String("course_id", courseID),
zap.String("user_id", userID.String()),
zap.Bool("is_passed", isPassed))
return certificate, nil
}