feat(backend): Go 服务骨架(配置、日志、数据库、中间件、Wire)
- config: Viper YAML 配置 + 环境变量覆盖 - logs: zap 结构化日志 + trace_id 链路追踪 + 敏感信息脱敏 - db: PostgreSQL (GORM) + Redis (go-redis) 连接管理 - middleware: Trace/Logger/CORS/Recovery 四层中间件 - provider: Wire 编译时依赖注入 - main.go: 优雅启动/关闭 HTTP 服务 Made-with: Cursor
This commit is contained in:
46
backend/go-service/app/provider/provider.go
Normal file
46
backend/go-service/app/provider/provider.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// Package provider 提供全局依赖注入配置
|
||||||
|
// 使用 Wire 编译时依赖注入,集中管理所有模块的 Provider Set
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/echochat/backend/config"
|
||||||
|
"github.com/echochat/backend/pkg/db"
|
||||||
|
"github.com/google/wire"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// App 应用根容器,持有所有基础设施组件
|
||||||
|
type App struct {
|
||||||
|
Config *config.Config
|
||||||
|
DB *gorm.DB
|
||||||
|
Redis *redis.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewApp 创建应用实例
|
||||||
|
func NewApp(cfg *config.Config, gormDB *gorm.DB, redisClient *redis.Client) *App {
|
||||||
|
return &App{
|
||||||
|
Config: cfg,
|
||||||
|
DB: gormDB,
|
||||||
|
Redis: redisClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// provideDBConfig 从全局 Config 中提取 DatabaseConfig
|
||||||
|
func provideDBConfig(cfg *config.Config) *config.DatabaseConfig {
|
||||||
|
return &cfg.Database
|
||||||
|
}
|
||||||
|
|
||||||
|
// provideRedisConfig 从全局 Config 中提取 RedisConfig
|
||||||
|
func provideRedisConfig(cfg *config.Config) *config.RedisConfig {
|
||||||
|
return &cfg.Redis
|
||||||
|
}
|
||||||
|
|
||||||
|
// InfraSet 基础设施层 Provider Set
|
||||||
|
var InfraSet = wire.NewSet(
|
||||||
|
provideDBConfig,
|
||||||
|
provideRedisConfig,
|
||||||
|
db.NewPostgres,
|
||||||
|
db.NewRedis,
|
||||||
|
NewApp,
|
||||||
|
)
|
||||||
17
backend/go-service/app/provider/wire.go
Normal file
17
backend/go-service/app/provider/wire.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
//go:build wireinject
|
||||||
|
|
||||||
|
// Package provider 提供 Wire 依赖注入入口
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/echochat/backend/config"
|
||||||
|
"github.com/google/wire"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitializeApp 初始化整个应用(Wire 自动生成实现)
|
||||||
|
func InitializeApp(cfg *config.Config) (*App, error) {
|
||||||
|
wire.Build(
|
||||||
|
InfraSet,
|
||||||
|
)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
30
backend/go-service/app/provider/wire_gen.go
Normal file
30
backend/go-service/app/provider/wire_gen.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// Code generated by Wire. DO NOT EDIT.
|
||||||
|
|
||||||
|
//go:generate go run -mod=mod github.com/google/wire/cmd/wire
|
||||||
|
//go:build !wireinject
|
||||||
|
// +build !wireinject
|
||||||
|
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/echochat/backend/config"
|
||||||
|
"github.com/echochat/backend/pkg/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Injectors from wire.go:
|
||||||
|
|
||||||
|
// InitializeApp 初始化整个应用(Wire 自动生成实现)
|
||||||
|
func InitializeApp(cfg *config.Config) (*App, error) {
|
||||||
|
databaseConfig := provideDBConfig(cfg)
|
||||||
|
gormDB, err := db.NewPostgres(databaseConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
redisConfig := provideRedisConfig(cfg)
|
||||||
|
client, err := db.NewRedis(redisConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
app := NewApp(cfg, gormDB, client)
|
||||||
|
return app, nil
|
||||||
|
}
|
||||||
101
backend/go-service/cmd/server/main.go
Normal file
101
backend/go-service/cmd/server/main.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
// Package main 是 EchoChat 后端服务的入口
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/echochat/backend/config"
|
||||||
|
"github.com/echochat/backend/pkg/logs"
|
||||||
|
"github.com/echochat/backend/pkg/middleware"
|
||||||
|
"github.com/echochat/backend/pkg/utils"
|
||||||
|
"github.com/echochat/backend/app/provider"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 1. 加载配置
|
||||||
|
cfg, err := config.Load("config", "config.dev")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("加载配置失败: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 初始化日志系统
|
||||||
|
if err := logs.Init(cfg.Log.Level, cfg.Log.Format, cfg.Log.OutputPath); err != nil {
|
||||||
|
fmt.Printf("初始化日志失败: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer logs.Sync()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
logs.Info(ctx, "main", "EchoChat 服务启动中",
|
||||||
|
zap.String("mode", cfg.Server.Mode),
|
||||||
|
zap.Int("port", cfg.Server.Port),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 3. 通过 Wire 初始化所有组件
|
||||||
|
app, err := provider.InitializeApp(cfg)
|
||||||
|
if err != nil {
|
||||||
|
logs.Fatal(ctx, "main", "初始化应用失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
_ = app
|
||||||
|
|
||||||
|
// 4. 创建 Gin Engine
|
||||||
|
if cfg.Server.Mode == "release" {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
}
|
||||||
|
engine := gin.New()
|
||||||
|
|
||||||
|
// 5. 注册中间件(顺序:Trace → Logger → CORS → Recovery)
|
||||||
|
engine.Use(
|
||||||
|
middleware.Trace(),
|
||||||
|
middleware.Logger(),
|
||||||
|
middleware.CORS(),
|
||||||
|
middleware.Recovery(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 6. 注册路由
|
||||||
|
engine.GET("/health", func(c *gin.Context) {
|
||||||
|
utils.ResponseOK(c, gin.H{
|
||||||
|
"status": "ok",
|
||||||
|
"service": "echochat",
|
||||||
|
"time": time.Now().Format("2006-01-02 15:04:05"),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 7. 启动 HTTP 服务(优雅关闭)
|
||||||
|
addr := fmt.Sprintf(":%d", cfg.Server.Port)
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: engine,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
logs.Info(ctx, "main", "HTTP 服务启动", zap.String("addr", addr))
|
||||||
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
logs.Fatal(ctx, "main", "HTTP 服务启动失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 等待中断信号,优雅关闭
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-quit
|
||||||
|
|
||||||
|
logs.Info(ctx, "main", "正在关闭服务...")
|
||||||
|
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||||
|
logs.Error(ctx, "main", "服务关闭失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
logs.Info(ctx, "main", "服务已停止")
|
||||||
|
}
|
||||||
30
backend/go-service/config/config.dev.yaml
Normal file
30
backend/go-service/config/config.dev.yaml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
server:
|
||||||
|
port: 8080
|
||||||
|
mode: debug
|
||||||
|
|
||||||
|
database:
|
||||||
|
host: localhost
|
||||||
|
port: 5432
|
||||||
|
user: echochat
|
||||||
|
password: echochat_dev_2026
|
||||||
|
dbname: echochat
|
||||||
|
sslmode: disable
|
||||||
|
max_idle_conns: 10
|
||||||
|
max_open_conns: 100
|
||||||
|
|
||||||
|
redis:
|
||||||
|
host: localhost
|
||||||
|
port: 6379
|
||||||
|
password: ""
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
jwt:
|
||||||
|
secret: echochat-dev-jwt-secret-2026-please-change-in-production
|
||||||
|
access_expire_min: 120
|
||||||
|
refresh_expire_day: 7
|
||||||
|
issuer: echochat
|
||||||
|
|
||||||
|
log:
|
||||||
|
level: debug
|
||||||
|
format: text
|
||||||
|
output_path: stdout
|
||||||
98
backend/go-service/config/config.go
Normal file
98
backend/go-service/config/config.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
// Package config 提供应用配置管理功能
|
||||||
|
// 使用 Viper 读取 YAML 配置文件,支持环境变量覆盖
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config 应用全局配置结构体
|
||||||
|
type Config struct {
|
||||||
|
Server ServerConfig `mapstructure:"server"`
|
||||||
|
Database DatabaseConfig `mapstructure:"database"`
|
||||||
|
Redis RedisConfig `mapstructure:"redis"`
|
||||||
|
JWT JWTConfig `mapstructure:"jwt"`
|
||||||
|
Log LogConfig `mapstructure:"log"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerConfig HTTP 服务配置
|
||||||
|
type ServerConfig struct {
|
||||||
|
Port int `mapstructure:"port"` // 监听端口
|
||||||
|
Mode string `mapstructure:"mode"` // 运行模式: debug/release
|
||||||
|
}
|
||||||
|
|
||||||
|
// DatabaseConfig PostgreSQL 数据库配置
|
||||||
|
type DatabaseConfig struct {
|
||||||
|
Host string `mapstructure:"host"`
|
||||||
|
Port int `mapstructure:"port"`
|
||||||
|
User string `mapstructure:"user"`
|
||||||
|
Password string `mapstructure:"password"`
|
||||||
|
DBName string `mapstructure:"dbname"`
|
||||||
|
SSLMode string `mapstructure:"sslmode"`
|
||||||
|
MaxIdleConns int `mapstructure:"max_idle_conns"` // 最大空闲连接数
|
||||||
|
MaxOpenConns int `mapstructure:"max_open_conns"` // 最大打开连接数
|
||||||
|
}
|
||||||
|
|
||||||
|
// DSN 生成 PostgreSQL 连接字符串
|
||||||
|
func (d *DatabaseConfig) DSN() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
||||||
|
d.Host, d.Port, d.User, d.Password, d.DBName, d.SSLMode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RedisConfig Redis 配置
|
||||||
|
type RedisConfig struct {
|
||||||
|
Host string `mapstructure:"host"`
|
||||||
|
Port int `mapstructure:"port"`
|
||||||
|
Password string `mapstructure:"password"`
|
||||||
|
DB int `mapstructure:"db"` // 数据库编号
|
||||||
|
}
|
||||||
|
|
||||||
|
// Addr 生成 Redis 连接地址
|
||||||
|
func (r *RedisConfig) Addr() string {
|
||||||
|
return fmt.Sprintf("%s:%d", r.Host, r.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWTConfig JWT 认证配置
|
||||||
|
type JWTConfig struct {
|
||||||
|
Secret string `mapstructure:"secret"` // 签名密钥
|
||||||
|
AccessExpireMin int `mapstructure:"access_expire_min"` // Access Token 有效期(分钟)
|
||||||
|
RefreshExpireDay int `mapstructure:"refresh_expire_day"` // Refresh Token 有效期(天)
|
||||||
|
Issuer string `mapstructure:"issuer"` // 签发者
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogConfig 日志配置
|
||||||
|
type LogConfig struct {
|
||||||
|
Level string `mapstructure:"level"` // debug/info/warn/error
|
||||||
|
Format string `mapstructure:"format"` // text(开发)/json(生产)
|
||||||
|
OutputPath string `mapstructure:"output_path"` // stdout 或文件路径
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load 加载配置文件并返回 Config 实例
|
||||||
|
// configPath 为配置文件所在目录,configName 为文件名(不含扩展名)
|
||||||
|
func Load(configPath, configName string) (*Config, error) {
|
||||||
|
v := viper.New()
|
||||||
|
v.SetConfigName(configName)
|
||||||
|
v.SetConfigType("yaml")
|
||||||
|
v.AddConfigPath(configPath)
|
||||||
|
|
||||||
|
// 环境变量覆盖:ECHOCHAT_SERVER_PORT → server.port
|
||||||
|
v.SetEnvPrefix("ECHOCHAT")
|
||||||
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||||
|
v.AutomaticEnv()
|
||||||
|
|
||||||
|
if err := v.ReadInConfig(); err != nil {
|
||||||
|
return nil, fmt.Errorf("读取配置文件失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg Config
|
||||||
|
if err := v.Unmarshal(&cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析配置文件失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
67
backend/go-service/go.mod
Normal file
67
backend/go-service/go.mod
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
module github.com/echochat/backend
|
||||||
|
|
||||||
|
go 1.23.12
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-gonic/gin v1.11.0
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/google/wire v0.7.0
|
||||||
|
github.com/redis/go-redis/v9 v9.18.0
|
||||||
|
github.com/spf13/viper v1.21.0
|
||||||
|
go.uber.org/zap v1.27.1
|
||||||
|
gorm.io/driver/postgres v1.6.0
|
||||||
|
gorm.io/gorm v1.31.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bytedance/sonic v1.14.0 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||||
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
|
go.uber.org/mock v0.5.0 // indirect
|
||||||
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
|
golang.org/x/arch v0.20.0 // indirect
|
||||||
|
golang.org/x/crypto v0.40.0 // indirect
|
||||||
|
golang.org/x/mod v0.26.0 // indirect
|
||||||
|
golang.org/x/net v0.42.0 // indirect
|
||||||
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
|
golang.org/x/text v0.28.0 // indirect
|
||||||
|
golang.org/x/tools v0.35.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.9 // indirect
|
||||||
|
)
|
||||||
159
backend/go-service/go.sum
Normal file
159
backend/go-service/go.sum
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
|
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||||
|
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||||
|
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||||
|
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||||
|
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
|
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||||
|
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
|
||||||
|
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||||
|
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||||
|
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||||
|
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||||
|
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
|
||||||
|
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||||
|
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||||
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
|
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||||
|
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||||
|
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
|
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||||
|
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||||
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||||
|
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||||
|
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||||
|
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||||
|
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||||
|
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||||
|
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||||
|
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||||
|
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||||
|
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||||
|
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||||
|
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||||
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
|
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||||
|
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||||
|
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||||
|
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||||
|
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||||
|
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||||
|
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
93
backend/go-service/pkg/db/postgres.go
Normal file
93
backend/go-service/pkg/db/postgres.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
// Package db 提供数据库连接管理
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/echochat/backend/config"
|
||||||
|
"github.com/echochat/backend/pkg/logs"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GormDB 全局 GORM 数据库实例
|
||||||
|
var GormDB *gorm.DB
|
||||||
|
|
||||||
|
// zapGormLogger 将 GORM 日志适配到 zap
|
||||||
|
type zapGormLogger struct{}
|
||||||
|
|
||||||
|
func (l *zapGormLogger) LogMode(logger.LogLevel) logger.Interface { return l }
|
||||||
|
|
||||||
|
func (l *zapGormLogger) Info(ctx context.Context, msg string, data ...interface{}) {
|
||||||
|
logs.Info(ctx, "gorm", fmt.Sprintf(msg, data...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *zapGormLogger) Warn(ctx context.Context, msg string, data ...interface{}) {
|
||||||
|
logs.Warn(ctx, "gorm", fmt.Sprintf(msg, data...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *zapGormLogger) Error(ctx context.Context, msg string, data ...interface{}) {
|
||||||
|
logs.Error(ctx, "gorm", fmt.Sprintf(msg, data...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *zapGormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
|
||||||
|
elapsed := time.Since(begin)
|
||||||
|
sql, rows := fc()
|
||||||
|
fields := []zap.Field{
|
||||||
|
zap.Duration("latency", elapsed),
|
||||||
|
zap.Int64("rows", rows),
|
||||||
|
zap.String("sql", sql),
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
fields = append(fields, zap.Error(err))
|
||||||
|
logs.Error(ctx, "gorm.trace", "SQL执行失败", fields...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if elapsed > 200*time.Millisecond {
|
||||||
|
logs.Warn(ctx, "gorm.trace", "慢SQL告警", fields...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logs.Debug(ctx, "gorm.trace", "SQL执行", fields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPostgres 初始化 PostgreSQL 数据库连接
|
||||||
|
func NewPostgres(cfg *config.DatabaseConfig) (*gorm.DB, error) {
|
||||||
|
funcName := "db.NewPostgres"
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
logs.Info(ctx, funcName, "正在连接 PostgreSQL",
|
||||||
|
zap.String("host", cfg.Host),
|
||||||
|
zap.Int("port", cfg.Port),
|
||||||
|
zap.String("dbname", cfg.DBName),
|
||||||
|
)
|
||||||
|
|
||||||
|
db, err := gorm.Open(postgres.Open(cfg.DSN()), &gorm.Config{
|
||||||
|
Logger: &zapGormLogger{},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logs.Error(ctx, funcName, "PostgreSQL 连接失败", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("连接 PostgreSQL 失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取底层 sql.DB 失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
|
||||||
|
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
|
||||||
|
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||||
|
|
||||||
|
if err := sqlDB.Ping(); err != nil {
|
||||||
|
logs.Error(ctx, funcName, "PostgreSQL Ping 失败", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("ping PostgreSQL 失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
GormDB = db
|
||||||
|
logs.Info(ctx, funcName, "PostgreSQL 连接成功")
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
40
backend/go-service/pkg/db/redis.go
Normal file
40
backend/go-service/pkg/db/redis.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/echochat/backend/config"
|
||||||
|
"github.com/echochat/backend/pkg/logs"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Redis 全局 Redis 客户端实例
|
||||||
|
var Redis *redis.Client
|
||||||
|
|
||||||
|
// NewRedis 初始化 Redis 客户端连接
|
||||||
|
func NewRedis(cfg *config.RedisConfig) (*redis.Client, error) {
|
||||||
|
funcName := "db.NewRedis"
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
logs.Info(ctx, funcName, "正在连接 Redis",
|
||||||
|
zap.String("addr", cfg.Addr()),
|
||||||
|
zap.Int("db", cfg.DB),
|
||||||
|
)
|
||||||
|
|
||||||
|
client := redis.NewClient(&redis.Options{
|
||||||
|
Addr: cfg.Addr(),
|
||||||
|
Password: cfg.Password,
|
||||||
|
DB: cfg.DB,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := client.Ping(ctx).Err(); err != nil {
|
||||||
|
logs.Error(ctx, funcName, "Redis 连接失败", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("连接 Redis 失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
Redis = client
|
||||||
|
logs.Info(ctx, funcName, "Redis 连接成功")
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
26
backend/go-service/pkg/middleware/cors.go
Normal file
26
backend/go-service/pkg/middleware/cors.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CORS 跨域中间件
|
||||||
|
// 允许前端跨域访问后端 API
|
||||||
|
func CORS() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
c.Header("Access-Control-Allow-Origin", "*")
|
||||||
|
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH")
|
||||||
|
c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Authorization, X-Request-ID")
|
||||||
|
c.Header("Access-Control-Expose-Headers", "X-Request-ID")
|
||||||
|
c.Header("Access-Control-Max-Age", "86400")
|
||||||
|
|
||||||
|
if c.Request.Method == http.MethodOptions {
|
||||||
|
c.AbortWithStatus(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
46
backend/go-service/pkg/middleware/logger.go
Normal file
46
backend/go-service/pkg/middleware/logger.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/echochat/backend/pkg/logs"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger 请求日志中间件
|
||||||
|
// 记录每个请求的完整信息:方法、路径、状态码、耗时、IP、User-Agent
|
||||||
|
// 自动携带 trace_id,慢请求(>500ms)记录 WARN
|
||||||
|
func Logger() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
latency := time.Since(start)
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
funcName := "middleware.Logger"
|
||||||
|
|
||||||
|
fields := []zap.Field{
|
||||||
|
zap.String("method", c.Request.Method),
|
||||||
|
zap.String("path", c.Request.URL.Path),
|
||||||
|
zap.Int("status", c.Writer.Status()),
|
||||||
|
zap.Duration("latency", latency),
|
||||||
|
zap.String("ip", c.ClientIP()),
|
||||||
|
zap.String("user_agent", c.Request.UserAgent()),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Errors) > 0 {
|
||||||
|
fields = append(fields, zap.String("error", c.Errors.String()))
|
||||||
|
logs.Error(ctx, funcName, "请求处理异常", fields...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if latency > 500*time.Millisecond {
|
||||||
|
logs.Warn(ctx, funcName, "慢请求告警", fields...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logs.Info(ctx, funcName, "请求完成", fields...)
|
||||||
|
}
|
||||||
|
}
|
||||||
36
backend/go-service/pkg/middleware/recovery.go
Normal file
36
backend/go-service/pkg/middleware/recovery.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"runtime/debug"
|
||||||
|
|
||||||
|
"github.com/echochat/backend/pkg/logs"
|
||||||
|
"github.com/echochat/backend/pkg/utils"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Recovery Panic 恢复中间件
|
||||||
|
// 捕获 panic 后记录 ERROR 日志含堆栈信息,返回 500 响应
|
||||||
|
func Recovery() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
logs.Error(ctx, "middleware.Recovery", "服务发生 panic",
|
||||||
|
zap.Any("error", r),
|
||||||
|
zap.String("stack", string(debug.Stack())),
|
||||||
|
zap.String("path", c.Request.URL.Path),
|
||||||
|
zap.String("method", c.Request.Method),
|
||||||
|
)
|
||||||
|
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError, utils.Response{
|
||||||
|
Code: 500,
|
||||||
|
Message: "服务内部错误",
|
||||||
|
TraceID: logs.GetTraceID(ctx),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
26
backend/go-service/pkg/middleware/trace.go
Normal file
26
backend/go-service/pkg/middleware/trace.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Package middleware 提供 Gin HTTP 中间件
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/echochat/backend/pkg/logs"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Trace 链路追踪中间件
|
||||||
|
// 从请求头提取 X-Request-ID,不存在则自动生成
|
||||||
|
// 注入 context,后续所有日志自动携带 trace_id
|
||||||
|
// 在响应头中返回 X-Request-ID
|
||||||
|
func Trace() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
traceID := c.GetHeader("X-Request-ID")
|
||||||
|
if traceID == "" {
|
||||||
|
traceID = logs.GenerateTraceID()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := logs.WithTraceID(c.Request.Context(), traceID)
|
||||||
|
c.Request = c.Request.WithContext(ctx)
|
||||||
|
c.Header("X-Request-ID", traceID)
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
91
backend/go-service/pkg/utils/response.go
Normal file
91
backend/go-service/pkg/utils/response.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
// Package utils 提供通用工具函数
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/echochat/backend/pkg/logs"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Response 统一 API 响应结构
|
||||||
|
type Response struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data interface{} `json:"data,omitempty"`
|
||||||
|
TraceID string `json:"trace_id,omitempty"`
|
||||||
|
Time string `json:"time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseOK 成功响应 200
|
||||||
|
func ResponseOK(c *gin.Context, data interface{}) {
|
||||||
|
c.JSON(http.StatusOK, Response{
|
||||||
|
Code: 0,
|
||||||
|
Message: "success",
|
||||||
|
Data: data,
|
||||||
|
TraceID: logs.GetTraceID(c.Request.Context()),
|
||||||
|
Time: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseCreated 创建成功响应 201
|
||||||
|
func ResponseCreated(c *gin.Context, data interface{}) {
|
||||||
|
c.JSON(http.StatusCreated, Response{
|
||||||
|
Code: 0,
|
||||||
|
Message: "created",
|
||||||
|
Data: data,
|
||||||
|
TraceID: logs.GetTraceID(c.Request.Context()),
|
||||||
|
Time: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseBadRequest 参数错误响应 400
|
||||||
|
func ResponseBadRequest(c *gin.Context, message string) {
|
||||||
|
c.JSON(http.StatusBadRequest, Response{
|
||||||
|
Code: 400,
|
||||||
|
Message: message,
|
||||||
|
TraceID: logs.GetTraceID(c.Request.Context()),
|
||||||
|
Time: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseUnauthorized 未认证响应 401
|
||||||
|
func ResponseUnauthorized(c *gin.Context, message string) {
|
||||||
|
c.JSON(http.StatusUnauthorized, Response{
|
||||||
|
Code: 401,
|
||||||
|
Message: message,
|
||||||
|
TraceID: logs.GetTraceID(c.Request.Context()),
|
||||||
|
Time: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseForbidden 无权限响应 403
|
||||||
|
func ResponseForbidden(c *gin.Context, message string) {
|
||||||
|
c.JSON(http.StatusForbidden, Response{
|
||||||
|
Code: 403,
|
||||||
|
Message: message,
|
||||||
|
TraceID: logs.GetTraceID(c.Request.Context()),
|
||||||
|
Time: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseNotFound 资源不存在响应 404
|
||||||
|
func ResponseNotFound(c *gin.Context, message string) {
|
||||||
|
c.JSON(http.StatusNotFound, Response{
|
||||||
|
Code: 404,
|
||||||
|
Message: message,
|
||||||
|
TraceID: logs.GetTraceID(c.Request.Context()),
|
||||||
|
Time: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseError 服务器内部错误响应 500
|
||||||
|
func ResponseError(c *gin.Context, message string) {
|
||||||
|
c.JSON(http.StatusInternalServerError, Response{
|
||||||
|
Code: 500,
|
||||||
|
Message: message,
|
||||||
|
TraceID: logs.GetTraceID(c.Request.Context()),
|
||||||
|
Time: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user