Files
rond-with-you/internal/config/config.go
T
2026-09-18 13:05:49 +08:00

158 lines
5.3 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 (
"bufio"
"crypto/rand"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
)
type Config struct {
Listen string
DSN string
SecretKey string
UploadDir string
DataDir string
SessionDays int
Debug bool
// 底图。amap 走免密钥瓦片;填了 map_key 或选 tianditu 时改由服务端代理,
// 密钥不出现在页面源码里。
MapProvider string // amap | tianditu | custom
MapKey string
MapTileURL string // custom 模式下的瓦片模板,支持 {s} {x} {y} {z} {key}
MapCRS string // custom 模式下底图的坐标系:gcj02 | wgs84
// 地理编码(后台「按名称搜坐标」)。高德 Web 服务类型密钥,与瓦片用的
// JS 密钥不是同一个;只经服务端代理调用,不进入前端源码。留空则功能关闭。
GeocodeKey string
// TrackScript 是第三方统计 / 追踪脚本。填 URL 会包成 <script src>;
// 需要附加属性或初始化代码的服务(百度统计、Umami 等)可以把整段 <script> 贴进来。
TrackScript string
// 地点图片的对象存储(S3 兼容,Cloudflare R2 等)。存哪边由后台「图片存储」
// 设置决定,凭据只留在服务端、不进数据库也不下发前端。全部留空 = 只能用本地磁盘。
S3Endpoint string // R2 形如 https://<account>.r2.cloudflarestorage.com
S3Region string // R2 填 auto
S3Bucket string
S3AccessKey string
S3SecretKey string
S3PublicBase string // 公开访问前缀(自定义域名或 r2.dev),拼对象 key 即为 <img src>
}
// S3Ready 判断对象存储是否配置齐全(缺一不可,否则只能在本地存)。
func (c *Config) S3Ready() bool {
return c.S3Endpoint != "" && c.S3Bucket != "" && c.S3AccessKey != "" && c.S3SecretKey != ""
}
// MediaDir 是本地图片的落盘目录。
func (c *Config) MediaDir() string { return filepath.Join(c.DataDir, "media") }
func Load(path string) (*Config, error) {
kv := map[string]string{}
f, err := os.Open(path)
if err == nil {
defer f.Close()
sc := bufio.NewScanner(f)
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
continue
}
k, v, ok := strings.Cut(line, "=")
if !ok {
continue
}
kv[strings.ToLower(strings.TrimSpace(k))] = strings.TrimSpace(v)
}
} else if !os.IsNotExist(err) {
return nil, fmt.Errorf("读取配置 %s: %w", path, err)
}
cfg := &Config{
Listen: get(kv, "listen", ":8080"),
DSN: get(kv, "dsn", ""),
SecretKey: get(kv, "secret_key", ""),
UploadDir: get(kv, "upload_dir", "storage/uploads"),
DataDir: get(kv, "data_dir", "storage/data"),
SessionDays: getInt(kv, "session_days", 14),
Debug: get(kv, "debug", "false") == "true",
MapProvider: strings.ToLower(get(kv, "map_provider", "amap")),
MapKey: get(kv, "map_key", ""),
MapTileURL: get(kv, "map_tile_url", ""),
MapCRS: strings.ToLower(get(kv, "map_crs", "gcj02")),
GeocodeKey: get(kv, "geocode_key", ""),
TrackScript: strings.TrimSpace(get(kv, "track_script", "")),
S3Endpoint: strings.TrimRight(get(kv, "s3_endpoint", ""), "/"),
S3Region: get(kv, "s3_region", "auto"),
S3Bucket: get(kv, "s3_bucket", ""),
S3AccessKey: get(kv, "s3_access_key", ""),
S3SecretKey: get(kv, "s3_secret_key", ""),
S3PublicBase: normalizeBaseURL(get(kv, "s3_public_base", "")),
}
if cfg.DSN == "" {
return nil, fmt.Errorf("配置缺少 dsn(PostgreSQL 连接串)")
}
if cfg.MapProvider != "amap" && cfg.MapProvider != "tianditu" && cfg.MapProvider != "custom" {
return nil, fmt.Errorf("map_provider 只支持 amap / tianditu / custom,当前为 %q", cfg.MapProvider)
}
if cfg.MapProvider == "tianditu" && cfg.MapKey == "" {
return nil, fmt.Errorf("map_provider = tianditu 时必须填写 map_key(天地图密钥)")
}
if cfg.MapProvider == "custom" && cfg.MapTileURL == "" {
return nil, fmt.Errorf("map_provider = custom 时必须填写 map_tile_url")
}
if cfg.MapCRS != "gcj02" && cfg.MapCRS != "wgs84" {
return nil, fmt.Errorf("map_crs 只支持 gcj02 / wgs84,当前为 %q", cfg.MapCRS)
}
if cfg.SecretKey == "" {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return nil, err
}
cfg.SecretKey = hex.EncodeToString(b)
}
for _, d := range []string{cfg.UploadDir, cfg.DataDir, cfg.MediaDir()} {
if err := os.MkdirAll(filepath.Clean(d), 0o755); err != nil {
return nil, fmt.Errorf("创建目录 %s: %w", d, err)
}
}
return cfg, nil
}
// normalizeBaseURL 把「域名」补成完整 URL。
// s3_public_base 是直接拼进 <img src> 的,写成裸域名(r2.example.com)会得到相对地址,
// 浏览器把当前页面的路径拼上去,图全变破图——这种写法太容易发生了,这里统一补 https。
func normalizeBaseURL(v string) string {
v = strings.TrimRight(strings.TrimSpace(v), "/")
if v == "" || strings.Contains(v, "://") {
return v
}
return "https://" + v
}
func get(kv map[string]string, key, def string) string {
if v, ok := kv[key]; ok && v != "" {
return v
}
return def
}
func getInt(kv map[string]string, key string, def int) int {
if v, ok := kv[key]; ok && v != "" {
if n, err := strconv.Atoi(v); err == nil {
return n
}
}
return def
}