package controller import ( "context" "net/http" "strconv" "time" "dubbo.apache.org/dubbo-go/v3/client" "dubbo.apache.org/dubbo-go/v3/common/constant" "github.com/gin-gonic/gin" "github.com/topfans/backend/gateway/dto" "github.com/topfans/backend/gateway/pkg/response" "github.com/topfans/backend/pkg/logger" pbCommon "github.com/topfans/backend/pkg/proto/common" pbGallery "github.com/topfans/backend/pkg/proto/gallery" pbSocial "github.com/topfans/backend/pkg/proto/social" "go.uber.org/zap" ) // GalleryController 展馆相关控制器 type GalleryController struct { galleryService pbGallery.GalleryService socialService pbSocial.SocialService } // NewGalleryController 创建展馆控制器 func NewGalleryController(galleryClient *client.Client, socialClient *client.Client) (*GalleryController, error) { galleryService, err := pbGallery.NewGalleryService(galleryClient) if err != nil { return nil, err } socialService, err := pbSocial.NewSocialService(socialClient) if err != nil { return nil, err } return &GalleryController{ galleryService: galleryService, socialService: socialService, }, nil } // GetMyGallery 获取我的展馆 // @Summary 获取我的展馆 // @Description 获取当前用户的展馆信息 // @Tags galleries // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} response.Response{data=dto.GetMyGalleryResponseDTO} // @Router /api/v1/galleries/me [get] func (ctrl *GalleryController) GetMyGallery(c *gin.Context) { // 从上下文获取用户信息(中间件已验证) userID, exists := c.Get("user_id") if !exists { response.Error(c, http.StatusUnauthorized, "用户未认证") return } starID, exists := c.Get("star_id") if !exists { response.Error(c, http.StatusUnauthorized, "明星身份未设置") return } // 创建带有附加信息的context(Triple协议要求attachments值为string类型) ctx := context.WithValue(context.Background(), constant.AttachmentKey, map[string]interface{}{ "user_id": strconv.FormatInt(userID.(int64), 10), "star_id": strconv.FormatInt(starID.(int64), 10), }) // 调用RPC服务 resp, err := ctrl.galleryService.GetMyGallery(ctx, &pbGallery.GetMyGalleryRequest{}) if err != nil { logger.Logger.Error("GetMyGallery RPC failed", zap.Any("user_id", userID), zap.Any("star_id", starID), zap.Error(err), ) // 解析 RPC 错误消息 _, msg := parseRPCError(err) errorMsg := "获取展馆信息失败" if msg != "" { errorMsg += "," + msg } else if resp != nil && resp.Base != nil && resp.Base.Message != "" { errorMsg += "," + resp.Base.Message } else { errorMsg += ": " + err.Error() } response.Error(c, http.StatusInternalServerError, errorMsg) return } // 检查业务错误 if resp.Base.Code != pbCommon.StatusCode_STATUS_OK { response.Error(c, http.StatusBadRequest, resp.Base.Message) return } // 转换为DTO galleryDTO := dto.ConvertGalleryData(resp.Data) logger.Logger.Info("GetMyGallery success", zap.Any("user_id", userID), zap.Any("star_id", starID), zap.Int32("slot_total", galleryDTO.SlotTotal), ) response.Success(c, galleryDTO) } // GetUserGallery 获取他人展馆 // @Summary 获取他人展馆 // @Description 获取指定用户的展馆信息 // @Tags galleries // @Accept json // @Produce json // @Security BearerAuth // @Param target_uid path int true "用户ID" // @Success 200 {object} response.Response{data=dto.GetUserGalleryResponseDTO} // @Router /api/v1/galleries/{target_uid} [get] func (ctrl *GalleryController) GetUserGallery(c *gin.Context) { // 从上下文获取用户信息(中间件已验证) userID, exists := c.Get("user_id") if !exists { response.Error(c, http.StatusUnauthorized, "用户未认证") return } starID, exists := c.Get("star_id") if !exists { response.Error(c, http.StatusUnauthorized, "明星身份未设置") return } // 获取目标用户ID targetUIDStr := c.Param("target_uid") targetUID, err := strconv.ParseInt(targetUIDStr, 10, 64) if err != nil { response.Error(c, http.StatusBadRequest, "无效的用户ID") return } // 创建带有附加信息的context(Triple协议要求attachments值为string类型) ctx := context.WithValue(context.Background(), constant.AttachmentKey, map[string]interface{}{ "user_id": strconv.FormatInt(userID.(int64), 10), "star_id": strconv.FormatInt(starID.(int64), 10), }) // 调用RPC服务 resp, err := ctrl.galleryService.GetUserGallery(ctx, &pbGallery.GetUserGalleryRequest{ TargetUid: targetUID, }) if err != nil { logger.Logger.Error("GetUserGallery RPC failed", zap.Any("user_id", userID), zap.Any("star_id", starID), zap.Int64("target_uid", targetUID), zap.Error(err), ) // 解析 RPC 错误消息 _, msg := parseRPCError(err) errorMsg := "获取展馆信息失败" if msg != "" { errorMsg += "," + msg } else if resp != nil && resp.Base != nil && resp.Base.Message != "" { errorMsg += "," + resp.Base.Message } else { errorMsg += ": " + err.Error() } response.Error(c, http.StatusInternalServerError, errorMsg) return } // 检查业务错误 if resp.Base.Code != pbCommon.StatusCode_STATUS_OK { response.Error(c, http.StatusBadRequest, resp.Base.Message) return } // 转换为DTO galleryDTO := dto.ConvertGalleryDataToUser(resp.Data) logger.Logger.Info("GetUserGallery success", zap.Any("user_id", userID), zap.Any("star_id", starID), zap.Int64("target_uid", targetUID), zap.Int32("slot_total", galleryDTO.SlotTotal), ) response.Success(c, galleryDTO) } // PlaceAsset 在展位展示藏品 // @Summary 放置藏品 // @Description 在指定展位放置数字藏品进行展示 // @Tags galleries // @Accept json // @Produce json // @Security BearerAuth // @Param request body dto.PlaceAssetRequestDTO true "展位ID和资产ID" // @Success 200 {object} response.Response // @Router /api/v1/galleries/place [post] func (ctrl *GalleryController) PlaceAsset(c *gin.Context) { // 从上下文获取用户信息(中间件已验证) userID, exists := c.Get("user_id") if !exists { response.Error(c, http.StatusUnauthorized, "用户未认证") return } starID, exists := c.Get("star_id") if !exists { response.Error(c, http.StatusUnauthorized, "明星身份未设置") return } // 解析请求参数 var req dto.PlaceAssetRequestDTO if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, http.StatusBadRequest, "参数错误: "+err.Error()) return } // 创建带有附加信息的context(Triple协议要求attachments值为string类型) ctx := context.WithValue(context.Background(), constant.AttachmentKey, map[string]interface{}{ "user_id": strconv.FormatInt(userID.(int64), 10), "star_id": strconv.FormatInt(starID.(int64), 10), }) // 转换为Proto请求 pbReq := dto.ConvertPlaceAssetRequest(&req) // 调用RPC服务 resp, err := ctrl.galleryService.PlaceAsset(ctx, pbReq) if err != nil { logger.Logger.Error("PlaceAsset RPC failed", zap.Any("user_id", userID), zap.Any("star_id", starID), zap.Int64("asset_id", req.AssetID), zap.Int64("slot_id", req.SlotID), zap.Error(err), ) // 优先使用响应中的错误消息,如果没有则解析 RPC 错误 errorMsg := "放置资产失败" if resp != nil && resp.Base != nil && resp.Base.Message != "" { errorMsg += "," + resp.Base.Message } else { // 解析 RPC 错误消息(去掉 code_XX: 前缀) _, msg := parseRPCError(err) if msg != "" { errorMsg += "," + msg } else { errorMsg += ": " + err.Error() } } response.Error(c, http.StatusInternalServerError, errorMsg) return } // 检查业务错误 if resp.Base.Code != pbCommon.StatusCode_STATUS_OK { errorMsg := resp.Base.Message if errorMsg == "" { errorMsg = "未知错误" } response.Error(c, http.StatusBadRequest, errorMsg) return } // 转换为DTO placeDTO := dto.ConvertPlaceAssetData(resp.Data) logger.Logger.Info("PlaceAsset success", zap.Any("user_id", userID), zap.Any("star_id", starID), zap.Int64("asset_id", req.AssetID), zap.Int64("slot_id", req.SlotID), ) response.Success(c, placeDTO) } // UnlockSlot 解锁/购买新展位 // @Summary 解锁展位 // @Description 解锁或购买新的展位 // @Tags galleries // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} response.Response // @Router /api/v1/galleries/slots_unlock [post] func (ctrl *GalleryController) UnlockSlot(c *gin.Context) { // 从上下文获取用户信息(中间件已验证) userID, exists := c.Get("user_id") if !exists { response.Error(c, http.StatusUnauthorized, "用户未认证") return } starID, exists := c.Get("star_id") if !exists { response.Error(c, http.StatusUnauthorized, "明星身份未设置") return } // 创建带有附加信息的context(Triple协议要求attachments值为string类型) ctx := context.WithValue(context.Background(), constant.AttachmentKey, map[string]interface{}{ "user_id": strconv.FormatInt(userID.(int64), 10), "star_id": strconv.FormatInt(starID.(int64), 10), }) // 调用RPC服务 resp, err := ctrl.galleryService.UnlockSlot(ctx, &pbGallery.UnlockSlotRequest{}) if err != nil { logger.Logger.Error("UnlockSlot RPC failed", zap.Any("user_id", userID), zap.Any("star_id", starID), zap.Error(err), ) // 解析 RPC 错误消息 _, msg := parseRPCError(err) errorMsg := "解锁展位失败" if msg != "" { errorMsg += "," + msg } else if resp != nil && resp.Base != nil && resp.Base.Message != "" { errorMsg += "," + resp.Base.Message } else { errorMsg += ": " + err.Error() } response.Error(c, http.StatusInternalServerError, errorMsg) return } // 检查业务错误 if resp.Base.Code != pbCommon.StatusCode_STATUS_OK { response.Error(c, http.StatusBadRequest, resp.Base.Message) return } // 转换为DTO unlockDTO := dto.ConvertUnlockSlotData(resp.Data) logger.Logger.Info("UnlockSlot success", zap.Any("user_id", userID), zap.Any("star_id", starID), zap.Int32("slot_total", unlockDTO.SlotTotal), zap.Int64("crystal_balance", unlockDTO.CrystalBalance), ) response.Success(c, unlockDTO) } // RemoveFromSlot 从展位移除资产(统一接口) // @Summary 移除藏品 // @Description 从指定展位移除展示的藏品 // @Tags galleries // @Accept json // @Produce json // @Security BearerAuth // @Param slot_id path int true "展位ID" // @Success 200 {object} response.Response // @Router /api/v1/galleries/slots/{slot_id}/asset [delete] func (ctrl *GalleryController) RemoveFromSlot(c *gin.Context) { // 从上下文获取用户信息(中间件已验证) userID, exists := c.Get("user_id") if !exists { response.Error(c, http.StatusUnauthorized, "用户未认证") return } starID, exists := c.Get("star_id") if !exists { response.Error(c, http.StatusUnauthorized, "明星身份未设置") return } // 获取展位ID slotIDStr := c.Param("slot_id") slotID, err := strconv.ParseInt(slotIDStr, 10, 64) if err != nil { response.Error(c, http.StatusBadRequest, "无效的展位ID") return } // 创建带有附加信息的context(Triple协议要求attachments值为string类型) ctx := context.WithValue(context.Background(), constant.AttachmentKey, map[string]interface{}{ "user_id": strconv.FormatInt(userID.(int64), 10), "star_id": strconv.FormatInt(starID.(int64), 10), }) // 调用RPC服务 resp, err := ctrl.galleryService.RemoveFromSlot(ctx, &pbGallery.RemoveFromSlotRequest{ SlotId: slotID, }) if err != nil { logger.Logger.Error("RemoveFromSlot RPC failed", zap.Any("user_id", userID), zap.Any("star_id", starID), zap.Int64("slot_id", slotID), zap.Error(err), ) // 优先使用响应中的错误消息,如果没有则解析 RPC 错误 errorMsg := "移除资产失败" if resp != nil && resp.Base != nil && resp.Base.Message != "" { errorMsg += "," + resp.Base.Message } else { // 解析 RPC 错误消息(去掉 code_XX: 前缀) _, msg := parseRPCError(err) if msg != "" { errorMsg += "," + msg } else { errorMsg += ": " + err.Error() } } response.Error(c, http.StatusInternalServerError, errorMsg) return } // 检查业务错误 if resp.Base.Code != pbCommon.StatusCode_STATUS_OK { errorMsg := resp.Base.Message if errorMsg == "" { errorMsg = "未知错误" } response.Error(c, http.StatusBadRequest, errorMsg) return } logger.Logger.Info("RemoveFromSlot success", zap.Any("user_id", userID), zap.Any("star_id", starID), zap.Int64("slot_id", slotID), ) // 返回空对象表示成功 response.Success(c, gin.H{"message": "移除成功"}) } // GetRandomGallery 获取随机玩家的展馆 // @Summary 获取随机展馆 // @Description 随机返回一个玩家的展馆信息,格式与 GET /api/v1/galleries/:target_uid 一致 // @Tags galleries // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} response.Response{data=dto.GetUserGalleryResponseDTO} // @Router /api/v1/galleries/random [get] func (ctrl *GalleryController) GetRandomGallery(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { response.Error(c, http.StatusUnauthorized, "用户未认证") return } starID, exists := c.Get("star_id") if !exists { response.Error(c, http.StatusUnauthorized, "明星身份未设置") return } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() ctx = context.WithValue(ctx, constant.AttachmentKey, map[string]interface{}{ "user_id": strconv.FormatInt(userID.(int64), 10), "star_id": strconv.FormatInt(starID.(int64), 10), }) // 1. 通过 SocialService 获取一个随机用户 randomResp, err := ctrl.socialService.GetRandomUsers(ctx, &pbSocial.GetRandomUsersRequest{Count: 1}) if err != nil { logger.Logger.Error("GetRandomGallery: GetRandomUsers RPC failed", zap.Any("user_id", userID), zap.Error(err), ) _, msg := parseRPCError(err) errMsg := "获取随机用户失败" if msg != "" { errMsg += "," + msg } response.Error(c, http.StatusInternalServerError, errMsg) return } if randomResp.Base.Code != pbCommon.StatusCode_STATUS_OK { response.Error(c, http.StatusBadRequest, randomResp.Base.Message) return } if len(randomResp.Users) == 0 { response.Error(c, http.StatusNotFound, "暂无其他用户") return } targetUID := randomResp.Users[0].UserId // 2. 用随机用户的 ID 获取其展馆信息 galleryResp, err := ctrl.galleryService.GetUserGallery(ctx, &pbGallery.GetUserGalleryRequest{ TargetUid: targetUID, }) if err != nil { logger.Logger.Error("GetRandomGallery: GetUserGallery RPC failed", zap.Any("user_id", userID), zap.Int64("target_uid", targetUID), zap.Error(err), ) _, msg := parseRPCError(err) errMsg := "获取展馆信息失败" if msg != "" { errMsg += "," + msg } else { errMsg += ": " + err.Error() } response.Error(c, http.StatusInternalServerError, errMsg) return } if galleryResp.Base.Code != pbCommon.StatusCode_STATUS_OK { response.Error(c, http.StatusBadRequest, galleryResp.Base.Message) return } galleryDTO := dto.ConvertGalleryDataToUser(galleryResp.Data) logger.Logger.Info("GetRandomGallery success", zap.Any("user_id", userID), zap.Int64("target_uid", targetUID), zap.Int32("slot_total", galleryDTO.SlotTotal), ) response.Success(c, galleryDTO) } // GetMyExhibitedAssets 获取我展出的作品列表 // @Summary 获取我展出的作品列表 // @Description 获取当前用户正在展出的作品列表 // @Tags galleries // @Accept json // @Produce json // @Security BearerAuth // @Param page query int false "页码" default(1) // @Param page_size query int false "每页数量" default(20) // @Success 200 {object} response.Response{data=dto.GetMyExhibitedAssetsResponseDTO} // @Router /api/v1/me/exhibited-assets [get] func (ctrl *GalleryController) GetMyExhibitedAssets(c *gin.Context) { // 从上下文获取用户信息(中间件已验证) userID, exists := c.Get("user_id") if !exists { response.Error(c, http.StatusUnauthorized, "用户未认证") return } starID, exists := c.Get("star_id") if !exists { response.Error(c, http.StatusUnauthorized, "明星身份未设置") return } // 解析分页参数 page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) if page <= 0 { page = 1 } if pageSize <= 0 { pageSize = 20 } if pageSize > 100 { pageSize = 100 } // 创建带有附加信息的context(Triple协议要求attachments值为string类型) ctx := context.WithValue(context.Background(), constant.AttachmentKey, map[string]interface{}{ "user_id": strconv.FormatInt(userID.(int64), 10), "star_id": strconv.FormatInt(starID.(int64), 10), }) // 调用RPC服务 resp, err := ctrl.galleryService.GetMyExhibitedAssets(ctx, &pbGallery.GetMyExhibitedAssetsRequest{ Page: int32(page), PageSize: int32(pageSize), }) if err != nil { logger.Logger.Error("GetMyExhibitedAssets RPC failed", zap.Any("user_id", userID), zap.Any("star_id", starID), zap.Error(err), ) _, msg := parseRPCError(err) errorMsg := "获取展出的作品列表失败" if msg != "" { errorMsg += "," + msg } response.Error(c, http.StatusInternalServerError, errorMsg) return } // 检查业务错误 if resp.Base.Code != pbCommon.StatusCode_STATUS_OK { response.Error(c, http.StatusBadRequest, resp.Base.Message) return } // 转换为DTO items := make([]*dto.ExhibitedAssetItemDTO, 0, len(resp.Data.Items)) for _, item := range resp.Data.Items { items = append(items, &dto.ExhibitedAssetItemDTO{ AssetID: item.AssetId, Name: item.Name, CoverURL: item.CoverUrl, LikeCount: item.LikeCount, ExhibitedAt: item.ExhibitedAt, ExpireAt: item.ExpireAt, Earnings: item.Earnings, }) } result := &dto.GetMyExhibitedAssetsResponseDTO{ Items: items, Page: resp.Data.Page, PageSize: resp.Data.PageSize, Total: resp.Data.Total, HasMore: resp.Data.HasMore, } logger.Logger.Info("GetMyExhibitedAssets success", zap.Any("user_id", userID), zap.Any("star_id", starID), zap.Int32("count", int32(len(items))), ) response.Success(c, result) } // GetInspirationFlow 获取灵感瀑布藏品列表 // @Summary 获取灵感瀑布藏品列表 // @Description 获取灵感瀑布藏品列表,支持随机展示和双向滚动 // @Tags galleries // @Accept json // @Produce json // @Security BearerAuth // @Param cursor query string false "游标" // @Param direction query string false "滚动方向:right(加载新数据)/ left(加载历史)" default(right) // @Param limit query int false "每页数量" default(10) // @Param type query string false "过滤类型:badge/poster/original/all" default(all) // @Param session_id query string false "会话ID" // @Success 200 {object} response.Response{data=dto.GetInspirationFlowResponseDTO} // @Router /api/v1/inspiration-flow [get] func (ctrl *GalleryController) GetInspirationFlow(c *gin.Context) { // 从上下文获取用户信息(中间件已验证) userID, exists := c.Get("user_id") if !exists { response.Error(c, http.StatusUnauthorized, "用户未认证") return } starID, exists := c.Get("star_id") if !exists { response.Error(c, http.StatusUnauthorized, "明星身份未设置") return } // 解析请求参数 cursor := c.Query("cursor") direction := c.DefaultQuery("direction", "right") limitStr := c.DefaultQuery("limit", "10") limit, _ := strconv.Atoi(limitStr) materialType := c.DefaultQuery("type", "all") sessionID := c.Query("session_id") // 创建带有附加信息的context(Triple协议要求attachments值为string类型) ctx := context.WithValue(context.Background(), constant.AttachmentKey, map[string]interface{}{ "user_id": strconv.FormatInt(userID.(int64), 10), "star_id": strconv.FormatInt(starID.(int64), 10), }) // 调用RPC服务 resp, err := ctrl.galleryService.GetInspirationFlow(ctx, &pbGallery.GetInspirationFlowRequest{ Cursor: cursor, Direction: direction, Limit: int32(limit), Type: materialType, SessionId: sessionID, }) if err != nil { logger.Logger.Error("GetInspirationFlow RPC failed", zap.Any("user_id", userID), zap.Any("star_id", starID), zap.Error(err), ) _, msg := parseRPCError(err) errorMsg := "获取灵感瀑布列表失败" if msg != "" { errorMsg += "," + msg } response.Error(c, http.StatusInternalServerError, errorMsg) return } // 检查业务错误 if resp.Base.Code != pbCommon.StatusCode_STATUS_OK { response.Error(c, http.StatusBadRequest, resp.Base.Message) return } // 转换为DTO flowDTO := dto.ConvertInspirationFlowData(resp.Data) logger.Logger.Info("GetInspirationFlow success", zap.Any("user_id", userID), zap.Any("star_id", starID), zap.Int("count", len(flowDTO.Items)), ) response.Success(c, flowDTO) }