feat(v0.11.0): F381-F385 database migrations and models for creator analytics

Add daily_track_stats, geographic_play_stats, track_discovery_sources tables.
Add source and country_code columns to track_plays.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
senke 2026-03-10 16:21:01 +01:00
parent 19fec9e40a
commit b955a3c0b4
3 changed files with 148 additions and 0 deletions

View file

@ -0,0 +1,84 @@
package models
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// DailyTrackStats represents daily aggregated playback statistics for a track
type DailyTrackStats struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
TrackID uuid.UUID `gorm:"type:uuid;not null;uniqueIndex:uq_daily_track_stats_track_date" json:"track_id"`
Date time.Time `gorm:"type:date;not null;uniqueIndex:uq_daily_track_stats_track_date" json:"date"`
TotalPlays int64 `gorm:"not null;default:0" json:"total_plays"`
UniqueListeners int64 `gorm:"not null;default:0" json:"unique_listeners"`
CompleteListens int64 `gorm:"not null;default:0" json:"complete_listens"`
TotalPlayTime int64 `gorm:"not null;default:0" json:"total_play_time"`
AvgCompletionRate float64 `gorm:"type:decimal(5,2);not null;default:0" json:"avg_completion_rate"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Track Track `gorm:"foreignKey:TrackID;constraint:OnDelete:CASCADE" json:"-"`
}
func (DailyTrackStats) TableName() string {
return "daily_track_stats"
}
func (m *DailyTrackStats) BeforeCreate(tx *gorm.DB) error {
if m.ID == uuid.Nil {
m.ID = uuid.New()
}
return nil
}
// GeographicPlayStats represents aggregated geographic play data (anonymized)
type GeographicPlayStats struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
TrackID uuid.UUID `gorm:"type:uuid;not null" json:"track_id"`
CountryCode string `gorm:"size:2;not null" json:"country_code"`
Region string `gorm:"size:100;not null;default:''" json:"region"`
Date time.Time `gorm:"type:date;not null" json:"date"`
PlayCount int64 `gorm:"not null;default:0" json:"play_count"`
UniqueListeners int64 `gorm:"not null;default:0" json:"unique_listeners"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Track Track `gorm:"foreignKey:TrackID;constraint:OnDelete:CASCADE" json:"-"`
}
func (GeographicPlayStats) TableName() string {
return "geographic_play_stats"
}
func (m *GeographicPlayStats) BeforeCreate(tx *gorm.DB) error {
if m.ID == uuid.Nil {
m.ID = uuid.New()
}
return nil
}
// TrackDiscoverySource records how a user discovered a track
type TrackDiscoverySource struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
TrackID uuid.UUID `gorm:"type:uuid;not null" json:"track_id"`
UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"`
Source string `gorm:"size:50;not null" json:"source"` // search, feed, share, profile, playlist, direct
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
Track Track `gorm:"foreignKey:TrackID;constraint:OnDelete:CASCADE" json:"-"`
User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"-"`
}
func (TrackDiscoverySource) TableName() string {
return "track_discovery_sources"
}
func (m *TrackDiscoverySource) BeforeCreate(tx *gorm.DB) error {
if m.ID == uuid.Nil {
m.ID = uuid.New()
}
return nil
}

View file

@ -0,0 +1,57 @@
-- Migration 945: Creator Analytics v0.11.0 (F381-F385)
-- Adds tables for geographic stats, discovery sources, and daily aggregated stats
-- Daily aggregated track stats for efficient creator dashboard queries
CREATE TABLE IF NOT EXISTS daily_track_stats (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
track_id UUID NOT NULL REFERENCES tracks(id) ON DELETE CASCADE,
date DATE NOT NULL,
total_plays BIGINT NOT NULL DEFAULT 0,
unique_listeners BIGINT NOT NULL DEFAULT 0,
complete_listens BIGINT NOT NULL DEFAULT 0,
total_play_time BIGINT NOT NULL DEFAULT 0, -- seconds
avg_completion_rate DECIMAL(5,2) NOT NULL DEFAULT 0, -- percentage 0-100
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT uq_daily_track_stats_track_date UNIQUE(track_id, date)
);
CREATE INDEX IF NOT EXISTS idx_daily_track_stats_track_id ON daily_track_stats(track_id);
CREATE INDEX IF NOT EXISTS idx_daily_track_stats_date ON daily_track_stats(date);
CREATE INDEX IF NOT EXISTS idx_daily_track_stats_track_date ON daily_track_stats(track_id, date);
-- Discovery sources: how users find tracks
CREATE TABLE IF NOT EXISTS track_discovery_sources (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
track_id UUID NOT NULL REFERENCES tracks(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
source VARCHAR(50) NOT NULL, -- 'search', 'feed', 'share', 'profile', 'playlist', 'direct'
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_track_discovery_track_id ON track_discovery_sources(track_id);
CREATE INDEX IF NOT EXISTS idx_track_discovery_source ON track_discovery_sources(source);
CREATE INDEX IF NOT EXISTS idx_track_discovery_created_at ON track_discovery_sources(created_at);
-- Geographic play stats (aggregated, anonymized — never store individual user locations)
CREATE TABLE IF NOT EXISTS geographic_play_stats (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
track_id UUID NOT NULL REFERENCES tracks(id) ON DELETE CASCADE,
country_code VARCHAR(2) NOT NULL, -- ISO 3166-1 alpha-2
region VARCHAR(100) NOT NULL DEFAULT '',
date DATE NOT NULL,
play_count BIGINT NOT NULL DEFAULT 0,
unique_listeners BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT uq_geo_stats_track_country_region_date UNIQUE(track_id, country_code, region, date)
);
CREATE INDEX IF NOT EXISTS idx_geo_stats_track_id ON geographic_play_stats(track_id);
CREATE INDEX IF NOT EXISTS idx_geo_stats_country ON geographic_play_stats(country_code);
CREATE INDEX IF NOT EXISTS idx_geo_stats_date ON geographic_play_stats(date);
-- Add source column to track_plays for discovery tracking
ALTER TABLE track_plays ADD COLUMN IF NOT EXISTS source VARCHAR(50) DEFAULT '';
-- Add country_code to track_plays for geographic aggregation
ALTER TABLE track_plays ADD COLUMN IF NOT EXISTS country_code VARCHAR(2) DEFAULT '';

View file

@ -0,0 +1,7 @@
-- Rollback migration 945: Creator Analytics v0.11.0
ALTER TABLE track_plays DROP COLUMN IF EXISTS country_code;
ALTER TABLE track_plays DROP COLUMN IF EXISTS source;
DROP TABLE IF EXISTS geographic_play_stats;
DROP TABLE IF EXISTS track_discovery_sources;
DROP TABLE IF EXISTS daily_track_stats;