From e545f41c3b70f90a03925a9f2c3a07812b542676 Mon Sep 17 00:00:00 2001 From: zerosaturation Date: Mon, 25 May 2026 19:03:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E4=BF=AE=E6=94=B9=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E4=B8=BA=E5=A4=8D=E5=90=88=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/dev.sh | 1 + backend/pkg/models/asset.go | 2 +- .../service/asset_like_service.go | 39 +++--------- .../assetService/service/asset_service.go | 6 +- .../repository/gallery_repository.go | 62 +++++++++++++++---- frontend/pages/asset-detail/asset-detail.vue | 2 +- 6 files changed, 65 insertions(+), 47 deletions(-) diff --git a/backend/dev.sh b/backend/dev.sh index 92b8bca..3a2ac4c 100755 --- a/backend/dev.sh +++ b/backend/dev.sh @@ -408,6 +408,7 @@ start_watcher "gateway" "gateway:pkg/proto" "gateway/gateway start_watcher "userService" "services/userService" "services/userService/userService" 20000 1 start_watcher "assetService" "services/assetService:pkg/proto/asset" "services/assetService/assetService" 20003 1 start_watcher "socialService" "services/socialService" "services/socialService/socialService" 20002 1 +start_watcher "galleryService" "services/galleryService" "services/galleryService/galleryService" 20004 1 start_watcher "activityService" "services/activityService" "services/activityService/activityService" 20005 1 start_watcher "taskService" "services/taskService" "services/taskService/taskService" 20006 1 start_watcher "starbookService" "services/starbookService" "services/starbookService/starbookService" 20007 1 diff --git a/backend/pkg/models/asset.go b/backend/pkg/models/asset.go index 87626c2..6f09e8f 100644 --- a/backend/pkg/models/asset.go +++ b/backend/pkg/models/asset.go @@ -121,7 +121,7 @@ type MintOrder struct { MaterialURL *string `gorm:"type:varchar(500);column:material_url"` // 用户上传的素材URL(阶段一写入) Name *string `gorm:"type:varchar(100);column:name"` // 藏品名称(阶段一写入,可选) Description *string `gorm:"type:text;column:description"` // 藏品描述(阶段一写入,可选) - MaterialType *string `gorm:"type:varchar(50);column:material_type"` // 素材类型(可选) + MaterialType *string `gorm:"type:varchar(100);column:material_type"` // 素材类型(可选,多个用逗号分隔) Event *string `gorm:"type:varchar(100);column:event"` // 藏品事件(可选) Info *string `gorm:"type:text;column:info"` // 藏品信息(必填) CreatedAt int64 `gorm:"not null;index:idx_mint_orders_created_at,sort:desc;column:created_at"` diff --git a/backend/services/assetService/service/asset_like_service.go b/backend/services/assetService/service/asset_like_service.go index 35c62c2..d661540 100644 --- a/backend/services/assetService/service/asset_like_service.go +++ b/backend/services/assetService/service/asset_like_service.go @@ -74,37 +74,18 @@ func shouldUpdateMaterialType(oldLikes, newLikes int32, createdAt int64, isIncre // 点赞增加时的临界点 if isIncrement { - // new → hot (超过20) - if oldLikes <= 20 && newLikes > 20 { + // 超过20 → 添加 hot + if newLikes > 20 { return true, "hot" } - // new → potential (达到10且在1小时内) - if oldLikes < 10 && newLikes >= 10 && isWithin1Hour { + // 1小时内达到10 → 添加 potential + if newLikes >= 10 && isWithin1Hour { return true, "potential" } } - // 点赞减少时的临界点(降级检测) - if !isIncrement { - // hot → new/potential (降到20以下) - if oldLikes > 20 && newLikes <= 20 { - // 重新计算 - if newLikes >= 10 && isWithin1Hour { - return true, "potential" - } - return true, "new" - } - // potential → new (降到10以下) - if oldLikes >= 10 && newLikes < 10 { - return true, "new" - } - // hot 降到 21-20 区间 - if oldLikes > 20 && newLikes > 20 { - return false, "" // 仍在 hot 区间 - } - } - + // 点赞减少时不降级(保留原类型) return false, "" } @@ -191,15 +172,13 @@ func (s *AssetLikeService) LikeAsset(ctx context.Context, assetID, userID, starI return fmt.Errorf("failed to increment like count: %w", err) } - // 3.3 更新素材类型(只在临界点更新) + // 3.3 更新素材类型(追加类型,不重复) oldLikes := asset.LikeCount newLikes := oldLikes + 1 shouldUpdate, newMaterialType := shouldUpdateMaterialType(oldLikes, newLikes, asset.CreatedAt, true) - if shouldUpdate { - if err := tx.Model(&models.Asset{}). - Where("id = ?", assetID). - UpdateColumn("material_type", newMaterialType). - Error; err != nil { + if shouldUpdate && newMaterialType != "" { + // 使用 PostgreSQL CONCAT 函数追加类型 + if err := tx.Raw("UPDATE assets SET material_type = CONCAT(COALESCE(material_type, ''), ',', ?) WHERE id = ?", newMaterialType, assetID).Error; err != nil { logger.Logger.Warn("Failed to update material type", zap.Error(err), zap.Int64("asset_id", assetID), diff --git a/backend/services/assetService/service/asset_service.go b/backend/services/assetService/service/asset_service.go index 94e864d..646a803 100644 --- a/backend/services/assetService/service/asset_service.go +++ b/backend/services/assetService/service/asset_service.go @@ -441,13 +441,13 @@ func (s *assetService) GetAsset(req *pb.GetAssetRequest, userID, starID int64) ( var ownerNickname string profile, err := s.userClient.GetFanProfile(context.Background(), asset.OwnerUID, asset.StarID) if err != nil { - logger.Logger.Warn("Failed to get owner fan profile, will return without nickname", + logger.Logger.Warn("Failed to get owner fan profile, using fallback nickname", zap.Int64("owner_uid", asset.OwnerUID), zap.Int64("star_id", asset.StarID), zap.Error(err), ) - // 获取失败时,使用空字符串 - ownerNickname = "" + // 获取失败时,使用 User{uid} 作为 fallback + ownerNickname = fmt.Sprintf("User%d", asset.OwnerUID) } else { ownerNickname = profile.Nickname } diff --git a/backend/services/galleryService/repository/gallery_repository.go b/backend/services/galleryService/repository/gallery_repository.go index 4d30869..c4c3a8a 100644 --- a/backend/services/galleryService/repository/gallery_repository.go +++ b/backend/services/galleryService/repository/gallery_repository.go @@ -2,6 +2,7 @@ package repository import ( "errors" + "strings" "time" "github.com/topfans/backend/pkg/models" @@ -514,7 +515,7 @@ func (r *galleryRepository) CountValidExhibitions(starID int64, materialType str if materialType != "" && materialType != "all" && materialType != "random" { query = query.Joins("JOIN assets a ON a.id = exhibitions.asset_id"). - Where("a.material_type = ?", materialType) + Where("a.material_type LIKE ?", "%"+materialType+"%") } err := query.Count(&count).Error @@ -532,7 +533,7 @@ func (r *galleryRepository) GetRandomExhibitions(starID int64, materialType stri if materialType != "" && materialType != "all" && materialType != "random" { baseQuery = baseQuery.Joins("JOIN assets a ON a.id = exhibitions.asset_id"). - Where("a.material_type = ?", materialType) + Where("a.material_type LIKE ?", "%"+materialType+"%") } // 排除已展示的ID @@ -568,26 +569,42 @@ func (r *galleryRepository) GetRandomExhibitions(starID int64, materialType stri return nil, err } - // Read-repair: 检查并修正 material_type(查询时自动修正数据不一致) + // Read-repair: 检查并补充 material_type(多类型共存,不降级) for _, item := range items { item.Span = calcSpanByLikes(item.LikeCount) - correctType := calcMaterialType(item.LikeCount, item.CreatedAt) - if item.MaterialType != correctType { - // 异步修正,不阻塞返回(使用 goroutine 避免影响响应时间) - go func(assetID int64, correct string) { + correctTypes := calcMaterialTypes(item.LikeCount, item.CreatedAt) + + // 如果缺少任何类型,追加(不覆盖原有类型) + missingTypes := []string{} + for _, ct := range strings.Split(correctTypes, ",") { + ct = strings.TrimSpace(ct) + if ct != "" && !strings.Contains(item.MaterialType, ct) { + missingTypes = append(missingTypes, ct) + } + } + + if len(missingTypes) > 0 { + newType := strings.Join(missingTypes, ",") + // 异步修正 + go func(assetID int64, newType string) { r.db.Model(&models.Asset{}). Where("id = ?", assetID). - Update("material_type", correct) - }(item.AssetID, correctType) - // 同时修正当前对象的值,保证返回给前端的是正确的 - item.MaterialType = correctType + Update("material_type", gorm.Expr("CONCAT(COALESCE(material_type, ''), ',', ?)", newType)) + }(item.AssetID, newType) + + // 修正当前返回对象 + if item.MaterialType == "" { + item.MaterialType = correctTypes + } else { + item.MaterialType = item.MaterialType + "," + strings.Join(missingTypes, ",") + } } } return items, nil } -// calcMaterialType 根据点赞数计算素材类型 +// calcMaterialType 根据点赞数计算素材类型(单类型,互斥) func calcMaterialType(likes int32, createdAt int64) string { now := time.Now().UnixMilli() isWithin1Hour := createdAt > 0 && now-createdAt < 3600*1000 @@ -602,6 +619,27 @@ func calcMaterialType(likes int32, createdAt int64) string { return "new" } +// calcMaterialTypes 根据点赞数计算素材类型(多类型共存) +func calcMaterialTypes(likes int32, createdAt int64) string { + now := time.Now().UnixMilli() + isWithin1Hour := createdAt > 0 && now-createdAt < 3600*1000 + + types := []string{} + + // 超过20 → hot + if likes > 20 { + types = append(types, "hot") + } + // 1小时内达到10 → potential + if isWithin1Hour && likes >= 10 { + types = append(types, "potential") + } + // new 默认存在 + types = append(types, "new") + + return strings.Join(types, ",") +} + // calcSpanByLikes 根据点赞数计算卡片大小 // 0-30 → span 1, 31-100 → span 2, 101-200 → span 3, 200+ → span 4 func calcSpanByLikes(likes int32) int32 { diff --git a/frontend/pages/asset-detail/asset-detail.vue b/frontend/pages/asset-detail/asset-detail.vue index b636e9f..ba0fa4e 100644 --- a/frontend/pages/asset-detail/asset-detail.vue +++ b/frontend/pages/asset-detail/asset-detail.vue @@ -1012,7 +1012,7 @@ onUnmounted(() => { width: 352rpx; height: 520rpx; margin-bottom: 32rpx; - /* animation: card-3d-flip 15s ease-in-out infinite; */ + animation: card-3d-flip 15s ease-in-out infinite; transform-origin: center center; }