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:
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -17,26 +23,34 @@ 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=") {
|
||||||
@@ -136,4 +180,4 @@ func MaskPassword(dsn string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return strings.Join(parts, " ")
|
return strings.Join(parts, " ")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed *.sql
|
||||||
|
var FS embed.FS
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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,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
|
||||||
Reference in New Issue
Block a user