topfans/backend/services/assetService/repository/ranking_repository.go
2026-06-01 15:08:25 +08:00

286 lines
12 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 repository
import (
"errors"
"time"
"github.com/topfans/backend/pkg/models"
"gorm.io/gorm"
)
// RankingItem 排行榜项
type RankingItem struct {
AssetID int64
AssetName string
CoverURL string
OwnerUID int64
OwnerNickname string
OwnerAvatar *string
LikeCount int32
IsOriginal bool
ExhibitionID int64 // 当前展出记录ID未展出时为0
}
// RankingRepository 排行榜Repository接口
type RankingRepository interface {
// GetHotRankingDisplaying 获取热度排行榜-展示中
GetHotRankingDisplaying(starID int64, limit, offset int) ([]*RankingItem, int64, error)
// GetHotRankingMonth 获取热度排行榜-本月
GetHotRankingMonth(starID int64, limit, offset int) ([]*RankingItem, int64, error)
// GetHotRankingTotal 获取热度排行榜-全部
GetHotRankingTotal(starID int64, limit, offset int) ([]*RankingItem, int64, error)
// GetOriginalRankingDisplaying 获取自制排行榜-展示中
GetOriginalRankingDisplaying(starID int64, limit, offset int) ([]*RankingItem, int64, error)
// GetOriginalRankingMonth 获取自制排行榜-本月
GetOriginalRankingMonth(starID int64, limit, offset int) ([]*RankingItem, int64, error)
// GetOriginalRankingTotal 获取自制排行榜-全部
GetOriginalRankingTotal(starID int64, limit, offset int) ([]*RankingItem, int64, error)
// GetMyBestRanking 获取用户最佳的排名
GetMyBestRanking(userID, starID int64, dimension string, isOriginalOnly bool) (*RankingItem, int32, error)
}
// rankingRepository 排行榜Repository实现
type rankingRepository struct {
db *gorm.DB
}
// NewRankingRepository 创建排行榜Repository实例
func NewRankingRepository(db *gorm.DB) RankingRepository {
return &rankingRepository{
db: db,
}
}
// GetHotRankingDisplaying 获取热度排行榜-展示中
func (r *rankingRepository) GetHotRankingDisplaying(starID int64, limit, offset int) ([]*RankingItem, int64, error) {
return r.getHotRankingByDimension(starID, "displaying", false, limit, offset)
}
// GetHotRankingMonth 获取热度排行榜-本月
func (r *rankingRepository) GetHotRankingMonth(starID int64, limit, offset int) ([]*RankingItem, int64, error) {
return r.getHotRankingByDimension(starID, "month", false, limit, offset)
}
// GetHotRankingTotal 获取热度排行榜-全部
func (r *rankingRepository) GetHotRankingTotal(starID int64, limit, offset int) ([]*RankingItem, int64, error) {
return r.getHotRankingByDimension(starID, "total", false, limit, offset)
}
// GetOriginalRankingDisplaying 获取自制排行榜-展示中
func (r *rankingRepository) GetOriginalRankingDisplaying(starID int64, limit, offset int) ([]*RankingItem, int64, error) {
return r.getHotRankingByDimension(starID, "displaying", true, limit, offset)
}
// GetOriginalRankingMonth 获取自制排行榜-本月
func (r *rankingRepository) GetOriginalRankingMonth(starID int64, limit, offset int) ([]*RankingItem, int64, error) {
return r.getHotRankingByDimension(starID, "month", true, limit, offset)
}
// GetOriginalRankingTotal 获取自制排行榜-全部
func (r *rankingRepository) GetOriginalRankingTotal(starID int64, limit, offset int) ([]*RankingItem, int64, error) {
return r.getHotRankingByDimension(starID, "total", true, limit, offset)
}
// getHotRankingByDimension 根据维度获取排行榜
func (r *rankingRepository) getHotRankingByDimension(starID int64, dimension string, isOriginalOnly bool, limit, offset int) ([]*RankingItem, int64, error) {
if starID <= 0 {
return nil, 0, errors.New("star_id must be greater than 0")
}
if limit <= 0 {
limit = 10
}
now := time.Now().UnixMilli()
startOfMonth := time.Date(time.Now().Year(), time.Now().Month(), 1, 0, 0, 0, 0, time.Local).UnixMilli()
// 构建基础查询
// fan_profiles JOIN 条件按维度区分:
// - displaying/month: 使用 exhibitions.occupier_uid展位占用者必然是当前star的注册粉丝
// - total: 使用 assets.owner_uid无展览上下文时的全局拥有者
// 之所以不能统一用 assets.owner_uid是因为藏品拥有者不一定是当前star的粉丝。
db := r.db.Model(&models.Asset{}).
Select("assets.id as asset_id, assets.name as asset_name, assets.cover_url, assets.owner_uid, MAX(assets.like_count) as like_count, MAX(assets.is_original::int) as is_original, MAX(fp.nickname) as owner_nickname, MAX(fp.avatar_url) as owner_avatar").
Where("assets.star_id = ? AND assets.is_active = ? AND assets.status = ?", starID, true, models.AssetStatusActive)
if isOriginalOnly {
db = db.Where("assets.is_original = ?", true)
}
// 根据维度添加条件
switch dimension {
case "displaying":
// 展示中:关联 Exhibition 表筛选未过期的、未删除的且是当前star的展品
// occupier_star_id 表示展品所属的明星
// 用展品的 occupier展位占用者去 JOIN fan_profiles而不是 assets.owner_uid
db = db.Select("assets.id as asset_id, assets.name as asset_name, assets.cover_url, assets.owner_uid, MAX(assets.like_count) as like_count, MAX(assets.is_original::int) as is_original, MAX(fp.nickname) as owner_nickname, MAX(fp.avatar_url) as owner_avatar, MAX(exhibitions.id) as exhibition_id").
Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id AND exhibitions.deleted_at IS NULL").
Joins("LEFT JOIN fan_profiles fp ON fp.user_id = exhibitions.occupier_uid AND fp.star_id = exhibitions.occupier_star_id").
Where("exhibitions.expire_at > ?", now).
Where("exhibitions.occupier_star_id = ?", starID).
Group("assets.id, exhibitions.id")
case "month":
// 本月:本月内展览过的藏品(包括已下架的,只要 expire_at 在本月内即可)
db = db.Select("assets.id as asset_id, assets.name as asset_name, assets.cover_url, assets.owner_uid, MAX(assets.like_count) as like_count, MAX(assets.is_original::int) as is_original, MAX(fp.nickname) as owner_nickname, MAX(fp.avatar_url) as owner_avatar, MAX(exhibitions.id) as exhibition_id").
Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id").
Joins("LEFT JOIN fan_profiles fp ON fp.user_id = exhibitions.occupier_uid AND fp.star_id = exhibitions.occupier_star_id").
Where("exhibitions.expire_at >= ?", startOfMonth).
Where("exhibitions.occupier_star_id = ?", starID).
Group("assets.id, exhibitions.id")
case "total":
// 全部:直接使用 assets 表的 like_count无需额外条件
// fan_profiles JOIN 使用藏品的拥有者(无展览上下文)
db = db.Select("assets.id as asset_id, assets.name as asset_name, assets.cover_url, assets.owner_uid, MAX(assets.like_count) as like_count, MAX(assets.is_original::int) as is_original, MAX(fp.nickname) as owner_nickname, MAX(fp.avatar_url) as owner_avatar, 0 as exhibition_id").
Joins("LEFT JOIN fan_profiles fp ON fp.user_id = assets.owner_uid AND fp.star_id = ?", starID)
}
// 统计总数(先查询 ID 列表再 Count避免 DISTINCT 干扰)
var total int64
countDB := r.db.Model(&models.Asset{}).
Select("assets.id").
Where("assets.star_id = ? AND assets.is_active = ? AND assets.status = ?", starID, true, models.AssetStatusActive)
if isOriginalOnly {
countDB = countDB.Where("assets.is_original = ?", true)
}
switch dimension {
case "displaying":
countDB = countDB.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id AND exhibitions.deleted_at IS NULL").
Joins("LEFT JOIN fan_profiles fp ON fp.user_id = exhibitions.occupier_uid AND fp.star_id = exhibitions.occupier_star_id").
Where("exhibitions.expire_at > ?", now).
Where("exhibitions.occupier_star_id = ?", starID)
case "month":
// 本月:本月内展览过的藏品(包括已下架的)
countDB = countDB.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id").
Joins("LEFT JOIN fan_profiles fp ON fp.user_id = exhibitions.occupier_uid AND fp.star_id = exhibitions.occupier_star_id").
Where("exhibitions.expire_at >= ?", startOfMonth).
Where("exhibitions.occupier_star_id = ?", starID)
case "total":
countDB = countDB.Joins("LEFT JOIN fan_profiles fp ON fp.user_id = assets.owner_uid AND fp.star_id = ?", starID)
}
if err := countDB.Count(&total).Error; err != nil {
return nil, 0, err
}
// 查询列表(使用 GROUP BY 去重,按点赞数排序)
var results []*RankingItem
var err error
if dimension == "total" {
err = db.Group("assets.id").
Order("MAX(assets.like_count) DESC, assets.id ASC").
Limit(limit).
Offset(offset).
Scan(&results).Error
} else {
err = db.Group("assets.id, exhibitions.id").
Order("MAX(assets.like_count) DESC, assets.id ASC").
Limit(limit).
Offset(offset).
Scan(&results).Error
}
if err != nil {
return nil, 0, err
}
return results, total, nil
}
// GetMyBestRanking 获取用户最佳的排名
func (r *rankingRepository) GetMyBestRanking(userID, starID int64, dimension string, isOriginalOnly bool) (*RankingItem, int32, error) {
if userID <= 0 || starID <= 0 {
return nil, 0, errors.New("user_id and star_id must be greater than 0")
}
now := time.Now().UnixMilli()
startOfMonth := time.Date(time.Now().Year(), time.Now().Month(), 1, 0, 0, 0, 0, time.Local).UnixMilli()
// 构建基础查询,与 getHotRankingByDimension 保持一致
db := r.db.Model(&models.Asset{}).
Select("assets.id as asset_id, assets.name as asset_name, assets.cover_url, assets.owner_uid, assets.like_count, assets.is_original, fp.nickname as owner_nickname, fp.avatar_url as owner_avatar").
Joins("LEFT JOIN fan_profiles fp ON fp.user_id = assets.owner_uid AND fp.star_id = ?", starID).
Where("assets.owner_uid = ? AND assets.star_id = ? AND assets.is_active = ? AND assets.status = ?", userID, starID, true, models.AssetStatusActive)
if isOriginalOnly {
db = db.Where("assets.is_original = ?", true)
}
// 根据维度添加条件,与 getHotRankingByDimension 保持一致
switch dimension {
case "displaying":
// 展示中:关联 Exhibition 表筛选未过期的、未删除的且是当前star的展品
db = db.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id AND exhibitions.deleted_at IS NULL").
Where("exhibitions.expire_at > ?", now).
Where("exhibitions.occupier_star_id = ?", starID)
case "month":
// 本月:本月内展览过的藏品(包括已下架的)
db = db.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id").
Where("exhibitions.expire_at >= ?", startOfMonth).
Where("exhibitions.occupier_star_id = ?", starID)
}
// 获取用户在该star下点赞数最高的藏品
var result struct {
AssetID int64
AssetName string
CoverURL string
OwnerUID int64
LikeCount int32
IsOriginal bool
OwnerNickname string
OwnerAvatar *string
}
if err := db.Order("assets.like_count DESC, assets.id ASC").First(&result).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, 0, nil
}
return nil, 0, err
}
// 计算该藏品的排名(与 getHotRankingByDimension 的排序逻辑一致like_count DESC, id ASC
var rank int64
rankingDB := r.db.Model(&models.Asset{}).
Select("assets.id").
Joins("LEFT JOIN fan_profiles fp ON fp.user_id = assets.owner_uid AND fp.star_id = ?", starID).
Where("assets.star_id = ? AND assets.is_active = ? AND assets.status = ?", starID, true, models.AssetStatusActive).
Where("assets.like_count > ? OR (assets.like_count = ? AND assets.id < ?)", result.LikeCount, result.LikeCount, result.AssetID)
if isOriginalOnly {
rankingDB = rankingDB.Where("assets.is_original = ?", true)
}
switch dimension {
case "displaying":
rankingDB = rankingDB.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id AND exhibitions.deleted_at IS NULL").
Where("exhibitions.expire_at > ?", now).
Where("exhibitions.occupier_star_id = ?", starID)
case "month":
// 本月:本月内展览过的藏品(包括已下架的)
rankingDB = rankingDB.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id").
Where("exhibitions.expire_at >= ?", startOfMonth).
Where("exhibitions.occupier_star_id = ?", starID)
}
if err := rankingDB.Count(&rank).Error; err != nil {
return nil, 0, err
}
return &RankingItem{
AssetID: result.AssetID,
AssetName: result.AssetName,
CoverURL: result.CoverURL,
OwnerUID: result.OwnerUID,
LikeCount: result.LikeCount,
IsOriginal: result.IsOriginal,
OwnerNickname: result.OwnerNickname,
OwnerAvatar: result.OwnerAvatar,
}, int32(rank + 1), nil
}