feat:修改类型为复合类型

This commit is contained in:
zerosaturation 2026-05-25 19:03:23 +08:00
parent 0852f96436
commit e545f41c3b
6 changed files with 65 additions and 47 deletions

View File

@ -408,6 +408,7 @@ start_watcher "gateway" "gateway:pkg/proto" "gateway/gateway
start_watcher "userService" "services/userService" "services/userService/userService" 20000 1 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 "assetService" "services/assetService:pkg/proto/asset" "services/assetService/assetService" 20003 1
start_watcher "socialService" "services/socialService" "services/socialService/socialService" 20002 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 "activityService" "services/activityService" "services/activityService/activityService" 20005 1
start_watcher "taskService" "services/taskService" "services/taskService/taskService" 20006 1 start_watcher "taskService" "services/taskService" "services/taskService/taskService" 20006 1
start_watcher "starbookService" "services/starbookService" "services/starbookService/starbookService" 20007 1 start_watcher "starbookService" "services/starbookService" "services/starbookService/starbookService" 20007 1

View File

@ -121,7 +121,7 @@ type MintOrder struct {
MaterialURL *string `gorm:"type:varchar(500);column:material_url"` // 用户上传的素材URL阶段一写入 MaterialURL *string `gorm:"type:varchar(500);column:material_url"` // 用户上传的素材URL阶段一写入
Name *string `gorm:"type:varchar(100);column:name"` // 藏品名称(阶段一写入,可选) Name *string `gorm:"type:varchar(100);column:name"` // 藏品名称(阶段一写入,可选)
Description *string `gorm:"type:text;column:description"` // 藏品描述(阶段一写入,可选) 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"` // 藏品事件(可选) Event *string `gorm:"type:varchar(100);column:event"` // 藏品事件(可选)
Info *string `gorm:"type:text;column:info"` // 藏品信息(必填) Info *string `gorm:"type:text;column:info"` // 藏品信息(必填)
CreatedAt int64 `gorm:"not null;index:idx_mint_orders_created_at,sort:desc;column:created_at"` CreatedAt int64 `gorm:"not null;index:idx_mint_orders_created_at,sort:desc;column:created_at"`

View File

@ -74,37 +74,18 @@ func shouldUpdateMaterialType(oldLikes, newLikes int32, createdAt int64, isIncre
// 点赞增加时的临界点 // 点赞增加时的临界点
if isIncrement { if isIncrement {
// new → hot (超过20) // 超过20 → 添加 hot
if oldLikes <= 20 && newLikes > 20 { if newLikes > 20 {
return true, "hot" return true, "hot"
} }
// new → potential (达到10且在1小时内) // 1小时内达到10 → 添加 potential
if oldLikes < 10 && newLikes >= 10 && isWithin1Hour {
return true, "potential"
}
}
// 点赞减少时的临界点(降级检测)
if !isIncrement {
// hot → new/potential (降到20以下)
if oldLikes > 20 && newLikes <= 20 {
// 重新计算
if newLikes >= 10 && isWithin1Hour { if newLikes >= 10 && isWithin1Hour {
return true, "potential" 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, "" 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) return fmt.Errorf("failed to increment like count: %w", err)
} }
// 3.3 更新素材类型(只在临界点更新 // 3.3 更新素材类型(追加类型,不重复
oldLikes := asset.LikeCount oldLikes := asset.LikeCount
newLikes := oldLikes + 1 newLikes := oldLikes + 1
shouldUpdate, newMaterialType := shouldUpdateMaterialType(oldLikes, newLikes, asset.CreatedAt, true) shouldUpdate, newMaterialType := shouldUpdateMaterialType(oldLikes, newLikes, asset.CreatedAt, true)
if shouldUpdate { if shouldUpdate && newMaterialType != "" {
if err := tx.Model(&models.Asset{}). // 使用 PostgreSQL CONCAT 函数追加类型
Where("id = ?", assetID). if err := tx.Raw("UPDATE assets SET material_type = CONCAT(COALESCE(material_type, ''), ',', ?) WHERE id = ?", newMaterialType, assetID).Error; err != nil {
UpdateColumn("material_type", newMaterialType).
Error; err != nil {
logger.Logger.Warn("Failed to update material type", logger.Logger.Warn("Failed to update material type",
zap.Error(err), zap.Error(err),
zap.Int64("asset_id", assetID), zap.Int64("asset_id", assetID),

View File

@ -441,13 +441,13 @@ func (s *assetService) GetAsset(req *pb.GetAssetRequest, userID, starID int64) (
var ownerNickname string var ownerNickname string
profile, err := s.userClient.GetFanProfile(context.Background(), asset.OwnerUID, asset.StarID) profile, err := s.userClient.GetFanProfile(context.Background(), asset.OwnerUID, asset.StarID)
if err != nil { 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("owner_uid", asset.OwnerUID),
zap.Int64("star_id", asset.StarID), zap.Int64("star_id", asset.StarID),
zap.Error(err), zap.Error(err),
) )
// 获取失败时,使用空字符串 // 获取失败时,使用 User{uid} 作为 fallback
ownerNickname = "" ownerNickname = fmt.Sprintf("User%d", asset.OwnerUID)
} else { } else {
ownerNickname = profile.Nickname ownerNickname = profile.Nickname
} }

View File

@ -2,6 +2,7 @@ package repository
import ( import (
"errors" "errors"
"strings"
"time" "time"
"github.com/topfans/backend/pkg/models" "github.com/topfans/backend/pkg/models"
@ -514,7 +515,7 @@ func (r *galleryRepository) CountValidExhibitions(starID int64, materialType str
if materialType != "" && materialType != "all" && materialType != "random" { if materialType != "" && materialType != "all" && materialType != "random" {
query = query.Joins("JOIN assets a ON a.id = exhibitions.asset_id"). 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 err := query.Count(&count).Error
@ -532,7 +533,7 @@ func (r *galleryRepository) GetRandomExhibitions(starID int64, materialType stri
if materialType != "" && materialType != "all" && materialType != "random" { if materialType != "" && materialType != "all" && materialType != "random" {
baseQuery = baseQuery.Joins("JOIN assets a ON a.id = exhibitions.asset_id"). baseQuery = baseQuery.Joins("JOIN assets a ON a.id = exhibitions.asset_id").
Where("a.material_type = ?", materialType) Where("a.material_type LIKE ?", "%"+materialType+"%")
} }
// 排除已展示的ID // 排除已展示的ID
@ -568,26 +569,42 @@ func (r *galleryRepository) GetRandomExhibitions(starID int64, materialType stri
return nil, err return nil, err
} }
// Read-repair: 检查并修正 material_type查询时自动修正数据不一致 // Read-repair: 检查并补充 material_type多类型共存不降级
for _, item := range items { for _, item := range items {
item.Span = calcSpanByLikes(item.LikeCount) item.Span = calcSpanByLikes(item.LikeCount)
correctType := calcMaterialType(item.LikeCount, item.CreatedAt) correctTypes := calcMaterialTypes(item.LikeCount, item.CreatedAt)
if item.MaterialType != correctType {
// 异步修正,不阻塞返回(使用 goroutine 避免影响响应时间) // 如果缺少任何类型,追加(不覆盖原有类型)
go func(assetID int64, correct string) { 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{}). r.db.Model(&models.Asset{}).
Where("id = ?", assetID). Where("id = ?", assetID).
Update("material_type", correct) Update("material_type", gorm.Expr("CONCAT(COALESCE(material_type, ''), ',', ?)", newType))
}(item.AssetID, correctType) }(item.AssetID, newType)
// 同时修正当前对象的值,保证返回给前端的是正确的
item.MaterialType = correctType // 修正当前返回对象
if item.MaterialType == "" {
item.MaterialType = correctTypes
} else {
item.MaterialType = item.MaterialType + "," + strings.Join(missingTypes, ",")
}
} }
} }
return items, nil return items, nil
} }
// calcMaterialType 根据点赞数计算素材类型 // calcMaterialType 根据点赞数计算素材类型(单类型,互斥)
func calcMaterialType(likes int32, createdAt int64) string { func calcMaterialType(likes int32, createdAt int64) string {
now := time.Now().UnixMilli() now := time.Now().UnixMilli()
isWithin1Hour := createdAt > 0 && now-createdAt < 3600*1000 isWithin1Hour := createdAt > 0 && now-createdAt < 3600*1000
@ -602,6 +619,27 @@ func calcMaterialType(likes int32, createdAt int64) string {
return "new" 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 根据点赞数计算卡片大小 // calcSpanByLikes 根据点赞数计算卡片大小
// 0-30 → span 1, 31-100 → span 2, 101-200 → span 3, 200+ → span 4 // 0-30 → span 1, 31-100 → span 2, 101-200 → span 3, 200+ → span 4
func calcSpanByLikes(likes int32) int32 { func calcSpanByLikes(likes int32) int32 {

View File

@ -1012,7 +1012,7 @@ onUnmounted(() => {
width: 352rpx; width: 352rpx;
height: 520rpx; height: 520rpx;
margin-bottom: 32rpx; 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; transform-origin: center center;
} }