fix: 修复星册更多的bug
This commit is contained in:
parent
bcaa743446
commit
35ebfa337e
@ -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和所有者查询资产(用于权限验证)
|
||||
|
||||
@ -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 查询用户的活动藏品列表
|
||||
|
||||
@ -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 查询用户的典藏藏品列表
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -496,7 +496,7 @@ watch(() => props.isActive, (newVal) => {
|
||||
/* 藏品行 - 水平滚动 */
|
||||
.nft-row {
|
||||
width: 100%;
|
||||
height: 320rpx;
|
||||
height: 288rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<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">
|
||||
@ -20,16 +21,13 @@
|
||||
class="nft-grid-item"
|
||||
@click="handleCardClick(item)"
|
||||
>
|
||||
<NftCard
|
||||
:cover-image="item.cover_url_signed"
|
||||
:width="cardSize"
|
||||
:height="cardSize"
|
||||
:locked="false"
|
||||
:custom-style="cardCustomStyle"
|
||||
<image
|
||||
class="nft-cover"
|
||||
:src="item.coverUrl || item.cover_url_signed"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="nft-info">
|
||||
<text class="nft-name">{{ item.name }}</text>
|
||||
<text class="nft-likes">★{{ item.like_count }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@ -49,7 +47,7 @@
|
||||
<text class="no-more-text">— 没有更多了 —</text>
|
||||
</view>
|
||||
|
||||
<BottomNav :activeTab="1" />
|
||||
<BottomNav v-if="showBottomNav" :activeTab="1" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -203,6 +196,16 @@ onReachBottom(() => {
|
||||
background: #0d0820;
|
||||
}
|
||||
|
||||
.background-image {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.loading-container,
|
||||
.empty-container {
|
||||
display: flex;
|
||||
@ -219,19 +222,26 @@ onReachBottom(() => {
|
||||
|
||||
/* 藏品网格容器 */
|
||||
.nft-grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
column-gap: 15rpx;
|
||||
row-gap: 20rpx;
|
||||
padding: 250rpx 30rpx 180rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 250rpx 30rpx 100rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.nft-grid-item {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-top: 133.33%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 32rpx;
|
||||
margin-left: 32rpx;
|
||||
}
|
||||
|
||||
.nft-cover {
|
||||
width: 192rpx;
|
||||
height: 224rpx;
|
||||
}
|
||||
|
||||
.nft-grid-item:active {
|
||||
@ -242,22 +252,18 @@ onReachBottom(() => {
|
||||
.nft-info {
|
||||
padding: 10rpx 0;
|
||||
text-align: center;
|
||||
width: 192rpx;
|
||||
}
|
||||
|
||||
.nft-name {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nft-likes {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* 加载更多 */
|
||||
.load-more {
|
||||
display: flex;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user