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 }