2026-05-14 17:44:16 +08:00

517 lines
20 KiB
Go

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
}