feat(core): implement cordis configuration extension and migrate all plugins

This commit is contained in:
ryan
2026-08-29 10:53:53 +08:00
parent b43c429544
commit ed8491addf
74 changed files with 1354 additions and 1145 deletions
@@ -0,0 +1,27 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package driver_http
type httpAppConfig struct {
Addr string `config:"addr" env:"APP_ADDR" default:":8000"`
AppName string `config:"app_name" env:"APP_NAME" default:"Wavelet"`
APIPrefix string `config:"api_prefix" env:"APP_API_PREFIX" default:"/api/v1"`
Env string `config:"env" env:"APP_ENV" default:"development"`
GracefulShutdownTimeout int `config:"graceful_shutdown_timeout" env:"APP_GRACEFUL_SHUTDOWN_TIMEOUT" default:"30"`
SessionCookieName string `config:"session_cookie_name" env:"APP_SESSION_COOKIE_NAME" default:"wavelet_session"`
SessionSecret string `config:"session_secret" env:"APP_SESSION_SECRET" secret:"true"`
SessionDomain string `config:"session_domain" env:"APP_SESSION_DOMAIN"`
SessionAge int `config:"session_age" env:"APP_SESSION_AGE" default:"86400"`
SessionHTTPOnly bool `config:"session_http_only" env:"APP_SESSION_HTTP_ONLY" default:"true"`
SessionSecure bool `config:"session_secure" env:"APP_SESSION_SECURE"`
}
type httpRedisConfig struct {
Addrs []string `config:"addrs" env:"REDIS_ADDR"`
Username string `config:"username" env:"REDIS_USERNAME"`
Password string `config:"password" env:"REDIS_PASSWORD" secret:"true"`
DB int `config:"db" env:"REDIS_DB"`
KeyPrefix string `config:"key_prefix" env:"REDIS_KEY_PREFIX"`
MinIdleConn int `config:"min_idle_conn" env:"REDIS_MIN_IDLE_CONN"`
}
+42 -72
View File
@@ -4,19 +4,9 @@
package driver_http
import (
"Wavelet/pkg/config"
"Wavelet/pkg/trace"
"Wavelet/pkg/util"
"context"
"errors"
"log"
"net"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/redis"
@@ -26,102 +16,82 @@ import (
// BuildEngine 构建并初始化 Gin 路由引擎及全部中间件和路由
func BuildEngine() (*gin.Engine, error) {
return BuildEngineWithConfig(httpAppConfig{}, httpRedisConfig{})
}
// BuildEngineWithConfig constructs the Gin engine with explicitly injected configuration.
func BuildEngineWithConfig(appCfg httpAppConfig, redisCfg httpRedisConfig) (*gin.Engine, error) {
// 运行模式
if config.Config.App.IsProduction() {
if appCfg.Env == "production" || appCfg.Env == "prod" {
gin.SetMode(gin.ReleaseMode)
}
setAPIPrefix(appCfg.APIPrefix)
// 初始化路由
r := gin.New()
r.Use(gin.Recovery())
r.Use(corsMiddleware())
cfg := config.Config.Redis
addrs := cfg.Addrs
addrs := redisCfg.Addrs
sessionAddr := "localhost:6379"
if len(addrs) > 0 {
sessionAddr = addrs[0]
}
sessionSecret := appCfg.SessionSecret
if sessionSecret == "" {
sessionSecret = "wavelet-default-session-secret"
}
sessionStore, err := redis.NewStoreWithDB(
cfg.MinIdleConn,
redisCfg.MinIdleConn,
"tcp",
sessionAddr,
cfg.Username,
cfg.Password,
strconv.Itoa(cfg.DB),
[]byte(config.Config.App.SessionSecret),
redisCfg.Username,
redisCfg.Password,
strconv.Itoa(redisCfg.DB),
[]byte(sessionSecret),
)
if err != nil {
return nil, err
}
// 设置 Session Redis Key 前缀
if cfg.KeyPrefix != "" {
if err := redis.SetKeyPrefix(sessionStore, cfg.KeyPrefix+"session:"); err != nil {
if redisCfg.KeyPrefix != "" {
if err := redis.SetKeyPrefix(sessionStore, redisCfg.KeyPrefix+"session:"); err != nil {
log.Printf("[API] set session key prefix failed: %v\n", err)
}
}
sessionCookieName := appCfg.SessionCookieName
if sessionCookieName == "" {
sessionCookieName = "wavelet_session"
}
sessionAge := appCfg.SessionAge
if sessionAge <= 0 {
sessionAge = 86400
}
sessionStore.Options(sessions.Options{
Path: "/",
Domain: config.Config.App.SessionDomain,
MaxAge: config.Config.App.SessionAge,
HttpOnly: config.Config.App.SessionHTTPOnly,
Secure: config.Config.App.SessionSecure,
Domain: appCfg.SessionDomain,
MaxAge: sessionAge,
HttpOnly: appCfg.SessionHTTPOnly,
Secure: appCfg.SessionSecure,
SameSite: http.SameSiteLaxMode,
})
r.Use(sessions.Sessions(config.Config.App.SessionCookieName, sessionStore))
r.Use(sessions.Sessions(sessionCookieName, sessionStore))
appName := appCfg.AppName
if appName == "" {
appName = "Wavelet"
}
// 补充中间件
r.Use(otelgin.Middleware(config.Config.App.AppName), errorHandlerMiddleware(), loggerMiddleware())
r.Use(otelgin.Middleware(appName), errorHandlerMiddleware(), loggerMiddleware())
return r, nil
}
// Serve 启动 HTTP API 服务。onStarted 仅会在 HTTP 地址成功绑定后调用。
func Serve(onStarted func()) {
r, err := BuildEngine()
if err != nil {
log.Fatalf("[API] init session store failed: %v\n", err)
}
srv := &http.Server{
Addr: config.Config.App.Addr,
Handler: r,
ReadHeaderTimeout: 10 * time.Second,
}
listener, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", config.Config.App.Addr)
if err != nil {
log.Fatalf("[API] server failed to listen on %s: %v\n", config.Config.App.Addr, err)
}
if onStarted != nil {
onStarted()
}
util.Go(func() {
log.Printf("[API] server listening on %s\n", config.Config.App.Addr)
if err := srv.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("[API] server failed: %v\n", err)
}
})
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Duration(config.Config.App.GracefulShutdownTimeout)*time.Second)
trace.Shutdown(shutdownCtx)
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("[API] server forced to shutdown: %v\n", err)
cancel()
os.Exit(1)
}
cancel()
log.Println("[API] server exited")
}
@@ -5,13 +5,13 @@
package driver_http
import (
"Wavelet/pkg/config"
"Wavelet/pkg/logger"
"Wavelet/pkg/response"
"context"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
@@ -21,6 +21,26 @@ import (
otel_trace "Wavelet/pkg/trace"
)
var (
apiPrefixMu sync.RWMutex
apiPrefix = "/api/v1"
)
func setAPIPrefix(prefix string) {
if prefix == "" {
return
}
apiPrefixMu.Lock()
defer apiPrefixMu.Unlock()
apiPrefix = prefix
}
func getAPIPrefix() string {
apiPrefixMu.RLock()
defer apiPrefixMu.RUnlock()
return apiPrefix
}
func loggerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 初始化 Trace
@@ -46,7 +66,7 @@ func loggerMiddleware() gin.HandlerFunc {
// 打印日志
// 排除健康检查接口
healthPath := config.Config.App.APIPrefix + "/health"
healthPath := getAPIPrefix() + "/health"
if c.Request.URL.Path != healthPath {
logger.InfoF(
ctx,
+32 -1
View File
@@ -91,10 +91,31 @@ func (p *Plugin) Name() string {
return "driver_http"
}
// DeclareConfig declares configuration bindings for driver_http.
func (p *Plugin) DeclareConfig() []core.ConfigBinding {
return []core.ConfigBinding{
{Prefix: "app", Target: &httpAppConfig{}},
{Prefix: "redis", Target: &httpRedisConfig{}},
}
}
// Apply mounts the HTTP driver plugin into the micro-kernel Context.
func (p *Plugin) Apply(ctx *core.Context) error {
var appCfg httpAppConfig
if err := ctx.Config().Bind("app", &appCfg); err != nil {
return err
}
var redisCfg httpRedisConfig
_ = ctx.Config().Bind("redis", &redisCfg)
p.mu.Lock()
p.coreCtx = ctx
if p.addr == defaultAddr && appCfg.Addr != "" {
p.addr = appCfg.Addr
}
if appCfg.GracefulShutdownTimeout > 0 {
p.shutdownTimeout = time.Duration(appCfg.GracefulShutdownTimeout) * time.Second
}
p.mu.Unlock()
// Bind DBService from Context
@@ -147,7 +168,17 @@ func (p *Plugin) Start(ctx context.Context) error {
}
if p.engine == nil {
p.engine = gin.New()
var appCfg httpAppConfig
var redisCfg httpRedisConfig
if p.coreCtx != nil {
_ = p.coreCtx.Config().Bind("app", &appCfg)
_ = p.coreCtx.Config().Bind("redis", &redisCfg)
}
var err error
p.engine, err = BuildEngineWithConfig(appCfg, redisCfg)
if err != nil {
p.engine = gin.New()
}
}
// Mount routes collected in Context RouterExtension