package main import ( "context" "log" "net/http" "time" "github.com/gin-gonic/gin" "kefu-cloud/server/internal/config" "kefu-cloud/server/internal/handler" "kefu-cloud/server/internal/metrics" "kefu-cloud/server/internal/middleware" "kefu-cloud/server/internal/model" "kefu-cloud/server/internal/storage" "kefu-cloud/server/internal/ws" ) func main() { cfg := config.Load() // 初始化数据库 model.InitDB(cfg.Database.DSN()) // 初始化 JWT(有效期可用 JWT_EXPIRE_HOURS 配置,默认 24 小时) middleware.InitJWT(cfg.JWT.Secret, cfg.JWT.ExpireTime) // 对象存储(MinIO / S3 兼容) store, err := storage.NewS3(cfg.Storage) if err != nil { log.Fatalf("对象存储初始化失败: %v", err) } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := store.EnsureBucket(ctx); err != nil { log.Printf("警告: 确保存储桶失败(可稍后启动 MinIO): %v", err) } else { log.Printf("对象存储就绪: %s bucket=%s public=%s", cfg.Storage.Endpoint, cfg.Storage.Bucket, cfg.Storage.PublicBase) } // 启动 WebSocket Hub ws.SetAllowedOrigins(cfg.AllowedOrigins) // 多 API 副本部署时通过 REDIS_ADDR 启用跨节点广播;单实例保持进程内投递 if cfg.Redis.Addr != "" { bus := ws.NewRedisBus(cfg.Redis.Addr, cfg.Redis.Password, cfg.Redis.DB) ws.DefaultHub.SetEventBus(bus) log.Printf("WebSocket 跨节点广播已启用: redis=%s db=%d", cfg.Redis.Addr, cfg.Redis.DB) defer func() { _ = bus.Close() }() } go ws.DefaultHub.Run() // 设置 Gin 模式 gin.SetMode(cfg.Server.Mode) r := gin.Default() // 全站请求统计(运维监控 24h 曲线) r.Use(metrics.Middleware()) // CORS r.Use(func(c *gin.Context) { origin := c.GetHeader("Origin") allowed := false for _, candidate := range cfg.AllowedOrigins { if candidate == "*" || candidate == origin { allowed = true break } } if allowed && origin != "" { c.Header("Access-Control-Allow-Origin", origin) c.Header("Vary", "Origin") } c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") c.Header("Access-Control-Allow-Headers", "Content-Type,Authorization,X-Visitor-Token") if c.Request.Method == "OPTIONS" { if origin != "" && !allowed { c.AbortWithStatus(http.StatusForbidden) return } c.AbortWithStatus(204) return } c.Next() }) // 健康检查 r.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok"}) }) // 注册路由 handler.SetupRoutes(r, store, cfg.Storage) log.Printf("客服云服务启动在 :%s", cfg.Server.Port) if err := r.Run(":" + cfg.Server.Port); err != nil { log.Fatalf("启动失败: %v", err) } }