fix(backend): service stability — batch 3 accumulated (bcrypt off-txn / login anti-enum / MQ stub / aiChat / event reliability / gateway aggregate)

- 3.1 bcrypt 移出事务 (Register): repository.HashPassword 前移到 db.Transaction 之前。
- 3.2 Login 消除用户枚举 + 限流 + timing 抹平: pkg/errors 加 ErrInvalidCredential / ErrTooManyLoginAttempts;
  user-not-found 跑 dummy bcrypt 抹平 ~100ms 时序差; mobile 5次/ip 20次 per 15min 限流 (Redis, fail-open 降级)。
- 3.3 MQ streams adapter 停用 → stub: 0 业务调用方, noop EventProducer.Publish; Init 不再装配 streams;
  11 处硬编码 'gallery'/'default' 抽常量到 pkg/queue/consts (值不变, 消漂移)。
- 3.5 JWT 密钥治理: pkg/jwt MustInit fail-fast + atomic.Value, 50-goroutine race_test 零告警;
  MustInit 调用点 gateway main + auth_provider + loadgen 同步更新。
- 3.6 aiChat 健壮性: SaveContext 用 persona.ID(非 req.PersonaId); Redis/memory 错误 记 WARN 不静默;
  Dify err 映射稳定用户文案。
- 3.7 statistic.Client 重构: TrackEvent 改 buffered channel (cap 1024) + dispatchLoop worker。
- 3.8 网关聚合: StarCache (60s TTL, singleflight) 替换 GetFanIdentities 链式调用;
  DeleteAccount 改网关直调 userService.DeleteAccount(避免改 hand-written triple.go);
  铸造双写改异步 channel+consumer (3 retry)。
- 大量单测: 各子项 TDD (RED→GREEN), 关键并发 race_test (50 goroutine)。
- .env.example JWT_SECRET 改为 ≥32 字节 base64 示例(原为空, 被 MustInit 立即拒)。

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zerosaturation 2026-07-24 14:03:21 +08:00
parent a337f43f86
commit 8a767fb400
25 changed files with 1281 additions and 1167 deletions

View File

@ -31,6 +31,7 @@ import (
"github.com/topfans/backend/pkg/database"
pbAsset "github.com/topfans/backend/pkg/proto/asset"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"go.uber.org/zap"
"github.com/topfans/backend/pkg/logger"
@ -261,43 +262,55 @@ func generateUploadID() string {
return fmt.Sprintf("upload_%d_%s", time.Now().Unix(), uuid.New().String()[:8])
}
// parseRPCError 解析 Dubbo RPC 错误字符串
// 格式: "code_17: 错误消息" 或 "Failed to xxx: code_17: 错误消息"
// parseRPCError 解析 Dubbo RPC 错误。
// 优先使用 google.golang.org/grpc/status.FromError 提取标准 gRPC status;
// 退回到字符串扫描 "code_XX: msg" 兼容 history-only 非标错误。
// 签名不变 (code int, message string),所有 7+ 调用点保持兼容。
func parseRPCError(err error) (code int, message string) {
if err == nil {
return http.StatusOK, ""
}
// 1) 标准 gRPC statusDubbo-go Triple 现在已透传 grpc/status
if st, ok := status.FromError(err); ok && st.Code() != codes.Unknown {
return grpcCodeToHTTP(st.Code()), st.Message()
}
// 2) 历史遗留:扫描 "code_XX: msg" 字符串
errStr := err.Error()
// 查找最后一个 "code_XX:" 出现的位置(处理嵌套错误)
if strings.Contains(errStr, "code_") {
// 找到 "code_" 的最后一个位置
lastCodeIdx := strings.LastIndex(errStr, "code_")
if lastCodeIdx != -1 {
// 从 "code_" 开始提取后面的部分
remaining := errStr[lastCodeIdx:]
// 分割 "code_XX: message"
parts := strings.SplitN(remaining, ":", 2)
if len(parts) == 2 {
// 提取状态码
codeStr := strings.TrimSpace(strings.TrimPrefix(parts[0], "code_"))
if c, parseErr := strconv.Atoi(codeStr); parseErr == nil {
code = c
} else {
code = http.StatusInternalServerError
}
// 提取并清理消息(去掉可能的前缀)
message = strings.TrimSpace(parts[1])
return code, message
if lastCodeIdx := strings.LastIndex(errStr, "code_"); lastCodeIdx != -1 {
remaining := errStr[lastCodeIdx:]
if parts := strings.SplitN(remaining, ":", 2); len(parts) == 2 {
codeStr := strings.TrimSpace(strings.TrimPrefix(parts[0], "code_"))
if c, parseErr := strconv.Atoi(codeStr); parseErr == nil {
return c, strings.TrimSpace(parts[1])
}
}
}
// 如果不是标准格式,返回原始错误消息
return http.StatusInternalServerError, errStr
// 3) 兜底:不对前端暴露 err.Error()(可能含 SQL、内部栈
return http.StatusInternalServerError, "服务暂时不可用"
}
// grpcCodeToHTTP 将 google.rpc.Code 映射为 HTTP 状态码。
// 与 grpc-gateway 约定一致。
func grpcCodeToHTTP(c codes.Code) int {
switch c {
case codes.NotFound:
return http.StatusNotFound
case codes.InvalidArgument:
return http.StatusBadRequest
case codes.Unauthenticated:
return http.StatusUnauthorized
case codes.PermissionDenied:
return http.StatusForbidden
case codes.AlreadyExists:
return http.StatusConflict
case codes.FailedPrecondition:
return http.StatusPreconditionFailed
default:
return http.StatusInternalServerError
}
}
// PreCreateMintOrder 阶段一:预创建铸造订单(生成 order_id

View File

@ -209,10 +209,7 @@ func (ctrl *AuthController) RefreshToken(c *gin.Context) {
resp, err := ctrl.userServiceClient.RefreshToken(ctx, &pb.RefreshTokenRequest{})
if err != nil {
logger.Logger.Error("RefreshToken failed", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"code": "INTERNAL_ERROR",
"message": err.Error(),
})
response.InternalError(c, "刷新令牌失败")
return
}
@ -285,10 +282,7 @@ func (ctrl *AuthController) Logout(c *gin.Context) {
func (ctrl *AuthController) ValidateToken(c *gin.Context) {
var req pb.ValidateTokenRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": "BAD_REQUEST",
"message": err.Error(),
})
response.BadRequest(c, "请求参数错误")
return
}
@ -297,10 +291,7 @@ func (ctrl *AuthController) ValidateToken(c *gin.Context) {
resp, err := ctrl.userServiceClient.ValidateToken(ctx, &req)
if err != nil {
logger.Logger.Error("ValidateToken failed", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"code": "INTERNAL_ERROR",
"message": err.Error(),
})
response.InternalError(c, "令牌校验失败")
return
}

View File

@ -158,10 +158,7 @@ func (ctrl *UserController) GetUser(c *gin.Context) {
})
if err != nil {
logger.Logger.Error("GetUser failed", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"code": "INTERNAL_ERROR",
"message": err.Error(),
})
response.InternalError(c, "获取用户信息失败")
return
}
@ -249,10 +246,7 @@ func (ctrl *UserController) GetFanProfile(c *gin.Context) {
})
if err != nil {
logger.Logger.Error("GetFanProfile failed", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"code": "INTERNAL_ERROR",
"message": err.Error(),
})
response.InternalError(c, "获取粉丝档案失败")
return
}
@ -485,10 +479,7 @@ func (ctrl *UserController) AddIdentity(c *gin.Context) {
func (ctrl *UserController) SwitchIdentity(c *gin.Context) {
var req pb.SwitchIdentityRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": "BAD_REQUEST",
"message": err.Error(),
})
response.BadRequest(c, "请求参数错误")
return
}

View File

