v0.0.32
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
admin 2025-09-18 01:19:09 +03:00
parent ecf62c7346
commit 222469e40a
12 changed files with 109 additions and 2022 deletions

2
.env
View File

@ -13,4 +13,4 @@ SMTP_FROM=info@tailly.ru
AppURL="https://tailly.ru" AppURL="https://tailly.ru"
MESSAGE_SERVICE_ADDRESS="tailly_messages:50052" MESSAGE_SERVICE_ADDRESS="tailly_messages:50052"
SUBSCRIBE_SERVICE_ADDRESS="tailly_subscribers:50053" SUBSCRIBE_SERVICE_ADDRESS="tailly_subscribers:50053"
CLIP_SERVICE_ADDRESS="tailly_clips:50054" CLIP_SERVICE_ADDRESS="tailly_clips:50054"

View File

@ -132,7 +132,7 @@ func main() {
userService := service.NewUserService(userRepo) userService := service.NewUserService(userRepo)
postService := service.NewPostService(postRepo) postService := service.NewPostService(postRepo)
commentService := service.NewCommentService(commentRepo, postRepo) commentService := service.NewCommentService(commentRepo, postRepo)
likeService := service.NewLikeService(likeRepo, postRepo, userRepo) likeService := service.NewLikeService(likeRepo, postRepo)
auditService := service.NewAuditService(auditRepo) auditService := service.NewAuditService(auditRepo)
recoveryService := service.NewRecoveryService(recoveryRepo, userRepo, sessionRepo, deviceRepo, mailService) recoveryService := service.NewRecoveryService(recoveryRepo, userRepo, sessionRepo, deviceRepo, mailService)
sessionService := service.NewSessionService(sessionRepo, deviceRepo, userRepo, mailService) sessionService := service.NewSessionService(sessionRepo, deviceRepo, userRepo, mailService)

View File

@ -50,6 +50,5 @@ models:
ClipComment: ClipComment:
model: tailly_back_v2/internal/domain.ClipComment model: tailly_back_v2/internal/domain.ClipComment
autobind: autobind:
- "tailly_back_v2/internal/domain" - "tailly_back_v2/internal/domain"

View File

@ -23,7 +23,6 @@ type Post struct {
Title string `json:"title"` Title string `json:"title"`
Content string `json:"content"` Content string `json:"content"`
AuthorID int `json:"authorId"` AuthorID int `json:"authorId"`
Author *User `json:"author,omitempty"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
} }
@ -40,46 +39,10 @@ type Comment struct {
// Лайк к посту // Лайк к посту
type Like struct { type Like struct {
ID int `json:"id"` ID int `json:"id"`
PostID int `json:"postId"` PostID int `json:"postId"`
UserID int `json:"userId"` UserID int `json:"userId"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
IsRead bool `json:"isRead"`
NotifiedAt time.Time `json:"notifiedAt"`
}
type LikeNotification struct {
ID int `json:"id"`
LikerID int `json:"-"` // Для внутреннего использования
Liker *User `json:"liker"` // Для GraphQL
LikerUsername string `json:"likerUsername"` // Добавьте это поле
LikerAvatar string `json:"likerAvatar"` // Добавьте это поле
PostID int `json:"-"` // Для внутреннего использования
Post *Post `json:"post"` // Для GraphQL
PostTitle string `json:"postTitle"` // Добавьте это поле
PostAuthorID int `json:"postAuthorId"` // Добавьте это поле
IsRead bool `json:"isRead"`
CreatedAt time.Time `json:"-"`
CreatedAtStr string `json:"createdAt"` // Для GraphQL
}
// Вспомогательные методы
func (n *LikeNotification) SetLiker(user *User) {
n.Liker = user
n.LikerID = user.ID
n.LikerUsername = user.Username // Добавьте эту строку
n.LikerAvatar = user.Avatar // Добавьте эту строку
}
func (n *LikeNotification) SetPost(post *Post) {
n.Post = post
n.PostID = post.ID
n.PostTitle = post.Title // Добавьте эту строку
n.PostAuthorID = post.AuthorID // Добавьте эту строку
}
func (n *LikeNotification) SetCreatedAtStr() {
n.CreatedAtStr = n.CreatedAt.Format(time.RFC3339)
} }
// Токены для аутентификации // Токены для аутентификации

File diff suppressed because it is too large Load Diff

