package service import ( "sort" appErrors "github.com/topfans/backend/pkg/errors" "github.com/topfans/backend/pkg/models" pb "github.com/topfans/backend/pkg/proto/starbook" assetRepo "github.com/topfans/backend/services/assetService/repository" starbookRepo "github.com/topfans/backend/services/starbookService/repository" "gorm.io/gorm" ) // StarbookService 星册服务接口 type StarbookService interface { // GetStarbookHome 获取星册首页数据 GetStarbookHome(ownerUID, starID int64) (*pb.GetStarbookHomeResponse, error) // GetStarbookItems 获取星册藏品列表(分页) GetStarbookItems(req *pb.GetStarbookItemsRequest, ownerUID, starID int64) (*pb.GetStarbookItemsResponse, error) } // starbookService 星册服务实现 type starbookService struct { db *gorm.DB registryRepo starbookRepo.AssetRegistryRepository assetRepo assetRepo.AssetRepository collectionRepo starbookRepo.CollectionRepository activityRepo starbookRepo.ActivityAssetRepository } // NewStarbookService 创建星册服务实例 func NewStarbookService( db *gorm.DB, registryRepo starbookRepo.AssetRegistryRepository, assetRepo assetRepo.AssetRepository, collectionRepo starbookRepo.CollectionRepository, activityRepo starbookRepo.ActivityAssetRepository, ) StarbookService { return &starbookService{ db: db, registryRepo: registryRepo, assetRepo: assetRepo, collectionRepo: collectionRepo, activityRepo: activityRepo, } } // 常量 const ( HomePageSize = 3 // 首页每组最多显示数量 CastloveCategory = "castlove" CategoryNameRegular = "原创" CategoryNameCollection = "典藏" CategoryNameActivity = "活动" ) // GetStarbookHome 获取星册首页数据 func (s *starbookService) GetStarbookHome(ownerUID, starID int64) (*pb.GetStarbookHomeResponse, error) { // 1. 查询所有索引记录 registries, err := s.registryRepo.GetByOwner(ownerUID, starID) if err != nil { return nil, err } // 2. 按 type 分组 typeGroups := make(map[string][]*models.AssetRegistry) for _, reg := range registries { typeGroups[reg.AssetType] = append(typeGroups[reg.AssetType], reg) } // 3. 构建响应 groups := make([]*pb.AssetGroup, 0) // 处理原创藏品 (regular) if regs, ok := typeGroups[models.AssetTypeRegular]; ok { group := s.buildRegularGroup(ownerUID, starID, regs) if group != nil { groups = append(groups, group) } } // 处理典藏藏品 (collection) if regs, ok := typeGroups[models.AssetTypeCollection]; ok { group := s.buildCollectionGroup(ownerUID, starID, regs) if group != nil { groups = append(groups, group) } } // 处理活动藏品 (activity) if regs, ok := typeGroups[models.AssetTypeActivity]; ok { group := s.buildActivityGroup(ownerUID, starID, regs) if group != nil { groups = append(groups, group) } } return &pb.GetStarbookHomeResponse{ Data: &pb.StarbookHomeData{ Groups: groups, }, }, nil } // buildRegularGroup 构建原创藏品分组 func (s *starbookService) buildRegularGroup(ownerUID, starID int64, registries []*models.AssetRegistry) *pb.AssetGroup { // 按 grade 分组 gradeGroups := make(map[int32][]*models.AssetRegistry) for _, reg := range registries { if reg.Grade != nil { gradeGroups[*reg.Grade] = append(gradeGroups[*reg.Grade], reg) } } // 构建 GradeSection grades := make([]*pb.GradeSection, 0) for grade, regs := range gradeGroups { // 排序:按点赞数降序,保留前3 sort.Slice(regs, func(i, j int) bool { return regs[i].LikeCount > regs[j].LikeCount }) // 截取前3条(点赞数最高的3个) displayRegs := regs hasMore := false if len(regs) > HomePageSize { displayRegs = regs[:HomePageSize] hasMore = true } // 获取资产详情并生成预签名URL items := s.buildAssetItemsFromRegistries(displayRegs, models.AssetTypeRegular) gradeSection := &pb.GradeSection{ Grade: grade, Items: items, TotalCount: int32(len(regs)), HasMore: hasMore, } grades = append(grades, gradeSection) } // 按 grade 降序排序 sort.Slice(grades, func(i, j int) bool { return grades[i].Grade > grades[j].Grade }) // 计算 total_count 和 has_more totalCount := int32(0) hasMore := false for _, g := range grades { totalCount += g.TotalCount if g.HasMore { hasMore = true } } return &pb.AssetGroup{ Type: models.AssetTypeRegular, Category: CastloveCategory, CategoryName: CategoryNameRegular, Grades: grades, TotalCount: totalCount, HasMore: hasMore, } } // buildCollectionGroup 构建典藏藏品分组 func (s *starbookService) buildCollectionGroup(ownerUID, starID int64, registries []*models.AssetRegistry) *pb.AssetGroup { // 按 category 分组 categoryGroups := make(map[string][]*models.AssetRegistry) for _, reg := range registries { if reg.CollectionCategory != nil && *reg.CollectionCategory != "" { categoryGroups[*reg.CollectionCategory] = append(categoryGroups[*reg.CollectionCategory], reg) } } // 构建 AssetGroup items allItems := make([]*pb.AssetItem, 0) for category, regs := range categoryGroups { // 排序:按创建时间降序 sort.Slice(regs, func(i, j int) bool { return regs[i].CreatedAt > regs[j].CreatedAt }) // 截取前3条(点赞数最高的3个) displayRegs := regs if len(regs) > HomePageSize { displayRegs = regs[:HomePageSize] } // 获取资产详情 items := s.buildAssetItemsFromRegistries(displayRegs, models.AssetTypeCollection) for _, item := range items { item.Category = category } allItems = append(allItems, items...) } // 计算 total_count 和 has_more totalCount := int32(len(registries)) hasMore := len(registries) > HomePageSize return &pb.AssetGroup{ Type: models.AssetTypeCollection, Category: "", CategoryName: CategoryNameCollection, Items: allItems, TotalCount: totalCount, HasMore: hasMore, } } // buildActivityGroup 构建活动藏品分组 func (s *starbookService) buildActivityGroup(ownerUID, starID int64, registries []*models.AssetRegistry) *pb.AssetGroup { // 按 activity_type 分组 typeGroups := make(map[string][]*models.AssetRegistry) for _, reg := range registries { if reg.ActivityType != nil && *reg.ActivityType != "" { typeGroups[*reg.ActivityType] = append(typeGroups[*reg.ActivityType], reg) } } // 构建 AssetGroup items allItems := make([]*pb.AssetItem, 0) for activityType, regs := range typeGroups { // 排序:按创建时间降序 sort.Slice(regs, func(i, j int) bool { return regs[i].CreatedAt > regs[j].CreatedAt }) // 截取前3条(点赞数最高的3个) displayRegs := regs if len(regs) > HomePageSize { displayRegs = regs[:HomePageSize] } // 获取资产详情 items := s.buildAssetItemsFromRegistries(displayRegs, models.AssetTypeActivity) for _, item := range items { item.Category = activityType } allItems = append(allItems, items...) } // 计算 total_count 和 has_more totalCount := int32(len(registries)) hasMore := len(registries) > HomePageSize return &pb.AssetGroup{ Type: models.AssetTypeActivity, Category: "", CategoryName: CategoryNameActivity, Items: allItems, TotalCount: totalCount, HasMore: hasMore, } } // buildAssetItemsFromRegistries 从索引记录构建资产项(使用批量查询优化) func (s *starbookService) buildAssetItemsFromRegistries(registries []*models.AssetRegistry, assetType 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, LikeCount: reg.LikeCount, CreatedAt: reg.CreatedAt, Category: CastloveCategory, Grade: 0, DisplayStatus: reg.DisplayStatus, } // grade 处理 if assetType == models.AssetTypeRegular && reg.Grade != nil { item.Grade = *reg.Grade } // 填充资产信息 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) } return items } // GetStarbookItems 获取星册藏品列表(分页) func (s *starbookService) GetStarbookItems(req *pb.GetStarbookItemsRequest, ownerUID, starID int64) (*pb.GetStarbookItemsResponse, error) { // 参数验证 if req.Type == "" { return nil, appErrors.ErrInvalidAssetType } assetType := req.Type page := req.Page if page <= 0 { page = 1 } pageSize := req.PageSize if pageSize <= 0 { pageSize = 20 } offset := (page - 1) * pageSize var registries []*models.AssetRegistry var totalCount int64 var err error switch assetType { case models.AssetTypeRegular: grade := req.Grade if grade <= 0 { grade = 1 } registries, err = s.registryRepo.GetByOwnerAndTypeAndGrade(ownerUID, starID, assetType, grade, int(pageSize), int(offset)) if err != nil { return nil, err } totalCount, err = s.registryRepo.CountByOwnerAndTypeAndGrade(ownerUID, starID, assetType, grade) if err != nil { return nil, err } case models.AssetTypeCollection: category := req.Category if category == "" { category = CastloveCategory } registries, err = s.registryRepo.GetByOwnerAndTypeAndCategory(ownerUID, starID, assetType, category, int(pageSize), int(offset)) if err != nil { return nil, err } totalCount, err = s.registryRepo.CountByOwnerAndTypeAndCategory(ownerUID, starID, assetType, category) if err != nil { return nil, err } case models.AssetTypeActivity: category := req.Category if category == "" { category = CastloveCategory } registries, err = s.registryRepo.GetByOwnerAndTypeAndCategory(ownerUID, starID, assetType, category, int(pageSize), int(offset)) if err != nil { return nil, err } totalCount, err = s.registryRepo.CountByOwnerAndTypeAndCategory(ownerUID, starID, assetType, category) if err != nil { return nil, err } default: return nil, appErrors.ErrInvalidAssetType } // 构建 items items := s.buildAssetItemsFromRegistries(registries, assetType) hasMore := int64(page*pageSize) < totalCount return &pb.GetStarbookItemsResponse{ Data: &pb.AssetListData{ Items: items, Total: totalCount, Page: page, PageSize: pageSize, HasMore: hasMore, }, }, nil }