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
This commit is contained in:
valitovgaziz
2026-06-12 10:47:41 +05:00
parent ec83b97c25
commit b0350abfbe
18 changed files with 610 additions and 206 deletions
+5
View File
@@ -6,6 +6,7 @@ require (
github.com/go-chi/chi/v5 v5.2.3 github.com/go-chi/chi/v5 v5.2.3
github.com/go-chi/cors v1.2.2 github.com/go-chi/cors v1.2.2
github.com/golang-jwt/jwt/v5 v5.3.0 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 golang.org/x/crypto v0.43.0
gorm.io/driver/postgres v1.6.0 gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.0 gorm.io/gorm v1.31.0
@@ -15,8 +16,12 @@ require (
github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.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/leodido/go-urn v1.4.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/stretchr/testify v1.11.1 // 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 go.uber.org/multierr v1.10.0 // indirect
golang.org/x/sys v0.37.0 // indirect golang.org/x/sys v0.37.0 // indirect
) )
+1 -5
View File
@@ -33,6 +33,7 @@ func (a *App) Initialize() error {
// Инициализация базы данных // Инициализация базы данных
dbConfig := &database.Config{ dbConfig := &database.Config{
URL: a.cfg.DatabaseURL, URL: a.cfg.DatabaseURL,
Schema: a.cfg.DBSchema,
} }
a.db = database.NewDatabase(dbConfig) a.db = database.NewDatabase(dbConfig)
@@ -46,11 +47,6 @@ func (a *App) Initialize() error {
return err return err
} }
// Выполнение миграций
if err := a.db.Migrate(); err != nil {
return err
}
// Настройка роутера // Настройка роутера
router := routes.SetupRouter(a.db.DB, a.cfg) router := routes.SetupRouter(a.db.DB, a.cfg)
@@ -11,6 +11,7 @@ import (
type Config struct { type Config struct {
Port string Port string
DatabaseURL string DatabaseURL string
DBSchema string
StaticURL string `env:"STATIC_URL" envDefault:"http://localhost:8080"` StaticURL string `env:"STATIC_URL" envDefault:"http://localhost:8080"`
JWTSecret string `env:"JWT_SECRET,required"` JWTSecret string `env:"JWT_SECRET,required"`
@@ -34,6 +35,7 @@ func Load() *Config {
return &Config{ return &Config{
Port: port, Port: port,
DatabaseURL: databaseURL, DatabaseURL: databaseURL,
DBSchema: getEnv("DB_SCHEMA", "public"),
JWTSecret: jwtSecret, JWTSecret: jwtSecret,
} }
} }
+58 -14
View File
@@ -1,9 +1,15 @@
package database package database
import ( import (
"api_bb/migrations"
"database/sql"
"fmt" "fmt"
"strings" "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" "go.uber.org/zap"
"gorm.io/driver/postgres" "gorm.io/driver/postgres"
"gorm.io/gorm" "gorm.io/gorm"
@@ -18,25 +24,33 @@ type Database struct {
type Config struct { type Config struct {
URL string URL string
Schema string
} }
func NewDatabase(cfg *Config) *Database { func NewDatabase(cfg *Config) *Database {
if cfg.Schema == "" {
cfg.Schema = "public"
}
return &Database{ return &Database{
cfg: cfg, cfg: cfg,
} }
} }
// Connect устанавливает соединение с базой данных
func (d *Database) Connect() error { func (d *Database) Connect() error {
zapLogger := logger.Get() zapLogger := logger.Get()
// Логирование попытки подключения к БД
zapLogger.Info("attempting to connect to database", zapLogger.Info("attempting to connect to database",
zap.String("host", ExtractHostFromDSN(d.cfg.URL)), zap.String("host", ExtractHostFromDSN(d.cfg.URL)),
zap.String("database", ExtractDBNameFromDSN(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 { if err != nil {
zapLogger.Error("failed to connect to database", zapLogger.Error("failed to connect to database",
zap.Error(err), zap.Error(err),
@@ -47,7 +61,21 @@ func (d *Database) Connect() error {
d.DB = db 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", zapLogger.Info("successfully connected to database",
zap.String("host", ExtractHostFromDSN(d.cfg.URL)), zap.String("host", ExtractHostFromDSN(d.cfg.URL)),
zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)), zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)),
@@ -56,7 +84,32 @@ func (d *Database) Connect() error {
return nil 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 { func (d *Database) Ping() error {
zapLogger := logger.Get() zapLogger := logger.Get()
@@ -75,7 +128,6 @@ func (d *Database) Ping() error {
return nil return nil
} }
// Close закрывает соединение с базой данных
func (d *Database) Close() error { func (d *Database) Close() error {
zapLogger := logger.Get() zapLogger := logger.Get()
@@ -99,11 +151,7 @@ func (d *Database) Close() error {
return nil return nil
} }
// Вспомогательные функции для работы с DSN
// ExtractHostFromDSN извлекает хост из DSN строки
func ExtractHostFromDSN(dsn string) string { func ExtractHostFromDSN(dsn string) string {
// Простая реализация для PostgreSQL DSN
parts := strings.Split(dsn, " ") parts := strings.Split(dsn, " ")
for _, part := range parts { for _, part := range parts {
if strings.HasPrefix(part, "host=") { if strings.HasPrefix(part, "host=") {
@@ -113,9 +161,7 @@ func ExtractHostFromDSN(dsn string) string {
return "unknown" return "unknown"
} }
// ExtractDBNameFromDSN извлекает имя базы данных из DSN строки
func ExtractDBNameFromDSN(dsn string) string { func ExtractDBNameFromDSN(dsn string) string {
// Простая реализация для PostgreSQL DSN
parts := strings.Split(dsn, " ") parts := strings.Split(dsn, " ")
for _, part := range parts { for _, part := range parts {
if strings.HasPrefix(part, "dbname=") { if strings.HasPrefix(part, "dbname=") {
@@ -125,9 +171,7 @@ func ExtractDBNameFromDSN(dsn string) string {
return "unknown" return "unknown"
} }
// MaskPassword маскирует пароль в DSN строке для безопасного логирования
func MaskPassword(dsn string) string { func MaskPassword(dsn string) string {
// Простая реализация - заменяет пароль на ***
parts := strings.Split(dsn, " ") parts := strings.Split(dsn, " ")
for i, part := range parts { for i, part := range parts {
if strings.HasPrefix(part, "password=") { if strings.HasPrefix(part, "password=") {
@@ -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"
}
}
@@ -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;
@@ -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);
+6
View File
@@ -0,0 +1,6 @@
package migrations
import "embed"
//go:embed *.sql
var FS embed.FS
-16
View File
@@ -209,22 +209,6 @@ vue_site: valitovgaziz_build_spa stop_valitovgaziz build_valitovgaziz start_vali
wn: wn:
watch -n 2 'docker ps' 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 # Остановка api_yal
stop_api_yal: stop_api_yal:
docker compose down api_yal docker compose down api_yal
+5 -30
View File
@@ -49,7 +49,6 @@ services:
- web-network - web-network
- internal - internal
- app-network - app-network
- bb-network
depends_on: depends_on:
easysite: easysite:
condition: service_healthy condition: service_healthy
@@ -140,7 +139,7 @@ services:
timeout: 10s timeout: 10s
retries: 5 retries: 5
# REST API on Golang (Gorm, Chi) логика обработки информации для сайта БегущийБашкир Работает с БД db_bb on PostgresQL # REST API on Golang (Gorm, Chi) логика обработки информации для сайта БегущийБашкир
api_bb: api_bb:
build: build:
context: ./BB/api_bb context: ./BB/api_bb
@@ -150,22 +149,22 @@ services:
container_name: api_bb container_name: api_bb
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
db_bb: db:
condition: service_healthy condition: service_healthy
env_file: env_file:
- ./BB/api_bb/.env - ./BB/api_bb/.env
volumes: volumes:
- api_bb_uploads:/app/uploads - api_bb_uploads:/app/uploads
environment: environment:
# Database connection settings DB_HOST: db
DB_HOST: db_bb
DB_PORT: 5432 DB_PORT: 5432
DB_USER: postgres DB_USER: postgres
DB_PASSWORD: postgres DB_PASSWORD: postgres
DB_NAME: bb_db DB_NAME: bb_db
DB_SCHEMA: bb
APP_PORT: 8080 APP_PORT: 8080
networks: networks:
- bb-network - app-network
healthcheck: healthcheck:
test: test:
[ [
@@ -180,27 +179,6 @@ services:
timeout: 10s timeout: 10s
retries: 3 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 # SPA app прилжение выполнено на nuxt.js интерфейс для туристического бизнеса. Хранение информации в api_yal REST API app
easysite: easysite:
build: build:
@@ -280,7 +258,6 @@ volumes:
certbot_data: # volume для данных Certbot certbot_data: # volume для данных Certbot
certbot_www: # volume для данных Certbot certbot_www: # volume для данных Certbot
db_tp_data: # Volume для данных БД yalarba.ru db_tp_data: # Volume для данных БД yalarba.ru
db_bb_data: # Volume для данных БД Бегущий башкир
api_bb_uploads: # Volume для загружаемых файлов бегущий башкир api_bb_uploads: # Volume для загружаемых файлов бегущий башкир
analytics_logs: # Volume для логов аналитики analytics_logs: # Volume для логов аналитики
analytics_data: # Volume для данных аналитики analytics_data: # Volume для данных аналитики
@@ -292,8 +269,6 @@ networks:
driver: bridge driver: bridge
app-network: app-network:
driver: bridge driver: bridge
bb-network:
driver: bridge
# Эта опция автоматически удаляет orphans (Не используемые контейнеры) # Эта опция автоматически удаляет orphans (Не используемые контейнеры)
x-remove-orphans: true x-remove-orphans: true
+1 -1
View File
@@ -3,7 +3,7 @@ DB_HOST=db_tp
DB_PORT=5432 DB_PORT=5432
DB_USER=postgres DB_USER=postgres
DB_PASSWORD=postgres DB_PASSWORD=postgres
DB_NAME=db_yal DB_NAME=mydb
APP_PORT=8787 APP_PORT=8787
JWT_SECRET=secret JWT_SECRET=secret
UPLOAD_PATH=./storage/uploads UPLOAD_PATH=./storage/uploads
+5
View File
@@ -5,6 +5,7 @@ go 1.25.0
require ( require (
github.com/go-chi/chi/v5 v5.2.5 github.com/go-chi/chi/v5 v5.2.5
github.com/golang-jwt/jwt/v5 v5.3.1 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 golang.org/x/crypto v0.49.0
gorm.io/gorm v1.31.1 gorm.io/gorm v1.31.1
) )
@@ -13,12 +14,16 @@ require (
github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.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/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/leodido/go-urn v1.4.0 // 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 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 go.uber.org/multierr v1.10.0 // indirect
golang.org/x/sync v0.20.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect golang.org/x/sys v0.42.0 // indirect
@@ -2,13 +2,19 @@ package database
import ( import (
"api_yal/internal/config" "api_yal/internal/config"
"api_yal/internal/models" "api_yal/migrations"
"api_yal/internal/logger" "database/sql"
"fmt" "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" "go.uber.org/zap"
"gorm.io/driver/postgres" "gorm.io/driver/postgres"
"gorm.io/gorm" "gorm.io/gorm"
"api_yal/internal/logger"
) )
func NewPostgresConnection(cfg *config.Config) (*gorm.DB, error) { 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) return nil, fmt.Errorf("failed to connect to database: %w", err)
} }
zapLogger.Info("AutoMigrate models") zapLogger.Info("Configure connection pool")
// Автомиграция sqlDB, err := db.DB()
if err := autoMigrate(db); err != nil { if err != nil {
zapLogger.Error("can't migrate models, error = %s", zap.Error(err)) return nil, fmt.Errorf("failed to get underlying sql.DB: %w", err)
return nil, fmt.Errorf("can't migrate models, error = %s", 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 return db, nil
} }
func autoMigrate(db *gorm.DB) error { func runMigrations(sqlDB *sql.DB) error {
zapLogger := logger.Get() zapLogger := logger.Get()
zapLogger.Debug("Start migration")
models := []interface{}{ source, err := iofs.New(migrations.FS, ".")
&models.Account{}, if err != nil {
&models.UpdateHistory{}, return fmt.Errorf("failed to create migration source: %w", err)
&models.Object{},
&models.ObjectImage{},
&models.Amenity{},
&models.RatingVote{},
&models.VoteBreakdown{},
&models.Rating{},
&models.Feedback{},
&models.Comment{},
&models.Appeal{},
&models.AppealHistory{},
&models.PasswordReset{},
} }
for _, model := range models { driver, err := postgres.WithInstance(sqlDB, &postgres.Config{})
if err := db.AutoMigrate(model); err != nil { if err != nil {
return fmt.Errorf("failed to migrate %T: %w", model, err) 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 return nil
} }
@@ -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;
@@ -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);
@@ -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;
@@ -0,0 +1,6 @@
package migrations
import "embed"
//go:embed *.sql
var FS embed.FS