328 lines
9.1 KiB
Go
328 lines
9.1 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"flag"
|
||
"fmt"
|
||
"net/http"
|
||
"os"
|
||
"os/signal"
|
||
"strings"
|
||
"syscall"
|
||
"time"
|
||
|
||
"ezops/internal/api/routes"
|
||
"ezops/internal/config"
|
||
"ezops/pkg/database"
|
||
"ezops/pkg/logger"
|
||
ezredis "ezops/pkg/redis"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/spf13/viper"
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
// 版本信息(通过编译时 ldflags 注入)// Logan.Li
|
||
var (
|
||
Version = "dev"
|
||
BuildTime = "unknown"
|
||
GitCommit = "unknown"
|
||
)
|
||
|
||
// CLI 参数定义 // Logan.Li
|
||
var (
|
||
flagPort = flag.String("port", "", "服务端口 (默认: 8080)")
|
||
flagConfig = flag.String("config", "", "配置文件路径 (默认: config.yaml)")
|
||
flagMode = flag.String("mode", "", "运行模式: development, production, release")
|
||
flagLogLevel = flag.String("log-level", "", "日志级别: debug, info, warn, error")
|
||
|
||
// 数据库参数
|
||
flagDBDriver = flag.String("db-driver", "", "数据库驱动: sqlite, mysql, postgres (默认: sqlite)")
|
||
flagDBHost = flag.String("db-host", "", "数据库主机")
|
||
flagDBPort = flag.Int("db-port", 0, "数据库端口")
|
||
flagDBUser = flag.String("db-user", "", "数据库用户名")
|
||
flagDBPassword = flag.String("db-password", "", "数据库密码")
|
||
flagDBName = flag.String("db-name", "", "数据库名 / SQLite 文件路径")
|
||
flagDBCharset = flag.String("db-charset", "", "数据库字符集")
|
||
|
||
// Redis 参数
|
||
flagRedisHost = flag.String("redis-host", "", "Redis 主机")
|
||
flagRedisPort = flag.Int("redis-port", 0, "Redis 端口")
|
||
flagRedisPassword = flag.String("redis-password", "", "Redis 密码")
|
||
flagRedisDB = flag.Int("redis-db", 0, "Redis 数据库编号")
|
||
flagRedisDriver = flag.String("redis-driver", "", "Redis 驱动: redis (外部Redis), redka (内嵌,需CGO)")
|
||
|
||
// JWT 参数
|
||
flagJWTSecret = flag.String("jwt-secret", "", "JWT 密钥")
|
||
flagJWTExpire = flag.Int("jwt-expire", 0, "JWT 过期时间 (小时)")
|
||
|
||
// 显示版本
|
||
flagVersion = flag.Bool("version", false, "显示版本信息")
|
||
)
|
||
|
||
// @title AIOps运维平台 API
|
||
// @version 1.0
|
||
// @description AIOps智能运维平台后端API文档
|
||
// @termsOfService http://swagger.io/terms/
|
||
|
||
// @contact.name API Support
|
||
// @contact.url http://www.swagger.io/support
|
||
// @contact.email support@swagger.io
|
||
|
||
// @license.name Apache 2.0
|
||
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
||
|
||
// @host localhost:8080
|
||
// @BasePath /api/v1
|
||
|
||
// @securityDefinitions.apikey Bearer
|
||
// @in header
|
||
// @name Authorization
|
||
// @description Type "Bearer" followed by a space and JWT token.
|
||
|
||
func main() {
|
||
// 解析 CLI 参数 // Logan.Li
|
||
flag.Parse()
|
||
|
||
// 显示版本信息 // Logan.Li
|
||
if *flagVersion {
|
||
fmt.Printf("AIOps 智能运维平台\n")
|
||
fmt.Printf(" 版本: %s\n", Version)
|
||
fmt.Printf(" 构建: %s\n", BuildTime)
|
||
fmt.Printf(" 提交: %s\n", GitCommit)
|
||
os.Exit(0)
|
||
}
|
||
|
||
// 加载配置 // Logan.Li
|
||
cfg := loadConfig()
|
||
|
||
// 初始化日志
|
||
log := logger.New(cfg.LogLevel)
|
||
defer log.Sync()
|
||
|
||
log.Info("AIOps 启动中",
|
||
zap.String("version", Version),
|
||
zap.String("mode", cfg.Mode),
|
||
zap.String("port", cfg.Port),
|
||
)
|
||
|
||
// 设置Gin模式
|
||
if cfg.Mode == "production" || cfg.Mode == "release" {
|
||
gin.SetMode(gin.ReleaseMode)
|
||
}
|
||
|
||
// 初始化数据库
|
||
db, err := database.New(cfg.Database)
|
||
if err != nil {
|
||
log.Fatal("数据库连接失败", zap.Error(err))
|
||
}
|
||
|
||
// 自动迁移
|
||
if err := database.AutoMigrate(db); err != nil {
|
||
log.Fatal("数据库迁移失败", zap.Error(err))
|
||
}
|
||
|
||
// 初始化种子数据
|
||
seedData(db)
|
||
|
||
// 初始化缓存(Redka 内嵌 / Redis 外部)// Logan.Li
|
||
cache, err := ezredis.New(ezredis.Config{
|
||
Driver: cfg.Redis.Driver, // 从配置读取 driver
|
||
Host: cfg.Redis.Host,
|
||
Port: cfg.Redis.Port,
|
||
Password: cfg.Redis.Password,
|
||
DB: cfg.Redis.DB,
|
||
DataDir: "data/redka",
|
||
})
|
||
if err != nil {
|
||
log.Warn("缓存初始化失败,继续启动", zap.Error(err))
|
||
cache = nil
|
||
} else {
|
||
log.Info("缓存已就绪", zap.String("driver", cfg.Redis.Driver))
|
||
defer cache.Close()
|
||
}
|
||
|
||
// 创建Gin实例
|
||
r := gin.New()
|
||
|
||
// 中间件
|
||
r.Use(gin.Recovery())
|
||
r.Use(logger.GinLogger(log))
|
||
r.Use(CORS())
|
||
|
||
// 版本信息 API
|
||
r.GET("/version", func(c *gin.Context) {
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"version": Version,
|
||
"build_time": BuildTime,
|
||
"git_commit": GitCommit,
|
||
"mode": cfg.Mode,
|
||
"db_driver": cfg.Database.Driver,
|
||
})
|
||
})
|
||
|
||
// 注册路由
|
||
routes.Register(r, db, log, cfg, cache)
|
||
|
||
// 前端静态文件服务(go:embed 模式,二进制部署用) // Logan.Li
|
||
setupFrontend(r)
|
||
|
||
// 创建HTTP服务器
|
||
srv := &http.Server{
|
||
Addr: ":" + cfg.Port,
|
||
Handler: r,
|
||
}
|
||
|
||
// 优雅关闭
|
||
go func() {
|
||
log.Info("服务已启动", zap.String("addr", srv.Addr))
|
||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||
log.Fatal("服务启动失败", zap.Error(err))
|
||
}
|
||
}()
|
||
|
||
// 等待中断信号
|
||
quit := make(chan os.Signal, 1)
|
||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||
<-quit
|
||
|
||
log.Info("正在关闭服务...")
|
||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer cancel()
|
||
|
||
if err := srv.Shutdown(ctx); err != nil {
|
||
log.Fatal("强制关闭服务", zap.Error(err))
|
||
}
|
||
|
||
log.Info("服务已退出")
|
||
}
|
||
|
||
// loadConfig 加载配置(CLI flags 优先于 config.yaml)// Logan.Li
|
||
func loadConfig() *config.Config {
|
||
viper.SetConfigName("config")
|
||
viper.SetConfigType("yaml")
|
||
viper.AddConfigPath("./configs")
|
||
viper.AddConfigPath(".")
|
||
|
||
// 如果指定了配置文件路径,使用它 // Logan.Li
|
||
if *flagConfig != "" {
|
||
viper.SetConfigFile(*flagConfig)
|
||
}
|
||
|
||
// 默认值(独立二进制模式:SQLite + 无 Redis)// Logan.Li
|
||
viper.SetDefault("mode", "release")
|
||
viper.SetDefault("port", "8080")
|
||
viper.SetDefault("log_level", "info")
|
||
|
||
viper.SetDefault("database.driver", "sqlite")
|
||
viper.SetDefault("database.host", "localhost")
|
||
viper.SetDefault("database.port", 3306)
|
||
viper.SetDefault("database.user", "")
|
||
viper.SetDefault("database.password", "")
|
||
viper.SetDefault("database.dbname", "data/aiops.db")
|
||
viper.SetDefault("database.charset", "utf8mb4")
|
||
viper.SetDefault("database.max_conn", 100)
|
||
viper.SetDefault("database.max_idle", 10)
|
||
|
||
viper.SetDefault("redis.host", "localhost")
|
||
viper.SetDefault("redis.port", 6379)
|
||
viper.SetDefault("redis.driver", "redka")
|
||
viper.SetDefault("redis.password", "")
|
||
viper.SetDefault("redis.db", 0)
|
||
|
||
viper.SetDefault("jwt.secret", "") // 生产环境必须通过配置或 JWT_SECRET 环境变量设置
|
||
viper.SetDefault("jwt.expire_hour", 24)
|
||
|
||
viper.SetDefault("websocket.read_buffer_size", 1024)
|
||
viper.SetDefault("websocket.write_buffer_size", 1024)
|
||
|
||
// 读取配置文件(不存在也不报错)// Logan.Li
|
||
if err := viper.ReadInConfig(); err != nil {
|
||
// 配置文件不存在是正常的(独立二进制模式)
|
||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||
fmt.Fprintf(os.Stderr, "警告: 读取配置文件失败: %v,使用默认配置\n", err)
|
||
}
|
||
}
|
||
|
||
// 环境变量覆盖 // Logan.Li
|
||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||
viper.AutomaticEnv()
|
||
|
||
// CLI flags 覆盖(最高优先级)// Logan.Li
|
||
applyFlags()
|
||
|
||
// 解析配置 // Logan.Li
|
||
var cfg config.Config
|
||
if err := viper.Unmarshal(&cfg); err != nil {
|
||
fmt.Fprintf(os.Stderr, "配置解析失败: %v\n", err)
|
||
os.Exit(1)
|
||
}
|
||
|
||
return &cfg
|
||
}
|
||
|
||
// applyFlags 将 CLI 参数映射到 viper(覆盖 config.yaml 和默认值)// Logan.Li
|
||
func applyFlags() {
|
||
// 检查是否有任何 flag 被显式设置 // Logan.Li
|
||
flag.Visit(func(f *flag.Flag) {
|
||
switch f.Name {
|
||
case "port":
|
||
viper.Set("port", *flagPort)
|
||
case "mode":
|
||
viper.Set("mode", *flagMode)
|
||
case "log-level":
|
||
viper.Set("log_level", *flagLogLevel)
|
||
|
||
case "db-driver":
|
||
viper.Set("database.driver", *flagDBDriver)
|
||
case "db-host":
|
||
viper.Set("database.host", *flagDBHost)
|
||
case "db-port":
|
||
viper.Set("database.port", *flagDBPort)
|
||
case "db-user":
|
||
viper.Set("database.user", *flagDBUser)
|
||
case "db-password":
|
||
viper.Set("database.password", *flagDBPassword)
|
||
case "db-name":
|
||
viper.Set("database.dbname", *flagDBName)
|
||
case "db-charset":
|
||
viper.Set("database.charset", *flagDBCharset)
|
||
|
||
case "redis-host":
|
||
viper.Set("redis.host", *flagRedisHost)
|
||
case "redis-port":
|
||
viper.Set("redis.port", *flagRedisPort)
|
||
case "redis-password":
|
||
viper.Set("redis.password", *flagRedisPassword)
|
||
case "redis-db":
|
||
viper.Set("redis.db", *flagRedisDB)
|
||
case "redis-driver":
|
||
viper.Set("redis.driver", *flagRedisDriver)
|
||
|
||
case "jwt-secret":
|
||
viper.Set("jwt.secret", *flagJWTSecret)
|
||
case "jwt-expire":
|
||
viper.Set("jwt.expire_hour", *flagJWTExpire)
|
||
}
|
||
})
|
||
}
|
||
|
||
// CORS 跨域中间件
|
||
func CORS() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
|
||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
|
||
c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length")
|
||
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||
|
||
if c.Request.Method == "OPTIONS" {
|
||
c.AbortWithStatus(204)
|
||
return
|
||
}
|
||
|
||
c.Next()
|
||
}
|
||
}
|