topfans/backend/services/starbookService/service/starbook_service.go
2026-04-20 16:00:10 +08:00

492 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}