modified: begushiybashkir/bbvue/src/router/index.js
modified: begushiybashkir/bbvue/src/views/Login.vue new file: begushiybashkir/bbvue/src/views/Logout.vue modified: begushiybashkir/bbvue/src/views/News.vue modified: begushiybashkir/bbvue/src/views/Profile.vue modified: serv_nginx/api_bb/cmd/main.go modified: serv_nginx/api_bb/go.mod modified: serv_nginx/api_bb/go.sum new file: serv_nginx/api_bb/internal/app/app.go new file: serv_nginx/api_bb/internal/database/database.go new file: serv_nginx/api_bb/internal/database/migrate.go new file: serv_nginx/api_bb/internal/handlers/news_handler.go new file: serv_nginx/api_bb/internal/models/news.go new file: serv_nginx/api_bb/internal/repository/comment_repository.go new file: serv_nginx/api_bb/internal/repository/news_repository.go modified: serv_nginx/api_bb/internal/routes/routes.go new file: serv_nginx/api_bb/internal/service/news_service.go modified: serv_nginx/api_bb/pkg/utils/utils.go save router paths to login logout profile from upsunction commit
This commit is contained in:
+17
-104
@@ -2,29 +2,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"api_bb/internal/app"
|
||||
"api_bb/internal/config"
|
||||
"api_bb/pkg/logger"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"api_bb/internal/config"
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/routes"
|
||||
"api_bb/pkg/logger"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
// Загрузка конфигурации
|
||||
cfg := config.Load()
|
||||
|
||||
|
||||
// Инициализация логгера
|
||||
if err := logger.Init(
|
||||
os.Getenv("LOG_LEVEL"),
|
||||
@@ -35,58 +28,13 @@ func main() {
|
||||
}
|
||||
defer logger.Sync()
|
||||
|
||||
zapLogger := logger.Get()
|
||||
|
||||
// Логируем начало работы
|
||||
logger.LogApplicationStart(os.Getenv("REST_API_VERSION"), os.Getenv("ENVIRONMENT"), "")
|
||||
|
||||
// Логирование попытки подключения к БД
|
||||
zapLogger.Info("attempting to connect to database",
|
||||
zap.String("host", extractHostFromDSN(cfg.DatabaseURL)), // функция для извлечения хоста из DSN
|
||||
zap.String("database", extractDBNameFromDSN(cfg.DatabaseURL)), // функция для извлечения имени БД
|
||||
)
|
||||
|
||||
// Подключение к базе данных
|
||||
db, err := gorm.Open(postgres.Open(cfg.DatabaseURL), &gorm.Config{})
|
||||
if err != nil {
|
||||
zapLogger.Fatal("failed to connect to database",
|
||||
zap.Error(err),
|
||||
zap.String("database_url", maskPassword(cfg.DatabaseURL)), // маскируем пароль в логах
|
||||
)
|
||||
}
|
||||
|
||||
// Логирование успешного подключения к БД
|
||||
zapLogger.Info("successfully connected to database",
|
||||
zap.String("host", extractHostFromDSN(cfg.DatabaseURL)),
|
||||
zap.String("database", extractDBNameFromDSN(cfg.DatabaseURL)),
|
||||
)
|
||||
|
||||
// Проверка соединения с БД
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
zapLogger.Fatal("failed to get database instance", zap.Error(err))
|
||||
}
|
||||
|
||||
if err := sqlDB.Ping(); err != nil {
|
||||
zapLogger.Fatal("database ping failed", zap.Error(err))
|
||||
}
|
||||
|
||||
zapLogger.Info("database ping successful")
|
||||
|
||||
// Автомиграция
|
||||
zapLogger.Info("starting database migration")
|
||||
if err := db.AutoMigrate(&models.User{}); err != nil {
|
||||
zapLogger.Fatal("database migration failed", zap.Error(err))
|
||||
}
|
||||
zapLogger.Info("database migration completed successfully")
|
||||
|
||||
// Настройка роутера
|
||||
router := routes.SetupRouter(db, cfg)
|
||||
|
||||
// Настройка HTTP сервера
|
||||
server := &http.Server{
|
||||
Addr: ":" + cfg.Port,
|
||||
Handler: router,
|
||||
// Создание и инициализация приложения
|
||||
application := app.NewApp(cfg)
|
||||
if err := application.Initialize(); err != nil {
|
||||
logger.Get().Fatal("failed to initialize application", zap.Error(err))
|
||||
}
|
||||
|
||||
// Канал для graceful shutdown
|
||||
@@ -96,56 +44,21 @@ func main() {
|
||||
|
||||
// Запуск сервера в горутине
|
||||
go func() {
|
||||
zapLogger.Info("starting HTTP server", zap.String("port", cfg.Port))
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
zapLogger.Fatal("failed to start server", zap.Error(err))
|
||||
if err := application.Start(); err != nil {
|
||||
logger.Get().Fatal("failed to start server", zap.Error(err))
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
// Ожидание сигнала shutdown
|
||||
<-quit
|
||||
zapLogger.Info("shutdown signal received")
|
||||
logger.Get().Info("shutdown signal received")
|
||||
|
||||
// Логирование закрытия соединения с БД
|
||||
zapLogger.Info("closing database connection")
|
||||
if err := sqlDB.Close(); err != nil {
|
||||
zapLogger.Error("failed to close database connection", zap.Error(err))
|
||||
} else {
|
||||
zapLogger.Info("database connection closed successfully")
|
||||
}
|
||||
|
||||
// Graceful shutdown
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
server.SetKeepAlivesEnabled(false)
|
||||
if err := server.Shutdown(ctx); err != nil {
|
||||
zapLogger.Fatal("could not gracefully shutdown the server", zap.Error(err))
|
||||
// Graceful shutdown приложения
|
||||
if err := application.Shutdown(); err != nil {
|
||||
logger.Get().Fatal("could not gracefully shutdown the application", zap.Error(err))
|
||||
}
|
||||
|
||||
logger.LogApplicationShutdown("graceful shutdown")
|
||||
close(done)
|
||||
}
|
||||
|
||||
// Вспомогательные функции для работы с DSN
|
||||
|
||||
// extractHostFromDSN извлекает хост из DSN строки
|
||||
func extractHostFromDSN(dsn string) string {
|
||||
// Простая реализация - в продакшене лучше использовать парсер DSN
|
||||
// Для postgres DSN формата: "host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai"
|
||||
// Можно использовать более сложный парсер или регулярные выражения
|
||||
return dsn // Заглушка - нужно реализовать парсинг DSN
|
||||
}
|
||||
|
||||
// extractDBNameFromDSN извлекает имя базы данных из DSN строки
|
||||
func extractDBNameFromDSN(dsn string) string {
|
||||
// Аналогично extractHostFromDSN - нужно реализовать парсинг
|
||||
return dsn // Заглушка - нужно реализовать парсинг DSN
|
||||
}
|
||||
|
||||
// maskPassword маскирует пароль в DSN строке для безопасного логирования
|
||||
func maskPassword(dsn string) string {
|
||||
// Простая реализация - заменяет пароль на ***
|
||||
// В продакшене нужно использовать более надежный метод
|
||||
return dsn // Заглушка - нужно реализовать маскирование пароля
|
||||
<-done
|
||||
}
|
||||
@@ -11,9 +11,17 @@ require (
|
||||
gorm.io/gorm v1.31.0
|
||||
)
|
||||
|
||||
require go.uber.org/multierr v1.10.0 // indirect
|
||||
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/leodido/go-urn v1.4.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-playground/validator/v10 v10.28.0
|
||||
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
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
||||
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
@@ -21,6 +29,8 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -36,6 +46,8 @@ golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"api_bb/internal/config"
|
||||
"api_bb/internal/database"
|
||||
"api_bb/internal/routes"
|
||||
"api_bb/pkg/logger"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
cfg *config.Config
|
||||
db *database.Database
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
func NewApp(cfg *config.Config) *App {
|
||||
return &App{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize инициализирует приложение (БД, миграции, роутинг)
|
||||
func (a *App) Initialize() error {
|
||||
zapLogger := logger.Get()
|
||||
|
||||
// Инициализация базы данных
|
||||
dbConfig := &database.Config{
|
||||
URL: a.cfg.DatabaseURL,
|
||||
}
|
||||
a.db = database.NewDatabase(dbConfig)
|
||||
|
||||
// Подключение к БД
|
||||
if err := a.db.Connect(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Проверка соединения
|
||||
if err := a.db.Ping(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Выполнение миграций
|
||||
if err := a.db.Migrate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Настройка роутера
|
||||
router := routes.SetupRouter(a.db.DB, a.cfg)
|
||||
|
||||
// Настройка HTTP сервера
|
||||
a.server = &http.Server{
|
||||
Addr: ":" + a.cfg.Port,
|
||||
Handler: router,
|
||||
}
|
||||
|
||||
zapLogger.Info("application initialized successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start запускает HTTP сервер
|
||||
func (a *App) Start() error {
|
||||
zapLogger := logger.Get()
|
||||
|
||||
zapLogger.Info("starting HTTP server", zap.String("port", a.cfg.Port))
|
||||
|
||||
if err := a.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown gracefully останавливает приложение
|
||||
func (a *App) Shutdown() error {
|
||||
zapLogger := logger.Get()
|
||||
|
||||
zapLogger.Info("shutdown signal received")
|
||||
|
||||
// Graceful shutdown сервера
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
a.server.SetKeepAlivesEnabled(false)
|
||||
if err := a.server.Shutdown(ctx); err != nil {
|
||||
zapLogger.Error("could not gracefully shutdown the server", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// Закрытие соединения с БД
|
||||
if err := a.db.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
zapLogger.Info("application shutdown completed")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDB возвращает экземпляр базы данных
|
||||
func (a *App) GetDB() *gorm.DB {
|
||||
return a.db.DB
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"api_bb/pkg/logger"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
DB *gorm.DB
|
||||
cfg *Config
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
func NewDatabase(cfg *Config) *Database {
|
||||
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)),
|
||||
)
|
||||
|
||||
db, err := gorm.Open(postgres.Open(d.cfg.URL), &gorm.Config{})
|
||||
if err != nil {
|
||||
zapLogger.Error("failed to connect to database",
|
||||
zap.Error(err),
|
||||
zap.String("database_url", MaskPassword(d.cfg.URL)),
|
||||
)
|
||||
return fmt.Errorf("failed to connect to database: %w", err)
|
||||
}
|
||||
|
||||
d.DB = db
|
||||
|
||||
// Логирование успешного подключения к БД
|
||||
zapLogger.Info("successfully connected to database",
|
||||
zap.String("host", ExtractHostFromDSN(d.cfg.URL)),
|
||||
zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ping проверяет соединение с базой данных
|
||||
func (d *Database) Ping() error {
|
||||
zapLogger := logger.Get()
|
||||
|
||||
sqlDB, err := d.DB.DB()
|
||||
if err != nil {
|
||||
zapLogger.Error("failed to get database instance", zap.Error(err))
|
||||
return fmt.Errorf("failed to get database instance: %w", err)
|
||||
}
|
||||
|
||||
if err := sqlDB.Ping(); err != nil {
|
||||
zapLogger.Error("database ping failed", zap.Error(err))
|
||||
return fmt.Errorf("database ping failed: %w", err)
|
||||
}
|
||||
|
||||
zapLogger.Info("database ping successful")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close закрывает соединение с базой данных
|
||||
func (d *Database) Close() error {
|
||||
zapLogger := logger.Get()
|
||||
|
||||
if d.DB == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
sqlDB, err := d.DB.DB()
|
||||
if err != nil {
|
||||
zapLogger.Error("failed to get database instance for closing", zap.Error(err))
|
||||
return fmt.Errorf("failed to get database instance: %w", err)
|
||||
}
|
||||
|
||||
zapLogger.Info("closing database connection")
|
||||
if err := sqlDB.Close(); err != nil {
|
||||
zapLogger.Error("failed to close database connection", zap.Error(err))
|
||||
return fmt.Errorf("failed to close database connection: %w", err)
|
||||
}
|
||||
|
||||
zapLogger.Info("database connection closed successfully")
|
||||
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=") {
|
||||
return strings.TrimPrefix(part, "host=")
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// ExtractDBNameFromDSN извлекает имя базы данных из DSN строки
|
||||
func ExtractDBNameFromDSN(dsn string) string {
|
||||
// Простая реализация для PostgreSQL DSN
|
||||
parts := strings.Split(dsn, " ")
|
||||
for _, part := range parts {
|
||||
if strings.HasPrefix(part, "dbname=") {
|
||||
return strings.TrimPrefix(part, "dbname=")
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// MaskPassword маскирует пароль в DSN строке для безопасного логирования
|
||||
func MaskPassword(dsn string) string {
|
||||
// Простая реализация - заменяет пароль на ***
|
||||
parts := strings.Split(dsn, " ")
|
||||
for i, part := range parts {
|
||||
if strings.HasPrefix(part, "password=") {
|
||||
parts[i] = "password=***"
|
||||
break
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
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{},
|
||||
// Добавьте другие модели здесь
|
||||
}
|
||||
|
||||
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"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/service"
|
||||
"api_bb/pkg/logger"
|
||||
"api_bb/pkg/utils"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type NewsHandler struct {
|
||||
newsService service.NewsService
|
||||
logger logger.Interface
|
||||
validator *validator.Validate
|
||||
}
|
||||
|
||||
func NewNewsHandler(newsService service.NewsService, log logger.Interface) *NewsHandler {
|
||||
return &NewsHandler{
|
||||
newsService: newsService,
|
||||
logger: log,
|
||||
validator: validator.New(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetNews возвращает список новостей с пагинацией и фильтрацией
|
||||
func (h *NewsHandler) GetNews(w http.ResponseWriter, r *http.Request) {
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
||||
category := r.URL.Query().Get("category")
|
||||
|
||||
if limit == 0 {
|
||||
limit = 10
|
||||
}
|
||||
if limit > 50 {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
news, total, err := h.newsService.GetAllNews(limit, offset, category)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get news", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get news")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"news": news,
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
}
|
||||
|
||||
// GetNewsByID возвращает конкретную новость
|
||||
func (h *NewsHandler) GetNewsByID(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := chi.URLParam(r, "id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
|
||||
return
|
||||
}
|
||||
|
||||
news, err := h.newsService.GetNewsByID(uint(id))
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusNotFound, "News not found")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, news)
|
||||
}
|
||||
|
||||
// CreateNews создает новую новость
|
||||
func (h *NewsHandler) CreateNews(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := r.Context().Value("userID").(uint)
|
||||
if !ok {
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.CreateNewsRequest
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.validator.Struct(req); err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
news, err := h.newsService.CreateNews(req, userID)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to create news", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create news")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusCreated, news)
|
||||
}
|
||||
|
||||
// UpdateNews обновляет новость
|
||||
func (h *NewsHandler) UpdateNews(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := r.Context().Value("userID").(uint)
|
||||
if !ok {
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
idStr := chi.URLParam(r, "id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.UpdateNewsRequest
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.validator.Struct(req); err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
news, err := h.newsService.UpdateNews(uint(id), req, userID)
|
||||
if err != nil {
|
||||
if err.Error() == "access denied" {
|
||||
utils.RespondWithError(w, http.StatusForbidden, "Access denied")
|
||||
return
|
||||
}
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update news")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, news)
|
||||
}
|
||||
|
||||
// DeleteNews удаляет новость
|
||||
func (h *NewsHandler) DeleteNews(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := r.Context().Value("userID").(uint)
|
||||
if !ok {
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
idStr := chi.URLParam(r, "id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.newsService.DeleteNews(uint(id), userID)
|
||||
if err != nil {
|
||||
if err.Error() == "access denied" {
|
||||
utils.RespondWithError(w, http.StatusForbidden, "Access denied")
|
||||
return
|
||||
}
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete news")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{"message": "News deleted successfully"})
|
||||
}
|
||||
|
||||
// CreateComment создает комментарий к новости
|
||||
func (h *NewsHandler) CreateComment(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := r.Context().Value("userID").(uint)
|
||||
if !ok {
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
newsIDStr := chi.URLParam(r, "id")
|
||||
newsID, err := strconv.ParseUint(newsIDStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.CreateCommentRequest
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.validator.Struct(req); err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
comment, err := h.newsService.CreateComment(uint(newsID), req, userID)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create comment")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusCreated, comment)
|
||||
}
|
||||
|
||||
// GetComments возвращает комментарии к новости
|
||||
func (h *NewsHandler) GetComments(w http.ResponseWriter, r *http.Request) {
|
||||
newsIDStr := chi.URLParam(r, "id")
|
||||
newsID, err := strconv.ParseUint(newsIDStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
|
||||
return
|
||||
}
|
||||
|
||||
comments, err := h.newsService.GetCommentsByNewsID(uint(newsID))
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get comments")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, comments)
|
||||
}
|
||||
|
||||
// DeleteComment удаляет комментарий
|
||||
func (h *NewsHandler) DeleteComment(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := r.Context().Value("userID").(uint)
|
||||
if !ok {
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
commentIDStr := chi.URLParam(r, "commentId")
|
||||
commentID, err := strconv.ParseUint(commentIDStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid comment ID")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.newsService.DeleteComment(uint(commentID), userID)
|
||||
if err != nil {
|
||||
if err.Error() == "access denied" {
|
||||
utils.RespondWithError(w, http.StatusForbidden, "Access denied")
|
||||
return
|
||||
}
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete comment")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{"message": "Comment deleted successfully"})
|
||||
}
|
||||
|
||||
// GetUserNews возвращает новости конкретного пользователя
|
||||
func (h *NewsHandler) GetUserNews(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := r.Context().Value("userID").(uint)
|
||||
if !ok {
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
||||
|
||||
if limit == 0 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
news, total, err := h.newsService.GetUserNews(userID, limit, offset)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get user news")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"news": news,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type NewsCategory string
|
||||
|
||||
const (
|
||||
NewsCategoryEvents NewsCategory = "events"
|
||||
NewsCategoryTraining NewsCategory = "training"
|
||||
NewsCategoryAchievements NewsCategory = "achievements"
|
||||
NewsCategoryCommunity NewsCategory = "community"
|
||||
)
|
||||
|
||||
type News struct {
|
||||
ID uint `json:"id" gorm:"primarykey"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"`
|
||||
|
||||
Title string `json:"title" gorm:"size:255;not null"`
|
||||
Excerpt string `json:"excerpt" gorm:"size:500;not null"`
|
||||
Content string `json:"content" gorm:"type:text;not null"`
|
||||
Image string `json:"image" gorm:"size:255"`
|
||||
Category NewsCategory `json:"category" gorm:"type:varchar(20);not null"`
|
||||
Views int `json:"views" gorm:"default:0"`
|
||||
|
||||
// Связи
|
||||
AuthorID uint `json:"author_id" gorm:"not null"`
|
||||
Author User `json:"author" gorm:"foreignKey:AuthorID"`
|
||||
|
||||
Comments []Comment `json:"comments,omitempty" gorm:"foreignKey:NewsID"`
|
||||
}
|
||||
|
||||
type Comment struct {
|
||||
ID uint `json:"id" gorm:"primarykey"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
Content string `json:"content" gorm:"type:text;not null"`
|
||||
|
||||
// Связи
|
||||
NewsID uint `json:"news_id" gorm:"not null"`
|
||||
AuthorID uint `json:"author_id" gorm:"not null"`
|
||||
Author User `json:"author" gorm:"foreignKey:AuthorID"`
|
||||
}
|
||||
|
||||
// DTO для создания новости
|
||||
type CreateNewsRequest struct {
|
||||
Title string `json:"title" validate:"required,min=5,max=255"`
|
||||
Excerpt string `json:"excerpt" validate:"required,min=10,max=500"`
|
||||
Content string `json:"content" validate:"required,min=50"`
|
||||
Image string `json:"image"`
|
||||
Category NewsCategory `json:"category" validate:"required,oneof=events training achievements community"`
|
||||
}
|
||||
|
||||
// DTO для обновления новости
|
||||
type UpdateNewsRequest struct {
|
||||
Title string `json:"title" validate:"omitempty,min=5,max=255"`
|
||||
Excerpt string `json:"excerpt" validate:"omitempty,min=10,max=500"`
|
||||
Content string `json:"content" validate:"omitempty,min=50"`
|
||||
Image string `json:"image"`
|
||||
Category NewsCategory `json:"category" validate:"omitempty,oneof=events training achievements community"`
|
||||
}
|
||||
|
||||
// DTO для ответа с новостью
|
||||
type NewsResponse struct {
|
||||
ID uint `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Title string `json:"title"`
|
||||
Excerpt string `json:"excerpt"`
|
||||
Content string `json:"content"`
|
||||
Image string `json:"image"`
|
||||
Category NewsCategory `json:"category"`
|
||||
Views int `json:"views"`
|
||||
Author AuthorInfo `json:"author"`
|
||||
Comments int `json:"comments_count"`
|
||||
}
|
||||
|
||||
type AuthorInfo struct {
|
||||
ID uint `json:"id"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Email string `json:"email,omitempty"`
|
||||
}
|
||||
|
||||
// DTO для комментария
|
||||
type CreateCommentRequest struct {
|
||||
Content string `json:"content" validate:"required,min=1,max=1000"`
|
||||
}
|
||||
|
||||
type CommentResponse struct {
|
||||
ID uint `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Content string `json:"content"`
|
||||
Author AuthorInfo `json:"author"`
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type CommentRepository interface {
|
||||
Create(comment *models.Comment) error
|
||||
GetByNewsID(newsID uint) ([]models.Comment, error)
|
||||
Delete(id uint) error
|
||||
GetByID(id uint) (*models.Comment, error)
|
||||
}
|
||||
|
||||
type commentRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewCommentRepository(db *gorm.DB) CommentRepository {
|
||||
return &commentRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *commentRepository) Create(comment *models.Comment) error {
|
||||
return r.db.Create(comment).Error
|
||||
}
|
||||
|
||||
func (r *commentRepository) GetByNewsID(newsID uint) ([]models.Comment, error) {
|
||||
var comments []models.Comment
|
||||
err := r.db.Preload("Author").Where("news_id = ?", newsID).
|
||||
Order("created_at ASC").Find(&comments).Error
|
||||
return comments, err
|
||||
}
|
||||
|
||||
func (r *commentRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&models.Comment{}, id).Error
|
||||
}
|
||||
|
||||
func (r *commentRepository) GetByID(id uint) (*models.Comment, error) {
|
||||
var comment models.Comment
|
||||
err := r.db.Preload("Author").Where("id = ?", id).First(&comment).Error
|
||||
return &comment, err
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type NewsRepository interface {
|
||||
Create(news *models.News) error
|
||||
GetByID(id uint) (*models.News, error)
|
||||
GetAll(limit, offset int, category string) ([]models.News, int64, error)
|
||||
Update(news *models.News) error
|
||||
Delete(id uint) error
|
||||
IncrementViews(id uint) error
|
||||
GetByAuthor(authorID uint, limit, offset int) ([]models.News, int64, error)
|
||||
}
|
||||
|
||||
type newsRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewNewsRepository(db *gorm.DB) NewsRepository {
|
||||
return &newsRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *newsRepository) Create(news *models.News) error {
|
||||
return r.db.Create(news).Error
|
||||
}
|
||||
|
||||
func (r *newsRepository) GetByID(id uint) (*models.News, error) {
|
||||
var news models.News
|
||||
err := r.db.Preload("Author").Preload("Comments.Author").
|
||||
Where("id = ?", id).First(&news).Error
|
||||
return &news, err
|
||||
}
|
||||
|
||||
func (r *newsRepository) GetAll(limit, offset int, category string) ([]models.News, int64, error) {
|
||||
var news []models.News
|
||||
var total int64
|
||||
|
||||
query := r.db.Preload("Author")
|
||||
|
||||
if category != "" && category != "all" {
|
||||
query = query.Where("category = ?", category)
|
||||
}
|
||||
|
||||
// Получаем общее количество
|
||||
if err := query.Model(&models.News{}).Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Получаем данные с пагинацией
|
||||
err := query.Order("created_at DESC").
|
||||
Limit(limit).Offset(offset).
|
||||
Find(&news).Error
|
||||
|
||||
return news, total, err
|
||||
}
|
||||
|
||||
func (r *newsRepository) Update(news *models.News) error {
|
||||
return r.db.Save(news).Error
|
||||
}
|
||||
|
||||
func (r *newsRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&models.News{}, id).Error
|
||||
}
|
||||
|
||||
func (r *newsRepository) IncrementViews(id uint) error {
|
||||
return r.db.Model(&models.News{}).Where("id = ?", id).
|
||||
Update("views", gorm.Expr("views + ?", 1)).Error
|
||||
}
|
||||
|
||||
func (r *newsRepository) GetByAuthor(authorID uint, limit, offset int) ([]models.News, int64, error) {
|
||||
var news []models.News
|
||||
var total int64
|
||||
|
||||
query := r.db.Preload("Author").Where("author_id = ?", authorID)
|
||||
|
||||
if err := query.Model(&models.News{}).Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
err := query.Order("created_at DESC").
|
||||
Limit(limit).Offset(offset).
|
||||
Find(&news).Error
|
||||
|
||||
return news, total, err
|
||||
}
|
||||
@@ -25,6 +25,8 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
|
||||
|
||||
// Initialize repositories
|
||||
userRepo := repository.NewUserRepository(db)
|
||||
newsRepo := repository.NewNewsRepository(db)
|
||||
commentRepo := repository.NewCommentRepository(db)
|
||||
|
||||
// Initialize logger
|
||||
baseLogger := logger.NewWrapper(logger.Get()) // Создаем базовый логгер
|
||||
@@ -33,11 +35,13 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
|
||||
jwtService := service.NewJWTService(config.JWTSecret)
|
||||
authService := service.NewAuthService(userRepo, jwtService, baseLogger) // Передаем логгер
|
||||
userService := service.NewUserService(userRepo, jwtService, baseLogger)
|
||||
newsService := service.NewNewsService(newsRepo, commentRepo, baseLogger)
|
||||
|
||||
// Initialize handlers
|
||||
healthHandler := handlers.NewHealthHandler()
|
||||
authHandler := handlers.NewAuthHandler(authService, jwtService)
|
||||
userHandler := handlers.NewUserHandler(&userService)
|
||||
newsHandler := handlers.NewNewsHandler(newsService, baseLogger)
|
||||
|
||||
// Health routes
|
||||
r.Mount("/api", healthHandler.Routes())
|
||||
@@ -58,6 +62,29 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
|
||||
// Здесь будут другие защищенные маршруты пользователя
|
||||
})
|
||||
|
||||
r.Route("/news", func(r chi.Router) {
|
||||
|
||||
// Публичные маршруты
|
||||
r.Get("/", newsHandler.GetNews)
|
||||
r.Get("/{id}", newsHandler.GetNewsByID)
|
||||
r.Get("/{id}/comments", newsHandler.GetComments)
|
||||
r.Get("/check", healthHandler.Check)
|
||||
|
||||
// Защищенные маршруты
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.AuthMiddleware(jwtService, userRepo))
|
||||
r.Use(middleware.RequireAuth)
|
||||
|
||||
r.Post("/", newsHandler.CreateNews)
|
||||
r.Put("/{id}", newsHandler.UpdateNews)
|
||||
r.Delete("/{id}", newsHandler.DeleteNews)
|
||||
r.Post("/{id}/comments", newsHandler.CreateComment)
|
||||
r.Delete("/comments/{commentId}", newsHandler.DeleteComment)
|
||||
r.Get("/my/news", newsHandler.GetUserNews)
|
||||
r.Get("/check", healthHandler.Check)
|
||||
})
|
||||
})
|
||||
|
||||
// Здесь будут добавлены другие маршруты:
|
||||
// r.Mount("/events", eventHandler.Routes())
|
||||
// r.Mount("/reviews", reviewHandler.Routes())
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/repository"
|
||||
"api_bb/pkg/logger"
|
||||
"errors"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type NewsService interface {
|
||||
CreateNews(req models.CreateNewsRequest, authorID uint) (*models.NewsResponse, error)
|
||||
GetNewsByID(id uint) (*models.NewsResponse, error)
|
||||
GetAllNews(limit, offset int, category string) ([]models.NewsResponse, int64, error)
|
||||
UpdateNews(id uint, req models.UpdateNewsRequest, userID uint) (*models.NewsResponse, error)
|
||||
DeleteNews(id uint, userID uint) error
|
||||
IncrementViews(id uint) error
|
||||
CreateComment(newsID uint, req models.CreateCommentRequest, authorID uint) (*models.CommentResponse, error)
|
||||
GetCommentsByNewsID(newsID uint) ([]models.CommentResponse, error)
|
||||
DeleteComment(commentID, userID uint) error
|
||||
GetUserNews(userID uint, limit, offset int) ([]models.NewsResponse, int64, error)
|
||||
}
|
||||
|
||||
type newsService struct {
|
||||
newsRepo repository.NewsRepository
|
||||
commentRepo repository.CommentRepository
|
||||
logger logger.Interface
|
||||
}
|
||||
|
||||
func NewNewsService(newsRepo repository.NewsRepository, commentRepo repository.CommentRepository, log logger.Interface) NewsService {
|
||||
|
||||
|
||||
serviceLogger := log.With(zap.String("service", "news"))
|
||||
|
||||
return &newsService{
|
||||
newsRepo: newsRepo,
|
||||
commentRepo: commentRepo,
|
||||
logger: serviceLogger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *newsService) CreateNews(req models.CreateNewsRequest, authorID uint) (*models.NewsResponse, error) {
|
||||
news := &models.News{
|
||||
Title: req.Title,
|
||||
Excerpt: req.Excerpt,
|
||||
Content: req.Content,
|
||||
Image: req.Image,
|
||||
Category: req.Category,
|
||||
AuthorID: authorID,
|
||||
}
|
||||
|
||||
if err := s.newsRepo.Create(news); err != nil {
|
||||
s.logger.Error("Failed to create news", zap.Error(err))
|
||||
return nil, errors.New("failed to create news")
|
||||
}
|
||||
|
||||
// Получаем созданную новость с автором
|
||||
createdNews, err := s.newsRepo.GetByID(news.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.toNewsResponse(createdNews), nil
|
||||
}
|
||||
|
||||
func (s *newsService) GetNewsByID(id uint) (*models.NewsResponse, error) {
|
||||
news, err := s.newsRepo.GetByID(id)
|
||||
if err != nil {
|
||||
return nil, errors.New("news not found")
|
||||
}
|
||||
|
||||
// Увеличиваем счетчик просмотров
|
||||
go s.newsRepo.IncrementViews(id)
|
||||
|
||||
return s.toNewsResponse(news), nil
|
||||
}
|
||||
|
||||
func (s *newsService) GetAllNews(limit, offset int, category string) ([]models.NewsResponse, int64, error) {
|
||||
news, total, err := s.newsRepo.GetAll(limit, offset, category)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
responses := make([]models.NewsResponse, len(news))
|
||||
for i, n := range news {
|
||||
responses[i] = *s.toNewsResponse(&n)
|
||||
}
|
||||
|
||||
return responses, total, nil
|
||||
}
|
||||
|
||||
func (s *newsService) UpdateNews(id uint, req models.UpdateNewsRequest, userID uint) (*models.NewsResponse, error) {
|
||||
news, err := s.newsRepo.GetByID(id)
|
||||
if err != nil {
|
||||
return nil, errors.New("news not found")
|
||||
}
|
||||
|
||||
// Проверяем права доступа
|
||||
if news.AuthorID != userID {
|
||||
return nil, errors.New("access denied")
|
||||
}
|
||||
|
||||
// Обновляем поля
|
||||
if req.Title != "" {
|
||||
news.Title = req.Title
|
||||
}
|
||||
if req.Excerpt != "" {
|
||||
news.Excerpt = req.Excerpt
|
||||
}
|
||||
if req.Content != "" {
|
||||
news.Content = req.Content
|
||||
}
|
||||
if req.Image != "" {
|
||||
news.Image = req.Image
|
||||
}
|
||||
if req.Category != "" {
|
||||
news.Category = req.Category
|
||||
}
|
||||
|
||||
if err := s.newsRepo.Update(news); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.toNewsResponse(news), nil
|
||||
}
|
||||
|
||||
func (s *newsService) DeleteNews(id uint, userID uint) error {
|
||||
news, err := s.newsRepo.GetByID(id)
|
||||
if err != nil {
|
||||
return errors.New("news not found")
|
||||
}
|
||||
|
||||
// Проверяем права доступа
|
||||
if news.AuthorID != userID {
|
||||
return errors.New("access denied")
|
||||
}
|
||||
|
||||
return s.newsRepo.Delete(id)
|
||||
}
|
||||
|
||||
func (s *newsService) IncrementViews(id uint) error {
|
||||
return s.newsRepo.IncrementViews(id)
|
||||
}
|
||||
|
||||
func (s *newsService) CreateComment(newsID uint, req models.CreateCommentRequest, authorID uint) (*models.CommentResponse, error) {
|
||||
// Проверяем существование новости
|
||||
_, err := s.newsRepo.GetByID(newsID)
|
||||
if err != nil {
|
||||
return nil, errors.New("news not found")
|
||||
}
|
||||
|
||||
comment := &models.Comment{
|
||||
Content: req.Content,
|
||||
NewsID: newsID,
|
||||
AuthorID: authorID,
|
||||
}
|
||||
|
||||
if err := s.commentRepo.Create(comment); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Получаем созданный комментарий с автором
|
||||
createdComment, err := s.commentRepo.GetByID(comment.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.toCommentResponse(createdComment), nil
|
||||
}
|
||||
|
||||
func (s *newsService) GetCommentsByNewsID(newsID uint) ([]models.CommentResponse, error) {
|
||||
comments, err := s.commentRepo.GetByNewsID(newsID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responses := make([]models.CommentResponse, len(comments))
|
||||
for i, c := range comments {
|
||||
responses[i] = *s.toCommentResponse(&c)
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
func (s *newsService) DeleteComment(commentID, userID uint) error {
|
||||
comment, err := s.commentRepo.GetByID(commentID)
|
||||
if err != nil {
|
||||
return errors.New("comment not found")
|
||||
}
|
||||
|
||||
// Проверяем права доступа
|
||||
if comment.AuthorID != userID {
|
||||
return errors.New("access denied")
|
||||
}
|
||||
|
||||
return s.commentRepo.Delete(commentID)
|
||||
}
|
||||
|
||||
func (s *newsService) GetUserNews(userID uint, limit, offset int) ([]models.NewsResponse, int64, error) {
|
||||
news, total, err := s.newsRepo.GetByAuthor(userID, limit, offset)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
responses := make([]models.NewsResponse, len(news))
|
||||
for i, n := range news {
|
||||
responses[i] = *s.toNewsResponse(&n)
|
||||
}
|
||||
|
||||
return responses, total, nil
|
||||
}
|
||||
|
||||
// Вспомогательные методы для преобразования
|
||||
func (s *newsService) toNewsResponse(news *models.News) *models.NewsResponse {
|
||||
return &models.NewsResponse{
|
||||
ID: news.ID,
|
||||
CreatedAt: news.CreatedAt,
|
||||
UpdatedAt: news.UpdatedAt,
|
||||
Title: news.Title,
|
||||
Excerpt: news.Excerpt,
|
||||
Content: news.Content,
|
||||
Image: news.Image,
|
||||
Category: news.Category,
|
||||
Views: news.Views,
|
||||
Author: models.AuthorInfo{
|
||||
ID: news.Author.ID,
|
||||
FirstName: news.Author.FirstName,
|
||||
LastName: news.Author.LastName,
|
||||
},
|
||||
Comments: len(news.Comments),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *newsService) toCommentResponse(comment *models.Comment) *models.CommentResponse {
|
||||
return &models.CommentResponse{
|
||||
ID: comment.ID,
|
||||
CreatedAt: comment.CreatedAt,
|
||||
Content: comment.Content,
|
||||
Author: models.AuthorInfo{
|
||||
ID: comment.Author.ID,
|
||||
FirstName: comment.Author.FirstName,
|
||||
LastName: comment.Author.LastName,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,11 @@ package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func RespondWithJSON(w http.ResponseWriter, statusCode int, data interface{}) {
|
||||
@@ -13,4 +17,59 @@ func RespondWithJSON(w http.ResponseWriter, statusCode int, data interface{}) {
|
||||
|
||||
func RespondWithError(w http.ResponseWriter, statusCode int, message string) {
|
||||
RespondWithJSON(w, statusCode, map[string]string{"error": message})
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeJSONBody декодирует JSON тело запроса
|
||||
func DecodeJSONBody(w http.ResponseWriter, r *http.Request, dst interface{}) error {
|
||||
if r.Header.Get("Content-Type") != "application/json" {
|
||||
return errors.New("Content-Type header is not application/json")
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 1048576) // 1MB limit
|
||||
|
||||
dec := json.NewDecoder(r.Body)
|
||||
dec.DisallowUnknownFields()
|
||||
|
||||
err := dec.Decode(dst)
|
||||
if err != nil {
|
||||
var syntaxError *json.SyntaxError
|
||||
var unmarshalTypeError *json.UnmarshalTypeError
|
||||
|
||||
switch {
|
||||
case errors.As(err, &syntaxError):
|
||||
return fmt.Errorf("request body contains badly-formed JSON (at position %d)", syntaxError.Offset)
|
||||
|
||||
case errors.Is(err, io.ErrUnexpectedEOF):
|
||||
return errors.New("request body contains badly-formed JSON")
|
||||
|
||||
case errors.As(err, &unmarshalTypeError):
|
||||
return fmt.Errorf("request body contains an invalid value for the %q field (at position %d)", unmarshalTypeError.Field, unmarshalTypeError.Offset)
|
||||
|
||||
case strings.HasPrefix(err.Error(), "json: unknown field "):
|
||||
fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ")
|
||||
return fmt.Errorf("request body contains unknown field %s", fieldName)
|
||||
|
||||
case errors.Is(err, io.EOF):
|
||||
return errors.New("request body must not be empty")
|
||||
|
||||
case err.Error() == "http: request body too large":
|
||||
return errors.New("request body must not be larger than 1MB")
|
||||
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = dec.Decode(&struct{}{})
|
||||
if err != io.EOF {
|
||||
return errors.New("request body must only contain a single JSON object")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserIDFromContext извлекает userID из контекста
|
||||
func GetUserIDFromContext(r *http.Request) (uint, bool) {
|
||||
userID, ok := r.Context().Value("userID").(uint)
|
||||
return userID, ok
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user