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 @@
-
+
+
@@ -20,16 +21,13 @@
class="nft-grid-item"
@click="handleCardClick(item)"
>
-
{{ item.name }}
- ★{{ item.like_count }}
@@ -49,7 +47,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();
- }
-});