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