751 lines
21 KiB
Go
751 lines
21 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"sort"
|
||
"time"
|
||
|
||
appErrors "github.com/topfans/backend/pkg/errors"
|
||
"github.com/topfans/backend/pkg/logger"
|
||
"github.com/topfans/backend/pkg/models"
|
||
pb "github.com/topfans/backend/pkg/proto/asset"
|
||
pbCommon "github.com/topfans/backend/pkg/proto/common"
|
||
"github.com/topfans/backend/pkg/validator"
|
||
"github.com/topfans/backend/services/assetService/client"
|
||
"github.com/topfans/backend/services/assetService/repository"
|
||
"go.uber.org/zap"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// AssetService 资产服务接口
|
||
type AssetService interface {
|
||
// GetMyAssets 获取我的藏品列表
|
||
GetMyAssets(req *pb.GetMyAssetsRequest, userID, starID int64) (*pb.GetMyAssetsResponse, error)
|
||
|
||
// GetAsset 获取资产详情
|
||
GetAsset(req *pb.GetAssetRequest, userID, starID int64) (*pb.GetAssetResponse, error)
|
||
|
||
// GetAssetStatus 查询上链状态
|
||
GetAssetStatus(req *pb.GetAssetStatusRequest, userID, starID int64) (*pb.GetAssetStatusResponse, error)
|
||
|
||
// GetAssetForRPC 获取资产信息(内部RPC调用)
|
||
GetAssetForRPC(req *pb.GetAssetForRPCRequest) (*pb.GetAssetForRPCResponse, error)
|
||
}
|
||
|
||
// RegistryRepository 资产注册表Repository接口(用于分组查询)
|
||
type RegistryRepository interface {
|
||
// GetByOwner 查询用户的注册记录
|
||
GetByOwner(ownerUID, starID int64) ([]*models.AssetRegistry, error)
|
||
}
|
||
|
||
// assetService 资产服务实现
|
||
type assetService struct {
|
||
assetRepo repository.AssetRepository
|
||
mintOrderRepo repository.MintOrderRepository
|
||
assetLikeRepo repository.AssetLikeRepository
|
||
userClient client.UserServiceClient
|
||
db *gorm.DB
|
||
registryRepo RegistryRepository
|
||
}
|
||
|
||
// NewAssetService 创建资产服务实例
|
||
func NewAssetService(
|
||
assetRepo repository.AssetRepository,
|
||
mintOrderRepo repository.MintOrderRepository,
|
||
assetLikeRepo repository.AssetLikeRepository,
|
||
userClient client.UserServiceClient,
|
||
db *gorm.DB,
|
||
registryRepo RegistryRepository,
|
||
) AssetService {
|
||
return &assetService{
|
||
assetRepo: assetRepo,
|
||
mintOrderRepo: mintOrderRepo,
|
||
assetLikeRepo: assetLikeRepo,
|
||
userClient: userClient,
|
||
db: db,
|
||
registryRepo: registryRepo,
|
||
}
|
||
}
|
||
|
||
// GetMyAssets 获取我的藏品列表(分组格式,与星册home一致)
|
||
func (s *assetService) GetMyAssets(req *pb.GetMyAssetsRequest, userID, starID int64) (*pb.GetMyAssetsResponse, error) {
|
||
// 1. 参数验证
|
||
if !validator.ValidateUserID(userID) {
|
||
logger.Logger.Warn("Invalid user_id",
|
||
zap.Int64("user_id", userID),
|
||
)
|
||
return nil, appErrors.ErrInvalidUserID
|
||
}
|
||
|
||
if !validator.ValidateStarID(starID) {
|
||
logger.Logger.Warn("Invalid star_id",
|
||
zap.Int64("star_id", starID),
|
||
)
|
||
return nil, appErrors.ErrInvalidStarID
|
||
}
|
||
|
||
// 设置默认分页参数
|
||
page := req.Page
|
||
if page <= 0 {
|
||
page = 1
|
||
}
|
||
|
||
pageSize := req.PageSize
|
||
if pageSize <= 0 {
|
||
pageSize = 20
|
||
}
|
||
if pageSize > 100 {
|
||
pageSize = 100
|
||
}
|
||
|
||
// 2. 查询注册记录(用于获取类型和展示状态)
|
||
var registries []*models.AssetRegistry
|
||
if s.registryRepo != nil {
|
||
regs, err := s.registryRepo.GetByOwner(userID, starID)
|
||
if err != nil {
|
||
logger.Logger.Error("Failed to get registries",
|
||
zap.Int64("user_id", userID),
|
||
zap.Int64("star_id", starID),
|
||
zap.Error(err),
|
||
)
|
||
// 继续执行,使用空注册列表
|
||
registries = []*models.AssetRegistry{}
|
||
} else {
|
||
registries = regs
|
||
}
|
||
} else {
|
||
registries = []*models.AssetRegistry{}
|
||
}
|
||
|
||
// 3. 创建 assetID -> registry 映射
|
||
registryMap := make(map[int64]*models.AssetRegistry)
|
||
for _, reg := range registries {
|
||
registryMap[reg.AssetID] = reg
|
||
}
|
||
|
||
// 4. 查询资产列表(不分页,获取所有资产用于分组)
|
||
allAssets, err := s.assetRepo.GetByOwner(userID, starID, 1000, 0) // 最多获取1000个
|
||
if err != nil {
|
||
logger.Logger.Error("Failed to get assets",
|
||
zap.Int64("user_id", userID),
|
||
zap.Int64("star_id", starID),
|
||
zap.Error(err),
|
||
)
|
||
return nil, fmt.Errorf("failed to get assets: %w", err)
|
||
}
|
||
|
||
// 5. 按 type 分组
|
||
typeGroups := make(map[string][]*models.AssetRegistry)
|
||
for _, reg := range registries {
|
||
typeGroups[reg.AssetType] = append(typeGroups[reg.AssetType], reg)
|
||
}
|
||
|
||
// 6. 构建响应分组
|
||
groups := make([]*pb.AssetGroup, 0)
|
||
|
||
// 处理原创藏品 (regular)
|
||
if regs, ok := typeGroups[models.AssetTypeRegular]; ok {
|
||
group := s.buildRegularGroupForAssets(allAssets, registryMap, regs)
|
||
if group != nil {
|
||
groups = append(groups, group)
|
||
}
|
||
}
|
||
|
||
// 处理典藏藏品 (collection)
|
||
if regs, ok := typeGroups[models.AssetTypeCollection]; ok {
|
||
group := s.buildCollectionGroupForAssets(allAssets, registryMap, regs)
|
||
if group != nil {
|
||
groups = append(groups, group)
|
||
}
|
||
}
|
||
|
||
// 处理活动藏品 (activity)
|
||
if regs, ok := typeGroups[models.AssetTypeActivity]; ok {
|
||
group := s.buildActivityGroupForAssets(allAssets, registryMap, regs)
|
||
if group != nil {
|
||
groups = append(groups, group)
|
||
}
|
||
}
|
||
|
||
// 7. 查询总数
|
||
total, err := s.assetRepo.CountByOwner(userID, starID)
|
||
if err != nil {
|
||
logger.Logger.Error("Failed to count assets",
|
||
zap.Int64("user_id", userID),
|
||
zap.Int64("star_id", starID),
|
||
zap.Error(err),
|
||
)
|
||
return nil, fmt.Errorf("failed to count assets: %w", err)
|
||
}
|
||
|
||
hasMore := (page * pageSize) < int32(total)
|
||
|
||
// 8. 构建响应
|
||
response := &pb.GetMyAssetsResponse{
|
||
Base: &pbCommon.BaseResponse{
|
||
Code: pbCommon.StatusCode_STATUS_OK,
|
||
Message: "",
|
||
Timestamp: time.Now().UnixMilli(),
|
||
},
|
||
Data: &pb.AssetListData{
|
||
Groups: groups,
|
||
Total: total,
|
||
Page: page,
|
||
PageSize: pageSize,
|
||
HasMore: hasMore,
|
||
},
|
||
}
|
||
|
||
logger.Logger.Debug("Get my assets successful",
|
||
zap.Int64("user_id", userID),
|
||
zap.Int64("star_id", starID),
|
||
zap.Int("asset_count", len(allAssets)),
|
||
zap.Int64("total", total),
|
||
)
|
||
|
||
return response, nil
|
||
}
|
||
|
||
// buildRegularGroupForAssets 构建原创藏品分组
|
||
func (s *assetService) buildRegularGroupForAssets(allAssets []*models.Asset, registryMap map[int64]*models.AssetRegistry, registries []*models.AssetRegistry) *pb.AssetGroup {
|
||
// 创建 assetID -> asset 映射
|
||
assetMap := make(map[int64]*models.Asset)
|
||
for _, asset := range allAssets {
|
||
assetMap[asset.ID] = asset
|
||
}
|
||
|
||
// 按 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 {
|
||
// 构建 items(返回所有,不限制数量)
|
||
items := make([]*pb.AssetItem, 0)
|
||
for _, reg := range regs {
|
||
asset := assetMap[reg.AssetID]
|
||
if asset == nil {
|
||
continue
|
||
}
|
||
item := &pb.AssetItem{
|
||
AssetId: asset.ID,
|
||
Name: asset.Name,
|
||
CoverUrlSigned: asset.CoverURL,
|
||
LikeCount: asset.LikeCount,
|
||
CreatedAt: asset.CreatedAt,
|
||
Category: "castlove",
|
||
Grade: grade,
|
||
DisplayStatus: reg.DisplayStatus,
|
||
}
|
||
items = append(items, item)
|
||
}
|
||
|
||
gradeSection := &pb.GradeSection{
|
||
Grade: grade,
|
||
Items: items,
|
||
TotalCount: int32(len(regs)),
|
||
HasMore: false, // 返回所有,不限制
|
||
}
|
||
grades = append(grades, gradeSection)
|
||
}
|
||
|
||
// 按 grade 降序排序
|
||
sort.Slice(grades, func(i, j int) bool {
|
||
return grades[i].Grade > grades[j].Grade
|
||
})
|
||
|
||
// 计算 total_count
|
||
totalCount := int32(0)
|
||
for _, g := range grades {
|
||
totalCount += g.TotalCount
|
||
}
|
||
|
||
return &pb.AssetGroup{
|
||
Type: models.AssetTypeRegular,
|
||
Category: "castlove",
|
||
CategoryName: "原创",
|
||
Grades: grades,
|
||
TotalCount: totalCount,
|
||
HasMore: false,
|
||
}
|
||
}
|
||
|
||
// buildCollectionGroupForAssets 构建典藏藏品分组
|
||
func (s *assetService) buildCollectionGroupForAssets(allAssets []*models.Asset, registryMap map[int64]*models.AssetRegistry, registries []*models.AssetRegistry) *pb.AssetGroup {
|
||
// 创建 assetID -> asset 映射
|
||
assetMap := make(map[int64]*models.Asset)
|
||
for _, asset := range allAssets {
|
||
assetMap[asset.ID] = asset
|
||
}
|
||
|
||
// 构建 items
|
||
items := make([]*pb.AssetItem, 0)
|
||
for _, reg := range registries {
|
||
asset := assetMap[reg.AssetID]
|
||
if asset == nil {
|
||
continue
|
||
}
|
||
category := ""
|
||
if reg.CollectionCategory != nil {
|
||
category = *reg.CollectionCategory
|
||
}
|
||
item := &pb.AssetItem{
|
||
AssetId: asset.ID,
|
||
Name: asset.Name,
|
||
CoverUrlSigned: asset.CoverURL,
|
||
LikeCount: asset.LikeCount,
|
||
CreatedAt: asset.CreatedAt,
|
||
Category: category,
|
||
Grade: 0,
|
||
DisplayStatus: reg.DisplayStatus,
|
||
}
|
||
items = append(items, item)
|
||
}
|
||
|
||
return &pb.AssetGroup{
|
||
Type: models.AssetTypeCollection,
|
||
Category: "",
|
||
CategoryName: "典藏",
|
||
Items: items,
|
||
TotalCount: int32(len(registries)),
|
||
HasMore: false,
|
||
}
|
||
}
|
||
|
||
// buildActivityGroupForAssets 构建活动藏品分组
|
||
func (s *assetService) buildActivityGroupForAssets(allAssets []*models.Asset, registryMap map[int64]*models.AssetRegistry, registries []*models.AssetRegistry) *pb.AssetGroup {
|
||
// 创建 assetID -> asset 映射
|
||
assetMap := make(map[int64]*models.Asset)
|
||
for _, asset := range allAssets {
|
||
assetMap[asset.ID] = asset
|
||
}
|
||
|
||
// 构建 items
|
||
items := make([]*pb.AssetItem, 0)
|
||
for _, reg := range registries {
|
||
asset := assetMap[reg.AssetID]
|
||
if asset == nil {
|
||
continue
|
||
}
|
||
activityType := ""
|
||
if reg.ActivityType != nil {
|
||
activityType = *reg.ActivityType
|
||
}
|
||
item := &pb.AssetItem{
|
||
AssetId: asset.ID,
|
||
Name: asset.Name,
|
||
CoverUrlSigned: asset.CoverURL,
|
||
LikeCount: asset.LikeCount,
|
||
CreatedAt: asset.CreatedAt,
|
||
Category: activityType,
|
||
Grade: 0,
|
||
DisplayStatus: reg.DisplayStatus,
|
||
}
|
||
items = append(items, item)
|
||
}
|
||
|
||
return &pb.AssetGroup{
|
||
Type: models.AssetTypeActivity,
|
||
Category: "",
|
||
CategoryName: "活动",
|
||
Items: items,
|
||
TotalCount: int32(len(registries)),
|
||
HasMore: false,
|
||
}
|
||
}
|
||
|
||
// GetAsset 获取资产详情
|
||
func (s *assetService) GetAsset(req *pb.GetAssetRequest, userID, starID int64) (*pb.GetAssetResponse, error) {
|
||
// 1. 参数验证
|
||
if !validator.ValidateUserID(userID) {
|
||
logger.Logger.Warn("Invalid user_id",
|
||
zap.Int64("user_id", userID),
|
||
)
|
||
return nil, appErrors.ErrInvalidUserID
|
||
}
|
||
|
||
if !validator.ValidateStarID(starID) {
|
||
logger.Logger.Warn("Invalid star_id",
|
||
zap.Int64("star_id", starID),
|
||
)
|
||
return nil, appErrors.ErrInvalidStarID
|
||
}
|
||
|
||
if req.AssetId <= 0 {
|
||
logger.Logger.Warn("Invalid asset_id",
|
||
zap.Int64("asset_id", req.AssetId),
|
||
)
|
||
return nil, fmt.Errorf("invalid asset_id: %d", req.AssetId)
|
||
}
|
||
|
||
// 2. 查询资产(验证所有权)
|
||
// 首先尝试通过所有者查询(验证是否为资产所有者)
|
||
asset, err := s.assetRepo.GetByIDAndOwner(req.AssetId, userID, starID)
|
||
if err != nil {
|
||
if errors.Is(err, appErrors.ErrAssetNotFound) {
|
||
// 所有者验证失败,检查资产是否正在展出中
|
||
// 如果正在展出,则允许访问(即使不是所有者)
|
||
isExhibiting, checkErr := s.assetRepo.IsExhibiting(req.AssetId)
|
||
if checkErr != nil {
|
||
logger.Logger.Warn("Failed to check exhibition status",
|
||
zap.Int64("asset_id", req.AssetId),
|
||
zap.Error(checkErr),
|
||
)
|
||
}
|
||
if isExhibiting {
|
||
// 资产正在展出中,通过 ID 查询(不验证所有权)
|
||
asset, err = s.assetRepo.GetByID(req.AssetId)
|
||
if err != nil {
|
||
if errors.Is(err, appErrors.ErrAssetNotFound) {
|
||
logger.Logger.Warn("Asset not found",
|
||
zap.Int64("asset_id", req.AssetId),
|
||
)
|
||
return nil, appErrors.ErrAssetNotFound
|
||
}
|
||
logger.Logger.Error("Failed to get asset by ID",
|
||
zap.Int64("asset_id", req.AssetId),
|
||
zap.Error(err),
|
||
)
|
||
return nil, fmt.Errorf("failed to get asset: %w", err)
|
||
}
|
||
logger.Logger.Info("Allowing access to exhibited asset",
|
||
zap.Int64("asset_id", req.AssetId),
|
||
zap.Int64("user_id", userID),
|
||
zap.Int64("star_id", starID),
|
||
)
|
||
} else {
|
||
logger.Logger.Warn("Asset not found or access denied",
|
||
zap.Int64("asset_id", req.AssetId),
|
||
zap.Int64("user_id", userID),
|
||
zap.Int64("star_id", starID),
|
||
)
|
||
return nil, appErrors.ErrAssetAccessDenied
|
||
}
|
||
} else {
|
||
logger.Logger.Error("Failed to get asset",
|
||
zap.Int64("asset_id", req.AssetId),
|
||
zap.Error(err),
|
||
)
|
||
return nil, fmt.Errorf("failed to get asset: %w", err)
|
||
}
|
||
}
|
||
|
||
// 3. 获取所有者的昵称(在该star下的nickname)
|
||
var ownerNickname string
|
||
profile, err := s.userClient.GetFanProfile(context.Background(), asset.OwnerUID, asset.StarID)
|
||
if err != nil {
|
||
logger.Logger.Warn("Failed to get owner fan profile, will return without nickname",
|
||
zap.Int64("owner_uid", asset.OwnerUID),
|
||
zap.Int64("star_id", asset.StarID),
|
||
zap.Error(err),
|
||
)
|
||
// 获取失败时,使用空字符串
|
||
ownerNickname = ""
|
||
} else {
|
||
ownerNickname = profile.Nickname
|
||
}
|
||
|
||
// 4. 检查当前用户是否已点赞
|
||
isLiked, err := s.assetLikeRepo.Exists(asset.ID, userID, starID)
|
||
if err != nil {
|
||
logger.Logger.Warn("Failed to check like status, will return is_liked as false",
|
||
zap.Int64("asset_id", asset.ID),
|
||
zap.Int64("user_id", userID),
|
||
zap.Int64("star_id", starID),
|
||
zap.Error(err),
|
||
)
|
||
// 检查失败时,默认为未点赞
|
||
isLiked = false
|
||
}
|
||
|
||
// 5. 从 asset_registry 表获取 display_status
|
||
displayStatus, err := s.assetRepo.GetDisplayStatusByAssetID(asset.ID)
|
||
if err != nil {
|
||
logger.Logger.Warn("Failed to get display status, will return 0",
|
||
zap.Int64("asset_id", asset.ID),
|
||
zap.Error(err),
|
||
)
|
||
displayStatus = 0
|
||
}
|
||
|
||
// 6. 计算当前展出收益和过期时间(仅展出中时有值)
|
||
earnings := int64(0)
|
||
exhibitionExpireAt := int64(0)
|
||
if displayStatus == 1 {
|
||
// 获取展出开始时间,用于计算展出时长
|
||
exhibitionStartTime, _ := s.assetRepo.GetExhibitionStartTime(asset.ID)
|
||
if exhibitionStartTime == 0 {
|
||
exhibitionStartTime = asset.CreatedAt // 兜底
|
||
}
|
||
earnings = calculateRealtimeEarnings(asset.LikeCount, exhibitionStartTime, time.Now().UnixMilli())
|
||
|
||
// 获取展出过期时间
|
||
exhibitionExpireAt, _ = s.assetRepo.GetExhibitionExpireTime(asset.ID)
|
||
}
|
||
|
||
// 7. 构建响应
|
||
response := &pb.GetAssetResponse{
|
||
Base: &pbCommon.BaseResponse{
|
||
Code: pbCommon.StatusCode_STATUS_OK,
|
||
Message: "",
|
||
Timestamp: time.Now().UnixMilli(),
|
||
},
|
||
Asset: ModelToProtoAssetDetail(asset, ownerNickname, isLiked, displayStatus, earnings, exhibitionExpireAt),
|
||
}
|
||
|
||
logger.Logger.Debug("Get asset successful",
|
||
zap.Int64("asset_id", req.AssetId),
|
||
zap.Int64("user_id", userID),
|
||
zap.String("owner_nickname", ownerNickname),
|
||
zap.Bool("is_liked", isLiked),
|
||
)
|
||
|
||
return response, nil
|
||
}
|
||
|
||
// GetAssetStatus 查询上链状态
|
||
func (s *assetService) GetAssetStatus(req *pb.GetAssetStatusRequest, userID, starID int64) (*pb.GetAssetStatusResponse, error) {
|
||
// 1. 参数验证
|
||
if !validator.ValidateUserID(userID) {
|
||
logger.Logger.Warn("Invalid user_id",
|
||
zap.Int64("user_id", userID),
|
||
)
|
||
return nil, appErrors.ErrInvalidUserID
|
||
}
|
||
|
||
if !validator.ValidateStarID(starID) {
|
||
logger.Logger.Warn("Invalid star_id",
|
||
zap.Int64("star_id", starID),
|
||
)
|
||
return nil, appErrors.ErrInvalidStarID
|
||
}
|
||
|
||
if req.AssetId <= 0 {
|
||
logger.Logger.Warn("Invalid asset_id",
|
||
zap.Int64("asset_id", req.AssetId),
|
||
)
|
||
return nil, fmt.Errorf("invalid asset_id: %d", req.AssetId)
|
||
}
|
||
|
||
// 2. 查询资产(验证所有权)
|
||
asset, err := s.assetRepo.GetByIDAndOwner(req.AssetId, userID, starID)
|
||
if err != nil {
|
||
if errors.Is(err, appErrors.ErrAssetNotFound) {
|
||
logger.Logger.Warn("Asset not found or access denied",
|
||
zap.Int64("asset_id", req.AssetId),
|
||
zap.Int64("user_id", userID),
|
||
zap.Int64("star_id", starID),
|
||
)
|
||
return nil, appErrors.ErrAssetAccessDenied
|
||
}
|
||
logger.Logger.Error("Failed to get asset",
|
||
zap.Int64("asset_id", req.AssetId),
|
||
zap.Error(err),
|
||
)
|
||
return nil, fmt.Errorf("failed to get asset: %w", err)
|
||
}
|
||
|
||
// 3. 构建响应
|
||
response := &pb.GetAssetStatusResponse{
|
||
Base: &pbCommon.BaseResponse{
|
||
Code: pbCommon.StatusCode_STATUS_OK,
|
||
Message: "",
|
||
Timestamp: time.Now().UnixMilli(),
|
||
},
|
||
AssetId: asset.ID,
|
||
Status: getStatusString(asset.Status),
|
||
TxHash: getStringValue(asset.TxHash),
|
||
BlockNumber: getInt64Value(asset.BlockNumber),
|
||
MintedAt: getInt64Value(asset.MintedAt),
|
||
}
|
||
|
||
logger.Logger.Debug("Get asset status successful",
|
||
zap.Int64("asset_id", req.AssetId),
|
||
zap.Int32("status", asset.Status),
|
||
)
|
||
|
||
return response, nil
|
||
}
|
||
|
||
// GetAssetForRPC 获取资产信息(内部RPC调用,供Social Service使用)
|
||
func (s *assetService) GetAssetForRPC(req *pb.GetAssetForRPCRequest) (*pb.GetAssetForRPCResponse, error) {
|
||
// 1. 参数验证
|
||
if req.AssetId <= 0 {
|
||
logger.Logger.Warn("Invalid asset_id in RPC call",
|
||
zap.Int64("asset_id", req.AssetId),
|
||
)
|
||
return &pb.GetAssetForRPCResponse{
|
||
Base: appErrors.BuildBaseResponse(fmt.Errorf("invalid asset_id")),
|
||
}, nil
|
||
}
|
||
|
||
// 2. 查询资产
|
||
asset, err := s.assetRepo.GetByID(req.AssetId)
|
||
if err != nil {
|
||
if errors.Is(err, appErrors.ErrAssetNotFound) {
|
||
logger.Logger.Warn("Asset not found in RPC call",
|
||
zap.Int64("asset_id", req.AssetId),
|
||
)
|
||
return &pb.GetAssetForRPCResponse{
|
||
Base: appErrors.BuildBaseResponse(appErrors.ErrAssetNotFound),
|
||
}, nil
|
||
}
|
||
logger.Logger.Error("Failed to get asset in RPC call",
|
||
zap.Int64("asset_id", req.AssetId),
|
||
zap.Error(err),
|
||
)
|
||
return &pb.GetAssetForRPCResponse{
|
||
Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
|
||
}, nil
|
||
}
|
||
|
||
// 3. 构建响应
|
||
response := &pb.GetAssetForRPCResponse{
|
||
Base: &pbCommon.BaseResponse{
|
||
Code: pbCommon.StatusCode_STATUS_OK,
|
||
Message: "",
|
||
Timestamp: time.Now().UnixMilli(),
|
||
},
|
||
AssetId: asset.ID,
|
||
OwnerUid: asset.OwnerUID,
|
||
StarId: asset.StarID,
|
||
}
|
||
|
||
logger.Logger.Debug("GetAssetForRPC successful",
|
||
zap.Int64("asset_id", req.AssetId),
|
||
)
|
||
|
||
return response, nil
|
||
}
|
||
|
||
// ========== 辅助函数 ==========
|
||
|
||
// ModelToProtoAsset 将数据库模型转换为Proto格式(AssetListItem)
|
||
func ModelToProtoAsset(asset *models.Asset) *pb.AssetListItem {
|
||
if asset == nil {
|
||
return nil
|
||
}
|
||
|
||
return &pb.AssetListItem{
|
||
AssetId: asset.ID,
|
||
Name: asset.Name,
|
||
CoverUrl: asset.CoverURL,
|
||
Status: getStatusString(asset.Status),
|
||
TxHash: getStringValue(asset.TxHash),
|
||
CreatedAt: asset.CreatedAt,
|
||
MintedAt: getInt64Value(asset.MintedAt),
|
||
LikeCount: asset.LikeCount,
|
||
}
|
||
}
|
||
|
||
// ModelToProtoAssetDetail 将数据库模型转换为Proto格式(Asset详情)
|
||
func ModelToProtoAssetDetail(asset *models.Asset, ownerNickname string, isLiked bool, displayStatus int32, earnings int64, exhibitionExpireAt int64) *pb.Asset {
|
||
if asset == nil {
|
||
return nil
|
||
}
|
||
|
||
return &pb.Asset{
|
||
AssetId: asset.ID,
|
||
OwnerUid: asset.OwnerUID,
|
||
StarId: asset.StarID,
|
||
Name: asset.Name,
|
||
CoverUrl: asset.CoverURL,
|
||
MaterialUrl: getStringValue(asset.MaterialURL),
|
||
Description: getStringValue(asset.Description),
|
||
Grade: getInt32Value(asset.Grade),
|
||
Tags: []string(asset.Tags),
|
||
Visibility: asset.Visibility,
|
||
Status: asset.Status,
|
||
TxHash: getStringValue(asset.TxHash),
|
||
BlockNumber: getInt64Value(asset.BlockNumber),
|
||
LikeCount: asset.LikeCount,
|
||
CreatedAt: asset.CreatedAt,
|
||
UpdatedAt: asset.UpdatedAt,
|
||
MintedAt: getInt64Value(asset.MintedAt),
|
||
Owner: nil, // 保留用于兼容性
|
||
OwnerNickname: ownerNickname, // 持有者昵称
|
||
IsLiked: isLiked, // 当前用户是否已点赞
|
||
Info: asset.Info,
|
||
DisplayStatus: displayStatus, // 展示状态:0=待展示, 1=已展示
|
||
Earnings: earnings, // 当前展出收益(实时计算)
|
||
ExhibitionExpireAt: exhibitionExpireAt, // 展出过期时间
|
||
}
|
||
}
|
||
|
||
// getStringValue 获取字符串指针的值
|
||
func getStringValue(ptr *string) string {
|
||
if ptr == nil {
|
||
return ""
|
||
}
|
||
return *ptr
|
||
}
|
||
|
||
// getInt32Value 获取int32指针的值
|
||
func getInt32Value(ptr *int32) int32 {
|
||
if ptr == nil {
|
||
return 0
|
||
}
|
||
return *ptr
|
||
}
|
||
|
||
// getInt64Value 获取int64指针的值
|
||
func getInt64Value(ptr *int64) int64 {
|
||
if ptr == nil {
|
||
return 0
|
||
}
|
||
return *ptr
|
||
}
|
||
|
||
// getStatusString 将状态码转换为字符串
|
||
func getStatusString(status int32) string {
|
||
switch status {
|
||
case models.AssetStatusPending:
|
||
return "pending"
|
||
case models.AssetStatusActive:
|
||
return "minted"
|
||
default:
|
||
return "unknown"
|
||
}
|
||
}
|
||
|
||
// calculateRealtimeEarnings 实时计算展示收益
|
||
// 公式:R1 = R0 × T × [100% + Buff(n)]
|
||
// R0 = 5 水晶/小时,T = 上架时长(小时),Buff(n) 根据点赞数计算
|
||
func calculateRealtimeEarnings(likeCount int32, startTime, now int64) int64 {
|
||
R0 := int64(5) // 水晶/小时
|
||
|
||
// 计算上架时长(毫秒转小时)
|
||
T := (now - startTime) / 3600000
|
||
if T <= 0 {
|
||
T = 1 // 最少1小时
|
||
}
|
||
|
||
// 计算Buff
|
||
var buff int
|
||
switch {
|
||
case likeCount >= 30:
|
||
buff = 30
|
||
case likeCount >= 10:
|
||
buff = 20
|
||
case likeCount >= 5:
|
||
buff = 10
|
||
default:
|
||
buff = 0
|
||
}
|
||
|
||
// 基础收益
|
||
baseRevenue := R0 * T
|
||
|
||
// 应用Buff加成:R1 = R0 × T × (100% + Buff)
|
||
buffedRevenue := baseRevenue * (100 + int64(buff)) / 100
|
||
|
||
return buffedRevenue
|
||
}
|