topfans/backend/services/socialService/service/asset_like_service.go
2026-06-16 21:30:58 +08:00

808 lines
23 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"
"strconv"
"time"
"github.com/topfans/backend/pkg/database"
"github.com/topfans/backend/pkg/logger"
assetPb "github.com/topfans/backend/pkg/proto/asset"
eventPb "github.com/topfans/backend/pkg/proto/event"
notifPb "github.com/topfans/backend/pkg/proto/notification"
pbCommon "github.com/topfans/backend/pkg/proto/common"
pb "github.com/topfans/backend/pkg/proto/social"
"github.com/topfans/backend/pkg/statistic"
"github.com/topfans/backend/services/socialService/client"
"github.com/topfans/backend/services/socialService/repository"
"go.uber.org/zap"
"google.golang.org/grpc/codes"
"google.golang.org/protobuf/types/known/structpb"
)
// NotificationClientInterface 通知客户端抽象接口
// 用于在单元测试中注入 mock 实现;生产注入 NotificationClient
type NotificationClientInterface interface {
CreateNotification(ctx context.Context, req *notifPb.CreateNotificationRequest) (*notifPb.CreateNotificationResponse, error)
}
// AssetLikeService 资产点赞业务逻辑层
type AssetLikeService struct {
assetClient *client.AssetClient
socialRepo repository.SocialRepository
notificationClient NotificationClientInterface
}
// NewAssetLikeService 创建资产点赞Service实例
func NewAssetLikeService(assetClient *client.AssetClient, socialRepo repository.SocialRepository, notificationClient NotificationClientInterface) *AssetLikeService {
return &AssetLikeService{
assetClient: assetClient,
socialRepo: socialRepo,
notificationClient: notificationClient,
}
}
// 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 != uint32(codes.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 != uint32(codes.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)
// 事件埋点asset.likefire-and-forget不阻塞业务
statistic.Get().TrackEvent(context.Background(), &eventPb.Event{
EventType: "asset.like",
UserId: userID,
StarId: starID,
OccurredAt: time.Now().UnixMilli(),
Properties: map[string]string{
"asset_id": strconv.FormatInt(assetID, 10),
},
})
// 创建点赞通知(失败仅日志,不影响点赞主路径)
s.fireLikeNotification(ctx, getAssetResp, assetID, userID, starID)
return likeResp.LikeCount, nil
}
// fireLikeNotification 触发点赞通知fire-and-log
// 失败仅记 ERROR 日志,不向上抛错,保证点赞主路径不受影响。
func (s *AssetLikeService) fireLikeNotification(ctx context.Context, asset *assetPb.GetAssetForRPCResponse, assetID, userID, starID int64) {
if asset == nil {
logger.Logger.Warn("skip notification: nil asset",
zap.Int64("asset_id", assetID))
return
}
if asset.OwnerUid <= 0 {
logger.Logger.Warn("skip notification: invalid owner_uid",
zap.Int64("asset_id", assetID), zap.Int64("owner_uid", asset.OwnerUid))
return
}
if s.notificationClient == nil {
logger.Logger.Warn("skip notification: notificationClient not configured",
zap.Int64("asset_id", assetID))
return
}
// 构造通知 data含 actor info 直接从 asset 拿 title/cover
data := map[string]interface{}{
"target_type": "asset",
"target_id": assetID,
"actor_id": userID,
"asset_title": asset.Name,
"asset_cover": asset.CoverUrl,
"star_id": starID,
}
dataStruct, err := structpb.NewStruct(data)
if err != nil {
logger.Logger.Error("failed to build notification data struct",
zap.Int64("asset_id", assetID), zap.Error(err))
return
}
_, notifErr := s.notificationClient.CreateNotification(ctx, &notifPb.CreateNotificationRequest{
UserId: asset.OwnerUid,
StarId: starID,
Type: "like",
Title: "新点赞",
Content: fmt.Sprintf("用户 %d 点赞了你的藏品", userID),
Data: dataStruct,
})
if notifErr != nil {
logger.Logger.Error("failed to create like notification (like itself succeeded)",
zap.Int64("asset_id", assetID),
zap.Int64("actor_id", userID),
zap.Int64("owner_uid", asset.OwnerUid),
zap.Error(notifErr),
)
return
}
logger.Logger.Info("like notification created",
zap.Int64("asset_id", assetID), zap.Int64("owner_uid", asset.OwnerUid))
}
// 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 != uint32(codes.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 != uint32(codes.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 != uint32(codes.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: uint32(codes.Internal),
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: uint32(codes.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: uint32(codes.Internal),
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: uint32(codes.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: uint32(codes.Internal),
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: uint32(codes.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: uint32(codes.Internal),
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: uint32(codes.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: uint32(codes.Internal),
Message: "Failed to get asset",
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
if getAssetResp.Base.Code != uint32(codes.OK) {
logger.Logger.Warn("Asset not found",
zap.Int64("asset_id", assetID),
)
return &pb.GetAssetLikersResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(codes.NotFound),
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: uint32(codes.Internal),
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: uint32(codes.OK),
Message: "success",
Timestamp: time.Now().UnixMilli(),
},
Users: pbUsers,
Total: total,
HasMore: hasMore,
NextCursor: nextCursor,
}, nil
}