后端 Go+Gin:认证/CSRF/限流、板块、帖子、评论、点赞、通知、用户资料、置顶推荐;前端 Next.js 16:发帖/编辑/删除、搜索、分页、点赞、通知中心、设置;基础设施 docker-compose 与配置模板;添加 .gitignore 与专有许可证(保留所有权利)
113 lines
2.6 KiB
Go
113 lines
2.6 KiB
Go
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 > 默认值
|
|
func Parse() (*Config, error) {
|
|
workPath, err := os.Getwd()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("获取工作目录失败: %w", 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 := 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
|
|
}
|
|
|
|
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)
|
|
}
|