fix: 修复星册更多的bug

This commit is contained in:
zerosaturation 2026-04-20 17:04:34 +08:00
parent bcaa743446
commit 35ebfa337e
6 changed files with 165 additions and 165 deletions

View File

@ -18,6 +18,9 @@ type AssetRepository interface {
// GetByID 根据ID查询资产 // GetByID 根据ID查询资产
GetByID(assetID int64) (*models.Asset, error) GetByID(assetID int64) (*models.Asset, error)
// GetByIDs 批量查询资产
GetByIDs(assetIDs []int64) ([]*models.Asset, error)
// GetByIDAndOwner 根据ID和所有者查询资产用于权限验证 // GetByIDAndOwner 根据ID和所有者查询资产用于权限验证
GetByIDAndOwner(assetID, ownerUID, starID int64) (*models.Asset, error) 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 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和所有者查询资产用于权限验证 // GetByIDAndOwner 根据ID和所有者查询资产用于权限验证

View File

@ -19,6 +19,9 @@ type ActivityAssetRepository interface {
// GetByAssetID 根据asset_id查询 // GetByAssetID 根据asset_id查询
GetByAssetID(assetID int64) (*models.ActivityAsset, error) GetByAssetID(assetID int64) (*models.ActivityAsset, error)
// GetByAssetIDs 批量查询
GetByAssetIDs(assetIDs []int64) ([]*models.ActivityAsset, error)
// GetByOwner 查询用户的活动藏品列表 // GetByOwner 查询用户的活动藏品列表
GetByOwner(ownerUID, starID int64, limit, offset int) ([]*models.ActivityAsset, error) 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 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 查询用户的活动藏品列表 // GetByOwner 查询用户的活动藏品列表

View File

@ -19,6 +19,9 @@ type CollectionRepository interface {
// GetByAssetID 根据asset_id查询 // GetByAssetID 根据asset_id查询
GetByAssetID(assetID int64) (*models.CollectionAsset, error) GetByAssetID(assetID int64) (*models.CollectionAsset, error)
// GetByAssetIDs 批量查询
GetByAssetIDs(assetIDs []int64) ([]*models.CollectionAsset, error)
// GetByOwner 查询用户的典藏藏品列表 // GetByOwner 查询用户的典藏藏品列表
GetByOwner(ownerUID, starID int64, limit, offset int) ([]*models.CollectionAsset, error) 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 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 查询用户的典藏藏品列表 // GetByOwner 查询用户的典藏藏品列表

View File

@ -1,13 +1,8 @@
package service package service
import ( import (
"fmt"
"os"
"sort" "sort"
"strings"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/aliyun/credentials-go/credentials"
appErrors "github.com/topfans/backend/pkg/errors" appErrors "github.com/topfans/backend/pkg/errors"
"github.com/topfans/backend/pkg/models" "github.com/topfans/backend/pkg/models"
pb "github.com/topfans/backend/pkg/proto/starbook" 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 { func (s *starbookService) buildAssetItemsFromRegistries(registries []*models.AssetRegistry, assetType string) []*pb.AssetItem {
items := make([]*pb.AssetItem, 0) if len(registries) == 0 {
coverURLs := make([]string, 0) return []*pb.AssetItem{}
assetMap := make(map[string]*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 { for _, reg := range registries {
item := &pb.AssetItem{ item := &pb.AssetItem{
AssetId: reg.AssetID, AssetId: reg.AssetID,
@ -283,45 +333,20 @@ func (s *starbookService) buildAssetItemsFromRegistries(registries []*models.Ass
item.Grade = *reg.Grade item.Grade = *reg.Grade
} }
// 获取原始资产信息 // 填充资产信息
switch assetType { if name, ok := assetNameMap[reg.AssetID]; ok {
case models.AssetTypeRegular: item.Name = name
if asset, err := s.assetRepo.GetByID(reg.AssetID); err == nil { }
item.Name = asset.Name if coverURL, ok := assetCoverMap[reg.AssetID]; ok {
coverURLs = append(coverURLs, asset.CoverURL) item.CoverUrlSigned = coverURL // 直接返回原始 URL
assetMap[asset.CoverURL] = item }
} if cat, ok := categoryMap[reg.AssetID]; ok {
case models.AssetTypeCollection: item.Category = cat
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
}
}
} }
items = append(items, item) 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 return items
} }
@ -410,82 +435,3 @@ func (s *starbookService) GetStarbookItems(req *pb.GetStarbookItemsRequest, owne
}, nil }, 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
}

View File

@ -496,7 +496,7 @@ watch(() => props.isActive, (newVal) => {
/* 藏品行 - 水平滚动 */ /* 藏品行 - 水平滚动 */
.nft-row { .nft-row {
width: 100%; width: 100%;
height: 320rpx; height: 288rpx;
white-space: nowrap; white-space: nowrap;
} }

View File

@ -1,6 +1,7 @@
<template> <template>
<view class="page-container"> <view class="page-container">
<Header :showBack="true" backIconColor="#e6e6e6" :title="pageTitle" /> <image class="background-image" src="/static/background/starbook.jpg" mode="aspectFill"></image>
<Header :showGuideIcon="false" :showTaskIcon="false" :showStarActivityIcon="false" :showBack="true" backIconColor="#e6e6e6" />
<!-- 加载中 --> <!-- 加载中 -->
<view v-if="loading && page === 1" class="loading-container"> <view v-if="loading && page === 1" class="loading-container">
@ -20,16 +21,13 @@
class="nft-grid-item" class="nft-grid-item"
@click="handleCardClick(item)" @click="handleCardClick(item)"
> >
<NftCard <image
:cover-image="item.cover_url_signed" class="nft-cover"
:width="cardSize" :src="item.coverUrl || item.cover_url_signed"
:height="cardSize" mode="aspectFill"
:locked="false"
:custom-style="cardCustomStyle"
/> />
<view class="nft-info"> <view class="nft-info">
<text class="nft-name">{{ item.name }}</text> <text class="nft-name">{{ item.name }}</text>
<text class="nft-likes">{{ item.like_count }}</text>
</view> </view>
</view> </view>
</view> </view>
@ -49,7 +47,7 @@
<text class="no-more-text"> 没有更多了 </text> <text class="no-more-text"> 没有更多了 </text>
</view> </view>
<BottomNav :activeTab="1" /> <BottomNav v-if="showBottomNav" :activeTab="1" />
</view> </view>
</template> </template>
@ -57,8 +55,8 @@
import { ref, computed, onMounted } from 'vue'; import { ref, computed, onMounted } from 'vue';
import Header from "../components/Header.vue"; import Header from "../components/Header.vue";
import BottomNav from "../components/BottomNav.vue"; import BottomNav from "../components/BottomNav.vue";
import NftCard from "../components/NftCard.vue";
import { getStarbookItemsApi } from '@/utils/api.js'; import { getStarbookItemsApi } from '@/utils/api.js';
import { getAssetCoverRealUrl } from '@/utils/assetImageHelper.js';
// //
const screenWidth = ref(0); const screenWidth = ref(0);
@ -79,6 +77,9 @@ const items = ref([]);
// //
const loading = ref(false); const loading = ref(false);
//
const showBottomNav = ref(false);
// //
const cardSize = computed(() => { const cardSize = computed(() => {
if (screenWidth.value === 0) return 200; if (screenWidth.value === 0) return 200;
@ -89,13 +90,6 @@ const cardSize = computed(() => {
return Math.floor(availableWidth / 3); return Math.floor(availableWidth / 3);
}); });
//
const cardCustomStyle = {
position: 'absolute',
top: '0',
left: '0'
};
// //
const pageTitle = computed(() => { const pageTitle = computed(() => {
if (type.value === 'regular') { if (type.value === 'regular') {
@ -132,10 +126,15 @@ const loadData = async (append = false) => {
pageSize.value pageSize.value
); );
if (response.code === 200 && response.data) { 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) { if (append) {
items.value = [...items.value, ...(response.data.items || [])]; items.value = [...items.value, ...newItems];
} else { } else {
items.value = response.data.items || []; items.value = newItems;
} }
total.value = response.data.total || 0; total.value = response.data.total || 0;
} }
@ -186,12 +185,6 @@ onMounted(() => {
loadData(); loadData();
}); });
//
onReachBottom(() => {
if (hasMore.value && !loading.value) {
loadMore();
}
});
</script> </script>
<style scoped> <style scoped>
@ -203,6 +196,16 @@ onReachBottom(() => {
background: #0d0820; background: #0d0820;
} }
.background-image {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 0;
pointer-events: none;
}
.loading-container, .loading-container,
.empty-container { .empty-container {
display: flex; display: flex;
@ -219,19 +222,26 @@ onReachBottom(() => {
/* 藏品网格容器 */ /* 藏品网格容器 */
.nft-grid-container { .nft-grid-container {
display: grid; position: relative;
grid-template-columns: repeat(3, 1fr); z-index: 1;
column-gap: 15rpx; display: flex;
row-gap: 20rpx; flex-wrap: wrap;
padding: 250rpx 30rpx 180rpx; padding: 250rpx 30rpx 100rpx;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
} }
.nft-grid-item { .nft-grid-item {
position: relative; display: flex;
width: 100%; flex-direction: column;
padding-top: 133.33%; align-items: center;
margin-bottom: 32rpx;
margin-left: 32rpx;
}
.nft-cover {
width: 192rpx;
height: 224rpx;
} }
.nft-grid-item:active { .nft-grid-item:active {
@ -242,22 +252,18 @@ onReachBottom(() => {
.nft-info { .nft-info {
padding: 10rpx 0; padding: 10rpx 0;
text-align: center; text-align: center;
width: 192rpx;
} }
.nft-name { .nft-name {
display: block; display: block;
font-size: 22rpx; font-size: 24rpx;
color: rgba(255, 255, 255, 0.9); color: rgba(255, 255, 255, 0.9);
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.nft-likes {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.5);
}
/* 加载更多 */ /* 加载更多 */
.load-more { .load-more {
display: flex; display: flex;