topfans/backend/services/galleryService/service/exhibition_service.go

326 lines
10 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 service
import (
"context"
"errors"
"time"
"github.com/topfans/backend/pkg/database"
"github.com/topfans/backend/pkg/logger"
"github.com/topfans/backend/pkg/models"
pb "github.com/topfans/backend/pkg/proto/gallery"
pbCommon "github.com/topfans/backend/pkg/proto/common"
"github.com/topfans/backend/services/galleryService/client"
"github.com/topfans/backend/services/galleryService/config"
"github.com/topfans/backend/services/galleryService/repository"
"go.uber.org/zap"
)
// ExhibitionService 展品服务接口
type ExhibitionService interface {
PlaceAsset(userID, starID int64, req *pb.PlaceAssetRequest) (*pb.PlaceAssetData, error)
// RemoveFromSlot 从展位移除资产(统一接口,支持展位所有者踢走和占位者下架)
RemoveFromSlot(userID, starID int64, req *pb.RemoveFromSlotRequest) error
// GetMyExhibitedAssets 获取我展出的作品列表
GetMyExhibitedAssets(ctx context.Context, userID, starID int64, req *pb.GetMyExhibitedAssetsRequest) (*pb.GetMyExhibitedAssetsResponse, error)
// GetUserExhibitedAssets 获取他人展出的作品列表
GetUserExhibitedAssets(ctx context.Context, userID, starID int64, req *pb.GetUserExhibitedAssetsRequest) (*pb.GetUserExhibitedAssetsResponse, error)
// RemoveExhibitionByAsset 根据资产ID移除展品内部RPC用于领取收益后下架
RemoveExhibitionByAsset(ctx context.Context, assetID int64) error
}
// exhibitionService 展品服务实现
type exhibitionService struct {
repo repository.GalleryRepository
assetClient client.AssetRPCClient
}
// NewExhibitionService 创建展品服务实例
func NewExhibitionService(repo repository.GalleryRepository, assetClient client.AssetRPCClient) ExhibitionService {
return &exhibitionService{
repo: repo,
assetClient: assetClient,
}
}
// PlaceAsset 在展位展示藏品支持放置到他人展馆方案A实现
func (s *exhibitionService) PlaceAsset(userID, starID int64, req *pb.PlaceAssetRequest) (*pb.PlaceAssetData, error) {
// 1. 验证资产是否存在且属于当前用户 (RPC Asset Service)
if s.assetClient == nil {
return nil, errors.New("asset client not initialized")
}
asset, err := s.assetClient.GetAssetForRPC(req.AssetId, userID, starID)
if err != nil {
// GetAssetForRPC 已经包含了详细的错误信息,直接返回
return nil, err
}
if asset == nil {
return nil, errors.New("资产信息为空")
}
// 验证资产所有权(双重验证,确保安全)
if asset.OwnerUid != userID {
return nil, errors.New("资产不属于当前用户")
}
// 2. 获取目标展位信息
slot, err := s.repo.GetSlotByID(req.SlotId)
if err != nil {
return nil, err
}
if !slot.IsEnabled {
return nil, errors.New("展位未解锁")
}
// 3. 检查展位是否已被占用(使用时间判断)
now := time.Now().UnixMilli()
existingExhibition, err := s.repo.GetActiveExhibitionBySlot(req.SlotId, now)
if err != nil {
return nil, err
}
if existingExhibition != nil {
return nil, errors.New("展位已被占用")
}
// 4. 校验权限:只能放到自己的展位
if slot.UserID != userID {
return nil, errors.New("只能在自己的展位放置藏品")
}
// 5. 执行放置逻辑
expireAt := now + config.GetExhibitionDuration()*1000
exhibition := &models.Exhibition{
AssetID: req.AssetId,
SlotID: req.SlotId,
HostProfileID: slot.HostProfileID,
OccupierUID: userID,
OccupierStarID: starID,
StartTime: now,
ExpireAt: expireAt,
CreatedAt: now,
UpdatedAt: now,
}
// 6. 事务性创建展品并更新展示状态(原子操作)
if err := s.repo.PlaceExhibitionTx(exhibition, 1); err != nil {
return nil, err
}
// 6.5 添加到 ZSET 用于过期清理
ctx := context.Background()
database.AddExpiringAsset(ctx, exhibition.AssetID, exhibition.ExpireAt)
database.AddExpiringAssetToStar(ctx, starID, exhibition.AssetID, exhibition.ExpireAt)
// 7. 发布事件(不再强依赖同步更新 Asset Service 状态)
// TODO: 实现事件发布逻辑
// go s.publishEvent("gallery.exhibit", exhibition)
// 9. 构造响应
return &pb.PlaceAssetData{
Status: "OCCUPIED",
OccupiedUntil: time.UnixMilli(expireAt).Format(time.RFC3339),
OccupierUid: userID,
}, nil
}
// RemoveFromSlot 从展位移除资产(统一接口,支持展位所有者踢走和占位者下架)
func (s *exhibitionService) RemoveFromSlot(userID, starID int64, req *pb.RemoveFromSlotRequest) error {
// 1. 查询展位信息
slot, err := s.repo.GetSlotByID(req.SlotId)
if err != nil {
return err
}
// 2. 查询展品展示记录
exhibition, err := s.repo.GetExhibitionBySlot(req.SlotId)
if err != nil {
return err
}
if exhibition == nil {
return errors.New("展位当前没有展品")
}
// 3. 验证权限(展位所有者或占位者都可以移除)
isSlotOwner := slot.UserID == userID && slot.StarID == starID
isOccupier := exhibition.OccupierUID == userID && exhibition.OccupierStarID == starID
if !isSlotOwner && !isOccupier {
return errors.New("无权移除资产,只有展位所有者或占位者可以操作")
}
// 4. 事务性删除展品并更新展示状态(原子操作)
if err := s.repo.RemoveExhibitionTx(exhibition.ID, exhibition.AssetID); err != nil {
return err
}
// 4.5 从 ZSET 移除
ctx := context.Background()
database.RemoveExpiringAsset(ctx, exhibition.AssetID)
database.RemoveExpiringAssetFromStar(ctx, exhibition.OccupierStarID, exhibition.AssetID)
// 5. 保留点赞记录,允许下次展出时用户重新点赞
// 点赞记录用于历史查询,每次展出通过 exhibition_id 区分
// 6. 发布事件
// TODO: 实现事件发布逻辑
// go s.publishEvent("gallery.remove_from_slot", exhibition)
return nil
}
// GetMyExhibitedAssets 获取我展出的作品列表
func (s *exhibitionService) GetMyExhibitedAssets(ctx context.Context, userID, starID int64, req *pb.GetMyExhibitedAssetsRequest) (*pb.GetMyExhibitedAssetsResponse, 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.repo.GetMyExhibitedAssets(userID, starID, int(page), int(pageSize))
if err != nil {
logger.Logger.Error("Failed to get my exhibited assets",
zap.Error(err),
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
)
return &pb.GetMyExhibitedAssetsResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_INTERNAL_ERROR,
Message: "Failed to get my exhibited assets: " + err.Error(),
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
// 转换为 proto 类型
pbItems := make([]*pb.ExhibitedAssetItem, 0, len(items))
for _, item := range items {
pbItems = append(pbItems, &pb.ExhibitedAssetItem{
AssetId: item.AssetID,
Name: item.Name,
CoverUrl: item.CoverURL,
LikeCount: item.LikeCount,
ExhibitedAt: item.ExhibitedAt,
ExpireAt: item.ExpireAt,
Earnings: item.Earnings,
HourlyEarnings: item.HourlyEarnings,
SlotIndex: item.SlotIndex,
IsLenticular: item.IsLenticular,
})
}
hasMore := int64(page)*int64(pageSize) < total
return &pb.GetMyExhibitedAssetsResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "success",
Timestamp: time.Now().UnixMilli(),
},
Data: &pb.ExhibitedAssetsData{
Items: pbItems,
Page: page,
PageSize: pageSize,
Total: total,
HasMore: hasMore,
},
}, nil
}
// GetUserExhibitedAssets 获取他人展出的作品列表
func (s *exhibitionService) GetUserExhibitedAssets(ctx context.Context, userID, starID int64, req *pb.GetUserExhibitedAssetsRequest) (*pb.GetUserExhibitedAssetsResponse, 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.repo.GetUserExhibitedAssets(userID, starID, int(page), int(pageSize))
if err != nil {
logger.Logger.Error("Failed to get user exhibited assets",
zap.Error(err),
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
)
return &pb.GetUserExhibitedAssetsResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_INTERNAL_ERROR,
Message: "Failed to get user exhibited assets: " + err.Error(),
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
pbItems := make([]*pb.ExhibitedAssetItem, 0, len(items))
for _, item := range items {
pbItems = append(pbItems, &pb.ExhibitedAssetItem{
AssetId: item.AssetID,
Name: item.Name,
CoverUrl: item.CoverURL,
LikeCount: item.LikeCount,
ExhibitedAt: item.ExhibitedAt,
ExpireAt: item.ExpireAt,
Earnings: item.Earnings,
})
}
hasMore := int64(page)*int64(pageSize) < total
return &pb.GetUserExhibitedAssetsResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "success",
Timestamp: time.Now().UnixMilli(),
},
Data: &pb.ExhibitedAssetsData{
Items: pbItems,
Page: page,
PageSize: pageSize,
Total: total,
HasMore: hasMore,
},
}, nil
}
// RemoveExhibitionByAsset 根据资产ID移除展品内部RPC用于领取收益后下架
func (s *exhibitionService) RemoveExhibitionByAsset(ctx context.Context, assetID int64) error {
// 1. 查询展品展示记录
exhibition, err := s.repo.GetExhibitionByAsset(assetID)
if err != nil {
return err
}
if exhibition == nil {
return errors.New("资产当前没有展出记录")
}
// 2. 事务性删除展品并更新展示状态(原子操作)
if err := s.repo.RemoveExhibitionTx(exhibition.ID, exhibition.AssetID); err != nil {
return err
}
// 2.5 从 ZSET 移除
database.RemoveExpiringAsset(ctx, exhibition.AssetID)
database.RemoveExpiringAssetFromStar(ctx, exhibition.OccupierStarID, exhibition.AssetID)
// 3. 保留点赞记录,允许下次展出时用户重新点赞
// 点赞记录用于历史查询,每次展出通过 exhibition_id 区分
return nil
}