View File

@ -23,134 +23,12 @@ func (r *likeResolver) Post(ctx context.Context, obj *domain.Like) (*domain.Post
// User is the resolver for the user field. // User is the resolver for the user field.
func (r *likeResolver) User(ctx context.Context, obj *domain.Like) (*domain.User, error) { func (r *likeResolver) User(ctx context.Context, obj *domain.Like) (*domain.User, error) {
user, err := r.Services.User.GetByID(ctx, obj.UserID) // This would typically use a UserService to fetch the user
if err != nil { // For now, we'll return nil as the user service isn't shown in the provided code
return nil, fmt.Errorf("failed to get user: %w", err) return nil, nil
}
return user, nil
} }
// CreatedAt is the resolver for the createdAt field. // CreatedAt is the resolver for the createdAt field.
func (r *likeResolver) CreatedAt(ctx context.Context, obj *domain.Like) (string, error) { func (r *likeResolver) CreatedAt(ctx context.Context, obj *domain.Like) (string, error) {
return obj.CreatedAt.Format(time.RFC3339), nil return obj.CreatedAt.Format(time.RFC3339), nil
} }
// IsRead is the resolver for the isRead field.
func (r *likeResolver) IsRead(ctx context.Context, obj *domain.Like) (bool, error) {
return obj.IsRead, nil
}
// NotifiedAt is the resolver for the notifiedAt field.
func (r *likeResolver) NotifiedAt(ctx context.Context, obj *domain.Like) (*string, error) {
if obj.NotifiedAt.IsZero() {
return nil, nil
}
formatted := obj.NotifiedAt.Format(time.RFC3339)
return &formatted, nil
}
// MarkLikeNotificationAsRead is the resolver for the markLikeNotificationAsRead field.
func (r *mutationResolver) MarkLikeNotificationAsRead(ctx context.Context, notificationID int) (*MarkLikeNotificationReadResult, error) {
userID, err := getUserIDFromContext(ctx)
if err != nil {
return nil, fmt.Errorf("authentication required: %w", err)
}
err = r.Services.Like.MarkLikeNotificationAsRead(ctx, notificationID, userID)
if err != nil {
return nil, fmt.Errorf("failed to mark notification as read: %w", err)
}
return &MarkLikeNotificationReadResult{
Success: true,
Message: "Notification marked as read successfully",
}, nil
}
// MarkAllLikeNotificationsAsRead is the resolver for the markAllLikeNotificationsAsRead field.
func (r *mutationResolver) MarkAllLikeNotificationsAsRead(ctx context.Context) (*MarkLikeNotificationReadResult, error) {
userID, err := getUserIDFromContext(ctx)
if err != nil {
return nil, fmt.Errorf("authentication required: %w", err)
}
err = r.Services.Like.MarkAllLikeNotificationsAsRead(ctx, userID)
if err != nil {
return nil, fmt.Errorf("failed to mark all notifications as read: %w", err)
}
return &MarkLikeNotificationReadResult{
Success: true,
Message: "All notifications marked as read successfully",
}, nil
}
func (r *queryResolver) GetLikeNotifications(ctx context.Context, unreadOnly *bool, limit *int, offset *int) (*LikeNotificationsResponse, error) {
userID, err := getUserIDFromContext(ctx)
if err != nil {
return nil, fmt.Errorf("authentication required: %w", err)
}
unreadOnlyVal := false
if unreadOnly != nil {
unreadOnlyVal = *unreadOnly
}
limitVal := 20
if limit != nil {
limitVal = *limit
}
offsetVal := 0
if offset != nil {
offsetVal = *offset
}
notifications, totalCount, unreadCount, err := r.Services.Like.GetLikeNotifications(ctx, userID, unreadOnlyVal, limitVal, offsetVal)
if err != nil {
return nil, fmt.Errorf("failed to get like notifications: %w", err)
}
// Заполняем дополнительные поля для GraphQL
for _, notification := range notifications {
// Получаем информацию о пользователе, который поставил лайк
likerUser, err := r.Services.User.GetByID(ctx, notification.LikerID)
if err != nil {
return nil, fmt.Errorf("failed to get liker user: %w", err)
}
notification.SetLiker(likerUser)
// Получаем информацию о посте
post, err := r.Services.Post.GetByID(ctx, notification.PostID)
if err != nil {
return nil, fmt.Errorf("failed to get post: %w", err)
}
// Получаем автора поста
postAuthor, err := r.Services.User.GetByID(ctx, post.AuthorID)
if err != nil {
return nil, fmt.Errorf("failed to get post author: %w", err)
}
// Устанавливаем автора поста
post.AuthorID = postAuthor.ID
notification.SetPost(post)
// Форматируем дату
notification.SetCreatedAtStr()
}
return &LikeNotificationsResponse{
Notifications: notifications,
TotalCount: totalCount,
UnreadCount: unreadCount,
}, nil
}
// LikeNotification returns LikeNotificationResolver implementation.
func (r *Resolver) LikeNotification() LikeNotificationResolver { return &likeNotificationResolver{r} }
type likeNotificationResolver struct{ *Resolver }
func (r *likeNotificationResolver) CreatedAt(ctx context.Context, obj *domain.LikeNotification) (string, error) {
return obj.CreatedAt.Format(time.RFC3339), nil
}

View File

@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"io" "io"
"strconv" "strconv"
"tailly_back_v2/internal/domain"
) )
type FollowResult struct { type FollowResult struct {
@ -39,17 +38,6 @@ type FollowingResponse struct {
TotalCount int `json:"totalCount"` TotalCount int `json:"totalCount"`
} }
type LikeNotificationsResponse struct {
Notifications []*domain.LikeNotification `json:"notifications"`
TotalCount int `json:"totalCount"`
UnreadCount int `json:"unreadCount"`
}
type MarkLikeNotificationReadResult struct {
Success bool `json:"success"`
Message string `json:"message"`
}
type MarkNotificationReadResult struct { type MarkNotificationReadResult struct {
Success bool `json:"success"` Success bool `json:"success"`
Message string `json:"message"` Message string `json:"message"`

View File

@ -17,11 +17,6 @@ type User {
limit: Int = 20 limit: Int = 20
offset: Int = 0 offset: Int = 0
): NotificationsResponse! ): NotificationsResponse!
likeNotifications(
unreadOnly: Boolean = false
limit: Int = 20
offset: Int = 0
): LikeNotificationsResponse!
} }
# Пост в блоге # Пост в блоге
@ -50,12 +45,10 @@ type Comment {
# Лайк к посту # Лайк к посту
type Like { type Like {
id: Int! # Уникальный идентификатор id: Int! # Уникальный идентификатор
post: Post! # Пост, который лайкнули post: Post! # Пост, который лайкнули
user: User! # Пользователь, который поставил лайк user: User! # Пользователь, который поставил лайк
createdAt: String! # Дата создания createdAt: String! # Дата создания
isRead: Boolean! # Прочитано ли уведомление
notifiedAt: String # Когда было отправлено уведомление
} }
type Tokens { type Tokens {
@ -177,23 +170,7 @@ type MarkNotificationReadResult {
message: String! message: String!
} }
type LikeNotification {
id: Int!
liker: User!
post: Post!
isRead: Boolean!
createdAt: String!
}
type LikeNotificationsResponse {
notifications: [LikeNotification!]!
totalCount: Int!
unreadCount: Int!
}
type MarkLikeNotificationReadResult {
success: Boolean!
message: String!
}
# Клип # Клип
type Clip { type Clip {
id: Int! id: Int!
@ -269,12 +246,6 @@ type Query {
clipComments(clipId: Int!, limit: Int, offset: Int): [ClipComment!]! # Комментарии клипа clipComments(clipId: Int!, limit: Int, offset: Int): [ClipComment!]! # Комментарии клипа
clipLikes(clipId: Int!, limit: Int, offset: Int): [ClipLike!]! # Лайки клипа clipLikes(clipId: Int!, limit: Int, offset: Int): [ClipLike!]! # Лайки клипа
isLiked(clipId: Int!): Boolean! # Проверить лайк текущего пользователя isLiked(clipId: Int!): Boolean! # Проверить лайк текущего пользователя
getLikeNotifications(
unreadOnly: Boolean = false
limit: Int = 20
offset: Int = 0
): LikeNotificationsResponse!
} }
@ -323,8 +294,6 @@ type Mutation {
unlikeClip(clipId: Int!): Boolean! # Убрать лайк unlikeClip(clipId: Int!): Boolean! # Убрать лайк
createClipComment(clipId: Int!, content: String!): ClipComment! # Создать комментарий createClipComment(clipId: Int!, content: String!): ClipComment! # Создать комментарий
deleteClipComment(commentId: Int!): Boolean! # Удалить комментарий deleteClipComment(commentId: Int!): Boolean! # Удалить комментарий
markLikeNotificationAsRead(notificationId: Int!): MarkLikeNotificationReadResult!
markAllLikeNotificationsAsRead: MarkLikeNotificationReadResult!
} }
type Subscription { type Subscription {

View File

@ -256,71 +256,3 @@ func (r *userResolver) SubscriptionNotifications(ctx context.Context, obj *domai
UnreadCount: int(res.UnreadCount), UnreadCount: int(res.UnreadCount),
}, nil }, nil
} }
// LikeNotifications is the resolver for the likeNotifications field.
func (r *userResolver) LikeNotifications(ctx context.Context, obj *domain.User, unreadOnly *bool, limit *int, offset *int) (*LikeNotificationsResponse, error) {
currentUserID, err := getUserIDFromContext(ctx)
if err != nil {
return nil, fmt.Errorf("authentication required: %w", err)
}
// Можно смотреть только свои уведомления
if obj.ID != currentUserID {
return nil, fmt.Errorf("access denied: can only view your own notifications")
}
unreadOnlyVal := false
if unreadOnly != nil {
unreadOnlyVal = *unreadOnly
}
limitVal := 20
if limit != nil {
limitVal = *limit
}
offsetVal := 0
if offset != nil {
offsetVal = *offset
}
notifications, totalCount, unreadCount, err := r.Services.Like.GetLikeNotifications(ctx, obj.ID, unreadOnlyVal, limitVal, offsetVal)
if err != nil {
return nil, fmt.Errorf("failed to get like notifications: %w", err)
}
// Заполняем дополнительные поля для GraphQL
for _, notification := range notifications {
// Получаем информацию о пользователе, который поставил лайк
likerUser, err := r.Services.User.GetByID(ctx, notification.LikerID)
if err != nil {
return nil, fmt.Errorf("failed to get liker user: %w", err)
}
notification.SetLiker(likerUser)
// Получаем информацию о посте
post, err := r.Services.Post.GetByID(ctx, notification.PostID)
if err != nil {
return nil, fmt.Errorf("failed to get post: %w", err)
}
// Получаем автора поста
postAuthor, err := r.Services.User.GetByID(ctx, post.AuthorID)
if err != nil {
return nil, fmt.Errorf("failed to get post author: %w", err)
}
// Устанавливаем автора поста
post.Author = postAuthor
notification.SetPost(post)
// Форматируем дату
notification.SetCreatedAtStr()
}
return &LikeNotificationsResponse{
Notifications: notifications,
TotalCount: totalCount,
UnreadCount: unreadCount,
}, nil
}

View File

@ -19,18 +19,22 @@ type LikeRepository interface {
GetByUserAndPost(ctx context.Context, userID, postID int) (*domain.Like, error) GetByUserAndPost(ctx context.Context, userID, postID int) (*domain.Like, error)
Delete(ctx context.Context, id int) error Delete(ctx context.Context, id int) error
DeleteByUserAndPost(ctx context.Context, userID, postID int) error DeleteByUserAndPost(ctx context.Context, userID, postID int) error
// Новые методы для уведомлений
GetUnreadLikeNotifications(ctx context.Context, userID int, limit, offset int) ([]*domain.LikeNotification, error)
GetAllLikeNotifications(ctx context.Context, userID int, limit, offset int) ([]*domain.LikeNotification, error)
MarkLikeNotificationAsRead(ctx context.Context, notificationID, userID int) error
MarkAllLikeNotificationsAsRead(ctx context.Context, userID int) error
GetLikeNotificationCounts(ctx context.Context, userID int) (totalCount, unreadCount int, err error)
} }
type likeRepository struct { type likeRepository struct {
db *sql.DB db *sql.DB
} }
func (r *likeRepository) GetByID(ctx context.Context, id int) (*domain.Like, error) {
//TODO implement me
panic("implement me")
}
func (r *likeRepository) Delete(ctx context.Context, id int) error {
//TODO implement me
panic("implement me")
}
func NewLikeRepository(db *sql.DB) *likeRepository { func NewLikeRepository(db *sql.DB) *likeRepository {
return &likeRepository{db: db} return &likeRepository{db: db}
} }
@ -45,8 +49,8 @@ func (r *likeRepository) Create(ctx context.Context, like *domain.Like) error {
} }
query := ` query := `
INSERT INTO likes (post_id, user_id, created_at, is_read, notified_at) INSERT INTO likes (post_id, user_id, created_at)
VALUES ($1, $2, $3, $4, $5) VALUES ($1, $2, $3)
RETURNING id RETURNING id
` `
@ -54,43 +58,14 @@ func (r *likeRepository) Create(ctx context.Context, like *domain.Like) error {
like.PostID, like.PostID,
like.UserID, like.UserID,
like.CreatedAt, like.CreatedAt,
like.IsRead,
like.NotifiedAt,
).Scan(&like.ID) ).Scan(&like.ID)
return err return err
} }
func (r *likeRepository) GetByID(ctx context.Context, id int) (*domain.Like, error) {
query := `
SELECT id, post_id, user_id, created_at, is_read, notified_at
FROM likes
WHERE id = $1
`
like := &domain.Like{}
err := r.db.QueryRowContext(ctx, query, id).Scan(
&like.ID,
&like.PostID,
&like.UserID,
&like.CreatedAt,
&like.IsRead,
&like.NotifiedAt,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrLikeNotFound
}
return nil, err
}
return like, nil
}
func (r *likeRepository) GetByPostID(ctx context.Context, postID int) ([]*domain.Like, error) { func (r *likeRepository) GetByPostID(ctx context.Context, postID int) ([]*domain.Like, error) {
query := ` query := `
SELECT id, post_id, user_id, created_at, is_read, notified_at SELECT id, post_id, user_id, created_at
FROM likes FROM likes
WHERE post_id = $1 WHERE post_id = $1
ORDER BY created_at DESC ORDER BY created_at DESC
@ -110,8 +85,6 @@ func (r *likeRepository) GetByPostID(ctx context.Context, postID int) ([]*domain
&like.PostID, &like.PostID,
&like.UserID, &like.UserID,
&like.CreatedAt, &like.CreatedAt,
&like.IsRead,
&like.NotifiedAt,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@ -122,9 +95,18 @@ func (r *likeRepository) GetByPostID(ctx context.Context, postID int) ([]*domain
return likes, nil return likes, nil
} }
func (r *likeRepository) DeleteByUserAndPost(ctx context.Context, userID, postID int) error {
query := `
DELETE FROM likes
WHERE user_id = $1 AND post_id = $2
`
_, err := r.db.ExecContext(ctx, query, userID, postID)
return err
}
func (r *likeRepository) GetByUserAndPost(ctx context.Context, userID, postID int) (*domain.Like, error) { func (r *likeRepository) GetByUserAndPost(ctx context.Context, userID, postID int) (*domain.Like, error) {
query := ` query := `
SELECT id, post_id, user_id, created_at, is_read, notified_at SELECT id, post_id, user_id, created_at
FROM likes FROM likes
WHERE user_id = $1 AND post_id = $2 WHERE user_id = $1 AND post_id = $2
LIMIT 1 LIMIT 1
@ -136,8 +118,6 @@ func (r *likeRepository) GetByUserAndPost(ctx context.Context, userID, postID in
&like.PostID, &like.PostID,
&like.UserID, &like.UserID,
&like.CreatedAt, &like.CreatedAt,
&like.IsRead,
&like.NotifiedAt,
) )
if err != nil { if err != nil {
@ -149,166 +129,3 @@ func (r *likeRepository) GetByUserAndPost(ctx context.Context, userID, postID in
return like, nil return like, nil
} }
func (r *likeRepository) Delete(ctx context.Context, id int) error {
query := `DELETE FROM likes WHERE id = $1`
_, err := r.db.ExecContext(ctx, query, id)
return err
}
func (r *likeRepository) DeleteByUserAndPost(ctx context.Context, userID, postID int) error {
query := `DELETE FROM likes WHERE user_id = $1 AND post_id = $2`
_, err := r.db.ExecContext(ctx, query, userID, postID)
return err
}
// Новые методы для уведомлений
func (r *likeRepository) GetUnreadLikeNotifications(ctx context.Context, userID int, limit, offset int) ([]*domain.LikeNotification, error) {
query := `
SELECT l.id, l.post_id, l.user_id as liker_id, l.created_at, l.is_read,
u.username as liker_username, u.avatar as liker_avatar,
p.title as post_title, p.author_id as post_author_id
FROM likes l
JOIN users u ON l.user_id = u.id
JOIN posts p ON l.post_id = p.id
WHERE p.author_id = $1 AND l.is_read = false
ORDER BY l.created_at DESC
LIMIT $2 OFFSET $3
`
rows, err := r.db.QueryContext(ctx, query, userID, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
var notifications []*domain.LikeNotification
for rows.Next() {
notification := &domain.LikeNotification{}
err := rows.Scan(
&notification.ID,
&notification.PostID,
&notification.LikerID,
&notification.CreatedAt,
&notification.IsRead,
&notification.LikerUsername,
&notification.LikerAvatar,
&notification.PostTitle,
&notification.PostAuthorID,
)
if err != nil {
return nil, err
}
notifications = append(notifications, notification)
}
return notifications, nil
}
func (r *likeRepository) GetAllLikeNotifications(ctx context.Context, userID int, limit, offset int) ([]*domain.LikeNotification, error) {
query := `
SELECT l.id, l.post_id, l.user_id as liker_id, l.created_at, l.is_read,
u.username as liker_username, u.avatar as liker_avatar,
p.title as post_title, p.author_id as post_author_id
FROM likes l
JOIN users u ON l.user_id = u.id
JOIN posts p ON l.post_id = p.id
WHERE p.author_id = $1
ORDER BY l.created_at DESC
LIMIT $2 OFFSET $3
`
rows, err := r.db.QueryContext(ctx, query, userID, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
var notifications []*domain.LikeNotification
for rows.Next() {
notification := &domain.LikeNotification{}
err := rows.Scan(
&notification.ID,
&notification.PostID,
&notification.LikerID,
&notification.CreatedAt,
&notification.IsRead,
&notification.LikerUsername,
&notification.LikerAvatar,
&notification.PostTitle,
&notification.PostAuthorID,
)
if err != nil {
return nil, err
}
notifications = append(notifications, notification)
}
return notifications, nil
}
func (r *likeRepository) MarkLikeNotificationAsRead(ctx context.Context, notificationID, userID int) error {
query := `
UPDATE likes
SET is_read = true
WHERE id = $1 AND post_id IN (
SELECT id FROM posts WHERE author_id = $2
)
`
result, err := r.db.ExecContext(ctx, query, notificationID, userID)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
return ErrLikeNotFound
}
return nil
}
func (r *likeRepository) MarkAllLikeNotificationsAsRead(ctx context.Context, userID int) error {
query := `
UPDATE likes
SET is_read = true
WHERE post_id IN (
SELECT id FROM posts WHERE author_id = $1
) AND is_read = false
`
_, err := r.db.ExecContext(ctx, query, userID)
return err
}
func (r *likeRepository) GetLikeNotificationCounts(ctx context.Context, userID int) (totalCount, unreadCount int, err error) {
queryTotal := `
SELECT COUNT(*)
FROM likes l
JOIN posts p ON l.post_id = p.id
WHERE p.author_id = $1
`
queryUnread := `
SELECT COUNT(*)
FROM likes l
JOIN posts p ON l.post_id = p.id
WHERE p.author_id = $1 AND l.is_read = false
`
err = r.db.QueryRowContext(ctx, queryTotal, userID).Scan(&totalCount)
if err != nil {
return 0, 0, err
}
err = r.db.QueryRowContext(ctx, queryUnread, userID).Scan(&unreadCount)
if err != nil {
return 0, 0, err
}
return totalCount, unreadCount, nil
}

View File

@ -15,33 +15,26 @@ type LikeService interface {
GetByPostID(ctx context.Context, postID int) ([]*domain.Like, error) GetByPostID(ctx context.Context, postID int) ([]*domain.Like, error)
GetCountForPost(ctx context.Context, postID int) (int, error) GetCountForPost(ctx context.Context, postID int) (int, error)
CheckIfLiked(ctx context.Context, userID, postID int) (bool, error) CheckIfLiked(ctx context.Context, userID, postID int) (bool, error)
// Новые методы для уведомлений
GetLikeNotifications(ctx context.Context, userID int, unreadOnly bool, limit, offset int) ([]*domain.LikeNotification, int, int, error)
MarkLikeNotificationAsRead(ctx context.Context, notificationID, userID int) error
MarkAllLikeNotificationsAsRead(ctx context.Context, userID int) error
} }
// Реализация сервиса лайков // Реализация сервиса лайков
type likeService struct { type likeService struct {
likeRepo repository.LikeRepository likeRepo repository.LikeRepository
postRepo repository.PostRepository postRepo repository.PostRepository // Для проверки существования поста
userRepo repository.UserRepository // Добавляем для получения информации о пользователях
} }
// Конструктор сервиса // Конструктор сервиса
func NewLikeService(likeRepo repository.LikeRepository, postRepo repository.PostRepository, userRepo repository.UserRepository) LikeService { func NewLikeService(likeRepo repository.LikeRepository, postRepo repository.PostRepository) LikeService {
return &likeService{ return &likeService{
likeRepo: likeRepo, likeRepo: likeRepo,
postRepo: postRepo, postRepo: postRepo,
userRepo: userRepo,
} }
} }
// Поставить лайк посту // Поставить лайк посту
func (s *likeService) LikePost(ctx context.Context, userID, postID int) (*domain.Like, error) { func (s *likeService) LikePost(ctx context.Context, userID, postID int) (*domain.Like, error) {
// Проверяем существование поста // Проверяем существование поста
post, err := s.postRepo.GetByID(ctx, postID) if _, err := s.postRepo.GetByID(ctx, postID); err != nil {
if err != nil {
if errors.Is(err, repository.ErrPostNotFound) { if errors.Is(err, repository.ErrPostNotFound) {
return nil, errors.New("post not found") return nil, errors.New("post not found")
} }
@ -55,17 +48,10 @@ func (s *likeService) LikePost(ctx context.Context, userID, postID int) (*domain
return nil, errors.New("you have already liked this post") return nil, errors.New("you have already liked this post")
} }
// Проверяем, что пользователь не лайкает свой собственный пост
if post.AuthorID == userID {
return nil, errors.New("cannot like your own post")
}
like := &domain.Like{ like := &domain.Like{
PostID: postID, PostID: postID,
UserID: userID, UserID: userID,
CreatedAt: time.Now(), CreatedAt: time.Now(),
IsRead: false,
NotifiedAt: time.Now(),
} }
if err := s.likeRepo.Create(ctx, like); err != nil { if err := s.likeRepo.Create(ctx, like); err != nil {
@ -110,6 +96,7 @@ func (s *likeService) GetByPostID(ctx context.Context, postID int) ([]*domain.Li
return nil, err return nil, err
} }
// Возвращаем пустой слайс вместо nil
if likes == nil { if likes == nil {
return []*domain.Like{}, nil return []*domain.Like{}, nil
} }
@ -137,35 +124,3 @@ func (s *likeService) CheckIfLiked(ctx context.Context, userID, postID int) (boo
} }
return true, nil return true, nil
} }
// Новые методы для уведомлений
func (s *likeService) GetLikeNotifications(ctx context.Context, userID int, unreadOnly bool, limit, offset int) ([]*domain.LikeNotification, int, int, error) {
var notifications []*domain.LikeNotification
var err error
if unreadOnly {
notifications, err = s.likeRepo.GetUnreadLikeNotifications(ctx, userID, limit, offset)
} else {
notifications, err = s.likeRepo.GetAllLikeNotifications(ctx, userID, limit, offset)
}
if err != nil {
return nil, 0, 0, err
}
totalCount, unreadCount, err := s.likeRepo.GetLikeNotificationCounts(ctx, userID)
if err != nil {
return nil, 0, 0, err
}
return notifications, totalCount, unreadCount, nil
}
func (s *likeService) MarkLikeNotificationAsRead(ctx context.Context, notificationID, userID int) error {
return s.likeRepo.MarkLikeNotificationAsRead(ctx, notificationID, userID)
}
func (s *likeService) MarkAllLikeNotificationsAsRead(ctx context.Context, userID int) error {
return s.likeRepo.MarkAllLikeNotificationsAsRead(ctx, userID)
}

View File

@ -1,2 +0,0 @@
ALTER TABLE likes ADD COLUMN is_read BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE likes ADD COLUMN notified_at TIMESTAMP WITH TIME ZONE DEFAULT NOW();