topfans/backend/services/socialService/service/asset_like_service.go
2026-05-28 17:45:07 +08:00

725 lines
20 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) (int32, 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 0, fmt.Errorf("获取藏品信息失败,请稍后重试")
}
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 0, fmt.Errorf("藏品不存在")
}
// 2. 检查是否已点赞
checkLikeReq := &assetPb.CheckAssetLikeRequest{
AssetId: assetID,
UserId: userID,
StarId: starID,
}
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 0, fmt.Errorf("点赞失败,请稍后重试")
}
if checkLikeResp.IsLiked {
logger.Logger.Warn("Already liked asset",
zap.Int64("asset_id", assetID),
zap.Int64("user_id", userID),
)
return 0, fmt.Errorf("当前展示已点赞")
}
// 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 0, fmt.Errorf("点赞失败,请稍后重试")
}
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 0, fmt.Errorf("点赞失败: %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),
zap.Int32("new_like_count", likeResp.LikeCount),
)
// 缓存失效
_ = database.InvalidateAssetLikersCache(ctx, assetID)
return likeResp.LikeCount, 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,
UserId: userID,
StarId: starID,
}
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("取消点赞失败,请稍后重试")
}
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("取消点赞失败,请稍后重试")
}
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("取消点赞失败: %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,
UserId: userID,
StarId: starID,
}
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("取消点赞失败,请稍后重试")
}
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("查询点赞状态失败: %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("获取点赞列表失败,请稍后重试")
}
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("获取点赞列表失败: %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
}
// 提取 assetIDs 用于批量查询
assetIDs := make([]int64, 0, len(items))
likedAtMap := make(map[int64]int64)
for _, item := range items {
assetIDs = append(assetIDs, item.AssetID)
likedAtMap[item.AssetID] = item.LikedAt
}
// 批量获取排名
rankMap, _ := s.socialRepo.GetAssetRanks(assetIDs, starID)
// 批量获取过去1小时新增点赞
hourlyNewLikesMap, _ := s.socialRepo.GetHourlyNewLikes(assetIDs)
// 批量获取用户点赞后新增点赞数
userLikedCountAfterMap, _ := s.socialRepo.GetLikesCountAfterUserLiked(userID, assetIDs, likedAtMap)
// 计算 status_text 并转换为 proto 类型
pbItems := make([]*pb.LikedAssetItem, 0, len(items))
for _, item := range items {
statusText := computeStatusText(&statusTextInput{
UserLikedCountAfter: userLikedCountAfterMap[item.AssetID],
Rank: rankMap[item.AssetID],
HourlyNewLikes: hourlyNewLikesMap[item.AssetID],
IsExpired: item.ExpireAt > 0 && item.ExpireAt < time.Now().UnixMilli(),
})
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,
StatusText: statusText,
})
}
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
}
// statusTextInput status_text 计算输入
type statusTextInput struct {
UserLikedCountAfter int32 // 用户点赞后新增点赞数
Rank int // 排行榜排名
HourlyNewLikes int32 // 过去1小时新增点赞数
IsExpired bool // 是否已过期
}
// computeStatusText 计算 status_text 状态标签
func computeStatusText(item *statusTextInput) string {
// T0: 眼光拉满
if item.UserLikedCountAfter >= 50 && !item.IsExpired {
return "眼光拉满"
}
// T1: 排名型
switch {
case item.Rank >= 1 && item.Rank <= 5:
return fmt.Sprintf("屠榜顶流Top%d", item.Rank)
case item.Rank > 5 && item.Rank <= 10:
return fmt.Sprintf("第%d爆款", item.Rank)
case item.Rank == 20 || item.Rank == 50 || item.Rank == 100 || item.Rank == 200:
return fmt.Sprintf("排名破%d", item.Rank)
}
// T3/T4: 状态型
switch {
case item.HourlyNewLikes >= 20:
return "火速破圈"
case item.HourlyNewLikes >= 10:
return "小爆出圈"
case item.HourlyNewLikes >= 5:
return "热度积累"
case item.HourlyNewLikes >= 0:
return "缓慢涨粉"
default:
return "潜力待挖"
}
}
// 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
}