spark-server/internal/transport/http/server.go
zhr ae9a71034d feat: implement Spark IM V3 modular monolith
Add HTTP /v1 MVC, TCP send path, groups with server-side member-avatar collage, object-storage upload, and friends/favorites around the architecture-v3 layout.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 19:52:11 +08:00

116 lines
2.6 KiB
Go

package httpx
import (
"context"
"fmt"
"net"
"net/http"
"sync"
"sync/atomic"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
authlogic "spark-server/internal/module/auth/logic"
favlogic "spark-server/internal/module/favorite/logic"
friendlogic "spark-server/internal/module/friend/logic"
grouplogic "spark-server/internal/module/group/logic"
uploadlogic "spark-server/internal/module/upload/logic"
userlogic "spark-server/internal/module/user/logic"
"spark-server/internal/pkg/ratelimit"
)
type Server struct {
log *zap.Logger
http *http.Server
ready chan struct{}
readyOnce sync.Once
listening atomic.Bool
}
// Health is the readiness checker. Nil means tests: /readyz is always ready.
type Health interface {
Ready(ctx context.Context) (ok bool, data any)
}
// EngineDeps wires Application services into Gin. Auth nil disables Bearer checks (tests).
type EngineDeps struct {
Auth authlogic.Service
Login *authlogic.LoginService
Users *userlogic.Service
Friends *friendlogic.Service
Groups *grouplogic.Service
Files *uploadlogic.Service
Favorites *favlogic.Service
Health Health
AuthLimiter *ratelimit.Limiter
}
func New(log *zap.Logger, addr string, deps *EngineDeps) *Server {
if log == nil {
log = zap.NewNop()
}
if deps == nil {
deps = &EngineDeps{}
}
r := NewEngine(log, deps)
return &Server{
log: log,
ready: make(chan struct{}),
http: &http.Server{
Addr: addr,
Handler: r,
ReadHeaderTimeout: 5 * time.Second,
},
}
}
func NewEngine(log *zap.Logger, deps *EngineDeps) *gin.Engine {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
_ = r.SetTrustedProxies([]string{"127.0.0.1", "::1"})
if deps == nil {
deps = &EngineDeps{}
}
r.Use(Trace(log), Recovery(log), AccessLog(), AuthRateLimit(deps.AuthLimiter), Auth(deps.Auth))
registerRoutes(r, deps)
return r
}
func (s *Server) Start() error {
ln, err := net.Listen("tcp", s.http.Addr)
if err != nil {
s.signalReady()
return err
}
s.listening.Store(true)
s.signalReady()
s.log.Info("http listening", zap.String("addr", ln.Addr().String()))
err = s.http.Serve(ln)
if err == http.ErrServerClosed {
return nil
}
return err
}
func (s *Server) WaitReady(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err()
case <-s.ready:
if s.listening.Load() {
return nil
}
return fmt.Errorf("http not listening")
}
}
func (s *Server) signalReady() {
s.readyOnce.Do(func() { close(s.ready) })
}
func (s *Server) Stop(ctx context.Context) error {
return s.http.Shutdown(ctx)
}