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:
parent
19fec9e40a
commit
b955a3c0b4
3 changed files with 148 additions and 0 deletions
84
veza-backend-api/internal/models/daily_track_stats.go
Normal file
84
veza-backend-api/internal/models/daily_track_stats.go
Normal 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
|
||||
}
|
||||
57
veza-backend-api/migrations/945_creator_analytics_v0110.sql
Normal file
57
veza-backend-api/migrations/945_creator_analytics_v0110.sql
Normal 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 '';
|
||||
|
|
@ -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;
|
||||
Loading…
Reference in a new issue