From acef92c79787f25cc705ceadaca4789ba1475cc9 Mon Sep 17 00:00:00 2001 From: tongtongstudio Date: Tue, 22 Sep 2026 18:57:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(config):=20=E7=BB=9F=E4=B8=80=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=20.env=20=E5=B9=B6=E5=BC=BA=E5=8C=96=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E5=90=AF=E5=8A=A8=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrate-channel-model-price-tiers/main.go | 2 + .../migrate-logical-model-families/main.go | 2 + backend/cmd/migrate-schema/main.go | 2 + backend/cmd/migrate-sqlite-postgres/main.go | 2 + .../cmd/reseed-logical-model-sources/main.go | 2 + backend/cmd/server/main.go | 4 + backend/internal/config/dotenv.go | 83 +++++++++++++++++++ scripts/start-local.ps1 | 39 ++++++++- 8 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 backend/internal/config/dotenv.go diff --git a/backend/cmd/migrate-channel-model-price-tiers/main.go b/backend/cmd/migrate-channel-model-price-tiers/main.go index e915d33d6..9b351d768 100644 --- a/backend/cmd/migrate-channel-model-price-tiers/main.go +++ b/backend/cmd/migrate-channel-model-price-tiers/main.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "infinite-canvas/backend/internal/config" "infinite-canvas/backend/internal/database" "infinite-canvas/backend/internal/model" "infinite-canvas/backend/internal/repository" @@ -38,6 +39,7 @@ type familyPlan struct { } func main() { + config.LoadDotEnv() apply := flag.Bool("apply", false, "创建系统模型价格档并切换当前前台目录版本") flag.Parse() db, err := database.Open(database.Config{Driver: "postgres", DSN: os.Getenv("DATABASE_URL"), DataDir: os.Getenv("CANVAS_BACKEND_DATA_DIR")}) diff --git a/backend/cmd/migrate-logical-model-families/main.go b/backend/cmd/migrate-logical-model-families/main.go index b27a4e1a2..86863944a 100644 --- a/backend/cmd/migrate-logical-model-families/main.go +++ b/backend/cmd/migrate-logical-model-families/main.go @@ -12,6 +12,7 @@ import ( "strconv" "strings" + "infinite-canvas/backend/internal/config" "infinite-canvas/backend/internal/database" "infinite-canvas/backend/internal/model" "infinite-canvas/backend/internal/repository" @@ -55,6 +56,7 @@ var familyDefinitions = []familyDefinition{ } func main() { + config.LoadDotEnv() apply := flag.Bool("apply", false, "写入模型家族并归档旧 SKU 目录项") flag.Parse() diff --git a/backend/cmd/migrate-schema/main.go b/backend/cmd/migrate-schema/main.go index 48233ff52..0af7c2efa 100644 --- a/backend/cmd/migrate-schema/main.go +++ b/backend/cmd/migrate-schema/main.go @@ -8,10 +8,12 @@ import ( "strings" "infinite-canvas/backend/internal/canvas" + "infinite-canvas/backend/internal/config" "infinite-canvas/backend/internal/database" ) func main() { + config.LoadDotEnv() command := "up" if len(os.Args) > 1 { command = strings.ToLower(strings.TrimSpace(os.Args[1])) diff --git a/backend/cmd/migrate-sqlite-postgres/main.go b/backend/cmd/migrate-sqlite-postgres/main.go index 6e20ab51b..2a1d307dd 100644 --- a/backend/cmd/migrate-sqlite-postgres/main.go +++ b/backend/cmd/migrate-sqlite-postgres/main.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "infinite-canvas/backend/internal/config" "infinite-canvas/backend/internal/database" "infinite-canvas/backend/internal/model" @@ -22,6 +23,7 @@ type tableMigration struct { } func main() { + config.LoadDotEnv() sourcePath := strings.TrimSpace(os.Getenv("SQLITE_SOURCE_PATH")) targetDSN := strings.TrimSpace(os.Getenv("DATABASE_URL")) if sourcePath == "" || targetDSN == "" { diff --git a/backend/cmd/reseed-logical-model-sources/main.go b/backend/cmd/reseed-logical-model-sources/main.go index 9071abf60..401fe8957 100644 --- a/backend/cmd/reseed-logical-model-sources/main.go +++ b/backend/cmd/reseed-logical-model-sources/main.go @@ -9,6 +9,7 @@ import ( "sort" "strings" + "infinite-canvas/backend/internal/config" "infinite-canvas/backend/internal/database" "infinite-canvas/backend/internal/model" "infinite-canvas/backend/internal/repository" @@ -25,6 +26,7 @@ type sourcePlan struct { } func main() { + config.LoadDotEnv() apply := flag.Bool("apply", false, "将符合条件的前台模型改为系统渠道模型驱动") flag.Parse() diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 62380516e..ebd37cf8b 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -15,6 +15,7 @@ import ( "syscall" "time" + "infinite-canvas/backend/internal/config" "infinite-canvas/backend/internal/database" "infinite-canvas/backend/internal/handler" "infinite-canvas/backend/internal/repository" @@ -33,6 +34,8 @@ func main() { } func run(ctx context.Context) error { + // 先加载仓库根的 .env(若存在),再读取环境变量,保证直连 `go run` 也能读到配置。 + config.LoadDotEnv() dataDir := env("CANVAS_BACKEND_DATA_DIR", "data") if err := os.MkdirAll(dataDir, 0o755); err != nil { return err @@ -48,6 +51,7 @@ func run(ctx context.Context) error { if err := database.ConfigurePool(db); err != nil { return err } + log.Printf("database driver: %s (DATABASE_URL %s)", db.Dialector.Name(), map[bool]string{true: "set", false: "empty"}[strings.TrimSpace(os.Getenv("DATABASE_URL")) != ""]) sqlDB, err := db.DB() if err != nil { return err diff --git a/backend/internal/config/dotenv.go b/backend/internal/config/dotenv.go new file mode 100644 index 000000000..b6be1731c --- /dev/null +++ b/backend/internal/config/dotenv.go @@ -0,0 +1,83 @@ +// Package config 提供轻量的运行时配置辅助能力。 +// +// 后端只从环境变量读取配置;为方便本地开发(不依赖启动脚本注入), +// 这里提供一个零依赖的 .env 加载器,供 server / migrate-schema 等命令共用。 +package config + +import ( + "bufio" + "log" + "os" + "path/filepath" + "strings" +) + +// LoadDotEnv 从若干候选路径读取 .env 并注入进程环境变量。 +// +// 规则: +// - 已存在的环境变量优先,不会被覆盖(Docker/脚本注入的值始终生效); +// - 跳过空行与 # 注释,兼容 export 前缀与首尾引号; +// - 找不到 .env 时静默跳过,不影响生产部署。 +func LoadDotEnv() { + path := findDotEnvPath() + if path == "" { + return + } + file, err := os.Open(path) + if err != nil { + return + } + defer file.Close() + + log.Printf("已从 %s 加载 .env 配置(已存在的环境变量优先,不覆盖)", path) + + scanner := bufio.NewScanner(file) + // 连接串可能较长,放宽单行上限 + scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) + + for scanner.Scan() { + line := strings.TrimPrefix(scanner.Text(), "\ufeff") // 去掉可能的 UTF-8 BOM + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + line = strings.TrimSpace(strings.TrimPrefix(line, "export ")) + + idx := strings.IndexByte(line, '=') + if idx <= 0 { + continue + } + key := strings.TrimSpace(line[:idx]) + value := strings.TrimSpace(line[idx+1:]) + if len(value) >= 2 && value[0] == '"' && value[len(value)-1] == '"' { + value = value[1 : len(value)-1] + } + if key == "" { + continue + } + if _, exists := os.LookupEnv(key); exists { + continue + } + _ = os.Setenv(key, value) + } +} + +// findDotEnvPath 依次查找:显式指定的 CANVAS_ENV_FILE、当前目录、上级目录、上上级目录。 +func findDotEnvPath() string { + if explicit := strings.TrimSpace(os.Getenv("CANVAS_ENV_FILE")); explicit != "" { + if info, err := os.Stat(explicit); err == nil && !info.IsDir() { + return explicit + } + } + candidates := []string{ + ".env", + filepath.Join("..", ".env"), + filepath.Join("..", "..", ".env"), + } + for _, candidate := range candidates { + if info, err := os.Stat(candidate); err == nil && !info.IsDir() { + return candidate + } + } + return "" +} diff --git a/scripts/start-local.ps1 b/scripts/start-local.ps1 index c41ad427a..46df185b2 100644 --- a/scripts/start-local.ps1 +++ b/scripts/start-local.ps1 @@ -13,6 +13,39 @@ $dataDir = Join-Path $repoRoot ".local\project-workbench-debug" $goBuildCache = Join-Path $repoRoot ".local\cache\go-build" $goModuleCache = Join-Path $repoRoot ".local\cache\go-mod" +# 从仓库根 .env 加载配置(KEY=VALUE,支持 # 注释与空行;不存在则跳过)。 +# 后端只读取环境变量,这里把 .env 注入进程环境,子进程(go/bun)自动继承。 +$envFile = Join-Path $repoRoot ".env" +if (-not (Test-Path -LiteralPath $envFile)) { + # Windows 常见坑:在记事本等里新建文件常变成 .env.txt + $alt = Join-Path $repoRoot ".env.txt" + if (Test-Path -LiteralPath $alt) { $envFile = $alt } + else { Write-Host "未找到 $envFile,将使用内置默认值(SQLite)。如需 PostgreSQL 请先创建该文件。" -ForegroundColor Yellow } +} +if (Test-Path -LiteralPath $envFile) { + Write-Host "正在从 $envFile 加载配置..." -ForegroundColor Cyan + $loaded = @() + Get-Content -LiteralPath $envFile | ForEach-Object { + $line = $_.Trim() + if ($line -eq "" -or $line.StartsWith("#")) { return } + if ($line -match '^\s*export\s+') { $line = $line -replace '^\s*export\s+', '' } + if ($line -match '^\s*([^=]+)=(.*)$') { + $name = $matches[1].Trim() + $value = $matches[2].Trim() + if ($value.StartsWith('"') -and $value.EndsWith('"')) { + $value = $value.Substring(1, $value.Length - 2) + } + [Environment]::SetEnvironmentVariable($name, $value, [System.EnvironmentVariableTarget]::Process) + $loaded += $name + } + } + Write-Host "已加载变量:$($loaded -join ', ')" -ForegroundColor Cyan + if (-not $env:DATABASE_URL) { Write-Host "警告:DATABASE_URL 为空,后端将回退 SQLite(数据不会同步)。" -ForegroundColor Yellow } +} +# 本地绑定默认值:未显式配置时,后端只监听本机、数据落在本地调试目录 +if (-not $env:CANVAS_BACKEND_ADDR) { $env:CANVAS_BACKEND_ADDR = '127.0.0.1:7381' } +if (-not $env:CANVAS_BACKEND_DATA_DIR) { $env:CANVAS_BACKEND_DATA_DIR = $dataDir } + foreach ($commandName in @("go", "bun")) { if (-not (Get-Command $commandName -ErrorAction SilentlyContinue)) { throw "未找到 $commandName,请先安装项目要求的运行时。" @@ -70,11 +103,9 @@ $dataDirLiteral = ConvertTo-PowerShellLiteral $dataDir $backendCommand = @" `$ErrorActionPreference = 'Stop' Set-Location -LiteralPath $backendDirLiteral -`$env:CANVAS_BACKEND_ADDR = '127.0.0.1:7381' -`$env:CANVAS_BACKEND_DATA_DIR = $dataDirLiteral `$env:GOCACHE = $(ConvertTo-PowerShellLiteral $goBuildCache) `$env:GOMODCACHE = $(ConvertTo-PowerShellLiteral $goModuleCache) -Write-Host '影策后端:http://127.0.0.1:7381' -ForegroundColor Cyan +Write-Host "影策后端:$($env:CANVAS_BACKEND_ADDR)" -ForegroundColor Cyan go run ./cmd/server "@ @@ -91,4 +122,4 @@ $webProcess = Start-Process -FilePath $powerShellPath -WindowStyle Normal -Worki Write-Host "已打开前后端开发窗口。" -ForegroundColor Green Write-Host "后端窗口 PID: $($backendProcess.Id);前端窗口 PID: $($webProcess.Id)" -Write-Host "访问 http://localhost:3000;分别在两个窗口按 Ctrl+C 停止服务。" +Write-Host "访问 http://localhost:7380;分别在两个窗口按 Ctrl+C 停止服务。"