topfans/backend/services/socialService/service/asset_like_service.go
2026-05-22 18:06:33 +08:00

653 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"
"time"
"github.com/topfans/backend/pkg/database"
"github.com/topfans/backend/pkg/logger"
assetPb "github.com/topfans/backend/pkg/proto/asset"
pbCommon "github.com/topfans/backend/pkg/proto/common"
pb "github.com/topfans/backend/pkg/proto/social"
"github.com/topfans/backend/services/socialService/client"
"github.com/topfans/backend/services/socialService/repository"
"go.uber.org/zap"
)
// AssetLikeService 资产点赞业务逻辑层
type AssetLikeService struct {
assetClient *client.AssetClient
socialRepo repository.SocialRepository
}
// NewAssetLikeService 创建资产点赞Service实例
func NewAssetLikeService(assetClient *client.AssetClient, socialRepo repository.SocialRepository) *AssetLikeService {
return &AssetLikeService{
assetClient: assetClient,
socialRepo: socialRepo,
}
}
// LikeAsset 点赞资产
func (s *AssetLikeService) LikeAsset(ctx context.Context, assetID, userID, starID int64) error {
logger.Logger.Debug("AssetLikeService.LikeAsset called",
zap.Int64("asset_id", assetID),
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
)
// 1. 验证资产是否存在使用内部RPC不验证所有权
getAssetReq := &assetPb.GetAssetForRPCRequest{
AssetId: assetID,
}
getAssetResp, err := s.assetClient.GetAssetForRPC(ctx, getAssetReq)
if err != nil {
logger.Logger.Error("Failed to get asset for RPC",
zap.Error(err),
zap.Int64("asset_id", assetID),
)
return fmt.Errorf("failed to get asset: %w", err)
}
if getAssetResp.Base.Code != pbCommon.StatusCode_STATUS_OK {
logger.Logger.Warn("Asset not found or error",
zap.Int64("asset_id", assetID),
zap.Int32("code", int32(getAssetResp.Base.Code)),
zap.String("message", getAssetResp.Base.Message),
)
return fmt.Errorf("asset not found: %s", getAssetResp.Base.Message)
}
// 2. 检查是否已点赞
checkLikeReq := &assetPb.CheckAssetLikeRequest{
AssetId: assetID,
}
checkLikeResp, err := s.assetClient.CheckAssetLike(ctx, checkLikeReq)
if err != nil {
logger.Logger.Error("Failed to check asset like",
zap.Error(err),
zap.Int64("asset_id", assetID),
)
return fmt.Errorf("failed to check asset like: %w", err)
}
if checkLikeResp.IsLiked {
logger.Logger.Warn("Already liked asset",
zap.Int64("asset_id", assetID),
zap.Int64("user_id", userID),
)
return fmt.Errorf("already liked this asset")
}
// 3. 调用 Asset Service 点赞接口
likeReq := &assetPb.LikeAssetRequest{
AssetId: assetID,
}
likeResp, err := s.assetClient.LikeAsset(ctx, likeReq)
if err != nil {
logger.Logger.Error("Failed to like asset",
zap.Error(err),
zap.Int64("asset_id", assetID),
)
return fmt.Errorf("failed to like asset: %w", err)
}
if likeResp.Base.Code != pbCommon.StatusCode_STATUS_OK {
logger.Logger.Warn("Like asset failed",
zap.Int64("asset_id", assetID),
zap.Int32("code", int32(likeResp.Base.Code)),
zap.String("message", likeResp.Base.Message),
)
return fmt.Errorf("like asset failed: %s", likeResp.Base.Message)
}
logger.Logger.Info("Successfully liked asset",
zap.Int64("asset_id", assetID),
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
)
// 缓存失效
_ = database.InvalidateAssetLikersCache(ctx, assetID)
return nil
}
// UnlikeAsset 取消点赞资产
func (s *AssetLikeService) UnlikeAsset(ctx context.Context, assetID, userID, starID int64) error {
logger.Logger.Debug("AssetLikeService.UnlikeAsset called",
zap.Int64("asset_id", assetID),
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
)
// 1. 检查是否已点赞
checkLikeReq := &assetPb.CheckAssetLikeRequest{
AssetId: assetID,
}
checkLikeResp, err := s.assetClient.CheckAssetLike(ctx, checkLikeReq)
if err != nil {
logger.Logger.Error("Failed to check asset like",
zap.Error(err),
zap.Int64("asset_id", assetID),
)
return fmt.Errorf("failed to check asset like: %w", err)
}
if !checkLikeResp.IsLiked {
logger.Logger.Warn("Not liked asset yet",
zap.Int64("asset_id", assetID),
zap.Int64("user_id", userID),
)
return fmt.Errorf("not liked this asset yet")
}
// 2. 调用 Asset Service 取消点赞接口
unlikeReq := &assetPb.UnlikeAssetRequest{
AssetId: assetID,
}
unlikeResp, err := s.assetClient.UnlikeAsset(ctx, unlikeReq)
if err != nil {
logger.Logger.Error("Failed to unlike asset",
zap.Error(err),
zap.Int64("asset_id", assetID),
)
return fmt.Errorf("failed to unlike asset: %w", err)
}
if unlikeResp.Base.Code != pbCommon.StatusCode_STATUS_OK {
logger.Logger.Warn("Unlike asset failed",
zap.Int64("asset_id", assetID),
zap.Int32("code", int32(unlikeResp.Base.Code)),
zap.String("message", unlikeResp.Base.Message),
)
return fmt.Errorf("unlike asset failed: %s", unlikeResp.Base.Message)
}
logger.Logger.Info("Successfully unliked asset",
zap.Int64("asset_id", assetID),
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
)
// 缓存失效
_ = database.InvalidateAssetLikersCache(ctx, assetID)
return nil
}
// CheckAssetLike 检查是否已点赞资产
func (s *AssetLikeService) CheckAssetLike(ctx context.Context, assetID, userID, starID int64) (bool, error) {
logger.Logger.Debug("AssetLikeService.CheckAssetLike called",
zap.Int64("asset_id", assetID),
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
)
checkLikeReq := &assetPb.CheckAssetLikeRequest{
AssetId: assetID,
}
checkLikeResp, err := s.assetClient.CheckAssetLike(ctx, checkLikeReq)
if err != nil {
logger.Logger.Error("Failed to check asset like",
zap.Error(err),
zap.Int64("asset_id", assetID),
)
return false, fmt.Errorf("failed to check asset like: %w", err)
}
if checkLikeResp.Base.Code != pbCommon.StatusCode_STATUS_OK {
logger.Logger.Warn("Check asset like failed",
zap.Int64("asset_id", assetID),
zap.Int32("code", int32(checkLikeResp.Base.Code)),
zap.String("message", checkLikeResp.Base.Message),
)
return false, fmt.Errorf("check asset like failed: %s", checkLikeResp.Base.Message)
}
return checkLikeResp.IsLiked, nil
}
// GetAssetLikes 获取资产点赞列表
func (s *AssetLikeService) GetAssetLikes(ctx context.Context, assetID int64, page, pageSize int32) (*assetPb.GetAssetLikesResponse, error) {
logger.Logger.Debug("AssetLikeService.GetAssetLikes called",
zap.Int64("asset_id", assetID),
zap.Int32("page", page),
zap.Int32("page_size", pageSize),
)
getLikesReq := &assetPb.GetAssetLikesRequest{
AssetId: assetID,
Page: page,
PageSize: pageSize,
}
getLikesResp, err := s.assetClient.GetAssetLikes(ctx, getLikesReq)
if err != nil {
logger.Logger.Error("Failed to get asset likes",
zap.Error(err),
zap.Int64("asset_id", assetID),
)
return nil, fmt.Errorf("failed to get asset likes: %w", err)
}
if getLikesResp.Base.Code != pbCommon.StatusCode_STATUS_OK {
logger.Logger.Warn("Get asset likes failed",
zap.Int64("asset_id", assetID),
zap.Int32("code", int32(getLikesResp.Base.Code)),
zap.String("message", getLikesResp.Base.Message),
)
return nil, fmt.Errorf("get asset likes failed: %s", getLikesResp.Base.Message)
}
return getLikesResp, nil
}
// GetMyLikedAssets 获取我点赞的作品列表
func (s *AssetLikeService) GetMyLikedAssets(ctx context.Context, req *pb.GetMyLikedAssetsRequest, userID, starID int64) (*pb.GetMyLikedAssetsResponse, error) {
page := req.Page
if page <= 0 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
if pageSize > 100 {
pageSize = 100
}
items, total, err := s.socialRepo.GetMyLikedAssets(userID, starID, int(page), int(pageSize), req.OrderBy)
if err != nil {
logger.Logger.Error("Failed to get my liked assets",
zap.Error(err),
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
)
return &pb.GetMyLikedAssetsResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_INTERNAL_ERROR,
Message: "Failed to get my liked assets: " + err.Error(),
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
// 转换为 proto 类型
pbItems := make([]*pb.LikedAssetItem, 0, len(items))
for _, item := range items {
pbItems = append(pbItems, &pb.LikedAssetItem{
AssetId: item.AssetID,
Name: item.Name,
CoverUrl: item.CoverURL,
LikeCount: item.LikeCount,
LikedAt: item.LikedAt,
Earnings: item.Earnings,
HourlyEarnings: item.HourlyEarnings,
IsLenticular: item.IsLenticular,
ExpireAt: item.ExpireAt,
})
}
hasMore := int64(page)*int64(pageSize) < total
return &pb.GetMyLikedAssetsResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "success",
Timestamp: time.Now().UnixMilli(),
},
Data: &pb.LikedAssetsData{
Items: pbItems,
Page: page,
PageSize: pageSize,
Total: total,
HasMore: hasMore,
},
}, nil
}
// GetMyTodayLikedAssets 获取我今日点赞的作品列表
func (s *AssetLikeService) GetMyTodayLikedAssets(ctx context.Context, req *pb.GetMyTodayLikedAssetsRequest, userID, starID int64) (*pb.GetMyTodayLikedAssetsResponse, error) {
page := req.Page
if page <= 0 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
if pageSize > 100 {
pageSize = 100
}
items, total, err := s.socialRepo.GetMyTodayLikedAssets(userID, starID, int(page), int(pageSize))
if err != nil {
logger.Logger.Error("Failed to get my today liked assets",
zap.Error(err),
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
)
return &pb.GetMyTodayLikedAssetsResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_INTERNAL_ERROR,
Message: "Failed to get my today liked assets: " + err.Error(),
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
pbItems := make([]*pb.LikedAssetItem, 0, len(items))
for _, item := range items {
pbItems = append(pbItems, &pb.LikedAssetItem{
AssetId: item.AssetID,
Name: item.Name,
CoverUrl: item.CoverURL,
LikeCount: item.LikeCount,
LikedAt: item.LikedAt,
Earnings: item.Earnings,
HourlyEarnings: item.HourlyEarnings,
})
}
hasMore := int64(page)*int64(pageSize) < total
return &pb.GetMyTodayLikedAssetsResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "success",
Timestamp: time.Now().UnixMilli(),
},
Data: &pb.LikedAssetsData{
Items: pbItems,
Page: page,
PageSize: pageSize,
Total: total,
HasMore: hasMore,
},
}, nil
}
// GetMyWeekLikedAssets 获取我本周点赞的作品列表
func (s *AssetLikeService) GetMyWeekLikedAssets(ctx context.Context, req *pb.GetMyWeekLikedAssetsRequest, userID, starID int64) (*pb.GetMyWeekLikedAssetsResponse, error) {
page := req.Page
if page <= 0 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
if pageSize > 100 {
pageSize = 100
}
items, total, err := s.socialRepo.GetMyWeekLikedAssets(userID, starID, int(page), int(pageSize))
if err != nil {
logger.Logger.Error("Failed to get my week liked assets",
zap.Error(err),
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
)
return &pb.GetMyWeekLikedAssetsResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_INTERNAL_ERROR,
Message: "Failed to get my week liked assets: " + err.Error(),
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
pbItems := make([]*pb.LikedAssetItem, 0, len(items))
for _, item := range items {
pbItems = append(pbItems, &pb.LikedAssetItem{
AssetId: item.AssetID,
Name: item.Name,
CoverUrl: item.CoverURL,
LikeCount: item.LikeCount,
LikedAt: item.LikedAt,
Earnings: item.Earnings,
HourlyEarnings: item.HourlyEarnings,
})
}
hasMore := int64(page)*int64(pageSize) < total
return &pb.GetMyWeekLikedAssetsResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "success",
Timestamp: time.Now().UnixMilli(),
},
Data: &pb.LikedAssetsData{
Items: pbItems,
Page: page,
PageSize: pageSize,
Total: total,
HasMore: hasMore,
},
}, nil
}
// GetUserLikedAssets 获取他人点赞的作品列表
func (s *AssetLikeService) GetUserLikedAssets(ctx context.Context, req *pb.GetUserLikedAssetsRequest, userID, starID int64) (*pb.GetUserLikedAssetsResponse, error) {
page := req.Page
if page <= 0 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
if pageSize > 100 {
pageSize = 100
}
items, total, err := s.socialRepo.GetUserLikedAssets(userID, starID, int(page), int(pageSize))
if err != nil {
logger.Logger.Error("Failed to get user liked assets",
zap.Error(err),
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
)
return &pb.GetUserLikedAssetsResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_INTERNAL_ERROR,
Message: "Failed to get user liked assets: " + err.Error(),
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
pbItems := make([]*pb.LikedAssetItem, 0, len(items))
for _, item := range items {
pbItems = append(pbItems, &pb.LikedAssetItem{
AssetId: item.AssetID,
Name: item.Name,
CoverUrl: item.CoverURL,
LikeCount: item.LikeCount,
LikedAt: item.LikedAt,
Earnings: item.Earnings,
HourlyEarnings: item.HourlyEarnings,
})
}
hasMore := int64(page)*int64(pageSize) < total
return &pb.GetUserLikedAssetsResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "success",
Timestamp: time.Now().UnixMilli(),
},
Data: &pb.LikedAssetsData{
Items: pbItems,
Page: page,
PageSize: pageSize,
Total: total,
HasMore: hasMore,
},
}, nil
}
// GetAssetLikers 获取资产点赞用户列表(带缓存)
func (s *AssetLikeService) GetAssetLikers(ctx context.Context, assetID, starID int64, cursor int64, pageSize int32) (*pb.GetAssetLikersResponse, error) {
logger.Logger.Debug("AssetLikeService.GetAssetLikers called",
zap.Int64("asset_id", assetID),
zap.Int64("star_id", starID),
zap.Int64("cursor", cursor),
zap.Int32("page_size", pageSize),
)
// 参数校验
if pageSize <= 0 {
pageSize = 20
}
if pageSize > 100 {
pageSize = 100
}
// 0. 校验资产是否存在
getAssetReq := &assetPb.GetAssetForRPCRequest{AssetId: assetID}
getAssetResp, err := s.assetClient.GetAssetForRPC(ctx, getAssetReq)
if err != nil {
logger.Logger.Error("Failed to get asset for RPC",
zap.Error(err),
zap.Int64("asset_id", assetID),
)
return &pb.GetAssetLikersResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_INTERNAL_ERROR,
Message: "Failed to get asset",
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
if getAssetResp.Base.Code != pbCommon.StatusCode_STATUS_OK {
logger.Logger.Warn("Asset not found",
zap.Int64("asset_id", assetID),
)
return &pb.GetAssetLikersResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_NOT_FOUND,
Message: "Asset not found",
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
// 1. 先查缓存
cache, err := database.GetAssetLikersCache(ctx, assetID)
if err != nil {
logger.Logger.Warn("Failed to get asset likers cache",
zap.Error(err),
zap.Int64("asset_id", assetID),
)
// 缓存错误不影响主流程,继续查 DB
}
if cache != nil && len(cache.Users) > 0 {
// 缓存命中,从缓存中切片返回
return sliceFromCache(cache, cursor, pageSize)
}
// 2. 缓存未命中,查 DB
// 从已获取的资产信息中获取 starID
actualStarID := getAssetResp.StarId
if actualStarID == 0 {
actualStarID = starID // 兜底使用传入的 starID
}
// 查询数据库,最多缓存 1000 条
dbResults, err := s.socialRepo.GetByAssetWithUsers(assetID, actualStarID, 0, 1000)
if err != nil {
logger.Logger.Error("Failed to get asset likers from DB",
zap.Error(err),
zap.Int64("asset_id", assetID),
)
return &pb.GetAssetLikersResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_INTERNAL_ERROR,
Message: "Failed to get asset likers",
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
// 3. 写入缓存
total := int64(len(dbResults))
cacheUsers := make([]database.AssetLikerWithTotal, 0, len(dbResults))
for _, r := range dbResults {
cacheUsers = append(cacheUsers, database.AssetLikerWithTotal{
UserID: r.UserID,
Nickname: r.Nickname,
Avatar: r.Avatar,
FanLevel: r.FanLevel,
LikedAt: r.LikedAt,
StarID: r.StarID,
})
}
cache = &database.AssetLikersCache{
Users: cacheUsers,
Total: total,
UpdatedAt: time.Now().UnixMilli(),
}
_ = database.SetAssetLikersCache(ctx, assetID, cache, 60*time.Second)
// 4. 返回数据
return sliceFromCache(cache, cursor, pageSize)
}
// sliceFromCache 从缓存中按游标切片
func sliceFromCache(cache *database.AssetLikersCache, cursor int64, pageSize int32) (*pb.GetAssetLikersResponse, error) {
users := cache.Users
total := cache.Total
// 找到起始位置
start := 0
if cursor > 0 {
for i, u := range users {
if u.LikedAt < cursor {
start = i
break
}
}
}
// 找到结束位置
end := start + int(pageSize)
hasMore := end < len(users)
if end > len(users) {
end = len(users)
}
// 构建返回结果
pbUsers := make([]*pb.AssetLiker, 0, end-start)
var nextCursor int64
for i := start; i < end; i++ {
u := users[i]
pbUsers = append(pbUsers, &pb.AssetLiker{
UserId: u.UserID,
Nickname: u.Nickname,
Avatar: u.Avatar,
FanLevel: u.FanLevel,
LikedAt: u.LikedAt,
})
nextCursor = u.LikedAt
}
return &pb.GetAssetLikersResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "success",
Timestamp: time.Now().UnixMilli(),
},
Users: pbUsers,
Total: total,
HasMore: hasMore,
NextCursor: nextCursor,
}, nil
}