Files
rill/internal/config/config.go
T
kevin b683fb293e 增加注册登录与鉴权,并按功能拆分 internal 模块
- 新增 /api/auth/register、/api/auth/login,JWT 签发与 Bearer 鉴权中间件
- notes 需登录,users/user-groups 仅管理员;auth 配置项随版本 1→2 自动补全
- internal/api 仅保留路由装配,拆分为 auth/user/usergroup/note/httpx/testutil
- 同步更新 Swagger 文档与前端注册接口路径
2026-09-20 02:03:06 +08:00

278 lines
7.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
import (
_ "embed"
"errors"
"fmt"
"io/fs"
"log/slog"
"os"
"path/filepath"
"strings"
"time"
"github.com/goccy/go-yaml"
)
//go:embed config.default.yaml
var defaultConfigYAML []byte
type Config struct {
Version int `yaml:"version"`
Server ServerConfig `yaml:"server"`
Log LogConfig `yaml:"log"`
Static StaticConfig `yaml:"static"`
API APIConfig `yaml:"api"`
Auth AuthConfig `yaml:"auth"`
Database DatabaseConfig `yaml:"database"`
}
type ServerConfig struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Sock string `yaml:"sock"`
Mode string `yaml:"mode"`
}
// TCPEnabled 是否启用 TCP 监听(port 为 0 表示不启用)。
func (s ServerConfig) TCPEnabled() bool {
return s.Port > 0
}
// SockEnabled 是否启用 unix socket 监听(sock 留空表示不启用)。
func (s ServerConfig) SockEnabled() bool {
return strings.TrimSpace(s.Sock) != ""
}
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 AuthConfig struct {
Secret string `yaml:"secret"`
TokenTTL string `yaml:"token_ttl"`
}
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,
Sock: "web.sock",
Mode: "release",
},
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",
},
},
Auth: AuthConfig{
TokenTTL: "24h",
},
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:
data = applyConfigUpgrade(path, data)
case errors.Is(err, fs.ErrNotExist):
if err := generateConfig(path); err != nil {
return nil, err
}
data = defaultConfigYAML
default:
return nil, fmt.Errorf("读取配置文件 %s 失败: %w", path, err)
}
if err := yaml.Unmarshal(data, cfg); err != nil {
return nil, fmt.Errorf("解析配置文件 %s 失败: %w", path, err)
}
if err := cfg.validate(); err != nil {
return nil, err
}
return cfg, nil
}
func generateConfig(path string) error {
if dir := filepath.Dir(path); dir != "." {
if err := os.MkdirAll(dir, 0o755); err != nil {
return fmt.Errorf("创建配置目录 %s 失败: %w", dir, err)
}
}
if err := os.WriteFile(path, defaultConfigYAML, 0o644); err != nil {
return fmt.Errorf("生成默认配置文件 %s 失败: %w", path, err)
}
slog.Info("配置文件不存在,已生成默认配置", "path", path)
return 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 无效: %d0 表示不启用 TCP", c.Server.Port)
}
if !c.Server.TCPEnabled() && !c.Server.SockEnabled() {
return fmt.Errorf("server.port 与 server.sock 至少需要启用一个")
}
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 := time.ParseDuration(c.Auth.TokenTTL); err != nil {
return fmt.Errorf("auth.token_ttl 无效: %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
}
// TokenTTLDuration 登录凭证有效期,配置无效时返回 0。
func (c *Config) TokenTTLDuration() time.Duration {
ttl, _ := time.ParseDuration(c.Auth.TokenTTL)
return ttl
}
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)
}
}