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 }