diff --git a/backend/proto/task.proto b/backend/proto/task.proto index 79e0354..d7e9069 100644 --- a/backend/proto/task.proto +++ b/backend/proto/task.proto @@ -78,6 +78,7 @@ message OnboardingStage { string status = 6; // pending/completed/in_progress bool is_current = 7; bool all_tasks_completed = 8; // 该阶段所有任务是否完成 + bool is_reward_claimed = 9; // 该阶段奖励是否已领取 } message CompleteGuideRequest { diff --git a/backend/services/taskService/model/task_models.go b/backend/services/taskService/model/task_models.go index 3ff41bd..9cb0584 100644 --- a/backend/services/taskService/model/task_models.go +++ b/backend/services/taskService/model/task_models.go @@ -49,21 +49,22 @@ func (UserOnboardingProgress) TableName() string { return "user_onboarding_progr // UserOnboardingStatus 引导流程状态表(per-user per-star) type UserOnboardingStatus struct { - ID int64 `gorm:"primaryKey;column:id;autoIncrement"` - UserID int64 `gorm:"column:user_id;not null;uniqueIndex:uk_user_star_onboarding"` - StarID int64 `gorm:"column:star_id;not null;uniqueIndex:uk_user_star_onboarding"` - IsOnboardingCompleted bool `gorm:"column:is_onboarding_completed;default:false"` - IsOnboardingClaimed bool `gorm:"column:is_onboarding_claimed;default:false"` - HasFriendDisplayBonus bool `gorm:"column:has_friend_display_bonus;default:false"` - OnboardingCompletedAt *int64 `gorm:"column:onboarding_completed_at"` - OnboardingClaimedAt *int64 `gorm:"column:onboarding_claimed_at"` - CreatedAt int64 `gorm:"column:created_at"` - UpdatedAt int64 `gorm:"column:updated_at"` - CurrentStage int64 `gorm:"column:current_stage;default:0"` - Status string `gorm:"column:status;size:20;default:pending"` - IsFirstLoginBonusClaimed bool `gorm:"column:is_first_login_bonus_claimed;default:false"` // 废弃字段 - CompletedAt *int64 `gorm:"column:completed_at"` // 废弃字段 - ClaimedAt *int64 `gorm:"column:claimed_at"` // 废弃字段 + ID int64 `gorm:"primaryKey;column:id;autoIncrement"` + UserID int64 `gorm:"column:user_id;not null;uniqueIndex:uk_user_star_onboarding"` + StarID int64 `gorm:"column:star_id;not null;uniqueIndex:uk_user_star_onboarding"` + IsOnboardingCompleted bool `gorm:"column:is_onboarding_completed;default:false"` + IsOnboardingClaimed bool `gorm:"column:is_onboarding_claimed;default:false"` + HasFriendDisplayBonus bool `gorm:"column:has_friend_display_bonus;default:false"` + OnboardingCompletedAt *int64 `gorm:"column:onboarding_completed_at"` + OnboardingClaimedAt *int64 `gorm:"column:onboarding_claimed_at"` + ClaimedStages []int64 `gorm:"column:claimed_stages;type:text;serializer:json"` // 已领取奖励的阶段列表 + CreatedAt int64 `gorm:"column:created_at"` + UpdatedAt int64 `gorm:"column:updated_at"` + CurrentStage int64 `gorm:"column:current_stage;default:0"` + Status string `gorm:"column:status;size:20;default:pending"` + IsFirstLoginBonusClaimed bool `gorm:"column:is_first_login_bonus_claimed;default:false"` // 废弃字段 + CompletedAt *int64 `gorm:"column:completed_at"` // 废弃字段 + ClaimedAt *int64 `gorm:"column:claimed_at"` // 废弃字段 } func (UserOnboardingStatus) TableName() string { return "user_onboarding_status" } diff --git a/backend/services/taskService/repository/onboarding_repo.go b/backend/services/taskService/repository/onboarding_repo.go index b6dd042..5b17bf6 100644 --- a/backend/services/taskService/repository/onboarding_repo.go +++ b/backend/services/taskService/repository/onboarding_repo.go @@ -8,6 +8,7 @@ import ( "github.com/topfans/backend/services/taskService/model" "go.uber.org/zap" "gorm.io/gorm" + "gorm.io/gorm/clause" ) type OnboardingRepository interface { @@ -186,42 +187,22 @@ func (r *onboardingRepository) SaveStageConfigs(configs []*model.OnboardingStage zap.Int64("exp_reward", cfg.ExpReward)) cfg.UpdatedAt = now - - // First try to update existing record - // Use Select to specify fields so GORM properly handles JSON serialization - result := r.db.Model(&model.OnboardingStageConfig{}). - Where("stage = ?", cfg.Stage). - Select("name", "required_task_keys", "crystal_reward", "exp_reward", "is_active", "updated_at"). - Updates(cfg) - - if result.Error != nil { - logger.Logger.Error("SaveStageConfigs: failed to update config", - zap.Int("stage", cfg.Stage), - zap.Error(result.Error)) - return result.Error + // 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", + }), } - - logger.Logger.Info("SaveStageConfigs: update result", + 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.Int64("rows_affected", result.RowsAffected)) - - // If no rows affected, create new record - if result.RowsAffected == 0 { - cfg.CreatedAt = now - if err := r.db.Create(cfg).Error; err != nil { - logger.Logger.Error("SaveStageConfigs: failed to create config", - zap.Int("stage", cfg.Stage), - zap.Error(err)) - return err - } - logger.Logger.Info("SaveStageConfigs: created config", - zap.Int("stage", cfg.Stage), - zap.String("name", cfg.Name)) - } else { - logger.Logger.Info("SaveStageConfigs: updated config", - zap.Int("stage", cfg.Stage), - zap.String("name", cfg.Name)) - } + zap.String("name", cfg.Name)) } logger.Logger.Info("SaveStageConfigs: all configs saved successfully") diff --git a/frontend/components/GuideOverlay.vue b/frontend/components/GuideOverlay.vue index 4988094..45667f7 100644 --- a/frontend/components/GuideOverlay.vue +++ b/frontend/components/GuideOverlay.vue @@ -51,6 +51,8 @@