package activityapi import ( "net/http" "strings" activityv1 "hyapp.local/api/proto/activity/v1" walletv1 "hyapp.local/api/proto/wallet/v1" "hyapp/pkg/appcode" "hyapp/services/gateway-service/internal/auth" "hyapp/services/gateway-service/internal/transport/http/httpkit" ) type achievementData struct { Definition achievementDefinitionData `json:"definition"` CycleKey string `json:"cycle_key"` ProgressValue int64 `json:"progress_value"` TargetValue int64 `json:"target_value"` UserStatus string `json:"user_status"` UnlockedAtMS int64 `json:"unlocked_at_ms"` RewardStatus string `json:"reward_status"` } type achievementDefinitionData struct { AchievementID string `json:"achievement_id"` CollectionID string `json:"collection_id"` CollectionType string `json:"collection_type"` AchievementType string `json:"achievement_type"` Title string `json:"title"` Description string `json:"description"` Status string `json:"status"` Version int64 `json:"version"` PrimaryBadgeResourceID int64 `json:"primary_badge_resource_id"` PrimaryBadgeResource *achievementResourceData `json:"primary_badge_resource,omitempty"` RewardResourceGroupID int64 `json:"reward_resource_group_id"` RewardResourceGroup *achievementResourceGroupData `json:"reward_resource_group,omitempty"` AutoPinPolicy string `json:"auto_pin_policy"` HonorVisibility bool `json:"honor_visibility"` EffectiveFromMS int64 `json:"effective_from_ms"` EffectiveToMS int64 `json:"effective_to_ms"` 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"` Conditions []achievementConditionData `json:"conditions"` } type achievementConditionData struct { ConditionID string `json:"condition_id"` AchievementID string `json:"achievement_id"` Version int64 `json:"version"` MetricType string `json:"metric_type"` TargetValue int64 `json:"target_value"` TargetUnit string `json:"target_unit"` DimensionFilterJSON string `json:"dimension_filter_json"` } type achievementResourceData struct { ResourceID int64 `json:"resource_id"` ResourceCode string `json:"resource_code"` ResourceType string `json:"resource_type"` Name string `json:"name"` Status string `json:"status"` Grantable bool `json:"grantable"` GrantStrategy string `json:"grant_strategy"` WalletAssetType string `json:"wallet_asset_type,omitempty"` WalletAssetAmount int64 `json:"wallet_asset_amount,omitempty"` UsageScopes []string `json:"usage_scopes"` AssetURL string `json:"asset_url"` PreviewURL string `json:"preview_url"` AnimationURL string `json:"animation_url"` MetadataJSON string `json:"metadata_json"` SortOrder int32 `json:"sort_order"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` } type achievementResourceGroupItemData struct { GroupItemID int64 `json:"group_item_id"` ItemType string `json:"item_type"` ResourceID int64 `json:"resource_id"` Resource *achievementResourceData `json:"resource,omitempty"` WalletAssetType string `json:"wallet_asset_type,omitempty"` WalletAssetAmount int64 `json:"wallet_asset_amount,omitempty"` Quantity int64 `json:"quantity"` DurationMS int64 `json:"duration_ms"` SortOrder int32 `json:"sort_order"` } type achievementResourceGroupData struct { GroupID int64 `json:"group_id"` GroupCode string `json:"group_code"` Name string `json:"name"` Status string `json:"status"` Description string `json:"description"` SortOrder int32 `json:"sort_order"` Items []achievementResourceGroupItemData `json:"items"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` } type badgeProfileData struct { StripBadges []badgeDisplayItemData `json:"strip_badges"` ProfileTileBadges []badgeDisplayItemData `json:"profile_tile_badges"` HonorBadges []badgeDisplayItemData `json:"honor_badges"` ServerTimeMS int64 `json:"server_time_ms"` } type badgeDisplayItemData struct { Slot string `json:"slot"` Position int32 `json:"position"` BadgeForm string `json:"badge_form"` ResourceID int64 `json:"resource_id"` Resource *achievementResourceData `json:"resource,omitempty"` EntitlementID string `json:"entitlement_id"` SourceType string `json:"source_type"` SourceID string `json:"source_id"` PinMode string `json:"pin_mode"` UpdatedAtMS int64 `json:"updated_at_ms"` } type badgeDisplayRequestBody struct { Slot string `json:"slot"` CommandID string `json:"command_id"` Items []badgeDisplayItemData `json:"items"` } func (h *Handler) listAchievements(writer http.ResponseWriter, request *http.Request) { if h.achievementClient == 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.achievementClient.ListAchievements(request.Context(), &activityv1.ListAchievementsRequest{ Meta: httpkit.ActivityMeta(request), UserId: auth.UserIDFromContext(request.Context()), CollectionType: strings.TrimSpace(request.URL.Query().Get("collection_type")), CollectionId: strings.TrimSpace(request.URL.Query().Get("collection_id")), Page: page, PageSize: pageSize, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } resources, ok := h.resolveAchievementResources(writer, request, achievementResourceIDs(resp.GetAchievements())) if !ok { return } resourceGroups, ok := h.resolveAchievementResourceGroups(writer, request, achievementResourceGroupIDs(resp.GetAchievements())) if !ok { return } items := make([]achievementData, 0, len(resp.GetAchievements())) for _, item := range resp.GetAchievements() { items = append(items, achievementFromProto(item, resources, resourceGroups)) } httpkit.WriteOK(writer, request, map[string]any{ "achievements": items, "total": resp.GetTotal(), "page": page, "page_size": pageSize, "server_time_ms": resp.GetServerTimeMs(), }) } func (h *Handler) listMyBadges(writer http.ResponseWriter, request *http.Request) { if h.achievementClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } resp, err := h.achievementClient.ListMyBadges(request.Context(), &activityv1.ListMyBadgesRequest{ Meta: httpkit.ActivityMeta(request), UserId: auth.UserIDFromContext(request.Context()), }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } resources, ok := h.resolveAchievementResources(writer, request, badgeProfileResourceIDs(resp)) if !ok { return } httpkit.WriteOK(writer, request, badgeProfileFromProto(resp, resources)) } func (h *Handler) setBadgeDisplay(writer http.ResponseWriter, request *http.Request) { if h.achievementClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } var body badgeDisplayRequestBody if !httpkit.Decode(writer, request, &body) { return } slot := strings.TrimSpace(body.Slot) commandID := strings.TrimSpace(body.CommandID) if slot == "" || commandID == "" { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } items := make([]*activityv1.BadgeDisplayItem, 0, len(body.Items)) for _, item := range body.Items { items = append(items, badgeDisplayItemToProto(slot, item)) } resp, err := h.achievementClient.SetBadgeDisplay(request.Context(), &activityv1.SetBadgeDisplayRequest{ Meta: httpkit.ActivityMeta(request), UserId: auth.UserIDFromContext(request.Context()), Slot: slot, Items: items, CommandId: commandID, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } resources, ok := h.resolveAchievementResources(writer, request, badgeProfileResourceIDs(resp.GetProfile())) if !ok { return } httpkit.WriteOK(writer, request, badgeProfileFromProto(resp.GetProfile(), resources)) } func achievementFromProto(item *activityv1.UserAchievement, resources map[int64]achievementResourceData, resourceGroups map[int64]achievementResourceGroupData) achievementData { return achievementData{ Definition: achievementDefinitionFromProto(item.GetDefinition(), resources, resourceGroups), CycleKey: item.GetCycleKey(), ProgressValue: item.GetProgressValue(), TargetValue: item.GetTargetValue(), UserStatus: item.GetUserStatus(), UnlockedAtMS: item.GetUnlockedAtMs(), RewardStatus: item.GetRewardStatus(), } } func achievementDefinitionFromProto(item *activityv1.AchievementDefinition, resources map[int64]achievementResourceData, resourceGroups map[int64]achievementResourceGroupData) achievementDefinitionData { conditions := make([]achievementConditionData, 0, len(item.GetConditions())) for _, condition := range item.GetConditions() { conditions = append(conditions, achievementConditionFromProto(condition)) } data := achievementDefinitionData{ AchievementID: item.GetAchievementId(), CollectionID: item.GetCollectionId(), CollectionType: item.GetCollectionType(), AchievementType: item.GetAchievementType(), Title: item.GetTitle(), Description: item.GetDescription(), Status: item.GetStatus(), Version: item.GetVersion(), PrimaryBadgeResourceID: item.GetPrimaryBadgeResourceId(), RewardResourceGroupID: item.GetRewardResourceGroupId(), AutoPinPolicy: item.GetAutoPinPolicy(), HonorVisibility: item.GetHonorVisibility(), EffectiveFromMS: item.GetEffectiveFromMs(), EffectiveToMS: item.GetEffectiveToMs(), SortOrder: item.GetSortOrder(), DisplayConfigJSON: item.GetDisplayConfigJson(), CreatedByAdminID: item.GetCreatedByAdminId(), UpdatedByAdminID: item.GetUpdatedByAdminId(), CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), Conditions: conditions, } if resource, ok := resources[item.GetPrimaryBadgeResourceId()]; ok { data.PrimaryBadgeResource = &resource } if group, ok := resourceGroups[item.GetRewardResourceGroupId()]; ok { data.RewardResourceGroup = &group } return data } func achievementConditionFromProto(item *activityv1.AchievementCondition) achievementConditionData { return achievementConditionData{ ConditionID: item.GetConditionId(), AchievementID: item.GetAchievementId(), Version: item.GetVersion(), MetricType: item.GetMetricType(), TargetValue: item.GetTargetValue(), TargetUnit: item.GetTargetUnit(), DimensionFilterJSON: item.GetDimensionFilterJson(), } } func badgeProfileFromProto(resp *activityv1.ListMyBadgesResponse, resources map[int64]achievementResourceData) badgeProfileData { return badgeProfileData{ StripBadges: badgeItemsFromProto(resp.GetStripBadges(), resources), ProfileTileBadges: badgeItemsFromProto(resp.GetProfileTileBadges(), resources), HonorBadges: badgeItemsFromProto(resp.GetHonorBadges(), resources), ServerTimeMS: resp.GetServerTimeMs(), } } func badgeItemsFromProto(items []*activityv1.BadgeDisplayItem, resources map[int64]achievementResourceData) []badgeDisplayItemData { result := make([]badgeDisplayItemData, 0, len(items)) for _, item := range items { result = append(result, badgeDisplayItemFromProto(item, resources)) } return result } func badgeDisplayItemFromProto(item *activityv1.BadgeDisplayItem, resources map[int64]achievementResourceData) badgeDisplayItemData { data := badgeDisplayItemData{ Slot: item.GetSlot(), Position: item.GetPosition(), BadgeForm: item.GetBadgeForm(), ResourceID: item.GetResourceId(), EntitlementID: item.GetEntitlementId(), SourceType: item.GetSourceType(), SourceID: item.GetSourceId(), PinMode: item.GetPinMode(), UpdatedAtMS: item.GetUpdatedAtMs(), } if resource, ok := resources[item.GetResourceId()]; ok { data.Resource = &resource } return data } func badgeDisplayItemToProto(slot string, item badgeDisplayItemData) *activityv1.BadgeDisplayItem { return &activityv1.BadgeDisplayItem{ Slot: slot, Position: item.Position, BadgeForm: item.BadgeForm, ResourceId: item.ResourceID, EntitlementId: strings.TrimSpace(item.EntitlementID), SourceType: strings.TrimSpace(item.SourceType), SourceId: strings.TrimSpace(item.SourceID), PinMode: item.PinMode, UpdatedAtMs: item.UpdatedAtMS, } } func (h *Handler) resolveAchievementResources(writer http.ResponseWriter, request *http.Request, ids []int64) (map[int64]achievementResourceData, bool) { ids = uniquePositiveIDs(ids) resources := make(map[int64]achievementResourceData, len(ids)) if len(ids) == 0 { return resources, true } if h.walletClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return nil, false } for _, id := range ids { resp, err := h.walletClient.GetResource(request.Context(), &walletv1.GetResourceRequest{ RequestId: httpkit.RequestIDFromContext(request.Context()), AppCode: appcode.FromContext(request.Context()), ResourceId: id, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return nil, false } resource := achievementResourceFromProto(resp.GetResource()) if resource.ResourceID > 0 { resources[resource.ResourceID] = resource } } return resources, true } func (h *Handler) resolveAchievementResourceGroups(writer http.ResponseWriter, request *http.Request, ids []int64) (map[int64]achievementResourceGroupData, bool) { ids = uniquePositiveIDs(ids) groups := make(map[int64]achievementResourceGroupData, len(ids)) if len(ids) == 0 { return groups, true } if h.walletClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return nil, false } for _, id := range ids { resp, err := h.walletClient.GetResourceGroup(request.Context(), &walletv1.GetResourceGroupRequest{ RequestId: httpkit.RequestIDFromContext(request.Context()), AppCode: appcode.FromContext(request.Context()), GroupId: id, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return nil, false } group := achievementResourceGroupFromProto(resp.GetGroup()) if group.GroupID > 0 { groups[group.GroupID] = group } } return groups, true } func achievementResourceIDs(items []*activityv1.UserAchievement) []int64 { ids := make([]int64, 0, len(items)) for _, item := range items { if item.GetDefinition().GetPrimaryBadgeResourceId() > 0 { ids = append(ids, item.GetDefinition().GetPrimaryBadgeResourceId()) } } return ids } func achievementResourceGroupIDs(items []*activityv1.UserAchievement) []int64 { ids := make([]int64, 0, len(items)) for _, item := range items { if item.GetDefinition().GetRewardResourceGroupId() > 0 { ids = append(ids, item.GetDefinition().GetRewardResourceGroupId()) } } return ids } func badgeProfileResourceIDs(resp *activityv1.ListMyBadgesResponse) []int64 { if resp == nil { return nil } ids := make([]int64, 0, len(resp.GetStripBadges())+len(resp.GetProfileTileBadges())+len(resp.GetHonorBadges())) for _, item := range resp.GetStripBadges() { ids = append(ids, item.GetResourceId()) } for _, item := range resp.GetProfileTileBadges() { ids = append(ids, item.GetResourceId()) } for _, item := range resp.GetHonorBadges() { ids = append(ids, item.GetResourceId()) } return ids } func uniquePositiveIDs(ids []int64) []int64 { seen := make(map[int64]struct{}, len(ids)) result := make([]int64, 0, len(ids)) for _, id := range ids { if id <= 0 { continue } if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} result = append(result, id) } return result } func achievementResourceFromProto(item *walletv1.Resource) achievementResourceData { if item == nil { return achievementResourceData{} } return achievementResourceData{ ResourceID: item.GetResourceId(), ResourceCode: item.GetResourceCode(), ResourceType: item.GetResourceType(), Name: item.GetName(), Status: item.GetStatus(), Grantable: item.GetGrantable(), GrantStrategy: item.GetGrantStrategy(), WalletAssetType: item.GetWalletAssetType(), WalletAssetAmount: item.GetWalletAssetAmount(), UsageScopes: item.GetUsageScopes(), AssetURL: item.GetAssetUrl(), PreviewURL: item.GetPreviewUrl(), AnimationURL: item.GetAnimationUrl(), MetadataJSON: item.GetMetadataJson(), SortOrder: item.GetSortOrder(), CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), } } func achievementResourceGroupFromProto(group *walletv1.ResourceGroup) achievementResourceGroupData { if group == nil { return achievementResourceGroupData{} } items := make([]achievementResourceGroupItemData, 0, len(group.GetItems())) for _, item := range group.GetItems() { items = append(items, achievementResourceGroupItemData{ GroupItemID: item.GetGroupItemId(), ItemType: item.GetItemType(), ResourceID: item.GetResourceId(), Resource: achievementResourcePointerFromProto(item.GetResource()), WalletAssetType: item.GetWalletAssetType(), WalletAssetAmount: item.GetWalletAssetAmount(), Quantity: item.GetQuantity(), DurationMS: item.GetDurationMs(), SortOrder: item.GetSortOrder(), }) } return achievementResourceGroupData{ GroupID: group.GetGroupId(), GroupCode: group.GetGroupCode(), Name: group.GetName(), Status: group.GetStatus(), Description: group.GetDescription(), SortOrder: group.GetSortOrder(), Items: items, CreatedAtMS: group.GetCreatedAtMs(), UpdatedAtMS: group.GetUpdatedAtMs(), } } func achievementResourcePointerFromProto(item *walletv1.Resource) *achievementResourceData { resource := achievementResourceFromProto(item) if resource.ResourceID == 0 { return nil } return &resource }