// Package httpapi 提供 HTTP 接口与静态资源托管。 package httpapi import ( "encoding/json" "io/fs" "log/slog" "net/http" "strings" "time" "git.yeij.top/AskaEth/Scoreboard/server/internal/webui" ) // Options 是构建路由所需的依赖。 type Options struct { Logger *slog.Logger Version string } // New 组装并返回服务端的 HTTP 处理器。 func New(opts Options) http.Handler { logger := opts.Logger if logger == nil { logger = slog.Default() } mux := http.NewServeMux() mux.HandleFunc("GET /api/health", func(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]any{ "ok": true, "version": opts.Version, }) }) assets, err := webui.FS() if err != nil { logger.Error("前端资源不可用", "err", err) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.Error(w, "前端资源不可用", http.StatusServiceUnavailable) }) } else { mux.Handle("/", staticHandler(assets)) } return withRequestLog(mux, logger) } // staticHandler 把 URL 映射到多页面前端产物。 // // / → index.html // /display → display.html // /admin → admin.html // /其他 → 按文件路径查找 func staticHandler(assets fs.FS) http.Handler { files := http.FileServerFS(assets) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { p := strings.TrimPrefix(r.URL.Path, "/") switch p { case "", "index.html": http.ServeFileFS(w, r, assets, "index.html") case "display", "display/": http.ServeFileFS(w, r, assets, "display.html") case "admin", "admin/": http.ServeFileFS(w, r, assets, "admin.html") default: if _, err := fs.Stat(assets, p); err != nil { http.NotFound(w, r) return } files.ServeHTTP(w, r) } }) } func writeJSON(w http.ResponseWriter, code int, v any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(code) _ = json.NewEncoder(w).Encode(v) } type statusRecorder struct { http.ResponseWriter status int } func (r *statusRecorder) WriteHeader(code int) { r.status = code r.ResponseWriter.WriteHeader(code) } func withRequestLog(next http.Handler, logger *slog.Logger) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK} next.ServeHTTP(rec, r) logger.Debug("http", "method", r.Method, "path", r.URL.Path, "status", rec.status, "dur", time.Since(start), ) }) }