Phase 1: Go backend scaffold

- Project structure: cmd/boosapi, internal/, pkg/
- Config loader (all env vars from Node.js original)
- Database layer with SQLite/MySQL/Postgres support
- Migration runner (sequential SQL files)
- Health check endpoint (GET /healthz)
- HTTP middleware: logger, auth, CORS, panic recovery
- chi router with /admin, /v1, /api route groups
- Graceful shutdown (SIGINT/SIGTERM)
- Multi-stage Dockerfile (alpine, ~20MB image)
- Initial schema migration (users, api_keys, sites, usage_logs)

Signed-off-by: Boos4721 <boos4721@icloud.com>
This commit is contained in:
boos
2026-05-16 01:07:02 +08:00
parent f0188d3f68
commit b6ecd613d4
9 changed files with 686 additions and 88 deletions
+132
View File
@@ -0,0 +1,132 @@
// Package main is the entry point for the BoosAPI server.
package main
import (
"context"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/go-chi/chi/v5"
chimw "github.com/go-chi/chi/v5/middleware"
"github.com/cita-777/boosapi/internal/config"
"github.com/cita-777/boosapi/internal/db"
"github.com/cita-777/boosapi/internal/server/handler"
"github.com/cita-777/boosapi/internal/server/middleware"
)
var version = "dev"
func main() {
// Load config
cfg, err := config.Load()
if err != nil {
slog.Error("failed to load config", "error", err)
os.Exit(1)
}
// Set timezone
if cfg.Location() != time.UTC {
time.Local = cfg.Location()
}
// Configure slog
logLevel := new(slog.LevelVar)
switch cfg.LogLevel {
case "debug":
logLevel.Set(slog.LevelDebug)
case "info":
logLevel.Set(slog.LevelInfo)
case "warn":
logLevel.Set(slog.LevelWarn)
case "error":
logLevel.Set(slog.LevelError)
default:
logLevel.Set(slog.LevelInfo)
}
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: logLevel,
})))
// Connect database
database, err := db.Open(cfg.DBType, cfg.DBURL, cfg.DataDir)
if err != nil {
slog.Error("failed to connect database", "error", err)
os.Exit(1)
}
defer db.Close(database)
// Run migrations
if err := db.RunMigrations(database, "internal/db/migrate"); err != nil {
slog.Error("failed to run migrations", "error", err)
os.Exit(1)
}
// Build router
r := chi.NewRouter()
// Global middleware
r.Use(chimw.RequestID)
r.Use(chimw.RealIP)
r.Use(middleware.Recover)
r.Use(middleware.Logger)
r.Use(middleware.CORS)
// Health check (no auth required)
r.Get("/healthz", handler.NewHealthHandler(database, version).ServeHTTP)
// Admin routes (require auth)
r.Route("/admin", func(r chi.Router) {
r.Use(middleware.AuthToken(cfg.AuthToken))
// TODO: admin handlers
})
// Proxy routes (require proxy token)
r.Route("/v1", func(r chi.Router) {
r.Use(middleware.ProxyToken(cfg.ProxyToken))
// TODO: proxy handlers
})
r.Route("/api", func(r chi.Router) {
// TODO: API handlers
})
// Start server
addr := cfg.ListenAddr()
srv := &http.Server{
Addr: addr,
Handler: r,
ReadTimeout: 30 * time.Second,
WriteTimeout: 300 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Graceful shutdown
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
go func() {
slog.Info("server starting", "addr", addr, "version", version)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("server error", "error", err)
os.Exit(1)
}
}()
<-quit
slog.Info("shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
slog.Error("force shutdown", "error", err)
os.Exit(1)
}
slog.Info("server stopped")
}