- 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>
284 lines
9.8 KiB
Go
284 lines
9.8 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"os"
|
||
"os/signal"
|
||
"syscall"
|
||
|
||
"dubbo.apache.org/dubbo-go/v3/client"
|
||
_ "dubbo.apache.org/dubbo-go/v3/imports"
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/joho/godotenv"
|
||
"github.com/topfans/backend/gateway/config"
|
||
"github.com/topfans/backend/gateway/pkg/starcache"
|
||
"github.com/topfans/backend/gateway/router"
|
||
"github.com/topfans/backend/pkg/database"
|
||
"github.com/topfans/backend/pkg/jwt"
|
||
"github.com/topfans/backend/pkg/logger"
|
||
pb "github.com/topfans/backend/pkg/proto/user"
|
||
"go.uber.org/zap"
|
||
|
||
docs "github.com/topfans/backend/gateway/docs"
|
||
"github.com/topfans/backend/gateway/socket"
|
||
pbModeration "github.com/topfans/backend/pkg/proto/moderation"
|
||
)
|
||
|
||
// @title TopFans API
|
||
// @version 1.0
|
||
// @description TopFans 后端 API 文档 - 微服务架构 Gateway 层
|
||
// @host localhost:8080
|
||
// @basePath /api/v1
|
||
// @securityDefinitions.apikey BearerAuth
|
||
// @in header
|
||
// @name Authorization
|
||
// @schemes http
|
||
func main() {
|
||
// 加载 backend/.env(与 assetService 等保持一致;不覆盖已设置的系统环境变量)
|
||
_ = godotenv.Load("../.env", ".env")
|
||
|
||
// 1. 初始化日志
|
||
if err := logger.Init(logger.Config{
|
||
ServiceName: "gateway",
|
||
Environment: "development",
|
||
LogLevel: "info",
|
||
LogPath: "logs",
|
||
}); err != nil {
|
||
fmt.Printf("Failed to initialize logger: %v\n", err)
|
||
os.Exit(1)
|
||
}
|
||
defer logger.Logger.Sync()
|
||
|
||
logger.Logger.Info("Starting Top-Fans Gateway...")
|
||
|
||
// 2. 初始化 JWT secret (必须在任何 JWT 操作前)
|
||
// fail-fast:空 / 短 / 默认值 三种弱 key 任一命中即 Fatal,绝不静默降级。
|
||
jwtSecret := os.Getenv("JWT_SECRET")
|
||
if err := jwt.MustInit(jwtSecret); err != nil {
|
||
logger.Logger.Fatal("FATAL: JWT secret init failed", zap.Error(err))
|
||
}
|
||
logger.Logger.Info("JWT secret loaded", zap.Int("bytes", len(jwtSecret)))
|
||
|
||
// 2.5 加载配置
|
||
cfg := config.Load()
|
||
if err := cfg.Validate(); err != nil {
|
||
logger.Logger.Fatal("Invalid configuration", zap.Error(err))
|
||
}
|
||
|
||
// 覆盖 Swagger 的 BasePath,避免出现 /api/v1/api/v1 前綴重複
|
||
// Swagger 內的 paths 已經包含 /api/v1/...,這裡設為空即可
|
||
docs.SwaggerInfo.BasePath = ""
|
||
|
||
logger.Logger.Info("Configuration loaded",
|
||
zap.String("port", cfg.Server.Port),
|
||
zap.String("mode", cfg.Server.Mode),
|
||
zap.String("user_service_url", cfg.Dubbo.UserServiceURL),
|
||
zap.String("social_service_url", cfg.Dubbo.SocialServiceURL),
|
||
zap.String("asset_service_url", cfg.Dubbo.AssetServiceURL),
|
||
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("notification_service_url", cfg.Dubbo.NotificationServiceURL),
|
||
)
|
||
|
||
// 3. 设置 Gin 模式
|
||
gin.SetMode(cfg.Server.Mode)
|
||
|
||
// 3.5 初始化 Redis(连接失败仅警告,不阻塞启动)
|
||
logger.Logger.Info("Connecting to Redis...")
|
||
if err := database.InitRedis(database.RedisConfig{
|
||
Host: cfg.Redis.Host,
|
||
Port: cfg.Redis.Port,
|
||
Password: cfg.Redis.Password,
|
||
DB: cfg.Redis.DB,
|
||
}); err != nil {
|
||
logger.Logger.Warn("Redis not available, continuing without Redis", zap.Error(err))
|
||
} else {
|
||
logger.Logger.Info("Redis connected successfully")
|
||
defer database.CloseRedis()
|
||
}
|
||
|
||
// 3.6 初始化数据库(Gateway 本地持久化镭射卡实例)
|
||
logger.Logger.Info("Connecting to database...")
|
||
if err := database.Init(database.Config{
|
||
Host: cfg.DB.Host,
|
||
Port: cfg.DB.Port,
|
||
User: cfg.DB.User,
|
||
Password: cfg.DB.Password,
|
||
DBName: cfg.DB.DBName,
|
||
SSLMode: cfg.DB.SSLMode,
|
||
TimeZone: cfg.DB.TimeZone,
|
||
}); err != nil {
|
||
logger.Logger.Fatal("Failed to connect to database", zap.Error(err))
|
||
}
|
||
logger.Logger.Info("Database connected successfully")
|
||
|
||
// 4. 初始化 Dubbo Clients
|
||
logger.Logger.Info("Connecting to Dubbo services...")
|
||
|
||
// 4.1 UserService Client
|
||
userClient, err := client.NewClient(
|
||
client.WithClientURL(cfg.Dubbo.UserServiceURL),
|
||
)
|
||
if err != nil {
|
||
logger.Logger.Fatal("Failed to create User Service Dubbo client", zap.Error(err))
|
||
}
|
||
logger.Logger.Info("User Service Dubbo client connected successfully")
|
||
|
||
// 4.2 SocialService Client
|
||
socialClient, err := client.NewClient(
|
||
client.WithClientURL(cfg.Dubbo.SocialServiceURL),
|
||
)
|
||
if err != nil {
|
||
logger.Logger.Fatal("Failed to create Social Service Dubbo client", zap.Error(err))
|
||
}
|
||
logger.Logger.Info("Social Service Dubbo client connected successfully")
|
||
|
||
// 4.3 AssetService Client
|
||
assetClient, err := client.NewClient(
|
||
client.WithClientURL(cfg.Dubbo.AssetServiceURL),
|
||
)
|
||
if err != nil {
|
||
logger.Logger.Fatal("Failed to create Asset Service Dubbo client", zap.Error(err))
|
||
}
|
||
logger.Logger.Info("Asset Service Dubbo client connected successfully")
|
||
|
||
// 4.4 GalleryService Client
|
||
galleryClient, err := client.NewClient(
|
||
client.WithClientURL(cfg.Dubbo.GalleryServiceURL),
|
||
)
|
||
if err != nil {
|
||
logger.Logger.Fatal("Failed to create Gallery Service Dubbo client", zap.Error(err))
|
||
}
|
||
logger.Logger.Info("Gallery Service Dubbo client connected successfully")
|
||
|
||
// 4.5 ActivityService Client
|
||
activityClient, err := client.NewClient(
|
||
client.WithClientURL(cfg.Dubbo.ActivityServiceURL),
|
||
)
|
||
if err != nil {
|
||
logger.Logger.Fatal("Failed to create Activity Service Dubbo client", zap.Error(err))
|
||
}
|
||
logger.Logger.Info("Activity Service Dubbo client connected successfully")
|
||
|
||
// 4.6 TaskService Client
|
||
taskClient, err := client.NewClient(
|
||
client.WithClientURL(cfg.Dubbo.TaskServiceURL),
|
||
)
|
||
if err != nil {
|
||
logger.Logger.Fatal("Failed to create Task Service Dubbo client", zap.Error(err))
|
||
}
|
||
logger.Logger.Info("Task Service Dubbo client connected successfully")
|
||
|
||
// 4.7 AIChatService Client (原 4.7 StarbookService Client 已删除 - 批次 4.2)
|
||
aiChatClient, err := client.NewClient(
|
||
client.WithClientURL(cfg.Dubbo.AIChatServiceURL),
|
||
)
|
||
if err != nil {
|
||
logger.Logger.Fatal("Failed to create AI Chat Service Dubbo client", zap.Error(err))
|
||
}
|
||
logger.Logger.Info("AI Chat Service Dubbo client connected successfully")
|
||
|
||
// 4.8 StatisticService Client
|
||
statisticClient, err := client.NewClient(
|
||
client.WithClientURL(cfg.Dubbo.StatisticServiceURL),
|
||
)
|
||
if err != nil {
|
||
logger.Logger.Fatal("Failed to create Statistic Service Dubbo client", zap.Error(err))
|
||
}
|
||
logger.Logger.Info("Statistic Service Dubbo client connected successfully")
|
||
|
||
// 4.9 NotificationService Client
|
||
notificationClient, err := client.NewClient(
|
||
client.WithClientURL(cfg.Dubbo.NotificationServiceURL),
|
||
)
|
||
if err != nil {
|
||
logger.Logger.Fatal("Failed to create Notification Service Dubbo client", zap.Error(err))
|
||
}
|
||
logger.Logger.Info("Notification Service Dubbo client connected successfully")
|
||
|
||
// 4.10 ModerationService Client (举报反馈,端口 20011)
|
||
moderationClient, err := client.NewClient(
|
||
client.WithClientURL(cfg.Dubbo.ModerationServiceURL),
|
||
)
|
||
if err != nil {
|
||
logger.Logger.Fatal("Failed to create Moderation Service Dubbo client", zap.Error(err))
|
||
}
|
||
logger.Logger.Info("Moderation Service Dubbo client connected successfully",
|
||
zap.String("url", cfg.Dubbo.ModerationServiceURL))
|
||
|
||
// 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.12 初始化 Activity Hub(WebSocket 实时推送)
|
||
redisClient := database.GetRedis()
|
||
activityHub := socket.NewActivityHub(redisClient, cfg.WebSocket.ActivityPath)
|
||
hubCtx, hubCancel := context.WithCancel(context.Background())
|
||
go activityHub.Run(hubCtx)
|
||
defer func() {
|
||
hubCancel()
|
||
activityHub.Close()
|
||
}()
|
||
logger.Logger.Info("ActivityHub initialized",
|
||
zap.String("path", cfg.WebSocket.ActivityPath),
|
||
zap.Bool("redis_available", redisClient != nil),
|
||
)
|
||
|
||
// 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)
|
||
if err != nil {
|
||
logger.Logger.Fatal("Failed to create UserSocialService for star cache", zap.Error(err))
|
||
}
|
||
starCache := starcache.New(userSvcForStarCache)
|
||
|
||
// 5. 设置路由
|
||
logger.Logger.Info("Setting up routes...")
|
||
// ★ 批次 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))
|
||
}
|
||
logger.Logger.Info("Routes configured successfully")
|
||
|
||
// 5.1 启动铸造异步 consumer(mintSuccessCh 消费者)
|
||
// 服务稳定性 plan §3.8:把 gateway 内 laser_card 镜像写从铸造主路径上异步拆出来。
|
||
// ctx 与 hub 共享生命周期 — gateway 整体停服时这两个 goroutine 同时退出。
|
||
mintCtx, mintCancel := context.WithCancel(context.Background())
|
||
defer mintCancel()
|
||
if router.AssetController != nil {
|
||
router.AssetController.RunMintSuccessConsumer(mintCtx)
|
||
logger.Logger.Info("MintSuccessConsumer started in background")
|
||
}
|
||
|
||
// 6. 启动服务器
|
||
addr := ":" + cfg.Server.Port
|
||
logger.Logger.Info("Starting HTTP server", zap.String("address", addr))
|
||
|
||
// 优雅关闭
|
||
go func() {
|
||
if err := r.Run(addr); err != nil {
|
||
logger.Logger.Fatal("Failed to start server", zap.Error(err))
|
||
}
|
||
}()
|
||
|
||
logger.Logger.Info("Gateway server started successfully",
|
||
zap.String("address", fmt.Sprintf("http://localhost%s", addr)),
|
||
)
|
||
|
||
// 7. 等待中断信号
|
||
quit := make(chan os.Signal, 1)
|
||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||
<-quit
|
||
|
||
logger.Logger.Info("Shutting down gateway server...")
|
||
hubCancel()
|
||
activityHub.Close()
|
||
logger.Logger.Info("Gateway server stopped")
|
||
}
|