feat: 新增星册类型选择
This commit is contained in:
parent
bebdc13c80
commit
bcaa743446
@ -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 ""
|
||||
|
||||
@ -22,12 +22,13 @@ type ServerConfig struct {
|
||||
|
||||
// DubboConfig Dubbo 服务配置
|
||||
type DubboConfig struct {
|
||||
UserServiceURL string
|
||||
SocialServiceURL string
|
||||
AssetServiceURL string
|
||||
GalleryServiceURL string
|
||||
ActivityServiceURL string
|
||||
TaskServiceURL string
|
||||
UserServiceURL string
|
||||
SocialServiceURL string
|
||||
AssetServiceURL string
|
||||
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"),
|
||||
|
||||
@ -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,
|
||||
|
||||
157
backend/gateway/controller/starbook_controller.go
Normal file
157
backend/gateway/controller/starbook_controller.go
Normal 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)),
|
||||
)
|
||||
|
||||
// 调用星册服务(通过 Dubbo,user/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)
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
52
backend/pkg/models/activity_asset.go
Normal file
52
backend/pkg/models/activity_asset.go
Normal 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 // 已激活
|
||||
)
|
||||
@ -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"` // 藏品信息(必填)
|
||||
|
||||
60
backend/pkg/models/asset_registry.go
Normal file
60
backend/pkg/models/asset_registry.go
Normal 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 // 已激活
|
||||
)
|
||||
69
backend/pkg/models/collection_asset.go
Normal file
69
backend/pkg/models/collection_asset.go
Normal 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
|
||||
}
|
||||
@ -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" +
|
||||
|
||||
748
backend/pkg/proto/starbook/starbook.pb.go
Normal file
748
backend/pkg/proto/starbook/starbook.pb.go
Normal 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
|
||||
}
|
||||
149
backend/pkg/proto/starbook/starbook.triple.go
Normal file
149
backend/pkg/proto/starbook/starbook.triple.go
Normal 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
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -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; // 用户上传的素材URL(必填,前端已上传到OSS)
|
||||
string description = 4; // 藏品描述(可选)
|
||||
int32 rarity = 5; // 稀有度(可选)
|
||||
int32 grade = 5; // 星册等级(可选)
|
||||
repeated string tags = 6; // 标签列表(可选)
|
||||
string material_type = 7; // 素材类型(可选)
|
||||
string info = 8; // 藏品信息(必填)
|
||||
|
||||
99
backend/proto/starbook.proto
Normal file
99
backend/proto/starbook.proto
Normal 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;
|
||||
}
|
||||
@ -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 $$;
|
||||
@ -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")
|
||||
|
||||
203
backend/services/starbookService/main.go
Normal file
203
backend/services/starbookService/main.go
Normal 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")
|
||||
}
|
||||
221
backend/services/starbookService/provider/starbook_provider.go
Normal file
221
backend/services/starbookService/provider/starbook_provider.go
Normal 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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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(®istry).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, appErrors.ErrAssetRegistryNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return ®istry, 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(®istry).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, appErrors.ErrAssetRegistryNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return ®istry, 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(®istry).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, appErrors.ErrAssetRegistryNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return ®istry, 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(®istries).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(®istries).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(®istries).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(®istries).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(®istries).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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
123
backend/services/starbookService/service/collection_service.go
Normal file
123
backend/services/starbookService/service/collection_service.go
Normal 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)
|
||||
}
|
||||
491
backend/services/starbookService/service/starbook_service.go
Normal file
491
backend/services/starbookService/service/starbook_service.go
Normal 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
|
||||
}
|
||||
28
frontend/pages/components/HorizontalScroll.vue
Normal file
28
frontend/pages/components/HorizontalScroll.vue
Normal 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>
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/* 锁图标 */
|
||||
|
||||
@ -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,50 +40,48 @@
|
||||
<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 class="group-header">
|
||||
<text class="group-title">{{ group.category_name }} · {{ formatGrade(group.grade) }}</text>
|
||||
</view>
|
||||
<!-- 分组内容 -->
|
||||
<view class="nft-row">
|
||||
<view
|
||||
v-for="(item, index) in group.items"
|
||||
:key="item.asset_id"
|
||||
class="nft-grid-item"
|
||||
@click="handleCardClick(item)"
|
||||
>
|
||||
<NftCard
|
||||
:cover-image="item.cover_url_signed"
|
||||
:width="cardSize"
|
||||
:height="cardSize"
|
||||
:locked="false"
|
||||
:custom-style="cardCustomStyle"
|
||||
/>
|
||||
<view class="nft-info">
|
||||
<text class="nft-name">{{ item.name }}</text>
|
||||
<text class="nft-likes">★{{ item.like_count }}</text>
|
||||
</view>
|
||||
<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">{{ formatGrade(gradeItem.grade) }}</text>
|
||||
</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 class="more-overlay">
|
||||
<text class="more-text">更多></text>
|
||||
<scroll-view class="nft-row" scroll-x :show-scrollbar="false" :enable-flex="true">
|
||||
<view class="nft-row-content">
|
||||
<view
|
||||
v-for="item in gradeItem.items"
|
||||
:key="item.asset_id"
|
||||
class="nft-grid-item"
|
||||
@click="handleCardClick(item)"
|
||||
@touchstart.stop
|
||||
>
|
||||
<image
|
||||
class="nft-image"
|
||||
:src="item.coverUrl"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="nft-info">
|
||||
<text class="nft-name">{{ item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 更多按钮 -->
|
||||
<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>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@ -92,45 +90,38 @@
|
||||
<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">
|
||||
<view
|
||||
v-for="(item, index) in group.items"
|
||||
:key="item.asset_id"
|
||||
class="nft-grid-item"
|
||||
@click="handleCardClick(item)"
|
||||
>
|
||||
<NftCard
|
||||
:cover-image="item.cover_url_signed"
|
||||
:width="cardSize"
|
||||
:height="cardSize"
|
||||
:locked="false"
|
||||
:custom-style="cardCustomStyle"
|
||||
/>
|
||||
<view class="nft-info">
|
||||
<text class="nft-name">{{ item.name }}</text>
|
||||
<text class="nft-likes">★{{ item.like_count }}</text>
|
||||
<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
|
||||
>
|
||||
<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)" @touchstart.stop>
|
||||
<!-- <view class="nft-image more-placeholder"></view> -->
|
||||
<view class="more-overlay">
|
||||
<text class="more-text">更多></text>
|
||||
</view>
|
||||
</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 class="more-overlay">
|
||||
<text class="more-text">更多></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -138,48 +129,42 @@
|
||||
<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">
|
||||
<view
|
||||
v-for="(item, index) in group.items"
|
||||
:key="item.asset_id"
|
||||
class="nft-grid-item"
|
||||
@click="handleCardClick(item)"
|
||||
>
|
||||
<NftCard
|
||||
:cover-image="item.cover_url_signed"
|
||||
:width="cardSize"
|
||||
:height="cardSize"
|
||||
:locked="false"
|
||||
:custom-style="cardCustomStyle"
|
||||
/>
|
||||
<view class="nft-info">
|
||||
<text class="nft-name">{{ item.name }}</text>
|
||||
<text class="nft-likes">★{{ item.like_count }}</text>
|
||||
<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
|
||||
>
|
||||
<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)" @touchstart.stop>
|
||||
<!-- <view class="nft-image more-placeholder"></view> -->
|
||||
<view class="more-overlay">
|
||||
<text class="more-text">更多></text>
|
||||
</view>
|
||||
</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 class="more-overlay">
|
||||
<text class="more-text">更多></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</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;
|
||||
|
||||
@ -72,23 +72,34 @@ 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获取预签名URL(type='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;
|
||||
} else {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user