更新底层架构

This commit is contained in:
2026-03-18 21:51:17 +08:00
parent b69c6ccbca
commit 68bea98b81
71 changed files with 5220 additions and 7619 deletions

View File

@@ -5,7 +5,6 @@ import (
"encoding/json"
"errors"
"io/fs"
"os"
"path/filepath"
log "github.com/sirupsen/logrus"
@@ -19,10 +18,11 @@ import (
// ServerConfig 服务器配置结构体
// 包含服务器运行相关的配置信息
type ServerConfig struct {
Host string `json:"host" mapstructure:"host"` // 服务器监听地址
Port int `json:"port" mapstructure:"port"` // 服务器监听端口
Dist string `json:"dist" mapstructure:"dist"` // 静态文件目录
DevMode bool `json:"dev_mode" mapstructure:"dev_mode"` // 开发模式(跳过验证码等)
Host string `json:"host" mapstructure:"host"` // 服务器监听地址
Port int `json:"port" mapstructure:"port"` // 服务器监听端口
Dist string `json:"dist" mapstructure:"dist"` // 静态文件目录
DevMode bool `json:"dev_mode" mapstructure:"dev_mode"` // 开发模式(跳过验证码等)
AccessLog bool `json:"access_log" mapstructure:"access_log"` // 是否输出访问日志
}
// DatabaseConfig 数据库配置结构体
@@ -71,31 +71,12 @@ type LogConfig struct {
MaxAge int `json:"max_age" mapstructure:"max_age"` // 日志文件保留天数
}
// CookieConfig Cookie配置结构体
// 包含Cookie相关的安全配置信息
type CookieConfig struct {
Secure bool `json:"secure" mapstructure:"secure"` // 是否只在HTTPS下发送Cookie
SameSite string `json:"same_site" mapstructure:"same_site"` // SameSite属性Strict/Lax/None
Domain string `json:"domain" mapstructure:"domain"` // Cookie域名
MaxAge int `json:"max_age" mapstructure:"max_age"` // Cookie最大存活时间
}
// SecurityConfig 安全配置结构体
// 包含应用程序安全相关的配置信息
type SecurityConfig struct {
JWTSecret string `json:"jwt_secret" mapstructure:"jwt_secret"` // JWT签名密钥
EncryptionKey string `json:"encryption_key" mapstructure:"encryption_key"` // 数据加密密钥
JWTRefresh int `json:"jwt_refresh" mapstructure:"jwt_refresh"` // JWT令牌刷新阈值小时
Cookie CookieConfig `json:"cookie" mapstructure:"cookie"` // Cookie配置
}
// AppConfig 应用配置结构体
type AppConfig struct {
Server ServerConfig `json:"server" mapstructure:"server"`
Database DatabaseConfig `json:"database" mapstructure:"database"`
Redis RedisConfig `json:"redis" mapstructure:"redis"`
Log LogConfig `json:"log" mapstructure:"log"`
Security SecurityConfig `json:"security" mapstructure:"security"`
}
// ============================================================================
@@ -106,10 +87,11 @@ type AppConfig struct {
func GetDefaultAppConfig() *AppConfig {
return &AppConfig{
Server: ServerConfig{
Host: "0.0.0.0",
Port: 8080,
Dist: "",
DevMode: false,
Host: "0.0.0.0",
Port: 8080,
Dist: "",
DevMode: false,
AccessLog: true,
},
Database: DatabaseConfig{
Type: "sqlite",
@@ -140,37 +122,9 @@ func GetDefaultAppConfig() *AppConfig {
MaxBackups: 5,
MaxAge: 30,
},
Security: SecurityConfig{
JWTSecret: "",
EncryptionKey: "",
JWTRefresh: 6,
Cookie: CookieConfig{
Secure: true,
SameSite: "Lax",
Domain: "",
MaxAge: 86400,
},
},
}
}
// GetSecureDefaultAppConfig 获取带有安全密钥的默认应用配置
func GetSecureDefaultAppConfig() (*AppConfig, error) {
config := GetDefaultAppConfig()
// 生成安全密钥
jwtSecret, encryptionKey, err := GenerateSecureKeys()
if err != nil {
return nil, err
}
// 设置安全密钥
config.Security.JWTSecret = jwtSecret
config.Security.EncryptionKey = encryptionKey
return config, nil
}
// Init 初始化配置文件
func Init(cfgFilePath string) {
viper.SetConfigFile(cfgFilePath)
@@ -180,18 +134,10 @@ func Init(cfgFilePath string) {
if err := viper.ReadInConfig(); err != nil {
var pathError *fs.PathError
if errors.As(err, &pathError) {
log.Warn("未找到配置文件,使用默认配置")
log.Warn("未找到配置文件,使用默认配置在内存中运行(需通过安装页面初始化)")
// 生成带有安全密钥的默认配置
defaultConfig, configErr := GetSecureDefaultAppConfig()
if configErr != nil {
log.WithFields(
log.Fields{
"err": configErr,
},
).Error("生成安全配置失败,使用基础默认配置")
defaultConfig = GetDefaultAppConfig()
}
// 使用默认配置
defaultConfig := GetDefaultAppConfig()
// 将配置结构体转换为JSON
configBytes, marshalErr := json.MarshalIndent(defaultConfig, "", " ")
@@ -204,25 +150,7 @@ func Init(cfgFilePath string) {
return
}
// 写入配置文件
err = os.WriteFile(cfgFilePath, configBytes, 0o644)
if err != nil {
log.WithFields(
log.Fields{
"err": err,
},
).Error("写入默认配置文件失败")
} else {
// 只显示配置文件名,不显示完整路径
fileName := filepath.Base(cfgFilePath)
log.WithFields(
log.Fields{
"file": fileName,
},
).Info("写入默认配置文件成功(已生成安全密钥)")
}
// 将配置加载到viper中
// 将配置加载到viper中但不写入文件
err = viper.ReadConfig(bytes.NewBuffer(configBytes))
if err != nil {
log.WithFields(
@@ -231,8 +159,10 @@ func Init(cfgFilePath string) {
},
).Error("读取默认配置失败")
} else {
log.Info("已成功读取默认配置")
log.Info("已成功在内存中加载默认配置")
}
// 不在这里写入文件了,安装完成后通过 UpdateConfig 写入
} else {
log.WithFields(
log.Fields{
@@ -241,7 +171,7 @@ func Init(cfgFilePath string) {
).Fatal("配置文件解析错误")
}
}
// 只显示配置文件名,不显示完整路径
configFile := viper.ConfigFileUsed()
if configFile != "" {
@@ -266,24 +196,62 @@ func Init(cfgFilePath string) {
}
}
// CreateDefaultConfig 创建默认配置文件
func CreateDefaultConfig(filePath string) error {
// 生成带有安全密钥的默认配置
defaultConfig, err := GetSecureDefaultAppConfig()
if err != nil {
log.WithFields(
log.Fields{
"err": err,
},
).Error("生成安全配置失败,使用基础默认配置")
defaultConfig = GetDefaultAppConfig()
}
// 将配置结构体转换为JSON
configBytes, err := json.MarshalIndent(defaultConfig, "", " ")
if err != nil {
// UpdateConfig 更新配置文件
// 接收一个回调函数,在回调函数中修改配置对象,然后保存到文件
func UpdateConfig(updateFn func(*AppConfig)) error {
// 1. 获取当前配置
var currentConfig AppConfig
if err := viper.Unmarshal(&currentConfig); err != nil {
return err
}
return os.WriteFile(filePath, configBytes, 0o644)
// 2. 执行更新回调
updateFn(&currentConfig)
// 3. 将更新后的配置写回 Viper
// 注意:这里需要手动设置回 viper否则 viper.WriteConfig() 写入的还是旧配置
// 也可以直接序列化 currentConfig 写入文件
// 更新 Server 配置
viper.Set("server.host", currentConfig.Server.Host)
viper.Set("server.port", currentConfig.Server.Port)
viper.Set("server.dist", currentConfig.Server.Dist)
viper.Set("server.dev_mode", currentConfig.Server.DevMode)
viper.Set("server.access_log", currentConfig.Server.AccessLog)
// 更新 Database 配置
viper.Set("database.type", currentConfig.Database.Type)
viper.Set("database.mysql.host", currentConfig.Database.MySQL.Host)
viper.Set("database.mysql.port", currentConfig.Database.MySQL.Port)
viper.Set("database.mysql.username", currentConfig.Database.MySQL.Username)
viper.Set("database.mysql.password", currentConfig.Database.MySQL.Password)
viper.Set("database.mysql.database", currentConfig.Database.MySQL.Database)
viper.Set("database.mysql.charset", currentConfig.Database.MySQL.Charset)
viper.Set("database.mysql.max_idle_conns", currentConfig.Database.MySQL.MaxIdleConns)
viper.Set("database.mysql.max_open_conns", currentConfig.Database.MySQL.MaxOpenConns)
viper.Set("database.sqlite.path", currentConfig.Database.SQLite.Path)
// 更新 Redis 配置
viper.Set("redis.host", currentConfig.Redis.Host)
viper.Set("redis.port", currentConfig.Redis.Port)
viper.Set("redis.password", currentConfig.Redis.Password)
viper.Set("redis.db", currentConfig.Redis.DB)
// 更新 Log 配置
viper.Set("log.level", currentConfig.Log.Level)
viper.Set("log.file", currentConfig.Log.File)
viper.Set("log.max_size", currentConfig.Log.MaxSize)
viper.Set("log.max_backups", currentConfig.Log.MaxBackups)
viper.Set("log.max_age", currentConfig.Log.MaxAge)
// 4. 保存到文件
if err := viper.WriteConfig(); err != nil {
// 如果配置文件不存在(比如只用了默认配置没写文件),则尝试 SafeWriteConfig
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
return viper.SafeWriteConfig()
}
return err
}
return nil
}

View File

@@ -61,11 +61,6 @@ func validateConfig(config *AppConfig) error {
return fmt.Errorf("日志配置错误: %w", err)
}
// 验证安全配置
if err := validateSecurityConfig(&config.Security); err != nil {
return fmt.Errorf("安全配置错误: %w", err)
}
return nil
}
@@ -176,7 +171,6 @@ func validateLogConfig(config *LogConfig) error {
}
}
}
// 当日志文件路径为空时,不进行目录检查和创建
// 验证日志轮转配置
if config.MaxSize <= 0 {
@@ -192,32 +186,6 @@ func validateLogConfig(config *LogConfig) error {
return nil
}
// validateSecurityConfig 验证安全配置
func validateSecurityConfig(config *SecurityConfig) error {
if len(config.JWTSecret) < 16 {
return errors.New("JWT密钥长度不能少于16个字符")
}
if len(config.EncryptionKey) < 16 {
return errors.New("加密密钥长度不能少于16个字符")
}
if config.JWTRefresh < 1 || config.JWTRefresh > 23 {
return errors.New("JWT令牌刷新阈值必须在1-23小时之间")
}
// 检查是否使用默认值(生产环境警告)
if strings.Contains(config.JWTSecret, "default") {
log.Warn("检测到使用默认JWT密钥生产环境请更换为安全的密钥")
}
if strings.Contains(config.EncryptionKey, "default") {
log.Warn("检测到使用默认加密密钥,生产环境请更换为安全的密钥")
}
return nil
}
// contains 检查切片是否包含指定元素
func contains(slice []string, item string) bool {
for _, s := range slice {