feat(backend): establish gin sqlite service skeleton

This commit is contained in:
QiuSW
2026-07-25 22:29:56 +08:00
parent 84f2da3cf7
commit ab21219a07
32 changed files with 1854 additions and 42 deletions
+113
View File
@@ -0,0 +1,113 @@
package main
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"cmroubao/backend-api/internal/config"
"cmroubao/backend-api/internal/platform/database"
"cmroubao/backend-api/internal/transport/httpapi"
)
func main() {
if err := run(); err != nil {
log.Printf("cmroubao API stopped: %v", err)
os.Exit(1)
}
}
func run() error {
cfg, err := config.Load(os.LookupEnv)
if err != nil {
return err
}
startupContext, cancelStartup := context.WithTimeout(
context.Background(),
cfg.ReadTimeout,
)
defer cancelStartup()
db, err := database.Open(startupContext, cfg.DatabasePath)
if err != nil {
return errors.New("database startup failed")
}
defer func() {
if err := db.Close(); err != nil {
log.Print("database close failed")
}
}()
router, err := httpapi.NewRouter(db, func(event string) {
log.Print(event)
})
if err != nil {
return errors.New("HTTP router setup failed")
}
server := httpapi.NewServer(cfg, router)
signalContext, stopSignals := signal.NotifyContext(
context.Background(),
os.Interrupt,
syscall.SIGTERM,
)
defer stopSignals()
serverErrors := make(chan error, 1)
go func() {
serverErrors <- server.ListenAndServe()
}()
select {
case err := <-serverErrors:
if errors.Is(err, http.ErrServerClosed) {
return nil
}
return errors.New("HTTP server failed")
case <-signalContext.Done():
return shutdownServer(server, serverErrors, cfg.ShutdownTimeout)
}
}
func shutdownServer(
server *http.Server,
serverErrors <-chan error,
timeout time.Duration,
) error {
shutdownContext, cancelShutdown := context.WithTimeout(
context.Background(),
timeout,
)
defer cancelShutdown()
shutdownErr := server.Shutdown(shutdownContext)
if shutdownErr != nil {
if err := server.Close(); err != nil {
return errors.New("HTTP server force close failed")
}
}
waitTimeout := timeout
if waitTimeout > time.Second {
waitTimeout = time.Second
}
timer := time.NewTimer(waitTimeout)
defer timer.Stop()
select {
case err := <-serverErrors:
if err != nil && !errors.Is(err, http.ErrServerClosed) {
return errors.New("HTTP server failed during shutdown")
}
case <-timer.C:
_ = server.Close()
return errors.New("HTTP server did not stop")
}
if shutdownErr != nil {
return errors.New("HTTP server graceful shutdown timed out")
}
return nil
}
+75
View File
@@ -0,0 +1,75 @@
package main
import (
"errors"
"net"
"net/http"
"testing"
"time"
)
func TestShutdownServerForceClosesAfterGracefulTimeout(t *testing.T) {
handlerStarted := make(chan struct{})
releaseHandler := make(chan struct{})
server := &http.Server{
Handler: http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
close(handlerStarted)
<-releaseHandler
}),
ReadHeaderTimeout: time.Second,
}
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("net.Listen() error = %v", err)
}
serverErrors := make(chan error, 1)
go func() {
serverErrors <- server.Serve(listener)
}()
requestFinished := make(chan struct{})
go func() {
defer close(requestFinished)
client := http.Client{Timeout: time.Second}
response, err := client.Get("http://" + listener.Addr().String())
if err == nil {
_ = response.Body.Close()
}
}()
select {
case <-handlerStarted:
case <-time.After(time.Second):
t.Fatal("handler did not start")
}
err = shutdownServer(server, serverErrors, 10*time.Millisecond)
if err == nil {
t.Fatal("shutdownServer() error = nil")
}
close(releaseHandler)
select {
case <-requestFinished:
case <-time.After(time.Second):
t.Fatal("request did not finish after force close")
}
connection, dialErr := net.DialTimeout(
"tcp",
listener.Addr().String(),
100*time.Millisecond,
)
if dialErr == nil {
_ = connection.Close()
t.Fatal("listener still accepted connections after force close")
}
}
func TestShutdownServerAcceptsNormalServerClose(t *testing.T) {
serverErrors := make(chan error, 1)
serverErrors <- http.ErrServerClosed
server := &http.Server{}
if err := shutdownServer(server, serverErrors, time.Second); err != nil &&
!errors.Is(err, http.ErrServerClosed) {
t.Fatalf("shutdownServer() error = %v", err)
}
}
+79
View File
@@ -0,0 +1,79 @@
package main
import (
"context"
"errors"
"fmt"
"log"
"os"
"time"
"cmroubao/backend-api/internal/config"
"cmroubao/backend-api/internal/platform/database"
"cmroubao/backend-api/internal/platform/migration"
)
func main() {
if err := run(os.Args[1:]); err != nil {
log.Printf("migration command failed: %v", err)
os.Exit(1)
}
}
func run(arguments []string) error {
if len(arguments) != 1 {
return errors.New("usage: migrate <up|down|status>")
}
command := arguments[0]
if command != "up" && command != "down" && command != "status" {
return errors.New("migration command must be up, down, or status")
}
databasePath, err := config.LoadDatabasePath(os.LookupEnv)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
db, err := database.Open(ctx, databasePath)
if err != nil {
return errors.New("database startup failed")
}
defer func() {
if err := db.Close(); err != nil {
log.Print("database close failed")
}
}()
runner, err := migration.New(db)
if err != nil {
return errors.New("migration setup failed")
}
switch command {
case "up":
count, err := runner.Up(ctx)
if err != nil {
return errors.New("migration up failed")
}
fmt.Printf("applied=%d\n", count)
case "down":
if err := runner.Down(ctx); err != nil {
return errors.New("migration down failed")
}
fmt.Println("rolled_back=1")
case "status":
statuses, err := runner.Status(ctx)
if err != nil {
return errors.New("migration status failed")
}
for _, status := range statuses {
state := "pending"
if status.Applied {
state = "applied"
}
fmt.Printf("version=%d state=%s\n", status.Version, state)
}
}
return nil
}
+16
View File
@@ -0,0 +1,16 @@
package main
import "testing"
func TestRunRejectsMissingAndUnknownCommandsBeforeOpeningDatabase(t *testing.T) {
tests := [][]string{
nil,
{"unknown"},
{"up", "extra"},
}
for _, arguments := range tests {
if err := run(arguments); err == nil {
t.Fatalf("run(%v) error = nil", arguments)
}
}
}