- 新增 GET /api/v1/me/liked-assets 接口 - 新增 GET /api/v1/me/exhibited-assets 接口 - 新增 GetMyLikedAssets 和 GetMyExhibitedAssets RPC 方法 - 新增 ExhibitedAssetItemDTO 和 GetMyExhibitedAssetsResponseDTO - 前端新增 getUserLikedAssetsApi 和 getUserExhibitedAssetsApi(暂不实现) - 更新设计文档,标记他人作品统计接口为暂不实现 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
287 lines
9.3 KiB
Go
287 lines
9.3 KiB
Go
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)
|
||
}
|
||
|
||
// 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{}).
|
||
Select(`exhibitions.asset_id, a.name, a.cover_url, a.like_count,
|
||
exhibitions.start_time as exhibited_at, exhibitions.expire_at,
|
||
COALESCE(SUM(err.crystal_amount), 0) as earnings`).
|
||
Joins("JOIN assets a ON a.id = exhibitions.asset_id").
|
||
Joins("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 = ?", userID, starID).
|
||
Where("exhibitions.deleted_at IS NULL AND exhibitions.expire_at > ?", now).
|
||
Group("exhibitions.asset_id, a.name, a.cover_url, a.like_count, exhibitions.start_time, exhibitions.expire_at").
|
||
Order("exhibitions.start_time DESC").
|
||
Limit(pageSize).
|
||
Offset(offset).
|
||
Scan(&items).Error
|
||
|
||
if err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
return items, total, nil
|
||
}
|
||
|
||
// ==================== 辅助函数 ====================
|
||
|
||
// generateHostProfileID 生成 host_profile_id
|
||
// 注意:这里使用简单的生成逻辑,实际应该与 fan_profiles 表的逻辑一致
|
||
func generateHostProfileID(userID, starID int64) int64 {
|
||
// 使用简单的组合方式:userID * 1000000 + starID
|
||
// 实际项目中应该使用与 User Service 一致的逻辑
|
||
return userID*1000000 + starID
|
||
}
|