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" 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 { items := make([]*pb.AssetItem, 0) coverURLs := make([]string, 0) assetMap := make(map[string]*pb.AssetItem) for _, reg := range registries { item := &pb.AssetItem{ AssetId: reg.AssetID, LikeCount: reg.LikeCount, CreatedAt: reg.CreatedAt, Category: CastloveCategory, Grade: 0, } // grade 处理 if assetType == models.AssetTypeRegular && reg.Grade != nil { 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 } } } 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 } // 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 } // 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 }