614 lines
18 KiB
Go
614 lines
18 KiB
Go
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
|
||
} |