spark-server/internal/infrastructure/mysql/mysql.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

129 lines
2.7 KiB
Go

// Package mysql opens GORM and writes SQL logs through zap.
package mysql
import (
"context"
"fmt"
"strings"
"time"
"go.uber.org/zap"
mysqldriver "gorm.io/driver/mysql"
"gorm.io/gorm"
)
// DB is a GORM handle. Connection pool is the standard library pool behind GORM v2.
type DB struct {
gdb *gorm.DB
}
// Options configure Open, including the underlying sql.DB pool.
type Options struct {
DSN string
SlowThreshold time.Duration
LogLevel string
ZapLevel string
MaxOpenConns int
MaxIdleConns int
ConnMaxLifetime time.Duration
ConnMaxIdleTime time.Duration
}
const (
defaultMaxOpen = 64
defaultMaxIdle = 16
defaultConnLife = 30 * time.Minute
defaultConnIdle = 10 * time.Minute
defaultPingTimeout = 10 * time.Second
)
// Open connects to MySQL. An empty DSN skips the connection for local bring-up.
func Open(log *zap.Logger, o *Options) (*DB, error) {
if o == nil {
o = &Options{}
}
if o.DSN == "" {
return &DB{}, nil
}
applyPoolDefaults(o)
gdb, err := gorm.Open(mysqldriver.Open(withDialTimeout(o.DSN)), &gorm.Config{
Logger: newGormLogger(log, o.SlowThreshold, parseGormLevel(o.LogLevel, o.ZapLevel)),
})
if err != nil {
return nil, fmt.Errorf("mysql open: %w", err)
}
sqlDB, err := gdb.DB()
if err != nil {
return nil, fmt.Errorf("mysql pool: %w", err)
}
sqlDB.SetMaxOpenConns(o.MaxOpenConns)
sqlDB.SetMaxIdleConns(o.MaxIdleConns)
sqlDB.SetConnMaxLifetime(o.ConnMaxLifetime)
sqlDB.SetConnMaxIdleTime(o.ConnMaxIdleTime)
ctx, cancel := context.WithTimeout(context.Background(), defaultPingTimeout)
defer cancel()
if err := sqlDB.PingContext(ctx); err != nil {
_ = sqlDB.Close()
return nil, fmt.Errorf("mysql ping: %w", err)
}
return &DB{gdb: gdb}, nil
}
func applyPoolDefaults(o *Options) {
if o.MaxOpenConns <= 0 {
o.MaxOpenConns = defaultMaxOpen
}
if o.MaxIdleConns <= 0 {
o.MaxIdleConns = defaultMaxIdle
}
if o.ConnMaxLifetime <= 0 {
o.ConnMaxLifetime = defaultConnLife
}
if o.ConnMaxIdleTime <= 0 {
o.ConnMaxIdleTime = defaultConnIdle
}
}
func withDialTimeout(dsn string) string {
if strings.Contains(dsn, "timeout=") {
return dsn
}
sep := "?"
if strings.Contains(dsn, "?") {
sep = "&"
}
return dsn + sep + "timeout=10s"
}
// Gorm returns the ORM handle (nil when DSN was empty).
func (d *DB) Gorm() *gorm.DB {
if d == nil {
return nil
}
return d.gdb
}
func (d *DB) Close() error {
if d == nil || d.gdb == nil {
return nil
}
sqlDB, err := d.gdb.DB()
if err != nil {
return err
}
return sqlDB.Close()
}
func (d *DB) Ready() bool { return d != nil && d.gdb != nil }
func (d *DB) Ping(ctx context.Context) error {
if !d.Ready() {
return nil
}
sqlDB, err := d.gdb.DB()
if err != nil {
return err
}
return sqlDB.PingContext(ctx)
}