topfans/backend/services/galleryService/repository/gallery_repository.go
2026-05-07 14:07:36 +08:00

428 lines
14 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"
"github.com/topfans/backend/services/galleryService/config"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// GalleryRepository 展馆数据访问层接口
type GalleryRepository interface {
// 展位相关
GetSlotsByUser(userID, starID int64) ([]*models.BoothSlot, error)
GetSlotByID(slotID int64) (*models.BoothSlot, error)
GetSlotCount(userID, starID int64) (int64, error)
CreateInitialSlots(userID, starID, hostProfileID int64) error
CreateSlot(slot *models.BoothSlot) error
UnlockSlot(slotID int64) error
// 展品相关
GetExhibitionByAsset(assetID int64) (*models.Exhibition, error)
GetExhibitionBySlot(slotID int64) (*models.Exhibition, error)
GetExhibitionsByUser(userID, starID int64) ([]*models.Exhibition, error)
CreateExhibition(exhibition *models.Exhibition) error
DeleteExhibition(exhibitionID int64) error
DeleteExhibitionByAsset(assetID int64) error
GetExpiredExhibitions(beforeTime int64) ([]*models.Exhibition, error)
// 资产注册表相关
UpdateAssetRegistryDisplayStatus(assetID int64, displayStatus int32) error
// ========== 我的作品相关 ==========
// GetMyExhibitedAssets 获取我展出的作品列表(只返回展出中且未过期的,含收益)
// userID: 用户ID
// starID: 明星ID
// page: 页码从1开始
// pageSize: 每页数量
// 返回: 作品列表、总数量
GetMyExhibitedAssets(userID, starID int64, page, pageSize int) ([]*ExhibitedAssetInfo, int64, error)
// ========== 灵感瀑布相关 ==========
// CountValidExhibitions 统计有效展品数量
// starID: 明星ID
// materialType: 素材类型过滤(空字符串表示不过滤)
CountValidExhibitions(starID int64, materialType string) (int64, error)
// GetRandomExhibitions 获取随机展品列表
// starID: 明星ID
// materialType: 素材类型过滤(空字符串表示不过滤)
// excludeIDs: 排除的展品ID列表用于去重
// limit: 返回数量
// offset: 偏移量(随机生成)
GetRandomExhibitions(starID int64, materialType string, excludeIDs []int64, limit, offset int) ([]*InspirationFlowItem, error)
}
// InspirationFlowItem 灵感瀑布展品项
type InspirationFlowItem struct {
AssetID int64
Name string
CoverURL string
LikeCount int32
OwnerNickname string
Span int32 // 卡片大小: 0-30→1, 31-100→2, 101-200→3, 200+→4
MaterialType string // 素材类型: hot(人气王者), potential(潜力之星), new(新鲜上架)
CreatedAt int64 // 创建时间(用于判断是否为潜力之星)
}
// ExhibitedAssetInfo 我展出的作品信息
type ExhibitedAssetInfo struct {
AssetID int64
Name string
CoverURL string
LikeCount int32
ExhibitedAt int64
ExpireAt int64
Earnings int64
}
// galleryRepository Repository实现
type galleryRepository struct {
db *gorm.DB
}
// NewGalleryRepository 创建Repository实例
func NewGalleryRepository(db *gorm.DB) GalleryRepository {
return &galleryRepository{db: db}
}
// ==================== 展位相关 ====================
// GetSlotsByUser 获取用户的所有展位
func (r *galleryRepository) GetSlotsByUser(userID, starID int64) ([]*models.BoothSlot, error) {
var slots []*models.BoothSlot
err := r.db.Where("user_id = ? AND star_id = ?", userID, starID).
Order("slot_index ASC").
Find(&slots).Error
return slots, err
}
// GetSlotByID 根据ID获取展位
func (r *galleryRepository) GetSlotByID(slotID int64) (*models.BoothSlot, error) {
var slot models.BoothSlot
err := r.db.Where("slot_id = ?", slotID).First(&slot).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("展位不存在")
}
return nil, err
}
return &slot, nil
}
// GetSlotCount 获取用户的展位数量
func (r *galleryRepository) GetSlotCount(userID, starID int64) (int64, error) {
var count int64
err := r.db.Model(&models.BoothSlot{}).
Where("user_id = ? AND star_id = ?", userID, starID).
Count(&count).Error
return count, err
}
// CreateInitialSlots 创建初始展位(懒加载,支持并发安全)
func (r *galleryRepository) CreateInitialSlots(userID, starID int64, hostProfileID int64) error {
// 使用 PostgreSQL 的 ON CONFLICT 保证并发安全性
return r.db.Transaction(func(tx *gorm.DB) error {
now := time.Now().UnixMilli()
initialSlotCount := config.GalleryRules.InitialSlotCount
for i := 1; i <= initialSlotCount; i++ {
vis := "public"
if i > 3 {
vis = "private"
}
slot := &models.BoothSlot{
HostProfileID: hostProfileID,
UserID: userID,
StarID: starID,
SlotIndex: i,
Visibility: vis,
IsEnabled: true,
UnlockType: "free",
UnlockValue: 0,
CreatedAt: now,
UpdatedAt: now,
}
// 使用 Clause 处理冲突,确保幂等性
err := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "host_profile_id"}, {Name: "slot_index"}},
DoNothing: true,
}).Create(slot).Error
if err != nil {
return err
}
}
return nil
})
}
// CreateSlot 创建新展位(用于解锁)
func (r *galleryRepository) CreateSlot(slot *models.BoothSlot) error {
now := time.Now().UnixMilli()
slot.CreatedAt = now
slot.UpdatedAt = now
return r.db.Create(slot).Error
}
// UnlockSlot 解锁展位
func (r *galleryRepository) UnlockSlot(slotID int64) error {
now := time.Now().UnixMilli()
return r.db.Model(&models.BoothSlot{}).
Where("slot_id = ?", slotID).
Updates(map[string]interface{}{
"is_enabled": true,
"updated_at": now,
}).Error
}
// ==================== 展品相关 ====================
// GetExhibitionByAsset 根据资产ID获取展品展示记录不含已删除
func (r *galleryRepository) GetExhibitionByAsset(assetID int64) (*models.Exhibition, error) {
var exhibition models.Exhibition
err := r.db.Where("asset_id = ? AND deleted_at IS NULL", assetID).First(&exhibition).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil // 未找到记录,返回 nil不是错误
}
return nil, err
}
return &exhibition, nil
}
// GetExhibitionBySlot 根据展位ID获取展品展示记录不含已删除
func (r *galleryRepository) GetExhibitionBySlot(slotID int64) (*models.Exhibition, error) {
var exhibition models.Exhibition
err := r.db.Where("slot_id = ? AND deleted_at IS NULL", slotID).First(&exhibition).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil // 未找到记录,返回 nil不是错误
}
return nil, err
}
return &exhibition, nil
}
// GetExhibitionsByUser 获取用户的所有展品展示记录(不含已删除)
func (r *galleryRepository) GetExhibitionsByUser(userID, starID int64) ([]*models.Exhibition, error) {
var exhibitions []*models.Exhibition
err := r.db.Where("occupier_uid = ? AND occupier_star_id = ? AND deleted_at IS NULL", userID, starID).
Find(&exhibitions).Error
return exhibitions, err
}
// CreateExhibition 创建展品展示记录
func (r *galleryRepository) CreateExhibition(exhibition *models.Exhibition) error {
now := time.Now().UnixMilli()
exhibition.CreatedAt = now
exhibition.UpdatedAt = now
return r.db.Create(exhibition).Error
}
// DeleteExhibition 软删除展品展示记录根据ID
func (r *galleryRepository) DeleteExhibition(exhibitionID int64) error {
now := time.Now().UnixMilli()
return r.db.Model(&models.Exhibition{}).
Where("id = ?", exhibitionID).
Updates(map[string]interface{}{
"deleted_at": now,
"updated_at": now,
}).Error
}
// DeleteExhibitionByAsset 软删除展品展示记录根据资产ID
func (r *galleryRepository) DeleteExhibitionByAsset(assetID int64) error {
now := time.Now().UnixMilli()
return r.db.Model(&models.Exhibition{}).
Where("asset_id = ?", assetID).
Updates(map[string]interface{}{
"deleted_at": now,
"updated_at": now,
}).Error
}
// GetExpiredExhibitions 获取过期的展品展示记录(不含已删除)
func (r *galleryRepository) GetExpiredExhibitions(beforeTime int64) ([]*models.Exhibition, error) {
var exhibitions []*models.Exhibition
err := r.db.Where("expire_at <= ? AND deleted_at IS NULL", beforeTime).Find(&exhibitions).Error
return exhibitions, err
}
// UpdateAssetRegistryDisplayStatus 更新资产注册表的展示状态
func (r *galleryRepository) UpdateAssetRegistryDisplayStatus(assetID int64, displayStatus int32) error {
return r.db.Model(&models.AssetRegistry{}).
Where("asset_id = ?", assetID).
Update("display_status", displayStatus).Error
}
// ========== 我的作品相关实现 ==========
// GetMyExhibitedAssets 获取我展出的作品列表(只返回展出中且未过期的,含收益)
func (r *galleryRepository) GetMyExhibitedAssets(userID, starID int64, page, pageSize int) ([]*ExhibitedAssetInfo, int64, error) {
var items []*ExhibitedAssetInfo
var total int64
now := time.Now().UnixMilli()
// 计数查询
err := r.db.Model(&models.Exhibition{}).
Where("occupier_uid = ? AND occupier_star_id = ? AND deleted_at IS NULL AND expire_at > ?", userID, starID, now).
Count(&total).Error
if err != nil {
return nil, 0, err
}
// 数据查询
offset := (page - 1) * pageSize
err = r.db.Model(&models.Exhibition{}).
Raw(`
SELECT exhibitions.asset_id, a.name, a.cover_url, a.like_count,
exhibitions.start_time as exhibited_at, exhibitions.expire_at,
COALESCE(CAST(SUM(err.crystal_amount) / 10 AS bigint), 0) as earnings
FROM exhibitions
JOIN assets a ON a.id = exhibitions.asset_id
LEFT JOIN exhibition_revenue_records err ON err.asset_id = a.id AND err.status = 'claimable'
WHERE exhibitions.occupier_uid = ? AND exhibitions.occupier_star_id = ?
AND exhibitions.deleted_at IS NULL AND exhibitions.expire_at > ?
GROUP BY exhibitions.asset_id, a.name, a.cover_url, a.like_count, exhibitions.start_time, exhibitions.expire_at
ORDER BY exhibitions.start_time DESC
LIMIT ? OFFSET ?
`, userID, starID, now, pageSize, offset).Scan(&items).Error
if err != nil {
return nil, 0, err
}
return items, total, nil
}
// ========== 灵感瀑布相关实现 ==========
// CountValidExhibitions 统计有效展品数量
func (r *galleryRepository) CountValidExhibitions(starID int64, materialType string) (int64, error) {
var count int64
now := time.Now().UnixMilli()
query := r.db.Model(&models.Exhibition{}).
Where("exhibitions.occupier_star_id = ? AND exhibitions.expire_at > ? AND exhibitions.deleted_at IS NULL", starID, now)
if materialType != "" && materialType != "all" && materialType != "random" {
query = query.Joins("JOIN assets a ON a.id = exhibitions.asset_id").
Where("a.material_type = ?", materialType)
}
err := query.Count(&count).Error
return count, err
}
// GetRandomExhibitions 获取随机展品列表
func (r *galleryRepository) GetRandomExhibitions(starID int64, materialType string, excludeIDs []int64, limit, offset int) ([]*InspirationFlowItem, error) {
var items []*InspirationFlowItem
now := time.Now().UnixMilli()
// 构建基础查询
baseQuery := r.db.Model(&models.Exhibition{}).
Where("exhibitions.occupier_star_id = ? AND exhibitions.expire_at > ? AND exhibitions.deleted_at IS NULL", starID, now)
if materialType != "" && materialType != "all" && materialType != "random" {
baseQuery = baseQuery.Joins("JOIN assets a ON a.id = exhibitions.asset_id").
Where("a.material_type = ?", materialType)
}
// 排除已展示的ID
if len(excludeIDs) > 0 {
baseQuery = baseQuery.Where("exhibitions.id NOT IN ?", excludeIDs)
}
// 执行随机排序查询
var err error
if materialType == "" || materialType == "all" || materialType == "random" {
err = baseQuery.
Select(`exhibitions.asset_id, a.name, a.cover_url, a.like_count, fp.nickname as owner_nickname, a.material_type, a.created_at`).
Joins("JOIN assets a ON a.id = exhibitions.asset_id").
Joins("JOIN fan_profiles fp ON exhibitions.occupier_uid = fp.user_id AND exhibitions.occupier_star_id = fp.star_id").
Where("a.status = 1 AND a.is_active = true").
Order("RANDOM()").
Limit(limit).
Offset(offset).
Scan(&items).Error
} else {
// baseQuery 已经包含了 assets JOIN不需要重复添加
err = baseQuery.
Select(`exhibitions.asset_id, a.name, a.cover_url, a.like_count, fp.nickname as owner_nickname, a.material_type, a.created_at`).
Joins("JOIN fan_profiles fp ON exhibitions.occupier_uid = fp.user_id AND exhibitions.occupier_star_id = fp.star_id").
Where("a.status = 1 AND a.is_active = true").
Order("RANDOM()").
Limit(limit).
Offset(offset).
Scan(&items).Error
}
if err != nil {
return nil, err
}
// Read-repair: 检查并修正 material_type查询时自动修正数据不一致
for _, item := range items {
item.Span = calcSpanByLikes(item.LikeCount)
correctType := calcMaterialType(item.LikeCount, item.CreatedAt)
if item.MaterialType != correctType {
// 异步修正,不阻塞返回(使用 goroutine 避免影响响应时间)
go func(assetID int64, correct string) {
r.db.Model(&models.Asset{}).
Where("id = ?", assetID).
Update("material_type", correct)
}(item.AssetID, correctType)
// 同时修正当前对象的值,保证返回给前端的是正确的
item.MaterialType = correctType
}
}
return items, nil
}
// calcMaterialType 根据点赞数计算素材类型
func calcMaterialType(likes int32, createdAt int64) string {
now := time.Now().UnixMilli()
isWithin1Hour := createdAt > 0 && now-createdAt < 3600*1000
if likes > 20 {
return "hot"
}
// 上架1小时内且点赞>=10为潜力之星
if isWithin1Hour && likes >= 10 {
return "potential"
}
return "new"
}
// calcSpanByLikes 根据点赞数计算卡片大小
// 0-30 → span 1, 31-100 → span 2, 101-200 → span 3, 200+ → span 4
func calcSpanByLikes(likes int32) int32 {
if likes <= 30 {
return 1
} else if likes <= 100 {
return 2
} else if likes <= 200 {
return 3
}
return 4
}
// ==================== 辅助函数 ====================
// generateHostProfileID 生成 host_profile_id
// 注意:这里使用简单的生成逻辑,实际应该与 fan_profiles 表的逻辑一致
func generateHostProfileID(userID, starID int64) int64 {
// 使用简单的组合方式userID * 1000000 + starID
// 实际项目中应该使用与 User Service 一致的逻辑
return userID*1000000 + starID
}