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
222469e40a
commit
dcf9b4bcbf
@ -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)
|
likeService := service.NewLikeService(likeRepo, postRepo, userRepo)
|
||||||
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)
|
||||||
|
|||||||
@ -50,5 +50,6 @@ 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"
|
||||||
@ -23,6 +23,7 @@ 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"`
|
||||||
}
|
}
|
||||||
@ -43,6 +44,42 @@ type Like struct {
|
|||||||
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
@ -23,12 +23,134 @@ 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) {
|
||||||
// This would typically use a UserService to fetch the user
|
user, err := r.Services.User.GetByID(ctx, obj.UserID)
|
||||||
// For now, we'll return nil as the user service isn't shown in the provided code
|
if err != nil {
|
||||||
return nil, nil
|
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"tailly_back_v2/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FollowResult struct {
|
type FollowResult struct {
|
||||||
@ -38,6 +39,17 @@ 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"`
|
||||||
|
|||||||
@ -17,6 +17,11 @@ 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!
|
||||||
}
|
}
|
||||||
|
|
||||||
# Пост в блоге
|
# Пост в блоге
|
||||||
@ -49,6 +54,8 @@ type Like {
|
|||||||
post: Post! # Пост, который лайкнули
|
post: Post! # Пост, который лайкнули
|
||||||
user: User! # Пользователь, который поставил лайк
|
user: User! # Пользователь, который поставил лайк
|
||||||
createdAt: String! # Дата создания
|
createdAt: String! # Дата создания
|
||||||
|
isRead: Boolean! # Прочитано ли уведомление
|
||||||
|
notifiedAt: String # Когда было отправлено уведомление
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tokens {
|
type Tokens {
|
||||||
@ -170,7 +177,23 @@ 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!
|
||||||
@ -246,6 +269,12 @@ 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!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -294,6 +323,8 @@ 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 {
|
||||||
|
|||||||
@ -256,3 +256,71 @@ 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
|
||||||
|
}
|
||||||
|
|||||||
@ -19,22 +19,18 @@ 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}
|
||||||
}
|
}
|
||||||
@ -49,8 +45,8 @@ func (r *likeRepository) Create(ctx context.Context, like *domain.Like) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
INSERT INTO likes (post_id, user_id, created_at)
|
INSERT INTO likes (post_id, user_id, created_at, is_read, notified_at)
|
||||||
VALUES ($1, $2, $3)
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`
|
`
|
||||||
|
|
||||||
@ -58,14 +54,43 @@ 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
|
SELECT id, post_id, user_id, created_at, is_read, notified_at
|
||||||
FROM likes
|
FROM likes
|
||||||
WHERE post_id = $1
|
WHERE post_id = $1
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
@ -85,6 +110,8 @@ 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
|
||||||
@ -95,18 +122,9 @@ 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
|
SELECT id, post_id, user_id, created_at, is_read, notified_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
|
||||||
@ -118,6 +136,8 @@ 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 {
|
||||||
@ -129,3 +149,166 @@ 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(
|
||||||
|
¬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)
|
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) LikeService {
|
func NewLikeService(likeRepo repository.LikeRepository, postRepo repository.PostRepository, userRepo repository.UserRepository) 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) {
|
||||||
// Проверяем существование поста
|
// Проверяем существование поста
|
||||||
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) {
|
if errors.Is(err, repository.ErrPostNotFound) {
|
||||||
return nil, errors.New("post not found")
|
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")
|
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 {
|
||||||
@ -96,7 +110,6 @@ 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
|
||||||
}
|
}
|
||||||
@ -124,3 +137,35 @@ 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)
|
||||||
|
}
|
||||||
|
|||||||
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