diff --git a/cmd/root.go b/cmd/root.go index 78cad35..0472b96 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,128 +1,144 @@ -package cmd - -import ( - "io" - "networkDev/config" - "networkDev/utils/logger" - "os" - "path/filepath" - - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "github.com/spf13/viper" - "gopkg.in/natefinch/lumberjack.v2" -) - -var cfgFile string - -// rootCmd 代表没有调用子命令时的基础命令 -var rootCmd = &cobra.Command{ - Use: "networkDev", - Short: "一个基于Cobra的网络验证服务器应用", - Long: `networkDev是一个使用Cobra CLI框架构建的网络验证服务器应用, -集成了Viper配置管理、Logrus日志记录和embed静态资源嵌入功能。`, - PersistentPreRun: func(cmd *cobra.Command, args []string) { - // 在加载配置前配置logrus用于非HTTP日志 - - setupLogrusForNonHTTP() - - }, -} - -// Execute 添加所有子命令到根命令并设置适当的标志 -// 这由main.main()调用。只需要对rootCmd执行一次。 -func Execute() { - err := rootCmd.Execute() - if err != nil { - os.Exit(1) - } -} - -func init() { - cobra.OnInitialize(initConfig) - - // 在这里定义标志和配置设置 - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "配置文件路径 (默认为 config.json)") -} - -// setupLogrusForNonHTTP 配置logrus用于非HTTP日志 -// 在加载配置文件之前进行基本的logrus设置 -func setupLogrusForNonHTTP() { - // 设置日志格式 - logrus.SetFormatter(&logrus.TextFormatter{ - TimestampFormat: "2006-01-02 15:04:05", - FullTimestamp: true, - ForceColors: false, - DisableColors: true, - }) - - // 设置默认日志级别 - logrus.SetLevel(logrus.InfoLevel) - - // 设置输出目标(稍后会根据配置文件调整) - logrus.SetOutput(os.Stdout) - if cfgFile != "" { - // 使用命令行指定的配置文件 - config.Init(cfgFile) - } else { - // 使用默认配置文件路径 - config.Init("./config.json") - } - - // 根据配置文件进一步配置logrus - setupLogrusFromConfig() - - // 初始化HTTP日志处理器 - logger.InitLogger() - - // 记录配置加载完成 - logrus.WithField("config_file", viper.ConfigFileUsed()).Info("配置文件加载完成") -} - -// initConfig 读取配置文件和环境变量 -func initConfig() { - -} - -// setupLogrusFromConfig 根据配置文件进一步配置logrus -// 设置日志级别和输出目标,支持日志切割功能 -func setupLogrusFromConfig() { - // 设置日志级别 - if level := viper.GetString("log.level"); level != "" { - if logLevel, err := logrus.ParseLevel(level); err == nil { - logrus.SetLevel(logLevel) - } - } - - // 设置日志输出目标 - logFile := viper.GetString("log.file") - if logFile != "" { - // 确保日志目录存在 - logDir := filepath.Dir(logFile) - if err := os.MkdirAll(logDir, 0755); err != nil { - logrus.WithError(err).Error("创建日志目录失败") - return - } - - // 配置lumberjack日志轮转 - lumberjackLogger := &lumberjack.Logger{ - Filename: logFile, - MaxSize: viper.GetInt("log.max_size"), // MB - MaxBackups: viper.GetInt("log.max_backups"), // 保留的旧日志文件数量 - MaxAge: viper.GetInt("log.max_age"), // 天数 - Compress: true, // 压缩旧日志文件 - } - - // 同时输出到控制台和文件(带日志切割) - multiWriter := io.MultiWriter(os.Stdout, lumberjackLogger) - logrus.SetOutput(multiWriter) - - logrus.WithFields(logrus.Fields{ - "file": logFile, - "max_size": viper.GetInt("log.max_size"), - "max_backups": viper.GetInt("log.max_backups"), - "max_age": viper.GetInt("log.max_age"), - }).Info("日志切割功能已启用") - } - // 当日志文件路径为空时,保持默认输出到控制台,不创建任何目录 -} +package cmd + +import ( + "io" + "networkDev/config" + "networkDev/utils/logger" + "os" + "path/filepath" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "gopkg.in/natefinch/lumberjack.v2" +) + +// ============================================================================ +// 全局变量 +// ============================================================================ + +var cfgFile string + +// ============================================================================ +// 命令定义 +// ============================================================================ + +// rootCmd 代表没有调用子命令时的基础命令 +var rootCmd = &cobra.Command{ + Use: "networkDev", + Short: "一个基于Cobra的网络验证服务器应用", + Long: `networkDev是一个使用Cobra CLI框架构建的网络验证服务器应用, +集成了Viper配置管理、Logrus日志记录和embed静态资源嵌入功能。`, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + // 在加载配置前配置logrus用于非HTTP日志 + + setupLogrusForNonHTTP() + + }, +} + +// ============================================================================ +// 公共函数 +// ============================================================================ + +// Execute 添加所有子命令到根命令并设置适当的标志 +// 这由main.main()调用。只需要对rootCmd执行一次。 +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + cobra.OnInitialize(initConfig) + + // 在这里定义标志和配置设置 + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "配置文件路径 (默认为 config.json)") +} + +// ============================================================================ +// 私有函数 +// ============================================================================ + +// setupLogrusForNonHTTP 配置logrus用于非HTTP日志 +// 在加载配置文件之前进行基本的logrus设置 +func setupLogrusForNonHTTP() { + // 设置日志格式 + logrus.SetFormatter(&logrus.TextFormatter{ + TimestampFormat: "2006-01-02 15:04:05", + FullTimestamp: true, + ForceColors: false, + DisableColors: true, + }) + + // 设置默认日志级别 + logrus.SetLevel(logrus.InfoLevel) + + // 设置输出目标(稍后会根据配置文件调整) + logrus.SetOutput(os.Stdout) + if cfgFile != "" { + // 使用命令行指定的配置文件 + config.Init(cfgFile) + } else { + // 使用默认配置文件路径 + config.Init("./config.json") + } + + // 根据配置文件进一步配置logrus + setupLogrusFromConfig() + + // 初始化HTTP日志处理器 + logger.InitLogger() + + // 记录配置加载完成 + logrus.WithField("config_file", viper.ConfigFileUsed()).Info("配置文件加载完成") +} + +// initConfig 读取配置文件和环境变量 +func initConfig() { + +} + +// setupLogrusFromConfig 根据配置文件进一步配置logrus +// 设置日志级别和输出目标,支持日志切割功能 +func setupLogrusFromConfig() { + // 设置日志级别 + if level := viper.GetString("log.level"); level != "" { + if logLevel, err := logrus.ParseLevel(level); err == nil { + logrus.SetLevel(logLevel) + } + } + + // 设置日志输出目标 + logFile := viper.GetString("log.file") + if logFile != "" { + // 确保日志目录存在 + logDir := filepath.Dir(logFile) + if err := os.MkdirAll(logDir, 0755); err != nil { + logrus.WithError(err).Error("创建日志目录失败") + return + } + + // 配置lumberjack日志轮转 + lumberjackLogger := &lumberjack.Logger{ + Filename: logFile, + MaxSize: viper.GetInt("log.max_size"), // MB + MaxBackups: viper.GetInt("log.max_backups"), // 保留的旧日志文件数量 + MaxAge: viper.GetInt("log.max_age"), // 天数 + Compress: true, // 压缩旧日志文件 + } + + // 同时输出到控制台和文件(带日志切割) + multiWriter := io.MultiWriter(os.Stdout, lumberjackLogger) + logrus.SetOutput(multiWriter) + + logrus.WithFields(logrus.Fields{ + "file": logFile, + "max_size": viper.GetInt("log.max_size"), + "max_backups": viper.GetInt("log.max_backups"), + "max_age": viper.GetInt("log.max_age"), + }).Info("日志切割功能已启用") + } + // 当日志文件路径为空时,保持默认输出到控制台,不创建任何目录 +} diff --git a/cmd/server.go b/cmd/server.go index dd23538..f723925 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -1,190 +1,206 @@ -package cmd - -import ( - "context" - "fmt" - "io" - "net/http" - "os" - "os/signal" - "syscall" - "time" - - "networkDev/database" - "networkDev/middleware" - "networkDev/server" - "networkDev/utils" - "networkDev/utils/logger" - "networkDev/web" - - "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -// serverCmd 代表服务器命令 -var serverCmd = &cobra.Command{ - Use: "server", - Short: "启动HTTP服务器", - Long: `启动一个简单的HTTP服务器,监听配置文件中指定的端口。`, - Run: runServer, -} - -func init() { - // 将服务器命令添加到根命令 - rootCmd.AddCommand(serverCmd) - - // 添加服务器特定的标志 - serverCmd.Flags().StringP("host", "H", "", "服务器监听地址 (覆盖配置文件)") - serverCmd.Flags().IntP("port", "p", 0, "服务器监听端口 (覆盖配置文件)") -} - -// runServer 运行HTTP服务器 -func runServer(cmd *cobra.Command, args []string) { - // 获取配置 - host := getServerHost(cmd) - port := getServerPort(cmd) - addr := fmt.Sprintf("%s:%d", host, port) - - // 获取全局日志实例 - logger := logger.GetLogger() - logger.LogServerStart(host, port) - - // 初始化Redis(如果配置存在,失败不致命) - utils.InitRedis() - - // 初始化数据库(根据 viper 配置选择 SQLite 或 MySQL) - // 如果初始化失败则回退并退出 - if _, err := database.Init(); err != nil { - logrus.WithError(err).Fatal("数据库初始化失败") - } - // 执行自动迁移(确保表结构存在) - if err := database.AutoMigrate(); err != nil { - logrus.WithError(err).Fatal("数据库自动迁移失败") - } - // 初始化默认系统设置(包含管理员账号) - if err := database.SeedDefaultSettings(); err != nil { - logrus.WithError(err).Fatal("默认系统设置初始化失败") - } - - // 创建HTTP服务器 - server := createHTTPServer(addr) - - // 启动服务器 - startServer(server) -} - -// getServerHost 获取服务器监听地址 -func getServerHost(cmd *cobra.Command) string { - if host, _ := cmd.Flags().GetString("host"); host != "" { - return host - } - return viper.GetString("server.host") -} - -// getServerPort 获取服务器监听端口 -func getServerPort(cmd *cobra.Command) int { - if port, _ := cmd.Flags().GetInt("port"); port != 0 { - return port - } - return viper.GetInt("server.port") -} - -// createHTTPServer 创建HTTP服务器 -func createHTTPServer(addr string) *http.Server { - // 配置Gin模式和日志 - configureGin() - - // 创建Gin引擎 - router := gin.New() - - // 添加恢复中间件 - router.Use(gin.Recovery()) - - // 添加日志中间件 - router.Use(middleware.WrapHandler()) - - // 添加开发模式中间件(统一管理开发模式功能) - router.Use(middleware.DevModeMiddleware(router)) - - // 加载模板 - if err := loadTemplates(router); err != nil { - logrus.WithError(err).Fatal("模板加载失败") - } - - // 注册路由 - registerRoutes(router) - - return &http.Server{ - Addr: addr, - Handler: router, - } -} - -// loadTemplates 加载模板到Gin引擎 -func loadTemplates(router *gin.Engine) error { - tmpl, err := web.ParseTemplates() - if err != nil { - return err - } - router.SetHTMLTemplate(tmpl) - return nil -} - -// registerRoutes 注册HTTP路由 -func registerRoutes(router *gin.Engine) { - // 使用server包中的路由注册函数 - server.RegisterRoutes(router) -} - -// startServer 启动服务器并处理优雅关闭 -func startServer(server *http.Server) { - // 获取全局日志实例 - logger := logger.GetLogger() - - // 创建一个通道来接收操作系统信号 - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - - // 在goroutine中启动服务器 - go func() { - logger.WithField("addr", server.Addr).Info("HTTP服务器已启动") - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - logger.LogError(err, "服务器启动失败") - os.Exit(1) - } - }() - - // 等待中断信号 - <-sigChan - logger.Info("收到关闭信号,正在优雅关闭服务器...") - - // 创建一个带超时的上下文 - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - // 优雅关闭服务器 - if err := server.Shutdown(ctx); err != nil { - logger.LogError(err, "服务器关闭时出错") - } else { - logger.LogServerStop() - } -} - -// configureGin 配置Gin的全局设置 -func configureGin() { - // 禁用Gin的颜色输出,提高控制台兼容性 - gin.DisableConsoleColor() - - // 设置Gin的输出为丢弃,因为我们使用自定义日志中间件 - gin.DefaultWriter = io.Discard - gin.DefaultErrorWriter = io.Discard - - // 根据配置设置Gin模式 - if viper.GetString("app.mode") == "production" { - gin.SetMode(gin.ReleaseMode) - } else { - gin.SetMode(gin.DebugMode) - } -} +package cmd + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "networkDev/database" + "networkDev/middleware" + "networkDev/server" + "networkDev/utils" + "networkDev/utils/logger" + "networkDev/web" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// ============================================================================ +// 命令定义 +// ============================================================================ + +// serverCmd 代表服务器命令 +var serverCmd = &cobra.Command{ + Use: "server", + Short: "启动HTTP服务器", + Long: `启动一个简单的HTTP服务器,监听配置文件中指定的端口。`, + Run: runServer, +} + +// ============================================================================ +// 初始化函数 +// ============================================================================ + +func init() { + // 将服务器命令添加到根命令 + rootCmd.AddCommand(serverCmd) + + // 添加服务器特定的标志 + serverCmd.Flags().StringP("host", "H", "", "服务器监听地址 (覆盖配置文件)") + serverCmd.Flags().IntP("port", "p", 0, "服务器监听端口 (覆盖配置文件)") +} + +// ============================================================================ +// 主要函数 +// ============================================================================ + +// runServer 运行HTTP服务器 +func runServer(cmd *cobra.Command, args []string) { + // 获取配置 + host := getServerHost(cmd) + port := getServerPort(cmd) + addr := fmt.Sprintf("%s:%d", host, port) + + // 获取全局日志实例 + logger := logger.GetLogger() + logger.LogServerStart(host, port) + + // 初始化Redis(如果配置存在,失败不致命) + utils.InitRedis() + + // 初始化数据库(根据 viper 配置选择 SQLite 或 MySQL) + // 如果初始化失败则回退并退出 + if _, err := database.Init(); err != nil { + logrus.WithError(err).Fatal("数据库初始化失败") + } + // 执行自动迁移(确保表结构存在) + if err := database.AutoMigrate(); err != nil { + logrus.WithError(err).Fatal("数据库自动迁移失败") + } + // 初始化默认系统设置(包含管理员账号) + if err := database.SeedDefaultSettings(); err != nil { + logrus.WithError(err).Fatal("默认系统设置初始化失败") + } + + // 创建HTTP服务器 + server := createHTTPServer(addr) + + // 启动服务器 + startServer(server) +} + +// ============================================================================ +// 辅助函数 +// ============================================================================ + +// getServerHost 获取服务器监听地址 +func getServerHost(cmd *cobra.Command) string { + if host, _ := cmd.Flags().GetString("host"); host != "" { + return host + } + return viper.GetString("server.host") +} + +// getServerPort 获取服务器监听端口 +func getServerPort(cmd *cobra.Command) int { + if port, _ := cmd.Flags().GetInt("port"); port != 0 { + return port + } + return viper.GetInt("server.port") +} + +// createHTTPServer 创建HTTP服务器 +func createHTTPServer(addr string) *http.Server { + // 配置Gin模式和日志 + configureGin() + + // 创建Gin引擎 + router := gin.New() + + // 添加恢复中间件 + router.Use(gin.Recovery()) + + // 添加日志中间件 + router.Use(middleware.WrapHandler()) + + // 添加开发模式中间件(统一管理开发模式功能) + router.Use(middleware.DevModeMiddleware(router)) + + // 加载模板 + if err := loadTemplates(router); err != nil { + logrus.WithError(err).Fatal("模板加载失败") + } + + // 注册路由 + registerRoutes(router) + + return &http.Server{ + Addr: addr, + Handler: router, + } +} + +// loadTemplates 加载模板到Gin引擎 +func loadTemplates(router *gin.Engine) error { + tmpl, err := web.ParseTemplates() + if err != nil { + return err + } + router.SetHTMLTemplate(tmpl) + return nil +} + +// registerRoutes 注册HTTP路由 +func registerRoutes(router *gin.Engine) { + // 使用server包中的路由注册函数 + server.RegisterRoutes(router) +} + +// startServer 启动服务器并处理优雅关闭 +func startServer(server *http.Server) { + // 获取全局日志实例 + logger := logger.GetLogger() + + // 创建一个通道来接收操作系统信号 + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + // 在goroutine中启动服务器 + go func() { + logger.WithField("addr", server.Addr).Info("HTTP服务器已启动") + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + logger.LogError(err, "服务器启动失败") + os.Exit(1) + } + }() + + // 等待中断信号 + <-sigChan + logger.Info("收到关闭信号,正在优雅关闭服务器...") + + // 创建一个带超时的上下文 + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // 优雅关闭服务器 + if err := server.Shutdown(ctx); err != nil { + logger.LogError(err, "服务器关闭时出错") + } else { + logger.LogServerStop() + } +} + +// configureGin 配置Gin的全局设置 +func configureGin() { + // 禁用Gin的颜色输出,提高控制台兼容性 + gin.DisableConsoleColor() + + // 设置Gin的输出为丢弃,因为我们使用自定义日志中间件 + gin.DefaultWriter = io.Discard + gin.DefaultErrorWriter = io.Discard + + // 根据配置设置Gin模式 + if viper.GetString("app.mode") == "production" { + gin.SetMode(gin.ReleaseMode) + } else { + gin.SetMode(gin.DebugMode) + } +} diff --git a/config/config.go b/config/config.go index 7e1adcf..6929949 100644 --- a/config/config.go +++ b/config/config.go @@ -1,269 +1,277 @@ -package config - -import ( - "bytes" - "encoding/json" - "errors" - "io/fs" - "os" - - log "github.com/sirupsen/logrus" - "github.com/spf13/viper" -) - -// ServerConfig 服务器配置结构体 -// 包含服务器运行相关的配置信息 -type ServerConfig struct { - Host string `json:"host" mapstructure:"host"` // 服务器监听地址 - Port int `json:"port" mapstructure:"port"` // 服务器监听端口 - Dist string `json:"dist" mapstructure:"dist"` // 静态文件目录 - DevMode bool `json:"dev_mode" mapstructure:"dev_mode"` // 开发模式(跳过验证码等) -} - -// DatabaseConfig 数据库配置结构体 -// 包含数据库连接相关的配置信息 -type DatabaseConfig struct { - Type string `json:"type" mapstructure:"type"` // 数据库类型(mysql/sqlite) - MySQL MySQLConfig `json:"mysql" mapstructure:"mysql"` // MySQL配置 - SQLite SQLiteConfig `json:"sqlite" mapstructure:"sqlite"` // SQLite配置 -} - -// MySQLConfig MySQL数据库配置结构体 -// 包含MySQL数据库连接的详细配置信息 -type MySQLConfig struct { - Host string `json:"host" mapstructure:"host"` // 数据库主机地址 - Port int `json:"port" mapstructure:"port"` // 数据库端口 - Username string `json:"username" mapstructure:"username"` // 数据库用户名 - Password string `json:"password" mapstructure:"password"` // 数据库密码 - Database string `json:"database" mapstructure:"database"` // 数据库名称 - Charset string `json:"charset" mapstructure:"charset"` // 字符集 - MaxIdleConns int `json:"max_idle_conns" mapstructure:"max_idle_conns"` // 最大空闲连接数 - MaxOpenConns int `json:"max_open_conns" mapstructure:"max_open_conns"` // 最大打开连接数 -} - -// SQLiteConfig SQLite数据库配置结构体 -// 包含SQLite数据库文件路径配置 -type SQLiteConfig struct { - Path string `json:"path" mapstructure:"path"` // 数据库文件路径 -} - -// RedisConfig Redis配置结构体 -// 包含Redis连接相关的配置信息 -type RedisConfig struct { - Host string `json:"host" mapstructure:"host"` // Redis服务器地址 - Port int `json:"port" mapstructure:"port"` // Redis服务器端口 - Password string `json:"password" mapstructure:"password"` // Redis密码 - DB int `json:"db" mapstructure:"db"` // Redis数据库编号 -} - -// LogConfig 日志配置结构体 -// 包含日志记录相关的配置信息 -type LogConfig struct { - Level string `json:"level" mapstructure:"level"` // 日志级别 - File string `json:"file" mapstructure:"file"` // 日志文件路径 - MaxSize int `json:"max_size" mapstructure:"max_size"` // 单个日志文件最大大小(MB) - MaxBackups int `json:"max_backups" mapstructure:"max_backups"` // 保留的旧日志文件数量 - MaxAge int `json:"max_age" mapstructure:"max_age"` // 日志文件保留天数 -} - -// CookieConfig Cookie配置结构体 -// 包含Cookie相关的安全配置信息 -type CookieConfig struct { - Secure bool `json:"secure" mapstructure:"secure"` // 是否只在HTTPS下发送Cookie - SameSite string `json:"same_site" mapstructure:"same_site"` // SameSite属性(Strict/Lax/None) - Domain string `json:"domain" mapstructure:"domain"` // Cookie域名 - MaxAge int `json:"max_age" mapstructure:"max_age"` // Cookie最大存活时间(秒) -} - -// SecurityConfig 安全配置结构体 -// 包含应用程序安全相关的配置信息 -type SecurityConfig struct { - JWTSecret string `json:"jwt_secret" mapstructure:"jwt_secret"` // JWT签名密钥 - EncryptionKey string `json:"encryption_key" mapstructure:"encryption_key"` // 数据加密密钥 - JWTRefresh int `json:"jwt_refresh" mapstructure:"jwt_refresh"` // JWT令牌刷新阈值(小时) - Cookie CookieConfig `json:"cookie" mapstructure:"cookie"` // Cookie配置 -} - -// AppConfig 应用配置结构体 -type AppConfig struct { - Server ServerConfig `json:"server" mapstructure:"server"` - Database DatabaseConfig `json:"database" mapstructure:"database"` - Redis RedisConfig `json:"redis" mapstructure:"redis"` - Log LogConfig `json:"log" mapstructure:"log"` - Security SecurityConfig `json:"security" mapstructure:"security"` -} - -// GetDefaultAppConfig 获取默认应用配置 -func GetDefaultAppConfig() *AppConfig { - return &AppConfig{ - Server: ServerConfig{ - Host: "0.0.0.0", - Port: 8080, - Dist: "", - DevMode: false, - }, - Database: DatabaseConfig{ - Type: "sqlite", - MySQL: MySQLConfig{ - Host: "localhost", - Port: 3306, - Username: "root", - Password: "password", - Database: "networkdev", - Charset: "utf8mb4", - MaxIdleConns: 10, - MaxOpenConns: 100, - }, - SQLite: SQLiteConfig{ - Path: "./database.db", - }, - }, - Redis: RedisConfig{ - Host: "localhost", - Port: 6379, - Password: "", - DB: 0, - }, - Log: LogConfig{ - Level: "info", - File: "./logs/app.log", - MaxSize: 100, - MaxBackups: 5, - MaxAge: 30, - }, - Security: SecurityConfig{ - JWTSecret: "", - EncryptionKey: "", - JWTRefresh: 6, - Cookie: CookieConfig{ - Secure: true, - SameSite: "Lax", - Domain: "", - MaxAge: 86400, - }, - }, - } -} - -// GetSecureDefaultAppConfig 获取带有安全密钥的默认应用配置 -func GetSecureDefaultAppConfig() (*AppConfig, error) { - config := GetDefaultAppConfig() - - // 生成安全密钥 - jwtSecret, encryptionKey, err := GenerateSecureKeys() - if err != nil { - return nil, err - } - - // 设置安全密钥 - config.Security.JWTSecret = jwtSecret - config.Security.EncryptionKey = encryptionKey - - return config, nil -} - -// Init 初始化配置文件 -func Init(cfgFilePath string) { - viper.SetConfigFile(cfgFilePath) - viper.SetConfigType("json") - viper.AddConfigPath(".") - - if err := viper.ReadInConfig(); err != nil { - var pathError *fs.PathError - if errors.As(err, &pathError) { - log.Warn("未找到配置文件,使用默认配置") - - // 生成带有安全密钥的默认配置 - defaultConfig, configErr := GetSecureDefaultAppConfig() - if configErr != nil { - log.WithFields( - log.Fields{ - "err": configErr, - }, - ).Error("生成安全配置失败,使用基础默认配置") - defaultConfig = GetDefaultAppConfig() - } - - // 将配置结构体转换为JSON - configBytes, marshalErr := json.MarshalIndent(defaultConfig, "", " ") - if marshalErr != nil { - log.WithFields( - log.Fields{ - "err": marshalErr, - }, - ).Fatal("序列化默认配置失败") - return - } - - // 写入配置文件 - err = os.WriteFile(cfgFilePath, configBytes, 0o644) - if err != nil { - log.WithFields( - log.Fields{ - "err": err, - }, - ).Error("写入默认配置文件失败") - } else { - log.WithFields( - log.Fields{ - "file": cfgFilePath, - }, - ).Info("写入默认配置文件成功(已生成安全密钥)") - } - - // 将配置加载到viper中 - err = viper.ReadConfig(bytes.NewBuffer(configBytes)) - if err != nil { - log.WithFields( - log.Fields{ - "err": err, - }, - ).Error("读取默认配置失败") - } else { - log.Info("已成功读取默认配置") - } - } else { - log.WithFields( - log.Fields{ - "err": err, - }, - ).Fatal("配置文件解析错误") - } - } - log.WithFields( - log.Fields{ - "file": viper.ConfigFileUsed(), - }, - ).Info("使用配置文件") - - // 验证配置 - if _, err := ValidateConfig(); err != nil { - log.WithFields( - log.Fields{ - "err": err, - }, - ).Fatal("配置验证失败") - } -} - -// CreateDefaultConfig 创建默认配置文件 -func CreateDefaultConfig(filePath string) error { - // 生成带有安全密钥的默认配置 - defaultConfig, err := GetSecureDefaultAppConfig() - if err != nil { - log.WithFields( - log.Fields{ - "err": err, - }, - ).Error("生成安全配置失败,使用基础默认配置") - defaultConfig = GetDefaultAppConfig() - } - - // 将配置结构体转换为JSON - configBytes, err := json.MarshalIndent(defaultConfig, "", " ") - if err != nil { - return err - } - - return os.WriteFile(filePath, configBytes, 0o644) -} +package config + +import ( + "bytes" + "encoding/json" + "errors" + "io/fs" + "os" + + log "github.com/sirupsen/logrus" + "github.com/spf13/viper" +) + +// ============================================================================ +// 结构体定义 +// ============================================================================ + +// ServerConfig 服务器配置结构体 +// 包含服务器运行相关的配置信息 +type ServerConfig struct { + Host string `json:"host" mapstructure:"host"` // 服务器监听地址 + Port int `json:"port" mapstructure:"port"` // 服务器监听端口 + Dist string `json:"dist" mapstructure:"dist"` // 静态文件目录 + DevMode bool `json:"dev_mode" mapstructure:"dev_mode"` // 开发模式(跳过验证码等) +} + +// DatabaseConfig 数据库配置结构体 +// 包含数据库连接相关的配置信息 +type DatabaseConfig struct { + Type string `json:"type" mapstructure:"type"` // 数据库类型(mysql/sqlite) + MySQL MySQLConfig `json:"mysql" mapstructure:"mysql"` // MySQL配置 + SQLite SQLiteConfig `json:"sqlite" mapstructure:"sqlite"` // SQLite配置 +} + +// MySQLConfig MySQL数据库配置结构体 +// 包含MySQL数据库连接的详细配置信息 +type MySQLConfig struct { + Host string `json:"host" mapstructure:"host"` // 数据库主机地址 + Port int `json:"port" mapstructure:"port"` // 数据库端口 + Username string `json:"username" mapstructure:"username"` // 数据库用户名 + Password string `json:"password" mapstructure:"password"` // 数据库密码 + Database string `json:"database" mapstructure:"database"` // 数据库名称 + Charset string `json:"charset" mapstructure:"charset"` // 字符集 + MaxIdleConns int `json:"max_idle_conns" mapstructure:"max_idle_conns"` // 最大空闲连接数 + MaxOpenConns int `json:"max_open_conns" mapstructure:"max_open_conns"` // 最大打开连接数 +} + +// SQLiteConfig SQLite数据库配置结构体 +// 包含SQLite数据库文件路径配置 +type SQLiteConfig struct { + Path string `json:"path" mapstructure:"path"` // 数据库文件路径 +} + +// RedisConfig Redis配置结构体 +// 包含Redis连接相关的配置信息 +type RedisConfig struct { + Host string `json:"host" mapstructure:"host"` // Redis服务器地址 + Port int `json:"port" mapstructure:"port"` // Redis服务器端口 + Password string `json:"password" mapstructure:"password"` // Redis密码 + DB int `json:"db" mapstructure:"db"` // Redis数据库编号 +} + +// LogConfig 日志配置结构体 +// 包含日志记录相关的配置信息 +type LogConfig struct { + Level string `json:"level" mapstructure:"level"` // 日志级别 + File string `json:"file" mapstructure:"file"` // 日志文件路径 + MaxSize int `json:"max_size" mapstructure:"max_size"` // 单个日志文件最大大小(MB) + MaxBackups int `json:"max_backups" mapstructure:"max_backups"` // 保留的旧日志文件数量 + MaxAge int `json:"max_age" mapstructure:"max_age"` // 日志文件保留天数 +} + +// CookieConfig Cookie配置结构体 +// 包含Cookie相关的安全配置信息 +type CookieConfig struct { + Secure bool `json:"secure" mapstructure:"secure"` // 是否只在HTTPS下发送Cookie + SameSite string `json:"same_site" mapstructure:"same_site"` // SameSite属性(Strict/Lax/None) + Domain string `json:"domain" mapstructure:"domain"` // Cookie域名 + MaxAge int `json:"max_age" mapstructure:"max_age"` // Cookie最大存活时间(秒) +} + +// SecurityConfig 安全配置结构体 +// 包含应用程序安全相关的配置信息 +type SecurityConfig struct { + JWTSecret string `json:"jwt_secret" mapstructure:"jwt_secret"` // JWT签名密钥 + EncryptionKey string `json:"encryption_key" mapstructure:"encryption_key"` // 数据加密密钥 + JWTRefresh int `json:"jwt_refresh" mapstructure:"jwt_refresh"` // JWT令牌刷新阈值(小时) + Cookie CookieConfig `json:"cookie" mapstructure:"cookie"` // Cookie配置 +} + +// AppConfig 应用配置结构体 +type AppConfig struct { + Server ServerConfig `json:"server" mapstructure:"server"` + Database DatabaseConfig `json:"database" mapstructure:"database"` + Redis RedisConfig `json:"redis" mapstructure:"redis"` + Log LogConfig `json:"log" mapstructure:"log"` + Security SecurityConfig `json:"security" mapstructure:"security"` +} + +// ============================================================================ +// 公共函数 +// ============================================================================ + +// GetDefaultAppConfig 获取默认应用配置 +func GetDefaultAppConfig() *AppConfig { + return &AppConfig{ + Server: ServerConfig{ + Host: "0.0.0.0", + Port: 8080, + Dist: "", + DevMode: false, + }, + Database: DatabaseConfig{ + Type: "sqlite", + MySQL: MySQLConfig{ + Host: "localhost", + Port: 3306, + Username: "root", + Password: "password", + Database: "networkdev", + Charset: "utf8mb4", + MaxIdleConns: 10, + MaxOpenConns: 100, + }, + SQLite: SQLiteConfig{ + Path: "./database.db", + }, + }, + Redis: RedisConfig{ + Host: "localhost", + Port: 6379, + Password: "", + DB: 0, + }, + Log: LogConfig{ + Level: "info", + File: "./logs/app.log", + MaxSize: 100, + MaxBackups: 5, + MaxAge: 30, + }, + Security: SecurityConfig{ + JWTSecret: "", + EncryptionKey: "", + JWTRefresh: 6, + Cookie: CookieConfig{ + Secure: true, + SameSite: "Lax", + Domain: "", + MaxAge: 86400, + }, + }, + } +} + +// GetSecureDefaultAppConfig 获取带有安全密钥的默认应用配置 +func GetSecureDefaultAppConfig() (*AppConfig, error) { + config := GetDefaultAppConfig() + + // 生成安全密钥 + jwtSecret, encryptionKey, err := GenerateSecureKeys() + if err != nil { + return nil, err + } + + // 设置安全密钥 + config.Security.JWTSecret = jwtSecret + config.Security.EncryptionKey = encryptionKey + + return config, nil +} + +// Init 初始化配置文件 +func Init(cfgFilePath string) { + viper.SetConfigFile(cfgFilePath) + viper.SetConfigType("json") + viper.AddConfigPath(".") + + if err := viper.ReadInConfig(); err != nil { + var pathError *fs.PathError + if errors.As(err, &pathError) { + log.Warn("未找到配置文件,使用默认配置") + + // 生成带有安全密钥的默认配置 + defaultConfig, configErr := GetSecureDefaultAppConfig() + if configErr != nil { + log.WithFields( + log.Fields{ + "err": configErr, + }, + ).Error("生成安全配置失败,使用基础默认配置") + defaultConfig = GetDefaultAppConfig() + } + + // 将配置结构体转换为JSON + configBytes, marshalErr := json.MarshalIndent(defaultConfig, "", " ") + if marshalErr != nil { + log.WithFields( + log.Fields{ + "err": marshalErr, + }, + ).Fatal("序列化默认配置失败") + return + } + + // 写入配置文件 + err = os.WriteFile(cfgFilePath, configBytes, 0o644) + if err != nil { + log.WithFields( + log.Fields{ + "err": err, + }, + ).Error("写入默认配置文件失败") + } else { + log.WithFields( + log.Fields{ + "file": cfgFilePath, + }, + ).Info("写入默认配置文件成功(已生成安全密钥)") + } + + // 将配置加载到viper中 + err = viper.ReadConfig(bytes.NewBuffer(configBytes)) + if err != nil { + log.WithFields( + log.Fields{ + "err": err, + }, + ).Error("读取默认配置失败") + } else { + log.Info("已成功读取默认配置") + } + } else { + log.WithFields( + log.Fields{ + "err": err, + }, + ).Fatal("配置文件解析错误") + } + } + log.WithFields( + log.Fields{ + "file": viper.ConfigFileUsed(), + }, + ).Info("使用配置文件") + + // 验证配置 + if _, err := ValidateConfig(); err != nil { + log.WithFields( + log.Fields{ + "err": err, + }, + ).Fatal("配置验证失败") + } +} + +// CreateDefaultConfig 创建默认配置文件 +func CreateDefaultConfig(filePath string) error { + // 生成带有安全密钥的默认配置 + defaultConfig, err := GetSecureDefaultAppConfig() + if err != nil { + log.WithFields( + log.Fields{ + "err": err, + }, + ).Error("生成安全配置失败,使用基础默认配置") + defaultConfig = GetDefaultAppConfig() + } + + // 将配置结构体转换为JSON + configBytes, err := json.MarshalIndent(defaultConfig, "", " ") + if err != nil { + return err + } + + return os.WriteFile(filePath, configBytes, 0o644) +} diff --git a/config/security.go b/config/security.go index 1cf55bd..ba60aaf 100644 --- a/config/security.go +++ b/config/security.go @@ -7,6 +7,10 @@ import ( "fmt" ) +// ============================================================================ +// 公共函数 +// ============================================================================ + // GenerateSecureJWTSecret 生成安全的JWT密钥 // 生成64字节(512位)的随机密钥,使用base64编码 func GenerateSecureJWTSecret() (string, error) { @@ -15,7 +19,7 @@ func GenerateSecureJWTSecret() (string, error) { if _, err := rand.Read(bytes); err != nil { return "", fmt.Errorf("生成JWT密钥失败: %w", err) } - + // 使用base64编码,便于配置文件存储 return base64.StdEncoding.EncodeToString(bytes), nil } @@ -28,7 +32,7 @@ func GenerateSecureEncryptionKey() (string, error) { if _, err := rand.Read(bytes); err != nil { return "", fmt.Errorf("生成加密密钥失败: %w", err) } - + // 使用十六进制编码 return hex.EncodeToString(bytes), nil } @@ -39,11 +43,11 @@ func GenerateSecureKeys() (jwtSecret, encryptionKey string, err error) { if err != nil { return "", "", err } - + encryptionKey, err = GenerateSecureEncryptionKey() if err != nil { return "", "", err } - + return jwtSecret, encryptionKey, nil -} \ No newline at end of file +} diff --git a/config/validator.go b/config/validator.go index 4f141aa..67db127 100644 --- a/config/validator.go +++ b/config/validator.go @@ -13,6 +13,10 @@ import ( "github.com/spf13/viper" ) +// ============================================================================ +// 公共函数 +// ============================================================================ + // ValidateConfig 验证配置 func ValidateConfig() (*AppConfig, error) { var config AppConfig @@ -31,6 +35,10 @@ func ValidateConfig() (*AppConfig, error) { return &config, nil } +// ============================================================================ +// 私有函数 +// ============================================================================ + // validateConfig 验证配置 func validateConfig(config *AppConfig) error { // 验证服务器配置 diff --git a/constants/status.go b/constants/status.go index 82df70f..28efa6c 100644 --- a/constants/status.go +++ b/constants/status.go @@ -1,7 +1,11 @@ -package constants - -// 应用程序版本信息 -const ( - // AppVersion 应用程序版本号 - AppVersion = "0.3.0" -) +package constants + +// ============================================================================ +// 常量定义 +// ============================================================================ + +// 应用程序版本信息 +const ( + // AppVersion 应用程序版本号 + AppVersion = "0.3.0" +) diff --git a/controllers/admin/api.go b/controllers/admin/api.go index 6f32d43..547e8b9 100644 --- a/controllers/admin/api.go +++ b/controllers/admin/api.go @@ -13,9 +13,17 @@ import ( "github.com/sirupsen/logrus" ) +// ============================================================================ +// 全局变量 +// ============================================================================ + // 创建基础控制器实例 var apiBaseController = controllers.NewBaseController() +// ============================================================================ +// 页面处理器 +// ============================================================================ + // APIFragmentHandler 接口列表页面片段处理器 func APIFragmentHandler(c *gin.Context) { c.HTML(http.StatusOK, "apis.html", gin.H{ @@ -23,6 +31,10 @@ func APIFragmentHandler(c *gin.Context) { }) } +// ============================================================================ +// API处理器 +// ============================================================================ + // APIListHandler 接口列表API处理器 func APIListHandler(c *gin.Context) { // 获取分页参数 @@ -142,6 +154,10 @@ func APIListHandler(c *gin.Context) { c.JSON(http.StatusOK, response) } +// ============================================================================ +// 辅助函数 +// ============================================================================ + // getAPIStatusName 获取API状态名称 func getAPIStatusName(status int) string { switch status { @@ -238,7 +254,7 @@ func APIGetTypesHandler(c *gin.Context) { validTypes := []int{ models.APITypeGetBulletin, models.APITypeGetUpdateUrl, models.APITypeCheckAppVersion, models.APITypeGetCardInfo, models.APITypeSingleLogin, - models.APITypeUserLogin, models.APITypeUserRegin, models.APITypeUserRecharge, models.APITypeCardRegin, + models.APITypeUserLogin, models.APITypeUserRegin, models.APITypeUserRecharge, models.APITypeLogOut, models.APITypeGetExpired, models.APITypeCheckUserStatus, models.APITypeGetAppData, models.APITypeGetVariable, models.APITypeUpdatePwd, models.APITypeMacChangeBind, models.APITypeIPChangeBind, diff --git a/controllers/admin/app.go b/controllers/admin/app.go index 4de43c2..8a2ea88 100644 --- a/controllers/admin/app.go +++ b/controllers/admin/app.go @@ -15,8 +15,16 @@ import ( "github.com/sirupsen/logrus" ) +// ============================================================================ +// 全局变量 +// ============================================================================ + var appBaseController = controllers.NewBaseController() +// ============================================================================ +// 页面处理器 +// ============================================================================ + // AppsFragmentHandler 应用列表页面片段处理器 func AppsFragmentHandler(c *gin.Context) { c.HTML(http.StatusOK, "apps.html", gin.H{ @@ -24,6 +32,10 @@ func AppsFragmentHandler(c *gin.Context) { }) } +// ============================================================================ +// API处理器 +// ============================================================================ + // AppsListHandler 应用列表API处理器 func AppsListHandler(c *gin.Context) { // 获取分页参数 @@ -338,28 +350,7 @@ func AppCreateHandler(c *gin.Context) { } // 为应用创建所有默认接口 - defaultAPITypes := []int{ - models.APITypeGetBulletin, // 获取程序公告 - models.APITypeGetUpdateUrl, // 获取更新地址 - models.APITypeCheckAppVersion, // 检测最新版本 - models.APITypeGetCardInfo, // 获取卡密信息 - models.APITypeSingleLogin, // 卡密登录 - models.APITypeUserLogin, // 用户登录 - models.APITypeUserRegin, // 用户注册 - models.APITypeUserRecharge, // 用户充值 - models.APITypeCardRegin, // 卡密注册 - models.APITypeLogOut, // 退出登录 - models.APITypeGetExpired, // 获取到期时间 - models.APITypeCheckUserStatus, // 检测账号状态 - models.APITypeGetAppData, // 获取程序数据 - models.APITypeGetVariable, // 获取变量数据 - models.APITypeUpdatePwd, // 修改账号密码 - models.APITypeMacChangeBind, // 机器码转绑 - models.APITypeIPChangeBind, // IP转绑 - models.APITypeDisableUser, // 封停用户 - models.APITypeBlackUser, // 添加黑名单 - models.APITypeUserDeductedTime, // 扣除时间 - } + defaultAPITypes := models.GetDefaultAPITypes() // 批量创建默认接口 for _, apiType := range defaultAPITypes { diff --git a/controllers/admin/auth.go b/controllers/admin/auth.go index fd8225c..26bfae2 100644 --- a/controllers/admin/auth.go +++ b/controllers/admin/auth.go @@ -16,9 +16,17 @@ import ( "github.com/spf13/viper" ) +// ============================================================================ +// 全局变量 +// ============================================================================ + // 创建BaseController实例 var authBaseController = controllers.NewBaseController() +// ============================================================================ +// 页面处理器 +// ============================================================================ + // LoginPageHandler 管理员登录页渲染处理器 // - 如果已登录则重定向到 /admin // - 否则渲染 web/template/admin/login.html 模板 @@ -63,6 +71,10 @@ func LoginPageHandler(c *gin.Context) { c.HTML(http.StatusOK, "login.html", data) } +// ============================================================================ +// API处理器 +// ============================================================================ + // LoginHandler 管理员登录接口 // - 接收JSON: {username, password} // - 验证用户存在与密码正确性 @@ -74,11 +86,11 @@ func LoginHandler(c *gin.Context) { Password string `json:"password"` Captcha string `json:"captcha"` } - + if !authBaseController.BindJSON(c, &body) { return } - + if !authBaseController.ValidateRequired(c, map[string]interface{}{ "用户名": body.Username, "密码": body.Password, @@ -178,7 +190,11 @@ func LogoutHandler(c *gin.Context) { }) } -// clearInvalidJWTCookie 清理失效的JWT Cookie +// ============================================================================ +// 辅助函数 +// ============================================================================ + +// clearInvalidJWTCookie 清理无效的JWT Cookie // - 统一的Cookie清理函数,确保一致性 // - 在JWT校验失败时自动调用,提升安全性和用户体验 func clearInvalidJWTCookie(c *gin.Context) { @@ -192,7 +208,11 @@ func getJWTSecret() []byte { return []byte(viper.GetString("security.jwt_secret")) } -// JWTClaims JWT载荷结构 +// ============================================================================ +// 结构体定义 +// ============================================================================ + +// JWTClaims JWT载荷结构体 type JWTClaims struct { Username string `json:"username"` PasswordHash string `json:"password_hash"` // 密码哈希摘要,用于验证密码是否被修改 diff --git a/controllers/admin/captcha.go b/controllers/admin/captcha.go index c4074b8..b58084d 100644 --- a/controllers/admin/captcha.go +++ b/controllers/admin/captcha.go @@ -15,12 +15,20 @@ import ( "github.com/mojocn/base64Captcha" ) +// ============================================================================ +// 全局变量 +// ============================================================================ + // 创建基础控制器实例 var captchaBaseController = controllers.NewBaseController() // 全局验证码存储器 var store = base64Captcha.DefaultMemStore +// ============================================================================ +// 辅助函数 +// ============================================================================ + // secureRandomInt 生成安全的随机整数,范围 [0, max) func secureRandomInt(max int) (int, error) { n, err := rand.Int(rand.Reader, big.NewInt(int64(max))) @@ -30,6 +38,10 @@ func secureRandomInt(max int) (int, error) { return int(n.Int64()), nil } +// ============================================================================ +// API处理器 +// ============================================================================ + // CaptchaHandler 生成验证码图片 // GET /admin/captcha - 返回验证码图片 func CaptchaHandler(c *gin.Context) { @@ -87,8 +99,6 @@ func CaptchaHandler(c *gin.Context) { c.Data(http.StatusOK, "image/png", imgData) } - - // VerifyCaptcha 验证验证码 // 这个函数将在登录处理中被调用 // 支持大小写不敏感匹配 @@ -97,7 +107,7 @@ func VerifyCaptcha(c *gin.Context, captchaValue string) bool { if middleware.ShouldSkipCaptcha(c) { return true } - + // 从cookie中获取验证码ID captchaId, err := c.Cookie("captcha_id") if err != nil { diff --git a/controllers/admin/function.go b/controllers/admin/function.go index cd208d8..0285bb2 100644 --- a/controllers/admin/function.go +++ b/controllers/admin/function.go @@ -12,9 +12,17 @@ import ( "github.com/sirupsen/logrus" ) +// ============================================================================ +// 全局变量 +// ============================================================================ + // 创建基础控制器实例 var functionBaseController = controllers.NewBaseController() +// ============================================================================ +// 页面处理器 +// ============================================================================ + // FunctionFragmentHandler 公共函数列表页面片段处理器 func FunctionFragmentHandler(c *gin.Context) { c.HTML(http.StatusOK, "functions.html", gin.H{ @@ -22,6 +30,10 @@ func FunctionFragmentHandler(c *gin.Context) { }) } +// ============================================================================ +// API处理器 +// ============================================================================ + // FunctionListHandler 函数列表API处理器 func FunctionListHandler(c *gin.Context) { // 获取分页参数 @@ -321,4 +333,4 @@ func FunctionsBatchDeleteHandler(c *gin.Context) { logrus.WithField("function_ids", req.IDs).Info("Successfully batch deleted functions") functionBaseController.HandleSuccess(c, "批量删除成功", nil) -} \ No newline at end of file +} diff --git a/controllers/admin/handlers.go b/controllers/admin/handlers.go index 39f7a65..836297c 100644 --- a/controllers/admin/handlers.go +++ b/controllers/admin/handlers.go @@ -14,9 +14,17 @@ import ( "github.com/spf13/viper" ) +// ============================================================================ +// 全局变量 +// ============================================================================ + // 创建基础控制器实例 var handlersBaseController = controllers.NewBaseController() +// ============================================================================ +// 辅助函数 +// ============================================================================ + // formatDBType 格式化数据库类型显示 // 将配置文件中的小写类型转换为友好的显示格式 func formatDBType(dbType string) string { @@ -34,6 +42,10 @@ func formatDBType(dbType string) string { } } +// ============================================================================ +// 页面处理器 +// ============================================================================ + // AdminIndexHandler 后台首页处理器/admin 与 /admin/ 根路径入口 // - 未登录:重定向到 /admin/login // - 已登录:渲染后台布局页(或重定向到 /admin/layout) @@ -107,6 +119,10 @@ func DashboardFragmentHandler(c *gin.Context) { c.HTML(http.StatusOK, "dashboard.html", data) } +// ============================================================================ +// API处理器 +// ============================================================================ + // SystemInfoHandler 系统信息API接口 // - 返回系统运行状态的JSON数据,用于前端定时刷新 func SystemInfoHandler(c *gin.Context) { diff --git a/controllers/admin/settings.go b/controllers/admin/settings.go index 0f5feb0..6ffca22 100644 --- a/controllers/admin/settings.go +++ b/controllers/admin/settings.go @@ -1,134 +1,146 @@ -package admin - -import ( - "context" - "fmt" - "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" - "net/http" - "networkDev/controllers" - "networkDev/models" - "networkDev/services" - "networkDev/utils" -) - -// 创建基础控制器实例 -var settingsBaseController = controllers.NewBaseController() - -// SettingsFragmentHandler 设置片段渲染 -// - 渲染设置表单(通过前端JS调用API加载/保存) -func SettingsFragmentHandler(c *gin.Context) { - c.HTML(http.StatusOK, "settings.html", gin.H{}) -} - -// SettingsQueryHandler 设置查询API -// - 返回所有设置项的 name:value 映射 -func SettingsQueryHandler(c *gin.Context) { - db, ok := settingsBaseController.GetDB(c) - if !ok { - return - } - var list []models.Settings - if err := db.Find(&list).Error; err != nil { - settingsBaseController.HandleInternalError(c, "查询失败", err) - return - } - res := map[string]string{} - for _, s := range list { - res[s.Name] = s.Value - } - settingsBaseController.HandleSuccess(c, "ok", res) -} - -// SettingsUpdateHandler 更新系统设置处理器 -// - 接收JSON格式的设置数据,支持两种格式: -// 1. 直接字段格式: {"site_title": "值", "site_keywords": "值"} -// 2. 嵌套格式: {"settings": {"site_title": "值", "site_keywords": "值"}} -// -// - 自动创建不存在的设置项 -// - 更新已存在的设置项 -// - 更新完成后: -// 1. 删除对应的Redis缓存键,确保后续读取走数据库并重建缓存 -// 2. 刷新SettingsService内存缓存 -func SettingsUpdateHandler(c *gin.Context) { - // 先尝试解析为直接字段格式 - var directBody map[string]interface{} - if !settingsBaseController.BindJSON(c, &directBody) { - return - } - - // 提取设置数据 - var settingsData map[string]string - - // 检查是否为嵌套格式(包含settings字段) - if settings, exists := directBody["settings"]; exists { - if settingsMap, ok := settings.(map[string]interface{}); ok { - settingsData = make(map[string]string) - for k, v := range settingsMap { - if str, ok := v.(string); ok { - settingsData[k] = str - } - } - } else { - settingsBaseController.HandleValidationError(c, "settings字段格式错误") - return - } - } else { - // 直接字段格式 - settingsData = make(map[string]string) - for k, v := range directBody { - if str, ok := v.(string); ok { - settingsData[k] = str - } else if v != nil { - // 转换其他类型为字符串 - settingsData[k] = fmt.Sprintf("%v", v) - } - } - } - - if len(settingsData) == 0 { - settingsBaseController.HandleValidationError(c, "无设置项") - return - } - - db, ok := settingsBaseController.GetDB(c) - if !ok { - return - } - - // 记录需要失效的缓存键,统一删除,减少与Redis交互次数 - keysToDel := make([]string, 0, len(settingsData)) - - // 批量处理设置项 - for k, v := range settingsData { - var s models.Settings - if err := db.Where("name = ?", k).First(&s).Error; err != nil { - // 不存在则创建 - s = models.Settings{Name: k, Value: v} - if err := db.Create(&s).Error; err != nil { - logrus.WithError(err).WithField("setting_name", k).Error("创建设置失败") - settingsBaseController.HandleInternalError(c, fmt.Sprintf("保存设置 %s 失败", k), err) - return - } - - } else { - // 存在则更新 - if err := db.Model(&models.Settings{}).Where("id = ?", s.ID).Update("value", v).Error; err != nil { - logrus.WithError(err).WithField("setting_name", k).Error("更新设置失败") - settingsBaseController.HandleInternalError(c, fmt.Sprintf("更新设置 %s 失败", k), err) - return - } - - } - // 收集对应的Redis缓存键(与services/query.go中的键命名保持一致) - keysToDel = append(keysToDel, fmt.Sprintf("setting:%s", k)) - } - - // 删除Redis缓存键(如果Redis不可用则静默跳过) - _ = utils.RedisDel(context.Background(), keysToDel...) - - // 刷新内存中的设置缓存,保证后续读取一致 - services.GetSettingsService().RefreshCache() - - settingsBaseController.HandleSuccess(c, "保存成功", nil) -} +package admin + +import ( + "context" + "fmt" + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + "net/http" + "networkDev/controllers" + "networkDev/models" + "networkDev/services" + "networkDev/utils" +) + +// ============================================================================ +// 全局变量 +// ============================================================================ + +// 创建基础控制器实例 +var settingsBaseController = controllers.NewBaseController() + +// ============================================================================ +// 页面处理器 +// ============================================================================ + +// SettingsFragmentHandler 设置片段渲染 +// - 渲染设置表单(通过前端JS调用API加载/保存) +func SettingsFragmentHandler(c *gin.Context) { + c.HTML(http.StatusOK, "settings.html", gin.H{}) +} + +// ============================================================================ +// API处理器 +// ============================================================================ + +// SettingsQueryHandler 设置查询API +// - 返回所有设置项的 name:value 映射 +func SettingsQueryHandler(c *gin.Context) { + db, ok := settingsBaseController.GetDB(c) + if !ok { + return + } + var list []models.Settings + if err := db.Find(&list).Error; err != nil { + settingsBaseController.HandleInternalError(c, "查询失败", err) + return + } + res := map[string]string{} + for _, s := range list { + res[s.Name] = s.Value + } + settingsBaseController.HandleSuccess(c, "ok", res) +} + +// SettingsUpdateHandler 更新系统设置处理器 +// - 接收JSON格式的设置数据,支持两种格式: +// 1. 直接字段格式: {"site_title": "值", "site_keywords": "值"} +// 2. 嵌套格式: {"settings": {"site_title": "值", "site_keywords": "值"}} +// +// - 自动创建不存在的设置项 +// - 更新已存在的设置项 +// - 更新完成后: +// 1. 删除对应的Redis缓存键,确保后续读取走数据库并重建缓存 +// 2. 刷新SettingsService内存缓存 +func SettingsUpdateHandler(c *gin.Context) { + // 先尝试解析为直接字段格式 + var directBody map[string]interface{} + if !settingsBaseController.BindJSON(c, &directBody) { + return + } + + // 提取设置数据 + var settingsData map[string]string + + // 检查是否为嵌套格式(包含settings字段) + if settings, exists := directBody["settings"]; exists { + if settingsMap, ok := settings.(map[string]interface{}); ok { + settingsData = make(map[string]string) + for k, v := range settingsMap { + if str, ok := v.(string); ok { + settingsData[k] = str + } + } + } else { + settingsBaseController.HandleValidationError(c, "settings字段格式错误") + return + } + } else { + // 直接字段格式 + settingsData = make(map[string]string) + for k, v := range directBody { + if str, ok := v.(string); ok { + settingsData[k] = str + } else if v != nil { + // 转换其他类型为字符串 + settingsData[k] = fmt.Sprintf("%v", v) + } + } + } + + if len(settingsData) == 0 { + settingsBaseController.HandleValidationError(c, "无设置项") + return + } + + db, ok := settingsBaseController.GetDB(c) + if !ok { + return + } + + // 记录需要失效的缓存键,统一删除,减少与Redis交互次数 + keysToDel := make([]string, 0, len(settingsData)) + + // 批量处理设置项 + for k, v := range settingsData { + var s models.Settings + if err := db.Where("name = ?", k).First(&s).Error; err != nil { + // 不存在则创建 + s = models.Settings{Name: k, Value: v} + if err := db.Create(&s).Error; err != nil { + logrus.WithError(err).WithField("setting_name", k).Error("创建设置失败") + settingsBaseController.HandleInternalError(c, fmt.Sprintf("保存设置 %s 失败", k), err) + return + } + + } else { + // 存在则更新 + if err := db.Model(&models.Settings{}).Where("id = ?", s.ID).Update("value", v).Error; err != nil { + logrus.WithError(err).WithField("setting_name", k).Error("更新设置失败") + settingsBaseController.HandleInternalError(c, fmt.Sprintf("更新设置 %s 失败", k), err) + return + } + + } + // 收集对应的Redis缓存键(与services/query.go中的键命名保持一致) + keysToDel = append(keysToDel, fmt.Sprintf("setting:%s", k)) + } + + // 删除Redis缓存键(如果Redis不可用则静默跳过) + _ = utils.RedisDel(context.Background(), keysToDel...) + + // 刷新内存中的设置缓存,保证后续读取一致 + services.GetSettingsService().RefreshCache() + + settingsBaseController.HandleSuccess(c, "保存成功", nil) +} diff --git a/controllers/admin/user.go b/controllers/admin/user.go index 16cfb37..1ddcfff 100644 --- a/controllers/admin/user.go +++ b/controllers/admin/user.go @@ -10,15 +10,27 @@ import ( "github.com/gin-gonic/gin" ) +// ============================================================================ +// 全局变量 +// ============================================================================ + // 创建基础控制器实例 var baseController = controllers.NewBaseController() +// ============================================================================ +// 页面处理器 +// ============================================================================ + // UserFragmentHandler 个人资料片段渲染 // - 渲染个人资料与修改密码表单 func UserFragmentHandler(c *gin.Context) { c.HTML(http.StatusOK, "user.html", gin.H{}) } +// ============================================================================ +// API处理器 +// ============================================================================ + // UserProfileQueryHandler 获取当前登录管理员的用户名 // - 返回 JSON: {username} // - 直接从JWT获取用户名信息 @@ -51,20 +63,20 @@ func UserPasswordUpdateHandler(c *gin.Context) { NewPassword string `json:"new_password"` ConfirmPassword string `json:"confirm_password"` } - + if !baseController.BindJSON(c, &body) { return } // 基础校验 if !baseController.ValidateRequired(c, map[string]interface{}{ - "旧密码": body.OldPassword, - "新密码": body.NewPassword, + "旧密码": body.OldPassword, + "新密码": body.NewPassword, "确认密码": body.ConfirmPassword, }) { return } - + if len(body.NewPassword) < 6 { baseController.HandleValidationError(c, "新密码长度不能少于6位") return @@ -253,7 +265,7 @@ func UserProfileUpdateHandler(c *gin.Context) { // 重新签发JWT并写入Cookie // 创建虚拟用户对象用于生成JWT令牌 adminUser := models.User{ - Username: username, // 使用新的用户名 + Username: username, // 使用新的用户名 Password: adminPassword, PasswordSalt: adminPasswordSalt, } diff --git a/controllers/admin/variable.go b/controllers/admin/variable.go index 4f9763f..4c59ad7 100644 --- a/controllers/admin/variable.go +++ b/controllers/admin/variable.go @@ -12,9 +12,17 @@ import ( "github.com/sirupsen/logrus" ) +// ============================================================================ +// 全局变量 +// ============================================================================ + // 创建基础控制器实例 var variableBaseController = controllers.NewBaseController() +// ============================================================================ +// 页面处理器 +// ============================================================================ + // VariableFragmentHandler 公共变量列表页面片段处理器 func VariableFragmentHandler(c *gin.Context) { c.HTML(http.StatusOK, "variables.html", gin.H{ @@ -22,6 +30,10 @@ func VariableFragmentHandler(c *gin.Context) { }) } +// ============================================================================ +// API处理器 +// ============================================================================ + // VariableListHandler 变量列表API处理器 func VariableListHandler(c *gin.Context) { // 获取分页参数 diff --git a/controllers/base.go b/controllers/base.go index 4b55b2d..c496c0e 100644 --- a/controllers/base.go +++ b/controllers/base.go @@ -10,14 +10,26 @@ import ( "gorm.io/gorm" ) +// ============================================================================ +// 结构体定义 +// ============================================================================ + // BaseController 基础控制器结构体 type BaseController struct{} +// ============================================================================ +// 构造函数 +// ============================================================================ + // NewBaseController 创建基础控制器实例 func NewBaseController() *BaseController { return &BaseController{} } +// ============================================================================ +// 数据库相关方法 +// ============================================================================ + // GetDB 获取数据库连接,统一错误处理 func (bc *BaseController) GetDB(c *gin.Context) (*gorm.DB, bool) { db, err := database.GetDB() @@ -28,6 +40,10 @@ func (bc *BaseController) GetDB(c *gin.Context) (*gorm.DB, bool) { return db, true } +// ============================================================================ +// 错误处理方法 +// ============================================================================ + // HandleDatabaseError 统一处理数据库连接错误 func (bc *BaseController) HandleDatabaseError(c *gin.Context, err error) { c.JSON(http.StatusInternalServerError, gin.H{ @@ -64,6 +80,10 @@ func (bc *BaseController) HandleInternalError(c *gin.Context, message string, er }) } +// ============================================================================ +// 成功响应方法 +// ============================================================================ + // HandleSuccess 统一处理成功响应 func (bc *BaseController) HandleSuccess(c *gin.Context, message string, data interface{}) { c.JSON(http.StatusOK, gin.H{ @@ -82,6 +102,10 @@ func (bc *BaseController) HandleCreated(c *gin.Context, message string, data int }) } +// ============================================================================ +// 辅助方法 +// ============================================================================ + // ValidateRequired 验证必填字段 func (bc *BaseController) ValidateRequired(c *gin.Context, fields map[string]interface{}) bool { for fieldName, fieldValue := range fields { diff --git a/controllers/home/home.go b/controllers/home/home.go index f9f3378..76df9c0 100644 --- a/controllers/home/home.go +++ b/controllers/home/home.go @@ -10,8 +10,16 @@ import ( "gorm.io/gorm" ) +// ============================================================================ +// 全局变量 +// ============================================================================ + var homeBaseController = controllers.NewBaseController() +// ============================================================================ +// 辅助函数 +// ============================================================================ + // getSettingValue 获取配置值,优先从数据库获取,不存在时使用默认值 func getSettingValue(settingName string, defaultValue string, db *gorm.DB) string { if setting, err := services.FindSettingByName(settingName, db); err == nil { @@ -20,6 +28,10 @@ func getSettingValue(settingName string, defaultValue string, db *gorm.DB) strin return defaultValue } +// ============================================================================ +// 页面处理器 +// ============================================================================ + // RootHandler 主页处理器 func RootHandler(c *gin.Context) { // 获取数据库连接 diff --git a/database/database.go b/database/database.go index 58210b8..d3f6f98 100644 --- a/database/database.go +++ b/database/database.go @@ -1,124 +1,136 @@ -package database - -import ( - "fmt" - "networkDev/utils" - "sync" - - "github.com/glebarez/sqlite" - "github.com/sirupsen/logrus" - "github.com/spf13/viper" - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -var ( - // dbInstance 全局 *gorm.DB 实例,使用单例确保全局复用 - dbInstance *gorm.DB - // once 确保初始化只执行一次 - once sync.Once -) - -// Init 初始化数据库连接(根据配置自动选择驱动) -// - 默认使用 SQLite(github.com/glebarez/sqlite) -// - 生产环境支持 MySQL(gorm.io/driver/mysql) -func Init() (*gorm.DB, error) { - var initErr error - once.Do(func() { - dbType := viper.GetString("database.type") - switch dbType { - case "mysql": - initErr = initMySQL() - default: - initErr = initSQLite() - } - - // 如果数据库初始化成功,配置连接池和启动健康检查 - if initErr == nil && dbInstance != nil { - // 加载数据库配置 - var configPrefix string - if dbType == "mysql" { - configPrefix = "database.mysql" - } else { - configPrefix = "database.sqlite" - } - - dbConfig := utils.LoadDatabaseConfig(configPrefix) - - // 验证配置 - if err := utils.ValidateDatabaseConfig(dbConfig); err != nil { - logrus.WithError(err).Warn("数据库配置验证失败,使用默认配置") - dbConfig = utils.GetDefaultDatabaseConfig() - } - - // 配置连接池 - if err := utils.ConfigureConnectionPool(dbInstance, dbConfig); err != nil { - logrus.WithError(err).Error("配置数据库连接池失败") - } - - // 启动健康检查 - utils.StartHealthCheck(dbInstance, dbConfig) - } - }) - return dbInstance, initErr -} - -// GetDB 获取全局 *gorm.DB 实例 -// 如果未初始化,会尝试初始化一次 -func GetDB() (*gorm.DB, error) { - if dbInstance != nil { - return dbInstance, nil - } - return Init() -} - -// initSQLite 初始化 SQLite 数据库 -// 使用 viper 中的 database.sqlite.path 作为数据库文件路径 -func initSQLite() error { - path := viper.GetString("database.sqlite.path") - if path == "" { - path = "./database.db" - } - dsn := fmt.Sprintf("file:%s?cache=shared&_busy_timeout=5000&_fk=1", path) - db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) - if err != nil { - logrus.WithError(err).Error("SQLite 初始化失败") - return err - } - - // SQLite 连接池配置(SQLite 对连接池支持有限,但仍可设置基本参数) - if sqlDB, err := db.DB(); err == nil { - // SQLite 通常使用单连接,但可以设置一些基本参数 - sqlDB.SetMaxOpenConns(1) // SQLite 建议使用单连接 - sqlDB.SetMaxIdleConns(1) - } - - dbInstance = db - logrus.WithField("path", path).Info("SQLite 连接已建立") - return nil -} - -// initMySQL 初始化 MySQL 数据库 -// 从 viper 读取 database.mysql.* 配置构建 DSN -func initMySQL() error { - host := viper.GetString("database.mysql.host") - port := viper.GetInt("database.mysql.port") - user := viper.GetString("database.mysql.username") - pass := viper.GetString("database.mysql.password") - dbname := viper.GetString("database.mysql.database") - charset := viper.GetString("database.mysql.charset") - if charset == "" { - charset = "utf8mb4" - } - - dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local", user, pass, host, port, dbname, charset) - db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) - if err != nil { - logrus.WithError(err).Error("MySQL 初始化失败") - return err - } - - dbInstance = db - logrus.WithField("host", host).WithField("database", dbname).Info("MySQL 连接已建立") - return nil -} +package database + +import ( + "fmt" + "networkDev/utils" + "sync" + + "github.com/glebarez/sqlite" + "github.com/sirupsen/logrus" + "github.com/spf13/viper" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +// ============================================================================ +// 全局变量 +// ============================================================================ + +var ( + // dbInstance 全局 *gorm.DB 实例,使用单例确保全局复用 + dbInstance *gorm.DB + // once 确保初始化只执行一次 + once sync.Once +) + +// ============================================================================ +// 公共函数 +// ============================================================================ + +// Init 初始化数据库连接(根据配置自动选择驱动) +// - 默认使用 SQLite(github.com/glebarez/sqlite) +// - 生产环境支持 MySQL(gorm.io/driver/mysql) +func Init() (*gorm.DB, error) { + var initErr error + once.Do(func() { + dbType := viper.GetString("database.type") + switch dbType { + case "mysql": + initErr = initMySQL() + default: + initErr = initSQLite() + } + + // 如果数据库初始化成功,配置连接池和启动健康检查 + if initErr == nil && dbInstance != nil { + // 加载数据库配置 + var configPrefix string + if dbType == "mysql" { + configPrefix = "database.mysql" + } else { + configPrefix = "database.sqlite" + } + + dbConfig := utils.LoadDatabaseConfig(configPrefix) + + // 验证配置 + if err := utils.ValidateDatabaseConfig(dbConfig); err != nil { + logrus.WithError(err).Warn("数据库配置验证失败,使用默认配置") + dbConfig = utils.GetDefaultDatabaseConfig() + } + + // 配置连接池 + if err := utils.ConfigureConnectionPool(dbInstance, dbConfig); err != nil { + logrus.WithError(err).Error("配置数据库连接池失败") + } + + // 启动健康检查 + utils.StartHealthCheck(dbInstance, dbConfig) + } + }) + return dbInstance, initErr +} + +// GetDB 获取全局 *gorm.DB 实例 +// 如果未初始化,会尝试初始化一次 +func GetDB() (*gorm.DB, error) { + if dbInstance != nil { + return dbInstance, nil + } + return Init() +} + +// ============================================================================ +// 私有函数 +// ============================================================================ + +// initSQLite 初始化 SQLite 数据库 +// 使用 viper 中的 database.sqlite.path 作为数据库文件路径 +func initSQLite() error { + path := viper.GetString("database.sqlite.path") + if path == "" { + path = "./database.db" + } + dsn := fmt.Sprintf("file:%s?cache=shared&_busy_timeout=5000&_fk=1", path) + db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) + if err != nil { + logrus.WithError(err).Error("SQLite 初始化失败") + return err + } + + // SQLite 连接池配置(SQLite 对连接池支持有限,但仍可设置基本参数) + if sqlDB, err := db.DB(); err == nil { + // SQLite 通常使用单连接,但可以设置一些基本参数 + sqlDB.SetMaxOpenConns(1) // SQLite 建议使用单连接 + sqlDB.SetMaxIdleConns(1) + } + + dbInstance = db + logrus.WithField("path", path).Info("SQLite 连接已建立") + return nil +} + +// initMySQL 初始化 MySQL 数据库 +// 从 viper 读取 database.mysql.* 配置构建 DSN +func initMySQL() error { + host := viper.GetString("database.mysql.host") + port := viper.GetInt("database.mysql.port") + user := viper.GetString("database.mysql.username") + pass := viper.GetString("database.mysql.password") + dbname := viper.GetString("database.mysql.database") + charset := viper.GetString("database.mysql.charset") + if charset == "" { + charset = "utf8mb4" + } + + dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local", user, pass, host, port, dbname, charset) + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) + if err != nil { + logrus.WithError(err).Error("MySQL 初始化失败") + return err + } + + dbInstance = db + logrus.WithField("host", host).WithField("database", dbname).Info("MySQL 连接已建立") + return nil +} diff --git a/database/migrate.go b/database/migrate.go index 5d539f1..6aadfb2 100644 --- a/database/migrate.go +++ b/database/migrate.go @@ -1,172 +1,180 @@ -package database - -import ( - "fmt" - "networkDev/models" - "strings" - - "github.com/sirupsen/logrus" - "gorm.io/gorm" -) - -// AutoMigrate 自动迁移数据库模型 -// - 会确保必要的数据表结构存在 -// - 不会破坏已有数据 -func AutoMigrate() error { - db, err := GetDB() - if err != nil { - return err - } - if err := db.AutoMigrate(&models.User{}, &models.Settings{}, &models.App{}, &models.API{}, &models.Variable{}, &models.Function{}); err != nil { - logrus.WithError(err).Error("AutoMigrate 执行失败") - return err - } - - // 兼容迁移:如果 users.password_salt 列长度 < 64,则扩大到 64 - if err := ensureUserPasswordSaltLength(db); err != nil { - logrus.WithError(err).Error("调整 users.password_salt 列长度失败") - return err - } - - // 兼容迁移:确保 tasks.verification_code 字段类型为 LONGTEXT 以支持大图片数据 - if err := ensureVerificationCodeType(db); err != nil { - logrus.WithError(err).Error("调整 tasks.verification_code 字段类型失败") - return err - } - - logrus.Info("AutoMigrate 执行完成") - return nil -} - -// ensureVerificationCodeType 确保tasks.verification_code字段类型为LONGTEXT以支持大图片数据 -// 中文注释:检查并修改verification_code字段类型,支持Base64编码的大图片数据存储 -func ensureVerificationCodeType(db *gorm.DB) error { - // 获取数据库方言类型 - dialector := db.Dialector.Name() - - // 根据不同数据库类型执行不同的检查逻辑 - switch dialector { - case "mysql": - // MySQL/MariaDB使用INFORMATION_SCHEMA - var result struct { - ColumnName string `gorm:"column:COLUMN_NAME"` - ColumnType string `gorm:"column:COLUMN_TYPE"` - } - - err := db.Raw("SELECT COLUMN_NAME, COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ? LIMIT 1", - "tasks", "verification_code").Scan(&result).Error - - if err != nil { - return nil // 查询失败则跳过 - } - - // 检查列类型,如果不是LONGTEXT则修改 - if !strings.Contains(strings.ToLower(result.ColumnType), "longtext") { - alterSQL := "ALTER TABLE tasks MODIFY COLUMN verification_code LONGTEXT" - if err := db.Exec(alterSQL).Error; err != nil { - return fmt.Errorf("修改verification_code字段类型失败: %v", err) - } - logrus.Info("verification_code字段类型已更新为LONGTEXT") - } - case "sqlite": - // SQLite使用pragma_table_info检查列信息 - var columns []struct { - CID int `gorm:"column:cid"` - Name string `gorm:"column:name"` - Type string `gorm:"column:type"` - NotNull int `gorm:"column:notnull"` - DfltValue *string `gorm:"column:dflt_value"` - PK int `gorm:"column:pk"` - } - - err := db.Raw("PRAGMA table_info(tasks)").Scan(&columns).Error - if err != nil { - return nil // 查询失败则跳过 - } - - // 查找verification_code列 - for _, col := range columns { - if col.Name == "verification_code" { - // SQLite中,如果列类型不是TEXT,需要重建表 - if !strings.Contains(strings.ToLower(col.Type), "text") { - // SQLite不支持直接修改列类型,但GORM的AutoMigrate会处理这种情况 - logrus.Info("SQLite检测到verification_code字段类型需要更新,依赖GORM AutoMigrate处理") - } - break - } - } - default: - // 其他数据库类型暂不处理 - logrus.Infof("数据库类型 %s 暂不支持verification_code字段类型检查", dialector) - } - - return nil -} - -// ensureUserPasswordSaltLength 确保users.password_salt列长度至少为64 -// 中文注释:检查并修改password_salt列长度,兼容32字节(64十六进制字符)的盐值 -func ensureUserPasswordSaltLength(db *gorm.DB) error { - // 获取数据库方言类型 - dialector := db.Dialector.Name() - - // 根据不同数据库类型执行不同的检查逻辑 - switch dialector { - case "mysql": - // MySQL/MariaDB使用INFORMATION_SCHEMA - var result struct { - ColumnName string `gorm:"column:COLUMN_NAME"` - ColumnType string `gorm:"column:COLUMN_TYPE"` - } - - err := db.Raw("SELECT COLUMN_NAME, COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ? LIMIT 1", - "users", "password_salt").Scan(&result).Error - - if err != nil { - return nil // 查询失败则跳过 - } - - // 检查列类型,如果长度小于64则修改 - if strings.Contains(strings.ToLower(result.ColumnType), "varchar") { - if strings.Contains(result.ColumnType, "(32)") || strings.Contains(result.ColumnType, "(16)") { - alterSQL := "ALTER TABLE users MODIFY COLUMN password_salt VARCHAR(64)" - if err := db.Exec(alterSQL).Error; err != nil { - return fmt.Errorf("修改password_salt列长度失败: %v", err) - } - logrus.Info("password_salt列长度已更新为64") - } - } - case "sqlite": - // SQLite使用pragma_table_info检查列信息 - var columns []struct { - CID int `gorm:"column:cid"` - Name string `gorm:"column:name"` - Type string `gorm:"column:type"` - NotNull int `gorm:"column:notnull"` - DfltValue *string `gorm:"column:dflt_value"` - PK int `gorm:"column:pk"` - } - - err := db.Raw("PRAGMA table_info(users)").Scan(&columns).Error - if err != nil { - return nil // 查询失败则跳过 - } - - // 查找password_salt列 - for _, col := range columns { - if col.Name == "password_salt" { - // SQLite中,如果列类型包含长度限制且小于64,需要重建表 - if strings.Contains(strings.ToLower(col.Type), "varchar(32)") || - strings.Contains(strings.ToLower(col.Type), "varchar(16)") { - // SQLite不支持直接修改列类型,但GORM的AutoMigrate会处理这种情况 - logrus.Info("SQLite检测到password_salt列长度需要更新,依赖GORM AutoMigrate处理") - } - break - } - } - default: - // 其他数据库类型暂不处理 - logrus.Infof("数据库类型 %s 暂不支持password_salt列长度检查", dialector) - } - - return nil -} +package database + +import ( + "fmt" + "networkDev/models" + "strings" + + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +// ============================================================================ +// 公共函数 +// ============================================================================ + +// AutoMigrate 自动迁移数据库模型 +// - 会确保必要的数据表结构存在 +// - 不会破坏已有数据 +func AutoMigrate() error { + db, err := GetDB() + if err != nil { + return err + } + if err := db.AutoMigrate(&models.User{}, &models.Settings{}, &models.App{}, &models.API{}, &models.Variable{}, &models.Function{}); err != nil { + logrus.WithError(err).Error("AutoMigrate 执行失败") + return err + } + + // 兼容迁移:如果 users.password_salt 列长度 < 64,则扩大到 64 + if err := ensureUserPasswordSaltLength(db); err != nil { + logrus.WithError(err).Error("调整 users.password_salt 列长度失败") + return err + } + + // 兼容迁移:确保 tasks.verification_code 字段类型为 LONGTEXT 以支持大图片数据 + if err := ensureVerificationCodeType(db); err != nil { + logrus.WithError(err).Error("调整 tasks.verification_code 字段类型失败") + return err + } + + logrus.Info("AutoMigrate 执行完成") + return nil +} + +// ============================================================================ +// 私有函数 +// ============================================================================ + +// ensureVerificationCodeType 确保tasks.verification_code字段类型为LONGTEXT以支持大图片数据 +// 中文注释:检查并修改verification_code字段类型,支持Base64编码的大图片数据存储 +func ensureVerificationCodeType(db *gorm.DB) error { + // 获取数据库方言类型 + dialector := db.Dialector.Name() + + // 根据不同数据库类型执行不同的检查逻辑 + switch dialector { + case "mysql": + // MySQL/MariaDB使用INFORMATION_SCHEMA + var result struct { + ColumnName string `gorm:"column:COLUMN_NAME"` + ColumnType string `gorm:"column:COLUMN_TYPE"` + } + + err := db.Raw("SELECT COLUMN_NAME, COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ? LIMIT 1", + "tasks", "verification_code").Scan(&result).Error + + if err != nil { + return nil // 查询失败则跳过 + } + + // 检查列类型,如果不是LONGTEXT则修改 + if !strings.Contains(strings.ToLower(result.ColumnType), "longtext") { + alterSQL := "ALTER TABLE tasks MODIFY COLUMN verification_code LONGTEXT" + if err := db.Exec(alterSQL).Error; err != nil { + return fmt.Errorf("修改verification_code字段类型失败: %v", err) + } + logrus.Info("verification_code字段类型已更新为LONGTEXT") + } + case "sqlite": + // SQLite使用pragma_table_info检查列信息 + var columns []struct { + CID int `gorm:"column:cid"` + Name string `gorm:"column:name"` + Type string `gorm:"column:type"` + NotNull int `gorm:"column:notnull"` + DfltValue *string `gorm:"column:dflt_value"` + PK int `gorm:"column:pk"` + } + + err := db.Raw("PRAGMA table_info(tasks)").Scan(&columns).Error + if err != nil { + return nil // 查询失败则跳过 + } + + // 查找verification_code列 + for _, col := range columns { + if col.Name == "verification_code" { + // SQLite中,如果列类型不是TEXT,需要重建表 + if !strings.Contains(strings.ToLower(col.Type), "text") { + // SQLite不支持直接修改列类型,但GORM的AutoMigrate会处理这种情况 + logrus.Info("SQLite检测到verification_code字段类型需要更新,依赖GORM AutoMigrate处理") + } + break + } + } + default: + // 其他数据库类型暂不处理 + logrus.Infof("数据库类型 %s 暂不支持verification_code字段类型检查", dialector) + } + + return nil +} + +// ensureUserPasswordSaltLength 确保users.password_salt列长度至少为64 +// 中文注释:检查并修改password_salt列长度,兼容32字节(64十六进制字符)的盐值 +func ensureUserPasswordSaltLength(db *gorm.DB) error { + // 获取数据库方言类型 + dialector := db.Dialector.Name() + + // 根据不同数据库类型执行不同的检查逻辑 + switch dialector { + case "mysql": + // MySQL/MariaDB使用INFORMATION_SCHEMA + var result struct { + ColumnName string `gorm:"column:COLUMN_NAME"` + ColumnType string `gorm:"column:COLUMN_TYPE"` + } + + err := db.Raw("SELECT COLUMN_NAME, COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ? LIMIT 1", + "users", "password_salt").Scan(&result).Error + + if err != nil { + return nil // 查询失败则跳过 + } + + // 检查列类型,如果长度小于64则修改 + if strings.Contains(strings.ToLower(result.ColumnType), "varchar") { + if strings.Contains(result.ColumnType, "(32)") || strings.Contains(result.ColumnType, "(16)") { + alterSQL := "ALTER TABLE users MODIFY COLUMN password_salt VARCHAR(64)" + if err := db.Exec(alterSQL).Error; err != nil { + return fmt.Errorf("修改password_salt列长度失败: %v", err) + } + logrus.Info("password_salt列长度已更新为64") + } + } + case "sqlite": + // SQLite使用pragma_table_info检查列信息 + var columns []struct { + CID int `gorm:"column:cid"` + Name string `gorm:"column:name"` + Type string `gorm:"column:type"` + NotNull int `gorm:"column:notnull"` + DfltValue *string `gorm:"column:dflt_value"` + PK int `gorm:"column:pk"` + } + + err := db.Raw("PRAGMA table_info(users)").Scan(&columns).Error + if err != nil { + return nil // 查询失败则跳过 + } + + // 查找password_salt列 + for _, col := range columns { + if col.Name == "password_salt" { + // SQLite中,如果列类型包含长度限制且小于64,需要重建表 + if strings.Contains(strings.ToLower(col.Type), "varchar(32)") || + strings.Contains(strings.ToLower(col.Type), "varchar(16)") { + // SQLite不支持直接修改列类型,但GORM的AutoMigrate会处理这种情况 + logrus.Info("SQLite检测到password_salt列长度需要更新,依赖GORM AutoMigrate处理") + } + break + } + } + default: + // 其他数据库类型暂不处理 + logrus.Infof("数据库类型 %s 暂不支持password_salt列长度检查", dialector) + } + + return nil +} diff --git a/database/settings.go b/database/settings.go index 0ebc393..42ccffd 100644 --- a/database/settings.go +++ b/database/settings.go @@ -1,178 +1,186 @@ -package database - -import ( - "networkDev/models" - "networkDev/utils" - - "github.com/sirupsen/logrus" - "gorm.io/gorm" -) - -// SeedDefaultSettings 初始化默认系统设置 -// - 检查各项设置是否已存在,如不存在则创建默认值 -// - 包含站点基本信息、SEO设置等常用配置项 -func SeedDefaultSettings() error { - db, err := GetDB() - if err != nil { - return err - } - - // 定义默认设置项 - defaultSettings := []models.Settings{ - { - Name: "site_title", - Value: "凌动技术", - Description: "网站标题,显示在浏览器标题栏和页面顶部", - }, - { - Name: "site_keywords", - Value: "验证,网络,管理系统,网络验证,卡密管理,账户管理", - Description: "网站关键词,用于SEO优化,多个关键词用逗号分隔", - }, - { - Name: "site_description", - Value: "专业的网络验证管理系统,提供便捷的在线网络验证服务和设备管理功能", - Description: "网站描述,用于SEO优化和社交媒体分享", - }, - { - Name: "site_logo", - Value: "/favicon.ico", - Description: "网站Logo图片路径", - }, - { - Name: "contact_email", - Value: "admin@example.com", - Description: "联系邮箱,用于客服和业务咨询", - }, - { - Name: "max_upload_size", - Value: "10485760", - Description: "文件上传最大尺寸(字节),默认10MB", - }, - { - Name: "default_user_role", - Value: "1", - Description: "新用户默认角色,0=管理员,1=普通用户", - }, - { - Name: "session_timeout", - Value: "3600", - Description: "会话超时时间(秒),默认1小时", - }, - { - Name: "maintenance_mode", - Value: "0", - Description: "维护模式,0=关闭维护模式,1=开启维护模式", - }, - // ===== 管理员账号相关默认项 ===== - { - Name: "admin_username", - Value: "admin", - Description: "管理员用户名", - }, - { - Name: "admin_password", - Value: "", - Description: "管理员密码哈希值", - }, - { - Name: "admin_password_salt", - Value: "", - Description: "管理员密码加密盐值", - }, - // ===== 页脚与备案相关默认项 ===== - { - Name: "footer_text", - Value: "Copyright © 2025 凌动技术. All Rights Reserved.", - Description: "页脚展示的版权或说明信息", - }, - { - Name: "icp_record", - Value: "京ICP备12345678号", - Description: "ICP备案号,留空则不显示", - }, - { - Name: "icp_record_link", - Value: "https://beian.miit.gov.cn", - Description: "工信部ICP备案查询链接,留空则不显示", - }, - { - Name: "psb_record", - Value: "京公网安备 11000002000001号", - Description: "公安备案号,留空则不显示", - }, - { - Name: "psb_record_link", - Value: "https://www.beian.gov.cn/portal/registerSystemInfo?recordcode=11000002000001", - Description: "公安备案查询链接,留空则不显示", - }, - } - - // 逐个检查并创建不存在的设置项 - for _, setting := range defaultSettings { - var count int64 - if err := db.Model(&models.Settings{}).Where("name = ?", setting.Name).Count(&count).Error; err != nil { - return err - } - - if count == 0 { - if err := db.Create(&setting).Error; err != nil { - logrus.WithError(err).WithField("name", setting.Name).Error("创建默认设置失败") - return err - } - logrus.WithField("name", setting.Name).WithField("value", setting.Value).Info("创建默认设置项") - } - } - - // 初始化默认管理员账号(如果密码为空) - if err := initDefaultAdmin(db); err != nil { - return err - } - - logrus.Info("默认系统设置初始化完成") - return nil -} - -// initDefaultAdmin 初始化默认管理员账号 -// 如果admin_password为空,则生成默认密码admin123的哈希值 -func initDefaultAdmin(db *gorm.DB) error { - var passwordSetting models.Settings - if err := db.Where("name = ?", "admin_password").First(&passwordSetting).Error; err != nil { - logrus.WithError(err).Error("获取管理员密码设置失败") - return err - } - - // 如果密码已设置,跳过初始化 - if passwordSetting.Value != "" { - logrus.Debug("管理员密码已设置,跳过默认密码初始化") - return nil - } - - // 生成密码盐值 - salt, err := utils.GenerateRandomSalt() - if err != nil { - logrus.WithError(err).Error("生成密码盐值失败") - return err - } - - // 使用盐值生成密码哈希(默认密码:admin123) - hash, err := utils.HashPasswordWithSalt("admin123", salt) - if err != nil { - logrus.WithError(err).Error("生成密码哈希失败") - return err - } - - // 更新密码和盐值 - if err := db.Model(&models.Settings{}).Where("name = ?", "admin_password").Update("value", hash).Error; err != nil { - logrus.WithError(err).Error("更新管理员密码失败") - return err - } - - if err := db.Model(&models.Settings{}).Where("name = ?", "admin_password_salt").Update("value", salt).Error; err != nil { - logrus.WithError(err).Error("更新管理员密码盐值失败") - return err - } - - logrus.Info("默认管理员账号初始化完成,用户名: admin, 密码: admin123") - return nil -} +package database + +import ( + "networkDev/models" + "networkDev/utils" + + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +// ============================================================================ +// 公共函数 +// ============================================================================ + +// SeedDefaultSettings 初始化默认系统设置 +// - 检查各项设置是否已存在,如不存在则创建默认值 +// - 包含站点基本信息、SEO设置等常用配置项 +func SeedDefaultSettings() error { + db, err := GetDB() + if err != nil { + return err + } + + // 定义默认设置项 + defaultSettings := []models.Settings{ + { + Name: "site_title", + Value: "凌动技术", + Description: "网站标题,显示在浏览器标题栏和页面顶部", + }, + { + Name: "site_keywords", + Value: "验证,网络,管理系统,网络验证,卡密管理,账户管理", + Description: "网站关键词,用于SEO优化,多个关键词用逗号分隔", + }, + { + Name: "site_description", + Value: "专业的网络验证管理系统,提供便捷的在线网络验证服务和设备管理功能", + Description: "网站描述,用于SEO优化和社交媒体分享", + }, + { + Name: "site_logo", + Value: "/favicon.ico", + Description: "网站Logo图片路径", + }, + { + Name: "contact_email", + Value: "admin@example.com", + Description: "联系邮箱,用于客服和业务咨询", + }, + { + Name: "max_upload_size", + Value: "10485760", + Description: "文件上传最大尺寸(字节),默认10MB", + }, + { + Name: "default_user_role", + Value: "1", + Description: "新用户默认角色,0=管理员,1=普通用户", + }, + { + Name: "session_timeout", + Value: "3600", + Description: "会话超时时间(秒),默认1小时", + }, + { + Name: "maintenance_mode", + Value: "0", + Description: "维护模式,0=关闭维护模式,1=开启维护模式", + }, + // ===== 管理员账号相关默认项 ===== + { + Name: "admin_username", + Value: "admin", + Description: "管理员用户名", + }, + { + Name: "admin_password", + Value: "", + Description: "管理员密码哈希值", + }, + { + Name: "admin_password_salt", + Value: "", + Description: "管理员密码加密盐值", + }, + // ===== 页脚与备案相关默认项 ===== + { + Name: "footer_text", + Value: "Copyright © 2025 凌动技术. All Rights Reserved.", + Description: "页脚展示的版权或说明信息", + }, + { + Name: "icp_record", + Value: "京ICP备12345678号", + Description: "ICP备案号,留空则不显示", + }, + { + Name: "icp_record_link", + Value: "https://beian.miit.gov.cn", + Description: "工信部ICP备案查询链接,留空则不显示", + }, + { + Name: "psb_record", + Value: "京公网安备 11000002000001号", + Description: "公安备案号,留空则不显示", + }, + { + Name: "psb_record_link", + Value: "https://www.beian.gov.cn/portal/registerSystemInfo?recordcode=11000002000001", + Description: "公安备案查询链接,留空则不显示", + }, + } + + // 逐个检查并创建不存在的设置项 + for _, setting := range defaultSettings { + var count int64 + if err := db.Model(&models.Settings{}).Where("name = ?", setting.Name).Count(&count).Error; err != nil { + return err + } + + if count == 0 { + if err := db.Create(&setting).Error; err != nil { + logrus.WithError(err).WithField("name", setting.Name).Error("创建默认设置失败") + return err + } + logrus.WithField("name", setting.Name).WithField("value", setting.Value).Debug("创建默认设置项") + } + } + + // 初始化默认管理员账号(如果密码为空) + if err := initDefaultAdmin(db); err != nil { + return err + } + + logrus.Info("默认系统设置初始化完成") + return nil +} + +// ============================================================================ +// 私有函数 +// ============================================================================ + +// initDefaultAdmin 初始化默认管理员账号 +// 如果admin_password为空,则生成默认密码admin123的哈希值 +func initDefaultAdmin(db *gorm.DB) error { + var passwordSetting models.Settings + if err := db.Where("name = ?", "admin_password").First(&passwordSetting).Error; err != nil { + logrus.WithError(err).Error("获取管理员密码设置失败") + return err + } + + // 如果密码已设置,跳过初始化 + if passwordSetting.Value != "" { + logrus.Debug("管理员密码已设置,跳过默认密码初始化") + return nil + } + + // 生成密码盐值 + salt, err := utils.GenerateRandomSalt() + if err != nil { + logrus.WithError(err).Error("生成密码盐值失败") + return err + } + + // 使用盐值生成密码哈希(默认密码:admin123) + hash, err := utils.HashPasswordWithSalt("admin123", salt) + if err != nil { + logrus.WithError(err).Error("生成密码哈希失败") + return err + } + + // 更新密码和盐值 + if err := db.Model(&models.Settings{}).Where("name = ?", "admin_password").Update("value", hash).Error; err != nil { + logrus.WithError(err).Error("更新管理员密码失败") + return err + } + + if err := db.Model(&models.Settings{}).Where("name = ?", "admin_password_salt").Update("value", salt).Error; err != nil { + logrus.WithError(err).Error("更新管理员密码盐值失败") + return err + } + + logrus.Info("默认管理员账号初始化完成,用户名: admin, 密码: admin123") + return nil +} diff --git a/main.go b/main.go index 5cec096..0ee83e5 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,9 @@ -package main - -import "networkDev/cmd" - -// main 是程序的入口点 -// 调用Cobra命令执行器来处理命令行参数和子命令 -func main() { - cmd.Execute() -} +package main + +import "networkDev/cmd" + +// main 是程序的入口点 +// 调用Cobra命令执行器来处理命令行参数和子命令 +func main() { + cmd.Execute() +} diff --git a/middleware/devmode.go b/middleware/devmode.go index 8cfa9ac..0f8d136 100644 --- a/middleware/devmode.go +++ b/middleware/devmode.go @@ -7,6 +7,10 @@ import ( "github.com/spf13/viper" ) +// ============================================================================ +// 结构体定义 +// ============================================================================ + // DevModeConfig 开发模式配置 type DevModeConfig struct { // 是否启用模板热重载 @@ -19,6 +23,10 @@ type DevModeConfig struct { EnableDebugLog bool } +// ============================================================================ +// 中间件函数 +// ============================================================================ + // DevModeMiddleware 开发模式中间件 // 统一管理所有开发模式相关的功能 func DevModeMiddleware(engine *gin.Engine) gin.HandlerFunc { @@ -45,6 +53,10 @@ func DevModeMiddleware(engine *gin.Engine) gin.HandlerFunc { } } +// ============================================================================ +// 公共函数 +// ============================================================================ + // IsDevMode 检查是否为开发模式 func IsDevMode() bool { return viper.GetBool("server.dev_mode") @@ -57,10 +69,10 @@ func GetDevModeConfig() DevModeConfig { } return DevModeConfig{ - EnableTemplateReload: true, // 开发模式下默认启用模板热重载 - SkipCaptcha: true, // 开发模式下默认跳过验证码 - ShowDetailedErrors: true, // 开发模式下显示详细错误 - EnableDebugLog: true, // 开发模式下启用调试日志 + EnableTemplateReload: true, // 开发模式下默认启用模板热重载 + SkipCaptcha: true, // 开发模式下默认跳过验证码 + ShowDetailedErrors: true, // 开发模式下显示详细错误 + EnableDebugLog: true, // 开发模式下启用调试日志 } } @@ -92,9 +104,13 @@ func ShouldSkipCaptcha(c *gin.Context) bool { return config.SkipCaptcha } +// ============================================================================ +// 私有函数 +// ============================================================================ + // reloadTemplates 重新加载模板(内部函数) func reloadTemplates(engine *gin.Engine) { if tmpl, err := web.ParseTemplates(); err == nil { engine.SetHTMLTemplate(tmpl) } -} \ No newline at end of file +} diff --git a/middleware/logging.go b/middleware/logging.go index af88e30..b3a58e4 100644 --- a/middleware/logging.go +++ b/middleware/logging.go @@ -1,97 +1,117 @@ -package middleware - -import ( - "strings" - "time" - - "github.com/gin-gonic/gin" - "networkDev/utils/logger" -) - -// LoggingMiddleware 日志记录中间件结构体 -// 用于记录HTTP请求的详细信息,包括方法、路径、状态码和响应时间 -type LoggingMiddleware struct { - logger *logger.Logger -} - -// NewLoggingMiddleware 创建新的日志记录中间件实例 -func NewLoggingMiddleware(logger *logger.Logger) *LoggingMiddleware { - return &LoggingMiddleware{ - logger: logger, - } -} - -// Handler 返回Gin中间件函数,用于记录HTTP请求日志 -// 记录格式遵循Apache Common Log Format -func (lm *LoggingMiddleware) Handler() gin.HandlerFunc { - return func(c *gin.Context) { - // 记录开始时间 - start := time.Now() - - // 处理请求 - c.Next() - - // 计算处理时间 - duration := time.Since(start) - - // 获取客户端IP - clientIP := getClientIP(c) - - // 记录日志 - Apache Common Log Format - // 使用专门的HTTP日志方法避免User-Agent中的反斜杠被转义 - lm.logger.LogRequestWithHeaders( - c.Request.Method, - c.Request.RequestURI, - clientIP, - c.Writer.Status(), - duration, - "-", // referer (已废弃) - c.Request.UserAgent(), - ) - } -} - -// getClientIP 获取客户端真实IP地址 -// 优先从X-Forwarded-For、X-Real-IP等头部获取,最后使用RemoteAddr -func getClientIP(c *gin.Context) string { - // 检查X-Forwarded-For头部 - xForwardedFor := c.GetHeader("X-Forwarded-For") - if xForwardedFor != "" { - // X-Forwarded-For可能包含多个IP,取第一个 - ips := strings.Split(xForwardedFor, ",") - if len(ips) > 0 { - return strings.TrimSpace(ips[0]) - } - } - - // 检查X-Real-IP头部 - xRealIP := c.GetHeader("X-Real-IP") - if xRealIP != "" { - return xRealIP - } - - // 检查X-Forwarded头部 - xForwarded := c.GetHeader("X-Forwarded") - if xForwarded != "" { - return xForwarded - } - - // 使用RemoteAddr - remoteAddr := c.Request.RemoteAddr - if strings.Contains(remoteAddr, ":") { - // 移除端口号 - if idx := strings.LastIndex(remoteAddr, ":"); idx != -1 { - return remoteAddr[:idx] - } - } - - return remoteAddr -} - -// WrapHandler 创建Gin日志中间件 -// 使用全局日志记录器创建日志中间件 -func WrapHandler() gin.HandlerFunc { - logger := logger.GetLogger() - middleware := NewLoggingMiddleware(logger) - return middleware.Handler() -} +package middleware + +import ( + "strings" + "time" + + "github.com/gin-gonic/gin" + "networkDev/utils/logger" +) + +// ============================================================================ +// 结构体定义 +// ============================================================================ + +// LoggingMiddleware 日志记录中间件结构体 +// 用于记录HTTP请求的详细信息,包括方法、路径、状态码和响应时间 +type LoggingMiddleware struct { + logger *logger.Logger +} + +// ============================================================================ +// 构造函数 +// ============================================================================ + +// NewLoggingMiddleware 创建新的日志记录中间件实例 +func NewLoggingMiddleware(logger *logger.Logger) *LoggingMiddleware { + return &LoggingMiddleware{ + logger: logger, + } +} + +// ============================================================================ +// 中间件函数 +// ============================================================================ + +// Handler 返回Gin中间件函数,用于记录HTTP请求日志 +// 记录格式遵循Apache Common Log Format +func (lm *LoggingMiddleware) Handler() gin.HandlerFunc { + return func(c *gin.Context) { + // 记录开始时间 + start := time.Now() + + // 处理请求 + c.Next() + + // 计算处理时间 + duration := time.Since(start) + + // 获取客户端IP + clientIP := getClientIP(c) + + // 记录日志 - Apache Common Log Format + // 使用专门的HTTP日志方法避免User-Agent中的反斜杠被转义 + lm.logger.LogRequestWithHeaders( + c.Request.Method, + c.Request.RequestURI, + clientIP, + c.Writer.Status(), + duration, + "-", // referer (已废弃) + c.Request.UserAgent(), + ) + } +} + +// ============================================================================ +// 私有函数 +// ============================================================================ + +// getClientIP 获取客户端真实IP地址 +// 优先从X-Forwarded-For、X-Real-IP等头部获取,最后使用RemoteAddr +func getClientIP(c *gin.Context) string { + // 检查X-Forwarded-For头部 + xForwardedFor := c.GetHeader("X-Forwarded-For") + if xForwardedFor != "" { + // X-Forwarded-For可能包含多个IP,取第一个 + ips := strings.Split(xForwardedFor, ",") + if len(ips) > 0 { + return strings.TrimSpace(ips[0]) + } + } + + // 检查X-Real-IP头部 + xRealIP := c.GetHeader("X-Real-IP") + if xRealIP != "" { + return xRealIP + } + + // 检查X-Forwarded头部 + xForwarded := c.GetHeader("X-Forwarded") + if xForwarded != "" { + return xForwarded + } + + // 使用RemoteAddr + remoteAddr := c.Request.RemoteAddr + if strings.Contains(remoteAddr, ":") { + // 移除端口号 + if idx := strings.LastIndex(remoteAddr, ":"); idx != -1 { + return remoteAddr[:idx] + } + } + + return remoteAddr +} + +// ============================================================================ +// 公共函数 +// ============================================================================ + +// WrapHandler 创建Gin日志中间件 +// 使用全局日志记录器创建日志中间件 +func WrapHandler() gin.HandlerFunc { + logger := logger.GetLogger() + middleware := NewLoggingMiddleware(logger) + return middleware.Handler() +} diff --git a/models/api.go b/models/api.go index 7da7df5..7c340ca 100644 --- a/models/api.go +++ b/models/api.go @@ -8,11 +8,63 @@ import ( "gorm.io/gorm" ) +// ============================================================================ +// 常量定义 +// ============================================================================ + +// API类型常量定义 +const ( + // 基础信息 + APITypeGetBulletin = 1 // 获取程序公告 + APITypeGetUpdateUrl = 2 // 获取更新地址 + APITypeCheckAppVersion = 3 // 检测最新版本 + APITypeGetCardInfo = 4 // 获取卡密信息 + + // 卡密相关 + APITypeSingleLogin = 10 // 卡密登录 + + // 账号管理 + APITypeUserLogin = 20 // 用户登录 + APITypeUserRegin = 21 // 用户注册 + APITypeUserRecharge = 22 // 用户充值 + + // 登出操作 + APITypeLogOut = 30 // 退出登录 + + // 状态查询 + APITypeGetExpired = 40 // 获取到期时间 + APITypeCheckUserStatus = 41 // 检测账号状态 + APITypeGetAppData = 42 // 获取程序数据 + APITypeGetVariable = 43 // 获取变量数据 + APITypeExecuteFunction = 44 // 执行远程函数 + + // 用户操作 + APITypeUpdatePwd = 50 // 修改账号密码 + APITypeMacChangeBind = 51 // 机器码转绑 + APITypeIPChangeBind = 52 // IP转绑 + + // 风控操作 + APITypeDisableUser = 60 // 封停用户 + APITypeBlackUser = 61 // 添加黑名单 + APITypeUserDeductedTime = 62 // 扣除时间 +) + +// 算法类型常量 +const ( + AlgorithmNone = 0 // 不加密 + AlgorithmRC4 = 1 // RC4 + AlgorithmRSA = 2 // RSA + AlgorithmRSADynamic = 3 // RSA(动态) + AlgorithmEasy = 4 // 易加密 +) + +// ============================================================================ +// 结构体定义 +// ============================================================================ + // API 接口表模型 // 用于管理API接口的配置信息 -// 包含加密算法配置、密钥管理等功能 -// 支持多种加密算法:不加密、RC4、RSA、RSA(动态) - +// CreatedAt/UpdatedAt 由 GORM 自动维护 type API struct { // ID:主键,自增 ID uint `gorm:"primaryKey;comment:API接口ID,自增主键" json:"id"` @@ -54,6 +106,10 @@ type API struct { UpdatedAt time.Time `gorm:"comment:更新时间" json:"updated_at"` } +// ============================================================================ +// 结构体方法 +// ============================================================================ + // BeforeCreate 在创建记录前自动生成UUID func (api *API) BeforeCreate(tx *gorm.DB) error { if api.UUID == "" { @@ -67,51 +123,9 @@ func (API) TableName() string { return "apis" } -// API类型常量定义 -const ( - // 基础信息获取类API - APITypeGetBulletin = 1 // 获取程序公告 - APITypeGetUpdateUrl = 2 // 获取更新地址 - APITypeCheckAppVersion = 3 // 检测最新版本 - APITypeGetCardInfo = 4 // 获取卡密信息 - - // 登录相关API - APITypeSingleLogin = 10 // 卡密登录 - - // 用户账号管理API - APITypeUserLogin = 20 // 用户登录 - APITypeUserRegin = 21 // 用户注册 - APITypeUserRecharge = 22 // 用户充值 - APITypeCardRegin = 23 // 卡密注册 - - // 登出API - APITypeLogOut = 30 // 退出登录 - - // 用户状态查询API - APITypeGetExpired = 40 // 获取到期时间 - APITypeCheckUserStatus = 41 // 检测账号状态 - APITypeGetAppData = 42 // 获取程序数据 - APITypeGetVariable = 43 // 获取变量数据 - - // 用户操作API - APITypeUpdatePwd = 50 // 修改账号密码 - APITypeMacChangeBind = 51 // 机器码转绑 - APITypeIPChangeBind = 52 // IP转绑 - - // 管理员操作API - APITypeDisableUser = 60 // 封停用户 - APITypeBlackUser = 61 // 添加黑名单 - APITypeUserDeductedTime = 62 // 扣除时间 -) - -// 算法类型常量 -const ( - AlgorithmNone = 0 // 不加密 - AlgorithmRC4 = 1 // RC4 - AlgorithmRSA = 2 // RSA - AlgorithmRSADynamic = 3 // RSA(动态) - AlgorithmEasy = 4 // 易加密 -) +// ============================================================================ +// 独立函数 +// ============================================================================ // GetAlgorithmName 获取算法名称 func GetAlgorithmName(algorithm int) string { @@ -131,84 +145,134 @@ func GetAlgorithmName(algorithm int) string { } } +// ============================================================================ +// 基础结构体定义 +// ============================================================================ + +// APITypeInfo 接口类型信息 +type APITypeInfo struct { + Type int `json:"type"` // 接口类型 + Name string `json:"name"` // 接口名称 +} + +// APICategoryInfo 接口分类信息 +type APICategoryInfo struct { + Name string `json:"name"` // 分类名称 + Types []APITypeInfo `json:"types"` // 该分类下的接口类型列表 +} + +// ============================================================================ +// 核心功能函数 +// ============================================================================ + +// GetAPITypes 获取API接口类型,支持按分类返回或返回完整列表 +// categorized: true=按分类返回[]APICategoryInfo, false=返回完整列表[]int +func GetAPITypes(categorized bool) interface{} { + // 层次化的接口类型组织结构 + apiCategories := []APICategoryInfo{ + { + Name: "基础信息", + Types: []APITypeInfo{ + {Type: APITypeGetBulletin, Name: "获取程序公告"}, + {Type: APITypeGetUpdateUrl, Name: "获取更新地址"}, + {Type: APITypeCheckAppVersion, Name: "检测最新版本"}, + {Type: APITypeGetCardInfo, Name: "获取卡密信息"}, + }, + }, + { + Name: "卡密相关", + Types: []APITypeInfo{ + {Type: APITypeSingleLogin, Name: "卡密登录"}, + }, + }, + { + Name: "账号管理", + Types: []APITypeInfo{ + {Type: APITypeUserLogin, Name: "用户登录"}, + {Type: APITypeUserRegin, Name: "用户注册"}, + {Type: APITypeUserRecharge, Name: "用户充值"}, + }, + }, + { + Name: "登出操作", + Types: []APITypeInfo{ + {Type: APITypeLogOut, Name: "退出登录"}, + }, + }, + { + Name: "状态查询", + Types: []APITypeInfo{ + {Type: APITypeGetExpired, Name: "获取到期时间"}, + {Type: APITypeCheckUserStatus, Name: "检测账号状态"}, + {Type: APITypeGetAppData, Name: "获取程序数据"}, + {Type: APITypeGetVariable, Name: "获取变量数据"}, + {Type: APITypeExecuteFunction, Name: "执行远程函数"}, + }, + }, + { + Name: "用户操作", + Types: []APITypeInfo{ + {Type: APITypeUpdatePwd, Name: "修改账号密码"}, + {Type: APITypeMacChangeBind, Name: "机器码转绑"}, + {Type: APITypeIPChangeBind, Name: "IP转绑"}, + }, + }, + { + Name: "风控操作", + Types: []APITypeInfo{ + {Type: APITypeDisableUser, Name: "封停用户"}, + {Type: APITypeBlackUser, Name: "添加黑名单"}, + {Type: APITypeUserDeductedTime, Name: "扣除时间"}, + }, + }, + } + + if categorized { + // 返回层次化的分类结构 + return apiCategories + } + + // 返回所有接口类型的扁平列表 + var allTypes []int + for _, category := range apiCategories { + for _, typeInfo := range category.Types { + allTypes = append(allTypes, typeInfo.Type) + } + } + return allTypes +} + +// GetAPITypeName 获取API类型名称 +// 通过调用GetAPITypes函数来获取名称,避免重复维护数据 +func GetAPITypeName(apiType int) string { + // 获取分类化的API类型数据 + categories := GetAPITypes(true).([]APICategoryInfo) + + // 遍历所有分类和类型,查找匹配的API类型 + for _, category := range categories { + for _, typeInfo := range category.Types { + if typeInfo.Type == apiType { + return typeInfo.Name + } + } + } + + // 如果没有找到匹配的类型,返回默认值 + return "未知API类型" +} + +// ============================================================================ +// 验证函数 +// ============================================================================ + // IsValidAlgorithm 验证算法类型是否有效 func IsValidAlgorithm(algorithm int) bool { return algorithm >= AlgorithmNone && algorithm <= AlgorithmEasy } -// GetAPITypeName 获取API类型名称 -func GetAPITypeName(apiType int) string { - switch apiType { - // 基础信息获取类API - case APITypeGetBulletin: - return "获取程序公告" - case APITypeGetUpdateUrl: - return "获取更新地址" - case APITypeCheckAppVersion: - return "检测最新版本" - case APITypeGetCardInfo: - return "获取卡密信息" - - // 登录相关API - case APITypeSingleLogin: - return "卡密登录" - - // 用户账号管理API - case APITypeUserLogin: - return "用户登录" - case APITypeUserRegin: - return "用户注册" - case APITypeUserRecharge: - return "用户充值" - case APITypeCardRegin: - return "卡密注册" - - // 登出API - case APITypeLogOut: - return "退出登录" - - // 用户状态查询API - case APITypeGetExpired: - return "获取到期时间" - case APITypeCheckUserStatus: - return "检测账号状态" - case APITypeGetAppData: - return "获取程序数据" - case APITypeGetVariable: - return "获取变量数据" - - // 用户操作API - case APITypeUpdatePwd: - return "修改账号密码" - case APITypeMacChangeBind: - return "机器码转绑" - case APITypeIPChangeBind: - return "IP转绑" - - // 管理员操作API - case APITypeDisableUser: - return "封停用户" - case APITypeBlackUser: - return "添加黑名单" - case APITypeUserDeductedTime: - return "扣除时间" - - default: - return "未知API类型" - } -} - // IsValidAPIType 验证API类型是否有效 func IsValidAPIType(apiType int) bool { - validTypes := []int{ - APITypeGetBulletin, APITypeGetUpdateUrl, APITypeCheckAppVersion, APITypeGetCardInfo, - APITypeSingleLogin, - APITypeUserLogin, APITypeUserRegin, APITypeUserRecharge, APITypeCardRegin, - APITypeLogOut, - APITypeGetExpired, APITypeCheckUserStatus, APITypeGetAppData, APITypeGetVariable, - APITypeUpdatePwd, APITypeMacChangeBind, APITypeIPChangeBind, - APITypeDisableUser, APITypeBlackUser, APITypeUserDeductedTime, - } + validTypes := GetDefaultAPITypes() for _, validType := range validTypes { if apiType == validType { @@ -218,15 +282,34 @@ func IsValidAPIType(apiType int) bool { return false } -// GetAPITypesByCategory 根据分类获取API类型列表 -func GetAPITypesByCategory() map[string][]int { - return map[string][]int{ - "基础信息获取": {APITypeGetBulletin, APITypeGetUpdateUrl, APITypeCheckAppVersion, APITypeGetCardInfo}, - "登录相关": {APITypeSingleLogin}, - "用户账号管理": {APITypeUserLogin, APITypeUserRegin, APITypeUserRecharge, APITypeCardRegin}, - "登出": {APITypeLogOut}, - "用户状态查询": {APITypeGetExpired, APITypeCheckUserStatus, APITypeGetAppData, APITypeGetVariable}, - "用户操作": {APITypeUpdatePwd, APITypeMacChangeBind, APITypeIPChangeBind}, - "管理员操作": {APITypeDisableUser, APITypeBlackUser, APITypeUserDeductedTime}, - } +// ============================================================================ +// 兼容性函数 +// ============================================================================ + +// GetDefaultAPITypes 获取默认创建的API接口类型列表(兼容性函数) +func GetDefaultAPITypes() []int { + return GetAPITypes(false).([]int) +} + +// GetAPITypesByCategory 根据分类获取API类型列表(兼容性函数) +// 返回传统的 map[string][]int 格式以保持向后兼容 +func GetAPITypesByCategory() map[string][]int { + categories := GetAPITypes(true).([]APICategoryInfo) + result := make(map[string][]int) + + for _, category := range categories { + var types []int + for _, typeInfo := range category.Types { + types = append(types, typeInfo.Type) + } + result[category.Name] = types + } + + return result +} + +// GetAPICategoriesInfo 获取完整的层次化分类信息 +// 返回新的 []APICategoryInfo 格式,包含完整的类型名称信息 +func GetAPICategoriesInfo() []APICategoryInfo { + return GetAPITypes(true).([]APICategoryInfo) } diff --git a/models/app.go b/models/app.go index a848a1a..29af733 100644 --- a/models/app.go +++ b/models/app.go @@ -10,15 +10,13 @@ import ( "gorm.io/gorm" ) +// ============================================================================ +// 结构体定义 +// ============================================================================ + // App 应用表模型 // 用于管理应用程序的基本信息 -// UUID 为应用的唯一标识符,自动生成 -// Status 为应用状态(1:启用 0:禁用),默认为1 -// Name 为应用名称 -// Secret 为应用密钥,用于API认证 -// Version 为应用版本号 // CreatedAt/UpdatedAt 由 GORM 自动维护 - type App struct { // ID:主键,自增,同时通过 json 标签保证前端接收为 id ID uint `gorm:"primaryKey;comment:应用ID,自增主键" json:"id"` @@ -107,6 +105,10 @@ type App struct { UpdatedAt time.Time `gorm:"comment:更新时间" json:"updated_at"` } +// ============================================================================ +// 结构体方法 +// ============================================================================ + // BeforeCreate 在创建记录前自动生成UUID和密钥 func (app *App) BeforeCreate(tx *gorm.DB) error { if app.UUID == "" { diff --git a/models/function.go b/models/function.go index a1c2806..b39f1f1 100644 --- a/models/function.go +++ b/models/function.go @@ -9,14 +9,13 @@ import ( "gorm.io/gorm" ) +// ============================================================================ +// 结构体定义 +// ============================================================================ + // Function 函数表模型 // 用于管理应用程序的函数代码 -// UUID 为函数的唯一标识符,自动生成并转换为大写 -// Alias 为函数别名,便于识别和管理 -// Code 为函数代码内容 -// Remark 为备注信息,用于描述函数用途 // CreatedAt/UpdatedAt 由 GORM 自动维护 - type Function struct { // ID:主键,自增 ID uint `gorm:"primaryKey;comment:函数ID,自增主键" json:"id"` @@ -44,6 +43,10 @@ type Function struct { UpdatedAt time.Time `gorm:"comment:更新时间" json:"updated_at"` } +// ============================================================================ +// 结构体方法 +// ============================================================================ + // BeforeCreate 在创建记录前自动生成UUID和Number func (function *Function) BeforeCreate(tx *gorm.DB) error { // 生成UUID @@ -59,4 +62,4 @@ func (function *Function) BeforeCreate(tx *gorm.DB) error { // TableName 指定表名 func (Function) TableName() string { return "functions" -} \ No newline at end of file +} diff --git a/models/settings.go b/models/settings.go index 246f2f7..5ad5982 100644 --- a/models/settings.go +++ b/models/settings.go @@ -2,13 +2,13 @@ package models import "time" +// ============================================================================ +// 结构体定义 +// ============================================================================ + // Settings 系统设置表模型 // 用于存储应用的配置参数 -// Name 为配置项名称,唯一索引 -// Value 为配置项的值 -// Description 为配置项描述说明 // CreatedAt/UpdatedAt 由 GORM 自动维护 - type Settings struct { ID uint `gorm:"primaryKey;comment:设置ID,自增主键"` Name string `gorm:"uniqueIndex;size:64;not null;comment:配置项名称,唯一索引"` diff --git a/models/user.go b/models/user.go index fbeb5c6..bcfd10f 100644 --- a/models/user.go +++ b/models/user.go @@ -1,31 +1,39 @@ -package models - -import ( - "strings" - "time" - - "github.com/google/uuid" - "gorm.io/gorm" -) - -// User 用户表模型 -// 说明:PasswordSalt 使用 32 字节随机盐(以 16 进制存储为 64 个字符),因此列长度设置为 64 -// 注意:此表只存储普通用户,管理员账号存储在settings表中 -type User struct { - ID uint `gorm:"primaryKey;comment:用户ID,自增主键"` - UUID string `gorm:"uniqueIndex;size:36;not null;comment:用户的唯一标识符" json:"uuid"` - Username string `gorm:"uniqueIndex;size:64;not null;comment:用户名,唯一索引"` - Password string `gorm:"size:255;not null;comment:密码哈希值"` - PasswordSalt string `gorm:"size:64;not null;comment:密码加密盐值"` - CreatedAt time.Time `gorm:"comment:创建时间"` - UpdatedAt time.Time `gorm:"comment:更新时间"` -} - -// BeforeCreate 在创建记录前自动生成UUID -func (user *User) BeforeCreate(tx *gorm.DB) error { - // 生成UUID - if user.UUID == "" { - user.UUID = strings.ToUpper(uuid.New().String()) - } - return nil -} +package models + +import ( + "strings" + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +// ============================================================================ +// 结构体定义 +// ============================================================================ + +// User 用户表模型 +// 此表只存储普通用户,管理员账号存储在settings表中 +// CreatedAt/UpdatedAt 由 GORM 自动维护 +type User struct { + ID uint `gorm:"primaryKey;comment:用户ID,自增主键"` + UUID string `gorm:"uniqueIndex;size:36;not null;comment:用户的唯一标识符" json:"uuid"` + Username string `gorm:"uniqueIndex;size:64;not null;comment:用户名,唯一索引"` + Password string `gorm:"size:255;not null;comment:密码哈希值"` + PasswordSalt string `gorm:"size:64;not null;comment:密码加密盐值"` + CreatedAt time.Time `gorm:"comment:创建时间"` + UpdatedAt time.Time `gorm:"comment:更新时间"` +} + +// ============================================================================ +// 结构体方法 +// ============================================================================ + +// BeforeCreate 在创建记录前自动生成UUID +func (user *User) BeforeCreate(tx *gorm.DB) error { + // 生成UUID + if user.UUID == "" { + user.UUID = strings.ToUpper(uuid.New().String()) + } + return nil +} diff --git a/models/variable.go b/models/variable.go index 2b808a7..7411520 100644 --- a/models/variable.go +++ b/models/variable.go @@ -9,14 +9,13 @@ import ( "gorm.io/gorm" ) +// ============================================================================ +// 结构体定义 +// ============================================================================ + // Variable 变量表模型 // 用于管理应用程序的变量数据 -// UUID 为变量的唯一标识符,自动生成并转换为大写 -// Alias 为变量别名,便于识别和管理 -// Data 为变量数据内容 -// Remark 为备注信息,用于描述变量用途 // CreatedAt/UpdatedAt 由 GORM 自动维护 - type Variable struct { // ID:主键,自增 ID uint `gorm:"primaryKey;comment:变量ID,自增主键" json:"id"` @@ -44,6 +43,10 @@ type Variable struct { UpdatedAt time.Time `gorm:"comment:更新时间" json:"updated_at"` } +// ============================================================================ +// 结构体方法 +// ============================================================================ + // BeforeCreate 在创建记录前自动生成UUID和Number func (variable *Variable) BeforeCreate(tx *gorm.DB) error { // 生成UUID diff --git a/server/admin.go b/server/admin.go index bc4e6be..fdafbd1 100644 --- a/server/admin.go +++ b/server/admin.go @@ -1,138 +1,142 @@ -package server - -import ( - adminctl "networkDev/controllers/admin" - "networkDev/utils" - - "github.com/gin-gonic/gin" -) - -// RegisterAdminRoutes 注册管理员后台相关路由 -// - /admin/login: 支持GET渲染登录页、POST提交登录 -// - /admin/logout: 管理员退出登录 -// - /admin/dashboard: 管理员仪表盘(示例) -// - /admin/fragment/*: 布局内动态片段加载 -// - /admin/api/settings*: 设置接口(查询/更新) -func RegisterAdminRoutes(router *gin.Engine) { - // /admin 根与前缀统一入口:根据是否登录跳转 - router.GET("/admin", adminctl.AdminIndexHandler) - router.GET("/admin/", adminctl.AdminIndexHandler) - - // Admin 认证相关路由 - router.GET("/admin/login", adminctl.LoginPageHandler) - router.POST("/admin/login", adminctl.LoginHandler) // CSRF验证在控制器内部处理 - - // 退出登录(无需拦截,幂等清理) - router.POST("/admin/logout", adminctl.LogoutHandler) - - // 验证码生成路由(无需认证) - router.GET("/admin/captcha", adminctl.CaptchaHandler) - - // CSRF令牌获取API(无需认证,但需要在登录页面等地方获取) - router.GET("/admin/api/csrf-token", func(c *gin.Context) { - // 生成新的CSRF令牌 - token, err := utils.GenerateCSRFToken() - if err != nil { - c.JSON(500, gin.H{"success": false, "message": "生成CSRF令牌失败"}) - return - } - - // 设置令牌到Cookie和响应头 - utils.SetCSRFToken(c, token) - - // 返回令牌给前端 - c.JSON(200, gin.H{ - "success": true, - "message": "CSRF令牌生成成功", - "csrf_token": token, - }) - }) - - // 后台布局页(需要管理员认证) - router.GET("/admin/layout", adminctl.AdminAuthRequired(), adminctl.AdminLayoutHandler) - - // 片段路由(需要管理员认证) - router.GET("/admin/dashboard", adminctl.AdminAuthRequired(), adminctl.DashboardFragmentHandler) - router.GET("/admin/user", adminctl.AdminAuthRequired(), adminctl.UserFragmentHandler) - router.GET("/admin/settings", adminctl.AdminAuthRequired(), adminctl.SettingsFragmentHandler) - router.GET("/admin/apps", adminctl.AdminAuthRequired(), adminctl.AppsFragmentHandler) - router.GET("/admin/apis", adminctl.AdminAuthRequired(), adminctl.APIFragmentHandler) - router.GET("/admin/variables", adminctl.AdminAuthRequired(), adminctl.VariableFragmentHandler) - router.GET("/admin/functions", adminctl.AdminAuthRequired(), adminctl.FunctionFragmentHandler) - - // 系统信息API(用于仪表盘定时刷新) - router.GET("/admin/api/system/info", adminctl.AdminAuthRequired(), adminctl.SystemInfoHandler) - - // 仪表盘统计数据API - router.GET("/admin/api/dashboard/stats", adminctl.AdminAuthRequired(), adminctl.DashboardStatsHandler) - - // 个人资料API - userGroup := router.Group("/admin/api/user", adminctl.AdminAuthRequired()) - { - userGroup.GET("/profile", adminctl.UserProfileQueryHandler) - userGroup.POST("/profile/update", adminctl.UserProfileUpdateHandler) - userGroup.POST("/password", adminctl.UserPasswordUpdateHandler) - } - - // 系统设置API - settingsGroup := router.Group("/admin/api/settings", adminctl.AdminAuthRequired()) - { - settingsGroup.GET("", adminctl.SettingsQueryHandler) - settingsGroup.POST("/update", adminctl.SettingsUpdateHandler) - } - - // 应用管理API - appsGroup := router.Group("/admin/api/apps", adminctl.AdminAuthRequired()) - { - appsGroup.GET("/list", adminctl.AppsListHandler) - appsGroup.GET("/simple", adminctl.AppsSimpleListHandler) - appsGroup.POST("/create", adminctl.AppCreateHandler) - appsGroup.POST("/update", adminctl.AppUpdateHandler) - appsGroup.POST("/delete", adminctl.AppDeleteHandler) - appsGroup.POST("/batch_delete", adminctl.AppsBatchDeleteHandler) - appsGroup.POST("/batch_update_status", adminctl.AppsBatchUpdateStatusHandler) - appsGroup.POST("/update_status", adminctl.AppUpdateStatusHandler) - appsGroup.POST("/reset_secret", adminctl.AppResetSecretHandler) - appsGroup.GET("/get_app_data", adminctl.AppGetAppDataHandler) - appsGroup.POST("/update_app_data", adminctl.AppUpdateAppDataHandler) - appsGroup.GET("/get_announcement", adminctl.AppGetAnnouncementHandler) - appsGroup.POST("/update_announcement", adminctl.AppUpdateAnnouncementHandler) - appsGroup.GET("/get_multi_config", adminctl.AppGetMultiConfigHandler) - appsGroup.POST("/update_multi_config", adminctl.AppUpdateMultiConfigHandler) - appsGroup.GET("/get_bind_config", adminctl.AppGetBindConfigHandler) - appsGroup.POST("/update_bind_config", adminctl.AppUpdateBindConfigHandler) - appsGroup.GET("/get_register_config", adminctl.AppGetRegisterConfigHandler) - appsGroup.POST("/update_register_config", adminctl.AppUpdateRegisterConfigHandler) - } - - // API接口管理API - apisGroup := router.Group("/admin/api/apis", adminctl.AdminAuthRequired()) - { - apisGroup.GET("/list", adminctl.APIListHandler) - apisGroup.POST("/update", adminctl.APIUpdateHandler) - apisGroup.POST("/update_status", adminctl.APIUpdateStatusHandler) - apisGroup.GET("/types", adminctl.APIGetTypesHandler) - apisGroup.POST("/generate_keys", adminctl.APIGenerateKeysHandler) - } - - // 变量管理API - variableGroup := router.Group("/admin/variable", adminctl.AdminAuthRequired()) - { - variableGroup.GET("/list", adminctl.VariableListHandler) - variableGroup.POST("/create", adminctl.VariableCreateHandler) - variableGroup.POST("/update", adminctl.VariableUpdateHandler) - variableGroup.POST("/delete", adminctl.VariableDeleteHandler) - variableGroup.POST("/batch_delete", adminctl.VariablesBatchDeleteHandler) - } - - // 函数管理API - functionGroup := router.Group("/admin/function", adminctl.AdminAuthRequired()) - { - functionGroup.GET("/list", adminctl.FunctionListHandler) - functionGroup.POST("/create", adminctl.FunctionCreateHandler) - functionGroup.POST("/update", adminctl.FunctionUpdateHandler) - functionGroup.POST("/delete", adminctl.FunctionDeleteHandler) - functionGroup.POST("/batch_delete", adminctl.FunctionsBatchDeleteHandler) - } - -} +package server + +import ( + adminctl "networkDev/controllers/admin" + "networkDev/utils" + + "github.com/gin-gonic/gin" +) + +// ============================================================================ +// 路由注册函数 +// ============================================================================ + +// RegisterAdminRoutes 注册管理员后台相关路由 +// - /admin/login: 支持GET渲染登录页、POST提交登录 +// - /admin/logout: 管理员退出登录 +// - /admin/dashboard: 管理员仪表盘(示例) +// - /admin/fragment/*: 布局内动态片段加载 +// - /admin/api/settings*: 设置接口(查询/更新) +func RegisterAdminRoutes(router *gin.Engine) { + // /admin 根与前缀统一入口:根据是否登录跳转 + router.GET("/admin", adminctl.AdminIndexHandler) + router.GET("/admin/", adminctl.AdminIndexHandler) + + // Admin 认证相关路由 + router.GET("/admin/login", adminctl.LoginPageHandler) + router.POST("/admin/login", adminctl.LoginHandler) // CSRF验证在控制器内部处理 + + // 退出登录(无需拦截,幂等清理) + router.POST("/admin/logout", adminctl.LogoutHandler) + + // 验证码生成路由(无需认证) + router.GET("/admin/captcha", adminctl.CaptchaHandler) + + // CSRF令牌获取API(无需认证,但需要在登录页面等地方获取) + router.GET("/admin/api/csrf-token", func(c *gin.Context) { + // 生成新的CSRF令牌 + token, err := utils.GenerateCSRFToken() + if err != nil { + c.JSON(500, gin.H{"success": false, "message": "生成CSRF令牌失败"}) + return + } + + // 设置令牌到Cookie和响应头 + utils.SetCSRFToken(c, token) + + // 返回令牌给前端 + c.JSON(200, gin.H{ + "success": true, + "message": "CSRF令牌生成成功", + "csrf_token": token, + }) + }) + + // 后台布局页(需要管理员认证) + router.GET("/admin/layout", adminctl.AdminAuthRequired(), adminctl.AdminLayoutHandler) + + // 片段路由(需要管理员认证) + router.GET("/admin/dashboard", adminctl.AdminAuthRequired(), adminctl.DashboardFragmentHandler) + router.GET("/admin/user", adminctl.AdminAuthRequired(), adminctl.UserFragmentHandler) + router.GET("/admin/settings", adminctl.AdminAuthRequired(), adminctl.SettingsFragmentHandler) + router.GET("/admin/apps", adminctl.AdminAuthRequired(), adminctl.AppsFragmentHandler) + router.GET("/admin/apis", adminctl.AdminAuthRequired(), adminctl.APIFragmentHandler) + router.GET("/admin/variables", adminctl.AdminAuthRequired(), adminctl.VariableFragmentHandler) + router.GET("/admin/functions", adminctl.AdminAuthRequired(), adminctl.FunctionFragmentHandler) + + // 系统信息API(用于仪表盘定时刷新) + router.GET("/admin/api/system/info", adminctl.AdminAuthRequired(), adminctl.SystemInfoHandler) + + // 仪表盘统计数据API + router.GET("/admin/api/dashboard/stats", adminctl.AdminAuthRequired(), adminctl.DashboardStatsHandler) + + // 个人资料API + userGroup := router.Group("/admin/api/user", adminctl.AdminAuthRequired()) + { + userGroup.GET("/profile", adminctl.UserProfileQueryHandler) + userGroup.POST("/profile/update", adminctl.UserProfileUpdateHandler) + userGroup.POST("/password", adminctl.UserPasswordUpdateHandler) + } + + // 系统设置API + settingsGroup := router.Group("/admin/api/settings", adminctl.AdminAuthRequired()) + { + settingsGroup.GET("", adminctl.SettingsQueryHandler) + settingsGroup.POST("/update", adminctl.SettingsUpdateHandler) + } + + // 应用管理API + appsGroup := router.Group("/admin/api/apps", adminctl.AdminAuthRequired()) + { + appsGroup.GET("/list", adminctl.AppsListHandler) + appsGroup.GET("/simple", adminctl.AppsSimpleListHandler) + appsGroup.POST("/create", adminctl.AppCreateHandler) + appsGroup.POST("/update", adminctl.AppUpdateHandler) + appsGroup.POST("/delete", adminctl.AppDeleteHandler) + appsGroup.POST("/batch_delete", adminctl.AppsBatchDeleteHandler) + appsGroup.POST("/batch_update_status", adminctl.AppsBatchUpdateStatusHandler) + appsGroup.POST("/update_status", adminctl.AppUpdateStatusHandler) + appsGroup.POST("/reset_secret", adminctl.AppResetSecretHandler) + appsGroup.GET("/get_app_data", adminctl.AppGetAppDataHandler) + appsGroup.POST("/update_app_data", adminctl.AppUpdateAppDataHandler) + appsGroup.GET("/get_announcement", adminctl.AppGetAnnouncementHandler) + appsGroup.POST("/update_announcement", adminctl.AppUpdateAnnouncementHandler) + appsGroup.GET("/get_multi_config", adminctl.AppGetMultiConfigHandler) + appsGroup.POST("/update_multi_config", adminctl.AppUpdateMultiConfigHandler) + appsGroup.GET("/get_bind_config", adminctl.AppGetBindConfigHandler) + appsGroup.POST("/update_bind_config", adminctl.AppUpdateBindConfigHandler) + appsGroup.GET("/get_register_config", adminctl.AppGetRegisterConfigHandler) + appsGroup.POST("/update_register_config", adminctl.AppUpdateRegisterConfigHandler) + } + + // API接口管理API + apisGroup := router.Group("/admin/api/apis", adminctl.AdminAuthRequired()) + { + apisGroup.GET("/list", adminctl.APIListHandler) + apisGroup.POST("/update", adminctl.APIUpdateHandler) + apisGroup.POST("/update_status", adminctl.APIUpdateStatusHandler) + apisGroup.GET("/types", adminctl.APIGetTypesHandler) + apisGroup.POST("/generate_keys", adminctl.APIGenerateKeysHandler) + } + + // 变量管理API + variableGroup := router.Group("/admin/variable", adminctl.AdminAuthRequired()) + { + variableGroup.GET("/list", adminctl.VariableListHandler) + variableGroup.POST("/create", adminctl.VariableCreateHandler) + variableGroup.POST("/update", adminctl.VariableUpdateHandler) + variableGroup.POST("/delete", adminctl.VariableDeleteHandler) + variableGroup.POST("/batch_delete", adminctl.VariablesBatchDeleteHandler) + } + + // 函数管理API + functionGroup := router.Group("/admin/function", adminctl.AdminAuthRequired()) + { + functionGroup.GET("/list", adminctl.FunctionListHandler) + functionGroup.POST("/create", adminctl.FunctionCreateHandler) + functionGroup.POST("/update", adminctl.FunctionUpdateHandler) + functionGroup.POST("/delete", adminctl.FunctionDeleteHandler) + functionGroup.POST("/batch_delete", adminctl.FunctionsBatchDeleteHandler) + } + +} diff --git a/server/home.go b/server/home.go index 2578898..3da4f5a 100644 --- a/server/home.go +++ b/server/home.go @@ -1,14 +1,18 @@ -package server - -import ( - "networkDev/controllers/home" - - "github.com/gin-gonic/gin" -) - -// RegisterHomeRoutes 注册主页路由 -// 只包含根路径,用于主页功能 -func RegisterHomeRoutes(router *gin.Engine) { - // 根路径 - 主页 - router.GET("/", home.RootHandler) -} +package server + +import ( + "networkDev/controllers/home" + + "github.com/gin-gonic/gin" +) + +// ============================================================================ +// 路由注册函数 +// ============================================================================ + +// RegisterHomeRoutes 注册主页路由 +// 只包含根路径,用于主页功能 +func RegisterHomeRoutes(router *gin.Engine) { + // 根路径 - 主页 + router.GET("/", home.RootHandler) +} diff --git a/server/routes.go b/server/routes.go index 52cc118..9f9b7f9 100644 --- a/server/routes.go +++ b/server/routes.go @@ -1,48 +1,56 @@ -package server - -import ( - "io/fs" - "log" - "net/http" - "networkDev/web" - - "github.com/gin-gonic/gin" -) - -// RegisterRoutes 聚合注册所有路由 -func RegisterRoutes(router *gin.Engine) { - registerStaticRoutes(router) - registerFaviconRoute(router) - RegisterHomeRoutes(router) - RegisterAdminRoutes(router) - -} - -// registerStaticRoutes 注册静态资源路由 -// 静态资源服务,将 /static/ 和 /assets/ 映射到嵌入的文件系统 -func registerStaticRoutes(router *gin.Engine) { - if fsys, err := web.GetStaticFS(); err == nil { - // 为 /static/ 路径创建子文件系统 - if staticSubFS, staticErr := fs.Sub(fsys, "static"); staticErr == nil { - router.StaticFS("/static", http.FS(staticSubFS)) - } else { - log.Printf("创建静态资源子文件系统失败: %v", staticErr) - } - // 为 /assets/ 路径创建子文件系统 - if assetsSubFS, assetsErr := fs.Sub(fsys, "assets"); assetsErr == nil { - router.StaticFS("/assets", http.FS(assetsSubFS)) - } else { - log.Printf("创建资产资源子文件系统失败: %v", assetsErr) - } - } else { - log.Printf("初始化静态资源文件系统失败: %v", err) - } -} - -// registerFaviconRoute 注册favicon路由 -func registerFaviconRoute(router *gin.Engine) { - // 将 /favicon.ico 重定向到 /assets/favicon.svg - router.GET("/favicon.ico", func(c *gin.Context) { - c.Redirect(http.StatusMovedPermanently, "/assets/favicon.svg") - }) -} +package server + +import ( + "io/fs" + "log" + "net/http" + "networkDev/web" + + "github.com/gin-gonic/gin" +) + +// ============================================================================ +// 公共函数 +// ============================================================================ + +// RegisterRoutes 聚合注册所有路由 +func RegisterRoutes(router *gin.Engine) { + registerStaticRoutes(router) + registerFaviconRoute(router) + RegisterHomeRoutes(router) + RegisterAdminRoutes(router) + +} + +// ============================================================================ +// 私有函数 +// ============================================================================ + +// registerStaticRoutes 注册静态资源路由 +// 静态资源服务,将 /static/ 和 /assets/ 映射到嵌入的文件系统 +func registerStaticRoutes(router *gin.Engine) { + if fsys, err := web.GetStaticFS(); err == nil { + // 为 /static/ 路径创建子文件系统 + if staticSubFS, staticErr := fs.Sub(fsys, "static"); staticErr == nil { + router.StaticFS("/static", http.FS(staticSubFS)) + } else { + log.Printf("创建静态资源子文件系统失败: %v", staticErr) + } + // 为 /assets/ 路径创建子文件系统 + if assetsSubFS, assetsErr := fs.Sub(fsys, "assets"); assetsErr == nil { + router.StaticFS("/assets", http.FS(assetsSubFS)) + } else { + log.Printf("创建资产资源子文件系统失败: %v", assetsErr) + } + } else { + log.Printf("初始化静态资源文件系统失败: %v", err) + } +} + +// registerFaviconRoute 注册favicon路由 +func registerFaviconRoute(router *gin.Engine) { + // 将 /favicon.ico 重定向到 /assets/favicon.svg + router.GET("/favicon.ico", func(c *gin.Context) { + c.Redirect(http.StatusMovedPermanently, "/assets/favicon.svg") + }) +} diff --git a/services/query.go b/services/query.go index 0f7047c..28b8c11 100644 --- a/services/query.go +++ b/services/query.go @@ -1,85 +1,101 @@ -package services - -import ( - "context" - "fmt" - "networkDev/models" - "networkDev/utils" - "time" - - "gorm.io/gorm" -) - -// FindSettingByName 根据名称查找设置 -// name: 设置名称 -// db: 数据库连接 -// 返回: 设置信息和错误 -func FindSettingByName(name string, db *gorm.DB) (*models.Settings, error) { - key := fmt.Sprintf("setting:%s", name) - return utils.RedisGetOrSet(context.Background(), key, 5*time.Minute, func() (*models.Settings, error) { - var setting models.Settings - err := db.Where("name = ?", name).First(&setting).Error - if err != nil { - return nil, err - } - return &setting, nil - }) -} - -// UpdateEntityByID 根据ID更新实体 -// model: 模型类型 -// id: 实体ID -// updates: 更新字段 -// db: 数据库连接 -// 返回: 错误 -func UpdateEntityByID(model interface{}, id uint, updates map[string]interface{}, db *gorm.DB) error { - return db.Model(model).Where("id = ?", id).Updates(updates).Error -} - -// BatchUpdateEntityStatus 批量更新实体状态 -// model: 模型类型 -// ids: 实体ID列表 -// status: 新状态 -// db: 数据库连接 -// 返回: 错误 -func BatchUpdateEntityStatus(model interface{}, ids []uint, status int, db *gorm.DB) error { - if len(ids) == 0 { - return nil - } - return db.Model(model).Where("id IN ?", ids).Update("status", status).Error -} - -// CountEntitiesByCondition 根据条件统计实体数量 -// model: 模型类型 -// condition: 查询条件 -// db: 数据库连接 -// args: 查询参数 -// 返回: 数量和错误 -func CountEntitiesByCondition(model interface{}, condition string, db *gorm.DB, args ...interface{}) (int64, error) { - var count int64 - err := db.Model(model).Where(condition, args...).Count(&count).Error - return count, err -} - -// FindEntitiesByCondition 根据条件查找实体 -// model: 模型类型 -// result: 结果容器 -// condition: 查询条件 -// db: 数据库连接 -// args: 查询参数 -// 返回: 错误 -func FindEntitiesByCondition(model interface{}, result interface{}, condition string, db *gorm.DB, args ...interface{}) error { - return db.Model(model).Where(condition, args...).Find(result).Error -} - -// CheckEntityExists 检查实体是否存在 -// model: 模型类型 -// condition: 查询条件 -// db: 数据库连接 -// args: 查询参数 -// 返回: 是否存在和错误 -func CheckEntityExists(model interface{}, condition string, db *gorm.DB, args ...interface{}) (bool, error) { - var count int64 - err := db.Model(model).Where(condition, args...).Count(&count).Error - return count > 0, err -} +package services + +import ( + "context" + "fmt" + "networkDev/models" + "networkDev/utils" + "time" + + "gorm.io/gorm" +) + +// ============================================================================ +// 查询函数 +// ============================================================================ + +// FindSettingByName 根据名称查找设置 +// name: 设置名称 +// db: 数据库连接 +// 返回: 设置信息和错误 +func FindSettingByName(name string, db *gorm.DB) (*models.Settings, error) { + key := fmt.Sprintf("setting:%s", name) + return utils.RedisGetOrSet(context.Background(), key, 5*time.Minute, func() (*models.Settings, error) { + var setting models.Settings + err := db.Where("name = ?", name).First(&setting).Error + if err != nil { + return nil, err + } + return &setting, nil + }) +} + +// ============================================================================ +// 更新函数 +// ============================================================================ + +// UpdateEntityByID 根据ID更新实体 +// model: 模型类型 +// id: 实体ID +// updates: 更新字段 +// db: 数据库连接 +// 返回: 错误 +func UpdateEntityByID(model interface{}, id uint, updates map[string]interface{}, db *gorm.DB) error { + return db.Model(model).Where("id = ?", id).Updates(updates).Error +} + +// BatchUpdateEntityStatus 批量更新实体状态 +// model: 模型类型 +// ids: 实体ID列表 +// status: 新状态 +// db: 数据库连接 +// 返回: 错误 +func BatchUpdateEntityStatus(model interface{}, ids []uint, status int, db *gorm.DB) error { + if len(ids) == 0 { + return nil + } + return db.Model(model).Where("id IN ?", ids).Update("status", status).Error +} + +// ============================================================================ +// 统计函数 +// ============================================================================ + +// CountEntitiesByCondition 根据条件统计实体数量 +// model: 模型类型 +// condition: 查询条件 +// db: 数据库连接 +// args: 查询参数 +// 返回: 数量和错误 +func CountEntitiesByCondition(model interface{}, condition string, db *gorm.DB, args ...interface{}) (int64, error) { + var count int64 + err := db.Model(model).Where(condition, args...).Count(&count).Error + return count, err +} + +// ============================================================================ +// 通用查询函数 +// ============================================================================ + +// FindEntitiesByCondition 根据条件查找实体 +// model: 模型类型 +// result: 结果容器 +// condition: 查询条件 +// db: 数据库连接 +// args: 查询参数 +// 返回: 错误 +func FindEntitiesByCondition(model interface{}, result interface{}, condition string, db *gorm.DB, args ...interface{}) error { + return db.Model(model).Where(condition, args...).Find(result).Error +} + +// CheckEntityExists 检查实体是否存在 +// model: 模型类型 +// condition: 查询条件 +// db: 数据库连接 +// args: 查询参数 +// 返回: 是否存在和错误 +func CheckEntityExists(model interface{}, condition string, db *gorm.DB, args ...interface{}) (bool, error) { + var count int64 + err := db.Model(model).Where(condition, args...).Count(&count).Error + return count > 0, err +} diff --git a/services/settings.go b/services/settings.go index 1be1d05..94ef7fe 100644 --- a/services/settings.go +++ b/services/settings.go @@ -9,15 +9,27 @@ import ( "github.com/sirupsen/logrus" ) +// ============================================================================ +// 结构体定义 +// ============================================================================ + // SettingsService 设置服务 type SettingsService struct { mu sync.RWMutex cache map[string]string } +// ============================================================================ +// 全局变量 +// ============================================================================ + var settingsService *SettingsService var settingsOnce sync.Once +// ============================================================================ +// 公共函数 +// ============================================================================ + // GetSettingsService 获取设置服务单例 func GetSettingsService() *SettingsService { settingsOnce.Do(func() { @@ -30,6 +42,10 @@ func GetSettingsService() *SettingsService { return settingsService } +// ============================================================================ +// 私有函数 +// ============================================================================ + // loadAllSettings 从数据库加载所有设置到缓存 func (s *SettingsService) loadAllSettings() { db, err := database.GetDB() diff --git a/utils/cookie.go b/utils/cookie.go index 92090f7..6fdfe70 100644 --- a/utils/cookie.go +++ b/utils/cookie.go @@ -7,6 +7,10 @@ import ( "github.com/spf13/viper" ) +// ============================================================================ +// Cookie创建函数 +// ============================================================================ + // CreateSecureCookie 创建安全的Cookie // name: Cookie名称 // value: Cookie值 @@ -67,6 +71,10 @@ func CreateExpiredCookie(name string) *http.Cookie { return CreateSecureCookie(name, "", -1) } +// ============================================================================ +// 配置函数 +// ============================================================================ + // GetDefaultCookieMaxAge 获取默认Cookie过期时间 func GetDefaultCookieMaxAge() int { maxAge := viper.GetInt("security.cookie.max_age") @@ -74,4 +82,4 @@ func GetDefaultCookieMaxAge() int { return 86400 // 默认24小时 } return maxAge -} \ No newline at end of file +} diff --git a/utils/crypto.go b/utils/crypto.go index aa44831..ab9ac50 100644 --- a/utils/crypto.go +++ b/utils/crypto.go @@ -15,6 +15,10 @@ import ( "golang.org/x/crypto/bcrypt" ) +// ============================================================================ +// 结构体定义 +// ============================================================================ + // CryptoManager 加密管理器,提供高性能的加密解密服务 type CryptoManager struct { key []byte @@ -23,9 +27,17 @@ type CryptoManager struct { inited bool } +// ============================================================================ +// 全局变量 +// ============================================================================ + // 全局加密管理器实例 var cryptoManager = &CryptoManager{} +// ============================================================================ +// 私有函数 +// ============================================================================ + // initCrypto 初始化加密管理器 // 缓存密钥和GCM实例,避免重复创建 func (cm *CryptoManager) initCrypto() error { @@ -63,6 +75,10 @@ func (cm *CryptoManager) initCrypto() error { return nil } +// ============================================================================ +// 加密解密函数 +// ============================================================================ + // EncryptString 字符串加密(AES-256-GCM) // 使用缓存的密钥和GCM实例,提高性能 func EncryptString(plain string) (string, error) { diff --git a/utils/csrf.go b/utils/csrf.go index e8ff026..d7cb833 100644 --- a/utils/csrf.go +++ b/utils/csrf.go @@ -9,6 +9,10 @@ import ( "github.com/gin-gonic/gin" ) +// ============================================================================ +// 常量定义 +// ============================================================================ + const ( CSRFTokenLength = 32 CSRFCookieName = "csrf_token" @@ -16,6 +20,10 @@ const ( CSRFFormField = "csrf_token" ) +// ============================================================================ +// 私有函数 +// ============================================================================ + // generateRandomBytes 生成指定长度的随机字节 func generateRandomBytes(length int) ([]byte, error) { bytes := make([]byte, length) @@ -26,6 +34,10 @@ func generateRandomBytes(length int) ([]byte, error) { return bytes, nil } +// ============================================================================ +// 公共函数 +// ============================================================================ + // GenerateCSRFToken 生成CSRF令牌 func GenerateCSRFToken() (string, error) { bytes, err := generateRandomBytes(CSRFTokenLength) @@ -190,4 +202,4 @@ func CSRFTokenHandler(c *gin.Context) { "csrf_token": token, }, }) -} \ No newline at end of file +} diff --git a/utils/database.go b/utils/database.go index 9ecd813..fe14334 100644 --- a/utils/database.go +++ b/utils/database.go @@ -1,327 +1,343 @@ -package utils - -import ( - "context" - "database/sql" - "encoding/json" - "fmt" - "sync" - "time" - - "github.com/redis/go-redis/v9" - "github.com/sirupsen/logrus" - "github.com/spf13/viper" - "gorm.io/gorm" -) - -// DatabaseConfig 数据库连接池配置结构体 -// 用于配置数据库连接池的各项参数,包括连接池大小、生命周期管理和健康检查等 -type DatabaseConfig struct { - // 连接池配置 - MaxIdleConns int `mapstructure:"max_idle_conns"` // 最大空闲连接数 - MaxOpenConns int `mapstructure:"max_open_conns"` // 最大打开连接数 - ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"` // 连接最大生存时间 - ConnMaxIdleTime time.Duration `mapstructure:"conn_max_idle_time"` // 连接最大空闲时间 - - // 健康检查配置 - PingTimeout time.Duration `mapstructure:"ping_timeout"` // Ping超时时间 - HealthCheckInterval time.Duration `mapstructure:"health_check_interval"` // 健康检查间隔 -} - -// GetDefaultDatabaseConfig 获取默认数据库配置 -// 返回一个包含合理默认值的数据库配置实例 -func GetDefaultDatabaseConfig() *DatabaseConfig { - return &DatabaseConfig{ - MaxIdleConns: 10, // 默认最大空闲连接数 - MaxOpenConns: 100, // 默认最大打开连接数 - ConnMaxLifetime: 30 * time.Minute, // 连接最大生存时间30分钟 - ConnMaxIdleTime: 10 * time.Minute, // 连接最大空闲时间10分钟 - PingTimeout: 5 * time.Second, // Ping超时5秒 - HealthCheckInterval: 30 * time.Second, // 健康检查间隔30秒 - } -} - -// LoadDatabaseConfig 从配置文件加载数据库配置 -// 使用指定的前缀从viper配置中读取数据库配置,如果配置项不存在则使用默认值 -func LoadDatabaseConfig(prefix string) *DatabaseConfig { - config := GetDefaultDatabaseConfig() - - // 从viper读取配置,如果不存在则使用默认值 - if viper.IsSet(prefix + ".max_idle_conns") { - config.MaxIdleConns = viper.GetInt(prefix + ".max_idle_conns") - } - if viper.IsSet(prefix + ".max_open_conns") { - config.MaxOpenConns = viper.GetInt(prefix + ".max_open_conns") - } - if viper.IsSet(prefix + ".conn_max_lifetime") { - config.ConnMaxLifetime = viper.GetDuration(prefix + ".conn_max_lifetime") - } - if viper.IsSet(prefix + ".conn_max_idle_time") { - config.ConnMaxIdleTime = viper.GetDuration(prefix + ".conn_max_idle_time") - } - if viper.IsSet(prefix + ".ping_timeout") { - config.PingTimeout = viper.GetDuration(prefix + ".ping_timeout") - } - if viper.IsSet(prefix + ".health_check_interval") { - config.HealthCheckInterval = viper.GetDuration(prefix + ".health_check_interval") - } - - return config -} - -// ConfigureConnectionPool 配置数据库连接池 -// 根据提供的配置参数设置GORM数据库的连接池属性 -func ConfigureConnectionPool(db *gorm.DB, config *DatabaseConfig) error { - sqlDB, err := db.DB() - if err != nil { - return fmt.Errorf("获取底层数据库连接失败: %w", err) - } - - // 设置连接池参数 - sqlDB.SetMaxIdleConns(config.MaxIdleConns) - sqlDB.SetMaxOpenConns(config.MaxOpenConns) - sqlDB.SetConnMaxLifetime(config.ConnMaxLifetime) - sqlDB.SetConnMaxIdleTime(config.ConnMaxIdleTime) - - // LogInfo("数据库连接池配置完成", map[string]interface{}{ - // "max_idle_conns": config.MaxIdleConns, - // "max_open_conns": config.MaxOpenConns, - // "conn_max_lifetime": config.ConnMaxLifetime, - // "conn_max_idle_time": config.ConnMaxIdleTime, - // }) - - return nil -} - -// PingDatabase 检查数据库连接健康状态 -// 使用指定的超时时间ping数据库以验证连接是否正常 -func PingDatabase(db *gorm.DB, timeout time.Duration) error { - sqlDB, err := db.DB() - if err != nil { - return fmt.Errorf("获取底层数据库连接失败: %w", err) - } - - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - return sqlDB.PingContext(ctx) -} - -// GetConnectionStats 获取数据库连接池统计信息 -// 返回当前数据库连接池的详细统计数据,包括连接数、等待时间等 -func GetConnectionStats(db *gorm.DB) (*sql.DBStats, error) { - sqlDB, err := db.DB() - if err != nil { - return nil, fmt.Errorf("获取底层数据库连接失败: %w", err) - } - - stats := sqlDB.Stats() - return &stats, nil -} - -// LogConnectionStats 记录数据库连接池统计信息 -// 获取并记录数据库连接池的统计信息到日志中,用于监控和调试 -func LogConnectionStats(db *gorm.DB) { - stats, err := GetConnectionStats(db) - if err != nil { - LogError("获取数据库连接池统计信息失败", err, nil) - return - } - - LogInfo("数据库连接池统计", map[string]interface{}{ - "open_connections": stats.OpenConnections, - "in_use": stats.InUse, - "idle": stats.Idle, - "wait_count": stats.WaitCount, - "wait_duration": stats.WaitDuration, - "max_idle_closed": stats.MaxIdleClosed, - "max_idle_time_closed": stats.MaxIdleTimeClosed, - "max_lifetime_closed": stats.MaxLifetimeClosed, - }) -} - -// StartHealthCheck 启动数据库健康检查 -// 启动一个后台goroutine定期检查数据库连接健康状态 -// 只在健康检查失败时输出错误日志,正常情况下不输出日志 -func StartHealthCheck(db *gorm.DB, config *DatabaseConfig) { - go func() { - ticker := time.NewTicker(config.HealthCheckInterval) - defer ticker.Stop() - - for range ticker.C { - if err := PingDatabase(db, config.PingTimeout); err != nil { - // 只在健康检查失败时输出错误日志 - LogError("数据库健康检查失败", err, map[string]interface{}{ - "ping_timeout": config.PingTimeout, - }) - } - - // 记录连接池统计信息(仅在调试模式下) - if logrus.GetLevel() == logrus.DebugLevel { - LogConnectionStats(db) - } - } - }() - - // LogInfo("数据库健康检查已启动", map[string]interface{}{ - // "check_interval": config.HealthCheckInterval, - // "ping_timeout": config.PingTimeout, - // }) -} - -// ValidateDatabaseConfig 验证数据库配置参数 -// 检查数据库配置参数的有效性,确保所有参数都在合理范围内 -func ValidateDatabaseConfig(config *DatabaseConfig) error { - if config.MaxIdleConns < 0 { - return fmt.Errorf("最大空闲连接数不能为负数: %d", config.MaxIdleConns) - } - if config.MaxOpenConns < 0 { - return fmt.Errorf("最大打开连接数不能为负数: %d", config.MaxOpenConns) - } - if config.MaxIdleConns > config.MaxOpenConns && config.MaxOpenConns > 0 { - return fmt.Errorf("最大空闲连接数(%d)不能大于最大打开连接数(%d)", config.MaxIdleConns, config.MaxOpenConns) - } - if config.ConnMaxLifetime < 0 { - return fmt.Errorf("连接最大生存时间不能为负数: %v", config.ConnMaxLifetime) - } - if config.ConnMaxIdleTime < 0 { - return fmt.Errorf("连接最大空闲时间不能为负数: %v", config.ConnMaxIdleTime) - } - if config.PingTimeout <= 0 { - return fmt.Errorf("Ping超时时间必须大于0: %v", config.PingTimeout) - } - if config.HealthCheckInterval <= 0 { - return fmt.Errorf("健康检查间隔必须大于0: %v", config.HealthCheckInterval) - } - - return nil -} - -var ( - // redisClient 全局Redis客户端 - redisClient *redis.Client - // redisOnce 确保只初始化一次 - redisOnce sync.Once - // redisAvailable 标记Redis是否可用 - redisAvailable bool -) - -// InitRedis 初始化Redis客户端(仅在配置存在时尝试连接) -// - 从 viper 读取 security.redis.* 配置 -// - 如果连接失败,则标记为不可用,不影响主流程 -func InitRedis() { - redisOnce.Do(func() { - host := viper.GetString("redis.host") - port := viper.GetInt("redis.port") - if host == "" || port == 0 { - logrus.Info("未配置Redis或配置不完整,跳过初始化") - redisAvailable = false - return - } - addr := fmt.Sprintf("%s:%d", host, port) - redisClient = redis.NewClient(&redis.Options{ - Addr: addr, - Password: viper.GetString("redis.password"), - DB: viper.GetInt("redis.db"), - }) - // 健康检查 - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - if err := redisClient.Ping(ctx).Err(); err != nil { - logrus.WithError(err).Warn("Redis初始化失败,标记为不可用") - redisAvailable = false - return - } - redisAvailable = true - logrus.WithField("addr", addr).Info("Redis 连接已建立") - }) -} - -// GetRedis 获取全局Redis客户端,可能返回nil(当不可用时) -func GetRedis() *redis.Client { - if redisClient == nil { - InitRedis() - } - if !redisAvailable { - return nil - } - return redisClient -} - -// IsRedisAvailable 判断Redis是否可用 -func IsRedisAvailable() bool { - if redisClient == nil { - InitRedis() - } - return redisAvailable -} - -// RedisGetOrSet 通用Redis缓存获取或设置函数(基于JSON序列化) -// - ctx: 上下文 -// - key: 缓存键 -// - ttl: 过期时间 -// - loader: 当缓存不存在时的加载函数(一般执行数据库查询) -// 返回:目标对象指针和错误 -func RedisGetOrSet[T any](ctx context.Context, key string, ttl time.Duration, loader func() (*T, error)) (*T, error) { - // 如果Redis不可用则直接调用加载函数 - if !IsRedisAvailable() { - return loader() - } - client := GetRedis() - if client == nil { - return loader() - } - - // 先尝试从缓存读取 - data, err := client.Get(ctx, key).Bytes() - if err == nil { - var out T - if uerr := json.Unmarshal(data, &out); uerr == nil { - return &out, nil - } - // 反序列化失败时视为未命中,继续加载 - logrus.WithError(err).WithField("key", key).Warn("Redis缓存反序列化失败,回退到loader") - } else if err != redis.Nil { - // 非空且非不存在的错误,记录告警但不中断 - logrus.WithError(err).WithField("key", key).Warn("读取Redis缓存失败") - } - - // 加载数据 - val, lerr := loader() - if lerr != nil { - return nil, lerr - } - if val == nil { - return nil, nil - } - - // 写回缓存(错误不影响主流程) - if b, merr := json.Marshal(val); merr == nil { - if serr := client.Set(ctx, key, b, ttl).Err(); serr != nil { - logrus.WithError(serr).WithField("key", key).Warn("写入Redis缓存失败") - } - } - return val, nil -} - -// RedisDel 删除一个或多个Redis键(当Redis不可用时静默返回) -// - ctx: 上下文 -// - keys: 需要删除的键名 -func RedisDel(ctx context.Context, keys ...string) error { - // 如果Redis不可用则直接返回 - if !IsRedisAvailable() { - return nil - } - client := GetRedis() - if client == nil { - return nil - } - if len(keys) == 0 { - return nil - } - if _, err := client.Del(ctx, keys...).Result(); err != nil { - logrus.WithError(err).WithField("keys", keys).Warn("删除Redis键失败") - return err - } - return nil -} +package utils + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "sync" + "time" + + "github.com/redis/go-redis/v9" + "github.com/sirupsen/logrus" + "github.com/spf13/viper" + "gorm.io/gorm" +) + +// ============================================================================ +// 结构体定义 +// ============================================================================ + +// DatabaseConfig 数据库连接池配置结构体 +// 用于配置数据库连接池的各项参数,包括连接池大小、生命周期管理和健康检查等 +type DatabaseConfig struct { + // 连接池配置 + MaxIdleConns int `mapstructure:"max_idle_conns"` // 最大空闲连接数 + MaxOpenConns int `mapstructure:"max_open_conns"` // 最大打开连接数 + ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"` // 连接最大生存时间 + ConnMaxIdleTime time.Duration `mapstructure:"conn_max_idle_time"` // 连接最大空闲时间 + + // 健康检查配置 + PingTimeout time.Duration `mapstructure:"ping_timeout"` // Ping超时时间 + HealthCheckInterval time.Duration `mapstructure:"health_check_interval"` // 健康检查间隔 +} + +// ============================================================================ +// 配置函数 +// ============================================================================ + +// GetDefaultDatabaseConfig 获取默认数据库配置 +// 返回一个包含合理默认值的数据库配置实例 +func GetDefaultDatabaseConfig() *DatabaseConfig { + return &DatabaseConfig{ + MaxIdleConns: 10, // 默认最大空闲连接数 + MaxOpenConns: 100, // 默认最大打开连接数 + ConnMaxLifetime: 30 * time.Minute, // 连接最大生存时间30分钟 + ConnMaxIdleTime: 10 * time.Minute, // 连接最大空闲时间10分钟 + PingTimeout: 5 * time.Second, // Ping超时5秒 + HealthCheckInterval: 30 * time.Second, // 健康检查间隔30秒 + } +} + +// LoadDatabaseConfig 从配置文件加载数据库配置 +// 使用指定的前缀从viper配置中读取数据库配置,如果配置项不存在则使用默认值 +func LoadDatabaseConfig(prefix string) *DatabaseConfig { + config := GetDefaultDatabaseConfig() + + // 从viper读取配置,如果不存在则使用默认值 + if viper.IsSet(prefix + ".max_idle_conns") { + config.MaxIdleConns = viper.GetInt(prefix + ".max_idle_conns") + } + if viper.IsSet(prefix + ".max_open_conns") { + config.MaxOpenConns = viper.GetInt(prefix + ".max_open_conns") + } + if viper.IsSet(prefix + ".conn_max_lifetime") { + config.ConnMaxLifetime = viper.GetDuration(prefix + ".conn_max_lifetime") + } + if viper.IsSet(prefix + ".conn_max_idle_time") { + config.ConnMaxIdleTime = viper.GetDuration(prefix + ".conn_max_idle_time") + } + if viper.IsSet(prefix + ".ping_timeout") { + config.PingTimeout = viper.GetDuration(prefix + ".ping_timeout") + } + if viper.IsSet(prefix + ".health_check_interval") { + config.HealthCheckInterval = viper.GetDuration(prefix + ".health_check_interval") + } + + return config +} + +// ConfigureConnectionPool 配置数据库连接池 +// 根据提供的配置参数设置GORM数据库的连接池属性 +func ConfigureConnectionPool(db *gorm.DB, config *DatabaseConfig) error { + sqlDB, err := db.DB() + if err != nil { + return fmt.Errorf("获取底层数据库连接失败: %w", err) + } + + // 设置连接池参数 + sqlDB.SetMaxIdleConns(config.MaxIdleConns) + sqlDB.SetMaxOpenConns(config.MaxOpenConns) + sqlDB.SetConnMaxLifetime(config.ConnMaxLifetime) + sqlDB.SetConnMaxIdleTime(config.ConnMaxIdleTime) + + // LogInfo("数据库连接池配置完成", map[string]interface{}{ + // "max_idle_conns": config.MaxIdleConns, + // "max_open_conns": config.MaxOpenConns, + // "conn_max_lifetime": config.ConnMaxLifetime, + // "conn_max_idle_time": config.ConnMaxIdleTime, + // }) + + return nil +} + +// PingDatabase 检查数据库连接健康状态 +// 使用指定的超时时间ping数据库以验证连接是否正常 +func PingDatabase(db *gorm.DB, timeout time.Duration) error { + sqlDB, err := db.DB() + if err != nil { + return fmt.Errorf("获取底层数据库连接失败: %w", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + return sqlDB.PingContext(ctx) +} + +// GetConnectionStats 获取数据库连接池统计信息 +// 返回当前数据库连接池的详细统计数据,包括连接数、等待时间等 +func GetConnectionStats(db *gorm.DB) (*sql.DBStats, error) { + sqlDB, err := db.DB() + if err != nil { + return nil, fmt.Errorf("获取底层数据库连接失败: %w", err) + } + + stats := sqlDB.Stats() + return &stats, nil +} + +// LogConnectionStats 记录数据库连接池统计信息 +// 获取并记录数据库连接池的统计信息到日志中,用于监控和调试 +func LogConnectionStats(db *gorm.DB) { + stats, err := GetConnectionStats(db) + if err != nil { + LogError("获取数据库连接池统计信息失败", err, nil) + return + } + + LogInfo("数据库连接池统计", map[string]interface{}{ + "open_connections": stats.OpenConnections, + "in_use": stats.InUse, + "idle": stats.Idle, + "wait_count": stats.WaitCount, + "wait_duration": stats.WaitDuration, + "max_idle_closed": stats.MaxIdleClosed, + "max_idle_time_closed": stats.MaxIdleTimeClosed, + "max_lifetime_closed": stats.MaxLifetimeClosed, + }) +} + +// StartHealthCheck 启动数据库健康检查 +// 启动一个后台goroutine定期检查数据库连接健康状态 +// 只在健康检查失败时输出错误日志,正常情况下不输出日志 +func StartHealthCheck(db *gorm.DB, config *DatabaseConfig) { + go func() { + ticker := time.NewTicker(config.HealthCheckInterval) + defer ticker.Stop() + + for range ticker.C { + if err := PingDatabase(db, config.PingTimeout); err != nil { + // 只在健康检查失败时输出错误日志 + LogError("数据库健康检查失败", err, map[string]interface{}{ + "ping_timeout": config.PingTimeout, + }) + } + + // 记录连接池统计信息(仅在调试模式下) + if logrus.GetLevel() == logrus.DebugLevel { + LogConnectionStats(db) + } + } + }() + + // LogInfo("数据库健康检查已启动", map[string]interface{}{ + // "check_interval": config.HealthCheckInterval, + // "ping_timeout": config.PingTimeout, + // }) +} + +// ValidateDatabaseConfig 验证数据库配置参数 +// 检查数据库配置参数的有效性,确保所有参数都在合理范围内 +func ValidateDatabaseConfig(config *DatabaseConfig) error { + if config.MaxIdleConns < 0 { + return fmt.Errorf("最大空闲连接数不能为负数: %d", config.MaxIdleConns) + } + if config.MaxOpenConns < 0 { + return fmt.Errorf("最大打开连接数不能为负数: %d", config.MaxOpenConns) + } + if config.MaxIdleConns > config.MaxOpenConns && config.MaxOpenConns > 0 { + return fmt.Errorf("最大空闲连接数(%d)不能大于最大打开连接数(%d)", config.MaxIdleConns, config.MaxOpenConns) + } + if config.ConnMaxLifetime < 0 { + return fmt.Errorf("连接最大生存时间不能为负数: %v", config.ConnMaxLifetime) + } + if config.ConnMaxIdleTime < 0 { + return fmt.Errorf("连接最大空闲时间不能为负数: %v", config.ConnMaxIdleTime) + } + if config.PingTimeout <= 0 { + return fmt.Errorf("Ping超时时间必须大于0: %v", config.PingTimeout) + } + if config.HealthCheckInterval <= 0 { + return fmt.Errorf("健康检查间隔必须大于0: %v", config.HealthCheckInterval) + } + + return nil +} + +// ============================================================================ +// 全局变量 +// ============================================================================ + +var ( + // redisClient 全局Redis客户端 + redisClient *redis.Client + // redisOnce 确保只初始化一次 + redisOnce sync.Once + // redisAvailable 标记Redis是否可用 + redisAvailable bool +) + +// ============================================================================ +// Redis函数 +// ============================================================================ + +// InitRedis 初始化Redis客户端(仅在配置存在时尝试连接) +// - 从 viper 读取 security.redis.* 配置 +// - 如果连接失败,则标记为不可用,不影响主流程 +func InitRedis() { + redisOnce.Do(func() { + host := viper.GetString("redis.host") + port := viper.GetInt("redis.port") + if host == "" || port == 0 { + logrus.Info("未配置Redis或配置不完整,跳过初始化") + redisAvailable = false + return + } + addr := fmt.Sprintf("%s:%d", host, port) + redisClient = redis.NewClient(&redis.Options{ + Addr: addr, + Password: viper.GetString("redis.password"), + DB: viper.GetInt("redis.db"), + }) + // 健康检查 + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + if err := redisClient.Ping(ctx).Err(); err != nil { + logrus.WithError(err).Warn("Redis初始化失败,标记为不可用") + redisAvailable = false + return + } + redisAvailable = true + logrus.WithField("addr", addr).Info("Redis 连接已建立") + }) +} + +// GetRedis 获取全局Redis客户端,可能返回nil(当不可用时) +func GetRedis() *redis.Client { + if redisClient == nil { + InitRedis() + } + if !redisAvailable { + return nil + } + return redisClient +} + +// IsRedisAvailable 判断Redis是否可用 +func IsRedisAvailable() bool { + if redisClient == nil { + InitRedis() + } + return redisAvailable +} + +// RedisGetOrSet 通用Redis缓存获取或设置函数(基于JSON序列化) +// - ctx: 上下文 +// - key: 缓存键 +// - ttl: 过期时间 +// - loader: 当缓存不存在时的加载函数(一般执行数据库查询) +// 返回:目标对象指针和错误 +func RedisGetOrSet[T any](ctx context.Context, key string, ttl time.Duration, loader func() (*T, error)) (*T, error) { + // 如果Redis不可用则直接调用加载函数 + if !IsRedisAvailable() { + return loader() + } + client := GetRedis() + if client == nil { + return loader() + } + + // 先尝试从缓存读取 + data, err := client.Get(ctx, key).Bytes() + if err == nil { + var out T + if uerr := json.Unmarshal(data, &out); uerr == nil { + return &out, nil + } + // 反序列化失败时视为未命中,继续加载 + logrus.WithError(err).WithField("key", key).Warn("Redis缓存反序列化失败,回退到loader") + } else if err != redis.Nil { + // 非空且非不存在的错误,记录告警但不中断 + logrus.WithError(err).WithField("key", key).Warn("读取Redis缓存失败") + } + + // 加载数据 + val, lerr := loader() + if lerr != nil { + return nil, lerr + } + if val == nil { + return nil, nil + } + + // 写回缓存(错误不影响主流程) + if b, merr := json.Marshal(val); merr == nil { + if serr := client.Set(ctx, key, b, ttl).Err(); serr != nil { + logrus.WithError(serr).WithField("key", key).Warn("写入Redis缓存失败") + } + } + return val, nil +} + +// RedisDel 删除一个或多个Redis键(当Redis不可用时静默返回) +// - ctx: 上下文 +// - keys: 需要删除的键名 +func RedisDel(ctx context.Context, keys ...string) error { + // 如果Redis不可用则直接返回 + if !IsRedisAvailable() { + return nil + } + client := GetRedis() + if client == nil { + return nil + } + if len(keys) == 0 { + return nil + } + if _, err := client.Del(ctx, keys...).Result(); err != nil { + logrus.WithError(err).WithField("keys", keys).Warn("删除Redis键失败") + return err + } + return nil +} diff --git a/utils/encrypt/easy.go b/utils/encrypt/easy.go index c8b3675..a92bae3 100644 --- a/utils/encrypt/easy.go +++ b/utils/encrypt/easy.go @@ -8,12 +8,20 @@ import ( "strings" ) +// ============================================================================ +// 结构体定义 +// ============================================================================ + // EasyEncrypt 易加密算法结构体 type EasyEncrypt struct { encryptKey []int // 加密密钥 decryptKey []int // 解密密钥 } +// ============================================================================ +// 构造函数 +// ============================================================================ + // NewEasyEncrypt 创建新的易加密实例 func NewEasyEncrypt(encryptKey, decryptKey []int) *EasyEncrypt { return &EasyEncrypt{ @@ -22,17 +30,21 @@ func NewEasyEncrypt(encryptKey, decryptKey []int) *EasyEncrypt { } } +// ============================================================================ +// 密钥生成函数 +// ============================================================================ + // GenerateEasyKey 生成易加密密钥对 func GenerateEasyKey() ([]int, []int, error) { // 使用crypto/rand生成随机长度(15-30位) var lengthByte [1]byte - + // 生成加密密钥长度 if _, err := rand.Read(lengthByte[:]); err != nil { return nil, nil, err } encryptKeyLen := 15 + int(lengthByte[0])%16 // 15-30位随机长度 - + encryptKey := make([]int, encryptKeyLen) encryptBytes := make([]byte, encryptKeyLen) if _, err := rand.Read(encryptBytes); err != nil { @@ -47,7 +59,7 @@ func GenerateEasyKey() ([]int, []int, error) { return nil, nil, err } decryptKeyLen := 15 + int(lengthByte[0])%16 // 15-30位随机长度 - + decryptKey := make([]int, decryptKeyLen) decryptBytes := make([]byte, decryptKeyLen) if _, err := rand.Read(decryptBytes); err != nil { @@ -60,6 +72,10 @@ func GenerateEasyKey() ([]int, []int, error) { return encryptKey, decryptKey, nil } +// ============================================================================ +// 方法函数 +// ============================================================================ + // Encrypt 加密函数 - 对应 UserLogin_encrypt_Up_42510 func (e *EasyEncrypt) Encrypt(input string) string { if input == "" { @@ -140,6 +156,10 @@ func (e *EasyEncrypt) Decrypt(input string) string { return result.String() } +// ============================================================================ +// 工具函数 +// ============================================================================ + // EncryptWithKey 使用指定密钥加密 func EncryptWithKey(input string, key []int) string { if input == "" || len(key) == 0 { diff --git a/utils/errors.go b/utils/errors.go index 2a0c70c..0e8338b 100644 --- a/utils/errors.go +++ b/utils/errors.go @@ -11,14 +11,18 @@ import ( "gorm.io/gorm" ) +// ============================================================================ +// 结构体定义 +// ============================================================================ + // ErrorResponse 统一的错误响应结构 // 用于标准化API错误响应格式 type ErrorResponse struct { - Success bool `json:"success"` // 请求是否成功,错误响应时固定为false - Message string `json:"message"` // 错误消息描述 + Success bool `json:"success"` // 请求是否成功,错误响应时固定为false + Message string `json:"message"` // 错误消息描述 ErrorCode string `json:"error_code,omitempty"` // 错误代码,用于客户端识别错误类型 - Data interface{} `json:"data"` // 附加数据,可为空 - Timestamp int64 `json:"timestamp"` // 响应时间戳 + Data interface{} `json:"data"` // 附加数据,可为空 + Timestamp int64 `json:"timestamp"` // 响应时间戳 } // SuccessResponse 统一的成功响应结构 @@ -26,10 +30,14 @@ type ErrorResponse struct { type SuccessResponse struct { Success bool `json:"success"` // 请求是否成功,成功响应时固定为true Message string `json:"message"` // 成功消息描述 - Data interface{} `json:"data"` // 响应数据 + Data interface{} `json:"data"` // 响应数据 Timestamp int64 `json:"timestamp"` // 响应时间戳 } +// ============================================================================ +// 常量定义 +// ============================================================================ + // ErrorCode 错误代码常量 // 定义标准化的错误代码,用于客户端识别和处理不同类型的错误 const ( @@ -59,15 +67,19 @@ const ( // LogEntry 日志条目结构 // 包含完整的日志信息,用于结构化日志记录 type LogEntry struct { - Level LogLevel `json:"level"` // 日志级别 - Message string `json:"message"` // 日志消息 - Error string `json:"error,omitempty"` // 错误信息,仅在错误日志中存在 - Context interface{} `json:"context,omitempty"` // 上下文信息,额外的结构化数据 - Timestamp time.Time `json:"timestamp"` // 日志时间戳 - File string `json:"file"` // 源文件路径 - Line int `json:"line"` // 源文件行号 + Level LogLevel `json:"level"` // 日志级别 + Message string `json:"message"` // 日志消息 + Error string `json:"error,omitempty"` // 错误信息,仅在错误日志中存在 + Context interface{} `json:"context,omitempty"` // 上下文信息,额外的结构化数据 + Timestamp time.Time `json:"timestamp"` // 日志时间戳 + File string `json:"file"` // 源文件路径 + Line int `json:"line"` // 源文件行号 } +// ============================================================================ +// 响应函数 +// ============================================================================ + // WriteErrorResponse 写入错误响应 // c: Gin上下文 // statusCode: HTTP状态码 @@ -102,6 +114,10 @@ func WriteSuccessResponse(c *gin.Context, statusCode int, message string, data i c.JSON(statusCode, response) } +// ============================================================================ +// 错误处理函数 +// ============================================================================ + // HandleDatabaseError 处理数据库错误 // c: Gin上下文 // err: 数据库错误 @@ -152,6 +168,10 @@ func HandleInternalError(c *gin.Context, err error, operation string) { WriteErrorResponse(c, 500, "服务器内部错误", ErrCodeInternalError, nil) } +// ============================================================================ +// 日志函数 +// ============================================================================ + // LogInfo 记录信息日志 // message: 日志消息 // context: 上下文信息 @@ -189,6 +209,10 @@ func LogDebug(message string, context interface{}) { printLog(logEntry) } +// ============================================================================ +// 私有函数 +// ============================================================================ + // createLogEntry 创建日志条目 // level: 日志级别 // message: 日志消息 @@ -252,4 +276,4 @@ func getLevelString(level LogLevel) string { default: return "UNKNOWN" } -} \ No newline at end of file +} diff --git a/utils/logger/logger.go b/utils/logger/logger.go index 3297dfa..2e7a7fe 100644 --- a/utils/logger/logger.go +++ b/utils/logger/logger.go @@ -4,12 +4,20 @@ import ( log "github.com/sirupsen/logrus" ) +// ============================================================================ +// 结构体定义 +// ============================================================================ + // Logger 日志工具结构体 // 封装logrus.Logger,提供统一的日志接口 type Logger struct { *log.Logger // 嵌入logrus.Logger,继承其所有方法 } +// ============================================================================ +// 构造函数 +// ============================================================================ + // NewLogger 创建新的日志实例,使用全局logrus配置 // 返回: 新的Logger实例 func NewLogger() *Logger { @@ -32,6 +40,10 @@ func InitLogger() *Logger { return logger } +// ============================================================================ +// 方法函数 +// ============================================================================ + // WithFields 添加字段到日志条目 // fields: 要添加的字段映射 // 返回: 包含字段的日志条目 @@ -89,6 +101,10 @@ func (l *Logger) LogError(err error, msg string) { l.WithError(err).Error(msg) } +// ============================================================================ +// 全局变量 +// ============================================================================ + // GlobalLogger 全局日志实例 // 提供全局访问的日志记录器 var GlobalLogger *Logger @@ -99,6 +115,10 @@ func init() { GlobalLogger = NewLogger() } +// ============================================================================ +// 全局函数 +// ============================================================================ + // GetLogger 获取全局日志实例 // 返回: 全局Logger实例 func GetLogger() *Logger { @@ -109,4 +129,4 @@ func GetLogger() *Logger { // logger: 要设置的Logger实例 func SetGlobalLogger(logger *Logger) { GlobalLogger = logger -} \ No newline at end of file +} diff --git a/utils/logger/server.go b/utils/logger/server.go index 383f797..09e73ad 100644 --- a/utils/logger/server.go +++ b/utils/logger/server.go @@ -23,4 +23,4 @@ func (l *Logger) LogServerStop() { // configFile: 配置文件路径 func (l *Logger) LogConfigLoad(configFile string) { l.WithField("config_file", configFile).Info("配置文件加载") -} \ No newline at end of file +} diff --git a/utils/timeutil/server.go b/utils/timeutil/server.go index 11b7e97..2dc090b 100644 --- a/utils/timeutil/server.go +++ b/utils/timeutil/server.go @@ -5,9 +5,17 @@ import ( "time" ) +// ============================================================================ +// 全局变量 +// ============================================================================ + // serverStartTime 记录进程启动时间(近似服务器启动时间) var serverStartTime = time.Now() +// ============================================================================ +// 公共函数 +// ============================================================================ + // GetServerStartTime 获取服务器启动时间 // 返回: 服务器启动的时间戳 func GetServerStartTime() time.Time { diff --git a/web/public.go b/web/public.go index 990a8f9..7619e0d 100644 --- a/web/public.go +++ b/web/public.go @@ -1,73 +1,73 @@ -package web - -import ( - "embed" - "html/template" - "io/fs" - "log" - "os" - "path/filepath" - - "github.com/spf13/viper" -) - -// TemplatesFS 嵌入模板的文件系统 -// -//go:embed template/*.html template/admin/*.html -var templatesFS embed.FS - -// StaticFS 嵌入静态资源的文件系统(包含 CSS/JS 的 static 与 图片/字体等资源的 assets) -// -//go:embed static/* assets/* -var staticFS embed.FS - -// getDistRootFS 获取基于 server.dist 的本地文件系统 -// 当 server.dist 非空且路径存在时,返回对应的本地只读 FS;否则返回 nil -func getDistRootFS() fs.FS { - // 从配置中读取 server.dist - distPath := viper.GetString("server.dist") - if distPath == "" { - return nil - } - // 归一化路径,兼容相对/绝对 - absPath := distPath - if !filepath.IsAbs(distPath) { - if p, err := filepath.Abs(distPath); err == nil { - absPath = p - } - } - // 检查目录是否存在 - if info, err := os.Stat(absPath); err == nil && info.IsDir() { - return os.DirFS(absPath) - } - log.Printf("server.dist 路径无效或不可访问:%s,将回退使用嵌入资源", distPath) - return nil -} - -// ParseTemplates 解析模板 -// 优先从 server.dist 指定目录加载(当配置非空且有效),否则回退到嵌入模板 -func ParseTemplates() (*template.Template, error) { // Go 顶级函数不支持箭头写法 - if distFS := getDistRootFS(); distFS != nil { - // 期望 dist 目录下存在 template 与 template/admin 结构 - // 如:{dist}/template/*.html 与 {dist}/template/admin/*.html - return template.ParseFS(distFS, "template/*.html", "template/admin/*.html") - } - // 默认:使用嵌入模板 - return template.ParseFS(templatesFS, "template/*.html", "template/admin/*.html") -} - -// GetStaticFS 返回静态资源文件系统(包含 static 与 assets 目录) -// 优先使用 server.dist 指定的本地目录;否则回退到嵌入静态资源 -func GetStaticFS() (fs.FS, error) { // Go 顶级函数不支持箭头写法 - if distFS := getDistRootFS(); distFS != nil { - // 直接返回以 dist 根为起点的 FS,routes 中会再基于此 FS Sub 出 static 与 assets - return distFS, nil - } - return staticFS, nil -} - -// IsDevMode 检查是否为开发模式 -// 注意:这个函数保留用于向后兼容,建议使用 middleware.IsDevMode() -func IsDevMode() bool { - return viper.GetBool("server.dev_mode") -} +package web + +import ( + "embed" + "html/template" + "io/fs" + "log" + "os" + "path/filepath" + + "github.com/spf13/viper" +) + +// TemplatesFS 嵌入模板的文件系统 +// +//go:embed template/*.html template/admin/*.html +var templatesFS embed.FS + +// StaticFS 嵌入静态资源的文件系统(包含 CSS/JS 的 static 与 图片/字体等资源的 assets) +// +//go:embed static/* assets/* +var staticFS embed.FS + +// getDistRootFS 获取基于 server.dist 的本地文件系统 +// 当 server.dist 非空且路径存在时,返回对应的本地只读 FS;否则返回 nil +func getDistRootFS() fs.FS { + // 从配置中读取 server.dist + distPath := viper.GetString("server.dist") + if distPath == "" { + return nil + } + // 归一化路径,兼容相对/绝对 + absPath := distPath + if !filepath.IsAbs(distPath) { + if p, err := filepath.Abs(distPath); err == nil { + absPath = p + } + } + // 检查目录是否存在 + if info, err := os.Stat(absPath); err == nil && info.IsDir() { + return os.DirFS(absPath) + } + log.Printf("server.dist 路径无效或不可访问:%s,将回退使用嵌入资源", distPath) + return nil +} + +// ParseTemplates 解析模板 +// 优先从 server.dist 指定目录加载(当配置非空且有效),否则回退到嵌入模板 +func ParseTemplates() (*template.Template, error) { // Go 顶级函数不支持箭头写法 + if distFS := getDistRootFS(); distFS != nil { + // 期望 dist 目录下存在 template 与 template/admin 结构 + // 如:{dist}/template/*.html 与 {dist}/template/admin/*.html + return template.ParseFS(distFS, "template/*.html", "template/admin/*.html") + } + // 默认:使用嵌入模板 + return template.ParseFS(templatesFS, "template/*.html", "template/admin/*.html") +} + +// GetStaticFS 返回静态资源文件系统(包含 static 与 assets 目录) +// 优先使用 server.dist 指定的本地目录;否则回退到嵌入静态资源 +func GetStaticFS() (fs.FS, error) { // Go 顶级函数不支持箭头写法 + if distFS := getDistRootFS(); distFS != nil { + // 直接返回以 dist 根为起点的 FS,routes 中会再基于此 FS Sub 出 static 与 assets + return distFS, nil + } + return staticFS, nil +} + +// IsDevMode 检查是否为开发模式 +// 注意:这个函数保留用于向后兼容,建议使用 middleware.IsDevMode() +func IsDevMode() bool { + return viper.GetBool("server.dev_mode") +}