diff --git a/docs/flutter对接/礼物TabFlutter对接.md b/docs/flutter对接/礼物TabFlutter对接.md index 43029e4d..09ad09ae 100644 --- a/docs/flutter对接/礼物TabFlutter对接.md +++ b/docs/flutter对接/礼物TabFlutter对接.md @@ -6,6 +6,7 @@ - 礼物 Tab 基础配置入口是 `GET /api/v1/gift-tabs`,不需要 `room_id`,只返回后台启用的 Tab 配置。 - `GET /api/v1/gift-tabs` 不按礼物数量过滤;只要后台启用了 Tab,即使该 Tab 下当前没有礼物也必须返回给 App 展示。 +- `GET /api/v1/gift-tabs` 会按 Tab 返回当前 active 礼物列表,App 可以用它提前预加载礼物资源;无礼物的 Tab 返回 `gifts: []`。 - 房间送礼面板入口是 `GET /api/v1/rooms/{room_id}/gift-panel`,返回当前房间可送礼物、可收礼人、金币余额和房间内可展示 Tab。 - `GET /api/v1/gift-tabs` 不能替代 `GET /api/v1/rooms/{room_id}/gift-panel`;App 真正送礼前必须以房间面板返回的数据为准。 - 送礼只调用 `POST /api/v1/rooms/gift/send`。 @@ -33,6 +34,12 @@ GET /api/v1/gift-tabs X-App-Code: lalu ``` +查询参数: + +| 参数 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `region_id` | int64 | 否 | 不传时预加载全部 active 礼物;传入时只返回 GLOBAL 和该区域可用礼物。 | + 成功响应: ```json @@ -52,7 +59,24 @@ X-App-Code: lalu "order": 10, "sort_order": 10, "created_at_ms": 1710000000000, - "updated_at_ms": 1710000000000 + "updated_at_ms": 1710000000000, + "gifts": [ + { + "gift_id": "rose", + "resource_id": 11, + "gift_type_code": "normal", + "name": "Rose", + "coin_price": 100, + "resource": { + "resource_id": 11, + "resource_code": "gift_rose", + "resource_type": "gift", + "name": "Rose", + "status": "active", + "asset_url": "https://cdn.example/rose.png" + } + } + ] } ], "total": 1 @@ -74,6 +98,7 @@ X-App-Code: lalu | `sort_order` | int | 后台配置排序值。 | | `created_at_ms` | int64 | 创建时间,Unix epoch milliseconds。 | | `updated_at_ms` | int64 | 更新时间,Unix epoch milliseconds。 | +| `gifts` | array | 当前 Tab 下的 active 礼物列表;没有礼物时返回空数组。 | ## 获取礼物面板 diff --git a/docs/openapi/gateway.swagger.yaml b/docs/openapi/gateway.swagger.yaml index ff8e4f4a..a0f44bf0 100644 --- a/docs/openapi/gateway.swagger.yaml +++ b/docs/openapi/gateway.swagger.yaml @@ -1904,9 +1904,16 @@ paths: - resources summary: App 礼物 Tab 配置列表 operationId: listGiftTabs + parameters: + - name: region_id + in: query + required: false + type: integer + format: int64 + description: 不传时预加载全部 active 礼物;传入时只返回 GLOBAL 和该区域可用礼物。 responses: "200": - description: 返回后台启用的礼物 Tab 配置;不按礼物数量过滤,即使 Tab 下当前没有礼物也返回。 + description: 返回后台启用的礼物 Tab 配置和当前 active 礼物;不按礼物数量过滤 Tab,即使 Tab 下当前没有礼物也返回 gifts 空数组。 schema: $ref: "#/definitions/GiftTabListEnvelope" "502": @@ -2580,6 +2587,11 @@ definitions: updated_at_ms: type: integer format: int64 + gifts: + type: array + description: 当前 Tab 下的 active 礼物列表;没有礼物时返回空数组。 + items: + $ref: "#/definitions/GiftConfigData" DeletePushTokenData: type: object required: diff --git a/docs/接口清单.md b/docs/接口清单.md index 92081f0e..d44b117f 100644 --- a/docs/接口清单.md +++ b/docs/接口清单.md @@ -84,7 +84,7 @@ | GET | `/api/v1/resources` | resources | `listResources` | App 资源列表 | | GET | `/api/v1/resource-groups/{group_id}` | resources | `getResourceGroup` | App 资源组详情 | | GET | `/api/v1/gifts` | resources | `listGifts` | App 礼物列表 | -| GET | `/api/v1/gift-tabs` | resources | `listGiftTabs` | App 礼物 Tab 配置列表,不依赖房间 | +| GET | `/api/v1/gift-tabs` | resources | `listGiftTabs` | App 礼物 Tab 配置和礼物预加载列表,不依赖房间 | | GET | `/api/v1/users/me/resources` | resources | `listMyResources` | 我的资源权益 | | POST | `/api/v1/users/me/resources/{resource_id}/equip` | resources | `equipMyResource` | 佩戴我的资源 | | GET | `/api/v1/messages/tabs` | messages | `listMessageTabs` | 消息 tab 分区摘要 | diff --git a/services/gateway-service/internal/transport/http/resourceapi/resource_handler.go b/services/gateway-service/internal/transport/http/resourceapi/resource_handler.go index 179e65a0..82c02754 100644 --- a/services/gateway-service/internal/transport/http/resourceapi/resource_handler.go +++ b/services/gateway-service/internal/transport/http/resourceapi/resource_handler.go @@ -92,16 +92,17 @@ type giftConfigData struct { } type giftTabData struct { - Key string `json:"key"` - GiftTypeCode string `json:"gift_type_code"` - Name string `json:"name"` - Label string `json:"label"` - TabKey string `json:"tab_key"` - Status string `json:"status"` - Order int32 `json:"order"` - SortOrder int32 `json:"sort_order"` - CreatedAtMS int64 `json:"created_at_ms"` - UpdatedAtMS int64 `json:"updated_at_ms"` + Key string `json:"key"` + GiftTypeCode string `json:"gift_type_code"` + Name string `json:"name"` + Label string `json:"label"` + TabKey string `json:"tab_key"` + Status string `json:"status"` + Order int32 `json:"order"` + SortOrder int32 `json:"sort_order"` + CreatedAtMS int64 `json:"created_at_ms"` + UpdatedAtMS int64 `json:"updated_at_ms"` + Gifts []giftConfigData `json:"gifts"` } type emojiPackData struct { @@ -236,6 +237,11 @@ func (h *Handler) listGiftTabs(writer http.ResponseWriter, request *http.Request httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } + regionID, filterRegion, ok := optionalNonNegativeInt64Query(request, "region_id") + if !ok { + httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") + return + } resp, err := h.walletClient.ListGiftTypeConfigs(request.Context(), &walletv1.ListGiftTypeConfigsRequest{ RequestId: httpkit.RequestIDFromContext(request.Context()), AppCode: appcode.FromContext(request.Context()), @@ -245,13 +251,52 @@ func (h *Handler) listGiftTabs(writer http.ResponseWriter, request *http.Request httpkit.WriteRPCError(writer, request, err) return } + gifts, err := h.listAllActiveGifts(request, regionID, filterRegion) + if err != nil { + httpkit.WriteRPCError(writer, request, err) + return + } + giftsByType := make(map[string][]giftConfigData, len(gifts)) + for _, item := range gifts { + giftsByType[item.GiftTypeCode] = append(giftsByType[item.GiftTypeCode], item) + } items := make([]giftTabData, 0, len(resp.GetGiftTypes())) for _, item := range resp.GetGiftTypes() { - items = append(items, giftTabFromProto(item)) + tab := giftTabFromProto(item) + tab.Gifts = giftsByType[tab.GiftTypeCode] + if tab.Gifts == nil { + tab.Gifts = []giftConfigData{} + } + items = append(items, tab) } httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items)}) } +func (h *Handler) listAllActiveGifts(request *http.Request, regionID int64, filterRegion bool) ([]giftConfigData, error) { + const pageSize int32 = 100 + items := make([]giftConfigData, 0, pageSize) + for page := int32(1); ; page++ { + resp, err := h.walletClient.ListGiftConfigs(request.Context(), &walletv1.ListGiftConfigsRequest{ + RequestId: httpkit.RequestIDFromContext(request.Context()), + AppCode: appcode.FromContext(request.Context()), + Page: page, + PageSize: pageSize, + ActiveOnly: true, + RegionId: regionID, + FilterRegion: filterRegion, + }) + if err != nil { + return nil, err + } + for _, gift := range resp.GetGifts() { + items = append(items, giftFromProto(gift)) + } + if len(resp.GetGifts()) == 0 || int64(len(items)) >= resp.GetTotal() { + return items, nil + } + } +} + func (h *Handler) listEmojiPacks(writer http.ResponseWriter, request *http.Request) { if h.walletClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") @@ -460,6 +505,7 @@ func giftTabFromProto(item *walletv1.GiftTypeConfig) giftTabData { SortOrder: item.GetSortOrder(), CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), + Gifts: []giftConfigData{}, } } diff --git a/services/gateway-service/internal/transport/http/response_test.go b/services/gateway-service/internal/transport/http/response_test.go index 10700d83..325d370a 100644 --- a/services/gateway-service/internal/transport/http/response_test.go +++ b/services/gateway-service/internal/transport/http/response_test.go @@ -369,6 +369,7 @@ type fakeWalletClient struct { lastGetResourceGroup *walletv1.GetResourceGroupRequest resourceGroupsByID map[int64]*walletv1.ResourceGroup lastListGiftConfigs *walletv1.ListGiftConfigsRequest + listGiftConfigsResp *walletv1.ListGiftConfigsResponse lastListGiftTypes *walletv1.ListGiftTypeConfigsRequest listGiftTypesResp *walletv1.ListGiftTypeConfigsResponse lastGrantResource *walletv1.GrantResourceRequest @@ -1153,6 +1154,12 @@ func (f *fakeWalletClient) GetResourceGroup(_ context.Context, req *walletv1.Get func (f *fakeWalletClient) ListGiftConfigs(_ context.Context, req *walletv1.ListGiftConfigsRequest) (*walletv1.ListGiftConfigsResponse, error) { f.lastListGiftConfigs = req + if f.err != nil { + return nil, f.err + } + if f.listGiftConfigsResp != nil { + return f.listGiftConfigsResp, nil + } return &walletv1.ListGiftConfigsResponse{}, nil } @@ -3252,6 +3259,26 @@ func TestListEmojiPacksFiltersRegionAndReturnsAssets(t *testing.T) { func TestListGiftTabsReturnsActiveTypeConfigs(t *testing.T) { walletClient := &fakeWalletClient{ + listGiftConfigsResp: &walletv1.ListGiftConfigsResponse{ + Gifts: []*walletv1.GiftConfig{ + { + GiftId: "rose", + ResourceId: 11, + Status: "active", + Name: "Rose", + GiftTypeCode: "normal", + CoinPrice: 100, + Resource: &walletv1.Resource{ + ResourceId: 11, + ResourceCode: "gift_rose", + ResourceType: "gift", + Name: "Rose", + Status: "active", + }, + }, + }, + Total: 1, + }, listGiftTypesResp: &walletv1.ListGiftTypeConfigsResponse{ GiftTypes: []*walletv1.GiftTypeConfig{ { @@ -3296,6 +3323,11 @@ func TestListGiftTabsReturnsActiveTypeConfigs(t *testing.T) { Order int32 `json:"order"` SortOrder int32 `json:"sort_order"` UpdatedAtMS int64 `json:"updated_at_ms"` + Gifts []struct { + GiftID string `json:"gift_id"` + GiftTypeCode string `json:"gift_type_code"` + CoinPrice int64 `json:"coin_price"` + } `json:"gifts"` } `json:"items"` Total int `json:"total"` } `json:"data"` @@ -3306,8 +3338,8 @@ func TestListGiftTabsReturnsActiveTypeConfigs(t *testing.T) { if walletClient.lastListGiftTypes == nil || walletClient.lastListGiftTypes.GetAppCode() != "lalu" || !walletClient.lastListGiftTypes.GetActiveOnly() { t.Fatalf("gift tabs request mismatch: %+v", walletClient.lastListGiftTypes) } - if walletClient.lastListGiftConfigs != nil { - t.Fatalf("gift tabs must not filter tabs by gift configs: %+v", walletClient.lastListGiftConfigs) + if walletClient.lastListGiftConfigs == nil || walletClient.lastListGiftConfigs.GetAppCode() != "lalu" || !walletClient.lastListGiftConfigs.GetActiveOnly() || walletClient.lastListGiftConfigs.GetPageSize() != 100 || walletClient.lastListGiftConfigs.GetFilterRegion() { + t.Fatalf("gift tabs gift preload request mismatch: %+v", walletClient.lastListGiftConfigs) } if envelope.Code != httpkit.CodeOK || envelope.Data.Total != 2 || len(envelope.Data.Items) != 2 { t.Fatalf("gift tabs envelope mismatch: %+v", envelope) @@ -3315,6 +3347,12 @@ func TestListGiftTabsReturnsActiveTypeConfigs(t *testing.T) { if envelope.Data.Items[0].Key != "normal" || envelope.Data.Items[0].GiftTypeCode != "normal" || envelope.Data.Items[0].Name != "普通礼物" || envelope.Data.Items[0].Label != "Gift" || envelope.Data.Items[0].TabKey != "Gift" || envelope.Data.Items[0].Order != 10 || envelope.Data.Items[0].SortOrder != 10 || envelope.Data.Items[0].UpdatedAtMS != 1_700_000_000_100 { t.Fatalf("first gift tab mismatch: %+v", envelope.Data.Items[0]) } + if len(envelope.Data.Items[0].Gifts) != 1 || envelope.Data.Items[0].Gifts[0].GiftID != "rose" || envelope.Data.Items[0].Gifts[0].GiftTypeCode != "normal" || envelope.Data.Items[0].Gifts[0].CoinPrice != 100 { + t.Fatalf("first gift tab gifts mismatch: %+v", envelope.Data.Items[0].Gifts) + } + if len(envelope.Data.Items[1].Gifts) != 0 { + t.Fatalf("empty gift tab must still be returned with empty gifts: %+v", envelope.Data.Items[1]) + } } func TestMessageReadAllReadAndDeleteRoutes(t *testing.T) {