package service import ( "context" "errors" "time" appErrors "github.com/topfans/backend/pkg/errors" "github.com/topfans/backend/pkg/logger" "github.com/topfans/backend/pkg/models" pb "github.com/topfans/backend/pkg/proto/activity" pbCommon "github.com/topfans/backend/pkg/proto/common" "github.com/topfans/backend/services/activityService/client" "github.com/topfans/backend/services/activityService/repository" "go.uber.org/zap" ) // ActivityService 活动Service接口 type ActivityService interface { // GetActivityList 获取活动列表 GetActivityList(ctx context.Context, req *pb.GetActivityListRequest) (*pb.GetActivityListResponse, error) // GetActivity 获取活动详情 GetActivity(ctx context.Context, req *pb.GetProgressRequest) (*pb.Activity, error) // GetActivityItems 获取活动道具列表 GetActivityItems(ctx context.Context, req *pb.GetProgressRequest) ([]*pb.ActivityItem, error) // GetProgress 获取活动进度 GetProgress(ctx context.Context, req *pb.GetProgressRequest) (*pb.GetProgressResponse, error) // PurchaseItem 购买道具 PurchaseItem(ctx context.Context, req *pb.PurchaseItemRequest) (*pb.PurchaseItemResponse, error) // GetContributionRanking 获取贡献点排名 GetContributionRanking(ctx context.Context, req *pb.ContributionRankingRequest) (*pb.ContributionRankingResponse, error) // GetMintingActivities 获取铸造活动列表(用于运营banner) GetMintingActivities(ctx context.Context, req *pb.GetMintingActivitiesRequest) (*pb.GetMintingActivitiesResponse, error) } // activityService 活动Service实现 type activityService struct { activityRepo repository.ActivityRepository mintingActivityRepo repository.MintingActivityRepository userRPCClient client.UserRPCClient } // NewActivityService 创建活动Service实例 func NewActivityService(activityRepo repository.ActivityRepository, mintingActivityRepo repository.MintingActivityRepository, userRPCClient client.UserRPCClient) ActivityService { return &activityService{ activityRepo: activityRepo, mintingActivityRepo: mintingActivityRepo, userRPCClient: userRPCClient, } } // GetActivityList 获取活动列表 func (s *activityService) GetActivityList(ctx context.Context, req *pb.GetActivityListRequest) (*pb.GetActivityListResponse, error) { logger.Logger.Info("GetActivityList request", zap.Int64("star_id", req.StarId), zap.String("status", req.Status), zap.Int32("page", req.Page), zap.Int32("page_size", req.PageSize), ) if req.Page <= 0 { req.Page = 1 } if req.PageSize <= 0 { req.PageSize = 10 } activities, total, err := s.activityRepo.GetActivitiesByStar(req.StarId, req.Status, int(req.Page), int(req.PageSize)) if err != nil { logger.Logger.Error("GetActivityList failed", zap.Error(err)) return &pb.GetActivityListResponse{ Base: &pbCommon.BaseResponse{ Code: 500, Message: "获取活动列表失败: " + err.Error(), }, }, nil } // 转换结果 pbActivities := make([]*pb.Activity, len(activities)) for i, activity := range activities { pbActivities[i] = s.convertActivity(activity) } return &pb.GetActivityListResponse{ Base: &pbCommon.BaseResponse{ Code: 200, Message: "ok", }, Activities: pbActivities, Page: req.Page, PageSize: req.PageSize, Total: int32(total), }, nil } // GetActivity 获取活动详情 func (s *activityService) GetActivity(ctx context.Context, req *pb.GetProgressRequest) (*pb.Activity, error) { logger.Logger.Info("GetActivity request", zap.Int64("activity_id", req.ActivityId)) if req.ActivityId <= 0 { return nil, errors.New("activity_id is required") } activity, err := s.activityRepo.GetActivityByID(req.ActivityId) if err != nil { logger.Logger.Error("GetActivity failed", zap.Error(err)) return nil, err } if activity == nil { return nil, appErrors.ErrActivityNotFound } return s.convertActivity(activity), nil } // GetActivityItems 获取活动道具列表 func (s *activityService) GetActivityItems(ctx context.Context, req *pb.GetProgressRequest) ([]*pb.ActivityItem, error) { logger.Logger.Info("GetActivityItems request", zap.Int64("activity_id", req.ActivityId)) if req.ActivityId <= 0 { return nil, errors.New("activity_id is required") } items, err := s.activityRepo.GetActivityItems(req.ActivityId) if err != nil { logger.Logger.Error("GetActivityItems failed", zap.Error(err)) return nil, err } // 转换结果 pbItems := make([]*pb.ActivityItem, len(items)) for i, item := range items { pbItems[i] = &pb.ActivityItem{ Id: item.ID, ItemType: item.ItemType, ItemName: item.ItemName, IconUrl: item.IconURL, CrystalCost: int32(item.CrystalCost), ContributionPoints: int32(item.ContributionPoints), } } return pbItems, nil } // GetProgress 获取活动进度 func (s *activityService) GetProgress(ctx context.Context, req *pb.GetProgressRequest) (*pb.GetProgressResponse, error) { logger.Logger.Info("GetProgress request", zap.Int64("activity_id", req.ActivityId)) if req.ActivityId <= 0 { return nil, errors.New("activity_id is required") } activity, err := s.activityRepo.GetActivityByID(req.ActivityId) if err != nil { logger.Logger.Error("GetProgress failed", zap.Error(err)) return nil, err } if activity == nil { return nil, appErrors.ErrActivityNotFound } currentStage := activity.GetStage() currentStatus := activity.GetCurrentStatus() return &pb.GetProgressResponse{ Base: &pbCommon.BaseResponse{ Code: 200, Message: "ok", }, ActivityId: activity.ID, CurrentProgress: activity.CurrentProgress, TargetProgress: activity.TargetProgress, CurrentStage: currentStage, EndTime: activity.EndTime, Status: currentStatus, }, nil } // PurchaseItem 购买道具 func (s *activityService) PurchaseItem(ctx context.Context, req *pb.PurchaseItemRequest) (*pb.PurchaseItemResponse, error) { logger.Logger.Info("PurchaseItem request", zap.Int64("activity_id", req.ActivityId), zap.String("item_type", req.ItemType), zap.Int32("quantity", req.Quantity), zap.Int64("star_id", req.StarId), ) // 参数校验 if req.ActivityId <= 0 { return &pb.PurchaseItemResponse{ Base: &pbCommon.BaseResponse{ Code: 400, Message: "activity_id is required", }, }, nil } if req.ItemType == "" { return &pb.PurchaseItemResponse{ Base: &pbCommon.BaseResponse{ Code: 400, Message: "item_type is required", }, }, nil } if req.Quantity <= 0 { req.Quantity = 1 } // 获取用户ID(从context中获取,这里简化处理) userID := req.UserId // 获取活动 activity, err := s.activityRepo.GetActivityByID(req.ActivityId) if err != nil { logger.Logger.Error("GetActivity failed", zap.Error(err)) return &pb.PurchaseItemResponse{ Base: &pbCommon.BaseResponse{ Code: 500, Message: "获取活动失败: " + err.Error(), }, }, nil } if activity == nil { return &pb.PurchaseItemResponse{ Base: &pbCommon.BaseResponse{ Code: 404, Message: "活动不存在", }, }, nil } // 检查活动状态 currentStatus := activity.GetCurrentStatus() if currentStatus != "active" { var message string switch currentStatus { case "expired": message = "activity:expired" case "pending": message = "activity:pending" case "completed": message = "activity:completed" default: message = "activity:expired" } return &pb.PurchaseItemResponse{ Base: &pbCommon.BaseResponse{ Code: pbCommon.StatusCode_STATUS_OK, Message: message, }, }, nil } // 获取道具 item, err := s.activityRepo.GetActivityItemByType(req.ActivityId, req.ItemType) if err != nil { logger.Logger.Error("GetActivityItemByType failed", zap.Error(err)) return &pb.PurchaseItemResponse{ Base: &pbCommon.BaseResponse{ Code: 500, Message: "获取道具失败: " + err.Error(), }, }, nil } if item == nil { return &pb.PurchaseItemResponse{ Base: &pbCommon.BaseResponse{ Code: 404, Message: "道具不存在", }, }, nil } // 计算总消费 totalCost := int64(item.CrystalCost) * int64(req.Quantity) totalContribution := int64(item.ContributionPoints) * int64(req.Quantity) // 通过RPC获取用户当前水晶余额 profile, err := s.userRPCClient.GetFanProfile(userID, req.StarId) if err != nil { logger.Logger.Error("GetFanProfile failed", zap.Error(err)) return &pb.PurchaseItemResponse{ Base: &pbCommon.BaseResponse{ Code: 500, Message: "获取粉丝档案失败: " + err.Error(), }, }, nil } if profile == nil { return &pb.PurchaseItemResponse{ Base: &pbCommon.BaseResponse{ Code: 404, Message: "粉丝档案不存在", }, }, nil } // 检查水晶余额是否足够 if profile.CrystalBalance < totalCost { return &pb.PurchaseItemResponse{ Base: &pbCommon.BaseResponse{ Code: 400, Message: "水晶余额不足", }, }, nil } // 通过RPC扣减水晶 newBalance, err := s.userRPCClient.UpdateCrystalBalance(userID, req.StarId, -totalCost) if err != nil { logger.Logger.Error("UpdateCrystalBalance failed", zap.Error(err)) return &pb.PurchaseItemResponse{ Base: &pbCommon.BaseResponse{ Code: 500, Message: "扣减水晶失败: " + err.Error(), }, }, nil } // 更新活动进度 newProgress := activity.CurrentProgress + totalContribution if newProgress > activity.TargetProgress { newProgress = activity.TargetProgress } err = s.activityRepo.UpdateActivityProgress(req.ActivityId, newProgress) if err != nil { logger.Logger.Error("UpdateActivityProgress failed", zap.Error(err)) } // 创建贡献记录 now := time.Now().UnixMilli() contribution := &models.ActivityContribution{ ActivityID: req.ActivityId, UserID: userID, StarID: req.StarId, ItemID: item.ID, ItemType: item.ItemType, Quantity: int(req.Quantity), CrystalSpent: totalCost, ContributionPoints: totalContribution, CreatedAt: now, } err = s.activityRepo.CreateContribution(contribution) if err != nil { logger.Logger.Error("CreateContribution failed", zap.Error(err)) } // 更新用户统计 stats, _ := s.activityRepo.GetUserStats(req.ActivityId, userID, req.StarId) if stats == nil { stats = &models.ActivityUserStats{ ActivityID: req.ActivityId, UserID: userID, StarID: req.StarId, TotalContribution: 0, TotalCrystalSpent: 0, TotalItems: 0, LastContributeAt: now, CreatedAt: now, UpdatedAt: now, } } stats.TotalContribution += totalContribution stats.TotalCrystalSpent += totalCost stats.TotalItems += int(req.Quantity) stats.LastContributeAt = now stats.UpdatedAt = now err = s.activityRepo.UpdateUserStats(stats) if err != nil { logger.Logger.Error("UpdateUserStats failed", zap.Error(err)) } logger.Logger.Info("PurchaseItem success", zap.Int64("user_id", userID), zap.Int64("total_cost", totalCost), zap.Int64("total_contribution", totalContribution), zap.Int64("new_progress", newProgress), ) return &pb.PurchaseItemResponse{ Base: &pbCommon.BaseResponse{ Code: 200, Message: "ok", }, TotalCrystalSpent: totalCost, TotalContribution: totalContribution, CurrentProgress: newProgress, RemainingBalance: newBalance, }, nil } // GetContributionRanking 获取贡献点排名 func (s *activityService) GetContributionRanking(ctx context.Context, req *pb.ContributionRankingRequest) (*pb.ContributionRankingResponse, error) { logger.Logger.Info("GetContributionRanking request", zap.Int64("activity_id", req.ActivityId), zap.Int64("star_id", req.StarId), zap.Int32("page", req.Page), zap.Int32("page_size", req.PageSize), ) if req.ActivityId <= 0 { return &pb.ContributionRankingResponse{ Base: &pbCommon.BaseResponse{ Code: 400, Message: "activity_id is required", }, }, nil } if req.Page <= 0 { req.Page = 1 } if req.PageSize <= 0 { req.PageSize = 10 } // 获取排名列表 stats, total, err := s.activityRepo.GetRanking(req.ActivityId, req.StarId, int(req.Page), int(req.PageSize)) if err != nil { logger.Logger.Error("GetRanking failed", zap.Error(err)) return &pb.ContributionRankingResponse{ Base: &pbCommon.BaseResponse{ Code: 500, Message: "获取排名失败: " + err.Error(), }, }, nil } // 转换结果 items := make([]*pb.ContributionRankingItem, len(stats)) for i, stat := range stats { // 从 fan_profiles 获取用户昵称和头像 var nickname, avatarUrl string if req.StarId > 0 { fanProfile, err := s.userRPCClient.GetFanProfile(stat.UserID, req.StarId) if err == nil && fanProfile != nil { nickname = fanProfile.Nickname avatarUrl = fanProfile.AvatarUrl } } items[i] = &pb.ContributionRankingItem{ Rank: int32(i + 1 + (int(req.Page)-1)*int(req.PageSize)), UserId: stat.UserID, Nickname: nickname, AvatarUrl: avatarUrl, TotalContribution: stat.TotalContribution, TotalCrystalSpent: stat.TotalCrystalSpent, } } // 查询当前用户的贡献排名 var myContribution *pb.MyContribution if req.UserId > 0 && req.StarId > 0 { // 获取当前用户的头像和昵称 var nickname, avatarUrl string fanProfile, err := s.userRPCClient.GetFanProfile(req.UserId, req.StarId) if err == nil && fanProfile != nil { nickname = fanProfile.Nickname avatarUrl = fanProfile.AvatarUrl } myStat, err := s.activityRepo.GetUserStats(req.ActivityId, req.UserId, req.StarId) if err == nil && myStat != nil { // 计算当前用户的排名 rank, err := s.activityRepo.GetUserRank(req.UserId, req.ActivityId, req.StarId) if err == nil { myContribution = &pb.MyContribution{ Rank: int32(rank), TotalContribution: myStat.TotalContribution, TotalCrystalSpent: myStat.TotalCrystalSpent, Status: "ranked", Nickname: nickname, AvatarUrl: avatarUrl, } } } else { // 如果没有查到用户的贡献值,返回贡献值为0的对象 myContribution = &pb.MyContribution{ Rank: 0, TotalContribution: 0, TotalCrystalSpent: 0, Status: "unranked", Nickname: nickname, AvatarUrl: avatarUrl, } } } return &pb.ContributionRankingResponse{ Base: &pbCommon.BaseResponse{ Code: 200, Message: "ok", }, Items: items, MyContribution: myContribution, Page: req.Page, PageSize: req.PageSize, Total: int32(total), }, nil } // convertActivity 转换Activity模型到proto func (s *activityService) convertActivity(activity *models.Activity) *pb.Activity { items := make([]*pb.ActivityItem, len(activity.Items)) for i, item := range activity.Items { items[i] = &pb.ActivityItem{ Id: item.ID, ItemType: item.ItemType, ItemName: item.ItemName, IconUrl: item.IconURL, CrystalCost: int32(item.CrystalCost), ContributionPoints: int32(item.ContributionPoints), } } // 解析 stage_configs 获取图片信息 coverImage, bannerImage, stageBg, stageTitle := activity.GetStageImages() return &pb.Activity{ Id: activity.ID, ActivityType: activity.ActivityType, Title: activity.Title, Theme: activity.Theme, Description: activity.Description, StarId: activity.StarID, StartTime: activity.StartTime, EndTime: activity.EndTime, OverallEndTime: activity.OverallEndTime, TargetProgress: activity.TargetProgress, CurrentProgress: activity.CurrentProgress, Status: activity.GetCurrentStatus(), CurrentStage: activity.GetStage(), Items: items, CoverImage: coverImage, BannerImage: bannerImage, CurrentStageBackground: stageBg, CurrentStageTitle: stageTitle, Icon: activity.Icon, } } // GetMintingActivities 获取铸造活动列表(用于运营banner) func (s *activityService) GetMintingActivities(ctx context.Context, req *pb.GetMintingActivitiesRequest) (*pb.GetMintingActivitiesResponse, error) { logger.Logger.Info("GetMintingActivities request", zap.Int64("star_id", req.StarId), zap.Int32("page", req.Page), zap.Int32("page_size", req.PageSize), ) if req.Page <= 0 { req.Page = 1 } if req.PageSize <= 0 { req.PageSize = 10 } activities, total, err := s.mintingActivityRepo.GetActiveMintingActivities(req.StarId, int(req.Page), int(req.PageSize)) if err != nil { logger.Logger.Error("GetMintingActivities failed", zap.Error(err)) return &pb.GetMintingActivitiesResponse{ Base: &pbCommon.BaseResponse{ Code: 500, Message: "获取铸造活动列表失败: " + err.Error(), }, }, nil } // 转换结果 pbActivities := make([]*pb.MintingActivity, len(activities)) for i, activity := range activities { pbActivities[i] = &pb.MintingActivity{ Id: activity.ID, Title: activity.Title, Description: activity.Description, CoverImage: activity.CoverImage, StarId: activity.StarID, Route: activity.Route, IsActive: activity.IsActive, CreatedAt: activity.CreatedAt, UpdatedAt: activity.UpdatedAt, } } return &pb.GetMintingActivitiesResponse{ Base: &pbCommon.BaseResponse{ Code: 200, Message: "ok", }, Activities: pbActivities, Page: req.Page, PageSize: req.PageSize, Total: int32(total), }, nil }