From b0350abfbe4eed4d68ba7855a340bf5042d8a586 Mon Sep 17 00:00:00 2001 From: valitovgaziz Date: Fri, 12 Jun 2026 10:47:41 +0500 Subject: [PATCH] DB optimization: pool, golang-migrate, consolidate to single Postgres - Fix DB_NAME=db_yal -> mydb in api_yal .env - Add connection pool (MaxOpenConns 25, MaxIdleConns 10, ConnMaxLifetime 30m) - Replace GORM AutoMigrate with golang-migrate in api_yal and api_bb - Create embedded SQL migrations for both APIs - Add DB_SCHEMA support to api_bb config - Consolidate to single Postgres: db_bb -> schema 'bb' on db container - Remove db_bb service, bb-network, db_bb volume from compose - Remove api_tp targets from Makefile - Clean up old migrate.go --- main_dc/BB/api_bb/go.mod | 5 + main_dc/BB/api_bb/internal/app/app.go | 8 +- main_dc/BB/api_bb/internal/config/config.go | 2 + .../BB/api_bb/internal/database/database.go | 76 ++++-- .../BB/api_bb/internal/database/migrate.go | 105 -------- .../BB/api_bb/migrations/000001_init.down.sql | 14 ++ .../BB/api_bb/migrations/000001_init.up.sql | 217 +++++++++++++++++ main_dc/BB/api_bb/migrations/embed.go | 6 + main_dc/Makefile | 16 -- main_dc/docker-compose.yml | 35 +-- main_dc/yalarba/api_yal/.env | 2 +- main_dc/yalarba/api_yal/go.mod | 5 + .../api_yal/internal/database/psql_db.go | 72 +++--- .../api_yal/migrations/000001_init.down.sql | 14 ++ .../api_yal/migrations/000001_init.up.sql | 227 ++++++++++++++++++ .../migrations/000002_cleanup_es.down.sql | 0 .../migrations/000002_cleanup_es.up.sql | 6 + main_dc/yalarba/api_yal/migrations/embed.go | 6 + 18 files changed, 610 insertions(+), 206 deletions(-) delete mode 100644 main_dc/BB/api_bb/internal/database/migrate.go create mode 100644 main_dc/BB/api_bb/migrations/000001_init.down.sql create mode 100644 main_dc/BB/api_bb/migrations/000001_init.up.sql create mode 100644 main_dc/BB/api_bb/migrations/embed.go create mode 100644 main_dc/yalarba/api_yal/migrations/000001_init.down.sql create mode 100644 main_dc/yalarba/api_yal/migrations/000001_init.up.sql create mode 100644 main_dc/yalarba/api_yal/migrations/000002_cleanup_es.down.sql create mode 100644 main_dc/yalarba/api_yal/migrations/000002_cleanup_es.up.sql create mode 100644 main_dc/yalarba/api_yal/migrations/embed.go diff --git a/main_dc/BB/api_bb/go.mod b/main_dc/BB/api_bb/go.mod index 4fd497a..6320571 100644 --- a/main_dc/BB/api_bb/go.mod +++ b/main_dc/BB/api_bb/go.mod @@ -6,6 +6,7 @@ require ( github.com/go-chi/chi/v5 v5.2.3 github.com/go-chi/cors v1.2.2 github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/golang-migrate/migrate/v4 v4.18.2 golang.org/x/crypto v0.43.0 gorm.io/driver/postgres v1.6.0 gorm.io/gorm v1.31.0 @@ -15,8 +16,12 @@ require ( github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/stretchr/testify v1.11.1 // indirect + go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/sys v0.37.0 // indirect ) diff --git a/main_dc/BB/api_bb/internal/app/app.go b/main_dc/BB/api_bb/internal/app/app.go index ad1001e..f6acdab 100644 --- a/main_dc/BB/api_bb/internal/app/app.go +++ b/main_dc/BB/api_bb/internal/app/app.go @@ -32,7 +32,8 @@ func (a *App) Initialize() error { // Инициализация базы данных dbConfig := &database.Config{ - URL: a.cfg.DatabaseURL, + URL: a.cfg.DatabaseURL, + Schema: a.cfg.DBSchema, } a.db = database.NewDatabase(dbConfig) @@ -46,11 +47,6 @@ func (a *App) Initialize() error { return err } - // Выполнение миграций - if err := a.db.Migrate(); err != nil { - return err - } - // Настройка роутера router := routes.SetupRouter(a.db.DB, a.cfg) diff --git a/main_dc/BB/api_bb/internal/config/config.go b/main_dc/BB/api_bb/internal/config/config.go index 7be3d20..82d9d5c 100644 --- a/main_dc/BB/api_bb/internal/config/config.go +++ b/main_dc/BB/api_bb/internal/config/config.go @@ -11,6 +11,7 @@ import ( type Config struct { Port string DatabaseURL string + DBSchema string StaticURL string `env:"STATIC_URL" envDefault:"http://localhost:8080"` JWTSecret string `env:"JWT_SECRET,required"` @@ -34,6 +35,7 @@ func Load() *Config { return &Config{ Port: port, DatabaseURL: databaseURL, + DBSchema: getEnv("DB_SCHEMA", "public"), JWTSecret: jwtSecret, } } diff --git a/main_dc/BB/api_bb/internal/database/database.go b/main_dc/BB/api_bb/internal/database/database.go index f312357..fba644e 100644 --- a/main_dc/BB/api_bb/internal/database/database.go +++ b/main_dc/BB/api_bb/internal/database/database.go @@ -1,9 +1,15 @@ package database import ( + "api_bb/migrations" + "database/sql" "fmt" "strings" + "time" + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database/postgres" + "github.com/golang-migrate/migrate/v4/source/iofs" "go.uber.org/zap" "gorm.io/driver/postgres" "gorm.io/gorm" @@ -17,26 +23,34 @@ type Database struct { } type Config struct { - URL string + URL string + Schema string } func NewDatabase(cfg *Config) *Database { + if cfg.Schema == "" { + cfg.Schema = "public" + } return &Database{ cfg: cfg, } } -// Connect устанавливает соединение с базой данных func (d *Database) Connect() error { zapLogger := logger.Get() - // Логирование попытки подключения к БД zapLogger.Info("attempting to connect to database", zap.String("host", ExtractHostFromDSN(d.cfg.URL)), zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)), + zap.String("schema", d.cfg.Schema), ) - db, err := gorm.Open(postgres.Open(d.cfg.URL), &gorm.Config{}) + dsn := d.cfg.URL + if d.cfg.Schema != "public" { + dsn = dsn + fmt.Sprintf(" search_path=%s", d.cfg.Schema) + } + + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) if err != nil { zapLogger.Error("failed to connect to database", zap.Error(err), @@ -47,7 +61,21 @@ func (d *Database) Connect() error { d.DB = db - // Логирование успешного подключения к БД + zapLogger.Info("Configure connection pool") + sqlDB, err := d.DB.DB() + if err != nil { + return fmt.Errorf("failed to get underlying sql.DB: %w", err) + } + sqlDB.SetMaxOpenConns(25) + sqlDB.SetMaxIdleConns(10) + sqlDB.SetConnMaxLifetime(30 * time.Minute) + + zapLogger.Info("Run database migrations") + if err := d.runMigrations(sqlDB); err != nil { + return fmt.Errorf("failed to run migrations: %w", err) + } + zapLogger.Info("Migrations completed successfully") + zapLogger.Info("successfully connected to database", zap.String("host", ExtractHostFromDSN(d.cfg.URL)), zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)), @@ -56,7 +84,32 @@ func (d *Database) Connect() error { return nil } -// Ping проверяет соединение с базой данных +func (d *Database) runMigrations(sqlDB *sql.DB) error { + zapLogger := logger.Get() + + source, err := iofs.New(migrations.FS, ".") + if err != nil { + return fmt.Errorf("failed to create migration source: %w", err) + } + + driver, err := postgres.WithInstance(sqlDB, &postgres.Config{}) + if err != nil { + return fmt.Errorf("failed to create postgres driver: %w", err) + } + + m, err := migrate.NewWithInstance("iofs", source, "postgres", driver) + if err != nil { + return fmt.Errorf("failed to create migrate instance: %w", err) + } + + if err := m.Up(); err != nil && err != migrate.ErrNoChange { + zapLogger.Error("Migration error", zap.Error(err)) + return fmt.Errorf("failed to apply migrations: %w", err) + } + + return nil +} + func (d *Database) Ping() error { zapLogger := logger.Get() @@ -75,7 +128,6 @@ func (d *Database) Ping() error { return nil } -// Close закрывает соединение с базой данных func (d *Database) Close() error { zapLogger := logger.Get() @@ -99,11 +151,7 @@ func (d *Database) Close() error { return nil } -// Вспомогательные функции для работы с DSN - -// ExtractHostFromDSN извлекает хост из DSN строки func ExtractHostFromDSN(dsn string) string { - // Простая реализация для PostgreSQL DSN parts := strings.Split(dsn, " ") for _, part := range parts { if strings.HasPrefix(part, "host=") { @@ -113,9 +161,7 @@ func ExtractHostFromDSN(dsn string) string { return "unknown" } -// ExtractDBNameFromDSN извлекает имя базы данных из DSN строки func ExtractDBNameFromDSN(dsn string) string { - // Простая реализация для PostgreSQL DSN parts := strings.Split(dsn, " ") for _, part := range parts { if strings.HasPrefix(part, "dbname=") { @@ -125,9 +171,7 @@ func ExtractDBNameFromDSN(dsn string) string { return "unknown" } -// MaskPassword маскирует пароль в DSN строке для безопасного логирования func MaskPassword(dsn string) string { - // Простая реализация - заменяет пароль на *** parts := strings.Split(dsn, " ") for i, part := range parts { if strings.HasPrefix(part, "password=") { @@ -136,4 +180,4 @@ func MaskPassword(dsn string) string { } } return strings.Join(parts, " ") -} \ No newline at end of file +} diff --git a/main_dc/BB/api_bb/internal/database/migrate.go b/main_dc/BB/api_bb/internal/database/migrate.go deleted file mode 100644 index 8051101..0000000 --- a/main_dc/BB/api_bb/internal/database/migrate.go +++ /dev/null @@ -1,105 +0,0 @@ -package database - -import ( - "go.uber.org/zap" - - "api_bb/internal/models" - "api_bb/pkg/logger" -) - -// Migrate выполняет автоматические миграции для всех моделей -func (d *Database) Migrate() error { - zapLogger := logger.Get() - - zapLogger.Info("starting database migration") - - // Список всех моделей для миграции - models := []interface{}{ - &models.User{}, - &models.News{}, - &models.Comment{}, - &models.Review{}, - &models.UserStats{}, - &models.Workout{}, - &models.Achievement{}, - &models.Event{}, - &models.EventRegistration{}, - &models.PersonalBest{}, - &models.TrainingPlan{}, - &models.EmailVerification{}, - // Добавьте другие модели здесь - } - - for _, model := range models { - modelName := getModelName(model) - zapLogger.Debug("migrating model", zap.String("model", modelName)) - - if err := d.DB.AutoMigrate(model); err != nil { - zapLogger.Error("failed to migrate model", - zap.String("model", modelName), - zap.Error(err), - ) - return err - } - } - - zapLogger.Info("database migration completed successfully") - return nil -} - -// MigrateModels выполняет миграции для конкретных моделей -func (d *Database) MigrateModels(models ...interface{}) error { - zapLogger := logger.Get() - - zapLogger.Info("starting migration for specific models", - zap.Int("model_count", len(models)), - ) - - for _, model := range models { - modelName := getModelName(model) - zapLogger.Debug("migrating model", zap.String("model", modelName)) - - if err := d.DB.AutoMigrate(model); err != nil { - zapLogger.Error("failed to migrate model", - zap.String("model", modelName), - zap.Error(err), - ) - return err - } - } - - zapLogger.Info("models migration completed successfully") - return nil -} - -// getModelName возвращает имя модели для логирования -func getModelName(model interface{}) string { - switch model.(type) { - case *models.User: - return "User" - case *models.News: - return "News" - case *models.Comment: - return "Comment" - case *models.Review: - return "Reviews" - case *models.UserStats: - return "Статистика Пользователя" - case *models.Workout: - return "Тренировки пользователя" - case *models.Achievement: - return "Достижения пользователя" - case *models.Event: - return "Событие" - case *models.EventRegistration: - return "Администрирование события" - case *models.PersonalBest: - return "Персональные достижения" - case *models.TrainingPlan: - return "Тренировочный план" - case *models.EmailVerification: - return "Верификация email" - default: - return "Unknown" - } -} diff --git a/main_dc/BB/api_bb/migrations/000001_init.down.sql b/main_dc/BB/api_bb/migrations/000001_init.down.sql new file mode 100644 index 0000000..2be17fe --- /dev/null +++ b/main_dc/BB/api_bb/migrations/000001_init.down.sql @@ -0,0 +1,14 @@ +DROP TABLE IF EXISTS galleries CASCADE; +DROP TABLE IF EXISTS email_verifications CASCADE; +DROP TABLE IF EXISTS training_workouts CASCADE; +DROP TABLE IF EXISTS training_plans CASCADE; +DROP TABLE IF EXISTS personal_bests CASCADE; +DROP TABLE IF EXISTS event_registrations CASCADE; +DROP TABLE IF EXISTS events CASCADE; +DROP TABLE IF EXISTS achievements CASCADE; +DROP TABLE IF EXISTS workouts CASCADE; +DROP TABLE IF EXISTS user_stats CASCADE; +DROP TABLE IF EXISTS reviews CASCADE; +DROP TABLE IF EXISTS comments CASCADE; +DROP TABLE IF EXISTS news CASCADE; +DROP TABLE IF EXISTS users CASCADE; diff --git a/main_dc/BB/api_bb/migrations/000001_init.up.sql b/main_dc/BB/api_bb/migrations/000001_init.up.sql new file mode 100644 index 0000000..d18239f --- /dev/null +++ b/main_dc/BB/api_bb/migrations/000001_init.up.sql @@ -0,0 +1,217 @@ +CREATE TABLE IF NOT EXISTS users ( + id BIGSERIAL PRIMARY KEY, + email VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL, + first_name VARCHAR(255) NOT NULL, + last_name VARCHAR(255) NOT NULL, + avatar VARCHAR(255), + phone VARCHAR(255), + experience VARCHAR(255), + goals VARCHAR(255), + newsletter BOOLEAN DEFAULT FALSE, + role VARCHAR(255) DEFAULT 'user', + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ, + deleted_at TIMESTAMPTZ, + email_verified BOOLEAN DEFAULT FALSE, + verified_at TIMESTAMPTZ +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users(email); +CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON users(deleted_at); + +CREATE TABLE IF NOT EXISTS news ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ, + deleted_at TIMESTAMPTZ, + title VARCHAR(255) NOT NULL, + excerpt VARCHAR(500) NOT NULL, + content TEXT NOT NULL, + image VARCHAR(255), + category VARCHAR(20) NOT NULL, + views BIGINT DEFAULT 0, + author_id BIGINT NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_news_deleted_at ON news(deleted_at); + +CREATE TABLE IF NOT EXISTS comments ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ, + content TEXT NOT NULL, + news_id BIGINT NOT NULL, + author_id BIGINT NOT NULL +); + +CREATE TABLE IF NOT EXISTS reviews ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ, + deleted_at TIMESTAMPTZ, + rating BIGINT NOT NULL, + text TEXT NOT NULL, + achievement VARCHAR(255), + distance VARCHAR(50), + improvement VARCHAR(100), + trainings BIGINT DEFAULT 0, + verified BOOLEAN DEFAULT FALSE, + author_id BIGINT NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_reviews_deleted_at ON reviews(deleted_at); + +CREATE TABLE IF NOT EXISTS user_stats ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + total_distance DECIMAL(10,2) DEFAULT 0, + total_time BIGINT DEFAULT 0, + avg_pace VARCHAR(20), + workouts_count BIGINT DEFAULT 0, + current_streak BIGINT DEFAULT 0, + longest_streak BIGINT DEFAULT 0, + weekly_distance DECIMAL(8,2) DEFAULT 0, + monthly_distance DECIMAL(8,2) DEFAULT 0, + best_5k VARCHAR(20), + best_10k VARCHAR(20), + best_half VARCHAR(20), + best_marathon VARCHAR(20), + last_workout TIMESTAMPTZ, + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_user_stats_user_id ON user_stats(user_id); + +CREATE TABLE IF NOT EXISTS workouts ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + type VARCHAR(20) NOT NULL, + distance_km DECIMAL(5,2) NOT NULL, + duration_min BIGINT NOT NULL, + pace VARCHAR(20), + calories BIGINT DEFAULT 0, + notes TEXT, + date TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ +); +CREATE INDEX IF NOT EXISTS idx_workouts_user_id ON workouts(user_id); +CREATE INDEX IF NOT EXISTS idx_workouts_date ON workouts(date); + +CREATE TABLE IF NOT EXISTS achievements ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + type VARCHAR(20) NOT NULL, + title VARCHAR(255) NOT NULL, + description TEXT, + result VARCHAR(100), + distance VARCHAR(50), + date TIMESTAMPTZ NOT NULL, + verified BOOLEAN DEFAULT FALSE, + badge_image VARCHAR(500), + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ +); +CREATE INDEX IF NOT EXISTS idx_achievements_user_id ON achievements(user_id); + +CREATE TABLE IF NOT EXISTS events ( + id BIGSERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + description TEXT NOT NULL, + date TIMESTAMPTZ NOT NULL, + location VARCHAR(255) NOT NULL, + type VARCHAR(50) NOT NULL, + distance VARCHAR(50), + participants_count BIGINT DEFAULT 0, + max_participants BIGINT DEFAULT 0, + registration_open BOOLEAN DEFAULT TRUE, + image VARCHAR(500), + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ +); + +CREATE TABLE IF NOT EXISTS event_registrations ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + event_id BIGINT NOT NULL, + status VARCHAR(50) DEFAULT 'pending', + notes TEXT, + result_time VARCHAR(20), + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ +); + +CREATE TABLE IF NOT EXISTS personal_bests ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + distance_type VARCHAR(20) NOT NULL, + time VARCHAR(20) NOT NULL, + pace VARCHAR(20), + date TIMESTAMPTZ NOT NULL, + verified BOOLEAN DEFAULT FALSE, + event_name VARCHAR(255), + location VARCHAR(255), + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ +); +CREATE INDEX IF NOT EXISTS idx_personal_bests_user_id ON personal_bests(user_id); + +CREATE TABLE IF NOT EXISTS training_plans ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + title VARCHAR(255) NOT NULL, + description TEXT, + weeks BIGINT NOT NULL DEFAULT 12, + workouts_per_week BIGINT NOT NULL DEFAULT 3, + target_distance VARCHAR(50), + target_date TIMESTAMPTZ, + current_week BIGINT DEFAULT 1, + completed BOOLEAN DEFAULT FALSE, + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ +); +CREATE INDEX IF NOT EXISTS idx_training_plans_user_id ON training_plans(user_id); + +CREATE TABLE IF NOT EXISTS training_workouts ( + id BIGSERIAL PRIMARY KEY, + plan_id BIGINT NOT NULL, + week BIGINT NOT NULL, + day BIGINT NOT NULL, + type VARCHAR(20) NOT NULL, + description TEXT, + distance_km DECIMAL(5,2), + duration_min BIGINT, + completed BOOLEAN DEFAULT FALSE, + completed_at TIMESTAMPTZ, + created_at TIMESTAMPTZ +); +CREATE INDEX IF NOT EXISTS idx_training_workouts_plan_id ON training_workouts(plan_id); + +CREATE TABLE IF NOT EXISTS email_verifications ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + token VARCHAR(100) NOT NULL, + email VARCHAR(255) NOT NULL, + type VARCHAR(20) NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + used BOOLEAN DEFAULT FALSE, + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ, + deleted_at TIMESTAMPTZ +); +CREATE INDEX IF NOT EXISTS idx_email_verifications_user_id ON email_verifications(user_id); +CREATE UNIQUE INDEX IF NOT EXISTS idx_email_verifications_token ON email_verifications(token); +CREATE INDEX IF NOT EXISTS idx_email_verifications_deleted_at ON email_verifications(deleted_at); + +CREATE TABLE IF NOT EXISTS galleries ( + id BIGSERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + description TEXT, + image_path VARCHAR(500) NOT NULL, + category VARCHAR(20) NOT NULL, + author_id BIGINT NOT NULL, + event_date TIMESTAMPTZ, + views BIGINT DEFAULT 0, + likes BIGINT DEFAULT 0, + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ +); +CREATE INDEX IF NOT EXISTS idx_galleries_author_id ON galleries(author_id); diff --git a/main_dc/BB/api_bb/migrations/embed.go b/main_dc/BB/api_bb/migrations/embed.go new file mode 100644 index 0000000..91cca1c --- /dev/null +++ b/main_dc/BB/api_bb/migrations/embed.go @@ -0,0 +1,6 @@ +package migrations + +import "embed" + +//go:embed *.sql +var FS embed.FS diff --git a/main_dc/Makefile b/main_dc/Makefile index 7597b0c..395790f 100644 --- a/main_dc/Makefile +++ b/main_dc/Makefile @@ -209,22 +209,6 @@ vue_site: valitovgaziz_build_spa stop_valitovgaziz build_valitovgaziz start_vali wn: watch -n 2 'docker ps' -# Остановка api_tp -stop_api_tp: - docker compose down api_tp - -# Пересборка api_tp -build_api_tp: - docker compose build api_tp --no-cache - -# Запуск api_tp -start_api_tp: - docker compose up api_tp -d - -# Полный цикл обновления api_tp -api_tp: stop_api_tp git build_api_tp start_api_tp wn - - # Остановка api_yal stop_api_yal: docker compose down api_yal diff --git a/main_dc/docker-compose.yml b/main_dc/docker-compose.yml index 1b28958..eed0637 100644 --- a/main_dc/docker-compose.yml +++ b/main_dc/docker-compose.yml @@ -49,7 +49,6 @@ services: - web-network - internal - app-network - - bb-network depends_on: easysite: condition: service_healthy @@ -140,7 +139,7 @@ services: timeout: 10s retries: 5 -# REST API on Golang (Gorm, Chi) логика обработки информации для сайта БегущийБашкир Работает с БД db_bb on PostgresQL +# REST API on Golang (Gorm, Chi) логика обработки информации для сайта БегущийБашкир api_bb: build: context: ./BB/api_bb @@ -150,22 +149,22 @@ services: container_name: api_bb restart: unless-stopped depends_on: - db_bb: + db: condition: service_healthy env_file: - ./BB/api_bb/.env volumes: - api_bb_uploads:/app/uploads environment: - # Database connection settings - DB_HOST: db_bb + DB_HOST: db DB_PORT: 5432 DB_USER: postgres DB_PASSWORD: postgres DB_NAME: bb_db + DB_SCHEMA: bb APP_PORT: 8080 networks: - - bb-network + - app-network healthcheck: test: [ @@ -180,27 +179,6 @@ services: timeout: 10s retries: 3 -# PostgresQL DB база данных для работы сайта Бегущий Башкир - db_bb: - image: postgres:15-alpine - restart: unless-stopped - ports: - - "5433:5432" - container_name: db_bb - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: bb_db - volumes: - - db_bb_data:/var/lib/postgresql/data - networks: - - bb-network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 30s - timeout: 10s - retries: 5 - # SPA app прилжение выполнено на nuxt.js интерфейс для туристического бизнеса. Хранение информации в api_yal REST API app easysite: build: @@ -280,7 +258,6 @@ volumes: certbot_data: # volume для данных Certbot certbot_www: # volume для данных Certbot db_tp_data: # Volume для данных БД yalarba.ru - db_bb_data: # Volume для данных БД Бегущий башкир api_bb_uploads: # Volume для загружаемых файлов бегущий башкир analytics_logs: # Volume для логов аналитики analytics_data: # Volume для данных аналитики @@ -292,8 +269,6 @@ networks: driver: bridge app-network: driver: bridge - bb-network: - driver: bridge # Эта опция автоматически удаляет orphans (Не используемые контейнеры) x-remove-orphans: true \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/.env b/main_dc/yalarba/api_yal/.env index fedbc98..d60e3f1 100644 --- a/main_dc/yalarba/api_yal/.env +++ b/main_dc/yalarba/api_yal/.env @@ -3,7 +3,7 @@ DB_HOST=db_tp DB_PORT=5432 DB_USER=postgres DB_PASSWORD=postgres -DB_NAME=db_yal +DB_NAME=mydb APP_PORT=8787 JWT_SECRET=secret UPLOAD_PATH=./storage/uploads diff --git a/main_dc/yalarba/api_yal/go.mod b/main_dc/yalarba/api_yal/go.mod index 998a5d5..9c135b1 100644 --- a/main_dc/yalarba/api_yal/go.mod +++ b/main_dc/yalarba/api_yal/go.mod @@ -5,6 +5,7 @@ go 1.25.0 require ( github.com/go-chi/chi/v5 v5.2.5 github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/golang-migrate/migrate/v4 v4.18.2 golang.org/x/crypto v0.49.0 gorm.io/gorm v1.31.1 ) @@ -13,12 +14,16 @@ require ( github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // 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/leodido/go-urn v1.4.0 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect + go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect diff --git a/main_dc/yalarba/api_yal/internal/database/psql_db.go b/main_dc/yalarba/api_yal/internal/database/psql_db.go index 8d0f86a..2afe57d 100644 --- a/main_dc/yalarba/api_yal/internal/database/psql_db.go +++ b/main_dc/yalarba/api_yal/internal/database/psql_db.go @@ -2,13 +2,19 @@ package database import ( "api_yal/internal/config" - "api_yal/internal/models" - "api_yal/internal/logger" + "api_yal/migrations" + "database/sql" "fmt" + "time" + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database/postgres" + "github.com/golang-migrate/migrate/v4/source/iofs" "go.uber.org/zap" "gorm.io/driver/postgres" "gorm.io/gorm" + + "api_yal/internal/logger" ) func NewPostgresConnection(cfg *config.Config) (*gorm.DB, error) { @@ -23,45 +29,47 @@ func NewPostgresConnection(cfg *config.Config) (*gorm.DB, error) { return nil, fmt.Errorf("failed to connect to database: %w", err) } - zapLogger.Info("AutoMigrate models") - // Автомиграция - if err := autoMigrate(db); err != nil { - zapLogger.Error("can't migrate models, error = %s", zap.Error(err)) - return nil, fmt.Errorf("can't migrate models, error = %s", err) + zapLogger.Info("Configure connection pool") + sqlDB, err := db.DB() + if err != nil { + return nil, fmt.Errorf("failed to get underlying sql.DB: %w", err) } - zapLogger.Info("Migrate complite successfully") + sqlDB.SetMaxOpenConns(25) + sqlDB.SetMaxIdleConns(10) + sqlDB.SetConnMaxLifetime(30 * time.Minute) - zapLogger.Info("Fill init data") + zapLogger.Info("Run database migrations") + if err := runMigrations(sqlDB); err != nil { + return nil, fmt.Errorf("failed to run migrations: %w", err) + } + zapLogger.Info("Migrations completed successfully") - zapLogger.Info("Successfully connected to database") + zapLogger.Info("Successfully connected to database") return db, nil } -func autoMigrate(db *gorm.DB) error { - zapLogger := logger.Get() - zapLogger.Debug("Start migration") - models := []interface{}{ - &models.Account{}, - &models.UpdateHistory{}, - &models.Object{}, - &models.ObjectImage{}, - &models.Amenity{}, - &models.RatingVote{}, - &models.VoteBreakdown{}, - &models.Rating{}, - &models.Feedback{}, - &models.Comment{}, - &models.Appeal{}, - &models.AppealHistory{}, - &models.PasswordReset{}, +func runMigrations(sqlDB *sql.DB) error { + zapLogger := logger.Get() + + source, err := iofs.New(migrations.FS, ".") + if err != nil { + return fmt.Errorf("failed to create migration source: %w", err) } - for _, model := range models { - if err := db.AutoMigrate(model); err != nil { - return fmt.Errorf("failed to migrate %T: %w", model, err) - } + driver, err := postgres.WithInstance(sqlDB, &postgres.Config{}) + if err != nil { + return fmt.Errorf("failed to create postgres driver: %w", err) + } + + m, err := migrate.NewWithInstance("iofs", source, "postgres", driver) + if err != nil { + return fmt.Errorf("failed to create migrate instance: %w", err) + } + + if err := m.Up(); err != nil && err != migrate.ErrNoChange { + zapLogger.Error("Migration error", zap.Error(err)) + return fmt.Errorf("failed to apply migrations: %w", err) } - zapLogger.Debug("End migration seccessfully") return nil } diff --git a/main_dc/yalarba/api_yal/migrations/000001_init.down.sql b/main_dc/yalarba/api_yal/migrations/000001_init.down.sql new file mode 100644 index 0000000..c7a3a95 --- /dev/null +++ b/main_dc/yalarba/api_yal/migrations/000001_init.down.sql @@ -0,0 +1,14 @@ +DROP TABLE IF EXISTS password_resets CASCADE; +DROP TABLE IF EXISTS appeal_histories CASCADE; +DROP TABLE IF EXISTS appeals CASCADE; +DROP TABLE IF EXISTS comments CASCADE; +DROP TABLE IF EXISTS feedbacks CASCADE; +DROP TABLE IF EXISTS rating_votes CASCADE; +DROP TABLE IF EXISTS vote_breakdowns CASCADE; +DROP TABLE IF EXISTS ratings CASCADE; +DROP TABLE IF EXISTS object_amenities CASCADE; +DROP TABLE IF EXISTS amenities CASCADE; +DROP TABLE IF EXISTS object_images CASCADE; +DROP TABLE IF EXISTS objects CASCADE; +DROP TABLE IF EXISTS update_histories CASCADE; +DROP TABLE IF EXISTS accounts CASCADE; diff --git a/main_dc/yalarba/api_yal/migrations/000001_init.up.sql b/main_dc/yalarba/api_yal/migrations/000001_init.up.sql new file mode 100644 index 0000000..dc1732f --- /dev/null +++ b/main_dc/yalarba/api_yal/migrations/000001_init.up.sql @@ -0,0 +1,227 @@ +CREATE TABLE IF NOT EXISTS accounts ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ, + deleted_at TIMESTAMPTZ, + email VARCHAR(255) NOT NULL, + password_hash VARCHAR(255) NOT NULL, + full_name VARCHAR(255) NOT NULL DEFAULT 'Unknown', + first_name VARCHAR(255) NOT NULL DEFAULT 'FirstName', + last_name VARCHAR(255) NOT NULL DEFAULT 'LastName', + phone VARCHAR(255), + city VARCHAR(255), + organization_form VARCHAR(255), + organization_name VARCHAR(255), + organization_short VARCHAR(255), + inn VARCHAR(255), + personal_inn VARCHAR(255), + is_active BOOLEAN NOT NULL DEFAULT TRUE, + is_verified BOOLEAN NOT NULL DEFAULT FALSE, + role VARCHAR(255) NOT NULL DEFAULT 'user' +); +CREATE INDEX IF NOT EXISTS idx_accounts_deleted_at ON accounts(deleted_at); +CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_email ON accounts(email); + +CREATE TABLE IF NOT EXISTS update_histories ( + id BIGSERIAL PRIMARY KEY, + model_id BIGINT, + model_type VARCHAR(255), + timestamp TIMESTAMPTZ, + updated_by BIGINT +); +CREATE INDEX IF NOT EXISTS idx_update_histories_updated_by ON update_histories(updated_by); + +CREATE TABLE IF NOT EXISTS objects ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ, + deleted_at TIMESTAMPTZ, + owner_id BIGINT, + title VARCHAR(255) DEFAULT '', + short_name VARCHAR(255) NOT NULL, + long_name VARCHAR(255), + type VARCHAR(255), + price DECIMAL DEFAULT 0, + price_period VARCHAR(255) DEFAULT 'per_unit', + phone VARCHAR(255), + email VARCHAR(255), + site VARCHAR(255), + short_description VARCHAR(255), + description VARCHAR(255), + address VARCHAR(255), + latitude DECIMAL, + longitude DECIMAL, + is_active BOOLEAN DEFAULT TRUE, + is_verified BOOLEAN DEFAULT FALSE, + status VARCHAR(255) DEFAULT 'active', + view_count BIGINT DEFAULT 0, + feedback_count BIGINT DEFAULT 0 +); +CREATE INDEX IF NOT EXISTS idx_objects_deleted_at ON objects(deleted_at); +CREATE INDEX IF NOT EXISTS idx_objects_is_active ON objects(is_active); + +CREATE TABLE IF NOT EXISTS object_images ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ, + deleted_at TIMESTAMPTZ, + object_id BIGINT NOT NULL, + url VARCHAR(255) NOT NULL, + is_primary BOOLEAN DEFAULT FALSE, + sort_order BIGINT DEFAULT 0 +); +CREATE INDEX IF NOT EXISTS idx_object_images_deleted_at ON object_images(deleted_at); +CREATE INDEX IF NOT EXISTS idx_object_images_object_id ON object_images(object_id); + +CREATE TABLE IF NOT EXISTS amenities ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + category VARCHAR(255), + icon VARCHAR(255), + description VARCHAR(255) +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_amenities_name ON amenities(name); + +CREATE TABLE IF NOT EXISTS object_amenities ( + object_id BIGINT NOT NULL, + amenity_id BIGINT NOT NULL, + PRIMARY KEY (object_id, amenity_id) +); + +CREATE TABLE IF NOT EXISTS ratings ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ, + deleted_at TIMESTAMPTZ, + owner_id BIGINT, + object_id BIGINT, + platform VARCHAR(255), + average_score DECIMAL, + total_votes BIGINT DEFAULT 0 +); +CREATE INDEX IF NOT EXISTS idx_ratings_deleted_at ON ratings(deleted_at); + +CREATE TABLE IF NOT EXISTS vote_breakdowns ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ, + deleted_at TIMESTAMPTZ, + rating_id BIGINT, + score_1 BIGINT DEFAULT 0, + score_2 BIGINT DEFAULT 0, + score_3 BIGINT DEFAULT 0, + score_4 BIGINT DEFAULT 0, + score_5 BIGINT DEFAULT 0 +); +CREATE INDEX IF NOT EXISTS idx_vote_breakdowns_deleted_at ON vote_breakdowns(deleted_at); + +CREATE TABLE IF NOT EXISTS rating_votes ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ, + deleted_at TIMESTAMPTZ, + platform VARCHAR(255), + target_id BIGINT, + voter_id BIGINT, + score BIGINT +); +CREATE INDEX IF NOT EXISTS idx_rating_votes_deleted_at ON rating_votes(deleted_at); + +CREATE TABLE IF NOT EXISTS feedbacks ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ, + deleted_at TIMESTAMPTZ, + owner_id BIGINT NOT NULL, + object_id BIGINT NOT NULL, + platform VARCHAR(255), + score BIGINT, + comment_count BIGINT DEFAULT 0, + text VARCHAR(255) +); +CREATE INDEX IF NOT EXISTS idx_feedbacks_deleted_at ON feedbacks(deleted_at); +CREATE INDEX IF NOT EXISTS idx_feedbacks_owner_id ON feedbacks(owner_id); +CREATE INDEX IF NOT EXISTS idx_feedbacks_object_id ON feedbacks(object_id); + +CREATE TABLE IF NOT EXISTS comments ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ, + deleted_at TIMESTAMPTZ, + author_id BIGINT NOT NULL, + feedback_id BIGINT NOT NULL, + text VARCHAR(255) NOT NULL, + parent_id BIGINT, + is_edited BOOLEAN DEFAULT FALSE, + is_verified BOOLEAN DEFAULT FALSE +); +CREATE INDEX IF NOT EXISTS idx_comments_deleted_at ON comments(deleted_at); +CREATE INDEX IF NOT EXISTS idx_comments_author_id ON comments(author_id); +CREATE INDEX IF NOT EXISTS idx_comments_feedback_id ON comments(feedback_id); +CREATE INDEX IF NOT EXISTS idx_comments_parent_id ON comments(parent_id); + +CREATE TABLE IF NOT EXISTS appeals ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ, + deleted_at TIMESTAMPTZ, + author_id BIGINT, + type VARCHAR(255) NOT NULL, + status VARCHAR(255) NOT NULL DEFAULT 'new', + priority VARCHAR(255) NOT NULL DEFAULT 'medium', + title VARCHAR(255) NOT NULL, + message TEXT NOT NULL, + attachments TEXT[], + contact_name VARCHAR(255), + contact_email VARCHAR(255), + contact_phone VARCHAR(255), + object_id BIGINT, + feedback_id BIGINT, + comment_id BIGINT, + ip_address VARCHAR(255), + user_agent VARCHAR(255), + assigned_to_id BIGINT, + resolved_at TIMESTAMPTZ, + resolved_by BIGINT, + resolution VARCHAR(255), + category VARCHAR(255), + labels TEXT[], + custom_data JSONB +); +CREATE INDEX IF NOT EXISTS idx_appeals_deleted_at ON appeals(deleted_at); +CREATE INDEX IF NOT EXISTS idx_appeals_author_id ON appeals(author_id); +CREATE INDEX IF NOT EXISTS idx_appeals_type ON appeals(type); +CREATE INDEX IF NOT EXISTS idx_appeals_status ON appeals(status); +CREATE INDEX IF NOT EXISTS idx_appeals_priority ON appeals(priority); +CREATE INDEX IF NOT EXISTS idx_appeals_object_id ON appeals(object_id); +CREATE INDEX IF NOT EXISTS idx_appeals_feedback_id ON appeals(feedback_id); +CREATE INDEX IF NOT EXISTS idx_appeals_comment_id ON appeals(comment_id); +CREATE INDEX IF NOT EXISTS idx_appeals_assigned_to_id ON appeals(assigned_to_id); + +CREATE TABLE IF NOT EXISTS appeal_histories ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ, + deleted_at TIMESTAMPTZ, + appeal_id BIGINT NOT NULL, + user_id BIGINT, + old_status VARCHAR(255), + new_status VARCHAR(255), + comment VARCHAR(255) +); +CREATE INDEX IF NOT EXISTS idx_appeal_histories_deleted_at ON appeal_histories(deleted_at); +CREATE INDEX IF NOT EXISTS idx_appeal_histories_appeal_id ON appeal_histories(appeal_id); + +CREATE TABLE IF NOT EXISTS password_resets ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ, + deleted_at TIMESTAMPTZ, + account_id BIGINT NOT NULL, + token VARCHAR(255) NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + used BOOLEAN DEFAULT FALSE +); +CREATE INDEX IF NOT EXISTS idx_password_resets_deleted_at ON password_resets(deleted_at); +CREATE INDEX IF NOT EXISTS idx_password_resets_account_id ON password_resets(account_id); +CREATE UNIQUE INDEX IF NOT EXISTS idx_password_resets_token ON password_resets(token); diff --git a/main_dc/yalarba/api_yal/migrations/000002_cleanup_es.down.sql b/main_dc/yalarba/api_yal/migrations/000002_cleanup_es.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/main_dc/yalarba/api_yal/migrations/000002_cleanup_es.up.sql b/main_dc/yalarba/api_yal/migrations/000002_cleanup_es.up.sql new file mode 100644 index 0000000..7d8a3c7 --- /dev/null +++ b/main_dc/yalarba/api_yal/migrations/000002_cleanup_es.up.sql @@ -0,0 +1,6 @@ +DROP TABLE IF EXISTS users CASCADE; +DROP TABLE IF EXISTS reviews CASCADE; +DROP TABLE IF EXISTS news CASCADE; +DROP TABLE IF EXISTS authentications CASCADE; +DROP TABLE IF EXISTS filters CASCADE; +DROP TABLE IF EXISTS reports CASCADE; diff --git a/main_dc/yalarba/api_yal/migrations/embed.go b/main_dc/yalarba/api_yal/migrations/embed.go new file mode 100644 index 0000000..91cca1c --- /dev/null +++ b/main_dc/yalarba/api_yal/migrations/embed.go @@ -0,0 +1,6 @@ +package migrations + +import "embed" + +//go:embed *.sql +var FS embed.FS