@ -79,7 +79,6 @@ 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),
zap.String("notification_service_url", cfg.Dubbo.NotificationServiceURL),
)
@ -172,16 +171,7 @@ 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")
// 4.8 AIChatService Client
// 4.7 AIChatService Client (原 4.7 StarbookService Client 已删除 - 批次 4.2)
aiChatClient, err := client.NewClient(
client.WithClientURL(cfg.Dubbo.AIChatServiceURL),
)
@ -190,7 +180,7 @@ func main() {
}
logger.Logger.Info("AI Chat Service Dubbo client connected successfully")
// 4.9 StatisticService Client
// 4.8 StatisticService Client
statisticClient, err := client.NewClient(
client.WithClientURL(cfg.Dubbo.StatisticServiceURL),
)
@ -199,7 +189,7 @@ func main() {
}
logger.Logger.Info("Statistic Service Dubbo client connected successfully")
// 4.10 NotificationService Client
// 4.9 NotificationService Client
notificationClient, err := client.NewClient(
client.WithClientURL(cfg.Dubbo.NotificationServiceURL),
)
@ -208,7 +198,7 @@ func main() {
}
logger.Logger.Info("Notification Service Dubbo client connected successfully")
// 4.11 ModerationService Client (举报反馈,端口 20011)
// 4.10 ModerationService Client (举报反馈,端口 20011)
moderationClient, err := client.NewClient(
client.WithClientURL(cfg.Dubbo.ModerationServiceURL),
)
@ -218,13 +208,13 @@ func main() {
logger.Logger.Info("Moderation Service Dubbo client connected successfully",
zap.String("url", cfg.Dubbo.ModerationServiceURL))
// 4.12 初始化 ModerationService pb client注入到 controller
// 4.11 初始化 ModerationService pb client注入到 controller
modSvc, err := pbModeration.NewModerationService(moderationClient)
if err != nil {
logger.Logger.Fatal("Failed to create ModerationService pb client", zap.Error(err))
}
// 4.13 初始化 Activity HubWebSocket 实时推送)
// 4.12 初始化 Activity HubWebSocket 实时推送)
redisClient := database.GetRedis()
activityHub := socket.NewActivityHub(redisClient, cfg.WebSocket.ActivityPath)
hubCtx, hubCancel := context.WithCancel(context.Background())
@ -238,7 +228,7 @@ func main() {
zap.Bool("redis_available", redisClient != nil),
)
// 4.14 初始化 Star cache替代 Register/Login 等路径上重复的 GetFanIdentities RPC
// 4.13 初始化 Star cache替代 Register/Login 等路径上重复的 GetFanIdentities RPC
// 注:UserSocialService client 内部已经在 consumer 注册阶段就绪,这里复用 4.1 的 dubbo client
// 重新 New 一个 conn,以避免在 dubbo-go 行为下复用同一个 Client 的兼容性坑。
userSvcForStarCache, err := pb.NewUserSocialService(userClient)
@ -249,7 +239,8 @@ func main() {
// 5. 设置路由
logger.Logger.Info("Setting up routes...")
r, err := router.SetupRouter(userClient, socialClient, assetClient, galleryClient, activityClient, taskClient, starbookClient, aiChatClient, statisticClient, notificationClient, modSvc, cfg.WebSocket.AIChatPath, activityHub, starCache)
// ★ 批次 4.2:starbookService 已删除,SetupRouter 签名同步移除 starbookClient
r, err := router.SetupRouter(userClient, socialClient, assetClient, galleryClient, activityClient, taskClient, aiChatClient, statisticClient, notificationClient, modSvc, cfg.WebSocket.AIChatPath, activityHub, starCache)
if err != nil {
logger.Logger.Fatal("Failed to setup router", zap.Error(err))
}

View File

@ -41,6 +41,9 @@ var (
ErrMaxIdentitiesReached = errors.New("maximum number of identities reached")
ErrInternalServer = errors.New("internal server error")
// 头像 / URL 同源校验 — UpdateAvatar 防 SSRF / 防任意公网 URL 写入 fan_profile.avatar_url
ErrInvalidAvatarURL = errors.New("avatar URL must be hosted on aliyuncs.com")
// 社交服务相关错误
ErrCannotAddSelf = errors.New("不能添加自己为好友")
ErrCannotSearchSelf = errors.New("不能查找自己")
@ -130,7 +133,8 @@ func ToGRPCCode(err error) codes.Code {
errors.Is(err, ErrInvalidOldPassword), errors.Is(err, ErrSameAsOldPassword),
errors.Is(err, ErrInvalidVerifyToken),
errors.Is(err, ErrInvalidStarID), errors.Is(err, ErrInvalidUserID),
errors.Is(err, ErrMaxIdentitiesReached), errors.Is(err, ErrInvalidNickname):
errors.Is(err, ErrMaxIdentitiesReached), errors.Is(err, ErrInvalidNickname),
errors.Is(err, ErrInvalidAvatarURL):
return codes.InvalidArgument
case errors.Is(err, ErrCannotAddSelf), errors.Is(err, ErrCannotSearchSelf), errors.Is(err, ErrNotFanOfStar), errors.Is(err, ErrAlreadyFriends),
errors.Is(err, ErrRequestAlreadyPending), errors.Is(err, ErrInvalidFriendUserID), errors.Is(err, ErrCannotProcessOwnRequest),

View File

@ -27,6 +27,8 @@ func NewHandler(serviceName string, port int) *Handler {
func (h *Handler) Start() {
mux := http.NewServeMux()
mux.HandleFunc("/health", h.handleHealth)
// /healthz alias for K8s probe convention; canonical path stays /health.
mux.HandleFunc("/healthz", h.handleHealth)
h.server = &http.Server{
Addr: fmt.Sprintf(":%d", h.port),

View File

@ -113,6 +113,9 @@ func (a *Adapter) GetInfo(ctx context.Context, queue, taskID string) (*adapter.T
}
// Delete 占位实现 — 同上 Asynq v0.26 不在 public API 提供。
//
// TODO: 实际未调用 — asynq.DeletedTaskInfo 返回 false 即视为不存在;
// 当前业务链路(修/重排队)无需 Delete 入口。若未来需要"取消已入队任务"再补 inspector 支持。
func (a *Adapter) Delete(ctx context.Context, queue, taskID string) error {
return errors.New("mq: asynq adapter does not support Delete in current version")
}

View File

@ -6,22 +6,26 @@
// 2. 业务侧通过 adapter.Get().TaskProducer()/EventProducer() 发送
// 3. 业务侧通过 adapter.Get().TaskConsumer()/EventConsumer() 注册 handler
// 4. main.go 调 mq.Close() 关闭连接
//
// ★ 批次 4.9-F 决断:Redis Streams 事件 adapter 已被停用(原先依赖的"对外广播"
// 事件模型简化为各服务本地 TrackEvent → Postgres 通道,详见 pkg/statistic),
// streams 子包已删除。Event 原语在本文件保留为 no-op 兼容桩,业务侧若仍调
// EventProducer().Publish 不会炸,但会产生一次性 WARN 提示,引导迁移。
package mq
import (
"context"
"fmt"
"sync"
"github.com/topfans/backend/pkg/logger"
"github.com/topfans/backend/pkg/mq/adapter"
asynqAdapter "github.com/topfans/backend/pkg/mq/asynq"
streamsAdapter "github.com/topfans/backend/pkg/mq/streams"
)
var (
initOnce sync.Once
cfg Config
stubEventProducer = streamsAdapter.NewStubProducer()
stubEventConsumer = streamsAdapter.NewStubConsumer()
initOnce sync.Once
cfg Config
)
// Init 初始化 MQ adapter。
@ -83,13 +87,42 @@ func (c *compositeAdapter) TaskConsumer() adapter.TaskConsumer {
}
func (c *compositeAdapter) EventProducer() adapter.EventProducer {
return stubEventProducer
return noopEventProducer{}
}
func (c *compositeAdapter) EventConsumer() adapter.EventConsumer {
return stubEventConsumer
return noopEventConsumer{}
}
func (c *compositeAdapter) Close() error {
return c.taskAdapter.Close()
}
// ---- No-op Event 原语 (4.9-F 兼容桩) ----
var publishWarnOnce sync.Once
type noopEventProducer struct{}
func (noopEventProducer) Publish(_ context.Context, _ string, _ adapter.Event) error {
publishWarnOnce.Do(func() {
if logger.Logger != nil {
logger.Logger.Warn("mq.EventProducer.Publish called but event adapter is deprecated; use pkg/statistic.TrackEvent instead")
}
})
return nil
}
type noopEventConsumer struct{}
func (noopEventConsumer) Subscribe(_ context.Context, _ []string, _ string, _ adapter.EventHandler) error {
return nil
}
func (noopEventConsumer) Ack(_ context.Context, _, _, _ string) error { return nil }
func (noopEventConsumer) Run(_ context.Context) error { return nil }
func (noopEventConsumer) Stop() error { return nil }
var (
_ adapter.EventProducer = noopEventProducer{}
_ adapter.EventConsumer = noopEventConsumer{}
)

View File

@ -1,7 +0,0 @@
// Package streams 是 adapter.Adapter 的 Redis Streams 实现,只提供 Event 原语。
//
// 任务原语由 sibling package asynq 实现,合成由 pkg/mq/mq.go 完成。
package streams
// 当前不在 streams 内部写死 key —— 统一从 pkg/mq/tasks 拉取常量,避免重复。
// 本文件只放 streams 私有的 helpers。

View File

@ -1,78 +0,0 @@
package streams
import (
"context"
"sync"
"github.com/topfans/backend/pkg/logger"
"github.com/topfans/backend/pkg/mq/adapter"
)
// Config is retained for source compatibility while the Redis Streams adapter
// is disabled.
type Config struct {
RedisAddr string
RedisDB int
Password string
ConsumerGroup string
MaxLen int64
ReadBlockMS int
}
var publishWarnOnce sync.Once
type stubProducer struct{}
func (stubProducer) Publish(_ context.Context, _ string, _ adapter.Event) error {
publishWarnOnce.Do(func() {
if logger.Logger != nil {
logger.Logger.Warn("streams.Publish called but streams adapter is deprecated; events should go via pkg/statistic.TrackEvent")
}
})
return nil
}
type stubConsumer struct{}
func (stubConsumer) Subscribe(_ context.Context, _ []string, _ string, _ adapter.EventHandler) error {
return nil
}
func (stubConsumer) Ack(_ context.Context, _, _, _ string) error { return nil }
func (stubConsumer) Run(_ context.Context) error { return nil }
func (stubConsumer) Stop() error { return nil }
// NewStubProducer returns the process-wide event producer compatibility stub.
func NewStubProducer() adapter.EventProducer { return stubProducer{} }
// NewStubConsumer returns the process-wide event consumer compatibility stub.
func NewStubConsumer() adapter.EventConsumer { return stubConsumer{} }
// Adapter retains the former streams adapter's exported API as a no-op stub.
type Adapter struct{}
// New retains constructor compatibility without creating a Redis client.
func New(_ Config) (*Adapter, error) { return &Adapter{}, nil }
func (a *Adapter) Publish(ctx context.Context, topic string, event adapter.Event) error {
return stubProducer{}.Publish(ctx, topic, event)
}
func (a *Adapter) Subscribe(ctx context.Context, topics []string, group string, handler adapter.EventHandler) error {
return stubConsumer{}.Subscribe(ctx, topics, group, handler)
}
func (a *Adapter) Ack(ctx context.Context, topic, group, msgID string) error {
return stubConsumer{}.Ack(ctx, topic, group, msgID)
}
func (a *Adapter) Run(ctx context.Context) error { return stubConsumer{}.Run(ctx) }
func (a *Adapter) Stop() error { return stubConsumer{}.Stop() }
func (a *Adapter) Close() error { return nil }
var (
_ adapter.EventProducer = (*Adapter)(nil)
_ adapter.EventConsumer = (*Adapter)(nil)
_ adapter.EventProducer = stubProducer{}
_ adapter.EventConsumer = stubConsumer{}
)

View File

@ -27,18 +27,19 @@ var (
secretVal string
)
// getSecret 读取并缓存 SECRET_KEY 环境变量
// 优先级:SECRET_KEY > JWT_SECRET(共用环境变量)
// getSecret 读取并缓存 SECRET_KEY 环境变量。
//
// 失败模式(fail-fast): SECRET_KEY 未设置时 panic,
// 避免使用硬编码 dev 默认值,防止生产意外跑出可伪造的周边验签。
//
// 批次 4.9-I 修复: 原实现把 JWT_SECRET 作为周边密钥兜底,
// 同时设置 "default-dev-secret-change-me" 兜底,容易被环境漂移到生产。
// 新实现仅信任 SECRET_KEY,缺则终止进程。
func getSecret() string {
secretOnce.Do(func() {
secretVal = os.Getenv("SECRET_KEY")
if secretVal == "" {
// 兜底:JWT_SECRET 也用作周边验真密钥
secretVal = os.Getenv("JWT_SECRET")
}
if secretVal == "" {
// ⚠️ 生产环境必须配置 SECRET_KEY,这里只是开发兜底
secretVal = "default-dev-secret-change-me"
panic("SECRET_KEY is required for peripheral HMAC signing (use a strong random ≥32 bytes)")
}
})
return secretVal

View File

@ -7,6 +7,11 @@
package asset
import (
context "context"
client "dubbo.apache.org/dubbo-go/v3/client"
constant "dubbo.apache.org/dubbo-go/v3/common/constant"
triple_protocol "dubbo.apache.org/dubbo-go/v3/protocol/triple/triple_protocol"
server "dubbo.apache.org/dubbo-go/v3/server"
common "github.com/topfans/backend/pkg/proto/common"
_ "google.golang.org/genproto/googleapis/api/annotations"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
@ -3944,6 +3949,208 @@ func (x *TrackShareResponse) GetShareEventId() int64 {
return 0
}
// GetAssetsByTypeRequest filters a user's starbook assets by type.
//
// This type is appended manually because regenerating asset.triple.go is
// intentionally prohibited for the starbook recovery change.
type GetAssetsByTypeRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
StarId int64 `protobuf:"varint,2,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"`
Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"`
Category string `protobuf:"bytes,4,opt,name=category,proto3" json:"category,omitempty"`
Grade int32 `protobuf:"varint,5,opt,name=grade,proto3" json:"grade,omitempty"`
Page int32 `protobuf:"varint,6,opt,name=page,proto3" json:"page,omitempty"`
PageSize int32 `protobuf:"varint,7,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetAssetsByTypeRequest) Reset() {
*x = GetAssetsByTypeRequest{}
mi := &file_asset_proto_msgTypes[56]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetAssetsByTypeRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetAssetsByTypeRequest) ProtoMessage() {}
func (x *GetAssetsByTypeRequest) ProtoReflect() protoreflect.Message {
mi := &file_asset_proto_msgTypes[56]
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 GetAssetsByTypeRequest.ProtoReflect.Descriptor instead.
func (*GetAssetsByTypeRequest) Descriptor() ([]byte, []int) {
return file_asset_proto_rawDescGZIP(), []int{56}
}
func (x *GetAssetsByTypeRequest) GetUserId() int64 {
if x != nil {
return x.UserId
}
return 0
}
func (x *GetAssetsByTypeRequest) GetStarId() int64 {
if x != nil {
return x.StarId
}
return 0
}
func (x *GetAssetsByTypeRequest) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *GetAssetsByTypeRequest) GetCategory() string {
if x != nil {
return x.Category
}
return ""
}
func (x *GetAssetsByTypeRequest) GetGrade() int32 {
if x != nil {
return x.Grade
}
return 0
}
func (x *GetAssetsByTypeRequest) GetPage() int32 {
if x != nil {
return x.Page
}
return 0
}
func (x *GetAssetsByTypeRequest) GetPageSize() int32 {
if x != nil {
return x.PageSize
}
return 0
}
// GetAssetsByTypeResponse returns the existing grouped AssetListData shape.
type GetAssetsByTypeResponse 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 *GetAssetsByTypeResponse) Reset() {
*x = GetAssetsByTypeResponse{}
mi := &file_asset_proto_msgTypes[57]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetAssetsByTypeResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetAssetsByTypeResponse) ProtoMessage() {}
func (x *GetAssetsByTypeResponse) ProtoReflect() protoreflect.Message {
mi := &file_asset_proto_msgTypes[57]
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 GetAssetsByTypeResponse.ProtoReflect.Descriptor instead.
func (*GetAssetsByTypeResponse) Descriptor() ([]byte, []int) {
return file_asset_proto_rawDescGZIP(), []int{57}
}
func (x *GetAssetsByTypeResponse) GetBase() *common.BaseResponse {
if x != nil {
return x.Base
}
return nil
}
func (x *GetAssetsByTypeResponse) GetData() *AssetListData {
if x != nil {
return x.Data
}
return nil
}
// AssetServiceGetAssetsByTypeProcedure is the fully-qualified RPC procedure.
const AssetServiceGetAssetsByTypeProcedure = "/topfans.asset.AssetService/GetAssetsByType"
// AssetServiceGetAssetsByTypeClient extends the generated AssetService client
// without editing asset.triple.go.
type AssetServiceGetAssetsByTypeClient interface {
AssetService
GetAssetsByType(context.Context, *GetAssetsByTypeRequest, ...client.CallOption) (*GetAssetsByTypeResponse, error)
}
// AssetServiceGetAssetsByTypeHandler extends the generated provider contract
// without editing asset.triple.go.
type AssetServiceGetAssetsByTypeHandler interface {
AssetServiceHandler
GetAssetsByType(context.Context, *GetAssetsByTypeRequest) (*GetAssetsByTypeResponse, error)
}
// NewAssetServiceGetAssetsByTypeClient constructs a client exposing the new RPC.
func NewAssetServiceGetAssetsByTypeClient(cli *client.Client, opts ...client.ReferenceOption) (AssetServiceGetAssetsByTypeClient, error) {
conn, err := cli.DialWithInfo("topfans.asset.AssetService", &AssetService_ClientInfo, opts...)
if err != nil {
return nil, err
}
return &AssetServiceImpl{conn: conn}, nil
}
func (c *AssetServiceImpl) GetAssetsByType(ctx context.Context, req *GetAssetsByTypeRequest, opts ...client.CallOption) (*GetAssetsByTypeResponse, error) {
resp := new(GetAssetsByTypeResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "GetAssetsByType", opts...); err != nil {
return nil, err
}
return resp, nil
}
func registerAssetServiceGetAssetsByType() {
AssetService_ClientInfo.MethodNames = append(AssetService_ClientInfo.MethodNames, "GetAssetsByType")
AssetService_ServiceInfo.Methods = append(AssetService_ServiceInfo.Methods, server.MethodInfo{
Name: "GetAssetsByType",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(GetAssetsByTypeRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*GetAssetsByTypeRequest)
res, err := handler.(AssetServiceGetAssetsByTypeHandler).GetAssetsByType(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
})
}
var File_asset_proto protoreflect.FileDescriptor
const file_asset_proto_rawDesc = "" +
@ -4272,7 +4479,13 @@ const file_asset_proto_rawDesc = "" +
"\x05extra\x18\a \x01(\v2\x17.google.protobuf.StructR\x05extra\"l\n" +
"\x12TrackShareResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12$\n" +
"\x0eshare_event_id\x18\x02 \x01(\x03R\fshareEventId2\x97\x13\n" +
"\x0eshare_event_id\x18\x02 \x01(\x03R\fshareEventId" +
"\"\xc1\x01\x0a\x16GetAssetsByTypeRequest\x12\x17\x0a\x07user_id\x18\x01 \x01(\x03R\x06userId\x12\x17\x0a\x07star" +
"_id\x18\x02 \x01(\x03R\x06starId\x12\x12\x0a\x04type\x18\x03 \x01(\x09R\x04type\x12\x1a\x0a\x08category\x18\x04 \x01(\x09R\x08cat" +
"egory\x12\x14\x0a\x05grade\x18\x05 \x01(\x05R\x05grade\x12\x12\x0a\x04page\x18\x06 \x01(\x05R\x04page\x12\x1b\x0a\x09page_size" +
"\x18\x07 \x01(\x05R\x08pageSize\"}\x0a\x17GetAssetsByTypeResponse\x120\x0a\x04base\x18\x01 \x01(\x0b2\x1c." +
"topfans.common.BaseResponseR\x04base\x120\x0a\x04data\x18\x02 \x01(\x0b2\x1c.topfans.as" +
"set.AssetListDataR\x04data2\xf9\x13\n" +
"\fAssetService\x12Z\n" +
"\rInitMintOrder\x12#.topfans.asset.InitMintOrderRequest\x1a$.topfans.asset.InitMintOrderResponse\x12\x94\x01\n" +
"\x12PreCreateMintOrder\x12(.topfans.asset.PreCreateMintOrderRequest\x1a).topfans.asset.PreCreateMintOrderResponse\")\x82\xd3\xe4\x93\x02#:\x01*\"\x1e/api/v1/assets/mints/precreate\x12\x81\x01\n" +
@ -4296,7 +4509,9 @@ const file_asset_proto_rawDesc = "" +
"\x13UnbindAssetMaterial\x12).topfans.asset.UnbindAssetMaterialRequest\x1a*.topfans.asset.UnbindAssetMaterialResponse\x12]\n" +
"\x0eGetAssetQrcode\x12$.topfans.asset.GetAssetQrcodeRequest\x1a%.topfans.asset.GetAssetQrcodeResponse\x12Q\n" +
"\n" +
"TrackShare\x12 .topfans.asset.TrackShareRequest\x1a!.topfans.asset.TrackShareResponseB2Z0github.com/topfans/backend/pkg/proto/asset;assetb\x06proto3"
"TrackShare\x12 .topfans.asset.TrackShareRequest\x1a!.topfans.asset.TrackShareResponse" +
"\x12`\x0a\x0fGetAssetsByType\x12%.topfans.asset.GetAssetsByTypeRequest\x1a&" +
".topfans.asset.GetAssetsByTypeResponseB2Z0github.com/topfans/backend/pkg/proto/asset;assetb\x06proto3"
var (
file_asset_proto_rawDescOnce sync.Once
@ -4310,7 +4525,7 @@ func file_asset_proto_rawDescGZIP() []byte {
return file_asset_proto_rawDescData
}
var file_asset_proto_msgTypes = make([]protoimpl.MessageInfo, 56)
var file_asset_proto_msgTypes = make([]protoimpl.MessageInfo, 58)
var file_asset_proto_goTypes = []any{
(*Asset)(nil), // 0: topfans.asset.Asset
(*OwnerInfo)(nil), // 1: topfans.asset.OwnerInfo
@ -4368,103 +4583,112 @@ var file_asset_proto_goTypes = []any{
(*GetAssetQrcodeResponse)(nil), // 53: topfans.asset.GetAssetQrcodeResponse
(*TrackShareRequest)(nil), // 54: topfans.asset.TrackShareRequest
(*TrackShareResponse)(nil), // 55: topfans.asset.TrackShareResponse
(*common.BaseResponse)(nil), // 56: topfans.common.BaseResponse
(*structpb.Struct)(nil), // 57: google.protobuf.Struct
(*GetAssetsByTypeRequest)(nil), // 56: topfans.asset.GetAssetsByTypeRequest
(*GetAssetsByTypeResponse)(nil), // 57: topfans.asset.GetAssetsByTypeResponse
(*common.BaseResponse)(nil), // 58: topfans.common.BaseResponse
(*structpb.Struct)(nil), // 59: google.protobuf.Struct
}
var file_asset_proto_depIdxs = []int32{
1, // 0: topfans.asset.Asset.owner:type_name -> topfans.asset.OwnerInfo
56, // 1: topfans.asset.InitMintOrderResponse.base:type_name -> topfans.common.BaseResponse
58, // 1: topfans.asset.InitMintOrderResponse.base:type_name -> topfans.common.BaseResponse
2, // 2: topfans.asset.InitMintOrderResponse.order:type_name -> topfans.asset.MintOrder
56, // 3: topfans.asset.PreCreateMintOrderResponse.base:type_name -> topfans.common.BaseResponse
58, // 3: topfans.asset.PreCreateMintOrderResponse.base:type_name -> topfans.common.BaseResponse
2, // 4: topfans.asset.PreCreateMintOrderResponse.order:type_name -> topfans.asset.MintOrder
56, // 5: topfans.asset.CreateMintOrderResponse.base:type_name -> topfans.common.BaseResponse
58, // 5: topfans.asset.CreateMintOrderResponse.base:type_name -> topfans.common.BaseResponse
2, // 6: topfans.asset.CreateMintOrderResponse.order:type_name -> topfans.asset.MintOrder
0, // 7: topfans.asset.CreateMintOrderResponse.asset:type_name -> topfans.asset.Asset
56, // 8: topfans.asset.EstimateMintCostResponse.base:type_name -> topfans.common.BaseResponse
56, // 9: topfans.asset.GetMyAssetsResponse.base:type_name -> topfans.common.BaseResponse
58, // 8: topfans.asset.EstimateMintCostResponse.base:type_name -> topfans.common.BaseResponse
58, // 9: topfans.asset.GetMyAssetsResponse.base:type_name -> topfans.common.BaseResponse
13, // 10: topfans.asset.GetMyAssetsResponse.data:type_name -> topfans.asset.AssetListData
14, // 11: topfans.asset.AssetListData.groups:type_name -> topfans.asset.AssetGroup
15, // 12: topfans.asset.AssetGroup.grades:type_name -> topfans.asset.GradeSection
16, // 13: topfans.asset.AssetGroup.items:type_name -> topfans.asset.AssetItem
16, // 14: topfans.asset.GradeSection.items:type_name -> topfans.asset.AssetItem
56, // 15: topfans.asset.GetAssetResponse.base:type_name -> topfans.common.BaseResponse
58, // 15: topfans.asset.GetAssetResponse.base:type_name -> topfans.common.BaseResponse
0, // 16: topfans.asset.GetAssetResponse.asset:type_name -> topfans.asset.Asset
56, // 17: topfans.asset.GetAssetStatusResponse.base:type_name -> topfans.common.BaseResponse
56, // 18: topfans.asset.GetMintOrderResponse.base:type_name -> topfans.common.BaseResponse
58, // 17: topfans.asset.GetAssetStatusResponse.base:type_name -> topfans.common.BaseResponse
58, // 18: topfans.asset.GetMintOrderResponse.base:type_name -> topfans.common.BaseResponse
2, // 19: topfans.asset.GetMintOrderResponse.order:type_name -> topfans.asset.MintOrder
0, // 20: topfans.asset.GetMintOrderResponse.asset:type_name -> topfans.asset.Asset
56, // 21: topfans.asset.CancelMintOrderResponse.base:type_name -> topfans.common.BaseResponse
56, // 22: topfans.asset.LikeAssetResponse.base:type_name -> topfans.common.BaseResponse
56, // 23: topfans.asset.UnlikeAssetResponse.base:type_name -> topfans.common.BaseResponse
56, // 24: topfans.asset.CheckAssetLikeResponse.base:type_name -> topfans.common.BaseResponse
56, // 25: topfans.asset.GetAssetLikesResponse.base:type_name -> topfans.common.BaseResponse
58, // 21: topfans.asset.CancelMintOrderResponse.base:type_name -> topfans.common.BaseResponse
58, // 22: topfans.asset.LikeAssetResponse.base:type_name -> topfans.common.BaseResponse
58, // 23: topfans.asset.UnlikeAssetResponse.base:type_name -> topfans.common.BaseResponse
58, // 24: topfans.asset.CheckAssetLikeResponse.base:type_name -> topfans.common.BaseResponse
58, // 25: topfans.asset.GetAssetLikesResponse.base:type_name -> topfans.common.BaseResponse
32, // 26: topfans.asset.GetAssetLikesResponse.likes:type_name -> topfans.asset.AssetLike
56, // 27: topfans.asset.GetAssetForRPCResponse.base:type_name -> topfans.common.BaseResponse
56, // 28: topfans.asset.UploadMaterialResponse.base:type_name -> topfans.common.BaseResponse
58, // 27: topfans.asset.GetAssetForRPCResponse.base:type_name -> topfans.common.BaseResponse
58, // 28: topfans.asset.UploadMaterialResponse.base:type_name -> topfans.common.BaseResponse
37, // 29: topfans.asset.UploadMaterialResponse.material:type_name -> topfans.asset.Material
38, // 30: topfans.asset.BindAssetMaterialsRequest.materials:type_name -> topfans.asset.AssetMaterialRelation
56, // 31: topfans.asset.BindAssetMaterialsResponse.base:type_name -> topfans.common.BaseResponse
56, // 32: topfans.asset.GetAssetMaterialsResponse.base:type_name -> topfans.common.BaseResponse
58, // 31: topfans.asset.BindAssetMaterialsResponse.base:type_name -> topfans.common.BaseResponse
58, // 32: topfans.asset.GetAssetMaterialsResponse.base:type_name -> topfans.common.BaseResponse
38, // 33: topfans.asset.GetAssetMaterialsResponse.materials:type_name -> topfans.asset.AssetMaterialRelation
46, // 34: topfans.asset.UpdateMaterialLayerOrderRequest.orders:type_name -> topfans.asset.MaterialLayerOrderItem
56, // 35: topfans.asset.UpdateMaterialLayerOrderResponse.base:type_name -> topfans.common.BaseResponse
56, // 36: topfans.asset.UnbindAssetMaterialResponse.base:type_name -> topfans.common.BaseResponse
56, // 37: topfans.asset.ClearAssetLikeRecordsResponse.base:type_name -> topfans.common.BaseResponse
56, // 38: topfans.asset.GetAssetQrcodeResponse.base:type_name -> topfans.common.BaseResponse
57, // 39: topfans.asset.TrackShareRequest.extra:type_name -> google.protobuf.Struct
56, // 40: topfans.asset.TrackShareResponse.base:type_name -> topfans.common.BaseResponse
3, // 41: topfans.asset.AssetService.InitMintOrder:input_type -> topfans.asset.InitMintOrderRequest
6, // 42: topfans.asset.AssetService.PreCreateMintOrder:input_type -> topfans.asset.PreCreateMintOrderRequest
5, // 43: topfans.asset.AssetService.CreateMintOrder:input_type -> topfans.asset.CreateMintOrderRequest
9, // 44: topfans.asset.AssetService.EstimateMintCost:input_type -> topfans.asset.EstimateMintCostRequest
11, // 45: topfans.asset.AssetService.GetMyAssets:input_type -> topfans.asset.GetMyAssetsRequest
18, // 46: topfans.asset.AssetService.GetAsset:input_type -> topfans.asset.GetAssetRequest
20, // 47: topfans.asset.AssetService.GetAssetStatus:input_type -> topfans.asset.GetAssetStatusRequest
22, // 48: topfans.asset.AssetService.GetMintOrder:input_type -> topfans.asset.GetMintOrderRequest
24, // 49: topfans.asset.AssetService.CancelMintOrder:input_type -> topfans.asset.CancelMintOrderRequest
35, // 50: topfans.asset.AssetService.GetAssetForRPC:input_type -> topfans.asset.GetAssetForRPCRequest
26, // 51: topfans.asset.AssetService.LikeAsset:input_type -> topfans.asset.LikeAssetRequest
28, // 52: topfans.asset.AssetService.UnlikeAsset:input_type -> topfans.asset.UnlikeAssetRequest
30, // 53: topfans.asset.AssetService.CheckAssetLike:input_type -> topfans.asset.CheckAssetLikeRequest
33, // 54: topfans.asset.AssetService.GetAssetLikes:input_type -> topfans.asset.GetAssetLikesRequest
50, // 55: topfans.asset.AssetService.ClearAssetLikeRecords:input_type -> topfans.asset.ClearAssetLikeRecordsRequest
39, // 56: topfans.asset.AssetService.UploadMaterial:input_type -> topfans.asset.UploadMaterialRequest
41, // 57: topfans.asset.AssetService.BindAssetMaterials:input_type -> topfans.asset.BindAssetMaterialsRequest
43, // 58: topfans.asset.AssetService.GetAssetMaterials:input_type -> topfans.asset.GetAssetMaterialsRequest
45, // 59: topfans.asset.AssetService.UpdateMaterialLayerOrder:input_type -> topfans.asset.UpdateMaterialLayerOrderRequest
48, // 60: topfans.asset.AssetService.UnbindAssetMaterial:input_type -> topfans.asset.UnbindAssetMaterialRequest
52, // 61: topfans.asset.AssetService.GetAssetQrcode:input_type -> topfans.asset.GetAssetQrcodeRequest
54, // 62: topfans.asset.AssetService.TrackShare:input_type -> topfans.asset.TrackShareRequest
4, // 63: topfans.asset.AssetService.InitMintOrder:output_type -> topfans.asset.InitMintOrderResponse
7, // 64: topfans.asset.AssetService.PreCreateMintOrder:output_type -> topfans.asset.PreCreateMintOrderResponse
8, // 65: topfans.asset.AssetService.CreateMintOrder:output_type -> topfans.asset.CreateMintOrderResponse
10, // 66: topfans.asset.AssetService.EstimateMintCost:output_type -> topfans.asset.EstimateMintCostResponse
12, // 67: topfans.asset.AssetService.GetMyAssets:output_type -> topfans.asset.GetMyAssetsResponse
19, // 68: topfans.asset.AssetService.GetAsset:output_type -> topfans.asset.GetAssetResponse
21, // 69: topfans.asset.AssetService.GetAssetStatus:output_type -> topfans.asset.GetAssetStatusResponse
23, // 70: topfans.asset.AssetService.GetMintOrder:output_type -> topfans.asset.GetMintOrderResponse
25, // 71: topfans.asset.AssetService.CancelMintOrder:output_type -> topfans.asset.CancelMintOrderResponse
36, // 72: topfans.asset.AssetService.GetAssetForRPC:output_type -> topfans.asset.GetAssetForRPCResponse
27, // 73: topfans.asset.AssetService.LikeAsset:output_type -> topfans.asset.LikeAssetResponse
29, // 74: topfans.asset.AssetService.UnlikeAsset:output_type -> topfans.asset.UnlikeAssetResponse
31, // 75: topfans.asset.AssetService.CheckAssetLike:output_type -> topfans.asset.CheckAssetLikeResponse
34, // 76: topfans.asset.AssetService.GetAssetLikes:output_type -> topfans.asset.GetAssetLikesResponse
51, // 77: topfans.asset.AssetService.ClearAssetLikeRecords:output_type -> topfans.asset.ClearAssetLikeRecordsResponse
40, // 78: topfans.asset.AssetService.UploadMaterial:output_type -> topfans.asset.UploadMaterialResponse
42, // 79: topfans.asset.AssetService.BindAssetMaterials:output_type -> topfans.asset.BindAssetMaterialsResponse
44, // 80: topfans.asset.AssetService.GetAssetMaterials:output_type -> topfans.asset.GetAssetMaterialsResponse
47, // 81: topfans.asset.AssetService.UpdateMaterialLayerOrder:output_type -> topfans.asset.UpdateMaterialLayerOrderResponse
49, // 82: topfans.asset.AssetService.UnbindAssetMaterial:output_type -> topfans.asset.UnbindAssetMaterialResponse
53, // 83: topfans.asset.AssetService.GetAssetQrcode:output_type -> topfans.asset.GetAssetQrcodeResponse
55, // 84: topfans.asset.AssetService.TrackShare:output_type -> topfans.asset.TrackShareResponse
63, // [63:85] is the sub-list for method output_type
41, // [41:63] is the sub-list for method input_type
41, // [41:41] is the sub-list for extension type_name
41, // [41:41] is the sub-list for extension extendee
0, // [0:41] is the sub-list for field type_name
58, // 35: topfans.asset.UpdateMaterialLayerOrderResponse.base:type_name -> topfans.common.BaseResponse
58, // 36: topfans.asset.UnbindAssetMaterialResponse.base:type_name -> topfans.common.BaseResponse
58, // 37: topfans.asset.ClearAssetLikeRecordsResponse.base:type_name -> topfans.common.BaseResponse
58, // 38: topfans.asset.GetAssetQrcodeResponse.base:type_name -> topfans.common.BaseResponse
59, // 39: topfans.asset.TrackShareRequest.extra:type_name -> google.protobuf.Struct
58, // 40: topfans.asset.TrackShareResponse.base:type_name -> topfans.common.BaseResponse
58, // 41: topfans.asset.GetAssetsByTypeResponse.base:type_name -> topfans.common.BaseResponse
13, // 42: topfans.asset.GetAssetsByTypeResponse.data:type_name -> topfans.asset.AssetListData
3, // 43: topfans.asset.AssetService.InitMintOrder:input_type -> topfans.asset.InitMintOrderRequest
6, // 44: topfans.asset.AssetService.PreCreateMintOrder:input_type -> topfans.asset.PreCreateMintOrderRequest
5, // 45: topfans.asset.AssetService.CreateMintOrder:input_type -> topfans.asset.CreateMintOrderRequest
9, // 46: topfans.asset.AssetService.EstimateMintCost:input_type -> topfans.asset.EstimateMintCostRequest
11, // 47: topfans.asset.AssetService.GetMyAssets:input_type -> topfans.asset.GetMyAssetsRequest
18, // 48: topfans.asset.AssetService.GetAsset:input_type -> topfans.asset.GetAssetRequest
20, // 49: topfans.asset.AssetService.GetAssetStatus:input_type -> topfans.asset.GetAssetStatusRequest
22, // 50: topfans.asset.AssetService.GetMintOrder:input_type -> topfans.asset.GetMintOrderRequest
24, // 51: topfans.asset.AssetService.CancelMintOrder:input_type -> topfans.asset.CancelMintOrderRequest
35, // 52: topfans.asset.AssetService.GetAssetForRPC:input_type -> topfans.asset.GetAssetForRPCRequest
26, // 53: topfans.asset.AssetService.LikeAsset:input_type -> topfans.asset.LikeAssetRequest
28, // 54: topfans.asset.AssetService.UnlikeAsset:input_type -> topfans.asset.UnlikeAssetRequest
30, // 55: topfans.asset.AssetService.CheckAssetLike:input_type -> topfans.asset.CheckAssetLikeRequest
33, // 56: topfans.asset.AssetService.GetAssetLikes:input_type -> topfans.asset.GetAssetLikesRequest
50, // 57: topfans.asset.AssetService.ClearAssetLikeRecords:input_type -> topfans.asset.ClearAssetLikeRecordsRequest
39, // 58: topfans.asset.AssetService.UploadMaterial:input_type -> topfans.asset.UploadMaterialRequest
41, // 59: topfans.asset.AssetService.BindAssetMaterials:input_type -> topfans.asset.BindAssetMaterialsRequest
43, // 60: topfans.asset.AssetService.GetAssetMaterials:input_type -> topfans.asset.GetAssetMaterialsRequest
45, // 61: topfans.asset.AssetService.UpdateMaterialLayerOrder:input_type -> topfans.asset.UpdateMaterialLayerOrderRequest
48, // 62: topfans.asset.AssetService.UnbindAssetMaterial:input_type -> topfans.asset.UnbindAssetMaterialRequest
52, // 63: topfans.asset.AssetService.GetAssetQrcode:input_type -> topfans.asset.GetAssetQrcodeRequest
54, // 64: topfans.asset.AssetService.TrackShare:input_type -> topfans.asset.TrackShareRequest
56, // 65: topfans.asset.AssetService.GetAssetsByType:input_type -> topfans.asset.GetAssetsByTypeRequest
4, // 66: topfans.asset.AssetService.InitMintOrder:output_type -> topfans.asset.InitMintOrderResponse
7, // 67: topfans.asset.AssetService.PreCreateMintOrder:output_type -> topfans.asset.PreCreateMintOrderResponse
8, // 68: topfans.asset.AssetService.CreateMintOrder:output_type -> topfans.asset.CreateMintOrderResponse
10, // 69: topfans.asset.AssetService.EstimateMintCost:output_type -> topfans.asset.EstimateMintCostResponse
12, // 70: topfans.asset.AssetService.GetMyAssets:output_type -> topfans.asset.GetMyAssetsResponse
19, // 71: topfans.asset.AssetService.GetAsset:output_type -> topfans.asset.GetAssetResponse
21, // 72: topfans.asset.AssetService.GetAssetStatus:output_type -> topfans.asset.GetAssetStatusResponse
23, // 73: topfans.asset.AssetService.GetMintOrder:output_type -> topfans.asset.GetMintOrderResponse
25, // 74: topfans.asset.AssetService.CancelMintOrder:output_type -> topfans.asset.CancelMintOrderResponse
36, // 75: topfans.asset.AssetService.GetAssetForRPC:output_type -> topfans.asset.GetAssetForRPCResponse
27, // 76: topfans.asset.AssetService.LikeAsset:output_type -> topfans.asset.LikeAssetResponse
29, // 77: topfans.asset.AssetService.UnlikeAsset:output_type -> topfans.asset.UnlikeAssetResponse
31, // 78: topfans.asset.AssetService.CheckAssetLike:output_type -> topfans.asset.CheckAssetLikeResponse
34, // 79: topfans.asset.AssetService.GetAssetLikes:output_type -> topfans.asset.GetAssetLikesResponse
51, // 80: topfans.asset.AssetService.ClearAssetLikeRecords:output_type -> topfans.asset.ClearAssetLikeRecordsResponse
40, // 81: topfans.asset.AssetService.UploadMaterial:output_type -> topfans.asset.UploadMaterialResponse
42, // 82: topfans.asset.AssetService.BindAssetMaterials:output_type -> topfans.asset.BindAssetMaterialsResponse
44, // 83: topfans.asset.AssetService.GetAssetMaterials:output_type -> topfans.asset.GetAssetMaterialsResponse
47, // 84: topfans.asset.AssetService.UpdateMaterialLayerOrder:output_type -> topfans.asset.UpdateMaterialLayerOrderResponse
49, // 85: topfans.asset.AssetService.UnbindAssetMaterial:output_type -> topfans.asset.UnbindAssetMaterialResponse
53, // 86: topfans.asset.AssetService.GetAssetQrcode:output_type -> topfans.asset.GetAssetQrcodeResponse
55, // 87: topfans.asset.AssetService.TrackShare:output_type -> topfans.asset.TrackShareResponse
57, // 88: topfans.asset.AssetService.GetAssetsByType:output_type -> topfans.asset.GetAssetsByTypeResponse
66, // [66:89] is the sub-list for method output_type
43, // [43:66] is the sub-list for method input_type
43, // [43:43] is the sub-list for extension type_name
43, // [43:43] is the sub-list for extension extendee
0, // [0:43] is the sub-list for field type_name
}
func init() { file_asset_proto_init() }
func init() {
file_asset_proto_init()
registerAssetServiceGetAssetsByType()
}
func file_asset_proto_init() {
if File_asset_proto != nil {
return
@ -4475,7 +4699,7 @@ func file_asset_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_asset_proto_rawDesc), len(file_asset_proto_rawDesc)),
NumEnums: 0,
NumMessages: 56,
NumMessages: 58,
NumExtensions: 0,
NumServices: 1,
},

View File

@ -1,757 +0,0 @@
// 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
DisplayStatus int32 `protobuf:"varint,8,opt,name=display_status,json=displayStatus,proto3" json:"display_status,omitempty"` // 展示状态0=待展示, 1=已展示
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
}
func (x *AssetItem) GetDisplayStatus() int32 {
if x != nil {
return x.DisplayStatus
}
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\"\xfb\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\x12%\n" +
"\x0edisplay_status\x18\b \x01(\x05R\rdisplayStatus\"\x90\x01\n" +
"\x17GetStarbookItemsRequest\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x12\x1a\n" +
"\bcategory\x18\x02 \x01(\tR\bcategory\x12\x14\n" +
"\x05grade\x18\x03 \x01(\x05R\x05grade\x12\x12\n" +
"\x04page\x18\x04 \x01(\x05R\x04page\x12\x1b\n" +
"\tpage_size\x18\x05 \x01(\x05R\bpageSize\"\x81\x01\n" +
"\x18GetStarbookItemsResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x123\n" +
"\x04data\x18\x02 \x01(\v2\x1f.topfans.starbook.AssetListDataR\x04data\"\xa4\x01\n" +
"\rAssetListData\x121\n" +
"\x05items\x18\x01 \x03(\v2\x1b.topfans.starbook.AssetItemR\x05items\x12\x14\n" +
"\x05total\x18\x02 \x01(\x03R\x05total\x12\x12\n" +
"\x04page\x18\x03 \x01(\x05R\x04page\x12\x1b\n" +
"\tpage_size\x18\x04 \x01(\x05R\bpageSize\x12\x19\n" +
"\bhas_more\x18\x05 \x01(\bR\ahasMore2\xa5\x02\n" +
"\x0fStarbookService\x12\x85\x01\n" +
"\x0fGetStarbookHome\x12(.topfans.starbook.GetStarbookHomeRequest\x1a).topfans.starbook.GetStarbookHomeResponse\"\x1d\x82\xd3\xe4\x93\x02\x17\x12\x15/api/v1/starbook/home\x12\x89\x01\n" +
"\x10GetStarbookItems\x12).topfans.starbook.GetStarbookItemsRequest\x1a*.topfans.starbook.GetStarbookItemsResponse\"\x1e\x82\xd3\xe4\x93\x02\x18\x12\x16/api/v1/starbook/itemsB8Z6github.com/topfans/backend/pkg/proto/starbook;starbookb\x06proto3"
var (
file_starbook_proto_rawDescOnce sync.Once
file_starbook_proto_rawDescData []byte
)
func file_starbook_proto_rawDescGZIP() []byte {
file_starbook_proto_rawDescOnce.Do(func() {
file_starbook_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_starbook_proto_rawDesc), len(file_starbook_proto_rawDesc)))
})
return file_starbook_proto_rawDescData
}
var file_starbook_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
var file_starbook_proto_goTypes = []any{
(*GetStarbookHomeRequest)(nil), // 0: topfans.starbook.GetStarbookHomeRequest
(*GetStarbookHomeResponse)(nil), // 1: topfans.starbook.GetStarbookHomeResponse
(*StarbookHomeData)(nil), // 2: topfans.starbook.StarbookHomeData
(*AssetGroup)(nil), // 3: topfans.starbook.AssetGroup
(*GradeSection)(nil), // 4: topfans.starbook.GradeSection
(*AssetItem)(nil), // 5: topfans.starbook.AssetItem
(*GetStarbookItemsRequest)(nil), // 6: topfans.starbook.GetStarbookItemsRequest
(*GetStarbookItemsResponse)(nil), // 7: topfans.starbook.GetStarbookItemsResponse
(*AssetListData)(nil), // 8: topfans.starbook.AssetListData
(*common.BaseResponse)(nil), // 9: topfans.common.BaseResponse
}
var file_starbook_proto_depIdxs = []int32{
9, // 0: topfans.starbook.GetStarbookHomeResponse.base:type_name -> topfans.common.BaseResponse
2, // 1: topfans.starbook.GetStarbookHomeResponse.data:type_name -> topfans.starbook.StarbookHomeData
3, // 2: topfans.starbook.StarbookHomeData.groups:type_name -> topfans.starbook.AssetGroup
4, // 3: topfans.starbook.AssetGroup.grades:type_name -> topfans.starbook.GradeSection
5, // 4: topfans.starbook.AssetGroup.items:type_name -> topfans.starbook.AssetItem
5, // 5: topfans.starbook.GradeSection.items:type_name -> topfans.starbook.AssetItem
9, // 6: topfans.starbook.GetStarbookItemsResponse.base:type_name -> topfans.common.BaseResponse
8, // 7: topfans.starbook.GetStarbookItemsResponse.data:type_name -> topfans.starbook.AssetListData
5, // 8: topfans.starbook.AssetListData.items:type_name -> topfans.starbook.AssetItem
0, // 9: topfans.starbook.StarbookService.GetStarbookHome:input_type -> topfans.starbook.GetStarbookHomeRequest
6, // 10: topfans.starbook.StarbookService.GetStarbookItems:input_type -> topfans.starbook.GetStarbookItemsRequest
1, // 11: topfans.starbook.StarbookService.GetStarbookHome:output_type -> topfans.starbook.GetStarbookHomeResponse
7, // 12: topfans.starbook.StarbookService.GetStarbookItems:output_type -> topfans.starbook.GetStarbookItemsResponse
11, // [11:13] is the sub-list for method output_type
9, // [9:11] is the sub-list for method input_type
9, // [9:9] is the sub-list for extension type_name
9, // [9:9] is the sub-list for extension extendee
0, // [0:9] is the sub-list for field type_name
}
func init() { file_starbook_proto_init() }
func file_starbook_proto_init() {
if File_starbook_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_starbook_proto_rawDesc), len(file_starbook_proto_rawDesc)),
NumEnums: 0,
NumMessages: 9,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_starbook_proto_goTypes,
DependencyIndexes: file_starbook_proto_depIdxs,
MessageInfos: file_starbook_proto_msgTypes,
}.Build()
File_starbook_proto = out.File
file_starbook_proto_goTypes = nil
file_starbook_proto_depIdxs = nil
}

View File

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

View File

@ -104,8 +104,9 @@ func (p *AIChatProvider) SendMessage(ctx context.Context, req *pb.ChatMessageReq
logger.Logger.Info("Received SendMessage request",
zap.Int64("user_id", userID),
zap.String("session_id", sessionID),
zap.String("message", message),
zap.Int("message_len", len(message)),
)
// 不打印 message 原文以防 PII 泄露;长度足够排查空包 / 超长包。
// 1. 前置审核
if !p.auditService.AuditText(message) {

View File

@ -31,7 +31,6 @@ 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"
"github.com/topfans/backend/services/assetService/util"
"github.com/topfans/backend/services/assetService/util/ossutil"
@ -163,7 +162,7 @@ func main() {
logger.Logger.Info("AssetLevelProvider initialized")
// 创建 Service 层实例
registryRepo := starbookRepo.NewAssetRegistryRepository(database.GetDB())
registryRepo := repository.NewAssetRegistryRepository(database.GetDB())
// 分享服务依赖:OSS QR 上传器 + Redis 缓存 + 落地页 base URL
ossCfg := util.OSSConfig{

View File

@ -42,6 +42,13 @@ type AssetProvider struct {
// 确保 AssetProvider 实现了 AssetServiceHandler 接口
var _ pb.AssetServiceHandler = (*AssetProvider)(nil)
// 确保 AssetProvider 同时满足扩展 handler(Task 1 在 asset.pb.go 里通过 init()
// 把 GetAssetsByType 追加进了共享的 AssetService_ServiceInfo.Methods,
// 其 dispatch 会把注册的 handler 断言为 AssetServiceGetAssetsByTypeHandler。
// 因此 main.go 无需改动:现有 RegisterAssetServiceHandler(srv, assetProvider)
// 已经把本方法一并路由,只要 *AssetProvider 实现了它)。
var _ pb.AssetServiceGetAssetsByTypeHandler = (*AssetProvider)(nil)
// NewAssetProvider 创建资产服务Provider实例
func NewAssetProvider(assetService service.AssetService, mintService service.MintService, assetLikeService *service.AssetLikeService, materialService *service.MaterialService) *AssetProvider {
return &AssetProvider{
@ -235,6 +242,60 @@ func (p *AssetProvider) GetMyAssets(ctx context.Context, req *pb.GetMyAssetsRequ
return resp, nil
}
// GetAssetsByType 按类型/分类/等级过滤查询星册藏品(starbook 分类页) — 委托给 AssetService。
//
// 身份必须来自网关经 Dubbo attachments 注入的可信 user_id/star_id;
// req.UserId/req.StarId 是调用方可控字段,一律不读,避免越权查看他人星册。
// 与 GetMyAssets 使用同一套身份提取,保证 gateway 侧调用方式一致。
func (p *AssetProvider) GetAssetsByType(ctx context.Context, req *pb.GetAssetsByTypeRequest) (*pb.GetAssetsByTypeResponse, error) {
logger.Logger.Info("Received GetAssetsByType request",
zap.String("type", req.Type),
zap.String("category", req.Category),
zap.Int32("grade", req.Grade),
)
userID, starID, err := extractUserInfoFromDubboAttachments(ctx)
if err != nil {
logger.Logger.Error("GetAssetsByType failed to extract user info from attachments",
zap.Error(err),
)
return &pb.GetAssetsByTypeResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(codes.Unauthenticated),
Message: "user authentication required",
Timestamp: 0,
},
}, err
}
resp, err := p.assetService.GetAssetsByType(req, userID, starID)
if err != nil {
logger.Logger.Error("GetAssetsByType failed",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Error(err),
)
if resp == nil {
resp = &pb.GetAssetsByTypeResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(appErrors.ToGRPCCode(err)),
Message: err.Error(),
Timestamp: 0,
},
}
}
return resp, err
}
logger.Logger.Debug("GetAssetsByType successful",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int("group_count", len(resp.Data.Groups)),
)
return resp, nil
}
// GetAsset 获取资产详情
func (p *AssetProvider) GetAsset(ctx context.Context, req *pb.GetAssetRequest) (*pb.GetAssetResponse, error) {
// 记录请求日志

View File

@ -58,6 +58,9 @@ type fakeAssetService struct {
func (f *fakeAssetService) GetMyAssets(*pb.GetMyAssetsRequest, int64, int64) (*pb.GetMyAssetsResponse, error) {
return nil, nil
}
func (f *fakeAssetService) GetAssetsByType(*pb.GetAssetsByTypeRequest, int64, int64) (*pb.GetAssetsByTypeResponse, error) {
return nil, nil
}
func (f *fakeAssetService) GetAsset(*pb.GetAssetRequest, int64, int64) (*pb.GetAssetResponse, error) {
return nil, nil
}

View File

@ -0,0 +1,232 @@
package provider
import (
"context"
"os"
"strconv"
"testing"
"time"
"dubbo.apache.org/dubbo-go/v3/common/constant"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"github.com/topfans/backend/pkg/database"
"github.com/topfans/backend/pkg/models"
pb "github.com/topfans/backend/pkg/proto/asset"
"github.com/topfans/backend/services/assetService/repository"
"github.com/topfans/backend/services/assetService/service"
"gorm.io/gorm"
)
// ---- GetAssetsByType (DB-backed, 自包含) ----
//
// 这些用例验证 starbook 分类查询的过滤语义:
// - type 过滤: 只返回请求类型的分组
// - grade 过滤: regular 类型下按等级筛选
// - category 过滤: 分类维度筛选
// - 未认证: ctx 无可信身份时拒绝
//
// 自包含约定(遵循 asset_level_service_test.go 的范式):
// - TEST_DB_* 覆盖,默认 localhost:15432 / postgres / 123456 / top-fans
// - 连不上则 t.Skip,不污染其它测试
// - 只清理本用例写入的 sentinel owner_uid 行,不做大范围 cleanup
const (
// sentinel 身份: 高位取值避免与既有测试/业务数据碰撞
sbTestOwnerUID = int64(990201)
sbTestStarID = int64(990202)
)
func sbEnvOrDefault(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
// starbookTestDB 建立 GetAssetsByType 用例专用的 DB 连接。
func starbookTestDB(t *testing.T) *gorm.DB {
t.Helper()
if os.Getenv("SKIP_DB_TESTS") != "" {
t.Skip("SKIP_DB_TESTS set")
}
portStr := sbEnvOrDefault("TEST_DB_PORT", "15432")
port, _ := strconv.Atoi(portStr)
if port == 0 {
port = 15432
}
if err := database.Init(database.Config{
Host: sbEnvOrDefault("TEST_DB_HOST", "localhost"),
Port: port,
User: sbEnvOrDefault("TEST_DB_USER", "postgres"),
Password: sbEnvOrDefault("TEST_DB_PASSWORD", "123456"),
DBName: sbEnvOrDefault("TEST_DB_NAME", "top-fans"),
SSLMode: "disable",
TimeZone: "Asia/Shanghai",
}); err != nil {
t.Skipf("Skipping: cannot connect to test database: %v", err)
}
db := database.GetDB()
if err := db.AutoMigrate(&models.Asset{}, &models.AssetRegistry{}); err != nil {
t.Skipf("Skipping: AutoMigrate failed: %v", err)
}
return db
}
// sbCleanup 只删本用例的 sentinel 行(assets/registry + FK 依赖的 user/star)。
func sbCleanup(t *testing.T, db *gorm.DB) {
t.Helper()
db.Where("owner_uid = ? AND star_id = ?", sbTestOwnerUID, sbTestStarID).Delete(&models.AssetRegistry{})
db.Where("owner_uid = ? AND star_id = ?", sbTestOwnerUID, sbTestStarID).Delete(&models.Asset{})
db.Exec("DELETE FROM stars WHERE star_id = ?", sbTestStarID)
db.Exec("DELETE FROM users WHERE id = ?", sbTestOwnerUID)
}
// sbSeed 写入 sentinel 藏品 + registry:
// - regular grade 1
// - regular grade 3
// - collection (category=手办)
func sbSeed(t *testing.T, db *gorm.DB) {
t.Helper()
sbCleanup(t, db)
now := time.Now().UnixMilli()
// assets FK: owner_uid -> users(id), star_id -> stars(star_id)。
// 显式 id 插入后按 CLAUDE.md 规范同步序列,避免后续 GORM 自增撞主键。
require.NoError(t, db.Exec(
`INSERT INTO users (id, mobile, password_hash, created_at, updated_at)
VALUES (?, ?, 'x', ?, ?) ON CONFLICT (id) DO NOTHING`,
sbTestOwnerUID, "99902010001", now, now).Error)
require.NoError(t, db.Exec(`SELECT setval('users_id_seq', (SELECT MAX(id) FROM users))`).Error)
require.NoError(t, db.Exec(
`INSERT INTO stars (star_id, name, identity_id, created_at, updated_at)
VALUES (?, 'sb_test_star', 'sb_test_identity', ?, ?) ON CONFLICT (star_id) DO NOTHING`,
sbTestStarID, now, now).Error)
require.NoError(t, db.Exec(`SELECT setval('stars_star_id_seq', (SELECT MAX(star_id) FROM stars))`).Error)
newAsset := func(name string) *models.Asset {
a := &models.Asset{
OwnerUID: sbTestOwnerUID,
StarID: sbTestStarID,
Name: name,
CoverURL: "https://cdn/" + name + ".png",
Status: 1,
IsActive: true,
CreatedAt: now,
UpdatedAt: now,
}
require.NoError(t, db.Create(a).Error)
return a
}
grade1 := int32(1)
grade3 := int32(3)
cat := "手办"
regA := newAsset("regular_g1")
regB := newAsset("regular_g3")
colC := newAsset("collection_x")
regs := []*models.AssetRegistry{
{AssetID: regA.ID, AssetType: models.AssetTypeRegular, OwnerUID: sbTestOwnerUID, StarID: sbTestStarID, Grade: &grade1, DisplayStatus: 1},
{AssetID: regB.ID, AssetType: models.AssetTypeRegular, OwnerUID: sbTestOwnerUID, StarID: sbTestStarID, Grade: &grade3, DisplayStatus: 1},
{AssetID: colC.ID, AssetType: models.AssetTypeCollection, OwnerUID: sbTestOwnerUID, StarID: sbTestStarID, CollectionCategory: &cat, DisplayStatus: 1},
}
for _, r := range regs {
require.NoError(t, db.Create(r).Error)
}
t.Cleanup(func() { sbCleanup(t, db) })
}
// sbProvider 组装真实 service真实 repo + test DB无关依赖传 nil。
func sbProvider(db *gorm.DB) *AssetProvider {
assetRepo := repository.NewAssetRepository(db)
registryRepo := repository.NewAssetRegistryRepository(db)
svc := service.NewAssetService(assetRepo, nil, nil, nil, db, registryRepo, nil)
return &AssetProvider{assetService: svc}
}
// sbCtx 构造携带可信身份的 Dubbo attachments ctx。
func sbCtx(userID, starID int64) context.Context {
return context.WithValue(context.Background(), constant.AttachmentKey, map[string]interface{}{
"user_id": userID,
"star_id": starID,
})
}
func TestGetAssetsByType_FilterByTypeRegular(t *testing.T) {
db := starbookTestDB(t)
sbSeed(t, db)
p := sbProvider(db)
resp, err := p.GetAssetsByType(sbCtx(sbTestOwnerUID, sbTestStarID), &pb.GetAssetsByTypeRequest{
Type: models.AssetTypeRegular,
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, resp.Data)
require.Len(t, resp.Data.Groups, 1, "type=regular 只应返回 regular 分组")
assert.Equal(t, models.AssetTypeRegular, resp.Data.Groups[0].Type)
// regular 分组含 grade 1 + grade 3 两个等级
var total int32
for _, g := range resp.Data.Groups[0].Grades {
total += g.TotalCount
}
assert.Equal(t, int32(2), total, "regular 应含两条(grade1+grade3)")
}
func TestGetAssetsByType_FilterByGrade(t *testing.T) {
db := starbookTestDB(t)
sbSeed(t, db)
p := sbProvider(db)
resp, err := p.GetAssetsByType(sbCtx(sbTestOwnerUID, sbTestStarID), &pb.GetAssetsByTypeRequest{
Type: models.AssetTypeRegular,
Grade: 3,
})
require.NoError(t, err)
require.NotNil(t, resp.Data)
require.Len(t, resp.Data.Groups, 1)
grades := resp.Data.Groups[0].Grades
require.Len(t, grades, 1, "grade=3 过滤后只剩一个等级段")
assert.Equal(t, int32(3), grades[0].Grade)
assert.Equal(t, int32(1), grades[0].TotalCount, "grade=3 只有一条")
}
func TestGetAssetsByType_FilterByCategory(t *testing.T) {
db := starbookTestDB(t)
sbSeed(t, db)
p := sbProvider(db)
resp, err := p.GetAssetsByType(sbCtx(sbTestOwnerUID, sbTestStarID), &pb.GetAssetsByTypeRequest{
Type: models.AssetTypeCollection,
Category: "手办",
})
require.NoError(t, err)
require.NotNil(t, resp.Data)
require.Len(t, resp.Data.Groups, 1, "type=collection + category=手办 只返回该分组")
assert.Equal(t, models.AssetTypeCollection, resp.Data.Groups[0].Type)
assert.Equal(t, int32(1), resp.Data.Groups[0].TotalCount)
}
func TestGetAssetsByType_NoIdentity(t *testing.T) {
// 无需 DB: 缺身份必须在触达 service 前拒绝。
p := &AssetProvider{}
resp, err := p.GetAssetsByType(context.Background(), &pb.GetAssetsByTypeRequest{
Type: models.AssetTypeRegular,
})
require.Error(t, err)
if resp != nil && resp.Base != nil {
assert.Equal(t, uint32(codes.Unauthenticated), resp.Base.Code)
}
}

View File

@ -0,0 +1,392 @@
package repository
import (
"errors"
appErrors "github.com/topfans/backend/pkg/errors"
"github.com/topfans/backend/pkg/models"
"gorm.io/gorm"
)
// AssetRegistryRepository 资产统一索引Repository接口
//
// 历史:曾位于 github.com/topfans/backend/services/starbookService/repository,
// starbookService 已被决断删除(批次 4.2)。assetService 是 AssetRegistry
// 数据的真正写入方(GetMyAssets、铸造流程等都在 assetService),
// 故该仓储下沉为 assetService 内置模块。
type AssetRegistryRepository interface {
// Create 创建索引记录
Create(registry *models.AssetRegistry) error
// GetByID 根据ID查询
GetByID(id int64) (*models.AssetRegistry, error)
// GetByAssetID 根据asset_id查询
GetByAssetID(assetID int64) (*models.AssetRegistry, error)
// GetByAssetTypeAndID 根据类型和asset_id查询
GetByAssetTypeAndID(assetType string, assetID int64) (*models.AssetRegistry, error)
// GetByOwner 查询用户的所有索引记录
GetByOwner(ownerUID, starID int64) ([]*models.AssetRegistry, error)
// GetByOwnerAndType 查询用户指定类型的索引记录
GetByOwnerAndType(ownerUID, starID int64, assetType string, limit, offset int) ([]*models.AssetRegistry, error)
// GetByOwnerAndTypeAndGrade 查询用户指定类型和等级的索引记录
GetByOwnerAndTypeAndGrade(ownerUID, starID int64, assetType string, grade int32, limit, offset int) ([]*models.AssetRegistry, error)
// GetByOwnerAndTypeAndCategory 查询用户指定类型和分类的索引记录
GetByOwnerAndTypeAndCategory(ownerUID, starID int64, assetType string, category string, limit, offset int) ([]*models.AssetRegistry, error)
// GetByOwnerAndTypeAndActivity 查询用户指定类型和活动的索引记录
GetByOwnerAndTypeAndActivity(ownerUID, starID int64, assetType string, activityID int64, limit, offset int) ([]*models.AssetRegistry, error)
// CountByOwner 统计用户的索引记录数量
CountByOwner(ownerUID, starID int64) (int64, error)
// CountByOwnerAndType 统计用户指定类型的索引记录数量
CountByOwnerAndType(ownerUID, starID int64, assetType string) (int64, error)
// CountByOwnerAndTypeAndGrade 统计用户指定类型和等级的索引记录数量
CountByOwnerAndTypeAndGrade(ownerUID, starID int64, assetType string, grade int32) (int64, error)
// CountByOwnerAndTypeAndCategory 统计用户指定类型和分类的索引记录数量
CountByOwnerAndTypeAndCategory(ownerUID, starID int64, assetType string, category string) (int64, error)
// CountByOwnerAndTypeAndActivity 统计用户指定类型和活动的索引记录数量
CountByOwnerAndTypeAndActivity(ownerUID, starID int64, assetType string, activityID int64) (int64, error)
// UpdateLikeCount 更新点赞数
UpdateLikeCount(assetID int64, likeCount int32) error
// UpdateGrade 更新等级
UpdateGrade(assetID int64, grade int32) error
// Delete 删除索引记录
Delete(assetID int64) error
// DeleteByAssetType 删除指定类型的索引记录
DeleteByAssetType(assetType string, assetID int64) error
}
// assetRegistryRepository 资产统一索引Repository实现
type assetRegistryRepository struct {
db *gorm.DB
}
// NewAssetRegistryRepository 创建资产统一索引Repository实例
func NewAssetRegistryRepository(db *gorm.DB) AssetRegistryRepository {
return &assetRegistryRepository{db: db}
}
// Create 创建索引记录
func (r *assetRegistryRepository) Create(registry *models.AssetRegistry) error {
if registry == nil {
return errors.New("registry cannot be nil")
}
if registry.OwnerUID <= 0 {
return errors.New("owner_uid must be greater than 0")
}
if registry.StarID <= 0 {
return errors.New("star_id must be greater than 0")
}
return r.db.Create(registry).Error
}
// GetByID 根据ID查询
func (r *assetRegistryRepository) GetByID(id int64) (*models.AssetRegistry, error) {
if id <= 0 {
return nil, errors.New("id must be greater than 0")
}
var registry models.AssetRegistry
if err := r.db.Where("id = ?", id).First(&registry).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, appErrors.ErrAssetRegistryNotFound
}
return nil, err
}
return &registry, nil
}
// GetByAssetID 根据asset_id查询
func (r *assetRegistryRepository) GetByAssetID(assetID int64) (*models.AssetRegistry, error) {
if assetID <= 0 {
return nil, errors.New("asset_id must be greater than 0")
}
var registry models.AssetRegistry
if err := r.db.Where("asset_id = ?", assetID).First(&registry).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, appErrors.ErrAssetRegistryNotFound
}
return nil, err
}
return &registry, nil
}
// GetByAssetTypeAndID 根据类型和asset_id查询
func (r *assetRegistryRepository) GetByAssetTypeAndID(assetType string, assetID int64) (*models.AssetRegistry, error) {
if assetType == "" {
return nil, errors.New("asset_type must not be empty")
}
if assetID <= 0 {
return nil, errors.New("asset_id must be greater than 0")
}
var registry models.AssetRegistry
if err := r.db.Where("asset_type = ? AND asset_id = ?", assetType, assetID).First(&registry).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, appErrors.ErrAssetRegistryNotFound
}
return nil, err
}
return &registry, nil
}
// GetByOwner 查询用户的所有索引记录
func (r *assetRegistryRepository) GetByOwner(ownerUID, starID int64) ([]*models.AssetRegistry, error) {
if ownerUID <= 0 {
return nil, errors.New("owner_uid must be greater than 0")
}
if starID <= 0 {
return nil, errors.New("star_id must be greater than 0")
}
var registries []*models.AssetRegistry
if err := r.db.
Joins("JOIN assets ON assets.id = asset_registry.asset_id AND assets.deleted_at IS NULL").
Where("asset_registry.owner_uid = ? AND asset_registry.star_id = ?", ownerUID, starID).
Order("asset_registry.created_at DESC").
Find(&registries).Error; err != nil {
return nil, err
}
return registries, nil
}
// GetByOwnerAndType 查询用户指定类型的索引记录
func (r *assetRegistryRepository) GetByOwnerAndType(ownerUID, starID int64, assetType string, limit, offset int) ([]*models.AssetRegistry, error) {
if ownerUID <= 0 {
return nil, errors.New("owner_uid must be greater than 0")
}
if starID <= 0 {
return nil, errors.New("star_id must be greater than 0")
}
var registries []*models.AssetRegistry
query := r.db.
Joins("JOIN assets ON assets.id = asset_registry.asset_id AND assets.deleted_at IS NULL").
Where("asset_registry.owner_uid = ? AND asset_registry.star_id = ? AND asset_registry.asset_type = ?", ownerUID, starID, assetType).
Order("asset_registry.created_at DESC")
if limit > 0 {
query = query.Limit(limit)
}
if offset > 0 {
query = query.Offset(offset)
}
if err := query.Find(&registries).Error; err != nil {
return nil, err
}
return registries, nil
}
// GetByOwnerAndTypeAndGrade 查询用户指定类型和等级的索引记录
func (r *assetRegistryRepository) GetByOwnerAndTypeAndGrade(ownerUID, starID int64, assetType string, grade int32, limit, offset int) ([]*models.AssetRegistry, error) {
if ownerUID <= 0 {
return nil, errors.New("owner_uid must be greater than 0")
}
if starID <= 0 {
return nil, errors.New("star_id must be greater than 0")
}
var registries []*models.AssetRegistry
query := r.db.
Joins("JOIN assets ON assets.id = asset_registry.asset_id AND assets.deleted_at IS NULL").
Where("asset_registry.owner_uid = ? AND asset_registry.star_id = ? AND asset_registry.asset_type = ? AND asset_registry.grade = ?", ownerUID, starID, assetType, grade).
Order("asset_registry.created_at DESC")
if limit > 0 {
query = query.Limit(limit)
}
if offset > 0 {
query = query.Offset(offset)
}
if err := query.Find(&registries).Error; err != nil {
return nil, err
}
return registries, nil
}
// GetByOwnerAndTypeAndCategory 查询用户指定类型和分类的索引记录
func (r *assetRegistryRepository) GetByOwnerAndTypeAndCategory(ownerUID, starID int64, assetType string, category string, limit, offset int) ([]*models.AssetRegistry, error) {
if ownerUID <= 0 {
return nil, errors.New("owner_uid must be greater than 0")
}
if starID <= 0 {
return nil, errors.New("star_id must be greater than 0")
}
var registries []*models.AssetRegistry
query := r.db.
Joins("JOIN assets ON assets.id = asset_registry.asset_id AND assets.deleted_at IS NULL").
Where("asset_registry.owner_uid = ? AND asset_registry.star_id = ? AND asset_registry.asset_type = ? AND asset_registry.collection_category = ?", ownerUID, starID, assetType, category).
Order("asset_registry.created_at DESC")
if limit > 0 {
query = query.Limit(limit)
}
if offset > 0 {
query = query.Offset(offset)
}
if err := query.Find(&registries).Error; err != nil {
return nil, err
}
return registries, nil
}
// GetByOwnerAndTypeAndActivity 查询用户指定类型和活动的索引记录
func (r *assetRegistryRepository) GetByOwnerAndTypeAndActivity(ownerUID, starID int64, assetType string, activityID int64, limit, offset int) ([]*models.AssetRegistry, error) {
if ownerUID <= 0 {
return nil, errors.New("owner_uid must be greater than 0")
}
if starID <= 0 {
return nil, errors.New("star_id must be greater than 0")
}
var registries []*models.AssetRegistry
query := r.db.
Joins("JOIN assets ON assets.id = asset_registry.asset_id AND assets.deleted_at IS NULL").
Where("asset_registry.owner_uid = ? AND asset_registry.star_id = ? AND asset_registry.asset_type = ? AND asset_registry.activity_id = ?", ownerUID, starID, assetType, activityID).
Order("asset_registry.created_at DESC")
if limit > 0 {
query = query.Limit(limit)
}
if offset > 0 {
query = query.Offset(offset)
}
if err := query.Find(&registries).Error; err != nil {
return nil, err
}
return registries, nil
}
// CountByOwner 统计用户的索引记录数量
func (r *assetRegistryRepository) CountByOwner(ownerUID, starID int64) (int64, error) {
if ownerUID <= 0 {
return 0, errors.New("owner_uid must be greater than 0")
}
if starID <= 0 {
return 0, errors.New("star_id must be greater than 0")
}
var count int64
if err := r.db.Model(&models.AssetRegistry{}).
Joins("JOIN assets ON assets.id = asset_registry.asset_id AND assets.deleted_at IS NULL").
Where("asset_registry.owner_uid = ? AND asset_registry.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{}).
Joins("JOIN assets ON assets.id = asset_registry.asset_id AND assets.deleted_at IS NULL").
Where("asset_registry.owner_uid = ? AND asset_registry.star_id = ? AND asset_registry.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{}).
Joins("JOIN assets ON assets.id = asset_registry.asset_id AND assets.deleted_at IS NULL").
Where("asset_registry.owner_uid = ? AND asset_registry.star_id = ? AND asset_registry.asset_type = ? AND asset_registry.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{}).
Joins("JOIN assets ON assets.id = asset_registry.asset_id AND assets.deleted_at IS NULL").
Where("asset_registry.owner_uid = ? AND asset_registry.star_id = ? AND asset_registry.asset_type = ? AND asset_registry.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{}).
Joins("JOIN assets ON assets.id = asset_registry.asset_id AND assets.deleted_at IS NULL").
Where("asset_registry.owner_uid = ? AND asset_registry.star_id = ? AND asset_registry.asset_type = ? AND asset_registry.activity_id = ?", ownerUID, starID, assetType, activityID).
Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
// UpdateLikeCount 更新点赞数
func (r *assetRegistryRepository) UpdateLikeCount(assetID int64, likeCount int32) error {
if assetID <= 0 {
return errors.New("asset_id must be greater than 0")
}
return r.db.Model(&models.AssetRegistry{}).
Where("asset_id = ?", assetID).
Update("like_count", likeCount).Error
}
// UpdateGrade 更新等级
func (r *assetRegistryRepository) UpdateGrade(assetID int64, grade int32) error {
if assetID <= 0 {
return errors.New("asset_id must be greater than 0")
}
return r.db.Model(&models.AssetRegistry{}).
Where("asset_id = ?", assetID).
Update("grade", grade).Error
}
// Delete 删除索引记录
func (r *assetRegistryRepository) Delete(assetID int64) error {
if assetID <= 0 {
return errors.New("asset_id must be greater than 0")
}
return r.db.Where("asset_id = ?", assetID).Delete(&models.AssetRegistry{}).Error
}
// DeleteByAssetType 删除指定类型的索引记录
func (r *assetRegistryRepository) DeleteByAssetType(assetType string, assetID int64) error {
if assetType == "" {
return errors.New("asset_type must not be empty")
}
if assetID <= 0 {
return errors.New("asset_id must be greater than 0")
}
return r.db.Where("asset_type = ? AND asset_id = ?", assetType, assetID).
Delete(&models.AssetRegistry{}).Error
}

View File

@ -25,6 +25,9 @@ type AssetService interface {
// GetMyAssets 获取我的藏品列表
GetMyAssets(req *pb.GetMyAssetsRequest, userID, starID int64) (*pb.GetMyAssetsResponse, error)
// GetAssetsByType 按类型/分类/等级过滤查询星册藏品starbook 分类页)
GetAssetsByType(req *pb.GetAssetsByTypeRequest, userID, starID int64) (*pb.GetAssetsByTypeResponse, error)
// GetAsset 获取资产详情
GetAsset(req *pb.GetAssetRequest, userID, starID int64) (*pb.GetAssetResponse, error)
@ -219,6 +222,152 @@ func (s *assetService) GetMyAssets(req *pb.GetMyAssetsRequest, userID, starID in
return response, nil
}
// GetAssetsByType 按类型/分类/等级过滤查询星册藏品。
//
// 复用 GetMyAssets 的分组骨架(注册表 + 资产联合分组),在此基础上叠加请求侧过滤:
// - req.Type 非空时只保留该类型(regular/collection/activity)
// - req.Grade >0 时按 registry.grade 过滤(仅 regular 类型有效,其它类型 grade 恒为 nil 会被过滤空)
// - req.Category 非空时按逻辑分类过滤:
// * regular -> 恒为 "castlove"
// * collection -> registry.collection_category
// * activity -> registry.activity_type
//
// 返回结构与 GetMyAssets 一致(AssetListData 分组),便于 gateway 复用旧 starbook 契约。
func (s *assetService) GetAssetsByType(req *pb.GetAssetsByTypeRequest, userID, starID int64) (*pb.GetAssetsByTypeResponse, error) {
// 1. 参数验证(身份由 provider 从 ctx 注入,这里做防御性校验)
if !validator.ValidateUserID(userID) {
logger.Logger.Warn("GetAssetsByType invalid user_id", zap.Int64("user_id", userID))
return nil, appErrors.ErrInvalidUserID
}
if !validator.ValidateStarID(starID) {
logger.Logger.Warn("GetAssetsByType invalid star_id", zap.Int64("star_id", starID))
return nil, appErrors.ErrInvalidStarID
}
page := req.Page
if page <= 0 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
if pageSize > 100 {
pageSize = 100
}
// 2. 查询注册记录 + 资产列表
var registries []*models.AssetRegistry
if s.registryRepo != nil {
regs, err := s.registryRepo.GetByOwner(userID, starID)
if err != nil {
logger.Logger.Error("GetAssetsByType failed to get registries",
zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Error(err))
registries = []*models.AssetRegistry{}
} else {
registries = regs
}
} else {
registries = []*models.AssetRegistry{}
}
allAssets, err := s.assetRepo.GetByOwner(userID, starID, 1000, 0)
if err != nil {
logger.Logger.Error("GetAssetsByType failed to get assets",
zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Error(err))
return nil, fmt.Errorf("failed to get assets: %w", err)
}
registryMap := make(map[int64]*models.AssetRegistry)
for _, reg := range registries {
registryMap[reg.AssetID] = reg
}
// 3. 请求侧过滤
filtered := make([]*models.AssetRegistry, 0, len(registries))
for _, reg := range registries {
if req.Type != "" && reg.AssetType != req.Type {
continue
}
if req.Grade > 0 {
// grade 仅 regular 类型有效;非 regular 的 reg.Grade 为 nil,直接排除
if reg.Grade == nil || *reg.Grade != req.Grade {
continue
}
}
if req.Category != "" && registryLogicalCategory(reg) != req.Category {
continue
}
filtered = append(filtered, reg)
}
// 4. 按 type 分组并复用既有分组构建器
typeGroups := make(map[string][]*models.AssetRegistry)
for _, reg := range filtered {
typeGroups[reg.AssetType] = append(typeGroups[reg.AssetType], reg)
}
groups := make([]*pb.AssetGroup, 0)
if regs, ok := typeGroups[models.AssetTypeRegular]; ok {
if group := s.buildRegularGroupForAssets(allAssets, registryMap, regs); group != nil {
groups = append(groups, group)
}
}
if regs, ok := typeGroups[models.AssetTypeCollection]; ok {
if group := s.buildCollectionGroupForAssets(allAssets, registryMap, regs); group != nil {
groups = append(groups, group)
}
}
if regs, ok := typeGroups[models.AssetTypeActivity]; ok {
if group := s.buildActivityGroupForAssets(allAssets, registryMap, regs); group != nil {
groups = append(groups, group)
}
}
total := int64(len(filtered))
response := &pb.GetAssetsByTypeResponse{
Base: &pbCommon.BaseResponse{
Code: uint32(codes.OK),
Message: "",
Timestamp: time.Now().UnixMilli(),
},
Data: &pb.AssetListData{
Groups: groups,
Total: total,
Page: page,
PageSize: pageSize,
HasMore: false, // 分组返回全部,不做二次分页(与 GetMyAssets 一致)
},
}
logger.Logger.Debug("GetAssetsByType successful",
zap.Int64("user_id", userID), zap.Int64("star_id", starID),
zap.String("type", req.Type), zap.String("category", req.Category),
zap.Int32("grade", req.Grade), zap.Int64("total", total))
return response, nil
}
// registryLogicalCategory 返回 registry 对应的逻辑分类值,与 AssetItem.Category 的填充口径一致:
// - regular -> "castlove"(原创统一归类)
// - collection -> collection_category
// - activity -> activity_type
func registryLogicalCategory(reg *models.AssetRegistry) string {
switch reg.AssetType {
case models.AssetTypeRegular:
return "castlove"
case models.AssetTypeCollection:
if reg.CollectionCategory != nil {
return *reg.CollectionCategory
}
case models.AssetTypeActivity:
if reg.ActivityType != nil {
return *reg.ActivityType
}
}
return ""
}
// buildRegularGroupForAssets 构建原创藏品分组
func (s *assetService) buildRegularGroupForAssets(allAssets []*models.Asset, registryMap map[int64]*models.AssetRegistry, registries []*models.AssetRegistry) *pb.AssetGroup {
// 创建 assetID -> asset 映射

View File

@ -27,7 +27,6 @@ import (
"github.com/topfans/backend/services/assetService/config"
"github.com/topfans/backend/services/assetService/repository"
"github.com/topfans/backend/services/assetService/util"
starbookRepo "github.com/topfans/backend/services/starbookService/repository"
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/clause"
@ -76,7 +75,7 @@ type mintService struct {
userClient client.UserServiceClient
db *gorm.DB
config *config.AssetConfig
registryRepo starbookRepo.AssetRegistryRepository // 资产索引仓库(用于星册体系)
registryRepo repository.AssetRegistryRepository // 资产索引仓库(原 starbookService/repository,批次 4.2 已下沉)
localMintCostRepo repository.MintCostRepository // 铸造消耗配置仓库
userMintCountRepo repository.UserMintCountRepository // 用户铸爱累计仓库
assetLevelService AssetLevelService // 资产等级服务
@ -89,7 +88,7 @@ func NewMintService(
userClient client.UserServiceClient,
db *gorm.DB,
cfg *config.AssetConfig,
registryRepo starbookRepo.AssetRegistryRepository,
registryRepo repository.AssetRegistryRepository,
localMintCostRepo repository.MintCostRepository,
userMintCountRepo repository.UserMintCountRepository,
assetLevelService AssetLevelService,

View File

@ -42,7 +42,7 @@ var (
dbName = flag.String("db-name", getEnv("DB_NAME", "top-fans"), "Database name")
assetServiceURL = flag.String("asset-service-url", getEnv("ASSET_SERVICE_URL", "tri://localhost:20003"), "Asset service URL")
userServiceURL = flag.String("user-service-url", getEnv("USER_SERVICE_URL", "tri://localhost:20000"), "User service URL")
taskServiceURL = flag.String("task-service-url", getEnv("TASK_SERVICE_URL", "tri://localhost:20002"), "Task service URL")
taskServiceURL = flag.String("task-service-url", getEnv("TASK_SERVICE_URL", "tri://localhost:20006"), "Task service URL")
mqRedisAddr = flag.String("mq-redis-addr", getEnv("MQ_REDIS_ADDR", "localhost:6379"), "MQ redis address")
mqRedisDB = flag.Int("mq-redis-db", getEnvInt("MQ_REDIS_DB", 2), "MQ redis db (avoid clashing with app cache db)")
mqRedisPassword = flag.String("mq-redis-password", getEnv("MQ_REDIS_PASSWORD", getEnv("REDIS_PASSWORD", "")), "MQ redis password")

View File

@ -278,7 +278,7 @@ func initDubboService() error {
// 创建 Dubbo Server
srv, err := server.NewServer(
server.WithServerProtocol(
protocol.WithPort(20000),
protocol.WithPort(*port),
protocol.WithTriple(),
),
)
@ -293,7 +293,7 @@ func initDubboService() error {
logger.Sugar.Info("Dubbo-go unified provider registered successfully",
"service", "topfans.user.UserSocialService",
"port", 20000,
"port", *port,
)
// 在后台启动 Dubbo 服务器

View File

@ -4,7 +4,9 @@ import (
"context"
"errors"
"fmt"
"net/url"
"strconv"
"strings"
"time"
eventPb "github.com/topfans/backend/pkg/proto/event"
@ -365,7 +367,7 @@ func (s *userService) CheckMobile(req *pb.CheckMobileRequest) (*pb.CheckMobileRe
// 2. 验证手机号格式
if !validator.ValidateMobile(req.Mobile) {
logger.Logger.Warn("Invalid mobile format",
zap.String("mobile", req.Mobile),
zap.String("mobile", maskMobile(req.Mobile)),
)
return &pb.CheckMobileResponse{
Base: appErrors.BuildBaseResponse(appErrors.ErrInvalidMobile),
@ -376,7 +378,7 @@ func (s *userService) CheckMobile(req *pb.CheckMobileRequest) (*pb.CheckMobileRe
exists, err := s.userRepo.ExistsByMobile(req.Mobile)
if err != nil {
logger.Logger.Error("Failed to check mobile",
zap.String("mobile", req.Mobile),
zap.String("mobile", maskMobile(req.Mobile)),
zap.Error(err),
)
return &pb.CheckMobileResponse{
@ -385,7 +387,7 @@ func (s *userService) CheckMobile(req *pb.CheckMobileRequest) (*pb.CheckMobileRe
}
logger.Logger.Info("Check mobile result",
zap.String("mobile", req.Mobile),
zap.String("mobile", maskMobile(req.Mobile)),
zap.Bool("exists", exists),
)
@ -1053,6 +1055,20 @@ func (s *userService) UpdateAvatar(req *pb.UpdateAvatarRequest, userID, starID i
return nil, fmt.Errorf("avatar_url too long, max length is 500")
}
// 2.1 同源校验(防 SSRF / 头像被指向任意公网 URL
// 仅允许 *.aliyuncs.com 子域,与 OSS 公网读域名约定。
// 与 gateway/controller/user_controller.go 的 OSS key 提取是两道独立检查:
// 这里兜底防止 service 被绕过;gateway 是公共入口。同源白名单比黑名单更稳。
if u, parseErr := url.Parse(req.AvatarUrl); parseErr == nil && u.Host != "" {
if !strings.Contains(u.Host, "aliyuncs.com") {
logger.Logger.Warn("Avatar URL not from aliyuncs.com",
zap.Int64("user_id", userID),
zap.String("host", u.Host),
)
return nil, appErrors.ErrInvalidAvatarURL
}
}
// 3. 更新头像
if err := s.fanProfileRepo.UpdateAvatar(userID, starID, req.AvatarUrl); err != nil {
if errors.Is(err, appErrors.ErrFanProfileNotFound) {