330 lines
9.9 KiB
Go
330 lines
9.9 KiB
Go
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
|
||
}
|