feat: 新增点赞列表标签增加

This commit is contained in:
zerosaturation 2026-05-28 17:45:07 +08:00
parent aef5e4a35d
commit 9a9f4d1b96
6 changed files with 258 additions and 5 deletions

View File

@ -803,6 +803,7 @@ func (ctrl *SocialController) LikeAsset(c *gin.Context) {
response.Success(c, gin.H{ response.Success(c, gin.H{
"asset_id": resp.AssetId, "asset_id": resp.AssetId,
"new_like_count": resp.NewLikeCount, "new_like_count": resp.NewLikeCount,
"is_liked": resp.IsLiked,
}) })
} }
@ -879,6 +880,7 @@ func (ctrl *SocialController) UnlikeAsset(c *gin.Context) {
response.Success(c, gin.H{ response.Success(c, gin.H{
"asset_id": resp.AssetId, "asset_id": resp.AssetId,
"new_like_count": resp.NewLikeCount, "new_like_count": resp.NewLikeCount,
"is_liked": resp.IsLiked,
}) })
} }
@ -1020,6 +1022,7 @@ func (ctrl *SocialController) GetMyLikedAssets(c *gin.Context) {
"hourly_earnings": item.HourlyEarnings, "hourly_earnings": item.HourlyEarnings,
"is_lenticular": item.IsLenticular, "is_lenticular": item.IsLenticular,
"expire_at": item.ExpireAt, "expire_at": item.ExpireAt,
"status_text": item.StatusText,
}) })
} }

View File

@ -2566,6 +2566,7 @@ type LikedAssetItem struct {
HourlyEarnings float64 `protobuf:"fixed64,7,opt,name=hourly_earnings,json=hourlyEarnings,proto3" json:"hourly_earnings,omitempty"` // 每小时收益 HourlyEarnings float64 `protobuf:"fixed64,7,opt,name=hourly_earnings,json=hourlyEarnings,proto3" json:"hourly_earnings,omitempty"` // 每小时收益
IsLenticular bool `protobuf:"varint,8,opt,name=is_lenticular,json=isLenticular,proto3" json:"is_lenticular,omitempty"` // 是否为光栅卡 IsLenticular bool `protobuf:"varint,8,opt,name=is_lenticular,json=isLenticular,proto3" json:"is_lenticular,omitempty"` // 是否为光栅卡
ExpireAt int64 `protobuf:"varint,9,opt,name=expire_at,json=expireAt,proto3" json:"expire_at,omitempty"` // 展示结束时间(毫秒时间戳) ExpireAt int64 `protobuf:"varint,9,opt,name=expire_at,json=expireAt,proto3" json:"expire_at,omitempty"` // 展示结束时间(毫秒时间戳)
StatusText string `protobuf:"bytes,10,opt,name=status_text,json=statusText,proto3" json:"status_text,omitempty"` // 动态状态标签(默认「潜力待挖」)
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -2663,6 +2664,13 @@ func (x *LikedAssetItem) GetExpireAt() int64 {
return 0 return 0
} }
func (x *LikedAssetItem) GetStatusText() string {
if x != nil {
return x.StatusText
}
return ""
}
// 获取我今日点赞的作品列表请求(暂不实现) // 获取我今日点赞的作品列表请求(暂不实现)
type GetMyTodayLikedAssetsRequest struct { type GetMyTodayLikedAssetsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
@ -3185,7 +3193,7 @@ const file_social_proto_rawDesc = "" +
"\x04page\x18\x02 \x01(\x05R\x04page\x12\x1b\n" + "\x04page\x18\x02 \x01(\x05R\x04page\x12\x1b\n" +
"\tpage_size\x18\x03 \x01(\x05R\bpageSize\x12\x14\n" + "\tpage_size\x18\x03 \x01(\x05R\bpageSize\x12\x14\n" +
"\x05total\x18\x04 \x01(\x03R\x05total\x12\x19\n" + "\x05total\x18\x04 \x01(\x03R\x05total\x12\x19\n" +
"\bhas_more\x18\x05 \x01(\bR\ahasMore\"\x9d\x02\n" + "\bhas_more\x18\x05 \x01(\bR\ahasMore\"\xbe\x02\n" +
"\x0eLikedAssetItem\x12\x19\n" + "\x0eLikedAssetItem\x12\x19\n" +
"\basset_id\x18\x01 \x01(\x03R\aassetId\x12\x12\n" + "\basset_id\x18\x01 \x01(\x03R\aassetId\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12\x1b\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x1b\n" +
@ -3196,7 +3204,10 @@ const file_social_proto_rawDesc = "" +
"\bearnings\x18\x06 \x01(\x03R\bearnings\x12'\n" + "\bearnings\x18\x06 \x01(\x03R\bearnings\x12'\n" +
"\x0fhourly_earnings\x18\a \x01(\x01R\x0ehourlyEarnings\x12#\n" + "\x0fhourly_earnings\x18\a \x01(\x01R\x0ehourlyEarnings\x12#\n" +
"\ris_lenticular\x18\b \x01(\bR\fisLenticular\x12\x1b\n" + "\ris_lenticular\x18\b \x01(\bR\fisLenticular\x12\x1b\n" +
"\texpire_at\x18\t \x01(\x03R\bexpireAt\"O\n" + "\texpire_at\x18\t \x01(\x03R\bexpireAt\x12\x1f\n" +
"\vstatus_text\x18\n" +
" \x01(\tR\n" +
"statusText\"O\n" +
"\x1cGetMyTodayLikedAssetsRequest\x12\x12\n" + "\x1cGetMyTodayLikedAssetsRequest\x12\x12\n" +
"\x04page\x18\x01 \x01(\x05R\x04page\x12\x1b\n" + "\x04page\x18\x01 \x01(\x05R\x04page\x12\x1b\n" +
"\tpage_size\x18\x02 \x01(\x05R\bpageSize\"\x86\x01\n" + "\tpage_size\x18\x02 \x01(\x05R\bpageSize\"\x86\x01\n" +

View File

@ -320,6 +320,7 @@ message LikedAssetItem {
double hourly_earnings = 7; // double hourly_earnings = 7; //
bool is_lenticular = 8; // bool is_lenticular = 8; //
int64 expire_at = 9; // int64 expire_at = 9; //
string status_text = 10; //
} }
// //

