259 lines
7.6 KiB
Go
259 lines
7.6 KiB
Go
package social
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// SocialService gère les interactions sociales
|
|
type SocialService interface {
|
|
CreatePost(ctx context.Context, userID uuid.UUID, content string, attachments map[string]uuid.UUID) (*Post, error)
|
|
GetGlobalFeed(ctx context.Context, limit, offset int) ([]FeedItem, error)
|
|
GetUserFeed(ctx context.Context, userID uuid.UUID, limit, offset int) ([]FeedItem, error)
|
|
|
|
// Interactions
|
|
ToggleLike(ctx context.Context, userID uuid.UUID, targetID uuid.UUID, targetType string) (bool, error)
|
|
AddComment(ctx context.Context, userID uuid.UUID, targetID uuid.UUID, targetType string, content string) (*Comment, error)
|
|
|
|
// Internal
|
|
CreateActivityPost(ctx context.Context, userID uuid.UUID, content string, meta map[string]interface{}) error
|
|
}
|
|
|
|
// Service implémente SocialService
|
|
type Service struct {
|
|
db *gorm.DB
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewService crée une nouvelle instance du service social
|
|
func NewService(db *gorm.DB, logger *zap.Logger) *Service {
|
|
return &Service{
|
|
db: db,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// CreatePost crée une nouvelle publication
|
|
func (s *Service) CreatePost(ctx context.Context, userID uuid.UUID, content string, attachments map[string]uuid.UUID) (*Post, error) {
|
|
post := &Post{
|
|
UserID: userID,
|
|
Content: content,
|
|
Type: PostTypeStatus,
|
|
}
|
|
|
|
// Handle attachments
|
|
if trackID, ok := attachments["track_id"]; ok {
|
|
post.TrackID = &trackID
|
|
post.Type = PostTypeShare
|
|
}
|
|
if playlistID, ok := attachments["playlist_id"]; ok {
|
|
post.PlaylistID = &playlistID
|
|
post.Type = PostTypeShare
|
|
}
|
|
|
|
if err := s.db.Create(post).Error; err != nil {
|
|
s.logger.Error("Failed to create post", zap.Error(err), zap.String("user_id", userID.String()))
|
|
return nil, err
|
|
}
|
|
|
|
return post, nil
|
|
}
|
|
|
|
// GetGlobalFeed récupère un flux d'activité global
|
|
func (s *Service) GetGlobalFeed(ctx context.Context, limit, offset int) ([]FeedItem, error) {
|
|
var posts []Post
|
|
if err := s.db.Order("created_at desc").Limit(limit).Offset(offset).Find(&posts).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var feed []FeedItem
|
|
for _, p := range posts {
|
|
targetType := "none"
|
|
targetID := uuid.Nil
|
|
|
|
if p.TrackID != nil {
|
|
targetType = "track"
|
|
targetID = *p.TrackID
|
|
} else if p.PlaylistID != nil {
|
|
targetType = "playlist"
|
|
targetID = *p.PlaylistID
|
|
}
|
|
|
|
item := FeedItem{
|
|
ID: fmt.Sprintf("post:%s", p.ID.String()),
|
|
Type: ActivityPost,
|
|
ActorID: p.UserID,
|
|
TargetID: targetID,
|
|
TargetType: targetType,
|
|
Content: p.Content,
|
|
CreatedAt: p.CreatedAt,
|
|
}
|
|
|
|
// Spécial pour les activités automatiques
|
|
if p.Type == PostTypeActivity {
|
|
item.Type = ActivityPurchase // Ou autre logique plus fine
|
|
}
|
|
|
|
feed = append(feed, item)
|
|
}
|
|
|
|
return feed, nil
|
|
}
|
|
|
|
// GetUserFeed récupère le flux d'un utilisateur
|
|
func (s *Service) GetUserFeed(ctx context.Context, userID uuid.UUID, limit, offset int) ([]FeedItem, error) {
|
|
var posts []Post
|
|
if err := s.db.Where("user_id = ?", userID).Order("created_at desc").Limit(limit).Offset(offset).Find(&posts).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var feed []FeedItem
|
|
for _, p := range posts {
|
|
item := FeedItem{
|
|
ID: fmt.Sprintf("post:%s", p.ID.String()),
|
|
Type: ActivityPost,
|
|
ActorID: p.UserID,
|
|
Content: p.Content,
|
|
CreatedAt: p.CreatedAt,
|
|
TargetType: "user_wall",
|
|
}
|
|
feed = append(feed, item)
|
|
}
|
|
|
|
return feed, nil
|
|
}
|
|
|
|
// ToggleLike ajoute ou supprime un like
|
|
// Transactionnelle : SELECT like + DELETE/CREATE + UPDATE compteur dans une seule transaction
|
|
func (s *Service) ToggleLike(ctx context.Context, userID uuid.UUID, targetID uuid.UUID, targetType string) (bool, error) {
|
|
var liked bool
|
|
|
|
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
// 1. VÉRIFICATION : Like existe déjà ? (SELECT dans la transaction)
|
|
var like Like
|
|
err := tx.Where("user_id = ? AND target_id = ? AND target_type = ?", userID, targetID, targetType).First(&like).Error
|
|
|
|
if err == nil {
|
|
// 2a. Mode UNLIKE : Like existe, on le supprime
|
|
if err := tx.Delete(&like).Error; err != nil {
|
|
return fmt.Errorf("ToggleLike: failed to delete like: %w", err)
|
|
}
|
|
|
|
// 3a. Décrémenter le compteur si c'est un post (dans la transaction)
|
|
if targetType == "post" {
|
|
if err := tx.Model(&Post{}).Where("id = ?", targetID).Update("like_count", gorm.Expr("like_count - 1")).Error; err != nil {
|
|
return fmt.Errorf("ToggleLike: failed to decrement like_count: %w", err)
|
|
}
|
|
}
|
|
|
|
liked = false
|
|
return nil
|
|
} else if err == gorm.ErrRecordNotFound {
|
|
// 2b. Mode LIKE : Like n'existe pas, on le crée
|
|
// Vérifier d'abord que la ressource existe (pour les posts)
|
|
if targetType == "post" {
|
|
var post Post
|
|
if err := tx.Where("id = ?", targetID).First(&post).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return fmt.Errorf("ToggleLike: post not found: %w", err)
|
|
}
|
|
return fmt.Errorf("ToggleLike: failed to check post existence: %w", err)
|
|
}
|
|
}
|
|
|
|
like = Like{
|
|
UserID: userID,
|
|
TargetID: targetID,
|
|
TargetType: targetType,
|
|
}
|
|
if err := tx.Create(&like).Error; err != nil {
|
|
return fmt.Errorf("ToggleLike: failed to create like: %w", err)
|
|
}
|
|
|
|
// 3b. Incrémenter le compteur si c'est un post (dans la transaction)
|
|
if targetType == "post" {
|
|
if err := tx.Model(&Post{}).Where("id = ?", targetID).Update("like_count", gorm.Expr("like_count + 1")).Error; err != nil {
|
|
return fmt.Errorf("ToggleLike: failed to increment like_count: %w", err)
|
|
}
|
|
}
|
|
|
|
liked = true
|
|
return nil
|
|
} else {
|
|
return fmt.Errorf("ToggleLike: failed to check like existence: %w", err)
|
|
}
|
|
})
|
|
|
|
if err != nil {
|
|
return false, err // Rollback automatique si erreur
|
|
}
|
|
|
|
return liked, nil
|
|
}
|
|
|
|
// AddComment ajoute un commentaire
|
|
// Transactionnelle : CREATE comment + UPDATE compteur dans une seule transaction
|
|
func (s *Service) AddComment(ctx context.Context, userID uuid.UUID, targetID uuid.UUID, targetType string, content string) (*Comment, error) {
|
|
var comment *Comment
|
|
|
|
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
// 1. VALIDATION : Post existe ? (SELECT dans la transaction si targetType == "post")
|
|
if targetType == "post" {
|
|
var post Post
|
|
if err := tx.First(&post, "id = ?", targetID).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return fmt.Errorf("post not found")
|
|
}
|
|
return fmt.Errorf("AddComment: failed to validate post: %w", err)
|
|
}
|
|
}
|
|
|
|
// 2. CRÉATION : Commentaire (INSERT dans la transaction)
|
|
comment = &Comment{
|
|
UserID: userID,
|
|
TargetID: targetID,
|
|
TargetType: targetType,
|
|
Content: content,
|
|
}
|
|
if err := tx.Create(comment).Error; err != nil {
|
|
return fmt.Errorf("AddComment: failed to create comment: %w", err)
|
|
}
|
|
|
|
// 3. MISE À JOUR : Compteur (UPDATE dans la transaction)
|
|
if targetType == "post" {
|
|
if err := tx.Model(&Post{}).Where("id = ?", targetID).Update("comment_count", gorm.Expr("comment_count + 1")).Error; err != nil {
|
|
return fmt.Errorf("AddComment: failed to increment comment_count: %w", err)
|
|
}
|
|
}
|
|
|
|
// 4. RETOUR nil = commit automatique
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err // Rollback automatique si erreur
|
|
}
|
|
|
|
return comment, nil
|
|
}
|
|
|
|
// CreateActivityPost crée un post automatique pour une activité (ex: Achat)
|
|
func (s *Service) CreateActivityPost(ctx context.Context, userID uuid.UUID, content string, meta map[string]interface{}) error {
|
|
post := &Post{
|
|
UserID: userID,
|
|
Content: content,
|
|
Type: PostTypeActivity,
|
|
}
|
|
|
|
if trackIDStr, ok := meta["track_id"].(string); ok {
|
|
if trackID, err := uuid.Parse(trackIDStr); err == nil {
|
|
post.TrackID = &trackID
|
|
}
|
|
}
|
|
|
|
return s.db.Create(post).Error
|
|
}
|