topfans/backend/services/taskService/service/onboarding_service.go
2026-05-16 02:42:32 +08:00

555 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}