492 lines
14 KiB
Go
492 lines
14 KiB
Go
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
|
||
}
|