View File

@ -137,6 +137,24 @@ type SocialRepository interface {
// 返回: 作品列表、总数量 // 返回: 作品列表、总数量
GetUserLikedAssets(userID, starID int64, page, pageSize int) ([]*LikedAssetInfo, int64, error) GetUserLikedAssets(userID, starID int64, page, pageSize int) ([]*LikedAssetInfo, int64, error)
// GetAssetRanks 批量获取资产的排行榜排名
// assetIDs: 资产ID列表
// starID: 明星ID
// 返回: map[assetID]rank
GetAssetRanks(assetIDs []int64, starID int64) (map[int64]int, error)
// GetHourlyNewLikes 批量获取过去1小时新增点赞数
// assetIDs: 资产ID列表
// 返回: map[assetID]hourlyNewLikes
GetHourlyNewLikes(assetIDs []int64) (map[int64]int32, error)
// GetLikesCountAfterUserLiked 批量获取用户点赞后新增点赞数
// userID: 用户ID
// assetIDs: 资产ID列表
// likedAtMap: map[assetID]userLikedAt 用户点赞时间
// 返回: map[assetID]新增点赞数
GetLikesCountAfterUserLiked(userID int64, assetIDs []int64, likedAtMap map[int64]int64) (map[int64]int32, error)
// ========== 藏品点赞用户相关 ========== // ========== 藏品点赞用户相关 ==========
// GetByAssetWithUsers 获取资产的点赞用户列表(带用户信息,游标分页) // GetByAssetWithUsers 获取资产的点赞用户列表(带用户信息,游标分页)
@ -169,6 +187,7 @@ type LikedAssetInfo struct {
HourlyEarnings float64 HourlyEarnings float64
IsLenticular bool // 是否为光栅卡 IsLenticular bool // 是否为光栅卡
ExpireAt int64 // 展出过期时间 ExpireAt int64 // 展出过期时间
StatusText string // 动态状态标签
} }
// RandomUserInfo 随机用户信息 // RandomUserInfo 随机用户信息
@ -870,6 +889,161 @@ func (r *socialRepositoryImpl) GetUserLikedAssets(userID, starID int64, page, pa
return items, total, nil return items, total, nil
} }
// GetAssetRanks 批量获取资产的排行榜排名
func (r *socialRepositoryImpl) GetAssetRanks(assetIDs []int64, starID int64) (map[int64]int, error) {
if len(assetIDs) == 0 {
return make(map[int64]int), nil
}
// 使用子查询获取每个资产的点赞数,然后按点赞数排名
type RankResult struct {
AssetID int64
Rank int
}
// 获取每个资产的点赞数
var assetLikes []struct {
AssetID int64 `gorm:"column:asset_id"`
Count int32 `gorm:"column:like_count"`
}
err := r.db.Model(&models.Asset{}).
Select("id as asset_id, like_count").
Where("id IN ? AND deleted_at IS NULL AND is_active = ?", assetIDs, true).
Find(&assetLikes).Error
if err != nil {
return nil, err
}
// 获取该明星下所有资产的点赞数用于排名
var totalAssets []struct {
AssetID int64 `gorm:"column:asset_id"`
Count int32 `gorm:"column:like_count"`
}
err = r.db.Model(&models.Asset{}).
Select("id as asset_id, like_count").
Where("deleted_at IS NULL AND is_active = ?", true).
Order("like_count DESC").
Find(&totalAssets).Error
if err != nil {
return nil, err
}
// 建立排名映射
rankMap := make(map[int64]int)
for i, asset := range totalAssets {
rankMap[asset.AssetID] = i + 1
}
// 返回目标资产的排名
result := make(map[int64]int)
for _, asset := range assetLikes {
if rank, ok := rankMap[asset.AssetID]; ok {
result[asset.AssetID] = rank
} else {
result[asset.AssetID] = 0
}
}
return result, nil
}
// GetHourlyNewLikes 批量获取过去1小时新增点赞数
func (r *socialRepositoryImpl) GetHourlyNewLikes(assetIDs []int64) (map[int64]int32, error) {
if len(assetIDs) == 0 {
return make(map[int64]int32), nil
}
oneHourAgo := time.Now().Add(-1 * time.Hour).UnixMilli()
type LikeCount struct {
AssetID int64 `gorm:"column:asset_id"`
Count int32 `gorm:"column:new_likes"`
}
var results []LikeCount
err := r.db.Model(&models.AssetLike{}).
Select("asset_id, COUNT(*) as new_likes").
Where("asset_id IN ? AND created_at >= ?", assetIDs, oneHourAgo).
Group("asset_id").
Find(&results).Error
if err != nil {
return nil, err
}
result := make(map[int64]int32)
for _, r := range results {
result[r.AssetID] = r.Count
}
// 未找到记录默认为0
for _, assetID := range assetIDs {
if _, ok := result[assetID]; !ok {
result[assetID] = 0
}
}
return result, nil
}
// GetLikesCountAfterUserLiked 批量获取用户点赞后新增点赞数
func (r *socialRepositoryImpl) GetLikesCountAfterUserLiked(userID int64, assetIDs []int64, likedAtMap map[int64]int64) (map[int64]int32, error) {
if len(assetIDs) == 0 {
return make(map[int64]int32), nil
}
type LikeCount struct {
AssetID int64 `gorm:"column:asset_id"`
Count int32 `gorm:"column:new_likes"`
}
var results []LikeCount
err := r.db.Model(&models.AssetLike{}).
Select("asset_id, COUNT(*) as new_likes").
Where("asset_id IN ?", assetIDs).
Group("asset_id").
Find(&results).Error
if err != nil {
return nil, err
}
// 获取用户点赞前的总点赞数
type TotalCount struct {
AssetID int64 `gorm:"column:asset_id"`
Count int32 `gorm:"column:total_likes"`
}
var totalResults []TotalCount
err = r.db.Model(&models.AssetLike{}).
Select("asset_id, COUNT(*) as total_likes").
Where("asset_id IN ?", assetIDs).
Group("asset_id").
Find(&totalResults).Error
if err != nil {
return nil, err
}
totalMap := make(map[int64]int32)
for _, t := range totalResults {
totalMap[t.AssetID] = t.Count
}
// 用户点赞前的总点赞数需要用用户点赞时间来计算
// 简化:获取用户在每个资产上的点赞时间,然后统计该时间点之后的点赞数
result := make(map[int64]int32)
for _, assetID := range assetIDs {
userLikedAt, ok := likedAtMap[assetID]
if !ok {
result[assetID] = 0
continue
}
var count int64
r.db.Model(&models.AssetLike{}).
Where("asset_id = ? AND created_at > ?", assetID, userLikedAt).
Count(&count)
result[assetID] = int32(count)
}
return result, nil
}
// ========== 藏品点赞用户相关实现 ========== // ========== 藏品点赞用户相关实现 ==========
// GetByAssetWithUsers 获取资产的点赞用户列表(带用户信息,游标分页) // GetByAssetWithUsers 获取资产的点赞用户列表(带用户信息,游标分页)

