Add classification annotations

This commit is contained in:
2025-10-27 23:12:15 +08:00
parent 3990ec01c6
commit 5aacb88c22
44 changed files with 2769 additions and 2241 deletions

View File

@@ -1,128 +1,144 @@
package cmd package cmd
import ( import (
"io" "io"
"networkDev/config" "networkDev/config"
"networkDev/utils/logger" "networkDev/utils/logger"
"os" "os"
"path/filepath" "path/filepath"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"gopkg.in/natefinch/lumberjack.v2" "gopkg.in/natefinch/lumberjack.v2"
) )
var cfgFile string // ============================================================================
// 全局变量
// rootCmd 代表没有调用子命令时的基础命令 // ============================================================================
var rootCmd = &cobra.Command{
Use: "networkDev", var cfgFile string
Short: "一个基于Cobra的网络验证服务器应用",
Long: `networkDev是一个使用Cobra CLI框架构建的网络验证服务器应用 // ============================================================================
集成了Viper配置管理、Logrus日志记录和embed静态资源嵌入功能。`, // 命令定义
PersistentPreRun: func(cmd *cobra.Command, args []string) { // ============================================================================
// 在加载配置前配置logrus用于非HTTP日志
// rootCmd 代表没有调用子命令时的基础命令
setupLogrusForNonHTTP() var rootCmd = &cobra.Command{
Use: "networkDev",
}, Short: "一个基于Cobra的网络验证服务器应用",
} Long: `networkDev是一个使用Cobra CLI框架构建的网络验证服务器应用
集成了Viper配置管理、Logrus日志记录和embed静态资源嵌入功能。`,
// Execute 添加所有子命令到根命令并设置适当的标志 PersistentPreRun: func(cmd *cobra.Command, args []string) {
// 这由main.main()调用。只需要对rootCmd执行一次。 // 在加载配置前配置logrus用于非HTTP日志
func Execute() {
err := rootCmd.Execute() setupLogrusForNonHTTP()
if err != nil {
os.Exit(1) },
} }
}
// ============================================================================
func init() { // 公共函数
cobra.OnInitialize(initConfig) // ============================================================================
// 在这里定义标志和配置设置 // Execute 添加所有子命令到根命令并设置适当的标志
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "配置文件路径 (默认为 config.json)") // 这由main.main()调用。只需要对rootCmd执行一次。
} func Execute() {
err := rootCmd.Execute()
// setupLogrusForNonHTTP 配置logrus用于非HTTP日志 if err != nil {
// 在加载配置文件之前进行基本的logrus设置 os.Exit(1)
func setupLogrusForNonHTTP() { }
// 设置日志格式 }
logrus.SetFormatter(&logrus.TextFormatter{
TimestampFormat: "2006-01-02 15:04:05", func init() {
FullTimestamp: true, cobra.OnInitialize(initConfig)
ForceColors: false,
DisableColors: true, // 在这里定义标志和配置设置
}) rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "配置文件路径 (默认为 config.json)")
}
// 设置默认日志级别
logrus.SetLevel(logrus.InfoLevel) // ============================================================================
// 私有函数
// 设置输出目标(稍后会根据配置文件调整) // ============================================================================
logrus.SetOutput(os.Stdout)
if cfgFile != "" { // setupLogrusForNonHTTP 配置logrus用于非HTTP日志
// 使用命令行指定的配置文件 // 在加载配置文件之前进行基本的logrus设置
config.Init(cfgFile) func setupLogrusForNonHTTP() {
} else { // 设置日志格式
// 使用默认配置文件路径 logrus.SetFormatter(&logrus.TextFormatter{
config.Init("./config.json") TimestampFormat: "2006-01-02 15:04:05",
} FullTimestamp: true,
ForceColors: false,
// 根据配置文件进一步配置logrus DisableColors: true,
setupLogrusFromConfig() })
// 初始化HTTP日志处理器 // 设置默认日志级别
logger.InitLogger() logrus.SetLevel(logrus.InfoLevel)
// 记录配置加载完成 // 设置输出目标(稍后会根据配置文件调整)
logrus.WithField("config_file", viper.ConfigFileUsed()).Info("配置文件加载完成") logrus.SetOutput(os.Stdout)
} if cfgFile != "" {
// 使用命令行指定的配置文件
// initConfig 读取配置文件和环境变量 config.Init(cfgFile)
func initConfig() { } else {
// 使用默认配置文件路径
} config.Init("./config.json")
}
// setupLogrusFromConfig 根据配置文件进一步配置logrus
// 设置日志级别和输出目标,支持日志切割功能 // 根据配置文件进一步配置logrus
func setupLogrusFromConfig() { setupLogrusFromConfig()
// 设置日志级别
if level := viper.GetString("log.level"); level != "" { // 初始化HTTP日志处理器
if logLevel, err := logrus.ParseLevel(level); err == nil { logger.InitLogger()
logrus.SetLevel(logLevel)
} // 记录配置加载完成
} logrus.WithField("config_file", viper.ConfigFileUsed()).Info("配置文件加载完成")
}
// 设置日志输出目标
logFile := viper.GetString("log.file") // initConfig 读取配置文件和环境变量
if logFile != "" { func initConfig() {
// 确保日志目录存在
logDir := filepath.Dir(logFile) }
if err := os.MkdirAll(logDir, 0755); err != nil {
logrus.WithError(err).Error("创建日志目录失败") // setupLogrusFromConfig 根据配置文件进一步配置logrus
return // 设置日志级别和输出目标,支持日志切割功能
} func setupLogrusFromConfig() {
// 设置日志级别
// 配置lumberjack日志轮转 if level := viper.GetString("log.level"); level != "" {
lumberjackLogger := &lumberjack.Logger{ if logLevel, err := logrus.ParseLevel(level); err == nil {
Filename: logFile, logrus.SetLevel(logLevel)
MaxSize: viper.GetInt("log.max_size"), // MB }
MaxBackups: viper.GetInt("log.max_backups"), // 保留的旧日志文件数量 }
MaxAge: viper.GetInt("log.max_age"), // 天数
Compress: true, // 压缩旧日志文件 // 设置日志输出目标
} logFile := viper.GetString("log.file")
if logFile != "" {
// 同时输出到控制台和文件(带日志切割) // 确保日志目录存在
multiWriter := io.MultiWriter(os.Stdout, lumberjackLogger) logDir := filepath.Dir(logFile)
logrus.SetOutput(multiWriter) if err := os.MkdirAll(logDir, 0755); err != nil {
logrus.WithError(err).Error("创建日志目录失败")
logrus.WithFields(logrus.Fields{ return
"file": logFile, }
"max_size": viper.GetInt("log.max_size"),
"max_backups": viper.GetInt("log.max_backups"), // 配置lumberjack日志轮转
"max_age": viper.GetInt("log.max_age"), lumberjackLogger := &lumberjack.Logger{
}).Info("日志切割功能已启用") 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("日志切割功能已启用")
}
// 当日志文件路径为空时,保持默认输出到控制台,不创建任何目录
}

View File

@@ -1,190 +1,206 @@
package cmd package cmd
import ( import (
"context" "context"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
"time" "time"
"networkDev/database" "networkDev/database"
"networkDev/middleware" "networkDev/middleware"
"networkDev/server" "networkDev/server"
"networkDev/utils" "networkDev/utils"
"networkDev/utils/logger" "networkDev/utils/logger"
"networkDev/web" "networkDev/web"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
// serverCmd 代表服务器命令 // ============================================================================
var serverCmd = &cobra.Command{ // 命令定义
Use: "server", // ============================================================================
Short: "启动HTTP服务器",
Long: `启动一个简单的HTTP服务器监听配置文件中指定的端口。`, // serverCmd 代表服务器命令
Run: runServer, var serverCmd = &cobra.Command{
} Use: "server",
Short: "启动HTTP服务器",
func init() { Long: `启动一个简单的HTTP服务器监听配置文件中指定的端口。`,
// 将服务器命令添加到根命令 Run: runServer,
rootCmd.AddCommand(serverCmd) }
// 添加服务器特定的标志 // ============================================================================
serverCmd.Flags().StringP("host", "H", "", "服务器监听地址 (覆盖配置文件)") // 初始化函数
serverCmd.Flags().IntP("port", "p", 0, "服务器监听端口 (覆盖配置文件)") // ============================================================================
}
func init() {
// runServer 运行HTTP服务器 // 将服务器命令添加到根命令
func runServer(cmd *cobra.Command, args []string) { rootCmd.AddCommand(serverCmd)
// 获取配置
host := getServerHost(cmd) // 添加服务器特定的标志
port := getServerPort(cmd) serverCmd.Flags().StringP("host", "H", "", "服务器监听地址 (覆盖配置文件)")
addr := fmt.Sprintf("%s:%d", host, port) serverCmd.Flags().IntP("port", "p", 0, "服务器监听端口 (覆盖配置文件)")
}
// 获取全局日志实例
logger := logger.GetLogger() // ============================================================================
logger.LogServerStart(host, port) // 主要函数
// ============================================================================
// 初始化Redis如果配置存在失败不致命
utils.InitRedis() // runServer 运行HTTP服务器
func runServer(cmd *cobra.Command, args []string) {
// 初始化数据库(根据 viper 配置选择 SQLite 或 MySQL // 获取配置
// 如果初始化失败则回退并退出 host := getServerHost(cmd)
if _, err := database.Init(); err != nil { port := getServerPort(cmd)
logrus.WithError(err).Fatal("数据库初始化失败") addr := fmt.Sprintf("%s:%d", host, port)
}
// 执行自动迁移(确保表结构存在) // 获取全局日志实例
if err := database.AutoMigrate(); err != nil { logger := logger.GetLogger()
logrus.WithError(err).Fatal("数据库自动迁移失败") logger.LogServerStart(host, port)
}
// 初始化默认系统设置(包含管理员账号 // 初始化Redis如果配置存在失败不致命
if err := database.SeedDefaultSettings(); err != nil { utils.InitRedis()
logrus.WithError(err).Fatal("默认系统设置初始化失败")
} // 初始化数据库(根据 viper 配置选择 SQLite 或 MySQL
// 如果初始化失败则回退并退出
// 创建HTTP服务器 if _, err := database.Init(); err != nil {
server := createHTTPServer(addr) logrus.WithError(err).Fatal("数据库初始化失败")
}
// 启动服务器 // 执行自动迁移(确保表结构存在)
startServer(server) if err := database.AutoMigrate(); err != nil {
} logrus.WithError(err).Fatal("数据库自动迁移失败")
}
// getServerHost 获取服务器监听地址 // 初始化默认系统设置(包含管理员账号)
func getServerHost(cmd *cobra.Command) string { if err := database.SeedDefaultSettings(); err != nil {
if host, _ := cmd.Flags().GetString("host"); host != "" { logrus.WithError(err).Fatal("默认系统设置初始化失败")
return host }
}
return viper.GetString("server.host") // 创建HTTP服务器
} server := createHTTPServer(addr)
// getServerPort 获取服务器监听端口 // 启动服务器
func getServerPort(cmd *cobra.Command) int { startServer(server)
if port, _ := cmd.Flags().GetInt("port"); port != 0 { }
return port
} // ============================================================================
return viper.GetInt("server.port") // 辅助函数
} // ============================================================================
// createHTTPServer 创建HTTP服务器 // getServerHost 获取服务器监听地址
func createHTTPServer(addr string) *http.Server { func getServerHost(cmd *cobra.Command) string {
// 配置Gin模式和日志 if host, _ := cmd.Flags().GetString("host"); host != "" {
configureGin() return host
}
// 创建Gin引擎 return viper.GetString("server.host")
router := gin.New() }
// 添加恢复中间件 // getServerPort 获取服务器监听端口
router.Use(gin.Recovery()) func getServerPort(cmd *cobra.Command) int {
if port, _ := cmd.Flags().GetInt("port"); port != 0 {
// 添加日志中间件 return port
router.Use(middleware.WrapHandler()) }
return viper.GetInt("server.port")
// 添加开发模式中间件(统一管理开发模式功能) }
router.Use(middleware.DevModeMiddleware(router))
// createHTTPServer 创建HTTP服务器
// 加载模板 func createHTTPServer(addr string) *http.Server {
if err := loadTemplates(router); err != nil { // 配置Gin模式和日志
logrus.WithError(err).Fatal("模板加载失败") configureGin()
}
// 创建Gin引擎
// 注册路由 router := gin.New()
registerRoutes(router)
// 添加恢复中间件
return &http.Server{ router.Use(gin.Recovery())
Addr: addr,
Handler: router, // 添加日志中间件
} router.Use(middleware.WrapHandler())
}
// 添加开发模式中间件(统一管理开发模式功能)
// loadTemplates 加载模板到Gin引擎 router.Use(middleware.DevModeMiddleware(router))
func loadTemplates(router *gin.Engine) error {
tmpl, err := web.ParseTemplates() // 加载模板
if err != nil { if err := loadTemplates(router); err != nil {
return err logrus.WithError(err).Fatal("模板加载失败")
} }
router.SetHTMLTemplate(tmpl)
return nil // 注册路由
} registerRoutes(router)
// registerRoutes 注册HTTP路由 return &http.Server{
func registerRoutes(router *gin.Engine) { Addr: addr,
// 使用server包中的路由注册函数 Handler: router,
server.RegisterRoutes(router) }
} }
// startServer 启动服务器并处理优雅关闭 // loadTemplates 加载模板到Gin引擎
func startServer(server *http.Server) { func loadTemplates(router *gin.Engine) error {
// 获取全局日志实例 tmpl, err := web.ParseTemplates()
logger := logger.GetLogger() if err != nil {
return err
// 创建一个通道来接收操作系统信号 }
sigChan := make(chan os.Signal, 1) router.SetHTMLTemplate(tmpl)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) return nil
}
// 在goroutine中启动服务器
go func() { // registerRoutes 注册HTTP路由
logger.WithField("addr", server.Addr).Info("HTTP服务器已启动") func registerRoutes(router *gin.Engine) {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { // 使用server包中的路由注册函数
logger.LogError(err, "服务器启动失败") server.RegisterRoutes(router)
os.Exit(1) }
}
}() // startServer 启动服务器并处理优雅关闭
func startServer(server *http.Server) {
// 等待中断信号 // 获取全局日志实例
<-sigChan logger := logger.GetLogger()
logger.Info("收到关闭信号,正在优雅关闭服务器...")
// 创建一个通道来接收操作系统信号
// 创建一个带超时的上下文 sigChan := make(chan os.Signal, 1)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
defer cancel()
// 在goroutine中启动服务器
// 优雅关闭服务器 go func() {
if err := server.Shutdown(ctx); err != nil { logger.WithField("addr", server.Addr).Info("HTTP服务器已启动")
logger.LogError(err, "服务器关闭时出错") if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
} else { logger.LogError(err, "服务器启动失败")
logger.LogServerStop() os.Exit(1)
} }
} }()
// configureGin 配置Gin的全局设置 // 等待中断信号
func configureGin() { <-sigChan
// 禁用Gin的颜色输出提高控制台兼容性 logger.Info("收到关闭信号,正在优雅关闭服务器...")
gin.DisableConsoleColor()
// 创建一个带超时的上下文
// 设置Gin的输出为丢弃因为我们使用自定义日志中间件 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
gin.DefaultWriter = io.Discard defer cancel()
gin.DefaultErrorWriter = io.Discard
// 优雅关闭服务器
// 根据配置设置Gin模式 if err := server.Shutdown(ctx); err != nil {
if viper.GetString("app.mode") == "production" { logger.LogError(err, "服务器关闭时出错")
gin.SetMode(gin.ReleaseMode) } else {
} else { logger.LogServerStop()
gin.SetMode(gin.DebugMode) }
} }
}
// 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)
}
}

View File

