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:
parent
a337f43f86
commit
8a767fb400
@ -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 status(Dubbo-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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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 Hub(WebSocket 实时推送)
|
||||
// 4.12 初始化 Activity Hub(WebSocket 实时推送)
|
||||
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))
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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{}
|
||||
)
|
||||
|
||||
@ -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。
|
||||
@ -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{}
|
||||
)
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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{
|
||||
|
||||
@ -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) {
|
||||
// 记录请求日志
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
232
backend/services/assetService/provider/asset_provider_test.go
Normal file
232
backend/services/assetService/provider/asset_provider_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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(®istry).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, appErrors.ErrAssetRegistryNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return ®istry, nil
|
||||
}
|
||||
|
||||
// GetByAssetID 根据asset_id查询
|
||||
func (r *assetRegistryRepository) GetByAssetID(assetID int64) (*models.AssetRegistry, error) {
|
||||
if assetID <= 0 {
|
||||
return nil, errors.New("asset_id must be greater than 0")
|
||||
}
|
||||
var registry models.AssetRegistry
|
||||
if err := r.db.Where("asset_id = ?", assetID).First(®istry).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, appErrors.ErrAssetRegistryNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return ®istry, nil
|
||||
}
|
||||
|
||||
// GetByAssetTypeAndID 根据类型和asset_id查询
|
||||
func (r *assetRegistryRepository) GetByAssetTypeAndID(assetType string, assetID int64) (*models.AssetRegistry, error) {
|
||||
if assetType == "" {
|
||||
return nil, errors.New("asset_type must not be empty")
|
||||
}
|
||||
if assetID <= 0 {
|
||||
return nil, errors.New("asset_id must be greater than 0")
|
||||
}
|
||||
var registry models.AssetRegistry
|
||||
if err := r.db.Where("asset_type = ? AND asset_id = ?", assetType, assetID).First(®istry).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, appErrors.ErrAssetRegistryNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return ®istry, nil
|
||||
}
|
||||
|
||||
// GetByOwner 查询用户的所有索引记录
|
||||
func (r *assetRegistryRepository) GetByOwner(ownerUID, starID int64) ([]*models.AssetRegistry, error) {
|
||||
if ownerUID <= 0 {
|
||||
return nil, errors.New("owner_uid must be greater than 0")
|
||||
}
|
||||
if starID <= 0 {
|
||||
return nil, errors.New("star_id must be greater than 0")
|
||||
}
|
||||
var registries []*models.AssetRegistry
|
||||
if err := r.db.
|
||||
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(®istries).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return registries, nil
|
||||
}
|
||||
|
||||
// GetByOwnerAndType 查询用户指定类型的索引记录
|
||||
func (r *assetRegistryRepository) GetByOwnerAndType(ownerUID, starID int64, assetType string, limit, offset int) ([]*models.AssetRegistry, error) {
|
||||
if ownerUID <= 0 {
|
||||
return nil, errors.New("owner_uid must be greater than 0")
|
||||
}
|
||||
if starID <= 0 {
|
||||
return nil, errors.New("star_id must be greater than 0")
|
||||
}
|
||||
var registries []*models.AssetRegistry
|
||||
query := r.db.
|
||||
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(®istries).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return registries, nil
|
||||
}
|
||||
|
||||
// GetByOwnerAndTypeAndGrade 查询用户指定类型和等级的索引记录
|
||||
func (r *assetRegistryRepository) GetByOwnerAndTypeAndGrade(ownerUID, starID int64, assetType string, grade int32, limit, offset int) ([]*models.AssetRegistry, error) {
|
||||
if ownerUID <= 0 {
|
||||
return nil, errors.New("owner_uid must be greater than 0")
|
||||
}
|
||||
if starID <= 0 {
|
||||
return nil, errors.New("star_id must be greater than 0")
|
||||
}
|
||||
var registries []*models.AssetRegistry
|
||||
query := r.db.
|
||||
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(®istries).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return registries, nil
|
||||
}
|
||||
|
||||
// GetByOwnerAndTypeAndCategory 查询用户指定类型和分类的索引记录
|
||||
func (r *assetRegistryRepository) GetByOwnerAndTypeAndCategory(ownerUID, starID int64, assetType string, category string, limit, offset int) ([]*models.AssetRegistry, error) {
|
||||
if ownerUID <= 0 {
|
||||
return nil, errors.New("owner_uid must be greater than 0")
|
||||
}
|
||||
if starID <= 0 {
|
||||
return nil, errors.New("star_id must be greater than 0")
|
||||
}
|
||||
var registries []*models.AssetRegistry
|
||||
query := r.db.
|
||||
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(®istries).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return registries, nil
|
||||
}
|
||||
|
||||
// GetByOwnerAndTypeAndActivity 查询用户指定类型和活动的索引记录
|
||||
func (r *assetRegistryRepository) GetByOwnerAndTypeAndActivity(ownerUID, starID int64, assetType string, activityID int64, limit, offset int) ([]*models.AssetRegistry, error) {
|
||||
if ownerUID <= 0 {
|
||||
return nil, errors.New("owner_uid must be greater than 0")
|
||||
}
|
||||
if starID <= 0 {
|
||||
return nil, errors.New("star_id must be greater than 0")
|
||||
}
|
||||
var registries []*models.AssetRegistry
|
||||
query := r.db.
|
||||
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(®istries).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return registries, nil
|
||||
}
|
||||
|
||||
// CountByOwner 统计用户的索引记录数量
|
||||
func (r *assetRegistryRepository) CountByOwner(ownerUID, starID int64) (int64, error) {
|
||||
if ownerUID <= 0 {
|
||||
return 0, errors.New("owner_uid must be greater than 0")
|
||||
}
|
||||
if starID <= 0 {
|
||||
return 0, errors.New("star_id must be greater than 0")
|
||||
}
|
||||
var count int64
|
||||
if err := r.db.Model(&models.AssetRegistry{}).
|
||||
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
|
||||
}
|
||||
@ -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 映射
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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 服务器
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user