package handler import ( "log" "net/http" "os" "path/filepath" "runtime" "github.com/gin-gonic/gin" "gopkg.in/yaml.v3" ) type versionConfig struct { Version string `yaml:"version"` BuildNumber int `yaml:"build_number"` ForceUpdate bool `yaml:"force_update"` ReleaseNotes string `yaml:"release_notes"` DownloadURLs map[string]string `yaml:"download_urls"` // Changelog 由 client 发版脚本从 CHANGELOG-client.md 写入(最近 3 条), // 供官网下载页运行时拉取渲染更新日志,无需重建官网。 Changelog []changelogEntry `yaml:"changelog"` } type changelogEntry struct { Version string `yaml:"version" json:"version"` Date string `yaml:"date" json:"date"` Intro string `yaml:"intro" json:"intro"` Sections []changelogSection `yaml:"sections" json:"sections"` } type changelogSection struct { Type string `yaml:"type" json:"type"` Items []string `yaml:"items" json:"items"` } // changelogOrEmpty 保证响应里 changelog 始终是数组(而非 null),方便前端遍历。 func changelogOrEmpty(c []changelogEntry) []changelogEntry { if c == nil { return []changelogEntry{} } return c } // GetVersion GET /version func GetVersion(c *gin.Context) { cfg, err := loadVersionConfig() if err != nil { log.Printf("[version] failed to load version config: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "version config unavailable"}) return } c.JSON(http.StatusOK, gin.H{ "version": cfg.Version, "build_number": cfg.BuildNumber, "force_update": cfg.ForceUpdate, "release_notes": cfg.ReleaseNotes, "download_urls": cfg.DownloadURLs, "changelog": changelogOrEmpty(cfg.Changelog), }) } func loadVersionConfig() (*versionConfig, error) { // 查找 config/version.yaml,相对于可执行文件或源码目录 candidates := []string{ "config/version.yaml", filepath.Join(sourceDir(), "config/version.yaml"), } for _, path := range candidates { data, err := os.ReadFile(path) if err != nil { continue } var cfg versionConfig if err := yaml.Unmarshal(data, &cfg); err != nil { return nil, err } return &cfg, nil } return nil, os.ErrNotExist } // sourceDir 返回当前源文件所在目录的上两级(backend 根目录) func sourceDir() string { _, filename, _, ok := runtime.Caller(0) if !ok { return "." } // handler/ → internal/ → backend/ return filepath.Join(filepath.Dir(filename), "..", "..") }