From 35ebfa337e61fb199f8b0e8b5c6329ab7b1d3615 Mon Sep 17 00:00:00 2001 From: zerosaturation Date: Mon, 20 Apr 2026 17:04:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=98=9F=E5=86=8C?= =?UTF-8?q?=E6=9B=B4=E5=A4=9A=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/asset_repository.go | 20 +- .../repository/activity_asset_repository.go | 17 +- .../repository/collection_repository.go | 17 +- .../service/starbook_service.go | 190 +++++++----------- frontend/pages/components/StarbookContent.vue | 2 +- frontend/pages/starbook/items.vue | 84 ++++---- 6 files changed, 165 insertions(+), 165 deletions(-) diff --git a/backend/services/assetService/repository/asset_repository.go b/backend/services/assetService/repository/asset_repository.go index d5ee9c6..82b9cb3 100644 --- a/backend/services/assetService/repository/asset_repository.go +++ b/backend/services/assetService/repository/asset_repository.go @@ -18,6 +18,9 @@ type AssetRepository interface { // GetByID 根据ID查询资产 GetByID(assetID int64) (*models.Asset, error) + // GetByIDs 批量查询资产 + GetByIDs(assetIDs []int64) ([]*models.Asset, error) + // GetByIDAndOwner 根据ID和所有者查询资产(用于权限验证) GetByIDAndOwner(assetID, ownerUID, starID int64) (*models.Asset, error) @@ -91,7 +94,22 @@ func (r *assetRepository) GetByID(assetID int64) (*models.Asset, error) { return nil, err } - return &asset, nil +return &asset, nil +} + +// GetByIDs 批量查询资产 +func (r *assetRepository) GetByIDs(assetIDs []int64) ([]*models.Asset, error) { + if len(assetIDs) == 0 { + return []*models.Asset{}, nil + } + + var assets []*models.Asset + if err := r.db.Where("id IN ? AND is_active = ?", assetIDs, true). + Find(&assets).Error; err != nil { + return nil, err + } + + return assets, nil } // GetByIDAndOwner 根据ID和所有者查询资产(用于权限验证) diff --git a/backend/services/starbookService/repository/activity_asset_repository.go b/backend/services/starbookService/repository/activity_asset_repository.go index 9e9c561..d0cc3cf 100644 --- a/backend/services/starbookService/repository/activity_asset_repository.go +++ b/backend/services/starbookService/repository/activity_asset_repository.go @@ -19,6 +19,9 @@ type ActivityAssetRepository interface { // GetByAssetID 根据asset_id查询 GetByAssetID(assetID int64) (*models.ActivityAsset, error) + // GetByAssetIDs 批量查询 + GetByAssetIDs(assetIDs []int64) ([]*models.ActivityAsset, error) + // GetByOwner 查询用户的活动藏品列表 GetByOwner(ownerUID, starID int64, limit, offset int) ([]*models.ActivityAsset, error) @@ -95,7 +98,19 @@ func (r *activityAssetRepository) GetByAssetID(assetID int64) (*models.ActivityA } return nil, err } - return &asset, nil +return &asset, nil +} + +// GetByAssetIDs 批量查询 +func (r *activityAssetRepository) GetByAssetIDs(assetIDs []int64) ([]*models.ActivityAsset, error) { + if len(assetIDs) == 0 { + return []*models.ActivityAsset{}, nil + } + var assets []*models.ActivityAsset + if err := r.db.Where("asset_id IN ?", assetIDs).Find(&assets).Error; err != nil { + return nil, err + } + return assets, nil } // GetByOwner 查询用户的活动藏品列表 diff --git a/backend/services/starbookService/repository/collection_repository.go b/backend/services/starbookService/repository/collection_repository.go index f413216..1c38466 100644 --- a/backend/services/starbookService/repository/collection_repository.go +++ b/backend/services/starbookService/repository/collection_repository.go @@ -19,6 +19,9 @@ type CollectionRepository interface { // GetByAssetID 根据asset_id查询 GetByAssetID(assetID int64) (*models.CollectionAsset, error) + // GetByAssetIDs 批量查询 + GetByAssetIDs(assetIDs []int64) ([]*models.CollectionAsset, error) + // GetByOwner 查询用户的典藏藏品列表 GetByOwner(ownerUID, starID int64, limit, offset int) ([]*models.CollectionAsset, error) @@ -92,7 +95,19 @@ func (r *collectionRepository) GetByAssetID(assetID int64) (*models.CollectionAs } return nil, err } - return &asset, nil +return &asset, nil +} + +// GetByAssetIDs 批量查询 +func (r *collectionRepository) GetByAssetIDs(assetIDs []int64) ([]*models.CollectionAsset, error) { + if len(assetIDs) == 0 { + return []*models.CollectionAsset{}, nil + } + var assets []*models.CollectionAsset + if err := r.db.Where("asset_id IN ?", assetIDs).Find(&assets).Error; err != nil { + return nil, err + } + return assets, nil } // GetByOwner 查询用户的典藏藏品列表 diff --git a/backend/services/starbookService/service/starbook_service.go b/backend/services/starbookService/service/starbook_service.go index 2eb1f82..f191337 100644 --- a/backend/services/starbookService/service/starbook_service.go +++ b/backend/services/starbookService/service/starbook_service.go @@ -1,13 +1,8 @@ package service import ( - "fmt" - "os" "sort" - "strings" - "github.com/aliyun/aliyun-oss-go-sdk/oss" - "github.com/aliyun/credentials-go/credentials" appErrors "github.com/topfans/backend/pkg/errors" "github.com/topfans/backend/pkg/models" pb "github.com/topfans/backend/pkg/proto/starbook" @@ -263,12 +258,67 @@ func (s *starbookService) buildActivityGroup(ownerUID, starID int64, registries } } -// buildAssetItemsFromRegistries 从索引记录构建资产项 +// buildAssetItemsFromRegistries 从索引记录构建资产项(使用批量查询优化) func (s *starbookService) buildAssetItemsFromRegistries(registries []*models.AssetRegistry, assetType string) []*pb.AssetItem { - items := make([]*pb.AssetItem, 0) - coverURLs := make([]string, 0) - assetMap := make(map[string]*pb.AssetItem) + if len(registries) == 0 { + return []*pb.AssetItem{} + } + items := make([]*pb.AssetItem, 0, len(registries)) + + // 收集所有 asset IDs + assetIDs := make([]int64, 0, len(registries)) + for _, reg := range registries { + assetIDs = append(assetIDs, reg.AssetID) + } + + // 批量查询资产信息(替代 N+1 查询) + var assetCoverMap map[int64]string // assetID -> coverURL + var assetNameMap map[int64]string // assetID -> name + var categoryMap map[int64]string // assetID -> category + + switch assetType { + case models.AssetTypeRegular: + assets, err := s.assetRepo.GetByIDs(assetIDs) + if err == nil && len(assets) > 0 { + assetCoverMap = make(map[int64]string) + assetNameMap = make(map[int64]string) + for _, asset := range assets { + assetCoverMap[asset.ID] = asset.CoverURL + assetNameMap[asset.ID] = asset.Name + } + } + case models.AssetTypeCollection: + colAssets, err := s.collectionRepo.GetByAssetIDs(assetIDs) + if err == nil && len(colAssets) > 0 { + assetCoverMap = make(map[int64]string) + assetNameMap = make(map[int64]string) + categoryMap = make(map[int64]string) + for _, colAsset := range colAssets { + assetCoverMap[colAsset.AssetID] = colAsset.CoverURL + assetNameMap[colAsset.AssetID] = colAsset.Name + if colAsset.Category != "" { + categoryMap[colAsset.AssetID] = colAsset.Category + } + } + } + case models.AssetTypeActivity: + actAssets, err := s.activityRepo.GetByAssetIDs(assetIDs) + if err == nil && len(actAssets) > 0 { + assetCoverMap = make(map[int64]string) + assetNameMap = make(map[int64]string) + categoryMap = make(map[int64]string) + for _, actAsset := range actAssets { + assetCoverMap[actAsset.AssetID] = actAsset.CoverURL + assetNameMap[actAsset.AssetID] = actAsset.Name + if actAsset.ActivityType != "" { + categoryMap[actAsset.AssetID] = actAsset.ActivityType + } + } + } + } + + // 构建 items for _, reg := range registries { item := &pb.AssetItem{ AssetId: reg.AssetID, @@ -283,45 +333,20 @@ func (s *starbookService) buildAssetItemsFromRegistries(registries []*models.Ass item.Grade = *reg.Grade } - // 获取原始资产信息 - switch assetType { - case models.AssetTypeRegular: - if asset, err := s.assetRepo.GetByID(reg.AssetID); err == nil { - item.Name = asset.Name - coverURLs = append(coverURLs, asset.CoverURL) - assetMap[asset.CoverURL] = item - } - case models.AssetTypeCollection: - if colAsset, err := s.collectionRepo.GetByAssetID(reg.AssetID); err == nil { - item.Name = colAsset.Name - coverURLs = append(coverURLs, colAsset.CoverURL) - assetMap[colAsset.CoverURL] = item - if colAsset.Category != "" { - item.Category = colAsset.Category - } - } - case models.AssetTypeActivity: - if actAsset, err := s.activityRepo.GetByAssetID(reg.AssetID); err == nil { - item.Name = actAsset.Name - coverURLs = append(coverURLs, actAsset.CoverURL) - assetMap[actAsset.CoverURL] = item - if actAsset.ActivityType != "" { - item.Category = actAsset.ActivityType - } - } + // 填充资产信息 + if name, ok := assetNameMap[reg.AssetID]; ok { + item.Name = name + } + if coverURL, ok := assetCoverMap[reg.AssetID]; ok { + item.CoverUrlSigned = coverURL // 直接返回原始 URL + } + if cat, ok := categoryMap[reg.AssetID]; ok { + item.Category = cat } items = append(items, item) } - // 批量生成预签名URL - signedURLs := s.batchGeneratePresignedURL(coverURLs) - for url, signedURL := range signedURLs { - if item, ok := assetMap[url]; ok { - item.CoverUrlSigned = signedURL - } - } - return items } @@ -410,82 +435,3 @@ func (s *starbookService) GetStarbookItems(req *pb.GetStarbookItemsRequest, owne }, nil } -// batchGeneratePresignedURL 批量生成预签名URL -func (s *starbookService) batchGeneratePresignedURL(urls []string) map[string]string { - result := make(map[string]string) - for _, url := range urls { - signedURL, err := s.generatePresignedURL(url, 3600) - if err != nil { - result[url] = url // 失败时返回原URL - } else { - result[url] = signedURL - } - } - return result -} - -// generatePresignedURL 生成预签名URL -func (s *starbookService) generatePresignedURL(filePath string, expireSeconds int64) (string, error) { - region := os.Getenv("OSS_REGION") - bucketName := os.Getenv("OSS_BUCKET_NAME") - roleArn := os.Getenv("OSS_STS_ROLE_ARN") - accessKeyID := os.Getenv("OSS_ACCESS_KEY_ID") - accessKeySecret := os.Getenv("OSS_ACCESS_KEY_SECRET") - - if region == "" || bucketName == "" || roleArn == "" || accessKeyID == "" || accessKeySecret == "" { - return "", fmt.Errorf("OSS配置不完整") - } - - // 使用 STS 方式获取临时凭证 - credConfig := new(credentials.Config). - SetType("ram_role_arn"). - SetAccessKeyId(accessKeyID). - SetAccessKeySecret(accessKeySecret). - SetRoleArn(roleArn). - SetRoleSessionName("topfans-download-session"). - SetPolicy(""). - SetRoleSessionExpiration(int(expireSeconds)) - - provider, err := credentials.NewCredential(credConfig) - if err != nil { - return "", fmt.Errorf("创建凭证提供器失败: %w", err) - } - - cred, err := provider.GetCredential() - if err != nil { - return "", fmt.Errorf("获取临时凭证失败: %w", err) - } - - // 创建 OSS 客户端 - endpoint := fmt.Sprintf("https://oss-%s.aliyuncs.com", region) - client, err := oss.New(endpoint, *cred.AccessKeyId, *cred.SecurityToken, - oss.SecurityToken(*cred.SecurityToken)) - if err != nil { - return "", fmt.Errorf("创建OSS客户端失败: %w", err) - } - - // 获取 Bucket - bucket, err := client.Bucket(bucketName) - if err != nil { - return "", fmt.Errorf("获取Bucket失败: %w", err) - } - - // 从完整 URL 中提取 OSS key - ossKey := filePath - if strings.HasPrefix(filePath, "https://") { - parts := strings.SplitN(filePath, ".oss-", 2) - if len(parts) == 2 { - keyParts := strings.SplitN(parts[1], "/", 2) - if len(keyParts) == 2 { - ossKey = keyParts[1] - } - } - } - - signedURL, err := bucket.SignURL(ossKey, oss.HTTPGet, expireSeconds) - if err != nil { - return "", err - } - - return signedURL, nil -} diff --git a/frontend/pages/components/StarbookContent.vue b/frontend/pages/components/StarbookContent.vue index d208227..34af902 100644 --- a/frontend/pages/components/StarbookContent.vue +++ b/frontend/pages/components/StarbookContent.vue @@ -496,7 +496,7 @@ watch(() => props.isActive, (newVal) => { /* 藏品行 - 水平滚动 */ .nft-row { width: 100%; - height: 320rpx; + height: 288rpx; white-space: nowrap; } diff --git a/frontend/pages/starbook/items.vue b/frontend/pages/starbook/items.vue index 602fc51..84312b5 100644 --- a/frontend/pages/starbook/items.vue +++ b/frontend/pages/starbook/items.vue @@ -1,6 +1,7 @@ @@ -57,8 +55,8 @@ import { ref, computed, onMounted } from 'vue'; import Header from "../components/Header.vue"; import BottomNav from "../components/BottomNav.vue"; -import NftCard from "../components/NftCard.vue"; import { getStarbookItemsApi } from '@/utils/api.js'; +import { getAssetCoverRealUrl } from '@/utils/assetImageHelper.js'; // 屏幕宽度 const screenWidth = ref(0); @@ -79,6 +77,9 @@ const items = ref([]); // 加载状态 const loading = ref(false); +// 是否显示底部导航(查看更多页面不显示) +const showBottomNav = ref(false); + // 计算卡片尺寸 const cardSize = computed(() => { if (screenWidth.value === 0) return 200; @@ -89,13 +90,6 @@ const cardSize = computed(() => { return Math.floor(availableWidth / 3); }); -// 卡片自定义样式 -const cardCustomStyle = { - position: 'absolute', - top: '0', - left: '0' -}; - // 页面标题 const pageTitle = computed(() => { if (type.value === 'regular') { @@ -132,10 +126,15 @@ const loadData = async (append = false) => { pageSize.value ); if (response.code === 200 && response.data) { + const newItems = response.data.data.items || []; + // 转换封面 URL + for (const item of newItems) { + item.coverUrl = await getAssetCoverRealUrl(item.cover_url_signed || ''); + } if (append) { - items.value = [...items.value, ...(response.data.items || [])]; + items.value = [...items.value, ...newItems]; } else { - items.value = response.data.items || []; + items.value = newItems; } total.value = response.data.total || 0; } @@ -186,12 +185,6 @@ onMounted(() => { loadData(); }); -// 触底加载更多 -onReachBottom(() => { - if (hasMore.value && !loading.value) { - loadMore(); - } -});