diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index dd9a4e588f..2d2ee38394 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -8,6 +8,8 @@ import ( "log/slog" "net/url" "os" + "path/filepath" + "runtime" "strings" "time" @@ -944,6 +946,30 @@ func Load() (*Config, error) { return load(false) } +// getDataDir returns the data directory used by setup, mirroring setup.GetDataDir(). +// This keeps config search paths consistent with where setup writes files, +// preventing path drift on Windows where /app/data maps to the drive root. +func getDataDir() string { + if dir := os.Getenv("DATA_DIR"); dir != "" { + return dir + } + if runtime.GOOS == "linux" { + dockerDataDir := "/app/data" + if info, err := os.Stat(dockerDataDir); err == nil && info.IsDir() { + testFile := dockerDataDir + "/.write_test" + if f, err := os.Create(testFile); err == nil { + _ = f.Close() + _ = os.Remove(testFile) + return dockerDataDir + } + } + } + if exePath, err := os.Executable(); err == nil { + return filepath.Dir(exePath) + } + return "." +} + // LoadForBootstrap 读取启动阶段配置。 // // 启动阶段允许 jwt.secret 先留空,后续由数据库初始化流程补齐并再次完整校验。 @@ -955,18 +981,12 @@ func load(allowMissingJWTSecret bool) (*Config, error) { viper.SetConfigName("config") viper.SetConfigType("yaml") - // Add config paths in priority order - // 1. DATA_DIR environment variable (highest priority) - if dataDir := os.Getenv("DATA_DIR"); dataDir != "" { - viper.AddConfigPath(dataDir) - } - // 2. Docker data directory - viper.AddConfigPath("/app/data") - // 3. Current directory - viper.AddConfigPath(".") - // 4. Config subdirectory + // Add config paths in the same priority order used by setup.GetDataDir(), + // so runtime and setup always agree on where the config file lives. + viper.AddConfigPath(getDataDir()) + // Fallback: Config subdirectory (legacy layout) viper.AddConfigPath("./config") - // 5. System config directory + // Fallback: System config directory (Linux package installations) viper.AddConfigPath("/etc/sub2api") // 环境变量支持 diff --git a/backend/internal/pkg/sysutil/restart.go b/backend/internal/pkg/sysutil/restart.go index 2146596fc4..f38d00bc52 100644 --- a/backend/internal/pkg/sysutil/restart.go +++ b/backend/internal/pkg/sysutil/restart.go @@ -22,7 +22,7 @@ import ( // - Service configured with Restart=always in systemd unit file func RestartService() error { if runtime.GOOS != "linux" { - log.Println("Service restart via exit only works on Linux with systemd") + log.Println("Automatic restart is not supported on this platform. Please restart the application manually.") return nil } diff --git a/backend/internal/pkg/timezone/timezone.go b/backend/internal/pkg/timezone/timezone.go index 40f6e38f89..0b6d0fce98 100644 --- a/backend/internal/pkg/timezone/timezone.go +++ b/backend/internal/pkg/timezone/timezone.go @@ -21,7 +21,7 @@ var ( // Example timezone values: "Asia/Shanghai", "America/New_York", "UTC" func Init(tz string) error { if tz == "" { - tz = "Asia/Shanghai" // Default timezone + tz = "UTC" // Default to UTC for cross-platform compatibility } loc, err := time.LoadLocation(tz) diff --git a/backend/internal/setup/handler.go b/backend/internal/setup/handler.go index c2944cedfb..90405e2c60 100644 --- a/backend/internal/setup/handler.go +++ b/backend/internal/setup/handler.go @@ -5,6 +5,7 @@ import ( "net/http" "net/mail" "regexp" + "runtime" "strings" "sync" "time" @@ -348,8 +349,12 @@ func install(c *gin.Context) { sysutil.RestartServiceAsync() }() + restartMsg := "Installation completed successfully. Service will restart automatically." + if runtime.GOOS != "linux" { + restartMsg = "Installation completed successfully. Please restart the application manually." + } response.Success(c, gin.H{ - "message": "Installation completed successfully. Service will restart automatically.", - "restart": true, + "message": restartMsg, + "restart": runtime.GOOS == "linux", }) } diff --git a/backend/internal/setup/setup.go b/backend/internal/setup/setup.go index 9256d24575..82f6698008 100644 --- a/backend/internal/setup/setup.go +++ b/backend/internal/setup/setup.go @@ -8,6 +8,8 @@ import ( "encoding/hex" "fmt" "os" + "path/filepath" + "runtime" "strconv" "strings" "time" @@ -38,26 +40,37 @@ func setupDefaultAdminConcurrency() int { } // GetDataDir returns the data directory for storing config and lock files. -// Priority: DATA_DIR env > /app/data (if exists and writable) > current directory +// Priority: DATA_DIR env > /app/data (Linux/Docker only, if exists and writable) > executable directory > current directory func GetDataDir() string { // Check DATA_DIR environment variable first if dir := os.Getenv("DATA_DIR"); dir != "" { return dir } - // Check if /app/data exists and is writable (Docker environment) - dockerDataDir := "/app/data" - if info, err := os.Stat(dockerDataDir); err == nil && info.IsDir() { - // Try to check if writable by creating a temp file - testFile := dockerDataDir + "/.write_test" - if f, err := os.Create(testFile); err == nil { - _ = f.Close() - _ = os.Remove(testFile) - return dockerDataDir + // Check if /app/data exists and is writable — Docker/Linux environments only. + // Skipped on Windows because the path /app/data resolves to the current drive root + // (e.g., C:\app\data), causing a mismatch between setup write path and runtime read path. + if runtime.GOOS == "linux" { + dockerDataDir := "/app/data" + if info, err := os.Stat(dockerDataDir); err == nil && info.IsDir() { + // Try to check if writable by creating a temp file + testFile := dockerDataDir + "/.write_test" + if f, err := os.Create(testFile); err == nil { + _ = f.Close() + _ = os.Remove(testFile) + return dockerDataDir + } } } - // Default to current directory + // Use the executable's directory so setup and runtime always agree on the path, + // even when the current working directory differs (common on Windows when launching + // via double-click or as a service). + if exePath, err := os.Executable(); err == nil { + return filepath.Dir(exePath) + } + + // Final fallback: current directory return "." } @@ -432,7 +445,7 @@ func writeConfigFile(cfg *SetupConfig) error { // Ensure timezone has a default value tz := cfg.Timezone if tz == "" { - tz = "Asia/Shanghai" + tz = "UTC" } // Prepare config for YAML (exclude sensitive data and admin config)