perf(analytics): optimize GetTrackStats to single query

This commit is contained in:
senke 2026-02-14 18:31:29 +01:00
parent 759154e660
commit 7de106b2dc

View file

@ -91,57 +91,45 @@ func (s *AnalyticsService) RecordPlay(ctx context.Context, trackID uuid.UUID, us
return nil return nil
} }
// GetTrackStats récupère les statistiques d'un track // GetTrackStats récupère les statistiques d'un track (optimisé: 1 requête au lieu de 5)
func (s *AnalyticsService) GetTrackStats(ctx context.Context, trackID uuid.UUID) (*types.TrackStats, error) { // Changed trackID to uuid.UUID func (s *AnalyticsService) GetTrackStats(ctx context.Context, trackID uuid.UUID) (*types.TrackStats, error) {
var stats types.TrackStats var result struct {
TrackDuration int `gorm:"column:track_duration"`
// Vérifier que le track existe TotalPlays int64 `gorm:"column:total_plays"`
var track models.Track UniqueListeners int64 `gorm:"column:unique_listeners"`
if err := s.db.WithContext(ctx).First(&track, "id = ?", trackID).Error; err != nil { // Updated query AvgDuration float64 `gorm:"column:avg_duration"`
if err == gorm.ErrRecordNotFound { CompletedPlays int64 `gorm:"column:completed_plays"`
return nil, errors.New("track not found")
}
return nil, fmt.Errorf("failed to get track: %w", err)
} }
// Total plays tx := s.db.WithContext(ctx).Raw(`
if err := s.db.WithContext(ctx).Model(&models.TrackPlay{}). SELECT
Where("track_id = ?", trackID). t.duration AS track_duration,
Count(&stats.TotalPlays).Error; err != nil { CAST(COUNT(tp.id) AS INTEGER) AS total_plays,
return nil, fmt.Errorf("failed to count total plays: %w", err) CAST(COUNT(DISTINCT CASE WHEN tp.user_id IS NOT NULL THEN tp.user_id END) AS INTEGER) AS unique_listeners,
COALESCE(AVG(tp.duration), 0) AS avg_duration,
CAST(COUNT(CASE WHEN t.duration > 0 AND tp.duration >= t.duration * 0.9 THEN 1 END) AS INTEGER) AS completed_plays
FROM tracks t
LEFT JOIN track_plays tp ON tp.track_id = t.id
WHERE t.id = ?
GROUP BY t.id, t.duration
`, trackID).Scan(&result)
if tx.Error != nil {
return nil, fmt.Errorf("failed to get track stats: %w", tx.Error)
}
if tx.RowsAffected == 0 {
return nil, errors.New("track not found")
} }
// Unique listeners (distinct user_id, en excluant NULL) stats := &types.TrackStats{
if err := s.db.WithContext(ctx).Model(&models.TrackPlay{}). TotalPlays: result.TotalPlays,
Where("track_id = ? AND user_id IS NOT NULL", trackID). UniqueListeners: result.UniqueListeners,
Distinct("user_id"). AverageDuration: result.AvgDuration,
Count(&stats.UniqueListeners).Error; err != nil { CompletionRate: 0,
return nil, fmt.Errorf("failed to count unique listeners: %w", err)
} }
if result.TotalPlays > 0 {
// Average duration stats.CompletionRate = float64(result.CompletedPlays) / float64(result.TotalPlays) * 100
var avgDuration float64
if err := s.db.WithContext(ctx).Model(&models.TrackPlay{}).
Where("track_id = ?", trackID).
Select("COALESCE(AVG(duration), 0)").
Scan(&avgDuration).Error; err != nil {
return nil, fmt.Errorf("failed to calculate average duration: %w", err)
} }
stats.AverageDuration = avgDuration return stats, nil
// Completion rate (90% de la durée du track)
if track.Duration > 0 && stats.TotalPlays > 0 {
var completedPlays int64
completionThreshold := int(float64(track.Duration) * 0.9)
if err := s.db.WithContext(ctx).Model(&models.TrackPlay{}).
Where("track_id = ? AND duration >= ?", trackID, completionThreshold).
Count(&completedPlays).Error; err != nil {
return nil, fmt.Errorf("failed to count completed plays: %w", err)
}
stats.CompletionRate = float64(completedPlays) / float64(stats.TotalPlays) * 100
}
return &stats, nil
} }
// GetPlaysOverTime récupère les lectures sur une période pour un graphique temporel // GetPlaysOverTime récupère les lectures sur une période pour un graphique temporel