package activityapi import ( "net/http" "strings" activityv1 "hyapp.local/api/proto/activity/v1" "hyapp/services/gateway-service/internal/auth" "hyapp/services/gateway-service/internal/transport/http/httpkit" ) type levelOverviewData struct { Tracks []levelTrackOverviewData `json:"tracks"` ServerTimeMS int64 `json:"server_time_ms"` } type levelTrackOverviewData struct { Track string `json:"track"` Name string `json:"name"` Level int32 `json:"level"` TierID int64 `json:"tier_id"` TotalValue int64 `json:"total_value"` CurrentLevelRequiredValue int64 `json:"current_level_required_value"` NextLevel int32 `json:"next_level"` NextLevelRequiredValue int64 `json:"next_level_required_value"` DisplayAvatarFrameResourceID int64 `json:"display_avatar_frame_resource_id"` DisplayAvatarFrameResource *achievementResourceData `json:"display_avatar_frame_resource,omitempty"` DisplayBadgeResourceID int64 `json:"display_badge_resource_id"` DisplayBadgeResource *achievementResourceData `json:"display_badge_resource,omitempty"` DisplayBadgeSourceLevel int32 `json:"display_badge_source_level"` RewardPendingCount int64 `json:"reward_pending_count"` SortOrder int32 `json:"sort_order"` } type levelTrackDetailData struct { Overview levelTrackOverviewData `json:"overview"` Rules []levelRuleData `json:"rules"` Tiers []levelTierData `json:"tiers"` ServerTimeMS int64 `json:"server_time_ms"` } type levelRuleData struct { Track string `json:"track"` Level int32 `json:"level"` RequiredValue int64 `json:"required_value"` Name string `json:"name"` Status string `json:"status"` RewardResourceGroupID int64 `json:"reward_resource_group_id"` RewardResourceGroup *achievementResourceGroupData `json:"reward_resource_group,omitempty"` SortOrder int32 `json:"sort_order"` DisplayConfigJSON string `json:"display_config_json"` CreatedByAdminID int64 `json:"created_by_admin_id"` UpdatedByAdminID int64 `json:"updated_by_admin_id"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` } type levelTierData struct { TierID int64 `json:"tier_id"` Track string `json:"track"` MinLevel int32 `json:"min_level"` MaxLevel int32 `json:"max_level"` Name string `json:"name"` DisplayAvatarFrameResourceID int64 `json:"display_avatar_frame_resource_id"` DisplayAvatarFrameResource *achievementResourceData `json:"display_avatar_frame_resource,omitempty"` DisplayBadgeResourceID int64 `json:"display_badge_resource_id"` DisplayBadgeResource *achievementResourceData `json:"display_badge_resource,omitempty"` RewardResourceGroupID int64 `json:"reward_resource_group_id"` RewardResourceGroup *achievementResourceGroupData `json:"reward_resource_group,omitempty"` Status string `json:"status"` DisplayConfigJSON string `json:"display_config_json"` CreatedByAdminID int64 `json:"created_by_admin_id"` UpdatedByAdminID int64 `json:"updated_by_admin_id"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` } type levelRewardData struct { RewardJobID string `json:"reward_job_id"` UserID int64 `json:"user_id"` Track string `json:"track"` RewardSourceType string `json:"reward_source_type"` RewardSourceID string `json:"reward_source_id"` ResourceGroupID int64 `json:"resource_group_id"` WalletCommandID string `json:"wallet_command_id"` WalletGrantID string `json:"wallet_grant_id"` Status string `json:"status"` AttemptCount int32 `json:"attempt_count"` FailureReason string `json:"failure_reason"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` } func (h *Handler) getMyLevelOverview(writer http.ResponseWriter, request *http.Request) { if h.growthLevelClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } resp, err := h.growthLevelClient.GetMyLevelOverview(request.Context(), &activityv1.GetMyLevelOverviewRequest{ Meta: httpkit.ActivityMeta(request), UserId: auth.UserIDFromContext(request.Context()), }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } // activity-service 只拥有等级关系和资源 ID;素材 URL 属于 wallet-service。Gateway 在同一响应补齐当前等级素材, // 避免 H5 再依赖后台接口或把线上资源 ID 映射硬编码进页面。 resources, ok := h.resolveAchievementResources(writer, request, levelOverviewResourceIDs(resp.GetTracks())) if !ok { return } tracks := make([]levelTrackOverviewData, 0, len(resp.GetTracks())) for _, item := range resp.GetTracks() { tracks = append(tracks, levelTrackOverviewFromProto(item, resources)) } httpkit.WriteOK(writer, request, levelOverviewData{Tracks: tracks, ServerTimeMS: resp.GetServerTimeMs()}) } func (h *Handler) getLevelTrack(writer http.ResponseWriter, request *http.Request) { if h.growthLevelClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } track := strings.TrimSpace(request.PathValue("track")) if track == "" { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } resp, err := h.growthLevelClient.GetLevelTrack(request.Context(), &activityv1.GetLevelTrackRequest{ Meta: httpkit.ActivityMeta(request), UserId: auth.UserIDFromContext(request.Context()), Track: track, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } // 等级层页面必须一次拿到区间、徽章、头像框和升级礼物;这里按正 ID 去重后解析, // 既保持 activity/wallet 的 owner 边界,也避免前端为每一行产生额外请求。 resources, ok := h.resolveAchievementResources(writer, request, levelTrackResourceIDs(resp.GetOverview(), resp.GetTiers())) if !ok { return } resourceGroups, ok := h.resolveAchievementResourceGroups(writer, request, levelTrackResourceGroupIDs(resp.GetRules(), resp.GetTiers())) if !ok { return } rules := make([]levelRuleData, 0, len(resp.GetRules())) for _, rule := range resp.GetRules() { rules = append(rules, levelRuleFromProto(rule, resourceGroups)) } tiers := make([]levelTierData, 0, len(resp.GetTiers())) for _, tier := range resp.GetTiers() { tiers = append(tiers, levelTierFromProto(tier, resources, resourceGroups)) } httpkit.WriteOK(writer, request, levelTrackDetailData{ Overview: levelTrackOverviewFromProto(resp.GetOverview(), resources), Rules: rules, Tiers: tiers, ServerTimeMS: resp.GetServerTimeMs(), }) } func (h *Handler) listLevelRewards(writer http.ResponseWriter, request *http.Request) { if h.growthLevelClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } page, ok := httpkit.PositiveInt32Query(request, "page", 1) if !ok { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 20) if !ok { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } resp, err := h.growthLevelClient.ListLevelRewards(request.Context(), &activityv1.ListLevelRewardsRequest{ Meta: httpkit.ActivityMeta(request), UserId: auth.UserIDFromContext(request.Context()), Track: strings.TrimSpace(request.URL.Query().Get("track")), Status: strings.TrimSpace(request.URL.Query().Get("status")), Page: page, PageSize: pageSize, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } rewards := make([]levelRewardData, 0, len(resp.GetRewards())) for _, reward := range resp.GetRewards() { rewards = append(rewards, levelRewardFromProto(reward)) } httpkit.WriteOK(writer, request, map[string]any{ "rewards": rewards, "total": resp.GetTotal(), "page": page, "page_size": pageSize, }) } func levelTrackOverviewFromProto(item *activityv1.LevelTrackOverview, resources map[int64]achievementResourceData) levelTrackOverviewData { data := levelTrackOverviewData{ Track: item.GetTrack(), Name: item.GetName(), Level: item.GetLevel(), TierID: item.GetTierId(), TotalValue: item.GetTotalValue(), CurrentLevelRequiredValue: item.GetCurrentLevelRequiredValue(), NextLevel: item.GetNextLevel(), NextLevelRequiredValue: item.GetNextLevelRequiredValue(), DisplayAvatarFrameResourceID: item.GetDisplayAvatarFrameResourceId(), DisplayBadgeResourceID: item.GetDisplayBadgeResourceId(), DisplayBadgeSourceLevel: item.GetDisplayBadgeSourceLevel(), RewardPendingCount: item.GetRewardPendingCount(), SortOrder: item.GetSortOrder(), } if resource, ok := resources[item.GetDisplayAvatarFrameResourceId()]; ok { data.DisplayAvatarFrameResource = &resource } if resource, ok := resources[item.GetDisplayBadgeResourceId()]; ok { data.DisplayBadgeResource = &resource } return data } func levelRuleFromProto(item *activityv1.LevelRule, resourceGroups map[int64]achievementResourceGroupData) levelRuleData { data := levelRuleData{ Track: item.GetTrack(), Level: item.GetLevel(), RequiredValue: item.GetRequiredValue(), Name: item.GetName(), Status: item.GetStatus(), RewardResourceGroupID: item.GetRewardResourceGroupId(), SortOrder: item.GetSortOrder(), DisplayConfigJSON: item.GetDisplayConfigJson(), CreatedByAdminID: item.GetCreatedByAdminId(), UpdatedByAdminID: item.GetUpdatedByAdminId(), CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), } if group, ok := resourceGroups[item.GetRewardResourceGroupId()]; ok { data.RewardResourceGroup = &group } return data } func levelTierFromProto(item *activityv1.LevelTier, resources map[int64]achievementResourceData, resourceGroups map[int64]achievementResourceGroupData) levelTierData { data := levelTierData{ TierID: item.GetTierId(), Track: item.GetTrack(), MinLevel: item.GetMinLevel(), MaxLevel: item.GetMaxLevel(), Name: item.GetName(), DisplayAvatarFrameResourceID: item.GetDisplayAvatarFrameResourceId(), DisplayBadgeResourceID: item.GetDisplayBadgeResourceId(), RewardResourceGroupID: item.GetRewardResourceGroupId(), Status: item.GetStatus(), DisplayConfigJSON: item.GetDisplayConfigJson(), CreatedByAdminID: item.GetCreatedByAdminId(), UpdatedByAdminID: item.GetUpdatedByAdminId(), CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), } if resource, ok := resources[item.GetDisplayAvatarFrameResourceId()]; ok { data.DisplayAvatarFrameResource = &resource } if resource, ok := resources[item.GetDisplayBadgeResourceId()]; ok { data.DisplayBadgeResource = &resource } if group, ok := resourceGroups[item.GetRewardResourceGroupId()]; ok { data.RewardResourceGroup = &group } return data } func levelOverviewResourceIDs(items []*activityv1.LevelTrackOverview) []int64 { ids := make([]int64, 0, len(items)*2) for _, item := range items { ids = append(ids, item.GetDisplayAvatarFrameResourceId(), item.GetDisplayBadgeResourceId()) } return ids } func levelTrackResourceIDs(overview *activityv1.LevelTrackOverview, tiers []*activityv1.LevelTier) []int64 { ids := levelOverviewResourceIDs([]*activityv1.LevelTrackOverview{overview}) for _, tier := range tiers { ids = append(ids, tier.GetDisplayAvatarFrameResourceId(), tier.GetDisplayBadgeResourceId()) } return ids } func levelTrackResourceGroupIDs(rules []*activityv1.LevelRule, tiers []*activityv1.LevelTier) []int64 { ids := make([]int64, 0, len(rules)+len(tiers)) for _, rule := range rules { ids = append(ids, rule.GetRewardResourceGroupId()) } for _, tier := range tiers { ids = append(ids, tier.GetRewardResourceGroupId()) } return ids } func levelRewardFromProto(item *activityv1.LevelRewardJob) levelRewardData { return levelRewardData{ RewardJobID: item.GetRewardJobId(), UserID: item.GetUserId(), Track: item.GetTrack(), RewardSourceType: item.GetRewardSourceType(), RewardSourceID: item.GetRewardSourceId(), ResourceGroupID: item.GetResourceGroupId(), WalletCommandID: item.GetWalletCommandId(), WalletGrantID: item.GetWalletGrantId(), Status: item.GetStatus(), AttemptCount: item.GetAttemptCount(), FailureReason: item.GetFailureReason(), CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), } }