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

479 lines
13 KiB
Go

package education
import (
"context"
"fmt"
"sync"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
)
// Tutorial représente un tutoriel vidéo
type Tutorial struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Author string `json:"author"`
Category string `json:"category"`
Tags []string `json:"tags"`
VideoURL string `json:"video_url"`
Thumbnail string `json:"thumbnail"`
Duration time.Duration `json:"duration"`
Quality VideoQuality `json:"quality"`
Language string `json:"language"`
IsFree bool `json:"is_free"`
IsPublished bool `json:"is_published"`
Views int64 `json:"views"`
Likes int64 `json:"likes"`
Dislikes int64 `json:"dislikes"`
Rating float64 `json:"rating"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
mu sync.RWMutex
}
// VideoQuality définit la qualité de la vidéo
type VideoQuality string
const (
VideoQualityHD VideoQuality = "hd"
VideoQuality4K VideoQuality = "4k"
VideoQuality8K VideoQuality = "8k"
)
// TutorialStep représente une étape dans un tutoriel
type TutorialStep struct {
ID string `json:"id"`
TutorialID string `json:"tutorial_id"`
Title string `json:"title"`
Description string `json:"description"`
Content string `json:"content"`
Order int `json:"order"`
Timestamp time.Duration `json:"timestamp"`
IsFree bool `json:"is_free"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TutorialComment représente un commentaire sur un tutoriel
type TutorialComment struct {
ID string `json:"id"`
TutorialID string `json:"tutorial_id"`
UserID string `json:"user_id"`
Username string `json:"username"`
Content string `json:"content"`
Rating int `json:"rating"` // 1-5 étoiles
IsHelpful bool `json:"is_helpful"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TutorialManager gère les tutoriels vidéo
type TutorialManager struct {
tutorials map[string]*Tutorial
steps map[string][]*TutorialStep
comments map[string][]*TutorialComment
logger *zap.Logger
mu sync.RWMutex
}
// NewTutorialManager crée un nouveau gestionnaire de tutoriels
func NewTutorialManager(logger *zap.Logger) *TutorialManager {
return &TutorialManager{
tutorials: make(map[string]*Tutorial),
steps: make(map[string][]*TutorialStep),
comments: make(map[string][]*TutorialComment),
logger: logger,
}
}
// CreateTutorial crée un nouveau tutoriel
func (tm *TutorialManager) CreateTutorial(ctx context.Context, title, description, author, category, videoURL, thumbnail, language string, duration time.Duration, quality VideoQuality, isFree bool, tags []string) (*Tutorial, error) {
tm.mu.Lock()
defer tm.mu.Unlock()
tutorialID := uuid.New().String()
tutorial := &Tutorial{
ID: tutorialID,
Title: title,
Description: description,
Author: author,
Category: category,
Tags: tags,
VideoURL: videoURL,
Thumbnail: thumbnail,
Duration: duration,
Quality: quality,
Language: language,
IsFree: isFree,
IsPublished: false,
Views: 0,
Likes: 0,
Dislikes: 0,
Rating: 0.0,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
tm.tutorials[tutorialID] = tutorial
tm.logger.Info("Tutoriel créé",
zap.String("tutorial_id", tutorialID),
zap.String("title", title),
zap.String("author", author))
return tutorial, nil
}
// GetTutorial récupère un tutoriel par son ID
func (tm *TutorialManager) GetTutorial(ctx context.Context, tutorialID string) (*Tutorial, error) {
tm.mu.RLock()
defer tm.mu.RUnlock()
tutorial, exists := tm.tutorials[tutorialID]
if !exists {
return nil, fmt.Errorf("tutoriel non trouvé: %s", tutorialID)
}
return tutorial, nil
}
// ListTutorials liste tous les tutoriels disponibles
func (tm *TutorialManager) ListTutorials(ctx context.Context, filters map[string]interface{}) ([]*Tutorial, error) {
tm.mu.RLock()
defer tm.mu.RUnlock()
var tutorials []*Tutorial
for _, tutorial := range tm.tutorials {
// Appliquer les filtres si fournis
if filters != nil {
if category, ok := filters["category"].(string); ok && tutorial.Category != category {
continue
}
if isPublished, ok := filters["is_published"].(bool); ok && tutorial.IsPublished != isPublished {
continue
}
if isFree, ok := filters["is_free"].(bool); ok && tutorial.IsFree != isFree {
continue
}
if language, ok := filters["language"].(string); ok && tutorial.Language != language {
continue
}
if author, ok := filters["author"].(string); ok && tutorial.Author != author {
continue
}
}
tutorials = append(tutorials, tutorial)
}
return tutorials, nil
}
// UpdateTutorial met à jour un tutoriel
func (tm *TutorialManager) UpdateTutorial(ctx context.Context, tutorialID string, updates map[string]interface{}) (*Tutorial, error) {
tm.mu.Lock()
defer tm.mu.Unlock()
tutorial, exists := tm.tutorials[tutorialID]
if !exists {
return nil, fmt.Errorf("tutoriel non trouvé: %s", tutorialID)
}
// Appliquer les mises à jour
if title, ok := updates["title"].(string); ok {
tutorial.Title = title
}
if description, ok := updates["description"].(string); ok {
tutorial.Description = description
}
if author, ok := updates["author"].(string); ok {
tutorial.Author = author
}
if category, ok := updates["category"].(string); ok {
tutorial.Category = category
}
if videoURL, ok := updates["video_url"].(string); ok {
tutorial.VideoURL = videoURL
}
if thumbnail, ok := updates["thumbnail"].(string); ok {
tutorial.Thumbnail = thumbnail
}
if duration, ok := updates["duration"].(time.Duration); ok {
tutorial.Duration = duration
}
if quality, ok := updates["quality"].(VideoQuality); ok {
tutorial.Quality = quality
}
if isPublished, ok := updates["is_published"].(bool); ok {
tutorial.IsPublished = isPublished
}
if tags, ok := updates["tags"].([]string); ok {
tutorial.Tags = tags
}
tutorial.UpdatedAt = time.Now()
tm.logger.Info("Tutoriel mis à jour",
zap.String("tutorial_id", tutorialID),
zap.String("title", tutorial.Title))
return tutorial, nil
}
// DeleteTutorial supprime un tutoriel
func (tm *TutorialManager) DeleteTutorial(ctx context.Context, tutorialID string) error {
tm.mu.Lock()
defer tm.mu.Unlock()
if _, exists := tm.tutorials[tutorialID]; !exists {
return fmt.Errorf("tutoriel non trouvé: %s", tutorialID)
}
delete(tm.tutorials, tutorialID)
delete(tm.steps, tutorialID)
delete(tm.comments, tutorialID)
tm.logger.Info("Tutoriel supprimé",
zap.String("tutorial_id", tutorialID))
return nil
}
// AddTutorialStep ajoute une étape à un tutoriel
func (tm *TutorialManager) AddTutorialStep(ctx context.Context, tutorialID, title, description, content string, order int, timestamp time.Duration, isFree bool) (*TutorialStep, error) {
tm.mu.Lock()
defer tm.mu.Unlock()
stepID := uuid.New().String()
step := &TutorialStep{
ID: stepID,
TutorialID: tutorialID,
Title: title,
Description: description,
Content: content,
Order: order,
Timestamp: timestamp,
IsFree: isFree,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
tm.steps[tutorialID] = append(tm.steps[tutorialID], step)
tm.logger.Info("Étape de tutoriel ajoutée",
zap.String("tutorial_id", tutorialID),
zap.String("step_id", stepID),
zap.String("title", title))
return step, nil
}
// GetTutorialSteps récupère toutes les étapes d'un tutoriel
func (tm *TutorialManager) GetTutorialSteps(ctx context.Context, tutorialID string) ([]*TutorialStep, error) {
tm.mu.RLock()
defer tm.mu.RUnlock()
steps, exists := tm.steps[tutorialID]
if !exists {
return []*TutorialStep{}, nil
}
return steps, nil
}
// AddTutorialComment ajoute un commentaire à un tutoriel
func (tm *TutorialManager) AddTutorialComment(ctx context.Context, tutorialID, userID, username, content string, rating int) (*TutorialComment, error) {
tm.mu.Lock()
defer tm.mu.Unlock()
commentID := uuid.New().String()
comment := &TutorialComment{
ID: commentID,
TutorialID: tutorialID,
UserID: userID,
Username: username,
Content: content,
Rating: rating,
IsHelpful: false,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
tm.comments[tutorialID] = append(tm.comments[tutorialID], comment)
// Mettre à jour la note moyenne du tutoriel
tm.updateTutorialRating(tutorialID)
tm.logger.Info("Commentaire ajouté",
zap.String("tutorial_id", tutorialID),
zap.String("comment_id", commentID),
zap.String("username", username))
return comment, nil
}
// GetTutorialComments récupère tous les commentaires d'un tutoriel
func (tm *TutorialManager) GetTutorialComments(ctx context.Context, tutorialID string) ([]*TutorialComment, error) {
tm.mu.RLock()
defer tm.mu.RUnlock()
comments, exists := tm.comments[tutorialID]
if !exists {
return []*TutorialComment{}, nil
}
return comments, nil
}
// IncrementViews incrémente le nombre de vues d'un tutoriel
func (tm *TutorialManager) IncrementViews(ctx context.Context, tutorialID string) error {
tm.mu.Lock()
defer tm.mu.Unlock()
tutorial, exists := tm.tutorials[tutorialID]
if !exists {
return fmt.Errorf("tutoriel non trouvé: %s", tutorialID)
}
tutorial.Views++
tutorial.UpdatedAt = time.Now()
tm.logger.Debug("Vues incrémentées",
zap.String("tutorial_id", tutorialID),
zap.Int64("views", tutorial.Views))
return nil
}
// LikeTutorial ajoute un like à un tutoriel
func (tm *TutorialManager) LikeTutorial(ctx context.Context, tutorialID string) error {
tm.mu.Lock()
defer tm.mu.Unlock()
tutorial, exists := tm.tutorials[tutorialID]
if !exists {
return fmt.Errorf("tutoriel non trouvé: %s", tutorialID)
}
tutorial.Likes++
tutorial.UpdatedAt = time.Now()
tm.logger.Debug("Like ajouté",
zap.String("tutorial_id", tutorialID),
zap.Int64("likes", tutorial.Likes))
return nil
}
// DislikeTutorial ajoute un dislike à un tutoriel
func (tm *TutorialManager) DislikeTutorial(ctx context.Context, tutorialID string) error {
tm.mu.Lock()
defer tm.mu.Unlock()
tutorial, exists := tm.tutorials[tutorialID]
if !exists {
return fmt.Errorf("tutoriel non trouvé: %s", tutorialID)
}
tutorial.Dislikes++
tutorial.UpdatedAt = time.Now()
tm.logger.Debug("Dislike ajouté",
zap.String("tutorial_id", tutorialID),
zap.Int64("dislikes", tutorial.Dislikes))
return nil
}
// updateTutorialRating met à jour la note moyenne d'un tutoriel
func (tm *TutorialManager) updateTutorialRating(tutorialID string) {
comments, exists := tm.comments[tutorialID]
if !exists || len(comments) == 0 {
return
}
var totalRating int
var ratedComments int
for _, comment := range comments {
if comment.Rating > 0 {
totalRating += comment.Rating
ratedComments++
}
}
if ratedComments > 0 {
tutorial, exists := tm.tutorials[tutorialID]
if exists {
tutorial.Rating = float64(totalRating) / float64(ratedComments)
tutorial.UpdatedAt = time.Now()
}
}
}
// SearchTutorials recherche des tutoriels par mots-clés
func (tm *TutorialManager) SearchTutorials(ctx context.Context, query string, filters map[string]interface{}) ([]*Tutorial, error) {
tm.mu.RLock()
defer tm.mu.RUnlock()
var results []*Tutorial
query = fmt.Sprintf("%%%s%%", query) // Recherche LIKE
for _, tutorial := range tm.tutorials {
// Vérifier si le tutoriel correspond à la recherche
matches := false
if contains(tutorial.Title, query) || contains(tutorial.Description, query) || contains(tutorial.Author, query) {
matches = true
}
// Vérifier les tags
for _, tag := range tutorial.Tags {
if contains(tag, query) {
matches = true
break
}
}
if !matches {
continue
}
// Appliquer les filtres si fournis
if filters != nil {
if category, ok := filters["category"].(string); ok && tutorial.Category != category {
continue
}
if isPublished, ok := filters["is_published"].(bool); ok && tutorial.IsPublished != isPublished {
continue
}
if isFree, ok := filters["is_free"].(bool); ok && tutorial.IsFree != isFree {
continue
}
}
results = append(results, tutorial)
}
return results, nil
}
// contains vérifie si une chaîne contient une sous-chaîne (insensible à la casse)
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr ||
(len(s) > len(substr) && (s[:len(substr)] == substr ||
s[len(s)-len(substr):] == substr ||
containsSubstring(s, substr))))
}
// containsSubstring vérifie si une chaîne contient une sous-chaîne
func containsSubstring(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}