package service import ( "context" "fmt" "strconv" "time" "github.com/topfans/backend/pkg/logger" pbCommon "github.com/topfans/backend/pkg/proto/common" pb "github.com/topfans/backend/pkg/proto/task" "github.com/topfans/backend/services/taskService/client" "github.com/topfans/backend/services/taskService/model" "github.com/topfans/backend/services/taskService/repository" "go.uber.org/zap" ) // OnboardingService 引导流程Service接口 type OnboardingService interface { CompleteGuide(ctx context.Context, userID int64, taskKey string, stages []*pb.OnboardingStage) (*pb.CompleteGuideResponse, error) GetOnboardingStatus(ctx context.Context, userID, starID int64) (*pb.GetOnboardingStatusResponse, error) AdvanceStage(ctx context.Context, userID, starID int64, targetStage int32) (*pb.AdvanceStageResponse, error) ClaimOnboardingReward(ctx context.Context, userID, starID int64, stage int32) (*pb.ClaimOnboardingRewardResponse, error) InitUserTasks(ctx context.Context, userID, starID int64) error } // onboardingService 引导流程Service实现 type onboardingService struct { onboardingRepo repository.OnboardingRepository dailyRepo repository.DailyTaskRepository userRPCClient client.UserServiceClient } // NewOnboardingService 创建引导Service实例 func NewOnboardingService( onboardingRepo repository.OnboardingRepository, dailyRepo repository.DailyTaskRepository, userRPCClient client.UserServiceClient, ) OnboardingService { return &onboardingService{ onboardingRepo: onboardingRepo, dailyRepo: dailyRepo, userRPCClient: userRPCClient, } } // CompleteGuide 完成引导任务 func (s *onboardingService) CompleteGuide(ctx context.Context, userID int64, taskKey string, stages []*pb.OnboardingStage) (*pb.CompleteGuideResponse, error) { logger.Logger.Info("CompleteGuide", zap.Int64("user_id", userID), zap.String("task_key", taskKey), zap.Int("stages_count", len(stages))) // 如果前端传了 stages 配置,则存储到数据库 if len(stages) > 0 { logger.Logger.Info("CompleteGuide: saving stage configs", zap.Int64("user_id", userID), zap.Int("count", len(stages))) for i, stage := range stages { logger.Logger.Info("CompleteGuide: stage detail", zap.Int64("user_id", userID), zap.Int("index", i), zap.Int32("stage", stage.Stage), zap.String("name", stage.Name), zap.Strings("required_task_keys", stage.RequiredTaskKeys)) } configs := make([]*model.OnboardingStageConfig, 0, len(stages)) for _, stage := range stages { configs = append(configs, &model.OnboardingStageConfig{ Stage: int(stage.Stage), Name: stage.Name, RequiredTaskKeys: stage.RequiredTaskKeys, CrystalReward: stage.CrystalReward, IsActive: true, }) } if err := s.onboardingRepo.SaveStageConfigs(configs); err != nil { logger.Logger.Error("CompleteGuide: failed to save stage configs", zap.Int64("user_id", userID), zap.Error(err)) // 不阻塞主流程,继续 } else { logger.Logger.Info("CompleteGuide: stage configs saved successfully", zap.Int64("user_id", userID), zap.Int("count", len(configs))) } } else { logger.Logger.Info("CompleteGuide: stages is empty, skipping save", zap.Int64("user_id", userID)) } // 获取或创建引导进度 progress, err := s.onboardingRepo.GetOrCreateOnboardingProgress(userID, taskKey) if err != nil { logger.Logger.Error("CompleteGuide: failed to get or create progress", zap.Int64("user_id", userID), zap.String("task_key", taskKey), zap.Error(err)) return &pb.CompleteGuideResponse{ Base: &pbCommon.BaseResponse{Code: 500, Message: err.Error()}, }, err } // 如果已完成,跳过进度更新,但仍然返回传入的 stages if progress.Status == "completed" { logger.Logger.Info("CompleteGuide: already completed, returning stages", zap.Int64("user_id", userID), zap.String("task_key", taskKey)) return &pb.CompleteGuideResponse{ Base: &pbCommon.BaseResponse{Code: pbCommon.StatusCode_STATUS_OK}, UserId: userID, CurrentStage: int32(0), Status: progress.Status, Stages: stages, }, nil } // 更新进度 now := time.Now().Unix() progress.Status = "completed" progress.CompletedAt = &now if err := s.onboardingRepo.UpdateOnboardingProgress(progress); err != nil { logger.Logger.Error("CompleteGuide: failed to update progress", zap.Int64("user_id", userID), zap.String("task_key", taskKey), zap.Error(err)) return &pb.CompleteGuideResponse{ Base: &pbCommon.BaseResponse{Code: 500, Message: err.Error()}, }, err } logger.Logger.Info("CompleteGuide: success", zap.Int64("user_id", userID), zap.String("task_key", taskKey)) return &pb.CompleteGuideResponse{ Base: &pbCommon.BaseResponse{Code: pbCommon.StatusCode_STATUS_OK}, UserId: userID, CurrentStage: int32(0), Status: "completed", Stages: stages, }, nil } // GetOnboardingStatus 获取引导状态 func (s *onboardingService) GetOnboardingStatus(ctx context.Context, userID, starID int64) (*pb.GetOnboardingStatusResponse, error) { logger.Logger.Debug("GetOnboardingStatus", zap.Int64("user_id", userID), zap.Int64("star_id", starID)) // 获取或创建用户引导状态 status, err := s.onboardingRepo.GetOrCreateOnboardingStatus(userID, starID) if err != nil { logger.Logger.Error("GetOnboardingStatus: failed to get status", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Error(err)) return &pb.GetOnboardingStatusResponse{}, err } // 获取用户所有引导进度 progressList, err := s.onboardingRepo.ListUserOnboardingProgressByUser(userID) if err != nil { logger.Logger.Error("GetOnboardingStatus: failed to list progress", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Error(err)) return &pb.GetOnboardingStatusResponse{}, err } // 构建 taskKey -> progress 映射 progressMap := make(map[string]*model.UserOnboardingProgress) for _, p := range progressList { progressMap[p.TaskKey] = p } // 获取所有活跃的阶段配置 stageConfigs, err := s.onboardingRepo.ListActiveStageConfigs() if err != nil { logger.Logger.Error("GetOnboardingStatus: failed to list stage configs", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Error(err)) return &pb.GetOnboardingStatusResponse{}, err } // 转换为 pb.OnboardingStage stages := make([]*pb.OnboardingStage, 0, len(stageConfigs)) for _, cfg := range stageConfigs { stage := &pb.OnboardingStage{ Stage: int32(cfg.Stage), Name: cfg.Name, RequiredTaskKeys: cfg.RequiredTaskKeys, CrystalReward: cfg.CrystalReward, Status: "locked", } // 判断阶段状态 if int64(cfg.Stage) < status.CurrentStage { stage.Status = "completed" } else if int64(cfg.Stage) == status.CurrentStage { if status.Status == "completed" { stage.Status = "completed" } else { stage.Status = "in_progress" } } // 判断是否为当前阶段 if int64(cfg.Stage) == status.CurrentStage { stage.IsCurrent = true } // 检查阶段任务是否全部完成 allCompleted := true for _, taskKey := range cfg.RequiredTaskKeys { progress, ok := progressMap[taskKey] if !ok || progress.Status != "completed" { allCompleted = false break } } stage.AllTasksCompleted = allCompleted // 检查该阶段奖励是否已领取(需要 proto 重新生成后生效) // stage.IsRewardClaimed = containsInt64(status.ClaimedStages, int64(cfg.Stage)) stages = append(stages, stage) } return &pb.GetOnboardingStatusResponse{ Base: &pbCommon.BaseResponse{Code: pbCommon.StatusCode_STATUS_OK}, UserId: userID, CurrentStage: int32(status.CurrentStage), Status: status.Status, Stages: stages, }, nil } // AdvanceStage 进入下一阶段 func (s *onboardingService) AdvanceStage(ctx context.Context, userID, starID int64, targetStage int32) (*pb.AdvanceStageResponse, error) { logger.Logger.Info("AdvanceStage", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Int32("target_stage", targetStage)) // 获取当前状态 status, err := s.onboardingRepo.GetOnboardingStatus(userID, starID) if err != nil { logger.Logger.Error("AdvanceStage: failed to get status", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Error(err)) return &pb.AdvanceStageResponse{ Base: &pbCommon.BaseResponse{Code: 500, Message: err.Error()}, }, err } // 获取目标阶段配置 targetConfig, err := s.onboardingRepo.GetStageConfig(int64(targetStage)) if err != nil { logger.Logger.Error("AdvanceStage: failed to get target stage config", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Int32("target_stage", targetStage), zap.Error(err)) return &pb.AdvanceStageResponse{ Base: &pbCommon.BaseResponse{Code: 500, Message: err.Error()}, }, err } // 获取用户所有引导进度 progressList, err := s.onboardingRepo.ListUserOnboardingProgressByUser(userID) if err != nil { logger.Logger.Error("AdvanceStage: failed to list progress", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Error(err)) return &pb.AdvanceStageResponse{ Base: &pbCommon.BaseResponse{Code: 500, Message: err.Error()}, }, err } // 构建 taskKey -> progress 映射 progressMap := make(map[string]*model.UserOnboardingProgress) for _, p := range progressList { progressMap[p.TaskKey] = p } // 检查目标阶段的前置任务是否全部完成 for _, taskKey := range targetConfig.RequiredTaskKeys { progress, ok := progressMap[taskKey] if !ok || progress.Status != "completed" { logger.Logger.Warn("AdvanceStage: required task not completed", zap.Int64("user_id", userID), zap.String("task_key", taskKey)) return &pb.AdvanceStageResponse{ Base: &pbCommon.BaseResponse{Code: 400, Message: "required task not completed"}, }, nil } } // 更新状态 status.CurrentStage = int64(targetStage) if int64(targetConfig.Stage) == 0 { status.Status = "in_progress" } if err := s.onboardingRepo.UpdateOnboardingStatus(status); err != nil { logger.Logger.Error("AdvanceStage: failed to update status", zap.Int64("user_id", userID), zap.Error(err)) return &pb.AdvanceStageResponse{ Base: &pbCommon.BaseResponse{Code: 500, Message: err.Error()}, }, err } logger.Logger.Info("AdvanceStage: success", zap.Int64("user_id", userID), zap.Int32("new_stage", targetStage)) return &pb.AdvanceStageResponse{ Base: &pbCommon.BaseResponse{Code: pbCommon.StatusCode_STATUS_OK}, CurrentStage: targetStage, Status: status.Status, }, nil } // ClaimOnboardingReward 领取引导阶段奖励 func (s *onboardingService) ClaimOnboardingReward(ctx context.Context, userID, starID int64, stage int32) (*pb.ClaimOnboardingRewardResponse, error) { logger.Logger.Info("ClaimOnboardingReward", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Int32("stage", stage)) // 获取阶段配置 config, err := s.onboardingRepo.GetStageConfig(int64(stage)) if err != nil { logger.Logger.Error("ClaimOnboardingReward: failed to get stage config", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Int32("stage", stage), zap.Error(err)) return &pb.ClaimOnboardingRewardResponse{ Base: &pbCommon.BaseResponse{Code: 500, Message: err.Error()}, Success: false, }, nil } logger.Logger.Info("ClaimOnboardingReward: got stage config", zap.Int64("user_id", userID), zap.Int32("stage", stage), zap.String("name", config.Name), zap.Int64("crystal_reward", config.CrystalReward)) // 获取用户引导状态 status, err := s.onboardingRepo.GetOnboardingStatus(userID, starID) if err != nil { logger.Logger.Error("ClaimOnboardingReward: failed to get status", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Error(err)) return &pb.ClaimOnboardingRewardResponse{ Base: &pbCommon.BaseResponse{Code: 500, Message: "user onboarding status not found"}, Success: false, }, nil } // 检查是否已经领取过该阶段奖励 if containsInt64(status.ClaimedStages, int64(stage)) { logger.Logger.Warn("ClaimOnboardingReward: reward already claimed", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Int32("stage", stage)) return &pb.ClaimOnboardingRewardResponse{ Base: &pbCommon.BaseResponse{Code: 400, Message: "reward already claimed"}, Success: false, }, nil } // 检查该阶段的前置任务是否全部完成 progressList, errProgress := s.onboardingRepo.ListUserOnboardingProgressByUser(userID) if errProgress != nil { logger.Logger.Error("ClaimOnboardingReward: failed to list progress", zap.Int64("user_id", userID), zap.Error(errProgress)) } progressMap := make(map[string]*model.UserOnboardingProgress) for _, p := range progressList { progressMap[p.TaskKey] = p } for _, taskKey := range config.RequiredTaskKeys { progress, ok := progressMap[taskKey] if !ok || progress.Status != "completed" { logger.Logger.Warn("ClaimOnboardingReward: required task not completed", zap.Int64("user_id", userID), zap.String("task_key", taskKey)) return &pb.ClaimOnboardingRewardResponse{ Base: &pbCommon.BaseResponse{Code: 400, Message: "required task not completed"}, Success: false, }, nil } } // 使用传入的 starID,如果为0则从 onboarding_status 中获取该用户的有效 starID balanceStarID := starID if balanceStarID == 0 { // 从用户已有的 onboarding_status 记录中获取非0的 starID statuses, err := s.onboardingRepo.GetUserOnboardingStatuses(userID) if err == nil && len(statuses) > 0 { for _, s := range statuses { if s.StarID > 0 { balanceStarID = s.StarID break } } } // 如果仍然为0,使用传入的starID(可能是0) if balanceStarID == 0 { logger.Logger.Warn("ClaimOnboardingReward: no valid star_id found", zap.Int64("user_id", userID), zap.Int64("star_id", starID)) balanceStarID = starID } } // 获取当前用户余额 currentProfile, errProfile := s.userRPCClient.GetFanProfile(ctx, userID, balanceStarID) var currentCrystal int64 if errProfile != nil { logger.Logger.Warn("ClaimOnboardingReward: failed to get fan profile", zap.Int64("user_id", userID), zap.Int64("balance_star_id", balanceStarID), zap.Error(errProfile)) } else if currentProfile != nil { currentCrystal = currentProfile.CrystalBalance } // 发放水晶奖励 var newCrystalBalance int64 logger.Logger.Info("ClaimOnboardingReward: about to update crystal", zap.Int64("user_id", userID), zap.Int64("balance_star_id", balanceStarID), zap.Int64("crystal_reward", config.CrystalReward), zap.Int64("current_crystal", currentCrystal)) if config.CrystalReward > 0 { newCrystalBalance, err = s.userRPCClient.UpdateCrystalBalance(ctx, userID, balanceStarID, config.CrystalReward, "onboarding_reward", strconv.FormatInt(int64(stage), 10), fmt.Sprintf("引导阶段%d奖励", stage)) if err != nil { logger.Logger.Error("ClaimOnboardingReward: failed to update crystal", zap.Int64("user_id", userID), zap.Error(err)) newCrystalBalance = currentCrystal } logger.Logger.Info("ClaimOnboardingReward: crystal update result", zap.Int64("user_id", userID), zap.Int64("new_balance", newCrystalBalance), zap.Error(err)) } else { logger.Logger.Info("ClaimOnboardingReward: crystal_reward is 0, skipping update") newCrystalBalance = currentCrystal } // 重新获取最新的余额,确保返回准确的数值(解决RPC返回值可能为0的问题) if config.CrystalReward > 0 { latestProfile, errLatest := s.userRPCClient.GetFanProfile(ctx, userID, balanceStarID) if errLatest != nil { logger.Logger.Warn("ClaimOnboardingReward: failed to get latest profile, using computed values", zap.Int64("user_id", userID), zap.Int64("balance_star_id", balanceStarID), zap.Error(errLatest)) } else if latestProfile != nil { newCrystalBalance = latestProfile.CrystalBalance logger.Logger.Info("ClaimOnboardingReward: updated with latest profile values", zap.Int64("user_id", userID), zap.Int64("new_crystal_balance", newCrystalBalance)) } } // 更新状态 now := time.Now().Unix() // 标记为已领取 status.IsOnboardingClaimed = true status.OnboardingClaimedAt = &now status.Status = "claimed" // 添加到已领取阶段列表 if !containsInt64(status.ClaimedStages, int64(stage)) { status.ClaimedStages = append(status.ClaimedStages, int64(stage)) } if err := s.onboardingRepo.UpdateOnboardingStatus(status); err != nil { logger.Logger.Error("ClaimOnboardingReward: failed to update status", zap.Int64("user_id", userID), zap.Error(err)) return &pb.ClaimOnboardingRewardResponse{ Base: &pbCommon.BaseResponse{Code: 500, Message: "failed to update status"}, Success: false, }, err } logger.Logger.Info("ClaimOnboardingReward: success", zap.Int64("user_id", userID), zap.Int32("stage", stage), zap.Int64("balance_star_id", balanceStarID), zap.Int64("new_crystal", newCrystalBalance)) return &pb.ClaimOnboardingRewardResponse{ Base: &pbCommon.BaseResponse{Code: pbCommon.StatusCode_STATUS_OK}, Success: true, CrystalBalance: strconv.FormatInt(newCrystalBalance, 10), }, nil } // containsInt64 检查切片是否包含指定值 func containsInt64(slice []int64, val int64) bool { for _, v := range slice { if v == val { return true } } return false } // InitUserTasks 初始化用户任务 // 由 userService 或其他服务在用户注册/新增粉丝身份时调用 func (s *onboardingService) InitUserTasks(ctx context.Context, userID, starID int64) error { logger.Logger.Info("InitUserTasks", zap.Int64("user_id", userID), zap.Int64("star_id", starID)) // 确保引导状态存在 _, err := s.onboardingRepo.GetOrCreateOnboardingStatus(userID, starID) if err != nil { logger.Logger.Error("InitUserTasks: failed to get or create onboarding status", zap.Int64("user_id", userID), zap.Error(err)) return err } // 初始化每日任务 if err := s.dailyRepo.InitDailyTasksForUser(userID, starID); err != nil { logger.Logger.Error("InitUserTasks: failed to init daily tasks", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Error(err)) return err } logger.Logger.Info("InitUserTasks: success", zap.Int64("user_id", userID), zap.Int64("star_id", starID)) return nil }