Some checks failed
CI / Lint + Build (push) Has been cancelled
审计修复: - CSP: unsafe-inline移除, nonce替代; unsafe-eval保留供Vditor使用 - HSTS: 始终启用(不再依赖release模式) - Controller Exp: common.GetGinExp包装, 不再直接读context - UID解析: common.ParseUIDParam统一, follow/admin controller改用 重构: - router/api.go(198行)拆为7个域文件: auth/settings/posts/comments/reactions/social/studio - 管理后台站点设置: 单页拆为4个子页(brand/security/registration/content)+子菜单 - 前台用户设置页: 1201行拆为6个独立模板+6个独立JS文件 新增: - DB健康检测中间件(middleware/db_health.go): 5s ping+降级页面 - 时区配置: config.yaml server.timezone→time.Local初始化 - .gitea/workflows/ci.yml: strict模式CI流水线
179 lines
5.4 KiB
Go
179 lines
5.4 KiB
Go
// Package config 管理应用配置的加载、解析和运行时访问。
|
||
package config
|
||
|
||
import (
|
||
"bufio"
|
||
"log"
|
||
"os"
|
||
"strings"
|
||
|
||
"github.com/spf13/viper"
|
||
)
|
||
|
||
// Config 应用总配置
|
||
type Config struct {
|
||
Server ServerConfig `mapstructure:"server"`
|
||
Database DatabaseConfig `mapstructure:"database"`
|
||
Session SessionConfig `mapstructure:"session"`
|
||
Redis RedisConfig `mapstructure:"redis"`
|
||
Bcrypt BcryptConfig `mapstructure:"bcrypt"`
|
||
Roles RolesConfig `mapstructure:"roles"`
|
||
Audit AuditConfig `mapstructure:"audit"`
|
||
}
|
||
|
||
// AuditConfig 审核系统配置(config.yaml 提供默认值,站点设置可运行时覆盖)
|
||
type AuditConfig struct {
|
||
Enabled bool `mapstructure:"enabled"`
|
||
UsernameAudit bool `mapstructure:"username_audit"`
|
||
AvatarAudit bool `mapstructure:"avatar_audit"`
|
||
BioAudit bool `mapstructure:"bio_audit"`
|
||
}
|
||
|
||
type ServerConfig struct {
|
||
Port string `mapstructure:"port"`
|
||
Mode string `mapstructure:"mode"`
|
||
CookieSecure bool `mapstructure:"cookie_secure"`
|
||
Timezone string `mapstructure:"timezone"`
|
||
}
|
||
|
||
type DatabaseConfig struct {
|
||
Host string `mapstructure:"host"`
|
||
Port string `mapstructure:"port"`
|
||
User string `mapstructure:"user"`
|
||
Password string `mapstructure:"password"`
|
||
DBName string `mapstructure:"dbname"`
|
||
SSLMode string `mapstructure:"sslmode"`
|
||
}
|
||
|
||
// SessionConfig 服务端会话配置
|
||
type SessionConfig struct {
|
||
IdleTimeout int `mapstructure:"idle_timeout"` // 不记住我:空闲超时(分钟),默认 1440(24小时)
|
||
RememberTimeout int `mapstructure:"remember_timeout"` // 记住我:空闲超时(分钟),默认 43200(30天)
|
||
CleanupInterval int `mapstructure:"cleanup_interval"` // 后台清理间隔(秒),默认 300,仅内存模式使用
|
||
}
|
||
|
||
// RedisConfig Redis 连接配置
|
||
type RedisConfig struct {
|
||
Enabled bool `mapstructure:"enabled"`
|
||
Host string `mapstructure:"host"`
|
||
Port string `mapstructure:"port"`
|
||
Password string `mapstructure:"password"`
|
||
DB int `mapstructure:"db"`
|
||
}
|
||
|
||
type BcryptConfig struct {
|
||
Cost int `mapstructure:"cost"`
|
||
}
|
||
|
||
// RolesConfig 角色配置(可扩展,新增角色只需改 config.yaml)
|
||
type RolesConfig struct {
|
||
Levels map[string]int `mapstructure:"levels"`
|
||
Names map[string]string `mapstructure:"names"`
|
||
Permissions RolesPermissions `mapstructure:"permissions"`
|
||
}
|
||
|
||
// RolesPermissions 角色间操作权限
|
||
type RolesPermissions struct {
|
||
OperableRoles map[string][]string `mapstructure:"operable_roles"`
|
||
}
|
||
|
||
// App 全局配置实例(初始化后只读)。
|
||
var App *Config
|
||
|
||
// Load 加载配置:config.yaml → .env 覆盖
|
||
func Load(configPath string) *Config {
|
||
// 1. 从 .env 加载环境变量(优先于 config.yaml)
|
||
loadEnvFile(".env")
|
||
|
||
v := viper.New()
|
||
|
||
// 2. 读取 config.yaml
|
||
v.SetConfigFile(configPath)
|
||
v.SetConfigType("yaml")
|
||
if err := v.ReadInConfig(); err != nil {
|
||
log.Fatalf("读取配置文件失败: %v", err)
|
||
}
|
||
|
||
// 3. 环境变量覆盖(DATABASE_PASSWORD → database.password)
|
||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||
v.AutomaticEnv()
|
||
bindEnvOverride(v)
|
||
|
||
c := &Config{}
|
||
if err := v.Unmarshal(c); err != nil {
|
||
log.Fatalf("解析配置失败: %v", err)
|
||
}
|
||
|
||
log.Printf("[Config] Session.IdleTimeout=%d min Session.RememberTimeout=%d min Session.CleanupInterval=%d s | Server.Mode=%s",
|
||
c.Session.IdleTimeout, c.Session.RememberTimeout, c.Session.CleanupInterval, c.Server.Mode)
|
||
|
||
App = c
|
||
return c
|
||
}
|
||
|
||
// loadEnvFile 读取 .env 文件并设置环境变量(仅当环境变量未设置时)
|
||
func loadEnvFile(path string) {
|
||
f, err := os.Open(path) //nolint:gosec // path 为内部 .env 文件路径
|
||
if err != nil {
|
||
return // .env 不存在,跳过
|
||
}
|
||
defer func() { _ = f.Close() }()
|
||
|
||
scanner := bufio.NewScanner(f)
|
||
for scanner.Scan() {
|
||
line := strings.TrimSpace(scanner.Text())
|
||
if line == "" || strings.HasPrefix(line, "#") {
|
||
continue
|
||
}
|
||
parts := strings.SplitN(line, "=", 2)
|
||
if len(parts) != 2 {
|
||
continue
|
||
}
|
||
key := strings.TrimSpace(parts[0])
|
||
val := strings.TrimSpace(parts[1])
|
||
if os.Getenv(key) == "" {
|
||
_ = os.Setenv(key, val)
|
||
}
|
||
}
|
||
}
|
||
|
||
// bindEnvOverride 将环境变量映射到 config 的嵌套键(.env 覆盖 config.yaml 默认值)
|
||
func bindEnvOverride(v *viper.Viper) {
|
||
// 数据库
|
||
_ = v.BindEnv("database.host", "DATABASE_HOST")
|
||
_ = v.BindEnv("database.port", "DATABASE_PORT")
|
||
_ = v.BindEnv("database.user", "DATABASE_USER")
|
||
_ = v.BindEnv("database.password", "DATABASE_PASSWORD")
|
||
_ = v.BindEnv("database.dbname", "DATABASE_DBNAME")
|
||
_ = v.BindEnv("database.sslmode", "DATABASE_SSLMODE")
|
||
// Redis
|
||
_ = v.BindEnv("redis.enabled", "REDIS_ENABLED")
|
||
_ = v.BindEnv("redis.host", "REDIS_HOST")
|
||
_ = v.BindEnv("redis.port", "REDIS_PORT")
|
||
_ = v.BindEnv("redis.password", "REDIS_PASSWORD")
|
||
_ = v.BindEnv("redis.db", "REDIS_DB")
|
||
// 服务
|
||
_ = v.BindEnv("server.port", "SERVER_PORT")
|
||
_ = v.BindEnv("server.mode", "SERVER_MODE")
|
||
}
|
||
|
||
// GetIdleTimeout 返回临时会话空闲超时(分钟),实现 controller.sessionConfig 接口
|
||
func (c *Config) GetIdleTimeout() int {
|
||
return c.Session.IdleTimeout
|
||
}
|
||
|
||
// GetRememberTimeout 返回记住我会话空闲超时(分钟)
|
||
func (c *Config) GetRememberTimeout() int {
|
||
return c.Session.RememberTimeout
|
||
}
|
||
|
||
// DSN 返回 PostgreSQL 连接字符串
|
||
func (d DatabaseConfig) DSN() string {
|
||
return "host=" + d.Host +
|
||
" port=" + d.Port +
|
||
" user=" + d.User +
|
||
" password=" + d.Password +
|
||
" dbname=" + d.DBName +
|
||
" sslmode=" + d.SSLMode
|
||
}
|