v0.0.33 Уведомления о лайках
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
1c1d10eb48
commit
d7b74cd403
@ -49,6 +49,8 @@ models:
|
||||
model: tailly_back_v2/internal/domain.ClipLike
|
||||
ClipComment:
|
||||
model: tailly_back_v2/internal/domain.ClipComment
|
||||
LikeNotification:
|
||||
model: tailly_back_v2/internal/domain.LikeNotification
|
||||
|
||||
autobind:
|
||||
- "tailly_back_v2/internal/domain"
|
||||
@ -23,6 +23,7 @@ type Post struct {
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
AuthorID int `json:"authorId"`
|
||||
Author *User `json:"author,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
@ -43,6 +44,34 @@ type Like struct {
|
||||
PostID int `json:"postId"`
|
||||
UserID int `json:"userId"`
|
||||
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
|
||||
PostID int `json:"-"` // Для внутреннего использования
|
||||
Post *Post `json:"post"` // Для GraphQL
|
||||
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
|
||||
}
|
||||
|
||||
func (n *LikeNotification) SetPost(post *Post) {
|
||||
n.Post = post
|
||||
n.PostID = post.ID
|
||||
}
|
||||
|
||||
func (n *LikeNotification) SetCreatedAtStr() {
|
||||
n.CreatedAtStr = n.CreatedAt.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// Токены для аутентификации
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
105
internal/http/graph/like_notification_resolvers.go
Normal file
105
internal/http/graph/like_notification_resolvers.go
Normal file
@ -0,0 +1,105 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// GetLikeNotifications is the resolver for the getLikeNotifications field.
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@ -23,12 +23,28 @@ func (r *likeResolver) Post(ctx context.Context, obj *domain.Like) (*domain.Post
|
||||
|
||||
// User is the resolver for the user field.
|
||||
func (r *likeResolver) User(ctx context.Context, obj *domain.Like) (*domain.User, error) {
|
||||
// This would typically use a UserService to fetch the user
|
||||
// For now, we'll return nil as the user service isn't shown in the provided code
|
||||
return nil, nil
|
||||
user, err := r.Services.User.GetByID(ctx, obj.UserID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// CreatedAt is the resolver for the createdAt field.
|
||||
func (r *likeResolver) CreatedAt(ctx context.Context, obj *domain.Like) (string, error) {
|
||||
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
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"tailly_back_v2/internal/domain"
|
||||
)
|
||||
|
||||
type FollowResult struct {
|
||||
@ -38,6 +39,17 @@ type FollowingResponse struct {
|
||||
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 {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
|
||||
@ -17,6 +17,11 @@ type User {
|
||||
limit: Int = 20
|
||||
offset: Int = 0
|
||||
): NotificationsResponse!
|
||||
likeNotifications(
|
||||
unreadOnly: Boolean = false
|
||||
limit: Int = 20
|
||||
offset: Int = 0
|
||||
): LikeNotificationsResponse!
|
||||
}
|
||||
|
||||
# Пост в блоге
|
||||
@ -49,6 +54,8 @@ type Like {
|
||||
post: Post! # Пост, который лайкнули
|
||||
user: User! # Пользователь, который поставил лайк
|
||||
createdAt: String! # Дата создания
|
||||
isRead: Boolean! # Прочитано ли уведомление
|
||||
notifiedAt: String # Когда было отправлено уведомление
|
||||
}
|
||||
|
||||
type Tokens {
|
||||
@ -170,7 +177,23 @@ type MarkNotificationReadResult {
|
||||
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 {
|
||||
id: Int!
|
||||
@ -246,6 +269,12 @@ type Query {
|
||||
clipComments(clipId: Int!, limit: Int, offset: Int): [ClipComment!]! # Комментарии клипа
|
||||
clipLikes(clipId: Int!, limit: Int, offset: Int): [ClipLike!]! # Лайки клипа
|
||||
isLiked(clipId: Int!): Boolean! # Проверить лайк текущего пользователя
|
||||
|
||||
getLikeNotifications(
|
||||
unreadOnly: Boolean = false
|
||||
limit: Int = 20
|
||||
offset: Int = 0
|
||||
): LikeNotificationsResponse!
|
||||
}
|
||||
|
||||
|
||||
@ -294,6 +323,8 @@ type Mutation {
|
||||
unlikeClip(clipId: Int!): Boolean! # Убрать лайк
|
||||
createClipComment(clipId: Int!, content: String!): ClipComment! # Создать комментарий
|
||||
deleteClipComment(commentId: Int!): Boolean! # Удалить комментарий
|
||||
markLikeNotificationAsRead(notificationId: Int!): MarkLikeNotificationReadResult!
|
||||
markAllLikeNotificationsAsRead: MarkLikeNotificationReadResult!
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
|
||||
@ -256,3 +256,71 @@ func (r *userResolver) SubscriptionNotifications(ctx context.Context, obj *domai
|
||||
UnreadCount: int(res.UnreadCount),
|
||||
}, 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
|
||||
}
|
||||
|
||||
@ -19,22 +19,18 @@ type LikeRepository interface {
|
||||
GetByUserAndPost(ctx context.Context, userID, postID int) (*domain.Like, error)
|
||||
Delete(ctx context.Context, id 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 {
|
||||
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 {
|
||||
return &likeRepository{db: db}
|
||||
}
|
||||
@ -49,8 +45,8 @@ func (r *likeRepository) Create(ctx context.Context, like *domain.Like) error {
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO likes (post_id, user_id, created_at)
|
||||
VALUES ($1, $2, $3)
|
||||
INSERT INTO likes (post_id, user_id, created_at, is_read, notified_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
@ -58,14 +54,43 @@ func (r *likeRepository) Create(ctx context.Context, like *domain.Like) error {
|
||||
like.PostID,
|
||||
like.UserID,
|
||||
like.CreatedAt,
|
||||
like.IsRead,
|
||||
like.NotifiedAt,
|
||||
).Scan(&like.ID)
|
||||
|
||||
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) {
|
||||
query := `
|
||||
SELECT id, post_id, user_id, created_at
|
||||
SELECT id, post_id, user_id, created_at, is_read, notified_at
|
||||
FROM likes
|
||||
WHERE post_id = $1
|
||||
ORDER BY created_at DESC
|
||||
@ -85,6 +110,8 @@ func (r *likeRepository) GetByPostID(ctx context.Context, postID int) ([]*domain
|
||||
&like.PostID,
|
||||
&like.UserID,
|
||||
&like.CreatedAt,
|
||||
&like.IsRead,
|
||||
&like.NotifiedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -95,18 +122,9 @@ func (r *likeRepository) GetByPostID(ctx context.Context, postID int) ([]*domain
|
||||
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) {
|
||||
query := `
|
||||
SELECT id, post_id, user_id, created_at
|
||||
SELECT id, post_id, user_id, created_at, is_read, notified_at
|
||||
FROM likes
|
||||
WHERE user_id = $1 AND post_id = $2
|
||||
LIMIT 1
|
||||
@ -118,6 +136,8 @@ func (r *likeRepository) GetByUserAndPost(ctx context.Context, userID, postID in
|
||||
&like.PostID,
|
||||
&like.UserID,
|
||||
&like.CreatedAt,
|
||||
&like.IsRead,
|
||||
&like.NotifiedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
@ -129,3 +149,166 @@ func (r *likeRepository) GetByUserAndPost(ctx context.Context, userID, postID in
|
||||
|
||||
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(
|
||||
¬ification.ID,
|
||||
¬ification.PostID,
|
||||
¬ification.LikerID,
|
||||
¬ification.CreatedAt,
|
||||
¬ification.IsRead,
|
||||
¬ification.LikerUsername,
|
||||
¬ification.LikerAvatar,
|
||||
¬ification.PostTitle,
|
||||
¬ification.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(
|
||||
¬ification.ID,
|
||||
¬ification.PostID,
|
||||
¬ification.LikerID,
|
||||
¬ification.CreatedAt,
|
||||
¬ification.IsRead,
|
||||
¬ification.LikerUsername,
|
||||
¬ification.LikerAvatar,
|
||||
¬ification.PostTitle,
|
||||
¬ification.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
|
||||
}
|
||||
|
||||
@ -15,26 +15,33 @@ type LikeService interface {
|
||||
GetByPostID(ctx context.Context, postID int) ([]*domain.Like, error)
|
||||
GetCountForPost(ctx context.Context, postID int) (int, 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 {
|
||||
likeRepo repository.LikeRepository
|
||||
postRepo repository.PostRepository // Для проверки существования поста
|
||||
postRepo repository.PostRepository
|
||||
userRepo repository.UserRepository // Добавляем для получения информации о пользователях
|
||||
}
|
||||
|
||||
// Конструктор сервиса
|
||||
func NewLikeService(likeRepo repository.LikeRepository, postRepo repository.PostRepository) LikeService {
|
||||
func NewLikeService(likeRepo repository.LikeRepository, postRepo repository.PostRepository, userRepo repository.UserRepository) LikeService {
|
||||
return &likeService{
|
||||
likeRepo: likeRepo,
|
||||
postRepo: postRepo,
|
||||
userRepo: userRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// Поставить лайк посту
|
||||
func (s *likeService) LikePost(ctx context.Context, userID, postID int) (*domain.Like, error) {
|
||||
// Проверяем существование поста
|
||||
if _, err := s.postRepo.GetByID(ctx, postID); err != nil {
|
||||
post, err := s.postRepo.GetByID(ctx, postID)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrPostNotFound) {
|
||||
return nil, errors.New("post not found")
|
||||
}
|
||||
@ -48,10 +55,17 @@ func (s *likeService) LikePost(ctx context.Context, userID, postID int) (*domain
|
||||
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{
|
||||
PostID: postID,
|
||||
UserID: userID,
|
||||
CreatedAt: time.Now(),
|
||||
IsRead: false,
|
||||
NotifiedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.likeRepo.Create(ctx, like); err != nil {
|
||||
@ -96,7 +110,6 @@ func (s *likeService) GetByPostID(ctx context.Context, postID int) ([]*domain.Li
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Возвращаем пустой слайс вместо nil
|
||||
if likes == nil {
|
||||
return []*domain.Like{}, nil
|
||||
}
|
||||
@ -124,3 +137,35 @@ func (s *likeService) CheckIfLiked(ctx context.Context, userID, postID int) (boo
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
2
migrations/0003_initial_schema.up.sql
Normal file
2
migrations/0003_initial_schema.up.sql
Normal file
@ -0,0 +1,2 @@
|
||||
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();
|
||||
Loading…
x
Reference in New Issue
Block a user