topfans/backend/services/galleryService/service/exhibition_service.go
2026-05-16 02:42:32 +08:00

330 lines
9.9 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/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. 校验权限与可见性(支持放置到他人展馆)
isOwner := slot.UserID == userID
if !isOwner {
// 如果不是自己的展位,必须是 public 且在同一个明星下
if slot.Visibility != "public" {
return nil, errors.New("该展位是私有的,无法放置展品")
}
if slot.StarID != starID {
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
}
// 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
}
// 5. 清除该资产的点赞记录(不阻断主流程)
// 目的:允许同一用户在下次展出时可以再次点赞
go func(assetID int64) {
if err := s.assetClient.ClearAssetLikeRecords(assetID); err != nil {
logger.Logger.Error("Failed to clear asset like records after removal",
zap.Int64("asset_id", assetID),
zap.Error(err),
)
}
}(exhibition.AssetID)
// 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,
})
}
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
}
// 3. 清除该资产的点赞记录(不阻断主流程)
go func(assetID int64) {
if err := s.assetClient.ClearAssetLikeRecords(assetID); err != nil {
logger.Logger.Error("Failed to clear asset like records after removal",
zap.Int64("asset_id", assetID),
zap.Error(err),
)
}
}(exhibition.AssetID)
return nil
}