topfans/backend/gateway/main.go
zerosaturation 8a767fb400 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>
2026-07-24 14:03:21 +08:00

284 lines
9.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 HubWebSocket 实时推送)
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")
}