feat: 新增星册类型选择

This commit is contained in:
zerosaturation 2026-04-20 16:00:10 +08:00
parent bebdc13c80
commit bcaa743446
32 changed files with 3772 additions and 206 deletions

View File

@ -21,7 +21,7 @@ cleanup() {
fi
# 清理所有 PID 文件并杀服务进程
for service in gateway activityService galleryService socialService assetService userService taskService; do
for service in gateway activityService galleryService socialService assetService userService taskService starbookService; do
pkill -9 -f "$service" 2>/dev/null || true
rm -f "/tmp/dev_sh_${service}.pid" "/tmp/dev_sh_${service}_restart" "/tmp/dev_sh_${service}.lock"
echo -e "${YELLOW} 🛑 $service 已停止${NC}"
@ -242,7 +242,8 @@ start_watcher() {
--exclude='socialService$' \
--exclude='galleryService$' \
--exclude='activityService$' \
--exclude='taskService$'
--exclude='taskService$' \
--exclude='starbookService$'
else
inotifywait -r -m -e modify,create,write "$watch_path" \
--exclude='\.git' \
@ -254,7 +255,8 @@ start_watcher() {
--exclude='socialService$' \
--exclude='galleryService$' \
--exclude='activityService$' \
--exclude='taskService$'
--exclude='taskService$' \
--exclude='starbookService$'
fi | while read event; do
# 时间戳防抖:每次事件更新标记文件
# Darwin 不支持 date +%s%N使用 python 获取纳秒时间戳
@ -307,13 +309,13 @@ echo ""
> /tmp/dev_sh_watchers.tmp
# 清理残留 PID 文件(上次非正常退出可能留下)
for service in activityService galleryService socialService assetService userService taskService gateway; do
for service in activityService galleryService socialService assetService userService taskService gateway starbookService; do
rm -f "/tmp/dev_sh_${service}.pid" "/tmp/dev_sh_${service}_restart"
done
# 停止现有服务(清理环境)
echo -e "${YELLOW}🛑 停止现有服务...${NC}"
for service in gateway userService socialService assetService galleryService activityService taskService; do
for service in gateway userService socialService assetService galleryService activityService taskService starbookService; do
pkill -9 -f "$service" 2>/dev/null || true
done
sleep 1
@ -328,6 +330,7 @@ build_service "socialService" "services/socialService" "services/socialService
build_service "galleryService" "services/galleryService" "services/galleryService/galleryService"
build_service "activityService" "services/activityService" "services/activityService/activityService"
build_service "taskService" "services/taskService" "services/taskService/taskService"
build_service "starbookService" "services/starbookService" "services/starbookService/starbookService"
cd "$SCRIPT_DIR"
# 启动所有服务
@ -339,6 +342,7 @@ start_service "socialService" "services/socialService/socialService" 20002
start_service "galleryService" "services/galleryService/galleryService" 20004 1
start_service "activityService" "services/activityService/activityService" 20005 1
start_service "taskService" "services/taskService/taskService" 20006 1
start_service "starbookService" "services/starbookService/starbookService" 20007 1
start_service "gateway" "gateway/gateway" 8080 0
# 启动所有文件监听器
@ -351,6 +355,7 @@ start_watcher "socialService" "services/socialService" "services/socialService
start_watcher "galleryService" "services/galleryService" "services/galleryService/galleryService" 20004 1
start_watcher "activityService" "services/activityService" "services/activityService/activityService" 20005 1
start_watcher "taskService" "services/taskService" "services/taskService/taskService" 20006 1
start_watcher "starbookService" "services/starbookService" "services/starbookService/starbookService" 20007 1
echo ""
echo -e "${GREEN}========================================${NC}"
@ -366,6 +371,7 @@ echo " - Asset Service: tri://localhost:20003"
echo " - Gallery Service: tri://localhost:20004"
echo " - Activity Service: tri://localhost:20005"
echo " - Task Service: tri://localhost:20006"
echo " - Starbook Service: tri://localhost:20007"
echo ""
echo -e "${YELLOW}按 Ctrl+C 停止所有服务${NC}"
echo ""

View File

@ -28,6 +28,7 @@ type DubboConfig struct {
GalleryServiceURL string
ActivityServiceURL string
TaskServiceURL string
StarbookServiceURL string
}
// JWTConfig JWT 配置
@ -73,6 +74,7 @@ func Load() *Config {
GalleryServiceURL: getEnv("DUBBO_GALLERY_SERVICE_URL", "tri://127.0.0.1:20004"),
ActivityServiceURL: getEnv("DUBBO_ACTIVITY_SERVICE_URL", "tri://127.0.0.1:20005"),
TaskServiceURL: getEnv("DUBBO_TASK_SERVICE_URL", "tri://127.0.0.1:20006"),
StarbookServiceURL: getEnv("DUBBO_STARBOOK_SERVICE_URL", "tri://127.0.0.1:20007"),
},
JWT: JWTConfig{
Secret: getEnv("JWT_SECRET", "topfans-secret-key-please-change-in-production"),

View File

@ -274,7 +274,7 @@ func (ctrl *AssetController) CreateMintOrder(c *gin.Context) {
Name: req.Name,
MaterialUrl: req.MaterialURL, // material_url 必填cover_url 由后端 AI 生成
Description: req.Description,
Rarity: req.Rarity,
Grade: req.Grade,
Tags: req.Tags,
MaterialType: req.MaterialType,
Info: req.Info,

View File

@ -0,0 +1,157 @@
package controller
import (
"context"
"net/http"
"strconv"
"dubbo.apache.org/dubbo-go/v3/client"
"dubbo.apache.org/dubbo-go/v3/common/constant"
"github.com/gin-gonic/gin"
"github.com/topfans/backend/gateway/pkg/response"
"github.com/topfans/backend/pkg/logger"
pb "github.com/topfans/backend/pkg/proto/starbook"
"go.uber.org/zap"
)
// StarbookController 星册控制器
type StarbookController struct {
starbookService pb.StarbookService
}
// NewStarbookController 创建星册控制器
func NewStarbookController(dubboClient *client.Client) (*StarbookController, error) {
// 创建 StarbookService 客户端
starbookService, err := pb.NewStarbookService(dubboClient)
if err != nil {
return nil, err
}
return &StarbookController{
starbookService: starbookService,
}, nil
}
// GetStarbookHome 获取星册首页
// @Summary 获取星册首页
// @Description 获取当前用户的星册首页数据,按类型和分组展示
// @Tags starbook
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} response.Response
// @Router /api/v1/starbook/home [get]
func (ctrl *StarbookController) GetStarbookHome(c *gin.Context) {
// 验证用户是否已登录(通过 AuthMiddleware 设置的上下文)
userID, exists := c.Get("user_id")
if !exists {
response.Error(c, http.StatusUnauthorized, "未授权")
return
}
starID, exists := c.Get("star_id")
if !exists {
response.Error(c, http.StatusUnauthorized, "未授权")
return
}
// 设置上下文和 Dubbo attachments
ctx := context.WithValue(c.Request.Context(), constant.AttachmentKey, map[string]interface{}{
"user_id": strconv.FormatInt(userID.(int64), 10),
"star_id": strconv.FormatInt(starID.(int64), 10),
})
logger.Logger.Debug("Calling GetStarbookHome",
zap.Int64("user_id", userID.(int64)),
zap.Int64("star_id", starID.(int64)),
)
// 调用星册服务(通过 Dubbouser/star ID 从 context 的 attachments 中获取)
resp, err := ctrl.starbookService.GetStarbookHome(ctx, &pb.GetStarbookHomeRequest{})
if err != nil {
logger.Logger.Error("GetStarbookHome RPC failed",
zap.Error(err),
zap.Int64("user_id", userID.(int64)),
zap.Int64("star_id", starID.(int64)),
)
response.Error(c, http.StatusInternalServerError, "获取星册首页失败: "+err.Error())
return
}
response.Success(c, resp)
}
// GetStarbookItems 获取星册藏品列表
// @Summary 获取星册藏品列表
// @Description 获取指定分组的藏品列表,支持分页
// @Tags starbook
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param type query string true "资产类型: regular/collection/activity"
// @Param category query string false "子分类regular 时固定为 castlove"
// @Param grade query int false "等级,仅 regular 类型有效"
// @Param page query int false "页码默认1"
// @Param page_size query int false "每页数量默认20"
// @Success 200 {object} response.Response
// @Router /api/v1/starbook/items [get]
func (ctrl *StarbookController) GetStarbookItems(c *gin.Context) {
// 验证用户是否已登录
userID, exists := c.Get("user_id")
if !exists {
response.Error(c, http.StatusUnauthorized, "未授权")
return
}
starID, exists := c.Get("star_id")
if !exists {
response.Error(c, http.StatusUnauthorized, "未授权")
return
}
// 解析查询参数
assetType := c.Query("type")
if assetType == "" {
response.Error(c, http.StatusBadRequest, "type 参数不能为空")
return
}
category := c.DefaultQuery("category", "castlove")
grade, _ := strconv.Atoi(c.DefaultQuery("grade", "0"))
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
// 设置上下文和 Dubbo attachments
ctx := context.WithValue(c.Request.Context(), constant.AttachmentKey, map[string]interface{}{
"user_id": strconv.FormatInt(userID.(int64), 10),
"star_id": strconv.FormatInt(starID.(int64), 10),
})
// 构建请求
req := &pb.GetStarbookItemsRequest{
Type: assetType,
Category: category,
Grade: int32(grade),
Page: int32(page),
PageSize: int32(pageSize),
}
logger.Logger.Debug("Calling GetStarbookItems",
zap.Int64("user_id", userID.(int64)),
zap.Int64("star_id", starID.(int64)),
zap.String("type", assetType),
zap.Int32("page", int32(page)),
)
// 调用星册服务
resp, err := ctrl.starbookService.GetStarbookItems(ctx, req)
if err != nil {
logger.Logger.Error("GetStarbookItems RPC failed",
zap.Error(err),
zap.Int64("user_id", userID.(int64)),
zap.Int64("star_id", starID.(int64)),
)
response.Error(c, http.StatusInternalServerError, "获取星册藏品列表失败: "+err.Error())
return
}
response.Success(c, resp)
}

View File

@ -93,8 +93,8 @@ func ConvertAsset(pbAsset *pbAsset.Asset) AssetDTO {
if pbAsset.Description != "" {
dto.Description = pbAsset.Description
}
if pbAsset.Rarity > 0 {
dto.Rarity = pbAsset.Rarity
if pbAsset.Grade > 0 {
dto.Grade = pbAsset.Grade
}
if len(pbAsset.Tags) > 0 {
dto.Tags = pbAsset.Tags

View File

@ -8,7 +8,7 @@ type CreateMintOrderRequestDTO struct {
Name string `json:"name"` // 藏品名称(可选)
MaterialURL string `json:"material_url" binding:"required"` // 用户上传的素材URL必填前端已上传到OSS
Description string `json:"description"` // 藏品描述(可选)
Rarity int32 `json:"rarity"` // 稀有度(可选)
Grade int32 `json:"grade"` // 星册等级(可选)
Tags []string `json:"tags"` // 标签列表(可选)
MaterialType string `json:"material_type"` // 素材类型(可选)
Info string `json:"info" binding:"required"` // 藏品信息(必填)
@ -67,7 +67,7 @@ type AssetDTO struct {
MaterialURL string `json:"material_url,omitempty"` // 用户上传的素材URL
MaterialURLSigned string `json:"material_url_signed,omitempty"` // 素材图预签名URL自动生成
Description string `json:"description,omitempty"` // 藏品描述(可选)
Rarity int32 `json:"rarity,omitempty"` // 稀有度(可选)
Grade int32 `json:"grade,omitempty"` // 星册等级(可选)
Tags []string `json:"tags,omitempty"` // 标签数组(可选)
Visibility string `json:"visibility"` // 可见性private, friends, public
Status int32 `json:"status"` // 状态0=Pending, 1=Active

View File

@ -60,6 +60,7 @@ func main() {
zap.String("gallery_service_url", cfg.Dubbo.GalleryServiceURL),
zap.String("activity_service_url", cfg.Dubbo.ActivityServiceURL),
zap.String("task_service_url", cfg.Dubbo.TaskServiceURL),
zap.String("starbook_service_url", cfg.Dubbo.StarbookServiceURL),
)
// 3. 设置 Gin 模式
@ -122,9 +123,18 @@ func main() {
}
logger.Logger.Info("Task Service Dubbo client connected successfully")
// 4.7 StarbookService Client
starbookClient, err := client.NewClient(
client.WithClientURL(cfg.Dubbo.StarbookServiceURL),
)
if err != nil {
logger.Logger.Fatal("Failed to create Starbook Service Dubbo client", zap.Error(err))
}
logger.Logger.Info("Starbook Service Dubbo client connected successfully")
// 5. 设置路由
logger.Logger.Info("Setting up routes...")
r, err := router.SetupRouter(userClient, socialClient, assetClient, galleryClient, activityClient, taskClient)
r, err := router.SetupRouter(userClient, socialClient, assetClient, galleryClient, activityClient, taskClient, starbookClient)
if err != nil {
logger.Logger.Fatal("Failed to setup router", zap.Error(err))
}

View File

@ -11,7 +11,7 @@ import (
)
// SetupRouter 设置路由
func SetupRouter(userClient *client.Client, socialClient *client.Client, assetClient *client.Client, galleryClient *client.Client, activityClient *client.Client, taskClient *client.Client) (*gin.Engine, error) {
func SetupRouter(userClient *client.Client, socialClient *client.Client, assetClient *client.Client, galleryClient *client.Client, activityClient *client.Client, taskClient *client.Client, starbookClient *client.Client) (*gin.Engine, error) {
r := gin.Default()
// 全局中间件
@ -71,6 +71,11 @@ func SetupRouter(userClient *client.Client, socialClient *client.Client, assetCl
return nil, err
}
starbookCtrl, err := controller.NewStarbookController(starbookClient)
if err != nil {
return nil, err
}
// API v1 路由组
v1 := r.Group("/api/v1")
{
@ -239,6 +244,14 @@ func SetupRouter(userClient *client.Client, socialClient *client.Client, assetCl
tasks.POST("/exhibition-revenue/claim", taskCtrl.ClaimExhibitionRevenue) // 领取单条展示收益
tasks.POST("/exhibition-revenue/claim-all", taskCtrl.ClaimAllExhibitionRevenue) // 一键领取所有展示收益
}
// 星册相关路由(需要认证)
starbook := v1.Group("/starbook")
starbook.Use(middleware.AuthMiddleware())
{
starbook.GET("/home", starbookCtrl.GetStarbookHome) // 获取星册首页
starbook.GET("/items", starbookCtrl.GetStarbookItems) // 获取星册藏品列表
}
}
return r, nil

View File

@ -57,6 +57,12 @@ var (
// 活动服务相关错误
ErrActivityNotFound = errors.New("活动不存在")
ErrActivityItemNotFound = errors.New("活动道具不存在")
// 星册服务相关错误
ErrCollectionAssetNotFound = errors.New("典藏藏品不存在")
ErrActivityAssetNotFound = errors.New("活动藏品不存在")
ErrAssetRegistryNotFound = errors.New("资产索引不存在")
ErrInvalidAssetType = errors.New("无效的资产类型")
)
// ToStatusCode 将错误转换为Proto状态码
@ -88,10 +94,10 @@ func ToStatusCode(err error) pb.StatusCode {
return pb.StatusCode_STATUS_BAD_REQUEST
case ErrAssetAccessDenied, ErrMintOrderAccessDenied:
return pb.StatusCode_STATUS_FORBIDDEN
case ErrActivityNotFound, ErrActivityItemNotFound:
case ErrActivityNotFound, ErrActivityItemNotFound, ErrCollectionAssetNotFound, ErrActivityAssetNotFound, ErrAssetRegistryNotFound:
return pb.StatusCode_STATUS_NOT_FOUND
case ErrInternalServer:
return pb.StatusCode_STATUS_INTERNAL_ERROR
case ErrInvalidAssetType:
return pb.StatusCode_STATUS_BAD_REQUEST
default:
return pb.StatusCode_STATUS_INTERNAL_ERROR
}

View File

@ -0,0 +1,52 @@
package models
import (
"time"
"gorm.io/gorm"
)
// ActivityAsset 活动藏品表模型
type ActivityAsset struct {
ID int64 `gorm:"primaryKey;autoIncrement;column:id"`
AssetID int64 `gorm:"unique;not null;column:asset_id"`
OwnerUID int64 `gorm:"not null;index:idx_activity_owner;column:owner_uid"`
StarID int64 `gorm:"not null;index:idx_activity_star;column:star_id"`
ActivityID int64 `gorm:"not null;index:idx_activity_owner;column:activity_id"`
ActivityType string `gorm:"type:varchar(50);not null;index:idx_activity_type;column:activity_type"`
Name string `gorm:"type:varchar(100);not null;column:name"`
CoverURL string `gorm:"type:varchar(500);not null;column:cover_url"`
LikeCount int32 `gorm:"default:0;column:like_count"`
Status int32 `gorm:"default:0;column:status"` // 0=Pending, 1=Active
Metadata JSONB `gorm:"type:jsonb;column:metadata"`
CreatedAt int64 `gorm:"not null;column:created_at"`
UpdatedAt int64 `gorm:"not null;column:updated_at"`
}
// TableName 指定表名
func (ActivityAsset) TableName() string {
return "activity_assets"
}
// BeforeCreate 创建前钩子
func (a *ActivityAsset) BeforeCreate(tx *gorm.DB) error {
now := time.Now().UnixMilli()
a.CreatedAt = now
a.UpdatedAt = now
if a.Status == 0 {
a.Status = AssetStatusPending
}
return nil
}
// BeforeUpdate 更新前钩子
func (a *ActivityAsset) BeforeUpdate(tx *gorm.DB) error {
a.UpdatedAt = time.Now().UnixMilli()
return nil
}
// 活动藏品状态常量
const (
ActivityAssetStatusPending = 0 // 待处理
ActivityAssetStatusActive = 1 // 已激活
)

View File

@ -19,7 +19,7 @@ type Asset struct {
// 预留字段(后续功能扩展)
Description *string `gorm:"type:text;column:description"` // 藏品描述(预留)
Rarity *int32 `gorm:"column:rarity"` // 稀有度(预留
Grade *int32 `gorm:"column:grade"` // 星册等级铸爱流程创建时默认为1
Tags StringArray `gorm:"type:jsonb;column:tags"` // 标签预留JSON数组
Visibility string `gorm:"type:varchar(20);default:'private';column:visibility"` // 可见性private, friends, public预留
Info string `gorm:"type:text;column:info"` // 藏品信息(必填)

View File

@ -0,0 +1,60 @@
package models
import (
"time"
"gorm.io/gorm"
)
// AssetRegistry 资产统一索引表模型
type AssetRegistry struct {
ID int64 `gorm:"primaryKey;autoIncrement;column:id"`
AssetID int64 `gorm:"not null;column:asset_id"`
AssetType string `gorm:"type:varchar(20);not null;column:asset_type"` // 'regular' | 'collection' | 'activity'
OwnerUID int64 `gorm:"not null;index:idx_registry_owner_star;column:owner_uid"`
StarID int64 `gorm:"not null;index:idx_registry_owner_star;column:star_id"`
// 普通藏品专属字段
Grade *int32 `gorm:"column:grade"` // 仅 regular 类型时有效
// 典藏专属字段
CollectionCategory *string `gorm:"type:varchar(50);column:collection_category"` // 仅 collection 类型时有效
// 活动专属字段
ActivityID *int64 `gorm:"column:activity_id"` // 仅 activity 类型时有效
ActivityType *string `gorm:"type:varchar(50);column:activity_type"` // 仅 activity 类型时有效
// 公共字段
Status int32 `gorm:"default:0;column:status"`
LikeCount int32 `gorm:"default:0;column:like_count"`
CreatedAt int64 `gorm:"not null;column:created_at"`
UpdatedAt int64 `gorm:"not null;column:updated_at"`
}
// TableName 指定表名
func (AssetRegistry) TableName() string {
return "asset_registry"
}
// BeforeCreate 创建前钩子
func (r *AssetRegistry) BeforeCreate(tx *gorm.DB) error {
now := time.Now().UnixMilli()
r.CreatedAt = now
r.UpdatedAt = now
return nil
}
// BeforeUpdate 更新前钩子
func (r *AssetRegistry) BeforeUpdate(tx *gorm.DB) error {
r.UpdatedAt = time.Now().UnixMilli()
return nil
}
// 资产类型常量
const (
AssetTypeRegular = "regular" // 普通藏品
AssetTypeCollection = "collection" // 典藏藏品
AssetTypeActivity = "activity" // 活动藏品
)
// AssetRegistry 状态常量
const (
AssetRegistryStatusPending = 0 // 待处理
AssetRegistryStatusActive = 1 // 已激活
)

View File

@ -0,0 +1,69 @@
package models
import (
"time"
"gorm.io/gorm"
)
// CollectionAsset 典藏藏品表模型
type CollectionAsset struct {
ID int64 `gorm:"primaryKey;autoIncrement;column:id"`
AssetID int64 `gorm:"unique;not null;column:asset_id"`
OwnerUID int64 `gorm:"not null;index:idx_collection_owner_star;column:owner_uid"`
StarID int64 `gorm:"not null;index:idx_collection_owner_star;column:star_id"`
Name string `gorm:"type:varchar(100);not null;column:name"`
CoverURL string `gorm:"type:varchar(500);not null;column:cover_url"`
Category string `gorm:"type:varchar(50);not null;index:idx_collection_category;column:category"`
LikeCount int32 `gorm:"default:0;column:like_count"`
Status int32 `gorm:"default:0;column:status"` // 0=Pending, 1=Active
Metadata JSONB `gorm:"type:jsonb;column:metadata"`
CreatedAt int64 `gorm:"not null;column:created_at"`
UpdatedAt int64 `gorm:"not null;column:updated_at"`
}
// TableName 指定表名
func (CollectionAsset) TableName() string {
return "collection_assets"
}
// BeforeCreate 创建前钩子
func (c *CollectionAsset) BeforeCreate(tx *gorm.DB) error {
now := time.Now().UnixMilli()
c.CreatedAt = now
c.UpdatedAt = now
if c.Status == 0 {
c.Status = AssetStatusPending
}
return nil
}
// BeforeUpdate 更新前钩子
func (c *CollectionAsset) BeforeUpdate(tx *gorm.DB) error {
c.UpdatedAt = time.Now().UnixMilli()
return nil
}
// 典藏藏品状态常量
const (
CollectionAssetStatusPending = 0 // 待处理
CollectionAssetStatusActive = 1 // 已激活
)
// JSONB 是 GORM 的 jsonb 类型别名
type JSONB []byte
// Scan 实现 sql.Scanner 接口
func (j *JSONB) Scan(value interface{}) error {
if value == nil {
*j = nil
return nil
}
bytes, ok := value.([]byte)
if !ok {
*j = nil
return nil
}
*j = bytes
return nil
}

View File

@ -33,7 +33,7 @@ type Asset struct {
CoverUrl string `protobuf:"bytes,5,opt,name=cover_url,json=coverUrl,proto3" json:"cover_url,omitempty"` // 封面图URL
MaterialUrl string `protobuf:"bytes,6,opt,name=material_url,json=materialUrl,proto3" json:"material_url,omitempty"` // 用户上传的素材URL
Description string `protobuf:"bytes,7,opt,name=description,proto3" json:"description,omitempty"` // 藏品描述(可选)
Rarity int32 `protobuf:"varint,8,opt,name=rarity,proto3" json:"rarity,omitempty"` // 稀有度(可选)
Grade int32 `protobuf:"varint,8,opt,name=grade,proto3" json:"grade,omitempty"` // 星册等级(可选)
Tags []string `protobuf:"bytes,9,rep,name=tags,proto3" json:"tags,omitempty"` // 标签数组(可选)
Visibility string `protobuf:"bytes,10,opt,name=visibility,proto3" json:"visibility,omitempty"` // 可见性private, friends, public预留
Status int32 `protobuf:"varint,11,opt,name=status,proto3" json:"status,omitempty"` // 状态0=Pending, 1=Active
@ -131,9 +131,9 @@ func (x *Asset) GetDescription() string {
return ""
}
func (x *Asset) GetRarity() int32 {
func (x *Asset) GetGrade() int32 {
if x != nil {
return x.Rarity
return x.Grade
}
return 0
}
@ -567,7 +567,7 @@ type CreateMintOrderRequest struct {
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // 藏品名称(可选)
MaterialUrl string `protobuf:"bytes,3,opt,name=material_url,json=materialUrl,proto3" json:"material_url,omitempty"` // 用户上传的素材URL必填前端已上传到OSS
Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` // 藏品描述(可选)
Rarity int32 `protobuf:"varint,5,opt,name=rarity,proto3" json:"rarity,omitempty"` // 稀有度(可选)
Grade int32 `protobuf:"varint,5,opt,name=grade,proto3" json:"grade,omitempty"` // 星册等级(可选)
Tags []string `protobuf:"bytes,6,rep,name=tags,proto3" json:"tags,omitempty"` // 标签列表(可选)
MaterialType string `protobuf:"bytes,7,opt,name=material_type,json=materialType,proto3" json:"material_type,omitempty"` // 素材类型(可选)
Info string `protobuf:"bytes,8,opt,name=info,proto3" json:"info,omitempty"` // 藏品信息(必填)
@ -633,9 +633,9 @@ func (x *CreateMintOrderRequest) GetDescription() string {
return ""
}
func (x *CreateMintOrderRequest) GetRarity() int32 {
func (x *CreateMintOrderRequest) GetGrade() int32 {
if x != nil {
return x.Rarity
return x.Grade
}
return 0
}
@ -2302,7 +2302,7 @@ var File_asset_proto protoreflect.FileDescriptor
const file_asset_proto_rawDesc = "" +
"\n" +
"\vasset.proto\x12\rtopfans.asset\x1a\x12proto/common.proto\x1a\x1cgoogle/api/annotations.proto\"\xee\x04\n" +
"\vasset.proto\x12\rtopfans.asset\x1a\x12proto/common.proto\x1a\x1cgoogle/api/annotations.proto\"\xec\x04\n" +
"\x05Asset\x12\x19\n" +
"\basset_id\x18\x01 \x01(\x03R\aassetId\x12\x1b\n" +
"\towner_uid\x18\x02 \x01(\x03R\bownerUid\x12\x17\n" +
@ -2310,8 +2310,8 @@ const file_asset_proto_rawDesc = "" +
"\x04name\x18\x04 \x01(\tR\x04name\x12\x1b\n" +
"\tcover_url\x18\x05 \x01(\tR\bcoverUrl\x12!\n" +
"\fmaterial_url\x18\x06 \x01(\tR\vmaterialUrl\x12 \n" +
"\vdescription\x18\a \x01(\tR\vdescription\x12\x16\n" +
"\x06rarity\x18\b \x01(\x05R\x06rarity\x12\x12\n" +
"\vdescription\x18\a \x01(\tR\vdescription\x12\x14\n" +
"\x05grade\x18\b \x01(\x05R\x05grade\x12\x12\n" +
"\x04tags\x18\t \x03(\tR\x04tags\x12\x1e\n" +
"\n" +
"visibility\x18\n" +
@ -2361,13 +2361,13 @@ const file_asset_proto_rawDesc = "" +
"\border_id\x18\x01 \x01(\tR\aorderId\"y\n" +
"\x15InitMintOrderResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12.\n" +
"\x05order\x18\x02 \x01(\v2\x18.topfans.asset.MintOrderR\x05order\"\xf1\x01\n" +
"\x05order\x18\x02 \x01(\v2\x18.topfans.asset.MintOrderR\x05order\"\xef\x01\n" +
"\x16CreateMintOrderRequest\x12\x19\n" +
"\border_id\x18\x02 \x01(\tR\aorderId\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12!\n" +
"\fmaterial_url\x18\x03 \x01(\tR\vmaterialUrl\x12 \n" +
"\vdescription\x18\x04 \x01(\tR\vdescription\x12\x16\n" +
"\x06rarity\x18\x05 \x01(\x05R\x06rarity\x12\x12\n" +
"\vdescription\x18\x04 \x01(\tR\vdescription\x12\x14\n" +
"\x05grade\x18\x05 \x01(\x05R\x05grade\x12\x12\n" +
"\x04tags\x18\x06 \x03(\tR\x04tags\x12#\n" +
"\rmaterial_type\x18\a \x01(\tR\fmaterialType\x12\x12\n" +
"\x04info\x18\b \x01(\tR\x04info\"\xaf\x01\n" +

View File

@ -0,0 +1,748 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v7.34.0
// source: starbook.proto
package starbook
import (
common "github.com/topfans/backend/pkg/proto/common"
_ "google.golang.org/genproto/googleapis/api/annotations"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// 星册首页请求
type GetStarbookHomeRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetStarbookHomeRequest) Reset() {
*x = GetStarbookHomeRequest{}
mi := &file_starbook_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetStarbookHomeRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetStarbookHomeRequest) ProtoMessage() {}
func (x *GetStarbookHomeRequest) ProtoReflect() protoreflect.Message {
mi := &file_starbook_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetStarbookHomeRequest.ProtoReflect.Descriptor instead.
func (*GetStarbookHomeRequest) Descriptor() ([]byte, []int) {
return file_starbook_proto_rawDescGZIP(), []int{0}
}
// 星册首页响应
type GetStarbookHomeResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
Data *StarbookHomeData `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetStarbookHomeResponse) Reset() {
*x = GetStarbookHomeResponse{}
mi := &file_starbook_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetStarbookHomeResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetStarbookHomeResponse) ProtoMessage() {}
func (x *GetStarbookHomeResponse) ProtoReflect() protoreflect.Message {
mi := &file_starbook_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetStarbookHomeResponse.ProtoReflect.Descriptor instead.
func (*GetStarbookHomeResponse) Descriptor() ([]byte, []int) {
return file_starbook_proto_rawDescGZIP(), []int{1}
}
func (x *GetStarbookHomeResponse) GetBase() *common.BaseResponse {
if x != nil {
return x.Base
}
return nil
}
func (x *GetStarbookHomeResponse) GetData() *StarbookHomeData {
if x != nil {
return x.Data
}
return nil
}
// 星册首页数据
type StarbookHomeData struct {
state protoimpl.MessageState `protogen:"open.v1"`
Groups []*AssetGroup `protobuf:"bytes,1,rep,name=groups,proto3" json:"groups,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *StarbookHomeData) Reset() {
*x = StarbookHomeData{}
mi := &file_starbook_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *StarbookHomeData) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*StarbookHomeData) ProtoMessage() {}
func (x *StarbookHomeData) ProtoReflect() protoreflect.Message {
mi := &file_starbook_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use StarbookHomeData.ProtoReflect.Descriptor instead.
func (*StarbookHomeData) Descriptor() ([]byte, []int) {
return file_starbook_proto_rawDescGZIP(), []int{2}
}
func (x *StarbookHomeData) GetGroups() []*AssetGroup {
if x != nil {
return x.Groups
}
return nil
}
// 资产分组
type AssetGroup struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // 'regular' / 'collection' / 'activity'
Category string `protobuf:"bytes,2,opt,name=category,proto3" json:"category,omitempty"` // 'castlove'(regular) / collection_category / activity_type
CategoryName string `protobuf:"bytes,3,opt,name=category_name,json=categoryName,proto3" json:"category_name,omitempty"`
// regular 使用 grades 分组collection/activity 使用 flat items 列表
Grades []*GradeSection `protobuf:"bytes,4,rep,name=grades,proto3" json:"grades,omitempty"` // 仅 regular 时有效
Items []*AssetItem `protobuf:"bytes,5,rep,name=items,proto3" json:"items,omitempty"` // collection / activity 时有效
TotalCount int32 `protobuf:"varint,6,opt,name=total_count,json=totalCount,proto3" json:"total_count,omitempty"`
HasMore bool `protobuf:"varint,7,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AssetGroup) Reset() {
*x = AssetGroup{}
mi := &file_starbook_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AssetGroup) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AssetGroup) ProtoMessage() {}
func (x *AssetGroup) ProtoReflect() protoreflect.Message {
mi := &file_starbook_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AssetGroup.ProtoReflect.Descriptor instead.
func (*AssetGroup) Descriptor() ([]byte, []int) {
return file_starbook_proto_rawDescGZIP(), []int{3}
}
func (x *AssetGroup) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *AssetGroup) GetCategory() string {
if x != nil {
return x.Category
}
return ""
}
func (x *AssetGroup) GetCategoryName() string {
if x != nil {
return x.CategoryName
}
return ""
}
func (x *AssetGroup) GetGrades() []*GradeSection {
if x != nil {
return x.Grades
}
return nil
}
func (x *AssetGroup) GetItems() []*AssetItem {
if x != nil {
return x.Items
}
return nil
}
func (x *AssetGroup) GetTotalCount() int32 {
if x != nil {
return x.TotalCount
}
return 0
}
func (x *AssetGroup) GetHasMore() bool {
if x != nil {
return x.HasMore
}
return false
}
// 等级分组(仅 regular 类型使用)
type GradeSection struct {
state protoimpl.MessageState `protogen:"open.v1"`
Grade int32 `protobuf:"varint,1,opt,name=grade,proto3" json:"grade,omitempty"` // 等级1/2/3/4/5...
Items []*AssetItem `protobuf:"bytes,2,rep,name=items,proto3" json:"items,omitempty"`
TotalCount int32 `protobuf:"varint,3,opt,name=total_count,json=totalCount,proto3" json:"total_count,omitempty"`
HasMore bool `protobuf:"varint,4,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GradeSection) Reset() {
*x = GradeSection{}
mi := &file_starbook_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GradeSection) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GradeSection) ProtoMessage() {}
func (x *GradeSection) ProtoReflect() protoreflect.Message {
mi := &file_starbook_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GradeSection.ProtoReflect.Descriptor instead.
func (*GradeSection) Descriptor() ([]byte, []int) {
return file_starbook_proto_rawDescGZIP(), []int{4}
}
func (x *GradeSection) GetGrade() int32 {
if x != nil {
return x.Grade
}
return 0
}
func (x *GradeSection) GetItems() []*AssetItem {
if x != nil {
return x.Items
}
return nil
}
func (x *GradeSection) GetTotalCount() int32 {
if x != nil {
return x.TotalCount
}
return 0
}
func (x *GradeSection) GetHasMore() bool {
if x != nil {
return x.HasMore
}
return false
}
// 资产项
type AssetItem struct {
state protoimpl.MessageState `protogen:"open.v1"`
AssetId int64 `protobuf:"varint,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
CoverUrlSigned string `protobuf:"bytes,3,opt,name=cover_url_signed,json=coverUrlSigned,proto3" json:"cover_url_signed,omitempty"` // 预签名封面URL
LikeCount int32 `protobuf:"varint,4,opt,name=like_count,json=likeCount,proto3" json:"like_count,omitempty"`
CreatedAt int64 `protobuf:"varint,5,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
Category string `protobuf:"bytes,6,opt,name=category,proto3" json:"category,omitempty"` // regular: 'castlove' / collection: category / activity: activity_type
Grade int32 `protobuf:"varint,7,opt,name=grade,proto3" json:"grade,omitempty"` // 仅 regular 时有效1/2/3...),其他类型为 0
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AssetItem) Reset() {
*x = AssetItem{}
mi := &file_starbook_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AssetItem) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AssetItem) ProtoMessage() {}
func (x *AssetItem) ProtoReflect() protoreflect.Message {
mi := &file_starbook_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AssetItem.ProtoReflect.Descriptor instead.
func (*AssetItem) Descriptor() ([]byte, []int) {
return file_starbook_proto_rawDescGZIP(), []int{5}
}
func (x *AssetItem) GetAssetId() int64 {
if x != nil {
return x.AssetId
}
return 0
}
func (x *AssetItem) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *AssetItem) GetCoverUrlSigned() string {
if x != nil {
return x.CoverUrlSigned
}
return ""
}
func (x *AssetItem) GetLikeCount() int32 {
if x != nil {
return x.LikeCount
}
return 0
}
func (x *AssetItem) GetCreatedAt() int64 {
if x != nil {
return x.CreatedAt
}
return 0
}
func (x *AssetItem) GetCategory() string {
if x != nil {
return x.Category
}
return ""
}
func (x *AssetItem) GetGrade() int32 {
if x != nil {
return x.Grade
}
return 0
}
// 藏品列表请求
type GetStarbookItemsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // 'regular' / 'collection' / 'activity'
Category string `protobuf:"bytes,2,opt,name=category,proto3" json:"category,omitempty"` // regular 时固定传 'castlove'
Grade int32 `protobuf:"varint,3,opt,name=grade,proto3" json:"grade,omitempty"` // 仅 regular 时有效1/2/3...
Page int32 `protobuf:"varint,4,opt,name=page,proto3" json:"page,omitempty"` // 默认 1
PageSize int32 `protobuf:"varint,5,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` // 默认 20
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetStarbookItemsRequest) Reset() {
*x = GetStarbookItemsRequest{}
mi := &file_starbook_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetStarbookItemsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetStarbookItemsRequest) ProtoMessage() {}
func (x *GetStarbookItemsRequest) ProtoReflect() protoreflect.Message {
mi := &file_starbook_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetStarbookItemsRequest.ProtoReflect.Descriptor instead.
func (*GetStarbookItemsRequest) Descriptor() ([]byte, []int) {
return file_starbook_proto_rawDescGZIP(), []int{6}
}
func (x *GetStarbookItemsRequest) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *GetStarbookItemsRequest) GetCategory() string {
if x != nil {
return x.Category
}
return ""
}
func (x *GetStarbookItemsRequest) GetGrade() int32 {
if x != nil {
return x.Grade
}
return 0
}
func (x *GetStarbookItemsRequest) GetPage() int32 {
if x != nil {
return x.Page
}
return 0
}
func (x *GetStarbookItemsRequest) GetPageSize() int32 {
if x != nil {
return x.PageSize
}
return 0
}
// 藏品列表响应
type GetStarbookItemsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
Data *AssetListData `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetStarbookItemsResponse) Reset() {
*x = GetStarbookItemsResponse{}
mi := &file_starbook_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetStarbookItemsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetStarbookItemsResponse) ProtoMessage() {}
func (x *GetStarbookItemsResponse) ProtoReflect() protoreflect.Message {
mi := &file_starbook_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetStarbookItemsResponse.ProtoReflect.Descriptor instead.
func (*GetStarbookItemsResponse) Descriptor() ([]byte, []int) {
return file_starbook_proto_rawDescGZIP(), []int{7}
}
func (x *GetStarbookItemsResponse) GetBase() *common.BaseResponse {
if x != nil {
return x.Base
}
return nil
}
func (x *GetStarbookItemsResponse) GetData() *AssetListData {
if x != nil {
return x.Data
}
return nil
}
// 藏品列表数据
type AssetListData struct {
state protoimpl.MessageState `protogen:"open.v1"`
Items []*AssetItem `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"`
Total int64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"`
Page int32 `protobuf:"varint,3,opt,name=page,proto3" json:"page,omitempty"`
PageSize int32 `protobuf:"varint,4,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"`
HasMore bool `protobuf:"varint,5,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AssetListData) Reset() {
*x = AssetListData{}
mi := &file_starbook_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AssetListData) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AssetListData) ProtoMessage() {}
func (x *AssetListData) ProtoReflect() protoreflect.Message {
mi := &file_starbook_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AssetListData.ProtoReflect.Descriptor instead.
func (*AssetListData) Descriptor() ([]byte, []int) {
return file_starbook_proto_rawDescGZIP(), []int{8}
}
func (x *AssetListData) GetItems() []*AssetItem {
if x != nil {
return x.Items
}
return nil
}
func (x *AssetListData) GetTotal() int64 {
if x != nil {
return x.Total
}
return 0
}
func (x *AssetListData) GetPage() int32 {
if x != nil {
return x.Page
}
return 0
}
func (x *AssetListData) GetPageSize() int32 {
if x != nil {
return x.PageSize
}
return 0
}
func (x *AssetListData) GetHasMore() bool {
if x != nil {
return x.HasMore
}
return false
}
var File_starbook_proto protoreflect.FileDescriptor
const file_starbook_proto_rawDesc = "" +
"\n" +
"\x0estarbook.proto\x12\x10topfans.starbook\x1a\x12proto/common.proto\x1a\x1cgoogle/api/annotations.proto\"\x18\n" +
"\x16GetStarbookHomeRequest\"\x83\x01\n" +
"\x17GetStarbookHomeResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x126\n" +
"\x04data\x18\x02 \x01(\v2\".topfans.starbook.StarbookHomeDataR\x04data\"H\n" +
"\x10StarbookHomeData\x124\n" +
"\x06groups\x18\x01 \x03(\v2\x1c.topfans.starbook.AssetGroupR\x06groups\"\x88\x02\n" +
"\n" +
"AssetGroup\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x12\x1a\n" +
"\bcategory\x18\x02 \x01(\tR\bcategory\x12#\n" +
"\rcategory_name\x18\x03 \x01(\tR\fcategoryName\x126\n" +
"\x06grades\x18\x04 \x03(\v2\x1e.topfans.starbook.GradeSectionR\x06grades\x121\n" +
"\x05items\x18\x05 \x03(\v2\x1b.topfans.starbook.AssetItemR\x05items\x12\x1f\n" +
"\vtotal_count\x18\x06 \x01(\x05R\n" +
"totalCount\x12\x19\n" +
"\bhas_more\x18\a \x01(\bR\ahasMore\"\x93\x01\n" +
"\fGradeSection\x12\x14\n" +
"\x05grade\x18\x01 \x01(\x05R\x05grade\x121\n" +
"\x05items\x18\x02 \x03(\v2\x1b.topfans.starbook.AssetItemR\x05items\x12\x1f\n" +
"\vtotal_count\x18\x03 \x01(\x05R\n" +
"totalCount\x12\x19\n" +
"\bhas_more\x18\x04 \x01(\bR\ahasMore\"\xd4\x01\n" +
"\tAssetItem\x12\x19\n" +
"\basset_id\x18\x01 \x01(\x03R\aassetId\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12(\n" +
"\x10cover_url_signed\x18\x03 \x01(\tR\x0ecoverUrlSigned\x12\x1d\n" +
"\n" +
"like_count\x18\x04 \x01(\x05R\tlikeCount\x12\x1d\n" +
"\n" +
"created_at\x18\x05 \x01(\x03R\tcreatedAt\x12\x1a\n" +
"\bcategory\x18\x06 \x01(\tR\bcategory\x12\x14\n" +
"\x05grade\x18\a \x01(\x05R\x05grade\"\x90\x01\n" +
"\x17GetStarbookItemsRequest\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x12\x1a\n" +
"\bcategory\x18\x02 \x01(\tR\bcategory\x12\x14\n" +
"\x05grade\x18\x03 \x01(\x05R\x05grade\x12\x12\n" +
"\x04page\x18\x04 \x01(\x05R\x04page\x12\x1b\n" +
"\tpage_size\x18\x05 \x01(\x05R\bpageSize\"\x81\x01\n" +
"\x18GetStarbookItemsResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x123\n" +
"\x04data\x18\x02 \x01(\v2\x1f.topfans.starbook.AssetListDataR\x04data\"\xa4\x01\n" +
"\rAssetListData\x121\n" +
"\x05items\x18\x01 \x03(\v2\x1b.topfans.starbook.AssetItemR\x05items\x12\x14\n" +
"\x05total\x18\x02 \x01(\x03R\x05total\x12\x12\n" +
"\x04page\x18\x03 \x01(\x05R\x04page\x12\x1b\n" +
"\tpage_size\x18\x04 \x01(\x05R\bpageSize\x12\x19\n" +
"\bhas_more\x18\x05 \x01(\bR\ahasMore2\xa5\x02\n" +
"\x0fStarbookService\x12\x85\x01\n" +
"\x0fGetStarbookHome\x12(.topfans.starbook.GetStarbookHomeRequest\x1a).topfans.starbook.GetStarbookHomeResponse\"\x1d\x82\xd3\xe4\x93\x02\x17\x12\x15/api/v1/starbook/home\x12\x89\x01\n" +
"\x10GetStarbookItems\x12).topfans.starbook.GetStarbookItemsRequest\x1a*.topfans.starbook.GetStarbookItemsResponse\"\x1e\x82\xd3\xe4\x93\x02\x18\x12\x16/api/v1/starbook/itemsB8Z6github.com/topfans/backend/pkg/proto/starbook;starbookb\x06proto3"
var (
file_starbook_proto_rawDescOnce sync.Once
file_starbook_proto_rawDescData []byte
)
func file_starbook_proto_rawDescGZIP() []byte {
file_starbook_proto_rawDescOnce.Do(func() {
file_starbook_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_starbook_proto_rawDesc), len(file_starbook_proto_rawDesc)))
})
return file_starbook_proto_rawDescData
}
var file_starbook_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
var file_starbook_proto_goTypes = []any{
(*GetStarbookHomeRequest)(nil), // 0: topfans.starbook.GetStarbookHomeRequest
(*GetStarbookHomeResponse)(nil), // 1: topfans.starbook.GetStarbookHomeResponse
(*StarbookHomeData)(nil), // 2: topfans.starbook.StarbookHomeData
(*AssetGroup)(nil), // 3: topfans.starbook.AssetGroup
(*GradeSection)(nil), // 4: topfans.starbook.GradeSection
(*AssetItem)(nil), // 5: topfans.starbook.AssetItem
(*GetStarbookItemsRequest)(nil), // 6: topfans.starbook.GetStarbookItemsRequest
(*GetStarbookItemsResponse)(nil), // 7: topfans.starbook.GetStarbookItemsResponse
(*AssetListData)(nil), // 8: topfans.starbook.AssetListData
(*common.BaseResponse)(nil), // 9: topfans.common.BaseResponse
}
var file_starbook_proto_depIdxs = []int32{
9, // 0: topfans.starbook.GetStarbookHomeResponse.base:type_name -> topfans.common.BaseResponse
2, // 1: topfans.starbook.GetStarbookHomeResponse.data:type_name -> topfans.starbook.StarbookHomeData
3, // 2: topfans.starbook.StarbookHomeData.groups:type_name -> topfans.starbook.AssetGroup
4, // 3: topfans.starbook.AssetGroup.grades:type_name -> topfans.starbook.GradeSection
5, // 4: topfans.starbook.AssetGroup.items:type_name -> topfans.starbook.AssetItem
5, // 5: topfans.starbook.GradeSection.items:type_name -> topfans.starbook.AssetItem
9, // 6: topfans.starbook.GetStarbookItemsResponse.base:type_name -> topfans.common.BaseResponse
8, // 7: topfans.starbook.GetStarbookItemsResponse.data:type_name -> topfans.starbook.AssetListData
5, // 8: topfans.starbook.AssetListData.items:type_name -> topfans.starbook.AssetItem
0, // 9: topfans.starbook.StarbookService.GetStarbookHome:input_type -> topfans.starbook.GetStarbookHomeRequest
6, // 10: topfans.starbook.StarbookService.GetStarbookItems:input_type -> topfans.starbook.GetStarbookItemsRequest
1, // 11: topfans.starbook.StarbookService.GetStarbookHome:output_type -> topfans.starbook.GetStarbookHomeResponse
7, // 12: topfans.starbook.StarbookService.GetStarbookItems:output_type -> topfans.starbook.GetStarbookItemsResponse
11, // [11:13] is the sub-list for method output_type
9, // [9:11] is the sub-list for method input_type
9, // [9:9] is the sub-list for extension type_name
9, // [9:9] is the sub-list for extension extendee
0, // [0:9] is the sub-list for field type_name
}
func init() { file_starbook_proto_init() }
func file_starbook_proto_init() {
if File_starbook_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_starbook_proto_rawDesc), len(file_starbook_proto_rawDesc)),
NumEnums: 0,
NumMessages: 9,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_starbook_proto_goTypes,
DependencyIndexes: file_starbook_proto_depIdxs,
MessageInfos: file_starbook_proto_msgTypes,
}.Build()
File_starbook_proto = out.File
file_starbook_proto_goTypes = nil
file_starbook_proto_depIdxs = nil
}

View File

@ -0,0 +1,149 @@
// Code generated by protoc-gen-triple. DO NOT EDIT.
//
// Source: starbook.proto
package starbook
import (
"context"
)
import (
"dubbo.apache.org/dubbo-go/v3"
"dubbo.apache.org/dubbo-go/v3/client"
"dubbo.apache.org/dubbo-go/v3/common"
"dubbo.apache.org/dubbo-go/v3/common/constant"
"dubbo.apache.org/dubbo-go/v3/protocol/triple/triple_protocol"
"dubbo.apache.org/dubbo-go/v3/server"
)
// This is a compile-time assertion to ensure that this generated file and the Triple package
// are compatible. If you get a compiler error that this constant is not defined, this code was
// generated with a version of Triple newer than the one compiled into your binary. You can fix the
// problem by either regenerating this code with an older version of Triple or updating the Triple
// version compiled into your binary.
const _ = triple_protocol.IsAtLeastVersion0_1_0
const (
// StarbookServiceName is the fully-qualified name of the StarbookService service.
StarbookServiceName = "topfans.starbook.StarbookService"
)
// These constants are the fully-qualified names of the RPCs defined in this package. They're
// exposed at runtime as procedure and as the final two segments of the HTTP route.
//
// Note that these are different from the fully-qualified method names used by
// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
// period.
const (
// StarbookServiceGetStarbookHomeProcedure is the fully-qualified name of the StarbookService's GetStarbookHome RPC.
StarbookServiceGetStarbookHomeProcedure = "/topfans.starbook.StarbookService/GetStarbookHome"
// StarbookServiceGetStarbookItemsProcedure is the fully-qualified name of the StarbookService's GetStarbookItems RPC.
StarbookServiceGetStarbookItemsProcedure = "/topfans.starbook.StarbookService/GetStarbookItems"
)
var (
_ StarbookService = (*StarbookServiceImpl)(nil)
)
// StarbookService is a client for the topfans.starbook.StarbookService service.
type StarbookService interface {
GetStarbookHome(ctx context.Context, req *GetStarbookHomeRequest, opts ...client.CallOption) (*GetStarbookHomeResponse, error)
GetStarbookItems(ctx context.Context, req *GetStarbookItemsRequest, opts ...client.CallOption) (*GetStarbookItemsResponse, error)
}
// NewStarbookService constructs a client for the starbook.StarbookService service.
func NewStarbookService(cli *client.Client, opts ...client.ReferenceOption) (StarbookService, error) {
conn, err := cli.DialWithInfo("topfans.starbook.StarbookService", &StarbookService_ClientInfo, opts...)
if err != nil {
return nil, err
}
return &StarbookServiceImpl{
conn: conn,
}, nil
}
func SetConsumerStarbookService(srv common.RPCService) {
dubbo.SetConsumerServiceWithInfo(srv, &StarbookService_ClientInfo)
}
// StarbookServiceImpl implements StarbookService.
type StarbookServiceImpl struct {
conn *client.Connection
}
func (c *StarbookServiceImpl) GetStarbookHome(ctx context.Context, req *GetStarbookHomeRequest, opts ...client.CallOption) (*GetStarbookHomeResponse, error) {
resp := new(GetStarbookHomeResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "GetStarbookHome", opts...); err != nil {
return nil, err
}
return resp, nil
}
func (c *StarbookServiceImpl) GetStarbookItems(ctx context.Context, req *GetStarbookItemsRequest, opts ...client.CallOption) (*GetStarbookItemsResponse, error) {
resp := new(GetStarbookItemsResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "GetStarbookItems", opts...); err != nil {
return nil, err
}
return resp, nil
}
var StarbookService_ClientInfo = client.ClientInfo{
InterfaceName: "topfans.starbook.StarbookService",
MethodNames: []string{"GetStarbookHome", "GetStarbookItems"},
ConnectionInjectFunc: func(dubboCliRaw interface{}, conn *client.Connection) {
dubboCli := dubboCliRaw.(*StarbookServiceImpl)
dubboCli.conn = conn
},
}
// StarbookServiceHandler is an implementation of the topfans.starbook.StarbookService service.
type StarbookServiceHandler interface {
GetStarbookHome(context.Context, *GetStarbookHomeRequest) (*GetStarbookHomeResponse, error)
GetStarbookItems(context.Context, *GetStarbookItemsRequest) (*GetStarbookItemsResponse, error)
}
func RegisterStarbookServiceHandler(srv *server.Server, hdlr StarbookServiceHandler, opts ...server.ServiceOption) error {
return srv.Register(hdlr, &StarbookService_ServiceInfo, opts...)
}
func SetProviderStarbookService(srv common.RPCService) {
dubbo.SetProviderServiceWithInfo(srv, &StarbookService_ServiceInfo)
}
var StarbookService_ServiceInfo = server.ServiceInfo{
InterfaceName: "topfans.starbook.StarbookService",
ServiceType: (*StarbookServiceHandler)(nil),
Methods: []server.MethodInfo{
{
Name: "GetStarbookHome",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(GetStarbookHomeRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*GetStarbookHomeRequest)
res, err := handler.(StarbookServiceHandler).GetStarbookHome(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "GetStarbookItems",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(GetStarbookItemsRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*GetStarbookItemsRequest)
res, err := handler.(StarbookServiceHandler).GetStarbookItems(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
},
}

View File

@ -18,7 +18,7 @@ message Asset {
string cover_url = 5; // URL
string material_url = 6; // URL
string description = 7; //
int32 rarity = 8; //
int32 grade = 8; //
repeated string tags = 9; //
string visibility = 10; // private, friends, public
int32 status = 11; // 0=Pending, 1=Active
@ -82,7 +82,7 @@ message CreateMintOrderRequest {
string name = 1; //
string material_url = 3; // URLOSS
string description = 4; //
int32 rarity = 5; //
int32 grade = 5; //
repeated string tags = 6; //
string material_type = 7; //
string info = 8; //

View File

@ -0,0 +1,99 @@
syntax = "proto3";
package topfans.starbook;
option go_package = "github.com/topfans/backend/pkg/proto/starbook;starbook";
import "proto/common.proto";
import "google/api/annotations.proto";
// - /
service StarbookService {
//
rpc GetStarbookHome(GetStarbookHomeRequest) returns (GetStarbookHomeResponse) {
option (google.api.http) = {
get: "/api/v1/starbook/home"
};
}
//
rpc GetStarbookItems(GetStarbookItemsRequest) returns (GetStarbookItemsResponse) {
option (google.api.http) = {
get: "/api/v1/starbook/items"
};
}
}
// ==================== ====================
//
message GetStarbookHomeRequest {
}
//
message GetStarbookHomeResponse {
topfans.common.BaseResponse base = 1;
StarbookHomeData data = 2;
}
//
message StarbookHomeData {
repeated AssetGroup groups = 1;
}
//
message AssetGroup {
string type = 1; // 'regular' / 'collection' / 'activity'
string category = 2; // 'castlove'(regular) / collection_category / activity_type
string category_name = 3;
// regular 使 grades collection/activity 使 flat items
repeated GradeSection grades = 4; // regular
repeated AssetItem items = 5; // collection / activity
int32 total_count = 6;
bool has_more = 7;
}
// regular 使
message GradeSection {
int32 grade = 1; // 1/2/3/4/5...
repeated AssetItem items = 2;
int32 total_count = 3;
bool has_more = 4;
}
//
message AssetItem {
int64 asset_id = 1;
string name = 2;
string cover_url_signed = 3; // URL
int32 like_count = 4;
int64 created_at = 5;
string category = 6; // regular: 'castlove' / collection: category / activity: activity_type
int32 grade = 7; // regular 1/2/3... 0
}
// ==================== ====================
//
message GetStarbookItemsRequest {
string type = 1; // 'regular' / 'collection' / 'activity'
string category = 2; // regular 'castlove'
int32 grade = 3; // regular 1/2/3...
int32 page = 4; // 1
int32 page_size = 5; // 20
}
//
message GetStarbookItemsResponse {
topfans.common.BaseResponse base = 1;
AssetListData data = 2;
}
//
message AssetListData {
repeated AssetItem items = 1;
int64 total = 2;
int32 page = 3;
int32 page_size = 4;
bool has_more = 5;
}

View File

@ -0,0 +1,108 @@
-- Starbook 典藏/活动藏品体系重构 - 数据库迁移
-- 创建时间: 2026-04-17
-- 状态: 新建(非破坏性变更)
-- =============================================
-- 1. 典藏藏品表 collection_assets
-- =============================================
CREATE TABLE IF NOT EXISTS collection_assets (
id BIGSERIAL PRIMARY KEY,
asset_id BIGINT UNIQUE NOT NULL,
owner_uid BIGINT NOT NULL,
star_id BIGINT NOT NULL,
name VARCHAR(100) NOT NULL,
cover_url VARCHAR(500) NOT NULL,
category VARCHAR(50) NOT NULL,
like_count INT NOT NULL DEFAULT 0,
status SMALLINT NOT NULL DEFAULT 0,
metadata JSONB,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL,
CONSTRAINT uk_collection_owner_star_name UNIQUE (owner_uid, star_id, name)
);
CREATE INDEX IF NOT EXISTS idx_collection_owner_star ON collection_assets (owner_uid, star_id);
CREATE INDEX IF NOT EXISTS idx_collection_category ON collection_assets (category);
CREATE INDEX IF NOT EXISTS idx_collection_asset_id ON collection_assets (asset_id);
-- =============================================
-- 2. 活动藏品表 activity_assets
-- =============================================
CREATE TABLE IF NOT EXISTS activity_assets (
id BIGSERIAL PRIMARY KEY,
asset_id BIGINT UNIQUE NOT NULL,
owner_uid BIGINT NOT NULL,
star_id BIGINT NOT NULL,
activity_id BIGINT NOT NULL,
activity_type VARCHAR(50) NOT NULL,
name VARCHAR(100) NOT NULL,
cover_url VARCHAR(500) NOT NULL,
like_count INT NOT NULL DEFAULT 0,
status SMALLINT NOT NULL DEFAULT 0,
metadata JSONB,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL,
CONSTRAINT uk_activity_owner_activity_name UNIQUE (owner_uid, activity_id, name)
);
CREATE INDEX IF NOT EXISTS idx_activity_owner ON activity_assets (owner_uid, activity_id);
CREATE INDEX IF NOT EXISTS idx_activity_star ON activity_assets (star_id, activity_id);
CREATE INDEX IF NOT EXISTS idx_activity_type ON activity_assets (activity_type);
CREATE INDEX IF NOT EXISTS idx_activity_asset_id ON activity_assets (asset_id);
-- =============================================
-- 3. 统一索引表 asset_registry
-- =============================================
CREATE TABLE IF NOT EXISTS asset_registry (
id BIGSERIAL PRIMARY KEY,
asset_id BIGINT NOT NULL,
asset_type VARCHAR(20) NOT NULL,
owner_uid BIGINT NOT NULL,
star_id BIGINT NOT NULL,
-- 普通藏品专属字段(其他类型时为 NULL
grade SMALLINT,
-- 典藏专属字段(其他类型时为 NULL
collection_category VARCHAR(50),
-- 活动专属字段(其他类型时为 NULL
activity_id BIGINT,
activity_type VARCHAR(50),
-- 公共字段
status SMALLINT NOT NULL DEFAULT 0,
like_count INT NOT NULL DEFAULT 0,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL,
CONSTRAINT uk_registry_asset_type_id UNIQUE (asset_type, asset_id),
CONSTRAINT uk_registry_owner_star_type_asset UNIQUE (owner_uid, star_id, asset_type, asset_id)
);
CREATE INDEX IF NOT EXISTS idx_registry_owner_star ON asset_registry (owner_uid, star_id);
CREATE INDEX IF NOT EXISTS idx_registry_type_star ON asset_registry (asset_type, star_id);
CREATE INDEX IF NOT EXISTS idx_registry_star_grade ON asset_registry (star_id, grade) WHERE asset_type = 'regular';
CREATE INDEX IF NOT EXISTS idx_registry_star_activity ON asset_registry (star_id, activity_id) WHERE asset_type = 'activity';
-- =============================================
-- 注释说明
-- =============================================
COMMENT ON TABLE collection_assets IS '典藏藏品表';
COMMENT ON TABLE activity_assets IS '活动藏品表';
COMMENT ON TABLE asset_registry IS '资产统一索引表';
COMMENT ON COLUMN collection_assets.category IS '典藏子分类(如 limited, classic';
COMMENT ON COLUMN activity_assets.activity_type IS '活动类型(如 birthday, anniversary, concert';
COMMENT ON COLUMN asset_registry.asset_type IS '资产类型regular(普通) | collection(典藏) | activity(活动)';
COMMENT ON COLUMN asset_registry.grade IS '普通藏品等级1/2/3...(仅 regular 类型有效)';
-- =============================================
-- 4. 修改 assets 表:将 rarity 改名为 grade
-- =============================================
-- 注意:如果 assets.rarity 字段不存在(新建数据库),则跳过此步
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'assets' AND column_name = 'rarity') THEN
ALTER TABLE assets RENAME COLUMN rarity TO grade;
COMMENT ON COLUMN assets.grade IS '星册等级1/2/3...铸爱流程创建时默认值为1';
END IF;
END $$;

View File

@ -26,6 +26,7 @@ import (
"github.com/topfans/backend/services/assetService/provider"
"github.com/topfans/backend/services/assetService/repository"
"github.com/topfans/backend/services/assetService/service"
starbookRepo "github.com/topfans/backend/services/starbookService/repository"
)
var (
@ -129,7 +130,8 @@ func main() {
// 创建 Service 层实例
assetService := service.NewAssetService(assetRepo, mintOrderRepo, assetLikeRepo, userClient, database.GetDB())
mintService := service.NewMintService(assetRepo, mintOrderRepo, userClient, database.GetDB(), config.GlobalAssetConfig)
registryRepo := starbookRepo.NewAssetRegistryRepository(database.GetDB())
mintService := service.NewMintService(assetRepo, mintOrderRepo, userClient, database.GetDB(), config.GlobalAssetConfig, registryRepo)
assetLikeService := service.NewAssetLikeService(assetRepo, assetLikeRepo, database.GetDB())
rankingService := service.NewRankingService(rankingRepo, userClient)
logger.Logger.Info("Service layer initialized")

View File

@ -0,0 +1,203 @@
package main
import (
"flag"
"fmt"
"os"
"os/signal"
"strconv"
"syscall"
_ "dubbo.apache.org/dubbo-go/v3/imports"
"dubbo.apache.org/dubbo-go/v3/protocol"
"dubbo.apache.org/dubbo-go/v3/server"
"github.com/topfans/backend/pkg/database"
"github.com/topfans/backend/pkg/health"
"github.com/topfans/backend/pkg/logger"
"github.com/topfans/backend/pkg/models"
pb "github.com/topfans/backend/pkg/proto/starbook"
assetRepo "github.com/topfans/backend/services/assetService/repository"
"github.com/topfans/backend/services/starbookService/provider"
"github.com/topfans/backend/services/starbookService/repository"
"github.com/topfans/backend/services/starbookService/service"
)
var (
port = flag.Int("port", getEnvInt("PORT", 20005), "Dubbo service port")
dbHost = flag.String("db-host", getEnv("DB_HOST", "localhost"), "Database host")
dbPort = flag.Int("db-port", getEnvInt("DB_PORT", 5432), "Database port")
dbUser = flag.String("db-user", getEnv("DB_USER", "postgres"), "Database user")
dbPassword = flag.String("db-password", getEnv("DB_PASSWORD", ""), "Database password")
dbName = flag.String("db-name", getEnv("DB_NAME", "top-fans"), "Database name")
healthHandler *health.Handler
)
func getEnv(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
func getEnvInt(key string, fallback int) int {
if v := os.Getenv(key); v != "" {
if n, err := strconv.Atoi(v); err == nil {
return n
}
}
return fallback
}
func main() {
flag.Parse()
// 初始化日志(必须在最前面)
env := os.Getenv("ENV")
if env == "" {
env = "development"
}
if err := logger.Init(logger.Config{
ServiceName: "starbook-service",
Environment: env,
LogLevel: os.Getenv("LOG_LEVEL"),
}); err != nil {
panic(fmt.Sprintf("Failed to initialize logger: %v", err))
}
defer logger.Sync()
logger.Sugar.Info("Starting Starbook Service...")
// 初始化数据库
if err := initDatabase(); err != nil {
logger.Sugar.Fatalf("Failed to initialize database: %v", err)
}
// 自动迁移数据库表
if err := autoMigrate(); err != nil {
logger.Sugar.Fatalf("Failed to migrate database: %v", err)
}
// 初始化 Repository
registryRepo := repository.NewAssetRegistryRepository(database.GetDB())
collectionRepo := repository.NewCollectionRepository(database.GetDB())
activityRepo := repository.NewActivityAssetRepository(database.GetDB())
assetRepository := assetRepo.NewAssetRepository(database.GetDB())
// 初始化 Service
starbookService := service.NewStarbookService(
database.GetDB(),
registryRepo,
assetRepository,
collectionRepo,
activityRepo,
)
// 初始化 Provider
starbookProvider := provider.NewStarbookProvider(starbookService)
// 初始化 Dubbo-go 服务器
if err := initDubboService(starbookProvider); err != nil {
logger.Sugar.Fatalf("Failed to initialize Dubbo service: %v", err)
}
// 等待信号(优雅关闭)
logger.Sugar.Info("Starbook service started successfully. Press Ctrl+C to exit.")
gracefulShutdown()
}
// initDatabase 初始化数据库连接
func initDatabase() error {
config := database.Config{
Host: *dbHost,
Port: *dbPort,
User: *dbUser,
Password: *dbPassword,
DBName: *dbName,
SSLMode: "disable",
TimeZone: "Asia/Shanghai",
}
return database.Init(config)
}
// autoMigrate 自动迁移数据库表
func autoMigrate() error {
db := database.GetDB()
if db == nil {
return fmt.Errorf("database is not initialized")
}
// 迁移星册相关的表
tables := []interface{}{
&models.CollectionAsset{},
&models.ActivityAsset{},
&models.AssetRegistry{},
}
for _, table := range tables {
if err := db.AutoMigrate(table); err != nil {
return fmt.Errorf("failed to migrate table: %w", err)
}
}
logger.Sugar.Info("Database migration completed successfully")
return nil
}
// initDubboService 初始化 Dubbo 服务
func initDubboService(starbookProvider *provider.StarbookProvider) error {
// 启动健康检查 HTTP 服务器
healthPort := *port + 1000 // e.g., 20005 -> 21005
healthHandler = health.NewHandler("starbook-service", healthPort)
healthHandler.Start()
// 创建 Dubbo Server
srv, err := server.NewServer(
server.WithServerProtocol(
protocol.WithPort(*port),
protocol.WithTriple(),
),
)
if err != nil {
return fmt.Errorf("failed to create Dubbo server: %w", err)
}
// 注册服务
if err := pb.RegisterStarbookServiceHandler(srv, starbookProvider); err != nil {
return fmt.Errorf("failed to register StarbookService handler: %w", err)
}
logger.Sugar.Infof("Dubbo-go provider registered successfully, service: topfans.starbook.StarbookService, port: %d", *port)
// 在后台启动 Dubbo 服务器
go func() {
if err := srv.Serve(); err != nil {
logger.Sugar.Fatalf("Failed to serve Dubbo: %v", err)
}
}()
return nil
}
// gracefulShutdown 优雅关闭
func gracefulShutdown() {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
logger.Sugar.Info("Shutting down Starbook Service...")
// 关闭健康检查服务器
if healthHandler != nil {
healthHandler.Stop()
}
// 关闭数据库连接
if err := database.Close(); err != nil {
logger.Sugar.Errorf("Error closing database: %v", err)
}
logger.Sugar.Info("Starbook Service stopped")
}

View File

@ -0,0 +1,221 @@
package provider
import (
"context"
"fmt"
"strconv"
"time"
"dubbo.apache.org/dubbo-go/v3/common/constant"
"github.com/topfans/backend/pkg/logger"
pb "github.com/topfans/backend/pkg/proto/starbook"
pbCommon "github.com/topfans/backend/pkg/proto/common"
"github.com/topfans/backend/services/starbookService/service"
"go.uber.org/zap"
)
// StarbookProvider 星册服务Provider实现
// 实现 Triple 协议生成的 StarbookServiceHandler 接口
type StarbookProvider struct {
starbookService service.StarbookService
}
// 确保 StarbookProvider 实现了 StarbookServiceHandler 接口
var _ pb.StarbookServiceHandler = (*StarbookProvider)(nil)
// NewStarbookProvider 创建星册服务Provider实例
func NewStarbookProvider(starbookService service.StarbookService) *StarbookProvider {
return &StarbookProvider{
starbookService: starbookService,
}
}
// GetStarbookHome 获取星册首页
func (p *StarbookProvider) GetStarbookHome(ctx context.Context, req *pb.GetStarbookHomeRequest) (*pb.GetStarbookHomeResponse, error) {
userID, starID, err := extractUserInfoFromDubboAttachments(ctx)
if err != nil {
return &pb.GetStarbookHomeResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_UNAUTHORIZED,
Message: "user authentication required",
Timestamp: 0,
},
}, err
}
resp, err := p.starbookService.GetStarbookHome(userID, starID)
if err != nil {
logger.Logger.Error("GetStarbookHome failed",
zap.Error(err),
)
return &pb.GetStarbookHomeResponse{
Base: &pbCommon.BaseResponse{
Code: toStatusCode(err),
Message: err.Error(),
Timestamp: time.Now().UnixMilli(),
},
}, err
}
return resp, nil
}
// GetStarbookItems 获取星册藏品列表
func (p *StarbookProvider) GetStarbookItems(ctx context.Context, req *pb.GetStarbookItemsRequest) (*pb.GetStarbookItemsResponse, error) {
userID, starID, err := extractUserInfoFromDubboAttachments(ctx)
if err != nil {
return &pb.GetStarbookItemsResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_UNAUTHORIZED,
Message: "user authentication required",
Timestamp: 0,
},
}, err
}
resp, err := p.starbookService.GetStarbookItems(req, userID, starID)
if err != nil {
logger.Logger.Error("GetStarbookItems failed",
zap.Error(err),
)
return &pb.GetStarbookItemsResponse{
Base: &pbCommon.BaseResponse{
Code: toStatusCode(err),
Message: err.Error(),
Timestamp: time.Now().UnixMilli(),
},
}, err
}
return resp, nil
}
// extractUserInfoFromDubboAttachments 从 Dubbo attachments 中提取用户信息
// 网关调用:网关已验证 Token 并将 user_id 和 star_id 通过 attachments 传递
func extractUserInfoFromDubboAttachments(ctx context.Context) (int64, int64, error) {
logger.Logger.Info("=== extractUserInfoFromDubboAttachments called ===")
// 使用 constant.AttachmentKey 获取 Dubbo attachments
attachments := ctx.Value(constant.AttachmentKey)
logger.Logger.Info("ctx.Value(constant.AttachmentKey) result",
zap.Any("attachments", attachments),
zap.String("type", fmt.Sprintf("%T", attachments)),
)
if attachments != nil {
logger.Logger.Info("Attachments found in context")
if attMap, ok := attachments.(map[string]interface{}); ok {
logger.Logger.Info("Attachments is map[string]interface{}",
zap.Int("map_size", len(attMap)))
userID, starID := extractUserInfoFromMap(attMap)
if userID > 0 && starID > 0 {
logger.Logger.Info("Successfully extracted user info",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
)
return userID, starID, nil
}
logger.Logger.Warn("Extracted zero user_id or star_id",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
)
} else {
logger.Logger.Warn("Attachments is not map[string]interface{}",
zap.String("actual_type", fmt.Sprintf("%T", attachments)),
zap.Any("value", attachments),
)
}
} else {
logger.Logger.Warn("No attachments found in context")
// 尝试打印所有 context 的值
logger.Logger.Warn("Context type", zap.String("type", fmt.Sprintf("%T", ctx)))
}
return 0, 0, fmt.Errorf("user info not found in Dubbo attachments (expected user_id and star_id from gateway)")
}
// getContextKeys 获取 context 中所有的 key
func getContextKeys(ctx context.Context) []string {
keys := make([]string, 0)
// 直接打印 ctx 类型
logger.Logger.Debug("Context type", zap.String("type", fmt.Sprintf("%T", ctx)))
return keys
}
// getContextValues 遍历 context 中的一些已知值
func getContextValues(ctx context.Context) map[string]interface{} {
result := make(map[string]interface{})
// 检查 constant.AttachmentKey
if v := ctx.Value(constant.AttachmentKey); v != nil {
result["AttachmentKey"] = v
}
return result
}
// extractUserInfoFromMap 从 map 中提取 user_id 和 star_id
// 支持多种类型int64, float64, string
func extractUserInfoFromMap(attMap map[string]interface{}) (int64, int64) {
var userID, starID int64
// 打印所有 key 和 value
for k, v := range attMap {
logger.Logger.Debug("Attachment map entry",
zap.String("key", k),
zap.Any("value", v),
zap.String("type", fmt.Sprintf("%T", v)),
)
}
// 提取 user_id
if v, ok := attMap["user_id"]; ok {
userID = parseIntValue(v)
}
// 提取 star_id
if v, ok := attMap["star_id"]; ok {
starID = parseIntValue(v)
}
return userID, starID
}
// parseIntValue 解析不同类型的整数值
func parseIntValue(v interface{}) int64 {
switch val := v.(type) {
case int64:
return val
case int:
return int64(val)
case float64:
return int64(val)
case string:
if i, err := strconv.ParseInt(val, 10, 64); err == nil {
return i
}
case []string:
if len(val) > 0 {
if i, err := strconv.ParseInt(val[0], 10, 64); err == nil {
return i
}
}
case []interface{}:
if len(val) > 0 {
if s, ok := val[0].(string); ok {
if i, err := strconv.ParseInt(s, 10, 64); err == nil {
return i
}
}
}
}
return 0
}
// toStatusCode 将错误转换为Proto状态码
func toStatusCode(err error) pbCommon.StatusCode {
if err == nil {
return pbCommon.StatusCode_STATUS_OK
}
// 简化处理,实际应该使用 appErrors.ToStatusCode
return pbCommon.StatusCode_STATUS_INTERNAL_ERROR
}

View File

@ -0,0 +1,232 @@
package repository
import (
"errors"
appErrors "github.com/topfans/backend/pkg/errors"
"github.com/topfans/backend/pkg/models"
"gorm.io/gorm"
)
// ActivityAssetRepository 活动藏品Repository接口
type ActivityAssetRepository interface {
// Create 创建活动藏品
Create(asset *models.ActivityAsset) error
// GetByID 根据ID查询
GetByID(id int64) (*models.ActivityAsset, error)
// GetByAssetID 根据asset_id查询
GetByAssetID(assetID int64) (*models.ActivityAsset, error)
// GetByOwner 查询用户的活动藏品列表
GetByOwner(ownerUID, starID int64, limit, offset int) ([]*models.ActivityAsset, error)
// GetByOwnerAndActivityType 查询用户指定活动类型的活动藏品
GetByOwnerAndActivityType(ownerUID, starID int64, activityType string, limit, offset int) ([]*models.ActivityAsset, error)
// GetByOwnerAndActivityID 查询用户指定活动的活动藏品
GetByOwnerAndActivityID(ownerUID, starID int64, activityID int64, limit, offset int) ([]*models.ActivityAsset, error)
// CountByOwner 统计用户的活动藏品数量
CountByOwner(ownerUID, starID int64) (int64, error)
// CountByOwnerAndActivityType 统计用户指定活动类型的活动藏品数量
CountByOwnerAndActivityType(ownerUID, starID int64, activityType string) (int64, error)
// UpdateLikeCount 更新点赞数
UpdateLikeCount(id int64, likeCount int32) error
// IncrementLikeCount 增加点赞数
IncrementLikeCount(id int64) error
// DecrementLikeCount 减少点赞数
DecrementLikeCount(id int64) error
}
// activityAssetRepository 活动藏品Repository实现
type activityAssetRepository struct {
db *gorm.DB
}
// NewActivityAssetRepository 创建活动藏品Repository实例
func NewActivityAssetRepository(db *gorm.DB) ActivityAssetRepository {
return &activityAssetRepository{db: db}
}
// Create 创建活动藏品
func (r *activityAssetRepository) Create(asset *models.ActivityAsset) error {
if asset == nil {
return errors.New("activity asset cannot be nil")
}
if asset.OwnerUID <= 0 {
return errors.New("owner_uid must be greater than 0")
}
if asset.StarID <= 0 {
return errors.New("star_id must be greater than 0")
}
return r.db.Create(asset).Error
}
// GetByID 根据ID查询
func (r *activityAssetRepository) GetByID(id int64) (*models.ActivityAsset, error) {
if id <= 0 {
return nil, errors.New("id must be greater than 0")
}
var asset models.ActivityAsset
if err := r.db.Where("id = ?", id).First(&asset).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, appErrors.ErrActivityAssetNotFound
}
return nil, err
}
return &asset, nil
}
// GetByAssetID 根据asset_id查询
func (r *activityAssetRepository) GetByAssetID(assetID int64) (*models.ActivityAsset, error) {
if assetID <= 0 {
return nil, errors.New("asset_id must be greater than 0")
}
var asset models.ActivityAsset
if err := r.db.Where("asset_id = ?", assetID).First(&asset).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, appErrors.ErrActivityAssetNotFound
}
return nil, err
}
return &asset, nil
}
// GetByOwner 查询用户的活动藏品列表
func (r *activityAssetRepository) GetByOwner(ownerUID, starID int64, limit, offset int) ([]*models.ActivityAsset, error) {
if ownerUID <= 0 {
return nil, errors.New("owner_uid must be greater than 0")
}
if starID <= 0 {
return nil, errors.New("star_id must be greater than 0")
}
var assets []*models.ActivityAsset
query := r.db.Where("owner_uid = ? AND star_id = ?", ownerUID, starID).
Order("created_at DESC")
if limit > 0 {
query = query.Limit(limit)
}
if offset > 0 {
query = query.Offset(offset)
}
if err := query.Find(&assets).Error; err != nil {
return nil, err
}
return assets, nil
}
// GetByOwnerAndActivityType 查询用户指定活动类型的活动藏品
func (r *activityAssetRepository) GetByOwnerAndActivityType(ownerUID, starID int64, activityType string, limit, offset int) ([]*models.ActivityAsset, error) {
if ownerUID <= 0 {
return nil, errors.New("owner_uid must be greater than 0")
}
if starID <= 0 {
return nil, errors.New("star_id must be greater than 0")
}
var assets []*models.ActivityAsset
query := r.db.Where("owner_uid = ? AND star_id = ? AND activity_type = ?", ownerUID, starID, activityType).
Order("created_at DESC")
if limit > 0 {
query = query.Limit(limit)
}
if offset > 0 {
query = query.Offset(offset)
}
if err := query.Find(&assets).Error; err != nil {
return nil, err
}
return assets, nil
}
// GetByOwnerAndActivityID 查询用户指定活动的活动藏品
func (r *activityAssetRepository) GetByOwnerAndActivityID(ownerUID, starID int64, activityID int64, limit, offset int) ([]*models.ActivityAsset, error) {
if ownerUID <= 0 {
return nil, errors.New("owner_uid must be greater than 0")
}
if starID <= 0 {
return nil, errors.New("star_id must be greater than 0")
}
var assets []*models.ActivityAsset
query := r.db.Where("owner_uid = ? AND star_id = ? AND activity_id = ?", ownerUID, starID, activityID).
Order("created_at DESC")
if limit > 0 {
query = query.Limit(limit)
}
if offset > 0 {
query = query.Offset(offset)
}
if err := query.Find(&assets).Error; err != nil {
return nil, err
}
return assets, nil
}
// CountByOwner 统计用户的活动藏品数量
func (r *activityAssetRepository) CountByOwner(ownerUID, starID int64) (int64, error) {
if ownerUID <= 0 {
return 0, errors.New("owner_uid must be greater than 0")
}
if starID <= 0 {
return 0, errors.New("star_id must be greater than 0")
}
var count int64
if err := r.db.Model(&models.ActivityAsset{}).
Where("owner_uid = ? AND star_id = ?", ownerUID, starID).
Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
// CountByOwnerAndActivityType 统计用户指定活动类型的活动藏品数量
func (r *activityAssetRepository) CountByOwnerAndActivityType(ownerUID, starID int64, activityType string) (int64, error) {
if ownerUID <= 0 {
return 0, errors.New("owner_uid must be greater than 0")
}
if starID <= 0 {
return 0, errors.New("star_id must be greater than 0")
}
var count int64
if err := r.db.Model(&models.ActivityAsset{}).
Where("owner_uid = ? AND star_id = ? AND activity_type = ?", ownerUID, starID, activityType).
Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
// UpdateLikeCount 更新点赞数
func (r *activityAssetRepository) UpdateLikeCount(id int64, likeCount int32) error {
if id <= 0 {
return errors.New("id must be greater than 0")
}
return r.db.Model(&models.ActivityAsset{}).
Where("id = ?", id).
Update("like_count", likeCount).Error
}
// IncrementLikeCount 增加点赞数
func (r *activityAssetRepository) IncrementLikeCount(id int64) error {
if id <= 0 {
return errors.New("id must be greater than 0")
}
return r.db.Model(&models.ActivityAsset{}).
Where("id = ?", id).
UpdateColumn("like_count", gorm.Expr("like_count + ?", 1)).Error
}
// DecrementLikeCount 减少点赞数
func (r *activityAssetRepository) DecrementLikeCount(id int64) error {
if id <= 0 {
return errors.New("id must be greater than 0")
}
return r.db.Model(&models.ActivityAsset{}).
Where("id = ? AND like_count > ?", id, 0).
UpdateColumn("like_count", gorm.Expr("like_count - ?", 1)).Error
}

View File

@ -0,0 +1,372 @@
package repository
import (
"errors"
appErrors "github.com/topfans/backend/pkg/errors"
"github.com/topfans/backend/pkg/models"
"gorm.io/gorm"
)
// AssetRegistryRepository 资产统一索引Repository接口
type AssetRegistryRepository interface {
// Create 创建索引记录
Create(registry *models.AssetRegistry) error
// GetByID 根据ID查询
GetByID(id int64) (*models.AssetRegistry, error)
// GetByAssetID 根据asset_id查询
GetByAssetID(assetID int64) (*models.AssetRegistry, error)
// GetByAssetTypeAndID 根据类型和asset_id查询
GetByAssetTypeAndID(assetType string, assetID int64) (*models.AssetRegistry, error)
// GetByOwner 查询用户的所有索引记录
GetByOwner(ownerUID, starID int64) ([]*models.AssetRegistry, error)
// GetByOwnerAndType 查询用户指定类型的索引记录
GetByOwnerAndType(ownerUID, starID int64, assetType string, limit, offset int) ([]*models.AssetRegistry, error)
// GetByOwnerAndTypeAndGrade 查询用户指定类型和等级的索引记录
GetByOwnerAndTypeAndGrade(ownerUID, starID int64, assetType string, grade int32, limit, offset int) ([]*models.AssetRegistry, error)
// GetByOwnerAndTypeAndCategory 查询用户指定类型和分类的索引记录
GetByOwnerAndTypeAndCategory(ownerUID, starID int64, assetType string, category string, limit, offset int) ([]*models.AssetRegistry, error)
// GetByOwnerAndTypeAndActivity 查询用户指定类型和活动的索引记录
GetByOwnerAndTypeAndActivity(ownerUID, starID int64, assetType string, activityID int64, limit, offset int) ([]*models.AssetRegistry, error)
// CountByOwner 统计用户的索引记录数量
CountByOwner(ownerUID, starID int64) (int64, error)
// CountByOwnerAndType 统计用户指定类型的索引记录数量
CountByOwnerAndType(ownerUID, starID int64, assetType string) (int64, error)
// CountByOwnerAndTypeAndGrade 统计用户指定类型和等级的索引记录数量
CountByOwnerAndTypeAndGrade(ownerUID, starID int64, assetType string, grade int32) (int64, error)
// CountByOwnerAndTypeAndCategory 统计用户指定类型和分类的索引记录数量
CountByOwnerAndTypeAndCategory(ownerUID, starID int64, assetType string, category string) (int64, error)
// CountByOwnerAndTypeAndActivity 统计用户指定类型和活动的索引记录数量
CountByOwnerAndTypeAndActivity(ownerUID, starID int64, assetType string, activityID int64) (int64, error)
// UpdateLikeCount 更新点赞数
UpdateLikeCount(assetID int64, likeCount int32) error
// UpdateGrade 更新等级
UpdateGrade(assetID int64, grade int32) error
// Delete 删除索引记录
Delete(assetID int64) error
// DeleteByAssetType 删除指定类型的索引记录
DeleteByAssetType(assetType string, assetID int64) error
}
// assetRegistryRepository 资产统一索引Repository实现
type assetRegistryRepository struct {
db *gorm.DB
}
// NewAssetRegistryRepository 创建资产统一索引Repository实例
func NewAssetRegistryRepository(db *gorm.DB) AssetRegistryRepository {
return &assetRegistryRepository{db: db}
}
// Create 创建索引记录
func (r *assetRegistryRepository) Create(registry *models.AssetRegistry) error {
if registry == nil {
return errors.New("registry cannot be nil")
}
if registry.OwnerUID <= 0 {
return errors.New("owner_uid must be greater than 0")
}
if registry.StarID <= 0 {
return errors.New("star_id must be greater than 0")
}
return r.db.Create(registry).Error
}
// GetByID 根据ID查询
func (r *assetRegistryRepository) GetByID(id int64) (*models.AssetRegistry, error) {
if id <= 0 {
return nil, errors.New("id must be greater than 0")
}
var registry models.AssetRegistry
if err := r.db.Where("id = ?", id).First(&registry).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, appErrors.ErrAssetRegistryNotFound
}
return nil, err
}
return &registry, nil
}
// GetByAssetID 根据asset_id查询
func (r *assetRegistryRepository) GetByAssetID(assetID int64) (*models.AssetRegistry, error) {
if assetID <= 0 {
return nil, errors.New("asset_id must be greater than 0")
}
var registry models.AssetRegistry
if err := r.db.Where("asset_id = ?", assetID).First(&registry).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, appErrors.ErrAssetRegistryNotFound
}
return nil, err
}
return &registry, nil
}
// GetByAssetTypeAndID 根据类型和asset_id查询
func (r *assetRegistryRepository) GetByAssetTypeAndID(assetType string, assetID int64) (*models.AssetRegistry, error) {
if assetType == "" {
return nil, errors.New("asset_type must not be empty")
}
if assetID <= 0 {
return nil, errors.New("asset_id must be greater than 0")
}
var registry models.AssetRegistry
if err := r.db.Where("asset_type = ? AND asset_id = ?", assetType, assetID).First(&registry).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, appErrors.ErrAssetRegistryNotFound
}
return nil, err
}
return &registry, nil
}
// GetByOwner 查询用户的所有索引记录
func (r *assetRegistryRepository) GetByOwner(ownerUID, starID int64) ([]*models.AssetRegistry, error) {
if ownerUID <= 0 {
return nil, errors.New("owner_uid must be greater than 0")
}
if starID <= 0 {
return nil, errors.New("star_id must be greater than 0")
}
var registries []*models.AssetRegistry
if err := r.db.Where("owner_uid = ? AND star_id = ?", ownerUID, starID).
Order("created_at DESC").
Find(&registries).Error; err != nil {
return nil, err
}
return registries, nil
}
// GetByOwnerAndType 查询用户指定类型的索引记录
func (r *assetRegistryRepository) GetByOwnerAndType(ownerUID, starID int64, assetType string, limit, offset int) ([]*models.AssetRegistry, error) {
if ownerUID <= 0 {
return nil, errors.New("owner_uid must be greater than 0")
}
if starID <= 0 {
return nil, errors.New("star_id must be greater than 0")
}
var registries []*models.AssetRegistry
query := r.db.Where("owner_uid = ? AND star_id = ? AND asset_type = ?", ownerUID, starID, assetType).
Order("created_at DESC")
if limit > 0 {
query = query.Limit(limit)
}
if offset > 0 {
query = query.Offset(offset)
}
if err := query.Find(&registries).Error; err != nil {
return nil, err
}
return registries, nil
}
// GetByOwnerAndTypeAndGrade 查询用户指定类型和等级的索引记录
func (r *assetRegistryRepository) GetByOwnerAndTypeAndGrade(ownerUID, starID int64, assetType string, grade int32, limit, offset int) ([]*models.AssetRegistry, error) {
if ownerUID <= 0 {
return nil, errors.New("owner_uid must be greater than 0")
}
if starID <= 0 {
return nil, errors.New("star_id must be greater than 0")
}
var registries []*models.AssetRegistry
query := r.db.Where("owner_uid = ? AND star_id = ? AND asset_type = ? AND grade = ?", ownerUID, starID, assetType, grade).
Order("created_at DESC")
if limit > 0 {
query = query.Limit(limit)
}
if offset > 0 {
query = query.Offset(offset)
}
if err := query.Find(&registries).Error; err != nil {
return nil, err
}
return registries, nil
}
// GetByOwnerAndTypeAndCategory 查询用户指定类型和分类的索引记录
func (r *assetRegistryRepository) GetByOwnerAndTypeAndCategory(ownerUID, starID int64, assetType string, category string, limit, offset int) ([]*models.AssetRegistry, error) {
if ownerUID <= 0 {
return nil, errors.New("owner_uid must be greater than 0")
}
if starID <= 0 {
return nil, errors.New("star_id must be greater than 0")
}
var registries []*models.AssetRegistry
query := r.db.Where("owner_uid = ? AND star_id = ? AND asset_type = ? AND collection_category = ?", ownerUID, starID, assetType, category).
Order("created_at DESC")
if limit > 0 {
query = query.Limit(limit)
}
if offset > 0 {
query = query.Offset(offset)
}
if err := query.Find(&registries).Error; err != nil {
return nil, err
}
return registries, nil
}
// GetByOwnerAndTypeAndActivity 查询用户指定类型和活动的索引记录
func (r *assetRegistryRepository) GetByOwnerAndTypeAndActivity(ownerUID, starID int64, assetType string, activityID int64, limit, offset int) ([]*models.AssetRegistry, error) {
if ownerUID <= 0 {
return nil, errors.New("owner_uid must be greater than 0")
}
if starID <= 0 {
return nil, errors.New("star_id must be greater than 0")
}
var registries []*models.AssetRegistry
query := r.db.Where("owner_uid = ? AND star_id = ? AND asset_type = ? AND activity_id = ?", ownerUID, starID, assetType, activityID).
Order("created_at DESC")
if limit > 0 {
query = query.Limit(limit)
}
if offset > 0 {
query = query.Offset(offset)
}
if err := query.Find(&registries).Error; err != nil {
return nil, err
}
return registries, nil
}
// CountByOwner 统计用户的索引记录数量
func (r *assetRegistryRepository) CountByOwner(ownerUID, starID int64) (int64, error) {
if ownerUID <= 0 {
return 0, errors.New("owner_uid must be greater than 0")
}
if starID <= 0 {
return 0, errors.New("star_id must be greater than 0")
}
var count int64
if err := r.db.Model(&models.AssetRegistry{}).
Where("owner_uid = ? AND star_id = ?", ownerUID, starID).
Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
// CountByOwnerAndType 统计用户指定类型的索引记录数量
func (r *assetRegistryRepository) CountByOwnerAndType(ownerUID, starID int64, assetType string) (int64, error) {
if ownerUID <= 0 {
return 0, errors.New("owner_uid must be greater than 0")
}
if starID <= 0 {
return 0, errors.New("star_id must be greater than 0")
}
var count int64
if err := r.db.Model(&models.AssetRegistry{}).
Where("owner_uid = ? AND star_id = ? AND asset_type = ?", ownerUID, starID, assetType).
Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
// CountByOwnerAndTypeAndGrade 统计用户指定类型和等级的索引记录数量
func (r *assetRegistryRepository) CountByOwnerAndTypeAndGrade(ownerUID, starID int64, assetType string, grade int32) (int64, error) {
if ownerUID <= 0 {
return 0, errors.New("owner_uid must be greater than 0")
}
if starID <= 0 {
return 0, errors.New("star_id must be greater than 0")
}
var count int64
if err := r.db.Model(&models.AssetRegistry{}).
Where("owner_uid = ? AND star_id = ? AND asset_type = ? AND grade = ?", ownerUID, starID, assetType, grade).
Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
// CountByOwnerAndTypeAndCategory 统计用户指定类型和分类的索引记录数量
func (r *assetRegistryRepository) CountByOwnerAndTypeAndCategory(ownerUID, starID int64, assetType string, category string) (int64, error) {
if ownerUID <= 0 {
return 0, errors.New("owner_uid must be greater than 0")
}
if starID <= 0 {
return 0, errors.New("star_id must be greater than 0")
}
var count int64
if err := r.db.Model(&models.AssetRegistry{}).
Where("owner_uid = ? AND star_id = ? AND asset_type = ? AND collection_category = ?", ownerUID, starID, assetType, category).
Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
// CountByOwnerAndTypeAndActivity 统计用户指定类型和活动的索引记录数量
func (r *assetRegistryRepository) CountByOwnerAndTypeAndActivity(ownerUID, starID int64, assetType string, activityID int64) (int64, error) {
if ownerUID <= 0 {
return 0, errors.New("owner_uid must be greater than 0")
}
if starID <= 0 {
return 0, errors.New("star_id must be greater than 0")
}
var count int64
if err := r.db.Model(&models.AssetRegistry{}).
Where("owner_uid = ? AND star_id = ? AND asset_type = ? AND activity_id = ?", ownerUID, starID, assetType, activityID).
Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
// UpdateLikeCount 更新点赞数
func (r *assetRegistryRepository) UpdateLikeCount(assetID int64, likeCount int32) error {
if assetID <= 0 {
return errors.New("asset_id must be greater than 0")
}
return r.db.Model(&models.AssetRegistry{}).
Where("asset_id = ?", assetID).
Update("like_count", likeCount).Error
}
// UpdateGrade 更新等级
func (r *assetRegistryRepository) UpdateGrade(assetID int64, grade int32) error {
if assetID <= 0 {
return errors.New("asset_id must be greater than 0")
}
return r.db.Model(&models.AssetRegistry{}).
Where("asset_id = ?", assetID).
Update("grade", grade).Error
}
// Delete 删除索引记录
func (r *assetRegistryRepository) Delete(assetID int64) error {
if assetID <= 0 {
return errors.New("asset_id must be greater than 0")
}
return r.db.Where("asset_id = ?", assetID).Delete(&models.AssetRegistry{}).Error
}
// DeleteByAssetType 删除指定类型的索引记录
func (r *assetRegistryRepository) DeleteByAssetType(assetType string, assetID int64) error {
if assetType == "" {
return errors.New("asset_type must not be empty")
}
if assetID <= 0 {
return errors.New("asset_id must be greater than 0")
}
return r.db.Where("asset_type = ? AND asset_id = ?", assetType, assetID).
Delete(&models.AssetRegistry{}).Error
}

View File

@ -0,0 +1,206 @@
package repository
import (
"errors"
appErrors "github.com/topfans/backend/pkg/errors"
"github.com/topfans/backend/pkg/models"
"gorm.io/gorm"
)
// CollectionRepository 典藏藏品Repository接口
type CollectionRepository interface {
// Create 创建典藏藏品
Create(asset *models.CollectionAsset) error
// GetByID 根据ID查询
GetByID(id int64) (*models.CollectionAsset, error)
// GetByAssetID 根据asset_id查询
GetByAssetID(assetID int64) (*models.CollectionAsset, error)
// GetByOwner 查询用户的典藏藏品列表
GetByOwner(ownerUID, starID int64, limit, offset int) ([]*models.CollectionAsset, error)
// GetByOwnerAndCategory 查询用户指定分类的典藏藏品
GetByOwnerAndCategory(ownerUID, starID int64, category string, limit, offset int) ([]*models.CollectionAsset, error)
// CountByOwner 统计用户的典藏藏品数量
CountByOwner(ownerUID, starID int64) (int64, error)
// CountByOwnerAndCategory 统计用户指定分类的典藏藏品数量
CountByOwnerAndCategory(ownerUID, starID int64, category string) (int64, error)
// UpdateLikeCount 更新点赞数
UpdateLikeCount(id int64, likeCount int32) error
// IncrementLikeCount 增加点赞数
IncrementLikeCount(id int64) error
// DecrementLikeCount 减少点赞数
DecrementLikeCount(id int64) error
}
// collectionRepository 典藏藏品Repository实现
type collectionRepository struct {
db *gorm.DB
}
// NewCollectionRepository 创建典藏藏品Repository实例
func NewCollectionRepository(db *gorm.DB) CollectionRepository {
return &collectionRepository{db: db}
}
// Create 创建典藏藏品
func (r *collectionRepository) Create(asset *models.CollectionAsset) error {
if asset == nil {
return errors.New("collection asset cannot be nil")
}
if asset.OwnerUID <= 0 {
return errors.New("owner_uid must be greater than 0")
}
if asset.StarID <= 0 {
return errors.New("star_id must be greater than 0")
}
return r.db.Create(asset).Error
}
// GetByID 根据ID查询
func (r *collectionRepository) GetByID(id int64) (*models.CollectionAsset, error) {
if id <= 0 {
return nil, errors.New("id must be greater than 0")
}
var asset models.CollectionAsset
if err := r.db.Where("id = ?", id).First(&asset).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, appErrors.ErrCollectionAssetNotFound
}
return nil, err
}
return &asset, nil
}
// GetByAssetID 根据asset_id查询
func (r *collectionRepository) GetByAssetID(assetID int64) (*models.CollectionAsset, error) {
if assetID <= 0 {
return nil, errors.New("asset_id must be greater than 0")
}
var asset models.CollectionAsset
if err := r.db.Where("asset_id = ?", assetID).First(&asset).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, appErrors.ErrCollectionAssetNotFound
}
return nil, err
}
return &asset, nil
}
// GetByOwner 查询用户的典藏藏品列表
func (r *collectionRepository) GetByOwner(ownerUID, starID int64, limit, offset int) ([]*models.CollectionAsset, error) {
if ownerUID <= 0 {
return nil, errors.New("owner_uid must be greater than 0")
}
if starID <= 0 {
return nil, errors.New("star_id must be greater than 0")
}
var assets []*models.CollectionAsset
query := r.db.Where("owner_uid = ? AND star_id = ?", ownerUID, starID).
Order("created_at DESC")
if limit > 0 {
query = query.Limit(limit)
}
if offset > 0 {
query = query.Offset(offset)
}
if err := query.Find(&assets).Error; err != nil {
return nil, err
}
return assets, nil
}
// GetByOwnerAndCategory 查询用户指定分类的典藏藏品
func (r *collectionRepository) GetByOwnerAndCategory(ownerUID, starID int64, category string, limit, offset int) ([]*models.CollectionAsset, error) {
if ownerUID <= 0 {
return nil, errors.New("owner_uid must be greater than 0")
}
if starID <= 0 {
return nil, errors.New("star_id must be greater than 0")
}
var assets []*models.CollectionAsset
query := r.db.Where("owner_uid = ? AND star_id = ? AND category = ?", ownerUID, starID, category).
Order("created_at DESC")
if limit > 0 {
query = query.Limit(limit)
}
if offset > 0 {
query = query.Offset(offset)
}
if err := query.Find(&assets).Error; err != nil {
return nil, err
}
return assets, nil
}
// CountByOwner 统计用户的典藏藏品数量
func (r *collectionRepository) CountByOwner(ownerUID, starID int64) (int64, error) {
if ownerUID <= 0 {
return 0, errors.New("owner_uid must be greater than 0")
}
if starID <= 0 {
return 0, errors.New("star_id must be greater than 0")
}
var count int64
if err := r.db.Model(&models.CollectionAsset{}).
Where("owner_uid = ? AND star_id = ?", ownerUID, starID).
Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
// CountByOwnerAndCategory 统计用户指定分类的典藏藏品数量
func (r *collectionRepository) CountByOwnerAndCategory(ownerUID, starID int64, category string) (int64, error) {
if ownerUID <= 0 {
return 0, errors.New("owner_uid must be greater than 0")
}
if starID <= 0 {
return 0, errors.New("star_id must be greater than 0")
}
var count int64
if err := r.db.Model(&models.CollectionAsset{}).
Where("owner_uid = ? AND star_id = ? AND category = ?", ownerUID, starID, category).
Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
// UpdateLikeCount 更新点赞数
func (r *collectionRepository) UpdateLikeCount(id int64, likeCount int32) error {
if id <= 0 {
return errors.New("id must be greater than 0")
}
return r.db.Model(&models.CollectionAsset{}).
Where("id = ?", id).
Update("like_count", likeCount).Error
}
// IncrementLikeCount 增加点赞数
func (r *collectionRepository) IncrementLikeCount(id int64) error {
if id <= 0 {
return errors.New("id must be greater than 0")
}
return r.db.Model(&models.CollectionAsset{}).
Where("id = ?", id).
UpdateColumn("like_count", gorm.Expr("like_count + ?", 1)).Error
}
// DecrementLikeCount 减少点赞数
func (r *collectionRepository) DecrementLikeCount(id int64) error {
if id <= 0 {
return errors.New("id must be greater than 0")
}
return r.db.Model(&models.CollectionAsset{}).
Where("id = ? AND like_count > ?", id, 0).
UpdateColumn("like_count", gorm.Expr("like_count - ?", 1)).Error
}

View File

@ -0,0 +1,132 @@
package service
import (
"time"
appErrors "github.com/topfans/backend/pkg/errors"
"github.com/topfans/backend/pkg/models"
"github.com/topfans/backend/services/starbookService/repository"
)
// ActivityAssetService 活动藏品服务接口
type ActivityAssetService interface {
// Create 创建活动藏品
Create(asset *models.ActivityAsset) error
// GetByID 根据ID获取
GetByID(id int64) (*models.ActivityAsset, error)
// GetByAssetID 根据asset_id获取
GetByAssetID(assetID int64) (*models.ActivityAsset, error)
// GetByOwner 获取用户的活动藏品
GetByOwner(ownerUID, starID int64, limit, offset int) ([]*models.ActivityAsset, error)
// GetByOwnerAndActivityType 获取用户指定活动类型的活动藏品
GetByOwnerAndActivityType(ownerUID, starID int64, activityType string, limit, offset int) ([]*models.ActivityAsset, error)
// GetByOwnerAndActivityID 获取用户指定活动的活动藏品
GetByOwnerAndActivityID(ownerUID, starID int64, activityID int64, limit, offset int) ([]*models.ActivityAsset, error)
// CountByOwner 统计用户的活动藏品数量
CountByOwner(ownerUID, starID int64) (int64, error)
// CountByOwnerAndActivityType 统计用户指定活动类型的活动藏品数量
CountByOwnerAndActivityType(ownerUID, starID int64, activityType string) (int64, error)
// IncrementLikeCount 增加点赞数
IncrementLikeCount(id int64) error
// DecrementLikeCount 减少点赞数
DecrementLikeCount(id int64) error
}
// activityAssetService 活动藏品服务实现
type activityAssetService struct {
activityRepo repository.ActivityAssetRepository
registryRepo repository.AssetRegistryRepository
}
// NewActivityAssetService 创建活动藏品服务实例
func NewActivityAssetService(
activityRepo repository.ActivityAssetRepository,
registryRepo repository.AssetRegistryRepository,
) ActivityAssetService {
return &activityAssetService{
activityRepo: activityRepo,
registryRepo: registryRepo,
}
}
// Create 创建活动藏品
func (s *activityAssetService) Create(asset *models.ActivityAsset) error {
if asset == nil {
return appErrors.ErrInvalidAssetStatus
}
// 创建活动藏品记录
if err := s.activityRepo.Create(asset); err != nil {
return err
}
// 同步写入 asset_registry
registry := &models.AssetRegistry{
AssetID: asset.AssetID,
AssetType: models.AssetTypeActivity,
OwnerUID: asset.OwnerUID,
StarID: asset.StarID,
ActivityID: &asset.ActivityID,
ActivityType: &asset.ActivityType,
Status: asset.Status,
LikeCount: asset.LikeCount,
CreatedAt: time.Now().UnixMilli(),
UpdatedAt: time.Now().UnixMilli(),
}
return s.registryRepo.Create(registry)
}
// GetByID 根据ID获取
func (s *activityAssetService) GetByID(id int64) (*models.ActivityAsset, error) {
return s.activityRepo.GetByID(id)
}
// GetByAssetID 根据asset_id获取
func (s *activityAssetService) GetByAssetID(assetID int64) (*models.ActivityAsset, error) {
return s.activityRepo.GetByAssetID(assetID)
}
// GetByOwner 获取用户的活动藏品
func (s *activityAssetService) GetByOwner(ownerUID, starID int64, limit, offset int) ([]*models.ActivityAsset, error) {
return s.activityRepo.GetByOwner(ownerUID, starID, limit, offset)
}
// GetByOwnerAndActivityType 获取用户指定活动类型的活动藏品
func (s *activityAssetService) GetByOwnerAndActivityType(ownerUID, starID int64, activityType string, limit, offset int) ([]*models.ActivityAsset, error) {
return s.activityRepo.GetByOwnerAndActivityType(ownerUID, starID, activityType, limit, offset)
}
// GetByOwnerAndActivityID 获取用户指定活动的活动藏品
func (s *activityAssetService) GetByOwnerAndActivityID(ownerUID, starID int64, activityID int64, limit, offset int) ([]*models.ActivityAsset, error) {
return s.activityRepo.GetByOwnerAndActivityID(ownerUID, starID, activityID, limit, offset)
}
// CountByOwner 统计用户的活动藏品数量
func (s *activityAssetService) CountByOwner(ownerUID, starID int64) (int64, error) {
return s.activityRepo.CountByOwner(ownerUID, starID)
}
// CountByOwnerAndActivityType 统计用户指定活动类型的活动藏品数量
func (s *activityAssetService) CountByOwnerAndActivityType(ownerUID, starID int64, activityType string) (int64, error) {
return s.activityRepo.CountByOwnerAndActivityType(ownerUID, starID, activityType)
}
// IncrementLikeCount 增加点赞数
func (s *activityAssetService) IncrementLikeCount(id int64) error {
return s.activityRepo.IncrementLikeCount(id)
}
// DecrementLikeCount 减少点赞数
func (s *activityAssetService) DecrementLikeCount(id int64) error {
return s.activityRepo.DecrementLikeCount(id)
}

View File

@ -0,0 +1,123 @@
package service
import (
"time"
appErrors "github.com/topfans/backend/pkg/errors"
"github.com/topfans/backend/pkg/models"
"github.com/topfans/backend/services/starbookService/repository"
)
// CollectionService 典藏服务接口
type CollectionService interface {
// Create 创建典藏藏品
Create(asset *models.CollectionAsset) error
// GetByID 根据ID获取
GetByID(id int64) (*models.CollectionAsset, error)
// GetByAssetID 根据asset_id获取
GetByAssetID(assetID int64) (*models.CollectionAsset, error)
// GetByOwner 获取用户的典藏藏品
GetByOwner(ownerUID, starID int64, limit, offset int) ([]*models.CollectionAsset, error)
// GetByOwnerAndCategory 获取用户指定分类的典藏藏品
GetByOwnerAndCategory(ownerUID, starID int64, category string, limit, offset int) ([]*models.CollectionAsset, error)
// CountByOwner 统计用户的典藏藏品数量
CountByOwner(ownerUID, starID int64) (int64, error)
// CountByOwnerAndCategory 统计用户指定分类的典藏藏品数量
CountByOwnerAndCategory(ownerUID, starID int64, category string) (int64, error)
// IncrementLikeCount 增加点赞数
IncrementLikeCount(id int64) error
// DecrementLikeCount 减少点赞数
DecrementLikeCount(id int64) error
}
// collectionService 典藏服务实现
type collectionService struct {
collectionRepo repository.CollectionRepository
registryRepo repository.AssetRegistryRepository
}
// NewCollectionService 创建典藏服务实例
func NewCollectionService(
collectionRepo repository.CollectionRepository,
registryRepo repository.AssetRegistryRepository,
) CollectionService {
return &collectionService{
collectionRepo: collectionRepo,
registryRepo: registryRepo,
}
}
// Create 创建典藏藏品
func (s *collectionService) Create(asset *models.CollectionAsset) error {
if asset == nil {
return appErrors.ErrInvalidAssetStatus
}
// 创建典藏藏品记录
if err := s.collectionRepo.Create(asset); err != nil {
return err
}
// 同步写入 asset_registry
registry := &models.AssetRegistry{
AssetID: asset.AssetID,
AssetType: models.AssetTypeCollection,
OwnerUID: asset.OwnerUID,
StarID: asset.StarID,
CollectionCategory: &asset.Category,
Status: asset.Status,
LikeCount: asset.LikeCount,
CreatedAt: time.Now().UnixMilli(),
UpdatedAt: time.Now().UnixMilli(),
}
return s.registryRepo.Create(registry)
}
// GetByID 根据ID获取
func (s *collectionService) GetByID(id int64) (*models.CollectionAsset, error) {
return s.collectionRepo.GetByID(id)
}
// GetByAssetID 根据asset_id获取
func (s *collectionService) GetByAssetID(assetID int64) (*models.CollectionAsset, error) {
return s.collectionRepo.GetByAssetID(assetID)
}
// GetByOwner 获取用户的典藏藏品
func (s *collectionService) GetByOwner(ownerUID, starID int64, limit, offset int) ([]*models.CollectionAsset, error) {
return s.collectionRepo.GetByOwner(ownerUID, starID, limit, offset)
}
// GetByOwnerAndCategory 获取用户指定分类的典藏藏品
func (s *collectionService) GetByOwnerAndCategory(ownerUID, starID int64, category string, limit, offset int) ([]*models.CollectionAsset, error) {
return s.collectionRepo.GetByOwnerAndCategory(ownerUID, starID, category, limit, offset)
}
// CountByOwner 统计用户的典藏藏品数量
func (s *collectionService) CountByOwner(ownerUID, starID int64) (int64, error) {
return s.collectionRepo.CountByOwner(ownerUID, starID)
}
// CountByOwnerAndCategory 统计用户指定分类的典藏藏品数量
func (s *collectionService) CountByOwnerAndCategory(ownerUID, starID int64, category string) (int64, error) {
return s.collectionRepo.CountByOwnerAndCategory(ownerUID, starID, category)
}
// IncrementLikeCount 增加点赞数
func (s *collectionService) IncrementLikeCount(id int64) error {
return s.collectionRepo.IncrementLikeCount(id)
}
// DecrementLikeCount 减少点赞数
func (s *collectionService) DecrementLikeCount(id int64) error {
return s.collectionRepo.DecrementLikeCount(id)
}

View File

@ -0,0 +1,491 @@
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
}

View File

@ -0,0 +1,28 @@
<template>
<scroll-view
class="h-scroll-container"
scroll-x
:show-scrollbar="false"
:enhanced="true"
:bounces="true"
:scroll-animation-duration="300"
:enable-flex="true"
>
<slot></slot>
</scroll-view>
</template>
<script setup>
defineOptions({
name: 'HorizontalScroll'
});
</script>
<style scoped>
.h-scroll-container {
width: 100%;
white-space: nowrap;
display: flex;
flex-direction: row;
}
</style>

View File

@ -126,7 +126,7 @@ const handleAddClick = () => {
<style scoped>
.nft-card {
position: absolute;
position: relative;
display: block;
pointer-events: none;
transition: transform 0.2s ease;
@ -176,6 +176,7 @@ const handleAddClick = () => {
z-index: 1;
/* 确保容器本身不会超出 */
box-sizing: border-box;
pointer-events: none;
}
.nft-cover {
@ -186,6 +187,7 @@ const handleAddClick = () => {
/* 图片圆角处理,与容器保持一致 */
border-radius: 16rpx;
z-index: 2;
pointer-events: none;
}
/* 添加按钮容器 */
@ -226,6 +228,7 @@ const handleAddClick = () => {
align-items: center;
justify-content: center;
z-index: 4;
pointer-events: none;
}
/* 锁图标 */

View File

@ -5,14 +5,14 @@
<!-- 内容区域 -->
<view class="content-wrapper">
<!-- 类型Tab -->
<!-- 类型Tab - 固定定位 -->
<view class="type-tabs">
<view
class="tab-item"
:class="{ active: currentType === 'regular' }"
@click="switchType('regular')"
>
<text>普通</text>
<text>原创</text>
</view>
<view
class="tab-item"
@ -40,51 +40,49 @@
<text class="empty-text">暂无藏品</text>
</view>
<!-- 藏品列表 -->
<view v-else class="nft-list-container">
<!-- 普通藏品 grade 分组 -->
<!-- 藏品列表 - 可滚动区域 -->
<scroll-view v-else class="nft-scroll-view" scroll-y :show-scrollbar="false">
<view class="nft-list-container">
<!-- 原创藏品 category > grade 分组 -->
<view v-if="currentType === 'regular'">
<view v-for="group in regularGroups" :key="group.category" class="nft-group">
<!-- 分组标题 -->
<view v-for="(group, gIndex) in regularGroups" :key="group.category" class="nft-group">
<!-- 分组标题category_name -->
<!-- <view class="group-header">
<text class="group-title">{{ group.category_name }}</text>
</view> -->
<!-- category 下的所有 grades -->
<view v-for="gradeItem in group.grades" :key="gradeItem.grade" class="grade-section">
<view class="group-header">
<text class="group-title">{{ group.category_name }} · {{ formatGrade(group.grade) }}</text>
<text class="group-title">{{ formatGrade(gradeItem.grade) }}</text>
</view>
<!-- 分组内容 -->
<view class="nft-row">
<scroll-view class="nft-row" scroll-x :show-scrollbar="false" :enable-flex="true">
<view class="nft-row-content">
<view
v-for="(item, index) in group.items"
v-for="item in gradeItem.items"
:key="item.asset_id"
class="nft-grid-item"
@click="handleCardClick(item)"
@touchstart.stop
>
<NftCard
:cover-image="item.cover_url_signed"
:width="cardSize"
:height="cardSize"
:locked="false"
:custom-style="cardCustomStyle"
<image
class="nft-image"
:src="item.coverUrl"
mode="aspectFill"
/>
<view class="nft-info">
<text class="nft-name">{{ item.name }}</text>
<text class="nft-likes">{{ item.like_count }}</text>
</view>
</view>
<!-- 更多按钮 -->
<view v-if="group.has_more" class="nft-grid-item more-item" @click="goToMore(group)">
<NftCard
:cover-image="''"
:width="cardSize"
:height="cardSize"
:locked="false"
:show-add-button="false"
operation="none"
:custom-style="cardCustomStyle"
/>
<view v-if="gradeItem.has_more" class="nft-grid-item more-item" @click="goToMore(group, gradeItem.grade)" @touchstart.stop>
<!-- <view class="nft-image more-placeholder"></view> -->
<view class="more-overlay">
<text class="more-text">更多></text>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</view>
@ -92,23 +90,23 @@
<view v-if="currentType === 'collection'">
<view v-for="group in collectionGroups" :key="group.category" class="nft-group">
<!-- 分组标题 -->
<view class="group-header">
<!-- <view class="group-header">
<text class="group-title">{{ group.category_name }}</text>
</view>
</view> -->
<!-- 分组内容 -->
<view class="nft-row">
<scroll-view class="nft-row" scroll-x :show-scrollbar="false" :enable-flex="true">
<view class="nft-row-content">
<view
v-for="(item, index) in group.items"
:key="item.asset_id"
class="nft-grid-item"
@click="handleCardClick(item)"
@touchstart.stop
>
<NftCard
:cover-image="item.cover_url_signed"
:width="cardSize"
:height="cardSize"
:locked="false"
:custom-style="cardCustomStyle"
<image
class="nft-image"
:src="item.coverUrl"
mode="aspectFill"
/>
<view class="nft-info">
<text class="nft-name">{{ item.name }}</text>
@ -116,21 +114,14 @@
</view>
</view>
<!-- 更多按钮 -->
<view v-if="group.has_more" class="nft-grid-item more-item" @click="goToMore(group)">
<NftCard
:cover-image="''"
:width="cardSize"
:height="cardSize"
:locked="false"
:show-add-button="false"
operation="none"
:custom-style="cardCustomStyle"
/>
<view v-if="group.has_more" class="nft-grid-item more-item" @click="goToMore(group)" @touchstart.stop>
<!-- <view class="nft-image more-placeholder"></view> -->
<view class="more-overlay">
<text class="more-text">更多></text>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
@ -138,23 +129,23 @@
<view v-if="currentType === 'activity'">
<view v-for="group in activityGroups" :key="group.category" class="nft-group">
<!-- 分组标题 -->
<view class="group-header">
<!-- <view class="group-header">
<text class="group-title">{{ group.category_name }}</text>
</view>
</view> -->
<!-- 分组内容 -->
<view class="nft-row">
<scroll-view class="nft-row" scroll-x :show-scrollbar="false" :enable-flex="true">
<view class="nft-row-content">
<view
v-for="(item, index) in group.items"
:key="item.asset_id"
class="nft-grid-item"
@click="handleCardClick(item)"
@touchstart.stop
>
<NftCard
:cover-image="item.cover_url_signed"
:width="cardSize"
:height="cardSize"
:locked="false"
:custom-style="cardCustomStyle"
<image
class="nft-image"
:src="item.coverUrl"
mode="aspectFill"
/>
<view class="nft-info">
<text class="nft-name">{{ item.name }}</text>
@ -162,24 +153,18 @@
</view>
</view>
<!-- 更多按钮 -->
<view v-if="group.has_more" class="nft-grid-item more-item" @click="goToMore(group)">
<NftCard
:cover-image="''"
:width="cardSize"
:height="cardSize"
:locked="false"
:show-add-button="false"
operation="none"
:custom-style="cardCustomStyle"
/>
<view v-if="group.has_more" class="nft-grid-item more-item" @click="goToMore(group)" @touchstart.stop>
<!-- <view class="nft-image more-placeholder"></view> -->
<view class="more-overlay">
<text class="more-text">更多></text>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
@ -187,8 +172,8 @@
<script setup>
import { ref, computed, onMounted, onActivated, watch } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import NftCard from './NftCard.vue';
import { getStarbookHomeApi } from '@/utils/api.js';
import { getAssetCoverRealUrl } from '@/utils/assetImageHelper.js';
//
const screenWidth = ref(0);
@ -202,26 +187,20 @@ const currentType = ref('regular');
//
const starbookData = ref([]);
// URL
const processedData = ref([]);
//
let lastLoadedAt = 0;
//
// 2.5
const cardSize = computed(() => {
if (screenWidth.value === 0) return 200;
const rpxToPx = screenWidth.value / 750;
const padding = 30 * rpxToPx; // 30rpx
const gap = 15 * rpxToPx; // 15rpx
const availableWidth = screenWidth.value - (padding * 2) - (gap * 2);
return Math.floor(availableWidth / 3);
const marginLeft = 24; // 24rpx
const gap = 15; // 15rpx
const availableWidth = 750 - marginLeft - (gap * 3.5);
return Math.floor(availableWidth / 2.5);
});
//
const cardCustomStyle = {
position: 'absolute',
top: '0',
left: '0'
};
// grade
const gradeMap = { 1: '一', 2: '二', 3: '三', 4: '四', 5: '五' };
function formatGrade(grade) {
@ -230,22 +209,19 @@ function formatGrade(grade) {
//
const hasData = computed(() => {
return starbookData.value.length > 0;
return processedData.value.length > 0;
});
//
// 使
const regularGroups = computed(() => {
const group = starbookData.value.find(g => g.type === 'regular');
if (!group) return [];
return (group.grades || []).sort((a, b) => b.grade - a.grade); // grade
return processedData.value.filter(g => g.type === 'regular');
});
const collectionGroups = computed(() => {
return starbookData.value.filter(g => g.type === 'collection');
return processedData.value.filter(g => g.type === 'collection');
});
const activityGroups = computed(() => {
return starbookData.value.filter(g => g.type === 'activity');
return processedData.value.filter(g => g.type === 'activity');
});
//
@ -262,8 +238,9 @@ const loadStarbookData = async () => {
loading.value = true;
try {
const response = await getStarbookHomeApi();
if (response.code === 200 && response.data && response.data.groups) {
starbookData.value = response.data.groups;
if (response.code === 200 && response.data && response.data.data.groups) {
// URL
await processGroupsWithValidUrls(response.data.data.groups);
}
} catch (error) {
console.error('获取星册数据失败:', error);
@ -273,11 +250,37 @@ const loadStarbookData = async () => {
duration: 2000
});
starbookData.value = [];
processedData.value = [];
} finally {
loading.value = false;
}
};
// URL
const processGroupsWithValidUrls = async (groups) => {
const processed = JSON.parse(JSON.stringify(groups)); //
// URL
for (const group of processed) {
// regular grades
if (group.grades) {
for (const grade of group.grades) {
for (const item of grade.items || []) {
item.coverUrl = await getAssetCoverRealUrl(item.cover_url_signed || '');
}
}
}
// collection/activity items
if (group.items) {
for (const item of group.items) {
item.coverUrl = await getAssetCoverRealUrl(item.cover_url_signed || '');
}
}
}
processedData.value = processed;
};
//
const handleCardClick = (item) => {
if (item.asset_id) {
@ -288,13 +291,13 @@ const handleCardClick = (item) => {
};
//
const goToMore = (group) => {
const goToMore = (group, grade) => {
const params = {
type: currentType.value,
category: group.category
};
if (currentType.value === 'regular' && group.grade) {
params.grade = group.grade;
if (currentType.value === 'regular' && grade) {
params.grade = grade;
}
const queryString = Object.entries(params)
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
@ -348,8 +351,8 @@ watch(() => props.isActive, (newVal) => {
width: 100%;
height: 100%;
z-index: 1;
overflow-y: auto;
overflow-x: hidden;
/* overflow-y: auto; */
overflow: hidden;
background: #0d0820;
}
@ -391,17 +394,21 @@ watch(() => props.isActive, (newVal) => {
z-index: 1;
width: 100%;
min-height: 100%;
padding: 224rpx 30rpx 120rpx;
padding: 192rpx 30rpx 120rpx;
box-sizing: border-box;
}
/* 类型Tab */
/* 类型Tab - 固定在顶部 */
.type-tabs {
/* position: fixed;
top: 0;
left: 0;
right: 0; */
display: flex;
justify-content: center;
gap: 40rpx;
margin-bottom: 30rpx;
padding: 0 20rpx;
padding: 20rpx 30rpx;
z-index: 100;
}
.tab-item {
@ -417,6 +424,20 @@ watch(() => props.isActive, (newVal) => {
border-bottom-color: #ffffff;
}
/* 可滚动区域 */
.nft-scroll-view {
height: calc(100vh - 320rpx);
}
/* 隐藏滚动条 */
.nft-scroll-view::-webkit-scrollbar {
display: none;
}
.nft-scroll-view {
scrollbar-width: none;
-ms-overflow-style: none;
}
/* 加载中 */
.loading-container {
display: flex;
@ -450,12 +471,21 @@ watch(() => props.isActive, (newVal) => {
/* 藏品分组 */
.nft-group {
margin-bottom: 40rpx;
margin-bottom: 50rpx;
}
/* 等级区块 */
.grade-section {
background: rgba(255, 255, 255, 0.03);
border-radius: 16rpx;
padding: 20rpx;
}
/* 分组标题 */
.group-header {
margin-bottom: 20rpx;
margin-bottom: 16rpx;
padding-bottom: 10rpx;
border-bottom: 1rpx solid rgba(255, 255, 255, 0.1);
}
.group-title {
@ -463,18 +493,51 @@ watch(() => props.isActive, (newVal) => {
color: rgba(255, 255, 255, 0.8);
}
/* 藏品行(横向滚动) */
/* 藏品行 - 水平滚动 */
.nft-row {
display: flex;
flex-wrap: wrap;
gap: 15rpx;
width: 100%;
height: 320rpx;
white-space: nowrap;
}
/* 藏品行内容容器 */
.nft-row-content {
display: inline-block;
white-space: nowrap;
padding-left: 24rpx;
height: 100%;
}
/* 隐藏滚动条 */
.nft-row::-webkit-scrollbar {
display: none;
}
.nft-row {
scrollbar-width: none;
-ms-overflow-style: none;
}
/* 藏品网格项 */
.nft-grid-item {
position: relative;
width: 210rpx;
flex-shrink: 0;
display: inline-block;
vertical-align: top;
margin-right: 32rpx;
height: 100%;
}
/* NFT 图片 */
.nft-image {
width: 192rpx;
height: 224rpx;
border-radius: 16rpx;
background: rgba(255, 255, 255, 0.05);
display: block;
}
/* 更多占位符 */
.more-placeholder {
background: rgba(255, 255, 255, 0.1);
}
.nft-grid-item.more-item {
@ -483,7 +546,7 @@ watch(() => props.isActive, (newVal) => {
/* 藏品信息 */
.nft-info {
padding: 8rpx 0;
padding: 12rpx 0;
text-align: center;
}
@ -503,12 +566,12 @@ watch(() => props.isActive, (newVal) => {
/* 更多覆盖层 */
.more-overlay {
position: absolute;
/* position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
transform: translate(-50%, -50%); */
width: 192rpx;
height: 224rpx;
display: flex;
align-items: center;
justify-content: center;

View File

@ -72,22 +72,33 @@ export function extractOssObjectPath(url) {
*/
export async function getAssetCoverRealUrl(coverUrl) {
// 默认图片路径
const DEFAULT_IMAGE = '';
const DEFAULT_IMAGE = '/static/nft/collection.png';
// 如果是本地静态资源,直接返回
// 如果是本地静态资源或为空,直接返回
if (!coverUrl || coverUrl.startsWith('/static/')) {
return coverUrl || DEFAULT_IMAGE;
}
try {
// 提取完整OSS对象路径保留 asset/13/87/filename.jpg 这样的前缀)
const objectPath = extractOssObjectPath(coverUrl);
let objectPath = extractOssObjectPath(coverUrl);
if (!objectPath) {
console.warn('无法提取OSS对象路径使用默认图片');
return DEFAULT_IMAGE;
}
// URL解码处理 %2F -> / 等编码)
let decodedPath = decodeURIComponent(objectPath);
// 去掉 query string?及其后面的内容)
const queryIndex = decodedPath.indexOf('?');
if (queryIndex !== -1) {
decodedPath = decodedPath.substring(0, queryIndex);
}
// 调用API获取预签名URLtype='asset'
const res = await getOssPresignedUrlApi(objectPath, 3600, 'asset');
const res = await getOssPresignedUrlApi(decodedPath, 3600, 'asset');
if (res.code === 200 && res.data && res.data.url) {
return res.data.url;