package repository import ( "errors" "time" "github.com/topfans/backend/pkg/logger" "github.com/topfans/backend/services/taskService/model" "go.uber.org/zap" "gorm.io/gorm" "gorm.io/gorm/clause" ) type OnboardingRepository interface { GetOnboardingStatus(userID int64, starID int64) (*model.UserOnboardingStatus, error) GetOrCreateOnboardingStatus(userID int64, starID int64) (*model.UserOnboardingStatus, error) GetUserOnboardingStatuses(userID int64) ([]*model.UserOnboardingStatus, error) UpdateOnboardingStatus(status *model.UserOnboardingStatus) error UpdateOnboardingProgress(progress *model.UserOnboardingProgress) error GetUserOnboardingProgress(userID int64, taskKey string) (*model.UserOnboardingProgress, error) GetOrCreateOnboardingProgress(userID int64, taskKey string) (*model.UserOnboardingProgress, error) ListActiveStageConfigs() ([]*model.OnboardingStageConfig, error) ListUserOnboardingProgressByUser(userID int64) ([]*model.UserOnboardingProgress, error) GetStageConfig(stage int64) (*model.OnboardingStageConfig, error) SaveStageConfigs(configs []*model.OnboardingStageConfig) error CountStageConfigs() (int64, error) } type onboardingRepository struct { db *gorm.DB } func NewOnboardingRepository(db *gorm.DB) OnboardingRepository { return &onboardingRepository{db: db} } func (r *onboardingRepository) GetOnboardingStatus(userID int64, starID int64) (*model.UserOnboardingStatus, error) { var status model.UserOnboardingStatus err := r.db.Where("user_id = ? AND star_id = ?", userID, starID).First(&status).Error if err != nil { return nil, err } return &status, nil } func (r *onboardingRepository) GetOrCreateOnboardingStatus(userID int64, starID int64) (*model.UserOnboardingStatus, error) { var status model.UserOnboardingStatus err := r.db.Where("user_id = ? AND star_id = ?", userID, starID).First(&status).Error if err == nil { return &status, nil } if !errors.Is(err, gorm.ErrRecordNotFound) { logger.Logger.Error("Failed to GetOrCreateOnboardingStatus", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Error(err)) return nil, err } now := time.Now().Unix() status = model.UserOnboardingStatus{ UserID: userID, StarID: starID, CurrentStage: 0, Status: "pending", CreatedAt: now, UpdatedAt: now, } if err := r.db.Create(&status).Error; err != nil { logger.Logger.Error("Failed to Create OnboardingStatus", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Error(err)) return nil, err } return &status, nil } func (r *onboardingRepository) GetUserOnboardingStatuses(userID int64) ([]*model.UserOnboardingStatus, error) { var statuses []*model.UserOnboardingStatus err := r.db.Where("user_id = ?", userID).Order("star_id DESC").Find(&statuses).Error if err != nil { logger.Logger.Error("Failed to GetUserOnboardingStatuses", zap.Int64("user_id", userID), zap.Error(err)) return nil, err } return statuses, nil } func (r *onboardingRepository) UpdateOnboardingStatus(status *model.UserOnboardingStatus) error { status.UpdatedAt = time.Now().Unix() if err := r.db.Save(status).Error; err != nil { logger.Logger.Error("Failed to UpdateOnboardingStatus", zap.Int64("user_id", status.UserID), zap.Error(err)) return err } return nil } func (r *onboardingRepository) UpdateOnboardingProgress(progress *model.UserOnboardingProgress) error { progress.UpdatedAt = time.Now().Unix() if err := r.db.Save(progress).Error; err != nil { logger.Logger.Error("Failed to UpdateOnboardingProgress", zap.Int64("user_id", progress.UserID), zap.String("task_key", progress.TaskKey), zap.Error(err)) return err } return nil } func (r *onboardingRepository) GetUserOnboardingProgress(userID int64, taskKey string) (*model.UserOnboardingProgress, error) { var progress model.UserOnboardingProgress err := r.db.Where("user_id = ? AND task_key = ?", userID, taskKey).First(&progress).Error if err != nil { return nil, err } return &progress, nil } func (r *onboardingRepository) GetOrCreateOnboardingProgress(userID int64, taskKey string) (*model.UserOnboardingProgress, error) { var progress model.UserOnboardingProgress err := r.db.Where("user_id = ? AND task_key = ?", userID, taskKey).First(&progress).Error if err == nil { return &progress, nil } if !errors.Is(err, gorm.ErrRecordNotFound) { logger.Logger.Error("Failed to GetOrCreateOnboardingProgress", zap.Int64("user_id", userID), zap.String("task_key", taskKey), zap.Error(err)) return nil, err } now := time.Now().Unix() progress = model.UserOnboardingProgress{ UserID: userID, TaskKey: taskKey, Status: "pending", CreatedAt: now, UpdatedAt: now, } if err := r.db.Create(&progress).Error; err != nil { logger.Logger.Error("Failed to Create OnboardingProgress", zap.Int64("user_id", userID), zap.String("task_key", taskKey), zap.Error(err)) return nil, err } return &progress, nil } func (r *onboardingRepository) ListActiveStageConfigs() ([]*model.OnboardingStageConfig, error) { var configs []*model.OnboardingStageConfig err := r.db.Where("is_active = ?", true).Order("sort_order ASC").Find(&configs).Error if err != nil { logger.Logger.Error("Failed to ListActiveStageConfigs", zap.Error(err)) return nil, err } return configs, nil } func (r *onboardingRepository) ListUserOnboardingProgressByUser(userID int64) ([]*model.UserOnboardingProgress, error) { var progressList []*model.UserOnboardingProgress err := r.db.Where("user_id = ?", userID).Find(&progressList).Error if err != nil { logger.Logger.Error("Failed to ListUserOnboardingProgressByUser", zap.Int64("user_id", userID), zap.Error(err)) return nil, err } return progressList, nil } func (r *onboardingRepository) GetStageConfig(stage int64) (*model.OnboardingStageConfig, error) { var config model.OnboardingStageConfig logger.Logger.Info("GetStageConfig: querying", zap.Int64("stage", stage)) err := r.db.Where("stage = ? AND is_active = ?", stage, true).First(&config).Error if err != nil { logger.Logger.Error("GetStageConfig: query failed", zap.Int64("stage", stage), zap.Error(err)) return nil, err } logger.Logger.Info("GetStageConfig: found", zap.Int64("stage", stage), zap.String("name", config.Name)) return &config, nil } func (r *onboardingRepository) SaveStageConfigs(configs []*model.OnboardingStageConfig) error { if len(configs) == 0 { logger.Logger.Warn("SaveStageConfigs: configs is empty, returning") return nil } now := time.Now().Unix() logger.Logger.Info("SaveStageConfigs: starting save", zap.Int("count", len(configs))) for _, cfg := range configs { logger.Logger.Info("SaveStageConfigs: processing config", zap.Int("stage", cfg.Stage), zap.String("name", cfg.Name), zap.Strings("required_task_keys", cfg.RequiredTaskKeys), zap.Int64("crystal_reward", cfg.CrystalReward), zap.Int64("exp_reward", cfg.ExpReward)) cfg.UpdatedAt = now // Use upsert via ON CONFLICT to properly handle JSON serialization upsert := clause.OnConflict{ Columns: []clause.Column{{Name: "stage"}}, DoUpdates: clause.AssignmentColumns([]string{ "name", "required_task_keys", "crystal_reward", "exp_reward", "is_active", "updated_at", }), } if err := r.db.Clauses(upsert).Create(cfg).Error; err != nil { logger.Logger.Error("SaveStageConfigs: failed to upsert config", zap.Int("stage", cfg.Stage), zap.Error(err)) return err } logger.Logger.Info("SaveStageConfigs: upserted config", zap.Int("stage", cfg.Stage), zap.String("name", cfg.Name)) } logger.Logger.Info("SaveStageConfigs: all configs saved successfully") return nil } func (r *onboardingRepository) CountStageConfigs() (int64, error) { var count int64 err := r.db.Model(&model.OnboardingStageConfig{}).Where("is_active = ?", true).Count(&count).Error return count, err }