221 lines
5.6 KiB
Go
221 lines
5.6 KiB
Go
package main
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"io/fs"
|
||
"log/slog"
|
||
"os"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/goccy/go-yaml"
|
||
)
|
||
|
||
type Config struct {
|
||
Server ServerConfig `yaml:"server"`
|
||
Log LogConfig `yaml:"log"`
|
||
Static StaticConfig `yaml:"static"`
|
||
API APIConfig `yaml:"api"`
|
||
Database DatabaseConfig `yaml:"database"`
|
||
}
|
||
|
||
type ServerConfig struct {
|
||
Host string `yaml:"host"`
|
||
Port int `yaml:"port"`
|
||
Mode string `yaml:"mode"`
|
||
}
|
||
|
||
type LogConfig struct {
|
||
Level string `yaml:"level"`
|
||
AccessLog bool `yaml:"access_log"`
|
||
}
|
||
|
||
type StaticConfig struct {
|
||
Dir string `yaml:"dir"`
|
||
}
|
||
|
||
type APIConfig struct {
|
||
Prefix string `yaml:"prefix"`
|
||
CORS CORSConfig `yaml:"cors"`
|
||
}
|
||
|
||
type CORSConfig struct {
|
||
Enabled bool `yaml:"enabled"`
|
||
AllowOrigins []string `yaml:"allow_origins"`
|
||
AllowMethods []string `yaml:"allow_methods"`
|
||
AllowHeaders []string `yaml:"allow_headers"`
|
||
AllowCredentials bool `yaml:"allow_credentials"`
|
||
MaxAge string `yaml:"max_age"`
|
||
}
|
||
|
||
type DatabaseConfig struct {
|
||
Driver string `yaml:"driver"` // sqlite3 / mysql
|
||
ConnectTimeout string `yaml:"connect_timeout"` // 建立连接超时
|
||
SQLite SQLiteConfig `yaml:"sqlite"`
|
||
MySQL MySQLConfig `yaml:"mysql"`
|
||
}
|
||
|
||
type SQLiteConfig struct {
|
||
Path string `yaml:"path"`
|
||
}
|
||
|
||
type MySQLConfig struct {
|
||
Host string `yaml:"host"`
|
||
Port int `yaml:"port"`
|
||
User string `yaml:"user"`
|
||
Password string `yaml:"password"`
|
||
Database string `yaml:"database"`
|
||
Charset string `yaml:"charset"`
|
||
MaxOpenConns int `yaml:"max_open_conns"`
|
||
MaxIdleConns int `yaml:"max_idle_conns"`
|
||
ConnMaxLifetime string `yaml:"conn_max_lifetime"`
|
||
}
|
||
|
||
func defaultConfig() *Config {
|
||
return &Config{
|
||
Server: ServerConfig{
|
||
Host: "0.0.0.0",
|
||
Port: 8080,
|
||
Mode: "debug",
|
||
},
|
||
Log: LogConfig{
|
||
Level: "info",
|
||
AccessLog: true,
|
||
},
|
||
Static: StaticConfig{
|
||
Dir: "./dist",
|
||
},
|
||
API: APIConfig{
|
||
Prefix: "/api",
|
||
CORS: CORSConfig{
|
||
Enabled: true,
|
||
AllowOrigins: []string{"http://localhost:5173"},
|
||
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
||
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"},
|
||
AllowCredentials: false,
|
||
MaxAge: "12h",
|
||
},
|
||
},
|
||
Database: DatabaseConfig{
|
||
Driver: "sqlite3",
|
||
ConnectTimeout: "10s",
|
||
SQLite: SQLiteConfig{
|
||
Path: "./data/rill.db",
|
||
},
|
||
MySQL: MySQLConfig{
|
||
Host: "127.0.0.1",
|
||
Port: 3306,
|
||
User: "root",
|
||
Password: "",
|
||
Database: "rill",
|
||
Charset: "utf8mb4",
|
||
MaxOpenConns: 10,
|
||
MaxIdleConns: 5,
|
||
ConnMaxLifetime: "1h",
|
||
},
|
||
},
|
||
}
|
||
}
|
||
|
||
// LoadConfig 读取 YAML 配置文件,未指定的配置项使用默认值;文件不存在时回退到默认配置。
|
||
func LoadConfig(path string) (*Config, error) {
|
||
cfg := defaultConfig()
|
||
|
||
data, err := os.ReadFile(path)
|
||
switch {
|
||
case err == nil:
|
||
if err := yaml.Unmarshal(data, cfg); err != nil {
|
||
return nil, fmt.Errorf("解析配置文件 %s 失败: %w", path, err)
|
||
}
|
||
case errors.Is(err, fs.ErrNotExist):
|
||
slog.Warn("配置文件不存在,使用默认配置", "path", path)
|
||
default:
|
||
return nil, fmt.Errorf("读取配置文件 %s 失败: %w", path, err)
|
||
}
|
||
|
||
if err := cfg.validate(); err != nil {
|
||
return nil, err
|
||
}
|
||
return cfg, nil
|
||
}
|
||
|
||
func (c *Config) validate() error {
|
||
switch c.Server.Mode {
|
||
case "debug", "release", "test":
|
||
default:
|
||
return fmt.Errorf("server.mode 无效: %q(可选: debug/release/test)", c.Server.Mode)
|
||
}
|
||
if c.Server.Port <= 0 || c.Server.Port > 65535 {
|
||
return fmt.Errorf("server.port 无效: %d", c.Server.Port)
|
||
}
|
||
if _, err := parseLogLevel(c.Log.Level); err != nil {
|
||
return err
|
||
}
|
||
if _, err := time.ParseDuration(c.API.CORS.MaxAge); err != nil {
|
||
return fmt.Errorf("api.cors.max_age 无效: %w", err)
|
||
}
|
||
if err := c.validateDatabase(); err != nil {
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (c *Config) validateDatabase() error {
|
||
if _, err := time.ParseDuration(c.Database.ConnectTimeout); err != nil {
|
||
return fmt.Errorf("database.connect_timeout 无效: %w", err)
|
||
}
|
||
|
||
switch c.Database.Driver {
|
||
case "sqlite3":
|
||
if strings.TrimSpace(c.Database.SQLite.Path) == "" {
|
||
return fmt.Errorf("database.sqlite.path 不能为空")
|
||
}
|
||
case "mysql":
|
||
db := c.Database.MySQL
|
||
if strings.TrimSpace(db.Host) == "" {
|
||
return fmt.Errorf("database.mysql.host 不能为空")
|
||
}
|
||
if db.Port <= 0 || db.Port > 65535 {
|
||
return fmt.Errorf("database.mysql.port 无效: %d", db.Port)
|
||
}
|
||
if strings.TrimSpace(db.Database) == "" {
|
||
return fmt.Errorf("database.mysql.database 不能为空")
|
||
}
|
||
if db.MaxOpenConns < 0 || db.MaxIdleConns < 0 {
|
||
return fmt.Errorf("database.mysql 连接池大小不能为负数")
|
||
}
|
||
if _, err := time.ParseDuration(db.ConnMaxLifetime); err != nil {
|
||
return fmt.Errorf("database.mysql.conn_max_lifetime 无效: %w", err)
|
||
}
|
||
default:
|
||
return fmt.Errorf("database.driver 无效: %q(可选: sqlite3/mysql)", c.Database.Driver)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (c *Config) LogLevel() slog.Level {
|
||
level, _ := parseLogLevel(c.Log.Level)
|
||
return level
|
||
}
|
||
|
||
func (c *Config) CORSMaxAge() time.Duration {
|
||
maxAge, _ := time.ParseDuration(c.API.CORS.MaxAge)
|
||
return maxAge
|
||
}
|
||
|
||
func parseLogLevel(level string) (slog.Level, error) {
|
||
switch level {
|
||
case "debug":
|
||
return slog.LevelDebug, nil
|
||
case "info":
|
||
return slog.LevelInfo, nil
|
||
case "warn":
|
||
return slog.LevelWarn, nil
|
||
case "error":
|
||
return slog.LevelError, nil
|
||
default:
|
||
return slog.LevelInfo, fmt.Errorf("log.level 无效: %q(可选: debug/info/warn/error)", level)
|
||
}
|
||
}
|