modified: README.md

modified:   serv_nginx/api_bb/go.mod
	modified:   serv_nginx/api_bb/go.sum
	new file:   serv_nginx/api_bb/internal/models/achievement.go
	new file:   serv_nginx/api_bb/internal/models/common.go
	new file:   serv_nginx/api_bb/internal/models/event.go
	new file:   serv_nginx/api_bb/internal/models/gallery.go
	modified:   serv_nginx/api_bb/internal/models/news.go
	new file:   serv_nginx/api_bb/internal/models/personal_best.go
	new file:   serv_nginx/api_bb/internal/models/training_plan.go
	modified:   serv_nginx/api_bb/internal/models/user.go
	new file:   serv_nginx/api_bb/internal/models/user_stats.go
	modified:   serv_nginx/api_bb/internal/models/workout.go
	modified:   serv_nginx/bbvue/src/components/NavigationMenu.vue
	new file:   serv_nginx/bbvue/src/components/writeLogo.vue
add satructs for begushiybashkir.ru site
This commit is contained in:
2025-10-17 05:09:53 +05:00
parent 280d9a0eb3
commit b19ce8fdfe
15 changed files with 741 additions and 50 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
## for yalarba.ru && begushiybashkir.ru ## for yalarba.ru && begushiybashkir.ru
### tasks: profile page need full upgreade ### tasks: profile page need full upgreade, main sender (почтовая рассылка)
### BackEnd REST API on Golang 1.25.1 ### BackEnd REST API on Golang 1.25.1
### FrontEnd vue3.js ### FrontEnd vue3.js
### Zagir Загир тренер ### Zagir Загир тренер
+5
View File
@@ -12,13 +12,17 @@ require (
) )
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect
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/go-sql-driver/mysql v1.8.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/stretchr/testify v1.11.1 // indirect github.com/stretchr/testify v1.11.1 // 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
gorm.io/driver/mysql v1.5.6 // indirect
) )
require ( require (
@@ -33,4 +37,5 @@ require (
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
golang.org/x/sync v0.17.0 // indirect golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.30.0 // indirect golang.org/x/text v0.30.0 // indirect
gorm.io/datatypes v1.2.7
) )
+12
View File
@@ -1,3 +1,5 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -15,8 +17,13 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 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 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= 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/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -58,7 +65,12 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
@@ -0,0 +1,72 @@
// models/achievement.go
package models
import (
"time"
"gorm.io/gorm"
)
type AchievementType string
const (
AchievementTypeDistance AchievementType = "distance"
AchievementTypeSpeed AchievementType = "speed"
AchievementTypeConsistency AchievementType = "consistency"
AchievementTypeEvent AchievementType = "event"
AchievementTypeSpecial AchievementType = "special"
)
type Achievement struct {
ID uint `json:"id" gorm:"primaryKey"`
UserID uint `json:"user_id" gorm:"not null;index"`
Type AchievementType `json:"type" gorm:"type:varchar(20);not null"`
Title string `json:"title" gorm:"size:255;not null"`
Description string `json:"description" gorm:"type:text"`
Result string `json:"result" gorm:"size:100"` // Достигнутый результат
Distance string `json:"distance" gorm:"size:50"` // Дистанция достижения
Date time.Time `json:"date" gorm:"not null"`
Verified bool `json:"verified" gorm:"default:false"`
BadgeImage string `json:"badge_image" gorm:"size:500"` // Изображение бейджа
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Связи
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
}
// BeforeCreate hook
func (a *Achievement) BeforeCreate(tx *gorm.DB) error {
if a.CreatedAt.IsZero() {
a.CreatedAt = time.Now()
}
if a.UpdatedAt.IsZero() {
a.UpdatedAt = time.Now()
}
return nil
}
// BeforeUpdate hook
func (a *Achievement) BeforeUpdate(tx *gorm.DB) error {
a.UpdatedAt = time.Now()
return nil
}
// DTO для создания достижения
type AchievementCreateRequest struct {
Type AchievementType `json:"type" validate:"required,oneof=distance speed consistency event special"`
Title string `json:"title" validate:"required,min=5,max=255"`
Description string `json:"description" validate:"max=1000"`
Result string `json:"result" validate:"max=100"`
Distance string `json:"distance" validate:"max=50"`
Date time.Time `json:"date" validate:"required"`
BadgeImage string `json:"badge_image" validate:"max=500"`
}
// DTO для ответа с достижениями пользователя
type UserAchievementsResponse struct {
TotalAchievements int `json:"total_achievements"`
Completed int `json:"completed"`
ProgressPercent float64 `json:"progress_percent"`
Achievements []Achievement `json:"achievements"`
}
@@ -0,0 +1,38 @@
// models/common.go
package models
import "time"
// Общая структура для информации об авторе
type AuthorInfo struct {
ID uint `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Avatar string `json:"avatar,omitempty"`
Email string `json:"email,omitempty"` // Добавляем email
}
// DTO для пагинации
type PaginationRequest struct {
Page int `form:"page" validate:"min=1" default:"1"`
PerPage int `form:"per_page" validate:"min=1,max=100" default:"10"`
}
type PaginationResponse struct {
Page int `json:"page"`
PerPage int `json:"per_page"`
Total int `json:"total"`
TotalPages int `json:"total_pages"`
}
// DTO для фильтров
type DateRangeFilter struct {
StartDate *time.Time `form:"start_date"`
EndDate *time.Time `form:"end_date"`
}
type WorkoutFilter struct {
DateRangeFilter
Type string `form:"type"`
UserID uint `form:"user_id"`
}
+101
View File
@@ -0,0 +1,101 @@
// models/event.go
package models
import (
"time"
"gorm.io/gorm"
)
type EventType string
const (
EventTypeRace EventType = "race"
EventTypeTraining EventType = "training"
EventTypeSocial EventType = "social"
EventTypeWorkshop EventType = "workshop"
)
type Event struct {
ID uint `json:"id" gorm:"primaryKey"`
Title string `json:"title" gorm:"size:255;not null"`
Description string `json:"description" gorm:"type:text;not null"`
Date time.Time `json:"date" gorm:"not null;index"`
Location string `json:"location" gorm:"size:255;not null"`
Type EventType `json:"type" gorm:"type:varchar(20);not null"`
Distance string `json:"distance" gorm:"size:50"` // Дистанция забега
ParticipantsCount int `json:"participants_count" gorm:"default:0"` // Количество участников
MaxParticipants int `json:"max_participants" gorm:"default:0"` // Максимальное количество участников
RegistrationOpen bool `json:"registration_open" gorm:"default:true"` // Открыта ли регистрация
Image string `json:"image" gorm:"size:500"` // Изображение события
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Связи
Registrations []EventRegistration `json:"registrations,omitempty" gorm:"foreignKey:EventID"`
}
type EventRegistration struct {
ID uint `json:"id" gorm:"primaryKey"`
EventID uint `json:"event_id" gorm:"not null;index"`
UserID uint `json:"user_id" gorm:"not null;index"`
Status string `json:"status" gorm:"size:20;default:pending"` // pending, confirmed, cancelled, completed
ResultTime *string `json:"result_time" gorm:"size:20"` // Результат забега
BibNumber *string `json:"bib_number" gorm:"size:10"` // Стартовый номер
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Связи
Event Event `json:"event,omitempty" gorm:"foreignKey:EventID"`
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
}
// BeforeCreate hooks
func (e *Event) BeforeCreate(tx *gorm.DB) error {
if e.CreatedAt.IsZero() {
e.CreatedAt = time.Now()
}
if e.UpdatedAt.IsZero() {
e.UpdatedAt = time.Now()
}
return nil
}
func (er *EventRegistration) BeforeCreate(tx *gorm.DB) error {
if er.CreatedAt.IsZero() {
er.CreatedAt = time.Now()
}
if er.UpdatedAt.IsZero() {
er.UpdatedAt = time.Now()
}
return nil
}
// BeforeUpdate hooks
func (e *Event) BeforeUpdate(tx *gorm.DB) error {
e.UpdatedAt = time.Now()
return nil
}
func (er *EventRegistration) BeforeUpdate(tx *gorm.DB) error {
er.UpdatedAt = time.Now()
return nil
}
// DTO для создания события
type EventCreateRequest struct {
Title string `json:"title" validate:"required,min=5,max=255"`
Description string `json:"description" validate:"required,min=10"`
Date time.Time `json:"date" validate:"required"`
Location string `json:"location" validate:"required,max=255"`
Type EventType `json:"type" validate:"required,oneof=race training social workshop"`
Distance string `json:"distance" validate:"max=50"`
MaxParticipants int `json:"max_participants" validate:"min=0"`
RegistrationOpen bool `json:"registration_open"`
Image string `json:"image" validate:"max=500"`
}
// DTO для регистрации на событие
type EventRegistrationRequest struct {
EventID uint `json:"event_id" validate:"required"`
}
@@ -0,0 +1,74 @@
// models/gallery.go
package models
import (
"time"
"gorm.io/gorm"
)
type GalleryCategory string
const (
GalleryCategoryTraining GalleryCategory = "training"
GalleryCategoryEvents GalleryCategory = "events"
GalleryCategoryCommunity GalleryCategory = "community"
GalleryCategoryAchievements GalleryCategory = "achievements"
)
type Gallery struct {
ID uint `json:"id" gorm:"primaryKey"`
Title string `json:"title" gorm:"size:255;not null"`
Description string `json:"description" gorm:"type:text"`
ImagePath string `json:"image_path" gorm:"size:500;not null"` // Путь к изображению
Category GalleryCategory `json:"category" gorm:"type:varchar(20);not null"`
AuthorID uint `json:"author_id" gorm:"not null;index"`
EventDate *time.Time `json:"event_date"` // Дата события на фото
Views int `json:"views" gorm:"default:0"`
Likes int `json:"likes" gorm:"default:0"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Связи
Author User `json:"author,omitempty" gorm:"foreignKey:AuthorID"`
}
// BeforeCreate hook
func (g *Gallery) BeforeCreate(tx *gorm.DB) error {
if g.CreatedAt.IsZero() {
g.CreatedAt = time.Now()
}
if g.UpdatedAt.IsZero() {
g.UpdatedAt = time.Now()
}
return nil
}
// BeforeUpdate hook
func (g *Gallery) BeforeUpdate(tx *gorm.DB) error {
g.UpdatedAt = time.Now()
return nil
}
// DTO для создания записи в галерее
type GalleryCreateRequest struct {
Title string `json:"title" validate:"required,min=5,max=255"`
Description string `json:"description" validate:"max=1000"`
ImagePath string `json:"image_path" validate:"required,max=500"`
Category GalleryCategory `json:"category" validate:"required,oneof=training events community achievements"`
EventDate *time.Time `json:"event_date"`
}
// DTO для ответа с галереей
type GalleryResponse struct {
ID uint `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
ImagePath string `json:"image_path"`
Category GalleryCategory `json:"category"`
EventDate *time.Time `json:"event_date"`
Views int `json:"views"`
Likes int `json:"likes"`
CreatedAt time.Time `json:"created_at"`
Author AuthorInfo `json:"author"`
}
@@ -80,13 +80,6 @@ type NewsResponse struct {
Comments int `json:"comments_count"` 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 для комментария // DTO для комментария
type CreateCommentRequest struct { type CreateCommentRequest struct {
Content string `json:"content" validate:"required,min=1,max=1000"` Content string `json:"content" validate:"required,min=1,max=1000"`
@@ -0,0 +1,73 @@
// models/personal_best.go
package models
import (
"time"
"gorm.io/gorm"
)
type DistanceType string
const (
Distance5K DistanceType = "5k"
Distance10K DistanceType = "10k"
DistanceHalf DistanceType = "half_marathon"
DistanceFull DistanceType = "marathon"
DistanceOther DistanceType = "other"
)
type PersonalBest struct {
ID uint `json:"id" gorm:"primaryKey"`
UserID uint `json:"user_id" gorm:"not null;index"`
DistanceType DistanceType `json:"distance_type" gorm:"type:varchar(20);not null"`
Time string `json:"time" gorm:"size:20;not null"` // Время в формате "HH:MM:SS"
Pace string `json:"pace" gorm:"size:20"` // Темп
Date time.Time `json:"date" gorm:"not null"`
Verified bool `json:"verified" gorm:"default:false"` // Подтвержден ли результат
EventName string `json:"event_name" gorm:"size:255"` // Название забега
Location string `json:"location" gorm:"size:255"` // Место проведения
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Связи
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
}
// BeforeCreate hook
func (pb *PersonalBest) BeforeCreate(tx *gorm.DB) error {
if pb.CreatedAt.IsZero() {
pb.CreatedAt = time.Now()
}
if pb.UpdatedAt.IsZero() {
pb.UpdatedAt = time.Now()
}
return nil
}
// BeforeUpdate hook
func (pb *PersonalBest) BeforeUpdate(tx *gorm.DB) error {
pb.UpdatedAt = time.Now()
return nil
}
// DTO для создания личного рекорда
type PersonalBestCreateRequest struct {
DistanceType DistanceType `json:"distance_type" validate:"required,oneof=5k 10k half_marathon marathon other"`
Time string `json:"time" validate:"required,max=20"`
Pace string `json:"pace" validate:"max=20"`
Date time.Time `json:"date" validate:"required"`
EventName string `json:"event_name" validate:"max=255"`
Location string `json:"location" validate:"max=255"`
}
// DTO для обновления личного рекорда
type PersonalBestUpdateRequest struct {
DistanceType DistanceType `json:"distance_type" validate:"omitempty,oneof=5k 10k half_marathon marathon other"`
Time string `json:"time" validate:"omitempty,max=20"`
Pace string `json:"pace" validate:"omitempty,max=20"`
Date time.Time `json:"date"`
EventName string `json:"event_name" validate:"omitempty,max=255"`
Location string `json:"location" validate:"omitempty,max=255"`
Verified bool `json:"verified"`
}
@@ -0,0 +1,75 @@
// models/training_plan.go
package models
import (
"time"
"gorm.io/gorm"
)
type TrainingPlan struct {
ID uint `json:"id" gorm:"primaryKey"`
UserID uint `json:"user_id" gorm:"not null;index"`
Title string `json:"title" gorm:"size:255;not null"`
Description string `json:"description" gorm:"type:text"`
Weeks int `json:"weeks" gorm:"not null;default:12"` // Длительность плана в неделях
WorkoutsPerWeek int `json:"workouts_per_week" gorm:"not null;default:3"` // Тренировок в неделю
TargetDistance string `json:"target_distance" gorm:"size:50"` // Целевая дистанция
TargetDate time.Time `json:"target_date"` // Дата цели
CurrentWeek int `json:"current_week" gorm:"default:1"` // Текущая неделя
Completed bool `json:"completed" gorm:"default:false"` // Завершен ли план
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Связи
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
Workouts []TrainingWorkout `json:"workouts,omitempty" gorm:"foreignKey:PlanID"`
}
type TrainingWorkout struct {
ID uint `json:"id" gorm:"primaryKey"`
PlanID uint `json:"plan_id" gorm:"not null;index"`
Week int `json:"week" gorm:"not null"` // Неделя плана
Day int `json:"day" gorm:"not null"` // День недели (1-7)
Type WorkoutType `json:"type" gorm:"type:varchar(20);not null"`
Description string `json:"description" gorm:"type:text"`
Distance float64 `json:"distance_km" gorm:"type:decimal(5,2)"`
Duration int `json:"duration_min"`
Completed bool `json:"completed" gorm:"default:false"`
CompletedAt *time.Time `json:"completed_at"`
CreatedAt time.Time `json:"created_at"`
}
// BeforeCreate hooks
func (tp *TrainingPlan) BeforeCreate(tx *gorm.DB) error {
if tp.CreatedAt.IsZero() {
tp.CreatedAt = time.Now()
}
if tp.UpdatedAt.IsZero() {
tp.UpdatedAt = time.Now()
}
return nil
}
func (tw *TrainingWorkout) BeforeCreate(tx *gorm.DB) error {
if tw.CreatedAt.IsZero() {
tw.CreatedAt = time.Now()
}
return nil
}
// BeforeUpdate hook
func (tp *TrainingPlan) BeforeUpdate(tx *gorm.DB) error {
tp.UpdatedAt = time.Now()
return nil
}
// DTO для создания плана тренировок
type TrainingPlanCreateRequest struct {
Title string `json:"title" validate:"required,min=5,max=255"`
Description string `json:"description" validate:"max=1000"`
Weeks int `json:"weeks" validate:"required,min=1,max=52"`
WorkoutsPerWeek int `json:"workouts_per_week" validate:"required,min=1,max=7"`
TargetDistance string `json:"target_distance" validate:"max=50"`
TargetDate time.Time `json:"target_date"`
}
+65 -23
View File
@@ -10,32 +10,43 @@ import (
// models/user.go - добавить поле Avatar // models/user.go - добавить поле Avatar
type User struct { type User struct {
ID uint `json:"id" gorm:"primaryKey"` ID uint `json:"id" gorm:"primaryKey"`
Email string `json:"email" gorm:"uniqueIndex;not null"` Email string `json:"email" gorm:"uniqueIndex;not null"`
Password string `json:"-" gorm:"not null"` Password string `json:"-" gorm:"not null"`
FirstName string `json:"first_name" gorm:"not null"` FirstName string `json:"first_name" gorm:"not null"`
LastName string `json:"last_name" gorm:"not null"` LastName string `json:"last_name" gorm:"not null"`
Avatar string `json:"avatar"` // Путь к файлу аватара Avatar string `json:"avatar"` // Путь к файлу аватара
Phone string `json:"phone"` Phone string `json:"phone"`
Experience string `json:"experience"` Experience string `json:"experience"`
Goals string `json:"goals"` Goals string `json:"goals"`
Newsletter bool `json:"newsletter"` Newsletter bool `json:"newsletter"`
Role string `json:"role" gorm:"default:user"` Role string `json:"role" gorm:"default:user"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
// Связи
Workouts []Workout `json:"workouts,omitempty" gorm:"foreignKey:UserID"`
PersonalBests []PersonalBest `json:"personal_bests,omitempty" gorm:"foreignKey:UserID"`
Achievements []Achievement `json:"achievements,omitempty" gorm:"foreignKey:UserID"`
TrainingPlans []TrainingPlan `json:"training_plans,omitempty" gorm:"foreignKey:UserID"`
News []News `json:"news,omitempty" gorm:"foreignKey:AuthorID"`
Comments []Comment `json:"comments,omitempty" gorm:"foreignKey:AuthorID"`
Reviews []Review `json:"reviews,omitempty" gorm:"foreignKey:AuthorID"`
Gallery []Gallery `json:"gallery,omitempty" gorm:"foreignKey:AuthorID"`
EventRegistrations []EventRegistration `json:"event_registrations,omitempty" gorm:"foreignKey:UserID"`
} }
type UserUpdate struct { type UserUpdate struct {
ID uint `json:"id"` ID uint `json:"id"`
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name"` LastName string `json:"last_name"`
Avatar string `json:"avatar"` Avatar string `json:"avatar"`
Phone string `json:"phone"` Phone string `json:"phone"`
Experience string `json:"experience"` Experience string `json:"experience"`
Goals string `json:"goals"` Goals string `json:"goals"`
Newsletter bool `json:"newsletter"` Newsletter bool `json:"newsletter"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
} }
// HashPassword хеширует пароль перед сохранением // HashPassword хеширует пароль перед сохранением
@@ -70,3 +81,34 @@ func (u *User) BeforeUpdate(tx *gorm.DB) error {
u.UpdatedAt = time.Now() u.UpdatedAt = time.Now()
return nil return nil
} }
// DTO для обновления профиля
type UserUpdateRequest struct {
FirstName string `json:"first_name" validate:"required,min=2,max=100"`
LastName string `json:"last_name" validate:"required,min=2,max=100"`
Phone string `json:"phone" validate:"max=20"`
Experience string `json:"experience" validate:"max=50"`
Goals string `json:"goals" validate:"max=100"`
Newsletter bool `json:"newsletter"`
}
// DTO для ответа с пользователем (без sensitive данных)
type UserResponse struct {
ID uint `json:"id"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Avatar string `json:"avatar"`
Phone string `json:"phone"`
Experience string `json:"experience"`
Goals string `json:"goals"`
Newsletter bool `json:"newsletter"`
Role string `json:"role"`
CreatedAt time.Time `json:"created_at"`
}
// DTO для ответа с пользователем и статистикой
type UserWithStatsResponse struct {
UserResponse
Stats *UserStatsResponse `json:"stats,omitempty"`
}
@@ -0,0 +1,68 @@
// models/user_stats.go
package models
import (
"time"
"gorm.io/gorm"
)
type UserStats struct {
ID uint `json:"id" gorm:"primaryKey"`
UserID uint `json:"user_id" gorm:"uniqueIndex;not null"`
TotalDistance float64 `json:"total_distance" gorm:"type:decimal(10,2);default:0"` // Общий пробег в км
TotalTime int `json:"total_time" gorm:"default:0"` // Общее время в минутах
AvgPace string `json:"avg_pace" gorm:"size:20"` // Средний темп
WorkoutsCount int `json:"workouts_count" gorm:"default:0"` // Количество тренировок
CurrentStreak int `json:"current_streak" gorm:"default:0"` // Текущая серия дней подряд
LongestStreak int `json:"longest_streak" gorm:"default:0"` // Самая длинная серия
WeeklyDistance float64 `json:"weekly_distance" gorm:"type:decimal(8,2);default:0"` // Пробег за неделю
MonthlyDistance float64 `json:"monthly_distance" gorm:"type:decimal(8,2);default:0"` // Пробег за месяц
Best5K string `json:"best_5k" gorm:"size:20"` // Лучший результат на 5к
Best10K string `json:"best_10k" gorm:"size:20"` // Лучший результат на 10к
BestHalf string `json:"best_half" gorm:"size:20"` // Лучший результат на полумарафон
BestMarathon string `json:"best_marathon" gorm:"size:20"` // Лучший результат на марафон
LastWorkout time.Time `json:"last_workout"` // Последняя тренировка
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Связи
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
}
// BeforeCreate hook
func (us *UserStats) BeforeCreate(tx *gorm.DB) error {
if us.CreatedAt.IsZero() {
us.CreatedAt = time.Now()
}
if us.UpdatedAt.IsZero() {
us.UpdatedAt = time.Now()
}
return nil
}
// BeforeUpdate hook
func (us *UserStats) BeforeUpdate(tx *gorm.DB) error {
us.UpdatedAt = time.Now()
return nil
}
// DTO для статистики пользователя
type UserStatsResponse struct {
TotalDistance float64 `json:"total_distance"`
TotalTime int `json:"total_time"`
AvgPace string `json:"avg_pace"`
WorkoutsCount int `json:"workouts_count"`
CurrentStreak int `json:"current_streak"`
LongestStreak int `json:"longest_streak"`
WeeklyDistance float64 `json:"weekly_distance"`
MonthlyDistance float64 `json:"monthly_distance"`
PersonalBests PersonalBestsSummary `json:"personal_bests"`
}
type PersonalBestsSummary struct {
Best5K string `json:"best_5k"`
Best10K string `json:"best_10k"`
BestHalf string `json:"best_half"`
BestMarathon string `json:"best_marathon"`
}
+85 -1
View File
@@ -1,5 +1,89 @@
// models/workout.go
package models package models
import (
"time"
"gorm.io/gorm"
)
type WorkoutType string
const (
WorkoutTypeEasy WorkoutType = "easy"
WorkoutTypeTempo WorkoutType = "tempo"
WorkoutTypeInterval WorkoutType = "interval"
WorkoutTypeLong WorkoutType = "long"
WorkoutTypeRecovery WorkoutType = "recovery"
)
type Workout struct { type Workout struct {
ID uint ID uint `json:"id" gorm:"primaryKey"`
UserID uint `json:"user_id" gorm:"not null;index"`
Type WorkoutType `json:"type" gorm:"type:varchar(20);not null"`
Distance float64 `json:"distance_km" gorm:"type:decimal(5,2);not null"` // Дистанция в км
Duration int `json:"duration_min" gorm:"not null"` // Продолжительность в минутах
Pace string `json:"pace" gorm:"size:20"` // Темп (например, "5:30")
Calories int `json:"calories" gorm:"default:0"` // Сожженные калории
Notes string `json:"notes" gorm:"type:text"` // Заметки к тренировке
Date time.Time `json:"date" gorm:"not null;index"` // Дата тренировки
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Связи
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
}
// BeforeCreate hook
func (w *Workout) BeforeCreate(tx *gorm.DB) error {
if w.CreatedAt.IsZero() {
w.CreatedAt = time.Now()
}
if w.UpdatedAt.IsZero() {
w.UpdatedAt = time.Now()
}
return nil
}
// BeforeUpdate hook
func (w *Workout) BeforeUpdate(tx *gorm.DB) error {
w.UpdatedAt = time.Now()
return nil
}
// DTO для создания тренировки
type WorkoutCreateRequest struct {
Type WorkoutType `json:"type" validate:"required,oneof=easy tempo interval long recovery"`
Distance float64 `json:"distance_km" validate:"required,min=0.1,max=1000"`
Duration int `json:"duration_min" validate:"required,min=1,max=1440"`
Pace string `json:"pace" validate:"max=20"`
Calories int `json:"calories" validate:"min=0,max=5000"`
Notes string `json:"notes" validate:"max=1000"`
Date time.Time `json:"date" validate:"required"`
}
// DTO для обновления тренировки
type WorkoutUpdateRequest struct {
Type WorkoutType `json:"type" validate:"omitempty,oneof=easy tempo interval long recovery"`
Distance float64 `json:"distance_km" validate:"omitempty,min=0.1,max=1000"`
Duration int `json:"duration_min" validate:"omitempty,min=1,max=1440"`
Pace string `json:"pace" validate:"omitempty,max=20"`
Calories int `json:"calories" validate:"omitempty,min=0,max=5000"`
Notes string `json:"notes" validate:"omitempty,max=1000"`
Date time.Time `json:"date"`
}
// DTO для статистики тренировок
type WorkoutStatsResponse struct {
TotalWorkouts int `json:"total_workouts"`
TotalDistance float64 `json:"total_distance_km"`
TotalTime int `json:"total_time_min"`
AveragePace string `json:"average_pace"`
MonthlyStats []MonthlyStat `json:"monthly_stats"`
}
type MonthlyStat struct {
Month string `json:"month"`
Distance float64 `json:"distance_km"`
Workouts int `json:"workouts"`
} }
@@ -2,13 +2,8 @@
<div> <div>
<!-- Бургер-меню для всех устройств --> <!-- Бургер-меню для всех устройств -->
<div class="burger-menu-container"> <div class="burger-menu-container">
<button <button class="burger-menu" :class="{ 'active': isMobileMenuOpen }" @click="toggleMobileMenu"
class="burger-menu" :aria-label="isMobileMenuOpen ? 'Закрыть меню' : 'Открыть меню'" :aria-expanded="isMobileMenuOpen">
:class="{ 'active': isMobileMenuOpen }"
@click="toggleMobileMenu"
:aria-label="isMobileMenuOpen ? 'Закрыть меню' : 'Открыть меню'"
:aria-expanded="isMobileMenuOpen"
>
<span></span> <span></span>
<span></span> <span></span>
<span></span> <span></span>
@@ -18,10 +13,8 @@
<div class="dropdown-menu" :class="{ 'active': isMobileMenuOpen }" ref="dropdownMenu"> <div class="dropdown-menu" :class="{ 'active': isMobileMenuOpen }" ref="dropdownMenu">
<nav class="dropdown-nav"> <nav class="dropdown-nav">
<div class="mobile-menu-header"> <div class="mobile-menu-header">
<div class="mobile-logo"> <!-- Добавьте мобильный логотип здесь -->
<img src="../assets/logo/Logo.png" alt="Little logo begushiy bashkir" class="little-logo"> <WriteLogo />
<span>Бегущий Башкир</span>
</div>
<button class="close-menu" @click="closeMobileMenu" aria-label="Закрыть меню"> <button class="close-menu" @click="closeMobileMenu" aria-label="Закрыть меню">
× ×
</button> </button>
@@ -73,8 +66,12 @@
</template> </template>
<script> <script>
import writeLogo from './writeLogo.vue';
export default { export default {
name: 'NavigationMenu', name: 'NavigationMenu',
components: {
writeLogo
},
data() { data() {
return { return {
isMobileMenuOpen: false, isMobileMenuOpen: false,
@@ -95,8 +92,8 @@ export default {
}, },
handleClickOutside(event) { handleClickOutside(event) {
if (!event.target.closest('.burger-menu-container') && if (!event.target.closest('.burger-menu-container') &&
!event.target.closest('.dropdown-menu') && !event.target.closest('.dropdown-menu') &&
this.isMobileMenuOpen) { this.isMobileMenuOpen) {
this.closeMobileMenu() this.closeMobileMenu()
} }
}, },
@@ -241,7 +238,7 @@ export default {
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
background: #2e8b57; background: #2e8b57;
color: white; color: white;
border-bottom: 1px solid rgba(255,255,255,0.1); border-bottom: 1px solid rgba(255, 255, 255, 0.1);
} }
.mobile-logo { .mobile-logo {
@@ -0,0 +1,57 @@
<template>
<div class="mobile-logo">
<img :src="logoUrl" alt="Little logo begushiy bashkir" class="little-logo">
<span>Бегущий Башкир</span>
</div>
</template>
<script>
export default {
name: 'WriteLogo',
computed: {
logoUrl() {
// Для правильного пути к изображению в разных окружениях
return new URL('../assets/logo/Logo.png', import.meta.url).href;
}
}
}
</script>
<style scoped>
.mobile-logo {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 15px;
background: rgba(255, 255, 255, 0.95);
border-radius: 50px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
}
.little-logo {
width: 30px;
height: 30px;
object-fit: contain;
}
.mobile-logo span {
font-weight: 600;
color: #2e8b57;
font-size: 0.9rem;
}
/* Скрыть на десктопе по умолчанию */
@media (min-width: 769px) {
.mobile-logo {
display: none;
}
}
/* Показать на мобильных устройствах */
@media (max-width: 768px) {
.mobile-logo {
display: flex;
}
}
</style>