Files
jiang13-bbs/backend/config/config.go
freefire b0170a99fd fix: 固定开发环境后端数据目录,避免 cwd 不同导致头像 404
从 cwd 向上解析 backend 根,并写入本地开发启动约定。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 05:13:58 +08:00

181 lines
4.5 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 (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"gopkg.in/ini.v1"
)
// Config 应用全局配置
type Config struct {
WorkPath string
Port int
DataDir string
JWTSecret string
DBDSN string
DevMode bool
}
// Parse 解析配置:环境变量 > app.ini > 默认值。
// 工作目录不跟 os.Getwd() 走:从 cwd 向上查找 backend 根(含 app.ini 或源码标记),
// 避免在仓库根 / cmd/jiang13 启动时把上传文件写进另一套 data/。
func Parse() (*Config, error) {
workPath, err := resolveWorkPath()
if err != nil {
return nil, err
}
// 默认值
cfg := &Config{
WorkPath: workPath,
Port: 3001,
DataDir: filepath.Join(workPath, "data"),
DBDSN: "postgres://postgres:postgres@localhost:5432/jiang13?sslmode=disable",
DevMode: true,
}
// 尝试加载 app.ini
iniPath := filepath.Join(workPath, "app.ini")
if _, err := os.Stat(iniPath); err == nil {
f, err := ini.Load(iniPath)
if err != nil {
return nil, fmt.Errorf("加载 app.ini 失败: %w", err)
}
if s := f.Section("server"); s.HasKey("HTTP_PORT") {
if v, err := s.Key("HTTP_PORT").Int(); err == nil {
cfg.Port = v
}
}
if s := f.Section("database"); s.HasKey("DSN") {
cfg.DBDSN = s.Key("DSN").String()
}
if s := f.Section("security"); s.HasKey("JWT_SECRET") {
cfg.JWTSecret = s.Key("JWT_SECRET").String()
}
if s := f.Section("paths"); s.HasKey("DATA") {
dataRel := s.Key("DATA").String()
if !filepath.IsAbs(dataRel) {
cfg.DataDir = filepath.Join(workPath, dataRel)
} else {
cfg.DataDir = dataRel
}
}
if s := f.Section("app"); s.HasKey("DEV_MODE") {
cfg.DevMode = s.Key("DEV_MODE").MustBool(true)
}
}
// 环境变量覆盖
if v := os.Getenv("HTTP_PORT"); v != "" {
if p, err := strconv.Atoi(v); err == nil {
cfg.Port = p
}
}
if v := os.Getenv("DB_DSN"); v != "" {
cfg.DBDSN = v
}
if v := os.Getenv("JWT_SECRET"); v != "" {
cfg.JWTSecret = v
}
if v := os.Getenv("DEV_MODE"); v != "" {
cfg.DevMode = strings.EqualFold(v, "true") || v == "1"
}
if err := rejectStrayDataDir(cfg.DataDir); err != nil {
return nil, err
}
// 确保数据目录存在
if err := os.MkdirAll(cfg.DataDir, 0755); err != nil {
return nil, fmt.Errorf("创建数据目录失败: %w", err)
}
// JWT 密钥:留空则自动生成并持久化
if strings.TrimSpace(cfg.JWTSecret) == "" {
secretFile := filepath.Join(cfg.DataDir, ".jwt_secret")
if data, err := os.ReadFile(secretFile); err == nil && len(data) > 0 {
cfg.JWTSecret = string(data)
} else {
cfg.JWTSecret = generateSecret(32)
_ = os.WriteFile(secretFile, []byte(cfg.JWTSecret), 0600)
}
}
return cfg, nil
}
// resolveWorkPath 定位后端根目录(app.ini 与 data/ 所在处)。
// 优先级:JIANG13_WORK_PATH > 从 cwd 向上找 app.ini > 源码树 backend/ > cwd。
func resolveWorkPath() (string, error) {
if v := strings.TrimSpace(os.Getenv("JIANG13_WORK_PATH")); v != "" {
abs, err := filepath.Abs(v)
if err != nil {
return "", fmt.Errorf("解析 JIANG13_WORK_PATH 失败: %w", err)
}
return abs, nil
}
cwd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("获取工作目录失败: %w", err)
}
dir := cwd
for range 8 {
if hasAppIni(dir) {
return dir, nil
}
nested := filepath.Join(dir, "backend")
if hasAppIni(nested) {
return nested, nil
}
if isSourceBackendRoot(dir) {
return dir, nil
}
if isSourceBackendRoot(nested) {
return nested, nil
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
return cwd, nil
}
func hasAppIni(dir string) bool {
_, err := os.Stat(filepath.Join(dir, "app.ini"))
return err == nil
}
func isSourceBackendRoot(dir string) bool {
_, e1 := os.Stat(filepath.Join(dir, "go.mod"))
_, e2 := os.Stat(filepath.Join(dir, "cmd", "jiang13"))
_, e3 := os.Stat(filepath.Join(dir, "app.ini.example"))
return e1 == nil && e2 == nil && e3 == nil
}
// rejectStrayDataDir 拦住把上传写进 cmd/jiang13/data 的错误 cwd。
func rejectStrayDataDir(dataDir string) error {
slash := filepath.ToSlash(dataDir)
if strings.Contains(slash, "/cmd/jiang13/") {
return fmt.Errorf("拒绝使用数据目录 %s:这是在 cmd/jiang13 下启动产生的错误路径。请在 backend/ 执行 go run ./cmd/jiang13,或设置 JIANG13_WORK_PATH 指向 backend 目录", dataDir)
}
return nil
}
func generateSecret(n int) string {
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, n)
for i := range b {
b[i] = chars[i%len(chars)]
}
return string(b)
}