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

614 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"
"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
}