topfans/backend/gateway/controller/gallery_controller.go
zerosaturation dcd8cd4527 feat: 实现我的作品统计接口(点赞/展出)
- 新增 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>
2026-04-29 13:35:21 +08:00

636 lines
18 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 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
}
// 创建带有附加信息的contextTriple协议要求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
}
// 创建带有附加信息的contextTriple协议要求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
}
// 创建带有附加信息的contextTriple协议要求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
}
// 创建带有附加信息的contextTriple协议要求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
}
// 创建带有附加信息的contextTriple协议要求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
}
// 创建带有附加信息的contextTriple协议要求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)
}