452 lines
13 KiB
Go
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
|
|
}
|