View File

@ -281,9 +281,33 @@ func (s *AssetLikeService) GetMyLikedAssets(ctx context.Context, req *pb.GetMyLi
}, nil }, nil
} }
// 转换为 proto 类型 // 提取 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)) pbItems := make([]*pb.LikedAssetItem, 0, len(items))
for _, item := range 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{ pbItems = append(pbItems, &pb.LikedAssetItem{
AssetId: item.AssetID, AssetId: item.AssetID,
Name: item.Name, Name: item.Name,
@ -294,6 +318,7 @@ func (s *AssetLikeService) GetMyLikedAssets(ctx context.Context, req *pb.GetMyLi
HourlyEarnings: item.HourlyEarnings, HourlyEarnings: item.HourlyEarnings,
IsLenticular: item.IsLenticular, IsLenticular: item.IsLenticular,
ExpireAt: item.ExpireAt, ExpireAt: item.ExpireAt,
StatusText: statusText,
}) })
} }
@ -315,6 +340,46 @@ func (s *AssetLikeService) GetMyLikedAssets(ctx context.Context, req *pb.GetMyLi
}, nil }, 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 获取我今日点赞的作品列表 // GetMyTodayLikedAssets 获取我今日点赞的作品列表
func (s *AssetLikeService) GetMyTodayLikedAssets(ctx context.Context, req *pb.GetMyTodayLikedAssetsRequest, userID, starID int64) (*pb.GetMyTodayLikedAssetsResponse, error) { func (s *AssetLikeService) GetMyTodayLikedAssets(ctx context.Context, req *pb.GetMyTodayLikedAssetsRequest, userID, starID int64) (*pb.GetMyTodayLikedAssetsResponse, error) {
page := req.Page page := req.Page

View File

@ -912,8 +912,7 @@ const loadLikedAssets = async () => {
expire_at: item.expire_at, expire_at: item.expire_at,
name: item.name, name: item.name,
is_lenticular: item.is_lenticular ?? false, is_lenticular: item.is_lenticular ?? false,
// status_text: item.status_text || '潜力待挖',
status_text: index < 3 ? '排名进榜' : '潜力待挖',
score: item.like_count, score: item.like_count,
reward: Math.floor(item.earnings || 0), reward: Math.floor(item.earnings || 0),
})); }));