@@ -1,269 +1,277 @@
package config package config
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"io/fs" "io/fs"
"os" "os"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
// ServerConfig 服务器配置结构体 // ============================================================================
// 包含服务器运行相关的配置信息 // 结构体定义
type ServerConfig struct { // ============================================================================
Host string `json:"host" mapstructure:"host"` // 服务器监听地址
Port int `json:"port" mapstructure:"port"` // 服务器监听端口 // ServerConfig 服务器配置结构体
Dist string `json:"dist" mapstructure:"dist"` // 静态文件目录 // 包含服务器运行相关的配置信息
DevMode bool `json:"dev_mode" mapstructure:"dev_mode"` // 开发模式(跳过验证码等) type ServerConfig struct {
} Host string `json:"host" mapstructure:"host"` // 服务器监听地址
Port int `json:"port" mapstructure:"port"` // 服务器监听端口
// DatabaseConfig 数据库配置结构体 Dist string `json:"dist" mapstructure:"dist"` // 静态文件目录
// 包含数据库连接相关的配置信息 DevMode bool `json:"dev_mode" mapstructure:"dev_mode"` // 开发模式(跳过验证码等)
type DatabaseConfig struct { }
Type string `json:"type" mapstructure:"type"` // 数据库类型mysql/sqlite
MySQL MySQLConfig `json:"mysql" mapstructure:"mysql"` // MySQL配置 // DatabaseConfig 数据库配置结构体
SQLite SQLiteConfig `json:"sqlite" mapstructure:"sqlite"` // SQLite配置 // 包含数据库连接相关的配置信息
} type DatabaseConfig struct {
Type string `json:"type" mapstructure:"type"` // 数据库类型mysql/sqlite
// MySQLConfig MySQL数据库配置结构体 MySQL MySQLConfig `json:"mysql" mapstructure:"mysql"` // MySQL配置
// 包含MySQL数据库连接的详细配置信息 SQLite SQLiteConfig `json:"sqlite" mapstructure:"sqlite"` // SQLite配置
type MySQLConfig struct { }
Host string `json:"host" mapstructure:"host"` // 数据库主机地址
Port int `json:"port" mapstructure:"port"` // 数据库端口 // MySQLConfig MySQL数据库配置结构体
Username string `json:"username" mapstructure:"username"` // 数据库用户名 // 包含MySQL数据库连接的详细配置信息
Password string `json:"password" mapstructure:"password"` // 数据库密码 type MySQLConfig struct {
Database string `json:"database" mapstructure:"database"` // 数据库名称 Host string `json:"host" mapstructure:"host"` // 数据库主机地址
Charset string `json:"charset" mapstructure:"charset"` // 字符集 Port int `json:"port" mapstructure:"port"` // 数据库端口
MaxIdleConns int `json:"max_idle_conns" mapstructure:"max_idle_conns"` // 最大空闲连接数 Username string `json:"username" mapstructure:"username"` // 数据库用户名
MaxOpenConns int `json:"max_open_conns" mapstructure:"max_open_conns"` // 最大打开连接数 Password string `json:"password" mapstructure:"password"` // 数据库密码
} Database string `json:"database" mapstructure:"database"` // 数据库名称
Charset string `json:"charset" mapstructure:"charset"` // 字符集
// SQLiteConfig SQLite数据库配置结构体 MaxIdleConns int `json:"max_idle_conns" mapstructure:"max_idle_conns"` // 最大空闲连接数
// 包含SQLite数据库文件路径配置 MaxOpenConns int `json:"max_open_conns" mapstructure:"max_open_conns"` // 最大打开连接数
type SQLiteConfig struct { }
Path string `json:"path" mapstructure:"path"` // 数据库文件路径
} // SQLiteConfig SQLite数据库配置结构体
// 包含SQLite数据库文件路径配置
// RedisConfig Redis配置结构体 type SQLiteConfig struct {
// 包含Redis连接相关的配置信息 Path string `json:"path" mapstructure:"path"` // 数据库文件路径
type RedisConfig struct { }
Host string `json:"host" mapstructure:"host"` // Redis服务器地址
Port int `json:"port" mapstructure:"port"` // Redis服务器端口 // RedisConfig Redis配置结构体
Password string `json:"password" mapstructure:"password"` // Redis密码 // 包含Redis连接相关的配置信息
DB int `json:"db" mapstructure:"db"` // Redis数据库编号 type RedisConfig struct {
} Host string `json:"host" mapstructure:"host"` // Redis服务器地址
Port int `json:"port" mapstructure:"port"` // Redis服务器端口
// LogConfig 日志配置结构体 Password string `json:"password" mapstructure:"password"` // Redis密码
// 包含日志记录相关的配置信息 DB int `json:"db" mapstructure:"db"` // Redis数据库编号
type LogConfig struct { }
Level string `json:"level" mapstructure:"level"` // 日志级别
File string `json:"file" mapstructure:"file"` // 日志文件路径 // LogConfig 日志配置结构体
MaxSize int `json:"max_size" mapstructure:"max_size"` // 单个日志文件最大大小(MB) // 包含日志记录相关的配置信息
MaxBackups int `json:"max_backups" mapstructure:"max_backups"` // 保留的旧日志文件数量 type LogConfig struct {
MaxAge int `json:"max_age" mapstructure:"max_age"` // 日志文件保留天数 Level string `json:"level" mapstructure:"level"` // 日志级别
} File string `json:"file" mapstructure:"file"` // 日志文件路径
MaxSize int `json:"max_size" mapstructure:"max_size"` // 单个日志文件最大大小(MB)
// CookieConfig Cookie配置结构体 MaxBackups int `json:"max_backups" mapstructure:"max_backups"` // 保留的旧日志文件数量
// 包含Cookie相关的安全配置信息 MaxAge int `json:"max_age" mapstructure:"max_age"` // 日志文件保留天数
type CookieConfig struct { }
Secure bool `json:"secure" mapstructure:"secure"` // 是否只在HTTPS下发送Cookie
SameSite string `json:"same_site" mapstructure:"same_site"` // SameSite属性Strict/Lax/None // CookieConfig Cookie配置结构体
Domain string `json:"domain" mapstructure:"domain"` // Cookie域名 // 包含Cookie相关的安全配置信息
MaxAge int `json:"max_age" mapstructure:"max_age"` // Cookie最大存活时间 type CookieConfig struct {
} Secure bool `json:"secure" mapstructure:"secure"` // 是否只在HTTPS下发送Cookie
SameSite string `json:"same_site" mapstructure:"same_site"` // SameSite属性Strict/Lax/None
// SecurityConfig 安全配置结构体 Domain string `json:"domain" mapstructure:"domain"` // Cookie域名
// 包含应用程序安全相关的配置信息 MaxAge int `json:"max_age" mapstructure:"max_age"` // Cookie最大存活时间
type SecurityConfig struct { }
JWTSecret string `json:"jwt_secret" mapstructure:"jwt_secret"` // JWT签名密钥
EncryptionKey string `json:"encryption_key" mapstructure:"encryption_key"` // 数据加密密钥 // SecurityConfig 安全配置结构体
JWTRefresh int `json:"jwt_refresh" mapstructure:"jwt_refresh"` // JWT令牌刷新阈值小时 // 包含应用程序安全相关的配置信息
Cookie CookieConfig `json:"cookie" mapstructure:"cookie"` // Cookie配置 type SecurityConfig struct {
} JWTSecret string `json:"jwt_secret" mapstructure:"jwt_secret"` // JWT签名密钥
EncryptionKey string `json:"encryption_key" mapstructure:"encryption_key"` // 数据加密密钥
// AppConfig 应用配置结构体 JWTRefresh int `json:"jwt_refresh" mapstructure:"jwt_refresh"` // JWT令牌刷新阈值小时
type AppConfig struct { Cookie CookieConfig `json:"cookie" mapstructure:"cookie"` // Cookie配置
Server ServerConfig `json:"server" mapstructure:"server"` }
Database DatabaseConfig `json:"database" mapstructure:"database"`
Redis RedisConfig `json:"redis" mapstructure:"redis"` // AppConfig 应用配置结构体
Log LogConfig `json:"log" mapstructure:"log"` type AppConfig struct {
Security SecurityConfig `json:"security" mapstructure:"security"` Server ServerConfig `json:"server" mapstructure:"server"`
} Database DatabaseConfig `json:"database" mapstructure:"database"`
Redis RedisConfig `json:"redis" mapstructure:"redis"`
// GetDefaultAppConfig 获取默认应用配置 Log LogConfig `json:"log" mapstructure:"log"`
func GetDefaultAppConfig() *AppConfig { Security SecurityConfig `json:"security" mapstructure:"security"`
return &AppConfig{ }
Server: ServerConfig{
Host: "0.0.0.0", // ============================================================================
Port: 8080, // 公共函数
Dist: "", // ============================================================================
DevMode: false,
}, // GetDefaultAppConfig 获取默认应用配置
Database: DatabaseConfig{ func GetDefaultAppConfig() *AppConfig {
Type: "sqlite", return &AppConfig{
MySQL: MySQLConfig{ Server: ServerConfig{
Host: "localhost", Host: "0.0.0.0",
Port: 3306, Port: 8080,
Username: "root", Dist: "",
Password: "password", DevMode: false,
Database: "networkdev", },
Charset: "utf8mb4", Database: DatabaseConfig{
MaxIdleConns: 10, Type: "sqlite",
MaxOpenConns: 100, MySQL: MySQLConfig{
}, Host: "localhost",
SQLite: SQLiteConfig{ Port: 3306,
Path: "./database.db", Username: "root",
}, Password: "password",
}, Database: "networkdev",
Redis: RedisConfig{ Charset: "utf8mb4",
Host: "localhost", MaxIdleConns: 10,
Port: 6379, MaxOpenConns: 100,
Password: "", },
DB: 0, SQLite: SQLiteConfig{
}, Path: "./database.db",
Log: LogConfig{ },
Level: "info", },
File: "./logs/app.log", Redis: RedisConfig{
MaxSize: 100, Host: "localhost",
MaxBackups: 5, Port: 6379,
MaxAge: 30, Password: "",
}, DB: 0,
Security: SecurityConfig{ },
JWTSecret: "", Log: LogConfig{
EncryptionKey: "", Level: "info",
JWTRefresh: 6, File: "./logs/app.log",
Cookie: CookieConfig{ MaxSize: 100,
Secure: true, MaxBackups: 5,
SameSite: "Lax", MaxAge: 30,
Domain: "", },
MaxAge: 86400, Security: SecurityConfig{
}, JWTSecret: "",
}, EncryptionKey: "",
} JWTRefresh: 6,
} Cookie: CookieConfig{
Secure: true,
// GetSecureDefaultAppConfig 获取带有安全密钥的默认应用配置 SameSite: "Lax",
func GetSecureDefaultAppConfig() (*AppConfig, error) { Domain: "",
config := GetDefaultAppConfig() MaxAge: 86400,
},
// 生成安全密钥 },
jwtSecret, encryptionKey, err := GenerateSecureKeys() }
if err != nil { }
return nil, err
} // GetSecureDefaultAppConfig 获取带有安全密钥的默认应用配置
func GetSecureDefaultAppConfig() (*AppConfig, error) {
// 设置安全密钥 config := GetDefaultAppConfig()
config.Security.JWTSecret = jwtSecret
config.Security.EncryptionKey = encryptionKey // 生成安全密钥
jwtSecret, encryptionKey, err := GenerateSecureKeys()
return config, nil if err != nil {
} return nil, err
}
// Init 初始化配置文件
func Init(cfgFilePath string) { // 设置安全密钥
viper.SetConfigFile(cfgFilePath) config.Security.JWTSecret = jwtSecret
viper.SetConfigType("json") config.Security.EncryptionKey = encryptionKey
viper.AddConfigPath(".")
return config, nil
if err := viper.ReadInConfig(); err != nil { }
var pathError *fs.PathError
if errors.As(err, &pathError) { // Init 初始化配置文件
log.Warn("未找到配置文件,使用默认配置") func Init(cfgFilePath string) {
viper.SetConfigFile(cfgFilePath)
// 生成带有安全密钥的默认配置 viper.SetConfigType("json")
defaultConfig, configErr := GetSecureDefaultAppConfig() viper.AddConfigPath(".")
if configErr != nil {
log.WithFields( if err := viper.ReadInConfig(); err != nil {
log.Fields{ var pathError *fs.PathError
"err": configErr, if errors.As(err, &pathError) {
}, log.Warn("未找到配置文件,使用默认配置")
).Error("生成安全配置失败,使用基础默认配置")
defaultConfig = GetDefaultAppConfig() // 生成带有安全密钥的默认配置
} defaultConfig, configErr := GetSecureDefaultAppConfig()
if configErr != nil {
// 将配置结构体转换为JSON log.WithFields(
configBytes, marshalErr := json.MarshalIndent(defaultConfig, "", " ") log.Fields{
if marshalErr != nil { "err": configErr,
log.WithFields( },
log.Fields{ ).Error("生成安全配置失败,使用基础默认配置")
"err": marshalErr, defaultConfig = GetDefaultAppConfig()
}, }
).Fatal("序列化默认配置失败")
return // 将配置结构体转换为JSON
} configBytes, marshalErr := json.MarshalIndent(defaultConfig, "", " ")
if marshalErr != nil {
// 写入配置文件 log.WithFields(
err = os.WriteFile(cfgFilePath, configBytes, 0o644) log.Fields{
if err != nil { "err": marshalErr,
log.WithFields( },
log.Fields{ ).Fatal("序列化默认配置失败")
"err": err, return
}, }
).Error("写入默认配置文件失败")
} else { // 写入配置文件
log.WithFields( err = os.WriteFile(cfgFilePath, configBytes, 0o644)
log.Fields{ if err != nil {
"file": cfgFilePath, log.WithFields(
}, log.Fields{
).Info("写入默认配置文件成功(已生成安全密钥)") "err": err,
} },
).Error("写入默认配置文件失败")
// 将配置加载到viper中 } else {
err = viper.ReadConfig(bytes.NewBuffer(configBytes)) log.WithFields(
if err != nil { log.Fields{
log.WithFields( "file": cfgFilePath,
log.Fields{ },
"err": err, ).Info("写入默认配置文件成功(已生成安全密钥)")
}, }
).Error("读取默认配置失败")
} else { // 将配置加载到viper中
log.Info("已成功读取默认配置") err = viper.ReadConfig(bytes.NewBuffer(configBytes))
} if err != nil {
} else { log.WithFields(
log.WithFields( log.Fields{
log.Fields{ "err": err,
"err": err, },
}, ).Error("读取默认配置失败")
).Fatal("配置文件解析错误") } else {
} log.Info("已成功读取默认配置")
} }
log.WithFields( } else {
log.Fields{ log.WithFields(
"file": viper.ConfigFileUsed(), log.Fields{
}, "err": err,
).Info("使用配置文件") },
).Fatal("配置文件解析错误")
// 验证配置 }
if _, err := ValidateConfig(); err != nil { }
log.WithFields( log.WithFields(
log.Fields{ log.Fields{
"err": err, "file": viper.ConfigFileUsed(),
}, },
).Fatal("配置验证失败") ).Info("使用配置文件")
}
} // 验证配置
if _, err := ValidateConfig(); err != nil {
// CreateDefaultConfig 创建默认配置文件 log.WithFields(
func CreateDefaultConfig(filePath string) error { log.Fields{
// 生成带有安全密钥的默认配置 "err": err,
defaultConfig, err := GetSecureDefaultAppConfig() },
if err != nil { ).Fatal("配置验证失败")
log.WithFields( }
log.Fields{ }
"err": err,
}, // CreateDefaultConfig 创建默认配置文件
).Error("生成安全配置失败,使用基础默认配置") func CreateDefaultConfig(filePath string) error {
defaultConfig = GetDefaultAppConfig() // 生成带有安全密钥的默认配置
} defaultConfig, err := GetSecureDefaultAppConfig()
if err != nil {
// 将配置结构体转换为JSON log.WithFields(
configBytes, err := json.MarshalIndent(defaultConfig, "", " ") log.Fields{
if err != nil { "err": err,
return err },
} ).Error("生成安全配置失败,使用基础默认配置")
defaultConfig = GetDefaultAppConfig()
return os.WriteFile(filePath, configBytes, 0o644) }
}
// 将配置结构体转换为JSON
configBytes, err := json.MarshalIndent(defaultConfig, "", " ")
if err != nil {
return err
}
return os.WriteFile(filePath, configBytes, 0o644)
}

View File

@@ -7,6 +7,10 @@ import (
"fmt" "fmt"
) )
// ============================================================================
// 公共函数
// ============================================================================
// GenerateSecureJWTSecret 生成安全的JWT密钥 // GenerateSecureJWTSecret 生成安全的JWT密钥
// 生成64字节512位的随机密钥使用base64编码 // 生成64字节512位的随机密钥使用base64编码
func GenerateSecureJWTSecret() (string, error) { func GenerateSecureJWTSecret() (string, error) {
@@ -15,7 +19,7 @@ func GenerateSecureJWTSecret() (string, error) {
if _, err := rand.Read(bytes); err != nil { if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("生成JWT密钥失败: %w", err) return "", fmt.Errorf("生成JWT密钥失败: %w", err)
} }
// 使用base64编码便于配置文件存储 // 使用base64编码便于配置文件存储
return base64.StdEncoding.EncodeToString(bytes), nil return base64.StdEncoding.EncodeToString(bytes), nil
} }
@@ -28,7 +32,7 @@ func GenerateSecureEncryptionKey() (string, error) {
if _, err := rand.Read(bytes); err != nil { if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("生成加密密钥失败: %w", err) return "", fmt.Errorf("生成加密密钥失败: %w", err)
} }
// 使用十六进制编码 // 使用十六进制编码
return hex.EncodeToString(bytes), nil return hex.EncodeToString(bytes), nil
} }
@@ -39,11 +43,11 @@ func GenerateSecureKeys() (jwtSecret, encryptionKey string, err error) {
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
encryptionKey, err = GenerateSecureEncryptionKey() encryptionKey, err = GenerateSecureEncryptionKey()
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
return jwtSecret, encryptionKey, nil return jwtSecret, encryptionKey, nil
} }

View File

