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/pkg/response" pbActivity "github.com/topfans/backend/pkg/proto/activity" pbCommon "github.com/topfans/backend/pkg/proto/common" "github.com/topfans/backend/pkg/logger" "go.uber.org/zap" ) // ActivityController 活动控制器 type ActivityController struct { activityService pbActivity.ActivityService } // NewActivityController 创建活动控制器 func NewActivityController(dubboClient *client.Client) (*ActivityController, error) { // 创建 ActivityService 客户端 activityService, err := pbActivity.NewActivityService(dubboClient) if err != nil { return nil, err } return &ActivityController{ activityService: activityService, }, nil } // GetActivityList 获取活动列表 // @Summary 获取活动列表 // @Description 获取活动列表,支持按star_id筛选 // @Tags activities // @Accept json // @Produce json // @Security BearerAuth // @Param star_id query int64 true "粉丝身份ID" // @Param status query string false "活动状态: pending/active/completed/expired" // @Param page query int false "页码,默认1" // @Param page_size query int false "每页数量,默认10" // @Success 200 {object} response.Response // @Router /api/v1/activities [get] func (ctrl *ActivityController) GetActivityList(c *gin.Context) { // 解析查询参数 starIDStr := c.Query("star_id") if starIDStr == "" { response.Error(c, http.StatusBadRequest, "star_id 是必填参数") return } starID, err := strconv.ParseInt(starIDStr, 10, 64) if err != nil { response.Error(c, http.StatusBadRequest, "star_id 参数错误") return } status := c.Query("status") page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10")) logger.Logger.Info("GetActivityList request", zap.Int64("star_id", starID), zap.String("status", status), zap.Int("page", page), zap.Int("page_size", pageSize), ) // 设置上下文 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // 调用 RPC resp, err := ctrl.activityService.GetActivityList(ctx, &pbActivity.GetActivityListRequest{ StarId: starID, Status: status, Page: int32(page), PageSize: int32(pageSize), }) if err != nil { logger.Logger.Error("GetActivityList RPC failed", zap.Error(err)) response.Error(c, http.StatusInternalServerError, "获取活动列表失败") return } if resp.Base.Code != pbCommon.StatusCode_STATUS_OK { response.ErrorWithCode(c, int(resp.Base.Code), resp.Base.Message) return } // 转换响应 data := convertActivityListResponse(resp) response.Success(c, data) } // GetActivity 获取活动详情 // @Summary 获取活动详情 // @Description 获取活动详情 // @Tags activities // @Accept json // @Produce json // @Security BearerAuth // @Param activity_id path int64 true "活动ID" // @Success 200 {object} response.Response // @Router /api/v1/activities/{activity_id} [get] func (ctrl *ActivityController) GetActivity(c *gin.Context) { // 解析路径参数 activityIDStr := c.Param("id") activityID, err := strconv.ParseInt(activityIDStr, 10, 64) if err != nil { response.Error(c, http.StatusBadRequest, "活动ID参数错误") return } logger.Logger.Info("GetActivity request", zap.Int64("activity_id", activityID)) // 设置上下文 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // 调用 RPC activity, err := ctrl.activityService.GetActivity(ctx, &pbActivity.GetProgressRequest{ ActivityId: activityID, }) if err != nil { logger.Logger.Error("GetActivity RPC failed", zap.Error(err)) response.Error(c, http.StatusInternalServerError, "获取活动详情失败") return } // 转换响应 data := convertActivityResponse(activity) response.Success(c, data) } // GetActivityItems 获取活动道具列表 // @Summary 获取活动道具列表 // @Description 获取活动道具列表 // @Tags activities // @Accept json // @Produce json // @Security BearerAuth // @Param activity_id path int64 true "活动ID" // @Success 200 {object} response.Response // @Router /api/v1/activities/{activity_id}/items [get] func (ctrl *ActivityController) GetActivityItems(c *gin.Context) { // 解析路径参数 activityIDStr := c.Param("id") activityID, err := strconv.ParseInt(activityIDStr, 10, 64) if err != nil { response.Error(c, http.StatusBadRequest, "活动ID参数错误") return } logger.Logger.Info("GetActivityItems request", zap.Int64("activity_id", activityID)) // 设置上下文 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // 调用 RPC resp, err := ctrl.activityService.GetActivityItems(ctx, &pbActivity.GetProgressRequest{ ActivityId: activityID, }) if err != nil { logger.Logger.Error("GetActivityItems RPC failed", zap.Error(err)) response.Error(c, http.StatusInternalServerError, "获取活动道具列表失败") return } // 转换响应 data := convertActivityItemsResponse(resp) response.Success(c, data) } // GetProgress 获取活动进度 // @Summary 获取活动进度 // @Description 获取活动进度 // @Tags activities // @Accept json // @Produce json // @Security BearerAuth // @Param activity_id path int64 true "活动ID" // @Success 200 {object} response.Response // @Router /api/v1/activities/{activity_id}/progress [get] func (ctrl *ActivityController) GetProgress(c *gin.Context) { // 解析路径参数 activityIDStr := c.Param("id") activityID, err := strconv.ParseInt(activityIDStr, 10, 64) if err != nil { response.Error(c, http.StatusBadRequest, "活动ID参数错误") return } logger.Logger.Info("GetProgress request", zap.Int64("activity_id", activityID)) // 设置上下文 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // 调用 RPC resp, err := ctrl.activityService.GetProgress(ctx, &pbActivity.GetProgressRequest{ ActivityId: activityID, }) if err != nil { logger.Logger.Error("GetProgress RPC failed", zap.Error(err)) response.Error(c, http.StatusInternalServerError, "获取活动进度失败") return } if resp.Base.Code != pbCommon.StatusCode_STATUS_OK { response.ErrorWithCode(c, int(resp.Base.Code), resp.Base.Message) return } // 转换响应 data := convertProgressResponse(resp) response.Success(c, data) } // PurchaseItem 购买道具 // @Summary 购买道具 // @Description 购买活动道具 // @Tags activities // @Accept json // @Produce json // @Security BearerAuth // @Param activity_id path int64 true "活动ID" // @Param request body pbActivity.PurchaseItemRequest true "购买请求" // @Success 200 {object} response.Response // @Router /api/v1/activities/{activity_id}/purchase [post] func (ctrl *ActivityController) PurchaseItem(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 } // 解析路径参数 activityIDStr := c.Param("id") activityID, err := strconv.ParseInt(activityIDStr, 10, 64) if err != nil { response.Error(c, http.StatusBadRequest, "活动ID参数错误") return } // 解析请求体 var req struct { ItemType string `json:"item_type"` Quantity int `json:"quantity"` } if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, http.StatusBadRequest, "请求参数错误") return } if req.ItemType == "" { response.Error(c, http.StatusBadRequest, "item_type 是必填参数") return } if req.Quantity <= 0 { req.Quantity = 1 } logger.Logger.Info("PurchaseItem request", zap.Int64("user_id", userID.(int64)), zap.Int64("star_id", starID.(int64)), zap.Int64("activity_id", activityID), zap.String("item_type", req.ItemType), zap.Int("quantity", req.Quantity), ) // 设置上下文 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), }) // 调用 RPC resp, err := ctrl.activityService.PurchaseItem(ctx, &pbActivity.PurchaseItemRequest{ ActivityId: activityID, ItemType: req.ItemType, Quantity: int32(req.Quantity), StarId: starID.(int64), UserId: userID.(int64), }) if err != nil { logger.Logger.Error("PurchaseItem RPC failed", zap.Error(err)) response.Error(c, http.StatusInternalServerError, "购买道具失败") return } if resp.Base.Code != pbCommon.StatusCode_STATUS_OK { response.ErrorWithCode(c, int(resp.Base.Code), resp.Base.Message) return } // 非 active 阶段:通过 message 信号返回 200 + activity_status activityStatusMap := map[string]string{ "activity:expired": "expired", "activity:pending": "pending", "activity:completed": "completed", "activity:incomplete": "incomplete", "activity:active": "active", } activityMessageMap := map[string]string{ "expired": "活动已结束", "pending": "活动未开始", "completed": "活动已完成", "incomplete": "活动未完成", "active": "活动进行中", } if status, ok := activityStatusMap[resp.Base.Message]; ok { response.Success(c, map[string]interface{}{ "activity_status": status, "message": activityMessageMap[status], }) return } // 正常购买成功 data := convertPurchaseResponse(resp) response.Success(c, data) } // GetContributionRanking 获取贡献点排名 // @Summary 获取贡献点排名 // @Description 获取活动贡献点排名 // @Tags activities // @Accept json // @Produce json // @Security BearerAuth // @Param activity_id path int64 true "活动ID" // @Param star_id query int64 false "粉丝身份ID" // @Param page query int false "页码,默认1" // @Param page_size query int false "每页数量,默认10" // @Success 200 {object} response.Response // @Router /api/v1/activities/{activity_id}/ranking [get] func (ctrl *ActivityController) GetContributionRanking(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 } // 解析路径参数 activityIDStr := c.Param("id") activityID, err := strconv.ParseInt(activityIDStr, 10, 64) if err != nil { response.Error(c, http.StatusBadRequest, "活动ID参数错误") return } // 解析查询参数 starIDParam := c.Query("star_id") var starIDParamInt int64 if starIDParam != "" { starIDParamInt, err = strconv.ParseInt(starIDParam, 10, 64) if err != nil { response.Error(c, http.StatusBadRequest, "star_id 参数错误") return } } page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10")) // 如果传了 star_id 参数,使用传入的值;否则使用当前用户的 star_id reqStarID := starID.(int64) if starIDParamInt > 0 { reqStarID = starIDParamInt } logger.Logger.Info("GetContributionRanking request", zap.Int64("user_id", userID.(int64)), zap.Int64("activity_id", activityID), zap.Int64("star_id", reqStarID), zap.Int("page", page), zap.Int("page_size", pageSize), ) // 设置上下文 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(reqStarID, 10), }) // 调用 RPC resp, err := ctrl.activityService.GetContributionRanking(ctx, &pbActivity.ContributionRankingRequest{ ActivityId: activityID, StarId: reqStarID, Page: int32(page), PageSize: int32(pageSize), UserId: userID.(int64), }) if err != nil { logger.Logger.Error("GetContributionRanking RPC failed", zap.Error(err)) response.Error(c, http.StatusInternalServerError, "获取贡献点排名失败") return } if resp.Base.Code != pbCommon.StatusCode_STATUS_OK { response.ErrorWithCode(c, int(resp.Base.Code), resp.Base.Message) return } // 转换响应 data := convertContributionRankingResponse(resp) response.Success(c, data) } // formatTimestamp 将时间戳转换为 YYYY-MM-DD HH:mm:ss 格式 // 毫秒级时间戳除以1000转换为秒级 func formatTimestamp(timestamp int64) string { if timestamp <= 0 { return "" } // 毫秒级时间戳(13位以上,如1773411298000)除以1000转为秒级 if timestamp >= 1e12 { timestamp = timestamp / 1000 } return time.Unix(timestamp, 0).Format("2006-01-02 15:04:05") } // convertActivityListResponse 转换活动列表响应 func convertActivityListResponse(resp *pbActivity.GetActivityListResponse) map[string]interface{} { activities := make([]map[string]interface{}, 0, len(resp.Activities)) for _, activity := range resp.Activities { activities = append(activities, map[string]interface{}{ "id": activity.Id, "activity_type": activity.ActivityType, "title": activity.Title, "theme": activity.Theme, "description": activity.Description, "star_id": activity.StarId, "start_time": formatTimestamp(activity.StartTime), "end_time": formatTimestamp(activity.EndTime), "overall_end_time": formatTimestamp(activity.OverallEndTime), "target_progress": activity.TargetProgress, "current_progress": activity.CurrentProgress, "status": activity.Status, "current_stage": activity.CurrentStage, "cover_image": activity.CoverImage, "banner_image": activity.BannerImage, "current_stage_background": activity.CurrentStageBackground, "current_stage_title": activity.CurrentStageTitle, }) } return map[string]interface{}{ "activities": activities, "page": resp.Page, "page_size": resp.PageSize, "total": resp.Total, } } // convertActivityResponse 转换活动响应 func convertActivityResponse(activity *pbActivity.Activity) map[string]interface{} { items := make([]map[string]interface{}, 0, len(activity.Items)) for _, item := range activity.Items { items = append(items, map[string]interface{}{ "id": item.Id, "item_type": item.ItemType, "item_name": item.ItemName, "icon_url": item.IconUrl, "crystal_cost": item.CrystalCost, "contribution_points": item.ContributionPoints, }) } return map[string]interface{}{ "id": activity.Id, "activity_type": activity.ActivityType, "title": activity.Title, "theme": activity.Theme, "description": activity.Description, "star_id": activity.StarId, "start_time": formatTimestamp(activity.StartTime), "end_time": formatTimestamp(activity.EndTime), "overall_end_time": formatTimestamp(activity.OverallEndTime), "target_progress": activity.TargetProgress, "current_progress": activity.CurrentProgress, "status": activity.Status, "current_stage": activity.CurrentStage, "cover_image": activity.CoverImage, "banner_image": activity.BannerImage, "current_stage_background": activity.CurrentStageBackground, "current_stage_title": activity.CurrentStageTitle, "items": items, } } // convertActivityItemsResponse 转换活动道具列表响应 func convertActivityItemsResponse(resp *pbActivity.ActivityItemsResponse) map[string]interface{} { items := resp.Items result := make([]map[string]interface{}, 0, len(items)) for _, item := range items { result = append(result, map[string]interface{}{ "id": item.Id, "item_type": item.ItemType, "item_name": item.ItemName, "icon_url": item.IconUrl, "crystal_cost": item.CrystalCost, "contribution_points": item.ContributionPoints, }) } return map[string]interface{}{ "items": result, } } // convertProgressResponse 转换进度响应 func convertProgressResponse(resp *pbActivity.GetProgressResponse) map[string]interface{} { return map[string]interface{}{ "activity_id": resp.ActivityId, "current_progress": resp.CurrentProgress, "target_progress": resp.TargetProgress, "current_stage": resp.CurrentStage, "end_time": formatTimestamp(resp.EndTime), "status": resp.Status, } } // convertPurchaseResponse 转换购买响应 func convertPurchaseResponse(resp *pbActivity.PurchaseItemResponse) map[string]interface{} { return map[string]interface{}{ "total_crystal_spent": resp.TotalCrystalSpent, "total_contribution": resp.TotalContribution, "current_progress": resp.CurrentProgress, "remaining_balance": resp.RemainingBalance, } } // convertContributionRankingResponse 转换排名响应 func convertContributionRankingResponse(resp *pbActivity.ContributionRankingResponse) map[string]interface{} { items := make([]map[string]interface{}, 0, len(resp.Items)) for _, item := range resp.Items { items = append(items, map[string]interface{}{ "rank": item.Rank, "user_id": item.UserId, "nickname": item.Nickname, "avatar_url": item.AvatarUrl, "total_contribution": item.TotalContribution, "total_crystal_spent": item.TotalCrystalSpent, }) } var myContribution map[string]interface{} if resp.MyContribution != nil { myContribution = map[string]interface{}{ "rank": resp.MyContribution.Rank, "total_contribution": resp.MyContribution.TotalContribution, "total_crystal_spent": resp.MyContribution.TotalCrystalSpent, "status": resp.MyContribution.Status, "nickname": resp.MyContribution.Nickname, "avatar_url": resp.MyContribution.AvatarUrl, } } return map[string]interface{}{ "items": items, "my_contribution": myContribution, "page": resp.Page, "page_size": resp.PageSize, "total": resp.Total, } }