@@ -13,6 +13,10 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
// ============================================================================
// 公共函数
// ============================================================================
// ValidateConfig 验证配置 // ValidateConfig 验证配置
func ValidateConfig() (*AppConfig, error) { func ValidateConfig() (*AppConfig, error) {
var config AppConfig var config AppConfig
@@ -31,6 +35,10 @@ func ValidateConfig() (*AppConfig, error) {
return &config, nil return &config, nil
} }
// ============================================================================
// 私有函数
// ============================================================================
// validateConfig 验证配置 // validateConfig 验证配置
func validateConfig(config *AppConfig) error { func validateConfig(config *AppConfig) error {
// 验证服务器配置 // 验证服务器配置

View File

@@ -1,7 +1,11 @@
package constants package constants
// 应用程序版本信息 // ============================================================================
const ( // 常量定义
// AppVersion 应用程序版本号 // ============================================================================
AppVersion = "0.3.0"
) // 应用程序版本信息
const (
// AppVersion 应用程序版本号
AppVersion = "0.3.0"
)

View File

@@ -13,9 +13,17 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// ============================================================================
// 全局变量
// ============================================================================
// 创建基础控制器实例 // 创建基础控制器实例
var apiBaseController = controllers.NewBaseController() var apiBaseController = controllers.NewBaseController()
// ============================================================================
// 页面处理器
// ============================================================================
// APIFragmentHandler 接口列表页面片段处理器 // APIFragmentHandler 接口列表页面片段处理器
func APIFragmentHandler(c *gin.Context) { func APIFragmentHandler(c *gin.Context) {
c.HTML(http.StatusOK, "apis.html", gin.H{ c.HTML(http.StatusOK, "apis.html", gin.H{
@@ -23,6 +31,10 @@ func APIFragmentHandler(c *gin.Context) {
}) })
} }
// ============================================================================
// API处理器
// ============================================================================
// APIListHandler 接口列表API处理器 // APIListHandler 接口列表API处理器
func APIListHandler(c *gin.Context) { func APIListHandler(c *gin.Context) {
// 获取分页参数 // 获取分页参数
@@ -142,6 +154,10 @@ func APIListHandler(c *gin.Context) {
c.JSON(http.StatusOK, response) c.JSON(http.StatusOK, response)
} }
// ============================================================================
// 辅助函数
// ============================================================================
// getAPIStatusName 获取API状态名称 // getAPIStatusName 获取API状态名称
func getAPIStatusName(status int) string { func getAPIStatusName(status int) string {
switch status { switch status {
@@ -238,7 +254,7 @@ func APIGetTypesHandler(c *gin.Context) {
validTypes := []int{ validTypes := []int{
models.APITypeGetBulletin, models.APITypeGetUpdateUrl, models.APITypeCheckAppVersion, models.APITypeGetCardInfo, models.APITypeGetBulletin, models.APITypeGetUpdateUrl, models.APITypeCheckAppVersion, models.APITypeGetCardInfo,
models.APITypeSingleLogin, models.APITypeSingleLogin,
models.APITypeUserLogin, models.APITypeUserRegin, models.APITypeUserRecharge, models.APITypeCardRegin, models.APITypeUserLogin, models.APITypeUserRegin, models.APITypeUserRecharge,
models.APITypeLogOut, models.APITypeLogOut,
models.APITypeGetExpired, models.APITypeCheckUserStatus, models.APITypeGetAppData, models.APITypeGetVariable, models.APITypeGetExpired, models.APITypeCheckUserStatus, models.APITypeGetAppData, models.APITypeGetVariable,
models.APITypeUpdatePwd, models.APITypeMacChangeBind, models.APITypeIPChangeBind, models.APITypeUpdatePwd, models.APITypeMacChangeBind, models.APITypeIPChangeBind,

View File

@@ -15,8 +15,16 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// ============================================================================
// 全局变量
// ============================================================================
var appBaseController = controllers.NewBaseController() var appBaseController = controllers.NewBaseController()
// ============================================================================
// 页面处理器
// ============================================================================
// AppsFragmentHandler 应用列表页面片段处理器 // AppsFragmentHandler 应用列表页面片段处理器
func AppsFragmentHandler(c *gin.Context) { func AppsFragmentHandler(c *gin.Context) {
c.HTML(http.StatusOK, "apps.html", gin.H{ c.HTML(http.StatusOK, "apps.html", gin.H{
@@ -24,6 +32,10 @@ func AppsFragmentHandler(c *gin.Context) {
}) })
} }
// ============================================================================
// API处理器
// ============================================================================
// AppsListHandler 应用列表API处理器 // AppsListHandler 应用列表API处理器
func AppsListHandler(c *gin.Context) { func AppsListHandler(c *gin.Context) {
// 获取分页参数 // 获取分页参数
@@ -338,28 +350,7 @@ func AppCreateHandler(c *gin.Context) {
} }
// 为应用创建所有默认接口 // 为应用创建所有默认接口
defaultAPITypes := []int{ defaultAPITypes := models.GetDefaultAPITypes()
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, // 扣除时间
}
// 批量创建默认接口 // 批量创建默认接口
for _, apiType := range defaultAPITypes { for _, apiType := range defaultAPITypes {

View File

@@ -16,9 +16,17 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
// ============================================================================
// 全局变量
// ============================================================================
// 创建BaseController实例 // 创建BaseController实例
var authBaseController = controllers.NewBaseController() var authBaseController = controllers.NewBaseController()
// ============================================================================
// 页面处理器
// ============================================================================
// LoginPageHandler 管理员登录页渲染处理器 // LoginPageHandler 管理员登录页渲染处理器
// - 如果已登录则重定向到 /admin // - 如果已登录则重定向到 /admin
// - 否则渲染 web/template/admin/login.html 模板 // - 否则渲染 web/template/admin/login.html 模板
@@ -63,6 +71,10 @@ func LoginPageHandler(c *gin.Context) {
c.HTML(http.StatusOK, "login.html", data) c.HTML(http.StatusOK, "login.html", data)
} }
// ============================================================================
// API处理器
// ============================================================================
// LoginHandler 管理员登录接口 // LoginHandler 管理员登录接口
// - 接收JSON: {username, password} // - 接收JSON: {username, password}
// - 验证用户存在与密码正确性 // - 验证用户存在与密码正确性
@@ -74,11 +86,11 @@ func LoginHandler(c *gin.Context) {
Password string `json:"password"` Password string `json:"password"`
Captcha string `json:"captcha"` Captcha string `json:"captcha"`
} }
if !authBaseController.BindJSON(c, &body) { if !authBaseController.BindJSON(c, &body) {
return return
} }
if !authBaseController.ValidateRequired(c, map[string]interface{}{ if !authBaseController.ValidateRequired(c, map[string]interface{}{
"用户名": body.Username, "用户名": body.Username,
"密码": body.Password, "密码": body.Password,
@@ -178,7 +190,11 @@ func LogoutHandler(c *gin.Context) {
}) })
} }
// clearInvalidJWTCookie 清理失效的JWT Cookie // ============================================================================
// 辅助函数
// ============================================================================
// clearInvalidJWTCookie 清理无效的JWT Cookie
// - 统一的Cookie清理函数确保一致性 // - 统一的Cookie清理函数确保一致性
// - 在JWT校验失败时自动调用提升安全性和用户体验 // - 在JWT校验失败时自动调用提升安全性和用户体验
func clearInvalidJWTCookie(c *gin.Context) { func clearInvalidJWTCookie(c *gin.Context) {
@@ -192,7 +208,11 @@ func getJWTSecret() []byte {
return []byte(viper.GetString("security.jwt_secret")) return []byte(viper.GetString("security.jwt_secret"))
} }
// JWTClaims JWT载荷结构 // ============================================================================
// 结构体定义
// ============================================================================
// JWTClaims JWT载荷结构体
type JWTClaims struct { type JWTClaims struct {
Username string `json:"username"` Username string `json:"username"`
PasswordHash string `json:"password_hash"` // 密码哈希摘要,用于验证密码是否被修改 PasswordHash string `json:"password_hash"` // 密码哈希摘要,用于验证密码是否被修改

View File

@@ -15,12 +15,20 @@ import (
"github.com/mojocn/base64Captcha" "github.com/mojocn/base64Captcha"
) )
// ============================================================================
// 全局变量
// ============================================================================
// 创建基础控制器实例 // 创建基础控制器实例
var captchaBaseController = controllers.NewBaseController() var captchaBaseController = controllers.NewBaseController()
// 全局验证码存储器 // 全局验证码存储器
var store = base64Captcha.DefaultMemStore var store = base64Captcha.DefaultMemStore
// ============================================================================
// 辅助函数
// ============================================================================
// secureRandomInt 生成安全的随机整数,范围 [0, max) // secureRandomInt 生成安全的随机整数,范围 [0, max)
func secureRandomInt(max int) (int, error) { func secureRandomInt(max int) (int, error) {
n, err := rand.Int(rand.Reader, big.NewInt(int64(max))) 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 return int(n.Int64()), nil
} }
// ============================================================================
// API处理器
// ============================================================================
// CaptchaHandler 生成验证码图片 // CaptchaHandler 生成验证码图片
// GET /admin/captcha - 返回验证码图片 // GET /admin/captcha - 返回验证码图片
func CaptchaHandler(c *gin.Context) { func CaptchaHandler(c *gin.Context) {
@@ -87,8 +99,6 @@ func CaptchaHandler(c *gin.Context) {
c.Data(http.StatusOK, "image/png", imgData) c.Data(http.StatusOK, "image/png", imgData)
} }
// VerifyCaptcha 验证验证码 // VerifyCaptcha 验证验证码
// 这个函数将在登录处理中被调用 // 这个函数将在登录处理中被调用
// 支持大小写不敏感匹配 // 支持大小写不敏感匹配
@@ -97,7 +107,7 @@ func VerifyCaptcha(c *gin.Context, captchaValue string) bool {
if middleware.ShouldSkipCaptcha(c) { if middleware.ShouldSkipCaptcha(c) {
return true return true
} }
// 从cookie中获取验证码ID // 从cookie中获取验证码ID
captchaId, err := c.Cookie("captcha_id") captchaId, err := c.Cookie("captcha_id")
if err != nil { if err != nil {

View File

@@ -12,9 +12,17 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// ============================================================================
// 全局变量
// ============================================================================
// 创建基础控制器实例 // 创建基础控制器实例
var functionBaseController = controllers.NewBaseController() var functionBaseController = controllers.NewBaseController()
// ============================================================================
// 页面处理器
// ============================================================================
// FunctionFragmentHandler 公共函数列表页面片段处理器 // FunctionFragmentHandler 公共函数列表页面片段处理器
func FunctionFragmentHandler(c *gin.Context) { func FunctionFragmentHandler(c *gin.Context) {
c.HTML(http.StatusOK, "functions.html", gin.H{ c.HTML(http.StatusOK, "functions.html", gin.H{
@@ -22,6 +30,10 @@ func FunctionFragmentHandler(c *gin.Context) {
}) })
} }
// ============================================================================
// API处理器
// ============================================================================
// FunctionListHandler 函数列表API处理器 // FunctionListHandler 函数列表API处理器
func FunctionListHandler(c *gin.Context) { 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") logrus.WithField("function_ids", req.IDs).Info("Successfully batch deleted functions")
functionBaseController.HandleSuccess(c, "批量删除成功", nil) functionBaseController.HandleSuccess(c, "批量删除成功", nil)
} }

View File

@@ -14,9 +14,17 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
// ============================================================================
// 全局变量
// ============================================================================
// 创建基础控制器实例 // 创建基础控制器实例
var handlersBaseController = controllers.NewBaseController() var handlersBaseController = controllers.NewBaseController()
// ============================================================================
// 辅助函数
// ============================================================================
// formatDBType 格式化数据库类型显示 // formatDBType 格式化数据库类型显示
// 将配置文件中的小写类型转换为友好的显示格式 // 将配置文件中的小写类型转换为友好的显示格式
func formatDBType(dbType string) string { func formatDBType(dbType string) string {
@@ -34,6 +42,10 @@ func formatDBType(dbType string) string {
} }
} }
// ============================================================================
// 页面处理器
// ============================================================================
// AdminIndexHandler 后台首页处理器/admin 与 /admin/ 根路径入口 // AdminIndexHandler 后台首页处理器/admin 与 /admin/ 根路径入口
// - 未登录:重定向到 /admin/login // - 未登录:重定向到 /admin/login
// - 已登录:渲染后台布局页(或重定向到 /admin/layout // - 已登录:渲染后台布局页(或重定向到 /admin/layout
@@ -107,6 +119,10 @@ func DashboardFragmentHandler(c *gin.Context) {
c.HTML(http.StatusOK, "dashboard.html", data) c.HTML(http.StatusOK, "dashboard.html", data)
} }
// ============================================================================
// API处理器
// ============================================================================
// SystemInfoHandler 系统信息API接口 // SystemInfoHandler 系统信息API接口
// - 返回系统运行状态的JSON数据用于前端定时刷新 // - 返回系统运行状态的JSON数据用于前端定时刷新
func SystemInfoHandler(c *gin.Context) { func SystemInfoHandler(c *gin.Context) {

View File

@@ -1,134 +1,146 @@
package admin package admin
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"net/http" "net/http"
"networkDev/controllers" "networkDev/controllers"
"networkDev/models" "networkDev/models"
"networkDev/services" "networkDev/services"
"networkDev/utils" "networkDev/utils"
) )
// 创建基础控制器实例 // ============================================================================
var settingsBaseController = controllers.NewBaseController() // 全局变量
// ============================================================================
// SettingsFragmentHandler 设置片段渲染
// - 渲染设置表单通过前端JS调用API加载/保存) // 创建基础控制器实例
func SettingsFragmentHandler(c *gin.Context) { var settingsBaseController = controllers.NewBaseController()
c.HTML(http.StatusOK, "settings.html", gin.H{})
} // ============================================================================
// 页面处理器
// SettingsQueryHandler 设置查询API // ============================================================================
// - 返回所有设置项的 name:value 映射
func SettingsQueryHandler(c *gin.Context) { // SettingsFragmentHandler 设置片段渲染
db, ok := settingsBaseController.GetDB(c) // - 渲染设置表单通过前端JS调用API加载/保存)
if !ok { func SettingsFragmentHandler(c *gin.Context) {
return c.HTML(http.StatusOK, "settings.html", gin.H{})
} }
var list []models.Settings
if err := db.Find(&list).Error; err != nil { // ============================================================================
settingsBaseController.HandleInternalError(c, "查询失败", err) // API处理器
return // ============================================================================
}
res := map[string]string{} // SettingsQueryHandler 设置查询API
for _, s := range list { // - 返回所有设置项的 name:value 映射
res[s.Name] = s.Value func SettingsQueryHandler(c *gin.Context) {
} db, ok := settingsBaseController.GetDB(c)
settingsBaseController.HandleSuccess(c, "ok", res) if !ok {
} return
}
// SettingsUpdateHandler 更新系统设置处理器 var list []models.Settings
// - 接收JSON格式的设置数据支持两种格式 if err := db.Find(&list).Error; err != nil {
// 1. 直接字段格式: {"site_title": "值", "site_keywords": "值"} settingsBaseController.HandleInternalError(c, "查询失败", err)
// 2. 嵌套格式: {"settings": {"site_title": "值", "site_keywords": "值"}} return
// }
// - 自动创建不存在的设置项 res := map[string]string{}
// - 更新已存在的设置项 for _, s := range list {
// - 更新完成后: res[s.Name] = s.Value
// 1. 删除对应的Redis缓存键确保后续读取走数据库并重建缓存 }
// 2. 刷新SettingsService内存缓存 settingsBaseController.HandleSuccess(c, "ok", res)
func SettingsUpdateHandler(c *gin.Context) { }
// 先尝试解析为直接字段格式
var directBody map[string]interface{} // SettingsUpdateHandler 更新系统设置处理器
if !settingsBaseController.BindJSON(c, &directBody) { // - 接收JSON格式的设置数据支持两种格式
return // 1. 直接字段格式: {"site_title": "值", "site_keywords": "值"}
} // 2. 嵌套格式: {"settings": {"site_title": "值", "site_keywords": "值"}}
//
// 提取设置数据 // - 自动创建不存在的设置项
var settingsData map[string]string // - 更新已存在的设置项
// - 更新完成后:
// 检查是否为嵌套格式包含settings字段 // 1. 删除对应的Redis缓存键确保后续读取走数据库并重建缓存
if settings, exists := directBody["settings"]; exists { // 2. 刷新SettingsService内存缓存
if settingsMap, ok := settings.(map[string]interface{}); ok { func SettingsUpdateHandler(c *gin.Context) {
settingsData = make(map[string]string) // 先尝试解析为直接字段格式
for k, v := range settingsMap { var directBody map[string]interface{}
if str, ok := v.(string); ok { if !settingsBaseController.BindJSON(c, &directBody) {
settingsData[k] = str return
} }
}
} else { // 提取设置数据
settingsBaseController.HandleValidationError(c, "settings字段格式错误") var settingsData map[string]string
return
} // 检查是否为嵌套格式包含settings字段
} else { if settings, exists := directBody["settings"]; exists {
// 直接字段格式 if settingsMap, ok := settings.(map[string]interface{}); ok {
settingsData = make(map[string]string) settingsData = make(map[string]string)
for k, v := range directBody { for k, v := range settingsMap {
if str, ok := v.(string); ok { if str, ok := v.(string); ok {
settingsData[k] = str settingsData[k] = str
} else if v != nil { }
// 转换其他类型为字符串 }
settingsData[k] = fmt.Sprintf("%v", v) } else {
} settingsBaseController.HandleValidationError(c, "settings字段格式错误")
} return
} }
} else {
if len(settingsData) == 0 { // 直接字段格式
settingsBaseController.HandleValidationError(c, "无设置项") settingsData = make(map[string]string)
return for k, v := range directBody {
} if str, ok := v.(string); ok {
settingsData[k] = str
db, ok := settingsBaseController.GetDB(c) } else if v != nil {
if !ok { // 转换其他类型为字符串
return settingsData[k] = fmt.Sprintf("%v", v)
} }
}
// 记录需要失效的缓存键统一删除减少与Redis交互次数 }
keysToDel := make([]string, 0, len(settingsData))
if len(settingsData) == 0 {
// 批量处理设置项 settingsBaseController.HandleValidationError(c, "无设置项")
for k, v := range settingsData { return
var s models.Settings }
if err := db.Where("name = ?", k).First(&s).Error; err != nil {
// 不存在则创建 db, ok := settingsBaseController.GetDB(c)
s = models.Settings{Name: k, Value: v} if !ok {
if err := db.Create(&s).Error; err != nil { return
logrus.WithError(err).WithField("setting_name", k).Error("创建设置失败") }
settingsBaseController.HandleInternalError(c, fmt.Sprintf("保存设置 %s 失败", k), err)
return // 记录需要失效的缓存键统一删除减少与Redis交互次数
} keysToDel := make([]string, 0, len(settingsData))
} else { // 批量处理设置项
// 存在则更新 for k, v := range settingsData {
if err := db.Model(&models.Settings{}).Where("id = ?", s.ID).Update("value", v).Error; err != nil { var s models.Settings
logrus.WithError(err).WithField("setting_name", k).Error("更新设置失败") if err := db.Where("name = ?", k).First(&s).Error; err != nil {
settingsBaseController.HandleInternalError(c, fmt.Sprintf("更新设置 %s 失败", k), err) // 不存在则创建
return 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)
// 收集对应的Redis缓存键与services/query.go中的键命名保持一致 return
keysToDel = append(keysToDel, fmt.Sprintf("setting:%s", k)) }
}
} else {
// 删除Redis缓存键如果Redis不可用则静默跳过 // 存在则更新
_ = utils.RedisDel(context.Background(), keysToDel...) 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)
services.GetSettingsService().RefreshCache() return
}
settingsBaseController.HandleSuccess(c, "保存成功", nil)
} }
// 收集对应的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)
}

View File

@@ -10,15 +10,27 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// ============================================================================
// 全局变量
// ============================================================================
// 创建基础控制器实例 // 创建基础控制器实例
var baseController = controllers.NewBaseController() var baseController = controllers.NewBaseController()
// ============================================================================
// 页面处理器
// ============================================================================
// UserFragmentHandler 个人资料片段渲染 // UserFragmentHandler 个人资料片段渲染
// - 渲染个人资料与修改密码表单 // - 渲染个人资料与修改密码表单
func UserFragmentHandler(c *gin.Context) { func UserFragmentHandler(c *gin.Context) {
c.HTML(http.StatusOK, "user.html", gin.H{}) c.HTML(http.StatusOK, "user.html", gin.H{})
} }
// ============================================================================
// API处理器
// ============================================================================
// UserProfileQueryHandler 获取当前登录管理员的用户名 // UserProfileQueryHandler 获取当前登录管理员的用户名
// - 返回 JSON: {username} // - 返回 JSON: {username}
// - 直接从JWT获取用户名信息 // - 直接从JWT获取用户名信息
@@ -51,20 +63,20 @@ func UserPasswordUpdateHandler(c *gin.Context) {
NewPassword string `json:"new_password"` NewPassword string `json:"new_password"`
ConfirmPassword string `json:"confirm_password"` ConfirmPassword string `json:"confirm_password"`
} }
if !baseController.BindJSON(c, &body) { if !baseController.BindJSON(c, &body) {
return return
} }
// 基础校验 // 基础校验
if !baseController.ValidateRequired(c, map[string]interface{}{ if !baseController.ValidateRequired(c, map[string]interface{}{
"旧密码": body.OldPassword, "旧密码": body.OldPassword,
"新密码": body.NewPassword, "新密码": body.NewPassword,
"确认密码": body.ConfirmPassword, "确认密码": body.ConfirmPassword,
}) { }) {
return return
} }
if len(body.NewPassword) < 6 { if len(body.NewPassword) < 6 {
baseController.HandleValidationError(c, "新密码长度不能少于6位") baseController.HandleValidationError(c, "新密码长度不能少于6位")
return return
@@ -253,7 +265,7 @@ func UserProfileUpdateHandler(c *gin.Context) {
// 重新签发JWT并写入Cookie // 重新签发JWT并写入Cookie
// 创建虚拟用户对象用于生成JWT令牌 // 创建虚拟用户对象用于生成JWT令牌
adminUser := models.User{ adminUser := models.User{
Username: username, // 使用新的用户名 Username: username, // 使用新的用户名
Password: adminPassword, Password: adminPassword,
PasswordSalt: adminPasswordSalt, PasswordSalt: adminPasswordSalt,
} }

View File

@@ -12,9 +12,17 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// ============================================================================
// 全局变量
// ============================================================================
// 创建基础控制器实例 // 创建基础控制器实例
var variableBaseController = controllers.NewBaseController() var variableBaseController = controllers.NewBaseController()
// ============================================================================
// 页面处理器
// ============================================================================
// VariableFragmentHandler 公共变量列表页面片段处理器 // VariableFragmentHandler 公共变量列表页面片段处理器
func VariableFragmentHandler(c *gin.Context) { func VariableFragmentHandler(c *gin.Context) {
c.HTML(http.StatusOK, "variables.html", gin.H{ c.HTML(http.StatusOK, "variables.html", gin.H{
@@ -22,6 +30,10 @@ func VariableFragmentHandler(c *gin.Context) {
}) })
} }
// ============================================================================
// API处理器
// ============================================================================
// VariableListHandler 变量列表API处理器 // VariableListHandler 变量列表API处理器
func VariableListHandler(c *gin.Context) { func VariableListHandler(c *gin.Context) {
// 获取分页参数 // 获取分页参数

View File

@@ -10,14 +10,26 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
// ============================================================================
// 结构体定义
// ============================================================================
// BaseController 基础控制器结构体 // BaseController 基础控制器结构体
type BaseController struct{} type BaseController struct{}
// ============================================================================
// 构造函数
// ============================================================================
// NewBaseController 创建基础控制器实例 // NewBaseController 创建基础控制器实例
func NewBaseController() *BaseController { func NewBaseController() *BaseController {
return &BaseController{} return &BaseController{}
} }
// ============================================================================
// 数据库相关方法
// ============================================================================
// GetDB 获取数据库连接,统一错误处理 // GetDB 获取数据库连接,统一错误处理
func (bc *BaseController) GetDB(c *gin.Context) (*gorm.DB, bool) { func (bc *BaseController) GetDB(c *gin.Context) (*gorm.DB, bool) {
db, err := database.GetDB() db, err := database.GetDB()
@@ -28,6 +40,10 @@ func (bc *BaseController) GetDB(c *gin.Context) (*gorm.DB, bool) {
return db, true return db, true
} }
// ============================================================================
// 错误处理方法
// ============================================================================
// HandleDatabaseError 统一处理数据库连接错误 // HandleDatabaseError 统一处理数据库连接错误
func (bc *BaseController) HandleDatabaseError(c *gin.Context, err error) { func (bc *BaseController) HandleDatabaseError(c *gin.Context, err error) {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
@@ -64,6 +80,10 @@ func (bc *BaseController) HandleInternalError(c *gin.Context, message string, er
}) })
} }
// ============================================================================
// 成功响应方法
// ============================================================================
// HandleSuccess 统一处理成功响应 // HandleSuccess 统一处理成功响应
func (bc *BaseController) HandleSuccess(c *gin.Context, message string, data interface{}) { func (bc *BaseController) HandleSuccess(c *gin.Context, message string, data interface{}) {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
@@ -82,6 +102,10 @@ func (bc *BaseController) HandleCreated(c *gin.Context, message string, data int
}) })
} }
// ============================================================================
// 辅助方法
// ============================================================================
// ValidateRequired 验证必填字段 // ValidateRequired 验证必填字段
func (bc *BaseController) ValidateRequired(c *gin.Context, fields map[string]interface{}) bool { func (bc *BaseController) ValidateRequired(c *gin.Context, fields map[string]interface{}) bool {
for fieldName, fieldValue := range fields { for fieldName, fieldValue := range fields {

View File

@@ -10,8 +10,16 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
// ============================================================================
// 全局变量
// ============================================================================
var homeBaseController = controllers.NewBaseController() var homeBaseController = controllers.NewBaseController()
// ============================================================================
// 辅助函数
// ============================================================================
// getSettingValue 获取配置值,优先从数据库获取,不存在时使用默认值 // getSettingValue 获取配置值,优先从数据库获取,不存在时使用默认值
func getSettingValue(settingName string, defaultValue string, db *gorm.DB) string { func getSettingValue(settingName string, defaultValue string, db *gorm.DB) string {
if setting, err := services.FindSettingByName(settingName, db); err == nil { 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 return defaultValue
} }
// ============================================================================
// 页面处理器
// ============================================================================
// RootHandler 主页处理器 // RootHandler 主页处理器
func RootHandler(c *gin.Context) { func RootHandler(c *gin.Context) {
// 获取数据库连接 // 获取数据库连接

View File

@@ -1,124 +1,136 @@
package database package database
import ( import (
"fmt" "fmt"
"networkDev/utils" "networkDev/utils"
"sync" "sync"
"github.com/glebarez/sqlite" "github.com/glebarez/sqlite"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/viper" "github.com/spf13/viper"
"gorm.io/driver/mysql" "gorm.io/driver/mysql"
"gorm.io/gorm" "gorm.io/gorm"
) )
var ( // ============================================================================
// dbInstance 全局 *gorm.DB 实例,使用单例确保全局复用 // 全局变量
dbInstance *gorm.DB // ============================================================================
// once 确保初始化只执行一次
once sync.Once var (
) // dbInstance 全局 *gorm.DB 实例,使用单例确保全局复用
dbInstance *gorm.DB
// Init 初始化数据库连接(根据配置自动选择驱动) // once 确保初始化只执行一次
// - 默认使用 SQLitegithub.com/glebarez/sqlite once sync.Once
// - 生产环境支持 MySQLgorm.io/driver/mysql )
func Init() (*gorm.DB, error) {
var initErr error // ============================================================================
once.Do(func() { // 公共函数
dbType := viper.GetString("database.type") // ============================================================================
switch dbType {
case "mysql": // Init 初始化数据库连接(根据配置自动选择驱动)
initErr = initMySQL() // - 默认使用 SQLitegithub.com/glebarez/sqlite
default: // - 生产环境支持 MySQLgorm.io/driver/mysql
initErr = initSQLite() func Init() (*gorm.DB, error) {
} var initErr error
once.Do(func() {
// 如果数据库初始化成功,配置连接池和启动健康检查 dbType := viper.GetString("database.type")
if initErr == nil && dbInstance != nil { switch dbType {
// 加载数据库配置 case "mysql":
var configPrefix string initErr = initMySQL()
if dbType == "mysql" { default:
configPrefix = "database.mysql" initErr = initSQLite()
} else { }
configPrefix = "database.sqlite"
} // 如果数据库初始化成功,配置连接池和启动健康检查
if initErr == nil && dbInstance != nil {
dbConfig := utils.LoadDatabaseConfig(configPrefix) // 加载数据库配置
var configPrefix string
// 验证配置 if dbType == "mysql" {
if err := utils.ValidateDatabaseConfig(dbConfig); err != nil { configPrefix = "database.mysql"
logrus.WithError(err).Warn("数据库配置验证失败,使用默认配置") } else {
dbConfig = utils.GetDefaultDatabaseConfig() configPrefix = "database.sqlite"
} }
// 配置连接池 dbConfig := utils.LoadDatabaseConfig(configPrefix)
if err := utils.ConfigureConnectionPool(dbInstance, dbConfig); err != nil {
logrus.WithError(err).Error("配置数据库连接池失败") // 验证配置
} if err := utils.ValidateDatabaseConfig(dbConfig); err != nil {
logrus.WithError(err).Warn("数据库配置验证失败,使用默认配置")
// 启动健康检查 dbConfig = utils.GetDefaultDatabaseConfig()
utils.StartHealthCheck(dbInstance, dbConfig) }
}
}) // 配置连接池
return dbInstance, initErr if err := utils.ConfigureConnectionPool(dbInstance, dbConfig); err != nil {
} logrus.WithError(err).Error("配置数据库连接池失败")
}
// GetDB 获取全局 *gorm.DB 实例
// 如果未初始化,会尝试初始化一次 // 启动健康检查
func GetDB() (*gorm.DB, error) { utils.StartHealthCheck(dbInstance, dbConfig)
if dbInstance != nil { }
return dbInstance, nil })
} return dbInstance, initErr
return Init() }
}
// GetDB 获取全局 *gorm.DB 实例
// initSQLite 初始化 SQLite 数据库 // 如果未初始化,会尝试初始化一次
// 使用 viper 中的 database.sqlite.path 作为数据库文件路径 func GetDB() (*gorm.DB, error) {
func initSQLite() error { if dbInstance != nil {
path := viper.GetString("database.sqlite.path") return dbInstance, nil
if path == "" { }
path = "./database.db" return Init()
} }
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
} // initSQLite 初始化 SQLite 数据库
// 使用 viper 中的 database.sqlite.path 作为数据库文件路径
// SQLite 连接池配置SQLite 对连接池支持有限,但仍可设置基本参数) func initSQLite() error {
if sqlDB, err := db.DB(); err == nil { path := viper.GetString("database.sqlite.path")
// SQLite 通常使用单连接,但可以设置一些基本参数 if path == "" {
sqlDB.SetMaxOpenConns(1) // SQLite 建议使用单连接 path = "./database.db"
sqlDB.SetMaxIdleConns(1) }
} dsn := fmt.Sprintf("file:%s?cache=shared&_busy_timeout=5000&_fk=1", path)
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
dbInstance = db if err != nil {
logrus.WithField("path", path).Info("SQLite 连接已建立") logrus.WithError(err).Error("SQLite 初始化失败")
return nil return err
} }
// initMySQL 初始化 MySQL 数据库 // SQLite 连接池配置SQLite 对连接池支持有限,但仍可设置基本参数)
// 从 viper 读取 database.mysql.* 配置构建 DSN if sqlDB, err := db.DB(); err == nil {
func initMySQL() error { // SQLite 通常使用单连接,但可以设置一些基本参数
host := viper.GetString("database.mysql.host") sqlDB.SetMaxOpenConns(1) // SQLite 建议使用单连接
port := viper.GetInt("database.mysql.port") sqlDB.SetMaxIdleConns(1)
user := viper.GetString("database.mysql.username") }
pass := viper.GetString("database.mysql.password")
dbname := viper.GetString("database.mysql.database") dbInstance = db
charset := viper.GetString("database.mysql.charset") logrus.WithField("path", path).Info("SQLite 连接已建立")
if charset == "" { return nil
charset = "utf8mb4" }
}
// initMySQL 初始化 MySQL 数据库
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local", user, pass, host, port, dbname, charset) // 从 viper 读取 database.mysql.* 配置构建 DSN
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) func initMySQL() error {
if err != nil { host := viper.GetString("database.mysql.host")
logrus.WithError(err).Error("MySQL 初始化失败") port := viper.GetInt("database.mysql.port")
return err user := viper.GetString("database.mysql.username")
} pass := viper.GetString("database.mysql.password")
dbname := viper.GetString("database.mysql.database")
dbInstance = db charset := viper.GetString("database.mysql.charset")
logrus.WithField("host", host).WithField("database", dbname).Info("MySQL 连接已建立") if charset == "" {
return nil 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
}

View File

@@ -1,172 +1,180 @@
package database package database
import ( import (
"fmt" "fmt"
"networkDev/models" "networkDev/models"
"strings" "strings"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"gorm.io/gorm" "gorm.io/gorm"
) )
// AutoMigrate 自动迁移数据库模型 // ============================================================================
// - 会确保必要的数据表结构存在 // 公共函数
// - 不会破坏已有数据 // ============================================================================
func AutoMigrate() error {
db, err := GetDB() // AutoMigrate 自动迁移数据库模型
if err != nil { // - 会确保必要的数据表结构存在
return err // - 不会破坏已有数据
} func AutoMigrate() error {
if err := db.AutoMigrate(&models.User{}, &models.Settings{}, &models.App{}, &models.API{}, &models.Variable{}, &models.Function{}); err != nil { db, err := GetDB()
logrus.WithError(err).Error("AutoMigrate 执行失败") if err != nil {
return err return err
} }
if err := db.AutoMigrate(&models.User{}, &models.Settings{}, &models.App{}, &models.API{}, &models.Variable{}, &models.Function{}); err != nil {
// 兼容迁移:如果 users.password_salt 列长度 < 64则扩大到 64 logrus.WithError(err).Error("AutoMigrate 执行失败")
if err := ensureUserPasswordSaltLength(db); err != nil { return err
logrus.WithError(err).Error("调整 users.password_salt 列长度失败") }
return err
} // 兼容迁移:如果 users.password_salt 列长度 < 64则扩大到 64
if err := ensureUserPasswordSaltLength(db); err != nil {
// 兼容迁移:确保 tasks.verification_code 字段类型为 LONGTEXT 以支持大图片数据 logrus.WithError(err).Error("调整 users.password_salt 列长度失败")
if err := ensureVerificationCodeType(db); err != nil { return err
logrus.WithError(err).Error("调整 tasks.verification_code 字段类型失败") }
return err
} // 兼容迁移:确保 tasks.verification_code 字段类型为 LONGTEXT 以支持大图片数据
if err := ensureVerificationCodeType(db); err != nil {
logrus.Info("AutoMigrate 执行完成") logrus.WithError(err).Error("调整 tasks.verification_code 字段类型失败")
return nil return err
} }
// ensureVerificationCodeType 确保tasks.verification_code字段类型为LONGTEXT以支持大图片数据 logrus.Info("AutoMigrate 执行完成")
// 中文注释检查并修改verification_code字段类型支持Base64编码的大图片数据存储 return nil
func ensureVerificationCodeType(db *gorm.DB) error { }
// 获取数据库方言类型
dialector := db.Dialector.Name() // ============================================================================
// 私有函数
// 根据不同数据库类型执行不同的检查逻辑 // ============================================================================
switch dialector {
case "mysql": // ensureVerificationCodeType 确保tasks.verification_code字段类型为LONGTEXT以支持大图片数据
// MySQL/MariaDB使用INFORMATION_SCHEMA // 中文注释检查并修改verification_code字段类型支持Base64编码的大图片数据存储
var result struct { func ensureVerificationCodeType(db *gorm.DB) error {
ColumnName string `gorm:"column:COLUMN_NAME"` // 获取数据库方言类型
ColumnType string `gorm:"column:COLUMN_TYPE"` dialector := db.Dialector.Name()
}
// 根据不同数据库类型执行不同的检查逻辑
err := db.Raw("SELECT COLUMN_NAME, COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ? LIMIT 1", switch dialector {
"tasks", "verification_code").Scan(&result).Error case "mysql":
// MySQL/MariaDB使用INFORMATION_SCHEMA
if err != nil { var result struct {
return nil // 查询失败则跳过 ColumnName string `gorm:"column:COLUMN_NAME"`
} ColumnType string `gorm:"column:COLUMN_TYPE"`
}
// 检查列类型如果不是LONGTEXT则修改
if !strings.Contains(strings.ToLower(result.ColumnType), "longtext") { err := db.Raw("SELECT COLUMN_NAME, COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ? LIMIT 1",
alterSQL := "ALTER TABLE tasks MODIFY COLUMN verification_code LONGTEXT" "tasks", "verification_code").Scan(&result).Error
if err := db.Exec(alterSQL).Error; err != nil {
return fmt.Errorf("修改verification_code字段类型失败: %v", err) if err != nil {
} return nil // 查询失败则跳过
logrus.Info("verification_code字段类型已更新为LONGTEXT") }
}
case "sqlite": // 检查列类型如果不是LONGTEXT则修改
// SQLite使用pragma_table_info检查列信息 if !strings.Contains(strings.ToLower(result.ColumnType), "longtext") {
var columns []struct { alterSQL := "ALTER TABLE tasks MODIFY COLUMN verification_code LONGTEXT"
CID int `gorm:"column:cid"` if err := db.Exec(alterSQL).Error; err != nil {
Name string `gorm:"column:name"` return fmt.Errorf("修改verification_code字段类型失败: %v", err)
Type string `gorm:"column:type"` }
NotNull int `gorm:"column:notnull"` logrus.Info("verification_code字段类型已更新为LONGTEXT")
DfltValue *string `gorm:"column:dflt_value"` }
PK int `gorm:"column:pk"` case "sqlite":
} // SQLite使用pragma_table_info检查列信息
var columns []struct {
err := db.Raw("PRAGMA table_info(tasks)").Scan(&columns).Error CID int `gorm:"column:cid"`
if err != nil { Name string `gorm:"column:name"`
return nil // 查询失败则跳过 Type string `gorm:"column:type"`
} NotNull int `gorm:"column:notnull"`
DfltValue *string `gorm:"column:dflt_value"`
// 查找verification_code列 PK int `gorm:"column:pk"`
for _, col := range columns { }
if col.Name == "verification_code" {
// SQLite中如果列类型不是TEXT需要重建表 err := db.Raw("PRAGMA table_info(tasks)").Scan(&columns).Error
if !strings.Contains(strings.ToLower(col.Type), "text") { if err != nil {
// SQLite不支持直接修改列类型但GORM的AutoMigrate会处理这种情况 return nil // 查询失败则跳过
logrus.Info("SQLite检测到verification_code字段类型需要更新依赖GORM AutoMigrate处理") }
}
break // 查找verification_code列
} for _, col := range columns {
} if col.Name == "verification_code" {
default: // SQLite中如果列类型不是TEXT需要重建表
// 其他数据库类型暂不处理 if !strings.Contains(strings.ToLower(col.Type), "text") {
logrus.Infof("数据库类型 %s 暂不支持verification_code字段类型检查", dialector) // SQLite不支持直接修改列类型但GORM的AutoMigrate会处理这种情况
} logrus.Info("SQLite检测到verification_code字段类型需要更新依赖GORM AutoMigrate处理")
}
return nil break
} }
}
// ensureUserPasswordSaltLength 确保users.password_salt列长度至少为64 default:
// 中文注释检查并修改password_salt列长度兼容32字节64十六进制字符的盐值 // 其他数据库类型暂不处理
func ensureUserPasswordSaltLength(db *gorm.DB) error { logrus.Infof("数据库类型 %s 暂不支持verification_code字段类型检查", dialector)
// 获取数据库方言类型 }
dialector := db.Dialector.Name()
return nil
// 根据不同数据库类型执行不同的检查逻辑 }
switch dialector {
case "mysql": // ensureUserPasswordSaltLength 确保users.password_salt列长度至少为64
// MySQL/MariaDB使用INFORMATION_SCHEMA // 中文注释检查并修改password_salt列长度兼容32字节64十六进制字符的盐值
var result struct { func ensureUserPasswordSaltLength(db *gorm.DB) error {
ColumnName string `gorm:"column:COLUMN_NAME"` // 获取数据库方言类型
ColumnType string `gorm:"column:COLUMN_TYPE"` dialector := db.Dialector.Name()
}
// 根据不同数据库类型执行不同的检查逻辑
err := db.Raw("SELECT COLUMN_NAME, COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ? LIMIT 1", switch dialector {
"users", "password_salt").Scan(&result).Error case "mysql":
// MySQL/MariaDB使用INFORMATION_SCHEMA
if err != nil { var result struct {
return nil // 查询失败则跳过 ColumnName string `gorm:"column:COLUMN_NAME"`
} ColumnType string `gorm:"column:COLUMN_TYPE"`
}
// 检查列类型如果长度小于64则修改
if strings.Contains(strings.ToLower(result.ColumnType), "varchar") { err := db.Raw("SELECT COLUMN_NAME, COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ? LIMIT 1",
if strings.Contains(result.ColumnType, "(32)") || strings.Contains(result.ColumnType, "(16)") { "users", "password_salt").Scan(&result).Error
alterSQL := "ALTER TABLE users MODIFY COLUMN password_salt VARCHAR(64)"
if err := db.Exec(alterSQL).Error; err != nil { if err != nil {
return fmt.Errorf("修改password_salt列长度失败: %v", err) return nil // 查询失败则跳过
} }
logrus.Info("password_salt列长度已更新为64")
} // 检查列类型如果长度小于64则修改
} if strings.Contains(strings.ToLower(result.ColumnType), "varchar") {
case "sqlite": if strings.Contains(result.ColumnType, "(32)") || strings.Contains(result.ColumnType, "(16)") {
// SQLite使用pragma_table_info检查列信息 alterSQL := "ALTER TABLE users MODIFY COLUMN password_salt VARCHAR(64)"
var columns []struct { if err := db.Exec(alterSQL).Error; err != nil {
CID int `gorm:"column:cid"` return fmt.Errorf("修改password_salt列长度失败: %v", err)
Name string `gorm:"column:name"` }
Type string `gorm:"column:type"` logrus.Info("password_salt列长度已更新为64")
NotNull int `gorm:"column:notnull"` }
DfltValue *string `gorm:"column:dflt_value"` }
PK int `gorm:"column:pk"` case "sqlite":
} // SQLite使用pragma_table_info检查列信息
var columns []struct {
err := db.Raw("PRAGMA table_info(users)").Scan(&columns).Error CID int `gorm:"column:cid"`
if err != nil { Name string `gorm:"column:name"`
return nil // 查询失败则跳过 Type string `gorm:"column:type"`
} NotNull int `gorm:"column:notnull"`
DfltValue *string `gorm:"column:dflt_value"`
// 查找password_salt列 PK int `gorm:"column:pk"`
for _, col := range columns { }
if col.Name == "password_salt" {
// SQLite中如果列类型包含长度限制且小于64需要重建表 err := db.Raw("PRAGMA table_info(users)").Scan(&columns).Error
if strings.Contains(strings.ToLower(col.Type), "varchar(32)") || if err != nil {
strings.Contains(strings.ToLower(col.Type), "varchar(16)") { return nil // 查询失败则跳过
// SQLite不支持直接修改列类型但GORM的AutoMigrate会处理这种情况 }
logrus.Info("SQLite检测到password_salt列长度需要更新依赖GORM AutoMigrate处理")
} // 查找password_salt列
break for _, col := range columns {
} if col.Name == "password_salt" {
} // SQLite中如果列类型包含长度限制且小于64需要重建表
default: if strings.Contains(strings.ToLower(col.Type), "varchar(32)") ||
// 其他数据库类型暂不处理 strings.Contains(strings.ToLower(col.Type), "varchar(16)") {
logrus.Infof("数据库类型 %s 暂不支持password_salt列长度检查", dialector) // SQLite不支持直接修改列类型但GORM的AutoMigrate会处理这种情况
} logrus.Info("SQLite检测到password_salt列长度需要更新依赖GORM AutoMigrate处理")
}
return nil break
} }
}
default:
// 其他数据库类型暂不处理
logrus.Infof("数据库类型 %s 暂不支持password_salt列长度检查", dialector)
}
return nil
}

View File

@@ -1,178 +1,186 @@
package database package database
import ( import (
"networkDev/models" "networkDev/models"
"networkDev/utils" "networkDev/utils"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"gorm.io/gorm" "gorm.io/gorm"
) )
// SeedDefaultSettings 初始化默认系统设置 // ============================================================================
// - 检查各项设置是否已存在,如不存在则创建默认值 // 公共函数
// - 包含站点基本信息、SEO设置等常用配置项 // ============================================================================
func SeedDefaultSettings() error {
db, err := GetDB() // SeedDefaultSettings 初始化默认系统设置
if err != nil { // - 检查各项设置是否已存在,如不存在则创建默认值
return err // - 包含站点基本信息、SEO设置等常用配置项
} func SeedDefaultSettings() error {
db, err := GetDB()
// 定义默认设置项 if err != nil {
defaultSettings := []models.Settings{ return err
{ }
Name: "site_title",
Value: "凌动技术", // 定义默认设置项
Description: "网站标题,显示在浏览器标题栏和页面顶部", defaultSettings := []models.Settings{
}, {
{ Name: "site_title",
Name: "site_keywords", Value: "凌动技术",
Value: "验证,网络,管理系统,网络验证,卡密管理,账户管理", Description: "网站标题,显示在浏览器标题栏和页面顶部",
Description: "网站关键词用于SEO优化多个关键词用逗号分隔", },
}, {
{ Name: "site_keywords",
Name: "site_description", Value: "验证,网络,管理系统,网络验证,卡密管理,账户管理",
Value: "专业的网络验证管理系统,提供便捷的在线网络验证服务和设备管理功能", Description: "网站关键词用于SEO优化多个关键词用逗号分隔",
Description: "网站描述用于SEO优化和社交媒体分享", },
}, {
{ Name: "site_description",
Name: "site_logo", Value: "专业的网络验证管理系统,提供便捷的在线网络验证服务和设备管理功能",
Value: "/favicon.ico", Description: "网站描述用于SEO优化和社交媒体分享",
Description: "网站Logo图片路径", },
}, {
{ Name: "site_logo",
Name: "contact_email", Value: "/favicon.ico",
Value: "admin@example.com", Description: "网站Logo图片路径",
Description: "联系邮箱,用于客服和业务咨询", },
}, {
{ Name: "contact_email",
Name: "max_upload_size", Value: "admin@example.com",
Value: "10485760", Description: "联系邮箱,用于客服和业务咨询",
Description: "文件上传最大尺寸字节默认10MB", },
}, {
{ Name: "max_upload_size",
Name: "default_user_role", Value: "10485760",
Value: "1", Description: "文件上传最大尺寸字节默认10MB",
Description: "新用户默认角色0=管理员1=普通用户", },
}, {
{ Name: "default_user_role",
Name: "session_timeout", Value: "1",
Value: "3600", Description: "新用户默认角色0=管理员1=普通用户",
Description: "会话超时时间默认1小时", },
}, {
{ Name: "session_timeout",
Name: "maintenance_mode", Value: "3600",
Value: "0", Description: "会话超时时间默认1小时",
Description: "维护模式0=关闭维护模式1=开启维护模式", },
}, {
// ===== 管理员账号相关默认项 ===== Name: "maintenance_mode",
{ Value: "0",
Name: "admin_username", Description: "维护模式0=关闭维护模式1=开启维护模式",
Value: "admin", },
Description: "管理员用户名", // ===== 管理员账号相关默认项 =====
}, {
{ Name: "admin_username",
Name: "admin_password", Value: "admin",
Value: "", Description: "管理员用户名",
Description: "管理员密码哈希值", },
}, {
{ Name: "admin_password",
Name: "admin_password_salt", Value: "",
Value: "", Description: "管理员密码哈希值",
Description: "管理员密码加密盐值", },
}, {
// ===== 页脚与备案相关默认项 ===== Name: "admin_password_salt",
{ Value: "",
Name: "footer_text", Description: "管理员密码加密盐值",
Value: "Copyright © 2025 凌动技术. All Rights Reserved.", },
Description: "页脚展示的版权或说明信息", // ===== 页脚与备案相关默认项 =====
}, {
{ Name: "footer_text",
Name: "icp_record", Value: "Copyright © 2025 凌动技术. All Rights Reserved.",
Value: "京ICP备12345678号", Description: "页脚展示的版权或说明信息",
Description: "ICP备案号留空则不显示", },
}, {
{ Name: "icp_record",
Name: "icp_record_link", Value: "京ICP备12345678号",
Value: "https://beian.miit.gov.cn", Description: "ICP备案号留空则不显示",
Description: "工信部ICP备案查询链接留空则不显示", },
}, {
{ Name: "icp_record_link",
Name: "psb_record", Value: "https://beian.miit.gov.cn",
Value: "京公网安备 11000002000001号", Description: "工信部ICP备案查询链接留空则不显示",
Description: "公安备案号,留空则不显示", },
}, {
{ Name: "psb_record",
Name: "psb_record_link", Value: "京公网安备 11000002000001号",
Value: "https://www.beian.gov.cn/portal/registerSystemInfo?recordcode=11000002000001", Description: "公安备案号,留空则不显示",
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 // 逐个检查并创建不存在的设置项
} for _, setting := range defaultSettings {
var count int64
if count == 0 { if err := db.Model(&models.Settings{}).Where("name = ?", setting.Name).Count(&count).Error; err != nil {
if err := db.Create(&setting).Error; err != nil { return err
logrus.WithError(err).WithField("name", setting.Name).Error("创建默认设置失败") }
return err
} if count == 0 {
logrus.WithField("name", setting.Name).WithField("value", setting.Value).Info("创建默认设置项") 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("默认系统设置初始化完成") if err := initDefaultAdmin(db); err != nil {
return nil return err
} }
// initDefaultAdmin 初始化默认管理员账号 logrus.Info("默认系统设置初始化完成")
// 如果admin_password为空则生成默认密码admin123的哈希值 return nil
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 // ============================================================================
}
// initDefaultAdmin 初始化默认管理员账号
// 如果密码已设置,跳过初始化 // 如果admin_password为空则生成默认密码admin123的哈希值
if passwordSetting.Value != "" { func initDefaultAdmin(db *gorm.DB) error {
logrus.Debug("管理员密码已设置,跳过默认密码初始化") var passwordSetting models.Settings
return nil if err := db.Where("name = ?", "admin_password").First(&passwordSetting).Error; err != nil {
} logrus.WithError(err).Error("获取管理员密码设置失败")
return err
// 生成密码盐值 }
salt, err := utils.GenerateRandomSalt()
if err != nil { // 如果密码已设置,跳过初始化
logrus.WithError(err).Error("生成密码盐值失败") if passwordSetting.Value != "" {
return err logrus.Debug("管理员密码已设置,跳过默认密码初始化")
} return nil
}
// 使用盐值生成密码哈希默认密码admin123
hash, err := utils.HashPasswordWithSalt("admin123", salt) // 生成密码盐值
if err != nil { salt, err := utils.GenerateRandomSalt()
logrus.WithError(err).Error("生成密码哈希失败") if err != nil {
return err logrus.WithError(err).Error("生成密码盐值失败")
} return err
}
// 更新密码和盐值
if err := db.Model(&models.Settings{}).Where("name = ?", "admin_password").Update("value", hash).Error; err != nil { // 使用盐值生成密码哈希默认密码admin123
logrus.WithError(err).Error("更新管理员密码失败") hash, err := utils.HashPasswordWithSalt("admin123", salt)
return err if 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 // 更新密码和盐值
} if err := db.Model(&models.Settings{}).Where("name = ?", "admin_password").Update("value", hash).Error; err != nil {
logrus.WithError(err).Error("更新管理员密码失败")
logrus.Info("默认管理员账号初始化完成,用户名: admin, 密码: admin123") return err
return nil }
}
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
}

18
main.go
View File

@@ -1,9 +1,9 @@
package main package main
import "networkDev/cmd" import "networkDev/cmd"
// main 是程序的入口点 // main 是程序的入口点
// 调用Cobra命令执行器来处理命令行参数和子命令 // 调用Cobra命令执行器来处理命令行参数和子命令
func main() { func main() {
cmd.Execute() cmd.Execute()
} }

View File

@@ -7,6 +7,10 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
// ============================================================================
// 结构体定义
// ============================================================================
// DevModeConfig 开发模式配置 // DevModeConfig 开发模式配置
type DevModeConfig struct { type DevModeConfig struct {
// 是否启用模板热重载 // 是否启用模板热重载
@@ -19,6 +23,10 @@ type DevModeConfig struct {
EnableDebugLog bool EnableDebugLog bool
} }
// ============================================================================
// 中间件函数
// ============================================================================
// DevModeMiddleware 开发模式中间件 // DevModeMiddleware 开发模式中间件
// 统一管理所有开发模式相关的功能 // 统一管理所有开发模式相关的功能
func DevModeMiddleware(engine *gin.Engine) gin.HandlerFunc { func DevModeMiddleware(engine *gin.Engine) gin.HandlerFunc {
@@ -45,6 +53,10 @@ func DevModeMiddleware(engine *gin.Engine) gin.HandlerFunc {
} }
} }
// ============================================================================
// 公共函数
// ============================================================================
// IsDevMode 检查是否为开发模式 // IsDevMode 检查是否为开发模式
func IsDevMode() bool { func IsDevMode() bool {
return viper.GetBool("server.dev_mode") return viper.GetBool("server.dev_mode")
@@ -57,10 +69,10 @@ func GetDevModeConfig() DevModeConfig {
} }
return DevModeConfig{ return DevModeConfig{
EnableTemplateReload: true, // 开发模式下默认启用模板热重载 EnableTemplateReload: true, // 开发模式下默认启用模板热重载
SkipCaptcha: true, // 开发模式下默认跳过验证码 SkipCaptcha: true, // 开发模式下默认跳过验证码
ShowDetailedErrors: true, // 开发模式下显示详细错误 ShowDetailedErrors: true, // 开发模式下显示详细错误
EnableDebugLog: true, // 开发模式下启用调试日志 EnableDebugLog: true, // 开发模式下启用调试日志
} }
} }
@@ -92,9 +104,13 @@ func ShouldSkipCaptcha(c *gin.Context) bool {
return config.SkipCaptcha return config.SkipCaptcha
} }
// ============================================================================
// 私有函数
// ============================================================================
// reloadTemplates 重新加载模板(内部函数) // reloadTemplates 重新加载模板(内部函数)
func reloadTemplates(engine *gin.Engine) { func reloadTemplates(engine *gin.Engine) {
if tmpl, err := web.ParseTemplates(); err == nil { if tmpl, err := web.ParseTemplates(); err == nil {
engine.SetHTMLTemplate(tmpl) engine.SetHTMLTemplate(tmpl)
} }
} }

View File

@@ -1,97 +1,117 @@
package middleware package middleware
import ( import (
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"networkDev/utils/logger" "networkDev/utils/logger"
) )
// LoggingMiddleware 日志记录中间件结构体 // ============================================================================
// 用于记录HTTP请求的详细信息包括方法、路径、状态码和响应时间 // 结构体定义
type LoggingMiddleware struct { // ============================================================================
logger *logger.Logger
} // LoggingMiddleware 日志记录中间件结构体
// 用于记录HTTP请求的详细信息包括方法、路径、状态码和响应时间
// NewLoggingMiddleware 创建新的日志记录中间件实例 type LoggingMiddleware struct {
func NewLoggingMiddleware(logger *logger.Logger) *LoggingMiddleware { logger *logger.Logger
return &LoggingMiddleware{ }
logger: logger,
} // ============================================================================
} // 构造函数
// ============================================================================
// Handler 返回Gin中间件函数用于记录HTTP请求日志
// 记录格式遵循Apache Common Log Format // NewLoggingMiddleware 创建新的日志记录中间件实例
func (lm *LoggingMiddleware) Handler() gin.HandlerFunc { func NewLoggingMiddleware(logger *logger.Logger) *LoggingMiddleware {
return func(c *gin.Context) { return &LoggingMiddleware{
// 记录开始时间 logger: logger,
start := time.Now() }
}
// 处理请求
c.Next() // ============================================================================
// 中间件函数
// 计算处理时间 // ============================================================================
duration := time.Since(start)
// Handler 返回Gin中间件函数用于记录HTTP请求日志
// 获取客户端IP // 记录格式遵循Apache Common Log Format
clientIP := getClientIP(c) func (lm *LoggingMiddleware) Handler() gin.HandlerFunc {
return func(c *gin.Context) {
// 记录日志 - Apache Common Log Format // 记录开始时间
// 使用专门的HTTP日志方法避免User-Agent中的反斜杠被转义 start := time.Now()
lm.logger.LogRequestWithHeaders(
c.Request.Method, // 处理请求
c.Request.RequestURI, c.Next()
clientIP,
c.Writer.Status(), // 计算处理时间
duration, duration := time.Since(start)
"-", // referer (已废弃)
c.Request.UserAgent(), // 获取客户端IP
) clientIP := getClientIP(c)
}
} // 记录日志 - Apache Common Log Format
// 使用专门的HTTP日志方法避免User-Agent中的反斜杠被转义
// getClientIP 获取客户端真实IP地址 lm.logger.LogRequestWithHeaders(
// 优先从X-Forwarded-For、X-Real-IP等头部获取最后使用RemoteAddr c.Request.Method,
func getClientIP(c *gin.Context) string { c.Request.RequestURI,
// 检查X-Forwarded-For头部 clientIP,
xForwardedFor := c.GetHeader("X-Forwarded-For") c.Writer.Status(),
if xForwardedFor != "" { duration,
// X-Forwarded-For可能包含多个IP取第一个 "-", // referer (已废弃)
ips := strings.Split(xForwardedFor, ",") c.Request.UserAgent(),
if len(ips) > 0 { )
return strings.TrimSpace(ips[0]) }
} }
}
// ============================================================================
// 检查X-Real-IP头部 // 私有函数
xRealIP := c.GetHeader("X-Real-IP") // ============================================================================
if xRealIP != "" {
return xRealIP // getClientIP 获取客户端真实IP地址
} // 优先从X-Forwarded-For、X-Real-IP等头部获取最后使用RemoteAddr
func getClientIP(c *gin.Context) string {
// 检查X-Forwarded头部 // 检查X-Forwarded-For头部
xForwarded := c.GetHeader("X-Forwarded") xForwardedFor := c.GetHeader("X-Forwarded-For")
if xForwarded != "" { if xForwardedFor != "" {
return xForwarded // X-Forwarded-For可能包含多个IP取第一个
} ips := strings.Split(xForwardedFor, ",")
if len(ips) > 0 {
// 使用RemoteAddr return strings.TrimSpace(ips[0])
remoteAddr := c.Request.RemoteAddr }
if strings.Contains(remoteAddr, ":") { }
// 移除端口号
if idx := strings.LastIndex(remoteAddr, ":"); idx != -1 { // 检查X-Real-IP头部
return remoteAddr[:idx] xRealIP := c.GetHeader("X-Real-IP")
} if xRealIP != "" {
} return xRealIP
}
return remoteAddr
} // 检查X-Forwarded头部
xForwarded := c.GetHeader("X-Forwarded")
// WrapHandler 创建Gin日志中间件 if xForwarded != "" {
// 使用全局日志记录器创建日志中间件 return xForwarded
func WrapHandler() gin.HandlerFunc { }
logger := logger.GetLogger()
middleware := NewLoggingMiddleware(logger) // 使用RemoteAddr
return middleware.Handler() 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()
}

View File

@@ -8,11 +8,63 @@ import (
"gorm.io/gorm" "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 接口表模型
// 用于管理API接口的配置信息 // 用于管理API接口的配置信息
// 包含加密算法配置、密钥管理等功能 // CreatedAt/UpdatedAt 由 GORM 自动维护
// 支持多种加密算法不加密、RC4、RSA、RSA动态
type API struct { type API struct {
// ID主键自增 // ID主键自增
ID uint `gorm:"primaryKey;comment:API接口ID自增主键" json:"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"` UpdatedAt time.Time `gorm:"comment:更新时间" json:"updated_at"`
} }
// ============================================================================
// 结构体方法
// ============================================================================
// BeforeCreate 在创建记录前自动生成UUID // BeforeCreate 在创建记录前自动生成UUID
func (api *API) BeforeCreate(tx *gorm.DB) error { func (api *API) BeforeCreate(tx *gorm.DB) error {
if api.UUID == "" { if api.UUID == "" {
@@ -67,51 +123,9 @@ func (API) TableName() string {
return "apis" 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 获取算法名称 // GetAlgorithmName 获取算法名称
func GetAlgorithmName(algorithm int) string { 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 验证算法类型是否有效 // IsValidAlgorithm 验证算法类型是否有效
func IsValidAlgorithm(algorithm int) bool { func IsValidAlgorithm(algorithm int) bool {
return algorithm >= AlgorithmNone && algorithm <= AlgorithmEasy 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类型是否有效 // IsValidAPIType 验证API类型是否有效
func IsValidAPIType(apiType int) bool { func IsValidAPIType(apiType int) bool {
validTypes := []int{ validTypes := GetDefaultAPITypes()
APITypeGetBulletin, APITypeGetUpdateUrl, APITypeCheckAppVersion, APITypeGetCardInfo,
APITypeSingleLogin,
APITypeUserLogin, APITypeUserRegin, APITypeUserRecharge, APITypeCardRegin,
APITypeLogOut,
APITypeGetExpired, APITypeCheckUserStatus, APITypeGetAppData, APITypeGetVariable,
APITypeUpdatePwd, APITypeMacChangeBind, APITypeIPChangeBind,
APITypeDisableUser, APITypeBlackUser, APITypeUserDeductedTime,
}
for _, validType := range validTypes { for _, validType := range validTypes {
if apiType == validType { if apiType == validType {
@@ -218,15 +282,34 @@ func IsValidAPIType(apiType int) bool {
return false return false
} }
// GetAPITypesByCategory 根据分类获取API类型列表 // ============================================================================
func GetAPITypesByCategory() map[string][]int { // 兼容性函数
return map[string][]int{ // ============================================================================
"基础信息获取": {APITypeGetBulletin, APITypeGetUpdateUrl, APITypeCheckAppVersion, APITypeGetCardInfo},
"登录相关": {APITypeSingleLogin}, // GetDefaultAPITypes 获取默认创建的API接口类型列表兼容性函数
"用户账号管理": {APITypeUserLogin, APITypeUserRegin, APITypeUserRecharge, APITypeCardRegin}, func GetDefaultAPITypes() []int {
"登出": {APITypeLogOut}, return GetAPITypes(false).([]int)
"用户状态查询": {APITypeGetExpired, APITypeCheckUserStatus, APITypeGetAppData, APITypeGetVariable}, }
"用户操作": {APITypeUpdatePwd, APITypeMacChangeBind, APITypeIPChangeBind},
"管理员操作": {APITypeDisableUser, APITypeBlackUser, APITypeUserDeductedTime}, // 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)
} }

View File

@@ -10,15 +10,13 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
// ============================================================================
// 结构体定义
// ============================================================================
// App 应用表模型 // App 应用表模型
// 用于管理应用程序的基本信息 // 用于管理应用程序的基本信息
// UUID 为应用的唯一标识符,自动生成
// Status 为应用状态1:启用 0:禁用默认为1
// Name 为应用名称
// Secret 为应用密钥用于API认证
// Version 为应用版本号
// CreatedAt/UpdatedAt 由 GORM 自动维护 // CreatedAt/UpdatedAt 由 GORM 自动维护
type App struct { type App struct {
// ID主键自增同时通过 json 标签保证前端接收为 id // ID主键自增同时通过 json 标签保证前端接收为 id
ID uint `gorm:"primaryKey;comment:应用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"` UpdatedAt time.Time `gorm:"comment:更新时间" json:"updated_at"`
} }
// ============================================================================
// 结构体方法
// ============================================================================
// BeforeCreate 在创建记录前自动生成UUID和密钥 // BeforeCreate 在创建记录前自动生成UUID和密钥
func (app *App) BeforeCreate(tx *gorm.DB) error { func (app *App) BeforeCreate(tx *gorm.DB) error {
if app.UUID == "" { if app.UUID == "" {

View File

@@ -9,14 +9,13 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
// ============================================================================
// 结构体定义
// ============================================================================
// Function 函数表模型 // Function 函数表模型
// 用于管理应用程序的函数代码 // 用于管理应用程序的函数代码
// UUID 为函数的唯一标识符,自动生成并转换为大写
// Alias 为函数别名,便于识别和管理
// Code 为函数代码内容
// Remark 为备注信息,用于描述函数用途
// CreatedAt/UpdatedAt 由 GORM 自动维护 // CreatedAt/UpdatedAt 由 GORM 自动维护
type Function struct { type Function struct {
// ID主键自增 // ID主键自增
ID uint `gorm:"primaryKey;comment:函数ID自增主键" json:"id"` ID uint `gorm:"primaryKey;comment:函数ID自增主键" json:"id"`
@@ -44,6 +43,10 @@ type Function struct {
UpdatedAt time.Time `gorm:"comment:更新时间" json:"updated_at"` UpdatedAt time.Time `gorm:"comment:更新时间" json:"updated_at"`
} }
// ============================================================================
// 结构体方法
// ============================================================================
// BeforeCreate 在创建记录前自动生成UUID和Number // BeforeCreate 在创建记录前自动生成UUID和Number
func (function *Function) BeforeCreate(tx *gorm.DB) error { func (function *Function) BeforeCreate(tx *gorm.DB) error {
// 生成UUID // 生成UUID
@@ -59,4 +62,4 @@ func (function *Function) BeforeCreate(tx *gorm.DB) error {
// TableName 指定表名 // TableName 指定表名
func (Function) TableName() string { func (Function) TableName() string {
return "functions" return "functions"
} }

View File

@@ -2,13 +2,13 @@ package models
import "time" import "time"
// ============================================================================
// 结构体定义
// ============================================================================
// Settings 系统设置表模型 // Settings 系统设置表模型
// 用于存储应用的配置参数 // 用于存储应用的配置参数
// Name 为配置项名称,唯一索引
// Value 为配置项的值
// Description 为配置项描述说明
// CreatedAt/UpdatedAt 由 GORM 自动维护 // CreatedAt/UpdatedAt 由 GORM 自动维护
type Settings struct { type Settings struct {
ID uint `gorm:"primaryKey;comment:设置ID自增主键"` ID uint `gorm:"primaryKey;comment:设置ID自增主键"`
Name string `gorm:"uniqueIndex;size:64;not null;comment:配置项名称,唯一索引"` Name string `gorm:"uniqueIndex;size:64;not null;comment:配置项名称,唯一索引"`

View File

@@ -1,31 +1,39 @@
package models package models
import ( import (
"strings" "strings"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"gorm.io/gorm" "gorm.io/gorm"
) )
// User 用户表模型 // ============================================================================
// 说明PasswordSalt 使用 32 字节随机盐(以 16 进制存储为 64 个字符),因此列长度设置为 64 // 结构体定义
// 注意此表只存储普通用户管理员账号存储在settings表中 // ============================================================================
type User struct {
ID uint `gorm:"primaryKey;comment:用户ID自增主键"` // User 用户表模型
UUID string `gorm:"uniqueIndex;size:36;not null;comment:用户的唯一标识符" json:"uuid"` // 此表只存储普通用户管理员账号存储在settings表中
Username string `gorm:"uniqueIndex;size:64;not null;comment:用户名,唯一索引"` // CreatedAt/UpdatedAt 由 GORM 自动维护
Password string `gorm:"size:255;not null;comment:密码哈希值"` type User struct {
PasswordSalt string `gorm:"size:64;not null;comment:密码加密盐值"` ID uint `gorm:"primaryKey;comment:用户ID自增主键"`
CreatedAt time.Time `gorm:"comment:创建时间"` UUID string `gorm:"uniqueIndex;size:36;not null;comment:用户的唯一标识符" json:"uuid"`
UpdatedAt time.Time `gorm:"comment:更新时间"` 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:密码加密盐值"`
// BeforeCreate 在创建记录前自动生成UUID CreatedAt time.Time `gorm:"comment:创建时间"`
func (user *User) BeforeCreate(tx *gorm.DB) error { UpdatedAt time.Time `gorm:"comment:更新时间"`
// 生成UUID }
if user.UUID == "" {
user.UUID = strings.ToUpper(uuid.New().String()) // ============================================================================
} // 结构体方法
return nil // ============================================================================
}
// BeforeCreate 在创建记录前自动生成UUID
func (user *User) BeforeCreate(tx *gorm.DB) error {
// 生成UUID
if user.UUID == "" {
user.UUID = strings.ToUpper(uuid.New().String())
}
return nil
}

View File

@@ -9,14 +9,13 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
// ============================================================================
// 结构体定义
// ============================================================================
// Variable 变量表模型 // Variable 变量表模型
// 用于管理应用程序的变量数据 // 用于管理应用程序的变量数据
// UUID 为变量的唯一标识符,自动生成并转换为大写
// Alias 为变量别名,便于识别和管理
// Data 为变量数据内容
// Remark 为备注信息,用于描述变量用途
// CreatedAt/UpdatedAt 由 GORM 自动维护 // CreatedAt/UpdatedAt 由 GORM 自动维护
type Variable struct { type Variable struct {
// ID主键自增 // ID主键自增
ID uint `gorm:"primaryKey;comment:变量ID自增主键" json:"id"` ID uint `gorm:"primaryKey;comment:变量ID自增主键" json:"id"`
@@ -44,6 +43,10 @@ type Variable struct {
UpdatedAt time.Time `gorm:"comment:更新时间" json:"updated_at"` UpdatedAt time.Time `gorm:"comment:更新时间" json:"updated_at"`
} }
// ============================================================================
// 结构体方法
// ============================================================================
// BeforeCreate 在创建记录前自动生成UUID和Number // BeforeCreate 在创建记录前自动生成UUID和Number
func (variable *Variable) BeforeCreate(tx *gorm.DB) error { func (variable *Variable) BeforeCreate(tx *gorm.DB) error {
// 生成UUID // 生成UUID

View File

@@ -1,138 +1,142 @@
package server package server
import ( import (
adminctl "networkDev/controllers/admin" adminctl "networkDev/controllers/admin"
"networkDev/utils" "networkDev/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// RegisterAdminRoutes 注册管理员后台相关路由 // ============================================================================
// - /admin/login: 支持GET渲染登录页、POST提交登录 // 路由注册函数
// - /admin/logout: 管理员退出登录 // ============================================================================
// - /admin/dashboard: 管理员仪表盘(示例)
// - /admin/fragment/*: 布局内动态片段加载 // RegisterAdminRoutes 注册管理员后台相关路由
// - /admin/api/settings*: 设置接口(查询/更新) // - /admin/login: 支持GET渲染登录页、POST提交登录
func RegisterAdminRoutes(router *gin.Engine) { // - /admin/logout: 管理员退出登录
// /admin 根与前缀统一入口:根据是否登录跳转 // - /admin/dashboard: 管理员仪表盘(示例)
router.GET("/admin", adminctl.AdminIndexHandler) // - /admin/fragment/*: 布局内动态片段加载
router.GET("/admin/", adminctl.AdminIndexHandler) // - /admin/api/settings*: 设置接口(查询/更新)
func RegisterAdminRoutes(router *gin.Engine) {
// Admin 认证相关路由 // /admin 根与前缀统一入口:根据是否登录跳转
router.GET("/admin/login", adminctl.LoginPageHandler) router.GET("/admin", adminctl.AdminIndexHandler)
router.POST("/admin/login", adminctl.LoginHandler) // CSRF验证在控制器内部处理 router.GET("/admin/", adminctl.AdminIndexHandler)
// 退出登录(无需拦截,幂等清理) // Admin 认证相关路由
router.POST("/admin/logout", adminctl.LogoutHandler) router.GET("/admin/login", adminctl.LoginPageHandler)
router.POST("/admin/login", adminctl.LoginHandler) // CSRF验证在控制器内部处理
// 验证码生成路由(无需认证)
router.GET("/admin/captcha", adminctl.CaptchaHandler) // 退出登录(无需拦截,幂等清理)
router.POST("/admin/logout", adminctl.LogoutHandler)
// CSRF令牌获取API无需认证但需要在登录页面等地方获取
router.GET("/admin/api/csrf-token", func(c *gin.Context) { // 验证码生成路由(无需认证)
// 生成新的CSRF令牌 router.GET("/admin/captcha", adminctl.CaptchaHandler)
token, err := utils.GenerateCSRFToken()
if err != nil { // CSRF令牌获取API无需认证但需要在登录页面等地方获取
c.JSON(500, gin.H{"success": false, "message": "生成CSRF令牌失败"}) router.GET("/admin/api/csrf-token", func(c *gin.Context) {
return // 生成新的CSRF令牌
} token, err := utils.GenerateCSRFToken()
if err != nil {
// 设置令牌到Cookie和响应头 c.JSON(500, gin.H{"success": false, "message": "生成CSRF令牌失败"})
utils.SetCSRFToken(c, token) return
}
// 返回令牌给前端
c.JSON(200, gin.H{ // 设置令牌到Cookie和响应头
"success": true, utils.SetCSRFToken(c, token)
"message": "CSRF令牌生成成功",
"csrf_token": 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/layout", adminctl.AdminAuthRequired(), adminctl.AdminLayoutHandler)
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/dashboard", adminctl.AdminAuthRequired(), adminctl.DashboardFragmentHandler)
router.GET("/admin/variables", adminctl.AdminAuthRequired(), adminctl.VariableFragmentHandler) router.GET("/admin/user", adminctl.AdminAuthRequired(), adminctl.UserFragmentHandler)
router.GET("/admin/functions", adminctl.AdminAuthRequired(), adminctl.FunctionFragmentHandler) router.GET("/admin/settings", adminctl.AdminAuthRequired(), adminctl.SettingsFragmentHandler)
router.GET("/admin/apps", adminctl.AdminAuthRequired(), adminctl.AppsFragmentHandler)
// 系统信息API用于仪表盘定时刷新 router.GET("/admin/apis", adminctl.AdminAuthRequired(), adminctl.APIFragmentHandler)
router.GET("/admin/api/system/info", adminctl.AdminAuthRequired(), adminctl.SystemInfoHandler) router.GET("/admin/variables", adminctl.AdminAuthRequired(), adminctl.VariableFragmentHandler)
router.GET("/admin/functions", adminctl.AdminAuthRequired(), adminctl.FunctionFragmentHandler)
// 仪表盘统计数据API
router.GET("/admin/api/dashboard/stats", adminctl.AdminAuthRequired(), adminctl.DashboardStatsHandler) // 系统信息API用于仪表盘定时刷新
router.GET("/admin/api/system/info", adminctl.AdminAuthRequired(), adminctl.SystemInfoHandler)
// 个人资料API
userGroup := router.Group("/admin/api/user", adminctl.AdminAuthRequired()) // 仪表盘统计数据API
{ router.GET("/admin/api/dashboard/stats", adminctl.AdminAuthRequired(), adminctl.DashboardStatsHandler)
userGroup.GET("/profile", adminctl.UserProfileQueryHandler)
userGroup.POST("/profile/update", adminctl.UserProfileUpdateHandler) // 个人资料API
userGroup.POST("/password", adminctl.UserPasswordUpdateHandler) userGroup := router.Group("/admin/api/user", adminctl.AdminAuthRequired())
} {
userGroup.GET("/profile", adminctl.UserProfileQueryHandler)
// 系统设置API userGroup.POST("/profile/update", adminctl.UserProfileUpdateHandler)
settingsGroup := router.Group("/admin/api/settings", adminctl.AdminAuthRequired()) userGroup.POST("/password", adminctl.UserPasswordUpdateHandler)
{ }
settingsGroup.GET("", adminctl.SettingsQueryHandler)
settingsGroup.POST("/update", adminctl.SettingsUpdateHandler) // 系统设置API
} settingsGroup := router.Group("/admin/api/settings", adminctl.AdminAuthRequired())
{
// 应用管理API settingsGroup.GET("", adminctl.SettingsQueryHandler)
appsGroup := router.Group("/admin/api/apps", adminctl.AdminAuthRequired()) settingsGroup.POST("/update", adminctl.SettingsUpdateHandler)
{ }
appsGroup.GET("/list", adminctl.AppsListHandler)
appsGroup.GET("/simple", adminctl.AppsSimpleListHandler) // 应用管理API
appsGroup.POST("/create", adminctl.AppCreateHandler) appsGroup := router.Group("/admin/api/apps", adminctl.AdminAuthRequired())
appsGroup.POST("/update", adminctl.AppUpdateHandler) {
appsGroup.POST("/delete", adminctl.AppDeleteHandler) appsGroup.GET("/list", adminctl.AppsListHandler)
appsGroup.POST("/batch_delete", adminctl.AppsBatchDeleteHandler) appsGroup.GET("/simple", adminctl.AppsSimpleListHandler)
appsGroup.POST("/batch_update_status", adminctl.AppsBatchUpdateStatusHandler) appsGroup.POST("/create", adminctl.AppCreateHandler)
appsGroup.POST("/update_status", adminctl.AppUpdateStatusHandler) appsGroup.POST("/update", adminctl.AppUpdateHandler)
appsGroup.POST("/reset_secret", adminctl.AppResetSecretHandler) appsGroup.POST("/delete", adminctl.AppDeleteHandler)
appsGroup.GET("/get_app_data", adminctl.AppGetAppDataHandler) appsGroup.POST("/batch_delete", adminctl.AppsBatchDeleteHandler)
appsGroup.POST("/update_app_data", adminctl.AppUpdateAppDataHandler) appsGroup.POST("/batch_update_status", adminctl.AppsBatchUpdateStatusHandler)
appsGroup.GET("/get_announcement", adminctl.AppGetAnnouncementHandler) appsGroup.POST("/update_status", adminctl.AppUpdateStatusHandler)
appsGroup.POST("/update_announcement", adminctl.AppUpdateAnnouncementHandler) appsGroup.POST("/reset_secret", adminctl.AppResetSecretHandler)
appsGroup.GET("/get_multi_config", adminctl.AppGetMultiConfigHandler) appsGroup.GET("/get_app_data", adminctl.AppGetAppDataHandler)
appsGroup.POST("/update_multi_config", adminctl.AppUpdateMultiConfigHandler) appsGroup.POST("/update_app_data", adminctl.AppUpdateAppDataHandler)
appsGroup.GET("/get_bind_config", adminctl.AppGetBindConfigHandler) appsGroup.GET("/get_announcement", adminctl.AppGetAnnouncementHandler)
appsGroup.POST("/update_bind_config", adminctl.AppUpdateBindConfigHandler) appsGroup.POST("/update_announcement", adminctl.AppUpdateAnnouncementHandler)
appsGroup.GET("/get_register_config", adminctl.AppGetRegisterConfigHandler) appsGroup.GET("/get_multi_config", adminctl.AppGetMultiConfigHandler)
appsGroup.POST("/update_register_config", adminctl.AppUpdateRegisterConfigHandler) appsGroup.POST("/update_multi_config", adminctl.AppUpdateMultiConfigHandler)
} appsGroup.GET("/get_bind_config", adminctl.AppGetBindConfigHandler)
appsGroup.POST("/update_bind_config", adminctl.AppUpdateBindConfigHandler)
// API接口管理API appsGroup.GET("/get_register_config", adminctl.AppGetRegisterConfigHandler)
apisGroup := router.Group("/admin/api/apis", adminctl.AdminAuthRequired()) appsGroup.POST("/update_register_config", adminctl.AppUpdateRegisterConfigHandler)
{ }
apisGroup.GET("/list", adminctl.APIListHandler)
apisGroup.POST("/update", adminctl.APIUpdateHandler) // API接口管理API
apisGroup.POST("/update_status", adminctl.APIUpdateStatusHandler) apisGroup := router.Group("/admin/api/apis", adminctl.AdminAuthRequired())
apisGroup.GET("/types", adminctl.APIGetTypesHandler) {
apisGroup.POST("/generate_keys", adminctl.APIGenerateKeysHandler) apisGroup.GET("/list", adminctl.APIListHandler)
} apisGroup.POST("/update", adminctl.APIUpdateHandler)
apisGroup.POST("/update_status", adminctl.APIUpdateStatusHandler)
// 变量管理API apisGroup.GET("/types", adminctl.APIGetTypesHandler)
variableGroup := router.Group("/admin/variable", adminctl.AdminAuthRequired()) apisGroup.POST("/generate_keys", adminctl.APIGenerateKeysHandler)
{ }
variableGroup.GET("/list", adminctl.VariableListHandler)
variableGroup.POST("/create", adminctl.VariableCreateHandler) // 变量管理API
variableGroup.POST("/update", adminctl.VariableUpdateHandler) variableGroup := router.Group("/admin/variable", adminctl.AdminAuthRequired())
variableGroup.POST("/delete", adminctl.VariableDeleteHandler) {
variableGroup.POST("/batch_delete", adminctl.VariablesBatchDeleteHandler) variableGroup.GET("/list", adminctl.VariableListHandler)
} variableGroup.POST("/create", adminctl.VariableCreateHandler)
variableGroup.POST("/update", adminctl.VariableUpdateHandler)
// 函数管理API variableGroup.POST("/delete", adminctl.VariableDeleteHandler)
functionGroup := router.Group("/admin/function", adminctl.AdminAuthRequired()) variableGroup.POST("/batch_delete", adminctl.VariablesBatchDeleteHandler)
{ }
functionGroup.GET("/list", adminctl.FunctionListHandler)
functionGroup.POST("/create", adminctl.FunctionCreateHandler) // 函数管理API
functionGroup.POST("/update", adminctl.FunctionUpdateHandler) functionGroup := router.Group("/admin/function", adminctl.AdminAuthRequired())
functionGroup.POST("/delete", adminctl.FunctionDeleteHandler) {
functionGroup.POST("/batch_delete", adminctl.FunctionsBatchDeleteHandler) 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)
}
}

View File

@@ -1,14 +1,18 @@
package server package server
import ( import (
"networkDev/controllers/home" "networkDev/controllers/home"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// RegisterHomeRoutes 注册主页路由 // ============================================================================
// 只包含根路径,用于主页功能 // 路由注册函数
func RegisterHomeRoutes(router *gin.Engine) { // ============================================================================
// 根路径 - 主页
router.GET("/", home.RootHandler) // RegisterHomeRoutes 注册主页路由
} // 只包含根路径,用于主页功能
func RegisterHomeRoutes(router *gin.Engine) {
// 根路径 - 主页
router.GET("/", home.RootHandler)
}

View File

@@ -1,48 +1,56 @@
package server package server
import ( import (
"io/fs" "io/fs"
"log" "log"
"net/http" "net/http"
"networkDev/web" "networkDev/web"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// RegisterRoutes 聚合注册所有路由 // ============================================================================
func RegisterRoutes(router *gin.Engine) { // 公共函数
registerStaticRoutes(router) // ============================================================================
registerFaviconRoute(router)
RegisterHomeRoutes(router) // RegisterRoutes 聚合注册所有路由
RegisterAdminRoutes(router) func RegisterRoutes(router *gin.Engine) {
registerStaticRoutes(router)
} registerFaviconRoute(router)
RegisterHomeRoutes(router)
// registerStaticRoutes 注册静态资源路由 RegisterAdminRoutes(router)
// 静态资源服务,将 /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) // registerStaticRoutes 注册静态资源路由
} // 静态资源服务,将 /static/ 和 /assets/ 映射到嵌入的文件系统
// 为 /assets/ 路径创建子文件系统 func registerStaticRoutes(router *gin.Engine) {
if assetsSubFS, assetsErr := fs.Sub(fsys, "assets"); assetsErr == nil { if fsys, err := web.GetStaticFS(); err == nil {
router.StaticFS("/assets", http.FS(assetsSubFS)) // 为 /static/ 路径创建子文件系统
} else { if staticSubFS, staticErr := fs.Sub(fsys, "static"); staticErr == nil {
log.Printf("创建资产资源子文件系统失败: %v", assetsErr) router.StaticFS("/static", http.FS(staticSubFS))
} } else {
} else { log.Printf("创建静态资源子文件系统失败: %v", staticErr)
log.Printf("初始化静态资源文件系统失败: %v", err) }
} // 为 /assets/ 路径创建子文件系统
} if assetsSubFS, assetsErr := fs.Sub(fsys, "assets"); assetsErr == nil {
router.StaticFS("/assets", http.FS(assetsSubFS))
// registerFaviconRoute 注册favicon路由 } else {
func registerFaviconRoute(router *gin.Engine) { log.Printf("创建资产资源子文件系统失败: %v", assetsErr)
// 将 /favicon.ico 重定向到 /assets/favicon.svg }
router.GET("/favicon.ico", func(c *gin.Context) { } else {
c.Redirect(http.StatusMovedPermanently, "/assets/favicon.svg") 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")
})
}

View File

@@ -1,85 +1,101 @@
package services package services
import ( import (
"context" "context"
"fmt" "fmt"
"networkDev/models" "networkDev/models"
"networkDev/utils" "networkDev/utils"
"time" "time"
"gorm.io/gorm" "gorm.io/gorm"
) )
// FindSettingByName 根据名称查找设置 // ============================================================================
// name: 设置名称 // 查询函数
// db: 数据库连接 // ============================================================================
// 返回: 设置信息和错误
func FindSettingByName(name string, db *gorm.DB) (*models.Settings, error) { // FindSettingByName 根据名称查找设置
key := fmt.Sprintf("setting:%s", name) // name: 设置名称
return utils.RedisGetOrSet(context.Background(), key, 5*time.Minute, func() (*models.Settings, error) { // db: 数据库连接
var setting models.Settings // 返回: 设置信息和错误
err := db.Where("name = ?", name).First(&setting).Error func FindSettingByName(name string, db *gorm.DB) (*models.Settings, error) {
if err != nil { key := fmt.Sprintf("setting:%s", name)
return nil, err return utils.RedisGetOrSet(context.Background(), key, 5*time.Minute, func() (*models.Settings, error) {
} var setting models.Settings
return &setting, nil err := db.Where("name = ?", name).First(&setting).Error
}) if err != nil {
} return nil, err
}
// UpdateEntityByID 根据ID更新实体 return &setting, nil
// 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
} // UpdateEntityByID 根据ID更新实体
// model: 模型类型
// BatchUpdateEntityStatus 批量更新实体状态 // id: 实体ID
// model: 模型类型 // updates: 更新字段
// ids: 实体ID列表 // db: 数据库连接
// status: 新状态 // 返回: 错误
// 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
func BatchUpdateEntityStatus(model interface{}, ids []uint, status int, db *gorm.DB) error { }
if len(ids) == 0 {
return nil // BatchUpdateEntityStatus 批量更新实体状态
} // model: 模型类型
return db.Model(model).Where("id IN ?", ids).Update("status", status).Error // ids: 实体ID列表
} // status: 新状态
// db: 数据库连接
// CountEntitiesByCondition 根据条件统计实体数量 // 返回: 错误
// model: 模型类型 func BatchUpdateEntityStatus(model interface{}, ids []uint, status int, db *gorm.DB) error {
// condition: 查询条件 if len(ids) == 0 {
// db: 数据库连接 return nil
// args: 查询参数 }
// 返回: 数量和错误 return db.Model(model).Where("id IN ?", ids).Update("status", status).Error
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 根据条件查找实体 // CountEntitiesByCondition 根据条件统计实体数量
// model: 模型类型 // model: 模型类型
// result: 结果容器 // condition: 查询条件
// condition: 查询条件 // db: 数据库连接
// db: 数据库连接 // args: 查询参数
// args: 查询参数 // 返回: 数量和错误
// 返回: 错误 func CountEntitiesByCondition(model interface{}, condition string, db *gorm.DB, args ...interface{}) (int64, error) {
func FindEntitiesByCondition(model interface{}, result interface{}, condition string, db *gorm.DB, args ...interface{}) error { var count int64
return db.Model(model).Where(condition, args...).Find(result).Error err := db.Model(model).Where(condition, args...).Count(&count).Error
} return count, err
}
// CheckEntityExists 检查实体是否存在
// model: 模型类型 // ============================================================================
// condition: 查询条件 // 通用查询函数
// db: 数据库连接 // ============================================================================
// args: 查询参数
// 返回: 是否存在和错误 // FindEntitiesByCondition 根据条件查找实体
func CheckEntityExists(model interface{}, condition string, db *gorm.DB, args ...interface{}) (bool, error) { // model: 模型类型
var count int64 // result: 结果容器
err := db.Model(model).Where(condition, args...).Count(&count).Error // condition: 查询条件
return count > 0, err // 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
}

View File

@@ -9,15 +9,27 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// ============================================================================
// 结构体定义
// ============================================================================
// SettingsService 设置服务 // SettingsService 设置服务
type SettingsService struct { type SettingsService struct {
mu sync.RWMutex mu sync.RWMutex
cache map[string]string cache map[string]string
} }
// ============================================================================
// 全局变量
// ============================================================================
var settingsService *SettingsService var settingsService *SettingsService
var settingsOnce sync.Once var settingsOnce sync.Once
// ============================================================================
// 公共函数
// ============================================================================
// GetSettingsService 获取设置服务单例 // GetSettingsService 获取设置服务单例
func GetSettingsService() *SettingsService { func GetSettingsService() *SettingsService {
settingsOnce.Do(func() { settingsOnce.Do(func() {
@@ -30,6 +42,10 @@ func GetSettingsService() *SettingsService {
return settingsService return settingsService
} }
// ============================================================================
// 私有函数
// ============================================================================
// loadAllSettings 从数据库加载所有设置到缓存 // loadAllSettings 从数据库加载所有设置到缓存
func (s *SettingsService) loadAllSettings() { func (s *SettingsService) loadAllSettings() {
db, err := database.GetDB() db, err := database.GetDB()

View File

@@ -7,6 +7,10 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
// ============================================================================
// Cookie创建函数
// ============================================================================
// CreateSecureCookie 创建安全的Cookie // CreateSecureCookie 创建安全的Cookie
// name: Cookie名称 // name: Cookie名称
// value: Cookie值 // value: Cookie值
@@ -67,6 +71,10 @@ func CreateExpiredCookie(name string) *http.Cookie {
return CreateSecureCookie(name, "", -1) return CreateSecureCookie(name, "", -1)
} }
// ============================================================================
// 配置函数
// ============================================================================
// GetDefaultCookieMaxAge 获取默认Cookie过期时间 // GetDefaultCookieMaxAge 获取默认Cookie过期时间
func GetDefaultCookieMaxAge() int { func GetDefaultCookieMaxAge() int {
maxAge := viper.GetInt("security.cookie.max_age") maxAge := viper.GetInt("security.cookie.max_age")
@@ -74,4 +82,4 @@ func GetDefaultCookieMaxAge() int {
return 86400 // 默认24小时 return 86400 // 默认24小时
} }
return maxAge return maxAge
} }

View File

@@ -15,6 +15,10 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
// ============================================================================
// 结构体定义
// ============================================================================
// CryptoManager 加密管理器,提供高性能的加密解密服务 // CryptoManager 加密管理器,提供高性能的加密解密服务
type CryptoManager struct { type CryptoManager struct {
key []byte key []byte
@@ -23,9 +27,17 @@ type CryptoManager struct {
inited bool inited bool
} }
// ============================================================================
// 全局变量
// ============================================================================
// 全局加密管理器实例 // 全局加密管理器实例
var cryptoManager = &CryptoManager{} var cryptoManager = &CryptoManager{}
// ============================================================================
// 私有函数
// ============================================================================
// initCrypto 初始化加密管理器 // initCrypto 初始化加密管理器
// 缓存密钥和GCM实例避免重复创建 // 缓存密钥和GCM实例避免重复创建
func (cm *CryptoManager) initCrypto() error { func (cm *CryptoManager) initCrypto() error {
@@ -63,6 +75,10 @@ func (cm *CryptoManager) initCrypto() error {
return nil return nil
} }
// ============================================================================
// 加密解密函数
// ============================================================================
// EncryptString 字符串加密AES-256-GCM // EncryptString 字符串加密AES-256-GCM
// 使用缓存的密钥和GCM实例提高性能 // 使用缓存的密钥和GCM实例提高性能
func EncryptString(plain string) (string, error) { func EncryptString(plain string) (string, error) {

View File

@@ -9,6 +9,10 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// ============================================================================
// 常量定义
// ============================================================================
const ( const (
CSRFTokenLength = 32 CSRFTokenLength = 32
CSRFCookieName = "csrf_token" CSRFCookieName = "csrf_token"
@@ -16,6 +20,10 @@ const (
CSRFFormField = "csrf_token" CSRFFormField = "csrf_token"
) )
// ============================================================================
// 私有函数
// ============================================================================
// generateRandomBytes 生成指定长度的随机字节 // generateRandomBytes 生成指定长度的随机字节
func generateRandomBytes(length int) ([]byte, error) { func generateRandomBytes(length int) ([]byte, error) {
bytes := make([]byte, length) bytes := make([]byte, length)
@@ -26,6 +34,10 @@ func generateRandomBytes(length int) ([]byte, error) {
return bytes, nil return bytes, nil
} }
// ============================================================================
// 公共函数
// ============================================================================
// GenerateCSRFToken 生成CSRF令牌 // GenerateCSRFToken 生成CSRF令牌
func GenerateCSRFToken() (string, error) { func GenerateCSRFToken() (string, error) {
bytes, err := generateRandomBytes(CSRFTokenLength) bytes, err := generateRandomBytes(CSRFTokenLength)
@@ -190,4 +202,4 @@ func CSRFTokenHandler(c *gin.Context) {
"csrf_token": token, "csrf_token": token,
}, },
}) })
} }

View File

@@ -1,327 +1,343 @@
package utils package utils
import ( import (
"context" "context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"sync" "sync"
"time" "time"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/viper" "github.com/spf13/viper"
"gorm.io/gorm" "gorm.io/gorm"
) )
// DatabaseConfig 数据库连接池配置结构体 // ============================================================================
// 用于配置数据库连接池的各项参数,包括连接池大小、生命周期管理和健康检查等 // 结构体定义
type DatabaseConfig struct { // ============================================================================
// 连接池配置
MaxIdleConns int `mapstructure:"max_idle_conns"` // 最大空闲连接数 // DatabaseConfig 数据库连接池配置结构体
MaxOpenConns int `mapstructure:"max_open_conns"` // 最大打开连接数 // 用于配置数据库连接池的各项参数,包括连接池大小、生命周期管理和健康检查等
ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"` // 连接最大生存时间 type DatabaseConfig struct {
ConnMaxIdleTime time.Duration `mapstructure:"conn_max_idle_time"` // 连接最大空闲时间 // 连接池配置
MaxIdleConns int `mapstructure:"max_idle_conns"` // 最大空闲连接数
// 健康检查配置 MaxOpenConns int `mapstructure:"max_open_conns"` // 最大打开连接数
PingTimeout time.Duration `mapstructure:"ping_timeout"` // Ping超时时间 ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"` // 连接最大生存时间
HealthCheckInterval time.Duration `mapstructure:"health_check_interval"` // 健康检查间隔 ConnMaxIdleTime time.Duration `mapstructure:"conn_max_idle_time"` // 连接最大空闲时间
}
// 健康检查配置
// GetDefaultDatabaseConfig 获取默认数据库配置 PingTimeout time.Duration `mapstructure:"ping_timeout"` // Ping超时时间
// 返回一个包含合理默认值的数据库配置实例 HealthCheckInterval time.Duration `mapstructure:"health_check_interval"` // 健康检查间隔
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秒 // GetDefaultDatabaseConfig 获取默认数据库配置
HealthCheckInterval: 30 * time.Second, // 健康检查间隔30秒 // 返回一个包含合理默认值的数据库配置实例
} func GetDefaultDatabaseConfig() *DatabaseConfig {
} return &DatabaseConfig{
MaxIdleConns: 10, // 默认最大空闲连接数
// LoadDatabaseConfig 从配置文件加载数据库配置 MaxOpenConns: 100, // 默认最大打开连接数
// 使用指定的前缀从viper配置中读取数据库配置如果配置项不存在则使用默认值 ConnMaxLifetime: 30 * time.Minute, // 连接最大生存时间30分钟
func LoadDatabaseConfig(prefix string) *DatabaseConfig { ConnMaxIdleTime: 10 * time.Minute, // 连接最大空闲时间10分钟
config := GetDefaultDatabaseConfig() PingTimeout: 5 * time.Second, // Ping超时5秒
HealthCheckInterval: 30 * time.Second, // 健康检查间隔30秒
// 从viper读取配置如果不存在则使用默认值 }
if viper.IsSet(prefix + ".max_idle_conns") { }
config.MaxIdleConns = viper.GetInt(prefix + ".max_idle_conns")
} // LoadDatabaseConfig 从配置文件加载数据库配置
if viper.IsSet(prefix + ".max_open_conns") { // 使用指定的前缀从viper配置中读取数据库配置如果配置项不存在则使用默认值
config.MaxOpenConns = viper.GetInt(prefix + ".max_open_conns") func LoadDatabaseConfig(prefix string) *DatabaseConfig {
} config := GetDefaultDatabaseConfig()
if viper.IsSet(prefix + ".conn_max_lifetime") {
config.ConnMaxLifetime = viper.GetDuration(prefix + ".conn_max_lifetime") // 从viper读取配置如果不存在则使用默认值
} if viper.IsSet(prefix + ".max_idle_conns") {
if viper.IsSet(prefix + ".conn_max_idle_time") { config.MaxIdleConns = viper.GetInt(prefix + ".max_idle_conns")
config.ConnMaxIdleTime = viper.GetDuration(prefix + ".conn_max_idle_time") }
} if viper.IsSet(prefix + ".max_open_conns") {
if viper.IsSet(prefix + ".ping_timeout") { config.MaxOpenConns = viper.GetInt(prefix + ".max_open_conns")
config.PingTimeout = viper.GetDuration(prefix + ".ping_timeout") }
} if viper.IsSet(prefix + ".conn_max_lifetime") {
if viper.IsSet(prefix + ".health_check_interval") { config.ConnMaxLifetime = viper.GetDuration(prefix + ".conn_max_lifetime")
config.HealthCheckInterval = viper.GetDuration(prefix + ".health_check_interval") }
} if viper.IsSet(prefix + ".conn_max_idle_time") {
config.ConnMaxIdleTime = viper.GetDuration(prefix + ".conn_max_idle_time")
return config }
} if viper.IsSet(prefix + ".ping_timeout") {
config.PingTimeout = viper.GetDuration(prefix + ".ping_timeout")
// ConfigureConnectionPool 配置数据库连接池 }
// 根据提供的配置参数设置GORM数据库的连接池属性 if viper.IsSet(prefix + ".health_check_interval") {
func ConfigureConnectionPool(db *gorm.DB, config *DatabaseConfig) error { config.HealthCheckInterval = viper.GetDuration(prefix + ".health_check_interval")
sqlDB, err := db.DB() }
if err != nil {
return fmt.Errorf("获取底层数据库连接失败: %w", err) return config
} }
// 设置连接池参数 // ConfigureConnectionPool 配置数据库连接池
sqlDB.SetMaxIdleConns(config.MaxIdleConns) // 根据提供的配置参数设置GORM数据库的连接池属性
sqlDB.SetMaxOpenConns(config.MaxOpenConns) func ConfigureConnectionPool(db *gorm.DB, config *DatabaseConfig) error {
sqlDB.SetConnMaxLifetime(config.ConnMaxLifetime) sqlDB, err := db.DB()
sqlDB.SetConnMaxIdleTime(config.ConnMaxIdleTime) if err != nil {
return fmt.Errorf("获取底层数据库连接失败: %w", err)
// LogInfo("数据库连接池配置完成", map[string]interface{}{ }
// "max_idle_conns": config.MaxIdleConns,
// "max_open_conns": config.MaxOpenConns, // 设置连接池参数
// "conn_max_lifetime": config.ConnMaxLifetime, sqlDB.SetMaxIdleConns(config.MaxIdleConns)
// "conn_max_idle_time": config.ConnMaxIdleTime, sqlDB.SetMaxOpenConns(config.MaxOpenConns)
// }) sqlDB.SetConnMaxLifetime(config.ConnMaxLifetime)
sqlDB.SetConnMaxIdleTime(config.ConnMaxIdleTime)
return nil
} // LogInfo("数据库连接池配置完成", map[string]interface{}{
// "max_idle_conns": config.MaxIdleConns,
// PingDatabase 检查数据库连接健康状态 // "max_open_conns": config.MaxOpenConns,
// 使用指定的超时时间ping数据库以验证连接是否正常 // "conn_max_lifetime": config.ConnMaxLifetime,
func PingDatabase(db *gorm.DB, timeout time.Duration) error { // "conn_max_idle_time": config.ConnMaxIdleTime,
sqlDB, err := db.DB() // })
if err != nil {
return fmt.Errorf("获取底层数据库连接失败: %w", err) return nil
} }
ctx, cancel := context.WithTimeout(context.Background(), timeout) // PingDatabase 检查数据库连接健康状态
defer cancel() // 使用指定的超时时间ping数据库以验证连接是否正常
func PingDatabase(db *gorm.DB, timeout time.Duration) error {
return sqlDB.PingContext(ctx) sqlDB, err := db.DB()
} if err != nil {
return fmt.Errorf("获取底层数据库连接失败: %w", err)
// GetConnectionStats 获取数据库连接池统计信息 }
// 返回当前数据库连接池的详细统计数据,包括连接数、等待时间等
func GetConnectionStats(db *gorm.DB) (*sql.DBStats, error) { ctx, cancel := context.WithTimeout(context.Background(), timeout)
sqlDB, err := db.DB() defer cancel()
if err != nil {
return nil, fmt.Errorf("获取底层数据库连接失败: %w", err) return sqlDB.PingContext(ctx)
} }
stats := sqlDB.Stats() // GetConnectionStats 获取数据库连接池统计信息
return &stats, nil // 返回当前数据库连接池的详细统计数据,包括连接数、等待时间等
} func GetConnectionStats(db *gorm.DB) (*sql.DBStats, error) {
sqlDB, err := db.DB()
// LogConnectionStats 记录数据库连接池统计信息 if err != nil {
// 获取并记录数据库连接池的统计信息到日志中,用于监控和调试 return nil, fmt.Errorf("获取底层数据库连接失败: %w", err)
func LogConnectionStats(db *gorm.DB) { }
stats, err := GetConnectionStats(db)
if err != nil { stats := sqlDB.Stats()
LogError("获取数据库连接池统计信息失败", err, nil) return &stats, nil
return }
}
// LogConnectionStats 记录数据库连接池统计信息
LogInfo("数据库连接池统计", map[string]interface{}{ // 获取并记录数据库连接池统计信息到日志中,用于监控和调试
"open_connections": stats.OpenConnections, func LogConnectionStats(db *gorm.DB) {
"in_use": stats.InUse, stats, err := GetConnectionStats(db)
"idle": stats.Idle, if err != nil {
"wait_count": stats.WaitCount, LogError("获取数据库连接池统计信息失败", err, nil)
"wait_duration": stats.WaitDuration, return
"max_idle_closed": stats.MaxIdleClosed, }
"max_idle_time_closed": stats.MaxIdleTimeClosed,
"max_lifetime_closed": stats.MaxLifetimeClosed, LogInfo("数据库连接池统计", map[string]interface{}{
}) "open_connections": stats.OpenConnections,
} "in_use": stats.InUse,
"idle": stats.Idle,
// StartHealthCheck 启动数据库健康检查 "wait_count": stats.WaitCount,
// 启动一个后台goroutine定期检查数据库连接健康状态 "wait_duration": stats.WaitDuration,
// 只在健康检查失败时输出错误日志,正常情况下不输出日志 "max_idle_closed": stats.MaxIdleClosed,
func StartHealthCheck(db *gorm.DB, config *DatabaseConfig) { "max_idle_time_closed": stats.MaxIdleTimeClosed,
go func() { "max_lifetime_closed": stats.MaxLifetimeClosed,
ticker := time.NewTicker(config.HealthCheckInterval) })
defer ticker.Stop() }
for range ticker.C { // StartHealthCheck 启动数据库健康检查
if err := PingDatabase(db, config.PingTimeout); err != nil { // 启动一个后台goroutine定期检查数据库连接健康状态
// 只在健康检查失败时输出错误日志 // 只在健康检查失败时输出错误日志,正常情况下不输出日志
LogError("数据库健康检查失败", err, map[string]interface{}{ func StartHealthCheck(db *gorm.DB, config *DatabaseConfig) {
"ping_timeout": config.PingTimeout, go func() {
}) ticker := time.NewTicker(config.HealthCheckInterval)
} defer ticker.Stop()
// 记录连接池统计信息(仅在调试模式下) for range ticker.C {
if logrus.GetLevel() == logrus.DebugLevel { if err := PingDatabase(db, config.PingTimeout); err != nil {
LogConnectionStats(db) // 只在健康检查失败时输出错误日志
} LogError("数据库健康检查失败", err, map[string]interface{}{
} "ping_timeout": config.PingTimeout,
}() })
}
// LogInfo("数据库健康检查已启动", map[string]interface{}{
// "check_interval": config.HealthCheckInterval, // 记录连接池统计信息(仅在调试模式下)
// "ping_timeout": config.PingTimeout, if logrus.GetLevel() == logrus.DebugLevel {
// }) LogConnectionStats(db)
} }
}
// ValidateDatabaseConfig 验证数据库配置参数 }()
// 检查数据库配置参数的有效性,确保所有参数都在合理范围内
func ValidateDatabaseConfig(config *DatabaseConfig) error { // LogInfo("数据库健康检查已启动", map[string]interface{}{
if config.MaxIdleConns < 0 { // "check_interval": config.HealthCheckInterval,
return fmt.Errorf("最大空闲连接数不能为负数: %d", config.MaxIdleConns) // "ping_timeout": config.PingTimeout,
} // })
if config.MaxOpenConns < 0 { }
return fmt.Errorf("最大打开连接数不能为负数: %d", config.MaxOpenConns)
} // ValidateDatabaseConfig 验证数据库配置参数
if config.MaxIdleConns > config.MaxOpenConns && config.MaxOpenConns > 0 { // 检查数据库配置参数的有效性,确保所有参数都在合理范围内
return fmt.Errorf("最大空闲连接数(%d)不能大于最大打开连接数(%d)", config.MaxIdleConns, config.MaxOpenConns) func ValidateDatabaseConfig(config *DatabaseConfig) error {
} if config.MaxIdleConns < 0 {
if config.ConnMaxLifetime < 0 { return fmt.Errorf("最大空闲连接数不能为负数: %d", config.MaxIdleConns)
return fmt.Errorf("连接最大生存时间不能为负数: %v", config.ConnMaxLifetime) }
} if config.MaxOpenConns < 0 {
if config.ConnMaxIdleTime < 0 { return fmt.Errorf("最大打开连接数不能为负数: %d", config.MaxOpenConns)
return fmt.Errorf("连接最大空闲时间不能为负数: %v", config.ConnMaxIdleTime) }
} if config.MaxIdleConns > config.MaxOpenConns && config.MaxOpenConns > 0 {
if config.PingTimeout <= 0 { return fmt.Errorf("最大空闲连接数(%d)不能大于最大打开连接数(%d)", config.MaxIdleConns, config.MaxOpenConns)
return fmt.Errorf("Ping超时时间必须大于0: %v", config.PingTimeout) }
} if config.ConnMaxLifetime < 0 {
if config.HealthCheckInterval <= 0 { return fmt.Errorf("连接最大生存时间不能为负数: %v", config.ConnMaxLifetime)
return fmt.Errorf("健康检查间隔必须大于0: %v", config.HealthCheckInterval) }
} if config.ConnMaxIdleTime < 0 {
return fmt.Errorf("连接最大空闲时间不能为负数: %v", config.ConnMaxIdleTime)
return nil }
} if config.PingTimeout <= 0 {
return fmt.Errorf("Ping超时时间必须大于0: %v", config.PingTimeout)
var ( }
// redisClient 全局Redis客户端 if config.HealthCheckInterval <= 0 {
redisClient *redis.Client return fmt.Errorf("健康检查间隔必须大于0: %v", config.HealthCheckInterval)
// redisOnce 确保只初始化一次 }
redisOnce sync.Once
// redisAvailable 标记Redis是否可用 return nil
redisAvailable bool }
)
// ============================================================================
// InitRedis 初始化Redis客户端仅在配置存在时尝试连接 // 全局变量
// - 从 viper 读取 security.redis.* 配置 // ============================================================================
// - 如果连接失败,则标记为不可用,不影响主流程
func InitRedis() { var (
redisOnce.Do(func() { // redisClient 全局Redis客户端
host := viper.GetString("redis.host") redisClient *redis.Client
port := viper.GetInt("redis.port") // redisOnce 确保只初始化一次
if host == "" || port == 0 { redisOnce sync.Once
logrus.Info("未配置Redis或配置不完整跳过初始化") // redisAvailable 标记Redis是否可用
redisAvailable = false redisAvailable bool
return )
}
addr := fmt.Sprintf("%s:%d", host, port) // ============================================================================
redisClient = redis.NewClient(&redis.Options{ // Redis函数
Addr: addr, // ============================================================================
Password: viper.GetString("redis.password"),
DB: viper.GetInt("redis.db"), // InitRedis 初始化Redis客户端仅在配置存在时尝试连接
}) // - 从 viper 读取 security.redis.* 配置
// 健康检查 // - 如果连接失败,则标记为不可用,不影响主流程
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) func InitRedis() {
defer cancel() redisOnce.Do(func() {
if err := redisClient.Ping(ctx).Err(); err != nil { host := viper.GetString("redis.host")
logrus.WithError(err).Warn("Redis初始化失败,标记为不可用") port := viper.GetInt("redis.port")
redisAvailable = false if host == "" || port == 0 {
return logrus.Info("未配置Redis或配置不完整跳过初始化")
} redisAvailable = false
redisAvailable = true return
logrus.WithField("addr", addr).Info("Redis 连接已建立") }
}) addr := fmt.Sprintf("%s:%d", host, port)
} redisClient = redis.NewClient(&redis.Options{
Addr: addr,
// GetRedis 获取全局Redis客户端可能返回nil当不可用时 Password: viper.GetString("redis.password"),
func GetRedis() *redis.Client { DB: viper.GetInt("redis.db"),
if redisClient == nil { })
InitRedis() // 健康检查
} ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
if !redisAvailable { defer cancel()
return nil if err := redisClient.Ping(ctx).Err(); err != nil {
} logrus.WithError(err).Warn("Redis初始化失败标记为不可用")
return redisClient redisAvailable = false
} return
}
// IsRedisAvailable 判断Redis是否可用 redisAvailable = true
func IsRedisAvailable() bool { logrus.WithField("addr", addr).Info("Redis 连接已建立")
if redisClient == nil { })
InitRedis() }
}
return redisAvailable // GetRedis 获取全局Redis客户端可能返回nil当不可用时
} func GetRedis() *redis.Client {
if redisClient == nil {
// RedisGetOrSet 通用Redis缓存获取或设置函数基于JSON序列化 InitRedis()
// - ctx: 上下文 }
// - key: 缓存键 if !redisAvailable {
// - ttl: 过期时间 return nil
// - loader: 当缓存不存在时的加载函数(一般执行数据库查询) }
// 返回:目标对象指针和错误 return redisClient
func RedisGetOrSet[T any](ctx context.Context, key string, ttl time.Duration, loader func() (*T, error)) (*T, error) { }
// 如果Redis不可用则直接调用加载函数
if !IsRedisAvailable() { // IsRedisAvailable 判断Redis是否可用
return loader() func IsRedisAvailable() bool {
} if redisClient == nil {
client := GetRedis() InitRedis()
if client == nil { }
return loader() return redisAvailable
} }
// 先尝试从缓存读取 // RedisGetOrSet 通用Redis缓存获取或设置函数基于JSON序列化
data, err := client.Get(ctx, key).Bytes() // - ctx: 上下文
if err == nil { // - key: 缓存键
var out T // - ttl: 过期时间
if uerr := json.Unmarshal(data, &out); uerr == nil { // - loader: 当缓存不存在时的加载函数(一般执行数据库查询)
return &out, nil // 返回:目标对象指针和错误
} func RedisGetOrSet[T any](ctx context.Context, key string, ttl time.Duration, loader func() (*T, error)) (*T, error) {
// 反序列化失败时视为未命中,继续加载 // 如果Redis不可用则直接调用加载函数
logrus.WithError(err).WithField("key", key).Warn("Redis缓存反序列化失败回退到loader") if !IsRedisAvailable() {
} else if err != redis.Nil { return loader()
// 非空且非不存在的错误,记录告警但不中断 }
logrus.WithError(err).WithField("key", key).Warn("读取Redis缓存失败") client := GetRedis()
} if client == nil {
return loader()
// 加载数据 }
val, lerr := loader()
if lerr != nil { // 先尝试从缓存读取
return nil, lerr data, err := client.Get(ctx, key).Bytes()
} if err == nil {
if val == nil { var out T
return nil, nil if uerr := json.Unmarshal(data, &out); uerr == nil {
} return &out, nil
}
// 写回缓存(错误不影响主流程) // 反序列化失败时视为未命中,继续加载
if b, merr := json.Marshal(val); merr == nil { logrus.WithError(err).WithField("key", key).Warn("Redis缓存反序列化失败回退到loader")
if serr := client.Set(ctx, key, b, ttl).Err(); serr != nil { } else if err != redis.Nil {
logrus.WithError(serr).WithField("key", key).Warn("写入Redis缓存失败") // 非空且非不存在的错误,记录告警但不中断
} logrus.WithError(err).WithField("key", key).Warn("读取Redis缓存失败")
} }
return val, nil
} // 加载数据
val, lerr := loader()
// RedisDel 删除一个或多个Redis键当Redis不可用时静默返回 if lerr != nil {
// - ctx: 上下文 return nil, lerr
// - keys: 需要删除的键名 }
func RedisDel(ctx context.Context, keys ...string) error { if val == nil {
// 如果Redis不可用则直接返回 return nil, nil
if !IsRedisAvailable() { }
return nil
} // 写回缓存(错误不影响主流程)
client := GetRedis() if b, merr := json.Marshal(val); merr == nil {
if client == nil { if serr := client.Set(ctx, key, b, ttl).Err(); serr != nil {
return nil logrus.WithError(serr).WithField("key", key).Warn("写入Redis缓存失败")
} }
if len(keys) == 0 { }
return nil return val, nil
} }
if _, err := client.Del(ctx, keys...).Result(); err != nil {
logrus.WithError(err).WithField("keys", keys).Warn("删除Redis键失败") // RedisDel 删除一个或多个Redis键当Redis不可用时静默返回
return err // - ctx: 上下文
} // - keys: 需要删除的键名
return nil 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
}

View File

@@ -8,12 +8,20 @@ import (
"strings" "strings"
) )
// ============================================================================
// 结构体定义
// ============================================================================
// EasyEncrypt 易加密算法结构体 // EasyEncrypt 易加密算法结构体
type EasyEncrypt struct { type EasyEncrypt struct {
encryptKey []int // 加密密钥 encryptKey []int // 加密密钥
decryptKey []int // 解密密钥 decryptKey []int // 解密密钥
} }
// ============================================================================
// 构造函数
// ============================================================================
// NewEasyEncrypt 创建新的易加密实例 // NewEasyEncrypt 创建新的易加密实例
func NewEasyEncrypt(encryptKey, decryptKey []int) *EasyEncrypt { func NewEasyEncrypt(encryptKey, decryptKey []int) *EasyEncrypt {
return &EasyEncrypt{ return &EasyEncrypt{
@@ -22,17 +30,21 @@ func NewEasyEncrypt(encryptKey, decryptKey []int) *EasyEncrypt {
} }
} }
// ============================================================================
// 密钥生成函数
// ============================================================================
// GenerateEasyKey 生成易加密密钥对 // GenerateEasyKey 生成易加密密钥对
func GenerateEasyKey() ([]int, []int, error) { func GenerateEasyKey() ([]int, []int, error) {
// 使用crypto/rand生成随机长度15-30位 // 使用crypto/rand生成随机长度15-30位
var lengthByte [1]byte var lengthByte [1]byte
// 生成加密密钥长度 // 生成加密密钥长度
if _, err := rand.Read(lengthByte[:]); err != nil { if _, err := rand.Read(lengthByte[:]); err != nil {
return nil, nil, err return nil, nil, err
} }
encryptKeyLen := 15 + int(lengthByte[0])%16 // 15-30位随机长度 encryptKeyLen := 15 + int(lengthByte[0])%16 // 15-30位随机长度
encryptKey := make([]int, encryptKeyLen) encryptKey := make([]int, encryptKeyLen)
encryptBytes := make([]byte, encryptKeyLen) encryptBytes := make([]byte, encryptKeyLen)
if _, err := rand.Read(encryptBytes); err != nil { if _, err := rand.Read(encryptBytes); err != nil {
@@ -47,7 +59,7 @@ func GenerateEasyKey() ([]int, []int, error) {
return nil, nil, err return nil, nil, err
} }
decryptKeyLen := 15 + int(lengthByte[0])%16 // 15-30位随机长度 decryptKeyLen := 15 + int(lengthByte[0])%16 // 15-30位随机长度
decryptKey := make([]int, decryptKeyLen) decryptKey := make([]int, decryptKeyLen)
decryptBytes := make([]byte, decryptKeyLen) decryptBytes := make([]byte, decryptKeyLen)
if _, err := rand.Read(decryptBytes); err != nil { if _, err := rand.Read(decryptBytes); err != nil {
@@ -60,6 +72,10 @@ func GenerateEasyKey() ([]int, []int, error) {
return encryptKey, decryptKey, nil return encryptKey, decryptKey, nil
} }
// ============================================================================
// 方法函数
// ============================================================================
// Encrypt 加密函数 - 对应 UserLogin_encrypt_Up_42510 // Encrypt 加密函数 - 对应 UserLogin_encrypt_Up_42510
func (e *EasyEncrypt) Encrypt(input string) string { func (e *EasyEncrypt) Encrypt(input string) string {
if input == "" { if input == "" {
@@ -140,6 +156,10 @@ func (e *EasyEncrypt) Decrypt(input string) string {
return result.String() return result.String()
} }
// ============================================================================
// 工具函数
// ============================================================================
// EncryptWithKey 使用指定密钥加密 // EncryptWithKey 使用指定密钥加密
func EncryptWithKey(input string, key []int) string { func EncryptWithKey(input string, key []int) string {
if input == "" || len(key) == 0 { if input == "" || len(key) == 0 {

View File

@@ -11,14 +11,18 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
// ============================================================================
// 结构体定义
// ============================================================================
// ErrorResponse 统一的错误响应结构 // ErrorResponse 统一的错误响应结构
// 用于标准化API错误响应格式 // 用于标准化API错误响应格式
type ErrorResponse struct { type ErrorResponse struct {
Success bool `json:"success"` // 请求是否成功错误响应时固定为false Success bool `json:"success"` // 请求是否成功错误响应时固定为false
Message string `json:"message"` // 错误消息描述 Message string `json:"message"` // 错误消息描述
ErrorCode string `json:"error_code,omitempty"` // 错误代码,用于客户端识别错误类型 ErrorCode string `json:"error_code,omitempty"` // 错误代码,用于客户端识别错误类型
Data interface{} `json:"data"` // 附加数据,可为空 Data interface{} `json:"data"` // 附加数据,可为空
Timestamp int64 `json:"timestamp"` // 响应时间戳 Timestamp int64 `json:"timestamp"` // 响应时间戳
} }
// SuccessResponse 统一的成功响应结构 // SuccessResponse 统一的成功响应结构
@@ -26,10 +30,14 @@ type ErrorResponse struct {
type SuccessResponse struct { type SuccessResponse struct {
Success bool `json:"success"` // 请求是否成功成功响应时固定为true Success bool `json:"success"` // 请求是否成功成功响应时固定为true
Message string `json:"message"` // 成功消息描述 Message string `json:"message"` // 成功消息描述
Data interface{} `json:"data"` // 响应数据 Data interface{} `json:"data"` // 响应数据
Timestamp int64 `json:"timestamp"` // 响应时间戳 Timestamp int64 `json:"timestamp"` // 响应时间戳
} }
// ============================================================================
// 常量定义
// ============================================================================
// ErrorCode 错误代码常量 // ErrorCode 错误代码常量
// 定义标准化的错误代码,用于客户端识别和处理不同类型的错误 // 定义标准化的错误代码,用于客户端识别和处理不同类型的错误
const ( const (
@@ -59,15 +67,19 @@ const (
// LogEntry 日志条目结构 // LogEntry 日志条目结构
// 包含完整的日志信息,用于结构化日志记录 // 包含完整的日志信息,用于结构化日志记录
type LogEntry struct { type LogEntry struct {
Level LogLevel `json:"level"` // 日志级别 Level LogLevel `json:"level"` // 日志级别
Message string `json:"message"` // 日志消息 Message string `json:"message"` // 日志消息
Error string `json:"error,omitempty"` // 错误信息,仅在错误日志中存在 Error string `json:"error,omitempty"` // 错误信息,仅在错误日志中存在
Context interface{} `json:"context,omitempty"` // 上下文信息,额外的结构化数据 Context interface{} `json:"context,omitempty"` // 上下文信息,额外的结构化数据
Timestamp time.Time `json:"timestamp"` // 日志时间戳 Timestamp time.Time `json:"timestamp"` // 日志时间戳
File string `json:"file"` // 源文件路径 File string `json:"file"` // 源文件路径
Line int `json:"line"` // 源文件行号 Line int `json:"line"` // 源文件行号
} }
// ============================================================================
// 响应函数
// ============================================================================
// WriteErrorResponse 写入错误响应 // WriteErrorResponse 写入错误响应
// c: Gin上下文 // c: Gin上下文
// statusCode: HTTP状态码 // statusCode: HTTP状态码
@@ -102,6 +114,10 @@ func WriteSuccessResponse(c *gin.Context, statusCode int, message string, data i
c.JSON(statusCode, response) c.JSON(statusCode, response)
} }
// ============================================================================
// 错误处理函数
// ============================================================================
// HandleDatabaseError 处理数据库错误 // HandleDatabaseError 处理数据库错误
// c: Gin上下文 // c: Gin上下文
// err: 数据库错误 // err: 数据库错误
@@ -152,6 +168,10 @@ func HandleInternalError(c *gin.Context, err error, operation string) {
WriteErrorResponse(c, 500, "服务器内部错误", ErrCodeInternalError, nil) WriteErrorResponse(c, 500, "服务器内部错误", ErrCodeInternalError, nil)
} }
// ============================================================================
// 日志函数
// ============================================================================
// LogInfo 记录信息日志 // LogInfo 记录信息日志
// message: 日志消息 // message: 日志消息
// context: 上下文信息 // context: 上下文信息
@@ -189,6 +209,10 @@ func LogDebug(message string, context interface{}) {
printLog(logEntry) printLog(logEntry)
} }
// ============================================================================
// 私有函数
// ============================================================================
// createLogEntry 创建日志条目 // createLogEntry 创建日志条目
// level: 日志级别 // level: 日志级别
// message: 日志消息 // message: 日志消息
@@ -252,4 +276,4 @@ func getLevelString(level LogLevel) string {
default: default:
return "UNKNOWN" return "UNKNOWN"
} }
} }

View File

@@ -4,12 +4,20 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// ============================================================================
// 结构体定义
// ============================================================================
// Logger 日志工具结构体 // Logger 日志工具结构体
// 封装logrus.Logger提供统一的日志接口 // 封装logrus.Logger提供统一的日志接口
type Logger struct { type Logger struct {
*log.Logger // 嵌入logrus.Logger继承其所有方法 *log.Logger // 嵌入logrus.Logger继承其所有方法
} }
// ============================================================================
// 构造函数
// ============================================================================
// NewLogger 创建新的日志实例使用全局logrus配置 // NewLogger 创建新的日志实例使用全局logrus配置
// 返回: 新的Logger实例 // 返回: 新的Logger实例
func NewLogger() *Logger { func NewLogger() *Logger {
@@ -32,6 +40,10 @@ func InitLogger() *Logger {
return logger return logger
} }
// ============================================================================
// 方法函数
// ============================================================================
// WithFields 添加字段到日志条目 // WithFields 添加字段到日志条目
// fields: 要添加的字段映射 // fields: 要添加的字段映射
// 返回: 包含字段的日志条目 // 返回: 包含字段的日志条目
@@ -89,6 +101,10 @@ func (l *Logger) LogError(err error, msg string) {
l.WithError(err).Error(msg) l.WithError(err).Error(msg)
} }
// ============================================================================
// 全局变量
// ============================================================================
// GlobalLogger 全局日志实例 // GlobalLogger 全局日志实例
// 提供全局访问的日志记录器 // 提供全局访问的日志记录器
var GlobalLogger *Logger var GlobalLogger *Logger
@@ -99,6 +115,10 @@ func init() {
GlobalLogger = NewLogger() GlobalLogger = NewLogger()
} }
// ============================================================================
// 全局函数
// ============================================================================
// GetLogger 获取全局日志实例 // GetLogger 获取全局日志实例
// 返回: 全局Logger实例 // 返回: 全局Logger实例
func GetLogger() *Logger { func GetLogger() *Logger {
@@ -109,4 +129,4 @@ func GetLogger() *Logger {
// logger: 要设置的Logger实例 // logger: 要设置的Logger实例
func SetGlobalLogger(logger *Logger) { func SetGlobalLogger(logger *Logger) {
GlobalLogger = logger GlobalLogger = logger
} }

View File

@@ -23,4 +23,4 @@ func (l *Logger) LogServerStop() {
// configFile: 配置文件路径 // configFile: 配置文件路径
func (l *Logger) LogConfigLoad(configFile string) { func (l *Logger) LogConfigLoad(configFile string) {
l.WithField("config_file", configFile).Info("配置文件加载") l.WithField("config_file", configFile).Info("配置文件加载")
} }

View File

@@ -5,9 +5,17 @@ import (
"time" "time"
) )
// ============================================================================
// 全局变量
// ============================================================================
// serverStartTime 记录进程启动时间(近似服务器启动时间) // serverStartTime 记录进程启动时间(近似服务器启动时间)
var serverStartTime = time.Now() var serverStartTime = time.Now()
// ============================================================================
// 公共函数
// ============================================================================
// GetServerStartTime 获取服务器启动时间 // GetServerStartTime 获取服务器启动时间
// 返回: 服务器启动的时间戳 // 返回: 服务器启动的时间戳
func GetServerStartTime() time.Time { func GetServerStartTime() time.Time {

View File

@@ -1,73 +1,73 @@
package web package web
import ( import (
"embed" "embed"
"html/template" "html/template"
"io/fs" "io/fs"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
// TemplatesFS 嵌入模板的文件系统 // TemplatesFS 嵌入模板的文件系统
// //
//go:embed template/*.html template/admin/*.html //go:embed template/*.html template/admin/*.html
var templatesFS embed.FS var templatesFS embed.FS
// StaticFS 嵌入静态资源的文件系统(包含 CSS/JS 的 static 与 图片/字体等资源的 assets // StaticFS 嵌入静态资源的文件系统(包含 CSS/JS 的 static 与 图片/字体等资源的 assets
// //
//go:embed static/* assets/* //go:embed static/* assets/*
var staticFS embed.FS var staticFS embed.FS
// getDistRootFS 获取基于 server.dist 的本地文件系统 // getDistRootFS 获取基于 server.dist 的本地文件系统
// 当 server.dist 非空且路径存在时,返回对应的本地只读 FS否则返回 nil // 当 server.dist 非空且路径存在时,返回对应的本地只读 FS否则返回 nil
func getDistRootFS() fs.FS { func getDistRootFS() fs.FS {
// 从配置中读取 server.dist // 从配置中读取 server.dist
distPath := viper.GetString("server.dist") distPath := viper.GetString("server.dist")
if distPath == "" { if distPath == "" {
return nil return nil
} }
// 归一化路径,兼容相对/绝对 // 归一化路径,兼容相对/绝对
absPath := distPath absPath := distPath
if !filepath.IsAbs(distPath) { if !filepath.IsAbs(distPath) {
if p, err := filepath.Abs(distPath); err == nil { if p, err := filepath.Abs(distPath); err == nil {
absPath = p absPath = p
} }
} }
// 检查目录是否存在 // 检查目录是否存在
if info, err := os.Stat(absPath); err == nil && info.IsDir() { if info, err := os.Stat(absPath); err == nil && info.IsDir() {
return os.DirFS(absPath) return os.DirFS(absPath)
} }
log.Printf("server.dist 路径无效或不可访问:%s将回退使用嵌入资源", distPath) log.Printf("server.dist 路径无效或不可访问:%s将回退使用嵌入资源", distPath)
return nil return nil
} }
// ParseTemplates 解析模板 // ParseTemplates 解析模板
// 优先从 server.dist 指定目录加载(当配置非空且有效),否则回退到嵌入模板 // 优先从 server.dist 指定目录加载(当配置非空且有效),否则回退到嵌入模板
func ParseTemplates() (*template.Template, error) { // Go 顶级函数不支持箭头写法 func ParseTemplates() (*template.Template, error) { // Go 顶级函数不支持箭头写法
if distFS := getDistRootFS(); distFS != nil { if distFS := getDistRootFS(); distFS != nil {
// 期望 dist 目录下存在 template 与 template/admin 结构 // 期望 dist 目录下存在 template 与 template/admin 结构
// 如:{dist}/template/*.html 与 {dist}/template/admin/*.html // 如:{dist}/template/*.html 与 {dist}/template/admin/*.html
return template.ParseFS(distFS, "template/*.html", "template/admin/*.html") return template.ParseFS(distFS, "template/*.html", "template/admin/*.html")
} }
// 默认:使用嵌入模板 // 默认:使用嵌入模板
return template.ParseFS(templatesFS, "template/*.html", "template/admin/*.html") return template.ParseFS(templatesFS, "template/*.html", "template/admin/*.html")
} }
// GetStaticFS 返回静态资源文件系统(包含 static 与 assets 目录) // GetStaticFS 返回静态资源文件系统(包含 static 与 assets 目录)
// 优先使用 server.dist 指定的本地目录;否则回退到嵌入静态资源 // 优先使用 server.dist 指定的本地目录;否则回退到嵌入静态资源
func GetStaticFS() (fs.FS, error) { // Go 顶级函数不支持箭头写法 func GetStaticFS() (fs.FS, error) { // Go 顶级函数不支持箭头写法
if distFS := getDistRootFS(); distFS != nil { if distFS := getDistRootFS(); distFS != nil {
// 直接返回以 dist 根为起点的 FSroutes 中会再基于此 FS Sub 出 static 与 assets // 直接返回以 dist 根为起点的 FSroutes 中会再基于此 FS Sub 出 static 与 assets
return distFS, nil return distFS, nil
} }
return staticFS, nil return staticFS, nil
} }
// IsDevMode 检查是否为开发模式 // IsDevMode 检查是否为开发模式
// 注意:这个函数保留用于向后兼容,建议使用 middleware.IsDevMode() // 注意:这个函数保留用于向后兼容,建议使用 middleware.IsDevMode()
func IsDevMode() bool { func IsDevMode() bool {
return viper.GetBool("server.dev_mode") return viper.GetBool("server.dev_mode")
} }